From eb3e895369c73712f407974b9181c1b83596555b Mon Sep 17 00:00:00 2001 From: Noella Spitz Date: Fri, 10 May 2024 15:01:42 +0100 Subject: [PATCH 01/78] Store output in data --- gudpy/core/container.py | 2 + gudpy/core/data_files.py | 119 ++++++++++++++++-- gudpy/core/file_library.py | 14 ++- gudpy/core/gudpy.py | 68 ++++++++++ gudpy/core/gudpy_yaml.py | 25 ++-- gudpy/core/gudrun_file.py | 11 +- gudpy/core/iterators.py | 22 +++- gudpy/core/output_file_handler.py | 63 +++++++--- gudpy/core/sample.py | 13 ++ gudpy/core/sample_background.py | 3 + gudpy/gui/widgets/core/output_tree.py | 8 +- gudpy/gui/widgets/core/worker.py | 9 +- gudpy/gui/widgets/dialogs/io.py | 6 +- gudpy/gui/widgets/slots/container_slots.py | 4 +- .../gui/widgets/slots/normalisation_slots.py | 8 +- .../widgets/slots/sample_background_slots.py | 4 +- gudpy/gui/widgets/slots/sample_slots.py | 6 +- 17 files changed, 313 insertions(+), 72 deletions(-) diff --git a/gudpy/core/container.py b/gudpy/core/container.py index e401a42bb..4e9863633 100644 --- a/gudpy/core/container.py +++ b/gudpy/core/container.py @@ -113,6 +113,8 @@ def __init__(self, config=None): if config: self.parseFromConfig(config) + self.outputFolder = "" + def __str__(self): """ Returns the string representation of the Container object. diff --git a/gudpy/core/data_files.py b/gudpy/core/data_files.py index f8a9b1ff8..6e5defc32 100644 --- a/gudpy/core/data_files.py +++ b/gudpy/core/data_files.py @@ -1,4 +1,77 @@ +import os + from core import config +from core import utils +from core.gud_file import GudFile + + +class DataFile: + def __init__(self, filename, isSampleDataFile: bool = False): + self.filename = filename + self.name = utils.replace_unwanted_chars( + os.path.splitext(filename)[0] + ) + self.ext = os.path.splitext(filename)[1] + self.outputFolder = "" + self._outputs = {} + self.isSampleDataFile = isSampleDataFile + + self.yamlignore = { + "str", + "yamlignore" + } + + def detectSampleDataFile(self, path): + for f in os.listdir(path): + if f == "Diagnostics": + self.isSampleDataFile = True + + def constructDictFromDir(self, dirPath, dict): + for f in os.listdir(dirPath): + ext = os.path.splitext(f) + dict[ext] = f + + def importOutputs(self, path): + self.detectSampleDataFile(path) + if self.isSampleDataFile: + outputsDir = os.path.join(path, "Outputs") + diagDir = os.path.join(path, "Diagnostics") + self.constructDictFromDir(outputsDir, self._outputs) + self.constructDictFromDir(diagDir, self.diagnostics) + else: + self.constructDictFromDir(path, self._outputs) + + def addOutput(self, path): + ext = os.path.splitext(path)[1] + self._outputs[ext] = path + + def outputs(self, ext): + return self._outputs[ext] + + def __str__(self): + return self.filename + + @property + def gudFile(self): + if not self.isSampleDataFile: + return None + if not self.gudFile: + gudPath = self._outputs.get(".gud", None) + if not gudPath: + return None + return GudFile(gudPath) + + @property + def mintFile(self): + if not self.isSampleDataFile: + return None + return self._outputs.get(".mint01", None) + + @property + def msubwFile(self): + if not self.isSampleDataFile: + return None + return self._outputs.get(".msubw01", None) class DataFiles: @@ -28,7 +101,10 @@ def __init__(self, dataFiles, name): name : str Name of the parent of the data files, e.g. Sample Background """ - self.dataFiles = dataFiles + + self._dataFiles = [] + self.setFiles(dataFiles) + self.name = name self.yamlignore = { @@ -36,6 +112,24 @@ def __init__(self, dataFiles, name): "yamlignore" } + self.outputFolders = {} + self._outputs = {} + + def setFiles(self, dataFilenames): + self._dataFiles.clear() + for dataFile in dataFilenames: + if not dataFile: + continue + self._dataFiles.append(DataFile(dataFile)) + + def importOutputs(self, path): + self.outputFolder = path + children = os.listdir(path) + + for df in self._dataFiles: + if df.name in children: + return + def __str__(self): """ Returns the string representation of the DataFiles object. @@ -50,11 +144,20 @@ def __str__(self): String representation of DataFiles. """ self.str = [ - df + config.spc10 + self.name + " data files" - for df in self.dataFiles + df.filename + config.spc10 + self.name + " data files" + for df in self._dataFiles ] return """\n""".join(self.str) + @property + def dataFilenames(self): + dfNames = [df.filename for df in self._dataFiles] + return dfNames + + @property + def dataFiles(self): + return self._dataFiles + def __len__(self): """ Returns the length of the dataFiles list member. @@ -68,15 +171,15 @@ def __len__(self): int Number of data files, """ - return len(self.dataFiles) + return len(self._dataFiles) def __getitem__(self, n): - return self.dataFiles[n] + return self._dataFiles[n].filename def __setitem__(self, n, item): if n >= len(self): - self.dataFiles.extend(n+1) - self.dataFiles[n] = item + self._dataFiles.extend(n+1) + self._dataFiles[n] = DataFile(item) def __iter__(self): - return iter(self.dataFiles) + return iter(self._dataFiles) diff --git a/gudpy/core/file_library.py b/gudpy/core/file_library.py index 77d7acdab..1781b82c2 100644 --- a/gudpy/core/file_library.py +++ b/gudpy/core/file_library.py @@ -64,8 +64,8 @@ def __init__(self, gudrunFile): } self.dataFiles = [ - *gudrunFile.normalisation.dataFiles.dataFiles, - *gudrunFile.normalisation.dataFilesBg.dataFiles, + *gudrunFile.normalisation.dataFiles.dataFilenames, + *gudrunFile.normalisation.dataFilesBg.dataFilenames, ] # If NXS files are being used @@ -90,15 +90,15 @@ def __init__(self, gudrunFile): # that file too. for sampleBackground in gudrunFile.sampleBackgrounds: - self.dataFiles.extend(sampleBackground.dataFiles.dataFiles) + self.dataFiles.extend(sampleBackground.dataFiles.dataFilenames) for sample in sampleBackground.samples: - self.dataFiles.extend(sample.dataFiles.dataFiles) + self.dataFiles.extend(sample.dataFiles.dataFilenames) if sample.totalCrossSectionSource == CrossSectionSource.FILE: self.files[sample.name] = sample.crossSectionFilename for container in sample.containers: - self.dataFiles.extend(container.dataFiles.dataFiles) + self.dataFiles.extend(container.dataFiles.dataFilenames) if container.totalCrossSectionSource == ( CrossSectionSource.FILE ): @@ -180,7 +180,7 @@ def exportMintData( if len(sample.dataFiles.dataFiles): path = os.path.join( self.gudrunFile.projectDir, - sample.dataFiles.dataFiles[0].replace( + sample.dataFiles.dataFilenames[0].replace( self.gudrunFile.instrument.dataFileType, "mint01" ), ) @@ -199,6 +199,8 @@ def exportMintData( self.gudrunFile.projectDir, safeSampleName + ".sample", ) + sample.sampleFile = path + if not os.path.exists(path): sample.write_out( self.gudrunFile.projectDir diff --git a/gudpy/core/gudpy.py b/gudpy/core/gudpy.py index 1072d5ead..5e6474ef2 100644 --- a/gudpy/core/gudpy.py +++ b/gudpy/core/gudpy.py @@ -485,6 +485,13 @@ def organiseOutput( gudrunOutput = outputHandler.organiseOutput(exclude=exclude) return gudrunOutput + def obtainDependencies( + fileSelfScattering, + gudrunFile: GudrunFile + ): + if gudrunFile.instrument.subWavelengthBinnedData: + return + def gudrun( self, gudrunFile: GudrunFile, @@ -615,6 +622,67 @@ def iterate(self, purge, save=True) -> typ.Tuple[int, str]: return self.exitcode +class InelasiticityIterator(GudrunIterator): + def __init__( + self, + gudrunFile: GudrunFile, + iterator: iterators.Iterator, + ): + + super().__init__( + gudrunFile=gudrunFile, + iterator=iterator + ) + + def copyDCS(self, prevOutput): + return + + def singleIteration( + self, + gudrunFile: GudrunFile, + gudrun: Gudrun, + purge: Purge, + prevOutput: handlers.GudrunOutput, + save=True + ) -> typ.Tuple[int, str]: # (exitcode, error) + modGfFile = self.iterator.performIteration(gudrunFile, prevOutput) + exitcode = gudrun.gudrun(modGfFile, purge, self.iterator, save=save) + if exitcode: + return exitcode + self.gudrunOutput = gudrun.gudrunOutput + return 0 + + def iterate(self, purge, save=True) -> typ.Tuple[int, str]: + prevOutput = None + + # If the iterator requires a prelimenary run + if self.iterator.requireDefault: + exitcode = self.gudrunObjects[0].gudrun( + self.gudrunFile, purge, self.iterator, save) + if exitcode: # An exit code != 0 indicates failure + self.exitcode = (exitcode, self.gudrunObjects[0].error) + return self.exitcode + prevOutput = self.gudrunObjects[0].gudrunOutput + + # Iterate through gudrun objects + for gudrun in self.gudrunObjects: + if gudrun.output: + # If object has already been run, skip + continue + exitcode = self.singleIteration( + self.gudrunFile, gudrun, purge, prevOutput, save) + if exitcode: # An exit code != 0 indicates failure + self.exitcode = (exitcode, gudrun.error) + return self.exitcode + + prevOutput = gudrun.gudrunOutput + + self.result = self.iterator.result + + self.exitcode = (0, "") + return self.exitcode + + class CompositionIterator: def __init__( self, diff --git a/gudpy/core/gudpy_yaml.py b/gudpy/core/gudpy_yaml.py index 4682db6c1..ccbb1d415 100644 --- a/gudpy/core/gudpy_yaml.py +++ b/gudpy/core/gudpy_yaml.py @@ -7,7 +7,7 @@ from core.composition import ( Component, Components, Composition, WeightedComponent ) -from core.data_files import DataFiles +from core.data_files import DataFile, DataFiles from core.element import Element from core.exception import YAMLException from core.gui_config import GUIConfig @@ -23,7 +23,6 @@ class YAML: - def __init__(self): self.yaml = self.getYamlModule() @@ -95,18 +94,17 @@ def maskYAMLDicttoClass(self, cls, yamldict): for k, v in yamldict.items(): if not hasattr(cls, k): # If attribute is not valid - raise YAMLException( - f"Invalid attribute '{k}' given to '{type(cls).__name__}'") + print(f"Ignoring depreciated attribute '{k}'" + f"given to '{type(cls).__name__}'") + continue if isinstance(cls.__dict__[k], Enum): setattr(cls, k, type(cls.__dict__[k])[v]) elif isinstance(cls.__dict__[k], DataFiles): - setattr( - cls, k, - DataFiles( - [v_ for v_ in v["dataFiles"]], v["name"]) - ) + for df in v["_dataFiles"]: + dataFile = DataFile(df["filename"]) + self.maskYAMLDicttoClass(dataFile, df) elif ( isinstance( @@ -233,7 +231,7 @@ def toYaml(self, var): elif isinstance(var, ( Instrument, Beam, Components, Normalisation, SampleBackground, Sample, Container, WeightedComponent, - Component, Composition, Element, DataFiles, GUIConfig + Component, Composition, Element, DataFiles, DataFile, GUIConfig )): return { k: self.toYaml(v) @@ -243,8 +241,15 @@ def toYaml(self, var): @abstractmethod def toBuiltin(self, yamlvar): + if yamlvar is None: + return None if isinstance(yamlvar, (list, tuple)): return [self.toBuiltin(v) for v in yamlvar] + elif (yamlvar.__class__.__module__ == + "ruamel.yaml.comments.CommentedMap"): + dict = {} + for k, v in yamlvar.items(): + dict[k] = v elif yamlvar.__class__.__module__ == "builtins": return yamlvar elif yamlvar.__class__.__module__ == "ruamel.yaml.scalarfloat": diff --git a/gudpy/core/gudrun_file.py b/gudpy/core/gudrun_file.py index 0a50f4d64..de60d15fd 100644 --- a/gudpy/core/gudrun_file.py +++ b/gudpy/core/gudrun_file.py @@ -182,6 +182,12 @@ def __deepcopy__(self, memo): setattr(result, k, deepcopy(v, memo)) return result + def runSamples(self): + return [ + s for sb in self.sampleBackgrounds + for s in sb.samples + if s.runThisSample] + def path(self): if not self.projectDir: return None @@ -1478,7 +1484,10 @@ def save(self, path='', format=None): self.write_out( path=f"{os.path.splitext(path)[0]}.txt", overwrite=True) elif format == Format.YAML: - self.write_yaml(path=f"{os.path.splitext(path)[0]}.yaml") + if self.path(): + filepath = os.path.join( + self.path(), f"{os.path.splitext(path)[0]}.yaml") + self.write_yaml(filepath) def write_yaml(self, path): self.yaml = YAML() diff --git a/gudpy/core/iterators.py b/gudpy/core/iterators.py index 233606938..34e8f1ac7 100644 --- a/gudpy/core/iterators.py +++ b/gudpy/core/iterators.py @@ -1,10 +1,12 @@ from copy import deepcopy +import os import math from enum import Enum from core.gud_file import GudFile from core.enums import Scales, IterationModes from core.gudrun_file import GudrunFile +from core import utils import core.output_file_handler as handlers @@ -493,7 +495,7 @@ def setSelfScatteringFiles(self, scale, gudrunFile, prevOutput=None): for sample in sampleBackground.samples: if sample.runThisSample and len(sample.dataFiles): target = sample - filename = target.dataFiles[0] + filename = target.dataFiles[0].filename targetFile = ( prevOutput.output( sample.name, filename, suffix) @@ -580,13 +582,29 @@ def organiseOutput(self, gudrunFile, exclude=[]): """ overwrite = (self.iterationCount == 1 and self.iterationType == "WavelengthIteration") + head = os.path.join( + utils.replace_unwanted_chars(self.name), + f"{self.iterationType}_{self.iterationCount}" + ) + + # Organise output history (don't overwrite) + outputHandler = handlers.GudrunOutputHandler( gudrunFile=gudrunFile, - head=f"{self.iterationType}_{self.iterationCount}", + head=head, overwrite=overwrite ) output = outputHandler.organiseOutput(exclude=exclude) self.gudrunOutputs.append(output) + + # Organise current output (overrides each time) + + outputHandler = handlers.GudrunOutputHandler( + gudrunFile=gudrunFile, + overwrite=True + ) + output = outputHandler.organiseOutput(exclude=exclude) + return output diff --git a/gudpy/core/output_file_handler.py b/gudpy/core/output_file_handler.py index 98631a584..243016e1f 100644 --- a/gudpy/core/output_file_handler.py +++ b/gudpy/core/output_file_handler.py @@ -203,12 +203,15 @@ def _createNormDir(self, dest: str): # Create normalisation folders and move datafiles for normFile in self.gudrunFile.normalisation.dataFiles: self._copyOutputs( - normFile, os.path.join( - dest, "Normalisation")) + normFile, dest, "Normalisation") + self.gudrunFile.normalisation.outputDir = os.path.join( + self.outputDir, "Normalisation" + ) for normBgFile in self.gudrunFile.normalisation.dataFilesBg: - self._copyOutputs(normBgFile, - os.path.join(dest, - "NormalisationBackground")) + self._copyOutputs(normBgFile, dest, "NormalisationBackground") + self.gudrunFile.normalisation.backgroundOutputDir = os.path.join( + self.outputDir, "NormalisationBackground" + ) def _createSampleBgDir(self, dest: str): """ @@ -227,10 +230,15 @@ def _createSampleBgDir(self, dest: str): for dataFile in sampleBackground.dataFiles: self._copyOutputs( dataFile, + dest, os.path.join( - dest, "SampleBackgrounds", - f"SampleBackground{count + 1}") - ) + "SampleBackgrounds", + f"SampleBackground{count + 1}" + )) + sampleBackground.outputDir = os.path.join( + self.outputDir, "SampleBackgrounds", + f"SampleBackground{count + 1}" + ) def _createSampleDir(self, dest: str): """ @@ -297,17 +305,21 @@ def _createSampleDir(self, dest: str): sampleOutputs[sample.name] = SampleOutput( sampleFile, gudFile, sampleOutput, sampleDiag) + sample.outputFolder = os.path.join( + self.outputDir, + utils.replace_unwanted_chars(sample.name) + + ) + # Create container folders within sample folder for container in sample.containers: - containerPath = os.path.join( - samplePath, - (utils.replace_unwanted_chars(container.name) - if container.name != "CONTAINER" - else "Container")) for dataFile in container.dataFiles: self._copyOutputs( dataFile, - containerPath + samplePath, + (utils.replace_unwanted_chars(container.name) + if container.name != "CONTAINER" + else "Container") ) return sampleOutputs @@ -349,7 +361,7 @@ def _createAddOutDir(self, dest: str, exclude: list[str] = []): continue return inputFile - def _copyOutputs(self, fpath, dest): + def _copyOutputs(self, dataFile, dest, folderName): """ Copy all files with the same basename as the provided filepath, except the original file. @@ -363,13 +375,15 @@ def _copyOutputs(self, fpath, dest): dest : str Directory for the files to be copied to """ - fname = os.path.splitext(fpath)[0] + dest = os.path.join(dest, folderName) + fname = os.path.splitext(dataFile.filename)[0] runDir = os.path.join(dest, fname) dirCreated = False for f in os.listdir(self.gudrunDir): # Get files with the same filename but not the same # extension - if os.path.splitext(f)[0] == fname: + name, ext = os.path.splitext(f) + if name == fname: if not dirCreated: utils.makeDir(runDir) dirCreated = True @@ -377,9 +391,15 @@ def _copyOutputs(self, fpath, dest): os.path.join(self.gudrunDir, f), os.path.join(runDir, f) ) + dataFile.outputFolder = os.path.join( + self.outputDir, folderName, fname + ) + dataFile.addOutput(os.path.join( + self.outputDir, folderName, fname, f + )) self.copiedFiles.append(f) - def _copyOutputsByExt(self, fpath, dest, folderName): + def _copyOutputsByExt(self, dataFile, dest, folderName): """ Copy all files with the same basename as the provided filepath and splits them into outputs @@ -404,7 +424,7 @@ def _copyOutputsByExt(self, fpath, dest, folderName): Dictionary mapping output extension to filepath """ # Data filename - fname = os.path.splitext(fpath)[0] + fname = os.path.splitext(dataFile.filename)[0] # Path to folder which will hold all outputs from the run runDir = os.path.join(dest, fname) # Has the run dir been created? @@ -431,9 +451,14 @@ def _copyOutputsByExt(self, fpath, dest, folderName): gudFile = GudFile(os.path.join(self.gudrunDir, f)) outputs[ext] = os.path.join( self.outputDir, folderName, fname, "Outputs", f) + dataFile.addOutput(os.path.join( + self.outputDir, folderName, fname, "Outputs", f)) + else: diagnostics[ext] = os.path.join( self.outputDir, folderName, fname, "Diagnostics", f) + dataFile.addOutput(os.path.join( + self.outputDir, folderName, fname, "Diagnostics", f)) shutil.copyfile( os.path.join(self.gudrunDir, f), os.path.join(dir, f) diff --git a/gudpy/core/sample.py b/gudpy/core/sample.py index 2f86ba95e..f16b9254f 100644 --- a/gudpy/core/sample.py +++ b/gudpy/core/sample.py @@ -1,3 +1,5 @@ +import os + from core.utils import bjoin, numifyBool from core.data_files import DataFiles from core.composition import Composition @@ -148,6 +150,17 @@ def __init__(self): "yamlignore" } + self.outputFolder = "" + self.sampleFile = "" + + def importOutputs(self, path): + self.outputFolder = path + for f in os.listdir(path): + ext = os.path.splitext(f) + if ext == ".sample": + self.sampleFile = f + return + def pathName(self): return utils.replace_unwanted_chars(self.name).translate( {ord(x): '' for x in r'/\!*~,&|[]'} diff --git a/gudpy/core/sample_background.py b/gudpy/core/sample_background.py index 4e454899e..2c4596ddb 100644 --- a/gudpy/core/sample_background.py +++ b/gudpy/core/sample_background.py @@ -18,6 +18,7 @@ class SampleBackground: Methods ------- """ + def __init__(self): """ Constructs all the necessary attributes for the @@ -37,6 +38,8 @@ def __init__(self): "yamlignore" } + self.outputFolder = "" + def __str__(self): """ Returns the string representation of the SampleBackground object. diff --git a/gudpy/gui/widgets/core/output_tree.py b/gudpy/gui/widgets/core/output_tree.py index b8f07eb71..c03e4e4d1 100644 --- a/gudpy/gui/widgets/core/output_tree.py +++ b/gudpy/gui/widgets/core/output_tree.py @@ -81,13 +81,7 @@ def setupData(self): enumerate(splicedOutput) if "Got to: SAMPLE" in l ][1:] for sample, index in zip( - [ - s for sb in - self.gudrunFile.sampleBackgrounds - for s in sb.samples - if s.runThisSample - ], indices - ): + self.gudrunFile.runSamples(), indices): s = deepcopy(sample) s.output = index + start diff --git a/gudpy/gui/widgets/core/worker.py b/gudpy/gui/widgets/core/worker.py index f27ef0638..1456e8123 100644 --- a/gudpy/gui/widgets/core/worker.py +++ b/gudpy/gui/widgets/core/worker.py @@ -39,13 +39,16 @@ def __init__(self, purgeFile: PurgeFile, gudrunFile: GudrunFile): self.appendDataFiless(gudrunFile.normalisation.dataFiles[0]) self.appendDataFiless(gudrunFile.normalisation.dataFilesBg[0]) - self.appendDataFiless([df for sb in gudrunFile.sampleBackgrounds + self.appendDataFiless([df.filename for sb + in gudrunFile.sampleBackgrounds for df in sb.dataFiles]) if not purgeFile.excludeSampleAndCan: - self.appendDataFiless([df for sb in gudrunFile.sampleBackgrounds + self.appendDataFiless([df.filename for sb + in gudrunFile.sampleBackgrounds for s in sb.samples for df in s.dataFiles if s.runThisSample]) - self.appendDataFiless([df for sb in gudrunFile.sampleBackgrounds + self.appendDataFiless([df.filename for sb + in gudrunFile.sampleBackgrounds for s in sb.samples for c in s.containers for df in c.dataFiles if s.runThisSample]) diff --git a/gudpy/gui/widgets/dialogs/io.py b/gudpy/gui/widgets/dialogs/io.py index a5510ac5c..1a3407819 100644 --- a/gudpy/gui/widgets/dialogs/io.py +++ b/gudpy/gui/widgets/dialogs/io.py @@ -177,11 +177,7 @@ def toggleRename(self, state): def performExport(self, filename=None): fl = GudPyFileLibrary(self.gudrunFile) archive = fl.exportMintData( - [ - s - for sb in self.gudrunFile.sampleBackgrounds - for s in sb.samples - ], + self.gudrunFile.samples(), renameDataFiles=self.widget.renameCheckBox.checkState(), exportTo=filename, includeParams=self.widget.includeParamsCheckBox.checkState() diff --git a/gudpy/gui/widgets/slots/container_slots.py b/gudpy/gui/widgets/slots/container_slots.py index 87391a142..7aab92bf0 100644 --- a/gudpy/gui/widgets/slots/container_slots.py +++ b/gudpy/gui/widgets/slots/container_slots.py @@ -26,7 +26,7 @@ def setContainer(self, container): # Populate the data files list. self.widget.containerDataFilesList.makeModel( - self.container.dataFiles.dataFiles + self.container.dataFiles.dataFilenames ) self.widget.containerDataFilesList.model().dataChanged.connect( @@ -640,7 +640,7 @@ def handleDataFilesAltered(self): if not self.widgetsRefreshing: self.parent.setModified() self.parent.gudrunFile.purged = False - self.container.dataFiles.dataFiles = ( + self.container.dataFiles.setFiles( self.widget.containerDataFilesList.model().stringList() ) diff --git a/gudpy/gui/widgets/slots/normalisation_slots.py b/gudpy/gui/widgets/slots/normalisation_slots.py index 235ff8ebf..494726a2e 100644 --- a/gudpy/gui/widgets/slots/normalisation_slots.py +++ b/gudpy/gui/widgets/slots/normalisation_slots.py @@ -20,10 +20,10 @@ def setNormalisation(self, normalisation): self.widgetsRefreshing = True self.widget.dataFilesList.makeModel( - self.normalisation.dataFiles.dataFiles + self.normalisation.dataFiles.dataFilenames ) self.widget.backgroundDataFilesList.makeModel( - self.normalisation.dataFilesBg.dataFiles + self.normalisation.dataFilesBg.dataFilenames ) self.widget.dataFilesList.setSelectionMode( @@ -645,7 +645,7 @@ def handleDataFilesAltered(self): if not self.widgetsRefreshing: self.parent.setModified() self.parent.gudrunFile.purged = False - self.normalisation.dataFiles.dataFiles = ( + self.normalisation.dataFiles.setFiles( self.widget.dataFilesList.model().stringList() ) @@ -659,7 +659,7 @@ def handleDataFilesBgAltered(self): if not self.widgetsRefreshing: self.parent.setModified() self.parent.gudrunFile.purged = False - self.normalisation.dataFilesBg.dataFiles = ( + self.normalisation.dataFilesBg.setFiles( self.widget.backgroundDataFilesList.model().stringList() ) diff --git a/gudpy/gui/widgets/slots/sample_background_slots.py b/gudpy/gui/widgets/slots/sample_background_slots.py index 3049e29e4..ad7ece9b0 100644 --- a/gudpy/gui/widgets/slots/sample_background_slots.py +++ b/gudpy/gui/widgets/slots/sample_background_slots.py @@ -21,7 +21,7 @@ def setSampleBackground(self, sampleBackground): # Populate data files list. self.widget.sampleBackgroundDataFilesList.makeModel( - self.sampleBackground.dataFiles.dataFiles + self.sampleBackground.dataFiles.dataFilenames ) self.widget.sampleBackgroundDataFilesList.model().dataChanged.connect( @@ -63,7 +63,7 @@ def handleDataFilesAltered(self): if not self.widgetsRefreshing: self.parent.setModified() self.parent.gudrunFile.purged = False - self.sampleBackground.dataFiles.dataFiles = ( + self.sampleBackground.dataFiles.setFiles( self.widget.sampleBackgroundDataFilesList.model().stringList() ) diff --git a/gudpy/gui/widgets/slots/sample_slots.py b/gudpy/gui/widgets/slots/sample_slots.py index e807cbd76..02277b18e 100644 --- a/gudpy/gui/widgets/slots/sample_slots.py +++ b/gudpy/gui/widgets/slots/sample_slots.py @@ -27,7 +27,7 @@ def setSample(self, sample): # Populate the data files list. self.widget.sampleDataFilesList.makeModel( - self.sample.dataFiles.dataFiles + self.sample.dataFiles.dataFilenames ) self.widget.sampleDataFilesList.model().dataChanged.connect( @@ -889,7 +889,7 @@ def handleDataFilesAltered(self): if not self.widgetsRefreshing: self.parent.setModified() self.parent.gudrunFile.purged = False - self.sample.dataFiles.dataFiles = ( + self.sample.dataFiles.setFiles( self.widget.sampleDataFilesList.model().stringList() ) @@ -1149,7 +1149,7 @@ def updateExpectedDCSLevel(self, _=None, __=None): actualDcsLevel = nthfloat(self.widget.dcsLabel.text(), 2) try: error = round( - ((actualDcsLevel - dcsLevel) / actualDcsLevel)*100, 1 + ((actualDcsLevel - dcsLevel) / actualDcsLevel)*100, 1 ) except ZeroDivisionError: error = 100. From c53816231baffb53ec9ef11e687ec2c10961842a Mon Sep 17 00:00:00 2001 From: Noella Spitz Date: Tue, 14 May 2024 11:45:22 +0100 Subject: [PATCH 02/78] Create separate parser classes --- gudpy/core/gudpy.py | 3 +- gudpy/core/gudrun_file.py | 169 +++++----------- gudpy/core/io.py | 402 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 448 insertions(+), 126 deletions(-) create mode 100644 gudpy/core/io.py diff --git a/gudpy/core/gudpy.py b/gudpy/core/gudpy.py index 5e6474ef2..2193cbe71 100644 --- a/gudpy/core/gudpy.py +++ b/gudpy/core/gudpy.py @@ -133,7 +133,7 @@ def setSaveLocation(self, projectDir: str): Path to the desired save location of project """ self.projectDir = projectDir - self.gudrunFile.filename = f"{os.path.basename(projectDir)}.yaml" + self.gudrunFile.filename = f"{os.path.basename(projectDir)}" self.gudrunFile.projectDir = projectDir self.originalGudrunFile.projectDir = projectDir self.autosaveLocation = ( @@ -517,6 +517,7 @@ def gudrun( tmp, gudrunFile.OUTPATH ) + assert (gudrunFile.path() is not None) gudrunFile.write_out(path) with subprocess.Popen( [self.BINARY_PATH, path], cwd=tmp, diff --git a/gudpy/core/gudrun_file.py b/gudpy/core/gudrun_file.py index de60d15fd..3cbc6eab1 100644 --- a/gudpy/core/gudrun_file.py +++ b/gudpy/core/gudrun_file.py @@ -2,6 +2,11 @@ import time import re from copy import deepcopy +from abc import abstractmethod +from enum import Enum +from ruamel.yaml import YAML as yaml +from ruamel.yaml import YAMLError +import os from core.utils import ( extract_nums_from_string, @@ -28,6 +33,7 @@ MergeWeights, Scales, NormalisationType, OutputUnits, Geometry ) +from core.gui_config import GUIConfig from core import utils from core import config as cfg from core.gudpy_yaml import YAML @@ -122,7 +128,6 @@ def __init__( self, projectDir=None, loadFile=None, - format=Format.YAML, config=False ): """ @@ -142,15 +147,11 @@ def __init__( If a new input file should be constructed from a config """ - self.yaml = YAML() - self.format = format - + self.yaml = self.getYamlModule() # Construct the outpath of generated input file - self.OUTPATH = "gudpy.txt" self.gudrunOutput = None self.projectDir = projectDir self.loadFile = loadFile - self.filename = None self.stream = None self.instrument = Instrument() @@ -182,22 +183,53 @@ def __deepcopy__(self, memo): setattr(result, k, deepcopy(v, memo)) return result + def setGudrunDir(self, dir): + self.instrument.GudrunInputFileDir = dir + + def convertToSample(self, container, persist=False): + + sample = container.convertToSample() + + if persist: + for i, sampleBackground in enumerate(self.sampleBackgrounds): + for sample in sampleBackground.samples: + if container in sample.containers: + sample.containers.remove(container) + break + self.sampleBackgrounds[i].append(sample) + return sample + + def determineError(self, sample): + gudPath = sample.dataFiles[0].replace( + self.instrument.dataFileType, + "gud" + ) + gudFile = GudFile( + os.path.join( + self.instrument.GudrunInputFileDir, gudPath + ) + ) + error = round( + ( + 1.0 - (gudFile.averageLevelMergedDCS / gudFile.expectedDCS) + ) * 100, 1 + ) + return error + def runSamples(self): return [ s for sb in self.sampleBackgrounds for s in sb.samples if s.runThisSample] - def path(self): - if not self.projectDir: - return None - else: - return os.path.join(self.projectDir, self.filename) - def checkNormDataFiles(self): return (len(self.normalisation.dataFiles) and len(self.normalisation.dataFilesBg)) + # ================================================================= + # TEXTFILE PARSING + # ================================================================= + def getNextToken(self): """ Pops the 'next token' from the stream and returns it. @@ -1474,119 +1506,6 @@ def __str__(self): + components ) - def save(self, path='', format=None): - if not path: - path = self.path() - - if not format: - format = self.format - if format == Format.TXT: - self.write_out( - path=f"{os.path.splitext(path)[0]}.txt", overwrite=True) - elif format == Format.YAML: - if self.path(): - filepath = os.path.join( - self.path(), f"{os.path.splitext(path)[0]}.yaml") - self.write_yaml(filepath) - - def write_yaml(self, path): - self.yaml = YAML() - self.yaml.writeYAML(self, path) - - def write_out(self, path='', overwrite=False, writeParameters=True): - """ - Writes out the string representation of the GudrunFile. - If 'overwrite' is True, then the initial file is overwritten. - Otherwise, it is written to 'gudpy_{initial filename}.txt'. - - Parameters - ---------- - overwrite : bool, optional - Overwrite the initial file? (default is False). - path : str, optional - Path to write to. - Returns - ------- - None - """ - if path: - if not overwrite: - assert (not os.path.exists(path)) - f = open( - path, "w", encoding="utf-8" - ) - elif not overwrite: - assert (not os.path.exists(os.path.join( - self.instrument.GudrunInputFileDir, - self.OUTPATH) - )) - f = open( - os.path.join( - self.instrument.GudrunInputFileDir, - self.OUTPATH - ), "w", encoding="utf-8") - else: - if not self.path(): - path = os.path.join( - self.instrument.GudrunInputFileDir, - self.OUTPATH) - f = open(path, "w", encoding="utf-8") - - if os.path.basename(f.name) == self.OUTPATH: - for sampleBackground in self.sampleBackgrounds: - sampleBackground.writeAllSamples = False - f.write(str(self)) - f.close() - - if writeParameters: - for sb in self.sampleBackgrounds: - for s in sb.samples: - if s.runThisSample: - gf = deepcopy(self) - gf.sampleBackgrounds = [deepcopy(sb)] - gf.sampleBackgrounds[0].samples = [deepcopy(s)] - gf.write_out( - path=os.path.join( - self.instrument.GudrunInputFileDir, - s.pathName(), - ), - overwrite=True, - writeParameters=False - ) - - def setGudrunDir(self, dir): - self.instrument.GudrunInputFileDir = dir - - def convertToSample(self, container, persist=False): - - sample = container.convertToSample() - - if persist: - for i, sampleBackground in enumerate(self.sampleBackgrounds): - for sample in sampleBackground.samples: - if container in sample.containers: - sample.containers.remove(container) - break - self.sampleBackgrounds[i].append(sample) - return sample - - def determineError(self, sample): - gudPath = sample.dataFiles[0].replace( - self.instrument.dataFileType, - "gud" - ) - gudFile = GudFile( - os.path.join( - self.instrument.GudrunInputFileDir, gudPath - ) - ) - error = round( - ( - 1.0 - (gudFile.averageLevelMergedDCS / gudFile.expectedDCS) - ) * 100, 1 - ) - return error - Container.getNextToken = GudrunFile.getNextToken Container.peekNextToken = GudrunFile.peekNextToken diff --git a/gudpy/core/io.py b/gudpy/core/io.py new file mode 100644 index 000000000..55acf8e4d --- /dev/null +++ b/gudpy/core/io.py @@ -0,0 +1,402 @@ +from abc import abstractmethod +from enum import Enum +from ruamel.yaml import YAML as yaml +from ruamel.yaml import YAMLError +import os +import typing as typ + +from core.composition import ( + Component, Components, Composition, WeightedComponent +) +from core.data_files import DataFile, DataFiles +from core.element import Element +from core.exception import YAMLException +from core.gui_config import GUIConfig +from core import utils +from core.instrument import Instrument +from core.beam import Beam +from core.normalisation import Normalisation +from core.sample_background import SampleBackground +from core.sample import Sample +from core.container import Container +from core.gudrun_file import GudrunFile +from core import config +import core.enums as enums + + +class YamlParser: + def __init__(self): + self.yaml = self.getYamlModule() + + @property + def path(self, format: enums.Format): + if not self.projectDir or self.projectDir: + raise RuntimeError("Save location not set.") + + if format == enums.Format.TXT: + return os.path.join(self.projectDir, f"{self.projectName}.txt") + elif format == enums.Format.YAML: + return os.path.join(self.projectDir, f"{self.projectName}.yaml") + else: + raise RuntimeError("Unsupported format.") + + def save(self, path: str = '', format: enums.Format = None): + if not path: + path = self.path() + + if not format: + format = self.format + if format == enums.Format.TXT: + self.write_out( + path=self.txtPath(), overwrite=True) + elif format == enums.Format.YAML: + if self.path(): + self.write_yaml(self.yamlPath()) + + @property + def projectName(self): + os.path.dirname(self.projectDir) + + def write_out( + self, + path: str = '', + overwrite: bool = False, + writeParameters: bool = True + ): + """ + Writes out the string representation of the GudrunFile. + If 'overwrite' is True, then the initial file is overwritten. + Otherwise, it is written to 'gudpy_{initial filename}.txt'. + + Parameters + ---------- + overwrite : bool, optional + Overwrite the initial file? (default is False). + path : str, optional + Path to write to. + Returns + ------- + None + """ + if path: + if not overwrite: + assert (not os.path.exists(path)) + f = open( + path, "w", encoding="utf-8" + ) + elif not overwrite: + assert (not os.path.exists(os.path.join( + self.instrument.GudrunInputFileDir, + self.OUTPATH) + )) + f = open( + os.path.join( + self.instrument.GudrunInputFileDir, + self.OUTPATH + ), "w", encoding="utf-8") + else: + if not self.path(): + path = os.path.join( + self.instrument.GudrunInputFileDir, + self.OUTPATH) + f = open(path, "w", encoding="utf-8") + + if os.path.basename(f.name) == self.OUTPATH: + for sampleBackground in self.sampleBackgrounds: + sampleBackground.writeAllSamples = False + f.write(str(self)) + f.close() + + if writeParameters: + for sb in self.sampleBackgrounds: + for s in sb.samples: + if s.runThisSample: + gf = deepcopy(self) + gf.sampleBackgrounds = [deepcopy(sb)] + gf.sampleBackgrounds[0].samples = [deepcopy(s)] + gf.write_out( + path=os.path.join( + self.instrument.GudrunInputFileDir, + s.pathName(), + ), + overwrite=True, + writeParameters=False + ) + +# ================================================================= +# YAML PARSING +# ================================================================= + + def getYamlModule(self) -> yaml: + yaml_ = yaml() + yaml_.preserve_quotes = True + yaml_.default_flow_style = None + yaml_.encoding = 'utf-8' + return yaml_ + + def parseYaml(self, path) -> GudrunFile: + yamldict = self.yamlToDict(path) + gudrunFile = GudrunFile() + for k, v in yamldict.items(): + if k == "GUI": + GUI = GUIConfig() + self.yamlToObject(GUI, v) + config.GUIConfig = GUI + continue + if not hasattr(gudrunFile, k): + # If attribute is not valid + print(f"Ignoring depreciated attribute '{k}'") + continue + + objType = type(gudrunFile.__dict__[k]) + parsingFunc = self.parsingFuncMap(objType) + if parsingFunc: + try: + obj = parsingFunc(v) + setattr(gudrunFile, k, obj) + except YAMLException as e: + raise YAMLException(e) + except YAMLError as e: + # Exception caused by yaml parsing library + raise YAMLException(f"Invalid YAML file: {str(e)}") + return gudrunFile + + def writeYAML(self, gudrunFile: GudrunFile, path: str) -> None: + with open(path, "wb") as fp: + outyaml = { + "Instrument": gudrunFile.instrument, + "Beam": gudrunFile.beam, + "Components": gudrunFile.components.components, + "Normalisation": gudrunFile.normalisation, + "SampleBackgrounds": gudrunFile.sampleBackgrounds, + "GUI": config.GUI + } + self.yaml.dump( + {k: self.toYaml(v) for k, v in outyaml.items()}, + fp + ) + + def parsingFuncMap(self, type: type) -> typ.Callable | None: + parsingFuncs = { + Instrument: self.parseYamlInstrument, + Beam: self.parseYamlBeam, + Normalisation: self.parseYamlNormalisation, + SampleBackground: self.parseYamlSampleBackground, + Sample: self.parseYamlSample, + Composition: self.parseYamlComposition, + Component: self.parseYamlComponent, + Components: self.parseYamlComponents, + list[Element]: self.parseYamlElements, + DataFiles: self.parseYamlDataFiles + } + if type in parsingFuncs.keys(): + return parsingFuncs[type] + else: + return None + + def yamlToDict(self, path) -> any: + # Read the input stream into our attribute. + with open(path, encoding=self.yaml.encoding) as fp: + return self.yaml.load(fp) + + def yamlToObject(self, obj: any, yamldict: any) -> any: + for k, v in yamldict.items(): + self.assignYamlToAttr(obj, k, v) + return obj + + def assignYamlToAttr(self, obj: any, key, val) -> None: + if not hasattr(obj, key): + # If attribute is not valid + print( + f"Ignoring depreciated attribute '{key}'" + f"given to '{type(obj).__name__}'" + ) + return + setattr(obj, key, type(obj.__dict__[key])(self.toBuiltin(val))) + + def parseYamlInstrument(self, yamldict: any) -> Instrument: + instrument = Instrument() + instrument = self.yamlToObject(instrument, yamldict) + return instrument + + def parseYamlBeam(self, yamldict: any) -> Normalisation: + beam = Beam() + beam = self.yamlToObject(beam, yamldict) + return beam + + def parseYamlNormalisation(self, yamldict: any) -> Normalisation: + normalisation = Normalisation() + for k, v in yamldict.items(): + if k == "composition": + composition = self.parseYamlComposition(yamldict[k]) + setattr(normalisation, k, composition) + else: + self.assignYamlToAttr(normalisation, k, v) + return normalisation + + def parseYamlSample(self, yamldict: any) -> Sample: + sample = Sample() + for k, v in yamldict.items(): + if k == "composition": + composition = self.parseYamlComposition() + setattr(sample, k, composition) + elif k == "containers": + for contyaml in yamldict[k]: + container = self.parseYamlContainer(contyaml) + sample.containers.append(container) + else: + self.assignYamlToAttr(sample, k, v) + return sample + + def parseYamlSampleBackground(self, yamldict: any) -> SampleBackground: + sampleBg = SampleBackground() + for k, v in yamldict.items(): + if k == "samples": + for sampleyaml in yamldict[k]: + sample = self.parseYamlSample(sampleyaml) + sample.name = utils.replace_unwanted_chars(sample.name) + sampleBg.samples.append(sample) + else: + self.assignYamlToAttr(sampleBg, k, v) + + def parseYamlComposition(self, yamldict: any) -> Composition: + composition = Composition() + for k, v in yamldict.items(): + if k == "elements": + elements = self.parseYamlElements(yamldict[k]) + setattr(composition, k, elements) + elif k == "weightedComponents": + weightedComponents = [] + for wc in yamldict[k]: + component = self.parseYamlComponent(wc) + ratio = wc["ratio"] + try: + weightedComponents.append( + WeightedComponent( + component, float(ratio)) + ) + except ValueError: + raise YAMLException( + "Invalid ratio given to Weighted Component") + setattr(composition, k, weightedComponents) + else: + self.assignYamlToAttr(composition, k, v) + return composition + + def parseYamlContainer(self, yamldict: any) -> Container: + container = Container() + for k, v in yamldict.items(): + if k == "composition": + composition = self.parseYamlComposition() + setattr(container, k, composition) + else: + self.assignYamlToAttr(container, k, v) + return container + + def parseYamlDataFiles(self, yamldict: any) -> DataFiles: + dataFiles = DataFiles() + for df in yamldict["_dataFiles"]: + dataFile = DataFile(df["filename"]) + dataFile = self.yamlToObject(dataFile, df) + dataFiles.dataFiles.append(dataFile) + return dataFiles + + def parseYamlComponent(self, yamldict: any) -> Component: + if ( + "component" not in yamldict + or "ratio" not in yamldict + ): + raise YAMLException( + "Weighted Component expects 'component' and" + + " 'ratio' to be provided") + component = Component() + self.yamlToObject( + component, yamldict["component"] + ) + return component + + def parseYamlComponents(self, yamllist: list[dict]) -> Components: + components = [] + for c in yamllist: + components.append(self.parseYamlComponent(c)) + return components + + def parseYamlElements(self, yamllist: list[str]) -> list[Element]: + elements = [] + for idx, element in enumerate(yamllist): + # Ensuring correct arguements are provided + if ( + "atomicSymbol" not in element + or "massNo" not in element + or "abundance" not in element + ): + raise YAMLException( + "Insufficient arguments given to element" + + f" {idx + 1}. Expects 'atomicSymbol', 'massNo'" + + " and 'abundance'" + ) + + # Setting element properties + try: + element_ = Element( + **{ + "atomicSymbol": element["atomicSymbol"], + "massNo": float(element["massNo"]), + "abundance": float(element["abundance"]) + } + ) + elements.append(element_) + except ValueError as e: + raise YAMLException( + f"Invalid number given to element {idx + 1}:" + f"{e}") + return elements + + @abstractmethod + def toYaml(self, var: any): + if var.__class__.__module__ == "ruamel.yaml.scalarfloat": + return float(var) + if var.__class__.__module__ == "builtins": + if isinstance(var, (list, tuple)): + return type(var)([self.toYaml(v) for v in var]) + else: + return var + elif isinstance(var, Enum): + return type(var)(var.value).name + elif isinstance(var, ( + Instrument, Beam, Components, Normalisation, + SampleBackground, Sample, Container, WeightedComponent, + Component, Composition, Element, DataFiles, DataFile, GUIConfig + )): + return { + k: self.toYaml(v) + for k, v in var.__dict__.items() + if k not in var.yamlignore + } + + @abstractmethod + def toBuiltin(self, yamlvar: any): + if yamlvar is None: + return None + if isinstance(yamlvar, (list, tuple)): + return [self.toBuiltin(v) for v in yamlvar] + elif (yamlvar.__class__.__module__ == + "ruamel.yaml.comments.CommentedMap"): + dict = {} + for k, v in yamlvar.items(): + dict[k] = v + return dict + elif yamlvar.__class__.__module__ == "builtins": + return yamlvar + elif yamlvar.__class__.__module__ == "ruamel.yaml.scalarfloat": + return float(yamlvar) + elif yamlvar.__class__.__module__ == "ruamel.yaml.scalarstring": + return str(yamlvar) + +# ================================================================= +# TXT PARSING +# ================================================================= + + +class TxtParser: + def __init__(self): From 2f6f6a30097fa17155256750afd4cf892a1dca84 Mon Sep 17 00:00:00 2001 From: Noella Spitz Date: Tue, 14 May 2024 16:39:49 +0100 Subject: [PATCH 03/78] Create dedicated iO handling --- gudpy/core/gudpy.py | 55 +- gudpy/core/gudrun_file.py | 1303 +------------------------- gudpy/core/io/__init__.py | 0 gudpy/core/{io.py => io/gudpy_io.py} | 292 +++--- gudpy/core/io/txt_parser.py | 1181 +++++++++++++++++++++++ gudpy/core/purge_file.py | 1 - 6 files changed, 1367 insertions(+), 1465 deletions(-) create mode 100644 gudpy/core/io/__init__.py rename gudpy/core/{io.py => io/gudpy_io.py} (56%) create mode 100644 gudpy/core/io/txt_parser.py diff --git a/gudpy/core/gudpy.py b/gudpy/core/gudpy.py index 2193cbe71..957ac7981 100644 --- a/gudpy/core/gudpy.py +++ b/gudpy/core/gudpy.py @@ -16,6 +16,7 @@ import core.output_file_handler as handlers from core.file_library import GudPyFileLibrary from core import data_files +from core.io import txt_parser, gudpy_io SUFFIX = ".exe" if os.name == "nt" else "" @@ -25,8 +26,8 @@ def __init__( self ): self.originalGudrunFile: GudrunFile = None - self.gudrunFile: GudrunFile = None - self.purgeFile = None + self._gudrunFile: GudrunFile = None + self.io = gudpy_io.GudpyIO() self.purge: Purge = None self.gudrun: Gudrun = None @@ -39,10 +40,17 @@ def __init__( self.projectDir = "" self.autosaveLocation = "" - def loadFromFile( + @property + def gudrunFile(self): + return self._gudrunFile + + @gudrunFile.setter + def gudrunFile(self, gudrunFile: GudrunFile): + self._gudrunFile = gudrunFile + + def loadFromGudrunFile( self, loadFile: str, - format: enums.Format, config: bool = False ): """Loads GudPy from an input file @@ -64,18 +72,12 @@ def loadFromFile( if not os.path.exists(loadFile): raise FileNotFoundError("Input file does not exist.") - self.gudrunFile = GudrunFile( - loadFile=loadFile, - format=format, - config=config - ) + self.gudrunFileParser = txt_parser.GudrunFileParser() + self.gudrunFile = self.gudrunFileParser.parse(loadFile, config) self.originalGudrunFile = copy.deepcopy(self.gudrunFile) self.originalGudrunFile.filename = "original" - self.projectDir == "" - self.autosaveLocation = "" - def loadFromProject(self, projectDir: str): """Loads GudPy from a project directory @@ -90,29 +92,10 @@ def loadFromProject(self, projectDir: str): Raised if there is no YAML input file in the project directory """ - loadFile = "" - - if os.path.exists(os.path.join( - projectDir, - f"{os.path.basename(projectDir)}.yaml" - )): - # If default file exists - loadFile = os.path.join( - projectDir, - f"{os.path.basename(projectDir)}.yaml" - ) - else: - # Try to find yaml files - for f in os.listdir(projectDir): - if os.path.splitext(f)[1] == ".yaml": - # If file is yaml - loadFile = os.path.join(projectDir, f) - if not loadFile: - raise FileNotFoundError( - "Could not find GudPy input file within the project") + if not os.path.exists(projectDir): + raise FileNotFoundError("Directory does not exist.") - self.loadFromFile(loadFile=loadFile, format=enums.Format.YAML) - self.setSaveLocation(projectDir) + self.gudrunFile = self.io.importProject(projectDir) def checkSaveLocation(self): """Checks if user has set the save location @@ -231,8 +214,8 @@ def runPurge(self): """ self.prepareRun() self.purge = Purge() - self.purgeFile = PurgeFile(self.gudrunFile) - exitcode = self.purge.purge(self.purgeFile) + purgeFile = PurgeFile(self.gudrunFile) + exitcode = self.purge.purge(purgeFile) self.gudrunFile.save() if exitcode: raise exc.PurgeException( diff --git a/gudpy/core/gudrun_file.py b/gudpy/core/gudrun_file.py index 3cbc6eab1..963e540f3 100644 --- a/gudpy/core/gudrun_file.py +++ b/gudpy/core/gudrun_file.py @@ -1,43 +1,13 @@ import os import time -import re from copy import deepcopy -from abc import abstractmethod -from enum import Enum -from ruamel.yaml import YAML as yaml -from ruamel.yaml import YAMLError -import os -from core.utils import ( - extract_nums_from_string, - firstword, boolifyNum, - extract_ints_from_string, - extract_floats_from_string, - firstNFloats, - firstNInts, - nthfloat, - nthint, - uniquifyName -) from core.instrument import Instrument from core.beam import Beam from core.normalisation import Normalisation -from core.sample import Sample from core.sample_background import SampleBackground -from core.container import Container -from core.composition import Component, Components, Composition -from core.element import Element -from core.data_files import DataFiles -from core.enums import ( - CrossSectionSource, Format, Instruments, FTModes, UnitsOfDensity, - MergeWeights, Scales, NormalisationType, OutputUnits, - Geometry -) -from core.gui_config import GUIConfig -from core import utils +from core.composition import Components from core import config as cfg -from core.gudpy_yaml import YAML -from core.exception import ParserException, YAMLException from core.gud_file import GudFile SUFFIX = ".exe" if os.name == "nt" else "" @@ -128,7 +98,19 @@ def __init__( self, projectDir=None, loadFile=None, - config=False + config=False, + params: tuple[ + Instrument, + Normalisation, + list[SampleBackground], + Components + ] = ( + Instrument(), + Beam(), + Normalisation(), + [], + Components() + ) ): """ Constructs all the necessary attributes for the GudrunFile object. @@ -146,33 +128,13 @@ def __init__( config : bool If a new input file should be constructed from a config """ + inst, norm, beam, sbg, comp = params - self.yaml = self.getYamlModule() - # Construct the outpath of generated input file - self.gudrunOutput = None - self.projectDir = projectDir - self.loadFile = loadFile - self.stream = None - - self.instrument = Instrument() - self.normalisation = Normalisation() - self.beam = Beam() - self.sampleBackgrounds = [] - self.components = Components() - - if not projectDir and not loadFile: - raise RuntimeError( - "GudrunFile needs to be initialised with either" - " a project directory or load file specified" - ) - - if loadFile: - self.setGudrunDir(os.path.dirname(loadFile)) - - if not config: - self.setGudrunDir(os.path.dirname(loadFile)) - - self.parse(loadFile, config=config) + self.instrument = inst + self.normalisation = norm + self.beam = beam + self.sampleBackgrounds = sbg + self.components = comp def __deepcopy__(self, memo): result = self.__class__.__new__(self.__class__) @@ -226,1225 +188,6 @@ def checkNormDataFiles(self): return (len(self.normalisation.dataFiles) and len(self.normalisation.dataFilesBg)) - # ================================================================= - # TEXTFILE PARSING - # ================================================================= - - def getNextToken(self): - """ - Pops the 'next token' from the stream and returns it. - Essentially removes the first line in the stream and returns it. - - Parameters - ---------- - None - Returns - ------- - str | None - """ - return self.stream.pop(0) if self.stream else None - - def peekNextToken(self): - """ - Returns the next token in the input stream, without removing it. - - Parameters - ---------- - None - Returns - ------- - str | None - """ - return self.stream[0] if self.stream else None - - def consumeTokens(self, n): - """ - Consume n tokens from the input stream. - - Parameters - ---------- - None - Returns - ------- - None - """ - for _ in range(n): - self.getNextToken() - - def consumeUpToDelim(self, delim): - """ - Consume tokens iteratively, until a delimiter is reached. - - Parameters - ---------- - None - Returns - ------- - None - """ - line = self.getNextToken() - while line[0] != delim: - line = self.getNextToken() - - def consumeWhitespace(self): - """ - Consume tokens iteratively, while they are whitespace. - - Parameters - ---------- - None - Returns - ------- - None - """ - line = self.peekNextToken() - if line and line.isspace(): - self.getNextToken() - line = self.peekNextToken() - - def parseInstrument(self): - """ - Intialises an Instrument object and assigns it to the - instrument attribute. - Parses the attributes of the Instrument from the input stream. - Raises a ParserException if any mandatory attributes are missing. - - - Parameters - ---------- - None - Returns - ------- - None - """ - try: - self.consumeWhitespace() - - # For string attributes, - # we simply extract the firstword in the line. - self.instrument.name = Instruments[firstword(self.getNextToken())] - self.consumeTokens(1) - self.instrument.dataFileDir = os.path.abspath( - firstword(self.getNextToken())) + os.path.sep - self.instrument.dataFileType = firstword(self.getNextToken()) - self.instrument.detectorCalibrationFileName = ( - firstword(self.getNextToken()) - ) - - # For single integer attributes, - # we extract the zeroth int from the line. - self.instrument.columnNoPhiVals = nthint(self.getNextToken(), 0) - self.instrument.groupFileName = firstword(self.getNextToken()) - self.instrument.deadtimeConstantsFileName = ( - firstword(self.getNextToken()) - ) - - # For N integer attributes, - # we extract the first N integers from the line. - self.instrument.spectrumNumbersForIncidentBeamMonitor = ( - extract_ints_from_string(self.getNextToken()) - ) - - # For integer pair attributes, - # we extract the first 2 integers from the line. - self.instrument.wavelengthRangeForMonitorNormalisation = ( - firstNFloats(self.getNextToken(), 2) - ) - - if all( - self.instrument.wavelengthRangeForMonitorNormalisation - ) == 0.0: - self.instrument.wavelengthRangeForMonitorNormalisation = [ - 0, 0 - ] - - self.instrument.spectrumNumbersForTransmissionMonitor = ( - extract_ints_from_string(self.getNextToken()) - ) - - # For single float attributes, - # we extract the zeroth float from the line. - self.instrument.incidentMonitorQuietCountConst = ( - nthfloat(self.getNextToken(), 0) - ) - self.instrument.transmissionMonitorQuietCountConst = ( - nthfloat(self.getNextToken(), 0) - ) - - self.instrument.channelNosSpikeAnalysis = ( - firstNInts(self.getNextToken(), 2) - ) - self.instrument.spikeAnalysisAcceptanceFactor = ( - nthfloat(self.getNextToken(), 0) - ) - - # Extract wavelength range - # Which consists of the first 3 floats - # (min, max, step) in the line. - wavelengthRange = firstNFloats(self.getNextToken(), 3) - self.instrument.wavelengthMin = wavelengthRange[0] - self.instrument.wavelengthMax = wavelengthRange[1] - self.instrument.wavelengthStep = wavelengthRange[2] - - self.instrument.NoSmoothsOnMonitor = nthint(self.getNextToken(), 0) - - # Extract X range - # Which consists of the first 3 floats - # (min, max, step) in the line. - XRange = firstNFloats(self.getNextToken(), 3) - - self.instrument.XMin = XRange[0] - self.instrument.XMax = XRange[1] - self.instrument.XStep = XRange[2] - - # Extract the grouping parameter panel. - # Each row in the panel consists of the first 4 ints - # (Group, XMin, XMax, Background Factor) in the line. - # If the marker line is encountered, - # then the panel has been parsed. - - line = self.getNextToken() - while "to end input of specified values" not in line: - group = nthint(line, 0) - xMin = nthfloat(line, 1) - xMax = nthfloat(line, 2) - backgroundFactor = nthfloat(line, 3) - self.instrument.groupingParameterPanel.append( - [group, xMin, xMax, backgroundFactor] - ) - line = self.getNextToken() - - self.instrument.groupsAcceptanceFactor = ( - nthfloat(self.getNextToken(), 0) - ) - self.instrument.mergePower = nthint(self.getNextToken(), 0) - - # For boolean attributes, we convert the first - # integer in the line to its boolean value. - self.instrument.subSingleAtomScattering = ( - boolifyNum(nthint(self.getNextToken(), 0)) - ) - - # For enumerated attributes, where the value of the attribute is - # the first integer in the line, and we must get the member, - # we do this: Enum[Enum(value).name] - self.instrument.mergeWeights = ( - MergeWeights[MergeWeights(nthint(self.getNextToken(), 0)).name] - ) - self.instrument.incidentFlightPath = ( - nthfloat(self.getNextToken(), 0) - ) - self.instrument.spectrumNumberForOutputDiagnosticFiles = ( - nthint(self.getNextToken(), 0) - ) - - self.instrument.neutronScatteringParametersFile = ( - firstword(self.getNextToken()) - - ) - self.instrument.scaleSelection = ( - Scales[Scales(nthint(self.getNextToken(), 0)).name] - ) - self.instrument.subWavelengthBinnedData = ( - boolifyNum(nthint(self.getNextToken(), 0)) - ) - self.consumeTokens(2) - self.instrument.logarithmicStepSize = ( - nthfloat(self.getNextToken(), 0) - ) - self.instrument.hardGroupEdges = ( - boolifyNum(nthint(self.getNextToken(), 0)) - ) - - # If NeXus files are being used, then we expect a NeXus definition - # file to be present, and extract it. - if ( - self.instrument.dataFileType == "NXS" - or self.instrument.dataFileType == "nxs" - ): - self.instrument.nxsDefinitionFile = ( - firstword(self.getNextToken()) - ) - - if self.config: - self.instrument.goodDetectorThreshold = nthint( - self.getNextToken(), 0 - ) - - # Consume whitespace and the closing brace. - self.consumeUpToDelim("}") - - # Resolve the paths, to make them relative. - # First construct the regular expression to match against. - pattern = re.compile(r"StartupFiles\S*") - - match = re.search( - pattern, - self.instrument.detectorCalibrationFileName - ) - - if match: - self.instrument.detectorCalibrationFileName = match.group() - - match = re.search( - pattern, - self.instrument.groupFileName - ) - - if match: - self.instrument.groupFileName = match.group() - - match = re.search( - pattern, - self.instrument.deadtimeConstantsFileName - ) - - if match: - self.instrument.deadtimeConstantsFileName = match.group() - - match = re.search( - pattern, - self.instrument.neutronScatteringParametersFile - ) - - if match: - self.instrument.neutronScatteringParametersFile = match.group() - - match = re.search( - pattern, - self.instrument.neutronScatteringParametersFile - ) - - if match: - self.instrument.neutronScatteringParametersFile = match.group() - - except Exception as e: - raise ParserException( - "Whilst parsing Instrument, an exception occured." - " The input file is most likely of an incorrect format, " - "and some attributes were missing." - f"{str(e)}" - ) from e - - def parseBeam(self): - """ - Intialises a Beam object and assigns it to the - beam attribute. - Parses the attributes of the Beam from the input stream. - Raises a ParserException if any mandatory attributes are missing. - - - Parameters - ---------- - None - Returns - ------- - None - """ - - try: - # Initialise beam attribute to a new instance of Beam. - self.beam = Beam() - - self.consumeWhitespace() - - # For enumerated attributes, - # where the member name of the attribute is - # the first 'word' in the line, and we must get the member, - # we do this: Enum[memberName]. - self.beam.sampleGeometry = Geometry[firstword(self.getNextToken())] - - # Set the global geometry. - cfg.geometry = self.beam.sampleGeometry - - # Ignore the number of beam values. - self.consumeTokens(1) - - # For N float attributes, - # we extract the first N floats from the line. - self.beam.beamProfileValues = ( - extract_floats_from_string(self.getNextToken()) - ) - - # For single float attributes, - # we extract the zeroth float from the line. - range = self.getNextToken() - self.beam.stepSizeAbsorption = nthfloat(range, 0) - self.beam.stepSizeMS = nthfloat(range, 1) - self.beam.noSlices = nthint(range, 2) - self.beam.angularStepForCorrections = ( - nthint(self.getNextToken(), 0) - ) - - # Extract the incident beam edges - # relative to the centroid of the sample. - incidentBeamEdges = self.getNextToken() - self.beam.incidentBeamLeftEdge = nthfloat(incidentBeamEdges, 0) - self.beam.incidentBeamRightEdge = nthfloat(incidentBeamEdges, 1) - self.beam.incidentBeamBottomEdge = nthfloat(incidentBeamEdges, 2) - self.beam.incidentBeamTopEdge = nthfloat(incidentBeamEdges, 3) - - # Extract the scattered beam edges - # relative to the centroid of the sample. - scatteredBeamEdges = self.getNextToken() - self.beam.scatteredBeamLeftEdge = nthfloat(scatteredBeamEdges, 0) - self.beam.scatteredBeamRightEdge = nthfloat(scatteredBeamEdges, 1) - self.beam.scatteredBeamBottomEdge = nthfloat(scatteredBeamEdges, 2) - self.beam.scatteredBeamTopEdge = nthfloat(scatteredBeamEdges, 3) - - # For string attributes, - # we simply extract the firstword in the line. - self.beam.filenameIncidentBeamSpectrumParams = ( - firstword(self.getNextToken()) - ) - - # Now match it against a pattern, - # to resolve the path to be relative. - pattern = re.compile(r"StartupFiles\S*") - - match = re.search( - pattern, - self.beam.filenameIncidentBeamSpectrumParams - ) - - if match: - self.beam.filenameIncidentBeamSpectrumParams = match.group() - - self.beam.overallBackgroundFactor = ( - nthfloat(self.getNextToken(), 0) - ) - self.beam.sampleDependantBackgroundFactor = ( - nthfloat(self.getNextToken(), 0) - ) - self.beam.shieldingAttenuationCoefficient = ( - nthfloat(self.getNextToken(), 0) - ) - - # Consume whitespace and the closing brace. - self.consumeUpToDelim("}") - - except Exception as e: - raise ParserException( - "Whilst parsing Beam, an exception occured." - " The input file is most likely of an incorrect format, " - "and some attributes were missing." - ) from e - - def parseNormalisation(self): - """ - Intialises a Normalisation object and assigns it to the - normalisation attribute. - Parses the attributes of the Normalisation from the input stream. - Raises a ParserException if any mandatory attributes are missing. - - - Parameters - ---------- - None - Returns - ------- - None - """ - - try: - # Initialise normalisation attribute - # to a new instance of Normalisation. - self.normalisation = Normalisation() - - self.consumeWhitespace() - - # The number of files and period number are both stored - # on the same line. - # So we extract the 0th integer for the number of files, - # and the 1st integer for the period number. - dataFileInfo = self.getNextToken() - numberOfFiles = nthint(dataFileInfo, 0) - self.normalisation.periodNumber = nthint(dataFileInfo, 1) - - # Extract data files - dataFiles = [] - for _ in range(numberOfFiles): - dataFiles.append(firstword(self.getNextToken())) - # Sorts list so that it is in ascending order - dataFiles.sort() - - # Create a DataFiles object from the dataFiles list constructed. - self.normalisation.dataFiles = ( - DataFiles(dataFiles, "NORMALISATION") - ) - - # The number of background files and - # background period number are both stored - # on the same line. - # So we extract the 0th integer for the number of background files, - # and the 1st integer for the background riod number. - dataFileInfoBg = self.getNextToken() - numberOfFilesBg = nthint(dataFileInfoBg, 0) - self.normalisation.periodNumberBg = nthint(dataFileInfoBg, 1) - - # Extract background data files - dataFilesBg = [] - for j in range(numberOfFilesBg): - dataFilesBg.append(firstword(self.getNextToken())) - - # Sorts list so that it is in ascending order - dataFilesBg.sort() - - # Create a DataFiles object from the dataFiles list constructed. - self.normalisation.dataFilesBg = ( - DataFiles(dataFilesBg, "NORMALISATION BACKGROUND") - ) - - # For boolean attributes, we convert the first - # integer in the line to its boolean value. - self.normalisation.forceCalculationOfCorrections = ( - boolifyNum(nthint(self.getNextToken(), 0)) - ) - - # Construct composition - composition = [] - line = self.getNextToken() - # Extract the composition. - # Each element in the composition consists of the first 'word', - # integer at the second position, and float at the third position, - # (Atomic Symbol, MassNo, Abundance) in the line. - # If the marker line is encountered, - # then the panel has been parsed. - while "end of composition input" not in line: - atomicSymbol = firstword(line) - massNo = nthfloat(line, 1) - abundance = nthfloat(line, 2) - - # Create an Element object and append to the composition list. - composition.append( - Element(atomicSymbol, massNo, abundance) - ) - line = self.getNextToken() - - # Create a Composition object from the dataFiles list constructed. - self.normalisation.composition = ( - Composition("Normalisation", elements=composition) - ) - - # For enumerated attributes, - # where the member name of the attribute is - # the first 'word' in the line, and we must get the member, - # we do this: Enum[memberName]. - self.normalisation.geometry = ( - Geometry[firstword(self.getNextToken())] - ) - - # Is the geometry FLATPLATE? - if ( - ( - self.normalisation.geometry == Geometry.SameAsBeam - and cfg.geometry == Geometry.FLATPLATE - ) - or self.normalisation.geometry == Geometry.FLATPLATE): - # If is is FLATPLATE, then extract the upstream and downstream - # thickness, the angle of rotation and sample width. - thickness = self.getNextToken() - self.normalisation.upstreamThickness = nthfloat(thickness, 0) - self.normalisation.downstreamThickness = ( - nthfloat(thickness, 1) - ) - geometryInfo = self.getNextToken() - self.normalisation.angleOfRotation = nthfloat(geometryInfo, 0) - self.normalisation.sampleWidth = nthfloat(geometryInfo, 1) - else: - - # Otherwise, it is CYLINDRICAL, - # then extract the inner and outer - # radii and the sample height. - radii = self.getNextToken() - self.normalisation.innerRadius = nthfloat(radii, 0) - self.normalisation.outerRadius = nthfloat(radii, 1) - self.normalisation.sampleHeight = ( - nthfloat(self.getNextToken(), 0) - ) - - # Extract the density. - density = nthfloat(self.getNextToken(), 0) - - # Take the absolute value of the density - since it could be -ve. - self.normalisation.density = abs(density) - - # Decide on the units of density. - # -ve density means it is atomic (atoms/A^3) - # +ve means it is chemical (gm/cm^3) - self.normalisation.densityUnits = ( - UnitsOfDensity.ATOMIC if - density < 0 - else UnitsOfDensity.CHEMICAL - ) - - self.normalisation.tempForNormalisationPC = ( - nthfloat(self.getNextToken(), 0) - ) - crossSectionSource = firstword(self.getNextToken()) - if ( - crossSectionSource == "TABLES" - or crossSectionSource == "TRANSMISSION" - ): - self.normalisation.totalCrossSectionSource = ( - CrossSectionSource[crossSectionSource] - ) - else: - self.normalisation.totalCrossSectionSource = ( - CrossSectionSource.FILE - ) - self.normalisation.crossSectionFilename = crossSectionSource - - self.normalisation.normalisationDifferentialCrossSectionFile = ( - firstword(self.getNextToken()) - ) - - self.normalisation.lowerLimitSmoothedNormalisation = ( - nthfloat(self.getNextToken(), 0) - ) - self.normalisation.normalisationDegreeSmoothing = ( - nthfloat(self.getNextToken(), 0) - ) - self.normalisation.minNormalisationSignalBR = ( - nthfloat(self.getNextToken(), 0) - ) - - # Consume whitespace and the closing brace. - self.consumeUpToDelim("}") - - # Resolve to relative. - pattern = re.compile(r"StartupFiles\S*") - - match = re.search( - pattern, - self.normalisation. - normalisationDifferentialCrossSectionFile - ) - - if match: - ( - self.normalisation. - normalisationDifferentialCrossSectionFile - ) = match.group() - - except Exception as e: - raise ParserException( - "Whilst parsing Normalisation, an exception occured." - " The input file is most likely of an incorrect format, " - "and some attributes were missing." - ) from e - - def parseSampleBackground(self): - """ - Intialises a SampleBackground object. - Parses the attributes of the SampleBackground from the input stream. - Raises a ParserException if any mandatory attributes are missing. - Returns the parsed object. - - Parameters - ---------- - None - Returns - ------- - sampleBackground : SampleBackground - The SampleBackground that was parsed from the input lines. - """ - - try: - sampleBackground = SampleBackground() - line = self.peekNextToken() - if "SAMPLE BACKGROUND" in line and "{" in line: - self.consumeTokens(1) - self.consumeWhitespace() - dataFileInfo = self.getNextToken() - numberOfFiles = nthint(dataFileInfo, 0) - sampleBackground.periodNumber = nthint(dataFileInfo, 1) - - dataFiles = [] - for _ in range(numberOfFiles): - dataFiles.append(firstword(self.getNextToken())) - sampleBackground.dataFiles = ( - DataFiles(dataFiles, "SAMPLE BACKGROUND") - ) - - # Consume whitespace and the closing brace. - self.consumeUpToDelim("}") - - return sampleBackground - except Exception as e: - raise ParserException( - "Whilst parsing Sample Background, an exception occured." - " The input file is most likely of an incorrect format, " - "and some attributes were missing." - ) from e - - def parseSample(self): - """ - Intialises a Sample object. - Parses the attributes of the Sample from the input stream. - Raises a ParserException if any mandatory attributes are missing. - Returns the parsed object. - - Parameters - ---------- - None - Returns - ------- - sample : Sample - The Sample that was parsed from the input lines. - """ - - try: - # Create a new instance of Sample. - sample = Sample() - - # Extract the sample name, and then discard whitespace lines. - sample.name = ( - str(self.getNextToken()[:-2]).strip() - .replace("SAMPLE", "").strip() - ) - sample.name = utils.replace_unwanted_chars(sample.name) - self.consumeWhitespace() - # The number of files and period number are both stored - # on the same line. - # So we extract the 0th integer for the number of files, - # and the 1st integer for the period number. - dataFileInfo = self.getNextToken() - numberOfFiles = nthint(dataFileInfo, 0) - sample.periodNumber = nthint(dataFileInfo, 1) - - # Extract data files - dataFiles = [] - for _ in range(numberOfFiles): - dataFiles.append(firstword(self.getNextToken())) - # Create a DataFiles object from the dataFiles list constructed. - sample.dataFiles = DataFiles(dataFiles, sample.name) - - # For boolean attributes, we convert the first - # integer in the line to its boolean value. - sample.forceCalculationOfCorrections = ( - boolifyNum(nthint(self.getNextToken(), 0)) - ) - - # Construct composition - composition = [] - line = self.getNextToken() - - # Extract the composition. - # Each element in the composition consists of the first 'word', - # integer at the second position, and float t the first position, - # (Atomic Symbol, MassNo, Abundance) in the line. - # If the marker line is encountered, - # then the panel has been parsed. - while "end of composition input" not in line: - - atomicSymbol = firstword(line) - massNo = nthfloat(line, 1) - abundance = nthfloat(line, 2) - - # Create an Element object and append to the composition list. - composition.append(Element(atomicSymbol, massNo, abundance)) - line = self.getNextToken() - - # Create a Composition object from the dataFiles list constructed. - sample.composition = Composition("Sample", elements=composition) - - # For enumerated attributes, - # where the member name of the attribute is - # the first 'word' in the line, and we must get the member, - # we do this: Enum[memberName]. - sample.geometry = Geometry[firstword(self.getNextToken())] - - # Is the geometry FLATPLATE? - if ( - ( - sample.geometry == Geometry.SameAsBeam - and cfg.geometry == Geometry.FLATPLATE - ) - or sample.geometry == Geometry.FLATPLATE): - # If is is FLATPLATE, then extract the upstream and downstream - # thickness, the angle of rotation and sample width. - thickness = self.getNextToken() - sample.upstreamThickness = nthfloat(thickness, 0) - sample.downstreamThickness = nthfloat(thickness, 1) - - geometryInfo = self.getNextToken() - sample.angleOfRotation = nthfloat(geometryInfo, 0) - sample.sampleWidth = nthfloat(geometryInfo, 1) - else: - - # Otherwise, it is CYLINDRICAL, - # then extract the inner and outer - # radii and the sample height. - radii = self.getNextToken() - sample.innerRadius = nthfloat(radii, 0) - sample.outerRadius = nthfloat(radii, 1) - sample.sampleHeight = nthfloat(self.getNextToken(), 0) - - # Extract the density. - density = nthfloat(self.getNextToken(), 0) - - # Decide on the units of density. - # -ve density means it is atomic (atoms/A^3) - # +ve means it is chemical (gm/cm^3) - sample.density = abs(density) - sample.densityUnits = ( - UnitsOfDensity.ATOMIC if - density < 0 - else UnitsOfDensity.CHEMICAL - ) - sample.tempForNormalisationPC = nthfloat(self.getNextToken(), 0) - crossSectionSource = firstword(self.getNextToken()) - if ( - crossSectionSource == "TABLES" - or crossSectionSource == "TRANSMISSION" - ): - sample.totalCrossSectionSource = ( - CrossSectionSource[crossSectionSource] - ) - else: - sample.totalCrossSectionSource = CrossSectionSource.FILE - sample.crossSectionFilename = crossSectionSource - sample.sampleTweakFactor = nthfloat(self.getNextToken(), 0) - - topHatW = nthfloat(self.getNextToken(), 0) - if topHatW == 0: - sample.topHatW = 0 - sample.FTMode = FTModes.NO_FT - elif topHatW < 0: - sample.topHatW = abs(topHatW) - sample.FTMode = FTModes.SUB_AVERAGE - else: - sample.topHatW = topHatW - sample.FTMode = FTModes.ABSOLUTE - - sample.minRadFT = nthfloat(self.getNextToken(), 0) - sample.grBroadening = nthfloat(self.getNextToken(), 0) - - # Extract the resonance values. - # Each row consists of the first 2 floats. - # (minWavelength, maxWavelength) in the line. - # If the marker line is encountered, - # then the values has been parsed. - line = self.getNextToken() - while ( - "to finish specifying wavelength range of resonance" - not in line - ): - sample.resonanceValues.append( - extract_floats_from_string(line) - ) - line = self.getNextToken() - - # Extract the exponential values. - # Each row consists of the first 3 numbers. - # (Amplitude, Decay, N) in the line. - # If the marker line is encountered, - # then the values has been parsed. - line = self.getNextToken() - if "to specify end of exponential parameter input" not in line: - sample.exponentialValues = [] - while "to specify end of exponential parameter input" not in line: - sample.exponentialValues.append( - extract_nums_from_string(line) - ) - - line = self.getNextToken() - - sample.normalisationCorrectionFactor = ( - nthfloat(self.getNextToken(), 0) - ) - sample.fileSelfScattering = firstword(self.getNextToken()) - sample.normaliseTo = ( - NormalisationType[ - NormalisationType(nthint(self.getNextToken(), 0)).name - ] - ) - sample.maxRadFT = nthfloat(self.getNextToken(), 0) - sample.outputUnits = ( - OutputUnits[OutputUnits(nthint(self.getNextToken(), 0)).name] - ) - sample.powerForBroadening = nthfloat(self.getNextToken(), 0) - sample.stepSize = nthfloat(self.getNextToken(), 0) - sample.runThisSample = boolifyNum(nthint(self.getNextToken(), 0)) - environmentValues = self.getNextToken() - sample.scatteringFraction = nthfloat(environmentValues, 0) - sample.attenuationCoefficient = nthfloat(environmentValues, 1) - - # Consume whitespace and the closing brace. - self.consumeUpToDelim("}") - - return sample - - except Exception as e: - raise ParserException( - "Whilst parsing Sample, an exception occured." - " The input file is most likely of an incorrect format, " - "and some attributes were missing." - ) from e - - def parseContainer(self): - """ - Intialises a Container object. - Parses the attributes of the Container from the input stream. - Raises a ParserException if any mandatory attributes are missing. - Returns the parsed object. - - Parameters - ---------- - None - Returns - ------- - container : Container - The Container that was parsed from the input lines. - """ - - try: - # Create a new instance of Container. - container = Container() - - # Extract the name from the lines, - # and then discard the unnecessary lines. - container.name = ( - str(self.getNextToken()[:-2]).strip() - .replace("CONTAINER", "").strip() - ) - self.consumeWhitespace() - - # The number of files and period number are both stored - # on the same line. - # So we extract the 0th integer for the number of files, - # and the 1st integer for the period number. - dataFileInfo = self.getNextToken() - numberOfFiles = nthint(dataFileInfo, 0) - container.periodNumber = nthint(dataFileInfo, 1) - - # Extract data files - dataFiles = [] - for _ in range(numberOfFiles): - dataFiles.append(firstword(self.getNextToken())) - - # Create a DataFiles object from the dataFiles list constructed. - container.dataFiles = DataFiles(dataFiles, container.name) - - # Construct composition - composition = [] - line = self.getNextToken() - # Extract the composition. - # Each element in the composition consists of the first 'word', - # integer at the second position, and float t the first position, - # (Atomic Symbol, MassNo, Abundance) in the line. - # If the marker line is encountered, - # then the panel has been parsed. - while "end of composition input" not in line: - - atomicSymbol = firstword(line) - massNo = nthfloat(line, 1) - abundance = nthfloat(line, 2) - - # Create an Element object and append to the composition list. - composition.append(Element(atomicSymbol, massNo, abundance)) - line = self.getNextToken() - # Create a Composition object from the dataFiles list constructed. - container.composition = Composition( - "Container", - elements=composition - ) - - # For enumerated attributes, - # where the member name of the attribute is - # the first 'word' in the line, and we must get the member, - # we do this: Enum[memberName]. - container.geometry = Geometry[firstword(self.getNextToken())] - - # Is the geometry FLATPLATE? - if ( - ( - container.geometry == Geometry.SameAsBeam - and cfg.geometry == Geometry.FLATPLATE - ) - or container.geometry == Geometry.FLATPLATE): - # If is is FLATPLATE, then extract the upstream and downstream - # thickness, the angle of rotation and sample width. - thickness = self.getNextToken() - container.upstreamThickness = nthfloat(thickness, 0) - container.downstreamThickness = nthfloat(thickness, 1) - - geometryValues = self.getNextToken() - container.angleOfRotation = nthfloat(geometryValues, 0) - container.sampleWidth = nthfloat(geometryValues, 1) - else: - - # Otherwise, it is CYLINDRICAL, - # then extract the inner and outer - # radii and the sample height. - radii = self.getNextToken() - container.innerRadius = nthfloat(radii, 0) - container.outerRadius = nthfloat(radii, 1) - container.sampleHeight = nthfloat(self.getNextToken(), 0) - - # Extract the density. - density = nthfloat(self.getNextToken(), 0) - - # Take the absolute value of the density - since it could be -ve. - container.density = abs(density) - - # Decide on the units of density. - # -ve density means it is atomic (atoms/A^3) - # +ve means it is chemical (gm/cm^3) - container.densityUnits = ( - UnitsOfDensity.ATOMIC if - density < 0 - else UnitsOfDensity.CHEMICAL - ) - crossSectionSource = firstword(self.getNextToken()) - if ( - crossSectionSource == "TABLES" - or crossSectionSource == "TRANSMISSION" - ): - container.totalCrossSectionSource = ( - CrossSectionSource[crossSectionSource] - ) - else: - container.totalCrossSectionSource = CrossSectionSource.FILE - container.crossSectionFilename = crossSectionSource - container.tweakFactor = nthfloat(self.getNextToken(), 0) - - environmentValues = self.getNextToken() - container.scatteringFraction = nthfloat(environmentValues, 0) - container.attenuationCoefficient = nthfloat(environmentValues, 1) - - # Consume whitespace and the closing brace. - self.consumeUpToDelim("}") - - return container - - except Exception as e: - raise ParserException( - "Whilst parsing Container, an exception occured." - " The input file is most likely of an incorrect format, " - "and some attributes were missing." - ) from e - - def parseComponents(self): - try: - while self.stream: - component = self.parseComponent() - if component: - self.components.addComponent(component) - except Exception as e: - raise ParserException( - "Whilst parsing Components, an exception occured." - " The input file is most likely of an incorrect format." - ) from e - - def parseComponent(self): - name = self.getNextToken().rstrip() - component = Component(name) - line = self.peekNextToken() - if "(" in line: - self.consumeTokens(1) - else: - return - line = self.getNextToken() - while line and ")" not in line: - atomicSymbol, massNo, abundance = line.split() - element = Element(atomicSymbol, float(massNo), float(abundance)) - component.addElement(element) - line = self.getNextToken() - return component - - def makeParse(self, key): - """ - Calls a parsing function from a dictionary of parsing functions - by the input key. - Returns the result of the called parsing function. - Only use case is as a helper function during parsing. - - Parameters - ---------- - key : str - Parsing function to call - (INSTRUMENT/BEAM/NORMALISATION/SAMPLE BACKGROUND/SAMPLE/CONTAINER) - Returns - ------- - NoneType - if parsing INSTRUMENT/BEAM/NORMALISATION - SampleBackground - if parsing SAMPLE BACKGROUND - Sample - if parsing Sample - Container - if parsing Container - None - if parsing Components - """ - - parsingFunctions = { - "INSTRUMENT": self.parseInstrument, - "BEAM": self.parseBeam, - "NORMALISATION": self.parseNormalisation, - "SAMPLE BACKGROUND": self.parseSampleBackground, - "SAMPLE": self.parseSample, - "CONTAINER": self.parseContainer, - "COMPONENTS": self.parseComponents - } - # Return the result of the parsing function that was called. - return parsingFunctions[key]() - - def sampleBackgroundHelper(self): - """ - Helper method for parsing Sample Background and its - Samples and their Containers. - Returns the SampleBackground object. - Parameters - ---------- - None - Returns - ------- - SampleBackground - The SampleBackground parsed from the lines. - """ - - # Parse sample background. - sampleBackground = self.makeParse("SAMPLE BACKGROUND") - - self.consumeWhitespace() - line = self.peekNextToken() - - # Parse all Samples and Containers belonging to the sample background. - while "END" not in line and "SAMPLE BACKGROUND" not in line: - if not line: - raise ParserException("Unexpected EOF during parsing.") - elif "GO" in line: - self.getNextToken() - elif "SAMPLE" in line and firstword(line) == "SAMPLE": - sample = self.makeParse("SAMPLE") - if not sample.name: - sample.name = utils.replace_unwanted_chars(uniquifyName( - "SAMPLE", - [s.name for s in sampleBackground.samples], - sep="", - incFirst=True - )) - sampleBackground.samples.append(sample) - elif "CONTAINER" in line and firstword(line) == "CONTAINER": - container = self.makeParse("CONTAINER") - if not container.name: - container.name = uniquifyName( - "CONTAINER", - [c.name - for c in sampleBackground.samples[-1].containers], - sep="", - incFirst=True) - sampleBackground.samples[-1].containers.append( - container - ) - self.consumeWhitespace() - line = self.peekNextToken() - return sampleBackground - - def parse(self, path, config=False): - """ - Parse the GudrunFile from its path. - Assign objects from the file to the attributes of the class. - Raises ParserException if Instrument, - Beam or Normalisation are missing. - - Parameters - ---------- - None - Returns - ------- - None - """ - self.config = config - # Ensure only valid files are given. - if not path: - raise ParserException( - "Path not supplied. Cannot parse from an empty path!" - ) - if not os.path.exists(path): - raise ParserException( - "The path supplied is invalid.\ - Cannot parse from an invalid path" + path - ) - if self.format == Format.YAML: - # YAML Files - try: - ( - self.instrument, - self.beam, - self.components, - self.normalisation, - self.sampleBackgrounds, - cfg.GUI - ) = self.yaml.parseYaml(path) - except YAMLException as e: - raise ParserException(e) - else: - # TXT Files - parsing = "" - KEYWORDS = { - "INSTRUMENT": False, - "BEAM": False, - "NORMALISATION": False - } - - # Decide the encoding - import chardet - with open(path, 'rb') as fp: - encoding = chardet.detect(fp.read())['encoding'] - - # Read the input stream into our attribute. - with open(path, encoding=encoding) as fp: - self.stream = fp.readlines() - - # Here we go! Get the first token and begin parsing. - line = self.getNextToken() - - # Iterate through the file, - # parsing the Instrument, Beam and Normalisation. - while ( - self.stream - and not all(value for value in KEYWORDS.values()) - ): - if ( - firstword(line) in KEYWORDS.keys() - and not KEYWORDS[firstword(line)] - ): - parsing = firstword(line) - self.makeParse(parsing) - KEYWORDS[parsing] = True - line = self.getNextToken() - - # If we didn't parse each one of the keywords, then panic. - if not all(KEYWORDS.values()) and not config: - raise ParserException(( - 'INSTRUMENT, BEAM and NORMALISATION' - ' were not parsed. It\'s possible the file' - ' supplied is of an incorrect format!' - )) - elif not KEYWORDS["INSTRUMENT"] and config: - raise ParserException(( - 'INSTRUMENT was not parsed. It\'s possible the file' - ' supplied is of an incorrect format!' - )) - - # Ignore whitespace. - self.consumeWhitespace() - line = self.peekNextToken() - - # Parse sample backgrounds, alongside their samples and containers. - while self.stream: - if "SAMPLE BACKGROUND" in line and "{" in line: - self.sampleBackgrounds.append( - self.sampleBackgroundHelper() - ) - elif "COMPONENTS:" in line: - self.makeParse("COMPONENTS") - line = self.getNextToken() - def __str__(self): """ Returns the string representation of the GudrunFile object. @@ -1505,9 +248,3 @@ def __str__(self): + footer + components ) - - -Container.getNextToken = GudrunFile.getNextToken -Container.peekNextToken = GudrunFile.peekNextToken -Container.consumeUpToDelim = GudrunFile.consumeUpToDelim -Container.consumeWhitespace = GudrunFile.consumeWhitespace diff --git a/gudpy/core/io/__init__.py b/gudpy/core/io/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/gudpy/core/io.py b/gudpy/core/io/gudpy_io.py similarity index 56% rename from gudpy/core/io.py rename to gudpy/core/io/gudpy_io.py index 55acf8e4d..fb42d37f6 100644 --- a/gudpy/core/io.py +++ b/gudpy/core/io/gudpy_io.py @@ -3,7 +3,6 @@ from ruamel.yaml import YAML as yaml from ruamel.yaml import YAMLError import os -import typing as typ from core.composition import ( Component, Components, Composition, WeightedComponent @@ -20,127 +19,138 @@ from core.sample import Sample from core.container import Container from core.gudrun_file import GudrunFile +from core.io.txt_parser import GudrunFileParser from core import config -import core.enums as enums -class YamlParser: +class GudpyIO: def __init__(self): - self.yaml = self.getYamlModule() + # Construct the outpath of generated input file + self.projectDir = "" + self.loadFile = "" + self.autosaveLocation = "" + + self.yaml = self._getYamlModule() + self.gudrunFileParser = GudrunFileParser() + + @property + def projectName(self) -> str: + return os.path.dirname(self.projectDir) + + @property + def autosavePath(self) -> str: + return f"{os.path.basename(projectDir)}.autosave" @property - def path(self, format: enums.Format): - if not self.projectDir or self.projectDir: + def path(self): + """Returns the path to the yaml file + + Returns + ------- + str + Path to yaml file + + Raises + ------ + RuntimeError + Raised if project directory is not set + """ + if not self.projectDir: raise RuntimeError("Save location not set.") + return os.path.join(self.projectDir, f"{self.projectName}.yaml") - if format == enums.Format.TXT: - return os.path.join(self.projectDir, f"{self.projectName}.txt") - elif format == enums.Format.YAML: - return os.path.join(self.projectDir, f"{self.projectName}.yaml") - else: - raise RuntimeError("Unsupported format.") + def setSaveLocation(self, projectDir: str): + """Sets the save location/project directory - def save(self, path: str = '', format: enums.Format = None): - if not path: - path = self.path() + Parameters + ---------- + projectDir : str + Path to new save location + """ + self.projectDir = projectDir - if not format: - format = self.format - if format == enums.Format.TXT: - self.write_out( - path=self.txtPath(), overwrite=True) - elif format == enums.Format.YAML: - if self.path(): - self.write_yaml(self.yamlPath()) + def checkSaveLocation(self) -> bool: + """Checks if user has set the save location + """ + return bool(self.projectDir) - @property - def projectName(self): - os.path.dirname(self.projectDir) - - def write_out( - self, - path: str = '', - overwrite: bool = False, - writeParameters: bool = True - ): + def save(self): + """Saves yaml file to project directory """ - Writes out the string representation of the GudrunFile. - If 'overwrite' is True, then the initial file is overwritten. - Otherwise, it is written to 'gudpy_{initial filename}.txt'. + self.writeYaml(self.path) + + def importGudrunFile(self, path) -> GudrunFile: + return self.gudrunFileParser.parse(path) + + def importProject(self, projectDir) -> GudrunFile: + """Imports from a project directory Parameters ---------- - overwrite : bool, optional - Overwrite the initial file? (default is False). - path : str, optional - Path to write to. + projectDir : str + Path to GudPy project folder + + Raises + ------ + FileNotFoundError + Raised if there is no YAML input file in the + project directory + Returns ------- - None + GudrunFile + Genererated GudrunFile from input """ - if path: - if not overwrite: - assert (not os.path.exists(path)) - f = open( - path, "w", encoding="utf-8" + loadFile = "" + + if os.path.exists(os.path.join( + projectDir, + f"{os.path.basename(projectDir)}.yaml" + )): + # If default file exists + loadFile = os.path.join( + projectDir, + f"{os.path.basename(projectDir)}.yaml" ) - elif not overwrite: - assert (not os.path.exists(os.path.join( - self.instrument.GudrunInputFileDir, - self.OUTPATH) - )) - f = open( - os.path.join( - self.instrument.GudrunInputFileDir, - self.OUTPATH - ), "w", encoding="utf-8") else: - if not self.path(): - path = os.path.join( - self.instrument.GudrunInputFileDir, - self.OUTPATH) - f = open(path, "w", encoding="utf-8") - - if os.path.basename(f.name) == self.OUTPATH: - for sampleBackground in self.sampleBackgrounds: - sampleBackground.writeAllSamples = False - f.write(str(self)) - f.close() - - if writeParameters: - for sb in self.sampleBackgrounds: - for s in sb.samples: - if s.runThisSample: - gf = deepcopy(self) - gf.sampleBackgrounds = [deepcopy(sb)] - gf.sampleBackgrounds[0].samples = [deepcopy(s)] - gf.write_out( - path=os.path.join( - self.instrument.GudrunInputFileDir, - s.pathName(), - ), - overwrite=True, - writeParameters=False - ) + # Try to find yaml files + for f in os.listdir(projectDir): + if os.path.splitext(f)[1] == ".yaml": + # If file is yaml + loadFile = os.path.join(projectDir, f) + if not loadFile: + raise FileNotFoundError( + "Could not find GudPy input file within the project") + + self.setSaveLocation(projectDir) + return self._parseYaml(loadFile) + + def exportGudrunFile(self, path): + return self.gudrunFileParser.export(path) + + def writeGudrunFile(self, path): + self. + return self.gudrunFileParser.writeGudrunFile(path) # ================================================================= -# YAML PARSING +# PARSING HELPERS # ================================================================= - def getYamlModule(self) -> yaml: + def _getYamlModule(self) -> yaml: yaml_ = yaml() yaml_.preserve_quotes = True yaml_.default_flow_style = None yaml_.encoding = 'utf-8' return yaml_ - def parseYaml(self, path) -> GudrunFile: - yamldict = self.yamlToDict(path) - gudrunFile = GudrunFile() + def _parseYaml(self, path) -> GudrunFile: + self.loadFile = path + yamldict = self._yamlToDict(path) + gudrunFile = GudrunFile(loadFile=path) for k, v in yamldict.items(): if k == "GUI": GUI = GUIConfig() - self.yamlToObject(GUI, v) + self._yamlToObject(GUI, v) config.GUIConfig = GUI continue if not hasattr(gudrunFile, k): @@ -149,7 +159,7 @@ def parseYaml(self, path) -> GudrunFile: continue objType = type(gudrunFile.__dict__[k]) - parsingFunc = self.parsingFuncMap(objType) + parsingFunc = self._parsingFuncMap(objType) if parsingFunc: try: obj = parsingFunc(v) @@ -161,7 +171,7 @@ def parseYaml(self, path) -> GudrunFile: raise YAMLException(f"Invalid YAML file: {str(e)}") return gudrunFile - def writeYAML(self, gudrunFile: GudrunFile, path: str) -> None: + def _writeYAML(self, gudrunFile: GudrunFile, path: str) -> None: with open(path, "wb") as fp: outyaml = { "Instrument": gudrunFile.instrument, @@ -172,39 +182,39 @@ def writeYAML(self, gudrunFile: GudrunFile, path: str) -> None: "GUI": config.GUI } self.yaml.dump( - {k: self.toYaml(v) for k, v in outyaml.items()}, + {k: self._toYaml(v) for k, v in outyaml.items()}, fp ) - def parsingFuncMap(self, type: type) -> typ.Callable | None: + def _parsingFuncMap(self, type: type) -> typ.Callable | None: parsingFuncs = { - Instrument: self.parseYamlInstrument, - Beam: self.parseYamlBeam, - Normalisation: self.parseYamlNormalisation, - SampleBackground: self.parseYamlSampleBackground, - Sample: self.parseYamlSample, - Composition: self.parseYamlComposition, - Component: self.parseYamlComponent, - Components: self.parseYamlComponents, - list[Element]: self.parseYamlElements, - DataFiles: self.parseYamlDataFiles + Instrument: self._parseYamlInstrument, + Beam: self._parseYamlBeam, + Normalisation: self._parseYamlNormalisation, + SampleBackground: self._parseYamlSampleBackground, + Sample: self._parseYamlSample, + Composition: self._parseYamlComposition, + Component: self._parseYamlComponent, + Components: self._parseYamlComponents, + list[Element]: self._parseYamlElements, + DataFiles: self._parseYamlDataFiles } if type in parsingFuncs.keys(): return parsingFuncs[type] else: return None - def yamlToDict(self, path) -> any: + def _yamlToDict(self, path) -> any: # Read the input stream into our attribute. with open(path, encoding=self.yaml.encoding) as fp: return self.yaml.load(fp) - def yamlToObject(self, obj: any, yamldict: any) -> any: + def _yamlToObject(self, obj: any, yamldict: any) -> any: for k, v in yamldict.items(): - self.assignYamlToAttr(obj, k, v) + self._assignYamlToAttr(obj, k, v) return obj - def assignYamlToAttr(self, obj: any, key, val) -> None: + def _assignYamlToAttr(self, obj: any, key, val) -> None: if not hasattr(obj, key): # If attribute is not valid print( @@ -214,61 +224,61 @@ def assignYamlToAttr(self, obj: any, key, val) -> None: return setattr(obj, key, type(obj.__dict__[key])(self.toBuiltin(val))) - def parseYamlInstrument(self, yamldict: any) -> Instrument: + def _parseYamlInstrument(self, yamldict: any) -> Instrument: instrument = Instrument() - instrument = self.yamlToObject(instrument, yamldict) + instrument = self._yamlToObject(instrument, yamldict) return instrument - def parseYamlBeam(self, yamldict: any) -> Normalisation: + def _parseYamlBeam(self, yamldict: any) -> Normalisation: beam = Beam() - beam = self.yamlToObject(beam, yamldict) + beam = self._yamlToObject(beam, yamldict) return beam - def parseYamlNormalisation(self, yamldict: any) -> Normalisation: + def _parseYamlNormalisation(self, yamldict: any) -> Normalisation: normalisation = Normalisation() for k, v in yamldict.items(): if k == "composition": - composition = self.parseYamlComposition(yamldict[k]) + composition = self._parseYamlComposition(yamldict[k]) setattr(normalisation, k, composition) else: - self.assignYamlToAttr(normalisation, k, v) + self._assignYamlToAttr(normalisation, k, v) return normalisation - def parseYamlSample(self, yamldict: any) -> Sample: + def _parseYamlSample(self, yamldict: any) -> Sample: sample = Sample() for k, v in yamldict.items(): if k == "composition": - composition = self.parseYamlComposition() + composition = self._parseYamlComposition() setattr(sample, k, composition) elif k == "containers": for contyaml in yamldict[k]: - container = self.parseYamlContainer(contyaml) + container = self._parseYamlContainer(contyaml) sample.containers.append(container) else: - self.assignYamlToAttr(sample, k, v) + self._assignYamlToAttr(sample, k, v) return sample - def parseYamlSampleBackground(self, yamldict: any) -> SampleBackground: + def _parseYamlSampleBackground(self, yamldict: any) -> SampleBackground: sampleBg = SampleBackground() for k, v in yamldict.items(): if k == "samples": for sampleyaml in yamldict[k]: - sample = self.parseYamlSample(sampleyaml) + sample = self._parseYamlSample(sampleyaml) sample.name = utils.replace_unwanted_chars(sample.name) sampleBg.samples.append(sample) else: - self.assignYamlToAttr(sampleBg, k, v) + self._assignYamlToAttr(sampleBg, k, v) - def parseYamlComposition(self, yamldict: any) -> Composition: + def _parseYamlComposition(self, yamldict: any) -> Composition: composition = Composition() for k, v in yamldict.items(): if k == "elements": - elements = self.parseYamlElements(yamldict[k]) + elements = self._parseYamlElements(yamldict[k]) setattr(composition, k, elements) elif k == "weightedComponents": weightedComponents = [] for wc in yamldict[k]: - component = self.parseYamlComponent(wc) + component = self._parseYamlComponent(wc) ratio = wc["ratio"] try: weightedComponents.append( @@ -280,28 +290,28 @@ def parseYamlComposition(self, yamldict: any) -> Composition: "Invalid ratio given to Weighted Component") setattr(composition, k, weightedComponents) else: - self.assignYamlToAttr(composition, k, v) + self._assignYamlToAttr(composition, k, v) return composition - def parseYamlContainer(self, yamldict: any) -> Container: + def _parseYamlContainer(self, yamldict: any) -> Container: container = Container() for k, v in yamldict.items(): if k == "composition": - composition = self.parseYamlComposition() + composition = self._parseYamlComposition() setattr(container, k, composition) else: - self.assignYamlToAttr(container, k, v) + self._assignYamlToAttr(container, k, v) return container - def parseYamlDataFiles(self, yamldict: any) -> DataFiles: + def _parseYamlDataFiles(self, yamldict: any) -> DataFiles: dataFiles = DataFiles() for df in yamldict["_dataFiles"]: dataFile = DataFile(df["filename"]) - dataFile = self.yamlToObject(dataFile, df) + dataFile = self._yamlToObject(dataFile, df) dataFiles.dataFiles.append(dataFile) return dataFiles - def parseYamlComponent(self, yamldict: any) -> Component: + def _parseYamlComponent(self, yamldict: any) -> Component: if ( "component" not in yamldict or "ratio" not in yamldict @@ -310,18 +320,18 @@ def parseYamlComponent(self, yamldict: any) -> Component: "Weighted Component expects 'component' and" + " 'ratio' to be provided") component = Component() - self.yamlToObject( + self._yamlToObject( component, yamldict["component"] ) return component - def parseYamlComponents(self, yamllist: list[dict]) -> Components: + def _parseYamlComponents(self, yamllist: list[dict]) -> Components: components = [] for c in yamllist: - components.append(self.parseYamlComponent(c)) + components.append(self._parseYamlComponent(c)) return components - def parseYamlElements(self, yamllist: list[str]) -> list[Element]: + def _parseYamlElements(self, yamllist: list[str]) -> list[Element]: elements = [] for idx, element in enumerate(yamllist): # Ensuring correct arguements are provided @@ -358,7 +368,7 @@ def toYaml(self, var: any): return float(var) if var.__class__.__module__ == "builtins": if isinstance(var, (list, tuple)): - return type(var)([self.toYaml(v) for v in var]) + return type(var)([self._toYaml(v) for v in var]) else: return var elif isinstance(var, Enum): @@ -369,7 +379,7 @@ def toYaml(self, var: any): Component, Composition, Element, DataFiles, DataFile, GUIConfig )): return { - k: self.toYaml(v) + k: self._toYaml(v) for k, v in var.__dict__.items() if k not in var.yamlignore } @@ -392,11 +402,3 @@ def toBuiltin(self, yamlvar: any): return float(yamlvar) elif yamlvar.__class__.__module__ == "ruamel.yaml.scalarstring": return str(yamlvar) - -# ================================================================= -# TXT PARSING -# ================================================================= - - -class TxtParser: - def __init__(self): diff --git a/gudpy/core/io/txt_parser.py b/gudpy/core/io/txt_parser.py new file mode 100644 index 000000000..bcac4a815 --- /dev/null +++ b/gudpy/core/io/txt_parser.py @@ -0,0 +1,1181 @@ +import re +import os +import typing as typ + +from core.composition import ( + Component, Components, Composition +) +from core.data_files import DataFiles +from core.element import Element +from core.exception import ParserException +from core import utils +from core.instrument import Instrument +from core.beam import Beam +from core.normalisation import Normalisation +from core.sample_background import SampleBackground +from core.sample import Sample +from core.container import Container +from core.gudrun_file import GudrunFile +from core import config +import core.enums as enums + + +class GudrunFileParser: + """Class to manage the parsing of Gudrun input files + """ + OUTPATH = "gudpy.txt" + + def __init__(self): + # Text stream + self.stream = None + + def writeGudrunFile(self, gudrunFile: GudrunFile, runDir: str): + gudrunFile.setGudrunDir(runDir) + for sampleBackground in self.sampleBackgrounds: + sampleBackground.writeAllSamples = False + + writePath = os.path.join(runDir, GudrunFileParser.OUTPATH) + + with open(writePath, "w", encoding="utf-8") as f: + f.write(str(gudrunFile)) + f.close() + + def export(self, gudrunFile: GudrunFile, path: str): + for sampleBackground in self.sampleBackgrounds: + sampleBackground.writeAllSamples = True + + with open(path, "w", encoding="utf-8") as f: + f.write(str(gudrunFile)) + f.close() + + def parse( + self, path: str, config=False + ) -> typ.Tuple[ + Instrument, Beam, Normalisation, + list[SampleBackground], Components]: + """ + Parse the GudrunFile from its path. + Assign objects from the file to the attributes of the class. + Raises ParserException if Instrument, + Beam or Normalisation are missing. + """ + self.config = config + + instrument = None + beam = None + components = None + normalisation = None + sampleBackgrounds = [] + + # Ensure only valid files are given. + if not path: + raise ParserException( + "Path not supplied. Cannot parse from an empty path!" + ) + if not os.path.exists(path): + raise ParserException( + "The path supplied is invalid.\ + Cannot parse from an invalid path" + path + ) + + KEYWORDS = { + "INSTRUMENT": False, + "BEAM": False, + "NORMALISATION": False + } + # Decide the encoding + import chardet + with open(path, 'rb') as fp: + encoding = chardet.detect(fp.read())['encoding'] + # Read the input stream into our attribute. + with open(path, encoding=encoding) as fp: + self.stream = fp.readlines() + # Here we go! Get the first token and begin parsing. + line = self._getNextToken() + + # Iterate through the file, + # parsing the Instrument, Beam and Normalisation. + while self.stream: + line = utils.firstword(line) + if line == "INSTRUMENT": + instrument = self._parseInstrument(line) + KEYWORDS[line] = True + elif line == "BEAM": + beam = self._parseBeam(line) + KEYWORDS[line] = True + elif line == "NORMALISATION": + normalisation = self._parseNormalisation(line) + KEYWORDS[line] = True + + # If we didn't parse each one of the keywords, then panic. + if not all(KEYWORDS.values()) and not config: + raise ParserException(( + 'INSTRUMENT, BEAM and NORMALISATION' + ' were not parsed. It\'s possible the file' + ' supplied is of an incorrect format!' + )) + elif not KEYWORDS["INSTRUMENT"] and config: + raise ParserException(( + 'INSTRUMENT was not parsed. It\'s possible the file' + ' supplied is of an incorrect format!' + )) + + # Ignore whitespace. + self._consumeWhitespace() + line = self._peekNextToken() + + # Parse sample backgrounds, alongside their samples and containers. + while self.stream: + if "SAMPLE BACKGROUND" in line and "{" in line: + sampleBackgrounds.append( + self._sampleBackgroundHelper() + ) + elif "COMPONENTS:" in line: + components = self._parseComponents(line) + line = self._getNextToken() + + return GudrunFile( + loadFile=path, + params=( + instrument, + beam, + normalisation, + sampleBackgrounds, + components + )) + +# ================================================================= +# PARSING HELPERS +# ================================================================= + def _getNextToken(self) -> str | None: + """ + Pops the 'next token' from the stream and returns it. + Essentially removes the first line in the stream and returns it. + """ + return self.stream.pop(0) if self.stream else None + + def _peekNextToken(self) -> str | None: + """ + Returns the next token in the input stream, without removing it. + """ + return self.stream[0] if self.stream else None + + def _consumeTokens(self, n: int) -> None: + """ + Consume n tokens from the input stream. + """ + for _ in range(n): + self._getNextToken() + + def _consumeUpToDelim(self, delim: str) -> None: + """ + Consume tokens iteratively, until a delimiter is reached. + """ + line = self._getNextToken() + while line[0] != delim: + line = self._getNextToken() + + def _consumeWhitespace(self) -> None: + """ + Consume tokens iteratively, while they are whitespace. + """ + line = self._peekNextToken() + if line and line.isspace(): + self._getNextToken() + line = self._peekNextToken() + + def _parseInstrument(self) -> Instrument: + """ + Intialises an Instrument object and assigns it to the + instrument attribute. + Parses the attributes of the Instrument from the input stream. + Raises a ParserException if any mandatory attributes are missing. + """ + try: + instrument = Instrument() + self._consumeWhitespace() + + # For string attributes, + # we simply extract the utils.firstword in the line. + instrument.name = enums.Instruments[ + utils.firstword(self._getNextToken())] + self._consumeTokens(1) + instrument.dataFileDir = os.path.abspath( + utils.firstword(self._getNextToken())) + os.path.sep + instrument.dataFileType = utils.firstword(self._getNextToken()) + instrument.detectorCalibrationFileName = ( + utils.firstword(self._getNextToken()) + ) + + # For single integer attributes, + # we extract the zeroth int from the line. + instrument.columnNoPhiVals = utils.nthint(self._getNextToken(), 0) + instrument.groupFileName = utils.firstword(self._getNextToken()) + instrument.deadtimeConstantsFileName = ( + utils.firstword(self._getNextToken()) + ) + + # For N integer attributes, + # we extract the first N integers from the line. + instrument.spectrumNumbersForIncidentBeamMonitor = ( + utils.extract_ints_from_string(self._getNextToken()) + ) + + # For integer pair attributes, + # we extract the first 2 integers from the line. + instrument.wavelengthRangeForMonitorNormalisation = ( + utils.firstNFloats(self._getNextToken(), 2) + ) + + if all( + instrument.wavelengthRangeForMonitorNormalisation + ) == 0.0: + instrument.wavelengthRangeForMonitorNormalisation = [ + 0, 0 + ] + + instrument.spectrumNumbersForTransmissionMonitor = ( + utils.extract_ints_from_string(self._getNextToken()) + ) + + # For single float attributes, + # we extract the zeroth float from the line. + instrument.incidentMonitorQuietCountConst = ( + utils.nthfloat(self._getNextToken(), 0) + ) + instrument.transmissionMonitorQuietCountConst = ( + utils.nthfloat(self._getNextToken(), 0) + ) + + instrument.channelNosSpikeAnalysis = ( + utils.firstNInts(self._getNextToken(), 2) + ) + instrument.spikeAnalysisAcceptanceFactor = ( + utils.nthfloat(self._getNextToken(), 0) + ) + + # Extract wavelength range + # Which consists of the first 3 floats + # (min, max, step) in the line. + wavelengthRange = utils.firstNFloats(self._getNextToken(), 3) + instrument.wavelengthMin = wavelengthRange[0] + instrument.wavelengthMax = wavelengthRange[1] + instrument.wavelengthStep = wavelengthRange[2] + + instrument.NoSmoothsOnMonitor = utils.nthint( + self._getNextToken(), 0) + + # Extract X range + # Which consists of the first 3 floats + # (min, max, step) in the line. + XRange = utils.firstNFloats(self._getNextToken(), 3) + + instrument.XMin = XRange[0] + instrument.XMax = XRange[1] + instrument.XStep = XRange[2] + + # Extract the grouping parameter panel. + # Each row in the panel consists of the first 4 ints + # (Group, XMin, XMax, Background Factor) in the line. + # If the marker line is encountered, + # then the panel has been parsed. + + line = self._getNextToken() + while "to end input of specified values" not in line: + group = utils.nthint(line, 0) + xMin = utils.nthfloat(line, 1) + xMax = utils.nthfloat(line, 2) + backgroundFactor = utils.nthfloat(line, 3) + instrument.groupingParameterPanel.append( + [group, xMin, xMax, backgroundFactor] + ) + line = self._getNextToken() + + instrument.groupsAcceptanceFactor = ( + utils.nthfloat(self._getNextToken(), 0) + ) + instrument.mergePower = utils.nthint(self._getNextToken(), 0) + + # For boolean attributes, we convert the first + # integer in the line to its boolean value. + instrument.subSingleAtomScattering = ( + utils.boolifyNum(utils.nthint(self._getNextToken(), 0)) + ) + + # For enumerated attributes, where the value of the attribute is + # the first integer in the line, and we must get the member, + # we do this: Enum[Enum(value).name] + instrument.mergeWeights = ( + enums.MergeWeights[enums.MergeWeights( + utils.nthint(self._getNextToken(), 0)).name] + ) + instrument.incidentFlightPath = ( + utils.nthfloat(self._getNextToken(), 0) + ) + instrument.spectrumNumberForOutputDiagnosticFiles = ( + utils.nthint(self._getNextToken(), 0) + ) + + instrument.neutronScatteringParametersFile = ( + utils.firstword(self._getNextToken()) + + ) + instrument.scaleSelection = ( + enums.Scales[enums.Scales( + utils.nthint(self._getNextToken(), 0)).name] + ) + instrument.subWavelengthBinnedData = ( + utils.boolifyNum(utils.nthint(self._getNextToken(), 0)) + ) + self._consumeTokens(2) + instrument.logarithmicStepSize = ( + utils.nthfloat(self._getNextToken(), 0) + ) + instrument.hardGroupEdges = ( + utils.boolifyNum(utils.nthint(self._getNextToken(), 0)) + ) + + # If NeXus files are being used, then we expect a NeXus definition + # file to be present, and extract it. + if ( + instrument.dataFileType == "NXS" + or instrument.dataFileType == "nxs" + ): + instrument.nxsDefinitionFile = ( + utils.firstword(self._getNextToken()) + ) + + if self.config: + instrument.goodDetectorThreshold = utils.nthint( + self._getNextToken(), 0 + ) + + # Consume whitespace and the closing brace. + self._consumeUpToDelim("}") + + # Resolve the paths, to make them relative. + # First construct the regular expression to match against. + pattern = re.compile(r"StartupFiles\S*") + + match = re.search( + pattern, + instrument.detectorCalibrationFileName + ) + + if match: + instrument.detectorCalibrationFileName = match.group() + + match = re.search( + pattern, + instrument.groupFileName + ) + + if match: + instrument.groupFileName = match.group() + + match = re.search( + pattern, + instrument.deadtimeConstantsFileName + ) + + if match: + instrument.deadtimeConstantsFileName = match.group() + + match = re.search( + pattern, + instrument.neutronScatteringParametersFile + ) + + if match: + instrument.neutronScatteringParametersFile = match.group() + + match = re.search( + pattern, + instrument.neutronScatteringParametersFile + ) + + if match: + instrument.neutronScatteringParametersFile = match.group() + + return instrument + + except Exception as e: + raise ParserException( + "Whilst parsing Instrument, an exception occured." + " The input file is most likely of an incorrect format, " + "and some attributes were missing." + f"{str(e)}" + ) from e + + def _parseBeam(self) -> Beam: + """ + Intialises a Beam object and assigns it to the + beam attribute. + Parses the attributes of the Beam from the input stream. + Raises a ParserException if any mandatory attributes are missing. + """ + + try: + # Initialise beam attribute to a new instance of Beam. + beam = Beam() + + self._consumeWhitespace() + + # For enumerated attributes, + # where the member name of the attribute is + # the first 'word' in the line, and we must get the member, + # we do this: Enum[memberName]. + beam.sampleGeometry = enums.Geometry[utils.firstword( + self._getNextToken())] + + # Set the global geometry. + config.geometry = beam.sampleGeometry + + # Ignore the number of beam values. + self._consumeTokens(1) + + # For N float attributes, + # we extract the first N floats from the line. + beam.beamProfileValues = ( + enums.extract_floats_from_string(self._getNextToken()) + ) + + # For single float attributes, + # we extract the zeroth float from the line. + range = self._getNextToken() + beam.stepSizeAbsorption = utils.nthfloat(range, 0) + beam.stepSizeMS = utils.nthfloat(range, 1) + beam.noSlices = utils.nthint(range, 2) + beam.angularStepForCorrections = ( + utils.nthint(self._getNextToken(), 0) + ) + + # Extract the incident beam edges + # relative to the centroid of the sample. + incidentBeamEdges = self._getNextToken() + beam.incidentBeamLeftEdge = utils.nthfloat(incidentBeamEdges, 0) + beam.incidentBeamRightEdge = utils.nthfloat(incidentBeamEdges, 1) + beam.incidentBeamBottomEdge = utils.nthfloat(incidentBeamEdges, 2) + beam.incidentBeamTopEdge = utils.nthfloat(incidentBeamEdges, 3) + + # Extract the scattered beam edges + # relative to the centroid of the sample. + scatteredBeamEdges = self._getNextToken() + beam.scatteredBeamLeftEdge = utils.nthfloat(scatteredBeamEdges, 0) + beam.scatteredBeamRightEdge = utils.nthfloat(scatteredBeamEdges, 1) + beam.scatteredBeamBottomEdge = utils.nthfloat( + scatteredBeamEdges, 2) + beam.scatteredBeamTopEdge = utils.nthfloat(scatteredBeamEdges, 3) + + # For string attributes, + # we simply extract the utils.firstword in the line. + beam.filenameIncidentBeamSpectrumParams = ( + utils.firstword(self._getNextToken()) + ) + + # Now match it against a pattern, + # to resolve the path to be relative. + pattern = re.compile(r"StartupFiles\S*") + + match = re.search( + pattern, + beam.filenameIncidentBeamSpectrumParams + ) + + if match: + beam.filenameIncidentBeamSpectrumParams = match.group() + + beam.overallBackgroundFactor = ( + utils.nthfloat(self._getNextToken(), 0) + ) + beam.sampleDependantBackgroundFactor = ( + utils.nthfloat(self._getNextToken(), 0) + ) + beam.shieldingAttenuationCoefficient = ( + utils.nthfloat(self._getNextToken(), 0) + ) + + # Consume whitespace and the closing brace. + self._consumeUpToDelim("}") + + return beam + + except Exception as e: + raise ParserException( + "Whilst parsing Beam, an exception occured." + " The input file is most likely of an incorrect format, " + "and some attributes were missing." + ) from e + + def _parseNormalisation(self) -> Normalisation: + """ + Intialises a Normalisation object and assigns it to the + normalisation attribute. + Parses the attributes of the Normalisation from the input stream. + Raises a ParserException if any mandatory attributes are missing. + """ + + try: + # Initialise normalisation attribute + # to a new instance of Normalisation. + normalisation = Normalisation() + + self._consumeWhitespace() + + # The number of files and period number are both stored + # on the same line. + # So we extract the 0th integer for the number of files, + # and the 1st integer for the period number. + dataFileInfo = self._getNextToken() + numberOfFiles = utils.nthint(dataFileInfo, 0) + normalisation.periodNumber = utils.nthint(dataFileInfo, 1) + + # Extract params files + dataFiles = [] + for _ in range(numberOfFiles): + dataFiles.append(utils.firstword(self._getNextToken())) + # Sorts list so that it is in ascending order + dataFiles.sort() + + # Create a DataFiles object from the dataFiles list constructed. + normalisation.dataFiles = ( + DataFiles(dataFiles, "NORMALISATION") + ) + + # The number of background files and + # background period number are both stored + # on the same line. + # So we extract the 0th integer for the number of background files, + # and the 1st integer for the background riod number. + dataFileInfoBg = self._getNextToken() + numberOfFilesBg = utils.nthint(dataFileInfoBg, 0) + normalisation.periodNumberBg = utils.nthint(dataFileInfoBg, 1) + + # Extract background params files + dataFilesBg = [] + for j in range(numberOfFilesBg): + dataFilesBg.append(utils.firstword(self._getNextToken())) + + # Sorts list so that it is in ascending order + dataFilesBg.sort() + + # Create a DataFiles object from the dataFiles list constructed. + normalisation.dataFilesBg = ( + DataFiles(dataFilesBg, "NORMALISATION BACKGROUND") + ) + + # For boolean attributes, we convert the first + # integer in the line to its boolean value. + normalisation.forceCalculationOfCorrections = ( + utils.boolifyNum(utils.nthint(self._getNextToken(), 0)) + ) + + # Construct composition + composition = [] + line = self._getNextToken() + # Extract the composition. + # Each element in the composition consists of the first 'word', + # integer at the second position, and float at the third position, + # (Atomic Symbol, MassNo, Abundance) in the line. + # If the marker line is encountered, + # then the panel has been parsed. + while "end of composition input" not in line: + atomicSymbol = utils.firstword(line) + massNo = utils.nthfloat(line, 1) + abundance = utils.nthfloat(line, 2) + + # Create an Element object and append to the composition list. + composition.append( + Element(atomicSymbol, massNo, abundance) + ) + line = self._getNextToken() + + # Create a Composition object from the dataFiles list constructed. + normalisation.composition = ( + Composition("Normalisation", elements=composition) + ) + + # For enumerated attributes, + # where the member name of the attribute is + # the first 'word' in the line, and we must get the member, + # we do this: Enum[memberName]. + normalisation.geometry = ( + enums.Geometry[utils.firstword(self._getNextToken())] + ) + + # Is the geometry FLATPLATE? + if ( + ( + normalisation.geometry == enums.Geometry.SameAsBeam + and config.geometry == enums.Geometry.FLATPLATE + ) + or normalisation.geometry == enums.Geometry.FLATPLATE): + # If is is FLATPLATE, then extract the upstream and downstream + # thickness, the angle of rotation and sample width. + thickness = self._getNextToken() + normalisation.upstreamThickness = utils.nthfloat(thickness, 0) + normalisation.downstreamThickness = ( + utils.nthfloat(thickness, 1) + ) + geometryInfo = self._getNextToken() + normalisation.angleOfRotation = utils.nthfloat(geometryInfo, 0) + normalisation.sampleWidth = utils.nthfloat(geometryInfo, 1) + else: + + # Otherwise, it is CYLINDRICAL, + # then extract the inner and outer + # radii and the sample height. + radii = self._getNextToken() + normalisation.innerRadius = utils.nthfloat(radii, 0) + normalisation.outerRadius = utils.nthfloat(radii, 1) + normalisation.sampleHeight = ( + utils.nthfloat(self._getNextToken(), 0) + ) + + # Extract the density. + density = utils.nthfloat(self._getNextToken(), 0) + + # Take the absolute value of the density - since it could be -ve. + normalisation.density = abs(density) + + # Decide on the units of density. + # -ve density means it is atomic (atoms/A^3) + # +ve means it is chemical (gm/cm^3) + normalisation.densityUnits = ( + enums.UnitsOfDensity.ATOMIC if + density < 0 + else enums.UnitsOfDensity.CHEMICAL + ) + + normalisation.tempForNormalisationPC = ( + utils.nthfloat(self._getNextToken(), 0) + ) + crossSectionSource = utils.firstword(self._getNextToken()) + if ( + crossSectionSource == "TABLES" + or crossSectionSource == "TRANSMISSION" + ): + normalisation.totalCrossSectionSource = ( + enums.CrossSectionSource[crossSectionSource] + ) + else: + normalisation.totalCrossSectionSource = ( + enums.CrossSectionSource.FILE + ) + normalisation.crossSectionFilename = crossSectionSource + + normalisation.normalisationDifferentialCrossSectionFile = ( + utils.firstword(self._getNextToken()) + ) + + normalisation.lowerLimitSmoothedNormalisation = ( + utils.nthfloat(self._getNextToken(), 0) + ) + normalisation.normalisationDegreeSmoothing = ( + utils.nthfloat(self._getNextToken(), 0) + ) + normalisation.minNormalisationSignalBR = ( + utils.nthfloat(self._getNextToken(), 0) + ) + + # Consume whitespace and the closing brace. + self._consumeUpToDelim("}") + + # Resolve to relative. + pattern = re.compile(r"StartupFiles\S*") + + match = re.search( + pattern, + normalisation. + normalisationDifferentialCrossSectionFile + ) + + if match: + ( + normalisation. + normalisationDifferentialCrossSectionFile + ) = match.group() + + return normalisation + + except Exception as e: + raise ParserException( + "Whilst parsing Normalisation, an exception occured." + " The input file is most likely of an incorrect format, " + "and some attributes were missing." + ) from e + + def _parseSampleBackground(self, _) -> SampleBackground: + """ + Intialises a SampleBackground object. + Parses the attributes of the SampleBackground from the input stream. + Raises a ParserException if any mandatory attributes are missing. + Returns the parsed object. + + Parameters + ---------- + None + Returns + ------- + sampleBackground : SampleBackground + The SampleBackground that was parsed from the input lines. + """ + + try: + sampleBackground = SampleBackground() + line = self._peekNextToken() + if "SAMPLE BACKGROUND" in line and "{" in line: + self._consumeTokens(1) + self._consumeWhitespace() + dataFileInfo = self._getNextToken() + numberOfFiles = utils.nthint(dataFileInfo, 0) + sampleBackground.periodNumber = utils.nthint(dataFileInfo, 1) + + dataFiles = [] + for _ in range(numberOfFiles): + dataFiles.append(utils.firstword(self._getNextToken())) + sampleBackground.dataFiles = ( + DataFiles(dataFiles, "SAMPLE BACKGROUND") + ) + + # Consume whitespace and the closing brace. + self._consumeUpToDelim("}") + + return sampleBackground + except Exception as e: + raise ParserException( + "Whilst parsing Sample Background, an exception occured." + " The input file is most likely of an incorrect format, " + "and some attributes were missing." + ) from e + + def _parseSample(self, _) -> Sample: + """ + Intialises a Sample object. + Parses the attributes of the Sample from the input stream. + Raises a ParserException if any mandatory attributes are missing. + Returns the parsed object. + """ + + try: + # Create a new instance of Sample. + sample = Sample() + + # Extract the sample name, and then discard whitespace lines. + sample.name = ( + str(self._getNextToken()[:-2]).strip() + .replace("SAMPLE", "").strip() + ) + sample.name = utils.replace_unwanted_chars(sample.name) + print(sample.name) + self._consumeWhitespace() + # The number of files and period number are both stored + # on the same line. + # So we extract the 0th integer for the number of files, + # and the 1st integer for the period number. + dataFileInfo = self._getNextToken() + numberOfFiles = utils.nthint(dataFileInfo, 0) + sample.periodNumber = utils.nthint(dataFileInfo, 1) + + # Extract params files + dataFiles = [] + for _ in range(numberOfFiles): + dataFiles.append(utils.firstword(self._getNextToken())) + # Create a DataFiles object from the dataFiles list constructed. + sample.dataFiles = DataFiles(dataFiles, sample.name) + + # For boolean attributes, we convert the first + # integer in the line to its boolean value. + sample.forceCalculationOfCorrections = ( + utils.boolifyNum(utils.nthint(self._getNextToken(), 0)) + ) + + # Construct composition + composition = [] + line = self._getNextToken() + + # Extract the composition. + # Each element in the composition consists of the first 'word', + # integer at the second position, and float t the first position, + # (Atomic Symbol, MassNo, Abundance) in the line. + # If the marker line is encountered, + # then the panel has been parsed. + while "end of composition input" not in line: + + atomicSymbol = utils.firstword(line) + massNo = utils.nthfloat(line, 1) + abundance = utils.nthfloat(line, 2) + + # Create an Element object and append to the composition list. + composition.append(Element(atomicSymbol, massNo, abundance)) + line = self._getNextToken() + + # Create a Composition object from the dataFiles list constructed. + sample.composition = Composition("Sample", elements=composition) + + # For enumerated attributes, + # where the member name of the attribute is + # the first 'word' in the line, and we must get the member, + # we do this: Enum[memberName]. + sample.geometry = enums.Geometry[utils.firstword( + self._getNextToken())] + + # Is the geometry FLATPLATE? + if ( + ( + sample.geometry == enums.Geometry.SameAsBeam + and config.geometry == enums.Geometry.FLATPLATE + ) + or sample.geometry == enums.Geometry.FLATPLATE): + # If is is FLATPLATE, then extract the upstream and downstream + # thickness, the angle of rotation and sample width. + thickness = self._getNextToken() + sample.upstreamThickness = utils.nthfloat(thickness, 0) + sample.downstreamThickness = utils.nthfloat(thickness, 1) + + geometryInfo = self._getNextToken() + sample.angleOfRotation = utils.nthfloat(geometryInfo, 0) + sample.sampleWidth = utils.nthfloat(geometryInfo, 1) + else: + + # Otherwise, it is CYLINDRICAL, + # then extract the inner and outer + # radii and the sample height. + radii = self._getNextToken() + sample.innerRadius = utils.nthfloat(radii, 0) + sample.outerRadius = utils.nthfloat(radii, 1) + sample.sampleHeight = utils.nthfloat(self._getNextToken(), 0) + + # Extract the density. + density = utils.nthfloat(self._getNextToken(), 0) + + # Decide on the units of density. + # -ve density means it is atomic (atoms/A^3) + # +ve means it is chemical (gm/cm^3) + sample.density = abs(density) + sample.densityUnits = ( + enums.UnitsOfDensity.ATOMIC if + density < 0 + else enums.UnitsOfDensity.CHEMICAL + ) + sample.tempForNormalisationPC = utils.nthfloat( + self._getNextToken(), 0) + crossSectionSource = utils.firstword(self._getNextToken()) + if ( + crossSectionSource == "TABLES" + or crossSectionSource == "TRANSMISSION" + ): + sample.totalCrossSectionSource = ( + enums.CrossSectionSource[crossSectionSource] + ) + else: + sample.totalCrossSectionSource = enums.CrossSectionSource.FILE + sample.crossSectionFilename = crossSectionSource + sample.sampleTweakFactor = utils.nthfloat(self._getNextToken(), 0) + + topHatW = utils.nthfloat(self._getNextToken(), 0) + if topHatW == 0: + sample.topHatW = 0 + sample.FTMode = FTModes.NO_FT + elif topHatW < 0: + sample.topHatW = abs(topHatW) + sample.FTMode = FTModes.SUB_AVERAGE + else: + sample.topHatW = topHatW + sample.FTMode = FTModes.ABSOLUTE + + sample.minRadFT = utils.nthfloat(self._getNextToken(), 0) + sample.grBroadening = utils.nthfloat(self._getNextToken(), 0) + + # Extract the resonance values. + # Each row consists of the first 2 floats. + # (minWavelength, maxWavelength) in the line. + # If the marker line is encountered, + # then the values has been parsed. + line = self._getNextToken() + while ( + "to finish specifying wavelength range of resonance" + not in line + ): + sample.resonanceValues.append( + enums.extract_floats_from_string(line) + ) + line = self._getNextToken() + + # Extract the exponential values. + # Each row consists of the first 3 numbers. + # (Amplitude, Decay, N) in the line. + # If the marker line is encountered, + # then the values has been parsed. + line = self._getNextToken() + if "to specify end of exponential parameter input" not in line: + sample.exponentialValues = [] + while "to specify end of exponential parameter input" not in line: + sample.exponentialValues.append( + utils.extract_nums_from_string(line) + ) + + line = self._getNextToken() + + sample.normalisationCorrectionFactor = ( + utils.nthfloat(self._getNextToken(), 0) + ) + sample.fileSelfScattering = utils.firstword(self._getNextToken()) + sample.normaliseTo = ( + enums.NormalisationType[ + enums.NormalisationType(utils.nthint( + self._getNextToken(), 0)).name + ] + ) + sample.maxRadFT = utils.nthfloat(self._getNextToken(), 0) + sample.outputUnits = ( + enums.OutputUnits[enums.OutputUnits( + utils.nthint(self._getNextToken(), 0)).name] + ) + sample.powerForBroadening = utils.nthfloat(self._getNextToken(), 0) + sample.stepSize = utils.nthfloat(self._getNextToken(), 0) + sample.runThisSample = utils.boolifyNum( + utils.nthint(self._getNextToken(), 0)) + environmentValues = self._getNextToken() + sample.scatteringFraction = utils.nthfloat(environmentValues, 0) + sample.attenuationCoefficient = utils.nthfloat( + environmentValues, 1) + + # Consume whitespace and the closing brace. + self._consumeUpToDelim("}") + + return sample + + except Exception as e: + raise ParserException( + "Whilst parsing Sample, an exception occured." + " The input file is most likely of an incorrect format, " + "and some attributes were missing." + ) from e + + def _parseContainer(self, _) -> Container: + """ + Intialises a Container object. + Parses the attributes of the Container from the input stream. + Raises a ParserException if any mandatory attributes are missing. + Returns the parsed object. + """ + + try: + # Create a new instance of Container. + container = Container() + + # Extract the name from the lines, + # and then discard the unnecessary lines. + container.name = ( + str(self._getNextToken()[:-2]).strip() + .replace("CONTAINER", "").strip() + ) + self._consumeWhitespace() + + # The number of files and period number are both stored + # on the same line. + # So we extract the 0th integer for the number of files, + # and the 1st integer for the period number. + dataFileInfo = self._getNextToken() + numberOfFiles = utils.nthint(dataFileInfo, 0) + container.periodNumber = utils.nthint(dataFileInfo, 1) + + # Extract params files + dataFiles = [] + for _ in range(numberOfFiles): + dataFiles.append(utils.firstword(self._getNextToken())) + + # Create a DataFiles object from the dataFiles list constructed. + container.dataFiles = DataFiles(dataFiles, container.name) + + # Construct composition + composition = [] + line = self._getNextToken() + # Extract the composition. + # Each element in the composition consists of the first 'word', + # integer at the second position, and float t the first position, + # (Atomic Symbol, MassNo, Abundance) in the line. + # If the marker line is encountered, + # then the panel has been parsed. + while "end of composition input" not in line: + + atomicSymbol = utils.firstword(line) + massNo = utils.nthfloat(line, 1) + abundance = utils.nthfloat(line, 2) + + # Create an Element object and append to the composition list. + composition.append(Element(atomicSymbol, massNo, abundance)) + line = self._getNextToken() + # Create a Composition object from the dataFiles list constructed. + container.composition = Composition( + "Container", + elements=composition + ) + + # For enumerated attributes, + # where the member name of the attribute is + # the first 'word' in the line, and we must get the member, + # we do this: Enum[memberName]. + container.geometry = enums.Geometry[utils.firstword( + self._getNextToken())] + + # Is the geometry FLATPLATE? + if ( + ( + container.geometry == enums.Geometry.SameAsBeam + and config.geometry == enums.Geometry.FLATPLATE + ) + or container.geometry == enums.Geometry.FLATPLATE): + # If is is FLATPLATE, then extract the upstream and downstream + # thickness, the angle of rotation and sample width. + thickness = self._getNextToken() + container.upstreamThickness = utils.nthfloat(thickness, 0) + container.downstreamThickness = utils.nthfloat(thickness, 1) + + geometryValues = self._getNextToken() + container.angleOfRotation = utils.nthfloat(geometryValues, 0) + container.sampleWidth = utils.nthfloat(geometryValues, 1) + else: + + # Otherwise, it is CYLINDRICAL, + # then extract the inner and outer + # radii and the sample height. + radii = self._getNextToken() + container.innerRadius = utils.nthfloat(radii, 0) + container.outerRadius = utils.nthfloat(radii, 1) + container.sampleHeight = utils.nthfloat( + self._getNextToken(), 0) + + # Extract the density. + density = utils.nthfloat(self._getNextToken(), 0) + + # Take the absolute value of the density - since it could be -ve. + container.density = abs(density) + + # Decide on the units of density. + # -ve density means it is atomic (atoms/A^3) + # +ve means it is chemical (gm/cm^3) + container.densityUnits = ( + enums.UnitsOfDensity.ATOMIC if + density < 0 + else enums.UnitsOfDensity.CHEMICAL + ) + crossSectionSource = utils.firstword(self._getNextToken()) + if ( + crossSectionSource == "TABLES" + or crossSectionSource == "TRANSMISSION" + ): + container.totalCrossSectionSource = ( + enums.CrossSectionSource[crossSectionSource] + ) + else: + container.totalCrossSectionSource = enums.CrossSectionSource.FILE + container.crossSectionFilename = crossSectionSource + container.tweakFactor = utils.nthfloat(self._getNextToken(), 0) + + environmentValues = self._getNextToken() + container.scatteringFraction = utils.nthfloat(environmentValues, 0) + container.attenuationCoefficient = utils.nthfloat( + environmentValues, 1) + + # Consume whitespace and the closing brace. + self._consumeUpToDelim("}") + + return container + + except Exception as e: + raise ParserException( + "Whilst parsing Container, an exception occured." + " The input file is most likely of an incorrect format, " + "and some attributes were missing." + ) from e + + def _parseComponents(self) -> Components: + components = Components + try: + while self.stream: + component = self._parseComponent() + if component: + components.addComponent(component) + return components + except Exception as e: + raise ParserException( + "Whilst parsing Components, an exception occured." + " The input file is most likely of an incorrect format." + ) from e + + def _parseComponent(self) -> Component: + name = self._getNextToken().rstrip() + component = Component(name) + line = self._peekNextToken() + if "(" in line: + self._consumeTokens(1) + else: + return + line = self._getNextToken() + while line and ")" not in line: + atomicSymbol, massNo, abundance = line.split() + element = Element(atomicSymbol, float(massNo), float(abundance)) + component.addElement(element) + line = self._getNextToken() + return component + + def _sampleBackgroundHelper(self) -> SampleBackground: + """ + Helper method for parsing Sample Background and its + Samples and their Containers. + Returns the SampleBackground object. + Parameters + ---------- + None + Returns + ------- + SampleBackground + The SampleBackground parsed from the lines. + """ + + # Parse sample background. + sampleBackground = self.makeParse("SAMPLE BACKGROUND") + + self._consumeWhitespace() + line = self._peekNextToken() + + # Parse all Samples and Containers belonging to the sample background. + while "END" not in line and "SAMPLE BACKGROUND" not in line: + if not line: + raise ParserException("Unexpected EOF during parsing.") + elif "GO" in line: + self._getNextToken() + elif "SAMPLE" in line and utils.firstword(line) == "SAMPLE": + sample = self.makeParse("SAMPLE") + if not sample.name: + sample.name = utils.replace_unwanted_chars( + utils.uniquifyName( + "SAMPLE", + [s.name for s in sampleBackground.samples], + sep="", + incFirst=True + )) + sampleBackground.samples.append(sample) + elif "CONTAINER" in line and utils.firstword(line) == "CONTAINER": + container = self.makeParse("CONTAINER") + if not container.name: + container.name = utils.uniquifyName( + "CONTAINER", + [c.name + for c in sampleBackground.samples[-1].containers], + sep="", + incFirst=True) + sampleBackground.samples[-1].containers.append( + container + ) + self._consumeWhitespace() + line = self._peekNextToken() + return sampleBackground + + +Container._getNextToken = TxtParser._getNextToken +Container._peekNextToken = TxtParser._peekNextToken +Container._consumeUpToDelim = TxtParser._consumeUpToDelim +Container._consumeWhitespace = TxtParser._consumeWhitespace diff --git a/gudpy/core/purge_file.py b/gudpy/core/purge_file.py index ef5e44c89..21d4ac91a 100644 --- a/gudpy/core/purge_file.py +++ b/gudpy/core/purge_file.py @@ -10,7 +10,6 @@ class PurgeFile: """ Class to represent a PurgeFile. - ... Attributes From beefca0891a11e9a7828292b552552b92322d7a5 Mon Sep 17 00:00:00 2001 From: Noella Spitz Date: Wed, 15 May 2024 11:09:20 +0100 Subject: [PATCH 04/78] Update functions --- gudpy/core/file_library.py | 5 +- gudpy/core/gud_file.py | 26 ---------- gudpy/core/gudpy.py | 59 +++++----------------- gudpy/core/gudrun_file.py | 19 ------- gudpy/core/io/gudpy_io.py | 41 +++++++++++---- gudpy/core/io/txt_parser.py | 9 ++-- gudpy/core/purge_file.py | 28 ----------- gudpy/test/test_gud_file.py | 5 +- gudpy/test/test_gudpy_io.py | 94 +++++++++++++++-------------------- gudpy/test/test_purge_file.py | 21 ++++---- 10 files changed, 103 insertions(+), 204 deletions(-) diff --git a/gudpy/core/file_library.py b/gudpy/core/file_library.py index 1781b82c2..65871a45e 100644 --- a/gudpy/core/file_library.py +++ b/gudpy/core/file_library.py @@ -4,6 +4,7 @@ from core import utils from core.enums import CrossSectionSource +from core.io.gudpy_io import GudPyIO class GudPyFileLibrary: @@ -202,9 +203,7 @@ def exportMintData( sample.sampleFile = path if not os.path.exists(path): - sample.write_out( - self.gudrunFile.projectDir - ) + GudPyIO.writeObject(sample, path) zipFile.write(path, arcname=os.path.basename(path)) return zipFile.filename diff --git a/gudpy/core/gud_file.py b/gudpy/core/gud_file.py index 8a430dfa3..53c4e3e3e 100644 --- a/gudpy/core/gud_file.py +++ b/gudpy/core/gud_file.py @@ -71,13 +71,6 @@ class GudFile: Contents of the .gud file. output : str Output for use in the GUI. - Methods - ------- - parse(): - Parses the GudFile from path, assigning values - to each of the attributes. - write_out(overwrite=False) - Writes out the string representation of the GudFile to a file. """ def __init__(self, path): @@ -350,22 +343,3 @@ def __str__(self): f'{self.suggestedTweakFactor}\n' ) - - def write_out(self, path): - """ - Writes out the string representation of the GudFile. - If 'overwrite' is True, then the initial file is overwritten. - Otherwise, it is written to 'gudpy_{initial filename}.gud'. - - Parameters - ---------- - overwrite : bool, optional - Overwrite the initial file? (default is False). - - Returns - ------- - None - """ - f = open(path, "w", encoding="utf-8") - f.write(str(self)) - f.close() diff --git a/gudpy/core/gudpy.py b/gudpy/core/gudpy.py index 957ac7981..2df8bf25e 100644 --- a/gudpy/core/gudpy.py +++ b/gudpy/core/gudpy.py @@ -16,7 +16,7 @@ import core.output_file_handler as handlers from core.file_library import GudPyFileLibrary from core import data_files -from core.io import txt_parser, gudpy_io +from core.io.gudpy_io import GudPyIO SUFFIX = ".exe" if os.name == "nt" else "" @@ -27,7 +27,7 @@ def __init__( ): self.originalGudrunFile: GudrunFile = None self._gudrunFile: GudrunFile = None - self.io = gudpy_io.GudpyIO() + self.io = GudPyIO() self.purge: Purge = None self.gudrun: Gudrun = None @@ -72,8 +72,7 @@ def loadFromGudrunFile( if not os.path.exists(loadFile): raise FileNotFoundError("Input file does not exist.") - self.gudrunFileParser = txt_parser.GudrunFileParser() - self.gudrunFile = self.gudrunFileParser.parse(loadFile, config) + self.gudrunFile = self.io.importGudrunFile(loadFile, config) self.originalGudrunFile = copy.deepcopy(self.gudrunFile) self.originalGudrunFile.filename = "original" @@ -105,7 +104,7 @@ def checkSaveLocation(self): bool If the save location is set or not """ - return bool(self.projectDir) + return self.io.checkSaveLocation() def setSaveLocation(self, projectDir: str): """Sets the save location @@ -115,28 +114,12 @@ def setSaveLocation(self, projectDir: str): projectDir : str Path to the desired save location of project """ - self.projectDir = projectDir - self.gudrunFile.filename = f"{os.path.basename(projectDir)}" - self.gudrunFile.projectDir = projectDir - self.originalGudrunFile.projectDir = projectDir - self.autosaveLocation = ( - f"{os.path.basename(projectDir)}.autosave" - ) + self.io.setSaveLocation(projectDir) - def save(self, path: str = "", format: enums.Format = enums.Format.YAML): + def save(self): """Saves current GudPy input file - - Parameters - ---------- - path : str, optional - Path to desired save location, by default "" - format : enums.Format, optional - Desired format of save file, by default enums.Format.YAML """ - if not path: - path = self.gudrunFile.path() - self.originalGudrunFile.save(path=path) - self.gudrunFile.save(path=path, format=format) + self.io.save() def saveAs(self, targetDir: str): """Save GudPy project to a new location, set current @@ -152,26 +135,7 @@ def saveAs(self, targetDir: str): IsADirectoryError Raised if the requested save location already exists """ - if os.path.exists(targetDir): - raise IsADirectoryError("Cannot be an existing directory") - - oldDir = self.projectDir - self.setSaveLocation(targetDir) - os.makedirs(targetDir) - - if os.path.exists(os.path.join(oldDir, "Purge")): - shutil.copytree( - os.path.join(oldDir, "Purge"), - os.path.join(targetDir, "Purge") - ) - if os.path.exists(os.path.join(oldDir, "Gudrun")): - shutil.copytree( - os.path.join(oldDir, "Gudrun"), - os.path.join(targetDir, "Gudrun") - ) - self.gudrunFile.filename = os.path.basename(targetDir) - self.gudrunFile.save(path=self.gudrunFile.path(), - format=enums.Format.YAML) + self.io.exportProject(targetDir) self.loadFromProject(projectDir=targetDir) def checkFilesExist(self): @@ -216,7 +180,7 @@ def runPurge(self): self.purge = Purge() purgeFile = PurgeFile(self.gudrunFile) exitcode = self.purge.purge(purgeFile) - self.gudrunFile.save() + self.io.save() if exitcode: raise exc.PurgeException( "Purge failed to run with the following output:\n" @@ -240,6 +204,7 @@ def runGudrun(self, gudrunFile: GudrunFile = None): if not gudrunFile: gudrunFile = self.gudrunFile + self.io.save() self.gudrun = Gudrun() exitcode = self.gudrun.gudrun(gudrunFile=gudrunFile) if exitcode: @@ -420,7 +385,7 @@ def organiseOutput(self, procDir: str, projectDir: str): def purge(self, purgeFile: PurgeFile): self.checkBinary() with tempfile.TemporaryDirectory() as tmp: - purgeFile.write_out(os.path.join( + GudPyIO.writeObject(purgeFile, os.path.join( tmp, f"{self.PROCESS}.dat" )) @@ -501,7 +466,7 @@ def gudrun( gudrunFile.OUTPATH ) assert (gudrunFile.path() is not None) - gudrunFile.write_out(path) + GudPyIO.writeGudrunFile(path) with subprocess.Popen( [self.BINARY_PATH, path], cwd=tmp, stdout=subprocess.PIPE, diff --git a/gudpy/core/gudrun_file.py b/gudpy/core/gudrun_file.py index 963e540f3..8937f8a0a 100644 --- a/gudpy/core/gudrun_file.py +++ b/gudpy/core/gudrun_file.py @@ -23,10 +23,6 @@ class GudrunFile: Attributes ---------- - path : str - Path to the file. - OUTPATH : str - Path to write to, when not overwriting the initial file. instrument : Instrument Instrument object extracted from the input file. beam : Beam @@ -35,11 +31,6 @@ class GudrunFile: Normalisation object extracted from the input file. sampleBackgrounds : SampleBackground[] List of SampleBackgrounds extracted from the input file. - purged : bool - Have the detectors been purged? - stream : str[] - List of strings, where each item represents a line - in the input stream. Methods ------- getNextToken(): @@ -85,20 +76,10 @@ class GudrunFile: sampleBackgroundHelper(): Parses the SampleBackground, its Samples and their Containers. Returns the SampleBackground object. - parse(): - Parse the GudrunFile from its path. - Assign objects from the file to the attributes of the class. - write_out(overwrite=False) - Writes out the string representation of the GudrunFile to a file. - purge(): - Create a PurgeFile from the GudrunFile, and run purge_det on it. """ def __init__( self, - projectDir=None, - loadFile=None, - config=False, params: tuple[ Instrument, Normalisation, diff --git a/gudpy/core/io/gudpy_io.py b/gudpy/core/io/gudpy_io.py index fb42d37f6..a48b2183a 100644 --- a/gudpy/core/io/gudpy_io.py +++ b/gudpy/core/io/gudpy_io.py @@ -2,6 +2,7 @@ from enum import Enum from ruamel.yaml import YAML as yaml from ruamel.yaml import YAMLError +import shutil import os from core.composition import ( @@ -23,12 +24,10 @@ from core import config -class GudpyIO: +class GudPyIO: def __init__(self): - # Construct the outpath of generated input file self.projectDir = "" self.loadFile = "" - self.autosaveLocation = "" self.yaml = self._getYamlModule() self.gudrunFileParser = GudrunFileParser() @@ -39,7 +38,7 @@ def projectName(self) -> str: @property def autosavePath(self) -> str: - return f"{os.path.basename(projectDir)}.autosave" + return f"{os.path.basename(self.projectDir)}.autosave" @property def path(self): @@ -79,8 +78,9 @@ def save(self): """ self.writeYaml(self.path) - def importGudrunFile(self, path) -> GudrunFile: - return self.gudrunFileParser.parse(path) + def importGudrunFile(self, path, config=False) -> GudrunFile: + self.loadFile = path + return self.gudrunFileParser.parse(path, config) def importProject(self, projectDir) -> GudrunFile: """Imports from a project directory @@ -125,12 +125,31 @@ def importProject(self, projectDir) -> GudrunFile: self.setSaveLocation(projectDir) return self._parseYaml(loadFile) - def exportGudrunFile(self, path): - return self.gudrunFileParser.export(path) + def exportProject(self, targetDir): + if os.path.exists(targetDir): + raise IsADirectoryError("Cannot be an existing directory") + + oldFile = os.path.join(targetDir, os.path.basename(self.loadFile)) + oldDir = self.projectDir + self.setSaveLocation(targetDir) + os.makedirs(targetDir) + + shutil.copytree(oldDir, targetDir) + os.rename(oldFile, self.path) + + @classmethod + def exportGudrunFile(cls, path): + return GudrunFileParser.export(path) + + @classmethod + def writeGudrunFile(cls, gudrunFile, path): + GudrunFileParser.writeGudrunFile(gudrunFile, path) - def writeGudrunFile(self, path): - self. - return self.gudrunFileParser.writeGudrunFile(path) + @classmethod + def writeObject(cls, obj, path): + with open(path, "w", encoding="utf-8") as f: + f.write(str(obj)) + f.close() # ================================================================= # PARSING HELPERS diff --git a/gudpy/core/io/txt_parser.py b/gudpy/core/io/txt_parser.py index bcac4a815..50427c24e 100644 --- a/gudpy/core/io/txt_parser.py +++ b/gudpy/core/io/txt_parser.py @@ -29,9 +29,10 @@ def __init__(self): # Text stream self.stream = None - def writeGudrunFile(self, gudrunFile: GudrunFile, runDir: str): + @classmethod + def writeGudrunFile(cls, gudrunFile: GudrunFile, runDir: str): gudrunFile.setGudrunDir(runDir) - for sampleBackground in self.sampleBackgrounds: + for sampleBackground in gudrunFile.sampleBackgrounds: sampleBackground.writeAllSamples = False writePath = os.path.join(runDir, GudrunFileParser.OUTPATH) @@ -41,7 +42,7 @@ def writeGudrunFile(self, gudrunFile: GudrunFile, runDir: str): f.close() def export(self, gudrunFile: GudrunFile, path: str): - for sampleBackground in self.sampleBackgrounds: + for sampleBackground in gudrunFile.sampleBackgrounds: sampleBackground.writeAllSamples = True with open(path, "w", encoding="utf-8") as f: @@ -59,8 +60,6 @@ def parse( Raises ParserException if Instrument, Beam or Normalisation are missing. """ - self.config = config - instrument = None beam = None components = None diff --git a/gudpy/core/purge_file.py b/gudpy/core/purge_file.py index 21d4ac91a..3da033984 100644 --- a/gudpy/core/purge_file.py +++ b/gudpy/core/purge_file.py @@ -24,12 +24,6 @@ class PurgeFile: standard deviation. ignoreBad : bool Ignore any existing bad spectrum files (spec.bad, spec.dat)? - Methods - ------- - write_out() - Writes out the string representation of the PurgeFile to purge_det.dat - purge() - Writes out the file, and then calls purge_det on that file. """ def __init__( @@ -52,28 +46,6 @@ def __init__( self.standardDeviation = standardDeviation self.ignoreBad = ignoreBad - def write_out(self, path=""): - """ - Writes out the string representation of the PurgeFile to - purge_det.dat. - - Parameters - ---------- - None - Returns - ------- - None - """ - # Write out the string representation of the PurgeFile - # To purge_det.dat. - if not path: - f = open("purge_det.dat", "w", encoding="utf-8") - f.write(str(self)) - else: - f = open(path, "w", encoding="utf-8") - f.write(str(self)) - f.close() - def __str__(self): """ Returns the string representation of the PurgeFile object. diff --git a/gudpy/test/test_gud_file.py b/gudpy/test/test_gud_file.py index a4480d101..9b22af6a3 100644 --- a/gudpy/test/test_gud_file.py +++ b/gudpy/test/test_gud_file.py @@ -4,6 +4,7 @@ from core.exception import ParserException from core.gud_file import GudFile from core import gudpy +from core.io.gudpy_io import GudPyIO from test.test_gudpy_workflows import GudPyContext @@ -337,7 +338,7 @@ def testWriteGudFileA(self): gudpy.projectDir, gf.fname ) - gf.write_out(path) + GudPyIO.writeObject(gf, path) gf1 = GudFile(path) dicA = gf.__dict__ @@ -363,7 +364,7 @@ def testWriteGudFileB(self): gudpy.projectDir, gf.fname ) - gf.write_out(path) + GudPyIO.writeObject(gf, path) gf1 = GudFile(path) dicA = gf.__dict__ diff --git a/gudpy/test/test_gudpy_io.py b/gudpy/test/test_gudpy_io.py index 591a62740..3cb60eb54 100644 --- a/gudpy/test/test_gudpy_io.py +++ b/gudpy/test/test_gudpy_io.py @@ -22,9 +22,10 @@ from core.enums import ( CrossSectionSource, FTModes, Instruments, Scales, UnitsOfDensity, MergeWeights, NormalisationType, OutputUnits, - Geometry, Format + Geometry ) from core import gudpy as gp +from core.io.gudpy_io import GudPyIO class GudPyContext: @@ -37,17 +38,15 @@ def __init__(self): self.gudpy = gp.GudPy() - self.gudpy.loadFromFile( + self.gudpy.loadFromGudrunFile( loadFile=path, - format=Format.TXT ) gPath = os.path.join(self.tempdir.name, "good_water.txt") - self.gudpy.gudrunFile.write_out(gPath, overwrite=True) + GudPyIO.writeGudrunFile(gPath) - self.gudpy.loadFromFile( + self.gudpy.loadFromGudrunFile( loadFile=gPath, - format=Format.TXT ) self.gudpy.setSaveLocation(os.path.join( @@ -806,10 +805,9 @@ def testLoadGudrunFile(self): def testWriteGudrunFile(self): with GudPyContext() as gudpy: - gudpy.gudrunFile.write_out( - gudpy.gudrunFile.loadFile, overwrite=True) + GudPyIO.writeGudrunFile(gudpy.io.loadFile) with open( - gudpy.gudrunFile.loadFile, + gudpy.io.loadFile, encoding="utf-8" ) as f: outlines = "\n".join(f.readlines()[:-5]) @@ -872,7 +870,7 @@ def valueInLines(value, lines): else: valueInLines(value, outlines) inlines = "" - with open(gudpy.gudrunFile.loadFile, encoding="utf-8") as f: + with open(gudpy.io.loadFile, encoding="utf-8") as f: inlines = f.read() for dic in self.dicts: for value in dic.values(): @@ -896,19 +894,15 @@ def valueInLines(value, lines): def testRewriteGudrunFile(self): with GudPyContext() as gudpy: - gudpy.gudrunFile.write_out( - gudpy.gudrunFile.loadFile, overwrite=True) + GudPyIO.writeGudrunFile(gudpy.io.loadFile) copyPath = os.path.join( gudpy.gudrunFile.instrument.GudrunInputFileDir, "copyGF.txt" ) - g1 = GudrunFile( - loadFile=gudpy.gudrunFile.loadFile, - format=Format.TXT - ) + g1 = gudpy.io.importGudrunFile(gudpy.io.loadFile) g1.instrument.GudrunInputFileDir = ( gudpy.gudrunFile.instrument.GudrunInputFileDir) - g1.write_out(copyPath, overwrite=True) + GudPyIO.writeGudrunFile(copyPath) def compareString(string1, string2): return string1 == string2 @@ -937,7 +931,7 @@ def compareString(string1, string2): with open( os.path.join( - gudpy.gudrunFile.loadFile + gudpy.io.loadFile ), encoding="utf-8" ) as fg: @@ -950,12 +944,8 @@ def compareString(string1, string2): def testReloadGudrunFile(self): with GudPyContext() as gudpy: - gudpy.gudrunFile.write_out( - gudpy.gudrunFile.loadFile, overwrite=True) - g1 = GudrunFile( - loadFile=gudpy.gudrunFile.loadFile, - format=Format.TXT - ) + GudPyIO.writeGudrunFile(gudpy.io.loadFile) + g1 = gudpy.io.importGudrunFile() g1.instrument.GudrunInputFileDir = ( gudpy.gudrunFile.instrument.GudrunInputFileDir) self.assertEqual( @@ -967,7 +957,7 @@ def testLoadEmptyGudrunFile(self): f = open("test_data.txt", "w", encoding="utf-8") f.close() with self.assertRaises(ParserException) as cm: - GudrunFile(loadFile="test_data.txt", format=Format.TXT) + GudPyIO.importGudrunFile("test_data.txt") self.assertEqual(( 'INSTRUMENT, BEAM and NORMALISATION' ' were not parsed. It\'s possible the file' @@ -987,7 +977,7 @@ def testLoadMissingInstrument(self): ) with self.assertRaises(ParserException) as cm: - GudrunFile(loadFile="test_data.txt", format=Format.TXT) + GudPyIO.importGudrunFile("test_data.txt") self.assertEqual(( 'INSTRUMENT, BEAM and NORMALISATION' ' were not parsed. It\'s possible the file' @@ -1009,7 +999,7 @@ def testLoadMissingBeam(self): + "\n\n}" ) with self.assertRaises(ParserException) as cm: - GudrunFile(loadFile="test_data.txt", format=Format.TXT) + GudPyIO.importGudrunFile("test_data.txt") self.assertEqual(( 'INSTRUMENT, BEAM and NORMALISATION' ' were not parsed. It\'s possible the file' @@ -1028,7 +1018,7 @@ def testLoadMissingNormalisation(self): f.write("BEAM {\n\n" + str(self.goodBeam) + "\n\n}\n\n") with self.assertRaises(ParserException) as cm: - GudrunFile(loadFile="test_data.txt", format=Format.TXT) + GudPyIO.importGudrunFile("test_data.txt") self.assertEqual(( 'INSTRUMENT, BEAM and NORMALISATION' ' were not parsed. It\'s possible the file' @@ -1047,7 +1037,7 @@ def testLoadMissingInstrumentAndBeam(self): ) with self.assertRaises(ParserException) as cm: - GudrunFile(loadFile="test_data.txt", format=Format.TXT) + GudPyIO.importGudrunFile("test_data.txt") self.assertEqual(( 'INSTRUMENT, BEAM and NORMALISATION' ' were not parsed. It\'s possible the file supplied' @@ -1061,7 +1051,7 @@ def testLoadMissingInstrumentAndNormalisation(self): f.write("BEAM {\n\n" + str(self.goodBeam) + "\n\n}") with self.assertRaises(ParserException) as cm: - GudrunFile(loadFile="test_data.txt", format=Format.TXT) + GudPyIO.importGudrunFile("test_data.txt") self.assertEqual(( 'INSTRUMENT, BEAM and NORMALISATION' ' were not parsed. It\'s possible the file' @@ -1078,7 +1068,7 @@ def testLoadMissingNormalisationAndBeam(self): ) with self.assertRaises(ParserException) as cm: - GudrunFile(loadFile="test_data.txt", format=Format.TXT) + GudPyIO.importGudrunFile("test_data.txt") self.assertEqual(( 'INSTRUMENT, BEAM and NORMALISATION' ' were not parsed. It\'s possible the file' @@ -1099,7 +1089,6 @@ def testLoadMissingInstrumentAttributesSeq(self): expectedInstrument.pop("goodDetectorThreshold", None) expectedInstrument.pop("yamlignore", None) for i in range(len(expectedInstrument.keys())): - badInstrument = str(self.goodInstrument).split("\n") del badInstrument[i] badInstrument = "\n".join(badInstrument) @@ -1111,7 +1100,7 @@ def testLoadMissingInstrumentAttributesSeq(self): ) with self.assertRaises(ParserException) as cm: - GudrunFile(loadFile="test_data.txt", format=Format.TXT) + GudPyIO.importGudrunFile("test_data.txt") self.assertEqual( "Whilst parsing Instrument, an exception occured." " The input file is most likely of an incorrect format, " @@ -1145,7 +1134,7 @@ def testLoadMissingInstrumentAttributesRand(self): "INSTRUMENT {\n\n" + str(badInstrument) + "\n\n}" ) with self.assertRaises(ParserException) as cm: - GudrunFile(loadFile="test_data.txt", format=Format.TXT) + GudPyIO.importGudrunFile("test_data.txt") self.assertEqual( "Whilst parsing Instrument, an exception occured." " The input file is most likely of an incorrect format, " @@ -1180,7 +1169,7 @@ def testLoadMissingBeamAttributesSeq(self): f.write("\n\nBEAM {\n\n" + str(badBeam) + "\n\n}") with self.assertRaises(ParserException) as cm: - GudrunFile(loadFile="test_data.txt", format=Format.TXT) + GudPyIO.importGudrunFile("test_data.txt") self.assertEqual( "Whilst parsing Beam, an exception occured." " The input file is most likely of an incorrect format, " @@ -1219,7 +1208,7 @@ def testLoadMissingBeamAttributesRand(self): f.write("\n\nBEAM {\n\n" + str(badBeam) + "\n\n}") with self.assertRaises(ParserException) as cm: - GudrunFile(loadFile="test_data.txt", format=Format.TXT) + GudPyIO.importGudrunFile("test_data.txt") self.assertEqual( "Whilst parsing Beam, an exception occured." " The input file is most likely of an incorrect format, " @@ -1244,7 +1233,6 @@ def testLoadMissingNormalisationAttributesSeq(self): Composition("") ) for i in range(len(expectedNormalisation.keys())): - badNormalisation = str(self.goodNormalisation).split("\n") del badNormalisation[i] badNormalisation = "\n".join(badNormalisation) @@ -1264,7 +1252,7 @@ def testLoadMissingNormalisationAttributesSeq(self): ) with self.assertRaises(ParserException) as cm: - GudrunFile(loadFile="test_data.txt", format=Format.TXT) + GudPyIO.importGudrunFile("test_data.txt") self.assertEqual( "Whilst parsing Beam, an exception occured." " The input file is most likely of an incorrect format, " @@ -1289,7 +1277,6 @@ def testLoadMissingNormalisationAttributesRand(self): Composition("") ) for i in range(50): - key = random.choice(list(expectedNormalisation)) j = list(expectedNormalisation).index(key) @@ -1313,7 +1300,7 @@ def testLoadMissingNormalisationAttributesRand(self): ) with self.assertRaises(ParserException) as cm: - GudrunFile(loadFile="test_data.txt", format=Format.TXT) + GudPyIO.importGudrunFile("test_data.txt") self.assertEqual( "Whilst parsing Normalisation, an exception occured." " The input file is most likely of an incorrect format, " @@ -1338,7 +1325,7 @@ def testLoadMissingSampleBackgroundAttributes(self): ) f.write("\n\n{}\n\nEND".format(str(badSampleBackground))) with self.assertRaises(ParserException) as cm: - GudrunFile(loadFile="test_data.txt", format=Format.TXT) + GudPyIO.importGudrunFile("test_data.txt") self.assertEqual( "Whilst parsing Sample Background, an exception occured." " The input file is most likely of an incorrect format, " @@ -1393,7 +1380,7 @@ def testLoadMissingSampleAttributesSeq(self): f.write("\n\n{}\n\nEND".format(str(badSampleBackground))) with self.assertRaises(ParserException) as cm: - GudrunFile(loadFile="test_data.txt", format=Format.TXT) + GudPyIO.importGudrunFile("test_data.txt") self.assertEqual( "Whilst parsing Sample, an exception occured." " The input file is most likely of an incorrect format, " @@ -1451,7 +1438,7 @@ def testLoadMissingSampleAttributesRand(self): f.write("\n\n{}\n\nEND".format(str(badSampleBackground))) with self.assertRaises(ParserException) as cm: - GudrunFile(loadFile="test_data.txt", format=Format.TXT) + GudPyIO.importGudrunFile("test_data.txt") self.assertEqual( "Whilst parsing Sample, an exception occured." " The input file is most likely of an incorrect format, " @@ -1511,7 +1498,7 @@ def testLoadMissingContainerAttributesSeq(self): ) f.write("\n\n{}\n\nEND".format(str(badSampleBackground))) with self.assertRaises(ParserException) as cm: - GudrunFile(loadFile="test_data.txt", format=Format.TXT) + GudPyIO.importGudrunFile("test_data.txt") self.assertEqual( "Whilst parsing Container, an exception occured." " The input file is most likely of an incorrect format, " @@ -1524,12 +1511,14 @@ def testAppendExponentialValues(self): # Remove last element of exponential values list gudpy.gudrunFile.sampleBackgrounds[ 0].samples[0].exponentialValues[0].pop() - gudpy.gudrunFile.write_out() - gudrunFile = GudrunFile(loadFile=os.path.join( - gudpy.gudrunFile.instrument.GudrunInputFileDir, - gudpy.gudrunFile.OUTPATH - ), format=Format.TXT) + gfFilePath = os.path.join( + gudpy.io.projectDir, + gudpy.io.gudrunFileParser.OUTPATH + ) + + gudpy.io.writeGudrunFile(gfFilePath) + gudrunFile = GudPyIO.importGudrunFile(gfFilePath) # Test that a default value is appended self.assertEqual( self.expectedSampleA["exponentialValues"], @@ -1591,7 +1580,7 @@ def testLoadMissingContainerAttributesRand(self): ) f.write("\n\n{}\n\nEND".format(str(badSampleBackground))) with self.assertRaises(ParserException) as cm: - GudrunFile(loadFile="test_data.txt", format=Format.TXT) + GudPyIO.importGudrunFile("test_data.txt") self.assertEqual( "Whilst parsing Container, an exception occured." " The input file is most likely of an incorrect format, " @@ -1601,9 +1590,8 @@ def testLoadMissingContainerAttributesRand(self): def testZeroExitGudrun(self): with GudPyContext() as gudpy: - gudpy.loadFromFile( - loadFile=gudpy.gudrunFile.loadFile, format=Format.TXT) + gudpy.loadFromGudrunFile(loadFile=gudpy.io.loadFile) gudpy.setSaveLocation(os.path.splitext( - gudpy.gudrunFile.loadFile)[0]) + gudpy.io.loadFile)[0]) gudpy.runGudrun() self.assertEqual(gudpy.gudrun.exitcode, 0) diff --git a/gudpy/test/test_purge_file.py b/gudpy/test/test_purge_file.py index cf138845e..496d39bd8 100644 --- a/gudpy/test/test_purge_file.py +++ b/gudpy/test/test_purge_file.py @@ -1,10 +1,12 @@ import os from unittest import TestCase from shutil import copyfile +import tempfile from core.purge_file import PurgeFile from core.enums import Format from core import gudpy +from core.io.gudpy_io import GudPyIO class TestPurgeFile(TestCase): @@ -28,9 +30,8 @@ def setUp(self) -> None: loadFile="test/TestData/NIMROD-water/good_water.txt", format=Format.TXT) - self.gudpy.gudrunFile.write_out( - path="test/TestData/NIMROD-water/good_water.txt", - overwrite=True + self.gudpy.loadFromGudrunFile( + path="test/TestData/NIMROD-water/good_water.txt" ) self.g = self.gudpy.gudrunFile self.expectedPurgeFile = { @@ -42,7 +43,6 @@ def setUp(self) -> None: return super().setUp() def tearDown(self) -> None: - [os.remove(f) for f in os.listdir() if f not in self.keepsakes] return super().tearDown() @@ -58,9 +58,10 @@ def testCreatePurgeClass(self): ) def testWritePurgeFile(self): - - purge = PurgeFile(self.g) - purge.write_out() - with open("purge_det.dat", encoding="utf-8") as f: - outlines = f.read() - self.assertEqual(outlines, str(purge)) + with tempfile.TemporaryDirectory() as tmp: + os.chdir(tmp) + purge = PurgeFile(self.g) + GudPyIO.writeObject(purge, "purge_det.dat") + with open("purge_det.dat", encoding="utf-8") as f: + outlines = f.read() + self.assertEqual(outlines, str(purge)) From af58312cfb4372b07b90ffc0c34d895d8af5de9b Mon Sep 17 00:00:00 2001 From: Noella Spitz Date: Wed, 15 May 2024 15:29:01 +0100 Subject: [PATCH 05/78] Continue refactoring --- gudpy/__init__.py | 0 gudpy/core/file_library.py | 7 +- gudpy/core/gudpy.py | 19 ++--- gudpy/core/gudrun_file.py | 33 ++++---- gudpy/core/io/gudpy_io.py | 80 +++++++++++------- .../{txt_parser.py => gudrun_file_parser.py} | 84 ++++++++++--------- gudpy/core/output_file_handler.py | 10 +-- gudpy/gui/widgets/core/control.py | 5 +- gudpy/gui/widgets/dialogs/io.py | 8 +- .../NIMROD-water/good_water/good_water.yaml | 28 +++---- gudpy/test/test_gudpy_io.py | 2 +- gudpy/test/test_purge_file.py | 20 ++--- 12 files changed, 145 insertions(+), 151 deletions(-) delete mode 100644 gudpy/__init__.py rename gudpy/core/io/{txt_parser.py => gudrun_file_parser.py} (95%) diff --git a/gudpy/__init__.py b/gudpy/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/gudpy/core/file_library.py b/gudpy/core/file_library.py index 65871a45e..c8feb0a2c 100644 --- a/gudpy/core/file_library.py +++ b/gudpy/core/file_library.py @@ -167,15 +167,10 @@ def checkFilesExist(self): def exportMintData( self, samples, + exportTo="", renameDataFiles=False, - exportTo=None, includeParams=False, ): - if not exportTo: - exportTo = os.path.join( - self.gudrunFile.projectDir, - Path(self.gudrunFile.path()).stem + ".zip", - ) with ZipFile(exportTo, "w", ZIP_DEFLATED) as zipFile: for sample in samples: if len(sample.dataFiles.dataFiles): diff --git a/gudpy/core/gudpy.py b/gudpy/core/gudpy.py index 2df8bf25e..c85e74e64 100644 --- a/gudpy/core/gudpy.py +++ b/gudpy/core/gudpy.py @@ -17,6 +17,7 @@ from core.file_library import GudPyFileLibrary from core import data_files from core.io.gudpy_io import GudPyIO +from core.io.gudrun_file_parser import GudrunFileParser SUFFIX = ".exe" if os.name == "nt" else "" @@ -74,9 +75,6 @@ def loadFromGudrunFile( self.gudrunFile = self.io.importGudrunFile(loadFile, config) - self.originalGudrunFile = copy.deepcopy(self.gudrunFile) - self.originalGudrunFile.filename = "original" - def loadFromProject(self, projectDir: str): """Loads GudPy from a project directory @@ -408,7 +406,7 @@ def purge(self, purgeFile: PurgeFile): return self.exitcode self.purgeLocation = self.organiseOutput( - tmp, purgeFile.gudrunFile.projectDir) + tmp, GudPyIO.projectDir) self.exitcode = 0 return self.exitcode @@ -465,7 +463,6 @@ def gudrun( tmp, gudrunFile.OUTPATH ) - assert (gudrunFile.path() is not None) GudPyIO.writeGudrunFile(path) with subprocess.Popen( [self.BINARY_PATH, path], cwd=tmp, @@ -489,13 +486,7 @@ def gudrun( self.gudrunOutput = self.organiseOutput( gudrunFile, exclude=purgeFiles) if save: - gudrunFile.save( - path=os.path.join( - gudrunFile.projectDir, - f"{gudrunFile.filename}" - ), - format=enums.Format.YAML - ) + GudPyIO.save() gudrunFile.setGudrunDir(self.gudrunOutput.path) self.exitcode = 0 @@ -893,8 +884,8 @@ def bactchProcess( batchSize: int, iterator: iterators.Iterator = None, ): - self.batchedGudrunFile.projectDir = (os.path.join( - self.batchedGudrunFile.projectDir, + self.batchedGudrunFile.outputFolder = (os.path.join( + GudPyIO.projectDir, f"BATCH_PROCESSING_BATCH_SIZE{batchSize}" )) exitcode = gudrun.gudrun(GudrunFile, purge, iterator) diff --git a/gudpy/core/gudrun_file.py b/gudpy/core/gudrun_file.py index 8937f8a0a..5dfdd1c3c 100644 --- a/gudpy/core/gudrun_file.py +++ b/gudpy/core/gudrun_file.py @@ -14,6 +14,7 @@ class GudrunFile: + OUTPATH = "gudpy.txt" """ Class to represent a GudFile (files with .gud extension). .gud files are outputted by gudrun_dcs, via merge_routines @@ -80,18 +81,11 @@ class GudrunFile: def __init__( self, - params: tuple[ - Instrument, - Normalisation, - list[SampleBackground], - Components - ] = ( - Instrument(), - Beam(), - Normalisation(), - [], - Components() - ) + instrument=Instrument(), + beam=Beam(), + normalisation=Normalisation(), + sampleBackgrounds: list[SampleBackground] = [], + components=Components() ): """ Constructs all the necessary attributes for the GudrunFile object. @@ -109,13 +103,16 @@ def __init__( config : bool If a new input file should be constructed from a config """ - inst, norm, beam, sbg, comp = params - - self.instrument = inst - self.normalisation = norm + self.instrument = instrument + self.normalisation = normalisation self.beam = beam - self.sampleBackgrounds = sbg - self.components = comp + self.sampleBackgrounds = sampleBackgrounds + self.components = components + self.purged = False + + @property + def path(self): + return os.path.join(self.outputFolder, self.OUTPATH) def __deepcopy__(self, memo): result = self.__class__.__new__(self.__class__) diff --git a/gudpy/core/io/gudpy_io.py b/gudpy/core/io/gudpy_io.py index a48b2183a..4dfc6282d 100644 --- a/gudpy/core/io/gudpy_io.py +++ b/gudpy/core/io/gudpy_io.py @@ -4,6 +4,7 @@ from ruamel.yaml import YAMLError import shutil import os +import typing as typ from core.composition import ( Component, Components, Composition, WeightedComponent @@ -20,28 +21,28 @@ from core.sample import Sample from core.container import Container from core.gudrun_file import GudrunFile -from core.io.txt_parser import GudrunFileParser +from core.io.gudrun_file_parser import GudrunFileParser from core import config class GudPyIO: - def __init__(self): - self.projectDir = "" - self.loadFile = "" + projectDir = "" + loadFile = "" + def __init__(self): self.yaml = self._getYamlModule() self.gudrunFileParser = GudrunFileParser() - @property - def projectName(self) -> str: - return os.path.dirname(self.projectDir) + @classmethod + def projectName(cls) -> str: + return os.path.dirname(cls.projectDir) - @property - def autosavePath(self) -> str: - return f"{os.path.basename(self.projectDir)}.autosave" + @classmethod + def autosavePath(cls) -> str: + return f"{os.path.basename(cls.projectDir)}.autosave" - @property - def path(self): + @classmethod + def path(cls): """Returns the path to the yaml file Returns @@ -54,11 +55,12 @@ def path(self): RuntimeError Raised if project directory is not set """ - if not self.projectDir: + if not cls.projectDir: raise RuntimeError("Save location not set.") - return os.path.join(self.projectDir, f"{self.projectName}.yaml") + return os.path.join(cls.projectDir, f"{cls.projectName()}.yaml") - def setSaveLocation(self, projectDir: str): + @classmethod + def setSaveLocation(cls, projectDir: str): """Sets the save location/project directory Parameters @@ -66,20 +68,21 @@ def setSaveLocation(self, projectDir: str): projectDir : str Path to new save location """ - self.projectDir = projectDir + cls.projectDir = projectDir - def checkSaveLocation(self) -> bool: + @classmethod + def checkSaveLocation(cls) -> bool: """Checks if user has set the save location """ - return bool(self.projectDir) + return bool(cls.projectDir) def save(self): """Saves yaml file to project directory """ - self.writeYaml(self.path) + self.writeYaml(self.path()) def importGudrunFile(self, path, config=False) -> GudrunFile: - self.loadFile = path + GudPyIO.loadFile = path return self.gudrunFileParser.parse(path, config) def importProject(self, projectDir) -> GudrunFile: @@ -135,7 +138,7 @@ def exportProject(self, targetDir): os.makedirs(targetDir) shutil.copytree(oldDir, targetDir) - os.rename(oldFile, self.path) + os.rename(oldFile, self.path()) @classmethod def exportGudrunFile(cls, path): @@ -165,8 +168,9 @@ def _getYamlModule(self) -> yaml: def _parseYaml(self, path) -> GudrunFile: self.loadFile = path yamldict = self._yamlToDict(path) - gudrunFile = GudrunFile(loadFile=path) + gudrunFile = GudrunFile() for k, v in yamldict.items(): + k = k.lower() if k == "GUI": GUI = GUIConfig() self._yamlToObject(GUI, v) @@ -188,16 +192,19 @@ def _parseYaml(self, path) -> GudrunFile: except YAMLError as e: # Exception caused by yaml parsing library raise YAMLException(f"Invalid YAML file: {str(e)}") + except Exception as e: + print(f"Failed at: {k}") + raise e return gudrunFile def _writeYAML(self, gudrunFile: GudrunFile, path: str) -> None: with open(path, "wb") as fp: outyaml = { - "Instrument": gudrunFile.instrument, - "Beam": gudrunFile.beam, - "Components": gudrunFile.components.components, - "Normalisation": gudrunFile.normalisation, - "SampleBackgrounds": gudrunFile.sampleBackgrounds, + "instrument": gudrunFile.instrument, + "beam": gudrunFile.beam, + "components": gudrunFile.components.components, + "normalisation": gudrunFile.normalisation, + "sampleBackgrounds": gudrunFile.sampleBackgrounds, "GUI": config.GUI } self.yaml.dump( @@ -205,7 +212,7 @@ def _writeYAML(self, gudrunFile: GudrunFile, path: str) -> None: fp ) - def _parsingFuncMap(self, type: type) -> typ.Callable | None: + def _parsingFuncMap(self, type: type) -> typ.Union[typ.Callable, None]: parsingFuncs = { Instrument: self._parseYamlInstrument, Beam: self._parseYamlBeam, @@ -241,7 +248,17 @@ def _assignYamlToAttr(self, obj: any, key, val) -> None: f"given to '{type(obj).__name__}'" ) return - setattr(obj, key, type(obj.__dict__[key])(self.toBuiltin(val))) + + try: + if isinstance(obj.__dict__[key], Enum): + setattr(obj, key, type(obj.__dict__[key])[val]) + return + + setattr(obj, key, type(obj.__dict__[key])(self.toBuiltin(val))) + except Exception as e: + raise YAMLException( + "Parsing failed while trying to assign attribute" + f"'{key}' to {type(obj).__name__}") from e def _parseYamlInstrument(self, yamldict: any) -> Instrument: instrument = Instrument() @@ -259,6 +276,9 @@ def _parseYamlNormalisation(self, yamldict: any) -> Normalisation: if k == "composition": composition = self._parseYamlComposition(yamldict[k]) setattr(normalisation, k, composition) + if k == "dataFiles": + dataFiles = self._parseYamlDataFiles(yamldict[k]) + setattr(normalisation, k, dataFiles) else: self._assignYamlToAttr(normalisation, k, v) return normalisation @@ -323,7 +343,7 @@ def _parseYamlContainer(self, yamldict: any) -> Container: return container def _parseYamlDataFiles(self, yamldict: any) -> DataFiles: - dataFiles = DataFiles() + dataFiles = DataFiles([], yamldict["name"]) for df in yamldict["_dataFiles"]: dataFile = DataFile(df["filename"]) dataFile = self._yamlToObject(dataFile, df) diff --git a/gudpy/core/io/txt_parser.py b/gudpy/core/io/gudrun_file_parser.py similarity index 95% rename from gudpy/core/io/txt_parser.py rename to gudpy/core/io/gudrun_file_parser.py index 50427c24e..6afeac82a 100644 --- a/gudpy/core/io/txt_parser.py +++ b/gudpy/core/io/gudrun_file_parser.py @@ -23,11 +23,11 @@ class GudrunFileParser: """Class to manage the parsing of Gudrun input files """ - OUTPATH = "gudpy.txt" def __init__(self): # Text stream self.stream = None + self.config = False @classmethod def writeGudrunFile(cls, gudrunFile: GudrunFile, runDir: str): @@ -35,7 +35,7 @@ def writeGudrunFile(cls, gudrunFile: GudrunFile, runDir: str): for sampleBackground in gudrunFile.sampleBackgrounds: sampleBackground.writeAllSamples = False - writePath = os.path.join(runDir, GudrunFileParser.OUTPATH) + writePath = os.path.join(runDir, gudrunFile.OUTPATH) with open(writePath, "w", encoding="utf-8") as f: f.write(str(gudrunFile)) @@ -66,6 +66,8 @@ def parse( normalisation = None sampleBackgrounds = [] + self.config = config + # Ensure only valid files are given. if not path: raise ParserException( @@ -94,22 +96,27 @@ def parse( # Iterate through the file, # parsing the Instrument, Beam and Normalisation. - while self.stream: - line = utils.firstword(line) - if line == "INSTRUMENT": - instrument = self._parseInstrument(line) - KEYWORDS[line] = True - elif line == "BEAM": - beam = self._parseBeam(line) - KEYWORDS[line] = True - elif line == "NORMALISATION": - normalisation = self._parseNormalisation(line) - KEYWORDS[line] = True + while (self.stream and not all(value for value in KEYWORDS.values())): + line = self._getNextToken() + kwd = utils.firstword(line) + if kwd == "INSTRUMENT": + instrument = self._parseInstrument() + KEYWORDS[kwd] = True + elif kwd == "BEAM": + beam = self._parseBeam() + KEYWORDS[kwd] = True + elif kwd == "NORMALISATION": + normalisation = self._parseNormalisation() + KEYWORDS[kwd] = True # If we didn't parse each one of the keywords, then panic. if not all(KEYWORDS.values()) and not config: + unparsed = [] + for k, v in KEYWORDS.items(): + if not v: + unparsed.append(k) raise ParserException(( - 'INSTRUMENT, BEAM and NORMALISATION' + f'{unparsed}' ' were not parsed. It\'s possible the file' ' supplied is of an incorrect format!' )) @@ -134,26 +141,24 @@ def parse( line = self._getNextToken() return GudrunFile( - loadFile=path, - params=( - instrument, - beam, - normalisation, - sampleBackgrounds, - components - )) + instrument=instrument, + beam=beam, + normalisation=normalisation, + sampleBackgrounds=sampleBackgrounds, + components=components + ) # ================================================================= # PARSING HELPERS # ================================================================= - def _getNextToken(self) -> str | None: + def _getNextToken(self) -> typ.Union[str, None]: """ Pops the 'next token' from the stream and returns it. Essentially removes the first line in the stream and returns it. """ return self.stream.pop(0) if self.stream else None - def _peekNextToken(self) -> str | None: + def _peekNextToken(self) -> typ.Union[str, None]: """ Returns the next token in the input stream, without removing it. """ @@ -436,7 +441,7 @@ def _parseBeam(self) -> Beam: # For N float attributes, # we extract the first N floats from the line. beam.beamProfileValues = ( - enums.extract_floats_from_string(self._getNextToken()) + utils.extract_floats_from_string(self._getNextToken()) ) # For single float attributes, @@ -504,6 +509,7 @@ def _parseBeam(self) -> Beam: "Whilst parsing Beam, an exception occured." " The input file is most likely of an incorrect format, " "and some attributes were missing." + f"{e}" ) from e def _parseNormalisation(self) -> Normalisation: @@ -704,7 +710,7 @@ def _parseNormalisation(self) -> Normalisation: "and some attributes were missing." ) from e - def _parseSampleBackground(self, _) -> SampleBackground: + def _parseSampleBackground(self) -> SampleBackground: """ Intialises a SampleBackground object. Parses the attributes of the SampleBackground from the input stream. @@ -748,7 +754,7 @@ def _parseSampleBackground(self, _) -> SampleBackground: "and some attributes were missing." ) from e - def _parseSample(self, _) -> Sample: + def _parseSample(self) -> Sample: """ Intialises a Sample object. Parses the attributes of the Sample from the input stream. @@ -875,13 +881,13 @@ def _parseSample(self, _) -> Sample: topHatW = utils.nthfloat(self._getNextToken(), 0) if topHatW == 0: sample.topHatW = 0 - sample.FTMode = FTModes.NO_FT + sample.FTMode = enums.FTModes.NO_FT elif topHatW < 0: sample.topHatW = abs(topHatW) - sample.FTMode = FTModes.SUB_AVERAGE + sample.FTMode = enums.FTModes.SUB_AVERAGE else: sample.topHatW = topHatW - sample.FTMode = FTModes.ABSOLUTE + sample.FTMode = enums.FTModes.ABSOLUTE sample.minRadFT = utils.nthfloat(self._getNextToken(), 0) sample.grBroadening = utils.nthfloat(self._getNextToken(), 0) @@ -897,7 +903,7 @@ def _parseSample(self, _) -> Sample: not in line ): sample.resonanceValues.append( - enums.extract_floats_from_string(line) + utils.extract_floats_from_string(line) ) line = self._getNextToken() @@ -952,7 +958,7 @@ def _parseSample(self, _) -> Sample: "and some attributes were missing." ) from e - def _parseContainer(self, _) -> Container: + def _parseContainer(self) -> Container: """ Intialises a Container object. Parses the attributes of the Container from the input stream. @@ -1135,7 +1141,7 @@ def _sampleBackgroundHelper(self) -> SampleBackground: """ # Parse sample background. - sampleBackground = self.makeParse("SAMPLE BACKGROUND") + sampleBackground = self._parseSampleBackground() self._consumeWhitespace() line = self._peekNextToken() @@ -1147,7 +1153,7 @@ def _sampleBackgroundHelper(self) -> SampleBackground: elif "GO" in line: self._getNextToken() elif "SAMPLE" in line and utils.firstword(line) == "SAMPLE": - sample = self.makeParse("SAMPLE") + sample = self._parseSample() if not sample.name: sample.name = utils.replace_unwanted_chars( utils.uniquifyName( @@ -1158,7 +1164,7 @@ def _sampleBackgroundHelper(self) -> SampleBackground: )) sampleBackground.samples.append(sample) elif "CONTAINER" in line and utils.firstword(line) == "CONTAINER": - container = self.makeParse("CONTAINER") + container = self._parseContainer() if not container.name: container.name = utils.uniquifyName( "CONTAINER", @@ -1174,7 +1180,7 @@ def _sampleBackgroundHelper(self) -> SampleBackground: return sampleBackground -Container._getNextToken = TxtParser._getNextToken -Container._peekNextToken = TxtParser._peekNextToken -Container._consumeUpToDelim = TxtParser._consumeUpToDelim -Container._consumeWhitespace = TxtParser._consumeWhitespace +Container._getNextToken = GudrunFileParser._getNextToken +Container._peekNextToken = GudrunFileParser._peekNextToken +Container._consumeUpToDelim = GudrunFileParser._consumeUpToDelim +Container._consumeWhitespace = GudrunFileParser._consumeWhitespace diff --git a/gudpy/core/output_file_handler.py b/gudpy/core/output_file_handler.py index 243016e1f..429a10eca 100644 --- a/gudpy/core/output_file_handler.py +++ b/gudpy/core/output_file_handler.py @@ -7,6 +7,7 @@ import core.utils as utils from core.gud_file import GudFile from core.gudrun_file import GudrunFile +from core.io.gudpy_io import GudPyIO @dataclass @@ -127,7 +128,7 @@ def __init__( super().__init__( gudrunFile.instrument.GudrunInputFileDir, - gudrunFile.projectDir, + GudPyIO.projectDir, "Gudrun", ) @@ -140,6 +141,7 @@ def __init__( # Directory where Gudrun files are outputted (temp) self.gudrunDir = self.procDir self.gudrunFile = gudrunFile + self.gudrunFile.outputFolder = self.outputDir # Make sure it is a temporary directory assert (self.gudrunDir.startswith(tempfile.gettempdir())) @@ -176,11 +178,9 @@ def organiseOutput(self, exclude: list[str] = []): inputFilePath = self._createAddOutDir(self.tempOutDir, exclude) # If overwrite, move previous directory - if self.overwrite and os.path.exists( - os.path.join(self.gudrunFile.projectDir, "Gudrun")): + if self.overwrite and os.path.exists(self.outputDir): with tempfile.TemporaryDirectory() as tmp: - shutil.move(os.path.join(self.gudrunFile.projectDir, "Gudrun"), - os.path.join(tmp, "prev")) + shutil.move(self.outputDir, os.path.join(tmp, "prev")) # Move over folders to output directory shutil.move(self.tempOutDir, utils.uniquify(self.outputDir)) diff --git a/gudpy/gui/widgets/core/control.py b/gudpy/gui/widgets/core/control.py index 2bebd34bf..51c1230db 100644 --- a/gudpy/gui/widgets/core/control.py +++ b/gudpy/gui/widgets/core/control.py @@ -31,7 +31,6 @@ def __init__(self): super().__init__() self.gudpy: gp.GudPy = gp.GudPy() self.mainWidget: QtWidgets.QMainWindow = GudPyMainWindow() - self.purged: bool = False # Current process thread running self.workerThread: QtCore.QThread = None @@ -396,7 +395,7 @@ def startProcess(self) -> None: os.path.join( self.gudpy.projectDir, "Purge", "purge_det.dat" ) - ): + ) or not self.gudpy.gudrunFile.purged: purgeResult = self.mainWidget.purgeOptionsMessageBox( "purge_det.dat found, but wasn't run in this session. " "Run Purge?", @@ -424,7 +423,7 @@ def runPurge(self) -> bool: self.startProcess() def purgeFinished(self, exitcode): - self.purged = True + self.gudpy.gudrunFile.purged = True if exitcode != 0: self.mainWidget.sendError( diff --git a/gudpy/gui/widgets/dialogs/io.py b/gudpy/gui/widgets/dialogs/io.py index 1a3407819..9285ebdc3 100644 --- a/gudpy/gui/widgets/dialogs/io.py +++ b/gudpy/gui/widgets/dialogs/io.py @@ -95,8 +95,6 @@ class ExportDialog(QDialog): Toggles renaming files to the sample name. performExport(filename) Performs an export to a filename. - export() - Performs a standard export. exportAs() Allows exporting to a specific file. """ @@ -174,7 +172,7 @@ def loadFilesList(self, rename=False): def toggleRename(self, state): self.loadFilesList(rename=bool(state)) - def performExport(self, filename=None): + def performExport(self, filename): fl = GudPyFileLibrary(self.gudrunFile) archive = fl.exportMintData( self.gudrunFile.samples(), @@ -189,11 +187,7 @@ def performExport(self, filename=None): ) self.widget.close() - def export(self): - self.performExport() - def exportAs(self): - dialog = QFileDialog() dialog.setDefaultSuffix("zip") dialog.setWindowTitle("Export to..") diff --git a/gudpy/test/TestData/NIMROD-water/good_water/good_water.yaml b/gudpy/test/TestData/NIMROD-water/good_water/good_water.yaml index 17c962596..075d507d8 100644 --- a/gudpy/test/TestData/NIMROD-water/good_water/good_water.yaml +++ b/gudpy/test/TestData/NIMROD-water/good_water/good_water.yaml @@ -1,4 +1,4 @@ -Instrument: +instrument: name: NIMROD dataFileDir: TestData/NIMROD-water/raw/ dataFileType: raw @@ -34,7 +34,7 @@ Instrument: logarithmicStepSize: 0.04 hardGroupEdges: true nxsDefinitionFile: '' -Beam: +beam: sampleGeometry: FLATPLATE beamProfileValues: [1.0, 1.0] stepSizeAbsorption: 0.05 @@ -53,15 +53,15 @@ Beam: overallBackgroundFactor: 1.0 sampleDependantBackgroundFactor: 0.0 shieldingAttenuationCoefficient: 0.0 -Components: [] -Normalisation: +components: [] +normalisation: periodNumber: 1 dataFiles: - dataFiles: [NIMROD00016702_V.raw] + _dataFiles: [NIMROD00016702_V.raw] name: NORMALISATION periodNumberBg: 1 dataFilesBg: - dataFiles: [NIMROD00016698_EmptyInst.raw, NIMROD00016703_EmptyInst.raw] + _dataFiles: [NIMROD00016698_EmptyInst.raw, NIMROD00016703_EmptyInst.raw] name: NORMALISATION BACKGROUND forceCalculationOfCorrections: true composition: @@ -86,7 +86,7 @@ Normalisation: lowerLimitSmoothedNormalisation: 0.01 normalisationDegreeSmoothing: 1.0 minNormalisationSignalBR: 0.0 -SampleBackgrounds: +sampleBackgrounds: - periodNumber: 1 dataFiles: dataFiles: [NIMROD00016698_EmptyInst.raw, NIMROD00016703_EmptyInst.raw] @@ -95,7 +95,7 @@ SampleBackgrounds: - name: H2O,_Can_N9 periodNumber: 1 dataFiles: - dataFiles: [NIMROD00016608_H2O_in_N9.raw, NIMROD00016610_H2O_in_N9.raw] + _dataFiles: [NIMROD00016608_H2O_in_N9.raw, NIMROD00016610_H2O_in_N9.raw] name: H2O,_Can_N9 forceCalculationOfCorrections: true composition: @@ -139,7 +139,7 @@ SampleBackgrounds: - name: N9 periodNumber: 1 dataFiles: - dataFiles: [NIMROD00016694_Empty_N9.raw, NIMROD00016699_Empty_N9.raw, NIMROD00016704_Empty_N9.raw] + _dataFiles: [NIMROD00016694_Empty_N9.raw, NIMROD00016699_Empty_N9.raw, NIMROD00016704_Empty_N9.raw] name: N9 composition: type_: Container @@ -165,7 +165,7 @@ SampleBackgrounds: - name: D2O,_Can_N10 periodNumber: 1 dataFiles: - dataFiles: [NIMROD00016609_D2O_in_N10.raw, NIMROD00016611_D2O_in_N10.raw] + _dataFiles: [NIMROD00016609_D2O_in_N10.raw, NIMROD00016611_D2O_in_N10.raw] name: D2O,_Can_N10 forceCalculationOfCorrections: true composition: @@ -209,7 +209,7 @@ SampleBackgrounds: - name: N10 periodNumber: 1 dataFiles: - dataFiles: [NIMROD00016695_Empty_N10.raw, NIMROD00016700_Empty_N10.raw, NIMROD00016705_Empty_N10.raw] + _dataFiles: [NIMROD00016695_Empty_N10.raw, NIMROD00016700_Empty_N10.raw, NIMROD00016705_Empty_N10.raw] name: N10 composition: type_: Container @@ -235,7 +235,7 @@ SampleBackgrounds: - name: HDO,_Can_N6 periodNumber: 1 dataFiles: - dataFiles: [NIMROD00016741_HDO_in_N6.raw, NIMROD00016743_HDO_in_N6.raw] + _dataFiles: [NIMROD00016741_HDO_in_N6.raw, NIMROD00016743_HDO_in_N6.raw] name: HDO,_Can_N6 forceCalculationOfCorrections: true composition: @@ -280,7 +280,7 @@ SampleBackgrounds: - name: N6 periodNumber: 1 dataFiles: - dataFiles: [NIMROD00014908_Empty_N6.raw] + _dataFiles: [NIMROD00014908_Empty_N6.raw] name: N6 composition: type_: Container @@ -351,7 +351,7 @@ SampleBackgrounds: - name: N8 periodNumber: 1 dataFiles: - dataFiles: [NIMROD00016994_Empty_N8.raw] + _dataFiles: [NIMROD00016994_Empty_N8.raw] name: N8 composition: type_: Container diff --git a/gudpy/test/test_gudpy_io.py b/gudpy/test/test_gudpy_io.py index 3cb60eb54..25d68ec71 100644 --- a/gudpy/test/test_gudpy_io.py +++ b/gudpy/test/test_gudpy_io.py @@ -1514,7 +1514,7 @@ def testAppendExponentialValues(self): gfFilePath = os.path.join( gudpy.io.projectDir, - gudpy.io.gudrunFileParser.OUTPATH + gudpy.gudrunFile.OUTPATH ) gudpy.io.writeGudrunFile(gfFilePath) diff --git a/gudpy/test/test_purge_file.py b/gudpy/test/test_purge_file.py index 496d39bd8..ec7b09919 100644 --- a/gudpy/test/test_purge_file.py +++ b/gudpy/test/test_purge_file.py @@ -4,7 +4,6 @@ import tempfile from core.purge_file import PurgeFile -from core.enums import Format from core import gudpy from core.io.gudpy_io import GudPyIO @@ -20,19 +19,11 @@ def setUp(self) -> None: else: dirpath = ( "/".join(os.path.realpath(__file__).split("/")[:-1]) - + "/" - + path ) self.gudpy = gudpy.GudPy() self.keepsakes = os.listdir() - copyfile(dirpath, "test/TestData/NIMROD-water/good_water.txt") - self.gudpy.loadFromFile( - loadFile="test/TestData/NIMROD-water/good_water.txt", - format=Format.TXT) - - self.gudpy.loadFromGudrunFile( - path="test/TestData/NIMROD-water/good_water.txt" - ) + refFile = os.path.join(dirpath, path) + self.gudpy.loadFromGudrunFile(loadFile=refFile) self.g = self.gudpy.gudrunFile self.expectedPurgeFile = { "standardDeviation": (10, 10), @@ -59,9 +50,10 @@ def testCreatePurgeClass(self): def testWritePurgeFile(self): with tempfile.TemporaryDirectory() as tmp: - os.chdir(tmp) + path = os.path.join(tmp, "purge_det.dat") purge = PurgeFile(self.g) - GudPyIO.writeObject(purge, "purge_det.dat") - with open("purge_det.dat", encoding="utf-8") as f: + GudPyIO.writeObject(purge, path) + with open(path, encoding="utf-8") as f: outlines = f.read() self.assertEqual(outlines, str(purge)) + f.close() From 576fc22fc6a51f8553a6b0054335f684f71e5719 Mon Sep 17 00:00:00 2001 From: Noella Spitz Date: Thu, 16 May 2024 15:47:11 +0100 Subject: [PATCH 06/78] Apply breaking changes --- gudpy/core/gudpy.py | 40 +++++++++--- gudpy/core/io/gudpy_io.py | 43 ++++++++----- gudpy/core/io/gudrun_file_parser.py | 11 ++-- gudpy/gudpy_cli.py | 5 +- gudpy/gudpy_gui.py | 2 +- gudpy/gui/widgets/core/control.py | 93 ++++++++++++++++----------- gudpy/gui/widgets/core/main_window.py | 2 +- gudpy/test/test_gudpy_yaml.py | 6 +- 8 files changed, 123 insertions(+), 79 deletions(-) diff --git a/gudpy/core/gudpy.py b/gudpy/core/gudpy.py index c85e74e64..07767e2ea 100644 --- a/gudpy/core/gudpy.py +++ b/gudpy/core/gudpy.py @@ -17,7 +17,6 @@ from core.file_library import GudPyFileLibrary from core import data_files from core.io.gudpy_io import GudPyIO -from core.io.gudrun_file_parser import GudrunFileParser SUFFIX = ".exe" if os.name == "nt" else "" @@ -38,9 +37,6 @@ def __init__( self.gudrunOutput = None - self.projectDir = "" - self.autosaveLocation = "" - @property def gudrunFile(self): return self._gudrunFile @@ -49,6 +45,10 @@ def gudrunFile(self): def gudrunFile(self, gudrunFile: GudrunFile): self._gudrunFile = gudrunFile + @property + def projectDir(self): + return self.io.projectDir + def loadFromGudrunFile( self, loadFile: str, @@ -60,8 +60,6 @@ def loadFromGudrunFile( ---------- loadFile : str Path of input file to load from - format : enums.Format - Format of the input file (YAML or TXT) config : bool, optional If loading from preset config, by default False @@ -75,6 +73,27 @@ def loadFromGudrunFile( self.gudrunFile = self.io.importGudrunFile(loadFile, config) + def loadFromYamlFile( + self, + loadFile: str, + ): + """Loads GudPy from a YAML input file + + Parameters + ---------- + loadFile : str + Path of input file to load from + + Raises + ------ + FileNotFoundError + Raised if input file does not exist + """ + if not os.path.exists(loadFile): + raise FileNotFoundError("Input file does not exist.") + + self.gudrunFile = self.io.importFromYamlFile(loadFile) + def loadFromProject(self, projectDir: str): """Loads GudPy from a project directory @@ -117,7 +136,7 @@ def setSaveLocation(self, projectDir: str): def save(self): """Saves current GudPy input file """ - self.io.save() + self.io.save(self.gudrunFile) def saveAs(self, targetDir: str): """Save GudPy project to a new location, set current @@ -133,7 +152,8 @@ def saveAs(self, targetDir: str): IsADirectoryError Raised if the requested save location already exists """ - self.io.exportProject(targetDir) + if self.projectDir: + self.io.exportProject(self.gudrunFile, targetDir) self.loadFromProject(projectDir=targetDir) def checkFilesExist(self): @@ -178,7 +198,7 @@ def runPurge(self): self.purge = Purge() purgeFile = PurgeFile(self.gudrunFile) exitcode = self.purge.purge(purgeFile) - self.io.save() + self.save() if exitcode: raise exc.PurgeException( "Purge failed to run with the following output:\n" @@ -202,7 +222,7 @@ def runGudrun(self, gudrunFile: GudrunFile = None): if not gudrunFile: gudrunFile = self.gudrunFile - self.io.save() + self.save() self.gudrun = Gudrun() exitcode = self.gudrun.gudrun(gudrunFile=gudrunFile) if exitcode: diff --git a/gudpy/core/io/gudpy_io.py b/gudpy/core/io/gudpy_io.py index 4dfc6282d..5bcf9d3a6 100644 --- a/gudpy/core/io/gudpy_io.py +++ b/gudpy/core/io/gudpy_io.py @@ -76,10 +76,10 @@ def checkSaveLocation(cls) -> bool: """ return bool(cls.projectDir) - def save(self): + def save(self, gudrunFile): """Saves yaml file to project directory """ - self.writeYaml(self.path()) + self._writeYAML(gudrunFile, self.path()) def importGudrunFile(self, path, config=False) -> GudrunFile: GudPyIO.loadFile = path @@ -128,21 +128,34 @@ def importProject(self, projectDir) -> GudrunFile: self.setSaveLocation(projectDir) return self._parseYaml(loadFile) - def exportProject(self, targetDir): + def importFromYamlFile(self, loadFile) -> GudrunFile: + return self._parseYaml(loadFile) + + def exportProject(self, gudrunFile, targetDir): if os.path.exists(targetDir): raise IsADirectoryError("Cannot be an existing directory") oldFile = os.path.join(targetDir, os.path.basename(self.loadFile)) - oldDir = self.projectDir - self.setSaveLocation(targetDir) os.makedirs(targetDir) + print("TARGET DIR: " + targetDir) + + if self.projectDir and oldFile: + shutil.copytree(self.projectDir, targetDir) + os.rename(oldFile, self.path()) + else: + yamlPath = os.path.join( + targetDir, + f"{os.path.basename(targetDir)}.yaml" + ) + print("yaml PATH: " + yamlPath) + self.exportYamlFile(gudrunFile, yamlPath) - shutil.copytree(oldDir, targetDir) - os.rename(oldFile, self.path()) + def exportYamlFile(self, gudrunFile, path): + self._writeYAML(gudrunFile, path) @classmethod - def exportGudrunFile(cls, path): - return GudrunFileParser.export(path) + def exportGudrunFile(cls, gudrunFile, path): + return GudrunFileParser.export(gudrunFile, path) @classmethod def writeGudrunFile(cls, gudrunFile, path): @@ -193,8 +206,7 @@ def _parseYaml(self, path) -> GudrunFile: # Exception caused by yaml parsing library raise YAMLException(f"Invalid YAML file: {str(e)}") except Exception as e: - print(f"Failed at: {k}") - raise e + raise YAMLException(f"Failed at: {k}. Details: {e}") return gudrunFile def _writeYAML(self, gudrunFile: GudrunFile, path: str) -> None: @@ -208,9 +220,10 @@ def _writeYAML(self, gudrunFile: GudrunFile, path: str) -> None: "GUI": config.GUI } self.yaml.dump( - {k: self._toYaml(v) for k, v in outyaml.items()}, + {k: self.toYaml(v) for k, v in outyaml.items()}, fp ) + print(fp) def _parsingFuncMap(self, type: type) -> typ.Union[typ.Callable, None]: parsingFuncs = { @@ -368,7 +381,7 @@ def _parseYamlComponents(self, yamllist: list[dict]) -> Components: components = [] for c in yamllist: components.append(self._parseYamlComponent(c)) - return components + return Components(components) def _parseYamlElements(self, yamllist: list[str]) -> list[Element]: elements = [] @@ -407,7 +420,7 @@ def toYaml(self, var: any): return float(var) if var.__class__.__module__ == "builtins": if isinstance(var, (list, tuple)): - return type(var)([self._toYaml(v) for v in var]) + return type(var)([self.toYaml(v) for v in var]) else: return var elif isinstance(var, Enum): @@ -418,7 +431,7 @@ def toYaml(self, var: any): Component, Composition, Element, DataFiles, DataFile, GUIConfig )): return { - k: self._toYaml(v) + k: self.toYaml(v) for k, v in var.__dict__.items() if k not in var.yamlignore } diff --git a/gudpy/core/io/gudrun_file_parser.py b/gudpy/core/io/gudrun_file_parser.py index 6afeac82a..29bf26316 100644 --- a/gudpy/core/io/gudrun_file_parser.py +++ b/gudpy/core/io/gudrun_file_parser.py @@ -60,10 +60,10 @@ def parse( Raises ParserException if Instrument, Beam or Normalisation are missing. """ - instrument = None - beam = None - components = None - normalisation = None + instrument = Instrument() + beam = Beam() + components = Components() + normalisation = Normalisation() sampleBackgrounds = [] self.config = config @@ -772,7 +772,6 @@ def _parseSample(self) -> Sample: .replace("SAMPLE", "").strip() ) sample.name = utils.replace_unwanted_chars(sample.name) - print(sample.name) self._consumeWhitespace() # The number of files and period number are both stored # on the same line. @@ -1097,7 +1096,7 @@ def _parseContainer(self) -> Container: ) from e def _parseComponents(self) -> Components: - components = Components + components = Components() try: while self.stream: component = self._parseComponent() diff --git a/gudpy/gudpy_cli.py b/gudpy/gudpy_cli.py index 9bd3a6372..d5d90c2b0 100644 --- a/gudpy/gudpy_cli.py +++ b/gudpy/gudpy_cli.py @@ -18,7 +18,6 @@ def loadProject(ctx, project): def loadFile(ctx, value): - print(value) file, format = value if not file: file = click.prompt("Path to load file", type=click.Path()) @@ -35,7 +34,7 @@ def loadFile(ctx, value): if not file or format: return ctx.obj = gp.GudPy() - ctx.obj.loadFromFile(file, format) + ctx.obj.loadFromGudrunFile(file) click.echo(click.style(u"\u2714", fg="green", bold=True) + f" GudPy input file {file} sucessfuly loaded") @@ -56,7 +55,7 @@ def loadConfig(ctx, cfg): return ctx.obj = gp.GudPy() - ctx.obj.loadFromFile(cfg, enums.Format.TXT, config=True) + ctx.obj.loadFromGudrunFile(cfg, config=True) def echoIndent(text): diff --git a/gudpy/gudpy_gui.py b/gudpy/gudpy_gui.py index ec4cc0152..ba0a8c74f 100644 --- a/gudpy/gudpy_gui.py +++ b/gudpy/gudpy_gui.py @@ -10,7 +10,7 @@ def __init__(self, args): super(GudPy, self).__init__(args) self.gudpy = GudPyController() self.aboutToQuit.connect(self.gudpy.cleanup) - sys.exit(self.exec_()) + sys.exit(self.exec()) def onException(self, cls, exception, traceback): self.gudpy.onException(cls, exception, traceback) diff --git a/gudpy/gui/widgets/core/control.py b/gudpy/gui/widgets/core/control.py index 51c1230db..6749db343 100644 --- a/gudpy/gui/widgets/core/control.py +++ b/gudpy/gui/widgets/core/control.py @@ -110,6 +110,16 @@ def connectUiSlots(self): self.mainWidget.ui.exportArchive.triggered.connect(self.exportArchive) self.mainWidget.ui.exit.triggered.connect(self.exit_) + @property + def gudrunFile(self) -> None: + return self.gudpy.gudrunFile + + @gudrunFile.setter + def gudrunFile(self, gudrunFile: gudrunFile) -> None: + self.gudpy = gp.GudPy() + self.gudpy.gudrunFile = gudrunFile + self.mainWidget.updateWidgets(self.gudpy.gudrunFile) + """ INPUT / OUTPUT @@ -123,7 +133,7 @@ def tryLoadAutosaved(self, projectDir): autoFileInfo = QtCore.QFileInfo(path) autoDate = autoFileInfo.lastModified() - fileInfo = QtCore.QFileInfo(self.gudpy.gudrunFile.path()) + fileInfo = QtCore.QFileInfo(self.gudpy.io.loadFile) currentDate = fileInfo.lastModified() if autoDate > currentDate: @@ -159,7 +169,13 @@ def loadFromFile(self): fmt = filters[filter] try: gudpy = gp.GudPy() - gudpy.loadFromFile(loadFile=filename, format=fmt) + if fmt == enums.Format.TXT: + gudpy.loadFromGudrunFile(filename) + elif fmt == enums.Format.YAML: + gudpy.loadFromYamlFile(filename) + else: + raise RuntimeError(f"Unsupported format: {fmt}") + self.gudpy = gudpy except (FileNotFoundError, exc.ParserException) as e: self.mainWidget.sendError(e) @@ -167,9 +183,9 @@ def loadFromFile(self): except IOError: self.mainWidget.sendError("Could not open file.") return - self.mainWidget.updateWidgets(self.gudpy.gudrunFile) + self.mainWidget.updateWidgets(self.gudrunFile) self.mainWidget.setWindowTitle( - f"GudPy - {self.gudpy.gudrunFile.filename}[*]") + f"GudPy - {os.path.basename(filename)}[*]") def loadFromProject(self): """Load from previous GudPy project @@ -186,7 +202,7 @@ def loadFromProject(self): if autosave: filename = autosave gudpy = gp.GudPy() - gudpy.loadFromFile(loadFile=filename) + gudpy.loadFromGudrunFile(loadFile=filename) self.gudpy = gudpy except (FileNotFoundError, exc.ParserException) as e: self.mainWidget.sendError(e) @@ -194,12 +210,12 @@ def loadFromProject(self): except IOError: self.mainWidget.sendError("Could not open file.") return - self.mainWidget.updateWidgets(self.gudpy.gudrunFile) + self.mainWidget.updateWidgets(self.gudrunFile) self.mainWidget.setWindowTitle( - f"GudPy - {self.gudpy.gudrunFile.filename}[*]") + f"GudPy - {self.gudpy.io.projectName}[*]") def newProject(self): - if self.gudpy.gudrunFile: + if self.gudrunFile: save = QtWidgets.QMessageBox.question( self.mainWidget, "GudPy", @@ -216,12 +232,11 @@ def newProject(self): if not configurationDialog.cancelled and result: self.gudpy = gp.GudPy() - self.gudpy.loadFromFile( + self.gudpy.loadFromGudrunFile( loadFile=configurationDialog.configuration, - format=enums.Format.TXT, config=True ) - self.mainWidget.updateWidgets(self.gudpy.gudrunFile) + self.mainWidget.updateWidgets(self.gudrunFile) def setSaveLocation(self, saveAs=False): """Function to let the user choose where the project is saved to @@ -266,6 +281,7 @@ def saveAs(self): return try: + print(dirname) self.gudpy.saveAs(dirname) except IsADirectoryError as e: self.mainWidget.sendError(e) @@ -278,18 +294,17 @@ def exportInputFile(self): self, "Export input file as..", ".", - "YAML (*.yaml);;Gudrun Compatible (*.txt)", + "Gudrun Compatible (*.txt)", ) - fmt = enums.Format.YAML if filename: ext = re.search(r"\((.+?)\)", filter).group(1).replace("*", "") - fmt = enums.Format.TXT if ext == ".txt" else enums.Format.YAML if filter and sys.platform.startswith("linux"): filename += ext if os.path.dirname(filename) == self.gudpy.projectDir: self.mainWidget.sendWarning("Do not modify project folder.") return - self.gudpy.save(path=filename, format=fmt) + self.gudpy.io.exportGudrunFile(self.gudrunFile, filename) + self.setUnModified() def exportArchive(self): @@ -297,12 +312,12 @@ def exportArchive(self): if not self.setSaveLocation(): return exportDialog = dialogs.io.ExportDialog( - self.gudpy.gudrunFile, self.mainWidget) + self.gudrunFile, self.mainWidget) exportDialog.widget.exec() def autosave(self): if self.gudpy.checkSaveLocation() and not self.workerThread: - self.gudpy.save(path=self.gudpy.autosaveLocation) + self.gudpy.save() """ @@ -312,7 +327,7 @@ def autosave(self): def checkFilesExist(self, showSuccessDialog: bool = False): result = file_library.GudPyFileLibrary( - self.gudpy.gudrunFile).checkFilesExist() + self.gudrunFile).checkFilesExist() if not all(r[0] for r in result[0]) or not all(r[0] for r in result[1]): undefined = [ @@ -322,7 +337,7 @@ def checkFilesExist(self, showSuccessDialog: bool = False): missingFilesDialog = dialogs.io.MissingFilesDialog( undefined, unresolved, self.mainWidget ) - missingFilesDialog.widget.exec_() + missingFilesDialog.widget.exec() return False if showSuccessDialog: @@ -352,16 +367,16 @@ def createPurgeProcess(self) -> bool: return False self.mainWidget.setControlsEnabled(False) purgeDialog = dialogs.purge.PurgeDialog( - self.gudpy.gudrunFile, self.mainWidget) - result = purgeDialog.widget.exec_() + self.gudrunFile, self.mainWidget) + result = purgeDialog.widget.exec() if (purgeDialog.cancelled or result == QDialogButtonBox.No): self.mainWidget.setControlsEnabled(True) return False - self.gudpy.purgeFile = PurgeFile(self.gudpy.gudrunFile) + self.gudpy.purgeFile = PurgeFile(self.gudrunFile) self.gudpy.purge = worker.PurgeWorker( purgeFile=self.gudpy.purgeFile, - gudrunFile=self.gudpy.gudrunFile, + gudrunFile=self.gudrunFile, ) self.connectProcessSignals( process=self.gudpy.purge, onFinish=self.purgeFinished @@ -373,7 +388,7 @@ def prepareRun(self) -> bool: if not self.checkFilesExist(): return False - if not self.gudpy.gudrunFile.checkNormDataFiles(): + if not self.gudrunFile.checkNormDataFiles(): self.mainWidget.sendWarning( "Please specify normalisation data files.") return False @@ -395,7 +410,7 @@ def startProcess(self) -> None: os.path.join( self.gudpy.projectDir, "Purge", "purge_det.dat" ) - ) or not self.gudpy.gudrunFile.purged: + ) or not self.gudrunFile.purged: purgeResult = self.mainWidget.purgeOptionsMessageBox( "purge_det.dat found, but wasn't run in this session. " "Run Purge?", @@ -423,7 +438,7 @@ def runPurge(self) -> bool: self.startProcess() def purgeFinished(self, exitcode): - self.gudpy.gudrunFile.purged = True + self.gudrunFile.purged = True if exitcode != 0: self.mainWidget.sendError( @@ -433,20 +448,20 @@ def purgeFinished(self, exitcode): self.workerThread = None return - thresh = self.gudpy.gudrunFile.instrument.goodDetectorThreshold + thresh = self.gudrunFile.instrument.goodDetectorThreshold if thresh and self.gudpy.purge.detectors < thresh: self.mainWidget.sendWarning( f"{self.gudpy.purge.detectors} " "detectors made it through the purge.\n" " The acceptable minimum for " - f"{self.gudpy.gudrunFile.instrument.name} is {thresh}" + f"{self.gudrunFile.instrument.name} is {thresh}" ) self.mainWidget.ui.goodDetectorsLabel.setText( f"Number of Good Detectors: {self.gudpy.purge.detectors}" ) self.mainWidget.outputSlots.setOutput( self.gudpy.purge.output, "purge_det", - gudrunFile=self.gudpy.gudrunFile + gudrunFile=self.gudrunFile ) if isinstance(self.workerThread, gp.Purge): @@ -456,7 +471,7 @@ def purgeFinished(self, exitcode): def runGudrun(self, gudrunFile=None): if not gudrunFile: - gudrunFile = self.gudpy.gudrunFile + gudrunFile = self.gudrunFile if not self.prepareRun(): return @@ -471,7 +486,7 @@ def runGudrun(self, gudrunFile=None): def iterateGudrun(self, dialog): iterationDialog = dialog( - self.mainWidget, self.gudpy.gudrunFile) + self.mainWidget, self.gudrunFile) iterationDialog.widget.exec() if not iterationDialog.params: return @@ -479,7 +494,7 @@ def iterateGudrun(self, dialog): return # If it is a Composition iteration, the gudrunFile must be specified if iterationDialog.iteratorType == iterators.Composition: - iterationDialog.params["gudrunFile"] = self.gudpy.gudrunFile + iterationDialog.params["gudrunFile"] = self.gudrunFile self.gudpy.iterator = iterationDialog.iteratorType( **iterationDialog.params) @@ -487,7 +502,7 @@ def iterateGudrun(self, dialog): # If Composition iterator, initialise Composition Worker if iterationDialog.iteratorType == iterators.Composition: self.gudpy.gudrunIterator = worker.CompositionWorker( - self.gudpy.iterator, self.gudpy.gudrunFile, self.gudpy.purge) + self.gudpy.iterator, self.gudrunFile, self.gudpy.purge) self.connectProcessSignals( process=self.gudpy.gudrunIterator, onFinish=self.compositionIterationFinished @@ -495,7 +510,7 @@ def iterateGudrun(self, dialog): # Else use standard GudrunIteratorWorker else: self.gudpy.gudrunIterator = worker.GudrunIteratorWorker( - self.gudpy.iterator, self.gudpy.gudrunFile, self.gudpy.purge) + self.gudpy.iterator, self.gudrunFile, self.gudpy.purge) self.connectProcessSignals( process=self.gudpy.gudrunIterator, onFinish=self.gudrunFinished @@ -535,7 +550,7 @@ def gudrunFinished(self, exitcode): self.mainWidget.outputSlots.setOutput( self.gudpy.gudrun.output, "Gudrun") self.mainWidget.updateWidgets( - gudrunFile=self.gudpy.gudrunFile, + gudrunFile=self.gudrunFile, gudrunOutput=self.gudpy.gudrun.gudrunOutput ) self.workerThread = None @@ -558,14 +573,14 @@ def runContainersAsSamples(self): if not self.prepareRun(): return gudrunFile = self.gudpy.runModes.convertContainersToSample( - self.gudpy.gudrunFile + self.gudrunFile ) self.runGudrun(gudrunFile=gudrunFile) def runFilesIndividually(self): if not self.prepareRun(): return - gudrunFile = self.gudpy.runModes.partition(self.gudpy.gudrunFile) + gudrunFile = self.gudpy.runModes.partition(self.gudrunFile) self.runGudrun(gudrunFile=gudrunFile) def runBatchProcessing(self): @@ -575,7 +590,7 @@ def runBatchProcessing(self): self.mainWidget ) self.gudpy.gudrunIterator = worker.BatchWorker( - gudrunFile=self.gudpy.gudrunFile, + gudrunFile=self.gudrunFile, purge=self.gudpy.purge, iterator=dialog.iterator, batchSize=dialog.batchSize, @@ -627,5 +642,5 @@ def exit_(self): ) if result == QtWidgets.QMessageBox.Yes: - self.gudpy.gudrunFile.save() + self.gudrunFile.save() sys.exit(0) diff --git a/gudpy/gui/widgets/core/main_window.py b/gudpy/gui/widgets/core/main_window.py index ef90960f6..299999acf 100644 --- a/gudpy/gui/widgets/core/main_window.py +++ b/gudpy/gui/widgets/core/main_window.py @@ -779,7 +779,7 @@ def setTreeActionsEnabled(self, state): def viewInput(self): self.currentState = str(self.gudrunFile) viewInputDialog = ViewInputDialog(self.gudrunFile, self) - viewInputDialog.widget.exec_() + viewInputDialog.widget.exec() def handleAllPlotModeChanged(self, index): plotMode = self.ui.allPlotComboBox.itemData(index) diff --git a/gudpy/test/test_gudpy_yaml.py b/gudpy/test/test_gudpy_yaml.py index c68a63804..b58d89e2a 100644 --- a/gudpy/test/test_gudpy_yaml.py +++ b/gudpy/test/test_gudpy_yaml.py @@ -8,17 +8,15 @@ class TestYAML(TestCase): def testYAML(self): gudpy1 = gudpy.GudPy() - gudpy1.loadFromFile( + gudpy1.loadFromGudrunFile( loadFile="test/TestData/NIMROD-water/water.txt", - format=Format.TXT ) gudpy1.save(path="test/TestData/NIMROD-water/water.yaml") gf1 = gudpy1.gudrunFile gudpy2 = gudpy.GudPy() - gudpy2.loadFromFile( + gudpy2.loadFromYamlFile( loadFile="test/TestData/NIMROD-water/water.yaml", - format=Format.YAML ) gf2 = gudpy2.gudrunFile From 6ce452d642f408d7f94492ec64ed696b74178734 Mon Sep 17 00:00:00 2001 From: Noella Spitz Date: Fri, 17 May 2024 15:04:58 +0100 Subject: [PATCH 07/78] Fix checks --- gudpy/core/io/gudpy_io.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gudpy/core/io/gudpy_io.py b/gudpy/core/io/gudpy_io.py index 5bcf9d3a6..7460a91b0 100644 --- a/gudpy/core/io/gudpy_io.py +++ b/gudpy/core/io/gudpy_io.py @@ -139,7 +139,7 @@ def exportProject(self, gudrunFile, targetDir): os.makedirs(targetDir) print("TARGET DIR: " + targetDir) - if self.projectDir and oldFile: + if os.path.exists(self.projectDir) and os.path.exists(oldFile): shutil.copytree(self.projectDir, targetDir) os.rename(oldFile, self.path()) else: From 6c89ddedfab3cc0332cc1f66e499c29f59e32d96 Mon Sep 17 00:00:00 2001 From: Noella Spitz Date: Mon, 20 May 2024 10:54:37 +0100 Subject: [PATCH 08/78] Fix YAML imports --- gudpy/core/gudpy.py | 3 +- gudpy/core/io/gudpy_io.py | 92 +++++++++++++++++++------------ gudpy/gui/widgets/core/control.py | 7 ++- 3 files changed, 63 insertions(+), 39 deletions(-) diff --git a/gudpy/core/gudpy.py b/gudpy/core/gudpy.py index 07767e2ea..2a599879a 100644 --- a/gudpy/core/gudpy.py +++ b/gudpy/core/gudpy.py @@ -152,8 +152,7 @@ def saveAs(self, targetDir: str): IsADirectoryError Raised if the requested save location already exists """ - if self.projectDir: - self.io.exportProject(self.gudrunFile, targetDir) + self.io.exportProject(self.gudrunFile, targetDir) self.loadFromProject(projectDir=targetDir) def checkFilesExist(self): diff --git a/gudpy/core/io/gudpy_io.py b/gudpy/core/io/gudpy_io.py index 7460a91b0..72ddcd8ee 100644 --- a/gudpy/core/io/gudpy_io.py +++ b/gudpy/core/io/gudpy_io.py @@ -183,7 +183,6 @@ def _parseYaml(self, path) -> GudrunFile: yamldict = self._yamlToDict(path) gudrunFile = GudrunFile() for k, v in yamldict.items(): - k = k.lower() if k == "GUI": GUI = GUIConfig() self._yamlToObject(GUI, v) @@ -194,8 +193,7 @@ def _parseYaml(self, path) -> GudrunFile: print(f"Ignoring depreciated attribute '{k}'") continue - objType = type(gudrunFile.__dict__[k]) - parsingFunc = self._parsingFuncMap(objType) + parsingFunc = self._parsingFuncMap(k) if parsingFunc: try: obj = parsingFunc(v) @@ -206,7 +204,7 @@ def _parseYaml(self, path) -> GudrunFile: # Exception caused by yaml parsing library raise YAMLException(f"Invalid YAML file: {str(e)}") except Exception as e: - raise YAMLException(f"Failed at: {k}. Details: {e}") + raise YAMLException(f"Failed at: '{k}'. Details: {e}") return gudrunFile def _writeYAML(self, gudrunFile: GudrunFile, path: str) -> None: @@ -223,23 +221,23 @@ def _writeYAML(self, gudrunFile: GudrunFile, path: str) -> None: {k: self.toYaml(v) for k, v in outyaml.items()}, fp ) - print(fp) - def _parsingFuncMap(self, type: type) -> typ.Union[typ.Callable, None]: + def _parsingFuncMap(self, key) -> typ.Union[typ.Callable, None]: parsingFuncs = { - Instrument: self._parseYamlInstrument, - Beam: self._parseYamlBeam, - Normalisation: self._parseYamlNormalisation, - SampleBackground: self._parseYamlSampleBackground, - Sample: self._parseYamlSample, - Composition: self._parseYamlComposition, - Component: self._parseYamlComponent, - Components: self._parseYamlComponents, - list[Element]: self._parseYamlElements, - DataFiles: self._parseYamlDataFiles + "instrument ": self._parseYamlInstrument, + "beam": self._parseYamlBeam, + "normalisation": self._parseYamlNormalisation, + "sampleBackgrounds": self._parseYamlSampleBackground, + "sample": self._parseYamlSample, + "composition": self._parseYamlComposition, + "component": self._parseYamlComponent, + "components": self._parseYamlComponents, + "elements": self._parseYamlElements, + "dataFiles": self._parseYamlDataFiles, + "dataFilesBg": self._parseYamlDataFiles } - if type in parsingFuncs.keys(): - return parsingFuncs[type] + if key in parsingFuncs.keys(): + return parsingFuncs[key] else: return None @@ -287,10 +285,10 @@ def _parseYamlNormalisation(self, yamldict: any) -> Normalisation: normalisation = Normalisation() for k, v in yamldict.items(): if k == "composition": - composition = self._parseYamlComposition(yamldict[k]) + composition = self._parseYamlComposition(v) setattr(normalisation, k, composition) - if k == "dataFiles": - dataFiles = self._parseYamlDataFiles(yamldict[k]) + elif k == "dataFiles" or k == "dataFilesBg": + dataFiles = self._parseYamlDataFiles(v) setattr(normalisation, k, dataFiles) else: self._assignYamlToAttr(normalisation, k, v) @@ -299,8 +297,11 @@ def _parseYamlNormalisation(self, yamldict: any) -> Normalisation: def _parseYamlSample(self, yamldict: any) -> Sample: sample = Sample() for k, v in yamldict.items(): - if k == "composition": - composition = self._parseYamlComposition() + if k == "dataFiles": + dataFiles = self._parseYamlDataFiles(v) + setattr(sample, k, dataFiles) + elif k == "composition": + composition = self._parseYamlComposition(v) setattr(sample, k, composition) elif k == "containers": for contyaml in yamldict[k]: @@ -310,19 +311,28 @@ def _parseYamlSample(self, yamldict: any) -> Sample: self._assignYamlToAttr(sample, k, v) return sample - def _parseYamlSampleBackground(self, yamldict: any) -> SampleBackground: - sampleBg = SampleBackground() - for k, v in yamldict.items(): - if k == "samples": - for sampleyaml in yamldict[k]: - sample = self._parseYamlSample(sampleyaml) - sample.name = utils.replace_unwanted_chars(sample.name) - sampleBg.samples.append(sample) - else: - self._assignYamlToAttr(sampleBg, k, v) + def _parseYamlSampleBackground( + self, yamllist: list + ) -> list[SampleBackground]: + sampleBackgrounds = [] + for sbg in yamllist: + sampleBg = SampleBackground() + for k, v in sbg.items(): + if k == "samples": + for sampleyaml in v: + sample = self._parseYamlSample(sampleyaml) + sample.name = utils.replace_unwanted_chars(sample.name) + sampleBg.samples.append(sample) + elif k == "dataFiles": + dataFiles = self._parseYamlDataFiles(v) + setattr(sampleBg, k, dataFiles) + else: + self._assignYamlToAttr(sampleBg, k, v) + sampleBackgrounds.append(sampleBg) + return sampleBackgrounds def _parseYamlComposition(self, yamldict: any) -> Composition: - composition = Composition() + composition = Composition(yamldict["type_"]) for k, v in yamldict.items(): if k == "elements": elements = self._parseYamlElements(yamldict[k]) @@ -349,8 +359,11 @@ def _parseYamlContainer(self, yamldict: any) -> Container: container = Container() for k, v in yamldict.items(): if k == "composition": - composition = self._parseYamlComposition() + composition = self._parseYamlComposition(v) setattr(container, k, composition) + elif k == "dataFiles": + dataFiles = self._parseYamlDataFiles(v) + setattr(container, k, dataFiles) else: self._assignYamlToAttr(container, k, v) return container @@ -359,7 +372,14 @@ def _parseYamlDataFiles(self, yamldict: any) -> DataFiles: dataFiles = DataFiles([], yamldict["name"]) for df in yamldict["_dataFiles"]: dataFile = DataFile(df["filename"]) - dataFile = self._yamlToObject(dataFile, df) + for k, v in df.items(): + if k == "_outputs": + outDict = {} + for k_, v_ in v: + outDict[k_] = v_ + dataFile._outputs = outDict + else: + self._assignYamlToAttr(dataFile, k, v) dataFiles.dataFiles.append(dataFile) return dataFiles diff --git a/gudpy/gui/widgets/core/control.py b/gudpy/gui/widgets/core/control.py index 6749db343..456c22d8d 100644 --- a/gudpy/gui/widgets/core/control.py +++ b/gudpy/gui/widgets/core/control.py @@ -128,7 +128,7 @@ def gudrunFile(self, gudrunFile: gudrunFile) -> None: def tryLoadAutosaved(self, projectDir): for f in os.listdir(projectDir): - if f == self.gudpy.autosaveLocation: + if f == self.gudpy.io.autosavePath: path = os.path.join(projectDir, f) autoFileInfo = QtCore.QFileInfo(path) autoDate = autoFileInfo.lastModified() @@ -210,7 +210,12 @@ def loadFromProject(self): except IOError: self.mainWidget.sendError("Could not open file.") return + except exc.YAMLException as e: + self.mainWidget.sendError(f"Could not parse input file: {e}") + return self.mainWidget.updateWidgets(self.gudrunFile) + for sample in self.gudrunFile.sampleBackgrounds[0].samples: + print(sample.name) self.mainWidget.setWindowTitle( f"GudPy - {self.gudpy.io.projectName}[*]") From e8bc3240b97e43d092ae3a2292fea7b6665ba019 Mon Sep 17 00:00:00 2001 From: Noella Spitz Date: Mon, 20 May 2024 10:57:59 +0100 Subject: [PATCH 09/78] Fix instrument parsing --- gudpy/core/io/gudpy_io.py | 4 +--- gudpy/gui/widgets/core/control.py | 2 -- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/gudpy/core/io/gudpy_io.py b/gudpy/core/io/gudpy_io.py index 72ddcd8ee..eed1887cf 100644 --- a/gudpy/core/io/gudpy_io.py +++ b/gudpy/core/io/gudpy_io.py @@ -137,7 +137,6 @@ def exportProject(self, gudrunFile, targetDir): oldFile = os.path.join(targetDir, os.path.basename(self.loadFile)) os.makedirs(targetDir) - print("TARGET DIR: " + targetDir) if os.path.exists(self.projectDir) and os.path.exists(oldFile): shutil.copytree(self.projectDir, targetDir) @@ -147,7 +146,6 @@ def exportProject(self, gudrunFile, targetDir): targetDir, f"{os.path.basename(targetDir)}.yaml" ) - print("yaml PATH: " + yamlPath) self.exportYamlFile(gudrunFile, yamlPath) def exportYamlFile(self, gudrunFile, path): @@ -224,7 +222,7 @@ def _writeYAML(self, gudrunFile: GudrunFile, path: str) -> None: def _parsingFuncMap(self, key) -> typ.Union[typ.Callable, None]: parsingFuncs = { - "instrument ": self._parseYamlInstrument, + "instrument": self._parseYamlInstrument, "beam": self._parseYamlBeam, "normalisation": self._parseYamlNormalisation, "sampleBackgrounds": self._parseYamlSampleBackground, diff --git a/gudpy/gui/widgets/core/control.py b/gudpy/gui/widgets/core/control.py index 456c22d8d..b08cf863d 100644 --- a/gudpy/gui/widgets/core/control.py +++ b/gudpy/gui/widgets/core/control.py @@ -215,7 +215,6 @@ def loadFromProject(self): return self.mainWidget.updateWidgets(self.gudrunFile) for sample in self.gudrunFile.sampleBackgrounds[0].samples: - print(sample.name) self.mainWidget.setWindowTitle( f"GudPy - {self.gudpy.io.projectName}[*]") @@ -286,7 +285,6 @@ def saveAs(self): return try: - print(dirname) self.gudpy.saveAs(dirname) except IsADirectoryError as e: self.mainWidget.sendError(e) From 317d6187cb1193abebd6dc0cbb2dbb296b709af3 Mon Sep 17 00:00:00 2001 From: Noella Spitz Date: Mon, 20 May 2024 11:55:30 +0100 Subject: [PATCH 10/78] Fix export, update test project --- gudpy/core/io/gudrun_file_parser.py | 3 +- gudpy/gui/widgets/core/control.py | 17 +- .../NIMROD-water/good_water/good_water.yaml | 185 ++++++++++++++++-- 3 files changed, 182 insertions(+), 23 deletions(-) diff --git a/gudpy/core/io/gudrun_file_parser.py b/gudpy/core/io/gudrun_file_parser.py index 29bf26316..cee80941a 100644 --- a/gudpy/core/io/gudrun_file_parser.py +++ b/gudpy/core/io/gudrun_file_parser.py @@ -41,7 +41,8 @@ def writeGudrunFile(cls, gudrunFile: GudrunFile, runDir: str): f.write(str(gudrunFile)) f.close() - def export(self, gudrunFile: GudrunFile, path: str): + @classmethod + def export(cls, gudrunFile: GudrunFile, path: str): for sampleBackground in gudrunFile.sampleBackgrounds: sampleBackground.writeAllSamples = True diff --git a/gudpy/gui/widgets/core/control.py b/gudpy/gui/widgets/core/control.py index b08cf863d..ea28e0d78 100644 --- a/gudpy/gui/widgets/core/control.py +++ b/gudpy/gui/widgets/core/control.py @@ -214,7 +214,6 @@ def loadFromProject(self): self.mainWidget.sendError(f"Could not parse input file: {e}") return self.mainWidget.updateWidgets(self.gudrunFile) - for sample in self.gudrunFile.sampleBackgrounds[0].samples: self.mainWidget.setWindowTitle( f"GudPy - {self.gudpy.io.projectName}[*]") @@ -293,22 +292,20 @@ def exportInputFile(self): """ Saves the current state of the input file as... """ - filename, filter = QFileDialog.getSaveFileName( - self, + filename, _ = QFileDialog.getSaveFileName( + self.mainWidget, "Export input file as..", ".", - "Gudrun Compatible (*.txt)", ) + print(filename) if filename: - ext = re.search(r"\((.+?)\)", filter).group(1).replace("*", "") - if filter and sys.platform.startswith("linux"): - filename += ext + filename.replace(".txt", "") + filename += ".txt" if os.path.dirname(filename) == self.gudpy.projectDir: self.mainWidget.sendWarning("Do not modify project folder.") return - self.gudpy.io.exportGudrunFile(self.gudrunFile, filename) - - self.setUnModified() + self.gudpy.io.exportGudrunFile(self.gudrunFile, filename) + self.setUnModified() def exportArchive(self): if not self.gudpy.checkSaveLocation(): diff --git a/gudpy/test/TestData/NIMROD-water/good_water/good_water.yaml b/gudpy/test/TestData/NIMROD-water/good_water/good_water.yaml index 075d507d8..924e570f4 100644 --- a/gudpy/test/TestData/NIMROD-water/good_water/good_water.yaml +++ b/gudpy/test/TestData/NIMROD-water/good_water/good_water.yaml @@ -1,6 +1,6 @@ instrument: name: NIMROD - dataFileDir: TestData/NIMROD-water/raw/ + dataFileDir: test/TestData/NIMROD-water/raw/ dataFileType: raw detectorCalibrationFileName: StartupFiles/NIMROD/NIMROD84modules+9monitors+LAB5Oct2012Detector.dat columnNoPhiVals: 4 @@ -57,12 +57,34 @@ components: [] normalisation: periodNumber: 1 dataFiles: - _dataFiles: [NIMROD00016702_V.raw] + _dataFiles: + - filename: NIMROD00016702_V.raw + name: NIMROD00016702_V + ext: .raw + outputFolder: '' + _outputs: {} + isSampleDataFile: false name: NORMALISATION + outputFolders: {} + _outputs: {} periodNumberBg: 1 dataFilesBg: - _dataFiles: [NIMROD00016698_EmptyInst.raw, NIMROD00016703_EmptyInst.raw] + _dataFiles: + - filename: NIMROD00016698_EmptyInst.raw + name: NIMROD00016698_EmptyInst + ext: .raw + outputFolder: '' + _outputs: {} + isSampleDataFile: false + - filename: NIMROD00016703_EmptyInst.raw + name: NIMROD00016703_EmptyInst + ext: .raw + outputFolder: '' + _outputs: {} + isSampleDataFile: false name: NORMALISATION BACKGROUND + outputFolders: {} + _outputs: {} forceCalculationOfCorrections: true composition: type_: Normalisation @@ -89,14 +111,42 @@ normalisation: sampleBackgrounds: - periodNumber: 1 dataFiles: - dataFiles: [NIMROD00016698_EmptyInst.raw, NIMROD00016703_EmptyInst.raw] + _dataFiles: + - filename: NIMROD00016698_EmptyInst.raw + name: NIMROD00016698_EmptyInst + ext: .raw + outputFolder: '' + _outputs: {} + isSampleDataFile: false + - filename: NIMROD00016703_EmptyInst.raw + name: NIMROD00016703_EmptyInst + ext: .raw + outputFolder: '' + _outputs: {} + isSampleDataFile: false name: SAMPLE BACKGROUND + outputFolders: {} + _outputs: {} samples: - name: H2O,_Can_N9 periodNumber: 1 dataFiles: - _dataFiles: [NIMROD00016608_H2O_in_N9.raw, NIMROD00016610_H2O_in_N9.raw] + _dataFiles: + - filename: NIMROD00016608_H2O_in_N9.raw + name: NIMROD00016608_H2O_in_N9 + ext: .raw + outputFolder: '' + _outputs: {} + isSampleDataFile: false + - filename: NIMROD00016610_H2O_in_N9.raw + name: NIMROD00016610_H2O_in_N9 + ext: .raw + outputFolder: '' + _outputs: {} + isSampleDataFile: false name: H2O,_Can_N9 + outputFolders: {} + _outputs: {} forceCalculationOfCorrections: true composition: type_: Sample @@ -139,8 +189,28 @@ sampleBackgrounds: - name: N9 periodNumber: 1 dataFiles: - _dataFiles: [NIMROD00016694_Empty_N9.raw, NIMROD00016699_Empty_N9.raw, NIMROD00016704_Empty_N9.raw] + _dataFiles: + - filename: NIMROD00016694_Empty_N9.raw + name: NIMROD00016694_Empty_N9 + ext: .raw + outputFolder: '' + _outputs: {} + isSampleDataFile: false + - filename: NIMROD00016699_Empty_N9.raw + name: NIMROD00016699_Empty_N9 + ext: .raw + outputFolder: '' + _outputs: {} + isSampleDataFile: false + - filename: NIMROD00016704_Empty_N9.raw + name: NIMROD00016704_Empty_N9 + ext: .raw + outputFolder: '' + _outputs: {} + isSampleDataFile: false name: N9 + outputFolders: {} + _outputs: {} composition: type_: Container elements: @@ -162,11 +232,28 @@ sampleBackgrounds: tweakFactor: 1.0 scatteringFraction: 1.0 attenuationCoefficient: 0.0 + outputFolder: '' + outputFolder: '' + sampleFile: '' - name: D2O,_Can_N10 periodNumber: 1 dataFiles: - _dataFiles: [NIMROD00016609_D2O_in_N10.raw, NIMROD00016611_D2O_in_N10.raw] + _dataFiles: + - filename: NIMROD00016609_D2O_in_N10.raw + name: NIMROD00016609_D2O_in_N10 + ext: .raw + outputFolder: '' + _outputs: {} + isSampleDataFile: false + - filename: NIMROD00016611_D2O_in_N10.raw + name: NIMROD00016611_D2O_in_N10 + ext: .raw + outputFolder: '' + _outputs: {} + isSampleDataFile: false name: D2O,_Can_N10 + outputFolders: {} + _outputs: {} forceCalculationOfCorrections: true composition: type_: Sample @@ -209,8 +296,28 @@ sampleBackgrounds: - name: N10 periodNumber: 1 dataFiles: - _dataFiles: [NIMROD00016695_Empty_N10.raw, NIMROD00016700_Empty_N10.raw, NIMROD00016705_Empty_N10.raw] + _dataFiles: + - filename: NIMROD00016695_Empty_N10.raw + name: NIMROD00016695_Empty_N10 + ext: .raw + outputFolder: '' + _outputs: {} + isSampleDataFile: false + - filename: NIMROD00016700_Empty_N10.raw + name: NIMROD00016700_Empty_N10 + ext: .raw + outputFolder: '' + _outputs: {} + isSampleDataFile: false + - filename: NIMROD00016705_Empty_N10.raw + name: NIMROD00016705_Empty_N10 + ext: .raw + outputFolder: '' + _outputs: {} + isSampleDataFile: false name: N10 + outputFolders: {} + _outputs: {} composition: type_: Container elements: @@ -232,11 +339,28 @@ sampleBackgrounds: tweakFactor: 1.0 scatteringFraction: 1.0 attenuationCoefficient: 0.0 + outputFolder: '' + outputFolder: '' + sampleFile: '' - name: HDO,_Can_N6 periodNumber: 1 dataFiles: - _dataFiles: [NIMROD00016741_HDO_in_N6.raw, NIMROD00016743_HDO_in_N6.raw] + _dataFiles: + - filename: NIMROD00016741_HDO_in_N6.raw + name: NIMROD00016741_HDO_in_N6 + ext: .raw + outputFolder: '' + _outputs: {} + isSampleDataFile: false + - filename: NIMROD00016743_HDO_in_N6.raw + name: NIMROD00016743_HDO_in_N6 + ext: .raw + outputFolder: '' + _outputs: {} + isSampleDataFile: false name: HDO,_Can_N6 + outputFolders: {} + _outputs: {} forceCalculationOfCorrections: true composition: type_: Sample @@ -280,8 +404,16 @@ sampleBackgrounds: - name: N6 periodNumber: 1 dataFiles: - _dataFiles: [NIMROD00014908_Empty_N6.raw] + _dataFiles: + - filename: NIMROD00014908_Empty_N6.raw + name: NIMROD00014908_Empty_N6 + ext: .raw + outputFolder: '' + _outputs: {} + isSampleDataFile: false name: N6 + outputFolders: {} + _outputs: {} composition: type_: Container elements: @@ -303,11 +435,28 @@ sampleBackgrounds: tweakFactor: 1.0 scatteringFraction: 1.0 attenuationCoefficient: 0.0 + outputFolder: '' + outputFolder: '' + sampleFile: '' - name: Null_Water,_Can_N8 periodNumber: 1 dataFiles: - dataFiles: [NIMROD00016742_NullWater_in_N8.raw, NIMROD00016744_NullWater_in_N8.raw] + _dataFiles: + - filename: NIMROD00016742_NullWater_in_N8.raw + name: NIMROD00016742_NullWater_in_N8 + ext: .raw + outputFolder: '' + _outputs: {} + isSampleDataFile: false + - filename: NIMROD00016744_NullWater_in_N8.raw + name: NIMROD00016744_NullWater_in_N8 + ext: .raw + outputFolder: '' + _outputs: {} + isSampleDataFile: false name: Null_Water,_Can_N8 + outputFolders: {} + _outputs: {} forceCalculationOfCorrections: true composition: type_: Sample @@ -351,8 +500,16 @@ sampleBackgrounds: - name: N8 periodNumber: 1 dataFiles: - _dataFiles: [NIMROD00016994_Empty_N8.raw] + _dataFiles: + - filename: NIMROD00016994_Empty_N8.raw + name: NIMROD00016994_Empty_N8 + ext: .raw + outputFolder: '' + _outputs: {} + isSampleDataFile: false name: N8 + outputFolders: {} + _outputs: {} composition: type_: Container elements: @@ -374,4 +531,8 @@ sampleBackgrounds: tweakFactor: 1.0 scatteringFraction: 1.0 attenuationCoefficient: 0.0 + outputFolder: '' + outputFolder: '' + sampleFile: '' + outputFolder: '' GUI: {useComponents: false} From 8604ee3a7dbeab328344035beea1101cbdede2cf Mon Sep 17 00:00:00 2001 From: Noella Spitz Date: Mon, 20 May 2024 11:59:01 +0100 Subject: [PATCH 11/78] Lint, delete gudpy_yaml.py --- gudpy/core/file_library.py | 1 - gudpy/core/gudpy_yaml.py | 258 ---------------------------- gudpy/core/io/gudrun_file_parser.py | 4 +- gudpy/gui/widgets/core/control.py | 1 - gudpy/test/test_purge_file.py | 1 - 5 files changed, 3 insertions(+), 262 deletions(-) delete mode 100644 gudpy/core/gudpy_yaml.py diff --git a/gudpy/core/file_library.py b/gudpy/core/file_library.py index c8feb0a2c..041e62022 100644 --- a/gudpy/core/file_library.py +++ b/gudpy/core/file_library.py @@ -1,6 +1,5 @@ import os from zipfile import ZipFile, ZIP_DEFLATED -from pathlib import Path from core import utils from core.enums import CrossSectionSource diff --git a/gudpy/core/gudpy_yaml.py b/gudpy/core/gudpy_yaml.py deleted file mode 100644 index ccbb1d415..000000000 --- a/gudpy/core/gudpy_yaml.py +++ /dev/null @@ -1,258 +0,0 @@ -from abc import abstractmethod -from enum import Enum -from ruamel.yaml import YAML as yaml -from ruamel.yaml import YAMLError -import os - -from core.composition import ( - Component, Components, Composition, WeightedComponent -) -from core.data_files import DataFile, DataFiles -from core.element import Element -from core.exception import YAMLException -from core.gui_config import GUIConfig -from core import utils - -from core.instrument import Instrument -from core.beam import Beam -from core.normalisation import Normalisation -from core.sample_background import SampleBackground -from core.sample import Sample -from core.container import Container -from core import config - - -class YAML: - def __init__(self): - self.yaml = self.getYamlModule() - - def getYamlModule(self): - yaml_ = yaml() - yaml_.preserve_quotes = True - yaml_.default_flow_style = None - yaml_.encoding = 'utf-8' - return yaml_ - - def parseYaml(self, path): - self.path = path - try: - parsedYAML = self.constructClasses(self.yamlToDict(path)) - except YAMLError as e: - # Exception caused by yaml parsing library - raise YAMLException(f"Invalid YAML file: {str(e)}") - except YAMLException as e: - # Exception caused by invalid arguments - raise YAMLException(e) - else: - return parsedYAML - - def yamlToDict(self, path): - # Read the input stream into our attribute. - with open(path, encoding=self.yaml.encoding) as fp: - return self.yaml.load(fp) - - def constructClasses(self, yamldict): - instrument = Instrument() - if "Instrument" in yamldict: - self.maskYAMLDicttoClass(instrument, yamldict["Instrument"]) - instrument.GudrunInputFileDir = os.path.dirname( - os.path.abspath( - self.path - ) - ) - - beam = Beam() - if "Beam" in yamldict: - self.maskYAMLDicttoClass(beam, yamldict["Beam"]) - - components = Components() - if "Components" in yamldict: - self.maskYAMLSeqtoClass(components, yamldict["Components"]) - - normalisation = Normalisation() - if "Normalisation" in yamldict: - self.maskYAMLDicttoClass(normalisation, yamldict["Normalisation"]) - - sampleBackgrounds = [] - if "SampleBackgrounds" in yamldict: - for sbyaml in yamldict["SampleBackgrounds"]: - sampleBackground = SampleBackground() - self.maskYAMLDicttoClass(sampleBackground, sbyaml) - sampleBackgrounds.append(sampleBackground) - - GUI = GUIConfig() - if "GUI" in yamldict: - self.maskYAMLDicttoClass(GUI, yamldict["GUI"]) - - return ( - instrument, beam, components, - normalisation, sampleBackgrounds, GUI - ) - - @abstractmethod - def maskYAMLDicttoClass(self, cls, yamldict): - for k, v in yamldict.items(): - if not hasattr(cls, k): - # If attribute is not valid - print(f"Ignoring depreciated attribute '{k}'" - f"given to '{type(cls).__name__}'") - continue - - if isinstance(cls.__dict__[k], Enum): - setattr(cls, k, type(cls.__dict__[k])[v]) - - elif isinstance(cls.__dict__[k], DataFiles): - for df in v["_dataFiles"]: - dataFile = DataFile(df["filename"]) - self.maskYAMLDicttoClass(dataFile, df) - - elif ( - isinstance( - cls, - (Component, Composition) - ) - and k == "elements" - ): - elements = [] - for idx, element in enumerate(v): - # Ensuring correct arguements are provided - if ( - "atomicSymbol" not in element - or "massNo" not in element - or "abundance" not in element - ): - raise YAMLException( - "Insufficient arguments given to element" - + f" {idx + 1}. Expects 'atomicSymbol', 'massNo'" - + " and 'abundance'" - ) - - # Setting element properties - try: - element_ = Element( - **{ - "atomicSymbol": element["atomicSymbol"], - "massNo": float(element["massNo"]), - "abundance": float(element["abundance"]) - } - ) - elements.append(element_) - except ValueError: - raise YAMLException( - f"Invalid number given to element {idx + 1}" - + f" in '{type(cls).__name__}") - setattr(cls, k, elements) - - elif isinstance(cls, Composition) and k == "weightedComponents": - weightedComponents = [] - for weightedComponent in v: - if ( - "component" not in weightedComponent - or "ratio" not in weightedComponent - ): - raise YAMLException( - "Weighted Component expects 'component' and" - + " 'ratio' to be provided") - component = Component() - self.maskYAMLDicttoClass( - component, weightedComponent["component"] - ) - ratio = weightedComponent["ratio"] - try: - weightedComponents.append( - WeightedComponent( - component, float(ratio)) - ) - except ValueError: - raise YAMLException( - "Invalid ratio given to Weighted Component") - setattr(cls, k, weightedComponents) - - elif ( - isinstance( - cls, - (Normalisation, Sample, Container) - ) - and k == "composition" - ): - self.maskYAMLDicttoClass(cls.__dict__[k], v) - - elif isinstance(cls, SampleBackground) and k == "samples": - for sampleyaml in yamldict[k]: - sample = Sample() - self.maskYAMLDicttoClass(sample, sampleyaml) - sample.name = utils.replace_unwanted_chars(sample.name) - cls.samples.append(sample) - - elif isinstance(cls, Sample) and k == "containers": - for contyaml in yamldict[k]: - container = Container() - self.maskYAMLDicttoClass(container, contyaml) - cls.containers.append(container) - - else: - setattr(cls, k, type(cls.__dict__[k])(self.toBuiltin(v))) - - def maskYAMLSeqtoClass(self, cls, yamlseq): - if isinstance(cls, Components): - components = [] - for component in yamlseq: - component_ = Component() - self.maskYAMLDicttoClass(component_, component) - components.append(component_) - setattr(cls, "components", components) - - def writeYAML(self, base, path): - with open(path, "wb") as fp: - outyaml = { - "Instrument": base.instrument, - "Beam": base.beam, - "Components": base.components.components, - "Normalisation": base.normalisation, - "SampleBackgrounds": base.sampleBackgrounds, - "GUI": config.GUI - } - self.yaml.dump( - {k: self.toYaml(v) for k, v in outyaml.items()}, - fp - ) - - @abstractmethod - def toYaml(self, var): - if var.__class__.__module__ == "ruamel.yaml.scalarfloat": - return float(var) - if var.__class__.__module__ == "builtins": - if isinstance(var, (list, tuple)): - return type(var)([self.toYaml(v) for v in var]) - else: - return var - elif isinstance(var, Enum): - return type(var)(var.value).name - elif isinstance(var, ( - Instrument, Beam, Components, Normalisation, - SampleBackground, Sample, Container, WeightedComponent, - Component, Composition, Element, DataFiles, DataFile, GUIConfig - )): - return { - k: self.toYaml(v) - for k, v in var.__dict__.items() - if k not in var.yamlignore - } - - @abstractmethod - def toBuiltin(self, yamlvar): - if yamlvar is None: - return None - if isinstance(yamlvar, (list, tuple)): - return [self.toBuiltin(v) for v in yamlvar] - elif (yamlvar.__class__.__module__ == - "ruamel.yaml.comments.CommentedMap"): - dict = {} - for k, v in yamlvar.items(): - dict[k] = v - elif yamlvar.__class__.__module__ == "builtins": - return yamlvar - elif yamlvar.__class__.__module__ == "ruamel.yaml.scalarfloat": - return float(yamlvar) - elif yamlvar.__class__.__module__ == "ruamel.yaml.scalarstring": - return str(yamlvar) diff --git a/gudpy/core/io/gudrun_file_parser.py b/gudpy/core/io/gudrun_file_parser.py index cee80941a..b95c36d39 100644 --- a/gudpy/core/io/gudrun_file_parser.py +++ b/gudpy/core/io/gudrun_file_parser.py @@ -1075,7 +1075,9 @@ def _parseContainer(self) -> Container: enums.CrossSectionSource[crossSectionSource] ) else: - container.totalCrossSectionSource = enums.CrossSectionSource.FILE + container.totalCrossSectionSource = ( + enums.CrossSectionSource.FILE + ) container.crossSectionFilename = crossSectionSource container.tweakFactor = utils.nthfloat(self._getNextToken(), 0) diff --git a/gudpy/gui/widgets/core/control.py b/gudpy/gui/widgets/core/control.py index ea28e0d78..568074702 100644 --- a/gudpy/gui/widgets/core/control.py +++ b/gudpy/gui/widgets/core/control.py @@ -1,5 +1,4 @@ import os -import re import sys import traceback import typing as typ diff --git a/gudpy/test/test_purge_file.py b/gudpy/test/test_purge_file.py index ec7b09919..0ca8b94fc 100644 --- a/gudpy/test/test_purge_file.py +++ b/gudpy/test/test_purge_file.py @@ -1,6 +1,5 @@ import os from unittest import TestCase -from shutil import copyfile import tempfile from core.purge_file import PurgeFile From ba9e8bcb4fe907739218eb7075ddaec29cb65c6c Mon Sep 17 00:00:00 2001 From: Noella Spitz Date: Mon, 20 May 2024 12:18:01 +0100 Subject: [PATCH 12/78] Small bug fixes --- gudpy/core/gudpy.py | 24 ++++++++++-------------- gudpy/test/test_gud_file.py | 7 ------- gudpy/test/test_gudpy_io.py | 12 ++++++------ 3 files changed, 16 insertions(+), 27 deletions(-) diff --git a/gudpy/core/gudpy.py b/gudpy/core/gudpy.py index 2a599879a..d76c55e94 100644 --- a/gudpy/core/gudpy.py +++ b/gudpy/core/gudpy.py @@ -230,6 +230,7 @@ def runGudrun(self, gudrunFile: GudrunFile = None): f"{self.gudrun.error}" ) self.gudrunOutput = self.gudrun.gudrunOutput + self.save() def iterateGudrun(self, iterator: iterators.Iterator): """Runs gudrun_dcs iteratively while tweaking parameters @@ -462,7 +463,6 @@ def gudrun( gudrunFile: GudrunFile, purge: Purge = None, iterator: iterators.Iterator = None, - save: bool = True ) -> int: self.checkBinary() if not purge: @@ -482,7 +482,7 @@ def gudrun( tmp, gudrunFile.OUTPATH ) - GudPyIO.writeGudrunFile(path) + GudPyIO.writeGudrunFile(gudrunFile, tmp) with subprocess.Popen( [self.BINARY_PATH, path], cwd=tmp, stdout=subprocess.PIPE, @@ -504,8 +504,6 @@ def gudrun( else: self.gudrunOutput = self.organiseOutput( gudrunFile, exclude=purgeFiles) - if save: - GudPyIO.save() gudrunFile.setGudrunDir(self.gudrunOutput.path) self.exitcode = 0 @@ -541,22 +539,21 @@ def singleIteration( gudrun: Gudrun, purge: Purge, prevOutput: handlers.GudrunOutput, - save=True ) -> typ.Tuple[int, str]: # (exitcode, error) modGfFile = self.iterator.performIteration(gudrunFile, prevOutput) - exitcode = gudrun.gudrun(modGfFile, purge, self.iterator, save=save) + exitcode = gudrun.gudrun(modGfFile, purge, self.iterator) if exitcode: return exitcode self.gudrunOutput = gudrun.gudrunOutput return 0 - def iterate(self, purge, save=True) -> typ.Tuple[int, str]: + def iterate(self, purge) -> typ.Tuple[int, str]: prevOutput = None # If the iterator requires a prelimenary run if self.iterator.requireDefault: exitcode = self.gudrunObjects[0].gudrun( - self.gudrunFile, purge, self.iterator, save) + self.gudrunFile, purge, self.iterator) if exitcode: # An exit code != 0 indicates failure self.exitcode = (exitcode, self.gudrunObjects[0].error) return self.exitcode @@ -568,7 +565,7 @@ def iterate(self, purge, save=True) -> typ.Tuple[int, str]: # If object has already been run, skip continue exitcode = self.singleIteration( - self.gudrunFile, gudrun, purge, prevOutput, save) + self.gudrunFile, gudrun, purge, prevOutput) if exitcode: # An exit code != 0 indicates failure self.exitcode = (exitcode, gudrun.error) return self.exitcode @@ -602,22 +599,21 @@ def singleIteration( gudrun: Gudrun, purge: Purge, prevOutput: handlers.GudrunOutput, - save=True ) -> typ.Tuple[int, str]: # (exitcode, error) modGfFile = self.iterator.performIteration(gudrunFile, prevOutput) - exitcode = gudrun.gudrun(modGfFile, purge, self.iterator, save=save) + exitcode = gudrun.gudrun(modGfFile, purge, self.iterator) if exitcode: return exitcode self.gudrunOutput = gudrun.gudrunOutput return 0 - def iterate(self, purge, save=True) -> typ.Tuple[int, str]: + def iterate(self, purge) -> typ.Tuple[int, str]: prevOutput = None # If the iterator requires a prelimenary run if self.iterator.requireDefault: exitcode = self.gudrunObjects[0].gudrun( - self.gudrunFile, purge, self.iterator, save) + self.gudrunFile, purge, self.iterator) if exitcode: # An exit code != 0 indicates failure self.exitcode = (exitcode, self.gudrunObjects[0].error) return self.exitcode @@ -629,7 +625,7 @@ def iterate(self, purge, save=True) -> typ.Tuple[int, str]: # If object has already been run, skip continue exitcode = self.singleIteration( - self.gudrunFile, gudrun, purge, prevOutput, save) + self.gudrunFile, gudrun, purge, prevOutput) if exitcode: # An exit code != 0 indicates failure self.exitcode = (exitcode, gudrun.error) return self.exitcode diff --git a/gudpy/test/test_gud_file.py b/gudpy/test/test_gud_file.py index 9b22af6a3..e268d57f4 100644 --- a/gudpy/test/test_gud_file.py +++ b/gudpy/test/test_gud_file.py @@ -251,13 +251,6 @@ def setUp(self) -> None: self.gudpy.gudrunFile.instrument.dataFileDir = str(dataFileDir) + "/" return super().setUp() - def tearDown(self) -> None: - [os.remove(f) for f in os.listdir() if f not in self.keepsakes] - [os.remove(os.path.join(self.gudpy.projectDir, f)) - for f in os.listdir(self.gudpy.projectDir) - if f not in self.keepsakes] - return super().tearDown() - def testEmptyPath(self): emptyPath = "" self.assertRaises(ParserException, GudFile, emptyPath) diff --git a/gudpy/test/test_gudpy_io.py b/gudpy/test/test_gudpy_io.py index 25d68ec71..f910d9be0 100644 --- a/gudpy/test/test_gudpy_io.py +++ b/gudpy/test/test_gudpy_io.py @@ -43,7 +43,7 @@ def __init__(self): ) gPath = os.path.join(self.tempdir.name, "good_water.txt") - GudPyIO.writeGudrunFile(gPath) + GudPyIO.writeGudrunFile(self.gudpy.gudrunFile, gPath) self.gudpy.loadFromGudrunFile( loadFile=gPath, @@ -805,7 +805,7 @@ def testLoadGudrunFile(self): def testWriteGudrunFile(self): with GudPyContext() as gudpy: - GudPyIO.writeGudrunFile(gudpy.io.loadFile) + GudPyIO.writeGudrunFile(gudpy.gudrunFile, gudpy.io.loadFile) with open( gudpy.io.loadFile, encoding="utf-8" @@ -894,7 +894,7 @@ def valueInLines(value, lines): def testRewriteGudrunFile(self): with GudPyContext() as gudpy: - GudPyIO.writeGudrunFile(gudpy.io.loadFile) + GudPyIO.writeGudrunFile(gudpy.gudrunFile, gudpy.io.loadFile) copyPath = os.path.join( gudpy.gudrunFile.instrument.GudrunInputFileDir, "copyGF.txt" @@ -902,7 +902,7 @@ def testRewriteGudrunFile(self): g1 = gudpy.io.importGudrunFile(gudpy.io.loadFile) g1.instrument.GudrunInputFileDir = ( gudpy.gudrunFile.instrument.GudrunInputFileDir) - GudPyIO.writeGudrunFile(copyPath) + GudPyIO.writeGudrunFile(g1, copyPath) def compareString(string1, string2): return string1 == string2 @@ -944,7 +944,7 @@ def compareString(string1, string2): def testReloadGudrunFile(self): with GudPyContext() as gudpy: - GudPyIO.writeGudrunFile(gudpy.io.loadFile) + GudPyIO.writeGudrunFile(gudpy.gudrunFile, gudpy.io.loadFile) g1 = gudpy.io.importGudrunFile() g1.instrument.GudrunInputFileDir = ( gudpy.gudrunFile.instrument.GudrunInputFileDir) @@ -1517,7 +1517,7 @@ def testAppendExponentialValues(self): gudpy.gudrunFile.OUTPATH ) - gudpy.io.writeGudrunFile(gfFilePath) + gudpy.io.writeGudrunFile(gudpy.gudrunFile, gfFilePath) gudrunFile = GudPyIO.importGudrunFile(gfFilePath) # Test that a default value is appended self.assertEqual( From 5f9209fbd4be07ba57f8354da541da9f96cef1ab Mon Sep 17 00:00:00 2001 From: Noella Spitz Date: Mon, 20 May 2024 13:38:15 +0100 Subject: [PATCH 13/78] Lint: remove unused import --- gudpy/test/test_gudpy_yaml.py | 1 - 1 file changed, 1 deletion(-) diff --git a/gudpy/test/test_gudpy_yaml.py b/gudpy/test/test_gudpy_yaml.py index b58d89e2a..314b5fff2 100644 --- a/gudpy/test/test_gudpy_yaml.py +++ b/gudpy/test/test_gudpy_yaml.py @@ -2,7 +2,6 @@ import os from core import gudpy -from core.enums import Format class TestYAML(TestCase): From 39aacf83a473cc16b001aead0bc3c3366b3b1a4a Mon Sep 17 00:00:00 2001 From: Noella Spitz Date: Mon, 20 May 2024 13:52:52 +0100 Subject: [PATCH 14/78] Test path fis --- gudpy/test/test_gudpy_io.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/gudpy/test/test_gudpy_io.py b/gudpy/test/test_gudpy_io.py index f910d9be0..b39027d07 100644 --- a/gudpy/test/test_gudpy_io.py +++ b/gudpy/test/test_gudpy_io.py @@ -42,8 +42,8 @@ def __init__(self): loadFile=path, ) - gPath = os.path.join(self.tempdir.name, "good_water.txt") - GudPyIO.writeGudrunFile(self.gudpy.gudrunFile, gPath) + gPath = os.path.join(self.tempdir.name, GudrunFile.OUTPATH) + GudPyIO.writeGudrunFile(self.gudpy.gudrunFile, self.tempdir.name) self.gudpy.loadFromGudrunFile( loadFile=gPath, @@ -803,6 +803,12 @@ def testLoadGudrunFile(self): sampleBackgroundsAttrsDict[key], ) + def testSaveAsProject(self): + with GudPyContext() as gudpy: + gudpy.saveAs(os.path.join( + gudpy.projectDir, "test" + )) + def testWriteGudrunFile(self): with GudPyContext() as gudpy: GudPyIO.writeGudrunFile(gudpy.gudrunFile, gudpy.io.loadFile) From 8594fd641addd257033c1bc2b42ac06254463437 Mon Sep 17 00:00:00 2001 From: Noella Spitz Date: Mon, 20 May 2024 14:41:25 +0100 Subject: [PATCH 15/78] Text updating --- gudpy/test/test_gudpy_io.py | 54 ++++++++++++++++++++----------- gudpy/test/test_gudrun_classes.py | 7 ++-- 2 files changed, 40 insertions(+), 21 deletions(-) diff --git a/gudpy/test/test_gudpy_io.py b/gudpy/test/test_gudpy_io.py index b39027d07..b939005c7 100644 --- a/gudpy/test/test_gudpy_io.py +++ b/gudpy/test/test_gudpy_io.py @@ -963,7 +963,8 @@ def testLoadEmptyGudrunFile(self): f = open("test_data.txt", "w", encoding="utf-8") f.close() with self.assertRaises(ParserException) as cm: - GudPyIO.importGudrunFile("test_data.txt") + with GudPyContext() as gudpy: + gudpy.loadFromGudrunFile("test_data.txt") self.assertEqual(( 'INSTRUMENT, BEAM and NORMALISATION' ' were not parsed. It\'s possible the file' @@ -983,7 +984,8 @@ def testLoadMissingInstrument(self): ) with self.assertRaises(ParserException) as cm: - GudPyIO.importGudrunFile("test_data.txt") + with GudPyContext() as gudpy: + gudpy.loadFromGudrunFile("test_data.txt") self.assertEqual(( 'INSTRUMENT, BEAM and NORMALISATION' ' were not parsed. It\'s possible the file' @@ -1005,7 +1007,8 @@ def testLoadMissingBeam(self): + "\n\n}" ) with self.assertRaises(ParserException) as cm: - GudPyIO.importGudrunFile("test_data.txt") + with GudPyContext() as gudpy: + gudpy.loadFromGudrunFile("test_data.txt") self.assertEqual(( 'INSTRUMENT, BEAM and NORMALISATION' ' were not parsed. It\'s possible the file' @@ -1024,7 +1027,8 @@ def testLoadMissingNormalisation(self): f.write("BEAM {\n\n" + str(self.goodBeam) + "\n\n}\n\n") with self.assertRaises(ParserException) as cm: - GudPyIO.importGudrunFile("test_data.txt") + with GudPyContext() as gudpy: + gudpy.loadFromGudrunFile("test_data.txt") self.assertEqual(( 'INSTRUMENT, BEAM and NORMALISATION' ' were not parsed. It\'s possible the file' @@ -1043,7 +1047,8 @@ def testLoadMissingInstrumentAndBeam(self): ) with self.assertRaises(ParserException) as cm: - GudPyIO.importGudrunFile("test_data.txt") + with GudPyContext() as gudpy: + gudpy.loadFromGudrunFile("test_data.txt") self.assertEqual(( 'INSTRUMENT, BEAM and NORMALISATION' ' were not parsed. It\'s possible the file supplied' @@ -1057,7 +1062,8 @@ def testLoadMissingInstrumentAndNormalisation(self): f.write("BEAM {\n\n" + str(self.goodBeam) + "\n\n}") with self.assertRaises(ParserException) as cm: - GudPyIO.importGudrunFile("test_data.txt") + with GudPyContext() as gudpy: + gudpy.loadFromGudrunFile("test_data.txt") self.assertEqual(( 'INSTRUMENT, BEAM and NORMALISATION' ' were not parsed. It\'s possible the file' @@ -1074,7 +1080,8 @@ def testLoadMissingNormalisationAndBeam(self): ) with self.assertRaises(ParserException) as cm: - GudPyIO.importGudrunFile("test_data.txt") + with GudPyContext() as gudpy: + gudpy.loadFromGudrunFile("test_data.txt") self.assertEqual(( 'INSTRUMENT, BEAM and NORMALISATION' ' were not parsed. It\'s possible the file' @@ -1106,7 +1113,8 @@ def testLoadMissingInstrumentAttributesSeq(self): ) with self.assertRaises(ParserException) as cm: - GudPyIO.importGudrunFile("test_data.txt") + with GudPyContext() as gudpy: + gudpy.loadFromGudrunFile("test_data.txt") self.assertEqual( "Whilst parsing Instrument, an exception occured." " The input file is most likely of an incorrect format, " @@ -1140,7 +1148,8 @@ def testLoadMissingInstrumentAttributesRand(self): "INSTRUMENT {\n\n" + str(badInstrument) + "\n\n}" ) with self.assertRaises(ParserException) as cm: - GudPyIO.importGudrunFile("test_data.txt") + with GudPyContext() as gudpy: + gudpy.loadFromGudrunFile("test_data.txt") self.assertEqual( "Whilst parsing Instrument, an exception occured." " The input file is most likely of an incorrect format, " @@ -1175,7 +1184,8 @@ def testLoadMissingBeamAttributesSeq(self): f.write("\n\nBEAM {\n\n" + str(badBeam) + "\n\n}") with self.assertRaises(ParserException) as cm: - GudPyIO.importGudrunFile("test_data.txt") + with GudPyContext() as gudpy: + gudpy.loadFromGudrunFile("test_data.txt") self.assertEqual( "Whilst parsing Beam, an exception occured." " The input file is most likely of an incorrect format, " @@ -1214,7 +1224,8 @@ def testLoadMissingBeamAttributesRand(self): f.write("\n\nBEAM {\n\n" + str(badBeam) + "\n\n}") with self.assertRaises(ParserException) as cm: - GudPyIO.importGudrunFile("test_data.txt") + with GudPyContext() as gudpy: + gudpy.loadFromGudrunFile("test_data.txt") self.assertEqual( "Whilst parsing Beam, an exception occured." " The input file is most likely of an incorrect format, " @@ -1258,7 +1269,8 @@ def testLoadMissingNormalisationAttributesSeq(self): ) with self.assertRaises(ParserException) as cm: - GudPyIO.importGudrunFile("test_data.txt") + with GudPyContext() as gudpy: + gudpy.loadFromGudrunFile("test_data.txt") self.assertEqual( "Whilst parsing Beam, an exception occured." " The input file is most likely of an incorrect format, " @@ -1306,7 +1318,8 @@ def testLoadMissingNormalisationAttributesRand(self): ) with self.assertRaises(ParserException) as cm: - GudPyIO.importGudrunFile("test_data.txt") + with GudPyContext() as gudpy: + gudpy.loadFromGudrunFile("test_data.txt") self.assertEqual( "Whilst parsing Normalisation, an exception occured." " The input file is most likely of an incorrect format, " @@ -1331,7 +1344,8 @@ def testLoadMissingSampleBackgroundAttributes(self): ) f.write("\n\n{}\n\nEND".format(str(badSampleBackground))) with self.assertRaises(ParserException) as cm: - GudPyIO.importGudrunFile("test_data.txt") + with GudPyContext() as gudpy: + gudpy.loadFromGudrunFile("test_data.txt") self.assertEqual( "Whilst parsing Sample Background, an exception occured." " The input file is most likely of an incorrect format, " @@ -1386,7 +1400,8 @@ def testLoadMissingSampleAttributesSeq(self): f.write("\n\n{}\n\nEND".format(str(badSampleBackground))) with self.assertRaises(ParserException) as cm: - GudPyIO.importGudrunFile("test_data.txt") + with GudPyContext() as gudpy: + gudpy.loadFromGudrunFile("test_data.txt") self.assertEqual( "Whilst parsing Sample, an exception occured." " The input file is most likely of an incorrect format, " @@ -1444,7 +1459,8 @@ def testLoadMissingSampleAttributesRand(self): f.write("\n\n{}\n\nEND".format(str(badSampleBackground))) with self.assertRaises(ParserException) as cm: - GudPyIO.importGudrunFile("test_data.txt") + with GudPyContext() as gudpy: + gudpy.loadFromGudrunFile("test_data.txt") self.assertEqual( "Whilst parsing Sample, an exception occured." " The input file is most likely of an incorrect format, " @@ -1504,7 +1520,8 @@ def testLoadMissingContainerAttributesSeq(self): ) f.write("\n\n{}\n\nEND".format(str(badSampleBackground))) with self.assertRaises(ParserException) as cm: - GudPyIO.importGudrunFile("test_data.txt") + with GudPyContext() as gudpy: + gudpy.loadFromGudrunFile("test_data.txt") self.assertEqual( "Whilst parsing Container, an exception occured." " The input file is most likely of an incorrect format, " @@ -1586,7 +1603,8 @@ def testLoadMissingContainerAttributesRand(self): ) f.write("\n\n{}\n\nEND".format(str(badSampleBackground))) with self.assertRaises(ParserException) as cm: - GudPyIO.importGudrunFile("test_data.txt") + with GudPyContext() as gudpy: + gudpy.loadFromGudrunFile("test_data.txt") self.assertEqual( "Whilst parsing Container, an exception occured." " The input file is most likely of an incorrect format, " diff --git a/gudpy/test/test_gudrun_classes.py b/gudpy/test/test_gudrun_classes.py index a783058d4..b3e21302a 100644 --- a/gudpy/test/test_gudrun_classes.py +++ b/gudpy/test/test_gudrun_classes.py @@ -15,17 +15,18 @@ FTModes, Instruments, Scales, UnitsOfDensity, MergeWeights, NormalisationType, OutputUnits, Geometry, CrossSectionSource ) +from core.io.gudpy_io import GudPyIO class TestGudrunClasses(TestCase): def testEmptyPath(self): - emptyPath = "" - self.assertRaises(RuntimeError, GudrunFile, loadFile=emptyPath) + self.assertRaises( + RuntimeError, GudPyIO.importGudrunFile, loadFile=emptyPath) def testInvalidPath(self): invalidPath = "invalid_path" - self.assertRaises(ParserException, GudrunFile, loadFile=invalidPath) + self.assertRaises(ParserException, GudPyIO.importGudrunFile, , loadFile=invalidPath) def testInstrumentInitDataTypes(self): From 904ec39cd3deeae69aa224a362995e61caf66b16 Mon Sep 17 00:00:00 2001 From: Noella Spitz Date: Mon, 20 May 2024 14:46:01 +0100 Subject: [PATCH 16/78] Syntax fix --- gudpy/test/test_gudrun_classes.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/gudpy/test/test_gudrun_classes.py b/gudpy/test/test_gudrun_classes.py index b3e21302a..2a44ae72a 100644 --- a/gudpy/test/test_gudrun_classes.py +++ b/gudpy/test/test_gudrun_classes.py @@ -26,7 +26,8 @@ def testEmptyPath(self): def testInvalidPath(self): invalidPath = "invalid_path" - self.assertRaises(ParserException, GudPyIO.importGudrunFile, , loadFile=invalidPath) + self.assertRaises( + ParserException, GudPyIO.importGudrunFile, loadFile=invalidPath) def testInstrumentInitDataTypes(self): From a0ad5e51d3edc7d57e22e9c844c8acf6c20f2f9c Mon Sep 17 00:00:00 2001 From: Noella Spitz Date: Mon, 20 May 2024 15:02:04 +0100 Subject: [PATCH 17/78] Remove unused import, allow tests without lint --- .github/workflows/pr.yml | 1 - gudpy/test/test_gudrun_classes.py | 1 - 2 files changed, 2 deletions(-) diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index ecf008222..10ad186e7 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -18,7 +18,6 @@ jobs: uses: "./.github/workflows/lint" Test: - needs: Lint strategy: fail-fast: false matrix: diff --git a/gudpy/test/test_gudrun_classes.py b/gudpy/test/test_gudrun_classes.py index 2a44ae72a..236cfd495 100644 --- a/gudpy/test/test_gudrun_classes.py +++ b/gudpy/test/test_gudrun_classes.py @@ -1,7 +1,6 @@ from unittest import TestCase from core.exception import ParserException -from core.gudrun_file import GudrunFile from core.beam import Beam from core.composition import Composition from core.container import Container From 912970bd4430899364915116a54b8d71387c34c3 Mon Sep 17 00:00:00 2001 From: Noella Spitz Date: Mon, 20 May 2024 15:18:24 +0100 Subject: [PATCH 18/78] update test --- gudpy/core/io/gudrun_file_parser.py | 2 +- gudpy/test/test_gudpy_io.py | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/gudpy/core/io/gudrun_file_parser.py b/gudpy/core/io/gudrun_file_parser.py index b95c36d39..d85e1b9f1 100644 --- a/gudpy/core/io/gudrun_file_parser.py +++ b/gudpy/core/io/gudrun_file_parser.py @@ -117,7 +117,7 @@ def parse( if not v: unparsed.append(k) raise ParserException(( - f'{unparsed}' + f'{", ".join(unparsed)}' ' were not parsed. It\'s possible the file' ' supplied is of an incorrect format!' )) diff --git a/gudpy/test/test_gudpy_io.py b/gudpy/test/test_gudpy_io.py index b939005c7..add20595a 100644 --- a/gudpy/test/test_gudpy_io.py +++ b/gudpy/test/test_gudpy_io.py @@ -966,7 +966,7 @@ def testLoadEmptyGudrunFile(self): with GudPyContext() as gudpy: gudpy.loadFromGudrunFile("test_data.txt") self.assertEqual(( - 'INSTRUMENT, BEAM and NORMALISATION' + 'INSTRUMENT, BEAM, NORMALISATION,' ' were not parsed. It\'s possible the file' ' supplied is of an incorrect format!' ), @@ -987,7 +987,7 @@ def testLoadMissingInstrument(self): with GudPyContext() as gudpy: gudpy.loadFromGudrunFile("test_data.txt") self.assertEqual(( - 'INSTRUMENT, BEAM and NORMALISATION' + 'INSTRUMENT, BEAM, NORMALISATION,' ' were not parsed. It\'s possible the file' ' supplied is of an incorrect format!' ), @@ -1010,7 +1010,7 @@ def testLoadMissingBeam(self): with GudPyContext() as gudpy: gudpy.loadFromGudrunFile("test_data.txt") self.assertEqual(( - 'INSTRUMENT, BEAM and NORMALISATION' + 'INSTRUMENT, BEAM, NORMALISATION,' ' were not parsed. It\'s possible the file' ' supplied is of an incorrect format!' ), @@ -1030,7 +1030,7 @@ def testLoadMissingNormalisation(self): with GudPyContext() as gudpy: gudpy.loadFromGudrunFile("test_data.txt") self.assertEqual(( - 'INSTRUMENT, BEAM and NORMALISATION' + 'INSTRUMENT, BEAM, NORMALISATION,' ' were not parsed. It\'s possible the file' ' supplied is of an incorrect format!' ), @@ -1050,7 +1050,7 @@ def testLoadMissingInstrumentAndBeam(self): with GudPyContext() as gudpy: gudpy.loadFromGudrunFile("test_data.txt") self.assertEqual(( - 'INSTRUMENT, BEAM and NORMALISATION' + 'INSTRUMENT, BEAM, NORMALISATION,' ' were not parsed. It\'s possible the file supplied' ' is of an incorrect format!'), str(cm.exception), @@ -1065,7 +1065,7 @@ def testLoadMissingInstrumentAndNormalisation(self): with GudPyContext() as gudpy: gudpy.loadFromGudrunFile("test_data.txt") self.assertEqual(( - 'INSTRUMENT, BEAM and NORMALISATION' + 'INSTRUMENT, BEAM, NORMALISATION,' ' were not parsed. It\'s possible the file' ' supplied is of an incorrect format!' ), @@ -1083,7 +1083,7 @@ def testLoadMissingNormalisationAndBeam(self): with GudPyContext() as gudpy: gudpy.loadFromGudrunFile("test_data.txt") self.assertEqual(( - 'INSTRUMENT, BEAM and NORMALISATION' + 'INSTRUMENT, BEAM, NORMALISATION,' ' were not parsed. It\'s possible the file' ' supplied is of an incorrect format!' ), From df2b13020efc3f7be4a2c0d777eb468b6eb339eb Mon Sep 17 00:00:00 2001 From: Noella Spitz Date: Mon, 20 May 2024 15:39:16 +0100 Subject: [PATCH 19/78] Revert error message --- gudpy/core/io/gudrun_file_parser.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gudpy/core/io/gudrun_file_parser.py b/gudpy/core/io/gudrun_file_parser.py index d85e1b9f1..925fcdf04 100644 --- a/gudpy/core/io/gudrun_file_parser.py +++ b/gudpy/core/io/gudrun_file_parser.py @@ -117,7 +117,7 @@ def parse( if not v: unparsed.append(k) raise ParserException(( - f'{", ".join(unparsed)}' + 'INSTRUMENT, BEAM, NORMALISATION,' ' were not parsed. It\'s possible the file' ' supplied is of an incorrect format!' )) From 3f3d212c967fda869e35e4918d3729d287eba67f Mon Sep 17 00:00:00 2001 From: Noella Spitz Date: Mon, 20 May 2024 16:15:51 +0100 Subject: [PATCH 20/78] Add output folder attr to tests --- gudpy/test/test_gudpy_io.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/gudpy/test/test_gudpy_io.py b/gudpy/test/test_gudpy_io.py index add20595a..e7b7168ba 100644 --- a/gudpy/test/test_gudpy_io.py +++ b/gudpy/test/test_gudpy_io.py @@ -391,6 +391,7 @@ def setUp(self) -> None: "grBroadening": 0.0, "powerForBroadening": 0.0, "stepSize": 0.0, + "outputFolder": '', "yamlignore": { "runAsSample", "topHatW", @@ -454,6 +455,8 @@ def setUp(self) -> None: "scatteringFraction": 1.0, "attenuationCoefficient": 0.0, "containers": [self.expectedContainerA], + "outputFolder": '', + "sampleFile": '', "yamlignore": { "yamlignore", } @@ -509,6 +512,8 @@ def setUp(self) -> None: "scatteringFraction": 1.0, "attenuationCoefficient": 0.0, "containers": [self.expectedContainerB], + "outputFolder": '', + "sampleFile": '', "yamlignore": { "yamlignore" } @@ -564,6 +569,8 @@ def setUp(self) -> None: "scatteringFraction": 1.0, "attenuationCoefficient": 0.0, "containers": [self.expectedContainerC], + "outputFolder": '', + "sampleFile": '', "yamlignore": { "yamlignore" } @@ -620,6 +627,8 @@ def setUp(self) -> None: "scatteringFraction": 1.0, "attenuationCoefficient": 0.0, "containers": [self.expectedContainerD], + "outputFolder": '', + "sampleFile": '', "yamlignore": { "yamlignore" } @@ -645,6 +654,7 @@ def setUp(self) -> None: self.expectedSampleB, self.expectedSampleC, ], + "outputFolder": '', "writeAllSamples": True, "yamlignore": { "writeAllSamples", @@ -750,7 +760,6 @@ def testLoadGudrunFile(self): ) ) for key_ in sampleAttrsDict.keys(): - if key_ == "containers": for j, container in enumerate(sample[key_]): containerAttrsDict = ( @@ -1540,7 +1549,7 @@ def testAppendExponentialValues(self): gudpy.gudrunFile.OUTPATH ) - gudpy.io.writeGudrunFile(gudpy.gudrunFile, gfFilePath) + gudpy.io.writeGudrunFile(gudpy.gudrunFile, gudpy.io.projectDir) gudrunFile = GudPyIO.importGudrunFile(gfFilePath) # Test that a default value is appended self.assertEqual( From a8c38540d3848d8c5f71d5c618f809d637bf8606 Mon Sep 17 00:00:00 2001 From: Noella Spitz Date: Mon, 20 May 2024 16:29:55 +0100 Subject: [PATCH 21/78] Update tests --- gudpy/core/io/gudrun_file_parser.py | 1 - gudpy/core/iterators.py | 2 +- gudpy/test/test_gudpy_io.py | 2 +- gudpy/test/test_gudpy_workflows.py | 17 +- gudpy/test/test_gudpy_yaml.py | 251 ++++++++++++++-------------- gudpy/test/test_gudrun_classes.py | 4 +- 6 files changed, 139 insertions(+), 138 deletions(-) diff --git a/gudpy/core/io/gudrun_file_parser.py b/gudpy/core/io/gudrun_file_parser.py index 925fcdf04..5404da501 100644 --- a/gudpy/core/io/gudrun_file_parser.py +++ b/gudpy/core/io/gudrun_file_parser.py @@ -109,7 +109,6 @@ def parse( elif kwd == "NORMALISATION": normalisation = self._parseNormalisation() KEYWORDS[kwd] = True - # If we didn't parse each one of the keywords, then panic. if not all(KEYWORDS.values()) and not config: unparsed = [] diff --git a/gudpy/core/iterators.py b/gudpy/core/iterators.py index 34e8f1ac7..2c3ac3ab1 100644 --- a/gudpy/core/iterators.py +++ b/gudpy/core/iterators.py @@ -495,7 +495,7 @@ def setSelfScatteringFiles(self, scale, gudrunFile, prevOutput=None): for sample in sampleBackground.samples: if sample.runThisSample and len(sample.dataFiles): target = sample - filename = target.dataFiles[0].filename + filename = target.dataFilenames[0] targetFile = ( prevOutput.output( sample.name, filename, suffix) diff --git a/gudpy/test/test_gudpy_io.py b/gudpy/test/test_gudpy_io.py index e7b7168ba..3022d7f3e 100644 --- a/gudpy/test/test_gudpy_io.py +++ b/gudpy/test/test_gudpy_io.py @@ -960,7 +960,7 @@ def compareString(string1, string2): def testReloadGudrunFile(self): with GudPyContext() as gudpy: GudPyIO.writeGudrunFile(gudpy.gudrunFile, gudpy.io.loadFile) - g1 = gudpy.io.importGudrunFile() + g1 = gudpy.io.importGudrunFile(gudpy.io.loadFile) g1.instrument.GudrunInputFileDir = ( gudpy.gudrunFile.instrument.GudrunInputFileDir) self.assertEqual( diff --git a/gudpy/test/test_gudpy_workflows.py b/gudpy/test/test_gudpy_workflows.py index 5e8e2f101..f0102609c 100644 --- a/gudpy/test/test_gudpy_workflows.py +++ b/gudpy/test/test_gudpy_workflows.py @@ -76,10 +76,9 @@ def testGudPyDCS(self): actualMintFile = ("test/TestData/water-ref/plain/" f"{mintFilename}.mint01") - actualData = open(gudpy.gudrunOutput.sampleOutputs[ - sample.name].outputs[sample.dataFiles[0]][".mint01"], - "r", encoding="utf-8" - ).readlines()[10:] + actualData = open(sample.dataFiles[0].mintFile, + "r", encoding="utf-8" + ).readlines()[10:] expectedData = open( actualMintFile, "r", encoding="utf-8" ).readlines()[10:] @@ -244,8 +243,7 @@ def testGudPyIterateBySubtractingWavelength(self): ) actualData = open( - gudpy.gudrunIterator.iterator.gudrunOutputs[-1].output( - sample.name, sample.dataFiles[0], ".mint01"), + sample.dataFiles[0].mintFile, "r", encoding="utf-8" ).readlines()[10:] expectedData = open( @@ -273,12 +271,7 @@ def testGudPyIterateBySubtractingWavelength(self): ) actualData = open( - gudpy.gudrunIterator.iterator.gudrunOutputs[ - len( - gudpy.gudrunIterator.iterator.gudrunOutputs - ) - 2 - ].output( - sample.name, sample.dataFiles[0], ".msubw01"), + sample.dataFiles[0].msubwFile, "r", encoding="utf-8" ).readlines()[10:] expectedData = open( diff --git a/gudpy/test/test_gudpy_yaml.py b/gudpy/test/test_gudpy_yaml.py index 314b5fff2..7656a88e1 100644 --- a/gudpy/test/test_gudpy_yaml.py +++ b/gudpy/test/test_gudpy_yaml.py @@ -1,4 +1,5 @@ from unittest import TestCase +import tempfile import os from core import gudpy @@ -6,142 +7,150 @@ class TestYAML(TestCase): def testYAML(self): - gudpy1 = gudpy.GudPy() - gudpy1.loadFromGudrunFile( - loadFile="test/TestData/NIMROD-water/water.txt", - ) - gudpy1.save(path="test/TestData/NIMROD-water/water.yaml") - gf1 = gudpy1.gudrunFile - - gudpy2 = gudpy.GudPy() - gudpy2.loadFromYamlFile( - loadFile="test/TestData/NIMROD-water/water.yaml", - ) - gf2 = gudpy2.gudrunFile - - gf1.instrument.GudrunInputFileDir = os.path.abspath( - gf1.instrument.GudrunInputFileDir) - gf2.instrument.GudrunInputFileDir = os.path.abspath( - gf2.instrument.GudrunInputFileDir) - - self.assertDictEqual( - gf1.instrument.__dict__, gf2.instrument.__dict__) - self.assertDictEqual(gf2.beam.__dict__, gf2.beam.__dict__) - - normalisationDataFilesA = gf1.normalisation.__dict__.pop("dataFiles") - normalisationDataFilesBgA = gf1.normalisation.__dict__.pop( - "dataFilesBg" - ) - normalisationCompositionA = gf1.normalisation.__dict__.pop( - "composition" - ) - normalisationElementsA = normalisationCompositionA.__dict__.pop( - "elements" - ) - - normalisationDataFilesB = gf2.normalisation.__dict__.pop("dataFiles") - normalisationDataFilesBgB = gf2.normalisation.__dict__.pop( - "dataFilesBg" - ) - normalisationCompositionB = gf2.normalisation.__dict__.pop( - "composition" - ) - normalisationElementsB = normalisationCompositionB.__dict__.pop( - "elements" - ) - - self.assertDictEqual( - normalisationDataFilesA.__dict__, normalisationDataFilesB.__dict__ - ) - self.assertDictEqual( - normalisationDataFilesBgA.__dict__, - normalisationDataFilesBgB.__dict__, - ) - self.assertDictEqual( - normalisationCompositionA.__dict__, - normalisationCompositionB.__dict__, - ) - self.assertDictEqual( - gf1.normalisation.__dict__, gf2.normalisation.__dict__ - ) - - for elementA, elementB in zip( - normalisationElementsA, normalisationElementsB - ): - self.assertDictEqual(elementA.__dict__, elementB.__dict__) - - sampleBackgroundDataFilesA = gf1.sampleBackgrounds[0].__dict__.pop( - "dataFiles" - ) - sampleBackgroundSamplesA = gf1.sampleBackgrounds[0].__dict__.pop( - "samples" - ) - - sampleBackgroundDataFilesB = gf2.sampleBackgrounds[0].__dict__.pop( - "dataFiles" - ) - sampleBackgroundSamplesB = gf2.sampleBackgrounds[0].__dict__.pop( - "samples" - ) - - self.assertDictEqual( - sampleBackgroundDataFilesA.__dict__, - sampleBackgroundDataFilesB.__dict__, - ) - self.assertDictEqual( - gf1.sampleBackgrounds[0].__dict__, - gf2.sampleBackgrounds[0].__dict__, - ) - - for sampleA, sampleB in zip( - sampleBackgroundSamplesA, sampleBackgroundSamplesB - ): - sampleDataFilesA = sampleA.__dict__.pop("dataFiles") - sampleCompositionA = sampleA.__dict__.pop("composition") - sampleElementsA = sampleCompositionA.__dict__.pop("elements") - sampleContainersA = sampleA.__dict__.pop("containers") - - sampleDataFilesB = sampleB.__dict__.pop("dataFiles") - sampleCompositionB = sampleB.__dict__.pop("composition") - sampleElementsB = sampleCompositionB.__dict__.pop("elements") - sampleContainersB = sampleB.__dict__.pop("containers") + with tempfile.TemporaryDirectory() as tmp: + gudpy1 = gudpy.GudPy() + gudpy1.loadFromGudrunFile( + loadFile="test/TestData/NIMROD-water/water.txt", + ) + gudpy1.saveAs(os.path.join(tmp.name, "water")) + gf1 = gudpy1.gudrunFile + + gudpy2 = gudpy.GudPy() + gudpy2.loadFromProject(os.path.join(tmp.name, "water")) + gf2 = gudpy2.gudrunFile + + gf1.instrument.GudrunInputFileDir = os.path.abspath( + gf1.instrument.GudrunInputFileDir) + gf2.instrument.GudrunInputFileDir = os.path.abspath( + gf2.instrument.GudrunInputFileDir) + + self.assertDictEqual( + gf1.instrument.__dict__, gf2.instrument.__dict__) + self.assertDictEqual(gf2.beam.__dict__, gf2.beam.__dict__) + + normalisationDataFilesA = gf1.normalisation.__dict__.pop( + "dataFiles") + normalisationDataFilesBgA = gf1.normalisation.__dict__.pop( + "dataFilesBg" + ) + normalisationCompositionA = gf1.normalisation.__dict__.pop( + "composition" + ) + normalisationElementsA = normalisationCompositionA.__dict__.pop( + "elements" + ) + normalisationDataFilesB = gf2.normalisation.__dict__.pop( + "dataFiles") + normalisationDataFilesBgB = gf2.normalisation.__dict__.pop( + "dataFilesBg" + ) + normalisationCompositionB = gf2.normalisation.__dict__.pop( + "composition" + ) + normalisationElementsB = normalisationCompositionB.__dict__.pop( + "elements" + ) + + self.assertDictEqual( + normalisationDataFilesA.__dict__, + normalisationDataFilesB.__dict__ + ) + self.assertDictEqual( + normalisationDataFilesBgA.__dict__, + normalisationDataFilesBgB.__dict__, + ) self.assertDictEqual( - sampleDataFilesA.__dict__, sampleDataFilesB.__dict__ + normalisationCompositionA.__dict__, + normalisationCompositionB.__dict__, ) self.assertDictEqual( - sampleCompositionA.__dict__, sampleCompositionB.__dict__ + gf1.normalisation.__dict__, gf2.normalisation.__dict__ ) - for elementA, elementB in zip(sampleElementsA, sampleElementsB): + + for elementA, elementB in zip( + normalisationElementsA, normalisationElementsB + ): self.assertDictEqual(elementA.__dict__, elementB.__dict__) - self.assertDictEqual(sampleA.__dict__, sampleB.__dict__) + sampleBackgroundDataFilesA = gf1.sampleBackgrounds[0].__dict__.pop( + "dataFiles" + ) + sampleBackgroundSamplesA = gf1.sampleBackgrounds[0].__dict__.pop( + "samples" + ) - for containerA, containerB in zip( - sampleContainersA, sampleContainersB + sampleBackgroundDataFilesB = gf2.sampleBackgrounds[0].__dict__.pop( + "dataFiles" + ) + sampleBackgroundSamplesB = gf2.sampleBackgrounds[0].__dict__.pop( + "samples" + ) + + self.assertDictEqual( + sampleBackgroundDataFilesA.__dict__, + sampleBackgroundDataFilesB.__dict__, + ) + self.assertDictEqual( + gf1.sampleBackgrounds[0].__dict__, + gf2.sampleBackgrounds[0].__dict__, + ) + + for sampleA, sampleB in zip( + sampleBackgroundSamplesA, sampleBackgroundSamplesB ): - containerDataFilesA = containerA.__dict__.pop("dataFiles") - containerCompositionA = containerA.__dict__.pop("composition") - containerElementsA = containerCompositionA.__dict__.pop( - "elements" - ) + sampleDataFilesA = sampleA.__dict__.pop("dataFiles") + sampleCompositionA = sampleA.__dict__.pop("composition") + sampleElementsA = sampleCompositionA.__dict__.pop("elements") + sampleContainersA = sampleA.__dict__.pop("containers") - containerDataFilesB = containerB.__dict__.pop("dataFiles") - containerCompositionB = containerB.__dict__.pop("composition") - containerElementsB = containerCompositionB.__dict__.pop( - "elements" - ) + sampleDataFilesB = sampleB.__dict__.pop("dataFiles") + sampleCompositionB = sampleB.__dict__.pop("composition") + sampleElementsB = sampleCompositionB.__dict__.pop("elements") + sampleContainersB = sampleB.__dict__.pop("containers") self.assertDictEqual( - containerDataFilesA.__dict__, containerDataFilesB.__dict__ + sampleDataFilesA.__dict__, sampleDataFilesB.__dict__ ) self.assertDictEqual( - containerCompositionA.__dict__, - containerCompositionB.__dict__, + sampleCompositionA.__dict__, sampleCompositionB.__dict__ ) for elementA, elementB in zip( - containerElementsA, containerElementsB - ): + sampleElementsA, sampleElementsB): self.assertDictEqual(elementA.__dict__, elementB.__dict__) - self.assertDictEqual(containerA.__dict__, containerB.__dict__) + self.assertDictEqual(sampleA.__dict__, sampleB.__dict__) + + for containerA, containerB in zip( + sampleContainersA, sampleContainersB + ): + containerDataFilesA = containerA.__dict__.pop("dataFiles") + containerCompositionA = containerA.__dict__.pop( + "composition") + containerElementsA = containerCompositionA.__dict__.pop( + "elements" + ) + + containerDataFilesB = containerB.__dict__.pop("dataFiles") + containerCompositionB = containerB.__dict__.pop( + "composition") + containerElementsB = containerCompositionB.__dict__.pop( + "elements" + ) + + self.assertDictEqual( + containerDataFilesA.__dict__, + containerDataFilesB.__dict__ + ) + self.assertDictEqual( + containerCompositionA.__dict__, + containerCompositionB.__dict__, + ) + for elementA, elementB in zip( + containerElementsA, containerElementsB + ): + self.assertDictEqual( + elementA.__dict__, elementB.__dict__) + + self.assertDictEqual( + containerA.__dict__, containerB.__dict__) diff --git a/gudpy/test/test_gudrun_classes.py b/gudpy/test/test_gudrun_classes.py index 236cfd495..1a5e172de 100644 --- a/gudpy/test/test_gudrun_classes.py +++ b/gudpy/test/test_gudrun_classes.py @@ -21,12 +21,12 @@ class TestGudrunClasses(TestCase): def testEmptyPath(self): emptyPath = "" self.assertRaises( - RuntimeError, GudPyIO.importGudrunFile, loadFile=emptyPath) + RuntimeError, GudPyIO.importGudrunFile, emptyPath) def testInvalidPath(self): invalidPath = "invalid_path" self.assertRaises( - ParserException, GudPyIO.importGudrunFile, loadFile=invalidPath) + ParserException, GudPyIO.importGudrunFile, invalidPath) def testInstrumentInitDataTypes(self): From e1a141d9c0f323c6c7b164be0761dfee06d974e0 Mon Sep 17 00:00:00 2001 From: Noella Spitz Date: Tue, 21 May 2024 11:28:20 +0100 Subject: [PATCH 22/78] Add writeGudrunFileTo method --- gudpy/core/io/gudrun_file_parser.py | 14 ++++++++------ gudpy/test/test_gudpy_io.py | 12 ++++++------ 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/gudpy/core/io/gudrun_file_parser.py b/gudpy/core/io/gudrun_file_parser.py index 5404da501..6ad1fe864 100644 --- a/gudpy/core/io/gudrun_file_parser.py +++ b/gudpy/core/io/gudrun_file_parser.py @@ -29,18 +29,20 @@ def __init__(self): self.stream = None self.config = False - @classmethod - def writeGudrunFile(cls, gudrunFile: GudrunFile, runDir: str): - gudrunFile.setGudrunDir(runDir) + def writeGudrunFileTo(cls, gudrunFile: GudrunFile, path: str): + gudrunFile.setGudrunDir(os.path.dirname(path)) for sampleBackground in gudrunFile.sampleBackgrounds: sampleBackground.writeAllSamples = False - writePath = os.path.join(runDir, gudrunFile.OUTPATH) - - with open(writePath, "w", encoding="utf-8") as f: + with open(path, "w", encoding="utf-8") as f: f.write(str(gudrunFile)) f.close() + @classmethod + def writeGudrunFile(cls, gudrunFile: GudrunFile, runDir: str): + cls.writeGudrunFile(gudrunFile, os.path.join( + runDir, GudrunFile.OUTPATH)) + @classmethod def export(cls, gudrunFile: GudrunFile, path: str): for sampleBackground in gudrunFile.sampleBackgrounds: diff --git a/gudpy/test/test_gudpy_io.py b/gudpy/test/test_gudpy_io.py index 3022d7f3e..2fff3c034 100644 --- a/gudpy/test/test_gudpy_io.py +++ b/gudpy/test/test_gudpy_io.py @@ -43,7 +43,7 @@ def __init__(self): ) gPath = os.path.join(self.tempdir.name, GudrunFile.OUTPATH) - GudPyIO.writeGudrunFile(self.gudpy.gudrunFile, self.tempdir.name) + GudPyIO.writeGudrunFileTo(self.gudpy.gudrunFile, gPath) self.gudpy.loadFromGudrunFile( loadFile=gPath, @@ -820,7 +820,7 @@ def testSaveAsProject(self): def testWriteGudrunFile(self): with GudPyContext() as gudpy: - GudPyIO.writeGudrunFile(gudpy.gudrunFile, gudpy.io.loadFile) + GudPyIO.writeGudrunFileTo(gudpy.gudrunFile, gudpy.io.loadFile) with open( gudpy.io.loadFile, encoding="utf-8" @@ -909,7 +909,7 @@ def valueInLines(value, lines): def testRewriteGudrunFile(self): with GudPyContext() as gudpy: - GudPyIO.writeGudrunFile(gudpy.gudrunFile, gudpy.io.loadFile) + GudPyIO.writeGudrunFileTo(gudpy.gudrunFile, gudpy.io.loadFile) copyPath = os.path.join( gudpy.gudrunFile.instrument.GudrunInputFileDir, "copyGF.txt" @@ -917,7 +917,7 @@ def testRewriteGudrunFile(self): g1 = gudpy.io.importGudrunFile(gudpy.io.loadFile) g1.instrument.GudrunInputFileDir = ( gudpy.gudrunFile.instrument.GudrunInputFileDir) - GudPyIO.writeGudrunFile(g1, copyPath) + GudPyIO.writeGudrunFileTo(g1, copyPath) def compareString(string1, string2): return string1 == string2 @@ -959,7 +959,7 @@ def compareString(string1, string2): def testReloadGudrunFile(self): with GudPyContext() as gudpy: - GudPyIO.writeGudrunFile(gudpy.gudrunFile, gudpy.io.loadFile) + GudPyIO.writeGudrunFileTo(gudpy.gudrunFile, gudpy.io.loadFile) g1 = gudpy.io.importGudrunFile(gudpy.io.loadFile) g1.instrument.GudrunInputFileDir = ( gudpy.gudrunFile.instrument.GudrunInputFileDir) @@ -1549,7 +1549,7 @@ def testAppendExponentialValues(self): gudpy.gudrunFile.OUTPATH ) - gudpy.io.writeGudrunFile(gudpy.gudrunFile, gudpy.io.projectDir) + gudpy.io.writeGudrunFileTo(gudpy.gudrunFile, gfFilePath) gudrunFile = GudPyIO.importGudrunFile(gfFilePath) # Test that a default value is appended self.assertEqual( From 234eeebede14f5a9073a888a671fd3a3e016df47 Mon Sep 17 00:00:00 2001 From: Noella Spitz Date: Tue, 21 May 2024 13:31:12 +0100 Subject: [PATCH 23/78] Switch to correct parser --- gudpy/test/test_gudpy_io.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/gudpy/test/test_gudpy_io.py b/gudpy/test/test_gudpy_io.py index 2fff3c034..679e76a1a 100644 --- a/gudpy/test/test_gudpy_io.py +++ b/gudpy/test/test_gudpy_io.py @@ -26,6 +26,7 @@ ) from core import gudpy as gp from core.io.gudpy_io import GudPyIO +from core.io.gudrun_file_parser import GudrunFileParser class GudPyContext: @@ -43,7 +44,7 @@ def __init__(self): ) gPath = os.path.join(self.tempdir.name, GudrunFile.OUTPATH) - GudPyIO.writeGudrunFileTo(self.gudpy.gudrunFile, gPath) + GudrunFileParser.writeGudrunFileTo(self.gudpy.gudrunFile, gPath) self.gudpy.loadFromGudrunFile( loadFile=gPath, @@ -820,7 +821,8 @@ def testSaveAsProject(self): def testWriteGudrunFile(self): with GudPyContext() as gudpy: - GudPyIO.writeGudrunFileTo(gudpy.gudrunFile, gudpy.io.loadFile) + GudrunFileParser.writeGudrunFileTo( + gudpy.gudrunFile, gudpy.io.loadFile) with open( gudpy.io.loadFile, encoding="utf-8" @@ -909,7 +911,8 @@ def valueInLines(value, lines): def testRewriteGudrunFile(self): with GudPyContext() as gudpy: - GudPyIO.writeGudrunFileTo(gudpy.gudrunFile, gudpy.io.loadFile) + GudrunFileParser.writeGudrunFileTo( + gudpy.gudrunFile, gudpy.io.loadFile) copyPath = os.path.join( gudpy.gudrunFile.instrument.GudrunInputFileDir, "copyGF.txt" @@ -917,7 +920,7 @@ def testRewriteGudrunFile(self): g1 = gudpy.io.importGudrunFile(gudpy.io.loadFile) g1.instrument.GudrunInputFileDir = ( gudpy.gudrunFile.instrument.GudrunInputFileDir) - GudPyIO.writeGudrunFileTo(g1, copyPath) + GudrunFileParser.writeGudrunFileTo(g1, copyPath) def compareString(string1, string2): return string1 == string2 @@ -959,7 +962,8 @@ def compareString(string1, string2): def testReloadGudrunFile(self): with GudPyContext() as gudpy: - GudPyIO.writeGudrunFileTo(gudpy.gudrunFile, gudpy.io.loadFile) + GudrunFileParser.writeGudrunFileTo( + gudpy.gudrunFile, gudpy.io.loadFile) g1 = gudpy.io.importGudrunFile(gudpy.io.loadFile) g1.instrument.GudrunInputFileDir = ( gudpy.gudrunFile.instrument.GudrunInputFileDir) From 2ed178aaddab905d44a4d8165f0ca615e8e573b5 Mon Sep 17 00:00:00 2001 From: Noella Spitz Date: Tue, 21 May 2024 13:58:21 +0100 Subject: [PATCH 24/78] Add decorator --- gudpy/core/io/gudrun_file_parser.py | 1 + 1 file changed, 1 insertion(+) diff --git a/gudpy/core/io/gudrun_file_parser.py b/gudpy/core/io/gudrun_file_parser.py index 6ad1fe864..cccd69a11 100644 --- a/gudpy/core/io/gudrun_file_parser.py +++ b/gudpy/core/io/gudrun_file_parser.py @@ -29,6 +29,7 @@ def __init__(self): self.stream = None self.config = False + @classmethod def writeGudrunFileTo(cls, gudrunFile: GudrunFile, path: str): gudrunFile.setGudrunDir(os.path.dirname(path)) for sampleBackground in gudrunFile.sampleBackgrounds: From 9b6b81337619a4bb65bd35a78afc5f55603aff64 Mon Sep 17 00:00:00 2001 From: Noella Spitz Date: Tue, 21 May 2024 15:10:39 +0100 Subject: [PATCH 25/78] Bugfixes --- gudpy/core/container.py | 3 +-- gudpy/core/io/gudrun_file_parser.py | 2 +- gudpy/core/iterators.py | 12 ++++-------- gudpy/test/test_gudpy_io.py | 5 ++--- gudpy/test/test_gudrun_classes.py | 6 ++++-- 5 files changed, 12 insertions(+), 16 deletions(-) diff --git a/gudpy/core/container.py b/gudpy/core/container.py index 4e9863633..dc491d9af 100644 --- a/gudpy/core/container.py +++ b/gudpy/core/container.py @@ -97,6 +97,7 @@ def __init__(self, config=None): self.grBroadening = 0. self.powerForBroadening = 0.0 self.stepSize = 0.0 + self.outputFolder = "" self.yamlignore = { "runAsSample", @@ -113,8 +114,6 @@ def __init__(self, config=None): if config: self.parseFromConfig(config) - self.outputFolder = "" - def __str__(self): """ Returns the string representation of the Container object. diff --git a/gudpy/core/io/gudrun_file_parser.py b/gudpy/core/io/gudrun_file_parser.py index cccd69a11..31e049a26 100644 --- a/gudpy/core/io/gudrun_file_parser.py +++ b/gudpy/core/io/gudrun_file_parser.py @@ -41,7 +41,7 @@ def writeGudrunFileTo(cls, gudrunFile: GudrunFile, path: str): @classmethod def writeGudrunFile(cls, gudrunFile: GudrunFile, runDir: str): - cls.writeGudrunFile(gudrunFile, os.path.join( + cls.writeGudrunFileTo(gudrunFile, os.path.join( runDir, GudrunFile.OUTPATH)) @classmethod diff --git a/gudpy/core/iterators.py b/gudpy/core/iterators.py index 2c3ac3ab1..2caccb44d 100644 --- a/gudpy/core/iterators.py +++ b/gudpy/core/iterators.py @@ -485,8 +485,6 @@ def setSelfScatteringFiles(self, scale, gudrunFile, prevOutput=None): extensions to msubw01. If the scale selected is the wavelength-scale, then set self scattering file extensions to mint01. """ - # Dict to pick suffix based on scale - suffix = {Scales.Q: ".msubw01", Scales.WAVELENGTH: ".mint01"}[scale] # Iterate through all of the samples, and set the suffixes of # all of their data files to the suffix @@ -495,12 +493,10 @@ def setSelfScatteringFiles(self, scale, gudrunFile, prevOutput=None): for sample in sampleBackground.samples: if sample.runThisSample and len(sample.dataFiles): target = sample - filename = target.dataFilenames[0] - targetFile = ( - prevOutput.output( - sample.name, filename, suffix) - if prevOutput else "" - ) + if scale == Scales.Q: + targetFile = target.dataFiles.dataFiles[0].msubwFile + else: + targetFile = target.dataFiles.dataFiles[0].mintFile target.fileSelfScattering = ( targetFile ) diff --git a/gudpy/test/test_gudpy_io.py b/gudpy/test/test_gudpy_io.py index 679e76a1a..b06312c0c 100644 --- a/gudpy/test/test_gudpy_io.py +++ b/gudpy/test/test_gudpy_io.py @@ -25,7 +25,6 @@ Geometry ) from core import gudpy as gp -from core.io.gudpy_io import GudPyIO from core.io.gudrun_file_parser import GudrunFileParser @@ -292,6 +291,7 @@ def setUp(self) -> None: "grBroadening": 0.0, "powerForBroadening": 0.0, "stepSize": 0.0, + "outputFolder": "", "yamlignore": { "runAsSample", "topHatW", @@ -771,7 +771,6 @@ def testLoadGudrunFile(self): ) for _key in containerAttrsDict.keys(): - if isinstance( container[_key], (DataFiles, Composition), @@ -1554,7 +1553,7 @@ def testAppendExponentialValues(self): ) gudpy.io.writeGudrunFileTo(gudpy.gudrunFile, gfFilePath) - gudrunFile = GudPyIO.importGudrunFile(gfFilePath) + gudrunFile = gudpy.io.importGudrunFile(gfFilePath) # Test that a default value is appended self.assertEqual( self.expectedSampleA["exponentialValues"], diff --git a/gudpy/test/test_gudrun_classes.py b/gudpy/test/test_gudrun_classes.py index 1a5e172de..26f8893b5 100644 --- a/gudpy/test/test_gudrun_classes.py +++ b/gudpy/test/test_gudrun_classes.py @@ -20,13 +20,15 @@ class TestGudrunClasses(TestCase): def testEmptyPath(self): emptyPath = "" + gudpyIO = GudPyIO() self.assertRaises( - RuntimeError, GudPyIO.importGudrunFile, emptyPath) + RuntimeError, gudpyIO.importGudrunFile, emptyPath) def testInvalidPath(self): invalidPath = "invalid_path" + gudpyIO = GudPyIO() self.assertRaises( - ParserException, GudPyIO.importGudrunFile, invalidPath) + ParserException, gudpyIO.importGudrunFile, invalidPath) def testInstrumentInitDataTypes(self): From 62b1819e6358b5605e82ff33f2cf4cfde1514c06 Mon Sep 17 00:00:00 2001 From: Noella Spitz Date: Wed, 22 May 2024 10:31:05 +0100 Subject: [PATCH 26/78] Correctly identify sample files --- gudpy/core/container.py | 2 +- gudpy/core/data_files.py | 37 +++---------------- gudpy/core/io/gudrun_file_parser.py | 4 +- gudpy/core/iterators.py | 4 +- gudpy/core/sample.py | 2 +- .../NIMROD-water/good_water/good_water.yaml | 32 ++++++++-------- gudpy/test/test_gudpy_workflows.py | 2 +- 7 files changed, 28 insertions(+), 55 deletions(-) diff --git a/gudpy/core/container.py b/gudpy/core/container.py index dc491d9af..e1ac277c7 100644 --- a/gudpy/core/container.py +++ b/gudpy/core/container.py @@ -71,7 +71,7 @@ def __init__(self, config=None): """ self.name = "" self.periodNumber = 1 - self.dataFiles = DataFiles([], "CONTAINER") + self.dataFiles = DataFiles([], "CONTAINER", True) self.composition = Composition("CONTAINER") self.geometry = Geometry.SameAsBeam self.upstreamThickness = 0.1 diff --git a/gudpy/core/data_files.py b/gudpy/core/data_files.py index 6e5defc32..db1176ac0 100644 --- a/gudpy/core/data_files.py +++ b/gudpy/core/data_files.py @@ -21,26 +21,6 @@ def __init__(self, filename, isSampleDataFile: bool = False): "yamlignore" } - def detectSampleDataFile(self, path): - for f in os.listdir(path): - if f == "Diagnostics": - self.isSampleDataFile = True - - def constructDictFromDir(self, dirPath, dict): - for f in os.listdir(dirPath): - ext = os.path.splitext(f) - dict[ext] = f - - def importOutputs(self, path): - self.detectSampleDataFile(path) - if self.isSampleDataFile: - outputsDir = os.path.join(path, "Outputs") - diagDir = os.path.join(path, "Diagnostics") - self.constructDictFromDir(outputsDir, self._outputs) - self.constructDictFromDir(diagDir, self.diagnostics) - else: - self.constructDictFromDir(path, self._outputs) - def addOutput(self, path): ext = os.path.splitext(path)[1] self._outputs[ext] = path @@ -90,7 +70,7 @@ class DataFiles: ------- """ - def __init__(self, dataFiles, name): + def __init__(self, dataFiles, name, isSampleDataFile=False): """ Constructs all the necessary attributes for the DataFiles object. @@ -102,8 +82,9 @@ def __init__(self, dataFiles, name): Name of the parent of the data files, e.g. Sample Background """ - self._dataFiles = [] + self._dataFiles: DataFile = [] self.setFiles(dataFiles) + self.isSampleDataFile = isSampleDataFile self.name = name @@ -120,15 +101,7 @@ def setFiles(self, dataFilenames): for dataFile in dataFilenames: if not dataFile: continue - self._dataFiles.append(DataFile(dataFile)) - - def importOutputs(self, path): - self.outputFolder = path - children = os.listdir(path) - - for df in self._dataFiles: - if df.name in children: - return + self._dataFiles.append(DataFile(dataFile, self.isSampleDataFile)) def __str__(self): """ @@ -174,7 +147,7 @@ def __len__(self): return len(self._dataFiles) def __getitem__(self, n): - return self._dataFiles[n].filename + return self._dataFiles[n] def __setitem__(self, n, item): if n >= len(self): diff --git a/gudpy/core/io/gudrun_file_parser.py b/gudpy/core/io/gudrun_file_parser.py index 31e049a26..1c3e66e0e 100644 --- a/gudpy/core/io/gudrun_file_parser.py +++ b/gudpy/core/io/gudrun_file_parser.py @@ -789,7 +789,7 @@ def _parseSample(self) -> Sample: for _ in range(numberOfFiles): dataFiles.append(utils.firstword(self._getNextToken())) # Create a DataFiles object from the dataFiles list constructed. - sample.dataFiles = DataFiles(dataFiles, sample.name) + sample.dataFiles = DataFiles(dataFiles, sample.name, True) # For boolean attributes, we convert the first # integer in the line to its boolean value. @@ -994,7 +994,7 @@ def _parseContainer(self) -> Container: dataFiles.append(utils.firstword(self._getNextToken())) # Create a DataFiles object from the dataFiles list constructed. - container.dataFiles = DataFiles(dataFiles, container.name) + container.dataFiles = DataFiles(dataFiles, container.name, True) # Construct composition composition = [] diff --git a/gudpy/core/iterators.py b/gudpy/core/iterators.py index 2caccb44d..5921dfe5d 100644 --- a/gudpy/core/iterators.py +++ b/gudpy/core/iterators.py @@ -494,9 +494,9 @@ def setSelfScatteringFiles(self, scale, gudrunFile, prevOutput=None): if sample.runThisSample and len(sample.dataFiles): target = sample if scale == Scales.Q: - targetFile = target.dataFiles.dataFiles[0].msubwFile + targetFile = target.dataFiles[0].msubwFile else: - targetFile = target.dataFiles.dataFiles[0].mintFile + targetFile = target.dataFiles[0].mintFile target.fileSelfScattering = ( targetFile ) diff --git a/gudpy/core/sample.py b/gudpy/core/sample.py index f16b9254f..9d0b99d01 100644 --- a/gudpy/core/sample.py +++ b/gudpy/core/sample.py @@ -110,7 +110,7 @@ def __init__(self): """ self.name = "" self.periodNumber = 1 - self.dataFiles = DataFiles([], "SAMPLE") + self.dataFiles = DataFiles([], "SAMPLE", True) self.forceCalculationOfCorrections = True self.composition = Composition("SAMPLE") self.geometry = Geometry.SameAsBeam diff --git a/gudpy/test/TestData/NIMROD-water/good_water/good_water.yaml b/gudpy/test/TestData/NIMROD-water/good_water/good_water.yaml index 924e570f4..721fa9940 100644 --- a/gudpy/test/TestData/NIMROD-water/good_water/good_water.yaml +++ b/gudpy/test/TestData/NIMROD-water/good_water/good_water.yaml @@ -137,13 +137,13 @@ sampleBackgrounds: ext: .raw outputFolder: '' _outputs: {} - isSampleDataFile: false + isSampleDataFile: true - filename: NIMROD00016610_H2O_in_N9.raw name: NIMROD00016610_H2O_in_N9 ext: .raw outputFolder: '' _outputs: {} - isSampleDataFile: false + isSampleDataFile: true name: H2O,_Can_N9 outputFolders: {} _outputs: {} @@ -195,19 +195,19 @@ sampleBackgrounds: ext: .raw outputFolder: '' _outputs: {} - isSampleDataFile: false + isSampleDataFile: true - filename: NIMROD00016699_Empty_N9.raw name: NIMROD00016699_Empty_N9 ext: .raw outputFolder: '' _outputs: {} - isSampleDataFile: false + isSampleDataFile: true - filename: NIMROD00016704_Empty_N9.raw name: NIMROD00016704_Empty_N9 ext: .raw outputFolder: '' _outputs: {} - isSampleDataFile: false + isSampleDataFile: true name: N9 outputFolders: {} _outputs: {} @@ -244,13 +244,13 @@ sampleBackgrounds: ext: .raw outputFolder: '' _outputs: {} - isSampleDataFile: false + isSampleDataFile: true - filename: NIMROD00016611_D2O_in_N10.raw name: NIMROD00016611_D2O_in_N10 ext: .raw outputFolder: '' _outputs: {} - isSampleDataFile: false + isSampleDataFile: true name: D2O,_Can_N10 outputFolders: {} _outputs: {} @@ -302,19 +302,19 @@ sampleBackgrounds: ext: .raw outputFolder: '' _outputs: {} - isSampleDataFile: false + isSampleDataFile: true - filename: NIMROD00016700_Empty_N10.raw name: NIMROD00016700_Empty_N10 ext: .raw outputFolder: '' _outputs: {} - isSampleDataFile: false + isSampleDataFile: true - filename: NIMROD00016705_Empty_N10.raw name: NIMROD00016705_Empty_N10 ext: .raw outputFolder: '' _outputs: {} - isSampleDataFile: false + isSampleDataFile: true name: N10 outputFolders: {} _outputs: {} @@ -351,13 +351,13 @@ sampleBackgrounds: ext: .raw outputFolder: '' _outputs: {} - isSampleDataFile: false + isSampleDataFile: true - filename: NIMROD00016743_HDO_in_N6.raw name: NIMROD00016743_HDO_in_N6 ext: .raw outputFolder: '' _outputs: {} - isSampleDataFile: false + isSampleDataFile: true name: HDO,_Can_N6 outputFolders: {} _outputs: {} @@ -410,7 +410,7 @@ sampleBackgrounds: ext: .raw outputFolder: '' _outputs: {} - isSampleDataFile: false + isSampleDataFile: true name: N6 outputFolders: {} _outputs: {} @@ -447,13 +447,13 @@ sampleBackgrounds: ext: .raw outputFolder: '' _outputs: {} - isSampleDataFile: false + isSampleDataFile: true - filename: NIMROD00016744_NullWater_in_N8.raw name: NIMROD00016744_NullWater_in_N8 ext: .raw outputFolder: '' _outputs: {} - isSampleDataFile: false + isSampleDataFile: true name: Null_Water,_Can_N8 outputFolders: {} _outputs: {} @@ -506,7 +506,7 @@ sampleBackgrounds: ext: .raw outputFolder: '' _outputs: {} - isSampleDataFile: false + isSampleDataFile: true name: N8 outputFolders: {} _outputs: {} diff --git a/gudpy/test/test_gudpy_workflows.py b/gudpy/test/test_gudpy_workflows.py index f0102609c..f6079bde6 100644 --- a/gudpy/test/test_gudpy_workflows.py +++ b/gudpy/test/test_gudpy_workflows.py @@ -71,7 +71,7 @@ def testGudPyDCS(self): for sample in gudpy.gudrunFile.sampleBackgrounds[0].samples: mintFilename = ( - os.path.splitext(sample.dataFiles[0])[0] + os.path.splitext(sample.dataFiles[0].filename)[0] ) actualMintFile = ("test/TestData/water-ref/plain/" From e0a3c9261bff77935a1b0a0334874201209fd1be Mon Sep 17 00:00:00 2001 From: Noella Spitz Date: Wed, 22 May 2024 10:48:06 +0100 Subject: [PATCH 27/78] Fix output data paths --- gudpy/core/data_files.py | 9 ++++----- gudpy/core/iterators.py | 1 + gudpy/test/test_gudpy_workflows.py | 14 ++++---------- 3 files changed, 9 insertions(+), 15 deletions(-) diff --git a/gudpy/core/data_files.py b/gudpy/core/data_files.py index db1176ac0..8e0e9eab4 100644 --- a/gudpy/core/data_files.py +++ b/gudpy/core/data_files.py @@ -35,11 +35,10 @@ def __str__(self): def gudFile(self): if not self.isSampleDataFile: return None - if not self.gudFile: - gudPath = self._outputs.get(".gud", None) - if not gudPath: - return None - return GudFile(gudPath) + gudPath = self._outputs.get(".gud", None) + if not gudPath: + return None + return GudFile(gudPath) @property def mintFile(self): diff --git a/gudpy/core/iterators.py b/gudpy/core/iterators.py index 5921dfe5d..7b77529f4 100644 --- a/gudpy/core/iterators.py +++ b/gudpy/core/iterators.py @@ -597,6 +597,7 @@ def organiseOutput(self, gudrunFile, exclude=[]): outputHandler = handlers.GudrunOutputHandler( gudrunFile=gudrunFile, + head="Current", overwrite=True ) output = outputHandler.organiseOutput(exclude=exclude) diff --git a/gudpy/test/test_gudpy_workflows.py b/gudpy/test/test_gudpy_workflows.py index f6079bde6..168d31e73 100644 --- a/gudpy/test/test_gudpy_workflows.py +++ b/gudpy/test/test_gudpy_workflows.py @@ -42,9 +42,8 @@ def __exit__(self, exc_type, exc_value, tb): class TestGudPyWorkflows(TestCase): def getGudFile(self, gudpy, sampleIndex) -> GudFile: - return gudpy.gudrunOutput.sampleOutputs[ - gudpy.gudrunFile.sampleBackgrounds[0].samples[ - sampleIndex].name].gudFile + return gudpy.gudrunFile.sampleBackgrounds[0].samples[ + sampleIndex].dataFiles[0].gudFile def testGudPyDCS(self): with GudPyContext() as gudpy: @@ -70,9 +69,7 @@ def testGudPyDCS(self): self.assertAlmostEqual(dcsLevelPercentage, 13.0, 0) for sample in gudpy.gudrunFile.sampleBackgrounds[0].samples: - mintFilename = ( - os.path.splitext(sample.dataFiles[0].filename)[0] - ) + mintFilename = sample.dataFiles[0].name actualMintFile = ("test/TestData/water-ref/plain/" f"{mintFilename}.mint01") @@ -233,10 +230,7 @@ def testGudPyIterateBySubtractingWavelength(self): for x in gudpy.gudrunFile.sampleBackgrounds[0].samples if x.runThisSample ]: - dataFilename = ( - os.path.splitext(sample.dataFiles[0])[0] - ) - + dataFilename = sample.dataFiles[0].name actualMintFile = ( f'test/TestData/water-ref/wavelength{i}/' f'{dataFilename}.mint01' From 30acd320d1267c32458832cf165e8a6c00fce5f1 Mon Sep 17 00:00:00 2001 From: Noella Spitz Date: Wed, 22 May 2024 12:26:57 +0100 Subject: [PATCH 28/78] Fix iteration output handling --- gudpy/core/gudpy.py | 2 +- gudpy/core/gudrun_file.py | 5 +- gudpy/core/io/gudpy_io.py | 2 +- gudpy/core/io/gudrun_file_parser.py | 61 +++++++++++-------- gudpy/gui/widgets/core/control.py | 15 +++-- gudpy/gui/widgets/core/main_window.py | 8 ++- .../gui/widgets/dialogs/view_input_dialog.py | 8 +-- gudpy/test/test_gudpy_workflows.py | 9 ++- 8 files changed, 68 insertions(+), 42 deletions(-) diff --git a/gudpy/core/gudpy.py b/gudpy/core/gudpy.py index d76c55e94..775992d42 100644 --- a/gudpy/core/gudpy.py +++ b/gudpy/core/gudpy.py @@ -906,7 +906,7 @@ def bactchProcess( exitcode = gudrun.gudrun(GudrunFile, purge, iterator) self.writeDiagnosticsFile( os.path.join( - self.batchedGudrunFile.path(), + self.batchedGudrunFile.outputFolder, "batch_processing_diagnostics.txt", ), self.batchedGudrunFile, diff --git a/gudpy/core/gudrun_file.py b/gudpy/core/gudrun_file.py index 5dfdd1c3c..0ee890ed3 100644 --- a/gudpy/core/gudrun_file.py +++ b/gudpy/core/gudrun_file.py @@ -109,10 +109,7 @@ def __init__( self.sampleBackgrounds = sampleBackgrounds self.components = components self.purged = False - - @property - def path(self): - return os.path.join(self.outputFolder, self.OUTPATH) + self.outputFolder = "" def __deepcopy__(self, memo): result = self.__class__.__new__(self.__class__) diff --git a/gudpy/core/io/gudpy_io.py b/gudpy/core/io/gudpy_io.py index eed1887cf..dfd2cd33d 100644 --- a/gudpy/core/io/gudpy_io.py +++ b/gudpy/core/io/gudpy_io.py @@ -83,7 +83,7 @@ def save(self, gudrunFile): def importGudrunFile(self, path, config=False) -> GudrunFile: GudPyIO.loadFile = path - return self.gudrunFileParser.parse(path, config) + return self.gudrunFileParser.parseFromPath(path, config) def importProject(self, projectDir) -> GudrunFile: """Imports from a project directory diff --git a/gudpy/core/io/gudrun_file_parser.py b/gudpy/core/io/gudrun_file_parser.py index 1c3e66e0e..bfe29a513 100644 --- a/gudpy/core/io/gudrun_file_parser.py +++ b/gudpy/core/io/gudrun_file_parser.py @@ -53,13 +53,11 @@ def export(cls, gudrunFile: GudrunFile, path: str): f.write(str(gudrunFile)) f.close() - def parse( - self, path: str, config=False - ) -> typ.Tuple[ + def parse(self, lines: list[str]) -> typ.Tuple[ Instrument, Beam, Normalisation, list[SampleBackground], Components]: """ - Parse the GudrunFile from its path. + Parse the GudrunFile from a list of lines. Assign objects from the file to the attributes of the class. Raises ParserException if Instrument, Beam or Normalisation are missing. @@ -70,31 +68,12 @@ def parse( normalisation = Normalisation() sampleBackgrounds = [] - self.config = config - - # Ensure only valid files are given. - if not path: - raise ParserException( - "Path not supplied. Cannot parse from an empty path!" - ) - if not os.path.exists(path): - raise ParserException( - "The path supplied is invalid.\ - Cannot parse from an invalid path" + path - ) - KEYWORDS = { "INSTRUMENT": False, "BEAM": False, "NORMALISATION": False } - # Decide the encoding - import chardet - with open(path, 'rb') as fp: - encoding = chardet.detect(fp.read())['encoding'] - # Read the input stream into our attribute. - with open(path, encoding=encoding) as fp: - self.stream = fp.readlines() + self.stream = lines # Here we go! Get the first token and begin parsing. line = self._getNextToken() @@ -151,9 +130,43 @@ def parse( components=components ) + def parseFromPath( + self, path: str, config=False + ) -> typ.Tuple[ + Instrument, Beam, Normalisation, + list[SampleBackground], Components]: + """ + Parse the GudrunFile from its path. + Assign objects from the file to the attributes of the class. + Raises ParserException if Instrument, + Beam or Normalisation are missing. + """ + self.config = config + + # Ensure only valid files are given. + if not path: + raise ParserException( + "Path not supplied. Cannot parse from an empty path!" + ) + if not os.path.exists(path): + raise ParserException( + "The path supplied is invalid.\ + Cannot parse from an invalid path" + path + ) + + # Decide the encoding + import chardet + with open(path, 'rb') as fp: + encoding = chardet.detect(fp.read())['encoding'] + # Read the input stream into our attribute. + with open(path, encoding=encoding) as fp: + lines = fp.readlines() + return self.parse(lines) + # ================================================================= # PARSING HELPERS # ================================================================= + def _getNextToken(self) -> typ.Union[str, None]: """ Pops the 'next token' from the stream and returns it. diff --git a/gudpy/gui/widgets/core/control.py b/gudpy/gui/widgets/core/control.py index 568074702..a92ba776b 100644 --- a/gudpy/gui/widgets/core/control.py +++ b/gudpy/gui/widgets/core/control.py @@ -81,7 +81,7 @@ def connectUiSlots(self): self.mainWidget.ui.exportInputFile.triggered.connect( self.exportInputFile) self.mainWidget.ui.viewLiveInputFile.triggered.connect( - self.mainWidget.viewInput) + self.viewInput) self.mainWidget.ui.insertSampleBackground.triggered.connect( self.mainWidget.ui.objectTree.insertSampleBackground ) @@ -318,6 +318,12 @@ def autosave(self): if self.gudpy.checkSaveLocation() and not self.workerThread: self.gudpy.save() + def viewInput(self): + text = self.mainWidget.viewInput() + self.gudrunFile = self.gudpy.io.gudrunFileParser.parse( + text.splitlines()) + self.mainWidget.updateWidgets(self.gudrunFile) + """ PROCESSES @@ -460,7 +466,7 @@ def purgeFinished(self, exitcode): ) self.mainWidget.outputSlots.setOutput( self.gudpy.purge.output, "purge_det", - gudrunFile=self.gudrunFile + self.gudrunFile ) if isinstance(self.workerThread, gp.Purge): @@ -529,7 +535,8 @@ def gudrunFinished(self, exitcode): self.mainWidget.outputSlots.setOutput( self.gudpy.gudrunIterator.output, - f"Gudrun {self.gudpy.gudrunIterator.iterator.name}") + f"Gudrun {self.gudpy.gudrunIterator.iterator.name}", + self.gudrunFile) self.mainWidget.sampleSlots.setSample( self.mainWidget.sampleSlots.sample) self.mainWidget.iterationResultsDialog( @@ -547,7 +554,7 @@ def gudrunFinished(self, exitcode): ) return self.mainWidget.outputSlots.setOutput( - self.gudpy.gudrun.output, "Gudrun") + self.gudpy.gudrun.output, "Gudrun", self.gudrunFile) self.mainWidget.updateWidgets( gudrunFile=self.gudrunFile, gudrunOutput=self.gudpy.gudrun.gudrunOutput diff --git a/gudpy/gui/widgets/core/main_window.py b/gudpy/gui/widgets/core/main_window.py index 4487531d1..2bf0506b2 100644 --- a/gudpy/gui/widgets/core/main_window.py +++ b/gudpy/gui/widgets/core/main_window.py @@ -6,6 +6,7 @@ from core.container import Container from core.sample import Sample +from core.io.gudpy_io import GudPyIO from gui.widgets.dialogs.iterators import ( CompositionIterationDialog, @@ -705,7 +706,7 @@ def purgeOptionsMessageBox(self, text): def setModified(self): if not self.modified: - if self.gudrunFile.path(): + if GudPyIO.projectDir: self.modified = True self.ui.setWindowModified(True) self.ui.save.setEnabled(True) @@ -737,7 +738,7 @@ def setControlsEnabled(self, state): self.ui.viewLiveInputFile.setEnabled(state) self.ui.save.setEnabled( state & self.modified - if self.gudrunFile.path() + if GudPyIO.projectDir else False ) self.ui.exportInputFile.setEnabled(state) @@ -778,7 +779,8 @@ def setTreeActionsEnabled(self, state): def viewInput(self): self.currentState = str(self.gudrunFile) viewInputDialog = ViewInputDialog(self.gudrunFile, self) - viewInputDialog.widget.exec() + text = viewInputDialog.widget.exec() + return text def handleAllPlotModeChanged(self, index): plotMode = self.ui.allPlotComboBox.itemData(index) diff --git a/gudpy/gui/widgets/dialogs/view_input_dialog.py b/gudpy/gui/widgets/dialogs/view_input_dialog.py index bcce525e6..c45faaea6 100644 --- a/gudpy/gui/widgets/dialogs/view_input_dialog.py +++ b/gudpy/gui/widgets/dialogs/view_input_dialog.py @@ -3,6 +3,7 @@ from PySide6.QtCore import QFile from PySide6.QtWidgets import QDialog from PySide6.QtUiTools import QUiLoader +from core.io.gudpy_io import GudPyIO class ViewInputDialog(QDialog): @@ -52,7 +53,7 @@ def initComponents(self): ) loader = QUiLoader() self.widget = loader.load(uifile) - self.widget.setWindowTitle(self.gudrunFile.path()) + self.widget.setWindowTitle(GudPyIO.projectName) self.widget.saveAndCloseButton.clicked.connect(self.save) self.widget.closeButton.clicked.connect(self.widget.close) self.widget.textEdit.setText(str(self.gudrunFile)) @@ -67,7 +68,6 @@ def save(self): """ Saves the input file and updates the UI appropiately. """ - with open(self.gudrunFile.path(), "w", encoding="utf-8") as fp: - fp.write(self.widget.textEdit.toPlainText()) + text = self.widget.textEdit.toPlainText() self.widget.close() - self.parent.updateFromFile() + return text diff --git a/gudpy/test/test_gudpy_workflows.py b/gudpy/test/test_gudpy_workflows.py index 168d31e73..759826609 100644 --- a/gudpy/test/test_gudpy_workflows.py +++ b/gudpy/test/test_gudpy_workflows.py @@ -265,7 +265,14 @@ def testGudPyIterateBySubtractingWavelength(self): ) actualData = open( - sample.dataFiles[0].msubwFile, + os.path.join( + gudpy.projectDir, "Gudrun", + "Inelasticity_Subtraction_(WavelengthIteration)", + f"WavelengthIteration_{i}", + sample.name, + sample.dataFiles[0].name, + "Diagnostics", + f"{sample.dataFiles[0].name}.msubw01"), "r", encoding="utf-8" ).readlines()[10:] expectedData = open( From 450357df4969f3462457f9ea3576e97e6b2bec4e Mon Sep 17 00:00:00 2001 From: Noella Spitz Date: Wed, 22 May 2024 14:00:58 +0100 Subject: [PATCH 29/78] Fix order of datafile initialisation --- gudpy/core/data_files.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gudpy/core/data_files.py b/gudpy/core/data_files.py index 8e0e9eab4..0c82174f4 100644 --- a/gudpy/core/data_files.py +++ b/gudpy/core/data_files.py @@ -82,8 +82,8 @@ def __init__(self, dataFiles, name, isSampleDataFile=False): """ self._dataFiles: DataFile = [] - self.setFiles(dataFiles) self.isSampleDataFile = isSampleDataFile + self.setFiles(dataFiles) self.name = name From ba91c2ba7ce06dcaefb6b1ed171f08f2dcdfe636 Mon Sep 17 00:00:00 2001 From: Noella Spitz Date: Wed, 22 May 2024 16:15:26 +0100 Subject: [PATCH 30/78] Change exception assertions --- gudpy/test/test_gudpy_io.py | 134 ++++++++++-------------------- gudpy/test/test_gudpy_yaml.py | 4 +- gudpy/test/test_gudrun_classes.py | 2 +- 3 files changed, 45 insertions(+), 95 deletions(-) diff --git a/gudpy/test/test_gudpy_io.py b/gudpy/test/test_gudpy_io.py index b06312c0c..f54aa3ecb 100644 --- a/gudpy/test/test_gudpy_io.py +++ b/gudpy/test/test_gudpy_io.py @@ -236,6 +236,7 @@ def setUp(self) -> None: "grBroadening": 0.0, "powerForBroadening": 0.0, "stepSize": 0.0, + "outputFolder": '', "yamlignore": { "runAsSample", "topHatW", @@ -342,6 +343,7 @@ def setUp(self) -> None: "grBroadening": 0.0, "powerForBroadening": 0.0, "stepSize": 0.0, + "outputFolder": '', "yamlignore": { "runAsSample", "topHatW", @@ -1159,15 +1161,10 @@ def testLoadMissingInstrumentAttributesRand(self): f.write( "INSTRUMENT {\n\n" + str(badInstrument) + "\n\n}" ) - with self.assertRaises(ParserException) as cm: - with GudPyContext() as gudpy: - gudpy.loadFromGudrunFile("test_data.txt") - self.assertEqual( - "Whilst parsing Instrument, an exception occured." - " The input file is most likely of an incorrect format, " - "and some attributes were missing.", - str(cm.exception) - ) + with GudPyContext() as gudpy: + self.assertRaises( + ParserException, gudpy.loadFromGudrunFile, + "test_data.txt") def testLoadMissingBeamAttributesSeq(self): expectedBeam = deepcopy(self.expectedBeam) @@ -1195,15 +1192,10 @@ def testLoadMissingBeamAttributesSeq(self): ) f.write("\n\nBEAM {\n\n" + str(badBeam) + "\n\n}") - with self.assertRaises(ParserException) as cm: - with GudPyContext() as gudpy: - gudpy.loadFromGudrunFile("test_data.txt") - self.assertEqual( - "Whilst parsing Beam, an exception occured." - " The input file is most likely of an incorrect format, " - "and some attributes were missing.", - str(cm.exception) - ) + with GudPyContext() as gudpy: + self.assertRaises( + ParserException, gudpy.loadFromGudrunFile, + "test_data.txt") def testLoadMissingBeamAttributesRand(self): expectedBeam = deepcopy(self.expectedBeam) @@ -1235,15 +1227,10 @@ def testLoadMissingBeamAttributesRand(self): ) f.write("\n\nBEAM {\n\n" + str(badBeam) + "\n\n}") - with self.assertRaises(ParserException) as cm: - with GudPyContext() as gudpy: - gudpy.loadFromGudrunFile("test_data.txt") - self.assertEqual( - "Whilst parsing Beam, an exception occured." - " The input file is most likely of an incorrect format, " - "and some attributes were missing.", - str(cm.exception) - ) + with GudPyContext() as gudpy: + self.assertRaises( + ParserException, gudpy.loadFromGudrunFile, + "test_data.txt") def testLoadMissingNormalisationAttributesSeq(self): expectedNormalisation = deepcopy(self.expectedNormalisation) @@ -1280,15 +1267,10 @@ def testLoadMissingNormalisationAttributesSeq(self): + "\n\n}" ) - with self.assertRaises(ParserException) as cm: - with GudPyContext() as gudpy: - gudpy.loadFromGudrunFile("test_data.txt") - self.assertEqual( - "Whilst parsing Beam, an exception occured." - " The input file is most likely of an incorrect format, " - "and some attributes were missing.", - str(cm.exception) - ) + with GudPyContext() as gudpy: + self.assertRaises( + ParserException, gudpy.loadFromGudrunFile, + "test_data.txt") def testLoadMissingNormalisationAttributesRand(self): expectedNormalisation = deepcopy(self.expectedNormalisation) @@ -1328,16 +1310,10 @@ def testLoadMissingNormalisationAttributesRand(self): + str(badNormalisation) + "\n\n}" ) - - with self.assertRaises(ParserException) as cm: - with GudPyContext() as gudpy: - gudpy.loadFromGudrunFile("test_data.txt") - self.assertEqual( - "Whilst parsing Normalisation, an exception occured." - " The input file is most likely of an incorrect format, " - "and some attributes were missing.", - str(cm.exception) - ) + with GudPyContext() as gudpy: + self.assertRaises( + ParserException, gudpy.loadFromGudrunFile, + "test_data.txt") def testLoadMissingSampleBackgroundAttributes(self): badSampleBackground = str(self.goodSampleBackground).split("\n") @@ -1355,15 +1331,10 @@ def testLoadMissingSampleBackgroundAttributes(self): + "\n\n}" ) f.write("\n\n{}\n\nEND".format(str(badSampleBackground))) - with self.assertRaises(ParserException) as cm: with GudPyContext() as gudpy: - gudpy.loadFromGudrunFile("test_data.txt") - self.assertEqual( - "Whilst parsing Sample Background, an exception occured." - " The input file is most likely of an incorrect format, " - "and some attributes were missing.", - str(cm.exception) - ) + self.assertRaises( + ParserException, gudpy.loadFromGudrunFile, + "test_data.txt") def testLoadMissingSampleAttributesSeq(self): expectedSampleA = deepcopy(self.expectedSampleA) @@ -1411,15 +1382,10 @@ def testLoadMissingSampleAttributesSeq(self): ) f.write("\n\n{}\n\nEND".format(str(badSampleBackground))) - with self.assertRaises(ParserException) as cm: - with GudPyContext() as gudpy: - gudpy.loadFromGudrunFile("test_data.txt") - self.assertEqual( - "Whilst parsing Sample, an exception occured." - " The input file is most likely of an incorrect format, " - "and some attributes were missing.", - str(cm.exception) - ) + with GudPyContext() as gudpy: + self.assertRaises( + ParserException, gudpy.loadFromGudrunFile, + "test_data.txt") def testLoadMissingSampleAttributesRand(self): expectedSampleA = deepcopy(self.expectedSampleA) @@ -1469,16 +1435,10 @@ def testLoadMissingSampleAttributesRand(self): + "\n\n}" ) f.write("\n\n{}\n\nEND".format(str(badSampleBackground))) - - with self.assertRaises(ParserException) as cm: - with GudPyContext() as gudpy: - gudpy.loadFromGudrunFile("test_data.txt") - self.assertEqual( - "Whilst parsing Sample, an exception occured." - " The input file is most likely of an incorrect format, " - "and some attributes were missing.", - str(cm.exception) - ) + with GudPyContext() as gudpy: + self.assertRaises( + ParserException, gudpy.loadFromGudrunFile, + "test_data.txt") def testLoadMissingContainerAttributesSeq(self): expectedContainerA = deepcopy(self.expectedContainerA) @@ -1531,15 +1491,10 @@ def testLoadMissingContainerAttributesSeq(self): + "\n\n}" ) f.write("\n\n{}\n\nEND".format(str(badSampleBackground))) - with self.assertRaises(ParserException) as cm: - with GudPyContext() as gudpy: - gudpy.loadFromGudrunFile("test_data.txt") - self.assertEqual( - "Whilst parsing Container, an exception occured." - " The input file is most likely of an incorrect format, " - "and some attributes were missing.", - str(cm.exception) - ) + with GudPyContext() as gudpy: + self.assertRaises( + ParserException, gudpy.loadFromGudrunFile, + "test_data.txt") def testAppendExponentialValues(self): with GudPyContext() as gudpy: @@ -1552,7 +1507,7 @@ def testAppendExponentialValues(self): gudpy.gudrunFile.OUTPATH ) - gudpy.io.writeGudrunFileTo(gudpy.gudrunFile, gfFilePath) + GudrunFileParser.writeGudrunFileTo(gudpy.gudrunFile, gfFilePath) gudrunFile = gudpy.io.importGudrunFile(gfFilePath) # Test that a default value is appended self.assertEqual( @@ -1614,15 +1569,10 @@ def testLoadMissingContainerAttributesRand(self): + "\n\n}" ) f.write("\n\n{}\n\nEND".format(str(badSampleBackground))) - with self.assertRaises(ParserException) as cm: - with GudPyContext() as gudpy: - gudpy.loadFromGudrunFile("test_data.txt") - self.assertEqual( - "Whilst parsing Container, an exception occured." - " The input file is most likely of an incorrect format, " - "and some attributes were missing.", - str(cm.exception) - ) + with GudPyContext() as gudpy: + self.assertRaises( + ParserException, gudpy.loadFromGudrunFile, + "test_data.txt") def testZeroExitGudrun(self): with GudPyContext() as gudpy: diff --git a/gudpy/test/test_gudpy_yaml.py b/gudpy/test/test_gudpy_yaml.py index 7656a88e1..362151ae6 100644 --- a/gudpy/test/test_gudpy_yaml.py +++ b/gudpy/test/test_gudpy_yaml.py @@ -12,11 +12,11 @@ def testYAML(self): gudpy1.loadFromGudrunFile( loadFile="test/TestData/NIMROD-water/water.txt", ) - gudpy1.saveAs(os.path.join(tmp.name, "water")) + gudpy1.saveAs(os.path.join(tmp, "water")) gf1 = gudpy1.gudrunFile gudpy2 = gudpy.GudPy() - gudpy2.loadFromProject(os.path.join(tmp.name, "water")) + gudpy2.loadFromProject(os.path.join(tmp, "water")) gf2 = gudpy2.gudrunFile gf1.instrument.GudrunInputFileDir = os.path.abspath( diff --git a/gudpy/test/test_gudrun_classes.py b/gudpy/test/test_gudrun_classes.py index 26f8893b5..7e69a5850 100644 --- a/gudpy/test/test_gudrun_classes.py +++ b/gudpy/test/test_gudrun_classes.py @@ -22,7 +22,7 @@ def testEmptyPath(self): emptyPath = "" gudpyIO = GudPyIO() self.assertRaises( - RuntimeError, gudpyIO.importGudrunFile, emptyPath) + ParserException, gudpyIO.importGudrunFile, emptyPath) def testInvalidPath(self): invalidPath = "invalid_path" From 9305a28010c43b85a63cd7873df3b5079aeca20a Mon Sep 17 00:00:00 2001 From: Noella Spitz Date: Thu, 23 May 2024 09:52:53 +0100 Subject: [PATCH 31/78] Fix assertions --- gudpy/test/test_gudpy_io.py | 50 +++++++++---------------------------- 1 file changed, 12 insertions(+), 38 deletions(-) diff --git a/gudpy/test/test_gudpy_io.py b/gudpy/test/test_gudpy_io.py index f54aa3ecb..4d50eab1e 100644 --- a/gudpy/test/test_gudpy_io.py +++ b/gudpy/test/test_gudpy_io.py @@ -1060,31 +1060,18 @@ def testLoadMissingInstrumentAndBeam(self): + "\n\n}" ) - with self.assertRaises(ParserException) as cm: - with GudPyContext() as gudpy: - gudpy.loadFromGudrunFile("test_data.txt") - self.assertEqual(( - 'INSTRUMENT, BEAM, NORMALISATION,' - ' were not parsed. It\'s possible the file supplied' - ' is of an incorrect format!'), - str(cm.exception), - ) + with GudPyContext() as gudpy: + self.assertRaises( + ParserException, gudpy.loadFromGudrunFile, "test_data.txt") def testLoadMissingInstrumentAndNormalisation(self): with open("test_data.txt", "w", encoding="utf-8") as f: f.write("' ' ' ' '/'\n\n") f.write("BEAM {\n\n" + str(self.goodBeam) + "\n\n}") - with self.assertRaises(ParserException) as cm: - with GudPyContext() as gudpy: - gudpy.loadFromGudrunFile("test_data.txt") - self.assertEqual(( - 'INSTRUMENT, BEAM, NORMALISATION,' - ' were not parsed. It\'s possible the file' - ' supplied is of an incorrect format!' - ), - str(cm.exception), - ) + with GudPyContext() as gudpy: + self.assertRaises( + ParserException, gudpy.loadFromGudrunFile, "test_data.txt") def testLoadMissingNormalisationAndBeam(self): with open("test_data.txt", "w", encoding="utf-8") as f: @@ -1093,16 +1080,9 @@ def testLoadMissingNormalisationAndBeam(self): "INSTRUMENT {\n\n" + str(self.goodInstrument) + "\n\n}" ) - with self.assertRaises(ParserException) as cm: - with GudPyContext() as gudpy: - gudpy.loadFromGudrunFile("test_data.txt") - self.assertEqual(( - 'INSTRUMENT, BEAM, NORMALISATION,' - ' were not parsed. It\'s possible the file' - ' supplied is of an incorrect format!' - ), - str(cm.exception), - ) + with GudPyContext() as gudpy: + self.assertRaises( + ParserException, gudpy.loadFromGudrunFile, "test_data.txt") def testLoadMissingInstrumentAttributesSeq(self): expectedInstrument = deepcopy(self.expectedInstrument) @@ -1126,15 +1106,9 @@ def testLoadMissingInstrumentAttributesSeq(self): "INSTRUMENT {\n\n" + str(badInstrument) + "\n\n}" ) - with self.assertRaises(ParserException) as cm: - with GudPyContext() as gudpy: - gudpy.loadFromGudrunFile("test_data.txt") - self.assertEqual( - "Whilst parsing Instrument, an exception occured." - " The input file is most likely of an incorrect format, " - "and some attributes were missing.", - str(cm.exception) - ) + with GudPyContext() as gudpy: + self.assertRaises( + ParserException, gudpy.loadFromGudrunFile, "test_data.txt") def testLoadMissingInstrumentAttributesRand(self): expectedInstrument = deepcopy(self.expectedInstrument) From a9af7fae2997fb022ee2ca3629af684a9e112046 Mon Sep 17 00:00:00 2001 From: Noella Spitz Date: Thu, 23 May 2024 10:02:04 +0100 Subject: [PATCH 32/78] Assertions --- gudpy/test/test_gudpy_io.py | 54 ++++++++++++------------------------- 1 file changed, 17 insertions(+), 37 deletions(-) diff --git a/gudpy/test/test_gudpy_io.py b/gudpy/test/test_gudpy_io.py index 4d50eab1e..daae1f69e 100644 --- a/gudpy/test/test_gudpy_io.py +++ b/gudpy/test/test_gudpy_io.py @@ -53,6 +53,10 @@ def __init__(self): self.tempdir.name, "good_water" )) + os.makedirs(os.path.join( + self.tempdir.name, "good_water" + )) + def __enter__(self): return self.gudpy @@ -976,16 +980,10 @@ def testReloadGudrunFile(self): def testLoadEmptyGudrunFile(self): f = open("test_data.txt", "w", encoding="utf-8") f.close() - with self.assertRaises(ParserException) as cm: - with GudPyContext() as gudpy: - gudpy.loadFromGudrunFile("test_data.txt") - self.assertEqual(( - 'INSTRUMENT, BEAM, NORMALISATION,' - ' were not parsed. It\'s possible the file' - ' supplied is of an incorrect format!' - ), - str(cm.exception), - ) + with GudPyContext() as gudpy: + self.assertRaises( + ParserException, gudpy.loadFromGudrunFile, + "test_data.txt") def testLoadMissingInstrument(self): with open("test_data.txt", "w", encoding="utf-8") as f: @@ -997,16 +995,10 @@ def testLoadMissingInstrument(self): + "\n\n}\n\n" ) - with self.assertRaises(ParserException) as cm: with GudPyContext() as gudpy: - gudpy.loadFromGudrunFile("test_data.txt") - self.assertEqual(( - 'INSTRUMENT, BEAM, NORMALISATION,' - ' were not parsed. It\'s possible the file' - ' supplied is of an incorrect format!' - ), - str(cm.exception), - ) + self.assertRaises( + ParserException, gudpy.loadFromGudrunFile, + "test_data.txt") def testLoadMissingBeam(self): with open("test_data.txt", "w", encoding="utf-8") as f: @@ -1020,16 +1012,10 @@ def testLoadMissingBeam(self): + str(self.goodNormalisation) + "\n\n}" ) - with self.assertRaises(ParserException) as cm: with GudPyContext() as gudpy: - gudpy.loadFromGudrunFile("test_data.txt") - self.assertEqual(( - 'INSTRUMENT, BEAM, NORMALISATION,' - ' were not parsed. It\'s possible the file' - ' supplied is of an incorrect format!' - ), - str(cm.exception), - ) + self.assertRaises( + ParserException, gudpy.loadFromGudrunFile, + "test_data.txt") def testLoadMissingNormalisation(self): with open("test_data.txt", "w", encoding="utf-8") as f: @@ -1040,16 +1026,10 @@ def testLoadMissingNormalisation(self): ) f.write("BEAM {\n\n" + str(self.goodBeam) + "\n\n}\n\n") - with self.assertRaises(ParserException) as cm: with GudPyContext() as gudpy: - gudpy.loadFromGudrunFile("test_data.txt") - self.assertEqual(( - 'INSTRUMENT, BEAM, NORMALISATION,' - ' were not parsed. It\'s possible the file' - ' supplied is of an incorrect format!' - ), - str(cm.exception), - ) + self.assertRaises( + ParserException, gudpy.loadFromGudrunFile, + "test_data.txt") def testLoadMissingInstrumentAndBeam(self): with open("test_data.txt", "w", encoding="utf-8") as f: From 05914ebf90e3c2456809a9f2d05f8d6ef47f02ff Mon Sep 17 00:00:00 2001 From: Noella Spitz Date: Thu, 23 May 2024 14:28:04 +0100 Subject: [PATCH 33/78] Remove elements from dict --- gudpy/core/utils.py | 4 + gudpy/test/TestData/NIMROD-water/water.txt | 2 +- gudpy/test/test_gudpy_io.py | 428 +++++++++++---------- 3 files changed, 225 insertions(+), 209 deletions(-) diff --git a/gudpy/core/utils.py b/gudpy/core/utils.py index 32dfe0530..1520a9504 100644 --- a/gudpy/core/utils.py +++ b/gudpy/core/utils.py @@ -23,6 +23,10 @@ def firstword(string): return string.split(" ")[0] +def lastword(string): + return string.split(" ")[-1] + + def replace_unwanted_chars(string): unwanted = [" "] for char in unwanted: diff --git a/gudpy/test/TestData/NIMROD-water/water.txt b/gudpy/test/TestData/NIMROD-water/water.txt index a6ecea359..ec85923c7 100644 --- a/gudpy/test/TestData/NIMROD-water/water.txt +++ b/gudpy/test/TestData/NIMROD-water/water.txt @@ -287,7 +287,7 @@ NIMROD00016742_NullWater_in_N8.msubw01 Name of file containing self sca CONTAINER N8 { -1 1 Number of files and period number +1 1 Number of files and period number NIMROD00016994_Empty_N8.raw CONTAINER N8 data files Ti 0 7.16 Composition Zr 0 3.438 Composition diff --git a/gudpy/test/test_gudpy_io.py b/gudpy/test/test_gudpy_io.py index daae1f69e..eb9460eee 100644 --- a/gudpy/test/test_gudpy_io.py +++ b/gudpy/test/test_gudpy_io.py @@ -37,6 +37,7 @@ def __init__(self): ) self.gudpy = gp.GudPy() + self.gudpy.testFilePath = os.path.join(testDir, "test_data.txt") self.gudpy.loadFromGudrunFile( loadFile=path, @@ -978,91 +979,90 @@ def testReloadGudrunFile(self): ) def testLoadEmptyGudrunFile(self): - f = open("test_data.txt", "w", encoding="utf-8") - f.close() with GudPyContext() as gudpy: + f = open(gudpy.testFilePath, "w", encoding="utf-8") + f.close() self.assertRaises( ParserException, gudpy.loadFromGudrunFile, - "test_data.txt") + gudpy.testFilePath) def testLoadMissingInstrument(self): - with open("test_data.txt", "w", encoding="utf-8") as f: - f.write("' ' ' ' '/'\n\n") - f.write("BEAM {\n\n" + str(self.goodBeam) + "\n\n}\n\n") - f.write( - "NORMALISATION {\n\n" - + str(self.goodNormalisation) - + "\n\n}\n\n" - ) - - with GudPyContext() as gudpy: - self.assertRaises( - ParserException, gudpy.loadFromGudrunFile, - "test_data.txt") + with GudPyContext() as gudpy: + with open(gudpy.testFilePath, "w", encoding="utf-8") as f: + f.write("' ' ' ' '/'\n\n") + f.write("BEAM {\n\n" + str(self.goodBeam) + "\n\n}\n\n") + f.write( + "NORMALISATION {\n\n" + + str(self.goodNormalisation) + + "\n\n}\n\n" + ) + self.assertRaises( + ParserException, gudpy.loadFromGudrunFile, + gudpy.testFilePath) def testLoadMissingBeam(self): - with open("test_data.txt", "w", encoding="utf-8") as f: - f.write("' ' ' ' '/'\n\n") - f.write( - "INSTRUMENT {\n\n" + str(self.goodInstrument) - + "\n\n}\n\n" - ) - f.write( - "NORMALISATION {\n\n" - + str(self.goodNormalisation) - + "\n\n}" - ) - with GudPyContext() as gudpy: - self.assertRaises( - ParserException, gudpy.loadFromGudrunFile, - "test_data.txt") + with GudPyContext() as gudpy: + with open(gudpy.testFilePath, "w", encoding="utf-8") as f: + f.write("' ' ' ' '/'\n\n") + f.write( + "INSTRUMENT {\n\n" + str(self.goodInstrument) + + "\n\n}\n\n" + ) + f.write( + "NORMALISATION {\n\n" + + str(self.goodNormalisation) + + "\n\n}" + ) + self.assertRaises( + ParserException, gudpy.loadFromGudrunFile, + gudpy.testFilePath) def testLoadMissingNormalisation(self): - with open("test_data.txt", "w", encoding="utf-8") as f: - f.write("' ' ' ' '/'\n\n") - f.write( - "INSTRUMENT {\n\n" + str(self.goodInstrument) - + "\n\n}\n\n" - ) - f.write("BEAM {\n\n" + str(self.goodBeam) + "\n\n}\n\n") - - with GudPyContext() as gudpy: - self.assertRaises( - ParserException, gudpy.loadFromGudrunFile, - "test_data.txt") + with GudPyContext() as gudpy: + with open(gudpy.testFilePath, "w", encoding="utf-8") as f: + f.write("' ' ' ' '/'\n\n") + f.write( + "INSTRUMENT {\n\n" + str(self.goodInstrument) + + "\n\n}\n\n" + ) + f.write("BEAM {\n\n" + str(self.goodBeam) + "\n\n}\n\n") + self.assertRaises( + ParserException, gudpy.loadFromGudrunFile, + gudpy.testFilePath) def testLoadMissingInstrumentAndBeam(self): - with open("test_data.txt", "w", encoding="utf-8") as f: - f.write("' ' ' ' '/'\n\n") - f.write( - "NORMALISATION {\n\n" - + str(self.goodNormalisation) - + "\n\n}" - ) - with GudPyContext() as gudpy: + with open(gudpy.testFilePath, "w", encoding="utf-8") as f: + f.write("' ' ' ' '/'\n\n") + f.write( + "NORMALISATION {\n\n" + + str(self.goodNormalisation) + + "\n\n}" + ) + self.assertRaises( - ParserException, gudpy.loadFromGudrunFile, "test_data.txt") + ParserException, gudpy.loadFromGudrunFile, gudpy.testFilePath) def testLoadMissingInstrumentAndNormalisation(self): - with open("test_data.txt", "w", encoding="utf-8") as f: - f.write("' ' ' ' '/'\n\n") - f.write("BEAM {\n\n" + str(self.goodBeam) + "\n\n}") - with GudPyContext() as gudpy: + with open(gudpy.testFilePath, "w", encoding="utf-8") as f: + f.write("' ' ' ' '/'\n\n") + f.write("BEAM {\n\n" + str(self.goodBeam) + "\n\n}") self.assertRaises( - ParserException, gudpy.loadFromGudrunFile, "test_data.txt") + ParserException, gudpy.loadFromGudrunFile, + gudpy.testFilePath) def testLoadMissingNormalisationAndBeam(self): - with open("test_data.txt", "w", encoding="utf-8") as f: - f.write("' ' ' ' '/'\n\n") - f.write( - "INSTRUMENT {\n\n" + str(self.goodInstrument) + "\n\n}" - ) - with GudPyContext() as gudpy: + with open(gudpy.testFilePath, "w", encoding="utf-8") as f: + f.write("' ' ' ' '/'\n\n") + f.write( + "INSTRUMENT {\n\n" + + str(self.goodInstrument) + "\n\n}" + ) self.assertRaises( - ParserException, gudpy.loadFromGudrunFile, "test_data.txt") + ParserException, gudpy.loadFromGudrunFile, + gudpy.testFilePath) def testLoadMissingInstrumentAttributesSeq(self): expectedInstrument = deepcopy(self.expectedInstrument) @@ -1080,15 +1080,16 @@ def testLoadMissingInstrumentAttributesSeq(self): del badInstrument[i] badInstrument = "\n".join(badInstrument) - with open("test_data.txt", "w", encoding="utf-8") as f: - f.write("' ' ' ' '/'\n\n") - f.write( - "INSTRUMENT {\n\n" + str(badInstrument) + "\n\n}" - ) - - with GudPyContext() as gudpy: - self.assertRaises( - ParserException, gudpy.loadFromGudrunFile, "test_data.txt") + with GudPyContext() as gudpy: + with open(gudpy.testFilePath, "w", encoding="utf-8") as f: + f.write("' ' ' ' '/'\n\n") + f.write( + "INSTRUMENT {\n\n" + + str(badInstrument) + "\n\n}" + ) + self.assertRaises( + ParserException, gudpy.loadFromGudrunFile, + gudpy.testFilePath) def testLoadMissingInstrumentAttributesRand(self): expectedInstrument = deepcopy(self.expectedInstrument) @@ -1102,7 +1103,6 @@ def testLoadMissingInstrumentAttributesRand(self): expectedInstrument.pop("goodDetectorThreshold", None) expectedInstrument.pop("yamlignore", None) for i in range(50): - key = random.choice(list(expectedInstrument)) j = list(expectedInstrument).index(key) @@ -1110,15 +1110,16 @@ def testLoadMissingInstrumentAttributesRand(self): del badInstrument[j] badInstrument = "\n".join(badInstrument) - with open("test_data.txt", "w", encoding="utf-8") as f: - f.write("' ' ' ' '/'\n\n") - f.write( - "INSTRUMENT {\n\n" + str(badInstrument) + "\n\n}" - ) with GudPyContext() as gudpy: + with open(gudpy.testFilePath, "w", encoding="utf-8") as f: + f.write("' ' ' ' '/'\n\n") + f.write( + "INSTRUMENT {\n\n" + + str(badInstrument) + "\n\n}" + ) self.assertRaises( ParserException, gudpy.loadFromGudrunFile, - "test_data.txt") + gudpy.testFilePath) def testLoadMissingBeamAttributesSeq(self): expectedBeam = deepcopy(self.expectedBeam) @@ -1136,20 +1137,18 @@ def testLoadMissingBeamAttributesSeq(self): badBeam = str(self.goodBeam).split("\n") del badBeam[i] badBeam = "\n".join(badBeam) - - with open("test_data.txt", "w", encoding="utf-8") as f: - f.write("' ' ' ' '/'\n\n") - f.write( - "INSTRUMENT {\n\n" - + str(self.goodInstrument) - + "\n\n}" - ) - f.write("\n\nBEAM {\n\n" + str(badBeam) + "\n\n}") - with GudPyContext() as gudpy: + with open(gudpy.testFilePath, "w", encoding="utf-8") as f: + f.write("' ' ' ' '/'\n\n") + f.write( + "INSTRUMENT {\n\n" + + str(self.goodInstrument) + + "\n\n}" + ) + f.write("\n\nBEAM {\n\n" + str(badBeam) + "\n\n}") self.assertRaises( ParserException, gudpy.loadFromGudrunFile, - "test_data.txt") + gudpy.testFilePath) def testLoadMissingBeamAttributesRand(self): expectedBeam = deepcopy(self.expectedBeam) @@ -1172,19 +1171,18 @@ def testLoadMissingBeamAttributesRand(self): del badBeam[j] badBeam = "\n".join(badBeam) - with open("test_data.txt", "w", encoding="utf-8") as f: - f.write("' ' ' ' '/'\n\n") - f.write( - "INSTRUMENT {\n\n" - + str(self.goodInstrument) - + "\n\n}" - ) - f.write("\n\nBEAM {\n\n" + str(badBeam) + "\n\n}") - with GudPyContext() as gudpy: + with open(gudpy.testFilePath, "w", encoding="utf-8") as f: + f.write("' ' ' ' '/'\n\n") + f.write( + "INSTRUMENT {\n\n" + + str(self.goodInstrument) + + "\n\n}" + ) + f.write("\n\nBEAM {\n\n" + str(badBeam) + "\n\n}") self.assertRaises( ParserException, gudpy.loadFromGudrunFile, - "test_data.txt") + gudpy.testFilePath) def testLoadMissingNormalisationAttributesSeq(self): expectedNormalisation = deepcopy(self.expectedNormalisation) @@ -1196,6 +1194,7 @@ def testLoadMissingNormalisationAttributesSeq(self): expectedNormalisation.pop("outerRadius", None) expectedNormalisation.pop("sampleHeight", None) expectedNormalisation.pop("crossSectionFilename") + expectedNormalisation.pop("outputFolder", None) expectedNormalisation.pop("yamlignore", None) self.goodNormalisation.dataFiles = DataFiles([], "") @@ -1206,25 +1205,24 @@ def testLoadMissingNormalisationAttributesSeq(self): badNormalisation = str(self.goodNormalisation).split("\n") del badNormalisation[i] badNormalisation = "\n".join(badNormalisation) - - with open("test_data.txt", "w", encoding="utf-8") as f: - f.write("' ' ' ' '/'\n\n") - f.write( - "INSTRUMENT {\n\n" - + str(self.goodInstrument) - + "\n\n}" - ) - f.write("\n\nBEAM {\n\n" + str(self.goodBeam) + "\n\n}") - f.write( - "\n\nNORMALISATION {\n\n" - + str(badNormalisation) - + "\n\n}" - ) - with GudPyContext() as gudpy: + with open(gudpy.testFilePath, "w", encoding="utf-8") as f: + f.write("' ' ' ' '/'\n\n") + f.write( + "INSTRUMENT {\n\n" + + str(self.goodInstrument) + + "\n\n}" + ) + f.write( + "\n\nBEAM {\n\n" + str(self.goodBeam) + "\n\n}") + f.write( + "\n\nNORMALISATION {\n\n" + + str(badNormalisation) + + "\n\n}" + ) self.assertRaises( ParserException, gudpy.loadFromGudrunFile, - "test_data.txt") + gudpy.testFilePath) def testLoadMissingNormalisationAttributesRand(self): expectedNormalisation = deepcopy(self.expectedNormalisation) @@ -1236,6 +1234,7 @@ def testLoadMissingNormalisationAttributesRand(self): expectedNormalisation.pop("outerRadius", None) expectedNormalisation.pop("sampleHeight", None) expectedNormalisation.pop("crossSectionFilename") + expectedNormalisation.pop("outputFolder", None) expectedNormalisation.pop("yamlignore", None) self.goodNormalisation.dataFiles = DataFiles([], "") @@ -1251,44 +1250,46 @@ def testLoadMissingNormalisationAttributesRand(self): del badNormalisation[j] badNormalisation = "\n".join(badNormalisation) - with open("test_data.txt", "w", encoding="utf-8") as f: - f.write("' ' ' ' '/'\n\n") - f.write( - "INSTRUMENT {\n\n" - + str(self.goodInstrument) - + "\n\n}" - ) - f.write("\n\nBEAM {\n\n" + str(self.goodBeam) + "\n\n}") - f.write( - "\n\nNORMALISATION {\n\n" - + str(badNormalisation) - + "\n\n}" - ) with GudPyContext() as gudpy: + with open(gudpy.testFilePath, "w", encoding="utf-8") as f: + f.write("' ' ' ' '/'\n\n") + f.write( + "INSTRUMENT {\n\n" + + str(self.goodInstrument) + + "\n\n}" + ) + f.write( + "\n\nBEAM {\n\n" + str(self.goodBeam) + "\n\n}") + f.write( + "\n\nNORMALISATION {\n\n" + + str(badNormalisation) + + "\n\n}" + ) self.assertRaises( ParserException, gudpy.loadFromGudrunFile, - "test_data.txt") + gudpy.testFilePath) def testLoadMissingSampleBackgroundAttributes(self): badSampleBackground = str(self.goodSampleBackground).split("\n") del badSampleBackground[2] badSampleBackground = "\n".join(badSampleBackground) - with open("test_data.txt", "w", encoding="utf-8") as f: - f.write("' ' ' ' '/'\n\n") - f.write( - "INSTRUMENT {\n\n" + str(self.goodInstrument) + "\n\n}" - ) - f.write("\n\nBEAM {\n\n" + str(self.goodBeam) + "\n\n}") - f.write( - "\n\nNORMALISATION {\n\n" - + str(self.goodNormalisation) - + "\n\n}" - ) - f.write("\n\n{}\n\nEND".format(str(badSampleBackground))) - with GudPyContext() as gudpy: - self.assertRaises( - ParserException, gudpy.loadFromGudrunFile, - "test_data.txt") + with GudPyContext() as gudpy: + with open(gudpy.testFilePath, "w", encoding="utf-8") as f: + f.write("' ' ' ' '/'\n\n") + f.write( + "INSTRUMENT {\n\n" + + str(self.goodInstrument) + "\n\n}" + ) + f.write("\n\nBEAM {\n\n" + str(self.goodBeam) + "\n\n}") + f.write( + "\n\nNORMALISATION {\n\n" + + str(self.goodNormalisation) + + "\n\n}" + ) + f.write("\n\n{}\n\nEND".format(str(badSampleBackground))) + self.assertRaises( + ParserException, gudpy.loadFromGudrunFile, + gudpy.testFilePath) def testLoadMissingSampleAttributesSeq(self): expectedSampleA = deepcopy(self.expectedSampleA) @@ -1304,6 +1305,8 @@ def testLoadMissingSampleAttributesSeq(self): expectedSampleA.pop("exponentialValues", None) expectedSampleA.pop("crossSectionFilename", None) expectedSampleA.pop("FTMode", None) + expectedSampleA.pop("outputFolder", None) + expectedSampleA.pop("sampleFile", None) expectedSampleA.pop("yamlignore", None) self.goodSampleBackground.samples[0].dataFiles = DataFiles([], "") @@ -1321,25 +1324,25 @@ def testLoadMissingSampleAttributesSeq(self): badSampleBackground = sbgStr.split("\n") del badSampleBackground[i + 10] badSampleBackground = "\n".join(badSampleBackground) - with open("test_data.txt", "w", encoding="utf-8") as f: - f.write("' ' ' ' '/'\n\n") - f.write( - "INSTRUMENT {\n\n" - + str(self.goodInstrument) - + "\n\n}" - ) - f.write("\n\nBEAM {\n\n" + str(self.goodBeam) + "\n\n}") - f.write( - "\n\nNORMALISATION {\n\n" - + str(self.goodNormalisation) - + "\n\n}" - ) - f.write("\n\n{}\n\nEND".format(str(badSampleBackground))) - with GudPyContext() as gudpy: + with open(gudpy.testFilePath, "w", encoding="utf-8") as f: + f.write("' ' ' ' '/'\n\n") + f.write( + "INSTRUMENT {\n\n" + + str(self.goodInstrument) + + "\n\n}" + ) + f.write( + "\n\nBEAM {\n\n" + str(self.goodBeam) + "\n\n}") + f.write( + "\n\nNORMALISATION {\n\n" + + str(self.goodNormalisation) + + "\n\n}" + ) + f.write("\n\n{}\n\nEND".format(str(badSampleBackground))) self.assertRaises( ParserException, gudpy.loadFromGudrunFile, - "test_data.txt") + gudpy.testFilePath) def testLoadMissingSampleAttributesRand(self): expectedSampleA = deepcopy(self.expectedSampleA) @@ -1355,6 +1358,8 @@ def testLoadMissingSampleAttributesRand(self): expectedSampleA.pop("exponentialValues", None) expectedSampleA.pop("crossSectionFilename", None) expectedSampleA.pop("FTMode", None) + expectedSampleA.pop("outputFolder", None) + expectedSampleA.pop("sampleFile", None) expectedSampleA.pop("yamlignore", None) self.goodSampleBackground.samples[0].dataFiles = DataFiles([], "") @@ -1375,24 +1380,25 @@ def testLoadMissingSampleAttributesRand(self): del badSampleBackground[j + 10] badSampleBackground = "\n".join(badSampleBackground) - with open("test_data.txt", "w", encoding="utf-8") as f: - f.write("' ' ' ' '/'\n\n") - f.write( - "INSTRUMENT {\n\n" - + str(self.goodInstrument) - + "\n\n}" - ) - f.write("\n\nBEAM {\n\n" + str(self.goodBeam) + "\n\n}") - f.write( - "\n\nNORMALISATION {\n\n" - + str(self.goodNormalisation) - + "\n\n}" - ) - f.write("\n\n{}\n\nEND".format(str(badSampleBackground))) with GudPyContext() as gudpy: + with open(gudpy.testFilePath, "w", encoding="utf-8") as f: + f.write("' ' ' ' '/'\n\n") + f.write( + "INSTRUMENT {\n\n" + + str(self.goodInstrument) + + "\n\n}" + ) + f.write( + "\n\nBEAM {\n\n" + str(self.goodBeam) + "\n\n}") + f.write( + "\n\nNORMALISATION {\n\n" + + str(self.goodNormalisation) + + "\n\n}" + ) + f.write("\n\n{}\n\nEND".format(str(badSampleBackground))) self.assertRaises( ParserException, gudpy.loadFromGudrunFile, - "test_data.txt") + gudpy.testFilePath) def testLoadMissingContainerAttributesSeq(self): expectedContainerA = deepcopy(self.expectedContainerA) @@ -1415,6 +1421,7 @@ def testLoadMissingContainerAttributesSeq(self): expectedContainerA.pop("powerForBroadening", None) expectedContainerA.pop("stepSize", None) expectedContainerA.pop("yamlignore", None) + expectedContainerA.pop("outputFolder", None) self.goodSampleBackground.samples[0].containers[0].dataFiles = ( DataFiles([], "") @@ -1431,24 +1438,25 @@ def testLoadMissingContainerAttributesSeq(self): del badSampleBackground[i + 44] badSampleBackground = "\n".join(badSampleBackground) - with open("test_data.txt", "w", encoding="utf-8") as f: - f.write("' ' ' ' '/'\n\n") - f.write( - "INSTRUMENT {\n\n" - + str(self.goodInstrument) - + "\n\n}" - ) - f.write("\n\nBEAM {\n\n" + str(self.goodBeam) + "\n\n}") - f.write( - "\n\nNORMALISATION {\n\n" - + str(self.goodNormalisation) - + "\n\n}" - ) - f.write("\n\n{}\n\nEND".format(str(badSampleBackground))) with GudPyContext() as gudpy: + with open(gudpy.testFilePath, "w", encoding="utf-8") as f: + f.write("' ' ' ' '/'\n\n") + f.write( + "INSTRUMENT {\n\n" + + str(self.goodInstrument) + + "\n\n}" + ) + f.write( + "\n\nBEAM {\n\n" + str(self.goodBeam) + "\n\n}") + f.write( + "\n\nNORMALISATION {\n\n" + + str(self.goodNormalisation) + + "\n\n}" + ) + f.write("\n\n{}\n\nEND".format(str(badSampleBackground))) self.assertRaises( ParserException, gudpy.loadFromGudrunFile, - "test_data.txt") + gudpy.testFilePath) def testAppendExponentialValues(self): with GudPyContext() as gudpy: @@ -1490,6 +1498,7 @@ def testLoadMissingContainerAttributesRand(self): expectedContainerA.pop("powerForBroadening", None) expectedContainerA.pop("stepSize", None) expectedContainerA.pop("yamlignore", None) + expectedContainerA.pop("outputFolder", None) self.goodSampleBackground.samples[0].containers[0].dataFiles = ( DataFiles([], "") @@ -1506,27 +1515,30 @@ def testLoadMissingContainerAttributesRand(self): ] sbgStr = str(self.goodSampleBackground) badSampleBackground = sbgStr.split("\n") + if not badSampleBackground[j + 44]: + continue del badSampleBackground[j + 44] badSampleBackground = "\n".join(badSampleBackground) - with open("test_data.txt", "w", encoding="utf-8") as f: - f.write("' ' ' ' '/'\n\n") - f.write( - "INSTRUMENT {\n\n" - + str(self.goodInstrument) - + "\n\n}" - ) - f.write("\n\nBEAM {\n\n" + str(self.goodBeam) + "\n\n}") - f.write( - "\n\nNORMALISATION {\n\n" - + str(self.goodNormalisation) - + "\n\n}" - ) - f.write("\n\n{}\n\nEND".format(str(badSampleBackground))) with GudPyContext() as gudpy: + with open(gudpy.testFilePath, "w", encoding="utf-8") as f: + f.write("' ' ' ' '/'\n\n") + f.write( + "INSTRUMENT {\n\n" + + str(self.goodInstrument) + + "\n\n}" + ) + f.write( + "\n\nBEAM {\n\n" + str(self.goodBeam) + "\n\n}") + f.write( + "\n\nNORMALISATION {\n\n" + + str(self.goodNormalisation) + + "\n\n}" + ) + f.write("\n\n{}\n\nEND".format(str(badSampleBackground))) self.assertRaises( ParserException, gudpy.loadFromGudrunFile, - "test_data.txt") + gudpy.testFilePath) def testZeroExitGudrun(self): with GudPyContext() as gudpy: From 6ae7b7bba0dfb6156628a797e38fe3684a49c42c Mon Sep 17 00:00:00 2001 From: Noella Spitz Date: Thu, 23 May 2024 14:29:52 +0100 Subject: [PATCH 34/78] Fix config setting --- gudpy/core/io/gudrun_file_parser.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/gudpy/core/io/gudrun_file_parser.py b/gudpy/core/io/gudrun_file_parser.py index bfe29a513..c82c52e61 100644 --- a/gudpy/core/io/gudrun_file_parser.py +++ b/gudpy/core/io/gudrun_file_parser.py @@ -53,9 +53,7 @@ def export(cls, gudrunFile: GudrunFile, path: str): f.write(str(gudrunFile)) f.close() - def parse(self, lines: list[str]) -> typ.Tuple[ - Instrument, Beam, Normalisation, - list[SampleBackground], Components]: + def parse(self, lines: list[str], config=False) -> GudrunFile: """ Parse the GudrunFile from a list of lines. Assign objects from the file to the attributes of the class. @@ -161,7 +159,7 @@ def parseFromPath( # Read the input stream into our attribute. with open(path, encoding=encoding) as fp: lines = fp.readlines() - return self.parse(lines) + return self.parse(lines, config) # ================================================================= # PARSING HELPERS From 0d8e261948dfc57353ac3b2f17bab01ff7c9ec26 Mon Sep 17 00:00:00 2001 From: Noella Spitz Date: Thu, 23 May 2024 16:29:42 +0100 Subject: [PATCH 35/78] Remove data files from assertion --- gudpy/test/test_gudpy_yaml.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/gudpy/test/test_gudpy_yaml.py b/gudpy/test/test_gudpy_yaml.py index 362151ae6..8ef01fbb5 100644 --- a/gudpy/test/test_gudpy_yaml.py +++ b/gudpy/test/test_gudpy_yaml.py @@ -30,9 +30,11 @@ def testYAML(self): normalisationDataFilesA = gf1.normalisation.__dict__.pop( "dataFiles") + normalisationDataFilesA.pop("_dataFiles") normalisationDataFilesBgA = gf1.normalisation.__dict__.pop( "dataFilesBg" ) + normalisationDataFilesBgA.pop("_dataFiles") normalisationCompositionA = gf1.normalisation.__dict__.pop( "composition" ) @@ -42,9 +44,11 @@ def testYAML(self): normalisationDataFilesB = gf2.normalisation.__dict__.pop( "dataFiles") + normalisationDataFilesB.pop("_dataFiles") normalisationDataFilesBgB = gf2.normalisation.__dict__.pop( "dataFilesBg" ) + normalisationDataFilesBgB.pop("_dataFiles") normalisationCompositionB = gf2.normalisation.__dict__.pop( "composition" ) @@ -76,6 +80,7 @@ def testYAML(self): sampleBackgroundDataFilesA = gf1.sampleBackgrounds[0].__dict__.pop( "dataFiles" ) + sampleBackgroundDataFilesA.pop("_dataFiles") sampleBackgroundSamplesA = gf1.sampleBackgrounds[0].__dict__.pop( "samples" ) @@ -83,6 +88,7 @@ def testYAML(self): sampleBackgroundDataFilesB = gf2.sampleBackgrounds[0].__dict__.pop( "dataFiles" ) + sampleBackgroundDataFilesB.pop("_dataFiles") sampleBackgroundSamplesB = gf2.sampleBackgrounds[0].__dict__.pop( "samples" ) @@ -100,11 +106,13 @@ def testYAML(self): sampleBackgroundSamplesA, sampleBackgroundSamplesB ): sampleDataFilesA = sampleA.__dict__.pop("dataFiles") + sampleDataFilesA.pop("_dataFiles") sampleCompositionA = sampleA.__dict__.pop("composition") sampleElementsA = sampleCompositionA.__dict__.pop("elements") sampleContainersA = sampleA.__dict__.pop("containers") sampleDataFilesB = sampleB.__dict__.pop("dataFiles") + sampleDataFilesB.pop("_dataFiles") sampleCompositionB = sampleB.__dict__.pop("composition") sampleElementsB = sampleCompositionB.__dict__.pop("elements") sampleContainersB = sampleB.__dict__.pop("containers") @@ -125,6 +133,7 @@ def testYAML(self): sampleContainersA, sampleContainersB ): containerDataFilesA = containerA.__dict__.pop("dataFiles") + containerDataFilesA.pop("_dataFiles") containerCompositionA = containerA.__dict__.pop( "composition") containerElementsA = containerCompositionA.__dict__.pop( @@ -132,6 +141,7 @@ def testYAML(self): ) containerDataFilesB = containerB.__dict__.pop("dataFiles") + containerDataFilesB.pop("_dataFiles") containerCompositionB = containerB.__dict__.pop( "composition") containerElementsB = containerCompositionB.__dict__.pop( From cfec70a50f27c21e0fa8c79731c14cb67740a6f1 Mon Sep 17 00:00:00 2001 From: Noella Spitz Date: Thu, 23 May 2024 16:30:07 +0100 Subject: [PATCH 36/78] Remove print statement --- gudpy/gui/widgets/core/control.py | 1 - 1 file changed, 1 deletion(-) diff --git a/gudpy/gui/widgets/core/control.py b/gudpy/gui/widgets/core/control.py index a92ba776b..1dc289346 100644 --- a/gudpy/gui/widgets/core/control.py +++ b/gudpy/gui/widgets/core/control.py @@ -296,7 +296,6 @@ def exportInputFile(self): "Export input file as..", ".", ) - print(filename) if filename: filename.replace(".txt", "") filename += ".txt" From 892f90a78141ef7e3d7125b0974bdb3f2e0bd534 Mon Sep 17 00:00:00 2001 From: Noella Spitz Date: Thu, 23 May 2024 16:43:41 +0100 Subject: [PATCH 37/78] Pop the dict --- gudpy/test/test_gudpy_yaml.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/gudpy/test/test_gudpy_yaml.py b/gudpy/test/test_gudpy_yaml.py index 8ef01fbb5..6be08f132 100644 --- a/gudpy/test/test_gudpy_yaml.py +++ b/gudpy/test/test_gudpy_yaml.py @@ -30,11 +30,11 @@ def testYAML(self): normalisationDataFilesA = gf1.normalisation.__dict__.pop( "dataFiles") - normalisationDataFilesA.pop("_dataFiles") + normalisationDataFilesA.__dict__.pop("_dataFiles") normalisationDataFilesBgA = gf1.normalisation.__dict__.pop( "dataFilesBg" ) - normalisationDataFilesBgA.pop("_dataFiles") + normalisationDataFilesBgA.__dict__.pop("_dataFiles") normalisationCompositionA = gf1.normalisation.__dict__.pop( "composition" ) @@ -44,11 +44,11 @@ def testYAML(self): normalisationDataFilesB = gf2.normalisation.__dict__.pop( "dataFiles") - normalisationDataFilesB.pop("_dataFiles") + normalisationDataFilesB.__dict__.pop("_dataFiles") normalisationDataFilesBgB = gf2.normalisation.__dict__.pop( "dataFilesBg" ) - normalisationDataFilesBgB.pop("_dataFiles") + normalisationDataFilesBgB.__dict__.pop("_dataFiles") normalisationCompositionB = gf2.normalisation.__dict__.pop( "composition" ) @@ -80,7 +80,7 @@ def testYAML(self): sampleBackgroundDataFilesA = gf1.sampleBackgrounds[0].__dict__.pop( "dataFiles" ) - sampleBackgroundDataFilesA.pop("_dataFiles") + sampleBackgroundDataFilesA.__dict__.pop("_dataFiles") sampleBackgroundSamplesA = gf1.sampleBackgrounds[0].__dict__.pop( "samples" ) @@ -88,7 +88,7 @@ def testYAML(self): sampleBackgroundDataFilesB = gf2.sampleBackgrounds[0].__dict__.pop( "dataFiles" ) - sampleBackgroundDataFilesB.pop("_dataFiles") + sampleBackgroundDataFilesB.__dict__.pop("_dataFiles") sampleBackgroundSamplesB = gf2.sampleBackgrounds[0].__dict__.pop( "samples" ) @@ -106,13 +106,13 @@ def testYAML(self): sampleBackgroundSamplesA, sampleBackgroundSamplesB ): sampleDataFilesA = sampleA.__dict__.pop("dataFiles") - sampleDataFilesA.pop("_dataFiles") + sampleDataFilesA.__dict__.pop("_dataFiles") sampleCompositionA = sampleA.__dict__.pop("composition") sampleElementsA = sampleCompositionA.__dict__.pop("elements") sampleContainersA = sampleA.__dict__.pop("containers") sampleDataFilesB = sampleB.__dict__.pop("dataFiles") - sampleDataFilesB.pop("_dataFiles") + sampleDataFilesB.__dict__.pop("_dataFiles") sampleCompositionB = sampleB.__dict__.pop("composition") sampleElementsB = sampleCompositionB.__dict__.pop("elements") sampleContainersB = sampleB.__dict__.pop("containers") @@ -133,7 +133,7 @@ def testYAML(self): sampleContainersA, sampleContainersB ): containerDataFilesA = containerA.__dict__.pop("dataFiles") - containerDataFilesA.pop("_dataFiles") + containerDataFilesA.__dict__.pop("_dataFiles") containerCompositionA = containerA.__dict__.pop( "composition") containerElementsA = containerCompositionA.__dict__.pop( @@ -141,7 +141,7 @@ def testYAML(self): ) containerDataFilesB = containerB.__dict__.pop("dataFiles") - containerDataFilesB.pop("_dataFiles") + containerDataFilesB.__dict__.pop("_dataFiles") containerCompositionB = containerB.__dict__.pop( "composition") containerElementsB = containerCompositionB.__dict__.pop( From 428884a679f077576e062a47f2f16266cb375923 Mon Sep 17 00:00:00 2001 From: Noella Spitz Date: Tue, 28 May 2024 09:58:12 +0100 Subject: [PATCH 38/78] Apply rebase --- gudpy/core/container.py | 2 + gudpy/core/data_files.py | 119 ++++++++++++++++-- gudpy/core/file_library.py | 14 ++- gudpy/core/gudpy.py | 68 ++++++++++ gudpy/core/gudpy_yaml.py | 25 ++-- gudpy/core/gudrun_file.py | 11 +- gudpy/core/iterators.py | 22 +++- gudpy/core/output_file_handler.py | 63 +++++++--- gudpy/core/sample.py | 13 ++ gudpy/core/sample_background.py | 3 + gudpy/gui/widgets/core/worker.py | 9 +- gudpy/gui/widgets/dialogs/io.py | 6 +- gudpy/gui/widgets/slots/container_slots.py | 4 +- .../gui/widgets/slots/normalisation_slots.py | 8 +- .../widgets/slots/sample_background_slots.py | 4 +- gudpy/gui/widgets/slots/sample_slots.py | 6 +- 16 files changed, 312 insertions(+), 65 deletions(-) diff --git a/gudpy/core/container.py b/gudpy/core/container.py index e401a42bb..4e9863633 100644 --- a/gudpy/core/container.py +++ b/gudpy/core/container.py @@ -113,6 +113,8 @@ def __init__(self, config=None): if config: self.parseFromConfig(config) + self.outputFolder = "" + def __str__(self): """ Returns the string representation of the Container object. diff --git a/gudpy/core/data_files.py b/gudpy/core/data_files.py index f8a9b1ff8..6e5defc32 100644 --- a/gudpy/core/data_files.py +++ b/gudpy/core/data_files.py @@ -1,4 +1,77 @@ +import os + from core import config +from core import utils +from core.gud_file import GudFile + + +class DataFile: + def __init__(self, filename, isSampleDataFile: bool = False): + self.filename = filename + self.name = utils.replace_unwanted_chars( + os.path.splitext(filename)[0] + ) + self.ext = os.path.splitext(filename)[1] + self.outputFolder = "" + self._outputs = {} + self.isSampleDataFile = isSampleDataFile + + self.yamlignore = { + "str", + "yamlignore" + } + + def detectSampleDataFile(self, path): + for f in os.listdir(path): + if f == "Diagnostics": + self.isSampleDataFile = True + + def constructDictFromDir(self, dirPath, dict): + for f in os.listdir(dirPath): + ext = os.path.splitext(f) + dict[ext] = f + + def importOutputs(self, path): + self.detectSampleDataFile(path) + if self.isSampleDataFile: + outputsDir = os.path.join(path, "Outputs") + diagDir = os.path.join(path, "Diagnostics") + self.constructDictFromDir(outputsDir, self._outputs) + self.constructDictFromDir(diagDir, self.diagnostics) + else: + self.constructDictFromDir(path, self._outputs) + + def addOutput(self, path): + ext = os.path.splitext(path)[1] + self._outputs[ext] = path + + def outputs(self, ext): + return self._outputs[ext] + + def __str__(self): + return self.filename + + @property + def gudFile(self): + if not self.isSampleDataFile: + return None + if not self.gudFile: + gudPath = self._outputs.get(".gud", None) + if not gudPath: + return None + return GudFile(gudPath) + + @property + def mintFile(self): + if not self.isSampleDataFile: + return None + return self._outputs.get(".mint01", None) + + @property + def msubwFile(self): + if not self.isSampleDataFile: + return None + return self._outputs.get(".msubw01", None) class DataFiles: @@ -28,7 +101,10 @@ def __init__(self, dataFiles, name): name : str Name of the parent of the data files, e.g. Sample Background """ - self.dataFiles = dataFiles + + self._dataFiles = [] + self.setFiles(dataFiles) + self.name = name self.yamlignore = { @@ -36,6 +112,24 @@ def __init__(self, dataFiles, name): "yamlignore" } + self.outputFolders = {} + self._outputs = {} + + def setFiles(self, dataFilenames): + self._dataFiles.clear() + for dataFile in dataFilenames: + if not dataFile: + continue + self._dataFiles.append(DataFile(dataFile)) + + def importOutputs(self, path): + self.outputFolder = path + children = os.listdir(path) + + for df in self._dataFiles: + if df.name in children: + return + def __str__(self): """ Returns the string representation of the DataFiles object. @@ -50,11 +144,20 @@ def __str__(self): String representation of DataFiles. """ self.str = [ - df + config.spc10 + self.name + " data files" - for df in self.dataFiles + df.filename + config.spc10 + self.name + " data files" + for df in self._dataFiles ] return """\n""".join(self.str) + @property + def dataFilenames(self): + dfNames = [df.filename for df in self._dataFiles] + return dfNames + + @property + def dataFiles(self): + return self._dataFiles + def __len__(self): """ Returns the length of the dataFiles list member. @@ -68,15 +171,15 @@ def __len__(self): int Number of data files, """ - return len(self.dataFiles) + return len(self._dataFiles) def __getitem__(self, n): - return self.dataFiles[n] + return self._dataFiles[n].filename def __setitem__(self, n, item): if n >= len(self): - self.dataFiles.extend(n+1) - self.dataFiles[n] = item + self._dataFiles.extend(n+1) + self._dataFiles[n] = DataFile(item) def __iter__(self): - return iter(self.dataFiles) + return iter(self._dataFiles) diff --git a/gudpy/core/file_library.py b/gudpy/core/file_library.py index 77d7acdab..1781b82c2 100644 --- a/gudpy/core/file_library.py +++ b/gudpy/core/file_library.py @@ -64,8 +64,8 @@ def __init__(self, gudrunFile): } self.dataFiles = [ - *gudrunFile.normalisation.dataFiles.dataFiles, - *gudrunFile.normalisation.dataFilesBg.dataFiles, + *gudrunFile.normalisation.dataFiles.dataFilenames, + *gudrunFile.normalisation.dataFilesBg.dataFilenames, ] # If NXS files are being used @@ -90,15 +90,15 @@ def __init__(self, gudrunFile): # that file too. for sampleBackground in gudrunFile.sampleBackgrounds: - self.dataFiles.extend(sampleBackground.dataFiles.dataFiles) + self.dataFiles.extend(sampleBackground.dataFiles.dataFilenames) for sample in sampleBackground.samples: - self.dataFiles.extend(sample.dataFiles.dataFiles) + self.dataFiles.extend(sample.dataFiles.dataFilenames) if sample.totalCrossSectionSource == CrossSectionSource.FILE: self.files[sample.name] = sample.crossSectionFilename for container in sample.containers: - self.dataFiles.extend(container.dataFiles.dataFiles) + self.dataFiles.extend(container.dataFiles.dataFilenames) if container.totalCrossSectionSource == ( CrossSectionSource.FILE ): @@ -180,7 +180,7 @@ def exportMintData( if len(sample.dataFiles.dataFiles): path = os.path.join( self.gudrunFile.projectDir, - sample.dataFiles.dataFiles[0].replace( + sample.dataFiles.dataFilenames[0].replace( self.gudrunFile.instrument.dataFileType, "mint01" ), ) @@ -199,6 +199,8 @@ def exportMintData( self.gudrunFile.projectDir, safeSampleName + ".sample", ) + sample.sampleFile = path + if not os.path.exists(path): sample.write_out( self.gudrunFile.projectDir diff --git a/gudpy/core/gudpy.py b/gudpy/core/gudpy.py index 1072d5ead..5e6474ef2 100644 --- a/gudpy/core/gudpy.py +++ b/gudpy/core/gudpy.py @@ -485,6 +485,13 @@ def organiseOutput( gudrunOutput = outputHandler.organiseOutput(exclude=exclude) return gudrunOutput + def obtainDependencies( + fileSelfScattering, + gudrunFile: GudrunFile + ): + if gudrunFile.instrument.subWavelengthBinnedData: + return + def gudrun( self, gudrunFile: GudrunFile, @@ -615,6 +622,67 @@ def iterate(self, purge, save=True) -> typ.Tuple[int, str]: return self.exitcode +class InelasiticityIterator(GudrunIterator): + def __init__( + self, + gudrunFile: GudrunFile, + iterator: iterators.Iterator, + ): + + super().__init__( + gudrunFile=gudrunFile, + iterator=iterator + ) + + def copyDCS(self, prevOutput): + return + + def singleIteration( + self, + gudrunFile: GudrunFile, + gudrun: Gudrun, + purge: Purge, + prevOutput: handlers.GudrunOutput, + save=True + ) -> typ.Tuple[int, str]: # (exitcode, error) + modGfFile = self.iterator.performIteration(gudrunFile, prevOutput) + exitcode = gudrun.gudrun(modGfFile, purge, self.iterator, save=save) + if exitcode: + return exitcode + self.gudrunOutput = gudrun.gudrunOutput + return 0 + + def iterate(self, purge, save=True) -> typ.Tuple[int, str]: + prevOutput = None + + # If the iterator requires a prelimenary run + if self.iterator.requireDefault: + exitcode = self.gudrunObjects[0].gudrun( + self.gudrunFile, purge, self.iterator, save) + if exitcode: # An exit code != 0 indicates failure + self.exitcode = (exitcode, self.gudrunObjects[0].error) + return self.exitcode + prevOutput = self.gudrunObjects[0].gudrunOutput + + # Iterate through gudrun objects + for gudrun in self.gudrunObjects: + if gudrun.output: + # If object has already been run, skip + continue + exitcode = self.singleIteration( + self.gudrunFile, gudrun, purge, prevOutput, save) + if exitcode: # An exit code != 0 indicates failure + self.exitcode = (exitcode, gudrun.error) + return self.exitcode + + prevOutput = gudrun.gudrunOutput + + self.result = self.iterator.result + + self.exitcode = (0, "") + return self.exitcode + + class CompositionIterator: def __init__( self, diff --git a/gudpy/core/gudpy_yaml.py b/gudpy/core/gudpy_yaml.py index 4682db6c1..ccbb1d415 100644 --- a/gudpy/core/gudpy_yaml.py +++ b/gudpy/core/gudpy_yaml.py @@ -7,7 +7,7 @@ from core.composition import ( Component, Components, Composition, WeightedComponent ) -from core.data_files import DataFiles +from core.data_files import DataFile, DataFiles from core.element import Element from core.exception import YAMLException from core.gui_config import GUIConfig @@ -23,7 +23,6 @@ class YAML: - def __init__(self): self.yaml = self.getYamlModule() @@ -95,18 +94,17 @@ def maskYAMLDicttoClass(self, cls, yamldict): for k, v in yamldict.items(): if not hasattr(cls, k): # If attribute is not valid - raise YAMLException( - f"Invalid attribute '{k}' given to '{type(cls).__name__}'") + print(f"Ignoring depreciated attribute '{k}'" + f"given to '{type(cls).__name__}'") + continue if isinstance(cls.__dict__[k], Enum): setattr(cls, k, type(cls.__dict__[k])[v]) elif isinstance(cls.__dict__[k], DataFiles): - setattr( - cls, k, - DataFiles( - [v_ for v_ in v["dataFiles"]], v["name"]) - ) + for df in v["_dataFiles"]: + dataFile = DataFile(df["filename"]) + self.maskYAMLDicttoClass(dataFile, df) elif ( isinstance( @@ -233,7 +231,7 @@ def toYaml(self, var): elif isinstance(var, ( Instrument, Beam, Components, Normalisation, SampleBackground, Sample, Container, WeightedComponent, - Component, Composition, Element, DataFiles, GUIConfig + Component, Composition, Element, DataFiles, DataFile, GUIConfig )): return { k: self.toYaml(v) @@ -243,8 +241,15 @@ def toYaml(self, var): @abstractmethod def toBuiltin(self, yamlvar): + if yamlvar is None: + return None if isinstance(yamlvar, (list, tuple)): return [self.toBuiltin(v) for v in yamlvar] + elif (yamlvar.__class__.__module__ == + "ruamel.yaml.comments.CommentedMap"): + dict = {} + for k, v in yamlvar.items(): + dict[k] = v elif yamlvar.__class__.__module__ == "builtins": return yamlvar elif yamlvar.__class__.__module__ == "ruamel.yaml.scalarfloat": diff --git a/gudpy/core/gudrun_file.py b/gudpy/core/gudrun_file.py index 0a50f4d64..de60d15fd 100644 --- a/gudpy/core/gudrun_file.py +++ b/gudpy/core/gudrun_file.py @@ -182,6 +182,12 @@ def __deepcopy__(self, memo): setattr(result, k, deepcopy(v, memo)) return result + def runSamples(self): + return [ + s for sb in self.sampleBackgrounds + for s in sb.samples + if s.runThisSample] + def path(self): if not self.projectDir: return None @@ -1478,7 +1484,10 @@ def save(self, path='', format=None): self.write_out( path=f"{os.path.splitext(path)[0]}.txt", overwrite=True) elif format == Format.YAML: - self.write_yaml(path=f"{os.path.splitext(path)[0]}.yaml") + if self.path(): + filepath = os.path.join( + self.path(), f"{os.path.splitext(path)[0]}.yaml") + self.write_yaml(filepath) def write_yaml(self, path): self.yaml = YAML() diff --git a/gudpy/core/iterators.py b/gudpy/core/iterators.py index 233606938..34e8f1ac7 100644 --- a/gudpy/core/iterators.py +++ b/gudpy/core/iterators.py @@ -1,10 +1,12 @@ from copy import deepcopy +import os import math from enum import Enum from core.gud_file import GudFile from core.enums import Scales, IterationModes from core.gudrun_file import GudrunFile +from core import utils import core.output_file_handler as handlers @@ -493,7 +495,7 @@ def setSelfScatteringFiles(self, scale, gudrunFile, prevOutput=None): for sample in sampleBackground.samples: if sample.runThisSample and len(sample.dataFiles): target = sample - filename = target.dataFiles[0] + filename = target.dataFiles[0].filename targetFile = ( prevOutput.output( sample.name, filename, suffix) @@ -580,13 +582,29 @@ def organiseOutput(self, gudrunFile, exclude=[]): """ overwrite = (self.iterationCount == 1 and self.iterationType == "WavelengthIteration") + head = os.path.join( + utils.replace_unwanted_chars(self.name), + f"{self.iterationType}_{self.iterationCount}" + ) + + # Organise output history (don't overwrite) + outputHandler = handlers.GudrunOutputHandler( gudrunFile=gudrunFile, - head=f"{self.iterationType}_{self.iterationCount}", + head=head, overwrite=overwrite ) output = outputHandler.organiseOutput(exclude=exclude) self.gudrunOutputs.append(output) + + # Organise current output (overrides each time) + + outputHandler = handlers.GudrunOutputHandler( + gudrunFile=gudrunFile, + overwrite=True + ) + output = outputHandler.organiseOutput(exclude=exclude) + return output diff --git a/gudpy/core/output_file_handler.py b/gudpy/core/output_file_handler.py index 98631a584..243016e1f 100644 --- a/gudpy/core/output_file_handler.py +++ b/gudpy/core/output_file_handler.py @@ -203,12 +203,15 @@ def _createNormDir(self, dest: str): # Create normalisation folders and move datafiles for normFile in self.gudrunFile.normalisation.dataFiles: self._copyOutputs( - normFile, os.path.join( - dest, "Normalisation")) + normFile, dest, "Normalisation") + self.gudrunFile.normalisation.outputDir = os.path.join( + self.outputDir, "Normalisation" + ) for normBgFile in self.gudrunFile.normalisation.dataFilesBg: - self._copyOutputs(normBgFile, - os.path.join(dest, - "NormalisationBackground")) + self._copyOutputs(normBgFile, dest, "NormalisationBackground") + self.gudrunFile.normalisation.backgroundOutputDir = os.path.join( + self.outputDir, "NormalisationBackground" + ) def _createSampleBgDir(self, dest: str): """ @@ -227,10 +230,15 @@ def _createSampleBgDir(self, dest: str): for dataFile in sampleBackground.dataFiles: self._copyOutputs( dataFile, + dest, os.path.join( - dest, "SampleBackgrounds", - f"SampleBackground{count + 1}") - ) + "SampleBackgrounds", + f"SampleBackground{count + 1}" + )) + sampleBackground.outputDir = os.path.join( + self.outputDir, "SampleBackgrounds", + f"SampleBackground{count + 1}" + ) def _createSampleDir(self, dest: str): """ @@ -297,17 +305,21 @@ def _createSampleDir(self, dest: str): sampleOutputs[sample.name] = SampleOutput( sampleFile, gudFile, sampleOutput, sampleDiag) + sample.outputFolder = os.path.join( + self.outputDir, + utils.replace_unwanted_chars(sample.name) + + ) + # Create container folders within sample folder for container in sample.containers: - containerPath = os.path.join( - samplePath, - (utils.replace_unwanted_chars(container.name) - if container.name != "CONTAINER" - else "Container")) for dataFile in container.dataFiles: self._copyOutputs( dataFile, - containerPath + samplePath, + (utils.replace_unwanted_chars(container.name) + if container.name != "CONTAINER" + else "Container") ) return sampleOutputs @@ -349,7 +361,7 @@ def _createAddOutDir(self, dest: str, exclude: list[str] = []): continue return inputFile - def _copyOutputs(self, fpath, dest): + def _copyOutputs(self, dataFile, dest, folderName): """ Copy all files with the same basename as the provided filepath, except the original file. @@ -363,13 +375,15 @@ def _copyOutputs(self, fpath, dest): dest : str Directory for the files to be copied to """ - fname = os.path.splitext(fpath)[0] + dest = os.path.join(dest, folderName) + fname = os.path.splitext(dataFile.filename)[0] runDir = os.path.join(dest, fname) dirCreated = False for f in os.listdir(self.gudrunDir): # Get files with the same filename but not the same # extension - if os.path.splitext(f)[0] == fname: + name, ext = os.path.splitext(f) + if name == fname: if not dirCreated: utils.makeDir(runDir) dirCreated = True @@ -377,9 +391,15 @@ def _copyOutputs(self, fpath, dest): os.path.join(self.gudrunDir, f), os.path.join(runDir, f) ) + dataFile.outputFolder = os.path.join( + self.outputDir, folderName, fname + ) + dataFile.addOutput(os.path.join( + self.outputDir, folderName, fname, f + )) self.copiedFiles.append(f) - def _copyOutputsByExt(self, fpath, dest, folderName): + def _copyOutputsByExt(self, dataFile, dest, folderName): """ Copy all files with the same basename as the provided filepath and splits them into outputs @@ -404,7 +424,7 @@ def _copyOutputsByExt(self, fpath, dest, folderName): Dictionary mapping output extension to filepath """ # Data filename - fname = os.path.splitext(fpath)[0] + fname = os.path.splitext(dataFile.filename)[0] # Path to folder which will hold all outputs from the run runDir = os.path.join(dest, fname) # Has the run dir been created? @@ -431,9 +451,14 @@ def _copyOutputsByExt(self, fpath, dest, folderName): gudFile = GudFile(os.path.join(self.gudrunDir, f)) outputs[ext] = os.path.join( self.outputDir, folderName, fname, "Outputs", f) + dataFile.addOutput(os.path.join( + self.outputDir, folderName, fname, "Outputs", f)) + else: diagnostics[ext] = os.path.join( self.outputDir, folderName, fname, "Diagnostics", f) + dataFile.addOutput(os.path.join( + self.outputDir, folderName, fname, "Diagnostics", f)) shutil.copyfile( os.path.join(self.gudrunDir, f), os.path.join(dir, f) diff --git a/gudpy/core/sample.py b/gudpy/core/sample.py index 2f86ba95e..f16b9254f 100644 --- a/gudpy/core/sample.py +++ b/gudpy/core/sample.py @@ -1,3 +1,5 @@ +import os + from core.utils import bjoin, numifyBool from core.data_files import DataFiles from core.composition import Composition @@ -148,6 +150,17 @@ def __init__(self): "yamlignore" } + self.outputFolder = "" + self.sampleFile = "" + + def importOutputs(self, path): + self.outputFolder = path + for f in os.listdir(path): + ext = os.path.splitext(f) + if ext == ".sample": + self.sampleFile = f + return + def pathName(self): return utils.replace_unwanted_chars(self.name).translate( {ord(x): '' for x in r'/\!*~,&|[]'} diff --git a/gudpy/core/sample_background.py b/gudpy/core/sample_background.py index 4e454899e..2c4596ddb 100644 --- a/gudpy/core/sample_background.py +++ b/gudpy/core/sample_background.py @@ -18,6 +18,7 @@ class SampleBackground: Methods ------- """ + def __init__(self): """ Constructs all the necessary attributes for the @@ -37,6 +38,8 @@ def __init__(self): "yamlignore" } + self.outputFolder = "" + def __str__(self): """ Returns the string representation of the SampleBackground object. diff --git a/gudpy/gui/widgets/core/worker.py b/gudpy/gui/widgets/core/worker.py index f27ef0638..1456e8123 100644 --- a/gudpy/gui/widgets/core/worker.py +++ b/gudpy/gui/widgets/core/worker.py @@ -39,13 +39,16 @@ def __init__(self, purgeFile: PurgeFile, gudrunFile: GudrunFile): self.appendDataFiless(gudrunFile.normalisation.dataFiles[0]) self.appendDataFiless(gudrunFile.normalisation.dataFilesBg[0]) - self.appendDataFiless([df for sb in gudrunFile.sampleBackgrounds + self.appendDataFiless([df.filename for sb + in gudrunFile.sampleBackgrounds for df in sb.dataFiles]) if not purgeFile.excludeSampleAndCan: - self.appendDataFiless([df for sb in gudrunFile.sampleBackgrounds + self.appendDataFiless([df.filename for sb + in gudrunFile.sampleBackgrounds for s in sb.samples for df in s.dataFiles if s.runThisSample]) - self.appendDataFiless([df for sb in gudrunFile.sampleBackgrounds + self.appendDataFiless([df.filename for sb + in gudrunFile.sampleBackgrounds for s in sb.samples for c in s.containers for df in c.dataFiles if s.runThisSample]) diff --git a/gudpy/gui/widgets/dialogs/io.py b/gudpy/gui/widgets/dialogs/io.py index a5510ac5c..1a3407819 100644 --- a/gudpy/gui/widgets/dialogs/io.py +++ b/gudpy/gui/widgets/dialogs/io.py @@ -177,11 +177,7 @@ def toggleRename(self, state): def performExport(self, filename=None): fl = GudPyFileLibrary(self.gudrunFile) archive = fl.exportMintData( - [ - s - for sb in self.gudrunFile.sampleBackgrounds - for s in sb.samples - ], + self.gudrunFile.samples(), renameDataFiles=self.widget.renameCheckBox.checkState(), exportTo=filename, includeParams=self.widget.includeParamsCheckBox.checkState() diff --git a/gudpy/gui/widgets/slots/container_slots.py b/gudpy/gui/widgets/slots/container_slots.py index 87391a142..7aab92bf0 100644 --- a/gudpy/gui/widgets/slots/container_slots.py +++ b/gudpy/gui/widgets/slots/container_slots.py @@ -26,7 +26,7 @@ def setContainer(self, container): # Populate the data files list. self.widget.containerDataFilesList.makeModel( - self.container.dataFiles.dataFiles + self.container.dataFiles.dataFilenames ) self.widget.containerDataFilesList.model().dataChanged.connect( @@ -640,7 +640,7 @@ def handleDataFilesAltered(self): if not self.widgetsRefreshing: self.parent.setModified() self.parent.gudrunFile.purged = False - self.container.dataFiles.dataFiles = ( + self.container.dataFiles.setFiles( self.widget.containerDataFilesList.model().stringList() ) diff --git a/gudpy/gui/widgets/slots/normalisation_slots.py b/gudpy/gui/widgets/slots/normalisation_slots.py index 235ff8ebf..494726a2e 100644 --- a/gudpy/gui/widgets/slots/normalisation_slots.py +++ b/gudpy/gui/widgets/slots/normalisation_slots.py @@ -20,10 +20,10 @@ def setNormalisation(self, normalisation): self.widgetsRefreshing = True self.widget.dataFilesList.makeModel( - self.normalisation.dataFiles.dataFiles + self.normalisation.dataFiles.dataFilenames ) self.widget.backgroundDataFilesList.makeModel( - self.normalisation.dataFilesBg.dataFiles + self.normalisation.dataFilesBg.dataFilenames ) self.widget.dataFilesList.setSelectionMode( @@ -645,7 +645,7 @@ def handleDataFilesAltered(self): if not self.widgetsRefreshing: self.parent.setModified() self.parent.gudrunFile.purged = False - self.normalisation.dataFiles.dataFiles = ( + self.normalisation.dataFiles.setFiles( self.widget.dataFilesList.model().stringList() ) @@ -659,7 +659,7 @@ def handleDataFilesBgAltered(self): if not self.widgetsRefreshing: self.parent.setModified() self.parent.gudrunFile.purged = False - self.normalisation.dataFilesBg.dataFiles = ( + self.normalisation.dataFilesBg.setFiles( self.widget.backgroundDataFilesList.model().stringList() ) diff --git a/gudpy/gui/widgets/slots/sample_background_slots.py b/gudpy/gui/widgets/slots/sample_background_slots.py index 3049e29e4..ad7ece9b0 100644 --- a/gudpy/gui/widgets/slots/sample_background_slots.py +++ b/gudpy/gui/widgets/slots/sample_background_slots.py @@ -21,7 +21,7 @@ def setSampleBackground(self, sampleBackground): # Populate data files list. self.widget.sampleBackgroundDataFilesList.makeModel( - self.sampleBackground.dataFiles.dataFiles + self.sampleBackground.dataFiles.dataFilenames ) self.widget.sampleBackgroundDataFilesList.model().dataChanged.connect( @@ -63,7 +63,7 @@ def handleDataFilesAltered(self): if not self.widgetsRefreshing: self.parent.setModified() self.parent.gudrunFile.purged = False - self.sampleBackground.dataFiles.dataFiles = ( + self.sampleBackground.dataFiles.setFiles( self.widget.sampleBackgroundDataFilesList.model().stringList() ) diff --git a/gudpy/gui/widgets/slots/sample_slots.py b/gudpy/gui/widgets/slots/sample_slots.py index e807cbd76..02277b18e 100644 --- a/gudpy/gui/widgets/slots/sample_slots.py +++ b/gudpy/gui/widgets/slots/sample_slots.py @@ -27,7 +27,7 @@ def setSample(self, sample): # Populate the data files list. self.widget.sampleDataFilesList.makeModel( - self.sample.dataFiles.dataFiles + self.sample.dataFiles.dataFilenames ) self.widget.sampleDataFilesList.model().dataChanged.connect( @@ -889,7 +889,7 @@ def handleDataFilesAltered(self): if not self.widgetsRefreshing: self.parent.setModified() self.parent.gudrunFile.purged = False - self.sample.dataFiles.dataFiles = ( + self.sample.dataFiles.setFiles( self.widget.sampleDataFilesList.model().stringList() ) @@ -1149,7 +1149,7 @@ def updateExpectedDCSLevel(self, _=None, __=None): actualDcsLevel = nthfloat(self.widget.dcsLabel.text(), 2) try: error = round( - ((actualDcsLevel - dcsLevel) / actualDcsLevel)*100, 1 + ((actualDcsLevel - dcsLevel) / actualDcsLevel)*100, 1 ) except ZeroDivisionError: error = 100. From 4dd11fd270a0a306c61ab3706a7e226baca008bf Mon Sep 17 00:00:00 2001 From: Noella Spitz Date: Tue, 14 May 2024 11:45:22 +0100 Subject: [PATCH 39/78] Create separate parser classes --- gudpy/core/gudpy.py | 3 +- gudpy/core/gudrun_file.py | 169 +++++----------- gudpy/core/io.py | 402 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 448 insertions(+), 126 deletions(-) create mode 100644 gudpy/core/io.py diff --git a/gudpy/core/gudpy.py b/gudpy/core/gudpy.py index 5e6474ef2..2193cbe71 100644 --- a/gudpy/core/gudpy.py +++ b/gudpy/core/gudpy.py @@ -133,7 +133,7 @@ def setSaveLocation(self, projectDir: str): Path to the desired save location of project """ self.projectDir = projectDir - self.gudrunFile.filename = f"{os.path.basename(projectDir)}.yaml" + self.gudrunFile.filename = f"{os.path.basename(projectDir)}" self.gudrunFile.projectDir = projectDir self.originalGudrunFile.projectDir = projectDir self.autosaveLocation = ( @@ -517,6 +517,7 @@ def gudrun( tmp, gudrunFile.OUTPATH ) + assert (gudrunFile.path() is not None) gudrunFile.write_out(path) with subprocess.Popen( [self.BINARY_PATH, path], cwd=tmp, diff --git a/gudpy/core/gudrun_file.py b/gudpy/core/gudrun_file.py index de60d15fd..3cbc6eab1 100644 --- a/gudpy/core/gudrun_file.py +++ b/gudpy/core/gudrun_file.py @@ -2,6 +2,11 @@ import time import re from copy import deepcopy +from abc import abstractmethod +from enum import Enum +from ruamel.yaml import YAML as yaml +from ruamel.yaml import YAMLError +import os from core.utils import ( extract_nums_from_string, @@ -28,6 +33,7 @@ MergeWeights, Scales, NormalisationType, OutputUnits, Geometry ) +from core.gui_config import GUIConfig from core import utils from core import config as cfg from core.gudpy_yaml import YAML @@ -122,7 +128,6 @@ def __init__( self, projectDir=None, loadFile=None, - format=Format.YAML, config=False ): """ @@ -142,15 +147,11 @@ def __init__( If a new input file should be constructed from a config """ - self.yaml = YAML() - self.format = format - + self.yaml = self.getYamlModule() # Construct the outpath of generated input file - self.OUTPATH = "gudpy.txt" self.gudrunOutput = None self.projectDir = projectDir self.loadFile = loadFile - self.filename = None self.stream = None self.instrument = Instrument() @@ -182,22 +183,53 @@ def __deepcopy__(self, memo): setattr(result, k, deepcopy(v, memo)) return result + def setGudrunDir(self, dir): + self.instrument.GudrunInputFileDir = dir + + def convertToSample(self, container, persist=False): + + sample = container.convertToSample() + + if persist: + for i, sampleBackground in enumerate(self.sampleBackgrounds): + for sample in sampleBackground.samples: + if container in sample.containers: + sample.containers.remove(container) + break + self.sampleBackgrounds[i].append(sample) + return sample + + def determineError(self, sample): + gudPath = sample.dataFiles[0].replace( + self.instrument.dataFileType, + "gud" + ) + gudFile = GudFile( + os.path.join( + self.instrument.GudrunInputFileDir, gudPath + ) + ) + error = round( + ( + 1.0 - (gudFile.averageLevelMergedDCS / gudFile.expectedDCS) + ) * 100, 1 + ) + return error + def runSamples(self): return [ s for sb in self.sampleBackgrounds for s in sb.samples if s.runThisSample] - def path(self): - if not self.projectDir: - return None - else: - return os.path.join(self.projectDir, self.filename) - def checkNormDataFiles(self): return (len(self.normalisation.dataFiles) and len(self.normalisation.dataFilesBg)) + # ================================================================= + # TEXTFILE PARSING + # ================================================================= + def getNextToken(self): """ Pops the 'next token' from the stream and returns it. @@ -1474,119 +1506,6 @@ def __str__(self): + components ) - def save(self, path='', format=None): - if not path: - path = self.path() - - if not format: - format = self.format - if format == Format.TXT: - self.write_out( - path=f"{os.path.splitext(path)[0]}.txt", overwrite=True) - elif format == Format.YAML: - if self.path(): - filepath = os.path.join( - self.path(), f"{os.path.splitext(path)[0]}.yaml") - self.write_yaml(filepath) - - def write_yaml(self, path): - self.yaml = YAML() - self.yaml.writeYAML(self, path) - - def write_out(self, path='', overwrite=False, writeParameters=True): - """ - Writes out the string representation of the GudrunFile. - If 'overwrite' is True, then the initial file is overwritten. - Otherwise, it is written to 'gudpy_{initial filename}.txt'. - - Parameters - ---------- - overwrite : bool, optional - Overwrite the initial file? (default is False). - path : str, optional - Path to write to. - Returns - ------- - None - """ - if path: - if not overwrite: - assert (not os.path.exists(path)) - f = open( - path, "w", encoding="utf-8" - ) - elif not overwrite: - assert (not os.path.exists(os.path.join( - self.instrument.GudrunInputFileDir, - self.OUTPATH) - )) - f = open( - os.path.join( - self.instrument.GudrunInputFileDir, - self.OUTPATH - ), "w", encoding="utf-8") - else: - if not self.path(): - path = os.path.join( - self.instrument.GudrunInputFileDir, - self.OUTPATH) - f = open(path, "w", encoding="utf-8") - - if os.path.basename(f.name) == self.OUTPATH: - for sampleBackground in self.sampleBackgrounds: - sampleBackground.writeAllSamples = False - f.write(str(self)) - f.close() - - if writeParameters: - for sb in self.sampleBackgrounds: - for s in sb.samples: - if s.runThisSample: - gf = deepcopy(self) - gf.sampleBackgrounds = [deepcopy(sb)] - gf.sampleBackgrounds[0].samples = [deepcopy(s)] - gf.write_out( - path=os.path.join( - self.instrument.GudrunInputFileDir, - s.pathName(), - ), - overwrite=True, - writeParameters=False - ) - - def setGudrunDir(self, dir): - self.instrument.GudrunInputFileDir = dir - - def convertToSample(self, container, persist=False): - - sample = container.convertToSample() - - if persist: - for i, sampleBackground in enumerate(self.sampleBackgrounds): - for sample in sampleBackground.samples: - if container in sample.containers: - sample.containers.remove(container) - break - self.sampleBackgrounds[i].append(sample) - return sample - - def determineError(self, sample): - gudPath = sample.dataFiles[0].replace( - self.instrument.dataFileType, - "gud" - ) - gudFile = GudFile( - os.path.join( - self.instrument.GudrunInputFileDir, gudPath - ) - ) - error = round( - ( - 1.0 - (gudFile.averageLevelMergedDCS / gudFile.expectedDCS) - ) * 100, 1 - ) - return error - Container.getNextToken = GudrunFile.getNextToken Container.peekNextToken = GudrunFile.peekNextToken diff --git a/gudpy/core/io.py b/gudpy/core/io.py new file mode 100644 index 000000000..55acf8e4d --- /dev/null +++ b/gudpy/core/io.py @@ -0,0 +1,402 @@ +from abc import abstractmethod +from enum import Enum +from ruamel.yaml import YAML as yaml +from ruamel.yaml import YAMLError +import os +import typing as typ + +from core.composition import ( + Component, Components, Composition, WeightedComponent +) +from core.data_files import DataFile, DataFiles +from core.element import Element +from core.exception import YAMLException +from core.gui_config import GUIConfig +from core import utils +from core.instrument import Instrument +from core.beam import Beam +from core.normalisation import Normalisation +from core.sample_background import SampleBackground +from core.sample import Sample +from core.container import Container +from core.gudrun_file import GudrunFile +from core import config +import core.enums as enums + + +class YamlParser: + def __init__(self): + self.yaml = self.getYamlModule() + + @property + def path(self, format: enums.Format): + if not self.projectDir or self.projectDir: + raise RuntimeError("Save location not set.") + + if format == enums.Format.TXT: + return os.path.join(self.projectDir, f"{self.projectName}.txt") + elif format == enums.Format.YAML: + return os.path.join(self.projectDir, f"{self.projectName}.yaml") + else: + raise RuntimeError("Unsupported format.") + + def save(self, path: str = '', format: enums.Format = None): + if not path: + path = self.path() + + if not format: + format = self.format + if format == enums.Format.TXT: + self.write_out( + path=self.txtPath(), overwrite=True) + elif format == enums.Format.YAML: + if self.path(): + self.write_yaml(self.yamlPath()) + + @property + def projectName(self): + os.path.dirname(self.projectDir) + + def write_out( + self, + path: str = '', + overwrite: bool = False, + writeParameters: bool = True + ): + """ + Writes out the string representation of the GudrunFile. + If 'overwrite' is True, then the initial file is overwritten. + Otherwise, it is written to 'gudpy_{initial filename}.txt'. + + Parameters + ---------- + overwrite : bool, optional + Overwrite the initial file? (default is False). + path : str, optional + Path to write to. + Returns + ------- + None + """ + if path: + if not overwrite: + assert (not os.path.exists(path)) + f = open( + path, "w", encoding="utf-8" + ) + elif not overwrite: + assert (not os.path.exists(os.path.join( + self.instrument.GudrunInputFileDir, + self.OUTPATH) + )) + f = open( + os.path.join( + self.instrument.GudrunInputFileDir, + self.OUTPATH + ), "w", encoding="utf-8") + else: + if not self.path(): + path = os.path.join( + self.instrument.GudrunInputFileDir, + self.OUTPATH) + f = open(path, "w", encoding="utf-8") + + if os.path.basename(f.name) == self.OUTPATH: + for sampleBackground in self.sampleBackgrounds: + sampleBackground.writeAllSamples = False + f.write(str(self)) + f.close() + + if writeParameters: + for sb in self.sampleBackgrounds: + for s in sb.samples: + if s.runThisSample: + gf = deepcopy(self) + gf.sampleBackgrounds = [deepcopy(sb)] + gf.sampleBackgrounds[0].samples = [deepcopy(s)] + gf.write_out( + path=os.path.join( + self.instrument.GudrunInputFileDir, + s.pathName(), + ), + overwrite=True, + writeParameters=False + ) + +# ================================================================= +# YAML PARSING +# ================================================================= + + def getYamlModule(self) -> yaml: + yaml_ = yaml() + yaml_.preserve_quotes = True + yaml_.default_flow_style = None + yaml_.encoding = 'utf-8' + return yaml_ + + def parseYaml(self, path) -> GudrunFile: + yamldict = self.yamlToDict(path) + gudrunFile = GudrunFile() + for k, v in yamldict.items(): + if k == "GUI": + GUI = GUIConfig() + self.yamlToObject(GUI, v) + config.GUIConfig = GUI + continue + if not hasattr(gudrunFile, k): + # If attribute is not valid + print(f"Ignoring depreciated attribute '{k}'") + continue + + objType = type(gudrunFile.__dict__[k]) + parsingFunc = self.parsingFuncMap(objType) + if parsingFunc: + try: + obj = parsingFunc(v) + setattr(gudrunFile, k, obj) + except YAMLException as e: + raise YAMLException(e) + except YAMLError as e: + # Exception caused by yaml parsing library + raise YAMLException(f"Invalid YAML file: {str(e)}") + return gudrunFile + + def writeYAML(self, gudrunFile: GudrunFile, path: str) -> None: + with open(path, "wb") as fp: + outyaml = { + "Instrument": gudrunFile.instrument, + "Beam": gudrunFile.beam, + "Components": gudrunFile.components.components, + "Normalisation": gudrunFile.normalisation, + "SampleBackgrounds": gudrunFile.sampleBackgrounds, + "GUI": config.GUI + } + self.yaml.dump( + {k: self.toYaml(v) for k, v in outyaml.items()}, + fp + ) + + def parsingFuncMap(self, type: type) -> typ.Callable | None: + parsingFuncs = { + Instrument: self.parseYamlInstrument, + Beam: self.parseYamlBeam, + Normalisation: self.parseYamlNormalisation, + SampleBackground: self.parseYamlSampleBackground, + Sample: self.parseYamlSample, + Composition: self.parseYamlComposition, + Component: self.parseYamlComponent, + Components: self.parseYamlComponents, + list[Element]: self.parseYamlElements, + DataFiles: self.parseYamlDataFiles + } + if type in parsingFuncs.keys(): + return parsingFuncs[type] + else: + return None + + def yamlToDict(self, path) -> any: + # Read the input stream into our attribute. + with open(path, encoding=self.yaml.encoding) as fp: + return self.yaml.load(fp) + + def yamlToObject(self, obj: any, yamldict: any) -> any: + for k, v in yamldict.items(): + self.assignYamlToAttr(obj, k, v) + return obj + + def assignYamlToAttr(self, obj: any, key, val) -> None: + if not hasattr(obj, key): + # If attribute is not valid + print( + f"Ignoring depreciated attribute '{key}'" + f"given to '{type(obj).__name__}'" + ) + return + setattr(obj, key, type(obj.__dict__[key])(self.toBuiltin(val))) + + def parseYamlInstrument(self, yamldict: any) -> Instrument: + instrument = Instrument() + instrument = self.yamlToObject(instrument, yamldict) + return instrument + + def parseYamlBeam(self, yamldict: any) -> Normalisation: + beam = Beam() + beam = self.yamlToObject(beam, yamldict) + return beam + + def parseYamlNormalisation(self, yamldict: any) -> Normalisation: + normalisation = Normalisation() + for k, v in yamldict.items(): + if k == "composition": + composition = self.parseYamlComposition(yamldict[k]) + setattr(normalisation, k, composition) + else: + self.assignYamlToAttr(normalisation, k, v) + return normalisation + + def parseYamlSample(self, yamldict: any) -> Sample: + sample = Sample() + for k, v in yamldict.items(): + if k == "composition": + composition = self.parseYamlComposition() + setattr(sample, k, composition) + elif k == "containers": + for contyaml in yamldict[k]: + container = self.parseYamlContainer(contyaml) + sample.containers.append(container) + else: + self.assignYamlToAttr(sample, k, v) + return sample + + def parseYamlSampleBackground(self, yamldict: any) -> SampleBackground: + sampleBg = SampleBackground() + for k, v in yamldict.items(): + if k == "samples": + for sampleyaml in yamldict[k]: + sample = self.parseYamlSample(sampleyaml) + sample.name = utils.replace_unwanted_chars(sample.name) + sampleBg.samples.append(sample) + else: + self.assignYamlToAttr(sampleBg, k, v) + + def parseYamlComposition(self, yamldict: any) -> Composition: + composition = Composition() + for k, v in yamldict.items(): + if k == "elements": + elements = self.parseYamlElements(yamldict[k]) + setattr(composition, k, elements) + elif k == "weightedComponents": + weightedComponents = [] + for wc in yamldict[k]: + component = self.parseYamlComponent(wc) + ratio = wc["ratio"] + try: + weightedComponents.append( + WeightedComponent( + component, float(ratio)) + ) + except ValueError: + raise YAMLException( + "Invalid ratio given to Weighted Component") + setattr(composition, k, weightedComponents) + else: + self.assignYamlToAttr(composition, k, v) + return composition + + def parseYamlContainer(self, yamldict: any) -> Container: + container = Container() + for k, v in yamldict.items(): + if k == "composition": + composition = self.parseYamlComposition() + setattr(container, k, composition) + else: + self.assignYamlToAttr(container, k, v) + return container + + def parseYamlDataFiles(self, yamldict: any) -> DataFiles: + dataFiles = DataFiles() + for df in yamldict["_dataFiles"]: + dataFile = DataFile(df["filename"]) + dataFile = self.yamlToObject(dataFile, df) + dataFiles.dataFiles.append(dataFile) + return dataFiles + + def parseYamlComponent(self, yamldict: any) -> Component: + if ( + "component" not in yamldict + or "ratio" not in yamldict + ): + raise YAMLException( + "Weighted Component expects 'component' and" + + " 'ratio' to be provided") + component = Component() + self.yamlToObject( + component, yamldict["component"] + ) + return component + + def parseYamlComponents(self, yamllist: list[dict]) -> Components: + components = [] + for c in yamllist: + components.append(self.parseYamlComponent(c)) + return components + + def parseYamlElements(self, yamllist: list[str]) -> list[Element]: + elements = [] + for idx, element in enumerate(yamllist): + # Ensuring correct arguements are provided + if ( + "atomicSymbol" not in element + or "massNo" not in element + or "abundance" not in element + ): + raise YAMLException( + "Insufficient arguments given to element" + + f" {idx + 1}. Expects 'atomicSymbol', 'massNo'" + + " and 'abundance'" + ) + + # Setting element properties + try: + element_ = Element( + **{ + "atomicSymbol": element["atomicSymbol"], + "massNo": float(element["massNo"]), + "abundance": float(element["abundance"]) + } + ) + elements.append(element_) + except ValueError as e: + raise YAMLException( + f"Invalid number given to element {idx + 1}:" + f"{e}") + return elements + + @abstractmethod + def toYaml(self, var: any): + if var.__class__.__module__ == "ruamel.yaml.scalarfloat": + return float(var) + if var.__class__.__module__ == "builtins": + if isinstance(var, (list, tuple)): + return type(var)([self.toYaml(v) for v in var]) + else: + return var + elif isinstance(var, Enum): + return type(var)(var.value).name + elif isinstance(var, ( + Instrument, Beam, Components, Normalisation, + SampleBackground, Sample, Container, WeightedComponent, + Component, Composition, Element, DataFiles, DataFile, GUIConfig + )): + return { + k: self.toYaml(v) + for k, v in var.__dict__.items() + if k not in var.yamlignore + } + + @abstractmethod + def toBuiltin(self, yamlvar: any): + if yamlvar is None: + return None + if isinstance(yamlvar, (list, tuple)): + return [self.toBuiltin(v) for v in yamlvar] + elif (yamlvar.__class__.__module__ == + "ruamel.yaml.comments.CommentedMap"): + dict = {} + for k, v in yamlvar.items(): + dict[k] = v + return dict + elif yamlvar.__class__.__module__ == "builtins": + return yamlvar + elif yamlvar.__class__.__module__ == "ruamel.yaml.scalarfloat": + return float(yamlvar) + elif yamlvar.__class__.__module__ == "ruamel.yaml.scalarstring": + return str(yamlvar) + +# ================================================================= +# TXT PARSING +# ================================================================= + + +class TxtParser: + def __init__(self): From 02026f113333e918c8dd524fe7df425dffaf6bc8 Mon Sep 17 00:00:00 2001 From: Noella Spitz Date: Tue, 14 May 2024 16:39:49 +0100 Subject: [PATCH 40/78] Create dedicated iO handling --- gudpy/core/gudpy.py | 55 +- gudpy/core/gudrun_file.py | 1303 +------------------------- gudpy/core/io/__init__.py | 0 gudpy/core/{io.py => io/gudpy_io.py} | 292 +++--- gudpy/core/io/txt_parser.py | 1181 +++++++++++++++++++++++ gudpy/core/purge_file.py | 1 - 6 files changed, 1367 insertions(+), 1465 deletions(-) create mode 100644 gudpy/core/io/__init__.py rename gudpy/core/{io.py => io/gudpy_io.py} (56%) create mode 100644 gudpy/core/io/txt_parser.py diff --git a/gudpy/core/gudpy.py b/gudpy/core/gudpy.py index 2193cbe71..957ac7981 100644 --- a/gudpy/core/gudpy.py +++ b/gudpy/core/gudpy.py @@ -16,6 +16,7 @@ import core.output_file_handler as handlers from core.file_library import GudPyFileLibrary from core import data_files +from core.io import txt_parser, gudpy_io SUFFIX = ".exe" if os.name == "nt" else "" @@ -25,8 +26,8 @@ def __init__( self ): self.originalGudrunFile: GudrunFile = None - self.gudrunFile: GudrunFile = None - self.purgeFile = None + self._gudrunFile: GudrunFile = None + self.io = gudpy_io.GudpyIO() self.purge: Purge = None self.gudrun: Gudrun = None @@ -39,10 +40,17 @@ def __init__( self.projectDir = "" self.autosaveLocation = "" - def loadFromFile( + @property + def gudrunFile(self): + return self._gudrunFile + + @gudrunFile.setter + def gudrunFile(self, gudrunFile: GudrunFile): + self._gudrunFile = gudrunFile + + def loadFromGudrunFile( self, loadFile: str, - format: enums.Format, config: bool = False ): """Loads GudPy from an input file @@ -64,18 +72,12 @@ def loadFromFile( if not os.path.exists(loadFile): raise FileNotFoundError("Input file does not exist.") - self.gudrunFile = GudrunFile( - loadFile=loadFile, - format=format, - config=config - ) + self.gudrunFileParser = txt_parser.GudrunFileParser() + self.gudrunFile = self.gudrunFileParser.parse(loadFile, config) self.originalGudrunFile = copy.deepcopy(self.gudrunFile) self.originalGudrunFile.filename = "original" - self.projectDir == "" - self.autosaveLocation = "" - def loadFromProject(self, projectDir: str): """Loads GudPy from a project directory @@ -90,29 +92,10 @@ def loadFromProject(self, projectDir: str): Raised if there is no YAML input file in the project directory """ - loadFile = "" - - if os.path.exists(os.path.join( - projectDir, - f"{os.path.basename(projectDir)}.yaml" - )): - # If default file exists - loadFile = os.path.join( - projectDir, - f"{os.path.basename(projectDir)}.yaml" - ) - else: - # Try to find yaml files - for f in os.listdir(projectDir): - if os.path.splitext(f)[1] == ".yaml": - # If file is yaml - loadFile = os.path.join(projectDir, f) - if not loadFile: - raise FileNotFoundError( - "Could not find GudPy input file within the project") + if not os.path.exists(projectDir): + raise FileNotFoundError("Directory does not exist.") - self.loadFromFile(loadFile=loadFile, format=enums.Format.YAML) - self.setSaveLocation(projectDir) + self.gudrunFile = self.io.importProject(projectDir) def checkSaveLocation(self): """Checks if user has set the save location @@ -231,8 +214,8 @@ def runPurge(self): """ self.prepareRun() self.purge = Purge() - self.purgeFile = PurgeFile(self.gudrunFile) - exitcode = self.purge.purge(self.purgeFile) + purgeFile = PurgeFile(self.gudrunFile) + exitcode = self.purge.purge(purgeFile) self.gudrunFile.save() if exitcode: raise exc.PurgeException( diff --git a/gudpy/core/gudrun_file.py b/gudpy/core/gudrun_file.py index 3cbc6eab1..963e540f3 100644 --- a/gudpy/core/gudrun_file.py +++ b/gudpy/core/gudrun_file.py @@ -1,43 +1,13 @@ import os import time -import re from copy import deepcopy -from abc import abstractmethod -from enum import Enum -from ruamel.yaml import YAML as yaml -from ruamel.yaml import YAMLError -import os -from core.utils import ( - extract_nums_from_string, - firstword, boolifyNum, - extract_ints_from_string, - extract_floats_from_string, - firstNFloats, - firstNInts, - nthfloat, - nthint, - uniquifyName -) from core.instrument import Instrument from core.beam import Beam from core.normalisation import Normalisation -from core.sample import Sample from core.sample_background import SampleBackground -from core.container import Container -from core.composition import Component, Components, Composition -from core.element import Element -from core.data_files import DataFiles -from core.enums import ( - CrossSectionSource, Format, Instruments, FTModes, UnitsOfDensity, - MergeWeights, Scales, NormalisationType, OutputUnits, - Geometry -) -from core.gui_config import GUIConfig -from core import utils +from core.composition import Components from core import config as cfg -from core.gudpy_yaml import YAML -from core.exception import ParserException, YAMLException from core.gud_file import GudFile SUFFIX = ".exe" if os.name == "nt" else "" @@ -128,7 +98,19 @@ def __init__( self, projectDir=None, loadFile=None, - config=False + config=False, + params: tuple[ + Instrument, + Normalisation, + list[SampleBackground], + Components + ] = ( + Instrument(), + Beam(), + Normalisation(), + [], + Components() + ) ): """ Constructs all the necessary attributes for the GudrunFile object. @@ -146,33 +128,13 @@ def __init__( config : bool If a new input file should be constructed from a config """ + inst, norm, beam, sbg, comp = params - self.yaml = self.getYamlModule() - # Construct the outpath of generated input file - self.gudrunOutput = None - self.projectDir = projectDir - self.loadFile = loadFile - self.stream = None - - self.instrument = Instrument() - self.normalisation = Normalisation() - self.beam = Beam() - self.sampleBackgrounds = [] - self.components = Components() - - if not projectDir and not loadFile: - raise RuntimeError( - "GudrunFile needs to be initialised with either" - " a project directory or load file specified" - ) - - if loadFile: - self.setGudrunDir(os.path.dirname(loadFile)) - - if not config: - self.setGudrunDir(os.path.dirname(loadFile)) - - self.parse(loadFile, config=config) + self.instrument = inst + self.normalisation = norm + self.beam = beam + self.sampleBackgrounds = sbg + self.components = comp def __deepcopy__(self, memo): result = self.__class__.__new__(self.__class__) @@ -226,1225 +188,6 @@ def checkNormDataFiles(self): return (len(self.normalisation.dataFiles) and len(self.normalisation.dataFilesBg)) - # ================================================================= - # TEXTFILE PARSING - # ================================================================= - - def getNextToken(self): - """ - Pops the 'next token' from the stream and returns it. - Essentially removes the first line in the stream and returns it. - - Parameters - ---------- - None - Returns - ------- - str | None - """ - return self.stream.pop(0) if self.stream else None - - def peekNextToken(self): - """ - Returns the next token in the input stream, without removing it. - - Parameters - ---------- - None - Returns - ------- - str | None - """ - return self.stream[0] if self.stream else None - - def consumeTokens(self, n): - """ - Consume n tokens from the input stream. - - Parameters - ---------- - None - Returns - ------- - None - """ - for _ in range(n): - self.getNextToken() - - def consumeUpToDelim(self, delim): - """ - Consume tokens iteratively, until a delimiter is reached. - - Parameters - ---------- - None - Returns - ------- - None - """ - line = self.getNextToken() - while line[0] != delim: - line = self.getNextToken() - - def consumeWhitespace(self): - """ - Consume tokens iteratively, while they are whitespace. - - Parameters - ---------- - None - Returns - ------- - None - """ - line = self.peekNextToken() - if line and line.isspace(): - self.getNextToken() - line = self.peekNextToken() - - def parseInstrument(self): - """ - Intialises an Instrument object and assigns it to the - instrument attribute. - Parses the attributes of the Instrument from the input stream. - Raises a ParserException if any mandatory attributes are missing. - - - Parameters - ---------- - None - Returns - ------- - None - """ - try: - self.consumeWhitespace() - - # For string attributes, - # we simply extract the firstword in the line. - self.instrument.name = Instruments[firstword(self.getNextToken())] - self.consumeTokens(1) - self.instrument.dataFileDir = os.path.abspath( - firstword(self.getNextToken())) + os.path.sep - self.instrument.dataFileType = firstword(self.getNextToken()) - self.instrument.detectorCalibrationFileName = ( - firstword(self.getNextToken()) - ) - - # For single integer attributes, - # we extract the zeroth int from the line. - self.instrument.columnNoPhiVals = nthint(self.getNextToken(), 0) - self.instrument.groupFileName = firstword(self.getNextToken()) - self.instrument.deadtimeConstantsFileName = ( - firstword(self.getNextToken()) - ) - - # For N integer attributes, - # we extract the first N integers from the line. - self.instrument.spectrumNumbersForIncidentBeamMonitor = ( - extract_ints_from_string(self.getNextToken()) - ) - - # For integer pair attributes, - # we extract the first 2 integers from the line. - self.instrument.wavelengthRangeForMonitorNormalisation = ( - firstNFloats(self.getNextToken(), 2) - ) - - if all( - self.instrument.wavelengthRangeForMonitorNormalisation - ) == 0.0: - self.instrument.wavelengthRangeForMonitorNormalisation = [ - 0, 0 - ] - - self.instrument.spectrumNumbersForTransmissionMonitor = ( - extract_ints_from_string(self.getNextToken()) - ) - - # For single float attributes, - # we extract the zeroth float from the line. - self.instrument.incidentMonitorQuietCountConst = ( - nthfloat(self.getNextToken(), 0) - ) - self.instrument.transmissionMonitorQuietCountConst = ( - nthfloat(self.getNextToken(), 0) - ) - - self.instrument.channelNosSpikeAnalysis = ( - firstNInts(self.getNextToken(), 2) - ) - self.instrument.spikeAnalysisAcceptanceFactor = ( - nthfloat(self.getNextToken(), 0) - ) - - # Extract wavelength range - # Which consists of the first 3 floats - # (min, max, step) in the line. - wavelengthRange = firstNFloats(self.getNextToken(), 3) - self.instrument.wavelengthMin = wavelengthRange[0] - self.instrument.wavelengthMax = wavelengthRange[1] - self.instrument.wavelengthStep = wavelengthRange[2] - - self.instrument.NoSmoothsOnMonitor = nthint(self.getNextToken(), 0) - - # Extract X range - # Which consists of the first 3 floats - # (min, max, step) in the line. - XRange = firstNFloats(self.getNextToken(), 3) - - self.instrument.XMin = XRange[0] - self.instrument.XMax = XRange[1] - self.instrument.XStep = XRange[2] - - # Extract the grouping parameter panel. - # Each row in the panel consists of the first 4 ints - # (Group, XMin, XMax, Background Factor) in the line. - # If the marker line is encountered, - # then the panel has been parsed. - - line = self.getNextToken() - while "to end input of specified values" not in line: - group = nthint(line, 0) - xMin = nthfloat(line, 1) - xMax = nthfloat(line, 2) - backgroundFactor = nthfloat(line, 3) - self.instrument.groupingParameterPanel.append( - [group, xMin, xMax, backgroundFactor] - ) - line = self.getNextToken() - - self.instrument.groupsAcceptanceFactor = ( - nthfloat(self.getNextToken(), 0) - ) - self.instrument.mergePower = nthint(self.getNextToken(), 0) - - # For boolean attributes, we convert the first - # integer in the line to its boolean value. - self.instrument.subSingleAtomScattering = ( - boolifyNum(nthint(self.getNextToken(), 0)) - ) - - # For enumerated attributes, where the value of the attribute is - # the first integer in the line, and we must get the member, - # we do this: Enum[Enum(value).name] - self.instrument.mergeWeights = ( - MergeWeights[MergeWeights(nthint(self.getNextToken(), 0)).name] - ) - self.instrument.incidentFlightPath = ( - nthfloat(self.getNextToken(), 0) - ) - self.instrument.spectrumNumberForOutputDiagnosticFiles = ( - nthint(self.getNextToken(), 0) - ) - - self.instrument.neutronScatteringParametersFile = ( - firstword(self.getNextToken()) - - ) - self.instrument.scaleSelection = ( - Scales[Scales(nthint(self.getNextToken(), 0)).name] - ) - self.instrument.subWavelengthBinnedData = ( - boolifyNum(nthint(self.getNextToken(), 0)) - ) - self.consumeTokens(2) - self.instrument.logarithmicStepSize = ( - nthfloat(self.getNextToken(), 0) - ) - self.instrument.hardGroupEdges = ( - boolifyNum(nthint(self.getNextToken(), 0)) - ) - - # If NeXus files are being used, then we expect a NeXus definition - # file to be present, and extract it. - if ( - self.instrument.dataFileType == "NXS" - or self.instrument.dataFileType == "nxs" - ): - self.instrument.nxsDefinitionFile = ( - firstword(self.getNextToken()) - ) - - if self.config: - self.instrument.goodDetectorThreshold = nthint( - self.getNextToken(), 0 - ) - - # Consume whitespace and the closing brace. - self.consumeUpToDelim("}") - - # Resolve the paths, to make them relative. - # First construct the regular expression to match against. - pattern = re.compile(r"StartupFiles\S*") - - match = re.search( - pattern, - self.instrument.detectorCalibrationFileName - ) - - if match: - self.instrument.detectorCalibrationFileName = match.group() - - match = re.search( - pattern, - self.instrument.groupFileName - ) - - if match: - self.instrument.groupFileName = match.group() - - match = re.search( - pattern, - self.instrument.deadtimeConstantsFileName - ) - - if match: - self.instrument.deadtimeConstantsFileName = match.group() - - match = re.search( - pattern, - self.instrument.neutronScatteringParametersFile - ) - - if match: - self.instrument.neutronScatteringParametersFile = match.group() - - match = re.search( - pattern, - self.instrument.neutronScatteringParametersFile - ) - - if match: - self.instrument.neutronScatteringParametersFile = match.group() - - except Exception as e: - raise ParserException( - "Whilst parsing Instrument, an exception occured." - " The input file is most likely of an incorrect format, " - "and some attributes were missing." - f"{str(e)}" - ) from e - - def parseBeam(self): - """ - Intialises a Beam object and assigns it to the - beam attribute. - Parses the attributes of the Beam from the input stream. - Raises a ParserException if any mandatory attributes are missing. - - - Parameters - ---------- - None - Returns - ------- - None - """ - - try: - # Initialise beam attribute to a new instance of Beam. - self.beam = Beam() - - self.consumeWhitespace() - - # For enumerated attributes, - # where the member name of the attribute is - # the first 'word' in the line, and we must get the member, - # we do this: Enum[memberName]. - self.beam.sampleGeometry = Geometry[firstword(self.getNextToken())] - - # Set the global geometry. - cfg.geometry = self.beam.sampleGeometry - - # Ignore the number of beam values. - self.consumeTokens(1) - - # For N float attributes, - # we extract the first N floats from the line. - self.beam.beamProfileValues = ( - extract_floats_from_string(self.getNextToken()) - ) - - # For single float attributes, - # we extract the zeroth float from the line. - range = self.getNextToken() - self.beam.stepSizeAbsorption = nthfloat(range, 0) - self.beam.stepSizeMS = nthfloat(range, 1) - self.beam.noSlices = nthint(range, 2) - self.beam.angularStepForCorrections = ( - nthint(self.getNextToken(), 0) - ) - - # Extract the incident beam edges - # relative to the centroid of the sample. - incidentBeamEdges = self.getNextToken() - self.beam.incidentBeamLeftEdge = nthfloat(incidentBeamEdges, 0) - self.beam.incidentBeamRightEdge = nthfloat(incidentBeamEdges, 1) - self.beam.incidentBeamBottomEdge = nthfloat(incidentBeamEdges, 2) - self.beam.incidentBeamTopEdge = nthfloat(incidentBeamEdges, 3) - - # Extract the scattered beam edges - # relative to the centroid of the sample. - scatteredBeamEdges = self.getNextToken() - self.beam.scatteredBeamLeftEdge = nthfloat(scatteredBeamEdges, 0) - self.beam.scatteredBeamRightEdge = nthfloat(scatteredBeamEdges, 1) - self.beam.scatteredBeamBottomEdge = nthfloat(scatteredBeamEdges, 2) - self.beam.scatteredBeamTopEdge = nthfloat(scatteredBeamEdges, 3) - - # For string attributes, - # we simply extract the firstword in the line. - self.beam.filenameIncidentBeamSpectrumParams = ( - firstword(self.getNextToken()) - ) - - # Now match it against a pattern, - # to resolve the path to be relative. - pattern = re.compile(r"StartupFiles\S*") - - match = re.search( - pattern, - self.beam.filenameIncidentBeamSpectrumParams - ) - - if match: - self.beam.filenameIncidentBeamSpectrumParams = match.group() - - self.beam.overallBackgroundFactor = ( - nthfloat(self.getNextToken(), 0) - ) - self.beam.sampleDependantBackgroundFactor = ( - nthfloat(self.getNextToken(), 0) - ) - self.beam.shieldingAttenuationCoefficient = ( - nthfloat(self.getNextToken(), 0) - ) - - # Consume whitespace and the closing brace. - self.consumeUpToDelim("}") - - except Exception as e: - raise ParserException( - "Whilst parsing Beam, an exception occured." - " The input file is most likely of an incorrect format, " - "and some attributes were missing." - ) from e - - def parseNormalisation(self): - """ - Intialises a Normalisation object and assigns it to the - normalisation attribute. - Parses the attributes of the Normalisation from the input stream. - Raises a ParserException if any mandatory attributes are missing. - - - Parameters - ---------- - None - Returns - ------- - None - """ - - try: - # Initialise normalisation attribute - # to a new instance of Normalisation. - self.normalisation = Normalisation() - - self.consumeWhitespace() - - # The number of files and period number are both stored - # on the same line. - # So we extract the 0th integer for the number of files, - # and the 1st integer for the period number. - dataFileInfo = self.getNextToken() - numberOfFiles = nthint(dataFileInfo, 0) - self.normalisation.periodNumber = nthint(dataFileInfo, 1) - - # Extract data files - dataFiles = [] - for _ in range(numberOfFiles): - dataFiles.append(firstword(self.getNextToken())) - # Sorts list so that it is in ascending order - dataFiles.sort() - - # Create a DataFiles object from the dataFiles list constructed. - self.normalisation.dataFiles = ( - DataFiles(dataFiles, "NORMALISATION") - ) - - # The number of background files and - # background period number are both stored - # on the same line. - # So we extract the 0th integer for the number of background files, - # and the 1st integer for the background riod number. - dataFileInfoBg = self.getNextToken() - numberOfFilesBg = nthint(dataFileInfoBg, 0) - self.normalisation.periodNumberBg = nthint(dataFileInfoBg, 1) - - # Extract background data files - dataFilesBg = [] - for j in range(numberOfFilesBg): - dataFilesBg.append(firstword(self.getNextToken())) - - # Sorts list so that it is in ascending order - dataFilesBg.sort() - - # Create a DataFiles object from the dataFiles list constructed. - self.normalisation.dataFilesBg = ( - DataFiles(dataFilesBg, "NORMALISATION BACKGROUND") - ) - - # For boolean attributes, we convert the first - # integer in the line to its boolean value. - self.normalisation.forceCalculationOfCorrections = ( - boolifyNum(nthint(self.getNextToken(), 0)) - ) - - # Construct composition - composition = [] - line = self.getNextToken() - # Extract the composition. - # Each element in the composition consists of the first 'word', - # integer at the second position, and float at the third position, - # (Atomic Symbol, MassNo, Abundance) in the line. - # If the marker line is encountered, - # then the panel has been parsed. - while "end of composition input" not in line: - atomicSymbol = firstword(line) - massNo = nthfloat(line, 1) - abundance = nthfloat(line, 2) - - # Create an Element object and append to the composition list. - composition.append( - Element(atomicSymbol, massNo, abundance) - ) - line = self.getNextToken() - - # Create a Composition object from the dataFiles list constructed. - self.normalisation.composition = ( - Composition("Normalisation", elements=composition) - ) - - # For enumerated attributes, - # where the member name of the attribute is - # the first 'word' in the line, and we must get the member, - # we do this: Enum[memberName]. - self.normalisation.geometry = ( - Geometry[firstword(self.getNextToken())] - ) - - # Is the geometry FLATPLATE? - if ( - ( - self.normalisation.geometry == Geometry.SameAsBeam - and cfg.geometry == Geometry.FLATPLATE - ) - or self.normalisation.geometry == Geometry.FLATPLATE): - # If is is FLATPLATE, then extract the upstream and downstream - # thickness, the angle of rotation and sample width. - thickness = self.getNextToken() - self.normalisation.upstreamThickness = nthfloat(thickness, 0) - self.normalisation.downstreamThickness = ( - nthfloat(thickness, 1) - ) - geometryInfo = self.getNextToken() - self.normalisation.angleOfRotation = nthfloat(geometryInfo, 0) - self.normalisation.sampleWidth = nthfloat(geometryInfo, 1) - else: - - # Otherwise, it is CYLINDRICAL, - # then extract the inner and outer - # radii and the sample height. - radii = self.getNextToken() - self.normalisation.innerRadius = nthfloat(radii, 0) - self.normalisation.outerRadius = nthfloat(radii, 1) - self.normalisation.sampleHeight = ( - nthfloat(self.getNextToken(), 0) - ) - - # Extract the density. - density = nthfloat(self.getNextToken(), 0) - - # Take the absolute value of the density - since it could be -ve. - self.normalisation.density = abs(density) - - # Decide on the units of density. - # -ve density means it is atomic (atoms/A^3) - # +ve means it is chemical (gm/cm^3) - self.normalisation.densityUnits = ( - UnitsOfDensity.ATOMIC if - density < 0 - else UnitsOfDensity.CHEMICAL - ) - - self.normalisation.tempForNormalisationPC = ( - nthfloat(self.getNextToken(), 0) - ) - crossSectionSource = firstword(self.getNextToken()) - if ( - crossSectionSource == "TABLES" - or crossSectionSource == "TRANSMISSION" - ): - self.normalisation.totalCrossSectionSource = ( - CrossSectionSource[crossSectionSource] - ) - else: - self.normalisation.totalCrossSectionSource = ( - CrossSectionSource.FILE - ) - self.normalisation.crossSectionFilename = crossSectionSource - - self.normalisation.normalisationDifferentialCrossSectionFile = ( - firstword(self.getNextToken()) - ) - - self.normalisation.lowerLimitSmoothedNormalisation = ( - nthfloat(self.getNextToken(), 0) - ) - self.normalisation.normalisationDegreeSmoothing = ( - nthfloat(self.getNextToken(), 0) - ) - self.normalisation.minNormalisationSignalBR = ( - nthfloat(self.getNextToken(), 0) - ) - - # Consume whitespace and the closing brace. - self.consumeUpToDelim("}") - - # Resolve to relative. - pattern = re.compile(r"StartupFiles\S*") - - match = re.search( - pattern, - self.normalisation. - normalisationDifferentialCrossSectionFile - ) - - if match: - ( - self.normalisation. - normalisationDifferentialCrossSectionFile - ) = match.group() - - except Exception as e: - raise ParserException( - "Whilst parsing Normalisation, an exception occured." - " The input file is most likely of an incorrect format, " - "and some attributes were missing." - ) from e - - def parseSampleBackground(self): - """ - Intialises a SampleBackground object. - Parses the attributes of the SampleBackground from the input stream. - Raises a ParserException if any mandatory attributes are missing. - Returns the parsed object. - - Parameters - ---------- - None - Returns - ------- - sampleBackground : SampleBackground - The SampleBackground that was parsed from the input lines. - """ - - try: - sampleBackground = SampleBackground() - line = self.peekNextToken() - if "SAMPLE BACKGROUND" in line and "{" in line: - self.consumeTokens(1) - self.consumeWhitespace() - dataFileInfo = self.getNextToken() - numberOfFiles = nthint(dataFileInfo, 0) - sampleBackground.periodNumber = nthint(dataFileInfo, 1) - - dataFiles = [] - for _ in range(numberOfFiles): - dataFiles.append(firstword(self.getNextToken())) - sampleBackground.dataFiles = ( - DataFiles(dataFiles, "SAMPLE BACKGROUND") - ) - - # Consume whitespace and the closing brace. - self.consumeUpToDelim("}") - - return sampleBackground - except Exception as e: - raise ParserException( - "Whilst parsing Sample Background, an exception occured." - " The input file is most likely of an incorrect format, " - "and some attributes were missing." - ) from e - - def parseSample(self): - """ - Intialises a Sample object. - Parses the attributes of the Sample from the input stream. - Raises a ParserException if any mandatory attributes are missing. - Returns the parsed object. - - Parameters - ---------- - None - Returns - ------- - sample : Sample - The Sample that was parsed from the input lines. - """ - - try: - # Create a new instance of Sample. - sample = Sample() - - # Extract the sample name, and then discard whitespace lines. - sample.name = ( - str(self.getNextToken()[:-2]).strip() - .replace("SAMPLE", "").strip() - ) - sample.name = utils.replace_unwanted_chars(sample.name) - self.consumeWhitespace() - # The number of files and period number are both stored - # on the same line. - # So we extract the 0th integer for the number of files, - # and the 1st integer for the period number. - dataFileInfo = self.getNextToken() - numberOfFiles = nthint(dataFileInfo, 0) - sample.periodNumber = nthint(dataFileInfo, 1) - - # Extract data files - dataFiles = [] - for _ in range(numberOfFiles): - dataFiles.append(firstword(self.getNextToken())) - # Create a DataFiles object from the dataFiles list constructed. - sample.dataFiles = DataFiles(dataFiles, sample.name) - - # For boolean attributes, we convert the first - # integer in the line to its boolean value. - sample.forceCalculationOfCorrections = ( - boolifyNum(nthint(self.getNextToken(), 0)) - ) - - # Construct composition - composition = [] - line = self.getNextToken() - - # Extract the composition. - # Each element in the composition consists of the first 'word', - # integer at the second position, and float t the first position, - # (Atomic Symbol, MassNo, Abundance) in the line. - # If the marker line is encountered, - # then the panel has been parsed. - while "end of composition input" not in line: - - atomicSymbol = firstword(line) - massNo = nthfloat(line, 1) - abundance = nthfloat(line, 2) - - # Create an Element object and append to the composition list. - composition.append(Element(atomicSymbol, massNo, abundance)) - line = self.getNextToken() - - # Create a Composition object from the dataFiles list constructed. - sample.composition = Composition("Sample", elements=composition) - - # For enumerated attributes, - # where the member name of the attribute is - # the first 'word' in the line, and we must get the member, - # we do this: Enum[memberName]. - sample.geometry = Geometry[firstword(self.getNextToken())] - - # Is the geometry FLATPLATE? - if ( - ( - sample.geometry == Geometry.SameAsBeam - and cfg.geometry == Geometry.FLATPLATE - ) - or sample.geometry == Geometry.FLATPLATE): - # If is is FLATPLATE, then extract the upstream and downstream - # thickness, the angle of rotation and sample width. - thickness = self.getNextToken() - sample.upstreamThickness = nthfloat(thickness, 0) - sample.downstreamThickness = nthfloat(thickness, 1) - - geometryInfo = self.getNextToken() - sample.angleOfRotation = nthfloat(geometryInfo, 0) - sample.sampleWidth = nthfloat(geometryInfo, 1) - else: - - # Otherwise, it is CYLINDRICAL, - # then extract the inner and outer - # radii and the sample height. - radii = self.getNextToken() - sample.innerRadius = nthfloat(radii, 0) - sample.outerRadius = nthfloat(radii, 1) - sample.sampleHeight = nthfloat(self.getNextToken(), 0) - - # Extract the density. - density = nthfloat(self.getNextToken(), 0) - - # Decide on the units of density. - # -ve density means it is atomic (atoms/A^3) - # +ve means it is chemical (gm/cm^3) - sample.density = abs(density) - sample.densityUnits = ( - UnitsOfDensity.ATOMIC if - density < 0 - else UnitsOfDensity.CHEMICAL - ) - sample.tempForNormalisationPC = nthfloat(self.getNextToken(), 0) - crossSectionSource = firstword(self.getNextToken()) - if ( - crossSectionSource == "TABLES" - or crossSectionSource == "TRANSMISSION" - ): - sample.totalCrossSectionSource = ( - CrossSectionSource[crossSectionSource] - ) - else: - sample.totalCrossSectionSource = CrossSectionSource.FILE - sample.crossSectionFilename = crossSectionSource - sample.sampleTweakFactor = nthfloat(self.getNextToken(), 0) - - topHatW = nthfloat(self.getNextToken(), 0) - if topHatW == 0: - sample.topHatW = 0 - sample.FTMode = FTModes.NO_FT - elif topHatW < 0: - sample.topHatW = abs(topHatW) - sample.FTMode = FTModes.SUB_AVERAGE - else: - sample.topHatW = topHatW - sample.FTMode = FTModes.ABSOLUTE - - sample.minRadFT = nthfloat(self.getNextToken(), 0) - sample.grBroadening = nthfloat(self.getNextToken(), 0) - - # Extract the resonance values. - # Each row consists of the first 2 floats. - # (minWavelength, maxWavelength) in the line. - # If the marker line is encountered, - # then the values has been parsed. - line = self.getNextToken() - while ( - "to finish specifying wavelength range of resonance" - not in line - ): - sample.resonanceValues.append( - extract_floats_from_string(line) - ) - line = self.getNextToken() - - # Extract the exponential values. - # Each row consists of the first 3 numbers. - # (Amplitude, Decay, N) in the line. - # If the marker line is encountered, - # then the values has been parsed. - line = self.getNextToken() - if "to specify end of exponential parameter input" not in line: - sample.exponentialValues = [] - while "to specify end of exponential parameter input" not in line: - sample.exponentialValues.append( - extract_nums_from_string(line) - ) - - line = self.getNextToken() - - sample.normalisationCorrectionFactor = ( - nthfloat(self.getNextToken(), 0) - ) - sample.fileSelfScattering = firstword(self.getNextToken()) - sample.normaliseTo = ( - NormalisationType[ - NormalisationType(nthint(self.getNextToken(), 0)).name - ] - ) - sample.maxRadFT = nthfloat(self.getNextToken(), 0) - sample.outputUnits = ( - OutputUnits[OutputUnits(nthint(self.getNextToken(), 0)).name] - ) - sample.powerForBroadening = nthfloat(self.getNextToken(), 0) - sample.stepSize = nthfloat(self.getNextToken(), 0) - sample.runThisSample = boolifyNum(nthint(self.getNextToken(), 0)) - environmentValues = self.getNextToken() - sample.scatteringFraction = nthfloat(environmentValues, 0) - sample.attenuationCoefficient = nthfloat(environmentValues, 1) - - # Consume whitespace and the closing brace. - self.consumeUpToDelim("}") - - return sample - - except Exception as e: - raise ParserException( - "Whilst parsing Sample, an exception occured." - " The input file is most likely of an incorrect format, " - "and some attributes were missing." - ) from e - - def parseContainer(self): - """ - Intialises a Container object. - Parses the attributes of the Container from the input stream. - Raises a ParserException if any mandatory attributes are missing. - Returns the parsed object. - - Parameters - ---------- - None - Returns - ------- - container : Container - The Container that was parsed from the input lines. - """ - - try: - # Create a new instance of Container. - container = Container() - - # Extract the name from the lines, - # and then discard the unnecessary lines. - container.name = ( - str(self.getNextToken()[:-2]).strip() - .replace("CONTAINER", "").strip() - ) - self.consumeWhitespace() - - # The number of files and period number are both stored - # on the same line. - # So we extract the 0th integer for the number of files, - # and the 1st integer for the period number. - dataFileInfo = self.getNextToken() - numberOfFiles = nthint(dataFileInfo, 0) - container.periodNumber = nthint(dataFileInfo, 1) - - # Extract data files - dataFiles = [] - for _ in range(numberOfFiles): - dataFiles.append(firstword(self.getNextToken())) - - # Create a DataFiles object from the dataFiles list constructed. - container.dataFiles = DataFiles(dataFiles, container.name) - - # Construct composition - composition = [] - line = self.getNextToken() - # Extract the composition. - # Each element in the composition consists of the first 'word', - # integer at the second position, and float t the first position, - # (Atomic Symbol, MassNo, Abundance) in the line. - # If the marker line is encountered, - # then the panel has been parsed. - while "end of composition input" not in line: - - atomicSymbol = firstword(line) - massNo = nthfloat(line, 1) - abundance = nthfloat(line, 2) - - # Create an Element object and append to the composition list. - composition.append(Element(atomicSymbol, massNo, abundance)) - line = self.getNextToken() - # Create a Composition object from the dataFiles list constructed. - container.composition = Composition( - "Container", - elements=composition - ) - - # For enumerated attributes, - # where the member name of the attribute is - # the first 'word' in the line, and we must get the member, - # we do this: Enum[memberName]. - container.geometry = Geometry[firstword(self.getNextToken())] - - # Is the geometry FLATPLATE? - if ( - ( - container.geometry == Geometry.SameAsBeam - and cfg.geometry == Geometry.FLATPLATE - ) - or container.geometry == Geometry.FLATPLATE): - # If is is FLATPLATE, then extract the upstream and downstream - # thickness, the angle of rotation and sample width. - thickness = self.getNextToken() - container.upstreamThickness = nthfloat(thickness, 0) - container.downstreamThickness = nthfloat(thickness, 1) - - geometryValues = self.getNextToken() - container.angleOfRotation = nthfloat(geometryValues, 0) - container.sampleWidth = nthfloat(geometryValues, 1) - else: - - # Otherwise, it is CYLINDRICAL, - # then extract the inner and outer - # radii and the sample height. - radii = self.getNextToken() - container.innerRadius = nthfloat(radii, 0) - container.outerRadius = nthfloat(radii, 1) - container.sampleHeight = nthfloat(self.getNextToken(), 0) - - # Extract the density. - density = nthfloat(self.getNextToken(), 0) - - # Take the absolute value of the density - since it could be -ve. - container.density = abs(density) - - # Decide on the units of density. - # -ve density means it is atomic (atoms/A^3) - # +ve means it is chemical (gm/cm^3) - container.densityUnits = ( - UnitsOfDensity.ATOMIC if - density < 0 - else UnitsOfDensity.CHEMICAL - ) - crossSectionSource = firstword(self.getNextToken()) - if ( - crossSectionSource == "TABLES" - or crossSectionSource == "TRANSMISSION" - ): - container.totalCrossSectionSource = ( - CrossSectionSource[crossSectionSource] - ) - else: - container.totalCrossSectionSource = CrossSectionSource.FILE - container.crossSectionFilename = crossSectionSource - container.tweakFactor = nthfloat(self.getNextToken(), 0) - - environmentValues = self.getNextToken() - container.scatteringFraction = nthfloat(environmentValues, 0) - container.attenuationCoefficient = nthfloat(environmentValues, 1) - - # Consume whitespace and the closing brace. - self.consumeUpToDelim("}") - - return container - - except Exception as e: - raise ParserException( - "Whilst parsing Container, an exception occured." - " The input file is most likely of an incorrect format, " - "and some attributes were missing." - ) from e - - def parseComponents(self): - try: - while self.stream: - component = self.parseComponent() - if component: - self.components.addComponent(component) - except Exception as e: - raise ParserException( - "Whilst parsing Components, an exception occured." - " The input file is most likely of an incorrect format." - ) from e - - def parseComponent(self): - name = self.getNextToken().rstrip() - component = Component(name) - line = self.peekNextToken() - if "(" in line: - self.consumeTokens(1) - else: - return - line = self.getNextToken() - while line and ")" not in line: - atomicSymbol, massNo, abundance = line.split() - element = Element(atomicSymbol, float(massNo), float(abundance)) - component.addElement(element) - line = self.getNextToken() - return component - - def makeParse(self, key): - """ - Calls a parsing function from a dictionary of parsing functions - by the input key. - Returns the result of the called parsing function. - Only use case is as a helper function during parsing. - - Parameters - ---------- - key : str - Parsing function to call - (INSTRUMENT/BEAM/NORMALISATION/SAMPLE BACKGROUND/SAMPLE/CONTAINER) - Returns - ------- - NoneType - if parsing INSTRUMENT/BEAM/NORMALISATION - SampleBackground - if parsing SAMPLE BACKGROUND - Sample - if parsing Sample - Container - if parsing Container - None - if parsing Components - """ - - parsingFunctions = { - "INSTRUMENT": self.parseInstrument, - "BEAM": self.parseBeam, - "NORMALISATION": self.parseNormalisation, - "SAMPLE BACKGROUND": self.parseSampleBackground, - "SAMPLE": self.parseSample, - "CONTAINER": self.parseContainer, - "COMPONENTS": self.parseComponents - } - # Return the result of the parsing function that was called. - return parsingFunctions[key]() - - def sampleBackgroundHelper(self): - """ - Helper method for parsing Sample Background and its - Samples and their Containers. - Returns the SampleBackground object. - Parameters - ---------- - None - Returns - ------- - SampleBackground - The SampleBackground parsed from the lines. - """ - - # Parse sample background. - sampleBackground = self.makeParse("SAMPLE BACKGROUND") - - self.consumeWhitespace() - line = self.peekNextToken() - - # Parse all Samples and Containers belonging to the sample background. - while "END" not in line and "SAMPLE BACKGROUND" not in line: - if not line: - raise ParserException("Unexpected EOF during parsing.") - elif "GO" in line: - self.getNextToken() - elif "SAMPLE" in line and firstword(line) == "SAMPLE": - sample = self.makeParse("SAMPLE") - if not sample.name: - sample.name = utils.replace_unwanted_chars(uniquifyName( - "SAMPLE", - [s.name for s in sampleBackground.samples], - sep="", - incFirst=True - )) - sampleBackground.samples.append(sample) - elif "CONTAINER" in line and firstword(line) == "CONTAINER": - container = self.makeParse("CONTAINER") - if not container.name: - container.name = uniquifyName( - "CONTAINER", - [c.name - for c in sampleBackground.samples[-1].containers], - sep="", - incFirst=True) - sampleBackground.samples[-1].containers.append( - container - ) - self.consumeWhitespace() - line = self.peekNextToken() - return sampleBackground - - def parse(self, path, config=False): - """ - Parse the GudrunFile from its path. - Assign objects from the file to the attributes of the class. - Raises ParserException if Instrument, - Beam or Normalisation are missing. - - Parameters - ---------- - None - Returns - ------- - None - """ - self.config = config - # Ensure only valid files are given. - if not path: - raise ParserException( - "Path not supplied. Cannot parse from an empty path!" - ) - if not os.path.exists(path): - raise ParserException( - "The path supplied is invalid.\ - Cannot parse from an invalid path" + path - ) - if self.format == Format.YAML: - # YAML Files - try: - ( - self.instrument, - self.beam, - self.components, - self.normalisation, - self.sampleBackgrounds, - cfg.GUI - ) = self.yaml.parseYaml(path) - except YAMLException as e: - raise ParserException(e) - else: - # TXT Files - parsing = "" - KEYWORDS = { - "INSTRUMENT": False, - "BEAM": False, - "NORMALISATION": False - } - - # Decide the encoding - import chardet - with open(path, 'rb') as fp: - encoding = chardet.detect(fp.read())['encoding'] - - # Read the input stream into our attribute. - with open(path, encoding=encoding) as fp: - self.stream = fp.readlines() - - # Here we go! Get the first token and begin parsing. - line = self.getNextToken() - - # Iterate through the file, - # parsing the Instrument, Beam and Normalisation. - while ( - self.stream - and not all(value for value in KEYWORDS.values()) - ): - if ( - firstword(line) in KEYWORDS.keys() - and not KEYWORDS[firstword(line)] - ): - parsing = firstword(line) - self.makeParse(parsing) - KEYWORDS[parsing] = True - line = self.getNextToken() - - # If we didn't parse each one of the keywords, then panic. - if not all(KEYWORDS.values()) and not config: - raise ParserException(( - 'INSTRUMENT, BEAM and NORMALISATION' - ' were not parsed. It\'s possible the file' - ' supplied is of an incorrect format!' - )) - elif not KEYWORDS["INSTRUMENT"] and config: - raise ParserException(( - 'INSTRUMENT was not parsed. It\'s possible the file' - ' supplied is of an incorrect format!' - )) - - # Ignore whitespace. - self.consumeWhitespace() - line = self.peekNextToken() - - # Parse sample backgrounds, alongside their samples and containers. - while self.stream: - if "SAMPLE BACKGROUND" in line and "{" in line: - self.sampleBackgrounds.append( - self.sampleBackgroundHelper() - ) - elif "COMPONENTS:" in line: - self.makeParse("COMPONENTS") - line = self.getNextToken() - def __str__(self): """ Returns the string representation of the GudrunFile object. @@ -1505,9 +248,3 @@ def __str__(self): + footer + components ) - - -Container.getNextToken = GudrunFile.getNextToken -Container.peekNextToken = GudrunFile.peekNextToken -Container.consumeUpToDelim = GudrunFile.consumeUpToDelim -Container.consumeWhitespace = GudrunFile.consumeWhitespace diff --git a/gudpy/core/io/__init__.py b/gudpy/core/io/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/gudpy/core/io.py b/gudpy/core/io/gudpy_io.py similarity index 56% rename from gudpy/core/io.py rename to gudpy/core/io/gudpy_io.py index 55acf8e4d..fb42d37f6 100644 --- a/gudpy/core/io.py +++ b/gudpy/core/io/gudpy_io.py @@ -3,7 +3,6 @@ from ruamel.yaml import YAML as yaml from ruamel.yaml import YAMLError import os -import typing as typ from core.composition import ( Component, Components, Composition, WeightedComponent @@ -20,127 +19,138 @@ from core.sample import Sample from core.container import Container from core.gudrun_file import GudrunFile +from core.io.txt_parser import GudrunFileParser from core import config -import core.enums as enums -class YamlParser: +class GudpyIO: def __init__(self): - self.yaml = self.getYamlModule() + # Construct the outpath of generated input file + self.projectDir = "" + self.loadFile = "" + self.autosaveLocation = "" + + self.yaml = self._getYamlModule() + self.gudrunFileParser = GudrunFileParser() + + @property + def projectName(self) -> str: + return os.path.dirname(self.projectDir) + + @property + def autosavePath(self) -> str: + return f"{os.path.basename(projectDir)}.autosave" @property - def path(self, format: enums.Format): - if not self.projectDir or self.projectDir: + def path(self): + """Returns the path to the yaml file + + Returns + ------- + str + Path to yaml file + + Raises + ------ + RuntimeError + Raised if project directory is not set + """ + if not self.projectDir: raise RuntimeError("Save location not set.") + return os.path.join(self.projectDir, f"{self.projectName}.yaml") - if format == enums.Format.TXT: - return os.path.join(self.projectDir, f"{self.projectName}.txt") - elif format == enums.Format.YAML: - return os.path.join(self.projectDir, f"{self.projectName}.yaml") - else: - raise RuntimeError("Unsupported format.") + def setSaveLocation(self, projectDir: str): + """Sets the save location/project directory - def save(self, path: str = '', format: enums.Format = None): - if not path: - path = self.path() + Parameters + ---------- + projectDir : str + Path to new save location + """ + self.projectDir = projectDir - if not format: - format = self.format - if format == enums.Format.TXT: - self.write_out( - path=self.txtPath(), overwrite=True) - elif format == enums.Format.YAML: - if self.path(): - self.write_yaml(self.yamlPath()) + def checkSaveLocation(self) -> bool: + """Checks if user has set the save location + """ + return bool(self.projectDir) - @property - def projectName(self): - os.path.dirname(self.projectDir) - - def write_out( - self, - path: str = '', - overwrite: bool = False, - writeParameters: bool = True - ): + def save(self): + """Saves yaml file to project directory """ - Writes out the string representation of the GudrunFile. - If 'overwrite' is True, then the initial file is overwritten. - Otherwise, it is written to 'gudpy_{initial filename}.txt'. + self.writeYaml(self.path) + + def importGudrunFile(self, path) -> GudrunFile: + return self.gudrunFileParser.parse(path) + + def importProject(self, projectDir) -> GudrunFile: + """Imports from a project directory Parameters ---------- - overwrite : bool, optional - Overwrite the initial file? (default is False). - path : str, optional - Path to write to. + projectDir : str + Path to GudPy project folder + + Raises + ------ + FileNotFoundError + Raised if there is no YAML input file in the + project directory + Returns ------- - None + GudrunFile + Genererated GudrunFile from input """ - if path: - if not overwrite: - assert (not os.path.exists(path)) - f = open( - path, "w", encoding="utf-8" + loadFile = "" + + if os.path.exists(os.path.join( + projectDir, + f"{os.path.basename(projectDir)}.yaml" + )): + # If default file exists + loadFile = os.path.join( + projectDir, + f"{os.path.basename(projectDir)}.yaml" ) - elif not overwrite: - assert (not os.path.exists(os.path.join( - self.instrument.GudrunInputFileDir, - self.OUTPATH) - )) - f = open( - os.path.join( - self.instrument.GudrunInputFileDir, - self.OUTPATH - ), "w", encoding="utf-8") else: - if not self.path(): - path = os.path.join( - self.instrument.GudrunInputFileDir, - self.OUTPATH) - f = open(path, "w", encoding="utf-8") - - if os.path.basename(f.name) == self.OUTPATH: - for sampleBackground in self.sampleBackgrounds: - sampleBackground.writeAllSamples = False - f.write(str(self)) - f.close() - - if writeParameters: - for sb in self.sampleBackgrounds: - for s in sb.samples: - if s.runThisSample: - gf = deepcopy(self) - gf.sampleBackgrounds = [deepcopy(sb)] - gf.sampleBackgrounds[0].samples = [deepcopy(s)] - gf.write_out( - path=os.path.join( - self.instrument.GudrunInputFileDir, - s.pathName(), - ), - overwrite=True, - writeParameters=False - ) + # Try to find yaml files + for f in os.listdir(projectDir): + if os.path.splitext(f)[1] == ".yaml": + # If file is yaml + loadFile = os.path.join(projectDir, f) + if not loadFile: + raise FileNotFoundError( + "Could not find GudPy input file within the project") + + self.setSaveLocation(projectDir) + return self._parseYaml(loadFile) + + def exportGudrunFile(self, path): + return self.gudrunFileParser.export(path) + + def writeGudrunFile(self, path): + self. + return self.gudrunFileParser.writeGudrunFile(path) # ================================================================= -# YAML PARSING +# PARSING HELPERS # ================================================================= - def getYamlModule(self) -> yaml: + def _getYamlModule(self) -> yaml: yaml_ = yaml() yaml_.preserve_quotes = True yaml_.default_flow_style = None yaml_.encoding = 'utf-8' return yaml_ - def parseYaml(self, path) -> GudrunFile: - yamldict = self.yamlToDict(path) - gudrunFile = GudrunFile() + def _parseYaml(self, path) -> GudrunFile: + self.loadFile = path + yamldict = self._yamlToDict(path) + gudrunFile = GudrunFile(loadFile=path) for k, v in yamldict.items(): if k == "GUI": GUI = GUIConfig() - self.yamlToObject(GUI, v) + self._yamlToObject(GUI, v) config.GUIConfig = GUI continue if not hasattr(gudrunFile, k): @@ -149,7 +159,7 @@ def parseYaml(self, path) -> GudrunFile: continue objType = type(gudrunFile.__dict__[k]) - parsingFunc = self.parsingFuncMap(objType) + parsingFunc = self._parsingFuncMap(objType) if parsingFunc: try: obj = parsingFunc(v) @@ -161,7 +171,7 @@ def parseYaml(self, path) -> GudrunFile: raise YAMLException(f"Invalid YAML file: {str(e)}") return gudrunFile - def writeYAML(self, gudrunFile: GudrunFile, path: str) -> None: + def _writeYAML(self, gudrunFile: GudrunFile, path: str) -> None: with open(path, "wb") as fp: outyaml = { "Instrument": gudrunFile.instrument, @@ -172,39 +182,39 @@ def writeYAML(self, gudrunFile: GudrunFile, path: str) -> None: "GUI": config.GUI } self.yaml.dump( - {k: self.toYaml(v) for k, v in outyaml.items()}, + {k: self._toYaml(v) for k, v in outyaml.items()}, fp ) - def parsingFuncMap(self, type: type) -> typ.Callable | None: + def _parsingFuncMap(self, type: type) -> typ.Callable | None: parsingFuncs = { - Instrument: self.parseYamlInstrument, - Beam: self.parseYamlBeam, - Normalisation: self.parseYamlNormalisation, - SampleBackground: self.parseYamlSampleBackground, - Sample: self.parseYamlSample, - Composition: self.parseYamlComposition, - Component: self.parseYamlComponent, - Components: self.parseYamlComponents, - list[Element]: self.parseYamlElements, - DataFiles: self.parseYamlDataFiles + Instrument: self._parseYamlInstrument, + Beam: self._parseYamlBeam, + Normalisation: self._parseYamlNormalisation, + SampleBackground: self._parseYamlSampleBackground, + Sample: self._parseYamlSample, + Composition: self._parseYamlComposition, + Component: self._parseYamlComponent, + Components: self._parseYamlComponents, + list[Element]: self._parseYamlElements, + DataFiles: self._parseYamlDataFiles } if type in parsingFuncs.keys(): return parsingFuncs[type] else: return None - def yamlToDict(self, path) -> any: + def _yamlToDict(self, path) -> any: # Read the input stream into our attribute. with open(path, encoding=self.yaml.encoding) as fp: return self.yaml.load(fp) - def yamlToObject(self, obj: any, yamldict: any) -> any: + def _yamlToObject(self, obj: any, yamldict: any) -> any: for k, v in yamldict.items(): - self.assignYamlToAttr(obj, k, v) + self._assignYamlToAttr(obj, k, v) return obj - def assignYamlToAttr(self, obj: any, key, val) -> None: + def _assignYamlToAttr(self, obj: any, key, val) -> None: if not hasattr(obj, key): # If attribute is not valid print( @@ -214,61 +224,61 @@ def assignYamlToAttr(self, obj: any, key, val) -> None: return setattr(obj, key, type(obj.__dict__[key])(self.toBuiltin(val))) - def parseYamlInstrument(self, yamldict: any) -> Instrument: + def _parseYamlInstrument(self, yamldict: any) -> Instrument: instrument = Instrument() - instrument = self.yamlToObject(instrument, yamldict) + instrument = self._yamlToObject(instrument, yamldict) return instrument - def parseYamlBeam(self, yamldict: any) -> Normalisation: + def _parseYamlBeam(self, yamldict: any) -> Normalisation: beam = Beam() - beam = self.yamlToObject(beam, yamldict) + beam = self._yamlToObject(beam, yamldict) return beam - def parseYamlNormalisation(self, yamldict: any) -> Normalisation: + def _parseYamlNormalisation(self, yamldict: any) -> Normalisation: normalisation = Normalisation() for k, v in yamldict.items(): if k == "composition": - composition = self.parseYamlComposition(yamldict[k]) + composition = self._parseYamlComposition(yamldict[k]) setattr(normalisation, k, composition) else: - self.assignYamlToAttr(normalisation, k, v) + self._assignYamlToAttr(normalisation, k, v) return normalisation - def parseYamlSample(self, yamldict: any) -> Sample: + def _parseYamlSample(self, yamldict: any) -> Sample: sample = Sample() for k, v in yamldict.items(): if k == "composition": - composition = self.parseYamlComposition() + composition = self._parseYamlComposition() setattr(sample, k, composition) elif k == "containers": for contyaml in yamldict[k]: - container = self.parseYamlContainer(contyaml) + container = self._parseYamlContainer(contyaml) sample.containers.append(container) else: - self.assignYamlToAttr(sample, k, v) + self._assignYamlToAttr(sample, k, v) return sample - def parseYamlSampleBackground(self, yamldict: any) -> SampleBackground: + def _parseYamlSampleBackground(self, yamldict: any) -> SampleBackground: sampleBg = SampleBackground() for k, v in yamldict.items(): if k == "samples": for sampleyaml in yamldict[k]: - sample = self.parseYamlSample(sampleyaml) + sample = self._parseYamlSample(sampleyaml) sample.name = utils.replace_unwanted_chars(sample.name) sampleBg.samples.append(sample) else: - self.assignYamlToAttr(sampleBg, k, v) + self._assignYamlToAttr(sampleBg, k, v) - def parseYamlComposition(self, yamldict: any) -> Composition: + def _parseYamlComposition(self, yamldict: any) -> Composition: composition = Composition() for k, v in yamldict.items(): if k == "elements": - elements = self.parseYamlElements(yamldict[k]) + elements = self._parseYamlElements(yamldict[k]) setattr(composition, k, elements) elif k == "weightedComponents": weightedComponents = [] for wc in yamldict[k]: - component = self.parseYamlComponent(wc) + component = self._parseYamlComponent(wc) ratio = wc["ratio"] try: weightedComponents.append( @@ -280,28 +290,28 @@ def parseYamlComposition(self, yamldict: any) -> Composition: "Invalid ratio given to Weighted Component") setattr(composition, k, weightedComponents) else: - self.assignYamlToAttr(composition, k, v) + self._assignYamlToAttr(composition, k, v) return composition - def parseYamlContainer(self, yamldict: any) -> Container: + def _parseYamlContainer(self, yamldict: any) -> Container: container = Container() for k, v in yamldict.items(): if k == "composition": - composition = self.parseYamlComposition() + composition = self._parseYamlComposition() setattr(container, k, composition) else: - self.assignYamlToAttr(container, k, v) + self._assignYamlToAttr(container, k, v) return container - def parseYamlDataFiles(self, yamldict: any) -> DataFiles: + def _parseYamlDataFiles(self, yamldict: any) -> DataFiles: dataFiles = DataFiles() for df in yamldict["_dataFiles"]: dataFile = DataFile(df["filename"]) - dataFile = self.yamlToObject(dataFile, df) + dataFile = self._yamlToObject(dataFile, df) dataFiles.dataFiles.append(dataFile) return dataFiles - def parseYamlComponent(self, yamldict: any) -> Component: + def _parseYamlComponent(self, yamldict: any) -> Component: if ( "component" not in yamldict or "ratio" not in yamldict @@ -310,18 +320,18 @@ def parseYamlComponent(self, yamldict: any) -> Component: "Weighted Component expects 'component' and" + " 'ratio' to be provided") component = Component() - self.yamlToObject( + self._yamlToObject( component, yamldict["component"] ) return component - def parseYamlComponents(self, yamllist: list[dict]) -> Components: + def _parseYamlComponents(self, yamllist: list[dict]) -> Components: components = [] for c in yamllist: - components.append(self.parseYamlComponent(c)) + components.append(self._parseYamlComponent(c)) return components - def parseYamlElements(self, yamllist: list[str]) -> list[Element]: + def _parseYamlElements(self, yamllist: list[str]) -> list[Element]: elements = [] for idx, element in enumerate(yamllist): # Ensuring correct arguements are provided @@ -358,7 +368,7 @@ def toYaml(self, var: any): return float(var) if var.__class__.__module__ == "builtins": if isinstance(var, (list, tuple)): - return type(var)([self.toYaml(v) for v in var]) + return type(var)([self._toYaml(v) for v in var]) else: return var elif isinstance(var, Enum): @@ -369,7 +379,7 @@ def toYaml(self, var: any): Component, Composition, Element, DataFiles, DataFile, GUIConfig )): return { - k: self.toYaml(v) + k: self._toYaml(v) for k, v in var.__dict__.items() if k not in var.yamlignore } @@ -392,11 +402,3 @@ def toBuiltin(self, yamlvar: any): return float(yamlvar) elif yamlvar.__class__.__module__ == "ruamel.yaml.scalarstring": return str(yamlvar) - -# ================================================================= -# TXT PARSING -# ================================================================= - - -class TxtParser: - def __init__(self): diff --git a/gudpy/core/io/txt_parser.py b/gudpy/core/io/txt_parser.py new file mode 100644 index 000000000..bcac4a815 --- /dev/null +++ b/gudpy/core/io/txt_parser.py @@ -0,0 +1,1181 @@ +import re +import os +import typing as typ + +from core.composition import ( + Component, Components, Composition +) +from core.data_files import DataFiles +from core.element import Element +from core.exception import ParserException +from core import utils +from core.instrument import Instrument +from core.beam import Beam +from core.normalisation import Normalisation +from core.sample_background import SampleBackground +from core.sample import Sample +from core.container import Container +from core.gudrun_file import GudrunFile +from core import config +import core.enums as enums + + +class GudrunFileParser: + """Class to manage the parsing of Gudrun input files + """ + OUTPATH = "gudpy.txt" + + def __init__(self): + # Text stream + self.stream = None + + def writeGudrunFile(self, gudrunFile: GudrunFile, runDir: str): + gudrunFile.setGudrunDir(runDir) + for sampleBackground in self.sampleBackgrounds: + sampleBackground.writeAllSamples = False + + writePath = os.path.join(runDir, GudrunFileParser.OUTPATH) + + with open(writePath, "w", encoding="utf-8") as f: + f.write(str(gudrunFile)) + f.close() + + def export(self, gudrunFile: GudrunFile, path: str): + for sampleBackground in self.sampleBackgrounds: + sampleBackground.writeAllSamples = True + + with open(path, "w", encoding="utf-8") as f: + f.write(str(gudrunFile)) + f.close() + + def parse( + self, path: str, config=False + ) -> typ.Tuple[ + Instrument, Beam, Normalisation, + list[SampleBackground], Components]: + """ + Parse the GudrunFile from its path. + Assign objects from the file to the attributes of the class. + Raises ParserException if Instrument, + Beam or Normalisation are missing. + """ + self.config = config + + instrument = None + beam = None + components = None + normalisation = None + sampleBackgrounds = [] + + # Ensure only valid files are given. + if not path: + raise ParserException( + "Path not supplied. Cannot parse from an empty path!" + ) + if not os.path.exists(path): + raise ParserException( + "The path supplied is invalid.\ + Cannot parse from an invalid path" + path + ) + + KEYWORDS = { + "INSTRUMENT": False, + "BEAM": False, + "NORMALISATION": False + } + # Decide the encoding + import chardet + with open(path, 'rb') as fp: + encoding = chardet.detect(fp.read())['encoding'] + # Read the input stream into our attribute. + with open(path, encoding=encoding) as fp: + self.stream = fp.readlines() + # Here we go! Get the first token and begin parsing. + line = self._getNextToken() + + # Iterate through the file, + # parsing the Instrument, Beam and Normalisation. + while self.stream: + line = utils.firstword(line) + if line == "INSTRUMENT": + instrument = self._parseInstrument(line) + KEYWORDS[line] = True + elif line == "BEAM": + beam = self._parseBeam(line) + KEYWORDS[line] = True + elif line == "NORMALISATION": + normalisation = self._parseNormalisation(line) + KEYWORDS[line] = True + + # If we didn't parse each one of the keywords, then panic. + if not all(KEYWORDS.values()) and not config: + raise ParserException(( + 'INSTRUMENT, BEAM and NORMALISATION' + ' were not parsed. It\'s possible the file' + ' supplied is of an incorrect format!' + )) + elif not KEYWORDS["INSTRUMENT"] and config: + raise ParserException(( + 'INSTRUMENT was not parsed. It\'s possible the file' + ' supplied is of an incorrect format!' + )) + + # Ignore whitespace. + self._consumeWhitespace() + line = self._peekNextToken() + + # Parse sample backgrounds, alongside their samples and containers. + while self.stream: + if "SAMPLE BACKGROUND" in line and "{" in line: + sampleBackgrounds.append( + self._sampleBackgroundHelper() + ) + elif "COMPONENTS:" in line: + components = self._parseComponents(line) + line = self._getNextToken() + + return GudrunFile( + loadFile=path, + params=( + instrument, + beam, + normalisation, + sampleBackgrounds, + components + )) + +# ================================================================= +# PARSING HELPERS +# ================================================================= + def _getNextToken(self) -> str | None: + """ + Pops the 'next token' from the stream and returns it. + Essentially removes the first line in the stream and returns it. + """ + return self.stream.pop(0) if self.stream else None + + def _peekNextToken(self) -> str | None: + """ + Returns the next token in the input stream, without removing it. + """ + return self.stream[0] if self.stream else None + + def _consumeTokens(self, n: int) -> None: + """ + Consume n tokens from the input stream. + """ + for _ in range(n): + self._getNextToken() + + def _consumeUpToDelim(self, delim: str) -> None: + """ + Consume tokens iteratively, until a delimiter is reached. + """ + line = self._getNextToken() + while line[0] != delim: + line = self._getNextToken() + + def _consumeWhitespace(self) -> None: + """ + Consume tokens iteratively, while they are whitespace. + """ + line = self._peekNextToken() + if line and line.isspace(): + self._getNextToken() + line = self._peekNextToken() + + def _parseInstrument(self) -> Instrument: + """ + Intialises an Instrument object and assigns it to the + instrument attribute. + Parses the attributes of the Instrument from the input stream. + Raises a ParserException if any mandatory attributes are missing. + """ + try: + instrument = Instrument() + self._consumeWhitespace() + + # For string attributes, + # we simply extract the utils.firstword in the line. + instrument.name = enums.Instruments[ + utils.firstword(self._getNextToken())] + self._consumeTokens(1) + instrument.dataFileDir = os.path.abspath( + utils.firstword(self._getNextToken())) + os.path.sep + instrument.dataFileType = utils.firstword(self._getNextToken()) + instrument.detectorCalibrationFileName = ( + utils.firstword(self._getNextToken()) + ) + + # For single integer attributes, + # we extract the zeroth int from the line. + instrument.columnNoPhiVals = utils.nthint(self._getNextToken(), 0) + instrument.groupFileName = utils.firstword(self._getNextToken()) + instrument.deadtimeConstantsFileName = ( + utils.firstword(self._getNextToken()) + ) + + # For N integer attributes, + # we extract the first N integers from the line. + instrument.spectrumNumbersForIncidentBeamMonitor = ( + utils.extract_ints_from_string(self._getNextToken()) + ) + + # For integer pair attributes, + # we extract the first 2 integers from the line. + instrument.wavelengthRangeForMonitorNormalisation = ( + utils.firstNFloats(self._getNextToken(), 2) + ) + + if all( + instrument.wavelengthRangeForMonitorNormalisation + ) == 0.0: + instrument.wavelengthRangeForMonitorNormalisation = [ + 0, 0 + ] + + instrument.spectrumNumbersForTransmissionMonitor = ( + utils.extract_ints_from_string(self._getNextToken()) + ) + + # For single float attributes, + # we extract the zeroth float from the line. + instrument.incidentMonitorQuietCountConst = ( + utils.nthfloat(self._getNextToken(), 0) + ) + instrument.transmissionMonitorQuietCountConst = ( + utils.nthfloat(self._getNextToken(), 0) + ) + + instrument.channelNosSpikeAnalysis = ( + utils.firstNInts(self._getNextToken(), 2) + ) + instrument.spikeAnalysisAcceptanceFactor = ( + utils.nthfloat(self._getNextToken(), 0) + ) + + # Extract wavelength range + # Which consists of the first 3 floats + # (min, max, step) in the line. + wavelengthRange = utils.firstNFloats(self._getNextToken(), 3) + instrument.wavelengthMin = wavelengthRange[0] + instrument.wavelengthMax = wavelengthRange[1] + instrument.wavelengthStep = wavelengthRange[2] + + instrument.NoSmoothsOnMonitor = utils.nthint( + self._getNextToken(), 0) + + # Extract X range + # Which consists of the first 3 floats + # (min, max, step) in the line. + XRange = utils.firstNFloats(self._getNextToken(), 3) + + instrument.XMin = XRange[0] + instrument.XMax = XRange[1] + instrument.XStep = XRange[2] + + # Extract the grouping parameter panel. + # Each row in the panel consists of the first 4 ints + # (Group, XMin, XMax, Background Factor) in the line. + # If the marker line is encountered, + # then the panel has been parsed. + + line = self._getNextToken() + while "to end input of specified values" not in line: + group = utils.nthint(line, 0) + xMin = utils.nthfloat(line, 1) + xMax = utils.nthfloat(line, 2) + backgroundFactor = utils.nthfloat(line, 3) + instrument.groupingParameterPanel.append( + [group, xMin, xMax, backgroundFactor] + ) + line = self._getNextToken() + + instrument.groupsAcceptanceFactor = ( + utils.nthfloat(self._getNextToken(), 0) + ) + instrument.mergePower = utils.nthint(self._getNextToken(), 0) + + # For boolean attributes, we convert the first + # integer in the line to its boolean value. + instrument.subSingleAtomScattering = ( + utils.boolifyNum(utils.nthint(self._getNextToken(), 0)) + ) + + # For enumerated attributes, where the value of the attribute is + # the first integer in the line, and we must get the member, + # we do this: Enum[Enum(value).name] + instrument.mergeWeights = ( + enums.MergeWeights[enums.MergeWeights( + utils.nthint(self._getNextToken(), 0)).name] + ) + instrument.incidentFlightPath = ( + utils.nthfloat(self._getNextToken(), 0) + ) + instrument.spectrumNumberForOutputDiagnosticFiles = ( + utils.nthint(self._getNextToken(), 0) + ) + + instrument.neutronScatteringParametersFile = ( + utils.firstword(self._getNextToken()) + + ) + instrument.scaleSelection = ( + enums.Scales[enums.Scales( + utils.nthint(self._getNextToken(), 0)).name] + ) + instrument.subWavelengthBinnedData = ( + utils.boolifyNum(utils.nthint(self._getNextToken(), 0)) + ) + self._consumeTokens(2) + instrument.logarithmicStepSize = ( + utils.nthfloat(self._getNextToken(), 0) + ) + instrument.hardGroupEdges = ( + utils.boolifyNum(utils.nthint(self._getNextToken(), 0)) + ) + + # If NeXus files are being used, then we expect a NeXus definition + # file to be present, and extract it. + if ( + instrument.dataFileType == "NXS" + or instrument.dataFileType == "nxs" + ): + instrument.nxsDefinitionFile = ( + utils.firstword(self._getNextToken()) + ) + + if self.config: + instrument.goodDetectorThreshold = utils.nthint( + self._getNextToken(), 0 + ) + + # Consume whitespace and the closing brace. + self._consumeUpToDelim("}") + + # Resolve the paths, to make them relative. + # First construct the regular expression to match against. + pattern = re.compile(r"StartupFiles\S*") + + match = re.search( + pattern, + instrument.detectorCalibrationFileName + ) + + if match: + instrument.detectorCalibrationFileName = match.group() + + match = re.search( + pattern, + instrument.groupFileName + ) + + if match: + instrument.groupFileName = match.group() + + match = re.search( + pattern, + instrument.deadtimeConstantsFileName + ) + + if match: + instrument.deadtimeConstantsFileName = match.group() + + match = re.search( + pattern, + instrument.neutronScatteringParametersFile + ) + + if match: + instrument.neutronScatteringParametersFile = match.group() + + match = re.search( + pattern, + instrument.neutronScatteringParametersFile + ) + + if match: + instrument.neutronScatteringParametersFile = match.group() + + return instrument + + except Exception as e: + raise ParserException( + "Whilst parsing Instrument, an exception occured." + " The input file is most likely of an incorrect format, " + "and some attributes were missing." + f"{str(e)}" + ) from e + + def _parseBeam(self) -> Beam: + """ + Intialises a Beam object and assigns it to the + beam attribute. + Parses the attributes of the Beam from the input stream. + Raises a ParserException if any mandatory attributes are missing. + """ + + try: + # Initialise beam attribute to a new instance of Beam. + beam = Beam() + + self._consumeWhitespace() + + # For enumerated attributes, + # where the member name of the attribute is + # the first 'word' in the line, and we must get the member, + # we do this: Enum[memberName]. + beam.sampleGeometry = enums.Geometry[utils.firstword( + self._getNextToken())] + + # Set the global geometry. + config.geometry = beam.sampleGeometry + + # Ignore the number of beam values. + self._consumeTokens(1) + + # For N float attributes, + # we extract the first N floats from the line. + beam.beamProfileValues = ( + enums.extract_floats_from_string(self._getNextToken()) + ) + + # For single float attributes, + # we extract the zeroth float from the line. + range = self._getNextToken() + beam.stepSizeAbsorption = utils.nthfloat(range, 0) + beam.stepSizeMS = utils.nthfloat(range, 1) + beam.noSlices = utils.nthint(range, 2) + beam.angularStepForCorrections = ( + utils.nthint(self._getNextToken(), 0) + ) + + # Extract the incident beam edges + # relative to the centroid of the sample. + incidentBeamEdges = self._getNextToken() + beam.incidentBeamLeftEdge = utils.nthfloat(incidentBeamEdges, 0) + beam.incidentBeamRightEdge = utils.nthfloat(incidentBeamEdges, 1) + beam.incidentBeamBottomEdge = utils.nthfloat(incidentBeamEdges, 2) + beam.incidentBeamTopEdge = utils.nthfloat(incidentBeamEdges, 3) + + # Extract the scattered beam edges + # relative to the centroid of the sample. + scatteredBeamEdges = self._getNextToken() + beam.scatteredBeamLeftEdge = utils.nthfloat(scatteredBeamEdges, 0) + beam.scatteredBeamRightEdge = utils.nthfloat(scatteredBeamEdges, 1) + beam.scatteredBeamBottomEdge = utils.nthfloat( + scatteredBeamEdges, 2) + beam.scatteredBeamTopEdge = utils.nthfloat(scatteredBeamEdges, 3) + + # For string attributes, + # we simply extract the utils.firstword in the line. + beam.filenameIncidentBeamSpectrumParams = ( + utils.firstword(self._getNextToken()) + ) + + # Now match it against a pattern, + # to resolve the path to be relative. + pattern = re.compile(r"StartupFiles\S*") + + match = re.search( + pattern, + beam.filenameIncidentBeamSpectrumParams + ) + + if match: + beam.filenameIncidentBeamSpectrumParams = match.group() + + beam.overallBackgroundFactor = ( + utils.nthfloat(self._getNextToken(), 0) + ) + beam.sampleDependantBackgroundFactor = ( + utils.nthfloat(self._getNextToken(), 0) + ) + beam.shieldingAttenuationCoefficient = ( + utils.nthfloat(self._getNextToken(), 0) + ) + + # Consume whitespace and the closing brace. + self._consumeUpToDelim("}") + + return beam + + except Exception as e: + raise ParserException( + "Whilst parsing Beam, an exception occured." + " The input file is most likely of an incorrect format, " + "and some attributes were missing." + ) from e + + def _parseNormalisation(self) -> Normalisation: + """ + Intialises a Normalisation object and assigns it to the + normalisation attribute. + Parses the attributes of the Normalisation from the input stream. + Raises a ParserException if any mandatory attributes are missing. + """ + + try: + # Initialise normalisation attribute + # to a new instance of Normalisation. + normalisation = Normalisation() + + self._consumeWhitespace() + + # The number of files and period number are both stored + # on the same line. + # So we extract the 0th integer for the number of files, + # and the 1st integer for the period number. + dataFileInfo = self._getNextToken() + numberOfFiles = utils.nthint(dataFileInfo, 0) + normalisation.periodNumber = utils.nthint(dataFileInfo, 1) + + # Extract params files + dataFiles = [] + for _ in range(numberOfFiles): + dataFiles.append(utils.firstword(self._getNextToken())) + # Sorts list so that it is in ascending order + dataFiles.sort() + + # Create a DataFiles object from the dataFiles list constructed. + normalisation.dataFiles = ( + DataFiles(dataFiles, "NORMALISATION") + ) + + # The number of background files and + # background period number are both stored + # on the same line. + # So we extract the 0th integer for the number of background files, + # and the 1st integer for the background riod number. + dataFileInfoBg = self._getNextToken() + numberOfFilesBg = utils.nthint(dataFileInfoBg, 0) + normalisation.periodNumberBg = utils.nthint(dataFileInfoBg, 1) + + # Extract background params files + dataFilesBg = [] + for j in range(numberOfFilesBg): + dataFilesBg.append(utils.firstword(self._getNextToken())) + + # Sorts list so that it is in ascending order + dataFilesBg.sort() + + # Create a DataFiles object from the dataFiles list constructed. + normalisation.dataFilesBg = ( + DataFiles(dataFilesBg, "NORMALISATION BACKGROUND") + ) + + # For boolean attributes, we convert the first + # integer in the line to its boolean value. + normalisation.forceCalculationOfCorrections = ( + utils.boolifyNum(utils.nthint(self._getNextToken(), 0)) + ) + + # Construct composition + composition = [] + line = self._getNextToken() + # Extract the composition. + # Each element in the composition consists of the first 'word', + # integer at the second position, and float at the third position, + # (Atomic Symbol, MassNo, Abundance) in the line. + # If the marker line is encountered, + # then the panel has been parsed. + while "end of composition input" not in line: + atomicSymbol = utils.firstword(line) + massNo = utils.nthfloat(line, 1) + abundance = utils.nthfloat(line, 2) + + # Create an Element object and append to the composition list. + composition.append( + Element(atomicSymbol, massNo, abundance) + ) + line = self._getNextToken() + + # Create a Composition object from the dataFiles list constructed. + normalisation.composition = ( + Composition("Normalisation", elements=composition) + ) + + # For enumerated attributes, + # where the member name of the attribute is + # the first 'word' in the line, and we must get the member, + # we do this: Enum[memberName]. + normalisation.geometry = ( + enums.Geometry[utils.firstword(self._getNextToken())] + ) + + # Is the geometry FLATPLATE? + if ( + ( + normalisation.geometry == enums.Geometry.SameAsBeam + and config.geometry == enums.Geometry.FLATPLATE + ) + or normalisation.geometry == enums.Geometry.FLATPLATE): + # If is is FLATPLATE, then extract the upstream and downstream + # thickness, the angle of rotation and sample width. + thickness = self._getNextToken() + normalisation.upstreamThickness = utils.nthfloat(thickness, 0) + normalisation.downstreamThickness = ( + utils.nthfloat(thickness, 1) + ) + geometryInfo = self._getNextToken() + normalisation.angleOfRotation = utils.nthfloat(geometryInfo, 0) + normalisation.sampleWidth = utils.nthfloat(geometryInfo, 1) + else: + + # Otherwise, it is CYLINDRICAL, + # then extract the inner and outer + # radii and the sample height. + radii = self._getNextToken() + normalisation.innerRadius = utils.nthfloat(radii, 0) + normalisation.outerRadius = utils.nthfloat(radii, 1) + normalisation.sampleHeight = ( + utils.nthfloat(self._getNextToken(), 0) + ) + + # Extract the density. + density = utils.nthfloat(self._getNextToken(), 0) + + # Take the absolute value of the density - since it could be -ve. + normalisation.density = abs(density) + + # Decide on the units of density. + # -ve density means it is atomic (atoms/A^3) + # +ve means it is chemical (gm/cm^3) + normalisation.densityUnits = ( + enums.UnitsOfDensity.ATOMIC if + density < 0 + else enums.UnitsOfDensity.CHEMICAL + ) + + normalisation.tempForNormalisationPC = ( + utils.nthfloat(self._getNextToken(), 0) + ) + crossSectionSource = utils.firstword(self._getNextToken()) + if ( + crossSectionSource == "TABLES" + or crossSectionSource == "TRANSMISSION" + ): + normalisation.totalCrossSectionSource = ( + enums.CrossSectionSource[crossSectionSource] + ) + else: + normalisation.totalCrossSectionSource = ( + enums.CrossSectionSource.FILE + ) + normalisation.crossSectionFilename = crossSectionSource + + normalisation.normalisationDifferentialCrossSectionFile = ( + utils.firstword(self._getNextToken()) + ) + + normalisation.lowerLimitSmoothedNormalisation = ( + utils.nthfloat(self._getNextToken(), 0) + ) + normalisation.normalisationDegreeSmoothing = ( + utils.nthfloat(self._getNextToken(), 0) + ) + normalisation.minNormalisationSignalBR = ( + utils.nthfloat(self._getNextToken(), 0) + ) + + # Consume whitespace and the closing brace. + self._consumeUpToDelim("}") + + # Resolve to relative. + pattern = re.compile(r"StartupFiles\S*") + + match = re.search( + pattern, + normalisation. + normalisationDifferentialCrossSectionFile + ) + + if match: + ( + normalisation. + normalisationDifferentialCrossSectionFile + ) = match.group() + + return normalisation + + except Exception as e: + raise ParserException( + "Whilst parsing Normalisation, an exception occured." + " The input file is most likely of an incorrect format, " + "and some attributes were missing." + ) from e + + def _parseSampleBackground(self, _) -> SampleBackground: + """ + Intialises a SampleBackground object. + Parses the attributes of the SampleBackground from the input stream. + Raises a ParserException if any mandatory attributes are missing. + Returns the parsed object. + + Parameters + ---------- + None + Returns + ------- + sampleBackground : SampleBackground + The SampleBackground that was parsed from the input lines. + """ + + try: + sampleBackground = SampleBackground() + line = self._peekNextToken() + if "SAMPLE BACKGROUND" in line and "{" in line: + self._consumeTokens(1) + self._consumeWhitespace() + dataFileInfo = self._getNextToken() + numberOfFiles = utils.nthint(dataFileInfo, 0) + sampleBackground.periodNumber = utils.nthint(dataFileInfo, 1) + + dataFiles = [] + for _ in range(numberOfFiles): + dataFiles.append(utils.firstword(self._getNextToken())) + sampleBackground.dataFiles = ( + DataFiles(dataFiles, "SAMPLE BACKGROUND") + ) + + # Consume whitespace and the closing brace. + self._consumeUpToDelim("}") + + return sampleBackground + except Exception as e: + raise ParserException( + "Whilst parsing Sample Background, an exception occured." + " The input file is most likely of an incorrect format, " + "and some attributes were missing." + ) from e + + def _parseSample(self, _) -> Sample: + """ + Intialises a Sample object. + Parses the attributes of the Sample from the input stream. + Raises a ParserException if any mandatory attributes are missing. + Returns the parsed object. + """ + + try: + # Create a new instance of Sample. + sample = Sample() + + # Extract the sample name, and then discard whitespace lines. + sample.name = ( + str(self._getNextToken()[:-2]).strip() + .replace("SAMPLE", "").strip() + ) + sample.name = utils.replace_unwanted_chars(sample.name) + print(sample.name) + self._consumeWhitespace() + # The number of files and period number are both stored + # on the same line. + # So we extract the 0th integer for the number of files, + # and the 1st integer for the period number. + dataFileInfo = self._getNextToken() + numberOfFiles = utils.nthint(dataFileInfo, 0) + sample.periodNumber = utils.nthint(dataFileInfo, 1) + + # Extract params files + dataFiles = [] + for _ in range(numberOfFiles): + dataFiles.append(utils.firstword(self._getNextToken())) + # Create a DataFiles object from the dataFiles list constructed. + sample.dataFiles = DataFiles(dataFiles, sample.name) + + # For boolean attributes, we convert the first + # integer in the line to its boolean value. + sample.forceCalculationOfCorrections = ( + utils.boolifyNum(utils.nthint(self._getNextToken(), 0)) + ) + + # Construct composition + composition = [] + line = self._getNextToken() + + # Extract the composition. + # Each element in the composition consists of the first 'word', + # integer at the second position, and float t the first position, + # (Atomic Symbol, MassNo, Abundance) in the line. + # If the marker line is encountered, + # then the panel has been parsed. + while "end of composition input" not in line: + + atomicSymbol = utils.firstword(line) + massNo = utils.nthfloat(line, 1) + abundance = utils.nthfloat(line, 2) + + # Create an Element object and append to the composition list. + composition.append(Element(atomicSymbol, massNo, abundance)) + line = self._getNextToken() + + # Create a Composition object from the dataFiles list constructed. + sample.composition = Composition("Sample", elements=composition) + + # For enumerated attributes, + # where the member name of the attribute is + # the first 'word' in the line, and we must get the member, + # we do this: Enum[memberName]. + sample.geometry = enums.Geometry[utils.firstword( + self._getNextToken())] + + # Is the geometry FLATPLATE? + if ( + ( + sample.geometry == enums.Geometry.SameAsBeam + and config.geometry == enums.Geometry.FLATPLATE + ) + or sample.geometry == enums.Geometry.FLATPLATE): + # If is is FLATPLATE, then extract the upstream and downstream + # thickness, the angle of rotation and sample width. + thickness = self._getNextToken() + sample.upstreamThickness = utils.nthfloat(thickness, 0) + sample.downstreamThickness = utils.nthfloat(thickness, 1) + + geometryInfo = self._getNextToken() + sample.angleOfRotation = utils.nthfloat(geometryInfo, 0) + sample.sampleWidth = utils.nthfloat(geometryInfo, 1) + else: + + # Otherwise, it is CYLINDRICAL, + # then extract the inner and outer + # radii and the sample height. + radii = self._getNextToken() + sample.innerRadius = utils.nthfloat(radii, 0) + sample.outerRadius = utils.nthfloat(radii, 1) + sample.sampleHeight = utils.nthfloat(self._getNextToken(), 0) + + # Extract the density. + density = utils.nthfloat(self._getNextToken(), 0) + + # Decide on the units of density. + # -ve density means it is atomic (atoms/A^3) + # +ve means it is chemical (gm/cm^3) + sample.density = abs(density) + sample.densityUnits = ( + enums.UnitsOfDensity.ATOMIC if + density < 0 + else enums.UnitsOfDensity.CHEMICAL + ) + sample.tempForNormalisationPC = utils.nthfloat( + self._getNextToken(), 0) + crossSectionSource = utils.firstword(self._getNextToken()) + if ( + crossSectionSource == "TABLES" + or crossSectionSource == "TRANSMISSION" + ): + sample.totalCrossSectionSource = ( + enums.CrossSectionSource[crossSectionSource] + ) + else: + sample.totalCrossSectionSource = enums.CrossSectionSource.FILE + sample.crossSectionFilename = crossSectionSource + sample.sampleTweakFactor = utils.nthfloat(self._getNextToken(), 0) + + topHatW = utils.nthfloat(self._getNextToken(), 0) + if topHatW == 0: + sample.topHatW = 0 + sample.FTMode = FTModes.NO_FT + elif topHatW < 0: + sample.topHatW = abs(topHatW) + sample.FTMode = FTModes.SUB_AVERAGE + else: + sample.topHatW = topHatW + sample.FTMode = FTModes.ABSOLUTE + + sample.minRadFT = utils.nthfloat(self._getNextToken(), 0) + sample.grBroadening = utils.nthfloat(self._getNextToken(), 0) + + # Extract the resonance values. + # Each row consists of the first 2 floats. + # (minWavelength, maxWavelength) in the line. + # If the marker line is encountered, + # then the values has been parsed. + line = self._getNextToken() + while ( + "to finish specifying wavelength range of resonance" + not in line + ): + sample.resonanceValues.append( + enums.extract_floats_from_string(line) + ) + line = self._getNextToken() + + # Extract the exponential values. + # Each row consists of the first 3 numbers. + # (Amplitude, Decay, N) in the line. + # If the marker line is encountered, + # then the values has been parsed. + line = self._getNextToken() + if "to specify end of exponential parameter input" not in line: + sample.exponentialValues = [] + while "to specify end of exponential parameter input" not in line: + sample.exponentialValues.append( + utils.extract_nums_from_string(line) + ) + + line = self._getNextToken() + + sample.normalisationCorrectionFactor = ( + utils.nthfloat(self._getNextToken(), 0) + ) + sample.fileSelfScattering = utils.firstword(self._getNextToken()) + sample.normaliseTo = ( + enums.NormalisationType[ + enums.NormalisationType(utils.nthint( + self._getNextToken(), 0)).name + ] + ) + sample.maxRadFT = utils.nthfloat(self._getNextToken(), 0) + sample.outputUnits = ( + enums.OutputUnits[enums.OutputUnits( + utils.nthint(self._getNextToken(), 0)).name] + ) + sample.powerForBroadening = utils.nthfloat(self._getNextToken(), 0) + sample.stepSize = utils.nthfloat(self._getNextToken(), 0) + sample.runThisSample = utils.boolifyNum( + utils.nthint(self._getNextToken(), 0)) + environmentValues = self._getNextToken() + sample.scatteringFraction = utils.nthfloat(environmentValues, 0) + sample.attenuationCoefficient = utils.nthfloat( + environmentValues, 1) + + # Consume whitespace and the closing brace. + self._consumeUpToDelim("}") + + return sample + + except Exception as e: + raise ParserException( + "Whilst parsing Sample, an exception occured." + " The input file is most likely of an incorrect format, " + "and some attributes were missing." + ) from e + + def _parseContainer(self, _) -> Container: + """ + Intialises a Container object. + Parses the attributes of the Container from the input stream. + Raises a ParserException if any mandatory attributes are missing. + Returns the parsed object. + """ + + try: + # Create a new instance of Container. + container = Container() + + # Extract the name from the lines, + # and then discard the unnecessary lines. + container.name = ( + str(self._getNextToken()[:-2]).strip() + .replace("CONTAINER", "").strip() + ) + self._consumeWhitespace() + + # The number of files and period number are both stored + # on the same line. + # So we extract the 0th integer for the number of files, + # and the 1st integer for the period number. + dataFileInfo = self._getNextToken() + numberOfFiles = utils.nthint(dataFileInfo, 0) + container.periodNumber = utils.nthint(dataFileInfo, 1) + + # Extract params files + dataFiles = [] + for _ in range(numberOfFiles): + dataFiles.append(utils.firstword(self._getNextToken())) + + # Create a DataFiles object from the dataFiles list constructed. + container.dataFiles = DataFiles(dataFiles, container.name) + + # Construct composition + composition = [] + line = self._getNextToken() + # Extract the composition. + # Each element in the composition consists of the first 'word', + # integer at the second position, and float t the first position, + # (Atomic Symbol, MassNo, Abundance) in the line. + # If the marker line is encountered, + # then the panel has been parsed. + while "end of composition input" not in line: + + atomicSymbol = utils.firstword(line) + massNo = utils.nthfloat(line, 1) + abundance = utils.nthfloat(line, 2) + + # Create an Element object and append to the composition list. + composition.append(Element(atomicSymbol, massNo, abundance)) + line = self._getNextToken() + # Create a Composition object from the dataFiles list constructed. + container.composition = Composition( + "Container", + elements=composition + ) + + # For enumerated attributes, + # where the member name of the attribute is + # the first 'word' in the line, and we must get the member, + # we do this: Enum[memberName]. + container.geometry = enums.Geometry[utils.firstword( + self._getNextToken())] + + # Is the geometry FLATPLATE? + if ( + ( + container.geometry == enums.Geometry.SameAsBeam + and config.geometry == enums.Geometry.FLATPLATE + ) + or container.geometry == enums.Geometry.FLATPLATE): + # If is is FLATPLATE, then extract the upstream and downstream + # thickness, the angle of rotation and sample width. + thickness = self._getNextToken() + container.upstreamThickness = utils.nthfloat(thickness, 0) + container.downstreamThickness = utils.nthfloat(thickness, 1) + + geometryValues = self._getNextToken() + container.angleOfRotation = utils.nthfloat(geometryValues, 0) + container.sampleWidth = utils.nthfloat(geometryValues, 1) + else: + + # Otherwise, it is CYLINDRICAL, + # then extract the inner and outer + # radii and the sample height. + radii = self._getNextToken() + container.innerRadius = utils.nthfloat(radii, 0) + container.outerRadius = utils.nthfloat(radii, 1) + container.sampleHeight = utils.nthfloat( + self._getNextToken(), 0) + + # Extract the density. + density = utils.nthfloat(self._getNextToken(), 0) + + # Take the absolute value of the density - since it could be -ve. + container.density = abs(density) + + # Decide on the units of density. + # -ve density means it is atomic (atoms/A^3) + # +ve means it is chemical (gm/cm^3) + container.densityUnits = ( + enums.UnitsOfDensity.ATOMIC if + density < 0 + else enums.UnitsOfDensity.CHEMICAL + ) + crossSectionSource = utils.firstword(self._getNextToken()) + if ( + crossSectionSource == "TABLES" + or crossSectionSource == "TRANSMISSION" + ): + container.totalCrossSectionSource = ( + enums.CrossSectionSource[crossSectionSource] + ) + else: + container.totalCrossSectionSource = enums.CrossSectionSource.FILE + container.crossSectionFilename = crossSectionSource + container.tweakFactor = utils.nthfloat(self._getNextToken(), 0) + + environmentValues = self._getNextToken() + container.scatteringFraction = utils.nthfloat(environmentValues, 0) + container.attenuationCoefficient = utils.nthfloat( + environmentValues, 1) + + # Consume whitespace and the closing brace. + self._consumeUpToDelim("}") + + return container + + except Exception as e: + raise ParserException( + "Whilst parsing Container, an exception occured." + " The input file is most likely of an incorrect format, " + "and some attributes were missing." + ) from e + + def _parseComponents(self) -> Components: + components = Components + try: + while self.stream: + component = self._parseComponent() + if component: + components.addComponent(component) + return components + except Exception as e: + raise ParserException( + "Whilst parsing Components, an exception occured." + " The input file is most likely of an incorrect format." + ) from e + + def _parseComponent(self) -> Component: + name = self._getNextToken().rstrip() + component = Component(name) + line = self._peekNextToken() + if "(" in line: + self._consumeTokens(1) + else: + return + line = self._getNextToken() + while line and ")" not in line: + atomicSymbol, massNo, abundance = line.split() + element = Element(atomicSymbol, float(massNo), float(abundance)) + component.addElement(element) + line = self._getNextToken() + return component + + def _sampleBackgroundHelper(self) -> SampleBackground: + """ + Helper method for parsing Sample Background and its + Samples and their Containers. + Returns the SampleBackground object. + Parameters + ---------- + None + Returns + ------- + SampleBackground + The SampleBackground parsed from the lines. + """ + + # Parse sample background. + sampleBackground = self.makeParse("SAMPLE BACKGROUND") + + self._consumeWhitespace() + line = self._peekNextToken() + + # Parse all Samples and Containers belonging to the sample background. + while "END" not in line and "SAMPLE BACKGROUND" not in line: + if not line: + raise ParserException("Unexpected EOF during parsing.") + elif "GO" in line: + self._getNextToken() + elif "SAMPLE" in line and utils.firstword(line) == "SAMPLE": + sample = self.makeParse("SAMPLE") + if not sample.name: + sample.name = utils.replace_unwanted_chars( + utils.uniquifyName( + "SAMPLE", + [s.name for s in sampleBackground.samples], + sep="", + incFirst=True + )) + sampleBackground.samples.append(sample) + elif "CONTAINER" in line and utils.firstword(line) == "CONTAINER": + container = self.makeParse("CONTAINER") + if not container.name: + container.name = utils.uniquifyName( + "CONTAINER", + [c.name + for c in sampleBackground.samples[-1].containers], + sep="", + incFirst=True) + sampleBackground.samples[-1].containers.append( + container + ) + self._consumeWhitespace() + line = self._peekNextToken() + return sampleBackground + + +Container._getNextToken = TxtParser._getNextToken +Container._peekNextToken = TxtParser._peekNextToken +Container._consumeUpToDelim = TxtParser._consumeUpToDelim +Container._consumeWhitespace = TxtParser._consumeWhitespace diff --git a/gudpy/core/purge_file.py b/gudpy/core/purge_file.py index ef5e44c89..21d4ac91a 100644 --- a/gudpy/core/purge_file.py +++ b/gudpy/core/purge_file.py @@ -10,7 +10,6 @@ class PurgeFile: """ Class to represent a PurgeFile. - ... Attributes From c369c023e04d0ad3a509436650f82fdb253763da Mon Sep 17 00:00:00 2001 From: Noella Spitz Date: Wed, 15 May 2024 11:09:20 +0100 Subject: [PATCH 41/78] Update functions --- gudpy/core/file_library.py | 5 +- gudpy/core/gud_file.py | 26 ---------- gudpy/core/gudpy.py | 59 +++++----------------- gudpy/core/gudrun_file.py | 19 ------- gudpy/core/io/gudpy_io.py | 41 +++++++++++---- gudpy/core/io/txt_parser.py | 9 ++-- gudpy/core/purge_file.py | 28 ----------- gudpy/test/test_gud_file.py | 5 +- gudpy/test/test_gudpy_io.py | 94 +++++++++++++++-------------------- gudpy/test/test_purge_file.py | 21 ++++---- 10 files changed, 103 insertions(+), 204 deletions(-) diff --git a/gudpy/core/file_library.py b/gudpy/core/file_library.py index 1781b82c2..65871a45e 100644 --- a/gudpy/core/file_library.py +++ b/gudpy/core/file_library.py @@ -4,6 +4,7 @@ from core import utils from core.enums import CrossSectionSource +from core.io.gudpy_io import GudPyIO class GudPyFileLibrary: @@ -202,9 +203,7 @@ def exportMintData( sample.sampleFile = path if not os.path.exists(path): - sample.write_out( - self.gudrunFile.projectDir - ) + GudPyIO.writeObject(sample, path) zipFile.write(path, arcname=os.path.basename(path)) return zipFile.filename diff --git a/gudpy/core/gud_file.py b/gudpy/core/gud_file.py index 8a430dfa3..53c4e3e3e 100644 --- a/gudpy/core/gud_file.py +++ b/gudpy/core/gud_file.py @@ -71,13 +71,6 @@ class GudFile: Contents of the .gud file. output : str Output for use in the GUI. - Methods - ------- - parse(): - Parses the GudFile from path, assigning values - to each of the attributes. - write_out(overwrite=False) - Writes out the string representation of the GudFile to a file. """ def __init__(self, path): @@ -350,22 +343,3 @@ def __str__(self): f'{self.suggestedTweakFactor}\n' ) - - def write_out(self, path): - """ - Writes out the string representation of the GudFile. - If 'overwrite' is True, then the initial file is overwritten. - Otherwise, it is written to 'gudpy_{initial filename}.gud'. - - Parameters - ---------- - overwrite : bool, optional - Overwrite the initial file? (default is False). - - Returns - ------- - None - """ - f = open(path, "w", encoding="utf-8") - f.write(str(self)) - f.close() diff --git a/gudpy/core/gudpy.py b/gudpy/core/gudpy.py index 957ac7981..2df8bf25e 100644 --- a/gudpy/core/gudpy.py +++ b/gudpy/core/gudpy.py @@ -16,7 +16,7 @@ import core.output_file_handler as handlers from core.file_library import GudPyFileLibrary from core import data_files -from core.io import txt_parser, gudpy_io +from core.io.gudpy_io import GudPyIO SUFFIX = ".exe" if os.name == "nt" else "" @@ -27,7 +27,7 @@ def __init__( ): self.originalGudrunFile: GudrunFile = None self._gudrunFile: GudrunFile = None - self.io = gudpy_io.GudpyIO() + self.io = GudPyIO() self.purge: Purge = None self.gudrun: Gudrun = None @@ -72,8 +72,7 @@ def loadFromGudrunFile( if not os.path.exists(loadFile): raise FileNotFoundError("Input file does not exist.") - self.gudrunFileParser = txt_parser.GudrunFileParser() - self.gudrunFile = self.gudrunFileParser.parse(loadFile, config) + self.gudrunFile = self.io.importGudrunFile(loadFile, config) self.originalGudrunFile = copy.deepcopy(self.gudrunFile) self.originalGudrunFile.filename = "original" @@ -105,7 +104,7 @@ def checkSaveLocation(self): bool If the save location is set or not """ - return bool(self.projectDir) + return self.io.checkSaveLocation() def setSaveLocation(self, projectDir: str): """Sets the save location @@ -115,28 +114,12 @@ def setSaveLocation(self, projectDir: str): projectDir : str Path to the desired save location of project """ - self.projectDir = projectDir - self.gudrunFile.filename = f"{os.path.basename(projectDir)}" - self.gudrunFile.projectDir = projectDir - self.originalGudrunFile.projectDir = projectDir - self.autosaveLocation = ( - f"{os.path.basename(projectDir)}.autosave" - ) + self.io.setSaveLocation(projectDir) - def save(self, path: str = "", format: enums.Format = enums.Format.YAML): + def save(self): """Saves current GudPy input file - - Parameters - ---------- - path : str, optional - Path to desired save location, by default "" - format : enums.Format, optional - Desired format of save file, by default enums.Format.YAML """ - if not path: - path = self.gudrunFile.path() - self.originalGudrunFile.save(path=path) - self.gudrunFile.save(path=path, format=format) + self.io.save() def saveAs(self, targetDir: str): """Save GudPy project to a new location, set current @@ -152,26 +135,7 @@ def saveAs(self, targetDir: str): IsADirectoryError Raised if the requested save location already exists """ - if os.path.exists(targetDir): - raise IsADirectoryError("Cannot be an existing directory") - - oldDir = self.projectDir - self.setSaveLocation(targetDir) - os.makedirs(targetDir) - - if os.path.exists(os.path.join(oldDir, "Purge")): - shutil.copytree( - os.path.join(oldDir, "Purge"), - os.path.join(targetDir, "Purge") - ) - if os.path.exists(os.path.join(oldDir, "Gudrun")): - shutil.copytree( - os.path.join(oldDir, "Gudrun"), - os.path.join(targetDir, "Gudrun") - ) - self.gudrunFile.filename = os.path.basename(targetDir) - self.gudrunFile.save(path=self.gudrunFile.path(), - format=enums.Format.YAML) + self.io.exportProject(targetDir) self.loadFromProject(projectDir=targetDir) def checkFilesExist(self): @@ -216,7 +180,7 @@ def runPurge(self): self.purge = Purge() purgeFile = PurgeFile(self.gudrunFile) exitcode = self.purge.purge(purgeFile) - self.gudrunFile.save() + self.io.save() if exitcode: raise exc.PurgeException( "Purge failed to run with the following output:\n" @@ -240,6 +204,7 @@ def runGudrun(self, gudrunFile: GudrunFile = None): if not gudrunFile: gudrunFile = self.gudrunFile + self.io.save() self.gudrun = Gudrun() exitcode = self.gudrun.gudrun(gudrunFile=gudrunFile) if exitcode: @@ -420,7 +385,7 @@ def organiseOutput(self, procDir: str, projectDir: str): def purge(self, purgeFile: PurgeFile): self.checkBinary() with tempfile.TemporaryDirectory() as tmp: - purgeFile.write_out(os.path.join( + GudPyIO.writeObject(purgeFile, os.path.join( tmp, f"{self.PROCESS}.dat" )) @@ -501,7 +466,7 @@ def gudrun( gudrunFile.OUTPATH ) assert (gudrunFile.path() is not None) - gudrunFile.write_out(path) + GudPyIO.writeGudrunFile(path) with subprocess.Popen( [self.BINARY_PATH, path], cwd=tmp, stdout=subprocess.PIPE, diff --git a/gudpy/core/gudrun_file.py b/gudpy/core/gudrun_file.py index 963e540f3..8937f8a0a 100644 --- a/gudpy/core/gudrun_file.py +++ b/gudpy/core/gudrun_file.py @@ -23,10 +23,6 @@ class GudrunFile: Attributes ---------- - path : str - Path to the file. - OUTPATH : str - Path to write to, when not overwriting the initial file. instrument : Instrument Instrument object extracted from the input file. beam : Beam @@ -35,11 +31,6 @@ class GudrunFile: Normalisation object extracted from the input file. sampleBackgrounds : SampleBackground[] List of SampleBackgrounds extracted from the input file. - purged : bool - Have the detectors been purged? - stream : str[] - List of strings, where each item represents a line - in the input stream. Methods ------- getNextToken(): @@ -85,20 +76,10 @@ class GudrunFile: sampleBackgroundHelper(): Parses the SampleBackground, its Samples and their Containers. Returns the SampleBackground object. - parse(): - Parse the GudrunFile from its path. - Assign objects from the file to the attributes of the class. - write_out(overwrite=False) - Writes out the string representation of the GudrunFile to a file. - purge(): - Create a PurgeFile from the GudrunFile, and run purge_det on it. """ def __init__( self, - projectDir=None, - loadFile=None, - config=False, params: tuple[ Instrument, Normalisation, diff --git a/gudpy/core/io/gudpy_io.py b/gudpy/core/io/gudpy_io.py index fb42d37f6..a48b2183a 100644 --- a/gudpy/core/io/gudpy_io.py +++ b/gudpy/core/io/gudpy_io.py @@ -2,6 +2,7 @@ from enum import Enum from ruamel.yaml import YAML as yaml from ruamel.yaml import YAMLError +import shutil import os from core.composition import ( @@ -23,12 +24,10 @@ from core import config -class GudpyIO: +class GudPyIO: def __init__(self): - # Construct the outpath of generated input file self.projectDir = "" self.loadFile = "" - self.autosaveLocation = "" self.yaml = self._getYamlModule() self.gudrunFileParser = GudrunFileParser() @@ -39,7 +38,7 @@ def projectName(self) -> str: @property def autosavePath(self) -> str: - return f"{os.path.basename(projectDir)}.autosave" + return f"{os.path.basename(self.projectDir)}.autosave" @property def path(self): @@ -79,8 +78,9 @@ def save(self): """ self.writeYaml(self.path) - def importGudrunFile(self, path) -> GudrunFile: - return self.gudrunFileParser.parse(path) + def importGudrunFile(self, path, config=False) -> GudrunFile: + self.loadFile = path + return self.gudrunFileParser.parse(path, config) def importProject(self, projectDir) -> GudrunFile: """Imports from a project directory @@ -125,12 +125,31 @@ def importProject(self, projectDir) -> GudrunFile: self.setSaveLocation(projectDir) return self._parseYaml(loadFile) - def exportGudrunFile(self, path): - return self.gudrunFileParser.export(path) + def exportProject(self, targetDir): + if os.path.exists(targetDir): + raise IsADirectoryError("Cannot be an existing directory") + + oldFile = os.path.join(targetDir, os.path.basename(self.loadFile)) + oldDir = self.projectDir + self.setSaveLocation(targetDir) + os.makedirs(targetDir) + + shutil.copytree(oldDir, targetDir) + os.rename(oldFile, self.path) + + @classmethod + def exportGudrunFile(cls, path): + return GudrunFileParser.export(path) + + @classmethod + def writeGudrunFile(cls, gudrunFile, path): + GudrunFileParser.writeGudrunFile(gudrunFile, path) - def writeGudrunFile(self, path): - self. - return self.gudrunFileParser.writeGudrunFile(path) + @classmethod + def writeObject(cls, obj, path): + with open(path, "w", encoding="utf-8") as f: + f.write(str(obj)) + f.close() # ================================================================= # PARSING HELPERS diff --git a/gudpy/core/io/txt_parser.py b/gudpy/core/io/txt_parser.py index bcac4a815..50427c24e 100644 --- a/gudpy/core/io/txt_parser.py +++ b/gudpy/core/io/txt_parser.py @@ -29,9 +29,10 @@ def __init__(self): # Text stream self.stream = None - def writeGudrunFile(self, gudrunFile: GudrunFile, runDir: str): + @classmethod + def writeGudrunFile(cls, gudrunFile: GudrunFile, runDir: str): gudrunFile.setGudrunDir(runDir) - for sampleBackground in self.sampleBackgrounds: + for sampleBackground in gudrunFile.sampleBackgrounds: sampleBackground.writeAllSamples = False writePath = os.path.join(runDir, GudrunFileParser.OUTPATH) @@ -41,7 +42,7 @@ def writeGudrunFile(self, gudrunFile: GudrunFile, runDir: str): f.close() def export(self, gudrunFile: GudrunFile, path: str): - for sampleBackground in self.sampleBackgrounds: + for sampleBackground in gudrunFile.sampleBackgrounds: sampleBackground.writeAllSamples = True with open(path, "w", encoding="utf-8") as f: @@ -59,8 +60,6 @@ def parse( Raises ParserException if Instrument, Beam or Normalisation are missing. """ - self.config = config - instrument = None beam = None components = None diff --git a/gudpy/core/purge_file.py b/gudpy/core/purge_file.py index 21d4ac91a..3da033984 100644 --- a/gudpy/core/purge_file.py +++ b/gudpy/core/purge_file.py @@ -24,12 +24,6 @@ class PurgeFile: standard deviation. ignoreBad : bool Ignore any existing bad spectrum files (spec.bad, spec.dat)? - Methods - ------- - write_out() - Writes out the string representation of the PurgeFile to purge_det.dat - purge() - Writes out the file, and then calls purge_det on that file. """ def __init__( @@ -52,28 +46,6 @@ def __init__( self.standardDeviation = standardDeviation self.ignoreBad = ignoreBad - def write_out(self, path=""): - """ - Writes out the string representation of the PurgeFile to - purge_det.dat. - - Parameters - ---------- - None - Returns - ------- - None - """ - # Write out the string representation of the PurgeFile - # To purge_det.dat. - if not path: - f = open("purge_det.dat", "w", encoding="utf-8") - f.write(str(self)) - else: - f = open(path, "w", encoding="utf-8") - f.write(str(self)) - f.close() - def __str__(self): """ Returns the string representation of the PurgeFile object. diff --git a/gudpy/test/test_gud_file.py b/gudpy/test/test_gud_file.py index a4480d101..9b22af6a3 100644 --- a/gudpy/test/test_gud_file.py +++ b/gudpy/test/test_gud_file.py @@ -4,6 +4,7 @@ from core.exception import ParserException from core.gud_file import GudFile from core import gudpy +from core.io.gudpy_io import GudPyIO from test.test_gudpy_workflows import GudPyContext @@ -337,7 +338,7 @@ def testWriteGudFileA(self): gudpy.projectDir, gf.fname ) - gf.write_out(path) + GudPyIO.writeObject(gf, path) gf1 = GudFile(path) dicA = gf.__dict__ @@ -363,7 +364,7 @@ def testWriteGudFileB(self): gudpy.projectDir, gf.fname ) - gf.write_out(path) + GudPyIO.writeObject(gf, path) gf1 = GudFile(path) dicA = gf.__dict__ diff --git a/gudpy/test/test_gudpy_io.py b/gudpy/test/test_gudpy_io.py index 591a62740..3cb60eb54 100644 --- a/gudpy/test/test_gudpy_io.py +++ b/gudpy/test/test_gudpy_io.py @@ -22,9 +22,10 @@ from core.enums import ( CrossSectionSource, FTModes, Instruments, Scales, UnitsOfDensity, MergeWeights, NormalisationType, OutputUnits, - Geometry, Format + Geometry ) from core import gudpy as gp +from core.io.gudpy_io import GudPyIO class GudPyContext: @@ -37,17 +38,15 @@ def __init__(self): self.gudpy = gp.GudPy() - self.gudpy.loadFromFile( + self.gudpy.loadFromGudrunFile( loadFile=path, - format=Format.TXT ) gPath = os.path.join(self.tempdir.name, "good_water.txt") - self.gudpy.gudrunFile.write_out(gPath, overwrite=True) + GudPyIO.writeGudrunFile(gPath) - self.gudpy.loadFromFile( + self.gudpy.loadFromGudrunFile( loadFile=gPath, - format=Format.TXT ) self.gudpy.setSaveLocation(os.path.join( @@ -806,10 +805,9 @@ def testLoadGudrunFile(self): def testWriteGudrunFile(self): with GudPyContext() as gudpy: - gudpy.gudrunFile.write_out( - gudpy.gudrunFile.loadFile, overwrite=True) + GudPyIO.writeGudrunFile(gudpy.io.loadFile) with open( - gudpy.gudrunFile.loadFile, + gudpy.io.loadFile, encoding="utf-8" ) as f: outlines = "\n".join(f.readlines()[:-5]) @@ -872,7 +870,7 @@ def valueInLines(value, lines): else: valueInLines(value, outlines) inlines = "" - with open(gudpy.gudrunFile.loadFile, encoding="utf-8") as f: + with open(gudpy.io.loadFile, encoding="utf-8") as f: inlines = f.read() for dic in self.dicts: for value in dic.values(): @@ -896,19 +894,15 @@ def valueInLines(value, lines): def testRewriteGudrunFile(self): with GudPyContext() as gudpy: - gudpy.gudrunFile.write_out( - gudpy.gudrunFile.loadFile, overwrite=True) + GudPyIO.writeGudrunFile(gudpy.io.loadFile) copyPath = os.path.join( gudpy.gudrunFile.instrument.GudrunInputFileDir, "copyGF.txt" ) - g1 = GudrunFile( - loadFile=gudpy.gudrunFile.loadFile, - format=Format.TXT - ) + g1 = gudpy.io.importGudrunFile(gudpy.io.loadFile) g1.instrument.GudrunInputFileDir = ( gudpy.gudrunFile.instrument.GudrunInputFileDir) - g1.write_out(copyPath, overwrite=True) + GudPyIO.writeGudrunFile(copyPath) def compareString(string1, string2): return string1 == string2 @@ -937,7 +931,7 @@ def compareString(string1, string2): with open( os.path.join( - gudpy.gudrunFile.loadFile + gudpy.io.loadFile ), encoding="utf-8" ) as fg: @@ -950,12 +944,8 @@ def compareString(string1, string2): def testReloadGudrunFile(self): with GudPyContext() as gudpy: - gudpy.gudrunFile.write_out( - gudpy.gudrunFile.loadFile, overwrite=True) - g1 = GudrunFile( - loadFile=gudpy.gudrunFile.loadFile, - format=Format.TXT - ) + GudPyIO.writeGudrunFile(gudpy.io.loadFile) + g1 = gudpy.io.importGudrunFile() g1.instrument.GudrunInputFileDir = ( gudpy.gudrunFile.instrument.GudrunInputFileDir) self.assertEqual( @@ -967,7 +957,7 @@ def testLoadEmptyGudrunFile(self): f = open("test_data.txt", "w", encoding="utf-8") f.close() with self.assertRaises(ParserException) as cm: - GudrunFile(loadFile="test_data.txt", format=Format.TXT) + GudPyIO.importGudrunFile("test_data.txt") self.assertEqual(( 'INSTRUMENT, BEAM and NORMALISATION' ' were not parsed. It\'s possible the file' @@ -987,7 +977,7 @@ def testLoadMissingInstrument(self): ) with self.assertRaises(ParserException) as cm: - GudrunFile(loadFile="test_data.txt", format=Format.TXT) + GudPyIO.importGudrunFile("test_data.txt") self.assertEqual(( 'INSTRUMENT, BEAM and NORMALISATION' ' were not parsed. It\'s possible the file' @@ -1009,7 +999,7 @@ def testLoadMissingBeam(self): + "\n\n}" ) with self.assertRaises(ParserException) as cm: - GudrunFile(loadFile="test_data.txt", format=Format.TXT) + GudPyIO.importGudrunFile("test_data.txt") self.assertEqual(( 'INSTRUMENT, BEAM and NORMALISATION' ' were not parsed. It\'s possible the file' @@ -1028,7 +1018,7 @@ def testLoadMissingNormalisation(self): f.write("BEAM {\n\n" + str(self.goodBeam) + "\n\n}\n\n") with self.assertRaises(ParserException) as cm: - GudrunFile(loadFile="test_data.txt", format=Format.TXT) + GudPyIO.importGudrunFile("test_data.txt") self.assertEqual(( 'INSTRUMENT, BEAM and NORMALISATION' ' were not parsed. It\'s possible the file' @@ -1047,7 +1037,7 @@ def testLoadMissingInstrumentAndBeam(self): ) with self.assertRaises(ParserException) as cm: - GudrunFile(loadFile="test_data.txt", format=Format.TXT) + GudPyIO.importGudrunFile("test_data.txt") self.assertEqual(( 'INSTRUMENT, BEAM and NORMALISATION' ' were not parsed. It\'s possible the file supplied' @@ -1061,7 +1051,7 @@ def testLoadMissingInstrumentAndNormalisation(self): f.write("BEAM {\n\n" + str(self.goodBeam) + "\n\n}") with self.assertRaises(ParserException) as cm: - GudrunFile(loadFile="test_data.txt", format=Format.TXT) + GudPyIO.importGudrunFile("test_data.txt") self.assertEqual(( 'INSTRUMENT, BEAM and NORMALISATION' ' were not parsed. It\'s possible the file' @@ -1078,7 +1068,7 @@ def testLoadMissingNormalisationAndBeam(self): ) with self.assertRaises(ParserException) as cm: - GudrunFile(loadFile="test_data.txt", format=Format.TXT) + GudPyIO.importGudrunFile("test_data.txt") self.assertEqual(( 'INSTRUMENT, BEAM and NORMALISATION' ' were not parsed. It\'s possible the file' @@ -1099,7 +1089,6 @@ def testLoadMissingInstrumentAttributesSeq(self): expectedInstrument.pop("goodDetectorThreshold", None) expectedInstrument.pop("yamlignore", None) for i in range(len(expectedInstrument.keys())): - badInstrument = str(self.goodInstrument).split("\n") del badInstrument[i] badInstrument = "\n".join(badInstrument) @@ -1111,7 +1100,7 @@ def testLoadMissingInstrumentAttributesSeq(self): ) with self.assertRaises(ParserException) as cm: - GudrunFile(loadFile="test_data.txt", format=Format.TXT) + GudPyIO.importGudrunFile("test_data.txt") self.assertEqual( "Whilst parsing Instrument, an exception occured." " The input file is most likely of an incorrect format, " @@ -1145,7 +1134,7 @@ def testLoadMissingInstrumentAttributesRand(self): "INSTRUMENT {\n\n" + str(badInstrument) + "\n\n}" ) with self.assertRaises(ParserException) as cm: - GudrunFile(loadFile="test_data.txt", format=Format.TXT) + GudPyIO.importGudrunFile("test_data.txt") self.assertEqual( "Whilst parsing Instrument, an exception occured." " The input file is most likely of an incorrect format, " @@ -1180,7 +1169,7 @@ def testLoadMissingBeamAttributesSeq(self): f.write("\n\nBEAM {\n\n" + str(badBeam) + "\n\n}") with self.assertRaises(ParserException) as cm: - GudrunFile(loadFile="test_data.txt", format=Format.TXT) + GudPyIO.importGudrunFile("test_data.txt") self.assertEqual( "Whilst parsing Beam, an exception occured." " The input file is most likely of an incorrect format, " @@ -1219,7 +1208,7 @@ def testLoadMissingBeamAttributesRand(self): f.write("\n\nBEAM {\n\n" + str(badBeam) + "\n\n}") with self.assertRaises(ParserException) as cm: - GudrunFile(loadFile="test_data.txt", format=Format.TXT) + GudPyIO.importGudrunFile("test_data.txt") self.assertEqual( "Whilst parsing Beam, an exception occured." " The input file is most likely of an incorrect format, " @@ -1244,7 +1233,6 @@ def testLoadMissingNormalisationAttributesSeq(self): Composition("") ) for i in range(len(expectedNormalisation.keys())): - badNormalisation = str(self.goodNormalisation).split("\n") del badNormalisation[i] badNormalisation = "\n".join(badNormalisation) @@ -1264,7 +1252,7 @@ def testLoadMissingNormalisationAttributesSeq(self): ) with self.assertRaises(ParserException) as cm: - GudrunFile(loadFile="test_data.txt", format=Format.TXT) + GudPyIO.importGudrunFile("test_data.txt") self.assertEqual( "Whilst parsing Beam, an exception occured." " The input file is most likely of an incorrect format, " @@ -1289,7 +1277,6 @@ def testLoadMissingNormalisationAttributesRand(self): Composition("") ) for i in range(50): - key = random.choice(list(expectedNormalisation)) j = list(expectedNormalisation).index(key) @@ -1313,7 +1300,7 @@ def testLoadMissingNormalisationAttributesRand(self): ) with self.assertRaises(ParserException) as cm: - GudrunFile(loadFile="test_data.txt", format=Format.TXT) + GudPyIO.importGudrunFile("test_data.txt") self.assertEqual( "Whilst parsing Normalisation, an exception occured." " The input file is most likely of an incorrect format, " @@ -1338,7 +1325,7 @@ def testLoadMissingSampleBackgroundAttributes(self): ) f.write("\n\n{}\n\nEND".format(str(badSampleBackground))) with self.assertRaises(ParserException) as cm: - GudrunFile(loadFile="test_data.txt", format=Format.TXT) + GudPyIO.importGudrunFile("test_data.txt") self.assertEqual( "Whilst parsing Sample Background, an exception occured." " The input file is most likely of an incorrect format, " @@ -1393,7 +1380,7 @@ def testLoadMissingSampleAttributesSeq(self): f.write("\n\n{}\n\nEND".format(str(badSampleBackground))) with self.assertRaises(ParserException) as cm: - GudrunFile(loadFile="test_data.txt", format=Format.TXT) + GudPyIO.importGudrunFile("test_data.txt") self.assertEqual( "Whilst parsing Sample, an exception occured." " The input file is most likely of an incorrect format, " @@ -1451,7 +1438,7 @@ def testLoadMissingSampleAttributesRand(self): f.write("\n\n{}\n\nEND".format(str(badSampleBackground))) with self.assertRaises(ParserException) as cm: - GudrunFile(loadFile="test_data.txt", format=Format.TXT) + GudPyIO.importGudrunFile("test_data.txt") self.assertEqual( "Whilst parsing Sample, an exception occured." " The input file is most likely of an incorrect format, " @@ -1511,7 +1498,7 @@ def testLoadMissingContainerAttributesSeq(self): ) f.write("\n\n{}\n\nEND".format(str(badSampleBackground))) with self.assertRaises(ParserException) as cm: - GudrunFile(loadFile="test_data.txt", format=Format.TXT) + GudPyIO.importGudrunFile("test_data.txt") self.assertEqual( "Whilst parsing Container, an exception occured." " The input file is most likely of an incorrect format, " @@ -1524,12 +1511,14 @@ def testAppendExponentialValues(self): # Remove last element of exponential values list gudpy.gudrunFile.sampleBackgrounds[ 0].samples[0].exponentialValues[0].pop() - gudpy.gudrunFile.write_out() - gudrunFile = GudrunFile(loadFile=os.path.join( - gudpy.gudrunFile.instrument.GudrunInputFileDir, - gudpy.gudrunFile.OUTPATH - ), format=Format.TXT) + gfFilePath = os.path.join( + gudpy.io.projectDir, + gudpy.io.gudrunFileParser.OUTPATH + ) + + gudpy.io.writeGudrunFile(gfFilePath) + gudrunFile = GudPyIO.importGudrunFile(gfFilePath) # Test that a default value is appended self.assertEqual( self.expectedSampleA["exponentialValues"], @@ -1591,7 +1580,7 @@ def testLoadMissingContainerAttributesRand(self): ) f.write("\n\n{}\n\nEND".format(str(badSampleBackground))) with self.assertRaises(ParserException) as cm: - GudrunFile(loadFile="test_data.txt", format=Format.TXT) + GudPyIO.importGudrunFile("test_data.txt") self.assertEqual( "Whilst parsing Container, an exception occured." " The input file is most likely of an incorrect format, " @@ -1601,9 +1590,8 @@ def testLoadMissingContainerAttributesRand(self): def testZeroExitGudrun(self): with GudPyContext() as gudpy: - gudpy.loadFromFile( - loadFile=gudpy.gudrunFile.loadFile, format=Format.TXT) + gudpy.loadFromGudrunFile(loadFile=gudpy.io.loadFile) gudpy.setSaveLocation(os.path.splitext( - gudpy.gudrunFile.loadFile)[0]) + gudpy.io.loadFile)[0]) gudpy.runGudrun() self.assertEqual(gudpy.gudrun.exitcode, 0) diff --git a/gudpy/test/test_purge_file.py b/gudpy/test/test_purge_file.py index cf138845e..496d39bd8 100644 --- a/gudpy/test/test_purge_file.py +++ b/gudpy/test/test_purge_file.py @@ -1,10 +1,12 @@ import os from unittest import TestCase from shutil import copyfile +import tempfile from core.purge_file import PurgeFile from core.enums import Format from core import gudpy +from core.io.gudpy_io import GudPyIO class TestPurgeFile(TestCase): @@ -28,9 +30,8 @@ def setUp(self) -> None: loadFile="test/TestData/NIMROD-water/good_water.txt", format=Format.TXT) - self.gudpy.gudrunFile.write_out( - path="test/TestData/NIMROD-water/good_water.txt", - overwrite=True + self.gudpy.loadFromGudrunFile( + path="test/TestData/NIMROD-water/good_water.txt" ) self.g = self.gudpy.gudrunFile self.expectedPurgeFile = { @@ -42,7 +43,6 @@ def setUp(self) -> None: return super().setUp() def tearDown(self) -> None: - [os.remove(f) for f in os.listdir() if f not in self.keepsakes] return super().tearDown() @@ -58,9 +58,10 @@ def testCreatePurgeClass(self): ) def testWritePurgeFile(self): - - purge = PurgeFile(self.g) - purge.write_out() - with open("purge_det.dat", encoding="utf-8") as f: - outlines = f.read() - self.assertEqual(outlines, str(purge)) + with tempfile.TemporaryDirectory() as tmp: + os.chdir(tmp) + purge = PurgeFile(self.g) + GudPyIO.writeObject(purge, "purge_det.dat") + with open("purge_det.dat", encoding="utf-8") as f: + outlines = f.read() + self.assertEqual(outlines, str(purge)) From 5da17866b70a6585cb636f4d8425fd14c07bcd72 Mon Sep 17 00:00:00 2001 From: Noella Spitz Date: Wed, 15 May 2024 15:29:01 +0100 Subject: [PATCH 42/78] Continue refactoring --- gudpy/__init__.py | 0 gudpy/core/file_library.py | 7 +- gudpy/core/gudpy.py | 19 ++--- gudpy/core/gudrun_file.py | 33 ++++---- gudpy/core/io/gudpy_io.py | 80 +++++++++++------- .../{txt_parser.py => gudrun_file_parser.py} | 84 ++++++++++--------- gudpy/core/output_file_handler.py | 10 +-- gudpy/gui/widgets/core/control.py | 5 +- gudpy/gui/widgets/dialogs/io.py | 8 +- .../NIMROD-water/good_water/good_water.yaml | 28 +++---- gudpy/test/test_gudpy_io.py | 2 +- gudpy/test/test_purge_file.py | 20 ++--- 12 files changed, 145 insertions(+), 151 deletions(-) delete mode 100644 gudpy/__init__.py rename gudpy/core/io/{txt_parser.py => gudrun_file_parser.py} (95%) diff --git a/gudpy/__init__.py b/gudpy/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/gudpy/core/file_library.py b/gudpy/core/file_library.py index 65871a45e..c8feb0a2c 100644 --- a/gudpy/core/file_library.py +++ b/gudpy/core/file_library.py @@ -167,15 +167,10 @@ def checkFilesExist(self): def exportMintData( self, samples, + exportTo="", renameDataFiles=False, - exportTo=None, includeParams=False, ): - if not exportTo: - exportTo = os.path.join( - self.gudrunFile.projectDir, - Path(self.gudrunFile.path()).stem + ".zip", - ) with ZipFile(exportTo, "w", ZIP_DEFLATED) as zipFile: for sample in samples: if len(sample.dataFiles.dataFiles): diff --git a/gudpy/core/gudpy.py b/gudpy/core/gudpy.py index 2df8bf25e..c85e74e64 100644 --- a/gudpy/core/gudpy.py +++ b/gudpy/core/gudpy.py @@ -17,6 +17,7 @@ from core.file_library import GudPyFileLibrary from core import data_files from core.io.gudpy_io import GudPyIO +from core.io.gudrun_file_parser import GudrunFileParser SUFFIX = ".exe" if os.name == "nt" else "" @@ -74,9 +75,6 @@ def loadFromGudrunFile( self.gudrunFile = self.io.importGudrunFile(loadFile, config) - self.originalGudrunFile = copy.deepcopy(self.gudrunFile) - self.originalGudrunFile.filename = "original" - def loadFromProject(self, projectDir: str): """Loads GudPy from a project directory @@ -408,7 +406,7 @@ def purge(self, purgeFile: PurgeFile): return self.exitcode self.purgeLocation = self.organiseOutput( - tmp, purgeFile.gudrunFile.projectDir) + tmp, GudPyIO.projectDir) self.exitcode = 0 return self.exitcode @@ -465,7 +463,6 @@ def gudrun( tmp, gudrunFile.OUTPATH ) - assert (gudrunFile.path() is not None) GudPyIO.writeGudrunFile(path) with subprocess.Popen( [self.BINARY_PATH, path], cwd=tmp, @@ -489,13 +486,7 @@ def gudrun( self.gudrunOutput = self.organiseOutput( gudrunFile, exclude=purgeFiles) if save: - gudrunFile.save( - path=os.path.join( - gudrunFile.projectDir, - f"{gudrunFile.filename}" - ), - format=enums.Format.YAML - ) + GudPyIO.save() gudrunFile.setGudrunDir(self.gudrunOutput.path) self.exitcode = 0 @@ -893,8 +884,8 @@ def bactchProcess( batchSize: int, iterator: iterators.Iterator = None, ): - self.batchedGudrunFile.projectDir = (os.path.join( - self.batchedGudrunFile.projectDir, + self.batchedGudrunFile.outputFolder = (os.path.join( + GudPyIO.projectDir, f"BATCH_PROCESSING_BATCH_SIZE{batchSize}" )) exitcode = gudrun.gudrun(GudrunFile, purge, iterator) diff --git a/gudpy/core/gudrun_file.py b/gudpy/core/gudrun_file.py index 8937f8a0a..5dfdd1c3c 100644 --- a/gudpy/core/gudrun_file.py +++ b/gudpy/core/gudrun_file.py @@ -14,6 +14,7 @@ class GudrunFile: + OUTPATH = "gudpy.txt" """ Class to represent a GudFile (files with .gud extension). .gud files are outputted by gudrun_dcs, via merge_routines @@ -80,18 +81,11 @@ class GudrunFile: def __init__( self, - params: tuple[ - Instrument, - Normalisation, - list[SampleBackground], - Components - ] = ( - Instrument(), - Beam(), - Normalisation(), - [], - Components() - ) + instrument=Instrument(), + beam=Beam(), + normalisation=Normalisation(), + sampleBackgrounds: list[SampleBackground] = [], + components=Components() ): """ Constructs all the necessary attributes for the GudrunFile object. @@ -109,13 +103,16 @@ def __init__( config : bool If a new input file should be constructed from a config """ - inst, norm, beam, sbg, comp = params - - self.instrument = inst - self.normalisation = norm + self.instrument = instrument + self.normalisation = normalisation self.beam = beam - self.sampleBackgrounds = sbg - self.components = comp + self.sampleBackgrounds = sampleBackgrounds + self.components = components + self.purged = False + + @property + def path(self): + return os.path.join(self.outputFolder, self.OUTPATH) def __deepcopy__(self, memo): result = self.__class__.__new__(self.__class__) diff --git a/gudpy/core/io/gudpy_io.py b/gudpy/core/io/gudpy_io.py index a48b2183a..4dfc6282d 100644 --- a/gudpy/core/io/gudpy_io.py +++ b/gudpy/core/io/gudpy_io.py @@ -4,6 +4,7 @@ from ruamel.yaml import YAMLError import shutil import os +import typing as typ from core.composition import ( Component, Components, Composition, WeightedComponent @@ -20,28 +21,28 @@ from core.sample import Sample from core.container import Container from core.gudrun_file import GudrunFile -from core.io.txt_parser import GudrunFileParser +from core.io.gudrun_file_parser import GudrunFileParser from core import config class GudPyIO: - def __init__(self): - self.projectDir = "" - self.loadFile = "" + projectDir = "" + loadFile = "" + def __init__(self): self.yaml = self._getYamlModule() self.gudrunFileParser = GudrunFileParser() - @property - def projectName(self) -> str: - return os.path.dirname(self.projectDir) + @classmethod + def projectName(cls) -> str: + return os.path.dirname(cls.projectDir) - @property - def autosavePath(self) -> str: - return f"{os.path.basename(self.projectDir)}.autosave" + @classmethod + def autosavePath(cls) -> str: + return f"{os.path.basename(cls.projectDir)}.autosave" - @property - def path(self): + @classmethod + def path(cls): """Returns the path to the yaml file Returns @@ -54,11 +55,12 @@ def path(self): RuntimeError Raised if project directory is not set """ - if not self.projectDir: + if not cls.projectDir: raise RuntimeError("Save location not set.") - return os.path.join(self.projectDir, f"{self.projectName}.yaml") + return os.path.join(cls.projectDir, f"{cls.projectName()}.yaml") - def setSaveLocation(self, projectDir: str): + @classmethod + def setSaveLocation(cls, projectDir: str): """Sets the save location/project directory Parameters @@ -66,20 +68,21 @@ def setSaveLocation(self, projectDir: str): projectDir : str Path to new save location """ - self.projectDir = projectDir + cls.projectDir = projectDir - def checkSaveLocation(self) -> bool: + @classmethod + def checkSaveLocation(cls) -> bool: """Checks if user has set the save location """ - return bool(self.projectDir) + return bool(cls.projectDir) def save(self): """Saves yaml file to project directory """ - self.writeYaml(self.path) + self.writeYaml(self.path()) def importGudrunFile(self, path, config=False) -> GudrunFile: - self.loadFile = path + GudPyIO.loadFile = path return self.gudrunFileParser.parse(path, config) def importProject(self, projectDir) -> GudrunFile: @@ -135,7 +138,7 @@ def exportProject(self, targetDir): os.makedirs(targetDir) shutil.copytree(oldDir, targetDir) - os.rename(oldFile, self.path) + os.rename(oldFile, self.path()) @classmethod def exportGudrunFile(cls, path): @@ -165,8 +168,9 @@ def _getYamlModule(self) -> yaml: def _parseYaml(self, path) -> GudrunFile: self.loadFile = path yamldict = self._yamlToDict(path) - gudrunFile = GudrunFile(loadFile=path) + gudrunFile = GudrunFile() for k, v in yamldict.items(): + k = k.lower() if k == "GUI": GUI = GUIConfig() self._yamlToObject(GUI, v) @@ -188,16 +192,19 @@ def _parseYaml(self, path) -> GudrunFile: except YAMLError as e: # Exception caused by yaml parsing library raise YAMLException(f"Invalid YAML file: {str(e)}") + except Exception as e: + print(f"Failed at: {k}") + raise e return gudrunFile def _writeYAML(self, gudrunFile: GudrunFile, path: str) -> None: with open(path, "wb") as fp: outyaml = { - "Instrument": gudrunFile.instrument, - "Beam": gudrunFile.beam, - "Components": gudrunFile.components.components, - "Normalisation": gudrunFile.normalisation, - "SampleBackgrounds": gudrunFile.sampleBackgrounds, + "instrument": gudrunFile.instrument, + "beam": gudrunFile.beam, + "components": gudrunFile.components.components, + "normalisation": gudrunFile.normalisation, + "sampleBackgrounds": gudrunFile.sampleBackgrounds, "GUI": config.GUI } self.yaml.dump( @@ -205,7 +212,7 @@ def _writeYAML(self, gudrunFile: GudrunFile, path: str) -> None: fp ) - def _parsingFuncMap(self, type: type) -> typ.Callable | None: + def _parsingFuncMap(self, type: type) -> typ.Union[typ.Callable, None]: parsingFuncs = { Instrument: self._parseYamlInstrument, Beam: self._parseYamlBeam, @@ -241,7 +248,17 @@ def _assignYamlToAttr(self, obj: any, key, val) -> None: f"given to '{type(obj).__name__}'" ) return - setattr(obj, key, type(obj.__dict__[key])(self.toBuiltin(val))) + + try: + if isinstance(obj.__dict__[key], Enum): + setattr(obj, key, type(obj.__dict__[key])[val]) + return + + setattr(obj, key, type(obj.__dict__[key])(self.toBuiltin(val))) + except Exception as e: + raise YAMLException( + "Parsing failed while trying to assign attribute" + f"'{key}' to {type(obj).__name__}") from e def _parseYamlInstrument(self, yamldict: any) -> Instrument: instrument = Instrument() @@ -259,6 +276,9 @@ def _parseYamlNormalisation(self, yamldict: any) -> Normalisation: if k == "composition": composition = self._parseYamlComposition(yamldict[k]) setattr(normalisation, k, composition) + if k == "dataFiles": + dataFiles = self._parseYamlDataFiles(yamldict[k]) + setattr(normalisation, k, dataFiles) else: self._assignYamlToAttr(normalisation, k, v) return normalisation @@ -323,7 +343,7 @@ def _parseYamlContainer(self, yamldict: any) -> Container: return container def _parseYamlDataFiles(self, yamldict: any) -> DataFiles: - dataFiles = DataFiles() + dataFiles = DataFiles([], yamldict["name"]) for df in yamldict["_dataFiles"]: dataFile = DataFile(df["filename"]) dataFile = self._yamlToObject(dataFile, df) diff --git a/gudpy/core/io/txt_parser.py b/gudpy/core/io/gudrun_file_parser.py similarity index 95% rename from gudpy/core/io/txt_parser.py rename to gudpy/core/io/gudrun_file_parser.py index 50427c24e..6afeac82a 100644 --- a/gudpy/core/io/txt_parser.py +++ b/gudpy/core/io/gudrun_file_parser.py @@ -23,11 +23,11 @@ class GudrunFileParser: """Class to manage the parsing of Gudrun input files """ - OUTPATH = "gudpy.txt" def __init__(self): # Text stream self.stream = None + self.config = False @classmethod def writeGudrunFile(cls, gudrunFile: GudrunFile, runDir: str): @@ -35,7 +35,7 @@ def writeGudrunFile(cls, gudrunFile: GudrunFile, runDir: str): for sampleBackground in gudrunFile.sampleBackgrounds: sampleBackground.writeAllSamples = False - writePath = os.path.join(runDir, GudrunFileParser.OUTPATH) + writePath = os.path.join(runDir, gudrunFile.OUTPATH) with open(writePath, "w", encoding="utf-8") as f: f.write(str(gudrunFile)) @@ -66,6 +66,8 @@ def parse( normalisation = None sampleBackgrounds = [] + self.config = config + # Ensure only valid files are given. if not path: raise ParserException( @@ -94,22 +96,27 @@ def parse( # Iterate through the file, # parsing the Instrument, Beam and Normalisation. - while self.stream: - line = utils.firstword(line) - if line == "INSTRUMENT": - instrument = self._parseInstrument(line) - KEYWORDS[line] = True - elif line == "BEAM": - beam = self._parseBeam(line) - KEYWORDS[line] = True - elif line == "NORMALISATION": - normalisation = self._parseNormalisation(line) - KEYWORDS[line] = True + while (self.stream and not all(value for value in KEYWORDS.values())): + line = self._getNextToken() + kwd = utils.firstword(line) + if kwd == "INSTRUMENT": + instrument = self._parseInstrument() + KEYWORDS[kwd] = True + elif kwd == "BEAM": + beam = self._parseBeam() + KEYWORDS[kwd] = True + elif kwd == "NORMALISATION": + normalisation = self._parseNormalisation() + KEYWORDS[kwd] = True # If we didn't parse each one of the keywords, then panic. if not all(KEYWORDS.values()) and not config: + unparsed = [] + for k, v in KEYWORDS.items(): + if not v: + unparsed.append(k) raise ParserException(( - 'INSTRUMENT, BEAM and NORMALISATION' + f'{unparsed}' ' were not parsed. It\'s possible the file' ' supplied is of an incorrect format!' )) @@ -134,26 +141,24 @@ def parse( line = self._getNextToken() return GudrunFile( - loadFile=path, - params=( - instrument, - beam, - normalisation, - sampleBackgrounds, - components - )) + instrument=instrument, + beam=beam, + normalisation=normalisation, + sampleBackgrounds=sampleBackgrounds, + components=components + ) # ================================================================= # PARSING HELPERS # ================================================================= - def _getNextToken(self) -> str | None: + def _getNextToken(self) -> typ.Union[str, None]: """ Pops the 'next token' from the stream and returns it. Essentially removes the first line in the stream and returns it. """ return self.stream.pop(0) if self.stream else None - def _peekNextToken(self) -> str | None: + def _peekNextToken(self) -> typ.Union[str, None]: """ Returns the next token in the input stream, without removing it. """ @@ -436,7 +441,7 @@ def _parseBeam(self) -> Beam: # For N float attributes, # we extract the first N floats from the line. beam.beamProfileValues = ( - enums.extract_floats_from_string(self._getNextToken()) + utils.extract_floats_from_string(self._getNextToken()) ) # For single float attributes, @@ -504,6 +509,7 @@ def _parseBeam(self) -> Beam: "Whilst parsing Beam, an exception occured." " The input file is most likely of an incorrect format, " "and some attributes were missing." + f"{e}" ) from e def _parseNormalisation(self) -> Normalisation: @@ -704,7 +710,7 @@ def _parseNormalisation(self) -> Normalisation: "and some attributes were missing." ) from e - def _parseSampleBackground(self, _) -> SampleBackground: + def _parseSampleBackground(self) -> SampleBackground: """ Intialises a SampleBackground object. Parses the attributes of the SampleBackground from the input stream. @@ -748,7 +754,7 @@ def _parseSampleBackground(self, _) -> SampleBackground: "and some attributes were missing." ) from e - def _parseSample(self, _) -> Sample: + def _parseSample(self) -> Sample: """ Intialises a Sample object. Parses the attributes of the Sample from the input stream. @@ -875,13 +881,13 @@ def _parseSample(self, _) -> Sample: topHatW = utils.nthfloat(self._getNextToken(), 0) if topHatW == 0: sample.topHatW = 0 - sample.FTMode = FTModes.NO_FT + sample.FTMode = enums.FTModes.NO_FT elif topHatW < 0: sample.topHatW = abs(topHatW) - sample.FTMode = FTModes.SUB_AVERAGE + sample.FTMode = enums.FTModes.SUB_AVERAGE else: sample.topHatW = topHatW - sample.FTMode = FTModes.ABSOLUTE + sample.FTMode = enums.FTModes.ABSOLUTE sample.minRadFT = utils.nthfloat(self._getNextToken(), 0) sample.grBroadening = utils.nthfloat(self._getNextToken(), 0) @@ -897,7 +903,7 @@ def _parseSample(self, _) -> Sample: not in line ): sample.resonanceValues.append( - enums.extract_floats_from_string(line) + utils.extract_floats_from_string(line) ) line = self._getNextToken() @@ -952,7 +958,7 @@ def _parseSample(self, _) -> Sample: "and some attributes were missing." ) from e - def _parseContainer(self, _) -> Container: + def _parseContainer(self) -> Container: """ Intialises a Container object. Parses the attributes of the Container from the input stream. @@ -1135,7 +1141,7 @@ def _sampleBackgroundHelper(self) -> SampleBackground: """ # Parse sample background. - sampleBackground = self.makeParse("SAMPLE BACKGROUND") + sampleBackground = self._parseSampleBackground() self._consumeWhitespace() line = self._peekNextToken() @@ -1147,7 +1153,7 @@ def _sampleBackgroundHelper(self) -> SampleBackground: elif "GO" in line: self._getNextToken() elif "SAMPLE" in line and utils.firstword(line) == "SAMPLE": - sample = self.makeParse("SAMPLE") + sample = self._parseSample() if not sample.name: sample.name = utils.replace_unwanted_chars( utils.uniquifyName( @@ -1158,7 +1164,7 @@ def _sampleBackgroundHelper(self) -> SampleBackground: )) sampleBackground.samples.append(sample) elif "CONTAINER" in line and utils.firstword(line) == "CONTAINER": - container = self.makeParse("CONTAINER") + container = self._parseContainer() if not container.name: container.name = utils.uniquifyName( "CONTAINER", @@ -1174,7 +1180,7 @@ def _sampleBackgroundHelper(self) -> SampleBackground: return sampleBackground -Container._getNextToken = TxtParser._getNextToken -Container._peekNextToken = TxtParser._peekNextToken -Container._consumeUpToDelim = TxtParser._consumeUpToDelim -Container._consumeWhitespace = TxtParser._consumeWhitespace +Container._getNextToken = GudrunFileParser._getNextToken +Container._peekNextToken = GudrunFileParser._peekNextToken +Container._consumeUpToDelim = GudrunFileParser._consumeUpToDelim +Container._consumeWhitespace = GudrunFileParser._consumeWhitespace diff --git a/gudpy/core/output_file_handler.py b/gudpy/core/output_file_handler.py index 243016e1f..429a10eca 100644 --- a/gudpy/core/output_file_handler.py +++ b/gudpy/core/output_file_handler.py @@ -7,6 +7,7 @@ import core.utils as utils from core.gud_file import GudFile from core.gudrun_file import GudrunFile +from core.io.gudpy_io import GudPyIO @dataclass @@ -127,7 +128,7 @@ def __init__( super().__init__( gudrunFile.instrument.GudrunInputFileDir, - gudrunFile.projectDir, + GudPyIO.projectDir, "Gudrun", ) @@ -140,6 +141,7 @@ def __init__( # Directory where Gudrun files are outputted (temp) self.gudrunDir = self.procDir self.gudrunFile = gudrunFile + self.gudrunFile.outputFolder = self.outputDir # Make sure it is a temporary directory assert (self.gudrunDir.startswith(tempfile.gettempdir())) @@ -176,11 +178,9 @@ def organiseOutput(self, exclude: list[str] = []): inputFilePath = self._createAddOutDir(self.tempOutDir, exclude) # If overwrite, move previous directory - if self.overwrite and os.path.exists( - os.path.join(self.gudrunFile.projectDir, "Gudrun")): + if self.overwrite and os.path.exists(self.outputDir): with tempfile.TemporaryDirectory() as tmp: - shutil.move(os.path.join(self.gudrunFile.projectDir, "Gudrun"), - os.path.join(tmp, "prev")) + shutil.move(self.outputDir, os.path.join(tmp, "prev")) # Move over folders to output directory shutil.move(self.tempOutDir, utils.uniquify(self.outputDir)) diff --git a/gudpy/gui/widgets/core/control.py b/gudpy/gui/widgets/core/control.py index 2bebd34bf..51c1230db 100644 --- a/gudpy/gui/widgets/core/control.py +++ b/gudpy/gui/widgets/core/control.py @@ -31,7 +31,6 @@ def __init__(self): super().__init__() self.gudpy: gp.GudPy = gp.GudPy() self.mainWidget: QtWidgets.QMainWindow = GudPyMainWindow() - self.purged: bool = False # Current process thread running self.workerThread: QtCore.QThread = None @@ -396,7 +395,7 @@ def startProcess(self) -> None: os.path.join( self.gudpy.projectDir, "Purge", "purge_det.dat" ) - ): + ) or not self.gudpy.gudrunFile.purged: purgeResult = self.mainWidget.purgeOptionsMessageBox( "purge_det.dat found, but wasn't run in this session. " "Run Purge?", @@ -424,7 +423,7 @@ def runPurge(self) -> bool: self.startProcess() def purgeFinished(self, exitcode): - self.purged = True + self.gudpy.gudrunFile.purged = True if exitcode != 0: self.mainWidget.sendError( diff --git a/gudpy/gui/widgets/dialogs/io.py b/gudpy/gui/widgets/dialogs/io.py index 1a3407819..9285ebdc3 100644 --- a/gudpy/gui/widgets/dialogs/io.py +++ b/gudpy/gui/widgets/dialogs/io.py @@ -95,8 +95,6 @@ class ExportDialog(QDialog): Toggles renaming files to the sample name. performExport(filename) Performs an export to a filename. - export() - Performs a standard export. exportAs() Allows exporting to a specific file. """ @@ -174,7 +172,7 @@ def loadFilesList(self, rename=False): def toggleRename(self, state): self.loadFilesList(rename=bool(state)) - def performExport(self, filename=None): + def performExport(self, filename): fl = GudPyFileLibrary(self.gudrunFile) archive = fl.exportMintData( self.gudrunFile.samples(), @@ -189,11 +187,7 @@ def performExport(self, filename=None): ) self.widget.close() - def export(self): - self.performExport() - def exportAs(self): - dialog = QFileDialog() dialog.setDefaultSuffix("zip") dialog.setWindowTitle("Export to..") diff --git a/gudpy/test/TestData/NIMROD-water/good_water/good_water.yaml b/gudpy/test/TestData/NIMROD-water/good_water/good_water.yaml index 17c962596..075d507d8 100644 --- a/gudpy/test/TestData/NIMROD-water/good_water/good_water.yaml +++ b/gudpy/test/TestData/NIMROD-water/good_water/good_water.yaml @@ -1,4 +1,4 @@ -Instrument: +instrument: name: NIMROD dataFileDir: TestData/NIMROD-water/raw/ dataFileType: raw @@ -34,7 +34,7 @@ Instrument: logarithmicStepSize: 0.04 hardGroupEdges: true nxsDefinitionFile: '' -Beam: +beam: sampleGeometry: FLATPLATE beamProfileValues: [1.0, 1.0] stepSizeAbsorption: 0.05 @@ -53,15 +53,15 @@ Beam: overallBackgroundFactor: 1.0 sampleDependantBackgroundFactor: 0.0 shieldingAttenuationCoefficient: 0.0 -Components: [] -Normalisation: +components: [] +normalisation: periodNumber: 1 dataFiles: - dataFiles: [NIMROD00016702_V.raw] + _dataFiles: [NIMROD00016702_V.raw] name: NORMALISATION periodNumberBg: 1 dataFilesBg: - dataFiles: [NIMROD00016698_EmptyInst.raw, NIMROD00016703_EmptyInst.raw] + _dataFiles: [NIMROD00016698_EmptyInst.raw, NIMROD00016703_EmptyInst.raw] name: NORMALISATION BACKGROUND forceCalculationOfCorrections: true composition: @@ -86,7 +86,7 @@ Normalisation: lowerLimitSmoothedNormalisation: 0.01 normalisationDegreeSmoothing: 1.0 minNormalisationSignalBR: 0.0 -SampleBackgrounds: +sampleBackgrounds: - periodNumber: 1 dataFiles: dataFiles: [NIMROD00016698_EmptyInst.raw, NIMROD00016703_EmptyInst.raw] @@ -95,7 +95,7 @@ SampleBackgrounds: - name: H2O,_Can_N9 periodNumber: 1 dataFiles: - dataFiles: [NIMROD00016608_H2O_in_N9.raw, NIMROD00016610_H2O_in_N9.raw] + _dataFiles: [NIMROD00016608_H2O_in_N9.raw, NIMROD00016610_H2O_in_N9.raw] name: H2O,_Can_N9 forceCalculationOfCorrections: true composition: @@ -139,7 +139,7 @@ SampleBackgrounds: - name: N9 periodNumber: 1 dataFiles: - dataFiles: [NIMROD00016694_Empty_N9.raw, NIMROD00016699_Empty_N9.raw, NIMROD00016704_Empty_N9.raw] + _dataFiles: [NIMROD00016694_Empty_N9.raw, NIMROD00016699_Empty_N9.raw, NIMROD00016704_Empty_N9.raw] name: N9 composition: type_: Container @@ -165,7 +165,7 @@ SampleBackgrounds: - name: D2O,_Can_N10 periodNumber: 1 dataFiles: - dataFiles: [NIMROD00016609_D2O_in_N10.raw, NIMROD00016611_D2O_in_N10.raw] + _dataFiles: [NIMROD00016609_D2O_in_N10.raw, NIMROD00016611_D2O_in_N10.raw] name: D2O,_Can_N10 forceCalculationOfCorrections: true composition: @@ -209,7 +209,7 @@ SampleBackgrounds: - name: N10 periodNumber: 1 dataFiles: - dataFiles: [NIMROD00016695_Empty_N10.raw, NIMROD00016700_Empty_N10.raw, NIMROD00016705_Empty_N10.raw] + _dataFiles: [NIMROD00016695_Empty_N10.raw, NIMROD00016700_Empty_N10.raw, NIMROD00016705_Empty_N10.raw] name: N10 composition: type_: Container @@ -235,7 +235,7 @@ SampleBackgrounds: - name: HDO,_Can_N6 periodNumber: 1 dataFiles: - dataFiles: [NIMROD00016741_HDO_in_N6.raw, NIMROD00016743_HDO_in_N6.raw] + _dataFiles: [NIMROD00016741_HDO_in_N6.raw, NIMROD00016743_HDO_in_N6.raw] name: HDO,_Can_N6 forceCalculationOfCorrections: true composition: @@ -280,7 +280,7 @@ SampleBackgrounds: - name: N6 periodNumber: 1 dataFiles: - dataFiles: [NIMROD00014908_Empty_N6.raw] + _dataFiles: [NIMROD00014908_Empty_N6.raw] name: N6 composition: type_: Container @@ -351,7 +351,7 @@ SampleBackgrounds: - name: N8 periodNumber: 1 dataFiles: - dataFiles: [NIMROD00016994_Empty_N8.raw] + _dataFiles: [NIMROD00016994_Empty_N8.raw] name: N8 composition: type_: Container diff --git a/gudpy/test/test_gudpy_io.py b/gudpy/test/test_gudpy_io.py index 3cb60eb54..25d68ec71 100644 --- a/gudpy/test/test_gudpy_io.py +++ b/gudpy/test/test_gudpy_io.py @@ -1514,7 +1514,7 @@ def testAppendExponentialValues(self): gfFilePath = os.path.join( gudpy.io.projectDir, - gudpy.io.gudrunFileParser.OUTPATH + gudpy.gudrunFile.OUTPATH ) gudpy.io.writeGudrunFile(gfFilePath) diff --git a/gudpy/test/test_purge_file.py b/gudpy/test/test_purge_file.py index 496d39bd8..ec7b09919 100644 --- a/gudpy/test/test_purge_file.py +++ b/gudpy/test/test_purge_file.py @@ -4,7 +4,6 @@ import tempfile from core.purge_file import PurgeFile -from core.enums import Format from core import gudpy from core.io.gudpy_io import GudPyIO @@ -20,19 +19,11 @@ def setUp(self) -> None: else: dirpath = ( "/".join(os.path.realpath(__file__).split("/")[:-1]) - + "/" - + path ) self.gudpy = gudpy.GudPy() self.keepsakes = os.listdir() - copyfile(dirpath, "test/TestData/NIMROD-water/good_water.txt") - self.gudpy.loadFromFile( - loadFile="test/TestData/NIMROD-water/good_water.txt", - format=Format.TXT) - - self.gudpy.loadFromGudrunFile( - path="test/TestData/NIMROD-water/good_water.txt" - ) + refFile = os.path.join(dirpath, path) + self.gudpy.loadFromGudrunFile(loadFile=refFile) self.g = self.gudpy.gudrunFile self.expectedPurgeFile = { "standardDeviation": (10, 10), @@ -59,9 +50,10 @@ def testCreatePurgeClass(self): def testWritePurgeFile(self): with tempfile.TemporaryDirectory() as tmp: - os.chdir(tmp) + path = os.path.join(tmp, "purge_det.dat") purge = PurgeFile(self.g) - GudPyIO.writeObject(purge, "purge_det.dat") - with open("purge_det.dat", encoding="utf-8") as f: + GudPyIO.writeObject(purge, path) + with open(path, encoding="utf-8") as f: outlines = f.read() self.assertEqual(outlines, str(purge)) + f.close() From a2be7cb9235fef33cf3cc2af10414f8f85b5fbb3 Mon Sep 17 00:00:00 2001 From: Noella Spitz Date: Thu, 16 May 2024 15:47:11 +0100 Subject: [PATCH 43/78] Apply breaking changes --- gudpy/core/gudpy.py | 40 +++++++++--- gudpy/core/io/gudpy_io.py | 43 ++++++++----- gudpy/core/io/gudrun_file_parser.py | 11 ++-- gudpy/gudpy_cli.py | 5 +- gudpy/gudpy_gui.py | 2 +- gudpy/gui/widgets/core/control.py | 93 ++++++++++++++++----------- gudpy/gui/widgets/core/main_window.py | 2 +- gudpy/test/test_gudpy_yaml.py | 6 +- 8 files changed, 123 insertions(+), 79 deletions(-) diff --git a/gudpy/core/gudpy.py b/gudpy/core/gudpy.py index c85e74e64..07767e2ea 100644 --- a/gudpy/core/gudpy.py +++ b/gudpy/core/gudpy.py @@ -17,7 +17,6 @@ from core.file_library import GudPyFileLibrary from core import data_files from core.io.gudpy_io import GudPyIO -from core.io.gudrun_file_parser import GudrunFileParser SUFFIX = ".exe" if os.name == "nt" else "" @@ -38,9 +37,6 @@ def __init__( self.gudrunOutput = None - self.projectDir = "" - self.autosaveLocation = "" - @property def gudrunFile(self): return self._gudrunFile @@ -49,6 +45,10 @@ def gudrunFile(self): def gudrunFile(self, gudrunFile: GudrunFile): self._gudrunFile = gudrunFile + @property + def projectDir(self): + return self.io.projectDir + def loadFromGudrunFile( self, loadFile: str, @@ -60,8 +60,6 @@ def loadFromGudrunFile( ---------- loadFile : str Path of input file to load from - format : enums.Format - Format of the input file (YAML or TXT) config : bool, optional If loading from preset config, by default False @@ -75,6 +73,27 @@ def loadFromGudrunFile( self.gudrunFile = self.io.importGudrunFile(loadFile, config) + def loadFromYamlFile( + self, + loadFile: str, + ): + """Loads GudPy from a YAML input file + + Parameters + ---------- + loadFile : str + Path of input file to load from + + Raises + ------ + FileNotFoundError + Raised if input file does not exist + """ + if not os.path.exists(loadFile): + raise FileNotFoundError("Input file does not exist.") + + self.gudrunFile = self.io.importFromYamlFile(loadFile) + def loadFromProject(self, projectDir: str): """Loads GudPy from a project directory @@ -117,7 +136,7 @@ def setSaveLocation(self, projectDir: str): def save(self): """Saves current GudPy input file """ - self.io.save() + self.io.save(self.gudrunFile) def saveAs(self, targetDir: str): """Save GudPy project to a new location, set current @@ -133,7 +152,8 @@ def saveAs(self, targetDir: str): IsADirectoryError Raised if the requested save location already exists """ - self.io.exportProject(targetDir) + if self.projectDir: + self.io.exportProject(self.gudrunFile, targetDir) self.loadFromProject(projectDir=targetDir) def checkFilesExist(self): @@ -178,7 +198,7 @@ def runPurge(self): self.purge = Purge() purgeFile = PurgeFile(self.gudrunFile) exitcode = self.purge.purge(purgeFile) - self.io.save() + self.save() if exitcode: raise exc.PurgeException( "Purge failed to run with the following output:\n" @@ -202,7 +222,7 @@ def runGudrun(self, gudrunFile: GudrunFile = None): if not gudrunFile: gudrunFile = self.gudrunFile - self.io.save() + self.save() self.gudrun = Gudrun() exitcode = self.gudrun.gudrun(gudrunFile=gudrunFile) if exitcode: diff --git a/gudpy/core/io/gudpy_io.py b/gudpy/core/io/gudpy_io.py index 4dfc6282d..5bcf9d3a6 100644 --- a/gudpy/core/io/gudpy_io.py +++ b/gudpy/core/io/gudpy_io.py @@ -76,10 +76,10 @@ def checkSaveLocation(cls) -> bool: """ return bool(cls.projectDir) - def save(self): + def save(self, gudrunFile): """Saves yaml file to project directory """ - self.writeYaml(self.path()) + self._writeYAML(gudrunFile, self.path()) def importGudrunFile(self, path, config=False) -> GudrunFile: GudPyIO.loadFile = path @@ -128,21 +128,34 @@ def importProject(self, projectDir) -> GudrunFile: self.setSaveLocation(projectDir) return self._parseYaml(loadFile) - def exportProject(self, targetDir): + def importFromYamlFile(self, loadFile) -> GudrunFile: + return self._parseYaml(loadFile) + + def exportProject(self, gudrunFile, targetDir): if os.path.exists(targetDir): raise IsADirectoryError("Cannot be an existing directory") oldFile = os.path.join(targetDir, os.path.basename(self.loadFile)) - oldDir = self.projectDir - self.setSaveLocation(targetDir) os.makedirs(targetDir) + print("TARGET DIR: " + targetDir) + + if self.projectDir and oldFile: + shutil.copytree(self.projectDir, targetDir) + os.rename(oldFile, self.path()) + else: + yamlPath = os.path.join( + targetDir, + f"{os.path.basename(targetDir)}.yaml" + ) + print("yaml PATH: " + yamlPath) + self.exportYamlFile(gudrunFile, yamlPath) - shutil.copytree(oldDir, targetDir) - os.rename(oldFile, self.path()) + def exportYamlFile(self, gudrunFile, path): + self._writeYAML(gudrunFile, path) @classmethod - def exportGudrunFile(cls, path): - return GudrunFileParser.export(path) + def exportGudrunFile(cls, gudrunFile, path): + return GudrunFileParser.export(gudrunFile, path) @classmethod def writeGudrunFile(cls, gudrunFile, path): @@ -193,8 +206,7 @@ def _parseYaml(self, path) -> GudrunFile: # Exception caused by yaml parsing library raise YAMLException(f"Invalid YAML file: {str(e)}") except Exception as e: - print(f"Failed at: {k}") - raise e + raise YAMLException(f"Failed at: {k}. Details: {e}") return gudrunFile def _writeYAML(self, gudrunFile: GudrunFile, path: str) -> None: @@ -208,9 +220,10 @@ def _writeYAML(self, gudrunFile: GudrunFile, path: str) -> None: "GUI": config.GUI } self.yaml.dump( - {k: self._toYaml(v) for k, v in outyaml.items()}, + {k: self.toYaml(v) for k, v in outyaml.items()}, fp ) + print(fp) def _parsingFuncMap(self, type: type) -> typ.Union[typ.Callable, None]: parsingFuncs = { @@ -368,7 +381,7 @@ def _parseYamlComponents(self, yamllist: list[dict]) -> Components: components = [] for c in yamllist: components.append(self._parseYamlComponent(c)) - return components + return Components(components) def _parseYamlElements(self, yamllist: list[str]) -> list[Element]: elements = [] @@ -407,7 +420,7 @@ def toYaml(self, var: any): return float(var) if var.__class__.__module__ == "builtins": if isinstance(var, (list, tuple)): - return type(var)([self._toYaml(v) for v in var]) + return type(var)([self.toYaml(v) for v in var]) else: return var elif isinstance(var, Enum): @@ -418,7 +431,7 @@ def toYaml(self, var: any): Component, Composition, Element, DataFiles, DataFile, GUIConfig )): return { - k: self._toYaml(v) + k: self.toYaml(v) for k, v in var.__dict__.items() if k not in var.yamlignore } diff --git a/gudpy/core/io/gudrun_file_parser.py b/gudpy/core/io/gudrun_file_parser.py index 6afeac82a..29bf26316 100644 --- a/gudpy/core/io/gudrun_file_parser.py +++ b/gudpy/core/io/gudrun_file_parser.py @@ -60,10 +60,10 @@ def parse( Raises ParserException if Instrument, Beam or Normalisation are missing. """ - instrument = None - beam = None - components = None - normalisation = None + instrument = Instrument() + beam = Beam() + components = Components() + normalisation = Normalisation() sampleBackgrounds = [] self.config = config @@ -772,7 +772,6 @@ def _parseSample(self) -> Sample: .replace("SAMPLE", "").strip() ) sample.name = utils.replace_unwanted_chars(sample.name) - print(sample.name) self._consumeWhitespace() # The number of files and period number are both stored # on the same line. @@ -1097,7 +1096,7 @@ def _parseContainer(self) -> Container: ) from e def _parseComponents(self) -> Components: - components = Components + components = Components() try: while self.stream: component = self._parseComponent() diff --git a/gudpy/gudpy_cli.py b/gudpy/gudpy_cli.py index 9bd3a6372..d5d90c2b0 100644 --- a/gudpy/gudpy_cli.py +++ b/gudpy/gudpy_cli.py @@ -18,7 +18,6 @@ def loadProject(ctx, project): def loadFile(ctx, value): - print(value) file, format = value if not file: file = click.prompt("Path to load file", type=click.Path()) @@ -35,7 +34,7 @@ def loadFile(ctx, value): if not file or format: return ctx.obj = gp.GudPy() - ctx.obj.loadFromFile(file, format) + ctx.obj.loadFromGudrunFile(file) click.echo(click.style(u"\u2714", fg="green", bold=True) + f" GudPy input file {file} sucessfuly loaded") @@ -56,7 +55,7 @@ def loadConfig(ctx, cfg): return ctx.obj = gp.GudPy() - ctx.obj.loadFromFile(cfg, enums.Format.TXT, config=True) + ctx.obj.loadFromGudrunFile(cfg, config=True) def echoIndent(text): diff --git a/gudpy/gudpy_gui.py b/gudpy/gudpy_gui.py index ec4cc0152..ba0a8c74f 100644 --- a/gudpy/gudpy_gui.py +++ b/gudpy/gudpy_gui.py @@ -10,7 +10,7 @@ def __init__(self, args): super(GudPy, self).__init__(args) self.gudpy = GudPyController() self.aboutToQuit.connect(self.gudpy.cleanup) - sys.exit(self.exec_()) + sys.exit(self.exec()) def onException(self, cls, exception, traceback): self.gudpy.onException(cls, exception, traceback) diff --git a/gudpy/gui/widgets/core/control.py b/gudpy/gui/widgets/core/control.py index 51c1230db..6749db343 100644 --- a/gudpy/gui/widgets/core/control.py +++ b/gudpy/gui/widgets/core/control.py @@ -110,6 +110,16 @@ def connectUiSlots(self): self.mainWidget.ui.exportArchive.triggered.connect(self.exportArchive) self.mainWidget.ui.exit.triggered.connect(self.exit_) + @property + def gudrunFile(self) -> None: + return self.gudpy.gudrunFile + + @gudrunFile.setter + def gudrunFile(self, gudrunFile: gudrunFile) -> None: + self.gudpy = gp.GudPy() + self.gudpy.gudrunFile = gudrunFile + self.mainWidget.updateWidgets(self.gudpy.gudrunFile) + """ INPUT / OUTPUT @@ -123,7 +133,7 @@ def tryLoadAutosaved(self, projectDir): autoFileInfo = QtCore.QFileInfo(path) autoDate = autoFileInfo.lastModified() - fileInfo = QtCore.QFileInfo(self.gudpy.gudrunFile.path()) + fileInfo = QtCore.QFileInfo(self.gudpy.io.loadFile) currentDate = fileInfo.lastModified() if autoDate > currentDate: @@ -159,7 +169,13 @@ def loadFromFile(self): fmt = filters[filter] try: gudpy = gp.GudPy() - gudpy.loadFromFile(loadFile=filename, format=fmt) + if fmt == enums.Format.TXT: + gudpy.loadFromGudrunFile(filename) + elif fmt == enums.Format.YAML: + gudpy.loadFromYamlFile(filename) + else: + raise RuntimeError(f"Unsupported format: {fmt}") + self.gudpy = gudpy except (FileNotFoundError, exc.ParserException) as e: self.mainWidget.sendError(e) @@ -167,9 +183,9 @@ def loadFromFile(self): except IOError: self.mainWidget.sendError("Could not open file.") return - self.mainWidget.updateWidgets(self.gudpy.gudrunFile) + self.mainWidget.updateWidgets(self.gudrunFile) self.mainWidget.setWindowTitle( - f"GudPy - {self.gudpy.gudrunFile.filename}[*]") + f"GudPy - {os.path.basename(filename)}[*]") def loadFromProject(self): """Load from previous GudPy project @@ -186,7 +202,7 @@ def loadFromProject(self): if autosave: filename = autosave gudpy = gp.GudPy() - gudpy.loadFromFile(loadFile=filename) + gudpy.loadFromGudrunFile(loadFile=filename) self.gudpy = gudpy except (FileNotFoundError, exc.ParserException) as e: self.mainWidget.sendError(e) @@ -194,12 +210,12 @@ def loadFromProject(self): except IOError: self.mainWidget.sendError("Could not open file.") return - self.mainWidget.updateWidgets(self.gudpy.gudrunFile) + self.mainWidget.updateWidgets(self.gudrunFile) self.mainWidget.setWindowTitle( - f"GudPy - {self.gudpy.gudrunFile.filename}[*]") + f"GudPy - {self.gudpy.io.projectName}[*]") def newProject(self): - if self.gudpy.gudrunFile: + if self.gudrunFile: save = QtWidgets.QMessageBox.question( self.mainWidget, "GudPy", @@ -216,12 +232,11 @@ def newProject(self): if not configurationDialog.cancelled and result: self.gudpy = gp.GudPy() - self.gudpy.loadFromFile( + self.gudpy.loadFromGudrunFile( loadFile=configurationDialog.configuration, - format=enums.Format.TXT, config=True ) - self.mainWidget.updateWidgets(self.gudpy.gudrunFile) + self.mainWidget.updateWidgets(self.gudrunFile) def setSaveLocation(self, saveAs=False): """Function to let the user choose where the project is saved to @@ -266,6 +281,7 @@ def saveAs(self): return try: + print(dirname) self.gudpy.saveAs(dirname) except IsADirectoryError as e: self.mainWidget.sendError(e) @@ -278,18 +294,17 @@ def exportInputFile(self): self, "Export input file as..", ".", - "YAML (*.yaml);;Gudrun Compatible (*.txt)", + "Gudrun Compatible (*.txt)", ) - fmt = enums.Format.YAML if filename: ext = re.search(r"\((.+?)\)", filter).group(1).replace("*", "") - fmt = enums.Format.TXT if ext == ".txt" else enums.Format.YAML if filter and sys.platform.startswith("linux"): filename += ext if os.path.dirname(filename) == self.gudpy.projectDir: self.mainWidget.sendWarning("Do not modify project folder.") return - self.gudpy.save(path=filename, format=fmt) + self.gudpy.io.exportGudrunFile(self.gudrunFile, filename) + self.setUnModified() def exportArchive(self): @@ -297,12 +312,12 @@ def exportArchive(self): if not self.setSaveLocation(): return exportDialog = dialogs.io.ExportDialog( - self.gudpy.gudrunFile, self.mainWidget) + self.gudrunFile, self.mainWidget) exportDialog.widget.exec() def autosave(self): if self.gudpy.checkSaveLocation() and not self.workerThread: - self.gudpy.save(path=self.gudpy.autosaveLocation) + self.gudpy.save() """ @@ -312,7 +327,7 @@ def autosave(self): def checkFilesExist(self, showSuccessDialog: bool = False): result = file_library.GudPyFileLibrary( - self.gudpy.gudrunFile).checkFilesExist() + self.gudrunFile).checkFilesExist() if not all(r[0] for r in result[0]) or not all(r[0] for r in result[1]): undefined = [ @@ -322,7 +337,7 @@ def checkFilesExist(self, showSuccessDialog: bool = False): missingFilesDialog = dialogs.io.MissingFilesDialog( undefined, unresolved, self.mainWidget ) - missingFilesDialog.widget.exec_() + missingFilesDialog.widget.exec() return False if showSuccessDialog: @@ -352,16 +367,16 @@ def createPurgeProcess(self) -> bool: return False self.mainWidget.setControlsEnabled(False) purgeDialog = dialogs.purge.PurgeDialog( - self.gudpy.gudrunFile, self.mainWidget) - result = purgeDialog.widget.exec_() + self.gudrunFile, self.mainWidget) + result = purgeDialog.widget.exec() if (purgeDialog.cancelled or result == QDialogButtonBox.No): self.mainWidget.setControlsEnabled(True) return False - self.gudpy.purgeFile = PurgeFile(self.gudpy.gudrunFile) + self.gudpy.purgeFile = PurgeFile(self.gudrunFile) self.gudpy.purge = worker.PurgeWorker( purgeFile=self.gudpy.purgeFile, - gudrunFile=self.gudpy.gudrunFile, + gudrunFile=self.gudrunFile, ) self.connectProcessSignals( process=self.gudpy.purge, onFinish=self.purgeFinished @@ -373,7 +388,7 @@ def prepareRun(self) -> bool: if not self.checkFilesExist(): return False - if not self.gudpy.gudrunFile.checkNormDataFiles(): + if not self.gudrunFile.checkNormDataFiles(): self.mainWidget.sendWarning( "Please specify normalisation data files.") return False @@ -395,7 +410,7 @@ def startProcess(self) -> None: os.path.join( self.gudpy.projectDir, "Purge", "purge_det.dat" ) - ) or not self.gudpy.gudrunFile.purged: + ) or not self.gudrunFile.purged: purgeResult = self.mainWidget.purgeOptionsMessageBox( "purge_det.dat found, but wasn't run in this session. " "Run Purge?", @@ -423,7 +438,7 @@ def runPurge(self) -> bool: self.startProcess() def purgeFinished(self, exitcode): - self.gudpy.gudrunFile.purged = True + self.gudrunFile.purged = True if exitcode != 0: self.mainWidget.sendError( @@ -433,20 +448,20 @@ def purgeFinished(self, exitcode): self.workerThread = None return - thresh = self.gudpy.gudrunFile.instrument.goodDetectorThreshold + thresh = self.gudrunFile.instrument.goodDetectorThreshold if thresh and self.gudpy.purge.detectors < thresh: self.mainWidget.sendWarning( f"{self.gudpy.purge.detectors} " "detectors made it through the purge.\n" " The acceptable minimum for " - f"{self.gudpy.gudrunFile.instrument.name} is {thresh}" + f"{self.gudrunFile.instrument.name} is {thresh}" ) self.mainWidget.ui.goodDetectorsLabel.setText( f"Number of Good Detectors: {self.gudpy.purge.detectors}" ) self.mainWidget.outputSlots.setOutput( self.gudpy.purge.output, "purge_det", - gudrunFile=self.gudpy.gudrunFile + gudrunFile=self.gudrunFile ) if isinstance(self.workerThread, gp.Purge): @@ -456,7 +471,7 @@ def purgeFinished(self, exitcode): def runGudrun(self, gudrunFile=None): if not gudrunFile: - gudrunFile = self.gudpy.gudrunFile + gudrunFile = self.gudrunFile if not self.prepareRun(): return @@ -471,7 +486,7 @@ def runGudrun(self, gudrunFile=None): def iterateGudrun(self, dialog): iterationDialog = dialog( - self.mainWidget, self.gudpy.gudrunFile) + self.mainWidget, self.gudrunFile) iterationDialog.widget.exec() if not iterationDialog.params: return @@ -479,7 +494,7 @@ def iterateGudrun(self, dialog): return # If it is a Composition iteration, the gudrunFile must be specified if iterationDialog.iteratorType == iterators.Composition: - iterationDialog.params["gudrunFile"] = self.gudpy.gudrunFile + iterationDialog.params["gudrunFile"] = self.gudrunFile self.gudpy.iterator = iterationDialog.iteratorType( **iterationDialog.params) @@ -487,7 +502,7 @@ def iterateGudrun(self, dialog): # If Composition iterator, initialise Composition Worker if iterationDialog.iteratorType == iterators.Composition: self.gudpy.gudrunIterator = worker.CompositionWorker( - self.gudpy.iterator, self.gudpy.gudrunFile, self.gudpy.purge) + self.gudpy.iterator, self.gudrunFile, self.gudpy.purge) self.connectProcessSignals( process=self.gudpy.gudrunIterator, onFinish=self.compositionIterationFinished @@ -495,7 +510,7 @@ def iterateGudrun(self, dialog): # Else use standard GudrunIteratorWorker else: self.gudpy.gudrunIterator = worker.GudrunIteratorWorker( - self.gudpy.iterator, self.gudpy.gudrunFile, self.gudpy.purge) + self.gudpy.iterator, self.gudrunFile, self.gudpy.purge) self.connectProcessSignals( process=self.gudpy.gudrunIterator, onFinish=self.gudrunFinished @@ -535,7 +550,7 @@ def gudrunFinished(self, exitcode): self.mainWidget.outputSlots.setOutput( self.gudpy.gudrun.output, "Gudrun") self.mainWidget.updateWidgets( - gudrunFile=self.gudpy.gudrunFile, + gudrunFile=self.gudrunFile, gudrunOutput=self.gudpy.gudrun.gudrunOutput ) self.workerThread = None @@ -558,14 +573,14 @@ def runContainersAsSamples(self): if not self.prepareRun(): return gudrunFile = self.gudpy.runModes.convertContainersToSample( - self.gudpy.gudrunFile + self.gudrunFile ) self.runGudrun(gudrunFile=gudrunFile) def runFilesIndividually(self): if not self.prepareRun(): return - gudrunFile = self.gudpy.runModes.partition(self.gudpy.gudrunFile) + gudrunFile = self.gudpy.runModes.partition(self.gudrunFile) self.runGudrun(gudrunFile=gudrunFile) def runBatchProcessing(self): @@ -575,7 +590,7 @@ def runBatchProcessing(self): self.mainWidget ) self.gudpy.gudrunIterator = worker.BatchWorker( - gudrunFile=self.gudpy.gudrunFile, + gudrunFile=self.gudrunFile, purge=self.gudpy.purge, iterator=dialog.iterator, batchSize=dialog.batchSize, @@ -627,5 +642,5 @@ def exit_(self): ) if result == QtWidgets.QMessageBox.Yes: - self.gudpy.gudrunFile.save() + self.gudrunFile.save() sys.exit(0) diff --git a/gudpy/gui/widgets/core/main_window.py b/gudpy/gui/widgets/core/main_window.py index 62a728ab8..4487531d1 100644 --- a/gudpy/gui/widgets/core/main_window.py +++ b/gudpy/gui/widgets/core/main_window.py @@ -778,7 +778,7 @@ def setTreeActionsEnabled(self, state): def viewInput(self): self.currentState = str(self.gudrunFile) viewInputDialog = ViewInputDialog(self.gudrunFile, self) - viewInputDialog.widget.exec_() + viewInputDialog.widget.exec() def handleAllPlotModeChanged(self, index): plotMode = self.ui.allPlotComboBox.itemData(index) diff --git a/gudpy/test/test_gudpy_yaml.py b/gudpy/test/test_gudpy_yaml.py index c68a63804..b58d89e2a 100644 --- a/gudpy/test/test_gudpy_yaml.py +++ b/gudpy/test/test_gudpy_yaml.py @@ -8,17 +8,15 @@ class TestYAML(TestCase): def testYAML(self): gudpy1 = gudpy.GudPy() - gudpy1.loadFromFile( + gudpy1.loadFromGudrunFile( loadFile="test/TestData/NIMROD-water/water.txt", - format=Format.TXT ) gudpy1.save(path="test/TestData/NIMROD-water/water.yaml") gf1 = gudpy1.gudrunFile gudpy2 = gudpy.GudPy() - gudpy2.loadFromFile( + gudpy2.loadFromYamlFile( loadFile="test/TestData/NIMROD-water/water.yaml", - format=Format.YAML ) gf2 = gudpy2.gudrunFile From ad8fddcfc8931cb4e09b51a9d63adddbb61e53df Mon Sep 17 00:00:00 2001 From: Noella Spitz Date: Fri, 17 May 2024 15:04:58 +0100 Subject: [PATCH 44/78] Fix checks --- gudpy/core/io/gudpy_io.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gudpy/core/io/gudpy_io.py b/gudpy/core/io/gudpy_io.py index 5bcf9d3a6..7460a91b0 100644 --- a/gudpy/core/io/gudpy_io.py +++ b/gudpy/core/io/gudpy_io.py @@ -139,7 +139,7 @@ def exportProject(self, gudrunFile, targetDir): os.makedirs(targetDir) print("TARGET DIR: " + targetDir) - if self.projectDir and oldFile: + if os.path.exists(self.projectDir) and os.path.exists(oldFile): shutil.copytree(self.projectDir, targetDir) os.rename(oldFile, self.path()) else: From 6ebff8fdfce2e12ea3b91206508c9415a5d62c10 Mon Sep 17 00:00:00 2001 From: Noella Spitz Date: Mon, 20 May 2024 10:54:37 +0100 Subject: [PATCH 45/78] Fix YAML imports --- gudpy/core/gudpy.py | 3 +- gudpy/core/io/gudpy_io.py | 92 +++++++++++++++++++------------ gudpy/gui/widgets/core/control.py | 7 ++- 3 files changed, 63 insertions(+), 39 deletions(-) diff --git a/gudpy/core/gudpy.py b/gudpy/core/gudpy.py index 07767e2ea..2a599879a 100644 --- a/gudpy/core/gudpy.py +++ b/gudpy/core/gudpy.py @@ -152,8 +152,7 @@ def saveAs(self, targetDir: str): IsADirectoryError Raised if the requested save location already exists """ - if self.projectDir: - self.io.exportProject(self.gudrunFile, targetDir) + self.io.exportProject(self.gudrunFile, targetDir) self.loadFromProject(projectDir=targetDir) def checkFilesExist(self): diff --git a/gudpy/core/io/gudpy_io.py b/gudpy/core/io/gudpy_io.py index 7460a91b0..72ddcd8ee 100644 --- a/gudpy/core/io/gudpy_io.py +++ b/gudpy/core/io/gudpy_io.py @@ -183,7 +183,6 @@ def _parseYaml(self, path) -> GudrunFile: yamldict = self._yamlToDict(path) gudrunFile = GudrunFile() for k, v in yamldict.items(): - k = k.lower() if k == "GUI": GUI = GUIConfig() self._yamlToObject(GUI, v) @@ -194,8 +193,7 @@ def _parseYaml(self, path) -> GudrunFile: print(f"Ignoring depreciated attribute '{k}'") continue - objType = type(gudrunFile.__dict__[k]) - parsingFunc = self._parsingFuncMap(objType) + parsingFunc = self._parsingFuncMap(k) if parsingFunc: try: obj = parsingFunc(v) @@ -206,7 +204,7 @@ def _parseYaml(self, path) -> GudrunFile: # Exception caused by yaml parsing library raise YAMLException(f"Invalid YAML file: {str(e)}") except Exception as e: - raise YAMLException(f"Failed at: {k}. Details: {e}") + raise YAMLException(f"Failed at: '{k}'. Details: {e}") return gudrunFile def _writeYAML(self, gudrunFile: GudrunFile, path: str) -> None: @@ -223,23 +221,23 @@ def _writeYAML(self, gudrunFile: GudrunFile, path: str) -> None: {k: self.toYaml(v) for k, v in outyaml.items()}, fp ) - print(fp) - def _parsingFuncMap(self, type: type) -> typ.Union[typ.Callable, None]: + def _parsingFuncMap(self, key) -> typ.Union[typ.Callable, None]: parsingFuncs = { - Instrument: self._parseYamlInstrument, - Beam: self._parseYamlBeam, - Normalisation: self._parseYamlNormalisation, - SampleBackground: self._parseYamlSampleBackground, - Sample: self._parseYamlSample, - Composition: self._parseYamlComposition, - Component: self._parseYamlComponent, - Components: self._parseYamlComponents, - list[Element]: self._parseYamlElements, - DataFiles: self._parseYamlDataFiles + "instrument ": self._parseYamlInstrument, + "beam": self._parseYamlBeam, + "normalisation": self._parseYamlNormalisation, + "sampleBackgrounds": self._parseYamlSampleBackground, + "sample": self._parseYamlSample, + "composition": self._parseYamlComposition, + "component": self._parseYamlComponent, + "components": self._parseYamlComponents, + "elements": self._parseYamlElements, + "dataFiles": self._parseYamlDataFiles, + "dataFilesBg": self._parseYamlDataFiles } - if type in parsingFuncs.keys(): - return parsingFuncs[type] + if key in parsingFuncs.keys(): + return parsingFuncs[key] else: return None @@ -287,10 +285,10 @@ def _parseYamlNormalisation(self, yamldict: any) -> Normalisation: normalisation = Normalisation() for k, v in yamldict.items(): if k == "composition": - composition = self._parseYamlComposition(yamldict[k]) + composition = self._parseYamlComposition(v) setattr(normalisation, k, composition) - if k == "dataFiles": - dataFiles = self._parseYamlDataFiles(yamldict[k]) + elif k == "dataFiles" or k == "dataFilesBg": + dataFiles = self._parseYamlDataFiles(v) setattr(normalisation, k, dataFiles) else: self._assignYamlToAttr(normalisation, k, v) @@ -299,8 +297,11 @@ def _parseYamlNormalisation(self, yamldict: any) -> Normalisation: def _parseYamlSample(self, yamldict: any) -> Sample: sample = Sample() for k, v in yamldict.items(): - if k == "composition": - composition = self._parseYamlComposition() + if k == "dataFiles": + dataFiles = self._parseYamlDataFiles(v) + setattr(sample, k, dataFiles) + elif k == "composition": + composition = self._parseYamlComposition(v) setattr(sample, k, composition) elif k == "containers": for contyaml in yamldict[k]: @@ -310,19 +311,28 @@ def _parseYamlSample(self, yamldict: any) -> Sample: self._assignYamlToAttr(sample, k, v) return sample - def _parseYamlSampleBackground(self, yamldict: any) -> SampleBackground: - sampleBg = SampleBackground() - for k, v in yamldict.items(): - if k == "samples": - for sampleyaml in yamldict[k]: - sample = self._parseYamlSample(sampleyaml) - sample.name = utils.replace_unwanted_chars(sample.name) - sampleBg.samples.append(sample) - else: - self._assignYamlToAttr(sampleBg, k, v) + def _parseYamlSampleBackground( + self, yamllist: list + ) -> list[SampleBackground]: + sampleBackgrounds = [] + for sbg in yamllist: + sampleBg = SampleBackground() + for k, v in sbg.items(): + if k == "samples": + for sampleyaml in v: + sample = self._parseYamlSample(sampleyaml) + sample.name = utils.replace_unwanted_chars(sample.name) + sampleBg.samples.append(sample) + elif k == "dataFiles": + dataFiles = self._parseYamlDataFiles(v) + setattr(sampleBg, k, dataFiles) + else: + self._assignYamlToAttr(sampleBg, k, v) + sampleBackgrounds.append(sampleBg) + return sampleBackgrounds def _parseYamlComposition(self, yamldict: any) -> Composition: - composition = Composition() + composition = Composition(yamldict["type_"]) for k, v in yamldict.items(): if k == "elements": elements = self._parseYamlElements(yamldict[k]) @@ -349,8 +359,11 @@ def _parseYamlContainer(self, yamldict: any) -> Container: container = Container() for k, v in yamldict.items(): if k == "composition": - composition = self._parseYamlComposition() + composition = self._parseYamlComposition(v) setattr(container, k, composition) + elif k == "dataFiles": + dataFiles = self._parseYamlDataFiles(v) + setattr(container, k, dataFiles) else: self._assignYamlToAttr(container, k, v) return container @@ -359,7 +372,14 @@ def _parseYamlDataFiles(self, yamldict: any) -> DataFiles: dataFiles = DataFiles([], yamldict["name"]) for df in yamldict["_dataFiles"]: dataFile = DataFile(df["filename"]) - dataFile = self._yamlToObject(dataFile, df) + for k, v in df.items(): + if k == "_outputs": + outDict = {} + for k_, v_ in v: + outDict[k_] = v_ + dataFile._outputs = outDict + else: + self._assignYamlToAttr(dataFile, k, v) dataFiles.dataFiles.append(dataFile) return dataFiles diff --git a/gudpy/gui/widgets/core/control.py b/gudpy/gui/widgets/core/control.py index 6749db343..456c22d8d 100644 --- a/gudpy/gui/widgets/core/control.py +++ b/gudpy/gui/widgets/core/control.py @@ -128,7 +128,7 @@ def gudrunFile(self, gudrunFile: gudrunFile) -> None: def tryLoadAutosaved(self, projectDir): for f in os.listdir(projectDir): - if f == self.gudpy.autosaveLocation: + if f == self.gudpy.io.autosavePath: path = os.path.join(projectDir, f) autoFileInfo = QtCore.QFileInfo(path) autoDate = autoFileInfo.lastModified() @@ -210,7 +210,12 @@ def loadFromProject(self): except IOError: self.mainWidget.sendError("Could not open file.") return + except exc.YAMLException as e: + self.mainWidget.sendError(f"Could not parse input file: {e}") + return self.mainWidget.updateWidgets(self.gudrunFile) + for sample in self.gudrunFile.sampleBackgrounds[0].samples: + print(sample.name) self.mainWidget.setWindowTitle( f"GudPy - {self.gudpy.io.projectName}[*]") From 27bcd7c09e8c2ecc6de0ef103c73edbe1e468fbc Mon Sep 17 00:00:00 2001 From: Noella Spitz Date: Mon, 20 May 2024 10:57:59 +0100 Subject: [PATCH 46/78] Fix instrument parsing --- gudpy/core/io/gudpy_io.py | 4 +--- gudpy/gui/widgets/core/control.py | 2 -- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/gudpy/core/io/gudpy_io.py b/gudpy/core/io/gudpy_io.py index 72ddcd8ee..eed1887cf 100644 --- a/gudpy/core/io/gudpy_io.py +++ b/gudpy/core/io/gudpy_io.py @@ -137,7 +137,6 @@ def exportProject(self, gudrunFile, targetDir): oldFile = os.path.join(targetDir, os.path.basename(self.loadFile)) os.makedirs(targetDir) - print("TARGET DIR: " + targetDir) if os.path.exists(self.projectDir) and os.path.exists(oldFile): shutil.copytree(self.projectDir, targetDir) @@ -147,7 +146,6 @@ def exportProject(self, gudrunFile, targetDir): targetDir, f"{os.path.basename(targetDir)}.yaml" ) - print("yaml PATH: " + yamlPath) self.exportYamlFile(gudrunFile, yamlPath) def exportYamlFile(self, gudrunFile, path): @@ -224,7 +222,7 @@ def _writeYAML(self, gudrunFile: GudrunFile, path: str) -> None: def _parsingFuncMap(self, key) -> typ.Union[typ.Callable, None]: parsingFuncs = { - "instrument ": self._parseYamlInstrument, + "instrument": self._parseYamlInstrument, "beam": self._parseYamlBeam, "normalisation": self._parseYamlNormalisation, "sampleBackgrounds": self._parseYamlSampleBackground, diff --git a/gudpy/gui/widgets/core/control.py b/gudpy/gui/widgets/core/control.py index 456c22d8d..b08cf863d 100644 --- a/gudpy/gui/widgets/core/control.py +++ b/gudpy/gui/widgets/core/control.py @@ -215,7 +215,6 @@ def loadFromProject(self): return self.mainWidget.updateWidgets(self.gudrunFile) for sample in self.gudrunFile.sampleBackgrounds[0].samples: - print(sample.name) self.mainWidget.setWindowTitle( f"GudPy - {self.gudpy.io.projectName}[*]") @@ -286,7 +285,6 @@ def saveAs(self): return try: - print(dirname) self.gudpy.saveAs(dirname) except IsADirectoryError as e: self.mainWidget.sendError(e) From 3c5ca044fb26a1cc178a0d47026607cf484d1a9f Mon Sep 17 00:00:00 2001 From: Noella Spitz Date: Mon, 20 May 2024 11:55:30 +0100 Subject: [PATCH 47/78] Fix export, update test project --- gudpy/core/io/gudrun_file_parser.py | 3 +- gudpy/gui/widgets/core/control.py | 17 +- .../NIMROD-water/good_water/good_water.yaml | 185 ++++++++++++++++-- 3 files changed, 182 insertions(+), 23 deletions(-) diff --git a/gudpy/core/io/gudrun_file_parser.py b/gudpy/core/io/gudrun_file_parser.py index 29bf26316..cee80941a 100644 --- a/gudpy/core/io/gudrun_file_parser.py +++ b/gudpy/core/io/gudrun_file_parser.py @@ -41,7 +41,8 @@ def writeGudrunFile(cls, gudrunFile: GudrunFile, runDir: str): f.write(str(gudrunFile)) f.close() - def export(self, gudrunFile: GudrunFile, path: str): + @classmethod + def export(cls, gudrunFile: GudrunFile, path: str): for sampleBackground in gudrunFile.sampleBackgrounds: sampleBackground.writeAllSamples = True diff --git a/gudpy/gui/widgets/core/control.py b/gudpy/gui/widgets/core/control.py index b08cf863d..ea28e0d78 100644 --- a/gudpy/gui/widgets/core/control.py +++ b/gudpy/gui/widgets/core/control.py @@ -214,7 +214,6 @@ def loadFromProject(self): self.mainWidget.sendError(f"Could not parse input file: {e}") return self.mainWidget.updateWidgets(self.gudrunFile) - for sample in self.gudrunFile.sampleBackgrounds[0].samples: self.mainWidget.setWindowTitle( f"GudPy - {self.gudpy.io.projectName}[*]") @@ -293,22 +292,20 @@ def exportInputFile(self): """ Saves the current state of the input file as... """ - filename, filter = QFileDialog.getSaveFileName( - self, + filename, _ = QFileDialog.getSaveFileName( + self.mainWidget, "Export input file as..", ".", - "Gudrun Compatible (*.txt)", ) + print(filename) if filename: - ext = re.search(r"\((.+?)\)", filter).group(1).replace("*", "") - if filter and sys.platform.startswith("linux"): - filename += ext + filename.replace(".txt", "") + filename += ".txt" if os.path.dirname(filename) == self.gudpy.projectDir: self.mainWidget.sendWarning("Do not modify project folder.") return - self.gudpy.io.exportGudrunFile(self.gudrunFile, filename) - - self.setUnModified() + self.gudpy.io.exportGudrunFile(self.gudrunFile, filename) + self.setUnModified() def exportArchive(self): if not self.gudpy.checkSaveLocation(): diff --git a/gudpy/test/TestData/NIMROD-water/good_water/good_water.yaml b/gudpy/test/TestData/NIMROD-water/good_water/good_water.yaml index 075d507d8..924e570f4 100644 --- a/gudpy/test/TestData/NIMROD-water/good_water/good_water.yaml +++ b/gudpy/test/TestData/NIMROD-water/good_water/good_water.yaml @@ -1,6 +1,6 @@ instrument: name: NIMROD - dataFileDir: TestData/NIMROD-water/raw/ + dataFileDir: test/TestData/NIMROD-water/raw/ dataFileType: raw detectorCalibrationFileName: StartupFiles/NIMROD/NIMROD84modules+9monitors+LAB5Oct2012Detector.dat columnNoPhiVals: 4 @@ -57,12 +57,34 @@ components: [] normalisation: periodNumber: 1 dataFiles: - _dataFiles: [NIMROD00016702_V.raw] + _dataFiles: + - filename: NIMROD00016702_V.raw + name: NIMROD00016702_V + ext: .raw + outputFolder: '' + _outputs: {} + isSampleDataFile: false name: NORMALISATION + outputFolders: {} + _outputs: {} periodNumberBg: 1 dataFilesBg: - _dataFiles: [NIMROD00016698_EmptyInst.raw, NIMROD00016703_EmptyInst.raw] + _dataFiles: + - filename: NIMROD00016698_EmptyInst.raw + name: NIMROD00016698_EmptyInst + ext: .raw + outputFolder: '' + _outputs: {} + isSampleDataFile: false + - filename: NIMROD00016703_EmptyInst.raw + name: NIMROD00016703_EmptyInst + ext: .raw + outputFolder: '' + _outputs: {} + isSampleDataFile: false name: NORMALISATION BACKGROUND + outputFolders: {} + _outputs: {} forceCalculationOfCorrections: true composition: type_: Normalisation @@ -89,14 +111,42 @@ normalisation: sampleBackgrounds: - periodNumber: 1 dataFiles: - dataFiles: [NIMROD00016698_EmptyInst.raw, NIMROD00016703_EmptyInst.raw] + _dataFiles: + - filename: NIMROD00016698_EmptyInst.raw + name: NIMROD00016698_EmptyInst + ext: .raw + outputFolder: '' + _outputs: {} + isSampleDataFile: false + - filename: NIMROD00016703_EmptyInst.raw + name: NIMROD00016703_EmptyInst + ext: .raw + outputFolder: '' + _outputs: {} + isSampleDataFile: false name: SAMPLE BACKGROUND + outputFolders: {} + _outputs: {} samples: - name: H2O,_Can_N9 periodNumber: 1 dataFiles: - _dataFiles: [NIMROD00016608_H2O_in_N9.raw, NIMROD00016610_H2O_in_N9.raw] + _dataFiles: + - filename: NIMROD00016608_H2O_in_N9.raw + name: NIMROD00016608_H2O_in_N9 + ext: .raw + outputFolder: '' + _outputs: {} + isSampleDataFile: false + - filename: NIMROD00016610_H2O_in_N9.raw + name: NIMROD00016610_H2O_in_N9 + ext: .raw + outputFolder: '' + _outputs: {} + isSampleDataFile: false name: H2O,_Can_N9 + outputFolders: {} + _outputs: {} forceCalculationOfCorrections: true composition: type_: Sample @@ -139,8 +189,28 @@ sampleBackgrounds: - name: N9 periodNumber: 1 dataFiles: - _dataFiles: [NIMROD00016694_Empty_N9.raw, NIMROD00016699_Empty_N9.raw, NIMROD00016704_Empty_N9.raw] + _dataFiles: + - filename: NIMROD00016694_Empty_N9.raw + name: NIMROD00016694_Empty_N9 + ext: .raw + outputFolder: '' + _outputs: {} + isSampleDataFile: false + - filename: NIMROD00016699_Empty_N9.raw + name: NIMROD00016699_Empty_N9 + ext: .raw + outputFolder: '' + _outputs: {} + isSampleDataFile: false + - filename: NIMROD00016704_Empty_N9.raw + name: NIMROD00016704_Empty_N9 + ext: .raw + outputFolder: '' + _outputs: {} + isSampleDataFile: false name: N9 + outputFolders: {} + _outputs: {} composition: type_: Container elements: @@ -162,11 +232,28 @@ sampleBackgrounds: tweakFactor: 1.0 scatteringFraction: 1.0 attenuationCoefficient: 0.0 + outputFolder: '' + outputFolder: '' + sampleFile: '' - name: D2O,_Can_N10 periodNumber: 1 dataFiles: - _dataFiles: [NIMROD00016609_D2O_in_N10.raw, NIMROD00016611_D2O_in_N10.raw] + _dataFiles: + - filename: NIMROD00016609_D2O_in_N10.raw + name: NIMROD00016609_D2O_in_N10 + ext: .raw + outputFolder: '' + _outputs: {} + isSampleDataFile: false + - filename: NIMROD00016611_D2O_in_N10.raw + name: NIMROD00016611_D2O_in_N10 + ext: .raw + outputFolder: '' + _outputs: {} + isSampleDataFile: false name: D2O,_Can_N10 + outputFolders: {} + _outputs: {} forceCalculationOfCorrections: true composition: type_: Sample @@ -209,8 +296,28 @@ sampleBackgrounds: - name: N10 periodNumber: 1 dataFiles: - _dataFiles: [NIMROD00016695_Empty_N10.raw, NIMROD00016700_Empty_N10.raw, NIMROD00016705_Empty_N10.raw] + _dataFiles: + - filename: NIMROD00016695_Empty_N10.raw + name: NIMROD00016695_Empty_N10 + ext: .raw + outputFolder: '' + _outputs: {} + isSampleDataFile: false + - filename: NIMROD00016700_Empty_N10.raw + name: NIMROD00016700_Empty_N10 + ext: .raw + outputFolder: '' + _outputs: {} + isSampleDataFile: false + - filename: NIMROD00016705_Empty_N10.raw + name: NIMROD00016705_Empty_N10 + ext: .raw + outputFolder: '' + _outputs: {} + isSampleDataFile: false name: N10 + outputFolders: {} + _outputs: {} composition: type_: Container elements: @@ -232,11 +339,28 @@ sampleBackgrounds: tweakFactor: 1.0 scatteringFraction: 1.0 attenuationCoefficient: 0.0 + outputFolder: '' + outputFolder: '' + sampleFile: '' - name: HDO,_Can_N6 periodNumber: 1 dataFiles: - _dataFiles: [NIMROD00016741_HDO_in_N6.raw, NIMROD00016743_HDO_in_N6.raw] + _dataFiles: + - filename: NIMROD00016741_HDO_in_N6.raw + name: NIMROD00016741_HDO_in_N6 + ext: .raw + outputFolder: '' + _outputs: {} + isSampleDataFile: false + - filename: NIMROD00016743_HDO_in_N6.raw + name: NIMROD00016743_HDO_in_N6 + ext: .raw + outputFolder: '' + _outputs: {} + isSampleDataFile: false name: HDO,_Can_N6 + outputFolders: {} + _outputs: {} forceCalculationOfCorrections: true composition: type_: Sample @@ -280,8 +404,16 @@ sampleBackgrounds: - name: N6 periodNumber: 1 dataFiles: - _dataFiles: [NIMROD00014908_Empty_N6.raw] + _dataFiles: + - filename: NIMROD00014908_Empty_N6.raw + name: NIMROD00014908_Empty_N6 + ext: .raw + outputFolder: '' + _outputs: {} + isSampleDataFile: false name: N6 + outputFolders: {} + _outputs: {} composition: type_: Container elements: @@ -303,11 +435,28 @@ sampleBackgrounds: tweakFactor: 1.0 scatteringFraction: 1.0 attenuationCoefficient: 0.0 + outputFolder: '' + outputFolder: '' + sampleFile: '' - name: Null_Water,_Can_N8 periodNumber: 1 dataFiles: - dataFiles: [NIMROD00016742_NullWater_in_N8.raw, NIMROD00016744_NullWater_in_N8.raw] + _dataFiles: + - filename: NIMROD00016742_NullWater_in_N8.raw + name: NIMROD00016742_NullWater_in_N8 + ext: .raw + outputFolder: '' + _outputs: {} + isSampleDataFile: false + - filename: NIMROD00016744_NullWater_in_N8.raw + name: NIMROD00016744_NullWater_in_N8 + ext: .raw + outputFolder: '' + _outputs: {} + isSampleDataFile: false name: Null_Water,_Can_N8 + outputFolders: {} + _outputs: {} forceCalculationOfCorrections: true composition: type_: Sample @@ -351,8 +500,16 @@ sampleBackgrounds: - name: N8 periodNumber: 1 dataFiles: - _dataFiles: [NIMROD00016994_Empty_N8.raw] + _dataFiles: + - filename: NIMROD00016994_Empty_N8.raw + name: NIMROD00016994_Empty_N8 + ext: .raw + outputFolder: '' + _outputs: {} + isSampleDataFile: false name: N8 + outputFolders: {} + _outputs: {} composition: type_: Container elements: @@ -374,4 +531,8 @@ sampleBackgrounds: tweakFactor: 1.0 scatteringFraction: 1.0 attenuationCoefficient: 0.0 + outputFolder: '' + outputFolder: '' + sampleFile: '' + outputFolder: '' GUI: {useComponents: false} From 4a9ae78ee3ff4ddc152e17b27b3c25894c5395e3 Mon Sep 17 00:00:00 2001 From: Noella Spitz Date: Mon, 20 May 2024 11:59:01 +0100 Subject: [PATCH 48/78] Lint, delete gudpy_yaml.py --- gudpy/core/file_library.py | 1 - gudpy/core/gudpy_yaml.py | 258 ---------------------------- gudpy/core/io/gudrun_file_parser.py | 4 +- gudpy/gui/widgets/core/control.py | 1 - gudpy/test/test_purge_file.py | 1 - 5 files changed, 3 insertions(+), 262 deletions(-) delete mode 100644 gudpy/core/gudpy_yaml.py diff --git a/gudpy/core/file_library.py b/gudpy/core/file_library.py index c8feb0a2c..041e62022 100644 --- a/gudpy/core/file_library.py +++ b/gudpy/core/file_library.py @@ -1,6 +1,5 @@ import os from zipfile import ZipFile, ZIP_DEFLATED -from pathlib import Path from core import utils from core.enums import CrossSectionSource diff --git a/gudpy/core/gudpy_yaml.py b/gudpy/core/gudpy_yaml.py deleted file mode 100644 index ccbb1d415..000000000 --- a/gudpy/core/gudpy_yaml.py +++ /dev/null @@ -1,258 +0,0 @@ -from abc import abstractmethod -from enum import Enum -from ruamel.yaml import YAML as yaml -from ruamel.yaml import YAMLError -import os - -from core.composition import ( - Component, Components, Composition, WeightedComponent -) -from core.data_files import DataFile, DataFiles -from core.element import Element -from core.exception import YAMLException -from core.gui_config import GUIConfig -from core import utils - -from core.instrument import Instrument -from core.beam import Beam -from core.normalisation import Normalisation -from core.sample_background import SampleBackground -from core.sample import Sample -from core.container import Container -from core import config - - -class YAML: - def __init__(self): - self.yaml = self.getYamlModule() - - def getYamlModule(self): - yaml_ = yaml() - yaml_.preserve_quotes = True - yaml_.default_flow_style = None - yaml_.encoding = 'utf-8' - return yaml_ - - def parseYaml(self, path): - self.path = path - try: - parsedYAML = self.constructClasses(self.yamlToDict(path)) - except YAMLError as e: - # Exception caused by yaml parsing library - raise YAMLException(f"Invalid YAML file: {str(e)}") - except YAMLException as e: - # Exception caused by invalid arguments - raise YAMLException(e) - else: - return parsedYAML - - def yamlToDict(self, path): - # Read the input stream into our attribute. - with open(path, encoding=self.yaml.encoding) as fp: - return self.yaml.load(fp) - - def constructClasses(self, yamldict): - instrument = Instrument() - if "Instrument" in yamldict: - self.maskYAMLDicttoClass(instrument, yamldict["Instrument"]) - instrument.GudrunInputFileDir = os.path.dirname( - os.path.abspath( - self.path - ) - ) - - beam = Beam() - if "Beam" in yamldict: - self.maskYAMLDicttoClass(beam, yamldict["Beam"]) - - components = Components() - if "Components" in yamldict: - self.maskYAMLSeqtoClass(components, yamldict["Components"]) - - normalisation = Normalisation() - if "Normalisation" in yamldict: - self.maskYAMLDicttoClass(normalisation, yamldict["Normalisation"]) - - sampleBackgrounds = [] - if "SampleBackgrounds" in yamldict: - for sbyaml in yamldict["SampleBackgrounds"]: - sampleBackground = SampleBackground() - self.maskYAMLDicttoClass(sampleBackground, sbyaml) - sampleBackgrounds.append(sampleBackground) - - GUI = GUIConfig() - if "GUI" in yamldict: - self.maskYAMLDicttoClass(GUI, yamldict["GUI"]) - - return ( - instrument, beam, components, - normalisation, sampleBackgrounds, GUI - ) - - @abstractmethod - def maskYAMLDicttoClass(self, cls, yamldict): - for k, v in yamldict.items(): - if not hasattr(cls, k): - # If attribute is not valid - print(f"Ignoring depreciated attribute '{k}'" - f"given to '{type(cls).__name__}'") - continue - - if isinstance(cls.__dict__[k], Enum): - setattr(cls, k, type(cls.__dict__[k])[v]) - - elif isinstance(cls.__dict__[k], DataFiles): - for df in v["_dataFiles"]: - dataFile = DataFile(df["filename"]) - self.maskYAMLDicttoClass(dataFile, df) - - elif ( - isinstance( - cls, - (Component, Composition) - ) - and k == "elements" - ): - elements = [] - for idx, element in enumerate(v): - # Ensuring correct arguements are provided - if ( - "atomicSymbol" not in element - or "massNo" not in element - or "abundance" not in element - ): - raise YAMLException( - "Insufficient arguments given to element" - + f" {idx + 1}. Expects 'atomicSymbol', 'massNo'" - + " and 'abundance'" - ) - - # Setting element properties - try: - element_ = Element( - **{ - "atomicSymbol": element["atomicSymbol"], - "massNo": float(element["massNo"]), - "abundance": float(element["abundance"]) - } - ) - elements.append(element_) - except ValueError: - raise YAMLException( - f"Invalid number given to element {idx + 1}" - + f" in '{type(cls).__name__}") - setattr(cls, k, elements) - - elif isinstance(cls, Composition) and k == "weightedComponents": - weightedComponents = [] - for weightedComponent in v: - if ( - "component" not in weightedComponent - or "ratio" not in weightedComponent - ): - raise YAMLException( - "Weighted Component expects 'component' and" - + " 'ratio' to be provided") - component = Component() - self.maskYAMLDicttoClass( - component, weightedComponent["component"] - ) - ratio = weightedComponent["ratio"] - try: - weightedComponents.append( - WeightedComponent( - component, float(ratio)) - ) - except ValueError: - raise YAMLException( - "Invalid ratio given to Weighted Component") - setattr(cls, k, weightedComponents) - - elif ( - isinstance( - cls, - (Normalisation, Sample, Container) - ) - and k == "composition" - ): - self.maskYAMLDicttoClass(cls.__dict__[k], v) - - elif isinstance(cls, SampleBackground) and k == "samples": - for sampleyaml in yamldict[k]: - sample = Sample() - self.maskYAMLDicttoClass(sample, sampleyaml) - sample.name = utils.replace_unwanted_chars(sample.name) - cls.samples.append(sample) - - elif isinstance(cls, Sample) and k == "containers": - for contyaml in yamldict[k]: - container = Container() - self.maskYAMLDicttoClass(container, contyaml) - cls.containers.append(container) - - else: - setattr(cls, k, type(cls.__dict__[k])(self.toBuiltin(v))) - - def maskYAMLSeqtoClass(self, cls, yamlseq): - if isinstance(cls, Components): - components = [] - for component in yamlseq: - component_ = Component() - self.maskYAMLDicttoClass(component_, component) - components.append(component_) - setattr(cls, "components", components) - - def writeYAML(self, base, path): - with open(path, "wb") as fp: - outyaml = { - "Instrument": base.instrument, - "Beam": base.beam, - "Components": base.components.components, - "Normalisation": base.normalisation, - "SampleBackgrounds": base.sampleBackgrounds, - "GUI": config.GUI - } - self.yaml.dump( - {k: self.toYaml(v) for k, v in outyaml.items()}, - fp - ) - - @abstractmethod - def toYaml(self, var): - if var.__class__.__module__ == "ruamel.yaml.scalarfloat": - return float(var) - if var.__class__.__module__ == "builtins": - if isinstance(var, (list, tuple)): - return type(var)([self.toYaml(v) for v in var]) - else: - return var - elif isinstance(var, Enum): - return type(var)(var.value).name - elif isinstance(var, ( - Instrument, Beam, Components, Normalisation, - SampleBackground, Sample, Container, WeightedComponent, - Component, Composition, Element, DataFiles, DataFile, GUIConfig - )): - return { - k: self.toYaml(v) - for k, v in var.__dict__.items() - if k not in var.yamlignore - } - - @abstractmethod - def toBuiltin(self, yamlvar): - if yamlvar is None: - return None - if isinstance(yamlvar, (list, tuple)): - return [self.toBuiltin(v) for v in yamlvar] - elif (yamlvar.__class__.__module__ == - "ruamel.yaml.comments.CommentedMap"): - dict = {} - for k, v in yamlvar.items(): - dict[k] = v - elif yamlvar.__class__.__module__ == "builtins": - return yamlvar - elif yamlvar.__class__.__module__ == "ruamel.yaml.scalarfloat": - return float(yamlvar) - elif yamlvar.__class__.__module__ == "ruamel.yaml.scalarstring": - return str(yamlvar) diff --git a/gudpy/core/io/gudrun_file_parser.py b/gudpy/core/io/gudrun_file_parser.py index cee80941a..b95c36d39 100644 --- a/gudpy/core/io/gudrun_file_parser.py +++ b/gudpy/core/io/gudrun_file_parser.py @@ -1075,7 +1075,9 @@ def _parseContainer(self) -> Container: enums.CrossSectionSource[crossSectionSource] ) else: - container.totalCrossSectionSource = enums.CrossSectionSource.FILE + container.totalCrossSectionSource = ( + enums.CrossSectionSource.FILE + ) container.crossSectionFilename = crossSectionSource container.tweakFactor = utils.nthfloat(self._getNextToken(), 0) diff --git a/gudpy/gui/widgets/core/control.py b/gudpy/gui/widgets/core/control.py index ea28e0d78..568074702 100644 --- a/gudpy/gui/widgets/core/control.py +++ b/gudpy/gui/widgets/core/control.py @@ -1,5 +1,4 @@ import os -import re import sys import traceback import typing as typ diff --git a/gudpy/test/test_purge_file.py b/gudpy/test/test_purge_file.py index ec7b09919..0ca8b94fc 100644 --- a/gudpy/test/test_purge_file.py +++ b/gudpy/test/test_purge_file.py @@ -1,6 +1,5 @@ import os from unittest import TestCase -from shutil import copyfile import tempfile from core.purge_file import PurgeFile From a4cb2c607f07f86a718b7cc8d190a930f9fc0256 Mon Sep 17 00:00:00 2001 From: Noella Spitz Date: Mon, 20 May 2024 12:18:01 +0100 Subject: [PATCH 49/78] Small bug fixes --- gudpy/core/gudpy.py | 24 ++++++++++-------------- gudpy/test/test_gud_file.py | 7 ------- gudpy/test/test_gudpy_io.py | 12 ++++++------ 3 files changed, 16 insertions(+), 27 deletions(-) diff --git a/gudpy/core/gudpy.py b/gudpy/core/gudpy.py index 2a599879a..d76c55e94 100644 --- a/gudpy/core/gudpy.py +++ b/gudpy/core/gudpy.py @@ -230,6 +230,7 @@ def runGudrun(self, gudrunFile: GudrunFile = None): f"{self.gudrun.error}" ) self.gudrunOutput = self.gudrun.gudrunOutput + self.save() def iterateGudrun(self, iterator: iterators.Iterator): """Runs gudrun_dcs iteratively while tweaking parameters @@ -462,7 +463,6 @@ def gudrun( gudrunFile: GudrunFile, purge: Purge = None, iterator: iterators.Iterator = None, - save: bool = True ) -> int: self.checkBinary() if not purge: @@ -482,7 +482,7 @@ def gudrun( tmp, gudrunFile.OUTPATH ) - GudPyIO.writeGudrunFile(path) + GudPyIO.writeGudrunFile(gudrunFile, tmp) with subprocess.Popen( [self.BINARY_PATH, path], cwd=tmp, stdout=subprocess.PIPE, @@ -504,8 +504,6 @@ def gudrun( else: self.gudrunOutput = self.organiseOutput( gudrunFile, exclude=purgeFiles) - if save: - GudPyIO.save() gudrunFile.setGudrunDir(self.gudrunOutput.path) self.exitcode = 0 @@ -541,22 +539,21 @@ def singleIteration( gudrun: Gudrun, purge: Purge, prevOutput: handlers.GudrunOutput, - save=True ) -> typ.Tuple[int, str]: # (exitcode, error) modGfFile = self.iterator.performIteration(gudrunFile, prevOutput) - exitcode = gudrun.gudrun(modGfFile, purge, self.iterator, save=save) + exitcode = gudrun.gudrun(modGfFile, purge, self.iterator) if exitcode: return exitcode self.gudrunOutput = gudrun.gudrunOutput return 0 - def iterate(self, purge, save=True) -> typ.Tuple[int, str]: + def iterate(self, purge) -> typ.Tuple[int, str]: prevOutput = None # If the iterator requires a prelimenary run if self.iterator.requireDefault: exitcode = self.gudrunObjects[0].gudrun( - self.gudrunFile, purge, self.iterator, save) + self.gudrunFile, purge, self.iterator) if exitcode: # An exit code != 0 indicates failure self.exitcode = (exitcode, self.gudrunObjects[0].error) return self.exitcode @@ -568,7 +565,7 @@ def iterate(self, purge, save=True) -> typ.Tuple[int, str]: # If object has already been run, skip continue exitcode = self.singleIteration( - self.gudrunFile, gudrun, purge, prevOutput, save) + self.gudrunFile, gudrun, purge, prevOutput) if exitcode: # An exit code != 0 indicates failure self.exitcode = (exitcode, gudrun.error) return self.exitcode @@ -602,22 +599,21 @@ def singleIteration( gudrun: Gudrun, purge: Purge, prevOutput: handlers.GudrunOutput, - save=True ) -> typ.Tuple[int, str]: # (exitcode, error) modGfFile = self.iterator.performIteration(gudrunFile, prevOutput) - exitcode = gudrun.gudrun(modGfFile, purge, self.iterator, save=save) + exitcode = gudrun.gudrun(modGfFile, purge, self.iterator) if exitcode: return exitcode self.gudrunOutput = gudrun.gudrunOutput return 0 - def iterate(self, purge, save=True) -> typ.Tuple[int, str]: + def iterate(self, purge) -> typ.Tuple[int, str]: prevOutput = None # If the iterator requires a prelimenary run if self.iterator.requireDefault: exitcode = self.gudrunObjects[0].gudrun( - self.gudrunFile, purge, self.iterator, save) + self.gudrunFile, purge, self.iterator) if exitcode: # An exit code != 0 indicates failure self.exitcode = (exitcode, self.gudrunObjects[0].error) return self.exitcode @@ -629,7 +625,7 @@ def iterate(self, purge, save=True) -> typ.Tuple[int, str]: # If object has already been run, skip continue exitcode = self.singleIteration( - self.gudrunFile, gudrun, purge, prevOutput, save) + self.gudrunFile, gudrun, purge, prevOutput) if exitcode: # An exit code != 0 indicates failure self.exitcode = (exitcode, gudrun.error) return self.exitcode diff --git a/gudpy/test/test_gud_file.py b/gudpy/test/test_gud_file.py index 9b22af6a3..e268d57f4 100644 --- a/gudpy/test/test_gud_file.py +++ b/gudpy/test/test_gud_file.py @@ -251,13 +251,6 @@ def setUp(self) -> None: self.gudpy.gudrunFile.instrument.dataFileDir = str(dataFileDir) + "/" return super().setUp() - def tearDown(self) -> None: - [os.remove(f) for f in os.listdir() if f not in self.keepsakes] - [os.remove(os.path.join(self.gudpy.projectDir, f)) - for f in os.listdir(self.gudpy.projectDir) - if f not in self.keepsakes] - return super().tearDown() - def testEmptyPath(self): emptyPath = "" self.assertRaises(ParserException, GudFile, emptyPath) diff --git a/gudpy/test/test_gudpy_io.py b/gudpy/test/test_gudpy_io.py index 25d68ec71..f910d9be0 100644 --- a/gudpy/test/test_gudpy_io.py +++ b/gudpy/test/test_gudpy_io.py @@ -43,7 +43,7 @@ def __init__(self): ) gPath = os.path.join(self.tempdir.name, "good_water.txt") - GudPyIO.writeGudrunFile(gPath) + GudPyIO.writeGudrunFile(self.gudpy.gudrunFile, gPath) self.gudpy.loadFromGudrunFile( loadFile=gPath, @@ -805,7 +805,7 @@ def testLoadGudrunFile(self): def testWriteGudrunFile(self): with GudPyContext() as gudpy: - GudPyIO.writeGudrunFile(gudpy.io.loadFile) + GudPyIO.writeGudrunFile(gudpy.gudrunFile, gudpy.io.loadFile) with open( gudpy.io.loadFile, encoding="utf-8" @@ -894,7 +894,7 @@ def valueInLines(value, lines): def testRewriteGudrunFile(self): with GudPyContext() as gudpy: - GudPyIO.writeGudrunFile(gudpy.io.loadFile) + GudPyIO.writeGudrunFile(gudpy.gudrunFile, gudpy.io.loadFile) copyPath = os.path.join( gudpy.gudrunFile.instrument.GudrunInputFileDir, "copyGF.txt" @@ -902,7 +902,7 @@ def testRewriteGudrunFile(self): g1 = gudpy.io.importGudrunFile(gudpy.io.loadFile) g1.instrument.GudrunInputFileDir = ( gudpy.gudrunFile.instrument.GudrunInputFileDir) - GudPyIO.writeGudrunFile(copyPath) + GudPyIO.writeGudrunFile(g1, copyPath) def compareString(string1, string2): return string1 == string2 @@ -944,7 +944,7 @@ def compareString(string1, string2): def testReloadGudrunFile(self): with GudPyContext() as gudpy: - GudPyIO.writeGudrunFile(gudpy.io.loadFile) + GudPyIO.writeGudrunFile(gudpy.gudrunFile, gudpy.io.loadFile) g1 = gudpy.io.importGudrunFile() g1.instrument.GudrunInputFileDir = ( gudpy.gudrunFile.instrument.GudrunInputFileDir) @@ -1517,7 +1517,7 @@ def testAppendExponentialValues(self): gudpy.gudrunFile.OUTPATH ) - gudpy.io.writeGudrunFile(gfFilePath) + gudpy.io.writeGudrunFile(gudpy.gudrunFile, gfFilePath) gudrunFile = GudPyIO.importGudrunFile(gfFilePath) # Test that a default value is appended self.assertEqual( From 0ce46c18385b9b54298470d7293e7f0f896357d7 Mon Sep 17 00:00:00 2001 From: Noella Spitz Date: Mon, 20 May 2024 13:38:15 +0100 Subject: [PATCH 50/78] Lint: remove unused import --- gudpy/test/test_gudpy_yaml.py | 1 - 1 file changed, 1 deletion(-) diff --git a/gudpy/test/test_gudpy_yaml.py b/gudpy/test/test_gudpy_yaml.py index b58d89e2a..314b5fff2 100644 --- a/gudpy/test/test_gudpy_yaml.py +++ b/gudpy/test/test_gudpy_yaml.py @@ -2,7 +2,6 @@ import os from core import gudpy -from core.enums import Format class TestYAML(TestCase): From e3f17377078613d8c3c33b86d68f8a75bd6f4a1a Mon Sep 17 00:00:00 2001 From: Noella Spitz Date: Mon, 20 May 2024 13:52:52 +0100 Subject: [PATCH 51/78] Test path fis --- gudpy/test/test_gudpy_io.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/gudpy/test/test_gudpy_io.py b/gudpy/test/test_gudpy_io.py index f910d9be0..b39027d07 100644 --- a/gudpy/test/test_gudpy_io.py +++ b/gudpy/test/test_gudpy_io.py @@ -42,8 +42,8 @@ def __init__(self): loadFile=path, ) - gPath = os.path.join(self.tempdir.name, "good_water.txt") - GudPyIO.writeGudrunFile(self.gudpy.gudrunFile, gPath) + gPath = os.path.join(self.tempdir.name, GudrunFile.OUTPATH) + GudPyIO.writeGudrunFile(self.gudpy.gudrunFile, self.tempdir.name) self.gudpy.loadFromGudrunFile( loadFile=gPath, @@ -803,6 +803,12 @@ def testLoadGudrunFile(self): sampleBackgroundsAttrsDict[key], ) + def testSaveAsProject(self): + with GudPyContext() as gudpy: + gudpy.saveAs(os.path.join( + gudpy.projectDir, "test" + )) + def testWriteGudrunFile(self): with GudPyContext() as gudpy: GudPyIO.writeGudrunFile(gudpy.gudrunFile, gudpy.io.loadFile) From 44a8478adafde6ea12a9e0081432a145974c8840 Mon Sep 17 00:00:00 2001 From: Noella Spitz Date: Mon, 20 May 2024 14:41:25 +0100 Subject: [PATCH 52/78] Text updating --- gudpy/test/test_gudpy_io.py | 54 ++++++++++++++++++++----------- gudpy/test/test_gudrun_classes.py | 7 ++-- 2 files changed, 40 insertions(+), 21 deletions(-) diff --git a/gudpy/test/test_gudpy_io.py b/gudpy/test/test_gudpy_io.py index b39027d07..b939005c7 100644 --- a/gudpy/test/test_gudpy_io.py +++ b/gudpy/test/test_gudpy_io.py @@ -963,7 +963,8 @@ def testLoadEmptyGudrunFile(self): f = open("test_data.txt", "w", encoding="utf-8") f.close() with self.assertRaises(ParserException) as cm: - GudPyIO.importGudrunFile("test_data.txt") + with GudPyContext() as gudpy: + gudpy.loadFromGudrunFile("test_data.txt") self.assertEqual(( 'INSTRUMENT, BEAM and NORMALISATION' ' were not parsed. It\'s possible the file' @@ -983,7 +984,8 @@ def testLoadMissingInstrument(self): ) with self.assertRaises(ParserException) as cm: - GudPyIO.importGudrunFile("test_data.txt") + with GudPyContext() as gudpy: + gudpy.loadFromGudrunFile("test_data.txt") self.assertEqual(( 'INSTRUMENT, BEAM and NORMALISATION' ' were not parsed. It\'s possible the file' @@ -1005,7 +1007,8 @@ def testLoadMissingBeam(self): + "\n\n}" ) with self.assertRaises(ParserException) as cm: - GudPyIO.importGudrunFile("test_data.txt") + with GudPyContext() as gudpy: + gudpy.loadFromGudrunFile("test_data.txt") self.assertEqual(( 'INSTRUMENT, BEAM and NORMALISATION' ' were not parsed. It\'s possible the file' @@ -1024,7 +1027,8 @@ def testLoadMissingNormalisation(self): f.write("BEAM {\n\n" + str(self.goodBeam) + "\n\n}\n\n") with self.assertRaises(ParserException) as cm: - GudPyIO.importGudrunFile("test_data.txt") + with GudPyContext() as gudpy: + gudpy.loadFromGudrunFile("test_data.txt") self.assertEqual(( 'INSTRUMENT, BEAM and NORMALISATION' ' were not parsed. It\'s possible the file' @@ -1043,7 +1047,8 @@ def testLoadMissingInstrumentAndBeam(self): ) with self.assertRaises(ParserException) as cm: - GudPyIO.importGudrunFile("test_data.txt") + with GudPyContext() as gudpy: + gudpy.loadFromGudrunFile("test_data.txt") self.assertEqual(( 'INSTRUMENT, BEAM and NORMALISATION' ' were not parsed. It\'s possible the file supplied' @@ -1057,7 +1062,8 @@ def testLoadMissingInstrumentAndNormalisation(self): f.write("BEAM {\n\n" + str(self.goodBeam) + "\n\n}") with self.assertRaises(ParserException) as cm: - GudPyIO.importGudrunFile("test_data.txt") + with GudPyContext() as gudpy: + gudpy.loadFromGudrunFile("test_data.txt") self.assertEqual(( 'INSTRUMENT, BEAM and NORMALISATION' ' were not parsed. It\'s possible the file' @@ -1074,7 +1080,8 @@ def testLoadMissingNormalisationAndBeam(self): ) with self.assertRaises(ParserException) as cm: - GudPyIO.importGudrunFile("test_data.txt") + with GudPyContext() as gudpy: + gudpy.loadFromGudrunFile("test_data.txt") self.assertEqual(( 'INSTRUMENT, BEAM and NORMALISATION' ' were not parsed. It\'s possible the file' @@ -1106,7 +1113,8 @@ def testLoadMissingInstrumentAttributesSeq(self): ) with self.assertRaises(ParserException) as cm: - GudPyIO.importGudrunFile("test_data.txt") + with GudPyContext() as gudpy: + gudpy.loadFromGudrunFile("test_data.txt") self.assertEqual( "Whilst parsing Instrument, an exception occured." " The input file is most likely of an incorrect format, " @@ -1140,7 +1148,8 @@ def testLoadMissingInstrumentAttributesRand(self): "INSTRUMENT {\n\n" + str(badInstrument) + "\n\n}" ) with self.assertRaises(ParserException) as cm: - GudPyIO.importGudrunFile("test_data.txt") + with GudPyContext() as gudpy: + gudpy.loadFromGudrunFile("test_data.txt") self.assertEqual( "Whilst parsing Instrument, an exception occured." " The input file is most likely of an incorrect format, " @@ -1175,7 +1184,8 @@ def testLoadMissingBeamAttributesSeq(self): f.write("\n\nBEAM {\n\n" + str(badBeam) + "\n\n}") with self.assertRaises(ParserException) as cm: - GudPyIO.importGudrunFile("test_data.txt") + with GudPyContext() as gudpy: + gudpy.loadFromGudrunFile("test_data.txt") self.assertEqual( "Whilst parsing Beam, an exception occured." " The input file is most likely of an incorrect format, " @@ -1214,7 +1224,8 @@ def testLoadMissingBeamAttributesRand(self): f.write("\n\nBEAM {\n\n" + str(badBeam) + "\n\n}") with self.assertRaises(ParserException) as cm: - GudPyIO.importGudrunFile("test_data.txt") + with GudPyContext() as gudpy: + gudpy.loadFromGudrunFile("test_data.txt") self.assertEqual( "Whilst parsing Beam, an exception occured." " The input file is most likely of an incorrect format, " @@ -1258,7 +1269,8 @@ def testLoadMissingNormalisationAttributesSeq(self): ) with self.assertRaises(ParserException) as cm: - GudPyIO.importGudrunFile("test_data.txt") + with GudPyContext() as gudpy: + gudpy.loadFromGudrunFile("test_data.txt") self.assertEqual( "Whilst parsing Beam, an exception occured." " The input file is most likely of an incorrect format, " @@ -1306,7 +1318,8 @@ def testLoadMissingNormalisationAttributesRand(self): ) with self.assertRaises(ParserException) as cm: - GudPyIO.importGudrunFile("test_data.txt") + with GudPyContext() as gudpy: + gudpy.loadFromGudrunFile("test_data.txt") self.assertEqual( "Whilst parsing Normalisation, an exception occured." " The input file is most likely of an incorrect format, " @@ -1331,7 +1344,8 @@ def testLoadMissingSampleBackgroundAttributes(self): ) f.write("\n\n{}\n\nEND".format(str(badSampleBackground))) with self.assertRaises(ParserException) as cm: - GudPyIO.importGudrunFile("test_data.txt") + with GudPyContext() as gudpy: + gudpy.loadFromGudrunFile("test_data.txt") self.assertEqual( "Whilst parsing Sample Background, an exception occured." " The input file is most likely of an incorrect format, " @@ -1386,7 +1400,8 @@ def testLoadMissingSampleAttributesSeq(self): f.write("\n\n{}\n\nEND".format(str(badSampleBackground))) with self.assertRaises(ParserException) as cm: - GudPyIO.importGudrunFile("test_data.txt") + with GudPyContext() as gudpy: + gudpy.loadFromGudrunFile("test_data.txt") self.assertEqual( "Whilst parsing Sample, an exception occured." " The input file is most likely of an incorrect format, " @@ -1444,7 +1459,8 @@ def testLoadMissingSampleAttributesRand(self): f.write("\n\n{}\n\nEND".format(str(badSampleBackground))) with self.assertRaises(ParserException) as cm: - GudPyIO.importGudrunFile("test_data.txt") + with GudPyContext() as gudpy: + gudpy.loadFromGudrunFile("test_data.txt") self.assertEqual( "Whilst parsing Sample, an exception occured." " The input file is most likely of an incorrect format, " @@ -1504,7 +1520,8 @@ def testLoadMissingContainerAttributesSeq(self): ) f.write("\n\n{}\n\nEND".format(str(badSampleBackground))) with self.assertRaises(ParserException) as cm: - GudPyIO.importGudrunFile("test_data.txt") + with GudPyContext() as gudpy: + gudpy.loadFromGudrunFile("test_data.txt") self.assertEqual( "Whilst parsing Container, an exception occured." " The input file is most likely of an incorrect format, " @@ -1586,7 +1603,8 @@ def testLoadMissingContainerAttributesRand(self): ) f.write("\n\n{}\n\nEND".format(str(badSampleBackground))) with self.assertRaises(ParserException) as cm: - GudPyIO.importGudrunFile("test_data.txt") + with GudPyContext() as gudpy: + gudpy.loadFromGudrunFile("test_data.txt") self.assertEqual( "Whilst parsing Container, an exception occured." " The input file is most likely of an incorrect format, " diff --git a/gudpy/test/test_gudrun_classes.py b/gudpy/test/test_gudrun_classes.py index a783058d4..b3e21302a 100644 --- a/gudpy/test/test_gudrun_classes.py +++ b/gudpy/test/test_gudrun_classes.py @@ -15,17 +15,18 @@ FTModes, Instruments, Scales, UnitsOfDensity, MergeWeights, NormalisationType, OutputUnits, Geometry, CrossSectionSource ) +from core.io.gudpy_io import GudPyIO class TestGudrunClasses(TestCase): def testEmptyPath(self): - emptyPath = "" - self.assertRaises(RuntimeError, GudrunFile, loadFile=emptyPath) + self.assertRaises( + RuntimeError, GudPyIO.importGudrunFile, loadFile=emptyPath) def testInvalidPath(self): invalidPath = "invalid_path" - self.assertRaises(ParserException, GudrunFile, loadFile=invalidPath) + self.assertRaises(ParserException, GudPyIO.importGudrunFile, , loadFile=invalidPath) def testInstrumentInitDataTypes(self): From 58b44bf638840b94f5bd9db9b94373b0c8bac366 Mon Sep 17 00:00:00 2001 From: Noella Spitz Date: Mon, 20 May 2024 14:46:01 +0100 Subject: [PATCH 53/78] Syntax fix --- gudpy/test/test_gudrun_classes.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/gudpy/test/test_gudrun_classes.py b/gudpy/test/test_gudrun_classes.py index b3e21302a..2a44ae72a 100644 --- a/gudpy/test/test_gudrun_classes.py +++ b/gudpy/test/test_gudrun_classes.py @@ -26,7 +26,8 @@ def testEmptyPath(self): def testInvalidPath(self): invalidPath = "invalid_path" - self.assertRaises(ParserException, GudPyIO.importGudrunFile, , loadFile=invalidPath) + self.assertRaises( + ParserException, GudPyIO.importGudrunFile, loadFile=invalidPath) def testInstrumentInitDataTypes(self): From 6bc281e7053cdb24da139f0acddc2c5c4bb8aa52 Mon Sep 17 00:00:00 2001 From: Noella Spitz Date: Mon, 20 May 2024 15:02:04 +0100 Subject: [PATCH 54/78] Remove unused import, allow tests without lint --- .github/workflows/pr.yml | 1 - gudpy/test/test_gudrun_classes.py | 1 - 2 files changed, 2 deletions(-) diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index ecf008222..10ad186e7 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -18,7 +18,6 @@ jobs: uses: "./.github/workflows/lint" Test: - needs: Lint strategy: fail-fast: false matrix: diff --git a/gudpy/test/test_gudrun_classes.py b/gudpy/test/test_gudrun_classes.py index 2a44ae72a..236cfd495 100644 --- a/gudpy/test/test_gudrun_classes.py +++ b/gudpy/test/test_gudrun_classes.py @@ -1,7 +1,6 @@ from unittest import TestCase from core.exception import ParserException -from core.gudrun_file import GudrunFile from core.beam import Beam from core.composition import Composition from core.container import Container From bdfc56740ea8fdacc1a561ca35197385ca60a763 Mon Sep 17 00:00:00 2001 From: Noella Spitz Date: Mon, 20 May 2024 15:18:24 +0100 Subject: [PATCH 55/78] update test --- gudpy/core/io/gudrun_file_parser.py | 2 +- gudpy/test/test_gudpy_io.py | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/gudpy/core/io/gudrun_file_parser.py b/gudpy/core/io/gudrun_file_parser.py index b95c36d39..d85e1b9f1 100644 --- a/gudpy/core/io/gudrun_file_parser.py +++ b/gudpy/core/io/gudrun_file_parser.py @@ -117,7 +117,7 @@ def parse( if not v: unparsed.append(k) raise ParserException(( - f'{unparsed}' + f'{", ".join(unparsed)}' ' were not parsed. It\'s possible the file' ' supplied is of an incorrect format!' )) diff --git a/gudpy/test/test_gudpy_io.py b/gudpy/test/test_gudpy_io.py index b939005c7..add20595a 100644 --- a/gudpy/test/test_gudpy_io.py +++ b/gudpy/test/test_gudpy_io.py @@ -966,7 +966,7 @@ def testLoadEmptyGudrunFile(self): with GudPyContext() as gudpy: gudpy.loadFromGudrunFile("test_data.txt") self.assertEqual(( - 'INSTRUMENT, BEAM and NORMALISATION' + 'INSTRUMENT, BEAM, NORMALISATION,' ' were not parsed. It\'s possible the file' ' supplied is of an incorrect format!' ), @@ -987,7 +987,7 @@ def testLoadMissingInstrument(self): with GudPyContext() as gudpy: gudpy.loadFromGudrunFile("test_data.txt") self.assertEqual(( - 'INSTRUMENT, BEAM and NORMALISATION' + 'INSTRUMENT, BEAM, NORMALISATION,' ' were not parsed. It\'s possible the file' ' supplied is of an incorrect format!' ), @@ -1010,7 +1010,7 @@ def testLoadMissingBeam(self): with GudPyContext() as gudpy: gudpy.loadFromGudrunFile("test_data.txt") self.assertEqual(( - 'INSTRUMENT, BEAM and NORMALISATION' + 'INSTRUMENT, BEAM, NORMALISATION,' ' were not parsed. It\'s possible the file' ' supplied is of an incorrect format!' ), @@ -1030,7 +1030,7 @@ def testLoadMissingNormalisation(self): with GudPyContext() as gudpy: gudpy.loadFromGudrunFile("test_data.txt") self.assertEqual(( - 'INSTRUMENT, BEAM and NORMALISATION' + 'INSTRUMENT, BEAM, NORMALISATION,' ' were not parsed. It\'s possible the file' ' supplied is of an incorrect format!' ), @@ -1050,7 +1050,7 @@ def testLoadMissingInstrumentAndBeam(self): with GudPyContext() as gudpy: gudpy.loadFromGudrunFile("test_data.txt") self.assertEqual(( - 'INSTRUMENT, BEAM and NORMALISATION' + 'INSTRUMENT, BEAM, NORMALISATION,' ' were not parsed. It\'s possible the file supplied' ' is of an incorrect format!'), str(cm.exception), @@ -1065,7 +1065,7 @@ def testLoadMissingInstrumentAndNormalisation(self): with GudPyContext() as gudpy: gudpy.loadFromGudrunFile("test_data.txt") self.assertEqual(( - 'INSTRUMENT, BEAM and NORMALISATION' + 'INSTRUMENT, BEAM, NORMALISATION,' ' were not parsed. It\'s possible the file' ' supplied is of an incorrect format!' ), @@ -1083,7 +1083,7 @@ def testLoadMissingNormalisationAndBeam(self): with GudPyContext() as gudpy: gudpy.loadFromGudrunFile("test_data.txt") self.assertEqual(( - 'INSTRUMENT, BEAM and NORMALISATION' + 'INSTRUMENT, BEAM, NORMALISATION,' ' were not parsed. It\'s possible the file' ' supplied is of an incorrect format!' ), From 38dbb0cdb030bdf917f01e14a1f01090c267f04b Mon Sep 17 00:00:00 2001 From: Noella Spitz Date: Mon, 20 May 2024 15:39:16 +0100 Subject: [PATCH 56/78] Revert error message --- gudpy/core/io/gudrun_file_parser.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gudpy/core/io/gudrun_file_parser.py b/gudpy/core/io/gudrun_file_parser.py index d85e1b9f1..925fcdf04 100644 --- a/gudpy/core/io/gudrun_file_parser.py +++ b/gudpy/core/io/gudrun_file_parser.py @@ -117,7 +117,7 @@ def parse( if not v: unparsed.append(k) raise ParserException(( - f'{", ".join(unparsed)}' + 'INSTRUMENT, BEAM, NORMALISATION,' ' were not parsed. It\'s possible the file' ' supplied is of an incorrect format!' )) From 351d83a5fe019dcb80187db0e4c1d8991984c8b1 Mon Sep 17 00:00:00 2001 From: Noella Spitz Date: Mon, 20 May 2024 16:15:51 +0100 Subject: [PATCH 57/78] Add output folder attr to tests --- gudpy/test/test_gudpy_io.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/gudpy/test/test_gudpy_io.py b/gudpy/test/test_gudpy_io.py index add20595a..e7b7168ba 100644 --- a/gudpy/test/test_gudpy_io.py +++ b/gudpy/test/test_gudpy_io.py @@ -391,6 +391,7 @@ def setUp(self) -> None: "grBroadening": 0.0, "powerForBroadening": 0.0, "stepSize": 0.0, + "outputFolder": '', "yamlignore": { "runAsSample", "topHatW", @@ -454,6 +455,8 @@ def setUp(self) -> None: "scatteringFraction": 1.0, "attenuationCoefficient": 0.0, "containers": [self.expectedContainerA], + "outputFolder": '', + "sampleFile": '', "yamlignore": { "yamlignore", } @@ -509,6 +512,8 @@ def setUp(self) -> None: "scatteringFraction": 1.0, "attenuationCoefficient": 0.0, "containers": [self.expectedContainerB], + "outputFolder": '', + "sampleFile": '', "yamlignore": { "yamlignore" } @@ -564,6 +569,8 @@ def setUp(self) -> None: "scatteringFraction": 1.0, "attenuationCoefficient": 0.0, "containers": [self.expectedContainerC], + "outputFolder": '', + "sampleFile": '', "yamlignore": { "yamlignore" } @@ -620,6 +627,8 @@ def setUp(self) -> None: "scatteringFraction": 1.0, "attenuationCoefficient": 0.0, "containers": [self.expectedContainerD], + "outputFolder": '', + "sampleFile": '', "yamlignore": { "yamlignore" } @@ -645,6 +654,7 @@ def setUp(self) -> None: self.expectedSampleB, self.expectedSampleC, ], + "outputFolder": '', "writeAllSamples": True, "yamlignore": { "writeAllSamples", @@ -750,7 +760,6 @@ def testLoadGudrunFile(self): ) ) for key_ in sampleAttrsDict.keys(): - if key_ == "containers": for j, container in enumerate(sample[key_]): containerAttrsDict = ( @@ -1540,7 +1549,7 @@ def testAppendExponentialValues(self): gudpy.gudrunFile.OUTPATH ) - gudpy.io.writeGudrunFile(gudpy.gudrunFile, gfFilePath) + gudpy.io.writeGudrunFile(gudpy.gudrunFile, gudpy.io.projectDir) gudrunFile = GudPyIO.importGudrunFile(gfFilePath) # Test that a default value is appended self.assertEqual( From 20eb90d45acef56a500710edffb6b755a0833a85 Mon Sep 17 00:00:00 2001 From: Noella Spitz Date: Mon, 20 May 2024 16:29:55 +0100 Subject: [PATCH 58/78] Update tests --- gudpy/core/io/gudrun_file_parser.py | 1 - gudpy/core/iterators.py | 2 +- gudpy/test/test_gudpy_io.py | 2 +- gudpy/test/test_gudpy_workflows.py | 17 +- gudpy/test/test_gudpy_yaml.py | 251 ++++++++++++++-------------- gudpy/test/test_gudrun_classes.py | 4 +- 6 files changed, 139 insertions(+), 138 deletions(-) diff --git a/gudpy/core/io/gudrun_file_parser.py b/gudpy/core/io/gudrun_file_parser.py index 925fcdf04..5404da501 100644 --- a/gudpy/core/io/gudrun_file_parser.py +++ b/gudpy/core/io/gudrun_file_parser.py @@ -109,7 +109,6 @@ def parse( elif kwd == "NORMALISATION": normalisation = self._parseNormalisation() KEYWORDS[kwd] = True - # If we didn't parse each one of the keywords, then panic. if not all(KEYWORDS.values()) and not config: unparsed = [] diff --git a/gudpy/core/iterators.py b/gudpy/core/iterators.py index 34e8f1ac7..2c3ac3ab1 100644 --- a/gudpy/core/iterators.py +++ b/gudpy/core/iterators.py @@ -495,7 +495,7 @@ def setSelfScatteringFiles(self, scale, gudrunFile, prevOutput=None): for sample in sampleBackground.samples: if sample.runThisSample and len(sample.dataFiles): target = sample - filename = target.dataFiles[0].filename + filename = target.dataFilenames[0] targetFile = ( prevOutput.output( sample.name, filename, suffix) diff --git a/gudpy/test/test_gudpy_io.py b/gudpy/test/test_gudpy_io.py index e7b7168ba..3022d7f3e 100644 --- a/gudpy/test/test_gudpy_io.py +++ b/gudpy/test/test_gudpy_io.py @@ -960,7 +960,7 @@ def compareString(string1, string2): def testReloadGudrunFile(self): with GudPyContext() as gudpy: GudPyIO.writeGudrunFile(gudpy.gudrunFile, gudpy.io.loadFile) - g1 = gudpy.io.importGudrunFile() + g1 = gudpy.io.importGudrunFile(gudpy.io.loadFile) g1.instrument.GudrunInputFileDir = ( gudpy.gudrunFile.instrument.GudrunInputFileDir) self.assertEqual( diff --git a/gudpy/test/test_gudpy_workflows.py b/gudpy/test/test_gudpy_workflows.py index 5e8e2f101..f0102609c 100644 --- a/gudpy/test/test_gudpy_workflows.py +++ b/gudpy/test/test_gudpy_workflows.py @@ -76,10 +76,9 @@ def testGudPyDCS(self): actualMintFile = ("test/TestData/water-ref/plain/" f"{mintFilename}.mint01") - actualData = open(gudpy.gudrunOutput.sampleOutputs[ - sample.name].outputs[sample.dataFiles[0]][".mint01"], - "r", encoding="utf-8" - ).readlines()[10:] + actualData = open(sample.dataFiles[0].mintFile, + "r", encoding="utf-8" + ).readlines()[10:] expectedData = open( actualMintFile, "r", encoding="utf-8" ).readlines()[10:] @@ -244,8 +243,7 @@ def testGudPyIterateBySubtractingWavelength(self): ) actualData = open( - gudpy.gudrunIterator.iterator.gudrunOutputs[-1].output( - sample.name, sample.dataFiles[0], ".mint01"), + sample.dataFiles[0].mintFile, "r", encoding="utf-8" ).readlines()[10:] expectedData = open( @@ -273,12 +271,7 @@ def testGudPyIterateBySubtractingWavelength(self): ) actualData = open( - gudpy.gudrunIterator.iterator.gudrunOutputs[ - len( - gudpy.gudrunIterator.iterator.gudrunOutputs - ) - 2 - ].output( - sample.name, sample.dataFiles[0], ".msubw01"), + sample.dataFiles[0].msubwFile, "r", encoding="utf-8" ).readlines()[10:] expectedData = open( diff --git a/gudpy/test/test_gudpy_yaml.py b/gudpy/test/test_gudpy_yaml.py index 314b5fff2..7656a88e1 100644 --- a/gudpy/test/test_gudpy_yaml.py +++ b/gudpy/test/test_gudpy_yaml.py @@ -1,4 +1,5 @@ from unittest import TestCase +import tempfile import os from core import gudpy @@ -6,142 +7,150 @@ class TestYAML(TestCase): def testYAML(self): - gudpy1 = gudpy.GudPy() - gudpy1.loadFromGudrunFile( - loadFile="test/TestData/NIMROD-water/water.txt", - ) - gudpy1.save(path="test/TestData/NIMROD-water/water.yaml") - gf1 = gudpy1.gudrunFile - - gudpy2 = gudpy.GudPy() - gudpy2.loadFromYamlFile( - loadFile="test/TestData/NIMROD-water/water.yaml", - ) - gf2 = gudpy2.gudrunFile - - gf1.instrument.GudrunInputFileDir = os.path.abspath( - gf1.instrument.GudrunInputFileDir) - gf2.instrument.GudrunInputFileDir = os.path.abspath( - gf2.instrument.GudrunInputFileDir) - - self.assertDictEqual( - gf1.instrument.__dict__, gf2.instrument.__dict__) - self.assertDictEqual(gf2.beam.__dict__, gf2.beam.__dict__) - - normalisationDataFilesA = gf1.normalisation.__dict__.pop("dataFiles") - normalisationDataFilesBgA = gf1.normalisation.__dict__.pop( - "dataFilesBg" - ) - normalisationCompositionA = gf1.normalisation.__dict__.pop( - "composition" - ) - normalisationElementsA = normalisationCompositionA.__dict__.pop( - "elements" - ) - - normalisationDataFilesB = gf2.normalisation.__dict__.pop("dataFiles") - normalisationDataFilesBgB = gf2.normalisation.__dict__.pop( - "dataFilesBg" - ) - normalisationCompositionB = gf2.normalisation.__dict__.pop( - "composition" - ) - normalisationElementsB = normalisationCompositionB.__dict__.pop( - "elements" - ) - - self.assertDictEqual( - normalisationDataFilesA.__dict__, normalisationDataFilesB.__dict__ - ) - self.assertDictEqual( - normalisationDataFilesBgA.__dict__, - normalisationDataFilesBgB.__dict__, - ) - self.assertDictEqual( - normalisationCompositionA.__dict__, - normalisationCompositionB.__dict__, - ) - self.assertDictEqual( - gf1.normalisation.__dict__, gf2.normalisation.__dict__ - ) - - for elementA, elementB in zip( - normalisationElementsA, normalisationElementsB - ): - self.assertDictEqual(elementA.__dict__, elementB.__dict__) - - sampleBackgroundDataFilesA = gf1.sampleBackgrounds[0].__dict__.pop( - "dataFiles" - ) - sampleBackgroundSamplesA = gf1.sampleBackgrounds[0].__dict__.pop( - "samples" - ) - - sampleBackgroundDataFilesB = gf2.sampleBackgrounds[0].__dict__.pop( - "dataFiles" - ) - sampleBackgroundSamplesB = gf2.sampleBackgrounds[0].__dict__.pop( - "samples" - ) - - self.assertDictEqual( - sampleBackgroundDataFilesA.__dict__, - sampleBackgroundDataFilesB.__dict__, - ) - self.assertDictEqual( - gf1.sampleBackgrounds[0].__dict__, - gf2.sampleBackgrounds[0].__dict__, - ) - - for sampleA, sampleB in zip( - sampleBackgroundSamplesA, sampleBackgroundSamplesB - ): - sampleDataFilesA = sampleA.__dict__.pop("dataFiles") - sampleCompositionA = sampleA.__dict__.pop("composition") - sampleElementsA = sampleCompositionA.__dict__.pop("elements") - sampleContainersA = sampleA.__dict__.pop("containers") - - sampleDataFilesB = sampleB.__dict__.pop("dataFiles") - sampleCompositionB = sampleB.__dict__.pop("composition") - sampleElementsB = sampleCompositionB.__dict__.pop("elements") - sampleContainersB = sampleB.__dict__.pop("containers") + with tempfile.TemporaryDirectory() as tmp: + gudpy1 = gudpy.GudPy() + gudpy1.loadFromGudrunFile( + loadFile="test/TestData/NIMROD-water/water.txt", + ) + gudpy1.saveAs(os.path.join(tmp.name, "water")) + gf1 = gudpy1.gudrunFile + + gudpy2 = gudpy.GudPy() + gudpy2.loadFromProject(os.path.join(tmp.name, "water")) + gf2 = gudpy2.gudrunFile + + gf1.instrument.GudrunInputFileDir = os.path.abspath( + gf1.instrument.GudrunInputFileDir) + gf2.instrument.GudrunInputFileDir = os.path.abspath( + gf2.instrument.GudrunInputFileDir) + + self.assertDictEqual( + gf1.instrument.__dict__, gf2.instrument.__dict__) + self.assertDictEqual(gf2.beam.__dict__, gf2.beam.__dict__) + + normalisationDataFilesA = gf1.normalisation.__dict__.pop( + "dataFiles") + normalisationDataFilesBgA = gf1.normalisation.__dict__.pop( + "dataFilesBg" + ) + normalisationCompositionA = gf1.normalisation.__dict__.pop( + "composition" + ) + normalisationElementsA = normalisationCompositionA.__dict__.pop( + "elements" + ) + normalisationDataFilesB = gf2.normalisation.__dict__.pop( + "dataFiles") + normalisationDataFilesBgB = gf2.normalisation.__dict__.pop( + "dataFilesBg" + ) + normalisationCompositionB = gf2.normalisation.__dict__.pop( + "composition" + ) + normalisationElementsB = normalisationCompositionB.__dict__.pop( + "elements" + ) + + self.assertDictEqual( + normalisationDataFilesA.__dict__, + normalisationDataFilesB.__dict__ + ) + self.assertDictEqual( + normalisationDataFilesBgA.__dict__, + normalisationDataFilesBgB.__dict__, + ) self.assertDictEqual( - sampleDataFilesA.__dict__, sampleDataFilesB.__dict__ + normalisationCompositionA.__dict__, + normalisationCompositionB.__dict__, ) self.assertDictEqual( - sampleCompositionA.__dict__, sampleCompositionB.__dict__ + gf1.normalisation.__dict__, gf2.normalisation.__dict__ ) - for elementA, elementB in zip(sampleElementsA, sampleElementsB): + + for elementA, elementB in zip( + normalisationElementsA, normalisationElementsB + ): self.assertDictEqual(elementA.__dict__, elementB.__dict__) - self.assertDictEqual(sampleA.__dict__, sampleB.__dict__) + sampleBackgroundDataFilesA = gf1.sampleBackgrounds[0].__dict__.pop( + "dataFiles" + ) + sampleBackgroundSamplesA = gf1.sampleBackgrounds[0].__dict__.pop( + "samples" + ) - for containerA, containerB in zip( - sampleContainersA, sampleContainersB + sampleBackgroundDataFilesB = gf2.sampleBackgrounds[0].__dict__.pop( + "dataFiles" + ) + sampleBackgroundSamplesB = gf2.sampleBackgrounds[0].__dict__.pop( + "samples" + ) + + self.assertDictEqual( + sampleBackgroundDataFilesA.__dict__, + sampleBackgroundDataFilesB.__dict__, + ) + self.assertDictEqual( + gf1.sampleBackgrounds[0].__dict__, + gf2.sampleBackgrounds[0].__dict__, + ) + + for sampleA, sampleB in zip( + sampleBackgroundSamplesA, sampleBackgroundSamplesB ): - containerDataFilesA = containerA.__dict__.pop("dataFiles") - containerCompositionA = containerA.__dict__.pop("composition") - containerElementsA = containerCompositionA.__dict__.pop( - "elements" - ) + sampleDataFilesA = sampleA.__dict__.pop("dataFiles") + sampleCompositionA = sampleA.__dict__.pop("composition") + sampleElementsA = sampleCompositionA.__dict__.pop("elements") + sampleContainersA = sampleA.__dict__.pop("containers") - containerDataFilesB = containerB.__dict__.pop("dataFiles") - containerCompositionB = containerB.__dict__.pop("composition") - containerElementsB = containerCompositionB.__dict__.pop( - "elements" - ) + sampleDataFilesB = sampleB.__dict__.pop("dataFiles") + sampleCompositionB = sampleB.__dict__.pop("composition") + sampleElementsB = sampleCompositionB.__dict__.pop("elements") + sampleContainersB = sampleB.__dict__.pop("containers") self.assertDictEqual( - containerDataFilesA.__dict__, containerDataFilesB.__dict__ + sampleDataFilesA.__dict__, sampleDataFilesB.__dict__ ) self.assertDictEqual( - containerCompositionA.__dict__, - containerCompositionB.__dict__, + sampleCompositionA.__dict__, sampleCompositionB.__dict__ ) for elementA, elementB in zip( - containerElementsA, containerElementsB - ): + sampleElementsA, sampleElementsB): self.assertDictEqual(elementA.__dict__, elementB.__dict__) - self.assertDictEqual(containerA.__dict__, containerB.__dict__) + self.assertDictEqual(sampleA.__dict__, sampleB.__dict__) + + for containerA, containerB in zip( + sampleContainersA, sampleContainersB + ): + containerDataFilesA = containerA.__dict__.pop("dataFiles") + containerCompositionA = containerA.__dict__.pop( + "composition") + containerElementsA = containerCompositionA.__dict__.pop( + "elements" + ) + + containerDataFilesB = containerB.__dict__.pop("dataFiles") + containerCompositionB = containerB.__dict__.pop( + "composition") + containerElementsB = containerCompositionB.__dict__.pop( + "elements" + ) + + self.assertDictEqual( + containerDataFilesA.__dict__, + containerDataFilesB.__dict__ + ) + self.assertDictEqual( + containerCompositionA.__dict__, + containerCompositionB.__dict__, + ) + for elementA, elementB in zip( + containerElementsA, containerElementsB + ): + self.assertDictEqual( + elementA.__dict__, elementB.__dict__) + + self.assertDictEqual( + containerA.__dict__, containerB.__dict__) diff --git a/gudpy/test/test_gudrun_classes.py b/gudpy/test/test_gudrun_classes.py index 236cfd495..1a5e172de 100644 --- a/gudpy/test/test_gudrun_classes.py +++ b/gudpy/test/test_gudrun_classes.py @@ -21,12 +21,12 @@ class TestGudrunClasses(TestCase): def testEmptyPath(self): emptyPath = "" self.assertRaises( - RuntimeError, GudPyIO.importGudrunFile, loadFile=emptyPath) + RuntimeError, GudPyIO.importGudrunFile, emptyPath) def testInvalidPath(self): invalidPath = "invalid_path" self.assertRaises( - ParserException, GudPyIO.importGudrunFile, loadFile=invalidPath) + ParserException, GudPyIO.importGudrunFile, invalidPath) def testInstrumentInitDataTypes(self): From 92d56623e8e12bf05c2e4ca0da5a9b2ddc630b7a Mon Sep 17 00:00:00 2001 From: Noella Spitz Date: Tue, 21 May 2024 11:28:20 +0100 Subject: [PATCH 59/78] Add writeGudrunFileTo method --- gudpy/core/io/gudrun_file_parser.py | 14 ++++++++------ gudpy/test/test_gudpy_io.py | 12 ++++++------ 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/gudpy/core/io/gudrun_file_parser.py b/gudpy/core/io/gudrun_file_parser.py index 5404da501..6ad1fe864 100644 --- a/gudpy/core/io/gudrun_file_parser.py +++ b/gudpy/core/io/gudrun_file_parser.py @@ -29,18 +29,20 @@ def __init__(self): self.stream = None self.config = False - @classmethod - def writeGudrunFile(cls, gudrunFile: GudrunFile, runDir: str): - gudrunFile.setGudrunDir(runDir) + def writeGudrunFileTo(cls, gudrunFile: GudrunFile, path: str): + gudrunFile.setGudrunDir(os.path.dirname(path)) for sampleBackground in gudrunFile.sampleBackgrounds: sampleBackground.writeAllSamples = False - writePath = os.path.join(runDir, gudrunFile.OUTPATH) - - with open(writePath, "w", encoding="utf-8") as f: + with open(path, "w", encoding="utf-8") as f: f.write(str(gudrunFile)) f.close() + @classmethod + def writeGudrunFile(cls, gudrunFile: GudrunFile, runDir: str): + cls.writeGudrunFile(gudrunFile, os.path.join( + runDir, GudrunFile.OUTPATH)) + @classmethod def export(cls, gudrunFile: GudrunFile, path: str): for sampleBackground in gudrunFile.sampleBackgrounds: diff --git a/gudpy/test/test_gudpy_io.py b/gudpy/test/test_gudpy_io.py index 3022d7f3e..2fff3c034 100644 --- a/gudpy/test/test_gudpy_io.py +++ b/gudpy/test/test_gudpy_io.py @@ -43,7 +43,7 @@ def __init__(self): ) gPath = os.path.join(self.tempdir.name, GudrunFile.OUTPATH) - GudPyIO.writeGudrunFile(self.gudpy.gudrunFile, self.tempdir.name) + GudPyIO.writeGudrunFileTo(self.gudpy.gudrunFile, gPath) self.gudpy.loadFromGudrunFile( loadFile=gPath, @@ -820,7 +820,7 @@ def testSaveAsProject(self): def testWriteGudrunFile(self): with GudPyContext() as gudpy: - GudPyIO.writeGudrunFile(gudpy.gudrunFile, gudpy.io.loadFile) + GudPyIO.writeGudrunFileTo(gudpy.gudrunFile, gudpy.io.loadFile) with open( gudpy.io.loadFile, encoding="utf-8" @@ -909,7 +909,7 @@ def valueInLines(value, lines): def testRewriteGudrunFile(self): with GudPyContext() as gudpy: - GudPyIO.writeGudrunFile(gudpy.gudrunFile, gudpy.io.loadFile) + GudPyIO.writeGudrunFileTo(gudpy.gudrunFile, gudpy.io.loadFile) copyPath = os.path.join( gudpy.gudrunFile.instrument.GudrunInputFileDir, "copyGF.txt" @@ -917,7 +917,7 @@ def testRewriteGudrunFile(self): g1 = gudpy.io.importGudrunFile(gudpy.io.loadFile) g1.instrument.GudrunInputFileDir = ( gudpy.gudrunFile.instrument.GudrunInputFileDir) - GudPyIO.writeGudrunFile(g1, copyPath) + GudPyIO.writeGudrunFileTo(g1, copyPath) def compareString(string1, string2): return string1 == string2 @@ -959,7 +959,7 @@ def compareString(string1, string2): def testReloadGudrunFile(self): with GudPyContext() as gudpy: - GudPyIO.writeGudrunFile(gudpy.gudrunFile, gudpy.io.loadFile) + GudPyIO.writeGudrunFileTo(gudpy.gudrunFile, gudpy.io.loadFile) g1 = gudpy.io.importGudrunFile(gudpy.io.loadFile) g1.instrument.GudrunInputFileDir = ( gudpy.gudrunFile.instrument.GudrunInputFileDir) @@ -1549,7 +1549,7 @@ def testAppendExponentialValues(self): gudpy.gudrunFile.OUTPATH ) - gudpy.io.writeGudrunFile(gudpy.gudrunFile, gudpy.io.projectDir) + gudpy.io.writeGudrunFileTo(gudpy.gudrunFile, gfFilePath) gudrunFile = GudPyIO.importGudrunFile(gfFilePath) # Test that a default value is appended self.assertEqual( From 723acaf1883b42c182d4a7bd07823e0d5267703a Mon Sep 17 00:00:00 2001 From: Noella Spitz Date: Tue, 21 May 2024 13:31:12 +0100 Subject: [PATCH 60/78] Switch to correct parser --- gudpy/test/test_gudpy_io.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/gudpy/test/test_gudpy_io.py b/gudpy/test/test_gudpy_io.py index 2fff3c034..679e76a1a 100644 --- a/gudpy/test/test_gudpy_io.py +++ b/gudpy/test/test_gudpy_io.py @@ -26,6 +26,7 @@ ) from core import gudpy as gp from core.io.gudpy_io import GudPyIO +from core.io.gudrun_file_parser import GudrunFileParser class GudPyContext: @@ -43,7 +44,7 @@ def __init__(self): ) gPath = os.path.join(self.tempdir.name, GudrunFile.OUTPATH) - GudPyIO.writeGudrunFileTo(self.gudpy.gudrunFile, gPath) + GudrunFileParser.writeGudrunFileTo(self.gudpy.gudrunFile, gPath) self.gudpy.loadFromGudrunFile( loadFile=gPath, @@ -820,7 +821,8 @@ def testSaveAsProject(self): def testWriteGudrunFile(self): with GudPyContext() as gudpy: - GudPyIO.writeGudrunFileTo(gudpy.gudrunFile, gudpy.io.loadFile) + GudrunFileParser.writeGudrunFileTo( + gudpy.gudrunFile, gudpy.io.loadFile) with open( gudpy.io.loadFile, encoding="utf-8" @@ -909,7 +911,8 @@ def valueInLines(value, lines): def testRewriteGudrunFile(self): with GudPyContext() as gudpy: - GudPyIO.writeGudrunFileTo(gudpy.gudrunFile, gudpy.io.loadFile) + GudrunFileParser.writeGudrunFileTo( + gudpy.gudrunFile, gudpy.io.loadFile) copyPath = os.path.join( gudpy.gudrunFile.instrument.GudrunInputFileDir, "copyGF.txt" @@ -917,7 +920,7 @@ def testRewriteGudrunFile(self): g1 = gudpy.io.importGudrunFile(gudpy.io.loadFile) g1.instrument.GudrunInputFileDir = ( gudpy.gudrunFile.instrument.GudrunInputFileDir) - GudPyIO.writeGudrunFileTo(g1, copyPath) + GudrunFileParser.writeGudrunFileTo(g1, copyPath) def compareString(string1, string2): return string1 == string2 @@ -959,7 +962,8 @@ def compareString(string1, string2): def testReloadGudrunFile(self): with GudPyContext() as gudpy: - GudPyIO.writeGudrunFileTo(gudpy.gudrunFile, gudpy.io.loadFile) + GudrunFileParser.writeGudrunFileTo( + gudpy.gudrunFile, gudpy.io.loadFile) g1 = gudpy.io.importGudrunFile(gudpy.io.loadFile) g1.instrument.GudrunInputFileDir = ( gudpy.gudrunFile.instrument.GudrunInputFileDir) From dd47a3e25914d58545c82d18d3de70c5be5f7838 Mon Sep 17 00:00:00 2001 From: Noella Spitz Date: Tue, 21 May 2024 13:58:21 +0100 Subject: [PATCH 61/78] Add decorator --- gudpy/core/io/gudrun_file_parser.py | 1 + 1 file changed, 1 insertion(+) diff --git a/gudpy/core/io/gudrun_file_parser.py b/gudpy/core/io/gudrun_file_parser.py index 6ad1fe864..cccd69a11 100644 --- a/gudpy/core/io/gudrun_file_parser.py +++ b/gudpy/core/io/gudrun_file_parser.py @@ -29,6 +29,7 @@ def __init__(self): self.stream = None self.config = False + @classmethod def writeGudrunFileTo(cls, gudrunFile: GudrunFile, path: str): gudrunFile.setGudrunDir(os.path.dirname(path)) for sampleBackground in gudrunFile.sampleBackgrounds: From ced04ff5d95f634dec442f4e9979acc7a03cc665 Mon Sep 17 00:00:00 2001 From: Noella Spitz Date: Tue, 21 May 2024 15:10:39 +0100 Subject: [PATCH 62/78] Bugfixes --- gudpy/core/container.py | 3 +-- gudpy/core/io/gudrun_file_parser.py | 2 +- gudpy/core/iterators.py | 12 ++++-------- gudpy/test/test_gudpy_io.py | 5 ++--- gudpy/test/test_gudrun_classes.py | 6 ++++-- 5 files changed, 12 insertions(+), 16 deletions(-) diff --git a/gudpy/core/container.py b/gudpy/core/container.py index 4e9863633..dc491d9af 100644 --- a/gudpy/core/container.py +++ b/gudpy/core/container.py @@ -97,6 +97,7 @@ def __init__(self, config=None): self.grBroadening = 0. self.powerForBroadening = 0.0 self.stepSize = 0.0 + self.outputFolder = "" self.yamlignore = { "runAsSample", @@ -113,8 +114,6 @@ def __init__(self, config=None): if config: self.parseFromConfig(config) - self.outputFolder = "" - def __str__(self): """ Returns the string representation of the Container object. diff --git a/gudpy/core/io/gudrun_file_parser.py b/gudpy/core/io/gudrun_file_parser.py index cccd69a11..31e049a26 100644 --- a/gudpy/core/io/gudrun_file_parser.py +++ b/gudpy/core/io/gudrun_file_parser.py @@ -41,7 +41,7 @@ def writeGudrunFileTo(cls, gudrunFile: GudrunFile, path: str): @classmethod def writeGudrunFile(cls, gudrunFile: GudrunFile, runDir: str): - cls.writeGudrunFile(gudrunFile, os.path.join( + cls.writeGudrunFileTo(gudrunFile, os.path.join( runDir, GudrunFile.OUTPATH)) @classmethod diff --git a/gudpy/core/iterators.py b/gudpy/core/iterators.py index 2c3ac3ab1..2caccb44d 100644 --- a/gudpy/core/iterators.py +++ b/gudpy/core/iterators.py @@ -485,8 +485,6 @@ def setSelfScatteringFiles(self, scale, gudrunFile, prevOutput=None): extensions to msubw01. If the scale selected is the wavelength-scale, then set self scattering file extensions to mint01. """ - # Dict to pick suffix based on scale - suffix = {Scales.Q: ".msubw01", Scales.WAVELENGTH: ".mint01"}[scale] # Iterate through all of the samples, and set the suffixes of # all of their data files to the suffix @@ -495,12 +493,10 @@ def setSelfScatteringFiles(self, scale, gudrunFile, prevOutput=None): for sample in sampleBackground.samples: if sample.runThisSample and len(sample.dataFiles): target = sample - filename = target.dataFilenames[0] - targetFile = ( - prevOutput.output( - sample.name, filename, suffix) - if prevOutput else "" - ) + if scale == Scales.Q: + targetFile = target.dataFiles.dataFiles[0].msubwFile + else: + targetFile = target.dataFiles.dataFiles[0].mintFile target.fileSelfScattering = ( targetFile ) diff --git a/gudpy/test/test_gudpy_io.py b/gudpy/test/test_gudpy_io.py index 679e76a1a..b06312c0c 100644 --- a/gudpy/test/test_gudpy_io.py +++ b/gudpy/test/test_gudpy_io.py @@ -25,7 +25,6 @@ Geometry ) from core import gudpy as gp -from core.io.gudpy_io import GudPyIO from core.io.gudrun_file_parser import GudrunFileParser @@ -292,6 +291,7 @@ def setUp(self) -> None: "grBroadening": 0.0, "powerForBroadening": 0.0, "stepSize": 0.0, + "outputFolder": "", "yamlignore": { "runAsSample", "topHatW", @@ -771,7 +771,6 @@ def testLoadGudrunFile(self): ) for _key in containerAttrsDict.keys(): - if isinstance( container[_key], (DataFiles, Composition), @@ -1554,7 +1553,7 @@ def testAppendExponentialValues(self): ) gudpy.io.writeGudrunFileTo(gudpy.gudrunFile, gfFilePath) - gudrunFile = GudPyIO.importGudrunFile(gfFilePath) + gudrunFile = gudpy.io.importGudrunFile(gfFilePath) # Test that a default value is appended self.assertEqual( self.expectedSampleA["exponentialValues"], diff --git a/gudpy/test/test_gudrun_classes.py b/gudpy/test/test_gudrun_classes.py index 1a5e172de..26f8893b5 100644 --- a/gudpy/test/test_gudrun_classes.py +++ b/gudpy/test/test_gudrun_classes.py @@ -20,13 +20,15 @@ class TestGudrunClasses(TestCase): def testEmptyPath(self): emptyPath = "" + gudpyIO = GudPyIO() self.assertRaises( - RuntimeError, GudPyIO.importGudrunFile, emptyPath) + RuntimeError, gudpyIO.importGudrunFile, emptyPath) def testInvalidPath(self): invalidPath = "invalid_path" + gudpyIO = GudPyIO() self.assertRaises( - ParserException, GudPyIO.importGudrunFile, invalidPath) + ParserException, gudpyIO.importGudrunFile, invalidPath) def testInstrumentInitDataTypes(self): From 8c2683a3402a7cf6cc358bc288b2b5d0f1293384 Mon Sep 17 00:00:00 2001 From: Noella Spitz Date: Wed, 22 May 2024 10:31:05 +0100 Subject: [PATCH 63/78] Correctly identify sample files --- gudpy/core/container.py | 2 +- gudpy/core/data_files.py | 37 +++---------------- gudpy/core/io/gudrun_file_parser.py | 4 +- gudpy/core/iterators.py | 4 +- gudpy/core/sample.py | 2 +- .../NIMROD-water/good_water/good_water.yaml | 32 ++++++++-------- gudpy/test/test_gudpy_workflows.py | 2 +- 7 files changed, 28 insertions(+), 55 deletions(-) diff --git a/gudpy/core/container.py b/gudpy/core/container.py index dc491d9af..e1ac277c7 100644 --- a/gudpy/core/container.py +++ b/gudpy/core/container.py @@ -71,7 +71,7 @@ def __init__(self, config=None): """ self.name = "" self.periodNumber = 1 - self.dataFiles = DataFiles([], "CONTAINER") + self.dataFiles = DataFiles([], "CONTAINER", True) self.composition = Composition("CONTAINER") self.geometry = Geometry.SameAsBeam self.upstreamThickness = 0.1 diff --git a/gudpy/core/data_files.py b/gudpy/core/data_files.py index 6e5defc32..db1176ac0 100644 --- a/gudpy/core/data_files.py +++ b/gudpy/core/data_files.py @@ -21,26 +21,6 @@ def __init__(self, filename, isSampleDataFile: bool = False): "yamlignore" } - def detectSampleDataFile(self, path): - for f in os.listdir(path): - if f == "Diagnostics": - self.isSampleDataFile = True - - def constructDictFromDir(self, dirPath, dict): - for f in os.listdir(dirPath): - ext = os.path.splitext(f) - dict[ext] = f - - def importOutputs(self, path): - self.detectSampleDataFile(path) - if self.isSampleDataFile: - outputsDir = os.path.join(path, "Outputs") - diagDir = os.path.join(path, "Diagnostics") - self.constructDictFromDir(outputsDir, self._outputs) - self.constructDictFromDir(diagDir, self.diagnostics) - else: - self.constructDictFromDir(path, self._outputs) - def addOutput(self, path): ext = os.path.splitext(path)[1] self._outputs[ext] = path @@ -90,7 +70,7 @@ class DataFiles: ------- """ - def __init__(self, dataFiles, name): + def __init__(self, dataFiles, name, isSampleDataFile=False): """ Constructs all the necessary attributes for the DataFiles object. @@ -102,8 +82,9 @@ def __init__(self, dataFiles, name): Name of the parent of the data files, e.g. Sample Background """ - self._dataFiles = [] + self._dataFiles: DataFile = [] self.setFiles(dataFiles) + self.isSampleDataFile = isSampleDataFile self.name = name @@ -120,15 +101,7 @@ def setFiles(self, dataFilenames): for dataFile in dataFilenames: if not dataFile: continue - self._dataFiles.append(DataFile(dataFile)) - - def importOutputs(self, path): - self.outputFolder = path - children = os.listdir(path) - - for df in self._dataFiles: - if df.name in children: - return + self._dataFiles.append(DataFile(dataFile, self.isSampleDataFile)) def __str__(self): """ @@ -174,7 +147,7 @@ def __len__(self): return len(self._dataFiles) def __getitem__(self, n): - return self._dataFiles[n].filename + return self._dataFiles[n] def __setitem__(self, n, item): if n >= len(self): diff --git a/gudpy/core/io/gudrun_file_parser.py b/gudpy/core/io/gudrun_file_parser.py index 31e049a26..1c3e66e0e 100644 --- a/gudpy/core/io/gudrun_file_parser.py +++ b/gudpy/core/io/gudrun_file_parser.py @@ -789,7 +789,7 @@ def _parseSample(self) -> Sample: for _ in range(numberOfFiles): dataFiles.append(utils.firstword(self._getNextToken())) # Create a DataFiles object from the dataFiles list constructed. - sample.dataFiles = DataFiles(dataFiles, sample.name) + sample.dataFiles = DataFiles(dataFiles, sample.name, True) # For boolean attributes, we convert the first # integer in the line to its boolean value. @@ -994,7 +994,7 @@ def _parseContainer(self) -> Container: dataFiles.append(utils.firstword(self._getNextToken())) # Create a DataFiles object from the dataFiles list constructed. - container.dataFiles = DataFiles(dataFiles, container.name) + container.dataFiles = DataFiles(dataFiles, container.name, True) # Construct composition composition = [] diff --git a/gudpy/core/iterators.py b/gudpy/core/iterators.py index 2caccb44d..5921dfe5d 100644 --- a/gudpy/core/iterators.py +++ b/gudpy/core/iterators.py @@ -494,9 +494,9 @@ def setSelfScatteringFiles(self, scale, gudrunFile, prevOutput=None): if sample.runThisSample and len(sample.dataFiles): target = sample if scale == Scales.Q: - targetFile = target.dataFiles.dataFiles[0].msubwFile + targetFile = target.dataFiles[0].msubwFile else: - targetFile = target.dataFiles.dataFiles[0].mintFile + targetFile = target.dataFiles[0].mintFile target.fileSelfScattering = ( targetFile ) diff --git a/gudpy/core/sample.py b/gudpy/core/sample.py index f16b9254f..9d0b99d01 100644 --- a/gudpy/core/sample.py +++ b/gudpy/core/sample.py @@ -110,7 +110,7 @@ def __init__(self): """ self.name = "" self.periodNumber = 1 - self.dataFiles = DataFiles([], "SAMPLE") + self.dataFiles = DataFiles([], "SAMPLE", True) self.forceCalculationOfCorrections = True self.composition = Composition("SAMPLE") self.geometry = Geometry.SameAsBeam diff --git a/gudpy/test/TestData/NIMROD-water/good_water/good_water.yaml b/gudpy/test/TestData/NIMROD-water/good_water/good_water.yaml index 924e570f4..721fa9940 100644 --- a/gudpy/test/TestData/NIMROD-water/good_water/good_water.yaml +++ b/gudpy/test/TestData/NIMROD-water/good_water/good_water.yaml @@ -137,13 +137,13 @@ sampleBackgrounds: ext: .raw outputFolder: '' _outputs: {} - isSampleDataFile: false + isSampleDataFile: true - filename: NIMROD00016610_H2O_in_N9.raw name: NIMROD00016610_H2O_in_N9 ext: .raw outputFolder: '' _outputs: {} - isSampleDataFile: false + isSampleDataFile: true name: H2O,_Can_N9 outputFolders: {} _outputs: {} @@ -195,19 +195,19 @@ sampleBackgrounds: ext: .raw outputFolder: '' _outputs: {} - isSampleDataFile: false + isSampleDataFile: true - filename: NIMROD00016699_Empty_N9.raw name: NIMROD00016699_Empty_N9 ext: .raw outputFolder: '' _outputs: {} - isSampleDataFile: false + isSampleDataFile: true - filename: NIMROD00016704_Empty_N9.raw name: NIMROD00016704_Empty_N9 ext: .raw outputFolder: '' _outputs: {} - isSampleDataFile: false + isSampleDataFile: true name: N9 outputFolders: {} _outputs: {} @@ -244,13 +244,13 @@ sampleBackgrounds: ext: .raw outputFolder: '' _outputs: {} - isSampleDataFile: false + isSampleDataFile: true - filename: NIMROD00016611_D2O_in_N10.raw name: NIMROD00016611_D2O_in_N10 ext: .raw outputFolder: '' _outputs: {} - isSampleDataFile: false + isSampleDataFile: true name: D2O,_Can_N10 outputFolders: {} _outputs: {} @@ -302,19 +302,19 @@ sampleBackgrounds: ext: .raw outputFolder: '' _outputs: {} - isSampleDataFile: false + isSampleDataFile: true - filename: NIMROD00016700_Empty_N10.raw name: NIMROD00016700_Empty_N10 ext: .raw outputFolder: '' _outputs: {} - isSampleDataFile: false + isSampleDataFile: true - filename: NIMROD00016705_Empty_N10.raw name: NIMROD00016705_Empty_N10 ext: .raw outputFolder: '' _outputs: {} - isSampleDataFile: false + isSampleDataFile: true name: N10 outputFolders: {} _outputs: {} @@ -351,13 +351,13 @@ sampleBackgrounds: ext: .raw outputFolder: '' _outputs: {} - isSampleDataFile: false + isSampleDataFile: true - filename: NIMROD00016743_HDO_in_N6.raw name: NIMROD00016743_HDO_in_N6 ext: .raw outputFolder: '' _outputs: {} - isSampleDataFile: false + isSampleDataFile: true name: HDO,_Can_N6 outputFolders: {} _outputs: {} @@ -410,7 +410,7 @@ sampleBackgrounds: ext: .raw outputFolder: '' _outputs: {} - isSampleDataFile: false + isSampleDataFile: true name: N6 outputFolders: {} _outputs: {} @@ -447,13 +447,13 @@ sampleBackgrounds: ext: .raw outputFolder: '' _outputs: {} - isSampleDataFile: false + isSampleDataFile: true - filename: NIMROD00016744_NullWater_in_N8.raw name: NIMROD00016744_NullWater_in_N8 ext: .raw outputFolder: '' _outputs: {} - isSampleDataFile: false + isSampleDataFile: true name: Null_Water,_Can_N8 outputFolders: {} _outputs: {} @@ -506,7 +506,7 @@ sampleBackgrounds: ext: .raw outputFolder: '' _outputs: {} - isSampleDataFile: false + isSampleDataFile: true name: N8 outputFolders: {} _outputs: {} diff --git a/gudpy/test/test_gudpy_workflows.py b/gudpy/test/test_gudpy_workflows.py index f0102609c..f6079bde6 100644 --- a/gudpy/test/test_gudpy_workflows.py +++ b/gudpy/test/test_gudpy_workflows.py @@ -71,7 +71,7 @@ def testGudPyDCS(self): for sample in gudpy.gudrunFile.sampleBackgrounds[0].samples: mintFilename = ( - os.path.splitext(sample.dataFiles[0])[0] + os.path.splitext(sample.dataFiles[0].filename)[0] ) actualMintFile = ("test/TestData/water-ref/plain/" From 18cfc092b812cf810ce5356f453407ebdb0f9747 Mon Sep 17 00:00:00 2001 From: Noella Spitz Date: Wed, 22 May 2024 10:48:06 +0100 Subject: [PATCH 64/78] Fix output data paths --- gudpy/core/data_files.py | 9 ++++----- gudpy/core/iterators.py | 1 + gudpy/test/test_gudpy_workflows.py | 14 ++++---------- 3 files changed, 9 insertions(+), 15 deletions(-) diff --git a/gudpy/core/data_files.py b/gudpy/core/data_files.py index db1176ac0..8e0e9eab4 100644 --- a/gudpy/core/data_files.py +++ b/gudpy/core/data_files.py @@ -35,11 +35,10 @@ def __str__(self): def gudFile(self): if not self.isSampleDataFile: return None - if not self.gudFile: - gudPath = self._outputs.get(".gud", None) - if not gudPath: - return None - return GudFile(gudPath) + gudPath = self._outputs.get(".gud", None) + if not gudPath: + return None + return GudFile(gudPath) @property def mintFile(self): diff --git a/gudpy/core/iterators.py b/gudpy/core/iterators.py index 5921dfe5d..7b77529f4 100644 --- a/gudpy/core/iterators.py +++ b/gudpy/core/iterators.py @@ -597,6 +597,7 @@ def organiseOutput(self, gudrunFile, exclude=[]): outputHandler = handlers.GudrunOutputHandler( gudrunFile=gudrunFile, + head="Current", overwrite=True ) output = outputHandler.organiseOutput(exclude=exclude) diff --git a/gudpy/test/test_gudpy_workflows.py b/gudpy/test/test_gudpy_workflows.py index f6079bde6..168d31e73 100644 --- a/gudpy/test/test_gudpy_workflows.py +++ b/gudpy/test/test_gudpy_workflows.py @@ -42,9 +42,8 @@ def __exit__(self, exc_type, exc_value, tb): class TestGudPyWorkflows(TestCase): def getGudFile(self, gudpy, sampleIndex) -> GudFile: - return gudpy.gudrunOutput.sampleOutputs[ - gudpy.gudrunFile.sampleBackgrounds[0].samples[ - sampleIndex].name].gudFile + return gudpy.gudrunFile.sampleBackgrounds[0].samples[ + sampleIndex].dataFiles[0].gudFile def testGudPyDCS(self): with GudPyContext() as gudpy: @@ -70,9 +69,7 @@ def testGudPyDCS(self): self.assertAlmostEqual(dcsLevelPercentage, 13.0, 0) for sample in gudpy.gudrunFile.sampleBackgrounds[0].samples: - mintFilename = ( - os.path.splitext(sample.dataFiles[0].filename)[0] - ) + mintFilename = sample.dataFiles[0].name actualMintFile = ("test/TestData/water-ref/plain/" f"{mintFilename}.mint01") @@ -233,10 +230,7 @@ def testGudPyIterateBySubtractingWavelength(self): for x in gudpy.gudrunFile.sampleBackgrounds[0].samples if x.runThisSample ]: - dataFilename = ( - os.path.splitext(sample.dataFiles[0])[0] - ) - + dataFilename = sample.dataFiles[0].name actualMintFile = ( f'test/TestData/water-ref/wavelength{i}/' f'{dataFilename}.mint01' From 7d011002f7b01f1ace1099144006656a65132000 Mon Sep 17 00:00:00 2001 From: Noella Spitz Date: Wed, 22 May 2024 12:26:57 +0100 Subject: [PATCH 65/78] Fix iteration output handling --- gudpy/core/gudpy.py | 2 +- gudpy/core/gudrun_file.py | 5 +- gudpy/core/io/gudpy_io.py | 2 +- gudpy/core/io/gudrun_file_parser.py | 61 +++++++++++-------- gudpy/gui/widgets/core/control.py | 15 +++-- gudpy/gui/widgets/core/main_window.py | 8 ++- .../gui/widgets/dialogs/view_input_dialog.py | 8 +-- gudpy/test/test_gudpy_workflows.py | 9 ++- 8 files changed, 68 insertions(+), 42 deletions(-) diff --git a/gudpy/core/gudpy.py b/gudpy/core/gudpy.py index d76c55e94..775992d42 100644 --- a/gudpy/core/gudpy.py +++ b/gudpy/core/gudpy.py @@ -906,7 +906,7 @@ def bactchProcess( exitcode = gudrun.gudrun(GudrunFile, purge, iterator) self.writeDiagnosticsFile( os.path.join( - self.batchedGudrunFile.path(), + self.batchedGudrunFile.outputFolder, "batch_processing_diagnostics.txt", ), self.batchedGudrunFile, diff --git a/gudpy/core/gudrun_file.py b/gudpy/core/gudrun_file.py index 5dfdd1c3c..0ee890ed3 100644 --- a/gudpy/core/gudrun_file.py +++ b/gudpy/core/gudrun_file.py @@ -109,10 +109,7 @@ def __init__( self.sampleBackgrounds = sampleBackgrounds self.components = components self.purged = False - - @property - def path(self): - return os.path.join(self.outputFolder, self.OUTPATH) + self.outputFolder = "" def __deepcopy__(self, memo): result = self.__class__.__new__(self.__class__) diff --git a/gudpy/core/io/gudpy_io.py b/gudpy/core/io/gudpy_io.py index eed1887cf..dfd2cd33d 100644 --- a/gudpy/core/io/gudpy_io.py +++ b/gudpy/core/io/gudpy_io.py @@ -83,7 +83,7 @@ def save(self, gudrunFile): def importGudrunFile(self, path, config=False) -> GudrunFile: GudPyIO.loadFile = path - return self.gudrunFileParser.parse(path, config) + return self.gudrunFileParser.parseFromPath(path, config) def importProject(self, projectDir) -> GudrunFile: """Imports from a project directory diff --git a/gudpy/core/io/gudrun_file_parser.py b/gudpy/core/io/gudrun_file_parser.py index 1c3e66e0e..bfe29a513 100644 --- a/gudpy/core/io/gudrun_file_parser.py +++ b/gudpy/core/io/gudrun_file_parser.py @@ -53,13 +53,11 @@ def export(cls, gudrunFile: GudrunFile, path: str): f.write(str(gudrunFile)) f.close() - def parse( - self, path: str, config=False - ) -> typ.Tuple[ + def parse(self, lines: list[str]) -> typ.Tuple[ Instrument, Beam, Normalisation, list[SampleBackground], Components]: """ - Parse the GudrunFile from its path. + Parse the GudrunFile from a list of lines. Assign objects from the file to the attributes of the class. Raises ParserException if Instrument, Beam or Normalisation are missing. @@ -70,31 +68,12 @@ def parse( normalisation = Normalisation() sampleBackgrounds = [] - self.config = config - - # Ensure only valid files are given. - if not path: - raise ParserException( - "Path not supplied. Cannot parse from an empty path!" - ) - if not os.path.exists(path): - raise ParserException( - "The path supplied is invalid.\ - Cannot parse from an invalid path" + path - ) - KEYWORDS = { "INSTRUMENT": False, "BEAM": False, "NORMALISATION": False } - # Decide the encoding - import chardet - with open(path, 'rb') as fp: - encoding = chardet.detect(fp.read())['encoding'] - # Read the input stream into our attribute. - with open(path, encoding=encoding) as fp: - self.stream = fp.readlines() + self.stream = lines # Here we go! Get the first token and begin parsing. line = self._getNextToken() @@ -151,9 +130,43 @@ def parse( components=components ) + def parseFromPath( + self, path: str, config=False + ) -> typ.Tuple[ + Instrument, Beam, Normalisation, + list[SampleBackground], Components]: + """ + Parse the GudrunFile from its path. + Assign objects from the file to the attributes of the class. + Raises ParserException if Instrument, + Beam or Normalisation are missing. + """ + self.config = config + + # Ensure only valid files are given. + if not path: + raise ParserException( + "Path not supplied. Cannot parse from an empty path!" + ) + if not os.path.exists(path): + raise ParserException( + "The path supplied is invalid.\ + Cannot parse from an invalid path" + path + ) + + # Decide the encoding + import chardet + with open(path, 'rb') as fp: + encoding = chardet.detect(fp.read())['encoding'] + # Read the input stream into our attribute. + with open(path, encoding=encoding) as fp: + lines = fp.readlines() + return self.parse(lines) + # ================================================================= # PARSING HELPERS # ================================================================= + def _getNextToken(self) -> typ.Union[str, None]: """ Pops the 'next token' from the stream and returns it. diff --git a/gudpy/gui/widgets/core/control.py b/gudpy/gui/widgets/core/control.py index 568074702..a92ba776b 100644 --- a/gudpy/gui/widgets/core/control.py +++ b/gudpy/gui/widgets/core/control.py @@ -81,7 +81,7 @@ def connectUiSlots(self): self.mainWidget.ui.exportInputFile.triggered.connect( self.exportInputFile) self.mainWidget.ui.viewLiveInputFile.triggered.connect( - self.mainWidget.viewInput) + self.viewInput) self.mainWidget.ui.insertSampleBackground.triggered.connect( self.mainWidget.ui.objectTree.insertSampleBackground ) @@ -318,6 +318,12 @@ def autosave(self): if self.gudpy.checkSaveLocation() and not self.workerThread: self.gudpy.save() + def viewInput(self): + text = self.mainWidget.viewInput() + self.gudrunFile = self.gudpy.io.gudrunFileParser.parse( + text.splitlines()) + self.mainWidget.updateWidgets(self.gudrunFile) + """ PROCESSES @@ -460,7 +466,7 @@ def purgeFinished(self, exitcode): ) self.mainWidget.outputSlots.setOutput( self.gudpy.purge.output, "purge_det", - gudrunFile=self.gudrunFile + self.gudrunFile ) if isinstance(self.workerThread, gp.Purge): @@ -529,7 +535,8 @@ def gudrunFinished(self, exitcode): self.mainWidget.outputSlots.setOutput( self.gudpy.gudrunIterator.output, - f"Gudrun {self.gudpy.gudrunIterator.iterator.name}") + f"Gudrun {self.gudpy.gudrunIterator.iterator.name}", + self.gudrunFile) self.mainWidget.sampleSlots.setSample( self.mainWidget.sampleSlots.sample) self.mainWidget.iterationResultsDialog( @@ -547,7 +554,7 @@ def gudrunFinished(self, exitcode): ) return self.mainWidget.outputSlots.setOutput( - self.gudpy.gudrun.output, "Gudrun") + self.gudpy.gudrun.output, "Gudrun", self.gudrunFile) self.mainWidget.updateWidgets( gudrunFile=self.gudrunFile, gudrunOutput=self.gudpy.gudrun.gudrunOutput diff --git a/gudpy/gui/widgets/core/main_window.py b/gudpy/gui/widgets/core/main_window.py index 4487531d1..2bf0506b2 100644 --- a/gudpy/gui/widgets/core/main_window.py +++ b/gudpy/gui/widgets/core/main_window.py @@ -6,6 +6,7 @@ from core.container import Container from core.sample import Sample +from core.io.gudpy_io import GudPyIO from gui.widgets.dialogs.iterators import ( CompositionIterationDialog, @@ -705,7 +706,7 @@ def purgeOptionsMessageBox(self, text): def setModified(self): if not self.modified: - if self.gudrunFile.path(): + if GudPyIO.projectDir: self.modified = True self.ui.setWindowModified(True) self.ui.save.setEnabled(True) @@ -737,7 +738,7 @@ def setControlsEnabled(self, state): self.ui.viewLiveInputFile.setEnabled(state) self.ui.save.setEnabled( state & self.modified - if self.gudrunFile.path() + if GudPyIO.projectDir else False ) self.ui.exportInputFile.setEnabled(state) @@ -778,7 +779,8 @@ def setTreeActionsEnabled(self, state): def viewInput(self): self.currentState = str(self.gudrunFile) viewInputDialog = ViewInputDialog(self.gudrunFile, self) - viewInputDialog.widget.exec() + text = viewInputDialog.widget.exec() + return text def handleAllPlotModeChanged(self, index): plotMode = self.ui.allPlotComboBox.itemData(index) diff --git a/gudpy/gui/widgets/dialogs/view_input_dialog.py b/gudpy/gui/widgets/dialogs/view_input_dialog.py index bcce525e6..c45faaea6 100644 --- a/gudpy/gui/widgets/dialogs/view_input_dialog.py +++ b/gudpy/gui/widgets/dialogs/view_input_dialog.py @@ -3,6 +3,7 @@ from PySide6.QtCore import QFile from PySide6.QtWidgets import QDialog from PySide6.QtUiTools import QUiLoader +from core.io.gudpy_io import GudPyIO class ViewInputDialog(QDialog): @@ -52,7 +53,7 @@ def initComponents(self): ) loader = QUiLoader() self.widget = loader.load(uifile) - self.widget.setWindowTitle(self.gudrunFile.path()) + self.widget.setWindowTitle(GudPyIO.projectName) self.widget.saveAndCloseButton.clicked.connect(self.save) self.widget.closeButton.clicked.connect(self.widget.close) self.widget.textEdit.setText(str(self.gudrunFile)) @@ -67,7 +68,6 @@ def save(self): """ Saves the input file and updates the UI appropiately. """ - with open(self.gudrunFile.path(), "w", encoding="utf-8") as fp: - fp.write(self.widget.textEdit.toPlainText()) + text = self.widget.textEdit.toPlainText() self.widget.close() - self.parent.updateFromFile() + return text diff --git a/gudpy/test/test_gudpy_workflows.py b/gudpy/test/test_gudpy_workflows.py index 168d31e73..759826609 100644 --- a/gudpy/test/test_gudpy_workflows.py +++ b/gudpy/test/test_gudpy_workflows.py @@ -265,7 +265,14 @@ def testGudPyIterateBySubtractingWavelength(self): ) actualData = open( - sample.dataFiles[0].msubwFile, + os.path.join( + gudpy.projectDir, "Gudrun", + "Inelasticity_Subtraction_(WavelengthIteration)", + f"WavelengthIteration_{i}", + sample.name, + sample.dataFiles[0].name, + "Diagnostics", + f"{sample.dataFiles[0].name}.msubw01"), "r", encoding="utf-8" ).readlines()[10:] expectedData = open( From c5eef91bec0f062e44ff46a560796626d4e34c1d Mon Sep 17 00:00:00 2001 From: Noella Spitz Date: Wed, 22 May 2024 14:00:58 +0100 Subject: [PATCH 66/78] Fix order of datafile initialisation --- gudpy/core/data_files.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gudpy/core/data_files.py b/gudpy/core/data_files.py index 8e0e9eab4..0c82174f4 100644 --- a/gudpy/core/data_files.py +++ b/gudpy/core/data_files.py @@ -82,8 +82,8 @@ def __init__(self, dataFiles, name, isSampleDataFile=False): """ self._dataFiles: DataFile = [] - self.setFiles(dataFiles) self.isSampleDataFile = isSampleDataFile + self.setFiles(dataFiles) self.name = name From 603a65459c1b2ea7d8214ef648cdbcf4da7da343 Mon Sep 17 00:00:00 2001 From: Noella Spitz Date: Wed, 22 May 2024 16:15:26 +0100 Subject: [PATCH 67/78] Change exception assertions --- gudpy/test/test_gudpy_io.py | 134 ++++++++++-------------------- gudpy/test/test_gudpy_yaml.py | 4 +- gudpy/test/test_gudrun_classes.py | 2 +- 3 files changed, 45 insertions(+), 95 deletions(-) diff --git a/gudpy/test/test_gudpy_io.py b/gudpy/test/test_gudpy_io.py index b06312c0c..f54aa3ecb 100644 --- a/gudpy/test/test_gudpy_io.py +++ b/gudpy/test/test_gudpy_io.py @@ -236,6 +236,7 @@ def setUp(self) -> None: "grBroadening": 0.0, "powerForBroadening": 0.0, "stepSize": 0.0, + "outputFolder": '', "yamlignore": { "runAsSample", "topHatW", @@ -342,6 +343,7 @@ def setUp(self) -> None: "grBroadening": 0.0, "powerForBroadening": 0.0, "stepSize": 0.0, + "outputFolder": '', "yamlignore": { "runAsSample", "topHatW", @@ -1159,15 +1161,10 @@ def testLoadMissingInstrumentAttributesRand(self): f.write( "INSTRUMENT {\n\n" + str(badInstrument) + "\n\n}" ) - with self.assertRaises(ParserException) as cm: - with GudPyContext() as gudpy: - gudpy.loadFromGudrunFile("test_data.txt") - self.assertEqual( - "Whilst parsing Instrument, an exception occured." - " The input file is most likely of an incorrect format, " - "and some attributes were missing.", - str(cm.exception) - ) + with GudPyContext() as gudpy: + self.assertRaises( + ParserException, gudpy.loadFromGudrunFile, + "test_data.txt") def testLoadMissingBeamAttributesSeq(self): expectedBeam = deepcopy(self.expectedBeam) @@ -1195,15 +1192,10 @@ def testLoadMissingBeamAttributesSeq(self): ) f.write("\n\nBEAM {\n\n" + str(badBeam) + "\n\n}") - with self.assertRaises(ParserException) as cm: - with GudPyContext() as gudpy: - gudpy.loadFromGudrunFile("test_data.txt") - self.assertEqual( - "Whilst parsing Beam, an exception occured." - " The input file is most likely of an incorrect format, " - "and some attributes were missing.", - str(cm.exception) - ) + with GudPyContext() as gudpy: + self.assertRaises( + ParserException, gudpy.loadFromGudrunFile, + "test_data.txt") def testLoadMissingBeamAttributesRand(self): expectedBeam = deepcopy(self.expectedBeam) @@ -1235,15 +1227,10 @@ def testLoadMissingBeamAttributesRand(self): ) f.write("\n\nBEAM {\n\n" + str(badBeam) + "\n\n}") - with self.assertRaises(ParserException) as cm: - with GudPyContext() as gudpy: - gudpy.loadFromGudrunFile("test_data.txt") - self.assertEqual( - "Whilst parsing Beam, an exception occured." - " The input file is most likely of an incorrect format, " - "and some attributes were missing.", - str(cm.exception) - ) + with GudPyContext() as gudpy: + self.assertRaises( + ParserException, gudpy.loadFromGudrunFile, + "test_data.txt") def testLoadMissingNormalisationAttributesSeq(self): expectedNormalisation = deepcopy(self.expectedNormalisation) @@ -1280,15 +1267,10 @@ def testLoadMissingNormalisationAttributesSeq(self): + "\n\n}" ) - with self.assertRaises(ParserException) as cm: - with GudPyContext() as gudpy: - gudpy.loadFromGudrunFile("test_data.txt") - self.assertEqual( - "Whilst parsing Beam, an exception occured." - " The input file is most likely of an incorrect format, " - "and some attributes were missing.", - str(cm.exception) - ) + with GudPyContext() as gudpy: + self.assertRaises( + ParserException, gudpy.loadFromGudrunFile, + "test_data.txt") def testLoadMissingNormalisationAttributesRand(self): expectedNormalisation = deepcopy(self.expectedNormalisation) @@ -1328,16 +1310,10 @@ def testLoadMissingNormalisationAttributesRand(self): + str(badNormalisation) + "\n\n}" ) - - with self.assertRaises(ParserException) as cm: - with GudPyContext() as gudpy: - gudpy.loadFromGudrunFile("test_data.txt") - self.assertEqual( - "Whilst parsing Normalisation, an exception occured." - " The input file is most likely of an incorrect format, " - "and some attributes were missing.", - str(cm.exception) - ) + with GudPyContext() as gudpy: + self.assertRaises( + ParserException, gudpy.loadFromGudrunFile, + "test_data.txt") def testLoadMissingSampleBackgroundAttributes(self): badSampleBackground = str(self.goodSampleBackground).split("\n") @@ -1355,15 +1331,10 @@ def testLoadMissingSampleBackgroundAttributes(self): + "\n\n}" ) f.write("\n\n{}\n\nEND".format(str(badSampleBackground))) - with self.assertRaises(ParserException) as cm: with GudPyContext() as gudpy: - gudpy.loadFromGudrunFile("test_data.txt") - self.assertEqual( - "Whilst parsing Sample Background, an exception occured." - " The input file is most likely of an incorrect format, " - "and some attributes were missing.", - str(cm.exception) - ) + self.assertRaises( + ParserException, gudpy.loadFromGudrunFile, + "test_data.txt") def testLoadMissingSampleAttributesSeq(self): expectedSampleA = deepcopy(self.expectedSampleA) @@ -1411,15 +1382,10 @@ def testLoadMissingSampleAttributesSeq(self): ) f.write("\n\n{}\n\nEND".format(str(badSampleBackground))) - with self.assertRaises(ParserException) as cm: - with GudPyContext() as gudpy: - gudpy.loadFromGudrunFile("test_data.txt") - self.assertEqual( - "Whilst parsing Sample, an exception occured." - " The input file is most likely of an incorrect format, " - "and some attributes were missing.", - str(cm.exception) - ) + with GudPyContext() as gudpy: + self.assertRaises( + ParserException, gudpy.loadFromGudrunFile, + "test_data.txt") def testLoadMissingSampleAttributesRand(self): expectedSampleA = deepcopy(self.expectedSampleA) @@ -1469,16 +1435,10 @@ def testLoadMissingSampleAttributesRand(self): + "\n\n}" ) f.write("\n\n{}\n\nEND".format(str(badSampleBackground))) - - with self.assertRaises(ParserException) as cm: - with GudPyContext() as gudpy: - gudpy.loadFromGudrunFile("test_data.txt") - self.assertEqual( - "Whilst parsing Sample, an exception occured." - " The input file is most likely of an incorrect format, " - "and some attributes were missing.", - str(cm.exception) - ) + with GudPyContext() as gudpy: + self.assertRaises( + ParserException, gudpy.loadFromGudrunFile, + "test_data.txt") def testLoadMissingContainerAttributesSeq(self): expectedContainerA = deepcopy(self.expectedContainerA) @@ -1531,15 +1491,10 @@ def testLoadMissingContainerAttributesSeq(self): + "\n\n}" ) f.write("\n\n{}\n\nEND".format(str(badSampleBackground))) - with self.assertRaises(ParserException) as cm: - with GudPyContext() as gudpy: - gudpy.loadFromGudrunFile("test_data.txt") - self.assertEqual( - "Whilst parsing Container, an exception occured." - " The input file is most likely of an incorrect format, " - "and some attributes were missing.", - str(cm.exception) - ) + with GudPyContext() as gudpy: + self.assertRaises( + ParserException, gudpy.loadFromGudrunFile, + "test_data.txt") def testAppendExponentialValues(self): with GudPyContext() as gudpy: @@ -1552,7 +1507,7 @@ def testAppendExponentialValues(self): gudpy.gudrunFile.OUTPATH ) - gudpy.io.writeGudrunFileTo(gudpy.gudrunFile, gfFilePath) + GudrunFileParser.writeGudrunFileTo(gudpy.gudrunFile, gfFilePath) gudrunFile = gudpy.io.importGudrunFile(gfFilePath) # Test that a default value is appended self.assertEqual( @@ -1614,15 +1569,10 @@ def testLoadMissingContainerAttributesRand(self): + "\n\n}" ) f.write("\n\n{}\n\nEND".format(str(badSampleBackground))) - with self.assertRaises(ParserException) as cm: - with GudPyContext() as gudpy: - gudpy.loadFromGudrunFile("test_data.txt") - self.assertEqual( - "Whilst parsing Container, an exception occured." - " The input file is most likely of an incorrect format, " - "and some attributes were missing.", - str(cm.exception) - ) + with GudPyContext() as gudpy: + self.assertRaises( + ParserException, gudpy.loadFromGudrunFile, + "test_data.txt") def testZeroExitGudrun(self): with GudPyContext() as gudpy: diff --git a/gudpy/test/test_gudpy_yaml.py b/gudpy/test/test_gudpy_yaml.py index 7656a88e1..362151ae6 100644 --- a/gudpy/test/test_gudpy_yaml.py +++ b/gudpy/test/test_gudpy_yaml.py @@ -12,11 +12,11 @@ def testYAML(self): gudpy1.loadFromGudrunFile( loadFile="test/TestData/NIMROD-water/water.txt", ) - gudpy1.saveAs(os.path.join(tmp.name, "water")) + gudpy1.saveAs(os.path.join(tmp, "water")) gf1 = gudpy1.gudrunFile gudpy2 = gudpy.GudPy() - gudpy2.loadFromProject(os.path.join(tmp.name, "water")) + gudpy2.loadFromProject(os.path.join(tmp, "water")) gf2 = gudpy2.gudrunFile gf1.instrument.GudrunInputFileDir = os.path.abspath( diff --git a/gudpy/test/test_gudrun_classes.py b/gudpy/test/test_gudrun_classes.py index 26f8893b5..7e69a5850 100644 --- a/gudpy/test/test_gudrun_classes.py +++ b/gudpy/test/test_gudrun_classes.py @@ -22,7 +22,7 @@ def testEmptyPath(self): emptyPath = "" gudpyIO = GudPyIO() self.assertRaises( - RuntimeError, gudpyIO.importGudrunFile, emptyPath) + ParserException, gudpyIO.importGudrunFile, emptyPath) def testInvalidPath(self): invalidPath = "invalid_path" From e8364b275f9aff33e4ac68d9fec283925ab926a6 Mon Sep 17 00:00:00 2001 From: Noella Spitz Date: Thu, 23 May 2024 09:52:53 +0100 Subject: [PATCH 68/78] Fix assertions --- gudpy/test/test_gudpy_io.py | 50 +++++++++---------------------------- 1 file changed, 12 insertions(+), 38 deletions(-) diff --git a/gudpy/test/test_gudpy_io.py b/gudpy/test/test_gudpy_io.py index f54aa3ecb..4d50eab1e 100644 --- a/gudpy/test/test_gudpy_io.py +++ b/gudpy/test/test_gudpy_io.py @@ -1060,31 +1060,18 @@ def testLoadMissingInstrumentAndBeam(self): + "\n\n}" ) - with self.assertRaises(ParserException) as cm: - with GudPyContext() as gudpy: - gudpy.loadFromGudrunFile("test_data.txt") - self.assertEqual(( - 'INSTRUMENT, BEAM, NORMALISATION,' - ' were not parsed. It\'s possible the file supplied' - ' is of an incorrect format!'), - str(cm.exception), - ) + with GudPyContext() as gudpy: + self.assertRaises( + ParserException, gudpy.loadFromGudrunFile, "test_data.txt") def testLoadMissingInstrumentAndNormalisation(self): with open("test_data.txt", "w", encoding="utf-8") as f: f.write("' ' ' ' '/'\n\n") f.write("BEAM {\n\n" + str(self.goodBeam) + "\n\n}") - with self.assertRaises(ParserException) as cm: - with GudPyContext() as gudpy: - gudpy.loadFromGudrunFile("test_data.txt") - self.assertEqual(( - 'INSTRUMENT, BEAM, NORMALISATION,' - ' were not parsed. It\'s possible the file' - ' supplied is of an incorrect format!' - ), - str(cm.exception), - ) + with GudPyContext() as gudpy: + self.assertRaises( + ParserException, gudpy.loadFromGudrunFile, "test_data.txt") def testLoadMissingNormalisationAndBeam(self): with open("test_data.txt", "w", encoding="utf-8") as f: @@ -1093,16 +1080,9 @@ def testLoadMissingNormalisationAndBeam(self): "INSTRUMENT {\n\n" + str(self.goodInstrument) + "\n\n}" ) - with self.assertRaises(ParserException) as cm: - with GudPyContext() as gudpy: - gudpy.loadFromGudrunFile("test_data.txt") - self.assertEqual(( - 'INSTRUMENT, BEAM, NORMALISATION,' - ' were not parsed. It\'s possible the file' - ' supplied is of an incorrect format!' - ), - str(cm.exception), - ) + with GudPyContext() as gudpy: + self.assertRaises( + ParserException, gudpy.loadFromGudrunFile, "test_data.txt") def testLoadMissingInstrumentAttributesSeq(self): expectedInstrument = deepcopy(self.expectedInstrument) @@ -1126,15 +1106,9 @@ def testLoadMissingInstrumentAttributesSeq(self): "INSTRUMENT {\n\n" + str(badInstrument) + "\n\n}" ) - with self.assertRaises(ParserException) as cm: - with GudPyContext() as gudpy: - gudpy.loadFromGudrunFile("test_data.txt") - self.assertEqual( - "Whilst parsing Instrument, an exception occured." - " The input file is most likely of an incorrect format, " - "and some attributes were missing.", - str(cm.exception) - ) + with GudPyContext() as gudpy: + self.assertRaises( + ParserException, gudpy.loadFromGudrunFile, "test_data.txt") def testLoadMissingInstrumentAttributesRand(self): expectedInstrument = deepcopy(self.expectedInstrument) From 4d575664102f5864444b7320eab907bdf65318d4 Mon Sep 17 00:00:00 2001 From: Noella Spitz Date: Thu, 23 May 2024 10:02:04 +0100 Subject: [PATCH 69/78] Assertions --- gudpy/test/test_gudpy_io.py | 54 ++++++++++++------------------------- 1 file changed, 17 insertions(+), 37 deletions(-) diff --git a/gudpy/test/test_gudpy_io.py b/gudpy/test/test_gudpy_io.py index 4d50eab1e..daae1f69e 100644 --- a/gudpy/test/test_gudpy_io.py +++ b/gudpy/test/test_gudpy_io.py @@ -53,6 +53,10 @@ def __init__(self): self.tempdir.name, "good_water" )) + os.makedirs(os.path.join( + self.tempdir.name, "good_water" + )) + def __enter__(self): return self.gudpy @@ -976,16 +980,10 @@ def testReloadGudrunFile(self): def testLoadEmptyGudrunFile(self): f = open("test_data.txt", "w", encoding="utf-8") f.close() - with self.assertRaises(ParserException) as cm: - with GudPyContext() as gudpy: - gudpy.loadFromGudrunFile("test_data.txt") - self.assertEqual(( - 'INSTRUMENT, BEAM, NORMALISATION,' - ' were not parsed. It\'s possible the file' - ' supplied is of an incorrect format!' - ), - str(cm.exception), - ) + with GudPyContext() as gudpy: + self.assertRaises( + ParserException, gudpy.loadFromGudrunFile, + "test_data.txt") def testLoadMissingInstrument(self): with open("test_data.txt", "w", encoding="utf-8") as f: @@ -997,16 +995,10 @@ def testLoadMissingInstrument(self): + "\n\n}\n\n" ) - with self.assertRaises(ParserException) as cm: with GudPyContext() as gudpy: - gudpy.loadFromGudrunFile("test_data.txt") - self.assertEqual(( - 'INSTRUMENT, BEAM, NORMALISATION,' - ' were not parsed. It\'s possible the file' - ' supplied is of an incorrect format!' - ), - str(cm.exception), - ) + self.assertRaises( + ParserException, gudpy.loadFromGudrunFile, + "test_data.txt") def testLoadMissingBeam(self): with open("test_data.txt", "w", encoding="utf-8") as f: @@ -1020,16 +1012,10 @@ def testLoadMissingBeam(self): + str(self.goodNormalisation) + "\n\n}" ) - with self.assertRaises(ParserException) as cm: with GudPyContext() as gudpy: - gudpy.loadFromGudrunFile("test_data.txt") - self.assertEqual(( - 'INSTRUMENT, BEAM, NORMALISATION,' - ' were not parsed. It\'s possible the file' - ' supplied is of an incorrect format!' - ), - str(cm.exception), - ) + self.assertRaises( + ParserException, gudpy.loadFromGudrunFile, + "test_data.txt") def testLoadMissingNormalisation(self): with open("test_data.txt", "w", encoding="utf-8") as f: @@ -1040,16 +1026,10 @@ def testLoadMissingNormalisation(self): ) f.write("BEAM {\n\n" + str(self.goodBeam) + "\n\n}\n\n") - with self.assertRaises(ParserException) as cm: with GudPyContext() as gudpy: - gudpy.loadFromGudrunFile("test_data.txt") - self.assertEqual(( - 'INSTRUMENT, BEAM, NORMALISATION,' - ' were not parsed. It\'s possible the file' - ' supplied is of an incorrect format!' - ), - str(cm.exception), - ) + self.assertRaises( + ParserException, gudpy.loadFromGudrunFile, + "test_data.txt") def testLoadMissingInstrumentAndBeam(self): with open("test_data.txt", "w", encoding="utf-8") as f: From 9ad5c682b4b4b73494b7bda4174d54cc13d24f4f Mon Sep 17 00:00:00 2001 From: Noella Spitz Date: Thu, 23 May 2024 14:28:04 +0100 Subject: [PATCH 70/78] Remove elements from dict --- gudpy/core/utils.py | 4 + gudpy/test/TestData/NIMROD-water/water.txt | 2 +- gudpy/test/test_gudpy_io.py | 428 +++++++++++---------- 3 files changed, 225 insertions(+), 209 deletions(-) diff --git a/gudpy/core/utils.py b/gudpy/core/utils.py index 32dfe0530..1520a9504 100644 --- a/gudpy/core/utils.py +++ b/gudpy/core/utils.py @@ -23,6 +23,10 @@ def firstword(string): return string.split(" ")[0] +def lastword(string): + return string.split(" ")[-1] + + def replace_unwanted_chars(string): unwanted = [" "] for char in unwanted: diff --git a/gudpy/test/TestData/NIMROD-water/water.txt b/gudpy/test/TestData/NIMROD-water/water.txt index a6ecea359..ec85923c7 100644 --- a/gudpy/test/TestData/NIMROD-water/water.txt +++ b/gudpy/test/TestData/NIMROD-water/water.txt @@ -287,7 +287,7 @@ NIMROD00016742_NullWater_in_N8.msubw01 Name of file containing self sca CONTAINER N8 { -1 1 Number of files and period number +1 1 Number of files and period number NIMROD00016994_Empty_N8.raw CONTAINER N8 data files Ti 0 7.16 Composition Zr 0 3.438 Composition diff --git a/gudpy/test/test_gudpy_io.py b/gudpy/test/test_gudpy_io.py index daae1f69e..eb9460eee 100644 --- a/gudpy/test/test_gudpy_io.py +++ b/gudpy/test/test_gudpy_io.py @@ -37,6 +37,7 @@ def __init__(self): ) self.gudpy = gp.GudPy() + self.gudpy.testFilePath = os.path.join(testDir, "test_data.txt") self.gudpy.loadFromGudrunFile( loadFile=path, @@ -978,91 +979,90 @@ def testReloadGudrunFile(self): ) def testLoadEmptyGudrunFile(self): - f = open("test_data.txt", "w", encoding="utf-8") - f.close() with GudPyContext() as gudpy: + f = open(gudpy.testFilePath, "w", encoding="utf-8") + f.close() self.assertRaises( ParserException, gudpy.loadFromGudrunFile, - "test_data.txt") + gudpy.testFilePath) def testLoadMissingInstrument(self): - with open("test_data.txt", "w", encoding="utf-8") as f: - f.write("' ' ' ' '/'\n\n") - f.write("BEAM {\n\n" + str(self.goodBeam) + "\n\n}\n\n") - f.write( - "NORMALISATION {\n\n" - + str(self.goodNormalisation) - + "\n\n}\n\n" - ) - - with GudPyContext() as gudpy: - self.assertRaises( - ParserException, gudpy.loadFromGudrunFile, - "test_data.txt") + with GudPyContext() as gudpy: + with open(gudpy.testFilePath, "w", encoding="utf-8") as f: + f.write("' ' ' ' '/'\n\n") + f.write("BEAM {\n\n" + str(self.goodBeam) + "\n\n}\n\n") + f.write( + "NORMALISATION {\n\n" + + str(self.goodNormalisation) + + "\n\n}\n\n" + ) + self.assertRaises( + ParserException, gudpy.loadFromGudrunFile, + gudpy.testFilePath) def testLoadMissingBeam(self): - with open("test_data.txt", "w", encoding="utf-8") as f: - f.write("' ' ' ' '/'\n\n") - f.write( - "INSTRUMENT {\n\n" + str(self.goodInstrument) - + "\n\n}\n\n" - ) - f.write( - "NORMALISATION {\n\n" - + str(self.goodNormalisation) - + "\n\n}" - ) - with GudPyContext() as gudpy: - self.assertRaises( - ParserException, gudpy.loadFromGudrunFile, - "test_data.txt") + with GudPyContext() as gudpy: + with open(gudpy.testFilePath, "w", encoding="utf-8") as f: + f.write("' ' ' ' '/'\n\n") + f.write( + "INSTRUMENT {\n\n" + str(self.goodInstrument) + + "\n\n}\n\n" + ) + f.write( + "NORMALISATION {\n\n" + + str(self.goodNormalisation) + + "\n\n}" + ) + self.assertRaises( + ParserException, gudpy.loadFromGudrunFile, + gudpy.testFilePath) def testLoadMissingNormalisation(self): - with open("test_data.txt", "w", encoding="utf-8") as f: - f.write("' ' ' ' '/'\n\n") - f.write( - "INSTRUMENT {\n\n" + str(self.goodInstrument) - + "\n\n}\n\n" - ) - f.write("BEAM {\n\n" + str(self.goodBeam) + "\n\n}\n\n") - - with GudPyContext() as gudpy: - self.assertRaises( - ParserException, gudpy.loadFromGudrunFile, - "test_data.txt") + with GudPyContext() as gudpy: + with open(gudpy.testFilePath, "w", encoding="utf-8") as f: + f.write("' ' ' ' '/'\n\n") + f.write( + "INSTRUMENT {\n\n" + str(self.goodInstrument) + + "\n\n}\n\n" + ) + f.write("BEAM {\n\n" + str(self.goodBeam) + "\n\n}\n\n") + self.assertRaises( + ParserException, gudpy.loadFromGudrunFile, + gudpy.testFilePath) def testLoadMissingInstrumentAndBeam(self): - with open("test_data.txt", "w", encoding="utf-8") as f: - f.write("' ' ' ' '/'\n\n") - f.write( - "NORMALISATION {\n\n" - + str(self.goodNormalisation) - + "\n\n}" - ) - with GudPyContext() as gudpy: + with open(gudpy.testFilePath, "w", encoding="utf-8") as f: + f.write("' ' ' ' '/'\n\n") + f.write( + "NORMALISATION {\n\n" + + str(self.goodNormalisation) + + "\n\n}" + ) + self.assertRaises( - ParserException, gudpy.loadFromGudrunFile, "test_data.txt") + ParserException, gudpy.loadFromGudrunFile, gudpy.testFilePath) def testLoadMissingInstrumentAndNormalisation(self): - with open("test_data.txt", "w", encoding="utf-8") as f: - f.write("' ' ' ' '/'\n\n") - f.write("BEAM {\n\n" + str(self.goodBeam) + "\n\n}") - with GudPyContext() as gudpy: + with open(gudpy.testFilePath, "w", encoding="utf-8") as f: + f.write("' ' ' ' '/'\n\n") + f.write("BEAM {\n\n" + str(self.goodBeam) + "\n\n}") self.assertRaises( - ParserException, gudpy.loadFromGudrunFile, "test_data.txt") + ParserException, gudpy.loadFromGudrunFile, + gudpy.testFilePath) def testLoadMissingNormalisationAndBeam(self): - with open("test_data.txt", "w", encoding="utf-8") as f: - f.write("' ' ' ' '/'\n\n") - f.write( - "INSTRUMENT {\n\n" + str(self.goodInstrument) + "\n\n}" - ) - with GudPyContext() as gudpy: + with open(gudpy.testFilePath, "w", encoding="utf-8") as f: + f.write("' ' ' ' '/'\n\n") + f.write( + "INSTRUMENT {\n\n" + + str(self.goodInstrument) + "\n\n}" + ) self.assertRaises( - ParserException, gudpy.loadFromGudrunFile, "test_data.txt") + ParserException, gudpy.loadFromGudrunFile, + gudpy.testFilePath) def testLoadMissingInstrumentAttributesSeq(self): expectedInstrument = deepcopy(self.expectedInstrument) @@ -1080,15 +1080,16 @@ def testLoadMissingInstrumentAttributesSeq(self): del badInstrument[i] badInstrument = "\n".join(badInstrument) - with open("test_data.txt", "w", encoding="utf-8") as f: - f.write("' ' ' ' '/'\n\n") - f.write( - "INSTRUMENT {\n\n" + str(badInstrument) + "\n\n}" - ) - - with GudPyContext() as gudpy: - self.assertRaises( - ParserException, gudpy.loadFromGudrunFile, "test_data.txt") + with GudPyContext() as gudpy: + with open(gudpy.testFilePath, "w", encoding="utf-8") as f: + f.write("' ' ' ' '/'\n\n") + f.write( + "INSTRUMENT {\n\n" + + str(badInstrument) + "\n\n}" + ) + self.assertRaises( + ParserException, gudpy.loadFromGudrunFile, + gudpy.testFilePath) def testLoadMissingInstrumentAttributesRand(self): expectedInstrument = deepcopy(self.expectedInstrument) @@ -1102,7 +1103,6 @@ def testLoadMissingInstrumentAttributesRand(self): expectedInstrument.pop("goodDetectorThreshold", None) expectedInstrument.pop("yamlignore", None) for i in range(50): - key = random.choice(list(expectedInstrument)) j = list(expectedInstrument).index(key) @@ -1110,15 +1110,16 @@ def testLoadMissingInstrumentAttributesRand(self): del badInstrument[j] badInstrument = "\n".join(badInstrument) - with open("test_data.txt", "w", encoding="utf-8") as f: - f.write("' ' ' ' '/'\n\n") - f.write( - "INSTRUMENT {\n\n" + str(badInstrument) + "\n\n}" - ) with GudPyContext() as gudpy: + with open(gudpy.testFilePath, "w", encoding="utf-8") as f: + f.write("' ' ' ' '/'\n\n") + f.write( + "INSTRUMENT {\n\n" + + str(badInstrument) + "\n\n}" + ) self.assertRaises( ParserException, gudpy.loadFromGudrunFile, - "test_data.txt") + gudpy.testFilePath) def testLoadMissingBeamAttributesSeq(self): expectedBeam = deepcopy(self.expectedBeam) @@ -1136,20 +1137,18 @@ def testLoadMissingBeamAttributesSeq(self): badBeam = str(self.goodBeam).split("\n") del badBeam[i] badBeam = "\n".join(badBeam) - - with open("test_data.txt", "w", encoding="utf-8") as f: - f.write("' ' ' ' '/'\n\n") - f.write( - "INSTRUMENT {\n\n" - + str(self.goodInstrument) - + "\n\n}" - ) - f.write("\n\nBEAM {\n\n" + str(badBeam) + "\n\n}") - with GudPyContext() as gudpy: + with open(gudpy.testFilePath, "w", encoding="utf-8") as f: + f.write("' ' ' ' '/'\n\n") + f.write( + "INSTRUMENT {\n\n" + + str(self.goodInstrument) + + "\n\n}" + ) + f.write("\n\nBEAM {\n\n" + str(badBeam) + "\n\n}") self.assertRaises( ParserException, gudpy.loadFromGudrunFile, - "test_data.txt") + gudpy.testFilePath) def testLoadMissingBeamAttributesRand(self): expectedBeam = deepcopy(self.expectedBeam) @@ -1172,19 +1171,18 @@ def testLoadMissingBeamAttributesRand(self): del badBeam[j] badBeam = "\n".join(badBeam) - with open("test_data.txt", "w", encoding="utf-8") as f: - f.write("' ' ' ' '/'\n\n") - f.write( - "INSTRUMENT {\n\n" - + str(self.goodInstrument) - + "\n\n}" - ) - f.write("\n\nBEAM {\n\n" + str(badBeam) + "\n\n}") - with GudPyContext() as gudpy: + with open(gudpy.testFilePath, "w", encoding="utf-8") as f: + f.write("' ' ' ' '/'\n\n") + f.write( + "INSTRUMENT {\n\n" + + str(self.goodInstrument) + + "\n\n}" + ) + f.write("\n\nBEAM {\n\n" + str(badBeam) + "\n\n}") self.assertRaises( ParserException, gudpy.loadFromGudrunFile, - "test_data.txt") + gudpy.testFilePath) def testLoadMissingNormalisationAttributesSeq(self): expectedNormalisation = deepcopy(self.expectedNormalisation) @@ -1196,6 +1194,7 @@ def testLoadMissingNormalisationAttributesSeq(self): expectedNormalisation.pop("outerRadius", None) expectedNormalisation.pop("sampleHeight", None) expectedNormalisation.pop("crossSectionFilename") + expectedNormalisation.pop("outputFolder", None) expectedNormalisation.pop("yamlignore", None) self.goodNormalisation.dataFiles = DataFiles([], "") @@ -1206,25 +1205,24 @@ def testLoadMissingNormalisationAttributesSeq(self): badNormalisation = str(self.goodNormalisation).split("\n") del badNormalisation[i] badNormalisation = "\n".join(badNormalisation) - - with open("test_data.txt", "w", encoding="utf-8") as f: - f.write("' ' ' ' '/'\n\n") - f.write( - "INSTRUMENT {\n\n" - + str(self.goodInstrument) - + "\n\n}" - ) - f.write("\n\nBEAM {\n\n" + str(self.goodBeam) + "\n\n}") - f.write( - "\n\nNORMALISATION {\n\n" - + str(badNormalisation) - + "\n\n}" - ) - with GudPyContext() as gudpy: + with open(gudpy.testFilePath, "w", encoding="utf-8") as f: + f.write("' ' ' ' '/'\n\n") + f.write( + "INSTRUMENT {\n\n" + + str(self.goodInstrument) + + "\n\n}" + ) + f.write( + "\n\nBEAM {\n\n" + str(self.goodBeam) + "\n\n}") + f.write( + "\n\nNORMALISATION {\n\n" + + str(badNormalisation) + + "\n\n}" + ) self.assertRaises( ParserException, gudpy.loadFromGudrunFile, - "test_data.txt") + gudpy.testFilePath) def testLoadMissingNormalisationAttributesRand(self): expectedNormalisation = deepcopy(self.expectedNormalisation) @@ -1236,6 +1234,7 @@ def testLoadMissingNormalisationAttributesRand(self): expectedNormalisation.pop("outerRadius", None) expectedNormalisation.pop("sampleHeight", None) expectedNormalisation.pop("crossSectionFilename") + expectedNormalisation.pop("outputFolder", None) expectedNormalisation.pop("yamlignore", None) self.goodNormalisation.dataFiles = DataFiles([], "") @@ -1251,44 +1250,46 @@ def testLoadMissingNormalisationAttributesRand(self): del badNormalisation[j] badNormalisation = "\n".join(badNormalisation) - with open("test_data.txt", "w", encoding="utf-8") as f: - f.write("' ' ' ' '/'\n\n") - f.write( - "INSTRUMENT {\n\n" - + str(self.goodInstrument) - + "\n\n}" - ) - f.write("\n\nBEAM {\n\n" + str(self.goodBeam) + "\n\n}") - f.write( - "\n\nNORMALISATION {\n\n" - + str(badNormalisation) - + "\n\n}" - ) with GudPyContext() as gudpy: + with open(gudpy.testFilePath, "w", encoding="utf-8") as f: + f.write("' ' ' ' '/'\n\n") + f.write( + "INSTRUMENT {\n\n" + + str(self.goodInstrument) + + "\n\n}" + ) + f.write( + "\n\nBEAM {\n\n" + str(self.goodBeam) + "\n\n}") + f.write( + "\n\nNORMALISATION {\n\n" + + str(badNormalisation) + + "\n\n}" + ) self.assertRaises( ParserException, gudpy.loadFromGudrunFile, - "test_data.txt") + gudpy.testFilePath) def testLoadMissingSampleBackgroundAttributes(self): badSampleBackground = str(self.goodSampleBackground).split("\n") del badSampleBackground[2] badSampleBackground = "\n".join(badSampleBackground) - with open("test_data.txt", "w", encoding="utf-8") as f: - f.write("' ' ' ' '/'\n\n") - f.write( - "INSTRUMENT {\n\n" + str(self.goodInstrument) + "\n\n}" - ) - f.write("\n\nBEAM {\n\n" + str(self.goodBeam) + "\n\n}") - f.write( - "\n\nNORMALISATION {\n\n" - + str(self.goodNormalisation) - + "\n\n}" - ) - f.write("\n\n{}\n\nEND".format(str(badSampleBackground))) - with GudPyContext() as gudpy: - self.assertRaises( - ParserException, gudpy.loadFromGudrunFile, - "test_data.txt") + with GudPyContext() as gudpy: + with open(gudpy.testFilePath, "w", encoding="utf-8") as f: + f.write("' ' ' ' '/'\n\n") + f.write( + "INSTRUMENT {\n\n" + + str(self.goodInstrument) + "\n\n}" + ) + f.write("\n\nBEAM {\n\n" + str(self.goodBeam) + "\n\n}") + f.write( + "\n\nNORMALISATION {\n\n" + + str(self.goodNormalisation) + + "\n\n}" + ) + f.write("\n\n{}\n\nEND".format(str(badSampleBackground))) + self.assertRaises( + ParserException, gudpy.loadFromGudrunFile, + gudpy.testFilePath) def testLoadMissingSampleAttributesSeq(self): expectedSampleA = deepcopy(self.expectedSampleA) @@ -1304,6 +1305,8 @@ def testLoadMissingSampleAttributesSeq(self): expectedSampleA.pop("exponentialValues", None) expectedSampleA.pop("crossSectionFilename", None) expectedSampleA.pop("FTMode", None) + expectedSampleA.pop("outputFolder", None) + expectedSampleA.pop("sampleFile", None) expectedSampleA.pop("yamlignore", None) self.goodSampleBackground.samples[0].dataFiles = DataFiles([], "") @@ -1321,25 +1324,25 @@ def testLoadMissingSampleAttributesSeq(self): badSampleBackground = sbgStr.split("\n") del badSampleBackground[i + 10] badSampleBackground = "\n".join(badSampleBackground) - with open("test_data.txt", "w", encoding="utf-8") as f: - f.write("' ' ' ' '/'\n\n") - f.write( - "INSTRUMENT {\n\n" - + str(self.goodInstrument) - + "\n\n}" - ) - f.write("\n\nBEAM {\n\n" + str(self.goodBeam) + "\n\n}") - f.write( - "\n\nNORMALISATION {\n\n" - + str(self.goodNormalisation) - + "\n\n}" - ) - f.write("\n\n{}\n\nEND".format(str(badSampleBackground))) - with GudPyContext() as gudpy: + with open(gudpy.testFilePath, "w", encoding="utf-8") as f: + f.write("' ' ' ' '/'\n\n") + f.write( + "INSTRUMENT {\n\n" + + str(self.goodInstrument) + + "\n\n}" + ) + f.write( + "\n\nBEAM {\n\n" + str(self.goodBeam) + "\n\n}") + f.write( + "\n\nNORMALISATION {\n\n" + + str(self.goodNormalisation) + + "\n\n}" + ) + f.write("\n\n{}\n\nEND".format(str(badSampleBackground))) self.assertRaises( ParserException, gudpy.loadFromGudrunFile, - "test_data.txt") + gudpy.testFilePath) def testLoadMissingSampleAttributesRand(self): expectedSampleA = deepcopy(self.expectedSampleA) @@ -1355,6 +1358,8 @@ def testLoadMissingSampleAttributesRand(self): expectedSampleA.pop("exponentialValues", None) expectedSampleA.pop("crossSectionFilename", None) expectedSampleA.pop("FTMode", None) + expectedSampleA.pop("outputFolder", None) + expectedSampleA.pop("sampleFile", None) expectedSampleA.pop("yamlignore", None) self.goodSampleBackground.samples[0].dataFiles = DataFiles([], "") @@ -1375,24 +1380,25 @@ def testLoadMissingSampleAttributesRand(self): del badSampleBackground[j + 10] badSampleBackground = "\n".join(badSampleBackground) - with open("test_data.txt", "w", encoding="utf-8") as f: - f.write("' ' ' ' '/'\n\n") - f.write( - "INSTRUMENT {\n\n" - + str(self.goodInstrument) - + "\n\n}" - ) - f.write("\n\nBEAM {\n\n" + str(self.goodBeam) + "\n\n}") - f.write( - "\n\nNORMALISATION {\n\n" - + str(self.goodNormalisation) - + "\n\n}" - ) - f.write("\n\n{}\n\nEND".format(str(badSampleBackground))) with GudPyContext() as gudpy: + with open(gudpy.testFilePath, "w", encoding="utf-8") as f: + f.write("' ' ' ' '/'\n\n") + f.write( + "INSTRUMENT {\n\n" + + str(self.goodInstrument) + + "\n\n}" + ) + f.write( + "\n\nBEAM {\n\n" + str(self.goodBeam) + "\n\n}") + f.write( + "\n\nNORMALISATION {\n\n" + + str(self.goodNormalisation) + + "\n\n}" + ) + f.write("\n\n{}\n\nEND".format(str(badSampleBackground))) self.assertRaises( ParserException, gudpy.loadFromGudrunFile, - "test_data.txt") + gudpy.testFilePath) def testLoadMissingContainerAttributesSeq(self): expectedContainerA = deepcopy(self.expectedContainerA) @@ -1415,6 +1421,7 @@ def testLoadMissingContainerAttributesSeq(self): expectedContainerA.pop("powerForBroadening", None) expectedContainerA.pop("stepSize", None) expectedContainerA.pop("yamlignore", None) + expectedContainerA.pop("outputFolder", None) self.goodSampleBackground.samples[0].containers[0].dataFiles = ( DataFiles([], "") @@ -1431,24 +1438,25 @@ def testLoadMissingContainerAttributesSeq(self): del badSampleBackground[i + 44] badSampleBackground = "\n".join(badSampleBackground) - with open("test_data.txt", "w", encoding="utf-8") as f: - f.write("' ' ' ' '/'\n\n") - f.write( - "INSTRUMENT {\n\n" - + str(self.goodInstrument) - + "\n\n}" - ) - f.write("\n\nBEAM {\n\n" + str(self.goodBeam) + "\n\n}") - f.write( - "\n\nNORMALISATION {\n\n" - + str(self.goodNormalisation) - + "\n\n}" - ) - f.write("\n\n{}\n\nEND".format(str(badSampleBackground))) with GudPyContext() as gudpy: + with open(gudpy.testFilePath, "w", encoding="utf-8") as f: + f.write("' ' ' ' '/'\n\n") + f.write( + "INSTRUMENT {\n\n" + + str(self.goodInstrument) + + "\n\n}" + ) + f.write( + "\n\nBEAM {\n\n" + str(self.goodBeam) + "\n\n}") + f.write( + "\n\nNORMALISATION {\n\n" + + str(self.goodNormalisation) + + "\n\n}" + ) + f.write("\n\n{}\n\nEND".format(str(badSampleBackground))) self.assertRaises( ParserException, gudpy.loadFromGudrunFile, - "test_data.txt") + gudpy.testFilePath) def testAppendExponentialValues(self): with GudPyContext() as gudpy: @@ -1490,6 +1498,7 @@ def testLoadMissingContainerAttributesRand(self): expectedContainerA.pop("powerForBroadening", None) expectedContainerA.pop("stepSize", None) expectedContainerA.pop("yamlignore", None) + expectedContainerA.pop("outputFolder", None) self.goodSampleBackground.samples[0].containers[0].dataFiles = ( DataFiles([], "") @@ -1506,27 +1515,30 @@ def testLoadMissingContainerAttributesRand(self): ] sbgStr = str(self.goodSampleBackground) badSampleBackground = sbgStr.split("\n") + if not badSampleBackground[j + 44]: + continue del badSampleBackground[j + 44] badSampleBackground = "\n".join(badSampleBackground) - with open("test_data.txt", "w", encoding="utf-8") as f: - f.write("' ' ' ' '/'\n\n") - f.write( - "INSTRUMENT {\n\n" - + str(self.goodInstrument) - + "\n\n}" - ) - f.write("\n\nBEAM {\n\n" + str(self.goodBeam) + "\n\n}") - f.write( - "\n\nNORMALISATION {\n\n" - + str(self.goodNormalisation) - + "\n\n}" - ) - f.write("\n\n{}\n\nEND".format(str(badSampleBackground))) with GudPyContext() as gudpy: + with open(gudpy.testFilePath, "w", encoding="utf-8") as f: + f.write("' ' ' ' '/'\n\n") + f.write( + "INSTRUMENT {\n\n" + + str(self.goodInstrument) + + "\n\n}" + ) + f.write( + "\n\nBEAM {\n\n" + str(self.goodBeam) + "\n\n}") + f.write( + "\n\nNORMALISATION {\n\n" + + str(self.goodNormalisation) + + "\n\n}" + ) + f.write("\n\n{}\n\nEND".format(str(badSampleBackground))) self.assertRaises( ParserException, gudpy.loadFromGudrunFile, - "test_data.txt") + gudpy.testFilePath) def testZeroExitGudrun(self): with GudPyContext() as gudpy: From f421d3822aa64bc14f274923fe6bba8ae0fe9d5e Mon Sep 17 00:00:00 2001 From: Noella Spitz Date: Thu, 23 May 2024 14:29:52 +0100 Subject: [PATCH 71/78] Fix config setting --- gudpy/core/io/gudrun_file_parser.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/gudpy/core/io/gudrun_file_parser.py b/gudpy/core/io/gudrun_file_parser.py index bfe29a513..c82c52e61 100644 --- a/gudpy/core/io/gudrun_file_parser.py +++ b/gudpy/core/io/gudrun_file_parser.py @@ -53,9 +53,7 @@ def export(cls, gudrunFile: GudrunFile, path: str): f.write(str(gudrunFile)) f.close() - def parse(self, lines: list[str]) -> typ.Tuple[ - Instrument, Beam, Normalisation, - list[SampleBackground], Components]: + def parse(self, lines: list[str], config=False) -> GudrunFile: """ Parse the GudrunFile from a list of lines. Assign objects from the file to the attributes of the class. @@ -161,7 +159,7 @@ def parseFromPath( # Read the input stream into our attribute. with open(path, encoding=encoding) as fp: lines = fp.readlines() - return self.parse(lines) + return self.parse(lines, config) # ================================================================= # PARSING HELPERS From 3252d2077b1bf35388b92c0861b92227696686ef Mon Sep 17 00:00:00 2001 From: Noella Spitz Date: Thu, 23 May 2024 16:29:42 +0100 Subject: [PATCH 72/78] Remove data files from assertion --- gudpy/test/test_gudpy_yaml.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/gudpy/test/test_gudpy_yaml.py b/gudpy/test/test_gudpy_yaml.py index 362151ae6..8ef01fbb5 100644 --- a/gudpy/test/test_gudpy_yaml.py +++ b/gudpy/test/test_gudpy_yaml.py @@ -30,9 +30,11 @@ def testYAML(self): normalisationDataFilesA = gf1.normalisation.__dict__.pop( "dataFiles") + normalisationDataFilesA.pop("_dataFiles") normalisationDataFilesBgA = gf1.normalisation.__dict__.pop( "dataFilesBg" ) + normalisationDataFilesBgA.pop("_dataFiles") normalisationCompositionA = gf1.normalisation.__dict__.pop( "composition" ) @@ -42,9 +44,11 @@ def testYAML(self): normalisationDataFilesB = gf2.normalisation.__dict__.pop( "dataFiles") + normalisationDataFilesB.pop("_dataFiles") normalisationDataFilesBgB = gf2.normalisation.__dict__.pop( "dataFilesBg" ) + normalisationDataFilesBgB.pop("_dataFiles") normalisationCompositionB = gf2.normalisation.__dict__.pop( "composition" ) @@ -76,6 +80,7 @@ def testYAML(self): sampleBackgroundDataFilesA = gf1.sampleBackgrounds[0].__dict__.pop( "dataFiles" ) + sampleBackgroundDataFilesA.pop("_dataFiles") sampleBackgroundSamplesA = gf1.sampleBackgrounds[0].__dict__.pop( "samples" ) @@ -83,6 +88,7 @@ def testYAML(self): sampleBackgroundDataFilesB = gf2.sampleBackgrounds[0].__dict__.pop( "dataFiles" ) + sampleBackgroundDataFilesB.pop("_dataFiles") sampleBackgroundSamplesB = gf2.sampleBackgrounds[0].__dict__.pop( "samples" ) @@ -100,11 +106,13 @@ def testYAML(self): sampleBackgroundSamplesA, sampleBackgroundSamplesB ): sampleDataFilesA = sampleA.__dict__.pop("dataFiles") + sampleDataFilesA.pop("_dataFiles") sampleCompositionA = sampleA.__dict__.pop("composition") sampleElementsA = sampleCompositionA.__dict__.pop("elements") sampleContainersA = sampleA.__dict__.pop("containers") sampleDataFilesB = sampleB.__dict__.pop("dataFiles") + sampleDataFilesB.pop("_dataFiles") sampleCompositionB = sampleB.__dict__.pop("composition") sampleElementsB = sampleCompositionB.__dict__.pop("elements") sampleContainersB = sampleB.__dict__.pop("containers") @@ -125,6 +133,7 @@ def testYAML(self): sampleContainersA, sampleContainersB ): containerDataFilesA = containerA.__dict__.pop("dataFiles") + containerDataFilesA.pop("_dataFiles") containerCompositionA = containerA.__dict__.pop( "composition") containerElementsA = containerCompositionA.__dict__.pop( @@ -132,6 +141,7 @@ def testYAML(self): ) containerDataFilesB = containerB.__dict__.pop("dataFiles") + containerDataFilesB.pop("_dataFiles") containerCompositionB = containerB.__dict__.pop( "composition") containerElementsB = containerCompositionB.__dict__.pop( From 41864e00ae7f6f911bab1c40cc75efd5011b03af Mon Sep 17 00:00:00 2001 From: Noella Spitz Date: Thu, 23 May 2024 16:30:07 +0100 Subject: [PATCH 73/78] Remove print statement --- gudpy/gui/widgets/core/control.py | 1 - 1 file changed, 1 deletion(-) diff --git a/gudpy/gui/widgets/core/control.py b/gudpy/gui/widgets/core/control.py index a92ba776b..1dc289346 100644 --- a/gudpy/gui/widgets/core/control.py +++ b/gudpy/gui/widgets/core/control.py @@ -296,7 +296,6 @@ def exportInputFile(self): "Export input file as..", ".", ) - print(filename) if filename: filename.replace(".txt", "") filename += ".txt" From 6e672ed6d495c267e8c0db6173b52a1ba1272e7d Mon Sep 17 00:00:00 2001 From: Noella Spitz Date: Thu, 23 May 2024 16:43:41 +0100 Subject: [PATCH 74/78] Pop the dict --- gudpy/test/test_gudpy_yaml.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/gudpy/test/test_gudpy_yaml.py b/gudpy/test/test_gudpy_yaml.py index 8ef01fbb5..6be08f132 100644 --- a/gudpy/test/test_gudpy_yaml.py +++ b/gudpy/test/test_gudpy_yaml.py @@ -30,11 +30,11 @@ def testYAML(self): normalisationDataFilesA = gf1.normalisation.__dict__.pop( "dataFiles") - normalisationDataFilesA.pop("_dataFiles") + normalisationDataFilesA.__dict__.pop("_dataFiles") normalisationDataFilesBgA = gf1.normalisation.__dict__.pop( "dataFilesBg" ) - normalisationDataFilesBgA.pop("_dataFiles") + normalisationDataFilesBgA.__dict__.pop("_dataFiles") normalisationCompositionA = gf1.normalisation.__dict__.pop( "composition" ) @@ -44,11 +44,11 @@ def testYAML(self): normalisationDataFilesB = gf2.normalisation.__dict__.pop( "dataFiles") - normalisationDataFilesB.pop("_dataFiles") + normalisationDataFilesB.__dict__.pop("_dataFiles") normalisationDataFilesBgB = gf2.normalisation.__dict__.pop( "dataFilesBg" ) - normalisationDataFilesBgB.pop("_dataFiles") + normalisationDataFilesBgB.__dict__.pop("_dataFiles") normalisationCompositionB = gf2.normalisation.__dict__.pop( "composition" ) @@ -80,7 +80,7 @@ def testYAML(self): sampleBackgroundDataFilesA = gf1.sampleBackgrounds[0].__dict__.pop( "dataFiles" ) - sampleBackgroundDataFilesA.pop("_dataFiles") + sampleBackgroundDataFilesA.__dict__.pop("_dataFiles") sampleBackgroundSamplesA = gf1.sampleBackgrounds[0].__dict__.pop( "samples" ) @@ -88,7 +88,7 @@ def testYAML(self): sampleBackgroundDataFilesB = gf2.sampleBackgrounds[0].__dict__.pop( "dataFiles" ) - sampleBackgroundDataFilesB.pop("_dataFiles") + sampleBackgroundDataFilesB.__dict__.pop("_dataFiles") sampleBackgroundSamplesB = gf2.sampleBackgrounds[0].__dict__.pop( "samples" ) @@ -106,13 +106,13 @@ def testYAML(self): sampleBackgroundSamplesA, sampleBackgroundSamplesB ): sampleDataFilesA = sampleA.__dict__.pop("dataFiles") - sampleDataFilesA.pop("_dataFiles") + sampleDataFilesA.__dict__.pop("_dataFiles") sampleCompositionA = sampleA.__dict__.pop("composition") sampleElementsA = sampleCompositionA.__dict__.pop("elements") sampleContainersA = sampleA.__dict__.pop("containers") sampleDataFilesB = sampleB.__dict__.pop("dataFiles") - sampleDataFilesB.pop("_dataFiles") + sampleDataFilesB.__dict__.pop("_dataFiles") sampleCompositionB = sampleB.__dict__.pop("composition") sampleElementsB = sampleCompositionB.__dict__.pop("elements") sampleContainersB = sampleB.__dict__.pop("containers") @@ -133,7 +133,7 @@ def testYAML(self): sampleContainersA, sampleContainersB ): containerDataFilesA = containerA.__dict__.pop("dataFiles") - containerDataFilesA.pop("_dataFiles") + containerDataFilesA.__dict__.pop("_dataFiles") containerCompositionA = containerA.__dict__.pop( "composition") containerElementsA = containerCompositionA.__dict__.pop( @@ -141,7 +141,7 @@ def testYAML(self): ) containerDataFilesB = containerB.__dict__.pop("dataFiles") - containerDataFilesB.pop("_dataFiles") + containerDataFilesB.__dict__.pop("_dataFiles") containerCompositionB = containerB.__dict__.pop( "composition") containerElementsB = containerCompositionB.__dict__.pop( From e5501a9f21254133103770f657eba354bbf81625 Mon Sep 17 00:00:00 2001 From: Noella Spitz Date: Tue, 28 May 2024 10:28:54 +0100 Subject: [PATCH 75/78] Fix purge file path --- gudpy/test/test_purge_file.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/gudpy/test/test_purge_file.py b/gudpy/test/test_purge_file.py index 0ca8b94fc..277b8840e 100644 --- a/gudpy/test/test_purge_file.py +++ b/gudpy/test/test_purge_file.py @@ -17,7 +17,8 @@ def setUp(self) -> None: dirpath = Path().resolve() / "test/" / Path(path) else: dirpath = ( - "/".join(os.path.realpath(__file__).split("/")[:-1]) + os.path.sep.join(os.path.realpath( + __file__).split(os.path.sep)[:-1]) ) self.gudpy = gudpy.GudPy() self.keepsakes = os.listdir() From acbf1adc444911e76ed68e17c899f4c0697206fe Mon Sep 17 00:00:00 2001 From: Noella Spitz Date: Tue, 28 May 2024 10:53:44 +0100 Subject: [PATCH 76/78] Change directory sourcing --- gudpy/core/gudpy.py | 2 +- gudpy/test/test_purge_file.py | 5 +---- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/gudpy/core/gudpy.py b/gudpy/core/gudpy.py index 775992d42..7f4639464 100644 --- a/gudpy/core/gudpy.py +++ b/gudpy/core/gudpy.py @@ -69,7 +69,7 @@ def loadFromGudrunFile( Raised if input file does not exist """ if not os.path.exists(loadFile): - raise FileNotFoundError("Input file does not exist.") + raise FileNotFoundError(f"Input file '{loadFile}' does not exist.") self.gudrunFile = self.io.importGudrunFile(loadFile, config) diff --git a/gudpy/test/test_purge_file.py b/gudpy/test/test_purge_file.py index 277b8840e..70d2f11c7 100644 --- a/gudpy/test/test_purge_file.py +++ b/gudpy/test/test_purge_file.py @@ -16,10 +16,7 @@ def setUp(self) -> None: from pathlib import Path dirpath = Path().resolve() / "test/" / Path(path) else: - dirpath = ( - os.path.sep.join(os.path.realpath( - __file__).split(os.path.sep)[:-1]) - ) + dirpath = (os.path.dirname(__file__)) self.gudpy = gudpy.GudPy() self.keepsakes = os.listdir() refFile = os.path.join(dirpath, path) From e44a75b824ec7531e53abbecf28c42cd3c458ea2 Mon Sep 17 00:00:00 2001 From: Noella Spitz Date: Tue, 28 May 2024 11:16:37 +0100 Subject: [PATCH 77/78] Fix path separator for windows --- gudpy/test/test_purge_file.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gudpy/test/test_purge_file.py b/gudpy/test/test_purge_file.py index 70d2f11c7..eef63fe08 100644 --- a/gudpy/test/test_purge_file.py +++ b/gudpy/test/test_purge_file.py @@ -10,7 +10,7 @@ class TestPurgeFile(TestCase): def setUp(self) -> None: - path = "TestData/NIMROD-water/water.txt" + path = f"TestData{os.path.sep}NIMROD-water{os.path.sep}water.txt" if os.name == "nt": from pathlib import Path From 9e7841421a212fa28fdfebf72078d5e6a8ba3703 Mon Sep 17 00:00:00 2001 From: Noella Spitz Date: Tue, 28 May 2024 14:32:18 +0100 Subject: [PATCH 78/78] Fix windows --- gudpy/test/test_purge_file.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/gudpy/test/test_purge_file.py b/gudpy/test/test_purge_file.py index eef63fe08..bd69ae7ac 100644 --- a/gudpy/test/test_purge_file.py +++ b/gudpy/test/test_purge_file.py @@ -11,12 +11,7 @@ class TestPurgeFile(TestCase): def setUp(self) -> None: path = f"TestData{os.path.sep}NIMROD-water{os.path.sep}water.txt" - - if os.name == "nt": - from pathlib import Path - dirpath = Path().resolve() / "test/" / Path(path) - else: - dirpath = (os.path.dirname(__file__)) + dirpath = (os.path.dirname(__file__)) self.gudpy = gudpy.GudPy() self.keepsakes = os.listdir() refFile = os.path.join(dirpath, path)