...
 
Commits (37)
......@@ -35,6 +35,8 @@ doc:
- python -m pip install silx --upgrade --pre
# for now we need to install nabu from sources
- install_nabu_master
- install_tomoscan
- install_nxtomomill
- python -m pip install jupyterlab
- python -m pip install Sphinx
- python -m pip install nbsphinx
......@@ -67,7 +69,8 @@ doc:
- source ./ci/install_scripts.sh
- install_orange3
- install_anyqt 'master'
- install_bliss
- install_tomoscan
- install_nxtomomill
- python -m pip install -r requirements-dev.txt
- python -m pip install -r requirements-test.txt
- python -m pip install fabio --upgrade --pre
......@@ -96,6 +99,7 @@ test:test-tomwer-tutorials:
- export ORANGE_WEB_LOG='False'
- python --version
- source ./ci/install_scripts.sh
- install_tomoscan
- python -m pip install pip --upgrade
- python -m pip install setuptools --upgrade
- python -m pip install numpy --upgrade
......@@ -103,7 +107,6 @@ test:test-tomwer-tutorials:
- python -m pip install jupyter_client
- python -m pip install nbconvert
- python -m pip install ipykernel
- install_bliss
- python -m pip install -r requirements-dev.txt
- python -m pip install -r requirements-test.txt
- python -m pip install fabio --upgrade --pre
......
......@@ -22,12 +22,15 @@ Change Log
* add interface for data listener
* add interface for sinogram viewer
* axis: update interface
* viewer: add prototype of the next data viewer
* orangecontrib
* add sinogram widget: used to compute a sinogram from an edf or hdf5 (NXtomo) acquisition
* add data listener widget: used to connect with a tango device and retrieve information from it.
* add nabu widget: used to reconstruct using the nabu library
* add nxtomomill widget: used to convert from a bliss.hdf5 acquisition to a (NXTomo) .nx acquisition
* add prototype of the DataViewer
* add prototype of the new DataValidator (Move previous one to OldDataValidatorOW)
* app
* add nabu application: GUI for calling nabu or create nabu configuration file
......
......@@ -23,35 +23,26 @@ function silx_version(){
function install_nxtomomill(){
git clone --single-branch --branch master https://gitlab.esrf.fr/tomotools/nxtomomill.git
git clone --single-branch --branch 0.3 https://gitlab.esrf.fr/tomotools/nxtomomill.git
cd nxtomomill
python -m pip install .
cd ..
}
function install_orange3(){
git clone --single-branch --branch master https://github.com/payno/orange3.git
cd orange3
function install_tomoscan(){
git clone --single-branch --branch 0.3 https://gitlab.esrf.fr/tomotools/tomoscan.git
cd tomoscan
python -m pip install .
cd ..
}
function install_bliss(){
# install bliss with minimal dependacy
git clone --single-branch --branch master https://gitlab.esrf.fr/bliss/bliss.git
cd bliss
# move to the 1.3.0 tag
git checkout 1.3.0
git apply ../ci/patch_bliss.txt
python -m pip install louie
python -m pip install gevent
python -m pip install netifaces
python -m pip install treelib
python -m pip install tabulate
python -m pip install typeguard
python -m pip install cerberus
python -m pip install . --no-deps
function install_orange3(){
git clone --single-branch --branch master https://github.com/payno/orange3.git
cd orange3
python -m pip install .
cd ..
}
......
......@@ -89,7 +89,9 @@ This is the list of actual widgets/boxes proposed by the workflow tool:
widgets/datalistener.rst
widgets/dataselectorwidget.rst
widgets/datatransfertwidget.rst
widgets/dataviewerwidget.rst
widgets/datavalidatorwidget.rst
widgets/olddatavalidatorwidget.rst
widgets/imagestackviewerwidget.rst
widgets/datawatcherwidget.rst
widgets/ftserieswidget.rst
......
data validator
``````````````
.. snapshotqt:: img/scanvalidatorwidget.png
import tempfile
from tomwer.test.utils import UtilsTest
from orangecontrib.tomwer.widgets.control.DataValidatorOW import DataValidatorOW
from tomwer.gui.viewerqwidget import ImageStackViewerValidator
from tomwer.core.scan.scanfactory import ScanFactory
from tomwer.core import utils
folder = UtilsTest.getInternalTestDir("001_0.28_19keV_Al63")
scan = ScanFactory.create_scan_object(folder)
widget = DataValidatorOW()
widget.addScan(scan)
widget.widget.stackImageViewerTab.setCurrentIndex(1)
widget.show()
.. image:: img/datavalidator.png
Widget displaying results of a reconstruction and asking to the user if he want to validate or not the reconstruction.
User can also ask for some modification on the reconstruction parameters.
......
data viewer
```````````
.. image:: img/dataviewer.png
Allow browsing through data (raw or normalized radios) and reconstructed slices.
(old) data validator
````````````````````
deprecated; use data validator instead.
.. snapshotqt:: img/oldscanvalidatorwidget.png
import tempfile
from tomwer.test.utils import UtilsTest
from orangecontrib.tomwer.widgets.control.DataValidatorOW import DataValidatorOW
from tomwer.gui.viewerqwidget import ImageStackViewerValidator
from tomwer.core.scan.scanfactory import ScanFactory
from tomwer.core import utils
folder = UtilsTest.getInternalTestDir("001_0.28_19keV_Al63")
scan = ScanFactory.create_scan_object(folder)
widget = DataValidatorOW()
widget.addScan(scan)
widget.widget.stackImageViewerTab.setCurrentIndex(1)
widget.show()
Widget displaying results of a reconstruction and asking to the user if he want to validate or not the reconstruction.
User can also ask for some modification on the reconstruction parameters.
Behavior
''''''''
Buttons
*******
* 'Change reconstruction parameters' will send a signal to the ftseries widget and ask for the user new reconstruction parameters (and then run again a reconstruction)
* 'Validate' will send a signal to the next connected boxes saying this data is ok for nect operations
This inherit from [ImageStackViewerWidget](imagestackviewer.html)
......@@ -159,16 +159,7 @@ class TestCopyNFolder(OrangeWorflowTest):
time.sleep(0.2)
self.assertTrue(self.dataHasBeenCopied())
signal_manager = self.canvas_window.current_document().scheme().signal_manager
timeout = self.TIMEOUT_TEST
while timeout > 0 and self.app.hasPendingEvents():
timeout = timeout - 0.2
self.app.processEvents()
print('blocking_nodes:', signal_manager.pending_nodes())
time.sleep(0.2)
if timeout <= 0.0:
raise TimeoutError('Loop infinitely on some qt event...')
self.app.processEvents()
def outpudirIsEmpty(self):
return len(os.listdir(self.outputdir)) == 0
......
This diff is collapsed.
......@@ -43,6 +43,7 @@ from silx.gui import qt
from tomwer.core import settings
from tomwer.core import utils
import functools
import copy
import logging
......@@ -242,12 +243,13 @@ class AxisOW(widget.OWWidget, WidgetLongProcessing):
def new_data_in(self, scan):
if scan is None:
return
scan_ = copy.copy(scan)
if not (settings.isOnLbsram() and utils.isLowOnMemory(
settings.get_lbsram_path())):
self._widget.setScan(scan=scan)
elif scan.axis_params is None:
scan.axis_params = QAxisRP()
self.process(scan=scan)
self._widget.setScan(scan=scan_)
elif scan_.axis_params is None:
scan_.axis_params = QAxisRP()
self.process(scan=scan_)
def process(self, scan):
if scan is None:
......
......@@ -41,6 +41,7 @@ from tomwer.core.scan.scanbase import TomwerScanBase
from tomwer.gui.reconstruction.darkref.darkrefcopywidget import DarkRefAndCopyWidget
from tomwer.synctools.ftseries import QReconsParams
from tomwer.web.client import OWClient
import copy
logger = logging.getLogger(__name__)
......@@ -128,7 +129,7 @@ class DarkRefAndCopyOW(widget.OWWidget, OWClient, WidgetLongProcessing):
if scanID is None:
return
assert isinstance(scanID, TomwerScanBase)
return self.widget.process(scanID)
return self.widget.process(copy.copy(scanID))
def signalReady(self, scanID):
assert isinstance(scanID, TomwerScanBase)
......@@ -143,6 +144,5 @@ class DarkRefAndCopyOW(widget.OWWidget, OWClient, WidgetLongProcessing):
return self.widget.recons_params
def close(self):
logger.info('close Dark refs')
self.widget.close()
super(DarkRefAndCopyOW, self).close()
......@@ -37,6 +37,7 @@ from tomwer.gui.reconstruction.ftserie import FtserieWidget
from tomwer.web.client import OWClient
from tomwer.core.scan.scanbase import TomwerScanBase
from silx.gui import qt
import copy
class FtseriesOW(widget.OWWidget, OWClient, WidgetLongProcessing):
......@@ -153,7 +154,9 @@ class FtseriesOW(widget.OWWidget, OWClient, WidgetLongProcessing):
if scan is None:
return
assert isinstance(scan, TomwerScanBase)
self._ftserie.pathReceived(scan)
scan_ = copy.copy(scan)
scan_.clear_latest_reconstructions()
self._ftserie.pathReceived(scan_)
def _updateSettingsVals(self):
self._rpSetting = self._ftserie.recons_params.to_dict()
......
......@@ -45,6 +45,7 @@ from Orange.widgets import widget, gui
from Orange.widgets.widget import Input, Output
from orangecontrib.tomwer.orange.settings import CallbackSettingsHandler
from tomwer.web.client import OWClient
import copy
_logger = logging.getLogger(__name__)
......@@ -159,7 +160,9 @@ class NabuOW(widget.OWWidget, OWClient, WidgetLongProcessing):
assert isinstance(scan, (TomwerScanBase, type(None)))
if scan is None:
return
self._processingStack.add(scan, self.getConfiguration())
scan_ = copy.copy(scan)
scan_.clear_latest_reconstructions()
self._processingStack.add(scan_, self.getConfiguration())
@Inputs.reprocess
def reprocess(self, scan):
......
......@@ -31,6 +31,7 @@ __date__ = "10/01/2018"
import functools
import logging
import os
import copy
from silx.gui import qt
from ..utils import WidgetLongProcessing
from Orange.widgets import settings
......@@ -128,19 +129,20 @@ class TofuOW(widget.OWWidget, OWClient, WidgetLongProcessing):
@Inputs.data_in
def process(self, scan):
if scan is not None:
assert isinstance(scan, TomwerScanBase)
scan_ = copy.copy(scan)
self._executeButton.setEnabled(True)
self._lastScan = scan
self._mainWidget.loadFromScan(scan.path)
self._lastScan = scan_
self._mainWidget.loadFromScan(scan_.path)
recons_param = self._mainWidget.getParameters()
add_options = self._mainWidget.getAdditionalRecoOptions()
# TODO: should be recorded in self._viewer widget
remove_existing = self._mainWidget.removeOutputDir()
assert isinstance(scan, TomwerScanBase)
callback = functools.partial(self.Outputs.data_out.send, scan)
callback = functools.partial(self.Outputs.data_out.send, scan_)
self._reconsStack.add(recons_obj=LaminoReconstruction(),
scan_id=scan,
scan_id=scan_,
recons_params=recons_param,
additional_opts=add_options,
remove_existing=remove_existing,
......
......@@ -219,9 +219,9 @@ class TestAxisStack(TestCaseQt):
self.qapp.processEvents()
self._mainWindow.new_data_in(scan)
self.assertEqual(self._scan1.axis_params.value, None)
self.assertEqual(self._scan2.axis_params.value, None)
self.assertEqual(self._scan3.axis_params.value, None)
self.assertEqual(self._scan1.axis_params, None)
self.assertEqual(self._scan2.axis_params, None)
self.assertEqual(self._scan3.axis_params, None)
def testUnlockStack(self):
"""Check that all axis position will be computed properly if we set a
......@@ -235,9 +235,9 @@ class TestAxisStack(TestCaseQt):
time.sleep(0.2)
self.qapp.processEvents()
self.assertNotEqual(self._scan1.axis_params.value, None)
self.assertNotEqual(self._scan2.axis_params.value, None)
self.assertNotEqual(self._scan3.axis_params.value, None)
self.assertNotEqual(self._scan1.axis_params, None)
self.assertNotEqual(self._scan2.axis_params, None)
self.assertNotEqual(self._scan3.axis_params, None)
def testLockStack(self):
"""Check that axis position will be simply copy if we are in a lock
......
......@@ -37,19 +37,13 @@ import os
from silx.gui import qt
from tomwer.core.utils import ignoreLowMemory
from tomwer.core import settings
try:
import bliss
except ImportError:
has_bliss = False
else:
from orangecontrib.tomwer.widgets.control.DataListenerOW import DataListenerOW
from orangecontrib.tomwer.test.utils import OrangeWorflowTest
from tomwer.test.utils import UtilsTest
from tomwer.synctools.datalistener import _mock_acquisition_info
from silx.gui.utils.testutils import TestCaseQt
from silx.gui.utils.testutils import SignalListener
from tomwer.test.utils import skip_gui_test, skip_orange_workflows_test
has_bliss = True
from orangecontrib.tomwer.widgets.control.DataListenerOW import DataListenerOW
from orangecontrib.tomwer.test.utils import OrangeWorflowTest
from tomwer.test.utils import UtilsTest
from tomwer.synctools.datalistener import _mock_acquisition_info
from silx.gui.utils.testutils import TestCaseQt
from silx.gui.utils.testutils import SignalListener
from tomwer.test.utils import skip_gui_test, skip_orange_workflows_test
logging.disable(logging.INFO)
......
......@@ -34,8 +34,9 @@ import unittest
from silx.gui import qt
from orangecontrib.tomwer.widgets.control.DataValidatorOW import \
DataValidatorOW
from orangecontrib.tomwer.widgets.control.OldDataValidatorOW import \
OldDataValidatorOW
from orangecontrib.tomwer.widgets.control.DataValidatorOW import DataValidatorOW
from tomwer.gui.qtapplicationmanager import QApplicationManager
from tomwer.core.scan.edfscan import EDFTomoScan
from tomwer.core.scan.scanbase import TomwerScanBase
......@@ -47,9 +48,9 @@ _qapp = QApplicationManager()
logging.disable(logging.INFO)
class TestAccumulation(unittest.TestCase):
class BaseTestAccumulation:
"""Test that all scans will be validated if many are send and no answer
is give until all received"""
is give until all received"""
NB_SCAN = 10
"""Number of scan to accumulate"""
......@@ -58,7 +59,7 @@ class TestAccumulation(unittest.TestCase):
N_RECONS = 2
DIM_MOCK_SCAN = 10
def setUp(self):
def _setUp(self):
self.scans = []
for iScan in range(TestAccumulation.NB_SCAN):
scanID = tempfile.mkdtemp()
......@@ -69,12 +70,9 @@ class TestAccumulation(unittest.TestCase):
dim=self.DIM_MOCK_SCAN)
self.scans.append(scan)
self.scanValidator = None
self.scanValidator = DataValidatorOW(None)
self.scanValidator._warnValManualShow = True
self.scanValidator.setAttribute(qt.Qt.WA_DeleteOnClose)
def tearDown(self):
def _tearDown(self):
_qapp.processEvents()
for f in self.scans:
if os.path.isdir(f.path):
......@@ -82,20 +80,46 @@ class TestAccumulation(unittest.TestCase):
self.scanValidator.close()
def test(self):
assert self.scanValidator is not None
for scan in self.scans:
self.scanValidator.addScan(scan)
_qapp.processEvents()
self.assertTrue(len(self.scanValidator._scansToValidate) == TestAccumulation.NB_SCAN)
self.assertEqual(self.scanValidator.getNScanToValidate(), TestAccumulation.NB_SCAN)
for scanID in self.scans:
self.scanValidator._validateScan(scanID)
_qapp.processEvents()
self.assertTrue(len(self.scanValidator._scansToValidate) == 0)
self.assertEqual(self.scanValidator.getNScanToValidate(), 0)
class TestAccumulation(unittest.TestCase, BaseTestAccumulation):
"""Test validation for the current data validator"""
def setUp(self):
BaseTestAccumulation._setUp(self)
self.scanValidator = DataValidatorOW(None)
self.scanValidator._warnValManualShow = True
self.scanValidator.setAttribute(qt.Qt.WA_DeleteOnClose)
def tearDown(self):
BaseTestAccumulation._tearDown(self)
class TestAccumulationOld(unittest.TestCase, BaseTestAccumulation):
def setUp(self):
BaseTestAccumulation._setUp(self)
self.scanValidator = OldDataValidatorOW(None)
self.scanValidator._warnValManualShow = True
self.scanValidator.setAttribute(qt.Qt.WA_DeleteOnClose)
def tearDown(self):
BaseTestAccumulation._tearDown(self)
class TestValidation(unittest.TestCase):
"""Test the validation command"""
class BaseTestValidation(unittest.TestCase):
"""Test validation for the old data validator"""
class _ValidationReceiver(qt.QObject):
def __init__(self):
qt.QObject.__init__(self)
......@@ -104,19 +128,6 @@ class TestValidation(unittest.TestCase):
def count(self):
self.counter = self.counter + 1
class _DataValidatorWidgetPatched(DataValidatorOW):
"""A DataValidatorOW without the Orange send signal process"""
scanReady = qt.Signal(TomwerScanBase)
_warnValManualShow = True
def _validated(self, scan):
"""Callback when the validate button is pushed"""
assert isinstance(scan, TomwerScanBase)
if scan is not None:
self.Outputs.data_out.send(scan)
self.scanReady.emit(scan)
del self._scansToValidate[scan.path]
@classmethod
def setUpClass(cls):
cls.rootdir = tempfile.mkdtemp()
......@@ -137,18 +148,55 @@ class TestValidation(unittest.TestCase):
shutil.rmtree(cls.rootdir)
def tearDown(self):
self.scanValidator.setAttribute(qt.Qt.WA_DeleteOnClose)
self.scanValidator.close()
del self.scanValidator
self.scanValidator = None
unittest.TestCase.tearDown(self)
def setUp(self):
unittest.TestCase.setUp(self)
self.scanValidator = TestValidation._DataValidatorWidgetPatched()
self.scanValidator = None
class TestValidation(BaseTestValidation):
"""Test validation for the latest data validation widget"""
def setUp(self):
BaseTestValidation.setUp(self)
self.scanValidator = DataValidatorOW()
self.scanValidator._warnValManualShow = True
self.scanValidator.scanReady.connect(self.validationReceiver.count)
self.scanValidator._widget.sigScanReady.connect(self.validationReceiver.count)
self.validationReceiver.counter = 0
self.scanValidator.setAttribute(qt.Qt.WA_DeleteOnClose)
def testAutomaticValidation(self):
for scan in self.scans:
self.scanValidator.addScan(EDFTomoScan(scan))
self.assertTrue(self.validationReceiver.counter == 0)
self.scanValidator.setAutomaticValidation(True)
while (_qapp.hasPendingEvents()):
_qapp.processEvents()
self.assertTrue(self.validationReceiver.counter == 2)
self.assertTrue(len(self.scanValidator._widget._scansToValidate) == 0)
class TestValidationOld(BaseTestValidation):
"""Test validation for the latest data validation widget"""
class _DataValidatorWidgetPatched(OldDataValidatorOW):
"""A DataValidatorOW without the Orange send signal process"""
scanReady = qt.Signal(TomwerScanBase)
_warnValManualShow = True
def _validated(self, scan):
"""Callback when the validate button is pushed"""
assert isinstance(scan, TomwerScanBase)
if scan is not None:
self.Outputs.data_out.send(scan)
self.scanReady.emit(scan)
del self._scansToValidate[scan.path]
def testAutomaticValidation(self):
for scan in self.scans:
self.scanValidator.addScan(EDFTomoScan(scan))
......@@ -160,10 +208,19 @@ class TestValidation(unittest.TestCase):
self.assertTrue(self.validationReceiver.counter == 2)
self.assertTrue(len(self.scanValidator._scansToValidate) == 0)
def setUp(self):
BaseTestValidation.setUp(self)
self.scanValidator = TestValidationOld._DataValidatorWidgetPatched()
self.scanValidator._warnValManualShow = True
self.scanValidator.scanReady.connect(self.validationReceiver.count)
self.validationReceiver.counter = 0
self.scanValidator.setAttribute(qt.Qt.WA_DeleteOnClose)
def suite():
test_suite = unittest.TestSuite()
for ui in (TestAccumulation, TestValidation):
for ui in (TestAccumulation, TestAccumulationOld, TestValidation,
TestValidationOld):
test_suite.addTest(unittest.defaultTestLoader.loadTestsFromTestCase(ui))
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__ = "21/07/2020"
from silx.gui import qt
from Orange.widgets import widget, gui
from Orange.widgets.widget import Input
from tomwer.gui.visualization.dataviewer import DataViewer
from tomwer.core.scan.scanbase import TomwerScanBase
from Orange.widgets import settings
import logging
_logger = logging.getLogger(__name__)
class DataViewerOW(widget.OWWidget):
"""a data viewer able to:
- display slices (latest reconstructed if any)
- display radios with or without normalization
:param parent: the parent widget
"""
name = "data viewer"
id = "orange.widgets.tomwer.dataviewer"
description = "allow user too browse through data"
icon = "icons/eye.png"
priority = 70
category = "esrfWidgets"
keywords = ["tomography", "file", "tomwer", "acquisition", "validation"]
want_main_area = True
resizing_enabled = True
compress_signal = False
_viewer_config = settings.Setting(dict())
class Inputs:
data_in = Input(name='data', type=TomwerScanBase)
def __init__(self, parent=None):
widget.OWWidget.__init__(self, parent)
self._layout = gui.vBox(self.mainArea, self.name).layout()
self.viewer = DataViewer(parent=self)
self._layout.addWidget(self.viewer)
self._setSettings(settings=self._viewer_config)
# connect signal / slot
self.viewer.sigConfigChanged.connect(self._updateSettings)
@Inputs.data_in
def addScan(self, scan):
self.viewer.setScan(scan)
def sizeHint(self):
return qt.QSize(400, 500)
def _updateSettings(self):
self._viewer_config['mode'] = self.viewer.getDisplayMode()
self._viewer_config['slice_opt'] = self.viewer.getSliceOption()
self._viewer_config['radio_opt'] = self.viewer.getRadioOption()
def _setSettings(self, settings):
old_state = self.viewer.blockSignals(True)
if 'mode' in settings:
self.viewer.setDisplayMode(settings['mode'])
if 'slice_opt' in settings:
self.viewer.setSliceOption(settings['slice_opt'])
if 'radio_opt' in settings:
self.viewer.setRadioOption(settings['radio_opt'])
self.viewer.blockSignals(old_state)
......@@ -44,11 +44,11 @@ class ImageStackViewerOW(widget.OWWidget):
:param parent:the parent widget
:param FtserieReconstruction ftseries: the initial reconstruction to show
"""
name = "data viewer"
name = "(old) data viewer"
id = "orange.widgets.tomwer.imagestackviewer"
description = "Show a small recap of the reconstruction runned"
icon = "icons/eye.png"
priority = 70
icon = "icons/eyecrack.png"
priority = 700
category = "esrfWidgets"
keywords = ["tomography", "file", "tomwer", "acquisition", "validation"]
......
......@@ -84,11 +84,11 @@ def main():
module_name="tomwer.app.sinogramviewer",
description="Allows to compute on the fly sinogram "
"and display them")
launcher.add_command("slicestack",
launcher.add_command("slice-stack",
module_name="tomwer.app.slicestack",
description="Display each reconstruction contained "
"in the ROOT_DIR or sub-folders")
launcher.add_command("radiostack",
launcher.add_command("radio-stack",
module_name="tomwer.app.radiostack",
description="Display each radio contained in the "
"ROOT_DIR or sub-folders")
......
......@@ -9,6 +9,7 @@ import argparse
from tomwer.gui.utils.splashscreen import getMainSplashScreen
from tomwer.gui.stacks import RadioStack
from tomwer.gui import icons
from tomwer.core.scan.hdf5scan import HDF5TomoScan
import signal
logging.basicConfig()
......@@ -25,6 +26,8 @@ def addFolderAndSubFolder(stack, path):
_path = os.path.join(path, f)
if os.path.isdir(_path) is True:
addFolderAndSubFolder(stack, _path)
elif os.path.isfile(_path) and HDF5TomoScan.is_nexus_nxtomo_file(_path):
stack.addLeafScan(_path)
def sigintHandler(*args):
......@@ -56,7 +59,7 @@ def main(argv):
timer = qt.QTimer()
timer.start(500)
# Application have to wake up Python interpreter, else SIGINT is not
# catched
# cached
timer.timeout.connect(lambda: None)
splash = getMainSplashScreen()
......
......@@ -140,6 +140,12 @@ class FolderTransfert(SingleProcess):
else:
_entry = None
output_scan = scan._deduce_transfert_scan(outputdir)
try:
output_scan._update_latest_recons_urls(old_path=scan.path,
new_path=output_scan.path)
except Exception as e:
logger.warning('Fail to convert url of latest reconstruction. Reason is:' + e)
output_scan.clear_latest_reconstructions()
self.__noticeTransfertSuccess(input_scan=scan,
output_scan=output_scan)
else:
......@@ -153,7 +159,13 @@ class FolderTransfert(SingleProcess):
self._signalCopying(scanID=source, outputdir=target)
output_scan = scan._deduce_transfert_scan(outputdir)
try:
output_scan._update_latest_recons_urls(old_path=scan.path,
new_path=output_scan.path)
except Exception as e:
logger.warning(
'Fail to convert url of latest reconstruction. Reason is:' + e)
output_scan.clear_latest_reconstructions()
RSyncManager().sync_file(source=source,
target=target,
wait=self._block,
......
......@@ -50,6 +50,7 @@ from multiprocessing import Process
from nabu.io.config import generate_nabu_configfile
from nabu import version as nabu_version
from silx.io.dictdump import h5todict
from silx.io.url import DataUrl
import h5py
_logger = TomwerLogger(__name__)
......@@ -79,10 +80,16 @@ def run_reconstruction(scan: TomwerScanBase, config: dict,
config['reconstruction']['rotation_axis_position'] = str(cor_nabu_ref)
nabu_configurations = _interpret_tomwer_configuration(config, scan=scan)
output_urls = []
for nabu_configuration in nabu_configurations:
l_config, slice_index = nabu_configuration
_run_single_reconstruction(config=l_config, scan=scan, local=local,
slice_index=slice_index, dry_run=dry_run)
output_urls.extend(_run_single_reconstruction(config=l_config,
scan=scan, local=local,
slice_index=slice_index,
dry_run=dry_run)
)
# tag latest reconstructions
scan.set_latest_reconstructions(output_urls)
class Nabu(SingleProcess):
......@@ -278,7 +285,7 @@ def _launch_reconstruction(config_file, slice_index):
worker_process.process_chunk()
def _run_single_reconstruction(scan, config, dry_run, slice_index, local):
def _run_single_reconstruction(scan, config, dry_run, slice_index, local) -> list:
"""
:param scan:
......@@ -286,8 +293,7 @@ def _run_single_reconstruction(scan, config, dry_run, slice_index, local):
:param dry_run:
:param Union[None,int] slice_index: slice index to reconstruct
:param local:
:param use_cuda:
:return:
:return: list of output urls
"""
config['dataset'] = get_nabu_dataset_desc(scan=scan)
if local is True:
......@@ -329,7 +335,7 @@ def _run_single_reconstruction(scan, config, dry_run, slice_index, local):
config = treateOutputConfig(config)
# the policy is to save nabu .cfg file at the same location as the
conf_file = os.path.join(config['output']['location'],
config['output']['file_prefix'] + NABU_CONFIG_FILE_EXTENSION)
config['output']['file_prefix'] + NABU_CONFIG_FILE_EXTENSION)
_logger.info('create %s' % conf_file)
# add some tomwer metadata and save the configuration
......@@ -337,6 +343,7 @@ def _run_single_reconstruction(scan, config, dry_run, slice_index, local):
with _TomwerInfo(config) as config_to_dump:
generate_nabu_configfile(fname=conf_file, config=config_to_dump,
options_level='advanced')
if slice_index is not None and dry_run is False and local:
_logger.info('run nabu reconstruction for %s with %s'
'' % (scan.path, config))
......@@ -344,6 +351,39 @@ def _run_single_reconstruction(scan, config, dry_run, slice_index, local):
process = Process(target=_launch_reconstruction, args=(conf_file, slice_index))
process.start()
process.join()
file_format = config_to_dump['output']['file_format']
file_name = '_'.join((config_to_dump['output']['file_prefix'],
str(slice_index).zfill(4)))
output_file_name = '.'.join((file_name,
config_to_dump['output']['file_format'],
))
output_file_name = os.path.join(config_to_dump['output']['location'],
output_file_name)
file_format = file_format.lower()
if file_format in ('npy', 'npz'):
scheme = 'numpy'
data_path = None
data_slice = None
elif file_format in ('hdf5', 'h5', 'hdf'):
scheme = 'silx'
data_path = '/'.join((scan.entry, 'reconstruction', 'results', 'data'))
data_slice = None
elif file_format in ('edf',):
scheme = 'fabio'
data_path = None
data_slice = None
else:
raise ValueError('file format not managed')
if os.path.exists(output_file_name):
url = DataUrl(file_path=output_file_name, data_path=data_path,
data_slice=data_slice, scheme=scheme)
return [url, ]
else:
_logger.warning('nabu output file not found. Something went wrong...' + output_file_name)
return []
else:
return []
def get_nabu_dataset_analyzer(scan: TomwerScanBase) -> DatasetAnalyzer:
......
......@@ -37,6 +37,7 @@ from tomwer.core.process.reconstruction.ftseries.params import ReconsParams
from tomwer.core.scan.scanbase import TomwerScanBase
from tomwer.core.scan.hdf5scan import HDF5TomoScan
from tomwer.core.settings import get_lbsram_path, get_dest_path
from silx.io.url import DataUrl
from tomwer.core.log import TomwerLogger
from tomwer.core import settings
from tomwer.unitsystem import metricsystem
......@@ -222,6 +223,7 @@ class PyHSTCaller(SingleProcess):
[slices.add(slice) for slice in fixed_slice]
else:
slices.add(fixed_slice)
recons_urls = []
for slice in slices:
for recons_param in recons_list:
if slice is None and rec_slices_only is True:
......@@ -232,15 +234,18 @@ class PyHSTCaller(SingleProcess):
convert_lbsram_to_dest = full_rec_to_dest
else:
convert_lbsram_to_dest = slices_rec_to_dest
self._process_frm_one_slice_one_rp_set(scan=scan,
slice=slice,
recons_param=recons_param,
has_several_pag=has_several_pag,
dry_run=dry_run,
run_recons=run_recons,
cuda_devices=cuda_devices,
convert_lbsram_to_dest=convert_lbsram_to_dest,
hdf5_info=hdf5_info)
recons_urls.extend(
self._process_frm_one_slice_one_rp_set(scan=scan,
slice=slice,
recons_param=recons_param,
has_several_pag=has_several_pag,
dry_run=dry_run,
run_recons=run_recons,
cuda_devices=cuda_devices,
convert_lbsram_to_dest=convert_lbsram_to_dest,
hdf5_info=hdf5_info)
)
scan.set_latest_reconstructions(recons_urls)
if dry_run is False:
entry = 'entry'
......@@ -262,7 +267,7 @@ class PyHSTCaller(SingleProcess):
run_recons: bool,
convert_lbsram_to_dest: bool,
dry_run: bool,
hdf5_info: _pyhst_hdf5_info) -> None:
hdf5_info: _pyhst_hdf5_info) -> list:
"""
process pyhstcaller from one specific slice indice and one set
of reconstruction parameter (so all paramter should be unique)
......@@ -287,6 +292,8 @@ class PyHSTCaller(SingleProcess):
:param bool dry_run: if True then skip call to pyhst and only create
.par and .rec files
:param hdf5_info: information regarding hdf5 if any created
:return: list of the url to access the slice reconstructed
:rtype: list
"""
assert isinstance(recons_param, dict)
assert isinstance(slice, (type(None), int))
......@@ -294,7 +301,6 @@ class PyHSTCaller(SingleProcess):
logger.info('manage .par for volume')
else:
logger.info('manage .par for slice %s' % slice)
# deduce file base name
if PaganinMode.from_value(recons_param['PAGANIN']['MODE']) == PaganinMode.off:
base_file_name = os.path.basename(scan.path)
......@@ -344,6 +350,7 @@ class PyHSTCaller(SingleProcess):
assert os.path.exists(os.path.join(scan.path, par_file_name))
except ValueError as e:
logger.error('Fail to generate .par file. ' + str(e))
return list()
else:
if recons_param['PYHSTEXE']['MAKE_OAR_FILE']:
# create .rec if needed
......@@ -377,6 +384,24 @@ class PyHSTCaller(SingleProcess):
exec_path=exec_path)
except ValueError as e:
logger.error(e)
return list()
else:
if dry_run is True:
return list()
else:
if recons_param['FT']['VOLOUTFILE'] == 1:
output_file_ext = '.vol'
scheme = 'tomwer'
else:
output_file_ext = '.edf'
scheme = 'fabio'
recons_file = par_file_name.replace('.par', output_file_ext)
recons_file = os.path.join(scan.path, recons_file)
recons_url = DataUrl(file_path=recons_file,
data_slice=None, scheme=scheme)
return [recons_url, ]
else:
return list()
def get_center_of_rotation(self, scan: TomwerScanBase, use_tomwer_axis: bool,
use_old_tomwer_processes: str, output_file: str):
......
......@@ -83,21 +83,21 @@ class ScanValidator(BaseProcess):
def lastReceivedRecons(self):
return list(self._scansToValidate.values())[0]
def addScan(self, ftserie):
def addScan(self, scan):
"""
Return the index on the current orderred dict
:param ftserie:
:param scan:
:return:
"""
_ftserie = ftserie
if type(ftserie) is str:
_ftserie = scan
if type(scan) is str:
_ftserie = ScanFactory.create_scan_object(_ftserie)
info = 'Scan %s has been added by the Scan validator' % _ftserie
logger.info(info)
_ftserie = ftserie
if type(ftserie) is str:
_ftserie = scan
if type(scan) is str:
_ftserie = ScanFactory.create_scan_object(_ftserie)
self._scansToValidate[str(_ftserie)] = _ftserie
......@@ -279,11 +279,11 @@ class ScanValidatorP(ScanValidator):
def __init__(self, memoryReleaser=None):
ScanValidator.__init__(self, memoryReleaser)
def _sendScanReady(self, scanID):
self.scanReady.emit(scanID)
def _sendScanReady(self, scan):
self.scanReady.emit(scan)
def _sendScanCanceledAt(self, scanID):
self.scanCanceledAt.emit(scanID)
def _sendScanCanceledAt(self, scan):
self.scanCanceledAt.emit(scan)
def _sendUpdateReconsParam(self, ftserie):
self.updateReconsParam.emit(ftserie)
def _sendUpdateReconsParam(self, scan):
self.updateReconsParam.emit(scan)
......@@ -715,6 +715,9 @@ class TestPyHStHDF5(unittest.TestCase):
self.assertFalse(os.path.exists(self.converted_flat_file_51))
self.assertFalse(os.path.exists(self.converted_dark_file))
# TODO: speed up the test. The datasaet looks too large for this kind
# of tests
# check values given to some parameters in the par file
for par_file in par_files:
with self.subTest(par_file=par_file):
......@@ -740,6 +743,9 @@ class TestPyHStHDF5(unittest.TestCase):
self.assertTrue(os.path.exists(self.converted_dark_file))
self.assertTrue(os.path.exists(self.converted_proj_file))
# TODO: speed up the test. The datasaet looks too large for this kind
# of tests
# check values given to some parameters in the par file
for par_file in par_files:
with self.subTest(par_file=par_file):
......
......@@ -327,3 +327,23 @@ class HDF5TomoScan(_tsHDF5TomoScan, TomwerScanBase):
for proj_frame in proj_frames:
self._projections_with_angles[proj_frame.rotation_angle] = proj_frame.url
return self._projections_with_angles
@staticmethod
def is_nexus_nxtomo_file(file_path: str) -> bool:
if h5py.is_hdf5(file_path):
return len(HDF5TomoScan.get_nxtomo_entries(file_path)) > 0
@staticmethod
def get_nxtomo_entries(file_path: str) -> tuple:
def entry_is_nx_tomo(entry):
return 'beam' in entry and 'instrument' in entry and 'sample' in entry
if not h5py.is_hdf5(file_path):
return tuple()
else:
res = []
with h5py.File(file_path, mode='r', swmr=True, libver='latest') as h5s:
for entry_name, node in h5s.items():
if entry_is_nx_tomo(node):
res.append(entry_name)
return tuple(res)
......@@ -87,6 +87,8 @@ class TomwerScanBase:
self._notify_ffc_rsc_missing = True
"""Should we notify the user if ffc fails because cannot find dark or
flat. Used to avoid several warnings. Only display one"""
self._latest_reconstructions = []
"""list of url related to latest reconstruction from nabu or pyhst"""
def _init_index_process_file(self, overwrite_proc_file=False):
if not overwrite_proc_file and self.process_file is not None and os.path.exists(self.process_file):
......@@ -116,6 +118,10 @@ class TomwerScanBase:
def _flat_field_correction(self, data, index: typing.Union[int, None],
dark, flat1, flat2, index_flat1: int,
index_flat2: int):
"""
compute flat field correction for a provided data from is index
one dark and two flats (require also indexes)
"""
assert type(data) is numpy.ndarray
can_process = True
......@@ -498,6 +504,26 @@ class TomwerScanBase:
results.append(url)
return results
@property
def latest_reconstructions(self):
"""List of latest reconstructions"""
return self._latest_reconstructions
def clear_latest_reconstructions(self):
self._latest_reconstructions = []
def set_latest_reconstructions(self, urls: typing.Iterable):
self._latest_reconstructions = urls
def _update_latest_recons_urls(self, old_path, new_path):
new_urls = []
for recons_url in self._latest_reconstructions:
new_urls.append(DataUrl(file_path=recons_url.path().replace(old_path, new_path, 1),
data_path=recons_url.data_path(),
data_slice=recons_url.data_slice(),
scheme=recons_url.scheme()))
self._latest_reconstructions = new_urls
class _TomwerBaseDock(object):
"""
......
This diff is collapsed.
......@@ -30,6 +30,7 @@ __date__ = "29/05/2017"
import unittest
from . import test_datalist
from . import test_datalistener
from . import test_datavalidator
from . import test_datawatcher
from . import test_h5editor
from . import test_inputwidget
......@@ -42,6 +43,7 @@ def suite():
test_suite.addTests([
test_datalist.suite(),
test_datalistener.suite(),
test_datavalidator.suite(),
test_datawatcher.suite(),
test_h5editor.suite(),
test_inputwidget.suite(),
......
# coding: utf-8
#/*##########################################################################
# Copyright (C) 2016 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__ = "30/07/2020"
import unittest
from silx.gui.utils.testutils import TestCaseQt
from silx.gui import qt
from tomwer.gui.control.datavalidator import DataValidator
class TestValidatorGUI(TestCaseQt):
"""Test that the ImageStackWidget can be load and remove without any issue
"""
def setUp(self):
TestCaseQt.setUp(self)
self._widget = DataValidator(None)
def tearDown(self):
self._widget.setAttribute(qt.Qt.WA_DeleteOnClose)
self._widget.close()
self._widget = None
unittest.TestCase.tearDown(self)
def test(self):
"""Make sur the addLeaf and clear functions are working"""
pass
def suite():
test_suite = unittest.TestSuite()
for ui in (TestValidatorGUI, ):
test_suite.addTest(
unittest.defaultTestLoader.loadTestsFromTestCase(ui))
return test_suite
if __name__ == '__main__':
unittest.main(defaultTest="suite")
\ No newline at end of file
from silx.gui import qt
from tomwer.core.utils.device import CudaDevice
from tomwer.core.utils.device import _CudaPlatformBase
import logging
_logger = logging.getLogger(__name__)
class _PlatformSelectorBase():
......@@ -53,11 +56,16 @@ class CudaPlatfornGroup(qt.QGroupBox, _CudaPlatformBase):
self.setLayout(qt.QVBoxLayout())
self.setTitle("GPU devices to use")
self._devices = {}
for device in self.getExistingDevices():
qcb = qt.QCheckBox(device.name)
self.layout().addWidget(qcb)
self._devices[device] = qcb
qcb.stateChanged.connect(self._selectionUpdated)
try:
existing_devices = self.getExistingDevices()
except Exception as e:
_logger.error(e)
else:
for device in existing_devices:
qcb = qt.QCheckBox(device.name)
self.layout().addWidget(qcb)
self._devices[device] = qcb
qcb.stateChanged.connect(self._selectionUpdated)
def activate_all(self):
for device in self._devices:
......
......@@ -75,7 +75,7 @@ class PyHSTWidget(H5StructEditor, qt.QWidget):
self._makeConnection()
# expose API
self.getCudaDevices = self._cudaSelector.getExistingDevices()
self.getCudaDevices = self._cudaSelector.getExistingDevices
def setReconsParams(self, recons_params):
if isinstance(recons_params, QReconsParams):
......
......@@ -37,8 +37,12 @@ from collections import OrderedDict
from tomwer.gui.qfolderdialog import QScanDialog
from tomwer.core.scan.scanbase import TomwerScanBase
from tomwer.core.scan.scanfactory import ScanFactory
import logging
import h5py
import os
_logger = logging.getLogger(__name__)
class _ImageStack(qt.QMainWindow):
"""
......@@ -189,8 +193,25 @@ class RadioStack(_ImageStack):
"""
slices = {}
for scan in self._scans:
scan_ = ScanFactory.create_scan_object(scan_path=scan)
imgs = scan_.projections
if len(imgs) > 0:
slices[scan] = imgs
# manage hdf5
if h5py.is_hdf5(scan):
try:
scans = ScanFactory.create_scan_objects(scan_path=scan)
except Exception as e:
_logger.warning(e)
else:
for scan_ in scans:
imgs = scan_.projections
if len(imgs) > 0:
slices[str(scan_)] = imgs
else:
# manage edf
try:
scan_ = ScanFactory.create_scan_object(scan_path=scan)
except ValueError:
pass
else:
imgs = scan_.projections
if len(imgs) > 0:
slices[scan] = imgs
return slices
This diff is collapsed.
......@@ -29,11 +29,13 @@ __date__ = "29/05/2017"
import unittest
from . import test_sinogramviewer
from . import test_dataviewer
def suite():
test_suite = unittest.TestSuite()
test_suite.addTests([
test_dataviewer.suite(),
test_sinogramviewer.suite(),
])
return test_suite
# coding: utf-8
#/*##########################################################################
# Copyright (C) 2016 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__ = "30/07/2020"
import unittest
from silx.gui.utils.testutils import TestCaseQt
from silx.gui import qt
from tomwer.gui.visualization.dataviewer import DataViewer, ImageStack
class TestImageStack(TestCaseQt):
"""Test that the ImageStackWidget can be load and remove without any issue
"""
def setUp(self):
TestCaseQt.setUp(self)
self._widget = ImageStack(None)
def tearDown(self):
self._widget.setAttribute(qt.Qt.WA_DeleteOnClose)
self._widget.close()
self._widget = None
unittest.TestCase.tearDown(self)
def test(self):
"""Make sur the addLeaf and clear functions are working"""
pass
class TestDataViewer(TestCaseQt):
"""Test that the data viewer can be load and remove without any issue"""
def setUp(self):
TestCaseQt.setUp(self)
self._widget = DataViewer(None)
def tearDown(self):
self._widget.setAttribute(qt.Qt.WA_DeleteOnClose)
self._widget.close()
self._widget = None
unittest.TestCase.tearDown(self)
def test(self):
"""Make sur the addLeaf and clear functions are working"""
pass
def suite():
test_suite = unittest.TestSuite()
for ui in (TestDataViewer, TestImageStack):
test_suite.addTest(
unittest.defaultTestLoader.loadTestsFromTestCase(ui))
return test_suite