diff --git a/gudpy/core/beam.py b/gudpy/core/beam.py index 14048211e..b72ef043d 100644 --- a/gudpy/core/beam.py +++ b/gudpy/core/beam.py @@ -47,16 +47,12 @@ class Beam: Sample dependant background factor. shieldingAttenuationCoefficient : float Absorption coefficient for the shielding. - Methods - ------- + yamlignore : str{} + Class attributes to ignore during yaml serialisation. """ def __init__(self): """ Constructs all the necessary attributes for the Beam object. - - Parameters - ---------- - None """ self.sampleGeometry = Geometry.FLATPLATE self.beamProfileValues = [1., 1.] @@ -85,14 +81,9 @@ def __str__(self): """ Returns the string representation of the Beam object. - Parameters - ---------- - None - Returns ------- - string : str - String representation of Beam. + str : String representation of Beam. """ absorptionAndMSLine = ( diff --git a/gudpy/core/composition.py b/gudpy/core/composition.py index 3eb669814..2e64031be 100644 --- a/gudpy/core/composition.py +++ b/gudpy/core/composition.py @@ -8,24 +8,85 @@ class ChemicalFormulaParser(): + """ + Chemical formula parser. Uses regular expressions, and Sears91 data + to parse chemical formulae. + + ... + + Attributes + ---------- + stream : char[] + Stream of chars to parse. + regex : re.Pattern + Regular expression pattern for chemical formulae. + sears91 : Sears91 + Sears91 isotope data. + + Methods + ---------- + consumeTokens(n) + Consumes n tokens from the stream. + parse(stream) + Parses a chemical formula from the stream. + parseElement() + Parses an Element. + parseSymbol() + Parses an atomic symbol. + parseMassNo() + Parses a mass number. + parseAbundance() + Parses abundance. + """ def __init__(self): - self.stream = None + """ + Constructs all the necessary attributes for the ChemicalFormulaParser object. + """ + self.stream = [] self.regex = re.compile(r"[A-Z][a-z]?(\[\d+\])?\d*") self.sears91 = Sears91() def consumeTokens(self, n): + """ + Consumes n tokens from the input stream. + + Parameters + ---------- + n : int + Number of tokens to consume. + """ for _ in range(n): if self.stream: self.stream.pop(0) def parse(self, stream): + """ + Core method of the ChemicalFormulaParser. + Parses a chemical formula from a given stream of text. + + Parameters + ---------- + stream : str + Input stream. + + Returns + ------- + Element[] | False : List of parsed Element objects, or False if failure. + """ + # Check string is a chemical formula. if not self.regex.match(stream): return None + + # Split string into list of chars. self.stream = list(stream) elements = [] + while self.stream: + # Parse an element. element = self.parseElement() + # If element is valid, append it. + # Otherwise, parsing has failed, so return Fale. if element: elements.append(element) else: @@ -33,19 +94,38 @@ def parse(self, stream): return elements def parseElement(self): + """ + Parses the next element from the stream. + + Returns + ------- + Element | None : Parsed Element, if any. + """ + + # Parse symbol. symbol = self.parseSymbol() + + # Parse mass number. massNo = self.parseMassNo() + + # Parse abundance. abundance = self.parseAbundance() + + # Infer D as H[2]. if symbol == "D": symbol = "H" massNo = 2.0 + + # Check isotope is valid. if symbol in massData.keys(): if ( not self.sears91.isotopes(symbol) or self.sears91.isIsotope(symbol, massNo) ): + # Construct and return Element object. return Element(symbol, massNo, abundance) else: + # Isotope is invalid, raise exception. validIsotopes = "\n - ".join( [ f"{self.sears91.isotope(isotope)}" @@ -59,32 +139,103 @@ def parseElement(self): ) def parseSymbol(self): + """ + Parses an atomic symbol. + + Returns + ------- + str | None : atomic symbol parsed, if any. + """ if self.stream: + # Use regular expression to extract atomic symbol. match = re.match(r"[A-Z][a-z]|[A-Z]", "".join(self.stream)) if match: + # Consume len(atomicSymbol) tokens from the stream. self.consumeTokens(len(match.group(0))) + + # Return atomicSymbol. return match.group(0) def parseMassNo(self): + """ + Parses a mass number. + + Returns + ------- + int : parsed mass number. + """ if self.stream: + # Use regular expression to extract mass number. match = re.match(r"\[\d+\]", "".join(self.stream)) if match: + # Consume len(str(massNo)) tokens from the stream. self.consumeTokens(len(match.group(0))) + # Cast to int and return mass number. return int("".join(match.group(0)[1:-1])) + + # If no mass number is supplied, then it is the natural istope. + # So return 0. return 0 def parseAbundance(self): + """ + Parses an abundance. + + Returns + ------- + float : parsed abundance. + """ if self.stream: + # Use regular expression to extract abundance. match = re.match(r"\d+\.\d+|\d+", "".join(self.stream)) if match: + # Consume len(str(abundance)) tokens from the stream. self.consumeTokens(len(match.group(0))) + # Cast to float and return abundance. return float(match.group(0)) + + # If no abundance is supplied, then return 1.0. return 1.0 class Component(): + """ + Class to represent a component. This essentially maintains a composition, and can be named. + + ... + + Attributes + ---------- + name : str + Component name. + elements : Element[] + Composition. + parser : ChemicalFormulaParser + Parser to use for parsing chemical formulae. + yamlignore : str{} + Class attributes to ignore during yaml serialisation. + + Methods + ------- + addElement(element) + Adds an element to the internal composition. + parse(persistent=True) + Parses chemical formula from `name`. + eq(obj) + Checks for equality between components. + """ def __init__(self, name="", elements=[]): + """ + Constructs all the necessary attributes for the Component object. + + Parameters + ---------- + name : str, optional + Name to assign to component. + elements : Element[] + List of Element objects to assign to internal composition. + """ self.name = name self.elements = elements self.parser = ChemicalFormulaParser() @@ -95,22 +246,66 @@ def __init__(self, name="", elements=[]): } def addElement(self, element): + """ + Adds an element to the internal composition. + + Parameters + ---------- + element : Element + Target Element object to append. + """ self.elements.append(element) def parse(self, persistent=True): + """ + Parses chemical fromula from `name`. + Optionally assign the parsed chemical formula to the internal composition. + + Parameters + ---------- + persistent : bool, optional + Should the parsed chemical formula persist in the object. + + Returns + ------- + None | Element[] : If not persistent, list of parsed Elements. + """ + # Parse elements from name. elements = self.parser.parse(self.name) + + # If persistent, assign elements. if elements and persistent: self.elements = elements + # Otherwise return them. elif elements and not persistent: return elements def __str__(self): + """ + Returns the string representation of the Component object. + + Returns + ------- + str : String representation of Component. + """ if not self.elements: return f"{self.name}\n{{\n}}" elements = "\n".join([str(x) for x in self.elements]) return f"{self.name}\n(\n{elements}\n)" def eq(self, obj): + """ + Checks for equality between `obj` and the current object. + + Parameters + ---------- + obj : Component + Object to compare against. + + Returns + ------- + bool : self == obj + """ return all( [ e.eq(el) for e, el in zip(self.elements, obj.elements) @@ -119,8 +314,35 @@ def eq(self, obj): class Components(): + """ + Class to represent a set of components. + + ... + + Attributes + ---------- + components : Component[] + List of Components. + yamlignore : str{} + Class attributes to ignore during yaml serialisation. + + Methods + ------- + addComponent(element) + Adds an Component to the components. + count() + Returns number of components. + """ def __init__(self, components=[]): + """ + Constructs all the necessary attributes for the Components object. + + Parameters + ---------- + components : Component[], optional + List of Component objects to assign to internal components. + """ self.components = components self.yamlignore = { @@ -128,20 +350,74 @@ def __init__(self, components=[]): } def addComponent(self, component): + """ + Adds a component to the internal components. + + Parameters + ---------- + component : Component + Target Component object to append. + """ self.components.append(component) + def count(self): + """ + Counts number of components. + + Returns + ------- + int : number of components. + """ + return len(self.components) + def __str__(self): + """ + Returns the string representation of the Components object. + + Returns + ------- + str : String representation of Components. + """ return "\n".join( [str(x) for x in self.components] ) - def count(self): - return len(self.components) - class WeightedComponent(): - + """ + Class to represent a weighted component. + This essentially maintains a composition, and can be named. + However, this also has a weighting. + + ... + + Attributes + ---------- + component : Component + Component to be weighted. + ratio : float + Weighting of component. + yamlignore : str{} + Class attributes to ignore during yaml serialisation. + + Methods + ------- + translate() + Applies ratio to component. + eq(obj) + Checks for equality between weighted components. + """ def __init__(self, component, ratio): + """ + Constructs all the necessary attributes for the WeightedComponent object. + + Parameters + ---------- + component : Component + Component to be weighted. + ratio : float + Weighting of component. + """ self.component = component self.ratio = ratio self.yamlignore = { @@ -149,6 +425,13 @@ def __init__(self, component, ratio): } def translate(self): + """ + Applies the ratio to the component. + + Returns + ------- + Element[] : list of elements, with ratio applied. + """ elements = [] for element in self.component.elements: abundance = self.ratio * element.abundance @@ -159,14 +442,65 @@ def translate(self): ) return elements + def eq(self, obj): + """ + Checks for equality between `obj` and the current object. + + Returns + ------- + bool : self == obj + """ if hasattr(obj, "component") and hasattr(obj, "ratio"): return self.component == obj.component and self.ratio == obj.ratio class Composition(): - + """ + Class to represent a Composition. + This can be a collection of elements, or weighted components. + + ... + + Attributes + ---------- + type_ : str + Composition type. + elements : Element[] + List of elements that constitute composition. + weightedComponents : WeightedComponent[] + List of weighted components that constitute composition. + yamlignore : str{} + Class attributes to ignore during yaml serialisation. + + Methods + ------- + addComponent(element) + Adds an Component to the components. + addElement(element) + Adds an element to the internal composition. + addElements(elements) + Adds elements to the internal composition. + shallowTranslate() + Performs a shallow translate of weighted components to elements. + translate() + Performs a deep translate of weighted components to elements. + sumAndMutate(elements, target) + Sums elements into target. + calculateExpectedDCSLevel(elements) + Calculates expected DCS level given a composition. + """ def __init__(self, type_, elements=None): + """ + Constructs all the necessary attributes for the Composition object. + + Parameters + ---------- + type_ : str + Composition type. + elements : Element[] + List of Element objects to assign to internal composition. + """ self.type_ = type_ if not elements: self.elements = [] @@ -179,17 +513,50 @@ def __init__(self, type_, elements=None): } def addComponent(self, component, ratio): + """ + Adds a weighted component to the internal components. + + Parameters + ---------- + component : Component + Target Component object to append. + ratio : float + Ratio to use for component. + """ self.weightedComponents.append( WeightedComponent(component, ratio) ) def addElement(self, element): + """ + Adds an element to the internal composition. + + Parameters + ---------- + element : Element + Target Element object to append. + """ self.elements.append(element) def addElements(self, elements): + """ + Adds an element to the internal composition. + + Parameters + ---------- + elements : Element[] + Target Element objects to append. + """ self.elements.extend(elements) def shallowTranslate(self): + """ + Performs a shallow translate of weighted components to elements. + + Returns + ------- + Element[] : list of translated elements. + """ elements = [] for component in self.weightedComponents: elements.extend(component.translate()) @@ -213,6 +580,13 @@ def sumAndMutate(elements, target): Sums the abundances of elements within the composition. This ensures that the same element isn't written out multiple times. + + Parameters + ---------- + elements : Element[] + List of Elements. + target : Element[] + Target list of Elements to sum into. """ for element in elements: exists = False @@ -226,21 +600,29 @@ def sumAndMutate(elements, target): if not exists: target.append(element) - def __str__(self): - string = "" - for el in self.elements: - string += ( - str(el) + " " - "Composition\n" - ) - - return string.rstrip() - @staticmethod def calculateExpectedDCSLevel(elements): + """ + Calculates expected DCS level given a composition. + + Parameters + ---------- + elements : Element[] + List of Elements that contsitute composition. + + + Returns + ------- + float : expected DCS level. + """ + + # Calculate total abundance. totalAbundance = sum([el.abundance for el in elements]) s91 = Sears91() + + # If there are elements. if len(elements) and totalAbundance > 0.0: + # Return average bound scattering cross section / 4.0 / pi return round(sum( [ s91.totalXS( @@ -250,4 +632,23 @@ def calculateExpectedDCSLevel(elements): ) * (el.abundance/totalAbundance) for el in elements ] ) / 4.0 / math.pi, 5) + + # Otherwise return 0.0. return 0.0 + + def __str__(self): + """ + Returns the string representation of the Composition object. + + Returns + ------- + str : String representation of Composition. + """ + string = "" + for el in self.elements: + string += ( + str(el) + " " + "Composition\n" + ) + + return string.rstrip() \ No newline at end of file diff --git a/gudpy/core/composition_iterator.py b/gudpy/core/composition_iterator.py index 00954713f..28ad9361d 100644 --- a/gudpy/core/composition_iterator.py +++ b/gudpy/core/composition_iterator.py @@ -2,7 +2,6 @@ import math import os import time - from core.gud_file import GudFile @@ -10,38 +9,84 @@ def gss( f, bounds, n, maxN, rtol, args=(), startIterFunc=None, endIterFunc=None ): + """ + Golden-section search. Used to find extremum of a given cost function + `f` (with arguments `args`), with interval `bounds`. Converges when `n >= maxN` or + current result within `rtol`. + + Parameters + ---------- + f : function + Function to evaluate against. + bounds : float[] + Lower bound, start point, upper bound. + n : int + Current iteration. + maxN : int + Maximum number of iterations. + rtol : float + Relative tolerance for convergence. + args : tuple(any), optional + Arguments to pass to evaluation function. + startIterFunc : function, optional + Function to call at the start of an iteration. + endIterFunc : function, optional + Function to call at the end of an iteration. + + Returns + ------- + None | float : Final result + """ + # If available, call startIterFunc. if startIterFunc: startIterFunc(n) + + # If we have reached maximum number of iterations, return the current centre. if n >= maxN: return bounds[1] + # Check to see if we are within the convergence tolerance. if ( (abs(bounds[2] - bounds[0]) / min([abs(bounds[0]), abs(bounds[2])])) < (rtol/100)**2 ): + # If available, call endIterFunc. if endIterFunc: endIterFunc(n) + # Return average of centre and upper bound. return (bounds[2] + bounds[1]) / 2 # Calculate a potential centre = c + 2 - GR * (upper-c) d = bounds[1] + (2 - (1 + math.sqrt(5))/2)*(bounds[2]-bounds[1]) - # If the new centre evaluates to less than the current + # Call evaluation function, using arguments and potential centre. fd1 = f(d, *args) + + # If no result, return None. if fd1 is None: + # If available, call endIterFunc. if endIterFunc: endIterFunc(n) return None + + # Call evaluation function, using arguments and current centre. fd2 = f(bounds[1], *args) if fd2 is None: + # If available, call endIterFunc. if endIterFunc: endIterFunc(n) return None + + # If the new centre evaluates to less than the current if fd1 < fd2: # Swap them, making the previous centre the new lower bound. bounds = [bounds[1], d, bounds[2]] + + # If available, call endIterFunc. if endIterFunc: endIterFunc(n) + + # Recurse using new bounds. return gss( f, bounds, n+1, maxN, rtol, args=args, startIterFunc=startIterFunc, endIterFunc=endIterFunc @@ -49,8 +94,12 @@ def gss( # Otherwise, swap and reverse. else: bounds = [d, bounds[1], bounds[0]] + + # If available, call endIterFunc. if endIterFunc: endIterFunc() + + # Recurse using new bounds. return gss( f, bounds, n+1, maxN, rtol, args=args, startIterFunc=startIterFunc, endIterFunc=endIterFunc @@ -58,10 +107,26 @@ def gss( def calculateTotalMolecules(components, sample): + """ + Calculates the total number of molecules in `sample` that belong + to `components`. + + Parameters + ---------- + components : Components + Components object to check sample component membership against. + sample : Sample + Target sample object. + + Returns + ------- + float : Total sum of molecules + """ # Sum molecules in sample composition, belongiong to components. total = 0 for wc in sample.composition.weightedComponents: for c in components: + # If c == wc.component, add to the sum. if wc.component.eq(c): total += wc.ratio break @@ -88,6 +153,7 @@ class CompositionIterator(): Components to perform iteration on. ratio : float Starting ratio. + Methods ---------- setComponent(component, ratio=1) @@ -104,52 +170,69 @@ class CompositionIterator(): Performs n iterations using cost function f, args and bounds. """ def __init__(self, gudrunFile): + """ + Constructs all the necessary attributes for the CompositionIterator object. + + Parameters + ---------- + gudrunFile : GudrunFile + Parent GudrunFile object. + """ self.gudrunFile = gudrunFile self.components = [] self.ratio = 0 - """ - Sets component and ratio. - Parameters - ---------- - component : Component - Component to set. - ratio : int, optional - Ratio of component. - """ def setComponent(self, component, ratio=1): + """ + Sets component and ratio. + + Parameters + ---------- + component : Component + Component to set. + ratio : int, optional + Ratio of component. + """ self.components = [component] self.ratio = ratio - """ - Sets components and ratio. - Parameters - ---------- - components : Component[] - Components to set. - ratio : int, optional - Ratio of component. - """ def setComponents(self, components, ratio=1): + """ + Sets components and ratio. + + Parameters + ---------- + components : Component[] + Components to set. + ratio : int, optional + Ratio of component. + """ self.components = [c for c in components if c] self.ratio = ratio - """ - Cost function for processing a single component. - - Parameters - ---------- - x : float - Chosen ratio. - sampleBackground : SampleBackground - Target Sample Background. - """ def processSingleComponent(self, x, sampleBackground): + """ + Cost function for processing a single component. + + Parameters + ---------- + x : float + Chosen ratio. + sampleBackground : SampleBackground + Target Sample Background. + + Returns + ------- + float : Determined cost + """ self.gudrunFile.sampleBackgrounds = [sampleBackground] + # Ensure x is not negative. x = abs(x) + + # Filter components to find targets. weightedComponents = [ wc for wc in ( sampleBackground.samples[0].composition.weightedComponents @@ -157,13 +240,21 @@ def processSingleComponent(self, x, sampleBackground): for c in self.components if c.eq(wc.component) ] + + # Apply ratio to components. for component in weightedComponents: component.ratio = x + # Translate into atomic composition. sampleBackground.samples[0].composition.translate() + + # Process. self.gudrunFile.process() + # Sleep to prevent race conditions. time.sleep(1) + + # Read the .gud file into a GudFile object. gudPath = sampleBackground.samples[0].dataFiles[0].replace( self.gudrunFile.instrument.dataFileType, "gud" @@ -174,26 +265,36 @@ def processSingleComponent(self, x, sampleBackground): ) ) + # Determine cost. if gudFile.averageLevelMergedDCS == gudFile.expectedDCS: return 0 else: return (gudFile.expectedDCS-gudFile.averageLevelMergedDCS)**2 - """ - Cost function for processing two components. - Parameters - ---------- - x : float - Chosen ratio. - sampleBackground : SampleBackground - Target Sample Background. - totalMolecules : float - Sum of molecules of both components. - """ def processTwoComponents(self, x, sampleBackground, totalMolecules): + """ + Cost function for processing two components. + + Parameters + ---------- + x : float + Chosen ratio. + sampleBackground : SampleBackground + Target Sample Background. + totalMolecules : float + Sum of molecules of both components. + + Returns + ------- + float : Determined cost + """ self.gudrunFile.sampleBackgrounds = [sampleBackground] + + # Ensure x is not negative. x = abs(x) + + # Filter components to find targets. wcA = wcB = None for weightedComponent in ( sampleBackground.samples[0].composition.weightedComponents @@ -203,14 +304,21 @@ def processTwoComponents(self, x, sampleBackground, totalMolecules): elif weightedComponent.component.eq(self.components[1]): wcB = weightedComponent + # Apply ratio to components, maintaining totalMolecules. if wcA and wcB: wcA.ratio = x wcB.ratio = abs(totalMolecules - x) + # Translate into atomic composition. sampleBackground.samples[0].composition.translate() + + # Process. self.gudrunFile.process() + # Sleep to prevent race conditions. time.sleep(1) + + # Read the .gud file into a GudFile object. gudPath = sampleBackground.samples[0].dataFiles[0].replace( self.gudrunFile.instrument.dataFileType, "gud" @@ -221,6 +329,7 @@ def processTwoComponents(self, x, sampleBackground, totalMolecules): ) ) + # Determine cost. if gudFile.averageLevelMergedDCS == gudFile.expectedDCS: return 0 else: @@ -233,20 +342,22 @@ def processTwoComponents(self, x, sampleBackground, totalMolecules): ] ) - """ - This method is the core of the CompositionIterato. - It performs n iterations of tweaking by the ratio of component(s). - - Parameters - ---------- - n : int - Number of iterations to perform. - rtol : float - Relative tolerance - """ def iterate(self, n=10, rtol=10.): + """ + This method is the core of the CompositionIterator. + It performs n iterations of tweaking by the ratio of component(s). + + Parameters + ---------- + n : int + Number of iterations to perform. + rtol : float + Relative tolerance + """ + # Check components and ratio has been set. if not self.components or not self.ratio: return None + # Only include samples that are marked for analysis. for sampleBackground in self.gudrunFile.sampleBackgrounds: for sample in sampleBackground.samples: @@ -256,15 +367,20 @@ def iterate(self, n=10, rtol=10.): if len(self.components) == 1: self.maxIterations = n self.rtol = rtol + # Perform golden-section search. + # Interval is [1e-2, ratio, 10] self.gss( self.processSingleComponent, [1e-2, self.ratio, 10], 0, args=(sb,) ) elif len(self.components) == 2: + # Calculate total molecules. totalMolecules = self.calculateTotalMolecules(sample) + # Perform golden-section search. + # Interval is [1e-2, ratio, 10] self.gss( self.processTwoComponents, [1e-2, self.ratio, 10], 0, @@ -272,4 +388,22 @@ def iterate(self, n=10, rtol=10.): ) def gss(self, f, bounds, n, args=()): + """ + Wrapper for calling gss using class attributes. + + Parameters + ---------- + f : function + Function to evaluate against. + bounds : float[] + Lower bound, start point, upper bound. + n : int + Current iteration. + args : tuple(any), optional + Arguments to pass to evaluation function. + + Returns + ------- + None | float : Final result + """ return gss(f, bounds, n, self.maxIterations, self.rtol, args=args) diff --git a/gudpy/core/config.py b/gudpy/core/config.py index 895da49f6..7e5e6c095 100644 --- a/gudpy/core/config.py +++ b/gudpy/core/config.py @@ -4,22 +4,34 @@ from core.enums import Geometry from core.gui_config import GUIConfig +# Spacing definitions, for writing input file to gudrun_dcs. spc2 = " " spc5 = " " +# Global geometry. geometry = Geometry.FLATPLATE + +# Constant - number of 'GudPy' core objects. +# Currently, this consists of: Instrument, Components, Beam, Normalisation. NUM_GUDPY_CORE_OBJECTS = 4 + +# Should components be used in sample compositions? USE_USER_DEFINED_COMPONENTS = False + +# Should compositions be normalised to 1? NORMALISE_COMPOSITIONS = False +# Root directory that script is running from. __rootdir__ = os.path.dirname(os.path.abspath(sys.argv[0])) +# Root for container configurations. __root__ = ( os.path.join(sys._MEIPASS, "bin", "configs", "containers") if hasattr(sys, "_MEIPASS") else os.path.join(__rootdir__, "bin", "configs", "containers") ) +# Container configurations. containerConfigurations = { os.path.basename(path) .replace(".config", "") diff --git a/gudpy/core/container.py b/gudpy/core/container.py index 126f07ba9..b877838dc 100644 --- a/gudpy/core/container.py +++ b/gudpy/core/container.py @@ -54,11 +54,38 @@ class Container: TABLES / TRANSMISSION monitor / filename crossSectionFilename : str Filename for total cross section source if applicable. - scatteringFractionAttenuationCoefficient : tuple(float, float) - Sample environment scattering fraction and attenuation coefficient, - per Angstrom + tweakFactor : float + Container tweak factor. + scatteringFraction : float + Sample environment scattering fraction. + attenuationCoefficient : float + Attenuation coefficient per angstrom. + runAsSample : bool + Should the container be run as a sample? + topHatW : float + Width of top hat function for Fourier Transform. + FTMode : FTModes + Mode for Fourier Transform. + minRadFT : float + Minimum radius for Fourier Transform. + maxRadFT : float + Maximum radius for Fourier Transform. + grBroadening : float + Broadening of g(r) at r = 1 Angstrom + powerForBroadening : float + Broadening power + 0 = constant, 0.5 = sqrt(r), 1 = r + stepSize : float + Step size in radius for final g(r). + yamlignore : str{} + Class attributes to ignore during yaml serialisation. + Methods ------- + convertToSample + Converts the container to a sample. + parseFromConfig(path) + Parses the container from a configuration file. """ def __init__(self, config_=None): """ @@ -66,7 +93,8 @@ def __init__(self, config_=None): Parameters ---------- - None + config_ : path + Path to parse configuration from. """ self.name = "" self.periodNumber = 1 @@ -112,103 +140,16 @@ def __init__(self, config_=None): if config_: self.parseFromConfig(config_) - def __str__(self): + def convertToSample(self): """ - Returns the string representation of the Container object. - - Parameters - ---------- - None + Converts the container to a sample object. Returns ------- - string : str - String representation of Container. + Sample : converted sample. """ - nameLine = ( - f"CONTAINER {self.name}{config.spc5}" - if self.name != "CONTAINER" - else - f"CONTAINER{config.spc5}" - ) - - dataFilesLines = ( - f'{str(self.dataFiles)}\n' - if len(self.dataFiles) > 0 - else - '' - ) - - if self.densityUnits == UnitsOfDensity.ATOMIC: - units = 'atoms/\u212b^3' - density = -self.density - elif self.densityUnits == UnitsOfDensity.CHEMICAL: - units = 'gm/cm^3' - density = self.density - - compositionSuffix = "" if str(self.composition) == "" else "\n" - - geometryLines = ( - f'{self.upstreamThickness}{config.spc2}' - f'{self.downstreamThickness}{config.spc5}' - f'Upstream and downstream thicknesses [cm]\n' - f'{self.angleOfRotation}{config.spc2}' - f'{self.sampleWidth}{config.spc5}' - f'Angle of rotation and sample width (cm)\n' - if ( - self.geometry == Geometry.SameAsBeam - and config.geometry == Geometry.FLATPLATE - ) - or self.geometry == Geometry.FLATPLATE - else - f'{self.innerRadius}{config.spc2}{self.outerRadius}{config.spc5}' - f'Inner and outer radii [cm]\n' - f'{self.sampleHeight}{config.spc5}' - f'Sample height (cm)\n' - ) - - densityLine = ( - f'{density}{config.spc5}' - f'Density {units}?\n' - ) - - crossSectionSource = ( - CrossSectionSource(self.totalCrossSectionSource.value).name - ) - crossSectionLine = ( - f"{crossSectionSource}{config.spc5}" - if self.totalCrossSectionSource != CrossSectionSource.FILE - else - f"{self.crossSectionFilename}{config.spc5}" - ) - - return ( - f'{nameLine}{{\n\n' - f'{len(self.dataFiles)}{config.spc2}' - f'{self.periodNumber}{config.spc5}' - f'Number of files and period number\n' - f'{dataFilesLines}' - f'{str(self.composition)}{compositionSuffix}' - f'*{config.spc2}0{config.spc2}0{config.spc5}' - f'* 0 0 to specify end of composition input\n' - f'SameAsBeam{config.spc5}' - f'Geometry\n' - f'{geometryLines}' - f'{densityLine}' - f'{crossSectionLine}' - f'Total cross section source\n' - f'{self.tweakFactor}{config.spc5}' - f'Tweak factor\n' - f'{self.scatteringFraction}{config.spc2}' - f'{self.attenuationCoefficient}{config.spc5}' - f'Sample environment scattering fraction ' - f'and attenuation coefficient [per \u212b]\n' - f'\n}}\n' - ) - - def convertToSample(self): - + # Basic sample parameters sample = Sample() sample.name = self.name sample.periodNumber = self.periodNumber @@ -227,6 +168,10 @@ def convertToSample(self): sample.densityUnits = self.densityUnits sample.totalCrossSectionSource = self.totalCrossSectionSource sample.sampleTweakFactor = self.tweakFactor + sample.attenuationCoefficient = self.attenuationCoefficient + sample.scatteringFraction = 1.0 + + # Fourier Transform parameters. sample.topHatW = self.topHatW sample.FTMode = self.FTMode sample.grBroadening = self.grBroadening @@ -237,11 +182,18 @@ def convertToSample(self): sample.minRadFT = self.minRadFT sample.powerForBroadening = self.powerForBroadening sample.stepSize = self.stepSize - sample.scatteringFraction = 1.0 return sample def parseFromConfig(self, path): + """ + Parses the container from a path to a configuration file. + + Parameters + ---------- + path : str + Path to parse from. + """ if not os.path.exists(path): raise ParserException( "The path supplied is invalid.\ @@ -369,3 +321,93 @@ def parseFromConfig(self, path): " The input file is most likely of an incorrect format, " "and some attributes were missing." ) from e + + def __str__(self): + """ + Returns the string representation of the Container object. + + Returns + ------- + str : String representation of Container. + """ + + nameLine = ( + f"CONTAINER {self.name}{config.spc5}" + if self.name != "CONTAINER" + else + f"CONTAINER{config.spc5}" + ) + + dataFilesLines = ( + f'{str(self.dataFiles)}\n' + if len(self.dataFiles) > 0 + else + '' + ) + + if self.densityUnits == UnitsOfDensity.ATOMIC: + units = 'atoms/\u212b^3' + density = -self.density + elif self.densityUnits == UnitsOfDensity.CHEMICAL: + units = 'gm/cm^3' + density = self.density + + compositionSuffix = "" if str(self.composition) == "" else "\n" + + geometryLines = ( + f'{self.upstreamThickness}{config.spc2}' + f'{self.downstreamThickness}{config.spc5}' + f'Upstream and downstream thicknesses [cm]\n' + f'{self.angleOfRotation}{config.spc2}' + f'{self.sampleWidth}{config.spc5}' + f'Angle of rotation and sample width (cm)\n' + if ( + self.geometry == Geometry.SameAsBeam + and config.geometry == Geometry.FLATPLATE + ) + or self.geometry == Geometry.FLATPLATE + else + f'{self.innerRadius}{config.spc2}{self.outerRadius}{config.spc5}' + f'Inner and outer radii [cm]\n' + f'{self.sampleHeight}{config.spc5}' + f'Sample height (cm)\n' + ) + + densityLine = ( + f'{density}{config.spc5}' + f'Density {units}?\n' + ) + + crossSectionSource = ( + CrossSectionSource(self.totalCrossSectionSource.value).name + ) + crossSectionLine = ( + f"{crossSectionSource}{config.spc5}" + if self.totalCrossSectionSource != CrossSectionSource.FILE + else + f"{self.crossSectionFilename}{config.spc5}" + ) + + return ( + f'{nameLine}{{\n\n' + f'{len(self.dataFiles)}{config.spc2}' + f'{self.periodNumber}{config.spc5}' + f'Number of files and period number\n' + f'{dataFilesLines}' + f'{str(self.composition)}{compositionSuffix}' + f'*{config.spc2}0{config.spc2}0{config.spc5}' + f'* 0 0 to specify end of composition input\n' + f'SameAsBeam{config.spc5}' + f'Geometry\n' + f'{geometryLines}' + f'{densityLine}' + f'{crossSectionLine}' + f'Total cross section source\n' + f'{self.tweakFactor}{config.spc5}' + f'Tweak factor\n' + f'{self.scatteringFraction}{config.spc2}' + f'{self.attenuationCoefficient}{config.spc5}' + f'Sample environment scattering fraction ' + f'and attenuation coefficient [per \u212b]\n' + f'\n}}\n' + ) diff --git a/gudpy/core/data_files.py b/gudpy/core/data_files.py index dda71a534..cb1cad58d 100644 --- a/gudpy/core/data_files.py +++ b/gudpy/core/data_files.py @@ -13,8 +13,8 @@ class DataFiles: List of filenames belonging to the object. name : str Name of the parent of the data files, e.g. Sample Background - Methods - ------- + yamlignore : str{} + Class attributes to ignore during yaml serialisation. """ def __init__(self, dataFiles, name): """ @@ -39,14 +39,9 @@ def __str__(self): """ Returns the string representation of the DataFiles object. - Parameters - ---------- - None - Returns ------- - string : str - String representation of DataFiles. + str : String representation of DataFiles. """ self.str = [ df + config.spc5 + self.name + " data files" @@ -58,24 +53,47 @@ def __len__(self): """ Returns the length of the dataFiles list member. - Parameters - ---------- - None - Returns ------- - int - Number of data files, + int : Number of data files, """ return len(self.dataFiles) def __getitem__(self, n): + """ + Gets the dataFile at index `n`. + + Parameters + ---------- + n : int + Index to retrieve from. + Returns + ------- + str : selected data file. + """ return self.dataFiles[n] def __setitem__(self, n, item): + """ + Sets the dataFile at index `n` to `item`. + + Parameters + ---------- + n : int + Index to set at. + item : str + Item to set value at index to. + """ if n >= len(self): self.dataFiles.extend(n+1) self.dataFiles[n] = item def __iter__(self): + """ + Wrapper for iterating the internal list of data files. + + Returns + ------- + Iterator : iterator on `dataFiles`. + """ return iter(self.dataFiles) diff --git a/gudpy/core/density_iterator.py b/gudpy/core/density_iterator.py index 3e9f2a386..431c513f7 100644 --- a/gudpy/core/density_iterator.py +++ b/gudpy/core/density_iterator.py @@ -34,4 +34,12 @@ def applyCoefficientToAttribute(self, object, coefficient): object.density *= coefficient def organiseOutput(self, n): + """ + Organises the output, using `n` to name the organised directory. + + Parameters + ---------- + n : int + Iteration number. + """ self.gudrunFile.iterativeOrganise(f"IterateByDensity_{n}") diff --git a/gudpy/core/element.py b/gudpy/core/element.py index 6b06ae538..faa1b1996 100644 --- a/gudpy/core/element.py +++ b/gudpy/core/element.py @@ -12,8 +12,13 @@ class Element: The atomic number belonging to the element (total number of nucleons). abundance : float Abundance of the element. + yamlignore : str{} + Class attributes to ignore during yaml serialisation. + Methods ------- + eq(obj) + Checks for equality between elements. """ def __init__(self, atomicSymbol, massNo, abundance): """ @@ -41,10 +46,6 @@ def __str__(self): """ Returns the string representation of the Element object. - Parameters - ---------- - None - Returns ------- string : str @@ -62,10 +63,6 @@ def __repr__(self): """ Returns the string representation of the Element object. - Parameters - ---------- - None - Returns ------- string : str @@ -74,6 +71,18 @@ def __repr__(self): return str(self) def eq(self, obj): + """ + Checks for equality between `obj` and the current object. + + Parameters + ---------- + obj : Element + Object to compare against. + + Returns + ------- + bool : self == obj + """ if ( hasattr(obj, 'atomicSymbol') and hasattr(obj, 'massNo') diff --git a/gudpy/core/enums.py b/gudpy/core/enums.py index 320066a4e..f10cbf08c 100644 --- a/gudpy/core/enums.py +++ b/gudpy/core/enums.py @@ -3,6 +3,20 @@ def enumFromDict(clsname, _dict): + """ + Creates an instance of `Enum` with name `clsname` from `_dict`. + + Parameters + ---------- + clsname : str + Resultant class name. + _dict : dict + Mapping from enum value to [display name, access name]. + + Returns + ------- + Enum : resultant Enum. + """ return Enum( value=clsname, names=chain.from_iterable( @@ -12,6 +26,9 @@ def enumFromDict(clsname, _dict): class Instruments(Enum): + """ + Enumerates Instrument names. + """ SANDALS = 0 GEM = 1 NIMROD = 2 @@ -22,6 +39,9 @@ class Instruments(Enum): class Scales(Enum): + """ + Enumrates scales. + """ Q = 1 D_SPACING = 2 WAVELENGTH = 3 @@ -29,15 +49,20 @@ class Scales(Enum): TOF = 5 +""" +Enumerates density units. +""" UNITS_OF_DENSITY = { 0: ["atoms/\u212b^3", "ATOMIC"], 1: ["gm/cm^3", "CHEMICAL"] } - UnitsOfDensity = enumFromDict("UnitsOfDensity", UNITS_OF_DENSITY) +""" +Enumerates merge weights modes. +""" MERGE_WEIGHTS = { 0: ["None", "NONE"], 1: ["By Detector", "DETECTOR"], @@ -46,6 +71,9 @@ class Scales(Enum): MergeWeights = enumFromDict("MergeWeights", MERGE_WEIGHTS) +""" +Enumerates normalisation types. +""" NORMALISATION_TYPES = { 0: ["Nothing", "NOTHING"], 1: ["^2", "AVERAGE_SQUARED"], @@ -54,6 +82,9 @@ class Scales(Enum): NormalisationType = enumFromDict("NormalisationType", NORMALISATION_TYPES) +""" +Enumerates output units. +""" OUTPUT_UNITS = { 0: ["barns/atom/sr", "BARNS_ATOM_SR"], 1: ["cm^-1/sr", "INV_CM_SR"] @@ -63,17 +94,25 @@ class Scales(Enum): class Geometry(Enum): + """ + Enumerates geometry. + """ FLATPLATE = 0 CYLINDRICAL = 1 SameAsBeam = 2 class CrossSectionSource(Enum): + """ + Enumerates cross section source types. + """ TABLES = 0 TRANSMISSION = 1 FILE = 2 - +""" +Enumerates Fourier Transform modes. +""" FT_MODES = { 0: ["No Fourier Transform", "NO_FT"], 1: ["Subtract Average (Qmin)", "SUB_AVERAGE"], @@ -84,10 +123,15 @@ class CrossSectionSource(Enum): class Format(Enum): + """ + Enumerates input file formats. + """ TXT = 0 YAML = 1 - +""" +Enumerates extrapolation modes. +""" EXTRAPOLATION_MODES = { 0: ["BACKWARDS"], 1: ["FORWARDS"], @@ -97,6 +141,9 @@ class Format(Enum): ExtrapolationModes = enumFromDict("ExtrapolationModes", EXTRAPOLATION_MODES) +""" +Enumerates iteration modes. +""" ITERATION_MODES = { 0: ["None", "NONE"], 1: ["Tweak Factor", "TWEAK_FACTOR"], diff --git a/gudpy/core/exception.py b/gudpy/core/exception.py index ab37d26f5..9bf5dd8aa 100644 --- a/gudpy/core/exception.py +++ b/gudpy/core/exception.py @@ -1,6 +1,14 @@ class ParserException(Exception): + """ + Stub class for ParserException. + Raised when errors occur whilst parsing from an input file. + """ pass class ChemicalFormulaParserException(Exception): + """ + Stub class for ChemicalFormulaParserException. + Raised when errors occur whilst parsing a chemical formula. + """ pass diff --git a/gudpy/core/file_library.py b/gudpy/core/file_library.py index f1ac04694..f47b6ec0f 100644 --- a/gudpy/core/file_library.py +++ b/gudpy/core/file_library.py @@ -13,14 +13,25 @@ class GudPyFileLibrary(): Attributes ---------- + gudrunFile : GudrunFile + Reference GudrunFile object. + dataFileDir : str + Data file directory. + fileDir : str + Gudrun input filele directory. dirs : str[] List of directories. files: str[] List of files + dataFiles : str[] + List of data files. + Methods ------- checkFilesExist() Checks if the files and directories exist, in the current file system. + exportMintData(samples, renameDataFiles=False, exportTo=None, includeParams=False) + Exports mint data. """ def __init__(self, gudrunFile): @@ -97,7 +108,7 @@ def checkFilesExist(self): Returns ------- - (bool, str)[] + (bool, str)[] : List of tuples of boolean values and paths, indicating if the given path exists. """ @@ -136,6 +147,24 @@ def exportMintData( self, samples, renameDataFiles=False, exportTo=None, includeParams=False ): + """ + Exports mint01 files outputted from given `samples`. + + Parameters + ---------- + samples : Sample[] + List of Sample objects to export. + renameDataFiles : bool, optional + Should mint01 files be renamed to sample? + exportTo : NoneType | str, optional + Export target. + includeParams : bool, optional + Should a sample parameters file be produced for each sample? + + Returns + ------- + str : path to produced zip file. + """ if not exportTo: exportTo = ( os.path.join( diff --git a/gudpy/core/gud_file.py b/gudpy/core/gud_file.py index 9fe490c5f..cd8e3c03e 100644 --- a/gudpy/core/gud_file.py +++ b/gudpy/core/gud_file.py @@ -3,10 +3,15 @@ import os from os.path import isfile import re + +# Set precision. from decimal import Decimal, getcontext getcontext().prec = 5 +# Regular expression for extracting percentages. percentageRegex = r'\d*[.]?\d*%' + +# Regular expression for extracting floats. floatRegex = r'\d*[.]?\d' @@ -68,12 +73,18 @@ class GudFile: closer to the expected level. Particularly used when iterating by tweak factor - where the suggested tweak factor is applied accross iterations. - contents : str + stream : str Contents of the .gud file. output : str Output for use in the GUI. Methods ------- + getNextLine(ignoreEmpty=False) + Gets the next line from the stream. + peekNextLine() + Gets the next line from the stream without removing it. + consumeLine(n) + Consumes n lines from the stream. parse(): Parses the GudFile from path, assigning values to each of the attributes. @@ -140,7 +151,7 @@ def getNextLine(self, ignoreEmpty=False): Should empty lines be ignored? Returns ------- - str | None + str | None : next line in stream, if available. """ if ignoreEmpty and self.stream: line = self.stream.pop(0) @@ -154,12 +165,9 @@ def peekNextLine(self): """ Returns the next line in the input stream, without removing it. - Parameters - ---------- - None Returns ------- - str | None + str | None : next line in stream, if available. """ return self.stream[0] if self.stream else None @@ -171,9 +179,6 @@ def consumeLines(self, n): ---------- n : int Number of lines to consume - Returns - ------- - None """ for _ in range(n): self.getNextLine() @@ -182,13 +187,6 @@ def parse(self): """ Parses the GudFile from its path, assigning extracted variables to their corresponding attributes. - - Parameters - ---------- - None - Returns - ------- - None """ # Read the contents into an auxilliary variable. @@ -294,18 +292,31 @@ def parse(self): f"{str(e)}" ) from e - def __str__(self): + def write_out(self, overwrite=False): """ - Returns the string representation of the GudFile object. + 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 ---------- - None + overwrite : bool, optional + Overwrite the initial file? (default is False). + """ + if not overwrite: + f = open(self.outpath, "w", encoding="utf-8") + else: + f = open(self.path, "w", encoding="utf-8") + f.write(str(self)) + f.close() + + def __str__(self): + """ + Returns the string representation of the GudFile object. Returns ------- - string : str - String representation of GudFile. + str : String representation of GudFile. """ outLine = ( f'{self.err}' @@ -347,26 +358,4 @@ def __str__(self): f' Suggested tweak factor: ' f'{self.suggestedTweakFactor}\n' - ) - - def write_out(self, overwrite=False): - """ - 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 - """ - if not overwrite: - f = open(self.outpath, "w", encoding="utf-8") - else: - f = open(self.path, "w", encoding="utf-8") - f.write(str(self)) - f.close() + ) \ No newline at end of file diff --git a/gudpy/core/gudpy_yaml.py b/gudpy/core/gudpy_yaml.py index d0f8d6882..c94861b77 100644 --- a/gudpy/core/gudpy_yaml.py +++ b/gudpy/core/gudpy_yaml.py @@ -20,11 +20,52 @@ class YAML: + """ + Class for performing YAML serialisation / deserialisation. + ... + + Methods + ------- + getYamlModule() + Returns object wrapping YAML module. + parseYaml(path) + Parses YAML. + yamlToDict(path) + Deserialises YAML into dict. + constructClasses(yamldict) + Converts YAML dictionary into GudPy objects. + maskYAMLDicttoClass(cls, yamldict) + Converts YAML dictionary into type `cls`. + maskYAMLSeqtoClass(cls, yamlseq) + Converts YAML sequence into type `cls`. + writeYAML(base, path) + Writes YAML to `path` by serialising `base`. + toYaml(var) + Converts given variable to YAML. + toBuiltin(yamlvar) + Converts given YAML variable to builtin types. + + Attributes + ---------- + yaml : ruamel.yaml.YAML + YAML module wrapper. + """ def __init__(self): + """ + Constructs all the necessary attributes for the YAML class. + """ self.yaml = self.getYamlModule() def getYamlModule(self): + """ + Creates a wrapper for the ruamel.yaml.YAML module. + Also configures said module. + + Returns + ------- + ruamel.yaml.YAML : module wrapper + """ yaml_ = yaml() yaml_.preserve_quotes = True yaml_.default_flow_style = None @@ -32,10 +73,34 @@ def getYamlModule(self): return yaml_ def parseYaml(self, path): + """ + Parses YAML from `path`. + + Parameters + ---------- + path : str + Path to YAML file. + + Returns + ------- + (Instrument, Beam, Components, Normalisation, SampleBackground, GUIConfig) : Constructed classes. + """ self.path = path return self.constructClasses(self.yamlToDict(path)) def yamlToDict(self, path): + """ + Loads YAML from `path` into a dictionary. + + Parameters + ---------- + path : str + Path to parse YAL from. + + Returns + ------- + dict : Dictionary of YAML. + """ # Decide the encoding import chardet with open(path, 'rb') as fp: @@ -46,6 +111,18 @@ def yamlToDict(self, path): return self.yaml.load(fp) def constructClasses(self, yamldict): + """ + Converts a dictionary of YAML into GudPy objects. + + Parameters + ---------- + yamldict : dict + Dictionary to create objects from. + + Returns + ------- + (Instrument, Beam, Components, Normalisation, SampleBackground, GUIConfig) : Constructed classes. + """ instrument = Instrument() self.maskYAMLDicttoClass(instrument, yamldict["Instrument"]) instrument.GudrunInputFileDir = os.path.dirname( @@ -56,7 +133,7 @@ def constructClasses(self, yamldict): beam = Beam() self.maskYAMLDicttoClass(beam, yamldict["Beam"]) components = Components() - self.maskYAMLSeqtoClss(components, yamldict["Components"]) + self.maskYAMLSeqtoClass(components, yamldict["Components"]) normalisation = Normalisation() self.maskYAMLDicttoClass(normalisation, yamldict["Normalisation"]) sampleBackgrounds = [] @@ -75,6 +152,20 @@ def constructClasses(self, yamldict): @abstractmethod def maskYAMLDicttoClass(self, cls, yamldict): + """ + Converts YAML dictionary into type `cls`. + + Parameters + ---------- + cls : any + Target class. + yamldict : dict + Dictionary of YAML. + + Returns + ------- + any : Created object. + """ for k, v in yamldict.items(): if isinstance(cls.__dict__[k], Enum): setattr(cls, k, type(cls.__dict__[k])[v]) @@ -136,7 +227,17 @@ def maskYAMLDicttoClass(self, cls, yamldict): else: setattr(cls, k, type(cls.__dict__[k])(self.toBuiltin(v))) - def maskYAMLSeqtoClss(self, cls, yamlseq): + def maskYAMLSeqtoClass(self, cls, yamlseq): + """ + Converts YAML dequence into type `cls`. + + Parameters + ---------- + cls : any + Target class + yamlseq : any[] + Sequence of YAML. + """ if isinstance(cls, Components): components = [] for component in yamlseq: @@ -146,6 +247,16 @@ def maskYAMLSeqtoClss(self, cls, yamlseq): setattr(cls, "components", components) def writeYAML(self, base, path): + """ + Serialises and writes `base` to YAML. + + Parameters + ---------- + base : GudrunFile + Base class to serialise. + path : str + Path to write to. + """ with open(path, "wb") as fp: outyaml = { "Instrument": base.instrument, @@ -162,6 +273,18 @@ def writeYAML(self, base, path): @abstractmethod def toYaml(self, var): + """ + Converts a given variable to YAML. + + Parameters + ---------- + var : any + Target variable. + + Returns + ------- + any : YAML serialised variable. + """ if var.__class__.__module__ == "ruamel.yaml.scalarfloat": return float(var) if var.__class__.__module__ == "builtins": @@ -184,6 +307,15 @@ def toYaml(self, var): @abstractmethod def toBuiltin(self, yamlvar): + """ + Converts `yamlvar` to builtin type. + + Parameters + ---------- + yamlvar : any + Target YAML variable. + any : variable casted to builtin. + """ if isinstance(yamlvar, (list, tuple)): return [self.toBuiltin(v) for v in yamlvar] elif yamlvar.__class__.__module__ == "builtins": diff --git a/gudpy/core/gudrun_file.py b/gudpy/core/gudrun_file.py index 502c5e7a8..e039db693 100644 --- a/gudpy/core/gudrun_file.py +++ b/gudpy/core/gudrun_file.py @@ -44,9 +44,9 @@ class GudrunFile: """ - Class to represent a GudFile (files with .gud extension). - .gud files are outputted by gudrun_dcs, via merge_routines - each .gud file belongs to an individual sample. + Class to represent a GudrunFile. + This is the core class of GudPy, and provides and interface + between GudPy and Gudrun. ... @@ -54,8 +54,12 @@ class GudrunFile: ---------- path : str Path to the file. + yaml : YAML + YAML wrapper for performing YAML serialisation/de-serialisation. outpath : str Path to write to, when not overwriting the initial file. + components : Components + Global Components. instrument : Instrument Instrument object extracted from the input file. beam : Beam @@ -69,6 +73,8 @@ class GudrunFile: stream : str[] List of strings, where each item represents a line in the input stream. + purgeFile : PurgeFile + Interface for purging detectors. Methods ------- getNextToken(): @@ -107,6 +113,12 @@ class GudrunFile: Initialises a Container object. Parses the attributes of the Container from the input stream. Returns the Container object. + parseComponents() + Parses components and appeds them to Components. + parseComponent() + Initialises a Component object. + Parses the attributes of the Component from the input stream. + Returns the component object. makeParse(key): Uses the key to call a parsing function from a dictionary of parsing functions. @@ -114,16 +126,28 @@ class GudrunFile: sampleBackgroundHelper(): Parses the SampleBackground, its Samples and their Containers. Returns the SampleBackground object. - parse(): + parse(config_=False): Parse the GudrunFile from its path. Assign objects from the file to the attributes of the class. - write_out(overwrite=False) + save(path='', format=None) + Saves the GudrunFile to the given path in the given format. + write_yaml(path) + Writes the GudrunFile as YAML to the given path. + write_out(path='', overwrite=False, writeParameters=True) Writes out the string representation of the GudrunFile to a file. - dcs(path='', purge=True): + dcs(path='', headless=True, iterative=False): Call gudrun_dcs on the path supplied. If the path is its default value, then use the path attribute as the path. - process(): + process(headless=True, iterative=False): Write out the GudrunFile, and call gudrun_dcs on the outputted file. + convertToSample(container, persist=False) + Converts a given container to a Sample object. + naiveOrganise() + Performs naive organisation of output files. + iterativeOrganise(head) + Performs iterative organisation using `head`. + determineError(sample) + Determines error in results. purge(): Create a PurgeFile from the GudrunFile, and run purge_det on it. """ @@ -179,9 +203,6 @@ 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 @@ -192,9 +213,6 @@ def peekNextToken(self): """ Returns the next token in the input stream, without removing it. - Parameters - ---------- - None Returns ------- str | None @@ -207,10 +225,8 @@ def consumeTokens(self, n): Parameters ---------- - None - Returns - ------- - None + n : int + Number of tokens to consume. """ for _ in range(n): self.getNextToken() @@ -221,10 +237,8 @@ def consumeUpToDelim(self, delim): Parameters ---------- - None - Returns - ------- - None + delim : str + Delimiter to seek until. """ line = self.getNextToken() while line[0] != delim: @@ -233,13 +247,6 @@ def consumeUpToDelim(self, delim): def consumeWhitespace(self): """ Consume tokens iteratively, while they are whitespace. - - Parameters - ---------- - None - Returns - ------- - None """ line = self.peekNextToken() if line and line.isspace(): @@ -252,14 +259,6 @@ def parseInstrument(self): 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: # Initialise instrument attribute to a new instance of Instrument. @@ -480,14 +479,6 @@ def parseBeam(self): 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: @@ -584,14 +575,6 @@ def parseNormalisation(self): 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: @@ -784,9 +767,6 @@ def parseSampleBackground(self): Raises a ParserException if any mandatory attributes are missing. Returns the parsed object. - Parameters - ---------- - None Returns ------- sampleBackground : SampleBackground @@ -828,9 +808,6 @@ def parseSample(self): Raises a ParserException if any mandatory attributes are missing. Returns the parsed object. - Parameters - ---------- - None Returns ------- sample : Sample @@ -1034,9 +1011,6 @@ def parseContainer(self): Raises a ParserException if any mandatory attributes are missing. Returns the parsed object. - Parameters - ---------- - None Returns ------- container : Container @@ -1173,6 +1147,11 @@ def parseContainer(self): ) from e def parseComponents(self): + """ + Parses components and appends them to the Components. + Raises a ParserException if manditory attributes are missing, + or if components are incorrectly defined. + """ try: while self.stream: component = self.parseComponent() @@ -1185,6 +1164,14 @@ def parseComponents(self): ) from e def parseComponent(self): + """ + Initialises a Component object, and parses attributes + from the stream into this object. + + Returns + ------- + Component | None : parsed component, if success. + """ name = self.getNextToken().rstrip() component = Component(name) line = self.peekNextToken() @@ -1212,6 +1199,7 @@ def makeParse(self, key): key : str Parsing function to call (INSTRUMENT/BEAM/NORMALISATION/SAMPLE BACKGROUND/SAMPLE/CONTAINER) + Returns ------- NoneType @@ -1243,13 +1231,10 @@ 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. + SampleBackground : The SampleBackground parsed from the lines. """ # Parse sample background. @@ -1283,10 +1268,8 @@ def parse(self, config_=False): Parameters ---------- - None - Returns - ------- - None + config_ : bool + Is this an Instrument configuration? """ self.config = config_ # Ensure only valid files are given. @@ -1377,14 +1360,9 @@ def __str__(self): """ Returns the string representation of the GudrunFile object. - Parameters - ---------- - None - Returns ------- - string : str - String representation of GudrunFile. + str : String representation of GudrunFile. """ LINEBREAK = "\n\n" @@ -1435,7 +1413,16 @@ def __str__(self): ) def save(self, path='', format=None): + """ + Saves the GudrunFile object to `path` in `format`. + Parameters + ---------- + path : str + Path to write to. + format : None | Format, optional + Format to use when writing. + """ if not path: path = self.path @@ -1447,23 +1434,30 @@ def save(self, path='', format=None): self.write_yaml(path=path.replace(path.split(".")[-1], "yaml")) def write_yaml(self, path): + """ + Serialises GudrunFile object to YAML and writes to `path`. + + Parameters + ---------- + path : str + Path to write to. + """ 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'. + Otherwise, it is written to `outpath`. Parameters ---------- - overwrite : bool, optional - Overwrite the initial file? (default is False). path : str, optional Path to write to. - Returns - ------- - None + overwrite : bool, optional + Overwrite the initial file? (default is False). + writeParameters : bool, optional + Should a sample parameters file be written? """ if path: f = open( @@ -1507,22 +1501,24 @@ def dcs(self, path='', headless=True, iterative=False): Parameters ---------- - overwrite : bool, optional - Overwrite the initial file? (default is False). path : str, optional - Path to parse from (default is empty, which indicates self.path). - purge : bool, optional - Should detectors be purged? + Path to write to before calling Gudrun. + headless : bool, optional + Is this a headless process? + iterative : bool, optional + Is this part of an iterative workflow? + Returns ------- - subprocess.CompletedProcess - The result of calling gudrun_dcs using subprocess.run. - Can access stdout/stderr from this. + subprocess.CompletedProcess | (QProcess, self.write_out, [path, False]) + The result of calling gudrun_dcs using subprocess.run, if headless. + Otherwise, a QProcess, and intermediate function to call with arguments. """ if not path: path = os.path.basename(self.path) if headless: try: + # Find Gudrun binary, and call it gudrun_dcs = resolve("bin", f"gudrun_dcs{SUFFIX}") cwd = os.getcwd() os.chdir(self.instrument.GudrunInputFileDir) @@ -1533,10 +1529,13 @@ def dcs(self, path='', headless=True, iterative=False): except FileNotFoundError: os.chdir(cwd) return False + # Only perform a naive organise if this is not part of an iterative workflow. if not iterative: self.naiveOrganise() return result else: + # `_MEIPASS` indicates that this is being run from a PyInstaller binary. + # Find Gudrun binary. if hasattr(sys, '_MEIPASS'): gudrun_dcs = os.path.join(sys._MEIPASS, f"gudrun_dcs{SUFFIX}") else: @@ -1548,6 +1547,7 @@ def dcs(self, path='', headless=True, iterative=False): if not os.path.exists(gudrun_dcs): return FileNotFoundError() else: + # Create a QProcess which calls Gudrun. proc = QProcess() proc.setProgram(gudrun_dcs) proc.setArguments([path]) @@ -1568,13 +1568,16 @@ def process(self, headless=True, iterative=False): Parameters ---------- - purge : bool, optional - Should detectors be purged? + headless : bool, optional + Is this a headless process? + iterative : bool, optional + Is this part of an iterative workflow? + Returns ------- - subprocess.CompletedProcess - The result of calling gudrun_dcs using subprocess.run. - Can access stdout/stderr from this. + subprocess.CompletedProcess | (QProcess, self.write_out, [path, False]) + The result of calling gudrun_dcs using subprocess.run, if headless. + Otherwise, a QProcess, and intermediate function to call with arguments. """ cwd = os.getcwd() os.chdir(self.instrument.GudrunInputFileDir) @@ -1593,12 +1596,16 @@ def purge(self, *args, **kwargs): Parameters ---------- - None + args : Sequence + Sequence of arguments + args : dict + Dictionary of keyword arguments. + Returns ------- - subprocess.CompletedProcess - The result of calling purge_det using subprocess.run. - Can access stdout/stderr from this. + subprocess.CompletedProcess | (QProcess, self.write_out, [path, False]) + The result of calling purge_det using subprocess.run, if headless. + Otherwise, a QProcess, and intermediate function to call with arguments. """ self.purgeFile = PurgeFile(self) result = self.purgeFile.purge(*args, **kwargs) @@ -1607,7 +1614,20 @@ def purge(self, *args, **kwargs): return result def convertToSample(self, container, persist=False): + """ + Converts a given container to a Sample object. + Parameters + ---------- + container : Container + Container object to convert. + persist : bool, optional + Should this conversion persist in the GudrunFile? + + Returns + ------- + Sample : converted container. + """ sample = container.convertToSample() if persist: @@ -1620,14 +1640,39 @@ def convertToSample(self, container, persist=False): return sample def naiveOrganise(self): + """ + Uses the OutputFileHandler, to perform naive + organisation of the output. + """ outputFileHandler = OutputFileHandler(self) outputFileHandler.naiveOrganise() def iterativeOrganise(self, head): + """ + Uses the OutputFileHandler, to perform an iterative + organisation of the output. + + Parameters + ---------- + head : str + Directory to pipe organised files into. + """ outputFileHandler = OutputFileHandler(self) outputFileHandler.iterativeOrganise(head) def determineError(self, sample): + """ + Determines error in results, relevant to `sample`. + + Parameters + ---------- + sample : Sample + Target sample. + + Returns + ------- + float : computed error. + """ gudPath = sample.dataFiles[0].replace( self.instrument.dataFileType, "gud" diff --git a/gudpy/core/gui_config.py b/gudpy/core/gui_config.py index 752d1cf56..e81a1f803 100644 --- a/gudpy/core/gui_config.py +++ b/gudpy/core/gui_config.py @@ -1,4 +1,16 @@ class GUIConfig(): + """ + A simple class for defining the GUI configuration. + + ... + + Attributes + ---------- + useComponents : bool + Should components be used? + yamlignore : str{} + Class attributes to ignore during yaml serialisation. + """ def __init__(self): self.useComponents = False self.yamlignore = { diff --git a/gudpy/core/instrument.py b/gudpy/core/instrument.py index ba66f3acc..e4a431bca 100644 --- a/gudpy/core/instrument.py +++ b/gudpy/core/instrument.py @@ -33,7 +33,7 @@ class Instrument: module and data acquisition dead times. spectrumNumbersForIncidentBeamMonitor : int[] Number of spectra of incident beam monitor. - wavelengthRangeForMonitorNormalisation : tuple(float, float) + wavelengthRangeForMonitorNormalisation : float[] Input wavelength range for monitor normalisation. 0 0 signals to divide channel by channel. spectrumNumbersForTransmissionMonitor : int[] @@ -42,7 +42,7 @@ class Instrument: Quiet count constant for incident beam monitor. transmissionMonitorQuietCountConst : float Quiet count constant for transmission beam monitor. - channelNosSpikeAnalysis : tuple(int, int) + channelNosSpikeAnalysis : int[] First and last channel numbers to check for spikes. 0 0 signals to use all channels. spikeAnalysisAcceptanceFactor : float @@ -73,10 +73,8 @@ class Instrument: Power used to set X-weighting for merge. subSingleAtomScattering : bool Should we subtract a background from each group prior to merge? - mergeWeights : int - 0 = None - 1 = By detector - 2 = By channel + mergeWeights : MergeWeights + Merge weights by..? incidentFlightPath : float Incident flight path. spectrumNumberForOutputDiagnosticFiles : int @@ -103,17 +101,15 @@ class Instrument: Should hard group edges be used? nxsDefinitionFile : str NeXus definition file to be used, if NeXus files are being used. - Methods - ------- + goodDetectorThreshold : int + Threshold to use when checking if number of purged detectors is acceptable. + yamlignore : str{} + Class attributes to ignore during yaml serialisation. """ def __init__(self): """ Constructs all the necessary attributes for the Instrument object. - - Parameters - ---------- - None """ self.name = Instruments.NIMROD self.GudrunInputFileDir = "" @@ -180,10 +176,6 @@ def __str__(self): """ Returns the string representation of the Instrument object. - Parameters - ---------- - None - Returns ------- string : str diff --git a/gudpy/core/isotopes.py b/gudpy/core/isotopes.py index 007a6244f..973b8059b 100644 --- a/gudpy/core/isotopes.py +++ b/gudpy/core/isotopes.py @@ -1,4 +1,47 @@ class Sears91(): + """ + Wrapper class for Sears91 isotope data. + + ... + + Attributes + ---------- + sears91Data : tuple[] + Isotope data. + + Methods + ------- + isotopeData(element, mass) + Returns the isotope data for a given element, mass combination. + isotope(isotope_) + Returns the isotope name. + element(isotope) + Returns the isotope element. + mass(isotope) + Returns the isotope mass. + spin(isotope) + Returns the isotope spin. + atwt(isotope) + Returns the isotope atwt. + boundCoherent(isotope) + Returns the isotope bound coherent. + boundIncoherent(isotope) + Returns the isotope bound incoherent. + boundCoherentXS(isotope) + Returns the isotope bound coherent cross section. + boundIncoherentXS(isotope) + Returns the istope bound incoherent cross section. + totalXS(isotope) + Returns the isotope total cross section. + absorptionXS(isotope) + Returns the isotope absorption cross section. + isotopes(element) + Returns all isotopes of a given element. + findIsotope(element, mass) + Returns isotope of element with given mass. + isIsotope(element, mass) + Determines if the given element, mass combination refers to a valid isotope. + """ sears91Data = [ ("Unknown", "Unknown", 0, "", 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), @@ -370,54 +413,212 @@ class Sears91(): ] def isotopeData(self, element, mass): + """ + Returns the isotope data for a given element, mass combination. + + Parameters + ---------- + element : str + Target element. + mass : float + Target mass. + + Returns + ------- + isotopes : found isotope. + """ if self.isIsotope(element, mass): return [isotope for isotope in self.sears91Data if self.element(isotope) == element and self.mass(isotope) == mass][0] @staticmethod def isotope(isotope_): + """ + Returns the isotope name. + + Parameters + ---------- + isotope_ : tuple + Isotope data. + + Returns + ------- + str : isotope name. + """ return isotope_[0] @staticmethod def element(isotope): + """ + Returns the isotope element. + + Parameters + ---------- + isotope : tuple + Isotope data. + + Returns + ------- + str : isotope element. + """ return isotope[1] @staticmethod def mass(isotope): + """ + Returns the isotope mass. + + Parameters + ---------- + isotope : tuple + Isotope data. + + Returns + ------- + float : isotope mass. + """ return isotope[2] @staticmethod def spin(isotope): + """ + Returns the isotope spin. + + Parameters + ---------- + isotope : tuple + Isotope data. + + Returns + ------- + float : isotope spin. + """ return isotope[3] @staticmethod def atwt(isotope): + """ + Returns the isotope atwt. + + Parameters + ---------- + isotope : tuple + Isotope data. + + Returns + ------- + float : isotope atwt. + """ return isotope[4] @staticmethod def boundCoherent(isotope): + """ + Returns the isotope bound coherent. + + Parameters + ---------- + isotope : tuple + Isotope data. + + Returns + ------- + float : isotope bound coherent. + """ return isotope[5] @staticmethod def boundIncoherent(isotope): + """ + Returns the isotope bound incoherent. + + Parameters + ---------- + isotope : tuple + Isotope data. + + Returns + ------- + float : isotope bound incoherent. + """ return isotope[6] @staticmethod def boundCoherentXS(isotope): + """ + Returns the isotope bound coherent cross section. + + Parameters + ---------- + isotope : tuple + Isotope data. + + Returns + ------- + float : isotope bound coherent cross section. + """ return isotope[7] @staticmethod def boundIncoherentXS(isotope): + """ + Returns the isotope bound incoherent cross section. + + Parameters + ---------- + isotope : tuple + Isotope data. + + Returns + ------- + float : isotope bound incoherent cross section. + """ return isotope[8] @staticmethod def totalXS(isotope): + """ + Returns the isotope total cross section. + + Parameters + ---------- + isotope : tuple + Isotope data. + + Returns + ------- + float : isotope total cross section. + """ return isotope[9] @staticmethod def absorptionXS(isotope): + """ + Returns the isotope absorption total cross section. + + Parameters + ---------- + isotope : tuple + Isotope data. + + Returns + ------- + float : isotope absorption total cross section. + """ return isotope[10] def isotopes(self, element): + """ + Returns all isotopes of a given element. + + Parameters + ---------- + element : str + Target element. + + Returns + ------- + tuple[] : list of found isotopes + """ return [ isotope for isotope in self.sears91Data @@ -425,11 +626,39 @@ def isotopes(self, element): ] def findIsotope(self, element, mass): + """ + Finds an isotope of `element` with a given `mass`. + + Parameters + ---------- + element : str + Target element. + mass : float + Target mass. + + Returns + ------- + tuple : isotope data. + """ for isotope in self.isotopes(element): if self.mass(isotope) == mass: return isotope def isIsotope(self, element, mass): + """ + Determines if the given element, mass combination refers to a valid isotope. + + Parameters + ---------- + element : str + Target element. + mass : float + Target mass. + + Returns + ------- + bool : is isotope valid? + """ isotopes = self.isotopes(element) for isotope in isotopes: if self.mass(isotope) == mass: diff --git a/gudpy/core/mass_data.py b/gudpy/core/mass_data.py index 98e444d29..27784f005 100644 --- a/gudpy/core/mass_data.py +++ b/gudpy/core/mass_data.py @@ -1,3 +1,6 @@ +""" +Mass Data for natural isotopes. +""" massData = { "H": 1.00798175, # Uncertainty = (1): VSMOW "He": 4.002602, # Uncertainty = (2) diff --git a/gudpy/core/normalisation.py b/gudpy/core/normalisation.py index a53480cef..b07f87d50 100644 --- a/gudpy/core/normalisation.py +++ b/gudpy/core/normalisation.py @@ -61,16 +61,12 @@ class Normalisation: Degree of smoothing on Vanadium. minNormalisationSignalBR : float Vanadium signal to background acceptance ratio. - Methods - ------- + yamlignore : str{} + Class attributes to ignore during yaml serialisation. """ def __init__(self): """ Constructs all the necessary attributes for the Normalistion object. - - Parameters - ---------- - None """ self.periodNumber = 1 self.dataFiles = DataFiles([], "NORMALISATION") @@ -104,10 +100,6 @@ def __str__(self): """ Returns the string representation of the Normalisation object. - Parameters - ---------- - None - Returns ------- string : str diff --git a/gudpy/core/output_file_handler.py b/gudpy/core/output_file_handler.py index 30249efa0..974c2730f 100644 --- a/gudpy/core/output_file_handler.py +++ b/gudpy/core/output_file_handler.py @@ -3,8 +3,40 @@ class OutputFileHandler(): + """ + Class to handle organising the output files of Gudrun. + ... + + Parameters + ---------- + gudrunFile : GudrunFile + Reference GudrunFile object. + outputs : str[]{} + Dictionary of output files. + runFiles : str[] + + Methods + ------- + getRunFiles() + Sets the run files. + organiseSampleFiles(run, sampleRunFile, tree="") + Organises sample output files. + naiveOrganise() + Performs a naive organise. + iterativeOrganise(head) + Performs an iterative organise using `head`. + """ def __init__(self, gudrunFile): + """ + Sets up all the attributes for the OutputFileHandler class. + Collects the run files. + + Parameters + ---------- + gudrunFile : GudrunFile + Reference GudrunFile object. + """ self.gudrunFile = gudrunFile self.getRunFiles() self.outputs = { @@ -42,6 +74,11 @@ def __init__(self, gudrunFile): } def getRunFiles(self): + """ + Collects and assigns the run files. + Each run file is prefixed with the name of the first + data file belonging to the Sample. + """ self.runFiles = [ [os.path.splitext(s.dataFiles[0])[0], s.pathName()] for sampleBackground in self.gudrunFile.sampleBackgrounds @@ -50,17 +87,37 @@ def getRunFiles(self): ] def organiseSampleFiles(self, run, sampleRunFile, tree=""): + """ + Organises the Sample outputs according to `tree`. + + Parameters + ---------- + run : str + Run file name, without extension. + sampleRunFile : str + Run file name, with extension. + tree : str, optional + Structure to use. + """ dir = self.gudrunFile.instrument.GudrunInputFileDir + + # If tree is present, construct the paths + # otherwise, just use the current directory as the root. if tree: outputDir = os.path.join(dir, tree, run) else: outputDir = os.path.join(dir, run) + # Remove tree if it exists. if os.path.exists(outputDir): shutil.rmtree(outputDir) + + # Set up necessary directories. if not os.path.exists(outputDir): os.makedirs(outputDir) os.makedirs(os.path.join(outputDir, "outputs")) os.makedirs(os.path.join(outputDir, "diagnostics")) + + # Copy relevant files into newly created directories.`` if os.path.exists(os.path.join(dir, sampleRunFile)): shutil.copyfile( os.path.join(dir, sampleRunFile), @@ -81,10 +138,26 @@ def organiseSampleFiles(self, run, sampleRunFile, tree=""): ) def naiveOrganise(self): + """ + Performs a naive organise of output files. + This simply creates a directory named after the first data file + (i.e. what the results are merged to), and creates the + diagnostic / output directories there. + """ for run, runFile in self.runFiles: self.organiseSampleFiles(run, runFile) def iterativeOrganise(self, head): + """ + Performs an 'iterative' organise of output files. + This simply creates a directory named `head`, and + naively organises output files into there. + + Parameters + ---------- + head : str + Root directory name. + """ path = os.path.join( self.gudrunFile.instrument.GudrunInputFileDir, head diff --git a/gudpy/core/purge_file.py b/gudpy/core/purge_file.py index fc95f4717..1f6922b6c 100644 --- a/gudpy/core/purge_file.py +++ b/gudpy/core/purge_file.py @@ -56,13 +56,6 @@ 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. @@ -74,13 +67,83 @@ def write_out(self, path=""): f.write(str(self)) f.close() - def __str__(self): + def purge( + self, + standardDeviation=(10, 10), + ignoreBad=True, + excludeSampleAndCan=True, + headless=True + ): """ - Returns the string representation of the PurgeFile object. + Write out the current state of the PurgeFile, then + purge detectors by calling purge_det on that file. Parameters ---------- - None + standardDeviation: tuple(int, int), optional + Number of std deviations allowed above and below + the mean ratio and the range of std's allowed around the mean + standard deviation. Default is (10, 10) + ignoreBad : bool, optional + Ignore any existing bad spectrum files (spec.bad, spec.dat)? + Default is True. + excludeSampleAndCan : bool, optional + Exclude sample and container data files? + headless : bool + Should headless mode be used? + + Returns + ------- + subprocess.CompletedProcess | (QProcess, self.write_out, [path, False]) + The result of calling purge_det using subprocess.run, if headless. + Otherwise, a QProcess, and intermediate function to call with arguments. + """ + self.standardDeviation = standardDeviation + self.ignoreBad = ignoreBad + self.excludeSampleAndCan = excludeSampleAndCan + if headless: + try: + cwd = os.getcwd() + purge_det = resolve("bin", f"purge_det{SUFFIX}") + os.chdir(self.gudrunFile.instrument.GudrunInputFileDir) + self.write_out() + result = subprocess.run( + [purge_det, "purge_det.dat"], + capture_output=True, + text=True + ) + os.chdir(cwd) + except FileNotFoundError: + return False + return result + else: + if hasattr(sys, '_MEIPASS'): + purge_det = os.path.join(sys._MEIPASS, f"purge_det{SUFFIX}") + else: + purge_det = resolve( + os.path.join( + config.__rootdir__, "bin" + ), f"purge_det{SUFFIX}" + ) + if not os.path.exists(purge_det): + return FileNotFoundError() + proc = QProcess() + proc.setProgram(purge_det) + proc.setArguments([]) + return ( + proc, + self.write_out, + [ + os.path.join( + self.gudrunFile.instrument.GudrunInputFileDir, + "purge_det.dat" + ) + ] + ) + + def __str__(self): + """ + Returns the string representation of the PurgeFile object. Returns ------- @@ -211,77 +274,4 @@ def __str__(self): f'Ignore any existing bad spectrum and spike files' f' (spec.bad, spike.dat)?\n' f'{dataFilesLines}' - ) - - def purge( - self, - standardDeviation=(10, 10), - ignoreBad=True, - excludeSampleAndCan=True, - headless=True - ): - """ - Write out the current state of the PurgeFile, then - purge detectors by calling purge_det on that file. - - Parameters - ---------- - standardDeviation: tuple(int, int), optional - Number of std deviations allowed above and below - the mean ratio and the range of std's allowed around the mean - standard deviation. Default is (10, 10) - ignoreBad : bool, optional - Ignore any existing bad spectrum files (spec.bad, spec.dat)? - Default is True. - excludeSampleAndCan : bool, optional - Exclude sample and container data files? - headless : bool - Should headless mode be used? - Returns - ------- - subprocess.CompletedProcess - The result of calling purge_det using subprocess.run. - Can access stdout/stderr from this. - """ - self.standardDeviation = standardDeviation - self.ignoreBad = ignoreBad - self.excludeSampleAndCan = excludeSampleAndCan - if headless: - try: - cwd = os.getcwd() - purge_det = resolve("bin", f"purge_det{SUFFIX}") - os.chdir(self.gudrunFile.instrument.GudrunInputFileDir) - self.write_out() - result = subprocess.run( - [purge_det, "purge_det.dat"], - capture_output=True, - text=True - ) - os.chdir(cwd) - except FileNotFoundError: - return False - return result - else: - if hasattr(sys, '_MEIPASS'): - purge_det = os.path.join(sys._MEIPASS, f"purge_det{SUFFIX}") - else: - purge_det = resolve( - os.path.join( - config.__rootdir__, "bin" - ), f"purge_det{SUFFIX}" - ) - if not os.path.exists(purge_det): - return FileNotFoundError() - proc = QProcess() - proc.setProgram(purge_det) - proc.setArguments([]) - return ( - proc, - self.write_out, - [ - os.path.join( - self.gudrunFile.instrument.GudrunInputFileDir, - "purge_det.dat" - ) - ] - ) + ) \ No newline at end of file diff --git a/gudpy/core/radius_iterator.py b/gudpy/core/radius_iterator.py index a7a972bf3..2c1228809 100644 --- a/gudpy/core/radius_iterator.py +++ b/gudpy/core/radius_iterator.py @@ -10,6 +10,7 @@ class RadiusIterator(SingleParamIterator): radius of each sample across iterations. The new radii are determined by applying a coefficient calculated from the results of gudrun_dcs in the previous iteration. + ... Methods @@ -21,14 +22,41 @@ class RadiusIterator(SingleParamIterator): organiseOutput Organises the output of the iteration. """ + def applyCoefficientToAttribute(self, object, coefficient): + """ + Applies a computed coefficient the target attribute of the target object. + + Parameters + ---------- + object : Sample + Target Sample. + coefficient : float + Coefficient to apply to target attribute. + """ if self.targetRadius == "inner": object.innerRadius *= coefficient elif self.targetRadius == "outer": object.outerRadius *= coefficient def setTargetRadius(self, targetRadius): + """ + Sets the targetRadius. + + Parameters + ---------- + targetRadius : str + Target Radius to set (inner/outer) + """ self.targetRadius = targetRadius def organiseOutput(self, n): + """ + Performs iterative organisation on the output. + + Parameters + ---------- + n : int + Iteration number + """ self.gudrunFile.iterativeOrganise(f"IterateByRadius_{n}") diff --git a/gudpy/core/run_containers_as_samples.py b/gudpy/core/run_containers_as_samples.py index a98539836..747c6ff57 100644 --- a/gudpy/core/run_containers_as_samples.py +++ b/gudpy/core/run_containers_as_samples.py @@ -2,12 +2,38 @@ class RunContainersAsSamples: + """ + Class for running containers as samples. + + ... + Attributes + ---------- + gudrunFile : GudrunFile + Reference GudrunFile object. + + Methods + ------- + convertContainers() + Converts the containers to samples. + runContainersAsSamples(path='', headless=False) + Runs containers as samples. + """ def __init__(self, gudrunFile): + """ + Sets up the attributes for the RunContainersAsSamples class. + Paremeters + ---------- + gudrunFile : GudrunFile + Reference GudrunFile object. + """ self.gudrunFile = deepcopy(gudrunFile) def convertContainers(self): + """ + Converts containers to samples. + """ containersAsSamples = [] for sampleBackground in self.gudrunFile.sampleBackgrounds: for sample in sampleBackground.samples: @@ -18,5 +44,21 @@ def convertContainers(self): sampleBackground.samples = containersAsSamples def runContainersAsSamples(self, path='', headless=False): + """ + Converts containers to samples, and then processes said samples. + + Parameters + ---------- + path : str, optional + Path to write to, for Gudrun to use. + headless : bool, optional + Is his a headless process? + + Returns + ------- + subprocess.CompletedProcess | (QProcess, self.write_out, [path, False]) + The result of calling gudrun_dcs using subprocess.run, if headless. + Otherwise, a QProcess, and intermediate function to call with arguments. + """ self.convertContainers() return self.gudrunFile.dcs(path=path, headless=headless) diff --git a/gudpy/core/sample.py b/gudpy/core/sample.py index 6ca85af46..2fefc4185 100644 --- a/gudpy/core/sample.py +++ b/gudpy/core/sample.py @@ -45,8 +45,8 @@ class Sample: Height of the sample - if its geometry is CYLINDRICAL. density : str Density of the sample - densityUnits : int - 0 = atoms/Angstrom^3, 1 = gm/cm^3 + densityUnits : UnitsOfDensity + Units to use for output density (atoms/Angstrom^3, gm/cm^3) tempForNormalisationPC : float Temperature for Placzek Correction. overallBackgroundFactor : float @@ -59,24 +59,26 @@ class Sample: Tweak factor for the sample. topHatW : float Width of top hat function for Fourier transform. + FTMode : FTModes + Fourier Transform mode. minRadFT : float Minimum radius for Fourier transform. grBroadening : float Broadening of g(r) at r = 1 Angstrom - resonanceValues : tuple[] - List of tuples storing wavelength ranges for resonance values. - exponentialValues : tuple[] - List of tuples storing exponential amplitude and decay values. + resonanceValues : [][] + List of lists storing wavelength ranges for resonance values. + exponentialValues : []][] + List of lists storing exponential amplitude and decay values. normalisationCorrectionFactor : float Factor to multiply normalisation by prior to dividing into sample. fileSelfScattering : str Name of file containing scattering as a function of wavelength. - normaliseTo : int - Normalisation type required on the final merged DCS data. - 0 = nothing, 1 = ^2, 2 = + normaliseTo : NormalisationType + Normalisation type required on the final merged DCS data + nothing, ^2, maxRadFT : float Maximum radiues for Fourier transform. - outputUnits : int + outputUnits : OutputUnits Output units for final merged DCS, barns/atom/sr or cm^-1/sr powerForBroadening : float Broadening power @@ -93,16 +95,17 @@ class Sample: compensate for different attenuation in different containers. containers : Container[] List of Container objects attached to this sample. + yamlignore : str{} + Class attributes to ignore during yaml serialisation. + Methods ------- + pathName() + Converts the sample name into a path name. """ def __init__(self): """ Constructs all the necessary attributes for the Sample object. - - Parameters - ---------- - None """ self.name = "" self.periodNumber = 1 @@ -147,6 +150,13 @@ def __init__(self): } def pathName(self): + """ + Converts the sample name into a path name. + + Returns + ------- + str : path representation of sample. + """ return self.name.replace(" ", "_").translate( {ord(x): '' for x in r'/\!*~,&|[]'} ) + ".sample" @@ -155,14 +165,9 @@ def __str__(self): """ Returns the string representation of the Sample object. - Parameters - ---------- - None - Returns ------- - string : str - String representation of Sample. + str : String representation of Sample. """ nameLine = ( @@ -201,6 +206,7 @@ def __str__(self): ) if self.densityUnits == UnitsOfDensity.ATOMIC: + # Negative density refers to atomic density. density = self.density*-1 elif self.densityUnits == UnitsOfDensity.CHEMICAL: density = self.density @@ -271,6 +277,7 @@ def __str__(self): f' and attenuation coefficient [per \u212b]\n' ) + # Containers to write out. SAMPLE_CONTAINERS = ( "\n".join([str(x) for x in self.containers if not x.runAsSample]) if len(self.containers) > 0 diff --git a/gudpy/core/sample_background.py b/gudpy/core/sample_background.py index 4e454899e..6eb13db25 100644 --- a/gudpy/core/sample_background.py +++ b/gudpy/core/sample_background.py @@ -15,17 +15,15 @@ class SampleBackground: DataFiles object storing data files belonging to the container. samples : Sample[] List of Sample objects against the SampleBackground. - Methods - ------- + writeAllSamples : bool + Should all samples be used? + yamlignore : str{} + Class attributes to ignore during yaml serialisation. """ def __init__(self): """ Constructs all the necessary attributes for the SampleBackground object. - - Parameters - ---------- - None """ self.periodNumber = 1 self.dataFiles = DataFiles([], "SAMPLE BACKGROUND") @@ -41,26 +39,26 @@ def __str__(self): """ Returns the string representation of the SampleBackground object. - Parameters - ---------- - None - Returns ------- - string : str - String representation of SampleBackground. + str : String representation of SampleBackground. """ TAB = " " + + # Convert necessary containers to samples. CONV_SAMPLES = [ str(c.convertToSample()) for s in self.samples for c in s.containers if c.runAsSample ] + + # Determine sample to write. if self.writeAllSamples: samples = [str(x) for x in self.samples] else: samples = [str(x) for x in self.samples if x.runThisSample] + SAMPLES = "\n".join([*samples, *CONV_SAMPLES]) self.writeAllSamples = True diff --git a/gudpy/core/single_param_iterator.py b/gudpy/core/single_param_iterator.py index 63da54a31..adba26d83 100644 --- a/gudpy/core/single_param_iterator.py +++ b/gudpy/core/single_param_iterator.py @@ -18,6 +18,7 @@ class SingleParamIterator(): ---------- gudrunFile : GudrunFile Input GudrunFile that we will be using for iterating. + Methods ---------- performIteration(_n) @@ -26,7 +27,7 @@ class SingleParamIterator(): To be overriden by sub-classes. iterate(n) Perform n iterations of iterating by tweak factor. - organiseOutput + organiseOutput(n) To be overriden by sub-classes. """ def __init__(self, gudrunFile): @@ -89,18 +90,6 @@ def applyCoefficientToAttribute(self, object, coefficient): """ pass - def organiseOutput(self, n): - """ - Stub method to be overriden by sub-classes. - This method should organise the output of the iteration. - - Parameters - ---------- - n : int - Iteration no. - """ - pass - def iterate(self, n): """ This method is the core of the SingleParamIterator. @@ -119,3 +108,15 @@ def iterate(self, n): time.sleep(1) self.performIteration(i) self.organiseOutput(i) + + def organiseOutput(self, n): + """ + Stub method to be overriden by sub-classes. + This method should organise the output of the iteration. + + Parameters + ---------- + n : int + Iteration number. + """ + pass \ No newline at end of file diff --git a/gudpy/core/thickness_iterator.py b/gudpy/core/thickness_iterator.py index 3bf5a09a6..02d3a017f 100644 --- a/gudpy/core/thickness_iterator.py +++ b/gudpy/core/thickness_iterator.py @@ -14,12 +14,23 @@ class ThicknessIterator(SingleParamIterator): Methods ---------- - applyCoefficientToAttribute + applyCoefficientToAttribute(object, coefficient) Multiplies a sample's thicknesses by a given coefficient. - organiseOutput + organiseOutput(n) Organises the output of the iteration. """ def applyCoefficientToAttribute(self, object, coefficient): + """ + Updates the upstream and downstream thickness of the target object + by applying the given coefficient. + + Parameters + ---------- + object : Sample + Target object. + coefficient : float + Coefficient to use. + """ # Determine a new total thickness. totalThickness = object.upstreamThickness + object.downstreamThickness totalThickness *= coefficient @@ -28,4 +39,12 @@ def applyCoefficientToAttribute(self, object, coefficient): object.upstreamThickness = totalThickness / 2 def organiseOutput(self, n): + """ + Organises the output for the current iteration (n). + + Parameters + ---------- + n : int + Iteration number. + """ self.gudrunFile.iterativeOrganise(f"IterateByThickness_{n}") diff --git a/gudpy/core/tweak_factor_iterator.py b/gudpy/core/tweak_factor_iterator.py index d7d8a6421..35a161079 100644 --- a/gudpy/core/tweak_factor_iterator.py +++ b/gudpy/core/tweak_factor_iterator.py @@ -19,6 +19,7 @@ class TweakFactorIterator(): ---------- gudrunFile : GudrunFile Input GudrunFile that we will be using for iterating. + Methods ---------- performIteration(_n) diff --git a/gudpy/core/utils.py b/gudpy/core/utils.py index b28e29767..72ea85a13 100644 --- a/gudpy/core/utils.py +++ b/gudpy/core/utils.py @@ -5,6 +5,21 @@ def spacify(iterable, num_spaces=1): + """ + Spacifies an iterable. In effect, joins the iterable by + `num_spaces` whitespaces. + + Parameters + ---------- + iterable : list | tuple + Target iterable. + num_spaces : int, optional + Number of spaces to use. + + Returns + ------- + str : Joined iterable. + """ try: return (" " * num_spaces).join(iterable) except TypeError: @@ -12,19 +27,66 @@ def spacify(iterable, num_spaces=1): def numifyBool(boolean): + """ + Converts a boolean value to integer. + + Parameters + ---------- + boolean : bool + Boolean value to convert. + + Returns + ------- + int : integer representation of `boolean`. + """ return sum([boolean]) def boolifyNum(num): + """ + Converts an integer value to boolean. + + Parameters + ---------- + num : int + Integer value to convert. + + Returns + ------- + bool : boolean representation of `num`. + """ return not not num def firstword(string): - + """ + Returns the "first word" in a string. + + Parameters + ---------- + string : str + Target string. + + Returns + ------- + str : first word. + """ return string.split(" ")[0] def extract_ints_from_string(string): + """ + Casts chars to integers, until no more casting can be performed. + + Parameters + ---------- + string : str + Target string to extract ints from. + + Returns + ------- + int[] : extracted integers. + """ ret = [] for x in [y for y in string.split(" ") if y]: try: @@ -36,6 +98,18 @@ def extract_ints_from_string(string): def extract_floats_from_string(string): + """ + Casts chars to floats, until no more casting can be performed. + + Parameters + ---------- + string : str + Target string to extract floats from. + + Returns + ------- + float[] : extracted integers. + """ ret = [] for x in [y for y in string.split(" ") if y]: try: @@ -47,6 +121,18 @@ def extract_floats_from_string(string): def isfloat(string): + """ + Checks if a string is a float. + + Parameters + ---------- + string : str + Target string for conversion. + + Returns + ------- + bool : is string a float? + """ try: float(string) return True @@ -55,29 +141,105 @@ def isfloat(string): def isnumeric(string): + """ + Checks if a string is a number. + + Parameters + ---------- + string : str + Target string for conversion. + + Returns + ------- + bool : is string a number? + """ return isfloat(string) | string.isnumeric() def extract_nums_from_string(string): + """ + Casts chars to numbers, until no more casting can be performed. + + Parameters + ---------- + string : str + Target string to extract floats from. + + Returns + ------- + float / int[] | None: extracted numbers. + """ if string: ret = [x for x in string.split(" ") if isnumeric(x)] return [float(x) if '.' in x else int(x) for x in ret] def consume(iterable, n): - + """ + Consumes `n` values from an iterable by reference. + + Parameters + ---------- + iterable : Iterable + Target iterable to consume from. + n : int + Number of values to consume. + """ deque(iterable, maxlen=0) if not n else next(islice(iterable, n, n), None) def count_occurrences(substring, iterable): + """ + Counts the number of substrings in an iterable. + + Parameters + ---------- + substring : str + Substring to count. + iterable : Iterable + Target iterable to count substrings from. + + Returns + ------- + int : number of occurences of substring. + """ return sum(1 for string in iterable if substring in string) def iteristype(iter, type): + """ + Checks if all values in `iter` are of type `type`. + + Parameters + ---------- + iter : Iterable + Target iterable to check types. + type : Any + Type to ensrure. + + Returns + ------- + bool : are all elements of `iter` of type `type`? + """ return all(isinstance(x, type) for x in iter) def isin(iter1, iter2): + """ + Check if `iter1` is in `iter2`. + + Parameters + ---------- + iter1 : Iterable + First iterable. + iter2 : Iterable + Second iterable. + + Returns + ------- + bool : is `iter1` in `iter2`? + int : Index of `iter2` that `iter1` occurs in. + """ if isinstance(iter1, (list, tuple)): for i, line in enumerate(iter2): if all(word.lower() in str(line).lower() for word in iter1): @@ -91,18 +253,74 @@ def isin(iter1, iter2): def nthword(string, n): + """ + Returns the nth word of a given string. + + Parameters + ---------- + string : str + Target tring. + n : int + Word number to extract. + + Returns + ------- + str : nth word. + """ return string.split()[n] def nthint(string, n): + """ + Returns the nth int of a given string. + + Parameters + ---------- + string : str + Target tring. + n : int + Int number to extract. + + Returns + ------- + int : nth int. + """ return int(nthword(string, n)) def nthfloat(string, n): + """ + Returns the nth float of a given string. + + Parameters + ---------- + string : str + Target tring. + n : int + Float number to extract. + + Returns + ------- + float : nth float. + """ return float(nthword(string, n)) def firstNInts(string, n): + """ + Returns the fist n ints in a given string. + + Parameters + ---------- + string : str + Target string to extract from. + n : int + Number of ints to extract. + + Returns + ------- + int[] : extracted integers. + """ ints = [int(x) for x in string.split()[:n]] if len(ints) != n: raise ValueError(f"Could not find {n} ints in {string}") @@ -111,6 +329,20 @@ def firstNInts(string, n): def firstNFloats(string, n): + """ + Returns the fist n floats in a given string. + + Parameters + ---------- + string : str + Target string to extract from. + n : int + Number of floats to extract. + + Returns + ------- + float[] : extracted floats. + """ floats = [float(x) for x in string.split()[:n]] if len(floats) != n: raise ValueError(f"Could not find {n} floats in {string}") @@ -119,32 +351,76 @@ def firstNFloats(string, n): def bjoin(iterable, sep, lastsep=None, endsep='', sameseps=False, suffix=None): + """ + A better version of `join`. + + Parameters + ---------- + iterable : Iterable + Iterable to join. + sep : str + Separator to use when joining. + lastsep : None | str + Separator to use for final join, if any. + endsep : str + Separator to use at the end. + sameseps : bool + Should the same separator be used for the end separator? + suffix : None | str + Suffix to append, if any. + + Returns + ------- + str : Joined string. + """ + + # Cast to str. iterable = [ str(i) if not isinstance(i, (str, list, tuple)) else i for i in iterable ] + + # Spacify iterable = [ spacify(i, num_spaces=2) if isinstance(i, (list, tuple)) else i for i in iterable ] + # if no lastsep, then lastsep is sep. if not lastsep: lastsep = sep + # If sameseps, then endsep is seo. if sameseps: endsep = sep + # If iterable is empty, return empty string. if len(iterable) == 0: return "" + # If length of iterbale is one, simply append the sep. elif len(iterable) == 1: return (iterable[0]) + sep + # If suffix, append it. if suffix: iterable = [i + f" {suffix}" for i in iterable] + # Perform join. return sep.join(iterable[:-1]) + lastsep + iterable[-1] + endsep def resolve(*args): + """ + Resolve the absolute path of a list of paths. + + Parameters + ---------- + str[] : args + Path arguments to use. + + Returns + ------- + str : absolute path that is resolved. + """ relativePath = os.sep.join(args) topLevel = os.sep.join( os.path.realpath(__file__).split(os.sep)[:-3] @@ -153,11 +429,45 @@ def resolve(*args): def breplace(str, old, new): + """ + A better version of `replace`. + + Parameters + ---------- + str : str + Target string for replacement. + old : str + What is to be replaced + new : str + To be replaced with + + Returns + ------- + str : resultant string. + """ pattern = re.compile(old, re.IGNORECASE) return pattern.sub(new, str) def nthreplace(str, old, new, nth): + """ + Replace the `nth` instance of `old` in `str` with `new`. + + Parameters + ---------- + str : str + Target string for replacement. + old : str + What is to be replaced + new : str + To be replaced with + nth : int + Number instance of `old` to replace. + + Returns + ------- + str : resultant string. + """ tokens = str.split(old) if len(tokens) > nth: string = f'{old.join(tokens[:nth])}{new}{old.join(tokens[nth:])}' diff --git a/gudpy/core/wavelength_subtraction_iterator.py b/gudpy/core/wavelength_subtraction_iterator.py index 8296ee192..48fd9edfe 100644 --- a/gudpy/core/wavelength_subtraction_iterator.py +++ b/gudpy/core/wavelength_subtraction_iterator.py @@ -32,20 +32,25 @@ class WavelengthSubtractionIterator(): QStep : float Step size for corrections on Q scale. Stored, as we switch between scales this data needs to be held. + Methods ---------- - enableLogarithmicBinning + enableLogarithmicBinning() Enables logarithmic binning - disableLogarithmicBinning + disableLogarithmicBinning() Disables logarithmic binning - collectQRange + collectQRange() Collects QMax, QMin and QStep, and stores them as attributes. - applyQRange + applyQRange() Applies the Q range and step collected to the X-scale. - applyWavelengthRanges + applyWavelengthRanges() Apply the wavelength ranges of the instrument to the X-scale. - zeroTopHatWidths + zeroTopHatWidths() Set width of top hat functions for FT to zero, for each sample. + resetTopHatWidths() + Reset the width of top hat functions to their previous values. + collectTopHatWidths() + Collect witdth of top hat functions for each sample. setSelfScatteringFiles(scale) Alters file extensions of self scattering files, to the relevant extension for the scale inputted. diff --git a/gudpy/gui/widgets/charts/beam_plot.py b/gudpy/gui/widgets/charts/beam_plot.py index e797ebb3a..4ef641b53 100644 --- a/gudpy/gui/widgets/charts/beam_plot.py +++ b/gudpy/gui/widgets/charts/beam_plot.py @@ -3,28 +3,65 @@ class BeamChart(QChart): + """ + Plots the beam profile. Inherits QChart. + + Methods + ------- + setBeam(beam) + Sets the beam. + plot() + Updates the plot. + """ def __init__(self): - super().__init__() + """ + Constructs all the necessary attributes for the BeamChart object. + """ + super(BeamChart, self).__init__() + + # Show the chart legend. self.legend().setVisible(False) + + # Initialise the series. self.areaSeries = QAreaSeries(self) self.addSeries(self.areaSeries) def setBeam(self, beam): + """ + Sets the beam object. + + Parameters + ---------- + beam : Beam + Beam to set. + """ self.beam = beam def plot(self): + """ + Updates the plot using the current beam. + """ + # Upper series of the area series. self.upperSeries = QLineSeries(self) + + # Intensity values. intensities = [ QPointF(float(x+1), float(y)) for x, y in enumerate(self.beam.beamProfileValues) ] self.upperSeries.append(intensities) + + # Lower series of the area series. self.lowerSeries = QLineSeries(self) self.lowerSeries.append([QPoint(p.x(), 0) for p in intensities]) + + # Construct area series. self.areaSeries.setUpperSeries(self.upperSeries) self.areaSeries.setLowerSeries(self.lowerSeries) + + # Axis and titles. self.createDefaultAxes() self.axisY().setTitleText("Intensity") self.axisY().setRange(0, 2) diff --git a/gudpy/gui/widgets/charts/chart.py b/gudpy/gui/widgets/charts/chart.py index 4bffb58fb..c3e2d7b6b 100644 --- a/gudpy/gui/widgets/charts/chart.py +++ b/gudpy/gui/widgets/charts/chart.py @@ -11,10 +11,71 @@ class GudPyChart(QChart): + """ + Core plotting functionality of GudPy. Inherits QChart. + This is used for embedded plots throughout the GUI. + + Methods + ------- + connectMarkers() + Connects markers. + disconnectMarkers() + Disconnects markers. + handleMarkerClicked() + Handles a marker being clicked. + updateMarkerOpacity(marker) + Updates the opacity of the given marker. + addSamples(samples) + Adds samples to the plot. + AddSample(sample) + Adds a single sample to the plot. + removeAllSeries() + Removes all series from the plot. + plot(plotMode=None) + Plots the chart using plotMode. + toggleVisible(seriesType) + Toggles visibility of series of a specified type. + isVisible(seriesType) + Returns whether a given type of series is visible or not. + isSampleVisible(sample) + Returns whether a given sample is visible or not. + toggleSampleVisibility(state, sample) + Toggles the visibility of a given sample. + toggleLogarithmicAxis(axis) + Toggles logarithmic mode of `axis`. + + Attributes + ---------- + inputDir : str + Directory of input file. + samples : Sample[] + List of Sample objects being plotted. + configs : {} + Map of Samples to SamplePlotConfigs. + logarithmicA : bool + All axes logarithmic? + logarithmicX : bool + X-Axis logarithmic? + logarithmicY : bool + Y-Axis logarithmic? + plotMode : PlotModes + Mode for plotting. + label : QGraphicsTextItem + Label for mouse coordinates. + """ def __init__(self, gudrunFile, parent=None): + """ + Constructs all the necessary attributes for the GudPyChart object. - super().__init__(parent) + Parameters + ---------- + gudrunFile : GudrunFile + Reference GudrunFile object to create plot from. + parent : Any | None, optional + Parent object of chart. + """ + super(GudPyChart, self).__init__(parent) self.inputDir = gudrunFile.instrument.GudrunInputFileDir self.legend().setMarkerShape(QLegend.MarkerShapeFromSeries) @@ -32,13 +93,20 @@ def __init__(self, gudrunFile, parent=None): self.plotMode = PlotModes.SF_MINT01 + # Set up label for mouse coordinates. self.label = QGraphicsTextItem("x=,y=", self) def connectMarkers(self): + """ + Connects markers in the legend to the `handleMarkerClicked` slot. + """ for marker in self.legend().markers(): marker.clicked.connect(self.handleMarkerClicked) def disconnectMarkers(self): + """ + Disconnects markers in the legend from the `handleMarkerClicked` slot. + """ for marker in self.legend().markers(): try: marker.clicked.disconnect(self.handleMarkerClicked) @@ -46,15 +114,39 @@ def disconnectMarkers(self): continue def handleMarkerClicked(self): + """ + Slot for handling markers in the legend being clicked. + Alters visibility of corresponding series, and opacity of + marker. + """ + # Get the sender object, i.e marker. marker = QObject.sender(self) + # Double check type of marker. if marker.type() == QLegendMarker.LegendMarkerTypeXY: + # Toggle the visibility of series. marker.series().setVisible(not marker.series().isVisible()) + # Ensure marker remains visible. marker.setVisible(True) + # Update the opacity of the marker. self.updateMarkerOpacity(marker) def updateMarkerOpacity(self, marker): + """ + Updates the opacity of a given marker. + This opacity relates to the visibility of the + corresponding series. + + Parameters + ---------- + marker : QLegendMarker + Marker to alter opacity. + """ + + # Determine alpha from series visibility. alpha = 1.0 if marker.series().isVisible() else 0.5 + # Update the opacities! + brush = marker.labelBrush() color = brush.color() color.setAlphaF(alpha) @@ -62,7 +154,7 @@ def updateMarkerOpacity(self, marker): marker.setLabelBrush(brush) brush = marker.brush() - color = brush.color() + color = brush.color()# color.setAlphaF(alpha) brush.setColor(color) marker.setBrush(brush) @@ -74,41 +166,82 @@ def updateMarkerOpacity(self, marker): marker.setPen(pen) def addSamples(self, samples): + """ + Adds a collection of samples to the plot. + + Parameters + ---------- + samples : Sample[] + List of Sample objects to add. + """ for sample in samples: self.addSample(sample) def addSample(self, sample): + """ + Adds a samples to the plot. + + Parameters + ---------- + sample : Sample + Sample object to add. + """ self.samples.append(sample) def removeAllSeries(self): + """ + Removes all series from the plot. + """ for series in self.series(): self.removeSeries(series) def plot(self, plotMode=None): + """ + Core functionality of the class. + Plots samples using the given plotting mode. + + Parameters + ---------- + plotMode : PlotModes | None + Plotting mode to use. + """ + + # If a plot mode is given, then update the attribute. if plotMode: self.plotMode = plotMode + # Remove all series from the plot. self.removeAllSeries() + + # Remove all axes.# for axis in self.axes(): self.removeAxis(axis) + # Determine whether to plot DCS level or not. plotsDCS = self.plotMode in [ PlotModes.SF, PlotModes.SF_CANS, PlotModes.SF_MDCS01, PlotModes.SF_MDCS01_CANS ] + + # Determine whether to plot samples or not. plotsSamples = self.plotMode in [ PlotModes.SF, PlotModes.SF_MDCS01, PlotModes.SF_MINT01, PlotModes.RDF ] + + # Determine whether to plot containers or not. plotsContainers = self.plotMode in [ PlotModes.SF_CANS, PlotModes.SF_MINT01_CANS, PlotModes.SF_MDCS01_CANS, PlotModes.RDF_CANS ] + + # Iterate samples, adding them to the series. for sample in self.samples: + # Determine minima. if self.series(): pointsX = [ p.x() @@ -125,6 +258,7 @@ def plot(self, plotMode=None): else: minX = 0 minY = 0 + # If plotting logarithmically, then apply offset. if self.logarithmicX or self.logarithmicA: offsetX = 1 + minX else: @@ -133,12 +267,16 @@ def plot(self, plotMode=None): offsetY = 1 + minY else: offsetY = 0 + + # Construct plotting configuration for the sample. plotConfig = SamplePlotConfig( sample, self.inputDir, offsetX, offsetY, self ) + + # Maintain series visibility. visible = True if sample in self.configs.keys(): if not any( @@ -148,20 +286,29 @@ def plot(self, plotMode=None): ] ): visible = False + + # Add it to the map of configurations. self.configs[sample] = plotConfig + + # Iterate series in the configuration. for series in plotConfig.plotData(self.plotMode): if series: + # Add the relevant series to the plot. if isinstance(sample, Sample) and plotsSamples: self.addSeries(series) elif isinstance(sample, Container) and plotsContainers: self.addSeries(series) + + # If the series is empty or was not visible before, hide it. if not series.points() or not visible: series.hide() + # Plot DCS level if necessary. if ( len(sample.dataFiles) and plotsDCS and plotConfig.mdcs01Series ): + # Use a dashed line. pen = QPen(plotConfig.dcsSeries.pen()) pen.setStyle(Qt.PenStyle.DashLine) pen.setWidth(2) @@ -183,33 +330,52 @@ def plot(self, plotMode=None): ]: XLabel = "r, \u212b" YLabel = "G(r)" + + # As long as we have series in the plot, update the axes. if self.series(): + + # Determine limits automatically. self.createDefaultAxes() + + # Update the axes labels. self.axisX().setTitleText(XLabel) self.axisY().setTitleText(YLabel) + # If X-Axis needs to be logarithmic.. if self.logarithmicX or self.logarithmicA: + + # Swap out the current X-Axis for a logarithmic axis. self.removeAxis(self.axisX()) self.addAxis(self.logarithmicXAxis, Qt.AlignBottom) + + # Attach the series to the new X-Axis. for series in self.series(): series.attachAxis(self.logarithmicXAxis) + # If Y-Axis needs to be logarithmic.. if self.logarithmicY or self.logarithmicA: - self.addAxis(self.logarithmicYAxis, Qt.AlignLeft) + + # Swap out the current Y-Axis for a logarithmic axis. self.removeAxis(self.axisY()) + self.addAxis(self.logarithmicYAxis, Qt.AlignLeft) + + # Attach the series to the new Y-Axis. for series in self.series(): series.attachAxis(self.logarithmicYAxis) + # Connect the legend markers. self.connectMarkers() def toggleVisible(self, seriesType): """ - Toggles visibility of a given series, or set of series'. + Toggles visibility of series of a specified type. + Parameters ---------- - series : dict | QLineSeries - Series(') to toggle visibility on. + seriesType : SeriesTypes + Target type to toggle visibility of. """ + targetAttr = ( { SeriesTypes.MINT01: "mint01Series", @@ -220,6 +386,7 @@ def toggleVisible(self, seriesType): }[seriesType] ) + # Update visibility of relevant series. for sample in self.samples: if self.configs[sample].__dict__[targetAttr]: self.configs[sample].__dict__[targetAttr].setVisible( @@ -228,14 +395,18 @@ def toggleVisible(self, seriesType): def isVisible(self, seriesType): """ - Method for determining if a given series or set of series' is visible. + Returns whether a given type of series is visible or not. + Parameters ---------- - series : dict | QLineSeries - Series(') to check visibility of. + seriesType : SeriesTypes + Target type to check visibility of. + + Returns + ------- + bool : are any series of the specified type visible? """ - # If it's a dict, assume that if any value (series) - # is visible, then they all should be. + targetAttr = ( { SeriesTypes.MINT01: "mint01Series", @@ -246,6 +417,7 @@ def isVisible(self, seriesType): }[seriesType] ) + # Determine if any of the series of the specified type are visible. return any( [ self.configs[sample].__dict__[targetAttr].isVisible() @@ -255,14 +427,36 @@ def isVisible(self, seriesType): ) def isSampleVisible(self, sample): + """ + Determine if given Sample is visible in the plot. + + Parameters + ---------- + sample : Sample + Sample object to check series visibility of. + + Returns + ------- + bool : Are any of the sample's series visible? + """ + # If only plotting mint01 series, then just check that. if self.plotMode in [PlotModes.SF_MINT01, PlotModes.SF_MINT01_CANS]: return self.configs[sample].mint01Series.isVisible() + # If only plotting mdcs01 series, then just check that. elif self.plotMode in [PlotModes.SF_MDCS01, PlotModes.SF_MDCS01_CANS]: return ( self.configs[sample].mdcs01Series.isVisible() | self.configs[sample].dcsSeries.isVisible() ) + # If checking SF, then check for both mint01 and mdcs01 visibility. + elif self.plotMode in [PlotModes.SF, PlotModes.SF_CANS]: + return ( + self.configs[sample].mint01Series.isVisible() + | self.configs[sample].mdcs01Serie.isVisible() + | self.configs[sample].dcsSeries.isVisible() + ) + # If checking RDF, then check for both mdor01 and mgor01 visibility. elif self.plotMode in [PlotModes.RDF, PlotModes.RDF_CANS]: return ( self.configs[sample].mdor01Series.isVisible() @@ -270,6 +464,16 @@ def isSampleVisible(self, sample): ) def toggleSampleVisibility(self, state, sample): + """ + Toggles the visibility of a given sample. + + Parameters + ---------- + state : bool + Should sample be visible or not? + sample : Sample + Sample object to alter series visibility of. + """ self.configs[sample].mint01Series.setVisible(state) self.configs[sample].mdcs01Series.setVisible(state) self.configs[sample].dcsSeries.setVisible(state) @@ -277,6 +481,14 @@ def toggleSampleVisibility(self, state, sample): self.configs[sample].mgor01Series.setVisible(state) def toggleLogarithmicAxis(self, axis): + """ + Toggles using logarithmic axes or not. + + Parameters + ---------- + axis : Axes + Axes to be toggled. + """ if axis == Axes.A: self.logarithmicA = not self.logarithmicA self.logarithmicX = self.logarithmicA @@ -288,4 +500,5 @@ def toggleLogarithmicAxis(self, axis): self.logarithmicY = not self.logarithmicY self.logarithmicA = self.logarithmicX and self.logarithmicY + # Re-plot. self.plot() diff --git a/gudpy/gui/widgets/charts/chartview.py b/gudpy/gui/widgets/charts/chartview.py index 404e89f21..c043e82eb 100644 --- a/gudpy/gui/widgets/charts/chartview.py +++ b/gudpy/gui/widgets/charts/chartview.py @@ -19,24 +19,42 @@ class GudPyChartView(QChartView): Class to represent a GudPyChartView. Inherits QChartView. ... + Attributes ---------- chart : GudPyChart Chart to be shown in the view. + clipboard : QClipboard + Clipboard for copying. + previousPos : QPoint + Previous mouse position. + Methods ------- wheelEvent(event): Event handler for using the scroll wheel. - toggleLogarithmicAxes(): - Toggles logarithmic axes in the chart. - contextMenuEvent(event): - Creates context menu. + mouseMoveEvent(event) + Event handler for moving the mouse. + mousePressEvent(event) + Event handler for pressing the mouse buttons. + copyPlot() + Copies the current plot to the clipboard. + mouseReleaseEvent(event) + Event handler for releasing the mouse button. keyPressEvent(event): Handles key presses. enterEvent(event): Handles the mouse entering the chart view. leaveEvent(event): Handles the mouse leaving the chart view. + contextMenuEvent(event): + Creates context menu. + toggleLogarithmicAxes(): + Toggles logarithmic axes in the chart. + setChart(chart) + Sets the chart in the view. + resizeEvent(event) + Event handler for resizing the plot. """ def __init__(self, parent): """ @@ -60,6 +78,8 @@ def __init__(self, parent): # Enable Antialiasing. self.setRenderHint(QPainter.Antialiasing) + + # Initialise clipboard. self.clipboard = QClipboard(self.parent()) self.previousPos = 0 @@ -73,6 +93,7 @@ def wheelEvent(self, event): event : QWheelEvent Event that triggered the function call. """ + # Decide on the zoom factor. # If y > 0, zoom in, if y < 0 zoom out. zoomFactor = 2.0 if event.angleDelta().y() > 0 else 0.5 @@ -97,22 +118,45 @@ def wheelEvent(self, event): self.chart().scroll(delta.x(), -delta.y()) def mouseMoveEvent(self, event): + """ + Event handler called when the mouse is moved. + This event is overridden for tracking the mouse coordinates + and translating the view. + Parameters + ---------- + event : QMouseEvent + Event that triggered the function call. + """ + + # Ensure correct event type is caught. if isinstance(event, QMouseEvent): + + # If the middle mouse button is held, then translate the view. if event.buttons() & Qt.MouseButton.MiddleButton: + # Determine offset. if self.previousPos: offset = event.pos() - self.previousPos else: offset = event.pos() + + # Zoom a very small amount self.chart().zoom(1 + 0.00000001) + + # Scroll the view. self.chart().scroll(-offset.x(), offset.y()) self.previousPos = event.pos() event.accept() else: if type(self.chart()) == GudPyChart: + # If the mouse is within the plot area. if self.chart().plotArea().contains(event.pos()): + + # Determine the current mouse position, in axes coordinates. pos = self.chart().mapToValue(event.pos()) + + # Set the mouse coordinate label. self.chart().label.setPlainText( f"x={round(pos.x(), 4)}, y={round(pos.y(), 4)}" ) @@ -123,7 +167,17 @@ def mouseMoveEvent(self, event): return super().mouseMoveEvent(event) def mousePressEvent(self, event): + """ + Event handler called when the mouse is pressed. + This event is overridden for rubber band zoom / translation. + Parameters + ---------- + event : QMouseEvent + Event that triggered the function call. + """ if isinstance(event, QMouseEvent): + # If middle mouse was pressed, set the previous position, + # for translating. if event.button() == Qt.MouseButton.MiddleButton: self.previousPos = event.pos() elif event.button() == Qt.MouseButton.LeftButton: @@ -134,10 +188,27 @@ def mousePressEvent(self, event): return super().mousePressEvent(event) def copyPlot(self): + """ + Copies the current plot to the clipboard. + """ + # Grab a pixmap from the current view. pixMap = self.grab() + # Set the pixmap in the clipboard. + # This allows it to be pasted. self.clipboard.setPixmap(pixMap) def mouseReleaseEvent(self, event): + """ + Event handler for releasing the mouse button. + This is overriden to support rubber band zoom. + + Parameters + ---------- + event : QMouseEvent + Event that triggered the function call. + """ + + # Ensure correct type of event was caught. if isinstance(event, QMouseEvent): if event.button() == Qt.MouseButton.RightButton: event.accept() @@ -169,8 +240,14 @@ def keyPressEvent(self, event): """ Handles key presses. Used for implementing hotkeys / shortcuts. + + Parameters + ---------- + event : QKeyEvent + Event that triggered the function call. """ modifiers = QApplication.keyboardModifiers() + # 'Ctrl+C' refers to copying. if event.key() == Qt.Key_C and modifiers == Qt.ControlModifier: self.copyPlot() # 'L/l' refers to logarithms. @@ -194,8 +271,12 @@ def enterEvent(self, event): """ Handles the mouse entering the chart view. Gives focus to the chart view. - """ + Parameters + ---------- + event : QMouseEvent + Event that triggered the function call. + """ # Acquire focus. self.setFocus(Qt.OtherFocusReason) return super().enterEvent(event) @@ -204,6 +285,11 @@ def leaveEvent(self, event): """ Handles the mouse leaving the chart view. Gives focus back to the parent. + + Parameters + ---------- + event : QMouseEvent + Event that triggered the function call. """ # Relinquish focus. self.parent().setFocus(Qt.OtherFocusReason) @@ -213,6 +299,7 @@ def contextMenuEvent(self, event): """ Creates context menu, so that on right clicking the chartview, the user is able to perform actions. + Parameters ---------- event : QMouseEvent @@ -221,12 +308,16 @@ def contextMenuEvent(self, event): if isinstance(event, QContextMenuEvent): self.menu = QMenu(self) actionMap = {} + + # If no chart is initialised, then don't add any actions. if self.chart(): + # Action for resetting the view. resetAction = QAction("Reset View", self.menu) resetAction.triggered.connect(self.chart().zoomReset) self.menu.addAction(resetAction) + # Actions for toggling logarithmic axes. toggleLogarithmicMenu = QMenu(self.menu) toggleLogarithmicMenu.setTitle("Toggle logarithmic axes") @@ -270,6 +361,7 @@ def contextMenuEvent(self, event): self.menu.addMenu(toggleLogarithmicMenu) + # Actions specific to SF_MFCS01 / SF_MDCS01_CANS if self.chart().plotMode in [ PlotModes.SF_MDCS01, PlotModes.SF_MDCS01_CANS ]: @@ -284,6 +376,7 @@ def contextMenuEvent(self, event): ) ) self.menu.addAction(showDCSLevelAction) + # Actions specific to RDF / RDF_CANS. elif ( self.chart().plotMode in [ @@ -314,7 +407,11 @@ def contextMenuEvent(self, event): ) ) self.menu.addAction(showMgor01Action) + + # Ensure at least a single Sample is present in the chart. if len(self.chart().samples) > 1: + + # Actions for showing / hiding samples. showMenu = QMenu(self.menu) showMenu.setTitle("Show..") if self.chart().plotMode in [ @@ -341,6 +438,7 @@ def contextMenuEvent(self, event): actionMap[action] = sample self.menu.addMenu(showMenu) + # Action for copying plots. copyAction = QAction("Copy plot", self.menu) copyAction.triggered.connect(self.copyPlot) self.menu.addAction(copyAction) @@ -356,10 +454,25 @@ def contextMenuEvent(self, event): def toggleLogarithmicAxes(self, axis): """ Toggles logarithmic axes in the chart. + + Parameters + ---------- + axis : Axes + Target Axes. """ self.chart().toggleLogarithmicAxis(axis) def setChart(self, chart): + """ + Sets the chart in the view. + + Parameters + ---------- + chart : QChart | GudPyChart + Chart to set. + """ + # If it's a GudPyChart, then set the position of + # the mouse coordinates label. if type(chart) == GudPyChart: chart.label.setPos( self.mapToScene( @@ -370,6 +483,14 @@ def setChart(self, chart): return super().setChart(chart) def resizeEvent(self, event): + """ + Handles resizing of the chart view. + + Parameters + ---------- + event : QResizeEvent + Event that triggered the function call. + """ if type(self.chart()) == GudPyChart: self.chart().label.setPos( self.mapToScene(25, self.sceneRect().height()-50) diff --git a/gudpy/gui/widgets/charts/enums.py b/gudpy/gui/widgets/charts/enums.py index 117185992..b23bfdf5f 100644 --- a/gudpy/gui/widgets/charts/enums.py +++ b/gudpy/gui/widgets/charts/enums.py @@ -3,6 +3,20 @@ def enumFromDict(clsname, _dict): + """ + Creates an instance of `Enum` with name `clsname` from `_dict`. + + Parameters + ---------- + clsname : str + Resultant class name. + _dict : dict + Mapping from enum value to [display name, access name]. + + Returns + ------- + Enum : resultant Enum. + """ return Enum( value=clsname, # Cartesian product of all keys and values. @@ -48,11 +62,16 @@ def enumFromDict(clsname, _dict): ] } - +""" +Enumerates plot modes. +""" PlotModes = enumFromDict( "PlotModes", PLOT_MODES ) +""" +Dictionary describing splitting modes. +""" SPLIT_PLOTS = { PlotModes.SF_MINT01_RDF: (PlotModes.SF_MINT01, PlotModes.RDF), PlotModes.SF_MDCS01_RDF: (PlotModes.SF_MDCS01, PlotModes.RDF), @@ -66,7 +85,9 @@ def enumFromDict(clsname, _dict): PlotModes.SF_RDF_CANS: (PlotModes.SF_CANS, PlotModes.RDF_CANS) } - +""" +Enumerates series types. +""" class SeriesTypes(Enum): MINT01 = 0 MDCS01 = 1 @@ -74,7 +95,9 @@ class SeriesTypes(Enum): MDOR01 = 3 DCSLEVEL = 4 - +""" +Enumerates axis selection. +""" class Axes(Enum): X = 0 Y = 1 diff --git a/gudpy/gui/widgets/charts/sample_plot_config.py b/gudpy/gui/widgets/charts/sample_plot_config.py index 821312ebb..39d52d5bf 100644 --- a/gudpy/gui/widgets/charts/sample_plot_config.py +++ b/gudpy/gui/widgets/charts/sample_plot_config.py @@ -9,16 +9,83 @@ class SamplePlotConfig(): + """ + Class for managing configurations of sample plots. + This is used to determine which datasets etc, pertaining to a specific sample, + should be shown. + + ... + + Attributes + ---------- + sample : Sample + Reference Sample object. + inputDir : str + Input file directory. + parent : Any + Parent object. + + Methods + ------- + constructDataSets(offsetX, offsetY) + Loads datasets and constructs series. + series() + Returns all of the series. + SF() + Returns series that should be shown by `SF`. + SF_MINT01() + Returns series that should be shown by `SF_MINT01`. + SF_MDCS01() + Returns series that should be shown by `SF_MDCS01`. + RDF() + Returns series that should be shown by `RDF`. + plotData(plotMode) + Returns series that should be plotted by a given plotMode. + """ def __init__(self, sample, inputDir, offsetX, offsetY, parent): + """ + Constructs all the necessary attributes for the SamplePlotConfig object. + + Parameters + ---------- + sample : Sample + Reference Sample object. + inputDir : str + Input file directory. + offsetX : float + X-Offset for data. + offsetY : float + Y-Offset for data. + parent : Any + Parent object. + """ self.sample = sample self.inputDir = inputDir self.parent = parent + + # Construct the datasets. self.constructDataSets(offsetX, offsetY) def constructDataSets(self, offsetX, offsetY): + """ + This is the core function of the configuration, it is called every time + a configuration is initialised. + Reads data in from the input directory, and then constructs the relevant + data sets from that. + + Parameters + ---------- + offsetX : float + X-Offset for data. + offsetY : float + Y-Offset for data. + """ + + # Ensure that there are actually some data files. if len(self.sample.dataFiles): + # Base file path. baseFile = self.sample.dataFiles[0] ext = os.path.splitext(self.sample.dataFiles[0])[-1] @@ -113,6 +180,13 @@ def constructDataSets(self, offsetX, offsetY): # return all series def series(self): + """ + Returns all of the series. + + Returns + ------- + QLineSeries[] : series + """ return [ self.mint01Series, self.mdcs01Series, @@ -122,6 +196,13 @@ def series(self): ] def SF(self): + """ + Returns series that should be shown by `SF`. + + Returns + ------- + QLineSeries[] : series + """ return [ self.mint01Series, self.mdcs01Series, @@ -129,23 +210,51 @@ def SF(self): ] def SF_MINT01(self): + """ + Returns series that should be shown by `SF_MINT01`. + + Returns + ------- + QLineSeries[] : series + """ return [ self.mint01Series ] def SF_MDCS01(self): + """ + Returns series that should be shown by `SF_MDCS01`. + + Returns + ------- + QLineSeries[] : series + """ return [ self.mdcs01Series, self.dcsSeries ] def RDF(self): + """ + Returns series that should be shown by `RDF`. + + Returns + ------- + QLineSeries[] : series + """ return [ self.mdor01Series, self.mgor01Series ] def plotData(self, plotMode): + """ + Returns series that should be plotted by a given plotMode. + + Returns + ------- + QLineSeries[] : series + """ if len(self.sample.dataFiles): return { PlotModes.SF: self.SF, diff --git a/gudpy/gui/widgets/charts/sample_plot_data.py b/gudpy/gui/widgets/charts/sample_plot_data.py index d390baec0..776ac04d8 100644 --- a/gudpy/gui/widgets/charts/sample_plot_data.py +++ b/gudpy/gui/widgets/charts/sample_plot_data.py @@ -6,21 +6,104 @@ class Point(): + """ + Wrapper class for representing a point, with error. + + ... + + Attributes + ---------- + x : int | float + X value. + y : int | float + Y value. + err : int | float + Error. + + Methods + ------- + toQPointF() + Returns the internal point as a QPointF. + toQPoint() + Returns the internal point as a QPoint. + """ + def __init__(self, x, y, err): + """ + Constructs all the necessary attributes for the Point object. + + Parameters + ---------- + x : int | float + X value. + y : int | float + Y value. + err : int | float + Error. + """ self.x = x self.y = y self.err = err def toQPointF(self): + """ + Returns the internal point as a QPointF. + + Returns + ------- + QPointF : casted point. + """ return QPointF(self.x, self.y) def toQPoint(self): + """ + Returns the internal point as a QPoint. + + Returns + ------- + QPoint : casted point. + """ return QPoint(self.x, self.y) class GudPyPlot(): - # mint01 / mdcs01 / mdor01 / mgor01 / dcs + """ + Class for wrapping datasets as Qt-style plots. + In effect, this provides an interface between the data + and the GudPy plotting functionality. + + ... + + Attributes + ---------- + path : str + Path to dataset. + dataSet : None | Point[] + Internal dataset. + + Methods + ------- + constructDataSet(path) + Reads a dataset from a path, and constructs a plottable dataset. + toQPointList() + Casts the dataset to a list of QPoints. + toQPointFList() + Casts the dataset to a list of QPointFs. + toLineSeries(parent, offsetX, offsetY) + Constructs a line series from the dataset. + """ + def __init__(self, path, exists): + """ + Constructs all the necessary attributes for the GudPyPlot object. + + Parameters + ---------- + path : str + Path to dataset. + exists : bool + Whether the path actually exists or not. + """ if not exists: self.dataSet = None else: @@ -28,6 +111,20 @@ def __init__(self, path, exists): @abstractmethod def constructDataSet(self, path): + """ + Reads a dataset from a path and then constructs a plottable dataset. + This is always called when initialising a GudPyPlot object, + as long as the path exists. + + Parameters + ---------- + path : str + Path to dataset. + + Returns + ------- + dataSet : QPoint[] + """ dataSet = [] with open(path, "r", encoding="utf-8") as fp: for dataLine in fp.readlines(): @@ -36,67 +133,243 @@ def constructDataSet(self, path): if dataLine[0] == "#": continue + # Extract x, y, error x, y, err, *__ = [float(n) for n in dataLine.split()] + + # Cast to point and append to dataset. dataSet.append(Point(x, y, err)) + return dataSet def toQPointList(self): + """ + Casts the dataset to a list of QPoints. + + Returns + ------- + QPoint[] : casted dataset. + """ return [x.toQPoint() for x in self.dataSet] if self.dataSet else None def toQPointFList(self): + """ + Casts the dataset to a list of QPointFs. + + Returns + ------- + QPointF[] : casted dataset. + """ return [x.toQPointF() for x in self.dataSet] if self.dataSet else None def toLineSeries(self, parent, offsetX, offsetY): + """ + Constructs a line series from the dataset. + + Parameters + ---------- + parent : Any + Parent object for the resultant line series. + offsetX : float + Offset for X values. + offsetY : float + Offset for Y values. + + Returns + ------- + QLineSeries : constructed line series. + """ + # Create line series. self.series = QLineSeries(parent) + + # Cast points to QPointF points = self.toQPointFList() if points: + # Apply offset to points. points = [ QPointF(p.x() + offsetX, p.y() + offsetY) for p in points ] + # Add points to series. self.series.append(points) return self.series class Mint01Plot(GudPyPlot): + """ + Class for representing a Mint01 plot. + Inherits GudPyPlot. + + ... + + Attributes + ---------- + XLabel : str + Label for X-Axis. + YLabel : str + Label for Y-Axis. + """ def __init__(self, path, exists): + """ + Constructs all the necessary attributes for the Mint01Plot object. + + Parameters + ---------- + path : str + Path to dataset. + exists : bool + Whether the path actually exists or not. + """ self.path = path + + # Set labels. self.XLabel = "Q, 1\u212b" self.YLabel = "DCS, barns/sr/atom" - super().__init__(path, exists) + super(Mint01Plot, self).__init__(path, exists) class Mdcs01Plot(GudPyPlot): + """ + Class for representing a Mdcs01 plot. + Inherits GudPyPlot. + + ... + + Attributes + ---------- + XLabel : str + Label for X-Axis. + YLabel : str + Label for Y-Axis. + """ def __init__(self, path, exists): + """ + Constructs all the necessary attributes for the Mdcs01Plot object. + + Parameters + ---------- + path : str + Path to dataset. + exists : bool + Whether the path actually exists or not. + """ self.path = path + + # Set labels. self.XLabel = "Q, 1\u212b" self.YLabel = "DCS, barns/sr/atom" - super().__init__(path, exists) + super(Mdcs01Plot, self).__init__(path, exists) class Mdor01Plot(GudPyPlot): + """ + Class for representing a Mdor01 plot. + Inherits GudPyPlot. + + ... + + Attributes + ---------- + XLabel : str + Label for X-Axis. + YLabel : str + Label for Y-Axis. + """ def __init__(self, path, exists): + """ + Constructs all the necessary attributes for the Mdor01Plot object. + + Parameters + ---------- + path : str + Path to dataset. + exists : bool + Whether the path actually exists or not. + """ self.path = path + + # Set labels. self.XLabel = "r, \u212b" self.YLabel = "G(r)" - super().__init__(path, exists) + super(Mdor01Plot, self).__init__(path, exists) class Mgor01Plot(GudPyPlot): + """ + Class for representing a Mgor01 plot. + Inherits GudPyPlot. + + ... + + Attributes + ---------- + XLabel : str + Label for X-Axis. + YLabel : str + Label for Y-Axis. + """ def __init__(self, path, exists): + """ + Constructs all the necessary attributes for the Mgor01Plot object. + + Parameters + ---------- + path : str + Path to dataset. + exists : bool + Whether the path actually exists or not. + """ self.path = path + + # Set labels. self.XLabel = "r, \u212b" self.YLabel = "G(r)" - super().__init__(path, exists) + super(Mgor01Plot, self).__init__(path, exists) class DCSLevel: + """ + Class for wrapping the DCS level into a Qt-style plot. + Provides an interface between the GudFile and the GudPy + plotting functionality. + + ... + + Attributes + ---------- + path : str + Path to dataset. + dcsLevel : float + DCS level. + data : QPointF[] + Extended DCS level. + visible : bool + Should this be visible? + + Methods + ------- + extractDCSLevel(path) + Extract the DCS level from the given path. + extend(xAxis) + Extrapolate the DCS level, to extend the entire xAxis. + toLineSeries(parent) + Construct a line series from the data. + """ def __init__(self, path, exists): + """ + Constructs all the necessary attributes for the GudPyPlot object. + + Parameters + ---------- + path : str + Path to dataset. + exists : bool + Whether the path actually exists or not. + """ self.path = path if not exists: self.dcsLevel = None @@ -108,14 +381,46 @@ def __init__(self, path, exists): @abstractmethod def extractDCSLevel(self, path): + """ + Reads the given path in as a GudFile, and extracts the DCS level. + + Parameters + ---------- + path : str + Path to read from. + + Returns + ------- + float : expected DCS level. + """ gudFile = GudFile(path) return gudFile.expectedDCS def extend(self, xAxis): + """ + Extrapolate the DCS level, to extend the entire xAxis. + + Parameters + ---------- + xAxis : float[] + X-Axis values to extrapolate until. + """ if self.dcsLevel: self.data = [QPointF(x, self.dcsLevel) for x in xAxis] def toLineSeries(self, parent): + """ + Construct a line series from the data. + + Parameters + ---------- + parent : Any + Parent object for the resultant line series. + + Returns + ------- + QLineSeries : constructed line series. + """ self.series = QLineSeries(parent) if self.data: self.series.append(self.data) diff --git a/gudpy/gui/widgets/core/exponential_spinbox.py b/gudpy/gui/widgets/core/exponential_spinbox.py index 035121433..629f7ab65 100644 --- a/gudpy/gui/widgets/core/exponential_spinbox.py +++ b/gudpy/gui/widgets/core/exponential_spinbox.py @@ -7,6 +7,7 @@ class ExponentialValidator(QValidator): """ Class to represent an ExponentialValidator. Inherits QValidator. + ... Attributes @@ -15,6 +16,7 @@ class ExponentialValidator(QValidator): Regular expression pattern to validate against. symbols : str[] List of symbols. + Methods ------- valid(string): @@ -25,6 +27,9 @@ class ExponentialValidator(QValidator): Searches the string using the regular expression. """ def __init__(self): + """ + Constructs all the necessary attributes for the ExponentialValidator object. + """ super(ExponentialValidator, self).__init__() # Regular expression that matches scientific notation. self.regex = re.compile(r"(([+-]?\d+(\.\d*)?|\.\d+)([eE][+-]?\d+)?)") @@ -39,6 +44,10 @@ def valid(self, string): ---------- string : str String to be checked. + + Returns + ------- + bool : Is string valid? """ match = self.regex.search(string) return match.group(0) == string if match else "" @@ -53,6 +62,10 @@ def validate(self, string, position): String to be checked. position : int Position in string to validate at. + + Returns + ------- + QValidator.State : state of string at position. """ if self.valid(string): return QValidator.State.Acceptable @@ -81,9 +94,10 @@ def search(self, string): ---------- string : str String to be checked. + Returns ------- - Match + Match : regex match. """ return self.regex.search(string) @@ -91,6 +105,7 @@ def search(self, string): class ExponentialSpinBox(QDoubleSpinBox): """ Class to represent an ExponentialSpinBox. Inherits QDoubleSpinBox. + ... Attributes @@ -111,10 +126,29 @@ class ExponentialSpinBox(QDoubleSpinBox): Searches the string using the validator. stepBy(steps): Steps the value in the spin box. + keyPressEvent(event) + Event handler for key presses. + removeSuffix() + Removes the current suffix. + appendSuffix() + Appends a previously removed suffix. + focusInEvent(event) + Event handler for the spin box gaining focus. + focusOutEvent(event) + Event handler for the spin box losing focus. """ def __init__(self, parent): + """ + Constructs all the necessary attributes for the ExponentialValidator object. + + Parameters + ---------- + parent : Any + Parent object. + """ super(ExponentialSpinBox, self).__init__(parent=parent) self.validator = ExponentialValidator() + # Set precision. self.setDecimals(16) self.editingFinished.connect(self.appendSuffix) @@ -128,9 +162,10 @@ def validate(self, text, position): String to validate. position : int Position to validate at. + Returns ------- - QValidator.State + QValidator.State : state of string at position """ return self.validator.validate(text, position) @@ -142,9 +177,10 @@ def fixup(self, text): ---------- text: str String to fixup. + Returns ------- - str + str: 'fixed-up' string. """ match = self.validator.regex.search(text) return match.groups()[0] if match else "" @@ -157,9 +193,10 @@ def valueFromText(self, text): ---------- text: str String to convert to float. + Returns ------- - float + float : text casted to float. """ return float(text) @@ -171,9 +208,10 @@ def textFromValue(self, value): ---------- value: float Float to convert to string. + Returns ------- - str + str : float casted to string. """ return str(value) @@ -185,9 +223,10 @@ def search(self, string): ---------- string : str String to search. + Returns ------- - Match + Match : regex match. """ return self.validator.search(string) @@ -218,22 +257,56 @@ def stepBy(self, steps): self.lineEdit().setText(string) def keyPressEvent(self, event): + """ + Event handler for key presses. + Used for ensuring focus in the spin box. + + Parameters + ---------- + event : KeyPressEvent + Event that triggered the function. + """ if event.key() == Qt.MouseButton.LeftButton: self.focusInEvent(event) return super().keyPressEvent(event) def removeSuffix(self): + """ + Removes the current suffix, storing it to an auxilliary variable. + """ self.prevSuffix = self.suffix() self.setSuffix("") def appendSuffix(self): + """ + Sets the suffix from an auxilliary variable, + a previously removed suffix. + """ self.setSuffix(self.prevSuffix) self.clearFocus() def focusInEvent(self, event): + """ + Event handler for the spin box gaining focus. + Removes the current suffix. + + Parameters + ---------- + event : QFocusEvent + Event that triggered the function, + """ self.removeSuffix() return super().focusInEvent(event) def focusOutEvent(self, event): + """ + Event handler for the spin box losing focus. + Appends the suffix that was removed on gaining focus. + + Parameters + ---------- + event : QFocusEvent + Event that triggered the function, + """ self.appendSuffix() return super().focusOutEvent(event) diff --git a/gudpy/gui/widgets/core/gudpy_tree.py b/gudpy/gui/widgets/core/gudpy_tree.py index ce1462dff..16b83e634 100644 --- a/gudpy/gui/widgets/core/gudpy_tree.py +++ b/gudpy/gui/widgets/core/gudpy_tree.py @@ -37,6 +37,7 @@ class GudPyTreeModel(QAbstractItemModel): Icon for samples. containerIcon : QIcon Icon for containers. + Methods ------- index(row, column, parent) @@ -61,6 +62,8 @@ class GudPyTreeModel(QAbstractItemModel): Returns flags associated with a given index. isSample(index) Returns whether a given index is associated with a sample. + isContainer(index) + Returns whether a given index is associated with a container. isIncluded(index) Returns whether a given index of a sample is to be ran. insertRow(obj, parent) @@ -96,6 +99,7 @@ def index(self, row, column, parent=QModelIndex()): Creates a QPersistentModelIndex and adds it to the dict, to keep the internal pointer of the QModelIndex in reference. + Parameters ---------- row : int @@ -104,6 +108,7 @@ def index(self, row, column, parent=QModelIndex()): Column number. parent, optional: QModelIndex Parent index. + Returns ------- QModelIndex @@ -157,14 +162,15 @@ def parent(self, index): If the index is invalid, then an invalid QModelIndex is returned. Parent is decided on by checking the data type of the internal pointer of the index. + Parameters ---------- index : QModelIndex Index to find parent index of. + Returns ------- - QModelIndex - Parent index. + QModelIndex : Parent index. """ if not index.isValid(): return QModelIndex() @@ -185,14 +191,15 @@ def parent(self, index): def findParent(self, item): """ Finds the parent of a given Sample or Container. + Parameters ---------- item : Sample | Container Object to find parent of. + Returns ------- - SampleBackground | Sample - Parent object. + SampleBackground | Sample : Parent object. """ for i, sampleBackground in enumerate( self.gudrunFile.sampleBackgrounds @@ -213,16 +220,17 @@ def data(self, index, role): QVariant is returned. Otherwise returns check state of index, or a QVariant constructed from its name. + Parameters ---------- index : QModelIndex Index to extract data from. role : int Role. + Returns ------- - QVariant | QCheckState - Data at index. + QVariant | QCheckState : Data at index. """ if not index.isValid(): return None @@ -258,6 +266,7 @@ def setData(self, index, value, role): Sets data at a given index, if the index is valid. Only used for assigning CheckStates to samples, and altering the names of samples/containers. + Parameters ---------- index : QModelIndex @@ -266,10 +275,10 @@ def setData(self, index, value, role): Value to assign to data. role : int Role. + Returns ------- - bool - Success / Failure. + bool : Success / Failure. """ if not index.isValid(): return False @@ -293,14 +302,15 @@ def setData(self, index, value, role): def checkState(self, index): """ Returns the check state of a given index. + Parameters ---------- index : QModelIndex Index to return check state from. + Returns ------- - QCheckState - Check state. + QCheckState : Check state. """ return Qt.Checked if self.isIncluded(index) else Qt.Unchecked @@ -308,14 +318,15 @@ def rowCount(self, parent=QModelIndex()): """ Returns the row count of a given parent index. The row count returned depends on the data type of the parent. + Parameters ---------- - parent : QModelIndex + parent : QModelIndex, optional Parent index to retrieve row count from. + Returns ------- - int - Row count. + int : Row count. """ # If the parent is invalid, then it is a top level node. if not parent.isValid(): @@ -356,20 +367,28 @@ def rowCount(self, parent=QModelIndex()): def columnCount(self, parent=QModelIndex()): """ Returns the column count of an index. + Parameters ---------- - parent : QModelIndex + parent : QModelIndex, optional Parent index to retrieve column row count from. + Returns ------- - int - Column count. This is always 1. + int : Column count. This is always 1. """ return 1 def setEnabled(self, state): + """ + Enables / disables "editing" of the tree. + + Parameters + ---------- + state : bool + Should editing be enabled? + """ self.flags_ = {} - # self.flags_[Sample] if state: self.flags_[Sample] = Qt.ItemIsEditable | Qt.ItemIsUserCheckable self.flags_[Container] = Qt.ItemIsEditable @@ -380,14 +399,15 @@ def setEnabled(self, state): def flags(self, index): """ Returns flags associated with a given index. + Parameters ---------- index : QModelIndex Index to retreive flags from. + Returns ------- - int - Flags. + int : Flags. """ flags = super().flags(index) # If it is a sample, append checkable flag. @@ -401,48 +421,52 @@ def flags(self, index): def isSample(self, index): """ Returns whether a given index is associated with a sample. + Parameters ---------- index : QModelIndex Index to check if sample is associated with. + Returns ------- - bool - Is it a sample or not? + bool : Is it a sample or not? """ return isinstance(index.parent().internalPointer(), SampleBackground) def isContainer(self, index): """ Returns whether a given index is associated with a container. + Parameters ---------- index : QModelIndex Index to check if container is associated with. + Returns ------- - bool - Is it a container or not? + bool : Is it a container or not? """ return isinstance(index.parent().internalPointer(), Sample) def isIncluded(self, index): """ Returns whether a given index of a sample is to be ran. + Parameters ---------- index : QModelIndex Index to check if the associated sample is to be included or not. + Returns ------- - bool - Is it to be included? + bool : Is it to be included? """ return self.isSample(index) and index.internalPointer().runThisSample def insertRow(self, obj, parent): """ Insert a row containing an object to a parent index. + Parameters ---------- obj : SampleBackground | Sample | Container @@ -543,6 +567,7 @@ def insertRow(self, obj, parent): def removeRow(self, index): """ Remove a row from an index. + Parameters ---------- index : QModelIndex @@ -614,17 +639,19 @@ class GudPyTreeView(QTreeView): GudrunFile object to build the tree from. parent : QWidget Parent widget. - model : QStandardItemModel + model_ : GudPyTreeModel Model to be used for the tree view. sibling : QStackedWidget Sibling widget to communicate signals and slots to/from. + clipboard : Any + Current clipboard contents. contextMenuEnabled : bool Is the context tree enabled? + Methods ------- - buildTree(gudrunFile, sibling) - Builds the tree view from the gudrunFile, pairing - the modelIndexes with pages of the sibling QStackedWidget. + buildTree(gudrunFile, parent) + Builds the tree view from the gudrunFile. makeModel() Creates the model for the tree view from the GudrunFile. currentChanged(current, previous) @@ -639,28 +666,38 @@ class GudPyTreeView(QTreeView): Removes the current index from the model. contextMenuEvent(event) Creates context menu, for right clicking the table. - insertSampleBackground_(sampleBackground) + insertSampleBackground(sampleBackground) Inserts a SampleBackground into the GudrunFile. - insertSample_(sample) + insertSample(sample) Inserts a Sample into the GudrunFile. - insertContainer_(container) + insertContainer(container) Inserts a Container into the GudrunFile. copy() Copies the current object to the clipboard. - cut_() + del_() + Deletes the current object. + cut() Cuts the current object to the clipboard. - paste_() + paste() Pastes the clipboard back into the GudrunFile. - duplicate() + duplicateSample() Duplicates the current Sample. duplicateOnlySample() Duplicates the current Sample without any containers. - setSamplesSelectected(selected) + setSampleSelected(selected) Sets sample states to selected. selectOnlyThisSample() Selects only the current sample, and deselects all others. setContextEnabled(state) Enable/Disable the context menu. + getSamples() + Gets all of the samples in the tree. + getContainers() + Gets all of the containers in the ree. + convertToSample(): + Converts the current container to a sample. + _expand(parent, _first, _last) + Expand the tree from the parent. """ def __init__(self, parent): @@ -677,20 +714,25 @@ def __init__(self, parent): def buildTree(self, gudrunFile, parent): """ Constructs all the necessary attributes for the GudPyTreeView object. - Calls the makeModel method, - to create a QStandardItemModel for the tree view. + Calls the makeModel method, to create a GudPyTreeModel + for the tree view. + Parameters ---------- gudrunFile : GudrunFile GudrunFile object to create the tree from. - sibling : QStackedWidget - Sibling widget to communicate signals and slots to/from. + parent : Any + Parent widget. """ self.gudrunFile = gudrunFile self.parent = parent self.makeModel() + + # Select the Instrument. self.setCurrentIndex(self.model().index(0, 0)) self.setHeaderHidden(True) + + # Expand the tree. self.expandToDepth(0) self.model().rowsInserted.connect(self._expand) @@ -702,12 +744,20 @@ def makeModel(self): Creates the QStandardItemModel to be used for the GudPyTreeView. The model is constructed from the GudrunFile. """ + # Construct the model. self.model_ = GudPyTreeModel(self, self.gudrunFile) self.setModel(self.model_) def currentChanged(self, current, previous): """ Slot method for current index being changed in the tree view. + + Parameters + ---------- + current : QModelIndex + Current index. + previous : QModelIndex + Previous index. """ if current.internalPointer(): self.click(current) @@ -717,11 +767,14 @@ def click(self, modelIndex): """ Sets the current index of the sibling QStackedWidget to the absolute index of the modelIndex. + Parameters ---------- modelIndex : QModelIndex - QModelIndex of the QStandardItem that was clicked in the tree view. + QModelIndex that was clicked in the tree view. """ + + # Map of objects to indexes in the QStackedWidgets, and setter functions. indexMap = { Instrument: (0, None), Beam: (1, None), @@ -734,12 +787,20 @@ def click(self, modelIndex): Container: (6, self.parent.containerSlots.setContainer) } self.parent.setTreeActionsEnabled(False) + + # Get the type of the current object. type_ = type(modelIndex.internalPointer()) + index, setter = indexMap[type_] + # Set the index in the stacked widget. self.parent.mainWidget.objectStack.setCurrentIndex(index) self.parent.updateComponents() + + # Call the setter if necessary. if setter: setter(modelIndex.internalPointer()) + + # Enable correct actions. if isinstance( modelIndex.internalPointer(), (Instrument, Beam, Components, Normalisation, SampleBackground) @@ -786,28 +847,30 @@ def click(self, modelIndex): def currentObject(self): """ Returns the object associated with the current index. + Returns ------- Instrument | Beam | Normalisation | - SampleBackground | Sample | Container - Object associated with the current index. + SampleBackground | Sample | Container : Object associated with the current index. """ return self.currentIndex().internalPointer() def insertRow(self, obj): """ Inserts an object into the current row in the model. + Parameters ---------- - obj : SampleBackground | Sample | Container - Object to be inserted. + SampleBackground | Sample | Container : Object to be inserted. """ currentIndex = self.currentIndex() + # Insert a row. self.model().insertRow(obj, currentIndex) newIndex = self.model().index( currentIndex.row()+1, 0, self.model().parent(currentIndex) ) + # Expend where the new row was inserted, so it is visible. self.expandRecursively(newIndex, 0) self.parent.updateAllSamples() @@ -821,6 +884,7 @@ def contextMenuEvent(self, event): """ Creates context menu, so that on right clicking the table, the user is able to perform menu actions. + Parameters ---------- event : QMouseEvent @@ -982,6 +1046,7 @@ def insertSampleBackground(self, sampleBackground=None): """ Inserts a SampleBackground into the GudrunFile. Inserts it into the tree. + Parameters ---------- sampleBackground : SampleBackground, optional @@ -995,6 +1060,7 @@ def insertSample(self, sample=None): """ Inserts a Sample into the GudrunFile. Inserts it into the tree. + Parameters ---------- sample : Sample, optional @@ -1009,6 +1075,7 @@ def insertContainer(self, container=None): """ Inserts a Container into the GudrunFile. Inserts it into the tree. + Parameters ---------- container : Container, optional @@ -1075,6 +1142,11 @@ def setSamplesSelected(self, selected): """ Sets all samples status to 'selected'. Used for selecting / deselecting all samples + + Parameters + ---------- + selected : bool + Should samples be selected? """ for sampleBackground in self.gudrunFile.sampleBackgrounds: for sample in sampleBackground.samples: @@ -1097,10 +1169,22 @@ def selectOnlyThisSample(self): def setContextEnabled(self, state): """ Disables/Enables the context menu. + + Parameters + ---------- + state : bool + Should the context menu be enabled? """ self.contextMenuEnabled = state def getSamples(self): + """ + Gets all of the samples in the tree. + + Returns + ------- + Sample[] : all samples in the tree. + """ samples = [] for i in range( NUM_GUDPY_CORE_OBJECTS, @@ -1115,6 +1199,13 @@ def getSamples(self): return samples def getContainers(self): + """ + Gets all of the containers in the tree. + + Returns + ------- + Container[] : all containers in the tree. + """ containers = [] for i in range( NUM_GUDPY_CORE_OBJECTS, @@ -1129,10 +1220,22 @@ def getContainers(self): return containers def convertToSample(self): + """ + Converts the current container to a sample, + and appends it to the current sample background. + """ container = self.currentObject() sample = container.convertToSample() self.removeRow() self.insertSample(sample) def _expand(self, parent, _first, _last): + """ + Expands the tree from the parent. + + Parameters + ---------- + parent : QModelIndex + Parent index. + """ self.expand(parent) diff --git a/gudpy/gui/widgets/core/main_window.py b/gudpy/gui/widgets/core/main_window.py index 7beb5976c..5da69cf5f 100644 --- a/gudpy/gui/widgets/core/main_window.py +++ b/gudpy/gui/widgets/core/main_window.py @@ -132,28 +132,173 @@ class GudPyMainWindow(QMainWindow): ---------- gudrunFile : GudrunFile GudrunFile object currently associated with the application. + modified : bool + Has the GudrunFile been modified? clipboard : SampleBackground | Sample | Container Stores copied objects. - iterator : TweakFactorIterator | WavelengthSubtractionIterator + iterator : TweakFactorIterator | WavelengthSubtractionIterator | ThicknessIterator | DensityIterator | CompositionIterator Iterator to use in iterations. + queue : Queue + Queue for tasks. + results : {} + Dictionary storing results. + allPlots : [] + List of all plots. + plotModes : {} + Plot modes for each sample / container. + proc : QProcess + Currently running process. + output : str + Output of processing. + outputIterations : {} + Map of output per iteration. + previousProcTitle : str + Title of the previous process. + error : str + Error that occurred during processing. + cwd : str + Current working directory. + warning : str + Warning to be given to the user. + worker : CompositionWorker + Worker for composition iterations. + workerThread : QThread + Thread for worker. + timer : QTimer + Timer for autosaving. + Methods ------- initComponents() - Loads the UI file for the GudPyMainWindow + Sets up the UI and slots. + updateWidgets(fromFile=False) + Updates widget contents in the GUI. + handleObjectsChanged() + Handles change in the tree view. loadInputFile_() Loads an input file. saveInputFile() Saves the current GudPy file. + newInputFile() + Creates a new input file. updateFromFile() Updates from the original input file. updateGeometries() Updates geometries across objects. updateCompositions() Updates compositions across objects - Deletes the current object. + focusResult() + Focuses the results section on the current Sample / Container. + updateSamples() + Updates the results of each sample. + updateAllSamples() + Updates the results in the "All Samples" plot. + updateResults() + Updates results throughout the GUI. + updateComponents() + Updates geometries and compositions. exit_() - Exits + Quits GudPy. + makeProc(cmd, slot, finished=None, func=None, args=None) + Creates and starts a QProcess. + runPurge_() + Runs a purge. + runGudrun_() + Runs gudrun. + runContainersAsSamples() + Runs conainers as samples. + runFilesIndividually() + Runs files individually. + purgeOptionsMessageBox(dcs, finished, func, args, text) + Purge options message box, for running a purge. + purgeBeforeRunning(default=True) + Runs a purge before running gudrun. + iterateGudrun(dialog, name) + Iterate Gudrun. + batchProcessing() + Run batch processing. + batchProcessFinished(ec, es) + Slot for handling a batch process finished. + nextBatchProcess() + Begins the next batch process in the queue. + progressBatchProcess() + Slot for measuring and setting progress whilst batch processing. + batchProcessingFinished() + Slot for handling a batch processing pipeline finishing. + finishedCompositionIteration(originalSample, updatedSample) + Slot for handling a composition iteration finishing. + finishedCompositionIterations() + Slot for handling composition iterations finishing. + startedCompositionIteration(sample) + Slot for handling starting a composition iteration. + errorCompositionIteration(output) + Handles errors occuring during composition iterations. + progressCompositionIteration(currentIteration) + Slot for measuring and setting progress whilst iterating by composition. + nextCompositionIteration() + Begins the next composition iteration. + iterateByComposition() + Iterate by composition. + nextIteration() + Begins the next iteration, when performing basic iterations. + nextIterableProc() + Starts the next process in the queue. + iterationStarted() + Slot for handling the start of an iteration. + progressIteration() + Slot for measuring and setting progress whilst iterating. + checkFilesExist_() + Checks that data files exist. + autosave() + Slot for autosaving. + setModified() + Sets the current window to be modified. + setUnModified() + Sets the current window to be unmodified. + setControlsEnabled(state) + Toggles controls. + setActionsEnabled(state) + Toggles actions. + setTreeActionsEnabled(state) + Toggles tree actions. + progressIncrementDCS(gudrunFile) + Calculates progress of gudrun, base on the current output. + progressDCS() + Slot for measuring and setting progress whilst running gudrun. + progressIncrementPurge() + Calculates progress of purging, based on the current output. + progressPurge() + Slot for measuring and setting progress whilst purging. + procStarted() + Slot for handling a process being started. + runGudrunFinished(ec, es, gudrunFile=None) + Slot for handling running gudrun finishing. + procFinished(ec, es) + Slot for handling a process finishing. + stopProc() + Stops the currently running process and any threads. + viewInput() + Views the current input file. + handleAllPlotModeChanged(index) + Slot for handling change in the "All Samples" plot mode. + handleSamplePlotModeChanged(index) + Slot for handling change in the current Sample's plot mode. + handleContainerPlotModeChanged(index) + Slot for handling change in the current Container's plot mode. + isPlotModeSplittable(plotMode) + Determines if a given plot mode can be split into two different plots. + splitPlotMode(plotMode) + Determines how to split a given plot mode. + handlePlotModeChanged(plot, plotMode) + Calls `plot` with `plotMode`. + onException(cls, exception, tb) + Exception handler. + export() + Export output data. + cleanup() + Stops any currently running process and performs an autosave. """ + def __init__(self): """ Constructs all the necessary attributes for the GudPyMainWindow object. @@ -185,8 +330,10 @@ def __init__(self): def initComponents(self): """ - Loads the UI file for the GudPyMainWindow. + Sets up the UI and slots. """ + + # Load the UI file. if hasattr(sys, '_MEIPASS'): uifile = QFile( os.path.join( @@ -202,6 +349,7 @@ def initComponents(self): ) ) + # Register custom widgets. loader = QUiLoader() loader.registerCustomWidget(GudPyTreeView) loader.registerCustomWidget(OutputTreeView) @@ -237,11 +385,14 @@ def initComponents(self): loader.registerCustomWidget(GudPyChartView) self.mainWidget = loader.load(uifile) + # Create a status bar. self.mainWidget.statusBar_ = QStatusBar(self) self.mainWidget.statusBarWidget = QWidget(self.mainWidget.statusBar_) self.mainWidget.statusBarLayout = QHBoxLayout( self.mainWidget.statusBarWidget ) + + # Create a current task label and add it to the status bar. self.mainWidget.currentTaskLabel = QLabel( self.mainWidget.statusBarWidget ) @@ -249,6 +400,8 @@ def initComponents(self): self.mainWidget.currentTaskLabel.setSizePolicy( QSizePolicy(QSizePolicy.Minimum, QSizePolicy.Preferred) ) + + # Create a stop button and add it to the status bar. self.mainWidget.stopTaskButton = QToolButton( self.mainWidget.statusBarWidget ) @@ -264,6 +417,7 @@ def initComponents(self): QSizePolicy(QSizePolicy.Minimum, QSizePolicy.Preferred) ) + # Create a progress bar and add it to the status bar. self.mainWidget.progressBar = QProgressBar( self.mainWidget.statusBarWidget ) @@ -284,6 +438,7 @@ def initComponents(self): self.mainWidget.statusBar_.addWidget(self.mainWidget.statusBarWidget) self.mainWidget.setStatusBar(self.mainWidget.statusBar_) + # Create the beam plot and add it to the beam page. self.mainWidget.beamPlot = QChartView( self.mainWidget ) @@ -297,6 +452,7 @@ def initComponents(self): self.mainWidget.beamPlot.setChart(self.mainWidget.beamChart) + # Create the sample top plot and add it to the sample page. self.mainWidget.sampleTopPlot = GudPyChartView( self.mainWidget ) @@ -305,6 +461,7 @@ def initComponents(self): self.mainWidget.sampleTopPlot ) + # Create the sample bottom plot and add it to the sample page. self.mainWidget.sampleBottomPlot = GudPyChartView(self.mainWidget) self.mainWidget.bottomPlotLayout.addWidget( @@ -313,6 +470,7 @@ def initComponents(self): self.mainWidget.bottomSamplePlotFrame.setVisible(False) + # Create the container top plot and add it to the container page. self.mainWidget.containerTopPlot = GudPyChartView( self.mainWidget ) @@ -321,6 +479,7 @@ def initComponents(self): self.mainWidget.containerTopPlot ) + # Create the container bottom plot and add it to the container page. self.mainWidget.containerBottomPlot = GudPyChartView( self.mainWidget ) @@ -331,6 +490,7 @@ def initComponents(self): self.mainWidget.bottomContainerPlotFrame.setVisible(False) + # Create the all sample top plot and add it to the output page. self.mainWidget.allSampleTopPlot = GudPyChartView( self.mainWidget ) @@ -341,12 +501,15 @@ def initComponents(self): self.mainWidget.allSampleBottomPlot = GudPyChartView(self.mainWidget) + # Create the all sample bottom plot and add it to the output page. self.mainWidget.bottomAllPlotLayout.addWidget( self.mainWidget.allSampleBottomPlot ) + # By default, hide the bottom plot. self.mainWidget.bottomPlotFrame.setVisible(False) + # Populate the all sample top plot combo box. for plotMode in [ plotMode for plotMode in PlotModes if plotMode not in [ @@ -360,6 +523,7 @@ def initComponents(self): self.handleAllPlotModeChanged ) + # Populate the sample plot combo box. for plotMode in [ plotMode for plotMode in PlotModes if "(Cans)" not in plotMode.name @@ -370,6 +534,7 @@ def initComponents(self): self.handleSamplePlotModeChanged ) + # Populate thecontainer plot combo box. for plotMode in [ plotMode for plotMode in PlotModes if "(Cans)" in plotMode.name ]: @@ -381,6 +546,7 @@ def initComponents(self): self.handleContainerPlotModeChanged ) + # Set the window title, and setup slots. self.mainWidget.setWindowTitle("GudPy") self.mainWidget.show() self.instrumentSlots = InstrumentSlots(self.mainWidget, self) @@ -393,6 +559,8 @@ def initComponents(self): self.sampleSlots = SampleSlots(self.mainWidget, self) self.containerSlots = ContainerSlots(self.mainWidget, self) self.outputSlots = OutputSlots(self.mainWidget, self) + + # Connect actions to slots. self.mainWidget.runPurge.triggered.connect( self.runPurge_ )