././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1739271865.9677656 tomwer-1.4.8/0000755000175000017500000000000014752627272012155 5ustar00paynopayno././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1736265726.0 tomwer-1.4.8/LICENSE0000644000175000017500000000352414737247776013200 0ustar00paynopayno The Tomwer library goal is to provide some useful process for acquisition/reconstruction automation. Tomwer is distributed under the MIT license. Tomwer is using the Qt library. A word of caution is to be provided. If users develop and distribute software using modules accessing Qt by means of Riverbank Computing Qt bindings PyQt4 or PyQt5, those users will be conditioned by the license of their PyQt4/5 software (GPL or commercial). If the end user does not own a commercial license of PyQt4 or PyQt5 and wishes to be free of any distribution condition, (s)he should be able to use PySide because it uses the LGPL license. It can also be used as an Orange3 add-on. If you wan't to distribute Orange you must stick to there license (GNU [GPL-3.0]+ license) The MIT license follows: Copyright (c) European Synchrotron Radiation Facility (ESRF) 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. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739271825.0 tomwer-1.4.8/MANIFEST.in0000644000175000017500000000000014752627221013673 0ustar00paynopayno././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1739271865.9677656 tomwer-1.4.8/PKG-INFO0000644000175000017500000003214714752627272013261 0ustar00paynopaynoMetadata-Version: 2.2 Name: tomwer Version: 1.4.8 Summary: "tomography workflow tools" Home-page: https://gitlab.esrf.fr/tomotools/tomwer Author: Henri Payno, Pierre Paleo, Pierre-Olivier Autran, Jérôme Lesaint, Alessandro Mirone Author-email: henri.payno@esrf.fr, pierre.paleo@esrf.fr, pierre-olivier.autran@esrf.fr, jerome.lesaint@esrf.fr, mirone@esrf.fr License: MIT Project-URL: Bug Tracker, https://gitlab.esrf.fr/tomotools/tomwer/-/issues Keywords: orange3 add-on,ewoks Classifier: Intended Audience :: Education Classifier: Intended Audience :: Science/Research Classifier: License :: OSI Approved :: MIT License Classifier: Programming Language :: Python :: 3 Classifier: Environment :: Console Classifier: Environment :: X11 Applications :: Qt Classifier: Operating System :: POSIX Classifier: Natural Language :: English Classifier: Topic :: Scientific/Engineering :: Physics Classifier: Topic :: Software Development :: Libraries :: Python Modules Requires-Python: >=3.6 Description-Content-Type: text/x-rst License-File: LICENSE Requires-Dist: numpy Requires-Dist: setuptools Requires-Dist: psutil Requires-Dist: silx[full]<2.1.1,>=2.0 Requires-Dist: tomoscan>=2.1.0a18 Requires-Dist: nxtomo>=1.3.0dev4 Requires-Dist: nxtomomill>=1.1.0a0 Requires-Dist: processview>=1.5.0 Requires-Dist: ewoks>=0.1.1 Requires-Dist: ewokscore<1.1.0 Requires-Dist: sluurp>=0.4.1 Requires-Dist: packaging Requires-Dist: pyunitsystem>=2.0.0a Requires-Dist: tqdm Provides-Extra: full-base Requires-Dist: orange-canvas-core; extra == "full-base" Requires-Dist: orange-widget-base; extra == "full-base" Requires-Dist: ewoks[orange]>=0.1.1; extra == "full-base" Requires-Dist: ewoksorange>=0.7.0rc0; extra == "full-base" Requires-Dist: rsyncmanager; extra == "full-base" Requires-Dist: fabio; extra == "full-base" Requires-Dist: h5py>=3; extra == "full-base" Requires-Dist: lxml; extra == "full-base" Requires-Dist: werkzeug; extra == "full-base" Requires-Dist: json-rpc; extra == "full-base" Requires-Dist: scipy; extra == "full-base" Requires-Dist: Pillow; extra == "full-base" Requires-Dist: glymur; extra == "full-base" Requires-Dist: resource; extra == "full-base" Requires-Dist: tifffile; extra == "full-base" Requires-Dist: hdf5plugin; extra == "full-base" Requires-Dist: pyicat_plus; extra == "full-base" Requires-Dist: ewoksnotify[full]; extra == "full-base" Requires-Dist: pyicat_plus; extra == "full-base" Provides-Extra: full-no-nabu Requires-Dist: orange-canvas-core; extra == "full-no-nabu" Requires-Dist: orange-widget-base; extra == "full-no-nabu" Requires-Dist: ewoks[orange]>=0.1.1; extra == "full-no-nabu" Requires-Dist: ewoksorange>=0.7.0rc0; extra == "full-no-nabu" Requires-Dist: rsyncmanager; extra == "full-no-nabu" Requires-Dist: fabio; extra == "full-no-nabu" Requires-Dist: h5py>=3; extra == "full-no-nabu" Requires-Dist: lxml; extra == "full-no-nabu" Requires-Dist: werkzeug; extra == "full-no-nabu" Requires-Dist: json-rpc; extra == "full-no-nabu" Requires-Dist: scipy; extra == "full-no-nabu" Requires-Dist: Pillow; extra == "full-no-nabu" Requires-Dist: glymur; extra == "full-no-nabu" Requires-Dist: resource; extra == "full-no-nabu" Requires-Dist: tifffile; extra == "full-no-nabu" Requires-Dist: hdf5plugin; extra == "full-no-nabu" Requires-Dist: pyicat_plus; extra == "full-no-nabu" Requires-Dist: ewoksnotify[full]; extra == "full-no-nabu" Requires-Dist: pyicat_plus; extra == "full-no-nabu" Provides-Extra: full-no-cuda Requires-Dist: orange-canvas-core; extra == "full-no-cuda" Requires-Dist: orange-widget-base; extra == "full-no-cuda" Requires-Dist: ewoks[orange]>=0.1.1; extra == "full-no-cuda" Requires-Dist: ewoksorange>=0.7.0rc0; extra == "full-no-cuda" Requires-Dist: rsyncmanager; extra == "full-no-cuda" Requires-Dist: fabio; extra == "full-no-cuda" Requires-Dist: h5py>=3; extra == "full-no-cuda" Requires-Dist: lxml; extra == "full-no-cuda" Requires-Dist: werkzeug; extra == "full-no-cuda" Requires-Dist: json-rpc; extra == "full-no-cuda" Requires-Dist: scipy; extra == "full-no-cuda" Requires-Dist: Pillow; extra == "full-no-cuda" Requires-Dist: glymur; extra == "full-no-cuda" Requires-Dist: resource; extra == "full-no-cuda" Requires-Dist: tifffile; extra == "full-no-cuda" Requires-Dist: hdf5plugin; extra == "full-no-cuda" Requires-Dist: pyicat_plus; extra == "full-no-cuda" Requires-Dist: ewoksnotify[full]; extra == "full-no-cuda" Requires-Dist: pyicat_plus; extra == "full-no-cuda" Requires-Dist: nabu>=2023.3.1dev; extra == "full-no-cuda" Provides-Extra: full Requires-Dist: orange-canvas-core; extra == "full" Requires-Dist: orange-widget-base; extra == "full" Requires-Dist: ewoks[orange]>=0.1.1; extra == "full" Requires-Dist: ewoksorange>=0.7.0rc0; extra == "full" Requires-Dist: rsyncmanager; extra == "full" Requires-Dist: fabio; extra == "full" Requires-Dist: h5py>=3; extra == "full" Requires-Dist: lxml; extra == "full" Requires-Dist: werkzeug; extra == "full" Requires-Dist: json-rpc; extra == "full" Requires-Dist: scipy; extra == "full" Requires-Dist: Pillow; extra == "full" Requires-Dist: glymur; extra == "full" Requires-Dist: resource; extra == "full" Requires-Dist: tifffile; extra == "full" Requires-Dist: hdf5plugin; extra == "full" Requires-Dist: pyicat_plus; extra == "full" Requires-Dist: ewoksnotify[full]; extra == "full" Requires-Dist: pyicat_plus; extra == "full" Requires-Dist: nabu[full]>=2023.3.1dev; extra == "full" Requires-Dist: pycuda<2024.1.1; extra == "full" Requires-Dist: scikit-cuda; extra == "full" Provides-Extra: doc Requires-Dist: orange-canvas-core; extra == "doc" Requires-Dist: orange-widget-base; extra == "doc" Requires-Dist: ewoks[orange]>=0.1.1; extra == "doc" Requires-Dist: ewoksorange>=0.7.0rc0; extra == "doc" Requires-Dist: rsyncmanager; extra == "doc" Requires-Dist: fabio; extra == "doc" Requires-Dist: h5py>=3; extra == "doc" Requires-Dist: lxml; extra == "doc" Requires-Dist: werkzeug; extra == "doc" Requires-Dist: json-rpc; extra == "doc" Requires-Dist: scipy; extra == "doc" Requires-Dist: Pillow; extra == "doc" Requires-Dist: glymur; extra == "doc" Requires-Dist: resource; extra == "doc" Requires-Dist: tifffile; extra == "doc" Requires-Dist: hdf5plugin; extra == "doc" Requires-Dist: pyicat_plus; extra == "doc" Requires-Dist: ewoksnotify[full]; extra == "doc" Requires-Dist: pyicat_plus; extra == "doc" Requires-Dist: nabu>=2023.3.1dev; extra == "doc" Requires-Dist: Sphinx>=4.0.0; extra == "doc" Requires-Dist: nbsphinx; extra == "doc" Requires-Dist: pandoc; extra == "doc" Requires-Dist: jupyterlab; extra == "doc" Requires-Dist: pydata_sphinx_theme; extra == "doc" Requires-Dist: sphinx-design; extra == "doc" Requires-Dist: sphinx-autodoc-typehints; extra == "doc" Requires-Dist: sphinxcontrib-youtube; extra == "doc" Provides-Extra: dev-spec Requires-Dist: black; extra == "dev-spec" Requires-Dist: flake8; extra == "dev-spec" Requires-Dist: timeout-decorator; extra == "dev-spec" Requires-Dist: pyopencl; extra == "dev-spec" Provides-Extra: dev Requires-Dist: orange-canvas-core; extra == "dev" Requires-Dist: orange-widget-base; extra == "dev" Requires-Dist: ewoks[orange]>=0.1.1; extra == "dev" Requires-Dist: ewoksorange>=0.7.0rc0; extra == "dev" Requires-Dist: rsyncmanager; extra == "dev" Requires-Dist: fabio; extra == "dev" Requires-Dist: h5py>=3; extra == "dev" Requires-Dist: lxml; extra == "dev" Requires-Dist: werkzeug; extra == "dev" Requires-Dist: json-rpc; extra == "dev" Requires-Dist: scipy; extra == "dev" Requires-Dist: Pillow; extra == "dev" Requires-Dist: glymur; extra == "dev" Requires-Dist: resource; extra == "dev" Requires-Dist: tifffile; extra == "dev" Requires-Dist: hdf5plugin; extra == "dev" Requires-Dist: pyicat_plus; extra == "dev" Requires-Dist: ewoksnotify[full]; extra == "dev" Requires-Dist: pyicat_plus; extra == "dev" Requires-Dist: nabu[full]>=2023.3.1dev; extra == "dev" Requires-Dist: pycuda<2024.1.1; extra == "dev" Requires-Dist: scikit-cuda; extra == "dev" Requires-Dist: black; extra == "dev" Requires-Dist: flake8; extra == "dev" Requires-Dist: timeout-decorator; extra == "dev" Requires-Dist: pyopencl; extra == "dev" Provides-Extra: dev-no-cuda Requires-Dist: orange-canvas-core; extra == "dev-no-cuda" Requires-Dist: orange-widget-base; extra == "dev-no-cuda" Requires-Dist: ewoks[orange]>=0.1.1; extra == "dev-no-cuda" Requires-Dist: ewoksorange>=0.7.0rc0; extra == "dev-no-cuda" Requires-Dist: rsyncmanager; extra == "dev-no-cuda" Requires-Dist: fabio; extra == "dev-no-cuda" Requires-Dist: h5py>=3; extra == "dev-no-cuda" Requires-Dist: lxml; extra == "dev-no-cuda" Requires-Dist: werkzeug; extra == "dev-no-cuda" Requires-Dist: json-rpc; extra == "dev-no-cuda" Requires-Dist: scipy; extra == "dev-no-cuda" Requires-Dist: Pillow; extra == "dev-no-cuda" Requires-Dist: glymur; extra == "dev-no-cuda" Requires-Dist: resource; extra == "dev-no-cuda" Requires-Dist: tifffile; extra == "dev-no-cuda" Requires-Dist: hdf5plugin; extra == "dev-no-cuda" Requires-Dist: pyicat_plus; extra == "dev-no-cuda" Requires-Dist: ewoksnotify[full]; extra == "dev-no-cuda" Requires-Dist: pyicat_plus; extra == "dev-no-cuda" Requires-Dist: nabu>=2023.3.1dev; extra == "dev-no-cuda" Requires-Dist: black; extra == "dev-no-cuda" Requires-Dist: flake8; extra == "dev-no-cuda" Requires-Dist: timeout-decorator; extra == "dev-no-cuda" Requires-Dist: pyopencl; extra == "dev-no-cuda" Provides-Extra: test Requires-Dist: orange-canvas-core; extra == "test" Requires-Dist: orange-widget-base; extra == "test" Requires-Dist: ewoks[orange]>=0.1.1; extra == "test" Requires-Dist: ewoksorange>=0.7.0rc0; extra == "test" Requires-Dist: rsyncmanager; extra == "test" Requires-Dist: fabio; extra == "test" Requires-Dist: h5py>=3; extra == "test" Requires-Dist: lxml; extra == "test" Requires-Dist: werkzeug; extra == "test" Requires-Dist: json-rpc; extra == "test" Requires-Dist: scipy; extra == "test" Requires-Dist: Pillow; extra == "test" Requires-Dist: glymur; extra == "test" Requires-Dist: resource; extra == "test" Requires-Dist: tifffile; extra == "test" Requires-Dist: hdf5plugin; extra == "test" Requires-Dist: pyicat_plus; extra == "test" Requires-Dist: ewoksnotify[full]; extra == "test" Requires-Dist: pyicat_plus; extra == "test" Requires-Dist: nabu>=2023.3.1dev; extra == "test" Requires-Dist: pytest-asyncio; extra == "test" Requires-Dist: tomoscan[test]>=2.1.0a18; extra == "test" Provides-Extra: setup-requires Requires-Dist: setuptools; extra == "setup-requires" Requires-Dist: numpy>=1.12; extra == "setup-requires" .. image:: doc/img/tomwer.png :alt: Tomwer Logo :align: left :width: 400px Introduction ------------ **Tomwer** provides tools to automate acquisition and reconstruction processes for tomography. The package includes: - A library to individually access each acquisition process. - Graphical User Interface (GUI) applications to control key processes such as reconstruction and data transfer, which can be executed as standalone applications. - An Orange add-on to help users define custom workflows (`Orange3 `_). Tomwer relies on `Nabu `_ for tomographic reconstruction. **Note**: Currently, the software is only compatible with Linux. Documentation ------------- The latest version of the documentation is available `here `_. Installation ------------ Step 1: Installing Tomwer ''''''''''''''''''''''''' To install Tomwer with all features: .. code-block:: bash pip install tomwer[full] Alternatively, you can install the latest development branch from the repository: .. code-block:: bash pip install git+https://gitlab.esrf.fr/tomotools/tomwer/#egg=tomwer[full] Step 2: (Optional) Update Orange-CANVAS-CORE and Orange-WIDGET-BASE '''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''' If you need access to additional 'processing' wheels and 'reprocess action,' you may want to update these Orange forks. This is optional, as the project works with the native Orange libraries. .. code-block:: bash pip install git+https://github.com/payno/orange-canvas-core --no-deps --upgrade pip install git+https://github.com/payno/orange-widget-base --no-deps --upgrade Launching Applications ----------------------- After installation, Tomwer includes several applications. You can launch an application by running: .. code-block:: bash tomwer [options] - If you run `tomwer` without arguments, a manual page will be displayed. - For application-specific help, run: .. code-block:: bash tomwer --help Tomwer Canvas - Orange Canvas ----------------------------- You can launch the Orange canvas to create workflows using the available building blocks: .. code-block:: bash tomwer canvas - Alternatively, you can use `orange-canvas`. - If you're using a virtual environment, remember to activate it: .. code-block:: bash source myvirtualenv/bin/activate Building Documentation ----------------------- To build the documentation: .. code-block:: bash sphinx-build doc build/html The documentation will be generated in `doc/build/html`, and the entry point is `index.html`. To view the documentation in a browser: .. code-block:: bash firefox build/html/index.html **Note**: Building the documentation requires `sphinx` to be installed, which is not a hard dependency of Tomwer. If needed, install it separately. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1736265726.0 tomwer-1.4.8/README.rst0000644000175000017500000000557714737247776013674 0ustar00paynopayno.. image:: doc/img/tomwer.png :alt: Tomwer Logo :align: left :width: 400px Introduction ------------ **Tomwer** provides tools to automate acquisition and reconstruction processes for tomography. The package includes: - A library to individually access each acquisition process. - Graphical User Interface (GUI) applications to control key processes such as reconstruction and data transfer, which can be executed as standalone applications. - An Orange add-on to help users define custom workflows (`Orange3 `_). Tomwer relies on `Nabu `_ for tomographic reconstruction. **Note**: Currently, the software is only compatible with Linux. Documentation ------------- The latest version of the documentation is available `here `_. Installation ------------ Step 1: Installing Tomwer ''''''''''''''''''''''''' To install Tomwer with all features: .. code-block:: bash pip install tomwer[full] Alternatively, you can install the latest development branch from the repository: .. code-block:: bash pip install git+https://gitlab.esrf.fr/tomotools/tomwer/#egg=tomwer[full] Step 2: (Optional) Update Orange-CANVAS-CORE and Orange-WIDGET-BASE '''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''' If you need access to additional 'processing' wheels and 'reprocess action,' you may want to update these Orange forks. This is optional, as the project works with the native Orange libraries. .. code-block:: bash pip install git+https://github.com/payno/orange-canvas-core --no-deps --upgrade pip install git+https://github.com/payno/orange-widget-base --no-deps --upgrade Launching Applications ----------------------- After installation, Tomwer includes several applications. You can launch an application by running: .. code-block:: bash tomwer [options] - If you run `tomwer` without arguments, a manual page will be displayed. - For application-specific help, run: .. code-block:: bash tomwer --help Tomwer Canvas - Orange Canvas ----------------------------- You can launch the Orange canvas to create workflows using the available building blocks: .. code-block:: bash tomwer canvas - Alternatively, you can use `orange-canvas`. - If you're using a virtual environment, remember to activate it: .. code-block:: bash source myvirtualenv/bin/activate Building Documentation ----------------------- To build the documentation: .. code-block:: bash sphinx-build doc build/html The documentation will be generated in `doc/build/html`, and the entry point is `index.html`. To view the documentation in a browser: .. code-block:: bash firefox build/html/index.html **Note**: Building the documentation requires `sphinx` to be installed, which is not a hard dependency of Tomwer. If needed, install it separately. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739271825.0 tomwer-1.4.8/pyproject.toml0000644000175000017500000000044214752627221015063 0ustar00paynopayno[build-system] requires = [ "setuptools>=46.4", "wheel", ] build-backend = "setuptools.build_meta" [tool.pytest.ini_options] # consider_namespace_packages = true # for pytest>=8.1 addopts = "--import-mode=importlib" # for all pytest versions, pytest-cov needs `pip install -e .` ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1739271865.9677656 tomwer-1.4.8/setup.cfg0000644000175000017500000000707514752627272014007 0ustar00paynopayno[metadata] name = tomwer version = attr: tomwer.__version__ author = Henri Payno, Pierre Paleo, Pierre-Olivier Autran, Jérôme Lesaint, Alessandro Mirone author_email = henri.payno@esrf.fr, pierre.paleo@esrf.fr, pierre-olivier.autran@esrf.fr, jerome.lesaint@esrf.fr, mirone@esrf.fr description = "tomography workflow tools" long_description = file: README.rst long_description_content_type = text/x-rst license = MIT url = https://gitlab.esrf.fr/tomotools/tomwer project_urls = Bug Tracker = https://gitlab.esrf.fr/tomotools/tomwer/-/issues classifiers = Intended Audience :: Education Intended Audience :: Science/Research License :: OSI Approved :: MIT License Programming Language :: Python :: 3 Environment :: Console Environment :: X11 Applications :: Qt Operating System :: POSIX Natural Language :: English Topic :: Scientific/Engineering :: Physics Topic :: Software Development :: Libraries :: Python Modules keywords = orange3 add-on,ewoks [options] package_dir = =src packages = find_namespace: python_requires = >=3.6 install_requires = numpy setuptools psutil silx[full] >= 2.0,<2.1.1 tomoscan>=2.1.0a18 nxtomo>=1.3.0dev4 nxtomomill>=1.1.0a0 processview>=1.5.0 ewoks>=0.1.1 ewokscore<1.1.0 sluurp>=0.4.1 packaging pyunitsystem>=2.0.0a tqdm [options.packages.find] where = src [options.entry_points] console_scripts = tomwer=tomwer.__main__:main orange3.addon = tomwer-add-on=orangecontrib.tomwer orange.widgets = tomwer=orangecontrib.tomwer.widgets orangecanvas.examples = tomo_examples=orangecontrib.tomwer.examples orange.widgets.tutorials = tomo_tutorials=orangecontrib.tomwer.tutorials tomo_tutorials_id16b=orangecontrib.tomwer.tutorials.id16b orange.canvas.help = html-index=orangecontrib.tomwer.widgets:WIDGET_HELP_PATH ewoks.tasks.class = tomwer.core.process.*.*=tomwer [options.package_data] tomwer.resources = gui/icons/*.png gui/icons/*.svg gui/icons/*.npy gui/illustrations/*.svg gui/illustrations/*.png orangecontrib.tomwer = tutorials/*.ows tutorials/id16b/*.ows widgets/icons/*.png widgets/icons/*.svg widgets/cluster/icons/*.png widgets/cluster/icons/*.svg widgets/control/icons/*.png widgets/control/icons/*.svg widgets/debugtools/icons/*.png widgets/debugtools/icons/*.svg widgets/edit/icons/*.png widgets/edit/icons/*.svg widgets/dataportal/icons/*.png widgets/dataportal/icons/*.svg widgets/reconstruction/icons/*.png widgets/reconstruction/icons/*.svg widgets/visualization/icons/*.png widgets/visualization/icons/*.svg widgets/other/icons/*.png widgets/other/icons/*.svg [options.extras_require] full_base = orange-canvas-core orange-widget-base ewoks[orange]>=0.1.1 ewoksorange >= 0.7.0rc0 rsyncmanager fabio h5py>=3 lxml werkzeug json-rpc scipy Pillow glymur resource tifffile hdf5plugin pyicat_plus ewoksnotify[full] pyicat_plus full_no_nabu = %(full_base)s full_no_cuda = %(full_base)s nabu>=2023.3.1dev full = %(full_base)s nabu[full]>=2023.3.1dev pycuda<2024.1.1 scikit-cuda doc = %(full_no_cuda)s Sphinx>=4.0.0 nbsphinx pandoc jupyterlab pydata_sphinx_theme sphinx-design sphinx-autodoc-typehints sphinxcontrib-youtube dev_spec = black flake8 timeout-decorator pyopencl dev = %(full)s %(dev_spec)s dev_no_cuda = %(full_no_cuda)s %(dev_spec)s test = %(full_no_cuda)s pytest-asyncio tomoscan[test]>=2.1.0a18 setup_requires = setuptools numpy>=1.12 [build_sphinx] source-dir = ./doc [flake8] ignore = E501,W503,E203,E265,E266,E712,E262 max-line-length = 88 exclude = .eggs doc/ext [coverage:run] omit = setup.py */test/* [egg_info] tag_build = tag_date = 0 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739271825.0 tomwer-1.4.8/setup.py0000644000175000017500000000010514752627221013655 0ustar00paynopaynoimport setuptools if __name__ == "__main__": setuptools.setup() ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1739271865.8597631 tomwer-1.4.8/src/0000755000175000017500000000000014752627272012744 5ustar00paynopayno././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1739271865.8597631 tomwer-1.4.8/src/orangecontrib/0000755000175000017500000000000014752627272015600 5ustar00paynopayno././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1739271865.8717635 tomwer-1.4.8/src/orangecontrib/tomwer/0000755000175000017500000000000014752627272017115 5ustar00paynopayno././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1737644878.0 tomwer-1.4.8/src/orangecontrib/tomwer/__init__.py0000644000175000017500000000027214744455516021230 0ustar00paynopayno""" Orange add-on for tomography """ import logging from . import state_summary # noqa F401 fabio_logger = logging.getLogger("fabio.edfimage") fabio_logger.setLevel(logging.WARNING) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1739271865.8717635 tomwer-1.4.8/src/orangecontrib/tomwer/orange/0000755000175000017500000000000014752627272020370 5ustar00paynopayno././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1731088693.0 tomwer-1.4.8/src/orangecontrib/tomwer/orange/__init__.py0000644000175000017500000000000014713450465022462 0ustar00paynopayno././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1737644878.0 tomwer-1.4.8/src/orangecontrib/tomwer/orange/managedprocess.py0000644000175000017500000000667014744455516023747 0ustar00paynopaynofrom __future__ import annotations import functools import logging from ewoksorange.bindings import OWEwoksWidgetWithTaskStack from ewoksorange.bindings.owwidgets import invalid_data from orangewidget.widget import OWBaseWidget from processview.core.manager import DatasetState, ProcessManager from processview.core.superviseprocess import SuperviseProcess from orangecontrib.tomwer.widgets.utils import WidgetLongProcessing _logger = logging.getLogger(__name__) class _SuperviseMixIn(SuperviseProcess): def __init__(self, process_id=None): SuperviseProcess.__init__(self, process_id=process_id) self.destroyed.connect(functools.partial(ProcessManager().unregister, self)) def setCaption(self, caption): self.name = caption try: ProcessManager().process_renamed(process=self) except Exception as e: _logger.warning(f"Fail to update process name. Error is {e}") def notify_skip(self, scan, details=None): ProcessManager().notify_dataset_state( dataset=scan, process=self, state=DatasetState.SKIPPED, details=details ) def notify_pending(self, scan, details=None): ProcessManager().notify_dataset_state( dataset=scan, process=self, state=DatasetState.PENDING, details=details ) def notify_succeed(self, scan, details=None): ProcessManager().notify_dataset_state( dataset=scan, process=self, state=DatasetState.SUCCEED, details=details ) def notify_failed(self, scan, details=None): ProcessManager().notify_dataset_state( dataset=scan, process=self, state=DatasetState.FAILED, details=details ) def notify_on_going(self, scan, details=None): ProcessManager().notify_dataset_state( dataset=scan, process=self, state=DatasetState.ON_GOING, details=details ) class SuperviseOW(OWBaseWidget, _SuperviseMixIn, openclass=True): """ A basic OWWidget but registered on the process manager """ want_control_area = False def __init__(self, parent, process_id=None): OWBaseWidget.__init__(self, parent, process_id=process_id) _SuperviseMixIn.__init__(self, process_id=process_id) def setCaption(self, caption): OWBaseWidget.setCaption(self, caption) _SuperviseMixIn.setCaption(self, caption=caption) class TomwerWithStackStack( OWEwoksWidgetWithTaskStack, _SuperviseMixIn, WidgetLongProcessing, openclass=True ): def __init__(self, parent, process_id=None, *args, **kwargs): OWEwoksWidgetWithTaskStack.__init__(self, parent, args, kwargs) _SuperviseMixIn.__init__(self, process_id=process_id) self.task_executor_queue.sigComputationStarted.connect(self._startProcessing) self.task_executor_queue.sigComputationEnded.connect(self._endProcessing) def setCaption(self, caption): OWBaseWidget.setCaption(self, caption) _SuperviseMixIn.setCaption(self, caption=caption) def trigger_downstream(self) -> None: # for now ewoksorange send ewoks variable. This will work only if # all task are implemented using ewokwidget which is not the case today for ewoksname, var in self.get_task_outputs().items(): channel = self._get_output_signal(ewoksname) if invalid_data.is_invalid_data(var.value): channel.send(None) # or channel.invalidate? else: channel.send(var.value) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1736265726.0 tomwer-1.4.8/src/orangecontrib/tomwer/orange/settings.py0000644000175000017500000000132014737247776022610 0ustar00paynopaynofrom __future__ import annotations from orangewidget import settings class CallbackSettingsHandler(settings.SettingsHandler): """ Settings handler used to call some callback before packing data (so before) saving orange :class:`Setting` """ def __init__(self): super(CallbackSettingsHandler, self).__init__() self.__callbacks = [] def addCallback(self, _callback): self.__callbacks.append(_callback) def removeCallback(self, callback): self.__callbacks.remove(callback) def pack_data(self, widget): """""" for callback in self.__callbacks: callback() return super(CallbackSettingsHandler, self).pack_data(widget) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1731088693.0 tomwer-1.4.8/src/orangecontrib/tomwer/state_summary.py0000644000175000017500000000325714713450465022366 0ustar00paynopaynofrom orangewidget.utils.signals import PartialSummary, summarize from tomwer.core.cluster import SlurmClusterConfiguration from tomwer.core.futureobject import FutureTomwerObject from tomwer.core.scan.blissscan import BlissScan from tomwer.core.scan.scanbase import TomwerScanBase @summarize.register(object) # noqa F811 def summarize_(Object: object): # noqa F811 return PartialSummary("any object", "an oject of any type") @summarize.register(dict) # noqa F811 def summarize_(configuration: dict): # noqa F811 return PartialSummary( "any configuration", "any configuration that can be provided to a process" ) @summarize.register(SlurmClusterConfiguration) # noqa F811 def summarize_(cluster_config: SlurmClusterConfiguration): # noqa F811 return PartialSummary( "cluster configuration", "cluster configuration to launch some remote processing", ) @summarize.register(TomwerScanBase) # noqa F811 def summarize_(data: TomwerScanBase): # noqa F811 return PartialSummary( "dataset with processing history", "core object used to ship dataset and history of processing done on this dataset", ) @summarize.register(FutureTomwerObject) # noqa F811 def summarize_(future_data: FutureTomwerObject): # noqa F811 return PartialSummary( "dataset with pending processing", "object used when there is some pending processing (asyncio.future). Can be convert back to `data`", ) @summarize.register(BlissScan) # noqa F811 def summarize_(bliss_scan: BlissScan): # noqa F811 return PartialSummary( "raw dataset from bliss", "object used when debug some processing relative to bliss", ) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1739271865.8717635 tomwer-1.4.8/src/orangecontrib/tomwer/tests/0000755000175000017500000000000014752627272020257 5ustar00paynopayno././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1738938720.0 tomwer-1.4.8/src/orangecontrib/tomwer/tests/TestAcquisition.py0000644000175000017500000001714414751414540023757 0ustar00paynopayno"""Full tomwer test suite.""" from __future__ import annotations import logging import os import shutil from silx.gui import qt from tomwer.core.scan.edfscan import EDFTomoScan from tomwer.tests.datasets import TomwerCIDatasets logger = logging.getLogger(__name__) class Simulation(qt.QThread): """Simulation is a simple class able to simulate an acquisition by copying files on a targetted directory. :param targetdir: the folder where the acquisition is stored :param manipulationId: the id of the simulation we want to simulate :param finalState: when launched, the state to reach before stopping :warning: the targetted directory won't be removed or cleaned during class destruction. This is to be managed by callers. """ advancement = { "not started": -1, "starting-s0": 0, "starting-s1": 1, "acquisitionRunning": 2, "acquisitionDone": 3, "reconstructionLaunched": 4, } sigAdvancementChanged = qt.Signal(int) __definedDataset = ["test01", "test10"] def __init__(self, targetdir, manipulationId, finalState=4): assert type(manipulationId) is str assert type(targetdir) is str assert manipulationId in self.__definedDataset super(Simulation, self).__init__() self.targetdir = targetdir self.outputFolder = os.path.sep.join((targetdir, manipulationId)) self.finalState = finalState self.currentState = "not started" self._createFinalXML = False ( self.originalFolder, self.nbSlices, self.manipulationId, ) = self.__getOriginalDataSet(manipulationId) self.stopFileCreationForRunningState = int(self.nbSlices / 2) self.srcPattern = None self.destPattern = None def __getOriginalDataSet(self, dataSetID): """Return paths to the requested scan""" assert dataSetID in self.__definedDataset dataDir = TomwerCIDatasets.get_dataset(os.path.join("edf_datasets", dataSetID)) assert os.path.isdir(dataDir) assert os.path.isfile(os.path.join(dataDir, dataSetID + ".info")) slices = EDFTomoScan(dataDir).projections nbSlices = len(slices) manipulationID = dataSetID return dataDir, nbSlices, manipulationID def advanceTo(self, state): """Reset the new advancement targetted :param state: the new state to reach when run will be executed """ assert state in Simulation.advancement assert type(state) is str self.finalState = Simulation.advancement[state] def setSrcDestPatterns(self, srcPattern, destPattern): """ If setted, will set the .info and .xml files into a different folder """ self.srcPattern = srcPattern self.destPattern = destPattern if srcPattern is not None or destPattern is not None: assert os.path.isdir(srcPattern) assert os.path.isdir(destPattern) targettedFolder = self.outputFolder.replace( self.srcPattern, self.destPattern, 1 ) if not os.path.isdir(targettedFolder): os.makedirs(targettedFolder) def __shouldExecStep(self, step): """Return True if the thread should exec this step to advance taking into consideration is current state and his final state """ return self.finalState >= self.advancement[step] and ( Simulation.advancement[self.currentState] + 1 == self.advancement[step] ) def run(self): """Main function, run the acquisition through all states until finalState is reached """ if self.__shouldExecStep("starting-s0") is True: logger.info("starting-s0") self._startAcquisition() self.currentState = "starting-s0" self.signalCurrentState() if self.__shouldExecStep("starting-s1") is True: self.copyInitialFiles() logger.info("starting-s1") self.currentState = "starting-s1" self.signalCurrentState() if self.__shouldExecStep("acquisitionRunning") is True: self._copyScans((0, self.stopFileCreationForRunningState)) logger.info("acquisitionRunning") self.currentState = "acquisitionRunning" self.signalCurrentState() if self.__shouldExecStep("acquisitionDone") is True: self._copyScans((self.stopFileCreationForRunningState, self.nbSlices)) if self._createFinalXML is True: inputXMLFile = os.path.join( self.originalFolder, self.manipulationId + ".xml" ) assert os.path.isfile(inputXMLFile) ouputXMLFile = os.path.join( self.outputFolder, self.manipulationId + ".xml" ) shutil.copyfile(inputXMLFile, ouputXMLFile) logger.info("acquisitionDone") self.currentState = "acquisitionDone" self.signalCurrentState() def signalCurrentState(self): """Signal the actual state of the simulation""" self.sigAdvancementChanged.emit(self.currentState) def _startAcquisition(self): """create needed data dir""" for newFolder in (self.targetdir, self.outputFolder): if not os.path.exists(self.outputFolder): os.makedirs(self.outputFolder) def _copyScans(self, _slicesRange): """copy the .edf file from the original directory to the outputFolder :_slicesRange tuple: the _range of slices data we want to copy """ logger.info("copying files from %s to %s" % (_slicesRange[0], _slicesRange[1])) for iSlice in list(range(_slicesRange[0], _slicesRange[1])): filename = "".join((self.manipulationId, format(iSlice, "04d"), ".edf")) srcFile = os.path.join(self.originalFolder, filename) outputFile = os.path.join(self.outputFolder, filename) assert os.path.isfile(srcFile) assert os.path.isdir(self.outputFolder) shutil.copyfile(srcFile, outputFile) def copyInitialFiles(self): """copy the .info file""" assert os.path.isdir(self.originalFolder) logger.info( "copying initial files (.info, .xml...) from %s to %s" % (self.originalFolder, self.manipulationId) ) for extension in (".info", ".db", ".cfg"): filename = "".join((self.manipulationId, extension)) srcFile = os.path.join(self.originalFolder, filename) targettedFolder = self.outputFolder if self.srcPattern is not None or self.destPattern is not None: targettedFolder = self.outputFolder.replace( self.srcPattern, self.destPattern, 1 ) assert os.path.isfile(srcFile) assert os.path.isdir(targettedFolder) assert os.path.isdir(self.originalFolder) shutil.copy2(srcFile, targettedFolder) def createFinalXML(self, val): """If activated, once all the file will be copied, this will create an .xml file into the output directory """ self._createFinalXML = val def createParFile(self): pass def createReconstructedFile(self): pass def createOARJob(self): pass def createDark(self): pass def createJPG(self): pass def createVolfloat(self): pass def createVolraw(self): pass def __createFileTo(self, filePath): assert type(filePath) is str open(filePath, "a").close() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1736265726.0 tomwer-1.4.8/src/orangecontrib/tomwer/tests/__init__.py0000644000175000017500000000000014737247776022370 0ustar00paynopayno././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1739271865.8717635 tomwer-1.4.8/src/orangecontrib/tomwer/tutorials/0000755000175000017500000000000014752627272021143 5ustar00paynopayno././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739271825.0 tomwer-1.4.8/src/orangecontrib/tomwer/tutorials/EBS_tomo_listener.ows0000644000175000017500000001121314752627221025241 0ustar00paynopayno wait for bliss scan and create NXtomo compute reduced darks and flats compute COR compute slice browse dataset and reconstructed slice(s) {'_blissConfiguration': {}, '_nxtomo_cfg_file': '', '_static_input': {'nxtomomill_cfg_file': ''}, 'controlAreaVisible': True, 'savedWidgetGeometry': None, '__version__': 1} {'_ewoks_default_inputs': {'data': None, 'dark_ref_params': None}, 'controlAreaVisible': True, 'savedWidgetGeometry': None, '__version__': 1} {'_ewoks_default_inputs': {'data': None, 'axis_params': {'MODE': 'sino-sliding-window', 'POSITION_VALUE': None, 'CALC_INPUT_TYPE': 'transmission_nopag', 'ANGLE_MODE': '0-180', 'USE_SINOGRAM': True, 'SINOGRAM_LINE': 'middle', 'SINOGRAM_SUBSAMPLING': 10, 'AXIS_URL_1': '', 'AXIS_URL_2': '', 'LOOK_AT_STDMAX': False, 'NEAR_WX': 5, 'FINE_STEP_X': 0.1, 'SCALE_IMG2_TO_IMG1': False, 'NEAR_POSITION': 0.0, 'PADDING_MODE': 'edge', 'FLIP_LR': True, 'COMPOSITE_OPTS': {'theta': 10, 'oversampling': 4, 'n_subsampling_y': 10, 'take_log': True, 'near_pos': 0.0, 'near_width': 20}, 'SIDE': 'left', 'COR_OPTIONS': ''}, 'gui': {'mode_is_lock': True, 'value_is_lock': False, 'auto_update_estimated_cor': True}}, 'controlAreaVisible': True, 'savedWidgetGeometry': b'\x01\xd9\xd0\xcb\x00\x03\x00\x00\x00\x00\x01k\x00\x00\x00\xac\x00\x00\x06\x01\x00\x00\x03\x8e\x00\x00\x01k\x00\x00\x00\xd1\x00\x00\x06\x01\x00\x00\x03\x8e\x00\x00\x00\x00\x00\x00\x00\x00\x07\x80\x00\x00\x01k\x00\x00\x00\xd1\x00\x00\x06\x01\x00\x00\x03\x8e', '__version__': 1} {'_ewoks_default_inputs': {'data': None, 'nabu_params': None}, 'controlAreaVisible': True, 'savedWidgetGeometry': None, '__version__': 1} {'_viewer_config': {}, 'controlAreaVisible': True, 'savedWidgetGeometry': None, '__version__': 1} ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1731088693.0 tomwer-1.4.8/src/orangecontrib/tomwer/tutorials/__init__.py0000644000175000017500000000000014713450465023235 0ustar00paynopayno././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1736265726.0 tomwer-1.4.8/src/orangecontrib/tomwer/tutorials/append_raw_darks_and_flats_frames_to_NXtomos.ows0000644000175000017500000001255314737247776032762 0ustar00paynopayno Context: we want to modify a NXtomo in order to append some (raw) darks / flats to it. Then those raw darks and flats will be used for reduced darks / flats calculation. warning: reduced darks and flats are usually mean / median of a serie of darks / flats. when raw darks and flats are raw frame before mean / median 'reduction' NXtomo to be patched Step 3 : process NXtomo step1: define frame series to be ignored (here darks and flats series to avoid conflict with the one added by the dark / flat patch) result: NXtomo with patched frame (step 2) and reduced darks and flats to be used for reconstruction standard darks / flats reduction (but taking into account patched darks / flats frames) step2: provide series of darks / flats from other(s) NXtomo to be added to the `NXtomo to be patched` {'_ewoks_default_inputs': {'operations': {}}, '_ewoks_execinfo': {}, '_ewoks_varinfo': {}, 'controlAreaVisible': True, 'default_inputs': {}, 'savedWidgetGeometry': b'\x01\xd9\xd0\xcb\x00\x03\x00\x00\x00\x00\x07\xe6\x00\x00\x01\x18\x00\x00\n/\x00\x00\x03-\x00\x00\x07\xeb\x00\x00\x015\x00\x00\n*\x00\x00\x03(\x00\x00\x00\x01\x00\x00\x00\x00\x07\x7f\x00\x00\x07\xeb\x00\x00\x015\x00\x00\n*\x00\x00\x03(', '__version__': 1} {'_urlsSetting': {}, 'controlAreaVisible': True, 'savedWidgetGeometry': None, '__version__': 1} {'_ewoks_default_inputs': {'data': None, 'dark_ref_params': {'DOWHEN': 'before', 'DARKCAL': 'mean', 'DARKOVE': 0, 'DARKRMV': 0, 'DKFILE': 'darkend[0-9]{3,4}', 'REFSCAL': 'median', 'REFSOVE': 0, 'REFSRMV': 0, 'RFFILE': 'ref*.*[0-9]{3,4}_[0-9]{3,4}'}}, 'controlAreaVisible': True, 'savedWidgetGeometry': b'\x01\xd9\xd0\xcb\x00\x03\x00\x00\x00\x00\x06\x9b\x00\x00\x02K\x00\x00\n\x95\x00\x00\x04\x8f\x00\x00\x06\xa0\x00\x00\x02h\x00\x00\n\x90\x00\x00\x04\x8a\x00\x00\x00\x01\x00\x00\x00\x00\x07\x7f\x00\x00\x06\xa0\x00\x00\x02h\x00\x00\n\x90\x00\x00\x04\x8a', '__version__': 1} {'_scanIDs': [], 'controlAreaVisible': True, 'savedWidgetGeometry': None, '__version__': 1} {'_scanIDs': [], 'controlAreaVisible': True, 'savedWidgetGeometry': None, '__version__': 1} ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1731088693.0 tomwer-1.4.8/src/orangecontrib/tomwer/tutorials/cast_volume.ows0000644000175000017500000000554614713450465024223 0ustar00paynopayno define volume volume casting display original volume display volume casted {'_scanIDs': [], 'controlAreaVisible': True, 'savedWidgetGeometry': None, '__version__': 1} {'_ewoks_default_inputs': {'data': None, 'cast_volume_params': {}}, 'controlAreaVisible': True, 'savedWidgetGeometry': None, '__version__': 1} {'controlAreaVisible': True, 'savedWidgetGeometry': None, '__version__': 1} {'controlAreaVisible': True, 'savedWidgetGeometry': None, '__version__': 1} ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1736265726.0 tomwer-1.4.8/src/orangecontrib/tomwer/tutorials/copy_reduced_darks_and_flats_meth1.ows0000644000175000017500000001407714737247776030662 0ustar00paynopayno step2: provide scans for which we need to copy reduced darks and flats invalid any existing darks or flats frames (else reduced darks and flats will use them instead of making the copy) will copy reduced darks / flats to `dataset_{darks|flats}.hdf5` to be used for reconstruction step1: provide scan with already computed darks and flats result: scans with copied reduced darks / flats Context: on this workflow we want to copy reduced flats / darks from a dataset to others. This one of the various way to do this. Note: You can also replace the two upper widgets (providing reduced darks and flats) by a python script providing a python dictionary. * Key should be position of the flat / dark in the sequence (so 0 if at the beginning for example) * Value is the 2D numpy array with mean / median /xxx of the darks and flats serie {'_scanIDs': [], 'controlAreaVisible': True, 'savedWidgetGeometry': None, '__version__': 1} gASVLwEAAAAAAAB9lCiMFV9ld29rc19kZWZhdWx0X2lucHV0c5R9lIwKb3BlcmF0aW9uc5R9lCiM G3RvbW9zY2FuLmVzcmYuc2Nhbi5oZGY1c2NhbpSMCEltYWdlS2V5lJOUSwKFlFKUaAdLA4WUUpRo B0sBhZRSlGgLdXOMD19ld29rc19leGVjaW5mb5R9lIwOX2V3b2tzX3ZhcmluZm+UfZSMEmNvbnRy b2xBcmVhVmlzaWJsZZSIjA5kZWZhdWx0X2lucHV0c5R9lIwTc2F2ZWRXaWRnZXRHZW9tZXRyeZRD QgHZ0MsAAwAAAAAH5gAAARgAAAovAAADLQAAB+sAAAE1AAAKKgAAAygAAAABAAAAAAd/AAAH6wAA ATUAAAoqAAADKJSMC19fdmVyc2lvbl9flEsBdS4= {'_ewoks_default_inputs': {'data': None, 'dark_ref_params': None}, 'controlAreaVisible': True, 'savedWidgetGeometry': b'\x01\xd9\xd0\xcb\x00\x03\x00\x00\x00\x00\x07\x0e\x00\x00\x015\x00\x00\x0b\x08\x00\x00\x03\x10\x00\x00\x07\x13\x00\x00\x01R\x00\x00\x0b\x03\x00\x00\x03\x0b\x00\x00\x00\x01\x00\x00\x00\x00\x07\x7f\x00\x00\x07\x13\x00\x00\x01R\x00\x00\x0b\x03\x00\x00\x03\x0b', '__version__': 1} {'_tomo_obj_setting': '', 'controlAreaVisible': True, 'savedWidgetGeometry': None, '__version__': 1} {'_tomo_obj_setting': '', 'controlAreaVisible': True, 'savedWidgetGeometry': None, '__version__': 1} {'_scanIDs': [], 'controlAreaVisible': True, 'savedWidgetGeometry': None, '__version__': 1} ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1731088693.0 tomwer-1.4.8/src/orangecontrib/tomwer/tutorials/copy_reduced_darks_and_flats_meth2.ows0000644000175000017500000001212414713450465030633 0ustar00paynopayno step2: provide scans for which we need to copy reduced darks and flats invalid any existing darks or flats frames (else reduced darks and flats will use them instead of making the copy) will copy reduced darks / flats to `dataset_{darks|flats}.hdf5` to be used for reconstruction step1: provide reduced darks and flats from the 'copy' tab, unselecting auto and setting them through `select darks url` and `select flats url` result: scans with copied reduced darks / flats Context: on this workflow we want to copy reduced flats / darks from a dataset to others. This one of the various way to do this. Note: reduced darks / flats can be saved to disk from another python script using the `silx.io.dictdump` dicttoh5 function. Reduced darks and flats are dictionary with index in the sequence as key (so index==0 if the dark / flat serie is done at the beginning) and the reduced darks / flats as value (2D numpy array) {'_scanIDs': [], 'controlAreaVisible': True, 'savedWidgetGeometry': None, '__version__': 1} gASVLwEAAAAAAAB9lCiMFV9ld29rc19kZWZhdWx0X2lucHV0c5R9lIwKb3BlcmF0aW9uc5R9lCiM G3RvbW9zY2FuLmVzcmYuc2Nhbi5oZGY1c2NhbpSMCEltYWdlS2V5lJOUSwKFlFKUaAdLA4WUUpRo B0sBhZRSlGgLdXOMD19ld29rc19leGVjaW5mb5R9lIwOX2V3b2tzX3ZhcmluZm+UfZSMEmNvbnRy b2xBcmVhVmlzaWJsZZSIjA5kZWZhdWx0X2lucHV0c5R9lIwTc2F2ZWRXaWRnZXRHZW9tZXRyeZRD QgHZ0MsAAwAAAAAH5gAAARgAAAovAAADLQAAB+sAAAE1AAAKKgAAAygAAAABAAAAAAd/AAAH6wAA ATUAAAoqAAADKJSMC19fdmVyc2lvbl9flEsBdS4= {'_ewoks_default_inputs': {'data': None, 'dark_ref_params': {'DOWHEN': 'before', 'DARKCAL': 'mean', 'DARKOVE': 0, 'DARKRMV': 0, 'DKFILE': 'darkend[0-9]{3,4}', 'REFSCAL': 'median', 'REFSOVE': 0, 'REFSRMV': 0, 'RFFILE': 'ref*.*[0-9]{3,4}_[0-9]{3,4}'}}, 'controlAreaVisible': True, 'savedWidgetGeometry': b'\x01\xd9\xd0\xcb\x00\x03\x00\x00\x00\x00\x06\x9b\x00\x00\x02K\x00\x00\n\x95\x00\x00\x04\x8f\x00\x00\x06\xa0\x00\x00\x02h\x00\x00\n\x90\x00\x00\x04\x8a\x00\x00\x00\x01\x00\x00\x00\x00\x07\x7f\x00\x00\x06\xa0\x00\x00\x02h\x00\x00\n\x90\x00\x00\x04\x8a', '__version__': 1} {'_scanIDs': [], 'controlAreaVisible': True, 'savedWidgetGeometry': None, '__version__': 1} ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739271825.0 tomwer-1.4.8/src/orangecontrib/tomwer/tutorials/default_cor_search.ows0000644000175000017500000001113314752627221025502 0ustar00paynopayno provide dataset compute reduced darks and flats compute center of rotation manually or using one of the numerous existing algorithms reconstruct one or several slices browse reconstructed slices and browse dataset {'_scanIDs': [], 'controlAreaVisible': True, 'savedWidgetGeometry': None, '__version__': 1} {'_ewoks_default_inputs': {'data': None, 'dark_ref_params': None}, 'controlAreaVisible': True, 'savedWidgetGeometry': None, '__version__': 1} {'_ewoks_default_inputs': {'data': None, 'axis_params': {'MODE': 'manual', 'POSITION_VALUE': None, 'CALC_INPUT_TYPE': 'transmission_nopag', 'ANGLE_MODE': '0-180', 'USE_SINOGRAM': False, 'SINOGRAM_LINE': '', 'SINOGRAM_SUBSAMPLING': 10, 'AXIS_URL_1': '', 'AXIS_URL_2': '', 'LOOK_AT_STDMAX': False, 'NEAR_WX': 5, 'FINE_STEP_X': 0.1, 'SCALE_IMG2_TO_IMG1': False, 'NEAR_POSITION': 0.0, 'PADDING_MODE': 'edge', 'FLIP_LR': True, 'COMPOSITE_OPTS': {'theta': 10, 'n_subsampling_y': 10, 'oversampling': 4, 'take_log': True, 'near_pos': 0, 'near_width': 20}, 'SIDE': 'all', 'COR_OPTIONS': ''}, 'gui': {'mode_is_lock': False, 'value_is_lock': False, 'auto_update_estimated_cor': True}}, 'controlAreaVisible': True, 'savedWidgetGeometry': None, '__version__': 1} {'_ewoks_default_inputs': {'data': None, 'nabu_params': None}, 'controlAreaVisible': True, 'savedWidgetGeometry': None, '__version__': 1} {'_viewer_config': {}, 'controlAreaVisible': True, 'savedWidgetGeometry': None, '__version__': 1} ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739271825.0 tomwer-1.4.8/src/orangecontrib/tomwer/tutorials/drac_publication.ows0000644000175000017500000001376514752627221025205 0ustar00paynopayno provide dataset publish reconstructed volume to icat reconstruct the volume {'_scanIDs': [], 'controlAreaVisible': True, 'savedWidgetGeometry': None, '__version__': 1} {'_ewoks_default_inputs': {'data': None, 'dark_ref_params': None}, 'controlAreaVisible': True, 'savedWidgetGeometry': None, '__version__': 1} {'_ewoks_default_inputs': {'data': None, 'axis_params': {'MODE': 'manual', 'POSITION_VALUE': None, 'CALC_INPUT_TYPE': 'transmission_nopag', 'ANGLE_MODE': '0-180', 'USE_SINOGRAM': False, 'SINOGRAM_LINE': '', 'SINOGRAM_SUBSAMPLING': 10, 'AXIS_URL_1': '', 'AXIS_URL_2': '', 'LOOK_AT_STDMAX': False, 'NEAR_WX': 5, 'FINE_STEP_X': 0.1, 'SCALE_IMG2_TO_IMG1': False, 'NEAR_POSITION': 0.0, 'PADDING_MODE': 'edge', 'FLIP_LR': True, 'COMPOSITE_OPTS': {'theta': 10, 'n_subsampling_y': 10, 'oversampling': 4, 'take_log': True, 'near_pos': 0, 'near_width': 20}, 'SIDE': 'all', 'COR_OPTIONS': ''}, 'gui': {'mode_is_lock': False, 'value_is_lock': False, 'auto_update_estimated_cor': True}}, 'controlAreaVisible': True, 'savedWidgetGeometry': None, '__version__': 1} {'_ewoks_default_inputs': {'data': None, 'nabu_params': None}, 'controlAreaVisible': True, 'savedWidgetGeometry': None, '__version__': 1} {'_viewer_config': {}, 'controlAreaVisible': True, 'savedWidgetGeometry': None, '__version__': 1} {'_ewoks_default_inputs': {'data': None, 'nabu_volume_params': None, 'nabu_params': None}, 'controlAreaVisible': True, 'savedWidgetGeometry': None, '__version__': 1} {'controlAreaVisible': True, 'savedWidgetGeometry': None, '__version__': 1} {'_ewoks_default_inputs': {}, '_ewoks_execinfo': {}, '_ewoks_varinfo': {}, 'controlAreaVisible': True, 'default_inputs': {}, 'savedWidgetGeometry': None, '__version__': 1} ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1731088693.0 tomwer-1.4.8/src/orangecontrib/tomwer/tutorials/hello_world_python_script.ows0000644000175000017500000000712614713450465027175 0ustar00paynopayno 1: provide a scan either by: * using the 'select button' * copy-paste a file containing a NXtomo to the interface 2: print some information about this scan {'_tomo_obj_setting': '', 'controlAreaVisible': True, 'savedWidgetGeometry': b'\x01\xd9\xd0\xcb\x00\x03\x00\x00\x00\x00\x08C\x00\x00\x02=\x00\x00\r\xb3\x00\x00\x02\xd8\x00\x00\x08C\x00\x00\x02b\x00\x00\r\xb3\x00\x00\x02\xd8\x00\x00\x00\x01\x00\x00\x00\x00\x07\x80\x00\x00\x08C\x00\x00\x02b\x00\x00\r\xb3\x00\x00\x02\xd8', '__version__': 1} gASVKQUAAAAAAAB9lCiMEmNvbnRyb2xBcmVhVmlzaWJsZZSIjBJjdXJyZW50U2NyaXB0SW5kZXiU SwCMEWxpYnJhcnlMaXN0U291cmNllF2UjDFvcmFuZ2Vjb250cmliLnRvbXdlci53aWRnZXRzLm90 aGVyLlB5dGhvblNjcmlwdE9XlIwGU2NyaXB0lJOUKYGUfZQojARuYW1llIwLSGVsbG8gd29ybGSU jAZzY3JpcHSUWOgBAABmcm9tIHRvbXdlci5jb3JlLnNjYW4uaGRmNXNjYW4gaW1wb3J0IEhERjVU b21vU2Nhbgpmcm9tIHRvbXdlci5jb3JlLnNjYW4uZWRmc2NhbiBpbXBvcnQgRURGVG9tb1NjYW4K c2NhbiA9IGluX2RhdGEKaWYgaXNpbnN0YW5jZShzY2FuLCBIREY1VG9tb1NjYW4pOgogICAgcHJp bnQoInNjYW4gbWFzdGVyIGZpbGUgaXMiLCBzY2FuLm1hc3Rlcl9maWxlKQplbHNlOgogICAgcHJp bnQoInNjYW4gZGlyZWN0b3J5IGlzIiwgc2Nhbi5wYXRoKQoKcHJpbnQoZiJzY2FuIGhhcyB7bGVu KHNjYW4ucHJvamVjdGlvbnMpfSBwcm9qZWN0aW9ucywge2xlbihzY2FuLmRhcmtzKX0gZGFya3Mg YW5kIHtsZW4oc2Nhbi5mbGF0cyl9IGZsYXRzIikKCnByaW50KGYic2NhbiBlbmVyZ3kgaXMge3Nj YW4uZW5lcmd5fSwgcm90YXRpb24gYW5nbGUgZ29lcyBmcm9tIHttaW4oc2Nhbi5yb3RhdGlvbl9h bmdsZSl9IHRvIHttYXgoc2Nhbi5yb3RhdGlvbl9hbmdsZSl9IGRlZ3JlZXMiKZSMBWZsYWdzlEsA jAhmaWxlbmFtZZROdWJhjBNzYXZlZFdpZGdldEdlb21ldHJ5lENCAdnQywADAAAAAAhKAAAA0wAA DWYAAARRAAAISgAAAPgAAA1mAAAEUQAAAAEAAAAAB4AAAAhKAAAA+AAADWYAAARRlIwKc2NyaXB0 VGV4dJRY6AEAAGZyb20gdG9td2VyLmNvcmUuc2Nhbi5oZGY1c2NhbiBpbXBvcnQgSERGNVRvbW9T Y2FuCmZyb20gdG9td2VyLmNvcmUuc2Nhbi5lZGZzY2FuIGltcG9ydCBFREZUb21vU2NhbgpzY2Fu ID0gaW5fZGF0YQppZiBpc2luc3RhbmNlKHNjYW4sIEhERjVUb21vU2Nhbik6CiAgICBwcmludCgi c2NhbiBtYXN0ZXIgZmlsZSBpcyIsIHNjYW4ubWFzdGVyX2ZpbGUpCmVsc2U6CiAgICBwcmludCgi c2NhbiBkaXJlY3RvcnkgaXMiLCBzY2FuLnBhdGgpCgpwcmludChmInNjYW4gaGFzIHtsZW4oc2Nh bi5wcm9qZWN0aW9ucyl9IHByb2plY3Rpb25zLCB7bGVuKHNjYW4uZGFya3MpfSBkYXJrcyBhbmQg e2xlbihzY2FuLmZsYXRzKX0gZmxhdHMiKQoKcHJpbnQoZiJzY2FuIGVuZXJneSBpcyB7c2Nhbi5l bmVyZ3l9LCByb3RhdGlvbiBhbmdsZSBnb2VzIGZyb20ge21pbihzY2FuLnJvdGF0aW9uX2FuZ2xl KX0gdG8ge21heChzY2FuLnJvdGF0aW9uX2FuZ2xlKX0gZGVncmVlcyIplIwNc3BsaXR0ZXJTdGF0 ZZROjAtfX3ZlcnNpb25fX5RLAXUu ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1739271865.8717635 tomwer-1.4.8/src/orangecontrib/tomwer/tutorials/id16b/0000755000175000017500000000000014752627272022050 5ustar00paynopayno././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739271825.0 tomwer-1.4.8/src/orangecontrib/tomwer/tutorials/id16b/ID16b_full_volume.ows0000644000175000017500000000327714752627221026023 0ustar00paynopayno {'_scanIDs': [], 'controlAreaVisible': True, 'savedWidgetGeometry': None, '__version__': 1} {'_rpSetting': {}, 'controlAreaVisible': True, 'savedWidgetGeometry': None, 'static_input': {'data': None, 'nabu_params': None}, '__version__': 1} {'_rpSetting': {}, 'controlAreaVisible': True, 'savedWidgetGeometry': None, 'static_input': {'data': None, 'nabu_volume_params': None, 'nabu_params': None}, '__version__': 1} ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739271825.0 tomwer-1.4.8/src/orangecontrib/tomwer/tutorials/id16b/ID16b_insitu.ows0000644000175000017500000007157614752627221025014 0ustar00paynopayno {'_muted': False, 'controlAreaVisible': False, 'savedWidgetGeometry': b'\x01\xd9\xd0\xcb\x00\x03\x00\x00\x00\x00\x04n\x00\x00\x01\xfe\x00\x00\x05\xc1\x00\x00\x03-\x00\x00\x04n\x00\x00\x02#\x00\x00\x05\xc1\x00\x00\x03-\x00\x00\x00\x00\x00\x00\x00\x00\n\x00\x00\x00\x04n\x00\x00\x02#\x00\x00\x05\xc1\x00\x00\x03-', '__version__': 1} {'_ewoks_default_inputs': {'data': None, 'nabu_params': {'preproc': {'flatfield': 1, 'double_flatfield_enabled': 0, 'dff_sigma': 0.0, 'ccd_filter_enabled': 0, 'ccd_filter_threshold': 0.04, 'take_logarithm': True, 'log_min_clip': 1e-06, 'log_max_clip': 10.0, 'sino_rings_correction': 'None', 'sino_rings_options': 'sigma=1.0 ; levels=10 ; padding=False', 'tilt_correction': '', 'autotilt_options': '', 'normalize_srcurrent': 1}, 'reconstruction': {'method': 'FBP', 'slice_plane': 'XY', 'angles_file': '', 'axis_correction_file': '', 'angle_offset': 0.0, 'fbp_filter_type': 'ramlak', 'padding_type': 'edges', 'iterations': 200, 'optim_algorithm': 'chambolle-pock', 'weight_tv': 0.01, 'preconditioning_filter': 1, 'positivity_constraint': 1, 'rotation_axis_position': '', 'translation_movements_file': '', 'clip_outer_circle': 1, 'centered_axis': 1, 'start_x': 0, 'end_x': -1, 'start_y': 0, 'end_y': -1, 'start_z': 0, 'end_z': -1, 'enable_halftomo': 0}, 'dataset': {'binning': 1, 'binning_z': 1, 'projections_subsampling': 1}, 'tomwer_slices': 'middle', 'output': {'file_format': 'vol', 'location': '', 'output_dir_mode': 'same folder as scan'}, 'phase': {'method': 'Paganin', 'delta_beta': '180', 'padding_type': 'edge', 'unsharp_coeff': 3.0, 'unsharp_sigma': 0.8, 'ctf_geometry': ' z1_v=None; z1_h=None; detec_pixel_size=None; magnification=True', 'beam_shape': 'parallel', 'ctf_advanced_params': ' length_scale=1e-05; lim1=1e-05; lim2=0.2; normalize_by_mean=True', 'ctf_translations_file': ''}, 'configuration_level': 'optional', 'mode_locked': True, 'cluster_config': None}}, 'controlAreaVisible': True, 'savedWidgetGeometry': b'\x01\xd9\xd0\xcb\x00\x03\x00\x00\x00\x00\x02\xaa\x00\x00\x00\xb4\x00\x00\x083\x00\x00\x04\x12\x00\x00\x02\xaa\x00\x00\x00\xb4\x00\x00\x083\x00\x00\x04\x12\x00\x00\x00\x00\x00\x00\x00\x00\n\x00\x00\x00\x02\xaa\x00\x00\x00\xb4\x00\x00\x083\x00\x00\x04\x12', '__version__': 1} gASVPgEAAAAAAAB9lCiMDl92aWV3ZXJfY29uZmlnlH2UKIwEbW9kZZSMI3RvbXdlci5ndWkudmlz dWFsaXphdGlvbi5kYXRhdmlld2VylIwMX0Rpc3BsYXlNb2RllJOUjAZzbGljZXOUhZRSlIwJc2xp Y2Vfb3B0lGgEjApfU2xpY2VNb2RllJOUjAZsYXRlc3SUhZRSlIwJcmFkaW9fb3B0lGgEjApfUmFk aW9Nb2RllJOUjApub3JtYWxpemVklIWUUpR1jBJjb250cm9sQXJlYVZpc2libGWUiIwTc2F2ZWRX aWRnZXRHZW9tZXRyeZRDQgHZ0MsAAwAAAAAASAAAAIIAAAeRAAAEpAAAAEgAAACnAAAHkQAABKQA AAAAAAAAAAoAAAAASAAAAKcAAAeRAAAEpJSMC19fdmVyc2lvbl9flEsBdS4= {'_ewoks_default_inputs': {'data': None, 'axis_params': {'MODE': 'centered', 'POSITION_VALUE': -27.7489905564438, 'CALC_INPUT_TYPE': 'transmission_nopag', 'ANGLE_MODE': '0-180', 'USE_SINOGRAM': False, 'SINOGRAM_LINE': '', 'SINOGRAM_SUBSAMPLING': 10, 'AXIS_URL_1': '', 'AXIS_URL_2': '', 'LOOK_AT_STDMAX': False, 'NEAR_WX': 5, 'FINE_STEP_X': 0.1, 'SCALE_IMG2_TO_IMG1': False, 'NEAR_POSITION': 0.0, 'PADDING_MODE': 'edge', 'FLIP_LR': True, 'COMPOSITE_OPTS': {'theta': 10, 'n_subsampling_y': 10, 'oversampling': 4, 'take_log': True, 'near_pos': 0, 'near_width': 20}, 'SIDE': 'center', 'COR_OPTIONS': ''}, 'gui': {'mode_is_lock': True, 'value_is_lock': False, 'auto_update_estimated_cor': False}}, 'controlAreaVisible': True, 'savedWidgetGeometry': b'\x01\xd9\xd0\xcb\x00\x03\x00\x00\x00\x00\x02\xda\x00\x00\x00Y\x00\x00\x08\xac\x00\x00\x03U\x00\x00\x02\xda\x00\x00\x00~\x00\x00\x08\xac\x00\x00\x03U\x00\x00\x00\x00\x00\x00\x00\x00\n\x00\x00\x00\x02\xda\x00\x00\x00~\x00\x00\x08\xac\x00\x00\x03U', '__version__': 1} gASVwwUAAAAAAAB9lCiMEmNvbnRyb2xBcmVhVmlzaWJsZZSIjBJjdXJyZW50U2NyaXB0SW5kZXiU SwCMEWxpYnJhcnlMaXN0U291cmNllF2UjDFvcmFuZ2Vjb250cmliLnRvbXdlci53aWRnZXRzLm90 aGVyLlB5dGhvblNjcmlwdE9XlIwGU2NyaXB0lJOUKYGUfZQojARuYW1llIwLSGVsbG8gd29ybGSU jAZzY3JpcHSUWDUCAABpbXBvcnQgb3MKc2NhbiA9IGluX2RhdGEKCnZvbF9mbG9hdF9wYXRoID0g b3MucGF0aC5qb2luKAogICAgb3MucGF0aC5kaXJuYW1lKHNjYW4ucGF0aCksCiAgICAiLnZvbGZs b2F0X25vYmFja3VwIiwKKQoKdm9sX2Zsb2F0X3N5bV9saW5rX3BhdGggPSBvcy5wYXRoLmpvaW4o CiAgICBvcy5wYXRoLmRpcm5hbWUoc2Nhbi5wYXRoKSwKICAgICJ2b2xmbG9hdCIsCikKCmlmIG5v dCBvcy5wYXRoLmV4aXN0cyh2b2xfZmxvYXRfcGF0aCk6CiAgICBwcmludChmImNyZWF0ZXMge3Zv bF9mbG9hdF9wYXRofSIpCiAgICBvcy5tYWtlZGlycyh2b2xfZmxvYXRfcGF0aCwgbW9kZT0wbzc3 NCkKICAgICMgZG9uJ3Qga25vdyB3aHkgYnV0IGZpcnN0IGNyZWF0aW9uIHdpdGggMG83NzQgZmFp bHMuLi4KICAgIG9zLmNobW9kKHZvbF9mbG9hdF9wYXRoLCAwbzc3NCkKICAgIAogICAgIyBjcmVh dGUgc3ltbGluayBmcm9tIHZvbGZsb2F0IHRvICIudm9sZmxvYXRfbm9iYWNrdXAiCiAgICBvcy5z eW1saW5rKHZvbF9mbG9hdF9wYXRoLCB2b2xfZmxvYXRfc3ltX2xpbmtfcGF0aCkKCgpvdXRfZGF0 YSA9IHNjYW4KlIwFZmxhZ3OUSwCMCGZpbGVuYW1llE51YmGME3NhdmVkV2lkZ2V0R2VvbWV0cnmU Q0IB2dDLAAMAAAAAAEgAAABAAAAGBwAAA3YAAABIAAAAQAAABgcAAAN2AAAAAAAAAAAKAAAAAEgA AABAAAAGBwAAA3aUjApzY3JpcHRUZXh0lFg1AgAAaW1wb3J0IG9zCnNjYW4gPSBpbl9kYXRhCgp2 b2xfZmxvYXRfcGF0aCA9IG9zLnBhdGguam9pbigKICAgIG9zLnBhdGguZGlybmFtZShzY2FuLnBh dGgpLAogICAgIi52b2xmbG9hdF9ub2JhY2t1cCIsCikKCnZvbF9mbG9hdF9zeW1fbGlua19wYXRo ID0gb3MucGF0aC5qb2luKAogICAgb3MucGF0aC5kaXJuYW1lKHNjYW4ucGF0aCksCiAgICAidm9s ZmxvYXQiLAopCgppZiBub3Qgb3MucGF0aC5leGlzdHModm9sX2Zsb2F0X3BhdGgpOgogICAgcHJp bnQoZiJjcmVhdGVzIHt2b2xfZmxvYXRfcGF0aH0iKQogICAgb3MubWFrZWRpcnModm9sX2Zsb2F0 X3BhdGgsIG1vZGU9MG83NzQpCiAgICAjIGRvbid0IGtub3cgd2h5IGJ1dCBmaXJzdCBjcmVhdGlv biB3aXRoIDBvNzc0IGZhaWxzLi4uCiAgICBvcy5jaG1vZCh2b2xfZmxvYXRfcGF0aCwgMG83NzQp CiAgICAKICAgICMgY3JlYXRlIHN5bWxpbmsgZnJvbSB2b2xmbG9hdCB0byAiLnZvbGZsb2F0X25v YmFja3VwIgogICAgb3Muc3ltbGluayh2b2xfZmxvYXRfcGF0aCwgdm9sX2Zsb2F0X3N5bV9saW5r X3BhdGgpCgoKb3V0X2RhdGEgPSBzY2FuCpSMDXNwbGl0dGVyU3RhdGWUTowLX192ZXJzaW9uX1+U SwF1Lg== gASVKQ8AAAAAAAB9lCiMEmNvbnRyb2xBcmVhVmlzaWJsZZSIjBJjdXJyZW50U2NyaXB0SW5kZXiU SwCMEWxpYnJhcnlMaXN0U291cmNllF2UjDFvcmFuZ2Vjb250cmliLnRvbXdlci53aWRnZXRzLm90 aGVyLlB5dGhvblNjcmlwdE9XlIwGU2NyaXB0lJOUKYGUfZQojARuYW1llIwLSGVsbG8gd29ybGSU jAZzY3JpcHSUWOgGAABpbXBvcnQgc2h1dGlsCmltcG9ydCBvcwpmcm9tIHRvbXdlci5jb3JlLnNj YW4uZWRmc2NhbiBpbXBvcnQgRURGVG9tb1NjYW4KZnJvbSBzaWx4LmlvLmRpY3RkdW1wIGltcG9y dCBkaWN0dG9pbmksIGxvYWQgYXMgbG9hZF9pbmkKCmRlZiBnZXRfa2V5KGxpbmUpOgogICAgIiIi InJldHVybiBrZXkgbmFtZSBvZiBhIC5pbmZvIGZpbGUiIiIKICAgIHJldHVybiBsaW5lLnNwbGl0 KCI9IilbMF0ucmVwbGFjZSgiICIsICIiKQoKCmRlZiBnZXRfcmF3X2RpY3QoZmlsZV9wYXRoKToK ICAgICIiInJldHVybiB0aGUgaW5mbyBmaWxlIG1ldGFkYXRhIGFzIGEgZGljdGlvbmFyeSB3aXRo IG1ldGFkYXRhIG5hbWUgYXMga2V5IGFuZCByYXcgbGluZSBhcyB2YWx1ZSIiIgogICAgbWV0YWRh dGEgPSB7fQogICAgd2l0aCBvcGVuKGZpbGVfcGF0aCwgbW9kZT0iciIpIGFzIGY6CiAgICAgICAg bGluZSA9IGYucmVhZGxpbmUoKQogICAgICAgIHdoaWxlIGxpbmU6CiAgICAgICAgICAgIGtleSA9 IGdldF9rZXkobGluZSkKICAgICAgICAgICAgbWV0YWRhdGFba2V5XSA9IGxpbmUKICAgICAgICAg ICAgbGluZSA9IGYucmVhZGxpbmUoKQogICAgcmV0dXJuIG1ldGFkYXRhCgpzY2FuID0gaW5fZGF0 YQpwcmludCgidHJlYXQiLCBpbl9kYXRhKQoKaWYgaXNpbnN0YW5jZShzY2FuLCBFREZUb21vU2Nh bik6CiAgICAjIGNvcHkgdGhlIC5pbmZvIGZpbGUKICAgIGluZm9fZmlsZSA9IHNjYW4uZ2V0X2lu Zm9fZmlsZShzY2FuLnBhdGgpCiAgICBwcmludChmIlxuXG5UcnlpbmcgdG8gcmVhZCBpbmZvX2Zp bGU6IHtpbmZvX2ZpbGV9IikKICAgICNpbmZvX2ZpbGUgPSBpbmZvX2ZpbGVbNzpdCiAgICBwcmlu dChmIlxuRm9yY2UgdG8gcmVhZCAqdGhpcyogaW5mb19maWxlOiB7aW5mb19maWxlfSIpCiAgICBw cmludChmIlxuSW5pdGlhbCBkaXN0YW5jZSBpbiBpbnB1dCBmaWxlOiBcdHtnZXRfcmF3X2RpY3Qo aW5mb19maWxlKVsnRGlzdGFuY2UnXX0iKQogICAgcmF3X2luZm9fZmlsZSA9IGluZm9fZmlsZVs6 LTVdICsgIl9yYXcuaW5mbyIKICAgIGlmIG5vdCBvcy5wYXRoLmV4aXN0cyhyYXdfaW5mb19maWxl KToKICAgICAgICAjIGNvcHkgdGhlIG9yaWdpbmFsIGZpbGUgb25seSB0aGUgZmlyc3QgdGltZS4g T3RoZXJ3aXNlIHdlIHdpbGwgb3ZlcndyaXRlIHdpdGggYW4gdXBkYXRlZCBmaWxlLgogICAgICAg IHNodXRpbC5jb3B5ZmlsZSgKICAgICAgICAgICAgaW5mb19maWxlLAogICAgICAgICAgICByYXdf aW5mb19maWxlLAogICAgICAgICkKICAgIG1ldGFkYXRhID0gZ2V0X3Jhd19kaWN0KGluZm9fZmls ZSkKICAgICMgb3ZlcndyaXRlIGRpc3RhbmNlIGtleQogICAgbmV3X2Rpc3RhbmNlID0gMC4xMzIx MzY5NyoxMDAwICMgMjgwbm0gcGNvMQogICAgI25ld19kaXN0YW5jZSA9IDAuMTM5OSAqMTAwMCAj IDI4MG5tIHBjbzIKICAgICNuZXdfZGlzdGFuY2UgPSAwLjEyODczNjkzICoxMDAwICMgMjVubSBw Y28xCiAgICAjbmV3X2Rpc3RhbmNlID0gIDAuMDUwMDA2MDUgKiAxMDAwICMgNTBubQogICAgbWV0 YWRhdGFbIkRpc3RhbmNlIl0gPSAiRGlzdGFuY2U9IFx0XHQiICsgc3RyKG5ld19kaXN0YW5jZSkg KyAiXG4iCiAgICB3aXRoIG9wZW4oaW5mb19maWxlLCBtb2RlPSJ3IikgYXMgZjoKICAgICAgICBm b3IgXywgbGluZSBpbiBtZXRhZGF0YS5pdGVtcygpOgogICAgICAgICAgICBmLndyaXRlKGxpbmUp CiAgICAjIHJlc2V0IGtleSBmb3IgY29oZXJlbmNlCiAgICBzY2FuLl9kaXN0YW5jZSA9IE5vbmUK b3V0X2RhdGEgPSBzY2FulIwFZmxhZ3OUSwCMCGZpbGVuYW1llE51YmGME3NhdmVkV2lkZ2V0R2Vv bWV0cnmUQ0IB2dDLAAMAAAAAAfoAAAJ5AAAHgQAABpsAAAH6AAACngAAB4EAAAabAAAAAAAAAAAK AAAAAfoAAAKeAAAHgQAABpuUjApzY3JpcHRUZXh0lFjoBgAAaW1wb3J0IHNodXRpbAppbXBvcnQg b3MKZnJvbSB0b213ZXIuY29yZS5zY2FuLmVkZnNjYW4gaW1wb3J0IEVERlRvbW9TY2FuCmZyb20g c2lseC5pby5kaWN0ZHVtcCBpbXBvcnQgZGljdHRvaW5pLCBsb2FkIGFzIGxvYWRfaW5pCgpkZWYg Z2V0X2tleShsaW5lKToKICAgICIiIiJyZXR1cm4ga2V5IG5hbWUgb2YgYSAuaW5mbyBmaWxlIiIi CiAgICByZXR1cm4gbGluZS5zcGxpdCgiPSIpWzBdLnJlcGxhY2UoIiAiLCAiIikKCgpkZWYgZ2V0 X3Jhd19kaWN0KGZpbGVfcGF0aCk6CiAgICAiIiJyZXR1cm4gdGhlIGluZm8gZmlsZSBtZXRhZGF0 YSBhcyBhIGRpY3Rpb25hcnkgd2l0aCBtZXRhZGF0YSBuYW1lIGFzIGtleSBhbmQgcmF3IGxpbmUg YXMgdmFsdWUiIiIKICAgIG1ldGFkYXRhID0ge30KICAgIHdpdGggb3BlbihmaWxlX3BhdGgsIG1v ZGU9InIiKSBhcyBmOgogICAgICAgIGxpbmUgPSBmLnJlYWRsaW5lKCkKICAgICAgICB3aGlsZSBs aW5lOgogICAgICAgICAgICBrZXkgPSBnZXRfa2V5KGxpbmUpCiAgICAgICAgICAgIG1ldGFkYXRh W2tleV0gPSBsaW5lCiAgICAgICAgICAgIGxpbmUgPSBmLnJlYWRsaW5lKCkKICAgIHJldHVybiBt ZXRhZGF0YQoKc2NhbiA9IGluX2RhdGEKcHJpbnQoInRyZWF0IiwgaW5fZGF0YSkKCmlmIGlzaW5z dGFuY2Uoc2NhbiwgRURGVG9tb1NjYW4pOgogICAgIyBjb3B5IHRoZSAuaW5mbyBmaWxlCiAgICBp bmZvX2ZpbGUgPSBzY2FuLmdldF9pbmZvX2ZpbGUoc2Nhbi5wYXRoKQogICAgcHJpbnQoZiJcblxu VHJ5aW5nIHRvIHJlYWQgaW5mb19maWxlOiB7aW5mb19maWxlfSIpCiAgICAjaW5mb19maWxlID0g aW5mb19maWxlWzc6XQogICAgcHJpbnQoZiJcbkZvcmNlIHRvIHJlYWQgKnRoaXMqIGluZm9fZmls ZToge2luZm9fZmlsZX0iKQogICAgcHJpbnQoZiJcbkluaXRpYWwgZGlzdGFuY2UgaW4gaW5wdXQg ZmlsZTogXHR7Z2V0X3Jhd19kaWN0KGluZm9fZmlsZSlbJ0Rpc3RhbmNlJ119IikKICAgIHJhd19p bmZvX2ZpbGUgPSBpbmZvX2ZpbGVbOi01XSArICJfcmF3LmluZm8iCiAgICBpZiBub3Qgb3MucGF0 aC5leGlzdHMocmF3X2luZm9fZmlsZSk6CiAgICAgICAgIyBjb3B5IHRoZSBvcmlnaW5hbCBmaWxl IG9ubHkgdGhlIGZpcnN0IHRpbWUuIE90aGVyd2lzZSB3ZSB3aWxsIG92ZXJ3cml0ZSB3aXRoIGFu IHVwZGF0ZWQgZmlsZS4KICAgICAgICBzaHV0aWwuY29weWZpbGUoCiAgICAgICAgICAgIGluZm9f ZmlsZSwKICAgICAgICAgICAgcmF3X2luZm9fZmlsZSwKICAgICAgICApCiAgICBtZXRhZGF0YSA9 IGdldF9yYXdfZGljdChpbmZvX2ZpbGUpCiAgICAjIG92ZXJ3cml0ZSBkaXN0YW5jZSBrZXkKICAg IG5ld19kaXN0YW5jZSA9IDAuMTMyMTM2OTcqMTAwMCAjIDI4MG5tIHBjbzEKICAgICNuZXdfZGlz dGFuY2UgPSAwLjEzOTkgKjEwMDAgIyAyODBubSBwY28yCiAgICAjbmV3X2Rpc3RhbmNlID0gMC4x Mjg3MzY5MyAqMTAwMCAjIDI1bm0gcGNvMQogICAgI25ld19kaXN0YW5jZSA9ICAwLjA1MDAwNjA1 ICogMTAwMCAjIDUwbm0KICAgIG1ldGFkYXRhWyJEaXN0YW5jZSJdID0gIkRpc3RhbmNlPSBcdFx0 IiArIHN0cihuZXdfZGlzdGFuY2UpICsgIlxuIgogICAgd2l0aCBvcGVuKGluZm9fZmlsZSwgbW9k ZT0idyIpIGFzIGY6CiAgICAgICAgZm9yIF8sIGxpbmUgaW4gbWV0YWRhdGEuaXRlbXMoKToKICAg ICAgICAgICAgZi53cml0ZShsaW5lKQogICAgIyByZXNldCBrZXkgZm9yIGNvaGVyZW5jZQogICAg c2Nhbi5fZGlzdGFuY2UgPSBOb25lCm91dF9kYXRhID0gc2NhbpSMDXNwbGl0dGVyU3RhdGWUTowL X192ZXJzaW9uX1+USwF1Lg== gASVyw4AAAAAAAB9lCiMEmNvbnRyb2xBcmVhVmlzaWJsZZSIjBJjdXJyZW50U2NyaXB0SW5kZXiU SwCMEWxpYnJhcnlMaXN0U291cmNllF2UjDFvcmFuZ2Vjb250cmliLnRvbXdlci53aWRnZXRzLm90 aGVyLlB5dGhvblNjcmlwdE9XlIwGU2NyaXB0lJOUKYGUfZQojARuYW1llIwLSGVsbG8gd29ybGSU jAZzY3JpcHSUWLkGAABpbXBvcnQgb3MKaW1wb3J0IG51bXB5CmZyb20gdG9tb3NjYW4uZmFjdG9y eSBpbXBvcnQgRmFjdG9yeQpmcm9tIG5hYnUubWlzYy51dGlscyBpbXBvcnQgcmVzY2FsZV9kYXRh CgppZiBpbl9kYXRhIGlzIG5vdCBOb25lOgogICAgc2NhbiA9IGluX2RhdGEKICAgIAogICAgIyB3 YXJuaW5nOiBzY2FuLmxhdGVzdF9yZWNvbnN0cnVjdGlvbnMgaXMgZGVmaW5lZCBieSB0aGUgJ25h YnUgc2xpY2UgcmVjb25zdHJ1Y3Rpb24nLiBTbyBpZiBhIHNjYW4gaXMgbG9hZGVkIGFuZCBub3Qg Z28gdHJob3VnaCB0aGlzIHdpZGdldAogICAgIyBoZSB3b24ndCBiZSBhd2FyZSBhYm91dCBzbGlj ZSByZWNvbnN0cnVjdGlvbnMKICAgIHByaW50KCJjaGVjayIsIHNjYW4sICIgLSBsYXRlc3RfcmVj b25zdHJ1Y3Rpb25zIiwgc2Nhbi5sYXRlc3RfcmVjb25zdHJ1Y3Rpb25zKQogICAgaW5fdm9sdW1l cyA9IFsKICAgICAgICBGYWN0b3J5LmNyZWF0ZV90b21vX29iamVjdF9mcm9tX2lkZW50aWZpZXIo aWRlbnRpZmllcikgZm9yIGlkZW50aWZpZXIgaW4gc2Nhbi5sYXRlc3RfcmVjb25zdHJ1Y3Rpb25z CiAgICBdCgogICAgIyBsb29rcyBsaWtlIHRoZXJlIGlzIHNvbWUgZXh0cmEgdm9sdW1lIGV4aXN0 aW5nOiBmaWx0ZXIgdGhlbQogICAgcmVzID0gW10KICAgIGZvciB2b2wgaW4gaW5fdm9sdW1lczoK ICAgICAgICBpZiBvcy5wYXRoLmV4aXN0cyh2b2wuZmlsZV9wYXRoKToKICAgICAgICAgICAgcmVz LmFwcGVuZCh2b2wpCiAgICAKICAgIGluX3ZvbHVtZXMgPSByZXMKICAgIHByaW50KCJzbGljZXMg dG8gYmUgaGFuZGxlZCIsIGluX3ZvbHVtZXMpCgogICAgW3ByaW50KHZvbC5nZXRfaWRlbnRpZmll cigpLnRvX3N0cigpKSBmb3Igdm9sIGluIGluX3ZvbHVtZXNdCiAgICAKICAgICMgcmV0cmlldmUg dGhlIG1pbiAvIG1heCBmcm9tIHRoZSBmaXJzdCBhY3F1aXNpdGlvbgogICAgaWYgc2Nhbi5wYXRo LmVuZHN3aXRoKCJfMV8iKToKICAgICAgICBmb3Igdm9sIGluIGluX3ZvbHVtZXM6CiAgICAgICAg ICAgIHByaW50KCJyZWNvbXB1dGUgbWluIGFuZCBtYXggZnJvbSIsIHZvbC5maWxlX3BhdGgpCiAg ICAgICAgICAgIGRhdGEgPSB2b2wubG9hZF9kYXRhKCkKICAgICAgICAgICAgbmV3X21pbiwgbmV3 X21heCA9IG51bXB5LnBlcmNlbnRpbGUoZGF0YVswXSwgKDUsIDk1KSkKCiAgICAjIGFwcGx5IGNh c3QgLyByZWNsYW1pbmcgZGF0YQogICAgaWYgIm5ld19taW4iIGluIGdsb2JhbHMoKSBhbmQgIm5l d19tYXgiIGluIGdsb2JhbHMoKToKICAgICAgICBmb3Igdm9sIGluIGluX3ZvbHVtZXM6CiAgICAg ICAgICAgIHByaW50KCJub3JtYWxpemUiLCB2b2wuZmlsZV9wYXRoKQogICAgICAgICAgICB2b2wu bG9hZF9kYXRhKCkKICAgICAgICAgICAgZGF0YSA9IHZvbC5kYXRhCiAgICAgICAgICAgIGRhdGEg PSByZXNjYWxlX2RhdGEoCiAgICAgICAgICAgICAgICBkYXRhPWRhdGEsCiAgICAgICAgICAgICAg ICBuZXdfbWluPW5ld19taW4sCiAgICAgICAgICAgICAgICBuZXdfbWF4PW5ld19tYXgsCiAgICAg ICAgICAgICAgICBkYXRhX21pbj1kYXRhLm1pbigpLAogICAgICAgICAgICAgICAgZGF0YV9tYXg9 ZGF0YS5tYXgoKSwKICAgICAgICAgICAgKQogICAgICAgICAgICB2b2wuZGF0YSA9IGRhdGEKICAg ICAgICAgICAgdm9sLnNhdmVfZGF0YSgpCiAgICAgICAgCiAgICBvdXRfZGF0YSA9IHNjYW4KZWxz ZToKICAgIG91dF9kYXRhID0gaW5fZGF0YZSMBWZsYWdzlEsAjAhmaWxlbmFtZZROdWJhjBNzYXZl ZFdpZGdldEdlb21ldHJ5lENCAdnQywADAAAAAAHKAAAA5wAACf8AAAWfAAABygAAAOcAAAn/AAAF nwAAAAAAAAAACgAAAAHKAAAA5wAACf8AAAWflIwKc2NyaXB0VGV4dJRYuQYAAGltcG9ydCBvcwpp bXBvcnQgbnVtcHkKZnJvbSB0b21vc2Nhbi5mYWN0b3J5IGltcG9ydCBGYWN0b3J5CmZyb20gbmFi dS5taXNjLnV0aWxzIGltcG9ydCByZXNjYWxlX2RhdGEKCmlmIGluX2RhdGEgaXMgbm90IE5vbmU6 CiAgICBzY2FuID0gaW5fZGF0YQogICAgCiAgICAjIHdhcm5pbmc6IHNjYW4ubGF0ZXN0X3JlY29u c3RydWN0aW9ucyBpcyBkZWZpbmVkIGJ5IHRoZSAnbmFidSBzbGljZSByZWNvbnN0cnVjdGlvbicu IFNvIGlmIGEgc2NhbiBpcyBsb2FkZWQgYW5kIG5vdCBnbyB0cmhvdWdoIHRoaXMgd2lkZ2V0CiAg ICAjIGhlIHdvbid0IGJlIGF3YXJlIGFib3V0IHNsaWNlIHJlY29uc3RydWN0aW9ucwogICAgcHJp bnQoImNoZWNrIiwgc2NhbiwgIiAtIGxhdGVzdF9yZWNvbnN0cnVjdGlvbnMiLCBzY2FuLmxhdGVz dF9yZWNvbnN0cnVjdGlvbnMpCiAgICBpbl92b2x1bWVzID0gWwogICAgICAgIEZhY3RvcnkuY3Jl YXRlX3RvbW9fb2JqZWN0X2Zyb21faWRlbnRpZmllcihpZGVudGlmaWVyKSBmb3IgaWRlbnRpZmll ciBpbiBzY2FuLmxhdGVzdF9yZWNvbnN0cnVjdGlvbnMKICAgIF0KCiAgICAjIGxvb2tzIGxpa2Ug dGhlcmUgaXMgc29tZSBleHRyYSB2b2x1bWUgZXhpc3Rpbmc6IGZpbHRlciB0aGVtCiAgICByZXMg PSBbXQogICAgZm9yIHZvbCBpbiBpbl92b2x1bWVzOgogICAgICAgIGlmIG9zLnBhdGguZXhpc3Rz KHZvbC5maWxlX3BhdGgpOgogICAgICAgICAgICByZXMuYXBwZW5kKHZvbCkKICAgIAogICAgaW5f dm9sdW1lcyA9IHJlcwogICAgcHJpbnQoInNsaWNlcyB0byBiZSBoYW5kbGVkIiwgaW5fdm9sdW1l cykKCiAgICBbcHJpbnQodm9sLmdldF9pZGVudGlmaWVyKCkudG9fc3RyKCkpIGZvciB2b2wgaW4g aW5fdm9sdW1lc10KICAgIAogICAgIyByZXRyaWV2ZSB0aGUgbWluIC8gbWF4IGZyb20gdGhlIGZp cnN0IGFjcXVpc2l0aW9uCiAgICBpZiBzY2FuLnBhdGguZW5kc3dpdGgoIl8xXyIpOgogICAgICAg IGZvciB2b2wgaW4gaW5fdm9sdW1lczoKICAgICAgICAgICAgcHJpbnQoInJlY29tcHV0ZSBtaW4g YW5kIG1heCBmcm9tIiwgdm9sLmZpbGVfcGF0aCkKICAgICAgICAgICAgZGF0YSA9IHZvbC5sb2Fk X2RhdGEoKQogICAgICAgICAgICBuZXdfbWluLCBuZXdfbWF4ID0gbnVtcHkucGVyY2VudGlsZShk YXRhWzBdLCAoNSwgOTUpKQoKICAgICMgYXBwbHkgY2FzdCAvIHJlY2xhbWluZyBkYXRhCiAgICBp ZiAibmV3X21pbiIgaW4gZ2xvYmFscygpIGFuZCAibmV3X21heCIgaW4gZ2xvYmFscygpOgogICAg ICAgIGZvciB2b2wgaW4gaW5fdm9sdW1lczoKICAgICAgICAgICAgcHJpbnQoIm5vcm1hbGl6ZSIs IHZvbC5maWxlX3BhdGgpCiAgICAgICAgICAgIHZvbC5sb2FkX2RhdGEoKQogICAgICAgICAgICBk YXRhID0gdm9sLmRhdGEKICAgICAgICAgICAgZGF0YSA9IHJlc2NhbGVfZGF0YSgKICAgICAgICAg ICAgICAgIGRhdGE9ZGF0YSwKICAgICAgICAgICAgICAgIG5ld19taW49bmV3X21pbiwKICAgICAg ICAgICAgICAgIG5ld19tYXg9bmV3X21heCwKICAgICAgICAgICAgICAgIGRhdGFfbWluPWRhdGEu bWluKCksCiAgICAgICAgICAgICAgICBkYXRhX21heD1kYXRhLm1heCgpLAogICAgICAgICAgICAp CiAgICAgICAgICAgIHZvbC5kYXRhID0gZGF0YQogICAgICAgICAgICB2b2wuc2F2ZV9kYXRhKCkK ICAgICAgICAKICAgIG91dF9kYXRhID0gc2NhbgplbHNlOgogICAgb3V0X2RhdGEgPSBpbl9kYXRh lIwNc3BsaXR0ZXJTdGF0ZZROjAtfX3ZlcnNpb25fX5RLAXUu {'controlAreaVisible': True, 'dest_dir_settings': None, 'savedWidgetGeometry': b'\x01\xd9\xd0\xcb\x00\x03\x00\x00\x00\x00\x04>\x00\x00\x02h\x00\x00\x05\xad\x00\x00\x02\xe8\x00\x00\x04>\x00\x00\x02h\x00\x00\x05\xad\x00\x00\x02\xe8\x00\x00\x00\x00\x00\x00\x00\x00\n\x00\x00\x00\x04>\x00\x00\x02h\x00\x00\x05\xad\x00\x00\x02\xe8', '__version__': 1} gASVPgEAAAAAAAB9lCiMDl92aWV3ZXJfY29uZmlnlH2UKIwEbW9kZZSMI3RvbXdlci5ndWkudmlz dWFsaXphdGlvbi5kYXRhdmlld2VylIwMX0Rpc3BsYXlNb2RllJOUjAZzbGljZXOUhZRSlIwJc2xp Y2Vfb3B0lGgEjApfU2xpY2VNb2RllJOUjAZsYXRlc3SUhZRSlIwJcmFkaW9fb3B0lGgEjApfUmFk aW9Nb2RllJOUjApub3JtYWxpemVklIWUUpR1jBJjb250cm9sQXJlYVZpc2libGWUiIwTc2F2ZWRX aWRnZXRHZW9tZXRyeZRDQgHZ0MsAAwAAAAABBgAAAJgAAAhPAAAEugAAAQYAAAC9AAAITwAABLoA AAAAAAAAAAoAAAABBgAAAL0AAAhPAAAEupSMC19fdmVyc2lvbl9flEsBdS4= gASV8hAAAAAAAAB9lCiMEmNvbnRyb2xBcmVhVmlzaWJsZZSIjBJjdXJyZW50U2NyaXB0SW5kZXiU SwCMEWxpYnJhcnlMaXN0U291cmNllF2UjDFvcmFuZ2Vjb250cmliLnRvbXdlci53aWRnZXRzLm90 aGVyLlB5dGhvblNjcmlwdE9XlIwGU2NyaXB0lJOUKYGUfZQojARuYW1llIwLSGVsbG8gd29ybGSU jAZzY3JpcHSUWLwHAABmcm9tIHRvbXdlci5jb3JlLnNjYW4uZWRmc2NhbiBpbXBvcnQgRURGVG9t b1NjYW4KaW1wb3J0IG9zCmltcG9ydCBzaHV0aWwKaW1wb3J0IHNpbHguaW8KCmRlZiBnZXRfa2V5 KGxpbmUpOgogICAgIiIiInJldHVybiBrZXkgbmFtZSBvZiBhIC5pbmZvIGZpbGUiIiIKICAgIHJl dHVybiBsaW5lLnNwbGl0KCI9IilbMF0ucmVwbGFjZSgiICIsICIiKQoKZGVmIGdldF9yYXdfZGlj dChmaWxlX3BhdGgpOgogICAgIiIicmV0dXJuIHRoZSBpbmZvIGZpbGUgbWV0YWRhdGEgYXMgYSBk aWN0aW9uYXJ5IHdpdGggbWV0YWRhdGEgbmFtZSBhcyBrZXkgYW5kIHJhdyBsaW5lIGFzIHZhbHVl IiIiCiAgICBtZXRhZGF0YSA9IHt9CiAgICB3aXRoIG9wZW4oZmlsZV9wYXRoLCBtb2RlPSJyIikg YXMgZjoKICAgICAgICBsaW5lID0gZi5yZWFkbGluZSgpCiAgICAgICAgd2hpbGUgbGluZToKICAg ICAgICAgICAga2V5ID0gZ2V0X2tleShsaW5lKQogICAgICAgICAgICBtZXRhZGF0YVtrZXldID0g bGluZQogICAgICAgICAgICBsaW5lID0gZi5yZWFkbGluZSgpCiAgICByZXR1cm4gbWV0YWRhdGEK CiMgUmVhZCB0aGUgZGF0YQpzY2FuID0gaW5fZGF0YQpwcmludCgiV29ya2luZyBvbjogIiwgaW5f ZGF0YSkKaWYgaXNpbnN0YW5jZShzY2FuLCBFREZUb21vU2Nhbik6CiAgICBpbmZvX2ZpbGUgPSBz Y2FuLmdldF9pbmZvX2ZpbGUoc2Nhbi5wYXRoKQogICAgcHJpbnQoZiJcblJlYWRpbmcgKnRoaXMq IGluZm9fZmlsZToge2luZm9fZmlsZX0iKQogICAgcmF3X2luZm9fZmlsZSA9IGluZm9fZmlsZVs6 LTVdICsgIl9yYXcuaW5mbyIKICAgIGlmIG5vdCBvcy5wYXRoLmV4aXN0cyhyYXdfaW5mb19maWxl KToKICAgICAgICAjIGNvcHkgdGhlIG9yaWdpbmFsIGZpbGUgb25seSB0aGUgZmlyc3QgdGltZS4g T3RoZXJ3aXNlIHdlIHdpbGwgb3ZlcndyaXRlIHdpdGggYW4gdXBkYXRlZCBmaWxlLgogICAgICAg IHNodXRpbC5jb3B5ZmlsZSgKICAgICAgICAgICAgaW5mb19maWxlLAogICAgICAgICAgICByYXdf aW5mb19maWxlLAogICAgICAgICkKICAgIG1ldGFkYXRhID0gZ2V0X3Jhd19kaWN0KGluZm9fZmls ZSkKICAgIAogICAgIyBSZWFkIHRoZSBpbWFnZXMKICAgIGVkZk5hbWUgPSBpbmZvX2ZpbGVbOi01 XSsiMDA1MC5lZGYiCiAgICBlZGYgPSBzaWx4LmlvLm9wZW4oZWRmTmFtZSkKICAgICMgUmVhZCB0 aGUgcGFyYW1ldGVycyBmcm9tIHRoZSBpbWFnZQogICAgY3gxID0gZWRmWyJzY2FuXzAvaW5zdHJ1 bWVudC9wb3NpdGlvbmVycy9jeCJdWzBdICNtbQogICAgc3gxID0gZWRmWyJzY2FuXzAvaW5zdHJ1 bWVudC9wb3NpdGlvbmVycy9zeCJdWzBdICNtbQogICAgcHhfbWFnbmlmaWVkX2VkZiA9IGVkZlsi c2Nhbl8wL2luc3RydW1lbnQvZGV0ZWN0b3JfMC9vdGhlcnMvcGl4ZWxfc2l6ZSJdWzBdIC8gMWU2 CiAgICBweDEgPSBlZGZbInNjYW5fMC9pbnN0cnVtZW50L2RldGVjdG9yXzAvb3RoZXJzL29wdGlj X3VzZWQiXVswXSAvIDFlNgogICAgbWFnID0gcHgxIC8gcHhfbWFnbmlmaWVkX2VkZgogICAgc3gw ID0gc3gxIC0gY3gxIC8gbWFnICMgbW0KICAgIHoxID0gKHN4MSAtIHN4MCkgIyBtbQogICAgejIg PSAoY3gxIC0gc3gxICsgc3gwKSAjIG1tCiAgICBtYWduaWZpY2F0aW9uID0gKHoxICsgejIpIC8g ejEKICAgIGRpc3RhbmNlID0gejIgLyBtYWduaWZpY2F0aW9uICMgbW0KICAgIHByaW50KCJDb21w dXRlZCBkaXN0YW5jZSBpbiBbbW1dOiAiLCBkaXN0YW5jZSkKICAgIG1ldGFkYXRhWyJEaXN0YW5j ZSJdID0gIkRpc3RhbmNlPSBcdFx0ICIgKyBzdHIoZGlzdGFuY2UpICsgIlxuIgoKICAgIHdpdGgg b3BlbihpbmZvX2ZpbGUsIG1vZGU9InciKSBhcyBmOgogICAgICAgIGZvciBfLCBsaW5lIGluIG1l dGFkYXRhLml0ZW1zKCk6CiAgICAgICAgICAgIGYud3JpdGUobGluZSkKICAgICMgcmVzZXQga2V5 IGZvciBjb2hlcmVuY2UKICAgIHNjYW4uX2Rpc3RhbmNlID0gTm9uZQpvdXRfZGF0YSA9IHNjYW6U jAVmbGFnc5RLAIwIZmlsZW5hbWWUTnViYYwTc2F2ZWRXaWRnZXRHZW9tZXRyeZRDQgHZ0MsAAwAA AAAMQAAAATEAABCdAAAEwQAADEAAAAFWAAAQnQAABMEAAAABAAAAAAoAAAAMQAAAAVYAABCdAAAE wZSMCnNjcmlwdFRleHSUWLwHAABmcm9tIHRvbXdlci5jb3JlLnNjYW4uZWRmc2NhbiBpbXBvcnQg RURGVG9tb1NjYW4KaW1wb3J0IG9zCmltcG9ydCBzaHV0aWwKaW1wb3J0IHNpbHguaW8KCmRlZiBn ZXRfa2V5KGxpbmUpOgogICAgIiIiInJldHVybiBrZXkgbmFtZSBvZiBhIC5pbmZvIGZpbGUiIiIK ICAgIHJldHVybiBsaW5lLnNwbGl0KCI9IilbMF0ucmVwbGFjZSgiICIsICIiKQoKZGVmIGdldF9y YXdfZGljdChmaWxlX3BhdGgpOgogICAgIiIicmV0dXJuIHRoZSBpbmZvIGZpbGUgbWV0YWRhdGEg YXMgYSBkaWN0aW9uYXJ5IHdpdGggbWV0YWRhdGEgbmFtZSBhcyBrZXkgYW5kIHJhdyBsaW5lIGFz IHZhbHVlIiIiCiAgICBtZXRhZGF0YSA9IHt9CiAgICB3aXRoIG9wZW4oZmlsZV9wYXRoLCBtb2Rl PSJyIikgYXMgZjoKICAgICAgICBsaW5lID0gZi5yZWFkbGluZSgpCiAgICAgICAgd2hpbGUgbGlu ZToKICAgICAgICAgICAga2V5ID0gZ2V0X2tleShsaW5lKQogICAgICAgICAgICBtZXRhZGF0YVtr ZXldID0gbGluZQogICAgICAgICAgICBsaW5lID0gZi5yZWFkbGluZSgpCiAgICByZXR1cm4gbWV0 YWRhdGEKCiMgUmVhZCB0aGUgZGF0YQpzY2FuID0gaW5fZGF0YQpwcmludCgiV29ya2luZyBvbjog IiwgaW5fZGF0YSkKaWYgaXNpbnN0YW5jZShzY2FuLCBFREZUb21vU2Nhbik6CiAgICBpbmZvX2Zp bGUgPSBzY2FuLmdldF9pbmZvX2ZpbGUoc2Nhbi5wYXRoKQogICAgcHJpbnQoZiJcblJlYWRpbmcg KnRoaXMqIGluZm9fZmlsZToge2luZm9fZmlsZX0iKQogICAgcmF3X2luZm9fZmlsZSA9IGluZm9f ZmlsZVs6LTVdICsgIl9yYXcuaW5mbyIKICAgIGlmIG5vdCBvcy5wYXRoLmV4aXN0cyhyYXdfaW5m b19maWxlKToKICAgICAgICAjIGNvcHkgdGhlIG9yaWdpbmFsIGZpbGUgb25seSB0aGUgZmlyc3Qg dGltZS4gT3RoZXJ3aXNlIHdlIHdpbGwgb3ZlcndyaXRlIHdpdGggYW4gdXBkYXRlZCBmaWxlLgog ICAgICAgIHNodXRpbC5jb3B5ZmlsZSgKICAgICAgICAgICAgaW5mb19maWxlLAogICAgICAgICAg ICByYXdfaW5mb19maWxlLAogICAgICAgICkKICAgIG1ldGFkYXRhID0gZ2V0X3Jhd19kaWN0KGlu Zm9fZmlsZSkKICAgIAogICAgIyBSZWFkIHRoZSBpbWFnZXMKICAgIGVkZk5hbWUgPSBpbmZvX2Zp bGVbOi01XSsiMDA1MC5lZGYiCiAgICBlZGYgPSBzaWx4LmlvLm9wZW4oZWRmTmFtZSkKICAgICMg UmVhZCB0aGUgcGFyYW1ldGVycyBmcm9tIHRoZSBpbWFnZQogICAgY3gxID0gZWRmWyJzY2FuXzAv aW5zdHJ1bWVudC9wb3NpdGlvbmVycy9jeCJdWzBdICNtbQogICAgc3gxID0gZWRmWyJzY2FuXzAv aW5zdHJ1bWVudC9wb3NpdGlvbmVycy9zeCJdWzBdICNtbQogICAgcHhfbWFnbmlmaWVkX2VkZiA9 IGVkZlsic2Nhbl8wL2luc3RydW1lbnQvZGV0ZWN0b3JfMC9vdGhlcnMvcGl4ZWxfc2l6ZSJdWzBd IC8gMWU2CiAgICBweDEgPSBlZGZbInNjYW5fMC9pbnN0cnVtZW50L2RldGVjdG9yXzAvb3RoZXJz L29wdGljX3VzZWQiXVswXSAvIDFlNgogICAgbWFnID0gcHgxIC8gcHhfbWFnbmlmaWVkX2VkZgog ICAgc3gwID0gc3gxIC0gY3gxIC8gbWFnICMgbW0KICAgIHoxID0gKHN4MSAtIHN4MCkgIyBtbQog ICAgejIgPSAoY3gxIC0gc3gxICsgc3gwKSAjIG1tCiAgICBtYWduaWZpY2F0aW9uID0gKHoxICsg ejIpIC8gejEKICAgIGRpc3RhbmNlID0gejIgLyBtYWduaWZpY2F0aW9uICMgbW0KICAgIHByaW50 KCJDb21wdXRlZCBkaXN0YW5jZSBpbiBbbW1dOiAiLCBkaXN0YW5jZSkKICAgIG1ldGFkYXRhWyJE aXN0YW5jZSJdID0gIkRpc3RhbmNlPSBcdFx0ICIgKyBzdHIoZGlzdGFuY2UpICsgIlxuIgoKICAg IHdpdGggb3BlbihpbmZvX2ZpbGUsIG1vZGU9InciKSBhcyBmOgogICAgICAgIGZvciBfLCBsaW5l IGluIG1ldGFkYXRhLml0ZW1zKCk6CiAgICAgICAgICAgIGYud3JpdGUobGluZSkKICAgICMgcmVz ZXQga2V5IGZvciBjb2hlcmVuY2UKICAgIHNjYW4uX2Rpc3RhbmNlID0gTm9uZQpvdXRfZGF0YSA9 IHNjYW6UjA1zcGxpdHRlclN0YXRllEMfAAAA/wAAAAEAAAACAAACdgAAAM8B/////wEAAAACAJSM C19fdmVyc2lvbl9flEsBdS4= {'_scanIDs': [], 'controlAreaVisible': True, 'savedWidgetGeometry': b'\x01\xd9\xd0\xcb\x00\x03\x00\x00\x00\x00\x02\xac\x00\x00\x03:\x00\x00\x06\x17\x00\x00\x04\xe8\x00\x00\x02\xac\x00\x00\x03_\x00\x00\x06\x17\x00\x00\x04\xe8\x00\x00\x00\x00\x00\x00\x00\x00\n\x00\x00\x00\x02\xac\x00\x00\x03_\x00\x00\x06\x17\x00\x00\x04\xe8', '__version__': 1} {'acquisitionMethod': ('[scan].xml',), 'controlAreaVisible': True, 'folderObserved': '/lbsram/data', 'linuxFilePatternSetting': None, 'savedWidgetGeometry': b'\x01\xd9\xd0\xcb\x00\x03\x00\x00\x00\x00\x0c\xf3\x00\x00\x00|\x00\x00\x13\x7f\x00\x00\x02n\x00\x00\x0c\xf3\x00\x00\x00\xa1\x00\x00\x13\x7f\x00\x00\x02n\x00\x00\x00\x01\x00\x00\x00\x00\n\x00\x00\x00\x0c\xf3\x00\x00\x00\xa1\x00\x00\x13\x7f\x00\x00\x02n', '__version__': 1} {'_ewoks_default_inputs': {'data': None, 'dark_ref_params': {'DOWHEN': 'before', 'DARKCAL': 'mean', 'DARKOVE': 0, 'DARKRMV': 0, 'DKFILE': 'darkend[0-9]{3,4}', 'REFSCAL': 'median', 'REFSOVE': 0, 'REFSRMV': 0, 'RFFILE': 'ref*.*[0-9]{3,4}_[0-9]{3,4}'}}, 'controlAreaVisible': True, 'savedWidgetGeometry': b'\x01\xd9\xd0\xcb\x00\x03\x00\x00\x00\x00\x01+\x00\x00\x01\x99\x00\x00\x08\xd4\x00\x00\x04\x06\x00\x00\x01+\x00\x00\x01\x99\x00\x00\x08\xd4\x00\x00\x04\x06\x00\x00\x00\x00\x00\x00\x00\x00\n\x00\x00\x00\x01+\x00\x00\x01\x99\x00\x00\x08\xd4\x00\x00\x04\x06', '__version__': 1} ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739271825.0 tomwer-1.4.8/src/orangecontrib/tomwer/tutorials/id16b/ID16b_normalization.ows0000644000175000017500000005411214752627221026352 0ustar00paynopayno {'acquisitionMethod': ('[scan].xml',), 'controlAreaVisible': True, 'folderObserved': '/lbsram/data/', 'linuxFilePatternSetting': '', 'savedWidgetGeometry': b'\x01\xd9\xd0\xcb\x00\x03\x00\x00\x00\x00\x02I\x00\x00\x01h\x00\x00\x056\x00\x00\x03G\x00\x00\x02I\x00\x00\x01h\x00\x00\x056\x00\x00\x03G\x00\x00\x00\x00\x00\x00\x00\x00\x07\x80\x00\x00\x02I\x00\x00\x01h\x00\x00\x056\x00\x00\x03G', '__version__': 1} {'_ewoks_default_inputs': {'data': None, 'dark_ref_params': {'DOWHEN': 'before', 'DARKCAL': 'Average', 'DARKOVE': 1, 'DARKRMV': 0, 'DKFILE': 'darkend[0-9]{3,4}', 'REFSCAL': 'Median', 'REFSOVE': 1, 'REFSRMV': 0, 'RFFILE': 'ref*.*[0-9]{3,4}_[0-9]{3,4}'}}, 'controlAreaVisible': True, 'savedWidgetGeometry': b'\x01\xd9\xd0\xcb\x00\x03\x00\x00\x00\x00\x00H\x00\x00\x00\x1b\x00\x00\x03\xef\x00\x00\x02\x1f\x00\x00\x00H\x00\x00\x00@\x00\x00\x03\xef\x00\x00\x02\x1f\x00\x00\x00\x00\x00\x00\x00\x00\n\x00\x00\x00\x00H\x00\x00\x00@\x00\x00\x03\xef\x00\x00\x02\x1f', '__version__': 1} {'_scanIDs': [], 'controlAreaVisible': True, 'savedWidgetGeometry': b'\x01\xd9\xd0\xcb\x00\x03\x00\x00\x00\x00\x02\x80\x00\x00\x01C\x00\x00\x04\xff\x00\x00\x03G\x00\x00\x02\x80\x00\x00\x01h\x00\x00\x04\xff\x00\x00\x03G\x00\x00\x00\x00\x00\x00\x00\x00\x07\x80\x00\x00\x02\x80\x00\x00\x01h\x00\x00\x04\xff\x00\x00\x03G', '__version__': 1} {'_muted': False, 'controlAreaVisible': True, 'savedWidgetGeometry': None, '__version__': 1} {'_ewoks_default_inputs': {'data': None, 'nabu_params': {'preproc': {'flatfield': 1, 'double_flatfield_enabled': 0, 'dff_sigma': 0.0, 'ccd_filter_enabled': 0, 'ccd_filter_threshold': 0.04, 'take_logarithm': True, 'log_min_clip': 1e-06, 'log_max_clip': 10.0, 'sino_rings_correction': 'None', 'sino_rings_options': 'sigma=1.0 ; levels=10', 'tilt_correction': '', 'autotilt_options': ''}, 'reconstruction': {'method': 'FBP', 'angles_file': '', 'axis_correction_file': '', 'angle_offset': 0.0, 'fbp_filter_type': 'ramlak', 'padding_type': 'edges', 'iterations': 200, 'optim_algorithm': 'chambolle-pock', 'weight_tv': 0.01, 'preconditioning_filter': 1, 'positivity_constraint': 1, 'rotation_axis_position': '', 'translation_movements_file': '', 'clip_outer_circle': 1, 'start_x': 0, 'end_x': -1, 'start_y': 0, 'end_y': -1, 'start_z': 0, 'end_z': -1, 'enable_halftomo': 0}, 'dataset': {'binning': 1, 'binning_z': 1, 'projections_subsampling': 1}, 'tomwer_slices': 'middle', 'output': {'file_format': 'vol', 'location': ''}, 'phase': {'method': 'Paganin', 'delta_beta': '500', 'padding_type': 'edge', 'unsharp_coeff': 3.0, 'unsharp_sigma': 0.65}, 'configuration_level': 'optional', 'mode_locked': False, 'cluster_config': None}}, 'controlAreaVisible': True, 'savedWidgetGeometry': b'\x01\xd9\xd0\xcb\x00\x03\x00\x00\x00\x00\x00H\x00\x00\x00@\x00\x00\x05\xd1\x00\x00\x03\x9e\x00\x00\x00H\x00\x00\x00@\x00\x00\x05\xd1\x00\x00\x03\x9e\x00\x00\x00\x00\x00\x00\x00\x00\n\x00\x00\x00\x00H\x00\x00\x00@\x00\x00\x05\xd1\x00\x00\x03\x9e', '__version__': 1} gASVWQEAAAAAAAB9lCiMDl92aWV3ZXJfY29uZmlnlH2UKIwEbW9kZZSMCGJ1aWx0aW5zlIwHZ2V0 YXR0cpSTlIwjdG9td2VyLmd1aS52aXN1YWxpemF0aW9uLmRhdGF2aWV3ZXKUjAxfRGlzcGxheU1v ZGWUk5SMBlNMSUNFU5SGlFKUjAlzbGljZV9vcHSUaAZoB4wKX1NsaWNlTW9kZZSTlIwGTEFURVNU lIaUUpSMCXJhZGlvX29wdJRoBmgHjApfUmFkaW9Nb2RllJOUjApOT1JNQUxJWkVElIaUUpR1jBJj b250cm9sQXJlYVZpc2libGWUiIwTc2F2ZWRXaWRnZXRHZW9tZXRyeZRDQgHZ0MsAAwAAAAAASAAA AIIAAAeRAAAEpAAAAEgAAACnAAAHkQAABKQAAAAAAAAAAAoAAAAASAAAAKcAAAeRAAAEpJSMC19f dmVyc2lvbl9flEsBdS4= {'_ewoks_default_inputs': {'data': None, 'axis_params': {'MODE': 'centered', 'POSITION_VALUE': -11.37684057962989, 'CALC_INPUT_TYPE': 'transmission_nopag', 'ANGLE_MODE': '0-180', 'USE_SINOGRAM': False, 'SINOGRAM_LINE': '', 'SINOGRAM_SUBSAMPLING': 10, 'AXIS_URL_1': '', 'AXIS_URL_2': '', 'LOOK_AT_STDMAX': False, 'NEAR_WX': 10, 'FINE_STEP_X': 0.1, 'SCALE_IMG2_TO_IMG1': False, 'NEAR_POSITION': 0.0, 'PADDING_MODE': 'edge', 'FLIP_LR': True, 'COMPOSITE_OPTS': {'theta': 10, 'oversampling': 4, 'n_subsampling_y': 10, 'take_log': True, 'near_pos': 0.0, 'near_width': 20}, 'SIDE': 'left', 'COR_OPTIONS': ''}, 'gui': {'mode_is_lock': True, 'value_is_lock': False, 'auto_update_estimated_cor': True}}, 'controlAreaVisible': True, 'savedWidgetGeometry': b'\x01\xd9\xd0\xcb\x00\x03\x00\x00\x00\x00\x00H\x00\x00\x00\x1b\x00\x00\x05]\x00\x00\x03\x17\x00\x00\x00H\x00\x00\x00@\x00\x00\x05]\x00\x00\x03\x17\x00\x00\x00\x00\x00\x00\x00\x00\n\x00\x00\x00\x00H\x00\x00\x00@\x00\x00\x05]\x00\x00\x03\x17', '__version__': 1} gASVwwUAAAAAAAB9lCiMEmNvbnRyb2xBcmVhVmlzaWJsZZSIjBJjdXJyZW50U2NyaXB0SW5kZXiU SwCMEWxpYnJhcnlMaXN0U291cmNllF2UjDFvcmFuZ2Vjb250cmliLnRvbXdlci53aWRnZXRzLm90 aGVyLlB5dGhvblNjcmlwdE9XlIwGU2NyaXB0lJOUKYGUfZQojARuYW1llIwLSGVsbG8gd29ybGSU jAZzY3JpcHSUWDUCAABpbXBvcnQgb3MKc2NhbiA9IGluX2RhdGEKCnZvbF9mbG9hdF9wYXRoID0g b3MucGF0aC5qb2luKAogICAgb3MucGF0aC5kaXJuYW1lKHNjYW4ucGF0aCksCiAgICAiLnZvbGZs b2F0X25vYmFja3VwIiwKKQoKdm9sX2Zsb2F0X3N5bV9saW5rX3BhdGggPSBvcy5wYXRoLmpvaW4o CiAgICBvcy5wYXRoLmRpcm5hbWUoc2Nhbi5wYXRoKSwKICAgICJ2b2xmbG9hdCIsCikKCmlmIG5v dCBvcy5wYXRoLmV4aXN0cyh2b2xfZmxvYXRfcGF0aCk6CiAgICBwcmludChmImNyZWF0ZXMge3Zv bF9mbG9hdF9wYXRofSIpCiAgICBvcy5tYWtlZGlycyh2b2xfZmxvYXRfcGF0aCwgbW9kZT0wbzc3 NCkKICAgICMgZG9uJ3Qga25vdyB3aHkgYnV0IGZpcnN0IGNyZWF0aW9uIHdpdGggMG83NzQgZmFp bHMuLi4KICAgIG9zLmNobW9kKHZvbF9mbG9hdF9wYXRoLCAwbzc3NCkKICAgIAogICAgIyBjcmVh dGUgc3ltbGluayBmcm9tIHZvbGZsb2F0IHRvICIudm9sZmxvYXRfbm9iYWNrdXAiCiAgICBvcy5z eW1saW5rKHZvbF9mbG9hdF9wYXRoLCB2b2xfZmxvYXRfc3ltX2xpbmtfcGF0aCkKCgpvdXRfZGF0 YSA9IHNjYW4KlIwFZmxhZ3OUSwCMCGZpbGVuYW1llE51YmGME3NhdmVkV2lkZ2V0R2VvbWV0cnmU Q0IB2dDLAAMAAAAAAEgAAAAbAAAGBwAAA3YAAABIAAAAQAAABgcAAAN2AAAAAAAAAAAKAAAAAEgA AABAAAAGBwAAA3aUjApzY3JpcHRUZXh0lFg1AgAAaW1wb3J0IG9zCnNjYW4gPSBpbl9kYXRhCgp2 b2xfZmxvYXRfcGF0aCA9IG9zLnBhdGguam9pbigKICAgIG9zLnBhdGguZGlybmFtZShzY2FuLnBh dGgpLAogICAgIi52b2xmbG9hdF9ub2JhY2t1cCIsCikKCnZvbF9mbG9hdF9zeW1fbGlua19wYXRo ID0gb3MucGF0aC5qb2luKAogICAgb3MucGF0aC5kaXJuYW1lKHNjYW4ucGF0aCksCiAgICAidm9s ZmxvYXQiLAopCgppZiBub3Qgb3MucGF0aC5leGlzdHModm9sX2Zsb2F0X3BhdGgpOgogICAgcHJp bnQoZiJjcmVhdGVzIHt2b2xfZmxvYXRfcGF0aH0iKQogICAgb3MubWFrZWRpcnModm9sX2Zsb2F0 X3BhdGgsIG1vZGU9MG83NzQpCiAgICAjIGRvbid0IGtub3cgd2h5IGJ1dCBmaXJzdCBjcmVhdGlv biB3aXRoIDBvNzc0IGZhaWxzLi4uCiAgICBvcy5jaG1vZCh2b2xfZmxvYXRfcGF0aCwgMG83NzQp CiAgICAKICAgICMgY3JlYXRlIHN5bWxpbmsgZnJvbSB2b2xmbG9hdCB0byAiLnZvbGZsb2F0X25v YmFja3VwIgogICAgb3Muc3ltbGluayh2b2xfZmxvYXRfcGF0aCwgdm9sX2Zsb2F0X3N5bV9saW5r X3BhdGgpCgoKb3V0X2RhdGEgPSBzY2FuCpSMDXNwbGl0dGVyU3RhdGWUTowLX192ZXJzaW9uX1+U SwF1Lg== gASVMw8AAAAAAAB9lCiMEmNvbnRyb2xBcmVhVmlzaWJsZZSIjBJjdXJyZW50U2NyaXB0SW5kZXiU SwCMEWxpYnJhcnlMaXN0U291cmNllF2UjDFvcmFuZ2Vjb250cmliLnRvbXdlci53aWRnZXRzLm90 aGVyLlB5dGhvblNjcmlwdE9XlIwGU2NyaXB0lJOUKYGUfZQojARuYW1llIwLSGVsbG8gd29ybGSU jAZzY3JpcHSUWO0GAABpbXBvcnQgc2h1dGlsCmltcG9ydCBvcwpmcm9tIHRvbXdlci5jb3JlLnNj YW4uZWRmc2NhbiBpbXBvcnQgRURGVG9tb1NjYW4KZnJvbSBzaWx4LmlvLmRpY3RkdW1wIGltcG9y dCBkaWN0dG9pbmksIGxvYWQgYXMgbG9hZF9pbmkKCmRlZiBnZXRfa2V5KGxpbmUpOgogICAgIiIi InJldHVybiBrZXkgbmFtZSBvZiBhIC5pbmZvIGZpbGUiIiIKICAgIHJldHVybiBsaW5lLnNwbGl0 KCI9IilbMF0ucmVwbGFjZSgiICIsICIiKQoKCmRlZiBnZXRfcmF3X2RpY3QoZmlsZV9wYXRoKToK ICAgICIiInJldHVybiB0aGUgaW5mbyBmaWxlIG1ldGFkYXRhIGFzIGEgZGljdGlvbmFyeSB3aXRo IG1ldGFkYXRhIG5hbWUgYXMga2V5IGFuZCByYXcgbGluZSBhcyB2YWx1ZSIiIgogICAgbWV0YWRh dGEgPSB7fQogICAgd2l0aCBvcGVuKGZpbGVfcGF0aCwgbW9kZT0iciIpIGFzIGY6CiAgICAgICAg bGluZSA9IGYucmVhZGxpbmUoKQogICAgICAgIHdoaWxlIGxpbmU6CiAgICAgICAgICAgIGtleSA9 IGdldF9rZXkobGluZSkKICAgICAgICAgICAgbWV0YWRhdGFba2V5XSA9IGxpbmUKICAgICAgICAg ICAgbGluZSA9IGYucmVhZGxpbmUoKQogICAgcmV0dXJuIG1ldGFkYXRhCgpzY2FuID0gaW5fZGF0 YQpwcmludCgidHJlYXQiLCBpbl9kYXRhKQoKaWYgaXNpbnN0YW5jZShzY2FuLCBFREZUb21vU2Nh bik6CiAgICAjIGNvcHkgdGhlIC5pbmZvIGZpbGUKICAgIGluZm9fZmlsZSA9IHNjYW4uZ2V0X2lu Zm9fZmlsZShzY2FuLnBhdGgpCiAgICBwcmludChmIlxuXG5UcnlpbmcgdG8gcmVhZCBpbmZvX2Zp bGU6IHtpbmZvX2ZpbGV9IikKICAgICNpbmZvX2ZpbGUgPSBpbmZvX2ZpbGVbNzpdCiAgICBwcmlu dChmIlxuRm9yY2UgdG8gcmVhZCAqdGhpcyogaW5mb19maWxlOiB7aW5mb19maWxlfSIpCiAgICBw cmludChmIlxuSW5pdGlhbCBkaXN0YW5jZSBpbiBpbnB1dCBmaWxlOiBcdHtnZXRfcmF3X2RpY3Qo aW5mb19maWxlKVsnRGlzdGFuY2UnXX0iKQogICAgcmF3X2luZm9fZmlsZSA9IGluZm9fZmlsZVs6 LTVdICsgIl9yYXcuaW5mbyIKICAgIGlmIG5vdCBvcy5wYXRoLmV4aXN0cyhyYXdfaW5mb19maWxl KToKICAgICAgICAjIGNvcHkgdGhlIG9yaWdpbmFsIGZpbGUgb25seSB0aGUgZmlyc3QgdGltZS4g T3RoZXJ3aXNlIHdlIHdpbGwgb3ZlcndyaXRlIHdpdGggYW4gdXBkYXRlZCBmaWxlLgogICAgICAg IHNodXRpbC5jb3B5ZmlsZSgKICAgICAgICAgICAgaW5mb19maWxlLAogICAgICAgICAgICByYXdf aW5mb19maWxlLAogICAgICAgICkKICAgIG1ldGFkYXRhID0gZ2V0X3Jhd19kaWN0KGluZm9fZmls ZSkKICAgICMgb3ZlcndyaXRlIGRpc3RhbmNlIGtleQogICAgbmV3X2Rpc3RhbmNlID0gMC4xNjg1 ICoxMDAwICMgMjgwbm0KICAgICNuZXdfZGlzdGFuY2UgPSAwLjA5MTcgIzEwMG5tCiAgICAjbmV3 X2Rpc3RhbmNlID0gMC4xMjUgIzE1MG5tCiAgICAjbmV3X2Rpc3RhbmNlID0gIDAuMDUwMDA2MDUg IyAqIDEwMDAgIzUwbm0KICAgICNuZXdfZGlzdGFuY2UgPSAwLjA5OSAqIDEwMDAgIzE1MG5tCiAg ICBtZXRhZGF0YVsiRGlzdGFuY2UiXSA9ICJEaXN0YW5jZT0gXHRcdCIgKyBzdHIobmV3X2Rpc3Rh bmNlKSArICJcbiIKICAgIHdpdGggb3BlbihpbmZvX2ZpbGUsIG1vZGU9InciKSBhcyBmOgogICAg ICAgIGZvciBfLCBsaW5lIGluIG1ldGFkYXRhLml0ZW1zKCk6CiAgICAgICAgICAgIGYud3JpdGUo bGluZSkKICAgICMgcmVzZXQga2V5IGZvciBjb2hlcmVuY2UKICAgIHNjYW4uX2Rpc3RhbmNlID0g Tm9uZQpvdXRfZGF0YSA9IHNjYW6UjAVmbGFnc5RLAIwIZmlsZW5hbWWUTnViYYwTc2F2ZWRXaWRn ZXRHZW9tZXRyeZRDQgHZ0MsAAwAAAAABfwAAAJAAAAjIAAAEsgAAAX8AAAC1AAAIyAAABLIAAAAA AAAAAAoAAAABfwAAALUAAAjIAAAEspSMCnNjcmlwdFRleHSUWO0GAABpbXBvcnQgc2h1dGlsCmlt cG9ydCBvcwpmcm9tIHRvbXdlci5jb3JlLnNjYW4uZWRmc2NhbiBpbXBvcnQgRURGVG9tb1NjYW4K ZnJvbSBzaWx4LmlvLmRpY3RkdW1wIGltcG9ydCBkaWN0dG9pbmksIGxvYWQgYXMgbG9hZF9pbmkK CmRlZiBnZXRfa2V5KGxpbmUpOgogICAgIiIiInJldHVybiBrZXkgbmFtZSBvZiBhIC5pbmZvIGZp bGUiIiIKICAgIHJldHVybiBsaW5lLnNwbGl0KCI9IilbMF0ucmVwbGFjZSgiICIsICIiKQoKCmRl ZiBnZXRfcmF3X2RpY3QoZmlsZV9wYXRoKToKICAgICIiInJldHVybiB0aGUgaW5mbyBmaWxlIG1l dGFkYXRhIGFzIGEgZGljdGlvbmFyeSB3aXRoIG1ldGFkYXRhIG5hbWUgYXMga2V5IGFuZCByYXcg bGluZSBhcyB2YWx1ZSIiIgogICAgbWV0YWRhdGEgPSB7fQogICAgd2l0aCBvcGVuKGZpbGVfcGF0 aCwgbW9kZT0iciIpIGFzIGY6CiAgICAgICAgbGluZSA9IGYucmVhZGxpbmUoKQogICAgICAgIHdo aWxlIGxpbmU6CiAgICAgICAgICAgIGtleSA9IGdldF9rZXkobGluZSkKICAgICAgICAgICAgbWV0 YWRhdGFba2V5XSA9IGxpbmUKICAgICAgICAgICAgbGluZSA9IGYucmVhZGxpbmUoKQogICAgcmV0 dXJuIG1ldGFkYXRhCgpzY2FuID0gaW5fZGF0YQpwcmludCgidHJlYXQiLCBpbl9kYXRhKQoKaWYg aXNpbnN0YW5jZShzY2FuLCBFREZUb21vU2Nhbik6CiAgICAjIGNvcHkgdGhlIC5pbmZvIGZpbGUK ICAgIGluZm9fZmlsZSA9IHNjYW4uZ2V0X2luZm9fZmlsZShzY2FuLnBhdGgpCiAgICBwcmludChm IlxuXG5UcnlpbmcgdG8gcmVhZCBpbmZvX2ZpbGU6IHtpbmZvX2ZpbGV9IikKICAgICNpbmZvX2Zp bGUgPSBpbmZvX2ZpbGVbNzpdCiAgICBwcmludChmIlxuRm9yY2UgdG8gcmVhZCAqdGhpcyogaW5m b19maWxlOiB7aW5mb19maWxlfSIpCiAgICBwcmludChmIlxuSW5pdGlhbCBkaXN0YW5jZSBpbiBp bnB1dCBmaWxlOiBcdHtnZXRfcmF3X2RpY3QoaW5mb19maWxlKVsnRGlzdGFuY2UnXX0iKQogICAg cmF3X2luZm9fZmlsZSA9IGluZm9fZmlsZVs6LTVdICsgIl9yYXcuaW5mbyIKICAgIGlmIG5vdCBv cy5wYXRoLmV4aXN0cyhyYXdfaW5mb19maWxlKToKICAgICAgICAjIGNvcHkgdGhlIG9yaWdpbmFs IGZpbGUgb25seSB0aGUgZmlyc3QgdGltZS4gT3RoZXJ3aXNlIHdlIHdpbGwgb3ZlcndyaXRlIHdp dGggYW4gdXBkYXRlZCBmaWxlLgogICAgICAgIHNodXRpbC5jb3B5ZmlsZSgKICAgICAgICAgICAg aW5mb19maWxlLAogICAgICAgICAgICByYXdfaW5mb19maWxlLAogICAgICAgICkKICAgIG1ldGFk YXRhID0gZ2V0X3Jhd19kaWN0KGluZm9fZmlsZSkKICAgICMgb3ZlcndyaXRlIGRpc3RhbmNlIGtl eQogICAgbmV3X2Rpc3RhbmNlID0gMC4xNjg1ICoxMDAwICMgMjgwbm0KICAgICNuZXdfZGlzdGFu Y2UgPSAwLjA5MTcgIzEwMG5tCiAgICAjbmV3X2Rpc3RhbmNlID0gMC4xMjUgIzE1MG5tCiAgICAj bmV3X2Rpc3RhbmNlID0gIDAuMDUwMDA2MDUgIyAqIDEwMDAgIzUwbm0KICAgICNuZXdfZGlzdGFu Y2UgPSAwLjA5OSAqIDEwMDAgIzE1MG5tCiAgICBtZXRhZGF0YVsiRGlzdGFuY2UiXSA9ICJEaXN0 YW5jZT0gXHRcdCIgKyBzdHIobmV3X2Rpc3RhbmNlKSArICJcbiIKICAgIHdpdGggb3BlbihpbmZv X2ZpbGUsIG1vZGU9InciKSBhcyBmOgogICAgICAgIGZvciBfLCBsaW5lIGluIG1ldGFkYXRhLml0 ZW1zKCk6CiAgICAgICAgICAgIGYud3JpdGUobGluZSkKICAgICMgcmVzZXQga2V5IGZvciBjb2hl cmVuY2UKICAgIHNjYW4uX2Rpc3RhbmNlID0gTm9uZQpvdXRfZGF0YSA9IHNjYW6UjA1zcGxpdHRl clN0YXRllE6MC19fdmVyc2lvbl9flEsBdS4= gASVyw4AAAAAAAB9lCiMEmNvbnRyb2xBcmVhVmlzaWJsZZSIjBJjdXJyZW50U2NyaXB0SW5kZXiU SwCMEWxpYnJhcnlMaXN0U291cmNllF2UjDFvcmFuZ2Vjb250cmliLnRvbXdlci53aWRnZXRzLm90 aGVyLlB5dGhvblNjcmlwdE9XlIwGU2NyaXB0lJOUKYGUfZQojARuYW1llIwLSGVsbG8gd29ybGSU jAZzY3JpcHSUWLkGAABpbXBvcnQgb3MKaW1wb3J0IG51bXB5CmZyb20gdG9tb3NjYW4uZmFjdG9y eSBpbXBvcnQgRmFjdG9yeQpmcm9tIG5hYnUubWlzYy51dGlscyBpbXBvcnQgcmVzY2FsZV9kYXRh CgppZiBpbl9kYXRhIGlzIG5vdCBOb25lOgogICAgc2NhbiA9IGluX2RhdGEKICAgIAogICAgIyB3 YXJuaW5nOiBzY2FuLmxhdGVzdF9yZWNvbnN0cnVjdGlvbnMgaXMgZGVmaW5lZCBieSB0aGUgJ25h YnUgc2xpY2UgcmVjb25zdHJ1Y3Rpb24nLiBTbyBpZiBhIHNjYW4gaXMgbG9hZGVkIGFuZCBub3Qg Z28gdHJob3VnaCB0aGlzIHdpZGdldAogICAgIyBoZSB3b24ndCBiZSBhd2FyZSBhYm91dCBzbGlj ZSByZWNvbnN0cnVjdGlvbnMKICAgIHByaW50KCJjaGVjayIsIHNjYW4sICIgLSBsYXRlc3RfcmVj b25zdHJ1Y3Rpb25zIiwgc2Nhbi5sYXRlc3RfcmVjb25zdHJ1Y3Rpb25zKQogICAgaW5fdm9sdW1l cyA9IFsKICAgICAgICBGYWN0b3J5LmNyZWF0ZV90b21vX29iamVjdF9mcm9tX2lkZW50aWZpZXIo aWRlbnRpZmllcikgZm9yIGlkZW50aWZpZXIgaW4gc2Nhbi5sYXRlc3RfcmVjb25zdHJ1Y3Rpb25z CiAgICBdCgogICAgIyBsb29rcyBsaWtlIHRoZXJlIGlzIHNvbWUgZXh0cmEgdm9sdW1lIGV4aXN0 aW5nOiBmaWx0ZXIgdGhlbQogICAgcmVzID0gW10KICAgIGZvciB2b2wgaW4gaW5fdm9sdW1lczoK ICAgICAgICBpZiBvcy5wYXRoLmV4aXN0cyh2b2wuZmlsZV9wYXRoKToKICAgICAgICAgICAgcmVz LmFwcGVuZCh2b2wpCiAgICAKICAgIGluX3ZvbHVtZXMgPSByZXMKICAgIHByaW50KCJzbGljZXMg dG8gYmUgaGFuZGxlZCIsIGluX3ZvbHVtZXMpCgogICAgW3ByaW50KHZvbC5nZXRfaWRlbnRpZmll cigpLnRvX3N0cigpKSBmb3Igdm9sIGluIGluX3ZvbHVtZXNdCiAgICAKICAgICMgcmV0cmlldmUg dGhlIG1pbiAvIG1heCBmcm9tIHRoZSBmaXJzdCBhY3F1aXNpdGlvbgogICAgaWYgc2Nhbi5wYXRo LmVuZHN3aXRoKCJfMV8iKToKICAgICAgICBmb3Igdm9sIGluIGluX3ZvbHVtZXM6CiAgICAgICAg ICAgIHByaW50KCJyZWNvbXB1dGUgbWluIGFuZCBtYXggZnJvbSIsIHZvbC5maWxlX3BhdGgpCiAg ICAgICAgICAgIGRhdGEgPSB2b2wubG9hZF9kYXRhKCkKICAgICAgICAgICAgbmV3X21pbiwgbmV3 X21heCA9IG51bXB5LnBlcmNlbnRpbGUoZGF0YVswXSwgKDUsIDk1KSkKCiAgICAjIGFwcGx5IGNh c3QgLyByZWNsYW1pbmcgZGF0YQogICAgaWYgIm5ld19taW4iIGluIGdsb2JhbHMoKSBhbmQgIm5l d19tYXgiIGluIGdsb2JhbHMoKToKICAgICAgICBmb3Igdm9sIGluIGluX3ZvbHVtZXM6CiAgICAg ICAgICAgIHByaW50KCJub3JtYWxpemUiLCB2b2wuZmlsZV9wYXRoKQogICAgICAgICAgICB2b2wu bG9hZF9kYXRhKCkKICAgICAgICAgICAgZGF0YSA9IHZvbC5kYXRhCiAgICAgICAgICAgIGRhdGEg PSByZXNjYWxlX2RhdGEoCiAgICAgICAgICAgICAgICBkYXRhPWRhdGEsCiAgICAgICAgICAgICAg ICBuZXdfbWluPW5ld19taW4sCiAgICAgICAgICAgICAgICBuZXdfbWF4PW5ld19tYXgsCiAgICAg ICAgICAgICAgICBkYXRhX21pbj1kYXRhLm1pbigpLAogICAgICAgICAgICAgICAgZGF0YV9tYXg9 ZGF0YS5tYXgoKSwKICAgICAgICAgICAgKQogICAgICAgICAgICB2b2wuZGF0YSA9IGRhdGEKICAg ICAgICAgICAgdm9sLnNhdmVfZGF0YSgpCiAgICAgICAgCiAgICBvdXRfZGF0YSA9IHNjYW4KZWxz ZToKICAgIG91dF9kYXRhID0gaW5fZGF0YZSMBWZsYWdzlEsAjAhmaWxlbmFtZZROdWJhjBNzYXZl ZFdpZGdldEdlb21ldHJ5lENCAdnQywADAAAAAAHKAAAA5wAACf8AAAWfAAABygAAAOcAAAn/AAAF nwAAAAAAAAAACgAAAAHKAAAA5wAACf8AAAWflIwKc2NyaXB0VGV4dJRYuQYAAGltcG9ydCBvcwpp bXBvcnQgbnVtcHkKZnJvbSB0b21vc2Nhbi5mYWN0b3J5IGltcG9ydCBGYWN0b3J5CmZyb20gbmFi dS5taXNjLnV0aWxzIGltcG9ydCByZXNjYWxlX2RhdGEKCmlmIGluX2RhdGEgaXMgbm90IE5vbmU6 CiAgICBzY2FuID0gaW5fZGF0YQogICAgCiAgICAjIHdhcm5pbmc6IHNjYW4ubGF0ZXN0X3JlY29u c3RydWN0aW9ucyBpcyBkZWZpbmVkIGJ5IHRoZSAnbmFidSBzbGljZSByZWNvbnN0cnVjdGlvbicu IFNvIGlmIGEgc2NhbiBpcyBsb2FkZWQgYW5kIG5vdCBnbyB0cmhvdWdoIHRoaXMgd2lkZ2V0CiAg ICAjIGhlIHdvbid0IGJlIGF3YXJlIGFib3V0IHNsaWNlIHJlY29uc3RydWN0aW9ucwogICAgcHJp bnQoImNoZWNrIiwgc2NhbiwgIiAtIGxhdGVzdF9yZWNvbnN0cnVjdGlvbnMiLCBzY2FuLmxhdGVz dF9yZWNvbnN0cnVjdGlvbnMpCiAgICBpbl92b2x1bWVzID0gWwogICAgICAgIEZhY3RvcnkuY3Jl YXRlX3RvbW9fb2JqZWN0X2Zyb21faWRlbnRpZmllcihpZGVudGlmaWVyKSBmb3IgaWRlbnRpZmll ciBpbiBzY2FuLmxhdGVzdF9yZWNvbnN0cnVjdGlvbnMKICAgIF0KCiAgICAjIGxvb2tzIGxpa2Ug dGhlcmUgaXMgc29tZSBleHRyYSB2b2x1bWUgZXhpc3Rpbmc6IGZpbHRlciB0aGVtCiAgICByZXMg PSBbXQogICAgZm9yIHZvbCBpbiBpbl92b2x1bWVzOgogICAgICAgIGlmIG9zLnBhdGguZXhpc3Rz KHZvbC5maWxlX3BhdGgpOgogICAgICAgICAgICByZXMuYXBwZW5kKHZvbCkKICAgIAogICAgaW5f dm9sdW1lcyA9IHJlcwogICAgcHJpbnQoInNsaWNlcyB0byBiZSBoYW5kbGVkIiwgaW5fdm9sdW1l cykKCiAgICBbcHJpbnQodm9sLmdldF9pZGVudGlmaWVyKCkudG9fc3RyKCkpIGZvciB2b2wgaW4g aW5fdm9sdW1lc10KICAgIAogICAgIyByZXRyaWV2ZSB0aGUgbWluIC8gbWF4IGZyb20gdGhlIGZp cnN0IGFjcXVpc2l0aW9uCiAgICBpZiBzY2FuLnBhdGguZW5kc3dpdGgoIl8xXyIpOgogICAgICAg IGZvciB2b2wgaW4gaW5fdm9sdW1lczoKICAgICAgICAgICAgcHJpbnQoInJlY29tcHV0ZSBtaW4g YW5kIG1heCBmcm9tIiwgdm9sLmZpbGVfcGF0aCkKICAgICAgICAgICAgZGF0YSA9IHZvbC5sb2Fk X2RhdGEoKQogICAgICAgICAgICBuZXdfbWluLCBuZXdfbWF4ID0gbnVtcHkucGVyY2VudGlsZShk YXRhWzBdLCAoNSwgOTUpKQoKICAgICMgYXBwbHkgY2FzdCAvIHJlY2xhbWluZyBkYXRhCiAgICBp ZiAibmV3X21pbiIgaW4gZ2xvYmFscygpIGFuZCAibmV3X21heCIgaW4gZ2xvYmFscygpOgogICAg ICAgIGZvciB2b2wgaW4gaW5fdm9sdW1lczoKICAgICAgICAgICAgcHJpbnQoIm5vcm1hbGl6ZSIs IHZvbC5maWxlX3BhdGgpCiAgICAgICAgICAgIHZvbC5sb2FkX2RhdGEoKQogICAgICAgICAgICBk YXRhID0gdm9sLmRhdGEKICAgICAgICAgICAgZGF0YSA9IHJlc2NhbGVfZGF0YSgKICAgICAgICAg ICAgICAgIGRhdGE9ZGF0YSwKICAgICAgICAgICAgICAgIG5ld19taW49bmV3X21pbiwKICAgICAg ICAgICAgICAgIG5ld19tYXg9bmV3X21heCwKICAgICAgICAgICAgICAgIGRhdGFfbWluPWRhdGEu bWluKCksCiAgICAgICAgICAgICAgICBkYXRhX21heD1kYXRhLm1heCgpLAogICAgICAgICAgICAp CiAgICAgICAgICAgIHZvbC5kYXRhID0gZGF0YQogICAgICAgICAgICB2b2wuc2F2ZV9kYXRhKCkK ICAgICAgICAKICAgIG91dF9kYXRhID0gc2NhbgplbHNlOgogICAgb3V0X2RhdGEgPSBpbl9kYXRh lIwNc3BsaXR0ZXJTdGF0ZZROjAtfX3ZlcnNpb25fX5RLAXUu {'controlAreaVisible': True, 'dest_dir_settings': None, 'savedWidgetGeometry': b'\x01\xd9\xd0\xcb\x00\x03\x00\x00\x00\x00\x04>\x00\x00\x02h\x00\x00\x05\xad\x00\x00\x02\xe8\x00\x00\x04>\x00\x00\x02h\x00\x00\x05\xad\x00\x00\x02\xe8\x00\x00\x00\x00\x00\x00\x00\x00\n\x00\x00\x00\x04>\x00\x00\x02h\x00\x00\x05\xad\x00\x00\x02\xe8', '__version__': 1} {'controlAreaVisible': True, 'savedWidgetGeometry': b'\x01\xd9\xd0\xcb\x00\x03\x00\x00\x00\x00\x01\xaa\x00\x00\x00\x8b\x00\x00\x08\xbc\x00\x00\x04\x90\x00\x00\x01\xaa\x00\x00\x00\xb0\x00\x00\x08\xbc\x00\x00\x04\x90\x00\x00\x00\x00\x00\x00\x00\x00\n\x00\x00\x00\x01\xaa\x00\x00\x00\xb0\x00\x00\x08\xbc\x00\x00\x04\x90', '__version__': 1} ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1736265726.0 tomwer-1.4.8/src/orangecontrib/tomwer/tutorials/id16b/__init__.py0000644000175000017500000000000014737247776024161 0ustar00paynopayno././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739271825.0 tomwer-1.4.8/src/orangecontrib/tomwer/tutorials/simple_slice_reconstruction.ows0000644000175000017500000001052314752627221027501 0ustar00paynopayno provide dataset compute reduced darks and flats compute center of rotation define one or several slice to be reconstructed display reconstructed slices and browse dataset {'_scanIDs': [], 'controlAreaVisible': True, 'savedWidgetGeometry': None, '__version__': 1} {'_ewoks_default_inputs': {'data': None, 'dark_ref_params': None}, 'controlAreaVisible': True, 'savedWidgetGeometry': None, '__version__': 1} {'_ewoks_default_inputs': {'data': None, 'axis_params': {'MODE': 'manual', 'POSITION_VALUE': None, 'CALC_INPUT_TYPE': 'transmission_nopag', 'ANGLE_MODE': '0-180', 'USE_SINOGRAM': False, 'SINOGRAM_LINE': '', 'SINOGRAM_SUBSAMPLING': 10, 'AXIS_URL_1': '', 'AXIS_URL_2': '', 'LOOK_AT_STDMAX': False, 'NEAR_WX': 5, 'FINE_STEP_X': 0.1, 'SCALE_IMG2_TO_IMG1': False, 'NEAR_POSITION': 0.0, 'PADDING_MODE': 'edge', 'FLIP_LR': True, 'COMPOSITE_OPTS': {'theta': 10, 'n_subsampling_y': 10, 'oversampling': 4, 'take_log': True, 'near_pos': 0, 'near_width': 20}, 'SIDE': 'all', 'COR_OPTIONS': ''}, 'gui': {'mode_is_lock': False, 'value_is_lock': False, 'auto_update_estimated_cor': True}}, 'controlAreaVisible': True, 'savedWidgetGeometry': None, '__version__': 1} {'_ewoks_default_inputs': {'data': None, 'nabu_params': None}, 'controlAreaVisible': True, 'savedWidgetGeometry': None, '__version__': 1} {'_viewer_config': {}, 'controlAreaVisible': True, 'savedWidgetGeometry': None, '__version__': 1} ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739271825.0 tomwer-1.4.8/src/orangecontrib/tomwer/tutorials/simple_slice_reconstruction_on_slurm.ows0000644000175000017500000001467014752627221031426 0ustar00paynopayno provide dataset compute reduced darks and flats compute center of rotation slice reconstruction will be done on slurm as it has a 'slurm cluster' input display reconstructed slices and browse dataset Define the slurm partition to be used for the reconstruction. Warning: you must ask for one GPU wait for reconstruction to be done over slurm {'_scanIDs': [], 'controlAreaVisible': True, 'savedWidgetGeometry': None, '__version__': 1} {'_ewoks_default_inputs': {'data': None, 'dark_ref_params': None}, 'controlAreaVisible': True, 'savedWidgetGeometry': None, '__version__': 1} {'_ewoks_default_inputs': {'data': None, 'axis_params': {'MODE': 'manual', 'POSITION_VALUE': None, 'CALC_INPUT_TYPE': 'transmission_nopag', 'ANGLE_MODE': '0-180', 'USE_SINOGRAM': False, 'SINOGRAM_LINE': '', 'SINOGRAM_SUBSAMPLING': 10, 'AXIS_URL_1': '', 'AXIS_URL_2': '', 'LOOK_AT_STDMAX': False, 'NEAR_WX': 5, 'FINE_STEP_X': 0.1, 'SCALE_IMG2_TO_IMG1': False, 'NEAR_POSITION': 0.0, 'PADDING_MODE': 'edge', 'FLIP_LR': True, 'COMPOSITE_OPTS': {'theta': 10, 'n_subsampling_y': 10, 'oversampling': 4, 'take_log': True, 'near_pos': 0, 'near_width': 20}, 'SIDE': 'all', 'COR_OPTIONS': ''}, 'gui': {'mode_is_lock': False, 'value_is_lock': False, 'auto_update_estimated_cor': True}}, 'controlAreaVisible': True, 'savedWidgetGeometry': None, '__version__': 1} {'_ewoks_default_inputs': {'data': None, 'nabu_params': None}, 'controlAreaVisible': True, 'savedWidgetGeometry': None, '__version__': 1} {'_viewer_config': {}, 'controlAreaVisible': True, 'savedWidgetGeometry': None, '__version__': 1} {'_ewoks_default_inputs': {}, 'controlAreaVisible': True, 'savedWidgetGeometry': b'\x01\xd9\xd0\xcb\x00\x03\x00\x00\x00\x00\nH\x00\x00\x01l\x00\x00\x0c#\x00\x00\x02\xce\x00\x00\nH\x00\x00\x01\x91\x00\x00\x0c#\x00\x00\x02\xce\x00\x00\x00\x01\x00\x00\x00\x00\x07\x80\x00\x00\nH\x00\x00\x01\x91\x00\x00\x0c#\x00\x00\x02\xce', '__version__': 1} {'_ewoks_default_inputs': {}, 'controlAreaVisible': True, 'savedWidgetGeometry': None, '__version__': 1} ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739271825.0 tomwer-1.4.8/src/orangecontrib/tomwer/tutorials/simple_volume_local_reconstruction.ows0000644000175000017500000001347114752627221031070 0ustar00paynopayno provide dataset compute reduced darks and flats compute center of rotation define one or several slice to be reconstructed display reconstructed slices and browse dataset browse reconstructed volume reconstruct volume {'_scanIDs': [], 'controlAreaVisible': True, 'savedWidgetGeometry': None, '__version__': 1} {'_ewoks_default_inputs': {'data': None, 'dark_ref_params': None}, 'controlAreaVisible': True, 'savedWidgetGeometry': None, '__version__': 1} {'_ewoks_default_inputs': {'data': None, 'axis_params': {'MODE': 'manual', 'POSITION_VALUE': None, 'CALC_INPUT_TYPE': 'transmission_nopag', 'ANGLE_MODE': '0-180', 'USE_SINOGRAM': False, 'SINOGRAM_LINE': '', 'SINOGRAM_SUBSAMPLING': 10, 'AXIS_URL_1': '', 'AXIS_URL_2': '', 'LOOK_AT_STDMAX': False, 'NEAR_WX': 5, 'FINE_STEP_X': 0.1, 'SCALE_IMG2_TO_IMG1': False, 'NEAR_POSITION': 0.0, 'PADDING_MODE': 'edge', 'FLIP_LR': True, 'COMPOSITE_OPTS': {'theta': 10, 'n_subsampling_y': 10, 'oversampling': 4, 'take_log': True, 'near_pos': 0, 'near_width': 20}, 'SIDE': 'all', 'COR_OPTIONS': ''}, 'gui': {'mode_is_lock': False, 'value_is_lock': False, 'auto_update_estimated_cor': True}}, 'controlAreaVisible': True, 'savedWidgetGeometry': None, '__version__': 1} {'_ewoks_default_inputs': {'data': None, 'nabu_params': None}, 'controlAreaVisible': True, 'savedWidgetGeometry': None, '__version__': 1} {'_viewer_config': {}, 'controlAreaVisible': True, 'savedWidgetGeometry': None, '__version__': 1} {'_ewoks_default_inputs': {'data': None, 'nabu_volume_params': None, 'nabu_params': None}, 'controlAreaVisible': True, 'savedWidgetGeometry': None, '__version__': 1} {'controlAreaVisible': True, 'savedWidgetGeometry': None, '__version__': 1} ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739271825.0 tomwer-1.4.8/src/orangecontrib/tomwer/tutorials/simple_volume_to_slurm_reconstruction.ows0000644000175000017500000002170714752627221031643 0ustar00paynopayno provide dataset compute reduced darks and flats compute center of rotation define one or several slice to be reconstructed display reconstructed slices and browse dataset browse reconstructed volume reconstruct volume wait for the slurm job to be finished define slurm node to be used {'_scanIDs': [], 'controlAreaVisible': True, 'savedWidgetGeometry': None, '__version__': 1} {'_ewoks_default_inputs': {'data': None, 'dark_ref_params': None}, 'controlAreaVisible': True, 'savedWidgetGeometry': None, '__version__': 1} {'_ewoks_default_inputs': {'data': None, 'axis_params': {'MODE': 'manual', 'POSITION_VALUE': None, 'CALC_INPUT_TYPE': 'transmission_nopag', 'ANGLE_MODE': '0-180', 'USE_SINOGRAM': False, 'SINOGRAM_LINE': '', 'SINOGRAM_SUBSAMPLING': 10, 'AXIS_URL_1': '', 'AXIS_URL_2': '', 'LOOK_AT_STDMAX': False, 'NEAR_WX': 5, 'FINE_STEP_X': 0.1, 'SCALE_IMG2_TO_IMG1': False, 'NEAR_POSITION': 0.0, 'PADDING_MODE': 'edge', 'FLIP_LR': True, 'COMPOSITE_OPTS': {'theta': 10, 'n_subsampling_y': 10, 'oversampling': 4, 'take_log': True, 'near_pos': 0, 'near_width': 20}, 'SIDE': 'all', 'COR_OPTIONS': ''}, 'gui': {'mode_is_lock': False, 'value_is_lock': False, 'auto_update_estimated_cor': True}}, 'controlAreaVisible': True, 'savedWidgetGeometry': None, '__version__': 1} {'_ewoks_default_inputs': {'data': None, 'nabu_params': None}, 'controlAreaVisible': True, 'savedWidgetGeometry': None, '__version__': 1} {'_viewer_config': {}, 'controlAreaVisible': True, 'savedWidgetGeometry': None, '__version__': 1} {'_ewoks_default_inputs': {'data': None, 'nabu_volume_params': None, 'nabu_params': None}, 'controlAreaVisible': True, 'savedWidgetGeometry': None, '__version__': 1} {'controlAreaVisible': True, 'savedWidgetGeometry': None, '__version__': 1} {'_ewoks_default_inputs': {}, 'controlAreaVisible': True, 'savedWidgetGeometry': None, '__version__': 1} {'_ewoks_default_inputs': {}, '_ewoks_execinfo': {}, '_ewoks_varinfo': {}, 'controlAreaVisible': True, 'default_inputs': {}, 'savedWidgetGeometry': None, '__version__': 1} {'_ewoks_default_inputs': {}, '_ewoks_execinfo': {}, '_ewoks_varinfo': {}, 'controlAreaVisible': True, 'default_inputs': {}, 'savedWidgetGeometry': None, '__version__': 1} ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739271825.0 tomwer-1.4.8/src/orangecontrib/tomwer/tutorials/using_saaxis_to_find_cor.ows0000644000175000017500000001211014752627221026724 0ustar00paynopayno provide dataset compute reduced darks and flats compute center of rotation refine COR reconstruct one or several slices browse reconstructed slices and browse dataset {'_scanIDs': [], 'controlAreaVisible': True, 'savedWidgetGeometry': None, '__version__': 1} {'_ewoks_default_inputs': {'data': None, 'dark_ref_params': None}, 'controlAreaVisible': True, 'savedWidgetGeometry': None, '__version__': 1} {'_ewoks_default_inputs': {'data': None, 'axis_params': {'MODE': 'manual', 'POSITION_VALUE': None, 'CALC_INPUT_TYPE': 'transmission_nopag', 'ANGLE_MODE': '0-180', 'USE_SINOGRAM': False, 'SINOGRAM_LINE': '', 'SINOGRAM_SUBSAMPLING': 10, 'AXIS_URL_1': '', 'AXIS_URL_2': '', 'LOOK_AT_STDMAX': False, 'NEAR_WX': 5, 'FINE_STEP_X': 0.1, 'SCALE_IMG2_TO_IMG1': False, 'NEAR_POSITION': 0.0, 'PADDING_MODE': 'edge', 'FLIP_LR': True, 'COMPOSITE_OPTS': {'theta': 10, 'n_subsampling_y': 10, 'oversampling': 4, 'take_log': True, 'near_pos': 0, 'near_width': 20}, 'SIDE': 'all', 'COR_OPTIONS': ''}, 'gui': {'mode_is_lock': False, 'value_is_lock': False, 'auto_update_estimated_cor': True}}, 'controlAreaVisible': True, 'savedWidgetGeometry': None, '__version__': 1} {'_ewoks_default_inputs': {'data': None, 'sa_axis_params': None}, 'controlAreaVisible': True, 'savedWidgetGeometry': None, '__version__': 1} {'_ewoks_default_inputs': {'data': None, 'nabu_params': None}, 'controlAreaVisible': True, 'savedWidgetGeometry': None, '__version__': 1} {'_viewer_config': {}, 'controlAreaVisible': True, 'savedWidgetGeometry': None, '__version__': 1} ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739271825.0 tomwer-1.4.8/src/orangecontrib/tomwer/tutorials/volume_casting_on_slurm.ows0000644000175000017500000001662014752627221026631 0ustar00paynopayno cast a volume remotly (using slurm) Do the volume casting remotly Define the node configuration to do the casting. WARNING: casting is a pure IO operation. No need any GPU here. Wait for casting to be done. Needed if you want to resume processing once casting is done Alternative input to the cast volume. If you want to launch it in post-processing {'_scanIDs': [], 'controlAreaVisible': True, 'savedWidgetGeometry': None, '__version__': 1} {'_ewoks_default_inputs': {'data': None, 'dark_ref_params': None}, 'controlAreaVisible': True, 'savedWidgetGeometry': None, '__version__': 1} {'_ewoks_default_inputs': {'data': None, 'axis_params': {'MODE': 'manual', 'POSITION_VALUE': None, 'CALC_INPUT_TYPE': 'transmission_nopag', 'ANGLE_MODE': '0-180', 'USE_SINOGRAM': False, 'SINOGRAM_LINE': '', 'SINOGRAM_SUBSAMPLING': 10, 'AXIS_URL_1': '', 'AXIS_URL_2': '', 'LOOK_AT_STDMAX': False, 'NEAR_WX': 5, 'FINE_STEP_X': 0.1, 'SCALE_IMG2_TO_IMG1': False, 'NEAR_POSITION': 0.0, 'PADDING_MODE': 'edge', 'FLIP_LR': True, 'COMPOSITE_OPTS': {'theta': 10, 'n_subsampling_y': 10, 'oversampling': 4, 'take_log': True, 'near_pos': 0, 'near_width': 20}, 'SIDE': 'all', 'COR_OPTIONS': ''}, 'gui': {'mode_is_lock': False, 'value_is_lock': False, 'auto_update_estimated_cor': True}}, 'controlAreaVisible': True, 'savedWidgetGeometry': None, '__version__': 1} {'_ewoks_default_inputs': {'data': None, 'nabu_params': None}, 'controlAreaVisible': True, 'savedWidgetGeometry': None, '__version__': 1} {'_ewoks_default_inputs': {'data': None, 'nabu_volume_params': None, 'nabu_params': None}, 'controlAreaVisible': True, 'savedWidgetGeometry': None, '__version__': 1} {'_ewoks_default_inputs': {'data': None, 'cast_volume_params': {}}, 'controlAreaVisible': True, 'savedWidgetGeometry': None, '__version__': 1} {'_ewoks_default_inputs': {}, 'controlAreaVisible': True, 'savedWidgetGeometry': b'\x01\xd9\xd0\xcb\x00\x03\x00\x00\x00\x00\x02\xea\x00\x00\x01W\x00\x00\x04\x81\x00\x00\x03\x08\x00\x00\x02\xea\x00\x00\x01W\x00\x00\x04\x81\x00\x00\x03\x08\x00\x00\x00\x00\x00\x00\x00\x00\x07\x80\x00\x00\x02\xea\x00\x00\x01W\x00\x00\x04\x81\x00\x00\x03\x08', '__version__': 1} {'_ewoks_default_inputs': {}, '_ewoks_execinfo': {}, '_ewoks_task_options': {}, '_ewoks_varinfo': {}, 'controlAreaVisible': True, 'default_inputs': {}, 'savedWidgetGeometry': None, '__version__': 1} {'_scanIDs': [], 'controlAreaVisible': True, 'savedWidgetGeometry': None, '__version__': 1} ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1739271865.8717635 tomwer-1.4.8/src/orangecontrib/tomwer/widgets/0000755000175000017500000000000014752627272020563 5ustar00paynopayno././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1737644878.0 tomwer-1.4.8/src/orangecontrib/tomwer/widgets/__init__.py0000644000175000017500000000227614744455516022704 0ustar00paynopayno""" (Ewoks)Orange Widgets dedicated for tomography """ from ewoksorange.pkg_meta import get_distribution ICON = "../widgets/icons/tomwer.png" BACKGROUND = "#C0CCFF" # Entry point for main Orange categories/widgets discovery def widget_discovery(discovery): dist = get_distribution("tomwer") pkgs = [ "orangecontrib.tomwer.widgets.cluster", "orangecontrib.tomwer.widgets.control", "orangecontrib.tomwer.widgets.debugtools", "orangecontrib.tomwer.widgets.edit", "orangecontrib.tomwer.widgets.dataportal", "orangecontrib.tomwer.widgets.reconstruction", # "orangecontrib.tomwer.widgets.stitching", "orangecontrib.tomwer.widgets.visualization", "orangecontrib.tomwer.widgets.other", ] for pkg in pkgs: discovery.process_category_package(pkg, distribution=dist) WIDGET_HELP_PATH = ( # Used for development. # You still need to build help pages using # make htmlhelp # inside doc folder # ( # "/home/payno/Documents/dev/tomography/tomwer/build/html/canvas/widgets/widgets.html", # None, # ), ("https://tomwer.readthedocs.io/en/latest/canvas/widgets/widgets.html", None), ) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1739271865.8717635 tomwer-1.4.8/src/orangecontrib/tomwer/widgets/cluster/0000755000175000017500000000000014752627272022244 5ustar00paynopayno././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739271825.0 tomwer-1.4.8/src/orangecontrib/tomwer/widgets/cluster/FutureSupervisorOW.py0000644000175000017500000001776014752627221026445 0ustar00paynopaynofrom __future__ import annotations import time from orangewidget import gui # from orangewidget.widget import Input, Output, OWBaseWidget from orangecontrib.tomwer.orange.managedprocess import TomwerWithStackStack from ewokscore.missing_data import MISSING_DATA from ewoksorange.gui.orange_imports import Input from processview.core.manager import DatasetState, ProcessManager from processview.core.superviseprocess import SuperviseProcess from silx.gui import qt from silx.gui.utils.concurrent import submitToQtMainThread from tomwer.core.futureobject import FutureTomwerObject from tomwer.gui.cluster.supervisor import ( FutureTomwerScanObserverWidget as _FutureTomwerScanObserverWidget, ) from tomwer.core.process.cluster.supervisor import FutureSupervisorTask class FutureSupervisorOW( TomwerWithStackStack, ewokstaskclass=FutureSupervisorTask, ): """ Orange widget to define a slurm cluster as input of other widgets (based on nabu for now) """ name = "future supervisor" id = "orange.widgets.tomwer.cluster.FutureSupervisorOW.FutureSupervisorOW" description = "Observe slurm job registered." icon = "icons/slurmobserver.svg" priority = 22 keywords = [ "tomography", "tomwer", "slurm", "observer", "cluster", "job", "sbatch", "supervisor", "future", ] want_main_area = True want_control_area = False resizing_enabled = True class Inputs: # redefine the input to allow multiple and default future_tomo_obj = Input( name="future_tomo_obj", type=FutureTomwerObject, doc="data with some remote processing", multiple=True, default=True, ) def __init__(self, parent=None): super().__init__(parent) # gui layout = gui.vBox(self.mainArea, self.name).layout() self._widget = FutureTomwerObjectObserverWidget( parent=self, name=self.windowTitle() ) layout.addWidget(self._widget) # connect signal / slot self._widget.observationTable.model().sigStatusUpdated.connect( self._convertBackAutomatically ) self._widget.sigConversionRequested.connect(self._convertBack) def convertWhenFinished(self): return self._widget.convertWhenFinished() def _convertBackAutomatically(self, future_tomo_obj, status): if not isinstance(future_tomo_obj, FutureTomwerObject): raise TypeError( f"future_tomo_obj is expected to be an instance of {FutureTomwerObject} and not {type(future_tomo_obj)}" ) if status in ("finished", "completed") and self.convertWhenFinished(): self._convertBack(future_tomo_obj) def _convertBack(self, future_tomo_obj): if not isinstance(future_tomo_obj, FutureTomwerObject): raise TypeError( f"future_tomo_obj is expected to be an instance of {FutureTomwerObject} and not {type(future_tomo_obj)}" ) self._widget.removeFutureTomoObj(future_tomo_obj=future_tomo_obj) self.execute_ewoks_task() def handleNewSignals(self) -> None: """Invoked by the workflow signal propagation manager after all signals handlers have been called. note: this widget can receive two signals: 'dataset' and 'colormap'. The 'colormap' is handled by orange directly while the 'dataset' signal is handled by the ewoks task. This function will be only triggered when the 'dataset' signal is send """ # update GUI from received future_tomo_obj # warning: this code will work because the task has only one input. # so we can pass it directly to the widget. # this won't be the case the task can have several input. future_tomo_obj = self.get_task_input_value("future_tomo_obj", MISSING_DATA) if future_tomo_obj is not MISSING_DATA: self._widget.addFutureTomoObj(future_tomo_obj=future_tomo_obj) @Inputs.future_tomo_obj def add(self, future_tomo_obj, signal_id=None): # required because today ewoksorange is not handling multiple inputs self.set_dynamic_input("future_tomo_obj", future_tomo_obj) class FutureTomwerObjectObserverWidget( _FutureTomwerScanObserverWidget, SuperviseProcess ): """add dataset state notification (ProcessManager) to the original FutureTomwerScanObserverWidget""" REFRESH_FREQUENCE = 10 """time between call to updateView""" def __init__(self, name, parent=None): super().__init__(parent=parent) self.name = name self._updateThread = _RefreshThread( callback=self.updateView, refresh_frequence=self.REFRESH_FREQUENCE ) self.destroyed.connect(self.stopRefresh) self._updateThread.start() def stopRefresh(self): if self._updateThread is not None: self._updateThread.stop() self._updateThread.wait(self.REFRESH_FREQUENCE + 1) self._updateThread = None def close(self): self.stopRefresh() super().close() def addFutureTomoObj(self, future_tomo_obj: FutureTomwerObject): super().addFutureTomoObj(future_tomo_obj) self._updateTomoObjSupervisor(future_tomo_obj) def removeFutureTomoObj(self, future_tomo_obj: FutureTomwerObject): self._updateTomoObjSupervisor(future_tomo_obj) super().removeFutureTomoObj(future_tomo_obj) def _updateTomoObjSupervisor(self, future_tomo_obj): r_id = future_tomo_obj.process_requester_id if r_id is not None: requester_name = ProcessManager().get_process(r_id).name else: requester_name = "unknow" details = f"job spawn by {requester_name}" if future_tomo_obj is None: return elif future_tomo_obj.status == "error": state = DatasetState.FAILED elif future_tomo_obj.status == "pending": details = "\n".join([details, "pending"]) state = DatasetState.PENDING elif future_tomo_obj.status in ("finished", "completed"): details = future_tomo_obj.logs or "no log found" state = DatasetState.SUCCEED elif future_tomo_obj.status == "running": details = "\n".join([details, "running"]) state = DatasetState.ON_GOING elif future_tomo_obj.status == "cancelled": details = "\n".join([details, "job cancelled"]) state = DatasetState.SKIPPED elif future_tomo_obj.status is None: return else: raise ValueError( f"future scan status '{future_tomo_obj.status}' is not managed, {type(future_tomo_obj.status)}" ) ProcessManager().notify_dataset_state( dataset=future_tomo_obj.tomo_obj, process=self, state=state, details=details, ) def _updateStatus(self, future_tomo_obj): self._updateTomoObjSupervisor(future_tomo_obj) super()._updateStatus(future_tomo_obj) class _RefreshThread(qt.QThread): """Simple thread to call a refresh callback each refresh_frequence (seconds)""" TIME_BETWEEN_LOOP = 1.0 def __init__(self, callback, refresh_frequence) -> None: super().__init__() self._callback = callback self._refresh_frequence = refresh_frequence self._stop = False def stop(self): self._stop = True self._callback = None def run(self): w_t = self._refresh_frequence + self.TIME_BETWEEN_LOOP while not self._stop: if w_t <= 0: if self._callback is not None: try: submitToQtMainThread(self._callback) except AttributeError: # can happen when closing pass w_t = self._refresh_frequence + self.TIME_BETWEEN_LOOP w_t -= self.TIME_BETWEEN_LOOP time.sleep(self.TIME_BETWEEN_LOOP) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739271825.0 tomwer-1.4.8/src/orangecontrib/tomwer/widgets/cluster/SlurmClusterOW.py0000644000175000017500000000500514752627221025522 0ustar00paynopaynofrom __future__ import annotations import logging from silx.gui import qt from orangewidget import gui from orangewidget.settings import Setting from orangewidget.widget import Output, OWBaseWidget from tomwer.core.cluster import SlurmClusterConfiguration from tomwer.gui.cluster.slurm import SlurmSettingsWindow _logger = logging.getLogger(__name__) class SlurmClusterOW(OWBaseWidget, openclass=True): """ Orange widget to define a slurm cluster as input of other widgets (based on nabu for now) """ name = "slurm cluster" id = "orange.widgets.tomwer.cluster.SlurmClusterOW.SlurmClusterOW" description = "Let the user configure the cluster to be used." icon = "icons/slurm.svg" priority = 20 keywords = ["tomography", "tomwer", "slurm", "cluster", "configuration"] want_main_area = True want_control_area = False resizing_enabled = True _ewoks_default_inputs = Setting(dict()) class Outputs: config_out = Output(name="cluster_config", type=SlurmClusterConfiguration) def __init__(self, parent=None) -> None: super().__init__(parent) layout = gui.vBox(self.mainArea, self.name).layout() self._widget = SlurmSettingsWindow(parent=self) self._widget.setWindowFlags(qt.Qt.Widget) layout.addWidget(self._widget) if self._ewoks_default_inputs != {}: self._widget.setConfiguration(self._ewoks_default_inputs) # trigger the signal to avoid any user request self.Outputs.config_out.send(self.getConfiguration()) # connect signal / slot self._widget.sigConfigChanged.connect(self._configurationChanged) def __new__(cls, *args, **kwargs): # ensure backward compatibility with 'static_input' static_input = kwargs.get("stored_settings", {}).get("_static_input", None) if static_input not in (None, {}): _logger.warning( "static_input has been deprecated. Will be replaced by _ewoks_default_inputs in the workflow file. Please save the workflow to apply modifications" ) kwargs["stored_settings"]["_ewoks_default_inputs"] = static_input return super().__new__(cls, *args, **kwargs) def _configurationChanged(self): slurmClusterconfiguration = self.getConfiguration() self.Outputs.config_out.send(slurmClusterconfiguration) self._ewoks_default_inputs = slurmClusterconfiguration.to_dict() def getConfiguration(self): return self._widget.getSlurmClusterConfiguration() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1737644878.0 tomwer-1.4.8/src/orangecontrib/tomwer/widgets/cluster/__init__.py0000644000175000017500000000060114744455516024353 0ustar00paynopayno""" (Ewoks)Orange Widgets for remote processing through slurm """ NAME = "compute cluster" ID = "orangecontrib.tomwer.widgets.cluster" DESCRIPTION = "widgets to submit jobs to cluster" LONG_DESCRIPTION = ( "widgets to submit slurm job, observe job and convert them back to tomwer object" ) ICON = "../../widgets/icons/tomwer_cluster.png" BACKGROUND = "#C0CCFF" PRIORITY = 5 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1739271865.8757637 tomwer-1.4.8/src/orangecontrib/tomwer/widgets/cluster/icons/0000755000175000017500000000000014752627272023357 5ustar00paynopayno././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1731088693.0 tomwer-1.4.8/src/orangecontrib/tomwer/widgets/cluster/icons/slurm.png0000644000175000017500000000371314713450465025226 0ustar00paynopaynoPNG  IHDR- DsBIT|d pHYs+tEXtSoftwarewww.inkscape.org<HIDATXklT2Ƌ)xi!-Q&D+`XGãHѪPTFJR( ԤTJ5nZb(ͣt]zN ̟{Μ3߽1s>5 JaLg_0jwቾ .Gu7}(¦R9~F:|J!=y7'A# >r;n&mpo2F $3їB]l uiD.O}ϡ-6 ̊+L(Y\.3{2wOի*5N 6.v#} BUPl X0bĿy ' w''jN!L7:#vML',(0L>:&e0@/F3(:'MeW|L?-/px %Έ~i_ IY0#yBI$rpb!|jg6vO``J/!|ZV`ir &o騘B   ;WiĈHVGrxЊ=9M8dr=Qy * 47:>%= "%r!0 M4# mSA{q,'php*([BacH - CT b j 0+T17L 3i !8v(ggo0{PiT>f$QH䞮B(IUijTK\h4=>" LRi^V*Mi5uj7 L3W`Wmo 2 DW(a,n,U&Pۘ(#ݏP(6:0(VYh+.Ϗ̐MDOJibq/JLq4),e"_ S#)a%`58]\IL*cԌ:a DI$@ vɶlٲ0 _yyO:Y 6e5ZgK<6--R/]تꀾɺZo߾}_z_X,]#G|rȑ#?+//KKKظq㬛7o>z=g~=I6m^_TT_wttLl6553f}޼y+z=ϱW~5;;Bii+.];wO,^eme{\y---9-Z`>o`kkDQܒ1](X]]… 6:O^r2~;vlĉwfff6oCzYׯ_}Ƿ:M2e(%KRźf͚euG}3dȐ?y<]ɭ[z;|ӧ%cOԸnuv>@^^ɓ'7$gc}ykܸqI&5%,D\~~SNM8qwVVV4wݜVSDP`%""nIȺU!=U*"~"'mmmYA\χvWLwܹ^X}&=ꖮZjѣOdeew߾}/_5jԧɶ"rcٲeUUU߹sg֭[\dBↆ9q}ʕϟ.@nnnawc+l6/ image/svg+xml slurm ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1731088693.0 tomwer-1.4.8/src/orangecontrib/tomwer/widgets/cluster/icons/slurmobserver.png0000644000175000017500000000621414713450465026775 0ustar00paynopaynoPNG  IHDR,+jsBIT|d pHYs+tEXtSoftwarewww.inkscape.org< IDATX͙{pTU#ФCI@% f$0$* R": P2s{uf,uft & Ef)P0$B GIsCh1-!:u:Ugwz-9D`p\@  )`& /NvGLPڻ{ll?z&(7&z {@'aaa9pӹ]iik3^:UJi8j{KXC `#B92<_---CU)E_c6#f IJZ^U]jT'N 0XK/Z > 9nzb25)#ҾTUʨ4GssPg髯\ `aoB\b! _@+5E ,x˖GΜ=m0C\ti8 3{"rʕ;viZd'L#)XxfTeקDEF6;p`Nӯ,g/ع!Enׇ d)Y[KX,-V3gΞ;7. l6DW765ipk]ZqtLYt`#p>˅%%]f*Vic~cÙ_}A֤jUu:]vxtC۽Fm=)#H??4T3"u=AIo#Kej qy 7B=Ik˹Y%%^zَNx.a`gyh4:|}|ؽK^*;ՕK{wǞN 0`4PL#' FhZ^Q:qDVzA~ɘK %xJ /Ve$OREvnNwYvkEwwˀp[z/P 40 YQU5]>ָ{\G?RsW5U\:ĉoDoQAs LW,>wNct9sc@$ت39ب3.YnkĨ +S2wnom ~Ckq}ZZwd{zL~Ca6Yy=0|ى#A< #bϯ/ =2Au"h"KLfp` Η=!)`z+~gkmZWsh ׫UW4B~ٙ$BO|% nw^%Z-ZG>S"+.}8&,ioWw=mlvvvNeO5h0$s.{g]!DOAGJ雐pzΜ91{쿔FvO2!DÇGvs,dɒK.ao>nذa+J&2))~bŊWNZ^ !ĩǏ߷gϞ' +''y! ̬BȜÇ>}jO_*ɵFvcl벲mV|VIENDB`././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1731088693.0 tomwer-1.4.8/src/orangecontrib/tomwer/widgets/cluster/icons/slurmobserver.svg0000644000175000017500000015723014713450465027015 0ustar00paynopayno image/svg+xml ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1739271865.8757637 tomwer-1.4.8/src/orangecontrib/tomwer/widgets/cluster/tests/0000755000175000017500000000000014752627272023406 5ustar00paynopayno././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1736265726.0 tomwer-1.4.8/src/orangecontrib/tomwer/widgets/cluster/tests/__init__.py0000644000175000017500000000000014737247776025517 0ustar00paynopayno././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1739271865.8757637 tomwer-1.4.8/src/orangecontrib/tomwer/widgets/control/0000755000175000017500000000000014752627272022243 5ustar00paynopayno././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1736265726.0 tomwer-1.4.8/src/orangecontrib/tomwer/widgets/control/AdvancementOW.py0000644000175000017500000000212414737247776025321 0ustar00paynopaynofrom __future__ import annotations import logging from orangewidget import gui, widget from processview.gui.processmanager import ProcessManagerWindow logger = logging.getLogger(__name__) class AdvancementOW(widget.OWBaseWidget, openclass=True): """ A simple widget managing the copy of an incoming folder to an other one :param parent: the parent widget """ # note of this widget should be the one registred on the documentation name = "advancement" id = "orangecontrib.widgets.tomwer.control.AdvancementOW.AdvancementOW" description = "This widget can display advancement of processes and scans" icon = "icons/advancement.svg" priority = 5 keywords = ["tomography", "process", "advancement"] want_main_area = True want_control_area = False resizing_enabled = True def __init__(self, parent=None): widget.OWBaseWidget.__init__(self, parent) self._widget = ProcessManagerWindow(parent=self) self._box = gui.vBox(self.mainArea, self.name) layout = self._box.layout() layout.addWidget(self._widget) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739271825.0 tomwer-1.4.8/src/orangecontrib/tomwer/widgets/control/DataDiscoveryOW.py0000644000175000017500000001754714752627221025634 0ustar00paynopaynofrom __future__ import annotations import fnmatch import logging import os import h5py from orangewidget import gui, widget from orangewidget.widget import Output from silx.gui import qt from orangewidget.settings import Setting import tomwer.core.process.control.datadiscovery from orangecontrib.tomwer.widgets.utils import WidgetLongProcessing from tomwer.core.scan.blissscan import BlissScan from tomwer.core.scan.edfscan import EDFTomoScan from tomwer.core.scan.nxtomoscan import NXtomoScan from tomwer.core.scan.scanbase import TomwerScanBase from tomwer.core.scan.scanfactory import ScanFactory from tomwer.core.scan.scantype import ScanType from tomwer.gui.control.datadiscovery import DataDiscoveryWidget logger = logging.getLogger(__name__) class DataDiscoveryOW(widget.OWBaseWidget, WidgetLongProcessing, openclass=True): """ This widget will browse a folder and sub folder to find valid tomo scan project. Contrary to the scan watcher it will parse all folder / sub folders then stop. """ name = "scan discovery" id = "orangecontrib.widgets.tomwer.control.DataDiscoveryOW.DataDiscoveryOW" description = ( "This widget will browse a folder and sub folder to find valid tomo scan project. \n" "Contrary to the scan watcher it will parse all folder / sub folders then stop." ) icon = "icons/datadiscover.svg" priority = 11 keywords = [ "tomography", "tomwer", "datadiscovery", "data", "discovery", "search", "research", "hdf5", "NXtomo", ] want_main_area = True want_control_area = False resizing_enabled = True ewokstaskclass = tomwer.core.process.control.datadiscovery._DataDiscoveryPlaceHolder class Outputs: data = Output(name="data", type=TomwerScanBase, doc="one scan to be process") settings = Setting(dict()) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._widget = DataDiscoveryWidget(parent=self) self._box = gui.vBox(self.mainArea, self.name) layout = self._box.layout() layout.addWidget(self._widget) self._widget.setConfiguration(self.settings) # getting ready for processing self._processingThread = _DiscoverThread() # connect signal / slot self._processingThread.sigDataFound.connect(self._sendData) self._processingThread.finished.connect(self._endProcessing) self._widget.widget.controlWidget._qpbstartstop.released.connect( self._switchObservation ) # dedicated one for settings self._widget.widget.controlWidget._filterQLE.editingFinished.connect( self._updatesettings ) self._widget.widget.controlWidget._qteFolderSelected.editingFinished.connect( self._updatesettings ) self._widget.widget.configWidget.sigScanTypeChanged.connect( self._updatesettings ) def _updatesettings(self): self.settings = self.getConfiguration() # expose some API def getConfiguration(self) -> dict: return self._widget.getConfiguration() def setConfiguration(self, config: dict): self._widget.setConfiguration(config) def getProcessingThread(self) -> qt.QThread: return self._processingThread def setFolderObserved(self, dir_: str): self._widget.widget.setFolderObserved(dir_) def setSearchScanType(self, scan_type): self._widget.widget.setSearchScanType(scan_type) def setFilePattern(self, pattern: str | None): self._widget.widget.setLinuxFilePattern(pattern) def _switchObservation(self, *args, **kwargs): """stop or start the disceovery according to the thread state""" thread = self.getProcessingThread() if thread.isRunning(): thread.quit() self._endProcessing() else: self.start_discovery() def start_discovery(self, wait: int | None = None): """start the discovery of scans :param wait: optional waiting time in second """ thread = self.getProcessingThread() if thread.isRunning(): logger.warning("Discovery is already running") return self._startProcessing() thread.setConfiguration(self.getConfiguration()) thread.start() if wait is not None: thread.wait(wait) def _sendData(self, data): if data is not None: self.Outputs.data.send(data) def _startProcessing(self, *args, **kwargs): self._widget.widget.controlWidget._qpbstartstop.setText("stop discovery") return super()._startProcessing(*args, **kwargs) def _endProcessing(self, *args, **kwargs): self._widget.widget.controlWidget._qpbstartstop.setText("start discovery") return super()._endProcessing(*args, **kwargs) class _DiscoverThread(qt.QThread): """ Thread to browse folder and sub folder and looking for some scan """ sigDataFound = qt.Signal(object) """emit each time a new dataset is found""" def __init__(self, **kwargs) -> None: super().__init__(**kwargs) self._root_dir = None self._research_type = None self._file_filter = None self._look_for_hdf5 = False def setConfiguration(self, config: dict): self._root_dir = config.get("start_folder", None) self._research_type = ScanType.from_value(config["scan_type_searched"]) self._file_filter = config.get("file_filter", None) self._look_for_hdf5 = self._research_type in ( ScanType.BLISS, ScanType.NX_TOMO, ) def run(self): if self._research_type is None: logger.error("No reserach type defined. Cannot research scan") elif not os.path.isdir(self._root_dir): logger.error(f"{self._root_dir} is not a directory") else: self.discover_scan(file_path=self._root_dir) def discover_scan(self, file_path): if ( os.path.isfile(file_path) and self._look_for_hdf5 and h5py.is_hdf5(file_path) ) or (os.path.isdir(file_path) and not self._look_for_hdf5): try: name_match = self._file_filter is None or fnmatch.fnmatch( os.path.basename(file_path), self._file_filter ) except Exception as e: logger.error(f"fnmatch fail. Error is {e}") name_match = True if name_match: self._treat_path(file_path) if os.path.isdir(file_path): [ self.discover_scan(file_path=os.path.join(file_path, file_)) for file_ in os.listdir(file_path) ] def _treat_path(self, folder_path: str): """treat file / folder at a specific location""" try: scan_objs = ScanFactory.create_scan_objects( scan_path=folder_path, accept_bliss_scan=(self._research_type == ScanType.BLISS), ) except Exception as e: logger.info(f"Fail to treat {folder_path}. Error is {e}") else: for scan in scan_objs: if ( isinstance(scan, BlissScan) and self._research_type is ScanType.BLISS ): self.sigDataFound.emit(scan) elif ( isinstance(scan, NXtomoScan) and self._research_type is ScanType.NX_TOMO ): self.sigDataFound.emit(scan) elif ( isinstance(scan, EDFTomoScan) and self._research_type is ScanType.SPEC ): self.sigDataFound.emit(scan) else: raise NotImplementedError("case not handled", self._research_type) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739271825.0 tomwer-1.4.8/src/orangecontrib/tomwer/widgets/control/DataListenerOW.py0000644000175000017500000003311614752627221025440 0ustar00paynopaynofrom __future__ import annotations from orangewidget import gui, settings, widget from orangewidget.widget import Output from silx.gui import qt import tomwer.core.process.control.datalistener.datalistener from orangecontrib.tomwer.widgets.utils import WidgetLongProcessing from tomwer.core.process.control.datalistener import DataListener from tomwer.core.process.control.datalistener.rpcserver import ( send_signal_to_local_rpc_servers, ) from tomwer.core.process.control.nxtomomill import H5ToNxProcess from tomwer.core.scan.blissscan import BlissScan from tomwer.core.scan.scanbase import TomwerScanBase from tomwer.gui.control.datalistener import DataListenerWidget from tomwer.synctools.datalistener import DataListenerQThread, MockDataListenerQThread from tomwer.synctools.stacks.control.datalistener import DataListenerProcessStack from tomwer.utils import docstring try: # new HDF5Config class from nxtomomill.io.config import TomoHDF5Config as HDF5Config except ImportError: from nxtomomill.io.config import HDF5Config import functools import logging import signal import sys from typing import Iterable logger = logging.getLogger(__name__) class DataListenerOW( widget.OWBaseWidget, WidgetLongProcessing, DataListener, openclass=True ): """ This widget is used to listen to a server notifying the widget when an acquisition is finished. Then the bliss file will be converted to .nx file, NXtomo compliant. """ name = "scan listener" id = "orangecontrib.widgets.tomwer.control.DataListenerOW.DataListenerOW" description = ( "The widget will receive information from bliss acquisition " "and wait for acquisition to be finished. Once finished it " "will call nxtomomill to convert from bliss .hdf5 to " "NXtomo compliant .nx file" ) icon = "icons/datalistener.svg" priority = 10 keywords = [ "tomography", "file", "tomwer", "listener", "datalistener", "hdf5", "NXtomo", ] want_main_area = True want_control_area = False resizing_enabled = True ewokstaskclass = ( tomwer.core.process.control.datalistener.datalistener._DataListenerTaskPlaceHolder ) _blissConfiguration = settings.Setting(dict()) # to keep backward compatibility _nxtomo_cfg_file = settings.Setting(str()) # to keep backward compatibility _static_input = settings.Setting(dict()) class Outputs: data = Output(name="data", type=TomwerScanBase, doc="one scan to be process") def __init__(self, parent=None): widget.OWBaseWidget.__init__(self, parent) WidgetLongProcessing.__init__(self) DataListener.__init__(self) self._processingStack = DataListenerProcessStack() self._processingStack.sigComputationEnded.connect(self._signal_scan_ready) self._widget = DataListenerWidget(parent=self) self._mock = False self._box = gui.vBox(self.mainArea, self.name) layout = self._box.layout() layout.addWidget(self._widget) # signal / slot connection self._widget.sigActivate.connect(self._activated) self._widget.sigDeactivate.connect(self._deactivated) self._widget.sigConfigurationChanged.connect(self._jsonRPCConfigChanged) self._widget.sigCFGFileChanged.connect(self._nxtomoFileChanged) self._widget.sigAcquisitionEnded.connect(self._process_bliss_file_frm_tuple) self._widget.sigServerStopped.connect(self._serverStopped) # manage server stop when delete directly the widget or stop by Ctr+C signal.signal(signal.SIGINT, self.handleSigTerm) onDestroy = functools.partial(self._stopServerBeforeclosing) self.destroyed.connect(onDestroy) # set up self._loadSettings() # for conveniance start the listener when create it. self.activate(True) def _loadSettings(self): if "bliss_server_configuration" in self._static_input: # pylint: disable=E1135 bliss_configuration = self._static_input[ # pylint: disable=E1136 "bliss_server_configuration" ] else: bliss_configuration = self._blissConfiguration if bliss_configuration != {}: self._widget.setBlissServerConfiguation(bliss_configuration) if "nxtomomill_cfg_file" in self._static_input: # pylint: disable=E1135 nxtomo_cfg_file = self._static_input[ # pylint: disable=E1136 "nxtomomill_cfg_file" ] else: nxtomo_cfg_file = self._nxtomo_cfg_file self._widget.setCFGFilePath(nxtomo_cfg_file) if "output_dir" in self._static_input: # pylint: disable=E1135 self._widget.setOutputFolder( self._static_input["output_dir"] # pylint: disable=E1136 ) def getNXTomomillConfiguration(self): cfg_file = self._widget.getCFGFilePath() def create_default_config(): configuration = HDF5Config() # insure minimal bacward compatibility if hasattr(configuration, "bam_single_file"): configuration.bam_single_file = True return configuration if cfg_file in (None, ""): config = create_default_config() else: try: config = HDF5Config.from_cfg_file(cfg_file) except Exception as e: logger.warning(f"Fail to load configuraiton file. Error is {e}") config = create_default_config() return config def _process_bliss_file_frm_tuple(self, t): master_file, entry, proposal_file, saving_file, success = t bliss_scan = BlissScan( master_file=master_file, entry=str(entry) + ".1", proposal_file=proposal_file, saving_file=saving_file, ) configuration = self.getNXTomomillConfiguration() # overwrite output file configuration.output_file = H5ToNxProcess.deduce_output_file_path( bliss_scan.master_file, entry=bliss_scan.entry, outputdir=self._widget.getOutputFolder(), scan=bliss_scan, ) if success: self._processingStack.add(data=bliss_scan, configuration=configuration) else: # TODO: maybe remove the acquisition ? pass def _activated(self): self.activate(True) def _deactivated(self): self.activate(False) def _serverStopped(self): """ Callback when the server is stopped """ self.activate(False) @docstring(DataListener.activate) def activate(self, activate=True): if activate and not self.is_port_available(): old = self._widget.blockSignals(True) self._widget.activate(activate=False) self._widget.blockSignals(old) dialog = _PortOccupyDialog(parent=self, port=self.port, host=self.host) dialog.setModal(False) if dialog.exec_() == qt.QDialog.Accepted: if dialog.retry_connection: return self.activate(activate=True) else: return old = self._widget.blockSignals(True) self.set_configuration(self._widget.getBlissServerConfiguration()) self._widget.activate(activate=activate) DataListener.activate(self, activate=activate) self.processing_state(activate, info="listener active") self._widget.blockSignals(old) def _signal_scan_ready(self, scan, future_tomo_obj): if scan is None: return assert isinstance(scan, Iterable) for s in scan: assert isinstance(s, TomwerScanBase) self.Outputs.data.send(s) def _ask_user_for_overwritting(self, file_path): msg = qt.QMessageBox(self) msg.setIcon(qt.QMessageBox.Question) types = qt.QMessageBox.Ok | qt.QMessageBox.Cancel msg.setStandardButtons(types) text = "NXtomomill will overwrite: \n %s. Do you agree ?" % file_path msg.setText(text) return msg.exec_() == qt.QMessageBox.Ok def _jsonRPCConfigChanged(self): self._blissConfiguration = self._widget.getBlissServerConfiguration() self._static_input["bliss_server_configuration"] = ( # pylint: disable=E1137 self._widget.getBlissServerConfiguration() ) self._static_input["output_dir"] = ( # pylint: disable=E1137 self._widget.getOutputFolder() ) if self.is_active(): self.activate(False) self.activate(True) def _nxtomoFileChanged(self, cfg_file): self._nxtomo_cfg_file = cfg_file self._static_input["nxtomomill_cfg_file"] = cfg_file # pylint: disable=E1137 def setMock(self, mock, acquisitions): self._mock = mock self._mock_acquisitions = acquisitions @docstring(DataListenerWidget.getHost) def getHost(self): return self._widget.getHost() @docstring(DataListenerWidget.getPort) def getPort(self): return self._widget.getPort() def close(self): signal.signal(signal.SIGINT, signal.SIG_DFL) self.activate(False) super().close() @docstring(DataListener.create_listening_thread) def create_listening_thread(self): if self._mock is True: thread = MockDataListenerQThread( host=self.getHost(), port=self.getPort(), acquisitions=None, mock_acquisitions=self._mock_acquisitions, ) else: thread = DataListenerQThread( host=self.getHost(), port=self.getPort(), acquisitions=None ) # connect thread thread.sigAcquisitionStarted.connect( self._widget._acquisitionStarted, qt.Qt.DirectConnection ) thread.sigAcquisitionEnded.connect( self._widget._acquisitionEnded, qt.Qt.DirectConnection ) thread.sigScanAdded.connect( self._widget._acquisitionUpdated, qt.Qt.DirectConnection ) thread.sigServerStop.connect( self._widget._serverStopped, qt.Qt.DirectConnection ) return thread @docstring(DataListener.delete_listening_thread) def delete_listening_thread(self): self._listening_thread.sigAcquisitionStarted.disconnect( self._widget._acquisitionStarted ) self._listening_thread.sigAcquisitionEnded.disconnect( self._widget._acquisitionEnded ) self._listening_thread.sigScanAdded.disconnect(self._widget._acquisitionUpdated) self._listening_thread.sigServerStop.disconnect(self._widget._serverStopped) DataListener.delete_listening_thread(self) def _stopServerBeforeclosing(self): self.activate(False) def handleSigTerm(self, signo, *args, **kwargs): if signo == signal.SIGINT: self._stopServerBeforeclosing() sys.exit() def _get_n_scan_observe(self): return self._widget._observationWidget.observationTable.model().rowCount() def _get_n_scan_finished(self): return self._widget._historyWindow.scanHistory.model().rowCount() class _PortOccupyDialog(qt.QDialog): def __init__(self, parent, port, host): qt.QDialog.__init__(self, parent) self.setLayout(qt.QVBoxLayout()) self._retry = False self.port = port self.host = host mess = ( f"port ({port}) of {host} already in use. \n Maybe an other " "instance of `datalistener` is running in this session or " "another tomwer session. \n As this widget is connecting with " "bliss we enforce it to be unique." ) self.layout().addWidget(qt.QLabel(mess, self)) self.setWindowTitle("Unable to launch two listener in parallel") types = qt.QDialogButtonBox.Cancel | qt.QDialogButtonBox.Retry self._buttons = qt.QDialogButtonBox(self) self._buttons.setStandardButtons(types) self.layout().addWidget(self._buttons) self._sendSIGTERMMsb = qt.QPushButton("send SIGTERM", self) self._sendSIGTERMMsb.setToolTip( "Try to send SIGTERM signal to the " "local tomwer-rpcserver if any " "occupies the reserved port" ) self._buttons.addButton(self._sendSIGTERMMsb, qt.QDialogButtonBox.ActionRole) self._sendSIGKILLMsb = qt.QPushButton("send SIGKILL", self) self._sendSIGKILLMsb.setToolTip( "Try to send SIGKILL signal to the " "local tomwer-rpcserver if any " "occupies the reserved port" ) self._buttons.addButton(self._sendSIGKILLMsb, qt.QDialogButtonBox.ActionRole) # set up # for now we don't want to show "send signal" feature self._sendSIGTERMMsb.hide() self._sendSIGKILLMsb.hide() # connect signal / slot self._buttons.button(qt.QDialogButtonBox.Cancel).released.connect(self.reject) self._buttons.button(qt.QDialogButtonBox.Retry).released.connect( self._retry_connect ) self._sendSIGTERMMsb.released.connect(self._emitSigterm) self._sendSIGKILLMsb.released.connect(self._emitSigkill) @property def retry_connection(self): return self._retry def _emitSigterm(self, *args, **kwargs): send_signal_to_local_rpc_servers(signal.SIGTERM, port=self.port) def _emitSigkill(self, *args, **kwargs): send_signal_to_local_rpc_servers(signal.SIGKILL, port=self.port) def _retry_connect(self, *args, **kargs): self._retry = True self.accept() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739271825.0 tomwer-1.4.8/src/orangecontrib/tomwer/widgets/control/DataSelectorOW.py0000644000175000017500000000775614752627221025446 0ustar00paynopaynofrom __future__ import annotations import logging from orangewidget import gui from orangewidget.settings import Setting from orangewidget.widget import Input, Output, OWBaseWidget from silx.gui import qt import tomwer.core.process.control.scanselector from tomwer.core.scan.nxtomoscan import NXtomoScan from tomwer.core.scan.scanbase import TomwerScanBase from tomwer.gui.control.scanselectorwidget import ScanSelectorWidget logger = logging.getLogger(__name__) class DataSelectorOW(OWBaseWidget, openclass=True): name = "scan selector" id = "orange.widgets.tomwer.scanselector" description = ( "List all received scan. Then user can select a specific" "scan to be passed to the next widget." ) icon = "icons/scanselector.svg" priority = 42 keywords = ["tomography", "selection", "tomwer", "scan", "data"] ewokstaskclass = tomwer.core.process.control.scanselector._ScanSelectorPlaceHolder want_main_area = True want_control_area = False resizing_enabled = True _scanIDs = Setting(list()) class Inputs: data = Input(name="data", type=TomwerScanBase, multiple=True) class Outputs: data = Output(name="data", type=TomwerScanBase) def __init__(self, parent=None): """ """ super().__init__(parent) self.widget = ScanSelectorWidget(parent=self) self.widget.setWindowFlags(qt.Qt.Widget) self._loadSettings() self.widget.sigUpdated.connect(self._updateSettings) self.widget.sigSelectionChanged.connect(self.changeSelection) layout = gui.vBox(self.mainArea, self.name).layout() layout.addWidget(self.widget) @Inputs.data def addScan(self, scan, *args, **kwargs): self.add(scan) def add(self, scan): if scan is not None: self.widget.add(scan=scan) def changeSelection(self, list_objs): if list_objs is None: return for obj_id in list_objs: item = self.widget.dataList._myitems.get(obj_id, None) if item: tomo_obj = item.data(qt.Qt.UserRole) assert isinstance(tomo_obj, TomwerScanBase) self.Outputs.data.send(tomo_obj) else: logger.error("%s not found in scan ids" % obj_id) def send(self): """send output signals for each selected items""" sItem = self.widget.dataList.selectedItems() if sItem and len(sItem) >= 1: selection = [ _item.data(qt.Qt.UserRole).get_identifier().to_str() for _item in sItem ] self.changeSelection(list_objs=selection) def _loadSettings(self): for scan in self._scanIDs: assert isinstance(scan, str) # kept for backward compatibility since 0.11. To be removed on the future version. if "@" in scan: entry, file_path = scan.split("@") nxtomo_scan = NXtomoScan(entry=entry, scan=file_path) self.addScan(nxtomo_scan) else: self.addScan(scan) def _updateSettings(self): self._scanIDs = [] for scan in self.widget.dataList._myitems: self._scanIDs.append(scan) def keyPressEvent(self, event): """ Forward: * delete key (shortcut orange and make sure the `delete` key will be interpreted we need to overwrite this function) * Ctrl + A to select all the scan """ modifiers = event.modifiers() key = event.key() if key == qt.Qt.Key_A and modifiers == qt.Qt.KeyboardModifier.ControlModifier: self.widget.dataList.keyPressEvent(event) if key == qt.Qt.Key_Delete: self.widget.removeSelectedDatasets() else: super().keyPressEvent(event) # expose API def setActiveScan(self, data): self.widget.setActiveData(data=data) def selectAll(self): return self.widget.selectAll() def n_scan(self): return self.widget.n_scan() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739271825.0 tomwer-1.4.8/src/orangecontrib/tomwer/widgets/control/DataTransfertOW.py0000644000175000017500000001346614752627221025631 0ustar00paynopaynofrom __future__ import annotations import functools import logging from orangewidget import gui, settings from orangewidget.widget import Input, Output from processview.core.manager import DatasetState, ProcessManager from silx.gui import qt import tomwer.core.process.control.scantransfer from orangecontrib.tomwer.orange.managedprocess import SuperviseOW from orangecontrib.tomwer.orange.settings import CallbackSettingsHandler from tomwer.core.process.control.scantransfer import ScanTransferTask from tomwer.core.scan.scanbase import TomwerScanBase from tomwer.gui.control.datatransfert import DataTransfertSelector from tomwer.utils import docstring logger = logging.getLogger(__name__) class DataTransfertOW(SuperviseOW): """ A simple widget managing the copy of an incoming folder to an other one :param parent: the parent widget """ name = "data transfer" id = "orange.widgets.tomwer.foldertransfert" description = "This widget insure data transfer of the received data " description += "to the given directory" icon = "icons/folder-transfert.svg" priority = 30 keywords = [ "tomography", "transfert", "cp", "copy", "move", "file", "tomwer", "folder", ] ewokstaskclass = tomwer.core.process.control.scantransfer.ScanTransferTask settingsHandler = CallbackSettingsHandler() want_main_area = True resizing_enabled = True dest_dir_settings = settings.Setting(str()) """Parameters directly editabled from the TOFU interface""" scanready = qt.Signal(TomwerScanBase) """emit when scan ready""" class Inputs: data = Input(name="data", type=TomwerScanBase) class Outputs: data = Output(name="data", type=TomwerScanBase) def __init__(self, parent=None): super().__init__(parent) self._destDir = None self._forceSync = False self._threads = [] # define GUI self._widget = DataTransfertSelector( parent=self, rnice_option=True, default_root_folder=ScanTransferTask.getDefaultOutputDir(), ) self._layout = gui.vBox(self.mainArea, self.name).layout() self._layout.addWidget(self._widget) # signal / SLOT connection self.settingsHandler.addCallback(self._updateSettingsVals) self._widget.sigSelectionChanged.connect(self._updateDestDir) # setting configuration if self.dest_dir_settings != "": self._widget.setFolder(self.dest_dir_settings) def _requestFolder(self): # pragma: no cover """Launch a QFileDialog to ask the user the output directory""" dialog = qt.QFileDialog(self) dialog.setWindowTitle("Destination folder") dialog.setModal(1) dialog.setFileMode(qt.QFileDialog.DirectoryOnly) if not dialog.exec_(): dialog.close() return None return dialog.selectedFiles()[0] def transfertDoneCallback(self, output_scan): if output_scan is None: return self.Outputs.data.send(output_scan) self.scanready.emit(output_scan) def _updateDestDir(self): self._destDir = self._widget.getFolder() def _updateSettingsVals(self): """function used to update the settings values""" self.dest_dir_settings = self._destDir @Inputs.data def process(self, scan): self._process(data=scan) def _process(self, data, move=False, force=True, noRsync=False): if data is None: return elif not isinstance(data, TomwerScanBase): raise TypeError("data is expected to be an instance of TomwerScanBase") inputs = { "data": data, "move": move, "overwrite": force, "noRsync": noRsync, "dest_dir": self._destDir, "block": self._forceSync, "serialize_output_data": False, } thread = ThreadDataTransfer( inputs=inputs, data=data, process=self, ) try: process = ScanTransferTask(inputs=inputs) except Exception as e: logger.error(e) else: dest_dir = process.getDestinationDir(data.path, ask_for_output=False) if dest_dir is not None: thread.finished.connect( functools.partial( self.transfertDoneCallback, data._deduce_transfert_scan( process.getDestinationDir(data.path) ), ) ) thread.start() self._threads.append(thread) @docstring(SuperviseOW) def reprocess(self, dataset): self.process(dataset) def setDestDir(self, dest_dir): self._destDir = dest_dir def setForceSync(self, sync): self._forceSync = sync def isCopying(self): # for now only move file is handled return False class ThreadDataTransfer(qt.QThread): def __init__(self, data, inputs, process) -> None: super().__init__() self._inputs = inputs self._data = data self._process = process def run(self): try: process = ScanTransferTask(inputs=self._inputs) process.run() except Exception as e: logger.error(f"data transfer failed. Reason is {e}") ProcessManager().notify_dataset_state( dataset=self._data, process=self._process, state=DatasetState.FAILED, details=str(e), ) else: ProcessManager().notify_dataset_state( dataset=self._data, process=self._process, state=DatasetState.SUCCEED, ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739271825.0 tomwer-1.4.8/src/orangecontrib/tomwer/widgets/control/DataValidatorOW.py0000644000175000017500000001321614752627221025577 0ustar00paynopaynofrom __future__ import annotations import logging from orangewidget import gui, settings from orangewidget.widget import Input, Output from processview.core.manager import DatasetState, ProcessManager from silx.gui import qt import tomwer.core.process.control.scanvalidator from orangecontrib.tomwer.orange.managedprocess import SuperviseOW from tomwer.core.scan.scanbase import TomwerScanBase, _TomwerBaseDock from tomwer.gui.control.datavalidator import DataValidator from tomwer.utils import docstring _logger = logging.getLogger(__name__) class DataValidatorOW(SuperviseOW): """a data viewer able to: - display slices (latest reconstructed if any) - display radios with or without normalization :param parent: the parent widget """ name = "scan validator" id = "orange.widgets.tomwer.datavalidator" description = """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""" icon = "icons/validator.png" priority = 23 keywords = ["tomography", "file", "tomwer", "acquisition", "validation"] want_main_area = True resizing_enabled = True ewokstaskclass = tomwer.core.process.control.scanvalidator._ScanValidatorPlaceHolder _viewer_config = settings.Setting(dict()) _warnValManualShow = False """ used to know if the message to inform user about `validate manually` has already been displayed. This informative message will be show under the following conditions: * the scanValidator contains at least `_NB_SCAN_BF_WARN` * this dialog have never been showed in the current session. """ _NB_SCAN_BF_WARN = 10 """ Limit of stored scans before displaying the informative message about `validate manually` checkbox """ class Inputs: data = Input(name="data", type=TomwerScanBase) class Outputs: data = Output(name="data", type=TomwerScanBase) recons_params_changed = Output( name="change recons params", type=_TomwerBaseDock, ) def __init__(self, parent=None): SuperviseOW.__init__(self, parent) self._layout = gui.vBox(self.mainArea, self.name).layout() self._widget = DataValidator(parent=self) self._layout.addWidget(self._widget) self._setSettings(settings=self._viewer_config) # connect signal / slots self._widget.sigChangeReconsParams.connect(self._changeReconsParamsEmited) self._widget.sigScanReady.connect(self._scanReadyEmitted) self._widget._centralWidget.sigConfigChanged.connect(self._updateSettings) def close(self): if self._widget is not None: self._widget.setAttribute(qt.Qt.WA_DeleteOnClose) self._widget.close() self._widget = None super().close() @Inputs.data def addScan(self, scan): if scan is None: return assert isinstance(scan, TomwerScanBase) self._widget.addScan(scan) ProcessManager().notify_dataset_state( dataset=scan, process=self, state=DatasetState.WAIT_USER_VALIDATION ) # in the case the memory is full, the scan can have been already # validated and so not accessible if ( self._warnValManualShow is False and len(self._widget._scans) >= self._NB_SCAN_BF_WARN ): mess = ( "Please note that the scanValidator is actually storing %s " "scan(s). \n" "Scan need to be validated manually in order to continue " "the workflow processing. \n" "you can either validate scan manually or uncheck the " "`validate manually` check box." % self._NB_SCAN_BF_WARN ) mess = qt.QMessageBox(self, qt.QMessageBox.Information, mess) mess.setModal(False) mess.show() self._warnValManualShow = True if self.isValidationManual(): self.show() self.activateWindow() self.raise_() @docstring def reprocess(self, dataset): self.addScan(dataset) def isValidationManual(self): return self._widget.isValidationManual() def _changeReconsParamsEmited(self, scan): self.Outputs.recons_params_changed.send(scan) def _scanReadyEmitted(self, scan): ProcessManager().notify_dataset_state( dataset=scan, process=self, state=DatasetState.SUCCEED ) self.Outputs.data.send(scan) def _updateSettings(self): viewer = self._widget._centralWidget self._viewer_config["mode"] = viewer.getDisplayMode() # pylint: disable=E1137 self._viewer_config["slice_opt"] = ( # pylint: disable=E1137 viewer.getSliceOption() ) self._viewer_config["radio_opt"] = ( # pylint: disable=E1137 viewer.getRadioOption() ) def _setSettings(self, settings): viewer = self._widget._centralWidget old_state = viewer.blockSignals(True) if "mode" in settings: viewer.setDisplayMode(settings["mode"]) if "slice_opt" in settings: viewer.setSliceOption(settings["slice_opt"]) if "radio_opt" in settings: viewer.setRadioOption(settings["radio_opt"]) viewer.blockSignals(old_state) def getNScanToValidate(self): return len(self._widget._scans) def _validateScan(self, scan): self._widget._validateScan(scan=scan) def setAutomaticValidation(self, auto): self._widget.setAutomaticValidation(auto) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739271825.0 tomwer-1.4.8/src/orangecontrib/tomwer/widgets/control/DataWatcherOW.py0000644000175000017500000001364414752627221025254 0ustar00paynopaynofrom __future__ import annotations import functools import logging from orangewidget import gui, widget from orangewidget.settings import Setting from orangewidget.widget import Output import tomwer.core.process.control.datawatcher.datawatcher from tomwer.core.scan.blissscan import BlissScan from tomwer.core.scan.scanbase import TomwerScanBase from tomwer.gui.control.datawatcher import DataWatcherWidget from ..utils import WidgetLongProcessing logger = logging.getLogger(__name__) class DataWatcherOW(widget.OWBaseWidget, WidgetLongProcessing, openclass=True): """ This widget is used to observe a selected folder and his sub-folders to detect if they are containing valid-finished acquisitions. """ name = "scan watcher" id = "orangecontrib.widgets.tomwer.datawatcherwidget.DataWatcherOW" description = ( "The widget will observe folder and sub folders of a given" " path and waiting for acquisition to be ended." " The widget will infinitely wait until an acquisition is " "ended. If an acquisition is ended then a signal " "containing the folder path is emitted." ) icon = "icons/datawatcher.svg" priority = 12 keywords = ["tomography", "file", "tomwer", "observer", "datawatcher"] want_main_area = True want_control_area = False resizing_enabled = True folderObserved = Setting(str()) acquisitionMethod = Setting(tuple()) linuxFilePatternSetting = Setting(str()) DEFAULT_DIRECTORY = "/lbsram/data/visitor" ewokstaskclass = ( tomwer.core.process.control.datawatcher.datawatcher.DataWatcherEwoksTask ) class Outputs: data = Output(name="data", type=TomwerScanBase) bliss_scan = Output( name="bliss data", type=BlissScan, doc="bliss scan to be process" ) def __init__(self, parent=None, displayAdvancement=True): """Simple class which will check advancement state of the acquisition for a specific folder :param parent: the parent widget """ widget.OWBaseWidget.__init__(self, parent) WidgetLongProcessing.__init__(self) self._widget = DataWatcherWidget(parent=self) self._widget.setFolderObserved(self.folderObserved) self._box = gui.vBox(self.mainArea, self.name) layout = self._box.layout() layout.addWidget(self._widget) # signal / slot connection self._widget.sigFolderObservedChanged.connect(self._updateSettings) self._widget.sigObservationModeChanged.connect(self._updateSettings) self._widget.sigObservationModeChanged.connect(self._setObsMethod) self._widget.sigFilterFileNamePatternChanged.connect( self._saveFilterLinuxPattern ) self._widget.sigFilterFileNamePatternChanged.connect( self._widget.setLinuxFilePattern ) self._widget.sigScanReady.connect(self._sendSignal) callback_start = functools.partial(self.processing_state, True, "watching on") self._widget.sigObservationStart.connect(callback_start) callback_end = functools.partial(self.processing_state, False, "watching off") self._widget.sigObservationEnd.connect(callback_end) self._loadSettings() def _loadSettings(self): if self.acquisitionMethod != tuple(): self.widget.getConfigWindow().setMode(self.acquisitionMethod) if self.folderObserved != str(): self.widget.setFolderObserved(self.folderObserved) if self.linuxFilePatternSetting != str(): # for the GUI self.widget._filterQLE.setText(self.linuxFilePatternSetting) # for providing it to the processing thread. Bad design but this widget shouldn't be used that # much and modifying internals is not the priority. self.widget.setLinuxFilePattern(self.linuxFilePatternSetting) def _updateSettings(self): self.folderObserved = self.widget.getFolderObserved() mode = self.widget.getConfigWindow().getMode() if mode is not None: self.acquisitionMethod = mode self.linuxFilePatternSetting = self.widget.getLinuxFilePattern() def _saveFilterLinuxPattern(self, *args, **kwargs): linuxFilePattern = self.widget.getFilterLinuxFileNamePattern() if linuxFilePattern is None: self.linuxFilePatternSetting = "" else: self.linuxFilePatternSetting = linuxFilePattern def _setObsMethod(self, mode): self.widget.setObsMethod(mode) @property def widget(self): return self._widget @property def currentStatus(self): return self._widget.currentStatus @property def sigTMStatusChanged(self): return self._widget.sigTMStatusChanged def resetStatus(self): self._widget.resetStatus() def _sendSignal(self, scan): if scan is None: pass elif isinstance(scan, TomwerScanBase): self.Outputs.data.send(scan) elif isinstance(scan, BlissScan): self.Outputs.bliss_scan.send(scan) else: raise TypeError( f"output is expected to be a {TomwerScanBase} or a {BlissScan}" ) def setFolderObserved(self, path): self.widget.setFolderObserved(path) def setObservation(self, b): self._widget.setObservation(b) def setTimeBreak(self, val): """ Set the time break between two loop observation :param val: time (in sec) """ self._widget.setWaitTimeBtwLoop(val) def startObservation(self): try: self.processing_info("processing") except Exception: pass self._widget.start() def stopObservation(self, succes=False): self.widget.stop(succes) try: self.Processing.clear() except Exception: pass def close(self): self.widget.close() super().close() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739271825.0 tomwer-1.4.8/src/orangecontrib/tomwer/widgets/control/EDF2NXTomomillOW.py0000644000175000017500000002260014752627221025520 0ustar00paynopaynofrom __future__ import annotations import logging from copy import copy from nxtomomill.io.config import TomoEDFConfig as EDFConfig from orangewidget import gui from silx.gui import qt from silx.gui.utils import blockSignals from orangecontrib.tomwer.orange.managedprocess import TomwerWithStackStack from orangecontrib.tomwer.widgets.control.NXTomomillMixIn import NXTomomillMixIn from tomwer.core.process.control.nxtomomill import EDFToNxProcess from tomwer.core.process.output import ProcessDataOutputDirMode from tomwer.core.scan.edfscan import EDFTomoScan from tomwer.core.scan.nxtomoscan import NXtomoScan from tomwer.gui.control.datalist import EDFDataListMainWindow logger = logging.getLogger(__name__) class EDF2NXOW( TomwerWithStackStack, NXTomomillMixIn, ewokstaskclass=EDFToNxProcess, ): """ Widget to allow user to pick some bliss files and that will convert them to HDF5scan. """ name = "nxtomomill - edf2nx (Spec-EDF)" id = "orange.widgets.tomwer.control.NXTomomillOW.EDF2NXOW" description = "Convert folders with .edf to .nx" icon = "icons/edf2nx.svg" priority = 121 keywords = [ "edf", "nexus", "tomwer", "file", "convert", "NXTomo", "tomography", "edf2nx", "nxtomomill", ] want_main_area = True want_control_area = False resizing_enabled = True CONFIG_CLS = EDFConfig LOGGER = logger _ewoks_inputs_to_hide_from_orange = ( "edf_to_nx_configuration", "progress", "serialize_output_data", ) def __init__(self, parent=None): self.__configuration_cache = None # cache updated for each folder in order to match `_execute_ewoks_task` design TomwerWithStackStack.__init__(self, parent=parent) NXTomomillMixIn.__init__(self) _layout = gui.vBox(self.mainArea, self.name).layout() self.widget = EDFDataListMainWindow(parent=self) _layout.addWidget(self.widget) # for edf default output path is still the 'near to input file' self.widget._dialog._nxTomomillOutputWidget._inScanFolder.setChecked(True) # add 'convert auto' check box self._convertAutoCB = qt.QCheckBox( "convert automatically when edf scan send through 'edf scan' channel", self ) self._convertAutoCB.setChecked(True) _layout.addWidget(self._convertAutoCB) # connect signal / slot self.widget._sendSelectedButton.clicked.connect(self._sendSelected) self.widget.sigNXTomoCFGFileChanged.connect(self._saveNXTomoCfgFile) self.widget.sigUpdated.connect(self._updateSettings) self._convertAutoCB.toggled.connect(self._updateSettings) # set default configuration is no existing configuration file defined in the settings self.update_default_inputs( edf_to_nx_configuration=EDFConfig().to_dict(), ) if isinstance(self.task_output_changed_callbacks, set): self.task_output_changed_callbacks.add(self._notify_state) elif isinstance(self.task_output_changed_callbacks, list): self.task_output_changed_callbacks.append(self._notify_state) else: raise NotImplementedError # handle settings self._loadSettings() def _updateSettings(self): # keep auto conversion setting self._ewoks_default_inputs[ # pylint: disable=E1137 "convert_auto_edfscan_channel" ] = self.convertAutoEDFScanReceivedFromChannel() # store scans self._scans = [] for scan in self.widget.datalist._myitems: self._scans.append(scan) # store output folder output_dir = self.widget.getOutputFolder() if isinstance(output_dir, ProcessDataOutputDirMode): output_dir = output_dir.value self._ewoks_default_inputs["output_dir"] = output_dir # pylint: disable=E1137 def convertAutoEDFScanReceivedFromChannel(self) -> bool: return self._convertAutoCB.isChecked() def setConvertAutoEDFScanReceivedFromChannel(self, checked: bool): self._convertAutoCB.setChecked(checked) def _loadSettings(self): with blockSignals(self.widget): for scan in self._scans: # pylint: disable=E1133 self.widget.add(scan) if ( "nxtomomill_cfg_file" in self._ewoks_default_inputs ): # pylint: disable=E1135 self._nxtomo_cfg_file = ( self._ewoks_default_inputs[ # pylint: disable=E1136 "nxtomomill_cfg_file" ] ) self.widget.setCFGFilePath(self._nxtomo_cfg_file) if "output_dir" in self._ewoks_default_inputs: # pylint: disable=E1135 self.widget.setOutputFolder( self._ewoks_default_inputs["output_dir"] # pylint: disable=E1136 ) if ( "convert_auto_edfscan_channel" in self._ewoks_default_inputs # pylint: disable=E1135 ): self.setConvertAutoEDFScanReceivedFromChannel( self._ewoks_default_inputs[ # pylint: disable=E1136 "convert_auto_edfscan_channel" ] ) def _convertAndSend(self, edf_scan: EDFTomoScan): # cache updated for each folder in order to match `_execute_ewoks_task` design # for edf2nx we need to update input dir from the DataList and output_dir from # the one requested by the user if not isinstance(edf_scan, EDFTomoScan): raise TypeError( f"edf_scan is expected to be a {EDFTomoScan} not {type(edf_scan)}" ) self.__configuration_cache = EDFConfig.from_dict( copy(self.get_default_input_values()["edf_to_nx_configuration"]) ) self.__configuration_cache.input_folder = edf_scan.path self.__configuration_cache.dataset_basename = edf_scan.dataset_basename output_file = EDFToNxProcess.deduce_output_file_path( folder_path=edf_scan.path, output_dir=self.widget.getOutputFolder(), scan=edf_scan, ) output_entry = "entry" self.__configuration_cache.output_file = output_file # keep 'edf_to_nx_configuration' up to date according to input folder and output_file updates self.update_default_inputs( edf_to_nx_configuration=self.__configuration_cache.to_dict() ) self.notify_on_going(scan=NXtomoScan(output_file, output_entry)) try: self._execute_ewoks_task( # pylint: disable=E1123 propagate=True, log_missing_inputs=False, ) except Exception: self._execute_ewoks_task(propagate=True) # pylint: disable=E1123, E1120 def get_task_inputs(self): return { "edf_to_nx_configuration": self.__configuration_cache.to_dict(), "serialize_output_data": False, } def handleNewSignals(self) -> None: """Invoked by the workflow signal propagation manager after all signals handlers have been called. """ # for now we want to avoid propagation any processing. # task will be executed only when the user validates the dialog edf_scan = super().get_task_inputs().get("edf_scan", None) if edf_scan is not None: if not isinstance(edf_scan, EDFTomoScan): raise TypeError("edf_scan is expected to be an instance of EDFTomoScan") self.add(edf_scan.path) if self.convertAutoEDFScanReceivedFromChannel(): self._convertAndSend(edf_scan) def _notify_state(self): try: task_executor = self.sender() task_suceeded = task_executor.succeeded config = task_executor.current_task.inputs.edf_to_nx_configuration config = EDFConfig.from_dict(config) scan = NXtomoScan( config.output_file, "entry", ) if task_suceeded: self.notify_succeed(scan=scan) else: self.notify_failed(scan=scan) except Exception as e: logger.error(f"failed to handle task finished callback. Reason is {e}") def _saveNXTomoCfgFile(self, cfg_file): super()._saveNXTomoCfgFile(cfg_file, keyword="edf_to_nx_configuration") def _sendSelected(self): """send all selected items to be converted""" self._canOverwriteOutputs = False for scan_id in self.widget.datalist._myitems: tomo_obj = self.widget.datalist.getEDFTomoScan(scan_id, None) if tomo_obj: self._convertAndSend(tomo_obj) def _sendAll(self): """send all items to be converted""" self._canOverwriteOutputs = False for scan_id in self.widget.datalist._myitems: tomo_obj = self.widget.datalist.getEDFTomoScan(scan_id, None) if tomo_obj: self._convertAndSend(tomo_obj) def keyPressEvent(self, event): # forward Ctrl+A to the list as the shift ease selection of all modifiers = event.modifiers() key = event.key() if key == qt.Qt.Key_A and modifiers == qt.Qt.KeyboardModifier.ControlModifier: self.widget._widget.datalist.keyPressEvent(event) super().keyPressEvent(event) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739271825.0 tomwer-1.4.8/src/orangecontrib/tomwer/widgets/control/EmailOW.py0000644000175000017500000000527514752627221024115 0ustar00paynopaynofrom orangewidget import gui from orangewidget.settings import Setting from silx.gui import qt from ewoksorange.bindings.owwidgets import OWEwoksWidgetNoThread import tomwer.core.process.control.emailnotifier from tomwer.core.utils.dictutils import concatenate_dict try: from ewoksnotify.gui.email import EmailWidget except ImportError: has_ewoksnotify = False else: has_ewoksnotify = True class EmailOW( OWEwoksWidgetNoThread, ewokstaskclass=tomwer.core.process.control.emailnotifier.TomoEmailTask, ): """ This widget will browse a folder and sub folder to find valid tomo scan project. Contrary to the scan watcher it will parse all folder / sub folders then stop. """ name = "email notifier" id = "orangecontrib.widgets.tomwer.control.EmailOW.EmailOW" description = ( "This widget will send an email to receivers when the input is provided. \n" ) icon = "icons/email.svg" priority = 146 keywords = [ "tomography", "tomwer", "tomo_obj", "email", "notifier", "notification", ] want_main_area = True want_control_area = False resizing_enabled = True _ewoks_default_inputs = Setting({"configuration": {}, "tomo_obj": None}) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) if not has_ewoksnotify: raise ImportError( "ewoksnotify not install but required. Please run 'pip install ewoksnotify[full]'" ) self._widget = EmailWidget(parent=self) self._box = gui.vBox(self.mainArea, self.name) layout = self._box.layout() layout.addWidget(self._widget) # load settings self._widget.setConfiguration(self._ewoks_default_inputs) # connect signal / slot self._widget.sigChanged.connect(self._updateSettings) def _updateSettings(self): self._ewoks_default_inputs = { "configuration": self.getConfiguration(), "tomo_obj": None, } # expose some API def getConfiguration(self) -> dict: return self._widget.getConfiguration() def setConfiguration(self, config: dict): self._widget.setConfiguration(config.get("configuration", {})) def get_task_inputs(self): return concatenate_dict( super().get_task_inputs(), {"configuration": self.getConfiguration()}, ) def sizeHint(self): return qt.QSize(500, 200) def _execute_ewoks_task(self, *args, **kwargs): arguments = self._get_task_arguments() if arguments.get("inputs", {}).get("tomo_obj", None) is not None: super()._execute_ewoks_task(*args, **kwargs) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739271825.0 tomwer-1.4.8/src/orangecontrib/tomwer/widgets/control/FilterOW.py0000644000175000017500000000637014752627221024310 0ustar00paynopaynofrom __future__ import annotations import logging from orangewidget import gui from orangewidget.widget import Input, Output from processview.core.manager import DatasetState from silx.gui import qt import tomwer.core.process.conditions.filters from orangecontrib.tomwer.orange.managedprocess import SuperviseOW from tomwer.core.process.conditions import filters from tomwer.core.scan.scanbase import TomwerScanBase from tomwer.gui.conditions.filter import FileNameFilterWidget from tomwer.utils import docstring logger = logging.getLogger(__name__) class NameFilterOW(SuperviseOW): name = "scan filter" id = "orange.widgets.tomwer.filterow" description = ( "Simple widget which filter some data directory if the name" "doesn't match with the pattern defined." ) icon = "icons/namefilter.svg" priority = 106 keywords = ["tomography", "selection", "tomwer", "folder", "filter"] want_main_area = True resizing_enabled = True ewokstaskclass = tomwer.core.process.conditions.filters.FileNameFilterTask class Inputs: data = Input(name="data", type=TomwerScanBase, multiple=True) class Outputs: data = Output(name="data", type=TomwerScanBase) def __init__(self, parent=None): """ """ super().__init__(parent) self.widget = FileNameFilterWidget(parent=self) self.widget.setContentsMargins(0, 0, 0, 0) layout = gui.vBox(self.mainArea, self.name).layout() layout.addWidget(self.widget) spacer = qt.QWidget(parent=self) spacer.setSizePolicy(qt.QSizePolicy.Minimum, qt.QSizePolicy.Expanding) layout.addWidget(spacer) @Inputs.data def applyfilter(self, scan, *args, **kwargs): if scan is None: return if not isinstance(scan, TomwerScanBase): raise TypeError(f"{scan} is expected to be an instance of {TomwerScanBase}") process = filters.FileNameFilterTask( inputs={ "data": scan, "pattern": self.getPattern(), "filter_type": self.getActiveFilter(), "invert_result": self.invertFilterAction(), "serialize_output_data": False, # avoid spending time on scan serialization / deserialization when use orange } ) process.run() out = process.outputs.data if out is not None: self.set_dataset_state(dataset=scan, state=DatasetState.SUCCEED) logger.processSucceed(f"{scan} pass through filter") self._signalScanReady(scan) else: self.set_dataset_state(dataset=scan, state=DatasetState.FAILED) logger.processFailed(f"{scan} NOT pass through filter") @docstring(SuperviseOW) def reprocess(self, dataset): self.applyfilter(dataset) def _signalScanReady(self, scan): self.Outputs.data.send(scan) def getPattern(self): return self.widget.getPattern() def setPattern(self, pattern): self.widget.setPattern(pattern) def getActiveFilter(self): return self.widget.getActiveFilter() def setActiveFilter(self, filter): self.widget.setActiveFilter(filter) def invertFilterAction(self): return self.widget.invertFilterAction() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739271825.0 tomwer-1.4.8/src/orangecontrib/tomwer/widgets/control/NXTomomillMixIn.py0000644000175000017500000000630714752627221025624 0ustar00paynopaynoimport logging import os from orangewidget.settings import Setting _logger = logging.getLogger(__name__) class NXTomomillMixIn: """ MixIn class for nxtomomill + ewoks """ _scans = Setting(list()) _ewoks_default_inputs = Setting(dict()) CONFIG_CLS = None # logger to be used. Expected to be redefine by child class LOGGER = None # logger to be used. Expected to be redefine by child class def __init__(self, *args, **kwargs) -> None: self.__configuration_cache = None # cache updated for each folder in order to match `_execute_ewoks_task` design def add(self, *args, **kwargs): self.widget.add(*args, **kwargs) def _updateSettings(self): raise NotImplementedError("Base class") def _saveNXTomoCfgFile(self, cfg_file, keyword: str): """save the nxtomofile to the setttings""" assert ( self.CONFIG_CLS is not None ), "inheriting classes are expected to redefine CONFIG_CLS" self._ewoks_default_inputs["nxtomomill_cfg_file"] = ( cfg_file # pylint: disable=E1137 ) if os.path.exists(cfg_file): try: configuration = self.CONFIG_CLS.from_cfg_file( cfg_file ) # pylint: disable=E1102 except Exception as e: self._logger.error( f"Fail to use configuration file {cfg_file}. Error is {e}. No conversion will be done." ) else: default_inputs = { keyword: configuration.to_dict(), } else: default_inputs = { keyword: self.CONFIG_CLS().to_dict(), # pylint: disable=E1102 } # hack: try to upgrade x_pixel_keys and y_pixels keys to list. # otherwise has they have the same values and are tuples they have the same id and orange raises an # dump_literals raise a ValueError - check_relaxed - is a recursive structure try: default_inputs[keyword]["KEYS_SECTION"]["x_pixel_keys"] = list( default_inputs[keyword]["KEYS_SECTION"]["x_pixel_keys"] ) default_inputs[keyword]["KEYS_SECTION"]["y_pixel_keys"] = list( default_inputs[keyword]["KEYS_SECTION"]["y_pixel_keys"] ) except KeyError: pass self.update_default_inputs( **default_inputs, ) def _get_task_arguments(self): adict = super()._get_task_arguments() # pop progress as does not fully exists on the orange-widget-base adict.pop("progress", None) return adict def __new__(cls, *args, **kwargs): # ensure backward compatibility with 'static_input' static_input = kwargs.get("stored_settings", {}).get("static_input", None) if static_input not in (None, {}): _logger.warning( "static_input has been deprecated. Will be replaced by _ewoks_default_inputs in the workflow file. Please save the workflow to apply modifications" ) kwargs["stored_settings"]["_ewoks_default_inputs"] = static_input return super().__new__(cls, *args, **kwargs) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739271825.0 tomwer-1.4.8/src/orangecontrib/tomwer/widgets/control/NXTomomillOW.py0000644000175000017500000003152214752627221025122 0ustar00paynopaynofrom __future__ import annotations import logging import os from copy import copy from nxtomomill.io.config import TomoHDF5Config as HDF5Config from orangewidget import gui from silx.gui import qt from silx.gui.utils import blockSignals from orangecontrib.tomwer.orange.managedprocess import TomwerWithStackStack from orangecontrib.tomwer.widgets.control.NXTomomillMixIn import NXTomomillMixIn from tomwer.core.process.control.nxtomomill import H5ToNxProcess from tomwer.core.scan.blissscan import BlissScan from tomwer.core.scan.nxtomoscan import NXtomoScan, NXtomoScanIdentifier from tomwer.gui.control.datalist import BlissHDF5DataListMainWindow from tomwer.gui.control.nxtomomill import NXTomomillInput, OverwriteMessage from tomwer.core.process.output import ProcessDataOutputDirMode from ewoksorange.bindings.owwidgets import invalid_data logger = logging.getLogger(__name__) class NXTomomillOW( TomwerWithStackStack, NXTomomillMixIn, ewokstaskclass=H5ToNxProcess, ): """ Widget to allow user to pick some bliss files and that will convert them to HDF5scan. """ name = "nxtomomill h52nx (bliss-HDF5)" id = "orange.widgets.tomwer.control.NXTomomillOW.NXTomomillOW" description = ( "Read a bliss .h5 file and extract from it all possible" "NxTomo. When validated create a TomwerBaseScan for each " "file and entry" ) icon = "icons/nxtomomill.svg" priority = 120 keywords = [ "hdf5", "nexus", "tomwer", "file", "convert", "NXTomo", "tomography", "nxtomomill", "h52nx", ] want_main_area = True want_control_area = False resizing_enabled = True CONFIG_CLS = HDF5Config LOGGER = logger _ewoks_inputs_to_hide_from_orange = ( "h5_to_nx_configuration", "progress", "serialize_output_data", ) def __init__(self, parent=None): TomwerWithStackStack.__init__(self, parent=parent) NXTomomillMixIn.__init__(self) _layout = gui.vBox(self.mainArea, self.name).layout() self.widget = BlissHDF5DataListMainWindow(parent=self) _layout.addWidget(self.widget) self.__request_input = True # do we ask the user for input if missing self._inputGUI = None """Gui with cache for missing field in files to be converted""" self._canOverwriteOutputs = False """Cache to know if we have to ask user permission for overwriting""" # expose API self.n_scan = self.widget.n_scan # alias used for the 'simple workflow' for now self.start = self._sendAll # connect signal / slot self.widget._sendSelectedButton.clicked.connect(self._sendSelected) self.widget.sigNXTomoCFGFileChanged.connect(self._saveNXTomoCfgFile) self.widget.sigUpdated.connect(self._updateSettings) # set default configuration is no existing configuration file defined in the settings self.update_default_inputs( h5_to_nx_configuration=HDF5Config().to_dict(), ) if isinstance(self.task_output_changed_callbacks, set): self.task_output_changed_callbacks.add(self._notify_state) elif isinstance(self.task_output_changed_callbacks, list): self.task_output_changed_callbacks.append(self._notify_state) else: raise NotImplementedError # handle settings self._loadSettings() def _updateSettings(self): self._scans = [] for scan in self.widget.datalist._myitems: # kept for backward compatibility since 0.11. To be removed on the future version. if "@" in scan: entry, file_path = scan.split("@") nxtomo_scan = NXtomoScan(entry=entry, scan=file_path) self.add(nxtomo_scan) else: self._scans.append(scan) output_dir = self.widget.getOutputFolder() if isinstance(output_dir, ProcessDataOutputDirMode): output_dir = output_dir.value self._ewoks_default_inputs["output_dir"] = output_dir # pylint: disable=E1137 @property def request_input(self): return self.__request_input @request_input.setter def request_input(self, request): self.__request_input = request def get_task_inputs(self): return { "h5_to_nx_configuration": self.__configuration_cache.to_dict(), "serialize_output_data": False, } def handleNewSignals(self) -> None: """Invoked by the workflow signal propagation manager after all signals handlers have been called. """ # for now we want to avoid propagation any processing. # task will be executed only when the user validates the dialog bliss_scan = super().get_task_inputs().get("bliss_scan", None) if bliss_scan is not None: if not isinstance(bliss_scan, BlissScan): raise TypeError("bliss_scan is expected to be an instance of BlissScan") self.add(bliss_scan.master_file) def _convertAndSend(self, bliss_url: str): """ :param bliss_url: string at entry@file format """ logger.processStarted(f"Start translate {bliss_url} to NXTomo") self.__configuration_cache = HDF5Config.from_dict( copy(self.get_default_input_values()["h5_to_nx_configuration"]) ) identifier = NXtomoScanIdentifier.from_str(bliss_url) bliss_scan = BlissScan( master_file=identifier.file_path, entry=identifier.data_path, proposal_file=None, ) output_file_path = H5ToNxProcess.deduce_output_file_path( bliss_scan.master_file, entry=bliss_scan.entry, outputdir=self.widget.getOutputFolder(), scan=bliss_scan, ) self.__configuration_cache.input_file = bliss_scan.master_file self.__configuration_cache.output_file = output_file_path self.__configuration_cache.entries = (bliss_scan.entry,) self.__configuration_cache.single_file = False self.__configuration_cache.overwrite = True self.__configuration_cache.request_input = self.request_input self.__configuration_cache.file_extension = ".nx" self._processBlissScan(bliss_scan) def _userAgreeForOverwrite(self, file_path): if self._canOverwriteOutputs: return True else: msg = OverwriteMessage(self) text = "NXtomomill will overwrite \n %s. Do you agree ?" % file_path msg.setText(text) if msg.exec_(): self._canOverwriteOutputs = msg.canOverwriteAll() return True else: return False def _processBlissScan(self, bliss_scan): if bliss_scan is None: return output_file_path = H5ToNxProcess.deduce_output_file_path( bliss_scan.master_file, entry=bliss_scan.entry, outputdir=self.widget.getOutputFolder(), scan=bliss_scan, ) # check user has rights to write on the folder dirname = os.path.dirname(output_file_path) if os.path.exists(dirname) and not os.access(dirname, os.W_OK): msg = qt.QMessageBox(self) msg.setIcon(qt.QMessageBox.Warning) text = f"You don't have write rights on '{dirname}'. Unable to generate the nexus file associated to {str(bliss_scan)}" msg.setWindowTitle("No rights to write") msg.setText(text) msg.show() return # check if need to overwrite the file elif os.path.exists(output_file_path): if not self._userAgreeForOverwrite(output_file_path): return # keep 'h5_to_nx_configuration' up to date according to input folder and output_file updates self.update_default_inputs( h5_to_nx_configuration=self.__configuration_cache.to_dict() ) try: self._execute_ewoks_task( # pylint: disable=E1123 propagate=True, log_missing_inputs=False, ) except Exception: self._execute_ewoks_task(propagate=True) # pylint: disable=E1123, E1120 def _loadSettings(self): with blockSignals(self.widget): for scan in self._scans: assert isinstance(scan, str) try: self.widget.add(scan) except Exception as e: logger.error(f"Fail to add {scan}. Error is {e}") else: logger.warning(f"{scan} is an invalid link to a file") if ( "nxtomomill_cfg_file" in self._ewoks_default_inputs ): # pylint: disable=E1135 nxtomo_cfg_file = self._ewoks_default_inputs[ # pylint: disable=E1136 "nxtomomill_cfg_file" ] self.widget.setCFGFilePath(nxtomo_cfg_file) if "output_dir" in self._ewoks_default_inputs: # pylint: disable=E1135 self.widget.setOutputFolder( self._ewoks_default_inputs["output_dir"] # pylint: disable=E1136 ) def getHDF5Config(self): configuration_file = self.widget.getCFGFilePath() if configuration_file in (None, ""): configuration = HDF5Config() else: try: configuration = HDF5Config.from_cfg_file(configuration_file) except Exception as e: logger.error( f"Fail to use configuration file {configuration_file}. Error is {e}. No conversion will be done." ) return None return configuration def _saveNXTomoCfgFile(self, cfg_file): super()._saveNXTomoCfgFile(cfg_file, keyword="h5_to_nx_configuration") def _sendSelected(self): """Send a signal for selected scans found to the next widget""" self._inputGUI = NXTomomillInput() # reset the GUI for input (reset all the cache for answers) self._canOverwriteOutputs = False for bliss_url in self.widget.datalist.selectedItems(): data = bliss_url.data(qt.Qt.UserRole) assert isinstance(data, NXtomoScan) identifier = data.get_identifier() self._inputGUI.setBlissScan( entry=identifier.data_path, file_path=identifier.file_path ) self._convertAndSend(identifier.to_str()) def _sendAll(self): """Send a signal for each scan found to the next widget""" self._inputGUI = NXTomomillInput() # reset the GUI for input (reset all the cache for answers) self._canOverwriteOutputs = False for bliss_url in self.widget.datalist._myitems.values(): data = bliss_url.data(qt.Qt.UserRole) identifier = data.get_identifier() assert isinstance(data, NXtomoScan) self._inputGUI.setBlissScan( entry=identifier.data_path, file_path=identifier.file_path ) self._convertAndSend(identifier.to_str()) def _notify_state(self): try: task_executor = self.sender() task_suceeded = task_executor.succeeded config = task_executor.current_task.inputs.h5_to_nx_configuration config = HDF5Config.from_dict(config) scan = task_executor.current_task.outputs.data if task_suceeded: self.notify_succeed(scan=scan) else: self.notify_failed(scan=scan) except Exception as e: logger.error(f"failed to handle task finished callback. Reason is {e}") def trigger_downstream(self) -> None: for ewoksname, var in self.get_task_outputs().items(): # note: for now we want to trigger 'data' for each items of 'datas' if ewoksname == "series" and not ( invalid_data.is_invalid_data(var.value) or var.value is None ): for data in var.value: data_channel = self._get_output_signal("data") data_channel.send(data) serie_channel = self._get_output_signal("series") # then send the list of value / series (also know as datas) serie_channel.send(var.value) elif ewoksname == "data" and not ( invalid_data.is_invalid_data(var.value) or var.value is None ): pass # handle by 'series' in this case def keyPressEvent(self, event): # forward Ctrl+A to the list as the shift ease selection of all modifiers = event.modifiers() key = event.key() if key == qt.Qt.Key_A and modifiers == qt.Qt.KeyboardModifier.ControlModifier: self.widget._widget.datalist.keyPressEvent(event) super().keyPressEvent(event) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739271825.0 tomwer-1.4.8/src/orangecontrib/tomwer/widgets/control/NXtomoConcatenate.py0000644000175000017500000001445614752627221026212 0ustar00paynopaynofrom __future__ import annotations import logging from tomoscan.series import Series from orangewidget import gui from orangewidget.settings import Setting from orangewidget.widget import Input, Output from orangecontrib.tomwer.orange.managedprocess import TomwerWithStackStack from silx.gui import qt from tomwer.core.scan.scanbase import TomwerScanBase from tomwer.core.scan.nxtomoscan import NXtomoScan from tomwer.core.process.control.nxtomoconcatenate import ( ConcatenateNXtomoTask, format_output_location, ) from tomwer.gui.control.series.nxtomoconcatenate import NXtomoConcatenateWidget _logger = logging.getLogger(__name__) class NXtomoConcatenateOW( TomwerWithStackStack, ewokstaskclass=ConcatenateNXtomoTask, ): """ widget used to call do a concatenation of a serie (of NXtomo) into a single Nxtomo :param parent: the parent widget """ # note of this widget should be the one registered on the documentation name = "NXtomo concatenation" id = "orangecontrib.tomwer.widgets.control.NXtomoConcatenate.NXtomoConcatenate" description = "concatenate a serie (of NXtomo / NXtomoScan) into a single Nxtomo" icon = "icons/concatenate_nxtomos.svg" priority = 200 keywords = [ "tomography", "nabu", "reconstruction", "concatenate", "NXtomo", "data", "NXtomoScan", ] want_main_area = True want_control_area = False resizing_enabled = True _ewoks_default_inputs = Setting( { "series": None, "output_file": "{common_path}/concatenate.nx", "output_entry": "entry0000", "overwrite": False, } ) _ewoks_inputs_to_hide_from_orange = ( "output_entry", "serialize_output_data", "progress", "output_file", "overwrite", ) TIMEOUT = 30 class Inputs: series = Input( name="series", type=Series, doc="series to concatenate", default=True, multiple=False, ) class Outputs: data = Output(name="data", type=TomwerScanBase, doc="concatenated scan") LOGGER = _logger def __init__(self, parent=None): super().__init__(parent) self.__series = None # gui definition _layout = gui.vBox(self.mainArea, self.name).layout() self.widget = NXtomoConcatenateWidget(parent=self) _layout.addWidget(self.widget) ## connect signal / slot self.widget.sigConfigChanged.connect(self._updateSettings) if isinstance(self.task_output_changed_callbacks, set): self.task_output_changed_callbacks.add(self._notify_state) elif isinstance(self.task_output_changed_callbacks, list): self.task_output_changed_callbacks.append(self._notify_state) else: raise NotImplementedError ## handle settings self._loadSettings() self.task_executor_queue.sigComputationStarted.connect(self._newTaskStarted) def _updateSettings(self): config = self.widget.getConfiguration() for key in ("output_file", "output_entry", "overwrite"): self._ewoks_default_inputs[key] = config[key] # pylint: disable=E1137 @property def request_input(self): return self.__request_input @request_input.setter def request_input(self, request): self.__request_input = request def get_task_inputs(self): assert self.__series is not None return { "series": self.__series, "output_file": self.widget.getConfiguration()["output_file"], "output_entry": self.widget.getConfiguration()["output_entry"], "overwrite": self.widget.getConfiguration()["overwrite"], } def handleNewSignals(self) -> None: """Invoked by the workflow signal propagation manager after all signals handlers have been called. """ # for now we want to avoid propagation any processing. # task will be executed only when the user validates the dialog data = super().get_task_inputs().get("data", None) if data is not None: if not isinstance(data, NXtomoScan): raise TypeError( f"data is expected to be an instance of NXtomoScan. {type(data)} are not handled" ) self.add(data.path) def _loadSettings(self): self.widget.setConfiguration(self._ewoks_default_inputs) def _newTaskStarted(self): try: task_executor = self.sender() scan_about_to_be_created = NXtomoScan( scan=format_output_location( file_path=task_executor.current_task.inputs.output_file_path, series=task_executor.current_task.inputs.series, ), entry=task_executor.current_task.inputs.output_entry, ) self.notify_on_going(scan_about_to_be_created) except Exception: pass def _notify_state(self): try: task_executor = self.sender() task_suceeded = task_executor.succeeded scan = task_executor.current_task.outputs.data if task_suceeded: self.notify_succeed(scan=scan) else: self.notify_failed(scan=scan) except Exception as e: _logger.error(f"failed to handle task finished callback. Reason is {e}") @Inputs.series def process_series(self, series: Series | None): self._process_series(series=series) def _process_series(self, series: Series | None): if series is None: return else: self.__series = series scan_about_to_be_created = NXtomoScan( scan=format_output_location( file_path=self.getOutputFilePath(), series=series, ), entry=self.getOutputEntry(), ) self.notify_pending(scan=scan_about_to_be_created) self.execute_ewoks_task() def sizeHint(self) -> qt.QSize: return qt.QSize(500, 200) # expose API def getOutputFilePath(self) -> str: return self.widget.getOutputFilePath() def getOutputEntry(self) -> str: return self.widget.getOutputEntry() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739271825.0 tomwer-1.4.8/src/orangecontrib/tomwer/widgets/control/NotifierOW.py0000644000175000017500000000633514752627221024643 0ustar00paynopaynofrom __future__ import annotations from orangewidget import gui, settings, widget from orangewidget.widget import Input, Output from silx.gui import qt from tomwer.core.tomwer_object import TomwerObject from tomwer.core.utils.deprecation import deprecated_warning class NotifierWidgetOW(widget.OWBaseWidget): """ simple widget which pop up and closes when recive a new object """ name = "notifier" id = "orangecontrib.tomwer.widgets.control.NotifierOW" description = "Simple widget which pop up for 2 second when recives a new object" icon = "icons/notification.png" priority = 145 keywords = ["control", "notifier", "notification"] want_main_area = True want_control_area = False resizing_enabled = False _muted = settings.Setting(False) class Inputs: tomo_obj = Input(name="tomo_obj", type=TomwerObject, multiple=True) class Outputs: tomo_obj = Output(name="tomo_obj", type=TomwerObject) def __init__(self, parent=None): super().__init__(parent) deprecated_warning( type_="class", name="orangecontrib.tomwer.widgets.control.NotifierOW", replacement="orangecontrib.ewoksnotify.ToneNotifierOW", reason="moved to ewoksnotify (pip install ewoksnotify[full] to get the orange widget)", since_version="1.4", ) self.pop_up = None layout = gui.vBox(self.mainArea, self.name).layout() self._soundButton = qt.QPushButton(parent=self) self._soundButton.setMinimumSize(150, 100) self._soundButton.setCheckable(True) layout.addWidget(self._soundButton) self._updateButtonIcon() # connect signal / slot self._soundButton.toggled.connect(self._switchMute) def _switchMute(self): self._muted = not self._muted self._updateButtonIcon() @Inputs.tomo_obj def process(self, tomo_obj, *args, **kwargs): self.notify(tomo_obj) self.Outputs.tomo_obj.send(tomo_obj) def _updateButtonIcon(self): style = qt.QApplication.style() if self._muted: icon = style.standardIcon(qt.QStyle.SP_MediaVolumeMuted) else: icon = style.standardIcon(qt.QStyle.SP_MediaVolume) self._soundButton.setIcon(icon) def notify(self, tomo_obj): if self.pop_up is not None: self.pop_up.close() if not self._muted: # emit sound when requested try: qt.QApplication.beep() except Exception: pass self.pop_up = NotificationMessage() text = f"Object {tomo_obj} received." self.pop_up.setText(text) self.pop_up.show() class NotificationMessage(qt.QMessageBox): EXPOSITION_TIME = 3000 # in ms def __init__(self) -> None: super().__init__() self.setModal(False) self.setIcon(qt.QMessageBox.Information) self.addButton( f"Ok - will close automatically after {self.EXPOSITION_TIME / 1000}s", qt.QMessageBox.YesRole, ) def show(self): super().show() timer = qt.QTimer(self) timer.singleShot( self.EXPOSITION_TIME, self.close, ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1736265726.0 tomwer-1.4.8/src/orangecontrib/tomwer/widgets/control/ReduceDarkFlatSelectorOW.py0000644000175000017500000000543114737247776027421 0ustar00paynopaynofrom __future__ import annotations import logging from orangewidget import gui from orangewidget.settings import Setting from orangewidget.widget import Input, Output, OWBaseWidget from tomwer.gui.control.reducedarkflatselector import ReduceDarkFlatSelectorDialog logger = logging.getLogger(__name__) class ReduceDarkFlatSelectorOW(OWBaseWidget, openclass=True): name = "reduce dark-flat selector" id = "orangecontrib.widgets.tomwer.control.ReduceDarkFlatSelectorOW.ReduceDarkFlatSelectorOW" description = "Allow user to select one or several reduced dark / flat" icon = "icons/reduced_darkflat_selector.svg" priority = 242 keywords = [ "tomography", "selection", "tomwer", "scan", "data", "reduce", "dark", "flat", "reduced", ] want_main_area = True want_control_area = False resizing_enabled = True _configuration = Setting(tuple()) class Inputs: in_reduced_frames = Input( name="reduced frames", type=dict, doc="dict of containing reduced frames (either dark or flat)", multiple=True, ) class Outputs: out_reduced_darks = Output( name="reduced dark(s)", doc="dict of containing reduced darks(s)", type=dict, ) out_reduced_flats = Output( name="reduced flat(s)", type=dict, doc="dict of containing reduced flat(s)", ) def __init__(self, parent=None): """ """ super().__init__(parent) self._dialog = ReduceDarkFlatSelectorDialog(parent=self) self._layout = gui.vBox(self.mainArea, self.name).layout() self._layout.addWidget(self._dialog) self._dialog.setConfiguration(self._configuration) # connect signal / slot self._dialog.sigSelectActiveAsDarks.connect(self._sendDarks) self._dialog.sigSelectActiveAsFlats.connect(self._sendFlats) self._dialog.sigUpdated.connect(self._updateConfiguration) def _updateConfiguration(self): self._configuration = self.getConfiguration() def _sendDarks(self, darks: dict): self.Outputs.out_reduced_darks.send(darks) def _sendFlats(self, flats: dict): self.Outputs.out_reduced_flats.send(flats) @Inputs.in_reduced_frames def addReduceFrames(self, reduce_frames: dict | None, *args, **kwargs): if reduce_frames is not None: self._dialog.addReduceFrames(reduce_frames) # expose API def getConfiguration(self) -> tuple: return self._dialog.getConfiguration() def setConfiguration(self, configuration: tuple) -> None: if configuration is None: return return self._dialog.setConfiguration(configuration) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739271825.0 tomwer-1.4.8/src/orangecontrib/tomwer/widgets/control/SingleTomoObjOW.py0000644000175000017500000001251714752627221025576 0ustar00paynopayno# coding: utf-8 from __future__ import annotations import logging import tomoscan.esrf.scan.utils from orangewidget import gui from orangewidget.settings import Setting from orangewidget.widget import Input, Output, OWBaseWidget import tomwer.core.process.control.singletomoobj from tomwer.core.scan.scanbase import TomwerScanBase from tomwer.core.scan.scanfactory import ScanFactory from tomwer.core.tomwer_object import TomwerObject from tomwer.core.volume.volumefactory import VolumeFactory from tomwer.gui.control.singletomoobj import SingleTomoObj _logger = logging.getLogger(__name__) class SingleTomoObjOW(OWBaseWidget, openclass=True): name = "single tomo obj" id = "orange.widgets.tomwer.control.SingleScanOW.SingleScanOW" description = "Definition of a single dataset" icon = "icons/single_tomo_obj.svg" priority = 51 keywords = ["tomography", "NXtomo", "tomwer", "folder", "scan"] want_main_area = True want_control_area = False resizing_enabled = True ewokstaskclass = tomwer.core.process.control.singletomoobj.SingleTomoObjProcess _tomo_obj_setting = Setting(str()) class Inputs: tomo_obj = Input(name="tomo_obj", type=TomwerObject) class Outputs: tomo_obj = Output( name="tomo_obj", type=TomwerObject, doc="one object to be process" ) reduced_darks = Output( name="reduced dark(s)", type=dict, doc="Reduced darks as a dict", ) reduced_flats = Output( name="reduced flat(s)", type=dict, doc="Reduced flats as a dict", ) relative_reduced_darks = Output( name="relative reduced dark(s)", type=dict, doc="Reduced darks as a dict. Indexes are provided as relative so in [0.0, 1.0[", ) relative_reduced_flats = Output( name="relative reduced flat(s)", type=dict, doc="Reduced flats as a dict. Indexes are provided as relative so in [0.0, 1.0[", ) def __init__(self, parent=None): super().__init__(parent) self._latest_received_scan = None # small work around to keep in scan processing cache and avoid recomputing it if not necessary self.widget = SingleTomoObj(parent=self) self.widget.sigTomoObjChanged.connect(self._updateSettings) self.widget.sigTomoObjChanged.connect(self._triggerObjDownstream) self._loadSettings() layout = gui.vBox(self.mainArea, self.name).layout() layout.addWidget(self.widget) @Inputs.tomo_obj def add(self, tomo_obj): if tomo_obj is None: return self.widget.setTomoObject(tomo_obj) def _loadSettings(self): if self._tomo_obj_setting != str(): self.widget.setTomoObject(self._tomo_obj_setting) def _updateSettings(self): self._tomo_obj_setting = self.widget.getTomoObjIdentifier() def _triggerObjDownstream(self): obj_identifier = self.widget.getTomoObjIdentifier() if ( self._latest_received_scan is not None and self._latest_received_scan.get_identifier() == obj_identifier ): data = self._latest_received_scan else: try: data = VolumeFactory.create_tomo_object_from_identifier(obj_identifier) except: # noqa E722 try: data = ScanFactory.create_tomo_object_from_identifier( obj_identifier ) except: # noqa E722 _logger.warning( f"Unable to find an obj with {obj_identifier} as identifier" ) return self.Outputs.tomo_obj.send(data) # if the data is a scan then provide also access to the reduced frames. Can be convenient if isinstance(data, TomwerScanBase): if data.reduced_darks not in (None, {}): reduced_darks = data.reduced_darks self.Outputs.reduced_darks.send(reduced_darks) # we want to send those in relative position to have something generic. This is a convention for now reduced_darks = ( tomoscan.esrf.scan.utils.from_absolute_reduced_frames_to_relative( reduced_frames=data.reduced_darks, scan=data ) ) reduced_darks["reduce_frames_name"] = ( f"darks from {data.get_identifier().short_description()}" ) self.Outputs.relative_reduced_darks.send(reduced_darks) if data.reduced_flats not in (None, {}): reduced_flats = data.reduced_flats self.Outputs.reduced_flats.send(reduced_flats) # we want to send those in relative position to have something generic. This is a convention for now reduced_flats = ( tomoscan.esrf.scan.utils.from_absolute_reduced_frames_to_relative( reduced_frames=data.reduced_flats, scan=data ) ) reduced_flats["reduce_frames_name"] = ( f"flats from {data.get_identifier().short_description()}" ) self.Outputs.relative_reduced_flats.send(reduced_flats) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739271825.0 tomwer-1.4.8/src/orangecontrib/tomwer/widgets/control/TimerOW.py0000644000175000017500000000656214752627221024146 0ustar00paynopaynofrom __future__ import annotations import functools import logging from orangewidget import gui from orangewidget.settings import Setting from orangewidget.widget import Input, Output from processview.core.manager import DatasetState, ProcessManager from silx.gui import qt import tomwer.core.process.control.timer from orangecontrib.tomwer.orange.managedprocess import SuperviseOW from tomwer.core.scan.scanbase import TomwerScanBase from tomwer.utils import docstring from tomwer.core.utils.deprecation import deprecated_warning _logger = logging.getLogger(__name__) class _TimerWidget(qt.QWidget): def __init__(self, parent, _time=None): if _time is not None: assert type(_time) is int qt.QWidget.__init__(self, parent) self.setLayout(qt.QGridLayout()) self.layout().addWidget(qt.QLabel("time to wait (in sec):", parent=self), 0, 0) self._timeLE = qt.QSpinBox(parent=self) self._timeLE.setMinimum(0) self._timeLE.setValue(_time or 1) self.layout().addWidget(self._timeLE, 0, 1) spacer = qt.QWidget(parent=self) spacer.setSizePolicy(qt.QSizePolicy.Minimum, qt.QSizePolicy.Expanding) self.layout().addWidget(spacer, 1, 0) # expose API self.timeChanged = self._timeLE.valueChanged class TimerOW(SuperviseOW): name = "timer" id = "orange.widgets.tomwer.filterow" description = ( "Simple widget which wait for a defined amont of time and" "release the data" ) icon = "icons/time.png" priority = 200 keywords = ["control", "timer", "wait", "data"] want_main_area = True resizing_enabled = True _waiting_time = Setting(int(1)) ewokstaskclass = tomwer.core.process.control.timer.TimerTask class Inputs: data = Input(name="data", type=TomwerScanBase, multiple=True) class Outputs: data = Output(name="data", type=TomwerScanBase) def __init__(self, parent=None): """ """ SuperviseOW.__init__(self, parent) deprecated_warning( type_="class", name="orangecontrib.tomwer.widgets.control.TimerOW", replacement="orangecontrib.ewoksnotify.WaiterOW", reason="moved to ewoksnotify (pip install ewoksnotify[full] to get the orange widget)", since_version="1.4", ) self._widget = _TimerWidget(parent=self, _time=self._waiting_time) self._widget.setContentsMargins(0, 0, 0, 0) layout = gui.vBox(self.mainArea, self.name).layout() layout.addWidget(self._widget) self._widget.timeChanged.connect(self._updateTime) @Inputs.data def process(self, scan, *args, **kwargs): if scan is None: return ProcessManager().notify_dataset_state( dataset=scan, process=self, state=DatasetState.ON_GOING, ) callback = functools.partial(self._get_out, scan) _timer = qt.QTimer(self) _timer.singleShot(self._waiting_time * 1000, callback) def _updateTime(self, newTime): self._waiting_time = newTime def _get_out(self, scan): ProcessManager().notify_dataset_state( dataset=scan, process=self, state=DatasetState.SUCCEED, ) self.Outputs.data.send(scan) @docstring(SuperviseOW) def reprocess(self, dataset): self.process(dataset) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739271825.0 tomwer-1.4.8/src/orangecontrib/tomwer/widgets/control/TomoObjSeriesOW.py0000644000175000017500000000356114752627221025606 0ustar00paynopayno# coding: utf-8 from __future__ import annotations import logging from orangewidget import gui from orangewidget.widget import Input, Output, OWBaseWidget from tomoscan.series import Series import tomwer.core.process.control.tomoobjseries from tomwer.core.tomwer_object import TomwerObject from tomwer.gui.control.series.seriescreator import SeriesWidgetDialog logger = logging.getLogger(__name__) class TomoObjSeriesOW(OWBaseWidget, openclass=True): name = "series of objects" id = "orange.widgets.tomwer.tomoobjseriesow" description = "Allow user define a series of object that will be defined as a Series (grouped together and can be used within a purpose like stitching)" icon = "icons/tomoobjseries.svg" priority = 55 keywords = ["tomography", "selection", "tomwer", "series", "group"] ewokstaskclass = tomwer.core.process.control.tomoobjseries._TomoobjseriesPlaceHolder want_main_area = True want_control_area = False resizing_enabled = True class Inputs: tomo_obj = Input(name="tomo obj", type=TomwerObject, multiple=True) class Outputs: series = Output(name="series", type=Series) def __init__(self, parent=None): """ """ super().__init__(parent) layout = gui.vBox(self.mainArea, self.name).layout() self._widget = SeriesWidgetDialog(self) layout.addWidget(self._widget) # connect signal / slot self._widget.sigSeriesSelected.connect(self._send_series) @Inputs.tomo_obj def addTomoObj(self, tomo_obj, *args, **kwargs): if tomo_obj is not None: self._widget.add(tomo_obj) def _send_series(self, series: Series): if not isinstance(series, Series): raise TypeError( f"series is expected to be an instance of {Series}. Not {type(series)}" ) self.Outputs.series.send(series) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739271825.0 tomwer-1.4.8/src/orangecontrib/tomwer/widgets/control/VolumeSelector.py0000644000175000017500000000765514752627221025574 0ustar00paynopayno# coding: utf-8 from __future__ import annotations import logging from orangewidget import gui from orangewidget.settings import Setting from orangewidget.widget import Input, Output, OWBaseWidget from silx.gui import qt import tomwer.core.process.control.volumeselector from tomwer.core.scan.nxtomoscan import NXtomoScan from tomwer.core.volume.volumebase import TomwerVolumeBase from tomwer.gui.control.volumeselectorwidget import VolumeSelectorWidget logger = logging.getLogger(__name__) class VolumeSelectorOW(OWBaseWidget, openclass=True): name = "volume selector" id = "orange.widgets.tomwer.volumeselector" description = ( "List all received volumes. Then user can select a specific" "volume to be passed to the next widget." ) icon = "icons/volumeselector.svg" priority = 62 keywords = ["tomography", "selection", "tomwer", "volume"] ewokstaskclass = ( tomwer.core.process.control.volumeselector._VolumeSelectorPlaceHolder ) want_main_area = True want_control_area = False resizing_enabled = True _scanIDs = Setting(list()) class Inputs: volume = Input(name="volume", type=TomwerVolumeBase, multiple=True) class Outputs: volume = Output(name="volume", type=TomwerVolumeBase) def __init__(self, parent=None): """ """ super().__init__(parent) self.widget = VolumeSelectorWidget(parent=self) self._loadSettings() self.widget.sigUpdated.connect(self._updateSettings) self.widget.sigSelectionChanged.connect(self.changeSelection) layout = gui.vBox(self.mainArea, self.name).layout() layout.addWidget(self.widget) # expose API self.setActiveScan = self.widget.setActiveData self.selectAll = self.widget.selectAll self.add = self.widget.add @Inputs.volume def _volumeReceived(self, volume, *args, **kwargs): self.addVolume(volume) def addVolume(self, volume): if volume is not None: self.widget.add(volume) def removeVolume(self, volume): if volume is not None: self.widget.remove(volume) def changeSelection(self, list_volume): if list_volume: for volume_id in list_volume: volume = self.widget.dataList.getVolume(volume_id, None) if volume is not None: assert isinstance(volume, TomwerVolumeBase) self.Outputs.volume.send(volume) else: logger.error(f"{volume_id} not found the list") def send(self): """send output signals for each selected items""" sItem = self.widget.dataList.selectedItems() if sItem and len(sItem) >= 1: selection = [_item.text() for _item in sItem] self.changeSelection(list_volume=selection) def _loadSettings(self): for scan in self._scanIDs: assert isinstance(scan, str) # kept for backward compatibility since 0.11. To be removed on the future version. if "@" in scan: entry, file_path = scan.split("@") nxtomo_scan = NXtomoScan(entry=entry, scan=file_path) self.addVolume(nxtomo_scan) else: self.addVolume(scan) def _updateSettings(self): self._scanIDs = [] for scan in self.widget.dataList._myitems: self._scanIDs.append(scan) def keyPressEvent(self, event): """ To shortcut orange and make sure the `delete` key will be interpreted we need to overwrite this function """ modifiers = event.modifiers() key = event.key() if key == qt.Qt.Key_A and modifiers == qt.Qt.KeyboardModifier.ControlModifier: self.widget.dataList.keyPressEvent(event) if key == qt.Qt.Key_Delete: self.widget._callbackRemoveSelectedDatasets() else: super().keyPressEvent(event) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1737644878.0 tomwer-1.4.8/src/orangecontrib/tomwer/widgets/control/__init__.py0000644000175000017500000000062614744455516024361 0ustar00paynopayno""" (Ewoks)Orange Widgets to 'control' the flow. Like defining input / output, stacking scans... Those widget are not doing any 'core' processing. """ NAME = "control" ID = "orangecontrib.tomwer.widgets.control" DESCRIPTION = "widgets for data control" LONG_DESCRIPTION = "Contains widget to control the scan flow" ICON = "../../widgets/icons/tomwer_control.png" BACKGROUND = "#C0CCFF" PRIORITY = 1 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1739271865.8797636 tomwer-1.4.8/src/orangecontrib/tomwer/widgets/control/icons/0000755000175000017500000000000014752627272023356 5ustar00paynopayno././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1731088693.0 tomwer-1.4.8/src/orangecontrib/tomwer/widgets/control/icons/advancement.png0000644000175000017500000000133114713450465026342 0ustar00paynopaynoPNG  IHDR/<sBIT|d pHYs 6 6+=tEXtSoftwarewww.inkscape.org<VIDAT8KkQmrմEh1RjDJAPЊЍm7RvJb Vk5ŶXM&d&.Bқp{9Z6 J 7u{:v{˕$J4M;b:nQjյ=]/{.H,l:]Jei$/,\g$F61G]ªthQ\=۹\nh>ZQ2r풧+TŽ˩WmjiB~dt'/`mnT-tulTZDEQe]dY4ە:@oKu/74۟D<Z'Ҋ,3T*uń= P] 8ѫc\1F 0_*_SMgfvNR? i_Ōl_cI 09߲YRfM& y8|l Ŷ image/svg+xml ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1731088693.0 tomwer-1.4.8/src/orangecontrib/tomwer/widgets/control/icons/concatenate_nxtomos.png0000644000175000017500000000140014713450465030125 0ustar00paynopaynoPNG  IHDRsBIT|d pHYs 6 6+=tEXtSoftwarewww.inkscape.org<}IDATHKhUW^HcP :Ah(P(8RI )v"֢"HT(UE4>k)XZW 8Y9 _\@+NA0T` .a53en( '`/$&%ƽ+a-Sl@w%vE2 ]?߸5s5`(r]h`*c2^MhzE^eؑ䖨IY},$5،%|ԕ؊N\>|ˠX&ӁFSQ `6<pJQ _?ኘQA`[E>C!~ aBdyMb.aƈkDy :MipS Os//EBq&"hªD ~m?/ :*>Ʉ:pKKTbH]^jk%qD рb~3ۢk2:\-^m"E} yUl)/+QrѤi~qjI"mA.ӓK>QQh ,EM)U7v=LbJc؎G[Ѷ2kOIENDB`././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1731088693.0 tomwer-1.4.8/src/orangecontrib/tomwer/widgets/control/icons/concatenate_nxtomos.svg0000644000175000017500000003375614713450465030163 0ustar00paynopayno image/svg+xml ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1731088693.0 tomwer-1.4.8/src/orangecontrib/tomwer/widgets/control/icons/datadiscover.png0000644000175000017500000000112614713450465026527 0ustar00paynopaynoPNG  IHDRysBIT|d pHYs 6 6+=tEXtSoftwarewww.inkscape.org<IDAT8ՓMHaxf:jK^!E4Ju( (J$P]6Pq%bC)saR6Bq*l"qYʡY2|U I 6ҸIrL;)eD=9&ϞpЫ/|pj[e>FZ#pY&ơ \;L%ٸ͜k{A$a @C-S ~@ B' ϲ"Z:IENDB`././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1731088693.0 tomwer-1.4.8/src/orangecontrib/tomwer/widgets/control/icons/datadiscover.svg0000644000175000017500000000727414713450465026554 0ustar00paynopayno image/svg+xml ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1731088693.0 tomwer-1.4.8/src/orangecontrib/tomwer/widgets/control/icons/datalistener.png0000644000175000017500000000406414713450465026542 0ustar00paynopaynoPNG  IHDRNGqsBIT|d pHYstEXtSoftwarewww.inkscape.org<IDATxy_C?^7E.תRKgoZ)B-ZK[m- *VPZj)1{νM~ޝ9̹3g3o^ u̔y@f <2TEѸuvF<ԁdkw&3p!/V8Ax~hS܄Cy7FC;Osn%r42G7Fev(0иSM/y\Mb | ,*C<4'rGL8DF-@s"R1؅#`$/6WotA6p`Ly6Vv6SLEBUHiWƤN m3AO?"9~[*.Lo~8'Y,ū+6^C91%IׄhOv]L0⊅m2n! \#ۚl4k/C U{ 2+kD+l!6 $/QRUq+ as_*n.c-Hɦ|*;ƵQq=Ђ1@/"QЌJQPR2:MhXBv =(׆}vGn>KHr ysЈ#. Vqvč!T3Z}5em x`$?"v> XCyP@J=ǞCccyw cB{tcZ8xMVԩe[$t䛶̸=YQ`M< mшl"sPVSqb s=0 @aGqg/JBh!ځ=jiE;å'0ʼ!4rk˝*;0۵„hNn$87=g($߀tq硣Iq,0O @+l6MŇ鶣 '̙d{xz3UhQ1gZMV\.,^14";}J0[=\vq`+:?Y}1(b1!Ϣ u(ߩ(xdJi8,64h \= F\ 0-)MH y8 ,!B&VSSnIG>K3ڎr}ݲ:q5ɶľ~Y9t!OA<&0,\ucpgkr#)P-8m11D[HOA(fu4L=pJ[ gqÑ=ˉe{ }7okܽy}N86PԢ{pdIENDB`././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1731088693.0 tomwer-1.4.8/src/orangecontrib/tomwer/widgets/control/icons/datalistener.svg0000644000175000017500000000610014713450465026546 0ustar00paynopayno image/svg+xml ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1731088693.0 tomwer-1.4.8/src/orangecontrib/tomwer/widgets/control/icons/datawatcher.svg0000644000175000017500000000642714713450465026372 0ustar00paynopayno image/svg+xml ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1731088693.0 tomwer-1.4.8/src/orangecontrib/tomwer/widgets/control/icons/edf2nx.png0000644000175000017500000000243214713450465025246 0ustar00paynopaynoPNG  IHDR,*ݹsBIT|d pHYs j juc-tEXtSoftwarewww.inkscape.org<IDATX՘kLUB[ n\&!("8g[ʦ-ƅe.1&.|KDG"Sc$fGbRtD\}eʸKopڞK>9s;/fPأĩ3ID7kտs}]nk"#Q JceG:y&afQ-qEw0ro,eǢO?Oq"@+Gvy;bGUjþ&姞6:dl)boN5+L( 2滨z%޴Y}:4=vL-4<҅%A`ޮGUlͫ06dࣷ6Q0Á# s rK # d/EݰdM ]iVɭq;&|TXNw"lC&{Cea^9 @ W4t:4m{ #x"(pmȵcRzgؠAF5^g#!eOU}(je>%'D0&$@9P4!؃.ra 6.s> y ]I@LxS*&Fgx"mGC'{xPױʄO"*Ӿ{8Qz ' %A'l` ⢊߶qt+P?uu4z)RnDH;K@{e 8,IENDB`././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1731088693.0 tomwer-1.4.8/src/orangecontrib/tomwer/widgets/control/icons/edf2nx.svg0000644000175000017500000001557714713450465025277 0ustar00paynopayno image/svg+xml ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1731088693.0 tomwer-1.4.8/src/orangecontrib/tomwer/widgets/control/icons/email.png0000644000175000017500000000136314713450465025151 0ustar00paynopaynoPNG  IHDR00W pHYsodtEXtSoftwarewww.inkscape.org<IDAThOUU.FH"A`FAeQ? YA qHD k!^AA,J h={w}}./g}o^'Ueu ^@57I68~ťN"fOC~ǻpb{pEra 5?X؏?YUu $sI:$&ϰͪUYLrZ>InxL2MTUIY6SuI?Q įhZ}CAM`)4M%YIҜ5 رŦ{^nhu Ub#:~O=&iK/e/kz] ^@ ėpS]slئ7܅aPm\Y!`֘tM/kz]3(iIENDB`././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1731088693.0 tomwer-1.4.8/src/orangecontrib/tomwer/widgets/control/icons/email.svg0000644000175000017500000000345414713450465025167 0ustar00paynopayno ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1731088693.0 tomwer-1.4.8/src/orangecontrib/tomwer/widgets/control/icons/esrf.png0000644000175000017500000031660614713450465025032 0ustar00paynopaynoPNG  IHDR, cHRMz&u0`:pQ<bKGD pHYs.#.#x?vtIME 93XgcIDATx}|\gy珿<$?'-UJJ+("(M֥ov즴P.&:)!!Pz\A@ Y1(A cEe=k/14>yٲG3̹@ >prj5Xj<?w=.];Q-֐Vv{܊~܇UAOXt7BxgOnYX%qnEfPGЖNt,89O1 bVshpIb ޹8 @&-e! +ps-nfWl_9έ?ڀ[ꃤp7O^Ⱥ@k'7E%q+al}8Q:n+p%C$Mg&wOa VVl *t}}%/pjD=Oi婿}81(%>dS|;@0m<뙡^q}'8WI?,zԁƍ7VwZ˚]Lܪx8έ?ǁuSj5Qd_Kt:z?w<6ŹEMH\i?mw^m`O6mm.;..gǹ'lPm W"a՞~-ZAT-C^پscpHz$rMЦ&<!Q$숚#B;DnȰMHk !< : ْ8Hh!|@FR  5R *hvwwbH;${X?{g;a#пH6pDz,+;Ԍdmv&[7_t ?=ӜJW5c|HGG(;KN"MMQ=\YjQ2/+4+"?B=OO$z5v"߻Mثb-!n=r/e)ϊ. +_s_ʘw?Š:3 !4<" 老AYuIm-Bۤn|v8oMEv!4ߑk6[s-Azi_3E/ \_^Yڿ~}?g5$J_O}|+._^yGyMOGr&(Jj NhϊiQɮ#RDNv sq,ٞ: ((l\ar9OgD>q\bVXʋPd Js' =w=39]T^~e9"jO.ٛQ/[1;{3~B6Wpr;뮄s4C<o%T+Av˾oΉD p}<75豷};N׺IaKjaS"0po=LslI榩1Q" ?d ;.r{Z^Ⱦ\K^$PlA1|R`&a73(0PwIi}[SvavKC0DNٽ.sV2ABqL- W+i{35DG 8p'Lʬʶd-j]&ocD!=y?qVP&г<5Yg?=(A r\rrzqĢ%$h>+* ~AAQ3In@ڂY)40=$Kٟ i̮=ןig5![{8]IF , )5Hp?DQD$/%Cٳ:"En<״5M@?h0'(?6Jf%]fv3@#g_l$9+I& 8Ose|d_26DSK~B^DIIBUnz)6~] lE'dאsH'E!d$y(2LCxOu\Ҁ~=s+@DABx}+(W$e#ļ0zOLtw*&[Ap>}GK~6g0vHmO 8qQ*:X@y%`ywRMVPG> 'EH;'ql$}"G,5$1{ܻR/AP'S%j96Jpx(?'BGH4gX:G$J3)ZSlP]4)K!r=T pW;e,rB!;CIfnVy sn`kX.yya*xe%)$WNig=|:)U~(ioAD_w|/c`ORW!\h+󴓀ĴD™1LQhbq)+{؀)yFO7c=unJb"Rwe&#ƈ ѧ$P82NrvXEͪW[S%B #2f=+ O׭ @o;$g(1-0|׼5T,٬*@q9QHaQyؐFXOk) ,B"eڶγ*9XkU431iBtϠd|8nDM'Ś=~[$v_{&t0-*c2H&S!RٴZGd%9ޢ'a84^CR~DLFmVmNl)Ix5xxIQ":YUZfj +N hu p׷2MS_ox}Rl\).a]}T7@W:g(;5G5zHeHYrbzTaF=C*Z ~h:nbgmšۅ ?-0h, 愷hriM7HwآYRd( qPE*ۺf-1$5Ib'UӺdCʥ6?5E_e(G,Lo5X3rIV<ڢկ i'`cz6 #O Lk\ӅW?_fZ>KrP KU Gڳ\g$QC֭@PoNe3X_LmO~V< WgEq)>yp;:LYNb]TOi!m6 W]9S5tٟgnx1*c2+lԒ׳C'wK ϋ I6+K_K(`7:FLʴB|&'8p˧r,&yZ*?L IR\#wűQ&3rA6N}&8Ć?t32)>k"M yYaVKMyBس`Rrpɡw4`To>s۲?/[7O"IY<eM@OͿ)cQ?"3i7™93ag η1{EKOEyBUv*rY3qib˽{8k;DjNrnjP@_OIp ENɢ`ʼnE"X9%xHp~p_:V3ŸȩHf*;UYd!ZR$ŀֻة3ˆjN7pRw/BE~ZP"iÂ3rc1uv24kɶ&,ˉ>@59SKTW_Եg3M"C@bա_wu]Zr_pXOvA~ b"om8ęnƄRk&aD"c|DOÚC>ãC "J#}TeXd.DNRrUZ_"1m4YzmgEn- o'f$˩st//#B{ʚ<(8q}){2Po dHIWSTڦpGd:xJ'B.R[_wEU%Y,Fkqb-N%ў<5eb jH7XDSCv=ĽdQZ4]F)1 I Lv}㢳}Y cc}Tu4(uv?1C8,h"Jh{E8"0**꽢{RWOW<+WKG(P٠- tkV& ԓJħ ,%Kgkfb%Hy.}eYݏMTs@z-T~2:uكh 2]xcq|P#emCuG;(@QQ;K=wG(h3CjyEC^HtK9ja΋BTٸ؏59rfdWMiѠ0i3dsؘ3ͽ< AK~ <{V/) O4T&e/28Aqj̞`pGylArzpg_ɫ޶E> e5{t@ <МH)7&j>*K užLc}gci/F~ bpj)aw$qEE~e=2 LɐҸՕs,>]c1 8 E6R.[ΈCʘ_0RY ȍfvr*7R]wgt kd֟߯~ ϴV<޴S?Lo{)inٗȀ.$r:B^khKCvoxŸG[I;ǜr,>UbE6;({Djڐ*F&)0݁qr3p^.q D5+SY*LI/үPx^Y7~g=)#@Ygo uX]Ehk7.Qx^>EyLl$65o2"3efgRC\aA-X.͸r|)[Ji YPVZe6WҰ ^&GhmMPh_|DLi(۔~|γ~?}(8AQ~4Q =l15E9KK5 W\ntQq<[J(1V;h [h)Ǹ^U֑c U y Rmu\jMJNw9MШ4#H٭M=( a% H&i9714T@^j7f"?u3~JRSJz*e !f ۣjU*^"N3PLHV}D4Y@}M_4T;L?A=7xh  !yJnqrcfRo`;r<) #9n`ղX-[-~8}UZL+}7Iv2E~JMo /NMsŌ8P7Ǻpw$1r{k> KJ/Hb֯!DZZnSV:*/xoYYL,-rڂ mFple+|$E%[ ]${'&["II`5Jb\v, 㷔cJ]|sm_u OuaMۊ)ʠ7Հ'ӂY,+u Fנ0@,;:cS4F k)𡥍HTa9ʒr\1Ϭ>$Ld|@7:eZ_%VoaXx^Y+L6HO6 Wc2HO%ӛ:Bxz#^󪀿>7ؼO6/O½CHM?/ʺbI4T{+Q1pNǸVo.2&wMظw`+O,81w,Wn3I P JǸGQbbÃÜ8R<.6=xQ/ dY:QL{93%:LDnS%~W7ī?y/;VfhA/VY 'mbp9Dd3r&>È=Rd`͞KLPwA,(@9? :TFٯNp'3eV9G=:Fp9y6_sg{YMȭl,m{zz8r#sw#F˼%55IdXv˄hMz WO-Rk3E:BNפ(ȤVPiu*dC1LIJhWiD(6'1ʳ+$~'+cFaf[~)m?ZG ~}L]q2+t%'&_~ },[zr̊g{?nHVPa*b Fpo65yH> sjꡦ<,o"A KblRwG0:XwRլ!'6_>E㛶qVi& n'yXרK(4>f=kfgA1+YHL:Ol kS42f;raEB o5_f@nA&ͮ|(l'J\Zx Vܵ{͈3-L%e&~v'ܐ.r4F<.UΦr5+ŵQ#{S刚[GmwDx a; DM-ce^=U*sg1#t3شmeƨً)wJB[Z/p oV31qr3*jT,SF%.@/klZ KS3PMYQ[5NR3yl*:BqNdϳ?Xக<⧆Nl?Z0s.3I@.hyэD !n&2n fUMfaӜ%9Kg?w> R]q=wRE}9ÙK%lVc~z#0쮃0u)=qi"rE2COԨR]ce$We*V_&gn=\2޻>h_Z.1 h7NSb?'J'یu|&%cŚpU0/E7XBEIlECk>hi;yeW(1 ž5/|:mzV;d-8 QyuBcyM%1C:ΜҶZsVZ/0? 8ql=돳Il'#ʋѤSx Yܟ X):d-1<7Аd`zwQsby6K-3x(nn?`s:?N$x; v>g*44+EBcȤHʔGXQ@KKfK*-aV@R~y16*OP%֯.IB|ڿ_/laBo _B٤#MD6Ț3 OZ1e{ȋU4é)?"qr R7x'ThU|v5,qMƲY6/bPKQ_zə]x-4>t$ +e,P-$䛴wӌcZiRJx NB9usk 6ҧt<]||TЈKf@]-1ج=ޱĢ%HT@1/BM7|sS݀3`UvܪpۉE =\b6&:m}j3JM v߹@o 7g⎍4EjeUMۜ v?Q* Uc|kgBKs8Tv Q`raģv Y3\Lqi vk]f?D&#A8D~r)6磃BUhyCX^иD7BNp#y٦y63P`%$ ev(4UTH#jΝT*r5Hd䠗-cK/I@PZʟ=g\.)@Ts 1iQY.iMSW8=-Z_ɤvBslf7'NPVC~hŸqk磇`Áy`Vf ĭQyfҐ˻Jo .X1wE(>͒E0Ф am#,&mIv_~CZG.,,‚ʪ $ ]}׈D8Vm/^HY_f5hE,緱e Y+`;x_8G %f_o*Q6$Dy_ڹ|`V.-*x T(sIy?`O]kq* icIε!h a>#_%h}HҺm@Z0?P|~/W覍H;n~Nsɚ-bs`r {զ7^Kl7b2ˠ'+vL󣞇3ߌec Hz?:G`<ŊlVq8t|kĦh*f6I ch8BUcꎲç8q\KrYIS[ɱ&OQ˧(9MФY)5lVV47n@߶ UWӅ9۞m+g`W {nQ/R\_U ٧!;؉*RJ K8R<>?ϡk*f%I'Vl>LzIK6a E%tUp/;E,㿽.ql9VGq^c+Q͑/ ))42QxuZ?J>t[=7{4I]=&D]*H4i'y),B-$4<}`[%6t'~Lu%]" 9I &6 1zHݣF);pMLdtEEfVeĝk]nm|e">Sz M:W{KSkD3Db}8,ܟ<:|~>-7=0 d2I"?V]T'}Vc%. h Va]‘"<N^fjnF&x[ ^{?2+-9,y? 0Q1b\`FD}pusęq}H 8qFwNSs)MF'Ė"m|5>z~Ö +dg{.Dy#kj>ʮ~T/'@ "S*s p?v?Q:uhbO'u_9*oE.B"NL?NreJZ=blg0ŷR*Һ1XI`F86іT@IET1%e&se_|?/w~kԅB1CU|[z* S]<)^u'`lg~fJo-WyW]Fxv*8QԄ1ЇFf4;F`zyg#qcqu\SXlu̪uLSLFߠ gSp%E)K~ 50 POZU93!Z.GWKUbQ>clش fԋ*sGy7 RLaT l~m7(t m8%V;X1kku c=eL mMyBv,i)@ԩt oYh-$0/ #HF:uU; qoݻǨp㹞TQeV]HdX33L"*}V/)іت[\7vVQ-89.;Yfuqx XDt+mcD*8rL21ârON!u7j΀R˴~R&8cZFtm>xRI(0Ỳ+M~ҍܪp2ANAWX[Ś[(|P¢+F:lͳ1͙L3JjrРgC,XM,R 餀‖_ڻ `@?2*7تlEcQ@ʍs;`Ry+Uj48D! r$Q0b/&A"?>4n"QDT>5(;;|\lA _rFgLf_^+:3{Qf)ENoD+pfUXZ5{UGR1vae+@_n*aeujZ`V`{B=e):-Xia/x wmxn04SEwIӲ%SǓO?լligZuFXPVXJq(DRs5B?bOkBi͙iQ1?c9:jYӾen1+Xq *Z\!ߘr):S)g\FStA 'D QDp"@1Bb,q*4FurGD(Oc%FRn*9G(mQZ e3zgX0,͔uqms sK5>և'ISJz^{lzfpdc ƈ׍dV PbƙO@ nL}fgy3^'CЀID͙ ?&6[G 3j@C6}ne^'SkIb(4ЕzbVfoVxcB>k(OMx_mgA~&4ZJ/kͦm2ɪG[tm[Gh@$Lk$1C5|r-gwH&?:%Bc4>y+x6@sSאtD e:=O`19#t+Nl'Q]n % +eusxv6N2~Fύ5K9_r8 a*_Zcp6+kX_0zfΞi(1—ݘ:&*&!r3ب8R2!We'K=h2ҢZxFCB*x8re1c2N,_"}{QL"3w8nCbu ,h-kY,X%eHeܲ(}QjJb%#1+2ɤ(e4fRm+vJ8AJqb9t2;t3:I!@Vd|~]pbkM^F~y%c)9)_t#]CFL ljCY~bQK he̽cM>Iz:hĄ\G1>OV'TUq#By7I#ꧦÂ_!.;'Vv,S" \VerW._@ڐ+w°tR "d:!]O?ֵ²pf>B8c&XXFm&pw19R0bc[t;\5Rtc.uO'OVamjҎò,s(cAWO†NRr;T=w?<I9JS-`Qy8bBezu:3G/t3RP^"m+0!,qH8 i1ncEȄph7b!||PM,7Vi6/xw;B-IgS5]]7Ok>n>W>gYЁ$*pG.ʃ1ompv2\`3#`\~r,^ѿncȏLR{Ĉ^*_3h>R3ouϹC,ߠ 枂^3m:jr.OiU^vM(r|"8]6@l ջ=H.Q&F>x9%V\EoZ6R/ӪtOXfFQIp1O8%[!w\"aTO VLy<=3ESsZZjimN֖qMVvr{#Gv`[b\6+hgt&Zorż~3R` imckDU׺\\7T+>}{Hu KQQs3S z(A-<*? ,{E)Yd0FHS(MJc‰)fz\FX-sZKr$#ydUgs<,5X-5WO>2q]båYei+펈n`B'n]yS$۳dZ` U*#SȐ ; (-܃9Vtr`V%WY-;{&ZdӊSu׏1 2⩆"+ױ7\ħ~qړ!r2o?BVZG҂l3%?^}Oɟ8AqN9d9tz#.،UM 5K>AIbB #@/FyEBJU?%#;@D*YRwLa՜\p\l շeOO h &^*ǸkI'&[6Yʈp@m"GWPjmcbbN\VKq9=J2YkdeRL9;(@gZmάŰ ܙ6tbOj6yn_sJ<5) iIڰ m|#ewo% {'= O=Ć+D *S"VׄQ%4^بиKL+N6mEX,+ǝ#M#8g͓%&!qr' /~`Hp˰L&s}\z$(%d&q4>tSMs4ur!ucP !V$qx /p0[V SezWvqHEFPS>lk'GvF)y3 'Ӫ.{A+=XJ%-ik231Z˴v|jf$=D5'-* Ync^ԡ&EXp~壯Eʽs\߸cGNq6<%opxϩ /YUy#N$y;ObҳEsƧ{&<̷a6՚1tTfVcO̶ 0泘pAWJL* (o Ir\4ef^MH4T E]/8E7}AFⱖ᭦%Vsuމ)>t P%(n.v q3bqi}7y?vg=гJ/cAF8:dF#tP1})9VYCR73SZ@QW~ô".ޞb3p>w$~ҟkH1*ű1*l+ 2EԜt pOf};ŜUUeiYmcI0)d!+ooFw98,}V>%ztv7g u*rJcf[ETS_5k'(Cg巶/c_N2ȯO"Rк3@;9]݇`Hn;=cr g>9.Ž-2U2bc^p̵ l45c)oG,jaiO 3̷J'6Z5BjEay7N76J0+SxO_WD9H@Nhcq/kREZ&PʴsIy#E5y_W{LW,ot!ig܄rJ6 s^I;k jJN?Cl&P>ū;`aD}~Nm>-?[zXGCrٶXL(χ?}[< `M8-X9i6iu!Ce,0~)|&D^Ȁ;:F1~0C=҅I9D>~=ړ`_zcE~SF&p/5Ռ'&:zmI&%gXBqF|D`#5?gʟ M-#J< '?@J$pQ!&(R~U2[0O?6ܕ, KK`ET'eO$)z.YEwp= 4KP[UE ×àWKKG{4<u>/V;D*&!F0BٻwpUe nYq>5gca4WT7sH> HxV3{V7WXMшpشby@UnfkO픂Z@]lYؐbSĵ[3E.xƨѬ»Qv Cf3e k~@϶}a'"AX;*>] ꃯ)tP\49ãJQQws77x6W\W[4\ E׭iƬbS<^bA:NbZPٲ"nR%wkMLsè" [㜈 ӾtF -ulՔi$sZlQ첒`O )_|7Km1$EoLKXiM"~#žI:l;mF-VϠ54)vt59)dֹt?9(NȢ(S<%v0_SSSIl7 >ihC]_s]@[0 #B2!#$Ü< BmF6 WIGYGQ#^keޓG d D5]3U~3^Q .Żd/ ݼ|80A}bDEoSz_36PoR+ $8'L};eZl޲r,+ڈR,ӏ2L*Vx][}ګ`or2]$cƝ %[|ȸT=>]T7zp#)~5 c V{YU~X"\'Oԍ%i,ql% ߋD~X6&2bȽKl2|y/cev{{8['SMv%Cl)lay lЬlo&bJrbRƶcYLfU0{F?p{u:+ "mSeUk1)-&h{ <aGH=[TɩL餑$)y$+ۧ&X'E()Ḃ~sbQs:oU]n: /.= 2!0SgCRKoSd3U˰:("TЀEh];Xp*ƨ|3'9ڼ\9WJc!9]IR=P jf=4d|krDUžIQnY*3HVcq(_{(WXgQgV-hŰeV Aփ6ˀَF^D͙ks0 (J*=)X57UÎzX 0A@?3AV?Xk )J^OȊ{v'&е¾~[wn3t'Z>`aPȓ^kȯ%q$gEGS04On7~c|1@5gʠq{v^J GL 6qSj>~/nǺpՂ%Y(iSqLg)6^VFjkt*~<;"g@<R[4Ηiٴxe^0"M{l xPs\l]´Ny!GDeen^ݟp G]| =kuh܀viO2u (6f8ܜ'xwgrmb5ŶUÑ3.G&))?hF V?^?TK˿{i 4) F(֐3h\T,Z/%^r޻{F)LA.bFu;R}jeC&?6?C|By_D.5/DXdkͳ)CWlP0شׯT[ C~}MC'~Bu4ГU#?8*\L58qQ/>3+u@3 #zKVFTJcHN ku%Z.h]>t, ͳ8[.ZjKFREw{@|Xa\apAfo /Ft+WR ~M;+5G<+]OǎY54HѠPqٯ59O{1׍a4ȼ?DKi^gn5d@*3Ⱦ,d8ʪy&*6*[-( bSs$:m)Uش0[!289& {YL]}4}F~{F*\5(uST&X=)e/ҳtL3T6zRC'9rϾ"J{H5٨#ȯ, eMS!9+'L+*M!+ |2~)[Qnf4EFATE/ڪaCp*2~;"V(^c{Gʣ(K>Ʀ(pui;D }PE@ȭT;܅z=A6R`G=w )W@y\8Lde&\vZ pD=)V G۝!P˧8CMy![\d~ru;.6[lܽXnQ.Eq3( G F Ů 7t"79}9?|zSDzፋ!fd-a7$) "CK ~[#XiITfA[XM%Lw`)i(؀࿬ z,_ 6Hxa>?5.,pntTbXuDUjrt[{iQ?EBY%hx"٧<2RRF&asE}gKhİRtj{|;(k9 T,]z4IVˌfakKԣ*6@ :T4>qh&Op!)EfBs8Ͻ s=9T33<]R6rPD__O5DEEײYOfdgdjaBn^P'V̩_`l8mk;5Eq~b}=y{ΊORWc7>;y6|5ɳ찦"jW1?yNo9f Q|<_-{%E 9}4M3M|Xđ";l} Ps[E5[ǍU#R}+/\ cϾr}d˻v{sL'"7MICxO"B{ ʺYsgSTB:4\]n,T5G=u6Cˡ_GBLז/#W(z;^3>s"6Oǔ;p &Bޘ (}Mg *tۯ!Qdz(3tG }DT)'~owܶ}U/.8Pȧ6gr>UȢ0?s0+a )b0Ċclbgm٦x,?dOK) VbiQc$Ϭ4J4k7)z_qaOJ;$)i_?p4E/659>2_`JxKcdg+ ! 2pv NBgh` %38cyny(^!" 0CT p¿LKDŭ~Dzͪi $*m0ix͟WO57 llXq{5gvdrNCMc'xzL < Xn b+n}ǹ4d.5L879,"rʑ(M\(LOS|1Ϊൗqs 9>@*2scM{>һ-?H\讲0 {vBIHUNwp >Ft=vM|>F?<kTsҶcI,NrG+{bM:^6~Dp)1<Ά)ҹhfm8GV wMn!cdHJ6 B@ GLc 7*t7i[FF {h* ֎evas3,[A*kqyu:ƺѵEi ՁgnyezљV@K.Iⳑ$*Ktz3z}H{3WF*T?R&sCȯ"q&I=vS12ɹcRıpǓS\mFZi hY 9i.{Y/=a5Yq>๬.r gWp H!g}gBVY^u<#r >ҡ*/>OǣDƒr4µ6]:঎h@弸 pVSzV>F>HSx >Ef nQ )v' !!pȁZk[A)$a=tzq`֓һO A2Mz~FқšFR]vo}UV=O[RtGx)9=偗zb|<|AW|`%Iaa]mxow΁Bqk@w6v%I`1㑫3>@CF)X$XSp%&Ng` G=$Z=k`:NPgs.w>?a)WR Icf?W;}H>Hl(Bs&u,*~6qJԎHl:־{(% : >Le G /mƞ+L,22VngPjlceizS?6:]MU ]No1"!3:-)B-1 z5D lbU|7Mn9ǂp02)L1m6O;x/ y8u餆h2'v ?hޕIDATO!{ `dYCe,'Sv>h/&֦%GQKBlao- $n}!lV}=]{FU.]ReT Rv>zOL1TMBUSwx_.6,@?m0c"q*"g;|p{ʛx6z YȨ`vfecp]6x)ʹgMGH4v ('0a=E0^fp4`p$ .$ u՛AHd HH--W^[@[oAk)ԍݭsH> T [cI EM9oe]ݳV_| A= e[.j\ǵ`xi<M r#H;XIu}p˟C}UQ8}ˮd/XaAƁ+s籈V7-:nV0Vǥ-5MCK.>}`!|&j</AAQ[mPe8MGIJ%G Sa%nhf̀5)ORMF3ec?~BՁ?MSCE-K͜g]*-\yU~S}ZiWe4< :6tpRl?q,|U9r]ggZ|f#]×g<5r*>q!K0+-bG;! ȯ@xB@fn+s޲ݢb&cR9kMX Q;0b{|p"CMĬ$I`* u3*#T|oݭ喪vG}^ *$ሏr|Cy"%tz⒴Cp:Dc4"ZhI2b/,caeq"3,vA$ո &B9G6=ȜՃowAf=zd,'b/6 F7#ln%f{^n }FGK6~9Y'ηULärL\UQD{>#'UjOɩ^F`&Efk]Ys:ݺ7o+ Oj=n衬kȻm`oU]Uì|V;>);YtN~o7.ZkMBϝ_#z{}cUPM*<}4wp[[6d6={aeě,|vRvB)KO殳1T?V -Vw ؐJv jL7F#Y3ȥU]4W;{]Z ol0AWU[7Q%|uoP8w'puk F䪟~%53Js'P, ? lnKXIq$YU dKo˗7|!ea;?2pq@M؃u9{}Y\ϐ0[B.d먷LQbƨ(3߬QZD鶵+/z\]]=&C5f`_~'Eu+Ί~ZlI\lURdYj!"ג<]*9vz57_I [!̖jr C>y_;Fv翝l?!܄߹br;?r*݂7Ł-Jx|.ZGc5*^*E6jƣDێ$o,WvohIr'O03fì!{#* $YJ\xIJh*&sBÈ5*'(u ]*R~LMjф5eqcD'drŇkoʐo8YwEDo\\~o8I ~ܽUB{C0/Yy?|i|Bz=K({1dTuFt)>'5)0S f]359CŲ2K_ Aۦ{ζ{P}Xo|YP*feF#)oHފHvIn&!vOrJ-;yKQ|ՎfQg#^4,lsLW.W $o؉n7hU?+3x7)±1$duq F2r5YlpS%8qNˡ"x6_R76N7qXrL)nmfqZ9k|9ԋڈl,'(%PJBN'=>cfjXQ-ͦ_C~ϡJӋ /xW=_}k;> '=-_t%OBoК8JYo>'|MxJ|SV/1| Mg7N`P_֫C(ʋFD!GxfgqN)%PܻD8dYbu1StGm9-="rpD1o>}OpUǽCGǎQCn9l="&alj]ITcg}#fcI,gHn♋j{"wH s;4'"YxD'ͱPM4GځpSTmwqF.˽Xhcx9Ql7)`B/^_$݀uy$!6JGDisIކTd|>I=ΐUðÖMϚGv #{m2SxeI$,% l4jZ8lKM¬Q6'Zp(k޿J=咮,ѤxQ)Oz-Bm` Co|Qβ&s&Uo}s Hۙȧ3Ty$E9:&ޗ;ՃE5 &Ϟ,s-OW>ox~6CSF+TVo 1ՋGr$m(S<.'l/pw~<]rʓ GlZ O "?"38y__#{l7=gH$mIg#RS84A\iRD'xyPRniɆǺZ gZdyz1F|泤t|trrRqzW ԗaLp최!Ig6|[+X} lm3(5_\:-ܯ7u)) ʐAO#OBnAmq|H2O:Isʡ#X^bpj+XI}Ny):sOm~w"c=ZյTmy$gU U/Zϖ0|P0"vc^$gZ \^tWh'(@S˕Y1@/J: OYRO yzwɤ#3?MoL,TE97Dp05C1ڹ{S 5Y{G8ˉbjtg;kouX Qz=:׾v+, :{o0n[a Htpg-t"9MExIM.^zΖU!%bv9ics}b6zJտ'~#lWm9deM8_!t\V|NԻf?A96]2}-zZ@)oxDD2 rG=o# mܽ(~NAow9@%et._d5;G5f?N=,hm]@xxx|UQC!5 ^3E-qz٧ZUI=Q(w(JhA ^R`7B7BMmPߒE9̛8G-T7 F8*ל7+U+4і>\|ˋ eV/O!E.-ݐti4<4-<^S3VBϧ.L Պ\pz|L+NpOrf4!b*U =@GrJp=( oBfBوęX YF jqL==D][}h؇9VAx#ۯ>\F"^u׌\Z76TyD#Oc4˜G&z8o\l ް.v P-M7T'(4Fi$/a {if`&86uX۪Atxjh*xbne3ܱqf>Ҷ&aUk[]F8+%-| rІuvY;ZsΤʬ#n;fc4߫DϘ('zUJ2jۗU,$fՐ| xY f4=/'7]0 d3GZ(./SZ>b |vfb"na<fulAfw/,05lN,ZXkQńS;-+_D,+D!{ƉKE޾ )MJ`Yȿh42|4]sr24<xeuĺ/u7*ϙI]NlP'DZ-8FUwDW-*3UEPM\ då~Wx߳߿GrY۲ouf%i×o%Iuᬙvb|6δa:NNLE%^,p(`hg@~ 5CK ]y$C2Y0ABaƻbc\5t$9 i'?օ"&fb2LͤzXl?ȀD< J{iUeVxwK0A7z|C ߃agPZ{~.nb^s9Ns3F0hD&RC؇u-}ׅ 33Jj'dž ^^2MA2FD/tY}('S61e;›HU,7VR~i&6ȏc^,>v^Bc48^!䳂ۮ;֊Fl/GDt}ܡGeL(hPծ'w1 "6 XG+Dڗ?m" YG!S}H2?WGz)S\+fvW3j1~Ƣad'%Lem2dEett6qΤq!x*AX_1"up3?gL5BNs/aNX[eϗf ؀K. 㓆mf.rff7IZ ۨ¹P=tt,Ndա1SS HK"|o#e{O7<6?v;)'v>{ 쭚UxY4p\Nq"uP 7g1Cj-NxsZ.gc# N=P!"U67K{bIL1-Gorٽ 8TW]Aal3GfǙrՠKDcQa{AM7t ^rȣ k2̯~Hv׉Q˝ lܕ10%Ֆ/%DvCQ}]-KEЖQ}/UVꑨ@;8;)]xهVc^8Sӏʠ ˻K3(W5Vo{fe񭣻Vë b‘(vYKaT+GSEnT*GeZW\[KeH>_ѥġJ>K]SH, Y+xՊ5ElBveRvz 'C.-vKؔ+:ClC/R+ |45'p#+[Gu]g힊 C.pYܖLSh( hmŶO:אg.DЯI28ɍUoh+#|E 2z HC<+?crzj9Q>1И&r(asQӮ^n:)% 91/9Q66΃QFD4sߧ|W|$wຟagGH> rPsV>VD!ITe/Et^g^=?4\v ?kIj2ԁVtkdN:yh\%fdž`0w>`P'E)ب+C,KuK[_OBnF]+/<@Geg ]h2 U}L*0SP7|.)ye/ t\!FC7Sfx\bbkͰV4!̟+a:I[JSX߬2l>哢dPT vcw"/P|Hx,tQ65uvRvR?72 ?ti#͌<[V( sɿ}~!pi;ʙ%c`X9|%>Κ)BVQmJW_RQ#DwBoz'KLO,0H1gC)wb1gڕ۩{gC4XbǸ~|Ҏ 7 =ҖB{VZd`$ϘsgBopl܄Z080$=nOA-h g)5ǝ4q(t@mWL`@#cMHeޢՋ eTѰUBU q*t8Č^hWSr3"aQ> ^JQ\mNOszOmD-C1?:aB-M?q5 MwMyz$#RDyX#LtOl@}ᩬ/Z85E v3e߬92vo'&4{,\@')={ef'bfGk"W,03=]5bDrw2Nwh=kpkP곞FLaR9L(@= Bak2$g/&jQErDȯa=Yለvr]>f&+[ZӺM@z߈Q VuӜwsZ6-NȬzXP/k_+ArLXY |D!*8?@ݗ wH(c"j¯YݼxGnۦe')fʑ:y>Kۯy S]('􌋲](+*3S RFoي헺|t.RhxaG.k\M ST-n$F8`zafe *CBԷœrn ee4޼L#ou/A5ڊ,Hjm2j51pRq(Ttс緣/; :xI vl\?YQ26^Fכ(* ]yj~8'sY6#LWR*Rb"/}>ukuvN4ӌo] @<2*|CpVYHT~%PP5'DQv7gPWFne3JYQSDS#Vt#ٹպrXV@;-! %9B=c 7| s-v7)>nTZ mljZU +%ǐ/EU.{# lo-ܲE1G$*C3ѾlU, \?Ez~9-_c߇wQŃ_` y|LBH<˼WD7Q)OjdΒq$":th2YŊ/P` 'Sj+jI:V` /nׯxt^2% 0jz=_9B";+IǕ(kM.E31=ΘH2o9ޜ˞kVvDپ?nq4E 5R;"o|c?h {YO ;2b^KYK=wsJKgDrFVZ'P<=Mif,u]Y&'{)8>fA#U@" Y~5oQ]lq=u0]^\>Ec-B?iq:"ކND3czPW 8CsU%,% ڋ +98Q?*g2D<~ۍ#맷L:C՚_Yfl~6l>$k*:mkwMRH[& ^0Dt곚3]0x 1ɛb;(]ILW|ø<T\sF{_3,muǧMŢ\޺("^8ntLė2~m$|ctȶްf/gnwVjݩ57kL˸D,LG2RhFozleGdSxq9.Wߘ窛KI3aiݪowpEqUxKio)_THɣ"oUÛ0LMIY (qF!4?Ierko6p1{ifk_ehު x)=WrwySCQu^2&kϊ~-2亡M5 G`ΠĤܴ'o.3-rl{qhAcǔ){f,|k\~Y+ν8Fb^Je ][IjhgՉ;IN ]aWs02_-d??τSHY3. M$VaW$Vou"R!Bg(%WU|0a5*=11 6R4LuC 1'ʚ34>"9S"Xs4z Tz*C,/rH|38!\4h=)3*FXҮ)ԜrQlQA-JDQ)k}G0Nk̓"8|5-m؃CFU|vMB@5T_urr3onBZ&1#i_>9.;޾+(   #42Ume1waɦi̒"&ܫ>k UTkoB͢y v0Bߌ~,{7EQutd^4'n0e lsv#O8ۺ Eb_>Kƙ~PC]z6ʹ[[A ]+2硫-4C餶0;Y8[bú,BN:$ Ά^-8#lS~_&N([b`^=nC*ZչVԥ,vv&-$5.zŢT" mG//pʼDHR¥Լ(VJ4! ˣ9Ty2V6Cȧ2ˑ~#r]MdZg*«Yɣ*̼U6D0e"/1?FtHSыr|]`*ZL|t}2ڡ܈5aCqmu9Xp&' 02_v+ya-V*aN+D!ۆ76N 2̔ٜ)ŒBb&#tbH29z2G. xH]{eH]N.Now#P@e\XN\ҎK[+m$kɡyXTö"#3z2,4=O99E~JƨҵDͧ0 RZ[|lgllag/&ev%^rHy-8KM{&L*+&EV-`EP?<ǽo K]r;_B?/+,~scR9Fe[ sCW( MoC2-r_ȷޓmχd@-Β&sH>\`mof5X5,4bf|:w OϞH{NP@v"Cs{,4d٬[<&5@:LqAv+uzEeV,27w:kc6YN?j2vN?܂dI$ǂZÜ;mka :)̊72^ ayh80rVrpzh6!XǶ䵅g0ڋn ݊dXUr]!צvy(GwJ|y f M ,y `Z O 3FofEVe?nH/Ch#7~_JGc7b=sr=G3>UÇt8MpLv5&{QfΛ-OZ6KsZgRN<hdZ=W4i|uZ'T~qzxQEUd\7n9pF׫xe`5<|#'la尒z&Ji^^+>- st/ΠMir^"o $LLPU[WR5JFZ5,M>C!kd6ew[ }SR[9wTZ7+ rw5,fi+BTU[D %7lqDfPT%SX=*{mY.&xgLwMⳞ·nexU 1=8nu:b&8l۪h%^@ҹi}=}$oiQC*f $Vݳ<*g~!f(Xފ+vdԝ--Jp6/B9f5#ѝ)E*"zSmw$yџ z=ڶjE,}g(n o{ !/U^~ÁOlG=$McZ؇̋zbU íRϺlAArjMƃzXlZp>g^v DO t\A^Y49Ə^'I,yb[NX ,$h g;WLO\JlWղ)=>{MsPdn]tu'(Xk,1s0{#K!IT_GHc<'UY)vWm4ȏXeU1F&c|O=ScLC,aZcS X)৛d^VDJZʱ, U9[VwDDdx5UQG\^Z8K#P-< ?%#*>S<{ps̷:%I<|һmԌo Z3**rJ\t(wxB _ӷٶ z8nos'Srkdf3f?KxL3OBUbY?'&>n^mvm)I'.%9&omDǦ .vy;\^"]QMгHgt^ O;G//1Q5ye 2Ρ~1L9\d~P^jaǃzHt$WRN@(8+Wǹ z!9nws͢g;)<%"Z5n3kE>yށAMhjBV臄MQ8"IM2C_B4]@T޳=c_"l0+ YQX]2/ 94 =f0hR 3Cu+<u|0m"4+޷&Q̞ѻ zB#3r-L'ɝ,zɞ]$dz7t[p a7g $Q:\ y =~AiT P'"K/@C\6S7yIQESX3]u_)gyb;ՋVK9;9gϢPz1Sfըx EGM<=D_f†i9L<6N4##j!^rOR`})%Êan'6)U;ַ:{l򯸄*BE^MFYu3@5x3~ j^s'=CSzA Y8vL;XKc@"[+6߃(~ 8bbY|A?gPR+]FE7Kf%s<]H6S6<+GأR7n֝hu7oyinW^y( ƪ&i* k_-e+SS\sА v-{1 t9kΈkG+ŵ3tBH7w6Ȝ1.DnD9PH *8^L)ᣵCmܳ"z\R4D:EcB J^Zţ"YSCcayf䲡NbK6ضr{א79Oߟ/:zE˃vSQÿ>F^N+fL>7/y'^dWµi]X9{n挛P O8JCc=%<!̋ pvSyi)W EMj!,*Qz ϥ/YUq6VCm]@0(β8Tuy[,b*[ 6?&">b1/.$q/7lrBȆN-2`fK|MLʅ>-*u_bU} A4\4" `UmPQ 4{Dū۸'j5 <=wKGC֭ڭ%hc)Xd a_ܠ:N) ײ7{p4`Z-e3EPq\ޏnIZI$oY{/Uy1JϨ5[Ti |ͮCݷ,0$flQo8rXQܼ84+R&ˌI+3^{"36[{ FA)4d#97m퉮d-Zٳ#`vM~&cgVA$̚{fj?)hʮ`?Y#^ݭߕav՞!ݪH_*CwBZI{+mt6p(ofT=1dwբόZ,Er0srȸ6+9c:AI|6`~8yd,EVz3 o5";Ί{9_mωPLZY]qt>XTێQx֍QȽ2\d_f"0n,XF3QDnyP[َGǝ /:n"/&)3?b|$&P̾OlF(t:)D.IgCxt3O>Vc79ζ߶KbGӅ]ftSg>"K''%32 `7#͇zmG[ 2)d^sr+ ĭ2O.i@bmJH>DuJk8Gh֋0Hb0Qs'Ṋ8ҼoIk"HOW-k |Y#_+;Qt1 [b g5KWM=h| W1_E2%:NNQ5k4ͨ}!n/ |n+}`N3:cq ETDn(IWQD"N"YsFi@' X^,&,`2X3TzN3}z6bлLc5MqQ?AʸP/%Zş$Df=Ki⛜#3[ EcՓsZOsIu c,d#-nix pl.eN+%7oѠX4c /l;l~?gZ._f|OkUb|0p o91G̿Pv$ݽ [_; #M×M;N7cjwq/G 対jژ]*Q!PlF4Ϥ=I=m)$6V^^Lc4uY1Ȗb/nw~ Ap?seԀ#Ġ#( $v@1YhqLD3>q^BTm7)Jy;H󩘍&sh*?4A|' w&!!SQXѼa=}>UqPD|߯ՍApgWzݟRT^]nTfv8iJ0/tp:P3r0b~M]l{ӄ)lLl bf,.XFGS;}ߜ9Q~^)J&84c '%ܱqc s/>*Ph%v|C줩z.H+Rw] {m3o`LsF;B ,t\i]6!vTsY6 cVZ-oPJim gMeҿgHEϼh.$>>ϽQSxF> ĨRG *珋Qy;1tR9.]:2L m7>'MtӑIHNbb,#S6)w(j :KƘkRiarڞ(mF|߹Zd>cvaCKj9se&&z{/N&M7^bNϊli2^{<1/G*7R1eP@sy[8s/*fT~>.pJTL*70i1g+lQ y'*L.sjgqa%1QOig=1*KZ-R)F|]%Qb %)<~=j[&{| y C>?'Bp]ƈly#_C1V$>!)򷷔FޠV]f؎n[$ݨB6 [k > `G*6U>b*;0>񋮧lHU¬)gAV5*GE!ՋY-ųoqFV y"}ox+_BSyQxCvlz[˔wO( &u SyPI gC4 ͚ kc"\heYc<ꦼfUVٙѠdž:YK^#c>7/D|"饀k=~ӼҟG39/3Y6VPE`%ui'.qVavڝǠmЇם-aD;sMyΨ#hZJ&X]m: ד[eh_E#̕sHʻ3oc֣۟5QBYTR6Fʭ>nY"4O)泜?Y[?rw4|Uzw9YKF9Irw>`(Tj2jM0{&OC{^EOߍb&zd3$;6cXl'mpFEZm׼ʐ߅"V_Angý™Nܦ|鈝Md,>zM9KfDM 2k Gk6.tOgȞSDX -YJ_;dl['NZ~^M`9,{2ʪ&ɹl<ռĥĪHf>bQIM,V;! 'EۿF_n}ʤ 9;`_TsGm0mȪfͤ8-eR_K,WvCn3Y x0qPǵ9ČRcu!-T)I!㲙"}Ρh m:>kZgs1>sj;d'DAu+Pk5FpA]lIJڭOлg|fܓ|8Q!G[o^^kct |S+sF>+rzQԚߞb'Ͷ%]]xBiv3ZbǓH^d̰T) 3@!w\J8#%Ր#{dD6CSH!#c=eB j[kד3}<^sV>< Cg0Iq\*d0Ѕ#Ǧ T T cns\7[T lJ('wZ6z8QcGSwCiDZ h,ZLL/RUFaw(ڦfXZk!V"9]A0d,Di =3Ld޲2g%Uݑ Pl2jFCtoQu(`Q1C?C*j?+Li6eOq~k  zA,R4drʳѯ-9+(e\mڌiQw)qULj獗=ɥeI?9@r6Vv<ƛނǟͬۄGE!cӕN$1Սb-c)q**dڼ">0ms[GjeݨVX&r\=ov6eB峤9qޛIi<.-]Ÿ농b{qM~.ҵځE{I{Nǵ*\eȟ`"~Xl;l>vƞP2 k 貇g_r41rNj`7-Ay*c{TdL?fU9g'3]㱔YV9 $^lI6\{ bzzpeaS d2WmbBp8 Ud{)_;pl-bfIE|,\>KQn0Gy@5z'qVExk3EP; VB&k|Ko ~ )M=UJCỆ,t6rJΦ. :Ǒ}pݔnj9~VwNfulbdSZ!I?-~n}aί!vJlˊ;vwFFr?jϨ݉ˡ}2$hqbIYl> FDyzгL,/bVeÒIf5\6S``0& ٪zԇo VigD߅+Px7SQw%%-<=7^mۆ}`XveA{n>v˨ Ų̶#xkwacFZuZH[J{ype7K1 b2gٻ |}ӂUǙ!PŮYe ܳl s#Oq:*uZM,94FfpR CqB6\~LzDD7,9/TuB9=jJ\`' ͚pԼ75On^(U7;=OPWŠƓk_c{>sz4gw=?>Sj3Ysusw[)t*䏊Y2LBJ145tšJBCH&T0sHU$y=f|Po-"M=4ǚbu>[W5*ӠWJ;27{尘سT"ZOrܧׄ5?j z/@xnXLg%Xi;$i@K%@f~-c]7#]Z=k hYF<2-x5{G^ů"I-4c3m)g~<>I!0߳E ~_uj3E'b$glѡ_?fЫ|RB8z_^yzu+ur >I.{f2^{4KPa6,2dh[OECS?lEapT"-}^#+T ¥ _%J69h_t֓.3ouQI-ة6 HBjVY8R |o6yjJ& 9Rlf'KM^LQ4aA*a(#(=&r{1Mo3T1ft ȟUЃfK8f\cG,f䯝S ̉0 [&nA~ŒM܈5_ki!6WӮE?Z,HW԰;'uh2wK R5 ۦOٮZ&Zd| N,6-ds4OEݢtbj̵Ep=_zҰTW5!HD*3 EXW-v?>{%^r.|lMidmE( tgmq6yb]x`46d5SJc_^6fYccyzا*Fh;7LrS#5 Ա!j5= sNG^>NCg)a_彰uf`4U1+"$my$yk s>uOG涟*bYQOO8,L#aІQ"v~PD~BEqZHUnmK7@yV~b G9RXO@CV&+)B{gog]>NY1S靅MSv+fJ-M3oWcdM6GQ?g ۋ/2KinϪMjƋn17.z(`")Mly[" <<3hlj8  C : t<%ցpM:e 5+hBR ie 42Xe 眽c}Z 4ɒ9{z}< *:7o.5#譊͢AAygN{>;>f +Y1۾]f=g;ٓyfJV#D7~ѕ7B=(u>:)YԔ:®) [P[G`l*q:e- CE$9hTTBkKSs= U;a9;)f}Іʁ}eoa?w󹮒3IQstl`WF%Ȯ5#lhDYLwM/<x^ *]*&|؁mT!c-R:ΧgEm#z LnhKTfYָ$j7yt[i(V 2F9μ,l$>~CPDO=9Ϻe/("ΨZET!uV%T9t bm; vmAMxM4ҥ0=ږ ՔXcͮyńGF17w2y鶘O(%\!YF霘sl ۨDfYoGC߾Ѱ_!J Ijvq\WqkOģȰG"HG}"n cX3RfJpt1:I܅.=(_úѢUѐT~*sQɤ7Gr6d1Z̳ϻg3gݙwp3[ mJoy;c>w zc&F;7gbw~TU6$\oz)ѽ]DA9l=<ф4Ӑ9qEW*ӡr!!,YF k*y)O*/|Q)JH/KdU!r{h,bti٬@ yX\J)~lUZ%X"z ]hSc_q2exlӢkJ.?.ufa~K`gqe_пY[ѝ "r^\&iZԂ~[Fm>U>:<%dFwRNo%۽KI*"?9##1, R"FB}x Kv/CZ%5,Su+P[yy6{@LGU (dWFPE|I:ϰF<?-EΆ2/hk|K ^Ry6o(B*֋#RY 80'ePjih TzF_-*?cпT-^)eJjؾ@! i"I¶?$:Z5p79jfLQs$)f 13͛v|/m#: q>3}}V3GS{v´  R8Q,)i u1&&ߎwc./#xfqbpMfe4 mQJ[<_~XsB&_R٨H p^1;5ŁK&j?1'!y.-RuEv2ipxNxu/d3R1zit2V1ϨAVB˰я]}"rtG^o1u0N]Ȭz8rf%đ{;2٥j՜2j3W{%_w9'BOQ'vQ?Bt:UŪL3T-oVs\!GUбs99ԉ<ЏiqTXQF!ypM/)̒f;6&m~x]/w;Ƴ y?j) 3ę/cfYg1GGBk+xE| o ?xҳ* n늹(ȐH#hkVufRL&:Pd3\z-F=7X< d^]L ؾJ ?)`Ҕ A3q=TeřC _šr[x *n-C+|+(c7 Q>NzN( =ߤrBFe&:^eN?@t+ՏG-;T=:n{yqA4V-+t9uR~:۪FVTP2:D|4]YI{#ނWdj",9鳞ry_KP_T/b>(<%m+?y$/XEpbΙxWTsuY2TfQ"EW&b 8xϜ|#eV9nFw͢R2| z`G 9Ac^\v]붕ʲSEF-][7ewY趽G6P}Ncc|\Ԁ{n:[Uj;Q}Wu[:G:Їx5\qW&p= f{ӟܚ׎_jʚgwQNnOU !VLr"6_<.X3˦2Tz p( cxG-Z]M+rhbΚ70UXBaxVGrI83 wFN ٮL4]mZ_C+/%vO]b #ԪQX7JR YV&/I%Rȓ~jtH~v_~z+=G\a"FUݙj@D '/V[p3%G)J_L9 5}}Wj?ѭWQU[%YĴ)[zժ-AAXGϥwі-$ tZj5X颤"Tҙ!ۊ^W1n‹6 d$c1]ä!`0Z3D~8daC ՌX!1{lv\ ~$:nZxlk厅{0=Ƙz` ;"u&L=hPUNr{y+h\ >N^\a!#c)G"L“7_%13(ASȳp*&jǎ00g2X1LՁڿ`._N׺᪡* Sq{{1;v(c;fX}X}~( Җ5)PKصL0WR.a!63c> x1\r)y):CyZcVv?.ŠSÏe{擄 }fn:̋u%8~1ٴۄ?#G!Ѫ% >l7{W[ .#ag{֒nUSPxa㴘GK*2fѢc}72`u6ON–̡(Pi.XY\7s̨f UݧjI-֙Z1/PXPa=TVV!q߻\;c7X nHOuz,W`_ZfYİk𜇯B/7-9E@/c ]>@;Xo v[bq0^f ?liwoX¨z-> 4SV]aX0Kc=>br+00sUQA Fw34X ~}6`yYY0v}Lo1''' cLg0:CS+9l)ϛl d=4 U9X՚>XvC/ n?_@wpyf k\,IDATuXb !gPI+ÇY ~y/U*DgGځ;wqpŞ;OhC/h2EH\Q 7n 8y d^ / 4… q3*c{ö9d[4}9\е*PSIԢK}/9pq LEt?oOHHI [8;;Ew0hc B:SnuMW5~'Bo;'ws̃/Ӫu},7[S/<C1I8XR 3h0`\M-4X7ʓ$B8RGLR e]S| .biyGشHH=q{u 9llpMΞp2 ifcڠC:skiP.fH4[7?QO6ڔ!Q1ۮc}V3j=+kqV1}mՒl* =`F\/-ofٍ7?}!wubQ5/O^ᡶ9 mוBj7qIfQ+Yc.M=v~j) {-OqU`N(qd.̂ou֒84kYIDSEҹ/oJP) $k 4pFB: \)K ="DItA@AIl ު k7bSY $8 %)]aw= RZuRNw75o"HVr9J`[H!'@Ab ChbOZeq{1հA-gQ8F 03c@1c)a7!>onj)T ӄr- &+oQ>rw]8XFn*`٭>ާIj C-c2 1`)wX盿~h3Vu8!n%pB0(b,]@`O5aH2&M`L7(>7*zSoxpե`{ϫ[~WăN>j-!8y }B8n& B^D/6NN/Kcҳd_'t$cH7cd`aH sj ;̐m_ӱ% 3ˁŷi(ߥ?1^b'0v6:1 NL~hA:Hr-0m؛ԣ^t!nÆOa|eۑW {gnLJD42>Q̉Zs3D |VxA{Fü,oB!u5F(d<1ƭm