diff --git a/Code/Mantid/Framework/PythonInterface/plugins/algorithms/LoadNMoldyn3Ascii.py b/Code/Mantid/Framework/PythonInterface/plugins/algorithms/LoadNMoldyn3Ascii.py index 951eb39e9911ddecb55cded5ec548f190b79c40c..8124fb10de1fa0e48cbb1bc1f5daa5b3eb41b2ca 100644 --- a/Code/Mantid/Framework/PythonInterface/plugins/algorithms/LoadNMoldyn3Ascii.py +++ b/Code/Mantid/Framework/PythonInterface/plugins/algorithms/LoadNMoldyn3Ascii.py @@ -4,18 +4,13 @@ from mantid.simpleapi import * from mantid.kernel import * from mantid.api import * +import ast +import re +import math import os import numpy as np -import math - - -def _split_line(a): - elements = a.split() # split line on character - extracted = [] - for n in elements: - extracted.append(float(n)) - return extracted # values as list +#============================================================================== def _find_starts(data, c, l1): for l in range(l1, len(data)): @@ -25,6 +20,7 @@ def _find_starts(data, c, l1): break return line +#============================================================================== def _find_tab_starts(data, c, l1): for l in range(l1, len(data)): @@ -34,6 +30,7 @@ def _find_tab_starts(data, c, l1): break return line +#============================================================================== def _find_ends(data, c, l1): for l in range(l1, len(data)): @@ -43,15 +40,7 @@ def _find_ends(data, c, l1): break return line - -def _find_char(data, c, l1): - for l in range(l1, len(data)): - char = data[l] - if char.find(c): - line = l - break - return line - +#============================================================================== def _make_list(a, l1, l2): data = '' @@ -60,21 +49,52 @@ def _make_list(a, l1, l2): alist = data.split(',') return alist +#============================================================================== + +def _cdl_find_dimensions(data): + """ + Gets the number of Q, time and frequency values in given raw data. + + @param data Raw data to search + """ + + num_q_values = _find_tab_starts(data, 'NQVALUES', 0) + num_time_values = _find_tab_starts(data, 'NTIMES', 0) + num_freq_values = _find_tab_starts(data, 'NFREQUENCIES', 0) + + q_el = data[num_q_values].split() + num_q = int(q_el[2]) + t_el = data[num_time_values].split() + num_t = int(t_el[2]) + f_el = data[num_freq_values].split() + num_f = int(f_el[2]) + + logger.debug(data[2][1:-1]) + logger.debug(data[3][1:-1]) + logger.debug(data[6][1:-1]) + + return num_q, num_t, num_f + +#============================================================================== class LoadNMoldyn3Ascii(PythonAlgorithm): _file_name = None + _file_type = None _functions = None _out_ws = None +#------------------------------------------------------------------------------- def category(self): return 'PythonAlgorithms;Inelastic;Simulation' +#------------------------------------------------------------------------------- def summary(self): return 'Imports functions from CDL and ASCII files output by nMOLDYN 3.' +#------------------------------------------------------------------------------- def PyInit(self): self.declareProperty(FileProperty('Filename', '', @@ -89,6 +109,7 @@ class LoadNMoldyn3Ascii(PythonAlgorithm): direction=Direction.Output), doc='Output workspace name') +#------------------------------------------------------------------------------- def validateInputs(self): issues = dict() @@ -105,124 +126,84 @@ class LoadNMoldyn3Ascii(PythonAlgorithm): return issues +#------------------------------------------------------------------------------- def PyExec(self): # Do setup self._setup() - # Load data file - data, name, ext = self._load_file() + loaded_ws = None # Run nMOLDYN import - if ext == 'cdl': - self._cdl_import(data, name) - elif ext == 'dat': - self._ascii_import(data, name) + if self._file_type == 'cdl': + loaded_ws = self._cdl_import() + elif self._file_type == 'dat': + loaders = [self._ascii_3d_import, self._ascii_2d_import] + for loader in loaders: + try: + logger.information('Attempting to load with loader {0}'.format(loader)) + loaded_ws = loader() + break + except (ValueError, SyntaxError) as err: + logger.information('Loader {0} failed to load data: {1}'.format(loader, err)) else: - raise RuntimeError('Unrecognised file format: %s' % ext) + raise RuntimeError('Unrecognised file extension: %s' % self._file_type) + + if loaded_ws is None: + raise RuntimeError('Failed to load any data, check file format') # Set the output workspace - self.setProperty('OutputWorkspace', self._out_ws) + self.setProperty('OutputWorkspace', loaded_ws) +#------------------------------------------------------------------------------- def _setup(self): """ Gets algorithm properties. """ self._file_name = self.getPropertyValue('Filename') + self._file_type = os.path.splitext(self._file_name)[1][1:] + self._out_ws = self.getPropertyValue('OutputWorkspace') raw_functions = self.getProperty('Functions').value self._functions = [x.strip() for x in raw_functions] +#------------------------------------------------------------------------------- - def _load_file(self): + def _cdl_import(self): """ - Attempts to load the sample file. - - @returns A tuple with the ASCII data, sample file name and file extension + Import data from CDL file. """ + logger.notice('Loading CDL file') - # Get some data about the file - path = self._file_name - base = os.path.basename(path) - name = os.path.splitext(base)[0] - ext = os.path.splitext(path)[1] - - # Remove dot from extension - if len(ext) > 1: - ext = ext[1:] - - logger.debug('Base filename for %s is %s' % (self._file_name, name)) - logger.debug('File type of %s is %s' % (self._file_name, ext)) - - if not os.path.isfile(path): - path = FileFinder.getFullPath(path) - - logger.information('Got file path for %s: %s' % (self._file_name, path)) + # Get file base name + base_filename = os.path.basename(self._file_name) + base_name = os.path.splitext(base_filename)[0] # Open file and get data - handle = open(path, 'r') data = [] - for line in handle: - line = line.rstrip() - data.append(line) - handle.close() - - return data, name, ext - - - def _find_dimensions(self, data): - """ - Gets the number of Q, time and frequency values in given raw data. - - @param data Raw data to search - """ - - num_q_values = _find_tab_starts(data, 'NQVALUES', 0) - num_time_values = _find_tab_starts(data, 'NTIMES', 0) - num_freq_values = _find_tab_starts(data, 'NFREQUENCIES', 0) - - q_el = data[num_q_values].split() - num_q = int(q_el[2]) - t_el = data[num_time_values].split() - num_t = int(t_el[2]) - f_el = data[num_freq_values].split() - num_f = int(f_el[2]) - - logger.debug(data[2][1:-1]) - logger.debug(data[3][1:-1]) - logger.debug(data[6][1:-1]) - - return num_q, num_t, num_f - - - def _cdl_import(self, data, name): - """ - Import data from CDL file. - - @param data Raw data - @param name Name of data file - """ - - logger.notice('Loading CDL file: %s' % name) + with open(self._file_name, 'r') as handle: + for line in handle: + line = line.rstrip() + data.append(line) len_data = len(data) # raw head - nQ, nT, nF = self._find_dimensions(data) + num_spec, nT, nF = _cdl_find_dimensions(data) ldata = _find_starts(data, 'data:', 0) lq1 = _find_starts(data, ' q =', ldata) # start Q values lq2 = _find_starts(data, ' q =', lq1 - 1) Qlist = _make_list(data, lq1, lq2) - if nQ != len(Qlist): + if num_spec != len(Qlist): raise RUntimeError('Error reading Q values') Qf = Qlist[0].split() Q = [float(Qf[2]) / 10.0] - for m in range(1, nQ - 1): + for m in range(1, num_spec - 1): Q.append(float(Qlist[m]) / 10.0) - Q.append(float(Qlist[nQ - 1][:-1]) / 10.0) + Q.append(float(Qlist[num_spec - 1][:-1]) / 10.0) logger.information('Q values = ' + str(Q)) lt1 = _find_starts(data, ' time =', lq2) # start T values @@ -263,17 +244,17 @@ class LoadNMoldyn3Ascii(PythonAlgorithm): if func[:3] == 'Fqt': nP = nT xEn = np.array(T) - eZero = np.zeros(nT) - xUnit = 'TOF' + zero_error = np.zeros(nT) + x_unit = 'TOF' elif func[:3] == 'Sqw': nP = nF xEn = np.array(F) - eZero = np.zeros(nF) - xUnit = 'Energy' + zero_error = np.zeros(nF) + x_unit = 'Energy' else: raise RuntimeError('Failed to parse function string ' + func) - for n in range(0, nQ): + for n in range(0, num_spec): for m in range(lstart, len_data): char = data[m] if char.startswith(' // ' + func): @@ -287,12 +268,12 @@ class LoadNMoldyn3Ascii(PythonAlgorithm): raise RuntimeError('Failed to parse function string ' + func) Qaxis = '' - for n in range(0, nQ): + for n in range(0, num_spec): logger.information(str(start)) logger.information('Reading : ' + data[start[n]]) Slist = _make_list(data, start[n] + 1, start[n + 1] - 1) - if n == nQ - 1: + if n == num_spec - 1: Slist[nP - 1] = Slist[nP - 1][:-1] S = [] for m in range(0, nP): @@ -303,111 +284,158 @@ class LoadNMoldyn3Ascii(PythonAlgorithm): logger.information('S values = ' + str(S[:2]) + ' to ' + str(S[-2:])) if n == 0: Qaxis += str(Q[n]) - xDat = xEn - yDat = np.array(S) - eDat = eZero + data_x = xEn + data_y = np.array(S) + data_e = zero_error else: Qaxis += ',' + str(Q[n]) - xDat = np.append(xDat, xEn) - yDat = np.append(yDat, np.array(S)) - eDat = np.append(eDat, eZero) - outWS = name + '_' + func - - CreateWorkspace(OutputWorkspace=outWS, - DataX=xDat, - DataY=yDat, - DataE=eDat, - Nspec=nQ, - UnitX=xUnit, + data_x = np.append(data_x, xEn) + data_y = np.append(data_y, np.array(S)) + data_e = np.append(data_e, zero_error) + + function_ws_name = base_name + '_' + func + CreateWorkspace(OutputWorkspace=function_ws_name, + DataX=data_x, + DataY=data_y, + DataE=data_e, + Nspec=num_spec, + UnitX=x_unit, VerticalAxisUnit='MomentumTransfer', VerticalAxisValues=Qaxis) - output_ws_list.append(outWS) + output_ws_list.append(function_ws_name) - GroupWorkspaces(InputWorkspaces=output_ws_list, - OutputWorkspace=self._out_ws) + out_ws = GroupWorkspaces(InputWorkspaces=output_ws_list, + OutputWorkspace=self._out_ws) + return out_ws - def _ascii_import(self, data, name): - """ - Import ASCII data. +#------------------------------------------------------------------------------- - @param data Raw ASCII data - @param name Name of data file + def _ascii_3d_import(self): """ - + Import 3D ASCII data (e.g. I(Q, t)). + """ + logger.notice('Loading ASCII data') from IndirectCommon import getEfixed from IndirectNeutron import ChangeAngles, InstrParas - logger.notice('Loading ASCII data: %s' % name) - - val = _split_line(data[3]) - Q = [] - for n in range(1, len(val)): - Q.append(val[n]) - - nQ = len(Q) - x = [] - y = [] - for n in range(4, len(data)): - val = _split_line(data[n]) - x.append(val[0]) - yval = val[1:] - y.append(yval) - - nX = len(x) - logger.information('nQ = ' + str(nQ)) - logger.information('nT = ' + str(nX)) - - xT = np.array(x) - eZero = np.zeros(nX) - #Qaxis = '' - for m in range(0, nQ): - logger.information('Q[' + str(m + 1) + '] : ' + str(Q[m])) - - S = [] - for n in range(0, nX): - S.append(y[n][m]) - - if m == 0: - #Qaxis += str(Q[m]) - xDat = xT - yDat = np.array(S) - eDat = eZero - else: - #Qaxis += ',' + str(Q[m]) - xDat = np.append(xDat, xT) - yDat = np.append(yDat, np.array(S)) - eDat = np.append(eDat, eZero) - - CreateWorkspace(OutputWorkspace=self._out_ws, - DataX=xDat, - DataY=yDat, - DataE=eDat, - Nspec=nQ, - UnitX='TOF') - - Qmax = Q[nQ - 1] - instr = 'MolDyn' - ana = 'simul' - if Qmax <= 2.0: - refl = '2' - else: - refl = '4' - - InstrParas(self._out_ws, instr, ana, refl) - efixed = getEfixed(self._out_ws) - logger.information('Qmax = ' + str(Qmax) + ' ; efixed = ' + str(efixed)) - pi4 = 4.0 * math.pi + # Read file + data = [] + x_axis = ('time', 'ns') + v_axis = ('q', 'ang^-1') + with open(self._file_name, 'r') as handle: + for line in handle: + line = line.strip() + + # Ignore empty lines + if line == '': + continue + + # Data line (if not comment) + elif line.strip()[0] != '#': + line_values = np.array([ast.literal_eval(t.strip()) if 'nan' not in t.lower() else np.nan for t in line.split()]) + data.append(line_values) + + if x_axis is None or v_axis is None: + raise ValueError('Data is not in expected format for 3D data') + logger.debug('X axis: {0}'.format(x_axis)) + logger.debug('V axis: {0}'.format(v_axis)) + + # Get axis and Y values + data = np.swapaxes(np.array(data), 0, 1) + x_axis_values = data[0,1:] + v_axis_values = data[1:,0] + y_values = np.ravel(data[1:,1:]) + + # Create the workspace + + wks = CreateWorkspace(OutputWorkspace=self._out_ws, + DataX=x_axis_values, + DataY=y_values, + NSpec=v_axis_values.size, + UnitX=x_axis[1], + EnableLogging=False) + + # Load the MolDyn instrument + q_max = v_axis_values[-1] + instrument = 'MolDyn' + reflection = '2' if q_max <= 2.0 else '4' + InstrParas(wks.name(), instrument, 'simul', reflection) + + # Process angles + efixed = getEfixed(wks.name()) + logger.information('Qmax={0}, Efixed={1}'.format(q_max, efixed)) wave = 1.8 * math.sqrt(25.2429 / efixed) - theta = [] - for n in range(0, nQ): - qw = wave * Q[n] / pi4 - ang = 2.0 * math.degrees(math.asin(qw)) - theta.append(ang) + qw = wave * v_axis_values / (4.0 * math.pi) + theta = 2.0 * np.degrees(np.arcsin(qw)) + ChangeAngles(wks.name(), instrument, theta) + + return wks - ChangeAngles(self._out_ws, instr, theta) +#------------------------------------------------------------------------------- + def _ascii_2d_import(self): + """ + Import 2D ASCII data (e.g. DoS). + """ + logger.notice('Loading ASCII data') + + # Regex + x_axis_regex = re.compile(r"\s*columns-1\s*=\s*([A-z0-9\-\s]+)\s*\(([A-z0-9-\s*]+)\)") + y_axis_regex = re.compile(r"\s*columns-2\s*=\s*([A-z\-\s]+)") + + # Read file + data = [] + x_axis = None + y_axis = None + with open(self._file_name, 'r') as handle: + for line in handle: + line = line.strip() + + # Ignore empty lines + if line == "": + continue + + x_axis_match = x_axis_regex.match(line) + y_axis_match = y_axis_regex.match(line) + + # Line (X) header + if x_axis_match: + x_axis = (x_axis_match.group(1).strip(), x_axis_match.group(2)) + + # Data (Y) header + elif y_axis_match: + y_axis = y_axis_match.group(1) + + # Data line (if not comment) + elif line.strip()[0] != '#': + line_values = np.array([ast.literal_eval(t.strip()) if 'nan' not in t.lower() else np.nan for t in line.split()]) + data.append(line_values) + + if x_axis is None or y_axis is None: + raise ValueError('Data is not in expected format for 2D data') + logger.debug('X axis: {0}'.format(x_axis)) + logger.debug('Y axis: {0}'.format(y_axis)) + + # Get axis and Y values + data = np.array(data) + x_axis_values = data[:,0] + y_values = data[:,1] + + # Create the workspace + wks = CreateWorkspace(OutputWorkspace=self._out_ws, + DataX=x_axis_values, + DataY=y_values, + NSpec=1, + UnitX=x_axis[1], + WorkspaceTitle=y_axis, + YUnitLabel=y_axis, + EnableLogging=False) + + return wks + +#============================================================================== # Register algorithm with Mantid AlgorithmFactory.subscribe(LoadNMoldyn3Ascii) diff --git a/Code/Mantid/Framework/PythonInterface/test/python/plugins/algorithms/LoadNMoldyn3AsciiTest.py b/Code/Mantid/Framework/PythonInterface/test/python/plugins/algorithms/LoadNMoldyn3AsciiTest.py index ba26ed1c9f5acb451c06474f4ccb7caf224e440b..9e65c1569384ecd852eb8205dce7faf667f23588 100644 --- a/Code/Mantid/Framework/PythonInterface/test/python/plugins/algorithms/LoadNMoldyn3AsciiTest.py +++ b/Code/Mantid/Framework/PythonInterface/test/python/plugins/algorithms/LoadNMoldyn3AsciiTest.py @@ -14,7 +14,6 @@ class LoadNMoldyn3AsciiTest(unittest.TestCase): """ Load an F(Q, t) function from an nMOLDYN 3 .cdl file """ - moldyn_group = LoadNMoldyn3Ascii(Filename=self._cdl_filename, Functions=['Fqt-total'], OutputWorkspace='__LoadNMoldyn3Ascii_test') @@ -34,9 +33,8 @@ class LoadNMoldyn3AsciiTest(unittest.TestCase): def test_load_sqw_from_cdl(self): """ - Load an S(Q, w) function from a nMOLDYN 3 .cdl file + Load an S(Q, w) function from an nMOLDYN 3 .cdl file """ - moldyn_group = LoadNMoldyn3Ascii(Filename=self._cdl_filename, Functions=['Sqw-total'], OutputWorkspace='__LoadNMoldyn3Ascii_test') @@ -53,11 +51,24 @@ class LoadNMoldyn3AsciiTest(unittest.TestCase): self.assertEqual(units, 'Energy') + def test_load_dos_from_dat(self): + """ + Load a density of states from an nMOLDYN 3 .dat file. + """ + moldyn_ws = LoadNMoldyn3Ascii(Filename='CDOS_Croco_total_10K.dat', + OutputWorkspace='__LoadNMoldyn3Ascii_test') + + self.assertTrue(isinstance(moldyn_ws, MatrixWorkspace)) + self.assertTrue(moldyn_ws.getNumberHistograms(), 1) + + self.assertEqual(moldyn_ws.getAxis(0).getUnit().label(), 'THz') + self.assertEqual(moldyn_ws.YUnitLabel(), 'dos-total vs frequency') + + def test_load_from_dat(self): """ Load a function from an nMOLDYN 3 .dat file """ - moldyn_ws = LoadNMoldyn3Ascii(Filename=self._dat_filename, OutputWorkspace='__LoadNMoldyn3Ascii_test') diff --git a/Code/Mantid/Testing/Data/UnitTest/CDOS_Croco_total_10K.dat.md5 b/Code/Mantid/Testing/Data/UnitTest/CDOS_Croco_total_10K.dat.md5 new file mode 100644 index 0000000000000000000000000000000000000000..d11a46b00c2a8df75d4c32288b60d06e7c0ebfb4 --- /dev/null +++ b/Code/Mantid/Testing/Data/UnitTest/CDOS_Croco_total_10K.dat.md5 @@ -0,0 +1 @@ +6f70d4023fb7ddf43f30994cd0c4f6b6 diff --git a/Code/Mantid/Testing/SystemTests/tests/analysis/reference/ISISIndirectSimulation_MolDynDAT.nxs.md5 b/Code/Mantid/Testing/SystemTests/tests/analysis/reference/ISISIndirectSimulation_MolDynDAT.nxs.md5 index a21e268edc40adb5165688c907468f36c0130937..ddc570ba6d30987d200d9cce06962f55263501bc 100644 --- a/Code/Mantid/Testing/SystemTests/tests/analysis/reference/ISISIndirectSimulation_MolDynDAT.nxs.md5 +++ b/Code/Mantid/Testing/SystemTests/tests/analysis/reference/ISISIndirectSimulation_MolDynDAT.nxs.md5 @@ -1 +1 @@ -bc22e1ca81aa608b028c11f7099b9f6c \ No newline at end of file +48df154fcd3ae3119874479f929a5666 diff --git a/Code/Mantid/docs/source/algorithms/LoadNMoldyn3Ascii-v1.rst b/Code/Mantid/docs/source/algorithms/LoadNMoldyn3Ascii-v1.rst index e8e00ea6fb3d8e8ad9fad75c664ae3b6bc8fb0b9..6e0aca39fff33dcc414e7c857e94d67803ef95a0 100644 --- a/Code/Mantid/docs/source/algorithms/LoadNMoldyn3Ascii-v1.rst +++ b/Code/Mantid/docs/source/algorithms/LoadNMoldyn3Ascii-v1.rst @@ -13,6 +13,10 @@ This algorithm is a loader from simulation data saved by version 3 of the nMOLDYN package, simulations can be loaded form either a plain ASCII (``.dat``) file or ``.cdl`` file. +Currently this supports loading :math:`S(Q, \omega)` and :math:`F(Q, t)` +functions from ``.cdl`` files, :math:`F(Q, t)` from ``.dat`` files and any 2D +function (such as density of states) from ``.dat`` files. + Usage -----