diff --git a/PyMca5/PyMcaCore/StackBase.py b/PyMca5/PyMcaCore/StackBase.py index 9321dbe78..44111ce14 100644 --- a/PyMca5/PyMcaCore/StackBase.py +++ b/PyMca5/PyMcaCore/StackBase.py @@ -35,6 +35,7 @@ """ from . import DataObject +from ..PyMcaIO.H5pyFileInstance import H5pyFileInstance import numpy import time import os @@ -67,7 +68,6 @@ PYMCA_PLUGINS_DIR = None pass - class StackBase(object): def __init__(self): self._stack = DataObject.DataObject() @@ -1190,6 +1190,113 @@ def getPositionersFromIndex(self, index): return positionersAtIdx + def save(self, file_, mode="w"): + """Save stack data and metadata to a HDF5 file. + + :param file_: File name or h5py.File instance. + :param str mode: File access mode. Can be "w", "w-", + "a" or "r+". + """ + # see https://github.com/vasole/pymca/issues/92 + infodict = self.getStackInfo() + + with H5pyFileInstance(file_, mode) as h5f: + entry = h5f.create_group("stack") + entry.attrs["NX_class"] = numpy.string_("NXentry") + + # todo: definition (@version, @url) + definition = entry.create_dataset("definition", shape=tuple()) # FIXME: group or dataset? + # definition.attrs["url"] = ??? + definition.attrs["version"] = numpy.string_("0.0.1") + + # todo + # coord = entry.create_group("coordinates") + # coord.create_dataset("x", data=?) + # coord.create_dataset("y", data=?) + + signal = entry.create_group("signal") + signal.attrs["NX_class"] = numpy.string_("NX_data") + signal.attrs["signal"] = numpy.string_("data") + # signal.attrs["axes"] = numpy.array(["dim_0", "dim_1", "dim_2"], + # dtype=numpy.string_) + data = signal.create_dataset("data", data=self.getStackData()) + data.attrs["interpretation"] = numpy.string_("spectrum") + # signal.create_dataset("dim_0", data=TODO) + # signal.create_dataset("dim_1", data=TODO) + # signal.create_dataset("dim_2", data=TODO) + mca_index = infodict.get("McaIndex", 2) + signal.create_dataset("mca_index", data=mca_index) + mca_calibration = infodict.get("McaCalibration", [0., 1., 0.]) + signal.create_dataset("mca_calibration", data=mca_calibration) + # signal.create_dataset("elapsed_time", data=TODO) + # signal.create_dataset("live_time", data=TODO) + # signal.create_dataset("preset_time", data=TODO) + + if "positioners" in infodict or "counters" in infodict: + info = entry.create_group("info") + if "positioners" in infodict: + positionersdict = infodict["positioners"] + positioners = info.create_group("positioners") + for name, values in positionersdict.items(): + positioners.create_dataset(name, + data=values) + if "counters" in infodict: + countersdict = infodict["counters"] + counters = info.create_group("counters") + for name, values in countersdict.items(): + counters.create_dataset(name, + data=values) + # todo: images + # image_name + # data + # dim0 + # dim1 + + def load(self, file_): + """Load stack data and metadate from a HDF5 file.""" + self._clearPositioners() + with H5pyFileInstance(file_, "r") as h5f: + if len(h5f) != 1: + print("More than one entry in the root group. Cannot load the stack.") + return # TODO: or raise IOError + entry = list(h5f.values())[0] + if "definition" in entry and entry["definition"].attrs.get("version") == "0.0.1": + # simple case of data saved by PyMca + return self._load_pymca_stack_0_0_1(entry) + + # todo: general case parsing, files written by other programs + + def _load_pymca_stack_0_0_1(self, entry): + if "signal" not in entry: + print("Could not read PyMca 0.0.1 stack format. " + "Signal dataset not found.") + signal = entry["signal"] + mca_index_dataset = signal.get("mca_index", None) + mca_index = mca_index_dataset[()] if mca_index_dataset is not None else 2 + if mca_index == -1: + mca_index = 2 + self.setStack(signal["data"], mcaindex=mca_index) + + # it seems mca_index is not updated in setStack if a value already exists in .info + self._stack.info['McaIndex'] = mca_index + mca_calib = signal.get('mca_calibration') + if mca_calib is None: + self._stack.info['McaCalib'] = [0.0, 1.0, 0.0] + else: + self._stack.info['McaCalib'] = mca_calib[()] + + if "info" in entry: + info = entry.get("info") + if "positioners" in info: + self.setPositioners(info["positioners"]) + if "counters" in info: + self._stack.info["counters"] = {} + for name, values in info["counters"].items(): + self._stack.info["counters"][name] = values[()] + + self.stackUpdated() + + def test(): #create a dummy stack diff --git a/PyMca5/PyMcaIO/H5pyFileInstance.py b/PyMca5/PyMcaIO/H5pyFileInstance.py new file mode 100644 index 000000000..0144bd41a --- /dev/null +++ b/PyMca5/PyMcaIO/H5pyFileInstance.py @@ -0,0 +1,97 @@ +#/*########################################################################## +# +# The PyMca X-Ray Fluorescence Toolkit +# +# Copyright (c) 2004-2017 European Synchrotron Radiation Facility +# +# This file is part of the PyMca X-ray Fluorescence Toolkit developed at +# the ESRF by the Software group. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# +#############################################################################*/ +""" +Context manager to handle transparently either a h5py.File instances or a file +name. + +This provides a level of abstraction for functions to accept both a file path +or an already opened file object. + +Instead of writing:: + + def func(file_): + if isinstance(file_, h5py.File): + h5f = file_ + must_be_closed = False + else: + h5f = h5py.File(file_, "w") + must_be_closed = False + + # do some work with h5f... + + if must_be_closed: + h5f.close() + +you can write:: + + def func(file_): + with H5pyFileInstance(file_, "w") as h5f: + # do some work with h5f... + +""" +import h5py + +__author__ = "P.Knobel - ESRF Data Analysis" +__contact__ = "pierre.knobel@esrf.fr" +__license__ = "MIT" +__copyright__ = "European Synchrotron Radiation Facility, Grenoble, France" + + +class H5pyFileInstance(object): + """This class is a context manager returning a h5py.File instance. + + The constructor accepts either an already opened file object, or a + filename to be opened on entry and closed on exit. + + When providing a file name to the constructor, it is guaranteed that + the file we be closed on exiting the ``with`` block. + """ + def __init__(self, file_, mode="r"): + """ + + :param file_: Either a filename or a h5py.File instance + :param str mode: Mode in which to open file; one of ("w", "r", "r+", + "a", "w-"). Ignored if :param:`file_` is a h5py.File + instance. + """ + if not isinstance(file_, h5py.File): + # assume file_ is a valid path and let h5py.File raise errors if + # it isn't + self.file_obj = h5py.File(file_, mode) + self._must_be_closed = True + else: + self.file_obj = file_ + self._must_be_closed = False + + def __enter__(self): + return self.file_obj + + def __exit__(self, exc_type, exc_val, exc_tb): + if self._must_be_closed: + self.file_obj.close()