From a35bffec23e87ba0eec80ca7b6b259f073bc6b05 Mon Sep 17 00:00:00 2001 From: "Brian R. Pauw" Date: Mon, 26 Sep 2022 19:36:36 +0200 Subject: [PATCH 1/3] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ffa8fd1..6245e82 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ McSAS3 (a refactored version of the original McSAS) fits scattering patterns to 6. Some bugs remain. Feel free to add bugs to the issues. They will be fixed as time permits. ## Installation -This package can be installed by ensuring that 1) you have SasModels (pip install sasmodels) and 2) the most recent 21.4+ version of attrs. After that, you can do +This package can be installed by ensuring that 1) you have SasModels (pip install sasmodels) and 2) the most recent 21.4+ version of attrs, as well as pandas. After that, you can do ```git clone https://github.com/BAMresearch/McSAS3.git``` in an appropriate location to install McSAS3 On Windows, if you want to use the sasmodels library, it is highly recommended to run ```pip install tinycc``` so that there's a compatible compiler available. From 617c44bb9fde6eeb2828c05895cb321b663bc2a0 Mon Sep 17 00:00:00 2001 From: Brian Pauw Date: Tue, 11 Oct 2022 18:05:44 +0200 Subject: [PATCH 2/3] Added type hints and reran unit tests. --- mcsas3/McData.py | 56 ++++++++++------------------ mcsas3/McData1D.py | 18 ++++----- mcsas3/McData2D.py | 44 ++++++---------------- mcsas3/McHDF.py | 8 ++-- mcsas3/McHat.py | 15 ++++---- mcsas3/__init__.py | 4 -- mcsas3/mcanalysis.py | 38 +++++++++---------- mcsas3/mccore.py | 51 ++++++++++++-------------- mcsas3/mcmodel.py | 69 +++++++++++++++++------------------ mcsas3/mcmodelhistogrammer.py | 14 +++---- mcsas3/mcopt.py | 10 +++-- 11 files changed, 141 insertions(+), 186 deletions(-) diff --git a/mcsas3/McData.py b/mcsas3/McData.py index 9d6e747..73d8b1c 100644 --- a/mcsas3/McData.py +++ b/mcsas3/McData.py @@ -1,9 +1,11 @@ +from ast import Str import numpy as np import pandas import h5py from mcsas3.McHDF import McHDF from pathlib import Path +# todo use attrs to @define a McData dataclass class McData(McHDF): """ @@ -63,9 +65,9 @@ def __init__( self, df: pandas.DataFrame = None, loadFromFile: Path = None, - resultIndex=1, - **kwargs, - ): + resultIndex:int=1, + **kwargs:dict, + )-> None: # reset everything so we're sure not to inherit anything from elsewhere: self.filename = None # input filename @@ -94,16 +96,16 @@ def __init__( if loadFromFile is not None: self.load(loadFromFile) - def processKwargs(self, **kwargs): + def processKwargs(self, **kwargs:dict)->None: for key, value in kwargs.items(): assert key in self.storeKeys, "Key {} is not a valid option".format(key) setattr(self, key, value) - def linkMeasData(self, measDataLink=None): + def linkMeasData(self, measDataLink:str=None)-> None: assert False, "defined in 1D and 2D subclasses" pass - def from_file(self, filename=None): + def from_file(self, filename:Path=None)->None: if filename is None: assert ( self.filename is not None @@ -133,15 +135,15 @@ def from_file(self, filename=None): False ), "Input file type could not be determined. Use from_pandas to load a dataframe or use df = [DataFrame] in input, or use 'loader' = 'from_pdh' or 'from_csv' in input" - def from_pandas(self, df=None): + def from_pandas(self, df:pandas.DataFrame=None)->None: assert False, "defined in 1D and 2D subclasses" pass - def from_csv(self, filename=None, csvargs=None): + def from_csv(self, filename:Path=None, csvargs=None)->None: assert False, "defined in 1D and 2D subclasses" pass - def from_pdh(self, filename=None): + def from_pdh(self, filename:Path=None)->None: assert False, "defined in 1D subclass only" pass @@ -151,7 +153,7 @@ def from_pdh(self, filename=None): # pass # universal reader for 1D and 2D! - def from_nexus(self, filename=None): + def from_nexus(self, filename:Path=None)->None: # optionally, path can be defined as a dict to point at Q, I and ISigma entries. def objBytesToStr(inObject): outObject = inObject @@ -275,22 +277,22 @@ def objBytesToStr(inObject): self.rawData = pandas.DataFrame(data=self.rawData) self.prepare() - def is2D(self): + def is2D(self)->bool: return self.rawData2D is not None - def clip(self): + def clip(self)->None: assert False, "defined in 1D and 2D subclasses" pass - def omit(self): + def omit(self)->None: assert False, "defined in the 1D and (maybe) 2D subclasses" pass - def reBin(self): + def reBin(self)->None: assert False, "defined in 1D and 2D subclasses" pass - def prepare(self): + def prepare(self)->None: """runs the clipping and binning (in that order), populates clippedData and binnedData""" self.clip() self.omit() @@ -300,7 +302,7 @@ def prepare(self): self.binnedData = self.clippedData.copy() self.linkMeasData() - def store(self, filename=None, path=None): + def store(self, filename:Path=None, path=None)->None: # path:str|None """stores the settings in an output file (HDF5)""" if path is None: path = f"{self.nxsEntryPoint}mcdata/" @@ -309,7 +311,7 @@ def store(self, filename=None, path=None): value = getattr(self, key, None) self._HDFstoreKV(filename=filename, path=path, key=key, value=value) - def load(self, filename: Path = None, path=None): + def load(self, filename: Path = None, path=None)->None: if path is None: path = f"{self.nxsEntryPoint}mcdata/" assert filename is not None @@ -342,24 +344,4 @@ def load(self, filename: Path = None, path=None): self.from_file() # try loading the data from the original file self.prepare() - # ### functions to extend the use of McData class to simulated model data - # def polate (self): - # """ Interpolates and extrapolates the data, for use with scale """ - # assert False, "defined in 1D or 2D subclass" - # pass - - # def interpolate(self, method = None): - # """ Interpolates the data, for use with scale """ - # assert False, "defined in 1D or 2D subclass" - # pass - - # def scale(self, Rscale:float = 1.): - # """ scales the dataset in Q to "pretend" to be an isoaxial R-scaling""" - # assert False, "defined in 1D or 2D subclass" - # pass - - # def extrapolate(self, method = None): - # """ extrapolates the dataset beyond min and max (for use with scale) """ - # assert False, "defined in 1D or 2D subclass" - # pass diff --git a/mcsas3/McData1D.py b/mcsas3/McData1D.py index 0162719..0082611 100644 --- a/mcsas3/McData1D.py +++ b/mcsas3/McData1D.py @@ -14,8 +14,8 @@ class McData1D(McData): omitQRanges = None # to skip or omit unwanted data ranges, for example with sharp XRD peaks def __init__( - self, df: pandas.DataFrame = None, loadFromFile: Path = None, resultIndex = 1, **kwargs - ): + self, df: pandas.DataFrame = None, loadFromFile: Path = None, resultIndex:int = 1, **kwargs:dict + )-> None: super().__init__(loadFromFile=loadFromFile, resultIndex=resultIndex, **kwargs) self.csvargs = { "sep": r"\s+", @@ -38,7 +38,7 @@ def __init__( self.from_file(self.filename) # link measData to the requested value - def linkMeasData(self, measDataLink=None): + def linkMeasData(self, measDataLink=None)->None: # measDataLink:str|None if measDataLink is None: measDataLink = self.measDataLink assert measDataLink in [ @@ -53,7 +53,7 @@ def linkMeasData(self, measDataLink=None): ISigma=measDataObj.ISigma.values, ) - def from_pdh(self, filename=None): + def from_pdh(self, filename:Path=None)->None: """reads from a PDH file, re-uses Ingo Bressler's code from the notebook example""" assert filename is not None, "from_pdh requires an input filename of a PDH file" skiprows, nrows = 5, -1 @@ -65,7 +65,7 @@ def from_pdh(self, filename=None): csvargs.update({"skiprows": skiprows, "nrows": nrows[0] - skiprows}) self.from_pandas(pandas.read_csv(filename, **csvargs)) - def from_pandas(self, df=None): + def from_pandas(self, df:pandas.DataFrame=None)->None: """uses a dataframe as input, should contain 'Q', 'I', and 'ISigma'""" assert isinstance( df, pandas.DataFrame @@ -80,14 +80,14 @@ def from_pandas(self, df=None): self.rawData = df self.prepare() - def from_csv(self, filename, csvargs={}): + def from_csv(self, filename:Path, csvargs:dict={})->None: """reads from a three-column csv file, takes pandas from_csv arguments""" assert filename is not None, "from_csv requires an input filename of a csv file" localCsvargs = self.csvargs.copy() localCsvargs.update(csvargs) self.from_pandas(pandas.read_csv(filename, **localCsvargs)) - def clip(self): + def clip(self)->None: self.clippedData = ( self.rawData.query(f"{self.dataRange[0]} <= Q < {self.dataRange[1]}") .dropna() @@ -97,7 +97,7 @@ def clip(self): len(self.clippedData) != 0 ), "Data clipping range too small, no datapoints found!" - def omit(self): + def omit(self)->None: # this can skip/omit unwanted ranges of data (for example a data range with an unwanted XRD peak in it) # requires an "omitQRanges" list of [[qmin, qmax]]-data ranges to omit @@ -112,7 +112,7 @@ def omit(self): inplace=True ) - def reBin(self, nbins=None, IEMin=0.01, QEMin=0.01): + def reBin(self, nbins=None, IEMin:float=0.01, QEMin:float=0.01) -> None: # nbins:int|None """Unweighted rebinning funcionality with extended uncertainty estimation, adapted from the datamerge methods, as implemented in Paulina's notebook of spring 2020""" if nbins is None: nbins = self.nbins diff --git a/mcsas3/McData2D.py b/mcsas3/McData2D.py index ae026e0..4e74ef9 100644 --- a/mcsas3/McData2D.py +++ b/mcsas3/McData2D.py @@ -3,6 +3,7 @@ from .McData import McData import h5py import logging +from pathlib import Path class McData2D(McData): """subclass for managing 2D datasets. Copied from 1D dataset handler, not every functionality is enabled""" @@ -20,13 +21,9 @@ class McData2D(McData): 0, ] # nudge in direction 0 and 1 in case of misaligned centers. Applied to measData - def __init__(self, df=None, loadFromFile=None, resultIndex=1, **kwargs): + def __init__(self, df=None, loadFromFile=None, resultIndex:int=1, **kwargs:dict)-> None: super().__init__(resultIndex=resultIndex, **kwargs) - self.csvargs = { - "sep": r"\s+", - "header": None, - "names": ["Q", "I", "ISigma"], - } # default for 1D, overwritten in subclass + self.csvargs = {} # not sure you'd want to load 2D from a CSV.... though I've seen stranger things self.dataRange = [0, np.inf] # min-max for data range to fit self.orthoQ1Range = [0, np.inf] self.orthoQ0Range = [0, np.inf] @@ -38,11 +35,12 @@ def __init__(self, df=None, loadFromFile=None, resultIndex=1, **kwargs): self.loader = "from_pandas" # TODO: need to handle this on restore state self.from_pandas(df) + # TODO not sure why loadFromFile is not used.. elif self.filename is not None: # filename has been set self.from_file(self.filename) # link measData to the requested value - def linkMeasData(self, measDataLink=None): + def linkMeasData(self, measDataLink=None)-> None: if measDataLink is None: measDataLink = self.measDataLink assert measDataLink in [ @@ -60,35 +58,15 @@ def linkMeasData(self, measDataLink=None): ISigma=measDataObj["ISigma"], ) - def from_pandas(self, df=None): + def from_pandas(self, df:pandas.DataFrame=None)->None: assert False, "2D data from_pandas not implemented yet" pass - # """uses a dataframe as input, should contain 'Q', 'I', and 'ISigma'""" - # assert isinstance( - # df, pandas.DataFrame - # ), "from_pandas requires a pandas DataFrame with 'Q', 'I', and 'ISigma'" - # # maybe add a check for the keys: - # assert all( - # [key in df.keys() for key in ["Q", "I", "ISigma"]] - # ), "from_pandas requires the dataframe to contain 'Q', 'I', and 'ISigma'" - # assert all( - # [df[key].dtype.kind in 'f' for key in ["Q", "I", "ISigma"]] - # ), "data could not be read correctly. If csv, did you supply the right csvargs?" - # self.rawData = df - # self.prepare() - - def from_csv(self, filename, csvargs={}): + def from_csv(self, filename:Path, csvargs:dict={})->None: assert False, "2D data from_csv not implemented yet" pass - # """reads from a three-column csv file, takes pandas from_csv arguments""" - # assert filename is not None, "from_csv requires an input filename of a csv file" - # localCsvargs = self.csvargs.copy() - # localCsvargs.update(csvargs) - # self.from_pandas(pandas.read_csv(filename, **localCsvargs)) - - def clip(self): + def clip(self) -> None: # copied from a jupyter notebook: # test with directly imported data @@ -162,13 +140,13 @@ def clip(self): (self.clippedData["Q"][1]).max(), ] - def omit(self): + def omit(self)-> None: # this can skip/omit unwanted ranges of data (for example a data range with an unwanted XRD peak in it) # requires an "omitQRanges" list of [[qmin, qmax]]-data ranges to omit logging.warning("Omitting ranges not implemented yet for 2D") pass - def reconstruct2D(self, modelI1D): + def reconstruct2D(self, modelI1D: np.ndarray) -> np.ndarray: """ Reconstructs a masked 2D data array from the (1D) model intensity, skipping the masked and clipped pixels (left as NaN) This function can be used to plot the resulting model intensity and comparing it with self.clippedData["I2D"] @@ -178,7 +156,7 @@ def reconstruct2D(self, modelI1D): RMI[np.where(self.clippedData["invMask"])] = modelI1D return RMI - def reBin(self, nbins=None, IEMin=0.01, QEMin=0.01): + def reBin(self, nbins:int=None, IEMin:float=0.01, QEMin:float=0.01)->None: print("2D data rebinning not implemented, binnedData = clippedData for now") self.binnedData = self.clippedData diff --git a/mcsas3/McHDF.py b/mcsas3/McHDF.py index ca8a938..33bf9ce 100644 --- a/mcsas3/McHDF.py +++ b/mcsas3/McHDF.py @@ -11,10 +11,10 @@ class McHDF(object): resultIndex = 1 # per default number 1, but can be changed. nxsEntryPoint = f"/analyses/MCResult{resultIndex}/" # changed to full path to result, not sure if this can work like this - def __init__(self): + def __init__(self)->None: pass - def _HDFSetResultIndex(self, resultIndex=None): + def _HDFSetResultIndex(self, resultIndex:int=None)->None: # resultIndex = -1 should go to the last existing one # assert ( @@ -26,7 +26,7 @@ def _HDFSetResultIndex(self, resultIndex=None): self.resultIndex = resultIndex self.nxsEntryPoint = f"/analyses/MCResult{self.resultIndex}/" - def _HDFloadKV(self, filename=None, path=None, datatype=None, default=None): + def _HDFloadKV(self, filename:Path=None, path: str = None, datatype=None, default=None): # outputs any hdf5 value type with h5py.File(filename, "r") as h5f: if path not in h5f: return default @@ -86,7 +86,7 @@ def _HDFloadKV(self, filename=None, path=None, datatype=None, default=None): return value - def _HDFstoreKV(self, filename=None, path=None, key=None, value=None): + def _HDFstoreKV(self, filename:Path=None, path:str=None, key:str=None, value=None)->None: assert filename is not None, "filename (output filename) cannot be empty" assert path is not None, "HDF5 path cannot be empty" assert key is not None, "key cannot be empty" diff --git a/mcsas3/McHat.py b/mcsas3/McHat.py index 52070df..6a52004 100644 --- a/mcsas3/McHat.py +++ b/mcsas3/McHat.py @@ -6,6 +6,7 @@ from .mcmodel import McModel from .mccore import McCore from io import StringIO +from pathlib import Path STORE_LOCK = None @@ -14,7 +15,7 @@ def initStoreLock(lock): global STORE_LOCK STORE_LOCK = lock - +# TODO: use attrs to @define a McHat dataclass class McHat(McHDF): """ The hat sits on top of the McCore. It takes care of parallel processing of each repetition. @@ -35,7 +36,7 @@ class McHat(McHDF): ] loadKeys = storeKeys - def __init__(self, loadFromFile=None, resultIndex=1, **kwargs): + def __init__(self, loadFromFile=None, resultIndex:int=1, **kwargs:dict) -> None: # reset to make sure we're not inheriting any settings from another instance: self._measData = None # measurement data dict with entries for Q, I, ISigma @@ -70,7 +71,7 @@ def __init__(self, loadFromFile=None, resultIndex=1, **kwargs): setattr(self, key, value) assert self.nRep > 0, "Must optimize for at least one repetition" - def fillFitParameterLimits(self, measData): + def fillFitParameterLimits(self, measData:dict)-> None: for key, val in self._modelArgs["fitParameterLimits"].items(): if isinstance(val, str): assert ( @@ -85,7 +86,7 @@ def fillFitParameterLimits(self, measData): np.pi / np.min(measData["Q"]), ] - def run(self, measData=None, filename=None, resultIndex=1): + def run(self, measData:dict=None, filename:Path=None, resultIndex:int=1)->None: """runs the full sequence: multiple repetitions of optimizations, to be parallelized. This probably needs to be taken out of core, and into a new parent""" @@ -123,7 +124,7 @@ def run(self, measData=None, filename=None, resultIndex=1): for output in sorted(outputs, key=lambda x: x[0]): print(output) - def runOnce(self, measData=None, filename=None, repetition=0, bufferStdIO=False, resultIndex=1): + def runOnce(self, measData:dict=None, filename:Path=None, repetition:int=0, bufferStdIO:bool=False, resultIndex:int=1) -> None: """runs the full sequence: multiple repetitions of optimizations, to be parallelized. This probably needs to be taken out of core, and into a new parent""" if bufferStdIO: @@ -161,7 +162,7 @@ def runOnce(self, measData=None, filename=None, repetition=0, bufferStdIO=False, return sys.stdout.getvalue() return - def store(self, filename=None, path=None): + def store(self, filename:Path=None, path:str=None)-> None: if path is None: path = f"{self.nxsEntryPoint}optimization/" """stores the settings in an output file (HDF5)""" @@ -170,7 +171,7 @@ def store(self, filename=None, path=None): value = getattr(self, key, None) self._HDFstoreKV(filename=filename, path=path, key=key, value=value) - def load(self, filename=None, path=None): + def load(self, filename:Path=None, path=None) -> None: #path:str|None if path is None: path = f"{self.nxsEntryPoint}optimization/" assert filename is not None diff --git a/mcsas3/__init__.py b/mcsas3/__init__.py index 91873d9..eca9685 100644 --- a/mcsas3/__init__.py +++ b/mcsas3/__init__.py @@ -6,9 +6,5 @@ """ import numpy as np -# from .osb import optimizeScalingAndBackground -# from .mccore import McCore -# from .mcmodel import McModel -# from .mcopt import McOpt # vim: set ts=4 sts=4 sw=4 tw=0: diff --git a/mcsas3/mcanalysis.py b/mcsas3/mcanalysis.py index a5d81da..018223d 100644 --- a/mcsas3/mcanalysis.py +++ b/mcsas3/mcanalysis.py @@ -8,7 +8,7 @@ import os.path import h5py import matplotlib.pyplot as plt - +from pathlib import Path class McAnalysis(McHDF): """ @@ -58,8 +58,8 @@ class McAnalysis(McHDF): _optKeys = ["scaling", "background", "gof", "accepted", "step"] def __init__( - self, inputFile=None, measData=None, histRanges=None, store=False, resultIndex=1 - ): + self, inputFile:Path=None, measData:dict=None, histRanges:pandas.DataFrame=None, store:bool=False, resultIndex:int=1 + ) -> None: # 1. open the input file, and for every repetition: # 2. set up the model again, and # 3. set up the optimization instance again, and @@ -143,14 +143,14 @@ def __init__( self.store(inputFile) @property - def modelIAvg(self): + def modelIAvg(self) -> pandas.DataFrame: return self._averagedI @property - def optParAvg(self): + def optParAvg(self) -> pandas.DataFrame: return self._averagedOpts - def histAndLoadReps(self, inputFile, store, resultIndex=1): + def histAndLoadReps(self, inputFile:Path, store:bool, resultIndex:int=1) -> None: """ for every repetition, runs its mcModelHistogrammer, and loads the results into the local namespace for further processing @@ -202,7 +202,7 @@ def histAndLoadReps(self, inputFile, store, resultIndex=1): self._concatHistograms[histIndex][repetition] = mh._histDict[histIndex] self._concatBinEdges[histIndex][repetition] = mh._binEdges[histIndex] - def ensureConcatEssentials(self, histIndex): + def ensureConcatEssentials(self, histIndex:int) -> None: """ small function that makes sure at least an empty dataframe exists for appending the concatenated data to""" if not histIndex in self._concatModes: self._concatModes[histIndex] = pandas.DataFrame(columns=self._modeKeys) @@ -211,7 +211,7 @@ def ensureConcatEssentials(self, histIndex): if not histIndex in self._concatBinEdges: self._concatBinEdges[histIndex] = dict() - def averageI(self): + def averageI(self) -> None: self._averagedI = pandas.DataFrame( data={ "modelIMean": np.array([i for k, i in self._concatI.items()]).mean( @@ -223,7 +223,7 @@ def averageI(self): } ) - def averageOpts(self): + def averageOpts(self) -> None: """ combines the multiindex dataframes into a single table with one row per histogram range """ self._averagedOpts = pandas.DataFrame( data={ @@ -232,14 +232,14 @@ def averageOpts(self): } ) - def averageModes(self): + def averageModes(self) -> None: """ combines the multiindex dataframes into a single table with one row per histogram range """ dfs = dict() for histIndex, histRange in self._histRanges.iterrows(): dfs[histIndex] = self.averageMode(histIndex) self._averagedModes = pandas.DataFrame(data=dfs).T - def averageMode(self, histIndex): + def averageMode(self, histIndex:int) -> pandas.DataFrame: """ Calculates the mean and standard deviation for each mode, for a particular repetition index, and returns a multiindex DataFrame @@ -252,7 +252,7 @@ def averageMode(self, histIndex): ) return df.stack() - def averageHistograms(self): + def averageHistograms(self) -> None: """ averages all the histogram ranges sequentially and stores the averaged histograms in a dict with {histIndex: histogram DataFrame} """ @@ -262,7 +262,7 @@ def averageHistograms(self): for key in aH.keys(): self._averagedHistograms[histIndex][key].astype(aH[key].dtype) - def averageHistogram(self, histIndex): + def averageHistogram(self, histIndex:int)-> None: """ produces a single averaged histogram for a given histogram range index. returns a dataframe """ # these are the columns and datatypes I want in my histograms. forced datatypes to prevent issues later on when storing cols = { @@ -308,7 +308,7 @@ def averageHistogram(self, histIndex): return averagedHistogram - def debugPlot(self, histIndex, **kwargs): + def debugPlot(self, histIndex:int , **kwargs:dict)->None: """ plots a single histogram, for debugging purposes only, can only be done after histogramming is complete""" histDataFrame = self._averagedHistograms[histIndex] plt.bar( @@ -323,7 +323,7 @@ def debugPlot(self, histIndex, **kwargs): if self._histRanges.loc[histIndex].binScale == "log": plt.xscale("log") - def debugReport(self, histIndex): + def debugReport(self, histIndex:int)->str: """ Preformats the rangeInfo results ready for printing (mostly translated from the original McSAS). Should be plotted with a fixed-width font because nothing says 2020 like misaligned text. @@ -340,7 +340,7 @@ def debugReport(self, histIndex): oString += self.debugAddString(fieldName, valMean, valStd) return oString - def debugAddString(self, fieldName, valMean, valStd): + def debugAddString(self, fieldName:str, valMean:float, valStd:float)->str: # not sure if val* needs to be float or more generic numeric types # does a bit of error checking to avoid division by zero for debug*Report methods if valMean != 0: oString = f"{fieldName.ljust(10)}: {valMean: 0.02e} ± {valStd: 0.02e} (± {valStd/valMean * 100: 0.02f} %) \n" @@ -348,7 +348,7 @@ def debugAddString(self, fieldName, valMean, valStd): oString = f"{fieldName.ljust(10)}: {valMean: 0.02e} ± {valStd: 0.02e} \n" return oString - def debugRunReport(self): + def debugRunReport(self) -> str: """ Preformats the run statistics results ready for printing (mostly translated from the original McSAS). Should be plotted with a fixed-width font because nothing says 2020 like misaligned text. @@ -364,7 +364,7 @@ def debugRunReport(self): return oString - def getNRep(self, inputFile): + def getNRep(self, inputFile:Path) -> None: """ Finds out which repetition indices are available in the results file, skipping potential missing indices note : repetition must be int""" self._repetitionList = [] # reinitialize to zero @@ -376,7 +376,7 @@ def getNRep(self, inputFile): f"{len(self._repetitionList)} repetitions found in McSAS file {inputFile}" ) - def store(self, filename=None): + def store(self, filename:Path=None) -> None: # store averaged histograms, for arhcival purposes only, these settings are not planned to be reused.: oDict = self._averagedHistograms.copy() # .to_dict(orient="index") for key in oDict.keys(): diff --git a/mcsas3/mccore.py b/mcsas3/mccore.py index afa9816..a3363fd 100644 --- a/mcsas3/mccore.py +++ b/mcsas3/mccore.py @@ -7,7 +7,7 @@ # import scipy.optimize from .McHDF import McHDF - +from pathlib import Path class McCore(McHDF): """ @@ -29,19 +29,19 @@ class McCore(McHDF): def __init__( self, - measData=None, - model=None, - opt=None, - loadFromFile=None, - loadFromRepetition=None, - resultIndex=1, + measData:dict=None, + model:McModel=None, + opt:McOpt=None, + loadFromFile=None, # Path|None + loadFromRepetition=None, # int|None + resultIndex:int=1, ): # make sure we reset state: - self._measData = None # measurement data dict with entries for Q, I, ISigma - self._model = None # instance of McModel - self._opt = None # instance of McOpt - self._OSB = None # optimizeScalingAndBackground instance for this data - self._outputFilename = None # store output data in here (HDF5) + self._measData = None + self._model = None + self._opt = None + self._OSB = None + self._outputFilename = None assert measData is not None, "measurement data must be provided to McCore" assert isinstance( @@ -131,7 +131,7 @@ def __init__( # """ # return self._model.kernel.result[self._model.kernel.q_input.nq] - def initModelI(self): + def initModelI(self) -> None: """calculate the total intensity from all contributions""" # set initial shape: I, V = self._model.calcModelIV(self._model.parameterSet.loc[0].to_dict()) @@ -154,7 +154,7 @@ def initModelI(self): def evaluate( self, testData=None - ): # , initial: bool = True): # takes 20 ms! initial is taken care of in osb when x0 is None + )->float: # , initial: bool = True): # takes 20 ms! initial is taken care of in osb when x0 is None """scale and calculate goodness-of-fit (GOF) from all contributions""" if testData is None: testData = self._opt.modelI @@ -163,40 +163,37 @@ def evaluate( self._opt.testX0, gof = self._OSB.match(testData, self._opt.x0) return gof - def contribIndex(self): + def contribIndex(self)->int: return self._opt.step % self._model.nContrib - def reEvaluate(self): + def reEvaluate(self)->float: """replace single contribution with new contribution, recalculate intensity and GOF""" # calculate old intensity to subtract: Iold, dummy = self._model.calcModelIV( self._model.parameterSet.loc[self.contribIndex()].to_dict() ) - # not needed: - # Vold = self.returnModelV() # = self._model.volumes[self._opt.contribIndex()] # calculate new intensity to add: Ipick, Vpick = self._model.calcModelIV(self._model.pickParameters) - # Vpick = self.returnModelV() # remove intensity from contribi from modelI # add intensity from Pick self._opt.testModelI = self._opt.modelI + ( Ipick - Iold - ) # / self._model.nContrib + ) # store pick volume in temporary location self._opt.testModelV = Vpick # recalculate reduced chi-squared for this option return self.evaluate(self._opt.testModelI) - def reject(self): + def reject(self) -> None: """reject pick""" # nothing to do. Can be used to fish out a running rejection/acceptance ratio later pass - def accept(self): + def accept(self) -> None: """accept pick""" # store parameters of accepted pick: self._model.parameterSet.loc[self.contribIndex()] = self._model.pickParameters @@ -211,7 +208,7 @@ def accept(self): # add one to the accepted moves counter: self._opt.accepted += 1 - def iterate(self): + def iterate(self) -> None: """pick, re-evaluate and accept/reject""" # pick new model parameters: self._model.pick() # 3 µs @@ -226,7 +223,7 @@ def iterate(self): # increment step counter in either case: self._opt.step += 1 - def optimize(self): + def optimize(self) -> None: """iterate until target GOF or maxiter reached""" print("Optimization of repetition {} started:".format(self._opt.repetition)) print( @@ -250,9 +247,9 @@ def optimize(self): ) ) - def store(self, filename=None): + def store(self, filename:Path=None) -> None: """stores the resulting model parameter-set of a single repetition in the NXcanSAS object, ready for histogramming""" - # not finished + self._outputFilename = filename self._model.store( filename=self._outputFilename, repetition=self._opt.repetition @@ -262,7 +259,7 @@ def store(self, filename=None): path=f"{self.nxsEntryPoint}optimization/repetition{self._opt.repetition}/", ) - def load(self, loadFromFile=None, loadFromRepetition=None, resultIndex=1): + def load(self, loadFromFile:Path=None, loadFromRepetition:int=None, resultIndex:int=1) -> None: """loads the configuration and set-up from the extended NXcanSAS file""" # not implemented yet assert ( diff --git a/mcsas3/mcmodel.py b/mcsas3/mcmodel.py index 654c48b..d3db6aa 100644 --- a/mcsas3/mcmodel.py +++ b/mcsas3/mcmodel.py @@ -1,11 +1,14 @@ +from typing import Tuple, List import pandas import numpy as np from .McHDF import McHDF import sasmodels import sasmodels.core, sasmodels.direct_model from scipy import interpolate +from pathlib import Path +# TODO: perhaps better defined as a dataclass with attrs class sphereParameters(object): # micro-class to mimick the nested structure of SasModels in simulation model: defaults = { @@ -16,15 +19,15 @@ class sphereParameters(object): "radius": 1, } - def __init__(self): + def __init__(self)->None: pass - +#ibid. class sphereInfo(object): # micro-class to mimick the nested structure of SasModels in simulation model: parameters = sphereParameters() - def __init__(self): + def __init__(self)->None: pass @@ -40,7 +43,7 @@ class mcsasSphereModel(object): measQ = None # needs to be set later when initializing info = sphereInfo() - def __init__(self, **kwargs): + def __init__(self, **kwargs:dict)->None: # reset values to make sure we're not inheriting anything from another instance: self.sld = 1 # input SLD in units of 1e-6 1/A^2. @@ -60,11 +63,11 @@ def __init__(self, **kwargs): setattr(self, key, value) # assert all([key in kwargs.keys() for key in ['simDataQ0', 'simDataQ1', 'simDataI', 'simDataISigma']]), 'The following input arguments must be provided to describe the simulation data: simDataQ0, simDataQ1, simDataI, simDataISigma' - def make_kernel(self, measQ: np.ndarray = None): + def make_kernel(self, measQ: np.ndarray = None): # not sure of the output type... sasmodel? self.measQ = measQ return self.kernelfunc - def kernelfunc(self, **parDict): + def kernelfunc(self, **parDict:dict)-> Tuple[np.ndarray, np.ndarray]: # print('stop here. see what we have. return I, V') qr = self.measQ[0] * parDict["radius"] F = 3.0 * (np.sin(qr) - qr * np.cos(qr)) / (qr ** 3.0) @@ -78,7 +81,7 @@ def kernelfunc(self, **parDict): ) return I, V - +# ibid. class simParameters(object): # micro-class to mimick the nested structure of SasModels in simulation model: defaults = { @@ -93,7 +96,7 @@ class simParameters(object): def __init__(self): pass - +#ibid. class simInfo(object): # micro-class to mimick the nested structure of SasModels in simulation model: parameters = simParameters() @@ -101,7 +104,7 @@ class simInfo(object): def __init__(self): pass - +# ibid. class McSimPseudoModel(object): """ pretends to be a sasmodel """ @@ -125,7 +128,7 @@ class McSimPseudoModel(object): measQ = None # needs to be set later when initializing info = simInfo() - def __init__(self, **kwargs): + def __init__(self, **kwargs:dict)->None: # reset values to make sure we're not inheriting anything from another instance: self.extrapY0 = None @@ -178,29 +181,24 @@ def __init__(self, **kwargs): fill_value=(self.simDataISigma[0], np.nan), ) - def make_kernel(self, measQ: np.ndarray = None): + def make_kernel(self, measQ: np.ndarray = None): # return type? self.measQ = measQ return self.kernelfunc # create extrapolator, based on the previously determined fit values: - def extrapolatorHighQ(self, Q): + def extrapolatorHighQ(self, Q:np.ndarray)-> np.ndarray: y0 = self.extrapY0 # 2.21e-09 scaling = self.extrapScaling # 9.61e+01 return y0 + Q ** (-4) * scaling - def kernelfunc(self, **parDict): + def kernelfunc(self, **parDict:dict) -> Tuple[np.ndarray, np.ndarray]: # print('stop here. see what we have. return I, V') return self.interpscale(Rscale=parDict["factor"]) def interpscale( self, - # measQ, # Q vector of measurement data to which answers should be mapped -> is self.measQ - # simulation, # dictionary with "Q", "I", "ISigma" of simulation. Q is a two-element array with Qx, Qy, or for 1D data: Qx, None - # Ipolator = None, # interpolator function for I - # ISpolator = None, # interpolator function for ISigma - # extrapolator=None, # extrapolator function for high Q. Rscale: float = 1.0, # scaling factor for the data. fitting parameter. - ): + )->Tuple[np.ndarray, np.ndarray]: # calculate scaled intensity: qScaled = self.measQ[0] * Rscale @@ -220,6 +218,7 @@ def interpscale( return scaledSim["I"] * Rscale ** 6, Rscale ** 3 +# TODO: replace with attrs @define'd dataclass: class McModel(McHDF): """ Specifies the fit parameter details and contains random pickers. Configuration can be alternatively loaded from an existing result file. @@ -270,12 +269,12 @@ class McModel(McHDF): "seed", ] - def fitKeys(self): + def fitKeys(self) -> List[str]: return [key for key in self.fitParameterLimits.keys()] def __init__( - self, loadFromFile=None, loadFromRepetition=None, resultIndex=1, **kwargs - ): + self, loadFromFile=None, loadFromRepetition=None, resultIndex:int=1, **kwargs:dict + )->None: # reset everything so we're sure not to inherit anything from another instance: self.func = None # SasModels model instance @@ -333,7 +332,7 @@ def __init__( self.checkSettings() - def checkSettings(self): + def checkSettings(self) -> None: for key in self.settables: if key in ("seed",): continue @@ -345,7 +344,7 @@ def checkSettings(self): assert self.func is not None, "SasModels function has not been loaded" assert self.parameterSet is not None, "parameterSet has not been initialized" - def calcModelIV(self, parameters): + def calcModelIV(self, parameters:dict)-> Tuple[np.ndarray, np.ndarray]: # moved from McCore if (self.modelName.lower() != "sim") and ( self.modelName.lower() != "mcsas_sphere" @@ -374,11 +373,11 @@ def calcModelIV(self, parameters): # return Fsq / V_shell / (4 / 3 * np.pi), V_shell return Fsq / V_shell, V_shell - def pick(self): + def pick(self) -> None: """pick new random model parameter""" self.pickParameters = self.generateRandomParameterValues() - def generateRandomParameterValues(self): + def generateRandomParameterValues(self) -> None: """to be depreciated as soon as models can generate their own...""" # initialize dict with parameter-value pairs defaulting to None returnDict = dict.fromkeys([key for key in self.fitParameterLimits]) @@ -389,7 +388,7 @@ def generateRandomParameterValues(self): returnDict[parName] = self.randomGenerators[parName](upper, lower) return returnDict - def resetParameterSet(self): + def resetParameterSet(self) -> None: """fills the model parameter values with random values""" for contribi in range(self.nContrib): # can be improved with a list comprehension, but this only executes once.. @@ -397,7 +396,7 @@ def resetParameterSet(self): ####### Loading and Storing functions: ######## - def load(self, loadFromFile=None, loadFromRepetition=None): + def load(self, loadFromFile:Path=None, loadFromRepetition:int=None) -> None: """ loads a preset set of contributions from a previous optimization, stored in HDF5 nContrib is reset to the length of the previous optimization. @@ -446,7 +445,7 @@ def load(self, loadFromFile=None, loadFromRepetition=None): self.nContrib = self.parameterSet.shape[0] - def store(self, filename=None, repetition=None): + def store(self, filename:Path=None, repetition:int=None)->None: assert ( repetition is not None ), "Repetition number must be given when storing model parameters into a paramFile" @@ -506,7 +505,7 @@ def store(self, filename=None, repetition=None): ####### SasView SasModel helper functions: ######## - def availableModels(self): + def availableModels(self) -> None: # show me all the available models, 1D and 1D+2D print("\n \n 1D-only SasModel Models:\n") @@ -521,7 +520,7 @@ def availableModels(self): if modelInfo.parameters.has_2d: print("{} is available in 1D and 2D".format(modelInfo.id)) - def modelExists(self): + def modelExists(self) -> bool: return True # todo: this doesn't work anymore when combining models, e.g. sphere@hardsphere # # checks whether the given model name exists, throw exception if not @@ -532,18 +531,18 @@ def modelExists(self): # ) # return True - def loadModel(self): + def loadModel(self) -> None: # loads sasView model and puts the handle in the right place: self.modelExists() # check if model exists self.func = sasmodels.core.load_model(self.modelName, dtype=self.modelDType) - def loadMcsasSphereModel(self): + def loadMcsasSphereModel(self) -> None: self.func = mcsasSphereModel( **self.staticParameters # no arguments here... probably ) - def loadSimModel(self): + def loadSimModel(self) -> None: if not "simDataQ1" in self.staticParameters.keys(): # if it was None when written, it might not exist when loading self.staticParameters.update({"simDataQ1": None}) @@ -558,7 +557,7 @@ def loadSimModel(self): ) # simDataDict= self.staticParameters['simDataDict']) - def showModelParameters(self): + def showModelParameters(self) -> dict: # find out what the parameters are for the set model, e.g.: # mc.showModelParameters() assert ( diff --git a/mcsas3/mcmodelhistogrammer.py b/mcsas3/mcmodelhistogrammer.py index 79ad9ef..f2ffb87 100644 --- a/mcsas3/mcmodelhistogrammer.py +++ b/mcsas3/mcmodelhistogrammer.py @@ -5,7 +5,7 @@ from .mcopt import McOpt import matplotlib.pyplot as plt from .McHDF import McHDF - +from pathlib import Path class McModelHistogrammer(McHDF): """ @@ -54,7 +54,7 @@ class McModelHistogrammer(McHDF): ) # modes of the populations: total, mean, variance, skew, kurtosis _correctionFactor = 1e-5 # scaling factor to switch from SasModel units used in the model instance (1/(cm sr) for dimensions in Angstrom) to absolute units in 1/(m sr) for dimensions in nm - def __init__(self, coreInstance=None, histRanges=None, resultIndex=1): + def __init__(self, coreInstance:McCore=None, histRanges:pandas.DataFrame=None, resultIndex:int=1)-> None: # reset variables, make sure we don't inherit anything from another instance: self._model = None # instance of model to work with @@ -126,7 +126,7 @@ def __init__(self, coreInstance=None, histRanges=None, resultIndex=1): self.histogram(histRange, histIndex) self.modes(histRange, histIndex) - def debugPlot(self, histIndex): + def debugPlot(self, histIndex:int)->None: """ plots a single histogram, for debugging purposes only, can only be done after histogramming is complete""" plt.bar( self._binEdges[histIndex][:-1], @@ -137,7 +137,7 @@ def debugPlot(self, histIndex): if self._histRanges.loc[histIndex].binScale == "log": plt.xscale("log") - def histogram(self, histRange, histIndex): + def histogram(self, histRange:pandas.DataFrame, histIndex:int)->None: """ histograms the data into an individual range """ n, _ = np.histogram( @@ -152,7 +152,7 @@ def histogram(self, histRange, histIndex): n.astype(np.float64) * self._opt.x0[0] * self._correctionFactor ) - def modes(self, histRange, histIndex): + def modes(self, histRange:pandas.DataFrame, histIndex:int)->None: def calcModes(rset, frac): # function taken from the old McSAS code: val = sum(frac) @@ -193,7 +193,7 @@ def calcModes(rset, frac): } ) - def genX(self, histRange, parameterSet, volumes): + def genX(self, histRange: pandas.DataFrame, parameterSet: pandas.DataFrame, volumes:np.ndarray)->np.ndarray: """Generates bin edges""" if histRange.binScale == "linear": binEdges = np.linspace( @@ -217,7 +217,7 @@ def genX(self, histRange, parameterSet, volumes): ) return binEdges - def store(self, filename=None, repetition=None): + def store(self, filename:Path=None, repetition:int=None)->None: # TODO: CHECK USE OF KEYS IN STORE PATH: assert ( repetition is not None diff --git a/mcsas3/mcopt.py b/mcsas3/mcopt.py index c3f0917..d47351a 100644 --- a/mcsas3/mcopt.py +++ b/mcsas3/mcopt.py @@ -1,9 +1,11 @@ import numpy as np import h5py from .McHDF import McHDF +from pathlib import Path +# TODO: refactor this using attrs @define for clearer handling. -class McOpt(McHDF): +class McOpt(McHDF): """Class to store optimization settings and keep track of running variables""" accepted = None # number of accepted picks @@ -52,7 +54,7 @@ class McOpt(McHDF): "acceptedGofs", ] - def __init__(self, loadFromFile=None, resultIndex=1, **kwargs): + def __init__(self, loadFromFile =None, resultIndex:int=1, **kwargs:dict) -> None: # Multiple types (e.g. Path|None ) only supported from Python 3.10 """initializes the options to the MC algorithm, *or* loads them from a previous run. Note: If the parameters are loaded from a previous file, any additional key-value pairs are updated. """ @@ -89,7 +91,7 @@ def __init__(self, loadFromFile=None, resultIndex=1, **kwargs): assert key in self.storeKeys, "Key {} is not a valid option".format(key) setattr(self, key, value) - def store(self, filename=None, path=None): + def store(self, filename:Path=None, path=None) -> None: # Multiple types (e.g. Path|None ) only supported from Python 3.10 """stores the settings in an output file (HDF5)""" if path is None: path = f"{self.nxsEntryPoint}optimization/" @@ -98,7 +100,7 @@ def store(self, filename=None, path=None): value = getattr(self, key, None) self._HDFstoreKV(filename=filename, path=path, key=key, value=value) - def load(self, filename=None, repetition=None, path=None): + def load(self, filename:Path=None, repetition=None, path=None) -> None:# Multiple types (e.g. Path|None ) only supported from Python 3.10 if path is None: path = f"{self.nxsEntryPoint}optimization/" if repetition is None: From a47b993a191a7504c8505d8a42dd43cddcd2ca48 Mon Sep 17 00:00:00 2001 From: Brian Pauw Date: Thu, 13 Oct 2022 16:42:09 +0200 Subject: [PATCH 3/3] Finished typing. Modified tests 2 run concurrently --- mcsas3/McData.py | 13 ++--- mcsas3/McData1D.py | 11 +++-- mcsas3/McData2D.py | 5 +- mcsas3/McHDF.py | 6 +-- mcsas3/McHat.py | 11 +++-- mcsas3/McPlot.py | 8 ++-- mcsas3/__init__.py | 2 +- mcsas3/mcanalysis.py | 4 +- mcsas3/mccore.py | 40 +++------------- mcsas3/mcmodel.py | 11 +++-- mcsas3/mcmodelhistogrammer.py | 4 +- mcsas3/mcopt.py | 7 +-- test_McData1D_unittest.py | 12 ++--- test_optimizer_integraltest.py | 88 ++++++++++++++++------------------ 14 files changed, 98 insertions(+), 124 deletions(-) diff --git a/mcsas3/McData.py b/mcsas3/McData.py index 73d8b1c..309ce06 100644 --- a/mcsas3/McData.py +++ b/mcsas3/McData.py @@ -1,4 +1,5 @@ from ast import Str +from typing import Optional import numpy as np import pandas import h5py @@ -63,8 +64,8 @@ class McData(McHDF): def __init__( self, - df: pandas.DataFrame = None, - loadFromFile: Path = None, + df: Optional[pandas.DataFrame] = None, + loadFromFile: Optional[Path] = None, resultIndex:int=1, **kwargs:dict, )-> None: @@ -105,7 +106,7 @@ def linkMeasData(self, measDataLink:str=None)-> None: assert False, "defined in 1D and 2D subclasses" pass - def from_file(self, filename:Path=None)->None: + def from_file(self, filename:Optional[Path]=None)->None: if filename is None: assert ( self.filename is not None @@ -153,7 +154,7 @@ def from_pdh(self, filename:Path=None)->None: # pass # universal reader for 1D and 2D! - def from_nexus(self, filename:Path=None)->None: + def from_nexus(self, filename:Optional[Path]=None)->None: # optionally, path can be defined as a dict to point at Q, I and ISigma entries. def objBytesToStr(inObject): outObject = inObject @@ -302,7 +303,7 @@ def prepare(self)->None: self.binnedData = self.clippedData.copy() self.linkMeasData() - def store(self, filename:Path=None, path=None)->None: # path:str|None + def store(self, filename:Path, path:Optional[str]=None)->None: # path:str|None """stores the settings in an output file (HDF5)""" if path is None: path = f"{self.nxsEntryPoint}mcdata/" @@ -311,7 +312,7 @@ def store(self, filename:Path=None, path=None)->None: # path:str|None value = getattr(self, key, None) self._HDFstoreKV(filename=filename, path=path, key=key, value=value) - def load(self, filename: Path = None, path=None)->None: + def load(self, filename: Path, path:Optional[str]=None)->None: if path is None: path = f"{self.nxsEntryPoint}mcdata/" assert filename is not None diff --git a/mcsas3/McData1D.py b/mcsas3/McData1D.py index 0082611..fdd3c13 100644 --- a/mcsas3/McData1D.py +++ b/mcsas3/McData1D.py @@ -1,3 +1,4 @@ +from typing import Optional import numpy as np import pandas from .McData import McData @@ -14,7 +15,7 @@ class McData1D(McData): omitQRanges = None # to skip or omit unwanted data ranges, for example with sharp XRD peaks def __init__( - self, df: pandas.DataFrame = None, loadFromFile: Path = None, resultIndex:int = 1, **kwargs:dict + self, df: Optional[pandas.DataFrame] = None, loadFromFile: Optional[Path] = None, resultIndex:int = 1, **kwargs:dict )-> None: super().__init__(loadFromFile=loadFromFile, resultIndex=resultIndex, **kwargs) self.csvargs = { @@ -38,7 +39,7 @@ def __init__( self.from_file(self.filename) # link measData to the requested value - def linkMeasData(self, measDataLink=None)->None: # measDataLink:str|None + def linkMeasData(self, measDataLink: Optional[str]=None)->None: # measDataLink:str|None if measDataLink is None: measDataLink = self.measDataLink assert measDataLink in [ @@ -53,7 +54,7 @@ def linkMeasData(self, measDataLink=None)->None: # measDataLink:str|None ISigma=measDataObj.ISigma.values, ) - def from_pdh(self, filename:Path=None)->None: + def from_pdh(self, filename:Path)->None: """reads from a PDH file, re-uses Ingo Bressler's code from the notebook example""" assert filename is not None, "from_pdh requires an input filename of a PDH file" skiprows, nrows = 5, -1 @@ -65,7 +66,7 @@ def from_pdh(self, filename:Path=None)->None: csvargs.update({"skiprows": skiprows, "nrows": nrows[0] - skiprows}) self.from_pandas(pandas.read_csv(filename, **csvargs)) - def from_pandas(self, df:pandas.DataFrame=None)->None: + def from_pandas(self, df:pandas.DataFrame)->None: """uses a dataframe as input, should contain 'Q', 'I', and 'ISigma'""" assert isinstance( df, pandas.DataFrame @@ -112,7 +113,7 @@ def omit(self)->None: inplace=True ) - def reBin(self, nbins=None, IEMin:float=0.01, QEMin:float=0.01) -> None: # nbins:int|None + def reBin(self, nbins:Optional[int]=None, IEMin:float=0.01, QEMin:float=0.01) -> None: # nbins:int|None """Unweighted rebinning funcionality with extended uncertainty estimation, adapted from the datamerge methods, as implemented in Paulina's notebook of spring 2020""" if nbins is None: nbins = self.nbins diff --git a/mcsas3/McData2D.py b/mcsas3/McData2D.py index 4e74ef9..4eb3703 100644 --- a/mcsas3/McData2D.py +++ b/mcsas3/McData2D.py @@ -1,3 +1,4 @@ +from typing import Optional import numpy as np import pandas from .McData import McData @@ -40,7 +41,7 @@ def __init__(self, df=None, loadFromFile=None, resultIndex:int=1, **kwargs:dict) self.from_file(self.filename) # link measData to the requested value - def linkMeasData(self, measDataLink=None)-> None: + def linkMeasData(self, measDataLink:Optional[str]=None)-> None: if measDataLink is None: measDataLink = self.measDataLink assert measDataLink in [ @@ -156,7 +157,7 @@ def reconstruct2D(self, modelI1D: np.ndarray) -> np.ndarray: RMI[np.where(self.clippedData["invMask"])] = modelI1D return RMI - def reBin(self, nbins:int=None, IEMin:float=0.01, QEMin:float=0.01)->None: + def reBin(self, nbins:Optional[int]=None, IEMin:float=0.01, QEMin:float=0.01)->None: print("2D data rebinning not implemented, binnedData = clippedData for now") self.binnedData = self.clippedData diff --git a/mcsas3/McHDF.py b/mcsas3/McHDF.py index 33bf9ce..46725a1 100644 --- a/mcsas3/McHDF.py +++ b/mcsas3/McHDF.py @@ -14,7 +14,7 @@ class McHDF(object): def __init__(self)->None: pass - def _HDFSetResultIndex(self, resultIndex:int=None)->None: + def _HDFSetResultIndex(self, resultIndex:int)->None: # resultIndex = -1 should go to the last existing one # assert ( @@ -26,7 +26,7 @@ def _HDFSetResultIndex(self, resultIndex:int=None)->None: self.resultIndex = resultIndex self.nxsEntryPoint = f"/analyses/MCResult{self.resultIndex}/" - def _HDFloadKV(self, filename:Path=None, path: str = None, datatype=None, default=None): # outputs any hdf5 value type + def _HDFloadKV(self, filename:Path, path: str, datatype=None, default=None): # outputs any hdf5 value type with h5py.File(filename, "r") as h5f: if path not in h5f: return default @@ -86,7 +86,7 @@ def _HDFloadKV(self, filename:Path=None, path: str = None, datatype=None, defaul return value - def _HDFstoreKV(self, filename:Path=None, path:str=None, key:str=None, value=None)->None: + def _HDFstoreKV(self, filename:Path, path:str, key:str, value=None)->None: assert filename is not None, "filename (output filename) cannot be empty" assert path is not None, "HDF5 path cannot be empty" assert key is not None, "key cannot be empty" diff --git a/mcsas3/McHat.py b/mcsas3/McHat.py index 6a52004..9321f13 100644 --- a/mcsas3/McHat.py +++ b/mcsas3/McHat.py @@ -1,4 +1,5 @@ import sys, time +from typing import Optional import numpy as np import h5py from .McHDF import McHDF @@ -36,7 +37,7 @@ class McHat(McHDF): ] loadKeys = storeKeys - def __init__(self, loadFromFile=None, resultIndex:int=1, **kwargs:dict) -> None: + def __init__(self, loadFromFile:Optional[Path]=None, resultIndex:int=1, **kwargs:dict) -> None: # reset to make sure we're not inheriting any settings from another instance: self._measData = None # measurement data dict with entries for Q, I, ISigma @@ -86,7 +87,7 @@ def fillFitParameterLimits(self, measData:dict)-> None: np.pi / np.min(measData["Q"]), ] - def run(self, measData:dict=None, filename:Path=None, resultIndex:int=1)->None: + def run(self, measData:dict, filename:Path, resultIndex:int=1)->None: """runs the full sequence: multiple repetitions of optimizations, to be parallelized. This probably needs to be taken out of core, and into a new parent""" @@ -124,7 +125,7 @@ def run(self, measData:dict=None, filename:Path=None, resultIndex:int=1)->None: for output in sorted(outputs, key=lambda x: x[0]): print(output) - def runOnce(self, measData:dict=None, filename:Path=None, repetition:int=0, bufferStdIO:bool=False, resultIndex:int=1) -> None: + def runOnce(self, measData:dict, filename:Path, repetition:int=0, bufferStdIO:bool=False, resultIndex:int=1) -> None: """runs the full sequence: multiple repetitions of optimizations, to be parallelized. This probably needs to be taken out of core, and into a new parent""" if bufferStdIO: @@ -162,7 +163,7 @@ def runOnce(self, measData:dict=None, filename:Path=None, repetition:int=0, buff return sys.stdout.getvalue() return - def store(self, filename:Path=None, path:str=None)-> None: + def store(self, filename:Path, path:Optional[str]=None)-> None: if path is None: path = f"{self.nxsEntryPoint}optimization/" """stores the settings in an output file (HDF5)""" @@ -171,7 +172,7 @@ def store(self, filename:Path=None, path:str=None)-> None: value = getattr(self, key, None) self._HDFstoreKV(filename=filename, path=path, key=key, value=value) - def load(self, filename:Path=None, path=None) -> None: #path:str|None + def load(self, filename:Path, path:Optional[str]=None) -> None: #path:str|None if path is None: path = f"{self.nxsEntryPoint}optimization/" assert filename is not None diff --git a/mcsas3/McPlot.py b/mcsas3/McPlot.py index a47dc09..86f6460 100644 --- a/mcsas3/McPlot.py +++ b/mcsas3/McPlot.py @@ -10,7 +10,7 @@ # import h5py import matplotlib.pyplot as plt from pathlib import Path - +from typing import Optional class McPlot(McHDF): """ @@ -30,7 +30,7 @@ class McPlot(McHDF): def __init__(self, **kwargs): pass - def getHistReport(self, histIndex=0): + def getHistReport(self, histIndex:int=0) -> str: # helper function that gets the histogram statistics report preformatted from the # analysis run. Can also do some post-processing here but that should be avoided. @@ -40,7 +40,7 @@ def getHistReport(self, histIndex=0): 1 ] # first line discarded - def getRunReport(self): + def getRunReport(self) -> str: # helper function that gets the run statistics report preformatted from the # analysis run. Can also do some post-processing here but that should be avoided. @@ -48,7 +48,7 @@ def getRunReport(self): 1 ] # first line is discarded - def resultCard(self, mcres, saveHistFile: Path = None): + def resultCard(self, mcres, saveHistFile: Optional[Path]=None) -> None: """ Produces a standard "result card" as in the original McSAS, with the data and fit on the left-hand side, and the resulting histograms in the subsequent plots. diff --git a/mcsas3/__init__.py b/mcsas3/__init__.py index eca9685..8e84fe9 100644 --- a/mcsas3/__init__.py +++ b/mcsas3/__init__.py @@ -5,6 +5,6 @@ Refactored McSAS implementation """ -import numpy as np +# import numpy as np # vim: set ts=4 sts=4 sw=4 tw=0: diff --git a/mcsas3/mcanalysis.py b/mcsas3/mcanalysis.py index 018223d..471285e 100644 --- a/mcsas3/mcanalysis.py +++ b/mcsas3/mcanalysis.py @@ -58,7 +58,7 @@ class McAnalysis(McHDF): _optKeys = ["scaling", "background", "gof", "accepted", "step"] def __init__( - self, inputFile:Path=None, measData:dict=None, histRanges:pandas.DataFrame=None, store:bool=False, resultIndex:int=1 + self, inputFile:Path, measData:dict, histRanges:pandas.DataFrame, store:bool=False, resultIndex:int=1 ) -> None: # 1. open the input file, and for every repetition: # 2. set up the model again, and @@ -376,7 +376,7 @@ def getNRep(self, inputFile:Path) -> None: f"{len(self._repetitionList)} repetitions found in McSAS file {inputFile}" ) - def store(self, filename:Path=None) -> None: + def store(self, filename:Path) -> None: # store averaged histograms, for arhcival purposes only, these settings are not planned to be reused.: oDict = self._averagedHistograms.copy() # .to_dict(orient="index") for key in oDict.keys(): diff --git a/mcsas3/mccore.py b/mcsas3/mccore.py index a3363fd..6dcad51 100644 --- a/mcsas3/mccore.py +++ b/mcsas3/mccore.py @@ -1,5 +1,6 @@ # import pandas # import sasmodels +from typing import Optional import numpy as np from .osb import optimizeScalingAndBackground from .mcmodel import McModel @@ -32,8 +33,8 @@ def __init__( measData:dict=None, model:McModel=None, opt:McOpt=None, - loadFromFile=None, # Path|None - loadFromRepetition=None, # int|None + loadFromFile:Optional[Path]=None, + loadFromRepetition:Optional[int]=None, resultIndex:int=1, ): # make sure we reset state: @@ -102,35 +103,6 @@ def __init__( err_msg="background mismatch between loaded results and new calculation", ) - # def calcModelI(self, parameters): - # """calculates the intensity and volume of a particular set of parameters""" - # print("CalcModelI is depreciated, replace with calcModelIV!") - # return sasmodels.direct_model.call_kernel( - # self._model.kernel, - # dict(self._model.staticParameters, **parameters) - # ) - - # moved to mcmodel: - # def calcModelIV(self, parameters): - # F, Fsq, R_eff, V_shell, V_ratio = sasmodels.direct_model.call_Fq( - # self._model.kernel, - # dict(self._model.staticParameters, **parameters) - # # parameters - # ) - # # modelIntensity = Fsq/V_shell - # # modelVolume = V_shell - # return Fsq / V_shell, V_shell - - # def returnModelV(self): - # print("returnModelV is depreciated, replace with calcModelIV!") - - # """ - # Returns the volume of the last kernel calculation. - # Has to be run directly after calculation. May be replaced by more appropriate - # SasModels function calls once available. - # """ - # return self._model.kernel.result[self._model.kernel.q_input.nq] - def initModelI(self) -> None: """calculate the total intensity from all contributions""" # set initial shape: @@ -153,7 +125,7 @@ def initModelI(self) -> None: self._model.volumes[contribi] = V def evaluate( - self, testData=None + self, testData:Optional[dict]=None )->float: # , initial: bool = True): # takes 20 ms! initial is taken care of in osb when x0 is None """scale and calculate goodness-of-fit (GOF) from all contributions""" if testData is None: @@ -247,7 +219,7 @@ def optimize(self) -> None: ) ) - def store(self, filename:Path=None) -> None: + def store(self, filename:Path) -> None: """stores the resulting model parameter-set of a single repetition in the NXcanSAS object, ready for histogramming""" self._outputFilename = filename @@ -259,7 +231,7 @@ def store(self, filename:Path=None) -> None: path=f"{self.nxsEntryPoint}optimization/repetition{self._opt.repetition}/", ) - def load(self, loadFromFile:Path=None, loadFromRepetition:int=None, resultIndex:int=1) -> None: + def load(self, loadFromFile:Path, loadFromRepetition:int, resultIndex:int=1) -> None: """loads the configuration and set-up from the extended NXcanSAS file""" # not implemented yet assert ( diff --git a/mcsas3/mcmodel.py b/mcsas3/mcmodel.py index d3db6aa..6b257f4 100644 --- a/mcsas3/mcmodel.py +++ b/mcsas3/mcmodel.py @@ -1,4 +1,4 @@ -from typing import Tuple, List +from typing import Optional, Tuple, List import pandas import numpy as np from .McHDF import McHDF @@ -181,7 +181,7 @@ def __init__(self, **kwargs:dict)->None: fill_value=(self.simDataISigma[0], np.nan), ) - def make_kernel(self, measQ: np.ndarray = None): # return type? + def make_kernel(self, measQ: np.ndarray): # return type? self.measQ = measQ return self.kernelfunc @@ -273,7 +273,7 @@ def fitKeys(self) -> List[str]: return [key for key in self.fitParameterLimits.keys()] def __init__( - self, loadFromFile=None, loadFromRepetition=None, resultIndex:int=1, **kwargs:dict + self, loadFromFile:Optional[Path]=None, loadFromRepetition:Optional[int]=None, resultIndex:int=1, **kwargs:dict )->None: # reset everything so we're sure not to inherit anything from another instance: @@ -396,7 +396,7 @@ def resetParameterSet(self) -> None: ####### Loading and Storing functions: ######## - def load(self, loadFromFile:Path=None, loadFromRepetition:int=None) -> None: + def load(self, loadFromFile:Path, loadFromRepetition:int) -> None: """ loads a preset set of contributions from a previous optimization, stored in HDF5 nContrib is reset to the length of the previous optimization. @@ -445,10 +445,11 @@ def load(self, loadFromFile:Path=None, loadFromRepetition:int=None) -> None: self.nContrib = self.parameterSet.shape[0] - def store(self, filename:Path=None, repetition:int=None)->None: + def store(self, filename:Path, repetition:int)->None: assert ( repetition is not None ), "Repetition number must be given when storing model parameters into a paramFile" + assert filename is not None for parName in self.fitParameterLimits.keys(): self._HDFstoreKV( diff --git a/mcsas3/mcmodelhistogrammer.py b/mcsas3/mcmodelhistogrammer.py index f2ffb87..ee14504 100644 --- a/mcsas3/mcmodelhistogrammer.py +++ b/mcsas3/mcmodelhistogrammer.py @@ -54,7 +54,7 @@ class McModelHistogrammer(McHDF): ) # modes of the populations: total, mean, variance, skew, kurtosis _correctionFactor = 1e-5 # scaling factor to switch from SasModel units used in the model instance (1/(cm sr) for dimensions in Angstrom) to absolute units in 1/(m sr) for dimensions in nm - def __init__(self, coreInstance:McCore=None, histRanges:pandas.DataFrame=None, resultIndex:int=1)-> None: + def __init__(self, coreInstance:McCore, histRanges:pandas.DataFrame, resultIndex:int=1)-> None: # reset variables, make sure we don't inherit anything from another instance: self._model = None # instance of model to work with @@ -217,7 +217,7 @@ def genX(self, histRange: pandas.DataFrame, parameterSet: pandas.DataFrame, volu ) return binEdges - def store(self, filename:Path=None, repetition:int=None)->None: + def store(self, filename:Path, repetition:int)->None: # TODO: CHECK USE OF KEYS IN STORE PATH: assert ( repetition is not None diff --git a/mcsas3/mcopt.py b/mcsas3/mcopt.py index d47351a..0b9667e 100644 --- a/mcsas3/mcopt.py +++ b/mcsas3/mcopt.py @@ -2,6 +2,7 @@ import h5py from .McHDF import McHDF from pathlib import Path +from typing import Optional # TODO: refactor this using attrs @define for clearer handling. @@ -54,7 +55,7 @@ class McOpt(McHDF): "acceptedGofs", ] - def __init__(self, loadFromFile =None, resultIndex:int=1, **kwargs:dict) -> None: # Multiple types (e.g. Path|None ) only supported from Python 3.10 + def __init__(self, loadFromFile:Optional[Path]=None, resultIndex:int=1, **kwargs:dict) -> None: # Multiple types (e.g. Path|None ) only supported from Python 3.10 """initializes the options to the MC algorithm, *or* loads them from a previous run. Note: If the parameters are loaded from a previous file, any additional key-value pairs are updated. """ @@ -91,7 +92,7 @@ def __init__(self, loadFromFile =None, resultIndex:int=1, **kwargs:dict) -> None assert key in self.storeKeys, "Key {} is not a valid option".format(key) setattr(self, key, value) - def store(self, filename:Path=None, path=None) -> None: # Multiple types (e.g. Path|None ) only supported from Python 3.10 + def store(self, filename:Path, path:Optional[str]=None) -> None: # Multiple types (e.g. Path|None ) only supported from Python 3.10 """stores the settings in an output file (HDF5)""" if path is None: path = f"{self.nxsEntryPoint}optimization/" @@ -100,7 +101,7 @@ def store(self, filename:Path=None, path=None) -> None: # Multiple types (e.g. P value = getattr(self, key, None) self._HDFstoreKV(filename=filename, path=path, key=key, value=value) - def load(self, filename:Path=None, repetition=None, path=None) -> None:# Multiple types (e.g. Path|None ) only supported from Python 3.10 + def load(self, filename:Path, repetition:Optional[int]=None, path:Optional[str]=None) -> None:# Multiple types (e.g. Path|None ) only supported from Python 3.10 if path is None: path = f"{self.nxsEntryPoint}optimization/" if repetition is None: diff --git a/test_McData1D_unittest.py b/test_McData1D_unittest.py index f555475..25b73ea 100644 --- a/test_McData1D_unittest.py +++ b/test_McData1D_unittest.py @@ -70,7 +70,7 @@ def test_mcdata1d_nxs_with_omit_from_yaml(self): with open(readConfigFile, "r") as f: readDict = yaml.safe_load(f) # try loading the data - mds = McData1D.McData1D(filename=filename, resultIndex=0, **readDict) + _ = McData1D.McData1D(filename=filename, resultIndex=0, **readDict) def test_mcdata1d_csv_norebin(self): md = McData1D.McData1D( @@ -95,12 +95,12 @@ def test_restore_state(self): ) mds.store(filename="test_state.h5") del mds - md = McData1D.McData1D(loadFromFile=Path("test_state.h5")) + _ = McData1D.McData1D(loadFromFile=Path("test_state.h5")) - def test_restore_state_withIndex(self): - md = McData1D.McData1D( - loadFromFile=Path("testdata", "merged_096.nxs"), resultIndex=2 - ) + # def test_restore_state_withIndex(self): + # md = McData1D.McData1D( + # loadFromFile=Path("testdata", "merged_096.nxs"), resultIndex=2 + # ) def test_restore_state_fromDataframe(self): # create state: diff --git a/test_optimizer_integraltest.py b/test_optimizer_integraltest.py index 2b5f8f9..fde88c2 100644 --- a/test_optimizer_integraltest.py +++ b/test_optimizer_integraltest.py @@ -108,7 +108,7 @@ def test_optimizer_2D_cylinder(self): ) _ = McAnalysis(resPath, md, histRanges, store=True) - def test_optimizer_1D_mcsas_sphere(self): + def test_optimizer_1D_mcsas_sphere_and_rehistogrammer(self): # uses an internal sphere function for the case the sasmodels don't want to work. # remove any prior results file: resPath = Path("test_resultssphere.h5") @@ -169,10 +169,17 @@ def test_optimizer_1D_mcsas_sphere(self): ) _ = McAnalysis(resPath, md, histRanges, store=True, resultIndex=2) - def test_reHistogrammer(self): + # -- -- -- + # def test_reHistogrammer(self): + # immediately test the rehistogrammer as it requires the output of the steps until here.. # read the configuration file - resPath = Path("test_resultssphere.h5") + # resPath = Path("test_resultssphere.h5") + + # clear prior results: + del mds, mh, histRanges + # load the data + mds = McData1D.McData1D(loadFromFile=resPath, resultIndex=2) histRanges = pandas.DataFrame( @@ -212,7 +219,7 @@ def test_reHistogrammer(self): def test_optimizer_1D_sphere(self): # remove any prior results file: - resPath = Path("test_resultssphere.h5") + resPath = Path("test_resultssphere_1D.h5") if resPath.is_file(): resPath.unlink() @@ -323,7 +330,7 @@ def test_optimizer_1D_sphere_with_hardspherestructure(self): def test_optimizer_1D_sim_singlecore(self): # use a simulation for fitting. # remove any prior results file: - resPath = Path("test_resultssim.h5") + resPath = Path("test_resultssim_1D_singlecore.h5") if resPath.is_file(): resPath.unlink() @@ -402,7 +409,7 @@ def test_optimizer_1D_sim_singlecore(self): def test_optimizer_1D_sim_multicore(self): # use a simulation for fitting. # remove any prior results file: - resPath = Path("test_resultssim.h5") + resPath = Path("test_resultssim_1D_multicore.h5") if resPath.is_file(): resPath.unlink() @@ -523,7 +530,7 @@ def test_optimizer_1D_sim_histogram(self): def test_optimizer_1D_sphere_rehistogram(self): # same as above, but include a test of the re-histogramming functionality: # remove any prior results file: - resPath = Path("test_resultssphere.h5") + resPath = Path("test_resultssphere_rehist.h5") if resPath.is_file(): resPath.unlink() @@ -556,7 +563,7 @@ def test_optimizer_1D_sphere_rehistogram(self): seed=None, ) md = mds.measData.copy() - mh.run(md, "test_resultssphere.h5") + mh.run(md, resPath) # histogram the determined size contributions histRanges = pandas.DataFrame( [ @@ -580,7 +587,7 @@ def test_optimizer_1D_sphere_rehistogram(self): ), ] ) - _ = McAnalysis("test_resultssphere.h5", md, histRanges, store=True) + _ = McAnalysis(resPath, md, histRanges, store=True) # now change the histograms and re-run: histRanges = pandas.DataFrame( @@ -605,9 +612,10 @@ def test_optimizer_1D_sphere_rehistogram(self): ), ] ) - _ = McAnalysis("test_resultssphere.h5", md, histRanges, store=True) + _ = McAnalysis(resPath, md, histRanges, store=True) + - def test_optimizer_1D_sphere_createstate(self): + def test_optimizer_1D_sphere_state(self): # (re-)creates a state for the restore-state test. resPath = Path("test_state.h5") if resPath.is_file(): @@ -618,7 +626,7 @@ def test_optimizer_1D_sphere_createstate(self): nbins=100, csvargs={"sep": ";", "header": None, "names": ["Q", "I", "ISigma"]}, ) - mds.store(filename="test_state.h5") + mds.store(filename=resPath) # run the Monte Carlo method mh = McHat.McHat( @@ -634,7 +642,7 @@ def test_optimizer_1D_sphere_createstate(self): seed=None, ) md = mds.measData.copy() - mh.run(md, "test_state.h5") + mh.run(md, resPath) # histogram the determined size contributions histRanges = pandas.DataFrame( [ @@ -649,13 +657,14 @@ def test_optimizer_1D_sphere_createstate(self): ), ] ) - _ = McAnalysis("test_state.h5", md, histRanges, store=True) + _ = McAnalysis(resPath, md, histRanges, store=True) # state created - def test_optimizer_1D_sphere_restorestate(self): + # def test_optimizer_1D_sphere_restorestate(self): # can we recover a state as stored in the HDF5 file?: + del mds, mh, md, histRanges - mds = McData1D.McData1D(loadFromFile=Path("test_state.h5")) + mds = McData1D.McData1D(loadFromFile=resPath) # load required modules # run the Monte Carlo method mh = McHat.McHat( @@ -671,7 +680,7 @@ def test_optimizer_1D_sphere_restorestate(self): seed=None, ) md = mds.measData.copy() - mh.run(md, "test_resultssphere.h5") + mh.run(md, resPath) # histogram the determined size contributions histRanges = pandas.DataFrame( [ @@ -695,7 +704,7 @@ def test_optimizer_1D_sphere_restorestate(self): ), ] ) - _ = McAnalysis("test_resultssphere.h5", md, histRanges, store=True) + _ = McAnalysis(resPath, md, histRanges, store=True) # now change the histograms and re-run: histRanges = pandas.DataFrame( @@ -720,9 +729,9 @@ def test_optimizer_1D_sphere_restorestate(self): ), ] ) - _ = McAnalysis("test_resultssphere.h5", md, histRanges, store=True) + _ = McAnalysis(resPath, md, histRanges, store=True) - def test_optimizer_1D_sphere_createaccuratestate(self): + def test_optimizer_1D_sphere_accuratestate(self): # (re-)creates an accurate state for histogramming tests. resPath = Path("test_accuratestate.h5") if resPath.is_file(): @@ -733,7 +742,7 @@ def test_optimizer_1D_sphere_createaccuratestate(self): nbins=100, csvargs={"sep": ";", "header": None, "names": ["Q", "I", "ISigma"]}, ) - mds.store(filename="test_accuratestate.h5") + mds.store(filename=resPath) # run the Monte Carlo method mh = McHat.McHat( @@ -754,7 +763,7 @@ def test_optimizer_1D_sphere_createaccuratestate(self): seed=None, ) md = mds.measData.copy() - mh.run(md, "test_accuratestate.h5") + mh.run(md, resPath) # histogram the determined size contributions histRanges = pandas.DataFrame( [ @@ -796,29 +805,15 @@ def test_optimizer_1D_sphere_createaccuratestate(self): ), ] ) - _ = McAnalysis("test_accuratestate.h5", md, histRanges, store=True) + _ = McAnalysis(resPath, md, histRanges, store=True) # state created - def test_optimizer_1D_sphere_rehistogram_accuratestate(self): + # def test_optimizer_1D_sphere_rehistogram_accuratestate(self): # for troubleshooting the histogramming function : + del mds, md, histRanges, mh - mds = McData1D.McData1D(loadFromFile=Path("test_accuratestate.h5")) - # load required modules - # run the Monte Carlo method - # mh = McHat.McHat( - # modelName="sphere", - # nContrib=300, - # modelDType="default", - # fitParameterLimits={"radius": (1, 314)}, - # staticParameters={"background": 0, "scaling": 0.1e6}, - # maxIter=1e5, - # convCrit=1, - # nRep=4, - # nCores=0, - # seed=None, - # ) + mds = McData1D.McData1D(loadFromFile=resPath) md = mds.measData.copy() - # mh.run(md, "test_resultssphere.h5") # histogram the determined size contributions histRanges = pandas.DataFrame( [ @@ -860,7 +855,7 @@ def test_optimizer_1D_sphere_rehistogram_accuratestate(self): ), ] ) - mcres = McAnalysis("test_accuratestate.h5", md, histRanges, store=True) + mcres = McAnalysis(resPath, md, histRanges, store=True) # test whether the volume fraction of the first population is within expectation: np.testing.assert_allclose( mcres._averagedModes.loc[1, "totalValue"]["valMean"], 0.027, atol=0.001 @@ -910,7 +905,7 @@ def test_optimizer_1D_gaussianchain(self): seed=None, ) # test step seems to be broken? Maybe same issue with multicore processing with sasview - mh.run(md.measData, "test_resultsgaussianchain.h5") + mh.run(md.measData, resPath) histRanges = pandas.DataFrame( [ dict( @@ -925,17 +920,18 @@ def test_optimizer_1D_gaussianchain(self): ] ) _ = McAnalysis( - "test_resultsgaussianchain.h5", md.measData, histRanges, store=True + resPath, md.measData, histRanges, store=True ) def test_optimizer_nxsas_io(self): + tpath = Path("testdata", "test_nexus_io.nxs") # tests whether I can read and write in the same nexus file - if Path("testdata", "test_nexus_io.nxs").is_file(): - Path("testdata", "test_nexus_io.nxs").unlink() + if tpath.is_file(): + tpath.unlink() hpath = Path( "testdata", "20190725_11_expanded_stacked_processed_190807_161306.nxs" ) - tpath = Path("testdata", "test_nexus_io.nxs") + shutil.copy(hpath, tpath) od = McData1D.McData1D(filename=tpath)