Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP][StackBase] Stack save and load #95

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
109 changes: 108 additions & 1 deletion PyMca5/PyMcaCore/StackBase.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@

"""
from . import DataObject
from ..PyMcaIO.H5pyFileInstance import H5pyFileInstance
import numpy
import time
import os
Expand Down Expand Up @@ -67,7 +68,6 @@
PYMCA_PLUGINS_DIR = None
pass


class StackBase(object):
def __init__(self):
self._stack = DataObject.DataObject()
Expand Down Expand Up @@ -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
Expand Down
97 changes: 97 additions & 0 deletions PyMca5/PyMcaIO/H5pyFileInstance.py
Original file line number Diff line number Diff line change
@@ -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__ = "[email protected]"
__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()