Commit 00338127 authored by payno's avatar payno

[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)
This diff is collapsed.
......@@ -30,7 +30,6 @@ __date__ = "03/10/2018"
from silx.gui import qt
from id06workflow.core.geometry.TwoThetaGeometry import TwoThetaGeometry
from id06workflow.core.experiment import _METADATA_TYPES, _Dim
from id06workflow.gui.utils import _IllustrationWidget
from id06workflow.core.utils.char import THETA_CHAR
import logging
......@@ -130,15 +129,6 @@ class TwoThetaGeometryWidget(TwoThetaGeometry, qt.QWidget):
self.layout().addWidget(self._orientationCB, 4, 1)
# TODO: user should be able to save the configuration of the geometry
self.layout().addWidget(qt.QLabel('dim 1'), 5, 0)
self._dim1 = self.DimensionComboBox(parent=self, default='diffry',
locked=True)
self.layout().addWidget(self._dim1, 5, 1)
self.layout().addWidget(qt.QLabel('dim 2'), 6, 0)
self._dim2 = self.DimensionComboBox(parent=self, default='obpitch',
locked=True)
self.layout().addWidget(self._dim2, 6, 1)
def getSetupGeometry(self):
orientation = self._orientationCB.currentText()
assert orientation in TwoThetaGeometry.ORIENTATIONS
......@@ -185,194 +175,3 @@ class TwoThetaGeometryWidget(TwoThetaGeometry, qt.QWidget):
index = self._orientationCB.findText(orientation)
assert index >= 0
self._orientationCB.setCurrentIndex(index)
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)
self._table.verticalHeader().hide()
self._dims = {}
self.layout().addWidget(self._table, 0, 0, 6, 6)
self._addButton = qt.QPushButton('add dimension', parent=self)
self.layout().addWidget(self._addButton, 6, 5, 1, 1)
@property
def ndim(self):
return len(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
return widget
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 _DimensionItem(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
"""
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)
# axis
self._axis = qt.QSpinBox(parent=self)
self._axis.setMinimum(0)
# kind
self._kind = qt.QComboBox(parent=self)
for _kindName in _METADATA_TYPES:
self._kind.addItem(_kindName)
# name
self._names = qt.QComboBox(parent=self)
# size
self._size = qt.QSpinBox(parent=self)
self._size.setMinimum(0)
# relative
self._relative = qt.QCheckBox(parent=self)
self._relative.setVisible(False)
# 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._relative.toggled.connect(self._size.setVisible)
self._axis.valueChanged.connect(self._axisHasChanged)
self._kind.currentIndexChanged.connect(self._dimHasChanged)
self._size.valueChanged.connect(self._dimHasChanged)
self._names.currentIndexChanged.connect(self._dimHasChanged)
self._relative.toggled.connect(self._dimHasChanged)
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)
@property
def _row(self):
return self.__row
@property
def axis(self):
return self._axis.value()
@axis.setter
def axis(self, axis):
assert type(axis) is int
self._axis.setValue(axis)
@property
def kind(self):
return self._kind.currentText()
@property
def dim(self):
return _Dim(name=self.name, kind=self.kind, size=self.dimsize,
relative_prev_val=self.relative)
@property
def dimsize(self):
if self.relative is True:
return self._size.value()
else:
return None
@property
def relative(self):
return self._relative.isChecked()
@property
def name(self):
return self._names.currentText()
def setNames(self, names):
self._names.clear()
for name in names:
self._names.addItem(name)
def embedInTable(self, table, row):
self.__row = row
for column, widget in enumerate((self._axis, self._kind, self._names,
self._size, self._relative, self._rmButton)):
table.setCellWidget(row, column, widget)
......@@ -78,7 +78,7 @@ class NoiseReductionWidget(qt.QWidget):
self._maskCorrection = None
def _updateCorrection(self):
self._cache_data = self._experiment.data[...]
self._cache_data = self._experiment.data_flatten[...]
if self._params_has_changed is True:
self._params_has_changed = False
......@@ -106,7 +106,7 @@ class NoiseReductionWidget(qt.QWidget):
self._lowerThreshold.threshold = threshold
self._cache_data = self._lowerThreshold.dry_run(self._cache_data)
self._display.set(self._experiment.data,
self._display.set(self._experiment.data_flatten,
self._cache_data)
def validate_correction(self):
......
......@@ -29,11 +29,13 @@ __date__ = "29/05/2017"
import unittest
from . import test_geometry_gui
from . import test_dimension_gui
def suite():
test_suite = unittest.TestSuite()
test_suite.addTests([
test_geometry_gui.suite(),
test_dimension_gui.suite(),
])
return test_suite
# 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"
import unittest
from id06workflow.gui.dimensions import DimensionMapping
from silx.gui import qt
# TODO: look, not orking with the TestCaseQt, has unreleased widgets
class TestDimensionMapping(unittest.TestCase):
"""
Make sure the :class:`DimensionMapping` is correctly adding and removing
dimension items
"""
def setUp(self):
unittest.TestCase.setUp(self)
self._app = qt.QApplication.instance() or qt.QApplication([])
self.widget = DimensionMapping(parent=None)
def tearDown(self):
self.widget.setAttribute(qt.Qt.WA_DeleteOnClose)
self.widget.close()
unittest.TestCase.tearDown(self)
def testAddRmItems(self):
"""
Make sure adding and removing dimension items are correct
"""
dim1 = self.widget.addDim()
dim2 = self.widget.addDim()
self.assertTrue(self.widget.ndim is 2)
self.assertTrue(dim1.axis is 0)
self.assertTrue(dim2.axis is 1)
dim1.axis = 4
self.assertTrue(dim1.axis is 4)
self.widget.removeDim(dim1)
self.assertTrue(self.widget.ndim is 1)
dim2.remove()
self.assertTrue(self.widget.ndim is 0)
self.widget.addDim()
self.widget.addDim()
self.assertTrue(self.widget.ndim is 2)
self.widget.clear()
self.assertTrue(self.widget.ndim is 0)
def suite():
test_suite = unittest.TestSuite()
for ui in (TestDimensionMapping, ):
test_suite.addTest(unittest.defaultTestLoader.loadTestsFromTestCase(ui))
return test_suite
if __name__ == '__main__':
unittest.main(defaultTest="suite")
......@@ -29,9 +29,8 @@ __date__ = "03/10/2018"
import unittest
from id06workflow.gui.geometry import TwoThetaGeometryWidget, DimensionMapping
from id06workflow.gui.geometry import TwoThetaGeometryWidget
from silx.gui.utils.testutils import TestCaseQt
from silx.gui import qt
class TestTwoThetaExpSetupGUI(TestCaseQt):
......@@ -47,42 +46,9 @@ class TestTwoThetaExpSetupGUI(TestCaseQt):
widget = TwoThetaGeometryWidget
# TODO: look, not orking with the TestCaseQt, has unreleased widgets
class TestDimensionMapping(unittest.TestCase):
"""
Make sure the :class:`DimensionMapping` is correctly adding and removing
dimension items
"""
def setUp(self):
unittest.TestCase.setUp(self)
self._app = qt.QApplication.instance() or qt.QApplication([])
self.widget = DimensionMapping(parent=None)
def tearDown(self):
self.widget.setAttribute(qt.Qt.WA_DeleteOnClose)
self.widget.close()
unittest.TestCase.tearDown(self)
def testAddRmItems(self):
"""
Make sure adding and removing dimension items are correct
"""
dim1 = self.widget.addDim()
dim2 = self.widget.addDim()
self.assertTrue(self.widget.ndim is 2)
self.assertTrue(dim1.axis is 0)
self.assertTrue(dim2.axis is 1)
dim1.axis = 4
self.assertTrue(dim1.axis is 4)
self.widget.removeDim(dim1)
self.assertTrue(self.widget.ndim is 1)
dim2.remove()
self.assertTrue(self.widget.ndim is 0)
def suite():
test_suite = unittest.TestSuite()
for ui in (TestTwoThetaExpSetupGUI, TestDimensionMapping):
for ui in (TestTwoThetaExpSetupGUI, ):
test_suite.addTest(unittest.defaultTestLoader.loadTestsFromTestCase(ui))
return test_suite
......
......@@ -65,6 +65,19 @@ log = logging.getLogger(__name__)
app = QApplicationManager()
if AnyQt.USED_API == 'pyside':
from PySide.QtTest import QTest
elif AnyQt.USED_API == 'pyside2':
from PySide2.QtTest import QTest
elif AnyQt.USED_API == 'pyqt5':
from PyQt5.QtTest import QTest
elif AnyQt.USED_API == 'pyqt4':
from PyQt4.QtTest import QTest
else:
raise ImportError('Unsupported Qt bindings')
class OrangeWorflowTest(TestCase):
"""Define a specific TestCase reltive to OrangeWorkflow"""
......@@ -173,6 +186,9 @@ class OrangeWorflowTest(TestCase):
self.processOrangeEvents()
self.processOrangeEventsStack()
def qWait(self, time_ms):
QTest.qWait(time_ms)
def init(self):
# Fix streams before configuring logging (otherwise it will store
# and write to the old file descriptors)
......
# coding: utf-8
# /*##########################################################################
#