From 1fbf19364e59b50ac06d4fcbfa066e9cf33be71f Mon Sep 17 00:00:00 2001
From: Funkmich008 <87016586+Funkmich008@users.noreply.github.com>
Date: Sun, 17 Sep 2023 13:01:31 +0200
Subject: [PATCH 01/46] test update
---
src/sas/qtgui/Utilities/MuMagTool/MuMag.py | 35 +++++
src/sas/qtgui/Utilities/MuMagTool/MuMagLib.py | 137 ++++++++++++++++++
.../qtgui/Utilities/MuMagTool/UI/MuMagUI.ui | 58 ++++++++
3 files changed, 230 insertions(+)
create mode 100644 src/sas/qtgui/Utilities/MuMagTool/MuMag.py
create mode 100644 src/sas/qtgui/Utilities/MuMagTool/MuMagLib.py
create mode 100644 src/sas/qtgui/Utilities/MuMagTool/UI/MuMagUI.ui
diff --git a/src/sas/qtgui/Utilities/MuMagTool/MuMag.py b/src/sas/qtgui/Utilities/MuMagTool/MuMag.py
new file mode 100644
index 0000000000..0dd2f1df24
--- /dev/null
+++ b/src/sas/qtgui/Utilities/MuMagTool/MuMag.py
@@ -0,0 +1,35 @@
+from sas.qtgui.Utilities.MuMagTool.UI.MuMagUI import Ui_MainWindow
+from PySide6.QtWidgets import QWidget
+from PySide6 import QtWidgets
+import MuMagLib
+
+
+def on_press_2():
+ print("HELLO")
+
+class MuMag(QtWidgets.QMainWindow, Ui_MainWindow):
+ def __init__(self, parent=None):
+ super().__init__()
+
+ self.setupUi(self)
+ self.pushButton.clicked.connect(self.import_data_button_callback)
+ self.pushButton_2.clicked.connect(self.plot_experimental_data_button_callback)
+ self.pushButton.clicked.connect(on_press_2)
+
+ def import_data_button_callback(self):
+ MuMagLib.import_data_button_callback_sub()
+
+ def plot_experimental_data_button_callback(self):
+ MuMagLib.plot_experimental_data()
+
+
+def main():
+ """ Show a demo of the slider """
+ app = QtWidgets.QApplication([])
+ form = MuMag()
+ form.show()
+ app.exec_()
+
+
+if __name__ == "__main__":
+ main()
\ No newline at end of file
diff --git a/src/sas/qtgui/Utilities/MuMagTool/MuMagLib.py b/src/sas/qtgui/Utilities/MuMagTool/MuMagLib.py
new file mode 100644
index 0000000000..e299a5473a
--- /dev/null
+++ b/src/sas/qtgui/Utilities/MuMagTool/MuMagLib.py
@@ -0,0 +1,137 @@
+import numpy as np
+import math
+import matplotlib.pyplot as plt
+import matplotlib.pylab as pl
+import scipy.optimize as scopt
+from tkinter import *
+from tkinter import filedialog
+from tkinter import messagebox
+from tkinter import ttk
+import os
+import os.path
+from datetime import datetime
+from PyQt6.QtWidgets import QFileDialog
+import string
+
+#####################################################################################################################
+def get_directory():
+ fname = QFileDialog.getOpenFileName()
+ if fname:
+ fname = fname[0]
+ index = [i for i, val in enumerate(fname) if val == "/"]
+ fname = fname[0:index[-1]+1]
+ return fname
+
+#####################################################################################################################
+# Import experimental data and get information from filenames
+def import_data_button_callback_sub():
+
+ global q_exp
+ global I_exp
+ global sigma_exp
+ global B_0_exp
+ global Ms_exp
+ global Hdem_exp
+ global DataCounter
+
+ #print('here')
+ DataCounter = 0
+ # Predefine array's
+ #DIR = filedialog.askdirectory()
+ DIR = get_directory()
+ #print('here')
+ for name in os.listdir(DIR):
+ if name.find(".csv") != -1:
+ data = np.genfromtxt(DIR + '/' + name)
+ Lq = len(data[:, 0])
+ if DataCounter == 0:
+ q_exp = np.array([np.zeros(Lq)])
+ I_exp = np.array([np.zeros(Lq)])
+ sigma_exp = np.array([np.zeros(Lq)])
+ B_0_exp = np.array([np.zeros(1)])
+ Ms_exp = np.array([np.zeros(1)])
+ Hdem_exp = np.array([np.zeros(1)])
+ DataCounter = DataCounter + 1
+ else:
+ q_exp = np.append(q_exp, [np.zeros(Lq)], axis=0)
+ I_exp = np.append(I_exp, [np.zeros(Lq)], axis=0)
+ sigma_exp = np.append(sigma_exp, [np.zeros(Lq)], axis=0)
+ B_0_exp = np.append(B_0_exp, [np.zeros(1)])
+ Ms_exp = np.append(Ms_exp, [np.zeros(1)])
+ Hdem_exp = np.append(Hdem_exp, [np.zeros(1)])
+ DataCounter = DataCounter + 1
+
+ #print('here')
+ # Load the data and sort the data
+ for name in os.listdir(DIR):
+ if name.find(".csv") != -1:
+
+ str_name = name[0:len(name)-4]
+ str_name = str_name.split('_')
+ idx = int(str_name[0])
+ B0 = float(str_name[1])
+ Ms = float(str_name[2])
+ Hdem = float(str_name[3])
+
+ data = np.genfromtxt(DIR + '/' + name)
+
+ B_0_exp[idx-1] = B0
+ Ms_exp[idx-1] = Ms
+ Hdem_exp[idx-1] = Hdem
+ q_exp[idx-1, :] = data[:, 0].T * 1e9
+ I_exp[idx-1, :] = data[:, 1].T
+ sigma_exp[idx-1, :] = data[:, 2].T
+
+
+################################################################################################################
+# Plot Experimental Data: Generate Figure
+def plot_exp_data(q, I_exp, B_0, x_min, x_max, y_min, y_max):
+
+ fig = plt.figure()
+ fig.tight_layout()
+ ax = fig.add_subplot(1, 1, 1)
+
+ colors = pl.cm.jet(np.linspace(0, 1, len(B_0)))
+ for k in np.arange(0, len(B_0)):
+ plt.plot(q[k, :], I_exp[k, :], linestyle='-', color=colors[k], linewidth=0.5, label='B_0 = ' + str(B_0[k]) + ' T')
+ plt.plot(q[k, :], I_exp[k, :], '.', color=colors[k], linewidth=0.3, markersize=1)
+
+ box = ax.get_position()
+ ax.set_position([box.x0, box.y0, box.width * 0.8, box.height])
+
+ plt.xlabel('q [1/nm]')
+ plt.ylabel('I_exp')
+ plt.xlim(x_min, x_max)
+ plt.ylim(y_min, y_max)
+ plt.yscale('log')
+ plt.xscale('log')
+ plt.grid(True, which="both", linestyle='--')
+ plt.legend(loc='center left', bbox_to_anchor=(1, 0.5))
+ plt.show()
+
+
+
+#####################################################################################################################
+# Plot Experimental Data: Set Bounds and Call Plotting Function
+def plot_experimental_data():
+ global q_exp
+ global I_exp
+ global sigma_exp
+ global B_0_exp
+
+ if np.size(q_exp) > 1:
+ q_exp_min = np.amin(q_exp)*1e-9
+ q_exp_min = 10**(np.floor(np.log10(q_exp_min))) * np.floor(q_exp_min/10**(np.floor(np.log10(q_exp_min))))
+
+ q_exp_max = np.amax(q_exp)*1e-9
+ q_exp_max = 10**(np.floor(np.log10(q_exp_max))) * np.ceil(q_exp_max/10**(np.floor(np.log10(q_exp_max))))
+
+ I_exp_min = np.amin(I_exp)
+ I_exp_min = 10**(np.floor(np.log10(I_exp_min))) * np.floor(I_exp_min/10**(np.floor(np.log10(I_exp_min))))
+
+ I_exp_max = np.amax(I_exp)
+ I_exp_max = 10 ** (np.floor(np.log10(I_exp_max))) * np.ceil(I_exp_max / 10 ** (np.floor(np.log10(I_exp_max))))
+
+ plot_exp_data(q_exp*1e-9, I_exp, B_0_exp*1e-3, q_exp_min, q_exp_max, I_exp_min, I_exp_max)
+ else:
+ messagebox.showerror(title="Error!", message="No experimental data available! Please import experimental data!")
\ No newline at end of file
diff --git a/src/sas/qtgui/Utilities/MuMagTool/UI/MuMagUI.ui b/src/sas/qtgui/Utilities/MuMagTool/UI/MuMagUI.ui
new file mode 100644
index 0000000000..54c33a64cd
--- /dev/null
+++ b/src/sas/qtgui/Utilities/MuMagTool/UI/MuMagUI.ui
@@ -0,0 +1,58 @@
+
+
+ MainWindow
+
+
+
+ 0
+ 0
+ 800
+ 600
+
+
+
+ MainWindow
+
+
+
+
+
+ 20
+ 20
+ 113
+ 32
+
+
+
+ Import Data
+
+
+
+
+
+ 140
+ 20
+ 113
+ 32
+
+
+
+ Plot Data
+
+
+
+
+
+
+
+
+
From b2ea0642894c13b3c507a5f380545cda40fc3d77 Mon Sep 17 00:00:00 2001
From: Funkmich008 <87016586+Funkmich008@users.noreply.github.com>
Date: Sun, 17 Sep 2023 14:50:08 +0200
Subject: [PATCH 02/46] connected MuMag Fitter to the main sasview gui in
MainWindow & GuiManager
---
src/sas/qtgui/MainWindow/GuiManager.py | 8 ++++++++
src/sas/qtgui/MainWindow/UI/MainWindowUI.ui | 8 +++++++-
src/sas/qtgui/Utilities/MuMagTool/MuMag.py | 2 +-
src/sas/qtgui/Utilities/MuMagTool/MuMagLib.py | 2 +-
4 files changed, 17 insertions(+), 3 deletions(-)
diff --git a/src/sas/qtgui/MainWindow/GuiManager.py b/src/sas/qtgui/MainWindow/GuiManager.py
index 5e13147d41..05ca07f834 100644
--- a/src/sas/qtgui/MainWindow/GuiManager.py
+++ b/src/sas/qtgui/MainWindow/GuiManager.py
@@ -33,6 +33,7 @@
from sas.qtgui.Utilities.ResultPanel import ResultPanel
from sas.qtgui.Utilities.OrientationViewer.OrientationViewer import show_orientation_viewer
from sas.qtgui.Utilities.HidableDialog import hidable_dialog
+from sas.qtgui.Utilities.MuMagTool.MuMag import MuMag
from sas.qtgui.Utilities.Reports.ReportDialog import ReportDialog
from sas.qtgui.Utilities.Preferences.PreferencesPanel import PreferencesPanel
@@ -191,6 +192,7 @@ def addWidgets(self):
self.SLDCalculator = SldPanel(self)
self.DVCalculator = DensityPanel(self)
self.KIESSIGCalculator = KiessigPanel(self)
+ self.MuMag_Fitter = MuMag(self)
self.SlitSizeCalculator = SlitSizeCalculator(self)
self.GENSASCalculator = GenericScatteringCalculator(self)
self.ResolutionCalculator = ResolutionCalculatorPanel(self)
@@ -688,6 +690,7 @@ def addTriggers(self):
self._workspace.actionSLD_Calculator.triggered.connect(self.actionSLD_Calculator)
self._workspace.actionDensity_Volume_Calculator.triggered.connect(self.actionDensity_Volume_Calculator)
self._workspace.actionKeissig_Calculator.triggered.connect(self.actionKiessig_Calculator)
+ self._workspace.actionMuMag_Fitter.triggered.connect(self.actionMuMag_Fitter)
#self._workspace.actionKIESSING_Calculator.triggered.connect(self.actionKIESSING_Calculator)
self._workspace.actionSlit_Size_Calculator.triggered.connect(self.actionSlit_Size_Calculator)
self._workspace.actionSAS_Resolution_Estimator.triggered.connect(self.actionSAS_Resolution_Estimator)
@@ -979,6 +982,11 @@ def actionKiessig_Calculator(self):
"""
self.KIESSIGCalculator.show()
+ def actionMuMag_Fitter(self):
+ """
+ """
+ self.MuMag_Fitter.show()
+
def actionSlit_Size_Calculator(self):
"""
"""
diff --git a/src/sas/qtgui/MainWindow/UI/MainWindowUI.ui b/src/sas/qtgui/MainWindow/UI/MainWindowUI.ui
index 44b176e8aa..d66115c3e7 100755
--- a/src/sas/qtgui/MainWindow/UI/MainWindowUI.ui
+++ b/src/sas/qtgui/MainWindow/UI/MainWindowUI.ui
@@ -24,7 +24,7 @@
0
0
915
- 20
+ 22
diff --git a/src/sas/qtgui/Utilities/MuMagTool/MuMag.py b/src/sas/qtgui/Utilities/MuMagTool/MuMag.py
index 0dd2f1df24..428d095118 100644
--- a/src/sas/qtgui/Utilities/MuMagTool/MuMag.py
+++ b/src/sas/qtgui/Utilities/MuMagTool/MuMag.py
@@ -1,7 +1,7 @@
from sas.qtgui.Utilities.MuMagTool.UI.MuMagUI import Ui_MainWindow
from PySide6.QtWidgets import QWidget
from PySide6 import QtWidgets
-import MuMagLib
+from sas.qtgui.Utilities.MuMagTool import MuMagLib
def on_press_2():
diff --git a/src/sas/qtgui/Utilities/MuMagTool/MuMagLib.py b/src/sas/qtgui/Utilities/MuMagTool/MuMagLib.py
index e299a5473a..20cc209cbf 100644
--- a/src/sas/qtgui/Utilities/MuMagTool/MuMagLib.py
+++ b/src/sas/qtgui/Utilities/MuMagTool/MuMagLib.py
@@ -10,7 +10,7 @@
import os
import os.path
from datetime import datetime
-from PyQt6.QtWidgets import QFileDialog
+from PySide6.QtWidgets import QFileDialog
import string
#####################################################################################################################
From ca14f1d5d80ee87572683dc857742ae1d64ec965 Mon Sep 17 00:00:00 2001
From: Funkmich008 <87016586+Funkmich008@users.noreply.github.com>
Date: Mon, 18 Sep 2023 10:31:33 +0200
Subject: [PATCH 03/46] next update of the UI of MuMag adding buttons and text
edit fields, change of button names
---
src/sas/qtgui/Utilities/MuMagTool/MuMag.py | 8 +-
.../qtgui/Utilities/MuMagTool/UI/MuMagUI.ui | 206 +++++++++++++++++-
2 files changed, 206 insertions(+), 8 deletions(-)
diff --git a/src/sas/qtgui/Utilities/MuMagTool/MuMag.py b/src/sas/qtgui/Utilities/MuMagTool/MuMag.py
index 428d095118..fe3adba444 100644
--- a/src/sas/qtgui/Utilities/MuMagTool/MuMag.py
+++ b/src/sas/qtgui/Utilities/MuMagTool/MuMag.py
@@ -4,17 +4,13 @@
from sas.qtgui.Utilities.MuMagTool import MuMagLib
-def on_press_2():
- print("HELLO")
-
class MuMag(QtWidgets.QMainWindow, Ui_MainWindow):
def __init__(self, parent=None):
super().__init__()
self.setupUi(self)
- self.pushButton.clicked.connect(self.import_data_button_callback)
- self.pushButton_2.clicked.connect(self.plot_experimental_data_button_callback)
- self.pushButton.clicked.connect(on_press_2)
+ self.ImportDataButton.clicked.connect(self.import_data_button_callback)
+ self.PlotDataButton.clicked.connect(self.plot_experimental_data_button_callback)
def import_data_button_callback(self):
MuMagLib.import_data_button_callback_sub()
diff --git a/src/sas/qtgui/Utilities/MuMagTool/UI/MuMagUI.ui b/src/sas/qtgui/Utilities/MuMagTool/UI/MuMagUI.ui
index 54c33a64cd..4729a32303 100644
--- a/src/sas/qtgui/Utilities/MuMagTool/UI/MuMagUI.ui
+++ b/src/sas/qtgui/Utilities/MuMagTool/UI/MuMagUI.ui
@@ -14,7 +14,7 @@
MainWindow
-
+
20
@@ -27,7 +27,7 @@
Import Data
-
+
140
@@ -40,6 +40,208 @@
Plot Data
+
+
+
+ 10
+ 120
+ 131
+ 32
+
+
+
+ Simple Fit
+
+
+
+
+
+ 10
+ 160
+ 131
+ 32
+
+
+
+ Compare Results
+
+
+
+
+
+ 12
+ 200
+ 131
+ 32
+
+
+
+ Save Result
+
+
+
+
+
+ 250
+ 130
+ 104
+ 31
+
+
+
+ <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd">
+<html><head><meta name="qrichtext" content="1" /><style type="text/css">
+p, li { white-space: pre-wrap; }
+</style></head><body style=" font-family:'.AppleSystemUIFont'; font-size:13pt; font-weight:400; font-style:normal;">
+<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">0.6</p></body></html>
+
+
+
+
+
+ 390
+ 200
+ 104
+ 31
+
+
+
+ <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd">
+<html><head><meta name="qrichtext" content="1" /><style type="text/css">
+p, li { white-space: pre-wrap; }
+</style></head><body style=" font-family:'.AppleSystemUIFont'; font-size:13pt; font-weight:400; font-style:normal;">
+<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">20</p></body></html>
+
+
+
+
+
+ 250
+ 200
+ 104
+ 31
+
+
+
+ <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd">
+<html><head><meta name="qrichtext" content="1" /><style type="text/css">
+p, li { white-space: pre-wrap; }
+</style></head><body style=" font-family:'.AppleSystemUIFont'; font-size:13pt; font-weight:400; font-style:normal;">
+<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">5</p></body></html>
+
+
+
+
+
+ 530
+ 130
+ 104
+ 31
+
+
+
+ <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd">
+<html><head><meta name="qrichtext" content="1" /><style type="text/css">
+p, li { white-space: pre-wrap; }
+</style></head><body style=" font-family:'.AppleSystemUIFont'; font-size:13pt; font-weight:400; font-style:normal;">
+<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">75</p></body></html>
+
+
+
+
+
+ 530
+ 200
+ 104
+ 31
+
+
+
+ <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd">
+<html><head><meta name="qrichtext" content="1" /><style type="text/css">
+p, li { white-space: pre-wrap; }
+</style></head><body style=" font-family:'.AppleSystemUIFont'; font-size:13pt; font-weight:400; font-style:normal;">
+<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">200</p></body></html>
+
+
+
+
+
+ 170
+ 210
+ 60
+ 16
+
+
+
+ A [pJ/m]:
+
+
+
+
+
+ 280
+ 180
+ 60
+ 16
+
+
+
+ min
+
+
+
+
+
+ 420
+ 180
+ 60
+ 16
+
+
+
+ max
+
+
+
+
+
+ 550
+ 180
+ 60
+ 16
+
+
+
+ samples
+
+
+
+
+
+ 150
+ 140
+ 91
+ 16
+
+
+
+ q_max [1/nm]
+
+
+
+
+
+ 390
+ 140
+ 121
+ 16
+
+
+
+ mu_0*H_min [mT]
+
+
+
+
+
+ 270
+ 20
+ 171
+ 26
+
+
+ -
+
+ perpendicular
+
+
+ -
+
+ parallel
+
+
+
- -
+
-
-
@@ -72,8 +72,8 @@
160
0
- 541
- 131
+ 601
+ 121
@@ -84,8 +84,8 @@
10
20
- 521
- 111
+ 581
+ 101
@@ -214,21 +214,21 @@ p, li { white-space: pre-wrap; }
0
- 130
- 701
- 431
+ 120
+ 761
+ 441
- GroupBox
+ Display
- 20
- 40
- 501
- 221
+ 0
+ 20
+ 761
+ 411
@@ -239,7 +239,7 @@ p, li { white-space: pre-wrap; }
0
0
- 703
+ 761
22
From f1a5d812f5bf551804f37c4dd8d5da9912192834 Mon Sep 17 00:00:00 2001
From: Funkmich008 <87016586+Funkmich008@users.noreply.github.com>
Date: Mon, 22 Jan 2024 05:17:55 +0100
Subject: [PATCH 17/46] Update: Functionality of the compare button and save
button
---
src/sas/qtgui/Utilities/MuMagTool/MuMag.py | 51 ++++-
src/sas/qtgui/Utilities/MuMagTool/MuMagLib.py | 203 +++++++++++++++++-
2 files changed, 235 insertions(+), 19 deletions(-)
diff --git a/src/sas/qtgui/Utilities/MuMagTool/MuMag.py b/src/sas/qtgui/Utilities/MuMagTool/MuMag.py
index de8eadc1f7..a9dfaded9d 100644
--- a/src/sas/qtgui/Utilities/MuMagTool/MuMag.py
+++ b/src/sas/qtgui/Utilities/MuMagTool/MuMag.py
@@ -20,6 +20,8 @@ def __init__(self, parent=None):
self.ImportDataButton.clicked.connect(self.import_data_button_callback)
self.PlotDataButton.clicked.connect(self.plot_experimental_data_button_callback)
self.SimpleFitButton.clicked.connect(self.simple_fit_button_callback)
+ self.CompareResultsButton.clicked.connect(self.compare_data_button_callback)
+ self.SaveResultsButton.clicked.connect(self.save_data_button_callback)
# Plotting
layout = QVBoxLayout()
@@ -58,16 +60,6 @@ def plot_experimental_data_button_callback(self):
self.figure_canvas.draw()
def simple_fit_button_callback(self):
- self.axes.set_visible(False)
- self.axes1.set_visible(True)
- self.axes2.set_visible(True)
- self.axes3.set_visible(True)
- self.axes4.set_visible(True)
- self.axes.cla()
- self.axes1.cla()
- self.axes2.cla()
- self.axes3.cla()
- self.axes4.cla()
q_max = float(self.qMaxEdit.toPlainText())
H_min = float(self.HminEdit.toPlainText())
@@ -75,10 +67,49 @@ def simple_fit_button_callback(self):
A2 = float(self.AMaxEdit.toPlainText())
A_N = int(self.ASamplesEdit.toPlainText())
SANSgeometry = self.ScatteringGeometrySelect.currentText()
+
+ self.axes.cla()
+ self.axes1.cla()
+ self.axes2.cla()
+ self.axes3.cla()
+ self.axes4.cla()
+ self.axes.set_visible(False)
+ if SANSgeometry == 'perpendicular':
+ self.axes1.set_visible(True)
+ self.axes2.set_visible(True)
+ self.axes3.set_visible(True)
+ self.axes4.set_visible(True)
+ elif SANSgeometry == 'parallel':
+ self.axes1.set_visible(True)
+ self.axes2.set_visible(True)
+ self.axes3.set_visible(True)
+ self.axes4.set_visible(False)
+
self.MuMagLib_obj.simple_fit_button_callback(q_max, H_min, A1, A2, A_N, SANSgeometry,
self.fig, self.axes1, self.axes2, self.axes3, self.axes4)
self.figure_canvas.draw()
+ def compare_data_button_callback(self):
+ self.axes.cla()
+ self.axes1.cla()
+ self.axes2.cla()
+ self.axes3.cla()
+ self.axes4.cla()
+ self.axes.set_visible(True)
+ self.axes1.set_visible(False)
+ self.axes2.set_visible(False)
+ self.axes3.set_visible(False)
+ self.axes4.set_visible(False)
+
+ self.MuMagLib_obj.SimpleFit_CompareButtonCallback(self.fig, self.axes)
+
+
+
+ def save_data_button_callback(self):
+ self.MuMagLib_obj.save_button_callback()
+
+
+
def main():
""" Show a demo of the slider """
app = QtWidgets.QApplication([])
diff --git a/src/sas/qtgui/Utilities/MuMagTool/MuMagLib.py b/src/sas/qtgui/Utilities/MuMagTool/MuMagLib.py
index 1b00ede6c1..d9f62e320e 100644
--- a/src/sas/qtgui/Utilities/MuMagTool/MuMagLib.py
+++ b/src/sas/qtgui/Utilities/MuMagTool/MuMagLib.py
@@ -16,7 +16,6 @@
from sas.qtgui.Utilities.MuMagTool import MuMagParallelGeo
from sas.qtgui.Utilities.MuMagTool import MuMagPerpendicularGeo
-
class MuMagLib():
def __init__(self):
@@ -98,7 +97,6 @@ def import_data_button_callback_sub(self):
self.I_exp[idx-1, :] = data[:, 1].T
self.sigma_exp[idx-1, :] = data[:, 2].T
-
#####################################################################################################################
# Plot Experimental Data: Set Bounds and Call Plotting Function
def plot_experimental_data(self, figure, axes):
@@ -128,13 +126,9 @@ def plot_experimental_data(self, figure, axes):
def plot_exp_data(self, figure, axes, q, I_exp, B_0, x_min, x_max, y_min, y_max):
ax = axes
-
- # print(ax)
- # ax.cla()
-
colors = pl.cm.jet(np.linspace(0, 1, len(B_0)))
for k in np.arange(0, len(B_0)):
- print(k)
+ #print(k)
ax.loglog(q[k, :], I_exp[k, :], linestyle='-', color=colors[k], linewidth=0.5, label=r'$B_0 = ' + str(B_0[k]) + '$ T')
ax.loglog(q[k, :], I_exp[k, :], '.', color=colors[k], linewidth=0.3, markersize=1)
@@ -143,7 +137,6 @@ def plot_exp_data(self, figure, axes, q, I_exp, B_0, x_min, x_max, y_min, y_max)
ax.set_xlim(x_min, x_max)
ax.set_ylim(y_min, y_max)
figure.tight_layout()
- #ax.legend(loc='center left', bbox_to_anchor=(1, 0.5), usetex=True)
figure.canvas.draw()
#######################################################################################################################
@@ -211,7 +204,8 @@ def simple_fit_button_callback(self, q_max, H_min, A1, A2, A_N, SANSgeometry, fi
self.SimpleFit_A_sigma = A_Uncertainty
self.SimpleFit_SANSgeometry = "perpendicular"
- elif SANSgeometry == "parallel ":
+ elif SANSgeometry == "parallel":
+
A_1 = A1 * 1e-12
A_2 = A2 * 1e-12
A, chi_q, A_opt, chi_q_opt, I_res_opt, S_H_opt, sigma_I_res, sigma_S_H \
@@ -255,3 +249,194 @@ def simple_fit_button_callback(self, q_max, H_min, A1, A2, A_N, SANSgeometry, fi
messagebox.showerror(title="Error!",
message="No experimental Data available! Please import experimental data!")
+######################################################################################################################
+ def save_button_callback(self):
+
+ SimpleFit_q_exp = self.SimpleFit_q_exp
+ SimpleFit_I_exp = self.SimpleFit_I_exp
+ SimpleFit_sigma_exp = self.SimpleFit_sigma_exp
+ SimpleFit_B_0_exp = self.SimpleFit_B_0_exp
+ SimpleFit_Ms_exp = self.SimpleFit_Ms_exp
+ SimpleFit_Hdem_exp = self.SimpleFit_Hdem_exp
+ SimpleFit_I_fit = self.SimpleFit_I_fit
+ SimpleFit_A = self.SimpleFit_A
+ SimpleFit_chi_q = self.SimpleFit_chi_q
+ SimpleFit_S_H_fit = self.SimpleFit_S_H_fit
+ SimpleFit_S_M_fit = self.SimpleFit_S_M_fit
+ SimpleFit_I_res_fit = self.SimpleFit_I_res_fit
+ SimpleFit_A_opt = self.SimpleFit_A_opt
+ SimpleFit_chi_q_opt = self.SimpleFit_chi_q_opt
+ SimpleFit_A_sigma = self.SimpleFit_A_sigma
+ SimpleFit_SANSgeometry = self.SimpleFit_SANSgeometry
+
+ print('hello')
+ print(np.size(SimpleFit_q_exp))
+
+ if np.size(SimpleFit_q_exp) > 1:
+ if SimpleFit_SANSgeometry == 'perpendicular':
+ DIR = QFileDialog.getExistingDirectory()
+
+ now = datetime.now()
+ TimeStamp = now.strftime("%d_%m_%Y__%H_%M_%S")
+ FolderName = "SimpleFitResults_" + TimeStamp
+ path = os.path.join(DIR, FolderName)
+ os.mkdir(path)
+
+ InfoFile = open(path + "/InfoFile.txt", "w")
+ InfoFile.write("FitMagneticSANS Toolbox - SimpleFit Results Info File \n\n")
+ InfoFile.write("Timestamp: " + TimeStamp + " \n\n")
+ InfoFile.write("SANS geometry: " + SimpleFit_SANSgeometry + "\n\n")
+ InfoFile.write("Maximal Scattering Vector: q_max = " +
+ str(np.amax(SimpleFit_q_exp*1e-9)) + " 1/nm \n")
+ InfoFile.write("Minimal Applied Field: mu_0*H_min = " +
+ str(np.around(np.amin(SimpleFit_B_0_exp), 0)) + " mT \n\n")
+ InfoFile.write("Result for the exchange stiffness constant: A = (" +
+ str(np.around(SimpleFit_A_opt*1e12, 3)) + " +/- " +
+ str(np.around(SimpleFit_A_sigma*1e12, 2)) + ") pJ/m \n")
+ InfoFile.close()
+
+ FolderName2 = "SANS_Intensity_Fit"
+ path2 = os.path.join(path, FolderName2)
+ os.mkdir(path2)
+
+ for k in np.arange(0, np.size(SimpleFit_B_0_exp)):
+ np.savetxt(os.path.join(path2, str(k+1) + "_" + str(SimpleFit_B_0_exp[k]) + "_"
+ + str(SimpleFit_Ms_exp[k]) + "_" + str(SimpleFit_Hdem_exp[k]) + ".csv"),
+ np.array([SimpleFit_q_exp[k, :].T, SimpleFit_I_fit[k, :].T]).T, delimiter=" ")
+
+ FolderName3 = "SANS_Intensity_Exp"
+ path3 = os.path.join(path, FolderName3)
+ os.mkdir(path3)
+
+ for k in np.arange(0, np.size(SimpleFit_B_0_exp)):
+ np.savetxt(os.path.join(path3, str(k+1) + "_" + str(SimpleFit_B_0_exp[k]) + "_"
+ + str(SimpleFit_Ms_exp[k]) + "_" + str(SimpleFit_Hdem_exp[k]) + ".csv"),
+ np.array([SimpleFit_q_exp[k, :].T, SimpleFit_I_exp[k, :].T,
+ SimpleFit_sigma_exp[k, :]]).T, delimiter=" ")
+
+ FolderName4 = "Fit_Results"
+ path4 = os.path.join(path, FolderName4)
+ os.mkdir(path4)
+
+ np.savetxt(os.path.join(path4, "chi.csv"), np.array([SimpleFit_A, SimpleFit_chi_q]).T, delimiter=" ")
+ np.savetxt(os.path.join(path4, "S_H.csv"), np.array([SimpleFit_q_exp[0, :].T,
+ SimpleFit_S_H_fit[0, :]]).T, delimiter=" ")
+ np.savetxt(os.path.join(path4, "S_M.csv"), np.array([SimpleFit_q_exp[0, :].T,
+ SimpleFit_S_M_fit[0, :]]).T, delimiter=" ")
+ np.savetxt(os.path.join(path4, "I_res.csv"), np.array([SimpleFit_q_exp[0, :].T,
+ SimpleFit_I_res_fit[0, :]]).T, delimiter=" ")
+
+ elif SimpleFit_SANSgeometry == 'parallel':
+ DIR = QFileDialog.getExistingDirectory()
+
+ now = datetime.now()
+ TimeStamp = now.strftime("%d_%m_%Y__%H_%M_%S")
+ FolderName = "SimpleFitResults_" + TimeStamp
+ path = os.path.join(DIR, FolderName)
+ os.mkdir(path)
+
+ InfoFile = open(path + "/InfoFile.txt", "w")
+ InfoFile.write("FitMagneticSANS Toolbox - SimpleFit Results Info File \n\n")
+ InfoFile.write("Timestamp: " + TimeStamp + " \n\n")
+ InfoFile.write("SANS geometry: " + SimpleFit_SANSgeometry + "\n\n")
+ InfoFile.write("Maximal Scattering Vector: q_max = " +
+ str(np.amax(SimpleFit_q_exp * 1e-9)) + " 1/nm \n")
+ InfoFile.write("Minimal Applied Field: mu_0*H_min = " +
+ str(np.around(np.amin(SimpleFit_B_0_exp), 0)) + " mT \n\n")
+ InfoFile.write("Result for the exchange stiffness constant: A = (" +
+ str(np.around(SimpleFit_A_opt * 1e12, 3)) + " +/- " +
+ str(np.around(SimpleFit_A_sigma * 1e12, 2)) + ") pJ/m \n")
+ InfoFile.close()
+
+ FolderName2 = "SANS_Intensity_Fit"
+ path2 = os.path.join(path, FolderName2)
+ os.mkdir(path2)
+
+ for k in np.arange(0, np.size(SimpleFit_B_0_exp)):
+ np.savetxt(os.path.join(path2, str(k + 1) + "_" + str(int(SimpleFit_B_0_exp[k])) + "_" + str(
+ int(SimpleFit_Ms_exp[k])) + "_" + str(int(SimpleFit_Hdem_exp[k])) + ".csv"),
+ np.array([SimpleFit_q_exp[k, :].T, SimpleFit_I_fit[k, :].T]).T, delimiter=" ")
+
+ FolderName3 = "SANS_Intensity_Exp"
+ path3 = os.path.join(path, FolderName3)
+ os.mkdir(path3)
+
+ for k in np.arange(0, np.size(SimpleFit_B_0_exp)):
+ np.savetxt(os.path.join(path3, str(k + 1) + "_" + str(int(SimpleFit_B_0_exp[k])) + "_" + str(
+ int(SimpleFit_Ms_exp[k])) + "_" + str(int(SimpleFit_Hdem_exp[k])) + ".csv"),
+ np.array([SimpleFit_q_exp[k, :].T, SimpleFit_I_exp[k, :].T, SimpleFit_sigma_exp[k, :]]).T,
+ delimiter=" ")
+
+ FolderName4 = "Fit_Results"
+ path4 = os.path.join(path, FolderName4)
+ os.mkdir(path4)
+
+ np.savetxt(os.path.join(path4, "chi.csv"), np.array([SimpleFit_A, SimpleFit_chi_q]).T, delimiter=" ")
+ np.savetxt(os.path.join(path4, "S_H.csv"), np.array([SimpleFit_q_exp[0, :].T,
+ SimpleFit_S_H_fit[0, :]]).T, delimiter=" ")
+ np.savetxt(os.path.join(path4, "I_res.csv"), np.array([SimpleFit_q_exp[0, :].T,
+ SimpleFit_I_res_fit[0, :]]).T, delimiter=" ")
+
+ else:
+ messagebox.showerror(title="Error!", message="No SimpleFit results available!")
+
+
+#################################################################################################################
+
+ def SimpleFit_CompareButtonCallback(self, figure, axes):
+
+ SimpleFit_q_exp = self.SimpleFit_q_exp
+ SimpleFit_I_exp = self.SimpleFit_I_exp
+ SimpleFit_sigma_exp = self.SimpleFit_sigma_exp
+ SimpleFit_B_0_exp = self.SimpleFit_B_0_exp
+ SimpleFit_Ms_exp = self.SimpleFit_Ms_exp
+ SimpleFit_Hdem_exp = self.SimpleFit_Hdem_exp
+ SimpleFit_I_fit = self.SimpleFit_I_fit
+ SimpleFit_A = self.SimpleFit_A
+ SimpleFit_chi_q = self.SimpleFit_chi_q
+ SimpleFit_S_H_fit = self.SimpleFit_S_H_fit
+ SimpleFit_S_M_fit = self.SimpleFit_S_M_fit
+ SimpleFit_I_res_fit = self.SimpleFit_I_res_fit
+ SimpleFit_A_opt = self.SimpleFit_A_opt
+ SimpleFit_chi_q_opt = self.SimpleFit_chi_q_opt
+ SimpleFit_A_sigma = self.SimpleFit_A_sigma
+ SimpleFit_SANSgeometry = self.SimpleFit_SANSgeometry
+
+ if np.size(SimpleFit_q_exp) > 1:
+ q_exp_min = np.amin(SimpleFit_q_exp) * 1e-9
+ q_exp_min = 10 ** (np.floor(np.log10(q_exp_min))) * np.floor(q_exp_min / 10 ** (np.floor(np.log10(q_exp_min))))
+
+ q_exp_max = np.amax(SimpleFit_q_exp) * 1e-9
+ q_exp_max = 10 ** (np.floor(np.log10(q_exp_max))) * np.ceil(q_exp_max / 10 ** (np.floor(np.log10(q_exp_max))))
+
+ I_exp_min = np.amin(SimpleFit_I_exp)
+ I_exp_min = 10 ** (np.floor(np.log10(I_exp_min))) * np.floor(I_exp_min / 10 ** (np.floor(np.log10(I_exp_min))))
+
+ I_exp_max = np.amax(SimpleFit_I_exp)
+ I_exp_max = 10 ** (np.floor(np.log10(I_exp_max))) * np.ceil(I_exp_max / 10 ** (np.floor(np.log10(I_exp_max))))
+
+ self.PlotCompareExpFitData(figure, axes, SimpleFit_q_exp * 1e-9, SimpleFit_I_exp, SimpleFit_I_fit, SimpleFit_B_0_exp * 1e-3, q_exp_min, q_exp_max, I_exp_min, I_exp_max)
+ else:
+ messagebox.showerror(title="Error!", message="No SimpleFit results available!")
+
+ def PlotCompareExpFitData(self, figure, axes, q, I_exp, I_fit, B_0, x_min, x_max, y_min, y_max):
+
+ fig = figure
+ fig.tight_layout()
+ ax = axes
+
+ colors = pl.cm.jet(np.linspace(0, 1, len(B_0)))
+ for k in np.arange(0, len(B_0)):
+ ax.loglog(q[k, :], I_exp[k, :], '.', color=colors[k], linewidth=0.3, markersize=1)
+
+ colors = pl.cm.YlGn(np.linspace(0, 1, len(B_0)))
+ for k in np.arange(0, len(B_0)):
+ ax.plot(q[k, :], I_fit[k, :], linestyle='solid', color=colors[k],
+ linewidth=0.5, label='(fit) B_0 = ' + str(B_0[k]) + ' T')
+
+ ax.set_xlabel(r'$q$ [1/nm]', usetex=True)
+ ax.set_ylabel(r'$I_{\mathrm{exp}}$', usetex=True)
+ ax.set_xlim(x_min, x_max)
+ ax.set_ylim(y_min, y_max)
+ figure.tight_layout()
+ figure.canvas.draw()
From 82857fd41fec4c63407eb30dc9d797cdfac08327 Mon Sep 17 00:00:00 2001
From: lucas-wilkins
Date: Tue, 21 May 2024 11:57:46 +0100
Subject: [PATCH 18/46] Fixed latex problem, some variable renaming
---
src/sas/qtgui/Utilities/MuMagTool/MuMag.py | 103 +++++++++---------
src/sas/qtgui/Utilities/MuMagTool/MuMagLib.py | 8 +-
.../Utilities/MuMagTool/MuMagParallelGeo.py | 15 ++-
.../MuMagTool/MuMagPerpendicularGeo.py | 18 +--
4 files changed, 73 insertions(+), 71 deletions(-)
diff --git a/src/sas/qtgui/Utilities/MuMagTool/MuMag.py b/src/sas/qtgui/Utilities/MuMagTool/MuMag.py
index a9dfaded9d..4395d8eaae 100644
--- a/src/sas/qtgui/Utilities/MuMagTool/MuMag.py
+++ b/src/sas/qtgui/Utilities/MuMagTool/MuMag.py
@@ -4,7 +4,8 @@
from sas.qtgui.Utilities.MuMagTool import MuMagLib
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
-from matplotlib.figure import Figure
+# from matplotlib.figure import Figure
+import matplotlib.pyplot as plt
import matplotlib.pylab as pl
import numpy as np
@@ -27,17 +28,17 @@ def __init__(self, parent=None):
layout = QVBoxLayout()
self.PlotDisplayPanel.setLayout(layout)
- self.fig = Figure() #Figure(figsize=(width, height), dpi=dpi)
- self.axes = self.fig.add_subplot(111)
- self.axes.set_visible(False)
- self.axes1 = self.fig.add_subplot(221)
- self.axes1.set_visible(False)
- self.axes2 = self.fig.add_subplot(222)
- self.axes2.set_visible(False)
- self.axes3 = self.fig.add_subplot(223)
- self.axes3.set_visible(False)
- self.axes4 = self.fig.add_subplot(224)
- self.axes4.set_visible(False)
+ self.fig = plt.figure() #Figure(figsize=(width, height), dpi=dpi)
+ self.simple_fit_axes = self.fig.add_subplot(111)
+ self.simple_fit_axes.set_visible(False)
+ self.chi_squared_axes = self.fig.add_subplot(221)
+ self.chi_squared_axes.set_visible(False)
+ self.residuals_axes = self.fig.add_subplot(222)
+ self.residuals_axes.set_visible(False)
+ self.s_h_axes = self.fig.add_subplot(223)
+ self.s_h_axes.set_visible(False)
+ self.longitudinal_scattering_axes = self.fig.add_subplot(224)
+ self.longitudinal_scattering_axes.set_visible(False)
self.figure_canvas = FigureCanvas(self.fig)
layout.addWidget(self.figure_canvas)
@@ -46,17 +47,19 @@ def import_data_button_callback(self):
self.MuMagLib_obj.import_data_button_callback_sub()
def plot_experimental_data_button_callback(self):
- self.axes.set_visible(True)
- self.axes1.set_visible(False)
- self.axes2.set_visible(False)
- self.axes3.set_visible(False)
- self.axes4.set_visible(False)
- self.axes.cla()
- self.axes1.cla()
- self.axes2.cla()
- self.axes3.cla()
- self.axes4.cla()
- self.MuMagLib_obj.plot_experimental_data(self.fig, self.axes)
+ self.simple_fit_axes.set_visible(True)
+ self.chi_squared_axes.set_visible(False)
+ self.residuals_axes.set_visible(False)
+ self.s_h_axes.set_visible(False)
+ self.longitudinal_scattering_axes.set_visible(False)
+
+ self.simple_fit_axes.cla()
+ self.chi_squared_axes.cla()
+ self.residuals_axes.cla()
+ self.s_h_axes.cla()
+ self.longitudinal_scattering_axes.cla()
+
+ self.MuMagLib_obj.plot_experimental_data(self.fig, self.simple_fit_axes)
self.figure_canvas.draw()
def simple_fit_button_callback(self):
@@ -68,40 +71,40 @@ def simple_fit_button_callback(self):
A_N = int(self.ASamplesEdit.toPlainText())
SANSgeometry = self.ScatteringGeometrySelect.currentText()
- self.axes.cla()
- self.axes1.cla()
- self.axes2.cla()
- self.axes3.cla()
- self.axes4.cla()
- self.axes.set_visible(False)
+ self.simple_fit_axes.cla()
+ self.chi_squared_axes.cla()
+ self.residuals_axes.cla()
+ self.s_h_axes.cla()
+ self.longitudinal_scattering_axes.cla()
+ self.simple_fit_axes.set_visible(False)
if SANSgeometry == 'perpendicular':
- self.axes1.set_visible(True)
- self.axes2.set_visible(True)
- self.axes3.set_visible(True)
- self.axes4.set_visible(True)
+ self.chi_squared_axes.set_visible(True)
+ self.residuals_axes.set_visible(True)
+ self.s_h_axes.set_visible(True)
+ self.longitudinal_scattering_axes.set_visible(True)
elif SANSgeometry == 'parallel':
- self.axes1.set_visible(True)
- self.axes2.set_visible(True)
- self.axes3.set_visible(True)
- self.axes4.set_visible(False)
+ self.chi_squared_axes.set_visible(True)
+ self.residuals_axes.set_visible(True)
+ self.s_h_axes.set_visible(True)
+ self.longitudinal_scattering_axes.set_visible(False)
self.MuMagLib_obj.simple_fit_button_callback(q_max, H_min, A1, A2, A_N, SANSgeometry,
- self.fig, self.axes1, self.axes2, self.axes3, self.axes4)
+ self.fig, self.chi_squared_axes, self.residuals_axes, self.s_h_axes, self.longitudinal_scattering_axes)
self.figure_canvas.draw()
def compare_data_button_callback(self):
- self.axes.cla()
- self.axes1.cla()
- self.axes2.cla()
- self.axes3.cla()
- self.axes4.cla()
- self.axes.set_visible(True)
- self.axes1.set_visible(False)
- self.axes2.set_visible(False)
- self.axes3.set_visible(False)
- self.axes4.set_visible(False)
-
- self.MuMagLib_obj.SimpleFit_CompareButtonCallback(self.fig, self.axes)
+ self.simple_fit_axes.cla()
+ self.chi_squared_axes.cla()
+ self.residuals_axes.cla()
+ self.s_h_axes.cla()
+ self.longitudinal_scattering_axes.cla()
+ self.simple_fit_axes.set_visible(True)
+ self.chi_squared_axes.set_visible(False)
+ self.residuals_axes.set_visible(False)
+ self.s_h_axes.set_visible(False)
+ self.longitudinal_scattering_axes.set_visible(False)
+
+ self.MuMagLib_obj.SimpleFit_CompareButtonCallback(self.fig, self.simple_fit_axes)
diff --git a/src/sas/qtgui/Utilities/MuMagTool/MuMagLib.py b/src/sas/qtgui/Utilities/MuMagTool/MuMagLib.py
index d9f62e320e..c8bd7f0ecd 100644
--- a/src/sas/qtgui/Utilities/MuMagTool/MuMagLib.py
+++ b/src/sas/qtgui/Utilities/MuMagTool/MuMagLib.py
@@ -132,8 +132,8 @@ def plot_exp_data(self, figure, axes, q, I_exp, B_0, x_min, x_max, y_min, y_max)
ax.loglog(q[k, :], I_exp[k, :], linestyle='-', color=colors[k], linewidth=0.5, label=r'$B_0 = ' + str(B_0[k]) + '$ T')
ax.loglog(q[k, :], I_exp[k, :], '.', color=colors[k], linewidth=0.3, markersize=1)
- ax.set_xlabel(r'$q$ [1/nm]', usetex=True)
- ax.set_ylabel(r'$I_{\mathrm{exp}}$', usetex=True)
+ ax.set_xlabel(r'$q$ [1/nm]')
+ ax.set_ylabel(r'$I_{\mathrm{exp}}$')
ax.set_xlim(x_min, x_max)
ax.set_ylim(y_min, y_max)
figure.tight_layout()
@@ -434,8 +434,8 @@ def PlotCompareExpFitData(self, figure, axes, q, I_exp, I_fit, B_0, x_min, x_max
ax.plot(q[k, :], I_fit[k, :], linestyle='solid', color=colors[k],
linewidth=0.5, label='(fit) B_0 = ' + str(B_0[k]) + ' T')
- ax.set_xlabel(r'$q$ [1/nm]', usetex=True)
- ax.set_ylabel(r'$I_{\mathrm{exp}}$', usetex=True)
+ ax.set_xlabel(r'$q$ [1/nm]')
+ ax.set_ylabel(r'$I_{\mathrm{exp}}$')
ax.set_xlim(x_min, x_max)
ax.set_ylim(y_min, y_max)
figure.tight_layout()
diff --git a/src/sas/qtgui/Utilities/MuMagTool/MuMagParallelGeo.py b/src/sas/qtgui/Utilities/MuMagTool/MuMagParallelGeo.py
index 1302353fdd..fb69206e4b 100644
--- a/src/sas/qtgui/Utilities/MuMagTool/MuMagParallelGeo.py
+++ b/src/sas/qtgui/Utilities/MuMagTool/MuMagParallelGeo.py
@@ -190,27 +190,26 @@ def PlotFittingResultsPAR_SimpleFit(q, A, chi_q, A_opt, chi_q_opt, I_res_opt, S_
A_uncertainty_str = str(A_Uncertainty)
A_opt_str = str(A_opt * 1e12)
- axes1.set_title('$A_{\mathrm{opt}} = (' + A_opt_str[0:5] + ' \pm ' + A_uncertainty_str[0:4] + ')$ pJ/m',
- usetex=True)
+ axes1.set_title('$A_{\mathrm{opt}} = (' + A_opt_str[0:5] + ' \pm ' + A_uncertainty_str[0:4] + ')$ pJ/m')
axes1.plot(A * 1e12, chi_q)
axes1.plot(A_opt * 1e12, chi_q_opt, 'o')
axes1.set_xlim([min(A * 1e12), max(A * 1e12)])
- axes1.set_xlabel('$A$ [pJ/m]', usetex=True)
- axes1.set_ylabel('$\chi^2$', usetex=True)
+ axes1.set_xlabel('$A$ [pJ/m]')
+ axes1.set_ylabel('$\chi^2$')
axes2.plot(q[0, :] * 1e-9, I_res_opt[0, :], label='fit')
axes2.set_yscale('log')
axes2.set_xscale('log')
axes2.set_xlim([min(q[0, :] * 1e-9), max(q[0, :] * 1e-9)])
- axes2.set_xlabel('$q$ [1/nm]', usetex=True)
- axes2.set_ylabel('$I_{\mathrm{res}}$', usetex=True)
+ axes2.set_xlabel('$q$ [1/nm]')
+ axes2.set_ylabel('$I_{\mathrm{res}}$')
axes3.plot(q[0, :] * 1e-9, S_H_opt[0, :], label='fit')
axes3.set_yscale('log')
axes3.set_xscale('log')
axes3.set_xlim([min(q[0, :] * 1e-9), max(q[0, :] * 1e-9)])
- axes3.set_xlabel('$q$ [1/nm]', usetex=True)
- axes3.set_ylabel('$S_H$', usetex=True)
+ axes3.set_xlabel('$q$ [1/nm]')
+ axes3.set_ylabel('$S_H$')
figure.tight_layout()
diff --git a/src/sas/qtgui/Utilities/MuMagTool/MuMagPerpendicularGeo.py b/src/sas/qtgui/Utilities/MuMagTool/MuMagPerpendicularGeo.py
index 7be0aa61d0..c031b5a806 100644
--- a/src/sas/qtgui/Utilities/MuMagTool/MuMagPerpendicularGeo.py
+++ b/src/sas/qtgui/Utilities/MuMagTool/MuMagPerpendicularGeo.py
@@ -240,33 +240,33 @@ def PlotFittingResultsPERP_SimpleFit(q, A, chi_q, A_opt, chi_q_opt, I_res_opt, S
A_uncertainty_str = str(A_Uncertainty)
A_opt_str = str(A_opt * 1e12)
- axes1.set_title('$A_{\mathrm{opt}} = (' + A_opt_str[0:5] + ' \pm ' + A_uncertainty_str[0:4] + ')$ pJ/m', usetex=True)
+ axes1.set_title('$A_{\mathrm{opt}} = (' + A_opt_str[0:5] + ' \pm ' + A_uncertainty_str[0:4] + ')$ pJ/m')
axes1.plot(A * 1e12, chi_q)
axes1.plot(A_opt * 1e12, chi_q_opt, 'o')
axes1.set_xlim([min(A * 1e12), max(A * 1e12)])
- axes1.set_xlabel('$A$ [pJ/m]', usetex=True)
- axes1.set_ylabel('$\chi^2$', usetex=True)
+ axes1.set_xlabel('$A$ [pJ/m]')
+ axes1.set_ylabel('$\chi^2$')
axes2.plot(q[0, :] * 1e-9, I_res_opt[0, :], label='fit')
axes2.set_yscale('log')
axes2.set_xscale('log')
axes2.set_xlim([min(q[0, :] * 1e-9), max(q[0, :] * 1e-9)])
- axes2.set_xlabel('$q$ [1/nm]', usetex=True)
- axes2.set_ylabel('$I_{\mathrm{res}}$', usetex=True)
+ axes2.set_xlabel('$q$ [1/nm]')
+ axes2.set_ylabel('$I_{\mathrm{res}}$')
axes3.plot(q[0, :] * 1e-9, S_H_opt[0, :], label='fit')
axes3.set_yscale('log')
axes3.set_xscale('log')
axes3.set_xlim([min(q[0, :] * 1e-9), max(q[0, :] * 1e-9)])
- axes3.set_xlabel('$q$ [1/nm]', usetex=True)
- axes3.set_ylabel('$S_H$', usetex=True)
+ axes3.set_xlabel('$q$ [1/nm]')
+ axes3.set_ylabel('$S_H$')
axes4.plot(q[0, :] * 1e-9, S_M_opt[0, :], label='fit')
axes4.set_yscale('log')
axes4.set_xscale('log')
axes4.set_xlim([min(q[0, :] * 1e-9), max(q[0, :] * 1e-9)])
- axes4.set_xlabel('$q$ [1/nm]', usetex=True)
- axes4.set_ylabel('$S_M$', usetex=True)
+ axes4.set_xlabel('$q$ [1/nm]')
+ axes4.set_ylabel('$S_M$')
figure.tight_layout()
From 820dbf4262c1fb287112d798a446e3123954f037 Mon Sep 17 00:00:00 2001
From: lucas-wilkins
Date: Tue, 21 May 2024 12:06:50 +0100
Subject: [PATCH 19/46] Cleaning up
---
src/sas/qtgui/Utilities/MuMagTool/MuMag.py | 45 +++++++++++++++-------
1 file changed, 31 insertions(+), 14 deletions(-)
diff --git a/src/sas/qtgui/Utilities/MuMagTool/MuMag.py b/src/sas/qtgui/Utilities/MuMagTool/MuMag.py
index 4395d8eaae..93efcba998 100644
--- a/src/sas/qtgui/Utilities/MuMagTool/MuMag.py
+++ b/src/sas/qtgui/Utilities/MuMagTool/MuMag.py
@@ -66,38 +66,55 @@ def simple_fit_button_callback(self):
q_max = float(self.qMaxEdit.toPlainText())
H_min = float(self.HminEdit.toPlainText())
- A1 = float(self.AMinEdit.toPlainText())
- A2 = float(self.AMaxEdit.toPlainText())
- A_N = int(self.ASamplesEdit.toPlainText())
- SANSgeometry = self.ScatteringGeometrySelect.currentText()
+ exchange_A_min = float(self.AMinEdit.toPlainText())
+ exchange_A_max = float(self.AMaxEdit.toPlainText())
+ exchange_A_n = int(self.ASamplesEdit.toPlainText())
+ experiment_geometry = self.ScatteringGeometrySelect.currentText()
+ # Clear axes
self.simple_fit_axes.cla()
self.chi_squared_axes.cla()
self.residuals_axes.cla()
self.s_h_axes.cla()
self.longitudinal_scattering_axes.cla()
+
+ # Set axes visibility
self.simple_fit_axes.set_visible(False)
- if SANSgeometry == 'perpendicular':
- self.chi_squared_axes.set_visible(True)
- self.residuals_axes.set_visible(True)
- self.s_h_axes.set_visible(True)
+ self.chi_squared_axes.set_visible(True)
+ self.residuals_axes.set_visible(True)
+ self.s_h_axes.set_visible(True)
+
+ if experiment_geometry == 'perpendicular':
self.longitudinal_scattering_axes.set_visible(True)
- elif SANSgeometry == 'parallel':
- self.chi_squared_axes.set_visible(True)
- self.residuals_axes.set_visible(True)
- self.s_h_axes.set_visible(True)
+ else:
self.longitudinal_scattering_axes.set_visible(False)
- self.MuMagLib_obj.simple_fit_button_callback(q_max, H_min, A1, A2, A_N, SANSgeometry,
- self.fig, self.chi_squared_axes, self.residuals_axes, self.s_h_axes, self.longitudinal_scattering_axes)
+
+ self.MuMagLib_obj.simple_fit_button_callback(
+ q_max,
+ H_min,
+ exchange_A_min,
+ exchange_A_max,
+ exchange_A_n,
+ experiment_geometry,
+ self.fig,
+ self.chi_squared_axes,
+ self.residuals_axes,
+ self.s_h_axes,
+ self.longitudinal_scattering_axes)
+
self.figure_canvas.draw()
def compare_data_button_callback(self):
+
+ # Clear axes
self.simple_fit_axes.cla()
self.chi_squared_axes.cla()
self.residuals_axes.cla()
self.s_h_axes.cla()
self.longitudinal_scattering_axes.cla()
+
+ # Set visibility
self.simple_fit_axes.set_visible(True)
self.chi_squared_axes.set_visible(False)
self.residuals_axes.set_visible(False)
From 09c12fedb8f0f58c37660a221aef02a367abcad1 Mon Sep 17 00:00:00 2001
From: lucas-wilkins
Date: Tue, 21 May 2024 12:29:25 +0100
Subject: [PATCH 20/46] Dataclass for loading operation
---
.../qtgui/Utilities/MuMagTool/experimental_data.py | 14 ++++++++++++++
1 file changed, 14 insertions(+)
create mode 100644 src/sas/qtgui/Utilities/MuMagTool/experimental_data.py
diff --git a/src/sas/qtgui/Utilities/MuMagTool/experimental_data.py b/src/sas/qtgui/Utilities/MuMagTool/experimental_data.py
new file mode 100644
index 0000000000..d6449474e2
--- /dev/null
+++ b/src/sas/qtgui/Utilities/MuMagTool/experimental_data.py
@@ -0,0 +1,14 @@
+from dataclasses import dataclass
+
+import numpy as np
+
+from sas.qtgui.Plotting.PlotterData import Data1D
+
+
+@dataclass
+class ExperimentalData:
+ input_data: Data1D
+
+ applied_field: float
+ saturation_magnetisation: float
+ demagnetising_field: float
From e99133d51006fe5a5ebef971686c130eea2c742d Mon Sep 17 00:00:00 2001
From: lucas-wilkins
Date: Tue, 21 May 2024 13:54:38 +0100
Subject: [PATCH 21/46] Dataclass for results
---
.../Utilities/MuMagTool/experimental_data.py | 4 ++-
.../qtgui/Utilities/MuMagTool/fit_result.py | 28 +++++++++++++++++++
2 files changed, 31 insertions(+), 1 deletion(-)
create mode 100644 src/sas/qtgui/Utilities/MuMagTool/fit_result.py
diff --git a/src/sas/qtgui/Utilities/MuMagTool/experimental_data.py b/src/sas/qtgui/Utilities/MuMagTool/experimental_data.py
index d6449474e2..3caf8efb29 100644
--- a/src/sas/qtgui/Utilities/MuMagTool/experimental_data.py
+++ b/src/sas/qtgui/Utilities/MuMagTool/experimental_data.py
@@ -7,7 +7,9 @@
@dataclass
class ExperimentalData:
- input_data: Data1D
+ """ Datapoint used as input for the MuMag tool"""
+
+ scattering_curve: Data1D
applied_field: float
saturation_magnetisation: float
diff --git a/src/sas/qtgui/Utilities/MuMagTool/fit_result.py b/src/sas/qtgui/Utilities/MuMagTool/fit_result.py
new file mode 100644
index 0000000000..e9d08e9455
--- /dev/null
+++ b/src/sas/qtgui/Utilities/MuMagTool/fit_result.py
@@ -0,0 +1,28 @@
+from dataclasses import dataclass
+
+import numpy as np
+
+from sas.qtgui.Utilities.MuMagTool.experimental_data import ExperimentalData
+
+
+@dataclass
+class FitResults:
+ """ Output the MuMag fit """
+ truncated_input_data: list[ExperimentalData]
+
+ q: np.ndarray
+ I_fit: np.ndarray
+
+ S_H: np.ndarray
+ S_M: np.ndarray
+
+ I_residual: np.ndarray # Nuclear + Magnetic cross section at complete magnetic saturation
+
+ exchange_A: np.ndarray
+ exchange_A_chi_sq: np.ndarray
+
+ optimal_A: float
+ optimal_A_chi_sq: float
+ optimal_A_stdev: float # check
+
+ geometry: str
\ No newline at end of file
From c33809d8d8916167bc10ae45cd47e2208b99fa9a Mon Sep 17 00:00:00 2001
From: lucas-wilkins
Date: Tue, 21 May 2024 15:46:41 +0100
Subject: [PATCH 22/46] Directory selection
---
src/sas/qtgui/Utilities/MuMagTool/MuMagLib.py | 39 +++++++++++++------
1 file changed, 27 insertions(+), 12 deletions(-)
diff --git a/src/sas/qtgui/Utilities/MuMagTool/MuMagLib.py b/src/sas/qtgui/Utilities/MuMagTool/MuMagLib.py
index c8bd7f0ecd..1c48d73fbb 100644
--- a/src/sas/qtgui/Utilities/MuMagTool/MuMagLib.py
+++ b/src/sas/qtgui/Utilities/MuMagTool/MuMagLib.py
@@ -10,6 +10,8 @@
import os
import os.path
from datetime import datetime
+
+from PySide6 import QtWidgets
from PySide6.QtWidgets import QFileDialog
import string
@@ -47,13 +49,15 @@ def __init__(self):
self.SimpleFit_SANSgeometry = 0
#####################################################################################################################
- def get_directory(self):
- fname = QFileDialog.getOpenFileName()
- if fname:
- fname = fname[0]
- index = [i for i, val in enumerate(fname) if val == "/"]
- fname = fname[0:index[-1]+1]
- return fname
+
+ @staticmethod
+ def directory_popup():
+ directory = QFileDialog.getExistingDirectory()
+
+ if directory.strip() == "":
+ return None
+ else:
+ return directory
#####################################################################################################################
# Import experimental data and get information from filenames
@@ -61,10 +65,14 @@ def import_data_button_callback_sub(self):
self.DataCounter = 0
# Predefine array's
- DIR = self.get_directory()
- for name in os.listdir(DIR):
+ directory = MuMagLib.directory_popup()
+
+ if directory is None:
+ return
+
+ for name in os.listdir(directory):
if name.find(".csv") != -1:
- data = np.genfromtxt(DIR + '/' + name)
+ data = np.genfromtxt(directory + '/' + name)
Lq = len(data[:, 0])
if self.DataCounter == 0:
self.q_exp = np.array([np.zeros(Lq)])
@@ -84,12 +92,12 @@ def import_data_button_callback_sub(self):
self.DataCounter = self.DataCounter + 1
# Load the data and sort the data
- for name in os.listdir(DIR):
+ for name in os.listdir(directory):
if name.find(".csv") != -1:
str_name = name[0:len(name)-4]
str_name = str_name.split('_')
idx = int(str_name[0])
- data = np.genfromtxt(DIR + '/' + name)
+ data = np.genfromtxt(directory + '/' + name)
self.B_0_exp[idx-1] = float(str_name[1])
self.Ms_exp[idx-1] = float(str_name[2])
self.Hdem_exp[idx-1] = float(str_name[3])
@@ -440,3 +448,10 @@ def PlotCompareExpFitData(self, figure, axes, q, I_exp, I_fit, B_0, x_min, x_max
ax.set_ylim(y_min, y_max)
figure.tight_layout()
figure.canvas.draw()
+
+if __name__ == "__main__":
+
+ app = QtWidgets.QApplication([])
+ # app.exec_()
+
+ MuMagLib.directory_popup()
From f5bf60c622b35019fd35407aecd81843da5586df Mon Sep 17 00:00:00 2001
From: lucas-wilkins
Date: Tue, 21 May 2024 17:10:00 +0100
Subject: [PATCH 23/46] Refactored first few methods of MuMagLib
---
src/sas/qtgui/Utilities/MuMagTool/MuMag.py | 4 +-
src/sas/qtgui/Utilities/MuMagTool/MuMagLib.py | 169 ++++++++----------
2 files changed, 79 insertions(+), 94 deletions(-)
diff --git a/src/sas/qtgui/Utilities/MuMagTool/MuMag.py b/src/sas/qtgui/Utilities/MuMagTool/MuMag.py
index 93efcba998..e2f4233616 100644
--- a/src/sas/qtgui/Utilities/MuMagTool/MuMag.py
+++ b/src/sas/qtgui/Utilities/MuMagTool/MuMag.py
@@ -44,7 +44,7 @@ def __init__(self, parent=None):
layout.addWidget(self.figure_canvas)
def import_data_button_callback(self):
- self.MuMagLib_obj.import_data_button_callback_sub()
+ self.MuMagLib_obj.import_data()
def plot_experimental_data_button_callback(self):
self.simple_fit_axes.set_visible(True)
@@ -59,7 +59,7 @@ def plot_experimental_data_button_callback(self):
self.s_h_axes.cla()
self.longitudinal_scattering_axes.cla()
- self.MuMagLib_obj.plot_experimental_data(self.fig, self.simple_fit_axes)
+ self.MuMagLib_obj.plot_exp_data(self.fig, self.simple_fit_axes)
self.figure_canvas.draw()
def simple_fit_button_callback(self):
diff --git a/src/sas/qtgui/Utilities/MuMagTool/MuMagLib.py b/src/sas/qtgui/Utilities/MuMagTool/MuMagLib.py
index 1c48d73fbb..334b26c42f 100644
--- a/src/sas/qtgui/Utilities/MuMagTool/MuMagLib.py
+++ b/src/sas/qtgui/Utilities/MuMagTool/MuMagLib.py
@@ -17,38 +17,15 @@
from sas.qtgui.Utilities.MuMagTool import MuMagParallelGeo
from sas.qtgui.Utilities.MuMagTool import MuMagPerpendicularGeo
+from sas.qtgui.Utilities.MuMagTool.experimental_data import ExperimentalData
+
+from sasdata.dataloader.loader import Loader
+
class MuMagLib():
def __init__(self):
- # attributes for the input data
- self.q_exp = 0
- self.I_exp = 0
- self.sigma_exp = 0
- self.B_0_exp = 0
- self.Ms_exp = 0
- self.Hdem_exp = 0
- self.DataCounter = 0
-
- # attributes for the simple fit result
- self.SimpleFit_q_exp = 0
- self.SimpleFit_I_exp = 0
- self.SimpleFit_sigma_exp = 0
- self.SimpleFit_B_0_exp = 0
- self.SimpleFit_Ms_exp = 0
- self.SimpleFit_Hdem_exp = 0
- self.SimpleFit_I_fit = 0
- self.SimpleFit_A = 0
- self.SimpleFit_chi_q = 0
- self.SimpleFit_S_H_fit = 0
- self.SimpleFit_S_M_fit = 0
- self.SimpleFit_I_res_fit = 0
- self.SimpleFit_A_opt = 0
- self.SimpleFit_chi_q_opt = 0
- self.SimpleFit_A_sigma = 0
- self.SimpleFit_SANSgeometry = 0
-
- #####################################################################################################################
+ self.input_data: list[ExperimentalData] | None = None
@staticmethod
def directory_popup():
@@ -59,91 +36,99 @@ def directory_popup():
else:
return directory
- #####################################################################################################################
- # Import experimental data and get information from filenames
- def import_data_button_callback_sub(self):
- self.DataCounter = 0
- # Predefine array's
+ def import_data(self):
+ """Import experimental data and get information from filenames"""
+
+ # Get the directory from the user
directory = MuMagLib.directory_popup()
if directory is None:
return
- for name in os.listdir(directory):
- if name.find(".csv") != -1:
- data = np.genfromtxt(directory + '/' + name)
- Lq = len(data[:, 0])
- if self.DataCounter == 0:
- self.q_exp = np.array([np.zeros(Lq)])
- self.I_exp = np.array([np.zeros(Lq)])
- self.sigma_exp = np.array([np.zeros(Lq)])
- self.B_0_exp = np.array([np.zeros(1)])
- self.Ms_exp = np.array([np.zeros(1)])
- self.Hdem_exp = np.array([np.zeros(1)])
- self.DataCounter = self.DataCounter + 1
- else:
- self.q_exp = np.append(self.q_exp, [np.zeros(Lq)], axis=0)
- self.I_exp = np.append(self.I_exp, [np.zeros(Lq)], axis=0)
- self.sigma_exp = np.append(self.sigma_exp, [np.zeros(Lq)], axis=0)
- self.B_0_exp = np.append(self.B_0_exp, [np.zeros(1)])
- self.Ms_exp = np.append(self.Ms_exp, [np.zeros(1)])
- self.Hdem_exp = np.append(self.Hdem_exp, [np.zeros(1)])
- self.DataCounter = self.DataCounter + 1
-
- # Load the data and sort the data
- for name in os.listdir(directory):
- if name.find(".csv") != -1:
- str_name = name[0:len(name)-4]
- str_name = str_name.split('_')
- idx = int(str_name[0])
- data = np.genfromtxt(directory + '/' + name)
- self.B_0_exp[idx-1] = float(str_name[1])
- self.Ms_exp[idx-1] = float(str_name[2])
- self.Hdem_exp[idx-1] = float(str_name[3])
- self.q_exp[idx-1, :] = data[:, 0].T * 1e9
- self.I_exp[idx-1, :] = data[:, 1].T
- self.sigma_exp[idx-1, :] = data[:, 2].T
-
- #####################################################################################################################
- # Plot Experimental Data: Set Bounds and Call Plotting Function
- def plot_experimental_data(self, figure, axes):
+ # Load the data
+ loader = Loader()
+ input_names = [name for name in os.listdir(directory) if name.lower().endswith(".csv")]
+ input_paths = [os.path.join(directory, filename) for filename in input_names]
- if np.size(self.q_exp) > 1:
- q_exp_min = np.amin(self.q_exp)*1e-9
- q_exp_min = 10**(np.floor(np.log10(q_exp_min))) * np.floor(q_exp_min/10**(np.floor(np.log10(q_exp_min))))
+ input_data = loader.load(input_paths)
- q_exp_max = np.amax(self.q_exp)*1e-9
- q_exp_max = 10**(np.floor(np.log10(q_exp_max))) * np.ceil(q_exp_max/10**(np.floor(np.log10(q_exp_max))))
+ data = []
+ for filename, data1d in zip(input_names, input_data):
- I_exp_min = np.amin(self.I_exp)
- I_exp_min = 10**(np.floor(np.log10(I_exp_min))) * np.floor(I_exp_min/10**(np.floor(np.log10(I_exp_min))))
+ # Extract the metadata from the filename
+ filename_parts = filename.split(".")
+ filename = ".".join(filename_parts[-1])
+
+ parts = filename.split("_")
+
+ applied_field = float(parts[1]) # mT
+ saturation_magnetisation = float(parts[2]) # mT
+ demagnetising_field = float(parts[3]) # mT
+
+ # Create input data object
+ data.append(ExperimentalData(
+ scattering_curve=data1d,
+ applied_field=applied_field,
+ saturation_magnetisation=saturation_magnetisation,
+ demagnetising_field=demagnetising_field))
+
+ self.input_data = sorted(data, key=lambda x: x.applied_field)
+
+ def nice_log_plot_bounds(self, data: list[np.ndarray]):
+ """ Get nice bounds for the loglog plots
+
+ :return: (lower, upper) bounds appropriate to pass to plt.xlim/ylim
+ """
+
+ upper = np.amax(np.array(data))
+ lower = np.amin(np.array(data))
+
+ return (
+ 10 ** (np.floor(np.log10(lower))) * np.floor(lower / 10 ** (np.floor(np.log10(lower)))),
+ 10 ** (np.floor(np.log10(upper))) * np.ceil(upper / 10 ** (np.floor(np.log10(upper))))
+ )
- I_exp_max = np.amax(self.I_exp)
- I_exp_max = 10 ** (np.floor(np.log10(I_exp_max))) * np.ceil(I_exp_max / 10 ** (np.floor(np.log10(I_exp_max))))
- self.plot_exp_data(figure, axes, self.q_exp*1e-9, self.I_exp, self.B_0_exp*1e-3, q_exp_min, q_exp_max, I_exp_min, I_exp_max)
- else:
- messagebox.showerror(title="Error!", message="No experimental data available! Please import experimental data!")
################################################################################################################
- # Plot Experimental Data: Generate Figure
- def plot_exp_data(self, figure, axes, q, I_exp, B_0, x_min, x_max, y_min, y_max):
+ #
+ def plot_exp_data(self, figure, axes):
+ """ Plot Experimental Data: Generate Figure """
+
+ if self.input_data is None:
+
+ messagebox.showerror(
+ title="Error!",
+ message="No experimental data available! Please import experimental data!")
+
+ return
ax = axes
- colors = pl.cm.jet(np.linspace(0, 1, len(B_0)))
- for k in np.arange(0, len(B_0)):
- #print(k)
- ax.loglog(q[k, :], I_exp[k, :], linestyle='-', color=colors[k], linewidth=0.5, label=r'$B_0 = ' + str(B_0[k]) + '$ T')
- ax.loglog(q[k, :], I_exp[k, :], '.', color=colors[k], linewidth=0.3, markersize=1)
+ colors = pl.cm.jet(np.linspace(0, 1, len(self.input_data)))
+
+ for i, datum in enumerate(self.input_data):
+
+ ax.loglog(datum.scattering_curve.x,
+ datum.scattering_curve.y,
+ linestyle='-', color=colors[i], linewidth=0.5,
+ label=r'$B_0 = ' + str(datum.applied_field) + '$ T')
+
+ ax.loglog(datum.scattering_curve.x,
+ datum.scattering_curve.y, '.',
+ color=colors[i], linewidth=0.3, markersize=1)
+
+ # Plot limits
+ qlim = self.nice_log_plot_bounds([datum.scattering_curve.q for datum in self.input_data])
+ ilim = self.nice_log_plot_bounds([datum.scattering_curve.I for datum in self.input_data])
ax.set_xlabel(r'$q$ [1/nm]')
ax.set_ylabel(r'$I_{\mathrm{exp}}$')
- ax.set_xlim(x_min, x_max)
- ax.set_ylim(y_min, y_max)
+ ax.set_xlim(qlim)
+ ax.set_ylim(ilim)
figure.tight_layout()
figure.canvas.draw()
From 88300aef4d02032e0bd712d234931df041abdc10 Mon Sep 17 00:00:00 2001
From: lucas-wilkins
Date: Tue, 21 May 2024 17:13:34 +0100
Subject: [PATCH 24/46] Bugfixes
---
src/sas/qtgui/Utilities/MuMagTool/MuMagLib.py | 7 ++++---
1 file changed, 4 insertions(+), 3 deletions(-)
diff --git a/src/sas/qtgui/Utilities/MuMagTool/MuMagLib.py b/src/sas/qtgui/Utilities/MuMagTool/MuMagLib.py
index 334b26c42f..faa8c77821 100644
--- a/src/sas/qtgui/Utilities/MuMagTool/MuMagLib.py
+++ b/src/sas/qtgui/Utilities/MuMagTool/MuMagLib.py
@@ -58,10 +58,11 @@ def import_data(self):
# Extract the metadata from the filename
filename_parts = filename.split(".")
- filename = ".".join(filename_parts[-1])
+ filename = ".".join(filename_parts[:-1])
parts = filename.split("_")
+
applied_field = float(parts[1]) # mT
saturation_magnetisation = float(parts[2]) # mT
demagnetising_field = float(parts[3]) # mT
@@ -122,8 +123,8 @@ def plot_exp_data(self, figure, axes):
color=colors[i], linewidth=0.3, markersize=1)
# Plot limits
- qlim = self.nice_log_plot_bounds([datum.scattering_curve.q for datum in self.input_data])
- ilim = self.nice_log_plot_bounds([datum.scattering_curve.I for datum in self.input_data])
+ qlim = self.nice_log_plot_bounds([datum.scattering_curve.x for datum in self.input_data])
+ ilim = self.nice_log_plot_bounds([datum.scattering_curve.y for datum in self.input_data])
ax.set_xlabel(r'$q$ [1/nm]')
ax.set_ylabel(r'$I_{\mathrm{exp}}$')
From 84e91df8dad7ec3b94adc40c1c1ab13d7881ec7c Mon Sep 17 00:00:00 2001
From: lucas-wilkins
Date: Tue, 21 May 2024 18:00:04 +0100
Subject: [PATCH 25/46] More changes
---
src/sas/qtgui/Utilities/MuMagTool/MuMag.py | 29 +--
src/sas/qtgui/Utilities/MuMagTool/MuMagLib.py | 209 +++++++++---------
.../Utilities/MuMagTool/experimental_data.py | 9 +
.../Utilities/MuMagTool/fit_parameters.py | 10 +
4 files changed, 138 insertions(+), 119 deletions(-)
create mode 100644 src/sas/qtgui/Utilities/MuMagTool/fit_parameters.py
diff --git a/src/sas/qtgui/Utilities/MuMagTool/MuMag.py b/src/sas/qtgui/Utilities/MuMagTool/MuMag.py
index e2f4233616..a26eb18b35 100644
--- a/src/sas/qtgui/Utilities/MuMagTool/MuMag.py
+++ b/src/sas/qtgui/Utilities/MuMagTool/MuMag.py
@@ -10,6 +10,9 @@
import matplotlib.pylab as pl
import numpy as np
+from sas.qtgui.Utilities.MuMagTool.fit_parameters import FitParameters
+
+
class MuMag(QtWidgets.QMainWindow, Ui_MuMagTool):
def __init__(self, parent=None):
super().__init__()
@@ -62,14 +65,17 @@ def plot_experimental_data_button_callback(self):
self.MuMagLib_obj.plot_exp_data(self.fig, self.simple_fit_axes)
self.figure_canvas.draw()
+ def fit_parameters(self) -> FitParameters:
+ return FitParameters(
+ q_max = float(self.qMaxEdit.toPlainText()),
+ min_applied_field= float(self.HminEdit.toPlainText()),
+ exchange_A_min = float(self.AMinEdit.toPlainText()),
+ exchange_A_max = float(self.AMaxEdit.toPlainText()),
+ exchange_A_n = int(self.ASamplesEdit.toPlainText()),
+ experiment_geometry = self.ScatteringGeometrySelect.currentText())
+
def simple_fit_button_callback(self):
- q_max = float(self.qMaxEdit.toPlainText())
- H_min = float(self.HminEdit.toPlainText())
- exchange_A_min = float(self.AMinEdit.toPlainText())
- exchange_A_max = float(self.AMaxEdit.toPlainText())
- exchange_A_n = int(self.ASamplesEdit.toPlainText())
- experiment_geometry = self.ScatteringGeometrySelect.currentText()
# Clear axes
self.simple_fit_axes.cla()
@@ -84,19 +90,16 @@ def simple_fit_button_callback(self):
self.residuals_axes.set_visible(True)
self.s_h_axes.set_visible(True)
- if experiment_geometry == 'perpendicular':
+ parameters = self.fit_parameters()
+
+ if parameters.experiment_geometry == 'perpendicular':
self.longitudinal_scattering_axes.set_visible(True)
else:
self.longitudinal_scattering_axes.set_visible(False)
self.MuMagLib_obj.simple_fit_button_callback(
- q_max,
- H_min,
- exchange_A_min,
- exchange_A_max,
- exchange_A_n,
- experiment_geometry,
+ parameters,
self.fig,
self.chi_squared_axes,
self.residuals_axes,
diff --git a/src/sas/qtgui/Utilities/MuMagTool/MuMagLib.py b/src/sas/qtgui/Utilities/MuMagTool/MuMagLib.py
index faa8c77821..3dec90a405 100644
--- a/src/sas/qtgui/Utilities/MuMagTool/MuMagLib.py
+++ b/src/sas/qtgui/Utilities/MuMagTool/MuMagLib.py
@@ -18,6 +18,7 @@
from sas.qtgui.Utilities.MuMagTool import MuMagParallelGeo
from sas.qtgui.Utilities.MuMagTool import MuMagPerpendicularGeo
from sas.qtgui.Utilities.MuMagTool.experimental_data import ExperimentalData
+from sas.qtgui.Utilities.MuMagTool.fit_parameters import FitParameters
from sasdata.dataloader.loader import Loader
@@ -133,116 +134,112 @@ def plot_exp_data(self, figure, axes):
figure.tight_layout()
figure.canvas.draw()
- #######################################################################################################################
-
- def simple_fit_button_callback(self, q_max, H_min, A1, A2, A_N, SANSgeometry, figure, axes1, axes2, axes3, axes4):
-
- if np.size(self.q_exp) > 1:
-
- # search index for q_max
- q_diff = (self.q_exp[0, :]*1e-9 - q_max)**2
- K_q = np.where(q_diff == np.min(q_diff))
- K_q = K_q[0][0]
-
- # search index for H_min
- H_diff = (self.B_0_exp - H_min)**2
- K_H = np.where(H_diff == np.min(H_diff))
- K_H = K_H[0][0]
-
- # apply the restrictions
- mu_0 = 4*math.pi*1e-7
- q = self.q_exp[K_H:, 0:K_q]
- I_exp_red = self.I_exp[K_H:, 0:K_q]
- sigma = self.sigma_exp[K_H:, 0:K_q]
- H_0 = np.outer(self.B_0_exp[K_H:]/mu_0 * 1e-3, np.ones(K_q))
- H_dem = np.outer(self.Hdem_exp[K_H:], np.ones(K_q))/mu_0 * 1e-3
- Ms = np.outer(self.Ms_exp[K_H:], np.ones(K_q))/mu_0 * 1e-3
-
- # Least Squares Fit in case of perpendicular SANS geometry
- if SANSgeometry == "perpendicular":
- A_1 = A1 * 1e-12
- A_2 = A2 * 1e-12
- A, chi_q, A_opt, chi_q_opt, I_res_opt, S_H_opt, S_M_opt, sigma_I_res, sigma_S_H, sigma_S_M \
- = MuMagPerpendicularGeo.SweepA_PERP(q, I_exp_red, sigma, Ms, H_0, H_dem, A_1, A_2, A_N)
-
- A_opt = MuMagPerpendicularGeo.OptimA_SPI_PERP(q, I_exp_red, sigma, Ms, H_0, H_dem, A_1, 0.0001)
- chi_q_opt, I_res_opt, S_H_opt, S_M_opt, sigma_I_res, sigma_S_H, sigma_S_M = MuMagPerpendicularGeo.LSQ_PERP(q, I_exp_red, sigma, Ms, H_0, H_dem, A_opt)
-
- I_opt = MuMagPerpendicularGeo.SANS_Model_PERP(q, S_H_opt, S_M_opt, I_res_opt, Ms, H_0, H_dem, A_opt)
-
- d2chi_dA2 = MuMagPerpendicularGeo.FDM2Ord_PERP(q, I_exp_red, sigma, Ms, H_0, H_dem, A_opt)
-
- N_mu = len(I_exp_red[0, :])
- N_nu = len(I_exp_red[:, 0])
- A_Uncertainty = np.sqrt(2 / (N_mu * N_nu * d2chi_dA2))
-
- MuMagPerpendicularGeo.PlotFittingResultsPERP_SimpleFit(q, A, chi_q, A_opt, chi_q_opt, I_res_opt,
- S_H_opt, S_M_opt, A_Uncertainty * 1e12,
- figure, axes1, axes2, axes3, axes4)
-
- # Save to global Variables
- self.SimpleFit_q_exp = q
- self.SimpleFit_I_exp = I_exp_red
- self.SimpleFit_sigma_exp = sigma
- self.SimpleFit_B_0_exp = self.B_0_exp[K_H:]
- self.SimpleFit_Ms_exp = self.Ms_exp[K_H:]
- self.SimpleFit_Hdem_exp = self.Hdem_exp[K_H:]
- self.SimpleFit_I_fit = I_opt
- self.SimpleFit_A = A
- self.SimpleFit_chi_q = chi_q
- self.SimpleFit_S_H_fit = S_H_opt
- self.SimpleFit_S_M_fit = S_M_opt
- self.SimpleFit_I_res_fit = I_res_opt
- self.SimpleFit_A_opt = A_opt
- self.SimpleFit_chi_q_opt = chi_q_opt
- self.SimpleFit_A_sigma = A_Uncertainty
- self.SimpleFit_SANSgeometry = "perpendicular"
-
- elif SANSgeometry == "parallel":
-
- A_1 = A1 * 1e-12
- A_2 = A2 * 1e-12
- A, chi_q, A_opt, chi_q_opt, I_res_opt, S_H_opt, sigma_I_res, sigma_S_H \
- = MuMagParallelGeo.SweepA_PAR(q, I_exp_red, sigma, Ms, H_0, H_dem, A_1, A_2, A_N)
-
- A_opt = MuMagParallelGeo.OptimA_SPI_PAR(q, I_exp_red, sigma, Ms, H_0, H_dem, A_1, 0.0001)
- chi_q_opt, I_res_opt, S_H_opt, sigma_I_res, sigma_S_H = MuMagParallelGeo.LSQ_PAR(q, I_exp_red, sigma,
- Ms, H_0,
- H_dem,
- A_opt)
-
- I_opt = MuMagParallelGeo.SANS_Model_PAR(q, S_H_opt, I_res_opt, Ms, H_0, H_dem, A_opt)
-
- d2chi_dA2 = MuMagParallelGeo.FDM2Ord_PAR(q, I_exp_red, sigma, Ms, H_0, H_dem, A_opt)
- N_mu = len(I_exp_red[0, :])
- N_nu = len(I_exp_red[:, 0])
- A_Uncertainty = np.sqrt(2 / (N_mu * N_nu * d2chi_dA2))
-
- MuMagParallelGeo.PlotFittingResultsPAR_SimpleFit(q, A, chi_q, A_opt, chi_q_opt,
- I_res_opt, S_H_opt, A_Uncertainty * 1e12,
- figure, axes1, axes2, axes3, axes4)
-
- # Save to global Variables
- self.SimpleFit_q_exp = q
- self.SimpleFit_I_exp = I_exp_red
- self.SimpleFit_sigma_exp = sigma
- self.SimpleFit_B_0_exp = self.B_0_exp[K_H:]
- self.SimpleFit_Ms_exp = self.Ms_exp[K_H:]
- self.SimpleFit_Hdem_exp = self.Hdem_exp[K_H:]
- self.SimpleFit_I_fit = I_opt
- self.SimpleFit_A = A
- self.SimpleFit_chi_q = chi_q
- self.SimpleFit_S_H_fit = S_H_opt
- self.SimpleFit_I_res_fit = I_res_opt
- self.SimpleFit_A_opt = A_opt
- self.SimpleFit_chi_q_opt = chi_q_opt
- self.SimpleFit_A_sigma = A_Uncertainty
- self.SimpleFit_SANSgeometry = "parallel"
- else:
+ def simple_fit_button_callback(self, parameters: FitParameters, figure, axes1, axes2, axes3, axes4):
+
+ if self.input_data is None:
messagebox.showerror(title="Error!",
message="No experimental Data available! Please import experimental data!")
+ return None
+
+ # Use an index for data upto qmax based on first data set
+ # Not ideal, would be preferable make sure the data was
+ # compatible, using something like interpolation TODO
+ square_distance_from_qmax = (self.input_data[0].scattering_curve.x - parameters.q_max) ** 2
+ max_q_index = np.argmin(square_distance_from_qmax)[0]
+
+ filtered_inputs = [datum.restrict_by_index(max_q_index)
+ for datum in self.input_data
+ if datum.applied_field > parameters.min_applied_field]
+
+
+
+ # Least Squares Fit in case of perpendicular SANS geometry
+ if parameters.experiment_geometry == "perpendicular":
+
+ A_1 = A1 * 1e-12
+ A_2 = A2 * 1e-12
+
+ A, chi_q, A_opt, chi_q_opt, I_res_opt, S_H_opt, S_M_opt, sigma_I_res, sigma_S_H, sigma_S_M \
+ = MuMagPerpendicularGeo.SweepA_PERP(q, I_exp_red, sigma, Ms, H_0, H_dem, A_1, A_2, A_N)
+
+ A_opt = MuMagPerpendicularGeo.OptimA_SPI_PERP(q, I_exp_red, sigma, Ms, H_0, H_dem, A_1, 0.0001)
+ chi_q_opt, I_res_opt, S_H_opt, S_M_opt, sigma_I_res, sigma_S_H, sigma_S_M = MuMagPerpendicularGeo.LSQ_PERP(q, I_exp_red, sigma, Ms, H_0, H_dem, A_opt)
+
+ I_opt = MuMagPerpendicularGeo.SANS_Model_PERP(q, S_H_opt, S_M_opt, I_res_opt, Ms, H_0, H_dem, A_opt)
+
+ d2chi_dA2 = MuMagPerpendicularGeo.FDM2Ord_PERP(q, I_exp_red, sigma, Ms, H_0, H_dem, A_opt)
+
+ N_mu = len(I_exp_red[0, :])
+ N_nu = len(I_exp_red[:, 0])
+ A_Uncertainty = np.sqrt(2 / (N_mu * N_nu * d2chi_dA2))
+
+ MuMagPerpendicularGeo.PlotFittingResultsPERP_SimpleFit(q, A, chi_q, A_opt, chi_q_opt, I_res_opt,
+ S_H_opt, S_M_opt, A_Uncertainty * 1e12,
+ figure, axes1, axes2, axes3, axes4)
+
+ # Save to global Variables
+ self.SimpleFit_q_exp = q
+ self.SimpleFit_I_exp = I_exp_red
+ self.SimpleFit_sigma_exp = sigma
+ self.SimpleFit_B_0_exp = self.B_0_exp[K_H:]
+ self.SimpleFit_Ms_exp = self.Ms_exp[K_H:]
+ self.SimpleFit_Hdem_exp = self.Hdem_exp[K_H:]
+ self.SimpleFit_I_fit = I_opt
+ self.SimpleFit_A = A
+ self.SimpleFit_chi_q = chi_q
+ self.SimpleFit_S_H_fit = S_H_opt
+ self.SimpleFit_S_M_fit = S_M_opt
+ self.SimpleFit_I_res_fit = I_res_opt
+ self.SimpleFit_A_opt = A_opt
+ self.SimpleFit_chi_q_opt = chi_q_opt
+ self.SimpleFit_A_sigma = A_Uncertainty
+ self.SimpleFit_SANSgeometry = "perpendicular"
+
+ else:
+
+ A_1 = A1 * 1e-12
+ A_2 = A2 * 1e-12
+ A, chi_q, A_opt, chi_q_opt, I_res_opt, S_H_opt, sigma_I_res, sigma_S_H \
+ = MuMagParallelGeo.SweepA_PAR(q, I_exp_red, sigma, Ms, H_0, H_dem, A_1, A_2, A_N)
+
+ A_opt = MuMagParallelGeo.OptimA_SPI_PAR(q, I_exp_red, sigma, Ms, H_0, H_dem, A_1, 0.0001)
+ chi_q_opt, I_res_opt, S_H_opt, sigma_I_res, sigma_S_H = MuMagParallelGeo.LSQ_PAR(q, I_exp_red, sigma,
+ Ms, H_0,
+ H_dem,
+ A_opt)
+
+ I_opt = MuMagParallelGeo.SANS_Model_PAR(q, S_H_opt, I_res_opt, Ms, H_0, H_dem, A_opt)
+
+ d2chi_dA2 = MuMagParallelGeo.FDM2Ord_PAR(q, I_exp_red, sigma, Ms, H_0, H_dem, A_opt)
+ N_mu = len(I_exp_red[0, :])
+ N_nu = len(I_exp_red[:, 0])
+ A_Uncertainty = np.sqrt(2 / (N_mu * N_nu * d2chi_dA2))
+
+ MuMagParallelGeo.PlotFittingResultsPAR_SimpleFit(q, A, chi_q, A_opt, chi_q_opt,
+ I_res_opt, S_H_opt, A_Uncertainty * 1e12,
+ figure, axes1, axes2, axes3, axes4)
+
+ # Save to global Variables
+ self.SimpleFit_q_exp = q
+ self.SimpleFit_I_exp = I_exp_red
+ self.SimpleFit_sigma_exp = sigma
+ self.SimpleFit_B_0_exp = self.B_0_exp[K_H:]
+ self.SimpleFit_Ms_exp = self.Ms_exp[K_H:]
+ self.SimpleFit_Hdem_exp = self.Hdem_exp[K_H:]
+ self.SimpleFit_I_fit = I_opt
+ self.SimpleFit_A = A
+ self.SimpleFit_chi_q = chi_q
+ self.SimpleFit_S_H_fit = S_H_opt
+ self.SimpleFit_I_res_fit = I_res_opt
+ self.SimpleFit_A_opt = A_opt
+ self.SimpleFit_chi_q_opt = chi_q_opt
+ self.SimpleFit_A_sigma = A_Uncertainty
+ self.SimpleFit_SANSgeometry = "parallel"
+
+
+
######################################################################################################################
def save_button_callback(self):
diff --git a/src/sas/qtgui/Utilities/MuMagTool/experimental_data.py b/src/sas/qtgui/Utilities/MuMagTool/experimental_data.py
index 3caf8efb29..0aee94cd93 100644
--- a/src/sas/qtgui/Utilities/MuMagTool/experimental_data.py
+++ b/src/sas/qtgui/Utilities/MuMagTool/experimental_data.py
@@ -14,3 +14,12 @@ class ExperimentalData:
applied_field: float
saturation_magnetisation: float
demagnetising_field: float
+
+ def restrict_by_index(self, max_index: int):
+ """ Remove all points from data up to given index"""
+
+ x = self.scattering_curve.x[:max_index]
+ y = self.scattering_curve.y[:max_index]
+ dy = self.scattering_curve.dy[:max_index]
+
+ return Data1D(x=x, y=y, dy=dy)
diff --git a/src/sas/qtgui/Utilities/MuMagTool/fit_parameters.py b/src/sas/qtgui/Utilities/MuMagTool/fit_parameters.py
new file mode 100644
index 0000000000..8fc671bbb9
--- /dev/null
+++ b/src/sas/qtgui/Utilities/MuMagTool/fit_parameters.py
@@ -0,0 +1,10 @@
+from dataclasses import dataclass
+
+@dataclass
+class FitParameters:
+ q_max: float
+ min_applied_field: float
+ exchange_A_min: float
+ exchange_A_max: float
+ exchange_A_n: int
+ experiment_geometry: str
\ No newline at end of file
From 8ccfd98b71f566d5fe8e6d674d9263c95cf8d20f Mon Sep 17 00:00:00 2001
From: lucas-wilkins
Date: Thu, 23 May 2024 16:48:36 +0100
Subject: [PATCH 26/46] Updates to GUI
---
src/sas/qtgui/Utilities/MuMagTool/MuMag.py | 12 +-
.../qtgui/Utilities/MuMagTool/UI/MuMagUI.ui | 479 ++++++++++--------
2 files changed, 264 insertions(+), 227 deletions(-)
diff --git a/src/sas/qtgui/Utilities/MuMagTool/MuMag.py b/src/sas/qtgui/Utilities/MuMagTool/MuMag.py
index a26eb18b35..7ab5482ce3 100644
--- a/src/sas/qtgui/Utilities/MuMagTool/MuMag.py
+++ b/src/sas/qtgui/Utilities/MuMagTool/MuMag.py
@@ -67,12 +67,12 @@ def plot_experimental_data_button_callback(self):
def fit_parameters(self) -> FitParameters:
return FitParameters(
- q_max = float(self.qMaxEdit.toPlainText()),
- min_applied_field= float(self.HminEdit.toPlainText()),
- exchange_A_min = float(self.AMinEdit.toPlainText()),
- exchange_A_max = float(self.AMaxEdit.toPlainText()),
- exchange_A_n = int(self.ASamplesEdit.toPlainText()),
- experiment_geometry = self.ScatteringGeometrySelect.currentText())
+ q_max=self.qMaxSpinBox.value(),
+ min_applied_field=self.hMinSpinBox.value(),
+ exchange_A_min=self.aMinSpinBox.value(),
+ exchange_A_max=self.aMaxSpinBox.value(),
+ exchange_A_n=self.aSamplesSpinBox.value(),
+ experiment_geometry=self.ScatteringGeometrySelect.currentText())
def simple_fit_button_callback(self):
diff --git a/src/sas/qtgui/Utilities/MuMagTool/UI/MuMagUI.ui b/src/sas/qtgui/Utilities/MuMagTool/UI/MuMagUI.ui
index 3aaa0d3826..dc852b42e6 100644
--- a/src/sas/qtgui/Utilities/MuMagTool/UI/MuMagUI.ui
+++ b/src/sas/qtgui/Utilities/MuMagTool/UI/MuMagUI.ui
@@ -6,241 +6,278 @@
0
0
- 761
- 600
+ 549
+ 481
MuMagTool
-
-
-
- 0
- 0
- 161
- 121
-
-
-
- Import Data
-
-
-
-
- 0
- 20
- 161
- 101
-
-
-
-
-
-
-
- Plot Data
-
-
-
- -
-
-
- Import Data
-
-
-
- -
-
-
-
-
- perpendicular
+
+
-
+
+
+
-
+
+
+ Import Data
-
- -
-
- parallel
+
+
-
+
+
+ Import Data
+
+
+
+ -
+
+
+ Plot Data
+
+
+
+ -
+
+
+ Simple Fit
+
+
+
+ -
+
+
+ Compare Results
+
+
+
+ -
+
+
+ Save Result
+
+
+
+
+
+
+ -
+
+
+ Simple Fit Tool
-
-
-
-
-
-
-
-
-
- 160
- 0
- 601
- 121
-
-
-
- Simple Fit Tool
-
-
-
-
- 10
- 20
- 581
- 101
-
-
-
- -
-
-
- A [pJ/m]:
-
-
-
- -
-
-
- max
-
-
-
- -
-
-
- <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd">
-<html><head><meta name="qrichtext" content="1" /><style type="text/css">
-p, li { white-space: pre-wrap; }
-</style></head><body style=" font-family:'.AppleSystemUIFont'; font-size:13pt; font-weight:400; font-style:normal;">
-<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">5</p></body></html>
-
-
-
- -
-
-
- min
-
-
-
- -
-
-
- q_max [1/nm]
-
-
-
- -
-
-
- <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd">
-<html><head><meta name="qrichtext" content="1" /><style type="text/css">
-p, li { white-space: pre-wrap; }
-</style></head><body style=" font-family:'.AppleSystemUIFont'; font-size:13pt; font-weight:400; font-style:normal;">
-<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">0.6</p></body></html>
-
-
-
- -
-
-
- Simple Fit
-
-
-
- -
-
-
- <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd">
-<html><head><meta name="qrichtext" content="1" /><style type="text/css">
-p, li { white-space: pre-wrap; }
-</style></head><body style=" font-family:'.AppleSystemUIFont'; font-size:13pt; font-weight:400; font-style:normal;">
-<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">20</p></body></html>
-
-
-
- -
-
-
- <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd">
-<html><head><meta name="qrichtext" content="1" /><style type="text/css">
-p, li { white-space: pre-wrap; }
-</style></head><body style=" font-family:'.AppleSystemUIFont'; font-size:13pt; font-weight:400; font-style:normal;">
-<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">200</p></body></html>
-
-
-
- -
-
-
- samples
-
-
-
- -
-
-
- mu_0*H_min [mT]
-
-
-
- -
-
-
- <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd">
-<html><head><meta name="qrichtext" content="1" /><style type="text/css">
-p, li { white-space: pre-wrap; }
-</style></head><body style=" font-family:'.AppleSystemUIFont'; font-size:13pt; font-weight:400; font-style:normal;">
-<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">75</p></body></html>
-
-
-
- -
-
-
- Compare Results
-
-
-
- -
-
-
- Save Result
-
-
-
-
-
-
-
-
-
- 0
- 120
- 761
- 441
-
-
-
- Display
-
-
-
-
- 0
- 20
- 761
- 411
-
-
-
-
+
+ -
+
+
+
+ Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
+
+
+ 7
+
+
-
+
+
+ Maximum q
+
+
+
+ -
+
+
+ Applied Field (μ<sub>0</sub> H<sub>min</sub>):
+
+
+
+ -
+
+
+
+ 0
+
+
-
+
+
+ 10000.000000000000000
+
+
+ 75.000000000000000
+
+
+
+ -
+
+
+ mT
+
+
+
+
+
+
+ -
+
+
+
+ 0
+
+
-
+
+
+ 0.010000000000000
+
+
+ 0.010000000000000
+
+
+ 0.600000000000000
+
+
+
+ -
+
+
+ nm<sup>-1</sup>
+
+
+
+
+
+
+ -
+
+
+ A:
+
+
+
+ -
+
+
+
+ 0
+
+
-
+
+
+ 100000.000000000000000
+
+
+ 5.000000000000000
+
+
+
+ -
+
+
+ to
+
+
+
+ -
+
+
+ 100000.000000000000000
+
+
+ 20.000000000000000
+
+
+
+ -
+
+
+ pJ/m,
+
+
+
+ -
+
+
+ 10000
+
+
+ 200
+
+
+
+ -
+
+
+ steps
+
+
+
+
+
+
+ -
+
+
+ Analysis Method:
+
+
+
+ -
+
+
+
+ 0
+
+
-
+
+
-
+
+ perpendicular
+
+
+ -
+
+ parallel
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ -
+
+
+ Display
+
+
+
+
+ 0
+ 20
+ 761
+ 411
+
+
+
+
+
+
From 9c53242f86fd9a6e6dd0a2aa4deccdcae415a581 Mon Sep 17 00:00:00 2001
From: lucas-wilkins
Date: Thu, 23 May 2024 17:11:38 +0100
Subject: [PATCH 27/46] Minor changes to GUI
---
src/sas/qtgui/Utilities/MuMagTool/UI/MuMagUI.ui | 15 ++++++++++++---
1 file changed, 12 insertions(+), 3 deletions(-)
diff --git a/src/sas/qtgui/Utilities/MuMagTool/UI/MuMagUI.ui b/src/sas/qtgui/Utilities/MuMagTool/UI/MuMagUI.ui
index dc852b42e6..c6b39bff25 100644
--- a/src/sas/qtgui/Utilities/MuMagTool/UI/MuMagUI.ui
+++ b/src/sas/qtgui/Utilities/MuMagTool/UI/MuMagUI.ui
@@ -80,7 +80,7 @@
-
- Maximum q
+ Maximum q:
@@ -99,6 +99,9 @@
-
+
+ 5
+
10000.000000000000000
@@ -125,8 +128,11 @@
-
+
+ 5
+
- 0.010000000000000
+ 0.000010000000000
0.010000000000000
@@ -149,7 +155,7 @@
-
- A:
+ Exchange Coefficient (A):
@@ -228,6 +234,9 @@
-
+
+ perpendicular
+
-
perpendicular
From f58ece2b7fdca916b8de006218144f5acea678e5 Mon Sep 17 00:00:00 2001
From: lucas-wilkins
Date: Fri, 24 May 2024 12:05:50 +0100
Subject: [PATCH 28/46] Enum for experiment geometry
---
src/sas/qtgui/Utilities/MuMagTool/MuMag.py | 34 ++--
src/sas/qtgui/Utilities/MuMagTool/MuMagLib.py | 172 +++++++++---------
.../Utilities/MuMagTool/fit_parameters.py | 10 +-
.../qtgui/Utilities/MuMagTool/fit_result.py | 4 +-
4 files changed, 118 insertions(+), 102 deletions(-)
diff --git a/src/sas/qtgui/Utilities/MuMagTool/MuMag.py b/src/sas/qtgui/Utilities/MuMagTool/MuMag.py
index 7ab5482ce3..c695c87d20 100644
--- a/src/sas/qtgui/Utilities/MuMagTool/MuMag.py
+++ b/src/sas/qtgui/Utilities/MuMagTool/MuMag.py
@@ -10,7 +10,7 @@
import matplotlib.pylab as pl
import numpy as np
-from sas.qtgui.Utilities.MuMagTool.fit_parameters import FitParameters
+from sas.qtgui.Utilities.MuMagTool.fit_parameters import FitParameters, ExperimentGeometry
class MuMag(QtWidgets.QMainWindow, Ui_MuMagTool):
@@ -32,15 +32,15 @@ def __init__(self, parent=None):
self.PlotDisplayPanel.setLayout(layout)
self.fig = plt.figure() #Figure(figsize=(width, height), dpi=dpi)
- self.simple_fit_axes = self.fig.add_subplot(111)
+ self.simple_fit_axes = self.fig.add_subplot(1, 1, 1)
self.simple_fit_axes.set_visible(False)
- self.chi_squared_axes = self.fig.add_subplot(221)
+ self.chi_squared_axes = self.fig.add_subplot(2, 2, 1)
self.chi_squared_axes.set_visible(False)
- self.residuals_axes = self.fig.add_subplot(222)
+ self.residuals_axes = self.fig.add_subplot(2, 2, 2)
self.residuals_axes.set_visible(False)
- self.s_h_axes = self.fig.add_subplot(223)
+ self.s_h_axes = self.fig.add_subplot(2, 2, 3)
self.s_h_axes.set_visible(False)
- self.longitudinal_scattering_axes = self.fig.add_subplot(224)
+ self.longitudinal_scattering_axes = self.fig.add_subplot(2, 2, 4)
self.longitudinal_scattering_axes.set_visible(False)
self.figure_canvas = FigureCanvas(self.fig)
@@ -66,13 +66,22 @@ def plot_experimental_data_button_callback(self):
self.figure_canvas.draw()
def fit_parameters(self) -> FitParameters:
+
+ match self.ScatteringGeometrySelect.currentText().lower():
+ case "parallel":
+ geometry = ExperimentGeometry.PARALLEL
+ case "perpendicular":
+ geometry = ExperimentGeometry.PERPENDICULAR
+ case _:
+ raise ValueError(f"Unknown experiment geometry: {self.ScatteringGeometrySelect.currentText()}")
+
return FitParameters(
q_max=self.qMaxSpinBox.value(),
min_applied_field=self.hMinSpinBox.value(),
exchange_A_min=self.aMinSpinBox.value(),
exchange_A_max=self.aMaxSpinBox.value(),
exchange_A_n=self.aSamplesSpinBox.value(),
- experiment_geometry=self.ScatteringGeometrySelect.currentText())
+ experiment_geometry=geometry)
def simple_fit_button_callback(self):
@@ -92,10 +101,13 @@ def simple_fit_button_callback(self):
parameters = self.fit_parameters()
- if parameters.experiment_geometry == 'perpendicular':
- self.longitudinal_scattering_axes.set_visible(True)
- else:
- self.longitudinal_scattering_axes.set_visible(False)
+ match parameters.experiment_geometry:
+ case ExperimentGeometry.PERPENDICULAR:
+ self.longitudinal_scattering_axes.set_visible(True)
+ case ExperimentGeometry.PARALLEL:
+ self.longitudinal_scattering_axes.set_visible(False)
+ case _:
+ raise ValueError(f"Unknown Value: {parameters.experiment_geometry}")
self.MuMagLib_obj.simple_fit_button_callback(
diff --git a/src/sas/qtgui/Utilities/MuMagTool/MuMagLib.py b/src/sas/qtgui/Utilities/MuMagTool/MuMagLib.py
index 3dec90a405..082cd2863d 100644
--- a/src/sas/qtgui/Utilities/MuMagTool/MuMagLib.py
+++ b/src/sas/qtgui/Utilities/MuMagTool/MuMagLib.py
@@ -18,7 +18,7 @@
from sas.qtgui.Utilities.MuMagTool import MuMagParallelGeo
from sas.qtgui.Utilities.MuMagTool import MuMagPerpendicularGeo
from sas.qtgui.Utilities.MuMagTool.experimental_data import ExperimentalData
-from sas.qtgui.Utilities.MuMagTool.fit_parameters import FitParameters
+from sas.qtgui.Utilities.MuMagTool.fit_parameters import FitParameters, ExperimentGeometry
from sasdata.dataloader.loader import Loader
@@ -91,13 +91,6 @@ def nice_log_plot_bounds(self, data: list[np.ndarray]):
10 ** (np.floor(np.log10(upper))) * np.ceil(upper / 10 ** (np.floor(np.log10(upper))))
)
-
-
-
-
-
- ################################################################################################################
- #
def plot_exp_data(self, figure, axes):
""" Plot Experimental Data: Generate Figure """
@@ -156,87 +149,88 @@ def simple_fit_button_callback(self, parameters: FitParameters, figure, axes1, a
# Least Squares Fit in case of perpendicular SANS geometry
- if parameters.experiment_geometry == "perpendicular":
-
- A_1 = A1 * 1e-12
- A_2 = A2 * 1e-12
-
- A, chi_q, A_opt, chi_q_opt, I_res_opt, S_H_opt, S_M_opt, sigma_I_res, sigma_S_H, sigma_S_M \
- = MuMagPerpendicularGeo.SweepA_PERP(q, I_exp_red, sigma, Ms, H_0, H_dem, A_1, A_2, A_N)
-
- A_opt = MuMagPerpendicularGeo.OptimA_SPI_PERP(q, I_exp_red, sigma, Ms, H_0, H_dem, A_1, 0.0001)
- chi_q_opt, I_res_opt, S_H_opt, S_M_opt, sigma_I_res, sigma_S_H, sigma_S_M = MuMagPerpendicularGeo.LSQ_PERP(q, I_exp_red, sigma, Ms, H_0, H_dem, A_opt)
-
- I_opt = MuMagPerpendicularGeo.SANS_Model_PERP(q, S_H_opt, S_M_opt, I_res_opt, Ms, H_0, H_dem, A_opt)
-
- d2chi_dA2 = MuMagPerpendicularGeo.FDM2Ord_PERP(q, I_exp_red, sigma, Ms, H_0, H_dem, A_opt)
-
- N_mu = len(I_exp_red[0, :])
- N_nu = len(I_exp_red[:, 0])
- A_Uncertainty = np.sqrt(2 / (N_mu * N_nu * d2chi_dA2))
-
- MuMagPerpendicularGeo.PlotFittingResultsPERP_SimpleFit(q, A, chi_q, A_opt, chi_q_opt, I_res_opt,
- S_H_opt, S_M_opt, A_Uncertainty * 1e12,
- figure, axes1, axes2, axes3, axes4)
-
- # Save to global Variables
- self.SimpleFit_q_exp = q
- self.SimpleFit_I_exp = I_exp_red
- self.SimpleFit_sigma_exp = sigma
- self.SimpleFit_B_0_exp = self.B_0_exp[K_H:]
- self.SimpleFit_Ms_exp = self.Ms_exp[K_H:]
- self.SimpleFit_Hdem_exp = self.Hdem_exp[K_H:]
- self.SimpleFit_I_fit = I_opt
- self.SimpleFit_A = A
- self.SimpleFit_chi_q = chi_q
- self.SimpleFit_S_H_fit = S_H_opt
- self.SimpleFit_S_M_fit = S_M_opt
- self.SimpleFit_I_res_fit = I_res_opt
- self.SimpleFit_A_opt = A_opt
- self.SimpleFit_chi_q_opt = chi_q_opt
- self.SimpleFit_A_sigma = A_Uncertainty
- self.SimpleFit_SANSgeometry = "perpendicular"
-
- else:
-
- A_1 = A1 * 1e-12
- A_2 = A2 * 1e-12
- A, chi_q, A_opt, chi_q_opt, I_res_opt, S_H_opt, sigma_I_res, sigma_S_H \
- = MuMagParallelGeo.SweepA_PAR(q, I_exp_red, sigma, Ms, H_0, H_dem, A_1, A_2, A_N)
-
- A_opt = MuMagParallelGeo.OptimA_SPI_PAR(q, I_exp_red, sigma, Ms, H_0, H_dem, A_1, 0.0001)
- chi_q_opt, I_res_opt, S_H_opt, sigma_I_res, sigma_S_H = MuMagParallelGeo.LSQ_PAR(q, I_exp_red, sigma,
- Ms, H_0,
- H_dem,
- A_opt)
-
- I_opt = MuMagParallelGeo.SANS_Model_PAR(q, S_H_opt, I_res_opt, Ms, H_0, H_dem, A_opt)
-
- d2chi_dA2 = MuMagParallelGeo.FDM2Ord_PAR(q, I_exp_red, sigma, Ms, H_0, H_dem, A_opt)
- N_mu = len(I_exp_red[0, :])
- N_nu = len(I_exp_red[:, 0])
- A_Uncertainty = np.sqrt(2 / (N_mu * N_nu * d2chi_dA2))
-
- MuMagParallelGeo.PlotFittingResultsPAR_SimpleFit(q, A, chi_q, A_opt, chi_q_opt,
- I_res_opt, S_H_opt, A_Uncertainty * 1e12,
- figure, axes1, axes2, axes3, axes4)
-
- # Save to global Variables
- self.SimpleFit_q_exp = q
- self.SimpleFit_I_exp = I_exp_red
- self.SimpleFit_sigma_exp = sigma
- self.SimpleFit_B_0_exp = self.B_0_exp[K_H:]
- self.SimpleFit_Ms_exp = self.Ms_exp[K_H:]
- self.SimpleFit_Hdem_exp = self.Hdem_exp[K_H:]
- self.SimpleFit_I_fit = I_opt
- self.SimpleFit_A = A
- self.SimpleFit_chi_q = chi_q
- self.SimpleFit_S_H_fit = S_H_opt
- self.SimpleFit_I_res_fit = I_res_opt
- self.SimpleFit_A_opt = A_opt
- self.SimpleFit_chi_q_opt = chi_q_opt
- self.SimpleFit_A_sigma = A_Uncertainty
- self.SimpleFit_SANSgeometry = "parallel"
+ match parameters.experiment_geometry:
+ case ExperimentGeometry.PERPENDICULAR:
+
+ A_1 = A1 * 1e-12
+ A_2 = A2 * 1e-12
+
+ A, chi_q, A_opt, chi_q_opt, I_res_opt, S_H_opt, S_M_opt, sigma_I_res, sigma_S_H, sigma_S_M \
+ = MuMagPerpendicularGeo.SweepA_PERP(q, I_exp_red, sigma, Ms, H_0, H_dem, A_1, A_2, A_N)
+
+ A_opt = MuMagPerpendicularGeo.OptimA_SPI_PERP(q, I_exp_red, sigma, Ms, H_0, H_dem, A_1, 0.0001)
+ chi_q_opt, I_res_opt, S_H_opt, S_M_opt, sigma_I_res, sigma_S_H, sigma_S_M = MuMagPerpendicularGeo.LSQ_PERP(q, I_exp_red, sigma, Ms, H_0, H_dem, A_opt)
+
+ I_opt = MuMagPerpendicularGeo.SANS_Model_PERP(q, S_H_opt, S_M_opt, I_res_opt, Ms, H_0, H_dem, A_opt)
+
+ d2chi_dA2 = MuMagPerpendicularGeo.FDM2Ord_PERP(q, I_exp_red, sigma, Ms, H_0, H_dem, A_opt)
+
+ N_mu = len(I_exp_red[0, :])
+ N_nu = len(I_exp_red[:, 0])
+ A_Uncertainty = np.sqrt(2 / (N_mu * N_nu * d2chi_dA2))
+
+ MuMagPerpendicularGeo.PlotFittingResultsPERP_SimpleFit(q, A, chi_q, A_opt, chi_q_opt, I_res_opt,
+ S_H_opt, S_M_opt, A_Uncertainty * 1e12,
+ figure, axes1, axes2, axes3, axes4)
+
+ # Save to global Variables
+ self.SimpleFit_q_exp = q
+ self.SimpleFit_I_exp = I_exp_red
+ self.SimpleFit_sigma_exp = sigma
+ self.SimpleFit_B_0_exp = self.B_0_exp[K_H:]
+ self.SimpleFit_Ms_exp = self.Ms_exp[K_H:]
+ self.SimpleFit_Hdem_exp = self.Hdem_exp[K_H:]
+ self.SimpleFit_I_fit = I_opt
+ self.SimpleFit_A = A
+ self.SimpleFit_chi_q = chi_q
+ self.SimpleFit_S_H_fit = S_H_opt
+ self.SimpleFit_S_M_fit = S_M_opt
+ self.SimpleFit_I_res_fit = I_res_opt
+ self.SimpleFit_A_opt = A_opt
+ self.SimpleFit_chi_q_opt = chi_q_opt
+ self.SimpleFit_A_sigma = A_Uncertainty
+ self.SimpleFit_SANSgeometry = "perpendicular"
+
+ case ExperimentGeometry.PARALLEL:
+
+ A_1 = A1 * 1e-12
+ A_2 = A2 * 1e-12
+ A, chi_q, A_opt, chi_q_opt, I_res_opt, S_H_opt, sigma_I_res, sigma_S_H \
+ = MuMagParallelGeo.SweepA_PAR(q, I_exp_red, sigma, Ms, H_0, H_dem, A_1, A_2, A_N)
+
+ A_opt = MuMagParallelGeo.OptimA_SPI_PAR(q, I_exp_red, sigma, Ms, H_0, H_dem, A_1, 0.0001)
+ chi_q_opt, I_res_opt, S_H_opt, sigma_I_res, sigma_S_H = MuMagParallelGeo.LSQ_PAR(q, I_exp_red, sigma,
+ Ms, H_0,
+ H_dem,
+ A_opt)
+
+ I_opt = MuMagParallelGeo.SANS_Model_PAR(q, S_H_opt, I_res_opt, Ms, H_0, H_dem, A_opt)
+
+ d2chi_dA2 = MuMagParallelGeo.FDM2Ord_PAR(q, I_exp_red, sigma, Ms, H_0, H_dem, A_opt)
+ N_mu = len(I_exp_red[0, :])
+ N_nu = len(I_exp_red[:, 0])
+ A_Uncertainty = np.sqrt(2 / (N_mu * N_nu * d2chi_dA2))
+
+ MuMagParallelGeo.PlotFittingResultsPAR_SimpleFit(q, A, chi_q, A_opt, chi_q_opt,
+ I_res_opt, S_H_opt, A_Uncertainty * 1e12,
+ figure, axes1, axes2, axes3, axes4)
+
+ # Save to global Variables
+ self.SimpleFit_q_exp = q
+ self.SimpleFit_I_exp = I_exp_red
+ self.SimpleFit_sigma_exp = sigma
+ self.SimpleFit_B_0_exp = self.B_0_exp[K_H:]
+ self.SimpleFit_Ms_exp = self.Ms_exp[K_H:]
+ self.SimpleFit_Hdem_exp = self.Hdem_exp[K_H:]
+ self.SimpleFit_I_fit = I_opt
+ self.SimpleFit_A = A
+ self.SimpleFit_chi_q = chi_q
+ self.SimpleFit_S_H_fit = S_H_opt
+ self.SimpleFit_I_res_fit = I_res_opt
+ self.SimpleFit_A_opt = A_opt
+ self.SimpleFit_chi_q_opt = chi_q_opt
+ self.SimpleFit_A_sigma = A_Uncertainty
+ self.SimpleFit_SANSgeometry = "parallel"
diff --git a/src/sas/qtgui/Utilities/MuMagTool/fit_parameters.py b/src/sas/qtgui/Utilities/MuMagTool/fit_parameters.py
index 8fc671bbb9..20b65ff98b 100644
--- a/src/sas/qtgui/Utilities/MuMagTool/fit_parameters.py
+++ b/src/sas/qtgui/Utilities/MuMagTool/fit_parameters.py
@@ -1,4 +1,11 @@
from dataclasses import dataclass
+from enum import Enum
+
+
+class ExperimentGeometry(Enum):
+ PARALLEL = 1
+ PERPENDICULAR = 2
+
@dataclass
class FitParameters:
@@ -7,4 +14,5 @@ class FitParameters:
exchange_A_min: float
exchange_A_max: float
exchange_A_n: int
- experiment_geometry: str
\ No newline at end of file
+ experiment_geometry: ExperimentGeometry
+
diff --git a/src/sas/qtgui/Utilities/MuMagTool/fit_result.py b/src/sas/qtgui/Utilities/MuMagTool/fit_result.py
index e9d08e9455..b74cad7d46 100644
--- a/src/sas/qtgui/Utilities/MuMagTool/fit_result.py
+++ b/src/sas/qtgui/Utilities/MuMagTool/fit_result.py
@@ -3,6 +3,7 @@
import numpy as np
from sas.qtgui.Utilities.MuMagTool.experimental_data import ExperimentalData
+from sas.qtgui.Utilities.MuMagTool.fit_parameters import ExperimentGeometry
@dataclass
@@ -25,4 +26,5 @@ class FitResults:
optimal_A_chi_sq: float
optimal_A_stdev: float # check
- geometry: str
\ No newline at end of file
+ geometry: ExperimentGeometry
+
From 7c420f7af88a700afb60d9391b9d643ff5f37f8e Mon Sep 17 00:00:00 2001
From: lucas-wilkins
Date: Tue, 4 Jun 2024 18:03:05 +0100
Subject: [PATCH 29/46] Lots of refractoring
---
src/sas/qtgui/Utilities/MuMagTool/MuMagLib.py | 66 ++--
.../Utilities/MuMagTool/MuMagParallelGeo.py | 33 +-
.../MuMagTool/MuMagPerpendicularGeo.py | 298 ++++++++----------
.../qtgui/Utilities/MuMagTool/UI/MuMagUI.ui | 6 +-
.../Utilities/MuMagTool/experimental_data.py | 6 +-
.../Utilities/MuMagTool/fit_parameters.py | 2 +
.../qtgui/Utilities/MuMagTool/fit_result.py | 2 -
.../MuMagTool/least_squares_output.py | 18 ++
src/sas/qtgui/Utilities/MuMagTool/models.py | 103 ++++++
.../qtgui/Utilities/MuMagTool/sweep_output.py | 16 +
10 files changed, 315 insertions(+), 235 deletions(-)
create mode 100644 src/sas/qtgui/Utilities/MuMagTool/least_squares_output.py
create mode 100644 src/sas/qtgui/Utilities/MuMagTool/models.py
create mode 100644 src/sas/qtgui/Utilities/MuMagTool/sweep_output.py
diff --git a/src/sas/qtgui/Utilities/MuMagTool/MuMagLib.py b/src/sas/qtgui/Utilities/MuMagTool/MuMagLib.py
index 082cd2863d..f91825870a 100644
--- a/src/sas/qtgui/Utilities/MuMagTool/MuMagLib.py
+++ b/src/sas/qtgui/Utilities/MuMagTool/MuMagLib.py
@@ -140,65 +140,55 @@ def simple_fit_button_callback(self, parameters: FitParameters, figure, axes1, a
# Not ideal, would be preferable make sure the data was
# compatible, using something like interpolation TODO
square_distance_from_qmax = (self.input_data[0].scattering_curve.x - parameters.q_max) ** 2
- max_q_index = np.argmin(square_distance_from_qmax)[0]
+ max_q_index = int(np.argmin(square_distance_from_qmax))
filtered_inputs = [datum.restrict_by_index(max_q_index)
for datum in self.input_data
if datum.applied_field > parameters.min_applied_field]
-
# Least Squares Fit in case of perpendicular SANS geometry
match parameters.experiment_geometry:
case ExperimentGeometry.PERPENDICULAR:
- A_1 = A1 * 1e-12
- A_2 = A2 * 1e-12
- A, chi_q, A_opt, chi_q_opt, I_res_opt, S_H_opt, S_M_opt, sigma_I_res, sigma_S_H, sigma_S_M \
- = MuMagPerpendicularGeo.SweepA_PERP(q, I_exp_red, sigma, Ms, H_0, H_dem, A_1, A_2, A_N)
+ sweep_data = MuMagPerpendicularGeo.SweepA_PERP(parameters, filtered_inputs)
- A_opt = MuMagPerpendicularGeo.OptimA_SPI_PERP(q, I_exp_red, sigma, Ms, H_0, H_dem, A_1, 0.0001)
- chi_q_opt, I_res_opt, S_H_opt, S_M_opt, sigma_I_res, sigma_S_H, sigma_S_M = MuMagPerpendicularGeo.LSQ_PERP(q, I_exp_red, sigma, Ms, H_0, H_dem, A_opt)
+ A_opt = MuMagPerpendicularGeo.OptimA_SPI_PERP(filtered_inputs, sweep_data.optimal.exchange_A, epsilon=0.0001)
- I_opt = MuMagPerpendicularGeo.SANS_Model_PERP(q, S_H_opt, S_M_opt, I_res_opt, Ms, H_0, H_dem, A_opt)
+ least_squares_fit_at_optimum = MuMagPerpendicularGeo.LSQ_PERP(filtered_inputs, A_opt)
- d2chi_dA2 = MuMagPerpendicularGeo.FDM2Ord_PERP(q, I_exp_red, sigma, Ms, H_0, H_dem, A_opt)
+ I_opt = least_squares_fit_at_optimum.I_simulated
- N_mu = len(I_exp_red[0, :])
- N_nu = len(I_exp_red[:, 0])
- A_Uncertainty = np.sqrt(2 / (N_mu * N_nu * d2chi_dA2))
+ uncertainty = MuMagPerpendicularGeo.uncertainty_perp(filtered_inputs, A_opt)
- MuMagPerpendicularGeo.PlotFittingResultsPERP_SimpleFit(q, A, chi_q, A_opt, chi_q_opt, I_res_opt,
- S_H_opt, S_M_opt, A_Uncertainty * 1e12,
+ MuMagPerpendicularGeo.PlotFittingResultsPERP_SimpleFit(sweep_data, uncertainty * 1e12,
figure, axes1, axes2, axes3, axes4)
# Save to global Variables
- self.SimpleFit_q_exp = q
- self.SimpleFit_I_exp = I_exp_red
- self.SimpleFit_sigma_exp = sigma
- self.SimpleFit_B_0_exp = self.B_0_exp[K_H:]
- self.SimpleFit_Ms_exp = self.Ms_exp[K_H:]
- self.SimpleFit_Hdem_exp = self.Hdem_exp[K_H:]
- self.SimpleFit_I_fit = I_opt
- self.SimpleFit_A = A
- self.SimpleFit_chi_q = chi_q
- self.SimpleFit_S_H_fit = S_H_opt
- self.SimpleFit_S_M_fit = S_M_opt
- self.SimpleFit_I_res_fit = I_res_opt
- self.SimpleFit_A_opt = A_opt
- self.SimpleFit_chi_q_opt = chi_q_opt
- self.SimpleFit_A_sigma = A_Uncertainty
- self.SimpleFit_SANSgeometry = "perpendicular"
+ # self.SimpleFit_q_exp = q
+ # self.SimpleFit_I_exp = I_exp_red
+ # self.SimpleFit_sigma_exp = sigma
+ # self.SimpleFit_B_0_exp = self.B_0_exp[K_H:]
+ # self.SimpleFit_Ms_exp = self.Ms_exp[K_H:]
+ # self.SimpleFit_Hdem_exp = self.Hdem_exp[K_H:]
+ # self.SimpleFit_I_fit = I_opt
+ # self.SimpleFit_A = A
+ # self.SimpleFit_chi_q = chi_q
+ # self.SimpleFit_S_H_fit = S_H_opt
+ # self.SimpleFit_S_M_fit = S_M_opt
+ # self.SimpleFit_I_res_fit = I_res_opt
+ # self.SimpleFit_A_opt = A_opt
+ # self.SimpleFit_chi_q_opt = chi_q_opt
+ # self.SimpleFit_A_sigma = A_Uncertainty
+ # self.SimpleFit_SANSgeometry = "perpendicular"
case ExperimentGeometry.PARALLEL:
- A_1 = A1 * 1e-12
- A_2 = A2 * 1e-12
A, chi_q, A_opt, chi_q_opt, I_res_opt, S_H_opt, sigma_I_res, sigma_S_H \
- = MuMagParallelGeo.SweepA_PAR(q, I_exp_red, sigma, Ms, H_0, H_dem, A_1, A_2, A_N)
+ = MuMagParallelGeo.SweepA_PAR(q, I_exp_red, sigma, Ms, H_0, H_dem, A_min, A_max_J, A_N)
- A_opt = MuMagParallelGeo.OptimA_SPI_PAR(q, I_exp_red, sigma, Ms, H_0, H_dem, A_1, 0.0001)
+ A_opt = MuMagParallelGeo.OptimA_SPI_PAR(q, I_exp_red, sigma, Ms, H_0, H_dem, A_min, 0.0001)
chi_q_opt, I_res_opt, S_H_opt, sigma_I_res, sigma_S_H = MuMagParallelGeo.LSQ_PAR(q, I_exp_red, sigma,
Ms, H_0,
H_dem,
@@ -207,9 +197,9 @@ def simple_fit_button_callback(self, parameters: FitParameters, figure, axes1, a
I_opt = MuMagParallelGeo.SANS_Model_PAR(q, S_H_opt, I_res_opt, Ms, H_0, H_dem, A_opt)
d2chi_dA2 = MuMagParallelGeo.FDM2Ord_PAR(q, I_exp_red, sigma, Ms, H_0, H_dem, A_opt)
- N_mu = len(I_exp_red[0, :])
- N_nu = len(I_exp_red[:, 0])
- A_Uncertainty = np.sqrt(2 / (N_mu * N_nu * d2chi_dA2))
+ n_field_strengths = len(I_exp_red[0, :])
+ n_q = len(I_exp_red[:, 0])
+ A_Uncertainty = np.sqrt(2 / (n_field_strengths * n_q * d2chi_dA2))
MuMagParallelGeo.PlotFittingResultsPAR_SimpleFit(q, A, chi_q, A_opt, chi_q_opt,
I_res_opt, S_H_opt, A_Uncertainty * 1e12,
diff --git a/src/sas/qtgui/Utilities/MuMagTool/MuMagParallelGeo.py b/src/sas/qtgui/Utilities/MuMagTool/MuMagParallelGeo.py
index fb69206e4b..eb14e30e01 100644
--- a/src/sas/qtgui/Utilities/MuMagTool/MuMagParallelGeo.py
+++ b/src/sas/qtgui/Utilities/MuMagTool/MuMagParallelGeo.py
@@ -3,37 +3,6 @@
import matplotlib.pyplot as plt
-####################################################################################################
-# Functions for the analysis of parallel SANS ######################################################
-####################################################################################################
-# Lorentzian Model for the generation of clean synthetic test data for perpendicular SANS geometry
-def LorentzianModelPAR(q, A, M_s, H_0, H_dem, a_H, l_c):
-
- # All inputs in SI-units
- # Micromagnetic Model
- mu_0 = 4 * np.pi * 1e-7
- H_i = H_0 - H_dem
- l_H = np.sqrt((2 * A) / (mu_0 * M_s * H_i))
- H_eff = H_i * (1 + (l_H ** 2) * (q ** 2))
- p = M_s / H_eff
- R_H = (p ** 2) / 2
-
- # Lorentzian functions for S_H, S_M
- S_H = a_H**2/(1 + q**2 * l_c**2)**2
-
- # Magnetic SANS cross sections
- I_M = R_H * S_H
-
- # Model of I_res
- idx = np.argmax(H_0[:, 1])
- I_res = 0.9 * I_M[idx, :]
-
- # Total SANS cross section
- I_sim = I_res + I_M
- sigma = 0 * I_sim + 1
-
- return I_sim, sigma, S_H, I_res
-
####################################################################################################
# Least squares method for parallel SANS geometry
def LSQ_PAR(q, I_exp, sigma, M_s, H_0, H_dem, A):
@@ -115,7 +84,7 @@ def chi_q_PAR(q, I_exp, sigma, M_s, H_0, H_dem, A):
# Sweep over Exchange Stiffness A for parallel SANS geometry
def SweepA_PAR(q, I_exp, sigma, M_s, H_0, H_dem, A_1, A_2, A_N):
- A = np.linspace(A_1, A_2, A_N)
+ A = np.linspace(A_1, A_2, A_N) * 1e-12 # pJ/m -> J/m
chi_q = np.zeros(len(A))
for k in np.arange(0, len(A)):
chi_q[k], I_res, S_H, sigma_I_res, sigma_S_H = LSQ_PAR(q, I_exp, sigma, M_s, H_0, H_dem, A[k])
diff --git a/src/sas/qtgui/Utilities/MuMagTool/MuMagPerpendicularGeo.py b/src/sas/qtgui/Utilities/MuMagTool/MuMagPerpendicularGeo.py
index c031b5a806..ee841b424e 100644
--- a/src/sas/qtgui/Utilities/MuMagTool/MuMagPerpendicularGeo.py
+++ b/src/sas/qtgui/Utilities/MuMagTool/MuMagPerpendicularGeo.py
@@ -2,178 +2,142 @@
import scipy.optimize as scopt
import matplotlib.pyplot as plt
+from sas.qtgui.Utilities.MuMagTool.experimental_data import ExperimentalData
+from sas.qtgui.Utilities.MuMagTool.fit_parameters import FitParameters
+from sas.qtgui.Utilities.MuMagTool.fit_result import FitResults
+from sas.qtgui.Utilities.MuMagTool.least_squares_output import LeastSquaresOutput
+from sas.qtgui.Utilities.MuMagTool.sweep_output import SweepOutput
+mu_0 = 4 * np.pi * 1e-7
-####################################################################################################
-# Functions for the analysis of perpendicular SANS #################################################
-####################################################################################################
-# Lorentzian Model for the generation of clean synthetic test data for perpendicular SANS geometry
-def LorentzianModelPERP(q, A, M_s, H_0, H_dem, a_H, a_M, l_c):
-
- # All inputs in SI-units
- # Micromagnetic Model
- mu_0 = 4 * np.pi * 1e-7
- H_i = H_0 - H_dem
- l_H = np.sqrt((2*A)/(mu_0 * M_s * H_i))
- H_eff = H_i * (1 + l_H**2 * q**2)
- p = M_s/H_eff
- R_H = p**2 / 4 * (2 + 1/np.sqrt(1 + p))
- R_M = (np.sqrt(1 + p) - 1)/2
+def LSQ_PERP(data: list[ExperimentalData], A) -> LeastSquaresOutput:
+ """ Least squares fitting for a given exchange stiffness, A
- # Lorentzian functions for S_H, S_M
- S_H = a_H**2/(1 + q**2 * l_c**2)**2
- S_M = a_M**2/(1 + q**2 * l_c**2)**2
+ We are fitting the equation:
- # Magnetic SANS cross sections
- I_M = R_H * S_H + R_M * S_M
+ I_sim = I_res + response_H * S_H + response_M * S_M
+ = (I_res, S_H, S_M) . (1, response_H, response_M)
+ = (I_res, S_H, S_M) . least_squares_x
- # Model of I_res
- idx = np.argmax(H_0[:, 1])
- print(H_0[idx, 1]*mu_0*1e3)
- I_res = 0.9 * I_M[idx, :]
+ finding I_res, S_H, and S_M for each q value
- # Total SANS cross section
- I_sim = I_res + I_M
- sigma = 0 * I_sim + 1
- return I_sim, sigma, S_H, S_M, I_res
+ """
-####################################################################################################
-# Lorentzian Model for the generation of noisy synthetic test data for perpendicular SANS geometry
-def LorentzianNoisyModelPERP(q, A, M_s, H_0, H_dem, a_H, a_M, l_c, beta):
+ # Get matrices from the input data
+ n_data = len(data)
- # All inputs in SI-units
+ applied_field = np.array([datum.applied_field for datum in data])
+ demagnetising_field = np.array([datum.demagnetising_field for datum in data])
+ saturation_magnetisation = np.array([datum.saturation_magnetisation for datum in data])
- # Micromagnetic Model
- mu_0 = 4 * np.pi * 1e-7
- H_i = H_0 - H_dem
- l_H = np.sqrt((2*A)/(mu_0 * M_s * H_i))
- H_eff = H_i * (1 + l_H**2 * q**2)
- p = M_s/H_eff
- R_H = p**2 / 4 * (2 + 1/np.sqrt(1 + p))
- R_M = (np.sqrt(1 + p) - 1)/2
+ q = np.array([datum.scattering_curve.x for datum in data]) # TODO: Check for transpose
+ I = np.array([datum.scattering_curve.y for datum in data])
+ I_stdev = np.array([datum.scattering_curve.dy for datum in data])
- # Lorentzian functions for S_H, S_M
- S_H = a_H**2/(1 + q**2 * l_c**2)**2
- S_M = a_M**2/(1 + q**2 * l_c**2)**2
+ n_q = q.shape[0]
- # Magnetic SANS cross sections
- I_M = R_H * S_H + R_M * S_M
+ # Micromagnetic Model
+ internal_field = (applied_field - demagnetising_field).reshape(-1, 1)
+ magnetic_scattering_length = np.sqrt((2 * A) / (mu_0 * saturation_magnetisation.reshape(-1, 1) * internal_field))
+ effective_field = internal_field * (1 + (magnetic_scattering_length ** 2) * (q ** 2))
- # Model of I_res
- idx = np.argmax(H_0[:, 1])
- I_res = 0.9 * I_M[idx, :]
+ # Calculate the response functions
+ p = saturation_magnetisation.reshape(-1, 1) / effective_field
+ response_H = (p ** 2) / 4 * (2 + 1 / np.sqrt(1 + p))
+ response_M = (np.sqrt(1 + p) - 1) / 2
- # Model of standard deviation
- sigma = beta * (I_res + I_M)
+ print("Shapes:")
+ print(" I", I.shape)
+ print(" q", q.shape)
+ print(" sigma", I_stdev.shape)
+ print(" response H", response_H.shape)
+ print(" response M", response_M.shape)
- # Total SANS cross section
- I_sim = I_res + I_M + sigma * np.random.randn(len(sigma[:, 1]), len(sigma[1, :]))
+ # Lists for output of calculation
+ I_residual = []
+ S_H = []
+ S_M = []
- return I_sim, sigma, S_H, S_M, I_res
+ I_residual_error_weight = []
+ S_M_error_weight = []
+ S_H_error_weight = []
-####################################################################################################
-# Least squares method for perpendicular SANS geometry
-def LSQ_PERP(q, I_exp, sigma, M_s, H_0, H_dem, A):
+ for nu in range(n_q):
- # Micromagnetic Model
- mu_0 = 4 * np.pi * 1e-7
- H_i = H_0 - H_dem
- l_H = np.sqrt((2 * A) / (mu_0 * M_s * H_i))
- H_eff = H_i * (1 + (l_H ** 2) * (q ** 2))
- p = M_s / H_eff
- R_H = (p ** 2) / 4 * (2 + 1 / np.sqrt(1 + p))
- R_M = (np.sqrt(1 + p) - 1) / 2
- # Non-negative least squares loop
- L_nu = len(q[1, :])
- S_H = np.zeros((1, L_nu))
- S_M = np.zeros((1, L_nu))
- I_res = np.zeros((1, L_nu))
- sigma_S_H = np.zeros((1, L_nu))
- sigma_S_M = np.zeros((1, L_nu))
- sigma_I_res = np.zeros((1, L_nu))
- for nu in np.arange(0, L_nu):
- A = np.array([1/sigma[:, nu], R_H[:, nu]/sigma[:, nu], R_M[:, nu]/sigma[:, nu]]).T
- y = I_exp[:, nu]/sigma[:, nu]
- c = np.matmul(A.T, y)
- H = np.dot(A.T, A)
# non-negative linear least squares
- x = scopt.nnls(H, c)
- I_res[0, nu] = x[0][0]
- S_H[0, nu] = x[0][1]
- S_M[0, nu] = x[0][2]
+ least_squares_x = (np.array([np.ones((n_data,)), response_H[:, nu], response_M[:, nu]]) / I_stdev[:, nu]).T
+ least_squares_y = I[:, nu]/I_stdev[:, nu]
- Gamma = np.linalg.inv(np.dot(H.T, H))
- sigma_I_res[0, nu] = Gamma[0, 0]
- sigma_S_H[0, nu] = Gamma[1, 1]
- sigma_S_M[0, nu] = Gamma[2, 2]
+ print("Least Squares X", least_squares_x.shape)
+ print("Least Squares Y", least_squares_y.shape)
- I_sim = I_res + R_H * S_H + R_M * S_M
- s_q = np.mean(((I_exp - I_sim)/sigma)**2, axis=0)
+ least_squares_x_squared = np.dot(least_squares_x.T, least_squares_x)
- sigma_S_H = np.sqrt(np.abs(sigma_S_H * s_q))
- sigma_S_M = np.sqrt(np.abs(sigma_S_M * s_q))
- sigma_I_res = np.sqrt(np.abs(sigma_I_res * s_q))
+ # Non-negative least squares
+ fit_result = scopt.nnls(
+ least_squares_x_squared,
+ np.matmul(least_squares_x.T, least_squares_y))
- chi_q = np.mean(s_q)
+ I_residual.append(fit_result[0][0])
+ S_H.append(fit_result[0][1])
+ S_M.append(fit_result[0][2])
- return chi_q, I_res, S_H, S_M, sigma_I_res, sigma_S_H, sigma_S_M
+ errors = np.linalg.inv(np.dot(least_squares_x_squared.T, least_squares_x_squared))
-####################################################################################################
-# Least squares method for perpendicular SANS geometry
-def chi_q_PERP(q, I_exp, sigma, M_s, H_0, H_dem, A):
+ I_residual_error_weight.append(errors[0, 0])
+ S_H_error_weight.append(errors[1, 1])
+ S_M_error_weight.append(errors[2, 2])
- # Micromagnetic Model
- A = abs(A)
- mu_0 = 4 * np.pi * 1e-7
- H_i = H_0 - H_dem
- l_H = np.sqrt((2 * A) / (mu_0 * M_s * H_i))
- H_eff = H_i * (1 + (l_H ** 2) * (q ** 2))
- p = M_s / H_eff
- R_H = (p ** 2) / 4 * (2 + 1 / np.sqrt(1 + p))
- R_M = (np.sqrt(1 + p) - 1) / 2
+ # Arrayise
+ S_H = np.array(S_H)
+ S_M = np.array(S_M)
+ I_residual = np.array(I_residual)
- # Non-negative least squares loop
- L_nu = len(q[0, :])
- S_H = np.zeros((1, L_nu))
- S_M = np.zeros((1, L_nu))
- I_res = np.zeros((1, L_nu))
- for nu in np.arange(0, L_nu):
- A = np.array([1/sigma[:, nu], R_H[:, nu]/sigma[:, nu], R_M[:, nu]/sigma[:, nu]]).T
- y = I_exp[:, nu]/sigma[:, nu]
+ I_sim = I_residual.reshape(-1, 1) + response_H * S_H.reshape(-1, 1) + response_M * S_M.reshape(-1, 1)
- c = np.matmul(A.T, y)
- H = np.dot(A.T, A)
+ s_q = np.mean(((I - I_sim)/I_stdev)**2, axis=1)
- # non-negative linear least squares
- x = scopt.nnls(H, c)
- I_res[0, nu] = x[0][0]
- S_H[0, nu] = x[0][1]
- S_M[0, nu] = x[0][2]
+ sigma_I_res = np.sqrt(np.abs(np.array(I_residual_error_weight) * s_q))
+ sigma_S_H = np.sqrt(np.abs(np.array(S_H_error_weight) * s_q))
+ sigma_S_M = np.sqrt(np.abs(np.array(S_M_error_weight) * s_q))
- I_sim = I_res + R_H * S_H + R_M * S_M
- s_q = np.mean(((I_exp - I_sim)/sigma)**2, axis=0)
- chi_q = np.mean(s_q)
+ chi_sq = float(np.mean(s_q))
+
+ return LeastSquaresOutput(
+ exchange_A=A,
+ exchange_A_chi_sq=chi_sq,
+ q=np.mean(q, axis=1),
+ I_residual=I_residual,
+ I_simulated=I_sim,
+ S_H=S_H,
+ S_M=S_M,
+ I_residual_stdev=sigma_I_res,
+ S_H_stdev=sigma_S_H,
+ S_M_stdev=sigma_S_M)
- return chi_q
####################################################################################################
# Sweep over Exchange Stiffness A for perpendicular SANS geometry
-def SweepA_PERP(q, I_exp, sigma, M_s, H_0, H_dem, A_1, A_2, A_N):
+def SweepA_PERP(parameters: FitParameters, data: list[ExperimentalData]):
+
+ a_values = np.linspace(
+ parameters.exchange_A_min,
+ parameters.exchange_A_max,
+ parameters.exchange_A_n) * 1e-12 # From pJ/m to J/m
- A = np.linspace(A_1, A_2, A_N)
- chi_q = np.zeros(len(A))
- for k in np.arange(0, len(A)):
- chi_q[k], I_res, S_H, S_M, sigma_I_res, sigma_S_H, sigma_S_M = LSQ_PERP(q, I_exp, sigma, M_s, H_0, H_dem, A[k])
+ least_squared_fits = [LSQ_PERP(data, a) for a in a_values]
- min_idx = np.argmin(chi_q)
- A_opt = A[min_idx]
+ optimal_fit = min(least_squared_fits, key=lambda x: x.exchange_A_chi_sq)
- chi_q_opt, I_res_opt, S_H_opt, S_M_opt, sigma_I_res, sigma_S_H, sigma_S_M = LSQ_PERP(q, I_exp, sigma, M_s, H_0, H_dem, A_opt)
+ chi_sq = np.array([fit.exchange_A_chi_sq for fit in least_squared_fits])
- return A, chi_q, A_opt, chi_q_opt, I_res_opt, S_H_opt, S_M_opt, sigma_I_res, sigma_S_H, sigma_S_M
+ return SweepOutput(
+ exchange_A_checked=a_values,
+ exchange_A_chi_sq=chi_sq,
+ optimal=optimal_fit)
####################################################################################################
# find optimal Exchange Stiffness A for perpendicular SANS geometry using fsolve function (slow and very accurate)
@@ -188,7 +152,7 @@ def func(A):
# find optimal Exchange Stiffness A for perpendicular SANS geometry using
# successive parabolic interpolation (fast and accurate)
# implemented as Jarratt's Method
-def OptimA_SPI_PERP(q, I_exp, sigma, M_s, H_0, H_dem, A_1, eps):
+def OptimA_SPI_PERP(data: list[ExperimentalData], A_1, epsilon):
delta = A_1 * 0.1
@@ -196,12 +160,12 @@ def OptimA_SPI_PERP(q, I_exp, sigma, M_s, H_0, H_dem, A_1, eps):
x_2 = A_1
x_3 = A_1 + delta
- y_1 = chi_q_PERP(q, I_exp, sigma, M_s, H_0, H_dem, x_1)
- y_2 = chi_q_PERP(q, I_exp, sigma, M_s, H_0, H_dem, x_2)
- y_3 = chi_q_PERP(q, I_exp, sigma, M_s, H_0, H_dem, x_3)
+ y_1 = LSQ_PERP(data, x_1).exchange_A_chi_sq
+ y_2 = LSQ_PERP(data, x_2).exchange_A_chi_sq
+ y_3 = LSQ_PERP(data, x_3).exchange_A_chi_sq
x_4 = x_3 + 0.5 * ((x_2 - x_3)**2 * (y_3 - y_1) + (x_1 - x_3)**2 * (y_2 - y_3))/((x_2 - x_3) * (y_3 - y_1) + (x_1 - x_3) * (y_2 - y_3))
- while np.abs(2 * (x_4 - x_3)/(x_4 + x_3)) > eps:
+ while np.abs(2 * (x_4 - x_3)/(x_4 + x_3)) > epsilon:
x_1 = x_2
x_2 = x_3
@@ -209,7 +173,7 @@ def OptimA_SPI_PERP(q, I_exp, sigma, M_s, H_0, H_dem, A_1, eps):
y_1 = y_2
y_2 = y_3
- y_3 = chi_q_PERP(q, I_exp, sigma, M_s, H_0, H_dem, x_3)
+ y_3 = LSQ_PERP(data, x_3).exchange_A_chi_sq
x_4 = x_3 + 0.5 * ((x_2 - x_3) ** 2 * (y_3 - y_1) + (x_1 - x_3) ** 2 * (y_2 - y_3)) / ((x_2 - x_3) * (y_3 - y_1) + (x_1 - x_3) * (y_2 - y_3))
@@ -232,39 +196,47 @@ def SANS_Model_PERP(q, S_H, S_M, I_res, M_s, H_0, H_dem, A):
####################################################################################################
# Plot Fitting results of simple fit
-def PlotFittingResultsPERP_SimpleFit(q, A, chi_q, A_opt, chi_q_opt, I_res_opt, S_H_opt, S_M_opt, A_Uncertainty,
+def PlotFittingResultsPERP_SimpleFit(z: SweepOutput, A_Uncertainty,
figure, axes1, axes2, axes3, axes4):
if A_Uncertainty < 1e-4:
A_Uncertainty = 0
A_uncertainty_str = str(A_Uncertainty)
- A_opt_str = str(A_opt * 1e12)
+ A_opt_str = str(z.optimal.exchange_A * 1e12)
+
+ q = z.optimal.q * 1e-9
+
+ # Plot A search data
+
axes1.set_title('$A_{\mathrm{opt}} = (' + A_opt_str[0:5] + ' \pm ' + A_uncertainty_str[0:4] + ')$ pJ/m')
- axes1.plot(A * 1e12, chi_q)
- axes1.plot(A_opt * 1e12, chi_q_opt, 'o')
- axes1.set_xlim([min(A * 1e12), max(A * 1e12)])
+ axes1.plot(z.exchange_A_checked * 1e12, z.exchange_A_chi_sq)
+ axes1.plot(z.optimal.exchange_A * 1e12, z.optimal.exchange_A_chi_sq, 'o')
+
+ axes1.set_xlim([min(z.exchange_A_checked * 1e12), max(z.exchange_A_checked * 1e12)])
axes1.set_xlabel('$A$ [pJ/m]')
axes1.set_ylabel('$\chi^2$')
- axes2.plot(q[0, :] * 1e-9, I_res_opt[0, :], label='fit')
+ # Residual intensity plot
+
+ axes2.plot(q, z.optimal.I_residual, label='fit')
axes2.set_yscale('log')
axes2.set_xscale('log')
- axes2.set_xlim([min(q[0, :] * 1e-9), max(q[0, :] * 1e-9)])
+ axes2.set_xlim([min(q), max(q)])
axes2.set_xlabel('$q$ [1/nm]')
axes2.set_ylabel('$I_{\mathrm{res}}$')
- axes3.plot(q[0, :] * 1e-9, S_H_opt[0, :], label='fit')
+ axes3.plot(q, z.optimal.S_H[0, :], label='fit')
axes3.set_yscale('log')
axes3.set_xscale('log')
- axes3.set_xlim([min(q[0, :] * 1e-9), max(q[0, :] * 1e-9)])
+ axes3.set_xlim([min(q), max(q)])
axes3.set_xlabel('$q$ [1/nm]')
axes3.set_ylabel('$S_H$')
- axes4.plot(q[0, :] * 1e-9, S_M_opt[0, :], label='fit')
+ axes4.plot(q, z.optimal.S_M, label='fit')
axes4.set_yscale('log')
axes4.set_xscale('log')
- axes4.set_xlim([min(q[0, :] * 1e-9), max(q[0, :] * 1e-9)])
+ axes4.set_xlim([min(q), max(q)])
axes4.set_xlabel('$q$ [1/nm]')
axes4.set_ylabel('$S_M$')
@@ -283,10 +255,13 @@ def PlotSweepFitResultPERP(q_max_mat, H_min_mat, A_opt_mat):
plt.show()
####################################################################################################
-# Second Order Derivative of chi-square function via Finite Differences
-def FDM2Ord_PERP(q, I_exp, sigma, M_s, H_0, H_dem, A_opt):
+#
+def uncertainty_perp(data: list[ExperimentalData], A_opt: float):
+ """Calculate the uncertainty for the optimal exchange stiffness A"""
+
+ # Estimate variance from second order derivative of chi-square function via Finite Differences
- p = 0.001
+ p = 0.001 # fractional gap size for finite differences
dA = A_opt * p
A1 = A_opt - 2*dA
A2 = A_opt - 1*dA
@@ -294,12 +269,17 @@ def FDM2Ord_PERP(q, I_exp, sigma, M_s, H_0, H_dem, A_opt):
A4 = A_opt + 1*dA
A5 = A_opt + 2*dA
- chi1 = chi_q_PERP(q, I_exp, sigma, M_s, H_0, H_dem, A1)
- chi2 = chi_q_PERP(q, I_exp, sigma, M_s, H_0, H_dem, A2)
- chi3 = chi_q_PERP(q, I_exp, sigma, M_s, H_0, H_dem, A3)
- chi4 = chi_q_PERP(q, I_exp, sigma, M_s, H_0, H_dem, A4)
- chi5 = chi_q_PERP(q, I_exp, sigma, M_s, H_0, H_dem, A5)
+ chi1 = LSQ_PERP(data, A1).exchange_A_chi_sq
+ chi2 = LSQ_PERP(data, A2).exchange_A_chi_sq
+ chi3 = LSQ_PERP(data, A3).exchange_A_chi_sq
+ chi4 = LSQ_PERP(data, A4).exchange_A_chi_sq
+ chi5 = LSQ_PERP(data, A5).exchange_A_chi_sq
+
+ d2chi_dA2 = (-chi1 + 16 * chi2 - 30 * chi3 + 16 * chi4 - chi5)/(12 * dA**2)
+
+ # Scale variance by number of samples and return reciprocal square root
- d2_chi_dA2 = (-chi1 + 16 * chi2 - 30 * chi3 + 16 * chi4 - chi5)/(12 * dA**2)
+ n_field_strengths = len(data) # Number of fields
+ n_q = len(data[0].scattering_curve.x) # Number of q points
- return d2_chi_dA2
\ No newline at end of file
+ return np.sqrt(2 / (n_field_strengths * n_q * d2chi_dA2))
diff --git a/src/sas/qtgui/Utilities/MuMagTool/UI/MuMagUI.ui b/src/sas/qtgui/Utilities/MuMagTool/UI/MuMagUI.ui
index c6b39bff25..150f98107e 100644
--- a/src/sas/qtgui/Utilities/MuMagTool/UI/MuMagUI.ui
+++ b/src/sas/qtgui/Utilities/MuMagTool/UI/MuMagUI.ui
@@ -235,16 +235,16 @@
-
- perpendicular
+ Perpendicular
-
- perpendicular
+ Perpendicular
-
- parallel
+ Parallel
diff --git a/src/sas/qtgui/Utilities/MuMagTool/experimental_data.py b/src/sas/qtgui/Utilities/MuMagTool/experimental_data.py
index 0aee94cd93..11302de6e4 100644
--- a/src/sas/qtgui/Utilities/MuMagTool/experimental_data.py
+++ b/src/sas/qtgui/Utilities/MuMagTool/experimental_data.py
@@ -22,4 +22,8 @@ def restrict_by_index(self, max_index: int):
y = self.scattering_curve.y[:max_index]
dy = self.scattering_curve.dy[:max_index]
- return Data1D(x=x, y=y, dy=dy)
+ return ExperimentalData(
+ scattering_curve=Data1D(x=x, y=y, dy=dy),
+ applied_field=self.applied_field,
+ saturation_magnetisation=self.saturation_magnetisation,
+ demagnetising_field=self.demagnetising_field)
diff --git a/src/sas/qtgui/Utilities/MuMagTool/fit_parameters.py b/src/sas/qtgui/Utilities/MuMagTool/fit_parameters.py
index 20b65ff98b..9828841cf9 100644
--- a/src/sas/qtgui/Utilities/MuMagTool/fit_parameters.py
+++ b/src/sas/qtgui/Utilities/MuMagTool/fit_parameters.py
@@ -3,12 +3,14 @@
class ExperimentGeometry(Enum):
+ """ Type of experiment """
PARALLEL = 1
PERPENDICULAR = 2
@dataclass
class FitParameters:
+ """ Input parameters for the fit"""
q_max: float
min_applied_field: float
exchange_A_min: float
diff --git a/src/sas/qtgui/Utilities/MuMagTool/fit_result.py b/src/sas/qtgui/Utilities/MuMagTool/fit_result.py
index b74cad7d46..ad7b800bee 100644
--- a/src/sas/qtgui/Utilities/MuMagTool/fit_result.py
+++ b/src/sas/qtgui/Utilities/MuMagTool/fit_result.py
@@ -16,7 +16,6 @@ class FitResults:
S_H: np.ndarray
S_M: np.ndarray
-
I_residual: np.ndarray # Nuclear + Magnetic cross section at complete magnetic saturation
exchange_A: np.ndarray
@@ -27,4 +26,3 @@ class FitResults:
optimal_A_stdev: float # check
geometry: ExperimentGeometry
-
diff --git a/src/sas/qtgui/Utilities/MuMagTool/least_squares_output.py b/src/sas/qtgui/Utilities/MuMagTool/least_squares_output.py
new file mode 100644
index 0000000000..ec98183b39
--- /dev/null
+++ b/src/sas/qtgui/Utilities/MuMagTool/least_squares_output.py
@@ -0,0 +1,18 @@
+from dataclasses import dataclass
+
+import numpy as np
+
+
+@dataclass
+class LeastSquaresOutput:
+ """ Output from least squares method"""
+ exchange_A: float
+ exchange_A_chi_sq: float
+ q: np.ndarray
+ I_simulated: np.ndarray
+ I_residual: np.ndarray
+ S_H: np.ndarray
+ S_M: np.ndarray
+ I_residual_stdev: np.ndarray
+ S_H_stdev: np.ndarray
+ S_M_stdev: np.ndarray
\ No newline at end of file
diff --git a/src/sas/qtgui/Utilities/MuMagTool/models.py b/src/sas/qtgui/Utilities/MuMagTool/models.py
new file mode 100644
index 0000000000..49c8781764
--- /dev/null
+++ b/src/sas/qtgui/Utilities/MuMagTool/models.py
@@ -0,0 +1,103 @@
+
+####################################################################################################
+# Lorentzian Model for the generation of noisy synthetic test data for perpendicular SANS geometry
+def LorentzianNoisyModelPERP(q, A, M_s, H_0, H_dem, a_H, a_M, l_c, beta):
+
+ # All inputs in SI-units
+
+ # Micromagnetic Model
+ mu_0 = 4 * np.pi * 1e-7
+ H_i = H_0 - H_dem
+ l_H = np.sqrt((2*A)/(mu_0 * M_s * H_i))
+ H_eff = H_i * (1 + l_H**2 * q**2)
+ p = M_s/H_eff
+ R_H = p**2 / 4 * (2 + 1/np.sqrt(1 + p))
+ R_M = (np.sqrt(1 + p) - 1)/2
+
+ # Lorentzian functions for S_H, S_M
+ S_H = a_H**2/(1 + q**2 * l_c**2)**2
+ S_M = a_M**2/(1 + q**2 * l_c**2)**2
+
+ # Magnetic SANS cross sections
+ I_M = R_H * S_H + R_M * S_M
+
+ # Model of I_res
+ idx = np.argmax(H_0[:, 1])
+ I_res = 0.9 * I_M[idx, :]
+
+ # Model of standard deviation
+ sigma = beta * (I_res + I_M)
+
+ # Total SANS cross section
+ I_sim = I_res + I_M + sigma * np.random.randn(len(sigma[:, 1]), len(sigma[1, :]))
+
+ return I_sim, sigma, S_H, S_M, I_res
+
+
+
+
+####################################################################################################
+# Functions for the analysis of parallel SANS ######################################################
+####################################################################################################
+# Lorentzian Model for the generation of clean synthetic test data for perpendicular SANS geometry
+def LorentzianModelPAR(q, A, M_s, H_0, H_dem, a_H, l_c):
+
+ # All inputs in SI-units
+ # Micromagnetic Model
+ mu_0 = 4 * np.pi * 1e-7
+ H_i = H_0 - H_dem
+ l_H = np.sqrt((2 * A) / (mu_0 * M_s * H_i))
+ H_eff = H_i * (1 + (l_H ** 2) * (q ** 2))
+ p = M_s / H_eff
+ R_H = (p ** 2) / 2
+
+ # Lorentzian functions for S_H, S_M
+ S_H = a_H**2/(1 + q**2 * l_c**2)**2
+
+ # Magnetic SANS cross sections
+ I_M = R_H * S_H
+
+ # Model of I_res
+ idx = np.argmax(H_0[:, 1])
+ I_res = 0.9 * I_M[idx, :]
+
+ # Total SANS cross section
+ I_sim = I_res + I_M
+ sigma = 0 * I_sim + 1
+
+ return I_sim, sigma, S_H, I_res
+
+
+####################################################################################################
+# Functions for the analysis of perpendicular SANS #################################################
+####################################################################################################
+# Lorentzian Model for the generation of clean synthetic test data for perpendicular SANS geometry
+def LorentzianModelPERP(q, A, M_s, H_0, H_dem, a_H, a_M, l_c):
+
+ # All inputs in SI-units
+ # Micromagnetic Model
+ mu_0 = 4 * np.pi * 1e-7
+ H_i = H_0 - H_dem
+ l_H = np.sqrt((2*A)/(mu_0 * M_s * H_i))
+ H_eff = H_i * (1 + l_H**2 * q**2)
+ p = M_s/H_eff
+ R_H = p**2 / 4 * (2 + 1/np.sqrt(1 + p))
+ R_M = (np.sqrt(1 + p) - 1)/2
+
+ # Lorentzian functions for S_H, S_M
+ S_H = a_H**2/(1 + q**2 * l_c**2)**2
+ S_M = a_M**2/(1 + q**2 * l_c**2)**2
+
+ # Magnetic SANS cross sections
+ I_M = R_H * S_H + R_M * S_M
+
+ # Model of I_res
+ idx = np.argmax(H_0[:, 1])
+ print(H_0[idx, 1]*mu_0*1e3)
+ I_res = 0.9 * I_M[idx, :]
+
+ # Total SANS cross section
+ I_sim = I_res + I_M
+ sigma = 0 * I_sim + 1
+
+ return I_sim, sigma, S_H, S_M, I_res
diff --git a/src/sas/qtgui/Utilities/MuMagTool/sweep_output.py b/src/sas/qtgui/Utilities/MuMagTool/sweep_output.py
new file mode 100644
index 0000000000..2103579976
--- /dev/null
+++ b/src/sas/qtgui/Utilities/MuMagTool/sweep_output.py
@@ -0,0 +1,16 @@
+from dataclasses import dataclass
+
+import numpy as np
+
+from sas.qtgui.Utilities.MuMagTool.least_squares_output import LeastSquaresOutput
+
+
+@dataclass
+class SweepOutput:
+ """
+ Results from brute force optimisiation of the chi squared for the exchange A parameter
+ """
+
+ exchange_A_checked: np.ndarray
+ exchange_A_chi_sq: np.ndarray
+ optimal: LeastSquaresOutput
\ No newline at end of file
From 717a41951b78b96ee10a4da565bd9ae6a0acbc78 Mon Sep 17 00:00:00 2001
From: lucas-wilkins
Date: Thu, 6 Jun 2024 17:05:27 +0100
Subject: [PATCH 30/46] Debugging
---
src/sas/qtgui/Utilities/MuMagTool/MuMag.py | 11 ++++++--
.../MuMagTool/MuMagPerpendicularGeo.py | 26 +++++++++----------
2 files changed, 22 insertions(+), 15 deletions(-)
diff --git a/src/sas/qtgui/Utilities/MuMagTool/MuMag.py b/src/sas/qtgui/Utilities/MuMagTool/MuMag.py
index c695c87d20..5a8da62185 100644
--- a/src/sas/qtgui/Utilities/MuMagTool/MuMag.py
+++ b/src/sas/qtgui/Utilities/MuMagTool/MuMag.py
@@ -66,6 +66,13 @@ def plot_experimental_data_button_callback(self):
self.figure_canvas.draw()
def fit_parameters(self) -> FitParameters:
+ """ Get an object containing all the parameters needed for doing the fitting """
+
+ a_min = self.aMinSpinBox.value()
+ a_max = self.aMaxSpinBox.value()
+
+ if a_max <= a_min:
+ raise ValueError(f"minimum A must be less than maximum A")
match self.ScatteringGeometrySelect.currentText().lower():
case "parallel":
@@ -78,9 +85,9 @@ def fit_parameters(self) -> FitParameters:
return FitParameters(
q_max=self.qMaxSpinBox.value(),
min_applied_field=self.hMinSpinBox.value(),
- exchange_A_min=self.aMinSpinBox.value(),
- exchange_A_max=self.aMaxSpinBox.value(),
exchange_A_n=self.aSamplesSpinBox.value(),
+ exchange_A_min=a_min,
+ exchange_A_max=a_max,
experiment_geometry=geometry)
def simple_fit_button_callback(self):
diff --git a/src/sas/qtgui/Utilities/MuMagTool/MuMagPerpendicularGeo.py b/src/sas/qtgui/Utilities/MuMagTool/MuMagPerpendicularGeo.py
index ee841b424e..5fe0a55ebd 100644
--- a/src/sas/qtgui/Utilities/MuMagTool/MuMagPerpendicularGeo.py
+++ b/src/sas/qtgui/Utilities/MuMagTool/MuMagPerpendicularGeo.py
@@ -47,13 +47,6 @@ def LSQ_PERP(data: list[ExperimentalData], A) -> LeastSquaresOutput:
response_H = (p ** 2) / 4 * (2 + 1 / np.sqrt(1 + p))
response_M = (np.sqrt(1 + p) - 1) / 2
- print("Shapes:")
- print(" I", I.shape)
- print(" q", q.shape)
- print(" sigma", I_stdev.shape)
- print(" response H", response_H.shape)
- print(" response M", response_M.shape)
-
# Lists for output of calculation
I_residual = []
S_H = []
@@ -71,15 +64,20 @@ def LSQ_PERP(data: list[ExperimentalData], A) -> LeastSquaresOutput:
least_squares_x = (np.array([np.ones((n_data,)), response_H[:, nu], response_M[:, nu]]) / I_stdev[:, nu]).T
least_squares_y = I[:, nu]/I_stdev[:, nu]
- print("Least Squares X", least_squares_x.shape)
- print("Least Squares Y", least_squares_y.shape)
-
least_squares_x_squared = np.dot(least_squares_x.T, least_squares_x)
# Non-negative least squares
- fit_result = scopt.nnls(
- least_squares_x_squared,
- np.matmul(least_squares_x.T, least_squares_y))
+ try:
+ fit_result = scopt.nnls(
+ least_squares_x_squared,
+ np.matmul(least_squares_x.T, least_squares_y))
+
+ except ValueError as ve:
+ print("Value Error:")
+ print(" A =", A)
+
+ raise ve
+
I_residual.append(fit_result[0][0])
S_H.append(fit_result[0][1])
@@ -167,6 +165,8 @@ def OptimA_SPI_PERP(data: list[ExperimentalData], A_1, epsilon):
x_4 = x_3 + 0.5 * ((x_2 - x_3)**2 * (y_3 - y_1) + (x_1 - x_3)**2 * (y_2 - y_3))/((x_2 - x_3) * (y_3 - y_1) + (x_1 - x_3) * (y_2 - y_3))
while np.abs(2 * (x_4 - x_3)/(x_4 + x_3)) > epsilon:
+ print("x1,x2,x3,x4:", x_1, x_2, x_3, x_4)
+
x_1 = x_2
x_2 = x_3
x_3 = x_4
From 725983373ba3d1cdd99f9ad34df03593de290213 Mon Sep 17 00:00:00 2001
From: lucas-wilkins
Date: Fri, 7 Jun 2024 13:36:30 +0100
Subject: [PATCH 31/46] Debugged, starting on parallel case
---
src/sas/qtgui/Utilities/MuMagTool/MuMagLib.py | 2 +-
.../MuMagTool/MuMagPerpendicularGeo.py | 72 ++++++++++---------
.../MuMagTool/least_squares_output.py | 16 ++++-
src/sas/qtgui/Utilities/MuMagTool/models.py | 31 ++++----
.../qtgui/Utilities/MuMagTool/sweep_output.py | 2 +-
5 files changed, 71 insertions(+), 52 deletions(-)
diff --git a/src/sas/qtgui/Utilities/MuMagTool/MuMagLib.py b/src/sas/qtgui/Utilities/MuMagTool/MuMagLib.py
index f91825870a..235ab38c82 100644
--- a/src/sas/qtgui/Utilities/MuMagTool/MuMagLib.py
+++ b/src/sas/qtgui/Utilities/MuMagTool/MuMagLib.py
@@ -144,7 +144,7 @@ def simple_fit_button_callback(self, parameters: FitParameters, figure, axes1, a
filtered_inputs = [datum.restrict_by_index(max_q_index)
for datum in self.input_data
- if datum.applied_field > parameters.min_applied_field]
+ if datum.applied_field >= parameters.min_applied_field]
# Least Squares Fit in case of perpendicular SANS geometry
diff --git a/src/sas/qtgui/Utilities/MuMagTool/MuMagPerpendicularGeo.py b/src/sas/qtgui/Utilities/MuMagTool/MuMagPerpendicularGeo.py
index 5fe0a55ebd..45778671a9 100644
--- a/src/sas/qtgui/Utilities/MuMagTool/MuMagPerpendicularGeo.py
+++ b/src/sas/qtgui/Utilities/MuMagTool/MuMagPerpendicularGeo.py
@@ -1,11 +1,15 @@
+import sys
+
import numpy as np
import scipy.optimize as scopt
import matplotlib.pyplot as plt
+from sasdata.dataloader import Data1D
+from data_util.nxsunit import Converter
from sas.qtgui.Utilities.MuMagTool.experimental_data import ExperimentalData
from sas.qtgui.Utilities.MuMagTool.fit_parameters import FitParameters
from sas.qtgui.Utilities.MuMagTool.fit_result import FitResults
-from sas.qtgui.Utilities.MuMagTool.least_squares_output import LeastSquaresOutput
+from sas.qtgui.Utilities.MuMagTool.least_squares_output import LeastSquaresOutput, LeastSquaresOutputPerpendicular
from sas.qtgui.Utilities.MuMagTool.sweep_output import SweepOutput
mu_0 = 4 * np.pi * 1e-7
@@ -24,18 +28,32 @@ def LSQ_PERP(data: list[ExperimentalData], A) -> LeastSquaresOutput:
"""
+
+
# Get matrices from the input data
n_data = len(data)
- applied_field = np.array([datum.applied_field for datum in data])
- demagnetising_field = np.array([datum.demagnetising_field for datum in data])
- saturation_magnetisation = np.array([datum.saturation_magnetisation for datum in data])
+ # Factor of (1e-3 / mu_0) converts from mT to A/m
+ applied_field = np.array([datum.applied_field for datum in data]) * (1e-3 / mu_0)
+ demagnetising_field = np.array([datum.demagnetising_field for datum in data]) * (1e-3 / mu_0)
+ saturation_magnetisation = np.array([datum.saturation_magnetisation for datum in data]) * (1e-3 / mu_0)
+
+ # TODO: The following is how things should be done in the future, rather than hard-coding
+ # a scaling factor...
+ # def data_nanometers(data: Data1D):
+ # raw_data = data.x
+ # units = data.x_unit
+ # converter = Converter(units)
+ # return converter.scale("1/m", raw_data)
+ #
+ # q = np.array([data_nanometers(datum.scattering_curve) for datum in data])
- q = np.array([datum.scattering_curve.x for datum in data]) # TODO: Check for transpose
+
+ q = np.array([datum.scattering_curve.x for datum in data]) * 1e9
I = np.array([datum.scattering_curve.y for datum in data])
I_stdev = np.array([datum.scattering_curve.dy for datum in data])
- n_q = q.shape[0]
+ n_q = q.shape[1]
# Micromagnetic Model
internal_field = (applied_field - demagnetising_field).reshape(-1, 1)
@@ -47,6 +65,9 @@ def LSQ_PERP(data: list[ExperimentalData], A) -> LeastSquaresOutput:
response_H = (p ** 2) / 4 * (2 + 1 / np.sqrt(1 + p))
response_M = (np.sqrt(1 + p) - 1) / 2
+ # print("Input", q.shape, q[0,10])
+ # sys.exit()
+
# Lists for output of calculation
I_residual = []
S_H = []
@@ -89,14 +110,15 @@ def LSQ_PERP(data: list[ExperimentalData], A) -> LeastSquaresOutput:
S_H_error_weight.append(errors[1, 1])
S_M_error_weight.append(errors[2, 2])
+
# Arrayise
S_H = np.array(S_H)
S_M = np.array(S_M)
I_residual = np.array(I_residual)
- I_sim = I_residual.reshape(-1, 1) + response_H * S_H.reshape(-1, 1) + response_M * S_M.reshape(-1, 1)
+ I_sim = I_residual + response_H * S_H + response_M * S_M
- s_q = np.mean(((I - I_sim)/I_stdev)**2, axis=1)
+ s_q = np.mean(((I - I_sim)/I_stdev)**2, axis=0)
sigma_I_res = np.sqrt(np.abs(np.array(I_residual_error_weight) * s_q))
sigma_S_H = np.sqrt(np.abs(np.array(S_H_error_weight) * s_q))
@@ -104,10 +126,12 @@ def LSQ_PERP(data: list[ExperimentalData], A) -> LeastSquaresOutput:
chi_sq = float(np.mean(s_q))
- return LeastSquaresOutput(
+ output_q_values = np.mean(q, axis=0)
+
+ return LeastSquaresOutputPerpendicular(
exchange_A=A,
exchange_A_chi_sq=chi_sq,
- q=np.mean(q, axis=1),
+ q=output_q_values,
I_residual=I_residual,
I_simulated=I_sim,
S_H=S_H,
@@ -119,7 +143,7 @@ def LSQ_PERP(data: list[ExperimentalData], A) -> LeastSquaresOutput:
####################################################################################################
# Sweep over Exchange Stiffness A for perpendicular SANS geometry
-def SweepA_PERP(parameters: FitParameters, data: list[ExperimentalData]):
+def SweepA_PERP(parameters: FitParameters, data: list[ExperimentalData]) -> SweepOutput:
a_values = np.linspace(
parameters.exchange_A_min,
@@ -137,14 +161,6 @@ def SweepA_PERP(parameters: FitParameters, data: list[ExperimentalData]):
exchange_A_chi_sq=chi_sq,
optimal=optimal_fit)
-####################################################################################################
-# find optimal Exchange Stiffness A for perpendicular SANS geometry using fsolve function (slow and very accurate)
-def OptimA_fsolve_PERP(q, I_exp, sigma, M_s, H_0, H_dem, A_initial):
-
- def func(A):
- return chi_q_PERP(q, I_exp, sigma, M_s, H_0, H_dem, A)
-
- return scopt.fsolve(func, A_initial)
####################################################################################################
# find optimal Exchange Stiffness A for perpendicular SANS geometry using
@@ -165,7 +181,7 @@ def OptimA_SPI_PERP(data: list[ExperimentalData], A_1, epsilon):
x_4 = x_3 + 0.5 * ((x_2 - x_3)**2 * (y_3 - y_1) + (x_1 - x_3)**2 * (y_2 - y_3))/((x_2 - x_3) * (y_3 - y_1) + (x_1 - x_3) * (y_2 - y_3))
while np.abs(2 * (x_4 - x_3)/(x_4 + x_3)) > epsilon:
- print("x1,x2,x3,x4:", x_1, x_2, x_3, x_4)
+ # print("x1,x2,x3,x4:", x_1, x_2, x_3, x_4)
x_1 = x_2
x_2 = x_3
@@ -179,20 +195,6 @@ def OptimA_SPI_PERP(data: list[ExperimentalData], A_1, epsilon):
return x_4
-####################################################################################################
-# 1D-Cross-Section of the perendicular model
-def SANS_Model_PERP(q, S_H, S_M, I_res, M_s, H_0, H_dem, A):
-
- # Micromagnetic Model
- mu_0 = 4 * np.pi * 1e-7
- H_i = H_0 - H_dem
- l_H = np.sqrt((2 * A) / (mu_0 * M_s * H_i))
- H_eff = H_i * (1 + (l_H ** 2) * (q ** 2))
- p = M_s / H_eff
- R_H = (p ** 2) / 4 * (2 + 1 / np.sqrt(1 + p))
- R_M = (np.sqrt(1 + p) - 1) / 2
-
- return I_res + R_H * S_H + R_M * S_M
####################################################################################################
# Plot Fitting results of simple fit
@@ -226,7 +228,7 @@ def PlotFittingResultsPERP_SimpleFit(z: SweepOutput, A_Uncertainty,
axes2.set_xlabel('$q$ [1/nm]')
axes2.set_ylabel('$I_{\mathrm{res}}$')
- axes3.plot(q, z.optimal.S_H[0, :], label='fit')
+ axes3.plot(q, z.optimal.S_H, label='fit')
axes3.set_yscale('log')
axes3.set_xscale('log')
axes3.set_xlim([min(q), max(q)])
diff --git a/src/sas/qtgui/Utilities/MuMagTool/least_squares_output.py b/src/sas/qtgui/Utilities/MuMagTool/least_squares_output.py
index ec98183b39..16f48b3ff6 100644
--- a/src/sas/qtgui/Utilities/MuMagTool/least_squares_output.py
+++ b/src/sas/qtgui/Utilities/MuMagTool/least_squares_output.py
@@ -12,7 +12,19 @@ class LeastSquaresOutput:
I_simulated: np.ndarray
I_residual: np.ndarray
S_H: np.ndarray
- S_M: np.ndarray
I_residual_stdev: np.ndarray
S_H_stdev: np.ndarray
- S_M_stdev: np.ndarray
\ No newline at end of file
+
+
+@dataclass
+class LeastSquaresOutputParallel(LeastSquaresOutput):
+ """ Output from least squares method for parallel case"""
+ pass
+
+
+@dataclass
+class LeastSquaresOutputPerpendicular(LeastSquaresOutput):
+ """ Output from least squares method for perpendicular case"""
+ S_M: np.ndarray
+ S_M_stdev: np.ndarray
+
diff --git a/src/sas/qtgui/Utilities/MuMagTool/models.py b/src/sas/qtgui/Utilities/MuMagTool/models.py
index 49c8781764..f1fee8d004 100644
--- a/src/sas/qtgui/Utilities/MuMagTool/models.py
+++ b/src/sas/qtgui/Utilities/MuMagTool/models.py
@@ -1,8 +1,7 @@
+import numpy as np
-####################################################################################################
-# Lorentzian Model for the generation of noisy synthetic test data for perpendicular SANS geometry
def LorentzianNoisyModelPERP(q, A, M_s, H_0, H_dem, a_H, a_M, l_c, beta):
-
+ """ Lorentzian Model for the generation of noisy synthetic test data for perpendicular SANS geometry """
# All inputs in SI-units
# Micromagnetic Model
@@ -34,13 +33,8 @@ def LorentzianNoisyModelPERP(q, A, M_s, H_0, H_dem, a_H, a_M, l_c, beta):
return I_sim, sigma, S_H, S_M, I_res
-
-
-####################################################################################################
-# Functions for the analysis of parallel SANS ######################################################
-####################################################################################################
-# Lorentzian Model for the generation of clean synthetic test data for perpendicular SANS geometry
def LorentzianModelPAR(q, A, M_s, H_0, H_dem, a_H, l_c):
+ """ Lorentzian Model for the generation of clean synthetic test data for parallel SANS geometry"""
# All inputs in SI-units
# Micromagnetic Model
@@ -68,11 +62,8 @@ def LorentzianModelPAR(q, A, M_s, H_0, H_dem, a_H, l_c):
return I_sim, sigma, S_H, I_res
-####################################################################################################
-# Functions for the analysis of perpendicular SANS #################################################
-####################################################################################################
-# Lorentzian Model for the generation of clean synthetic test data for perpendicular SANS geometry
def LorentzianModelPERP(q, A, M_s, H_0, H_dem, a_H, a_M, l_c):
+ """ Lorentzian Model for the generation of clean synthetic test data for perpendicular SANS geometry"""
# All inputs in SI-units
# Micromagnetic Model
@@ -101,3 +92,17 @@ def LorentzianModelPERP(q, A, M_s, H_0, H_dem, a_H, a_M, l_c):
sigma = 0 * I_sim + 1
return I_sim, sigma, S_H, S_M, I_res
+
+def SANS_Model_PERP(q, S_H, S_M, I_res, M_s, H_0, H_dem, A):
+ """ 1D-Cross-Section of the perendicular model """
+
+ # Micromagnetic Model
+ mu_0 = 4 * np.pi * 1e-7
+ H_i = H_0 - H_dem
+ l_H = np.sqrt((2 * A) / (mu_0 * M_s * H_i))
+ H_eff = H_i * (1 + (l_H ** 2) * (q ** 2))
+ p = M_s / H_eff
+ R_H = (p ** 2) / 4 * (2 + 1 / np.sqrt(1 + p))
+ R_M = (np.sqrt(1 + p) - 1) / 2
+
+ return I_res + R_H * S_H + R_M * S_M
diff --git a/src/sas/qtgui/Utilities/MuMagTool/sweep_output.py b/src/sas/qtgui/Utilities/MuMagTool/sweep_output.py
index 2103579976..7b33bb41c3 100644
--- a/src/sas/qtgui/Utilities/MuMagTool/sweep_output.py
+++ b/src/sas/qtgui/Utilities/MuMagTool/sweep_output.py
@@ -2,7 +2,7 @@
import numpy as np
-from sas.qtgui.Utilities.MuMagTool.least_squares_output import LeastSquaresOutput
+from sas.qtgui.Utilities.MuMagTool.least_squares_output import LeastSquaresOutputParallel, LeastSquaresOutputPerpendicular
@dataclass
From 3068a81159cc807d7ac70e47e6618d50bbdd988f Mon Sep 17 00:00:00 2001
From: lucas-wilkins
Date: Fri, 7 Jun 2024 16:36:06 +0100
Subject: [PATCH 32/46] Calculations now working
---
src/sas/qtgui/Utilities/MuMagTool/MuMag.py | 2 +-
src/sas/qtgui/Utilities/MuMagTool/MuMagLib.py | 205 ++++++++++++----
.../Utilities/MuMagTool/MuMagParallelGeo.py | 230 +++++++-----------
.../MuMagTool/MuMagPerpendicularGeo.py | 51 ++--
...1_8000_1600_1.csv => 1_8000_1600_1070.csv} | 0
...10000_1600_1.csv => 2_10000_1600_1070.csv} | 0
...12000_1600_1.csv => 3_12000_1600_1070.csv} | 0
...14000_1600_1.csv => 4_14000_1600_1070.csv} | 0
...16000_1600_1.csv => 5_16000_1600_1070.csv} | 0
src/sas/qtgui/Utilities/MuMagTool/failure.py | 5 +
src/sas/qtgui/Utilities/MuMagTool/models.py | 13 +
.../qtgui/Utilities/MuMagTool/sweep_output.py | 10 +-
12 files changed, 290 insertions(+), 226 deletions(-)
rename src/sas/qtgui/Utilities/MuMagTool/SANSData/NdFeB_parallel_Bick_et_al/{1_8000_1600_1.csv => 1_8000_1600_1070.csv} (100%)
rename src/sas/qtgui/Utilities/MuMagTool/SANSData/NdFeB_parallel_Bick_et_al/{2_10000_1600_1.csv => 2_10000_1600_1070.csv} (100%)
rename src/sas/qtgui/Utilities/MuMagTool/SANSData/NdFeB_parallel_Bick_et_al/{3_12000_1600_1.csv => 3_12000_1600_1070.csv} (100%)
rename src/sas/qtgui/Utilities/MuMagTool/SANSData/NdFeB_parallel_Bick_et_al/{4_14000_1600_1.csv => 4_14000_1600_1070.csv} (100%)
rename src/sas/qtgui/Utilities/MuMagTool/SANSData/NdFeB_parallel_Bick_et_al/{5_16000_1600_1.csv => 5_16000_1600_1070.csv} (100%)
create mode 100644 src/sas/qtgui/Utilities/MuMagTool/failure.py
diff --git a/src/sas/qtgui/Utilities/MuMagTool/MuMag.py b/src/sas/qtgui/Utilities/MuMagTool/MuMag.py
index 5a8da62185..da3c17ea18 100644
--- a/src/sas/qtgui/Utilities/MuMagTool/MuMag.py
+++ b/src/sas/qtgui/Utilities/MuMagTool/MuMag.py
@@ -117,7 +117,7 @@ def simple_fit_button_callback(self):
raise ValueError(f"Unknown Value: {parameters.experiment_geometry}")
- self.MuMagLib_obj.simple_fit_button_callback(
+ self.MuMagLib_obj.do_fit(
parameters,
self.fig,
self.chi_squared_axes,
diff --git a/src/sas/qtgui/Utilities/MuMagTool/MuMagLib.py b/src/sas/qtgui/Utilities/MuMagTool/MuMagLib.py
index 235ab38c82..fb06bf32a6 100644
--- a/src/sas/qtgui/Utilities/MuMagTool/MuMagLib.py
+++ b/src/sas/qtgui/Utilities/MuMagTool/MuMagLib.py
@@ -17,8 +17,13 @@
from sas.qtgui.Utilities.MuMagTool import MuMagParallelGeo
from sas.qtgui.Utilities.MuMagTool import MuMagPerpendicularGeo
+from sas.qtgui.Utilities.MuMagTool.MuMagParallelGeo import LSQ_PAR
+from sas.qtgui.Utilities.MuMagTool.MuMagPerpendicularGeo import LSQ_PERP
from sas.qtgui.Utilities.MuMagTool.experimental_data import ExperimentalData
from sas.qtgui.Utilities.MuMagTool.fit_parameters import FitParameters, ExperimentGeometry
+from sas.qtgui.Utilities.MuMagTool.least_squares_output import LeastSquaresOutputPerpendicular, \
+ LeastSquaresOutputParallel
+from sas.qtgui.Utilities.MuMagTool.sweep_output import SweepOutput
from sasdata.dataloader.loader import Loader
@@ -128,7 +133,8 @@ def plot_exp_data(self, figure, axes):
figure.canvas.draw()
- def simple_fit_button_callback(self, parameters: FitParameters, figure, axes1, axes2, axes3, axes4):
+ # @staticmethod
+ def do_fit(self, parameters: FitParameters, figure, axes1, axes2, axes3, axes4):
if self.input_data is None:
messagebox.showerror(title="Error!",
@@ -152,18 +158,34 @@ def simple_fit_button_callback(self, parameters: FitParameters, figure, axes1, a
case ExperimentGeometry.PERPENDICULAR:
- sweep_data = MuMagPerpendicularGeo.SweepA_PERP(parameters, filtered_inputs)
+ sweep_data = MuMagPerpendicularGeo.sweep(parameters, filtered_inputs)
- A_opt = MuMagPerpendicularGeo.OptimA_SPI_PERP(filtered_inputs, sweep_data.optimal.exchange_A, epsilon=0.0001)
+ crude_A = sweep_data.optimal.exchange_A
+ refined_A = self.refine_exchange_A(filtered_inputs, crude_A, ExperimentGeometry.PERPENDICULAR)
- least_squares_fit_at_optimum = MuMagPerpendicularGeo.LSQ_PERP(filtered_inputs, A_opt)
+ refined = MuMagPerpendicularGeo.LSQ_PERP(filtered_inputs, refined_A)
- I_opt = least_squares_fit_at_optimum.I_simulated
+ uncertainty = self.uncertainty(filtered_inputs, refined_A, ExperimentGeometry.PERPENDICULAR)
- uncertainty = MuMagPerpendicularGeo.uncertainty_perp(filtered_inputs, A_opt)
+ self.plot_results(sweep_data, refined, uncertainty * 1e12, figure, axes1, axes2, axes3, axes4)
- MuMagPerpendicularGeo.PlotFittingResultsPERP_SimpleFit(sweep_data, uncertainty * 1e12,
- figure, axes1, axes2, axes3, axes4)
+
+
+ case ExperimentGeometry.PARALLEL:
+
+
+ sweep_data = MuMagPerpendicularGeo.sweep(parameters, filtered_inputs)
+
+ crude_A = sweep_data.optimal.exchange_A
+ print(crude_A)
+ refined_A = self.refine_exchange_A(filtered_inputs, parameters.exchange_A_min*1e-12, ExperimentGeometry.PARALLEL)
+
+ refined = MuMagPerpendicularGeo.LSQ_PAR(filtered_inputs, refined_A)
+
+ uncertainty = self.uncertainty(filtered_inputs, refined_A, ExperimentGeometry.PARALLEL)
+
+ self.plot_results(sweep_data, refined, uncertainty * 1e12,
+ figure, axes1, axes2, axes3, axes4)
# Save to global Variables
# self.SimpleFit_q_exp = q
@@ -176,55 +198,94 @@ def simple_fit_button_callback(self, parameters: FitParameters, figure, axes1, a
# self.SimpleFit_A = A
# self.SimpleFit_chi_q = chi_q
# self.SimpleFit_S_H_fit = S_H_opt
- # self.SimpleFit_S_M_fit = S_M_opt
# self.SimpleFit_I_res_fit = I_res_opt
# self.SimpleFit_A_opt = A_opt
# self.SimpleFit_chi_q_opt = chi_q_opt
# self.SimpleFit_A_sigma = A_Uncertainty
- # self.SimpleFit_SANSgeometry = "perpendicular"
+ # self.SimpleFit_SANSgeometry = "parallel"
+
+ @staticmethod
+ def refine_exchange_A(
+ data: list[ExperimentalData],
+ exchange_A_initial: float,
+ geometry: ExperimentGeometry,
+ epsilon: float = 0.0001):
+ """ Refines the A parameter using Jarratt's method of successive parabolic interpolation"""
+
+ match geometry:
case ExperimentGeometry.PARALLEL:
+ least_squares_function = LSQ_PAR
+ case ExperimentGeometry.PERPENDICULAR:
+ least_squares_function = LSQ_PERP
+ case _:
+ raise ValueError(f"Unknown experimental geometry: {geometry}")
- A, chi_q, A_opt, chi_q_opt, I_res_opt, S_H_opt, sigma_I_res, sigma_S_H \
- = MuMagParallelGeo.SweepA_PAR(q, I_exp_red, sigma, Ms, H_0, H_dem, A_min, A_max_J, A_N)
+ delta = exchange_A_initial * 0.1
- A_opt = MuMagParallelGeo.OptimA_SPI_PAR(q, I_exp_red, sigma, Ms, H_0, H_dem, A_min, 0.0001)
- chi_q_opt, I_res_opt, S_H_opt, sigma_I_res, sigma_S_H = MuMagParallelGeo.LSQ_PAR(q, I_exp_red, sigma,
- Ms, H_0,
- H_dem,
- A_opt)
+ x_1 = exchange_A_initial - delta
+ x_2 = exchange_A_initial
+ x_3 = exchange_A_initial + delta
- I_opt = MuMagParallelGeo.SANS_Model_PAR(q, S_H_opt, I_res_opt, Ms, H_0, H_dem, A_opt)
+ y_1 = least_squares_function(data, x_1).exchange_A_chi_sq
+ y_2 = least_squares_function(data, x_2).exchange_A_chi_sq
+ y_3 = least_squares_function(data, x_3).exchange_A_chi_sq
- d2chi_dA2 = MuMagParallelGeo.FDM2Ord_PAR(q, I_exp_red, sigma, Ms, H_0, H_dem, A_opt)
- n_field_strengths = len(I_exp_red[0, :])
- n_q = len(I_exp_red[:, 0])
- A_Uncertainty = np.sqrt(2 / (n_field_strengths * n_q * d2chi_dA2))
+ x_4 = x_3 + 0.5 * ((x_2 - x_3) ** 2 * (y_3 - y_1) + (x_1 - x_3) ** 2 * (y_2 - y_3)) \
+ / ((x_2 - x_3) * (y_3 - y_1) + (x_1 - x_3) * (y_2 - y_3))
- MuMagParallelGeo.PlotFittingResultsPAR_SimpleFit(q, A, chi_q, A_opt, chi_q_opt,
- I_res_opt, S_H_opt, A_Uncertainty * 1e12,
- figure, axes1, axes2, axes3, axes4)
+ for i in range(200):
+ if np.abs(2 * (x_4 - x_3) / (x_4 + x_3)) < epsilon:
+ break
+
+ x_1, x_2, x_3 = x_2, x_3, x_4
+ y_1, y_2, y_3 = y_2, y_3, least_squares_function(data, x_3).exchange_A_chi_sq
+
+ x_4 = x_3 + 0.5 * ((x_2 - x_3) ** 2 * (y_3 - y_1) + (x_1 - x_3) ** 2 * (y_2 - y_3)) \
+ / ((x_2 - x_3) * (y_3 - y_1) + (x_1 - x_3) * (y_2 - y_3))
+
+ return x_4
+
+ @staticmethod
+ def uncertainty(
+ data: list[ExperimentalData],
+ A_opt: float,
+ geometry: ExperimentGeometry):
+ """Calculate the uncertainty for the optimal exchange stiffness A"""
+
+ # Estimate variance from second order derivative of chi-square function via Finite Differences
+
+ match geometry:
+ case ExperimentGeometry.PARALLEL:
+ least_squares_function = LSQ_PAR
+ case ExperimentGeometry.PERPENDICULAR:
+ least_squares_function = LSQ_PERP
+ case _:
+ raise ValueError(f"Unknown experimental geometry: {geometry}")
+
+ p = 0.001 # fractional gap size for finite differences
+ dA = A_opt * p
+ A1 = A_opt - 2 * dA
+ A2 = A_opt - 1 * dA
+ A3 = A_opt
+ A4 = A_opt + 1 * dA
+ A5 = A_opt + 2 * dA
+
+ chi1 = least_squares_function(data, A1).exchange_A_chi_sq
+ chi2 = least_squares_function(data, A2).exchange_A_chi_sq
+ chi3 = least_squares_function(data, A3).exchange_A_chi_sq
+ chi4 = least_squares_function(data, A4).exchange_A_chi_sq
+ chi5 = least_squares_function(data, A5).exchange_A_chi_sq
+
+ d2chi_dA2 = (-chi1 + 16 * chi2 - 30 * chi3 + 16 * chi4 - chi5) / (12 * dA ** 2)
+
+ # Scale variance by number of samples and return reciprocal square root
+
+ n_field_strengths = len(data) # Number of fields
+ n_q = len(data[0].scattering_curve.x) # Number of q points
+
+ return np.sqrt(2 / (n_field_strengths * n_q * d2chi_dA2))
- # Save to global Variables
- self.SimpleFit_q_exp = q
- self.SimpleFit_I_exp = I_exp_red
- self.SimpleFit_sigma_exp = sigma
- self.SimpleFit_B_0_exp = self.B_0_exp[K_H:]
- self.SimpleFit_Ms_exp = self.Ms_exp[K_H:]
- self.SimpleFit_Hdem_exp = self.Hdem_exp[K_H:]
- self.SimpleFit_I_fit = I_opt
- self.SimpleFit_A = A
- self.SimpleFit_chi_q = chi_q
- self.SimpleFit_S_H_fit = S_H_opt
- self.SimpleFit_I_res_fit = I_res_opt
- self.SimpleFit_A_opt = A_opt
- self.SimpleFit_chi_q_opt = chi_q_opt
- self.SimpleFit_A_sigma = A_Uncertainty
- self.SimpleFit_SANSgeometry = "parallel"
-
-
-
-######################################################################################################################
def save_button_callback(self):
SimpleFit_q_exp = self.SimpleFit_q_exp
@@ -355,8 +416,60 @@ def save_button_callback(self):
else:
messagebox.showerror(title="Error!", message="No SimpleFit results available!")
+ @staticmethod
+ def plot_results(sweep_data: SweepOutput,
+ refined: LeastSquaresOutputPerpendicular | LeastSquaresOutputParallel,
+ A_Uncertainty,
+ figure, axes1, axes2, axes3, axes4):
+
+ if A_Uncertainty < 1e-4:
+ A_Uncertainty = 0
+
+ A_uncertainty_str = str(A_Uncertainty)
+ A_opt_str = str(refined.exchange_A * 1e12)
+
+ q = refined.q * 1e-9
+
+ # Plot A search data
+
+ axes1.set_title('$A_{\mathrm{opt}} = (' + A_opt_str[0:5] + ' \pm ' + A_uncertainty_str[0:4] + ')$ pJ/m')
+ axes1.plot(sweep_data.exchange_A_checked * 1e12, sweep_data.exchange_A_chi_sq)
+ axes1.plot(sweep_data.optimal.exchange_A * 1e12, sweep_data.optimal.exchange_A_chi_sq, 'o')
+
+ axes1.set_xlim([min(sweep_data.exchange_A_checked * 1e12), max(sweep_data.exchange_A_checked * 1e12)])
+ axes1.set_xlabel('$A$ [pJ/m]')
+ axes1.set_ylabel('$\chi^2$')
+
+ # Residual intensity plot
+
+ axes2.plot(q, refined.I_residual, label='fit')
+ axes2.set_yscale('log')
+ axes2.set_xscale('log')
+ axes2.set_xlim([min(q), max(q)])
+ axes2.set_xlabel('$q$ [1/nm]')
+ axes2.set_ylabel('$I_{\mathrm{res}}$')
+
+ # S_H parameter
+
+ axes3.plot(q, refined.S_H, label='fit')
+ axes3.set_yscale('log')
+ axes3.set_xscale('log')
+ axes3.set_xlim([min(q), max(q)])
+ axes3.set_xlabel('$q$ [1/nm]')
+ axes3.set_ylabel('$S_H$')
+
+ # S_M parameter
+ if isinstance(refined, LeastSquaresOutputPerpendicular):
+ axes4.plot(q, refined.S_M, label='fit')
+ axes4.set_yscale('log')
+ axes4.set_xscale('log')
+ axes4.set_xlim([min(q), max(q)])
+ axes4.set_xlabel('$q$ [1/nm]')
+ axes4.set_ylabel('$S_M$')
+
+ figure.tight_layout()
-#################################################################################################################
+ #################################################################################################################
def SimpleFit_CompareButtonCallback(self, figure, axes):
diff --git a/src/sas/qtgui/Utilities/MuMagTool/MuMagParallelGeo.py b/src/sas/qtgui/Utilities/MuMagTool/MuMagParallelGeo.py
index eb14e30e01..8ce7fbb537 100644
--- a/src/sas/qtgui/Utilities/MuMagTool/MuMagParallelGeo.py
+++ b/src/sas/qtgui/Utilities/MuMagTool/MuMagParallelGeo.py
@@ -2,185 +2,131 @@
import scipy.optimize as scopt
import matplotlib.pyplot as plt
+from sas.qtgui.Utilities.MuMagTool.experimental_data import ExperimentalData
+from sas.qtgui.Utilities.MuMagTool.fit_parameters import ExperimentGeometry
+from sas.qtgui.Utilities.MuMagTool.least_squares_output import LeastSquaresOutputParallel, \
+ LeastSquaresOutputPerpendicular
+from sas.qtgui.Utilities.MuMagTool.sweep_output import SweepOutput
-####################################################################################################
-# Least squares method for parallel SANS geometry
-def LSQ_PAR(q, I_exp, sigma, M_s, H_0, H_dem, A):
+mu_0 = 4 * np.pi * 1e-7
- # Micromagnetic Model
- mu_0 = 4 * np.pi * 1e-7
- H_i = H_0 - H_dem
- l_H = np.sqrt((2 * A) / (mu_0 * M_s * H_i))
- H_eff = H_i * (1 + (l_H ** 2) * (q ** 2))
- p = M_s / H_eff
- R_H = (p ** 2) / 2
-
- # Non-negative least squares loop
- L_nu = len(q[1, :])
- S_H = np.zeros((1, L_nu))
- I_res = np.zeros((1, L_nu))
- sigma_S_H = np.zeros((1, L_nu))
- sigma_I_res = np.zeros((1, L_nu))
- for nu in np.arange(0, L_nu):
- A = np.array([1/sigma[:, nu], R_H[:, nu]/sigma[:, nu]]).T
- y = I_exp[:, nu]/sigma[:, nu]
- c = np.matmul(A.T, y)
- H = np.dot(A.T, A)
+def LSQ_PAR(data: list[ExperimentalData], A):
- # non-negative linear least squares
- x = scopt.nnls(H, c)
- I_res[0, nu] = x[0][0]
- S_H[0, nu] = x[0][1]
+ """ Least squares fitting for a given exchange stiffness, A, parallel case
- Gamma = np.linalg.inv(np.dot(H.T, H))
- sigma_I_res[0, nu] = Gamma[0, 0]
- sigma_S_H[0, nu] = Gamma[1, 1]
+ We are fitting the equation:
- I_sim = I_res + R_H * S_H
- s_q = np.mean(((I_exp - I_sim)/sigma)**2, axis=0)
+ I_sim = I_res + response_H * S_H
+ = (I_res, S_H) . (1, response_H)
+ = (I_res, S_H) . least_squares_x
- sigma_S_H = np.sqrt(np.abs(sigma_S_H * s_q))
- sigma_I_res = np.sqrt(np.abs(sigma_I_res * s_q))
+ finding I_res and S_H for each q value
- chi_q = np.mean(s_q)
- return chi_q, I_res, S_H, sigma_I_res, sigma_S_H
+ """
-####################################################################################################
-# Least squares method for parallel SANS geometry
-def chi_q_PAR(q, I_exp, sigma, M_s, H_0, H_dem, A):
+ # Get matrices from the input data
+ n_data = len(data)
+
+ # Factor of (1e-3 / mu_0) converts from mT to A/m
+ applied_field = np.array([datum.applied_field for datum in data]) * (1e-3 / mu_0)
+ demagnetising_field = np.array([datum.demagnetising_field for datum in data]) * (1e-3 / mu_0)
+ saturation_magnetisation = np.array([datum.saturation_magnetisation for datum in data]) * (1e-3 / mu_0)
+
+ # TODO: The following is how things should be done in the future, rather than hard-coding
+ # a scaling factor...
+ # def data_nanometers(data: Data1D):
+ # raw_data = data.x
+ # units = data.x_unit
+ # converter = Converter(units)
+ # return converter.scale("1/m", raw_data)
+ #
+ # q = np.array([data_nanometers(datum.scattering_curve) for datum in data])
+
+ q = np.array([datum.scattering_curve.x for datum in data]) * 1e9
+ I = np.array([datum.scattering_curve.y for datum in data])
+ I_stdev = np.array([datum.scattering_curve.dy for datum in data])
+
+ n_q = q.shape[1]
# Micromagnetic Model
- mu_0 = 4 * np.pi * 1e-7
- H_i = H_0 - H_dem
- l_H = np.sqrt((2 * A) / (mu_0 * M_s * H_i))
- H_eff = H_i * (1 + (l_H ** 2) * (q ** 2))
- p = M_s / H_eff
- R_H = (p ** 2) / 2
-
- # Non-negative least squares loop
- L_nu = len(q[0, :])
- S_H = np.zeros((1, L_nu))
- I_res = np.zeros((1, L_nu))
- for nu in np.arange(0, L_nu):
- A = np.array([1/sigma[:, nu], R_H[:, nu]/sigma[:, nu]]).T
- y = I_exp[:, nu]/sigma[:, nu]
-
- c = np.matmul(A.T, y)
- H = np.dot(A.T, A)
+ internal_field = (applied_field - demagnetising_field).reshape(-1, 1)
+ magnetic_scattering_length = np.sqrt((2 * A) / (mu_0 * saturation_magnetisation.reshape(-1, 1) * internal_field))
+ effective_field = internal_field * (1 + (magnetic_scattering_length ** 2) * (q ** 2))
- # non-negative linear least squares
- x = scopt.nnls(H, c)
- I_res[0, nu] = x[0][0]
- S_H[0, nu] = x[0][1]
+ # Calculate the response functions
+ p = saturation_magnetisation.reshape(-1, 1) / effective_field
+ response_H = (p ** 2) / 2
- I_sim = I_res + R_H * S_H
- s_q = np.mean(((I_exp - I_sim)/sigma)**2, axis=0)
- chi_q = np.mean(s_q)
+ # print("Input", q.shape, q[0,10])
+ # sys.exit()
- return chi_q
+ # Lists for output of calculation
+ I_residual = []
+ S_H = []
-####################################################################################################
-# Sweep over Exchange Stiffness A for parallel SANS geometry
-def SweepA_PAR(q, I_exp, sigma, M_s, H_0, H_dem, A_1, A_2, A_N):
+ I_residual_error_weight = []
+ S_H_error_weight = []
- A = np.linspace(A_1, A_2, A_N) * 1e-12 # pJ/m -> J/m
- chi_q = np.zeros(len(A))
- for k in np.arange(0, len(A)):
- chi_q[k], I_res, S_H, sigma_I_res, sigma_S_H = LSQ_PAR(q, I_exp, sigma, M_s, H_0, H_dem, A[k])
+ for nu in range(n_q):
- min_idx = np.argmin(chi_q)
- A_opt = A[min_idx]
+ # non-negative linear least squares
+ least_squares_x = (np.array([np.ones((n_data,)), response_H[:, nu]]) / I_stdev[:, nu]).T
+ least_squares_y = I[:, nu] / I_stdev[:, nu]
- chi_q_opt, I_res_opt, S_H_opt, sigma_I_res, sigma_S_H = LSQ_PAR(q, I_exp, sigma, M_s, H_0, H_dem, A_opt)
+ least_squares_x_squared = np.dot(least_squares_x.T, least_squares_x)
- return A, chi_q, A_opt, chi_q_opt, I_res_opt, S_H_opt, sigma_I_res, sigma_S_H
+ # Non-negative least squares
+ try:
+ fit_result = scopt.nnls(
+ least_squares_x_squared,
+ np.matmul(least_squares_x.T, least_squares_y))
-####################################################################################################
-# find optimal Exchange Stiffness A for parallel SANS geometry using fsolve function (slow and very accurate)
-def OptimA_fsolve_PAR(q, I_exp, sigma, M_s, H_0, H_dem, A_initial):
+ except ValueError as ve:
+ print("Value Error:")
+ print(" A =", A)
- def func(A):
- return chi_q_PAR(q, I_exp, sigma, M_s, H_0, H_dem, A)
+ raise ve
- return scopt.fsolve(func, A_initial)
+ I_residual.append(fit_result[0][0])
+ S_H.append(fit_result[0][1])
-####################################################################################################
-# find optimal Exchange Stiffness A for parallel SANS geometry using successive parabolic interpolation (fast and accurate)
-# implemented as Jarratt's Method
-def OptimA_SPI_PAR(q, I_exp, sigma, M_s, H_0, H_dem, A_1, eps):
+ errors = np.linalg.inv(np.dot(least_squares_x_squared.T, least_squares_x_squared))
- delta = A_1 * 0.1
+ I_residual_error_weight.append(errors[0, 0])
+ S_H_error_weight.append(errors[1, 1])
- x_1 = A_1 - delta
- x_2 = A_1
- x_3 = A_1 + delta
+ # Arrayise
+ S_H = np.array(S_H)
+ I_residual = np.array(I_residual)
- y_1 = chi_q_PAR(q, I_exp, sigma, M_s, H_0, H_dem, x_1)
- y_2 = chi_q_PAR(q, I_exp, sigma, M_s, H_0, H_dem, x_2)
- y_3 = chi_q_PAR(q, I_exp, sigma, M_s, H_0, H_dem, x_3)
+ I_sim = I_residual + response_H * S_H
- x_4 = x_3 + 0.5 * ((x_2 - x_3)**2 * (y_3 - y_1) + (x_1 - x_3)**2 * (y_2 - y_3))/((x_2 - x_3) * (y_3 - y_1) + (x_1 - x_3) * (y_2 - y_3))
- while np.abs(2 * (x_4 - x_3)/(x_4 + x_3)) > eps:
+ s_q = np.mean(((I - I_sim) / I_stdev) ** 2, axis=0)
- x_1 = x_2
- x_2 = x_3
- x_3 = x_4
+ sigma_I_res = np.sqrt(np.abs(np.array(I_residual_error_weight) * s_q))
+ sigma_S_H = np.sqrt(np.abs(np.array(S_H_error_weight) * s_q))
- y_1 = y_2
- y_2 = y_3
- y_3 = chi_q_PAR(q, I_exp, sigma, M_s, H_0, H_dem, x_3)
+ chi_sq = float(np.mean(s_q))
- x_4 = x_3 + 0.5 * ((x_2 - x_3) ** 2 * (y_3 - y_1) + (x_1 - x_3) ** 2 * (y_2 - y_3)) / ((x_2 - x_3) * (y_3 - y_1) + (x_1 - x_3) * (y_2 - y_3))
+ output_q_values = np.mean(q, axis=0)
- return x_4
+ return LeastSquaresOutputParallel(
+ exchange_A=A,
+ exchange_A_chi_sq=chi_sq,
+ q=output_q_values,
+ I_residual=I_residual,
+ I_simulated=I_sim,
+ S_H=S_H,
+ I_residual_stdev=sigma_I_res,
+ S_H_stdev=sigma_S_H)
-####################################################################################################
-# 1D-Cross-Section of the parallel model
-def SANS_Model_PAR(q, S_H, I_res, M_s, H_0, H_dem, A):
- # Micromagnetic Model
- mu_0 = 4 * np.pi * 1e-7
- H_i = H_0 - H_dem
- l_H = np.sqrt((2 * A) / (mu_0 * M_s * H_i))
- H_eff = H_i * (1 + (l_H ** 2) * (q ** 2))
- p = M_s / H_eff
- R_H = (p ** 2) / 2
+####################################################################################################
- return I_res + R_H * S_H
####################################################################################################
# Plot Fitting results of simple fit
-def PlotFittingResultsPAR_SimpleFit(q, A, chi_q, A_opt, chi_q_opt, I_res_opt, S_H_opt, A_Uncertainty,
- figure, axes1, axes2, axes3, axes4):
-
- if A_Uncertainty < 1e-4:
- A_Uncertainty = 0
-
- A_uncertainty_str = str(A_Uncertainty)
- A_opt_str = str(A_opt * 1e12)
- axes1.set_title('$A_{\mathrm{opt}} = (' + A_opt_str[0:5] + ' \pm ' + A_uncertainty_str[0:4] + ')$ pJ/m')
- axes1.plot(A * 1e12, chi_q)
- axes1.plot(A_opt * 1e12, chi_q_opt, 'o')
- axes1.set_xlim([min(A * 1e12), max(A * 1e12)])
- axes1.set_xlabel('$A$ [pJ/m]')
- axes1.set_ylabel('$\chi^2$')
-
- axes2.plot(q[0, :] * 1e-9, I_res_opt[0, :], label='fit')
- axes2.set_yscale('log')
- axes2.set_xscale('log')
- axes2.set_xlim([min(q[0, :] * 1e-9), max(q[0, :] * 1e-9)])
- axes2.set_xlabel('$q$ [1/nm]')
- axes2.set_ylabel('$I_{\mathrm{res}}$')
-
- axes3.plot(q[0, :] * 1e-9, S_H_opt[0, :], label='fit')
- axes3.set_yscale('log')
- axes3.set_xscale('log')
- axes3.set_xlim([min(q[0, :] * 1e-9), max(q[0, :] * 1e-9)])
- axes3.set_xlabel('$q$ [1/nm]')
- axes3.set_ylabel('$S_H$')
-
- figure.tight_layout()
####################################################################################################
def PlotSweepFitResultPAR(q_max_mat, H_min_mat, A_opt_mat):
diff --git a/src/sas/qtgui/Utilities/MuMagTool/MuMagPerpendicularGeo.py b/src/sas/qtgui/Utilities/MuMagTool/MuMagPerpendicularGeo.py
index 45778671a9..0ad250d1de 100644
--- a/src/sas/qtgui/Utilities/MuMagTool/MuMagPerpendicularGeo.py
+++ b/src/sas/qtgui/Utilities/MuMagTool/MuMagPerpendicularGeo.py
@@ -4,18 +4,19 @@
import scipy.optimize as scopt
import matplotlib.pyplot as plt
+from sas.qtgui.Utilities.MuMagTool.MuMagParallelGeo import LSQ_PAR
from sasdata.dataloader import Data1D
from data_util.nxsunit import Converter
from sas.qtgui.Utilities.MuMagTool.experimental_data import ExperimentalData
-from sas.qtgui.Utilities.MuMagTool.fit_parameters import FitParameters
+from sas.qtgui.Utilities.MuMagTool.fit_parameters import FitParameters, ExperimentGeometry
from sas.qtgui.Utilities.MuMagTool.fit_result import FitResults
from sas.qtgui.Utilities.MuMagTool.least_squares_output import LeastSquaresOutput, LeastSquaresOutputPerpendicular
from sas.qtgui.Utilities.MuMagTool.sweep_output import SweepOutput
mu_0 = 4 * np.pi * 1e-7
-def LSQ_PERP(data: list[ExperimentalData], A) -> LeastSquaresOutput:
- """ Least squares fitting for a given exchange stiffness, A
+def LSQ_PERP(data: list[ExperimentalData], A) -> LeastSquaresOutputPerpendicular:
+ """ Least squares fitting for a given exchange stiffness, A, perpendicular case
We are fitting the equation:
@@ -142,15 +143,24 @@ def LSQ_PERP(data: list[ExperimentalData], A) -> LeastSquaresOutput:
####################################################################################################
-# Sweep over Exchange Stiffness A for perpendicular SANS geometry
-def SweepA_PERP(parameters: FitParameters, data: list[ExperimentalData]) -> SweepOutput:
+#
+def sweep(parameters: FitParameters, data: list[ExperimentalData]) -> SweepOutput:
+ """ Sweep over Exchange Stiffness A for perpendicular SANS geometry to
+ get an initial estimate which can then be refined"""
a_values = np.linspace(
parameters.exchange_A_min,
parameters.exchange_A_max,
parameters.exchange_A_n) * 1e-12 # From pJ/m to J/m
- least_squared_fits = [LSQ_PERP(data, a) for a in a_values]
+ if parameters.experiment_geometry == ExperimentGeometry.PERPENDICULAR:
+ least_squared_fits = [LSQ_PERP(data, a) for a in a_values]
+
+ elif parameters.experiment_geometry == ExperimentGeometry.PARALLEL:
+ least_squared_fits = [LSQ_PAR(data, a) for a in a_values]
+
+ else:
+ raise ValueError(f"Unknown ExperimentGeometry value: {parameters.experiment_geometry}")
optimal_fit = min(least_squared_fits, key=lambda x: x.exchange_A_chi_sq)
@@ -198,7 +208,7 @@ def OptimA_SPI_PERP(data: list[ExperimentalData], A_1, epsilon):
####################################################################################################
# Plot Fitting results of simple fit
-def PlotFittingResultsPERP_SimpleFit(z: SweepOutput, A_Uncertainty,
+def PlotFittingResultsPERP_SimpleFit(z: SweepOutput, refined: LeastSquaresOutputPerpendicular, A_Uncertainty,
figure, axes1, axes2, axes3, axes4):
if A_Uncertainty < 1e-4:
@@ -258,30 +268,3 @@ def PlotSweepFitResultPERP(q_max_mat, H_min_mat, A_opt_mat):
####################################################################################################
#
-def uncertainty_perp(data: list[ExperimentalData], A_opt: float):
- """Calculate the uncertainty for the optimal exchange stiffness A"""
-
- # Estimate variance from second order derivative of chi-square function via Finite Differences
-
- p = 0.001 # fractional gap size for finite differences
- dA = A_opt * p
- A1 = A_opt - 2*dA
- A2 = A_opt - 1*dA
- A3 = A_opt
- A4 = A_opt + 1*dA
- A5 = A_opt + 2*dA
-
- chi1 = LSQ_PERP(data, A1).exchange_A_chi_sq
- chi2 = LSQ_PERP(data, A2).exchange_A_chi_sq
- chi3 = LSQ_PERP(data, A3).exchange_A_chi_sq
- chi4 = LSQ_PERP(data, A4).exchange_A_chi_sq
- chi5 = LSQ_PERP(data, A5).exchange_A_chi_sq
-
- d2chi_dA2 = (-chi1 + 16 * chi2 - 30 * chi3 + 16 * chi4 - chi5)/(12 * dA**2)
-
- # Scale variance by number of samples and return reciprocal square root
-
- n_field_strengths = len(data) # Number of fields
- n_q = len(data[0].scattering_curve.x) # Number of q points
-
- return np.sqrt(2 / (n_field_strengths * n_q * d2chi_dA2))
diff --git a/src/sas/qtgui/Utilities/MuMagTool/SANSData/NdFeB_parallel_Bick_et_al/1_8000_1600_1.csv b/src/sas/qtgui/Utilities/MuMagTool/SANSData/NdFeB_parallel_Bick_et_al/1_8000_1600_1070.csv
similarity index 100%
rename from src/sas/qtgui/Utilities/MuMagTool/SANSData/NdFeB_parallel_Bick_et_al/1_8000_1600_1.csv
rename to src/sas/qtgui/Utilities/MuMagTool/SANSData/NdFeB_parallel_Bick_et_al/1_8000_1600_1070.csv
diff --git a/src/sas/qtgui/Utilities/MuMagTool/SANSData/NdFeB_parallel_Bick_et_al/2_10000_1600_1.csv b/src/sas/qtgui/Utilities/MuMagTool/SANSData/NdFeB_parallel_Bick_et_al/2_10000_1600_1070.csv
similarity index 100%
rename from src/sas/qtgui/Utilities/MuMagTool/SANSData/NdFeB_parallel_Bick_et_al/2_10000_1600_1.csv
rename to src/sas/qtgui/Utilities/MuMagTool/SANSData/NdFeB_parallel_Bick_et_al/2_10000_1600_1070.csv
diff --git a/src/sas/qtgui/Utilities/MuMagTool/SANSData/NdFeB_parallel_Bick_et_al/3_12000_1600_1.csv b/src/sas/qtgui/Utilities/MuMagTool/SANSData/NdFeB_parallel_Bick_et_al/3_12000_1600_1070.csv
similarity index 100%
rename from src/sas/qtgui/Utilities/MuMagTool/SANSData/NdFeB_parallel_Bick_et_al/3_12000_1600_1.csv
rename to src/sas/qtgui/Utilities/MuMagTool/SANSData/NdFeB_parallel_Bick_et_al/3_12000_1600_1070.csv
diff --git a/src/sas/qtgui/Utilities/MuMagTool/SANSData/NdFeB_parallel_Bick_et_al/4_14000_1600_1.csv b/src/sas/qtgui/Utilities/MuMagTool/SANSData/NdFeB_parallel_Bick_et_al/4_14000_1600_1070.csv
similarity index 100%
rename from src/sas/qtgui/Utilities/MuMagTool/SANSData/NdFeB_parallel_Bick_et_al/4_14000_1600_1.csv
rename to src/sas/qtgui/Utilities/MuMagTool/SANSData/NdFeB_parallel_Bick_et_al/4_14000_1600_1070.csv
diff --git a/src/sas/qtgui/Utilities/MuMagTool/SANSData/NdFeB_parallel_Bick_et_al/5_16000_1600_1.csv b/src/sas/qtgui/Utilities/MuMagTool/SANSData/NdFeB_parallel_Bick_et_al/5_16000_1600_1070.csv
similarity index 100%
rename from src/sas/qtgui/Utilities/MuMagTool/SANSData/NdFeB_parallel_Bick_et_al/5_16000_1600_1.csv
rename to src/sas/qtgui/Utilities/MuMagTool/SANSData/NdFeB_parallel_Bick_et_al/5_16000_1600_1070.csv
diff --git a/src/sas/qtgui/Utilities/MuMagTool/failure.py b/src/sas/qtgui/Utilities/MuMagTool/failure.py
new file mode 100644
index 0000000000..55a36e635e
--- /dev/null
+++ b/src/sas/qtgui/Utilities/MuMagTool/failure.py
@@ -0,0 +1,5 @@
+class FitFailure(Exception):
+ pass
+
+class LoadFailure(Exception):
+ pass
\ No newline at end of file
diff --git a/src/sas/qtgui/Utilities/MuMagTool/models.py b/src/sas/qtgui/Utilities/MuMagTool/models.py
index f1fee8d004..548f22ce2b 100644
--- a/src/sas/qtgui/Utilities/MuMagTool/models.py
+++ b/src/sas/qtgui/Utilities/MuMagTool/models.py
@@ -106,3 +106,16 @@ def SANS_Model_PERP(q, S_H, S_M, I_res, M_s, H_0, H_dem, A):
R_M = (np.sqrt(1 + p) - 1) / 2
return I_res + R_H * S_H + R_M * S_M
+
+def SANS_Model_PAR(q, S_H, I_res, M_s, H_0, H_dem, A):
+ """ 1D-Cross-Section of the parallel model"""
+
+ # Micromagnetic Model
+ mu_0 = 4 * np.pi * 1e-7
+ H_i = H_0 - H_dem
+ l_H = np.sqrt((2 * A) / (mu_0 * M_s * H_i))
+ H_eff = H_i * (1 + (l_H ** 2) * (q ** 2))
+ p = M_s / H_eff
+ R_H = (p ** 2) / 2
+
+ return I_res + R_H * S_H
\ No newline at end of file
diff --git a/src/sas/qtgui/Utilities/MuMagTool/sweep_output.py b/src/sas/qtgui/Utilities/MuMagTool/sweep_output.py
index 7b33bb41c3..e0644b0365 100644
--- a/src/sas/qtgui/Utilities/MuMagTool/sweep_output.py
+++ b/src/sas/qtgui/Utilities/MuMagTool/sweep_output.py
@@ -2,15 +2,19 @@
import numpy as np
-from sas.qtgui.Utilities.MuMagTool.least_squares_output import LeastSquaresOutputParallel, LeastSquaresOutputPerpendicular
+from sas.qtgui.Utilities.MuMagTool.least_squares_output import LeastSquaresOutput
+from typing import Generic, TypeVar
+
+T = TypeVar("T", bound=LeastSquaresOutput)
@dataclass
-class SweepOutput:
+class SweepOutput(Generic[T]):
"""
Results from brute force optimisiation of the chi squared for the exchange A parameter
"""
exchange_A_checked: np.ndarray
exchange_A_chi_sq: np.ndarray
- optimal: LeastSquaresOutput
\ No newline at end of file
+ optimal: T
+
From 384a5da28edb895c7df5bbb98dbdb32ce9427224 Mon Sep 17 00:00:00 2001
From: lucas-wilkins
Date: Mon, 10 Jun 2024 13:16:09 +0100
Subject: [PATCH 33/46] Some refactoring
---
src/sas/qtgui/Utilities/MuMagTool/MuMag.py | 125 ++++++++++++++----
src/sas/qtgui/Utilities/MuMagTool/MuMagLib.py | 104 +++++----------
.../Utilities/MuMagTool/SANSData/__init__.py | 0
.../qtgui/Utilities/MuMagTool/UI/MuMagUI.ui | 29 ++--
.../qtgui/Utilities/MuMagTool/UI/__init__.py | 0
src/sas/qtgui/Utilities/MuMagTool/__init__.py | 0
src/sas/qtgui/Utilities/MuMagTool/failure.py | 2 +
7 files changed, 145 insertions(+), 115 deletions(-)
create mode 100644 src/sas/qtgui/Utilities/MuMagTool/SANSData/__init__.py
create mode 100644 src/sas/qtgui/Utilities/MuMagTool/UI/__init__.py
create mode 100644 src/sas/qtgui/Utilities/MuMagTool/__init__.py
diff --git a/src/sas/qtgui/Utilities/MuMagTool/MuMag.py b/src/sas/qtgui/Utilities/MuMagTool/MuMag.py
index da3c17ea18..b9200d6a4b 100644
--- a/src/sas/qtgui/Utilities/MuMagTool/MuMag.py
+++ b/src/sas/qtgui/Utilities/MuMagTool/MuMag.py
@@ -10,60 +10,127 @@
import matplotlib.pylab as pl
import numpy as np
+from sas.qtgui.Utilities.MuMagTool.experimental_data import ExperimentalData
+from sas.qtgui.Utilities.MuMagTool.failure import LoadFailure
from sas.qtgui.Utilities.MuMagTool.fit_parameters import FitParameters, ExperimentGeometry
+from sas.qtgui.Utilities.MuMagTool.MuMagLib import MuMagLib
+from logging import getLogger
+
+log = getLogger("MuMag")
class MuMag(QtWidgets.QMainWindow, Ui_MuMagTool):
def __init__(self, parent=None):
super().__init__()
- self.setupUi(self)
- self.MuMagLib_obj = MuMagLib.MuMagLib()
+ self.setupUi(self)
# Callbacks
- self.ImportDataButton.clicked.connect(self.import_data_button_callback)
- self.PlotDataButton.clicked.connect(self.plot_experimental_data_button_callback)
+ self.ImportDataButton.clicked.connect(self.importData)
self.SimpleFitButton.clicked.connect(self.simple_fit_button_callback)
self.CompareResultsButton.clicked.connect(self.compare_data_button_callback)
self.SaveResultsButton.clicked.connect(self.save_data_button_callback)
- # Plotting
- layout = QVBoxLayout()
- self.PlotDisplayPanel.setLayout(layout)
+ #
+ # Data
+ #
+
+ self.data: list[ExperimentalData] | None = None
- self.fig = plt.figure() #Figure(figsize=(width, height), dpi=dpi)
- self.simple_fit_axes = self.fig.add_subplot(1, 1, 1)
- self.simple_fit_axes.set_visible(False)
- self.chi_squared_axes = self.fig.add_subplot(2, 2, 1)
+ #
+ # Plotting
+ #
+
+ # Data
+ data_plot_layout = QVBoxLayout()
+ self.data_tab.setLayout(data_plot_layout)
+ self.data_figure = plt.figure() #Figure(figsize=(width, height), dpi=dpi)
+ self.figure_canvas = FigureCanvas(self.data_figure)
+ self.data_axes = self.data_figure.add_subplot(1, 1, 1)
+ self.data_axes.set_visible(False)
+ data_plot_layout.addWidget(self.figure_canvas)
+
+ # Fit results
+ fit_results_layout = QVBoxLayout()
+ self.fit_results_tab.setLayout(fit_results_layout)
+ self.fit_results_figure = plt.figure()
+ self.fit_results_canvas = FigureCanvas(self.fit_results_figure)
+
+ self.chi_squared_axes = self.fit_results_figure.add_subplot(2, 2, 1)
self.chi_squared_axes.set_visible(False)
- self.residuals_axes = self.fig.add_subplot(2, 2, 2)
+ self.residuals_axes = self.fit_results_figure.add_subplot(2, 2, 2)
self.residuals_axes.set_visible(False)
- self.s_h_axes = self.fig.add_subplot(2, 2, 3)
+ self.s_h_axes = self.fit_results_figure.add_subplot(2, 2, 3)
self.s_h_axes.set_visible(False)
- self.longitudinal_scattering_axes = self.fig.add_subplot(2, 2, 4)
+ self.longitudinal_scattering_axes = self.fit_results_figure.add_subplot(2, 2, 4)
self.longitudinal_scattering_axes.set_visible(False)
- self.figure_canvas = FigureCanvas(self.fig)
- layout.addWidget(self.figure_canvas)
+ fit_results_layout.addWidget(self.fit_results_canvas)
+
+ def importData(self):
+ """ Callback for the import data button """
+
+ # Get the directory from the user
+ directory = MuMagLib.directory_popup()
+
+ if directory is None:
+ log.info("No directory selected")
+ return
+
+ try:
+ self.data = MuMagLib.import_data(directory)
+ except LoadFailure as lf:
+ log.error(repr(lf))
+ return
+
+ log.info(f"Loaded {len(self.data)} datasets")
+ self.plot_data()
+ self.data_figure.canvas.draw()
- def import_data_button_callback(self):
- self.MuMagLib_obj.import_data()
+ def hide_everything(self):
- def plot_experimental_data_button_callback(self):
- self.simple_fit_axes.set_visible(True)
+ self.data_axes.set_visible(True)
self.chi_squared_axes.set_visible(False)
self.residuals_axes.set_visible(False)
self.s_h_axes.set_visible(False)
self.longitudinal_scattering_axes.set_visible(False)
- self.simple_fit_axes.cla()
+ self.data_axes.cla()
self.chi_squared_axes.cla()
self.residuals_axes.cla()
self.s_h_axes.cla()
self.longitudinal_scattering_axes.cla()
- self.MuMagLib_obj.plot_exp_data(self.fig, self.simple_fit_axes)
- self.figure_canvas.draw()
+
+
+ def plot_data(self):
+ """ Plot Experimental Data: Generate Figure """
+
+ colors = pl.cm.jet(np.linspace(0, 1, len(self.data)))
+
+ for i, datum in enumerate(self.data):
+
+ self.data_axes.loglog(datum.scattering_curve.x,
+ datum.scattering_curve.y,
+ linestyle='-', color=colors[i], linewidth=0.5,
+ label=r'$B_0 = ' + str(datum.applied_field) + '$ T')
+
+ self.data_axes.loglog(datum.scattering_curve.x,
+ datum.scattering_curve.y, '.',
+ color=colors[i], linewidth=0.3, markersize=1)
+
+ # Plot limits
+ qlim = MuMagLib.nice_log_plot_bounds([datum.scattering_curve.x for datum in self.data])
+ ilim = MuMagLib.nice_log_plot_bounds([datum.scattering_curve.y for datum in self.data])
+
+ self.data_axes.set_xlabel(r'$q$ [1/nm]')
+ self.data_axes.set_ylabel(r'$I_{\mathrm{exp}}$')
+ self.data_axes.set_xlim(qlim)
+ self.data_axes.set_ylim(ilim)
+ self.data_figure.tight_layout()
+ self.data_figure.canvas.draw()
+ self.data_axes.set_visible(True)
+
def fit_parameters(self) -> FitParameters:
""" Get an object containing all the parameters needed for doing the fitting """
@@ -94,14 +161,14 @@ def simple_fit_button_callback(self):
# Clear axes
- self.simple_fit_axes.cla()
+ self.data_axes.cla()
self.chi_squared_axes.cla()
self.residuals_axes.cla()
self.s_h_axes.cla()
self.longitudinal_scattering_axes.cla()
# Set axes visibility
- self.simple_fit_axes.set_visible(False)
+ self.data_axes.set_visible(False)
self.chi_squared_axes.set_visible(True)
self.residuals_axes.set_visible(True)
self.s_h_axes.set_visible(True)
@@ -119,7 +186,7 @@ def simple_fit_button_callback(self):
self.MuMagLib_obj.do_fit(
parameters,
- self.fig,
+ self.data_figure,
self.chi_squared_axes,
self.residuals_axes,
self.s_h_axes,
@@ -130,20 +197,20 @@ def simple_fit_button_callback(self):
def compare_data_button_callback(self):
# Clear axes
- self.simple_fit_axes.cla()
+ self.data_axes.cla()
self.chi_squared_axes.cla()
self.residuals_axes.cla()
self.s_h_axes.cla()
self.longitudinal_scattering_axes.cla()
# Set visibility
- self.simple_fit_axes.set_visible(True)
+ self.data_axes.set_visible(True)
self.chi_squared_axes.set_visible(False)
self.residuals_axes.set_visible(False)
self.s_h_axes.set_visible(False)
self.longitudinal_scattering_axes.set_visible(False)
- self.MuMagLib_obj.SimpleFit_CompareButtonCallback(self.fig, self.simple_fit_axes)
+ self.MuMagLib_obj.SimpleFit_CompareButtonCallback(self.data_figure, self.data_axes)
diff --git a/src/sas/qtgui/Utilities/MuMagTool/MuMagLib.py b/src/sas/qtgui/Utilities/MuMagTool/MuMagLib.py
index fb06bf32a6..30a1e2f805 100644
--- a/src/sas/qtgui/Utilities/MuMagTool/MuMagLib.py
+++ b/src/sas/qtgui/Utilities/MuMagTool/MuMagLib.py
@@ -1,25 +1,18 @@
import numpy as np
-import math
-import matplotlib.pyplot as plt
import matplotlib.pylab as pl
-import scipy.optimize as scopt
-from tkinter import *
-from tkinter import filedialog
from tkinter import messagebox
-from tkinter import ttk
import os
import os.path
from datetime import datetime
from PySide6 import QtWidgets
from PySide6.QtWidgets import QFileDialog
-import string
-from sas.qtgui.Utilities.MuMagTool import MuMagParallelGeo
from sas.qtgui.Utilities.MuMagTool import MuMagPerpendicularGeo
from sas.qtgui.Utilities.MuMagTool.MuMagParallelGeo import LSQ_PAR
from sas.qtgui.Utilities.MuMagTool.MuMagPerpendicularGeo import LSQ_PERP
from sas.qtgui.Utilities.MuMagTool.experimental_data import ExperimentalData
+from sas.qtgui.Utilities.MuMagTool.failure import LoadFailure
from sas.qtgui.Utilities.MuMagTool.fit_parameters import FitParameters, ExperimentGeometry
from sas.qtgui.Utilities.MuMagTool.least_squares_output import LeastSquaresOutputPerpendicular, \
LeastSquaresOutputParallel
@@ -28,7 +21,7 @@
from sasdata.dataloader.loader import Loader
-class MuMagLib():
+class MuMagLib:
def __init__(self):
self.input_data: list[ExperimentalData] | None = None
@@ -43,46 +36,45 @@ def directory_popup():
return directory
- def import_data(self):
+ @staticmethod
+ def import_data(directory):
"""Import experimental data and get information from filenames"""
- # Get the directory from the user
- directory = MuMagLib.directory_popup()
-
- if directory is None:
- return
-
- # Load the data
- loader = Loader()
- input_names = [name for name in os.listdir(directory) if name.lower().endswith(".csv")]
- input_paths = [os.path.join(directory, filename) for filename in input_names]
+ try:
+ # Load the data
+ loader = Loader()
+ input_names = [name for name in os.listdir(directory) if name.lower().endswith(".csv")]
+ input_paths = [os.path.join(directory, filename) for filename in input_names]
- input_data = loader.load(input_paths)
+ input_data = loader.load(input_paths)
- data = []
- for filename, data1d in zip(input_names, input_data):
+ data = []
+ for filename, data1d in zip(input_names, input_data):
- # Extract the metadata from the filename
- filename_parts = filename.split(".")
- filename = ".".join(filename_parts[:-1])
+ # Extract the metadata from the filename
+ filename_parts = filename.split(".")
+ filename = ".".join(filename_parts[:-1])
- parts = filename.split("_")
+ parts = filename.split("_")
+ applied_field = float(parts[1]) # mT
+ saturation_magnetisation = float(parts[2]) # mT
+ demagnetising_field = float(parts[3]) # mT
- applied_field = float(parts[1]) # mT
- saturation_magnetisation = float(parts[2]) # mT
- demagnetising_field = float(parts[3]) # mT
+ # Create input data object
+ data.append(ExperimentalData(
+ scattering_curve=data1d,
+ applied_field=applied_field,
+ saturation_magnetisation=saturation_magnetisation,
+ demagnetising_field=demagnetising_field))
- # Create input data object
- data.append(ExperimentalData(
- scattering_curve=data1d,
- applied_field=applied_field,
- saturation_magnetisation=saturation_magnetisation,
- demagnetising_field=demagnetising_field))
+ return sorted(data, key=lambda x: x.applied_field)
- self.input_data = sorted(data, key=lambda x: x.applied_field)
+ except Exception as e:
+ raise LoadFailure(f"Failed to load from {directory}: " + repr(e))
- def nice_log_plot_bounds(self, data: list[np.ndarray]):
+ @staticmethod
+ def nice_log_plot_bounds(data: list[np.ndarray]):
""" Get nice bounds for the loglog plots
:return: (lower, upper) bounds appropriate to pass to plt.xlim/ylim
@@ -96,42 +88,6 @@ def nice_log_plot_bounds(self, data: list[np.ndarray]):
10 ** (np.floor(np.log10(upper))) * np.ceil(upper / 10 ** (np.floor(np.log10(upper))))
)
- def plot_exp_data(self, figure, axes):
- """ Plot Experimental Data: Generate Figure """
-
- if self.input_data is None:
-
- messagebox.showerror(
- title="Error!",
- message="No experimental data available! Please import experimental data!")
-
- return
-
- ax = axes
- colors = pl.cm.jet(np.linspace(0, 1, len(self.input_data)))
-
- for i, datum in enumerate(self.input_data):
-
- ax.loglog(datum.scattering_curve.x,
- datum.scattering_curve.y,
- linestyle='-', color=colors[i], linewidth=0.5,
- label=r'$B_0 = ' + str(datum.applied_field) + '$ T')
-
- ax.loglog(datum.scattering_curve.x,
- datum.scattering_curve.y, '.',
- color=colors[i], linewidth=0.3, markersize=1)
-
- # Plot limits
- qlim = self.nice_log_plot_bounds([datum.scattering_curve.x for datum in self.input_data])
- ilim = self.nice_log_plot_bounds([datum.scattering_curve.y for datum in self.input_data])
-
- ax.set_xlabel(r'$q$ [1/nm]')
- ax.set_ylabel(r'$I_{\mathrm{exp}}$')
- ax.set_xlim(qlim)
- ax.set_ylim(ilim)
- figure.tight_layout()
- figure.canvas.draw()
-
# @staticmethod
def do_fit(self, parameters: FitParameters, figure, axes1, axes2, axes3, axes4):
diff --git a/src/sas/qtgui/Utilities/MuMagTool/SANSData/__init__.py b/src/sas/qtgui/Utilities/MuMagTool/SANSData/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/src/sas/qtgui/Utilities/MuMagTool/UI/MuMagUI.ui b/src/sas/qtgui/Utilities/MuMagTool/UI/MuMagUI.ui
index 150f98107e..b9b1254452 100644
--- a/src/sas/qtgui/Utilities/MuMagTool/UI/MuMagUI.ui
+++ b/src/sas/qtgui/Utilities/MuMagTool/UI/MuMagUI.ui
@@ -262,19 +262,24 @@
-
-
-
- Display
+
+
+ 0
-
-
-
- 0
- 20
- 761
- 411
-
-
+
+
+ Data
+
+
+
+
+ Fit Results
+
+
+
+
+ Comparison
+
diff --git a/src/sas/qtgui/Utilities/MuMagTool/UI/__init__.py b/src/sas/qtgui/Utilities/MuMagTool/UI/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/src/sas/qtgui/Utilities/MuMagTool/__init__.py b/src/sas/qtgui/Utilities/MuMagTool/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/src/sas/qtgui/Utilities/MuMagTool/failure.py b/src/sas/qtgui/Utilities/MuMagTool/failure.py
index 55a36e635e..e8dfb573dd 100644
--- a/src/sas/qtgui/Utilities/MuMagTool/failure.py
+++ b/src/sas/qtgui/Utilities/MuMagTool/failure.py
@@ -1,5 +1,7 @@
class FitFailure(Exception):
+ """ Fit failed """
pass
class LoadFailure(Exception):
+ """ File loading failed """
pass
\ No newline at end of file
From a9fc7a3d405b8fd3e3f89791a2883d8f47b40bbb Mon Sep 17 00:00:00 2001
From: lucas-wilkins
Date: Mon, 10 Jun 2024 14:03:47 +0100
Subject: [PATCH 34/46] Much moving
---
src/sas/qtgui/Utilities/MuMagTool/MuMag.py | 25 +-
src/sas/qtgui/Utilities/MuMagTool/MuMagLib.py | 344 +++++++++++++++---
.../Utilities/MuMagTool/MuMagParallelGeo.py | 162 ---------
.../MuMagTool/MuMagPerpendicularGeo.py | 270 --------------
.../qtgui/Utilities/MuMagTool/fit_result.py | 24 +-
5 files changed, 306 insertions(+), 519 deletions(-)
delete mode 100644 src/sas/qtgui/Utilities/MuMagTool/MuMagParallelGeo.py
delete mode 100644 src/sas/qtgui/Utilities/MuMagTool/MuMagPerpendicularGeo.py
diff --git a/src/sas/qtgui/Utilities/MuMagTool/MuMag.py b/src/sas/qtgui/Utilities/MuMagTool/MuMag.py
index b9200d6a4b..378bd9da8c 100644
--- a/src/sas/qtgui/Utilities/MuMagTool/MuMag.py
+++ b/src/sas/qtgui/Utilities/MuMagTool/MuMag.py
@@ -17,6 +17,8 @@
from logging import getLogger
+from sas.qtgui.Utilities.MuMagTool.fit_result import FitResults
+
log = getLogger("MuMag")
class MuMag(QtWidgets.QMainWindow, Ui_MuMagTool):
@@ -36,6 +38,7 @@ def __init__(self, parent=None):
#
self.data: list[ExperimentalData] | None = None
+ self.fit_data: FitResults | None = None
#
# Plotting
@@ -84,12 +87,14 @@ def importData(self):
return
log.info(f"Loaded {len(self.data)} datasets")
+
+ self.hide_everything()
self.plot_data()
- self.data_figure.canvas.draw()
+
def hide_everything(self):
- self.data_axes.set_visible(True)
+ self.data_axes.set_visible(False)
self.chi_squared_axes.set_visible(False)
self.residuals_axes.set_visible(False)
self.s_h_axes.set_visible(False)
@@ -128,8 +133,9 @@ def plot_data(self):
self.data_axes.set_xlim(qlim)
self.data_axes.set_ylim(ilim)
self.data_figure.tight_layout()
- self.data_figure.canvas.draw()
+
self.data_axes.set_visible(True)
+ self.data_figure.canvas.draw()
def fit_parameters(self) -> FitParameters:
@@ -159,6 +165,9 @@ def fit_parameters(self) -> FitParameters:
def simple_fit_button_callback(self):
+ if self.data is None:
+ log.error("No data loaded")
+ return None
# Clear axes
self.data_axes.cla()
@@ -184,16 +193,12 @@ def simple_fit_button_callback(self):
raise ValueError(f"Unknown Value: {parameters.experiment_geometry}")
- self.MuMagLib_obj.do_fit(
- parameters,
- self.data_figure,
- self.chi_squared_axes,
- self.residuals_axes,
- self.s_h_axes,
- self.longitudinal_scattering_axes)
+ self.fit_data = MuMagLib.do_fit(self.data, parameters)
self.figure_canvas.draw()
+
+
def compare_data_button_callback(self):
# Clear axes
diff --git a/src/sas/qtgui/Utilities/MuMagTool/MuMagLib.py b/src/sas/qtgui/Utilities/MuMagTool/MuMagLib.py
index 30a1e2f805..8cccaa19c5 100644
--- a/src/sas/qtgui/Utilities/MuMagTool/MuMagLib.py
+++ b/src/sas/qtgui/Utilities/MuMagTool/MuMagLib.py
@@ -1,19 +1,18 @@
import numpy as np
import matplotlib.pylab as pl
-from tkinter import messagebox
import os
import os.path
from datetime import datetime
+import scipy.optimize
+
from PySide6 import QtWidgets
from PySide6.QtWidgets import QFileDialog
-from sas.qtgui.Utilities.MuMagTool import MuMagPerpendicularGeo
-from sas.qtgui.Utilities.MuMagTool.MuMagParallelGeo import LSQ_PAR
-from sas.qtgui.Utilities.MuMagTool.MuMagPerpendicularGeo import LSQ_PERP
from sas.qtgui.Utilities.MuMagTool.experimental_data import ExperimentalData
from sas.qtgui.Utilities.MuMagTool.failure import LoadFailure
from sas.qtgui.Utilities.MuMagTool.fit_parameters import FitParameters, ExperimentGeometry
+from sas.qtgui.Utilities.MuMagTool.fit_result import FitResults
from sas.qtgui.Utilities.MuMagTool.least_squares_output import LeastSquaresOutputPerpendicular, \
LeastSquaresOutputParallel
from sas.qtgui.Utilities.MuMagTool.sweep_output import SweepOutput
@@ -22,9 +21,9 @@
class MuMagLib:
- def __init__(self):
+ """ Library for methods supporting MuMag"""
- self.input_data: list[ExperimentalData] | None = None
+ mu_0 = 4 * np.pi * 1e-7
@staticmethod
def directory_popup():
@@ -89,91 +88,312 @@ def nice_log_plot_bounds(data: list[np.ndarray]):
)
- # @staticmethod
- def do_fit(self, parameters: FitParameters, figure, axes1, axes2, axes3, axes4):
+ @staticmethod
+ def do_fit(data: list[ExperimentalData], parameters: FitParameters):
+ """ Main fitting ("simple fit") """
- if self.input_data is None:
- messagebox.showerror(title="Error!",
- message="No experimental Data available! Please import experimental data!")
-
- return None
+ geometry = parameters.experiment_geometry
# Use an index for data upto qmax based on first data set
# Not ideal, would be preferable make sure the data was
# compatible, using something like interpolation TODO
- square_distance_from_qmax = (self.input_data[0].scattering_curve.x - parameters.q_max) ** 2
+ square_distance_from_qmax = (data[0].scattering_curve.x - parameters.q_max) ** 2
max_q_index = int(np.argmin(square_distance_from_qmax))
filtered_inputs = [datum.restrict_by_index(max_q_index)
- for datum in self.input_data
+ for datum in data
if datum.applied_field >= parameters.min_applied_field]
+ # Brute force check for something near the minimum
+ sweep_data = MuMagLib.sweep(parameters, filtered_inputs)
- # Least Squares Fit in case of perpendicular SANS geometry
- match parameters.experiment_geometry:
- case ExperimentGeometry.PERPENDICULAR:
+ # Refine this result
+ crude_A = sweep_data.optimal.exchange_A
+ refined = MuMagLib.refine_exchange_A(filtered_inputs, crude_A, geometry)
+ # Get uncertainty estimate TODO: Check units
+ uncertainty = MuMagLib.uncertainty(filtered_inputs, refined.exchange_A, geometry) * 1e12
- sweep_data = MuMagPerpendicularGeo.sweep(parameters, filtered_inputs)
+ return FitResults(
+ input_data=data,
+ sweep_data=sweep_data,
+ refined_fit_data=refined,
+ optimal_exchange_A_uncertainty=uncertainty)
- crude_A = sweep_data.optimal.exchange_A
- refined_A = self.refine_exchange_A(filtered_inputs, crude_A, ExperimentGeometry.PERPENDICULAR)
+ @staticmethod
+ def sweep(parameters: FitParameters, data: list[ExperimentalData]) -> SweepOutput:
+ """ Sweep over Exchange Stiffness A for perpendicular SANS geometry to
+ get an initial estimate which can then be refined"""
- refined = MuMagPerpendicularGeo.LSQ_PERP(filtered_inputs, refined_A)
+ a_values = np.linspace(
+ parameters.exchange_A_min,
+ parameters.exchange_A_max,
+ parameters.exchange_A_n) * 1e-12 # From pJ/m to J/m
- uncertainty = self.uncertainty(filtered_inputs, refined_A, ExperimentGeometry.PERPENDICULAR)
+ if parameters.experiment_geometry == ExperimentGeometry.PERPENDICULAR:
+ least_squared_fits = [MuMagLib.LSQ_PERP(data, a) for a in a_values]
- self.plot_results(sweep_data, refined, uncertainty * 1e12, figure, axes1, axes2, axes3, axes4)
+ elif parameters.experiment_geometry == ExperimentGeometry.PARALLEL:
+ least_squared_fits = [MuMagLib.LSQ_PAR(data, a) for a in a_values]
+ else:
+ raise ValueError(f"Unknown ExperimentGeometry value: {parameters.experiment_geometry}")
+ optimal_fit = min(least_squared_fits, key=lambda x: x.exchange_A_chi_sq)
- case ExperimentGeometry.PARALLEL:
+ chi_sq = np.array([fit.exchange_A_chi_sq for fit in least_squared_fits])
+
+ return SweepOutput(
+ exchange_A_checked=a_values,
+ exchange_A_chi_sq=chi_sq,
+ optimal=optimal_fit)
+
+ @staticmethod
+ def LSQ_PERP(data: list[ExperimentalData], A) -> LeastSquaresOutputPerpendicular:
+ """ Least squares fitting for a given exchange stiffness, A, perpendicular case
+
+ We are fitting the equation:
+
+ I_sim = I_res + response_H * S_H + response_M * S_M
+ = (I_res, S_H, S_M) . (1, response_H, response_M)
+ = (I_res, S_H, S_M) . least_squares_x
+
+ finding I_res, S_H, and S_M for each q value
+
+
+ """
+
+ # Get matrices from the input data
+ n_data = len(data)
+
+ # Factor of (1e-3 / mu_0) converts from mT to A/m
+ applied_field = np.array([datum.applied_field for datum in data]) * (1e-3 / MuMagLib.mu_0)
+ demagnetising_field = np.array([datum.demagnetising_field for datum in data]) * (1e-3 / MuMagLib.mu_0)
+ saturation_magnetisation = np.array([datum.saturation_magnetisation for datum in data]) * (1e-3 / MuMagLib.mu_0)
+ # TODO: The following is how things should be done in the future, rather than hard-coding
+ # a scaling factor...
+ # def data_nanometers(data: Data1D):
+ # raw_data = data.x
+ # units = data.x_unit
+ # converter = Converter(units)
+ # return converter.scale("1/m", raw_data)
+ #
+ # q = np.array([data_nanometers(datum.scattering_curve) for datum in data])
- sweep_data = MuMagPerpendicularGeo.sweep(parameters, filtered_inputs)
+ q = np.array([datum.scattering_curve.x for datum in data]) * 1e9
+ I = np.array([datum.scattering_curve.y for datum in data])
+ I_stdev = np.array([datum.scattering_curve.dy for datum in data])
- crude_A = sweep_data.optimal.exchange_A
- print(crude_A)
- refined_A = self.refine_exchange_A(filtered_inputs, parameters.exchange_A_min*1e-12, ExperimentGeometry.PARALLEL)
+ n_q = q.shape[1]
- refined = MuMagPerpendicularGeo.LSQ_PAR(filtered_inputs, refined_A)
+ # Micromagnetic Model
+ internal_field = (applied_field - demagnetising_field).reshape(-1, 1)
+ magnetic_scattering_length = np.sqrt(
+ (2 * A) / (MuMagLib.mu_0 * saturation_magnetisation.reshape(-1, 1) * internal_field))
+ effective_field = internal_field * (1 + (magnetic_scattering_length ** 2) * (q ** 2))
- uncertainty = self.uncertainty(filtered_inputs, refined_A, ExperimentGeometry.PARALLEL)
+ # Calculate the response functions
+ p = saturation_magnetisation.reshape(-1, 1) / effective_field
+ response_H = (p ** 2) / 4 * (2 + 1 / np.sqrt(1 + p))
+ response_M = (np.sqrt(1 + p) - 1) / 2
- self.plot_results(sweep_data, refined, uncertainty * 1e12,
- figure, axes1, axes2, axes3, axes4)
+ # print("Input", q.shape, q[0,10])
+ # sys.exit()
+
+ # Lists for output of calculation
+ I_residual = []
+ S_H = []
+ S_M = []
+
+ I_residual_error_weight = []
+ S_M_error_weight = []
+ S_H_error_weight = []
+
+ for nu in range(n_q):
+
+ # non-negative linear least squares
+ least_squares_x = (np.array([np.ones((n_data,)), response_H[:, nu], response_M[:, nu]]) / I_stdev[:, nu]).T
+ least_squares_y = I[:, nu] / I_stdev[:, nu]
+
+ least_squares_x_squared = np.dot(least_squares_x.T, least_squares_x)
+
+ # Non-negative least squares
+ try:
+ fit_result = scipy.optimize.nnls(
+ least_squares_x_squared,
+ np.matmul(least_squares_x.T, least_squares_y))
+
+ except ValueError as ve:
+ print("Value Error:")
+ print(" A =", A)
+
+ raise ve
+
+ I_residual.append(fit_result[0][0])
+ S_H.append(fit_result[0][1])
+ S_M.append(fit_result[0][2])
+
+ errors = np.linalg.inv(np.dot(least_squares_x_squared.T, least_squares_x_squared))
+
+ I_residual_error_weight.append(errors[0, 0])
+ S_H_error_weight.append(errors[1, 1])
+ S_M_error_weight.append(errors[2, 2])
+
+ # Arrayise
+ S_H = np.array(S_H)
+ S_M = np.array(S_M)
+ I_residual = np.array(I_residual)
+
+ I_sim = I_residual + response_H * S_H + response_M * S_M
+
+ s_q = np.mean(((I - I_sim) / I_stdev) ** 2, axis=0)
+
+ sigma_I_res = np.sqrt(np.abs(np.array(I_residual_error_weight) * s_q))
+ sigma_S_H = np.sqrt(np.abs(np.array(S_H_error_weight) * s_q))
+ sigma_S_M = np.sqrt(np.abs(np.array(S_M_error_weight) * s_q))
+
+ chi_sq = float(np.mean(s_q))
+
+ output_q_values = np.mean(q, axis=0)
+
+ return LeastSquaresOutputPerpendicular(
+ exchange_A=A,
+ exchange_A_chi_sq=chi_sq,
+ q=output_q_values,
+ I_residual=I_residual,
+ I_simulated=I_sim,
+ S_H=S_H,
+ S_M=S_M,
+ I_residual_stdev=sigma_I_res,
+ S_H_stdev=sigma_S_H,
+ S_M_stdev=sigma_S_M)
- # Save to global Variables
- # self.SimpleFit_q_exp = q
- # self.SimpleFit_I_exp = I_exp_red
- # self.SimpleFit_sigma_exp = sigma
- # self.SimpleFit_B_0_exp = self.B_0_exp[K_H:]
- # self.SimpleFit_Ms_exp = self.Ms_exp[K_H:]
- # self.SimpleFit_Hdem_exp = self.Hdem_exp[K_H:]
- # self.SimpleFit_I_fit = I_opt
- # self.SimpleFit_A = A
- # self.SimpleFit_chi_q = chi_q
- # self.SimpleFit_S_H_fit = S_H_opt
- # self.SimpleFit_I_res_fit = I_res_opt
- # self.SimpleFit_A_opt = A_opt
- # self.SimpleFit_chi_q_opt = chi_q_opt
- # self.SimpleFit_A_sigma = A_Uncertainty
- # self.SimpleFit_SANSgeometry = "parallel"
+
+ @staticmethod
+ def LSQ_PAR(data: list[ExperimentalData], A):
+
+ """ Least squares fitting for a given exchange stiffness, A, parallel case
+
+ We are fitting the equation:
+
+ I_sim = I_res + response_H * S_H
+ = (I_res, S_H) . (1, response_H)
+ = (I_res, S_H) . least_squares_x
+
+ finding I_res and S_H for each q value
+
+
+ """
+
+ # Get matrices from the input data
+ n_data = len(data)
+
+ # Factor of (1e-3 / mu_0) converts from mT to A/m
+ applied_field = np.array([datum.applied_field for datum in data]) * (1e-3 / MuMagLib.mu_0)
+ demagnetising_field = np.array([datum.demagnetising_field for datum in data]) * (1e-3 / MuMagLib.mu_0)
+ saturation_magnetisation = np.array([datum.saturation_magnetisation for datum in data]) * (1e-3 / MuMagLib.mu_0)
+
+ # TODO: The following is how things should be done in the future, rather than hard-coding
+ # a scaling factor...
+ # def data_nanometers(data: Data1D):
+ # raw_data = data.x
+ # units = data.x_unit
+ # converter = Converter(units)
+ # return converter.scale("1/m", raw_data)
+ #
+ # q = np.array([data_nanometers(datum.scattering_curve) for datum in data])
+
+ q = np.array([datum.scattering_curve.x for datum in data]) * 1e9
+ I = np.array([datum.scattering_curve.y for datum in data])
+ I_stdev = np.array([datum.scattering_curve.dy for datum in data])
+
+ n_q = q.shape[1]
+
+ # Micromagnetic Model
+ internal_field = (applied_field - demagnetising_field).reshape(-1, 1)
+ magnetic_scattering_length = np.sqrt(
+ (2 * A) / (MuMagLib.mu_0 * saturation_magnetisation.reshape(-1, 1) * internal_field))
+ effective_field = internal_field * (1 + (magnetic_scattering_length ** 2) * (q ** 2))
+
+ # Calculate the response functions
+ p = saturation_magnetisation.reshape(-1, 1) / effective_field
+ response_H = (p ** 2) / 2
+
+ # Lists for output of calculation
+ I_residual = []
+ S_H = []
+
+ I_residual_error_weight = []
+ S_H_error_weight = []
+
+ for nu in range(n_q):
+
+ # non-negative linear least squares
+ least_squares_x = (np.array([np.ones((n_data,)), response_H[:, nu]]) / I_stdev[:, nu]).T
+ least_squares_y = I[:, nu] / I_stdev[:, nu]
+
+ least_squares_x_squared = np.dot(least_squares_x.T, least_squares_x)
+
+ # Non-negative least squares
+ try:
+ fit_result = scipy.optimize.nnls(
+ least_squares_x_squared,
+ np.matmul(least_squares_x.T, least_squares_y))
+
+ except ValueError as ve:
+ print("Value Error:")
+ print(" A =", A)
+
+ raise ve
+
+ I_residual.append(fit_result[0][0])
+ S_H.append(fit_result[0][1])
+
+ errors = np.linalg.inv(np.dot(least_squares_x_squared.T, least_squares_x_squared))
+
+ I_residual_error_weight.append(errors[0, 0])
+ S_H_error_weight.append(errors[1, 1])
+
+ # Arrayise
+ S_H = np.array(S_H)
+ I_residual = np.array(I_residual)
+
+ I_sim = I_residual + response_H * S_H
+
+ s_q = np.mean(((I - I_sim) / I_stdev) ** 2, axis=0)
+
+ sigma_I_res = np.sqrt(np.abs(np.array(I_residual_error_weight) * s_q))
+ sigma_S_H = np.sqrt(np.abs(np.array(S_H_error_weight) * s_q))
+
+ chi_sq = float(np.mean(s_q))
+
+ output_q_values = np.mean(q, axis=0)
+
+ return LeastSquaresOutputParallel(
+ exchange_A=A,
+ exchange_A_chi_sq=chi_sq,
+ q=output_q_values,
+ I_residual=I_residual,
+ I_simulated=I_sim,
+ S_H=S_H,
+ I_residual_stdev=sigma_I_res,
+ S_H_stdev=sigma_S_H)
@staticmethod
def refine_exchange_A(
data: list[ExperimentalData],
exchange_A_initial: float,
geometry: ExperimentGeometry,
- epsilon: float = 0.0001):
+ epsilon: float = 0.0001) -> LeastSquaresOutputPerpendicular | LeastSquaresOutputParallel:
""" Refines the A parameter using Jarratt's method of successive parabolic interpolation"""
match geometry:
case ExperimentGeometry.PARALLEL:
- least_squares_function = LSQ_PAR
+ least_squares_function = MuMagLib.LSQ_PAR
case ExperimentGeometry.PERPENDICULAR:
- least_squares_function = LSQ_PERP
+ least_squares_function = MuMagLib.LSQ_PERP
case _:
raise ValueError(f"Unknown experimental geometry: {geometry}")
@@ -183,9 +403,12 @@ def refine_exchange_A(
x_2 = exchange_A_initial
x_3 = exchange_A_initial + delta
+ # We want all the least squares fitting data around the final value, so keep that in a variable
+ refined_least_squared_data = least_squares_function(data, x_3)
+
y_1 = least_squares_function(data, x_1).exchange_A_chi_sq
y_2 = least_squares_function(data, x_2).exchange_A_chi_sq
- y_3 = least_squares_function(data, x_3).exchange_A_chi_sq
+ y_3 = refined_least_squared_data.exchange_A_chi_sq
x_4 = x_3 + 0.5 * ((x_2 - x_3) ** 2 * (y_3 - y_1) + (x_1 - x_3) ** 2 * (y_2 - y_3)) \
/ ((x_2 - x_3) * (y_3 - y_1) + (x_1 - x_3) * (y_2 - y_3))
@@ -194,28 +417,30 @@ def refine_exchange_A(
if np.abs(2 * (x_4 - x_3) / (x_4 + x_3)) < epsilon:
break
+ refined_least_squared_data = least_squares_function(data, x_3)
+
x_1, x_2, x_3 = x_2, x_3, x_4
- y_1, y_2, y_3 = y_2, y_3, least_squares_function(data, x_3).exchange_A_chi_sq
+ y_1, y_2, y_3 = y_2, y_3, refined_least_squared_data.exchange_A_chi_sq
x_4 = x_3 + 0.5 * ((x_2 - x_3) ** 2 * (y_3 - y_1) + (x_1 - x_3) ** 2 * (y_2 - y_3)) \
/ ((x_2 - x_3) * (y_3 - y_1) + (x_1 - x_3) * (y_2 - y_3))
- return x_4
+ return refined_least_squared_data
@staticmethod
def uncertainty(
data: list[ExperimentalData],
A_opt: float,
- geometry: ExperimentGeometry):
+ geometry: ExperimentGeometry) -> float:
"""Calculate the uncertainty for the optimal exchange stiffness A"""
# Estimate variance from second order derivative of chi-square function via Finite Differences
match geometry:
case ExperimentGeometry.PARALLEL:
- least_squares_function = LSQ_PAR
+ least_squares_function = MuMagLib.LSQ_PAR
case ExperimentGeometry.PERPENDICULAR:
- least_squares_function = LSQ_PERP
+ least_squares_function = MuMagLib.LSQ_PERP
case _:
raise ValueError(f"Unknown experimental geometry: {geometry}")
@@ -369,8 +594,7 @@ def save_button_callback(self):
np.savetxt(os.path.join(path4, "I_res.csv"), np.array([SimpleFit_q_exp[0, :].T,
SimpleFit_I_res_fit[0, :]]).T, delimiter=" ")
- else:
- messagebox.showerror(title="Error!", message="No SimpleFit results available!")
+
@staticmethod
def plot_results(sweep_data: SweepOutput,
diff --git a/src/sas/qtgui/Utilities/MuMagTool/MuMagParallelGeo.py b/src/sas/qtgui/Utilities/MuMagTool/MuMagParallelGeo.py
deleted file mode 100644
index 8ce7fbb537..0000000000
--- a/src/sas/qtgui/Utilities/MuMagTool/MuMagParallelGeo.py
+++ /dev/null
@@ -1,162 +0,0 @@
-import numpy as np
-import scipy.optimize as scopt
-import matplotlib.pyplot as plt
-
-from sas.qtgui.Utilities.MuMagTool.experimental_data import ExperimentalData
-from sas.qtgui.Utilities.MuMagTool.fit_parameters import ExperimentGeometry
-from sas.qtgui.Utilities.MuMagTool.least_squares_output import LeastSquaresOutputParallel, \
- LeastSquaresOutputPerpendicular
-from sas.qtgui.Utilities.MuMagTool.sweep_output import SweepOutput
-
-mu_0 = 4 * np.pi * 1e-7
-
-def LSQ_PAR(data: list[ExperimentalData], A):
-
- """ Least squares fitting for a given exchange stiffness, A, parallel case
-
- We are fitting the equation:
-
- I_sim = I_res + response_H * S_H
- = (I_res, S_H) . (1, response_H)
- = (I_res, S_H) . least_squares_x
-
- finding I_res and S_H for each q value
-
-
- """
-
- # Get matrices from the input data
- n_data = len(data)
-
- # Factor of (1e-3 / mu_0) converts from mT to A/m
- applied_field = np.array([datum.applied_field for datum in data]) * (1e-3 / mu_0)
- demagnetising_field = np.array([datum.demagnetising_field for datum in data]) * (1e-3 / mu_0)
- saturation_magnetisation = np.array([datum.saturation_magnetisation for datum in data]) * (1e-3 / mu_0)
-
- # TODO: The following is how things should be done in the future, rather than hard-coding
- # a scaling factor...
- # def data_nanometers(data: Data1D):
- # raw_data = data.x
- # units = data.x_unit
- # converter = Converter(units)
- # return converter.scale("1/m", raw_data)
- #
- # q = np.array([data_nanometers(datum.scattering_curve) for datum in data])
-
- q = np.array([datum.scattering_curve.x for datum in data]) * 1e9
- I = np.array([datum.scattering_curve.y for datum in data])
- I_stdev = np.array([datum.scattering_curve.dy for datum in data])
-
- n_q = q.shape[1]
-
- # Micromagnetic Model
- internal_field = (applied_field - demagnetising_field).reshape(-1, 1)
- magnetic_scattering_length = np.sqrt((2 * A) / (mu_0 * saturation_magnetisation.reshape(-1, 1) * internal_field))
- effective_field = internal_field * (1 + (magnetic_scattering_length ** 2) * (q ** 2))
-
- # Calculate the response functions
- p = saturation_magnetisation.reshape(-1, 1) / effective_field
- response_H = (p ** 2) / 2
-
- # print("Input", q.shape, q[0,10])
- # sys.exit()
-
- # Lists for output of calculation
- I_residual = []
- S_H = []
-
- I_residual_error_weight = []
- S_H_error_weight = []
-
- for nu in range(n_q):
-
- # non-negative linear least squares
- least_squares_x = (np.array([np.ones((n_data,)), response_H[:, nu]]) / I_stdev[:, nu]).T
- least_squares_y = I[:, nu] / I_stdev[:, nu]
-
- least_squares_x_squared = np.dot(least_squares_x.T, least_squares_x)
-
- # Non-negative least squares
- try:
- fit_result = scopt.nnls(
- least_squares_x_squared,
- np.matmul(least_squares_x.T, least_squares_y))
-
- except ValueError as ve:
- print("Value Error:")
- print(" A =", A)
-
- raise ve
-
- I_residual.append(fit_result[0][0])
- S_H.append(fit_result[0][1])
-
- errors = np.linalg.inv(np.dot(least_squares_x_squared.T, least_squares_x_squared))
-
- I_residual_error_weight.append(errors[0, 0])
- S_H_error_weight.append(errors[1, 1])
-
- # Arrayise
- S_H = np.array(S_H)
- I_residual = np.array(I_residual)
-
- I_sim = I_residual + response_H * S_H
-
- s_q = np.mean(((I - I_sim) / I_stdev) ** 2, axis=0)
-
- sigma_I_res = np.sqrt(np.abs(np.array(I_residual_error_weight) * s_q))
- sigma_S_H = np.sqrt(np.abs(np.array(S_H_error_weight) * s_q))
-
- chi_sq = float(np.mean(s_q))
-
- output_q_values = np.mean(q, axis=0)
-
- return LeastSquaresOutputParallel(
- exchange_A=A,
- exchange_A_chi_sq=chi_sq,
- q=output_q_values,
- I_residual=I_residual,
- I_simulated=I_sim,
- S_H=S_H,
- I_residual_stdev=sigma_I_res,
- S_H_stdev=sigma_S_H)
-
-
-####################################################################################################
-
-
-####################################################################################################
-# Plot Fitting results of simple fit
-
-####################################################################################################
-def PlotSweepFitResultPAR(q_max_mat, H_min_mat, A_opt_mat):
- fig = plt.figure()
- ax = plt.axes(projection='3d')
- ax.plot_surface(q_max_mat * 1e-9, H_min_mat, A_opt_mat * 1e12)
- ax.set(xlabel='q_max [1/nm]', ylabel='H_min [mT]', zlabel='A_opt [pJ/m]')
- ax.grid()
-
- plt.tight_layout()
- plt.show()
-
-####################################################################################################
-# Second Order Derivative of chi-square function via Finite Differences
-def FDM2Ord_PAR(q, I_exp, sigma, M_s, H_0, H_dem, A_opt):
-
- p = 0.01
- dA = A_opt * p
- A1 = A_opt - 2*dA
- A2 = A_opt - 1*dA
- A3 = A_opt
- A4 = A_opt + 1*dA
- A5 = A_opt + 2*dA
-
- chi1 = chi_q_PAR(q, I_exp, sigma, M_s, H_0, H_dem, A1)
- chi2 = chi_q_PAR(q, I_exp, sigma, M_s, H_0, H_dem, A2)
- chi3 = chi_q_PAR(q, I_exp, sigma, M_s, H_0, H_dem, A3)
- chi4 = chi_q_PAR(q, I_exp, sigma, M_s, H_0, H_dem, A4)
- chi5 = chi_q_PAR(q, I_exp, sigma, M_s, H_0, H_dem, A5)
-
- d2_chi_dA2 = (-chi1 + 16 * chi2 - 30 * chi3 + 16 * chi4 - chi5)/(12 * dA**2)
-
- return d2_chi_dA2
\ No newline at end of file
diff --git a/src/sas/qtgui/Utilities/MuMagTool/MuMagPerpendicularGeo.py b/src/sas/qtgui/Utilities/MuMagTool/MuMagPerpendicularGeo.py
deleted file mode 100644
index 0ad250d1de..0000000000
--- a/src/sas/qtgui/Utilities/MuMagTool/MuMagPerpendicularGeo.py
+++ /dev/null
@@ -1,270 +0,0 @@
-import sys
-
-import numpy as np
-import scipy.optimize as scopt
-import matplotlib.pyplot as plt
-
-from sas.qtgui.Utilities.MuMagTool.MuMagParallelGeo import LSQ_PAR
-from sasdata.dataloader import Data1D
-from data_util.nxsunit import Converter
-from sas.qtgui.Utilities.MuMagTool.experimental_data import ExperimentalData
-from sas.qtgui.Utilities.MuMagTool.fit_parameters import FitParameters, ExperimentGeometry
-from sas.qtgui.Utilities.MuMagTool.fit_result import FitResults
-from sas.qtgui.Utilities.MuMagTool.least_squares_output import LeastSquaresOutput, LeastSquaresOutputPerpendicular
-from sas.qtgui.Utilities.MuMagTool.sweep_output import SweepOutput
-
-mu_0 = 4 * np.pi * 1e-7
-
-def LSQ_PERP(data: list[ExperimentalData], A) -> LeastSquaresOutputPerpendicular:
- """ Least squares fitting for a given exchange stiffness, A, perpendicular case
-
- We are fitting the equation:
-
- I_sim = I_res + response_H * S_H + response_M * S_M
- = (I_res, S_H, S_M) . (1, response_H, response_M)
- = (I_res, S_H, S_M) . least_squares_x
-
- finding I_res, S_H, and S_M for each q value
-
-
- """
-
-
-
- # Get matrices from the input data
- n_data = len(data)
-
- # Factor of (1e-3 / mu_0) converts from mT to A/m
- applied_field = np.array([datum.applied_field for datum in data]) * (1e-3 / mu_0)
- demagnetising_field = np.array([datum.demagnetising_field for datum in data]) * (1e-3 / mu_0)
- saturation_magnetisation = np.array([datum.saturation_magnetisation for datum in data]) * (1e-3 / mu_0)
-
- # TODO: The following is how things should be done in the future, rather than hard-coding
- # a scaling factor...
- # def data_nanometers(data: Data1D):
- # raw_data = data.x
- # units = data.x_unit
- # converter = Converter(units)
- # return converter.scale("1/m", raw_data)
- #
- # q = np.array([data_nanometers(datum.scattering_curve) for datum in data])
-
-
- q = np.array([datum.scattering_curve.x for datum in data]) * 1e9
- I = np.array([datum.scattering_curve.y for datum in data])
- I_stdev = np.array([datum.scattering_curve.dy for datum in data])
-
- n_q = q.shape[1]
-
- # Micromagnetic Model
- internal_field = (applied_field - demagnetising_field).reshape(-1, 1)
- magnetic_scattering_length = np.sqrt((2 * A) / (mu_0 * saturation_magnetisation.reshape(-1, 1) * internal_field))
- effective_field = internal_field * (1 + (magnetic_scattering_length ** 2) * (q ** 2))
-
- # Calculate the response functions
- p = saturation_magnetisation.reshape(-1, 1) / effective_field
- response_H = (p ** 2) / 4 * (2 + 1 / np.sqrt(1 + p))
- response_M = (np.sqrt(1 + p) - 1) / 2
-
- # print("Input", q.shape, q[0,10])
- # sys.exit()
-
- # Lists for output of calculation
- I_residual = []
- S_H = []
- S_M = []
-
- I_residual_error_weight = []
- S_M_error_weight = []
- S_H_error_weight = []
-
- for nu in range(n_q):
-
-
-
- # non-negative linear least squares
- least_squares_x = (np.array([np.ones((n_data,)), response_H[:, nu], response_M[:, nu]]) / I_stdev[:, nu]).T
- least_squares_y = I[:, nu]/I_stdev[:, nu]
-
- least_squares_x_squared = np.dot(least_squares_x.T, least_squares_x)
-
- # Non-negative least squares
- try:
- fit_result = scopt.nnls(
- least_squares_x_squared,
- np.matmul(least_squares_x.T, least_squares_y))
-
- except ValueError as ve:
- print("Value Error:")
- print(" A =", A)
-
- raise ve
-
-
- I_residual.append(fit_result[0][0])
- S_H.append(fit_result[0][1])
- S_M.append(fit_result[0][2])
-
- errors = np.linalg.inv(np.dot(least_squares_x_squared.T, least_squares_x_squared))
-
- I_residual_error_weight.append(errors[0, 0])
- S_H_error_weight.append(errors[1, 1])
- S_M_error_weight.append(errors[2, 2])
-
-
- # Arrayise
- S_H = np.array(S_H)
- S_M = np.array(S_M)
- I_residual = np.array(I_residual)
-
- I_sim = I_residual + response_H * S_H + response_M * S_M
-
- s_q = np.mean(((I - I_sim)/I_stdev)**2, axis=0)
-
- sigma_I_res = np.sqrt(np.abs(np.array(I_residual_error_weight) * s_q))
- sigma_S_H = np.sqrt(np.abs(np.array(S_H_error_weight) * s_q))
- sigma_S_M = np.sqrt(np.abs(np.array(S_M_error_weight) * s_q))
-
- chi_sq = float(np.mean(s_q))
-
- output_q_values = np.mean(q, axis=0)
-
- return LeastSquaresOutputPerpendicular(
- exchange_A=A,
- exchange_A_chi_sq=chi_sq,
- q=output_q_values,
- I_residual=I_residual,
- I_simulated=I_sim,
- S_H=S_H,
- S_M=S_M,
- I_residual_stdev=sigma_I_res,
- S_H_stdev=sigma_S_H,
- S_M_stdev=sigma_S_M)
-
-
-####################################################################################################
-#
-def sweep(parameters: FitParameters, data: list[ExperimentalData]) -> SweepOutput:
- """ Sweep over Exchange Stiffness A for perpendicular SANS geometry to
- get an initial estimate which can then be refined"""
-
- a_values = np.linspace(
- parameters.exchange_A_min,
- parameters.exchange_A_max,
- parameters.exchange_A_n) * 1e-12 # From pJ/m to J/m
-
- if parameters.experiment_geometry == ExperimentGeometry.PERPENDICULAR:
- least_squared_fits = [LSQ_PERP(data, a) for a in a_values]
-
- elif parameters.experiment_geometry == ExperimentGeometry.PARALLEL:
- least_squared_fits = [LSQ_PAR(data, a) for a in a_values]
-
- else:
- raise ValueError(f"Unknown ExperimentGeometry value: {parameters.experiment_geometry}")
-
- optimal_fit = min(least_squared_fits, key=lambda x: x.exchange_A_chi_sq)
-
- chi_sq = np.array([fit.exchange_A_chi_sq for fit in least_squared_fits])
-
- return SweepOutput(
- exchange_A_checked=a_values,
- exchange_A_chi_sq=chi_sq,
- optimal=optimal_fit)
-
-
-####################################################################################################
-# find optimal Exchange Stiffness A for perpendicular SANS geometry using
-# successive parabolic interpolation (fast and accurate)
-# implemented as Jarratt's Method
-def OptimA_SPI_PERP(data: list[ExperimentalData], A_1, epsilon):
-
- delta = A_1 * 0.1
-
- x_1 = A_1 - delta
- x_2 = A_1
- x_3 = A_1 + delta
-
- y_1 = LSQ_PERP(data, x_1).exchange_A_chi_sq
- y_2 = LSQ_PERP(data, x_2).exchange_A_chi_sq
- y_3 = LSQ_PERP(data, x_3).exchange_A_chi_sq
-
- x_4 = x_3 + 0.5 * ((x_2 - x_3)**2 * (y_3 - y_1) + (x_1 - x_3)**2 * (y_2 - y_3))/((x_2 - x_3) * (y_3 - y_1) + (x_1 - x_3) * (y_2 - y_3))
- while np.abs(2 * (x_4 - x_3)/(x_4 + x_3)) > epsilon:
-
- # print("x1,x2,x3,x4:", x_1, x_2, x_3, x_4)
-
- x_1 = x_2
- x_2 = x_3
- x_3 = x_4
-
- y_1 = y_2
- y_2 = y_3
- y_3 = LSQ_PERP(data, x_3).exchange_A_chi_sq
-
- x_4 = x_3 + 0.5 * ((x_2 - x_3) ** 2 * (y_3 - y_1) + (x_1 - x_3) ** 2 * (y_2 - y_3)) / ((x_2 - x_3) * (y_3 - y_1) + (x_1 - x_3) * (y_2 - y_3))
-
- return x_4
-
-
-####################################################################################################
-# Plot Fitting results of simple fit
-def PlotFittingResultsPERP_SimpleFit(z: SweepOutput, refined: LeastSquaresOutputPerpendicular, A_Uncertainty,
- figure, axes1, axes2, axes3, axes4):
-
- if A_Uncertainty < 1e-4:
- A_Uncertainty = 0
-
- A_uncertainty_str = str(A_Uncertainty)
- A_opt_str = str(z.optimal.exchange_A * 1e12)
-
- q = z.optimal.q * 1e-9
-
- # Plot A search data
-
- axes1.set_title('$A_{\mathrm{opt}} = (' + A_opt_str[0:5] + ' \pm ' + A_uncertainty_str[0:4] + ')$ pJ/m')
- axes1.plot(z.exchange_A_checked * 1e12, z.exchange_A_chi_sq)
- axes1.plot(z.optimal.exchange_A * 1e12, z.optimal.exchange_A_chi_sq, 'o')
-
- axes1.set_xlim([min(z.exchange_A_checked * 1e12), max(z.exchange_A_checked * 1e12)])
- axes1.set_xlabel('$A$ [pJ/m]')
- axes1.set_ylabel('$\chi^2$')
-
- # Residual intensity plot
-
- axes2.plot(q, z.optimal.I_residual, label='fit')
- axes2.set_yscale('log')
- axes2.set_xscale('log')
- axes2.set_xlim([min(q), max(q)])
- axes2.set_xlabel('$q$ [1/nm]')
- axes2.set_ylabel('$I_{\mathrm{res}}$')
-
- axes3.plot(q, z.optimal.S_H, label='fit')
- axes3.set_yscale('log')
- axes3.set_xscale('log')
- axes3.set_xlim([min(q), max(q)])
- axes3.set_xlabel('$q$ [1/nm]')
- axes3.set_ylabel('$S_H$')
-
- axes4.plot(q, z.optimal.S_M, label='fit')
- axes4.set_yscale('log')
- axes4.set_xscale('log')
- axes4.set_xlim([min(q), max(q)])
- axes4.set_xlabel('$q$ [1/nm]')
- axes4.set_ylabel('$S_M$')
-
- figure.tight_layout()
-
-
-####################################################################################################
-def PlotSweepFitResultPERP(q_max_mat, H_min_mat, A_opt_mat):
- fig = plt.figure()
- ax = plt.axes(projection='3d')
- ax.plot_surface(q_max_mat * 1e-9, H_min_mat, A_opt_mat * 1e12)
- ax.set(xlabel='q_max [1/nm]', ylabel='H_min [mT]', zlabel='A_opt [pJ/m]')
- ax.grid()
-
- plt.tight_layout()
- plt.show()
-
-####################################################################################################
-#
diff --git a/src/sas/qtgui/Utilities/MuMagTool/fit_result.py b/src/sas/qtgui/Utilities/MuMagTool/fit_result.py
index ad7b800bee..33b0e64e57 100644
--- a/src/sas/qtgui/Utilities/MuMagTool/fit_result.py
+++ b/src/sas/qtgui/Utilities/MuMagTool/fit_result.py
@@ -4,25 +4,15 @@
from sas.qtgui.Utilities.MuMagTool.experimental_data import ExperimentalData
from sas.qtgui.Utilities.MuMagTool.fit_parameters import ExperimentGeometry
+from sas.qtgui.Utilities.MuMagTool.least_squares_output import LeastSquaresOutputPerpendicular, \
+ LeastSquaresOutputParallel
+from sas.qtgui.Utilities.MuMagTool.sweep_output import SweepOutput
@dataclass
class FitResults:
""" Output the MuMag fit """
- truncated_input_data: list[ExperimentalData]
-
- q: np.ndarray
- I_fit: np.ndarray
-
- S_H: np.ndarray
- S_M: np.ndarray
- I_residual: np.ndarray # Nuclear + Magnetic cross section at complete magnetic saturation
-
- exchange_A: np.ndarray
- exchange_A_chi_sq: np.ndarray
-
- optimal_A: float
- optimal_A_chi_sq: float
- optimal_A_stdev: float # check
-
- geometry: ExperimentGeometry
+ input_data: list[ExperimentalData]
+ sweep_data: SweepOutput
+ refined_fit_data: LeastSquaresOutputParallel | LeastSquaresOutputPerpendicular
+ optimal_exchange_A_uncertainty: float
From e264f1063e57c4d88f77bf05341e46f437f9d388 Mon Sep 17 00:00:00 2001
From: lucas-wilkins
Date: Mon, 10 Jun 2024 14:31:39 +0100
Subject: [PATCH 35/46] GUI work
---
src/sas/qtgui/Utilities/MuMagTool/MuMag.py | 129 +++++++++++++-----
src/sas/qtgui/Utilities/MuMagTool/MuMagLib.py | 75 ++--------
2 files changed, 105 insertions(+), 99 deletions(-)
diff --git a/src/sas/qtgui/Utilities/MuMagTool/MuMag.py b/src/sas/qtgui/Utilities/MuMagTool/MuMag.py
index 378bd9da8c..734e6b9ca3 100644
--- a/src/sas/qtgui/Utilities/MuMagTool/MuMag.py
+++ b/src/sas/qtgui/Utilities/MuMagTool/MuMag.py
@@ -18,6 +18,7 @@
from logging import getLogger
from sas.qtgui.Utilities.MuMagTool.fit_result import FitResults
+from sas.qtgui.Utilities.MuMagTool.least_squares_output import LeastSquaresOutputPerpendicular
log = getLogger("MuMag")
@@ -29,7 +30,7 @@ def __init__(self, parent=None):
# Callbacks
self.ImportDataButton.clicked.connect(self.importData)
- self.SimpleFitButton.clicked.connect(self.simple_fit_button_callback)
+ self.SimpleFitButton.clicked.connect(self.onFit)
self.CompareResultsButton.clicked.connect(self.compare_data_button_callback)
self.SaveResultsButton.clicked.connect(self.save_data_button_callback)
@@ -60,16 +61,15 @@ def __init__(self, parent=None):
self.fit_results_canvas = FigureCanvas(self.fit_results_figure)
self.chi_squared_axes = self.fit_results_figure.add_subplot(2, 2, 1)
- self.chi_squared_axes.set_visible(False)
- self.residuals_axes = self.fit_results_figure.add_subplot(2, 2, 2)
- self.residuals_axes.set_visible(False)
+ self.residual_axes = self.fit_results_figure.add_subplot(2, 2, 2)
self.s_h_axes = self.fit_results_figure.add_subplot(2, 2, 3)
- self.s_h_axes.set_visible(False)
self.longitudinal_scattering_axes = self.fit_results_figure.add_subplot(2, 2, 4)
- self.longitudinal_scattering_axes.set_visible(False)
fit_results_layout.addWidget(self.fit_results_canvas)
+ # Set visibility
+ self.hide_everything()
+
def importData(self):
""" Callback for the import data button """
@@ -89,23 +89,22 @@ def importData(self):
log.info(f"Loaded {len(self.data)} datasets")
self.hide_everything()
+ self.plot_tabs.setTabEnabled(0, True)
self.plot_data()
-
def hide_everything(self):
-
- self.data_axes.set_visible(False)
- self.chi_squared_axes.set_visible(False)
- self.residuals_axes.set_visible(False)
- self.s_h_axes.set_visible(False)
- self.longitudinal_scattering_axes.set_visible(False)
+ """ Hide all plots, disable tabs"""
self.data_axes.cla()
self.chi_squared_axes.cla()
- self.residuals_axes.cla()
+ self.residual_axes.cla()
self.s_h_axes.cla()
self.longitudinal_scattering_axes.cla()
+ self.plot_tabs.setTabEnabled(1, False)
+ self.plot_tabs.setTabEnabled(2, False)
+ # weird order because of how the widget behaves when all are not enabled
+ self.plot_tabs.setTabEnabled(0, False)
def plot_data(self):
@@ -133,12 +132,12 @@ def plot_data(self):
self.data_axes.set_xlim(qlim)
self.data_axes.set_ylim(ilim)
self.data_figure.tight_layout()
-
+
self.data_axes.set_visible(True)
self.data_figure.canvas.draw()
- def fit_parameters(self) -> FitParameters:
+ def get_fit_parameters(self) -> FitParameters:
""" Get an object containing all the parameters needed for doing the fitting """
a_min = self.aMinSpinBox.value()
@@ -163,26 +162,13 @@ def fit_parameters(self) -> FitParameters:
exchange_A_max=a_max,
experiment_geometry=geometry)
- def simple_fit_button_callback(self):
+ def onFit(self):
if self.data is None:
log.error("No data loaded")
return None
- # Clear axes
- self.data_axes.cla()
- self.chi_squared_axes.cla()
- self.residuals_axes.cla()
- self.s_h_axes.cla()
- self.longitudinal_scattering_axes.cla()
-
- # Set axes visibility
- self.data_axes.set_visible(False)
- self.chi_squared_axes.set_visible(True)
- self.residuals_axes.set_visible(True)
- self.s_h_axes.set_visible(True)
-
- parameters = self.fit_parameters()
+ parameters = self.get_fit_parameters()
match parameters.experiment_geometry:
case ExperimentGeometry.PERPENDICULAR:
@@ -192,11 +178,84 @@ def simple_fit_button_callback(self):
case _:
raise ValueError(f"Unknown Value: {parameters.experiment_geometry}")
+ self.fit_data = MuMagLib.simple_fit(self.data, parameters)
+
+ self.show_fit_results()
+
+
+ def show_fit_results(self):
+ """ Show the results of the fit in the widget """
+
+ # Check for data
+ if self.fit_data is None:
+ log.error("No fit data to show")
+ return
+
+ # Some dereferencing to make things more readable
+ A_uncertainty = self.fit_data.optimal_exchange_A_uncertainty
+ refined = self.fit_data.refined_fit_data
+ sweep_data = self.fit_data.sweep_data
+
+ # Text to show TODO: Replace with field in dialog
+ if A_uncertainty < 1e-4:
+ A_uncertainty = 0
- self.fit_data = MuMagLib.do_fit(self.data, parameters)
+ A_uncertainty_str = str(A_uncertainty)
+ A_opt_str = str(refined.exchange_A * 1e12)
+
+ q = refined.q * 1e-9
+
+ # Clear plots
+ self.chi_squared_axes.cla()
+ self.residual_axes.cla()
+ self.s_h_axes.cla()
+ self.longitudinal_scattering_axes.cla()
+
+ # Plot A search data
+ self.chi_squared_axes.set_title('$A_{\mathrm{opt}} = (' + A_opt_str[0:5] + ' \pm ' + A_uncertainty_str[0:4] + ')$ pJ/m')
+ self.chi_squared_axes.plot(sweep_data.exchange_A_checked * 1e12, sweep_data.exchange_A_chi_sq)
+ self.chi_squared_axes.plot(sweep_data.optimal.exchange_A * 1e12, sweep_data.optimal.exchange_A_chi_sq, 'o')
+
+ self.chi_squared_axes.set_xlim([min(sweep_data.exchange_A_checked * 1e12), max(sweep_data.exchange_A_checked * 1e12)])
+ self.chi_squared_axes.set_xlabel('$A$ [pJ/m]')
+ self.chi_squared_axes.set_ylabel('$\chi^2$')
+
+ # Residual intensity plot
+ self.residual_axes.plot(q, refined.I_residual, label='fit')
+ self.residual_axes.set_yscale('log')
+ self.residual_axes.set_xscale('log')
+ self.residual_axes.set_xlim([min(q), max(q)])
+ self.residual_axes.set_xlabel('$q$ [1/nm]')
+ self.residual_axes.set_ylabel('$I_{\mathrm{res}}$')
+
+ # S_H parameter
+ self.s_h_axes.plot(q, refined.S_H, label='fit')
+ self.s_h_axes.set_yscale('log')
+ self.s_h_axes.set_xscale('log')
+ self.s_h_axes.set_xlim([min(q), max(q)])
+ self.s_h_axes.set_xlabel('$q$ [1/nm]')
+ self.s_h_axes.set_ylabel('$S_H$')
+
+ # S_M parameter
+ if isinstance(refined, LeastSquaresOutputPerpendicular):
+ self.longitudinal_scattering_axes.plot(q, refined.S_M, label='fit')
+ self.longitudinal_scattering_axes.set_yscale('log')
+ self.longitudinal_scattering_axes.set_xscale('log')
+ self.longitudinal_scattering_axes.set_xlim([min(q), max(q)])
+ self.longitudinal_scattering_axes.set_xlabel('$q$ [1/nm]')
+ self.longitudinal_scattering_axes.set_ylabel('$S_M$')
+
+ self.fit_results_figure.tight_layout()
+
+ self.chi_squared_axes.set_visible(True)
+ self.residual_axes.set_visible(True)
+ self.s_h_axes.set_visible(True)
+ self.longitudinal_scattering_axes.set_visible(True)
- self.figure_canvas.draw()
+ self.plot_tabs.setTabEnabled(1, True)
+ self.plot_tabs.setTabEnabled(2, True)
+ self.plot_tabs.setCurrentIndex(1)
def compare_data_button_callback(self):
@@ -204,14 +263,14 @@ def compare_data_button_callback(self):
# Clear axes
self.data_axes.cla()
self.chi_squared_axes.cla()
- self.residuals_axes.cla()
+ self.residual_axes.cla()
self.s_h_axes.cla()
self.longitudinal_scattering_axes.cla()
# Set visibility
self.data_axes.set_visible(True)
self.chi_squared_axes.set_visible(False)
- self.residuals_axes.set_visible(False)
+ self.residual_axes.set_visible(False)
self.s_h_axes.set_visible(False)
self.longitudinal_scattering_axes.set_visible(False)
diff --git a/src/sas/qtgui/Utilities/MuMagTool/MuMagLib.py b/src/sas/qtgui/Utilities/MuMagTool/MuMagLib.py
index 8cccaa19c5..2811933bef 100644
--- a/src/sas/qtgui/Utilities/MuMagTool/MuMagLib.py
+++ b/src/sas/qtgui/Utilities/MuMagTool/MuMagLib.py
@@ -89,7 +89,7 @@ def nice_log_plot_bounds(data: list[np.ndarray]):
@staticmethod
- def do_fit(data: list[ExperimentalData], parameters: FitParameters):
+ def simple_fit(data: list[ExperimentalData], parameters: FitParameters):
""" Main fitting ("simple fit") """
geometry = parameters.experiment_geometry
@@ -105,7 +105,7 @@ def do_fit(data: list[ExperimentalData], parameters: FitParameters):
if datum.applied_field >= parameters.min_applied_field]
# Brute force check for something near the minimum
- sweep_data = MuMagLib.sweep(parameters, filtered_inputs)
+ sweep_data = MuMagLib.sweep_exchange_A(parameters, filtered_inputs)
# Refine this result
crude_A = sweep_data.optimal.exchange_A
@@ -121,7 +121,7 @@ def do_fit(data: list[ExperimentalData], parameters: FitParameters):
optimal_exchange_A_uncertainty=uncertainty)
@staticmethod
- def sweep(parameters: FitParameters, data: list[ExperimentalData]) -> SweepOutput:
+ def sweep_exchange_A(parameters: FitParameters, data: list[ExperimentalData]) -> SweepOutput:
""" Sweep over Exchange Stiffness A for perpendicular SANS geometry to
get an initial estimate which can then be refined"""
@@ -131,10 +131,10 @@ def sweep(parameters: FitParameters, data: list[ExperimentalData]) -> SweepOutpu
parameters.exchange_A_n) * 1e-12 # From pJ/m to J/m
if parameters.experiment_geometry == ExperimentGeometry.PERPENDICULAR:
- least_squared_fits = [MuMagLib.LSQ_PERP(data, a) for a in a_values]
+ least_squared_fits = [MuMagLib.least_squares_perpendicular(data, a) for a in a_values]
elif parameters.experiment_geometry == ExperimentGeometry.PARALLEL:
- least_squared_fits = [MuMagLib.LSQ_PAR(data, a) for a in a_values]
+ least_squared_fits = [MuMagLib.least_squares_parallel(data, a) for a in a_values]
else:
raise ValueError(f"Unknown ExperimentGeometry value: {parameters.experiment_geometry}")
@@ -149,7 +149,7 @@ def sweep(parameters: FitParameters, data: list[ExperimentalData]) -> SweepOutpu
optimal=optimal_fit)
@staticmethod
- def LSQ_PERP(data: list[ExperimentalData], A) -> LeastSquaresOutputPerpendicular:
+ def least_squares_perpendicular(data: list[ExperimentalData], A) -> LeastSquaresOutputPerpendicular:
""" Least squares fitting for a given exchange stiffness, A, perpendicular case
We are fitting the equation:
@@ -271,7 +271,7 @@ def LSQ_PERP(data: list[ExperimentalData], A) -> LeastSquaresOutputPerpendicular
@staticmethod
- def LSQ_PAR(data: list[ExperimentalData], A):
+ def least_squares_parallel(data: list[ExperimentalData], A):
""" Least squares fitting for a given exchange stiffness, A, parallel case
@@ -391,9 +391,9 @@ def refine_exchange_A(
match geometry:
case ExperimentGeometry.PARALLEL:
- least_squares_function = MuMagLib.LSQ_PAR
+ least_squares_function = MuMagLib.least_squares_parallel
case ExperimentGeometry.PERPENDICULAR:
- least_squares_function = MuMagLib.LSQ_PERP
+ least_squares_function = MuMagLib.least_squares_perpendicular
case _:
raise ValueError(f"Unknown experimental geometry: {geometry}")
@@ -438,9 +438,9 @@ def uncertainty(
match geometry:
case ExperimentGeometry.PARALLEL:
- least_squares_function = MuMagLib.LSQ_PAR
+ least_squares_function = MuMagLib.least_squares_parallel
case ExperimentGeometry.PERPENDICULAR:
- least_squares_function = MuMagLib.LSQ_PERP
+ least_squares_function = MuMagLib.least_squares_perpendicular
case _:
raise ValueError(f"Unknown experimental geometry: {geometry}")
@@ -596,59 +596,6 @@ def save_button_callback(self):
- @staticmethod
- def plot_results(sweep_data: SweepOutput,
- refined: LeastSquaresOutputPerpendicular | LeastSquaresOutputParallel,
- A_Uncertainty,
- figure, axes1, axes2, axes3, axes4):
-
- if A_Uncertainty < 1e-4:
- A_Uncertainty = 0
-
- A_uncertainty_str = str(A_Uncertainty)
- A_opt_str = str(refined.exchange_A * 1e12)
-
- q = refined.q * 1e-9
-
- # Plot A search data
-
- axes1.set_title('$A_{\mathrm{opt}} = (' + A_opt_str[0:5] + ' \pm ' + A_uncertainty_str[0:4] + ')$ pJ/m')
- axes1.plot(sweep_data.exchange_A_checked * 1e12, sweep_data.exchange_A_chi_sq)
- axes1.plot(sweep_data.optimal.exchange_A * 1e12, sweep_data.optimal.exchange_A_chi_sq, 'o')
-
- axes1.set_xlim([min(sweep_data.exchange_A_checked * 1e12), max(sweep_data.exchange_A_checked * 1e12)])
- axes1.set_xlabel('$A$ [pJ/m]')
- axes1.set_ylabel('$\chi^2$')
-
- # Residual intensity plot
-
- axes2.plot(q, refined.I_residual, label='fit')
- axes2.set_yscale('log')
- axes2.set_xscale('log')
- axes2.set_xlim([min(q), max(q)])
- axes2.set_xlabel('$q$ [1/nm]')
- axes2.set_ylabel('$I_{\mathrm{res}}$')
-
- # S_H parameter
-
- axes3.plot(q, refined.S_H, label='fit')
- axes3.set_yscale('log')
- axes3.set_xscale('log')
- axes3.set_xlim([min(q), max(q)])
- axes3.set_xlabel('$q$ [1/nm]')
- axes3.set_ylabel('$S_H$')
-
- # S_M parameter
- if isinstance(refined, LeastSquaresOutputPerpendicular):
- axes4.plot(q, refined.S_M, label='fit')
- axes4.set_yscale('log')
- axes4.set_xscale('log')
- axes4.set_xlim([min(q), max(q)])
- axes4.set_xlabel('$q$ [1/nm]')
- axes4.set_ylabel('$S_M$')
-
- figure.tight_layout()
-
#################################################################################################################
def SimpleFit_CompareButtonCallback(self, figure, axes):
From 28fb43e3f41596acba6962baae3d9aa1e579083a Mon Sep 17 00:00:00 2001
From: lucas-wilkins
Date: Mon, 10 Jun 2024 17:21:56 +0100
Subject: [PATCH 36/46] Almost done, just need to adjust plots
---
src/sas/qtgui/Utilities/MuMagTool/MuMag.py | 111 ++--
src/sas/qtgui/Utilities/MuMagTool/MuMagLib.py | 293 ++++-------
.../qtgui/Utilities/MuMagTool/UI/MuMagUI.ui | 487 +++++++++++-------
.../qtgui/Utilities/MuMagTool/fit_result.py | 3 +-
4 files changed, 472 insertions(+), 422 deletions(-)
diff --git a/src/sas/qtgui/Utilities/MuMagTool/MuMag.py b/src/sas/qtgui/Utilities/MuMagTool/MuMag.py
index 734e6b9ca3..aa7bef2e71 100644
--- a/src/sas/qtgui/Utilities/MuMagTool/MuMag.py
+++ b/src/sas/qtgui/Utilities/MuMagTool/MuMag.py
@@ -1,17 +1,16 @@
from sas.qtgui.Utilities.MuMagTool.UI.MuMagUI import Ui_MuMagTool
-from PySide6.QtWidgets import QWidget, QVBoxLayout
+from PySide6.QtWidgets import QVBoxLayout
from PySide6 import QtWidgets
from sas.qtgui.Utilities.MuMagTool import MuMagLib
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
-# from matplotlib.figure import Figure
import matplotlib.pyplot as plt
import matplotlib.pylab as pl
import numpy as np
from sas.qtgui.Utilities.MuMagTool.experimental_data import ExperimentalData
-from sas.qtgui.Utilities.MuMagTool.failure import LoadFailure
+from sas.qtgui.Utilities.MuMagTool.failure import LoadFailure, FitFailure
from sas.qtgui.Utilities.MuMagTool.fit_parameters import FitParameters, ExperimentGeometry
from sas.qtgui.Utilities.MuMagTool.MuMagLib import MuMagLib
@@ -23,6 +22,8 @@
log = getLogger("MuMag")
class MuMag(QtWidgets.QMainWindow, Ui_MuMagTool):
+ """ Main widget for the MuMag tool """
+
def __init__(self, parent=None):
super().__init__()
@@ -31,8 +32,7 @@ def __init__(self, parent=None):
# Callbacks
self.ImportDataButton.clicked.connect(self.importData)
self.SimpleFitButton.clicked.connect(self.onFit)
- self.CompareResultsButton.clicked.connect(self.compare_data_button_callback)
- self.SaveResultsButton.clicked.connect(self.save_data_button_callback)
+ self.SaveResultsButton.clicked.connect(self.onSave)
#
# Data
@@ -67,6 +67,16 @@ def __init__(self, parent=None):
fit_results_layout.addWidget(self.fit_results_canvas)
+ # Comparison
+ comparison_layout = QVBoxLayout()
+ self.comparison_tab.setLayout(comparison_layout)
+ self.comparison_figure = plt.figure()
+ self.comparison_canvas = FigureCanvas(self.comparison_figure)
+
+ self.comparison_axes = self.comparison_figure.add_subplot(1, 1, 1)
+
+ comparison_layout.addWidget(self.comparison_canvas)
+
# Set visibility
self.hide_everything()
@@ -90,7 +100,7 @@ def importData(self):
self.hide_everything()
self.plot_tabs.setTabEnabled(0, True)
- self.plot_data()
+ self.show_input_data()
def hide_everything(self):
""" Hide all plots, disable tabs"""
@@ -107,7 +117,7 @@ def hide_everything(self):
self.plot_tabs.setTabEnabled(0, False)
- def plot_data(self):
+ def show_input_data(self):
""" Plot Experimental Data: Generate Figure """
colors = pl.cm.jet(np.linspace(0, 1, len(self.data)))
@@ -176,9 +186,15 @@ def onFit(self):
case ExperimentGeometry.PARALLEL:
self.longitudinal_scattering_axes.set_visible(False)
case _:
- raise ValueError(f"Unknown Value: {parameters.experiment_geometry}")
+ raise log.error(f"Unknown geometry: {parameters.experiment_geometry}")
+
+ try:
+ self.fit_data = MuMagLib.simple_fit(self.data, parameters)
+
+ except FitFailure as ff:
+ log.error("Fitting failed - are the parameters correct? "+repr(ff))
+ return
- self.fit_data = MuMagLib.simple_fit(self.data, parameters)
self.show_fit_results()
@@ -192,18 +208,17 @@ def show_fit_results(self):
return
# Some dereferencing to make things more readable
- A_uncertainty = self.fit_data.optimal_exchange_A_uncertainty
refined = self.fit_data.refined_fit_data
sweep_data = self.fit_data.sweep_data
- # Text to show TODO: Replace with field in dialog
- if A_uncertainty < 1e-4:
- A_uncertainty = 0
+ q = refined.q * 1e-9
+
+ # Update text boxes
+
+ self.exchange_a_display.setText(f"{self.fit_data.refined_fit_data.exchange_A * 1e12 : .5g} pJ/m")
+ self.exchange_a_std_display.setText(f"{self.fit_data.optimal_exchange_A_uncertainty : .5g} pJ/m")
- A_uncertainty_str = str(A_uncertainty)
- A_opt_str = str(refined.exchange_A * 1e12)
- q = refined.q * 1e-9
# Clear plots
self.chi_squared_axes.cla()
@@ -212,7 +227,6 @@ def show_fit_results(self):
self.longitudinal_scattering_axes.cla()
# Plot A search data
- self.chi_squared_axes.set_title('$A_{\mathrm{opt}} = (' + A_opt_str[0:5] + ' \pm ' + A_uncertainty_str[0:4] + ')$ pJ/m')
self.chi_squared_axes.plot(sweep_data.exchange_A_checked * 1e12, sweep_data.exchange_A_chi_sq)
self.chi_squared_axes.plot(sweep_data.optimal.exchange_A * 1e12, sweep_data.optimal.exchange_A_chi_sq, 'o')
@@ -252,35 +266,60 @@ def show_fit_results(self):
self.s_h_axes.set_visible(True)
self.longitudinal_scattering_axes.set_visible(True)
- self.plot_tabs.setTabEnabled(1, True)
- self.plot_tabs.setTabEnabled(2, True)
+ #
+ # Comparison Tab
+ #
- self.plot_tabs.setCurrentIndex(1)
+ # Plot limits
+ qlim = MuMagLib.nice_log_plot_bounds([datum.scattering_curve.x for datum in self.data])
+ ilim = MuMagLib.nice_log_plot_bounds([datum.scattering_curve.y for datum in self.data])
+ # Show the experimental data
+ # colors = pl.cm.jet(np.linspace(0, 1, len(self.fit_data.input_data)))
+ # for k, datum in enumerate(self.fit_data.input_data):
+ # self.comparison_axes.loglog(
+ # datum.scattering_curve.x,
+ # datum.scattering_curve.y,
+ # linestyle='dotted', color=colors[k], linewidth=0.3, markersize=1)
+
+ n_sim = self.fit_data.refined_fit_data.I_simulated.shape[0]
+ colors = pl.cm.YlGn(np.linspace(0, 1, n_sim))
+ for k in range(0, n_sim):
+ self.comparison_axes.loglog(
+ self.fit_data.refined_fit_data.q,
+ self.fit_data.refined_fit_data.I_simulated[k, :],
+ linestyle='solid', color=colors[k],
+ linewidth=0.5, label='(fit) B_0 = ' + str(self.fit_data.input_data[k].applied_field) + ' T')
+
+ self.comparison_axes.set_xlabel(r'$q$ [1/nm]')
+ self.comparison_axes.set_ylabel(r'$I_{\mathrm{exp}}$')
+ self.comparison_axes.set_xlim(qlim)
+ self.comparison_axes.set_ylim(ilim)
+ self.comparison_figure.tight_layout()
+ self.comparison_figure.canvas.draw()
- def compare_data_button_callback(self):
- # Clear axes
- self.data_axes.cla()
- self.chi_squared_axes.cla()
- self.residual_axes.cla()
- self.s_h_axes.cla()
- self.longitudinal_scattering_axes.cla()
+ #
+ # Set up tabs
+ #
- # Set visibility
- self.data_axes.set_visible(True)
- self.chi_squared_axes.set_visible(False)
- self.residual_axes.set_visible(False)
- self.s_h_axes.set_visible(False)
- self.longitudinal_scattering_axes.set_visible(False)
+ self.plot_tabs.setTabEnabled(1, True)
+ self.plot_tabs.setTabEnabled(2, True)
- self.MuMagLib_obj.SimpleFit_CompareButtonCallback(self.data_figure, self.data_axes)
+ self.plot_tabs.setCurrentIndex(1)
+ def onSave(self):
+ """ Save button pressed """
+ if self.fit_data is None:
+ log.error("Nothing to save!")
+ return
+
+ directory = MuMagLib.directory_popup()
- def save_data_button_callback(self):
- self.MuMagLib_obj.save_button_callback()
+ if directory is not None:
+ MuMagLib.save_data(self.fit_data, directory)
def main():
diff --git a/src/sas/qtgui/Utilities/MuMagTool/MuMagLib.py b/src/sas/qtgui/Utilities/MuMagTool/MuMagLib.py
index 2811933bef..63c089996e 100644
--- a/src/sas/qtgui/Utilities/MuMagTool/MuMagLib.py
+++ b/src/sas/qtgui/Utilities/MuMagTool/MuMagLib.py
@@ -10,7 +10,7 @@
from PySide6.QtWidgets import QFileDialog
from sas.qtgui.Utilities.MuMagTool.experimental_data import ExperimentalData
-from sas.qtgui.Utilities.MuMagTool.failure import LoadFailure
+from sas.qtgui.Utilities.MuMagTool.failure import LoadFailure, FitFailure
from sas.qtgui.Utilities.MuMagTool.fit_parameters import FitParameters, ExperimentGeometry
from sas.qtgui.Utilities.MuMagTool.fit_result import FitResults
from sas.qtgui.Utilities.MuMagTool.least_squares_output import LeastSquaresOutputPerpendicular, \
@@ -115,7 +115,8 @@ def simple_fit(data: list[ExperimentalData], parameters: FitParameters):
uncertainty = MuMagLib.uncertainty(filtered_inputs, refined.exchange_A, geometry) * 1e12
return FitResults(
- input_data=data,
+ parameters=parameters,
+ input_data=filtered_inputs,
sweep_data=sweep_data,
refined_fit_data=refined,
optimal_exchange_A_uncertainty=uncertainty)
@@ -225,10 +226,7 @@ def least_squares_perpendicular(data: list[ExperimentalData], A) -> LeastSquares
np.matmul(least_squares_x.T, least_squares_y))
except ValueError as ve:
- print("Value Error:")
- print(" A =", A)
-
- raise ve
+ raise FitFailure(f"A = {A} ({repr(ve)})")
I_residual.append(fit_result[0][0])
S_H.append(fit_result[0][1])
@@ -342,10 +340,7 @@ def least_squares_parallel(data: list[ExperimentalData], A):
np.matmul(least_squares_x.T, least_squares_y))
except ValueError as ve:
- print("Value Error:")
- print(" A =", A)
-
- raise ve
+ raise FitFailure(f"A = {A} ({repr(ve)})")
I_residual.append(fit_result[0][0])
S_H.append(fit_result[0][1])
@@ -467,198 +462,102 @@ def uncertainty(
return np.sqrt(2 / (n_field_strengths * n_q * d2chi_dA2))
- def save_button_callback(self):
-
- SimpleFit_q_exp = self.SimpleFit_q_exp
- SimpleFit_I_exp = self.SimpleFit_I_exp
- SimpleFit_sigma_exp = self.SimpleFit_sigma_exp
- SimpleFit_B_0_exp = self.SimpleFit_B_0_exp
- SimpleFit_Ms_exp = self.SimpleFit_Ms_exp
- SimpleFit_Hdem_exp = self.SimpleFit_Hdem_exp
- SimpleFit_I_fit = self.SimpleFit_I_fit
- SimpleFit_A = self.SimpleFit_A
- SimpleFit_chi_q = self.SimpleFit_chi_q
- SimpleFit_S_H_fit = self.SimpleFit_S_H_fit
- SimpleFit_S_M_fit = self.SimpleFit_S_M_fit
- SimpleFit_I_res_fit = self.SimpleFit_I_res_fit
- SimpleFit_A_opt = self.SimpleFit_A_opt
- SimpleFit_chi_q_opt = self.SimpleFit_chi_q_opt
- SimpleFit_A_sigma = self.SimpleFit_A_sigma
- SimpleFit_SANSgeometry = self.SimpleFit_SANSgeometry
-
- print('hello')
- print(np.size(SimpleFit_q_exp))
-
- if np.size(SimpleFit_q_exp) > 1:
- if SimpleFit_SANSgeometry == 'perpendicular':
- DIR = QFileDialog.getExistingDirectory()
-
- now = datetime.now()
- TimeStamp = now.strftime("%d_%m_%Y__%H_%M_%S")
- FolderName = "SimpleFitResults_" + TimeStamp
- path = os.path.join(DIR, FolderName)
- os.mkdir(path)
-
- InfoFile = open(path + "/InfoFile.txt", "w")
- InfoFile.write("FitMagneticSANS Toolbox - SimpleFit Results Info File \n\n")
- InfoFile.write("Timestamp: " + TimeStamp + " \n\n")
- InfoFile.write("SANS geometry: " + SimpleFit_SANSgeometry + "\n\n")
- InfoFile.write("Maximal Scattering Vector: q_max = " +
- str(np.amax(SimpleFit_q_exp*1e-9)) + " 1/nm \n")
- InfoFile.write("Minimal Applied Field: mu_0*H_min = " +
- str(np.around(np.amin(SimpleFit_B_0_exp), 0)) + " mT \n\n")
- InfoFile.write("Result for the exchange stiffness constant: A = (" +
- str(np.around(SimpleFit_A_opt*1e12, 3)) + " +/- " +
- str(np.around(SimpleFit_A_sigma*1e12, 2)) + ") pJ/m \n")
- InfoFile.close()
-
- FolderName2 = "SANS_Intensity_Fit"
- path2 = os.path.join(path, FolderName2)
- os.mkdir(path2)
-
- for k in np.arange(0, np.size(SimpleFit_B_0_exp)):
- np.savetxt(os.path.join(path2, str(k+1) + "_" + str(SimpleFit_B_0_exp[k]) + "_"
- + str(SimpleFit_Ms_exp[k]) + "_" + str(SimpleFit_Hdem_exp[k]) + ".csv"),
- np.array([SimpleFit_q_exp[k, :].T, SimpleFit_I_fit[k, :].T]).T, delimiter=" ")
-
- FolderName3 = "SANS_Intensity_Exp"
- path3 = os.path.join(path, FolderName3)
- os.mkdir(path3)
-
- for k in np.arange(0, np.size(SimpleFit_B_0_exp)):
- np.savetxt(os.path.join(path3, str(k+1) + "_" + str(SimpleFit_B_0_exp[k]) + "_"
- + str(SimpleFit_Ms_exp[k]) + "_" + str(SimpleFit_Hdem_exp[k]) + ".csv"),
- np.array([SimpleFit_q_exp[k, :].T, SimpleFit_I_exp[k, :].T,
- SimpleFit_sigma_exp[k, :]]).T, delimiter=" ")
-
- FolderName4 = "Fit_Results"
- path4 = os.path.join(path, FolderName4)
- os.mkdir(path4)
-
- np.savetxt(os.path.join(path4, "chi.csv"), np.array([SimpleFit_A, SimpleFit_chi_q]).T, delimiter=" ")
- np.savetxt(os.path.join(path4, "S_H.csv"), np.array([SimpleFit_q_exp[0, :].T,
- SimpleFit_S_H_fit[0, :]]).T, delimiter=" ")
- np.savetxt(os.path.join(path4, "S_M.csv"), np.array([SimpleFit_q_exp[0, :].T,
- SimpleFit_S_M_fit[0, :]]).T, delimiter=" ")
- np.savetxt(os.path.join(path4, "I_res.csv"), np.array([SimpleFit_q_exp[0, :].T,
- SimpleFit_I_res_fit[0, :]]).T, delimiter=" ")
-
- elif SimpleFit_SANSgeometry == 'parallel':
- DIR = QFileDialog.getExistingDirectory()
-
- now = datetime.now()
- TimeStamp = now.strftime("%d_%m_%Y__%H_%M_%S")
- FolderName = "SimpleFitResults_" + TimeStamp
- path = os.path.join(DIR, FolderName)
- os.mkdir(path)
-
- InfoFile = open(path + "/InfoFile.txt", "w")
- InfoFile.write("FitMagneticSANS Toolbox - SimpleFit Results Info File \n\n")
- InfoFile.write("Timestamp: " + TimeStamp + " \n\n")
- InfoFile.write("SANS geometry: " + SimpleFit_SANSgeometry + "\n\n")
- InfoFile.write("Maximal Scattering Vector: q_max = " +
- str(np.amax(SimpleFit_q_exp * 1e-9)) + " 1/nm \n")
- InfoFile.write("Minimal Applied Field: mu_0*H_min = " +
- str(np.around(np.amin(SimpleFit_B_0_exp), 0)) + " mT \n\n")
- InfoFile.write("Result for the exchange stiffness constant: A = (" +
- str(np.around(SimpleFit_A_opt * 1e12, 3)) + " +/- " +
- str(np.around(SimpleFit_A_sigma * 1e12, 2)) + ") pJ/m \n")
- InfoFile.close()
-
- FolderName2 = "SANS_Intensity_Fit"
- path2 = os.path.join(path, FolderName2)
- os.mkdir(path2)
-
- for k in np.arange(0, np.size(SimpleFit_B_0_exp)):
- np.savetxt(os.path.join(path2, str(k + 1) + "_" + str(int(SimpleFit_B_0_exp[k])) + "_" + str(
- int(SimpleFit_Ms_exp[k])) + "_" + str(int(SimpleFit_Hdem_exp[k])) + ".csv"),
- np.array([SimpleFit_q_exp[k, :].T, SimpleFit_I_fit[k, :].T]).T, delimiter=" ")
-
- FolderName3 = "SANS_Intensity_Exp"
- path3 = os.path.join(path, FolderName3)
- os.mkdir(path3)
-
- for k in np.arange(0, np.size(SimpleFit_B_0_exp)):
- np.savetxt(os.path.join(path3, str(k + 1) + "_" + str(int(SimpleFit_B_0_exp[k])) + "_" + str(
- int(SimpleFit_Ms_exp[k])) + "_" + str(int(SimpleFit_Hdem_exp[k])) + ".csv"),
- np.array([SimpleFit_q_exp[k, :].T, SimpleFit_I_exp[k, :].T, SimpleFit_sigma_exp[k, :]]).T,
- delimiter=" ")
-
- FolderName4 = "Fit_Results"
- path4 = os.path.join(path, FolderName4)
- os.mkdir(path4)
-
- np.savetxt(os.path.join(path4, "chi.csv"), np.array([SimpleFit_A, SimpleFit_chi_q]).T, delimiter=" ")
- np.savetxt(os.path.join(path4, "S_H.csv"), np.array([SimpleFit_q_exp[0, :].T,
- SimpleFit_S_H_fit[0, :]]).T, delimiter=" ")
- np.savetxt(os.path.join(path4, "I_res.csv"), np.array([SimpleFit_q_exp[0, :].T,
- SimpleFit_I_res_fit[0, :]]).T, delimiter=" ")
-
-
-
- #################################################################################################################
-
- def SimpleFit_CompareButtonCallback(self, figure, axes):
-
- SimpleFit_q_exp = self.SimpleFit_q_exp
- SimpleFit_I_exp = self.SimpleFit_I_exp
- SimpleFit_sigma_exp = self.SimpleFit_sigma_exp
- SimpleFit_B_0_exp = self.SimpleFit_B_0_exp
- SimpleFit_Ms_exp = self.SimpleFit_Ms_exp
- SimpleFit_Hdem_exp = self.SimpleFit_Hdem_exp
- SimpleFit_I_fit = self.SimpleFit_I_fit
- SimpleFit_A = self.SimpleFit_A
- SimpleFit_chi_q = self.SimpleFit_chi_q
- SimpleFit_S_H_fit = self.SimpleFit_S_H_fit
- SimpleFit_S_M_fit = self.SimpleFit_S_M_fit
- SimpleFit_I_res_fit = self.SimpleFit_I_res_fit
- SimpleFit_A_opt = self.SimpleFit_A_opt
- SimpleFit_chi_q_opt = self.SimpleFit_chi_q_opt
- SimpleFit_A_sigma = self.SimpleFit_A_sigma
- SimpleFit_SANSgeometry = self.SimpleFit_SANSgeometry
-
- if np.size(SimpleFit_q_exp) > 1:
- q_exp_min = np.amin(SimpleFit_q_exp) * 1e-9
- q_exp_min = 10 ** (np.floor(np.log10(q_exp_min))) * np.floor(q_exp_min / 10 ** (np.floor(np.log10(q_exp_min))))
-
- q_exp_max = np.amax(SimpleFit_q_exp) * 1e-9
- q_exp_max = 10 ** (np.floor(np.log10(q_exp_max))) * np.ceil(q_exp_max / 10 ** (np.floor(np.log10(q_exp_max))))
-
- I_exp_min = np.amin(SimpleFit_I_exp)
- I_exp_min = 10 ** (np.floor(np.log10(I_exp_min))) * np.floor(I_exp_min / 10 ** (np.floor(np.log10(I_exp_min))))
-
- I_exp_max = np.amax(SimpleFit_I_exp)
- I_exp_max = 10 ** (np.floor(np.log10(I_exp_max))) * np.ceil(I_exp_max / 10 ** (np.floor(np.log10(I_exp_max))))
-
- self.PlotCompareExpFitData(figure, axes, SimpleFit_q_exp * 1e-9, SimpleFit_I_exp, SimpleFit_I_fit, SimpleFit_B_0_exp * 1e-3, q_exp_min, q_exp_max, I_exp_min, I_exp_max)
- else:
- messagebox.showerror(title="Error!", message="No SimpleFit results available!")
+ @staticmethod
+ def _filename_string(datum: ExperimentalData):
+ """ Get the filename string associated with a bit of experimental data """
+
+ return f"{datum.applied_field}_{datum.saturation_magnetisation}_{datum.demagnetising_field}"
+
+ @staticmethod
+ def save_data(data: FitResults, directory: str):
+ """ Save the data """
+
+ now = datetime.now()
+ timestamp = now.strftime("%d-%m-%Y_%H-%M-%S")
+ output_folder = f"mumag_fit_{timestamp}"
+ path = os.path.join(directory, output_folder)
+
+ if not os.path.exists(path):
+ os.mkdir(path)
+
+ with open(os.path.join(path, "fit_info.txt"), "w") as fid:
+ fid.write(f"FitMagneticSANS Toolbox - SimpleFit Results Info File \n\n")
+ fid.write(f"Timestamp: {timestamp}\n")
+ fid.write(f"SANS geometry: {data.parameters.experiment_geometry.name}\n\n")
+ fid.write(f"Maximal Scattering Vector: q_max = {np.max(data.refined_fit_data.q)} /nm\n")
+ fid.write(f"Minimal Applied Field: mu_0*H_min = {data.input_data[0].applied_field}\n mT \n")
+ fid.write(f"Result for the exchange stiffness constant: "
+ f"A = {data.refined_fit_data.exchange_A} +- {data.optimal_exchange_A_uncertainty} pJ/m \n")
+
+ #
+ # Save fitted data
+ #
+
+ subpath = os.path.join(path, "intensity_fit")
+
+ if not os.path.exists(subpath):
+ os.mkdir(subpath)
+
+ for k, datum in enumerate(data.input_data):
+
+ filename = f"{k}_" + MuMagLib._filename_string(datum) + ".csv"
+
+ q = data.refined_fit_data.q
+ I = data.refined_fit_data.I_simulated[k, :]
+
+ np.savetxt(os.path.join(subpath, filename), np.array([q, I]).T, delimiter=",")
+
+ #
+ # Save original data (but truncated)
+ #
+
+ subpath = os.path.join(path, "input_intensity")
+
+ if not os.path.exists(subpath):
+ os.mkdir(subpath)
+
+ for k, datum in enumerate(data.input_data):
+ filename = f"{k}_" + MuMagLib._filename_string(datum) + ".csv"
+
+ q = datum.scattering_curve.x
+ I = datum.scattering_curve.y
+ dI = datum.scattering_curve.dy
+
+ np.savetxt(os.path.join(subpath, filename), np.array([q, I, dI]).T, delimiter=",")
+
+ #
+ # Optimised results
+ #
+
+ np.savetxt(os.path.join(path, "chi.csv"),
+ np.array([
+ data.sweep_data.exchange_A_checked,
+ data.sweep_data.exchange_A_chi_sq]).T,
+ delimiter=",")
- def PlotCompareExpFitData(self, figure, axes, q, I_exp, I_fit, B_0, x_min, x_max, y_min, y_max):
+ np.savetxt(os.path.join(path, "S_H.csv"),
+ np.array([
+ data.refined_fit_data.q,
+ data.refined_fit_data.S_H,
+ data.refined_fit_data.S_H_stdev]).T,
+ delimiter=",")
- fig = figure
- fig.tight_layout()
- ax = axes
+ np.savetxt(os.path.join(path, "I_res.csv"),
+ np.array([
+ data.refined_fit_data.q,
+ data.refined_fit_data.I_residual,
+ data.refined_fit_data.I_residual_stdev]).T,
+ delimiter=",")
- colors = pl.cm.jet(np.linspace(0, 1, len(B_0)))
- for k in np.arange(0, len(B_0)):
- ax.loglog(q[k, :], I_exp[k, :], '.', color=colors[k], linewidth=0.3, markersize=1)
+ if data.parameters.experiment_geometry == ExperimentGeometry.PERPENDICULAR:
+ np.savetxt(os.path.join(path, "S_M.csv"),
+ np.array([
+ data.refined_fit_data.q,
+ data.refined_fit_data.S_M,
+ data.refined_fit_data.S_M_stdev]).T,
+ delimiter=",")
- colors = pl.cm.YlGn(np.linspace(0, 1, len(B_0)))
- for k in np.arange(0, len(B_0)):
- ax.plot(q[k, :], I_fit[k, :], linestyle='solid', color=colors[k],
- linewidth=0.5, label='(fit) B_0 = ' + str(B_0[k]) + ' T')
- ax.set_xlabel(r'$q$ [1/nm]')
- ax.set_ylabel(r'$I_{\mathrm{exp}}$')
- ax.set_xlim(x_min, x_max)
- ax.set_ylim(y_min, y_max)
- figure.tight_layout()
- figure.canvas.draw()
-if __name__ == "__main__":
- app = QtWidgets.QApplication([])
- # app.exec_()
- MuMagLib.directory_popup()
diff --git a/src/sas/qtgui/Utilities/MuMagTool/UI/MuMagUI.ui b/src/sas/qtgui/Utilities/MuMagTool/UI/MuMagUI.ui
index b9b1254452..9a6f114fb4 100644
--- a/src/sas/qtgui/Utilities/MuMagTool/UI/MuMagUI.ui
+++ b/src/sas/qtgui/Utilities/MuMagTool/UI/MuMagUI.ui
@@ -6,255 +6,366 @@
0
0
- 549
- 481
+ 819
+ 480
MuMagTool
-
+
-
-
-
-
- Import Data
+
+
+ QFrame::StyledPanel
-
-
-
-
-
- Import Data
-
-
-
- -
-
-
- Plot Data
-
-
-
- -
-
-
- Simple Fit
-
-
-
- -
-
-
- Compare Results
-
-
-
- -
-
-
- Save Result
-
-
-
-
-
-
- -
-
-
- Simple Fit Tool
+
+ QFrame::Raised
-
+
-
-
-
-
- Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
-
-
- 7
-
-
-
-
+
+
+ GroupBox
+
+
+
-
+
- Maximum q:
+ Import Data
- -
-
+
-
+
- Applied Field (μ<sub>0</sub> H<sub>min</sub>):
+ Fit
- -
-
-
-
- 0
+
-
+
+
+ Save Result
+
+
+
+
+
+
+ -
+
+
+ Parameters
+
+
+
-
+
+
+
+ Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
-
-
-
-
- 5
-
-
- 10000.000000000000000
-
-
- 75.000000000000000
-
-
-
- -
-
+
+ 7
+
+
-
+
- mT
+ Analysis Method:
-
-
-
- -
-
-
-
- 0
-
-
-
-
-
- 5
-
-
- 0.000010000000000
-
-
- 0.010000000000000
-
-
- 0.600000000000000
-
+
-
+
+
+
+ 0
+
+
-
+
+
+ Perpendicular
+
+
-
+
+ Perpendicular
+
+
+ -
+
+ Parallel
+
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+
+ 40
+ 20
+
+
+
+
+
- -
-
+
-
+
- nm<sup>-1</sup>
+ Maximum q:
-
-
-
- -
-
-
- Exchange Coefficient (A):
-
-
-
- -
-
-
-
- 0
-
-
-
-
-
- 100000.000000000000000
-
-
- 5.000000000000000
-
+
-
+
+
+
+ 0
+
+
-
+
+
+ 5
+
+
+ 0.000010000000000
+
+
+ 0.010000000000000
+
+
+ 0.600000000000000
+
+
+
+ -
+
+
+ nm<sup>-1</sup>
+
+
+
+
- -
-
+
-
+
- to
+ Applied Field (μ<sub>0</sub> H<sub>min</sub>):
- -
-
-
- 100000.000000000000000
-
-
- 20.000000000000000
-
+
-
+
+
+
+ 0
+
+
-
+
+
+ 5
+
+
+ 10000.000000000000000
+
+
+ 75.000000000000000
+
+
+
+ -
+
+
+ mT
+
+
+
+
- -
-
+
-
+
- pJ/m,
+ Scan Range for A:
- -
-
-
- 10000
-
-
- 200
-
+
-
+
+
+
+ 0
+
+
+ 0
+
+
-
+
+
+ 100000.000000000000000
+
+
+ 5.000000000000000
+
+
+
+ -
+
+
+ to
+
+
+
+ -
+
+
+ 100000.000000000000000
+
+
+ 20.000000000000000
+
+
+
+ -
+
+
+ pJ/m
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+
+ 40
+ 20
+
+
+
+
+
- -
-
-
- steps
-
+
-
+
+
+
+ 7
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
-
+
+
+ 10000
+
+
+ 200
+
+
+
+ -
+
+
+ steps
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+
+ 40
+ 20
+
+
+
+
+
+
+
+
+ -
+
+
+ Results
+
+
+
+ Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
+
-
-
+
+
+ A value:
+
+
+
+ -
+
- Analysis Method:
+ A uncertainty:
-
-
-
-
- 0
-
-
-
-
-
- Perpendicular
-
-
-
-
- Perpendicular
-
-
- -
-
- Parallel
-
-
-
-
-
+
+
+
+
+
+
+ -
+
+
+
+
+ -
+
+
+ Qt::Vertical
+
+
+
+ 20
+ 40
+
+
+
+
@@ -290,7 +401,7 @@
0
0
- 549
+ 819
20
diff --git a/src/sas/qtgui/Utilities/MuMagTool/fit_result.py b/src/sas/qtgui/Utilities/MuMagTool/fit_result.py
index 33b0e64e57..e05562ee71 100644
--- a/src/sas/qtgui/Utilities/MuMagTool/fit_result.py
+++ b/src/sas/qtgui/Utilities/MuMagTool/fit_result.py
@@ -3,7 +3,7 @@
import numpy as np
from sas.qtgui.Utilities.MuMagTool.experimental_data import ExperimentalData
-from sas.qtgui.Utilities.MuMagTool.fit_parameters import ExperimentGeometry
+from sas.qtgui.Utilities.MuMagTool.fit_parameters import ExperimentGeometry, FitParameters
from sas.qtgui.Utilities.MuMagTool.least_squares_output import LeastSquaresOutputPerpendicular, \
LeastSquaresOutputParallel
from sas.qtgui.Utilities.MuMagTool.sweep_output import SweepOutput
@@ -12,6 +12,7 @@
@dataclass
class FitResults:
""" Output the MuMag fit """
+ parameters: FitParameters
input_data: list[ExperimentalData]
sweep_data: SweepOutput
refined_fit_data: LeastSquaresOutputParallel | LeastSquaresOutputPerpendicular
From c29f5c5f4883c70ae1f44b47202b13a134dcd4e0 Mon Sep 17 00:00:00 2001
From: lucas-wilkins
Date: Mon, 10 Jun 2024 17:27:34 +0100
Subject: [PATCH 37/46] Done!!!!!
---
src/sas/qtgui/Utilities/MuMagTool/MuMag.py | 19 ++++++++++---------
1 file changed, 10 insertions(+), 9 deletions(-)
diff --git a/src/sas/qtgui/Utilities/MuMagTool/MuMag.py b/src/sas/qtgui/Utilities/MuMagTool/MuMag.py
index aa7bef2e71..6fae9fe3d7 100644
--- a/src/sas/qtgui/Utilities/MuMagTool/MuMag.py
+++ b/src/sas/qtgui/Utilities/MuMagTool/MuMag.py
@@ -275,21 +275,22 @@ def show_fit_results(self):
ilim = MuMagLib.nice_log_plot_bounds([datum.scattering_curve.y for datum in self.data])
# Show the experimental data
- # colors = pl.cm.jet(np.linspace(0, 1, len(self.fit_data.input_data)))
- # for k, datum in enumerate(self.fit_data.input_data):
- # self.comparison_axes.loglog(
- # datum.scattering_curve.x,
- # datum.scattering_curve.y,
- # linestyle='dotted', color=colors[k], linewidth=0.3, markersize=1)
+ colors = pl.cm.jet(np.linspace(0, 1, len(self.fit_data.input_data)))
+ for k, datum in enumerate(self.fit_data.input_data):
+ self.comparison_axes.loglog(
+ datum.scattering_curve.x,
+ datum.scattering_curve.y,
+ linestyle='dotted', color=colors[k], linewidth=0.3, markersize=1)
+ # Show the fitted curves
n_sim = self.fit_data.refined_fit_data.I_simulated.shape[0]
colors = pl.cm.YlGn(np.linspace(0, 1, n_sim))
- for k in range(0, n_sim):
+ for k in range(n_sim):
self.comparison_axes.loglog(
- self.fit_data.refined_fit_data.q,
+ self.fit_data.refined_fit_data.q * 1e-9,
self.fit_data.refined_fit_data.I_simulated[k, :],
linestyle='solid', color=colors[k],
- linewidth=0.5, label='(fit) B_0 = ' + str(self.fit_data.input_data[k].applied_field) + ' T')
+ label='B_0 = ' + str(self.fit_data.input_data[k].applied_field) + ' T')
self.comparison_axes.set_xlabel(r'$q$ [1/nm]')
self.comparison_axes.set_ylabel(r'$I_{\mathrm{exp}}$')
From cd7db0eb782eb892cfcb080c14ff4e840cec0bf3 Mon Sep 17 00:00:00 2001
From: lucas-wilkins
Date: Mon, 10 Jun 2024 17:41:05 +0100
Subject: [PATCH 38/46] Tidying up
---
src/sas/qtgui/MainWindow/GuiManager.py | 2 +-
.../Utilities/{MuMagTool => MuMag}/MuMag.py | 14 +-
.../{MuMagTool => MuMag}/MuMagLib.py | 9 +-
.../10_1000_1340_10.csv | 0
.../11_2000_1340_10.csv | 0
.../12_3000_1340_10.csv | 0
.../13_8000_1340_10.csv | 0
.../1_0_1340_10.csv | 0
.../2_20_1340_10.csv | 0
.../3_35_1340_10.csv | 0
.../4_50_1340_10.csv | 0
.../5_75_1340_10.csv | 0
.../6_100_1340_10.csv | 0
.../7_200_1340_10.csv | 0
.../8_300_1340_10.csv | 0
.../9_600_1340_10.csv | 0
.../1_33_1640_22.874115.csv | 0
.../2_42_1640_23.456895.csv | 0
.../3_61_1640_23.748285.csv | 0
.../4_103_1640_24.039675.csv | 0
.../5_312_1640_24.331065.csv | 0
.../6_1270_1640_24.331065.csv | 0
.../1_8000_1600_1070.csv | 0
.../2_10000_1600_1070.csv | 0
.../3_12000_1600_1070.csv | 0
.../4_14000_1600_1070.csv | 0
.../5_16000_1600_1070.csv | 0
.../{MuMagTool => MuMag}/SANSData/__init__.py | 0
.../{MuMagTool => MuMag}/UI/MuMagUI.ui | 0
.../{MuMagTool => MuMag}/UI/__init__.py | 0
.../{MuMagTool => MuMag}/__init__.py | 0
.../qtgui/Utilities/MuMag/datastructures.py | 124 ++++++++++++++++++
.../Utilities/{MuMagTool => MuMag}/models.py | 2 +
.../Utilities/MuMagTool/experimental_data.py | 29 ----
src/sas/qtgui/Utilities/MuMagTool/failure.py | 7 -
.../Utilities/MuMagTool/fit_parameters.py | 20 ---
.../qtgui/Utilities/MuMagTool/fit_result.py | 19 ---
.../MuMagTool/least_squares_output.py | 30 -----
.../qtgui/Utilities/MuMagTool/sweep_output.py | 20 ---
39 files changed, 134 insertions(+), 142 deletions(-)
rename src/sas/qtgui/Utilities/{MuMagTool => MuMag}/MuMag.py (95%)
rename src/sas/qtgui/Utilities/{MuMagTool => MuMag}/MuMagLib.py (97%)
rename src/sas/qtgui/Utilities/{MuMagTool => MuMag}/SANSData/FeNiB_perpendicular_Bersweiler_et_al/10_1000_1340_10.csv (100%)
rename src/sas/qtgui/Utilities/{MuMagTool => MuMag}/SANSData/FeNiB_perpendicular_Bersweiler_et_al/11_2000_1340_10.csv (100%)
rename src/sas/qtgui/Utilities/{MuMagTool => MuMag}/SANSData/FeNiB_perpendicular_Bersweiler_et_al/12_3000_1340_10.csv (100%)
rename src/sas/qtgui/Utilities/{MuMagTool => MuMag}/SANSData/FeNiB_perpendicular_Bersweiler_et_al/13_8000_1340_10.csv (100%)
rename src/sas/qtgui/Utilities/{MuMagTool => MuMag}/SANSData/FeNiB_perpendicular_Bersweiler_et_al/1_0_1340_10.csv (100%)
rename src/sas/qtgui/Utilities/{MuMagTool => MuMag}/SANSData/FeNiB_perpendicular_Bersweiler_et_al/2_20_1340_10.csv (100%)
rename src/sas/qtgui/Utilities/{MuMagTool => MuMag}/SANSData/FeNiB_perpendicular_Bersweiler_et_al/3_35_1340_10.csv (100%)
rename src/sas/qtgui/Utilities/{MuMagTool => MuMag}/SANSData/FeNiB_perpendicular_Bersweiler_et_al/4_50_1340_10.csv (100%)
rename src/sas/qtgui/Utilities/{MuMagTool => MuMag}/SANSData/FeNiB_perpendicular_Bersweiler_et_al/5_75_1340_10.csv (100%)
rename src/sas/qtgui/Utilities/{MuMagTool => MuMag}/SANSData/FeNiB_perpendicular_Bersweiler_et_al/6_100_1340_10.csv (100%)
rename src/sas/qtgui/Utilities/{MuMagTool => MuMag}/SANSData/FeNiB_perpendicular_Bersweiler_et_al/7_200_1340_10.csv (100%)
rename src/sas/qtgui/Utilities/{MuMagTool => MuMag}/SANSData/FeNiB_perpendicular_Bersweiler_et_al/8_300_1340_10.csv (100%)
rename src/sas/qtgui/Utilities/{MuMagTool => MuMag}/SANSData/FeNiB_perpendicular_Bersweiler_et_al/9_600_1340_10.csv (100%)
rename src/sas/qtgui/Utilities/{MuMagTool => MuMag}/SANSData/Nanoperm_perpendicular_Honecker_et_al/1_33_1640_22.874115.csv (100%)
rename src/sas/qtgui/Utilities/{MuMagTool => MuMag}/SANSData/Nanoperm_perpendicular_Honecker_et_al/2_42_1640_23.456895.csv (100%)
rename src/sas/qtgui/Utilities/{MuMagTool => MuMag}/SANSData/Nanoperm_perpendicular_Honecker_et_al/3_61_1640_23.748285.csv (100%)
rename src/sas/qtgui/Utilities/{MuMagTool => MuMag}/SANSData/Nanoperm_perpendicular_Honecker_et_al/4_103_1640_24.039675.csv (100%)
rename src/sas/qtgui/Utilities/{MuMagTool => MuMag}/SANSData/Nanoperm_perpendicular_Honecker_et_al/5_312_1640_24.331065.csv (100%)
rename src/sas/qtgui/Utilities/{MuMagTool => MuMag}/SANSData/Nanoperm_perpendicular_Honecker_et_al/6_1270_1640_24.331065.csv (100%)
rename src/sas/qtgui/Utilities/{MuMagTool => MuMag}/SANSData/NdFeB_parallel_Bick_et_al/1_8000_1600_1070.csv (100%)
rename src/sas/qtgui/Utilities/{MuMagTool => MuMag}/SANSData/NdFeB_parallel_Bick_et_al/2_10000_1600_1070.csv (100%)
rename src/sas/qtgui/Utilities/{MuMagTool => MuMag}/SANSData/NdFeB_parallel_Bick_et_al/3_12000_1600_1070.csv (100%)
rename src/sas/qtgui/Utilities/{MuMagTool => MuMag}/SANSData/NdFeB_parallel_Bick_et_al/4_14000_1600_1070.csv (100%)
rename src/sas/qtgui/Utilities/{MuMagTool => MuMag}/SANSData/NdFeB_parallel_Bick_et_al/5_16000_1600_1070.csv (100%)
rename src/sas/qtgui/Utilities/{MuMagTool => MuMag}/SANSData/__init__.py (100%)
rename src/sas/qtgui/Utilities/{MuMagTool => MuMag}/UI/MuMagUI.ui (100%)
rename src/sas/qtgui/Utilities/{MuMagTool => MuMag}/UI/__init__.py (100%)
rename src/sas/qtgui/Utilities/{MuMagTool => MuMag}/__init__.py (100%)
create mode 100644 src/sas/qtgui/Utilities/MuMag/datastructures.py
rename src/sas/qtgui/Utilities/{MuMagTool => MuMag}/models.py (98%)
delete mode 100644 src/sas/qtgui/Utilities/MuMagTool/experimental_data.py
delete mode 100644 src/sas/qtgui/Utilities/MuMagTool/failure.py
delete mode 100644 src/sas/qtgui/Utilities/MuMagTool/fit_parameters.py
delete mode 100644 src/sas/qtgui/Utilities/MuMagTool/fit_result.py
delete mode 100644 src/sas/qtgui/Utilities/MuMagTool/least_squares_output.py
delete mode 100644 src/sas/qtgui/Utilities/MuMagTool/sweep_output.py
diff --git a/src/sas/qtgui/MainWindow/GuiManager.py b/src/sas/qtgui/MainWindow/GuiManager.py
index ab701ce215..31043bce1a 100644
--- a/src/sas/qtgui/MainWindow/GuiManager.py
+++ b/src/sas/qtgui/MainWindow/GuiManager.py
@@ -35,7 +35,7 @@
from sas.qtgui.Utilities.OrientationViewer.OrientationViewer import show_orientation_viewer
from sas.qtgui.Utilities.HidableDialog import hidable_dialog
-from sas.qtgui.Utilities.MuMagTool.MuMag import MuMag
+from sas.qtgui.Utilities.MuMag.MuMag import MuMag
from sas.qtgui.Utilities.DocViewWidget import DocViewWindow
from sas.qtgui.Utilities.DocRegenInProgess import DocRegenProgress
diff --git a/src/sas/qtgui/Utilities/MuMagTool/MuMag.py b/src/sas/qtgui/Utilities/MuMag/MuMag.py
similarity index 95%
rename from src/sas/qtgui/Utilities/MuMagTool/MuMag.py
rename to src/sas/qtgui/Utilities/MuMag/MuMag.py
index 6fae9fe3d7..907bcdb177 100644
--- a/src/sas/qtgui/Utilities/MuMagTool/MuMag.py
+++ b/src/sas/qtgui/Utilities/MuMag/MuMag.py
@@ -1,7 +1,7 @@
-from sas.qtgui.Utilities.MuMagTool.UI.MuMagUI import Ui_MuMagTool
+from sas.qtgui.Utilities.MuMag.UI.MuMagUI import Ui_MuMagTool
from PySide6.QtWidgets import QVBoxLayout
from PySide6 import QtWidgets
-from sas.qtgui.Utilities.MuMagTool import MuMagLib
+from sas.qtgui.Utilities.MuMag import MuMagLib
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
import matplotlib.pyplot as plt
@@ -9,16 +9,12 @@
import matplotlib.pylab as pl
import numpy as np
-from sas.qtgui.Utilities.MuMagTool.experimental_data import ExperimentalData
-from sas.qtgui.Utilities.MuMagTool.failure import LoadFailure, FitFailure
-from sas.qtgui.Utilities.MuMagTool.fit_parameters import FitParameters, ExperimentGeometry
-from sas.qtgui.Utilities.MuMagTool.MuMagLib import MuMagLib
+from sas.qtgui.Utilities.MuMag.datastructures import ExperimentalData, LoadFailure, FitFailure, ExperimentGeometry, \
+ FitParameters, FitResults, LeastSquaresOutputPerpendicular
+from sas.qtgui.Utilities.MuMag.MuMagLib import MuMagLib
from logging import getLogger
-from sas.qtgui.Utilities.MuMagTool.fit_result import FitResults
-from sas.qtgui.Utilities.MuMagTool.least_squares_output import LeastSquaresOutputPerpendicular
-
log = getLogger("MuMag")
class MuMag(QtWidgets.QMainWindow, Ui_MuMagTool):
diff --git a/src/sas/qtgui/Utilities/MuMagTool/MuMagLib.py b/src/sas/qtgui/Utilities/MuMag/MuMagLib.py
similarity index 97%
rename from src/sas/qtgui/Utilities/MuMagTool/MuMagLib.py
rename to src/sas/qtgui/Utilities/MuMag/MuMagLib.py
index 63c089996e..662e620278 100644
--- a/src/sas/qtgui/Utilities/MuMagTool/MuMagLib.py
+++ b/src/sas/qtgui/Utilities/MuMag/MuMagLib.py
@@ -9,13 +9,8 @@
from PySide6 import QtWidgets
from PySide6.QtWidgets import QFileDialog
-from sas.qtgui.Utilities.MuMagTool.experimental_data import ExperimentalData
-from sas.qtgui.Utilities.MuMagTool.failure import LoadFailure, FitFailure
-from sas.qtgui.Utilities.MuMagTool.fit_parameters import FitParameters, ExperimentGeometry
-from sas.qtgui.Utilities.MuMagTool.fit_result import FitResults
-from sas.qtgui.Utilities.MuMagTool.least_squares_output import LeastSquaresOutputPerpendicular, \
- LeastSquaresOutputParallel
-from sas.qtgui.Utilities.MuMagTool.sweep_output import SweepOutput
+from sas.qtgui.Utilities.MuMag.datastructures import ExperimentalData, LoadFailure, FitFailure, ExperimentGeometry, \
+ FitParameters, FitResults, LeastSquaresOutputParallel, LeastSquaresOutputPerpendicular, SweepOutput
from sasdata.dataloader.loader import Loader
diff --git a/src/sas/qtgui/Utilities/MuMagTool/SANSData/FeNiB_perpendicular_Bersweiler_et_al/10_1000_1340_10.csv b/src/sas/qtgui/Utilities/MuMag/SANSData/FeNiB_perpendicular_Bersweiler_et_al/10_1000_1340_10.csv
similarity index 100%
rename from src/sas/qtgui/Utilities/MuMagTool/SANSData/FeNiB_perpendicular_Bersweiler_et_al/10_1000_1340_10.csv
rename to src/sas/qtgui/Utilities/MuMag/SANSData/FeNiB_perpendicular_Bersweiler_et_al/10_1000_1340_10.csv
diff --git a/src/sas/qtgui/Utilities/MuMagTool/SANSData/FeNiB_perpendicular_Bersweiler_et_al/11_2000_1340_10.csv b/src/sas/qtgui/Utilities/MuMag/SANSData/FeNiB_perpendicular_Bersweiler_et_al/11_2000_1340_10.csv
similarity index 100%
rename from src/sas/qtgui/Utilities/MuMagTool/SANSData/FeNiB_perpendicular_Bersweiler_et_al/11_2000_1340_10.csv
rename to src/sas/qtgui/Utilities/MuMag/SANSData/FeNiB_perpendicular_Bersweiler_et_al/11_2000_1340_10.csv
diff --git a/src/sas/qtgui/Utilities/MuMagTool/SANSData/FeNiB_perpendicular_Bersweiler_et_al/12_3000_1340_10.csv b/src/sas/qtgui/Utilities/MuMag/SANSData/FeNiB_perpendicular_Bersweiler_et_al/12_3000_1340_10.csv
similarity index 100%
rename from src/sas/qtgui/Utilities/MuMagTool/SANSData/FeNiB_perpendicular_Bersweiler_et_al/12_3000_1340_10.csv
rename to src/sas/qtgui/Utilities/MuMag/SANSData/FeNiB_perpendicular_Bersweiler_et_al/12_3000_1340_10.csv
diff --git a/src/sas/qtgui/Utilities/MuMagTool/SANSData/FeNiB_perpendicular_Bersweiler_et_al/13_8000_1340_10.csv b/src/sas/qtgui/Utilities/MuMag/SANSData/FeNiB_perpendicular_Bersweiler_et_al/13_8000_1340_10.csv
similarity index 100%
rename from src/sas/qtgui/Utilities/MuMagTool/SANSData/FeNiB_perpendicular_Bersweiler_et_al/13_8000_1340_10.csv
rename to src/sas/qtgui/Utilities/MuMag/SANSData/FeNiB_perpendicular_Bersweiler_et_al/13_8000_1340_10.csv
diff --git a/src/sas/qtgui/Utilities/MuMagTool/SANSData/FeNiB_perpendicular_Bersweiler_et_al/1_0_1340_10.csv b/src/sas/qtgui/Utilities/MuMag/SANSData/FeNiB_perpendicular_Bersweiler_et_al/1_0_1340_10.csv
similarity index 100%
rename from src/sas/qtgui/Utilities/MuMagTool/SANSData/FeNiB_perpendicular_Bersweiler_et_al/1_0_1340_10.csv
rename to src/sas/qtgui/Utilities/MuMag/SANSData/FeNiB_perpendicular_Bersweiler_et_al/1_0_1340_10.csv
diff --git a/src/sas/qtgui/Utilities/MuMagTool/SANSData/FeNiB_perpendicular_Bersweiler_et_al/2_20_1340_10.csv b/src/sas/qtgui/Utilities/MuMag/SANSData/FeNiB_perpendicular_Bersweiler_et_al/2_20_1340_10.csv
similarity index 100%
rename from src/sas/qtgui/Utilities/MuMagTool/SANSData/FeNiB_perpendicular_Bersweiler_et_al/2_20_1340_10.csv
rename to src/sas/qtgui/Utilities/MuMag/SANSData/FeNiB_perpendicular_Bersweiler_et_al/2_20_1340_10.csv
diff --git a/src/sas/qtgui/Utilities/MuMagTool/SANSData/FeNiB_perpendicular_Bersweiler_et_al/3_35_1340_10.csv b/src/sas/qtgui/Utilities/MuMag/SANSData/FeNiB_perpendicular_Bersweiler_et_al/3_35_1340_10.csv
similarity index 100%
rename from src/sas/qtgui/Utilities/MuMagTool/SANSData/FeNiB_perpendicular_Bersweiler_et_al/3_35_1340_10.csv
rename to src/sas/qtgui/Utilities/MuMag/SANSData/FeNiB_perpendicular_Bersweiler_et_al/3_35_1340_10.csv
diff --git a/src/sas/qtgui/Utilities/MuMagTool/SANSData/FeNiB_perpendicular_Bersweiler_et_al/4_50_1340_10.csv b/src/sas/qtgui/Utilities/MuMag/SANSData/FeNiB_perpendicular_Bersweiler_et_al/4_50_1340_10.csv
similarity index 100%
rename from src/sas/qtgui/Utilities/MuMagTool/SANSData/FeNiB_perpendicular_Bersweiler_et_al/4_50_1340_10.csv
rename to src/sas/qtgui/Utilities/MuMag/SANSData/FeNiB_perpendicular_Bersweiler_et_al/4_50_1340_10.csv
diff --git a/src/sas/qtgui/Utilities/MuMagTool/SANSData/FeNiB_perpendicular_Bersweiler_et_al/5_75_1340_10.csv b/src/sas/qtgui/Utilities/MuMag/SANSData/FeNiB_perpendicular_Bersweiler_et_al/5_75_1340_10.csv
similarity index 100%
rename from src/sas/qtgui/Utilities/MuMagTool/SANSData/FeNiB_perpendicular_Bersweiler_et_al/5_75_1340_10.csv
rename to src/sas/qtgui/Utilities/MuMag/SANSData/FeNiB_perpendicular_Bersweiler_et_al/5_75_1340_10.csv
diff --git a/src/sas/qtgui/Utilities/MuMagTool/SANSData/FeNiB_perpendicular_Bersweiler_et_al/6_100_1340_10.csv b/src/sas/qtgui/Utilities/MuMag/SANSData/FeNiB_perpendicular_Bersweiler_et_al/6_100_1340_10.csv
similarity index 100%
rename from src/sas/qtgui/Utilities/MuMagTool/SANSData/FeNiB_perpendicular_Bersweiler_et_al/6_100_1340_10.csv
rename to src/sas/qtgui/Utilities/MuMag/SANSData/FeNiB_perpendicular_Bersweiler_et_al/6_100_1340_10.csv
diff --git a/src/sas/qtgui/Utilities/MuMagTool/SANSData/FeNiB_perpendicular_Bersweiler_et_al/7_200_1340_10.csv b/src/sas/qtgui/Utilities/MuMag/SANSData/FeNiB_perpendicular_Bersweiler_et_al/7_200_1340_10.csv
similarity index 100%
rename from src/sas/qtgui/Utilities/MuMagTool/SANSData/FeNiB_perpendicular_Bersweiler_et_al/7_200_1340_10.csv
rename to src/sas/qtgui/Utilities/MuMag/SANSData/FeNiB_perpendicular_Bersweiler_et_al/7_200_1340_10.csv
diff --git a/src/sas/qtgui/Utilities/MuMagTool/SANSData/FeNiB_perpendicular_Bersweiler_et_al/8_300_1340_10.csv b/src/sas/qtgui/Utilities/MuMag/SANSData/FeNiB_perpendicular_Bersweiler_et_al/8_300_1340_10.csv
similarity index 100%
rename from src/sas/qtgui/Utilities/MuMagTool/SANSData/FeNiB_perpendicular_Bersweiler_et_al/8_300_1340_10.csv
rename to src/sas/qtgui/Utilities/MuMag/SANSData/FeNiB_perpendicular_Bersweiler_et_al/8_300_1340_10.csv
diff --git a/src/sas/qtgui/Utilities/MuMagTool/SANSData/FeNiB_perpendicular_Bersweiler_et_al/9_600_1340_10.csv b/src/sas/qtgui/Utilities/MuMag/SANSData/FeNiB_perpendicular_Bersweiler_et_al/9_600_1340_10.csv
similarity index 100%
rename from src/sas/qtgui/Utilities/MuMagTool/SANSData/FeNiB_perpendicular_Bersweiler_et_al/9_600_1340_10.csv
rename to src/sas/qtgui/Utilities/MuMag/SANSData/FeNiB_perpendicular_Bersweiler_et_al/9_600_1340_10.csv
diff --git a/src/sas/qtgui/Utilities/MuMagTool/SANSData/Nanoperm_perpendicular_Honecker_et_al/1_33_1640_22.874115.csv b/src/sas/qtgui/Utilities/MuMag/SANSData/Nanoperm_perpendicular_Honecker_et_al/1_33_1640_22.874115.csv
similarity index 100%
rename from src/sas/qtgui/Utilities/MuMagTool/SANSData/Nanoperm_perpendicular_Honecker_et_al/1_33_1640_22.874115.csv
rename to src/sas/qtgui/Utilities/MuMag/SANSData/Nanoperm_perpendicular_Honecker_et_al/1_33_1640_22.874115.csv
diff --git a/src/sas/qtgui/Utilities/MuMagTool/SANSData/Nanoperm_perpendicular_Honecker_et_al/2_42_1640_23.456895.csv b/src/sas/qtgui/Utilities/MuMag/SANSData/Nanoperm_perpendicular_Honecker_et_al/2_42_1640_23.456895.csv
similarity index 100%
rename from src/sas/qtgui/Utilities/MuMagTool/SANSData/Nanoperm_perpendicular_Honecker_et_al/2_42_1640_23.456895.csv
rename to src/sas/qtgui/Utilities/MuMag/SANSData/Nanoperm_perpendicular_Honecker_et_al/2_42_1640_23.456895.csv
diff --git a/src/sas/qtgui/Utilities/MuMagTool/SANSData/Nanoperm_perpendicular_Honecker_et_al/3_61_1640_23.748285.csv b/src/sas/qtgui/Utilities/MuMag/SANSData/Nanoperm_perpendicular_Honecker_et_al/3_61_1640_23.748285.csv
similarity index 100%
rename from src/sas/qtgui/Utilities/MuMagTool/SANSData/Nanoperm_perpendicular_Honecker_et_al/3_61_1640_23.748285.csv
rename to src/sas/qtgui/Utilities/MuMag/SANSData/Nanoperm_perpendicular_Honecker_et_al/3_61_1640_23.748285.csv
diff --git a/src/sas/qtgui/Utilities/MuMagTool/SANSData/Nanoperm_perpendicular_Honecker_et_al/4_103_1640_24.039675.csv b/src/sas/qtgui/Utilities/MuMag/SANSData/Nanoperm_perpendicular_Honecker_et_al/4_103_1640_24.039675.csv
similarity index 100%
rename from src/sas/qtgui/Utilities/MuMagTool/SANSData/Nanoperm_perpendicular_Honecker_et_al/4_103_1640_24.039675.csv
rename to src/sas/qtgui/Utilities/MuMag/SANSData/Nanoperm_perpendicular_Honecker_et_al/4_103_1640_24.039675.csv
diff --git a/src/sas/qtgui/Utilities/MuMagTool/SANSData/Nanoperm_perpendicular_Honecker_et_al/5_312_1640_24.331065.csv b/src/sas/qtgui/Utilities/MuMag/SANSData/Nanoperm_perpendicular_Honecker_et_al/5_312_1640_24.331065.csv
similarity index 100%
rename from src/sas/qtgui/Utilities/MuMagTool/SANSData/Nanoperm_perpendicular_Honecker_et_al/5_312_1640_24.331065.csv
rename to src/sas/qtgui/Utilities/MuMag/SANSData/Nanoperm_perpendicular_Honecker_et_al/5_312_1640_24.331065.csv
diff --git a/src/sas/qtgui/Utilities/MuMagTool/SANSData/Nanoperm_perpendicular_Honecker_et_al/6_1270_1640_24.331065.csv b/src/sas/qtgui/Utilities/MuMag/SANSData/Nanoperm_perpendicular_Honecker_et_al/6_1270_1640_24.331065.csv
similarity index 100%
rename from src/sas/qtgui/Utilities/MuMagTool/SANSData/Nanoperm_perpendicular_Honecker_et_al/6_1270_1640_24.331065.csv
rename to src/sas/qtgui/Utilities/MuMag/SANSData/Nanoperm_perpendicular_Honecker_et_al/6_1270_1640_24.331065.csv
diff --git a/src/sas/qtgui/Utilities/MuMagTool/SANSData/NdFeB_parallel_Bick_et_al/1_8000_1600_1070.csv b/src/sas/qtgui/Utilities/MuMag/SANSData/NdFeB_parallel_Bick_et_al/1_8000_1600_1070.csv
similarity index 100%
rename from src/sas/qtgui/Utilities/MuMagTool/SANSData/NdFeB_parallel_Bick_et_al/1_8000_1600_1070.csv
rename to src/sas/qtgui/Utilities/MuMag/SANSData/NdFeB_parallel_Bick_et_al/1_8000_1600_1070.csv
diff --git a/src/sas/qtgui/Utilities/MuMagTool/SANSData/NdFeB_parallel_Bick_et_al/2_10000_1600_1070.csv b/src/sas/qtgui/Utilities/MuMag/SANSData/NdFeB_parallel_Bick_et_al/2_10000_1600_1070.csv
similarity index 100%
rename from src/sas/qtgui/Utilities/MuMagTool/SANSData/NdFeB_parallel_Bick_et_al/2_10000_1600_1070.csv
rename to src/sas/qtgui/Utilities/MuMag/SANSData/NdFeB_parallel_Bick_et_al/2_10000_1600_1070.csv
diff --git a/src/sas/qtgui/Utilities/MuMagTool/SANSData/NdFeB_parallel_Bick_et_al/3_12000_1600_1070.csv b/src/sas/qtgui/Utilities/MuMag/SANSData/NdFeB_parallel_Bick_et_al/3_12000_1600_1070.csv
similarity index 100%
rename from src/sas/qtgui/Utilities/MuMagTool/SANSData/NdFeB_parallel_Bick_et_al/3_12000_1600_1070.csv
rename to src/sas/qtgui/Utilities/MuMag/SANSData/NdFeB_parallel_Bick_et_al/3_12000_1600_1070.csv
diff --git a/src/sas/qtgui/Utilities/MuMagTool/SANSData/NdFeB_parallel_Bick_et_al/4_14000_1600_1070.csv b/src/sas/qtgui/Utilities/MuMag/SANSData/NdFeB_parallel_Bick_et_al/4_14000_1600_1070.csv
similarity index 100%
rename from src/sas/qtgui/Utilities/MuMagTool/SANSData/NdFeB_parallel_Bick_et_al/4_14000_1600_1070.csv
rename to src/sas/qtgui/Utilities/MuMag/SANSData/NdFeB_parallel_Bick_et_al/4_14000_1600_1070.csv
diff --git a/src/sas/qtgui/Utilities/MuMagTool/SANSData/NdFeB_parallel_Bick_et_al/5_16000_1600_1070.csv b/src/sas/qtgui/Utilities/MuMag/SANSData/NdFeB_parallel_Bick_et_al/5_16000_1600_1070.csv
similarity index 100%
rename from src/sas/qtgui/Utilities/MuMagTool/SANSData/NdFeB_parallel_Bick_et_al/5_16000_1600_1070.csv
rename to src/sas/qtgui/Utilities/MuMag/SANSData/NdFeB_parallel_Bick_et_al/5_16000_1600_1070.csv
diff --git a/src/sas/qtgui/Utilities/MuMagTool/SANSData/__init__.py b/src/sas/qtgui/Utilities/MuMag/SANSData/__init__.py
similarity index 100%
rename from src/sas/qtgui/Utilities/MuMagTool/SANSData/__init__.py
rename to src/sas/qtgui/Utilities/MuMag/SANSData/__init__.py
diff --git a/src/sas/qtgui/Utilities/MuMagTool/UI/MuMagUI.ui b/src/sas/qtgui/Utilities/MuMag/UI/MuMagUI.ui
similarity index 100%
rename from src/sas/qtgui/Utilities/MuMagTool/UI/MuMagUI.ui
rename to src/sas/qtgui/Utilities/MuMag/UI/MuMagUI.ui
diff --git a/src/sas/qtgui/Utilities/MuMagTool/UI/__init__.py b/src/sas/qtgui/Utilities/MuMag/UI/__init__.py
similarity index 100%
rename from src/sas/qtgui/Utilities/MuMagTool/UI/__init__.py
rename to src/sas/qtgui/Utilities/MuMag/UI/__init__.py
diff --git a/src/sas/qtgui/Utilities/MuMagTool/__init__.py b/src/sas/qtgui/Utilities/MuMag/__init__.py
similarity index 100%
rename from src/sas/qtgui/Utilities/MuMagTool/__init__.py
rename to src/sas/qtgui/Utilities/MuMag/__init__.py
diff --git a/src/sas/qtgui/Utilities/MuMag/datastructures.py b/src/sas/qtgui/Utilities/MuMag/datastructures.py
new file mode 100644
index 0000000000..0cf59de57b
--- /dev/null
+++ b/src/sas/qtgui/Utilities/MuMag/datastructures.py
@@ -0,0 +1,124 @@
+from dataclasses import dataclass
+from enum import Enum
+from typing import Generic
+
+import numpy as np
+
+from sas.qtgui.Plotting.PlotterData import Data1D
+
+from typing import TypeVar
+
+""" Data structures used in MuMag"""
+
+#
+# Errors
+#
+
+
+class LoadFailure(Exception):
+ """ File loading failed """
+ pass
+
+
+class FitFailure(Exception):
+ """ Fit failed """
+ pass
+
+#
+# Enums
+#
+
+
+class ExperimentGeometry(Enum):
+ """ Type of experiment """
+ PARALLEL = 1
+ PERPENDICULAR = 2
+
+#
+# Data classes
+#
+
+
+@dataclass
+class ExperimentalData:
+ """ Datapoint used as input for the MuMag tool"""
+
+ scattering_curve: Data1D
+
+ applied_field: float
+ saturation_magnetisation: float
+ demagnetising_field: float
+
+ def restrict_by_index(self, max_index: int):
+ """ Remove all points from data up to given index"""
+
+ x = self.scattering_curve.x[:max_index]
+ y = self.scattering_curve.y[:max_index]
+ dy = self.scattering_curve.dy[:max_index]
+
+ return ExperimentalData(
+ scattering_curve=Data1D(x=x, y=y, dy=dy),
+ applied_field=self.applied_field,
+ saturation_magnetisation=self.saturation_magnetisation,
+ demagnetising_field=self.demagnetising_field)
+
+
+@dataclass
+class FitParameters:
+ """ Input parameters for the fit"""
+ q_max: float
+ min_applied_field: float
+ exchange_A_min: float
+ exchange_A_max: float
+ exchange_A_n: int
+ experiment_geometry: ExperimentGeometry
+
+
+@dataclass
+class LeastSquaresOutput:
+ """ Output from least squares method"""
+ exchange_A: float
+ exchange_A_chi_sq: float
+ q: np.ndarray
+ I_simulated: np.ndarray
+ I_residual: np.ndarray
+ S_H: np.ndarray
+ I_residual_stdev: np.ndarray
+ S_H_stdev: np.ndarray
+
+
+@dataclass
+class LeastSquaresOutputParallel(LeastSquaresOutput):
+ """ Output from least squares method for parallel case"""
+ pass
+
+
+@dataclass
+class LeastSquaresOutputPerpendicular(LeastSquaresOutput):
+ """ Output from least squares method for perpendicular case"""
+ S_M: np.ndarray
+ S_M_stdev: np.ndarray
+
+
+T = TypeVar("T", bound=LeastSquaresOutput)
+
+
+@dataclass
+class SweepOutput(Generic[T]):
+ """
+ Results from brute force optimisiation of the chi squared for the exchange A parameter
+ """
+
+ exchange_A_checked: np.ndarray
+ exchange_A_chi_sq: np.ndarray
+ optimal: T
+
+
+@dataclass
+class FitResults:
+ """ Output the MuMag fit """
+ parameters: FitParameters
+ input_data: list[ExperimentalData]
+ sweep_data: SweepOutput
+ refined_fit_data: LeastSquaresOutputParallel | LeastSquaresOutputPerpendicular
+ optimal_exchange_A_uncertainty: float
diff --git a/src/sas/qtgui/Utilities/MuMagTool/models.py b/src/sas/qtgui/Utilities/MuMag/models.py
similarity index 98%
rename from src/sas/qtgui/Utilities/MuMagTool/models.py
rename to src/sas/qtgui/Utilities/MuMag/models.py
index 548f22ce2b..2bd6579064 100644
--- a/src/sas/qtgui/Utilities/MuMagTool/models.py
+++ b/src/sas/qtgui/Utilities/MuMag/models.py
@@ -1,5 +1,7 @@
import numpy as np
+""" Models that are useful for verifying results of MuMag """
+
def LorentzianNoisyModelPERP(q, A, M_s, H_0, H_dem, a_H, a_M, l_c, beta):
""" Lorentzian Model for the generation of noisy synthetic test data for perpendicular SANS geometry """
# All inputs in SI-units
diff --git a/src/sas/qtgui/Utilities/MuMagTool/experimental_data.py b/src/sas/qtgui/Utilities/MuMagTool/experimental_data.py
deleted file mode 100644
index 11302de6e4..0000000000
--- a/src/sas/qtgui/Utilities/MuMagTool/experimental_data.py
+++ /dev/null
@@ -1,29 +0,0 @@
-from dataclasses import dataclass
-
-import numpy as np
-
-from sas.qtgui.Plotting.PlotterData import Data1D
-
-
-@dataclass
-class ExperimentalData:
- """ Datapoint used as input for the MuMag tool"""
-
- scattering_curve: Data1D
-
- applied_field: float
- saturation_magnetisation: float
- demagnetising_field: float
-
- def restrict_by_index(self, max_index: int):
- """ Remove all points from data up to given index"""
-
- x = self.scattering_curve.x[:max_index]
- y = self.scattering_curve.y[:max_index]
- dy = self.scattering_curve.dy[:max_index]
-
- return ExperimentalData(
- scattering_curve=Data1D(x=x, y=y, dy=dy),
- applied_field=self.applied_field,
- saturation_magnetisation=self.saturation_magnetisation,
- demagnetising_field=self.demagnetising_field)
diff --git a/src/sas/qtgui/Utilities/MuMagTool/failure.py b/src/sas/qtgui/Utilities/MuMagTool/failure.py
deleted file mode 100644
index e8dfb573dd..0000000000
--- a/src/sas/qtgui/Utilities/MuMagTool/failure.py
+++ /dev/null
@@ -1,7 +0,0 @@
-class FitFailure(Exception):
- """ Fit failed """
- pass
-
-class LoadFailure(Exception):
- """ File loading failed """
- pass
\ No newline at end of file
diff --git a/src/sas/qtgui/Utilities/MuMagTool/fit_parameters.py b/src/sas/qtgui/Utilities/MuMagTool/fit_parameters.py
deleted file mode 100644
index 9828841cf9..0000000000
--- a/src/sas/qtgui/Utilities/MuMagTool/fit_parameters.py
+++ /dev/null
@@ -1,20 +0,0 @@
-from dataclasses import dataclass
-from enum import Enum
-
-
-class ExperimentGeometry(Enum):
- """ Type of experiment """
- PARALLEL = 1
- PERPENDICULAR = 2
-
-
-@dataclass
-class FitParameters:
- """ Input parameters for the fit"""
- q_max: float
- min_applied_field: float
- exchange_A_min: float
- exchange_A_max: float
- exchange_A_n: int
- experiment_geometry: ExperimentGeometry
-
diff --git a/src/sas/qtgui/Utilities/MuMagTool/fit_result.py b/src/sas/qtgui/Utilities/MuMagTool/fit_result.py
deleted file mode 100644
index e05562ee71..0000000000
--- a/src/sas/qtgui/Utilities/MuMagTool/fit_result.py
+++ /dev/null
@@ -1,19 +0,0 @@
-from dataclasses import dataclass
-
-import numpy as np
-
-from sas.qtgui.Utilities.MuMagTool.experimental_data import ExperimentalData
-from sas.qtgui.Utilities.MuMagTool.fit_parameters import ExperimentGeometry, FitParameters
-from sas.qtgui.Utilities.MuMagTool.least_squares_output import LeastSquaresOutputPerpendicular, \
- LeastSquaresOutputParallel
-from sas.qtgui.Utilities.MuMagTool.sweep_output import SweepOutput
-
-
-@dataclass
-class FitResults:
- """ Output the MuMag fit """
- parameters: FitParameters
- input_data: list[ExperimentalData]
- sweep_data: SweepOutput
- refined_fit_data: LeastSquaresOutputParallel | LeastSquaresOutputPerpendicular
- optimal_exchange_A_uncertainty: float
diff --git a/src/sas/qtgui/Utilities/MuMagTool/least_squares_output.py b/src/sas/qtgui/Utilities/MuMagTool/least_squares_output.py
deleted file mode 100644
index 16f48b3ff6..0000000000
--- a/src/sas/qtgui/Utilities/MuMagTool/least_squares_output.py
+++ /dev/null
@@ -1,30 +0,0 @@
-from dataclasses import dataclass
-
-import numpy as np
-
-
-@dataclass
-class LeastSquaresOutput:
- """ Output from least squares method"""
- exchange_A: float
- exchange_A_chi_sq: float
- q: np.ndarray
- I_simulated: np.ndarray
- I_residual: np.ndarray
- S_H: np.ndarray
- I_residual_stdev: np.ndarray
- S_H_stdev: np.ndarray
-
-
-@dataclass
-class LeastSquaresOutputParallel(LeastSquaresOutput):
- """ Output from least squares method for parallel case"""
- pass
-
-
-@dataclass
-class LeastSquaresOutputPerpendicular(LeastSquaresOutput):
- """ Output from least squares method for perpendicular case"""
- S_M: np.ndarray
- S_M_stdev: np.ndarray
-
diff --git a/src/sas/qtgui/Utilities/MuMagTool/sweep_output.py b/src/sas/qtgui/Utilities/MuMagTool/sweep_output.py
deleted file mode 100644
index e0644b0365..0000000000
--- a/src/sas/qtgui/Utilities/MuMagTool/sweep_output.py
+++ /dev/null
@@ -1,20 +0,0 @@
-from dataclasses import dataclass
-
-import numpy as np
-
-from sas.qtgui.Utilities.MuMagTool.least_squares_output import LeastSquaresOutput
-
-from typing import Generic, TypeVar
-
-T = TypeVar("T", bound=LeastSquaresOutput)
-
-@dataclass
-class SweepOutput(Generic[T]):
- """
- Results from brute force optimisiation of the chi squared for the exchange A parameter
- """
-
- exchange_A_checked: np.ndarray
- exchange_A_chi_sq: np.ndarray
- optimal: T
-
From e22e59c7fc03b15ef19aa248291c6d8db9963889 Mon Sep 17 00:00:00 2001
From: lucas-wilkins
Date: Wed, 10 Jul 2024 13:31:03 +0100
Subject: [PATCH 39/46] Initial docs
---
docs/sphinx-docs/source/user/tools.rst | 3 ++
.../Utilities/MuMag/media/mumag_help.rst | 41 +++++++++++++++++++
2 files changed, 44 insertions(+)
create mode 100644 src/sas/qtgui/Utilities/MuMag/media/mumag_help.rst
diff --git a/docs/sphinx-docs/source/user/tools.rst b/docs/sphinx-docs/source/user/tools.rst
index 5559a8e37b..27dac8a98c 100644
--- a/docs/sphinx-docs/source/user/tools.rst
+++ b/docs/sphinx-docs/source/user/tools.rst
@@ -27,3 +27,6 @@ Tools & Utilities
Image Viewer
File Converter
+
+ MuMag Tool
+
diff --git a/src/sas/qtgui/Utilities/MuMag/media/mumag_help.rst b/src/sas/qtgui/Utilities/MuMag/media/mumag_help.rst
new file mode 100644
index 0000000000..438daa6fd3
--- /dev/null
+++ b/src/sas/qtgui/Utilities/MuMag/media/mumag_help.rst
@@ -0,0 +1,41 @@
+.. mumag_help.rst
+
+MuMag Tool
+==========
+
+format of data files
+--------------------
+
+The experimental magnetic SANS data is expected to be stored in a separat folder for example named ExperimentalSANSData. Several informations of the experimental data must be given through the file names stored in the folder ExperimentalSANSData, as seen in the following:
+
+ expected filenames:
+
+ - 1_0_1340_10.csv
+ - 2_20_1340_10.csv
+ - 3_35_1340_10.csv
+ - 4_50_1340_10.csv
+ - ...
+
+ 1. Identifier: Index of the files (e.g. 1, 2, 3, 4, ...)
+ 2. Identifier: Externally applied magnetic field μ_0 H_0 in mT (e.g. 0, 20, 25, 50, ...)
+ 3. Identifier: Saturation magnetization μ_0 M_s in mT (e.g. 1340, 1340, 1340, 1340, ...)
+ 4. Identifier: Demagnetization field μ_0 H_d in mT (e.g. 10, 10, 10, 10, 10, ...)
+
+ (All these values could also be written as float number with dot separator e.g. 10.4345)
+
+Format of the data files:
+
++------------+------------+------------+
+| q [1/nm] | I(q) | std |
++============+============+============+
+|3.62523e-2 |2.85917e+3 |2.28223e+1 |
++------------+------------+------------+
+|4.07000e-2 |1.03769e+3 |1.39076e+1 |
++------------+------------+------------+
+|4.51118e-2 |4.61741e+2 |9.64427e+1 |
++------------+------------+------------+
+|4.95924e-2 |2.83047e+2 |7.65175e+1 |
++------------+------------+------------+
+
+Each of the files must have the same length and got to be sorted from the lowest to the highest q-value. In the files only the numerical data is stored, no headers.
+
From 47a2b05d1933e8f1ae88e1f53c2a33f6ae599145 Mon Sep 17 00:00:00 2001
From: lucas-wilkins
Date: Wed, 10 Jul 2024 13:51:55 +0100
Subject: [PATCH 40/46] Help button
---
src/sas/qtgui/Utilities/MuMag/MuMag.py | 5 ++++
src/sas/qtgui/Utilities/MuMag/UI/MuMagUI.ui | 26 +++++++++++++++++++++
2 files changed, 31 insertions(+)
diff --git a/src/sas/qtgui/Utilities/MuMag/MuMag.py b/src/sas/qtgui/Utilities/MuMag/MuMag.py
index 907bcdb177..0479bc8e9d 100644
--- a/src/sas/qtgui/Utilities/MuMag/MuMag.py
+++ b/src/sas/qtgui/Utilities/MuMag/MuMag.py
@@ -1,3 +1,5 @@
+import webbrowser
+
from sas.qtgui.Utilities.MuMag.UI.MuMagUI import Ui_MuMagTool
from PySide6.QtWidgets import QVBoxLayout
from PySide6 import QtWidgets
@@ -29,6 +31,7 @@ def __init__(self, parent=None):
self.ImportDataButton.clicked.connect(self.importData)
self.SimpleFitButton.clicked.connect(self.onFit)
self.SaveResultsButton.clicked.connect(self.onSave)
+ self.helpButton.clicked.connect(self.onHelp)
#
# Data
@@ -318,6 +321,8 @@ def onSave(self):
MuMagLib.save_data(self.fit_data, directory)
+ def onHelp(self):
+ webbrowser.open("https://www.sasview.org/docs/user/qtgui/Utilities/MuMag/mumag_help.html")
def main():
""" Show a demo of the slider """
diff --git a/src/sas/qtgui/Utilities/MuMag/UI/MuMagUI.ui b/src/sas/qtgui/Utilities/MuMag/UI/MuMagUI.ui
index 9a6f114fb4..a711e598a3 100644
--- a/src/sas/qtgui/Utilities/MuMag/UI/MuMagUI.ui
+++ b/src/sas/qtgui/Utilities/MuMag/UI/MuMagUI.ui
@@ -366,6 +366,32 @@
+ -
+
+
+
-
+
+
+ Qt::Horizontal
+
+
+
+ 40
+ 20
+
+
+
+
+ -
+
+
+ Help
+
+
+
+
+
+
From f219d139c6480643b764f005f7a17b8da713a636 Mon Sep 17 00:00:00 2001
From: lucas-wilkins
Date: Fri, 6 Dec 2024 11:07:22 +0000
Subject: [PATCH 41/46] Some docs
---
.../Utilities/MuMag/media/mumag_help.rst | 82 ++++++++++++++-----
1 file changed, 62 insertions(+), 20 deletions(-)
diff --git a/src/sas/qtgui/Utilities/MuMag/media/mumag_help.rst b/src/sas/qtgui/Utilities/MuMag/media/mumag_help.rst
index 438daa6fd3..101ef6e345 100644
--- a/src/sas/qtgui/Utilities/MuMag/media/mumag_help.rst
+++ b/src/sas/qtgui/Utilities/MuMag/media/mumag_help.rst
@@ -3,10 +3,22 @@
MuMag Tool
==========
-format of data files
---------------------
+Introduction
+------------
-The experimental magnetic SANS data is expected to be stored in a separat folder for example named ExperimentalSANSData. Several informations of the experimental data must be given through the file names stored in the folder ExperimentalSANSData, as seen in the following:
+Loading data
+------------
+
+To load data click on the `Import Data` button. This will give you a file chooser that allows you to select a
+**directory**. This directory should contain multiple files for measurements take with different applied magnetic fields.
+
+Currently, *the magnetic field and other information is expected to be in filename with a format as described below*
+
+Form of Data Files
+..................
+
+The experimental magnetic SANS data for a given analysis is expected to be stored as CSV files in a single folder.
+Information about each SANS scattering curve must be given through the file names, like in the following example:
expected filenames:
@@ -16,26 +28,56 @@ The experimental magnetic SANS data is expected to be stored in a separat folder
- 4_50_1340_10.csv
- ...
- 1. Identifier: Index of the files (e.g. 1, 2, 3, 4, ...)
- 2. Identifier: Externally applied magnetic field μ_0 H_0 in mT (e.g. 0, 20, 25, 50, ...)
- 3. Identifier: Saturation magnetization μ_0 M_s in mT (e.g. 1340, 1340, 1340, 1340, ...)
- 4. Identifier: Demagnetization field μ_0 H_d in mT (e.g. 10, 10, 10, 10, 10, ...)
+ The fields separated by underscores have the following meaning
+
+ 1. Identifier: Index of the files (e.g. 1, 2, 3, 4, ...) - (not needed by MuMag)
+ 2. Identifier: Externally applied magnetic field :math:`μ_0 H_0` in mT (e.g. 0, 20, 25, 50, ...)
+ 3. Identifier: Saturation magnetization :math:`μ_0 M_s` in mT (e.g. 1340, 1340, 1340, 1340, ...)
+ 4. Identifier: Demagnetization field :math:`μ_0 H_d` in mT (e.g. 10, 10, 10, 10, 10, ...)
(All these values could also be written as float number with dot separator e.g. 10.4345)
-Format of the data files:
+The CSV files are expected have three columns: momentum transfer :math:`q` in nm:math:`^{-1}`,
+scattering intensity :math:`I(q)`, and the standard error corresponding to `I(q)`.
+
+Each of the files must have the same length and got to be sorted from the lowest to the highest q-value.
+In the files only the numerical data is stored, no headers.
+
+Running MuMag
+-------------
+
+To run MuMag, load the data, set the parameters below, and click `Fit`.
+
+Parameters
+..........
+
+* `Analysis method` - This chooses one of two experiment types. Perpendicular is where the applied
+field is perpendicular to the beam (e.g. beam in x direction and field in z), and parallel where the applied field is parallel.
+
+* `Maximum q` - MuMag has the ability to exclude q values beyond a given value, specified here
+* `Applied field` - MuMag will use only data with applied field strengths above this value.
+MuMag requires the sample to be at (or close to) saturation, use this field to specify where this is.
+* `Scan range` - When calculating the exchange stiffness constant A,
+MuMag's minimisation step has two components.
+(1) A brute for search, then (2) a refinement.
+These three connected values that describe the values for which the brute force search will
+take place, as well as the values that will appear on any plots.
+
+Results
+.......
+
+* `A value` - The estimated exchange stiffness constant (A)
+* `A uncertainty` - An estimate of the uncertainty associated with it
+
+Plots
+.....
-+------------+------------+------------+
-| q [1/nm] | I(q) | std |
-+============+============+============+
-|3.62523e-2 |2.85917e+3 |2.28223e+1 |
-+------------+------------+------------+
-|4.07000e-2 |1.03769e+3 |1.39076e+1 |
-+------------+------------+------------+
-|4.51118e-2 |4.61741e+2 |9.64427e+1 |
-+------------+------------+------------+
-|4.95924e-2 |2.83047e+2 |7.65175e+1 |
-+------------+------------+------------+
-Each of the files must have the same length and got to be sorted from the lowest to the highest q-value. In the files only the numerical data is stored, no headers.
+When you load data the `data` plot will be populated. When you click `fit` the rest will be.
+* `Data` - A plot of all the loaded data
+* `Fit Results`
+ * :math:`\chi^2` - figure of merit used by MuMag to calculate the best A value, across different
+values of A (currently mean squared). This plot is useful to checking your problem is well conditioned.
+ * :math:`I_res` - The *residual intensity* - the part of the scattering that doesn't respond to
+applied field changes, inferred from the data (see above for details)
\ No newline at end of file
From f9c999b372ec05020c8fa16a54c2877a0e7a774c Mon Sep 17 00:00:00 2001
From: lucas-wilkins
Date: Fri, 6 Dec 2024 11:28:01 +0000
Subject: [PATCH 42/46] More docs
---
src/sas/qtgui/Utilities/MuMag/media/mumag_help.rst | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/src/sas/qtgui/Utilities/MuMag/media/mumag_help.rst b/src/sas/qtgui/Utilities/MuMag/media/mumag_help.rst
index 101ef6e345..323a76f5fa 100644
--- a/src/sas/qtgui/Utilities/MuMag/media/mumag_help.rst
+++ b/src/sas/qtgui/Utilities/MuMag/media/mumag_help.rst
@@ -80,4 +80,7 @@ When you load data the `data` plot will be populated. When you click `fit` the r
* :math:`\chi^2` - figure of merit used by MuMag to calculate the best A value, across different
values of A (currently mean squared). This plot is useful to checking your problem is well conditioned.
* :math:`I_res` - The *residual intensity* - the part of the scattering that doesn't respond to
-applied field changes, inferred from the data (see above for details)
\ No newline at end of file
+applied field changes, inferred from the data (see above for details)
+ * :math:`S_H` - Anisotropy field scattering function
+ * :math:`S_M` - Scattering function of the longitudinal magnetization
+* `Comparison` -
\ No newline at end of file
From 3d81f8d7f8c438510092cd58083630b40255e7a8 Mon Sep 17 00:00:00 2001
From: lucas-wilkins
Date: Fri, 6 Dec 2024 11:49:34 +0000
Subject: [PATCH 43/46] Plotting update
---
src/sas/qtgui/Utilities/MuMag/MuMag.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/sas/qtgui/Utilities/MuMag/MuMag.py b/src/sas/qtgui/Utilities/MuMag/MuMag.py
index 0479bc8e9d..199ba9d7c3 100644
--- a/src/sas/qtgui/Utilities/MuMag/MuMag.py
+++ b/src/sas/qtgui/Utilities/MuMag/MuMag.py
@@ -279,11 +279,11 @@ def show_fit_results(self):
self.comparison_axes.loglog(
datum.scattering_curve.x,
datum.scattering_curve.y,
- linestyle='dotted', color=colors[k], linewidth=0.3, markersize=1)
+ linestyle='None', color=colors[k], marker='x')
# Show the fitted curves
n_sim = self.fit_data.refined_fit_data.I_simulated.shape[0]
- colors = pl.cm.YlGn(np.linspace(0, 1, n_sim))
+ colors = pl.cm.jet(np.linspace(0, 1, n_sim))
for k in range(n_sim):
self.comparison_axes.loglog(
self.fit_data.refined_fit_data.q * 1e-9,
From 5e41d13fe6954d9f8b9b744a4d987eff66a75561 Mon Sep 17 00:00:00 2001
From: lucas-wilkins
Date: Fri, 6 Dec 2024 12:07:43 +0000
Subject: [PATCH 44/46] More docs
---
.../Utilities/MuMag/media/mumag_help.rst | 21 ++++++++++++++++++-
1 file changed, 20 insertions(+), 1 deletion(-)
diff --git a/src/sas/qtgui/Utilities/MuMag/media/mumag_help.rst b/src/sas/qtgui/Utilities/MuMag/media/mumag_help.rst
index 323a76f5fa..1704f653b5 100644
--- a/src/sas/qtgui/Utilities/MuMag/media/mumag_help.rst
+++ b/src/sas/qtgui/Utilities/MuMag/media/mumag_help.rst
@@ -6,6 +6,24 @@ MuMag Tool
Introduction
------------
+MuMag is an analysis tool for calculating exchange stiffness constants, residual scattering, anisotropy field
+and longitudinal magnetization based on unpolarized 1D SANS experiments with applied magnetic fields. The theory
+behind this has been published `here `_.
+
+Given some scattering curve recorded at different applied field intensities, :math:`I(q, H)`, MuMag will break
+the down these curves into two (in the case of parallel magnetic fields) and three, components (
+for perpendicular fields relative to the beam). In the perpendicular case, the resulting linear
+decomposition has the following form:
+
+:math:`I(q, H) = I_res(q) + S_H(q) R_H(q, H) + S_M(q) R_M(q, H)`
+
+Where :math:`R_H` and :math:`R_M` are known magnetic response functions (see the paper above), and :math:`I_res`,
+:math:`S_H` and :math:`S_M` are non-field dependent terms: the residual scattering function,
+the anisotropy field scattering function and the longitudinal magnetisation scattering function respectively.
+
+In the parallel case :math:`S_M` and :math:`R_M` are zero.
+
+
Loading data
------------
@@ -83,4 +101,5 @@ values of A (currently mean squared). This plot is useful to checking your probl
applied field changes, inferred from the data (see above for details)
* :math:`S_H` - Anisotropy field scattering function
* :math:`S_M` - Scattering function of the longitudinal magnetization
-* `Comparison` -
\ No newline at end of file
+* `Comparison` - Crosses show original data, lines show scattering curves reconstructed based on :math:`I_res`,
+:math:`S_H` and :math:`S_M`
\ No newline at end of file
From 68ae63b8d27fa2a6804aabb19c3275a7164c72b8 Mon Sep 17 00:00:00 2001
From: lucas-wilkins
Date: Fri, 6 Dec 2024 12:20:31 +0000
Subject: [PATCH 45/46] Docs formatting
---
.../Utilities/MuMag/media/mumag_help.rst | 58 ++++++++-----------
1 file changed, 24 insertions(+), 34 deletions(-)
diff --git a/src/sas/qtgui/Utilities/MuMag/media/mumag_help.rst b/src/sas/qtgui/Utilities/MuMag/media/mumag_help.rst
index 1704f653b5..e0c13e7d74 100644
--- a/src/sas/qtgui/Utilities/MuMag/media/mumag_help.rst
+++ b/src/sas/qtgui/Utilities/MuMag/media/mumag_help.rst
@@ -38,24 +38,24 @@ Form of Data Files
The experimental magnetic SANS data for a given analysis is expected to be stored as CSV files in a single folder.
Information about each SANS scattering curve must be given through the file names, like in the following example:
- expected filenames:
+Example filenames:
- - 1_0_1340_10.csv
- - 2_20_1340_10.csv
- - 3_35_1340_10.csv
- - 4_50_1340_10.csv
- - ...
+- 1_0_1340_10.csv
+- 2_20_1340_10.csv
+- 3_35_1340_10.csv
+- 4_50_1340_10.csv
+- ...
- The fields separated by underscores have the following meaning
+The fields separated by underscores have the following meaning
- 1. Identifier: Index of the files (e.g. 1, 2, 3, 4, ...) - (not needed by MuMag)
- 2. Identifier: Externally applied magnetic field :math:`μ_0 H_0` in mT (e.g. 0, 20, 25, 50, ...)
- 3. Identifier: Saturation magnetization :math:`μ_0 M_s` in mT (e.g. 1340, 1340, 1340, 1340, ...)
- 4. Identifier: Demagnetization field :math:`μ_0 H_d` in mT (e.g. 10, 10, 10, 10, 10, ...)
+1. Index of the files (e.g. 1, 2, 3, 4, ...) - (not used by SasView's MuMag, but is used by the MATLAB version)
+2. Externally applied magnetic field :math:`μ_0 H_0` in mT (e.g. 0, 20, 25, 50, ...)
+3. Saturation magnetization :math:`μ_0 M_s` in mT (e.g. 1340, 1340, 1340, 1340, ...)
+4. Demagnetization field :math:`μ_0 H_d` in mT (e.g. 10, 10, 10, 10, 10, ...)
- (All these values could also be written as float number with dot separator e.g. 10.4345)
+(All these values could also be written as float number with dot separator e.g. 10.4345)
-The CSV files are expected have three columns: momentum transfer :math:`q` in nm:math:`^{-1}`,
+The CSV files are expected have three columns: momentum transfer :math:`q` in nm :math:`^{-1}`,
scattering intensity :math:`I(q)`, and the standard error corresponding to `I(q)`.
Each of the files must have the same length and got to be sorted from the lowest to the highest q-value.
@@ -69,23 +69,16 @@ To run MuMag, load the data, set the parameters below, and click `Fit`.
Parameters
..........
-* `Analysis method` - This chooses one of two experiment types. Perpendicular is where the applied
-field is perpendicular to the beam (e.g. beam in x direction and field in z), and parallel where the applied field is parallel.
-
-* `Maximum q` - MuMag has the ability to exclude q values beyond a given value, specified here
-* `Applied field` - MuMag will use only data with applied field strengths above this value.
-MuMag requires the sample to be at (or close to) saturation, use this field to specify where this is.
-* `Scan range` - When calculating the exchange stiffness constant A,
-MuMag's minimisation step has two components.
-(1) A brute for search, then (2) a refinement.
-These three connected values that describe the values for which the brute force search will
-take place, as well as the values that will appear on any plots.
+* **Analysis method** - This chooses one of two experiment types. Perpendicular is where the applied field is perpendicular to the beam (e.g. beam in x direction and field in z), and parallel where the applied field is parallel.
+* **Maximum q** - MuMag has the ability to exclude q values beyond a given value, specified here
+* **Applied field** - MuMag will use only data with applied field strengths above this value. MuMag requires the sample to be at (or close to) saturation, use this field to specify where this is.
+* **Scan range** - When calculating the exchange stiffness constant A, MuMag's minimisation step has two components. (1) A brute for search, then (2) a refinement. These three connected values that describe the values for which the brute force search will take place, as well as the values that will appear on any plots.
Results
.......
-* `A value` - The estimated exchange stiffness constant (A)
-* `A uncertainty` - An estimate of the uncertainty associated with it
+* **A value** - The estimated exchange stiffness constant (A)
+* **A uncertainty** - An estimate of the uncertainty associated with it
Plots
.....
@@ -93,13 +86,10 @@ Plots
When you load data the `data` plot will be populated. When you click `fit` the rest will be.
-* `Data` - A plot of all the loaded data
-* `Fit Results`
- * :math:`\chi^2` - figure of merit used by MuMag to calculate the best A value, across different
-values of A (currently mean squared). This plot is useful to checking your problem is well conditioned.
- * :math:`I_res` - The *residual intensity* - the part of the scattering that doesn't respond to
-applied field changes, inferred from the data (see above for details)
+* **Data** - A plot of all the loaded data
+* **Fit Results**
+ * :math:`\chi^2` - figure of merit used by MuMag to calculate the best A value, across different values of A (currently mean squared). This plot is useful to checking your problem is well conditioned.
+ * :math:`I_res` - The *residual intensity* - the part of the scattering that doesn't respond to applied field changes, inferred from the data (see above for details)
* :math:`S_H` - Anisotropy field scattering function
* :math:`S_M` - Scattering function of the longitudinal magnetization
-* `Comparison` - Crosses show original data, lines show scattering curves reconstructed based on :math:`I_res`,
-:math:`S_H` and :math:`S_M`
\ No newline at end of file
+* **Comparison** - Crosses show original data, lines show scattering curves reconstructed based on :math:`I_res`, :math:`S_H` and :math:`S_M`
\ No newline at end of file
From 09f9a8134ecd003935e793dc268792ac4a604f52 Mon Sep 17 00:00:00 2001
From: Lucas Wilkins
Date: Thu, 12 Dec 2024 18:30:56 +0000
Subject: [PATCH 46/46] Typos in mumag_help.rst
---
src/sas/qtgui/Utilities/MuMag/media/mumag_help.rst | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/src/sas/qtgui/Utilities/MuMag/media/mumag_help.rst b/src/sas/qtgui/Utilities/MuMag/media/mumag_help.rst
index e0c13e7d74..0ff884eb75 100644
--- a/src/sas/qtgui/Utilities/MuMag/media/mumag_help.rst
+++ b/src/sas/qtgui/Utilities/MuMag/media/mumag_help.rst
@@ -11,7 +11,7 @@ and longitudinal magnetization based on unpolarized 1D SANS experiments with app
behind this has been published `here `_.
Given some scattering curve recorded at different applied field intensities, :math:`I(q, H)`, MuMag will break
-the down these curves into two (in the case of parallel magnetic fields) and three, components (
+down these curves into two (in the case of parallel magnetic fields) and three, components (
for perpendicular fields relative to the beam). In the perpendicular case, the resulting linear
decomposition has the following form:
@@ -72,7 +72,7 @@ Parameters
* **Analysis method** - This chooses one of two experiment types. Perpendicular is where the applied field is perpendicular to the beam (e.g. beam in x direction and field in z), and parallel where the applied field is parallel.
* **Maximum q** - MuMag has the ability to exclude q values beyond a given value, specified here
* **Applied field** - MuMag will use only data with applied field strengths above this value. MuMag requires the sample to be at (or close to) saturation, use this field to specify where this is.
-* **Scan range** - When calculating the exchange stiffness constant A, MuMag's minimisation step has two components. (1) A brute for search, then (2) a refinement. These three connected values that describe the values for which the brute force search will take place, as well as the values that will appear on any plots.
+* **Scan range** - When calculating the exchange stiffness constant A, MuMag's minimisation step has two components. (1) A brute for search, then (2) a refinement. These three values that describe the values for which the brute force search will take place (start, stop and step), as well as the values used for related plots.
Results
.......
@@ -92,4 +92,4 @@ When you load data the `data` plot will be populated. When you click `fit` the r
* :math:`I_res` - The *residual intensity* - the part of the scattering that doesn't respond to applied field changes, inferred from the data (see above for details)
* :math:`S_H` - Anisotropy field scattering function
* :math:`S_M` - Scattering function of the longitudinal magnetization
-* **Comparison** - Crosses show original data, lines show scattering curves reconstructed based on :math:`I_res`, :math:`S_H` and :math:`S_M`
\ No newline at end of file
+* **Comparison** - Crosses show original data, lines show scattering curves reconstructed based on :math:`I_res`, :math:`S_H` and :math:`S_M`