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
# /*##########################################################################
#
# 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__ = "01/10/2018"
from silx.gui import qt
from Orange.widgets import gui
from Orange.widgets.widget import OWWidget
from Orange.canvas.registry.description import InputSignal, OutputSignal
from id06workflow.gui.dimensions import DimensionWidget
from id06workflow.core.experiment import Experiment
from functools import partial
import logging
_logger = logging.getLogger(__file__)
class DimensionOW(OWWidget):
"""
Widget used to define the calibration of the experimentation (select motor
positions...)
"""
name = "dimension definition"
id = "orange.widgets.id06workflow.dimensiondefinition"