From 02b67a581b18dceb4c89bc1ca7befa711f23a5fb Mon Sep 17 00:00:00 2001 From: Jared Swift Date: Mon, 13 Jun 2022 13:24:35 +0100 Subject: [PATCH 01/38] docs: comprehensive commenting for Beam, Composition and CompositionIterator classes. --- gudpy/core/beam.py | 15 +- gudpy/core/composition.py | 428 +++++++++++++++++++++++++++-- gudpy/core/composition_iterator.py | 238 ++++++++++++---- 3 files changed, 601 insertions(+), 80 deletions(-) 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..12385b7bc 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,61 @@ 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. + + Returns + ------- + bool : self == obj + """ return all( [ e.eq(el) for e, el in zip(self.elements, obj.elements) @@ -119,8 +309,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 +345,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 +420,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 +437,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 +508,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 +575,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 +595,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 +627,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) From 9ec12776162fe73b58be9ecd790e6abae7033235 Mon Sep 17 00:00:00 2001 From: jswift-stfc Date: Wed, 15 Jun 2022 16:59:48 +0100 Subject: [PATCH 02/38] docs: comprehensive commenting for config. --- gudpy/core/config.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) 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", "") From 4eeb05bb2fba1ee587352beabc1ffc1cbe6e0e15 Mon Sep 17 00:00:00 2001 From: jswift-stfc Date: Wed, 15 Jun 2022 17:08:58 +0100 Subject: [PATCH 03/38] docs: comprehensive commenting of Container. --- gudpy/core/container.py | 234 +++++++++++++++++++++++----------------- 1 file changed, 138 insertions(+), 96 deletions(-) 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' + ) From 3f63c4087700b61e9bfb20a69b2eafcf21157c95 Mon Sep 17 00:00:00 2001 From: jswift-stfc Date: Wed, 15 Jun 2022 17:13:26 +0100 Subject: [PATCH 04/38] chore: comprehensive commenting of DataFiles. --- gudpy/core/data_files.py | 46 ++++++++++++++++++++++++++++------------ 1 file changed, 32 insertions(+), 14 deletions(-) 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) From 0144006209f63d3ba831fc5b16052ba7e1044f9c Mon Sep 17 00:00:00 2001 From: jswift-stfc Date: Wed, 15 Jun 2022 17:15:26 +0100 Subject: [PATCH 05/38] docs: comprehensive commenting for DensityIterator. --- gudpy/core/density_iterator.py | 8 ++++++++ 1 file changed, 8 insertions(+) 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}") From e5265aeb962d0ed0a76f94e60c48f8cdf39c2d81 Mon Sep 17 00:00:00 2001 From: Jared Swift Date: Thu, 16 Jun 2022 15:06:11 +0100 Subject: [PATCH 06/38] docs: comprehensive commenting of compositiion and element. --- gudpy/core/composition.py | 5 +++++ gudpy/core/element.py | 23 +++++++++++++++-------- 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/gudpy/core/composition.py b/gudpy/core/composition.py index 12385b7bc..2e64031be 100644 --- a/gudpy/core/composition.py +++ b/gudpy/core/composition.py @@ -297,6 +297,11 @@ def eq(self, obj): """ Checks for equality between `obj` and the current object. + Parameters + ---------- + obj : Component + Object to compare against. + Returns ------- bool : self == obj diff --git a/gudpy/core/element.py b/gudpy/core/element.py index 6b06ae538..82d2648af 100644 --- a/gudpy/core/element.py +++ b/gudpy/core/element.py @@ -12,8 +12,11 @@ class Element: The atomic number belonging to the element (total number of nucleons). abundance : float Abundance of the element. + Methods ------- + eq(obj) + Checks for equality between elements. """ def __init__(self, atomicSymbol, massNo, abundance): """ @@ -41,10 +44,6 @@ def __str__(self): """ Returns the string representation of the Element object. - Parameters - ---------- - None - Returns ------- string : str @@ -62,10 +61,6 @@ def __repr__(self): """ Returns the string representation of the Element object. - Parameters - ---------- - None - Returns ------- string : str @@ -74,6 +69,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') From 44bf10ead76bcd48d2fbf910537b27dc6470a2ec Mon Sep 17 00:00:00 2001 From: Jared Swift Date: Thu, 16 Jun 2022 16:10:55 +0100 Subject: [PATCH 07/38] docs: comprehensive commenting of enums. --- gudpy/core/enums.py | 50 ++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 47 insertions(+), 3 deletions(-) diff --git a/gudpy/core/enums.py b/gudpy/core/enums.py index 2c1876b9c..2e386031e 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 iteration modes. +""" ITERATION_MODES = { 0: ["None", "NONE"], 1: ["Tweak Factor", "TWEAK_FACTOR"], From bafdf9ea192cebeaad566cdc6c96a50ef3080126 Mon Sep 17 00:00:00 2001 From: Jared Swift Date: Thu, 16 Jun 2022 16:12:16 +0100 Subject: [PATCH 08/38] docs: comprehensive commenting for exceptions. --- gudpy/core/exception.py | 8 ++++++++ 1 file changed, 8 insertions(+) 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 From d2db2db62c04c18608f2f65b512458b5b85d1b5a Mon Sep 17 00:00:00 2001 From: Jared Swift Date: Thu, 16 Jun 2022 16:16:48 +0100 Subject: [PATCH 09/38] docs: comprehensive commenting for file library. --- gudpy/core/file_library.py | 32 +++++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/gudpy/core/file_library.py b/gudpy/core/file_library.py index f1ac04694..cd6158d39 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,25 @@ 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( From 6ccc74524f6182bb60e51c8534531fb1f30d4670 Mon Sep 17 00:00:00 2001 From: Jared Swift Date: Thu, 16 Jun 2022 16:25:32 +0100 Subject: [PATCH 10/38] refactor: remove unnecessary extraneous line. --- gudpy/core/file_library.py | 1 - 1 file changed, 1 deletion(-) diff --git a/gudpy/core/file_library.py b/gudpy/core/file_library.py index cd6158d39..f47b6ec0f 100644 --- a/gudpy/core/file_library.py +++ b/gudpy/core/file_library.py @@ -160,7 +160,6 @@ def exportMintData( Export target. includeParams : bool, optional Should a sample parameters file be produced for each sample? - Returns ------- From 1d437ef2875a3b365054a79b907727dbfada78db Mon Sep 17 00:00:00 2001 From: Jared Swift Date: Thu, 16 Jun 2022 16:44:37 +0100 Subject: [PATCH 11/38] docs: comprehensive commentung of GudFile. --- gudpy/core/gud_file.py | 77 ++++++++++++++++++------------------------ 1 file changed, 33 insertions(+), 44 deletions(-) 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 From 0d51acd9dc7ca8a66e30854b165346fc9e53e686 Mon Sep 17 00:00:00 2001 From: Jared Swift Date: Fri, 17 Jun 2022 09:24:33 +0100 Subject: [PATCH 12/38] docs: comprehensive commenting for GudPyYAML. --- gudpy/core/gudpy_yaml.py | 136 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 134 insertions(+), 2 deletions(-) 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": From 78001046349587977063b894f12410ee0526fb27 Mon Sep 17 00:00:00 2001 From: Jared Swift Date: Fri, 17 Jun 2022 10:20:33 +0100 Subject: [PATCH 13/38] docs: comprehensive commenting of GudrunFile. --- gudpy/core/gudrun_file.py | 243 ++++++++++++++++++++++---------------- 1 file changed, 144 insertions(+), 99 deletions(-) diff --git a/gudpy/core/gudrun_file.py b/gudpy/core/gudrun_file.py index e2f25fcd9..4d27cddbe 100644 --- a/gudpy/core/gudrun_file.py +++ b/gudpy/core/gudrun_file.py @@ -43,9 +43,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. ... @@ -53,8 +53,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 @@ -68,6 +72,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(): @@ -106,6 +112,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. @@ -113,16 +125,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. """ @@ -176,9 +200,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 @@ -189,9 +210,6 @@ def peekNextToken(self): """ Returns the next token in the input stream, without removing it. - Parameters - ---------- - None Returns ------- str | None @@ -204,10 +222,8 @@ def consumeTokens(self, n): Parameters ---------- - None - Returns - ------- - None + n : int + Number of tokens to consume. """ for _ in range(n): self.getNextToken() @@ -218,10 +234,8 @@ def consumeUpToDelim(self, delim): Parameters ---------- - None - Returns - ------- - None + delim : str + Delimiter to seek until. """ line = self.getNextToken() while line[0] != delim: @@ -230,13 +244,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(): @@ -249,14 +256,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. @@ -477,14 +476,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: @@ -581,14 +572,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: @@ -781,9 +764,6 @@ def parseSampleBackground(self): Raises a ParserException if any mandatory attributes are missing. Returns the parsed object. - Parameters - ---------- - None Returns ------- sampleBackground : SampleBackground @@ -825,9 +805,6 @@ def parseSample(self): Raises a ParserException if any mandatory attributes are missing. Returns the parsed object. - Parameters - ---------- - None Returns ------- sample : Sample @@ -1031,9 +1008,6 @@ def parseContainer(self): Raises a ParserException if any mandatory attributes are missing. Returns the parsed object. - Parameters - ---------- - None Returns ------- container : Container @@ -1170,6 +1144,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() @@ -1182,6 +1161,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() @@ -1209,6 +1196,7 @@ def makeParse(self, key): key : str Parsing function to call (INSTRUMENT/BEAM/NORMALISATION/SAMPLE BACKGROUND/SAMPLE/CONTAINER) + Returns ------- NoneType @@ -1240,13 +1228,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. @@ -1280,10 +1265,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. @@ -1374,14 +1357,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" @@ -1432,7 +1410,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 @@ -1444,23 +1431,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( @@ -1503,22 +1497,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) @@ -1529,10 +1525,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: @@ -1544,6 +1543,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]) @@ -1564,13 +1564,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) @@ -1589,12 +1592,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) @@ -1603,7 +1610,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: @@ -1616,14 +1636,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" From 0d124d682c9d79d687fa2e3862441aeb15605c1a Mon Sep 17 00:00:00 2001 From: Jared Swift Date: Fri, 17 Jun 2022 11:08:18 +0100 Subject: [PATCH 14/38] docs: comprehensive commenting of GUIConfig.~ --- gudpy/core/gui_config.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) 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 = { From 9f05b7c7ad3c4de412e22cd744140aec657ba25b Mon Sep 17 00:00:00 2001 From: Jared Swift Date: Fri, 17 Jun 2022 11:17:19 +0100 Subject: [PATCH 15/38] docs: comprehensive commenting of Instrument. --- gudpy/core/instrument.py | 24 ++++++++---------------- 1 file changed, 8 insertions(+), 16 deletions(-) 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 From 3c0987826d0bcdacf60d97d4d9060f9c029a0227 Mon Sep 17 00:00:00 2001 From: Jared Swift Date: Fri, 17 Jun 2022 11:52:07 +0100 Subject: [PATCH 16/38] docs: comprehensive commenting of isotopes. --- gudpy/core/isotopes.py | 229 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 229 insertions(+) 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: From 1bc38747c6432f67a881b17019d20843e506e3cf Mon Sep 17 00:00:00 2001 From: Jared Swift Date: Fri, 17 Jun 2022 12:19:11 +0100 Subject: [PATCH 17/38] docs: commenting for mass data. --- gudpy/core/mass_data.py | 3 +++ 1 file changed, 3 insertions(+) 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) From 113d2036c6e174c29dd598cd511b60148b6a1cfc Mon Sep 17 00:00:00 2001 From: Jared Swift Date: Fri, 17 Jun 2022 13:30:54 +0100 Subject: [PATCH 18/38] docs: more commenting! --- gudpy/core/element.py | 2 ++ gudpy/core/normalisation.py | 12 ++---------- 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/gudpy/core/element.py b/gudpy/core/element.py index 82d2648af..faa1b1996 100644 --- a/gudpy/core/element.py +++ b/gudpy/core/element.py @@ -12,6 +12,8 @@ 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 ------- 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 From 8575a20ddb82fc2ba4d4cbbdaa1dabda543532b6 Mon Sep 17 00:00:00 2001 From: Jared Swift Date: Fri, 17 Jun 2022 14:21:03 +0100 Subject: [PATCH 19/38] docs: comprehensive commenting of OutputFileHandler. --- gudpy/core/output_file_handler.py | 73 +++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) 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 From 01c2f2fa975a6a4a79fed3c2c620a5ae8135ac05 Mon Sep 17 00:00:00 2001 From: Jared Swift Date: Fri, 17 Jun 2022 14:27:54 +0100 Subject: [PATCH 20/38] docs: purge.. --- gudpy/core/purge_file.py | 158 ++++++++++++++++++--------------------- 1 file changed, 74 insertions(+), 84 deletions(-) diff --git a/gudpy/core/purge_file.py b/gudpy/core/purge_file.py index 90871195c..371ccd7ab 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 From 3bdf8ce510055e2d04cf5b4a8be236e3caeaea84 Mon Sep 17 00:00:00 2001 From: Jared Swift Date: Fri, 17 Jun 2022 14:37:45 +0100 Subject: [PATCH 21/38] docs: comprehensive commenting for radius iterator. --- gudpy/core/radius_iterator.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) 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}") From 3a4581e28b21d75e6429b2251f9ad85717513d8e Mon Sep 17 00:00:00 2001 From: Jared Swift Date: Fri, 17 Jun 2022 14:44:10 +0100 Subject: [PATCH 22/38] docs: comprehensive commenting of RunContainersAsSamples. --- gudpy/core/run_containers_as_samples.py | 42 +++++++++++++++++++++++++ 1 file changed, 42 insertions(+) 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) From 9b6e55c11e63373023c672e8680270aa2065a27d Mon Sep 17 00:00:00 2001 From: Jared Swift Date: Mon, 27 Jun 2022 08:47:59 +0100 Subject: [PATCH 23/38] docs: comprehensive commenting of sample background. --- gudpy/core/sample_background.py | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) 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 From 65db0293545e6ca8c7eed97c78f48abe6c224361 Mon Sep 17 00:00:00 2001 From: Jared Swift Date: Mon, 27 Jun 2022 08:54:56 +0100 Subject: [PATCH 24/38] docs: Sample. --- gudpy/core/sample.py | 47 +++++++++++++++++++++++++------------------- 1 file changed, 27 insertions(+), 20 deletions(-) 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 From d3f3f2e233226ed738c5da84e4e2649fa56cda8e Mon Sep 17 00:00:00 2001 From: Jared Swift Date: Mon, 27 Jun 2022 08:57:22 +0100 Subject: [PATCH 25/38] docs: SingleParamIterator. --- gudpy/core/single_param_iterator.py | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) 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 From 60dca372ddde05ff8aacd415b612ef611a7f35ed Mon Sep 17 00:00:00 2001 From: Jared Swift Date: Mon, 27 Jun 2022 08:59:23 +0100 Subject: [PATCH 26/38] docs: ThicknessIterator. --- gudpy/core/thickness_iterator.py | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) 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}") From ba37fec32e8fc4fcad537e2997d029fb9819422e Mon Sep 17 00:00:00 2001 From: Jared Swift Date: Mon, 27 Jun 2022 09:01:03 +0100 Subject: [PATCH 27/38] docs: TweakFactorIterator. --- gudpy/core/tweak_factor_iterator.py | 1 + 1 file changed, 1 insertion(+) 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) From 1eb12236b53dde79a1afcca30f72d6934f266a0c Mon Sep 17 00:00:00 2001 From: Jared Swift Date: Mon, 27 Jun 2022 09:28:44 +0100 Subject: [PATCH 28/38] docs: utils. --- gudpy/core/utils.py | 314 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 312 insertions(+), 2 deletions(-) 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:])}' From 85d352f51a97c7e50ce630a33d7930e8c8df016c Mon Sep 17 00:00:00 2001 From: Jared Swift Date: Mon, 27 Jun 2022 09:46:28 +0100 Subject: [PATCH 29/38] docs: WavelengthSubtractionIterator. --- gudpy/core/wavelength_subtraction_iterator.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) 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. From dc4a6647478293aeb13a7ec0821f8277d4d119e8 Mon Sep 17 00:00:00 2001 From: Jared Swift Date: Mon, 27 Jun 2022 09:55:43 +0100 Subject: [PATCH 30/38] docs: BeamChart. --- gudpy/gui/widgets/charts/beam_plot.py | 39 ++++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) 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) From 0e990b2ad2d04df191db617034880bdd934ef337 Mon Sep 17 00:00:00 2001 From: Jared Swift Date: Mon, 27 Jun 2022 10:38:13 +0100 Subject: [PATCH 31/38] docs: GudPyChart. --- gudpy/gui/widgets/charts/chart.py | 231 ++++++++++++++++++++++++++++-- 1 file changed, 220 insertions(+), 11 deletions(-) diff --git a/gudpy/gui/widgets/charts/chart.py b/gudpy/gui/widgets/charts/chart.py index 60e53f089..9e6c8985d 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,26 +267,35 @@ 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 ) + # 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, hide it. if not series.points(): 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) @@ -174,33 +317,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", @@ -211,6 +373,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( @@ -219,14 +382,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", @@ -237,6 +404,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() @@ -246,14 +414,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() @@ -261,6 +451,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) @@ -268,6 +468,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 @@ -279,4 +487,5 @@ def toggleLogarithmicAxis(self, axis): self.logarithmicY = not self.logarithmicY self.logarithmicA = self.logarithmicX and self.logarithmicY + # Re-plot. self.plot() From ec00d76dd169b22214eda3a45b11a7e4fd36f793 Mon Sep 17 00:00:00 2001 From: Jared Swift Date: Mon, 27 Jun 2022 10:55:43 +0100 Subject: [PATCH 32/38] docs: GudPyChartView. --- gudpy/gui/widgets/charts/chartview.py | 131 +++++++++++++++++++++++++- 1 file changed, 126 insertions(+), 5 deletions(-) diff --git a/gudpy/gui/widgets/charts/chartview.py b/gudpy/gui/widgets/charts/chartview.py index bc2c53655..a7213b30e 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() @@ -170,8 +241,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. @@ -195,8 +272,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) @@ -205,6 +286,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) @@ -214,6 +300,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 @@ -222,12 +309,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") @@ -271,6 +362,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 ]: @@ -285,6 +377,7 @@ def contextMenuEvent(self, event): ) ) self.menu.addAction(showDCSLevelAction) + # Actions specific to RDF / RDF_CANS. elif ( self.chart().plotMode in [ @@ -315,7 +408,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 [ @@ -342,6 +439,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) @@ -357,10 +455,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( @@ -371,6 +484,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) From 2009003db2758413b3c57a1e8745c686eff9a544 Mon Sep 17 00:00:00 2001 From: Jared Swift Date: Mon, 27 Jun 2022 10:57:59 +0100 Subject: [PATCH 33/38] docs: plotting enums. --- gudpy/gui/widgets/charts/enums.py | 29 ++++++++++++++++++++++++++--- 1 file changed, 26 insertions(+), 3 deletions(-) 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 From f27af81a53639aae6940b2bd9546438a0e5797bd Mon Sep 17 00:00:00 2001 From: Jared Swift Date: Mon, 27 Jun 2022 11:09:39 +0100 Subject: [PATCH 34/38] docs: SamplePlotConfig. --- .../gui/widgets/charts/sample_plot_config.py | 109 ++++++++++++++++++ 1 file changed, 109 insertions(+) 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, From 1b1252275f17587659f2c74349cc67850e66d402 Mon Sep 17 00:00:00 2001 From: Jared Swift Date: Mon, 27 Jun 2022 11:35:40 +0100 Subject: [PATCH 35/38] docs: SamplePlotData. --- gudpy/gui/widgets/charts/sample_plot_data.py | 315 ++++++++++++++++++- 1 file changed, 310 insertions(+), 5 deletions(-) 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) From 27df4bcc0a922fbabfd244407319f94cfb35bcaf Mon Sep 17 00:00:00 2001 From: Jared Swift Date: Mon, 27 Jun 2022 11:50:12 +0100 Subject: [PATCH 36/38] docs: ExponentialSpinBox. --- gudpy/gui/widgets/core/exponential_spinbox.py | 85 +++++++++++++++++-- 1 file changed, 79 insertions(+), 6 deletions(-) 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) From b131afc5937d73d8983b75a68d873418d2153892 Mon Sep 17 00:00:00 2001 From: Jared Swift Date: Mon, 27 Jun 2022 12:14:17 +0100 Subject: [PATCH 37/38] docs: GudPyTree. --- gudpy/gui/widgets/core/gudpy_tree.py | 193 ++++++++++++++++++++------- 1 file changed, 148 insertions(+), 45 deletions(-) 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) From 13297b8d4d538c547a108b31f22adcc646c92f39 Mon Sep 17 00:00:00 2001 From: Jared Swift Date: Mon, 27 Jun 2022 13:13:02 +0100 Subject: [PATCH 38/38] docs: WIP main window. --- gudpy/gui/widgets/core/main_window.py | 178 +++++++++++++++++++++++++- 1 file changed, 173 insertions(+), 5 deletions(-) diff --git a/gudpy/gui/widgets/core/main_window.py b/gudpy/gui/widgets/core/main_window.py index 69fc3d2f7..9d73d5b99 100644 --- a/gudpy/gui/widgets/core/main_window.py +++ b/gudpy/gui/widgets/core/main_window.py @@ -120,28 +120,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. @@ -172,8 +317,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( @@ -189,6 +336,7 @@ def initComponents(self): ) ) + # Register custom widgets. loader = QUiLoader() loader.registerCustomWidget(GudPyTreeView) loader.registerCustomWidget(OutputTreeView) @@ -220,11 +368,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 ) @@ -232,6 +383,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 ) @@ -247,6 +400,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 ) @@ -267,6 +421,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 ) @@ -280,6 +435,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 ) @@ -288,6 +444,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( @@ -296,6 +453,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 ) @@ -304,6 +462,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 ) @@ -314,6 +473,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 ) @@ -324,12 +484,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 [ @@ -343,6 +506,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 @@ -353,6 +517,7 @@ def initComponents(self): self.handleSamplePlotModeChanged ) + # Populate thecontainer plot combo box. for plotMode in [ plotMode for plotMode in PlotModes if "(Cans)" in plotMode.name ]: @@ -364,6 +529,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) @@ -376,6 +542,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_ )