summaryrefslogtreecommitdiff
path: root/silx/io/nxdata
diff options
context:
space:
mode:
Diffstat (limited to 'silx/io/nxdata')
-rw-r--r--silx/io/nxdata/__init__.py64
-rw-r--r--silx/io/nxdata/_utils.py183
-rw-r--r--silx/io/nxdata/parse.py841
-rw-r--r--silx/io/nxdata/write.py202
4 files changed, 0 insertions, 1290 deletions
diff --git a/silx/io/nxdata/__init__.py b/silx/io/nxdata/__init__.py
deleted file mode 100644
index 796810f..0000000
--- a/silx/io/nxdata/__init__.py
+++ /dev/null
@@ -1,64 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2018 European Synchrotron Radiation Facility
-#
-# 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.
-#
-# ###########################################################################*/
-"""
-:mod:`nxdata`: NXdata parsing and validation
---------------------------------------------
-
-To parse an existing NXdata group, use :class:`NXdata`.
-
-Following functions help you check the validity of a existing NXdata group:
- - :func:`is_valid_nxdata`
- - :func:`is_NXentry_with_default_NXdata`
- - :func:`is_NXroot_with_default_NXdata`
-
-To help you write a NXdata group, you can use :func:`save_NXdata`.
-
-.. currentmodule:: silx.io.nxdata
-
-Classes
-+++++++
-
-.. autoclass:: NXdata
- :members:
-
-
-Functions
-+++++++++
-
-.. autofunction:: get_default
-
-.. autofunction:: is_valid_nxdata
-
-.. autofunction:: is_NXentry_with_default_NXdata
-
-.. autofunction:: is_NXroot_with_default_NXdata
-
-.. autofunction:: save_NXdata
-
-"""
-from .parse import NXdata, get_default, is_valid_nxdata, InvalidNXdataError, \
- is_NXentry_with_default_NXdata, is_NXroot_with_default_NXdata
-from ._utils import get_attr_as_unicode, get_attr_as_string, nxdata_logger
-from .write import save_NXdata
diff --git a/silx/io/nxdata/_utils.py b/silx/io/nxdata/_utils.py
deleted file mode 100644
index 077f01d..0000000
--- a/silx/io/nxdata/_utils.py
+++ /dev/null
@@ -1,183 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2017-2018 European Synchrotron Radiation Facility
-#
-# 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.
-#
-# ###########################################################################*/
-"""Utility functions used by NXdata validation and parsing."""
-
-import copy
-import numpy
-import logging
-
-from silx.io import is_dataset
-from silx.utils.deprecation import deprecated
-from silx.third_party import six
-
-
-__authors__ = ["P. Knobel"]
-__license__ = "MIT"
-__date__ = "17/04/2018"
-
-
-nxdata_logger = logging.getLogger("silx.io.nxdata")
-
-
-INTERPDIM = {"scalar": 0,
- "spectrum": 1,
- "image": 2,
- "rgba-image": 3, # "hsla-image": 3, "cmyk-image": 3, # TODO
- "vertex": 1} # 3D scatter: 1D signal + 3 axes (x, y, z) of same legth
-"""Number of signal dimensions associated to each possible @interpretation
-attribute.
-"""
-
-
-@deprecated(since_version="0.8.0", replacement="get_attr_as_unicode")
-def get_attr_as_string(*args, **kwargs):
- return get_attr_as_unicode(*args, **kwargs)
-
-
-def get_attr_as_unicode(item, attr_name, default=None):
- """Return item.attrs[attr_name] as unicode or as a
- list of unicode.
-
- Numpy arrays of strings or bytes returned by h5py are converted to
- lists of unicode.
-
- :param item: Group or dataset
- :param attr_name: Attribute name
- :param default: Value to be returned if attribute is not found.
- :return: item.attrs[attr_name]
- """
- attr = item.attrs.get(attr_name, default)
-
- if isinstance(attr, six.binary_type):
- # byte-string
- return attr.decode("utf-8")
- elif isinstance(attr, numpy.ndarray) and not attr.shape:
- if isinstance(attr[()], six.binary_type):
- # byte string as ndarray scalar
- return attr[()].decode("utf-8")
- else:
- # other scalar, possibly unicode
- return attr[()]
- elif isinstance(attr, numpy.ndarray) and len(attr.shape):
- if hasattr(attr[0], "decode"):
- # array of byte-strings
- return [element.decode("utf-8") for element in attr]
- else:
- # other array, most likely unicode objects
- return [element for element in attr]
- else:
- return copy.deepcopy(attr)
-
-
-def get_uncertainties_names(group, signal_name):
- # Test consistency of @uncertainties
- uncertainties_names = get_attr_as_unicode(group, "uncertainties")
- if uncertainties_names is None:
- uncertainties_names = get_attr_as_unicode(group[signal_name], "uncertainties")
- if isinstance(uncertainties_names, six.text_type):
- uncertainties_names = [uncertainties_names]
- return uncertainties_names
-
-
-def get_signal_name(group):
- """Return the name of the (main) signal in a NXdata group.
- Return None if this info is missing (invalid NXdata).
-
- """
- signal_name = get_attr_as_unicode(group, "signal", default=None)
- if signal_name is None:
- nxdata_logger.info("NXdata group %s does not define a signal attr. "
- "Testing legacy specification.", group.name)
- for key in group:
- if "signal" in group[key].attrs:
- signal_name = key
- signal_attr = group[key].attrs["signal"]
- if signal_attr in [1, b"1", u"1"]:
- # This is the main (default) signal
- break
- return signal_name
-
-
-def get_auxiliary_signals_names(group):
- """Return list of auxiliary signals names"""
- auxiliary_signals_names = get_attr_as_unicode(group, "auxiliary_signals",
- default=[])
- if isinstance(auxiliary_signals_names, (six.text_type, six.binary_type)):
- auxiliary_signals_names = [auxiliary_signals_names]
- return auxiliary_signals_names
-
-
-def validate_auxiliary_signals(group, signal_name, auxiliary_signals_names):
- """Check data dimensionality and size. Return False if invalid."""
- issues = []
- for asn in auxiliary_signals_names:
- if asn not in group or not is_dataset(group[asn]):
- issues.append(
- "Cannot find auxiliary signal dataset '%s'" % asn)
- elif group[signal_name].shape != group[asn].shape:
- issues.append("Auxiliary signal dataset '%s' does not" % asn +
- " have the same shape as the main signal.")
- return issues
-
-
-def validate_number_of_axes(group, signal_name, num_axes):
- issues = []
- ndims = len(group[signal_name].shape)
- if 1 < ndims < num_axes:
- # ndim = 1 with several axes could be a scatter
- issues.append(
- "More @axes defined than there are " +
- "signal dimensions: " +
- "%d axes, %d dimensions." % (num_axes, ndims))
-
- # case of less axes than dimensions: number of axes must match
- # dimensionality defined by @interpretation
- elif ndims > num_axes:
- interpretation = get_attr_as_unicode(group[signal_name], "interpretation")
- if interpretation is None:
- interpretation = get_attr_as_unicode(group, "interpretation")
- if interpretation is None:
- issues.append("No @interpretation and not enough" +
- " @axes defined.")
-
- elif interpretation not in INTERPDIM:
- issues.append("Unrecognized @interpretation=" + interpretation +
- " for data with wrong number of defined @axes.")
- elif interpretation == "rgba-image":
- if ndims != 3 or group[signal_name].shape[-1] not in [3, 4]:
- issues.append(
- "Inconsistent RGBA Image. Expected 3 dimensions with " +
- "last one of length 3 or 4. Got ndim=%d " % ndims +
- "with last dimension of length %d." % group[signal_name].shape[-1])
- if num_axes != 2:
- issues.append(
- "Inconsistent number of axes for RGBA Image. Expected "
- "3, but got %d." % ndims)
-
- elif num_axes != INTERPDIM[interpretation]:
- issues.append(
- "%d-D signal with @interpretation=%s " % (ndims, interpretation) +
- "must define %d or %d axes." % (ndims, INTERPDIM[interpretation]))
- return issues
diff --git a/silx/io/nxdata/parse.py b/silx/io/nxdata/parse.py
deleted file mode 100644
index daf1b2e..0000000
--- a/silx/io/nxdata/parse.py
+++ /dev/null
@@ -1,841 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2017-2018 European Synchrotron Radiation Facility
-#
-# 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.
-#
-# ###########################################################################*/
-"""This package provides a collection of functions to work with h5py-like
-groups following the NeXus *NXdata* specification.
-
-See http://download.nexusformat.org/sphinx/classes/base_classes/NXdata.html
-
-The main class is :class:`NXdata`.
-You can also fetch the default NXdata in a NXroot or a NXentry with function
-:func:`get_default`.
-
-
-Other public functions:
-
- - :func:`is_valid_nxdata`
- - :func:`is_NXroot_with_default_NXdata`
- - :func:`is_NXentry_with_default_NXdata`
-
-"""
-
-import numpy
-from silx.io.utils import is_group, is_file, is_dataset
-
-from ._utils import get_attr_as_unicode, INTERPDIM, nxdata_logger, \
- get_uncertainties_names, get_signal_name, \
- get_auxiliary_signals_names, validate_auxiliary_signals, validate_number_of_axes
-from silx.third_party import six
-
-
-__authors__ = ["P. Knobel"]
-__license__ = "MIT"
-__date__ = "17/04/2018"
-
-
-class InvalidNXdataError(Exception):
- pass
-
-
-class NXdata(object):
- """NXdata parser.
-
- .. note::
-
- Before attempting to access any attribute or property,
- you should check that :attr:`is_valid` is *True*.
-
- :param group: h5py-like group following the NeXus *NXdata* specification.
- :param boolean validate: Set this parameter to *False* to skip the initial
- validation. This option is provided for optimisation purposes, for cases
- where :meth:`silx.io.nxdata.is_valid_nxdata` has already been called
- prior to instantiating this :class:`NXdata`.
- """
- def __init__(self, group, validate=True):
- super(NXdata, self).__init__()
-
- self.group = group
- """h5py-like group object with @NX_class=NXdata.
- """
-
- self.issues = []
- """List of error messages for malformed NXdata."""
-
- if validate:
- self._validate()
- self.is_valid = not self.issues
- """Validity status for this NXdata.
- If False, all properties and attributes will be None.
- """
-
- self._is_scatter = None
- self._axes = None
-
- self.signal = None
- """Main signal dataset in this NXdata group.
- In case more than one signal is present in this group,
- the other ones can be found in :attr:`auxiliary_signals`.
- """
-
- self.signal_name = None
- """Signal long name, as specified in the @long_name attribute of the
- signal dataset. If not specified, the dataset name is used."""
-
- self.signal_ndim = None
- self.signal_is_0d = None
- self.signal_is_1d = None
- self.signal_is_2d = None
- self.signal_is_3d = None
-
- self.axes_names = None
- """List of axes names in a NXdata group.
-
- This attribute is similar to :attr:`axes_dataset_names` except that
- if an axis dataset has a "@long_name" attribute, it will be used
- instead of the dataset name.
- """
-
- if not self.is_valid:
- nxdata_logger.debug("%s", self.issues)
- else:
- self.signal = self.group[self.signal_dataset_name]
- self.signal_name = get_attr_as_unicode(self.signal, "long_name")
-
- if self.signal_name is None:
- self.signal_name = self.signal_dataset_name
-
- # ndim will be available in very recent h5py versions only
- self.signal_ndim = getattr(self.signal, "ndim",
- len(self.signal.shape))
-
- self.signal_is_0d = self.signal_ndim == 0
- self.signal_is_1d = self.signal_ndim == 1
- self.signal_is_2d = self.signal_ndim == 2
- self.signal_is_3d = self.signal_ndim == 3
-
- self.axes_names = []
- # check if axis dataset defines @long_name
- for i, dsname in enumerate(self.axes_dataset_names):
- if dsname is not None and "long_name" in self.group[dsname].attrs:
- self.axes_names.append(get_attr_as_unicode(self.group[dsname], "long_name"))
- else:
- self.axes_names.append(dsname)
-
- # excludes scatters
- self.signal_is_1d = self.signal_is_1d and len(self.axes) <= 1 # excludes n-D scatters
-
- def _validate(self):
- """Fill :attr:`issues` with error messages for each error found."""
- if not is_group(self.group):
- raise TypeError("group must be a h5py-like group")
- if get_attr_as_unicode(self.group, "NX_class") != "NXdata":
- self.issues.append("Group has no attribute @NX_class='NXdata'")
-
- signal_name = get_signal_name(self.group)
- if signal_name is None:
- self.issues.append("No @signal attribute on the NXdata group, "
- "and no dataset with a @signal=1 attr found")
- # very difficult to do more consistency tests without signal
- return
-
- elif signal_name not in self.group or not is_dataset(self.group[signal_name]):
- self.issues.append("Cannot find signal dataset '%s'" % signal_name)
- return
-
- auxiliary_signals_names = get_auxiliary_signals_names(self.group)
- self.issues += validate_auxiliary_signals(self.group,
- signal_name,
- auxiliary_signals_names)
-
- if "axes" in self.group.attrs:
- axes_names = get_attr_as_unicode(self.group, "axes")
- if isinstance(axes_names, (six.text_type, six.binary_type)):
- axes_names = [axes_names]
-
- self.issues += validate_number_of_axes(self.group, signal_name,
- num_axes=len(axes_names))
-
- # Test consistency of @uncertainties
- uncertainties_names = get_uncertainties_names(self.group, signal_name)
- if uncertainties_names is not None:
- if len(uncertainties_names) != len(axes_names):
- self.issues.append("@uncertainties does not define the same " +
- "number of fields than @axes")
-
- # Test individual axes
- is_scatter = True # true if all axes have the same size as the signal
- signal_size = 1
- for dim in self.group[signal_name].shape:
- signal_size *= dim
- polynomial_axes_names = []
- for i, axis_name in enumerate(axes_names):
-
- if axis_name == ".":
- continue
- if axis_name not in self.group or not is_dataset(self.group[axis_name]):
- self.issues.append("Could not find axis dataset '%s'" % axis_name)
- continue
-
- axis_size = 1
- for dim in self.group[axis_name].shape:
- axis_size *= dim
-
- if len(self.group[axis_name].shape) != 1:
- # I don't know how to interpret n-D axes
- self.issues.append("Axis %s is not 1D" % axis_name)
- continue
- else:
- # for a 1-d axis,
- fg_idx = self.group[axis_name].attrs.get("first_good", 0)
- lg_idx = self.group[axis_name].attrs.get("last_good", len(self.group[axis_name]) - 1)
- axis_len = lg_idx + 1 - fg_idx
-
- if axis_len != signal_size:
- if axis_len not in self.group[signal_name].shape + (1, 2):
- self.issues.append(
- "Axis %s number of elements does not " % axis_name +
- "correspond to the length of any signal dimension,"
- " it does not appear to be a constant or a linear calibration," +
- " and this does not seem to be a scatter plot.")
- continue
- elif axis_len in (1, 2):
- polynomial_axes_names.append(axis_name)
- is_scatter = False
- else:
- if not is_scatter:
- self.issues.append(
- "Axis %s number of elements is equal " % axis_name +
- "to the length of the signal, but this does not seem" +
- " to be a scatter (other axes have different sizes)")
- continue
-
- # Test individual uncertainties
- errors_name = axis_name + "_errors"
- if errors_name not in self.group and uncertainties_names is not None:
- errors_name = uncertainties_names[i]
- if errors_name in self.group and axis_name not in polynomial_axes_names:
- if self.group[errors_name].shape != self.group[axis_name].shape:
- self.issues.append(
- "Errors '%s' does not have the same " % errors_name +
- "dimensions as axis '%s'." % axis_name)
-
- # test dimensions of errors associated with signal
- if "errors" in self.group and is_dataset(self.group["errors"]):
- if self.group["errors"].shape != self.group[signal_name].shape:
- self.issues.append(
- "Dataset containing standard deviations must " +
- "have the same dimensions as the signal.")
-
- @property
- def signal_dataset_name(self):
- """Name of the main signal dataset."""
- if not self.is_valid:
- raise InvalidNXdataError("Unable to parse invalid NXdata")
- signal_dataset_name = get_attr_as_unicode(self.group, "signal")
- if signal_dataset_name is None:
- # find a dataset with @signal == 1
- for dsname in self.group:
- signal_attr = self.group[dsname].attrs.get("signal")
- if signal_attr in [1, b"1", u"1"]:
- # This is the main (default) signal
- signal_dataset_name = dsname
- break
- assert signal_dataset_name is not None
- return signal_dataset_name
-
- @property
- def auxiliary_signals_dataset_names(self):
- """Sorted list of names of the auxiliary signals datasets.
-
- These are the names provided by the *@auxiliary_signals* attribute
- on the NXdata group.
-
- In case the NXdata group does not specify a *@signal* attribute
- but has a dataset with an attribute *@signal=1*,
- we look for datasets with attributes *@signal=2, @signal=3...*
- (deprecated NXdata specification)."""
- if not self.is_valid:
- raise InvalidNXdataError("Unable to parse invalid NXdata")
- signal_dataset_name = get_attr_as_unicode(self.group, "signal")
- if signal_dataset_name is not None:
- auxiliary_signals_names = get_attr_as_unicode(self.group, "auxiliary_signals")
- if auxiliary_signals_names is not None:
- if not isinstance(auxiliary_signals_names,
- (tuple, list, numpy.ndarray)):
- # tolerate a single string, but coerce into a list
- return [auxiliary_signals_names]
- return list(auxiliary_signals_names)
- return []
-
- # try old spec, @signal=1 (2, 3...) on dataset
- numbered_names = []
- for dsname in self.group:
- if dsname == self.signal_dataset_name:
- # main signal, not auxiliary
- continue
- ds = self.group[dsname]
- signal_attr = ds.attrs.get("signal")
- if signal_attr is not None and not is_dataset(ds):
- nxdata_logger.warning("Item %s with @signal=%s is not a dataset (%s)",
- dsname, signal_attr, type(ds))
- continue
- if signal_attr is not None:
- try:
- signal_number = int(signal_attr)
- except (ValueError, TypeError):
- nxdata_logger.warning("Could not parse attr @signal=%s on "
- "dataset %s as an int",
- signal_attr, dsname)
- continue
- numbered_names.append((signal_number, dsname))
- return [a[1] for a in sorted(numbered_names)]
-
- @property
- def auxiliary_signals_names(self):
- """List of names of the auxiliary signals.
-
- Similar to :attr:`auxiliary_signals_dataset_names`, but the @long_name
- is used when this attribute is present, instead of the dataset name.
- """
- if not self.is_valid:
- raise InvalidNXdataError("Unable to parse invalid NXdata")
-
- signal_names = []
- for asdn in self.auxiliary_signals_dataset_names:
- if "long_name" in self.group[asdn].attrs:
- signal_names.append(self.group[asdn].attrs["long_name"])
- else:
- signal_names.append(asdn)
- return signal_names
-
- @property
- def auxiliary_signals(self):
- """List of all auxiliary signal datasets."""
- if not self.is_valid:
- raise InvalidNXdataError("Unable to parse invalid NXdata")
-
- return [self.group[dsname] for dsname in self.auxiliary_signals_dataset_names]
-
- @property
- def interpretation(self):
- """*@interpretation* attribute associated with the *signal*
- dataset of the NXdata group. ``None`` if no interpretation
- attribute is present.
-
- The *interpretation* attribute provides information about the last
- dimensions of the signal. The allowed values are:
-
- - *"scalar"*: 0-D data to be plotted
- - *"spectrum"*: 1-D data to be plotted
- - *"image"*: 2-D data to be plotted
- - *"vertex"*: 3-D data to be plotted
-
- For example, a 3-D signal with interpretation *"spectrum"* should be
- considered to be a 2-D array of 1-D data. A 3-D signal with
- interpretation *"image"* should be interpreted as a 1-D array (a list)
- of 2-D images. An n-D array with interpretation *"image"* should be
- interpreted as an (n-2)-D array of images.
-
- A warning message is logged if the returned interpretation is not one
- of the allowed values, but no error is raised and the unknown
- interpretation is returned anyway.
- """
- if not self.is_valid:
- raise InvalidNXdataError("Unable to parse invalid NXdata")
-
- allowed_interpretations = [None, "scalar", "spectrum", "image",
- "rgba-image", # "hsla-image", "cmyk-image"
- "vertex"]
-
- interpretation = get_attr_as_unicode(self.signal, "interpretation")
- if interpretation is None:
- interpretation = get_attr_as_unicode(self.group, "interpretation")
-
- if interpretation not in allowed_interpretations:
- nxdata_logger.warning("Interpretation %s is not valid." % interpretation +
- " Valid values: " + ", ".join(allowed_interpretations))
- return interpretation
-
- @property
- def axes(self):
- """List of the axes datasets.
-
- The list typically has as many elements as there are dimensions in the
- signal dataset, the exception being scatter plots which use a 1D
- signal and multiple 1D axes of the same size.
-
- If an axis dataset applies to several dimensions of the signal, it
- will be repeated in the list.
-
- If a dimension of the signal has no dimension scale, `None` is
- inserted in its position in the list.
-
- .. note::
-
- The *@axes* attribute should define as many entries as there
- are dimensions in the signal, to avoid any ambiguity.
- If this is not the case, this implementation relies on the existence
- of an *@interpretation* (*spectrum* or *image*) attribute in the
- *signal* dataset.
-
- .. note::
-
- If an axis dataset defines attributes @first_good or @last_good,
- the output will be a numpy array resulting from slicing that
- axis (*axis[first_good:last_good + 1]*).
-
- :rtype: List[Dataset or 1D array or None]
- """
- if not self.is_valid:
- raise InvalidNXdataError("Unable to parse invalid NXdata")
-
- if self._axes is not None:
- # use cache
- return self._axes
- axes = []
- for axis_name in self.axes_dataset_names:
- if axis_name is None:
- axes.append(None)
- else:
- axes.append(self.group[axis_name])
-
- # keep only good range of axis data
- for i, axis in enumerate(axes):
- if axis is None:
- continue
- if "first_good" not in axis.attrs and "last_good" not in axis.attrs:
- continue
- fg_idx = axis.attrs.get("first_good", 0)
- lg_idx = axis.attrs.get("last_good", len(axis) - 1)
- axes[i] = axis[fg_idx:lg_idx + 1]
-
- self._axes = axes
- return self._axes
-
- @property
- def axes_dataset_names(self):
- """List of axes dataset names.
-
- If an axis dataset applies to several dimensions of the signal, its
- name will be repeated in the list.
-
- If a dimension of the signal has no dimension scale (i.e. there is a
- "." in that position in the *@axes* array), `None` is inserted in the
- output list in its position.
- """
- if not self.is_valid:
- raise InvalidNXdataError("Unable to parse invalid NXdata")
-
- numbered_names = [] # used in case of @axis=0 (old spec)
- axes_dataset_names = get_attr_as_unicode(self.group, "axes")
- if axes_dataset_names is None:
- # try @axes on signal dataset (older NXdata specification)
- axes_dataset_names = get_attr_as_unicode(self.signal, "axes")
- if axes_dataset_names is not None:
- # we expect a comma separated string
- if hasattr(axes_dataset_names, "split"):
- axes_dataset_names = axes_dataset_names.split(":")
- else:
- # try @axis on the individual datasets (oldest NXdata specification)
- for dsname in self.group:
- if not is_dataset(self.group[dsname]):
- continue
- axis_attr = self.group[dsname].attrs.get("axis")
- if axis_attr is not None:
- try:
- axis_num = int(axis_attr)
- except (ValueError, TypeError):
- nxdata_logger.warning("Could not interpret attr @axis as"
- "int on dataset %s", dsname)
- continue
- numbered_names.append((axis_num, dsname))
-
- ndims = len(self.signal.shape)
- if axes_dataset_names is None:
- if numbered_names:
- axes_dataset_names = []
- numbers = [a[0] for a in numbered_names]
- names = [a[1] for a in numbered_names]
- for i in range(ndims):
- if i in numbers:
- axes_dataset_names.append(names[numbers.index(i)])
- else:
- axes_dataset_names.append(None)
- return axes_dataset_names
- else:
- return [None] * ndims
-
- if isinstance(axes_dataset_names, (six.text_type, six.binary_type)):
- axes_dataset_names = [axes_dataset_names]
-
- for i, axis_name in enumerate(axes_dataset_names):
- if hasattr(axis_name, "decode"):
- axis_name = axis_name.decode()
- if axis_name == ".":
- axes_dataset_names[i] = None
-
- if len(axes_dataset_names) != ndims:
- if self.is_scatter and ndims == 1:
- # case of a 1D signal with arbitrary number of axes
- return list(axes_dataset_names)
- if self.interpretation != "rgba-image":
- # @axes may only define 1 or 2 axes if @interpretation=spectrum/image.
- # Use the existing names for the last few dims, and prepend with Nones.
- assert len(axes_dataset_names) == INTERPDIM[self.interpretation]
- all_dimensions_names = [None] * (ndims - INTERPDIM[self.interpretation])
- for axis_name in axes_dataset_names:
- all_dimensions_names.append(axis_name)
- else:
- # 2 axes applying to the first two dimensions.
- # The 3rd signal dimension is expected to contain 3(4) RGB(A) values.
- assert len(axes_dataset_names) == 2
- all_dimensions_names = [axn for axn in axes_dataset_names]
- all_dimensions_names.append(None)
- return all_dimensions_names
-
- return list(axes_dataset_names)
-
- @property
- def title(self):
- """Plot title. If not found, returns an empty string.
-
- This attribute does not appear in the NXdata specification, but it is
- implemented in *nexpy* as a dataset named "title" inside the NXdata
- group. This dataset is expected to contain text.
-
- Because the *nexpy* approach could cause a conflict if the signal
- dataset or an axis dataset happened to be called "title", we also
- support providing the title as an attribute of the NXdata group.
- """
- if not self.is_valid:
- raise InvalidNXdataError("Unable to parse invalid NXdata")
-
- title = self.group.get("title")
- data_dataset_names = [self.signal_name] + self.axes_dataset_names
- if (title is not None and is_dataset(title) and
- "title" not in data_dataset_names):
- return str(title[()])
-
- title = self.group.attrs.get("title")
- if title is None:
- return ""
- return str(title)
-
- def get_axis_errors(self, axis_name):
- """Return errors (uncertainties) associated with an axis.
-
- If the axis has attributes @first_good or @last_good, the output
- is trimmed accordingly (a numpy array will be returned rather than a
- dataset).
-
- :param str axis_name: Name of axis dataset. This dataset **must exist**.
- :return: Dataset with axis errors, or None
- :raise KeyError: if this group does not contain a dataset named axis_name
- """
- if not self.is_valid:
- raise InvalidNXdataError("Unable to parse invalid NXdata")
-
- # ensure axis_name is decoded, before comparing it with decoded attributes
- if hasattr(axis_name, "decode"):
- axis_name = axis_name.decode("utf-8")
- if axis_name not in self.group:
- # tolerate axis_name given as @long_name
- for item in self.group:
- long_name = get_attr_as_unicode(self.group[item], "long_name")
- if long_name is not None and long_name == axis_name:
- axis_name = item
- break
-
- if axis_name not in self.group:
- raise KeyError("group does not contain a dataset named '%s'" % axis_name)
-
- len_axis = len(self.group[axis_name])
-
- fg_idx = self.group[axis_name].attrs.get("first_good", 0)
- lg_idx = self.group[axis_name].attrs.get("last_good", len_axis - 1)
-
- # case of axisname_errors dataset present
- errors_name = axis_name + "_errors"
- if errors_name in self.group and is_dataset(self.group[errors_name]):
- if fg_idx != 0 or lg_idx != (len_axis - 1):
- return self.group[errors_name][fg_idx:lg_idx + 1]
- else:
- return self.group[errors_name]
- # case of uncertainties dataset name provided in @uncertainties
- uncertainties_names = get_attr_as_unicode(self.group, "uncertainties")
- if uncertainties_names is None:
- uncertainties_names = get_attr_as_unicode(self.signal, "uncertainties")
- if isinstance(uncertainties_names, six.text_type):
- uncertainties_names = [uncertainties_names]
- if uncertainties_names is not None:
- # take the uncertainty with the same index as the axis in @axes
- axes_ds_names = get_attr_as_unicode(self.group, "axes")
- if axes_ds_names is None:
- axes_ds_names = get_attr_as_unicode(self.signal, "axes")
- if isinstance(axes_ds_names, six.text_type):
- axes_ds_names = [axes_ds_names]
- elif isinstance(axes_ds_names, numpy.ndarray):
- # transform numpy.ndarray into list
- axes_ds_names = list(axes_ds_names)
- assert isinstance(axes_ds_names, list)
- if hasattr(axes_ds_names[0], "decode"):
- axes_ds_names = [ax_name.decode("utf-8") for ax_name in axes_ds_names]
- if axis_name not in axes_ds_names:
- raise KeyError("group attr @axes does not mention a dataset " +
- "named '%s'" % axis_name)
- errors = self.group[uncertainties_names[list(axes_ds_names).index(axis_name)]]
- if fg_idx == 0 and lg_idx == (len_axis - 1):
- return errors # dataset
- else:
- return errors[fg_idx:lg_idx + 1] # numpy array
- return None
-
- @property
- def errors(self):
- """Return errors (uncertainties) associated with the signal values.
-
- :return: Dataset with errors, or None
- """
- if not self.is_valid:
- raise InvalidNXdataError("Unable to parse invalid NXdata")
-
- if "errors" not in self.group:
- return None
- return self.group["errors"]
-
- @property
- def is_scatter(self):
- """True if the signal is 1D and all the axes have the
- same size as the signal."""
- if not self.is_valid:
- raise InvalidNXdataError("Unable to parse invalid NXdata")
-
- if self._is_scatter is not None:
- return self._is_scatter
- if not self.signal_is_1d:
- self._is_scatter = False
- else:
- self._is_scatter = True
- sigsize = 1
- for dim in self.signal.shape:
- sigsize *= dim
- for axis in self.axes:
- if axis is None:
- continue
- axis_size = 1
- for dim in axis.shape:
- axis_size *= dim
- self._is_scatter = self._is_scatter and (axis_size == sigsize)
- return self._is_scatter
-
- @property
- def is_x_y_value_scatter(self):
- """True if this is a scatter with a signal and two axes."""
- if not self.is_valid:
- raise InvalidNXdataError("Unable to parse invalid NXdata")
-
- return self.is_scatter and len(self.axes) == 2
-
- # we currently have no widget capable of plotting 4D data
- @property
- def is_unsupported_scatter(self):
- """True if this is a scatter with a signal and more than 2 axes."""
- if not self.is_valid:
- raise InvalidNXdataError("Unable to parse invalid NXdata")
-
- return self.is_scatter and len(self.axes) > 2
-
- @property
- def is_curve(self):
- """This property is True if the signal is 1D or :attr:`interpretation` is
- *"spectrum"*, and there is at most one axis with a consistent length.
- """
- if not self.is_valid:
- raise InvalidNXdataError("Unable to parse invalid NXdata")
-
- if self.signal_is_0d or self.interpretation not in [None, "spectrum"]:
- return False
- # the axis, if any, must be of the same length as the last dimension
- # of the signal, or of length 2 (a + b *x scale)
- if self.axes[-1] is not None and len(self.axes[-1]) not in [
- self.signal.shape[-1], 2]:
- return False
- if self.interpretation is None:
- # We no longer test whether x values are monotonic
- # (in the past, in that case, we used to consider it a scatter)
- return self.signal_is_1d
- # everything looks good
- return True
-
- @property
- def is_image(self):
- """True if the signal is 2D, or 3D with last dimension of length 3 or 4
- and interpretation *rgba-image*, or >2D with interpretation *image*.
- The axes (if any) length must also be consistent with the signal shape.
- """
- if not self.is_valid:
- raise InvalidNXdataError("Unable to parse invalid NXdata")
-
- if self.interpretation in ["scalar", "spectrum", "scaler"]:
- return False
- if self.signal_is_0d or self.signal_is_1d:
- return False
- if not self.signal_is_2d and \
- self.interpretation not in ["image", "rgba-image"]:
- return False
- if self.signal_is_3d and self.interpretation == "rgba-image":
- if self.signal.shape[-1] not in [3, 4]:
- return False
- img_axes = self.axes[0:2]
- img_shape = self.signal.shape[0:2]
- else:
- img_axes = self.axes[-2:]
- img_shape = self.signal.shape[-2:]
- for i, axis in enumerate(img_axes):
- if axis is not None and len(axis) not in [img_shape[i], 2]:
- return False
-
- return True
-
- @property
- def is_stack(self):
- """True in the signal is at least 3D and interpretation is not
- "scalar", "spectrum", "image" or "rgba-image".
- The axes length must also be consistent with the last 3 dimensions
- of the signal.
- """
- if not self.is_valid:
- raise InvalidNXdataError("Unable to parse invalid NXdata")
-
- if self.signal_ndim < 3 or self.interpretation in [
- "scalar", "scaler", "spectrum", "image", "rgba-image"]:
- return False
- stack_shape = self.signal.shape[-3:]
- for i, axis in enumerate(self.axes[-3:]):
- if axis is not None and len(axis) not in [stack_shape[i], 2]:
- return False
- return True
-
-
-def is_valid_nxdata(group): # noqa
- """Check if a h5py group is a **valid** NX_data group.
-
- :param group: h5py-like group
- :return: True if this NXdata group is valid.
- :raise TypeError: if group is not a h5py group, a spech5 group,
- or a fabioh5 group
- """
- nxd = NXdata(group)
- return nxd.is_valid
-
-
-def is_NXentry_with_default_NXdata(group, validate=True):
- """Return True if group is a valid NXentry defining a valid default
- NXdata.
-
- :param group: h5py-like object.
- :param bool validate: Set this to False if you are sure that the target group
- is valid NXdata (i.e. :func:`silx.io.nxdata.is_valid_nxdata(target_group)`
- returns True). Parameter provided for optimisation purposes."""
- if not is_group(group):
- return False
-
- if get_attr_as_unicode(group, "NX_class") != "NXentry":
- return False
-
- default_nxdata_name = group.attrs.get("default")
- if default_nxdata_name is None or default_nxdata_name not in group:
- return False
-
- default_nxdata_group = group.get(default_nxdata_name)
-
- if not is_group(default_nxdata_group):
- return False
-
- if not validate:
- return True
- else:
- return is_valid_nxdata(default_nxdata_group)
-
-
-def is_NXroot_with_default_NXdata(group, validate=True):
- """Return True if group is a valid NXroot defining a default NXentry
- defining a valid default NXdata.
-
- :param group: h5py-like object.
- :param bool validate: Set this to False if you are sure that the target group
- is valid NXdata (i.e. :func:`silx.io.nxdata.is_valid_nxdata(target_group)`
- returns True). Parameter provided for optimisation purposes.
- """
- if not is_group(group):
- return False
-
- # A NXroot is supposed to be at the root of a data file, and @NX_class
- # is therefore optional. We accept groups that are not located at the root
- # if they have @NX_class=NXroot (use case: several nexus files archived
- # in a single HDF5 file)
- if get_attr_as_unicode(group, "NX_class") != "NXroot" and not is_file(group):
- return False
-
- default_nxentry_name = group.attrs.get("default")
- if default_nxentry_name is None or default_nxentry_name not in group:
- return False
-
- default_nxentry_group = group.get(default_nxentry_name)
- return is_NXentry_with_default_NXdata(default_nxentry_group,
- validate=validate)
-
-
-def get_default(group, validate=True):
- """Return a :class:`NXdata` object corresponding to the default NXdata group
- in the group specified as parameter.
-
- This function can find the NXdata if the group is already a NXdata, or
- if it is a NXentry defining a default NXdata, or if it is a NXroot
- defining such a default valid NXentry.
-
- Return None if no valid NXdata could be found.
-
- :param group: h5py-like group following the Nexus specification
- (NXdata, NXentry or NXroot).
- :param bool validate: Set this to False if you are sure that group
- is valid NXdata (i.e. :func:`silx.io.nxdata.is_valid_nxdata(group)`
- returns True). Parameter provided for optimisation purposes.
- :return: :class:`NXdata` object or None
- :raise TypeError: if group is not a h5py-like group
- """
- if not is_group(group):
- raise TypeError("Provided parameter is not a h5py-like group")
-
- if is_NXroot_with_default_NXdata(group, validate=validate):
- default_entry = group[group.attrs["default"]]
- default_data = default_entry[default_entry.attrs["default"]]
- elif is_NXentry_with_default_NXdata(group, validate=validate):
- default_data = group[group.attrs["default"]]
- elif not validate or is_valid_nxdata(group):
- default_data = group
- else:
- return None
-
- return NXdata(default_data, validate=False)
diff --git a/silx/io/nxdata/write.py b/silx/io/nxdata/write.py
deleted file mode 100644
index 05eef4d..0000000
--- a/silx/io/nxdata/write.py
+++ /dev/null
@@ -1,202 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2017-2018 European Synchrotron Radiation Facility
-#
-# 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.
-#
-# ###########################################################################*/
-
-import h5py
-import os
-import logging
-import numpy
-from silx.third_party import six
-
-__authors__ = ["P. Knobel"]
-__license__ = "MIT"
-__date__ = "17/04/2018"
-
-
-_logger = logging.getLogger(__name__)
-
-
-def _str_to_utf8(text):
- return numpy.array(text, dtype=h5py.special_dtype(vlen=six.text_type))
-
-
-def save_NXdata(filename, signal, axes=None,
- signal_name="data", axes_names=None,
- signal_long_name=None, axes_long_names=None,
- signal_errors=None, axes_errors=None,
- title=None, interpretation=None,
- nxentry_name="entry", nxdata_name=None):
- """Write data to an NXdata group.
-
- .. note::
-
- No consistency checks are made regarding the dimensionality of the
- signal and number of axes. The user is responsible for providing
- meaningful data, that can be interpreted by visualization software.
-
- :param str filename: Path to output file. If the file does not
- exists, it is created.
- :param numpy.ndarray signal: Signal array.
- :param List[numpy.ndarray] axes: List of axes arrays.
- :param str signal_name: Name of signal dataset, in output file
- :param List[str] axes_names: List of dataset names for axes, in
- output file
- :param str signal_long_name: *@long_name* attribute for signal, or None.
- :param axes_long_names: None, or list of long names
- for axes
- :type axes_long_names: List[str, None]
- :param numpy.ndarray signal_errors: Array of errors associated with the
- signal
- :param axes_errors: List of arrays of errors
- associated with each axis
- :type axes_errors: List[numpy.ndarray, None]
- :param str title: Graph title (saved as a "title" dataset) or None.
- :param str interpretation: *@interpretation* attribute ("spectrum",
- "image", "rgba-image" or None). This is only needed in cases of
- ambiguous dimensionality, e.g. a 3D array which represents a RGBA
- image rather than a stack.
- :param str nxentry_name: Name of group in which the NXdata group
- is created. By default, "/entry" is used.
-
- .. note::
-
- The Nexus format specification requires for NXdata groups
- be part of a NXentry group.
- The specified group should have attribute *@NX_class=NXentry*, in
- order for the created file to be nexus compliant.
- :param str nxdata_name: Name of NXdata group. If omitted (None), the
- function creates a new group using the first available name ("data0",
- or "data1"...).
- Overwriting an existing group (or dataset) is not supported, you must
- delete it yourself prior to calling this function if this is what you
- want.
- :return: True if save was successful, else False.
- """
- if h5py is None:
- raise ImportError("h5py could not be imported, but is required by "
- "save_NXdata function")
-
- if axes_names is not None:
- assert axes is not None, "Axes names defined, but missing axes arrays"
- assert len(axes) == len(axes_names), \
- "Mismatch between number of axes and axes_names"
-
- if axes is not None and axes_names is None:
- axes_names = []
- for i, axis in enumerate(axes):
- axes_names.append("dim%d" % i if axis is not None else ".")
- if axes is None:
- axes = []
-
- # Open file in
- if os.path.exists(filename):
- errmsg = "Cannot write/append to existing path %s"
- if not os.path.isfile(filename):
- errmsg += " (not a file)"
- _logger.error(errmsg, filename)
- return False
- if not os.access(filename, os.W_OK):
- errmsg += " (no permission to write)"
- _logger.error(errmsg, filename)
- return False
- mode = "r+"
- else:
- mode = "w-"
-
- with h5py.File(filename, mode=mode) as h5f:
- # get or create entry
- if nxentry_name is not None:
- entry = h5f.require_group(nxentry_name)
- if "default" not in h5f.attrs:
- # set this entry as default
- h5f.attrs["default"] = _str_to_utf8(nxentry_name)
- if "NX_class" not in entry.attrs:
- entry.attrs["NX_class"] = u"NXentry"
- else:
- # write NXdata into the root of the file (invalid nexus!)
- entry = h5f
-
- # Create NXdata group
- if nxdata_name is not None:
- if nxdata_name in entry:
- _logger.error("Cannot assign an NXdata group to an existing"
- " group or dataset")
- return False
- else:
- # no name specified, take one that is available
- nxdata_name = "data0"
- i = 1
- while nxdata_name in entry:
- _logger.info("%s item already exists in NXentry group," +
- " trying %s", nxdata_name, "data%d" % i)
- nxdata_name = "data%d" % i
- i += 1
-
- data_group = entry.create_group(nxdata_name)
- data_group.attrs["NX_class"] = u"NXdata"
- data_group.attrs["signal"] = _str_to_utf8(signal_name)
- if axes:
- data_group.attrs["axes"] = _str_to_utf8(axes_names)
- if title:
- # not in NXdata spec, but implemented by nexpy
- data_group["title"] = title
- # better way imho
- data_group.attrs["title"] = _str_to_utf8(title)
-
- signal_dataset = data_group.create_dataset(signal_name,
- data=signal)
- if signal_long_name:
- signal_dataset.attrs["long_name"] = _str_to_utf8(signal_long_name)
- if interpretation:
- signal_dataset.attrs["interpretation"] = _str_to_utf8(interpretation)
-
- for i, axis_array in enumerate(axes):
- if axis_array is None:
- assert axes_names[i] in [".", None], \
- "Axis name defined for dim %d but no axis array" % i
- continue
- axis_dataset = data_group.create_dataset(axes_names[i],
- data=axis_array)
- if axes_long_names is not None:
- axis_dataset.attrs["long_name"] = _str_to_utf8(axes_long_names[i])
-
- if signal_errors is not None:
- data_group.create_dataset("errors",
- data=signal_errors)
-
- if axes_errors is not None:
- assert isinstance(axes_errors, (list, tuple)), \
- "axes_errors must be a list or a tuple of ndarray or None"
- assert len(axes_errors) == len(axes_names), \
- "Mismatch between number of axes_errors and axes_names"
- for i, axis_errors in enumerate(axes_errors):
- if axis_errors is not None:
- dsname = axes_names[i] + "_errors"
- data_group.create_dataset(dsname,
- data=axis_errors)
- if "default" not in entry.attrs:
- # set this NXdata as default
- entry.attrs["default"] = nxdata_name
-
- return True