From 9ab2044dd8c46c9506779d1e5c729beff937d0b4 Mon Sep 17 00:00:00 2001 From: GuiMacielPereira Date: Thu, 1 Aug 2024 16:51:10 +0100 Subject: [PATCH] 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 c72a75d..349a79a 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 ace64b4..32ac2d4 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 c33be59..7ac7de5 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 0000000..8cd0c40 --- /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()