Commit 86620979 authored by payno's avatar payno

[io] add dimensions when reading 3D spectra

parent f1ff3831
......@@ -32,6 +32,7 @@ __date__ = "07/16/2019"
from est.io import read_xas, write_xas, get_xasproc
from est.core.types import XASObject
from silx.io.url import DataUrl
from est.core.types import Dim
import h5py
import logging
......@@ -44,18 +45,26 @@ DEFAULT_CHANNEL_PATH = '/data/NXdata/Channel'
DEFAULT_CONF_PATH = '/configuration'
def read(spectra_url, channel_url, config_url=None):
def read(spectra_url, channel_url, config_url=None, dimensions=None):
"""
:param DataUrl spectra_url: data url to the spectra
:param DataUrl channel_url: data url to the channel / energy
:param DataUrl config_url: data url to the process configuration
:param dimensions: way the data has been stored.
Usually is (X, Y, channels) of (Channels, Y, X).
If None, by default is considered to be (Channels, Y, X)
:type: tuple
:return:
:rtype: XASObject
"""
dimensions_ = dimensions
if dimensions_ is None:
dimensions_ = (Dim.CHANNEL_ENERGY_DIM, Dim.Y_DIM, Dim.X_DIM)
reader = XASReader()
return reader.read_frm_url(spectra_url=spectra_url, channel_url=channel_url,
config_url=config_url)
config_url=config_url, dimensions=dimensions_)
def read_frm_file(file_path):
......@@ -72,10 +81,12 @@ def read_frm_file(file_path):
class XASReader(object):
"""Simple reader of a xas file"""
def read_frm_url(self, spectra_url, channel_url, config_url=None):
def read_frm_url(self, spectra_url, channel_url, dimensions=None,
config_url=None):
sp, en, conf = read_xas(spectra_url=spectra_url,
channel_url=channel_url,
config_url=config_url)
config_url=config_url,
dimensions=dimensions)
return XASObject(spectra=sp, energy=en, configuration=conf)
def read_from_file(self, file_path):
......
......@@ -42,4 +42,6 @@ def suite():
test_suite.addTest(test_process.suite())
from .test_types import suite as test_types_suite
test_suite.addTest(test_types_suite())
from .test_io import suite as test_io_suite
test_suite.addTest(test_io_suite())
return test_suite
# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2017-2019 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.
#
# ###########################################################################*/
__authors__ = ["H. Payno"]
__license__ = "MIT"
__date__ = "06/26/2019"
import unittest
from est.core.types import Dim
from est.core.io import XASReader
from silx.io.url import DataUrl
from est.core.types import XASObject
import numpy
import os
import tempfile
import h5py
import shutil
class TestSpectraDimensions(unittest.TestCase):
"""
Test reading spectra with different dimensions organisation
(X, Y, Channels), (Channels, Y, X), (Y, Channels, X)
"""
def setUp(self) -> None:
self.spectra_path = '/data/NXdata/data'
self.channel_path = '/data/NXdata/Channel'
self.output_dir = tempfile.mkdtemp()
self.filename = os.path.join(self.output_dir, 'myfile.h5')
def tearDown(self) -> None:
shutil.rmtree(self.output_dir)
def saveSpectra(self, spectra):
"""Save the spectra to the spectra file defined in setup and return the
associated silx url"""
with h5py.File(self.filename, 'a') as f:
f[self.spectra_path] = spectra
return DataUrl(file_path=self.filename,
data_path=self.spectra_path,
scheme='silx')
def saveChannel(self, channel):
"""Save the energy to the spectra file defined in setup and return the
associated silx url"""
with h5py.File(self.filename, 'a') as f:
f[self.channel_path] = channel
return DataUrl(file_path=self.filename,
data_path=self.channel_path,
scheme='silx')
def testDimensionsXYEnergy(self):
"""Test that spectra stored as X, Y Energy can be read"""
x_dim = 4
y_dim = 2
energy_dim = 3
shape = (x_dim, y_dim, energy_dim)
spectra = numpy.arange(x_dim*y_dim*energy_dim).reshape(shape)
channel = numpy.linspace(0, 1, energy_dim)
spectra_url = self.saveSpectra(spectra)
channel_url = self.saveChannel(channel=channel)
# if dims are incoherent with energy, should raise an error
dims = (Dim.CHANNEL_ENERGY_DIM, Dim.Y_DIM, Dim.X_DIM)
with self.assertRaises(ValueError):
XASReader().read_frm_url(spectra_url=spectra_url,
channel_url=channel_url,
dimensions=dims)
dims = (Dim.X_DIM, Dim.Y_DIM, Dim.CHANNEL_ENERGY_DIM)
xas_obj = XASReader().read_frm_url(spectra_url=spectra_url,
channel_url=channel_url,
dimensions=dims)
self.assertTrue(isinstance(xas_obj, XASObject))
self.assertTrue(xas_obj.n_spectrum == x_dim * y_dim)
numpy.testing.assert_array_equal(xas_obj.spectra[1].mu, spectra[1,0,:])
numpy.testing.assert_array_equal(xas_obj.spectra[2].energy, channel)
def testDimensionsChannelYX(self):
"""Test that spectra stored as Channel, Y, X can be read"""
x_dim = 10
y_dim = 5
energy_dim = 30
shape = (energy_dim, y_dim, x_dim)
spectra = numpy.arange(x_dim*y_dim*energy_dim).reshape(shape)
channel = numpy.linspace(0, 100, energy_dim)
spectra_url = self.saveSpectra(spectra)
channel_url = self.saveChannel(channel=channel)
# if dims are incoherent with energy, should raise an error
dims = (Dim.X_DIM, Dim.Y_DIM, Dim.CHANNEL_ENERGY_DIM)
with self.assertRaises(ValueError):
XASReader().read_frm_url(spectra_url=spectra_url,
channel_url=channel_url,
dimensions=dims)
dims = (Dim.CHANNEL_ENERGY_DIM, Dim.Y_DIM, Dim.X_DIM)
xas_obj = XASReader().read_frm_url(spectra_url=spectra_url,
channel_url=channel_url,
dimensions=dims)
self.assertTrue(isinstance(xas_obj, XASObject))
self.assertTrue(xas_obj.n_spectrum == x_dim * y_dim)
numpy.testing.assert_array_equal(xas_obj.spectra[1].mu, spectra[:, 0, 1])
numpy.testing.assert_array_equal(xas_obj.spectra[2].energy, channel)
def suite():
test_suite = unittest.TestSuite()
for ui in (TestSpectraDimensions, ):
test_suite.addTest(unittest.defaultTestLoader.loadTestsFromTestCase(ui))
return test_suite
if __name__ == '__main__':
unittest.main(defaultTest="suite")
......@@ -37,6 +37,7 @@ import h5py
import tempfile
import os
import shutil
from silx.utils.enum import Enum
try:
import larch
except ImportError:
......@@ -51,11 +52,21 @@ else:
_logger = logging.getLogger(__name__)
class Dim(Enum):
"""Define the possible dimension of the spectra"""
X_DIM = 'X'
Y_DIM = 'Y'
CHANNEL_ENERGY_DIM = 'channel / energy'
class XASObject(object):
"""Base class of XAS
:param spectra: absorbed beam as a list of :class:`.Spectrum` or a
numpy.ndarray
numpy.ndarray. If is a numpy array:
* dim0: channel,
* dim1: Y,
* dim2: X
:type: Union[numpy.ndarray, list]
:param energy: beam energy
:type: numpy.ndarray of one dimension
......@@ -323,7 +334,8 @@ class XASObject(object):
@staticmethod
def from_file(h5_file, entry='scan1', spectra_path='data/absorbed_beam',
energy_path='data/energy', configuration_path='configuration'):
energy_path='data/energy',
configuration_path='configuration', dimensions=None):
# load only mu and energy from the file
spectra_url = DataUrl(file_path=h5_file,
data_path='/'.join((entry, spectra_path)),
......@@ -339,7 +351,8 @@ class XASObject(object):
scheme='silx')
sp, en, conf = est.io.read_xas(spectra_url=spectra_url,
channel_url=energy_url,
config_url=config_url)
config_url=config_url,
dimensions=dimensions)
return XASObject(spectra=sp, energy=en, configuration=conf)
def dump(self, h5_file):
......
......@@ -31,8 +31,9 @@ __date__ = "03/07/2019"
from silx.gui.dialog.DataFileDialog import DataFileDialog
from silx.io.url import DataUrl
from silx.gui import qt
from est.core.io import read as read_xas, read_frm_file
from est.core.io import read as read_xas, read_frm_file, Dim
from est.io import InputType
from est.core.types import Dim
import logging
_logger = logging.getLogger(__name__)
......@@ -102,6 +103,7 @@ class XASObjectDialog(qt.QWidget):
self.getEnergyUrl = self._h5Dialog.getEnergyUrl
self.setConfigurationUrl = self._h5Dialog.setConfigurationUrl
self.getConfigurationUrl = self._h5Dialog.getConfigurationUrl
self.setDimensions = self._h5Dialog.setDimensions
# default setting
self._updateWidgetVisibility()
......@@ -128,7 +130,8 @@ class XASObjectDialog(qt.QWidget):
check_url(energy_url, 'energy / channel')
return read_xas(spectra_url=self._h5Dialog.getSpectraUrl(),
channel_url=self._h5Dialog.getEnergyUrl(),
config_url=self._h5Dialog.getConfigurationUrl())
config_url=self._h5Dialog.getConfigurationUrl(),
dimensions=self._h5Dialog.getDimensions())
else:
raise ValueError('unmanaged input type')
......@@ -225,27 +228,56 @@ class _XASObjFrmH5(qt.QWidget):
# spectra url
self._spectraSelector = _URLSelector(parent=self, name='spectra url',
layout=self.layout(), position=(0, 0))
self._bufWidget = qt.QWidget(parent=self)
self._bufWidget.setLayout(qt.QHBoxLayout())
spacer = qt.QWidget(parent=self)
spacer.setSizePolicy(qt.QSizePolicy.Expanding, qt.QSizePolicy.Minimum)
self._bufWidget.layout().addWidget(spacer)
self._dimensionSelection = _SpectraDimensions(parent=self._bufWidget)
self._bufWidget.layout().addWidget(self._dimensionSelection)
self.layout().addWidget(self._bufWidget, 1, 1)
# energy / channel url
self._energySelector = _URLSelector(parent=self, name='energy /channel url',
layout=self.layout(), position=(1, 0))
layout=self.layout(), position=(2, 0))
# configuration url
self._configSelector = _URLSelector(parent=self, name='configuration url',
layout=self.layout(),
position=(2, 0))
position=(3, 0))
# connect signal / slot
self._spectraSelector._qLineEdit.textChanged.connect(self._editingIsFinished)
self._energySelector._qLineEdit.textChanged.connect(self._editingIsFinished)
self._configSelector._qLineEdit.textChanged.connect(self._editingIsFinished)
# expose APi
self.setDimensions = self._dimensionSelection.setDimensions
self.getDimensions = self._dimensionSelection.getDimensions
def getSpectraUrl(self):
return self._spectraSelector.getUrlPath()
"""
:return: the DataUrl of the spectra
:rtype: DataUrl
"""
return DataUrl(path=self._spectraSelector.getUrlPath())
def getEnergyUrl(self):
return self._energySelector.getUrlPath()
"""
:return: the DataUrl of energy / channel
:rtype: DataUrl
"""
return DataUrl(self._energySelector.getUrlPath())
def getConfigurationUrl(self):
return self._configSelector.getUrlPath()
"""
:return: the DataUrl of the configuration
:rtype: DataUrl
"""
return DataUrl(self._configSelector.getUrlPath())
def setSpectraUrl(self, url):
self._spectraSelector.setUrlPath(url)
......@@ -259,6 +291,106 @@ class _XASObjFrmH5(qt.QWidget):
def _editingIsFinished(self, *args, **kwargs):
self.editingFinished.emit()
def getDimensionsInfo(self):
"""
:return: return the information regarding each dimensions
(dim0, dim1, dim2)
:rtype: tuple
"""
return self._dimensionSelection.getDimensions()
class _QDimComboBox(qt.QComboBox):
def __init__(self, parent):
qt.QComboBox.__init__(self, parent)
self.addItem(Dim.X_DIM.value)
self.addItem(Dim.Y_DIM.value)
self.addItem(Dim.CHANNEL_ENERGY_DIM.value)
def setDim(self, dim):
dim = Dim.from_value(dim)
assert dim in (Dim.X_DIM, Dim.Y_DIM, Dim.CHANNEL_ENERGY_DIM)
index = self.findText(dim.value)
assert index >= 0
self.setCurrentIndex(index)
class _SpectraDimensions(qt.QWidget):
def __init__(self, parent):
qt.QWidget.__init__(self, parent=parent)
self.setLayout(qt.QFormLayout())
self._dim0 = _QDimComboBox(parent=self)
self.layout().addRow('dim 0', self._dim0)
self._dim1 = _QDimComboBox(parent=self)
self.layout().addRow('dim 1', self._dim1)
self._dim2 = _QDimComboBox(parent=self)
self.layout().addRow('dim 2', self._dim2)
# set up
self._dim0.setDim(Dim.CHANNEL_ENERGY_DIM)
self._dim1.setDim(Dim.Y_DIM)
self._dim2.setDim(Dim.X_DIM)
# connect Signal / Slot
self._dim0.currentIndexChanged.connect(self._insureDimUnicity)
self._dim1.currentIndexChanged.connect(self._insureDimUnicity)
self._dim2.currentIndexChanged.connect(self._insureDimUnicity)
def _insureDimUnicity(self):
last_modified = self.sender()
get_second = self._dim0 if last_modified != self._dim0 else self._dim1
get_third = self._dim2 if last_modified == self._dim0 else self._dim1
unique_value = last_modified.currentText()
def getUnsetDimension():
values = self.getDimensions()
if Dim.X_DIM not in values:
return Dim.X_DIM
elif Dim.Y_DIM not in values:
return Dim.Y_DIM
elif Dim.CHANNEL_ENERGY_DIM not in values:
return Dim.CHANNEL_ENERGY_DIM
else:
return None
def _updateIfNecessary(dimCB, value):
"""Change the value of the combobox of the value == value for an
unset value"""
if dimCB.currentText() == value:
text = getUnsetDimension()
if text is None:
return
index = dimCB.findText(text)
assert index >= 0
dimCB.setCurrentIndex(index)
_updateIfNecessary(get_third, unique_value)
_updateIfNecessary(get_second, unique_value)
def getDimensions(self):
"""
:return: return the information regarding each dimensions
(dim0, dim1, dim2)
:rtype: tuple
"""
return (self._dim0.currentText(),
self._dim1.currentText(),
self._dim2.currentText()
)
def setDimensions(self, dims):
"""
:param dims: tuple containing (dim0, dim1, dim2)
:type: tuple
"""
assert isinstance(dims, tuple)
assert len(dims) == 3
self._dim0.setDim(dims[0])
self._dim1.setDim(dims[1])
self._dim2.setDim(dims[2])
if __name__ == '__main__':
app = qt.QApplication([])
......
......@@ -29,7 +29,6 @@ __date__ = "06/12/2019"
import logging
from datetime import datetime
import h5py
import numpy
from silx.io import utils
......@@ -59,12 +58,16 @@ class InputType(Enum):
xmu_spectrum = '*.xmu' # contains one spectrum
def read_xas(spectra_url, channel_url, config_url=None):
def read_xas(spectra_url, channel_url, dimensions=None, config_url=None):
"""
Read the given spectra url and the config url if any
:param Union[DataUrl, str] spectra_url:
:param DataUrl config_url:
:param DataUrl config_url:
:param dimensions: dimensions of the spectra. If None will be set to the
default (channel, Y, x)
:type: Union[tuple,None]
:return: spectra, energy, configuration
"""
def get_url(original_url, name):
......@@ -123,10 +126,37 @@ def read_xas(spectra_url, channel_url, config_url=None):
else:
_logger.warning('invalid url for', name, ', will not load it')
from est.core.types import Dim # avoid cyclic import
if dimensions is None:
dimensions_ = (Dim.CHANNEL_ENERGY_DIM, Dim.Y_DIM, Dim.X_DIM)
else:
dimensions_ = []
for dim in dimensions:
dimensions_.append(Dim.from_value(dim))
spectra = load_data(_spectra_url, name='spectra')
# make sure all dimensions are defined
for dim in Dim:
if not dim in dimensions_:
err = '%s is not defined in the dimensions' % dim
raise ValueError(err)
# fit spectra according to dimension
src_axis = (
dimensions_.index(Dim.CHANNEL_ENERGY_DIM),
dimensions_.index(Dim.Y_DIM),
dimensions_.index(Dim.X_DIM)
)
dst_axis = (0, 1, 2)
_logger.info('move axis for spectra %s, from %s to %s' % (spectra_url, src_axis, dst_axis))
spectra = numpy.moveaxis(spectra, src_axis, dst_axis)
energy = load_data(_energy_url, name='energy')
configuration = load_data(_config_url, name='configuration')
if not energy.ndim == 1:
raise ValueError('Energy / channel is not 1D')
if not energy.shape[0] == spectra.shape[0]:
err = 'Energy / channel and spectra dim1 have incoherent length (%s vs %s)' % (energy.shape[0], spectra.shape[0])
raise ValueError(err)
return (spectra, energy, configuration)
......
......@@ -67,6 +67,7 @@ class XASInputOW(OWWidget):
_spectra_url_setting = Setting(str())
_energy_url_setting = Setting(str())
_configuration_url_setting = Setting(str())
_dimensions_setting = Setting(tuple())
process_function = est.core.io.read_frm_file
......@@ -138,6 +139,10 @@ class XASInputOW(OWWidget):
load_url(self._spectra_url_setting, self._inputDialog.setSpectraUrl)
load_url(self._energy_url_setting, self._inputDialog.setEnergyUrl)
load_url(self._configuration_url_setting, self._inputDialog.setConfigurationUrl)
if len(self._dimensions_setting) == 3:
self._inputDialog.setDimensions(self._dimensions_setting)
else:
assert len(self._dimensions_setting) == 0
# set up
self._inputDialog.setCurrentType(input_type)
......@@ -147,6 +152,7 @@ class XASInputOW(OWWidget):
self._spectra_url_setting = self._inputDialog.getSpectraUrl()
self._energy_url_setting = self._inputDialog.getEnergyUrl()
self._configuration_url_setting = self._inputDialog.getConfigurationUrl()
self._dimensions_setting = self._inputDialog.getDimensions()
def sizeHint(self):
return qt.QSize(400, 200)
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment