From 83c7ffd51d885ee3b2eebbebcd1559170e200e6c Mon Sep 17 00:00:00 2001 From: Gavin Burnell Date: Sun, 31 Dec 2023 23:10:06 +0000 Subject: [PATCH] Removed DataFile subclasses for file formats and replace with function based loaders and savers and ecorators to manage a registry. --- .github/workflows/run-tests-action.yaml | 2 +- README.rst | 25 +- Stoner/Core.py | 92 +- Stoner/HDF5.py | 297 +++- Stoner/Image/core.py | 34 +- Stoner/Zip.py | 110 ++ Stoner/analysis/fitting/models/__init__.py | 5 +- Stoner/core/base.py | 56 +- Stoner/formats/__init__.py | 74 +- Stoner/formats/attocube.py | 31 + Stoner/formats/decorators.py | 290 ++++ Stoner/formats/facilities.py | 621 +++---- Stoner/formats/generic.py | 690 +++++--- Stoner/formats/instruments.py | 1490 ++++++++--------- Stoner/formats/maximus.py | 87 + Stoner/formats/rigs.py | 589 +++---- Stoner/formats/simulations.py | 273 ++- Stoner/tools/__init__.py | 14 +- Stoner/tools/decorators.py | 15 +- Stoner/tools/file.py | 4 + Stoner/tools/tests.py | 6 + doc/readme.rst | 25 +- doc/samples/joy_division_plot.py | 6 +- tests/Stoner/dummy.py | 9 +- tests/Stoner/folders/test_each.py | 2 +- tests/Stoner/mixedmetatest.dat | 15 + tests/Stoner/mixedmetatest2.dat | 1157 +++++++++++++ .../Stoner/{test_Core.py => test_datafile.py} | 4 +- .../{test_FileFormats.py => test_formats.py} | 26 +- tests/Stoner/{test_Zip.py => test_zipfile.py} | 0 30 files changed, 3987 insertions(+), 2062 deletions(-) create mode 100755 Stoner/formats/decorators.py create mode 100755 tests/Stoner/mixedmetatest.dat create mode 100755 tests/Stoner/mixedmetatest2.dat rename tests/Stoner/{test_Core.py => test_datafile.py} (96%) rename tests/Stoner/{test_FileFormats.py => test_formats.py} (87%) rename tests/Stoner/{test_Zip.py => test_zipfile.py} (100%) diff --git a/.github/workflows/run-tests-action.yaml b/.github/workflows/run-tests-action.yaml index 47a522dfe..9005ad764 100755 --- a/.github/workflows/run-tests-action.yaml +++ b/.github/workflows/run-tests-action.yaml @@ -10,7 +10,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.9", "3.10", "3.11"] + python-version: ["3.10", "3.11"] os: ["ubuntu-latest"] steps: - name: Check out repository code diff --git a/README.rst b/README.rst index 8f58e3b8f..a897f31a2 100755 --- a/README.rst +++ b/README.rst @@ -1,4 +1,4 @@ -.. image:: https://github.com/stonerlab/Stoner-PythonCode/actions/workflows/run-tests-action.yaml/badge.svg?branch=stable +.. image:: https://github.com/stonerlab/Stoner-PythonCode/actions/workflows/run-tests-action.yaml/badge.svg?branch=master :target: https://github.com/stonerlab/Stoner-PythonCode/actions/workflows/run-tests-action.yaml .. image:: https://coveralls.io/repos/github/stonerlab/Stoner-PythonCode/badge.svg?branch=master @@ -106,12 +106,12 @@ contrasr, traditional functional programming thinks in terms of various function .. note:: This is rather similar to pandas DataFrames and the package provides methods to easily convert to and from DataFrames. Unlike a DataFrame, a **Stoner.Data** object maintains a dictionary of additional metadata - attached to the dataset (e.g. of instrument settings, experimental ort environmental; conditions + attached to the dataset (e.g. of instrument settings, experimental ort environmental; conditions when thedata was taken). To assist with exporting to pandas DataFrames, the package will add a custom attrobute handler to pandas DataFrames **DataFrame.metadata** to hold this additional data. - + Unlike Pandas, the **Stoner** package's default is to operate in-place and also to return the object - from method calls to facilitate "chaining" of data methods into short single line pipelines. + from method calls to facilitate "chaining" of data methods into short single line pipelines. Data and Friends ---------------- @@ -207,6 +207,8 @@ Version 0.9 was tested with Python 2.7, 3.5, 3.6 using the standard unittest mod Version 0.10 is tested using **pytest** with Python 3.7-3.11 using a github action. +Version 0.11 is tested using **pytest** with Python 3.10 and 3.11 using a github action. + Citing the Stoner Package ~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -214,11 +216,20 @@ Citing the Stoner Package We maintain a digital object identifier (doi) for this package (linked to on the status bar at the top of this readme) and encourage any users to cite this package via that doi. +Development Version +------------------- + +New Features in 0.11 included: + + * Refactored loading and saving routines to reduce the size of the class heirarchy and allow easier creation + of user suplied loaders and savers. + * Dropped support for Python<3.10 to allow use of new syntax features such as match...case + + Stable Versions --------------- - -New Features in 0.10 include: +New Features in 0.10 included: * Support for Python 3.10 and 3.11 * Refactor Stoner.Core.DataFile to move functionality to mixin classes @@ -270,7 +281,7 @@ No further relases will be made to 0.7.x - 0.9.x Versions 0.6.x and earlier are now pre-historic! -.. _online documentation: http://stoner-pythoncode.readthedocs.io/en/stable/ +.. _online documentation: http://stoner-pythoncode.readthedocs.io/en/latest/ .. _github repository: http://www.github.com/stonerlab/Stoner-PythonCode/ .. _Dr Gavin Burnell: http://www.stoner.leeds.ac.uk/people/gb .. _User_Guide: http://stoner-pythoncode.readthedocs.io/en/latest/UserGuide/ugindex.html diff --git a/Stoner/Core.py b/Stoner/Core.py index 99f54fc6c..ace556ebc 100755 --- a/Stoner/Core.py +++ b/Stoner/Core.py @@ -29,7 +29,7 @@ from numpy import ma from .compat import string_types, int_types, index_types, _pattern_type, path_types -from .tools import all_type, isiterable, isLikeList, get_option, make_Data +from .tools import all_type, isiterable, isLikeList, get_option, make_Data, isclass from .tools.file import get_file_name_type, auto_load_classes from .core.exceptions import StonerLoadError, StonerSetasError @@ -1361,41 +1361,39 @@ def load(cls, *args, **kargs): If no class can load a file successfully then a StonerUnrecognisedFormat exception is raised. """ + from .formats import load + filename = kargs.pop("filename", args[0] if len(args) > 0 else None) + + # Sort out the filetype, checking whether we've been passed a class or an instance first. filetype = kargs.pop("filetype", None) + if isclass(filetype, metadataObject): + filetype = filetype.__name__ + elif filetype and isinstance(filetype, metadataObject): + filetype = filetype.__class__.__name__ + + # If filetype is None, then we want to auto-load by default auto_load = kargs.pop("auto_load", filetype is None) + # Fallback to ensure filetype is set + filetype = "DataFile" if filetype is None else filetype + loaded_class = kargs.pop("loaded_class", False) - if isinstance(filename, path_types) and urllib.parse.urlparse(str(filename)).scheme not in URL_SCHEMES: - filename, filetype = get_file_name_type(filename, filetype, DataFile) - elif not auto_load and not filetype: - raise StonerLoadError("Cannot read data from non-path like filenames !") + if filename is None or not filename: + filename = file_dialog("r", filename, cls, DataFile) + if auto_load: # We're going to try every subclass we canA - ret = auto_load_classes(filename, DataFile, debug=False, args=args, kargs=kargs) - if not isinstance(ret, DataFile): # autoload returned something that wasn't a data file! - return ret + kargs.pop("filetype", None) # make sure filetype is not set else: - if filetype is None or isinstance(filetype, DataFile): # set fieltype to DataFile - filetype = DataFile - if issubclass(filetype, DataFile): - ret = filetype() - ret = ret._load(filename, *args, **kargs) - ret["Loaded as"] = filetype.__name__ - else: - raise ValueError(f"Unable to load {filename}") + kargs.setdefault("filetype", filetype) # Makre sure filetype is set + ret = load(filename, *args, **kargs) + if not isinstance(ret, DataFile): # autoload returned something that wasn't a data file! + return ret for k, i in kargs.items(): if not callable(getattr(ret, k, lambda x: False)): setattr(ret, k, i) ret._kargs = kargs - filetype = ret.__class__.__name__ - if loaded_class: - self = ret - else: - self = make_Data() - self._public_attrs.update(ret._public_attrs) - copy_into(ret, self) - self.filetype = filetype - return self + return ret def rename(self, old_col, new_col): """Rename columns without changing the underlying data. @@ -1486,6 +1484,8 @@ def save(self, filename=None, **kargs): self: The current :py:class:`DataFile` object """ + from .formats.decorators import get_saver + as_loaded = kargs.pop("as_loaded", False) if filename is None: filename = self.filename @@ -1498,45 +1498,21 @@ def save(self, filename=None, **kargs): if ( isinstance(as_loaded, bool) and "Loaded as" in self ): # Use the Loaded as key to find a different save routine - cls = subclasses(DataFile)[self["Loaded as"]] # pylint: disable=unsubscriptable-object - elif isinstance(as_loaded, string_types) and as_loaded in subclasses( - DataFile - ): # pylint: disable=E1136, E1135 - cls = subclasses(DataFile)[as_loaded] # pylint: disable=unsubscriptable-object + as_loaded = self["Loaded as"] + if isinstance(as_loaded, string_types) and (saver := get_saver(as_loaded, silent=True)): + pass else: raise ValueError( f"{as_loaded} cannot be interpreted as a valid sub class of {type(self)}" + " so cannot be used to save this data" ) - ret = cls(self).save(filename) - self.filename = ret.filename - return self - # Normalise the extension to ensure it's something we like... - filename, ext = os.path.splitext(filename) - if f"*.{ext}" not in DataFile.patterns: # pylint: disable=unsupported-membership-test - ext = DataFile.patterns[0][2:] # pylint: disable=unsubscriptable-object - filename = f"{filename}.{ext}" - header = ["TDI Format 1.5"] - header.extend(self.column_headers[: self.data.shape[1]]) - header = "\t".join(header) - mdkeys = sorted(self.metadata) - if len(mdkeys) > len(self): - mdremains = mdkeys[len(self) :] - mdkeys = mdkeys[0 : len(self)] else: - mdremains = [] - mdtext = np.array([self.metadata.export(k) for k in mdkeys]) - if len(mdtext) < len(self): - mdtext = np.append(mdtext, np.zeros(len(self) - len(mdtext), dtype=str)) - data_out = np.column_stack([mdtext, self.data]) - fmt = ["%s"] * data_out.shape[1] - with io.open(filename, "w", errors="replace", encoding="utf-8") as f: - np.savetxt(f, data_out, fmt=fmt, header=header, delimiter="\t", comments="") - for k in mdremains: - f.write(self.metadata.export(k) + "\n") # (str2bytes(self.metadata.export(k) + "\n")) - - self.filename = filename - return self + saver = get_saver("DataFile") # Get my saver + # Normalise the extension to ensure it's something we like... + filename = pathlib.Path(filename) + if filename.suffix not in saver.patterns: + filename = filename.parent / f"{filename.stem}{saver.patterns[0][0]}" + return saver(self, filename) def swap_column(self, *swp, **kargs): """Swap pairs of columns in the data. diff --git a/Stoner/HDF5.py b/Stoner/HDF5.py index 1587d8729..06b195e42 100755 --- a/Stoner/HDF5.py +++ b/Stoner/HDF5.py @@ -23,6 +23,7 @@ from .folders import DataFolder from .Image.core import ImageFile, ImageArray from .core.utils import copy_into +from .formats.decorators import register_loader, register_saver def get_hdf_loader(f, default_loader=lambda *args, **kargs: None): @@ -114,6 +115,296 @@ def __exit__(self, _type, _value, _traceback): self.file.close() +@register_loader( + patterns=[(".hdf", 16), (".hf5", 16)], + mime_types=[("application/x-hdf", 16), ("application/x-hdf5", 16)], + name="HDF5File", + what="Data", +) +def load_hdf(new_data, filename, *args, **kargs): # pylint: disable=unused-argument + """Create a new HDF5File from an actual HDF file.""" + with HDFFileManager(filename, "r") as f: + data = f["data"] + if np.prod(np.array(data.shape)) > 0: + new_data.data = data[...] + else: + new_data.data = [[]] + metadata = f.require_group("metadata") + typehints = f.get("typehints", None) + if not isinstance(typehints, h5py.Group): + typehints = dict() + else: + typehints = typehints.attrs + if "column_headers" in f.attrs: + new_data.column_headers = [bytes2str(x) for x in f.attrs["column_headers"]] + if isinstance(new_data.column_headers, string_types): + new_data.column_headers = new_data.metadata.string_to_type(new_data.column_headers) + new_data.column_headers = [bytes2str(x) for x in new_data.column_headers] + else: + raise StonerLoadError("Couldn't work out where my column headers were !") + for i in sorted(metadata.attrs): + v = metadata.attrs[i] + t = typehints.get(i, "Detect") + if isinstance(v, string_types) and t != "Detect": # We have typehints and this looks like it got exported + new_data.metadata[f"{i}{{{t}}}".strip()] = f"{v}".strip() + else: + new_data[i] = metadata.attrs[i] + if isinstance(f, h5py.Group): + if f.name != "/": + new_data.filename = os.path.join(f.file.filename, f.name) + else: + new_data.filename = os.path.realpath(f.file.filename) + else: + new_data.filename = os.path.realpath(f.filename) + return new_data + + +@register_saver(patterns=[(".hdf", 16), (".hf5", 16)], name="HDF5File", what="Data") +def save_hdf(save_data, filename=None, **kargs): # pylint: disable=unused-argument + """Write the current object into an hdf5 file or group within a file. + + Writes the data in afashion that is compatible with being loaded in again. + + Args: + filename (string or h5py.Group): + Either a string, of h5py.File or h5py.Group object into which to save the file. If this is a string, + the corresponding file is opened for writing, written to and save again. + + Returns + A copy of the object + """ + if filename is None: + filename = save_data.filename + if filename is None or (isinstance(filename, bool) and not filename): # now go and ask for one + filename = save_data.__file_dialog("w") + save_data.filename = filename + if isinstance(filename, path_types): + mode = "r+" if os.path.exists(filename) else "w" + else: + mode = "" + compression = "gzip" + compression_opts = 6 + with HDFFileManager(filename, mode) as f: + f.require_dataset( + "data", + data=save_data.data, + shape=save_data.data.shape, + dtype=save_data.data.dtype, + ) + metadata = f.require_group("metadata") + typehints = f.require_group("typehints") + for k in save_data.metadata: + try: + typehints.attrs[k] = save_data.metadata._typehints[k] + metadata.attrs[k] = save_data[k] + except TypeError: + # We get this for trying to store a bad data type - fallback to metadata export to string + parts = save_data.metadata.export(k).split("=") + metadata.attrs[k] = "=".join(parts[1:]) + f.attrs["column_headers"] = [str2bytes(x) for x in save_data.column_headers] + f.attrs["filename"] = save_data.filename + f.attrs["type"] = type(save_data).__name__ + f.attrs["module"] = type(save_data).__module__ + return save_data + + +def _scan_SLS_meta(new_data, group): + """Scan the HDF5 Group for attributes and datasets and sub groups and recursively add them to the metadata.""" + root = ".".join(group.name.split("/")[2:]) + for name, thing in group.items(): + parts = thing.name.split("/") + name = ".".join(parts[2:]) + if isinstance(thing, h5py.Group): + _scan_SLS_meta(new_data, thing) + elif isinstance(thing, h5py.Dataset): + if thing.ndim > 1: + continue + if np.prod(thing.shape) == 1: + new_data.metadata[name] = thing[0] + else: + new_data.metadata[name] = thing[...] + for attr in group.attrs: + new_data.metadata[f"{root}.{attr}"] = group.attrs[attr] + + +@register_loader( + patterns=[(".hdf", 16), (".hdf5", 16)], + mime_types=[("application/x-hdf", 16), ("application/x-hdf5", 16)], + name="SLS_STXMFile", + what="Data", +) +def load_sls_stxm(new_data, filename, *args, **kargs): + """Load data from the hdf5 file produced by Pollux. + + Args: + h5file (string or h5py.Group): + Either a string or an h5py Group object to load data from + + Returns: + itnew_data after having loaded the data + """ + new_data.filename = filename + if isinstance(filename, path_types): # We got a string, so we'll treat it like a file... + try: + f = h5py.File(filename, "r") + except IOError as err: + raise StonerLoadError(f"Failed to open {filename} as a n hdf5 file") from err + elif isinstance(filename, h5py.File) or isinstance(filename, h5py.Group): + f = filename + else: + raise StonerLoadError(f"Couldn't interpret {filename} as a valid HDF5 file or group or filename") + items = [x for x in f.items()] + if len(items) == 1 and items[0][0] == "entry1": + group1 = [x for x in f["entry1"]] + if "definition" in group1 and bytes2str(f["entry1"]["definition"][0]) == "NXstxm": # Good HDF5 + pass + else: + raise StonerLoadError("HDF5 file lacks single top level group called entry1") + else: + raise StonerLoadError("HDF5 file lacks single top level group called entry1") + root = f["entry1"] + data = root["counter0"]["data"] + if np.prod(np.array(data.shape)) > 0: + new_data.data = data[...] + else: + new_data.data = [[]] + _scan_SLS_meta(new_data, root) + if "file_name" in f.attrs: + new_data["original filename"] = f.attrs["file_name"] + elif isinstance(f, h5py.Group): + new_data["original filename"] = f.name + else: + new_data["original filename"] = f.file.filename + + if isinstance(filename, path_types): + f.file.close() + new_data["Loaded from"] = new_data.filename + + if "instrument.sample_x.data" in new_data.metadata: + new_data.metadata["actual_x"] = new_data.metadata["instrument.sample_x.data"].reshape(new_data.shape) + if "instrument.sample_y.data" in new_data.metadata: + new_data.metadata["actual_y"] = new_data.metadata["instrument.sample_y.data"].reshape(new_data.shape) + new_data.metadata["sample_x"], new_data.metadata["sample_y"] = np.meshgrid( + new_data.metadata["counter0.sample_x"], new_data.metadata["counter0.sample_y"] + ) + if "control.data" in new_data.metadata: + new_data.metadata["beam current"] = ImageArray(new_data.metadata["control.data"].reshape(new_data.data.shape)) + new_data.metadata["beam current"].metadata = new_data.metadata + new_data.data = new_data.data[::-1] + return new_data + + +@register_loader( + patterns=[(".hdf5", 16), (".hdf", 16)], + mime_types=[("application/x-hdf", 16), ("application/x-hdf5", 16)], + name="STXMImage", + what="Image", +) +def load_stxm_image(new_data, filename, *args, **kargs): + """Initialise and load a STXM image produced by Pollux. + + Keyword Args: + regrid (bool): + If set True, the gridimage() method is automatically called to re-grid the image to known coordinates. + """ + regrid = kargs.pop("regrid", False) + bcn = kargs.pop("bcn", False) + d = DataFile() + d = load_sls_stxm(d, filename, *args, **kargs) + new_data.image = d.data + new_data.metadata = deepcopy(d.metadata) + new_data.filename = d.filename + if isinstance(regrid, tuple): + new_data.gridimage(*regrid) + elif isinstance(regrid, dict): + new_data.gridimage(**regrid) + elif regrid: + new_data.gridimage() + if bcn: + if regrid: + new_data.metadata["beam current"] = new_data.metadata["beam current"].gridimage() + new_data.image /= new_data["beam current"] + new_data.polarization = np.sign(new_data.get("collection.polarization.value", 0)) + return new_data + + +def _hgx_scan_group(new_data, grp, pth): + """Recursively list HDF5 Groups.""" + if pth in new_data.seen: + return None + new_data.seen.append(pth) + if not isinstance(grp, h5py.Group): + return None + if new_data.debug: + if new_data.debug: + print(f"Scanning in {pth}") + for x in grp: + if pth == "": + new_pth = x + else: + new_pth = pth + "." + x + if pth == "" and x == "data": # Special case for main data + continue + if isinstance(grp[x], type(grp)): + _hgx_scan_group(new_data, grp[x], new_pth) + elif isinstance(grp[x], h5py.Dataset): + y = grp[x][...] + new_data[new_pth] = y + return None + + +def _hgx_main_data(new_data, data_grp): + """Work through the main data group and build something that looks like a numpy 2D array.""" + if not isinstance(data_grp, h5py.Group) or data_grp.name != "/current/data": + raise StonerLoadError("HDF5 file not in expected format") + root = data_grp["datasets"] + for ix in root: # Hack - iterate over all items in root, but actually data is in Groups not DataSets + dataset = root[ix] + if isinstance(dataset, h5py.Dataset): + continue + x = dataset["x"][...] + y = dataset["y"][...] + e = dataset["error"][...] + new_data &= x + new_data &= y + new_data &= e + new_data.column_headers[-3] = bytes2str(dataset["x_command"][()]) + new_data.column_headers[-2] = bytes2str(dataset["y_command"][()]) + new_data.column_headers[-1] = bytes2str(dataset["error_command"][()]) + new_data.column_headers = [str(ix) for ix in new_data.column_headers] + + +@register_loader( + patterns=(".hgx", 16), + mime_types=[("application/x-hdf", 32), ("application/x-hdf5", 32)], + name="HGXFile", + what="Data", +) +def _load(new_data, filename, *args, **kargs): + """Load a GenX HDF file. + + Args: + filename (string or bool): + File to load. If None then the existing filename is used, if False, then a file dialog will be used. + + Returns: + A copy of the itnew_data after loading the data. + """ + new_data.filename = filename + new_data.seen = [] + with HDFFileManager(new_data.filename, "r") as f: + try: + if "current" in f and "config" in f["current"]: + pass + else: + raise StonerLoadError("Looks like an unexpected HDF layout!.") + _hgx_scan_group(new_data, f["current"], "") + _hgx_main_data(new_data, f["current"]["data"]) + except IOError as err: + raise StonerLoadError("Looks like an unexpected HDF layout!.") from err + return new_data + + class HDF5File(DataFile): """A sub class of DataFile that sores itself in a HDF5File or group. @@ -626,7 +917,7 @@ def _load(self, filename, *args, **kargs): self.data = data[...] else: self.data = [[]] - self.scan_meta(root) + _scan_SLS_meta(self, root) if "file_name" in f.attrs: self["original filename"] = f.attrs["file_name"] elif isinstance(f, h5py.Group): @@ -651,14 +942,14 @@ def _load(self, filename, *args, **kargs): self.data = self.data[::-1] return self - def scan_meta(self, group): + def _scan_SLS_meta(self, group): """Scan the HDF5 Group for attributes and datasets and sub groups and recursively add them to the metadata.""" root = ".".join(group.name.split("/")[2:]) for name, thing in group.items(): parts = thing.name.split("/") name = ".".join(parts[2:]) if isinstance(thing, h5py.Group): - self.scan_meta(thing) + _scan_SLS_meta(self, thing) elif isinstance(thing, h5py.Dataset): if thing.ndim > 1: continue diff --git a/Stoner/Image/core.py b/Stoner/Image/core.py index 0babdc206..ba48b21a8 100755 --- a/Stoner/Image/core.py +++ b/Stoner/Image/core.py @@ -1178,31 +1178,25 @@ def load(cls, *args, **kargs): If no class can load a file successfully then a RunttimeError exception is raised. """ - args = list(args) - filename = kargs.pop("filename", args.pop(0) if len(args) > 0 else None) + from ..formats import load + + filename = kargs.pop("filename", args[0] if len(args) > 0 else None) + if filename == args[0]: + args = tuple() if len(args) <= 1 else args[1:] filetype = kargs.pop("filetype", None) auto_load = kargs.pop("auto_load", filetype is None) - debug = kargs.pop("debug", False) + filetype = "ImageFile" if filetype is None else filetype + if filename is None or not filename: + filename = file_dialog("r", filename, cls, DataFile) - filename, filetype = get_file_name_type(filename, filetype, DataFile) if auto_load: # We're going to try every subclass we canA - try: - ret = auto_load_classes(filename, ImageFile, debug=debug, args=args, kargs=kargs) - except StonerUnrecognisedFormat: - ret = ImageFile() - ret = ret._load(filename, *args, **kargs) - ret["Loaded as"] = filetype.__name__ + kargs.pop("filetype", None) # make sure filetype is not set else: - if issubclass(filetype, ImageFile): - ret = filetype() - ret = ret._load(filename, *args, **kargs) - ret["Loaded as"] = filetype.__name__ - elif filetype is None or isinstance(filetype, ImageFile): - ret = cls() - ret = ret._load(filename, *args, **kargs) - ret["Loaded as"] = cls.__name__ - else: - raise ValueError(f"Unable to load {filename}") + kargs.setdefault("filetype", filetype) # Makre sure filetype is set + kargs.setdefault("what", "Image") + ret = load(filename, *args, **kargs) + if not isinstance(ret, ImageFile): # autoload returned something that wasn't a data file! + return ret for k, i in kargs.items(): if not callable(getattr(ret, k, lambda x: False)): diff --git a/Stoner/Zip.py b/Stoner/Zip.py index 82181e025..29cce8182 100755 --- a/Stoner/Zip.py +++ b/Stoner/Zip.py @@ -18,6 +18,7 @@ from .folders.core import baseFolder from .folders.utils import pathjoin from .tools import copy_into, make_Data +from .formats.decorators import register_loader, register_saver def test_is_zip(filename, member=""): @@ -53,6 +54,115 @@ def test_is_zip(filename, member=""): return test_is_zip(newfile, newmember) +@register_loader(patterns=(".zip", 16), mime_types=("application/zip", 16), name="ZippedFile", what="Data") +def load_zipfile(new_data, filename=None, *args, **kargs): + """Load a file from the zip file, opening it as necessary.""" + new_data.filename = filename + try: + if isinstance(new_data.filename, zf.ZipFile): # Loading from an ZipFile + if not new_data.filename.fp: # Open zipfile if necessary + other = zf.ZipFile(new_data.filename.filename, "r") + close_me = True + else: # Zip file is already open + other = new_data.filename + close_me = False + member = kargs.get("member", other.namelist()[0]) + solo_file = len(other.namelist()) == 1 + elif isinstance(new_data.filename, path_types) and zf.is_zipfile( + new_data.filename + ): # filename is a string that is a zip file + other = zf.ZipFile(new_data.filename, "a") + member = kargs.get("member", other.namelist()[0]) + close_me = True + solo_file = len(other.namelist()) == 1 + else: + raise StonerLoadError(f"{new_data.filename} does not appear to be a real zip file") + except StonerLoadError: + raise + except Exception as err: # pylint: disable=W0703 # Catching everything else here + try: + exc = format_exc() + other.close() + except (AttributeError, NameError, ValueError, TypeError, zf.BadZipFile, zf.LargeZipFile): + pass + raise StonerLoadError(f"{new_data.filename} threw an error when opening\n{exc}") from err + # Ok we can try reading now + info = other.getinfo(member) + data = other.read(info) # In Python 3 this would be a bytes + tmp = make_Data() << data.decode("utf-8") + copy_into(tmp, new_data) + # new_data.__init__(tmp << data) + new_data.filename = path.join(other.filename, member) + if close_me: + other.close() + if solo_file: + new_data.filename = str(filename) + return new_data + + +@register_saver(patterns=(".zip", 16), name="ZippedFile", what="Data") +def save(save_data, filename=None, **kargs): + """Override the save method to allow ZippedFile to be written out to disc (as a mininmalist output). + + Args: + filename (string or zipfile.ZipFile instance): + Filename to save as (using the same rules as for the load routines) + + Returns: + A copy of itsave_data. + """ + if filename is None: + filename = save_data.filename + if filename is None or (isinstance(filename, bool) and not filename): # now go and ask for one + filename = save_data.__file_dialog("w") + compression = kargs.pop("compression", zf.ZIP_DEFLATED) + try: + if isinstance(filename, path_types): # We;ve got a string filename + if test_is_zip(filename): # We can find an existing zip file somewhere in the filename + zipfile, member = test_is_zip(filename) + zipfile = zf.ZipFile(zipfile, "a") + close_me = True + elif path.exists(filename): # The fiule exists but isn't a zip file + raise IOError(f"{filename} Should either be a zip file or a new zip file") + else: # Path doesn't exist, use extension of file part to find where the zip file should be + parts = path.split(filename) + for i, part in enumerate(parts): + if path.splitext(part)[1].lower() == ".zip": + break + else: + raise IOError(f"Can't figure out where the zip file is in {filename}") + zipfile = zf.ZipFile(path.join(*parts[: i + 1]), "w", compression, True) + close_me = True + member = path.join("/", *parts[i + 1 :]) + elif isinstance(filename, zf.ZipFile): # Handle\ zipfile instance, opening if necessary + if not filename.fp: + filename = zf.ZipFile(filename.filename, "a") + close_me = True + else: + close_me = False + zipfile = filename + member = "" + + if member == "" or member == "/": # Is our file object a bare zip file - if so create a default member name + if len(zipfile.namelist()) > 0: + member = zipfile.namelist()[-1] + save_data.filename = path.join(filename, member) + else: + member = "DataFile.txt" + save_data.filename = filename + + zipfile.writestr(member, str2bytes(str(save_data))) + if close_me: + zipfile.close() + except (zipfile.BadZipFile, IOError, TypeError, ValueError) as err: + error = format_exc() + try: + zipfile.close() + finally: + raise IOError(f"Error saving zipfile\n{error}") from err + return save_data + + class ZippedFile(DataFile): """A sub class of DataFile that sores itself in a zip file. diff --git a/Stoner/analysis/fitting/models/__init__.py b/Stoner/analysis/fitting/models/__init__.py index 137303171..540907af8 100755 --- a/Stoner/analysis/fitting/models/__init__.py +++ b/Stoner/analysis/fitting/models/__init__.py @@ -211,10 +211,7 @@ def cfg_data_from_ini(inifile, filename=None, **kargs): raise RuntimeError("Configuration file lacks a [Data] section to describe data.") if config.has_option("Data", "type"): - typ = config.get("Data", "type").split(".") - typ_mod = ".".join(typ[:-1]) - typ = typ[-1] - typ = __import__(typ_mod, fromlist=[typ]).__getattribute__(typ) + typ = config.get("Data", "type") else: typ = None data = make_Data(**kargs) diff --git a/Stoner/core/base.py b/Stoner/core/base.py index 9691ae956..f16fc30df 100755 --- a/Stoner/core/base.py +++ b/Stoner/core/base.py @@ -37,12 +37,7 @@ from .exceptions import StonerAssertionError from .Typing import String_Types, RegExp, Filename -try: - from blist import sorteddict -except (StonerAssertionError, ImportError): # Fail if blist not present or Python 3 - from collections import OrderedDict - - sorteddict = OrderedDict +from collections import OrderedDict _asteval_interp = None @@ -123,7 +118,7 @@ class _evaluatable: """Placeholder to indicate that special action needed to convert a string representation to valid Python type.""" -class regexpDict(sorteddict): +class regexpDict(OrderedDict): """An ordered dictionary that permits looks up by regular expression.""" @@ -261,7 +256,7 @@ def has_key(self, name: Any) -> bool: class typeHintedDict(regexpDict): - """Extends a :py:class:`blist.sorteddict` to include type hints of what each key contains. + """Extends a :py:class:`regedpDict` to include type hints of what each key contains. The CM Physics Group at Leeds makes use of a standard file format that closely matches the :py:class:`DataFile` data structure. However, it is convenient for this file format @@ -293,11 +288,6 @@ class typeHintedDict(regexpDict): mapping of type hinted types to actual Python types __tests (dict): mapping of the regex patterns to actual python types - - Notes: - Rather than subclassing a plain dict, this is a subclass of a :py:class:`blist.sorteddict` which stores the - entries in a binary list structure. This makes accessing the keys much faster and also ensures that keys are - always returned in alphabetical order. """ allowed_keys: Tuple = string_types @@ -359,7 +349,7 @@ def __init__(self, *args: Any, **kargs: Any) -> None: embedded string type from the keyname) or determines the likely type hint from the value of the dict element. """ - self._typehints = sorteddict() + self._typehints = OrderedDict() super().__init__(*args, **kargs) for key in list(self.keys()): # Check through all the keys and see if they contain # type hints. If they do, move them to the @@ -457,6 +447,9 @@ def __mungevalue(self, typ: str, value: Any) -> Any: except ValueError: ret = str(value) break + elif issubclass(valuetype, str) and value == "None": + ret = None + break else: ret = valuetype(value) break @@ -640,7 +633,7 @@ def export_all(self) -> List[str]: (list of str): A list of exported strings Notes: - The keys are returned in sorted order as a result of the underlying blist.sorteddict meothd. + The keys are returned in sorted order as a result of the underlying OrderedDict meothd. """ return [self.export(x) for x in self] @@ -779,6 +772,39 @@ def _load(self, filename: Filename, *args: Any, **kargs: Any) -> "metadataObject raise NotImplementedError("Save is not implemented in the base class.") +class SortedMultivalueDict(OrderedDict): + + """Implement a simple multivalued dictionary where the values are always sorted lists of elements.""" + + @classmethod + def _matching(cls, val): + match val: + case (int(p), item): + return [(p, item)] + case [(int(p), item), *rest]: + return sorted([(p, item)] + cls._matching(rest)) + case []: + return [] + case _: + raise TypeError("Can only add items that are a typle of int,value") + + def get_value_list(self, name): + """Get the values stored in the dictionary under name.""" + return [item for _, item in self.get(name, [])] + + def __setitem__(self, name: Any, value: Union[List[Tuple[int, Any]], Tuple[int, Any]]) -> None: + """Insert or replace a value and then sort the values.""" + values = self._matching(value) + for p, value in values: + for ix, (old_p, old_value) in enumerate(self.get(name, [])): + if old_value == value: # replacing existing value + self[name][ix] = (p, value) + break + else: + super().__setitem__(name, self.get(name, []) + [(p, value)]) + super().__setitem__(name, sorted(self[name], key=lambda item: (item[0], str(item[1])))) + + if pd is not None and not hasattr(pd.DataFrame, "metadata"): # Don;t double add metadata @pd.api.extensions.register_dataframe_accessor("metadata") diff --git a/Stoner/formats/__init__.py b/Stoner/formats/__init__.py index c23eacaea..a3be4ad81 100755 --- a/Stoner/formats/__init__.py +++ b/Stoner/formats/__init__.py @@ -1,21 +1,57 @@ -"""Provides the subclasses for loading different file formats into :py:class:`Stoner.Data` objects. - -You do not need to use these classes directly, they are made available to :py:class:`Stoner.Data` which -will load each of them in turn when asked to load an unknown data file. - -Each class has a :py:attr:`Stoner.Core.DataFile.priority` attribute that is used to determine the order in which -they are tried by :py:class:`Stoner.Data` and friends where trying to load data. -Larger priority index classes are run last (so is a bit of a misnomer!). - -Each class should implement a :py:meth:`Stoner.Core.DataFile._load` method and optionally a -:py:meth:`Stoner.Core.DataFile.save` method. Classes should make every effort to -positively identify that the file is one that they understand and throw a -:py:exception:Stoner.cpre.exceptions.StonerLoadError` if not. - -Classes may also provide :py:attr:`Stoner.Core.DataFile.patterns` attribute which is a list of filename glob patterns -(e.g. ['*.data','*.txt']) which is used in the file dialog box to filter the list of files. Finally, classes can -provide a :py:attr:`Stoner.Core.DataFile.mime_type` attribute which gives a list of mime types that this class might -be able to open. This helps identify classes that could be use to load particular file types. +"""Provides functions to load files in a variety of formats. """ -__all__ = ["instruments", "generic", "rigs", "facilities", "simulations", "attocube", "maximus"] +__all__ = [ + "instruments", + "generic", + "rigs", + "facilities", + "simulations", + "attocube", + "maximus", + "register_loader", + "register_saver", + "next_loader", + "best_saver", + "load", + "get_loader", + "get_saver", +] +from copy import copy +import io +from pathlib import Path from . import instruments, generic, rigs, facilities, simulations, attocube, maximus +from .decorators import register_loader, register_saver, next_loader, best_saver, get_loader, get_saver +from ..core.exceptions import StonerLoadError +from ..tools import make_Data +from ..tools.file import get_mime_type + + +def load(filename, *args, **kargs): + """Use the function based loaders to try and load a file from disk.""" + if isinstance(filename, io.IOBase): + extension = "*" + elif isinstance(filename, bytes): + extension = "*" + elif hasattr(filename, "filename"): + extension = Path(filename.filename).suffix + else: + extension = Path(filename).suffix + what = kargs.pop("what", "Data") + mime_type = get_mime_type(filename) + if filetype := kargs.pop("filetype", False): + loader = get_loader(filetype) + print(f"Direct Loading {filetype}") + data = make_Data(what=what) + ret = loader(data, filename, *copy(args), *copy(kargs)) + ret["Loaded as"] = loader.name + return ret + for loader in next_loader(extension, mime_type=mime_type, what=what): + print(loader.name) + data = make_Data(what=what) + try: + ret = loader(data, filename, *copy(args), *copy(kargs)) + ret["Loaded as"] = loader.name + return ret + except StonerLoadError: + continue + raise StonerLoadError(f"Unable to find anything that would load {filename}") diff --git a/Stoner/formats/attocube.py b/Stoner/formats/attocube.py index d2078b560..3ab877055 100755 --- a/Stoner/formats/attocube.py +++ b/Stoner/formats/attocube.py @@ -19,6 +19,7 @@ from ..HDF5 import HDFFileManager from ..tools.file import FileManager from ..core.exceptions import StonerLoadError +from .decorators import register_loader PARAM_RE = re.compile(r"^([\d\\.eE\+\-]+)\s*([\%A-Za-z]\S*)?$") SCAN_NO = re.compile(r"SC_(\d+)") @@ -47,6 +48,36 @@ def _raise_error(openfile, message=""): pass +@register_loader(patterns=(".txt", 32), mime_types=("text/plain", 32), name="AttocubeScanParametersFile", what="Data") +def load_attocube_parameters(new_data, filename, *args, **kargs): + """Load the scan parameters text file as the metadata for a Data File. + + Args: + root_name (str): + The scan prefix e.g. SC_### + + Returns: + new_data: + The modififed scan stack. + """ + new_data.filename = filename + with FileManager(filename, "r") as parameters: + if not parameters.readline().startswith("Daisy Parameter Snapshot"): + raise StonerLoadError("Parameters file exists but does not have correct header") + for line in parameters: + if not line.strip(): + continue + parts = [x.strip() for x in line.strip().split(":")] + key = parts[0] + value = ":".join(parts[1:]) + units = PARAM_RE.match(value) + if units and units.groups()[1]: + key += f" [{units.groups()[1]}]" + value = units.groups()[0] + new_data[key] = value + return new_data + + class AttocubeScanMixin: """Provides the specialist methods for dealing with Attocube SPM scan files. diff --git a/Stoner/formats/decorators.py b/Stoner/formats/decorators.py new file mode 100755 index 000000000..56fcad16b --- /dev/null +++ b/Stoner/formats/decorators.py @@ -0,0 +1,290 @@ +# -*- coding: utf-8 -*- +""" +Decoiratoprs for manging loader and saver functions. +""" +from typing import Optional, List, Tuple, Callable +from importlib import import_module +from inspect import signature, _empty +from pathlib import Path +import sys + +from ..core.base import SortedMultivalueDict +from ..core.exceptions import StonerLoadError + +_loaders_by_type = SortedMultivalueDict() +_loaders_by_pattern = SortedMultivalueDict() +_loaders_by_name = dict() +_savers_by_pattern = SortedMultivalueDict() +_savers_by_name = dict() + +LoadQualifier = Optional[str | Tuple[str, int] | List[str] | List[Tuple[str, int]]] + + +def register_loader( + mime_types: LoadQualifier = None, + patterns: LoadQualifier = None, + priority: Optional[int] = 256, + name: Optional[str] = None, + what: Optional[str] = None, +) -> Callable: + """Decorator to store a loader function indexed by mime_type or extension and priority. + + Keyword Arguments: + mime_type(str, or list of str, tuple(str, int), list pf tuple(str, int) or None): + (default None) - Mime-types of files that this loader might work with + patterns (str or list of str, tuple(str, int), list pf tuple(str, int) or None): + (default None) - filenbame extensions of files that this loader might handle + priority (int): + (default 256) - sets default order in which loaders are tried when multiple loaders might work + with a given pattern or mime-type if per pattern/type priorities are not specified. + Lower priority numbers are tried first. + name (str, None): + (default None) - The human readable name for this loader (or the function name if None) + what (str, None): + (default None) - The type of object that this loader is going to return - if None then it will try to get + a return type annotation. + + Notes: + If not pattern is set, then the default "*" pattern is used. + """ + + def innner(func: Callable) -> Callable: + """Actual decorator for laoder functions. + + Args: + func (callable): + Loader function to be registered. + """ + if name is None: + inner_name = func.__name__ + else: + inner_name = name + if patterns and not isinstance(patterns, list): + inner_patterns = [patterns] + else: + inner_patterns = patterns + if mime_types and not isinstance(mime_types, list): + inner_mime_types = [mime_types] + else: + inner_mime_types = mime_types + func.patterns = inner_patterns + func.mime_types = inner_mime_types + func.priority = priority + func.name = inner_name + if what is None: + inner_what = signature(func).return_annotation + if inner_what == _empty: + inner_what = None + else: + inner_what = what + func.what = str(inner_what) + if inner_mime_types: + for mime_type in inner_mime_types: + match mime_type: # Accept either a tuple of (mime_type,priority) or just string mime_type + case (str(mime_type), int(p)): # per mime_type priority + _loaders_by_type[mime_type] = (p, func) + case str(mime_type): # Use global priority + _loaders_by_type[mime_type] = (func.priority, func) + case _: # Unrecognised + raise TypeError("Mime-Type should be a str or (str, int)") + if inner_patterns: + for pattern in inner_patterns: + match pattern: # Like mime-type, accept tuple or pattern, priority or just string pattern + case (str(pattern), int(p)): + _loaders_by_pattern[pattern] = (p, func) + case str(pattern): # Bare pattenr - use gloabl priority + _loaders_by_pattern[pattern] = (func.priority, func) + case _: # Unrecognised + raise TypeError("Pattern shpuld be either a str or (str, int)") + # All loaders register on the default + _loaders_by_pattern["*"] = (func.priority, func) + _loaders_by_name[inner_name] = func + return func + + return innner + + +def register_saver( + patterns: LoadQualifier = None, priority: int = 256, name: Optional[str] = None, what: Optional[str] = None +) -> Callable: + """Decorator to store a loader function indexed by mime_type or extension and priority. + + Keyword Arguments: + patterns (str or list of str, tuple(str, int), list pf tuple(str, int) or None): + (default None) - filenbame extensions of files that this loader might handle + priority (int): + (default 256) - sets default order in which loaders are tried when multiple savers might work + with a given pattern if per pattern priorities are not specified. + Lower priority numbers are tried first. + name (str, None): + (default None) - The human readable name for this loader (or the function name if None) + what (str, None): + (default None) - The type of object that this loader is going to return - if None then it will try to get + a return type annotation. + + Notes: + If not pattern is set, then the default "*" pattern is used. + """ + + def innner(func: Callable) -> Callable: + """Actual decorator for laoder functions. + + Args: + func (callable): + Loader function to be registered. + """ + if name is None: + inner_name = func.__name__ + else: + inner_name = name + if patterns and not isinstance(patterns, list): + inner_patterns = [patterns] + else: + inner_patterns = patterns + func.patterns = inner_patterns + func.priority = priority + func.name = inner_name + if what is None: + inner_what = signature(func).return_annotation + if inner_what == _empty: + inner_what = None + else: + inner_what = what + func.what = str(inner_what) + if inner_patterns: + for pattern in inner_patterns: + match pattern: # Like mime-type, accept tuple or pattern, priority or just string pattern + case (str(pattern), int(p)): + _savers_by_pattern[pattern] = (p, func) + case str(pattern): # Bare pattenr - use gloabl priority + _savers_by_pattern[pattern] = (func.priority, func) + case _: # Unrecognised + raise TypeError("Pattern shpuld be either a str or (str, int)") + # All savers register on the default + _savers_by_pattern["*"] = (func.priority, func) + _savers_by_name[inner_name] = func + + return func + + return innner + + +def next_loader(pattern: Optional[str] = "*", mime_type: Optional[str] = None, what: Optional[str] = None) -> Callable: + """Find possible loaders and yield them in turn. + + Keyword Args: + pattern (str, None): + (deault None) - if the file to load has an extension, use this. + mime-type (str,None): + (default None) - if we have a mime-type for the file, use this. + what (str, None): + (default None) - limit the return values to things that can load the specified class. If None, then + this check is skipped. + + Yields: + func (callable): + The next loader function to try. + + Notes: + If avaialbe, iterate through all loaders that match that particular mime-type, but also match the pattern + if it is available (which is should be!). If mime-type is not specified, then just match by pattern and if + neither are specified, then yse the default no pattern "*". + """ + if mime_type is not None and mime_type in _loaders_by_type: # Prefer mime-type if available + for func in _loaders_by_type.get_value_list(mime_type): + if pattern and pattern != "*" and pattern not in func.patterns: + # If we have both pattern and type, match patterns too. + continue + if what and what != func.what: # If we are limiting what we can load, do that check + continue + yield func + if pattern in _loaders_by_pattern: # Fall back to specific pattern + for func in _loaders_by_pattern.get_value_list(pattern): + if what and what != func.what: # If we are limiting what we can load, do that check + continue + yield func + if pattern != "*": # Fall back again to generic pattern + for func in _loaders_by_pattern.get_value_list("*"): + if what and what != func.what: # If we are limiting what we can load, do that check + continue + yield func + return StopIteration + + +def best_saver(filename: str, name: Optional[str], what: Optional[str] = None) -> Callable: + """Figure out the best saving routine registerd with the package.""" + if name and name in _savers_by_name: + return _savers_by_name[name] + extension = Path(filename).suffix + if extension in _savers_by_pattern: + for _, func in _savers_by_pattern[extension]: + if what is None or (what and what == func.what): + return func + for _, func in _savers_by_pattern["*"]: + if what is None or (what and what == func.what): + return func + raise ValueError(f"Unable to find a saving routine for {filename}") + + +def get_loader(filetype, silent=False): + """Return the loader function by name. + + Args: + filetype (str): Filetype to get loader for. + silent (bool): If False (default) raise a StonerLoadError if filetype doesn't have a loade. + + Returns: + (callable): Matching loader. + + Notes: + If the filetype is not found and contains a . then it tries to import a module with the same name int he + hope that that defines the missing loader. If that fails to work, then either raises StonerLoadError or + returns None, depending on *silent*. + """ + try: + return _loaders_by_name[filetype] + except KeyError as err: + if "." in filetype: + try: + module_name = ".".join(filetype.split(".")[:-1]) + if module_name not in sys.modules: + import_module(module_name) + ret = _loaders_by_name.get(filetype, _loaders_by_name.get(filetype.split(".")[-1], None)) + if ret: + return ret + except ImportError: + pass + if not silent: + raise StonerLoadError(f"Cannot locate a loader function for {filetype}") from err + + +def get_saver(filetype, silent=False): + """Return the saver function by name. + + Args: + filetype (str): Filetype to get saver for. + silent (bool): If False (default) raise a Stonersaverror if filetype doesn't have a loade. + + Returns: + (callable): Matching saver. + + Notes: + If the filetype is not found and contains a . then it tries to import a module with the same name int he + hope that that defines the missing saver. If that fails to work, then either raises Stonersaverror or + returns None, depending on *silent*. + """ + try: + return _savers_by_name[filetype] + except KeyError as err: + if "." in filetype: + try: + module_name = ".".join(filetype.split(".")[:-1]) + if module_name not in sys.modules: + import_module(module_name) + ret = _savers_by_name.get(filetype, _savers_by_name.get(filetype.split(".")[-1], None)) + if ret: + return ret + except ImportError: + pass + if not silent: + raise StonerLoadError(f"Cannot locate a loader function for {filetype}") from err diff --git a/Stoner/formats/facilities.py b/Stoner/formats/facilities.py index 363a3c324..a7fae5388 100755 --- a/Stoner/formats/facilities.py +++ b/Stoner/formats/facilities.py @@ -2,390 +2,313 @@ # -*- coding: utf-8 -*- """Implements DataFile like classes for various large scale facilities.""" -__all__ = ["BNLFile", "MDAASCIIFile", "OpenGDAFile", "RasorFile", "SNSFile", "ESRF_DataFile", "ESRF_ImageFile"] # Standard Library imports import linecache import re import numpy as np -from .. import Core, Image from ..compat import str2bytes from ..core.base import string_to_type from ..tools.file import FileManager from ..core.exceptions import StonerLoadError +from .decorators import register_loader + try: import fabio except ImportError: fabio = None -class BNLFile(Core.DataFile): - - """Reader of files in the SPEC format given by BNL (specifically u4b beamline but hopefully generalisable). - - Author RCT 12/2011 - - The file from BNL must be split into separate scan files before Stoner can use - them, a separate python script has been written for this and should be found - in data/Python/PythonCode/scripts. +def _BNL_find_lines(new_data): + """Return an array of ints [header_line,data_line,scan_line,date_line,motor_line].""" + with FileManager(new_data.filename, "r", errors="ignore", encoding="utf-8") as fp: + new_data.line_numbers = [0, 0, 0, 0, 0] + counter = 0 + for line in fp: + counter += 1 + if counter == 1 and line[0] != "#": + raise StonerLoadError("Not a BNL File ?") + if len(line) < 2: + continue # if there's nothing written on the line go to the next + if line[0:2] == "#L": + new_data.line_numbers[0] = counter + elif line[0:2] == "#S": + new_data.line_numbers[2] = counter + elif line[0:2] == "#D": + new_data.line_numbers[3] = counter + elif line[0:2] == "#P": + new_data.line_numbers[4] = counter + elif line[0] in ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"]: + new_data.line_numbers[1] = counter + break + + +def _BNL_get_metadata(new_data): + """Load metadta from file. + + Metadata found is scan number 'Snumber', scan type and parameters 'Stype', + scan date/time 'Sdatetime' and z motor position 'Smotor'. """ - - #: priority (int): is the load order for the class, smaller numbers are tried before larger numbers. - # .. note:: - # Subclasses with priority<=32 should make some positive identification that they have the right - # file type before attempting to read data. - priority = 64 - #: pattern (list of str): A list of file extensions that might contain this type of file. Used to construct - # the file load/save dialog boxes. - patterns = ["*.txt"] # Recognised filename patterns - - def __init__(self, *params): - """Note line numbers. - - Do a normal initiation using the parent class 'self' followed by adding an extra attribute line_numbers, - line_numbers is a list of important line numbers in the file. - I've left it open for someone to add options for more args if they wish. - """ - super().__init__(*params) - self.line_numbers = [] - - def __find_lines(self): - """Return an array of ints [header_line,data_line,scan_line,date_line,motor_line].""" - with FileManager(self.filename, "r", errors="ignore", encoding="utf-8") as fp: - self.line_numbers = [0, 0, 0, 0, 0] - counter = 0 - for line in fp: - counter += 1 - if counter == 1 and line[0] != "#": - raise Core.StonerLoadError("Not a BNL File ?") - if len(line) < 2: - continue # if there's nothing written on the line go to the next - if line[0:2] == "#L": - self.line_numbers[0] = counter - elif line[0:2] == "#S": - self.line_numbers[2] = counter - elif line[0:2] == "#D": - self.line_numbers[3] = counter - elif line[0:2] == "#P": - self.line_numbers[4] = counter - elif line[0] in ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"]: - self.line_numbers[1] = counter - break - - def __get_metadata(self): - """Load metadta from file. - - Metadata found is scan number 'Snumber', scan type and parameters 'Stype', - scan date/time 'Sdatetime' and z motor position 'Smotor'. - """ - scanLine = linecache.getline(self.filename, self.line_numbers[2]) - dateLine = linecache.getline(self.filename, self.line_numbers[3]) - motorLine = linecache.getline(self.filename, self.line_numbers[4]) - self.__setitem__("Snumber", scanLine.split()[1]) - tmp = "".join(scanLine.split()[2:]) - self.__setitem__("Stype", "".join(tmp.split(","))) # get rid of commas - self.__setitem__("Sdatetime", dateLine[3:-1]) # don't want \n at end of line so use -1 - self.__setitem__("Smotor", motorLine.split()[3]) - - def __parse_BNL_data(self): - """Parse BNL data. - - The meta data is labelled by #L type tags - so easy to find but #L must be excluded from the result. - """ - self.__find_lines() - # creates a list, line_numbers, formatted [header_line,data_line,scan_line,date_line,motor_line] - header_string = linecache.getline(self.filename, self.line_numbers[0]) - header_string = re.sub(r'["\n]', "", header_string) # get rid of new line character - header_string = re.sub(r"#L", "", header_string) # get rid of line indicator character - column_headers = map(lambda x: x.strip(), header_string.split()) - self.__get_metadata() - try: - self.data = np.genfromtxt(self.filename, skip_header=self.line_numbers[1] - 1) - except IOError: - self.data = np.array([0]) - print(f"Did not import any data for {self.filename}") - self.column_headers = column_headers - - def _load(self, filename, *args, **kargs): # fileType omitted, implicit in class call - """Load the file from disc. - - Args: - filename (string or bool): - File to load. If None then the existing filename is used, if False, then a file dialog will be used. - - Returns: - A copy of the itself after loading the data. - - Notes: - Overwrites load method in Core.DataFile class, no header positions and data - positions are needed because of the hash title structure used in BNL files. - - Normally its good to use _parse_plain_data method from Core.DataFile class - to load data but unfortunately Brookhaven data isn't very plain so there's - a new method below. - """ - self.filename = filename - try: - self.__parse_BNL_data() # call an internal function rather than put it in load function - except (IndexError, TypeError, ValueError, StonerLoadError) as err: - raise StonerLoadError("Not parseable as a NFLS file!") from err - linecache.clearcache() - return self + scanLine = linecache.getline(new_data.filename, new_data.line_numbers[2]) + dateLine = linecache.getline(new_data.filename, new_data.line_numbers[3]) + motorLine = linecache.getline(new_data.filename, new_data.line_numbers[4]) + new_data.__setitem__("Snumber", scanLine.split()[1]) + tmp = "".join(scanLine.split()[2:]) + new_data.__setitem__("Stype", "".join(tmp.split(","))) # get rid of commas + new_data.__setitem__("Sdatetime", dateLine[3:-1]) # don't want \n at end of line so use -1 + new_data.__setitem__("Smotor", motorLine.split()[3]) -class MDAASCIIFile(Core.DataFile): +def _parse_BNL_data(new_data): + """Parse BNL data. - """Reads files generated from the APS.""" - - priority = 16 - patterns = ["*.txt"] # Recognised filename patterns - - def _load(self, filename=None, *args, **kargs): - """Load function. File format has space delimited columns from row 3 onwards.""" - if filename is None or not filename: - self.get_filename("r") - else: - self.filename = filename - i = [0, 0, 0, 0] - with FileManager(self.filename, "r", errors="ignore", encoding="utf-8") as data: # Slightly ugly text handling - for i[0], line in enumerate(data): - if ( - i[0] == 0 and line.strip() != "## mda2ascii 1.2 generated output" - ): # bug out oif we don't like the header - raise Core.StonerLoadError("Not a file mda2ascii") - line.strip() - if "=" in line: - parts = line[2:].split("=") - self[parts[0].strip()] = string_to_type("".join(parts[1:]).strip()) - elif line.startswith("# Extra PV:"): - # Onto the next metadata bit - break - pvpat = re.compile(r"^#\s+Extra\s+PV\s\d+\:(.*)") - for i[1], line in enumerate(data): - if line.strip() == "": - continue - if line.startswith("# Extra PV"): - res = pvpat.match(line) - bits = [b.strip().strip(r'"') for b in res.group(1).split(",")] - if bits[1] == "": - key = bits[0] - else: - key = bits[1] - if len(bits) > 3: - key = f"{key} ({bits[3]})" - self[key] = string_to_type(bits[2]) + The meta data is labelled by #L type tags + so easy to find but #L must be excluded from the result. + """ + _BNL_find_lines(new_data) + # creates a list, line_numbers, formatted [header_line,data_line,scan_line,date_line,motor_line] + header_string = linecache.getline(new_data.filename, new_data.line_numbers[0]) + header_string = re.sub(r'["\n]', "", header_string) # get rid of new line character + header_string = re.sub(r"#L", "", header_string) # get rid of line indicator character + column_headers = map(lambda x: x.strip(), header_string.split()) + _BNL_get_metadata(new_data) + try: + new_data.data = np.genfromtxt(new_data.filename, skip_header=new_data.line_numbers[1] - 1) + except IOError: + new_data.data = np.array([0]) + print(f"Did not import any data for {new_data.filename}") + new_data.column_headers = column_headers + + +@register_loader(patterns=(".txt", 64), mime_types=("text/plain", 64), name="BNLFile", what="Data") +def load_bnl(new_data, filename, *args, **kargs): # fileType omitted, implicit in class call + """Load the file from disc. + + Args: + filename (string or bool): + File to load. If None then the existing filename is used, if False, then a file dialog will be used. + + Returns: + A copy of the itnew_data after loading the data. + + Notes: + Overwrites load method in Core.DataFile class, no header positions and data + positions are needed because of the hash title structure used in BNL files. + + Normally its good to use _parse_plain_data method from Core.DataFile class + to load data but unfortunately Brookhaven data isn't very plain so there's + a new method below. + """ + new_data.filename = filename + try: + _parse_BNL_data(new_data) # call an internal function rather than put it in load function + except (IndexError, TypeError, ValueError, StonerLoadError) as err: + raise StonerLoadError("Not parseable as a NFLS file!") from err + linecache.clearcache() + return new_data + + +@register_loader(patterns=(".txt", 32), mime_types=("text/plain", 32), name="MDAASCIIFile", what="Data") +def load_mdaasci(new_data, filename=None, *args, **kargs): + """Load function. File format has space delimited columns from row 3 onwards.""" + new_data.filename = filename + i = [0, 0, 0, 0] + with FileManager(new_data.filename, "r", errors="ignore", encoding="utf-8") as data: # Slightly ugly text handling + for i[0], line in enumerate(data): + if ( + i[0] == 0 and line.strip() != "## mda2ascii 1.2 generated output" + ): # bug out oif we don't like the header + raise StonerLoadError("Not a file mda2ascii") + line.strip() + if "=" in line: + parts = line[2:].split("=") + new_data[parts[0].strip()] = string_to_type("".join(parts[1:]).strip()) + elif line.startswith("# Extra PV:"): + # Onto the next metadata bit + break + pvpat = re.compile(r"^#\s+Extra\s+PV\s\d+\:(.*)") + for i[1], line in enumerate(data): + if line.strip() == "": + continue + if line.startswith("# Extra PV"): + res = pvpat.match(line) + bits = [b.strip().strip(r'"') for b in res.group(1).split(",")] + if bits[1] == "": + key = bits[0] else: - break # End of Extra PV stuff + key = bits[1] + if len(bits) > 3: + key = f"{key} ({bits[3]})" + new_data[key] = string_to_type(bits[2]) else: - raise Core.StonerLoadError("Overran Extra PV Block") - for i[2], line in enumerate(data): - line.strip() - if line.strip() == "": - continue - elif line.startswith("# Column Descriptions:"): - break # Start of column headers now - elif "=" in line: - parts = line[2:].split("=") - self[parts[0].strip()] = string_to_type("".join(parts[1:]).strip()) - else: - raise Core.StonerLoadError("Overran end of scan header before column descriptions") - colpat = re.compile(r"#\s+\d+\s+\[([^\]]*)\](.*)") - column_headers = [] - for i[3], line in enumerate(data): - res = colpat.match(line) - line.strip() - if line.strip() == "": - continue - elif line.startswith("# 1-D Scan Values"): - break # Start of data - elif res is not None: - if "," in res.group(2): - bits = [b.strip() for b in res.group(2).split(",")] - if bits[-2] == "": - colname = bits[0] - else: - colname = bits[-2] - if bits[-1] != "": - colname += f"({bits[-1]})" - if colname in column_headers: - colname = f"{bits[0]}:{colname}" + break # End of Extra PV stuff + else: + raise StonerLoadError("Overran Extra PV Block") + for i[2], line in enumerate(data): + line.strip() + if line.strip() == "": + continue + elif line.startswith("# Column Descriptions:"): + break # Start of column headers now + elif "=" in line: + parts = line[2:].split("=") + new_data[parts[0].strip()] = string_to_type("".join(parts[1:]).strip()) + else: + raise StonerLoadError("Overran end of scan header before column descriptions") + colpat = re.compile(r"#\s+\d+\s+\[([^\]]*)\](.*)") + column_headers = [] + for i[3], line in enumerate(data): + res = colpat.match(line) + line.strip() + if line.strip() == "": + continue + elif line.startswith("# 1-D Scan Values"): + break # Start of data + elif res is not None: + if "," in res.group(2): + bits = [b.strip() for b in res.group(2).split(",")] + if bits[-2] == "": + colname = bits[0] else: - colname = res.group(1).strip() - column_headers.append(colname) - else: - raise Core.StonerLoadError("Overand the end of file without reading data") - self.data = np.genfromtxt(self.filename, skip_header=sum(i)) # so that's ok then ! - self.column_headers = column_headers - return self + colname = bits[-2] + if bits[-1] != "": + colname += f"({bits[-1]})" + if colname in column_headers: + colname = f"{bits[0]}:{colname}" + else: + colname = res.group(1).strip() + column_headers.append(colname) + else: + raise StonerLoadError("Overand the end of file without reading data") + new_data.data = np.genfromtxt(new_data.filename, skip_header=sum(i)) # so that's ok then ! + new_data.column_headers = column_headers + return new_data -class OpenGDAFile(Core.DataFile): +@register_loader(patterns=(".dat", 16), mime_types=("text/plain", 16), name="OpenGDAFile", what="Data") +def load_gda(new_data, filename=None, *args, **kargs): + """Load an OpenGDA file. - """Extends Core.DataFile to load files from RASOR.""" + Args: + filename (string or bool): File to load. If None then the existing filename is used, + if False, then a file dialog will be used. - priority = 16 # Makes a positive ID of it's file type so give priority - patterns = ["*.dat"] # Recognised filename patterns + Returns: + A copy of the itnew_data after loading the data. + """ + new_data.filename = filename + i = 0 + with FileManager(new_data.filename, "r", errors="ignore", encoding="utf-8") as f: + for i, line in enumerate(f): + line = line.strip() + if i == 0 and line != "&SRS": + raise StonerLoadError(f"Not a GDA File from Rasor ?\n{line}") + if "&END" in line: + break + parts = line.split("=") + if len(parts) != 2: + continue + key = parts[0] + value = parts[1].strip() + new_data.metadata[key] = string_to_type(value) + column_headers = f.readline().strip().split("\t") + new_data.data = np.genfromtxt([str2bytes(l) for l in f], dtype="float", invalid_raise=False) + new_data.column_headers = column_headers + return new_data + + +@register_loader(patterns=(".dat", 16), mime_types=("text/plain", 16), name="SNSFile", what="Data") +def load_sns(new_data, filename=None, *args, **kargs): + """Load function. File format has space delimited columns from row 3 onwards.""" + new_data.filename = filename + with FileManager(new_data.filename, "r", errors="ignore", encoding="utf-8") as data: # Slightly ugly text handling + line = data.readline() + if not line.strip().startswith( + "# Datafile created by QuickNXS 0.9.39" + ): # bug out oif we don't like the header + raise StonerLoadError("Not a file from the SNS BL4A line") + for line in data: + if line.startswith("# "): # We're in the header + line = line[2:].strip() # strip the header and whitespace + + if line.startswith("["): # Look for a section header + section = line.strip().strip("[]") + if section == "Data": # The Data section has one line of column headers and then data + header = next(data)[2:].split("\t") + column_headers = [h.strip() for h in header] + new_data.data = np.genfromtxt(data) # we end by reading the raw data + elif section == "Global Options": # This section can go into metadata + for line in data: + line = line[2:].strip() + if line.strip() == "": + break + else: + new_data[line[2:10].strip()] = line[11:].strip() + elif ( + section == "Direct Beam Runs" or section == "Data Runs" + ): # These are constructed into lists ofg dictionaries for each file + sec = list() + header = next(data) + header = header[2:].strip() + keys = [s.strip() for s in header.split(" ") if s.strip()] + for line in data: + line = line[2:].strip() + if line == "": + break + else: + values = [s.strip() for s in line.split(" ") if s.strip()] + sec.append(dict(zip(keys, values))) + new_data[section] = sec + else: # We must still be in the opening un-labelled section of meta data + if ":" in line: + i = line.index(":") + key = line[:i].strip() + value = line[i + 1 :].strip() + new_data[key.strip()] = value.strip() + new_data.column_headers = column_headers + return new_data - def _load(self, filename=None, *args, **kargs): - """Load an OpenGDA file. - Args: - filename (string or bool): File to load. If None then the existing filename is used, - if False, then a file dialog will be used. +if fabio: - Returns: - A copy of the itself after loading the data. - """ + @register_loader( + patterns=(".edf", 16), + mime_types=[("application/octet-stream", 16), ("text/plain", 15)], + name="ESRF_DataFile", + what="Data", + ) + def load_esrf(self, filename=None, *args, **kargs): + """Load function. File format has space delimited columns from row 3 onwards.""" if filename is None or not filename: self.get_filename("r") else: self.filename = filename - i = 0 - with FileManager(self.filename, "r", errors="ignore", encoding="utf-8") as f: - for i, line in enumerate(f): - line = line.strip() - if i == 0 and line != "&SRS": - raise Core.StonerLoadError(f"Not a GDA File from Rasor ?\n{line}") - if "&END" in line: - break - parts = line.split("=") - if len(parts) != 2: - continue - key = parts[0] - value = parts[1].strip() - self.metadata[key] = string_to_type(value) - column_headers = f.readline().strip().split("\t") - self.data = np.genfromtxt([str2bytes(l) for l in f], dtype="float", invalid_raise=False) - self.column_headers = column_headers - return self - - -class RasorFile(OpenGDAFile): - - """Just an alias for OpenGDAFile.""" - - -class SNSFile(Core.DataFile): - - """Reads the ASCII exported PNR reduced files from BL-4A line at the SSNS at Oak Ridge National Lab. - - File has a large header marked up with # prefixes which include several section is [] - Each section seems to have a slightly different format - """ - - #: priority (int): is the load order for the class, smaller numbers are tried before larger numbers. - # .. note:: - # Subclasses with priority<=32 should make some positive identification that they have the right - # file type before attempting to read data. - priority = 16 - #: pattern (list of str): A list of file extensions that might contain this type of file. Used to construct - # the file load/save dialog boxes. - patterns = ["*.dat"] # Recognised filename patterns - - def _load(self, filename=None, *args, **kargs): + try: + img = fabio.edfimage.edfimage().read(self.filename) + self.data = img.data + self.metadata.update(img.header) + return self + except (OSError, ValueError, TypeError, IndexError) as err: + raise StonerLoadError("Not an ESRF data file !") from err + + @register_loader( + patterns=(".edf", 32), + mime_types=[("text/plain", 32), ("application/octet-stream", 32)], + name="FabioImage", + what="Image", + ) + def load_fabio(new_data, filename=None, *args, **kargs): """Load function. File format has space delimited columns from row 3 onwards.""" if filename is None or not filename: - self.get_filename("r") + new_data.get_filename("r") else: - self.filename = filename - - with FileManager(self.filename, "r", errors="ignore", encoding="utf-8") as data: # Slightly ugly text handling - line = data.readline() - if not line.strip().startswith( - "# Datafile created by QuickNXS 0.9.39" - ): # bug out oif we don't like the header - raise Core.StonerLoadError("Not a file from the SNS BL4A line") - for line in data: - if line.startswith("# "): # We're in the header - line = line[2:].strip() # strip the header and whitespace - - if line.startswith("["): # Look for a section header - section = line.strip().strip("[]") - if section == "Data": # The Data section has one line of column headers and then data - header = next(data)[2:].split("\t") - column_headers = [h.strip() for h in header] - self.data = np.genfromtxt(data) # we end by reading the raw data - elif section == "Global Options": # This section can go into metadata - for line in data: - line = line[2:].strip() - if line.strip() == "": - break - else: - self[line[2:10].strip()] = line[11:].strip() - elif ( - section == "Direct Beam Runs" or section == "Data Runs" - ): # These are constructed into lists ofg dictionaries for each file - sec = list() - header = next(data) - header = header[2:].strip() - keys = [s.strip() for s in header.split(" ") if s.strip()] - for line in data: - line = line[2:].strip() - if line == "": - break - else: - values = [s.strip() for s in line.split(" ") if s.strip()] - sec.append(dict(zip(keys, values))) - self[section] = sec - else: # We must still be in the opening un-labelled section of meta data - if ":" in line: - i = line.index(":") - key = line[:i].strip() - value = line[i + 1 :].strip() - self[key.strip()] = value.strip() - self.column_headers = column_headers - return self - - -if fabio: - - class ESRF_DataFile(Core.DataFile): - - """Utilise the fabIO library to read an edf file has a DataFile.""" - - priority = 16 - patterns = ["*.edf"] - mime_type = ["application/octet-stream", "text/plain"] - - def _load(self, filename=None, *args, **kargs): - """Load function. File format has space delimited columns from row 3 onwards.""" - if filename is None or not filename: - self.get_filename("r") - else: - self.filename = filename - try: - img = fabio.edfimage.edfimage().read(self.filename) - self.data = img.data - self.metadata.update(img.header) - return self - except (OSError, ValueError, TypeError, IndexError) as err: - raise StonerLoadError("Not an ESRF data file !") from err - - class FabioImageFile(Image.ImageFile): - - """Utilise the fabIO library to read an edf file has a DataFile.""" - - priority = 32 - patterns = ["*.edf"] - mime_type = ["text/plain", "application/octet-stream"] - - def _load(self, filename=None, *args, **kargs): - """Load function. File format has space delimited columns from row 3 onwards.""" - if filename is None or not filename: - self.get_filename("r") - else: - self.filename = filename - try: - img = fabio.open(self.filename) - self.image = img.data - self.metadata.update(img.header) - return self - except (OSError, ValueError, TypeError, IndexError) as err: - raise StonerLoadError("Not a Fabio Image file !") from err - -else: - ESRF_DataFile = None - ESRF_ImageFile = None + new_data.filename = filename + try: + img = fabio.open(new_data.filename) + new_data.image = img.data + new_data.metadata.update(img.header) + return new_data + except (OSError, ValueError, TypeError, IndexError) as err: + raise StonerLoadError("Not a Fabio Image file !") from err diff --git a/Stoner/formats/generic.py b/Stoner/formats/generic.py index 21a02eb32..f9df277ea 100755 --- a/Stoner/formats/generic.py +++ b/Stoner/formats/generic.py @@ -1,23 +1,32 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- """Implement DataFile classes for some generic file formats.""" -__all__ = ["CSVFile", "HyperSpyFile", "KermitPNGFile", "TDMSFile"] import csv import contextlib +import copy import io +from pathlib import Path import re from collections.abc import Mapping import sys import logging +from typing import Optional +import warnings import PIL import numpy as np -from ..Core import DataFile from ..compat import str2bytes, Hyperspy_ok, hs, hsload +from ..core.array import DataArray from ..core.exceptions import StonerLoadError +from ..core.utils import tab_delimited +from ..tools import make_Data +from ..Image import ImageArray + from ..tools.file import FileManager +from .decorators import register_loader, register_saver + class _refuse_log(logging.Filter): @@ -72,327 +81,452 @@ def _delim_detect(line): return current[0] -class CSVFile(DataFile): - - """A subclass of DataFiule for loading generic deliminated text fiules without metadata.""" +@register_loader( + patterns=[(".tif", 8), (".tiff", 8), (".png", 8), (".npy", 8)], + mime_types=[("image/tiff", 8), ("image/png", 8), ("application/octet-stream", 8)], + name="ImageFile", + what="Image", +) +def load_imagefile(new_image, filename, *args, **kargs): + """Load an ImageFile by calling the ImageArray method instead.""" + new_image._image = ImageArray(filename, *args, **kargs) + for k in new_image._image._public_attrs: + setattr(new_image, k, getattr(new_image._image, k, None)) + return new_image + + +@register_loader( + patterns=[(".dat", 8), (".txt", 8), ("*", 8)], + mime_types=[("application/tsv", 8), ("text/plain", 8), ("text/tab-separated-values", 8)], + name="DataFile", + what="Data", +) +def load_tdi_format(new_data, filename, *args, **kargs): + """Actually load the data from disc assuming a .tdi file format. - #: priority (int): is the load order for the class, smaller numbers are tried before larger numbers. - # .. note:: - # Subclasses with priority<=32 should make some positive identification that they have the right - # file type before attempting to read data. - priority = 128 # Rather generic file format so make it a low priority - #: pattern (list of str): A list of file extensions that might contain this type of file. Used to construct - # the file load/save dialog boxes. - patterns = ["*.csv", "*.txt"] # Recognised filename patterns - - _defaults = {"header_line": 0, "data_line": 1, "header_delim": ",", "data_delim": ","} + Args: + filename (str): + Path to filename to be loaded. If None or False, a dialog bax is raised to ask for the filename. - mime_type = ["application/csv", "text/plain", "text/csv"] + Returns: + DataFile: + A copy of the newly loaded :py:class`DataFile` object. - def _load(self, filename, *args, **kargs): - """Load generic deliminated files. + Exceptions: + StonerLoadError: + Raised if the first row does not start with 'TDI Format 1.5' or 'TDI Format=1.0'. - Args: - filename (string or bool): File to load. If None then the existing filename is used, - if False, then a file dialog will be used. + Note: + The *_load* methods shouldbe overridden in each child class to handle the process of loading data from + disc. If they encounter unexpected data, then they should raise StonerLoadError to signal this, so that + the loading class can try a different sub-class instead. + """ + if filename is None or not filename: + new_data.get_filename("r") + else: + new_data.filename = filename + with FileManager(new_data.filename, "r", encoding="utf-8", errors="ignore") as datafile: + line = datafile.readline() + if line.startswith("TDI Format 1.5"): + fmt = 1.5 + elif line.startswith("TDI Format=Text 1.0"): + fmt = 1.0 + else: + raise StonerLoadError("Not a TDI File") + + datafile.seek(0) + reader = csv.reader(datafile, dialect=tab_delimited()) + cols = 0 + for ix, metadata in enumerate(reader): + if ix == 0: + row = metadata + continue + if len(metadata) < 1: + continue + if cols == 0: + cols = len(metadata) + if len(metadata) > 1: + max_rows = ix + 1 + if "=" in metadata[0]: + new_data.metadata.import_key(metadata[0]) + col_headers_tmp = [x.strip() for x in row[1:]] + with warnings.catch_warnings(): + datafile.seek(0) + warnings.filterwarnings("ignore", "Some errors were detected !") + data = np.genfromtxt( + datafile, + skip_header=1, + usemask=True, + delimiter="\t", + usecols=range(1, cols), + invalid_raise=False, + comments="\0", + missing_values=[""], + filling_values=[np.nan], + max_rows=max_rows, + ) + if data.ndim < 2: + data = np.ma.atleast_2d(data) + retain = np.all(np.isnan(data), axis=1) + new_data.data = DataArray(data[~retain]) + new_data["TDI Format"] = fmt + new_data["Stoner.class"] = "Data" + if new_data.data.ndim == 2 and new_data.data.shape[1] > 0: + new_data.column_headers = col_headers_tmp + new_data.metadata = copy.deepcopy(new_data.metadata) # This fixes some type issues TODO - work out why! + return new_data + + +@register_saver( + patterns=[(".dat", 8), (".txt", 8), ("*", 8)], + name="DataFile", + what="Data", +) +def save_tdi_format(save_data, filename): + """Write out a DataFile to a tab delimited tdi text file.""" + + header = ["TDI Format 1.5"] + header.extend(save_data.column_headers[: save_data.data.shape[1]]) + header = "\t".join(header) + mdkeys = sorted(save_data.metadata) + if len(mdkeys) > len(save_data): + mdremains = mdkeys[len(save_data) :] + mdkeys = mdkeys[0 : len(save_data)] + else: + mdremains = [] + mdtext = np.array([save_data.metadata.export(k) for k in mdkeys]) + if len(mdtext) < len(save_data): + mdtext = np.append(mdtext, np.zeros(len(save_data) - len(mdtext), dtype=str)) + data_out = np.column_stack([mdtext, save_data.data]) + fmt = ["%s"] * data_out.shape[1] + with io.open(filename, "w", errors="replace", encoding="utf-8") as f: + np.savetxt(f, data_out, fmt=fmt, header=header, delimiter="\t", comments="") + for k in mdremains: + f.write(save_data.metadata.export(k) + "\n") # (str2bytes(save_data.metadata.export(k) + "\n")) + + save_data.filename = filename + return save_data + + +@register_loader( + patterns=[(".csv", 32), (".txt", 256)], + mime_types=[("application/csv", 16), ("text/plain", 256), ("text/csv", 16)], + name="CSVFile", + what="Data", +) +def load_csvfile(new_data: "DataFile", filename: str, *args, **kargs) -> "DataFile": + """Load generic deliminated files. - Keyword Arguments: - header_line (int): The line in the file that contains the column headers. - If None, then column headers are automatically generated. - data_line (int): The line on which the data starts - data_delim (string): The delimiter used for separating data values - header_delim (strong): The delimiter used for separating header values + Args: + filename (string or bool): File to load. If None then the existing filename is used, + if False, then a file dialog will be used. - Returns: - A copy of the current object after loading the data. - """ - header_line = kargs.pop("header_line", self._defaults["header_line"]) - data_line = kargs.pop("data_line", self._defaults["data_line"]) - data_delim = kargs.pop("data_delim", self._defaults["data_delim"]) - header_delim = kargs.pop("header_delim", self._defaults["header_delim"]) + Keyword Arguments: + header_line (int): The line in the file that contains the column headers. + If None, then column headers are automatically generated. + data_line (int): The line on which the data starts + data_delim (string): The delimiter used for separating data values + header_delim (strong): The delimiter used for separating header values - if filename is None or not filename: - self.get_filename("r") - else: - self.filename = filename - - if data_delim is None or header_delim is None: # - with FileManager(self.filename, "r") as datafile: - lines = datafile.readlines() - if header_line is not None and header_delim is None: - header_delim = _delim_detect(lines[header_line]) - if data_line is not None and data_delim is None: - data_delim = _delim_detect(lines[data_line]) - - with FileManager(self.filename, "r") as datafile: - if header_line is not None: - try: - for ix, line in enumerate(datafile): - if ix == header_line: - break - else: - raise StonerLoadError("Ran out of file before readching header") - header = line.strip() - column_headers = next(csv.reader(io.StringIO(header), delimiter=header_delim)) - data = np.genfromtxt(datafile, delimiter=data_delim, skip_header=data_line - header_line) - except (TypeError, ValueError, csv.Error, StopIteration, UnicodeDecodeError) as err: - raise StonerLoadError("Header and data on the same line") from err - else: # Generate - try: - data = np.genfromtxt(datafile, delimiter=data_delim, skip_header=data_line) - except (TypeError, ValueError) as err: - raise StonerLoadError("Failed to open file as CSV File") from err - column_headers = ["Column" + str(x) for x in range(np.shape(data)[1])] - - self.data = data - self.column_headers = column_headers - self._kargs = kargs - return self - - def save(self, filename=None, **kargs): - """Override the save method to allow CSVFiles to be written out to disc (as a mininmalist output). + Returns: + A copy of the current object after loading the data. + """ + _defaults = {"header_line": 0, "data_line": 1, "header_delim": ",", "data_delim": ","} - Args: - filename (string): Filename to save as (using the same rules as for the load routines) + header_line = kargs.pop("header_line", _defaults["header_line"]) + data_line = kargs.pop("data_line", _defaults["data_line"]) + data_delim = kargs.pop("data_delim", _defaults["data_delim"]) + header_delim = kargs.pop("header_delim", _defaults["header_delim"]) - Keyword Arguments: - deliminator (string): Record deliniminator (defaults to a comma) + new_data.filename = filename - Returns: - A copy of itself. - """ - delimiter = kargs.pop("deliminator", ",") - if filename is None: - filename = self.filename - if filename is None or (isinstance(filename, bool) and not filename): # now go and ask for one - filename = self.__file_dialog("w") - with FileManager(filename, "w") as outfile: - spamWriter = csv.writer(outfile, delimiter=delimiter, quotechar='"', quoting=csv.QUOTE_MINIMAL) - i = 0 - spamWriter.writerow(self.column_headers) - while i < self.data.shape[0]: - spamWriter.writerow(self.data[i, :]) - i += 1 - self.filename = filename - return self + if data_delim is None or header_delim is None: # + with FileManager(new_data.filename, "r") as datafile: + lines = datafile.readlines() + if header_line is not None and header_delim is None: + header_delim = _delim_detect(lines[header_line]) + if data_line is not None and data_delim is None: + data_delim = _delim_detect(lines[data_line]) + with FileManager(new_data.filename, "r") as datafile: + if header_line is not None: + try: + for ix, line in enumerate(datafile): + if ix == header_line: + break + else: + raise StonerLoadError("Ran out of file before readching header") + header = line.strip() + column_headers = next(csv.reader(io.StringIO(header), delimiter=header_delim)) + data = np.genfromtxt(datafile, delimiter=data_delim, skip_header=data_line - header_line) + except (TypeError, ValueError, csv.Error, StopIteration, UnicodeDecodeError) as err: + raise StonerLoadError("Header and data on the same line") from err + else: # Generate + try: + data = np.genfromtxt(datafile, delimiter=data_delim, skip_header=data_line) + except (TypeError, ValueError) as err: + raise StonerLoadError("Failed to open file as CSV File") from err + column_headers = ["Column" + str(x) for x in range(np.shape(data)[1])] -class JustNumbersFile(CSVFile): + new_data.data = data + new_data.column_headers = column_headers + new_data.metadata |= kargs + return new_data - """A reader format for things which are just a block of numbers with no headers or metadata.""" - priority = 256 # Rather generic file format so make it a low priority - #: pattern (list of str): A list of file extensions that might contain this type of file. Used to construct - # the file load/save dialog boxes. - patterns = ["*.csv", "*.txt"] # Recognised filename patterns +@register_saver(patterns=[(".csv", 32), (".txt", 256)], name="CSVFile", what="Data") +def save_csvfile(save_data: "DataFile", filename: Optional[str] = None, **kargs): + """Override the save method to allow CSVFiles to be written out to disc (as a mininmalist output). - _defaults = {"header_line": None, "data_line": 0, "header_delim": None, "data_delim": None} + Args: + filename (string): Filename to save as (using the same rules as for the load routines) + Keyword Arguments: + deliminator (string): Record deliniminator (defaults to a comma) + no_header (bool): Whether to skip the headers, defaults to False (include colu,n headers) -class KermitPNGFile(DataFile): + Returns: + A copy of itsave_data. + """ + delimiter = kargs.pop("deliminator", ",") + if filename is None: + filename = save_data.filename + no_header = kargs.get("no_header", False) + if filename is None or (isinstance(filename, bool) and not filename): # now go and ask for one + filename = save_data.__file_dialog("w") + with FileManager(filename, "w") as outfile: + spamWriter = csv.writer(outfile, delimiter=delimiter, quotechar='"', quoting=csv.QUOTE_MINIMAL) + i = 0 + if not no_header: + spamWriter.writerow(save_data.column_headers) + while i < save_data.data.shape[0]: + spamWriter.writerow(save_data.data[i, :]) + i += 1 + save_data.filename = filename + return save_data + + +@register_loader( + patterns=[(".csv", 64), (".txt", 512)], + mime_types=[("application/csv", 64), ("text/plain", 512), ("text/csv", 64)], + name="JustNumbers", + what="Data", +) +def load_justnumbers(new_data: "DataFile", filename: str, *args, **kargs) -> "DataFile": + """Load generic deliminated files with no headers or metadata. - """Loads PNG files with additional metadata embedded in them and extracts as metadata.""" + Args: + filename (str): + File to load. - #: priority (int): is the load order for the class, smaller numbers are tried before larger numbers. - # .. note:: - # Subclasses with priority<=32 should make some positive identification that they have the right - # file type before attempting to read data. - priority = 16 # We're checking for a the specoific PNG signature - #: pattern (list of str): A list of file extensions that might contain this type of file. Used to construct - # the file load/save dialog boxes. - patterns = ["*.png"] # Recognised filename patterns + Keyword Arguments: + data_delim (string): The delimiter used for separating data values + header_delim (strong): The delimiter used for separating header values - mime_type = ["image/png"] + Returns: + A copy of the current object after loading the data. + """ + _defaults = {"header_line": None, "data_line": 0, "data_delim": None} + for karg, val in _defaults.items(): + kargs.setdefault(karg, val) + return load_csvfile(new_data, filename, *args, **kargs) - def _check_signature(self, filename): - """Check that this is a PNG file and raie a StonerLoadError if not.""" - try: - with FileManager(filename, "rb") as test: - sig = test.read(8) - sig = [x for x in sig] - if self.debug: - print(sig) - if sig != [137, 80, 78, 71, 13, 10, 26, 10]: - raise StonerLoadError("Signature mismatrch") - except (StonerLoadError, IOError) as err: - from traceback import format_exc - raise StonerLoadError(f"Not a PNG file!>\n{format_exc()}") from err - return True +@register_saver(patterns=[(".csv", 32), (".txt", 256)], name="JustNumbersFile", what="Data") +def save_justnumbers(save_data: "DataFile", filename: Optional[str] = None, **kargs): + """Override the save method to allow JustNumbersFiles to be written out to disc (as a very mininmalist output). - def _load(self, filename=None, *args, **kargs): - """PNG file loader routine. + Args: + filename (string): Filename to save as (using the same rules as for the load routines) - Args: - filename (string or bool): File to load. If None then the existing filename is used, - if False, then a file dialog will be used. + Keyword Arguments: + deliminator (string): Record deliniminator (defaults to a comma) - Returns: - A copy of the itself after loading the data. - """ - if filename is None or not filename: - self.get_filename("r") - else: - self.filename = filename - self._check_signature(filename) - try: - with PIL.Image.open(self.filename, "r") as img: - for k in img.info: - self.metadata[k] = img.info[k] - self.data = np.asarray(img) - except IOError as err: - raise StonerLoadError("Unable to read as a PNG file.") from err + Returns: + A copy of itsave_data. + """ + kargs["no_header"] = False + return save_csvfile(save_data, filename, **kargs) + + +def _check_png_signature(filename): + """Check that this is a PNG file and raie a StonerLoadError if not.""" + try: + with FileManager(filename, "rb") as test: + sig = test.read(8) + sig = [x for x in sig] + if sig != [137, 80, 78, 71, 13, 10, 26, 10]: + raise StonerLoadError("Signature mismatrch") + except (StonerLoadError, IOError) as err: + from traceback import format_exc + + raise StonerLoadError(f"Not a PNG file!>\n{format_exc()}") from err + return True + + +@register_loader( + patterns=(".png", 16), + mime_types=("image/png", 16), + name="KermitPNGFile", + what="Data", +) +def load_pngfile(new_data, filename=None, *args, **kargs): + """PNG file loader routine. - return self + Args: + filename (string or bool): File to load. If None then the existing filename is used, + if False, then a file dialog will be used. - def save(self, filename=None, **kargs): - """Override the save method to allow KermitPNGFiles to be written out to disc. + Returns: + A copy of the itnew_data after loading the data. + """ + new_data.filename = filename + _check_png_signature(filename) + try: + with PIL.Image.open(new_data.filename, "r") as img: + for k in img.info: + new_data.metadata[k] = img.info[k] + new_data.data = np.asarray(img) + except IOError as err: + raise StonerLoadError("Unable to read as a PNG file.") from err + new_data["Stoner.class"] = "Data" # Reset my load class + new_data.metadata = copy.deepcopy(new_data.metadata) # Fixes up some data types (see TDI loader) + return new_data + + +@register_saver(patterns=(".png", 8), name="ImageFile", what="Image") +@register_saver(patterns=(".png", 16), name="KermitPngFile", what="Data") +def save_pngfile(save_data, filename=None, **kargs): + """Override the save method to allow KermitPNGFiles to be written out to disc. - Args: - filename (string): Filename to save as (using the same rules as for the load routines) + Args: + filename (string): Filename to save as (using the same rules as for the load routines) - Keyword Arguments: - deliminator (string): Record deliniminator (defaults to a comma) + Keyword Arguments: + deliminator (string): Record deliniminator (defaults to a comma) - Returns: - A copy of itself. - """ - if filename is None: - filename = self.filename - if filename is None or (isinstance(filename, bool) and not filename): # now go and ask for one - filename = self.__file_dialog("w") - - metadata = PIL.PngImagePlugin.PngInfo() - for k in self.metadata: - parts = self.metadata.export(k).split("=") - key = parts[0] - val = str2bytes("=".join(parts[1:])) - metadata.add_text(key, val) - img = PIL.Image.fromarray(self.data) - img.save(filename, "png", pnginfo=metadata) - self.filename = filename - return self + Returns: + A copy of itsave_data. + """ + if filename is None: + filename = save_data.filename + if filename is None or (isinstance(filename, bool) and not filename): # now go and ask for one + filename = save_data.__file_dialog("w") + + metadata = PIL.PngImagePlugin.PngInfo() + for k in save_data.metadata: + parts = save_data.metadata.export(k).split("=") + key = parts[0] + val = str2bytes("=".join(parts[1:])) + metadata.add_text(key, val) + img = PIL.Image.fromarray(save_data.data) + img.save(filename, "png", pnginfo=metadata) + save_data.filename = filename + return save_data try: # Optional tdms support from nptdms import TdmsFile - class TDMSFile(DataFile): - - """First stab at writing a file that will import TDMS files.""" - - #: priority (int): is the load order for the class, smaller numbers are tried before larger numbers. - # .. note:: - # Subclasses with priority<=32 should make some positive identification that they have the right - # file type before attempting to read data. - priority = 16 # Makes a positive ID of its file contents - #: pattern (list of str): A list of file extensions that might contain this type of file. Used to construct - # the file load/save dialog boxes. - patterns = ["*.tdms"] # Recognised filename patterns + @register_loader( + patterns=[(".tdms", 16), (".tdms_index", 16)], + mime_types=("application/octet-stream", 16), + name="TDMSFile", + what="Data", + ) + def load_tdms(new_data, filename=None, *args, **kargs): + """TDMS file loader routine. - mime_type = ["application/octet-stream"] + Args: + filename (string or bool): File to load. If None then the existing filename is used, + if False, then a file dialog will be used. - def _load(self, filename=None, *args, **kargs): - """TDMS file loader routine. + Returns: + A copy of the itnew_data after loading the data. + """ + filename = Path(filename) + if filename.suffix == ".tdms_index": + filename = filename.parent / f"{filename.stem}.tdms" # rewrite filename for not the index file! + new_data.filename = filename + # Open the file and read the main file header and unpack into a dict + try: + f = TdmsFile(new_data.filename) + for grp in f.groups(): + if grp.path == "/": + pass # skip the rooot group + elif grp.path == "/'TDI Format 1.5'": + tmp = make_Data(grp.as_dataframe()) + new_data.data = tmp.data + new_data.column_headers = tmp.column_headers + new_data.metadata.update(grp.properties) + else: + tmp = DataFile(grp.as_dataframe()) + new_data.data = tmp.data + new_data.column_headers = tmp.column_headers + except (IOError, ValueError, TypeError, StonerLoadError) as err: + from traceback import format_exc - Args: - filename (string or bool): File to load. If None then the existing filename is used, - if False, then a file dialog will be used. + raise StonerLoadError(f"Not a TDMS File \n{format_exc()}") from err - Returns: - A copy of the itself after loading the data. - """ - if filename is None or not filename: - self.get_filename("r") - else: - self.filename = filename - # Open the file and read the main file header and unpack into a dict - try: - f = TdmsFile(self.filename) - for grp in f.groups(): - if grp.path == "/": - pass # skip the rooot group - elif grp.path == "/'TDI Format 1.5'": - tmp = DataFile(grp.as_dataframe()) - self.data = tmp.data - self.column_headers = tmp.column_headers - self.metadata.update(grp.properties) - else: - tmp = DataFile(grp.as_dataframe()) - self.data = tmp.data - self.column_headers = tmp.column_headers - except (IOError, ValueError, TypeError, StonerLoadError) as err: - from traceback import format_exc - - raise StonerLoadError(f"Not a TDMS File \n{format_exc()}") from err - - return self + return new_data except ImportError: - TDMSFile = DataFile + pass if Hyperspy_ok: - class HyperSpyFile(DataFile): - - """Wrap the HyperSpy file to map to DataFile.""" - - priority = 64 # Makes an ID check but is quite generic - - patterns = ["*.emd", "*.dm4"] - - mime_type = ["application/x-hdf", "application/x-hdf5"] # Really an HDF5 file + def _unpack_hyperspy_meta(new_data, root, value): + """Recursively unpack a nested dict of metadata and append keys to new_data.metadata.""" + if isinstance(value, Mapping): + for item in value.keys(): + if root != "": + _unpack_hyperspy_meta(new_data, f"{root}.{item}", value[item]) + else: + _unpack_hyperspy_meta(new_data, f"{item}", value[item]) + else: + new_data.metadata[root] = value + def _unpack_hyperspy_axes(newdata, ax_manager): + """Unpack the axes managber as metadata.""" _axes_keys = ["name", "scale", "low_index", "low_value", "high_index", "high_value"] + for ax in ax_manager.signal_axes: + for k in _axes_keys: + newdata.metadata[f"{ax.name}.{k}"] = getattr(ax, k) + + @register_loader( + patterns=[(".emd", 32), (".dm4", 32)], + mime_types=[("application/x-hdf", 64), ("application/x-hdf5", 64)], + name="HyperSpyFile", + what="Data", + ) + def hypersput_load(new_data, filename=None, *args, **kargs): + """Load HyperSpy file loader routine. - def _unpack_meta(self, root, value): - """Recursively unpack a nested dict of metadata and append keys to self.metadata.""" - if isinstance(value, Mapping): - for item in value.keys(): - if root != "": - self._unpack_meta(f"{root}.{item}", value[item]) - else: - self._unpack_meta(f"{item}", value[item]) - else: - self.metadata[root] = value - - def _unpack_axes(self, ax_manager): - """Unpack the axes managber as metadata.""" - for ax in ax_manager.signal_axes: - for k in self._axes_keys: - self.metadata[f"{ax.name}.{k}"] = getattr(ax, k) - - def _load(self, filename=None, *args, **kargs): - """Load HyperSpy file loader routine. - - Args: - filename (string or bool): File to load. If None then the existing filename is used, - if False, then a file dialog will be used. - - Returns: - A copy of the itself after loading the data. - """ - if filename is None or not filename: - self.get_filename("r") + Args: + filename (string or bool): File to load. If None then the existing filename is used, + if False, then a file dialog will be used. + + Returns: + A copy of the itnew_data after loading the data. + """ + if filename is None or not filename: + new_data.get_filename("r") + else: + new_data.filename = filename + # Open the file and read the main file header and unpack into a dict + try: + with catch_sysout(): + signal = hsload(new_data.filename) + if hasattr(hs, "signals"): + Signal2D = hs.signals.Signal2D else: - self.filename = filename - # Open the file and read the main file header and unpack into a dict - try: - with catch_sysout(): - signal = hsload(self.filename) - if hasattr(hs, "signals"): - Signal2D = hs.signals.Signal2D - else: - Signal2D = hs.api.signals.Signal2D - if not isinstance(signal, Signal2D): - raise StonerLoadError("Not a 2D signal object - aborting!") - except Exception as err: # pylint: disable=W0703 Pretty generic error catcher - raise StonerLoadError(f"Not readable by HyperSpy error was {err}") from err - self.data = signal.data - self._unpack_meta("", signal.metadata.as_dictionary()) - self._unpack_axes(signal.axes_manager) - - return self - -else: - HyperSpyFile = DataFile + Signal2D = hs.api.signals.Signal2D + if not isinstance(signal, Signal2D): + raise StonerLoadError("Not a 2D signal object - aborting!") + except Exception as err: # pylint: disable=W0703 Pretty generic error catcher + raise StonerLoadError(f"Not readable by HyperSpy error was {err}") from err + new_data.data = signal.data + _unpack_hyperspy_meta(new_data, "", signal.metadata.as_dictionary()) + _unpack_hyperspy_axes(new_data, signal.axes_manager) + del new_data["General.FileIO.0.timestamp"] + return new_data diff --git a/Stoner/formats/instruments.py b/Stoner/formats/instruments.py index 40f2575fa..abfa2a5fd 100755 --- a/Stoner/formats/instruments.py +++ b/Stoner/formats/instruments.py @@ -1,826 +1,718 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- """Implement DataFile like classes to represent Instrument Manufacturer's File Formats'.""" -__all__ = ["LSTemperatureFile", "QDFile", "RigakuFile", "SPCFile", "XRDFile"] # Standard Library imports from datetime import datetime import re import struct +from ast import literal_eval import numpy as np -from .. import Core from ..compat import str2bytes, bytes2str from ..core.exceptions import StonerAssertionError, assertion, StonerLoadError from ..core.base import string_to_type from ..tools.file import FileManager, SizedFileManager - -class LSTemperatureFile(Core.DataFile): - - """A class that reads and writes Lakeshore Temperature Calibration Curves. - - .. warning:: - - This class works for cernox curves in Log Ohms/Kelvin and Log Ohms/Log Kelvin. It may or may not work with any - other temperature calibration data ! - - """ - - #: priority (int): is the load order for the class, smaller numbers are tried before larger numbers. - # .. note:: - # Subclasses with priority<=32 should make some positive identification that they have the right - # file type before attempting to read data. - priority = 16 - #: pattern (list of str): A list of file extensions that might contain this type of file. Used to construct - # the file load/save dialog boxes. - patterns = ["*.340"] - - def _load(self, filename=None, *args, **kargs): - """Load data for 340 files.""" - if filename is None or not filename: - self.get_filename("r") +from .decorators import register_loader, register_saver + + +@register_loader(patterns=(".340", 16), mime_types=("text/plain", 32), name="LSTemperatureFile", what="Data") +def load_340(new_data, filename=None, *args, **kargs): + """Load data for 340 files.""" + new_data.filename = filename + with FileManager(new_data.filename, "rb") as data: + keys = [] + vals = [] + for line in data: + line = bytes2str(line) + if line.strip() == "": + break + parts = [p.strip() for p in line.split(":")] + if len(parts) != 2: + raise StonerLoadError(f"Header doesn't contain two parts at {line.strip()}") + keys.append(parts[0]) + vals.append(parts[1]) else: - self.filename = filename - - with FileManager(self.filename, "rb") as data: - keys = [] - vals = [] - for line in data: - line = bytes2str(line) - if line.strip() == "": - break - parts = [p.strip() for p in line.split(":")] - if len(parts) != 2: - raise Core.StonerLoadError(f"Header doesn't contain two parts at {line.strip()}") - keys.append(parts[0]) - vals.append(parts[1]) + raise StonerLoadError("Overan the end of the file") + if keys != [ + "Sensor Model", + "Serial Number", + "Data Format", + "SetPoint Limit", + "Temperature coefficient", + "Number of Breakpoints", + ]: + raise StonerLoadError("Header did not contain recognised keys.") + for k, v in zip(keys, vals): + v = v.split()[0] + new_data.metadata[k] = string_to_type(v) + headers = bytes2str(next(data)).strip().split() + column_headers = headers[1:] + dat = np.genfromtxt(data) + new_data.data = dat[:, 1:] + new_data.column_headers = column_headers + return new_data + + +@register_saver(patterns=".340", name="LSTemperatureFile", what="Data") +def save_340(save_data, filename=None, **kargs): + """Override the save method to allow CSVFiles to be written out to disc (as a mininmalist output). + + Args: + filename (string): + Filename to save as (using the same rules as for the load routines) + + Keyword Arguments: + deliminator (string): + Record deliniminator (defaults to a comma) + + Returns: + A copy of itsave_data. + """ + if filename is None: + filename = save_data.filename + if filename is None or (isinstance(filename, bool) and not filename): # now go and ask for one + filename = save_data.__file_dialog("w") + if save_data.shape[1] == 2: # 2 columns, let's hope they're the right way round! + cols = [0, 1] + elif ( + save_data.setas.has_xcol and save_data.setas.has_ycol + ): # Use ycol, x col but assume x is real temperature and y is resistance + cols = [save_data.setas.ycol[0], save_data.setas.xcol] + else: + cols = range(save_data.shape[1]) + with FileManager(filename, "w", errors="ignore", encoding="utf-8", newline="\r\n") as f: + for k, v in ( + ("Sensor Model", "CX-1070-SD"), + ("Serial Number", "Unknown"), + ("Data Format", 4), + ("SetPoint Limit", 300.0), + ("Temperature coefficient", 1), + ("Number of Breakpoints", len(save_data)), + ): + if k in ["Sensor Model", "Serial Number", "Data Format", "SetPoint Limit"]: + kstr = f"{k+':':16s}" else: - raise Core.StonerLoadError("Overan the end of the file") - if keys != [ - "Sensor Model", - "Serial Number", - "Data Format", - "SetPoint Limit", - "Temperature coefficient", - "Number of Breakpoints", - ]: - raise Core.StonerLoadError("Header did not contain recognised keys.") - for k, v in zip(keys, vals): - v = v.split()[0] - self.metadata[k] = string_to_type(v) - headers = bytes2str(next(data)).strip().split() - column_headers = headers[1:] - dat = np.genfromtxt(data) - self.data = dat[:, 1:] - self.column_headers = column_headers - return self - - def save(self, filename=None, **kargs): - """Override the save method to allow CSVFiles to be written out to disc (as a mininmalist output). - - Args: - filename (string): - Filename to save as (using the same rules as for the load routines) - - Keyword Arguments: - deliminator (string): - Record deliniminator (defaults to a comma) - - Returns: - A copy of itself. - """ - if filename is None: - filename = self.filename - if filename is None or (isinstance(filename, bool) and not filename): # now go and ask for one - filename = self.__file_dialog("w") - if self.shape[1] == 2: # 2 columns, let's hope they're the right way round! - cols = [0, 1] - elif ( - self.setas.has_xcol and self.setas.has_ycol - ): # Use ycol, x col but assume x is real temperature and y is resistance - cols = [self.setas.ycol[0], self.setas.xcol] - else: - cols = range(self.shape[1]) - with FileManager(filename, "w", errors="ignore", encoding="utf-8", newline="\r\n") as f: - for k, v in ( - ("Sensor Model", "CX-1070-SD"), - ("Serial Number", "Unknown"), - ("Data Format", 4), - ("SetPoint Limit", 300.0), - ("Temperature coefficient", 1), - ("Number of Breakpoints", len(self)), - ): - if k in ["Sensor Model", "Serial Number", "Data Format", "SetPoint Limit"]: - kstr = f"{k+':':16s}" - else: - kstr = f"{k}: " - v = self.get(k, v) - if k == "Data Format": - units = ["()", "()", "()", "()", "(Log Ohms/Kelvin)", "(Log Ohms/Log Kelvin)"] - vstr = f"{v} {units[int(v)]}" - elif k == "SetPointLimit": - vstr = f"{v} (Kelvin)" - elif k == "Temperature coefficient": - vstr = f"{v} {['(positive)', '(negative)'][v]}" - elif k == "Number of Breakpoints": - vstr = str(len(self)) - else: - vstr = str(v) - f.write(f"{kstr}{vstr}\n") - f.write("\n") - f.write("No. ") - for i in cols: - f.write(f"{self.column_headers[i]:11s}") - f.write("\n\n") - for i in range( - len(self.data) - ): # This is a slow way to write the data, but there should only ever be 200 lines - line = "\t".join([f"{n:<10.8f}" for n in self.data[i, cols]]) - f.write(f"{i}\t") - f.write(f"{line}\n") - self.filename = filename - return self - - -class QDFile(Core.DataFile): - - """Extends Core.DataFile to load files from Quantum Design Systems - including PPMS, MPMS and SQUID-VSM.""" - - #: priority (int): is the load order for the class, smaller numbers are tried before larger numbers. - # .. note:: - # Subclasses with priority<=32 should make some positive identification that they have the right - # file type before attempting to read data. - priority = 15 # Is able to make a positive ID of its file content, so get priority to check - #: pattern (list of str): A list of file extensions that might contain this type of file. Used to construct - # the file load/save dialog boxes. - patterns = ["*.dat"] # Recognised filename patterns - - mime_type = ["application/x-wine-extension-ini", "text/plain"] - - def _load(self, filename=None, *args, **kargs): - """QD system file loader routine. - - Args: - filename (string or bool): - File to load. If None then the existing filename is used, if False, then a file dialog will be used. - - Returns: - A copy of the itself after loading the data. - """ - if filename is None or not filename: - self.get_filename("r") - else: - self.filename = filename - setas = {} - i = 0 - with FileManager(self.filename, "r", encoding="utf-8", errors="ignore") as f: # Read filename linewise - for i, line in enumerate(f): - line = line.strip() - if i == 0 and line != "[Header]": - raise Core.StonerLoadError("Not a Quantum Design File !") - if line == "[Header]" or line.startswith(";") or line == "": - continue - if "[Data]" in line: - break - if "," not in line: - raise Core.StonerLoadError("No data in file!") - parts = [x.strip() for x in line.split(",")] - if parts[1].split(":")[0] == "SEQUENCE FILE": - key = parts[1].split(":")[0].title() - value = parts[1].split(":")[1] - elif parts[0] == "INFO": - if parts[1] == "APPNAME": - parts[1], parts[2] = parts[2], parts[1] - if len(parts) > 2: - key = f"{parts[0]}.{parts[2]}" - else: - raise Core.StonerLoadError("No data in file!") - key = key.title() - value = parts[1] - elif parts[0] in ["BYAPP", "FILEOPENTIME"]: - key = parts[0].title() - value = " ".join(parts[1:]) - elif parts[0] == "FIELDGROUP": - key = f"{parts[0]}.{parts[1]}".title() - value = f'[{",".join(parts[2:])}]' - elif parts[0] == "STARTUPAXIS": - axis = parts[1][0].lower() - setas[axis] = setas.get(axis, []) + [int(parts[2])] - key = f"Startupaxis-{parts[1].strip()}" - value = parts[2].strip() + kstr = f"{k}: " + v = save_data.get(k, v) + if k == "Data Format": + units = ["()", "()", "()", "()", "(Log Ohms/Kelvin)", "(Log Ohms/Log Kelvin)"] + vstr = f"{v} {units[int(v)]}" + elif k == "SetPointLimit": + vstr = f"{v} (Kelvin)" + elif k == "Temperature coefficient": + vstr = f"{v} {['(positive)', '(negative)'][v]}" + elif k == "Number of Breakpoints": + vstr = str(len(save_data)) + else: + vstr = str(v) + f.write(f"{kstr}{vstr}\n") + f.write("\n") + f.write("No. ") + for i in cols: + f.write(f"{save_data.column_headers[i]:11s}") + f.write("\n\n") + for i in range( + len(save_data.data) + ): # This is a slow way to write the data, but there should only ever be 200 lines + line = "\t".join([f"{n:<10.8f}" for n in save_data.data[i, cols]]) + f.write(f"{i}\t") + f.write(f"{line}\n") + save_data.filename = filename + return save_data + + +@register_loader( + patterns=(".dat", 16), + mime_types=[("application/x-wine-extension-ini", 15), ("text/plain", 16)], + name="QDFile", + what="Data", +) +def load_qdfile(new_data, filename=None, *args, **kargs): + """QD system file loader routine. + + Args: + filename (string or bool): + File to load. If None then the existing filename is used, if False, then a file dialog will be used. + + Returns: + A copy of the itnew_data after loading the data. + """ + setas = {} + i = 0 + new_data.filename = filename + with FileManager(new_data.filename, "r", encoding="utf-8", errors="ignore") as f: # Read filename linewise + for i, line in enumerate(f): + line = line.strip() + if i == 0 and line != "[Header]": + raise StonerLoadError("Not a Quantum Design File !") + if line == "[Header]" or line.startswith(";") or line == "": + continue + if "[Data]" in line: + break + if "," not in line: + raise StonerLoadError("No data in file!") + parts = [x.strip() for x in line.split(",")] + if parts[1].split(":")[0] == "SEQUENCE FILE": + key = parts[1].split(":")[0].title() + value = parts[1].split(":")[1] + elif parts[0] == "INFO": + if parts[1] == "APPNAME": + parts[1], parts[2] = parts[2], parts[1] + if len(parts) > 2: + key = f"{parts[0]}.{parts[2]}" else: - key = parts[0] + "," + parts[1] - key = key.title() - value = " ".join(parts[2:]) - self.metadata[key] = string_to_type(value) + raise StonerLoadError("No data in file!") + key = key.title() + value = parts[1] + elif parts[0] in ["BYAPP", "FILEOPENTIME"]: + key = parts[0].title() + value = " ".join(parts[1:]) + elif parts[0] == "FIELDGROUP": + key = f"{parts[0]}.{parts[1]}".title() + value = f'[{",".join(parts[2:])}]' + elif parts[0] == "STARTUPAXIS": + axis = parts[1][0].lower() + setas[axis] = setas.get(axis, []) + [int(parts[2])] + key = f"Startupaxis-{parts[1].strip()}" + value = parts[2].strip() else: - raise Core.StonerLoadError("No data in file!") - if "Byapp" not in self: - raise Core.StonerLoadError("Not a Quantum Design File !") - - column_headers = f.readline().strip().split(",") - data = np.genfromtxt([str2bytes(l) for l in f], dtype="float", delimiter=",", invalid_raise=False) - if data.shape[0] == 0: - raise Core.StonerLoadError("No data in file!") - if data.shape[1] < len(column_headers): # Trap for buggy QD software not giving ewnough columns of data - data = np.append(data, np.ones((data.shape[0], len(column_headers) - data.shape[1])) * np.NaN, axis=1) - elif data.shape[1] > len(column_headers): # too much data - data = data[:, : len(column_headers) - data.shape[1]] - self.data = data - self.column_headers = column_headers - s = self.setas - for k in setas: - for ix in setas[k]: - s[ix - 1] = k - self.setas = s - return self - - -class RigakuFile(Core.DataFile): - - """Loads a .ras file as produced by Rigaku X-ray diffractormeters.""" - - #: priority (int): is the load order for the class, smaller numbers are tried before larger numbers. - # .. note:: - # Subclasses with priority<=32 should make some positive identification that they have the right - # file type before attempting to read data. - priority = 16 # Can make a positive id of file from first line - #: pattern (list of str): A list of file extensions that might contain this type of file. Used to construct - # the file load/save dialog boxes. - patterns = ["*.ras"] # Recognised filename patterns - - def _load(self, filename=None, *args, **kargs): - """Read a Rigaku ras file including handling the metadata nicely. - - Args: - filename (string or bool): - File to load. If None then the existing filename is used, if False, then a file dialog will be used. - - Returns: - A copy of the itself after loading the data. - """ - from ast import literal_eval - - if filename is None or not filename: - self.get_filename("rb") + key = parts[0] + "," + parts[1] + key = key.title() + value = " ".join(parts[2:]) + new_data.metadata[key] = string_to_type(value) else: - self.filename = filename - sh = re.compile(r"^\*([^\s]+)\s+(.*)$") # Regexp to grab the keys - ka = re.compile(r"(.*)\-(\d+)$") - header = dict() - i = 0 - with SizedFileManager(self.filename, "rb") as (f, end): - for i, line in enumerate(f): - line = bytes2str(line).strip() - if i == 0 and not line.startswith("*RAS_"): - raise StonerLoadError("Not a Rigaku file!") - if line == "*RAS_HEADER_START": - break - for line in f: - line = bytes2str(line).strip() - m = sh.match(line) - if m: - key = m.groups()[0].lower().replace("_", ".") - try: - value = m.groups()[1].decode("utf-8", "ignore") - except AttributeError: - value = m.groups()[1] - header[key] = value - if "*RAS_INT_START" in line: - break - keys = list(header.keys()) - keys.sort() - for key in keys: - m = ka.match(key) - value = header[key].strip() + raise StonerLoadError("No data in file!") + if "Byapp" not in new_data: + raise StonerLoadError("Not a Quantum Design File !") + + column_headers = f.readline().strip().split(",") + data = np.genfromtxt([str2bytes(l) for l in f], dtype="float", delimiter=",", invalid_raise=False) + if data.shape[0] == 0: + raise StonerLoadError("No data in file!") + if data.shape[1] < len(column_headers): # Trap for buggy QD software not giving ewnough columns of data + data = np.append(data, np.ones((data.shape[0], len(column_headers) - data.shape[1])) * np.NaN, axis=1) + elif data.shape[1] > len(column_headers): # too much data + data = data[:, : len(column_headers) - data.shape[1]] + new_data.data = data + new_data.column_headers = column_headers + s = new_data.setas + for k in setas: + for ix in setas[k]: + s[ix - 1] = k + new_data.setas = s + return new_data + + +def _to_Q(new_data, wavelength=1.540593): + """Add an additional function to covert an angualr scale to momentum transfer. + + Returns: + a copy of itnew_data. + """ + new_data.add_column( + (4 * np.pi / wavelength) * np.sin(np.pi * new_data.column(0) / 360), header="Momentum Transfer, Q ($\\AA$)" + ) + + +@register_loader( + patterns=(".ras", 16), + mime_types=[("application/x-wine-extension-ini", 16), ("text/plain", 16)], + name="RigakuFile", + what="Data", +) +def load_rigaku(new_data, filename=None, *args, **kargs): + """Read a Rigaku ras file including handling the metadata nicely. + + Args: + filename (string or bool): + File to load. If None then the existing filename is used, if False, then a file dialog will be used. + + Returns: + A copy of the itnew_data after loading the data. + """ + sh = re.compile(r"^\*([^\s]+)\s+(.*)$") # Regexp to grab the keys + ka = re.compile(r"(.*)\-(\d+)$") + header = dict() + i = 0 + new_data.filename = filename + with SizedFileManager(new_data.filename, "rb") as (f, end): + for i, line in enumerate(f): + line = bytes2str(line).strip() + if i == 0 and not line.startswith("*RAS_"): + raise StonerLoadError("Not a Rigaku file!") + if line == "*RAS_HEADER_START": + break + for line in f: + line = bytes2str(line).strip() + m = sh.match(line) + if m: + key = m.groups()[0].lower().replace("_", ".") try: - newvalue = literal_eval(value.strip('"')) - except (TypeError, ValueError, SyntaxError): - newvalue = literal_eval(value) - if newvalue == "-": - newvalue = np.nan # trap for missing float value - if m: - key = m.groups()[0] - idx = int(m.groups()[1]) - if key in self.metadata and not (isinstance(self[key], (np.ndarray, list))): - if isinstance(self[key], str): - self[key] = list([self[key]]) - if idx > 1: - self[key].extend([""] * idx - 1) - else: - self[key] = np.array(self[key]) - if idx > 1: - self[key] = np.append(self[key], np.ones(idx - 1) * np.nan) - if key not in self.metadata: - if isinstance(newvalue, str): - listval = [""] * (idx + 1) - listval[idx] = newvalue - self[key] = listval - else: - arrayval = np.ones(idx + 1) * np.nan - arrayval = arrayval.astype(type(newvalue)) - arrayval[idx] = newvalue - self[key] = arrayval + value = m.groups()[1].decode("utf-8", "ignore") + except AttributeError: + value = m.groups()[1] + header[key] = value + if "*RAS_INT_START" in line: + break + keys = list(header.keys()) + keys.sort() + for key in keys: + m = ka.match(key) + value = header[key].strip() + try: + newvalue = literal_eval(value.strip('"')) + except (TypeError, ValueError, SyntaxError): + newvalue = literal_eval(value) + if newvalue == "-": + newvalue = np.nan # trap for missing float value + if m: + key = m.groups()[0] + idx = int(m.groups()[1]) + if key in new_data.metadata and not (isinstance(new_data[key], (np.ndarray, list))): + if isinstance(new_data[key], str): + new_data[key] = list([new_data[key]]) + if idx > 1: + new_data[key].extend([""] * idx - 1) + else: + new_data[key] = np.array(new_data[key]) + if idx > 1: + new_data[key] = np.append(new_data[key], np.ones(idx - 1) * np.nan) + if key not in new_data.metadata: + if isinstance(newvalue, str): + listval = [""] * (idx + 1) + listval[idx] = newvalue + new_data[key] = listval else: - if isinstance(self[key][0], str) and isinstance(self[key], list): - if len(self[key]) < idx + 1: - self[key].extend([""] * (idx + 1 - len(self[key]))) - self[key][idx] = newvalue - else: - if idx + 1 > self[key].size: - self[key] = np.append( - self[key], (np.ones(idx + 1 - self[key].size) * np.nan).astype(self[key].dtype) - ) - try: - self[key][idx] = newvalue - except ValueError: - pass + arrayval = np.ones(idx + 1) * np.nan + arrayval = arrayval.astype(type(newvalue)) + arrayval[idx] = newvalue + new_data[key] = arrayval else: - self.metadata[key] = newvalue - - pos = f.tell() - max_rows = 0 - for max_rows, line in enumerate(f): - line = bytes2str(line).strip() - if "RAS_INT_END" in line: - break - f.seek(pos) - if max_rows > 0: - self.data = np.genfromtxt( - f, dtype="float", delimiter=" ", invalid_raise=False, comments="*", max_rows=max_rows - ) - column_headers = ["Column" + str(i) for i in range(self.data.shape[1])] - column_headers[0:2] = [self.metadata["meas.scan.unit.x"], self.metadata["meas.scan.unit.y"]] - for key in self.metadata: - if isinstance(self[key], list): - self[key] = np.array(self[key]) - self.setas = "xy" - self.column_headers = column_headers - pos = f.tell() - if pos < end: # Trap for Rigaku files with multiple scans in them. - self["_endpos"] = pos - if hasattr(filename, "seekable") and filename.seekable(): - filename.seek(pos) - return self - - def to_Q(self, wavelength=1.540593): - """Add an additional function to covert an angualr scale to momentum transfer. - - Returns: - a copy of itself. - """ - self.add_column( - (4 * np.pi / wavelength) * np.sin(np.pi * self.column(0) / 360), header="Momentum Transfer, Q ($\\AA$)" - ) - - -class SPCFile(Core.DataFile): - - """Extends Core.DataFile to load SPC files from Raman.""" - - #: priority (int): is the load order for the class, smaller numbers are tried before larger numbers. - # .. note:: - # Subclasses with priority<=32 should make some positive identification that they have the right - # file type before attempting to read data. - priority = 16 # Can't make a positive ID of itself - #: pattern (list of str): A list of file extensions that might contain this type of file. Used to construct - # the file load/save dialog boxes. - patterns = ["*.spc"] # Recognised filename patterns - - mime_type = ["application/octet-stream"] - - def __init__(self, *args, **kargs): - """Create a few local attributes.""" - super().__init__(*args, **kargs) - self._pts = None - self._header = None - self._filesize = None - self._xvars = None - self._yvars = None - - def _read_xdata(self, f): - """Read the xdata from the spc file.""" - self._pts = self._header["fnpts"] - if self._header["ftflgs"] & 128: # We need to read some X Data - if 4 * self._pts > self._filesize - f.tell(): - raise Core.StonerLoadError("Trying to read too much data!") - xvals = f.read(4 * self._pts) # I think storing X vals directly implies that each one is 4 bytes.... - xdata = np.array(struct.unpack(str2bytes(str(self._pts) + "f"), xvals)) - else: # Generate the X Data ourselves - first = self._header["ffirst"] - last = self._header["flast"] - if self._pts > 1e6: # Something not right here ! - raise Core.StonerLoadError("More than 1 million points requested. Bugging out now!") - xdata = np.linspace(first, last, self._pts) - return xdata - - def _read_ydata(self, f, data, column_headers): - """Read the y data and column headers from spc file.""" - n = self._header["fnsub"] - subhdr_keys = ( - "subflgs", - "subexp", - "subindx", - "subtime", - "subnext", - "subnois", - "subnpts", - "subscan", - "subwlevel", - "subresv", + if isinstance(new_data[key][0], str) and isinstance(new_data[key], list): + if len(new_data[key]) < idx + 1: + new_data[key].extend([""] * (idx + 1 - len(new_data[key]))) + new_data[key][idx] = newvalue + else: + if idx + 1 > new_data[key].size: + new_data[key] = np.append( + new_data[key], + (np.ones(idx + 1 - new_data[key].size) * np.nan).astype(new_data[key].dtype), + ) + try: + new_data[key][idx] = newvalue + except ValueError: + pass + else: + new_data.metadata[key] = newvalue + + pos = f.tell() + max_rows = 0 + for max_rows, line in enumerate(f): + line = bytes2str(line).strip() + if "RAS_INT_END" in line: + break + f.seek(pos) + if max_rows > 0: + new_data.data = np.genfromtxt( + f, dtype="float", delimiter=" ", invalid_raise=False, comments="*", max_rows=max_rows + ) + column_headers = ["Column" + str(i) for i in range(new_data.data.shape[1])] + column_headers[0:2] = [new_data.metadata["meas.scan.unit.x"], new_data.metadata["meas.scan.unit.y"]] + for key in new_data.metadata: + if isinstance(new_data[key], list): + new_data[key] = np.array(new_data[key]) + new_data.setas = "xy" + new_data.column_headers = column_headers + pos = f.tell() + if pos < end: # Trap for Rigaku files with multiple scans in them. + new_data["_endpos"] = pos + if hasattr(filename, "seekable") and filename.seekable(): + filename.seek(pos) + if kargs.pop("add_Q", False): + _to_Q(new_data) + return new_data + + +def _read_spc_xdata(new_data, f): + """Read the xdata from the spc file.""" + new_data._pts = new_data._header["fnpts"] + if new_data._header["ftflgs"] & 128: # We need to read some X Data + if 4 * new_data._pts > new_data._filesize - f.tell(): + raise StonerLoadError("Trying to read too much data!") + xvals = f.read(4 * new_data._pts) # I think storing X vals directly implies that each one is 4 bytes.... + xdata = np.array(struct.unpack(str2bytes(str(new_data._pts) + "f"), xvals)) + else: # Generate the X Data ourselves + first = new_data._header["ffirst"] + last = new_data._header["flast"] + if new_data._pts > 1e6: # Something not right here ! + raise StonerLoadError("More than 1 million points requested. Bugging out now!") + xdata = np.linspace(first, last, new_data._pts) + return xdata + + +def _read_spc_ydata(new_data, f, data, column_headers): + """Read the y data and column headers from spc file.""" + n = new_data._header["fnsub"] + subhdr_keys = ( + "subflgs", + "subexp", + "subindx", + "subtime", + "subnext", + "subnois", + "subnpts", + "subscan", + "subwlevel", + "subresv", + ) + if new_data._header["ftflgs"] & 1: + y_width = 2 + y_fmt = "h" + divisor = 2**16 + else: + y_width = 4 + y_fmt = "i" + divisor = 2**32 + if n * (y_width * new_data._pts + 32) > new_data._filesize - f.tell(): + raise StonerLoadError("No good, going to read too much data!") + for j in range(n): # We have n sub-scans + # Read the subheader and import into the main metadata dictionary as scan#: + subhdr = struct.unpack(b"BBHfffIIf4s", f.read(32)) + subheader = dict(zip(["scan" + str(j) + ":" + x for x in subhdr_keys], subhdr)) + + # Now read the y-data + exponent = subheader["scan" + str(j) + ":subexp"] + if int(exponent) & -128: # Data is unscaled direct floats + ydata = np.array(struct.unpack(str2bytes(str(new_data._pts) + "f"), f.read(new_data._pts * y_width))) + else: # Data is scaled by exponent + yvals = struct.unpack(str2bytes(str(new_data._pts) + y_fmt), f.read(new_data._pts * y_width)) + ydata = np.array(yvals, dtype="float64") * (2**exponent) / divisor + data[:, j + 1] = ydata + new_data._header = dict(new_data._header, **subheader) + column_headers.append("Scan" + str(j) + ":" + new_data._yvars[new_data._header["fytype"]]) + + return data + + +def _read_spc_loginfo(new_data, f): + """Read the log info section of the spc file.""" + logstc = struct.unpack(b"IIIII44s", f.read(64)) + logstc_keys = ("logsizd", "logsizm", "logtxto", "logbins", "logdsks", "logrsvr") + logheader = dict(zip(logstc_keys, logstc)) + new_data._header = dict(new_data._header, **logheader) + + # Can't handle either binary log information or ion disk log information (wtf is this anyway !) + if new_data._header["logbins"] + new_data._header["logdsks"] > new_data._filesize - f.tell(): + raise StonerLoadError("Too much logfile data to read") + f.read(new_data._header["logbins"] + new_data._header["logdsks"]) + + # The renishaw seems to put a 16 character timestamp next - it's not in the spec but never mind that. + new_data._header["Date-Time"] = f.read(16) + # Now read the rest of the file as log text + logtext = f.read() + # We expect things to be single lines terminated with a CR-LF of the format key=value + for line in re.split(b"[\r\n]+", logtext): + if b"=" in line: + parts = line.split(b"=") + key = parts[0].decode() + value = parts[1].decode() + new_data._header[key] = value + + +@register_loader(patterns=(".spc", 16), mime_types=("application/octet-stream", 16), name="SPCFile", what="Data") +def load_spc(new_data, filename=None, *args, **kargs): + """Read a .scf file produced by the Renishaw Raman system (among others). + + Args: + filename (string or bool): + File to load. If None then the existing filename is used, if False, then a file dialog will be used. + + Returns: + A copy of the itnew_data after loading the data. + + Todo: + Implement the second form of the file that stores multiple x-y curves in the one file. + + Notes: + Metadata keys are pretty much as specified in the spc.h file that defines the filerformat. + """ + if filename is None or not filename: + new_data.get_filename("r") + else: + new_data.filename = filename + # Open the file and read the main file header and unpack into a dict + with SizedFileManager(filename, "rb") as (f, length): + new_data._filesize = length + spchdr = struct.unpack(b"BBBciddiBBBBi9s9sH8f30s130siiBBHf48sfifB187s", f.read(512)) + keys = ( + "ftflgs", + "fversn", + "fexper", + "fexp", + "fnpts", + "ffirst", + "flast", + "fnsub", + "fxtype", + "fytype", + "fztype", + "fpost", + "fres", + "fsource", + "fpeakpt", + "fspare1", + "fspare2", + "fspare3", + "fspare4", + "fspare5", + "fspare6", + "fspare7", + "fspare8", + "fcm", + "nt", + "fcatx", + "flogoff", + "fmods", + "fprocs", + "flevel", + "fsampin", + "ffactor", + "fmethod", + "fzinc", + "fwplanes", + "fwinc", + "fwtype", + "fwtype", + "fresv", ) - if self._header["ftflgs"] & 1: - y_width = 2 - y_fmt = "h" - divisor = 2**16 - else: - y_width = 4 - y_fmt = "i" - divisor = 2**32 - if n * (y_width * self._pts + 32) > self._filesize - f.tell(): - raise Core.StonerLoadError("No good, going to read too much data!") - for j in range(n): # We have n sub-scans - # Read the subheader and import into the main metadata dictionary as scan#: - subhdr = struct.unpack(b"BBHfffIIf4s", f.read(32)) - subheader = dict(zip(["scan" + str(j) + ":" + x for x in subhdr_keys], subhdr)) - - # Now read the y-data - exponent = subheader["scan" + str(j) + ":subexp"] - if int(exponent) & -128: # Data is unscaled direct floats - ydata = np.array(struct.unpack(str2bytes(str(self._pts) + "f"), f.read(self._pts * y_width))) - else: # Data is scaled by exponent - yvals = struct.unpack(str2bytes(str(self._pts) + y_fmt), f.read(self._pts * y_width)) - ydata = np.array(yvals, dtype="float64") * (2**exponent) / divisor - data[:, j + 1] = ydata - self._header = dict(self._header, **subheader) - column_headers.append("Scan" + str(j) + ":" + self._yvars[self._header["fytype"]]) - - return data - - def _read_loginfo(self, f): - """Read the log info section of the spc file.""" - logstc = struct.unpack(b"IIIII44s", f.read(64)) - logstc_keys = ("logsizd", "logsizm", "logtxto", "logbins", "logdsks", "logrsvr") - logheader = dict(zip(logstc_keys, logstc)) - self._header = dict(self._header, **logheader) - - # Can't handle either binary log information or ion disk log information (wtf is this anyway !) - if self._header["logbins"] + self._header["logdsks"] > self._filesize - f.tell(): - raise Core.StonerLoadError("Too much logfile data to read") - f.read(self._header["logbins"] + self._header["logdsks"]) - - # The renishaw seems to put a 16 character timestamp next - it's not in the spec but never mind that. - self._header["Date-Time"] = f.read(16) - # Now read the rest of the file as log text - logtext = f.read() - # We expect things to be single lines terminated with a CR-LF of the format key=value - for line in re.split(b"[\r\n]+", logtext): - if b"=" in line: - parts = line.split(b"=") - key = parts[0].decode() - value = parts[1].decode() - self._header[key] = value - - def _load(self, filename=None, *args, **kargs): - """Read a .scf file produced by the Renishaw Raman system (among others). - - Args: - filename (string or bool): - File to load. If None then the existing filename is used, if False, then a file dialog will be used. - - Returns: - A copy of the itself after loading the data. - - Todo: - Implement the second form of the file that stores multiple x-y curves in the one file. - - Notes: - Metadata keys are pretty much as specified in the spc.h file that defines the filerformat. - """ - if filename is None or not filename: - self.get_filename("r") - else: - self.filename = filename - # Open the file and read the main file header and unpack into a dict - with SizedFileManager(filename, "rb") as (f, length): - self._filesize = length - spchdr = struct.unpack(b"BBBciddiBBBBi9s9sH8f30s130siiBBHf48sfifB187s", f.read(512)) - keys = ( - "ftflgs", - "fversn", - "fexper", - "fexp", - "fnpts", - "ffirst", - "flast", - "fnsub", - "fxtype", - "fytype", - "fztype", - "fpost", - "fres", - "fsource", - "fpeakpt", - "fspare1", - "fspare2", - "fspare3", - "fspare4", - "fspare5", - "fspare6", - "fspare7", - "fspare8", - "fcm", - "nt", - "fcatx", - "flogoff", - "fmods", - "fprocs", - "flevel", - "fsampin", - "ffactor", - "fmethod", - "fzinc", - "fwplanes", - "fwinc", - "fwtype", - "fwtype", - "fresv", + new_data._xvars = [ + "Arbitrary", + "Wavenumber (cm-1)", + "Micrometers (um)", + "Nanometers (nm)", + "Seconds", + "Minutes", + "Hertz (Hz)", + "Kilohertz (KHz)", + "Megahertz (MHz)", + "Mass (M/z)", + "Parts per million (PPM)", + "Days", + "Years", + "Raman Shift (cm-1)", + "Raman Shift (cm-1)", + "eV", + "XYZ text labels in fcatxt (old 0x4D version only)", + "Diode Number", + "Channel", + "Degrees", + "Temperature (F)", + "Temperature (C)", + "Temperature (K)", + "Data Points", + "Milliseconds (mSec)", + "Microseconds (uSec)", + "Nanoseconds (nSec)", + "Gigahertz (GHz)", + "Centimeters (cm)", + "Meters (m)", + "Millimeters (mm)", + "Hours", + "Hours", + ] + new_data._yvars = [ + "Arbitrary Intensity", + "Interferogram", + "Absorbance", + "Kubelka-Monk", + "Counts", + "Volts", + "Degrees", + "Milliamps", + "Millimeters", + "Millivolts", + "Log(1/R)", + "Percent", + "Percent", + "Intensity", + "Relative Intensity", + "Energy", + "Decibel", + "Temperature (F)", + "Temperature (C)", + "Temperature (K)", + "Index of Refraction [N]", + "Extinction Coeff. [K]", + "Real", + "Imaginary", + "Complex", + "Complex", + "Transmission (ALL HIGHER MUST HAVE VALLEYS!)", + "Reflectance", + "Arbitrary or Single Beam with Valley Peaks", + "Emission", + "Emission", + ] + + new_data._header = dict(zip(keys, spchdr)) + n = new_data._header["fnsub"] + + if new_data._header["ftflgs"] & 64 == 64 or not ( + 75 <= new_data._header["fversn"] <= 77 + ): # This is the multiple XY curves in file flag. + raise StonerLoadError( + "Filetype not implemented yet ! ftflgs={ftflgs}, fversn={fversn}".format(**new_data._header) ) - self._xvars = [ - "Arbitrary", - "Wavenumber (cm-1)", - "Micrometers (um)", - "Nanometers (nm)", - "Seconds", - "Minutes", - "Hertz (Hz)", - "Kilohertz (KHz)", - "Megahertz (MHz)", - "Mass (M/z)", - "Parts per million (PPM)", - "Days", - "Years", - "Raman Shift (cm-1)", - "Raman Shift (cm-1)", - "eV", - "XYZ text labels in fcatxt (old 0x4D version only)", - "Diode Number", - "Channel", - "Degrees", - "Temperature (F)", - "Temperature (C)", - "Temperature (K)", - "Data Points", - "Milliseconds (mSec)", - "Microseconds (uSec)", - "Nanoseconds (nSec)", - "Gigahertz (GHz)", - "Centimeters (cm)", - "Meters (m)", - "Millimeters (mm)", - "Hours", - "Hours", - ] - self._yvars = [ - "Arbitrary Intensity", - "Interferogram", - "Absorbance", - "Kubelka-Monk", - "Counts", - "Volts", - "Degrees", - "Milliamps", - "Millimeters", - "Millivolts", - "Log(1/R)", - "Percent", - "Percent", - "Intensity", - "Relative Intensity", - "Energy", - "Decibel", - "Temperature (F)", - "Temperature (C)", - "Temperature (K)", - "Index of Refraction [N]", - "Extinction Coeff. [K]", - "Real", - "Imaginary", - "Complex", - "Complex", - "Transmission (ALL HIGHER MUST HAVE VALLEYS!)", - "Reflectance", - "Arbitrary or Single Beam with Valley Peaks", - "Emission", - "Emission", - ] - - self._header = dict(zip(keys, spchdr)) - n = self._header["fnsub"] - - if self._header["ftflgs"] & 64 == 64 or not ( - 75 <= self._header["fversn"] <= 77 - ): # This is the multiple XY curves in file flag. - raise Core.StonerLoadError( - "Filetype not implemented yet ! ftflgs={ftflgs}, fversn={fversn}".format(**self._header) - ) - # Read the xdata and add it to the file. - xdata = self._read_xdata(f) - data = np.zeros((self._pts, (n + 1))) # initialise the data soace - data[:, 0] = xdata # Put in the X-Data - column_headers = [self._xvars[self._header["fxtype"]]] # And label the X column correctly - - # Now we're going to read the Y-data - data = self._read_ydata(f, data, column_headers) - if self._header["flogoff"] != 0: # Ok, we've got a log, so read the log header and merge into metadata - self._read_loginfo(f) - # Ok now build the Stoner.Core.DataFile instance to return - self.data = data - # The next bit generates the metadata. We don't just copy the metadata because we need to figure out - # the typehints first - hence the loop - # here to call Core.DataFile.__setitem() - for x in self._header: - self[x] = self._header[x] - self.column_headers = column_headers - if len(self.column_headers) == 2: - self.setas = "xy" - return self - - -class VSMFile(Core.DataFile): - - """Extends Core.DataFile to open VSM Files.""" - - #: priority (int): is the load order for the class, smaller numbers are tried before larger numbers. - # .. note:: - # Subclasses with priority<=32 should make some positive identification that they have the right - # file type before attempting to read data. - priority = 16 # Now makes a positive ID of its contents - #: pattern (list of str): A list of file extensions that might contain this type of file. Used to construct - # the file load/save dialog boxes. - patterns = ["*.fld"] # Recognised filename patterns - - def __parse_VSM(self, header_line=3, data_line=3, header_delim=","): - """Parsing deliminated data without a leading column of metadata.copy. - - Keyword Arguments: - header_line (int): - The line in the file that contains the column headers. If None, then column headers are automatically - generated. - data_line (int): - The line on which the data starts - header_delim (strong): - The delimiter used for separating header values - - Returns: - Nothing, but modifies the current object. - - Note: - The default values are configured fir read VSM data files - """ - try: - with FileManager(self.filename, errors="ignore", encoding="utf-8") as f: - for i, line in enumerate(f): - if i == 0: - first = line.strip() - self["Timestamp"] = first - check = datetime.strptime(first, "%a %b %d %H:%M:%S %Y") - if check is None: - raise Core.StonerLoadError("Not a VSM file ?") - elif i == 1: - assertion(line.strip() == "") - elif i == 2: - header_string = line.strip() - elif i == header_line: - unit_string = line.strip() - column_headers = [ - f"{h.strip()} ({u.strip()})" - for h, u in zip(header_string.split(header_delim), unit_string.split(header_delim)) - ] - elif i > 3: + # Read the xdata and add it to the file. + xdata = _read_spc_xdata(new_data, f) + data = np.zeros((new_data._pts, (n + 1))) # initialise the data soace + data[:, 0] = xdata # Put in the X-Data + column_headers = [new_data._xvars[new_data._header["fxtype"]]] # And label the X column correctly + + # Now we're going to read the Y-data + data = _read_spc_ydata(new_data, f, data, column_headers) + if new_data._header["flogoff"] != 0: # Ok, we've got a log, so read the log header and merge into metadata + _read_spc_loginfo(new_data, f) + # Ok now build the Stoner.DataFile instance to return + new_data.data = data + # The next bit generates the metadata. We don't just copy the metadata because we need to figure out + # the typehints first - hence the loop + # here to call DataFile.__setitem() + for x in new_data._header: + new_data[x] = new_data._header[x] + new_data.column_headers = column_headers + if len(new_data.column_headers) == 2: + new_data.setas = "xy" + return new_data + + +@register_loader(patterns=[(".fld", 16), (".dat", 32)], mime_types=("text/plain", 16), name="VSMFile", what="Data") +def load_vsm(new_data, filename=None, *args, header_line=3, data_line=3, header_delim=",", **kargs): + """VSM file loader routine. + + Args: + filename (string or bool): + File to load. If None then the existing filename is used, if False, then a file dialog will be used. + + Keyword Arguments: + header_line (int): + The line in the file that contains the column headers. If None, then column headers are automatically + generated. + data_line (int): + The line on which the data starts + header_delim (strong): + The delimiter used for separating header values + + Returns: + A copy of the itnew_data after loading the data. + """ + new_data.filename = filename + try: + with FileManager(filename, errors="ignore", encoding="utf-8") as f: + for i, line in enumerate(f): + if i == 0: + first = line.strip() + new_data["Timestamp"] = first + check = datetime.strptime(first, "%a %b %d %H:%M:%S %Y") + if check is None: + raise StonerLoadError("Not a VSM file ?") + elif i == 1: + assertion(line.strip() == "") + elif i == 2: + header_string = line.strip() + elif i == header_line: + unit_string = line.strip() + column_headers = [ + f"{h.strip()} ({u.strip()})" + for h, u in zip(header_string.split(header_delim), unit_string.split(header_delim)) + ] + elif i > 3: + break + except (StonerAssertionError, ValueError, AssertionError, TypeError) as err: + raise StonerLoadError(f"Not a VSM File {err}") from err + new_data.data = np.genfromtxt( + new_data.filename, + dtype="float", + usemask=True, + skip_header=data_line - 1, + missing_values=["6:0", "---"], + invalid_raise=False, + ) + + new_data.data = np.ma.mask_rows(new_data.data) + cols = new_data.data.shape[1] + new_data.data = np.reshape(new_data.data.compressed(), (-1, cols)) + new_data.column_headers = column_headers + new_data.setas(x="H_vsm (T)", y="m (emu)") # pylint: disable=not-callable + return new_data + + +@register_loader( + patterns=".dql", + mime_types=[("application/x-wine-extension-ini", 16), ("text/plain", 16)], + name="XRDFile", + what="Data", +) +def load_xrd(new_data, filename=None, *args, **kargs): + """Read an XRD DataFile as produced by the Brucker diffractometer. + + Args: + filename (string or bool): + File to load. If None then the existing filename is used, if False, then a file dialog will be used. + + Returns: + A copy of the itnew_data after loading the data. + + Notes: + Format is ini file like but not enough to do standard inifile processing - in particular + one can have multiple sections with the same name (!) + """ + if filename is None or not filename: + new_data.get_filename("r") + else: + new_data.filename = filename + sh = re.compile(r"\[(.+)\]") # Regexp to grab section name + with FileManager(new_data.filename, errors="ignore", encoding="utf-8") as f: # Read filename linewise + if f.readline().strip() != ";RAW4.00": # Check we have the correct fileformat + raise StonerLoadError("File Format Not Recognized !") + drive = 0 + for line in f: # for each line + m = sh.search(line) + if m: # This is a new section + section = m.group(1) + if section == "Drive": # If this is a Drive section we need to know which Drive Section it is + section = section + str(drive) + drive = drive + 1 + elif section == "Data": # Data section contains the business but has a redundant first line + f.readline() + for line in f: # Now start reading lines in this section... + if line.strip() == "": + # A blank line marks the end of the section, so go back to the outer loop which will + # handle a new section break - except (StonerAssertionError, ValueError, AssertionError, TypeError) as err: - raise Core.StonerLoadError(f"Not a VSM File {err}") from err - self.data = np.genfromtxt( - self.filename, - dtype="float", - usemask=True, - skip_header=data_line - 1, - missing_values=["6:0", "---"], - invalid_raise=False, - ) - - self.data = np.ma.mask_rows(self.data) - cols = self.data.shape[1] - self.data = np.reshape(self.data.compressed(), (-1, cols)) - self.column_headers = column_headers - self.setas(x="H_vsm (T)", y="m (emu)") # pylint: disable=not-callable - - def _load(self, filename=None, *args, **kargs): - """VSM file loader routine. - - Args: - filename (string or bool): - File to load. If None then the existing filename is used, if False, then a file dialog will be used. - - Returns: - A copy of the itself after loading the data. - """ - if filename is None or not filename: - self.get_filename("r") - else: - self.filename = filename - self.__parse_VSM() - return self - - -class XRDFile(Core.DataFile): - - """Loads Files from a Brucker D8 Discovery X-Ray Diffractometer.""" - - #: priority (int): is the load order for the class, smaller numbers are tried before larger numbers. - # .. note:: - # Subclasses with priority<=32 should make some positive identification that they have the right - # file type before attempting to read data. - priority = 16 # Makes a positive id of its file contents - #: pattern (list of str): A list of file extensions that might contain this type of file. Used to construct - # the file load/save dialog boxes. - patterns = ["*.dql"] # Recognised filename patterns - - mime_type = ["application/x-wine-extension-ini", "text/plain"] - - def __init__(self, *args, **kargs): - """Add a public attribute to XRD File.""" - super().__init__(*args, **kargs) - self._public_attrs = {"four_bounce": bool} - - def _load(self, filename=None, *args, **kargs): - """Read an XRD Core.DataFile as produced by the Brucker diffractometer. - - Args: - filename (string or bool): - File to load. If None then the existing filename is used, if False, then a file dialog will be used. - - Returns: - A copy of the itself after loading the data. - - Notes: - Format is ini file like but not enough to do standard inifile processing - in particular - one can have multiple sections with the same name (!) - """ - if filename is None or not filename: - self.get_filename("r") - else: - self.filename = filename - sh = re.compile(r"\[(.+)\]") # Regexp to grab section name - with FileManager(self.filename, errors="ignore", encoding="utf-8") as f: # Read filename linewise - if f.readline().strip() != ";RAW4.00": # Check we have the correct fileformat - raise Core.StonerLoadError("File Format Not Recognized !") - drive = 0 - for line in f: # for each line - m = sh.search(line) - if m: # This is a new section - section = m.group(1) - if section == "Drive": # If this is a Drive section we need to know which Drive Section it is - section = section + str(drive) - drive = drive + 1 - elif section == "Data": # Data section contains the business but has a redundant first line - f.readline() - for line in f: # Now start reading lines in this section... - if line.strip() == "": - # A blank line marks the end of the section, so go back to the outer loop which will - # handle a new section - break - if section == "Data": # In the Data section read lines of data value,vale - parts = line.split(",") - angle = parts[0].strip() - counts = parts[1].strip() - dataline = np.array([float(angle), float(counts)]) - self.data = np.append(self.data, dataline) - else: # Other sections contain metadata - parts = line.split("=") - key = parts[0].strip() - data = parts[1].strip() - # Keynames in main metadata are section:key - use theCore.DataFile magic to do type - # determination - self[section + ":" + key] = string_to_type(data) - column_headers = ["Angle", "Counts"] # Assume the columns were Angles and Counts - - self.data = np.reshape(self.data, (-1, 2)) - self.setas = "xy" - self.four_bounce = self["HardwareConfiguration:Monochromator"] == 1 - self.column_headers = column_headers - if kargs.pop("Q", False): - self.to_Q() - return self - - def to_Q(self, l=1.540593): - """Add an additional function to covert an angualr scale to momentum transfer. - - returns a copy of itself. - """ - self.add_column( - (4 * np.pi / l) * np.sin(np.pi * self.column(0) / 360), header="Momentum Transfer, Q ($\\AA^{-1}$)" - ) + if section == "Data": # In the Data section read lines of data value,vale + parts = line.split(",") + angle = parts[0].strip() + counts = parts[1].strip() + dataline = np.array([float(angle), float(counts)]) + new_data.data = np.append(new_data.data, dataline) + else: # Other sections contain metadata + parts = line.split("=") + key = parts[0].strip() + data = parts[1].strip() + # Keynames in main metadata are section:key - use theDataFile magic to do type + # determination + new_data[section + ":" + key] = string_to_type(data) + column_headers = ["Angle", "Counts"] # Assume the columns were Angles and Counts + + new_data.data = np.reshape(new_data.data, (-1, 2)) + new_data.setas = "xy" + new_data._public_attrs = {"four_bounce": bool} + new_data.four_bounce = new_data["HardwareConfiguration:Monochromator"] == 1 + new_data.column_headers = column_headers + if kargs.pop("Q", False): + _to_Q(new_data) + return new_data diff --git a/Stoner/formats/maximus.py b/Stoner/formats/maximus.py index 64a8383d2..105dd33fd 100755 --- a/Stoner/formats/maximus.py +++ b/Stoner/formats/maximus.py @@ -21,6 +21,9 @@ from ..HDF5 import HDFFileManager from ..tools.file import FileManager from ..core.exceptions import StonerLoadError +from ..tools import make_Data + +from .decorators import register_loader SCAN_NO = re.compile(r"MPI_(\d+)") @@ -36,6 +39,90 @@ def _raise_error(openfile, message=""): pass +@register_loader( + patterns=[(".hdr", 16), (".xsp", 16)], mime_types=("text/plain", 16), name="MaximusSpectra", what="Data" +) +def load_maximus_spectra(new_data, *args, **kargs): + """Maximus xsp file loader routine. + + Args: + filename (string or bool): File to load. If None then the existing filename is used, + if False, then a file dialog will be used. + + Returns: + A copy of the itnew_data after loading the data. + """ + filename = kargs.get("filename", args[0]) + if filename is None or not filename: + new_data.get_filename("r") + else: + new_data.filename = filename + # Open the file and read the main file header and unpack into a dict + try: + pth = Path(new_data.filename) + except (TypeError, ValueError) as err: + raise StonerLoadError("Can only open things that can be converted to paths!") from err + if pth.suffix != ".hdr": # Passed a .xim or .xsp file in instead of the hdr file. + pth = Path("_".join(str(pth).split("_")[:-1]) + ".hdr") + stem = pth.parent / pth.stem + + try: + hdr = _flatten_header(hdr_to_dict(pth)) + if "Point Scan" not in hdr["ScanDefinition.Type"]: + raise StonerLoadError("Not an Maximus Single Image File") + except (StonerLoadError, ValueError, TypeError, IOError) as err: + raise StonerLoadError("Error loading as Maximus File") from err + header, data, dims = read_scan(stem) + new_data.metadata.update(_flatten_header(header)) + new_data.data = np.column_stack((dims[0], data)) + headers = [new_data.metadata["ScanDefinition.Regions.PAxis.Name"]] + if len(dims) == 2: + headers.extend([str(x) for x in dims[1]]) + else: + headers.append(new_data.metadata["ScanDefinition.Channels.Name"]) + new_data.column_headers = headers + new_data.setas = "xy" + return new_data + + +@register_loader( + patterns=[(".hdr", 16), (".xim", 16)], mime_types=("text/plain", 16), name="MaximusImage", what="Image" +) +def load_maximus_image(new_data, filename, *args, **kargs): + """Load an ImageFile by calling the ImageArray method instead.""" + try: + new_data.filename = filename + pth = Path(new_data.filename) + except TypeError as err: + raise StonerLoadError(f"UUnable to interpret {filename} as a path like object") from err + if pth.suffix != ".hdr": # Passed a .xim or .xsp file in instead of the hdr file. + pth = Path("_".join(str(pth).split("_")[:-1]) + ".hdr") + stem = pth.parent / pth.stem + + try: + hdr = _flatten_header(hdr_to_dict(pth)) + if "Image Scan" not in hdr["ScanDefinition.Type"]: + raise StonerLoadError("Not an Maximus Single Image File") + except (StonerLoadError, ValueError, TypeError, IOError) as err: + raise StonerLoadError("Error loading as Maximus File") from err + data = read_scan(stem)[1] + new_data.metadata.update(hdr) + if isinstance(new_data, make_Data(None, what="Data")): + if data.ndim == 3: + data = data[:, :, 0] + new_data.data = data + elif isinstance(new_data, make_Data(None, what="Image")): + new_data.image = data + return new_data + + +@register_loader( + patterns=[(".hdr", 16), (".xim", 16)], mime_types=("text/plain", 16), name="MaximusImage", what="Data" +) +def load_maximus_data(new_data, filename, *args, **kargs): + return load_maximus_image(new_data, filename, *args, **kargs) + + class MaximusSpectra(DataFile): """Provides a :py:class:`Stoner.DataFile` subclass for loading Point spectra from Maximus.""" diff --git a/Stoner/formats/rigs.py b/Stoner/formats/rigs.py index 0c440c27b..fa43c9b66 100755 --- a/Stoner/formats/rigs.py +++ b/Stoner/formats/rigs.py @@ -2,7 +2,6 @@ # -*- coding: utf-8 -*- """Implement DataFile like classes for Various experimental rigs.""" -__all__ = ["BigBlueFile", "BirgeIVFile", "MokeFile", "FmokeFile", "EasyPlotFile", "PinkLibFile"] import re import csv @@ -10,335 +9,271 @@ from ..compat import bytes2str from ..core.base import string_to_type -from .. import Core -from .generic import CSVFile +from ..core.exceptions import StonerLoadError +from .generic import load_csvfile from ..tools.file import FileManager - -class BigBlueFile(CSVFile): - - """Extends CSVFile to load files from Nick Porter's old BigBlue code.""" - - #: priority (int): is the load order for the class, smaller numbers are tried before larger numbers. - # .. note:: - # Subclasses with priority<=32 should make some positive identification that they have the right - # file type before attempting to read data. - priority = 64 # Also rather generic file format so make a lower priority - #: pattern (list of str): A list of file extensions that might contain this type of file. Used to construct - # the file load/save dialog boxes. - patterns = ["*.dat", "*.iv", "*.rvt"] # Recognised filename patterns - - def _load(self, filename, *args, **kargs): - """Just call the parent class but with the right parameters set. - - Args: - filename (string or bool): File to load. If None then the existing filename is used, - if False, then a file dialog will be used. - - Returns: - A copy of the itself after loading the data. - """ - if filename is None or not filename: - self.get_filename("r") - else: - self.filename = filename - - super()._load(self.filename, *args, header_line=3, data_line=7, data_delim=" ", header_delim=",") - if np.all(np.isnan(self.data)): - raise Core.StonerLoadError("All data was NaN in Big Blue format") - return self - - -class BirgeIVFile(Core.DataFile): - - """Implements the IV File format used by the Birge Group in Michigan State University Condesned Matter Physiscs.""" - - patterns = ["*.dat"] - - def _load(self, filename, *args, **kargs): - """File loader for PinkLib. - - Args: - filename (string or bool): File to load. If None then the existing filename is used, - if False, then a file dialog will be used. - - Returns: - A copy of the itself after loading the data. - """ - if filename is None or not filename: - self.get_filename("r") - else: - self.filename = filename - ix = 0 - with FileManager(self.filename, "r", errors="ignore", encoding="utf-8") as f: # Read filename linewise - if not re.compile(r"\d{1,2}/\d{1,2}/\d{4}").match(f.readline()): - raise Core.StonerLoadError("Not a BirgeIVFile as no date on first line") - data = f.readlines() - expected = ["Vo(-))", "Vo(+))", "Ic(+)", "Ic(-)"] - for length, m in zip(data[-4:], expected): - if not length.startswith(m): - raise Core.StonerLoadError("Not a BirgeIVFile as wrong footer line") - key = length[: len(m)] - val = length[len(m) :] - if "STDEV" in val: - ix2 = val.index("STDEV") - key2 = val[ix2 : ix2 + 4 + len(key)] - val2 = val[ix2 + 4 + len(key) :] - self.metadata[key2] = string_to_type(val2.strip()) - val = val[:ix2] - self.metadata[key] = string_to_type(val.strip()) - for ix, line in enumerate(data): # Scan the ough lines to get metadata - if ":" in line: - parts = line.split(":") - self.metadata[parts[0].strip()] = string_to_type(parts[1].strip()) - elif "," in line: - for part in line.split(","): - parts = part.split(" ") - self.metadata[parts[0].strip()] = string_to_type(parts[1].strip()) - elif line.startswith("H "): - self.metadata["H"] = string_to_type(line.split(" ")[1].strip()) - else: - headers = [x.strip() for x in line.split(" ")] - break +from .decorators import register_loader + + +@register_loader( + patterns=[(".dat", 64), (".iv", 64), (".rvt", 64)], mime_types=("text/plain", 64), name="BigBlueFile", what="Data" +) +def load_bigblue(new_data, filename, *args, **kargs): + """Just call the parent class but with the right parameters set. + + Args: + filename (string or bool): File to load. If None then the existing filename is used, + if False, then a file dialog will be used. + + Returns: + A copy of the itnew_data after loading the data. + """ + new_data.filename = filename + + new_data = load_csvfile(new_data, filename, *args, header_line=3, data_line=7, data_delim=" ", header_delim=",") + if np.all(np.isnan(new_data.data)): + raise StonerLoadError("All data was NaN in Big Blue format") + return new_data + + +@register_loader(patterns=(".dat", 32), mime_types=("text/plain", 32), name="BirgeIVFile", what="Data") +def load_birge(new_data, filename, *args, **kargs): + """File loader for PinkLib. + + Args: + filename (string or bool): File to load. If None then the existing filename is used, + if False, then a file dialog will be used. + + Returns: + A copy of the itnew_data after loading the data. + """ + new_data.filename = filename + ix = 0 + with FileManager(new_data.filename, "r", errors="ignore", encoding="utf-8") as f: # Read filename linewise + if not re.compile(r"\d{1,2}/\d{1,2}/\d{4}").match(f.readline()): + raise StonerLoadError("Not a BirgeIVFile as no date on first line") + data = f.readlines() + expected = ["Vo(-))", "Vo(+))", "Ic(+)", "Ic(-)"] + for length, m in zip(data[-4:], expected): + if not length.startswith(m): + raise StonerLoadError("Not a BirgeIVFile as wrong footer line") + key = length[: len(m)] + val = length[len(m) :] + if "STDEV" in val: + ix2 = val.index("STDEV") + key2 = val[ix2 : ix2 + 4 + len(key)] + val2 = val[ix2 + 4 + len(key) :] + new_data.metadata[key2] = string_to_type(val2.strip()) + val = val[:ix2] + new_data.metadata[key] = string_to_type(val.strip()) + for ix, line in enumerate(data): # Scan the ough lines to get metadata + if ":" in line: + parts = line.split(":") + new_data.metadata[parts[0].strip()] = string_to_type(parts[1].strip()) + elif "," in line: + for part in line.split(","): + parts = part.split(" ") + new_data.metadata[parts[0].strip()] = string_to_type(parts[1].strip()) + elif line.startswith("H "): + new_data.metadata["H"] = string_to_type(line.split(" ")[1].strip()) else: - raise Core.StonerLoadError("Oops ran off the end of the file!") - self.data = np.genfromtxt(filename, skip_header=ix + 2, skip_footer=4) - self.column_headers = headers - - self.setas = "xy" - return self - - -class MokeFile(Core.DataFile): - - """Class that extgends Core.DataFile to load files from the Leeds MOKE system.""" - - #: priority (int): is the load order for the class, smaller numbers are tried before larger numbers. - # .. note:: - # Subclasses with priority<=32 should make some positive identification that they have the right - # file type before attempting to read data. - priority = 16 - #: pattern (list of str): A list of file extensions that might contain this type of file. Used to construct - # the file load/save dialog boxes. - patterns = ["*.dat", "*.txt"] - - def _load(self, filename, *args, **kargs): - """Leeds MOKE file loader routine. - - Args: - filename (string or bool): File to load. If None then the existing filename is used, - if False, then a file dialog will be used. - - Returns: - A copy of the itself after loading the data. - """ - if filename is None or not filename: - self.get_filename("r") + headers = [x.strip() for x in line.split(" ")] + break else: - self.filename = filename - with FileManager(self.filename, mode="rb") as f: + raise StonerLoadError("Oops ran off the end of the file!") + new_data.data = np.genfromtxt(filename, skip_header=ix + 2, skip_footer=4) + new_data.column_headers = headers + + new_data.setas = "xy" + return new_data + + +@register_loader(patterns=[(".dat", 16), (".txt", 16)], mime_types=("text/plain", 16), name="MokeFile", what="Data") +def load_old_moke(new_data, filename, *args, **kargs): + """Leeds MOKE file loader routine. + + Args: + filename (string or bool): File to load. If None then the existing filename is used, + if False, then a file dialog will be used. + + Returns: + A copy of the itnew_data after loading the data. + """ + new_data.filename = filename + with FileManager(new_data.filename, mode="rb") as f: + line = bytes2str(f.readline()).strip() + if line != "#Leeds CM Physics MOKE": + raise StonerLoadError("Not a Core.DataFile from the Leeds MOKE") + while line.startswith("#") or line == "": + parts = line.split(":") + if len(parts) > 1: + key = parts[0][1:] + data = ":".join(parts[1:]).strip() + new_data[key] = data line = bytes2str(f.readline()).strip() - if line != "#Leeds CM Physics MOKE": - raise Core.StonerLoadError("Not a Core.DataFile from the Leeds MOKE") - while line.startswith("#") or line == "": - parts = line.split(":") - if len(parts) > 1: - key = parts[0][1:] - data = ":".join(parts[1:]).strip() - self[key] = data - line = bytes2str(f.readline()).strip() - column_headers = [x.strip() for x in line.split(",")] - self.data = np.genfromtxt(f, delimiter=",") - self.setas = "xy.de" - self.column_headers = column_headers - return self - - -class FmokeFile(Core.DataFile): - - """Extends Core.DataFile to open Fmoke Files.""" - - #: priority (int): is the load order for the class, smaller numbers are tried before larger numbers. - # .. note:: - # Subclasses with priority<=32 should make some positive identification that they have the right - # file type before attempting to read data. - priority = 16 # Makes a positive ID check of its contents so give it priority in autoloading - #: pattern (list of str): A list of file extensions that might contain this type of file. Used to construct - # the file load/save dialog boxes. - patterns = ["*.dat"] # Recognised filename patterns - - def _load(self, filename, *args, **kargs): - """Sheffield Focussed MOKE file loader routine. - - Args: - filename (string or bool): File to load. If None then the existing filename is used, - if False, then a file dialog will be used. - - Returns: - A copy of the itself after loading the data. - """ - if filename is None or not filename: - self.get_filename("r") - else: - self.filename = filename - with FileManager(self.filename, mode="rb") as f: - try: - value = [float(x.strip()) for x in bytes2str(f.readline()).split("\t")] - except (TypeError, ValueError) as err: - f.close() - raise Core.StonerLoadError("Not an FMOKE file?") from err - label = [x.strip() for x in bytes2str(f.readline()).split("\t")] - if label[0] != "Header:": - f.close() - raise Core.StonerLoadError("Not a Focussed MOKE file !") - del label[0] - for k, v in zip(label, value): - self.metadata[k] = v # Create metadata from first 2 lines - column_headers = [x.strip() for x in bytes2str(f.readline()).split("\t")] - self.data = np.genfromtxt(f, dtype="float", delimiter="\t", invalid_raise=False) - self.column_headers = column_headers - return self - - -class EasyPlotFile(Core.DataFile): - - """A class that will extract as much as it can from an EasyPlot save File.""" - - #: priority (int): is the load order for the class, smaller numbers are tried before larger numbers. - # .. note:: - # Subclasses with priority<=32 should make some positive identification that they have the right - # file type before attempting to read data. - priority = 32 # Fairly generic, but can do some explicit testing - - def _load(self, filename, *args, **kargs): - """Private loader method.""" - if filename is None or not filename: - self.get_filename("r") - else: - self.filename = filename - - datastart = -1 - dataend = -1 - - i = 0 - with FileManager(self.filename, "r", errors="ignore", encoding="utf-8") as data: - if "******** EasyPlot save file ********" not in data.read(1024): - raise Core.StonerLoadError("Not an EasyPlot Save file?") - data.seek(0) - for i, line in enumerate(data): - line = line.strip() - if line == "": - continue - if line[0] not in "-0123456789" and dataend < 0 <= datastart: - dataend = i - if line.startswith('"') and ":" in line: - parts = [x.strip() for x in line.strip('"').split(":")] - self[parts[0]] = string_to_type(":".join(parts[1:])) - elif line.startswith("/"): # command - parts = [x.strip('"') for x in next(csv.reader([line], delimiter=" ")) if x != ""] - cmd = parts[0].strip("/") - if len(cmd) > 1: - cmdname = f"_{cmd}_cmd" - if cmdname in dir(self): # If this command is implemented as a function run it - cmd = getattr(self, f"_{cmd}_cmd") - cmd(parts[1:]) - else: - if len(parts[1:]) > 1: - cmd = cmd + "." + parts[1] - value = ",".join(parts[2:]) - elif len(parts[1:]) == 1: - value = parts[1] - else: - value = True - self[cmd] = value - elif line[0] in "-0123456789" and datastart < 0: # start of data - datastart = i - if "," in line: - delimiter = "," + column_headers = [x.strip() for x in line.split(",")] + new_data.data = np.genfromtxt(f, delimiter=",") + new_data.setas = "xy.de" + new_data.column_headers = column_headers + return new_data + + +@register_loader(patterns=(".dat", 16), mime_types=("text/plain", 16), name="FmokeFile", what="Data") +def load_fmoke(new_data, filename, *args, **kargs): + """Sheffield Focussed MOKE file loader routine. + + Args: + filename (string or bool): File to load. If None then the existing filename is used, + if False, then a file dialog will be used. + + Returns: + A copy of the itnew_data after loading the data. + """ + if filename is None or not filename: + new_data.get_filename("r") + else: + new_data.filename = filename + with FileManager(new_data.filename, mode="rb") as f: + try: + value = [float(x.strip()) for x in bytes2str(f.readline()).split("\t")] + except (TypeError, ValueError) as err: + f.close() + raise StonerLoadError("Not an FMOKE file?") from err + label = [x.strip() for x in bytes2str(f.readline()).split("\t")] + if label[0] != "Header:": + f.close() + raise StonerLoadError("Not a Focussed MOKE file !") + del label[0] + for k, v in zip(label, value): + new_data.metadata[k] = v # Create metadata from first 2 lines + column_headers = [x.strip() for x in bytes2str(f.readline()).split("\t")] + new_data.data = np.genfromtxt(f, dtype="float", delimiter="\t", invalid_raise=False) + new_data.column_headers = column_headers + return new_data + + +def _extend_columns(new_data, i): + """Ensure the column headers are at least i long.""" + if len(new_data.column_headers) < i: + length = len(new_data.column_headers) + new_data.data = np.append( + new_data.data, np.zeros((new_data.shape[0], i - length)), axis=1 + ) # Need to expand the array first + new_data.column_headers.extend([f"Column {x}" for x in range(length, i)]) + + +def _et_cmd(new_data, parts): + """Handle axis labellling command.""" + if parts[0] == "x": + _extend_columns(new_data, 1) + new_data.column_headers[0] = parts[1] + elif parts[0] == "y": + _extend_columns(new_data, 2) + new_data.column_headers[1] = parts[1] + elif parts[0] == "g": + new_data["title"] = parts[1] + + +def _td_cmd(new_data, parts): + new_data.setas = parts[0] + + +def _sa_cmd(new_data, parts): + """Implement the sa (set-axis?) command.""" + if parts[0] == "l": # Legend + col = int(parts[2]) + _extend_columns(new_data, col + 1) + new_data.column_headers[col] = parts[1] + + +@register_loader(patterns=("*", 64), mime_types=("text/plain", 64), name="EasyPlotFile", what="Data") +def load_easyplot(new_data, filename, *args, **kargs): + """Private loader method.""" + new_data.filename = filename + + datastart = -1 + dataend = -1 + + i = 0 + with FileManager(new_data.filename, "r", errors="ignore", encoding="utf-8") as data: + if "******** EasyPlot save file ********" not in data.read(1024): + raise StonerLoadError("Not an EasyPlot Save file?") + data.seek(0) + for i, line in enumerate(data): + line = line.strip() + if line == "": + continue + if line[0] not in "-0123456789" and dataend < 0 <= datastart: + dataend = i + if line.startswith('"') and ":" in line: + parts = [x.strip() for x in line.strip('"').split(":")] + new_data[parts[0]] = string_to_type(":".join(parts[1:])) + elif line.startswith("/"): # command + parts = [x.strip('"') for x in next(csv.reader([line], delimiter=" ")) if x != ""] + cmd = parts[0].strip("/") + if len(cmd) > 1: + cmdname = f"_{cmd}_cmd" + if cmdname in globals(): + cmd = globals()[cmdname] + cmd(new_data, parts[1:]) else: - delimiter = None - if dataend < 0: - dataend = i - self.data = np.genfromtxt(self.filename, skip_header=datastart, skip_footer=i - dataend, delimiter=delimiter) - if self.data.shape[1] == 2: - self.setas = "xy" - return self - - def _extend_columns(self, i): - """Ensure the column headers are at least i long.""" - if len(self.column_headers) < i: - length = len(self.column_headers) - self.data = np.append( - self.data, np.zeros((self.shape[0], i - length)), axis=1 - ) # Need to expand the array first - self.column_headers.extend([f"Column {x}" for x in range(length, i)]) - - def _et_cmd(self, parts): - """Handle axis labellling command.""" - if parts[0] == "x": - self._extend_columns(1) - self.column_headers[0] = parts[1] - elif parts[0] == "y": - self._extend_columns(2) - self.column_headers[1] = parts[1] - elif parts[0] == "g": - self["title"] = parts[1] - - def _td_cmd(self, parts): - self.setas = parts[0] - - def _sa_cmd(self, parts): - """Implement the sa (set-axis?) command.""" - if parts[0] == "l": # Legend - col = int(parts[2]) - self._extend_columns(col + 1) - self.column_headers[col] = parts[1] - - -class PinkLibFile(Core.DataFile): - - """Extends Core.DataFile to load files from MdV's PINK library - as used by the GMR anneal rig.""" - - #: priority (int): is the load order for the class, smaller numbers are tried before larger numbers. - # .. note:: - # Subclasses with priority<=32 should make some positive identification that they have the right - # file type before attempting to read data. - priority = 32 # reasonably generic format - #: pattern (list of str): A list of file extensions that might contain this type of file. Used to construct - # the file load/save dialog boxes. - patterns = ["*.dat"] # Recognised filename patterns - - def _load(self, filename=None, *args, **kargs): - """File loader for PinkLib. - - Args: - filename (string or bool): File to load. If None then the existing filename is used, - if False, then a file dialog will be used. - - Returns: - A copy of the itself after loading the data. - """ - if filename is None or not filename: - self.get_filename("r") - else: - self.filename = filename - with FileManager(self.filename, "r", errors="ignore", encoding="utf-8") as f: # Read filename linewise - if "PINKlibrary" not in f.readline(): - raise Core.StonerLoadError("Not a PINK file") - f = f.readlines() - happened_before = False - for i, line in enumerate(f): - if line[0] != "#" and not happened_before: - header_line = i - 2 # -2 because there's a commented out data line - happened_before = True - continue # want to get the metadata at the bottom of the file too - if any(s in line for s in ("Start time", "End time", "Title")): - tmp = line.strip("#").split(":") - self.metadata[tmp[0].strip()] = ":".join(tmp[1:]).strip() - column_headers = f[header_line].strip("#\t ").split("\t") - data = np.genfromtxt(f, dtype="float", delimiter="\t", invalid_raise=False, comments="#") - self.data = data[:, 0:-2] # Deal with an errant tab at the end of each line - self.column_headers = column_headers - if np.all([h in column_headers for h in ("T (C)", "R (Ohm)")]): - self.setas(x="T (C)", y="R (Ohm)") # pylint: disable=not-callable - return self + if len(parts[1:]) > 1: + cmd = cmd + "." + parts[1] + value = ",".join(parts[2:]) + elif len(parts[1:]) == 1: + value = parts[1] + else: + value = True + new_data[cmd] = value + elif line[0] in "-0123456789" and datastart < 0: # start of data + datastart = i + if "," in line: + delimiter = "," + else: + delimiter = None + if dataend < 0: + dataend = i + new_data.data = np.genfromtxt( + new_data.filename, skip_header=datastart, skip_footer=i - dataend, delimiter=delimiter + ) + if new_data.data.shape[1] == 2: + new_data.setas = "xy" + return new_data + + +@register_loader(patterns=(".dat", 64), mime_types=("text/plain", 64), name="PinkLibFile", what="Data") +def _load(new_data, filename=None, *args, **kargs): + """File loader for PinkLib. + + Args: + filename (string or bool): File to load. If None then the existing filename is used, + if False, then a file dialog will be used. + + Returns: + A copy of the itnew_data after loading the data. + """ + if filename is None or not filename: + new_data.get_filename("r") + else: + new_data.filename = filename + with FileManager(new_data.filename, "r", errors="ignore", encoding="utf-8") as f: # Read filename linewise + if "PINKlibrary" not in f.readline(): + raise StonerLoadError("Not a PINK file") + f = f.readlines() + happened_before = False + for i, line in enumerate(f): + if line[0] != "#" and not happened_before: + header_line = i - 2 # -2 because there's a commented out data line + happened_before = True + continue # want to get the metadata at the bottom of the file too + if any(s in line for s in ("Start time", "End time", "Title")): + tmp = line.strip("#").split(":") + new_data.metadata[tmp[0].strip()] = ":".join(tmp[1:]).strip() + column_headers = f[header_line].strip("#\t ").split("\t") + data = np.genfromtxt(f, dtype="float", delimiter="\t", invalid_raise=False, comments="#") + new_data.data = data[:, 0:-2] # Deal with an errant tab at the end of each line + new_data.column_headers = column_headers + if np.all([h in column_headers for h in ("T (C)", "R (Ohm)")]): + new_data.setas(x="T (C)", y="R (Ohm)") # pylint: disable=not-callable + return new_data diff --git a/Stoner/formats/simulations.py b/Stoner/formats/simulations.py index 9ffb7c6e0..f32ef4f0e 100755 --- a/Stoner/formats/simulations.py +++ b/Stoner/formats/simulations.py @@ -2,15 +2,15 @@ # -*- coding: utf-8 -*- """Implements classes to load file formats from various simulation packages.""" -__all__ = ["GenXFile", "OVFFile"] import re import numpy as np -from ..Core import DataFile from ..core.exceptions import StonerLoadError from ..tools.file import FileManager +from .decorators import register_loader + def _read_line(data, metadata): """Read a single line and add to a metadata dictionary.""" @@ -31,155 +31,128 @@ def _read_line(data, metadata): return True -class GenXFile(DataFile): - - """Extends DataFile for GenX Exported data.""" - - #: priority (int): is the load order for the class, smaller numbers are tried before larger numbers. - # .. note:: - # Subclasses with priority<=32 should make some positive identification that they have the right - # file type before attempting to read data. - priority = 16 - #: pattern (list of str): A list of file extensions that might contain this type of file. Used to construct - # the file load/save dialog boxes. - patterns = ["*.dat"] # Recognised filename patterns - - def _load(self, filename=None, *args, **kargs): - """Load function. File format has space delimited columns from row 3 onwards.""" - if filename is None or not filename: - self.get_filename("r") - else: - self.filename = filename - pattern = re.compile(r'# Dataset "([^\"]*)" exported from GenX on (.*)$') - pattern2 = re.compile(r"#\sFile\sexported\sfrom\sGenX\'s\sReflectivity\splugin") - i = 0 - ix = 0 - with FileManager(self.filename, "r", errors="ignore", encoding="utf-8") as datafile: +@register_loader(patterns=(".dat", 16), mime_types=("text/plain", 16), name="GenXFile", what="Data") +def load_genx(new_data, filename=None, *args, **kargs): + """Load function. File format has space delimited columns from row 3 onwards.""" + new_data.filename = filename + pattern = re.compile(r'# Dataset "([^\"]*)" exported from GenX on (.*)$') + pattern2 = re.compile(r"#\sFile\sexported\sfrom\sGenX\'s\sReflectivity\splugin") + i = 0 + ix = 0 + with FileManager(new_data.filename, "r", errors="ignore", encoding="utf-8") as datafile: + line = datafile.readline() + match = pattern.match(line) + match2 = pattern2.match(line) + if match is not None: + dataset = match.groups()[0] + date = match.groups()[1] + new_data["date"] = date + i = 2 + elif match2 is not None: line = datafile.readline() - match = pattern.match(line) - match2 = pattern2.match(line) - if match is not None: - dataset = match.groups()[0] - date = match.groups()[1] - self["date"] = date - i = 2 - elif match2 is not None: - line = datafile.readline() - self["date"] = line.split(":")[1].strip() - dataset = datafile.readline()[1:].strip() - i = 3 - else: - raise StonerLoadError("Not a GenXFile") - for ix, line in enumerate(datafile): - line = line.strip() - if line in ["# Headers:", "# Column labels:"]: - line = next(datafile)[1:].strip() - break - else: - raise StonerLoadError("Cannot find headers") - skip = ix + i + 2 - column_headers = [f.strip() for f in line.strip().split("\t")] - self.data = np.real(np.genfromtxt(self.filename, skip_header=skip, dtype=complex)) - self["dataset"] = dataset - if "sld" in dataset.lower(): - self["type"] = "SLD" - elif "asymmetry" in dataset.lower(): - self["type"] = "Asymmetry" - elif "dd" in dataset.lower(): - self["type"] = "Down" - elif "uu" in dataset.lower(): - self["type"] = "Up" - self.column_headers = column_headers - return self - - -class OVFFile(DataFile): - - """A class that reads OOMMF vector format files and constructs x,y,z,u,v,w data. - - OVF 1 and OVF 2 files with text or binary data and only files with a meshtype rectangular are supported - """ - - #: priority (int): is the load order for the class, smaller numbers are tried before larger numbers. - # .. note:: - # Subclasses with priority<=32 should make some positive identification that they have the right - # file type before attempting to read data. - priority = 16 - #: pattern (list of str): A list of file extensions that might contain this type of file. Used to construct - # the file load/save dialog boxes. - patterns = ["*.ovf"] # Recognised filename patterns - - mime_type = ["text/plain", "application/octet-stream"] - - def _load(self, filename=None, *args, **kargs): - """Load function. File format has space delimited columns from row 3 onwards. - - Notes: - This code can handle only the first segment in the data file. - """ - if filename is None or not filename: - self.get_filename("r") + new_data["date"] = line.split(":")[1].strip() + dataset = datafile.readline()[1:].strip() + i = 3 else: - self.filename = filename - # Reading in binary, converting to utf-8 where we should be in the text header - with FileManager(self.filename, "rb") as data: - line = data.readline().decode("utf-8", errors="ignore").strip("#\n \t\r") - if line not in ["OOMMF: rectangular mesh v1.0", "OOMMF OVF 2.0"]: - raise StonerLoadError("Not an OVF 1.0 or 2.0 file.") - while _read_line(data, self.metadata): - pass # Read the file until we reach the start of the data block. - if self.metadata["representation"] == "Binary": - size = ( # Work out the size of the data block to read - self.metadata["xnodes"] - * self.metadata["ynodes"] - * self.metadata["znodes"] - * self.metadata["valuedim"] - + 1 - ) * self.metadata["representation size"] - bin_data = data.read(size) - numbers = np.frombuffer(bin_data, dtype=f"f{new_data.metadata['representation size']}") chk = numbers[0] - if ( - chk != [1234567.0, 123456789012345.0][self.metadata["representation size"] // 4 - 1] - ): # If we have a good check number we can carry on, otherwise try the other endianness - numbers = np.frombuffer(bin_data, dtype=f">f{self.metadata['representation size']}") - chk = numbers[0] - if chk != [1234567.0, 123456789012345.0][self.metadata["representation size"] // 4 - 1]: - raise StonerLoadError("Bad binary data for ovf gile.") + if chk != [1234567.0, 123456789012345.0][new_data.metadata["representation size"] // 4 - 1]: + raise StonerLoadError("Bad binary data for ovf gile.") - data = np.reshape( - numbers[1:], - (self.metadata["xnodes"] * self.metadata["ynodes"] * self.metadata["znodes"], 3), - ) - else: - data = np.genfromtxt( - data, - max_rows=self.metadata["xnodes"] * self.metadata["ynodes"] * self.metadata["znodes"], - ) - xmin, xmax, xstep = ( - self.metadata["xmin"], - self.metadata["xmax"], - self.metadata["xstepsize"], - ) - ymin, ymax, ystep = ( - self.metadata["ymin"], - self.metadata["ymax"], - self.metadata["ystepsize"], - ) - zmin, zmax, zstep = ( - self.metadata["zmin"], - self.metadata["zmax"], - self.metadata["zstepsize"], - ) - Z, Y, X = np.meshgrid( - np.arange(zmin + zstep / 2, zmax, zstep) * 1e9, - np.arange(ymin + ystep / 2, ymax, ystep) * 1e9, - np.arange(xmin + xstep / 2, xmax, xstep) * 1e9, - indexing="ij", - ) - self.data = np.column_stack((X.ravel(), Y.ravel(), Z.ravel(), data)) - - column_headers = ["X (nm)", "Y (nm)", "Z (nm)", "U", "V", "W"] - self.setas = "xyzuvw" - self.column_headers = column_headers - return self + data = np.reshape( + numbers[1:], + (new_data.metadata["xnodes"] * new_data.metadata["ynodes"] * new_data.metadata["znodes"], 3), + ) + else: + data = np.genfromtxt( + data, + max_rows=new_data.metadata["xnodes"] * new_data.metadata["ynodes"] * new_data.metadata["znodes"], + ) + xmin, xmax, xstep = ( + new_data.metadata["xmin"], + new_data.metadata["xmax"], + new_data.metadata["xstepsize"], + ) + ymin, ymax, ystep = ( + new_data.metadata["ymin"], + new_data.metadata["ymax"], + new_data.metadata["ystepsize"], + ) + zmin, zmax, zstep = ( + new_data.metadata["zmin"], + new_data.metadata["zmax"], + new_data.metadata["zstepsize"], + ) + Z, Y, X = np.meshgrid( + np.arange(zmin + zstep / 2, zmax, zstep) * 1e9, + np.arange(ymin + ystep / 2, ymax, ystep) * 1e9, + np.arange(xmin + xstep / 2, xmax, xstep) * 1e9, + indexing="ij", + ) + new_data.data = np.column_stack((X.ravel(), Y.ravel(), Z.ravel(), data)) + + column_headers = ["X (nm)", "Y (nm)", "Z (nm)", "U", "V", "W"] + new_data.setas = "xyzuvw" + new_data.column_headers = column_headers + return new_data diff --git a/Stoner/tools/__init__.py b/Stoner/tools/__init__.py index a8e07ff35..f68c56e8f 100755 --- a/Stoner/tools/__init__.py +++ b/Stoner/tools/__init__.py @@ -19,6 +19,7 @@ "isLikeList", "isproperty", "isTuple", + "isclass", "copy_into", "make_Data", "quantize", @@ -41,7 +42,18 @@ from ..compat import bytes2str from .classes import attributeStore as AttributeStore, typedList, Options, get_option, set_option, copy_into -from .tests import all_size, all_type, isanynone, isComparable, isiterable, isLikeList, isnone, isproperty, isTuple +from .tests import ( + all_size, + all_type, + isanynone, + isComparable, + isiterable, + isLikeList, + isnone, + isproperty, + isTuple, + isclass, +) from .formatting import format_error, format_val, quantize, html_escape, tex_escape, ordinal from . import decorators from .decorators import make_Data, fix_signature diff --git a/Stoner/tools/decorators.py b/Stoner/tools/decorators.py index d0a412af7..a29655a25 100755 --- a/Stoner/tools/decorators.py +++ b/Stoner/tools/decorators.py @@ -415,10 +415,19 @@ def fix_signature(proxy_func, wrapped_func): def make_Data(*args, **kargs): - """Return an instance of Stoner.Data passig through constructor arguments. + """Return an instance of Stoner.Data or Stoner.ImageFile passig through constructor arguments. + + Keyword Arguments: + what (str): Controls whether to makle Data or ImaageFile based on the same what argument to the autoloaders. Calling make_Data(None) is a special case to return the Data class ratther than an instance """ + what = kargs.pop("what", "Data") + match what: + case "Image": + cls = import_module("Stoner.Image.core").ImageFile + case _: + cls = import_module("Stoner.core.data").Data if len(args) == 1 and args[0] is None: - return import_module("Stoner.core.data").Data - return import_module("Stoner.core.data").Data(*args, **kargs) + return cls + return cls(*args, **kargs) diff --git a/Stoner/tools/file.py b/Stoner/tools/file.py index 193d7c289..f7182e415 100755 --- a/Stoner/tools/file.py +++ b/Stoner/tools/file.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- """General file related tools.""" from importlib import import_module +from http.client import HTTPResponse import io import os import pathlib @@ -181,6 +182,9 @@ def auto_load_classes( def get_mime_type(filename: Union[pathlib.Path, str], debug: bool = False) -> Optional[str]: """Get the mime type of the file if filemagic is available.""" + if isinstance(filename, HTTPResponse): + if "Content-Type" in filename.headers: + return filename.headers["Content-Type"].split(";")[0].strip() if ( filemagic is not None and isinstance(filename, path_types) diff --git a/Stoner/tools/tests.py b/Stoner/tools/tests.py index 7690cfd33..34fd4ebcb 100755 --- a/Stoner/tools/tests.py +++ b/Stoner/tools/tests.py @@ -11,6 +11,7 @@ "isnone", "isproperty", "isTuple", + "isclass", ] from typing import Optional, Iterable as IterableType, Tuple, Union, Any @@ -213,3 +214,8 @@ def isTuple(obj: Any, *args: type, strict: bool = True) -> bool: else: bad = False return not bad + + +def isclass(obj, cls): + """Check whether obj is a class and if so whether it is a subclass of cls.""" + return obj is not None and isinstance(obj, type) and issubclass(obj, cls) diff --git a/doc/readme.rst b/doc/readme.rst index 8f58e3b8f..a897f31a2 100755 --- a/doc/readme.rst +++ b/doc/readme.rst @@ -1,4 +1,4 @@ -.. image:: https://github.com/stonerlab/Stoner-PythonCode/actions/workflows/run-tests-action.yaml/badge.svg?branch=stable +.. image:: https://github.com/stonerlab/Stoner-PythonCode/actions/workflows/run-tests-action.yaml/badge.svg?branch=master :target: https://github.com/stonerlab/Stoner-PythonCode/actions/workflows/run-tests-action.yaml .. image:: https://coveralls.io/repos/github/stonerlab/Stoner-PythonCode/badge.svg?branch=master @@ -106,12 +106,12 @@ contrasr, traditional functional programming thinks in terms of various function .. note:: This is rather similar to pandas DataFrames and the package provides methods to easily convert to and from DataFrames. Unlike a DataFrame, a **Stoner.Data** object maintains a dictionary of additional metadata - attached to the dataset (e.g. of instrument settings, experimental ort environmental; conditions + attached to the dataset (e.g. of instrument settings, experimental ort environmental; conditions when thedata was taken). To assist with exporting to pandas DataFrames, the package will add a custom attrobute handler to pandas DataFrames **DataFrame.metadata** to hold this additional data. - + Unlike Pandas, the **Stoner** package's default is to operate in-place and also to return the object - from method calls to facilitate "chaining" of data methods into short single line pipelines. + from method calls to facilitate "chaining" of data methods into short single line pipelines. Data and Friends ---------------- @@ -207,6 +207,8 @@ Version 0.9 was tested with Python 2.7, 3.5, 3.6 using the standard unittest mod Version 0.10 is tested using **pytest** with Python 3.7-3.11 using a github action. +Version 0.11 is tested using **pytest** with Python 3.10 and 3.11 using a github action. + Citing the Stoner Package ~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -214,11 +216,20 @@ Citing the Stoner Package We maintain a digital object identifier (doi) for this package (linked to on the status bar at the top of this readme) and encourage any users to cite this package via that doi. +Development Version +------------------- + +New Features in 0.11 included: + + * Refactored loading and saving routines to reduce the size of the class heirarchy and allow easier creation + of user suplied loaders and savers. + * Dropped support for Python<3.10 to allow use of new syntax features such as match...case + + Stable Versions --------------- - -New Features in 0.10 include: +New Features in 0.10 included: * Support for Python 3.10 and 3.11 * Refactor Stoner.Core.DataFile to move functionality to mixin classes @@ -270,7 +281,7 @@ No further relases will be made to 0.7.x - 0.9.x Versions 0.6.x and earlier are now pre-historic! -.. _online documentation: http://stoner-pythoncode.readthedocs.io/en/stable/ +.. _online documentation: http://stoner-pythoncode.readthedocs.io/en/latest/ .. _github repository: http://www.github.com/stonerlab/Stoner-PythonCode/ .. _Dr Gavin Burnell: http://www.stoner.leeds.ac.uk/people/gb .. _User_Guide: http://stoner-pythoncode.readthedocs.io/en/latest/UserGuide/ugindex.html diff --git a/doc/samples/joy_division_plot.py b/doc/samples/joy_division_plot.py index 65f5aadc4..12857bcea 100755 --- a/doc/samples/joy_division_plot.py +++ b/doc/samples/joy_division_plot.py @@ -6,8 +6,9 @@ from numpy import log10 from Stoner import Data, DataFolder -from Stoner.formats.instruments import RigakuFile +from Stoner.formats.instruments import load_rigaku from Stoner.plot.utils import joy_division +from Stoner.tools import make_Data class RigakuFolder(DataFolder): @@ -22,8 +23,7 @@ def load_files(self, filename): data = data.read() ix = 0 while len(data) > 0: - d = RigakuFile() - d._load(data) + d = load_rigaku(make_Data(), data) d.filename = f"{filename.stem}-{ix}{filename.suffix}" data = data[d.get("endpos", len(data)) :] ix += 1 diff --git a/tests/Stoner/dummy.py b/tests/Stoner/dummy.py index ed27540ea..95f992b2d 100755 --- a/tests/Stoner/dummy.py +++ b/tests/Stoner/dummy.py @@ -1,8 +1,11 @@ # -*- coding: utf-8 -*- """Module to check late importing for DataFile sub ty[es in load""" -from Stoner import Data +from Stoner.formats.generic import load_tdi_format +from Stoner.formats import register_loader -class ArbClass(Data): - pass +@register_loader(name="dummy.ArbClass", what="Data") +def arb_load(*args,**kargs): + """Just a dummy loader routine that passes through to tdi_load.""" + return load_tdi_format(*args, **kargs) diff --git a/tests/Stoner/folders/test_each.py b/tests/Stoner/folders/test_each.py index d5b175ca4..fd9eda32a 100755 --- a/tests/Stoner/folders/test_each.py +++ b/tests/Stoner/folders/test_each.py @@ -33,7 +33,7 @@ def test_each_call(): filenames = [path.relpath(x, start=fldr6.directory) for x in fldr6.each.filename.tolist()] assert filenames == paths, "Reading attributes from each failed." meths = [x for x in dir(fldr6.each) if not x.startswith("_")] - assert len(meths) == 138, "Dir of folders.each failed ({}).".format(len(meths)) + assert len(meths) == 137, "Dir of folders.each failed ({}).".format(len(meths)) def test_each_call_or_operator(): diff --git a/tests/Stoner/mixedmetatest.dat b/tests/Stoner/mixedmetatest.dat new file mode 100755 index 000000000..e59218b71 --- /dev/null +++ b/tests/Stoner/mixedmetatest.dat @@ -0,0 +1,15 @@ +TDI Format 1.5 1 2 3 4 +Stoner.class{String}=Data 0 1 2 3 +t0{Boolean}=True 4 5 6 7 +t1{I32}=1 8 9 10 11 +t10{List}=[1, (1, 2), 'abc'] +t11{List}=[[[1]]] +t12{Void}=None +t2{Double Float}=0.2 +t3{Cluster (I32,String)}={'a': 1, 'b': 'abc'} +t4{Cluster (I32,I32)}=(1, 2) +t5{1D Array (Invalid Type)}=array([0, 1, 2]) +t6{List}=[1, 2, 3] +t7{String}=abc +t8{String}=\\abc\cde +t9{Double Float}=1e-20 diff --git a/tests/Stoner/mixedmetatest2.dat b/tests/Stoner/mixedmetatest2.dat new file mode 100755 index 000000000..03e82f0ca --- /dev/null +++ b/tests/Stoner/mixedmetatest2.dat @@ -0,0 +1,1157 @@ +TDI Format 1.5 Voltage Current $\rho$ ($\Omega$m) Resistance T (K) VTI Temp Elapsed Time Sample Temp +2nd Parameter Class{String}=Counter 4.29526e-08 0.0 5.5617224e-08 2.07012 284.175 287.6 7.107121 284.175 +2nd Parameter Class Path{String}=..\\..\\..\\..\\..\\..\\.. 3.2571e-09 0.0 5.56154776667e-08 2.070055 284.149 287.8 12.18141 284.149 +Analysis Class{String}=Basic Plot 6.1824e-09 0.0 5.56134089333e-08 2.069978 284.194 288.0 17.1857 284.194 +Analysis Class Path{String}=plugins\\Plotting\\Plotting_Base_class\\Plotting_Base.lvclass 8.22448e-08 0.0 5.56241556e-08 2.070378 284.232 288.1 22.19398 284.232 +Analysis Classes{List}=['Read Temperature', 'Read Temperature'] 6.70626e-08 0.0 5.56292065333e-08 2.070566 284.23 288.1 27.20627 284.23 +Control:Magnet Output{Double Float}=-100000000.0 1.150303e-07 0.0 5.56345261333e-08 2.070764 284.251 287.9 32.21956 284.251 +Current{Double Float}=0.0 1.232516e-07 0.0 5.56290453333e-08 2.07056 284.252 287.5 37.22784 284.252 +Display Panel{String}= 9.80828e-08 0.0 5.56369441333e-08 2.070854 284.257 287.0 42.23413 284.257 +Elapsed Time{Double Float}=10905.61648 8.95585e-08 0.0 5.56357888667e-08 2.070811 284.242 286.4 47.24642 284.242 +File:Autodel{Boolean}=True 8.49687e-08 0.0 5.56414577333e-08 2.071022 284.232 285.8 52.2487 284.232 +File:Autosave{Boolean}=True 8.87011e-08 0.0 5.56409741333e-08 2.071004 284.262 285.2 57.25499 284.262 +File:Export Config{Boolean}=True 8.75411e-08 0.0 5.56423174667e-08 2.071054 284.225 284.6 62.26628 284.225 +File:Final Autosave{Boolean}=True 1.105413e-07 0.0 5.56407860667e-08 2.070997 284.243 284.0 67.26756 284.243 +Filter:Enable{Void}=None 1.216884e-07 0.0 5.56363262e-08 2.070831 284.218 283.4 72.27985 284.218 +Filter:Points{I32}=10 1.366182e-07 0.0 5.56358694667e-08 2.070814 284.182 282.8 77.28013 284.182 +Filter:Type{I32}=1 1.46721e-07 0.0 5.56347142e-08 2.070771 284.122 282.2 82.29142 284.122 +Filter:Window{Double Float}=1.0 1.570966e-07 0.0 5.56320275333e-08 2.070671 284.083 281.6 87.29471 284.083 +Iterator{I32}=0 1.767678e-07 0.0 5.56236451333e-08 2.070359 284.062 281.0 92.30899 284.062 +Loaded as{String}=DataFile 1.862001e-07 0.0 5.56225973333e-08 2.07032 284.007 280.4 97.31128 284.007 +Magnet: Power Supply{String}=Hg iPS 1.909918e-07 0.0 5.5614806e-08 2.07003 283.958 279.8 102.3196 283.958 +Magnet: Rate{Double Float}=1.5 2.207505e-07 0.0 5.56113670667e-08 2.069902 283.904 279.2 107.3269 283.904 +Magnet: Units{String}=Field 2.313428e-07 0.0 5.56091102667e-08 2.069818 283.846 278.7 112.3331 283.846 +Magnet: VISA Ref{Void}=None 2.278625e-07 0.0 5.55981486667e-08 2.06941 283.791 278.1 117.3424 283.791 +Measurement Timestamp{Void}=None 2.548981e-07 0.0 5.55936350667e-08 2.069242 283.72 277.5 122.3557 283.72 +Measurement Title{String}=TDI Format 1.5 2.752249e-07 0.0 5.5584581e-08 2.068905 283.651 276.9 127.358 283.651 +Measurment Select{I32}=6 2.818326e-07 0.0 5.55750970667e-08 2.068552 283.589 276.4 132.3693 283.589 +Notes{String}=IV of Cu Space at RT 3.020582e-07 0.0 5.55670102e-08 2.068251 283.524 275.8 137.3816 283.524 +Option: 2182 Autozero{Boolean}=True 2.986288e-07 0.0 5.55599174e-08 2.067987 283.434 275.3 142.3859 283.434 +Option: 2182 Front Autozero{Boolean}=True 3.128017e-07 0.0 5.55522335333e-08 2.067701 283.349 274.7 147.3951 283.349 +Option: 2182 Hold{Boolean}=True 3.33583e-07 0.0 5.55444422e-08 2.067411 283.266 274.2 152.4004 283.266 +Option: 2182 Hold Count{I32}=10 3.353987e-07 0.0 5.55306058667e-08 2.066896 283.18 273.6 157.4077 283.18 +Option: 2182 Hold Window{Double Float}=1.0 3.433677e-07 0.0 5.55276774e-08 2.066787 283.088 273.1 162.411 283.088 +Option: 2182 Line Sync{Boolean}=True 3.609204e-07 0.0 5.551325e-08 2.06625 282.999 272.6 167.4113 282.999 +Option: 2182 Reference Reading{Double Float}=nan 3.93756e-07 0.0 5.55047064e-08 2.065932 282.904 272.1 172.4236 282.904 +Option: 2182 Relative Mode{Boolean}=True 4.004143e-07 0.0 5.54942284e-08 2.065542 282.815 271.5 177.4289 282.815 +Option: 2182 Trigger Delay{Cluster (Boolean,Double Float)}={'Auto': True, 'Time(s)': 0.0} 4.077784e-07 0.0 5.54836160667e-08 2.065147 282.716 270.9 182.4421 282.716 +Option: 2182 Trigger Source{String}=Immediate 4.213969e-07 0.0 5.54727888e-08 2.064744 282.605 270.4 187.4504 282.605 +Option: 2182 Trigger Timer{Double Float}=0.0 4.248184e-07 0.0 5.54593017333e-08 2.064242 282.517 269.9 192.4537 282.517 +Option: 560 Coupling{String}=ground 4.451542e-07 0.0 5.54485013333e-08 2.06384 282.389 269.4 197.459 282.389 +Option: 560 Enabled{Boolean}=True 4.491887e-07 0.0 5.54389099333e-08 2.063483 282.288 268.9 202.4633 282.288 +Option: 560 Gain{I32}=1 4.554932e-07 0.0 5.54273304e-08 2.063052 282.188 268.3 207.4716 282.188 +Option: 560 Input Source{String}=A 4.737024e-07 0.0 5.54127955333e-08 2.062511 282.082 267.8 212.4759 282.082 +Option: 6221 Adjust Mode{String}=Offset 4.688604e-07 0.0 5.54023175333e-08 2.062121 281.948 267.3 217.4892 281.948 +Option: 6221 Amplitude{Double Float}=0.0 4.961472e-07 0.0 5.53879438667e-08 2.061586 281.828 266.8 222.4974 281.828 +Option: 6221 Compliance{Double Float}=2.0 4.998792e-07 0.0 5.53763374667e-08 2.061154 281.708 266.3 227.5077 281.708 +Option: 6221 Duty Cycle{Double Float}=50.0 5.177852e-07 0.0 5.53638713333e-08 2.06069 281.586 265.8 232.51 281.586 +Option: 6221 External Trigger{Boolean}=True 5.175331e-07 0.0 5.53476976e-08 2.060088 281.455 265.3 237.5103 281.455 +Option: 6221 External Trigger Ignore Multiple{Boolean}=True 5.375572e-07 0.0 5.53360643333e-08 2.059655 281.331 264.8 242.5186 281.331 +Option: 6221 Frequency{Double Float}=1023.0 5.49411e-07 0.0 5.53205085333e-08 2.059076 281.198 264.3 247.5269 281.198 +Option: 6221 Function{I32}=1 5.425004e-07 0.0 5.53079080667e-08 2.058607 281.073 263.8 252.5292 281.073 +Option: 6221 Idle Level{Double Float}=0.0 5.736207e-07 0.0 5.52925134667e-08 2.058034 280.93 263.3 257.5374 280.93 +Option: 6221 Input trigger Line{I32}=5 5.785642e-07 0.0 5.52844803333e-08 2.057735 280.811 262.8 262.5407 280.811 +Option: 6221 Offset{Double Float}=0.0 5.81893e-07 0.0 5.52649214e-08 2.057007 280.662 262.4 267.544 280.662 +Option: 6221 Output Trigger Line{I32}=4 5.838604e-07 0.0 5.52504402667e-08 2.056468 280.513 261.9 272.5573 280.513 +Option: 6221 Pass Through{Boolean}=True 5.86989e-07 0.0 5.52317948e-08 2.055774 280.403 261.4 277.5666 280.403 +Option: 6221 Phase Marker{Boolean}=True 6.046922e-07 0.0 5.52197854e-08 2.055327 280.277 260.9 282.5699 280.277 +Option: 6221 Phase Marker Phase{Double Float}=0.0 6.146283e-07 0.0 5.5204149e-08 2.054745 280.158 260.4 287.5732 280.158 +Option: 6221 Range Level{Double Float}=0.0003 6.202769e-07 0.0 5.51878678e-08 2.054139 280.031 259.9 292.5734 280.031 +Option: 6221 Ranging{I32}=1 6.235553e-07 0.0 5.51715866e-08 2.053533 279.895 259.4 297.5737 279.895 +Option: 6221 Update Clock{Double Float}=0.0 6.368212e-07 0.0 5.51561651333e-08 2.052959 279.771 259.0 302.579 279.771 +Option: 6221 VISA{Void}=None 6.377792e-07 0.0 5.51392122667e-08 2.052328 279.629 258.5 307.5813 279.629 +Option: AI Global{String}= 6.398467e-07 0.0 5.5125564e-08 2.05182 279.516 258.0 312.5916 279.516 +Option: AI Global 2{String}= 6.523049e-07 0.0 5.5108235e-08 2.051175 279.376 257.6 317.5959 279.376 +Option: AI Global 3{String}= 6.674375e-07 0.0 5.50904492667e-08 2.050513 279.243 257.1 322.6062 279.243 +Option: AI Global 4{String}= 6.613841e-07 0.0 5.50750278e-08 2.049939 279.098 256.6 327.6105 279.098 +Option: AI In{Void}=None 6.569956e-07 0.0 5.5057457e-08 2.049285 278.938 256.2 332.6157 278.938 +Option: AI In 2{String}= 6.641078e-07 0.0 5.5040934e-08 2.04867 278.78 255.7 337.619 278.78 +Option: AI In 3{String}= 6.614348e-07 0.0 5.50258080667e-08 2.048107 278.678 255.3 342.6323 278.678 +Option: AI In 4{String}= 6.807527e-07 0.0 5.50064640667e-08 2.047387 278.547 254.8 347.6386 278.547 +Option: AI In Scale{String}= 6.938669e-07 0.0 5.49941591333e-08 2.046929 278.399 254.4 352.6479 278.399 +Option: AI In Scale 2{String}= 6.93766e-07 0.0 5.49700597333e-08 2.046032 278.252 253.9 357.6482 278.252 +Option: AI In Scale 3{String}= 6.851403e-07 0.0 5.49514948667e-08 2.045341 278.108 253.5 362.6515 278.108 +Option: AI In Scale 4{String}= 6.971449e-07 0.0 5.49364764e-08 2.044782 277.958 253.0 367.6627 277.958 +Option: Abort on Compliance{Boolean}=True 7.091488e-07 0.0 5.49210012e-08 2.044206 277.817 252.5 372.674 277.817 +Option: Confirm Config{Boolean}=True 7.084944e-07 0.0 5.48989436667e-08 2.043385 277.661 252.1 377.6753 277.661 +Option: DAQ Global Channel{Boolean}=True 7.254406e-07 0.0 5.488457e-08 2.04285 277.514 251.7 382.6836 277.514 +Option: DAQ Input Rate{Double Float}=200000.0 7.252387e-07 0.0 5.48622438e-08 2.042019 277.36 251.2 387.6909 277.36 +Option: DAQ Input Samples{I32}=20000 7.380005e-07 0.0 5.48459626e-08 2.041413 277.215 250.8 392.6982 277.215 +Option: DCond Delta{Double Float}=5e-06 7.284667e-07 0.0 5.48245767333e-08 2.040617 277.063 250.4 397.6985 277.063 +Option: Differential{I32}=10106 7.446585e-07 0.0 5.48095851333e-08 2.040059 276.893 249.9 402.7057 276.893 +Option: Differential Conductance Mode{Boolean}=True 7.460193e-07 0.0 5.47863992e-08 2.039196 276.74 249.5 407.707 276.74 +Option: Lockin Standoff Time{Double Float}=0.0 7.45162e-07 0.0 5.47679955333e-08 2.038511 276.588 249.0 412.7093 276.588 +Option: Max Raw Aux 1 Signal{Double Float}=10.0 7.612019e-07 0.0 5.4749968e-08 2.03784 276.42 248.6 417.7096 276.42 +Option: Max Raw Aux 2 Signal{Double Float}=10.0 7.688183e-07 0.0 5.47305434e-08 2.037117 276.262 248.2 422.7229 276.262 +Option: Max Raw Current Signal{Double Float}=10.0 7.706342e-07 0.0 5.47098829333e-08 2.036348 276.099 247.8 427.7282 276.099 +Option: Max Raw voltage signal{Double Float}=0.1 7.677595e-07 0.0 5.46940047333e-08 2.035757 275.945 247.4 432.7335 275.945 +Option: SR830 Ref Phase{Double Float}=0.0 7.751222e-07 0.0 5.46706844667e-08 2.034889 275.765 246.9 437.7388 275.765 +Option: SR830 Synch Filter{Boolean}=True 7.752736e-07 0.0 5.4655666e-08 2.03433 275.613 246.5 442.744 275.613 +Option: SR830 Time Constant{I32}=8 7.940369e-07 0.0 5.4629471e-08 2.033355 275.44 246.1 447.7443 275.44 +Option: SR830 2x Line Filter{Boolean}=True 7.795604e-07 0.0 5.46142107333e-08 2.032787 275.27 245.7 452.7466 275.27 +Option: SR830 Autophase{Boolean}=True 8.024596e-07 0.0 5.45913740667e-08 2.031937 275.098 245.3 457.7469 275.098 +Option: SR830 DC Coupling?{Boolean}=True 8.1053e-07 0.0 5.45680538e-08 2.031069 274.927 244.9 462.7502 274.927 +Option: SR830 Display 1{I32}=0 8.182983e-07 0.0 5.45495695333e-08 2.030381 274.763 244.4 467.7535 274.763 +Option: SR830 Display 2{I32}=0 7.981567e-07 0.0 5.45275388667e-08 2.029561 274.592 244.1 472.7578 274.592 +Option: SR830 Dynamic Reserve{I32}=-1 8.105294e-07 0.0 5.45082486e-08 2.028843 274.418 243.6 477.77 274.418 +Option: SR830 Expand 1{Void}=None 8.117908e-07 0.0 5.44857343333e-08 2.028005 274.239 243.2 482.7723 274.239 +Option: SR830 Expand 2{Void}=None 8.116905e-07 0.0 5.44688352e-08 2.027376 274.059 242.8 487.7806 274.059 +Option: SR830 Filter Slope{I32}=1 8.222822e-07 0.0 5.44469657333e-08 2.026562 273.901 242.4 492.7889 273.901 +Option: SR830 Gain{I32}=-1 8.141602e-07 0.0 5.44237529333e-08 2.025698 273.717 242.0 497.7932 273.717 +Option: SR830 Ground Shield?{Boolean}=True 8.391276e-07 0.0 5.44025551333e-08 2.024909 273.549 241.7 502.7985 273.549 +Option: SR830 Harmonic{I32}=1 8.237446e-07 0.0 5.43781602e-08 2.024001 273.374 241.2 507.8018 273.374 +Option: SR830 Input{String}=A 8.291406e-07 0.0 5.43606968667e-08 2.023351 273.188 240.9 512.813 273.188 +Option: SR830 Line Filter{Boolean}=True 8.408933e-07 0.0 5.43386930667e-08 2.022532 273.008 240.5 517.8173 273.008 +Option: SR830 No Reset{Boolean}=True 8.38422e-07 0.0 5.43152653333e-08 2.02166 272.828 240.1 522.8206 272.828 +Option: SR830 Offset 1{Double Float}=0.0 8.439191e-07 0.0 5.42959750667e-08 2.020942 272.643 239.7 527.8229 272.643 +Option: SR830 Offset 2{Double Float}=0.0 8.554192e-07 0.0 5.42750190667e-08 2.020162 272.446 239.3 532.8232 272.446 +Option: SR830 Output Channel 1{Boolean}=True 8.546121e-07 0.0 5.42536869333e-08 2.019368 272.267 238.9 537.8235 272.267 +Option: SR830 Output Channel 2{Boolean}=True 8.524938e-07 0.0 5.42339130667e-08 2.018632 272.088 238.5 542.8318 272.088 +Option: SR830 Ratio 1{I32}=0 8.53199e-07 0.0 5.42111032667e-08 2.017783 271.893 238.1 547.84 271.893 +Option: SR830 Ratio 2{I32}=0 8.697433e-07 0.0 5.41868158e-08 2.016879 271.715 237.7 552.8473 271.715 +Option: SR830 Ref Amplitude{Double Float}=0.1 8.650022e-07 0.0 5.41648388667e-08 2.016061 271.534 237.4 557.8526 271.534 +Option: SR830 Ref Frequency{Double Float}=1013.0 8.677203e-07 0.0 5.41458172667e-08 2.015353 271.343 237.0 562.8619 271.343 +Option: SR830 Ref Source{Boolean}=True 8.628329e-07 0.0 5.4120482e-08 2.01441 271.158 236.6 567.8672 271.158 +Option: SR830 Requested Parameters{String}= 8.500219e-07 0.0 5.40929436667e-08 2.013385 270.969 236.2 572.8755 270.969 +Option: SR830 Trigger Edge{String}=Zero Crossing 8.794782e-07 0.0 5.40764475333e-08 2.012771 270.792 235.9 577.8878 270.792 +Option: SR830 VISA{String}= 8.874966e-07 0.0 5.40489898e-08 2.011749 270.594 235.5 582.8941 270.594 +Option: Sense Autorange{Boolean}=True 8.902711e-07 0.0 5.40286248667e-08 2.010991 270.411 235.1 587.8953 270.411 +Option: Sense Range Level{Double Float}=0.01 8.764509e-07 0.0 5.40032358667e-08 2.010046 270.21 234.7 592.8966 270.21 +Option: Sense Rate{Double Float}=1.0 8.854788e-07 0.0 5.39814738667e-08 2.009236 270.028 234.4 597.8979 270.028 +Option: Source Range Level{Double Float}=0.0 8.899679e-07 0.0 5.39600074e-08 2.008437 269.83 234.0 602.9042 269.83 +Option: Source Ranging{String}=Best Fixed Range 8.834613e-07 0.0 5.3937923e-08 2.007615 269.629 233.6 607.9045 269.629 +Option:560 Comport{String}=COM1 8.772568e-07 0.0 5.39144146667e-08 2.00674 269.443 233.2 612.9068 269.443 +Option:560 Filter High Pass{I32}=0 8.96778e-07 0.0 5.38914436667e-08 2.005885 269.264 232.9 617.9131 269.264 +Option:560 Filter Lowpass{I32}=0 9.026275e-07 0.0 5.3868204e-08 2.00502 269.066 232.5 622.9203 269.066 +Option:560 Filter Mode{I32}=0 9.021735e-07 0.0 5.38435941333e-08 2.004104 268.88 232.2 627.9206 268.88 +Option:560 Invert{Boolean}=True 9.124637e-07 0.0 5.38219664667e-08 2.003299 268.691 231.8 632.9229 268.691 +Option:560 dynamic reserve{String}=calibration gains 9.065614e-07 0.0 5.37962013333e-08 2.00234 268.504 231.4 637.9322 268.504 +Option:570 Enabled{Boolean}=True 9.315685e-07 0.0 5.37744662e-08 2.001531 268.298 231.0 642.9375 268.298 +Option:570 Filter High Pass{I32}=0 9.097887e-07 0.0 5.37531340667e-08 2.000737 268.097 230.7 647.9428 268.097 +Option:570 Filter Lowpass{I32}=0 9.17608e-07 0.0 5.37252464667e-08 1.999699 267.913 230.3 652.9491 267.913 +Option:570 Filter Mode{I32}=0 9.159431e-07 0.0 5.37084816667e-08 1.999075 267.727 230.0 657.9583 267.727 +Option:570 Gain Mode{I32}=0 9.194231e-07 0.0 5.36796806e-08 1.998003 267.523 229.6 662.9616 267.523 +Option:570 Input Offset{I32}=0 9.221472e-07 0.0 5.3659047e-08 1.997235 267.331 229.3 667.9679 267.331 +Option:570 Input Offset On{Boolean}=True 9.117054e-07 0.0 5.36322878e-08 1.996239 267.148 228.9 672.9692 267.148 +Option:570 Sensitivity{I32}=0 9.363203e-07 0.0 5.36060122e-08 1.995261 266.947 228.6 677.9725 266.947 +Option:6221 Calc Compliance{Boolean}=True 9.049465e-07 0.0 5.35860502667e-08 1.994518 266.755 228.2 682.9798 266.755 +Option:Compliance{Double Float}=5000.0 9.26282e-07 0.0 5.35606344e-08 1.993572 266.555 227.9 687.9911 266.555 +Option:Debug On{Boolean}=True 9.183127e-07 0.0 5.35391142e-08 1.992771 266.357 227.5 692.9994 266.357 +Option:GPIB{Void}=None 9.325359e-07 0.0 5.35121132e-08 1.991766 266.157 227.2 698.0056 266.157 +Option:GPIB 2{String}= 9.246679e-07 0.0 5.34879063333e-08 1.990865 265.957 226.8 703.0159 265.957 +Option:GPIB 2182{String}= 9.331424e-07 0.0 5.34638338e-08 1.989969 265.77 226.5 708.0192 265.77 +Option:Parallel Config Voltage Measurers{Boolean}=True 9.164459e-07 0.0 5.34430121333e-08 1.989194 265.567 226.1 713.0325 265.567 +Option:SR570 Comport{String}=COM1 9.317803e-07 0.0 5.34168708667e-08 1.988221 265.373 225.8 718.0348 265.373 +Option:SR830 Divide by 6221 Ampl{Boolean}=True 9.132184e-07 0.0 5.33912400667e-08 1.987267 265.171 225.5 723.0431 265.171 +Option:SR830 Settle Time{Double Float}=5.0 9.41414e-07 0.0 5.3368887e-08 1.986435 264.964 225.1 728.0454 264.964 +Option:Sense Digits{I32}=8 9.532149e-07 0.0 5.33395754667e-08 1.985344 264.759 224.8 733.0526 264.759 +Option:Source Delay{Double Float}=0.001 9.14428e-07 0.0 5.33191030667e-08 1.984582 264.56 224.5 738.0619 264.56 +Output Names{List}=['Voltage', 'Current', 'Resistance'] 9.399496e-07 0.0 5.32932573333e-08 1.98362 264.367 224.1 743.0742 264.367 +Parameter Panel{String}= 9.332913e-07 0.0 5.32692654e-08 1.982727 264.161 223.8 748.0785 264.161 +Parameter Select{I32}=0 9.266328e-07 0.0 5.32436077333e-08 1.981772 263.952 223.4 753.0838 263.952 +Paths{String}=Save 9.344513e-07 0.0 5.32170097333e-08 1.980782 263.752 223.1 758.0891 263.752 +Plotting Class{String}=IV-plot 9.267335e-07 0.0 5.31928834667e-08 1.979884 263.552 222.8 763.0944 263.552 +Plotting Class Path{String}=..\\..\\..\\..\\..\\..\\.. 9.247166e-07 0.0 5.316779e-08 1.97895 263.35 222.5 768.0986 263.35 +Plotting Select{I32}=0 9.190159e-07 0.0 5.31432876e-08 1.978038 263.145 222.1 773.0989 263.145 +Project Code{String}=Spincurrents 9.281957e-07 0.0 5.31189195333e-08 1.977131 262.955 221.8 778.1102 262.955 +Ref: Temperature Window{String}= 9.34097e-07 0.0 5.30933962e-08 1.976181 262.754 221.5 783.1125 262.754 +Ref:Control Subpanel{String}= 9.406041e-07 0.0 5.30671743333e-08 1.975205 262.548 221.1 788.1178 262.548 +Ref:Main Screen{String}= 9.177542e-07 0.0 5.3044069e-08 1.974345 262.342 220.8 793.1231 262.342 +Ref:Plotting Subpanel{String}= 9.335919e-07 0.0 5.30156709333e-08 1.973288 262.137 220.5 798.1334 262.137 +Ref:Toolbar{String}= 9.434277e-07 0.0 5.29930492e-08 1.972446 261.947 220.2 803.1447 261.947 +Ref:XY Subpanel{String}= 9.08322e-07 0.0 5.29712872e-08 1.971636 261.73 219.9 808.1529 261.73 +Resistance{Double Float}=inf 9.316247e-07 0.0 5.29419219333e-08 1.970543 261.529 219.5 813.1562 261.529 +Sample ID{String}=SC004_5_B 9.344998e-07 0.0 5.29174195333e-08 1.969631 261.322 219.2 818.1695 261.322 +Sample Temp{Double Float}=298.563 9.387876e-07 0.0 5.28868721333e-08 1.968494 261.113 218.9 823.1808 261.113 +Save:Filename{String}=${Sample ID}_${X-Y Class}_${2nd Parameter Class}_${iterator}_${Sample Temp}{%.0d}K_CuSpacer_IV_100uA_.txt 9.341564e-07 0.0 5.28645728e-08 1.967664 260.915 218.6 828.1811 260.915 +Save:Final Filename{String}=${Sample ID}_${X-Y Class}_${2nd Parameter Class}_${iterator}_RvT_CuSpacer_100uA_.txt 9.412659e-07 0.0 5.2839748e-08 1.96674 260.707 218.3 833.1914 260.707 +Save:Final path{String}=C:\\Data\\Spincurrents\\py07jtb\\SC004_7_B 9.544736e-07 0.0 5.28140903333e-08 1.965785 260.508 218.0 838.1937 260.508 +Save:Path{String}=C:\\Data\\Spincurrents\\py07jtb\\SC005_5_B 9.571963e-07 0.0 5.27878147333e-08 1.964807 260.298 217.7 843.2029 260.298 +Startup path{String}=C:\\Program Files (x86)\\National Instruments\\LabVIEW 2012\\Stonerlab\\Programs\\Source Meter IV 9.49378e-07 0.0 5.27667512667e-08 1.964023 260.087 217.4 848.2062 260.087 +Stoner.class{String}=Data 9.590122e-07 0.0 5.27409324e-08 1.963062 259.882 217.1 853.2135 259.882 +Sub Panel{String}= 9.535136e-07 0.0 5.27136627333e-08 1.962047 259.671 216.8 858.2258 259.671 +TDI Format{Double Float}=1.5 9.487218e-07 0.0 5.26919276e-08 1.961238 259.464 216.5 863.2331 259.464 +Timestamp{Void}=None 9.598187e-07 0.0 5.26608966e-08 1.960083 259.248 216.2 868.2434 259.248 +Toolbar{String}= 9.540186e-07 0.0 5.26362598667e-08 1.959166 259.035 215.8 873.2437 259.035 +User{String}=py07jtb 9.581033e-07 0.0 5.26129396e-08 1.958298 258.829 215.5 878.2519 258.829 +VTI Temp{Double Float}=290.0 9.583546e-07 0.0 5.25862341333e-08 1.957304 258.622 215.2 883.2592 258.622 +Voltage{Double Float}=0.0 9.547244e-07 0.0 5.25581584667e-08 1.956259 258.413 214.9 888.2645 258.413 +X-Y Class{String}=6221-2182 DC IV 9.588091e-07 0.0 5.25316410667e-08 1.955272 258.196 214.6 893.2728 258.196 +X-Y Class Path{String}=..\\..\\..\\..\\..\\..\\.. 9.416099e-07 0.0 5.25083208e-08 1.954404 257.994 214.3 898.2861 257.994 +iterate result{I32}=4 9.619862e-07 0.0 5.24831736e-08 1.953468 257.788 214.0 903.2934 257.788 +iterator{I32}=0 9.426175e-07 0.0 5.24578652e-08 1.952526 257.578 213.7 908.3047 257.578 +measure point{Boolean}=True 9.600689e-07 0.0 5.24300850667e-08 1.951492 257.371 213.4 913.317 257.371 +parameter: delta time{Double Float}=10.0 9.526547e-07 0.0 5.24041856e-08 1.950528 257.168 213.1 918.3282 257.168 +parameter: progress{Double Float}=58.343344 9.411548e-07 0.0 5.23800593333e-08 1.94963 256.967 212.8 923.3375 256.967 +temperature: rate window{I32}=30 9.638525e-07 0.0 5.23560942667e-08 1.948738 256.752 212.5 928.3428 256.752 +temperature: Level Meter VISA Ref{Void}=None 9.709125e-07 0.0 5.23300067333e-08 1.947767 256.549 212.2 933.3541 256.549 +temperature: Offset Compensation Suppression{Boolean}=True 9.581525e-07 0.0 5.22987070667e-08 1.946602 256.327 212.0 938.3584 256.327 +temperature: VISA Ref{Void}=None 9.606226e-07 0.0 5.22735061333e-08 1.945664 256.12 211.7 943.3587 256.12 +temperature: VISA Ref 2{Void}=None 9.472097e-07 0.0 5.22484664e-08 1.944732 255.91 211.4 948.366 255.91 +temperature: channels{String}= 9.53309e-07 0.0 5.22234266667e-08 1.9438 255.697 211.1 953.3742 255.697 +temperature: current temperature{Double Float}=152.67 9.581016e-07 0.0 5.21948136667e-08 1.942735 255.496 210.8 958.3795 255.496 +temperature: gas control{Boolean}=True 9.565885e-07 0.0 5.21710098e-08 1.941849 255.276 210.5 963.3858 255.276 +temperature: gast flow{I32}=21 9.628424e-07 0.0 5.21413758667e-08 1.940746 255.068 210.2 968.3971 255.068 +temperature: heater output{I32}=56 9.731817e-07 0.0 5.21189153333e-08 1.93991 254.849 209.9 973.4014 254.849 +temperature: pid controls{Cluster (Double Float,Double Float,Double Float)}={'I': 0.7, 'P': 7.0, 'D': 0.0} 9.744919e-07 0.0 5.20922904667e-08 1.938919 254.639 209.6 978.4057 254.639 +temperature: read sensor{String}=A 9.5598e-07 0.0 5.20674119333e-08 1.937993 254.427 209.3 983.408 254.427 +temperature: setpoint{Double Float}=175.0 9.743915e-07 0.0 5.20370794667e-08 1.936864 254.209 209.1 988.4162 254.209 +temperature: stability error{Double Float}=0.03 9.589061e-07 0.0 5.2016016e-08 1.93608 254.0 208.8 993.4205 254.0 +temperature: temp control mode{I32}=2 9.651616e-07 0.0 5.19851999333e-08 1.934933 253.788 208.5 998.4238 253.788 +temperature: temp control sensor{String}=A 9.557292e-07 0.0 5.19626319333e-08 1.934093 253.573 208.2 1003.437 253.573 +temperature: temp control units{String}=Kelvin 9.622854e-07 0.0 5.19360070667e-08 1.933102 253.34 207.9 1008.45 253.34 +temperature: temp controller{String}=Oxford ITC 503 9.643521e-07 0.0 5.19077433333e-08 1.93205 253.117 207.6 1013.455 253.117 +temperature: temp controller 2{String}=Lakeshore 340 9.571402e-07 0.0 5.18827573333e-08 1.93112 252.901 207.3 1018.456 252.901 +temperature: temp loop{String}=Loop 1 9.530044e-07 0.0 5.18563474e-08 1.930137 252.698 207.1 1023.456 252.698 +temperature: temp read units{String}=Kelvin 9.59553e-07 0.0 5.18344510667e-08 1.929322 252.49 206.8 1028.465 252.49 +temperature: values{List}=[175.0, 169.86, 152.67, 169.86, nan, 99.9, 24.0, 1.795315, 169.86, 5.14] 9.623859e-07 0.0 5.18034200667e-08 1.928167 252.284 206.5 1033.469 252.284 +temperature:Remote/Local Mode{String}=Local 9.638993e-07 0.0 5.17813356667e-08 1.927345 252.067 206.2 1038.48 252.067 +temperature:heater range{I32}=5 9.752984e-07 0.0 5.17503584e-08 1.926192 251.852 205.9 1043.493 251.852 +temperature:is stable?{Boolean}=True 9.447305e-07 0.0 5.17257485333e-08 1.925276 251.646 205.7 1048.495 251.646 +temperature:monitor{Boolean}=True 9.48312e-07 0.0 5.16996878667e-08 1.924306 251.423 205.4 1053.503 251.423 +temperature:parameter{String}=VTI Temp 9.519946e-07 0.0 5.16724450667e-08 1.923292 251.213 205.1 1058.507 251.213 +temperature:ramping{Boolean}=True 9.761542e-07 0.0 5.1648077e-08 1.922385 251.006 204.8 1063.516 251.006 +temperature:rate select{String}=Radio Selection 2 9.595091e-07 0.0 5.1621479e-08 1.921395 250.789 204.6 1068.521 250.789 +temperature:stability rate{Double Float}=0.05 9.603651e-07 0.0 5.15966004667e-08 1.920469 250.581 204.3 1073.524 250.581 +y{Double Float}=1.0 9.620811e-07 0.0 5.15713995333e-08 1.919531 250.355 204.0 1078.527 250.355 + 9.590032e-07 0.0 5.15409327333e-08 1.918397 250.151 203.8 1083.535 250.151 + 9.743374e-07 0.0 5.15182572667e-08 1.917553 249.929 203.5 1088.546 249.929 + 9.477556e-07 0.0 5.14885964667e-08 1.916449 249.725 203.2 1093.556 249.725 + 9.494191e-07 0.0 5.14620522e-08 1.915461 249.51 203.0 1098.565 249.51 + 9.602139e-07 0.0 5.14350512e-08 1.914456 249.288 202.7 1103.568 249.288 + 9.533546e-07 0.0 5.14098234e-08 1.913517 249.08 202.4 1108.572 249.08 + 9.71209e-07 0.0 5.13826612e-08 1.912506 248.866 202.2 1113.578 248.866 + 9.501264e-07 0.0 5.13549885333e-08 1.911476 248.652 201.9 1118.585 248.652 + 9.471977e-07 0.0 5.13286323333e-08 1.910495 248.432 201.6 1123.596 248.432 + 9.73226e-07 0.0 5.13035657333e-08 1.909562 248.221 201.4 1128.596 248.221 + 9.396336e-07 0.0 5.12760811333e-08 1.908539 247.996 201.1 1133.607 247.996 + 9.637428e-07 0.0 5.12467696e-08 1.907448 247.786 200.8 1138.613 247.786 + 9.556228e-07 0.0 5.12226164667e-08 1.906549 247.572 200.6 1143.618 247.572 + 9.506293e-07 0.0 5.11992156e-08 1.905678 247.358 200.3 1148.624 247.358 + 9.602622e-07 0.0 5.11693936e-08 1.904568 247.148 200.1 1153.635 247.148 + 9.437689e-07 0.0 5.11469330667e-08 1.903732 246.937 199.81 1158.641 246.937 + 9.543104e-07 0.0 5.1121356e-08 1.90278 246.712 199.57 1163.645 246.712 + 9.436163e-07 0.0 5.10888204667e-08 1.901569 246.51 199.31 1168.646 246.51 + 9.438185e-07 0.0 5.10650166e-08 1.900683 246.293 199.06 1173.657 246.293 + 9.385215e-07 0.0 5.10374782667e-08 1.899658 246.077 198.81 1178.659 246.077 + 9.359494e-07 0.0 5.10126266e-08 1.898733 245.858 198.56 1183.667 245.858 + 9.405898e-07 0.0 5.09868346e-08 1.897773 245.645 198.29 1188.677 245.645 + 9.492649e-07 0.0 5.09595380667e-08 1.896757 245.435 198.06 1193.684 245.435 + 9.395811e-07 0.0 5.09326176667e-08 1.895755 245.218 197.8 1198.685 245.218 + 9.473987e-07 0.0 5.09063958e-08 1.894779 245.003 197.54 1203.688 245.003 + 9.464799e-07 0.0 5.08791261333e-08 1.893764 244.784 197.3 1208.697 244.784 + 9.420545e-07 0.0 5.08536296667e-08 1.892815 244.572 197.04 1213.707 244.572 + 9.543088e-07 0.0 5.08257958e-08 1.891779 244.355 196.8 1218.711 244.355 + 9.412449e-07 0.0 5.08029322667e-08 1.890928 244.132 196.54 1223.716 244.132 + 9.47348e-07 0.0 5.07731908667e-08 1.889821 243.923 196.31 1228.722 243.923 + 9.628321e-07 0.0 5.07511333333e-08 1.889 243.697 196.06 1233.729 243.697 + 9.700953e-07 0.0 5.07188933333e-08 1.8878 243.489 195.82 1238.733 243.489 + 9.633367e-07 0.0 5.06974268667e-08 1.887001 243.269 195.57 1243.737 243.269 + 9.530458e-07 0.0 5.06709094667e-08 1.886014 243.047 195.33 1248.748 243.047 + 9.683279e-07 0.0 5.06418934667e-08 1.884934 242.846 195.08 1253.759 242.846 + 9.718095e-07 0.0 5.06183851333e-08 1.884059 242.62 194.84 1258.772 242.62 + 9.62074e-07 0.0 5.05907662e-08 1.883031 242.415 194.6 1263.772 242.415 + 9.620734e-07 0.0 5.05623681333e-08 1.881974 242.204 194.36 1268.781 242.204 + 9.525402e-07 0.0 5.05364418e-08 1.881009 241.981 194.11 1273.784 241.981 + 9.620238e-07 0.0 5.05108378667e-08 1.880056 241.775 193.88 1278.789 241.775 + 9.482527e-07 0.0 5.04825741333e-08 1.879004 241.554 193.63 1283.792 241.554 + 9.477999e-07 0.0 5.04567821333e-08 1.878044 241.342 193.39 1288.802 241.342 + 9.400813e-07 0.0 5.04289482667e-08 1.877008 241.126 193.15 1293.803 241.126 + 9.640905e-07 0.0 5.04003084e-08 1.875942 240.91 192.9 1298.816 240.91 + 9.477471e-07 0.0 5.03733074e-08 1.874937 240.697 192.67 1303.822 240.697 + 9.493117e-07 0.0 5.03513842e-08 1.874121 240.48 192.42 1308.835 240.48 + 9.436476e-07 0.0 5.03237921333e-08 1.873094 240.267 192.19 1313.84 240.267 + 9.509254e-07 0.0 5.02947761333e-08 1.872014 240.049 191.94 1318.844 240.049 + 9.556649e-07 0.0 5.02691722e-08 1.871061 239.835 191.71 1323.844 239.835 + 9.396264e-07 0.0 5.02438100667e-08 1.870117 239.614 191.46 1328.854 239.614 + 9.464862e-07 0.0 5.02160299333e-08 1.869083 239.413 191.23 1333.855 239.413 + 9.434591e-07 0.0 5.01928171333e-08 1.868219 239.198 190.99 1338.864 239.198 + 9.437617e-07 0.0 5.01644190667e-08 1.867162 238.975 190.75 1343.877 238.975 + 9.33826e-07 0.0 5.01339254e-08 1.866027 238.749 190.51 1348.888 238.749 + 9.272673e-07 0.0 5.01074617333e-08 1.865042 238.538 190.28 1353.896 238.538 + 9.500159e-07 0.0 5.0079601e-08 1.864005 238.317 190.04 1358.902 238.317 + 9.369528e-07 0.0 5.00568986667e-08 1.86316 238.112 189.81 1363.904 238.112 + 9.409861e-07 0.0 5.00292797333e-08 1.862132 237.888 189.58 1368.914 237.888 + 9.3337e-07 0.0 5.00012578e-08 1.861089 237.681 189.34 1373.917 237.681 + 9.394232e-07 0.0 4.99795495333e-08 1.860281 237.46 189.12 1378.93 237.46 + 9.400783e-07 0.0 4.99502111333e-08 1.859189 237.259 188.89 1383.937 237.259 + 9.336717e-07 0.0 4.99237474667e-08 1.858204 237.045 188.66 1388.937 237.045 + 9.335208e-07 0.0 4.98985196667e-08 1.857265 236.81 188.4 1393.944 236.81 + 9.435589e-07 0.0 4.98738292e-08 1.856346 236.596 188.19 1398.953 236.596 + 9.433586e-07 0.0 4.98466401333e-08 1.855334 236.388 187.96 1403.964 236.388 + 9.369501e-07 0.0 4.98193436e-08 1.854318 236.176 187.73 1408.97 236.176 + 9.44214e-07 0.0 4.97895484667e-08 1.853209 235.962 187.5 1413.98 235.962 + 9.299388e-07 0.0 4.97647774e-08 1.852287 235.755 187.28 1418.983 235.755 + 9.307453e-07 0.0 4.97400332e-08 1.851366 235.541 187.05 1423.984 235.541 + 9.462801e-07 0.0 4.97136501333e-08 1.850384 235.326 186.84 1428.985 235.326 + 9.40429e-07 0.0 4.96831027333e-08 1.849247 235.119 186.61 1433.995 235.119 + 9.384125e-07 0.0 4.96592451333e-08 1.848359 234.909 186.39 1438.998 234.909 + 9.570242e-07 0.0 4.9633077e-08 1.847385 234.688 186.16 1444.002 234.688 + 9.43605e-07 0.0 4.96099179333e-08 1.846523 234.473 185.95 1449.011 234.473 + 9.495596e-07 0.0 4.9583105e-08 1.845525 234.265 185.72 1454.019 234.265 + 9.506683e-07 0.0 4.95595698e-08 1.844649 234.056 185.5 1459.03 234.056 + 9.292294e-07 0.0 4.95293179333e-08 1.843523 233.84 185.28 1464.042 233.84 + 9.48348e-07 0.0 4.95051110667e-08 1.842622 233.626 185.06 1469.054 233.626 + 9.41891e-07 0.0 4.94746174e-08 1.841487 233.409 184.85 1474.066 233.409 + 9.397715e-07 0.0 4.94512165333e-08 1.840616 233.189 184.62 1479.076 233.189 + 9.43554e-07 0.0 4.94230065333e-08 1.839566 232.979 184.41 1484.08 232.979 + 9.377605e-07 0.0 4.93982623333e-08 1.838645 232.773 184.19 1489.09 232.773 + 9.423448e-07 0.0 4.93733300667e-08 1.837717 232.562 183.97 1494.099 232.562 + 9.277681e-07 0.0 4.93470007333e-08 1.836737 235.028 183.75 1499.111 235.028 + 9.482448e-07 0.0 4.93374093333e-08 1.83638 234.977 183.66 1501.115 234.977 + 9.348277e-07 0.0 4.93086351333e-08 1.835309 234.74 183.46 1506.126 234.74 + 9.287237e-07 0.0 4.92820371333e-08 1.834319 234.5 183.24 1511.136 234.5 + 9.243854e-07 0.0 4.92560839333e-08 1.833353 234.288 183.03 1516.137 234.288 + 9.26908e-07 0.0 4.92362832e-08 1.832616 234.084 182.81 1521.147 234.084 + 9.235286e-07 0.0 4.92086105333e-08 1.831586 233.892 182.6 1526.154 233.892 + 9.38408e-07 0.0 4.91816364e-08 1.830582 233.639 182.39 1531.165 233.639 + 9.277154e-07 0.0 4.91532652e-08 1.829526 233.457 182.19 1536.167 233.457 + 9.234773e-07 0.0 4.91275000667e-08 1.828567 233.245 181.97 1541.168 233.245 + 9.44915e-07 0.0 4.90987796e-08 1.827498 233.024 181.77 1546.174 233.024 + 9.240321e-07 0.0 4.90775549333e-08 1.826708 232.805 181.56 1551.179 232.805 + 9.235785e-07 0.0 4.90478941333e-08 1.825604 232.602 181.35 1556.183 232.602 + 9.143974e-07 0.0 4.90232574e-08 1.824687 232.398 181.15 1561.195 232.398 + 9.323538e-07 0.0 4.89964176e-08 1.823688 232.174 180.94 1566.202 232.174 + 9.360365e-07 0.0 4.89695778e-08 1.822689 231.975 180.74 1571.211 231.975 + 9.26755e-07 0.0 4.89486486667e-08 1.82191 231.772 180.53 1576.212 231.772 + 9.195415e-07 0.0 4.89204386667e-08 1.82086 231.555 180.34 1581.212 231.555 + 9.188865e-07 0.0 4.88971184e-08 1.819992 231.342 180.12 1586.222 231.342 + 9.346741e-07 0.0 4.88709771333e-08 1.819019 231.122 179.93 1591.224 231.122 + 9.213566e-07 0.0 4.88437880667e-08 1.818007 230.932 179.72 1596.235 230.932 + 9.294776e-07 0.0 4.88172975333e-08 1.817021 230.703 179.52 1601.235 230.703 + 9.2514e-07 0.0 4.87959922667e-08 1.816228 230.49 179.31 1606.248 230.49 + 9.306875e-07 0.0 4.8767433e-08 1.815165 230.281 179.12 1611.256 230.281 + 9.306373e-07 0.0 4.87376378667e-08 1.814056 230.055 178.91 1616.268 230.055 + 9.125292e-07 0.0 4.87149086667e-08 1.81321 229.866 178.72 1621.268 229.866 + 9.037033e-07 0.0 4.86891704e-08 1.812252 229.623 178.51 1626.278 229.623 + 9.153543e-07 0.0 4.86651784667e-08 1.811359 229.439 178.31 1631.282 229.439 + 9.064761e-07 0.0 4.86385804667e-08 1.810369 229.228 178.11 1636.291 229.228 + 9.047108e-07 0.0 4.86118481333e-08 1.809374 229.008 177.91 1641.301 229.008 + 9.043064e-07 0.0 4.85847934e-08 1.808367 228.798 177.71 1646.309 228.798 + 9.022891e-07 0.0 4.85635687333e-08 1.807577 228.586 177.52 1651.318 228.586 + 9.035498e-07 0.0 4.85358154667e-08 1.806544 228.38 177.31 1656.321 228.38 + 9.009252e-07 0.0 4.85080890667e-08 1.805512 228.141 177.11 1661.328 228.141 + 9.089472e-07 0.0 4.84822433333e-08 1.80455 227.946 176.92 1666.335 227.946 + 9.048609e-07 0.0 4.84613679333e-08 1.803773 227.726 176.71 1671.339 227.726 + 8.840795e-07 0.0 4.84351460667e-08 1.802797 227.519 176.52 1676.342 227.519 + 8.934621e-07 0.0 4.84098108e-08 1.801854 227.293 176.32 1681.353 227.293 + 9.046089e-07 0.0 4.83827023333e-08 1.800845 227.094 176.12 1686.366 227.094 + 8.967399e-07 0.0 4.83578238e-08 1.799919 226.892 175.92 1691.373 226.892 + 9.083907e-07 0.0 4.83342617333e-08 1.799042 226.675 175.73 1696.382 226.675 + 8.939646e-07 0.0 4.83030695333e-08 1.797881 226.468 175.53 1701.391 226.468 + 9.080385e-07 0.0 4.82804746667e-08 1.79704 226.264 175.34 1706.395 226.264 + 8.889211e-07 0.0 4.82566976667e-08 1.796155 226.017 175.14 1711.407 226.017 + 9.105084e-07 0.0 4.82281115333e-08 1.795091 225.833 174.94 1716.417 225.833 + 8.882144e-07 0.0 4.82011911333e-08 1.794089 225.626 174.74 1721.422 225.626 + 8.870038e-07 0.0 4.81779246e-08 1.793223 225.419 174.55 1726.433 225.419 + 9.00723e-07 0.0 4.81534759333e-08 1.792313 225.176 174.34 1731.444 225.176 + 9.014795e-07 0.0 4.81292422e-08 1.791411 224.984 174.15 1736.444 224.984 + 9.034977e-07 0.0 4.81037994667e-08 1.790464 224.772 173.94 1741.448 224.772 + 8.911388e-07 0.0 4.80754551333e-08 1.789409 224.545 173.74 1746.458 224.545 + 8.905833e-07 0.0 4.80557081333e-08 1.788674 224.344 173.56 1751.471 224.344 + 8.963415e-07 0.0 4.80239786e-08 1.787493 224.107 173.35 1756.472 224.107 + 8.811509e-07 0.0 4.80002284667e-08 1.786609 223.927 173.16 1761.484 223.927 + 8.887674e-07 0.0 4.79769888e-08 1.785744 223.692 172.96 1766.493 223.692 + 8.926512e-07 0.0 4.79481608667e-08 1.784671 223.49 172.77 1771.498 223.49 + 8.848327e-07 0.0 4.79212404667e-08 1.783669 223.273 172.57 1776.508 223.273 + 8.903803e-07 0.0 4.78997202667e-08 1.782868 223.058 172.38 1781.516 223.058 + 9.035467e-07 0.0 4.78770985333e-08 1.782026 222.847 172.19 1786.518 222.847 + 8.862453e-07 0.0 4.78480556667e-08 1.780945 222.644 172.0 1791.52 222.644 + 8.842775e-07 0.0 4.7822801e-08 1.780005 222.441 171.8 1796.526 222.441 + 8.947181e-07 0.0 4.77967940667e-08 1.779037 222.216 171.62 1801.532 222.216 + 8.918925e-07 0.0 4.77727752667e-08 1.778143 222.029 171.41 1806.541 222.029 + 8.676807e-07 0.0 4.77486221333e-08 1.777244 221.822 171.23 1811.544 221.822 + 8.938092e-07 0.0 4.77203315333e-08 1.776191 221.598 171.04 1816.545 221.598 + 8.852344e-07 0.0 4.7696662e-08 1.77531 221.42 170.84 1821.549 221.42 + 8.643018e-07 0.0 4.76661414667e-08 1.774174 221.219 170.65 1826.558 221.219 + 8.627281e-07 0.0 4.76448093333e-08 1.77338 221.004 170.45 1831.565 221.004 + 8.663689e-07 0.0 4.7620898e-08 1.77249 220.779 170.27 1836.573 220.779 + 8.824594e-07 0.0 4.75944343333e-08 1.771505 220.565 170.08 1841.578 220.565 + 8.768603e-07 0.0 4.75651765333e-08 1.770416 220.373 169.89 1846.59 220.373 + 8.7e-07 0.0 4.75440056e-08 1.769628 220.149 169.69 1851.6 220.149 + 8.718164e-07 0.0 4.7519342e-08 1.76871 219.959 169.5 1856.606 219.959 + 8.687383e-07 0.0 4.7493953e-08 1.767765 219.747 169.31 1861.606 219.747 + 8.72219e-07 0.0 4.74659848e-08 1.766724 219.517 169.13 1866.613 219.517 + 8.698491e-07 0.0 4.74435511333e-08 1.765889 219.329 168.94 1871.617 219.329 + 8.690912e-07 0.0 4.74222996e-08 1.765098 219.113 168.75 1876.627 219.113 + 8.553217e-07 0.0 4.73909999333e-08 1.763933 218.902 168.55 1881.633 218.902 + 8.576418e-07 0.0 4.7364993e-08 1.762965 218.674 168.37 1886.646 218.674 + 8.591544e-07 0.0 4.73413234667e-08 1.762084 218.466 168.17 1891.654 218.466 + 8.637445e-07 0.0 4.73205018e-08 1.761309 218.248 168.0 1896.661 218.248 + 8.640981e-07 0.0 4.72953546e-08 1.760373 218.067 167.8 1901.663 218.067 + 8.677785e-07 0.0 4.72692402e-08 1.759401 217.86 167.61 1906.676 217.86 + 8.504774e-07 0.0 4.72425347333e-08 1.758407 217.649 167.42 1911.688 217.649 + 8.631347e-07 0.0 4.72198861333e-08 1.757564 217.478 167.23 1916.7 217.478 + 8.497714e-07 0.0 4.71942016e-08 1.756608 217.238 167.04 1921.706 217.238 + 8.516373e-07 0.0 4.71641378e-08 1.755489 217.055 166.85 1926.714 217.055 + 8.504266e-07 0.0 4.71410056e-08 1.754628 216.855 166.67 1931.715 216.855 + 8.713085e-07 0.0 4.71178465333e-08 1.753766 216.624 166.48 1936.719 216.624 + 8.700467e-07 0.0 4.70914097333e-08 1.752782 216.427 166.29 1941.732 216.427 + 8.605646e-07 0.0 4.70656177333e-08 1.751822 216.23 166.11 1946.737 216.23 + 8.420535e-07 0.0 4.70442318667e-08 1.751026 216.024 165.93 1951.744 216.024 + 8.624807e-07 0.0 4.70146248e-08 1.749924 215.819 165.74 1956.752 215.819 + 8.571337e-07 0.0 4.69927553333e-08 1.74911 215.607 165.55 1961.752 215.607 + 8.565287e-07 0.0 4.69668558667e-08 1.748146 215.425 165.36 1966.756 215.425 + 8.48962e-07 0.0 4.69433744e-08 1.747272 215.22 165.18 1971.769 215.22 + 8.713071e-07 0.0 4.69147076667e-08 1.746205 215.005 164.99 1976.776 215.005 + 8.64094e-07 0.0 4.688905e-08 1.74525 214.799 164.81 1981.777 214.799 + 8.615722e-07 0.0 4.68644401333e-08 1.744334 214.611 164.62 1986.783 214.611 + 8.592008e-07 0.0 4.6839884e-08 1.74342 214.388 164.44 1991.789 214.388 + 8.476575e-07 0.0 4.68170204667e-08 1.742569 214.177 164.25 1996.791 214.177 + 8.330733e-07 0.0 4.67913896667e-08 1.741615 213.996 164.06 2001.798 213.996 + 8.377642e-07 0.0 4.67641468667e-08 1.740601 213.756 163.88 2006.806 213.756 + 8.35039e-07 0.0 4.67405310667e-08 1.739722 213.576 163.69 2011.808 213.576 + 8.296932e-07 0.0 4.67168884e-08 1.738842 213.341 163.52 2016.814 213.341 + 8.42303e-07 0.0 4.66913382e-08 1.737891 213.164 163.33 2021.816 213.164 + 8.31559e-07 0.0 4.66648476667e-08 1.736905 212.95 163.15 2026.825 212.95 + 8.432106e-07 0.0 4.66396736e-08 1.735968 212.728 162.96 2031.832 212.728 + 8.373082e-07 0.0 4.66156010667e-08 1.735072 212.523 162.78 2036.845 212.523 + 8.404872e-07 0.0 4.65915554e-08 1.734177 212.297 162.59 2041.852 212.297 + 8.340297e-07 0.0 4.65650648667e-08 1.733191 212.106 162.42 2046.855 212.106 + 8.361475e-07 0.0 4.65422550667e-08 1.732342 211.889 162.23 2051.866 211.889 + 8.521367e-07 0.0 4.65149048e-08 1.731324 211.697 162.05 2056.877 211.697 + 8.327673e-07 0.0 4.64916651333e-08 1.730459 211.469 161.86 2061.882 211.469 + 8.359458e-07 0.0 4.64672970667e-08 1.729552 211.271 161.69 2066.891 211.271 + 8.413929e-07 0.0 4.64422573333e-08 1.72862 211.063 161.5 2071.9 211.063 + 8.370051e-07 0.0 4.64186146667e-08 1.72774 210.871 161.32 2076.91 210.871 + 8.215189e-07 0.0 4.63924465333e-08 1.726766 210.642 161.14 2081.92 210.642 + 8.34678e-07 0.0 4.63639678667e-08 1.725706 210.443 160.96 2086.925 210.443 + 8.431074e-07 0.0 4.63432268e-08 1.724934 210.215 160.78 2091.936 210.215 + 8.343807e-07 0.0 4.63160646e-08 1.723923 210.01 160.58 2096.946 210.01 + 8.268149e-07 0.0 4.62937115333e-08 1.723091 209.801 160.4 2101.958 209.801 + 8.213168e-07 0.0 4.62708211333e-08 1.722239 209.593 160.22 2106.967 209.593 + 8.416939e-07 0.0 4.62445455333e-08 1.721261 209.374 160.05 2111.971 209.374 + 8.263596e-07 0.0 4.62224342667e-08 1.720438 209.182 159.86 2116.972 209.182 + 8.388679e-07 0.0 4.61940899333e-08 1.719383 208.967 159.69 2121.975 208.967 + 8.323619e-07 0.0 4.61722742e-08 1.718571 208.756 159.5 2126.984 208.756 + 8.284281e-07 0.0 4.61449508e-08 1.717554 208.567 159.33 2131.996 208.567 + 8.217694e-07 0.0 4.61206096e-08 1.716648 208.343 159.15 2137.005 208.343 + 8.356913e-07 0.0 4.60934474e-08 1.715637 208.168 158.98 2142.013 208.168 + 8.120845e-07 0.0 4.60701271333e-08 1.714769 207.962 158.8 2147.023 207.962 + 8.234847e-07 0.0 4.60476934667e-08 1.713934 207.772 158.63 2152.027 207.772 + 8.35438e-07 0.0 4.60185162667e-08 1.712848 207.563 158.44 2157.031 207.563 + 8.117812e-07 0.0 4.59987692667e-08 1.712113 207.37 158.26 2162.04 207.37 + 8.168763e-07 0.0 4.59762012667e-08 1.711273 207.175 158.09 2167.043 207.175 + 8.144043e-07 0.0 4.59511346667e-08 1.71034 206.982 157.92 2172.048 206.982 + 8.32764e-07 0.0 4.59231396e-08 1.709298 206.809 157.75 2177.048 206.809 + 8.163204e-07 0.0 4.58970252e-08 1.708326 206.59 157.57 2182.061 206.59 + 8.154633e-07 0.0 4.58726571333e-08 1.707419 206.41 157.39 2187.073 206.41 + 7.989181e-07 0.0 4.58526414667e-08 1.706674 206.209 157.22 2192.086 206.209 + 7.97808e-07 0.0 4.58265808e-08 1.705704 206.013 157.05 2197.088 206.013 + 8.282232e-07 0.0 4.58033948667e-08 1.704841 205.829 156.87 2202.098 205.829 + 8.094598e-07 0.0 4.57753729333e-08 1.703798 205.629 156.7 2207.11 205.629 + 8.168742e-07 0.0 4.5750978e-08 1.70289 205.437 156.52 2212.111 205.437 + 8.009857e-07 0.0 4.57302638e-08 1.702119 205.243 156.35 2217.122 205.243 + 8.070373e-07 0.0 4.57091734667e-08 1.701334 205.06 156.17 2222.129 205.06 + 8.080981e-07 0.0 4.56826023333e-08 1.700345 204.87 156.0 2227.129 204.87 + 8.148059e-07 0.0 4.56573745333e-08 1.699406 204.663 155.83 2232.137 204.663 + 8.037587e-07 0.0 4.56355856667e-08 1.698595 204.493 155.66 2237.146 204.493 + 7.959407e-07 0.0 4.56052532e-08 1.697466 204.287 155.49 2242.157 204.287 + 8.01842e-07 0.0 4.55856942667e-08 1.696738 204.099 155.31 2247.168 204.099 + 7.971506e-07 0.0 4.55581828e-08 1.695714 203.901 155.14 2252.17 203.901 + 7.980584e-07 0.0 4.55373342667e-08 1.694938 203.705 154.96 2257.181 203.705 + 8.16873e-07 0.0 4.55073242e-08 1.693821 203.527 154.81 2262.186 203.527 + 8.110717e-07 0.0 4.54874697333e-08 1.693082 203.322 154.63 2267.194 203.322 + 8.061786e-07 0.0 4.54611941333e-08 1.692104 203.121 154.46 2272.196 203.121 + 8.008323e-07 0.0 4.54393515333e-08 1.691291 202.936 154.28 2277.202 202.936 + 7.966961e-07 0.0 4.5414473e-08 1.690365 202.734 154.12 2282.209 202.734 + 7.91601e-07 0.0 4.5386263e-08 1.689315 202.527 153.94 2287.211 202.527 + 8.009333e-07 0.0 4.53666503333e-08 1.688585 202.317 153.78 2292.222 202.317 + 8.017896e-07 0.0 4.53462854e-08 1.687827 202.137 153.6 2297.223 202.137 + 7.989147e-07 0.0 4.53171082e-08 1.686741 201.918 153.44 2302.235 201.918 + 7.956861e-07 0.0 4.52932506e-08 1.685853 201.715 153.26 2307.246 201.715 + 7.859008e-07 0.0 4.52715154667e-08 1.685044 201.533 153.1 2312.251 201.533 + 8.000234e-07 0.0 4.52475772667e-08 1.684153 201.318 152.92 2317.253 201.318 + 8.098092e-07 0.0 4.52188030667e-08 1.683082 201.11 152.74 2322.266 201.11 + 7.957863e-07 0.0 4.51954828e-08 1.682214 200.911 152.57 2327.277 200.911 + 8.055209e-07 0.0 4.51729954e-08 1.681377 200.685 152.4 2332.286 200.685 + 7.907917e-07 0.0 4.51509916e-08 1.680558 200.477 152.23 2337.294 200.477 + 7.800985e-07 0.0 4.512391e-08 1.67955 200.272 152.05 2342.305 200.272 + 7.888756e-07 0.0 4.51016912667e-08 1.678723 200.101 151.89 2347.309 200.101 + 7.813086e-07 0.0 4.50745828e-08 1.677714 199.866 151.71 2352.319 199.866 + 7.837303e-07 0.0 4.50533044e-08 1.676922 199.695 151.55 2357.328 199.695 + 7.977528e-07 0.0 4.50299035333e-08 1.676051 199.486 151.38 2362.329 199.486 + 7.830253e-07 0.0 4.50040846667e-08 1.67509 199.264 151.22 2367.329 199.264 + 7.807603e-07 0.0 4.49762239333e-08 1.674053 199.073 151.04 2372.334 199.073 + 7.821146e-07 0.0 4.49588680667e-08 1.673407 198.864 150.88 2377.337 198.864 + 7.947243e-07 0.0 4.49336402667e-08 1.672468 198.675 150.71 2382.347 198.675 + 8.0219e-07 0.0 4.49084124667e-08 1.671529 198.475 150.55 2387.348 198.475 + 7.796435e-07 0.0 4.48846623333e-08 1.670645 198.285 150.38 2392.357 198.285 + 7.853418e-07 0.0 4.48606972667e-08 1.669753 198.083 150.22 2397.368 198.083 + 7.936656e-07 0.0 4.48390964667e-08 1.668949 197.883 150.05 2402.376 197.883 + 7.822149e-07 0.0 4.48111282667e-08 1.667908 197.689 149.88 2407.383 197.689 + 7.861483e-07 0.0 4.47869214e-08 1.667007 197.478 149.72 2412.39 197.478 + 8.02289e-07 0.0 4.47608338667e-08 1.666036 197.291 149.55 2417.4 197.291 + 8.079386e-07 0.0 4.47381315333e-08 1.665191 197.098 149.39 2422.413 197.098 + 8.044077e-07 0.0 4.47192442667e-08 1.664488 196.923 149.22 2427.427 196.923 + 7.92605e-07 0.0 4.46963538667e-08 1.663636 196.74 149.06 2432.427 196.74 + 7.923522e-07 0.0 4.46683050667e-08 1.662592 196.525 148.89 2437.427 196.525 + 7.897799e-07 0.0 4.46476177333e-08 1.661822 196.345 148.74 2442.435 196.345 + 7.950755e-07 0.0 4.46188435333e-08 1.660751 196.116 148.55 2447.441 196.116 + 7.967898e-07 0.0 4.45972158667e-08 1.659946 195.931 148.39 2452.444 195.931 + 8.005728e-07 0.0 4.45777375333e-08 1.659221 195.725 148.23 2457.446 195.725 + 7.808014e-07 0.0 4.45534500667e-08 1.658317 195.548 148.06 2462.456 195.548 + 7.959326e-07 0.0 4.45304522e-08 1.657461 195.327 147.89 2467.468 195.327 + 7.813046e-07 0.0 4.45054662e-08 1.656531 195.152 147.73 2472.48 195.152 + 7.992026e-07 0.0 4.44830862667e-08 1.655698 194.934 147.56 2477.488 194.934 + 7.828682e-07 0.0 4.44598466e-08 1.654833 194.747 147.4 2482.491 194.747 + 7.875078e-07 0.0 4.44334098e-08 1.653849 194.564 147.23 2487.493 194.564 + 7.794886e-07 0.0 4.44106806e-08 1.653003 194.373 147.08 2492.496 194.373 + 7.813043e-07 0.0 4.43819332667e-08 1.651933 194.182 146.91 2497.505 194.182 + 7.856912e-07 0.0 4.43611922e-08 1.651161 193.962 146.74 2502.518 193.962 + 7.836748e-07 0.0 4.43394033333e-08 1.65035 193.763 146.58 2507.519 193.763 + 7.761589e-07 0.0 4.43195488667e-08 1.649611 193.597 146.41 2512.519 193.597 + 7.776727e-07 0.0 4.42930583333e-08 1.648625 193.392 146.25 2517.53 193.392 + 7.763606e-07 0.0 4.42694425333e-08 1.647746 193.21 146.09 2522.539 193.21 + 7.730303e-07 0.0 4.42445908667e-08 1.646821 192.997 145.93 2527.539 192.997 + 7.638508e-07 0.0 4.42214318e-08 1.645959 192.812 145.76 2532.541 192.812 + 7.854898e-07 0.0 4.42036460667e-08 1.645297 192.619 145.6 2537.544 192.619 + 7.80698e-07 0.0 4.41741733333e-08 1.6442 192.44 145.43 2542.545 192.44 + 7.748973e-07 0.0 4.41514978667e-08 1.643356 192.232 145.27 2547.546 192.232 + 7.69045e-07 0.0 4.41290373333e-08 1.64252 192.037 145.1 2552.549 192.037 + 7.68137e-07 0.0 4.41038632667e-08 1.641583 191.858 144.95 2557.554 191.858 + 7.700032e-07 0.0 4.40770772e-08 1.640586 191.658 144.78 2562.56 191.658 + 7.549704e-07 0.0 4.40562018e-08 1.639809 191.468 144.62 2567.571 191.468 + 7.842932e-07 0.0 4.40338218667e-08 1.638976 191.28 144.45 2572.575 191.28 + 7.627797e-07 0.0 4.40118718e-08 1.638159 191.093 144.28 2577.583 191.093 + 7.64252e-07 0.0 4.39826677333e-08 1.637072 190.895 144.12 2582.595 190.895 + 7.627897e-07 0.0 4.3960449e-08 1.636245 190.705 143.96 2587.599 190.705 + 7.500278e-07 0.0 4.39395467333e-08 1.635467 190.528 143.81 2592.6 190.528 + 7.627386e-07 0.0 4.39146682e-08 1.634541 190.326 143.64 2597.605 190.326 + 7.547189e-07 0.0 4.38924763333e-08 1.633715 190.161 143.48 2602.609 190.161 + 7.556771e-07 0.0 4.38692904e-08 1.632852 189.954 143.32 2607.614 189.954 + 7.635956e-07 0.0 4.38445730667e-08 1.631932 189.76 143.16 2612.626 189.76 + 7.558787e-07 0.0 4.38199094667e-08 1.631014 189.586 143.0 2617.629 189.586 + 7.543638e-07 0.0 4.37974489333e-08 1.630178 189.378 142.84 2622.631 189.378 + 7.540614e-07 0.0 4.37765198e-08 1.629399 189.17 142.67 2627.635 189.17 + 7.64453e-07 0.0 4.37532532667e-08 1.628533 189.008 142.51 2632.639 189.008 + 7.488661e-07 0.0 4.37245059333e-08 1.627463 188.821 142.35 2637.645 188.821 + 7.581469e-07 0.0 4.37052156667e-08 1.626745 188.626 142.19 2642.652 188.626 + 7.578446e-07 0.0 4.36824864667e-08 1.625899 188.428 142.03 2647.66 188.428 + 7.541113e-07 0.0 4.36571512e-08 1.624956 188.237 141.87 2652.668 188.237 + 7.628379e-07 0.0 4.36355235333e-08 1.624151 188.069 141.7 2657.681 188.069 + 7.552724e-07 0.0 4.36143794667e-08 1.623364 187.854 141.54 2662.694 187.854 + 7.513366e-07 0.0 4.35842888e-08 1.622244 187.69 141.39 2667.704 187.69 + 7.457428e-07 0.0 4.35669866667e-08 1.6216 187.487 141.23 2672.716 187.487 + 7.450805e-07 0.0 4.35399319333e-08 1.620593 187.3 141.07 2677.727 187.3 + 7.501231e-07 0.0 4.3520373e-08 1.619865 187.117 140.9 2682.737 187.117 + 7.46292e-07 0.0 4.34966497333e-08 1.618982 186.938 140.75 2687.738 186.938 + 7.193067e-07 0.0 4.346758e-08 1.6179 186.742 140.59 2692.744 186.742 + 7.383223e-07 0.0 4.34511644667e-08 1.617289 186.55 140.44 2697.746 186.55 + 7.33228e-07 0.0 4.34260978667e-08 1.616356 186.369 140.27 2702.748 186.369 + 7.487637e-07 0.0 4.34025895333e-08 1.615481 186.177 140.11 2707.757 186.177 + 7.397341e-07 0.0 4.33789737333e-08 1.614602 185.99 139.95 2712.757 185.99 + 7.394311e-07 0.0 4.33561639333e-08 1.613753 185.813 139.8 2717.761 185.813 + 7.378679e-07 0.0 4.33312585333e-08 1.612826 185.633 139.64 2722.77 185.633 + 7.462398e-07 0.0 4.33104637333e-08 1.612052 185.442 139.48 2727.772 185.442 + 7.478037e-07 0.0 4.32870628667e-08 1.611181 185.253 139.32 2732.778 185.253 + 7.29444e-07 0.0 4.32631246667e-08 1.61029 185.063 139.16 2737.779 185.063 + 7.309061e-07 0.0 4.32396969333e-08 1.609418 184.882 139.01 2742.785 184.882 + 7.359498e-07 0.0 4.32183110667e-08 1.608622 184.691 138.85 2747.788 184.691 + 7.314102e-07 0.0 4.31928683333e-08 1.607675 184.502 138.69 2752.789 184.502 + 7.508806e-07 0.0 4.31694137333e-08 1.606802 184.311 138.53 2757.791 184.311 + 7.286911e-07 0.0 4.31485383333e-08 1.606025 184.136 138.37 2762.793 184.136 + 7.164292e-07 0.0 4.31225045333e-08 1.605056 183.935 138.22 2767.796 183.935 + 7.483561e-07 0.0 4.31022470667e-08 1.604302 183.756 138.06 2772.797 183.756 + 7.303498e-07 0.0 4.30782551333e-08 1.603409 183.569 137.9 2777.804 183.569 + 7.278281e-07 0.0 4.30543975333e-08 1.602521 183.383 137.74 2782.817 183.383 + 7.295433e-07 0.0 4.30335221333e-08 1.601744 183.185 137.58 2787.823 183.185 + 7.263139e-07 0.0 4.30119213333e-08 1.60094 182.993 137.43 2792.83 182.993 + 7.322672e-07 0.0 4.29843292667e-08 1.599913 182.816 137.26 2797.833 182.816 + 7.307532e-07 0.0 4.29636150667e-08 1.599142 182.625 137.11 2802.837 182.625 + 7.30249e-07 0.0 4.29343841333e-08 1.598054 182.451 136.95 2807.843 182.451 + 7.393777e-07 0.0 4.29168670667e-08 1.597402 182.265 136.79 2812.853 182.265 + 7.242455e-07 0.0 4.28920691333e-08 1.596479 182.072 136.63 2817.854 182.072 + 7.152167e-07 0.0 4.28678622667e-08 1.595578 181.892 136.46 2822.856 181.892 + 7.232363e-07 0.0 4.28445688667e-08 1.594711 181.701 136.31 2827.864 181.701 + 7.365018e-07 0.0 4.28234248e-08 1.593924 181.521 136.15 2832.874 181.521 + 7.192006e-07 0.0 4.28027106e-08 1.593153 181.333 136.0 2837.883 181.333 + 7.330711e-07 0.0 4.27761932e-08 1.592166 181.141 135.84 2842.89 181.141 + 7.337777e-07 0.0 4.27529266667e-08 1.5913 180.964 135.68 2847.9 180.964 + 7.285319e-07 0.0 4.27340394e-08 1.590597 180.782 135.52 2852.911 180.782 + 7.099697e-07 0.0 4.27065010667e-08 1.589572 180.595 135.37 2857.92 180.595 + 7.099189e-07 0.0 4.26850883333e-08 1.588775 180.413 135.21 2862.926 180.413 + 7.330708e-07 0.0 4.26637293333e-08 1.58798 180.216 135.05 2867.934 180.216 + 7.12995e-07 0.0 4.26385552667e-08 1.587043 180.015 134.88 2872.938 180.015 + 7.128438e-07 0.0 4.26157186e-08 1.586193 179.838 134.72 2877.946 179.838 + 7.140043e-07 0.0 4.25921296667e-08 1.585315 179.632 134.56 2882.955 179.632 + 7.059334e-07 0.0 4.25707975333e-08 1.584521 179.454 134.41 2887.956 179.454 + 6.938279e-07 0.0 4.25474504e-08 1.583652 179.279 134.25 2892.966 179.279 + 7.084048e-07 0.0 4.25239420667e-08 1.582777 179.092 134.1 2897.971 179.092 + 6.928183e-07 0.0 4.24986336667e-08 1.581835 178.912 133.94 2902.972 178.912 + 6.902459e-07 0.0 4.24762537333e-08 1.581002 178.72 133.78 2907.981 178.72 + 7.075965e-07 0.0 4.24572321333e-08 1.580294 178.523 133.62 2912.988 178.523 + 7.089085e-07 0.0 4.24321118e-08 1.579359 178.349 133.46 2917.989 178.349 + 6.937269e-07 0.0 4.24107528e-08 1.578564 178.19 133.31 2922.995 178.19 + 6.876729e-07 0.0 4.23849070667e-08 1.577602 177.997 133.16 2928.008 177.997 + 6.994254e-07 0.0 4.23643003333e-08 1.576835 177.805 133.01 2933.019 177.805 + 6.905476e-07 0.0 4.23422965333e-08 1.576016 177.622 132.85 2938.023 177.622 + 6.977115e-07 0.0 4.23213136667e-08 1.575235 177.445 132.7 2943.024 177.445 + 7.062845e-07 0.0 4.22958440667e-08 1.574287 177.25 132.55 2948.036 177.25 + 6.952388e-07 0.0 4.2269461e-08 1.573305 177.085 132.4 2953.042 177.085 + 7.058808e-07 0.0 4.22502244667e-08 1.572589 176.906 132.24 2958.045 176.906 + 6.954389e-07 0.0 4.22298595333e-08 1.571831 176.724 132.1 2963.046 176.724 + 6.846459e-07 0.0 4.22003868e-08 1.570734 176.54 131.95 2968.058 176.54 + 6.881759e-07 0.0 4.21801293333e-08 1.56998 176.363 131.8 2973.068 176.363 + 6.856543e-07 0.0 4.21595226e-08 1.569213 176.18 131.64 2978.069 176.18 + 6.769785e-07 0.0 4.21402860667e-08 1.568497 176.003 131.49 2983.076 176.003 + 6.937241e-07 0.0 4.21152463333e-08 1.567565 175.823 131.34 2988.086 175.823 + 6.905457e-07 0.0 4.20942634667e-08 1.566784 175.638 131.18 2993.086 175.638 + 6.929672e-07 0.0 4.20714805333e-08 1.565936 175.444 131.04 2998.096 175.444 + 6.951869e-07 0.0 4.20480259333e-08 1.565063 175.257 130.89 3003.1 175.257 + 6.867121e-07 0.0 4.20255654e-08 1.564227 175.086 130.74 3008.108 175.086 + 6.831308e-07 0.0 4.20030242667e-08 1.563388 174.916 130.59 3013.119 174.916 + 6.887283e-07 0.0 4.1979301e-08 1.562505 174.741 130.44 3018.131 174.741 + 6.925125e-07 0.0 4.19571628667e-08 1.561681 174.565 130.29 3023.134 174.565 + 6.884262e-07 0.0 4.19366904667e-08 1.560919 174.366 130.14 3028.134 174.366 + 6.80103e-07 0.0 4.19139344e-08 1.560072 174.19 129.99 3033.134 174.19 + 6.97602e-07 0.0 4.18895663333e-08 1.559165 173.997 129.85 3038.142 173.997 + 6.713258e-07 0.0 4.18674550667e-08 1.558342 173.824 129.69 3043.148 173.824 + 6.84743e-07 0.0 4.18464184667e-08 1.557559 173.661 129.54 3048.152 173.661 + 6.843395e-07 0.0 4.18266446e-08 1.556823 173.467 129.39 3053.164 173.467 + 6.956381e-07 0.0 4.18043721333e-08 1.555994 173.282 129.25 3058.176 173.282 + 6.9473e-07 0.0 4.17758666e-08 1.554933 173.118 129.1 3063.185 173.118 + 6.818176e-07 0.0 4.17579196667e-08 1.554265 172.938 128.95 3068.196 172.938 + 6.704687e-07 0.0 4.17374741333e-08 1.553504 172.758 128.8 3073.201 172.758 + 6.77479e-07 0.0 4.1713724e-08 1.55262 172.585 128.65 3078.204 172.585 + 6.674918e-07 0.0 4.16881738e-08 1.551669 172.409 128.51 3083.206 172.409 + 6.755619e-07 0.0 4.1667379e-08 1.550895 172.232 128.35 3088.215 172.232 + 6.881719e-07 0.0 4.16466916667e-08 1.550125 172.044 128.21 3093.223 172.044 + 6.796979e-07 0.0 4.16204160667e-08 1.549147 171.871 128.06 3098.229 171.871 + 6.872636e-07 0.0 4.16002660667e-08 1.548397 171.707 127.91 3103.234 171.707 + 6.846907e-07 0.0 4.15805728e-08 1.547664 171.519 127.76 3108.238 171.519 + 6.83682e-07 0.0 4.15524165333e-08 1.546616 171.336 127.62 3113.248 171.336 + 6.899864e-07 0.0 4.15361622e-08 1.546011 171.158 127.47 3118.26 171.158 + 6.854963e-07 0.0 4.15123314667e-08 1.545124 170.969 127.33 3123.262 170.969 + 6.873638e-07 0.0 4.14871305333e-08 1.544186 170.804 127.17 3128.269 170.804 + 7.046641e-07 0.0 4.1469909e-08 1.543545 170.635 127.03 3133.272 170.635 + 6.886367e-07 0.0 4.14425856e-08 1.542528 170.444 126.88 3138.284 170.444 + 6.948787e-07 0.0 4.14224893333e-08 1.54178 170.287 126.74 3143.298 170.287 + 6.997207e-07 0.0 4.14021244e-08 1.541022 170.088 126.59 3148.31 170.088 + 6.888263e-07 0.0 4.13791534e-08 1.540167 169.891 126.44 3153.32 169.891 + 6.936675e-07 0.0 4.13589765333e-08 1.539416 169.726 126.3 3158.322 169.726 + 6.859e-07 0.0 4.13332382667e-08 1.538458 169.542 126.14 3163.334 169.542 + 6.786876e-07 0.0 4.13110732667e-08 1.537633 169.376 126.0 3168.336 169.376 + 6.944237e-07 0.0 4.1290574e-08 1.53687 169.198 125.85 3173.343 169.198 + 6.888248e-07 0.0 4.12675761333e-08 1.536014 169.019 125.71 3178.35 169.019 + 6.837803e-07 0.0 4.12479634667e-08 1.535284 168.845 125.56 3183.362 168.845 + 6.920526e-07 0.0 4.12235148e-08 1.534374 168.668 125.41 3188.372 168.668 + 6.953317e-07 0.0 4.11993616667e-08 1.533475 168.486 125.27 3193.373 168.486 + 6.945241e-07 0.0 4.11762294667e-08 1.532614 168.317 125.12 3198.387 168.317 + 6.950281e-07 0.0 4.11568048667e-08 1.531891 168.138 124.97 3203.394 168.138 + 6.947762e-07 0.0 4.11357951333e-08 1.531109 167.957 124.83 3208.401 167.957 + 6.904373e-07 0.0 4.11123136667e-08 1.530235 167.788 124.68 3213.414 167.788 + 6.957834e-07 0.0 4.10895844667e-08 1.529389 167.614 124.54 3218.416 167.614 + 7.038036e-07 0.0 4.10695419333e-08 1.528643 167.432 124.39 3223.423 167.432 + 7.015964e-07 0.0 4.10491232667e-08 1.527883 167.239 124.25 3228.43 167.239 + 7.050644e-07 0.0 4.10230894667e-08 1.526914 167.076 124.1 3233.434 167.076 + 6.813575e-07 0.0 4.10034230667e-08 1.526182 166.883 123.95 3238.447 166.883 + 6.781295e-07 0.0 4.09795923333e-08 1.525295 166.707 123.8 3243.447 166.707 + 6.836786e-07 0.0 4.09586363333e-08 1.524515 166.528 123.65 3248.455 166.528 + 6.577008e-07 0.0 4.09376266e-08 1.523733 166.353 123.51 3253.463 166.353 + 6.662251e-07 0.0 4.09151392e-08 1.522896 166.164 123.36 3258.472 166.164 + 6.741439e-07 0.0 4.08905293333e-08 1.52198 165.999 123.22 3263.478 165.999 + 6.683438e-07 0.0 4.08711316e-08 1.521258 165.831 123.07 3268.487 165.831 + 6.533115e-07 0.0 4.08495039333e-08 1.520453 165.643 122.93 3273.5 165.643 + 6.657196e-07 0.0 4.08251358667e-08 1.519546 165.465 122.79 3278.513 165.465 + 6.721759e-07 0.0 4.08038037333e-08 1.518752 165.302 122.64 3283.525 165.302 + 6.679893e-07 0.0 4.07839224e-08 1.518012 165.118 122.49 3288.526 165.118 + 6.553797e-07 0.0 4.07617842667e-08 1.517188 164.939 122.35 3293.53 164.939 + 6.569423e-07 0.0 4.07396998667e-08 1.516366 164.773 122.21 3298.531 164.773 + 6.552783e-07 0.0 4.07153586667e-08 1.51546 164.601 122.07 3303.541 164.601 + 6.635507e-07 0.0 4.06968475333e-08 1.514771 164.426 121.92 3308.545 164.426 + 6.637519e-07 0.0 4.06753004667e-08 1.513969 164.254 121.78 3313.55 164.254 + 6.441301e-07 0.0 4.06509861333e-08 1.513064 164.076 121.63 3318.552 164.076 + 6.508387e-07 0.0 4.06307555333e-08 1.512311 163.908 121.49 3323.56 163.908 + 6.578496e-07 0.0 4.06066024e-08 1.511412 163.737 121.35 3328.566 163.737 + 6.482582e-07 0.0 4.0585727e-08 1.510635 163.574 121.2 3333.569 163.574 + 6.562354e-07 0.0 4.05634814e-08 1.509807 163.399 121.06 3338.577 163.399 + 6.424651e-07 0.0 4.05399999333e-08 1.508933 163.235 120.92 3343.583 163.235 + 6.461966e-07 0.0 4.05200648667e-08 1.508191 163.063 120.77 3348.588 163.063 + 6.426663e-07 0.0 4.04994581333e-08 1.507424 162.885 120.63 3353.602 162.885 + 6.401945e-07 0.0 4.04788782667e-08 1.506658 162.73 120.49 3358.602 162.73 + 6.41405e-07 0.0 4.04587551333e-08 1.505909 162.558 120.36 3363.611 162.558 + 6.521993e-07 0.0 4.04369662667e-08 1.505098 162.392 120.24 3368.615 162.392 + 6.423131e-07 0.0 4.04131086667e-08 1.50421 162.216 120.1 3373.629 162.216 + 6.470035e-07 0.0 4.03935766e-08 1.503483 162.061 119.98 3378.63 162.061 + 6.494754e-07 0.0 4.03727549333e-08 1.502708 161.891 119.85 3383.641 161.891 + 6.559822e-07 0.0 4.03553453333e-08 1.50206 161.716 119.73 3388.652 161.716 + 6.461449e-07 0.0 4.03341475333e-08 1.501271 161.542 119.59 3393.664 161.542 + 6.679861e-07 0.0 4.03107735333e-08 1.500401 161.39 119.47 3398.673 161.39 + 6.694492e-07 0.0 4.02917250667e-08 1.499692 161.219 119.34 3403.676 161.219 + 6.693979e-07 0.0 4.02689152667e-08 1.498843 161.044 119.22 3408.686 161.044 + 6.610233e-07 0.0 4.02454338e-08 1.497969 160.883 119.09 3413.697 160.883 + 6.63446e-07 0.0 4.02270032667e-08 1.497283 160.706 118.96 3418.699 160.706 + 6.58351e-07 0.0 4.02076592667e-08 1.496563 160.544 118.84 3423.71 160.544 + 6.682374e-07 0.0 4.0187563e-08 1.495815 160.383 118.7 3428.719 160.383 + 6.687922e-07 0.0 4.01640546667e-08 1.49494 160.211 118.57 3433.719 160.211 + 6.614251e-07 0.0 4.01437434667e-08 1.494184 160.041 118.45 3438.725 160.041 + 6.687411e-07 0.0 4.01223844667e-08 1.493389 159.895 118.32 3443.728 159.895 + 6.764585e-07 0.0 4.01037121333e-08 1.492694 159.724 118.19 3448.73 159.724 + 6.487668e-07 0.0 4.00786992667e-08 1.491763 159.573 118.07 3453.741 159.573 + 6.702036e-07 0.0 4.00613434e-08 1.491117 159.397 117.94 3458.752 159.397 + 6.569879e-07 0.0 4.00410053333e-08 1.49036 159.234 117.82 3463.758 159.234 + 6.503797e-07 0.0 4.0016691e-08 1.489455 159.06 117.69 3468.762 159.06 + 6.521456e-07 0.0 3.99984754e-08 1.488777 158.909 117.56 3473.764 158.909 + 6.548686e-07 0.0 3.99778149333e-08 1.488008 158.729 117.43 3478.777 158.729 + 6.481095e-07 0.0 3.99546827333e-08 1.487147 158.537 117.26 3483.783 158.537 + 6.202661e-07 0.0 3.99278429333e-08 1.486148 158.336 117.03 3488.787 158.336 + 6.156768e-07 0.0 3.98929968667e-08 1.484851 158.12 116.78 3493.788 158.12 + 6.003931e-07 0.0 3.98718796667e-08 1.484065 157.909 116.51 3498.79 157.909 + 6.306564e-07 0.0 3.98449592667e-08 1.483063 157.685 116.24 3503.795 157.685 + 6.144647e-07 0.0 3.98189254667e-08 1.482094 157.478 115.98 3508.806 157.478 + 6.324206e-07 0.0 3.97875183333e-08 1.480925 157.266 115.72 3513.81 157.266 + 6.296973e-07 0.0 3.97601143333e-08 1.479905 157.011 115.44 3518.814 157.011 + 6.137078e-07 0.0 3.97237906e-08 1.478553 156.708 115.05 3523.819 156.708 + 6.06394e-07 0.0 3.96868758e-08 1.477179 156.408 114.58 3528.821 156.408 + 5.902529e-07 0.0 3.96533999333e-08 1.475933 156.105 114.07 3533.831 156.105 + 6.024082e-07 0.0 3.96099296667e-08 1.474315 155.805 113.59 3538.843 155.805 + 5.966164e-07 0.0 3.95736059333e-08 1.472963 155.494 113.08 3543.856 155.494 + 6.12294e-07 0.0 3.95304312e-08 1.471356 155.181 112.58 3548.866 155.181 + 6.263163e-07 0.0 3.94956926e-08 1.470063 154.882 112.11 3553.873 154.882 + 5.981196e-07 0.0 3.94546134667e-08 1.468534 154.574 111.63 3558.884 154.574 + 6.132515e-07 0.0 3.94173762667e-08 1.467148 154.255 111.14 3563.892 154.255 + 6.174377e-07 0.0 3.93760284667e-08 1.465609 153.937 110.65 3568.906 153.937 + 6.098717e-07 0.0 3.9334009e-08 1.464045 153.634 110.18 3573.914 153.634 + 6.174367e-07 0.0 3.92912372667e-08 1.462453 153.294 109.68 3578.914 153.294 + 6.103755e-07 0.0 3.92545105333e-08 1.461086 152.968 109.22 3583.924 152.968 + 1.139965e-07 0.0 3.92116313333e-08 1.45949 152.646 108.73 3588.934 152.646 + 7.93441e-08 0.0 3.91705522e-08 1.457961 152.283 108.19 3593.94 152.283 + 6.07315e-08 0.0 3.9130091e-08 1.456455 151.945 107.74 3598.954 151.945 + 6.07815e-08 0.0 3.90891462e-08 1.454931 151.62 107.28 3603.964 151.62 + 6.41607e-08 0.0 3.9047879e-08 1.453395 151.284 106.79 3608.975 151.284 + 5.95703e-08 0.0 3.90016952e-08 1.451676 150.959 106.34 3613.985 150.959 + 5.56356e-08 0.0 3.89614758e-08 1.450179 150.63 105.86 3618.997 150.63 + 4.99358e-08 0.0 3.89187040667e-08 1.448587 150.304 105.41 3624.006 150.304 + 4.78169e-08 0.0 3.88747502e-08 1.446951 149.957 104.93 3629.013 149.957 + 4.9834e-08 0.0 3.88327576e-08 1.445388 149.605 104.46 3634.018 149.605 + 4.94316e-08 0.0 3.87889112e-08 1.443756 149.26 104.01 3639.027 149.26 + 4.5546e-08 0.0 3.87467574e-08 1.442187 148.94 103.54 3644.034 148.94 + 3.79797e-08 0.0 3.87060006667e-08 1.44067 148.592 103.1 3649.038 148.592 + 3.65167e-08 0.0 3.86667216e-08 1.439208 148.255 102.65 3654.039 148.255 + 3.96436e-08 0.0 3.86164003333e-08 1.437335 147.901 102.19 3659.05 147.901 + 1.95681e-08 0.0 3.85759122667e-08 1.435828 147.55 101.72 3664.056 147.55 + -3.763345e-07 0.0 3.85315016667e-08 1.434175 147.203 101.28 3669.063 147.203 + -3.725015e-07 0.0 3.84844581333e-08 1.432424 146.848 100.82 3674.068 146.848 + -3.866246e-07 0.0 3.8446603e-08 1.431015 146.506 100.38 3679.071 146.506 + -4.597628e-07 0.0 3.83991833333e-08 1.42925 146.162 99.93 3684.077 146.162 + -4.634453e-07 0.0 3.83546115333e-08 1.427591 145.81 99.49 3689.086 145.81 + -2.184593e-07 0.0 3.83149832e-08 1.426116 145.467 99.03 3694.093 145.467 + -2.228987e-07 0.0 3.82695516667e-08 1.424425 145.12 98.6 3699.095 145.12 + -2.311711e-07 0.0 3.82204931333e-08 1.422599 144.762 98.15 3704.108 144.762 + -2.332895e-07 0.0 3.81770228667e-08 1.420981 144.425 97.72 3709.109 144.425 + -2.464045e-07 0.0 3.81372333333e-08 1.4195 144.068 97.27 3714.118 144.068 + -2.536679e-07 0.0 3.80911838667e-08 1.417786 143.699 96.84 3719.127 143.699 + -2.569978e-07 0.0 3.80459404e-08 1.416102 143.34 96.39 3724.139 143.34 + -1.43357e-07 0.0 3.79960221333e-08 1.414244 142.97 95.94 3729.147 142.97 + -1.41953e-07 0.0 3.79516384e-08 1.412592 142.625 95.52 3734.152 142.625 + -1.535971e-07 0.0 3.79050784667e-08 1.410859 142.277 95.07 3739.164 142.277 + -1.559682e-07 0.0 3.78619843333e-08 1.409255 141.924 94.65 3744.173 141.924 + -1.49512e-07 0.0 3.78186215333e-08 1.407641 141.557 94.21 3749.178 141.557 + -1.445693e-07 0.0 3.77708794667e-08 1.405864 141.196 93.79 3754.185 141.196 + -1.467887e-07 0.0 3.77250180667e-08 1.404157 140.834 93.35 3759.194 140.834 + -1.636363e-07 0.0 3.76806880667e-08 1.402507 140.48 92.93 3764.204 140.48 + -1.536496e-07 0.0 3.76361968667e-08 1.400851 140.102 92.49 3769.214 140.102 + -1.517334e-07 0.0 3.75909534e-08 1.399167 139.747 92.05 3774.217 139.747 + -1.611154e-07 0.0 3.75410082667e-08 1.397308 139.384 91.63 3779.223 139.384 + -1.675216e-07 0.0 3.74967051333e-08 1.395659 139.014 91.22 3784.234 139.014 + -1.809388e-07 0.0 3.7449984e-08 1.39392 138.637 90.78 3789.242 138.637 + -1.837639e-07 0.0 3.74025912e-08 1.392156 138.265 90.37 3794.255 138.265 + -1.865889e-07 0.0 3.73589597333e-08 1.390532 137.893 89.94 3799.26 137.893 + -1.871442e-07 0.0 3.73126147333e-08 1.388807 137.531 89.52 3804.269 137.531 + -1.884054e-07 0.0 3.72604665333e-08 1.386866 137.155 89.09 3809.281 137.155 + -2.019238e-07 0.0 3.72151156e-08 1.385178 136.805 88.68 3814.288 136.805 + 1.60752e-08 0.0 3.71697109333e-08 1.383488 136.434 88.25 3819.291 136.434 + 4.3744e-09 0.0 3.71225599333e-08 1.381733 136.061 87.82 3824.304 136.061 + -1.9736e-08 0.0 3.70725073333e-08 1.37987 135.699 87.41 3829.313 135.699 + -1.72145e-08 0.0 3.7026431e-08 1.378155 135.331 86.98 3834.318 135.331 + -2.69496e-08 0.0 3.69792262667e-08 1.376398 134.947 86.58 3839.331 134.947 + -2.14522e-08 0.0 3.69352186667e-08 1.37476 134.575 86.15 3844.337 134.575 + -1.55005e-08 0.0 3.68825868667e-08 1.372801 134.206 85.75 3849.345 134.206 + -2.8464e-08 0.0 3.68376658e-08 1.371129 133.838 85.32 3854.351 133.838 + -2.75063e-08 0.0 3.67897088e-08 1.369344 133.464 84.91 3859.353 133.464 + -2.70022e-08 0.0 3.67409995333e-08 1.367531 133.099 84.49 3864.361 133.099 + -3.29544e-08 0.0 3.66939022667e-08 1.365778 132.708 84.08 3869.371 132.708 + -6.09992e-08 0.0 3.66471005333e-08 1.364036 132.349 83.66 3874.381 132.349 + -3.7545e-08 0.0 3.66005137333e-08 1.362302 131.961 83.26 3879.387 131.961 + -2.481825e-07 0.0 3.65509447333e-08 1.360457 131.587 82.83 3884.391 131.587 + -2.372372e-07 0.0 3.65012414e-08 1.358607 131.203 82.42 3889.391 131.203 + -2.411216e-07 0.0 3.64521291333e-08 1.356779 130.825 81.99 3894.404 130.825 + -2.25183e-07 0.0 3.64036616667e-08 1.354975 130.425 81.59 3899.417 130.425 + -2.257887e-07 0.0 3.63562151333e-08 1.353209 130.036 81.17 3904.43 130.036 + -2.38752e-07 0.0 3.63045505333e-08 1.351286 129.629 80.72 3909.438 129.629 + -2.264449e-07 0.0 3.62590384e-08 1.349592 129.238 80.3 3914.448 129.238 + -2.371386e-07 0.0 3.62103291333e-08 1.347779 128.845 79.88 3919.45 128.845 + -2.283122e-07 0.0 3.61606795333e-08 1.345931 128.482 79.47 3924.46 128.482 + -2.490936e-07 0.0 3.61107612667e-08 1.344073 128.101 79.07 3929.467 128.101 + -2.428897e-07 0.0 3.60643894e-08 1.342347 127.707 78.65 3934.481 127.707 + -2.504057e-07 0.0 3.60106292e-08 1.340346 127.31 78.22 3939.493 127.31 + -2.50154e-07 0.0 3.59643110667e-08 1.338622 126.925 77.8 3944.493 126.925 + -2.389063e-07 0.0 3.59135599333e-08 1.336733 126.544 77.39 3949.501 126.544 + -2.38705e-07 0.0 3.58653611333e-08 1.334939 126.143 76.99 3954.501 126.143 + -2.375955e-07 0.0 3.58155503333e-08 1.333085 125.755 76.57 3959.51 125.755 + -2.361335e-07 0.0 3.5765041e-08 1.331205 125.364 76.16 3964.513 125.364 + -2.508114e-07 0.0 3.57150958667e-08 1.329346 124.986 75.74 3969.514 124.986 + -2.496016e-07 0.0 3.56651776e-08 1.327488 124.588 75.33 3974.522 124.588 + -2.424393e-07 0.0 3.56145608e-08 1.325604 124.195 74.91 3979.534 124.195 + -2.569667e-07 0.0 3.55621170667e-08 1.323652 123.781 74.48 3984.547 123.781 + -2.518221e-07 0.0 3.55127361333e-08 1.321814 123.385 74.08 3989.552 123.385 + -2.498553e-07 0.0 3.5464806e-08 1.32003 122.992 73.65 3994.556 122.992 + -2.612045e-07 0.0 3.54086009333e-08 1.317938 122.595 73.24 3999.564 122.595 + -2.590363e-07 0.0 3.53603752667e-08 1.316143 122.184 72.82 4004.565 122.184 + -2.568678e-07 0.0 3.5309651e-08 1.314255 121.784 72.41 4009.565 121.784 + -2.549514e-07 0.0 3.525847e-08 1.31235 121.393 72.01 4014.578 121.393 + -2.568683e-07 0.0 3.52078532e-08 1.310466 121.002 71.58 4019.592 121.002 + -2.700335e-07 0.0 3.51579349333e-08 1.308608 120.591 71.16 4024.6 120.591 + -2.725557e-07 0.0 3.51029657333e-08 1.306562 120.181 70.73 4029.601 120.181 + -2.845612e-07 0.0 3.50752662e-08 1.305531 119.782 70.32 4034.61 119.782 + -2.827456e-07 0.0 3.50219358667e-08 1.303546 119.386 69.92 4039.62 119.386 + -2.871342e-07 0.0 3.49730654e-08 1.301727 118.972 69.49 4044.621 118.972 + -2.871345e-07 0.0 3.49226904e-08 1.299852 118.57 69.08 4049.631 118.57 + -2.887489e-07 0.0 3.48647927333e-08 1.297697 118.155 68.65 4054.648 118.155 + -2.899599e-07 0.0 3.48137729333e-08 1.295798 117.738 68.22 4059.649 117.738 + -2.9677e-07 0.0 3.47616247333e-08 1.293857 117.34 67.81 4064.652 117.34 + -2.993431e-07 0.0 3.47123244e-08 1.292022 116.945 67.4 4069.652 116.945 + -2.989903e-07 0.0 3.46553133333e-08 1.2899 116.535 66.96 4074.658 116.535 + -2.963171e-07 0.0 3.46056637333e-08 1.288052 116.132 66.53 4079.667 116.132 + -3.092804e-07 0.0 3.45504258667e-08 1.285996 115.719 66.12 4084.676 115.719 + -2.992437e-07 0.0 3.44975522667e-08 1.284028 115.301 65.68 4089.683 115.301 + -2.908708e-07 0.0 3.44459414e-08 1.282107 114.89 65.26 4094.696 114.89 + -3.098363e-07 0.0 3.43874795333e-08 1.279931 114.467 64.82 4099.696 114.467 + -3.03431e-07 0.0 3.43397643333e-08 1.278155 114.034 64.38 4104.701 114.034 + -3.174539e-07 0.0 3.4283613e-08 1.276065 113.636 63.96 4109.702 113.636 + -3.257768e-07 0.0 3.42315991333e-08 1.274129 113.244 63.54 4114.708 113.244 + -3.292074e-07 0.0 3.41754746667e-08 1.27204 112.844 63.1 4119.713 112.844 + -3.199784e-07 0.0 3.41249116e-08 1.270158 112.443 62.67 4124.722 112.443 + -3.422718e-07 0.0 3.40699424e-08 1.268112 112.034 62.23 4129.725 112.034 + -3.347063e-07 0.0 3.40136836e-08 1.266018 111.622 61.8 4134.737 111.622 + -3.357156e-07 0.0 3.39587144e-08 1.263972 111.189 61.36 4139.743 111.189 + -3.359179e-07 0.0 3.39032347333e-08 1.261907 110.799 60.91 4144.749 110.799 + -3.29462e-07 0.0 3.38498506667e-08 1.25992 110.373 60.48 4149.75 110.373 + -3.413155e-07 0.0 3.37964934667e-08 1.257934 109.956 60.05 4154.754 109.956 + -3.393997e-07 0.0 3.37383808667e-08 1.255771 109.546 59.59 4159.767 109.546 + -3.380379e-07 0.0 3.36852923333e-08 1.253795 109.117 59.14 4164.771 109.117 + -3.40409e-07 0.0 3.36270185333e-08 1.251626 108.701 58.7 4169.772 108.701 + -3.276985e-07 0.0 3.35719956e-08 1.249578 108.264 58.24 4174.772 108.264 + -3.30927e-07 0.0 3.35179398667e-08 1.247566 107.844 57.8 4179.782 107.844 + -3.438402e-07 0.0 3.34602034e-08 1.245417 107.409 57.34 4184.787 107.409 + -3.504484e-07 0.0 3.34047506e-08 1.243353 106.983 56.89 4189.791 106.983 + -3.6205e-07 0.0 3.33491097333e-08 1.241282 106.543 56.43 4194.799 106.543 + -3.519626e-07 0.0 3.32949734e-08 1.239267 106.108 55.97 4199.806 106.108 + 2.89279e-07 0.0 3.32362697333e-08 1.237082 105.686 55.5 4204.816 105.686 + 3.284705e-07 0.0 3.31812736667e-08 1.235035 105.242 55.05 4209.82 105.242 + 3.32102e-07 0.0 3.31199639333e-08 1.232753 104.818 54.57 4214.829 104.818 + 3.296338e-07 0.0 3.30648066667e-08 1.2307 104.383 54.12 4219.842 104.383 + 3.38711e-07 0.0 3.30045178667e-08 1.228456 103.932 53.64 4224.85 103.932 + 3.289228e-07 0.0 3.29507308e-08 1.226454 103.488 53.15 4229.851 103.488 + 3.314946e-07 0.0 3.28915166667e-08 1.22425 103.062 52.68 4234.857 103.062 + 3.408756e-07 0.0 3.28331354e-08 1.222077 102.617 52.22 4239.867 102.617 + 3.236253e-07 0.0 3.27759631333e-08 1.219949 102.152 51.72 4244.878 102.152 + 3.336621e-07 0.0 3.27163728667e-08 1.217731 101.707 51.22 4249.889 101.707 + 3.316947e-07 0.0 3.26569706667e-08 1.21552 101.261 50.74 4254.902 101.261 + 3.698268e-07 0.0 3.25964938e-08 1.213269 100.802 50.24 4259.908 100.802 + 3.735085e-07 0.0 3.25383006e-08 1.211103 100.346 49.76 4264.91 100.346 + 3.514151e-07 0.0 3.24792745333e-08 1.208906 99.889 49.25 4269.92 99.889 + 3.560552e-07 0.0 3.24190126e-08 1.206663 99.414 48.76 4274.929 99.414 + 3.650331e-07 0.0 3.23609e-08 1.2045 98.931 48.24 4279.94 98.931 + 3.576175e-07 0.0 3.23006918e-08 1.202259 98.393 47.67 4284.95 98.393 + 3.486392e-07 0.0 3.22729922667e-08 1.201228 98.219 47.49 4287.212 98.219 + 3.605427e-07 0.0 3.22162498667e-08 1.199116 97.713 46.96 4292.215 97.713 + 3.50757e-07 0.0 3.21898936667e-08 1.198135 97.528 46.75 4294.222 97.528 + 3.655863e-07 0.0 3.21278854e-08 1.195827 97.068 46.25 4299.224 97.068 + 3.470742e-07 0.0 3.20673816667e-08 1.193575 96.581 45.71 4304.229 96.581 + 3.467712e-07 0.0 3.20100213333e-08 1.19144 96.127 45.2 4309.237 96.127 + 3.568585e-07 0.0 3.19511296e-08 1.189248 95.658 44.69 4314.247 95.658 + 3.613976e-07 0.0 3.18851988e-08 1.186794 95.176 44.15 4319.256 95.176 + 3.399601e-07 0.0 3.18263070667e-08 1.184602 94.698 43.6 4324.262 94.698 + 3.433393e-07 0.0 3.17653734667e-08 1.182334 94.233 43.07 4329.263 94.233 + 3.471217e-07 0.0 3.17019412667e-08 1.179973 93.747 42.51 4334.267 93.747 + 3.32847e-07 0.0 3.1642351e-08 1.177755 93.266 41.98 4339.269 93.266 + 3.382438e-07 0.0 3.15791606e-08 1.175403 92.773 41.41 4344.274 92.773 + 3.451533e-07 0.0 3.15177434e-08 1.173117 92.283 40.83 4349.275 92.283 + 3.299705e-07 0.0 3.14524574e-08 1.170687 91.81 40.28 4354.282 91.81 + 3.257833e-07 0.0 3.13910133333e-08 1.1684 91.322 39.72 4359.292 91.322 + 3.423779e-07 0.0 3.13271781333e-08 1.166024 90.845 39.13 4364.301 90.845 + 3.3123e-07 0.0 3.12640683333e-08 1.163675 90.336 38.56 4369.313 90.336 + 3.179134e-07 0.0 3.12410436e-08 1.162818 90.144 38.32 4371.165 90.144 + 3.205865e-07 0.0 3.11767516667e-08 1.160425 89.651 37.71 4376.165 89.651 + 3.335992e-07 0.0 3.1112003e-08 1.158015 89.165 37.13 4381.167 89.165 + 3.258817e-07 0.0 3.10485170667e-08 1.155652 88.659 36.5 4386.168 88.659 + 3.254275e-07 0.0 3.10240415333e-08 1.154741 88.456 36.25 4388.219 88.456 + 3.195763e-07 0.0 3.09617646e-08 1.152423 87.956 35.64 4393.224 87.956 + 3.276969e-07 0.0 3.09331784667e-08 1.151359 87.75 35.39 4395.097 87.75 + 3.359178e-07 0.0 3.08706597333e-08 1.149032 87.242 34.73 4400.102 87.242 + 3.287553e-07 0.0 3.08447334e-08 1.148067 87.051 34.5 4402.175 87.051 + 3.159428e-07 0.0 3.07777279333e-08 1.145573 86.55 33.83 4407.207 86.55 + 3.294099e-07 0.0 3.07533867333e-08 1.144667 86.339 33.59 4409.066 86.339 + 3.238107e-07 0.0 3.06889873333e-08 1.14227 85.827 32.91 4414.072 85.827 + 3.316286e-07 0.0 3.06635983333e-08 1.141325 85.607 32.63 4416.049 85.607 + 3.302663e-07 0.0 3.05955988e-08 1.138794 85.076 31.93 4421.05 85.076 + 3.209348e-07 0.0 3.05657768e-08 1.137684 84.878 31.68 4423.217 84.878 + 3.24009e-07 0.0 3.05027207333e-08 1.135337 84.36 30.97 4428.229 84.36 + 3.147803e-07 0.0 3.04738390667e-08 1.134262 84.161 30.67 4430.21 84.161 + 3.248175e-07 0.0 3.04061888e-08 1.131744 83.627 29.97 4435.216 83.627 + 3.156875e-07 0.0 3.03817938667e-08 1.130836 83.428 29.67 4437.141 83.428 + 3.149808e-07 0.0 3.03146540667e-08 1.128337 82.876 28.9 4442.15 82.876 + 3.201256e-07 0.0 3.02888889333e-08 1.127378 82.674 28.63 4444.148 82.674 + 3.164931e-07 0.0 3.02205938667e-08 1.124836 82.136 27.84 4449.149 82.136 + 3.159885e-07 0.0 3.01944794667e-08 1.123864 81.917 27.51 4451.114 81.917 + 3.143743e-07 0.0 3.01270172667e-08 1.121353 81.391 26.71 4456.122 81.391 + 3.131632e-07 0.0 3.00962012e-08 1.120206 81.176 26.43 4458.217 81.176 + 3.05042e-07 0.0 3.00270464e-08 1.117632 80.626 25.6 4463.217 80.626 + 3.069079e-07 0.0 2.99998036e-08 1.116618 80.423 25.3 4465.197 80.423 + 3.047387e-07 0.0 2.99303264e-08 1.114032 79.877 24.45 4470.217 79.877 + 3.028721e-07 0.0 2.99046418667e-08 1.113076 79.656 24.15 4472.065 79.656 + 3.02216e-07 0.0 2.98341706e-08 1.110453 79.096 23.27 4477.073 79.096 + 3.024174e-07 0.0 2.98081368e-08 1.109484 78.856 22.92 4479.021 78.856 + 3.271832e-07 0.0 2.97368058e-08 1.106829 78.283 22.05 4484.034 78.283 + 3.244591e-07 0.0 2.97069569333e-08 1.105718 78.055 21.74 4486.153 78.055 + 3.200194e-07 0.0 2.96323482e-08 1.102941 77.451 20.85 4491.167 77.451 + 3.28443e-07 0.0 2.96063144e-08 1.101972 77.246 20.5 4493.238 77.246 + 3.19968e-07 0.0 2.95332370667e-08 1.099252 76.661 19.68 4498.248 76.661 + 3.219349e-07 0.0 2.95055912667e-08 1.098223 76.435 19.337 4500.116 76.435 + 3.106362e-07 0.0 2.94321109333e-08 1.095488 75.831 18.514 4505.122 75.831 + 3.178992e-07 0.0 2.94033098667e-08 1.094416 75.6 18.196 4507.174 75.6 + 3.045318e-07 0.0 2.93289160667e-08 1.091647 75.005 17.48 4512.185 75.005 + 3.029684e-07 0.0 2.93015389333e-08 1.090628 74.755 17.193 4514.109 74.755 + 3.124502e-07 0.0 2.92261242e-08 1.087821 74.143 16.558 4519.11 74.143 + 3.117942e-07 0.0 2.91956842667e-08 1.086688 73.905 16.31 4521.149 73.905 + 2.859181e-07 0.0 2.912481e-08 1.08405 73.278 15.746 4526.151 73.278 + 2.703318e-07 0.0 2.90956596667e-08 1.082965 73.049 15.567 4528.226 73.049 + 2.716933e-07 0.0 2.90208091333e-08 1.080179 72.418 15.094 4533.237 72.418 + 2.743666e-07 0.0 2.89901811333e-08 1.079039 72.184 14.92 4535.261 72.184 + 2.596375e-07 0.0 2.89181516e-08 1.076358 71.551 14.522 4540.274 71.551 + 2.538907e-07 0.0 2.88910162667e-08 1.075348 71.311 14.393 4542.194 71.311 + 2.531301e-07 0.0 2.88151716667e-08 1.072525 70.691 14.049 4547.198 70.691 + 2.615534e-07 0.0 2.87876333333e-08 1.0715 70.462 13.935 4549.224 70.462 + 2.390565e-07 0.0 2.87145022667e-08 1.068778 69.841 13.634 4554.233 69.841 + 2.518176e-07 0.0 2.86857012e-08 1.067706 69.584 13.52 4556.111 69.584 + 2.513121e-07 0.0 2.86130537333e-08 1.065002 68.973 13.262 4561.116 68.973 + 2.465206e-07 0.0 2.85828824667e-08 1.063879 68.718 13.153 4563.076 68.718 + 2.444519e-07 0.0 2.85114171333e-08 1.061219 68.095 12.896 4568.078 68.095 + 2.389536e-07 0.0 2.84782368e-08 1.059984 67.861 12.807 4570.204 67.861 + 2.307316e-07 0.0 2.84094312667e-08 1.057423 67.213 12.555 4575.207 67.213 + 2.280072e-07 0.0 2.83764658667e-08 1.056196 66.978 12.455 4577.261 66.978 + 2.163557e-07 0.0 2.83078215333e-08 1.053641 66.336 12.215 4582.263 66.336 + 2.142368e-07 0.0 2.82782682e-08 1.052541 66.113 12.133 4584.259 66.113 + 2.070231e-07 0.0 2.82080387333e-08 1.049927 65.476 11.894 4589.268 65.476 + 1.96985e-07 0.0 2.81748046667e-08 1.04869 65.232 11.796 4591.244 65.232 + 2.063669e-07 0.0 2.81080947333e-08 1.046207 64.592 11.578 4596.245 64.592 + 1.893681e-07 0.0 2.80790787333e-08 1.045127 64.351 11.483 4598.219 64.351 + 1.801843e-07 0.0 2.80112135333e-08 1.042601 64.548 11.261 4603.221 64.548 + 1.836672e-07 0.0 2.79388347333e-08 1.039907 63.905 11.041 4608.225 63.905 + 1.767569e-07 0.0 2.79096038e-08 1.038819 63.646 10.949 4610.181 63.646 + 1.738309e-07 0.0 2.78416311333e-08 1.036289 63.01 10.731 4615.195 63.01 + 1.7605e-07 0.0 2.78128569333e-08 1.035218 62.765 10.638 4617.196 62.765 + 1.725187e-07 0.0 2.77413378667e-08 1.032556 62.121 10.425 4622.201 62.121 + 1.704e-07 0.0 2.77169698e-08 1.031649 61.871 10.336 4624.194 61.871 + 1.586469e-07 0.0 2.76479493333e-08 1.02908 61.224 10.137 4629.206 61.224 + 1.506775e-07 0.0 2.76211901333e-08 1.028084 60.981 10.05 4631.184 60.981 + 1.362508e-07 0.0 2.75526801333e-08 1.025534 60.325 9.844 4636.191 60.325 + 1.51786e-07 0.0 2.75255716667e-08 1.024525 60.084 9.77 4638.217 60.084 + 1.176883e-07 0.0 2.74588617333e-08 1.022042 59.407 9.588 4643.22 59.407 + 1.106263e-07 0.0 2.74287710667e-08 1.020922 59.166 9.526 4645.203 59.166 + 1.060863e-07 0.0 2.73632432667e-08 1.018483 58.526 9.37 4650.208 58.526 + 9.05506e-08 0.0 2.73404066e-08 1.017633 58.273 9.304 4652.171 58.273 + 9.2719e-08 0.0 2.72673024e-08 1.014912 57.613 9.188 4657.183 57.613 + 8.21769e-08 0.0 2.72464001333e-08 1.014134 57.348 9.133 4659.048 57.348 + 8.76239e-08 0.0 2.71785080667e-08 1.011607 56.698 9.043 4664.049 56.698 + 8.03604e-08 0.0 2.71493577333e-08 1.010522 56.44 9.005 4666.231 56.44 + 7.61228e-08 0.0 2.70825403333e-08 1.008035 55.777 8.935 4671.238 55.777 + 8.32346e-08 0.0 2.70587364667e-08 1.007149 55.534 8.906 4673.22 55.534 + 7.4609e-08 0.0 2.69943639333e-08 1.004753 54.871 8.863 4678.226 54.871 + 6.44705e-08 0.0 2.69689480667e-08 1.003807 54.622 8.855 4680.206 54.622 + 6.41672e-08 0.0 2.6905489e-08 1.001445 53.952 8.819 4685.207 53.952 + 5.301981e-08 0.0 2.68769297333e-08 1.000382 53.697 8.814 4687.212 53.697 + 6.472133e-08 0.0 2.681067922e-08 0.9979161 53.038 8.812 4692.215 53.038 + 4.560447e-08 0.0 2.679155284e-08 0.9972042 52.77 8.813 4694.167 52.77 + 2.900926e-08 0.0 2.67282281067e-08 0.9948472 52.107 8.813 4699.177 52.107 + 3.324601e-08 0.0 2.670456126e-08 0.9939663 51.834 8.811 4701.114 51.834 + 2.683969e-08 0.0 2.66433589933e-08 0.9916883 51.182 8.823 4706.121 51.182 + 4.44417e-09 0.0 2.66178625267e-08 0.9907393 50.919 8.829 4708.108 50.919 + -1.537916e-08 0.0 2.65580922533e-08 0.9885146 50.23 8.845 4713.121 50.23 + -1.714473e-08 0.0 2.653242384e-08 0.9875592 49.984 8.856 4715.192 49.984 + -2.213876e-08 0.0 2.64737416667e-08 0.985375 49.313 8.885 4720.201 49.313 + -4.05495e-08 0.0 2.64518372733e-08 0.9845597 49.064 8.896 4722.209 49.064 + -6.289482e-08 0.0 2.63917634067e-08 0.9823237 48.398 8.922 4727.221 48.398 + -7.394135e-08 0.0 2.63739830467e-08 0.9816619 48.123 8.928 4729.101 48.123 + -1.045084e-07 0.0 2.63163003133e-08 0.9795149 47.477 8.971 4734.112 47.477 + -1.066773e-07 0.0 2.629481504e-08 0.9787152 47.21 8.978 4736.081 47.21 + -1.300315e-07 0.0 2.62375594867e-08 0.9765841 46.541 9.005 4741.081 46.541 + -1.510652e-07 0.0 2.62143816133e-08 0.9757214 46.293 9.02 4743.199 46.293 + -1.814304e-07 0.0 2.61628594067e-08 0.9738037 45.613 9.049 4748.21 45.613 + -1.824393e-07 0.0 2.61429538933e-08 0.9730628 45.359 9.054 4750.242 45.359 + -2.113416e-07 0.0 2.609060688e-08 0.9711144 44.688 9.089 4755.243 44.688 + -2.114429e-07 0.0 2.60681167933e-08 0.9702773 44.438 9.101 4757.213 44.438 + -2.316191e-07 0.0 2.60176585067e-08 0.9683992 43.784 9.133 4762.225 43.784 + -2.239525e-07 0.0 2.59991608067e-08 0.9677107 43.512 9.14 4764.109 43.512 + -2.319729e-07 0.0 2.59468943933e-08 0.9657653 42.847 9.169 4769.113 42.847 + -2.203213e-07 0.0 2.592723874e-08 0.9650337 42.584 9.181 4771.12 42.584 + -2.10285e-07 0.0 2.58814713733e-08 0.9633302 41.898 9.205 4776.132 41.898 + -1.969683e-07 0.0 2.58635593667e-08 0.9626635 41.635 9.215 4778.179 41.635 + -1.682682e-07 0.0 2.581509996e-08 0.9608598 40.959 9.238 4783.187 40.959 + -1.812819e-07 0.0 2.57990928e-08 0.960264 40.668 9.254 4785.071 40.668 + -1.498078e-07 0.0 2.575140178e-08 0.9584889 39.989 9.279 4790.073 39.989 + -1.419898e-07 0.0 2.57324661533e-08 0.9577841 39.719 9.284 4792.173 39.719 + -1.174763e-07 0.0 2.56883618333e-08 0.9561425 39.017 9.302 4797.191 39.017 + -1.257486e-07 0.0 2.567131762e-08 0.9555081 38.734 9.314 4799.117 38.734 + -1.206545e-07 0.0 2.56250908333e-08 0.9537875 38.017 9.325 4804.12 38.017 + -1.159637e-07 0.0 2.56083743933e-08 0.9531653 37.742 9.327 4806.206 37.742 + -1.22622e-07 0.0 2.556913294e-08 0.9517047 37.009 9.358 4811.206 37.009 + -1.273058e-07 0.0 2.55500334267e-08 0.9509938 36.741 9.361 4813.186 36.741 + -1.344254e-07 0.0 2.550989194e-08 0.9494997 35.993 9.373 4818.186 35.993 + -1.305921e-07 0.0 2.54967514533e-08 0.9490106 35.706 9.377 4820.198 35.706 + -1.38007e-07 0.0 2.545933156e-08 0.9476178 34.941 9.395 4825.2 34.941 + -1.465316e-07 0.0 2.544071296e-08 0.9469248 34.652 9.399 4827.233 34.652 + -1.553588e-07 0.0 2.54068797667e-08 0.9456655 33.882 9.419 4832.243 33.882 + -1.644382e-07 0.0 2.53913562067e-08 0.9450877 33.572 9.422 4834.117 33.572 + -1.738706e-07 0.0 2.53566417867e-08 0.9437956 32.816 9.418 4839.118 32.816 + -1.76645e-07 0.0 2.53427785867e-08 0.9432796 32.489 9.419 4841.083 32.489 + -1.76948e-07 0.0 2.53090824133e-08 0.9420254 31.71 9.443 4846.085 31.71 + -1.878935e-07 0.0 2.52930430133e-08 0.9414284 31.393 9.444 4848.122 31.393 + -2.03274e-07 0.0 2.526473898e-08 0.9403749 30.582 9.445 4853.126 30.582 + -2.056487e-07 0.0 2.524994082e-08 0.9398241 30.263 9.449 4855.102 30.263 + -2.192172e-07 0.0 2.522271414e-08 0.9388107 29.413 9.461 4860.114 29.413 + -2.331386e-07 0.0 2.521053548e-08 0.9383574 29.089 9.461 4862.219 29.089 + -2.320292e-07 0.0 2.51835962733e-08 0.9373547 28.236 9.468 4867.223 28.236 + -2.338954e-07 0.0 2.51726427333e-08 0.936947 27.899 9.474 4869.179 27.899 + -2.442879e-07 0.0 2.514653908e-08 0.9359754 27.022 9.478 4874.183 27.022 + -2.558369e-07 0.0 2.51417003933e-08 0.9357953 26.673 9.477 4876.209 26.673 + -2.664801e-07 0.0 2.511758756e-08 0.9348978 25.784 9.485 4881.209 25.784 + -2.674384e-07 0.0 2.51133103867e-08 0.9347386 25.427 9.49 4883.222 25.427 + -2.807045e-07 0.0 2.508889396e-08 0.9338298 24.48 9.498 4888.232 24.48 + -2.884216e-07 0.0 2.50829832933e-08 0.9336098 24.145 9.497 4890.224 24.145 + -2.939702e-07 0.0 2.50644506667e-08 0.93292 23.231 9.5 4895.227 23.231 + -3.03907e-07 0.0 2.50591310667e-08 0.932722 22.911 9.505 4897.207 22.911 + -3.209057e-07 0.0 2.504369348e-08 0.9321474 22.016 9.519 4902.208 22.016 + -3.042604e-07 0.0 2.50433845133e-08 0.9321359 21.664 9.517 4904.114 21.664 + -3.269584e-07 0.0 2.50290404e-08 0.931602 20.793 9.517 4909.12 20.793 + -3.224188e-07 0.0 2.50284170933e-08 0.9315788 20.436 9.522 4911.095 20.436 + -3.217632e-07 0.0 2.50182426867e-08 0.9312001 19.563 9.543 4916.097 19.563 + -3.141467e-07 0.0 2.50126329267e-08 0.9309913 19.261 9.546 4918.22 19.261 + -3.153067e-07 0.0 2.50081139533e-08 0.9308231 18.426 9.55 4923.225 18.426 + -3.147521e-07 0.0 2.50064831467e-08 0.9307624 18.093 9.557 4925.217 18.093 + -3.146006e-07 0.0 2.500157998e-08 0.9305799 17.267 9.569 4930.228 17.267 + -2.919026e-07 0.0 2.50003011267e-08 0.9305323 17.025 9.574 4932.193 17.025 + -2.960389e-07 0.0 2.49971174267e-08 0.9304138 16.188 9.586 4937.203 16.188 + -2.847402e-07 0.0 2.49955564733e-08 0.9303557 15.961 9.597 4939.221 15.961 + -2.572508e-07 0.0 2.49919993267e-08 0.9302233 15.277 9.617 4944.23 15.277 + -2.426232e-07 0.0 2.49912363133e-08 0.9301949 15.019 9.627 4946.151 15.019 + -2.061045e-07 0.0 2.49920503733e-08 0.9302252 14.426 9.638 4951.16 14.426 + -1.947556e-07 0.0 2.499040076e-08 0.9301638 14.165 9.64 4953.142 14.165 + -1.355391e-07 0.0 2.49904356867e-08 0.9301651 13.553 9.664 4958.153 13.553 + -1.391204e-07 0.0 2.49890010067e-08 0.9301117 13.334 9.675 4960.2 13.334 + -8.656208e-08 0.0 2.49895813267e-08 0.9301333 12.976 9.698 4965.208 12.976 + -5.105237e-08 0.0 2.49868731667e-08 0.9300325 12.47 9.712 4970.219 12.47 + -3.39028e-08 0.0 2.49862713533e-08 0.9300101 12.292 9.719 4972.202 12.292 + 8.66849e-09 0.0 2.498826486e-08 0.9300843 11.866 9.74 4977.203 11.866 + 5.971362e-08 0.0 2.49872896e-08 0.930048 11.514 9.757 4982.204 11.514 + 8.957415e-08 0.0 2.49878027533e-08 0.9300671 11.247 9.782 4987.214 11.247 + 1.068244e-07 0.0 2.49870585467e-08 0.9300394 11.036 9.792 4992.214 11.036 + 1.216538e-07 0.0 2.49884690467e-08 0.9300919 10.893 9.817 4997.227 10.893 + 1.213008e-07 0.0 2.49873675133e-08 0.9300509 10.792 9.84 5002.236 10.792 + 1.229654e-07 0.0 2.49870128733e-08 0.9300377 10.717 9.85 5007.246 10.717 + 1.182237e-07 0.0 2.49886947267e-08 0.9301003 10.673 9.872 5012.257 10.673 + 9.814886e-08 0.0 2.498873234e-08 0.9301017 10.643 9.895 5017.266 10.643 + 7.555174e-08 0.0 2.49875260267e-08 0.9300568 10.616 9.896 5022.271 10.616 + 5.522446e-08 0.0 2.49904840467e-08 0.9301669 10.604 9.907 5027.274 10.604 + 4.750723e-08 0.0 2.49897747667e-08 0.9301405 10.601 9.927 5032.278 10.601 + 2.980284e-08 0.0 2.49886651733e-08 0.9300992 10.591 9.935 5037.286 10.591 + 1.391424e-08 0.0 2.498681406e-08 0.9300303 10.588 9.94 5042.297 10.588 + -1.634976e-08 0.0 2.49862041867e-08 0.9300076 10.582 9.95 5047.304 10.582 + -2.744648e-08 0.0 2.498953834e-08 0.9301317 10.576 9.95 5052.312 10.576 + -4.651279e-08 0.0 2.49889419e-08 0.9301095 10.577 9.958 5057.324 10.577 + -6.628523e-08 0.0 2.498790216e-08 0.9300708 10.578 9.968 5062.332 10.578 + -8.217377e-08 0.0 2.49909622733e-08 0.9301847 10.572 9.966 5067.341 10.572 + -1.100166e-07 0.0 2.49867065933e-08 0.9300263 10.571 9.971 5072.346 10.571 + -1.152086e-07 0.0 2.49879800733e-08 0.9300737 10.566 9.971 5077.357 10.566 + -1.337234e-07 0.0 2.49900246267e-08 0.9301498 10.57 9.98 5082.366 10.57 + -1.458288e-07 0.0 2.49879317133e-08 0.9300719 10.569 9.985 5087.368 10.569 + -1.623732e-07 0.0 2.49867952533e-08 0.9300296 10.564 9.984 5092.373 10.564 + -1.738736e-07 0.0 2.49875287133e-08 0.9300569 10.558 9.984 5097.376 10.558 + -1.928389e-07 0.0 2.49897479e-08 0.9301395 10.558 9.983 5102.389 10.558 + -2.03179e-07 0.0 2.49874158733e-08 0.9300527 10.556 9.99 5107.397 10.556 + -1.967734e-07 0.0 2.498767648e-08 0.9300624 10.558 9.99 5112.4 10.558 + -2.132166e-07 0.0 2.49881708267e-08 0.9300808 10.551 9.99 5117.408 10.551 + -2.182604e-07 0.0 2.49879344e-08 0.930072 10.555 9.997 5122.421 10.555 + -2.259275e-07 0.0 2.49879290267e-08 0.9300718 10.546 9.991 5127.422 10.546 + -2.272389e-07 0.0 2.49886087533e-08 0.9300971 10.541 9.988 5132.429 10.541 + -2.411099e-07 0.0 2.498683018e-08 0.9300309 10.549 10.001 5137.441 10.549 + -2.473141e-07 0.0 2.498856308e-08 0.9300954 10.542 9.999 5142.446 10.542 + -2.508952e-07 0.0 2.49889714533e-08 0.9301106 10.534 9.993 5147.454 10.534 + -2.626978e-07 0.0 2.49905968867e-08 0.9301711 10.539 9.994 5152.465 10.539 + -2.592684e-07 0.0 2.49884690467e-08 0.9300919 10.538 10.007 5157.47 10.538 + -2.772754e-07 0.0 2.499008642e-08 0.9301521 10.528 9.989 5162.471 10.528 + -2.82521e-07 0.0 2.49890628e-08 0.930114 10.534 10.001 5167.481 10.534 + -2.703147e-07 0.0 2.498873234e-08 0.9301017 10.526 9.999 5172.482 10.526 + -2.895825e-07 0.0 2.498875652e-08 0.9301026 10.521 9.989 5177.486 10.521 + -2.878701e-07 0.0 2.49874158733e-08 0.9300527 10.523 9.994 5182.493 10.523 + -2.833279e-07 0.0 2.49876146867e-08 0.9300601 10.524 10.0 5187.504 10.524 + -2.89633e-07 0.0 2.49858495467e-08 0.9299944 10.519 10.0 5192.511 10.519 + -2.898853e-07 0.0 2.498845024e-08 0.9300912 10.516 9.998 5197.518 10.516 + -2.729374e-07 0.0 2.49867603267e-08 0.9300283 10.512 9.992 5202.519 10.512 + -2.713235e-07 0.0 2.49886732333e-08 0.9300995 10.515 9.995 5207.52 10.515 + -2.701128e-07 0.0 2.49867630133e-08 0.9300284 10.511 9.999 5212.525 10.511 + -2.637575e-07 0.0 2.498897414e-08 0.9301107 10.499 9.987 5217.527 10.499 + -2.805036e-07 0.0 2.498816008e-08 0.9300804 10.509 9.997 5222.539 10.509 + -2.715252e-07 0.0 2.498853084e-08 0.9300942 10.504 9.996 5227.542 10.504 + -2.689023e-07 0.0 2.49876872267e-08 0.9300628 10.498 9.992 5232.542 10.498 + -2.716765e-07 0.0 2.49866474867e-08 0.9300241 10.499 9.993 5237.547 10.499 + -2.634548e-07 0.0 2.49875287133e-08 0.9300569 10.498 9.998 5242.557 10.498 + -2.612859e-07 0.0 2.49893341533e-08 0.9301241 10.488 9.988 5247.569 10.488 + -2.558383e-07 0.0 2.49881009733e-08 0.9300782 10.488 9.987 5252.582 10.488 + -2.474654e-07 0.0 2.49871069067e-08 0.9300412 10.489 9.993 5257.582 10.489 + -2.373771e-07 0.0 2.498835352e-08 0.9300876 10.485 9.993 5262.596 10.485 + -2.333925e-07 0.0 2.49862955333e-08 0.930011 10.481 9.99 5267.604 10.481 + -2.327876e-07 0.0 2.498633046e-08 0.9300123 10.483 9.99 5272.606 10.483 + -2.245151e-07 0.0 2.49874105e-08 0.9300525 10.484 9.995 5277.609 10.484 + -2.149821e-07 0.0 2.49884878533e-08 0.9300926 10.48 9.997 5282.616 10.48 + -2.069621e-07 0.0 2.49879881333e-08 0.930074 10.474 9.986 5287.62 10.474 + -1.91023e-07 0.0 2.49885523333e-08 0.930095 10.477 9.994 5292.621 10.477 + -1.795229e-07 0.0 2.49862364267e-08 0.9300088 10.468 9.989 5297.626 10.468 + -1.815403e-07 0.0 2.498745886e-08 0.9300543 10.471 9.994 5302.634 10.471 + -1.670135e-07 0.0 2.49885818867e-08 0.9300961 10.469 9.996 5307.644 10.469 + -1.596497e-07 0.0 2.498766842e-08 0.9300621 10.468 9.992 5312.657 10.468 + -1.498137e-07 0.0 2.49888989133e-08 0.9301079 10.466 9.994 5317.66 10.466 + -1.374054e-07 0.0 2.49884206867e-08 0.9300901 10.459 9.989 5322.664 10.459 + -1.222231e-07 0.0 2.49882997867e-08 0.9300856 10.461 9.99 5327.669 10.461 + -1.10521e-07 0.0 2.49872304933e-08 0.9300458 10.463 9.999 5332.671 10.463 + -8.21233e-08 0.0 2.498967536e-08 0.9301368 10.458 9.992 5337.674 10.458 + -8.277913e-08 0.0 2.49881762e-08 0.930081 10.463 10.0 5342.677 10.463 + -4.696672e-08 0.0 2.49887592067e-08 0.9301027 10.459 9.998 5347.688 10.459 + -2.679077e-08 0.0 2.49876979733e-08 0.9300632 10.455 9.996 5352.695 10.455 + -1.35251e-08 0.0 2.49858414867e-08 0.9299941 10.456 9.994 5357.696 10.456 + 9.72772e-09 0.0 2.49871337733e-08 0.9300422 10.448 9.995 5362.705 10.448 + 3.514935e-08 0.0 2.498699138e-08 0.9300369 10.449 9.992 5367.713 10.449 + 5.759515e-08 0.0 2.49882299333e-08 0.930083 10.451 9.994 5372.728 10.451 + 9.598e-08 0.0 2.498886936e-08 0.9301068 10.057 9.296 5377.738 10.057 + 1.234193e-07 0.0 2.49881762e-08 0.930081 11.118 11.633 5382.749 11.118 + 1.254873e-07 0.0 2.498708004e-08 0.9300402 11.234 11.61 5384.615 11.234 + 1.438981e-07 0.0 2.49859892533e-08 0.9299996 11.112 10.613 5389.629 11.112 + 1.497994e-07 0.0 2.49876872267e-08 0.9300628 11.693 12.579 5394.633 11.693 + 1.522709e-07 0.0 2.49868516733e-08 0.9300317 11.99 13.284 5396.617 11.99 + 1.597866e-07 0.0 2.49865373333e-08 0.93002 12.444 13.284 5401.629 12.444 + 1.63872e-07 0.0 2.49875448333e-08 0.9300575 12.061 10.635 5406.631 12.061 + 1.560539e-07 0.0 2.49864943467e-08 0.9300184 11.054 7.352 5411.64 11.054 + 1.641747e-07 0.0 2.49853417667e-08 0.9299755 10.691 6.885 5413.621 10.691 + 1.656373e-07 0.0 2.498574208e-08 0.9299904 10.122 7.358 5418.634 10.122 + 1.80164e-07 0.0 2.498730572e-08 0.9300486 10.022 7.649 5420.605 10.022 + 1.998356e-07 0.0 2.49876657333e-08 0.930062 9.7703 7.613 5425.611 9.7703 + 1.984739e-07 0.0 2.49861800067e-08 0.9300067 9.3796 7.27 5430.623 9.3796 + 2.063928e-07 0.0 2.49887430867e-08 0.9301021 8.9813 6.983 5435.63 8.9813 + 2.055858e-07 0.0 2.498867592e-08 0.9300996 8.1784 4.719 5440.63 8.1784 + 2.130005e-07 0.0 2.499007836e-08 0.9301518 7.6816 3.669 5442.608 7.6816 + 2.181458e-07 0.0 2.499217396e-08 0.9302298 6.4469 2.861 5447.616 6.4469 + 2.379179e-07 0.0 2.49931438467e-08 0.9302659 5.8954 2.647 5449.624 5.8954 + 2.42962e-07 0.0 2.49955564733e-08 0.9303557 5.4079 2.444 5451.61 5.4079 + 2.702488e-07 0.0 2.49959352933e-08 0.9303698 4.5426 2.08 5456.622 4.5426 + 2.783204e-07 0.0 2.49974156467e-08 0.9304249 4.0797 1.866 5458.603 4.0797 + 2.949659e-07 0.0 2.49975768467e-08 0.9304309 3.2459 1.639 5463.611 3.2459 + 2.968825e-07 0.0 2.49991646667e-08 0.93049 2.9695 1.572 5465.613 2.9695 + 3.138803e-07 0.0 2.49980362667e-08 0.930448 2.4582 1.461 5470.614 2.4582 + 2.963274e-07 0.0 2.49997826e-08 0.930513 2.2719 1.446 5472.608 2.2719 + 3.115599e-07 0.0 2.49988342067e-08 0.9304777 2.0802 1.435 5477.613 2.0802 + 2.961761e-07 0.0 2.49990383933e-08 0.9304853 1.8545 1.431 5482.616 1.8545 + 2.919389e-07 0.0 2.50002124667e-08 0.930529 1.7593 1.427 5487.618 1.7593 + 3.061125e-07 0.0 2.49993822867e-08 0.9304981 1.7054 1.425 5492.623 1.7054 + 2.977902e-07 0.0 2.50011554867e-08 0.9305641 1.6494 1.423 5497.635 1.6494 diff --git a/tests/Stoner/test_Core.py b/tests/Stoner/test_datafile.py similarity index 96% rename from tests/Stoner/test_Core.py rename to tests/Stoner/test_datafile.py index d4c7b444f..2f5c76c38 100755 --- a/tests/Stoner/test_Core.py +++ b/tests/Stoner/test_datafile.py @@ -288,10 +288,10 @@ def test_dir(): ] ) attrs = set(dir(selfd)) - bad_keys - assert len(attrs) == 247, "DataFile.__dir__ failed." + assert len(attrs) == 246, "DataFile.__dir__ failed." selfd.setas.clear() attrs = set(dir(selfd)) - bad_keys - assert len(attrs) == 245, "DataFile.__dir__ failed." + assert len(attrs) == 244, "DataFile.__dir__ failed." def test_filter(): diff --git a/tests/Stoner/test_FileFormats.py b/tests/Stoner/test_formats.py similarity index 87% rename from tests/Stoner/test_FileFormats.py rename to tests/Stoner/test_formats.py index 27500e4bb..ef7110f34 100755 --- a/tests/Stoner/test_FileFormats.py +++ b/tests/Stoner/test_formats.py @@ -10,10 +10,13 @@ import pathlib import io import urllib +from numpy import isclose from Stoner import Data, __homepath__, __datapath__, ImageFile -from Stoner.Core import DataFile from Stoner.compat import Hyperspy_ok +from Stoner.formats import load as new_load +from Stoner.core.exceptions import StonerLoadError +from Stoner.formats.decorators import get_saver import pytest @@ -21,7 +24,6 @@ from Stoner.formats.maximus import MaximusStack from Stoner.tools.classes import subclasses from Stoner.core.exceptions import StonerUnrecognisedFormat -from Stoner.formats.facilities import FabioImageFile pth = __homepath__ / ".." datadir = __datapath__ @@ -54,17 +56,23 @@ def list_files(): @pytest.mark.parametrize("filename", listed_files) def test_one_file(tmpdir, filename): loaded = Data(filename, debug=False) - assert isinstance(loaded, DataFile), f"Failed to load {filename.name} correctly." - if "save" in subclasses()[loaded["Loaded as"]].__dict__: + assert isinstance(loaded, Data), f"Failed to load {filename.name} correctly (old load code)." + new_loaded = new_load(filename) + assert isinstance(new_loaded, Data), f"Failed to load {filename.name} correctly (new load code)." + assert loaded==new_loaded,"Difference between old and new loaders!" + try: + new_saver=get_saver(loaded["Loaded as"]) pth = pathlib.Path(tmpdir) / filename.name _, name, ext = pth.parent, pth.stem, pth.suffix pth2 = pathlib.Path(tmpdir) / f"{name}-2{ext}" - loaded.save(pth, as_loaded=True) + new_saver(loaded,pth) assert pth.exists() or pathlib.Path(loaded.filename).exists(), f"Failed to save as {pth}" pathlib.Path(loaded.filename).unlink() loaded.save(pth2, as_loaded=loaded["Loaded as"]) assert pth2.exists() or pathlib.Path(loaded.filename).exists(), "Failed to save as {}".format(pth) pathlib.Path(loaded.filename).unlink() + except StonerLoadError: + pass def test_csvfile(): @@ -136,11 +144,6 @@ def test_maximus_stack(tmpdir): assert stack2.shape == stack.shape, "Round trip through MaximusStack" -def test_fail_to_load(): - with pytest.raises(StonerUnrecognisedFormat): - _ = Data(datadir / "TDMS_File.tdms_index") - - def test_arb_class_load(): _ = Data(datadir / "TDI_Format_RT.txt", filetype="dummy.ArbClass") @@ -183,8 +186,7 @@ def test_ImageAutoLoad(): def test_FabioImageFle(): - loader = FabioImageFile() - loader._load(datadir / "working" / "hydra_0017.edf") + loader = new_load(datadir / "working" / "hydra_0017.edf") assert loader.shape == (512, 768) diff --git a/tests/Stoner/test_Zip.py b/tests/Stoner/test_zipfile.py similarity index 100% rename from tests/Stoner/test_Zip.py rename to tests/Stoner/test_zipfile.py