diff --git a/Mantid.user.properties b/Mantid.user.properties deleted file mode 100644 index 7d70a1df..00000000 --- a/Mantid.user.properties +++ /dev/null @@ -1 +0,0 @@ -UpdateInstrumentDefinitions.OnStartup = 0 \ No newline at end of file diff --git a/src/mvesuvio/analysis_fitting.py b/src/mvesuvio/analysis_fitting.py index d866764c..efcc4d11 100644 --- a/src/mvesuvio/analysis_fitting.py +++ b/src/mvesuvio/analysis_fitting.py @@ -9,8 +9,11 @@ import jacobi import time import re +from mantid.kernel import logger +from mantid.simpleapi import AnalysisDataService from mvesuvio.util import handle_config +from mvesuvio.util.analysis_helpers import print_table_workspace, pass_data_into_ws repoPath = Path(__file__).absolute().parent # Path to the repository @@ -20,7 +23,7 @@ def fitInYSpaceProcedure(yFitIC, IC, wsTOF): try: wsTOF = mtd[wsTOF] except KeyError: - print(f"Workspace to fit {wsTOF} not found.") + logger.notice(f"Workspace to fit {wsTOF} not found.") return wsResSum, wsRes = calculateMantidResolutionFirstMass(IC, yFitIC, wsTOF) @@ -39,7 +42,7 @@ def fitInYSpaceProcedure(yFitIC, IC, wsTOF): fitProfileMinuit(yFitIC, wsJoYAvg, wsResSum) fitProfileMantidFit(yFitIC, wsJoYAvg, wsResSum) - printYSpaceFitResults(wsJoYAvg.name()) + printYSpaceFitResults() yfitResults = ResultsYFitObject(IC, yFitIC, wsTOF.name(), wsJoYAvg.name()) yfitResults.save() @@ -104,7 +107,10 @@ def subtract_profiles_except_lightest(ic, ws): if len(ic.masses) == 1: return - ws_name_lightest_mass = ic.name + '_' + str(ic.number_of_iterations_for_corrections) + '_' + str(min(ic.masses)) + '_ncp' + # TODO: Make the fetching of these workspaces more robust, prone to error + profiles_table = mtd[ic.name + '_initial_parameters'] + lightest_mass_str = profiles_table.column('label')[np.argmin(profiles_table.column('mass'))] + ws_name_lightest_mass = ic.name + '_' + str(ic.number_of_iterations_for_corrections) + '_' + lightest_mass_str + '_ncp' ws_name_profiles = ic.name + '_' + str(ic.number_of_iterations_for_corrections) + '_total_ncp' wsNcpExceptFirst = Minus(mtd[ws_name_profiles], mtd[ws_name_lightest_mass], @@ -127,7 +133,9 @@ def switchFirstTwoAxis(A): def ySpaceReduction(wsTOF, mass0, yFitIC, ic): """Seperate procedures depending on masking specified.""" - ws_name_lightest_mass = ic.name + '_' + str(ic.number_of_iterations_for_corrections) + '_' + str(min(ic.masses)) + '_ncp' + profiles_table = mtd[ic.name + '_initial_parameters'] + lightest_mass_str = profiles_table.column('label')[np.argmin(profiles_table.column('mass'))] + ws_name_lightest_mass = ic.name + '_' + str(ic.number_of_iterations_for_corrections) + '_' + lightest_mass_str + '_ncp' ncp = mtd[ws_name_lightest_mass].extractY() rebinPars = yFitIC.range_for_rebinning_in_y_space @@ -199,7 +207,7 @@ def replaceZerosWithNCP(ws, ncp): ] # mask of ncp adjusted for last col present or not wsMasked = CloneWorkspace(ws, OutputWorkspace=ws.name() + "_NCPMasked") - passDataIntoWS(dataX, dataY, dataE, wsMasked) + pass_data_into_ws(dataX, dataY, dataE, wsMasked) SumSpectra(wsMasked, OutputWorkspace=wsMasked.name() + "_Sum") return wsMasked @@ -249,7 +257,7 @@ def dataXBining(ws, xp): dataE[dataY == 0] = 0 wsXBins = CloneWorkspace(ws, OutputWorkspace=ws.name() + "_XBinned") - wsXBins = passDataIntoWS(dataX, dataY, dataE, wsXBins) + wsXBins = pass_data_into_ws(dataX, dataY, dataE, wsXBins) return wsXBins @@ -414,15 +422,6 @@ def extractWS(ws): return ws.extractX(), ws.extractY(), ws.extractE() -def passDataIntoWS(dataX, dataY, dataE, ws): - "Modifies ws data to input data" - for i in range(ws.getNumberHistograms()): - ws.dataX(i)[:] = dataX[i, :] - ws.dataY(i)[:] = dataY[i, :] - ws.dataE(i)[:] = dataE[i, :] - return ws - - def symmetrizeWs(avgYSpace): """ Symmetrizes workspace after weighted average. @@ -438,7 +437,7 @@ def symmetrizeWs(avgYSpace): dataYS, dataES = weightedSymArr(dataY, dataE) wsSym = CloneWorkspace(avgYSpace, OutputWorkspace=avgYSpace.name() + "_sym") - wsSym = passDataIntoWS(dataX, dataYS, dataES, wsSym) + wsSym = pass_data_into_ws(dataX, dataYS, dataES, wsSym) return wsSym @@ -800,9 +799,9 @@ def model(x, A, sig_x, sig_y, sig_z): "Fitting Model not recognized, available options: 'gauss', 'gcc4c6', 'gcc4', 'gcc6', 'ansiogauss' gauss3d'" ) - print("\nShared Parameters: ", [key for key in sharedPars]) - print( - "\nUnshared Parameters: ", [key for key in defaultPars if key not in sharedPars] + logger.notice(f"\nShared Parameters: {[key for key in sharedPars]}") + logger.notice( + f"\nUnshared Parameters: {[key for key in defaultPars if key not in sharedPars]}" ) assert all( @@ -903,6 +902,8 @@ def createCorrelationTableWorkspace(wsYSpaceSym, parameters, corrMatrix): tableWS.addColumn(type="float", name=p) for p, arr in zip(parameters, corrMatrix): tableWS.addRow([p] + list(arr)) + print_table_workspace(tableWS) + def runMinos(mObj, yFitIC, constrFunc, wsName): @@ -965,7 +966,7 @@ def runAndPlotManualMinos(minuitObj, constrFunc, bestFitVals, bestFitErrs, showP """ # Reason for two distinct operations inside the same function is that its easier # to build the minos plots for each parameter as they are being calculated. - print("\nRunning Minos ... \n") + logger.notice("\nRunning Minos ... \n") # Set format of subplots height = 2 @@ -1224,7 +1225,7 @@ def oddPointsRes(x, res): def fitProfileMantidFit(yFitIC, wsYSpaceSym, wsRes): - print("\nFitting on the sum of spectra in the West domain ...\n") + logger.notice("\nFitting on the sum of spectra in the West domain ...\n") for minimizer in ["Levenberg-Marquardt", "Simplex"]: if yFitIC.fitting_model== "gauss": function = f"""composite=Convolution,FixResolution=true,NumDeriv=true; @@ -1282,40 +1283,10 @@ def fitProfileMantidFit(yFitIC, wsYSpaceSym, wsRes): return -def printYSpaceFitResults(wsJoYName): - print("\nFit in Y Space results:") - foundWS = [] - try: - wsFitLM = mtd[wsJoYName + "_lm_Parameters"] - foundWS.append(wsFitLM) - except KeyError: - pass - try: - wsFitSimplex = mtd[wsJoYName + "_simplex_Parameters"] - foundWS.append(wsFitSimplex) - except KeyError: - pass - try: - wsFitMinuit = mtd[wsJoYName + "_minuit_Parameters"] - foundWS.append(wsFitMinuit) - except KeyError: - pass - - for tableWS in foundWS: - print("\n" + " ".join(tableWS.getName().split("_")[-3:]) + ":") - # print(" ".join(tableWS.keys())) - for key in tableWS.keys(): - if key == "Name": - print( - f"{key:>20s}: " - + " ".join([f"{elem:7.8s}" for elem in tableWS.column(key)]) - ) - else: - print( - f"{key:>20s}: " - + " ".join([f"{elem:7.4f}" for elem in tableWS.column(key)]) - ) - print("\n") +def printYSpaceFitResults(): + for ws_name in mtd.getObjectNames(): + if ws_name.endswith('Parameters'): + print_table_workspace(mtd[ws_name]) class ResultsYFitObject: @@ -1388,7 +1359,7 @@ def save(self): def runGlobalFit(wsYSpace, wsRes, IC, yFitIC): - print("\nRunning GLobal Fit ...\n") + logger.notice("\nRunning GLobal Fit ...\n") dataX, dataY, dataE, dataRes, instrPars = extractData(wsYSpace, wsRes, IC) dataX, dataY, dataE, dataRes, instrPars = takeOutMaskedSpectra( @@ -1418,7 +1389,7 @@ def runGlobalFit(wsYSpace, wsRes, IC, yFitIC): # Minuit Fit with global cost function and local+global parameters initPars = minuitInitialParameters(defaultPars, sharedPars, len(dataY)) - print("\nRunning Global Fit ...\n") + logger.notice("\nRunning Global Fit ...\n") m = Minuit(totCost, **initPars) for i in range(len(dataY)): # Set limits for unshared parameters @@ -1467,7 +1438,7 @@ def constr(*pars): m.scipy(constraints=optimize.NonlinearConstraint(constr, 0, np.inf)) t1 = time.time() - print(f"\nTime of fitting: {t1-t0:.2f} seconds") + logger.notice(f"\nTime of fitting: {t1-t0:.2f} seconds") # Explicitly calculate errors m.hesse() @@ -1528,7 +1499,7 @@ def groupDetectors(ipData, yFitIC): checkNGroupsValid(yFitIC, ipData) - print(f"\nNumber of gropus: {yFitIC.number_of_global_fit_groups}") + logger.notice(f"\nNumber of gropus: {yFitIC.number_of_global_fit_groups}") L1 = ipData[:, -1].copy() theta = ipData[:, 2].copy() @@ -1661,11 +1632,11 @@ def formIdxList(clusters): idxList.append(list(idxs)) # Print groupings information - print("\nGroups formed successfully:\n") + logger.notice("\nGroups formed successfully:\n") groupLen = np.array([len(group) for group in idxList]) unique, counts = np.unique(groupLen, return_counts=True) for length, no in zip(unique, counts): - print(f"{no} groups with {length} detectors.") + logger.notice(f"{no} groups with {length} detectors.") return idxList @@ -1856,19 +1827,18 @@ def create_table_for_global_fit_parameters(wsName, m, chi2): t.addColumn(type="float", name="Value") t.addColumn(type="float", name="Error") - print(f"Value of Chi2/ndof: {chi2:.2f}") - print(f"Migrad Minimum valid: {m.valid}") - print("\nResults of Global Fit:\n") + logger.notice(f"Value of Chi2/ndof: {chi2:.2f}") + logger.notice(f"Migrad Minimum valid: {m.valid}") for p, v, e in zip(m.parameters, m.values, m.errors): - print(f"{p:>7s} = {v:>8.4f} \u00B1 {e:<8.4f}") t.addRow([p, v, e]) t.addRow(["Cost function", chi2, 0]) + print_table_workspace(t) def plotGlobalFit(dataX, dataY, dataE, mObj, totCost, wsName, yFitIC): if len(dataY) > 10: - print("\nToo many axes to show in figure, skipping the plot ...\n") + logger.notice("\nToo many axes to show in figure, skipping the plot ...\n") return rows = 2 @@ -1910,5 +1880,5 @@ def plotGlobalFit(dataX, dataY, dataE, mObj, totCost, wsName, yFitIC): def save_workspaces(yFitIC): for ws_name in mtd.getObjectNames(): - if ws_name.endswith('Parameters') or ws_name.endswith('Workspace'): + if ws_name.endswith('Parameters') or ws_name.endswith('parameters') or ws_name.endswith('Workspace'): SaveAscii(ws_name, str(yFitIC.figSavePath.parent / "output_files" / ws_name)) diff --git a/src/mvesuvio/analysis_reduction.py b/src/mvesuvio/analysis_reduction.py index 1aa1ca59..04c52b03 100644 --- a/src/mvesuvio/analysis_reduction.py +++ b/src/mvesuvio/analysis_reduction.py @@ -13,7 +13,7 @@ CreateWorkspace from mvesuvio.util.analysis_helpers import numerical_third_derivative, load_resolution, load_instrument_params, \ - extend_range_of_array + extend_range_of_array, print_table_workspace np.set_printoptions(suppress=True, precision=4, linewidth=200) @@ -238,13 +238,13 @@ def _initialize_table_fit_parameters(self): OutputWorkspace=self._workspace_being_fit.name()+ "_fit_results" ) table.setTitle("SciPy Fit Parameters") - table.addColumn(type="float", name="Spectrum") + table.addColumn(type="float", name="Spec") for label in self._profiles_table.column("label"): - table.addColumn(type="float", name=f"{label} intensity") - table.addColumn(type="float", name=f"{label} width") - table.addColumn(type="float", name=f"{label} center ") - table.addColumn(type="float", name="normalised chi2") - table.addColumn(type="float", name="no of iterations") + table.addColumn(type="float", name=f"{label} i") + table.addColumn(type="float", name=f"{label} w") + table.addColumn(type="float", name=f"{label} c") + table.addColumn(type="float", name="NChi2") + table.addColumn(type="float", name="Iter") return table @@ -327,6 +327,7 @@ def _fit_neutron_compton_profiles(self): self._row_being_fit += 1 assert np.any(self._fit_parameters), "Fitting parameters cannot be zero for all spectra!" + print_table_workspace(self._table_fit_results) return @@ -420,8 +421,8 @@ def _set_means_and_std(self): intensities = np.zeros(widths.shape) for i, label in enumerate(self._profiles_table.column("label")): - widths[i] = self._table_fit_results.column(f"{label} width") - intensities[i] = self._table_fit_results.column(f"{label} intensity") + widths[i] = self._table_fit_results.column(f"{label} w") + intensities[i] = self._table_fit_results.column(f"{label} i") self._set_means_and_std_arrays(widths, intensities) self._create_means_table() @@ -464,7 +465,6 @@ def _create_means_table(self): table.addColumn(type="float", name="mean_intensity") table.addColumn(type="float", name="std_intensity") - self.log().notice("\nmass mean widths mean intensities\n") for label, mass, mean_width, std_width, mean_intensity, std_intensity in zip( self._profiles_table.column("label"), self._masses, @@ -475,10 +475,9 @@ def _create_means_table(self): ): # Explicit conversion to float required to match profiles table table.addRow([label, float(mass), float(mean_width), float(std_width), float(mean_intensity), float(std_intensity)]) - self.log().notice(f"{label:6s} {mean_width:10.5f} \u00B1 {std_width:7.5f}" + \ - f"{mean_intensity:10.5f} \u00B1 {std_intensity:7.5f}\n") self.setPropertyValue("OutputMeansTable", table.name()) + print_table_workspace(table, precision=5) return table @@ -486,6 +485,8 @@ 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*self._profiles_table.rowCount()+3)) + spectrum_number = self._instrument_params[self._row_being_fit, 0] + self.log().notice(f"Skip spectrum {int(spectrum_number):3d}") return result = scipy.optimize.minimize( @@ -508,7 +509,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 - self.log().notice(' '.join(str(tableRow).split(",")).replace('[', '').replace(']', '')) + self.log().notice(f"Fit spectrum {int(spectrum_number):3d}: \u2713") # Pass fit profiles into workspaces ncp_for_each_mass, fse_for_each_mass = self._neutron_compton_profiles(fitPars) diff --git a/src/mvesuvio/main/__init__.py b/src/mvesuvio/main/__init__.py index 7295faf9..aeeb2cb9 100644 --- a/src/mvesuvio/main/__init__.py +++ b/src/mvesuvio/main/__init__.py @@ -44,6 +44,9 @@ def __set_up_parser(): def __setup_config(args): + + __set_logging_properties() + config_dir = handle_config.VESUVIO_CONFIG_PATH handle_config.setup_config_dir(config_dir) ipfolder_dir = handle_config.VESUVIO_IPFOLDER_PATH @@ -80,10 +83,24 @@ def __setup_config(args): handle_config.check_dir_exists("IP folder", ipfolder_dir) +def __set_logging_properties(): + from mantid.kernel import ConfigService + ConfigService.setString("logging.loggers.root.channel.class", "SplitterChannel") + ConfigService.setString("logging.loggers.root.channel.channel1", "consoleChannel") + ConfigService.setString("logging.loggers.root.channel.channel2", "fileChannel") + ConfigService.setString("logging.channels.consoleChannel.class", "ConsoleChannel") + ConfigService.setString("logging.channels.fileChannel.class", "FileChannel") + ConfigService.setString("logging.channels.fileChannel.path", "mantid.log") + ConfigService.setString("logging.channels.fileChannel.formatter.class", "PatternFormatter") + ConfigService.setString("logging.channels.fileChannel.formatter.pattern", "%Y-%m-%d %H:%M:%S,%i [%I] %p %s - %t") + # Set properties on Mantid.user.properties not working due to Mantid bug + # Need to set properties on file in Mantid installation + mantid_properties_file = path.join(ConfigService.getPropertiesDir(), "Mantid.properties") + ConfigService.saveConfig(mantid_properties_file) + return + + def __run_analysis(): - environ["MANTIDPROPERTIES"] = path.join( - handle_config.VESUVIO_CONFIG_PATH, "Mantid.user.properties" - ) from mvesuvio.main.run_routine import Runner Runner().run() diff --git a/src/mvesuvio/main/run_routine.py b/src/mvesuvio/main/run_routine.py index 354a9c0c..f84dab37 100644 --- a/src/mvesuvio/main/run_routine.py +++ b/src/mvesuvio/main/run_routine.py @@ -4,7 +4,8 @@ loadRawAndEmptyWsFromUserPath, cropAndMaskWorkspace, \ calculate_h_ratio, name_for_starting_ws, \ scattering_type, ws_history_matches_inputs, save_ws_from_load_vesuvio, \ - is_hydrogen_present, create_profiles_table, create_table_for_hydrogen_to_mass_ratios + is_hydrogen_present, create_profiles_table, create_table_for_hydrogen_to_mass_ratios, \ + print_table_workspace from mvesuvio.analysis_reduction import VesuvioAnalysisRoutine from mantid.api import mtd @@ -17,6 +18,8 @@ import importlib import sys import dill # To convert constraints to string +import re +import os class Runner: @@ -65,6 +68,7 @@ def setup(self): figSavePath.mkdir(exist_ok=True) self.yFitIC.figSavePath = figSavePath + self.mantid_log_file = "mantid.log" def import_from_inputs(self): name = "analysis_inputs" @@ -78,21 +82,59 @@ def import_from_inputs(self): def run(self): if not self.bckwd_ai.run_this_scattering_type and not self.fwd_ai.run_this_scattering_type: return - # Default workflow for procedure + fit in y space + + # Erase previous log + # Not working on Windows due to shared file locks + if os.name == 'posix': + with open(self.mantid_log_file, 'w') as file: + file.write('') # If any ws for y fit already loaded wsInMtd = [ws in mtd for ws in self.ws_to_fit_y_space] # Bool list if (len(wsInMtd) > 0) and all(wsInMtd): self.runAnalysisFitting() + self.make_summarised_log_file() return self.analysis_result, self.fitting_result self.runAnalysisRoutine() self.runAnalysisFitting() # Return results used only in tests + self.make_summarised_log_file() return self.analysis_result, self.fitting_result + def make_summarised_log_file(self): + pattern = re.compile(r"^\d{4}-\d{2}-\d{2}") + + log_file_save_path = self.make_log_file_name() + + try: + with open(self.mantid_log_file, "r") as infile, open(log_file_save_path, "w") as outfile: + for line in infile: + if "VesuvioAnalysisRoutine" in line: + outfile.write(line) + + if "Notice Python" in line: # For Fitting notices + outfile.write(line) + + if not pattern.match(line): + outfile.write(line) + except OSError: + print("Mantid log file not available. Unable to produce a summarized log file for this routine.") + return + + + def make_log_file_name(self): + filename = '' + if self.bckwd_ai.run_this_scattering_type: + filename += 'bckwd_' + if self.fwd_ai.run_this_scattering_type: + filename += 'fwd_' + filename += self.yFitIC.fitting_model + return self.experiment_path / (filename+ ".log") + + def runAnalysisFitting(self): for wsName, i_cls in zip(self.ws_to_fit_y_space, self.classes_to_fit_y_space): self.fitting_result = fitInYSpaceProcedure(self.yFitIC, i_cls, wsName) @@ -157,6 +199,7 @@ def run_joint_algs(cls, back_alg, front_alg): # Update original profiles table RenameWorkspace(fixed_profiles_table, receiving_profiles_table.name()) + print_table_workspace(mtd[receiving_profiles_table.name()]) # Even if the name is the same, need to trigger update front_alg.setPropertyValue("InputProfiles", receiving_profiles_table.name()) @@ -235,6 +278,7 @@ def _create_analysis_algorithm(self, ai): maskTOFRange=ai.mask_time_of_flight_range ) profiles_table = create_profiles_table(cropedWs.name()+"_initial_parameters", ai) + print_table_workspace(profiles_table) ipFilesPath = Path(handle_config.read_config_var("caching.ipfolder")) kwargs = { "InputWorkspace": cropedWs.name(), @@ -310,18 +354,16 @@ def _set_output_paths(self, ai): # Build Filename based on ic corr = "" - if ai.do_gamma_correction & (ai.number_of_iterations_for_corrections > 0): - corr += "_GC" if ai.do_multiple_scattering_correction & (ai.number_of_iterations_for_corrections > 0): - corr += "_MS" + corr += "MS" + if ai.do_gamma_correction & (ai.number_of_iterations_for_corrections > 0): + corr += "GC" fileName = ( - f"spec_{ai.detectors.strip()}_iter_{ai.number_of_iterations_for_corrections}{corr}" + ".npz" + f"{ai.detectors.strip()}_{ai.number_of_iterations_for_corrections}{corr}" ) - fileNameYSpace = fileName + "_ySpaceFit" + ".npz" - - self.results_save_path = outputPath / fileName - ai.ySpaceFitSavePath = outputPath / fileNameYSpace + self.results_save_path = outputPath / (fileName + ".npz") + ai.ySpaceFitSavePath = outputPath / (fileName + "_yfit.npz") # Set directories for figures figSavePath = experimentPath / "figures" diff --git a/src/mvesuvio/util/analysis_helpers.py b/src/mvesuvio/util/analysis_helpers.py index b2453bce..461fe266 100644 --- a/src/mvesuvio/util/analysis_helpers.py +++ b/src/mvesuvio/util/analysis_helpers.py @@ -6,12 +6,39 @@ import numpy as np import numbers -from mvesuvio.analysis_fitting import passDataIntoWS from mvesuvio.util import handle_config import ntpath +def pass_data_into_ws(dataX, dataY, dataE, ws): + "Modifies ws data to input data" + for i in range(ws.getNumberHistograms()): + ws.dataX(i)[:] = dataX[i, :] + ws.dataY(i)[:] = dataY[i, :] + ws.dataE(i)[:] = dataE[i, :] + return ws + + +def print_table_workspace(table, precision=3): + table_dict = table.toDict() + # Convert floats into strings + for key, values in table_dict.items(): + new_column = [int(item) if (isinstance(item, float) and item.is_integer()) else item for item in values] + table_dict[key] = [f"{item:.{precision}f}" if isinstance(item, float) else str(item) for item in new_column] + + max_spacing = [max([len(item) for item in values] + [len(key)]) for key, values in table_dict.items()] + header = "|" + "|".join(f"{item}{' '*(spacing-len(item))}" for item, spacing in zip(table_dict.keys(), max_spacing)) + "|" + logger.notice(f"Table {table.name()}:") + logger.notice(' '+'-'*(len(header)-2)+' ') + logger.notice(header) + for i in range(table.rowCount()): + table_row = "|".join(f"{values[i]}{' '*(spacing-len(str(values[i])))}" for values, spacing in zip(table_dict.values(), max_spacing)) + logger.notice("|" + table_row + "|") + logger.notice(' '+'-'*(len(header)-2)+' ') + return + + def create_profiles_table(name, ai): table = CreateEmptyTableWorkspace(OutputWorkspace=name) table.addColumn(type="str", name="label") @@ -59,7 +86,7 @@ def is_hydrogen_present(masses) -> bool: def ws_history_matches_inputs(runs, mode, ipfile, ws_path): if not (ws_path.is_file()): - logger.notice("Cached workspace not found") + logger.notice(f"Cached workspace not found at {ws_path}") return False ws = Load(Filename=str(ws_path)) @@ -228,7 +255,7 @@ def mask_time_of_flight_bins_with_zeros(ws, maskTOFRange): dataY[mask] = 0 - passDataIntoWS(dataX, dataY, dataE, ws) + pass_data_into_ws(dataX, dataY, dataE, ws) return diff --git a/tests/data/analysis/inputs/yspace_gauss_test_fwd_initial_parameters.nxs b/tests/data/analysis/inputs/yspace_gauss_test_fwd_initial_parameters.nxs new file mode 100644 index 00000000..c59ddddc Binary files /dev/null and b/tests/data/analysis/inputs/yspace_gauss_test_fwd_initial_parameters.nxs differ diff --git a/tests/system/analysis/test_yspace_fit.py b/tests/system/analysis/test_yspace_fit.py index 7dd4f124..9a9c7c32 100644 --- a/tests/system/analysis/test_yspace_fit.py +++ b/tests/system/analysis/test_yspace_fit.py @@ -1,5 +1,5 @@ from mvesuvio.main.run_routine import Runner -from mantid.simpleapi import Load, Plus, mtd, CreateWorkspace, CloneWorkspace +from mantid.simpleapi import Load, LoadAscii from mantid.api import AnalysisDataService from pathlib import Path import numpy as np @@ -55,6 +55,10 @@ def _load_workspaces(cls): str(cls._input_data_path / "yspace_tests_fwd_1_1.0079_ncp.nxs"), OutputWorkspace="yspace_gauss_test_fwd_1_1.0079_ncp" ) + Load( + str(cls._input_data_path / "yspace_gauss_test_fwd_initial_parameters.nxs"), + OutputWorkspace="yspace_gauss_test_fwd_initial_parameters" + ) return @classmethod diff --git a/tests/system/analysis/test_yspace_fit_GC.py b/tests/system/analysis/test_yspace_fit_GC.py index 534ab767..03d75466 100644 --- a/tests/system/analysis/test_yspace_fit_GC.py +++ b/tests/system/analysis/test_yspace_fit_GC.py @@ -55,6 +55,10 @@ def _load_workspaces(cls): str(cls._input_data_path / "yspace_tests_fwd_1_1.0079_ncp.nxs"), OutputWorkspace="yspace_gc_test_fwd_1_1.0079_ncp" ) + Load( + str(cls._input_data_path / "yspace_gauss_test_fwd_initial_parameters.nxs"), + OutputWorkspace="yspace_gc_test_fwd_initial_parameters" + ) return @classmethod diff --git a/tests/unit/analysis/test_analysis_helpers.py b/tests/unit/analysis/test_analysis_helpers.py index 84c7280d..79652f8a 100644 --- a/tests/unit/analysis/test_analysis_helpers.py +++ b/tests/unit/analysis/test_analysis_helpers.py @@ -3,10 +3,10 @@ import scipy import dill import numpy.testing as nptest -from mock import MagicMock +from mock import MagicMock, patch, call from mvesuvio.util.analysis_helpers import extractWS, _convert_dict_to_table, \ fix_profile_parameters, calculate_h_ratio, extend_range_of_array, numerical_third_derivative, \ - mask_time_of_flight_bins_with_zeros + mask_time_of_flight_bins_with_zeros, pass_data_into_ws, print_table_workspace from mantid.simpleapi import CreateWorkspace, DeleteWorkspace @@ -178,5 +178,54 @@ def test_mask_time_of_flight_bins_with_zeros(self): np.testing.assert_allclose(actual_data_y, expected_data_y) + def test_pass_data_into_ws(self): + + dataX = np.arange(20).reshape(4, 5) + dataY = np.arange(20, 40).reshape(4, 5) + dataE = np.arange(40, 60).reshape(4, 5) + + dataX_mock = np.zeros_like(dataX) + dataY_mock = np.zeros_like(dataY) + dataE_mock = np.zeros_like(dataE) + + ws_mock = MagicMock( + dataY=lambda row: dataY_mock[row], + dataX=lambda row: dataX_mock[row], + dataE=lambda row: dataE_mock[row], + getNumberHistograms=MagicMock(return_value=4) + ) + + pass_data_into_ws(dataX, dataY, dataE, ws_mock) + + np.testing.assert_allclose(dataX_mock, dataX) + np.testing.assert_allclose(dataY_mock, dataY) + np.testing.assert_allclose(dataE_mock, dataE) + + + @patch('mantid.kernel.logger.notice') + def test_print_table_workspace(self, mock_notice): + mock_table = MagicMock() + mock_table.name.return_value = "my_table" + mock_table.rowCount.return_value = 3 + mock_table.toDict.return_value = { + "names": ["1.0", "12.0", "16.0"], + "mass": [1, 12.0, 16.00000], + "width": [5, 10.3456, 15.23], + "bounds": ["[3, 6]", "[8, 13]", "[9, 17]"] + } + + print_table_workspace(mock_table, precision=2) + + mock_notice.assert_has_calls( + [call('Table my_table:'), + call(' ------------------------ '), + call('|names|mass|width|bounds |'), + call('|1.0 |1 |5 |[3, 6] |'), + call('|12.0 |12 |10.35|[8, 13]|'), + call('|16.0 |16 |15.23|[9, 17]|'), + call(' ------------------------ ')] + ) + + if __name__ == "__main__": unittest.main() diff --git a/tests/unit/analysis/test_analysis_reduction.py b/tests/unit/analysis/test_analysis_reduction.py index 43129e8b..28b1101e 100644 --- a/tests/unit/analysis/test_analysis_reduction.py +++ b/tests/unit/analysis/test_analysis_reduction.py @@ -92,6 +92,7 @@ def test_fit_neutron_compton_profiles_number_of_calls(self): alg._dataY = np.array([[1, 1], [2, 2], [3, 3]]) alg._fit_parameters = np.ones(3) # To avoid assertion error alg._fit_neutron_compton_profiles_to_row = MagicMock(return_value=None) + alg._table_fit_results = MagicMock(return_value=None) alg._fit_neutron_compton_profiles() self.assertEqual(alg._fit_neutron_compton_profiles_to_row.call_count, 3) @@ -366,10 +367,10 @@ def test_set_means_and_std(self): def pick_column(arg): table = { - '1.0 width': [5.6, 5.1, 0, 2, 5.4], - '12.0 width': [2.1, 1, 0, 2.3, 1.9], - '1.0 intensity': [7.8, 7.6, 0, 5, 7.3], - '12.0 intensity': [3.1, 2, 0, 3.2, 3.1], + '1.0 w': [5.6, 5.1, 0, 2, 5.4], + '12.0 w': [2.1, 1, 0, 2.3, 1.9], + '1.0 i': [7.8, 7.6, 0, 5, 7.3], + '12.0 i': [3.1, 2, 0, 3.2, 3.1], } return table[arg] diff --git a/tests/unit/analysis/test_run_routine.py b/tests/unit/analysis/test_run_routine.py new file mode 100644 index 00000000..e7f13abf --- /dev/null +++ b/tests/unit/analysis/test_run_routine.py @@ -0,0 +1,70 @@ + +import unittest +import numpy as np +import numpy.testing as nptest +from mock import MagicMock, patch, call +from mvesuvio.main.run_routine import Runner +from mvesuvio.util import handle_config +from pathlib import Path +import mvesuvio +import tempfile +from textwrap import dedent + +class TestRunRoutine(unittest.TestCase): + @classmethod + def setUpClass(cls): + # TODO: Avoid doing this in the future, can probably replace it with mock + mvesuvio.set_config( + ip_folder=str(Path(handle_config.VESUVIO_PACKAGE_PATH).joinpath("config", "ip_files")), + inputs_file=str(Path(__file__).absolute().parent.parent.parent / "data" / "analysis" / "inputs" / "analysis_test.py") + ) + pass + + def test_make_summarised_log_file(self): + runner = Runner() + mock_log_file = tempfile.NamedTemporaryFile(delete=False) + mock_mantid_log_file = tempfile.NamedTemporaryFile(delete=False) + mock_mantid_log_file.write(dedent(""" + 2025-01-08 10:48:44,832 [0] Notice CreateWorkspace - CreateWorkspace started (child) + 2025-01-08 10:48:44,844 [0] Notice CreateWorkspace - CreateWorkspace successful, Duration 0.01 seconds + 2025-01-08 10:48:44,860 [0] Notice VesuvioAnalysisRoutine - + Fitting neutron compton profiles ... + 2025-01-08 10:48:45,319 [0] Notice VesuvioAnalysisRoutine - Fit spectrum 148: ✓ + 2025-01-08 10:48:45,517 [0] Warning Python - Values in x were outside bounds during a minimize step, clipping to bounds + 2025-01-08 10:48:45,623 [0] Notice VesuvioAnalysisRoutine - Fit spectrum 151: ✓ + 2025-01-08 10:48:48,568 [0] Notice CreateEmptyTableWorkspace - CreateEmptyTableWorkspace started (child) + 2025-01-08 10:48:48,570 [0] Notice CreateEmptyTableWorkspace - CreateEmptyTableWorkspace successful, Duration 0.00 seconds + 2025-01-08 10:48:48,573 [0] Notice Python - Table analysis_inputs_fwd_0_means: + 2025-01-08 10:48:48,574 [0] Notice Python - ---------------------------------------------------------------- + 2025-01-08 10:48:48,576 [0] Notice Python - |label |mass |mean_width|std_width|mean_intensity|std_intensity| + 2025-01-08 10:48:48,578 [0] Notice Python - |1.0079|1.00790|5.29627 |0.19464 |0.91410 |0.00862 | + 2025-01-08 10:48:48,584 [0] Notice Python - ---------------------------------------------------------------- + 2025-01-08 10:48:48,588 [0] Notice VesuvioAnalysisRoutine - VesuvioAnalysisRoutine successful, Duration 3.89 seconds + 2025-01-08 10:48:49,390 [0] Notice Python - + Shared Parameters: ['sigma'] + 2025-01-08 10:48:49,391 [0] Notice Python - + Unshared Parameters: ['A', 'x0'] + """).encode()) + mock_mantid_log_file.close() + runner.mantid_log_file = mock_mantid_log_file.name + runner.make_log_file_name = MagicMock(return_value=mock_log_file.name) + runner.make_summarised_log_file() + current_log_file_content = mock_log_file.read() + self.assertEqual(dedent(""" + 2025-01-08 10:48:44,860 [0] Notice VesuvioAnalysisRoutine - + Fitting neutron compton profiles ... + 2025-01-08 10:48:45,319 [0] Notice VesuvioAnalysisRoutine - Fit spectrum 148: ✓ + 2025-01-08 10:48:45,623 [0] Notice VesuvioAnalysisRoutine - Fit spectrum 151: ✓ + 2025-01-08 10:48:48,573 [0] Notice Python - Table analysis_inputs_fwd_0_means: + 2025-01-08 10:48:48,574 [0] Notice Python - ---------------------------------------------------------------- + 2025-01-08 10:48:48,576 [0] Notice Python - |label |mass |mean_width|std_width|mean_intensity|std_intensity| + 2025-01-08 10:48:48,578 [0] Notice Python - |1.0079|1.00790|5.29627 |0.19464 |0.91410 |0.00862 | + 2025-01-08 10:48:48,584 [0] Notice Python - ---------------------------------------------------------------- + 2025-01-08 10:48:48,588 [0] Notice VesuvioAnalysisRoutine - VesuvioAnalysisRoutine successful, Duration 3.89 seconds + 2025-01-08 10:48:49,390 [0] Notice Python - + Shared Parameters: ['sigma'] + 2025-01-08 10:48:49,391 [0] Notice Python - + Unshared Parameters: ['A', 'x0'] + """).encode(), current_log_file_content) + +