From 91c876ef1b1d6067e81ed4ed6d310b102e35454b Mon Sep 17 00:00:00 2001 From: GuiMacielPereira Date: Mon, 29 Jul 2024 10:25:00 +0100 Subject: [PATCH 01/25] Change unit test to avoid mocking The unit test that existed before used a mock of mantid and of a table workspace to test. It is better practice to avoid mocking when possible, and this way the internal implementation of the unit test can change whilst maintaining the validity of the test. --- .../unit/analysis/test_analysis_functions.py | 25 ++++++++++--------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/tests/unit/analysis/test_analysis_functions.py b/tests/unit/analysis/test_analysis_functions.py index 08283e8b..037253e2 100644 --- a/tests/unit/analysis/test_analysis_functions.py +++ b/tests/unit/analysis/test_analysis_functions.py @@ -1,24 +1,25 @@ import unittest +import numpy as np +import numpy.testing as nptest from mock import MagicMock from mvesuvio.analysis_reduction import extractWS +from mantid.simpleapi import CreateWorkspace, DeleteWorkspace class TestAnalysisFunctions(unittest.TestCase): def setUp(self): - self.ws = MagicMock() - self.ws.extractX = MagicMock() - self.ws.extractY = MagicMock() - self.ws.extractE = MagicMock() + pass - def test_extract_ws_returns_3_values(self): - returned_values = extractWS(self.ws) - self.assertEqual(3, len(returned_values)) + def test_extract_ws(self): + data = [1, 2, 3] + ws = CreateWorkspace(DataX=data, DataY=data, DataE=data, NSpec=1, UnitX="some_unit") - def test_extract_ws_calls_extract_X_Y_and_E(self): - _ = extractWS(self.ws) - self.ws.extractX.assert_called_once() - self.ws.extractY.assert_called_once() - self.ws.extractE.assert_called_once() + dataX, dataY, dataE = extractWS(ws) + nptest.assert_array_equal([data], dataX) + nptest.assert_array_equal([data], dataY) + nptest.assert_array_equal([data], dataE) + + DeleteWorkspace(ws) if __name__ == "__main__": From 066767c4bad6b396cba40665c72a4cfd76a77062 Mon Sep 17 00:00:00 2001 From: GuiMacielPereira Date: Tue, 30 Jul 2024 09:40:50 +0100 Subject: [PATCH 02/25] Create new folder for transitioning into oop Started work to make analysis routine into an object. Created folder to keep my work organised. --- src/mvesuvio/oop/AnalysisRoutine.py | 71 +++++++++++++++++++++++ src/mvesuvio/oop/NeutronComptonProfile.py | 48 +++++++++++++++ src/mvesuvio/oop/__init__.py | 0 src/mvesuvio/oop/analysis_helpers.py | 0 4 files changed, 119 insertions(+) create mode 100644 src/mvesuvio/oop/AnalysisRoutine.py create mode 100644 src/mvesuvio/oop/NeutronComptonProfile.py create mode 100644 src/mvesuvio/oop/__init__.py create mode 100644 src/mvesuvio/oop/analysis_helpers.py diff --git a/src/mvesuvio/oop/AnalysisRoutine.py b/src/mvesuvio/oop/AnalysisRoutine.py new file mode 100644 index 00000000..ab40c58f --- /dev/null +++ b/src/mvesuvio/oop/AnalysisRoutine.py @@ -0,0 +1,71 @@ +from mvesuvio.oop.NeutronComptonProfile import NeutronComptonProfile + +class AnalysisRoutine: + + def __init__(self, number_of_iterations, spectrum_range, mask_spectra, + multiple_scattering_correction, gamma_correction, + transmission_guess, multiple_scattering_order, number_of_events): + + self._number_of_iterations = number_of_iterations + self._spectrum_range = spectrum_range + self._mask_spectra = mask_spectra + self._transmission_guess = transmission_guess + self._multiple_scattering_order = multiple_scattering_order + self._number_of_events = number_of_events + + self._multiple_scattering_correction = multiple_scattering_correction + self._gamma_correction = gamma_correction + + self._h_ratio = None + self._constraints = [] + self._profiles = {} + + # Only used for system tests, remove once tests are updated + self._run_hist_data = True + self._run_norm_voigt = False + + # Links to another AnalysisRoutine object: + self._profiles_destination = None + self._h_ratio_destination = None + + + def add_profiles(self, **args: NeutronComptonProfile): + for profile in args: + self._profiles[profile.label] = profile + + + def add_constraint(self, constraint_string: str): + self._constraints.append(constraint_string) + + + def add_h_ratio_to_next_lowest_mass(self, ratio: float): + self._h_ratio_to_next_lowest_mass = ratio + + + def send_ncp_fit_parameters(self): + self._profiles_destination.profiles = self.profiles + + + def send_h_ratio(self): + self._h_ratio_destination.h_ratio_to_next_lowest_mass = self._h_ratio + + @property + def h_ratio_to_next_lowest_mass(self): + return self._h_ratio + + @h_ratio_to_next_lowest_mass.setter + def h_ratio_to_next_lowest_mass(self, value): + self.h_ratio_to_next_lowest_mass = value + + @property + def profiles(self): + return self._profiles + + @profiles.setter + def profiles(self, incoming_profiles): + assert(isinstance(incoming_profiles, dict)) + common_keys = self._profiles.keys() & incoming_profiles.keys() + common_keys_profiles = {k: incoming_profiles[k] for k in common_keys} + self._profiles = {**self._profiles, **common_keys_profiles} + + diff --git a/src/mvesuvio/oop/NeutronComptonProfile.py b/src/mvesuvio/oop/NeutronComptonProfile.py new file mode 100644 index 00000000..de25aff7 --- /dev/null +++ b/src/mvesuvio/oop/NeutronComptonProfile.py @@ -0,0 +1,48 @@ + + + +class NeutronComptonProfile: + + def __init__(self, mass, label, intensity, width, center, + intensity_bounds, width_bounds, center_bounds): + self._mass = mass + self._label = label + self._intensity = intensity + self._width = width + self._center = center + self._intensity_bounds = intensity_bounds + self._width_bounds = width_bounds + self.center_bounds = center_bounds + + @property + def label(self): + return self._label + + @property + def mass(self): + return self._mass + + @property + def width(self): + return self._width + + @property + def intensity(self): + return self._intensity + + @property + def center(self): + return self._center + + @property + def width_bounds(self): + return self._width_bounds + + @property + def intensity_bounds(self): + return self._intensity_bounds + + @property + def center_bounds(self): + return self._center_bounds + diff --git a/src/mvesuvio/oop/__init__.py b/src/mvesuvio/oop/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/mvesuvio/oop/analysis_helpers.py b/src/mvesuvio/oop/analysis_helpers.py new file mode 100644 index 00000000..e69de29b From a945aa83987efcc8cec43bb0608d2dc28da468e8 Mon Sep 17 00:00:00 2001 From: GuiMacielPereira Date: Tue, 30 Jul 2024 15:12:54 +0100 Subject: [PATCH 03/25] Change most functions to be contained in object Most of the work consisted in including 'self' in functions and replace the previous 'ic' methods with 'self' methods --- src/mvesuvio/oop/AnalysisRoutine.py | 964 ++++++++++++++++++++++++++- src/mvesuvio/oop/analysis_helpers.py | 191 ++++++ src/mvesuvio/oop/run_routine.py | 14 + 3 files changed, 1168 insertions(+), 1 deletion(-) create mode 100644 src/mvesuvio/oop/run_routine.py diff --git a/src/mvesuvio/oop/AnalysisRoutine.py b/src/mvesuvio/oop/AnalysisRoutine.py index ab40c58f..d989b2b9 100644 --- a/src/mvesuvio/oop/AnalysisRoutine.py +++ b/src/mvesuvio/oop/AnalysisRoutine.py @@ -1,14 +1,23 @@ from mvesuvio.oop.NeutronComptonProfile import NeutronComptonProfile +from mvesuvio.oop.analysis_helpers import extractWS, histToPointData, loadConstants, \ + gaussian, lorentizian, numericalThirdDerivative, \ + switchFirstTwoAxis, createWS +from mantid.simpleapi import mtd, CreateEmptyTableWorkspace, MaskDetectors, SumSpectra +import numpy as np +from scipy import optimize class AnalysisRoutine: - def __init__(self, number_of_iterations, spectrum_range, mask_spectra, + def __init__(self, workspace, ip_file, number_of_iterations, spectrum_range, mask_spectra, multiple_scattering_correction, gamma_correction, transmission_guess, multiple_scattering_order, number_of_events): + self._workspace_to_fit = workspace + self._ip_file = ip_file self._number_of_iterations = number_of_iterations self._spectrum_range = spectrum_range self._mask_spectra = mask_spectra + self._mask_detector_index = self._transmission_guess = transmission_guess self._multiple_scattering_order = multiple_scattering_order self._number_of_events = number_of_events @@ -20,6 +29,8 @@ def __init__(self, number_of_iterations, spectrum_range, mask_spectra, self._constraints = [] self._profiles = {} + self._masses = [p.mass for p in self._profiles] + # Only used for system tests, remove once tests are updated self._run_hist_data = True self._run_norm_voigt = False @@ -28,6 +39,29 @@ def __init__(self, number_of_iterations, spectrum_range, mask_spectra, self._profiles_destination = None self._h_ratio_destination = None + # Variables used during fitting + + self._dataX, self._dataY, self._dataE = extractWS(self._workspace_to_fit) + resolutionPars, instrPars, kinematicArrays, ySpacesForEachMass = self.prepareFitArgs(self._dataX) + self._resolution_params = resolutionPars + self._instrument_params = instrPars + self._kinematic_arrays = kinematicArrays + self._y_space_arrays = ySpacesForEachMass + + self._initial_fit_parameters = [] + for p in self._profiles: + self._initial_fit_parameters.append(p.intensity) + self._initial_fit_parameters.append(p.width) + self._initial_fit_parameters.append(p.center) + + self._intensities = np.array([p.intensity for p in self._profiles])[:, np.newaxis] + self._widths = np.array([p.width for p in self._profiles])[:, np.newaxis] + self._centesr = np.array([p.center for p in self._profiles])[:, np.newaxis] + self._fit_parameters = np.zeros((len(self._dataY), 3 * len(self._profiles) + 3)) + + self._fig_save_path = None + + def add_profiles(self, **args: NeutronComptonProfile): for profile in args: @@ -69,3 +103,931 @@ def profiles(self, incoming_profiles): self._profiles = {**self._profiles, **common_keys_profiles} + def run(self): + + self.createTableInitialParameters() + + # Legacy code from Bootstrap + # if self.runningSampleWS: + # initialWs = RenameWorkspace( + # InputWorkspace=ic.sampleWS, OutputWorkspace=initialWs.name() + # ) + + self._workspace_to_fit = CloneWorkspace( + InputWorkspace=self._workspace_to_fit, + OutputWorkspace=self._name + "0" + ) + + for iteration in range(self._number_of_iterations + 1): + # Workspace from previous iteration + wsToBeFitted = mtd[self._name + str(iteration)] + + ncpTotal = self.fitNcpToWorkspace(wsToBeFitted) + + mWidths, stdWidths, mIntRatios, stdIntRatios = self.extractMeans(wsToBeFitted.name()) + + self.createMeansAndStdTableWS( + wsToBeFitted.name(), mWidths, stdWidths, mIntRatios, stdIntRatios + ) + + # When last iteration, skip MS and GC + if iteration == self._number_of_iterations: + break + + # TODO: Refactored until here ------- + + # Replace zero columns (bins) with ncp total fit + # If ws has no zero column, then remains unchanged + if iteration == 0: + wsNCPM = replaceZerosWithNCP(mtd[ic.name], ncpTotal) + + CloneWorkspace(InputWorkspace=ic.name, OutputWorkspace="tmpNameWs") + + if ic.GammaCorrectionFlag: + wsGC = createWorkspacesForGammaCorrection(ic, mWidths, mIntRatios, wsNCPM) + Minus( + LHSWorkspace="tmpNameWs", RHSWorkspace=wsGC, OutputWorkspace="tmpNameWs" + ) + + if ic.MSCorrectionFlag: + wsMS = createWorkspacesForMSCorrection(ic, mWidths, mIntRatios, wsNCPM) + Minus( + LHSWorkspace="tmpNameWs", RHSWorkspace=wsMS, OutputWorkspace="tmpNameWs" + ) + + remaskValues(ic.name, "tmpNameWS") # Masks cols in the same place as in ic.name + RenameWorkspace( + InputWorkspace="tmpNameWs", OutputWorkspace=ic.name + str(iteration + 1) + ) + + wsFinal = mtd[ic.name + str(ic.noOfMSIterations)] + fittingResults = resultsObject(ic) + fittingResults.save() + return wsFinal, fittingResults + + + def remaskValues(wsName, wsToMaskName): + """ + Uses the ws before the MS correction to look for masked columns or dataE + and implement the same masked values after the correction. + """ + ws = mtd[wsName] + dataX, dataY, dataE = extractWS(ws) + mask = np.all(dataY == 0, axis=0) + + wsM = mtd[wsToMaskName] + dataXM, dataYM, dataEM = extractWS(wsM) + dataYM[:, mask] = 0 + if np.all(dataE == 0): + dataEM = np.zeros(dataEM.shape) + + passDataIntoWS(dataXM, dataYM, dataEM, wsM) + return + + + def createTableInitialParameters(self): + print("\nRUNNING ", self.modeRunning, " SCATTERING.\n") + if self.modeRunning == "BACKWARD": + print(f"\nH ratio to next lowest mass = {self._h_ratio}\n") + + meansTableWS = CreateEmptyTableWorkspace( + OutputWorkspace=self.name + "_Initial_Parameters" + ) + meansTableWS.addColumn(type="float", name="Mass") + meansTableWS.addColumn(type="float", name="Initial Widths") + meansTableWS.addColumn(type="str", name="Bounds Widths") + meansTableWS.addColumn(type="float", name="Initial Intensities") + meansTableWS.addColumn(type="str", name="Bounds Intensities") + meansTableWS.addColumn(type="float", name="Initial Centers") + meansTableWS.addColumn(type="str", name="Bounds Centers") + + print("\nCreated Table with Initial Parameters:") + for p in self._profiles: + meansTableWS.addRow([p.mass, p.width, str(p.width_bounds), + p.intensity, str(p.intensity_bounds), + p.center, str(p.center_bounds)]) + print("\nMass: ", p.mass) + print(f"{'Initial Intensity:':>20s} {p.intensity:<8.3f} Bounds: {p.intensity_bounds}") + print(f"{'Initial Width:':>20s} {p.width:<8.3f} Bounds: {p.width_bounds}") + print(f"{'Initial Center:':>20s} {p.center:<8.3f} Bounds: {p.center_bounds}") + print("\n") + + + def fitNcpToWorkspace(self, ws): + """ + Performs the fit of ncp to the workspace. + Firtly the arrays required for the fit are prepared and then the fit is performed iteratively + on a spectrum by spectrum basis. + """ + + if self._runHistData: # Converts point data from workspaces to histogram data + self._dataY, self._dataX, self._dataE = histToPointData(self._dataY, self._dataX, self._dataE) + + + print("\nFitting NCP:\n") + + arrFitPars = self.fitNcpToArray() + + self.createTableWSForFitPars(ws.name(), len(self._profiles), arrFitPars) + + arrBestFitPars = arrFitPars[:, 1:-2] + + ncpForEachMass, ncpTotal = self.calculateNcpArr(arrBestFitPars) + ncpSumWSs = self.createNcpWorkspaces(ncpForEachMass, ncpTotal, ws) + + wsDataSum = SumSpectra(InputWorkspace=ws, OutputWorkspace=ws.name() + "_Sum") + self.plotSumNCPFits(wsDataSum, *ncpSumWSs) + return ncpTotal + + + def prepareFitArgs(self, dataX): + instrPars = self.loadInstrParsFileIntoArray(self._ip_file, self._firstSpec, self._lastSpec) + resolutionPars = self.loadResolutionPars(instrPars) + + v0, E0, delta_E, delta_Q = self.calculateKinematicsArrays(dataX, instrPars) + kinematicArrays = np.array([v0, E0, delta_E, delta_Q]) + ySpacesForEachMass = self.convertDataXToYSpacesForEachMass( + dataX, delta_Q, delta_E + ) + + kinematicArrays = self.reshapeArrayPerSpectrum(kinematicArrays) + ySpacesForEachMass = self.reshapeArrayPerSpectrum(ySpacesForEachMass) + return resolutionPars, instrPars, kinematicArrays, ySpacesForEachMass + + + def loadInstrParsFileIntoArray(self): + """Loads instrument parameters into array, from the file in the specified path""" + + data = np.loadtxt(self._ip_file, dtype=str)[1:].astype(float) + + spectra = data[:, 0] + select_rows = np.where((spectra >= self._firstSpec) & (spectra <= self._lastSpec)) + instrPars = data[select_rows] + return instrPars + + + @staticmethod + def loadResolutionPars(instrPars): + """Resolution of parameters to propagate into TOF resolution + Output: matrix with each parameter in each column""" + spectrums = instrPars[:, 0] + L = len(spectrums) + # For spec no below 135, back scattering detectors, mode is double difference + # For spec no 135 or above, front scattering detectors, mode is single difference + dE1 = np.where(spectrums < 135, 88.7, 73) # meV, STD + dE1_lorz = np.where(spectrums < 135, 40.3, 24) # meV, HFHM + dTOF = np.repeat(0.37, L) # us + dTheta = np.repeat(0.016, L) # rad + dL0 = np.repeat(0.021, L) # meters + dL1 = np.repeat(0.023, L) # meters + + resolutionPars = np.vstack((dE1, dTOF, dTheta, dL0, dL1, dE1_lorz)).transpose() + return resolutionPars + + + @staticmethod + def calculateKinematicsArrays(dataX, instrPars): + """Kinematics quantities calculated from TOF data""" + + mN, Ef, en_to_vel, vf, hbar = loadConstants() + det, plick, angle, T0, L0, L1 = np.hsplit(instrPars, 6) # each is of len(dataX) + t_us = dataX - T0 # T0 is electronic delay due to instruments + v0 = vf * L0 / (vf * t_us - L1) + E0 = np.square( + v0 / en_to_vel + ) # en_to_vel is a factor used to easily change velocity to energy and vice-versa + + delta_E = E0 - Ef + delta_Q2 = ( + 2.0 + * mN + / hbar**2 + * (E0 + Ef - 2.0 * np.sqrt(E0 * Ef) * np.cos(angle / 180.0 * np.pi)) + ) + delta_Q = np.sqrt(delta_Q2) + return v0, E0, delta_E, delta_Q # shape(no of spectrums, no of bins) + + + @staticmethod + def reshapeArrayPerSpectrum(A): + """ + Exchanges the first two axes of an array A. + Rearranges array to match iteration per spectrum + """ + return np.stack(np.split(A, len(A), axis=0), axis=2)[0] + + + def convertDataXToYSpacesForEachMass(self, dataX, delta_Q, delta_E): + "Calculates y spaces from TOF data, each row corresponds to one mass" + + # Prepare arrays to broadcast + dataX = dataX[np.newaxis, :, :] + delta_Q = delta_Q[np.newaxis, :, :] + delta_E = delta_E[np.newaxis, :, :] + + mN, Ef, en_to_vel, vf, hbar = loadConstants() + masses = self._masses.reshape(self._masses.size, 1, 1) + + energyRecoil = np.square(hbar * delta_Q) / 2.0 / masses + ySpacesForEachMass = ( + masses / hbar**2 / delta_Q * (delta_E - energyRecoil) + ) # y-scaling + return ySpacesForEachMass + + + def fitNcpToArray(self): + """Takes dataY as a 2D array and returns the 2D array best fit parameters.""" + + for index in range(len(self._dataY)): + + specFitPars = self.fitNcpToSingleSpec(index) + + self._fit_parameters[index] = specFitPars + + if np.all(specFitPars == 0): + print("Skipped spectra.") + else: + with np.printoptions( + suppress=True, precision=4, linewidth=200, threshold=sys.maxsize + ): + print(specFitPars) + + assert ~np.all( + self._fit_parameters == 0 + ), "Either Fits are all zero or assignment of fitting not working" + return self._fit_parameters + + + def createTableWSForFitPars(self, wsName, noOfMasses, arrFitPars): + tableWS = CreateEmptyTableWorkspace( + OutputWorkspace=wsName + "_Best_Fit_NCP_Parameters" + ) + tableWS.setTitle("SCIPY Fit") + tableWS.addColumn(type="float", name="Spec Idx") + for i in range(int(noOfMasses)): + tableWS.addColumn(type="float", name=f"Intensity {i}") + tableWS.addColumn(type="float", name=f"Width {i}") + tableWS.addColumn(type="float", name=f"Center {i}") + tableWS.addColumn(type="float", name="Norm Chi2") + tableWS.addColumn(type="float", name="No Iter") + + for row in arrFitPars: # Pass array onto table ws + tableWS.addRow(row) + return + + + def calculateNcpArr(self, arrBestFitPars): + """Calculates the matrix of NCP from matrix of best fit parameters""" + + allNcpForEachMass = [] + for index in range(len(arrBestFitPars)): + ncpForEachMass = self.calculateNcpRow(arrBestFitPars[index], index) + + allNcpForEachMass.append(ncpForEachMass) + + allNcpForEachMass = np.array(allNcpForEachMass) + allNcpTotal = np.sum(allNcpForEachMass, axis=1) + return allNcpForEachMass, allNcpTotal + + + def calculateNcpRow(self, initPars, index): + """input: all row shape + output: row shape with the ncpTotal for each mass""" + + if np.all(initPars == 0): + return np.zeros(self._y_space_arrays.shape) + + ncpForEachMass, ncpTotal = self.calculateNcpSpec(initPars, index) + return ncpForEachMass + + + def createNcpWorkspaces(self, ncpForEachMass, ncpTotal, ws): + """Creates workspaces from ncp array data""" + + # Need to rearrage array of yspaces into seperate arrays for each mass + ncpForEachMass = switchFirstTwoAxis(ncpForEachMass) + + # Use ws dataX to match with histogram data + dataX = ws.extractX()[ + :, : ncpTotal.shape[1] + ] # Make dataX match ncp shape automatically + assert ( + ncpTotal.shape == dataX.shape + ), "DataX and DataY in ws need to be the same shape." + + ncpTotWS = createWS( + dataX, ncpTotal, np.zeros(dataX.shape), ws.name() + "_TOF_Fitted_Profiles" + ) + MaskDetectors(Workspace=ncpTotWS, WorkspaceIndexList=ic.maskedDetectorIdx) + wsTotNCPSum = SumSpectra( + InputWorkspace=ncpTotWS, OutputWorkspace=ncpTotWS.name() + "_Sum" + ) + + # Individual ncp workspaces + wsMNCPSum = [] + for i, ncp_m in enumerate(ncpForEachMass): + ncpMWS = createWS( + dataX, + ncp_m, + np.zeros(dataX.shape), + ws.name() + "_TOF_Fitted_Profile_" + str(i), + ) + MaskDetectors(Workspace=ncpMWS, WorkspaceIndexList=self._mask_detector_index) + wsNCPSum = SumSpectra( + InputWorkspace=ncpMWS, OutputWorkspace=ncpMWS.name() + "_Sum" + ) + wsMNCPSum.append(wsNCPSum) + + return wsTotNCPSum, wsMNCPSum + + + def plotSumNCPFits(self, wsDataSum, wsTotNCPSum, wsMNCPSum): + # if IC.runningSampleWS: # Skip saving figure if running bootstrap + # return + + if ~self._fig_save_path: + return + lw = 2 + + fig, ax = plt.subplots(subplot_kw={"projection": "mantid"}) + ax.errorbar(wsDataSum, "k.", label="Spectra") + + ax.plot(wsTotNCPSum, "r-", label="Total NCP", linewidth=lw) + for m, wsNcp in zip(self._masses, wsMNCPSum): + ax.plot(wsNcp, label=f"NCP m={m}", linewidth=lw) + + ax.set_xlabel("TOF") + ax.set_ylabel("Counts") + ax.set_title("Sum of NCP fits") + ax.legend() + + fileName = wsDataSum.name() + "_NCP_Fits.pdf" + savePath = self._fig_save_path / fileName + plt.savefig(savePath, bbox_inches="tight") + plt.close(fig) + return + + + def extractMeans(self, wsName): + """Extract widths and intensities from tableWorkspace""" + + fitParsTable = mtd[wsName + "_Best_Fit_NCP_Parameters"] + widths = np.zeros(self._masses.size, fitParsTable.rowCount()) + intensities = np.zeros(widths.shape) + for i in range(self._masses.size): + widths[i] = fitParsTable.column(f"Width {i}") + intensities[i] = fitParsTable.column(f"Intensity {i}") + + ( + meanWidths, + stdWidths, + meanIntensityRatios, + stdIntensityRatios, + ) = self.calculateMeansAndStds(widths, intensities) + + assert ( + len(widths) == self._masses.size + ), "Widths and intensities must be in shape (noOfMasses, noOfSpec)" + return meanWidths, stdWidths, meanIntensityRatios, stdIntensityRatios + + + def createMeansAndStdTableWS( + self, wsName, meanWidths, stdWidths, meanIntensityRatios, stdIntensityRatios + ): + meansTableWS = CreateEmptyTableWorkspace( + OutputWorkspace=wsName + "_Mean_Widths_And_Intensities" + ) + meansTableWS.addColumn(type="float", name="Mass") + meansTableWS.addColumn(type="float", name="Mean Widths") + meansTableWS.addColumn(type="float", name="Std Widths") + meansTableWS.addColumn(type="float", name="Mean Intensities") + meansTableWS.addColumn(type="float", name="Std Intensities") + + print("\nCreated Table with means and std:") + print("\nMass Mean \u00B1 Std Widths Mean \u00B1 Std Intensities\n") + for m, mw, stdw, mi, stdi in zip( + self._masses.astype(float), + meanWidths, + stdWidths, + meanIntensityRatios, + stdIntensityRatios, + ): + meansTableWS.addRow([m, mw, stdw, mi, stdi]) + print(f"{m:5.2f} {mw:10.5f} \u00B1 {stdw:7.5f} {mi:10.5f} \u00B1 {stdi:7.5f}") + print("\n") + return + + + def calculateMeansAndStds(self, widthsIn, intensitiesIn): + betterWidths, betterIntensities = self.filterWidthsAndIntensities(widthsIn, intensitiesIn) + + meanWidths = np.nanmean(betterWidths, axis=1) + stdWidths = np.nanstd(betterWidths, axis=1) + + meanIntensityRatios = np.nanmean(betterIntensities, axis=1) + stdIntensityRatios = np.nanstd(betterIntensities, axis=1) + + return meanWidths, stdWidths, meanIntensityRatios, stdIntensityRatios + + + def filterWidthsAndIntensities(self, widthsIn, intensitiesIn): + """Puts nans in places to be ignored""" + + widths = widthsIn.copy() # Copy to avoid accidental changes in arrays + intensities = intensitiesIn.copy() + + zeroSpecs = np.all( + widths == 0, axis=0 + ) # Catches all failed fits, not just masked spectra + widths[:, zeroSpecs] = np.nan + intensities[:, zeroSpecs] = np.nan + + meanWidths = np.nanmean(widths, axis=1)[:, np.newaxis] + + widthDeviation = np.abs(widths - meanWidths) + stdWidths = np.nanstd(widths, axis=1)[:, np.newaxis] + + # Put nan in places where width deviation is bigger than std + filterMask = widthDeviation > stdWidths + betterWidths = np.where(filterMask, np.nan, widths) + + maskedIntensities = np.where(filterMask, np.nan, intensities) + betterIntensities = maskedIntensities / np.sum( + maskedIntensities, axis=0 + ) # Not nansum() + + # TODO: sort this out + # When trying to estimate HToMassIdxRatio and normalization fails, skip normalization + # if np.all(np.isnan(betterIntensities)) & IC.runningPreliminary: + # assert IC.noOfMSIterations == 0, ( + # "Calculation of mean intensities failed, cannot proceed with MS correction." + # "Try to run again with noOfMSIterations=0." + # ) + # betterIntensities = maskedIntensities + # else: + # pass + + assert np.all(meanWidths != np.nan), "At least one mean of widths is nan!" + assert np.sum(filterMask) >= 1, "No widths survive filtering condition" + assert not (np.all(np.isnan(betterWidths))), "All filtered widths are nan" + assert not (np.all(np.isnan(betterIntensities))), "All filtered intensities are nan" + assert np.nanmax(betterWidths) != np.nanmin( + betterWidths + ), f"All fitered widths have the same value: {np.nanmin(betterWidths)}" + assert np.nanmax(betterIntensities) != np.nanmin( + betterIntensities + ), f"All fitered widths have the same value: {np.nanmin(betterIntensities)}" + + return betterWidths, betterIntensities + + + def fitNcpToSingleSpec( + self, index): + """Fits the NCP and returns the best fit parameters for one spectrum""" + + if np.all(self._dataY == 0): + return np.zeros(len(self._initial_fit_parameters) + 3) + + result = optimize.minimize( + self.errorFunction, + self._initial_fit_parameters, + args=(index), + method="SLSQP", + bounds=self._bounds, + constraints=self._constraints, + ) + fitPars = result["x"] + + noDegreesOfFreedom = len(self._dataY) - len(fitPars) + specFitPars = np.append(self._instrument_params[0], fitPars) + return np.append(specFitPars, [result["fun"] / noDegreesOfFreedom, result["nit"]]) + + + def errorFunction(self, pars, index): + """Error function to be minimized, operates in TOF space""" + + ncpForEachMass, ncpTotal = self.calculateNcpSpec(pars, index) + + # Ignore any masked values from Jackknife or masked tof range + zerosMask = self._dataY[index] == 0 + ncpTotal = ncpTotal[~zerosMask] + dataYf = self._dataY[index, ~zerosMask] + dataEf = self._dataE[index, ~zerosMask] + + if np.all(self._dataE[index] == 0): # When errors not present + return np.sum((ncpTotal - dataYf) ** 2) + + return np.sum((ncpTotal - dataYf) ** 2 / dataEf**2) + + + def calculateNcpSpec(self, pars, index): + """Creates a synthetic C(t) to be fitted to TOF values of a single spectrum, from J(y) and resolution functions + Shapes: datax (1, n), ySpacesForEachMass (4, n), res (4, 2), deltaQ (1, n), E0 (1,n), + where n is no of bins""" + + masses, intensities, widths, centers = self.prepareArraysFromPars(pars) + v0, E0, deltaE, deltaQ = self._kinematic_arrays[index] + + gaussRes, lorzRes = self.caculateResolutionForEachMass(centers, index) + totalGaussWidth = np.sqrt(widths**2 + gaussRes**2) + + JOfY = self.pseudoVoigt(self._y_space_arrays[index] - self._centers[index], totalGaussWidth, lorzRes) + + FSE = ( + -numericalThirdDerivative(self._y_space_arrays[index], JOfY) + * widths**4 + / deltaQ + * 0.72 + ) + ncpForEachMass = intensities * (JOfY + FSE) * E0 * E0 ** (-0.92) * masses / deltaQ + ncpTotal = np.sum(ncpForEachMass, axis=0) + return ncpForEachMass, ncpTotal + + + def prepareArraysFromPars(self, initPars): + """Extracts the intensities, widths and centers from the fitting parameters + Reshapes all of the arrays to collumns, for the calculation of the ncp,""" + + masses = self._masses[:, np.newaxis] + intensities = initPars[::3].reshape(masses.shape) + widths = initPars[1::3].reshape(masses.shape) + centers = initPars[2::3].reshape(masses.shape) + return masses, intensities, widths, centers + + + def caculateResolutionForEachMass(self, centers, index): + """Calculates the gaussian and lorentzian resolution + output: two column vectors, each row corresponds to each mass""" + + gaussianResWidth = self.calcGaussianResolution(centers, index) + lorentzianResWidth = self.calcLorentzianResolution(centers, index) + return gaussianResWidth, lorentzianResWidth + + + @staticmethod + def kinematicsAtYCenters(self, centers, index): + """v0, E0, deltaE, deltaQ at the peak of the ncpTotal for each mass""" + + shapeOfArrays = centers.shape + proximityToYCenters = np.abs(self._y_space_arrays[index] - centers) + yClosestToCenters = proximityToYCenters.min(axis=1).reshape(shapeOfArrays) + yCentersMask = proximityToYCenters == yClosestToCenters + + v0, E0, deltaE, deltaQ = self._kinematic_arrays[index] + + # Expand arrays to match shape of yCentersMask + v0 = v0 * np.ones(shapeOfArrays) + E0 = E0 * np.ones(shapeOfArrays) + deltaE = deltaE * np.ones(shapeOfArrays) + deltaQ = deltaQ * np.ones(shapeOfArrays) + + v0 = v0[yCentersMask].reshape(shapeOfArrays) + E0 = E0[yCentersMask].reshape(shapeOfArrays) + deltaE = deltaE[yCentersMask].reshape(shapeOfArrays) + deltaQ = deltaQ[yCentersMask].reshape(shapeOfArrays) + return v0, E0, deltaE, deltaQ + + + def calcGaussianResolution(self, centers, index): + masses = self._masses.reshape((self._masses.size, 1)) + v0, E0, delta_E, delta_Q = self.kinematicsAtYCenters(centers, index) + det, plick, angle, T0, L0, L1 = self._instrument_params[index] + dE1, dTOF, dTheta, dL0, dL1, dE1_lorz = self._resolution_params[index] + mN, Ef, en_to_vel, vf, hbar = loadConstants() + + angle = angle * np.pi / 180 + + dWdE1 = 1.0 + (E0 / Ef) ** 1.5 * (L1 / L0) + dWdTOF = 2.0 * E0 * v0 / L0 + dWdL1 = 2.0 * E0**1.5 / Ef**0.5 / L0 + dWdL0 = 2.0 * E0 / L0 + + dW2 = ( + dWdE1**2 * dE1**2 + + dWdTOF**2 * dTOF**2 + + dWdL1**2 * dL1**2 + + dWdL0**2 * dL0**2 + ) + # conversion from meV^2 to A^-2, dydW = (M/q)^2 + dW2 *= (masses / hbar**2 / delta_Q) ** 2 + + dQdE1 = ( + 1.0 + - (E0 / Ef) ** 1.5 * L1 / L0 + - np.cos(angle) * ((E0 / Ef) ** 0.5 - L1 / L0 * E0 / Ef) + ) + dQdTOF = 2.0 * E0 * v0 / L0 + dQdL1 = 2.0 * E0**1.5 / L0 / Ef**0.5 + dQdL0 = 2.0 * E0 / L0 + dQdTheta = 2.0 * np.sqrt(E0 * Ef) * np.sin(angle) + + dQ2 = ( + dQdE1**2 * dE1**2 + + (dQdTOF**2 * dTOF**2 + dQdL1**2 * dL1**2 + dQdL0**2 * dL0**2) + * np.abs(Ef / E0 * np.cos(angle) - 1) + + dQdTheta**2 * dTheta**2 + ) + dQ2 *= (mN / hbar**2 / delta_Q) ** 2 + + # in A-1 #same as dy^2 = (dy/dw)^2*dw^2 + (dy/dq)^2*dq^2 + gaussianResWidth = np.sqrt(dW2 + dQ2) + return gaussianResWidth + + + def calcLorentzianResolution(self, centers, index): + masses = self._masses.reshape((self._masses.size, 1)) + v0, E0, delta_E, delta_Q = self.kinematicsAtYCenters(centers, index) + det, plick, angle, T0, L0, L1 = self._instrument_params[index] + dE1, dTOF, dTheta, dL0, dL1, dE1_lorz = self._resolution_params[index] + mN, Ef, en_to_vel, vf, hbar = loadConstants() + + angle = angle * np.pi / 180 + + dWdE1_lor = (1.0 + (E0 / Ef) ** 1.5 * (L1 / L0)) ** 2 + # conversion from meV^2 to A^-2 + dWdE1_lor *= (masses / hbar**2 / delta_Q) ** 2 + + dQdE1_lor = ( + 1.0 + - (E0 / Ef) ** 1.5 * L1 / L0 + - np.cos(angle) * ((E0 / Ef) ** 0.5 + L1 / L0 * E0 / Ef) + ) ** 2 + dQdE1_lor *= (mN / hbar**2 / delta_Q) ** 2 + + lorentzianResWidth = np.sqrt(dWdE1_lor + dQdE1_lor) * dE1_lorz # in A-1 + return lorentzianResWidth + + + def pseudoVoigt(self, x, sigma, gamma): + """Convolution between Gaussian with std sigma and Lorentzian with HWHM gamma""" + fg, fl = 2.0 * sigma * np.sqrt(2.0 * np.log(2.0)), 2.0 * gamma + f = 0.5346 * fl + np.sqrt(0.2166 * fl**2 + fg**2) + eta = 1.36603 * fl / f - 0.47719 * (fl / f) ** 2 + 0.11116 * (fl / f) ** 3 + sigma_v, gamma_v = f / (2.0 * np.sqrt(2.0 * np.log(2.0))), f / 2.0 + pseudo_voigt = eta * lorentizian(x, gamma_v) + (1.0 - eta) * gaussian(x, sigma_v) + + norm = ( + np.abs(np.trapz(pseudo_voigt, x, axis=1))[:, np.newaxis] if self._normVoigt else 1 + ) + return pseudo_voigt / norm + + + def createWorkspacesForMSCorrection(ic, meanWidths, meanIntensityRatios, wsNCPM): + """Creates _MulScattering and _TotScattering workspaces used for the MS correction""" + + createSlabGeometry(ic, wsNCPM) # Sample properties for MS correction + + sampleProperties = calcMSCorrectionSampleProperties( + ic, meanWidths, meanIntensityRatios + ) + print( + "\nThe sample properties for Multiple Scattering correction are:\n\n", + sampleProperties, + "\n", + ) + + return createMulScatWorkspaces(ic, wsNCPM, sampleProperties) + + + def createSlabGeometry(ic, wsNCPM): + half_height, half_width, half_thick = ( + 0.5 * ic.vertical_width, + 0.5 * ic.horizontal_width, + 0.5 * ic.thickness, + ) + xml_str = ( + ' ' + + ' ' + % (half_width, -half_height, half_thick) + + ' ' + % (half_width, half_height, half_thick) + + ' ' + % (half_width, -half_height, -half_thick) + + ' ' + % (-half_width, -half_height, half_thick) + + "" + ) + + CreateSampleShape(wsNCPM, xml_str) + + + def calcMSCorrectionSampleProperties(ic, meanWidths, meanIntensityRatios): + masses = ic.masses.flatten() + + # If Backsscattering mode and H is present in the sample, add H to MS properties + if ic.modeRunning == "BACKWARD": + if ic.HToMassIdxRatio is not None: # If H is present, ratio is a number + masses = np.append(masses, 1.0079) + meanWidths = np.append(meanWidths, 5.0) + + HIntensity = ic.HToMassIdxRatio * meanIntensityRatios[ic.massIdx] + meanIntensityRatios = np.append(meanIntensityRatios, HIntensity) + meanIntensityRatios /= np.sum(meanIntensityRatios) + + MSProperties = np.zeros(3 * len(masses)) + MSProperties[::3] = masses + MSProperties[1::3] = meanIntensityRatios + MSProperties[2::3] = meanWidths + sampleProperties = list(MSProperties) + + return sampleProperties + + + def createMulScatWorkspaces(ic, ws, sampleProperties): + """Uses the Mantid algorithm for the MS correction to create two Workspaces _TotScattering and _MulScattering""" + + print("\nEvaluating the Multiple Scattering Correction...\n") + # selects only the masses, every 3 numbers + MS_masses = sampleProperties[::3] + # same as above, but starts at first intensities + MS_amplitudes = sampleProperties[1::3] + + dens, trans = VesuvioThickness( + Masses=MS_masses, + Amplitudes=MS_amplitudes, + TransmissionGuess=ic.transmission_guess, + Thickness=0.1, + ) + + _TotScattering, _MulScattering = VesuvioCalculateMS( + ws, + NoOfMasses=len(MS_masses), + SampleDensity=dens.cell(9, 1), + AtomicProperties=sampleProperties, + BeamRadius=2.5, + NumScatters=ic.multiple_scattering_order, + NumEventsPerRun=int(ic.number_of_events), + ) + + data_normalisation = Integration(ws) + simulation_normalisation = Integration("_TotScattering") + for workspace in ("_MulScattering", "_TotScattering"): + Divide( + LHSWorkspace=workspace, + RHSWorkspace=simulation_normalisation, + OutputWorkspace=workspace, + ) + Multiply( + LHSWorkspace=workspace, + RHSWorkspace=data_normalisation, + OutputWorkspace=workspace, + ) + RenameWorkspace(InputWorkspace=workspace, OutputWorkspace=ws.name() + workspace) + SumSpectra( + ws.name() + workspace, OutputWorkspace=ws.name() + workspace + "_Sum" + ) + + DeleteWorkspaces([data_normalisation, simulation_normalisation, trans, dens]) + # The only remaining workspaces are the _MulScattering and _TotScattering + return mtd[ws.name() + "_MulScattering"] + + + def createWorkspacesForGammaCorrection(ic, meanWidths, meanIntensityRatios, wsNCPM): + """Creates _gamma_background correction workspace to be subtracted from the main workspace""" + + inputWS = wsNCPM.name() + + profiles = calcGammaCorrectionProfiles(ic.masses, meanWidths, meanIntensityRatios) + + # Approach below not currently suitable for current versions of Mantid, but will be in the future + # background, corrected = VesuvioCalculateGammaBackground(InputWorkspace=inputWS, ComptonFunction=profiles) + # DeleteWorkspace(corrected) + # RenameWorkspace(InputWorkspace= background, OutputWorkspace = inputWS+"_Gamma_Background") + + ws = CloneWorkspace(InputWorkspace=inputWS, OutputWorkspace="tmpGC") + for spec in range(ws.getNumberHistograms()): + background, corrected = VesuvioCalculateGammaBackground( + InputWorkspace=inputWS, ComptonFunction=profiles, WorkspaceIndexList=spec + ) + ws.dataY(spec)[:], ws.dataE(spec)[:] = ( + background.dataY(0)[:], + background.dataE(0)[:], + ) + DeleteWorkspace(background) + DeleteWorkspace(corrected) + RenameWorkspace( + InputWorkspace="tmpGC", OutputWorkspace=inputWS + "_Gamma_Background" + ) + + Scale( + InputWorkspace=inputWS + "_Gamma_Background", + OutputWorkspace=inputWS + "_Gamma_Background", + Factor=0.9, + Operation="Multiply", + ) + return mtd[inputWS + "_Gamma_Background"] + + + def calcGammaCorrectionProfiles(masses, meanWidths, meanIntensityRatios): + masses = masses.flatten() + profiles = "" + for mass, width, intensity in zip(masses, meanWidths, meanIntensityRatios): + profiles += ( + "name=GaussianComptonProfile,Mass=" + + str(mass) + + ",Width=" + + str(width) + + ",Intensity=" + + str(intensity) + + ";" + ) + print("\n The sample properties for Gamma Correction are:\n", profiles) + return profiles + + + class resultsObject: + """Used to collect results from workspaces and store them in .npz files for testing.""" + + def __init__(self, ic): + allIterNcp = [] + allFitWs = [] + allTotNcp = [] + allBestPar = [] + allMeanWidhts = [] + allMeanIntensities = [] + allStdWidths = [] + allStdIntensities = [] + j = 0 + while True: + try: + wsIterName = ic.name + str(j) + + # Extract ws that were fitted + ws = mtd[wsIterName] + allFitWs.append(ws.extractY()) + + # Extract total ncp + totNcpWs = mtd[wsIterName + "_TOF_Fitted_Profiles"] + allTotNcp.append(totNcpWs.extractY()) + + # Extract best fit parameters + fitParTable = mtd[wsIterName + "_Best_Fit_NCP_Parameters"] + bestFitPars = [] + for key in fitParTable.keys(): + bestFitPars.append(fitParTable.column(key)) + allBestPar.append(np.array(bestFitPars).T) + + # Extract individual ncp + allNCP = [] + i = 0 + while True: # By default, looks for all ncp ws until it breaks + try: + ncpWsToAppend = mtd[ + wsIterName + "_TOF_Fitted_Profile_" + str(i) + ] + allNCP.append(ncpWsToAppend.extractY()) + i += 1 + except KeyError: + break + allNCP = switchFirstTwoAxis(np.array(allNCP)) + allIterNcp.append(allNCP) + + # Extract Mean and Std Widths, Intensities + meansTable = mtd[wsIterName + "_Mean_Widths_And_Intensities"] + allMeanWidhts.append(meansTable.column("Mean Widths")) + allStdWidths.append(meansTable.column("Std Widths")) + allMeanIntensities.append(meansTable.column("Mean Intensities")) + allStdIntensities.append(meansTable.column("Std Intensities")) + + j += 1 + except KeyError: + break + + self.all_fit_workspaces = np.array(allFitWs) + self.all_spec_best_par_chi_nit = np.array(allBestPar) + self.all_tot_ncp = np.array(allTotNcp) + self.all_ncp_for_each_mass = np.array(allIterNcp) + + self.all_mean_widths = np.array(allMeanWidhts) + self.all_mean_intensities = np.array(allMeanIntensities) + self.all_std_widths = np.array(allStdWidths) + self.all_std_intensities = np.array(allStdIntensities) + + # Pass all attributes of ic into attributes to be used whithin this object + self.maskedDetectorIdx = ic.maskedDetectorIdx + self.masses = ic.masses + self.noOfMasses = ic.noOfMasses + self.resultsSavePath = ic.resultsSavePath + + def save(self): + """Saves all of the arrays stored in this object""" + + # TODO: Take out nans next time when running original results + # Because original results were recently saved with nans, mask spectra with nans + self.all_spec_best_par_chi_nit[:, self.maskedDetectorIdx, :] = np.nan + self.all_ncp_for_each_mass[:, self.maskedDetectorIdx, :, :] = np.nan + self.all_tot_ncp[:, self.maskedDetectorIdx, :] = np.nan + + savePath = self.resultsSavePath + np.savez( + savePath, + all_fit_workspaces=self.all_fit_workspaces, + all_spec_best_par_chi_nit=self.all_spec_best_par_chi_nit, + all_mean_widths=self.all_mean_widths, + all_mean_intensities=self.all_mean_intensities, + all_std_widths=self.all_std_widths, + all_std_intensities=self.all_std_intensities, + all_tot_ncp=self.all_tot_ncp, + all_ncp_for_each_mass=self.all_ncp_for_each_mass, + ) + diff --git a/src/mvesuvio/oop/analysis_helpers.py b/src/mvesuvio/oop/analysis_helpers.py index e69de29b..b0a58d00 100644 --- a/src/mvesuvio/oop/analysis_helpers.py +++ b/src/mvesuvio/oop/analysis_helpers.py @@ -0,0 +1,191 @@ + +from mantid.simpleapi import Load, Rebin, Scale, SumSpectra, Minus, CropWorkspace, \ + CloneWorkspace, MaskDetectors, CreateWorkspace +from mvesuvio.analysis_fitting import passDataIntoWS, replaceZerosWithNCP +import numpy as np + + +def loadRawAndEmptyWsFromUserPath(userWsRawPath, userWsEmptyPath, + tofBinning, name, scaleRaw, scaleEmpty, subEmptyFromRaw): + print("\nLoading local workspaces ...\n") + Load(Filename=str(userWsRawPath), OutputWorkspace=name + "raw") + Rebin( + InputWorkspace=name + "raw", + Params=tofBinning, + OutputWorkspace=name + "raw", + ) + + assert (isinstance(scaleRaw, float)) | ( + isinstance(scaleRaw, int) + ), "Scaling factor of raw ws needs to be float or int." + Scale( + InputWorkspace=name + "raw", + OutputWorkspace=name + "raw", + Factor=str(scaleRaw), + ) + + SumSpectra(InputWorkspace=name + "raw", OutputWorkspace=name + "raw" + "_sum") + wsToBeFitted = CloneWorkspace( + InputWorkspace=name + "raw", OutputWorkspace=name + "uncroped_unmasked" + ) + + # if mode=="DoubleDifference": + if subEmptyFromRaw: + Load(Filename=str(userWsEmptyPath), OutputWorkspace=name + "empty") + Rebin( + InputWorkspace=name + "empty", + Params=tofBinning, + OutputWorkspace=name + "empty", + ) + + assert (isinstance(scaleEmpty, float)) | ( + isinstance(scaleEmpty, int) + ), "Scaling factor of empty ws needs to be float or int" + Scale( + InputWorkspace=name + "empty", + OutputWorkspace=name + "empty", + Factor=str(scaleEmpty), + ) + + SumSpectra( + InputWorkspace=name + "empty", OutputWorkspace=name + "empty" + "_sum" + ) + + wsToBeFitted = Minus( + LHSWorkspace=name + "raw", + RHSWorkspace=name + "empty", + OutputWorkspace=name + "uncroped_unmasked", + ) + return wsToBeFitted + + +def cropAndMaskWorkspace(ws, firstSpec, lastSpec, maskedDetectorIdx, maskTOFRange): + """Returns cloned and cropped workspace with modified name""" + # Read initial Spectrum number + wsFirstSpec = ws.getSpectrumNumbers()[0] + assert ( + firstSpec >= wsFirstSpec + ), "Can't crop workspace, firstSpec < first spectrum in workspace." + + initialIdx = firstSpec - wsFirstSpec + lastIdx = lastSpec - wsFirstSpec + + newWsName = ws.name().split("uncroped")[0] # Retrieve original name + wsCrop = CropWorkspace( + InputWorkspace=ws, + StartWorkspaceIndex=initialIdx, + EndWorkspaceIndex=lastIdx, + OutputWorkspace=newWsName, + ) + + maskBinsWithZeros(wsCrop, maskTOFRange) # Used to mask resonance peaks + + MaskDetectors(Workspace=wsCrop, WorkspaceIndexList=maskedDetectorIdx) + return wsCrop + + +def maskBinsWithZeros(ws, maskTOFRange): + """ + Masks a given TOF range on ws with zeros on dataY. + Leaves errors dataE unchanged, as they are used by later treatments. + Used to mask resonance peaks. + """ + + if maskTOFRange is None: # Masked TOF bins not found, skip + return + + dataX, dataY, dataE = extractWS(ws) + start, end = [int(s) for s in maskTOFRange.split(",")] + assert ( + start <= end + ), "Start value for masking needs to be smaller or equal than end." + mask = (dataX >= start) & (dataX <= end) # TOF region to mask + + dataY[mask] = 0 + + passDataIntoWS(dataX, dataY, dataE, ws) + return + + +def extractWS(ws): + """Directly exctracts data from workspace into arrays""" + return ws.extractX(), ws.extractY(), ws.extractE() + + +def histToPointData(dataY, dataX, dataE): + """ + Used only when comparing with original results. + Sets each dataY point to the center of bins. + Last column of data is removed. + Removed original scaling by bin widths + """ + + histWidths = dataX[:, 1:] - dataX[:, :-1] + assert np.min(histWidths) == np.max( + histWidths + ), "Histogram widhts need to be the same length" + + dataYp = dataY[:, :-1] + dataEp = dataE[:, :-1] + dataXp = dataX[:, :-1] + histWidths[0, 0] / 2 + return dataYp, dataXp, dataEp + + +def loadConstants(): + """Output: the mass of the neutron, final energy of neutrons (selected by gold foil), + factor to change energies into velocities, final velocity of neutron and hbar""" + mN = 1.008 # a.m.u. + Ef = 4906.0 # meV + en_to_vel = 4.3737 * 1.0e-4 + vf = np.sqrt(Ef) * en_to_vel # m/us + hbar = 2.0445 + return mN, Ef, en_to_vel, vf, hbar + + +def gaussian(x, sigma): + """Gaussian function centered at zero""" + gaussian = np.exp(-(x**2) / 2 / sigma**2) + gaussian /= np.sqrt(2.0 * np.pi) * sigma + return gaussian + + +def lorentizian(x, gamma): + """Lorentzian centered at zero""" + lorentzian = gamma / np.pi / (x**2 + gamma**2) + return lorentzian + + +def numericalThirdDerivative(x, fun): + k6 = (-fun[:, 12:] + fun[:, :-12]) * 1 + k5 = (+fun[:, 11:-1] - fun[:, 1:-11]) * 24 + k4 = (-fun[:, 10:-2] + fun[:, 2:-10]) * 192 + k3 = (+fun[:, 9:-3] - fun[:, 3:-9]) * 488 + k2 = (+fun[:, 8:-4] - fun[:, 4:-8]) * 387 + k1 = (-fun[:, 7:-5] + fun[:, 5:-7]) * 1584 + + dev = k1 + k2 + k3 + k4 + k5 + k6 + dev /= np.power(x[:, 7:-5] - x[:, 6:-6], 3) + dev /= 12**3 + + derivative = np.zeros(fun.shape) + derivative[:, 6:-6] = dev + # Padded with zeros left and right to return array with same shape + return derivative + + +def switchFirstTwoAxis(A): + """Exchanges the first two indices of an array A, + rearranges matrices per spectrum for iteration of main fitting procedure + """ + return np.stack(np.split(A, len(A), axis=0), axis=2)[0] + + +def createWS(dataX, dataY, dataE, wsName): + ws = CreateWorkspace( + DataX=dataX.flatten(), + DataY=dataY.flatten(), + DataE=dataE.flatten(), + Nspec=len(dataY), + OutputWorkspace=wsName, + ) + return ws diff --git a/src/mvesuvio/oop/run_routine.py b/src/mvesuvio/oop/run_routine.py new file mode 100644 index 00000000..4f7571ac --- /dev/null +++ b/src/mvesuvio/oop/run_routine.py @@ -0,0 +1,14 @@ + +from mvesuvio.oop.analysis_helpers import loadRawAndEmptyWsFromUserPath, cropAndMaskWorkspace +from mvesuvio.oop.AnalysisRoutine import AnalysisRoutine + + +def run_analysis(): + + ws = loadRawAndEmptyWsFromUserPath() + + cropedWs = cropAndMaskWorkspace() + + AR = AnalysisRoutine() + AR.run() + From b6291d0963f74ed0c2fbeea5b82302b1f3c0496f Mon Sep 17 00:00:00 2001 From: GuiMacielPereira Date: Wed, 31 Jul 2024 13:56:00 +0100 Subject: [PATCH 04/25] Fix first part of routine The first part of the routine up until the MS and Gamma corrections is now working, have not tested it yet. --- src/mvesuvio/oop/AnalysisRoutine.py | 213 ++++++++++++---------- src/mvesuvio/oop/NeutronComptonProfile.py | 4 +- src/mvesuvio/oop/analysis_helpers.py | 4 +- src/mvesuvio/oop/run_routine.py | 37 +++- 4 files changed, 152 insertions(+), 106 deletions(-) diff --git a/src/mvesuvio/oop/AnalysisRoutine.py b/src/mvesuvio/oop/AnalysisRoutine.py index d989b2b9..8421d3fd 100644 --- a/src/mvesuvio/oop/AnalysisRoutine.py +++ b/src/mvesuvio/oop/AnalysisRoutine.py @@ -2,22 +2,27 @@ from mvesuvio.oop.analysis_helpers import extractWS, histToPointData, loadConstants, \ gaussian, lorentizian, numericalThirdDerivative, \ switchFirstTwoAxis, createWS -from mantid.simpleapi import mtd, CreateEmptyTableWorkspace, MaskDetectors, SumSpectra +from mantid.simpleapi import mtd, CreateEmptyTableWorkspace, MaskDetectors, SumSpectra, \ + CloneWorkspace +from mvesuvio.analysis_fitting import passDataIntoWS, replaceZerosWithNCP import numpy as np from scipy import optimize +import sys class AnalysisRoutine: - def __init__(self, workspace, ip_file, number_of_iterations, spectrum_range, mask_spectra, + def __init__(self, workspace, ip_file, number_of_iterations, mask_spectra, multiple_scattering_correction, gamma_correction, - transmission_guess, multiple_scattering_order, number_of_events): + transmission_guess=None, multiple_scattering_order=None, number_of_events=None): self._workspace_to_fit = workspace + self._name = workspace.name() self._ip_file = ip_file self._number_of_iterations = number_of_iterations - self._spectrum_range = spectrum_range + spectrum_list = workspace.getSpectrumNumbers() + self._firstSpec = min(spectrum_list) + self._lastSpec = max(spectrum_list) self._mask_spectra = mask_spectra - self._mask_detector_index = self._transmission_guess = transmission_guess self._multiple_scattering_order = multiple_scattering_order self._number_of_events = number_of_events @@ -29,41 +34,16 @@ def __init__(self, workspace, ip_file, number_of_iterations, spectrum_range, mas self._constraints = [] self._profiles = {} - self._masses = [p.mass for p in self._profiles] - # Only used for system tests, remove once tests are updated - self._run_hist_data = True + self._run_hist_data = False #True self._run_norm_voigt = False # Links to another AnalysisRoutine object: self._profiles_destination = None self._h_ratio_destination = None - # Variables used during fitting - - self._dataX, self._dataY, self._dataE = extractWS(self._workspace_to_fit) - resolutionPars, instrPars, kinematicArrays, ySpacesForEachMass = self.prepareFitArgs(self._dataX) - self._resolution_params = resolutionPars - self._instrument_params = instrPars - self._kinematic_arrays = kinematicArrays - self._y_space_arrays = ySpacesForEachMass - - self._initial_fit_parameters = [] - for p in self._profiles: - self._initial_fit_parameters.append(p.intensity) - self._initial_fit_parameters.append(p.width) - self._initial_fit_parameters.append(p.center) - - self._intensities = np.array([p.intensity for p in self._profiles])[:, np.newaxis] - self._widths = np.array([p.width for p in self._profiles])[:, np.newaxis] - self._centesr = np.array([p.center for p in self._profiles])[:, np.newaxis] - self._fit_parameters = np.zeros((len(self._dataY), 3 * len(self._profiles) + 3)) - - self._fig_save_path = None - - - def add_profiles(self, **args: NeutronComptonProfile): + def add_profiles(self, *args: NeutronComptonProfile): for profile in args: self._profiles[profile.label] = profile @@ -103,8 +83,40 @@ def profiles(self, incoming_profiles): self._profiles = {**self._profiles, **common_keys_profiles} + def _preprocess(self): + # Set up variables used during fitting + self._masses = np.array([p.mass for p in self._profiles.values()]) + self._dataX, self._dataY, self._dataE = extractWS(self._workspace_to_fit) + resolutionPars, instrPars, kinematicArrays, ySpacesForEachMass = self.prepareFitArgs() + self._resolution_params = resolutionPars + self._instrument_params = instrPars + self._kinematic_arrays = kinematicArrays + self._y_space_arrays = ySpacesForEachMass + + self._initial_fit_parameters = [] + self._bounds = [] + for p in self._profiles.values(): + self._initial_fit_parameters.append(p.intensity) + self._initial_fit_parameters.append(p.width) + self._initial_fit_parameters.append(p.center) + self._bounds.append(p._intensity_bounds) + self._bounds.append(p._width_bounds) + self._bounds.append(p._center_bounds) + + # self._intensities = np.array([p.intensity for p in self._profiles.values()])[:, np.newaxis] + # self._widths = np.array([p.width for p in self._profiles.values()])[:, np.newaxis] + # self._centers = np.array([p.center for p in self._profiles.values()])[:, np.newaxis] + self._fit_parameters = np.zeros((len(self._dataY), 3 * len(self._profiles) + 3)) + + self._fig_save_path = None + + def run(self): + assert len(self.profiles) > 0, "Add profiles before atempting to run the routine!" + + self._preprocess() + self.createTableInitialParameters() # Legacy code from Bootstrap @@ -186,12 +198,12 @@ def remaskValues(wsName, wsToMaskName): def createTableInitialParameters(self): - print("\nRUNNING ", self.modeRunning, " SCATTERING.\n") - if self.modeRunning == "BACKWARD": - print(f"\nH ratio to next lowest mass = {self._h_ratio}\n") + # print("\nRUNNING ", self.modeRunning, " SCATTERING.\n") + # if self.modeRunning == "BACKWARD": + # print(f"\nH ratio to next lowest mass = {self._h_ratio}\n") meansTableWS = CreateEmptyTableWorkspace( - OutputWorkspace=self.name + "_Initial_Parameters" + OutputWorkspace=self._name + "_Initial_Parameters" ) meansTableWS.addColumn(type="float", name="Mass") meansTableWS.addColumn(type="float", name="Initial Widths") @@ -202,7 +214,7 @@ def createTableInitialParameters(self): meansTableWS.addColumn(type="str", name="Bounds Centers") print("\nCreated Table with Initial Parameters:") - for p in self._profiles: + for p in self._profiles.values(): meansTableWS.addRow([p.mass, p.width, str(p.width_bounds), p.intensity, str(p.intensity_bounds), p.center, str(p.center_bounds)]) @@ -220,10 +232,9 @@ def fitNcpToWorkspace(self, ws): on a spectrum by spectrum basis. """ - if self._runHistData: # Converts point data from workspaces to histogram data + if self._run_hist_data: # Converts point data from workspaces to histogram data self._dataY, self._dataX, self._dataE = histToPointData(self._dataY, self._dataX, self._dataE) - print("\nFitting NCP:\n") arrFitPars = self.fitNcpToArray() @@ -240,16 +251,15 @@ def fitNcpToWorkspace(self, ws): return ncpTotal - def prepareFitArgs(self, dataX): - instrPars = self.loadInstrParsFileIntoArray(self._ip_file, self._firstSpec, self._lastSpec) + def prepareFitArgs(self): + instrPars = self.loadInstrParsFileIntoArray() resolutionPars = self.loadResolutionPars(instrPars) - v0, E0, delta_E, delta_Q = self.calculateKinematicsArrays(dataX, instrPars) + v0, E0, delta_E, delta_Q = self.calculateKinematicsArrays(instrPars) kinematicArrays = np.array([v0, E0, delta_E, delta_Q]) ySpacesForEachMass = self.convertDataXToYSpacesForEachMass( - dataX, delta_Q, delta_E + self._dataX, delta_Q, delta_E ) - kinematicArrays = self.reshapeArrayPerSpectrum(kinematicArrays) ySpacesForEachMass = self.reshapeArrayPerSpectrum(ySpacesForEachMass) return resolutionPars, instrPars, kinematicArrays, ySpacesForEachMass @@ -285,10 +295,11 @@ def loadResolutionPars(instrPars): return resolutionPars - @staticmethod - def calculateKinematicsArrays(dataX, instrPars): + def calculateKinematicsArrays(self, instrPars): """Kinematics quantities calculated from TOF data""" + dataX = self._dataX + mN, Ef, en_to_vel, vf, hbar = loadConstants() det, plick, angle, T0, L0, L1 = np.hsplit(instrPars, 6) # each is of len(dataX) t_us = dataX - T0 # T0 is electronic delay due to instruments @@ -338,11 +349,11 @@ def convertDataXToYSpacesForEachMass(self, dataX, delta_Q, delta_E): def fitNcpToArray(self): """Takes dataY as a 2D array and returns the 2D array best fit parameters.""" - for index in range(len(self._dataY)): + for row in range(len(self._dataY)): - specFitPars = self.fitNcpToSingleSpec(index) + specFitPars = self.fitNcpToSingleSpec(row) - self._fit_parameters[index] = specFitPars + self._fit_parameters[row] = specFitPars if np.all(specFitPars == 0): print("Skipped spectra.") @@ -380,8 +391,8 @@ def calculateNcpArr(self, arrBestFitPars): """Calculates the matrix of NCP from matrix of best fit parameters""" allNcpForEachMass = [] - for index in range(len(arrBestFitPars)): - ncpForEachMass = self.calculateNcpRow(arrBestFitPars[index], index) + for row in range(len(arrBestFitPars)): + ncpForEachMass = self.calculateNcpRow(arrBestFitPars[row], row) allNcpForEachMass.append(ncpForEachMass) @@ -390,14 +401,15 @@ def calculateNcpArr(self, arrBestFitPars): return allNcpForEachMass, allNcpTotal - def calculateNcpRow(self, initPars, index): + def calculateNcpRow(self, initPars, row): """input: all row shape output: row shape with the ncpTotal for each mass""" if np.all(initPars == 0): - return np.zeros(self._y_space_arrays.shape) + # return np.zeros(self._y_space_arrays.shape) + return np.zeros_like(self._y_space_arrays[row]) - ncpForEachMass, ncpTotal = self.calculateNcpSpec(initPars, index) + ncpForEachMass, ncpTotal = self.calculateNcpSpec(initPars, row) return ncpForEachMass @@ -415,10 +427,13 @@ def createNcpWorkspaces(self, ncpForEachMass, ncpTotal, ws): ncpTotal.shape == dataX.shape ), "DataX and DataY in ws need to be the same shape." - ncpTotWS = createWS( - dataX, ncpTotal, np.zeros(dataX.shape), ws.name() + "_TOF_Fitted_Profiles" - ) - MaskDetectors(Workspace=ncpTotWS, WorkspaceIndexList=ic.maskedDetectorIdx) + ncpTotWS = CloneWorkspace(InputWorkspace=ws.name(), OutputWorkspace=ws.name() + "_TOF_Fitted_Profiles") + passDataIntoWS(dataX, ncpTotal, np.zeros_like(dataX), ncpTotWS) + # ncpTotWS = createWS( + # dataX, ncpTotal, np.zeros(dataX.shape), ws.name() + "_TOF_Fitted_Profiles" + # ) + # MaskDetectors(Workspace=ncpTotWS, WorkspaceIndexList=ic.maskedDetectorIdx) + MaskDetectors(Workspace=ncpTotWS, SpectraList=self._mask_spectra) wsTotNCPSum = SumSpectra( InputWorkspace=ncpTotWS, OutputWorkspace=ncpTotWS.name() + "_Sum" ) @@ -426,13 +441,15 @@ def createNcpWorkspaces(self, ncpForEachMass, ncpTotal, ws): # Individual ncp workspaces wsMNCPSum = [] for i, ncp_m in enumerate(ncpForEachMass): - ncpMWS = createWS( - dataX, - ncp_m, - np.zeros(dataX.shape), - ws.name() + "_TOF_Fitted_Profile_" + str(i), - ) - MaskDetectors(Workspace=ncpMWS, WorkspaceIndexList=self._mask_detector_index) + ncpMWS = CloneWorkspace(InputWorkspace=ws.name(), OutputWorkspace=ws.name()+"_TOF_Fitted_Profile_" + str(i)) + passDataIntoWS(dataX, ncp_m, np.zeros_like(dataX), ncpMWS) + # ncpMWS = createWS( + # dataX, + # ncp_m, + # np.zeros(dataX.shape), + # ws.name() + "_TOF_Fitted_Profile_" + str(i), + # ) + MaskDetectors(Workspace=ncpMWS, SpectraList=self._mask_spectra) wsNCPSum = SumSpectra( InputWorkspace=ncpMWS, OutputWorkspace=ncpMWS.name() + "_Sum" ) @@ -445,7 +462,7 @@ def plotSumNCPFits(self, wsDataSum, wsTotNCPSum, wsMNCPSum): # if IC.runningSampleWS: # Skip saving figure if running bootstrap # return - if ~self._fig_save_path: + if not self._fig_save_path: return lw = 2 @@ -472,7 +489,7 @@ def extractMeans(self, wsName): """Extract widths and intensities from tableWorkspace""" fitParsTable = mtd[wsName + "_Best_Fit_NCP_Parameters"] - widths = np.zeros(self._masses.size, fitParsTable.rowCount()) + widths = np.zeros((self._masses.size, fitParsTable.rowCount())) intensities = np.zeros(widths.shape) for i in range(self._masses.size): widths[i] = fitParsTable.column(f"Width {i}") @@ -581,17 +598,16 @@ def filterWidthsAndIntensities(self, widthsIn, intensitiesIn): return betterWidths, betterIntensities - def fitNcpToSingleSpec( - self, index): + def fitNcpToSingleSpec(self, row): """Fits the NCP and returns the best fit parameters for one spectrum""" - if np.all(self._dataY == 0): + if np.all(self._dataY[row] == 0): return np.zeros(len(self._initial_fit_parameters) + 3) result = optimize.minimize( self.errorFunction, self._initial_fit_parameters, - args=(index), + args=(row), method="SLSQP", bounds=self._bounds, constraints=self._constraints, @@ -599,42 +615,42 @@ def fitNcpToSingleSpec( fitPars = result["x"] noDegreesOfFreedom = len(self._dataY) - len(fitPars) - specFitPars = np.append(self._instrument_params[0], fitPars) + specFitPars = np.append(self._instrument_params[row, 0], fitPars) return np.append(specFitPars, [result["fun"] / noDegreesOfFreedom, result["nit"]]) - def errorFunction(self, pars, index): + def errorFunction(self, pars, row): """Error function to be minimized, operates in TOF space""" - ncpForEachMass, ncpTotal = self.calculateNcpSpec(pars, index) + ncpForEachMass, ncpTotal = self.calculateNcpSpec(pars, row) # Ignore any masked values from Jackknife or masked tof range - zerosMask = self._dataY[index] == 0 + zerosMask = self._dataY[row] == 0 ncpTotal = ncpTotal[~zerosMask] - dataYf = self._dataY[index, ~zerosMask] - dataEf = self._dataE[index, ~zerosMask] + dataYf = self._dataY[row, ~zerosMask] + dataEf = self._dataE[row, ~zerosMask] - if np.all(self._dataE[index] == 0): # When errors not present + if np.all(self._dataE[row] == 0): # When errors not present return np.sum((ncpTotal - dataYf) ** 2) return np.sum((ncpTotal - dataYf) ** 2 / dataEf**2) - def calculateNcpSpec(self, pars, index): + def calculateNcpSpec(self, pars, row): """Creates a synthetic C(t) to be fitted to TOF values of a single spectrum, from J(y) and resolution functions Shapes: datax (1, n), ySpacesForEachMass (4, n), res (4, 2), deltaQ (1, n), E0 (1,n), where n is no of bins""" masses, intensities, widths, centers = self.prepareArraysFromPars(pars) - v0, E0, deltaE, deltaQ = self._kinematic_arrays[index] + v0, E0, deltaE, deltaQ = self._kinematic_arrays[row] - gaussRes, lorzRes = self.caculateResolutionForEachMass(centers, index) + gaussRes, lorzRes = self.caculateResolutionForEachMass(centers, row) totalGaussWidth = np.sqrt(widths**2 + gaussRes**2) - JOfY = self.pseudoVoigt(self._y_space_arrays[index] - self._centers[index], totalGaussWidth, lorzRes) + JOfY = self.pseudoVoigt(self._y_space_arrays[row] - centers, totalGaussWidth, lorzRes) FSE = ( - -numericalThirdDerivative(self._y_space_arrays[index], JOfY) + -numericalThirdDerivative(self._y_space_arrays[row], JOfY) * widths**4 / deltaQ * 0.72 @@ -655,25 +671,24 @@ def prepareArraysFromPars(self, initPars): return masses, intensities, widths, centers - def caculateResolutionForEachMass(self, centers, index): + def caculateResolutionForEachMass(self, centers, row): """Calculates the gaussian and lorentzian resolution output: two column vectors, each row corresponds to each mass""" - gaussianResWidth = self.calcGaussianResolution(centers, index) - lorentzianResWidth = self.calcLorentzianResolution(centers, index) + gaussianResWidth = self.calcGaussianResolution(centers, row) + lorentzianResWidth = self.calcLorentzianResolution(centers, row) return gaussianResWidth, lorentzianResWidth - @staticmethod - def kinematicsAtYCenters(self, centers, index): + def kinematicsAtYCenters(self, centers, row): """v0, E0, deltaE, deltaQ at the peak of the ncpTotal for each mass""" shapeOfArrays = centers.shape - proximityToYCenters = np.abs(self._y_space_arrays[index] - centers) + proximityToYCenters = np.abs(self._y_space_arrays[row] - centers) yClosestToCenters = proximityToYCenters.min(axis=1).reshape(shapeOfArrays) yCentersMask = proximityToYCenters == yClosestToCenters - v0, E0, deltaE, deltaQ = self._kinematic_arrays[index] + v0, E0, deltaE, deltaQ = self._kinematic_arrays[row] # Expand arrays to match shape of yCentersMask v0 = v0 * np.ones(shapeOfArrays) @@ -688,11 +703,11 @@ def kinematicsAtYCenters(self, centers, index): return v0, E0, deltaE, deltaQ - def calcGaussianResolution(self, centers, index): + def calcGaussianResolution(self, centers, row): masses = self._masses.reshape((self._masses.size, 1)) - v0, E0, delta_E, delta_Q = self.kinematicsAtYCenters(centers, index) - det, plick, angle, T0, L0, L1 = self._instrument_params[index] - dE1, dTOF, dTheta, dL0, dL1, dE1_lorz = self._resolution_params[index] + v0, E0, delta_E, delta_Q = self.kinematicsAtYCenters(centers, row) + det, plick, angle, T0, L0, L1 = self._instrument_params[row] + dE1, dTOF, dTheta, dL0, dL1, dE1_lorz = self._resolution_params[row] mN, Ef, en_to_vel, vf, hbar = loadConstants() angle = angle * np.pi / 180 @@ -734,11 +749,11 @@ def calcGaussianResolution(self, centers, index): return gaussianResWidth - def calcLorentzianResolution(self, centers, index): + def calcLorentzianResolution(self, centers, row): masses = self._masses.reshape((self._masses.size, 1)) - v0, E0, delta_E, delta_Q = self.kinematicsAtYCenters(centers, index) - det, plick, angle, T0, L0, L1 = self._instrument_params[index] - dE1, dTOF, dTheta, dL0, dL1, dE1_lorz = self._resolution_params[index] + v0, E0, delta_E, delta_Q = self.kinematicsAtYCenters(centers, row) + det, plick, angle, T0, L0, L1 = self._instrument_params[row] + dE1, dTOF, dTheta, dL0, dL1, dE1_lorz = self._resolution_params[row] mN, Ef, en_to_vel, vf, hbar = loadConstants() angle = angle * np.pi / 180 @@ -767,7 +782,7 @@ def pseudoVoigt(self, x, sigma, gamma): pseudo_voigt = eta * lorentizian(x, gamma_v) + (1.0 - eta) * gaussian(x, sigma_v) norm = ( - np.abs(np.trapz(pseudo_voigt, x, axis=1))[:, np.newaxis] if self._normVoigt else 1 + np.abs(np.trapz(pseudo_voigt, x, axis=1))[:, np.newaxis] if self._run_norm_voigt else 1 ) return pseudo_voigt / norm diff --git a/src/mvesuvio/oop/NeutronComptonProfile.py b/src/mvesuvio/oop/NeutronComptonProfile.py index de25aff7..31868b06 100644 --- a/src/mvesuvio/oop/NeutronComptonProfile.py +++ b/src/mvesuvio/oop/NeutronComptonProfile.py @@ -3,7 +3,7 @@ class NeutronComptonProfile: - def __init__(self, mass, label, intensity, width, center, + def __init__(self, label, mass, intensity, width, center, intensity_bounds, width_bounds, center_bounds): self._mass = mass self._label = label @@ -12,7 +12,7 @@ def __init__(self, mass, label, intensity, width, center, self._center = center self._intensity_bounds = intensity_bounds self._width_bounds = width_bounds - self.center_bounds = center_bounds + self._center_bounds = center_bounds @property def label(self): diff --git a/src/mvesuvio/oop/analysis_helpers.py b/src/mvesuvio/oop/analysis_helpers.py index b0a58d00..ace64b4a 100644 --- a/src/mvesuvio/oop/analysis_helpers.py +++ b/src/mvesuvio/oop/analysis_helpers.py @@ -59,7 +59,7 @@ def loadRawAndEmptyWsFromUserPath(userWsRawPath, userWsEmptyPath, return wsToBeFitted -def cropAndMaskWorkspace(ws, firstSpec, lastSpec, maskedDetectorIdx, maskTOFRange): +def cropAndMaskWorkspace(ws, firstSpec, lastSpec, maskedDetectors, maskTOFRange): """Returns cloned and cropped workspace with modified name""" # Read initial Spectrum number wsFirstSpec = ws.getSpectrumNumbers()[0] @@ -80,7 +80,7 @@ def cropAndMaskWorkspace(ws, firstSpec, lastSpec, maskedDetectorIdx, maskTOFRang maskBinsWithZeros(wsCrop, maskTOFRange) # Used to mask resonance peaks - MaskDetectors(Workspace=wsCrop, WorkspaceIndexList=maskedDetectorIdx) + MaskDetectors(Workspace=wsCrop, SpectraList=maskedDetectors) return wsCrop diff --git a/src/mvesuvio/oop/run_routine.py b/src/mvesuvio/oop/run_routine.py index 4f7571ac..c0969013 100644 --- a/src/mvesuvio/oop/run_routine.py +++ b/src/mvesuvio/oop/run_routine.py @@ -1,14 +1,45 @@ from mvesuvio.oop.analysis_helpers import loadRawAndEmptyWsFromUserPath, cropAndMaskWorkspace from mvesuvio.oop.AnalysisRoutine import AnalysisRoutine +from mvesuvio.oop.NeutronComptonProfile import NeutronComptonProfile +import numpy as np def run_analysis(): - ws = loadRawAndEmptyWsFromUserPath() + ws = loadRawAndEmptyWsFromUserPath( + userWsRawPath='/home/ljg28444/Documents/user_0/some_new_experiment/some_new_exp/input_ws/some_new_exp_raw_forward.nxs', + userWsEmptyPath='/home/ljg28444/Documents/user_0/some_new_experiment/some_new_exp/input_ws/some_new_exp_empty_forward.nxs', + tofBinning = "275.,1.,420", + name='exp', + scaleRaw=1, + scaleEmpty=1, + subEmptyFromRaw=False + ) - cropedWs = cropAndMaskWorkspace() + cropedWs = cropAndMaskWorkspace(ws, firstSpec=144, lastSpec=182, + maskedDetectors=[173, 174, 179], + maskTOFRange=None) - AR = AnalysisRoutine() + + AR = AnalysisRoutine(cropedWs, + ip_file='/home/ljg28444/.mvesuvio/ip_files/ip2018_3.par', + number_of_iterations=0, + mask_spectra=[173, 174, 179], + multiple_scattering_correction=False, + gamma_correction=False) + + H = NeutronComptonProfile('H', mass=1.0079, intensity=1, width=4.7, center=0, + intensity_bounds=[0, np.nan], width_bounds=[3, 6], center_bounds=[-3, 1]) + C = NeutronComptonProfile('C', mass=12, intensity=1, width=12.71, center=0, + intensity_bounds=[0, np.nan], width_bounds=[12.71, 12.71], center_bounds=[-3, 1]) + S = NeutronComptonProfile('S', mass=16, intensity=1, width=8.76, center=0, + intensity_bounds=[0, np.nan], width_bounds=[8.76, 8.76], center_bounds=[-3, 1]) + Co = NeutronComptonProfile('Co', mass=27, intensity=1, width=13.897, center=0, + intensity_bounds=[0, np.nan], width_bounds=[13.897, 13.897], center_bounds=[-3, 1]) + + AR.add_profiles(H, C, S, Co) AR.run() + +run_analysis() From 3927498d69b62a8f9bd9d28dd921c12a306730fb Mon Sep 17 00:00:00 2001 From: GuiMacielPereira Date: Wed, 31 Jul 2024 16:48:20 +0100 Subject: [PATCH 05/25] Fix remaining of routine Fixed second part of routine to use analysis object. Currently running in its entirety but have not checked tests --- src/mvesuvio/oop/AnalysisRoutine.py | 266 ++++++++++++++-------------- src/mvesuvio/oop/run_routine.py | 1 + 2 files changed, 136 insertions(+), 131 deletions(-) diff --git a/src/mvesuvio/oop/AnalysisRoutine.py b/src/mvesuvio/oop/AnalysisRoutine.py index 8421d3fd..c72a75dd 100644 --- a/src/mvesuvio/oop/AnalysisRoutine.py +++ b/src/mvesuvio/oop/AnalysisRoutine.py @@ -3,7 +3,9 @@ gaussian, lorentizian, numericalThirdDerivative, \ switchFirstTwoAxis, createWS from mantid.simpleapi import mtd, CreateEmptyTableWorkspace, MaskDetectors, SumSpectra, \ - CloneWorkspace + CloneWorkspace, DeleteWorkspace, VesuvioCalculateGammaBackground, \ + VesuvioCalculateMS, Scale, RenameWorkspace, Minus, CreateSampleShape, \ + VesuvioThickness, Integration, Divide, Multiply, DeleteWorkspaces from mvesuvio.analysis_fitting import passDataIntoWS, replaceZerosWithNCP import numpy as np from scipy import optimize @@ -12,8 +14,9 @@ class AnalysisRoutine: def __init__(self, workspace, ip_file, number_of_iterations, mask_spectra, - multiple_scattering_correction, gamma_correction, - transmission_guess=None, multiple_scattering_order=None, number_of_events=None): + multiple_scattering_correction, vertical_width, horizontal_width, thickness, + gamma_correction, transmission_guess=None, multiple_scattering_order=None, + number_of_events=None, results_path=None): self._workspace_to_fit = workspace self._name = workspace.name() @@ -26,10 +29,15 @@ def __init__(self, workspace, ip_file, number_of_iterations, mask_spectra, self._transmission_guess = transmission_guess self._multiple_scattering_order = multiple_scattering_order self._number_of_events = number_of_events + self._vertical_width = vertical_width + self._horizontal_width = horizontal_width + self._thickness = thickness self._multiple_scattering_correction = multiple_scattering_correction self._gamma_correction = gamma_correction + self._save_results_path = results_path + self._h_ratio = None self._constraints = [] self._profiles = {} @@ -151,33 +159,34 @@ def run(self): # Replace zero columns (bins) with ncp total fit # If ws has no zero column, then remains unchanged if iteration == 0: - wsNCPM = replaceZerosWithNCP(mtd[ic.name], ncpTotal) + wsNCPM = replaceZerosWithNCP(mtd[self._name], ncpTotal) - CloneWorkspace(InputWorkspace=ic.name, OutputWorkspace="tmpNameWs") + CloneWorkspace(InputWorkspace=self._name, OutputWorkspace="tmpNameWs") - if ic.GammaCorrectionFlag: - wsGC = createWorkspacesForGammaCorrection(ic, mWidths, mIntRatios, wsNCPM) + if self._gamma_correction: + wsGC = self.createWorkspacesForGammaCorrection(mWidths, mIntRatios, wsNCPM) Minus( LHSWorkspace="tmpNameWs", RHSWorkspace=wsGC, OutputWorkspace="tmpNameWs" ) - if ic.MSCorrectionFlag: - wsMS = createWorkspacesForMSCorrection(ic, mWidths, mIntRatios, wsNCPM) + if self._multiple_scattering_correction: + wsMS = self.createWorkspacesForMSCorrection(mWidths, mIntRatios, wsNCPM) Minus( LHSWorkspace="tmpNameWs", RHSWorkspace=wsMS, OutputWorkspace="tmpNameWs" ) - remaskValues(ic.name, "tmpNameWS") # Masks cols in the same place as in ic.name + self.remaskValues(self._name, "tmpNameWS") # Masks cols in the same place as in ic.name RenameWorkspace( - InputWorkspace="tmpNameWs", OutputWorkspace=ic.name + str(iteration + 1) + InputWorkspace="tmpNameWs", OutputWorkspace=self._name + str(iteration + 1) ) - wsFinal = mtd[ic.name + str(ic.noOfMSIterations)] - fittingResults = resultsObject(ic) - fittingResults.save() - return wsFinal, fittingResults + wsFinal = mtd[self._name + str(self._number_of_iterations)] + self._create_results_mehtods() + self.save_results() + return wsFinal, self + @staticmethod def remaskValues(wsName, wsToMaskName): """ Uses the ws before the MS correction to look for masked columns or dataE @@ -787,28 +796,26 @@ def pseudoVoigt(self, x, sigma, gamma): return pseudo_voigt / norm - def createWorkspacesForMSCorrection(ic, meanWidths, meanIntensityRatios, wsNCPM): + def createWorkspacesForMSCorrection(self, meanWidths, meanIntensityRatios, wsNCPM): """Creates _MulScattering and _TotScattering workspaces used for the MS correction""" - createSlabGeometry(ic, wsNCPM) # Sample properties for MS correction + self.createSlabGeometry(wsNCPM) # Sample properties for MS correction - sampleProperties = calcMSCorrectionSampleProperties( - ic, meanWidths, meanIntensityRatios - ) + sampleProperties = self.calcMSCorrectionSampleProperties(meanWidths, meanIntensityRatios) print( "\nThe sample properties for Multiple Scattering correction are:\n\n", sampleProperties, "\n", ) - return createMulScatWorkspaces(ic, wsNCPM, sampleProperties) + return self.createMulScatWorkspaces(wsNCPM, sampleProperties) - def createSlabGeometry(ic, wsNCPM): + def createSlabGeometry(self, wsNCPM): half_height, half_width, half_thick = ( - 0.5 * ic.vertical_width, - 0.5 * ic.horizontal_width, - 0.5 * ic.thickness, + 0.5 * self._vertical_width, + 0.5 * self._horizontal_width, + 0.5 * self._thickness, ) xml_str = ( ' ' @@ -826,16 +833,16 @@ def createSlabGeometry(ic, wsNCPM): CreateSampleShape(wsNCPM, xml_str) - def calcMSCorrectionSampleProperties(ic, meanWidths, meanIntensityRatios): - masses = ic.masses.flatten() + def calcMSCorrectionSampleProperties(self, meanWidths, meanIntensityRatios): + masses = self._masses.flatten() # If Backsscattering mode and H is present in the sample, add H to MS properties - if ic.modeRunning == "BACKWARD": - if ic.HToMassIdxRatio is not None: # If H is present, ratio is a number + if self._mode_running == "BACKWARD": + if self._h_ratio is not None: # If H is present, ratio is a number masses = np.append(masses, 1.0079) meanWidths = np.append(meanWidths, 5.0) - HIntensity = ic.HToMassIdxRatio * meanIntensityRatios[ic.massIdx] + HIntensity = self._h_ratio * meanIntensityRatios[np.argmin(self._masses)] meanIntensityRatios = np.append(meanIntensityRatios, HIntensity) meanIntensityRatios /= np.sum(meanIntensityRatios) @@ -848,7 +855,7 @@ def calcMSCorrectionSampleProperties(ic, meanWidths, meanIntensityRatios): return sampleProperties - def createMulScatWorkspaces(ic, ws, sampleProperties): + def createMulScatWorkspaces(self, ws, sampleProperties): """Uses the Mantid algorithm for the MS correction to create two Workspaces _TotScattering and _MulScattering""" print("\nEvaluating the Multiple Scattering Correction...\n") @@ -860,7 +867,7 @@ def createMulScatWorkspaces(ic, ws, sampleProperties): dens, trans = VesuvioThickness( Masses=MS_masses, Amplitudes=MS_amplitudes, - TransmissionGuess=ic.transmission_guess, + TransmissionGuess=self._transmission_guess, Thickness=0.1, ) @@ -870,8 +877,8 @@ def createMulScatWorkspaces(ic, ws, sampleProperties): SampleDensity=dens.cell(9, 1), AtomicProperties=sampleProperties, BeamRadius=2.5, - NumScatters=ic.multiple_scattering_order, - NumEventsPerRun=int(ic.number_of_events), + NumScatters=self._multiple_scattering_order, + NumEventsPerRun=int(self._number_of_events), ) data_normalisation = Integration(ws) @@ -897,12 +904,12 @@ def createMulScatWorkspaces(ic, ws, sampleProperties): return mtd[ws.name() + "_MulScattering"] - def createWorkspacesForGammaCorrection(ic, meanWidths, meanIntensityRatios, wsNCPM): + def createWorkspacesForGammaCorrection(self, meanWidths, meanIntensityRatios, wsNCPM): """Creates _gamma_background correction workspace to be subtracted from the main workspace""" inputWS = wsNCPM.name() - profiles = calcGammaCorrectionProfiles(ic.masses, meanWidths, meanIntensityRatios) + profiles = self.calcGammaCorrectionProfiles(meanWidths, meanIntensityRatios) # Approach below not currently suitable for current versions of Mantid, but will be in the future # background, corrected = VesuvioCalculateGammaBackground(InputWorkspace=inputWS, ComptonFunction=profiles) @@ -933,8 +940,8 @@ def createWorkspacesForGammaCorrection(ic, meanWidths, meanIntensityRatios, wsNC return mtd[inputWS + "_Gamma_Background"] - def calcGammaCorrectionProfiles(masses, meanWidths, meanIntensityRatios): - masses = masses.flatten() + def calcGammaCorrectionProfiles(self, meanWidths, meanIntensityRatios): + masses = self._masses.flatten() profiles = "" for mass, width, intensity in zip(masses, meanWidths, meanIntensityRatios): profiles += ( @@ -950,99 +957,96 @@ def calcGammaCorrectionProfiles(masses, meanWidths, meanIntensityRatios): return profiles - class resultsObject: + def _create_results_mehtods(self): """Used to collect results from workspaces and store them in .npz files for testing.""" - def __init__(self, ic): - allIterNcp = [] - allFitWs = [] - allTotNcp = [] - allBestPar = [] - allMeanWidhts = [] - allMeanIntensities = [] - allStdWidths = [] - allStdIntensities = [] - j = 0 - while True: - try: - wsIterName = ic.name + str(j) - - # Extract ws that were fitted - ws = mtd[wsIterName] - allFitWs.append(ws.extractY()) - - # Extract total ncp - totNcpWs = mtd[wsIterName + "_TOF_Fitted_Profiles"] - allTotNcp.append(totNcpWs.extractY()) - - # Extract best fit parameters - fitParTable = mtd[wsIterName + "_Best_Fit_NCP_Parameters"] - bestFitPars = [] - for key in fitParTable.keys(): - bestFitPars.append(fitParTable.column(key)) - allBestPar.append(np.array(bestFitPars).T) - - # Extract individual ncp - allNCP = [] - i = 0 - while True: # By default, looks for all ncp ws until it breaks - try: - ncpWsToAppend = mtd[ - wsIterName + "_TOF_Fitted_Profile_" + str(i) - ] - allNCP.append(ncpWsToAppend.extractY()) - i += 1 - except KeyError: - break - allNCP = switchFirstTwoAxis(np.array(allNCP)) - allIterNcp.append(allNCP) - - # Extract Mean and Std Widths, Intensities - meansTable = mtd[wsIterName + "_Mean_Widths_And_Intensities"] - allMeanWidhts.append(meansTable.column("Mean Widths")) - allStdWidths.append(meansTable.column("Std Widths")) - allMeanIntensities.append(meansTable.column("Mean Intensities")) - allStdIntensities.append(meansTable.column("Std Intensities")) - - j += 1 - except KeyError: - break - - self.all_fit_workspaces = np.array(allFitWs) - self.all_spec_best_par_chi_nit = np.array(allBestPar) - self.all_tot_ncp = np.array(allTotNcp) - self.all_ncp_for_each_mass = np.array(allIterNcp) - - self.all_mean_widths = np.array(allMeanWidhts) - self.all_mean_intensities = np.array(allMeanIntensities) - self.all_std_widths = np.array(allStdWidths) - self.all_std_intensities = np.array(allStdIntensities) - - # Pass all attributes of ic into attributes to be used whithin this object - self.maskedDetectorIdx = ic.maskedDetectorIdx - self.masses = ic.masses - self.noOfMasses = ic.noOfMasses - self.resultsSavePath = ic.resultsSavePath - - def save(self): - """Saves all of the arrays stored in this object""" - - # TODO: Take out nans next time when running original results - # Because original results were recently saved with nans, mask spectra with nans - self.all_spec_best_par_chi_nit[:, self.maskedDetectorIdx, :] = np.nan - self.all_ncp_for_each_mass[:, self.maskedDetectorIdx, :, :] = np.nan - self.all_tot_ncp[:, self.maskedDetectorIdx, :] = np.nan - - savePath = self.resultsSavePath - np.savez( - savePath, - all_fit_workspaces=self.all_fit_workspaces, - all_spec_best_par_chi_nit=self.all_spec_best_par_chi_nit, - all_mean_widths=self.all_mean_widths, - all_mean_intensities=self.all_mean_intensities, - all_std_widths=self.all_std_widths, - all_std_intensities=self.all_std_intensities, - all_tot_ncp=self.all_tot_ncp, - all_ncp_for_each_mass=self.all_ncp_for_each_mass, - ) + allIterNcp = [] + allFitWs = [] + allTotNcp = [] + allBestPar = [] + allMeanWidhts = [] + allMeanIntensities = [] + allStdWidths = [] + allStdIntensities = [] + j = 0 + while True: + try: + wsIterName = self._name + str(j) + + # Extract ws that were fitted + ws = mtd[wsIterName] + allFitWs.append(ws.extractY()) + + # Extract total ncp + totNcpWs = mtd[wsIterName + "_TOF_Fitted_Profiles"] + allTotNcp.append(totNcpWs.extractY()) + + # Extract best fit parameters + fitParTable = mtd[wsIterName + "_Best_Fit_NCP_Parameters"] + bestFitPars = [] + for key in fitParTable.keys(): + bestFitPars.append(fitParTable.column(key)) + allBestPar.append(np.array(bestFitPars).T) + + # Extract individual ncp + allNCP = [] + i = 0 + while True: # By default, looks for all ncp ws until it breaks + try: + ncpWsToAppend = mtd[ + wsIterName + "_TOF_Fitted_Profile_" + str(i) + ] + allNCP.append(ncpWsToAppend.extractY()) + i += 1 + except KeyError: + break + allNCP = switchFirstTwoAxis(np.array(allNCP)) + allIterNcp.append(allNCP) + + # Extract Mean and Std Widths, Intensities + meansTable = mtd[wsIterName + "_Mean_Widths_And_Intensities"] + allMeanWidhts.append(meansTable.column("Mean Widths")) + allStdWidths.append(meansTable.column("Std Widths")) + allMeanIntensities.append(meansTable.column("Mean Intensities")) + allStdIntensities.append(meansTable.column("Std Intensities")) + + j += 1 + except KeyError: + break + + self.all_fit_workspaces = np.array(allFitWs) + self.all_spec_best_par_chi_nit = np.array(allBestPar) + self.all_tot_ncp = np.array(allTotNcp) + self.all_ncp_for_each_mass = np.array(allIterNcp) + + self.all_mean_widths = np.array(allMeanWidhts) + self.all_mean_intensities = np.array(allMeanIntensities) + self.all_std_widths = np.array(allStdWidths) + self.all_std_intensities = np.array(allStdIntensities) + + def save_results(self): + """Saves all of the arrays stored in this object""" + + maskedDetectorIdx = np.array(self._mask_spectra) - self._firstSpec + + # TODO: Take out nans next time when running original results + # Because original results were recently saved with nans, mask spectra with nans + self.all_spec_best_par_chi_nit[:, maskedDetectorIdx, :] = np.nan + self.all_ncp_for_each_mass[:, maskedDetectorIdx, :, :] = np.nan + self.all_tot_ncp[:, maskedDetectorIdx, :] = np.nan + + if not self._save_results_path: + return + + np.savez( + self._save_results_path, + all_fit_workspaces=self.all_fit_workspaces, + all_spec_best_par_chi_nit=self.all_spec_best_par_chi_nit, + all_mean_widths=self.all_mean_widths, + all_mean_intensities=self.all_mean_intensities, + all_std_widths=self.all_std_widths, + all_std_intensities=self.all_std_intensities, + all_tot_ncp=self.all_tot_ncp, + all_ncp_for_each_mass=self.all_ncp_for_each_mass, + ) diff --git a/src/mvesuvio/oop/run_routine.py b/src/mvesuvio/oop/run_routine.py index c0969013..c33be594 100644 --- a/src/mvesuvio/oop/run_routine.py +++ b/src/mvesuvio/oop/run_routine.py @@ -27,6 +27,7 @@ def run_analysis(): number_of_iterations=0, mask_spectra=[173, 174, 179], multiple_scattering_correction=False, + vertical_width=0.1, horizontal_width=0.1, thickness=0.001, gamma_correction=False) H = NeutronComptonProfile('H', mass=1.0079, intensity=1, width=4.7, center=0, From 9ab2044dd8c46c9506779d1e5c729beff937d0b4 Mon Sep 17 00:00:00 2001 From: GuiMacielPereira Date: Thu, 1 Aug 2024 16:51:10 +0100 Subject: [PATCH 06/25] Fix system tests to run new analysis Added a system test for the new AnalysisReduction object. All system tests are passing. The system tests have a coverage of 95% of analysis_reduction.py and are very stringent, so it is fairly safe to say that this transition to OOP has not introduced any new bugs. --- src/mvesuvio/oop/AnalysisRoutine.py | 122 ++++++++------ src/mvesuvio/oop/analysis_helpers.py | 3 +- src/mvesuvio/oop/run_routine.py | 12 +- tests/system/analysis/test_new_analysis.py | 183 +++++++++++++++++++++ 4 files changed, 261 insertions(+), 59 deletions(-) create mode 100644 tests/system/analysis/test_new_analysis.py diff --git a/src/mvesuvio/oop/AnalysisRoutine.py b/src/mvesuvio/oop/AnalysisRoutine.py index c72a75dd..349a79a9 100644 --- a/src/mvesuvio/oop/AnalysisRoutine.py +++ b/src/mvesuvio/oop/AnalysisRoutine.py @@ -15,10 +15,10 @@ class AnalysisRoutine: def __init__(self, workspace, ip_file, number_of_iterations, mask_spectra, multiple_scattering_correction, vertical_width, horizontal_width, thickness, - gamma_correction, transmission_guess=None, multiple_scattering_order=None, + gamma_correction, mode_running, transmission_guess=None, multiple_scattering_order=None, number_of_events=None, results_path=None): - self._workspace_to_fit = workspace + self._workspace_being_fit = workspace self._name = workspace.name() self._ip_file = ip_file self._number_of_iterations = number_of_iterations @@ -32,6 +32,7 @@ def __init__(self, workspace, ip_file, number_of_iterations, mask_spectra, self._vertical_width = vertical_width self._horizontal_width = horizontal_width self._thickness = thickness + self._mode_running = mode_running self._multiple_scattering_correction = multiple_scattering_correction self._gamma_correction = gamma_correction @@ -39,11 +40,11 @@ def __init__(self, workspace, ip_file, number_of_iterations, mask_spectra, self._save_results_path = results_path self._h_ratio = None - self._constraints = [] + self._constraints = () self._profiles = {} # Only used for system tests, remove once tests are updated - self._run_hist_data = False #True + self._run_hist_data = True self._run_norm_voigt = False # Links to another AnalysisRoutine object: @@ -91,15 +92,9 @@ def profiles(self, incoming_profiles): self._profiles = {**self._profiles, **common_keys_profiles} - def _preprocess(self): + def _set_const_methods(self): # Set up variables used during fitting self._masses = np.array([p.mass for p in self._profiles.values()]) - self._dataX, self._dataY, self._dataE = extractWS(self._workspace_to_fit) - resolutionPars, instrPars, kinematicArrays, ySpacesForEachMass = self.prepareFitArgs() - self._resolution_params = resolutionPars - self._instrument_params = instrPars - self._kinematic_arrays = kinematicArrays - self._y_space_arrays = ySpacesForEachMass self._initial_fit_parameters = [] self._bounds = [] @@ -111,19 +106,33 @@ def _preprocess(self): self._bounds.append(p._width_bounds) self._bounds.append(p._center_bounds) - # self._intensities = np.array([p.intensity for p in self._profiles.values()])[:, np.newaxis] - # self._widths = np.array([p.width for p in self._profiles.values()])[:, np.newaxis] - # self._centers = np.array([p.center for p in self._profiles.values()])[:, np.newaxis] + self._fig_save_path = None + + + def _update_workspace_data(self): + self._dataX, self._dataY, self._dataE = extractWS(self._workspace_being_fit) + + if self._run_hist_data: # Converts point data from workspaces to histogram data + self._dataY, self._dataX, self._dataE = histToPointData(self._dataY, self._dataX, self._dataE) + + self._set_up_kinematic_arrays() + self._fit_parameters = np.zeros((len(self._dataY), 3 * len(self._profiles) + 3)) - self._fig_save_path = None - + + def _set_up_kinematic_arrays(self): + resolutionPars, instrPars, kinematicArrays, ySpacesForEachMass = self.prepareFitArgs() + self._resolution_params = resolutionPars + self._instrument_params = instrPars + self._kinematic_arrays = kinematicArrays + self._y_space_arrays = ySpacesForEachMass + def run(self): assert len(self.profiles) > 0, "Add profiles before atempting to run the routine!" - self._preprocess() + self._set_const_methods() self.createTableInitialParameters() @@ -133,21 +142,23 @@ def run(self): # InputWorkspace=ic.sampleWS, OutputWorkspace=initialWs.name() # ) - self._workspace_to_fit = CloneWorkspace( - InputWorkspace=self._workspace_to_fit, + CloneWorkspace( + InputWorkspace=self._workspace_being_fit, OutputWorkspace=self._name + "0" ) for iteration in range(self._number_of_iterations + 1): # Workspace from previous iteration - wsToBeFitted = mtd[self._name + str(iteration)] + self._workspace_being_fit = mtd[self._name + str(iteration)] + + self._update_workspace_data() - ncpTotal = self.fitNcpToWorkspace(wsToBeFitted) + ncpTotal = self.fitNcpToWorkspace() - mWidths, stdWidths, mIntRatios, stdIntRatios = self.extractMeans(wsToBeFitted.name()) + mWidths, stdWidths, mIntRatios, stdIntRatios = self.extractMeans(self._workspace_being_fit.name()) self.createMeansAndStdTableWS( - wsToBeFitted.name(), mWidths, stdWidths, mIntRatios, stdIntRatios + self._workspace_being_fit.name(), mWidths, stdWidths, mIntRatios, stdIntRatios ) # When last iteration, skip MS and GC @@ -180,10 +191,9 @@ def run(self): InputWorkspace="tmpNameWs", OutputWorkspace=self._name + str(iteration + 1) ) - wsFinal = mtd[self._name + str(self._number_of_iterations)] - self._create_results_mehtods() + self._set_up_results_mehtods() self.save_results() - return wsFinal, self + return self @staticmethod @@ -234,28 +244,27 @@ def createTableInitialParameters(self): print("\n") - def fitNcpToWorkspace(self, ws): + def fitNcpToWorkspace(self): """ Performs the fit of ncp to the workspace. Firtly the arrays required for the fit are prepared and then the fit is performed iteratively on a spectrum by spectrum basis. """ - if self._run_hist_data: # Converts point data from workspaces to histogram data - self._dataY, self._dataX, self._dataE = histToPointData(self._dataY, self._dataX, self._dataE) - print("\nFitting NCP:\n") arrFitPars = self.fitNcpToArray() - self.createTableWSForFitPars(ws.name(), len(self._profiles), arrFitPars) + self.createTableWSForFitPars(len(self._profiles), arrFitPars) arrBestFitPars = arrFitPars[:, 1:-2] ncpForEachMass, ncpTotal = self.calculateNcpArr(arrBestFitPars) - ncpSumWSs = self.createNcpWorkspaces(ncpForEachMass, ncpTotal, ws) + ncpSumWSs = self.createNcpWorkspaces(ncpForEachMass, ncpTotal) - wsDataSum = SumSpectra(InputWorkspace=ws, OutputWorkspace=ws.name() + "_Sum") + wsDataSum = SumSpectra( + InputWorkspace=self._workspace_being_fit.name(), + OutputWorkspace=self._workspace_being_fit.name() + "_Sum") self.plotSumNCPFits(wsDataSum, *ncpSumWSs) return ncpTotal @@ -378,9 +387,9 @@ def fitNcpToArray(self): return self._fit_parameters - def createTableWSForFitPars(self, wsName, noOfMasses, arrFitPars): + def createTableWSForFitPars(self, noOfMasses, arrFitPars): tableWS = CreateEmptyTableWorkspace( - OutputWorkspace=wsName + "_Best_Fit_NCP_Parameters" + OutputWorkspace=self._workspace_being_fit.name()+ "_Best_Fit_NCP_Parameters" ) tableWS.setTitle("SCIPY Fit") tableWS.addColumn(type="float", name="Spec Idx") @@ -422,25 +431,27 @@ def calculateNcpRow(self, initPars, row): return ncpForEachMass - def createNcpWorkspaces(self, ncpForEachMass, ncpTotal, ws): + def createNcpWorkspaces(self, ncpForEachMass, ncpTotal): """Creates workspaces from ncp array data""" # Need to rearrage array of yspaces into seperate arrays for each mass ncpForEachMass = switchFirstTwoAxis(ncpForEachMass) # Use ws dataX to match with histogram data - dataX = ws.extractX()[ - :, : ncpTotal.shape[1] - ] # Make dataX match ncp shape automatically + dataX = self._dataX + # dataX = ws.extractX()[ + # :, : ncpTotal.shape[1] + # ] # Make dataX match ncp shape automatically assert ( ncpTotal.shape == dataX.shape ), "DataX and DataY in ws need to be the same shape." - ncpTotWS = CloneWorkspace(InputWorkspace=ws.name(), OutputWorkspace=ws.name() + "_TOF_Fitted_Profiles") - passDataIntoWS(dataX, ncpTotal, np.zeros_like(dataX), ncpTotWS) - # ncpTotWS = createWS( - # dataX, ncpTotal, np.zeros(dataX.shape), ws.name() + "_TOF_Fitted_Profiles" - # ) + # ncpTotWS = CloneWorkspace(InputWorkspace=ws.name(), OutputWorkspace=ws.name() + "_TOF_Fitted_Profiles") + # passDataIntoWS(dataX, ncpTotal, np.zeros_like(dataX), ncpTotWS) + ncpTotWS = createWS( + dataX, ncpTotal, np.zeros(dataX.shape), self._workspace_being_fit.name() + "_TOF_Fitted_Profiles", + parentWorkspace=self._workspace_being_fit + ) # MaskDetectors(Workspace=ncpTotWS, WorkspaceIndexList=ic.maskedDetectorIdx) MaskDetectors(Workspace=ncpTotWS, SpectraList=self._mask_spectra) wsTotNCPSum = SumSpectra( @@ -450,14 +461,15 @@ def createNcpWorkspaces(self, ncpForEachMass, ncpTotal, ws): # Individual ncp workspaces wsMNCPSum = [] for i, ncp_m in enumerate(ncpForEachMass): - ncpMWS = CloneWorkspace(InputWorkspace=ws.name(), OutputWorkspace=ws.name()+"_TOF_Fitted_Profile_" + str(i)) - passDataIntoWS(dataX, ncp_m, np.zeros_like(dataX), ncpMWS) - # ncpMWS = createWS( - # dataX, - # ncp_m, - # np.zeros(dataX.shape), - # ws.name() + "_TOF_Fitted_Profile_" + str(i), - # ) + # ncpMWS = CloneWorkspace(InputWorkspace=ws.name(), OutputWorkspace=ws.name()+"_TOF_Fitted_Profile_" + str(i)) + # passDataIntoWS(dataX, ncp_m, np.zeros_like(dataX), ncpMWS) + ncpMWS = createWS( + dataX, + ncp_m, + np.zeros(dataX.shape), + self._workspace_being_fit.name() + "_TOF_Fitted_Profile_" + str(i), + parentWorkspace=self._workspace_being_fit + ) MaskDetectors(Workspace=ncpMWS, SpectraList=self._mask_spectra) wsNCPSum = SumSpectra( InputWorkspace=ncpMWS, OutputWorkspace=ncpMWS.name() + "_Sum" @@ -623,7 +635,7 @@ def fitNcpToSingleSpec(self, row): ) fitPars = result["x"] - noDegreesOfFreedom = len(self._dataY) - len(fitPars) + noDegreesOfFreedom = len(self._dataY[row]) - len(fitPars) specFitPars = np.append(self._instrument_params[row, 0], fitPars) return np.append(specFitPars, [result["fun"] / noDegreesOfFreedom, result["nit"]]) @@ -957,9 +969,11 @@ def calcGammaCorrectionProfiles(self, meanWidths, meanIntensityRatios): return profiles - def _create_results_mehtods(self): + def _set_up_results_mehtods(self): """Used to collect results from workspaces and store them in .npz files for testing.""" + self.wsFinal = mtd[self._name + str(self._number_of_iterations)] + allIterNcp = [] allFitWs = [] allTotNcp = [] diff --git a/src/mvesuvio/oop/analysis_helpers.py b/src/mvesuvio/oop/analysis_helpers.py index ace64b4a..32ac2d46 100644 --- a/src/mvesuvio/oop/analysis_helpers.py +++ b/src/mvesuvio/oop/analysis_helpers.py @@ -180,12 +180,13 @@ def switchFirstTwoAxis(A): return np.stack(np.split(A, len(A), axis=0), axis=2)[0] -def createWS(dataX, dataY, dataE, wsName): +def createWS(dataX, dataY, dataE, wsName, parentWorkspace=None): ws = CreateWorkspace( DataX=dataX.flatten(), DataY=dataY.flatten(), DataE=dataE.flatten(), Nspec=len(dataY), OutputWorkspace=wsName, + ParentWorkspace=parentWorkspace ) return ws diff --git a/src/mvesuvio/oop/run_routine.py b/src/mvesuvio/oop/run_routine.py index c33be594..7ac7de5e 100644 --- a/src/mvesuvio/oop/run_routine.py +++ b/src/mvesuvio/oop/run_routine.py @@ -10,7 +10,7 @@ def run_analysis(): ws = loadRawAndEmptyWsFromUserPath( userWsRawPath='/home/ljg28444/Documents/user_0/some_new_experiment/some_new_exp/input_ws/some_new_exp_raw_forward.nxs', userWsEmptyPath='/home/ljg28444/Documents/user_0/some_new_experiment/some_new_exp/input_ws/some_new_exp_empty_forward.nxs', - tofBinning = "275.,1.,420", + tofBinning = "110.,1.,420", name='exp', scaleRaw=1, scaleEmpty=1, @@ -24,11 +24,15 @@ def run_analysis(): AR = AnalysisRoutine(cropedWs, ip_file='/home/ljg28444/.mvesuvio/ip_files/ip2018_3.par', - number_of_iterations=0, + number_of_iterations=1, mask_spectra=[173, 174, 179], - multiple_scattering_correction=False, + multiple_scattering_correction=True, vertical_width=0.1, horizontal_width=0.1, thickness=0.001, - gamma_correction=False) + gamma_correction=True, + mode_running='FORWARD', + transmission_guess=0.853, + multiple_scattering_order=2, + number_of_events=1.0e5) H = NeutronComptonProfile('H', mass=1.0079, intensity=1, width=4.7, center=0, intensity_bounds=[0, np.nan], width_bounds=[3, 6], center_bounds=[-3, 1]) diff --git a/tests/system/analysis/test_new_analysis.py b/tests/system/analysis/test_new_analysis.py new file mode 100644 index 00000000..8cd0c407 --- /dev/null +++ b/tests/system/analysis/test_new_analysis.py @@ -0,0 +1,183 @@ +import unittest +import numpy as np +import numpy.testing as nptest +from pathlib import Path +from mvesuvio.util import handle_config +ipFilesPath = Path(handle_config.read_config_var("caching.ipfolder")) + +from mvesuvio.oop.AnalysisRoutine import AnalysisRoutine +from mvesuvio.oop.NeutronComptonProfile import NeutronComptonProfile +from mvesuvio.oop.analysis_helpers import loadRawAndEmptyWsFromUserPath, cropAndMaskWorkspace + +class AnalysisRunner: + _benchmarkResults = None + _currentResults = None + + @classmethod + def get_benchmark_result(cls): + if not AnalysisRunner._benchmarkResults: + cls._load_benchmark_results() + return AnalysisRunner._benchmarkResults + + @classmethod + def get_current_result(cls): + if not AnalysisRunner._currentResults: + cls._run() + return AnalysisRunner._currentResults + + @classmethod + def _run(cls): + + ws = loadRawAndEmptyWsFromUserPath( + userWsRawPath=str(Path(__file__).absolute().parent.parent.parent/"data"/"analysis"/"inputs"/"sample_test"/"input_ws"/"sample_test_raw_forward.nxs" ), + # userWsRawPath='/home/ljg28444/Documents/user_0/some_new_experiment/some_new_exp/input_ws/some_new_exp_raw_forward.nxs', + userWsEmptyPath=str(Path(__file__).absolute().parent.parent.parent/"data"/"analysis"/"inputs"/"sample_test"/"input_ws"/"sample_test_empty_forward.nxs" ), + # userWsEmptyPath='/home/ljg28444/Documents/user_0/some_new_experiment/some_new_exp/input_ws/some_new_exp_empty_forward.nxs', + tofBinning = "110.,1.,430", + name='exp', + scaleRaw=1, + scaleEmpty=1, + subEmptyFromRaw=False + ) + cropedWs = cropAndMaskWorkspace(ws, firstSpec=144, lastSpec=182, + maskedDetectors=[173, 174, 179], + maskTOFRange=None) + + AR = AnalysisRoutine(cropedWs, + ip_file='/home/ljg28444/.mvesuvio/ip_files/ip2018_3.par', + number_of_iterations=3, + mask_spectra=[173, 174, 179], + multiple_scattering_correction=True, + vertical_width=0.1, horizontal_width=0.1, thickness=0.001, + transmission_guess=0.8537, + multiple_scattering_order=2, + number_of_events=1.0e5, + gamma_correction=True, + mode_running='FORWARD') + + H = NeutronComptonProfile('H', mass=1.0079, intensity=1, width=4.7, center=0, + intensity_bounds=[0, np.nan], width_bounds=[3, 6], center_bounds=[-3, 1]) + C = NeutronComptonProfile('C', mass=12, intensity=1, width=12.71, center=0, + intensity_bounds=[0, np.nan], width_bounds=[12.71, 12.71], center_bounds=[-3, 1]) + S = NeutronComptonProfile('S', mass=16, intensity=1, width=8.76, center=0, + intensity_bounds=[0, np.nan], width_bounds=[8.76, 8.76], center_bounds=[-3, 1]) + Co = NeutronComptonProfile('Co', mass=27, intensity=1, width=13.897, center=0, + intensity_bounds=[0, np.nan], width_bounds=[13.897, 13.897], center_bounds=[-3, 1]) + + AR.add_profiles(H, C, S, Co) + AnalysisRunner._currentResults = AR.run() + + np.set_printoptions(suppress=True, precision=8, linewidth=150) + + + @classmethod + def _load_benchmark_results(cls): + benchmarkPath = Path(__file__).absolute().parent.parent.parent / "data" / "analysis" / "benchmark" + benchmarkResults = np.load( + str(benchmarkPath / "stored_spec_144-182_iter_3_GC_MS.npz") + ) + AnalysisRunner._benchmarkResults = benchmarkResults + + +class TestFitParameters(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.benchmarkResults = AnalysisRunner.get_benchmark_result() + cls.currentResults = AnalysisRunner.get_current_result() + + def setUp(self): + oriPars = self.benchmarkResults["all_spec_best_par_chi_nit"] + self.orispec = oriPars[:, :, 0] + self.orichi2 = oriPars[:, :, -2] + self.orinit = oriPars[:, :, -1] + self.orimainPars = oriPars[:, :, 1:-2] + self.oriintensities = self.orimainPars[:, :, 0::3] + self.oriwidths = self.orimainPars[:, :, 1::3] + self.oricenters = self.orimainPars[:, :, 2::3] + + optPars = self.currentResults.all_spec_best_par_chi_nit + self.optspec = optPars[:, :, 0] + self.optchi2 = optPars[:, :, -2] + self.optnit = optPars[:, :, -1] + self.optmainPars = optPars[:, :, 1:-2] + self.optintensities = self.optmainPars[:, :, 0::3] + self.optwidths = self.optmainPars[:, :, 1::3] + self.optcenters = self.optmainPars[:, :, 2::3] + + def test_chi2(self): + nptest.assert_almost_equal(self.orichi2, self.optchi2, decimal=6) + + def test_nit(self): + nptest.assert_almost_equal(self.orinit, self.optnit, decimal=-2) + + def test_intensities(self): + nptest.assert_almost_equal(self.oriintensities, self.optintensities, decimal=2) + + def test_widths(self): + nptest.assert_almost_equal(self.oriwidths, self.optwidths, decimal=2) + + def test_centers(self): + nptest.assert_almost_equal(self.oricenters, self.optcenters, decimal=1) + + +class TestNcp(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.benchmarkResults = AnalysisRunner.get_benchmark_result() + cls.currentResults = AnalysisRunner.get_current_result() + + def setUp(self): + self.orincp = self.benchmarkResults["all_tot_ncp"][:, :, :-1] + + self.optncp = self.currentResults.all_tot_ncp + + def test_ncp(self): + correctNansOri = np.where( + (self.orincp == 0) & np.isnan(self.optncp), np.nan, self.orincp + ) + nptest.assert_almost_equal(correctNansOri, self.optncp, decimal=4) + + +class TestMeanWidths(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.benchmarkResults = AnalysisRunner.get_benchmark_result() + cls.currentResults = AnalysisRunner.get_current_result() + + def setUp(self): + self.orimeanwidths = self.benchmarkResults["all_mean_widths"] + self.optmeanwidths = self.currentResults.all_mean_widths + + def test_widths(self): + nptest.assert_almost_equal(self.orimeanwidths, self.optmeanwidths, decimal=5) + + +class TestMeanIntensities(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.benchmarkResults = AnalysisRunner.get_benchmark_result() + cls.currentResults = AnalysisRunner.get_current_result() + + def setUp(self): + self.orimeanintensities = self.benchmarkResults["all_mean_intensities"] + self.optmeanintensities = self.currentResults.all_mean_intensities + + def test_intensities(self): + nptest.assert_almost_equal(self.orimeanintensities, self.optmeanintensities, decimal=6) + + +class TestFitWorkspaces(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.benchmarkResults = AnalysisRunner.get_benchmark_result() + cls.currentResults = AnalysisRunner.get_current_result() + + def setUp(self): + self.oriws = self.benchmarkResults["all_fit_workspaces"] + self.optws = self.currentResults.all_fit_workspaces + + def test_ws(self): + nptest.assert_almost_equal(self.oriws, self.optws, decimal=6) + +if __name__ == "__main__": + unittest.main() From a4e2fa89bde59aea900d06ce8986c90cd9e4a48f Mon Sep 17 00:00:00 2001 From: GuiMacielPereira Date: Fri, 2 Aug 2024 16:23:40 +0100 Subject: [PATCH 07/25] Clean the fitting of profiles Instead of fit the workspace in one go, and then create a table and the final workspaces, it makes more sense to populate the table and the final worksapaces row by row as the spectra are being fit row by row. --- src/mvesuvio/oop/AnalysisRoutine.py | 200 +++++++++++++++------------- 1 file changed, 111 insertions(+), 89 deletions(-) diff --git a/src/mvesuvio/oop/AnalysisRoutine.py b/src/mvesuvio/oop/AnalysisRoutine.py index 349a79a9..1ba1dc79 100644 --- a/src/mvesuvio/oop/AnalysisRoutine.py +++ b/src/mvesuvio/oop/AnalysisRoutine.py @@ -5,7 +5,8 @@ from mantid.simpleapi import mtd, CreateEmptyTableWorkspace, MaskDetectors, SumSpectra, \ CloneWorkspace, DeleteWorkspace, VesuvioCalculateGammaBackground, \ VesuvioCalculateMS, Scale, RenameWorkspace, Minus, CreateSampleShape, \ - VesuvioThickness, Integration, Divide, Multiply, DeleteWorkspaces + VesuvioThickness, Integration, Divide, Multiply, DeleteWorkspaces, \ + CreateWorkspace from mvesuvio.analysis_fitting import passDataIntoWS, replaceZerosWithNCP import numpy as np from scipy import optimize @@ -18,7 +19,6 @@ def __init__(self, workspace, ip_file, number_of_iterations, mask_spectra, gamma_correction, mode_running, transmission_guess=None, multiple_scattering_order=None, number_of_events=None, results_path=None): - self._workspace_being_fit = workspace self._name = workspace.name() self._ip_file = ip_file self._number_of_iterations = number_of_iterations @@ -39,6 +39,12 @@ def __init__(self, workspace, ip_file, number_of_iterations, mask_spectra, self._save_results_path = results_path + # Variables changing during fit + self._workspace_being_fit = workspace + self._row_being_fit = 0 + self._table_fit_results = None + self._fit_profiles_workspaces = {} + self._h_ratio = None self._constraints = () self._profiles = {} @@ -96,15 +102,6 @@ def _set_const_methods(self): # Set up variables used during fitting self._masses = np.array([p.mass for p in self._profiles.values()]) - self._initial_fit_parameters = [] - self._bounds = [] - for p in self._profiles.values(): - self._initial_fit_parameters.append(p.intensity) - self._initial_fit_parameters.append(p.width) - self._initial_fit_parameters.append(p.center) - self._bounds.append(p._intensity_bounds) - self._bounds.append(p._width_bounds) - self._bounds.append(p._center_bounds) self._fig_save_path = None @@ -119,6 +116,40 @@ def _update_workspace_data(self): self._fit_parameters = np.zeros((len(self._dataY), 3 * len(self._profiles) + 3)) + self._row_being_fit = 0 + + #Initialise Table for fit parameters + table = CreateEmptyTableWorkspace( + OutputWorkspace=self._workspace_being_fit.name()+ "_fit_results" + ) + table.setTitle("SciPy Fit Parameters") + table.addColumn(type="float", name="Spectrum") + for p in self._profiles.values(): + table.addColumn(type="float", name=f"{p.label} Intensity") + table.addColumn(type="float", name=f"{p.label} Width") + table.addColumn(type="float", name=f"{p.label} Center ") + table.addColumn(type="float", name="Normalised Chi2") + table.addColumn(type="float", name="Number of Iteraions") + + self._table_fit_results = table + + #Initialise workspaces for fitted ncp + self._fit_profiles_workspaces = {} + for element, p in self._profiles.items(): + self._fit_profiles_workspaces[element] = self._create_emtpy_ncp_workspace(f'_{element}_ncp') + self._fit_profiles_workspaces['total'] = self._create_emtpy_ncp_workspace(f'_total_ncp') + + + def _create_emtpy_ncp_workspace(self, suffix): + return CreateWorkspace( + DataX=np.zeros(self._dataX.size), + DataY=np.zeros(self._dataY.size), + DataE=np.zeros(self._dataE.size), + Nspec=self._workspace_being_fit.getNumberHistograms(), + OutputWorkspace=self._workspace_being_fit.name()+suffix, + ParentWorkspace=self._workspace_being_fit + ) + def _set_up_kinematic_arrays(self): resolutionPars, instrPars, kinematicArrays, ySpacesForEachMass = self.prepareFitArgs() @@ -153,7 +184,7 @@ def run(self): self._update_workspace_data() - ncpTotal = self.fitNcpToWorkspace() + self.fitNcpToWorkspace() mWidths, stdWidths, mIntRatios, stdIntRatios = self.extractMeans(self._workspace_being_fit.name()) @@ -170,7 +201,7 @@ def run(self): # Replace zero columns (bins) with ncp total fit # If ws has no zero column, then remains unchanged if iteration == 0: - wsNCPM = replaceZerosWithNCP(mtd[self._name], ncpTotal) + wsNCPM = replaceZerosWithNCP(mtd[self._name], self._fit_profiles_workspaces['total'].extractY()) CloneWorkspace(InputWorkspace=self._name, OutputWorkspace="tmpNameWs") @@ -253,20 +284,17 @@ def fitNcpToWorkspace(self): print("\nFitting NCP:\n") - arrFitPars = self.fitNcpToArray() + self.fitNcpToArray() - self.createTableWSForFitPars(len(self._profiles), arrFitPars) + # self.createTableWSForFitPars(len(self._profiles), arrFitPars) - arrBestFitPars = arrFitPars[:, 1:-2] - - ncpForEachMass, ncpTotal = self.calculateNcpArr(arrBestFitPars) - ncpSumWSs = self.createNcpWorkspaces(ncpForEachMass, ncpTotal) + # ncpForEachMass, ncpTotal = self.calculateNcpArr(arrBestFitPars) + # ncpSumWSs = self.createNcpWorkspaces(ncpForEachMass, ncpTotal) wsDataSum = SumSpectra( InputWorkspace=self._workspace_being_fit.name(), OutputWorkspace=self._workspace_being_fit.name() + "_Sum") - self.plotSumNCPFits(wsDataSum, *ncpSumWSs) - return ncpTotal + # self.plotSumNCPFits(wsDataSum, *ncpSumWSs) def prepareFitArgs(self): @@ -367,19 +395,13 @@ def convertDataXToYSpacesForEachMass(self, dataX, delta_Q, delta_E): def fitNcpToArray(self): """Takes dataY as a 2D array and returns the 2D array best fit parameters.""" - for row in range(len(self._dataY)): - - specFitPars = self.fitNcpToSingleSpec(row) + self._row_being_fit = 0 + while self._row_being_fit != len(self._dataY): + # for row in range(len(self._dataY)): - self._fit_parameters[row] = specFitPars + self.fitNcpToSingleSpec(self._row_being_fit) - if np.all(specFitPars == 0): - print("Skipped spectra.") - else: - with np.printoptions( - suppress=True, precision=4, linewidth=200, threshold=sys.maxsize - ): - print(specFitPars) + self._row_being_fit += 1 assert ~np.all( self._fit_parameters == 0 @@ -387,22 +409,6 @@ def fitNcpToArray(self): return self._fit_parameters - def createTableWSForFitPars(self, noOfMasses, arrFitPars): - tableWS = CreateEmptyTableWorkspace( - OutputWorkspace=self._workspace_being_fit.name()+ "_Best_Fit_NCP_Parameters" - ) - tableWS.setTitle("SCIPY Fit") - tableWS.addColumn(type="float", name="Spec Idx") - for i in range(int(noOfMasses)): - tableWS.addColumn(type="float", name=f"Intensity {i}") - tableWS.addColumn(type="float", name=f"Width {i}") - tableWS.addColumn(type="float", name=f"Center {i}") - tableWS.addColumn(type="float", name="Norm Chi2") - tableWS.addColumn(type="float", name="No Iter") - - for row in arrFitPars: # Pass array onto table ws - tableWS.addRow(row) - return def calculateNcpArr(self, arrBestFitPars): @@ -427,7 +433,7 @@ def calculateNcpRow(self, initPars, row): # return np.zeros(self._y_space_arrays.shape) return np.zeros_like(self._y_space_arrays[row]) - ncpForEachMass, ncpTotal = self.calculateNcpSpec(initPars, row) + ncpForEachMass = self.calculateNcpSpec(initPars, row) return ncpForEachMass @@ -509,12 +515,12 @@ def plotSumNCPFits(self, wsDataSum, wsTotNCPSum, wsMNCPSum): def extractMeans(self, wsName): """Extract widths and intensities from tableWorkspace""" - fitParsTable = mtd[wsName + "_Best_Fit_NCP_Parameters"] + fitParsTable = self._table_fit_results widths = np.zeros((self._masses.size, fitParsTable.rowCount())) intensities = np.zeros(widths.shape) - for i in range(self._masses.size): - widths[i] = fitParsTable.column(f"Width {i}") - intensities[i] = fitParsTable.column(f"Intensity {i}") + for i, p in enumerate(self._profiles.values()): + widths[i] = fitParsTable.column(f"{p.label} Width") + intensities[i] = fitParsTable.column(f"{p.label} Intensity") ( meanWidths, @@ -623,38 +629,67 @@ def fitNcpToSingleSpec(self, row): """Fits the NCP and returns the best fit parameters for one spectrum""" if np.all(self._dataY[row] == 0): - return np.zeros(len(self._initial_fit_parameters) + 3) + self._table_fit_results.addRow(np.zeros(3*len(self._profiles)+3)) + return + + # Pack profile parameters + initial_parameters = [] + bounds = [] + for p in self._profiles.values(): + for attr in ['intensity', 'width', 'center']: + initial_parameters.append(getattr(p, attr)) + for attr in ['_intensity_bounds', '_width_bounds', '_center_bounds']: + bounds.append(getattr(p, attr)) result = optimize.minimize( self.errorFunction, - self._initial_fit_parameters, + initial_parameters, args=(row), method="SLSQP", - bounds=self._bounds, + bounds=bounds, constraints=self._constraints, ) fitPars = result["x"] + # Pass results to table noDegreesOfFreedom = len(self._dataY[row]) - len(fitPars) - specFitPars = np.append(self._instrument_params[row, 0], fitPars) - return np.append(specFitPars, [result["fun"] / noDegreesOfFreedom, result["nit"]]) + normalised_chi2 = result["fun"] / noDegreesOfFreedom + number_iterations = result["nit"] + spectrum_number = self._instrument_params[row, 0] + tableRow = np.hstack((spectrum_number, fitPars, normalised_chi2, number_iterations)) + self._table_fit_results.addRow(tableRow) + self._fit_parameters[self._row_being_fit] = tableRow + + with np.printoptions( + suppress=True, precision=4, linewidth=200, threshold=sys.maxsize + ): + print(tableRow) + + # Pass fit profiles into workspaces + individual_ncps = self.calculateNcpSpec(fitPars, row) + for ncp, element in zip(individual_ncps, self._profiles.keys()): + self._fit_profiles_workspaces[element].dataY(row)[:] = ncp + + self._fit_profiles_workspaces['total'].dataY(row)[:] = np.sum(individual_ncps, axis=0) + return def errorFunction(self, pars, row): """Error function to be minimized, operates in TOF space""" - ncpForEachMass, ncpTotal = self.calculateNcpSpec(pars, row) + ncpForEachMass = self.calculateNcpSpec(pars, row) + ncpTotal = np.sum(ncpForEachMass, axis=0) # Ignore any masked values from Jackknife or masked tof range zerosMask = self._dataY[row] == 0 ncpTotal = ncpTotal[~zerosMask] - dataYf = self._dataY[row, ~zerosMask] - dataEf = self._dataE[row, ~zerosMask] + dataY = self._dataY[row, ~zerosMask] + dataE = self._dataE[row, ~zerosMask] if np.all(self._dataE[row] == 0): # When errors not present - return np.sum((ncpTotal - dataYf) ** 2) + return np.sum((ncpTotal - dataY) ** 2) - return np.sum((ncpTotal - dataYf) ** 2 / dataEf**2) + return np.sum((ncpTotal - dataY) ** 2 / dataE**2) def calculateNcpSpec(self, pars, row): @@ -662,7 +697,11 @@ def calculateNcpSpec(self, pars, row): Shapes: datax (1, n), ySpacesForEachMass (4, n), res (4, 2), deltaQ (1, n), E0 (1,n), where n is no of bins""" - masses, intensities, widths, centers = self.prepareArraysFromPars(pars) + intensities = pars[::3].reshape(-1, 1) + widths = pars[1::3].reshape(-1, 1) + centers = pars[2::3].reshape(-1, 1) + masses = self._masses.reshape(-1, 1) + v0, E0, deltaE, deltaQ = self._kinematic_arrays[row] gaussRes, lorzRes = self.caculateResolutionForEachMass(centers, row) @@ -677,19 +716,7 @@ def calculateNcpSpec(self, pars, row): * 0.72 ) ncpForEachMass = intensities * (JOfY + FSE) * E0 * E0 ** (-0.92) * masses / deltaQ - ncpTotal = np.sum(ncpForEachMass, axis=0) - return ncpForEachMass, ncpTotal - - - def prepareArraysFromPars(self, initPars): - """Extracts the intensities, widths and centers from the fitting parameters - Reshapes all of the arrays to collumns, for the calculation of the ncp,""" - - masses = self._masses[:, np.newaxis] - intensities = initPars[::3].reshape(masses.shape) - widths = initPars[1::3].reshape(masses.shape) - centers = initPars[2::3].reshape(masses.shape) - return masses, intensities, widths, centers + return ncpForEachMass def caculateResolutionForEachMass(self, centers, row): @@ -992,11 +1019,11 @@ def _set_up_results_mehtods(self): allFitWs.append(ws.extractY()) # Extract total ncp - totNcpWs = mtd[wsIterName + "_TOF_Fitted_Profiles"] + totNcpWs = mtd[wsIterName + "_total_ncp"] allTotNcp.append(totNcpWs.extractY()) # Extract best fit parameters - fitParTable = mtd[wsIterName + "_Best_Fit_NCP_Parameters"] + fitParTable = mtd[wsIterName + "_fit_results"] bestFitPars = [] for key in fitParTable.keys(): bestFitPars.append(fitParTable.column(key)) @@ -1004,16 +1031,11 @@ def _set_up_results_mehtods(self): # Extract individual ncp allNCP = [] - i = 0 - while True: # By default, looks for all ncp ws until it breaks - try: - ncpWsToAppend = mtd[ - wsIterName + "_TOF_Fitted_Profile_" + str(i) - ] - allNCP.append(ncpWsToAppend.extractY()) - i += 1 - except KeyError: - break + for p in self._profiles.values(): + ncpWsToAppend = mtd[ + wsIterName + f"_{p.label}_ncp" + ] + allNCP.append(ncpWsToAppend.extractY()) allNCP = switchFirstTwoAxis(np.array(allNCP)) allIterNcp.append(allNCP) From d58bee70e7fe4d9e2c5d81074b343ef97f041ed0 Mon Sep 17 00:00:00 2001 From: GuiMacielPereira Date: Mon, 5 Aug 2024 11:33:39 +0100 Subject: [PATCH 08/25] Cleaned fit of ncps to use object methods - Renamed functions - Changed functions to use self._row_being_fit instead of using the row as a function argument - Deleted functions made redundant due to previous commit --- src/mvesuvio/oop/AnalysisRoutine.py | 252 +++++++++------------------- 1 file changed, 79 insertions(+), 173 deletions(-) diff --git a/src/mvesuvio/oop/AnalysisRoutine.py b/src/mvesuvio/oop/AnalysisRoutine.py index 1ba1dc79..6cd663e6 100644 --- a/src/mvesuvio/oop/AnalysisRoutine.py +++ b/src/mvesuvio/oop/AnalysisRoutine.py @@ -142,7 +142,7 @@ def _update_workspace_data(self): def _create_emtpy_ncp_workspace(self, suffix): return CreateWorkspace( - DataX=np.zeros(self._dataX.size), + DataX=self._dataX, DataY=np.zeros(self._dataY.size), DataE=np.zeros(self._dataE.size), Nspec=self._workspace_being_fit.getNumberHistograms(), @@ -184,7 +184,9 @@ def run(self): self._update_workspace_data() - self.fitNcpToWorkspace() + self._fit_neutron_compton_profiles() + + self._create_summed_workspaces() mWidths, stdWidths, mIntRatios, stdIntRatios = self.extractMeans(self._workspace_being_fit.name()) @@ -196,8 +198,6 @@ def run(self): if iteration == self._number_of_iterations: break - # TODO: Refactored until here ------- - # Replace zero columns (bins) with ncp total fit # If ws has no zero column, then remains unchanged if iteration == 0: @@ -248,10 +248,6 @@ def remaskValues(wsName, wsToMaskName): def createTableInitialParameters(self): - # print("\nRUNNING ", self.modeRunning, " SCATTERING.\n") - # if self.modeRunning == "BACKWARD": - # print(f"\nH ratio to next lowest mass = {self._h_ratio}\n") - meansTableWS = CreateEmptyTableWorkspace( OutputWorkspace=self._name + "_Initial_Parameters" ) @@ -275,26 +271,22 @@ def createTableInitialParameters(self): print("\n") - def fitNcpToWorkspace(self): + def _fit_neutron_compton_profiles(self): """ - Performs the fit of ncp to the workspace. - Firtly the arrays required for the fit are prepared and then the fit is performed iteratively - on a spectrum by spectrum basis. + Performs the fit of neutron compton profiles to the workspace being fit. + The profiles are fit on a spectrum by spectrum basis. """ + print("\nFitting Neutron Compron Prolfiles:\n") - print("\nFitting NCP:\n") - - self.fitNcpToArray() - - # self.createTableWSForFitPars(len(self._profiles), arrFitPars) - - # ncpForEachMass, ncpTotal = self.calculateNcpArr(arrBestFitPars) - # ncpSumWSs = self.createNcpWorkspaces(ncpForEachMass, ncpTotal) + self._row_being_fit = 0 + while self._row_being_fit != len(self._dataY): + self._fit_neutron_compton_profiles_to_row() + self._row_being_fit += 1 - wsDataSum = SumSpectra( - InputWorkspace=self._workspace_being_fit.name(), - OutputWorkspace=self._workspace_being_fit.name() + "_Sum") - # self.plotSumNCPFits(wsDataSum, *ncpSumWSs) + assert ~np.all( + self._fit_parameters == 0 + ), "Fitting parameters cannot be zero for all spectra!" + return def prepareFitArgs(self): @@ -306,8 +298,8 @@ def prepareFitArgs(self): ySpacesForEachMass = self.convertDataXToYSpacesForEachMass( self._dataX, delta_Q, delta_E ) - kinematicArrays = self.reshapeArrayPerSpectrum(kinematicArrays) - ySpacesForEachMass = self.reshapeArrayPerSpectrum(ySpacesForEachMass) + kinematicArrays = np.swapaxes(kinematicArrays, 0, 1) + ySpacesForEachMass = np.swapaxes(ySpacesForEachMass, 0, 1) return resolutionPars, instrPars, kinematicArrays, ySpacesForEachMass @@ -365,15 +357,6 @@ def calculateKinematicsArrays(self, instrPars): return v0, E0, delta_E, delta_Q # shape(no of spectrums, no of bins) - @staticmethod - def reshapeArrayPerSpectrum(A): - """ - Exchanges the first two axes of an array A. - Rearranges array to match iteration per spectrum - """ - return np.stack(np.split(A, len(A), axis=0), axis=2)[0] - - def convertDataXToYSpacesForEachMass(self, dataX, delta_Q, delta_E): "Calculates y spaces from TOF data, each row corresponds to one mass" @@ -392,100 +375,7 @@ def convertDataXToYSpacesForEachMass(self, dataX, delta_Q, delta_E): return ySpacesForEachMass - def fitNcpToArray(self): - """Takes dataY as a 2D array and returns the 2D array best fit parameters.""" - - self._row_being_fit = 0 - while self._row_being_fit != len(self._dataY): - # for row in range(len(self._dataY)): - - self.fitNcpToSingleSpec(self._row_being_fit) - - self._row_being_fit += 1 - - assert ~np.all( - self._fit_parameters == 0 - ), "Either Fits are all zero or assignment of fitting not working" - return self._fit_parameters - - - - - def calculateNcpArr(self, arrBestFitPars): - """Calculates the matrix of NCP from matrix of best fit parameters""" - - allNcpForEachMass = [] - for row in range(len(arrBestFitPars)): - ncpForEachMass = self.calculateNcpRow(arrBestFitPars[row], row) - - allNcpForEachMass.append(ncpForEachMass) - - allNcpForEachMass = np.array(allNcpForEachMass) - allNcpTotal = np.sum(allNcpForEachMass, axis=1) - return allNcpForEachMass, allNcpTotal - - - def calculateNcpRow(self, initPars, row): - """input: all row shape - output: row shape with the ncpTotal for each mass""" - - if np.all(initPars == 0): - # return np.zeros(self._y_space_arrays.shape) - return np.zeros_like(self._y_space_arrays[row]) - - ncpForEachMass = self.calculateNcpSpec(initPars, row) - return ncpForEachMass - - - def createNcpWorkspaces(self, ncpForEachMass, ncpTotal): - """Creates workspaces from ncp array data""" - - # Need to rearrage array of yspaces into seperate arrays for each mass - ncpForEachMass = switchFirstTwoAxis(ncpForEachMass) - - # Use ws dataX to match with histogram data - dataX = self._dataX - # dataX = ws.extractX()[ - # :, : ncpTotal.shape[1] - # ] # Make dataX match ncp shape automatically - assert ( - ncpTotal.shape == dataX.shape - ), "DataX and DataY in ws need to be the same shape." - - # ncpTotWS = CloneWorkspace(InputWorkspace=ws.name(), OutputWorkspace=ws.name() + "_TOF_Fitted_Profiles") - # passDataIntoWS(dataX, ncpTotal, np.zeros_like(dataX), ncpTotWS) - ncpTotWS = createWS( - dataX, ncpTotal, np.zeros(dataX.shape), self._workspace_being_fit.name() + "_TOF_Fitted_Profiles", - parentWorkspace=self._workspace_being_fit - ) - # MaskDetectors(Workspace=ncpTotWS, WorkspaceIndexList=ic.maskedDetectorIdx) - MaskDetectors(Workspace=ncpTotWS, SpectraList=self._mask_spectra) - wsTotNCPSum = SumSpectra( - InputWorkspace=ncpTotWS, OutputWorkspace=ncpTotWS.name() + "_Sum" - ) - - # Individual ncp workspaces - wsMNCPSum = [] - for i, ncp_m in enumerate(ncpForEachMass): - # ncpMWS = CloneWorkspace(InputWorkspace=ws.name(), OutputWorkspace=ws.name()+"_TOF_Fitted_Profile_" + str(i)) - # passDataIntoWS(dataX, ncp_m, np.zeros_like(dataX), ncpMWS) - ncpMWS = createWS( - dataX, - ncp_m, - np.zeros(dataX.shape), - self._workspace_being_fit.name() + "_TOF_Fitted_Profile_" + str(i), - parentWorkspace=self._workspace_being_fit - ) - MaskDetectors(Workspace=ncpMWS, SpectraList=self._mask_spectra) - wsNCPSum = SumSpectra( - InputWorkspace=ncpMWS, OutputWorkspace=ncpMWS.name() + "_Sum" - ) - wsMNCPSum.append(wsNCPSum) - - return wsTotNCPSum, wsMNCPSum - - - def plotSumNCPFits(self, wsDataSum, wsTotNCPSum, wsMNCPSum): + def save_plots(self, wsDataSum, wsTotNCPSum, wsMNCPSum): # if IC.runningSampleWS: # Skip saving figure if running bootstrap # return @@ -512,6 +402,18 @@ def plotSumNCPFits(self, wsDataSum, wsTotNCPSum, wsMNCPSum): return + def _create_summed_workspaces(self): + + SumSpectra( + InputWorkspace=self._workspace_being_fit.name(), + OutputWorkspace=self._workspace_being_fit.name() + "_Sum") + + for ws in self._fit_profiles_workspaces.values(): + SumSpectra( + InputWorkspace=ws.name(), + OutputWorkspace=ws.name() + "_Sum" + ) + def extractMeans(self, wsName): """Extract widths and intensities from tableWorkspace""" @@ -521,7 +423,6 @@ def extractMeans(self, wsName): for i, p in enumerate(self._profiles.values()): widths[i] = fitParsTable.column(f"{p.label} Width") intensities[i] = fitParsTable.column(f"{p.label} Intensity") - ( meanWidths, stdWidths, @@ -625,14 +526,18 @@ def filterWidthsAndIntensities(self, widthsIn, intensitiesIn): return betterWidths, betterIntensities - def fitNcpToSingleSpec(self, row): - """Fits the NCP and returns the best fit parameters for one spectrum""" + def _fit_neutron_compton_profiles_to_row(self): + """ + Fits the neutron compton profiles to one spectrum. + Calculates the best fit parameters and adds them to results table. + Adds row with calculated profiles to results workspace. + """ - if np.all(self._dataY[row] == 0): + if np.all(self._dataY[self._row_being_fit] == 0): self._table_fit_results.addRow(np.zeros(3*len(self._profiles)+3)) return - # Pack profile parameters + # Need to transform profiles into parameter array for minimize initial_parameters = [] bounds = [] for p in self._profiles.values(): @@ -644,99 +549,100 @@ def fitNcpToSingleSpec(self, row): result = optimize.minimize( self.errorFunction, initial_parameters, - args=(row), method="SLSQP", bounds=bounds, constraints=self._constraints, ) fitPars = result["x"] - # Pass results to table - noDegreesOfFreedom = len(self._dataY[row]) - len(fitPars) + # Pass fit parameters to results table + noDegreesOfFreedom = len(self._dataY[self._row_being_fit]) - len(fitPars) normalised_chi2 = result["fun"] / noDegreesOfFreedom number_iterations = result["nit"] - spectrum_number = self._instrument_params[row, 0] + spectrum_number = self._instrument_params[self._row_being_fit, 0] tableRow = np.hstack((spectrum_number, fitPars, normalised_chi2, number_iterations)) self._table_fit_results.addRow(tableRow) + + # Store results for easier access when calculating means self._fit_parameters[self._row_being_fit] = tableRow with np.printoptions( - suppress=True, precision=4, linewidth=200, threshold=sys.maxsize + precision=4, linewidth=200, threshold=sys.maxsize ): print(tableRow) # Pass fit profiles into workspaces - individual_ncps = self.calculateNcpSpec(fitPars, row) + individual_ncps = self._neutron_compton_profiles(fitPars) for ncp, element in zip(individual_ncps, self._profiles.keys()): - self._fit_profiles_workspaces[element].dataY(row)[:] = ncp + self._fit_profiles_workspaces[element].dataY(self._row_being_fit)[:] = ncp - self._fit_profiles_workspaces['total'].dataY(row)[:] = np.sum(individual_ncps, axis=0) + self._fit_profiles_workspaces['total'].dataY(self._row_being_fit)[:] = np.sum(individual_ncps, axis=0) return - def errorFunction(self, pars, row): - """Error function to be minimized, operates in TOF space""" + def errorFunction(self, pars): + """Error function to be minimized, in TOF space""" - ncpForEachMass = self.calculateNcpSpec(pars, row) + ncpForEachMass = self._neutron_compton_profiles(pars) ncpTotal = np.sum(ncpForEachMass, axis=0) # Ignore any masked values from Jackknife or masked tof range - zerosMask = self._dataY[row] == 0 + zerosMask = self._dataY[self._row_being_fit] == 0 ncpTotal = ncpTotal[~zerosMask] - dataY = self._dataY[row, ~zerosMask] - dataE = self._dataE[row, ~zerosMask] + dataY = self._dataY[self._row_being_fit, ~zerosMask] + dataE = self._dataE[self._row_being_fit, ~zerosMask] - if np.all(self._dataE[row] == 0): # When errors not present + if np.all(self._dataE[self._row_being_fit] == 0): # When errors not present return np.sum((ncpTotal - dataY) ** 2) return np.sum((ncpTotal - dataY) ** 2 / dataE**2) - def calculateNcpSpec(self, pars, row): - """Creates a synthetic C(t) to be fitted to TOF values of a single spectrum, from J(y) and resolution functions - Shapes: datax (1, n), ySpacesForEachMass (4, n), res (4, 2), deltaQ (1, n), E0 (1,n), - where n is no of bins""" + def _neutron_compton_profiles(self, pars): + """ + Neutron Compron Profile distribution on TOF space for a single spectrum. + Calculated from kinematics, J(y) and resolution functions. + """ intensities = pars[::3].reshape(-1, 1) widths = pars[1::3].reshape(-1, 1) centers = pars[2::3].reshape(-1, 1) masses = self._masses.reshape(-1, 1) - v0, E0, deltaE, deltaQ = self._kinematic_arrays[row] + v0, E0, deltaE, deltaQ = self._kinematic_arrays[self._row_being_fit] - gaussRes, lorzRes = self.caculateResolutionForEachMass(centers, row) + gaussRes, lorzRes = self.caculateResolutionForEachMass(centers) totalGaussWidth = np.sqrt(widths**2 + gaussRes**2) - JOfY = self.pseudoVoigt(self._y_space_arrays[row] - centers, totalGaussWidth, lorzRes) + JOfY = self.pseudoVoigt(self._y_space_arrays[self._row_being_fit] - centers, totalGaussWidth, lorzRes) FSE = ( - -numericalThirdDerivative(self._y_space_arrays[row], JOfY) + -numericalThirdDerivative(self._y_space_arrays[self._row_being_fit], JOfY) * widths**4 / deltaQ * 0.72 ) - ncpForEachMass = intensities * (JOfY + FSE) * E0 * E0 ** (-0.92) * masses / deltaQ - return ncpForEachMass + return intensities * (JOfY + FSE) * E0 * E0 ** (-0.92) * masses / deltaQ - def caculateResolutionForEachMass(self, centers, row): + def caculateResolutionForEachMass(self, centers): """Calculates the gaussian and lorentzian resolution output: two column vectors, each row corresponds to each mass""" - gaussianResWidth = self.calcGaussianResolution(centers, row) - lorentzianResWidth = self.calcLorentzianResolution(centers, row) + gaussianResWidth = self.calcGaussianResolution(centers) + lorentzianResWidth = self.calcLorentzianResolution(centers) return gaussianResWidth, lorentzianResWidth - def kinematicsAtYCenters(self, centers, row): + def kinematicsAtYCenters(self, centers): """v0, E0, deltaE, deltaQ at the peak of the ncpTotal for each mass""" shapeOfArrays = centers.shape - proximityToYCenters = np.abs(self._y_space_arrays[row] - centers) + proximityToYCenters = np.abs(self._y_space_arrays[self._row_being_fit] - centers) yClosestToCenters = proximityToYCenters.min(axis=1).reshape(shapeOfArrays) yCentersMask = proximityToYCenters == yClosestToCenters - v0, E0, deltaE, deltaQ = self._kinematic_arrays[row] + v0, E0, deltaE, deltaQ = self._kinematic_arrays[self._row_being_fit] # Expand arrays to match shape of yCentersMask v0 = v0 * np.ones(shapeOfArrays) @@ -751,11 +657,11 @@ def kinematicsAtYCenters(self, centers, row): return v0, E0, deltaE, deltaQ - def calcGaussianResolution(self, centers, row): + def calcGaussianResolution(self, centers): masses = self._masses.reshape((self._masses.size, 1)) - v0, E0, delta_E, delta_Q = self.kinematicsAtYCenters(centers, row) - det, plick, angle, T0, L0, L1 = self._instrument_params[row] - dE1, dTOF, dTheta, dL0, dL1, dE1_lorz = self._resolution_params[row] + v0, E0, delta_E, delta_Q = self.kinematicsAtYCenters(centers) + det, plick, angle, T0, L0, L1 = self._instrument_params[self._row_being_fit] + dE1, dTOF, dTheta, dL0, dL1, dE1_lorz = self._resolution_params[self._row_being_fit] mN, Ef, en_to_vel, vf, hbar = loadConstants() angle = angle * np.pi / 180 @@ -797,11 +703,11 @@ def calcGaussianResolution(self, centers, row): return gaussianResWidth - def calcLorentzianResolution(self, centers, row): + def calcLorentzianResolution(self, centers): masses = self._masses.reshape((self._masses.size, 1)) - v0, E0, delta_E, delta_Q = self.kinematicsAtYCenters(centers, row) - det, plick, angle, T0, L0, L1 = self._instrument_params[row] - dE1, dTOF, dTheta, dL0, dL1, dE1_lorz = self._resolution_params[row] + v0, E0, delta_E, delta_Q = self.kinematicsAtYCenters(centers) + det, plick, angle, T0, L0, L1 = self._instrument_params[self._row_being_fit] + dE1, dTOF, dTheta, dL0, dL1, dE1_lorz = self._resolution_params[self._row_being_fit] mN, Ef, en_to_vel, vf, hbar = loadConstants() angle = angle * np.pi / 180 From fa669101029cb8d6138aa5aa2bbb2e01692b0cf6 Mon Sep 17 00:00:00 2001 From: GuiMacielPereira Date: Mon, 5 Aug 2024 16:09:42 +0100 Subject: [PATCH 09/25] Clean masking of columns in input workspace When the input workspace contains columns of zeros, these should be replaced by the fitted profile before doing the multiple scattering and gamma correction. --- src/mvesuvio/oop/AnalysisRoutine.py | 115 +++++++++++++++++---------- src/mvesuvio/oop/analysis_helpers.py | 2 +- src/mvesuvio/oop/run_routine.py | 2 +- 3 files changed, 77 insertions(+), 42 deletions(-) diff --git a/src/mvesuvio/oop/AnalysisRoutine.py b/src/mvesuvio/oop/AnalysisRoutine.py index 6cd663e6..c00f59d8 100644 --- a/src/mvesuvio/oop/AnalysisRoutine.py +++ b/src/mvesuvio/oop/AnalysisRoutine.py @@ -40,6 +40,8 @@ def __init__(self, workspace, ip_file, number_of_iterations, mask_spectra, self._save_results_path = results_path # Variables changing during fit + self._workspace_for_corrections = workspace + self._zero_columns_mask = None self._workspace_being_fit = workspace self._row_being_fit = 0 self._table_fit_results = None @@ -50,7 +52,7 @@ def __init__(self, workspace, ip_file, number_of_iterations, mask_spectra, self._profiles = {} # Only used for system tests, remove once tests are updated - self._run_hist_data = True + self._run_hist_data = True self._run_norm_voigt = False # Links to another AnalysisRoutine object: @@ -174,8 +176,8 @@ def run(self): # ) CloneWorkspace( - InputWorkspace=self._workspace_being_fit, - OutputWorkspace=self._name + "0" + InputWorkspace=self._workspace_being_fit.name(), + OutputWorkspace=self._workspace_being_fit.name() + "0" ) for iteration in range(self._number_of_iterations + 1): @@ -188,36 +190,34 @@ def run(self): self._create_summed_workspaces() - mWidths, stdWidths, mIntRatios, stdIntRatios = self.extractMeans(self._workspace_being_fit.name()) + mWidths, stdWidths, mIntRatios, stdIntRatios = self.extractMeans() self.createMeansAndStdTableWS( - self._workspace_being_fit.name(), mWidths, stdWidths, mIntRatios, stdIntRatios + mWidths, stdWidths, mIntRatios, stdIntRatios ) # When last iteration, skip MS and GC if iteration == self._number_of_iterations: break - # Replace zero columns (bins) with ncp total fit - # If ws has no zero column, then remains unchanged - if iteration == 0: - wsNCPM = replaceZerosWithNCP(mtd[self._name], self._fit_profiles_workspaces['total'].extractY()) + if iteration==0: + self._replace_zero_columns_with_ncp_fit() CloneWorkspace(InputWorkspace=self._name, OutputWorkspace="tmpNameWs") if self._gamma_correction: - wsGC = self.createWorkspacesForGammaCorrection(mWidths, mIntRatios, wsNCPM) + wsGC = self.createWorkspacesForGammaCorrection(mWidths, mIntRatios) Minus( LHSWorkspace="tmpNameWs", RHSWorkspace=wsGC, OutputWorkspace="tmpNameWs" ) if self._multiple_scattering_correction: - wsMS = self.createWorkspacesForMSCorrection(mWidths, mIntRatios, wsNCPM) + wsMS = self.createWorkspacesForMSCorrection(mWidths, mIntRatios) Minus( LHSWorkspace="tmpNameWs", RHSWorkspace=wsMS, OutputWorkspace="tmpNameWs" ) - self.remaskValues(self._name, "tmpNameWS") # Masks cols in the same place as in ic.name + self._remask_columns_with_zeros("tmpNameWS") RenameWorkspace( InputWorkspace="tmpNameWs", OutputWorkspace=self._name + str(iteration + 1) ) @@ -227,24 +227,6 @@ def run(self): return self - @staticmethod - def remaskValues(wsName, wsToMaskName): - """ - Uses the ws before the MS correction to look for masked columns or dataE - and implement the same masked values after the correction. - """ - ws = mtd[wsName] - dataX, dataY, dataE = extractWS(ws) - mask = np.all(dataY == 0, axis=0) - - wsM = mtd[wsToMaskName] - dataXM, dataYM, dataEM = extractWS(wsM) - dataYM[:, mask] = 0 - if np.all(dataE == 0): - dataEM = np.zeros(dataEM.shape) - - passDataIntoWS(dataXM, dataYM, dataEM, wsM) - return def createTableInitialParameters(self): @@ -414,7 +396,7 @@ def _create_summed_workspaces(self): OutputWorkspace=ws.name() + "_Sum" ) - def extractMeans(self, wsName): + def extractMeans(self): """Extract widths and intensities from tableWorkspace""" fitParsTable = self._table_fit_results @@ -437,10 +419,10 @@ def extractMeans(self, wsName): def createMeansAndStdTableWS( - self, wsName, meanWidths, stdWidths, meanIntensityRatios, stdIntensityRatios + self, meanWidths, stdWidths, meanIntensityRatios, stdIntensityRatios ): meansTableWS = CreateEmptyTableWorkspace( - OutputWorkspace=wsName + "_Mean_Widths_And_Intensities" + OutputWorkspace=self._workspace_being_fit.name() + "_Mean_Widths_And_Intensities" ) meansTableWS.addColumn(type="float", name="Mass") meansTableWS.addColumn(type="float", name="Mean Widths") @@ -567,7 +549,7 @@ def _fit_neutron_compton_profiles_to_row(self): self._fit_parameters[self._row_being_fit] = tableRow with np.printoptions( - precision=4, linewidth=200, threshold=sys.maxsize + suppress=True, precision=4, linewidth=200, threshold=sys.maxsize ): print(tableRow) @@ -741,10 +723,63 @@ def pseudoVoigt(self, x, sigma, gamma): return pseudo_voigt / norm - def createWorkspacesForMSCorrection(self, meanWidths, meanIntensityRatios, wsNCPM): + def _replace_zero_columns_with_ncp_fit(self): + """ + If the initial input contains columns with zeros + (to mask resonance peaks) then these sections must be approximated + by the total fitted function because multiple scattering and + gamma correction algorithms do not accept columns with zeros. + If no masked columns are present then the input workspace + for corrections is left unchanged. + """ + dataY = self._workspace_for_corrections.extractY() + ncp = self._fit_profiles_workspaces['total'].extractY() + + self._zero_columns_mask = np.all(dataY == 0, axis=0) # Masked Cols + + ws = CloneWorkspace( + InputWorkspace=self._workspace_for_corrections.name(), + OutputWorkspace=self._workspace_for_corrections.name() + "_CorrectionsInput" + ) + for row in range(ws.getNumberHistograms()): + # TODO: Once the option to chage point to hist is removed, remove [:len(ncp)] + ws.dataY(row)[self._zero_columns_mask] = ncp[row, self._zero_columns_mask[:len(ncp[row])]] + + self._workspace_for_corrections = ws + SumSpectra( + InputWorkspace=self._workspace_for_corrections.name(), + OutputWorkspace=self._workspace_for_corrections.name() + "_Sum" + ) + return + + + def _remask_columns_with_zeros(self, ws_to_remask_name): + """ + Uses previously stored information on masked columns in the + initial workspace to set these columns again to zero on the + workspace resulting from the multiple scattering or gamma correction. + """ + ws = mtd[ws_to_remask_name] + for row in range(ws.getNumberHistograms()): + ws.dataY(row)[self._zero_columns_mask] = 0 + ws.dataE(row)[self._zero_columns_mask] = 0 + # dataX, dataY, dataE = extractWS(ws) + # mask = np.all(dataY == 0, axis=0) + # + # wsM = mtd[wsToMaskName] + # dataXM, dataYM, dataEM = extractWS(wsM) + # dataYM[:, mask] = 0 + # if np.all(dataE == 0): + # dataEM = np.zeros(dataEM.shape) + # + # passDataIntoWS(dataXM, dataYM, dataEM, wsM) + return + + + def createWorkspacesForMSCorrection(self, meanWidths, meanIntensityRatios): """Creates _MulScattering and _TotScattering workspaces used for the MS correction""" - self.createSlabGeometry(wsNCPM) # Sample properties for MS correction + self.createSlabGeometry(self._workspace_for_corrections) # Sample properties for MS correction sampleProperties = self.calcMSCorrectionSampleProperties(meanWidths, meanIntensityRatios) print( @@ -753,7 +788,7 @@ def createWorkspacesForMSCorrection(self, meanWidths, meanIntensityRatios, wsNCP "\n", ) - return self.createMulScatWorkspaces(wsNCPM, sampleProperties) + return self.createMulScatWorkspaces(self._workspace_for_corrections, sampleProperties) def createSlabGeometry(self, wsNCPM): @@ -775,7 +810,7 @@ def createSlabGeometry(self, wsNCPM): + "" ) - CreateSampleShape(wsNCPM, xml_str) + CreateSampleShape(self._workspace_for_corrections, xml_str) def calcMSCorrectionSampleProperties(self, meanWidths, meanIntensityRatios): @@ -849,10 +884,10 @@ def createMulScatWorkspaces(self, ws, sampleProperties): return mtd[ws.name() + "_MulScattering"] - def createWorkspacesForGammaCorrection(self, meanWidths, meanIntensityRatios, wsNCPM): + def createWorkspacesForGammaCorrection(self, meanWidths, meanIntensityRatios): """Creates _gamma_background correction workspace to be subtracted from the main workspace""" - inputWS = wsNCPM.name() + inputWS = self._workspace_for_corrections.name() profiles = self.calcGammaCorrectionProfiles(meanWidths, meanIntensityRatios) diff --git a/src/mvesuvio/oop/analysis_helpers.py b/src/mvesuvio/oop/analysis_helpers.py index 32ac2d46..5246daed 100644 --- a/src/mvesuvio/oop/analysis_helpers.py +++ b/src/mvesuvio/oop/analysis_helpers.py @@ -91,7 +91,7 @@ def maskBinsWithZeros(ws, maskTOFRange): Used to mask resonance peaks. """ - if maskTOFRange is None: # Masked TOF bins not found, skip + if maskTOFRange is None: return dataX, dataY, dataE = extractWS(ws) diff --git a/src/mvesuvio/oop/run_routine.py b/src/mvesuvio/oop/run_routine.py index 7ac7de5e..dcf5d2f7 100644 --- a/src/mvesuvio/oop/run_routine.py +++ b/src/mvesuvio/oop/run_routine.py @@ -19,7 +19,7 @@ def run_analysis(): cropedWs = cropAndMaskWorkspace(ws, firstSpec=144, lastSpec=182, maskedDetectors=[173, 174, 179], - maskTOFRange=None) + maskTOFRange='120, 160') AR = AnalysisRoutine(cropedWs, From e0f79d71c43d202252507fffd1a3e53699072bef Mon Sep 17 00:00:00 2001 From: GuiMacielPereira Date: Mon, 5 Aug 2024 16:11:48 +0100 Subject: [PATCH 10/25] Try to fix system tests on gh actions --- tests/system/analysis/test_new_analysis.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/system/analysis/test_new_analysis.py b/tests/system/analysis/test_new_analysis.py index 8cd0c407..9deffdbf 100644 --- a/tests/system/analysis/test_new_analysis.py +++ b/tests/system/analysis/test_new_analysis.py @@ -3,7 +3,8 @@ import numpy.testing as nptest from pathlib import Path from mvesuvio.util import handle_config -ipFilesPath = Path(handle_config.read_config_var("caching.ipfolder")) + +ipFilesPath = Path(handle_config.VESUVIO_PACKAGE_PATH).joinpath("config", "ip_files") from mvesuvio.oop.AnalysisRoutine import AnalysisRoutine from mvesuvio.oop.NeutronComptonProfile import NeutronComptonProfile From f2b0f87e0b120ae87b67bb146db3155ba9f38f27 Mon Sep 17 00:00:00 2001 From: GuiMacielPereira Date: Tue, 6 Aug 2024 14:51:06 +0100 Subject: [PATCH 11/25] Clean calculation of means and corrections Made it easier to read and understand what the routine is doing when calculating the mean widths and intensity ratios. Put the multiple scattering and gamma corrections into its own function. --- src/mvesuvio/oop/AnalysisRoutine.py | 153 ++++++++++++++-------------- 1 file changed, 78 insertions(+), 75 deletions(-) diff --git a/src/mvesuvio/oop/AnalysisRoutine.py b/src/mvesuvio/oop/AnalysisRoutine.py index c00f59d8..c40dac4f 100644 --- a/src/mvesuvio/oop/AnalysisRoutine.py +++ b/src/mvesuvio/oop/AnalysisRoutine.py @@ -19,7 +19,7 @@ def __init__(self, workspace, ip_file, number_of_iterations, mask_spectra, gamma_correction, mode_running, transmission_guess=None, multiple_scattering_order=None, number_of_events=None, results_path=None): - self._name = workspace.name() + self._name = workspace.name() + '_' self._ip_file = ip_file self._number_of_iterations = number_of_iterations spectrum_list = workspace.getSpectrumNumbers() @@ -141,6 +141,11 @@ def _update_workspace_data(self): self._fit_profiles_workspaces[element] = self._create_emtpy_ncp_workspace(f'_{element}_ncp') self._fit_profiles_workspaces['total'] = self._create_emtpy_ncp_workspace(f'_total_ncp') + self._mean_widths = None + self._std_widths = None + self._mean_intensity_ratios = None + self._std_intensity_ratios = None + def _create_emtpy_ncp_workspace(self, suffix): return CreateWorkspace( @@ -175,60 +180,46 @@ def run(self): # InputWorkspace=ic.sampleWS, OutputWorkspace=initialWs.name() # ) - CloneWorkspace( + RenameWorkspace( InputWorkspace=self._workspace_being_fit.name(), - OutputWorkspace=self._workspace_being_fit.name() + "0" + OutputWorkspace=self._name + '0' ) for iteration in range(self._number_of_iterations + 1): - # Workspace from previous iteration - self._workspace_being_fit = mtd[self._name + str(iteration)] + self._workspace_being_fit = mtd[self._name + str(iteration)] self._update_workspace_data() self._fit_neutron_compton_profiles() self._create_summed_workspaces() - - mWidths, stdWidths, mIntRatios, stdIntRatios = self.extractMeans() - - self.createMeansAndStdTableWS( - mWidths, stdWidths, mIntRatios, stdIntRatios - ) + self._set_means_and_std() # When last iteration, skip MS and GC if iteration == self._number_of_iterations: break + # Do this because MS and Gamma corrections do not accept zero columns if iteration==0: self._replace_zero_columns_with_ncp_fit() - CloneWorkspace(InputWorkspace=self._name, OutputWorkspace="tmpNameWs") - - if self._gamma_correction: - wsGC = self.createWorkspacesForGammaCorrection(mWidths, mIntRatios) - Minus( - LHSWorkspace="tmpNameWs", RHSWorkspace=wsGC, OutputWorkspace="tmpNameWs" - ) - - if self._multiple_scattering_correction: - wsMS = self.createWorkspacesForMSCorrection(mWidths, mIntRatios) - Minus( - LHSWorkspace="tmpNameWs", RHSWorkspace=wsMS, OutputWorkspace="tmpNameWs" - ) + CloneWorkspace( + InputWorkspace=self._workspace_for_corrections.name(), + OutputWorkspace="next_iteration" + ) + self._correct_for_gamma_and_multiple_scattering("next_iteration") + self._remask_columns_with_zeros("next_iteration") - self._remask_columns_with_zeros("tmpNameWS") RenameWorkspace( - InputWorkspace="tmpNameWs", OutputWorkspace=self._name + str(iteration + 1) + InputWorkspace="next_iteration", + OutputWorkspace=self._name + str(iteration + 1) ) - self._set_up_results_mehtods() + self._set_results() self.save_results() return self - - def createTableInitialParameters(self): meansTableWS = CreateEmptyTableWorkspace( OutputWorkspace=self._name + "_Initial_Parameters" @@ -265,9 +256,7 @@ def _fit_neutron_compton_profiles(self): self._fit_neutron_compton_profiles_to_row() self._row_being_fit += 1 - assert ~np.all( - self._fit_parameters == 0 - ), "Fitting parameters cannot be zero for all spectra!" + assert np.any(self._fit_parameters), "Fitting parameters cannot be zero for all spectra!" return @@ -396,8 +385,8 @@ def _create_summed_workspaces(self): OutputWorkspace=ws.name() + "_Sum" ) - def extractMeans(self): - """Extract widths and intensities from tableWorkspace""" + def _set_means_and_std(self): + """Calculate mean widths and intensities from tableWorkspace""" fitParsTable = self._table_fit_results widths = np.zeros((self._masses.size, fitParsTable.rowCount())) @@ -413,18 +402,23 @@ def extractMeans(self): ) = self.calculateMeansAndStds(widths, intensities) assert ( - len(widths) == self._masses.size - ), "Widths and intensities must be in shape (noOfMasses, noOfSpec)" - return meanWidths, stdWidths, meanIntensityRatios, stdIntensityRatios + len(meanWidths) == len(self._profiles) + ), "Number of mean widths must match number of profiles!" + + self._mean_widths = meanWidths + self._std_widths = stdWidths + self._mean_intensity_ratios = meanIntensityRatios + self._std_intensity_ratios = stdIntensityRatios + self.createMeansAndStdTableWS() + return - def createMeansAndStdTableWS( - self, meanWidths, stdWidths, meanIntensityRatios, stdIntensityRatios - ): + + def createMeansAndStdTableWS(self): meansTableWS = CreateEmptyTableWorkspace( - OutputWorkspace=self._workspace_being_fit.name() + "_Mean_Widths_And_Intensities" + OutputWorkspace=self._workspace_being_fit.name() + "_MeanWidthsAndIntensities" ) - meansTableWS.addColumn(type="float", name="Mass") + meansTableWS.addColumn(type="str", name="Mass") meansTableWS.addColumn(type="float", name="Mean Widths") meansTableWS.addColumn(type="float", name="Std Widths") meansTableWS.addColumn(type="float", name="Mean Intensities") @@ -432,16 +426,16 @@ def createMeansAndStdTableWS( print("\nCreated Table with means and std:") print("\nMass Mean \u00B1 Std Widths Mean \u00B1 Std Intensities\n") - for m, mw, stdw, mi, stdi in zip( - self._masses.astype(float), - meanWidths, - stdWidths, - meanIntensityRatios, - stdIntensityRatios, + for p, mean_width, std_width, mean_intensity, std_intensity in zip( + self._profiles.values(), + self._mean_widths, + self._std_widths, + self._mean_intensity_ratios, + self._std_intensity_ratios, ): - meansTableWS.addRow([m, mw, stdw, mi, stdi]) - print(f"{m:5.2f} {mw:10.5f} \u00B1 {stdw:7.5f} {mi:10.5f} \u00B1 {stdi:7.5f}") - print("\n") + meansTableWS.addRow([p.label, mean_width, std_width, mean_intensity, std_intensity]) + print(f"{p.label:5s} {mean_width:10.5f} \u00B1 {std_width:7.5f} \ + {mean_intensity:10.5f} \u00B1 {std_intensity:7.5f}\n") return @@ -737,15 +731,14 @@ def _replace_zero_columns_with_ncp_fit(self): self._zero_columns_mask = np.all(dataY == 0, axis=0) # Masked Cols - ws = CloneWorkspace( + self._workspace_for_corrections = CloneWorkspace( InputWorkspace=self._workspace_for_corrections.name(), OutputWorkspace=self._workspace_for_corrections.name() + "_CorrectionsInput" ) - for row in range(ws.getNumberHistograms()): + for row in range(self._workspace_for_corrections.getNumberHistograms()): # TODO: Once the option to chage point to hist is removed, remove [:len(ncp)] - ws.dataY(row)[self._zero_columns_mask] = ncp[row, self._zero_columns_mask[:len(ncp[row])]] + self._workspace_for_corrections.dataY(row)[self._zero_columns_mask] = ncp[row, self._zero_columns_mask[:len(ncp[row])]] - self._workspace_for_corrections = ws SumSpectra( InputWorkspace=self._workspace_for_corrections.name(), OutputWorkspace=self._workspace_for_corrections.name() + "_Sum" @@ -759,29 +752,39 @@ def _remask_columns_with_zeros(self, ws_to_remask_name): initial workspace to set these columns again to zero on the workspace resulting from the multiple scattering or gamma correction. """ - ws = mtd[ws_to_remask_name] - for row in range(ws.getNumberHistograms()): - ws.dataY(row)[self._zero_columns_mask] = 0 - ws.dataE(row)[self._zero_columns_mask] = 0 - # dataX, dataY, dataE = extractWS(ws) - # mask = np.all(dataY == 0, axis=0) - # - # wsM = mtd[wsToMaskName] - # dataXM, dataYM, dataEM = extractWS(wsM) - # dataYM[:, mask] = 0 - # if np.all(dataE == 0): - # dataEM = np.zeros(dataEM.shape) - # - # passDataIntoWS(dataXM, dataYM, dataEM, wsM) + ws_to_remask = mtd[ws_to_remask_name] + for row in range(ws_to_remask.getNumberHistograms()): + ws_to_remask.dataY(row)[self._zero_columns_mask] = 0 + ws_to_remask.dataE(row)[self._zero_columns_mask] = 0 + return + + + def _correct_for_gamma_and_multiple_scattering(self, ws_name): + + if self._gamma_correction: + gamma_correction_ws = self.create_gamma_workspaces() + Minus( + LHSWorkspace=ws_name, + RHSWorkspace=gamma_correction_ws.name(), + OutputWorkspace=ws_name + ) + + if self._multiple_scattering_correction: + multiple_scattering_ws = self.create_multiple_scattering_workspaces() + Minus( + LHSWorkspace=ws_name, + RHSWorkspace=multiple_scattering_ws.name(), + OutputWorkspace=ws_name + ) return - def createWorkspacesForMSCorrection(self, meanWidths, meanIntensityRatios): + def create_multiple_scattering_workspaces(self): """Creates _MulScattering and _TotScattering workspaces used for the MS correction""" self.createSlabGeometry(self._workspace_for_corrections) # Sample properties for MS correction - sampleProperties = self.calcMSCorrectionSampleProperties(meanWidths, meanIntensityRatios) + sampleProperties = self.calcMSCorrectionSampleProperties(self._mean_widths, self._mean_intensity_ratios) print( "\nThe sample properties for Multiple Scattering correction are:\n\n", sampleProperties, @@ -884,12 +887,12 @@ def createMulScatWorkspaces(self, ws, sampleProperties): return mtd[ws.name() + "_MulScattering"] - def createWorkspacesForGammaCorrection(self, meanWidths, meanIntensityRatios): + def create_gamma_workspaces(self): """Creates _gamma_background correction workspace to be subtracted from the main workspace""" inputWS = self._workspace_for_corrections.name() - profiles = self.calcGammaCorrectionProfiles(meanWidths, meanIntensityRatios) + profiles = self.calcGammaCorrectionProfiles(self._mean_widths, self._mean_intensity_ratios) # Approach below not currently suitable for current versions of Mantid, but will be in the future # background, corrected = VesuvioCalculateGammaBackground(InputWorkspace=inputWS, ComptonFunction=profiles) @@ -937,7 +940,7 @@ def calcGammaCorrectionProfiles(self, meanWidths, meanIntensityRatios): return profiles - def _set_up_results_mehtods(self): + def _set_results(self): """Used to collect results from workspaces and store them in .npz files for testing.""" self.wsFinal = mtd[self._name + str(self._number_of_iterations)] @@ -981,7 +984,7 @@ def _set_up_results_mehtods(self): allIterNcp.append(allNCP) # Extract Mean and Std Widths, Intensities - meansTable = mtd[wsIterName + "_Mean_Widths_And_Intensities"] + meansTable = mtd[wsIterName + "_MeanWidthsAndIntensities"] allMeanWidhts.append(meansTable.column("Mean Widths")) allStdWidths.append(meansTable.column("Std Widths")) allMeanIntensities.append(meansTable.column("Mean Intensities")) From 35bf3dbf01dee2c45297a8578fdeb91542fb57fa Mon Sep 17 00:00:00 2001 From: GuiMacielPereira Date: Tue, 6 Aug 2024 15:06:29 +0100 Subject: [PATCH 12/25] Fix path in system test --- tests/system/analysis/test_new_analysis.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/tests/system/analysis/test_new_analysis.py b/tests/system/analysis/test_new_analysis.py index 9deffdbf..b343ba37 100644 --- a/tests/system/analysis/test_new_analysis.py +++ b/tests/system/analysis/test_new_analysis.py @@ -3,9 +3,6 @@ import numpy.testing as nptest from pathlib import Path from mvesuvio.util import handle_config - -ipFilesPath = Path(handle_config.VESUVIO_PACKAGE_PATH).joinpath("config", "ip_files") - from mvesuvio.oop.AnalysisRoutine import AnalysisRoutine from mvesuvio.oop.NeutronComptonProfile import NeutronComptonProfile from mvesuvio.oop.analysis_helpers import loadRawAndEmptyWsFromUserPath, cropAndMaskWorkspace @@ -31,9 +28,7 @@ def _run(cls): ws = loadRawAndEmptyWsFromUserPath( userWsRawPath=str(Path(__file__).absolute().parent.parent.parent/"data"/"analysis"/"inputs"/"sample_test"/"input_ws"/"sample_test_raw_forward.nxs" ), - # userWsRawPath='/home/ljg28444/Documents/user_0/some_new_experiment/some_new_exp/input_ws/some_new_exp_raw_forward.nxs', userWsEmptyPath=str(Path(__file__).absolute().parent.parent.parent/"data"/"analysis"/"inputs"/"sample_test"/"input_ws"/"sample_test_empty_forward.nxs" ), - # userWsEmptyPath='/home/ljg28444/Documents/user_0/some_new_experiment/some_new_exp/input_ws/some_new_exp_empty_forward.nxs', tofBinning = "110.,1.,430", name='exp', scaleRaw=1, @@ -45,7 +40,7 @@ def _run(cls): maskTOFRange=None) AR = AnalysisRoutine(cropedWs, - ip_file='/home/ljg28444/.mvesuvio/ip_files/ip2018_3.par', + ip_file=str(Path(__file__).absolute().parent.parent.parent.parent/"src"/"mvesuvio"/"config"/"ip_files"/'ip2018_3.par'), number_of_iterations=3, mask_spectra=[173, 174, 179], multiple_scattering_correction=True, From ba625de0597078dcbc1b98aceb6351d5e646bd08 Mon Sep 17 00:00:00 2001 From: GuiMacielPereira Date: Wed, 7 Aug 2024 10:33:46 +0100 Subject: [PATCH 13/25] Replace previous routine with new one This work changes analysis_routines.py to be the "glue" between current interface and new oop interface. The idea is to keep the current interface for now and focus on turning the analysis rouinte into a Mantid algorithm. The analysis system tests were changed to account for this re-routing. --- src/mvesuvio/analysis_routines.py | 53 +++++++++++++++++++++++++- tests/system/analysis/test_analysis.py | 17 +++++---- 2 files changed, 61 insertions(+), 9 deletions(-) diff --git a/src/mvesuvio/analysis_routines.py b/src/mvesuvio/analysis_routines.py index aa9283f8..1eb1a8dd 100644 --- a/src/mvesuvio/analysis_routines.py +++ b/src/mvesuvio/analysis_routines.py @@ -1,6 +1,9 @@ -from .analysis_reduction import iterativeFitForDataReduction +# from .analysis_reduction import iterativeFitForDataReduction from mantid.api import AnalysisDataService from mantid.simpleapi import CreateEmptyTableWorkspace +from mvesuvio.oop.analysis_helpers import loadRawAndEmptyWsFromUserPath, cropAndMaskWorkspace +from mvesuvio.oop.AnalysisRoutine import AnalysisRoutine +from mvesuvio.oop.NeutronComptonProfile import NeutronComptonProfile import numpy as np @@ -15,7 +18,53 @@ def runIndependentIterativeProcedure(IC, clearWS=True): if clearWS: AnalysisDataService.clear() - return iterativeFitForDataReduction(IC) + ws = loadRawAndEmptyWsFromUserPath( + userWsRawPath=IC.userWsRawPath, + userWsEmptyPath=IC.userWsEmptyPath, + tofBinning=IC.tofBinning, + name=IC.name, + scaleRaw=IC.scaleRaw, + scaleEmpty=IC.scaleEmpty, + subEmptyFromRaw=IC.subEmptyFromRaw + ) + + cropedWs = cropAndMaskWorkspace( + ws, + firstSpec=IC.firstSpec, + lastSpec=IC.lastSpec, + maskedDetectors=IC.maskedSpecAllNo, + maskTOFRange=IC.maskTOFRange + ) + + AR = AnalysisRoutine( + cropedWs, + ip_file=IC.InstrParsPath, + number_of_iterations=IC.noOfMSIterations, + mask_spectra=IC.maskedSpecAllNo, + multiple_scattering_correction=IC.MSCorrectionFlag, + vertical_width=IC.vertical_width, + horizontal_width=IC.horizontal_width, + thickness=IC.thickness, + gamma_correction=IC.GammaCorrectionFlag, + mode_running=IC.modeRunning, + transmission_guess=IC.transmission_guess, + multiple_scattering_order=IC.multiple_scattering_order, + number_of_events=IC.number_of_events + ) + + # Create Profiles + profiles = [] + for mass, intensity, width, center, intensity_bound, width_bound, center_bound in zip( + IC.masses, IC.initPars[::3], IC.initPars[1::3], IC.initPars[2::3], + IC.bounds[::3], IC.bounds[1::3], IC.bounds[2::3] + ): + profiles.append(NeutronComptonProfile( + str(mass), mass=mass, intensity=intensity, width=width, center=center, + intensity_bounds=intensity_bound, width_bounds=width_bound, center_bounds=center_bound + )) + + AR.add_profiles(*profiles) + return AR.run() def runJointBackAndForwardProcedure(bckwdIC, fwdIC, clearWS=True): diff --git a/tests/system/analysis/test_analysis.py b/tests/system/analysis/test_analysis.py index a0048d6e..69fef16e 100644 --- a/tests/system/analysis/test_analysis.py +++ b/tests/system/analysis/test_analysis.py @@ -47,14 +47,15 @@ def _run(cls): YSpaceFitInitialConditions(), True, ) + AnalysisRunner._currentResults = scattRes + return - wsFinal, forwardScatteringResults = scattRes - - # Test the results - np.set_printoptions(suppress=True, precision=8, linewidth=150) - - currentResults = forwardScatteringResults - AnalysisRunner._currentResults = currentResults + # wsFinal, forwardScatteringResults = scattRes + # + # # Test the results + # + # currentResults = forwardScatteringResults + # AnalysisRunner._currentResults = currentResults @classmethod def _load_benchmark_results(cls): @@ -90,6 +91,8 @@ def setUp(self): self.optwidths = self.optmainPars[:, :, 1::3] self.optcenters = self.optmainPars[:, :, 2::3] + np.set_printoptions(suppress=True, precision=8, linewidth=150) + def test_chi2(self): nptest.assert_almost_equal(self.orichi2, self.optchi2, decimal=6) From aab0f1524f6175742902af816dc3a3d9df399736 Mon Sep 17 00:00:00 2001 From: GuiMacielPereira Date: Thu, 8 Aug 2024 14:25:56 +0100 Subject: [PATCH 14/25] Rewrite routine to link two analysis routines Rewrote routine for chaining two analysis routines together, known as the 'JOINT' procedure. This means that the resulting means and intensities from one analysis routine, usually backscattering, are used as the starting guesses for forward routine, and all widths except the lightest element (usually Hydrogen) are fixed. New procedure using the AnalysisReduction object is easier to read and less prone to bugs. Also checked that figures and result files are saved at correct location. --- src/mvesuvio/analysis_routines.py | 98 ++++----------- src/mvesuvio/oop/AnalysisRoutine.py | 142 ++++++++++++++-------- src/mvesuvio/oop/NeutronComptonProfile.py | 99 ++++++++------- src/mvesuvio/util/process_inputs.py | 8 ++ 4 files changed, 186 insertions(+), 161 deletions(-) diff --git a/src/mvesuvio/analysis_routines.py b/src/mvesuvio/analysis_routines.py index 1eb1a8dd..7cae6a25 100644 --- a/src/mvesuvio/analysis_routines.py +++ b/src/mvesuvio/analysis_routines.py @@ -7,17 +7,7 @@ import numpy as np -def runIndependentIterativeProcedure(IC, clearWS=True): - """ - Runs the iterative fitting of NCP, cleaning any previously stored workspaces. - input: Backward or Forward scattering initial conditions object - output: Final workspace that was fitted, object with results arrays - """ - - # Clear worksapces before running one of the procedures below - if clearWS: - AnalysisDataService.clear() - +def _create_analysis_object_from_current_interface(IC): ws = loadRawAndEmptyWsFromUserPath( userWsRawPath=IC.userWsRawPath, userWsEmptyPath=IC.userWsEmptyPath, @@ -27,7 +17,6 @@ def runIndependentIterativeProcedure(IC, clearWS=True): scaleEmpty=IC.scaleEmpty, subEmptyFromRaw=IC.subEmptyFromRaw ) - cropedWs = cropAndMaskWorkspace( ws, firstSpec=IC.firstSpec, @@ -35,10 +24,10 @@ def runIndependentIterativeProcedure(IC, clearWS=True): maskedDetectors=IC.maskedSpecAllNo, maskTOFRange=IC.maskTOFRange ) - AR = AnalysisRoutine( cropedWs, ip_file=IC.InstrParsPath, + h_ratio_to_lowest_mass=IC.HToMassIdxRatio, number_of_iterations=IC.noOfMSIterations, mask_spectra=IC.maskedSpecAllNo, multiple_scattering_correction=IC.MSCorrectionFlag, @@ -49,21 +38,35 @@ def runIndependentIterativeProcedure(IC, clearWS=True): mode_running=IC.modeRunning, transmission_guess=IC.transmission_guess, multiple_scattering_order=IC.multiple_scattering_order, - number_of_events=IC.number_of_events + number_of_events=IC.number_of_events, + results_path=IC.resultsSavePath, + figures_path=IC.figSavePath ) - - # Create Profiles profiles = [] for mass, intensity, width, center, intensity_bound, width_bound, center_bound in zip( IC.masses, IC.initPars[::3], IC.initPars[1::3], IC.initPars[2::3], IC.bounds[::3], IC.bounds[1::3], IC.bounds[2::3] ): profiles.append(NeutronComptonProfile( - str(mass), mass=mass, intensity=intensity, width=width, center=center, + label=str(mass), mass=mass, intensity=intensity, width=width, center=center, intensity_bounds=intensity_bound, width_bounds=width_bound, center_bounds=center_bound )) - AR.add_profiles(*profiles) + return AR + + +def runIndependentIterativeProcedure(IC, clearWS=True): + """ + Runs the iterative fitting of NCP, cleaning any previously stored workspaces. + input: Backward or Forward scattering initial conditions object + output: Final workspace that was fitted, object with results arrays + """ + + # Clear worksapces before running one of the procedures below + if clearWS: + AnalysisDataService.clear() + + AR = _create_analysis_object_from_current_interface(IC) return AR.run() @@ -191,61 +194,14 @@ def calculateHToMassIdxRatio(fwdScatResults): def runJoint(bckwdIC, fwdIC): - wsFinal, bckwdScatResults = iterativeFitForDataReduction(bckwdIC) - setInitFwdParsFromBackResults(bckwdScatResults, bckwdIC, fwdIC) - wsFinal, fwdScatResults = iterativeFitForDataReduction(fwdIC) - return wsFinal, bckwdScatResults, fwdScatResults - - -def setInitFwdParsFromBackResults(bckwdScatResults, bckwdIC, fwdIC): - """ - Used to pass mean widths and intensities from back scattering onto intial conditions of forward scattering. - Checks if H is present and adjust the passing accordingly: - If H present, use HToMassIdxRatio to recalculate intensities and fix only non-H widths. - If H not present, widths and intensities are directly mapped and all widhts except first are fixed. - """ - # Get widts and intensity ratios from backscattering results - backMeanWidths = bckwdScatResults.all_mean_widths[-1] - backMeanIntensityRatios = bckwdScatResults.all_mean_intensities[-1] - - if isHPresent(fwdIC.masses): - assert len(backMeanWidths) == fwdIC.noOfMasses - 1, ( - "H Mass present, no of masses in frontneeds to be bigger" "than back by 1." - ) - - # Use H ratio to calculate intensity ratios - HIntensity = bckwdIC.HToMassIdxRatio * backMeanIntensityRatios[bckwdIC.massIdx] - # Add H intensity in the first idx - initialFwdIntensityRatios = np.append([HIntensity], backMeanIntensityRatios) - # Normalize intensities - initialFwdIntensityRatios /= np.sum(initialFwdIntensityRatios) - - # Set calculated intensity ratios to forward scattering - fwdIC.initPars[0::3] = initialFwdIntensityRatios - # Set forward widths from backscattering - fwdIC.initPars[4::3] = backMeanWidths - # Fix all widths except for H, i.e. the first one - fwdIC.bounds[4::3] = backMeanWidths[:, np.newaxis] * np.ones((1, 2)) - - else: # H mass not present anywhere - assert len(backMeanWidths) == fwdIC.noOfMasses, ( - "H Mass not present, no of masses needs to be the same for" - "front and back scattering." - ) - - # Set widths and intensity ratios - fwdIC.initPars[1::3] = backMeanWidths - fwdIC.initPars[0::3] = backMeanIntensityRatios - - if len(backMeanWidths) > 1: # In the case of single mass, width is not fixed - # Fix all widhts except first - fwdIC.bounds[4::3] = backMeanWidths[1:][:, np.newaxis] * np.ones((1, 2)) + backRoutine = _create_analysis_object_from_current_interface(bckwdIC) + frontRoutine = _create_analysis_object_from_current_interface(fwdIC) - print( - "\nChanged initial conditions of forward scattering according to mean widhts and intensity ratios from " - "backscattering.\n" - ) + backRoutine.run() + frontRoutine.set_initial_profiles_from(backRoutine) + print("\nCHANGED STARTING POINT OF PROFILES\n") + frontRoutine.run() return diff --git a/src/mvesuvio/oop/AnalysisRoutine.py b/src/mvesuvio/oop/AnalysisRoutine.py index c40dac4f..15394a5a 100644 --- a/src/mvesuvio/oop/AnalysisRoutine.py +++ b/src/mvesuvio/oop/AnalysisRoutine.py @@ -9,15 +9,16 @@ CreateWorkspace from mvesuvio.analysis_fitting import passDataIntoWS, replaceZerosWithNCP import numpy as np +import matplotlib.pyplot as plt from scipy import optimize import sys class AnalysisRoutine: - def __init__(self, workspace, ip_file, number_of_iterations, mask_spectra, + def __init__(self, workspace, ip_file, h_ratio_to_lowest_mass, number_of_iterations, mask_spectra, multiple_scattering_correction, vertical_width, horizontal_width, thickness, gamma_correction, mode_running, transmission_guess=None, multiple_scattering_order=None, - number_of_events=None, results_path=None): + number_of_events=None, results_path=None, figures_path=None): self._name = workspace.name() + '_' self._ip_file = ip_file @@ -38,6 +39,7 @@ def __init__(self, workspace, ip_file, number_of_iterations, mask_spectra, self._gamma_correction = gamma_correction self._save_results_path = results_path + self._save_figures_path = figures_path # Variables changing during fit self._workspace_for_corrections = workspace @@ -47,7 +49,8 @@ def __init__(self, workspace, ip_file, number_of_iterations, mask_spectra, self._table_fit_results = None self._fit_profiles_workspaces = {} - self._h_ratio = None + self._h_ratio = h_ratio_to_lowest_mass + #TODO: Code way of implementing constraints in fit self._constraints = () self._profiles = {} @@ -55,38 +58,55 @@ def __init__(self, workspace, ip_file, number_of_iterations, mask_spectra, self._run_hist_data = True self._run_norm_voigt = False - # Links to another AnalysisRoutine object: - self._profiles_destination = None - self._h_ratio_destination = None - def add_profiles(self, *args: NeutronComptonProfile): for profile in args: self._profiles[profile.label] = profile - def add_constraint(self, constraint_string: str): - self._constraints.append(constraint_string) + @property + def profiles(self): + return self._profiles + def set_initial_profiles_from(self, source: 'AnalysisRoutine'): + + # Set intensities + for p in self._profiles.values(): + if np.isclose(p.mass, 1, atol=0.1): # Hydrogen present + p.intensity = source._h_ratio * source._get_lightest_profile().mean_intensity + continue + p.intensity = source.profiles[p.label].mean_intensity - def add_h_ratio_to_next_lowest_mass(self, ratio: float): - self._h_ratio_to_next_lowest_mass = ratio + # Normalise intensities + sum_intensities = sum([p.intensity for p in self._profiles.values()]) + for p in self._profiles.values(): + p.intensity /= sum_intensities + + # Set widths + for p in self._profiles.values(): + try: + p.width = source.profiles[p.label].mean_width + except KeyError: + continue + # Fix all widths except lightest mass + for p in self._profiles.values(): + if p == self._get_lightest_profile(): + continue + p.width_bounds = [p.width, p.width] - def send_ncp_fit_parameters(self): - self._profiles_destination.profiles = self.profiles + return - def send_h_ratio(self): - self._h_ratio_destination.h_ratio_to_next_lowest_mass = self._h_ratio + def _get_lightest_profile(self): + profiles = [p for p in self._profiles.values()] + masses = [p.mass for p in self._profiles.values()] + return profiles[np.argmin(masses)] - @property - def h_ratio_to_next_lowest_mass(self): - return self._h_ratio - @h_ratio_to_next_lowest_mass.setter - def h_ratio_to_next_lowest_mass(self, value): - self.h_ratio_to_next_lowest_mass = value + def add_constraint(self, constraint_string: str): + self._constraints.append(constraint_string) + @property def profiles(self): @@ -105,9 +125,6 @@ def _set_const_methods(self): self._masses = np.array([p.mass for p in self._profiles.values()]) - self._fig_save_path = None - - def _update_workspace_data(self): self._dataX, self._dataY, self._dataE = extractWS(self._workspace_being_fit) @@ -126,10 +143,10 @@ def _update_workspace_data(self): ) table.setTitle("SciPy Fit Parameters") table.addColumn(type="float", name="Spectrum") - for p in self._profiles.values(): - table.addColumn(type="float", name=f"{p.label} Intensity") - table.addColumn(type="float", name=f"{p.label} Width") - table.addColumn(type="float", name=f"{p.label} Center ") + for key in self._profiles.keys(): + table.addColumn(type="float", name=f"{key} Intensity") + table.addColumn(type="float", name=f"{key} Width") + table.addColumn(type="float", name=f"{key} Center ") table.addColumn(type="float", name="Normalised Chi2") table.addColumn(type="float", name="Number of Iteraions") @@ -141,20 +158,43 @@ def _update_workspace_data(self): self._fit_profiles_workspaces[element] = self._create_emtpy_ncp_workspace(f'_{element}_ncp') self._fit_profiles_workspaces['total'] = self._create_emtpy_ncp_workspace(f'_total_ncp') - self._mean_widths = None - self._std_widths = None - self._mean_intensity_ratios = None - self._std_intensity_ratios = None + self._mean_widths = None + self._std_widths = None + self._mean_intensity_ratios = None + self._std_intensity_ratios = None + + @property + def mean_widths(self): + return self._mean_widths + + @mean_widths.setter + def mean_widths(self, value): + self._mean_widths = value + for i, p in enumerate(self._profiles.values()): + p.mean_width = self._mean_widths[i] + return + + + @property + def mean_intensity_ratios(self): + return self._mean_intensity_ratios + + @mean_intensity_ratios.setter + def mean_intensity_ratios(self, value): + self._mean_intensity_ratios = value + for i, p in enumerate(self.profiles.values()): + p.mean_intensity = self._mean_intensity_ratios[i] + return def _create_emtpy_ncp_workspace(self, suffix): return CreateWorkspace( - DataX=self._dataX, - DataY=np.zeros(self._dataY.size), - DataE=np.zeros(self._dataE.size), - Nspec=self._workspace_being_fit.getNumberHistograms(), - OutputWorkspace=self._workspace_being_fit.name()+suffix, - ParentWorkspace=self._workspace_being_fit + DataX=self._dataX, + DataY=np.zeros(self._dataY.size), + DataE=np.zeros(self._dataE.size), + Nspec=self._workspace_being_fit.getNumberHistograms(), + OutputWorkspace=self._workspace_being_fit.name()+suffix, + ParentWorkspace=self._workspace_being_fit ) @@ -193,6 +233,7 @@ def run(self): self._fit_neutron_compton_profiles() self._create_summed_workspaces() + self._save_plots() self._set_means_and_std() # When last iteration, skip MS and GC @@ -346,28 +387,31 @@ def convertDataXToYSpacesForEachMass(self, dataX, delta_Q, delta_E): return ySpacesForEachMass - def save_plots(self, wsDataSum, wsTotNCPSum, wsMNCPSum): + def _save_plots(self): # if IC.runningSampleWS: # Skip saving figure if running bootstrap # return - if not self._fig_save_path: + if not self._save_figures_path: return + lw = 2 fig, ax = plt.subplots(subplot_kw={"projection": "mantid"}) - ax.errorbar(wsDataSum, "k.", label="Spectra") - ax.plot(wsTotNCPSum, "r-", label="Total NCP", linewidth=lw) - for m, wsNcp in zip(self._masses, wsMNCPSum): - ax.plot(wsNcp, label=f"NCP m={m}", linewidth=lw) + ws_data_sum = mtd[self._workspace_being_fit.name()+"_Sum"] + ax.errorbar(ws_data_sum, fmt="k.", label="Sum of spectra") + + for key, ws in self._fit_profiles_workspaces.items(): + ws_sum = mtd[ws.name()+"_Sum"] + ax.plot(ws_sum, label=f'Sum of {key} profile', linewidth=lw) ax.set_xlabel("TOF") ax.set_ylabel("Counts") ax.set_title("Sum of NCP fits") ax.legend() - fileName = wsDataSum.name() + "_NCP_Fits.pdf" - savePath = self._fig_save_path / fileName + fileName = self._workspace_being_fit.name() + "_profiles_sum.pdf" + savePath = self._save_figures_path / fileName plt.savefig(savePath, bbox_inches="tight") plt.close(fig) return @@ -405,9 +449,9 @@ def _set_means_and_std(self): len(meanWidths) == len(self._profiles) ), "Number of mean widths must match number of profiles!" - self._mean_widths = meanWidths + self.mean_widths = meanWidths # Use setter self._std_widths = stdWidths - self._mean_intensity_ratios = meanIntensityRatios + self.mean_intensity_ratios = meanIntensityRatios # Use setter self._std_intensity_ratios = stdIntensityRatios self.createMeansAndStdTableWS() @@ -519,7 +563,7 @@ def _fit_neutron_compton_profiles_to_row(self): for p in self._profiles.values(): for attr in ['intensity', 'width', 'center']: initial_parameters.append(getattr(p, attr)) - for attr in ['_intensity_bounds', '_width_bounds', '_center_bounds']: + for attr in ['intensity_bounds', 'width_bounds', 'center_bounds']: bounds.append(getattr(p, attr)) result = optimize.minimize( diff --git a/src/mvesuvio/oop/NeutronComptonProfile.py b/src/mvesuvio/oop/NeutronComptonProfile.py index 31868b06..e6db7ca9 100644 --- a/src/mvesuvio/oop/NeutronComptonProfile.py +++ b/src/mvesuvio/oop/NeutronComptonProfile.py @@ -1,48 +1,65 @@ +from dataclasses import dataclass - - +@dataclass(frozen=False) class NeutronComptonProfile: + label: str + mass: float - def __init__(self, label, mass, intensity, width, center, - intensity_bounds, width_bounds, center_bounds): - self._mass = mass - self._label = label - self._intensity = intensity - self._width = width - self._center = center - self._intensity_bounds = intensity_bounds - self._width_bounds = width_bounds - self._center_bounds = center_bounds - - @property - def label(self): - return self._label - - @property - def mass(self): - return self._mass - - @property - def width(self): - return self._width - - @property - def intensity(self): - return self._intensity - - @property - def center(self): - return self._center + intensity: float + width: float + center: float - @property - def width_bounds(self): - return self._width_bounds + intensity_bounds: tuple[float, float] + width_bounds: tuple[float, float] + center_bounds: tuple[float, float] - @property - def intensity_bounds(self): - return self._intensity_bounds + mean_intensity: float = None + mean_width: float = None + mean_center: float = None - @property - def center_bounds(self): - return self._center_bounds +# class NeutronComptonProfile: +# +# def __init__(self, label, mass, intensity, width, center, +# intensity_bounds, width_bounds, center_bounds): +# self._mass = mass +# self._label = label +# self._intensity = intensity +# self._width = width +# self._center = center +# self._intensity_bounds = intensity_bounds +# self._width_bounds = width_bounds +# self._center_bounds = center_bounds +# +# @property +# def label(self): +# return self._label +# +# @property +# def mass(self): +# return self._mass +# +# @property +# def width(self): +# return self._width +# +# @property +# def intensity(self): +# return self._intensity +# +# @property +# def center(self): +# return self._center +# +# @property +# def width_bounds(self): +# return self._width_bounds +# +# @property +# def intensity_bounds(self): +# return self._intensity_bounds +# +# @property +# def center_bounds(self): +# return self._center_bounds +# diff --git a/src/mvesuvio/util/process_inputs.py b/src/mvesuvio/util/process_inputs.py index 0a6d7411..7c0cb9a8 100644 --- a/src/mvesuvio/util/process_inputs.py +++ b/src/mvesuvio/util/process_inputs.py @@ -81,6 +81,14 @@ def completeICFromInputs(IC, wsIC): except AttributeError: IC.normVoigt = True + #Create default for H ratio + # Only for completeness sake, will be removed anyway + # when transition to new interface is complete + try: + IC.HToMassIdxRatio + except AttributeError: + IC.HToMassIdxRatio = None + return From 7f9f2a10325141fe081806caa298297f3f1af14e Mon Sep 17 00:00:00 2001 From: GuiMacielPereira Date: Thu, 8 Aug 2024 15:39:09 +0100 Subject: [PATCH 15/25] Removed system test that was testing old interface The new AanalysisReduction class is already being tested by test_analysis.py so the removed test was redundant. --- src/mvesuvio/analysis_routines.py | 1 - src/mvesuvio/oop/AnalysisRoutine.py | 2 +- tests/system/analysis/test_new_analysis.py | 179 --------------------- 3 files changed, 1 insertion(+), 181 deletions(-) delete mode 100644 tests/system/analysis/test_new_analysis.py diff --git a/src/mvesuvio/analysis_routines.py b/src/mvesuvio/analysis_routines.py index 7cae6a25..ca8fb37c 100644 --- a/src/mvesuvio/analysis_routines.py +++ b/src/mvesuvio/analysis_routines.py @@ -200,7 +200,6 @@ def runJoint(bckwdIC, fwdIC): backRoutine.run() frontRoutine.set_initial_profiles_from(backRoutine) - print("\nCHANGED STARTING POINT OF PROFILES\n") frontRoutine.run() return diff --git a/src/mvesuvio/oop/AnalysisRoutine.py b/src/mvesuvio/oop/AnalysisRoutine.py index 15394a5a..709df7ac 100644 --- a/src/mvesuvio/oop/AnalysisRoutine.py +++ b/src/mvesuvio/oop/AnalysisRoutine.py @@ -869,7 +869,7 @@ def calcMSCorrectionSampleProperties(self, meanWidths, meanIntensityRatios): masses = np.append(masses, 1.0079) meanWidths = np.append(meanWidths, 5.0) - HIntensity = self._h_ratio * meanIntensityRatios[np.argmin(self._masses)] + HIntensity = self._h_ratio * meanIntensityRatios[np.argmin(masses)] meanIntensityRatios = np.append(meanIntensityRatios, HIntensity) meanIntensityRatios /= np.sum(meanIntensityRatios) diff --git a/tests/system/analysis/test_new_analysis.py b/tests/system/analysis/test_new_analysis.py deleted file mode 100644 index b343ba37..00000000 --- a/tests/system/analysis/test_new_analysis.py +++ /dev/null @@ -1,179 +0,0 @@ -import unittest -import numpy as np -import numpy.testing as nptest -from pathlib import Path -from mvesuvio.util import handle_config -from mvesuvio.oop.AnalysisRoutine import AnalysisRoutine -from mvesuvio.oop.NeutronComptonProfile import NeutronComptonProfile -from mvesuvio.oop.analysis_helpers import loadRawAndEmptyWsFromUserPath, cropAndMaskWorkspace - -class AnalysisRunner: - _benchmarkResults = None - _currentResults = None - - @classmethod - def get_benchmark_result(cls): - if not AnalysisRunner._benchmarkResults: - cls._load_benchmark_results() - return AnalysisRunner._benchmarkResults - - @classmethod - def get_current_result(cls): - if not AnalysisRunner._currentResults: - cls._run() - return AnalysisRunner._currentResults - - @classmethod - def _run(cls): - - ws = loadRawAndEmptyWsFromUserPath( - userWsRawPath=str(Path(__file__).absolute().parent.parent.parent/"data"/"analysis"/"inputs"/"sample_test"/"input_ws"/"sample_test_raw_forward.nxs" ), - userWsEmptyPath=str(Path(__file__).absolute().parent.parent.parent/"data"/"analysis"/"inputs"/"sample_test"/"input_ws"/"sample_test_empty_forward.nxs" ), - tofBinning = "110.,1.,430", - name='exp', - scaleRaw=1, - scaleEmpty=1, - subEmptyFromRaw=False - ) - cropedWs = cropAndMaskWorkspace(ws, firstSpec=144, lastSpec=182, - maskedDetectors=[173, 174, 179], - maskTOFRange=None) - - AR = AnalysisRoutine(cropedWs, - ip_file=str(Path(__file__).absolute().parent.parent.parent.parent/"src"/"mvesuvio"/"config"/"ip_files"/'ip2018_3.par'), - number_of_iterations=3, - mask_spectra=[173, 174, 179], - multiple_scattering_correction=True, - vertical_width=0.1, horizontal_width=0.1, thickness=0.001, - transmission_guess=0.8537, - multiple_scattering_order=2, - number_of_events=1.0e5, - gamma_correction=True, - mode_running='FORWARD') - - H = NeutronComptonProfile('H', mass=1.0079, intensity=1, width=4.7, center=0, - intensity_bounds=[0, np.nan], width_bounds=[3, 6], center_bounds=[-3, 1]) - C = NeutronComptonProfile('C', mass=12, intensity=1, width=12.71, center=0, - intensity_bounds=[0, np.nan], width_bounds=[12.71, 12.71], center_bounds=[-3, 1]) - S = NeutronComptonProfile('S', mass=16, intensity=1, width=8.76, center=0, - intensity_bounds=[0, np.nan], width_bounds=[8.76, 8.76], center_bounds=[-3, 1]) - Co = NeutronComptonProfile('Co', mass=27, intensity=1, width=13.897, center=0, - intensity_bounds=[0, np.nan], width_bounds=[13.897, 13.897], center_bounds=[-3, 1]) - - AR.add_profiles(H, C, S, Co) - AnalysisRunner._currentResults = AR.run() - - np.set_printoptions(suppress=True, precision=8, linewidth=150) - - - @classmethod - def _load_benchmark_results(cls): - benchmarkPath = Path(__file__).absolute().parent.parent.parent / "data" / "analysis" / "benchmark" - benchmarkResults = np.load( - str(benchmarkPath / "stored_spec_144-182_iter_3_GC_MS.npz") - ) - AnalysisRunner._benchmarkResults = benchmarkResults - - -class TestFitParameters(unittest.TestCase): - @classmethod - def setUpClass(cls): - cls.benchmarkResults = AnalysisRunner.get_benchmark_result() - cls.currentResults = AnalysisRunner.get_current_result() - - def setUp(self): - oriPars = self.benchmarkResults["all_spec_best_par_chi_nit"] - self.orispec = oriPars[:, :, 0] - self.orichi2 = oriPars[:, :, -2] - self.orinit = oriPars[:, :, -1] - self.orimainPars = oriPars[:, :, 1:-2] - self.oriintensities = self.orimainPars[:, :, 0::3] - self.oriwidths = self.orimainPars[:, :, 1::3] - self.oricenters = self.orimainPars[:, :, 2::3] - - optPars = self.currentResults.all_spec_best_par_chi_nit - self.optspec = optPars[:, :, 0] - self.optchi2 = optPars[:, :, -2] - self.optnit = optPars[:, :, -1] - self.optmainPars = optPars[:, :, 1:-2] - self.optintensities = self.optmainPars[:, :, 0::3] - self.optwidths = self.optmainPars[:, :, 1::3] - self.optcenters = self.optmainPars[:, :, 2::3] - - def test_chi2(self): - nptest.assert_almost_equal(self.orichi2, self.optchi2, decimal=6) - - def test_nit(self): - nptest.assert_almost_equal(self.orinit, self.optnit, decimal=-2) - - def test_intensities(self): - nptest.assert_almost_equal(self.oriintensities, self.optintensities, decimal=2) - - def test_widths(self): - nptest.assert_almost_equal(self.oriwidths, self.optwidths, decimal=2) - - def test_centers(self): - nptest.assert_almost_equal(self.oricenters, self.optcenters, decimal=1) - - -class TestNcp(unittest.TestCase): - @classmethod - def setUpClass(cls): - cls.benchmarkResults = AnalysisRunner.get_benchmark_result() - cls.currentResults = AnalysisRunner.get_current_result() - - def setUp(self): - self.orincp = self.benchmarkResults["all_tot_ncp"][:, :, :-1] - - self.optncp = self.currentResults.all_tot_ncp - - def test_ncp(self): - correctNansOri = np.where( - (self.orincp == 0) & np.isnan(self.optncp), np.nan, self.orincp - ) - nptest.assert_almost_equal(correctNansOri, self.optncp, decimal=4) - - -class TestMeanWidths(unittest.TestCase): - @classmethod - def setUpClass(cls): - cls.benchmarkResults = AnalysisRunner.get_benchmark_result() - cls.currentResults = AnalysisRunner.get_current_result() - - def setUp(self): - self.orimeanwidths = self.benchmarkResults["all_mean_widths"] - self.optmeanwidths = self.currentResults.all_mean_widths - - def test_widths(self): - nptest.assert_almost_equal(self.orimeanwidths, self.optmeanwidths, decimal=5) - - -class TestMeanIntensities(unittest.TestCase): - @classmethod - def setUpClass(cls): - cls.benchmarkResults = AnalysisRunner.get_benchmark_result() - cls.currentResults = AnalysisRunner.get_current_result() - - def setUp(self): - self.orimeanintensities = self.benchmarkResults["all_mean_intensities"] - self.optmeanintensities = self.currentResults.all_mean_intensities - - def test_intensities(self): - nptest.assert_almost_equal(self.orimeanintensities, self.optmeanintensities, decimal=6) - - -class TestFitWorkspaces(unittest.TestCase): - @classmethod - def setUpClass(cls): - cls.benchmarkResults = AnalysisRunner.get_benchmark_result() - cls.currentResults = AnalysisRunner.get_current_result() - - def setUp(self): - self.oriws = self.benchmarkResults["all_fit_workspaces"] - self.optws = self.currentResults.all_fit_workspaces - - def test_ws(self): - nptest.assert_almost_equal(self.oriws, self.optws, decimal=6) - -if __name__ == "__main__": - unittest.main() From 6a66e5b8541dd4d42fd17840d86d2d6c0cc3742b Mon Sep 17 00:00:00 2001 From: GuiMacielPereira Date: Fri, 9 Aug 2024 09:55:55 +0100 Subject: [PATCH 16/25] Clean procedure to estimate H ratio Updated procedure to estimate H ratio to use new object of AnalysisReduction. The ratio is set to always be in relation to the lowest mass in the sample. --- src/mvesuvio/analysis_routines.py | 122 ++++++---------------- src/mvesuvio/oop/AnalysisRoutine.py | 17 ++- src/mvesuvio/oop/NeutronComptonProfile.py | 47 --------- src/mvesuvio/run_routine.py | 13 +-- 4 files changed, 53 insertions(+), 146 deletions(-) diff --git a/src/mvesuvio/analysis_routines.py b/src/mvesuvio/analysis_routines.py index ca8fb37c..ff07837f 100644 --- a/src/mvesuvio/analysis_routines.py +++ b/src/mvesuvio/analysis_routines.py @@ -92,105 +92,51 @@ def runPreProcToEstHRatio(bckwdIC, fwdIC): Runs iterative procedure with alternating back and forward scattering. """ - assert ( - bckwdIC.runningSampleWS is False - ), "Preliminary procedure not suitable for Bootstrap." - fwdIC.runningPreliminary = True - - # Store original no of MS and set MS iterations to zero - oriMS = [] - for IC in [bckwdIC, fwdIC]: - oriMS.append(IC.noOfMSIterations) - IC.noOfMSIterations = 0 - - nIter = askUserNoOfIterations() - - HRatios = [] # List to store HRatios - massIdxs = [] - # Run preliminary forward with a good guess for the widths of non-H masses - wsFinal, fwdScatResults = iterativeFitForDataReduction(fwdIC) - for i in range(int(nIter)): # Loop until convergence is achieved - AnalysisDataService.clear() # Clears all Workspaces - - # Update H ratio - massIdx, HRatio = calculateHToMassIdxRatio(fwdScatResults) - bckwdIC.HToMassIdxRatio = HRatio - bckwdIC.massIdx = massIdx - HRatios.append(HRatio) - massIdxs.append(massIdx) - - wsFinal, bckwdScatResults, fwdScatResults = runJoint(bckwdIC, fwdIC) - - print(f"\nIdxs of masses for H ratio for each iteration: \n{massIdxs}") - print(f"\nCorresponding H ratios: \n{HRatios}") - - fwdIC.runningPreliminary = ( - False # Change to default since end of preliminary procedure - ) - - # Set original number of MS iterations - for IC, ori in zip([bckwdIC, fwdIC], oriMS): - IC.noOfMSIterations = ori - - # Update the H ratio with the best estimate, chages bckwdIC outside function - massIdx, HRatio = calculateHToMassIdxRatio(fwdScatResults) - bckwdIC.HToMassIdxRatio = HRatio - bckwdIC.massIdx = massIdx - HRatios.append(HRatio) - massIdxs.append(massIdx) - - return HRatios, massIdxs - + # assert ( + # bckwdIC.runningSampleWS is False + # ), "Preliminary procedure not suitable for Bootstrap." + # fwdIC.runningPreliminary = True -def createTableWSHRatios(HRatios, massIdxs): - tableWS = CreateEmptyTableWorkspace( - OutputWorkspace="H_Ratios_From_Preliminary_Procedure" - ) - tableWS.setTitle("H Ratios and Idxs at each iteration") - tableWS.addColumn(type="int", name="iter") - tableWS.addColumn(type="float", name="H Ratio") - tableWS.addColumn(type="int", name="Mass Idx") - for i, (hr, hi) in enumerate(zip(HRatios, massIdxs)): - tableWS.addRow([i, hr, hi]) - return - - -def askUserNoOfIterations(): - print("\nH was detected but HToMassIdxRatio was not provided.") - print( - "\nSugested preliminary procedure:\n\nrun_forward\nfor n:\n estimate_HToMassIdxRatio\n run_backward\n" - " run_forward" - ) userInput = input( - "\n\nDo you wish to run preliminary procedure to estimate HToMassIdxRatio? (y/n)" + "\nHydrogen intensity ratio to lowest mass is not set. Run procedure to estimate it?" ) if not ((userInput == "y") or (userInput == "Y")): - raise KeyboardInterrupt("Preliminary procedure interrupted.") + raise KeyboardInterrupt("Procedure interrupted.") - nIter = int(input("\nHow many iterations do you wish to run? n=")) - return nIter + table_h_ratios = createTableWSHRatios() + backRoutine = _create_analysis_object_from_current_interface(bckwdIC) + frontRoutine = _create_analysis_object_from_current_interface(fwdIC) -def calculateHToMassIdxRatio(fwdScatResults): - """ - Calculate H ratio to mass with highest peak. - Returns idx of mass and corresponding H ratio. - """ - fwdMeanIntensityRatios = fwdScatResults.all_mean_intensities[-1] + frontRoutine.run() + current_ratio = frontRoutine.calculate_h_ratio() + table_h_ratios.addRow([current_ratio]) + previous_ratio = np.nan - # To find idx of mass in backward scattering, take out first mass H - fwdIntensitiesNoH = fwdMeanIntensityRatios[1:] + while not np.isclose(current_ratio, previous_ratio, rtol=0.01): - massIdx = np.argmax( - fwdIntensitiesNoH - ) # Idex of forward inensities, which include H - assert ( - fwdIntensitiesNoH[massIdx] != 0 - ), "Cannot estimate H intensity since maximum peak from backscattering is zero." + backRoutine._h_ratio = current_ratio + backRoutine.run() + frontRoutine.set_initial_profiles_from(backRoutine) + frontRoutine.run() - HRatio = fwdMeanIntensityRatios[0] / fwdIntensitiesNoH[massIdx] + previous_ratio = current_ratio + current_ratio = frontRoutine.calculate_h_ratio() - return massIdx, HRatio + table_h_ratios.addRow([current_ratio]) + + print("\nProcedute to estimate Hydrogen ratio finished.", + "\nEstimates at each iteration converged:", + f"\n{table_h_ratios.column(0)}") + return + + +def createTableWSHRatios(): + table = CreateEmptyTableWorkspace( + OutputWorkspace="H_Ratios_Estimates" + ) + table.addColumn(type="float", name="H Ratio to lowest mass at each iteration") + return table def runJoint(bckwdIC, fwdIC): diff --git a/src/mvesuvio/oop/AnalysisRoutine.py b/src/mvesuvio/oop/AnalysisRoutine.py index 709df7ac..c998ab80 100644 --- a/src/mvesuvio/oop/AnalysisRoutine.py +++ b/src/mvesuvio/oop/AnalysisRoutine.py @@ -20,7 +20,7 @@ def __init__(self, workspace, ip_file, h_ratio_to_lowest_mass, number_of_iterati gamma_correction, mode_running, transmission_guess=None, multiple_scattering_order=None, number_of_events=None, results_path=None, figures_path=None): - self._name = workspace.name() + '_' + self._name = workspace.name() self._ip_file = ip_file self._number_of_iterations = number_of_iterations spectrum_list = workspace.getSpectrumNumbers() @@ -108,6 +108,19 @@ def add_constraint(self, constraint_string: str): self._constraints.append(constraint_string) + def calculate_h_ratio(self): + + if not np.isclose(self._get_lightest_profile().mass, 1, atol=0.1): # Hydrogen present + return None + + # Hydrogen is present + intensities = np.array([p.mean_intensity for p in self._profiles.values()]) + masses = np.array([p.mass for p in self._profiles.values()]) + + sorted_intensities = intensities[np.argsort(masses)] + return sorted_intensities[0] / sorted_intensities[1] + + @property def profiles(self): return self._profiles @@ -220,7 +233,7 @@ def run(self): # InputWorkspace=ic.sampleWS, OutputWorkspace=initialWs.name() # ) - RenameWorkspace( + CloneWorkspace( InputWorkspace=self._workspace_being_fit.name(), OutputWorkspace=self._name + '0' ) diff --git a/src/mvesuvio/oop/NeutronComptonProfile.py b/src/mvesuvio/oop/NeutronComptonProfile.py index e6db7ca9..77c17333 100644 --- a/src/mvesuvio/oop/NeutronComptonProfile.py +++ b/src/mvesuvio/oop/NeutronComptonProfile.py @@ -16,50 +16,3 @@ class NeutronComptonProfile: mean_intensity: float = None mean_width: float = None mean_center: float = None - - -# class NeutronComptonProfile: -# -# def __init__(self, label, mass, intensity, width, center, -# intensity_bounds, width_bounds, center_bounds): -# self._mass = mass -# self._label = label -# self._intensity = intensity -# self._width = width -# self._center = center -# self._intensity_bounds = intensity_bounds -# self._width_bounds = width_bounds -# self._center_bounds = center_bounds -# -# @property -# def label(self): -# return self._label -# -# @property -# def mass(self): -# return self._mass -# -# @property -# def width(self): -# return self._width -# -# @property -# def intensity(self): -# return self._intensity -# -# @property -# def center(self): -# return self._center -# -# @property -# def width_bounds(self): -# return self._width_bounds -# -# @property -# def intensity_bounds(self): -# return self._intensity_bounds -# -# @property -# def center_bounds(self): -# return self._center_bounds -# diff --git a/src/mvesuvio/run_routine.py b/src/mvesuvio/run_routine.py index 65a84282..591ba89b 100644 --- a/src/mvesuvio/run_routine.py +++ b/src/mvesuvio/run_routine.py @@ -34,13 +34,12 @@ def runProcedure(): if proc is None: return - ranPreliminary = False if (proc == "BACKWARD") | (proc == "JOINT"): + if isHPresent(fwdIC.masses) & (bckwdIC.HToMassIdxRatio is None): - HRatios, massIdxs = runPreProcToEstHRatio( - bckwdIC, fwdIC - ) # Sets H ratio to bckwdIC automatically - ranPreliminary = True + runPreProcToEstHRatio(bckwdIC, fwdIC) + return + assert isHPresent(fwdIC.masses) != ( bckwdIC.HToMassIdxRatio is None ), "When H is not present, HToMassIdxRatio has to be set to None" @@ -51,10 +50,6 @@ def runProcedure(): res = runIndependentIterativeProcedure(fwdIC) if proc == "JOINT": res = runJointBackAndForwardProcedure(bckwdIC, fwdIC) - - # If preliminary procedure ran, make TableWS with H ratios values - if ranPreliminary: - createTableWSHRatios(HRatios, massIdxs) return res # Names of workspaces to be fitted in y space From ea4498ff7c347b3274c14f3a1f5d9d05c35de797 Mon Sep 17 00:00:00 2001 From: GuiMacielPereira Date: Fri, 9 Aug 2024 11:52:22 +0100 Subject: [PATCH 17/25] Add constraints I added two functions to handle constraints with the new OOP interface, but promptly forgot that they are not necessary in the current interface as it is, so I commented them out for now, to be used in the future when the shift towards using the new interface happens --- src/mvesuvio/analysis_routines.py | 3 +- src/mvesuvio/oop/AnalysisRoutine.py | 77 +++++++++++++++++++---------- 2 files changed, 54 insertions(+), 26 deletions(-) diff --git a/src/mvesuvio/analysis_routines.py b/src/mvesuvio/analysis_routines.py index ff07837f..7079b681 100644 --- a/src/mvesuvio/analysis_routines.py +++ b/src/mvesuvio/analysis_routines.py @@ -40,7 +40,8 @@ def _create_analysis_object_from_current_interface(IC): multiple_scattering_order=IC.multiple_scattering_order, number_of_events=IC.number_of_events, results_path=IC.resultsSavePath, - figures_path=IC.figSavePath + figures_path=IC.figSavePath, + constraints=IC.constraints ) profiles = [] for mass, intensity, width, center, intensity_bound, width_bound, center_bound in zip( diff --git a/src/mvesuvio/oop/AnalysisRoutine.py b/src/mvesuvio/oop/AnalysisRoutine.py index c998ab80..bf488d40 100644 --- a/src/mvesuvio/oop/AnalysisRoutine.py +++ b/src/mvesuvio/oop/AnalysisRoutine.py @@ -18,7 +18,7 @@ class AnalysisRoutine: def __init__(self, workspace, ip_file, h_ratio_to_lowest_mass, number_of_iterations, mask_spectra, multiple_scattering_correction, vertical_width, horizontal_width, thickness, gamma_correction, mode_running, transmission_guess=None, multiple_scattering_order=None, - number_of_events=None, results_path=None, figures_path=None): + number_of_events=None, results_path=None, figures_path=None, constraints=()): self._name = workspace.name() self._ip_file = ip_file @@ -50,8 +50,7 @@ def __init__(self, workspace, ip_file, h_ratio_to_lowest_mass, number_of_iterati self._fit_profiles_workspaces = {} self._h_ratio = h_ratio_to_lowest_mass - #TODO: Code way of implementing constraints in fit - self._constraints = () + self._constraints = constraints self._profiles = {} # Only used for system tests, remove once tests are updated @@ -104,10 +103,6 @@ def _get_lightest_profile(self): return profiles[np.argmin(masses)] - def add_constraint(self, constraint_string: str): - self._constraints.append(constraint_string) - - def calculate_h_ratio(self): if not np.isclose(self._get_lightest_profile().mass, 1, atol=0.1): # Hydrogen present @@ -125,6 +120,7 @@ def calculate_h_ratio(self): def profiles(self): return self._profiles + @profiles.setter def profiles(self, incoming_profiles): assert(isinstance(incoming_profiles, dict)) @@ -133,11 +129,6 @@ def profiles(self, incoming_profiles): self._profiles = {**self._profiles, **common_keys_profiles} - def _set_const_methods(self): - # Set up variables used during fitting - self._masses = np.array([p.mass for p in self._profiles.values()]) - - def _update_workspace_data(self): self._dataX, self._dataY, self._dataE = extractWS(self._workspace_being_fit) @@ -223,9 +214,7 @@ def run(self): assert len(self.profiles) > 0, "Add profiles before atempting to run the routine!" - self._set_const_methods() - - self.createTableInitialParameters() + self._create_table_initial_parameters() # Legacy code from Bootstrap # if self.runningSampleWS: @@ -262,6 +251,8 @@ def run(self): OutputWorkspace="next_iteration" ) self._correct_for_gamma_and_multiple_scattering("next_iteration") + + # Need to remask columns of output of corrections self._remask_columns_with_zeros("next_iteration") RenameWorkspace( @@ -270,11 +261,11 @@ def run(self): ) self._set_results() - self.save_results() + self._save_results() return self - def createTableInitialParameters(self): + def _create_table_initial_parameters(self): meansTableWS = CreateEmptyTableWorkspace( OutputWorkspace=self._name + "_Initial_Parameters" ) @@ -296,6 +287,7 @@ def createTableInitialParameters(self): print(f"{'Initial Width:':>20s} {p.width:<8.3f} Bounds: {p.width_bounds}") print(f"{'Initial Center:':>20s} {p.center:<8.3f} Bounds: {p.center_bounds}") print("\n") + return def _fit_neutron_compton_profiles(self): @@ -391,7 +383,7 @@ def convertDataXToYSpacesForEachMass(self, dataX, delta_Q, delta_E): delta_E = delta_E[np.newaxis, :, :] mN, Ef, en_to_vel, vf, hbar = loadConstants() - masses = self._masses.reshape(self._masses.size, 1, 1) + masses = np.array([p.mass for p in self._profiles.values()]).reshape(-1, 1, 1) energyRecoil = np.square(hbar * delta_Q) / 2.0 / masses ySpacesForEachMass = ( @@ -446,7 +438,7 @@ def _set_means_and_std(self): """Calculate mean widths and intensities from tableWorkspace""" fitParsTable = self._table_fit_results - widths = np.zeros((self._masses.size, fitParsTable.rowCount())) + widths = np.zeros((len(self._profiles), fitParsTable.rowCount())) intensities = np.zeros(widths.shape) for i, p in enumerate(self._profiles.values()): widths[i] = fitParsTable.column(f"{p.label} Width") @@ -640,7 +632,7 @@ def _neutron_compton_profiles(self, pars): intensities = pars[::3].reshape(-1, 1) widths = pars[1::3].reshape(-1, 1) centers = pars[2::3].reshape(-1, 1) - masses = self._masses.reshape(-1, 1) + masses = np.array([p.mass for p in self._profiles.values()]).reshape(-1, 1) v0, E0, deltaE, deltaQ = self._kinematic_arrays[self._row_being_fit] @@ -691,7 +683,7 @@ def kinematicsAtYCenters(self, centers): def calcGaussianResolution(self, centers): - masses = self._masses.reshape((self._masses.size, 1)) + masses = np.array([p.mass for p in self._profiles.values()]).reshape(-1, 1) v0, E0, delta_E, delta_Q = self.kinematicsAtYCenters(centers) det, plick, angle, T0, L0, L1 = self._instrument_params[self._row_being_fit] dE1, dTOF, dTheta, dL0, dL1, dE1_lorz = self._resolution_params[self._row_being_fit] @@ -737,7 +729,7 @@ def calcGaussianResolution(self, centers): def calcLorentzianResolution(self, centers): - masses = self._masses.reshape((self._masses.size, 1)) + masses = np.array([p.mass for p in self._profiles.values()]).reshape(-1, 1) v0, E0, delta_E, delta_Q = self.kinematicsAtYCenters(centers) det, plick, angle, T0, L0, L1 = self._instrument_params[self._row_being_fit] dE1, dTOF, dTheta, dL0, dL1, dE1_lorz = self._resolution_params[self._row_being_fit] @@ -774,6 +766,41 @@ def pseudoVoigt(self, x, sigma, gamma): return pseudo_voigt / norm + # When interface is updated, uncomment to change the way + # constraints are handled: + + # def _get_parsed_constraints(self): + # + # parsed_constraints = [] + # + # for constraint in self._constraints: + # constraint['fun'] = self._get_parsed_constraint_function(constraint['fun']) + # + # parsed_constraints.append(constraint) + # + # return parsed_constraints + # + # + # def _get_parsed_constraint_function(self, function_string: str): + # + # profile_order = [key for key in self._profiles.keys()] + # attribute_order = ['intensity', 'width', 'center'] + # + # words = function_string.split(' ') + # for i, word in enumerate(words): + # if '.' in word: + # + # try: # Skip floats + # float(word) + # except ValueError: + # continue + # + # profile, attribute = word + # words[i] = f"pars[{profile_order.index(profile) + attribute_order.index(attribute)}]" + # + # return eval(f"lambda pars: {' '.join(words)}") + + def _replace_zero_columns_with_ncp_fit(self): """ If the initial input contains columns with zeros @@ -874,7 +901,7 @@ def createSlabGeometry(self, wsNCPM): def calcMSCorrectionSampleProperties(self, meanWidths, meanIntensityRatios): - masses = self._masses.flatten() + masses = [p.mass for p in self._profiles.values()] # If Backsscattering mode and H is present in the sample, add H to MS properties if self._mode_running == "BACKWARD": @@ -981,7 +1008,7 @@ def create_gamma_workspaces(self): def calcGammaCorrectionProfiles(self, meanWidths, meanIntensityRatios): - masses = self._masses.flatten() + masses = [p.mass for p in self._profiles.values()] profiles = "" for mass, width, intensity in zip(masses, meanWidths, meanIntensityRatios): profiles += ( @@ -1061,7 +1088,7 @@ def _set_results(self): self.all_std_widths = np.array(allStdWidths) self.all_std_intensities = np.array(allStdIntensities) - def save_results(self): + def _save_results(self): """Saves all of the arrays stored in this object""" maskedDetectorIdx = np.array(self._mask_spectra) - self._firstSpec From 1b9ba3aa017f81980874765aa2cd84b90db2fa07 Mon Sep 17 00:00:00 2001 From: GuiMacielPereira Date: Fri, 9 Aug 2024 14:19:12 +0100 Subject: [PATCH 18/25] Tidy some small parts of the code Made imports clearer, deleted useless comments and organized some sections into separate functions. --- src/mvesuvio/oop/AnalysisRoutine.py | 106 ++++++++++++++-------------- 1 file changed, 53 insertions(+), 53 deletions(-) diff --git a/src/mvesuvio/oop/AnalysisRoutine.py b/src/mvesuvio/oop/AnalysisRoutine.py index bf488d40..4f73b39c 100644 --- a/src/mvesuvio/oop/AnalysisRoutine.py +++ b/src/mvesuvio/oop/AnalysisRoutine.py @@ -1,17 +1,19 @@ -from mvesuvio.oop.NeutronComptonProfile import NeutronComptonProfile -from mvesuvio.oop.analysis_helpers import extractWS, histToPointData, loadConstants, \ - gaussian, lorentizian, numericalThirdDerivative, \ - switchFirstTwoAxis, createWS -from mantid.simpleapi import mtd, CreateEmptyTableWorkspace, MaskDetectors, SumSpectra, \ +import numpy as np +import matplotlib.pyplot as plt +from scipy import optimize +from mantid.simpleapi import mtd, CreateEmptyTableWorkspace, SumSpectra, \ CloneWorkspace, DeleteWorkspace, VesuvioCalculateGammaBackground, \ VesuvioCalculateMS, Scale, RenameWorkspace, Minus, CreateSampleShape, \ VesuvioThickness, Integration, Divide, Multiply, DeleteWorkspaces, \ CreateWorkspace -from mvesuvio.analysis_fitting import passDataIntoWS, replaceZerosWithNCP -import numpy as np -import matplotlib.pyplot as plt -from scipy import optimize -import sys + +from mvesuvio.oop.NeutronComptonProfile import NeutronComptonProfile +from mvesuvio.oop.analysis_helpers import histToPointData, loadConstants, \ + gaussian, lorentizian, numericalThirdDerivative + + +np.set_printoptions(suppress=True, precision=4, linewidth=200) + class AnalysisRoutine: @@ -23,9 +25,6 @@ def __init__(self, workspace, ip_file, h_ratio_to_lowest_mass, number_of_iterati self._name = workspace.name() self._ip_file = ip_file self._number_of_iterations = number_of_iterations - spectrum_list = workspace.getSpectrumNumbers() - self._firstSpec = min(spectrum_list) - self._lastSpec = max(spectrum_list) self._mask_spectra = mask_spectra self._transmission_guess = transmission_guess self._multiple_scattering_order = multiple_scattering_order @@ -34,25 +33,23 @@ def __init__(self, workspace, ip_file, h_ratio_to_lowest_mass, number_of_iterati self._horizontal_width = horizontal_width self._thickness = thickness self._mode_running = mode_running - self._multiple_scattering_correction = multiple_scattering_correction self._gamma_correction = gamma_correction - self._save_results_path = results_path self._save_figures_path = figures_path + self._h_ratio = h_ratio_to_lowest_mass + self._constraints = constraints + + self._profiles = {} # Variables changing during fit self._workspace_for_corrections = workspace - self._zero_columns_mask = None self._workspace_being_fit = workspace self._row_being_fit = 0 + self._zero_columns_boolean_mask = None self._table_fit_results = None self._fit_profiles_workspaces = {} - self._h_ratio = h_ratio_to_lowest_mass - self._constraints = constraints - self._profiles = {} - # Only used for system tests, remove once tests are updated self._run_hist_data = True self._run_norm_voigt = False @@ -67,6 +64,7 @@ def add_profiles(self, *args: NeutronComptonProfile): def profiles(self): return self._profiles + def set_initial_profiles_from(self, source: 'AnalysisRoutine'): # Set intensities @@ -130,7 +128,10 @@ def profiles(self, incoming_profiles): def _update_workspace_data(self): - self._dataX, self._dataY, self._dataE = extractWS(self._workspace_being_fit) + + self._dataX = self._workspace_being_fit.extractX() + self._dataY = self._workspace_being_fit.extractY() + self._dataE = self._workspace_being_fit.extractE() if self._run_hist_data: # Converts point data from workspaces to histogram data self._dataY, self._dataX, self._dataE = histToPointData(self._dataY, self._dataX, self._dataE) @@ -138,10 +139,23 @@ def _update_workspace_data(self): self._set_up_kinematic_arrays() self._fit_parameters = np.zeros((len(self._dataY), 3 * len(self._profiles) + 3)) - self._row_being_fit = 0 + self._table_fit_results = self._initialize_table_fit_parameters() - #Initialise Table for fit parameters + # Initialise workspaces for fitted ncp + self._fit_profiles_workspaces = {} + for element, p in self._profiles.items(): + self._fit_profiles_workspaces[element] = self._create_emtpy_ncp_workspace(f'_{element}_ncp') + self._fit_profiles_workspaces['total'] = self._create_emtpy_ncp_workspace(f'_total_ncp') + + # Initialise empty means + self._mean_widths = None + self._std_widths = None + self._mean_intensity_ratios = None + self._std_intensity_ratios = None + + + def _initialize_table_fit_parameters(self): table = CreateEmptyTableWorkspace( OutputWorkspace=self._workspace_being_fit.name()+ "_fit_results" ) @@ -153,24 +167,14 @@ def _update_workspace_data(self): table.addColumn(type="float", name=f"{key} Center ") table.addColumn(type="float", name="Normalised Chi2") table.addColumn(type="float", name="Number of Iteraions") + return table - self._table_fit_results = table - - #Initialise workspaces for fitted ncp - self._fit_profiles_workspaces = {} - for element, p in self._profiles.items(): - self._fit_profiles_workspaces[element] = self._create_emtpy_ncp_workspace(f'_{element}_ncp') - self._fit_profiles_workspaces['total'] = self._create_emtpy_ncp_workspace(f'_total_ncp') - - self._mean_widths = None - self._std_widths = None - self._mean_intensity_ratios = None - self._std_intensity_ratios = None @property def mean_widths(self): return self._mean_widths + @mean_widths.setter def mean_widths(self, value): self._mean_widths = value @@ -324,9 +328,13 @@ def loadInstrParsFileIntoArray(self): """Loads instrument parameters into array, from the file in the specified path""" data = np.loadtxt(self._ip_file, dtype=str)[1:].astype(float) - spectra = data[:, 0] - select_rows = np.where((spectra >= self._firstSpec) & (spectra <= self._lastSpec)) + + workspace_spectrum_list = self._workspace_being_fit.getSpectrumNumbers() + first_spec = min(workspace_spectrum_list) + last_spec = max(workspace_spectrum_list) + + select_rows = np.where((spectra >= first_spec) & (spectra <= last_spec)) instrPars = data[select_rows] return instrPars @@ -526,7 +534,7 @@ def filterWidthsAndIntensities(self, widthsIn, intensitiesIn): maskedIntensities, axis=0 ) # Not nansum() - # TODO: sort this out + # Keep this around in case it is needed again # When trying to estimate HToMassIdxRatio and normalization fails, skip normalization # if np.all(np.isnan(betterIntensities)) & IC.runningPreliminary: # assert IC.noOfMSIterations == 0, ( @@ -552,11 +560,6 @@ def filterWidthsAndIntensities(self, widthsIn, intensitiesIn): def _fit_neutron_compton_profiles_to_row(self): - """ - Fits the neutron compton profiles to one spectrum. - Calculates the best fit parameters and adds them to results table. - Adds row with calculated profiles to results workspace. - """ if np.all(self._dataY[self._row_being_fit] == 0): self._table_fit_results.addRow(np.zeros(3*len(self._profiles)+3)) @@ -591,10 +594,7 @@ def _fit_neutron_compton_profiles_to_row(self): # Store results for easier access when calculating means self._fit_parameters[self._row_being_fit] = tableRow - with np.printoptions( - suppress=True, precision=4, linewidth=200, threshold=sys.maxsize - ): - print(tableRow) + print(tableRow) # Pass fit profiles into workspaces individual_ncps = self._neutron_compton_profiles(fitPars) @@ -813,7 +813,7 @@ def _replace_zero_columns_with_ncp_fit(self): dataY = self._workspace_for_corrections.extractY() ncp = self._fit_profiles_workspaces['total'].extractY() - self._zero_columns_mask = np.all(dataY == 0, axis=0) # Masked Cols + self._zero_columns_boolean_mask = np.all(dataY == 0, axis=0) # Masked Cols self._workspace_for_corrections = CloneWorkspace( InputWorkspace=self._workspace_for_corrections.name(), @@ -821,7 +821,7 @@ def _replace_zero_columns_with_ncp_fit(self): ) for row in range(self._workspace_for_corrections.getNumberHistograms()): # TODO: Once the option to chage point to hist is removed, remove [:len(ncp)] - self._workspace_for_corrections.dataY(row)[self._zero_columns_mask] = ncp[row, self._zero_columns_mask[:len(ncp[row])]] + self._workspace_for_corrections.dataY(row)[self._zero_columns_boolean_mask] = ncp[row, self._zero_columns_boolean_mask[:len(ncp[row])]] SumSpectra( InputWorkspace=self._workspace_for_corrections.name(), @@ -838,8 +838,8 @@ def _remask_columns_with_zeros(self, ws_to_remask_name): """ ws_to_remask = mtd[ws_to_remask_name] for row in range(ws_to_remask.getNumberHistograms()): - ws_to_remask.dataY(row)[self._zero_columns_mask] = 0 - ws_to_remask.dataE(row)[self._zero_columns_mask] = 0 + ws_to_remask.dataY(row)[self._zero_columns_boolean_mask] = 0 + ws_to_remask.dataE(row)[self._zero_columns_boolean_mask] = 0 return @@ -1064,7 +1064,7 @@ def _set_results(self): wsIterName + f"_{p.label}_ncp" ] allNCP.append(ncpWsToAppend.extractY()) - allNCP = switchFirstTwoAxis(np.array(allNCP)) + allNCP = np.swapaxes(np.array(allNCP), 0, 1) allIterNcp.append(allNCP) # Extract Mean and Std Widths, Intensities @@ -1091,7 +1091,7 @@ def _set_results(self): def _save_results(self): """Saves all of the arrays stored in this object""" - maskedDetectorIdx = np.array(self._mask_spectra) - self._firstSpec + maskedDetectorIdx = np.array(self._mask_spectra) - min(self._workspace_being_fit.getSpectrumNumbers()) # TODO: Take out nans next time when running original results # Because original results were recently saved with nans, mask spectra with nans From d4614d08e6ca7a6f0722608fa7b98d780129685f Mon Sep 17 00:00:00 2001 From: GuiMacielPereira Date: Fri, 9 Aug 2024 14:33:53 +0100 Subject: [PATCH 19/25] Delete old files and rename new ones This commit finalizes the transition between the previous analysis routine using only functions and the new one using an object to run the routine. All of the intermediate files have been deleted and final routine was renamed to the existing analysis_reduction.py --- .../oop => legacy/examples}/run_routine.py | 0 src/mvesuvio/analysis_reduction.py | 1845 ++++++++--------- src/mvesuvio/analysis_routines.py | 6 +- src/mvesuvio/oop/AnalysisRoutine.py | 1116 ---------- src/mvesuvio/oop/NeutronComptonProfile.py | 18 - src/mvesuvio/oop/__init__.py | 0 .../{oop => util}/analysis_helpers.py | 0 7 files changed, 892 insertions(+), 2093 deletions(-) rename {src/mvesuvio/oop => legacy/examples}/run_routine.py (100%) delete mode 100644 src/mvesuvio/oop/AnalysisRoutine.py delete mode 100644 src/mvesuvio/oop/NeutronComptonProfile.py delete mode 100644 src/mvesuvio/oop/__init__.py rename src/mvesuvio/{oop => util}/analysis_helpers.py (100%) diff --git a/src/mvesuvio/oop/run_routine.py b/legacy/examples/run_routine.py similarity index 100% rename from src/mvesuvio/oop/run_routine.py rename to legacy/examples/run_routine.py diff --git a/src/mvesuvio/analysis_reduction.py b/src/mvesuvio/analysis_reduction.py index 749ad6fa..4973173d 100644 --- a/src/mvesuvio/analysis_reduction.py +++ b/src/mvesuvio/analysis_reduction.py @@ -1,1114 +1,1053 @@ +import numpy as np import matplotlib.pyplot as plt -import numpy as np -from mantid.simpleapi import * from scipy import optimize -from .analysis_fitting import passDataIntoWS, replaceZerosWithNCP +from mantid.simpleapi import mtd, CreateEmptyTableWorkspace, SumSpectra, \ + CloneWorkspace, DeleteWorkspace, VesuvioCalculateGammaBackground, \ + VesuvioCalculateMS, Scale, RenameWorkspace, Minus, CreateSampleShape, \ + VesuvioThickness, Integration, Divide, Multiply, DeleteWorkspaces, \ + CreateWorkspace + +from mvesuvio.util.analysis_helpers import histToPointData, loadConstants, \ + gaussian, lorentizian, numericalThirdDerivative + +from dataclasses import dataclass + + +np.set_printoptions(suppress=True, precision=4, linewidth=200) + + +@dataclass(frozen=False) +class NeutronComptonProfile: + label: str + mass: float + + intensity: float + width: float + center: float + + intensity_bounds: tuple[float, float] + width_bounds: tuple[float, float] + center_bounds: tuple[float, float] + + mean_intensity: float = None + mean_width: float = None + mean_center: float = None + + +class AnalysisRoutine: + + def __init__(self, workspace, ip_file, h_ratio_to_lowest_mass, number_of_iterations, mask_spectra, + multiple_scattering_correction, vertical_width, horizontal_width, thickness, + gamma_correction, mode_running, transmission_guess=None, multiple_scattering_order=None, + number_of_events=None, results_path=None, figures_path=None, constraints=()): + + self._name = workspace.name() + self._ip_file = ip_file + self._number_of_iterations = number_of_iterations + self._mask_spectra = mask_spectra + self._transmission_guess = transmission_guess + self._multiple_scattering_order = multiple_scattering_order + self._number_of_events = number_of_events + self._vertical_width = vertical_width + self._horizontal_width = horizontal_width + self._thickness = thickness + self._mode_running = mode_running + self._multiple_scattering_correction = multiple_scattering_correction + self._gamma_correction = gamma_correction + self._save_results_path = results_path + self._save_figures_path = figures_path + self._h_ratio = h_ratio_to_lowest_mass + self._constraints = constraints + + self._profiles = {} + + # Variables changing during fit + self._workspace_for_corrections = workspace + self._workspace_being_fit = workspace + self._row_being_fit = 0 + self._zero_columns_boolean_mask = None + self._table_fit_results = None + self._fit_profiles_workspaces = {} + + # Only used for system tests, remove once tests are updated + self._run_hist_data = True + self._run_norm_voigt = False + + + def add_profiles(self, *args: NeutronComptonProfile): + for profile in args: + self._profiles[profile.label] = profile + + + @property + def profiles(self): + return self._profiles + + + def set_initial_profiles_from(self, source: 'AnalysisRoutine'): + + # Set intensities + for p in self._profiles.values(): + if np.isclose(p.mass, 1, atol=0.1): # Hydrogen present + p.intensity = source._h_ratio * source._get_lightest_profile().mean_intensity + continue + p.intensity = source.profiles[p.label].mean_intensity + + # Normalise intensities + sum_intensities = sum([p.intensity for p in self._profiles.values()]) + for p in self._profiles.values(): + p.intensity /= sum_intensities + + # Set widths + for p in self._profiles.values(): + try: + p.width = source.profiles[p.label].mean_width + except KeyError: + continue -# Format print output of arrays -np.set_printoptions(suppress=True, precision=4, linewidth=100, threshold=sys.maxsize) + # Fix all widths except lightest mass + for p in self._profiles.values(): + if p == self._get_lightest_profile(): + continue + p.width_bounds = [p.width, p.width] + return -def iterativeFitForDataReduction(ic): - createTableInitialParameters(ic) - initialWs = loadRawAndEmptyWsFromUserPath( - ic - ) # Do this before alternative bootstrap to extract name() + def _get_lightest_profile(self): + profiles = [p for p in self._profiles.values()] + masses = [p.mass for p in self._profiles.values()] + return profiles[np.argmin(masses)] - if ic.runningSampleWS: - initialWs = RenameWorkspace( - InputWorkspace=ic.sampleWS, OutputWorkspace=initialWs.name() - ) - cropedWs = cropAndMaskWorkspace(ic, initialWs) - wsToBeFitted = CloneWorkspace( - InputWorkspace=cropedWs, OutputWorkspace=cropedWs.name() + "0" - ) + def calculate_h_ratio(self): - for iteration in range(ic.noOfMSIterations + 1): - # Workspace from previous iteration - wsToBeFitted = mtd[ic.name + str(iteration)] + if not np.isclose(self._get_lightest_profile().mass, 1, atol=0.1): # Hydrogen present + return None + + # Hydrogen is present + intensities = np.array([p.mean_intensity for p in self._profiles.values()]) + masses = np.array([p.mass for p in self._profiles.values()]) - ncpTotal = fitNcpToWorkspace(ic, wsToBeFitted) + sorted_intensities = intensities[np.argsort(masses)] + return sorted_intensities[0] / sorted_intensities[1] + - mWidths, stdWidths, mIntRatios, stdIntRatios = extractMeans( - wsToBeFitted.name(), ic - ) - createMeansAndStdTableWS( - wsToBeFitted.name(), ic, mWidths, stdWidths, mIntRatios, stdIntRatios - ) + @property + def profiles(self): + return self._profiles - # When last iteration, skip MS and GC - if iteration == ic.noOfMSIterations: - break - # Replace zero columns (bins) with ncp total fit - # If ws has no zero column, then remains unchanged - if iteration == 0: - wsNCPM = replaceZerosWithNCP(mtd[ic.name], ncpTotal) + @profiles.setter + def profiles(self, incoming_profiles): + assert(isinstance(incoming_profiles, dict)) + common_keys = self._profiles.keys() & incoming_profiles.keys() + common_keys_profiles = {k: incoming_profiles[k] for k in common_keys} + self._profiles = {**self._profiles, **common_keys_profiles} - CloneWorkspace(InputWorkspace=ic.name, OutputWorkspace="tmpNameWs") - if ic.GammaCorrectionFlag: - wsGC = createWorkspacesForGammaCorrection(ic, mWidths, mIntRatios, wsNCPM) - Minus( - LHSWorkspace="tmpNameWs", RHSWorkspace=wsGC, OutputWorkspace="tmpNameWs" - ) + def _update_workspace_data(self): - if ic.MSCorrectionFlag: - wsMS = createWorkspacesForMSCorrection(ic, mWidths, mIntRatios, wsNCPM) - Minus( - LHSWorkspace="tmpNameWs", RHSWorkspace=wsMS, OutputWorkspace="tmpNameWs" - ) + self._dataX = self._workspace_being_fit.extractX() + self._dataY = self._workspace_being_fit.extractY() + self._dataE = self._workspace_being_fit.extractE() - remaskValues(ic.name, "tmpNameWS") # Masks cols in the same place as in ic.name - RenameWorkspace( - InputWorkspace="tmpNameWs", OutputWorkspace=ic.name + str(iteration + 1) - ) - - wsFinal = mtd[ic.name + str(ic.noOfMSIterations)] - fittingResults = resultsObject(ic) - fittingResults.save() - return wsFinal, fittingResults + if self._run_hist_data: # Converts point data from workspaces to histogram data + self._dataY, self._dataX, self._dataE = histToPointData(self._dataY, self._dataX, self._dataE) + self._set_up_kinematic_arrays() -def remaskValues(wsName, wsToMaskName): - """ - Uses the ws before the MS correction to look for masked columns or dataE - and implement the same masked values after the correction. - """ - ws = mtd[wsName] - dataX, dataY, dataE = extractWS(ws) - mask = np.all(dataY == 0, axis=0) + self._fit_parameters = np.zeros((len(self._dataY), 3 * len(self._profiles) + 3)) + self._row_being_fit = 0 + self._table_fit_results = self._initialize_table_fit_parameters() - wsM = mtd[wsToMaskName] - dataXM, dataYM, dataEM = extractWS(wsM) - dataYM[:, mask] = 0 - if np.all(dataE == 0): - dataEM = np.zeros(dataEM.shape) + # Initialise workspaces for fitted ncp + self._fit_profiles_workspaces = {} + for element, p in self._profiles.items(): + self._fit_profiles_workspaces[element] = self._create_emtpy_ncp_workspace(f'_{element}_ncp') + self._fit_profiles_workspaces['total'] = self._create_emtpy_ncp_workspace(f'_total_ncp') - passDataIntoWS(dataXM, dataYM, dataEM, wsM) - return + # Initialise empty means + self._mean_widths = None + self._std_widths = None + self._mean_intensity_ratios = None + self._std_intensity_ratios = None -def createTableInitialParameters(ic): - print("\nRUNNING ", ic.modeRunning, " SCATTERING.\n") - if ic.modeRunning == "BACKWARD": - print(f"\nH ratio to mass with idx={ic.massIdx}: {ic.HToMassIdxRatio}\n") + def _initialize_table_fit_parameters(self): + table = CreateEmptyTableWorkspace( + OutputWorkspace=self._workspace_being_fit.name()+ "_fit_results" + ) + table.setTitle("SciPy Fit Parameters") + table.addColumn(type="float", name="Spectrum") + for key in self._profiles.keys(): + table.addColumn(type="float", name=f"{key} Intensity") + table.addColumn(type="float", name=f"{key} Width") + table.addColumn(type="float", name=f"{key} Center ") + table.addColumn(type="float", name="Normalised Chi2") + table.addColumn(type="float", name="Number of Iteraions") + return table + + + @property + def mean_widths(self): + return self._mean_widths + + + @mean_widths.setter + def mean_widths(self, value): + self._mean_widths = value + for i, p in enumerate(self._profiles.values()): + p.mean_width = self._mean_widths[i] + return - meansTableWS = CreateEmptyTableWorkspace( - OutputWorkspace=ic.name + "_Initial_Parameters" - ) - meansTableWS.addColumn(type="float", name="Mass") - meansTableWS.addColumn(type="float", name="Initial Widths") - meansTableWS.addColumn(type="str", name="Bounds Widths") - meansTableWS.addColumn(type="float", name="Initial Intensities") - meansTableWS.addColumn(type="str", name="Bounds Intensities") - meansTableWS.addColumn(type="float", name="Initial Centers") - meansTableWS.addColumn(type="str", name="Bounds Centers") - - print("\nCreated Table with Initial Parameters:") - for m, iw, bw, ii, bi, inc, bc in zip( - ic.masses.astype(float), - ic.initPars[1::3], - ic.bounds[1::3], - ic.initPars[0::3], - ic.bounds[0::3], - ic.initPars[2::3], - ic.bounds[2::3], - ): - meansTableWS.addRow([m, iw, str(bw), ii, str(bi), inc, str(bc)]) - print("\nMass: ", m) - print(f"{'Initial Intensity:':>20s} {ii:<8.3f} Bounds: {bi}") - print(f"{'Initial Width:':>20s} {iw:<8.3f} Bounds: {bw}") - print(f"{'Initial Center:':>20s} {inc:<8.3f} Bounds: {bc}") - print("\n") - - -def loadRawAndEmptyWsFromUserPath(ic): - print("\nLoading local workspaces ...\n") - Load(Filename=str(ic.userWsRawPath), OutputWorkspace=ic.name + "raw") - Rebin( - InputWorkspace=ic.name + "raw", - Params=ic.tofBinning, - OutputWorkspace=ic.name + "raw", - ) - assert (isinstance(ic.scaleRaw, float)) | ( - isinstance(ic.scaleRaw, int) - ), "Scaling factor of raw ws needs to be float or int." - Scale( - InputWorkspace=ic.name + "raw", - OutputWorkspace=ic.name + "raw", - Factor=str(ic.scaleRaw), - ) + @property + def mean_intensity_ratios(self): + return self._mean_intensity_ratios - SumSpectra(InputWorkspace=ic.name + "raw", OutputWorkspace=ic.name + "raw" + "_sum") - wsToBeFitted = CloneWorkspace( - InputWorkspace=ic.name + "raw", OutputWorkspace=ic.name + "uncroped_unmasked" - ) + @mean_intensity_ratios.setter + def mean_intensity_ratios(self, value): + self._mean_intensity_ratios = value + for i, p in enumerate(self.profiles.values()): + p.mean_intensity = self._mean_intensity_ratios[i] + return - # if ic.mode=="DoubleDifference": - if ic.subEmptyFromRaw: - Load(Filename=str(ic.userWsEmptyPath), OutputWorkspace=ic.name + "empty") - Rebin( - InputWorkspace=ic.name + "empty", - Params=ic.tofBinning, - OutputWorkspace=ic.name + "empty", - ) - assert (isinstance(ic.scaleEmpty, float)) | ( - isinstance(ic.scaleEmpty, int) - ), "Scaling factor of empty ws needs to be float or int" - Scale( - InputWorkspace=ic.name + "empty", - OutputWorkspace=ic.name + "empty", - Factor=str(ic.scaleEmpty), - ) + def _create_emtpy_ncp_workspace(self, suffix): + return CreateWorkspace( + DataX=self._dataX, + DataY=np.zeros(self._dataY.size), + DataE=np.zeros(self._dataE.size), + Nspec=self._workspace_being_fit.getNumberHistograms(), + OutputWorkspace=self._workspace_being_fit.name()+suffix, + ParentWorkspace=self._workspace_being_fit + ) - SumSpectra( - InputWorkspace=ic.name + "empty", OutputWorkspace=ic.name + "empty" + "_sum" - ) - wsToBeFitted = Minus( - LHSWorkspace=ic.name + "raw", - RHSWorkspace=ic.name + "empty", - OutputWorkspace=ic.name + "uncroped_unmasked", - ) - return wsToBeFitted - - -def cropAndMaskWorkspace(ic, ws): - """Returns cloned and cropped workspace with modified name""" - # Read initial Spectrum number - wsFirstSpec = ws.getSpectrumNumbers()[0] - assert ( - ic.firstSpec >= wsFirstSpec - ), "Can't crop workspace, firstSpec < first spectrum in workspace." - - initialIdx = ic.firstSpec - wsFirstSpec - lastIdx = ic.lastSpec - wsFirstSpec - - newWsName = ws.name().split("uncroped")[0] # Retrieve original name - wsCrop = CropWorkspace( - InputWorkspace=ws, - StartWorkspaceIndex=initialIdx, - EndWorkspaceIndex=lastIdx, - OutputWorkspace=newWsName, - ) + def _set_up_kinematic_arrays(self): + resolutionPars, instrPars, kinematicArrays, ySpacesForEachMass = self.prepareFitArgs() + self._resolution_params = resolutionPars + self._instrument_params = instrPars + self._kinematic_arrays = kinematicArrays + self._y_space_arrays = ySpacesForEachMass - maskBinsWithZeros(wsCrop, ic) # Used to mask resonance peaks - MaskDetectors(Workspace=wsCrop, WorkspaceIndexList=ic.maskedDetectorIdx) - return wsCrop + def run(self): + assert len(self.profiles) > 0, "Add profiles before atempting to run the routine!" -def maskBinsWithZeros(ws, IC): - """ - Masks a given TOF range on ws with zeros on dataY. - Leaves errors dataE unchanged, as they are used by later treatments. - Used to mask resonance peaks. - """ + self._create_table_initial_parameters() - if IC.maskTOFRange is None: # Masked TOF bins not found, skip - return + # Legacy code from Bootstrap + # if self.runningSampleWS: + # initialWs = RenameWorkspace( + # InputWorkspace=ic.sampleWS, OutputWorkspace=initialWs.name() + # ) - dataX, dataY, dataE = extractWS(ws) - start, end = [int(s) for s in IC.maskTOFRange.split(",")] - assert ( - start <= end - ), "Start value for masking needs to be smaller or equal than end." - mask = (dataX >= start) & (dataX <= end) # TOF region to mask + CloneWorkspace( + InputWorkspace=self._workspace_being_fit.name(), + OutputWorkspace=self._name + '0' + ) - dataY[mask] = 0 + for iteration in range(self._number_of_iterations + 1): - passDataIntoWS(dataX, dataY, dataE, ws) - return + self._workspace_being_fit = mtd[self._name + str(iteration)] + self._update_workspace_data() + self._fit_neutron_compton_profiles() -def fitNcpToWorkspace(IC, ws): - """ - Performs the fit of ncp to the workspace. - Firtly the arrays required for the fit are prepared and then the fit is performed iteratively - on a spectrum by spectrum basis. - """ + self._create_summed_workspaces() + self._save_plots() + self._set_means_and_std() - dataX, dataY, dataE = extractWS(ws) - if IC.runHistData: # Converts point data from workspaces to histogram data - dataY, dataX, dataE = histToPointData(dataY, dataX, dataE) + # When last iteration, skip MS and GC + if iteration == self._number_of_iterations: + break - resolutionPars, instrPars, kinematicArrays, ySpacesForEachMass = prepareFitArgs( - IC, dataX - ) + # Do this because MS and Gamma corrections do not accept zero columns + if iteration==0: + self._replace_zero_columns_with_ncp_fit() - print("\nFitting NCP:\n") + CloneWorkspace( + InputWorkspace=self._workspace_for_corrections.name(), + OutputWorkspace="next_iteration" + ) + self._correct_for_gamma_and_multiple_scattering("next_iteration") - arrFitPars = fitNcpToArray( - IC, dataY, dataE, resolutionPars, instrPars, kinematicArrays, ySpacesForEachMass - ) - createTableWSForFitPars(ws.name(), IC.noOfMasses, arrFitPars) - arrBestFitPars = arrFitPars[:, 1:-2] - ncpForEachMass, ncpTotal = calculateNcpArr( - IC, - arrBestFitPars, - resolutionPars, - instrPars, - kinematicArrays, - ySpacesForEachMass, - ) - ncpSumWSs = createNcpWorkspaces(ncpForEachMass, ncpTotal, ws, IC) + # Need to remask columns of output of corrections + self._remask_columns_with_zeros("next_iteration") - wsDataSum = SumSpectra(InputWorkspace=ws, OutputWorkspace=ws.name() + "_Sum") - plotSumNCPFits(wsDataSum, *ncpSumWSs, IC) - return ncpTotal + RenameWorkspace( + InputWorkspace="next_iteration", + OutputWorkspace=self._name + str(iteration + 1) + ) + self._set_results() + self._save_results() + return self -def extractWS(ws): - """Directly exctracts data from workspace into arrays""" - return ws.extractX(), ws.extractY(), ws.extractE() + def _create_table_initial_parameters(self): + meansTableWS = CreateEmptyTableWorkspace( + OutputWorkspace=self._name + "_Initial_Parameters" + ) + meansTableWS.addColumn(type="float", name="Mass") + meansTableWS.addColumn(type="float", name="Initial Widths") + meansTableWS.addColumn(type="str", name="Bounds Widths") + meansTableWS.addColumn(type="float", name="Initial Intensities") + meansTableWS.addColumn(type="str", name="Bounds Intensities") + meansTableWS.addColumn(type="float", name="Initial Centers") + meansTableWS.addColumn(type="str", name="Bounds Centers") + + print("\nCreated Table with Initial Parameters:") + for p in self._profiles.values(): + meansTableWS.addRow([p.mass, p.width, str(p.width_bounds), + p.intensity, str(p.intensity_bounds), + p.center, str(p.center_bounds)]) + print("\nMass: ", p.mass) + print(f"{'Initial Intensity:':>20s} {p.intensity:<8.3f} Bounds: {p.intensity_bounds}") + print(f"{'Initial Width:':>20s} {p.width:<8.3f} Bounds: {p.width_bounds}") + print(f"{'Initial Center:':>20s} {p.center:<8.3f} Bounds: {p.center_bounds}") + print("\n") + return -def histToPointData(dataY, dataX, dataE): - """ - Used only when comparing with original results. - Sets each dataY point to the center of bins. - Last column of data is removed. - Removed original scaling by bin widths - """ - histWidths = dataX[:, 1:] - dataX[:, :-1] - assert np.min(histWidths) == np.max( - histWidths - ), "Histogram widhts need to be the same length" + def _fit_neutron_compton_profiles(self): + """ + Performs the fit of neutron compton profiles to the workspace being fit. + The profiles are fit on a spectrum by spectrum basis. + """ + print("\nFitting Neutron Compron Prolfiles:\n") - dataYp = dataY[:, :-1] - dataEp = dataE[:, :-1] - dataXp = dataX[:, :-1] + histWidths[0, 0] / 2 - return dataYp, dataXp, dataEp + self._row_being_fit = 0 + while self._row_being_fit != len(self._dataY): + self._fit_neutron_compton_profiles_to_row() + self._row_being_fit += 1 + assert np.any(self._fit_parameters), "Fitting parameters cannot be zero for all spectra!" + return -def prepareFitArgs(ic, dataX): - instrPars = loadInstrParsFileIntoArray(ic.InstrParsPath, ic.firstSpec, ic.lastSpec) - resolutionPars = loadResolutionPars(instrPars) - v0, E0, delta_E, delta_Q = calculateKinematicsArrays(dataX, instrPars) - kinematicArrays = np.array([v0, E0, delta_E, delta_Q]) - ySpacesForEachMass = convertDataXToYSpacesForEachMass( - dataX, ic.masses, delta_Q, delta_E - ) + def prepareFitArgs(self): + instrPars = self.loadInstrParsFileIntoArray() + resolutionPars = self.loadResolutionPars(instrPars) - kinematicArrays = reshapeArrayPerSpectrum(kinematicArrays) - ySpacesForEachMass = reshapeArrayPerSpectrum(ySpacesForEachMass) - return resolutionPars, instrPars, kinematicArrays, ySpacesForEachMass - - -def loadInstrParsFileIntoArray(InstrParsPath, firstSpec, lastSpec): - """Loads instrument parameters into array, from the file in the specified path""" - - data = np.loadtxt(InstrParsPath, dtype=str)[1:].astype(float) - - spectra = data[:, 0] - select_rows = np.where((spectra >= firstSpec) & (spectra <= lastSpec)) - instrPars = data[select_rows] - return instrPars - - -def loadResolutionPars(instrPars): - """Resolution of parameters to propagate into TOF resolution - Output: matrix with each parameter in each column""" - spectrums = instrPars[:, 0] - L = len(spectrums) - # For spec no below 135, back scattering detectors, mode is double difference - # For spec no 135 or above, front scattering detectors, mode is single difference - dE1 = np.where(spectrums < 135, 88.7, 73) # meV, STD - dE1_lorz = np.where(spectrums < 135, 40.3, 24) # meV, HFHM - dTOF = np.repeat(0.37, L) # us - dTheta = np.repeat(0.016, L) # rad - dL0 = np.repeat(0.021, L) # meters - dL1 = np.repeat(0.023, L) # meters - - resolutionPars = np.vstack((dE1, dTOF, dTheta, dL0, dL1, dE1_lorz)).transpose() - return resolutionPars - - -def calculateKinematicsArrays(dataX, instrPars): - """Kinematics quantities calculated from TOF data""" - - mN, Ef, en_to_vel, vf, hbar = loadConstants() - det, plick, angle, T0, L0, L1 = np.hsplit(instrPars, 6) # each is of len(dataX) - t_us = dataX - T0 # T0 is electronic delay due to instruments - v0 = vf * L0 / (vf * t_us - L1) - E0 = np.square( - v0 / en_to_vel - ) # en_to_vel is a factor used to easily change velocity to energy and vice-versa - - delta_E = E0 - Ef - delta_Q2 = ( - 2.0 - * mN - / hbar**2 - * (E0 + Ef - 2.0 * np.sqrt(E0 * Ef) * np.cos(angle / 180.0 * np.pi)) - ) - delta_Q = np.sqrt(delta_Q2) - return v0, E0, delta_E, delta_Q # shape(no of spectrums, no of bins) - - -def reshapeArrayPerSpectrum(A): - """ - Exchanges the first two axes of an array A. - Rearranges array to match iteration per spectrum - """ - return np.stack(np.split(A, len(A), axis=0), axis=2)[0] - - -def convertDataXToYSpacesForEachMass(dataX, masses, delta_Q, delta_E): - "Calculates y spaces from TOF data, each row corresponds to one mass" - - # Prepare arrays to broadcast - dataX = dataX[np.newaxis, :, :] - delta_Q = delta_Q[np.newaxis, :, :] - delta_E = delta_E[np.newaxis, :, :] - - mN, Ef, en_to_vel, vf, hbar = loadConstants() - masses = masses.reshape(masses.size, 1, 1) - - energyRecoil = np.square(hbar * delta_Q) / 2.0 / masses - ySpacesForEachMass = ( - masses / hbar**2 / delta_Q * (delta_E - energyRecoil) - ) # y-scaling - return ySpacesForEachMass - - -def fitNcpToArray( - ic, dataY, dataE, resolutionPars, instrPars, kinematicArrays, ySpacesForEachMass -): - """Takes dataY as a 2D array and returns the 2D array best fit parameters.""" - - arrFitPars = np.zeros((len(dataY), len(ic.initPars) + 3)) - for i in range(len(dataY)): - specFitPars = fitNcpToSingleSpec( - dataY[i], - dataE[i], - ySpacesForEachMass[i], - resolutionPars[i], - instrPars[i], - kinematicArrays[i], - ic, + v0, E0, delta_E, delta_Q = self.calculateKinematicsArrays(instrPars) + kinematicArrays = np.array([v0, E0, delta_E, delta_Q]) + ySpacesForEachMass = self.convertDataXToYSpacesForEachMass( + self._dataX, delta_Q, delta_E ) + kinematicArrays = np.swapaxes(kinematicArrays, 0, 1) + ySpacesForEachMass = np.swapaxes(ySpacesForEachMass, 0, 1) + return resolutionPars, instrPars, kinematicArrays, ySpacesForEachMass + + + def loadInstrParsFileIntoArray(self): + """Loads instrument parameters into array, from the file in the specified path""" + + data = np.loadtxt(self._ip_file, dtype=str)[1:].astype(float) + spectra = data[:, 0] + + workspace_spectrum_list = self._workspace_being_fit.getSpectrumNumbers() + first_spec = min(workspace_spectrum_list) + last_spec = max(workspace_spectrum_list) + + select_rows = np.where((spectra >= first_spec) & (spectra <= last_spec)) + instrPars = data[select_rows] + return instrPars + + + @staticmethod + def loadResolutionPars(instrPars): + """Resolution of parameters to propagate into TOF resolution + Output: matrix with each parameter in each column""" + spectrums = instrPars[:, 0] + L = len(spectrums) + # For spec no below 135, back scattering detectors, mode is double difference + # For spec no 135 or above, front scattering detectors, mode is single difference + dE1 = np.where(spectrums < 135, 88.7, 73) # meV, STD + dE1_lorz = np.where(spectrums < 135, 40.3, 24) # meV, HFHM + dTOF = np.repeat(0.37, L) # us + dTheta = np.repeat(0.016, L) # rad + dL0 = np.repeat(0.021, L) # meters + dL1 = np.repeat(0.023, L) # meters + + resolutionPars = np.vstack((dE1, dTOF, dTheta, dL0, dL1, dE1_lorz)).transpose() + return resolutionPars + + + def calculateKinematicsArrays(self, instrPars): + """Kinematics quantities calculated from TOF data""" + + dataX = self._dataX + + mN, Ef, en_to_vel, vf, hbar = loadConstants() + det, plick, angle, T0, L0, L1 = np.hsplit(instrPars, 6) # each is of len(dataX) + t_us = dataX - T0 # T0 is electronic delay due to instruments + v0 = vf * L0 / (vf * t_us - L1) + E0 = np.square( + v0 / en_to_vel + ) # en_to_vel is a factor used to easily change velocity to energy and vice-versa + + delta_E = E0 - Ef + delta_Q2 = ( + 2.0 + * mN + / hbar**2 + * (E0 + Ef - 2.0 * np.sqrt(E0 * Ef) * np.cos(angle / 180.0 * np.pi)) + ) + delta_Q = np.sqrt(delta_Q2) + return v0, E0, delta_E, delta_Q # shape(no of spectrums, no of bins) - arrFitPars[i] = specFitPars - if np.all(specFitPars == 0): - print("Skipped spectra.") - else: - with np.printoptions( - suppress=True, precision=4, linewidth=200, threshold=sys.maxsize - ): - print(specFitPars) + def convertDataXToYSpacesForEachMass(self, dataX, delta_Q, delta_E): + "Calculates y spaces from TOF data, each row corresponds to one mass" - assert ~np.all( - arrFitPars == 0 - ), "Either Fits are all zero or assignment of fitting not working" - return arrFitPars + # Prepare arrays to broadcast + dataX = dataX[np.newaxis, :, :] + delta_Q = delta_Q[np.newaxis, :, :] + delta_E = delta_E[np.newaxis, :, :] + mN, Ef, en_to_vel, vf, hbar = loadConstants() + masses = np.array([p.mass for p in self._profiles.values()]).reshape(-1, 1, 1) -def createTableWSForFitPars(wsName, noOfMasses, arrFitPars): - tableWS = CreateEmptyTableWorkspace( - OutputWorkspace=wsName + "_Best_Fit_NCP_Parameters" - ) - tableWS.setTitle("SCIPY Fit") - tableWS.addColumn(type="float", name="Spec Idx") - for i in range(int(noOfMasses)): - tableWS.addColumn(type="float", name=f"Intensity {i}") - tableWS.addColumn(type="float", name=f"Width {i}") - tableWS.addColumn(type="float", name=f"Center {i}") - tableWS.addColumn(type="float", name="Norm Chi2") - tableWS.addColumn(type="float", name="No Iter") - - for row in arrFitPars: # Pass array onto table ws - tableWS.addRow(row) - return - - -def calculateNcpArr( - ic, arrBestFitPars, resolutionPars, instrPars, kinematicArrays, ySpacesForEachMass -): - """Calculates the matrix of NCP from matrix of best fit parameters""" - - allNcpForEachMass = [] - for i in range(len(arrBestFitPars)): - ncpForEachMass = calculateNcpRow( - arrBestFitPars[i], - ySpacesForEachMass[i], - resolutionPars[i], - instrPars[i], - kinematicArrays[i], - ic, - ) + energyRecoil = np.square(hbar * delta_Q) / 2.0 / masses + ySpacesForEachMass = ( + masses / hbar**2 / delta_Q * (delta_E - energyRecoil) + ) # y-scaling + return ySpacesForEachMass - allNcpForEachMass.append(ncpForEachMass) - allNcpForEachMass = np.array(allNcpForEachMass) - allNcpTotal = np.sum(allNcpForEachMass, axis=1) - return allNcpForEachMass, allNcpTotal + def _save_plots(self): + # if IC.runningSampleWS: # Skip saving figure if running bootstrap + # return + if not self._save_figures_path: + return -def calculateNcpRow( - initPars, ySpacesForEachMass, resolutionPars, instrPars, kinematicArrays, ic -): - """input: all row shape - output: row shape with the ncpTotal for each mass""" + lw = 2 - if np.all(initPars == 0): - return np.zeros(ySpacesForEachMass.shape) + fig, ax = plt.subplots(subplot_kw={"projection": "mantid"}) - ncpForEachMass, ncpTotal = calculateNcpSpec( - ic, initPars, ySpacesForEachMass, resolutionPars, instrPars, kinematicArrays - ) - return ncpForEachMass + ws_data_sum = mtd[self._workspace_being_fit.name()+"_Sum"] + ax.errorbar(ws_data_sum, fmt="k.", label="Sum of spectra") + for key, ws in self._fit_profiles_workspaces.items(): + ws_sum = mtd[ws.name()+"_Sum"] + ax.plot(ws_sum, label=f'Sum of {key} profile', linewidth=lw) -def createNcpWorkspaces(ncpForEachMass, ncpTotal, ws, ic): - """Creates workspaces from ncp array data""" + ax.set_xlabel("TOF") + ax.set_ylabel("Counts") + ax.set_title("Sum of NCP fits") + ax.legend() - # Need to rearrage array of yspaces into seperate arrays for each mass - ncpForEachMass = switchFirstTwoAxis(ncpForEachMass) + fileName = self._workspace_being_fit.name() + "_profiles_sum.pdf" + savePath = self._save_figures_path / fileName + plt.savefig(savePath, bbox_inches="tight") + plt.close(fig) + return - # Use ws dataX to match with histogram data - dataX = ws.extractX()[ - :, : ncpTotal.shape[1] - ] # Make dataX match ncp shape automatically - assert ( - ncpTotal.shape == dataX.shape - ), "DataX and DataY in ws need to be the same shape." - ncpTotWS = createWS( - dataX, ncpTotal, np.zeros(dataX.shape), ws.name() + "_TOF_Fitted_Profiles" - ) - MaskDetectors(Workspace=ncpTotWS, WorkspaceIndexList=ic.maskedDetectorIdx) - wsTotNCPSum = SumSpectra( - InputWorkspace=ncpTotWS, OutputWorkspace=ncpTotWS.name() + "_Sum" - ) - - # Individual ncp workspaces - wsMNCPSum = [] - for i, ncp_m in enumerate(ncpForEachMass): - ncpMWS = createWS( - dataX, - ncp_m, - np.zeros(dataX.shape), - ws.name() + "_TOF_Fitted_Profile_" + str(i), - ) - MaskDetectors(Workspace=ncpMWS, WorkspaceIndexList=ic.maskedDetectorIdx) - wsNCPSum = SumSpectra( - InputWorkspace=ncpMWS, OutputWorkspace=ncpMWS.name() + "_Sum" - ) - wsMNCPSum.append(wsNCPSum) + def _create_summed_workspaces(self): - return wsTotNCPSum, wsMNCPSum + SumSpectra( + InputWorkspace=self._workspace_being_fit.name(), + OutputWorkspace=self._workspace_being_fit.name() + "_Sum") + for ws in self._fit_profiles_workspaces.values(): + SumSpectra( + InputWorkspace=ws.name(), + OutputWorkspace=ws.name() + "_Sum" + ) -def createWS(dataX, dataY, dataE, wsName): - ws = CreateWorkspace( - DataX=dataX.flatten(), - DataY=dataY.flatten(), - DataE=dataE.flatten(), - Nspec=len(dataY), - OutputWorkspace=wsName, - ) - return ws + def _set_means_and_std(self): + """Calculate mean widths and intensities from tableWorkspace""" + + fitParsTable = self._table_fit_results + widths = np.zeros((len(self._profiles), fitParsTable.rowCount())) + intensities = np.zeros(widths.shape) + for i, p in enumerate(self._profiles.values()): + widths[i] = fitParsTable.column(f"{p.label} Width") + intensities[i] = fitParsTable.column(f"{p.label} Intensity") + ( + meanWidths, + stdWidths, + meanIntensityRatios, + stdIntensityRatios, + ) = self.calculateMeansAndStds(widths, intensities) + + assert ( + len(meanWidths) == len(self._profiles) + ), "Number of mean widths must match number of profiles!" + + self.mean_widths = meanWidths # Use setter + self._std_widths = stdWidths + self.mean_intensity_ratios = meanIntensityRatios # Use setter + self._std_intensity_ratios = stdIntensityRatios + + self.createMeansAndStdTableWS() + return -def plotSumNCPFits(wsDataSum, wsTotNCPSum, wsMNCPSum, IC): - if IC.runningSampleWS: # Skip saving figure if running bootstrap + def createMeansAndStdTableWS(self): + meansTableWS = CreateEmptyTableWorkspace( + OutputWorkspace=self._workspace_being_fit.name() + "_MeanWidthsAndIntensities" + ) + meansTableWS.addColumn(type="str", name="Mass") + meansTableWS.addColumn(type="float", name="Mean Widths") + meansTableWS.addColumn(type="float", name="Std Widths") + meansTableWS.addColumn(type="float", name="Mean Intensities") + meansTableWS.addColumn(type="float", name="Std Intensities") + + print("\nCreated Table with means and std:") + print("\nMass Mean \u00B1 Std Widths Mean \u00B1 Std Intensities\n") + for p, mean_width, std_width, mean_intensity, std_intensity in zip( + self._profiles.values(), + self._mean_widths, + self._std_widths, + self._mean_intensity_ratios, + self._std_intensity_ratios, + ): + meansTableWS.addRow([p.label, mean_width, std_width, mean_intensity, std_intensity]) + print(f"{p.label:5s} {mean_width:10.5f} \u00B1 {std_width:7.5f} \ + {mean_intensity:10.5f} \u00B1 {std_intensity:7.5f}\n") return - lw = 2 - fig, ax = plt.subplots(subplot_kw={"projection": "mantid"}) - ax.errorbar(wsDataSum, "k.", label="Spectra") + def calculateMeansAndStds(self, widthsIn, intensitiesIn): + betterWidths, betterIntensities = self.filterWidthsAndIntensities(widthsIn, intensitiesIn) - ax.plot(wsTotNCPSum, "r-", label="Total NCP", linewidth=lw) - for m, wsNcp in zip(IC.masses, wsMNCPSum): - ax.plot(wsNcp, label=f"NCP m={m}", linewidth=lw) + meanWidths = np.nanmean(betterWidths, axis=1) + stdWidths = np.nanstd(betterWidths, axis=1) - ax.set_xlabel("TOF") - ax.set_ylabel("Counts") - ax.set_title("Sum of NCP fits") - ax.legend() + meanIntensityRatios = np.nanmean(betterIntensities, axis=1) + stdIntensityRatios = np.nanstd(betterIntensities, axis=1) - fileName = wsDataSum.name() + "_NCP_Fits.pdf" - savePath = IC.figSavePath / fileName - plt.savefig(savePath, bbox_inches="tight") - plt.close(fig) - return + return meanWidths, stdWidths, meanIntensityRatios, stdIntensityRatios -def switchFirstTwoAxis(A): - """Exchanges the first two indices of an array A, - rearranges matrices per spectrum for iteration of main fitting procedure - """ - return np.stack(np.split(A, len(A), axis=0), axis=2)[0] + def filterWidthsAndIntensities(self, widthsIn, intensitiesIn): + """Puts nans in places to be ignored""" + widths = widthsIn.copy() # Copy to avoid accidental changes in arrays + intensities = intensitiesIn.copy() -def extractMeans(wsName, IC): - """Extract widths and intensities from tableWorkspace""" + zeroSpecs = np.all( + widths == 0, axis=0 + ) # Catches all failed fits, not just masked spectra + widths[:, zeroSpecs] = np.nan + intensities[:, zeroSpecs] = np.nan - fitParsTable = mtd[wsName + "_Best_Fit_NCP_Parameters"] - widths = np.zeros((IC.noOfMasses, fitParsTable.rowCount())) - intensities = np.zeros(widths.shape) - for i in range(IC.noOfMasses): - widths[i] = fitParsTable.column(f"Width {i}") - intensities[i] = fitParsTable.column(f"Intensity {i}") + meanWidths = np.nanmean(widths, axis=1)[:, np.newaxis] - ( - meanWidths, - stdWidths, - meanIntensityRatios, - stdIntensityRatios, - ) = calculateMeansAndStds(widths, intensities, IC) + widthDeviation = np.abs(widths - meanWidths) + stdWidths = np.nanstd(widths, axis=1)[:, np.newaxis] - assert ( - len(widths) == IC.noOfMasses - ), "Widths and intensities must be in shape (noOfMasses, noOfSpec)" - return meanWidths, stdWidths, meanIntensityRatios, stdIntensityRatios + # Put nan in places where width deviation is bigger than std + filterMask = widthDeviation > stdWidths + betterWidths = np.where(filterMask, np.nan, widths) + maskedIntensities = np.where(filterMask, np.nan, intensities) + betterIntensities = maskedIntensities / np.sum( + maskedIntensities, axis=0 + ) # Not nansum() -def createMeansAndStdTableWS( - wsName, IC, meanWidths, stdWidths, meanIntensityRatios, stdIntensityRatios -): - meansTableWS = CreateEmptyTableWorkspace( - OutputWorkspace=wsName + "_Mean_Widths_And_Intensities" - ) - meansTableWS.addColumn(type="float", name="Mass") - meansTableWS.addColumn(type="float", name="Mean Widths") - meansTableWS.addColumn(type="float", name="Std Widths") - meansTableWS.addColumn(type="float", name="Mean Intensities") - meansTableWS.addColumn(type="float", name="Std Intensities") - - print("\nCreated Table with means and std:") - print("\nMass Mean \u00B1 Std Widths Mean \u00B1 Std Intensities\n") - for m, mw, stdw, mi, stdi in zip( - IC.masses.astype(float), - meanWidths, - stdWidths, - meanIntensityRatios, - stdIntensityRatios, - ): - meansTableWS.addRow([m, mw, stdw, mi, stdi]) - print(f"{m:5.2f} {mw:10.5f} \u00B1 {stdw:7.5f} {mi:10.5f} \u00B1 {stdi:7.5f}") - print("\n") - return - - -def calculateMeansAndStds(widthsIn, intensitiesIn, IC): - betterWidths, betterIntensities = filterWidthsAndIntensities( - widthsIn, intensitiesIn, IC - ) + # Keep this around in case it is needed again + # When trying to estimate HToMassIdxRatio and normalization fails, skip normalization + # if np.all(np.isnan(betterIntensities)) & IC.runningPreliminary: + # assert IC.noOfMSIterations == 0, ( + # "Calculation of mean intensities failed, cannot proceed with MS correction." + # "Try to run again with noOfMSIterations=0." + # ) + # betterIntensities = maskedIntensities + # else: + # pass - meanWidths = np.nanmean(betterWidths, axis=1) - stdWidths = np.nanstd(betterWidths, axis=1) + assert np.all(meanWidths != np.nan), "At least one mean of widths is nan!" + assert np.sum(filterMask) >= 1, "No widths survive filtering condition" + assert not (np.all(np.isnan(betterWidths))), "All filtered widths are nan" + assert not (np.all(np.isnan(betterIntensities))), "All filtered intensities are nan" + assert np.nanmax(betterWidths) != np.nanmin( + betterWidths + ), f"All fitered widths have the same value: {np.nanmin(betterWidths)}" + assert np.nanmax(betterIntensities) != np.nanmin( + betterIntensities + ), f"All fitered widths have the same value: {np.nanmin(betterIntensities)}" - meanIntensityRatios = np.nanmean(betterIntensities, axis=1) - stdIntensityRatios = np.nanstd(betterIntensities, axis=1) + return betterWidths, betterIntensities - return meanWidths, stdWidths, meanIntensityRatios, stdIntensityRatios + def _fit_neutron_compton_profiles_to_row(self): -def filterWidthsAndIntensities(widthsIn, intensitiesIn, IC): - """Puts nans in places to be ignored""" + if np.all(self._dataY[self._row_being_fit] == 0): + self._table_fit_results.addRow(np.zeros(3*len(self._profiles)+3)) + return - widths = widthsIn.copy() # Copy to avoid accidental changes in arrays - intensities = intensitiesIn.copy() + # Need to transform profiles into parameter array for minimize + initial_parameters = [] + bounds = [] + for p in self._profiles.values(): + for attr in ['intensity', 'width', 'center']: + initial_parameters.append(getattr(p, attr)) + for attr in ['intensity_bounds', 'width_bounds', 'center_bounds']: + bounds.append(getattr(p, attr)) - zeroSpecs = np.all( - widths == 0, axis=0 - ) # Catches all failed fits, not just masked spectra - widths[:, zeroSpecs] = np.nan - intensities[:, zeroSpecs] = np.nan + result = optimize.minimize( + self.errorFunction, + initial_parameters, + method="SLSQP", + bounds=bounds, + constraints=self._constraints, + ) + fitPars = result["x"] - meanWidths = np.nanmean(widths, axis=1)[:, np.newaxis] + # Pass fit parameters to results table + noDegreesOfFreedom = len(self._dataY[self._row_being_fit]) - len(fitPars) + normalised_chi2 = result["fun"] / noDegreesOfFreedom + number_iterations = result["nit"] + spectrum_number = self._instrument_params[self._row_being_fit, 0] + tableRow = np.hstack((spectrum_number, fitPars, normalised_chi2, number_iterations)) + self._table_fit_results.addRow(tableRow) - widthDeviation = np.abs(widths - meanWidths) - stdWidths = np.nanstd(widths, axis=1)[:, np.newaxis] + # Store results for easier access when calculating means + self._fit_parameters[self._row_being_fit] = tableRow - # Put nan in places where width deviation is bigger than std - filterMask = widthDeviation > stdWidths - betterWidths = np.where(filterMask, np.nan, widths) + print(tableRow) - maskedIntensities = np.where(filterMask, np.nan, intensities) - betterIntensities = maskedIntensities / np.sum( - maskedIntensities, axis=0 - ) # Not nansum() + # Pass fit profiles into workspaces + individual_ncps = self._neutron_compton_profiles(fitPars) + for ncp, element in zip(individual_ncps, self._profiles.keys()): + self._fit_profiles_workspaces[element].dataY(self._row_being_fit)[:] = ncp - # When trying to estimate HToMassIdxRatio and normalization fails, skip normalization - if np.all(np.isnan(betterIntensities)) & IC.runningPreliminary: - assert IC.noOfMSIterations == 0, ( - "Calculation of mean intensities failed, cannot proceed with MS correction." - "Try to run again with noOfMSIterations=0." - ) - betterIntensities = maskedIntensities - else: - pass - - assert np.all(meanWidths != np.nan), "At least one mean of widths is nan!" - assert np.sum(filterMask) >= 1, "No widths survive filtering condition" - assert not (np.all(np.isnan(betterWidths))), "All filtered widths are nan" - assert not (np.all(np.isnan(betterIntensities))), "All filtered intensities are nan" - assert np.nanmax(betterWidths) != np.nanmin( - betterWidths - ), f"All fitered widths have the same value: {np.nanmin(betterWidths)}" - assert np.nanmax(betterIntensities) != np.nanmin( - betterIntensities - ), f"All fitered widths have the same value: {np.nanmin(betterIntensities)}" - - return betterWidths, betterIntensities - - -def fitNcpToSingleSpec( - dataY, dataE, ySpacesForEachMass, resolutionPars, instrPars, kinematicArrays, ic -): - """Fits the NCP and returns the best fit parameters for one spectrum""" - - if np.all(dataY == 0): - return np.zeros(len(ic.initPars) + 3) - - result = optimize.minimize( - errorFunction, - ic.initPars, - args=( - dataY, - dataE, - ySpacesForEachMass, - resolutionPars, - instrPars, - kinematicArrays, - ic, - ), - method="SLSQP", - bounds=ic.bounds, - constraints=ic.constraints, - ) + self._fit_profiles_workspaces['total'].dataY(self._row_being_fit)[:] = np.sum(individual_ncps, axis=0) + return - fitPars = result["x"] - noDegreesOfFreedom = len(dataY) - len(fitPars) - specFitPars = np.append(instrPars[0], fitPars) - return np.append(specFitPars, [result["fun"] / noDegreesOfFreedom, result["nit"]]) + def errorFunction(self, pars): + """Error function to be minimized, in TOF space""" + ncpForEachMass = self._neutron_compton_profiles(pars) + ncpTotal = np.sum(ncpForEachMass, axis=0) -def errorFunction( - pars, - dataY, - dataE, - ySpacesForEachMass, - resolutionPars, - instrPars, - kinematicArrays, - ic, -): - """Error function to be minimized, operates in TOF space""" + # Ignore any masked values from Jackknife or masked tof range + zerosMask = self._dataY[self._row_being_fit] == 0 + ncpTotal = ncpTotal[~zerosMask] + dataY = self._dataY[self._row_being_fit, ~zerosMask] + dataE = self._dataE[self._row_being_fit, ~zerosMask] - ncpForEachMass, ncpTotal = calculateNcpSpec( - ic, pars, ySpacesForEachMass, resolutionPars, instrPars, kinematicArrays - ) + if np.all(self._dataE[self._row_being_fit] == 0): # When errors not present + return np.sum((ncpTotal - dataY) ** 2) - # Ignore any masked values from Jackknife or masked tof range - zerosMask = dataY == 0 - ncpTotal = ncpTotal[~zerosMask] - dataYf = dataY[~zerosMask] - dataEf = dataE[~zerosMask] + return np.sum((ncpTotal - dataY) ** 2 / dataE**2) - if np.all(dataE == 0): # When errors not present - return np.sum((ncpTotal - dataYf) ** 2) - return np.sum((ncpTotal - dataYf) ** 2 / dataEf**2) + def _neutron_compton_profiles(self, pars): + """ + Neutron Compron Profile distribution on TOF space for a single spectrum. + Calculated from kinematics, J(y) and resolution functions. + """ + intensities = pars[::3].reshape(-1, 1) + widths = pars[1::3].reshape(-1, 1) + centers = pars[2::3].reshape(-1, 1) + masses = np.array([p.mass for p in self._profiles.values()]).reshape(-1, 1) -def calculateNcpSpec( - ic, pars, ySpacesForEachMass, resolutionPars, instrPars, kinematicArrays -): - """Creates a synthetic C(t) to be fitted to TOF values of a single spectrum, from J(y) and resolution functions - Shapes: datax (1, n), ySpacesForEachMass (4, n), res (4, 2), deltaQ (1, n), E0 (1,n), - where n is no of bins""" + v0, E0, deltaE, deltaQ = self._kinematic_arrays[self._row_being_fit] - masses, intensities, widths, centers = prepareArraysFromPars(ic, pars) - v0, E0, deltaE, deltaQ = kinematicArrays + gaussRes, lorzRes = self.caculateResolutionForEachMass(centers) + totalGaussWidth = np.sqrt(widths**2 + gaussRes**2) - gaussRes, lorzRes = caculateResolutionForEachMass( - masses, ySpacesForEachMass, centers, resolutionPars, instrPars, kinematicArrays - ) - totalGaussWidth = np.sqrt(widths**2 + gaussRes**2) + JOfY = self.pseudoVoigt(self._y_space_arrays[self._row_being_fit] - centers, totalGaussWidth, lorzRes) - JOfY = pseudoVoigt(ySpacesForEachMass - centers, totalGaussWidth, lorzRes, ic) + FSE = ( + -numericalThirdDerivative(self._y_space_arrays[self._row_being_fit], JOfY) + * widths**4 + / deltaQ + * 0.72 + ) + return intensities * (JOfY + FSE) * E0 * E0 ** (-0.92) * masses / deltaQ - FSE = ( - -numericalThirdDerivative(ySpacesForEachMass, JOfY) - * widths**4 - / deltaQ - * 0.72 - ) - ncpForEachMass = intensities * (JOfY + FSE) * E0 * E0 ** (-0.92) * masses / deltaQ - ncpTotal = np.sum(ncpForEachMass, axis=0) - return ncpForEachMass, ncpTotal + def caculateResolutionForEachMass(self, centers): + """Calculates the gaussian and lorentzian resolution + output: two column vectors, each row corresponds to each mass""" + gaussianResWidth = self.calcGaussianResolution(centers) + lorentzianResWidth = self.calcLorentzianResolution(centers) + return gaussianResWidth, lorentzianResWidth -def prepareArraysFromPars(ic, initPars): - """Extracts the intensities, widths and centers from the fitting parameters - Reshapes all of the arrays to collumns, for the calculation of the ncp,""" - masses = ic.masses[:, np.newaxis] - intensities = initPars[::3].reshape(masses.shape) - widths = initPars[1::3].reshape(masses.shape) - centers = initPars[2::3].reshape(masses.shape) - return masses, intensities, widths, centers + def kinematicsAtYCenters(self, centers): + """v0, E0, deltaE, deltaQ at the peak of the ncpTotal for each mass""" + shapeOfArrays = centers.shape + proximityToYCenters = np.abs(self._y_space_arrays[self._row_being_fit] - centers) + yClosestToCenters = proximityToYCenters.min(axis=1).reshape(shapeOfArrays) + yCentersMask = proximityToYCenters == yClosestToCenters -def caculateResolutionForEachMass( - masses, ySpacesForEachMass, centers, resolutionPars, instrPars, kinematicArrays -): - """Calculates the gaussian and lorentzian resolution - output: two column vectors, each row corresponds to each mass""" + v0, E0, deltaE, deltaQ = self._kinematic_arrays[self._row_being_fit] - v0, E0, delta_E, delta_Q = kinematicsAtYCenters( - ySpacesForEachMass, centers, kinematicArrays - ) + # Expand arrays to match shape of yCentersMask + v0 = v0 * np.ones(shapeOfArrays) + E0 = E0 * np.ones(shapeOfArrays) + deltaE = deltaE * np.ones(shapeOfArrays) + deltaQ = deltaQ * np.ones(shapeOfArrays) - gaussianResWidth = calcGaussianResolution( - masses, v0, E0, delta_E, delta_Q, resolutionPars, instrPars - ) - lorentzianResWidth = calcLorentzianResolution( - masses, v0, E0, delta_E, delta_Q, resolutionPars, instrPars - ) - return gaussianResWidth, lorentzianResWidth + v0 = v0[yCentersMask].reshape(shapeOfArrays) + E0 = E0[yCentersMask].reshape(shapeOfArrays) + deltaE = deltaE[yCentersMask].reshape(shapeOfArrays) + deltaQ = deltaQ[yCentersMask].reshape(shapeOfArrays) + return v0, E0, deltaE, deltaQ -def kinematicsAtYCenters(ySpacesForEachMass, centers, kinematicArrays): - """v0, E0, deltaE, deltaQ at the peak of the ncpTotal for each mass""" + def calcGaussianResolution(self, centers): + masses = np.array([p.mass for p in self._profiles.values()]).reshape(-1, 1) + v0, E0, delta_E, delta_Q = self.kinematicsAtYCenters(centers) + det, plick, angle, T0, L0, L1 = self._instrument_params[self._row_being_fit] + dE1, dTOF, dTheta, dL0, dL1, dE1_lorz = self._resolution_params[self._row_being_fit] + mN, Ef, en_to_vel, vf, hbar = loadConstants() - shapeOfArrays = centers.shape - proximityToYCenters = np.abs(ySpacesForEachMass - centers) - yClosestToCenters = proximityToYCenters.min(axis=1).reshape(shapeOfArrays) - yCentersMask = proximityToYCenters == yClosestToCenters + angle = angle * np.pi / 180 - v0, E0, deltaE, deltaQ = kinematicArrays + dWdE1 = 1.0 + (E0 / Ef) ** 1.5 * (L1 / L0) + dWdTOF = 2.0 * E0 * v0 / L0 + dWdL1 = 2.0 * E0**1.5 / Ef**0.5 / L0 + dWdL0 = 2.0 * E0 / L0 - # Expand arrays to match shape of yCentersMask - v0 = v0 * np.ones(shapeOfArrays) - E0 = E0 * np.ones(shapeOfArrays) - deltaE = deltaE * np.ones(shapeOfArrays) - deltaQ = deltaQ * np.ones(shapeOfArrays) + dW2 = ( + dWdE1**2 * dE1**2 + + dWdTOF**2 * dTOF**2 + + dWdL1**2 * dL1**2 + + dWdL0**2 * dL0**2 + ) + # conversion from meV^2 to A^-2, dydW = (M/q)^2 + dW2 *= (masses / hbar**2 / delta_Q) ** 2 - v0 = v0[yCentersMask].reshape(shapeOfArrays) - E0 = E0[yCentersMask].reshape(shapeOfArrays) - deltaE = deltaE[yCentersMask].reshape(shapeOfArrays) - deltaQ = deltaQ[yCentersMask].reshape(shapeOfArrays) - return v0, E0, deltaE, deltaQ + dQdE1 = ( + 1.0 + - (E0 / Ef) ** 1.5 * L1 / L0 + - np.cos(angle) * ((E0 / Ef) ** 0.5 - L1 / L0 * E0 / Ef) + ) + dQdTOF = 2.0 * E0 * v0 / L0 + dQdL1 = 2.0 * E0**1.5 / L0 / Ef**0.5 + dQdL0 = 2.0 * E0 / L0 + dQdTheta = 2.0 * np.sqrt(E0 * Ef) * np.sin(angle) + + dQ2 = ( + dQdE1**2 * dE1**2 + + (dQdTOF**2 * dTOF**2 + dQdL1**2 * dL1**2 + dQdL0**2 * dL0**2) + * np.abs(Ef / E0 * np.cos(angle) - 1) + + dQdTheta**2 * dTheta**2 + ) + dQ2 *= (mN / hbar**2 / delta_Q) ** 2 + # in A-1 #same as dy^2 = (dy/dw)^2*dw^2 + (dy/dq)^2*dq^2 + gaussianResWidth = np.sqrt(dW2 + dQ2) + return gaussianResWidth -def calcGaussianResolution(masses, v0, E0, delta_E, delta_Q, resolutionPars, instrPars): - # Currently the function that takes the most time in the fitting - assert masses.shape == ( - masses.size, - 1, - ), f"masses.shape: {masses.shape}. The shape of the masses array needs to be a collumn!" - det, plick, angle, T0, L0, L1 = instrPars - dE1, dTOF, dTheta, dL0, dL1, dE1_lorz = resolutionPars - mN, Ef, en_to_vel, vf, hbar = loadConstants() + def calcLorentzianResolution(self, centers): + masses = np.array([p.mass for p in self._profiles.values()]).reshape(-1, 1) + v0, E0, delta_E, delta_Q = self.kinematicsAtYCenters(centers) + det, plick, angle, T0, L0, L1 = self._instrument_params[self._row_being_fit] + dE1, dTOF, dTheta, dL0, dL1, dE1_lorz = self._resolution_params[self._row_being_fit] + mN, Ef, en_to_vel, vf, hbar = loadConstants() - angle = angle * np.pi / 180 + angle = angle * np.pi / 180 - dWdE1 = 1.0 + (E0 / Ef) ** 1.5 * (L1 / L0) - dWdTOF = 2.0 * E0 * v0 / L0 - dWdL1 = 2.0 * E0**1.5 / Ef**0.5 / L0 - dWdL0 = 2.0 * E0 / L0 + dWdE1_lor = (1.0 + (E0 / Ef) ** 1.5 * (L1 / L0)) ** 2 + # conversion from meV^2 to A^-2 + dWdE1_lor *= (masses / hbar**2 / delta_Q) ** 2 - dW2 = ( - dWdE1**2 * dE1**2 - + dWdTOF**2 * dTOF**2 - + dWdL1**2 * dL1**2 - + dWdL0**2 * dL0**2 - ) - # conversion from meV^2 to A^-2, dydW = (M/q)^2 - dW2 *= (masses / hbar**2 / delta_Q) ** 2 + dQdE1_lor = ( + 1.0 + - (E0 / Ef) ** 1.5 * L1 / L0 + - np.cos(angle) * ((E0 / Ef) ** 0.5 + L1 / L0 * E0 / Ef) + ) ** 2 + dQdE1_lor *= (mN / hbar**2 / delta_Q) ** 2 - dQdE1 = ( - 1.0 - - (E0 / Ef) ** 1.5 * L1 / L0 - - np.cos(angle) * ((E0 / Ef) ** 0.5 - L1 / L0 * E0 / Ef) - ) - dQdTOF = 2.0 * E0 * v0 / L0 - dQdL1 = 2.0 * E0**1.5 / L0 / Ef**0.5 - dQdL0 = 2.0 * E0 / L0 - dQdTheta = 2.0 * np.sqrt(E0 * Ef) * np.sin(angle) - - dQ2 = ( - dQdE1**2 * dE1**2 - + (dQdTOF**2 * dTOF**2 + dQdL1**2 * dL1**2 + dQdL0**2 * dL0**2) - * np.abs(Ef / E0 * np.cos(angle) - 1) - + dQdTheta**2 * dTheta**2 - ) - dQ2 *= (mN / hbar**2 / delta_Q) ** 2 - - # in A-1 #same as dy^2 = (dy/dw)^2*dw^2 + (dy/dq)^2*dq^2 - gaussianResWidth = np.sqrt(dW2 + dQ2) - return gaussianResWidth - - -def calcLorentzianResolution( - masses, v0, E0, delta_E, delta_Q, resolutionPars, instrPars -): - assert masses.shape == ( - masses.size, - 1, - ), "The shape of the masses array needs to be a collumn!" - - det, plick, angle, T0, L0, L1 = instrPars - dE1, dTOF, dTheta, dL0, dL1, dE1_lorz = resolutionPars - mN, Ef, en_to_vel, vf, hbar = loadConstants() - - angle = angle * np.pi / 180 - - dWdE1_lor = (1.0 + (E0 / Ef) ** 1.5 * (L1 / L0)) ** 2 - # conversion from meV^2 to A^-2 - dWdE1_lor *= (masses / hbar**2 / delta_Q) ** 2 - - dQdE1_lor = ( - 1.0 - - (E0 / Ef) ** 1.5 * L1 / L0 - - np.cos(angle) * ((E0 / Ef) ** 0.5 + L1 / L0 * E0 / Ef) - ) ** 2 - dQdE1_lor *= (mN / hbar**2 / delta_Q) ** 2 - - lorentzianResWidth = np.sqrt(dWdE1_lor + dQdE1_lor) * dE1_lorz # in A-1 - return lorentzianResWidth - - -def loadConstants(): - """Output: the mass of the neutron, final energy of neutrons (selected by gold foil), - factor to change energies into velocities, final velocity of neutron and hbar""" - mN = 1.008 # a.m.u. - Ef = 4906.0 # meV - en_to_vel = 4.3737 * 1.0e-4 - vf = np.sqrt(Ef) * en_to_vel # m/us - hbar = 2.0445 - return mN, Ef, en_to_vel, vf, hbar - - -def pseudoVoigt(x, sigma, gamma, IC): - """Convolution between Gaussian with std sigma and Lorentzian with HWHM gamma""" - fg, fl = 2.0 * sigma * np.sqrt(2.0 * np.log(2.0)), 2.0 * gamma - f = 0.5346 * fl + np.sqrt(0.2166 * fl**2 + fg**2) - eta = 1.36603 * fl / f - 0.47719 * (fl / f) ** 2 + 0.11116 * (fl / f) ** 3 - sigma_v, gamma_v = f / (2.0 * np.sqrt(2.0 * np.log(2.0))), f / 2.0 - pseudo_voigt = eta * lorentizian(x, gamma_v) + (1.0 - eta) * gaussian(x, sigma_v) - - norm = ( - np.abs(np.trapz(pseudo_voigt, x, axis=1))[:, np.newaxis] if IC.normVoigt else 1 - ) - return pseudo_voigt / norm + lorentzianResWidth = np.sqrt(dWdE1_lor + dQdE1_lor) * dE1_lorz # in A-1 + return lorentzianResWidth -def gaussian(x, sigma): - """Gaussian function centered at zero""" - gaussian = np.exp(-(x**2) / 2 / sigma**2) - gaussian /= np.sqrt(2.0 * np.pi) * sigma - return gaussian + def pseudoVoigt(self, x, sigma, gamma): + """Convolution between Gaussian with std sigma and Lorentzian with HWHM gamma""" + fg, fl = 2.0 * sigma * np.sqrt(2.0 * np.log(2.0)), 2.0 * gamma + f = 0.5346 * fl + np.sqrt(0.2166 * fl**2 + fg**2) + eta = 1.36603 * fl / f - 0.47719 * (fl / f) ** 2 + 0.11116 * (fl / f) ** 3 + sigma_v, gamma_v = f / (2.0 * np.sqrt(2.0 * np.log(2.0))), f / 2.0 + pseudo_voigt = eta * lorentizian(x, gamma_v) + (1.0 - eta) * gaussian(x, sigma_v) + norm = ( + np.abs(np.trapz(pseudo_voigt, x, axis=1))[:, np.newaxis] if self._run_norm_voigt else 1 + ) + return pseudo_voigt / norm + + + # When interface is updated, uncomment to change the way + # constraints are handled: + + # def _get_parsed_constraints(self): + # + # parsed_constraints = [] + # + # for constraint in self._constraints: + # constraint['fun'] = self._get_parsed_constraint_function(constraint['fun']) + # + # parsed_constraints.append(constraint) + # + # return parsed_constraints + # + # + # def _get_parsed_constraint_function(self, function_string: str): + # + # profile_order = [key for key in self._profiles.keys()] + # attribute_order = ['intensity', 'width', 'center'] + # + # words = function_string.split(' ') + # for i, word in enumerate(words): + # if '.' in word: + # + # try: # Skip floats + # float(word) + # except ValueError: + # continue + # + # profile, attribute = word + # words[i] = f"pars[{profile_order.index(profile) + attribute_order.index(attribute)}]" + # + # return eval(f"lambda pars: {' '.join(words)}") + + + def _replace_zero_columns_with_ncp_fit(self): + """ + If the initial input contains columns with zeros + (to mask resonance peaks) then these sections must be approximated + by the total fitted function because multiple scattering and + gamma correction algorithms do not accept columns with zeros. + If no masked columns are present then the input workspace + for corrections is left unchanged. + """ + dataY = self._workspace_for_corrections.extractY() + ncp = self._fit_profiles_workspaces['total'].extractY() + + self._zero_columns_boolean_mask = np.all(dataY == 0, axis=0) # Masked Cols + + self._workspace_for_corrections = CloneWorkspace( + InputWorkspace=self._workspace_for_corrections.name(), + OutputWorkspace=self._workspace_for_corrections.name() + "_CorrectionsInput" + ) + for row in range(self._workspace_for_corrections.getNumberHistograms()): + # TODO: Once the option to chage point to hist is removed, remove [:len(ncp)] + self._workspace_for_corrections.dataY(row)[self._zero_columns_boolean_mask] = ncp[row, self._zero_columns_boolean_mask[:len(ncp[row])]] -def lorentizian(x, gamma): - """Lorentzian centered at zero""" - lorentzian = gamma / np.pi / (x**2 + gamma**2) - return lorentzian + SumSpectra( + InputWorkspace=self._workspace_for_corrections.name(), + OutputWorkspace=self._workspace_for_corrections.name() + "_Sum" + ) + return -def numericalThirdDerivative(x, fun): - k6 = (-fun[:, 12:] + fun[:, :-12]) * 1 - k5 = (+fun[:, 11:-1] - fun[:, 1:-11]) * 24 - k4 = (-fun[:, 10:-2] + fun[:, 2:-10]) * 192 - k3 = (+fun[:, 9:-3] - fun[:, 3:-9]) * 488 - k2 = (+fun[:, 8:-4] - fun[:, 4:-8]) * 387 - k1 = (-fun[:, 7:-5] + fun[:, 5:-7]) * 1584 + def _remask_columns_with_zeros(self, ws_to_remask_name): + """ + Uses previously stored information on masked columns in the + initial workspace to set these columns again to zero on the + workspace resulting from the multiple scattering or gamma correction. + """ + ws_to_remask = mtd[ws_to_remask_name] + for row in range(ws_to_remask.getNumberHistograms()): + ws_to_remask.dataY(row)[self._zero_columns_boolean_mask] = 0 + ws_to_remask.dataE(row)[self._zero_columns_boolean_mask] = 0 + return - dev = k1 + k2 + k3 + k4 + k5 + k6 - dev /= np.power(x[:, 7:-5] - x[:, 6:-6], 3) - dev /= 12**3 - derivative = np.zeros(fun.shape) - derivative[:, 6:-6] = dev - # Padded with zeros left and right to return array with same shape - return derivative + def _correct_for_gamma_and_multiple_scattering(self, ws_name): + if self._gamma_correction: + gamma_correction_ws = self.create_gamma_workspaces() + Minus( + LHSWorkspace=ws_name, + RHSWorkspace=gamma_correction_ws.name(), + OutputWorkspace=ws_name + ) -def createWorkspacesForMSCorrection(ic, meanWidths, meanIntensityRatios, wsNCPM): - """Creates _MulScattering and _TotScattering workspaces used for the MS correction""" + if self._multiple_scattering_correction: + multiple_scattering_ws = self.create_multiple_scattering_workspaces() + Minus( + LHSWorkspace=ws_name, + RHSWorkspace=multiple_scattering_ws.name(), + OutputWorkspace=ws_name + ) + return - createSlabGeometry(ic, wsNCPM) # Sample properties for MS correction - sampleProperties = calcMSCorrectionSampleProperties( - ic, meanWidths, meanIntensityRatios - ) - print( - "\nThe sample properties for Multiple Scattering correction are:\n\n", - sampleProperties, - "\n", - ) + def create_multiple_scattering_workspaces(self): + """Creates _MulScattering and _TotScattering workspaces used for the MS correction""" - return createMulScatWorkspaces(ic, wsNCPM, sampleProperties) + self.createSlabGeometry(self._workspace_for_corrections) # Sample properties for MS correction + sampleProperties = self.calcMSCorrectionSampleProperties(self._mean_widths, self._mean_intensity_ratios) + print( + "\nThe sample properties for Multiple Scattering correction are:\n\n", + sampleProperties, + "\n", + ) -def createSlabGeometry(ic, wsNCPM): - half_height, half_width, half_thick = ( - 0.5 * ic.vertical_width, - 0.5 * ic.horizontal_width, - 0.5 * ic.thickness, - ) - xml_str = ( - ' ' - + ' ' - % (half_width, -half_height, half_thick) - + ' ' - % (half_width, half_height, half_thick) - + ' ' - % (half_width, -half_height, -half_thick) - + ' ' - % (-half_width, -half_height, half_thick) - + "" - ) + return self.createMulScatWorkspaces(self._workspace_for_corrections, sampleProperties) - CreateSampleShape(wsNCPM, xml_str) + def createSlabGeometry(self, wsNCPM): + half_height, half_width, half_thick = ( + 0.5 * self._vertical_width, + 0.5 * self._horizontal_width, + 0.5 * self._thickness, + ) + xml_str = ( + ' ' + + ' ' + % (half_width, -half_height, half_thick) + + ' ' + % (half_width, half_height, half_thick) + + ' ' + % (half_width, -half_height, -half_thick) + + ' ' + % (-half_width, -half_height, half_thick) + + "" + ) -def calcMSCorrectionSampleProperties(ic, meanWidths, meanIntensityRatios): - masses = ic.masses.flatten() + CreateSampleShape(self._workspace_for_corrections, xml_str) - # If Backsscattering mode and H is present in the sample, add H to MS properties - if ic.modeRunning == "BACKWARD": - if ic.HToMassIdxRatio is not None: # If H is present, ratio is a number - masses = np.append(masses, 1.0079) - meanWidths = np.append(meanWidths, 5.0) - HIntensity = ic.HToMassIdxRatio * meanIntensityRatios[ic.massIdx] - meanIntensityRatios = np.append(meanIntensityRatios, HIntensity) - meanIntensityRatios /= np.sum(meanIntensityRatios) + def calcMSCorrectionSampleProperties(self, meanWidths, meanIntensityRatios): + masses = [p.mass for p in self._profiles.values()] - MSProperties = np.zeros(3 * len(masses)) - MSProperties[::3] = masses - MSProperties[1::3] = meanIntensityRatios - MSProperties[2::3] = meanWidths - sampleProperties = list(MSProperties) + # If Backsscattering mode and H is present in the sample, add H to MS properties + if self._mode_running == "BACKWARD": + if self._h_ratio is not None: # If H is present, ratio is a number + masses = np.append(masses, 1.0079) + meanWidths = np.append(meanWidths, 5.0) - return sampleProperties + HIntensity = self._h_ratio * meanIntensityRatios[np.argmin(masses)] + meanIntensityRatios = np.append(meanIntensityRatios, HIntensity) + meanIntensityRatios /= np.sum(meanIntensityRatios) + MSProperties = np.zeros(3 * len(masses)) + MSProperties[::3] = masses + MSProperties[1::3] = meanIntensityRatios + MSProperties[2::3] = meanWidths + sampleProperties = list(MSProperties) -def createMulScatWorkspaces(ic, ws, sampleProperties): - """Uses the Mantid algorithm for the MS correction to create two Workspaces _TotScattering and _MulScattering""" + return sampleProperties - print("\nEvaluating the Multiple Scattering Correction...\n") - # selects only the masses, every 3 numbers - MS_masses = sampleProperties[::3] - # same as above, but starts at first intensities - MS_amplitudes = sampleProperties[1::3] - dens, trans = VesuvioThickness( - Masses=MS_masses, - Amplitudes=MS_amplitudes, - TransmissionGuess=ic.transmission_guess, - Thickness=0.1, - ) + def createMulScatWorkspaces(self, ws, sampleProperties): + """Uses the Mantid algorithm for the MS correction to create two Workspaces _TotScattering and _MulScattering""" - _TotScattering, _MulScattering = VesuvioCalculateMS( - ws, - NoOfMasses=len(MS_masses), - SampleDensity=dens.cell(9, 1), - AtomicProperties=sampleProperties, - BeamRadius=2.5, - NumScatters=ic.multiple_scattering_order, - NumEventsPerRun=int(ic.number_of_events), - ) + print("\nEvaluating the Multiple Scattering Correction...\n") + # selects only the masses, every 3 numbers + MS_masses = sampleProperties[::3] + # same as above, but starts at first intensities + MS_amplitudes = sampleProperties[1::3] - data_normalisation = Integration(ws) - simulation_normalisation = Integration("_TotScattering") - for workspace in ("_MulScattering", "_TotScattering"): - Divide( - LHSWorkspace=workspace, - RHSWorkspace=simulation_normalisation, - OutputWorkspace=workspace, - ) - Multiply( - LHSWorkspace=workspace, - RHSWorkspace=data_normalisation, - OutputWorkspace=workspace, + dens, trans = VesuvioThickness( + Masses=MS_masses, + Amplitudes=MS_amplitudes, + TransmissionGuess=self._transmission_guess, + Thickness=0.1, ) - RenameWorkspace(InputWorkspace=workspace, OutputWorkspace=ws.name() + workspace) - SumSpectra( - ws.name() + workspace, OutputWorkspace=ws.name() + workspace + "_Sum" + + _TotScattering, _MulScattering = VesuvioCalculateMS( + ws, + NoOfMasses=len(MS_masses), + SampleDensity=dens.cell(9, 1), + AtomicProperties=sampleProperties, + BeamRadius=2.5, + NumScatters=self._multiple_scattering_order, + NumEventsPerRun=int(self._number_of_events), ) - DeleteWorkspaces([data_normalisation, simulation_normalisation, trans, dens]) - # The only remaining workspaces are the _MulScattering and _TotScattering - return mtd[ws.name() + "_MulScattering"] + data_normalisation = Integration(ws) + simulation_normalisation = Integration("_TotScattering") + for workspace in ("_MulScattering", "_TotScattering"): + Divide( + LHSWorkspace=workspace, + RHSWorkspace=simulation_normalisation, + OutputWorkspace=workspace, + ) + Multiply( + LHSWorkspace=workspace, + RHSWorkspace=data_normalisation, + OutputWorkspace=workspace, + ) + RenameWorkspace(InputWorkspace=workspace, OutputWorkspace=ws.name() + workspace) + SumSpectra( + ws.name() + workspace, OutputWorkspace=ws.name() + workspace + "_Sum" + ) + DeleteWorkspaces([data_normalisation, simulation_normalisation, trans, dens]) + # The only remaining workspaces are the _MulScattering and _TotScattering + return mtd[ws.name() + "_MulScattering"] -def createWorkspacesForGammaCorrection(ic, meanWidths, meanIntensityRatios, wsNCPM): - """Creates _gamma_background correction workspace to be subtracted from the main workspace""" - inputWS = wsNCPM.name() + def create_gamma_workspaces(self): + """Creates _gamma_background correction workspace to be subtracted from the main workspace""" - profiles = calcGammaCorrectionProfiles(ic.masses, meanWidths, meanIntensityRatios) + inputWS = self._workspace_for_corrections.name() - # Approach below not currently suitable for current versions of Mantid, but will be in the future - # background, corrected = VesuvioCalculateGammaBackground(InputWorkspace=inputWS, ComptonFunction=profiles) - # DeleteWorkspace(corrected) - # RenameWorkspace(InputWorkspace= background, OutputWorkspace = inputWS+"_Gamma_Background") + profiles = self.calcGammaCorrectionProfiles(self._mean_widths, self._mean_intensity_ratios) - ws = CloneWorkspace(InputWorkspace=inputWS, OutputWorkspace="tmpGC") - for spec in range(ws.getNumberHistograms()): - background, corrected = VesuvioCalculateGammaBackground( - InputWorkspace=inputWS, ComptonFunction=profiles, WorkspaceIndexList=spec - ) - ws.dataY(spec)[:], ws.dataE(spec)[:] = ( - background.dataY(0)[:], - background.dataE(0)[:], + # Approach below not currently suitable for current versions of Mantid, but will be in the future + # background, corrected = VesuvioCalculateGammaBackground(InputWorkspace=inputWS, ComptonFunction=profiles) + # DeleteWorkspace(corrected) + # RenameWorkspace(InputWorkspace= background, OutputWorkspace = inputWS+"_Gamma_Background") + + ws = CloneWorkspace(InputWorkspace=inputWS, OutputWorkspace="tmpGC") + for spec in range(ws.getNumberHistograms()): + background, corrected = VesuvioCalculateGammaBackground( + InputWorkspace=inputWS, ComptonFunction=profiles, WorkspaceIndexList=spec + ) + ws.dataY(spec)[:], ws.dataE(spec)[:] = ( + background.dataY(0)[:], + background.dataE(0)[:], + ) + DeleteWorkspace(background) + DeleteWorkspace(corrected) + RenameWorkspace( + InputWorkspace="tmpGC", OutputWorkspace=inputWS + "_Gamma_Background" ) - DeleteWorkspace(background) - DeleteWorkspace(corrected) - RenameWorkspace( - InputWorkspace="tmpGC", OutputWorkspace=inputWS + "_Gamma_Background" - ) - Scale( - InputWorkspace=inputWS + "_Gamma_Background", - OutputWorkspace=inputWS + "_Gamma_Background", - Factor=0.9, - Operation="Multiply", - ) - return mtd[inputWS + "_Gamma_Background"] - - -def calcGammaCorrectionProfiles(masses, meanWidths, meanIntensityRatios): - masses = masses.flatten() - profiles = "" - for mass, width, intensity in zip(masses, meanWidths, meanIntensityRatios): - profiles += ( - "name=GaussianComptonProfile,Mass=" - + str(mass) - + ",Width=" - + str(width) - + ",Intensity=" - + str(intensity) - + ";" + Scale( + InputWorkspace=inputWS + "_Gamma_Background", + OutputWorkspace=inputWS + "_Gamma_Background", + Factor=0.9, + Operation="Multiply", ) - print("\n The sample properties for Gamma Correction are:\n", profiles) - return profiles + return mtd[inputWS + "_Gamma_Background"] + + + def calcGammaCorrectionProfiles(self, meanWidths, meanIntensityRatios): + masses = [p.mass for p in self._profiles.values()] + profiles = "" + for mass, width, intensity in zip(masses, meanWidths, meanIntensityRatios): + profiles += ( + "name=GaussianComptonProfile,Mass=" + + str(mass) + + ",Width=" + + str(width) + + ",Intensity=" + + str(intensity) + + ";" + ) + print("\n The sample properties for Gamma Correction are:\n", profiles) + return profiles -class resultsObject: - """Used to collect results from workspaces and store them in .npz files for testing.""" + def _set_results(self): + """Used to collect results from workspaces and store them in .npz files for testing.""" + + self.wsFinal = mtd[self._name + str(self._number_of_iterations)] - def __init__(self, ic): allIterNcp = [] allFitWs = [] allTotNcp = [] @@ -1120,18 +1059,18 @@ def __init__(self, ic): j = 0 while True: try: - wsIterName = ic.name + str(j) + wsIterName = self._name + str(j) # Extract ws that were fitted ws = mtd[wsIterName] allFitWs.append(ws.extractY()) # Extract total ncp - totNcpWs = mtd[wsIterName + "_TOF_Fitted_Profiles"] + totNcpWs = mtd[wsIterName + "_total_ncp"] allTotNcp.append(totNcpWs.extractY()) # Extract best fit parameters - fitParTable = mtd[wsIterName + "_Best_Fit_NCP_Parameters"] + fitParTable = mtd[wsIterName + "_fit_results"] bestFitPars = [] for key in fitParTable.keys(): bestFitPars.append(fitParTable.column(key)) @@ -1139,21 +1078,16 @@ def __init__(self, ic): # Extract individual ncp allNCP = [] - i = 0 - while True: # By default, looks for all ncp ws until it breaks - try: - ncpWsToAppend = mtd[ - wsIterName + "_TOF_Fitted_Profile_" + str(i) - ] - allNCP.append(ncpWsToAppend.extractY()) - i += 1 - except KeyError: - break - allNCP = switchFirstTwoAxis(np.array(allNCP)) + for p in self._profiles.values(): + ncpWsToAppend = mtd[ + wsIterName + f"_{p.label}_ncp" + ] + allNCP.append(ncpWsToAppend.extractY()) + allNCP = np.swapaxes(np.array(allNCP), 0, 1) allIterNcp.append(allNCP) # Extract Mean and Std Widths, Intensities - meansTable = mtd[wsIterName + "_Mean_Widths_And_Intensities"] + meansTable = mtd[wsIterName + "_MeanWidthsAndIntensities"] allMeanWidhts.append(meansTable.column("Mean Widths")) allStdWidths.append(meansTable.column("Std Widths")) allMeanIntensities.append(meansTable.column("Mean Intensities")) @@ -1173,24 +1107,22 @@ def __init__(self, ic): self.all_std_widths = np.array(allStdWidths) self.all_std_intensities = np.array(allStdIntensities) - # Pass all attributes of ic into attributes to be used whithin this object - self.maskedDetectorIdx = ic.maskedDetectorIdx - self.masses = ic.masses - self.noOfMasses = ic.noOfMasses - self.resultsSavePath = ic.resultsSavePath - - def save(self): + def _save_results(self): """Saves all of the arrays stored in this object""" + maskedDetectorIdx = np.array(self._mask_spectra) - min(self._workspace_being_fit.getSpectrumNumbers()) + # TODO: Take out nans next time when running original results # Because original results were recently saved with nans, mask spectra with nans - self.all_spec_best_par_chi_nit[:, self.maskedDetectorIdx, :] = np.nan - self.all_ncp_for_each_mass[:, self.maskedDetectorIdx, :, :] = np.nan - self.all_tot_ncp[:, self.maskedDetectorIdx, :] = np.nan + self.all_spec_best_par_chi_nit[:, maskedDetectorIdx, :] = np.nan + self.all_ncp_for_each_mass[:, maskedDetectorIdx, :, :] = np.nan + self.all_tot_ncp[:, maskedDetectorIdx, :] = np.nan + + if not self._save_results_path: + return - savePath = self.resultsSavePath np.savez( - savePath, + self._save_results_path, all_fit_workspaces=self.all_fit_workspaces, all_spec_best_par_chi_nit=self.all_spec_best_par_chi_nit, all_mean_widths=self.all_mean_widths, @@ -1200,3 +1132,4 @@ def save(self): all_tot_ncp=self.all_tot_ncp, all_ncp_for_each_mass=self.all_ncp_for_each_mass, ) + diff --git a/src/mvesuvio/analysis_routines.py b/src/mvesuvio/analysis_routines.py index 7079b681..7536b558 100644 --- a/src/mvesuvio/analysis_routines.py +++ b/src/mvesuvio/analysis_routines.py @@ -1,9 +1,9 @@ # from .analysis_reduction import iterativeFitForDataReduction from mantid.api import AnalysisDataService from mantid.simpleapi import CreateEmptyTableWorkspace -from mvesuvio.oop.analysis_helpers import loadRawAndEmptyWsFromUserPath, cropAndMaskWorkspace -from mvesuvio.oop.AnalysisRoutine import AnalysisRoutine -from mvesuvio.oop.NeutronComptonProfile import NeutronComptonProfile +from mvesuvio.util.analysis_helpers import loadRawAndEmptyWsFromUserPath, cropAndMaskWorkspace +from mvesuvio.analysis_reduction import AnalysisRoutine +from mvesuvio.analysis_reduction import NeutronComptonProfile import numpy as np diff --git a/src/mvesuvio/oop/AnalysisRoutine.py b/src/mvesuvio/oop/AnalysisRoutine.py deleted file mode 100644 index 4f73b39c..00000000 --- a/src/mvesuvio/oop/AnalysisRoutine.py +++ /dev/null @@ -1,1116 +0,0 @@ -import numpy as np -import matplotlib.pyplot as plt -from scipy import optimize -from mantid.simpleapi import mtd, CreateEmptyTableWorkspace, SumSpectra, \ - CloneWorkspace, DeleteWorkspace, VesuvioCalculateGammaBackground, \ - VesuvioCalculateMS, Scale, RenameWorkspace, Minus, CreateSampleShape, \ - VesuvioThickness, Integration, Divide, Multiply, DeleteWorkspaces, \ - CreateWorkspace - -from mvesuvio.oop.NeutronComptonProfile import NeutronComptonProfile -from mvesuvio.oop.analysis_helpers import histToPointData, loadConstants, \ - gaussian, lorentizian, numericalThirdDerivative - - -np.set_printoptions(suppress=True, precision=4, linewidth=200) - - -class AnalysisRoutine: - - def __init__(self, workspace, ip_file, h_ratio_to_lowest_mass, number_of_iterations, mask_spectra, - multiple_scattering_correction, vertical_width, horizontal_width, thickness, - gamma_correction, mode_running, transmission_guess=None, multiple_scattering_order=None, - number_of_events=None, results_path=None, figures_path=None, constraints=()): - - self._name = workspace.name() - self._ip_file = ip_file - self._number_of_iterations = number_of_iterations - self._mask_spectra = mask_spectra - self._transmission_guess = transmission_guess - self._multiple_scattering_order = multiple_scattering_order - self._number_of_events = number_of_events - self._vertical_width = vertical_width - self._horizontal_width = horizontal_width - self._thickness = thickness - self._mode_running = mode_running - self._multiple_scattering_correction = multiple_scattering_correction - self._gamma_correction = gamma_correction - self._save_results_path = results_path - self._save_figures_path = figures_path - self._h_ratio = h_ratio_to_lowest_mass - self._constraints = constraints - - self._profiles = {} - - # Variables changing during fit - self._workspace_for_corrections = workspace - self._workspace_being_fit = workspace - self._row_being_fit = 0 - self._zero_columns_boolean_mask = None - self._table_fit_results = None - self._fit_profiles_workspaces = {} - - # Only used for system tests, remove once tests are updated - self._run_hist_data = True - self._run_norm_voigt = False - - - def add_profiles(self, *args: NeutronComptonProfile): - for profile in args: - self._profiles[profile.label] = profile - - - @property - def profiles(self): - return self._profiles - - - def set_initial_profiles_from(self, source: 'AnalysisRoutine'): - - # Set intensities - for p in self._profiles.values(): - if np.isclose(p.mass, 1, atol=0.1): # Hydrogen present - p.intensity = source._h_ratio * source._get_lightest_profile().mean_intensity - continue - p.intensity = source.profiles[p.label].mean_intensity - - # Normalise intensities - sum_intensities = sum([p.intensity for p in self._profiles.values()]) - for p in self._profiles.values(): - p.intensity /= sum_intensities - - # Set widths - for p in self._profiles.values(): - try: - p.width = source.profiles[p.label].mean_width - except KeyError: - continue - - # Fix all widths except lightest mass - for p in self._profiles.values(): - if p == self._get_lightest_profile(): - continue - p.width_bounds = [p.width, p.width] - - return - - - def _get_lightest_profile(self): - profiles = [p for p in self._profiles.values()] - masses = [p.mass for p in self._profiles.values()] - return profiles[np.argmin(masses)] - - - def calculate_h_ratio(self): - - if not np.isclose(self._get_lightest_profile().mass, 1, atol=0.1): # Hydrogen present - return None - - # Hydrogen is present - intensities = np.array([p.mean_intensity for p in self._profiles.values()]) - masses = np.array([p.mass for p in self._profiles.values()]) - - sorted_intensities = intensities[np.argsort(masses)] - return sorted_intensities[0] / sorted_intensities[1] - - - @property - def profiles(self): - return self._profiles - - - @profiles.setter - def profiles(self, incoming_profiles): - assert(isinstance(incoming_profiles, dict)) - common_keys = self._profiles.keys() & incoming_profiles.keys() - common_keys_profiles = {k: incoming_profiles[k] for k in common_keys} - self._profiles = {**self._profiles, **common_keys_profiles} - - - def _update_workspace_data(self): - - self._dataX = self._workspace_being_fit.extractX() - self._dataY = self._workspace_being_fit.extractY() - self._dataE = self._workspace_being_fit.extractE() - - if self._run_hist_data: # Converts point data from workspaces to histogram data - self._dataY, self._dataX, self._dataE = histToPointData(self._dataY, self._dataX, self._dataE) - - self._set_up_kinematic_arrays() - - self._fit_parameters = np.zeros((len(self._dataY), 3 * len(self._profiles) + 3)) - self._row_being_fit = 0 - self._table_fit_results = self._initialize_table_fit_parameters() - - # Initialise workspaces for fitted ncp - self._fit_profiles_workspaces = {} - for element, p in self._profiles.items(): - self._fit_profiles_workspaces[element] = self._create_emtpy_ncp_workspace(f'_{element}_ncp') - self._fit_profiles_workspaces['total'] = self._create_emtpy_ncp_workspace(f'_total_ncp') - - # Initialise empty means - self._mean_widths = None - self._std_widths = None - self._mean_intensity_ratios = None - self._std_intensity_ratios = None - - - def _initialize_table_fit_parameters(self): - table = CreateEmptyTableWorkspace( - OutputWorkspace=self._workspace_being_fit.name()+ "_fit_results" - ) - table.setTitle("SciPy Fit Parameters") - table.addColumn(type="float", name="Spectrum") - for key in self._profiles.keys(): - table.addColumn(type="float", name=f"{key} Intensity") - table.addColumn(type="float", name=f"{key} Width") - table.addColumn(type="float", name=f"{key} Center ") - table.addColumn(type="float", name="Normalised Chi2") - table.addColumn(type="float", name="Number of Iteraions") - return table - - - @property - def mean_widths(self): - return self._mean_widths - - - @mean_widths.setter - def mean_widths(self, value): - self._mean_widths = value - for i, p in enumerate(self._profiles.values()): - p.mean_width = self._mean_widths[i] - return - - - @property - def mean_intensity_ratios(self): - return self._mean_intensity_ratios - - @mean_intensity_ratios.setter - def mean_intensity_ratios(self, value): - self._mean_intensity_ratios = value - for i, p in enumerate(self.profiles.values()): - p.mean_intensity = self._mean_intensity_ratios[i] - return - - - def _create_emtpy_ncp_workspace(self, suffix): - return CreateWorkspace( - DataX=self._dataX, - DataY=np.zeros(self._dataY.size), - DataE=np.zeros(self._dataE.size), - Nspec=self._workspace_being_fit.getNumberHistograms(), - OutputWorkspace=self._workspace_being_fit.name()+suffix, - ParentWorkspace=self._workspace_being_fit - ) - - - def _set_up_kinematic_arrays(self): - resolutionPars, instrPars, kinematicArrays, ySpacesForEachMass = self.prepareFitArgs() - self._resolution_params = resolutionPars - self._instrument_params = instrPars - self._kinematic_arrays = kinematicArrays - self._y_space_arrays = ySpacesForEachMass - - - def run(self): - - assert len(self.profiles) > 0, "Add profiles before atempting to run the routine!" - - self._create_table_initial_parameters() - - # Legacy code from Bootstrap - # if self.runningSampleWS: - # initialWs = RenameWorkspace( - # InputWorkspace=ic.sampleWS, OutputWorkspace=initialWs.name() - # ) - - CloneWorkspace( - InputWorkspace=self._workspace_being_fit.name(), - OutputWorkspace=self._name + '0' - ) - - for iteration in range(self._number_of_iterations + 1): - - self._workspace_being_fit = mtd[self._name + str(iteration)] - self._update_workspace_data() - - self._fit_neutron_compton_profiles() - - self._create_summed_workspaces() - self._save_plots() - self._set_means_and_std() - - # When last iteration, skip MS and GC - if iteration == self._number_of_iterations: - break - - # Do this because MS and Gamma corrections do not accept zero columns - if iteration==0: - self._replace_zero_columns_with_ncp_fit() - - CloneWorkspace( - InputWorkspace=self._workspace_for_corrections.name(), - OutputWorkspace="next_iteration" - ) - self._correct_for_gamma_and_multiple_scattering("next_iteration") - - # Need to remask columns of output of corrections - self._remask_columns_with_zeros("next_iteration") - - RenameWorkspace( - InputWorkspace="next_iteration", - OutputWorkspace=self._name + str(iteration + 1) - ) - - self._set_results() - self._save_results() - return self - - - def _create_table_initial_parameters(self): - meansTableWS = CreateEmptyTableWorkspace( - OutputWorkspace=self._name + "_Initial_Parameters" - ) - meansTableWS.addColumn(type="float", name="Mass") - meansTableWS.addColumn(type="float", name="Initial Widths") - meansTableWS.addColumn(type="str", name="Bounds Widths") - meansTableWS.addColumn(type="float", name="Initial Intensities") - meansTableWS.addColumn(type="str", name="Bounds Intensities") - meansTableWS.addColumn(type="float", name="Initial Centers") - meansTableWS.addColumn(type="str", name="Bounds Centers") - - print("\nCreated Table with Initial Parameters:") - for p in self._profiles.values(): - meansTableWS.addRow([p.mass, p.width, str(p.width_bounds), - p.intensity, str(p.intensity_bounds), - p.center, str(p.center_bounds)]) - print("\nMass: ", p.mass) - print(f"{'Initial Intensity:':>20s} {p.intensity:<8.3f} Bounds: {p.intensity_bounds}") - print(f"{'Initial Width:':>20s} {p.width:<8.3f} Bounds: {p.width_bounds}") - print(f"{'Initial Center:':>20s} {p.center:<8.3f} Bounds: {p.center_bounds}") - print("\n") - return - - - def _fit_neutron_compton_profiles(self): - """ - Performs the fit of neutron compton profiles to the workspace being fit. - The profiles are fit on a spectrum by spectrum basis. - """ - print("\nFitting Neutron Compron Prolfiles:\n") - - self._row_being_fit = 0 - while self._row_being_fit != len(self._dataY): - self._fit_neutron_compton_profiles_to_row() - self._row_being_fit += 1 - - assert np.any(self._fit_parameters), "Fitting parameters cannot be zero for all spectra!" - return - - - def prepareFitArgs(self): - instrPars = self.loadInstrParsFileIntoArray() - resolutionPars = self.loadResolutionPars(instrPars) - - v0, E0, delta_E, delta_Q = self.calculateKinematicsArrays(instrPars) - kinematicArrays = np.array([v0, E0, delta_E, delta_Q]) - ySpacesForEachMass = self.convertDataXToYSpacesForEachMass( - self._dataX, delta_Q, delta_E - ) - kinematicArrays = np.swapaxes(kinematicArrays, 0, 1) - ySpacesForEachMass = np.swapaxes(ySpacesForEachMass, 0, 1) - return resolutionPars, instrPars, kinematicArrays, ySpacesForEachMass - - - def loadInstrParsFileIntoArray(self): - """Loads instrument parameters into array, from the file in the specified path""" - - data = np.loadtxt(self._ip_file, dtype=str)[1:].astype(float) - spectra = data[:, 0] - - workspace_spectrum_list = self._workspace_being_fit.getSpectrumNumbers() - first_spec = min(workspace_spectrum_list) - last_spec = max(workspace_spectrum_list) - - select_rows = np.where((spectra >= first_spec) & (spectra <= last_spec)) - instrPars = data[select_rows] - return instrPars - - - @staticmethod - def loadResolutionPars(instrPars): - """Resolution of parameters to propagate into TOF resolution - Output: matrix with each parameter in each column""" - spectrums = instrPars[:, 0] - L = len(spectrums) - # For spec no below 135, back scattering detectors, mode is double difference - # For spec no 135 or above, front scattering detectors, mode is single difference - dE1 = np.where(spectrums < 135, 88.7, 73) # meV, STD - dE1_lorz = np.where(spectrums < 135, 40.3, 24) # meV, HFHM - dTOF = np.repeat(0.37, L) # us - dTheta = np.repeat(0.016, L) # rad - dL0 = np.repeat(0.021, L) # meters - dL1 = np.repeat(0.023, L) # meters - - resolutionPars = np.vstack((dE1, dTOF, dTheta, dL0, dL1, dE1_lorz)).transpose() - return resolutionPars - - - def calculateKinematicsArrays(self, instrPars): - """Kinematics quantities calculated from TOF data""" - - dataX = self._dataX - - mN, Ef, en_to_vel, vf, hbar = loadConstants() - det, plick, angle, T0, L0, L1 = np.hsplit(instrPars, 6) # each is of len(dataX) - t_us = dataX - T0 # T0 is electronic delay due to instruments - v0 = vf * L0 / (vf * t_us - L1) - E0 = np.square( - v0 / en_to_vel - ) # en_to_vel is a factor used to easily change velocity to energy and vice-versa - - delta_E = E0 - Ef - delta_Q2 = ( - 2.0 - * mN - / hbar**2 - * (E0 + Ef - 2.0 * np.sqrt(E0 * Ef) * np.cos(angle / 180.0 * np.pi)) - ) - delta_Q = np.sqrt(delta_Q2) - return v0, E0, delta_E, delta_Q # shape(no of spectrums, no of bins) - - - def convertDataXToYSpacesForEachMass(self, dataX, delta_Q, delta_E): - "Calculates y spaces from TOF data, each row corresponds to one mass" - - # Prepare arrays to broadcast - dataX = dataX[np.newaxis, :, :] - delta_Q = delta_Q[np.newaxis, :, :] - delta_E = delta_E[np.newaxis, :, :] - - mN, Ef, en_to_vel, vf, hbar = loadConstants() - masses = np.array([p.mass for p in self._profiles.values()]).reshape(-1, 1, 1) - - energyRecoil = np.square(hbar * delta_Q) / 2.0 / masses - ySpacesForEachMass = ( - masses / hbar**2 / delta_Q * (delta_E - energyRecoil) - ) # y-scaling - return ySpacesForEachMass - - - def _save_plots(self): - # if IC.runningSampleWS: # Skip saving figure if running bootstrap - # return - - if not self._save_figures_path: - return - - lw = 2 - - fig, ax = plt.subplots(subplot_kw={"projection": "mantid"}) - - ws_data_sum = mtd[self._workspace_being_fit.name()+"_Sum"] - ax.errorbar(ws_data_sum, fmt="k.", label="Sum of spectra") - - for key, ws in self._fit_profiles_workspaces.items(): - ws_sum = mtd[ws.name()+"_Sum"] - ax.plot(ws_sum, label=f'Sum of {key} profile', linewidth=lw) - - ax.set_xlabel("TOF") - ax.set_ylabel("Counts") - ax.set_title("Sum of NCP fits") - ax.legend() - - fileName = self._workspace_being_fit.name() + "_profiles_sum.pdf" - savePath = self._save_figures_path / fileName - plt.savefig(savePath, bbox_inches="tight") - plt.close(fig) - return - - - def _create_summed_workspaces(self): - - SumSpectra( - InputWorkspace=self._workspace_being_fit.name(), - OutputWorkspace=self._workspace_being_fit.name() + "_Sum") - - for ws in self._fit_profiles_workspaces.values(): - SumSpectra( - InputWorkspace=ws.name(), - OutputWorkspace=ws.name() + "_Sum" - ) - - def _set_means_and_std(self): - """Calculate mean widths and intensities from tableWorkspace""" - - fitParsTable = self._table_fit_results - widths = np.zeros((len(self._profiles), fitParsTable.rowCount())) - intensities = np.zeros(widths.shape) - for i, p in enumerate(self._profiles.values()): - widths[i] = fitParsTable.column(f"{p.label} Width") - intensities[i] = fitParsTable.column(f"{p.label} Intensity") - ( - meanWidths, - stdWidths, - meanIntensityRatios, - stdIntensityRatios, - ) = self.calculateMeansAndStds(widths, intensities) - - assert ( - len(meanWidths) == len(self._profiles) - ), "Number of mean widths must match number of profiles!" - - self.mean_widths = meanWidths # Use setter - self._std_widths = stdWidths - self.mean_intensity_ratios = meanIntensityRatios # Use setter - self._std_intensity_ratios = stdIntensityRatios - - self.createMeansAndStdTableWS() - return - - - def createMeansAndStdTableWS(self): - meansTableWS = CreateEmptyTableWorkspace( - OutputWorkspace=self._workspace_being_fit.name() + "_MeanWidthsAndIntensities" - ) - meansTableWS.addColumn(type="str", name="Mass") - meansTableWS.addColumn(type="float", name="Mean Widths") - meansTableWS.addColumn(type="float", name="Std Widths") - meansTableWS.addColumn(type="float", name="Mean Intensities") - meansTableWS.addColumn(type="float", name="Std Intensities") - - print("\nCreated Table with means and std:") - print("\nMass Mean \u00B1 Std Widths Mean \u00B1 Std Intensities\n") - for p, mean_width, std_width, mean_intensity, std_intensity in zip( - self._profiles.values(), - self._mean_widths, - self._std_widths, - self._mean_intensity_ratios, - self._std_intensity_ratios, - ): - meansTableWS.addRow([p.label, mean_width, std_width, mean_intensity, std_intensity]) - print(f"{p.label:5s} {mean_width:10.5f} \u00B1 {std_width:7.5f} \ - {mean_intensity:10.5f} \u00B1 {std_intensity:7.5f}\n") - return - - - def calculateMeansAndStds(self, widthsIn, intensitiesIn): - betterWidths, betterIntensities = self.filterWidthsAndIntensities(widthsIn, intensitiesIn) - - meanWidths = np.nanmean(betterWidths, axis=1) - stdWidths = np.nanstd(betterWidths, axis=1) - - meanIntensityRatios = np.nanmean(betterIntensities, axis=1) - stdIntensityRatios = np.nanstd(betterIntensities, axis=1) - - return meanWidths, stdWidths, meanIntensityRatios, stdIntensityRatios - - - def filterWidthsAndIntensities(self, widthsIn, intensitiesIn): - """Puts nans in places to be ignored""" - - widths = widthsIn.copy() # Copy to avoid accidental changes in arrays - intensities = intensitiesIn.copy() - - zeroSpecs = np.all( - widths == 0, axis=0 - ) # Catches all failed fits, not just masked spectra - widths[:, zeroSpecs] = np.nan - intensities[:, zeroSpecs] = np.nan - - meanWidths = np.nanmean(widths, axis=1)[:, np.newaxis] - - widthDeviation = np.abs(widths - meanWidths) - stdWidths = np.nanstd(widths, axis=1)[:, np.newaxis] - - # Put nan in places where width deviation is bigger than std - filterMask = widthDeviation > stdWidths - betterWidths = np.where(filterMask, np.nan, widths) - - maskedIntensities = np.where(filterMask, np.nan, intensities) - betterIntensities = maskedIntensities / np.sum( - maskedIntensities, axis=0 - ) # Not nansum() - - # Keep this around in case it is needed again - # When trying to estimate HToMassIdxRatio and normalization fails, skip normalization - # if np.all(np.isnan(betterIntensities)) & IC.runningPreliminary: - # assert IC.noOfMSIterations == 0, ( - # "Calculation of mean intensities failed, cannot proceed with MS correction." - # "Try to run again with noOfMSIterations=0." - # ) - # betterIntensities = maskedIntensities - # else: - # pass - - assert np.all(meanWidths != np.nan), "At least one mean of widths is nan!" - assert np.sum(filterMask) >= 1, "No widths survive filtering condition" - assert not (np.all(np.isnan(betterWidths))), "All filtered widths are nan" - assert not (np.all(np.isnan(betterIntensities))), "All filtered intensities are nan" - assert np.nanmax(betterWidths) != np.nanmin( - betterWidths - ), f"All fitered widths have the same value: {np.nanmin(betterWidths)}" - assert np.nanmax(betterIntensities) != np.nanmin( - betterIntensities - ), f"All fitered widths have the same value: {np.nanmin(betterIntensities)}" - - return betterWidths, betterIntensities - - - def _fit_neutron_compton_profiles_to_row(self): - - if np.all(self._dataY[self._row_being_fit] == 0): - self._table_fit_results.addRow(np.zeros(3*len(self._profiles)+3)) - return - - # Need to transform profiles into parameter array for minimize - initial_parameters = [] - bounds = [] - for p in self._profiles.values(): - for attr in ['intensity', 'width', 'center']: - initial_parameters.append(getattr(p, attr)) - for attr in ['intensity_bounds', 'width_bounds', 'center_bounds']: - bounds.append(getattr(p, attr)) - - result = optimize.minimize( - self.errorFunction, - initial_parameters, - method="SLSQP", - bounds=bounds, - constraints=self._constraints, - ) - fitPars = result["x"] - - # Pass fit parameters to results table - noDegreesOfFreedom = len(self._dataY[self._row_being_fit]) - len(fitPars) - normalised_chi2 = result["fun"] / noDegreesOfFreedom - number_iterations = result["nit"] - spectrum_number = self._instrument_params[self._row_being_fit, 0] - tableRow = np.hstack((spectrum_number, fitPars, normalised_chi2, number_iterations)) - self._table_fit_results.addRow(tableRow) - - # Store results for easier access when calculating means - self._fit_parameters[self._row_being_fit] = tableRow - - print(tableRow) - - # Pass fit profiles into workspaces - individual_ncps = self._neutron_compton_profiles(fitPars) - for ncp, element in zip(individual_ncps, self._profiles.keys()): - self._fit_profiles_workspaces[element].dataY(self._row_being_fit)[:] = ncp - - self._fit_profiles_workspaces['total'].dataY(self._row_being_fit)[:] = np.sum(individual_ncps, axis=0) - return - - - def errorFunction(self, pars): - """Error function to be minimized, in TOF space""" - - ncpForEachMass = self._neutron_compton_profiles(pars) - ncpTotal = np.sum(ncpForEachMass, axis=0) - - # Ignore any masked values from Jackknife or masked tof range - zerosMask = self._dataY[self._row_being_fit] == 0 - ncpTotal = ncpTotal[~zerosMask] - dataY = self._dataY[self._row_being_fit, ~zerosMask] - dataE = self._dataE[self._row_being_fit, ~zerosMask] - - if np.all(self._dataE[self._row_being_fit] == 0): # When errors not present - return np.sum((ncpTotal - dataY) ** 2) - - return np.sum((ncpTotal - dataY) ** 2 / dataE**2) - - - def _neutron_compton_profiles(self, pars): - """ - Neutron Compron Profile distribution on TOF space for a single spectrum. - Calculated from kinematics, J(y) and resolution functions. - """ - - intensities = pars[::3].reshape(-1, 1) - widths = pars[1::3].reshape(-1, 1) - centers = pars[2::3].reshape(-1, 1) - masses = np.array([p.mass for p in self._profiles.values()]).reshape(-1, 1) - - v0, E0, deltaE, deltaQ = self._kinematic_arrays[self._row_being_fit] - - gaussRes, lorzRes = self.caculateResolutionForEachMass(centers) - totalGaussWidth = np.sqrt(widths**2 + gaussRes**2) - - JOfY = self.pseudoVoigt(self._y_space_arrays[self._row_being_fit] - centers, totalGaussWidth, lorzRes) - - FSE = ( - -numericalThirdDerivative(self._y_space_arrays[self._row_being_fit], JOfY) - * widths**4 - / deltaQ - * 0.72 - ) - return intensities * (JOfY + FSE) * E0 * E0 ** (-0.92) * masses / deltaQ - - - def caculateResolutionForEachMass(self, centers): - """Calculates the gaussian and lorentzian resolution - output: two column vectors, each row corresponds to each mass""" - - gaussianResWidth = self.calcGaussianResolution(centers) - lorentzianResWidth = self.calcLorentzianResolution(centers) - return gaussianResWidth, lorentzianResWidth - - - def kinematicsAtYCenters(self, centers): - """v0, E0, deltaE, deltaQ at the peak of the ncpTotal for each mass""" - - shapeOfArrays = centers.shape - proximityToYCenters = np.abs(self._y_space_arrays[self._row_being_fit] - centers) - yClosestToCenters = proximityToYCenters.min(axis=1).reshape(shapeOfArrays) - yCentersMask = proximityToYCenters == yClosestToCenters - - v0, E0, deltaE, deltaQ = self._kinematic_arrays[self._row_being_fit] - - # Expand arrays to match shape of yCentersMask - v0 = v0 * np.ones(shapeOfArrays) - E0 = E0 * np.ones(shapeOfArrays) - deltaE = deltaE * np.ones(shapeOfArrays) - deltaQ = deltaQ * np.ones(shapeOfArrays) - - v0 = v0[yCentersMask].reshape(shapeOfArrays) - E0 = E0[yCentersMask].reshape(shapeOfArrays) - deltaE = deltaE[yCentersMask].reshape(shapeOfArrays) - deltaQ = deltaQ[yCentersMask].reshape(shapeOfArrays) - return v0, E0, deltaE, deltaQ - - - def calcGaussianResolution(self, centers): - masses = np.array([p.mass for p in self._profiles.values()]).reshape(-1, 1) - v0, E0, delta_E, delta_Q = self.kinematicsAtYCenters(centers) - det, plick, angle, T0, L0, L1 = self._instrument_params[self._row_being_fit] - dE1, dTOF, dTheta, dL0, dL1, dE1_lorz = self._resolution_params[self._row_being_fit] - mN, Ef, en_to_vel, vf, hbar = loadConstants() - - angle = angle * np.pi / 180 - - dWdE1 = 1.0 + (E0 / Ef) ** 1.5 * (L1 / L0) - dWdTOF = 2.0 * E0 * v0 / L0 - dWdL1 = 2.0 * E0**1.5 / Ef**0.5 / L0 - dWdL0 = 2.0 * E0 / L0 - - dW2 = ( - dWdE1**2 * dE1**2 - + dWdTOF**2 * dTOF**2 - + dWdL1**2 * dL1**2 - + dWdL0**2 * dL0**2 - ) - # conversion from meV^2 to A^-2, dydW = (M/q)^2 - dW2 *= (masses / hbar**2 / delta_Q) ** 2 - - dQdE1 = ( - 1.0 - - (E0 / Ef) ** 1.5 * L1 / L0 - - np.cos(angle) * ((E0 / Ef) ** 0.5 - L1 / L0 * E0 / Ef) - ) - dQdTOF = 2.0 * E0 * v0 / L0 - dQdL1 = 2.0 * E0**1.5 / L0 / Ef**0.5 - dQdL0 = 2.0 * E0 / L0 - dQdTheta = 2.0 * np.sqrt(E0 * Ef) * np.sin(angle) - - dQ2 = ( - dQdE1**2 * dE1**2 - + (dQdTOF**2 * dTOF**2 + dQdL1**2 * dL1**2 + dQdL0**2 * dL0**2) - * np.abs(Ef / E0 * np.cos(angle) - 1) - + dQdTheta**2 * dTheta**2 - ) - dQ2 *= (mN / hbar**2 / delta_Q) ** 2 - - # in A-1 #same as dy^2 = (dy/dw)^2*dw^2 + (dy/dq)^2*dq^2 - gaussianResWidth = np.sqrt(dW2 + dQ2) - return gaussianResWidth - - - def calcLorentzianResolution(self, centers): - masses = np.array([p.mass for p in self._profiles.values()]).reshape(-1, 1) - v0, E0, delta_E, delta_Q = self.kinematicsAtYCenters(centers) - det, plick, angle, T0, L0, L1 = self._instrument_params[self._row_being_fit] - dE1, dTOF, dTheta, dL0, dL1, dE1_lorz = self._resolution_params[self._row_being_fit] - mN, Ef, en_to_vel, vf, hbar = loadConstants() - - angle = angle * np.pi / 180 - - dWdE1_lor = (1.0 + (E0 / Ef) ** 1.5 * (L1 / L0)) ** 2 - # conversion from meV^2 to A^-2 - dWdE1_lor *= (masses / hbar**2 / delta_Q) ** 2 - - dQdE1_lor = ( - 1.0 - - (E0 / Ef) ** 1.5 * L1 / L0 - - np.cos(angle) * ((E0 / Ef) ** 0.5 + L1 / L0 * E0 / Ef) - ) ** 2 - dQdE1_lor *= (mN / hbar**2 / delta_Q) ** 2 - - lorentzianResWidth = np.sqrt(dWdE1_lor + dQdE1_lor) * dE1_lorz # in A-1 - return lorentzianResWidth - - - def pseudoVoigt(self, x, sigma, gamma): - """Convolution between Gaussian with std sigma and Lorentzian with HWHM gamma""" - fg, fl = 2.0 * sigma * np.sqrt(2.0 * np.log(2.0)), 2.0 * gamma - f = 0.5346 * fl + np.sqrt(0.2166 * fl**2 + fg**2) - eta = 1.36603 * fl / f - 0.47719 * (fl / f) ** 2 + 0.11116 * (fl / f) ** 3 - sigma_v, gamma_v = f / (2.0 * np.sqrt(2.0 * np.log(2.0))), f / 2.0 - pseudo_voigt = eta * lorentizian(x, gamma_v) + (1.0 - eta) * gaussian(x, sigma_v) - - norm = ( - np.abs(np.trapz(pseudo_voigt, x, axis=1))[:, np.newaxis] if self._run_norm_voigt else 1 - ) - return pseudo_voigt / norm - - - # When interface is updated, uncomment to change the way - # constraints are handled: - - # def _get_parsed_constraints(self): - # - # parsed_constraints = [] - # - # for constraint in self._constraints: - # constraint['fun'] = self._get_parsed_constraint_function(constraint['fun']) - # - # parsed_constraints.append(constraint) - # - # return parsed_constraints - # - # - # def _get_parsed_constraint_function(self, function_string: str): - # - # profile_order = [key for key in self._profiles.keys()] - # attribute_order = ['intensity', 'width', 'center'] - # - # words = function_string.split(' ') - # for i, word in enumerate(words): - # if '.' in word: - # - # try: # Skip floats - # float(word) - # except ValueError: - # continue - # - # profile, attribute = word - # words[i] = f"pars[{profile_order.index(profile) + attribute_order.index(attribute)}]" - # - # return eval(f"lambda pars: {' '.join(words)}") - - - def _replace_zero_columns_with_ncp_fit(self): - """ - If the initial input contains columns with zeros - (to mask resonance peaks) then these sections must be approximated - by the total fitted function because multiple scattering and - gamma correction algorithms do not accept columns with zeros. - If no masked columns are present then the input workspace - for corrections is left unchanged. - """ - dataY = self._workspace_for_corrections.extractY() - ncp = self._fit_profiles_workspaces['total'].extractY() - - self._zero_columns_boolean_mask = np.all(dataY == 0, axis=0) # Masked Cols - - self._workspace_for_corrections = CloneWorkspace( - InputWorkspace=self._workspace_for_corrections.name(), - OutputWorkspace=self._workspace_for_corrections.name() + "_CorrectionsInput" - ) - for row in range(self._workspace_for_corrections.getNumberHistograms()): - # TODO: Once the option to chage point to hist is removed, remove [:len(ncp)] - self._workspace_for_corrections.dataY(row)[self._zero_columns_boolean_mask] = ncp[row, self._zero_columns_boolean_mask[:len(ncp[row])]] - - SumSpectra( - InputWorkspace=self._workspace_for_corrections.name(), - OutputWorkspace=self._workspace_for_corrections.name() + "_Sum" - ) - return - - - def _remask_columns_with_zeros(self, ws_to_remask_name): - """ - Uses previously stored information on masked columns in the - initial workspace to set these columns again to zero on the - workspace resulting from the multiple scattering or gamma correction. - """ - ws_to_remask = mtd[ws_to_remask_name] - for row in range(ws_to_remask.getNumberHistograms()): - ws_to_remask.dataY(row)[self._zero_columns_boolean_mask] = 0 - ws_to_remask.dataE(row)[self._zero_columns_boolean_mask] = 0 - return - - - def _correct_for_gamma_and_multiple_scattering(self, ws_name): - - if self._gamma_correction: - gamma_correction_ws = self.create_gamma_workspaces() - Minus( - LHSWorkspace=ws_name, - RHSWorkspace=gamma_correction_ws.name(), - OutputWorkspace=ws_name - ) - - if self._multiple_scattering_correction: - multiple_scattering_ws = self.create_multiple_scattering_workspaces() - Minus( - LHSWorkspace=ws_name, - RHSWorkspace=multiple_scattering_ws.name(), - OutputWorkspace=ws_name - ) - return - - - def create_multiple_scattering_workspaces(self): - """Creates _MulScattering and _TotScattering workspaces used for the MS correction""" - - self.createSlabGeometry(self._workspace_for_corrections) # Sample properties for MS correction - - sampleProperties = self.calcMSCorrectionSampleProperties(self._mean_widths, self._mean_intensity_ratios) - print( - "\nThe sample properties for Multiple Scattering correction are:\n\n", - sampleProperties, - "\n", - ) - - return self.createMulScatWorkspaces(self._workspace_for_corrections, sampleProperties) - - - def createSlabGeometry(self, wsNCPM): - half_height, half_width, half_thick = ( - 0.5 * self._vertical_width, - 0.5 * self._horizontal_width, - 0.5 * self._thickness, - ) - xml_str = ( - ' ' - + ' ' - % (half_width, -half_height, half_thick) - + ' ' - % (half_width, half_height, half_thick) - + ' ' - % (half_width, -half_height, -half_thick) - + ' ' - % (-half_width, -half_height, half_thick) - + "" - ) - - CreateSampleShape(self._workspace_for_corrections, xml_str) - - - def calcMSCorrectionSampleProperties(self, meanWidths, meanIntensityRatios): - masses = [p.mass for p in self._profiles.values()] - - # If Backsscattering mode and H is present in the sample, add H to MS properties - if self._mode_running == "BACKWARD": - if self._h_ratio is not None: # If H is present, ratio is a number - masses = np.append(masses, 1.0079) - meanWidths = np.append(meanWidths, 5.0) - - HIntensity = self._h_ratio * meanIntensityRatios[np.argmin(masses)] - meanIntensityRatios = np.append(meanIntensityRatios, HIntensity) - meanIntensityRatios /= np.sum(meanIntensityRatios) - - MSProperties = np.zeros(3 * len(masses)) - MSProperties[::3] = masses - MSProperties[1::3] = meanIntensityRatios - MSProperties[2::3] = meanWidths - sampleProperties = list(MSProperties) - - return sampleProperties - - - def createMulScatWorkspaces(self, ws, sampleProperties): - """Uses the Mantid algorithm for the MS correction to create two Workspaces _TotScattering and _MulScattering""" - - print("\nEvaluating the Multiple Scattering Correction...\n") - # selects only the masses, every 3 numbers - MS_masses = sampleProperties[::3] - # same as above, but starts at first intensities - MS_amplitudes = sampleProperties[1::3] - - dens, trans = VesuvioThickness( - Masses=MS_masses, - Amplitudes=MS_amplitudes, - TransmissionGuess=self._transmission_guess, - Thickness=0.1, - ) - - _TotScattering, _MulScattering = VesuvioCalculateMS( - ws, - NoOfMasses=len(MS_masses), - SampleDensity=dens.cell(9, 1), - AtomicProperties=sampleProperties, - BeamRadius=2.5, - NumScatters=self._multiple_scattering_order, - NumEventsPerRun=int(self._number_of_events), - ) - - data_normalisation = Integration(ws) - simulation_normalisation = Integration("_TotScattering") - for workspace in ("_MulScattering", "_TotScattering"): - Divide( - LHSWorkspace=workspace, - RHSWorkspace=simulation_normalisation, - OutputWorkspace=workspace, - ) - Multiply( - LHSWorkspace=workspace, - RHSWorkspace=data_normalisation, - OutputWorkspace=workspace, - ) - RenameWorkspace(InputWorkspace=workspace, OutputWorkspace=ws.name() + workspace) - SumSpectra( - ws.name() + workspace, OutputWorkspace=ws.name() + workspace + "_Sum" - ) - - DeleteWorkspaces([data_normalisation, simulation_normalisation, trans, dens]) - # The only remaining workspaces are the _MulScattering and _TotScattering - return mtd[ws.name() + "_MulScattering"] - - - def create_gamma_workspaces(self): - """Creates _gamma_background correction workspace to be subtracted from the main workspace""" - - inputWS = self._workspace_for_corrections.name() - - profiles = self.calcGammaCorrectionProfiles(self._mean_widths, self._mean_intensity_ratios) - - # Approach below not currently suitable for current versions of Mantid, but will be in the future - # background, corrected = VesuvioCalculateGammaBackground(InputWorkspace=inputWS, ComptonFunction=profiles) - # DeleteWorkspace(corrected) - # RenameWorkspace(InputWorkspace= background, OutputWorkspace = inputWS+"_Gamma_Background") - - ws = CloneWorkspace(InputWorkspace=inputWS, OutputWorkspace="tmpGC") - for spec in range(ws.getNumberHistograms()): - background, corrected = VesuvioCalculateGammaBackground( - InputWorkspace=inputWS, ComptonFunction=profiles, WorkspaceIndexList=spec - ) - ws.dataY(spec)[:], ws.dataE(spec)[:] = ( - background.dataY(0)[:], - background.dataE(0)[:], - ) - DeleteWorkspace(background) - DeleteWorkspace(corrected) - RenameWorkspace( - InputWorkspace="tmpGC", OutputWorkspace=inputWS + "_Gamma_Background" - ) - - Scale( - InputWorkspace=inputWS + "_Gamma_Background", - OutputWorkspace=inputWS + "_Gamma_Background", - Factor=0.9, - Operation="Multiply", - ) - return mtd[inputWS + "_Gamma_Background"] - - - def calcGammaCorrectionProfiles(self, meanWidths, meanIntensityRatios): - masses = [p.mass for p in self._profiles.values()] - profiles = "" - for mass, width, intensity in zip(masses, meanWidths, meanIntensityRatios): - profiles += ( - "name=GaussianComptonProfile,Mass=" - + str(mass) - + ",Width=" - + str(width) - + ",Intensity=" - + str(intensity) - + ";" - ) - print("\n The sample properties for Gamma Correction are:\n", profiles) - return profiles - - - def _set_results(self): - """Used to collect results from workspaces and store them in .npz files for testing.""" - - self.wsFinal = mtd[self._name + str(self._number_of_iterations)] - - allIterNcp = [] - allFitWs = [] - allTotNcp = [] - allBestPar = [] - allMeanWidhts = [] - allMeanIntensities = [] - allStdWidths = [] - allStdIntensities = [] - j = 0 - while True: - try: - wsIterName = self._name + str(j) - - # Extract ws that were fitted - ws = mtd[wsIterName] - allFitWs.append(ws.extractY()) - - # Extract total ncp - totNcpWs = mtd[wsIterName + "_total_ncp"] - allTotNcp.append(totNcpWs.extractY()) - - # Extract best fit parameters - fitParTable = mtd[wsIterName + "_fit_results"] - bestFitPars = [] - for key in fitParTable.keys(): - bestFitPars.append(fitParTable.column(key)) - allBestPar.append(np.array(bestFitPars).T) - - # Extract individual ncp - allNCP = [] - for p in self._profiles.values(): - ncpWsToAppend = mtd[ - wsIterName + f"_{p.label}_ncp" - ] - allNCP.append(ncpWsToAppend.extractY()) - allNCP = np.swapaxes(np.array(allNCP), 0, 1) - allIterNcp.append(allNCP) - - # Extract Mean and Std Widths, Intensities - meansTable = mtd[wsIterName + "_MeanWidthsAndIntensities"] - allMeanWidhts.append(meansTable.column("Mean Widths")) - allStdWidths.append(meansTable.column("Std Widths")) - allMeanIntensities.append(meansTable.column("Mean Intensities")) - allStdIntensities.append(meansTable.column("Std Intensities")) - - j += 1 - except KeyError: - break - - self.all_fit_workspaces = np.array(allFitWs) - self.all_spec_best_par_chi_nit = np.array(allBestPar) - self.all_tot_ncp = np.array(allTotNcp) - self.all_ncp_for_each_mass = np.array(allIterNcp) - - self.all_mean_widths = np.array(allMeanWidhts) - self.all_mean_intensities = np.array(allMeanIntensities) - self.all_std_widths = np.array(allStdWidths) - self.all_std_intensities = np.array(allStdIntensities) - - def _save_results(self): - """Saves all of the arrays stored in this object""" - - maskedDetectorIdx = np.array(self._mask_spectra) - min(self._workspace_being_fit.getSpectrumNumbers()) - - # TODO: Take out nans next time when running original results - # Because original results were recently saved with nans, mask spectra with nans - self.all_spec_best_par_chi_nit[:, maskedDetectorIdx, :] = np.nan - self.all_ncp_for_each_mass[:, maskedDetectorIdx, :, :] = np.nan - self.all_tot_ncp[:, maskedDetectorIdx, :] = np.nan - - if not self._save_results_path: - return - - np.savez( - self._save_results_path, - all_fit_workspaces=self.all_fit_workspaces, - all_spec_best_par_chi_nit=self.all_spec_best_par_chi_nit, - all_mean_widths=self.all_mean_widths, - all_mean_intensities=self.all_mean_intensities, - all_std_widths=self.all_std_widths, - all_std_intensities=self.all_std_intensities, - all_tot_ncp=self.all_tot_ncp, - all_ncp_for_each_mass=self.all_ncp_for_each_mass, - ) - diff --git a/src/mvesuvio/oop/NeutronComptonProfile.py b/src/mvesuvio/oop/NeutronComptonProfile.py deleted file mode 100644 index 77c17333..00000000 --- a/src/mvesuvio/oop/NeutronComptonProfile.py +++ /dev/null @@ -1,18 +0,0 @@ -from dataclasses import dataclass - -@dataclass(frozen=False) -class NeutronComptonProfile: - label: str - mass: float - - intensity: float - width: float - center: float - - intensity_bounds: tuple[float, float] - width_bounds: tuple[float, float] - center_bounds: tuple[float, float] - - mean_intensity: float = None - mean_width: float = None - mean_center: float = None diff --git a/src/mvesuvio/oop/__init__.py b/src/mvesuvio/oop/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/src/mvesuvio/oop/analysis_helpers.py b/src/mvesuvio/util/analysis_helpers.py similarity index 100% rename from src/mvesuvio/oop/analysis_helpers.py rename to src/mvesuvio/util/analysis_helpers.py From 959110a95e440d6df5246833819e4f1855fd2755 Mon Sep 17 00:00:00 2001 From: GuiMacielPereira Date: Fri, 9 Aug 2024 14:43:09 +0100 Subject: [PATCH 20/25] Update import in unit test --- tests/unit/analysis/test_analysis_functions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/analysis/test_analysis_functions.py b/tests/unit/analysis/test_analysis_functions.py index 037253e2..15ef2324 100644 --- a/tests/unit/analysis/test_analysis_functions.py +++ b/tests/unit/analysis/test_analysis_functions.py @@ -2,7 +2,7 @@ import numpy as np import numpy.testing as nptest from mock import MagicMock -from mvesuvio.analysis_reduction import extractWS +from mvesuvio.util.analysis_helpers import extractWS from mantid.simpleapi import CreateWorkspace, DeleteWorkspace From 457198e595e7abb025e5019443e97a7a64ec320f Mon Sep 17 00:00:00 2001 From: GuiMacielPereira Date: Mon, 12 Aug 2024 08:26:38 +0100 Subject: [PATCH 21/25] Separate imports by groups Separated import statements by three groups of standard library, external imports and mvesuvio package imports --- src/mvesuvio/analysis_routines.py | 3 ++- src/mvesuvio/run_routine.py | 1 + src/mvesuvio/util/analysis_helpers.py | 3 ++- src/mvesuvio/util/process_inputs.py | 4 ++-- 4 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/mvesuvio/analysis_routines.py b/src/mvesuvio/analysis_routines.py index 7536b558..0e61e19b 100644 --- a/src/mvesuvio/analysis_routines.py +++ b/src/mvesuvio/analysis_routines.py @@ -1,10 +1,11 @@ # from .analysis_reduction import iterativeFitForDataReduction from mantid.api import AnalysisDataService from mantid.simpleapi import CreateEmptyTableWorkspace +import numpy as np + from mvesuvio.util.analysis_helpers import loadRawAndEmptyWsFromUserPath, cropAndMaskWorkspace from mvesuvio.analysis_reduction import AnalysisRoutine from mvesuvio.analysis_reduction import NeutronComptonProfile -import numpy as np def _create_analysis_object_from_current_interface(IC): diff --git a/src/mvesuvio/run_routine.py b/src/mvesuvio/run_routine.py index 591ba89b..57a22fe1 100644 --- a/src/mvesuvio/run_routine.py +++ b/src/mvesuvio/run_routine.py @@ -11,6 +11,7 @@ createTableWSHRatios, isHPresent, ) + from mantid.api import mtd def runRoutine( diff --git a/src/mvesuvio/util/analysis_helpers.py b/src/mvesuvio/util/analysis_helpers.py index 5246daed..ca6575d8 100644 --- a/src/mvesuvio/util/analysis_helpers.py +++ b/src/mvesuvio/util/analysis_helpers.py @@ -1,9 +1,10 @@ from mantid.simpleapi import Load, Rebin, Scale, SumSpectra, Minus, CropWorkspace, \ CloneWorkspace, MaskDetectors, CreateWorkspace -from mvesuvio.analysis_fitting import passDataIntoWS, replaceZerosWithNCP import numpy as np +from mvesuvio.analysis_fitting import passDataIntoWS + def loadRawAndEmptyWsFromUserPath(userWsRawPath, userWsEmptyPath, tofBinning, name, scaleRaw, scaleEmpty, subEmptyFromRaw): diff --git a/src/mvesuvio/util/process_inputs.py b/src/mvesuvio/util/process_inputs.py index 7c0cb9a8..02731201 100644 --- a/src/mvesuvio/util/process_inputs.py +++ b/src/mvesuvio/util/process_inputs.py @@ -1,9 +1,9 @@ from mantid.simpleapi import Load, LoadVesuvio, SaveNexus, DeleteWorkspace -from pathlib import Path from mvesuvio.util import handle_config from mantid.kernel import logger -import ntpath +from pathlib import Path +import ntpath def _get_expr_path(): inputsPath = Path(handle_config.read_config_var("caching.inputs")) From e0a2488a8776bd6ccb572413bae9d546829ca91f Mon Sep 17 00:00:00 2001 From: GuiMacielPereira Date: Mon, 12 Aug 2024 08:36:06 +0100 Subject: [PATCH 22/25] Fix typo and assertion --- src/mvesuvio/util/analysis_helpers.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/mvesuvio/util/analysis_helpers.py b/src/mvesuvio/util/analysis_helpers.py index ca6575d8..959b5cf4 100644 --- a/src/mvesuvio/util/analysis_helpers.py +++ b/src/mvesuvio/util/analysis_helpers.py @@ -2,6 +2,7 @@ from mantid.simpleapi import Load, Rebin, Scale, SumSpectra, Minus, CropWorkspace, \ CloneWorkspace, MaskDetectors, CreateWorkspace import numpy as np +import numbers from mvesuvio.analysis_fitting import passDataIntoWS @@ -16,9 +17,7 @@ def loadRawAndEmptyWsFromUserPath(userWsRawPath, userWsEmptyPath, OutputWorkspace=name + "raw", ) - assert (isinstance(scaleRaw, float)) | ( - isinstance(scaleRaw, int) - ), "Scaling factor of raw ws needs to be float or int." + assert (isinstance(numbers.Real)), "Scaling factor of raw ws needs to be float or int." Scale( InputWorkspace=name + "raw", OutputWorkspace=name + "raw", @@ -27,7 +26,7 @@ def loadRawAndEmptyWsFromUserPath(userWsRawPath, userWsEmptyPath, SumSpectra(InputWorkspace=name + "raw", OutputWorkspace=name + "raw" + "_sum") wsToBeFitted = CloneWorkspace( - InputWorkspace=name + "raw", OutputWorkspace=name + "uncroped_unmasked" + InputWorkspace=name + "raw", OutputWorkspace=name + "uncropped_unmasked" ) # if mode=="DoubleDifference": @@ -55,7 +54,7 @@ def loadRawAndEmptyWsFromUserPath(userWsRawPath, userWsEmptyPath, wsToBeFitted = Minus( LHSWorkspace=name + "raw", RHSWorkspace=name + "empty", - OutputWorkspace=name + "uncroped_unmasked", + OutputWorkspace=name + "uncropped_unmasked", ) return wsToBeFitted @@ -71,7 +70,7 @@ def cropAndMaskWorkspace(ws, firstSpec, lastSpec, maskedDetectors, maskTOFRange) initialIdx = firstSpec - wsFirstSpec lastIdx = lastSpec - wsFirstSpec - newWsName = ws.name().split("uncroped")[0] # Retrieve original name + newWsName = ws.name().split("uncropped")[0] # Retrieve original name wsCrop = CropWorkspace( InputWorkspace=ws, StartWorkspaceIndex=initialIdx, @@ -140,7 +139,8 @@ def loadConstants(): en_to_vel = 4.3737 * 1.0e-4 vf = np.sqrt(Ef) * en_to_vel # m/us hbar = 2.0445 - return mN, Ef, en_to_vel, vf, hbar + constants = (mN, Ef, en_to_vel, vf, hbar) + return constants def gaussian(x, sigma): From 3caecaee8f542589484cd37c8b232637b0258652 Mon Sep 17 00:00:00 2001 From: GuiMacielPereira Date: Mon, 12 Aug 2024 09:24:54 +0100 Subject: [PATCH 23/25] Fix assertion and rename variables Implemented review suggestions --- src/mvesuvio/analysis_reduction.py | 4 +-- src/mvesuvio/util/analysis_helpers.py | 40 +++++++++++---------------- 2 files changed, 18 insertions(+), 26 deletions(-) diff --git a/src/mvesuvio/analysis_reduction.py b/src/mvesuvio/analysis_reduction.py index 4973173d..2789ed57 100644 --- a/src/mvesuvio/analysis_reduction.py +++ b/src/mvesuvio/analysis_reduction.py @@ -8,7 +8,7 @@ CreateWorkspace from mvesuvio.util.analysis_helpers import histToPointData, loadConstants, \ - gaussian, lorentizian, numericalThirdDerivative + gaussian, lorentzian, numericalThirdDerivative from dataclasses import dataclass @@ -777,7 +777,7 @@ def pseudoVoigt(self, x, sigma, gamma): f = 0.5346 * fl + np.sqrt(0.2166 * fl**2 + fg**2) eta = 1.36603 * fl / f - 0.47719 * (fl / f) ** 2 + 0.11116 * (fl / f) ** 3 sigma_v, gamma_v = f / (2.0 * np.sqrt(2.0 * np.log(2.0))), f / 2.0 - pseudo_voigt = eta * lorentizian(x, gamma_v) + (1.0 - eta) * gaussian(x, sigma_v) + pseudo_voigt = eta * lorentzian(x, gamma_v) + (1.0 - eta) * gaussian(x, sigma_v) norm = ( np.abs(np.trapz(pseudo_voigt, x, axis=1))[:, np.newaxis] if self._run_norm_voigt else 1 diff --git a/src/mvesuvio/util/analysis_helpers.py b/src/mvesuvio/util/analysis_helpers.py index 959b5cf4..35648577 100644 --- a/src/mvesuvio/util/analysis_helpers.py +++ b/src/mvesuvio/util/analysis_helpers.py @@ -17,7 +17,7 @@ def loadRawAndEmptyWsFromUserPath(userWsRawPath, userWsEmptyPath, OutputWorkspace=name + "raw", ) - assert (isinstance(numbers.Real)), "Scaling factor of raw ws needs to be float or int." + assert (isinstance(scaleRaw, numbers.Real)), "Scaling factor of raw ws needs to be float or int." Scale( InputWorkspace=name + "raw", OutputWorkspace=name + "raw", @@ -144,43 +144,35 @@ def loadConstants(): def gaussian(x, sigma): - """Gaussian function centered at zero""" - gaussian = np.exp(-(x**2) / 2 / sigma**2) - gaussian /= np.sqrt(2.0 * np.pi) * sigma - return gaussian + """Gaussian centered at zero""" + gauss = np.exp(-(x**2) / 2 / sigma**2) + gauss /= np.sqrt(2.0 * np.pi) * sigma + return gauss -def lorentizian(x, gamma): +def lorentzian(x, gamma): """Lorentzian centered at zero""" - lorentzian = gamma / np.pi / (x**2 + gamma**2) - return lorentzian + return gamma / np.pi / (x**2 + gamma**2) -def numericalThirdDerivative(x, fun): - k6 = (-fun[:, 12:] + fun[:, :-12]) * 1 - k5 = (+fun[:, 11:-1] - fun[:, 1:-11]) * 24 - k4 = (-fun[:, 10:-2] + fun[:, 2:-10]) * 192 - k3 = (+fun[:, 9:-3] - fun[:, 3:-9]) * 488 - k2 = (+fun[:, 8:-4] - fun[:, 4:-8]) * 387 - k1 = (-fun[:, 7:-5] + fun[:, 5:-7]) * 1584 +def numericalThirdDerivative(x, y): + k6 = (- y[:, 12:] + y[:, :-12]) * 1 + k5 = (+ y[:, 11:-1] - y[:, 1:-11]) * 24 + k4 = (- y[:, 10:-2] + y[:, 2:-10]) * 192 + k3 = (+ y[:, 9:-3] - y[:, 3:-9]) * 488 + k2 = (+ y[:, 8:-4] - y[:, 4:-8]) * 387 + k1 = (- y[:, 7:-5] + y[:, 5:-7]) * 1584 dev = k1 + k2 + k3 + k4 + k5 + k6 dev /= np.power(x[:, 7:-5] - x[:, 6:-6], 3) dev /= 12**3 - derivative = np.zeros(fun.shape) - derivative[:, 6:-6] = dev + derivative = np.zeros_like(y) # Padded with zeros left and right to return array with same shape + derivative[:, 6:-6] = dev return derivative -def switchFirstTwoAxis(A): - """Exchanges the first two indices of an array A, - rearranges matrices per spectrum for iteration of main fitting procedure - """ - return np.stack(np.split(A, len(A), axis=0), axis=2)[0] - - def createWS(dataX, dataY, dataE, wsName, parentWorkspace=None): ws = CreateWorkspace( DataX=dataX.flatten(), From 68abc66e0b7fdf83cc1adecdaa1a0a33caff57be Mon Sep 17 00:00:00 2001 From: GuiMacielPereira Date: Thu, 22 Aug 2024 12:15:28 +0100 Subject: [PATCH 24/25] Uncomment unused functions Uncomment functions to use in the future --- src/mvesuvio/analysis_reduction.py | 63 ++++++++++++++---------------- 1 file changed, 30 insertions(+), 33 deletions(-) diff --git a/src/mvesuvio/analysis_reduction.py b/src/mvesuvio/analysis_reduction.py index 2789ed57..7cd73126 100644 --- a/src/mvesuvio/analysis_reduction.py +++ b/src/mvesuvio/analysis_reduction.py @@ -785,39 +785,36 @@ def pseudoVoigt(self, x, sigma, gamma): return pseudo_voigt / norm - # When interface is updated, uncomment to change the way - # constraints are handled: - - # def _get_parsed_constraints(self): - # - # parsed_constraints = [] - # - # for constraint in self._constraints: - # constraint['fun'] = self._get_parsed_constraint_function(constraint['fun']) - # - # parsed_constraints.append(constraint) - # - # return parsed_constraints - # - # - # def _get_parsed_constraint_function(self, function_string: str): - # - # profile_order = [key for key in self._profiles.keys()] - # attribute_order = ['intensity', 'width', 'center'] - # - # words = function_string.split(' ') - # for i, word in enumerate(words): - # if '.' in word: - # - # try: # Skip floats - # float(word) - # except ValueError: - # continue - # - # profile, attribute = word - # words[i] = f"pars[{profile_order.index(profile) + attribute_order.index(attribute)}]" - # - # return eval(f"lambda pars: {' '.join(words)}") + def _get_parsed_constraints(self): + + parsed_constraints = [] + + for constraint in self._constraints: + constraint['fun'] = self._get_parsed_constraint_function(constraint['fun']) + + parsed_constraints.append(constraint) + + return parsed_constraints + + + def _get_parsed_constraint_function(self, function_string: str): + + profile_order = [key for key in self._profiles.keys()] + attribute_order = ['intensity', 'width', 'center'] + + words = function_string.split(' ') + for i, word in enumerate(words): + if '.' in word: + + try: # Skip floats + float(word) + except ValueError: + continue + + profile, attribute = word + words[i] = f"pars[{profile_order.index(profile) + attribute_order.index(attribute)}]" + + return eval(f"lambda pars: {' '.join(words)}") def _replace_zero_columns_with_ncp_fit(self): From f10fbf3d4378d1489a3da685f8a156a3b7e24ef5 Mon Sep 17 00:00:00 2001 From: GuiMacielPereira Date: Thu, 22 Aug 2024 16:24:46 +0100 Subject: [PATCH 25/25] Fix typos from code review --- src/mvesuvio/analysis_reduction.py | 18 +++++++++--------- src/mvesuvio/util/analysis_helpers.py | 4 ++-- src/mvesuvio/util/process_inputs.py | 2 +- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/mvesuvio/analysis_reduction.py b/src/mvesuvio/analysis_reduction.py index 7cd73126..3dcf50fe 100644 --- a/src/mvesuvio/analysis_reduction.py +++ b/src/mvesuvio/analysis_reduction.py @@ -185,7 +185,7 @@ def _initialize_table_fit_parameters(self): table.addColumn(type="float", name=f"{key} Width") table.addColumn(type="float", name=f"{key} Center ") table.addColumn(type="float", name="Normalised Chi2") - table.addColumn(type="float", name="Number of Iteraions") + table.addColumn(type="float", name="Number of Iterations") return table @@ -235,7 +235,7 @@ def _set_up_kinematic_arrays(self): def run(self): - assert len(self.profiles) > 0, "Add profiles before atempting to run the routine!" + assert len(self.profiles) > 0, "Add profiles before attempting to run the routine!" self._create_table_initial_parameters() @@ -318,7 +318,7 @@ def _fit_neutron_compton_profiles(self): Performs the fit of neutron compton profiles to the workspace being fit. The profiles are fit on a spectrum by spectrum basis. """ - print("\nFitting Neutron Compron Prolfiles:\n") + print("\nFitting Neutron Compton Prolfiles:\n") self._row_being_fit = 0 while self._row_being_fit != len(self._dataY): @@ -402,7 +402,7 @@ def calculateKinematicsArrays(self, instrPars): def convertDataXToYSpacesForEachMass(self, dataX, delta_Q, delta_E): - "Calculates y spaces from TOF data, each row corresponds to one mass" + """"Calculates y spaces from TOF data, each row corresponds to one mass""" # Prepare arrays to broadcast dataX = dataX[np.newaxis, :, :] @@ -570,10 +570,10 @@ def filterWidthsAndIntensities(self, widthsIn, intensitiesIn): assert not (np.all(np.isnan(betterIntensities))), "All filtered intensities are nan" assert np.nanmax(betterWidths) != np.nanmin( betterWidths - ), f"All fitered widths have the same value: {np.nanmin(betterWidths)}" + ), f"All filtered widths have the same value: {np.nanmin(betterWidths)}" assert np.nanmax(betterIntensities) != np.nanmin( betterIntensities - ), f"All fitered widths have the same value: {np.nanmin(betterIntensities)}" + ), f"All filtered intensities have the same value: {np.nanmin(betterIntensities)}" return betterWidths, betterIntensities @@ -644,7 +644,7 @@ def errorFunction(self, pars): def _neutron_compton_profiles(self, pars): """ - Neutron Compron Profile distribution on TOF space for a single spectrum. + Neutron Compton Profile distribution on TOF space for a single spectrum. Calculated from kinematics, J(y) and resolution functions. """ @@ -836,7 +836,7 @@ def _replace_zero_columns_with_ncp_fit(self): OutputWorkspace=self._workspace_for_corrections.name() + "_CorrectionsInput" ) for row in range(self._workspace_for_corrections.getNumberHistograms()): - # TODO: Once the option to chage point to hist is removed, remove [:len(ncp)] + # TODO: Once the option to change point to hist is removed, remove [:len(ncp)] self._workspace_for_corrections.dataY(row)[self._zero_columns_boolean_mask] = ncp[row, self._zero_columns_boolean_mask[:len(ncp[row])]] SumSpectra( @@ -919,7 +919,7 @@ def createSlabGeometry(self, wsNCPM): def calcMSCorrectionSampleProperties(self, meanWidths, meanIntensityRatios): masses = [p.mass for p in self._profiles.values()] - # If Backsscattering mode and H is present in the sample, add H to MS properties + # If Backscattering mode and H is present in the sample, add H to MS properties if self._mode_running == "BACKWARD": if self._h_ratio is not None: # If H is present, ratio is a number masses = np.append(masses, 1.0079) diff --git a/src/mvesuvio/util/analysis_helpers.py b/src/mvesuvio/util/analysis_helpers.py index 35648577..8b7ef763 100644 --- a/src/mvesuvio/util/analysis_helpers.py +++ b/src/mvesuvio/util/analysis_helpers.py @@ -108,7 +108,7 @@ def maskBinsWithZeros(ws, maskTOFRange): def extractWS(ws): - """Directly exctracts data from workspace into arrays""" + """Directly extracts data from workspace into arrays""" return ws.extractX(), ws.extractY(), ws.extractE() @@ -123,7 +123,7 @@ def histToPointData(dataY, dataX, dataE): histWidths = dataX[:, 1:] - dataX[:, :-1] assert np.min(histWidths) == np.max( histWidths - ), "Histogram widhts need to be the same length" + ), "Histogram widths need to be the same length" dataYp = dataY[:, :-1] dataEp = dataE[:, :-1] diff --git a/src/mvesuvio/util/process_inputs.py b/src/mvesuvio/util/process_inputs.py index 02731201..46449a00 100644 --- a/src/mvesuvio/util/process_inputs.py +++ b/src/mvesuvio/util/process_inputs.py @@ -82,7 +82,7 @@ def completeICFromInputs(IC, wsIC): IC.normVoigt = True #Create default for H ratio - # Only for completeness sake, will be removed anyway + # Only for completeness' sake, will be removed anyway # when transition to new interface is complete try: IC.HToMassIdxRatio