Commit 00338127 authored by payno's avatar payno
Browse files

[gui] add dimension management to the gui and steam operations

parent a7126863
......@@ -18,6 +18,7 @@ Widgets
widgets/com
widgets/datareduction
widgets/dataselection
widgets/dimension
widgets/geometry
widgets/mapping
widgets/metadata
......
dimension definition
====================
.. snapshotqt:: orangecontrib/id06workflow/widgets/screenshots/dimension_definition_screenshot.py
Signals
-------
- (Experiment)
**Outputs**:
- (Experiment)
Description
-----------
Used to define the dimension in order to interpret correctly the dataset.
......@@ -55,6 +55,12 @@ _METADATA_TYPES = {
'positioner': POSITIONER_METADATA,
}
_METADATA_TYPES_I = {}
"""used to retrieve the metadata name (str) for the silx.io.fabioh5 id"""
for key, value in _METADATA_TYPES.items():
assert value not in _METADATA_TYPES_I
_METADATA_TYPES_I[value] = key
class Experiment(object):
"""
......@@ -319,8 +325,11 @@ class Experiment(object):
def set_dims(self, dims):
assert isinstance(dims, dict)
self.__dims.clear()
_fail = False, None # register fail status and latest reason
for axis, dim in dims.items():
print(" ##### axis %s" % axis)
if dim.size is None:
assert isinstance(dim, _Dim)
# try to deduce dimension size
try:
unique_dim_value = metadatautils.getUnique(self,
......@@ -329,11 +338,16 @@ class Experiment(object):
relative_prev_val=dim.relative_prev_val,
cycle_length=dim.size,
axis=axis)
dim.size = len(unique_dim_value)
print('unique_dim_value: %s' % unique_dim_value)
except Exception as e:
_logger.warning('Fail to deduce size of axis %s because %s' % (axis, e))
print('add dim for axis %s' % axis)
self.__dims.add_dim(axis=axis, dim=dims[axis])
if _fail[0] is False:
_fail = True, e
else:
dim._setSize(size=len(unique_dim_value))
self.__dims.add_dim(axis=axis, dim=dim)
if _fail[0] is True:
raise ValueError('Fail to define all diemnsion size. Latest error first error was %s' % _fail[1])
class AcquisitionDims(object):
......@@ -344,7 +358,7 @@ class AcquisitionDims(object):
self.__dims = {}
def add_dim(self, axis, dim):
assert type(dim) is _Dim
assert isinstance(dim, _Dim)
self.__dims[axis] = dim
def clear(self):
......@@ -406,17 +420,37 @@ class _Dim(object):
else:
self.__kind = kind
self.__name = name
self.size = size
self._size = size
self.__relative_prev_val = relative_prev_val
@property
def kind(self):
return self.__kind
def _setKind(self, kind):
self.__kind = kind
@property
def name(self):
return self.__name
def _setName(self, name):
self.__name = name
@property
def relative_prev_val(self):
return self.__relative_prev_val
def _set_relative_prev_val(self, value):
self.__relative_prev_val = value
@property
def size(self):
return self._size
def _setSize(self, size):
"""
.. note: having a setter was needed for GUI and Signal?SLOT stuff
(see :class:`_DimensionItem`)
"""
self._size = size
......@@ -93,13 +93,13 @@ class ThresholdRemoval(OverwritingOperation):
if None given, will set the value of the
threshold
"""
if self._compute(self.data):
if self._compute(self.data_flatten):
self.registerOperation()
return self.data
return self.data_flatten
def dry_run(self, cache_data=None):
if cache_data is None:
self._cache_data = self.data[...]
self._cache_data = self.data_flatten[...]
else:
self._cache_data = cache_data
return self._compute(self._cache_data)
......@@ -141,13 +141,13 @@ class BackgroundSubtraction(OverwritingOperation):
return self._name
def compute(self):
if self._compute(self.data):
if self._compute(self.data_flatten):
self.registerOperation()
return self.data
return self.data_flatten
def dry_run(self, cache_data=None):
if cache_data is None:
self._cache_data = self.data[...]
self._cache_data = self.data_flatten[...]
else:
self._cache_data = cache_data
self._cache_data = self._compute(self._cache_data)
......@@ -159,7 +159,7 @@ class BackgroundSubtraction(OverwritingOperation):
self.data_flatten = self._cache_data
self.clear_cache()
self.registerOperation()
return self.data
return self.data_flatten
def _compute(self, data):
if data is None:
......@@ -221,16 +221,17 @@ class MaskSubstraction(OverwritingOperation):
'scale:', str(self._scale)))
def compute(self):
self._compute(self._experiment.data)
self._compute(self._experiment.data_flatten)
self.registerOperation()
return self._experiment.data
return self._experiment.data_flatten
def clear_cache(self):
self._cache_data = None
def dry_run(self, cache_data=None):
if cache_data is None:
self._cache_data = self._experiment.data[...]
assert cache_data.ndim is 3
self._cache_data = self._experiment.data_flatten[...]
else:
self._cache_data = cache_data
return self._compute(self._cache_data)
......@@ -239,9 +240,10 @@ class MaskSubstraction(OverwritingOperation):
if self._cache_data is None:
raise ValueError('No data in cache')
self.registerOperation()
return self._experiment.data
return self._experiment.data_flatten
def _compute(self, data):
assert data.ndim is 3
return self.apply_mask(data, self.mask_type, self.scale)
@staticmethod
......
......@@ -52,15 +52,15 @@ class TestOperationStream(unittest.TestCase):
ff_files=data_bb_files)
self.experiment = Experiment(dataset=self.dataset, geometry=None)
def applyGeometry(self):
"""Apply some geometry"""
def applyDimsDef(self):
"""Apply experimentation dimension definition"""
dim1 = _Dim(kind=POSITIONER_METADATA, name='diffry',
relative_prev_val=True, size=31)
dim2 = _Dim(kind=POSITIONER_METADATA, name='obpitch')
self.experiment.set_dims(dims={0: dim1, 1: dim2})
def testGeometry(self):
"""Make sure geometry is well applied"""
def testDimensionManual(self):
"""Make sure dimensions are well applied"""
self.assertTrue(len(metadatautils.getUnique(self.experiment,
kind=POSITIONER_METADATA,
key='diffry',
......@@ -71,7 +71,7 @@ class TestOperationStream(unittest.TestCase):
self.assertTrue(len(metadatautils.getUnique(self.experiment,
kind=POSITIONER_METADATA,
key='obpitch')) is 2)
self.applyGeometry()
self.applyDimsDef()
self.assertTrue(self.experiment.dims.ndim is 2)
self.assertTrue(self.experiment.dims.shape == (31, 2))
self.assertTrue(self.experiment.data.shape == (31, 2, 2048, 2048))
......
......@@ -74,7 +74,7 @@ def getUnique(experiment, key, kind, relative_prev_val=False, cycle_length=None,
else:
res.append(value[0])
except Exception as e:
_logger.error("fail to load %s, %s cause %s" % (kind, key, e))
_logger.error("fail to load %s, %s cause %s" % (kind, key, e.message))
if cycle_length is not None and len(res) >= cycle_length:
break
return numpy.unique(res)
# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2016-2017 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__ = "03/10/2018"
from silx.gui import qt
from id06workflow.core.experiment import _Dim, Experiment
from id06workflow.core.experiment import _METADATA_TYPES, _METADATA_TYPES_I, DEFAULT_METADATA
import logging
_logger = logging.getLogger(__file__)
class DimensionMapping(qt.QWidget):
"""
Widget used to define the number of dimension and with which values they are
mapped
"""
_V_HEADERS = ['Axis', 'Kind', 'Name', 'Size', 'Is relative', '']
def __init__(self, parent):
qt.QWidget.__init__(self, parent)
self.setLayout(qt.QGridLayout())
self._table = qt.QTableWidget(parent=self)
self._table.setColumnCount(6)
self._table.setHorizontalHeaderLabels(self._V_HEADERS)
header = self._table.horizontalHeader()
if qt.qVersion() < "5.0":
setResizeMode = header.setResizeMode
else:
setResizeMode = header.setSectionResizeMode
for iColumn in (0, 3, 4, 5):
setResizeMode(iColumn, qt.QHeaderView.ResizeToContents)
for iColumn in (1, 2):
setResizeMode(iColumn, qt.QHeaderView.Stretch)
self._table.verticalHeader().hide()
self._dims = {}
self.layout().addWidget(self._table, 0, 0, 6, 6)
self._addButton = qt.QPushButton('add', parent=self)
self.layout().addWidget(self._addButton, 6, 5, 1, 1)
# connect Signal/SLOT
self._addButton.pressed.connect(self.addDim)
def clear(self):
widgets = list(self._dims.values())
for widget in widgets:
self.removeDim(widget)
@property
def ndim(self):
return len(self._dims)
@property
def dims(self):
return self._dims
def addDim(self, axis=None, dim=None):
"""
:param axis: which axis is defining this dimension
:param :class:_Dim dim: definition of the dimension to add
"""
if axis is None:
axis = self._getNextFreeAxis()
row = self._table.rowCount()
self._table.setRowCount(row + 1)
widget = _DimensionItem(parent=self, table=self._table, row=row)
widget.removed.connect(self.removeDim)
if dim is not None:
widget.setDim(dim)
widget.axis = axis
self._dims[row] = widget
print('add a widget to the dim')
return widget
def setDims(self, dims):
"""
:param dict dims: axis as key and :class:`_Dim` as value
:return:
"""
self.clear()
for axis, dim in dims.items():
assert type(axis) is int
assert isinstance(dim, _Dim)
self.addDim(axis=axis, dim=dim)
def removeDim(self, row):
"""
:param int or _DimensionItem row: row or item to remove
"""
if isinstance(row, _DimensionItem):
iRow = row._row
else:
iRow = row
self._table.setRowCount(self._table.rowCount() - 1)
self._table.removeRow(iRow)
self._dims[iRow].removed.disconnect(self.removeDim)
self._dims[iRow].setAttribute(qt.Qt.WA_DeleteOnClose)
self._dims[iRow].close()
del self._dims[iRow]
ini_rows = sorted(list(self._dims.keys()))
for row in ini_rows:
if row <= iRow:
continue
widget = self._dims[row]
new_row = row - 1
assert new_row >= 0
widget.embedInTable(table=self._table, row=new_row)
self._dims[new_row] = widget
del self._dims[row]
def _getNextFreeAxis(self):
"""
:return int: next unused axis
"""
res = 0
usedAxis = []
[usedAxis.append(_dim.axis) for _dim in self._dims.values()]
while res in usedAxis:
res = res + 1
return res
class DimensionWidget(DimensionMapping):
"""
Widget to define dimensions and try to fit those with experiment dataset
"""
fitSucceed = qt.Signal()
"""Signal emitted when the fit succeed"""
fitFailed = qt.Signal()
"""Signal emitted when the fit fail"""
def __init__(self, parent):
DimensionMapping.__init__(self, parent)
print('Create Diemsnion widget')
self.__experiment = None
self._fitButton = qt.QPushButton('fit', parent=self)
self.layout().addWidget(self._fitButton, 6, 0, 1, 1)
# connect Signal/SLOT
self._fitButton.pressed.connect(self.fit)
@property
def experiment(self):
return self.__experiment
def setExperiment(self, experiment):
"""
:param experiment: the experiment for which we want to define the
dimensions.
:type experiment: :class:`Experiment`
"""
print('set experiment')
assert isinstance(experiment, Experiment)
self.__experiment = experiment
if len(self.experiment.metadata) > 0:
for widget in self._dims.values():
widget._setMetadata(self.experiment.metadata[0])
def fit(self):
"""
Fit current dimension size to the dataset
:return : return status of the fit and fail reasonif any
:rtype: (bool str or None)
"""
if self.__experiment is None:
_logger.warning('No experiment to be fitted')
return
if self.ndim is 0:
_logger.warning('No Dim defined to fit experiment dataset')
return
try:
self.__experiment.set_dims(self.dims)
except Exception as e:
self.fitFailed.emit()
return False, e
else:
self.fitSucceed.emit()
return True, None
class _DimensionItem(_Dim, qt.QWidget):
"""Widget use to define a dimension"""
removed = qt.Signal(qt.QObject)
"""Signal emitted when the Item should be removed"""
dimValueChanged = qt.Signal()
"""Signal emitted when the dimension definition is changed"""
axisChanged = qt.Signal(int, int)
"""Signal emitted when the axis value is changed: id (row), new_value_value
"""
class _SizeWidget(qt.QWidget):
valueChanged = qt.Signal(int)
"""Signal emitted when the value of the Spin box change but decorelated
with the active state
"""
def __init__(self, parent):
qt.QWidget.__init__(self, parent)
self.setLayout(qt.QVBoxLayout())
self._sizeSP = qt.QSpinBox(parent=self)
"""_sizeSP will be used to define the size. Will be editable if the
size editable (so if dimension is set from a relative parameter)
"""
self.layout().addWidget(self._sizeSP)
self.layout().setContentsMargins(0, 0, 0, 0)
self._active = False
self._sizeSP.hide()
# expose API
self.setMinimum = self._sizeSP.setMinimum
# connect Signal/SLOT
self._sizeSP.valueChanged.connect(self._valueHasChanged)
def setValue(self, size, editable=True):
"""
:param int or None size: the size of the dimension. If None, not
define yet
:param editable: the size will be editable if given by the use (so)
if is relative.
"""
if size is None:
self._sizeSP.hide()
self._sizeLabel.hide()
else:
assert type(size) is int
self._sizeSP.show()
self._sizeSP.setValue(size)
self._sizeSP.setEnabled(editable)
def toggle(self, checked):
self._active = checked
if checked:
self._sizeSP.show()
else:
self._sizeSP.hide()
def _valueHasChanged(self, value):
self.valueChanged.emit(value)
def __init__(self, parent, table, row):
"""
:param QTableWidget table: if has to be embed in a table the
parent table
:param int row: row position in the QTableWidget. Also used as ID
"""
qt.QWidget.__init__(self, parent)
_Dim.__init__(self, kind=DEFAULT_METADATA, name='')
self.__metadata = None
# axis
self._axis = qt.QSpinBox(parent=self)
self._axis.setMinimum(0)
# kind
self._kindCB = qt.QComboBox(parent=self)
for _kindName in _METADATA_TYPES:
self._kindCB.addItem(_kindName)
# name
self._namesCB = qt.QComboBox(parent=self)
# size
self._sizeWidget = self._SizeWidget(parent=self)
self._sizeWidget.setMinimum(0)
# relative
self._relativeCB = qt.QCheckBox(parent=self)
# rm button
style = qt.QApplication.style()
icon = style.standardIcon(qt.QStyle.SP_BrowserStop)
self._rmButton = qt.QPushButton(icon=icon, parent=self)
# connect Signal/slot
self._rmButton.pressed.connect(self.remove)
self._relativeCB.toggled.connect(self._sizeWidget.toggle)
self._axis.valueChanged.connect(self._axisHasChanged)
self._kindCB.currentIndexChanged.connect(self._dimHasChanged)
self._sizeWidget.valueChanged.connect(self._dimHasChanged)
self._namesCB.currentIndexChanged.connect(self._dimHasChanged)
self._relativeCB.toggled.connect(self._dimHasChanged)
self._kindCB.currentIndexChanged.connect(self._updateNames)
# update values from _Dim
self._kindCB.currentTextChanged.connect(self._setKind)
self._namesCB.currentTextChanged.connect(self._setName)
self._sizeWidget.valueChanged.connect(self._setSize)
self._relativeCB.toggled.connect(self._set_relative_prev_val)
self.embedInTable(table=table, row=row)
self.__row = row
def _axisHasChanged(self, value):
self.axisChanged.emit(self._row, value)
def _dimHasChanged(self, *args, **kwargs):
self.dimValueChanged.emit()
def remove(self):
self.removed.emit(self)
def setDim(self, dim):
assert isinstance(dim, _Dim)
print('should set %s' % dim.name)
idx = self._namesCB.findText(dim.name)
if idx >= 0:
self._namesCB.setCurrentIndex(idx)
else:
self._namesCB.addItem(dim.name)
_kind = _METADATA_TYPES_I[dim.kind]
idx = self._kindCB.findText(_kind)
assert idx >= 0
self._namesCB.setCurrentIndex(idx)
if dim.size is not None:
self._sizeWidget.setValue(dim.size)
self._relativeCB.setChecked(dim.relative_prev_val)
@property
def _row(self):
return self.__row
@property
def axis(self):
return self._axis.value()
@axis.setter
def axis(self, axis):