pax_global_header00006660000000000000000000000064151143060240014506gustar00rootroot0000000000000052 comment=b16983926c9abb3b4228148a29cba98677a5d5aa nxtomomill-v2.0.1/000077500000000000000000000000001511430602400140765ustar00rootroot00000000000000nxtomomill-v2.0.1/.bandit000066400000000000000000000000251511430602400153350ustar00rootroot00000000000000[bandit] skips = B101nxtomomill-v2.0.1/.coveragerc000066400000000000000000000001251511430602400162150ustar00rootroot00000000000000[run] omit = *test_*.py, nxtomomill/converter/hdf5/acquisition/baseaquisition.py, nxtomomill-v2.0.1/.gitignore000066400000000000000000000012601511430602400160650ustar00rootroot00000000000000# Build files build dist ESRF_Orange3_Add_on.egg-info Orange/version.py doc/build *.so *.pyd *.pyc MANIFEST # Cython generated files __pycache__ # Editor files .idea .idea/* *~ .project .pydevproject .settings/* .DS_Store # Windows temp files Thumbs.db # PyLint, Coverage reports .pylint_cache/* htmlcov/* # Tmp files *tmp tmp* *.edf *.info *.cfg *.xml *.db *.tar.bz2 *.nx *.hdf5 *.h5 *.log dataset/ datasets/ *.egg-info* # pytest[-cov] artefacts .coverage pylint.txt # notebook checkpoints *.ipynb_checkpoints/ # notebook nbconvert *.nbconvert.ipynb # octave files octave-workspace # sphinx doc/_generated html #Local files sandbox # gitlabdataset test dataset *__archive__*nxtomomill-v2.0.1/.gitlab-ci.yml000066400000000000000000000122071511430602400165340ustar00rootroot00000000000000stages: - style - linting - security - dev_test - test - doc - deploy variables: http_proxy: http://proxy.esrf.fr:3128 https_proxy: http://proxy.esrf.fr:3128 no_proxy: .esrf.fr,localhost .build_template: tags: - linux before_script: - arch - python --version - python -m pip install pip setuptools wheel packaging --upgrade - rm -rf artifacts - mkdir artifacts # style black: stage: style extends: .build_template image: python:3.10-buster before_script: - pip install black script: - LC_ALL=C.UTF-8 black --check --safe . flake8: stage: style extends: .build_template image: python:3.10-buster before_script: - pip install flake8 script: - flake8 nxtomomill --ignore=E501,W503,E203,E731 # linting pylint: stage: linting extends: .build_template image: docker-registry.esrf.fr/dau/ewoks:python_3.12 before_script: - mkdir -p artifacts/linting/ - pip install pylint - pip install git+https://gitlab.esrf.fr/tomotools/nxtomo - pip install .[test] script: - pylint --errors-only --exit-zero --output-format=text nxtomomill | tee pylint.txt - N_ERRS=`wc -l < pylint.txt` - echo "Found $N_ERRS linting errors" # exit 1 must be on the "script" section. If in "after_script" for example the exit 1 will not make the test failed - if [ $N_ERRS != "0" ]; then exit 1; fi after_script: - mv pylint.txt artifacts/linting/ artifacts: paths: - artifacts/linting/ when: on_failure expire_in: 5h # security bandit: stage: security image: docker-registry.esrf.fr/dau/ewoks:python_3.12 before_script: - python -m pip install bandit - python -m pip list script: - bandit -r . # doc doc: stage: doc extends: .build_template image: docker-registry.esrf.fr/dau/ewoks:python_3.10_doc script: - python -m pip install tomoscan --pre - python -m pip install -e .[doc] - sphinx-build doc html - mv html artifacts/doc artifacts: paths: - artifacts/doc/ when: on_success expire_in: 2h only: - main # dev_tests .dev_test_template: extends: .build_template before_script: - python -m pip install pytest pytest-cov silx --pre - python -m pip install git+https://gitlab.esrf.fr/tomotools/nxtomo - python -m pip install git+https://gitlab.esrf.fr/tomotools/tomoscan - python -m pip install -e .[test] script: - python -m pytest $PYTEST_OPTIONS dev_test_python3_10: stage: dev_test extends: .dev_test_template variables: PYTEST_OPTIONS: " --cov-config=.coveragerc --cov-report term-missing --cov-report html:code_coverage_infos --cov=nxtomomill nxtomomill/" image: docker-registry.esrf.fr/dau/ewoks:python_3.10_glx coverage: '/(?i)total.*? (100(?:\.0+)?\%|[1-9]?\d(?:\.\d+)?\%)$/' artifacts: paths: - artifacts/code_coverage/ when: on_success expire_in: 6h dev_test_python3_11: stage: dev_test extends: .dev_test_template image: docker-registry.esrf.fr/dau/ewoks:python_3.11_glx variables: TOMOTOOLS_SWMR: "True" PYTEST_OPTIONS: "--cov-config=.coveragerc --cov=nxtomomill nxtomomill/ --capture=no --verbose" dev_test_python3_12: stage: dev_test extends: .dev_test_template image: docker-registry.esrf.fr/dau/ewoks:python_3.12_glx variables: TOMOTOOLS_SWMR: "True" PYTEST_OPTIONS: "--cov-config=.coveragerc --cov=nxtomomill nxtomomill/ --capture=no --verbose" # tests .test_template: extends: .build_template before_script: - python -m pip install pytest pytest-cov silx --pre - python -m pip install git+https://gitlab.esrf.fr/tomotools/nxtomo - python -m pip install git+https://gitlab.esrf.fr/tomotools/tomoscan.git - python -m pip install -e .[test] script: - python -m pytest $PYTEST_OPTIONS test_python3_8: stage: test extends: .test_template image: docker-registry.esrf.fr/dau/ewoks:python_3.12_glx variables: TOMOTOOLS_SWMR: "False" PYTEST_OPTIONS: "--cov-config=.coveragerc --cov=nxtomomill nxtomomill/" only: - main allow_failure: true test_python3_10: stage: test extends: .test_template variables: PYTEST_OPTIONS: " --cov-config=.coveragerc --cov-report term-missing --cov-report html:code_coverage_infos --cov=nxtomomill nxtomomill/" image: docker-registry.esrf.fr/dau/ewoks:python_3.10_glx coverage: '/(?i)total.*? (100(?:\.0+)?\%|[1-9]?\d(?:\.\d+)?\%)$/' artifacts: paths: - artifacts/code_coverage/ when: on_success expire_in: 6h only: - main allow_failure: true test_python3_11: stage: test extends: .test_template image: docker-registry.esrf.fr/dau/ewoks:python_3.11_glx variables: TOMOTOOLS_SWMR: "True" PYTEST_OPTIONS: "--cov-config=.coveragerc --cov=nxtomomill nxtomomill/ --capture=no --verbose" only: - main allow_failure: true test_python3_12: stage: test extends: test_python3_11 image: docker-registry.esrf.fr/dau/ewoks:python_3.12_glx only: - main allow_failure: true # deploy pages: stage: deploy image: python:3.13 script: - rm -rf public - mv artifacts/doc public after_script: - ls -Rl public artifacts: paths: - public expire_in: 1h only: - main nxtomomill-v2.0.1/.gitlab/000077500000000000000000000000001511430602400154165ustar00rootroot00000000000000nxtomomill-v2.0.1/.gitlab/issue_templates/000077500000000000000000000000001511430602400206245ustar00rootroot00000000000000nxtomomill-v2.0.1/.gitlab/issue_templates/bug_report.md000066400000000000000000000015471511430602400233250ustar00rootroot00000000000000Description ----------- (Summarize the bug encountered concisely) Steps to reproduce ------------------ (How one can reproduce the issue - this is very important) 1. ... 2. ... 3. ... What is the current bug behavior? --------------------------------- (What actually happens) What is the expected correct behavior? -------------------------------------- (What you should see instead) Relevant logs, error output, screenshots ... -------------------------------------------- (Paste any relevant logs - please use code blocks (```) to format console output, logs, and code as it's very hard to read otherwise.) Possible fixes -------------- (If you can, link to the line of code that might be responsible for the problem) What versions of software are you using? ---------------------------------------- Any other comments? ------------------- ... nxtomomill-v2.0.1/.gitlab/issue_templates/feature_request.md000066400000000000000000000003161511430602400243510ustar00rootroot00000000000000Description ----------- User Case(s) ------------ Test Case(s) ------------ Raw material (example script, data...) -------------------------------------- Any other comments? ------------------- ... nxtomomill-v2.0.1/.gitlab/merge_request_templates/000077500000000000000000000000001511430602400223435ustar00rootroot00000000000000nxtomomill-v2.0.1/.gitlab/merge_request_templates/default_merge_request.md000066400000000000000000000004171511430602400272420ustar00rootroot00000000000000Description / Goal ------------------ TODO list --------- - [ ] ... - [ ] doc - [ ] add test INFO (how to use...) -------------------- Any warning ? ------------- associated materials (screenshot, test scripts...) -------------------------------------------------- nxtomomill-v2.0.1/.pre-commit-config.yaml000066400000000000000000000001711511430602400203560ustar00rootroot00000000000000repos: - repo: https://github.com/psf/black rev: 18.6b4 hooks: - id: black language_version: python3 nxtomomill-v2.0.1/.readthedocs.yaml000066400000000000000000000011441511430602400173250ustar00rootroot00000000000000# Read the Docs configuration file # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details # Required version: 2 # Set the version of Python and other tools you might need build: os: ubuntu-24.04 tools: python: "3.12" # Build documentation in the docs/ directory with Sphinx sphinx: configuration: doc/conf.py # If using Sphinx, optionally build your docs in additional formats such as PDF # formats: # - pdf # Optionally declare the Python requirements required to build your docs python: install: - method: pip path: . extra_requirements: - doc nxtomomill-v2.0.1/CHANGELOG.md000066400000000000000000000253711511430602400157170ustar00rootroot00000000000000# Changelog All notable changes to this project will be documented in this file. The format follows the [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) conventions and this project adheres to [Semantic Versioning 2.0.0](https://semver.org/spec/v2.0.0.html). ## [2.0.1] - 2025-12-04 ### Fixed - **h52nx**: be more resilient to esrf mounting point ## [2.0.0] - 2025-11-21 ### Changed - Save NXtomo according to McStats (a flip between *x*, *y* and *z* axes is now supported). ### Added - **h52nx** - Handle `rotation_is_clockwise`. - Allow users to provide mechanical flips. - Try to deduce scan category/type from `technique` / `scan_category`. ### Fixed - **h52nx** – Improve robustness to invalid unit. ### Misc - The minimum Python version required is now **3.10**. ## [1.2.7] - 2025-09-22 ### Fixed - **h52nx** – Read `optic_pixel_size` first to determine detector/pixel size (PR 337). ## [1.2.6] - 2025-09-19 ### Fixed - **h52nx** – Fix execution from the configuration file (PR 334). ## [1.2.5] - 2025-07-29 ### Fixed - **h52nx** – Improve robustness if the camera is defined but no dataset is created (PR 328). ## [1.2.4] - 2025-07-29 ### Removed - **h52nx** – Remove `bam_single_file` option (PR 324). - Applications – Remove `configuration-level` option (PR 324). ### Changed - Move config files to **pydantic** (PR 324). ## [1.2.3] - 2025-07-21 ### Fixed - **h52nx** – Fix performance issues from `deduce_machine_current` (PR 320). ## [1.2.0] - 2025‑07‑18 ### Changed - Refactored acquisition handling: added `_AcquisitionConstructor` classes (PR 313). - `pcotomo` deprecated in favour of `multitomo`. - Renamed `machine_electric_current` → `machine_current` (PR 305, 285). - Switched from `pyunit` to **pint** for units handling (PR 310, 309, 306, 294). - **Documentation** - Use `program-output` (PR 299, 293). ### Added - **h52nx** - `--with-master-file` option (replaces the old `--no-master-file` flag). - Propagation distance, sample‑to‑source distance, and sample pixel size handling. - Bliss “back‑and‑forth” sequence handling. - `sequence_number` handling. ### Fixed - Silx deprecation warning removed (PR 315). ### Misc - Minimum supported Python version is now **3.9**. - Added **Bandit** to the CI pipeline. ## [1.1.0] - 2024‑12‑06 ### Added - **h52nx** - `--no-master-file` option (PR 272). - Cropping fix for machine‑current points (PR 177). - Motor geometry fix (PR 195). - Source‑to‑sample distance handling (PR 202). - Region‑of‑interest (ROI) support (PR 172). - **edf2nx** - Option to use real angle values (PR 181). ### Changed - **h52nx** - Improved machine‑current deduction (PR 179). ### Misc - Progress reporting moved to **tqdm** (PR 187). - Removed legacy `3D‑XRD` and `XRD‑CT` classes (PR 196). - Dropped `require_x_translation` and `require_z_translation` (PR 199). - Removed `scan_numbers` usage (PR 173). - Updated to `__future__` annotations (PR 166). - Sources moved to the `src` directory (PR 130). ## [1.0.11] - 2024‑10‑07 ### Changed - **h52nx** – Modified Bliss file scan ordering to use each scan’s `start_time` when present. ## [1.0.9] - 2024‑08‑27 ### Added - **h52nx** – Added handling for Z‑series version 3 (PR 258). ## [1.0.0] - 2024‑02‑23 ### Added - **app** - `nx-copy` – Copies an NXtomo and updates relative HDF5 VDS links (PR 188). ### Removed - Removed `is_xrdct_entry` (PR 209). ### Misc - Documentation rebuilt with **sphinx‑pydata‑theme**. - Upgraded to **silx 2.0**. ## [0.13.2] - 2023‑08‑03 ### Added - **converter → hdf5** - `bliss_original_files` option. ## [0.13.0] - 2023‑08‑01 ### Changed - **hdf5** - Uses `technique/image` Bliss metadata when available (PR 147, 168). - Improved multi‑tomo (`pcotomo`) robustness (PR 160, 159). - `estimated_cor_from_motor` now set for 360° scans (PR 161). - Better handling of cancelled scans (PR 158, 165). - Removed `real pixel size` and `magnification` fields. - **nexus** - Strengthened node‑name vs‑path handling robustness (PR 173). - **app** - `edf2nx-check` – Verify previous conversions and optionally delete EDF sources (PR 164). - `split-nxfile` – Split a file containing multiple NXtomo entries (PR 172). - `z-concatenate-scans` – Concatenate a Z‑series into a single NXtomo (PR 174, 175). - Deprecated `h5-quick-start` → `h5-config` and `edf-quick-start` → `edf-config` (PR 166). - **misc** - Deprecated `from_dx_to_nx` → `from_dx_config_to_nx` (PR 167). - Replaced `str.format` with f‑strings where possible (PR 154). - Added batch‑processing example to docs. ### Added - **edf2** - `output-checks` option to validate generated volumes (PR 155). - `delete-edf` option to delete source EDF files after conversion (PR 155). - `zstages2nxs` – Added `output_filename_template` option (PR 176). ### Removed - **converter** - Removed `x`/`y` real‑pixel‑size and magnification metadata. - **hdf5** - Removed unused plugins system (PR 177). ## [0.12.0] - 2023‑02‑23 ### Added - **app** - `zstages2nxs` command (PR 145). - **converter → hdf5** - Frame‑flip information (PR 109). ### Changed - **converter → edf2nx** - Better handling of current units (PR 150). ### misc - Dropped `numpy.distutils` for packaging (PR 151). ## [0.11.0] - 2022‑12‑15 ### Added - **converter → hdf5** - Support for `{detector_name}` placeholders in paths. - Flip handling. - Fixed inability to locate pixel position & energy for Z‑series. ## [0.10.9] - 2022‑10‑26 ### Added - **converter → hdf5converter** - Support for EBStomo `pcotomo` second version. ## [0.10.1] - 2022‑08‑31 ### Fixed - **converter** - Default distance, X/Y/Z translation units set to **millimeter** in `EDFConfig`. - Fixed field‑of‑view deduction in `edf2nx`. ## [0.10.0] - 2022‑08‑30 ### Added - **converter** - `hdf5converter` - Added magnification management (PR 110). - Added sub‑selection of NXtomo by rotation angle and pcotomo‑specific parameters (PR 100). - `edf2nx` - Added option to avoid data duplication (PR 114, 115, 118). - **nexus** - Added `probe` to `NXsource` (PR 117). ### Changed - **converter** - Improved `NXdetector.data` setter (PR 122). - Renamed attribute `unit` → `units`. ### Removed - **converter** - Removed deprecated `h5_to_nx` (since 0.5.0). ## [0.9.0] - 2022‑06‑24 ### Added - **converter → hdf5** - Machine electrical current handling (PR 106). - `is_rearranged` attribute management (PR 111). - **converter → edf** - Configuration‑file support (PR 104). - **nexus** - Added Nxtomo concatenation (PR 109). ## [0.8.0] - 2021‑06‑04 ### Added - **converter → hdf5** - `bam_single_file` option (PR 96). - pcotomo management (PR 91, 88). - Added **nexus** module exposing an API to edit an NXtomo (PR 87). ## [0.7.0] - 2021‑01‑07 ### Added - **converter → hdf5** - ExternalLink handling for Bliss proposal files (PR 85). - **converter → edf** - Fixed progress‑bar issues (PR 80). - **patch‑nx** - Option to convert all frames of a given type (PR 84). - **dxfile2nx** - Single‑value pixel‑size support (PR 77). - **miscellaneous** - Added missing aliases `flat`/`ref` (PR 83). - Integrated validator (PR 81). ## [0.6.0] - 2021‑10‑04 ### Added - **app** - `dxfile2nx` – Convert DX‑files to NXtomo. - `h52nx` – New `duplicate_data` flag to force frame duplication. - `h5-3dxrd-2nx` – Convert Bliss‑HDF5 3D‑XRD to enhanced NXtomo (updates VDS links). - **converter** - Added `dxfileconverter`. ## [0.5.0] - 2021‑04‑20 ### Fixed - **utils** - Fixed negative‑index handling. ### Added - **converter → hdf5** - Added `start_time` & `end_time` fields. - Full configuration‑file support (`HDF5Config`, `HDF5ConfigHandler`). - `ignore_sub_entries` to skip specific scans. - **app** - `patch-nx` – Added `--embed-data` flag. - Added `--ignore-sub-entries` option. ### Changed - **converter → hdf5** - Reworked virtual‑dataset creation. - Enforced projection count consistency (`tomo_n`). - Improved key/path discovery logic. - Warning when no acquisitions are found. - **utils** - Refactored `_insert_frame_data` into `_FrameAppender` class. - **app** - Renamed `tomoh52nx` → `h52nx` (deprecating former). - Renamed `tomoedf2nx` → `edf2nx` (deprecating former). - `h52nx` now accepts a configuration file (`--config`). ## [0.4.0] - 2020‑11‑09 ### Added - **utils** - `change_image_key_control` – Modify frame type in‑place. - `add_dark_flat_nx_file` – Insert dark/flat series into an existing NXtomo. - **converter → h5_to_nx** - Proposal‑file handling (External/SoftLink). - Saved discovered magnified/sample pixel size as `pixel_size`. - Optional `display_advancement` flag. - Z‑series split by Z value. - Added NXdata for root‑level detector/image display. - **app** - `tomoh52nx` – Added warnings for files already containing NXtomo entries, auto‑creates output directories, checks write permissions, splits Z‑series. ### Changed - **app** - `patch-nx` – Modify existing NXtomo (add dark/flat, change frame type). - **converter → h5_to_nx** - Enforced relative paths for output files. ### Misc - Minimum required **h5py ≥ 3**. - Adopted **black** code‑style formatting. ## [0.3.4] - 2020‑10‑05 ### Fixed - Logging issues in the converter. ## [0.3.3] - 2020‑08‑26 ### Added - **h5_to_nx** - `set-param` option to pre‑define parameters (e.g., energy) and avoid interactive prompts. - **io** - Support for HDF5 files via `tomoscan.io.HDF5File`. ## [0.3.1] - 2020‑08‑19 ### Added - `field_of_view` parameter. - Plugin system allowing users to define motor positions from a Python script (PR !19). ## [0.3.0] - 2020‑03‑20 ### Added - **app** - Various options to set titles. - **h5_to_nx** - Plugin support for custom motor positions. - Distances now expressed in **metres**. - **edf_to_nx** - Distances now expressed in **metres**. ## [0.2.0] - 2020‑04‑22 ### Added - Entry point on `__main__`. - **converter → h5_to_nx** - Optional callback for input handling. - **doc** - API documentation. - Tutorials for `tomoedf2nx` and `tomoh5tonx`. ## [0.1.0] - 2020‑03‑12 ### Added - **app** - `tomoedf2nx` – Convert Bliss + EDF acquisitions to HDF5/NXtomo. - `tomoh5tonx` – Convert Bliss/HDF5 acquisitions to NXtomo. - **converter** - `h5_to_nx` – Core conversion function from Bliss HDF5 to NXtomo. - `get_bliss_tomo_entries` – Retrieve Bliss “roor” entries (e.g., `tomo:basic`, `tomo:fullturn`). nxtomomill-v2.0.1/LICENSE000066400000000000000000000023511511430602400151040ustar00rootroot00000000000000 The nxtomomill library goal is to provide a python interface to read ESRF tomography dataset. nxtomomill is distributed under the MIT 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. nxtomomill-v2.0.1/README.md000066400000000000000000000022251511430602400153560ustar00rootroot00000000000000# nxtomomill nxtomomill provide a set of applications and tools around the [NXtomo](https://manual.nexusformat.org/classes/applications/NXtomo.html) format defined by the [NeXus community](https://manual.nexusformat.org/index.html#). It includes for example the convertion from bliss raw data (@ESRF) to NXtomo, or from spec EDF (@ESRF) to NXtomo. But also creation from scratch and edition of an NXtomo from a python API. It also embed a `nexus` module allowing users to easily edit Nxtomo ## installation To install the latest 'nxtomomill' pip package ```bash pip install nxtomomill ``` You can also install nxtomomill from source: ```bash pip install git+https://gitlab.esrf.fr/tomotools/nxtomomill.git ``` ## documentation General documentation can be found here: [https://tomotools.gitlab-pages.esrf.fr/nxtomomill/](https://tomotools.gitlab-pages.esrf.fr/nxtomomill/) ## application documentation regarding applications can be found here: [https://tomotools.gitlab-pages.esrf.fr/nxtomomill/tutorials/index.html](https://tomotools.gitlab-pages.esrf.fr/nxtomomill/tutorials/index.html) or to get help you can directly go for ```bash nxtomomill --help ``` nxtomomill-v2.0.1/doc/000077500000000000000000000000001511430602400146435ustar00rootroot00000000000000nxtomomill-v2.0.1/doc/Makefile000066400000000000000000000011761511430602400163100ustar00rootroot00000000000000# Minimal makefile for Sphinx documentation # # You can set these variables from the command line, and also # from the environment for the first two. SPHINXOPTS ?= SPHINXBUILD ?= sphinx-build SOURCEDIR = source BUILDDIR = build # Put it first so that "make" without argument is like "make help". help: @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) .PHONY: help Makefile # Catch-all target: route all unknown targets to Sphinx using the new # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). %: Makefile @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) nxtomomill-v2.0.1/doc/_static/000077500000000000000000000000001511430602400162715ustar00rootroot00000000000000nxtomomill-v2.0.1/doc/_static/navbar_icons/000077500000000000000000000000001511430602400207355ustar00rootroot00000000000000nxtomomill-v2.0.1/doc/_static/navbar_icons/gitlab.svg000066400000000000000000000057351511430602400227320ustar00rootroot00000000000000 nxtomomill-v2.0.1/doc/_static/navbar_icons/pypi.svg000066400000000000000000001707701511430602400224530ustar00rootroot00000000000000 nxtomomill-v2.0.1/doc/_templates/000077500000000000000000000000001511430602400170005ustar00rootroot00000000000000nxtomomill-v2.0.1/doc/_templates/autosummary/000077500000000000000000000000001511430602400213665ustar00rootroot00000000000000nxtomomill-v2.0.1/doc/_templates/autosummary/class.rst000066400000000000000000000011421511430602400232230ustar00rootroot00000000000000{{ fullname | escape | underline}} .. currentmodule:: {{ module }} .. autoclass:: {{ objname }} :members: :show-inheritance: :inherited-members: {% block methods %} .. automethod:: __init__ {% if methods %} .. rubric:: {{ _('Methods') }} .. autosummary:: {% for item in methods %} ~{{ name }}.{{ item }} {%- endfor %} {% endif %} {% endblock %} {% block attributes %} {% if attributes %} .. rubric:: {{ _('Attributes') }} .. autosummary:: {% for item in attributes %} ~{{ name }}.{{ item }} {%- endfor %} {% endif %} {% endblock %}nxtomomill-v2.0.1/doc/_templates/autosummary/module.rst000066400000000000000000000021451511430602400234070ustar00rootroot00000000000000{{ fullname | escape | underline}} .. automodule:: {{ fullname }} {% block attributes %} {% if attributes %} .. rubric:: Module Attributes .. autosummary:: :toctree: {% for item in attributes %} {{ item }} {%- endfor %} {% endif %} {% endblock %} {% block functions %} {% if functions %} .. rubric:: {{ _('Functions') }} .. autosummary:: :toctree: {% for item in functions %} {{ item }} {%- endfor %} {% endif %} {% endblock %} {% block classes %} {% if classes %} .. rubric:: {{ _('Classes') }} .. autosummary:: :toctree: {% for item in classes %} {{ item }} {%- endfor %} {% endif %} {% endblock %} {% block exceptions %} {% if exceptions %} .. rubric:: {{ _('Exceptions') }} .. autosummary:: :toctree: {% for item in exceptions %} {{ item }} {%- endfor %} {% endif %} {% endblock %} {% block modules %} {% if modules %} .. rubric:: Modules .. autosummary:: :toctree: :recursive: {% for item in modules %} {{ item }} {%- endfor %} {% endif %} {% endblock %}nxtomomill-v2.0.1/doc/_templates/version.html000066400000000000000000000001001511430602400213420ustar00rootroot00000000000000 {{ version }}nxtomomill-v2.0.1/doc/api.rst000066400000000000000000000001471511430602400161500ustar00rootroot00000000000000API Reference ============= .. autosummary:: :toctree: _generated :recursive: nxtomomillnxtomomill-v2.0.1/doc/conf.py000066400000000000000000000064441511430602400161520ustar00rootroot00000000000000# Configuration file for the Sphinx documentation builder. # # This file only contains a selection of the most common options. For a full # list see the documentation: # https://www.sphinx-doc.org/en/master/usage/configuration.html # -- Path setup -------------------------------------------------------------- # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. # # import os # import sys # sys.path.insert(0, os.path.abspath('.')) # -- Project information ----------------------------------------------------- project = "nxtomomill" copyright = "2020-2025, ESRF" author = "P.-O. Autran, J. Lesaint, A. Mirone, C. Nemoz, P. Paleo, H. Payno, A. Sole, N. Vigano" # The full version, including alpha/beta/rc tags release = "2.0" version = release # -- General configuration --------------------------------------------------- # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ "sphinx.ext.autodoc", "sphinx.ext.autosectionlabel", "sphinx.ext.intersphinx", "sphinx.ext.viewcode", "sphinx.ext.doctest", "sphinx.ext.inheritance_diagram", "sphinx.ext.autosummary", "nbsphinx", "sphinx_design", "sphinx_autodoc_typehints", "sphinxcontrib.programoutput", "myst_parser", ] source_suffix = { ".rst": "restructuredtext", ".md": "markdown", } # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ["_static"] # Add any paths that contain templates here, relative to this directory. templates_path = ["_templates"] # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This pattern also affects html_static_path and html_extra_path. exclude_patterns = [] # -- Options for HTML output ------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # html_theme = "pydata_sphinx_theme" # The name of the Pygments (syntax highlighting) style to use. pygments_style = "sphinx" html_logo = "img/nxtomomill.png" # autosummary options autosummary_generate = True autodoc_default_flags = [ "members", "undoc-members", "show-inheritance", ] html_theme_options = { "icon_links": [ { "name": "pypi", "url": "https://pypi.org/project/nxtomomill", "icon": "_static/navbar_icons/pypi.svg", "type": "local", }, { "name": "gitlab", "url": "https://gitlab.esrf.fr/tomotools/nxtomomill", "icon": "_static/navbar_icons/gitlab.svg", "type": "local", }, ], "show_toc_level": 1, "navbar_align": "left", "show_version_warning_banner": True, "navbar_start": ["navbar-logo", "version"], "navbar_center": ["navbar-nav"], "footer_start": ["copyright"], "footer_center": ["sphinx-version"], } nxtomomill-v2.0.1/doc/development/000077500000000000000000000000001511430602400171655ustar00rootroot00000000000000nxtomomill-v2.0.1/doc/development/changelog.rst000066400000000000000000000001001511430602400216350ustar00rootroot00000000000000.. include:: ../../CHANGELOG.md :parser: myst_parser.sphinx_ nxtomomill-v2.0.1/doc/development/create_your_own_sequence.rst000066400000000000000000000065271511430602400250250ustar00rootroot00000000000000How to define your own sequence to build an NXtomo ================================================== Design """""" The conversion process is done as follow: .. image:: img/nxtomomill_design_1.png The first step this can be done two ways .. image:: img/nxtomomill_design_2.png Until now we were only using the title to deduce the acquisition and the type of frames of each Bliss entry. But the `FRAME_TYPE_SECTION` allow us to ignore those titles and define manually the sequence of the acquisition. Coming back to the `FRAME_TYPE_SECTION` section `FRAME_TYPE_SECTION` section '''''''''''''''''''''''''''' If this section is fill then the `ENTRIES_AND_TITLES_SECTION` will be ignored. Those are mutually exclusive sections. From it we can define `data_scans` that allow us to define a sequence of scan defining an acquisition using [url](https://fr.wikipedia.org/wiki/Uniform_Resource_Locator). Like: .. code-block:: text data_scans = ( (frame_type=projections, entry=silx:///path/to/file?/path/to/scan/node,), (frame_type=projections, entry=/path_relative_to_file), ) Here we will create one acquisition from `silx:///path/to/file?/path/to/scan/node` to be used as a set of projections and `/path_relative_to_file` as a set of projections to. .. note:: Url can be relative to different file .. warning:: The created acquisition will follow the provided order Example: create an NXtomo using the `data_scans` field """""""""""""""""""""""""""""""""""""""""""""""""""""" Using the configuration file '''''''''''''''''''''''''''' You can find a file `conversion_using_data_scans.cfg` in the `solution` folder that create an acquisition using `data_scans`: .. image:: img/nxtomomill_example_data_scans.png It can be executed by calling: .. code-block:: bash nxtomomill h52nx --config conversion_using_data_scans.cfg Using the python API '''''''''''''''''''' .. code-block:: python from nxtomomill.converter import from_h5_to_nx from nxtomomill.io.config import TomoHDF5Config from nxtomomill.io.config.h52nxtomo_models import FrameGroup from silx.io.url import DataUrl input_file_path = "bambou_hercules_0001.h5" configuration = TomoHDF5Config() configuration.input_file = input_file_path configuration.output_file = "bambou_hercules_0001.nx" configuration.data_scans = ( FrameGroup( url=DataUrl( file_path=input_file_path, data_path="1.1", scheme="silx", ), frame_type="initialization", ), FrameGroup( url=DataUrl( file_path=input_file_path, data_path="2.1", scheme="silx", ), frame_type="darks", ), FrameGroup( url=DataUrl( file_path=input_file_path, data_path="3.1", scheme="silx", ), frame_type="flats", ), FrameGroup( url=DataUrl( file_path=input_file_path, data_path="4.1", scheme="silx", ), frame_type="projections", ), ) res = from_h5_to_nx(configuration=configuration) .. note:: you will see another way to create an NXtomo from scratch that could be another alternative. nxtomomill-v2.0.1/doc/development/design/000077500000000000000000000000001511430602400204365ustar00rootroot00000000000000nxtomomill-v2.0.1/doc/development/design/acquisitions_class_diag.mdj000066400000000000000000001452271511430602400260310ustar00rootroot00000000000000{ "_type": "Project", "_id": "AAAAAAFF+h6SjaM2Hec=", "name": "Untitled", "ownedElements": [ { "_type": "UMLModel", "_id": "AAAAAAFF+qBWK6M3Z8Y=", "_parent": { "$ref": "AAAAAAFF+h6SjaM2Hec=" }, "name": "Model", "ownedElements": [ { "_type": "UMLClassDiagram", "_id": "AAAAAAFF+qBtyKM79qY=", "_parent": { "$ref": "AAAAAAFF+qBWK6M3Z8Y=" }, "name": "Main", "defaultDiagram": true, "ownedViews": [ { "_type": "UMLClassView", "_id": "AAAAAAF5YEo6dhiOqvw=", "_parent": { "$ref": "AAAAAAFF+qBtyKM79qY=" }, "model": { "$ref": "AAAAAAF5YEo6dRiMOo4=" }, "subViews": [ { "_type": "UMLNameCompartmentView", "_id": "AAAAAAF5YEo6dhiPytk=", "_parent": { "$ref": "AAAAAAF5YEo6dhiOqvw=" }, "model": { "$ref": "AAAAAAF5YEo6dRiMOo4=" }, "subViews": [ { "_type": "LabelView", "_id": "AAAAAAF5YEo6dhiQ7VI=", "_parent": { "$ref": "AAAAAAF5YEo6dhiPytk=" }, "visible": false, "font": "Arial;13;0", "left": -160, "top": -240, "height": 13 }, { "_type": "LabelView", "_id": "AAAAAAF5YEo6dhiRYSo=", "_parent": { "$ref": "AAAAAAF5YEo6dhiPytk=" }, "font": "Arial;13;3", "left": 45, "top": 143, "width": 555.39453125, "height": 13, "text": "BaseAcquisition" }, { "_type": "LabelView", "_id": "AAAAAAF5YEo6dhiS7HE=", "_parent": { "$ref": "AAAAAAF5YEo6dhiPytk=" }, "visible": false, "font": "Arial;13;0", "left": -160, "top": -240, "width": 73.67724609375, "height": 13, "text": "(from Model)" }, { "_type": "LabelView", "_id": "AAAAAAF5YEo6dxiTJno=", "_parent": { "$ref": "AAAAAAF5YEo6dhiPytk=" }, "visible": false, "font": "Arial;13;0", "left": -160, "top": -240, "height": 13, "horizontalAlignment": 1 } ], "font": "Arial;13;0", "left": 40, "top": 136, "width": 565.39453125, "height": 25, "stereotypeLabel": { "$ref": "AAAAAAF5YEo6dhiQ7VI=" }, "nameLabel": { "$ref": "AAAAAAF5YEo6dhiRYSo=" }, "namespaceLabel": { "$ref": "AAAAAAF5YEo6dhiS7HE=" }, "propertyLabel": { "$ref": "AAAAAAF5YEo6dxiTJno=" } }, { "_type": "UMLAttributeCompartmentView", "_id": "AAAAAAF5YEo6dxiU47Y=", "_parent": { "$ref": "AAAAAAF5YEo6dhiOqvw=" }, "model": { "$ref": "AAAAAAF5YEo6dRiMOo4=" }, "subViews": [ { "_type": "UMLAttributeView", "_id": "AAAAAAF5YE3qxhuhs2k=", "_parent": { "$ref": "AAAAAAF5YEo6dxiU47Y=" }, "model": { "$ref": "AAAAAAF5YE3qnRuekgo=" }, "font": "Arial;13;0", "left": 45, "top": 166, "width": 555.39453125, "height": 13, "text": "+configuration", "horizontalAlignment": 0 }, { "_type": "UMLAttributeView", "_id": "AAAAAAF5YE5rDRvMRFw=", "_parent": { "$ref": "AAAAAAF5YEo6dxiU47Y=" }, "model": { "$ref": "AAAAAAF5YE5q+RvJz2Q=" }, "font": "Arial;13;0", "left": 45, "top": 181, "width": 555.39453125, "height": 13, "text": "+root_url", "horizontalAlignment": 0 } ], "font": "Arial;13;0", "left": 40, "top": 161, "width": 565.39453125, "height": 38 }, { "_type": "UMLOperationCompartmentView", "_id": "AAAAAAF5YEo6dxiVdxg=", "_parent": { "$ref": "AAAAAAF5YEo6dhiOqvw=" }, "model": { "$ref": "AAAAAAF5YEo6dRiMOo4=" }, "subViews": [ { "_type": "UMLOperationView", "_id": "AAAAAAF5YE8aExwSFh4=", "_parent": { "$ref": "AAAAAAF5YEo6dxiVdxg=" }, "model": { "$ref": "AAAAAAF5YE8aAhwP3cw=" }, "font": "Arial;13;0", "left": 45, "top": 204, "width": 555.39453125, "height": 13, "text": "+register_step(url, entry_type, copy_frames)", "horizontalAlignment": 0 }, { "_type": "UMLOperationView", "_id": "AAAAAAF5YFSnrh0hmNg=", "_parent": { "$ref": "AAAAAAF5YEo6dxiVdxg=" }, "model": { "$ref": "AAAAAAF5YFSnnB0euqk=" }, "font": "Arial;13;0", "left": 45, "top": 219, "width": 555.39453125, "height": 13, "text": "+write_as_nxtomo(output_file, data_path, input_file_path, request_input, plugins, input_callback)", "horizontalAlignment": 0 }, { "_type": "UMLOperationView", "_id": "AAAAAAF5YFcTpx5jjkw=", "_parent": { "$ref": "AAAAAAF5YEo6dxiVdxg=" }, "model": { "$ref": "AAAAAAF5YFcTmR5gyVI=" }, "font": "Arial;13;0", "left": 45, "top": 234, "width": 555.39453125, "height": 13, "text": "+set_plugins(plugins)", "horizontalAlignment": 0 } ], "font": "Arial;13;0", "left": 40, "top": 199, "width": 565.39453125, "height": 53 }, { "_type": "UMLReceptionCompartmentView", "_id": "AAAAAAF5YEo6dxiWRj0=", "_parent": { "$ref": "AAAAAAF5YEo6dhiOqvw=" }, "model": { "$ref": "AAAAAAF5YEo6dRiMOo4=" }, "visible": false, "font": "Arial;13;0", "left": -80, "top": -120, "width": 10, "height": 10 }, { "_type": "UMLTemplateParameterCompartmentView", "_id": "AAAAAAF5YEo6dxiXRX4=", "_parent": { "$ref": "AAAAAAF5YEo6dhiOqvw=" }, "model": { "$ref": "AAAAAAF5YEo6dRiMOo4=" }, "visible": false, "font": "Arial;13;0", "left": -80, "top": -120, "width": 10, "height": 10 } ], "font": "Arial;13;0", "containerChangeable": true, "left": 40, "top": 136, "width": 565.39453125, "height": 116, "nameCompartment": { "$ref": "AAAAAAF5YEo6dhiPytk=" }, "attributeCompartment": { "$ref": "AAAAAAF5YEo6dxiU47Y=" }, "operationCompartment": { "$ref": "AAAAAAF5YEo6dxiVdxg=" }, "receptionCompartment": { "$ref": "AAAAAAF5YEo6dxiWRj0=" }, "templateParameterCompartment": { "$ref": "AAAAAAF5YEo6dxiXRX4=" } }, { "_type": "UMLClassView", "_id": "AAAAAAF5YErP9Ri4ZmE=", "_parent": { "$ref": "AAAAAAFF+qBtyKM79qY=" }, "model": { "$ref": "AAAAAAF5YErP9Ri2oM4=" }, "subViews": [ { "_type": "UMLNameCompartmentView", "_id": "AAAAAAF5YErP9Ri5YMs=", "_parent": { "$ref": "AAAAAAF5YErP9Ri4ZmE=" }, "model": { "$ref": "AAAAAAF5YErP9Ri2oM4=" }, "subViews": [ { "_type": "LabelView", "_id": "AAAAAAF5YErP9Ri6JAM=", "_parent": { "$ref": "AAAAAAF5YErP9Ri5YMs=" }, "visible": false, "font": "Arial;13;0", "height": 13 }, { "_type": "LabelView", "_id": "AAAAAAF5YErP9Ri7Bvo=", "_parent": { "$ref": "AAAAAAF5YErP9Ri5YMs=" }, "font": "Arial;13;1", "left": 77, "top": 399, "width": 127.1181640625, "height": 13, "text": "StandardAcquisition" }, { "_type": "LabelView", "_id": "AAAAAAF5YErP9Ri8Thk=", "_parent": { "$ref": "AAAAAAF5YErP9Ri5YMs=" }, "visible": false, "font": "Arial;13;0", "width": 73.67724609375, "height": 13, "text": "(from Model)" }, { "_type": "LabelView", "_id": "AAAAAAF5YErP9Ri95vA=", "_parent": { "$ref": "AAAAAAF5YErP9Ri5YMs=" }, "visible": false, "font": "Arial;13;0", "height": 13, "horizontalAlignment": 1 } ], "font": "Arial;13;0", "left": 72, "top": 392, "width": 137.1181640625, "height": 25, "stereotypeLabel": { "$ref": "AAAAAAF5YErP9Ri6JAM=" }, "nameLabel": { "$ref": "AAAAAAF5YErP9Ri7Bvo=" }, "namespaceLabel": { "$ref": "AAAAAAF5YErP9Ri8Thk=" }, "propertyLabel": { "$ref": "AAAAAAF5YErP9Ri95vA=" } }, { "_type": "UMLAttributeCompartmentView", "_id": "AAAAAAF5YErP9Ri+rm4=", "_parent": { "$ref": "AAAAAAF5YErP9Ri4ZmE=" }, "model": { "$ref": "AAAAAAF5YErP9Ri2oM4=" }, "font": "Arial;13;0", "left": 72, "top": 417, "width": 137.1181640625, "height": 10 }, { "_type": "UMLOperationCompartmentView", "_id": "AAAAAAF5YErP9hi/8bA=", "_parent": { "$ref": "AAAAAAF5YErP9Ri4ZmE=" }, "model": { "$ref": "AAAAAAF5YErP9Ri2oM4=" }, "font": "Arial;13;0", "left": 72, "top": 427, "width": 137.1181640625, "height": 10 }, { "_type": "UMLReceptionCompartmentView", "_id": "AAAAAAF5YErP9hjAH18=", "_parent": { "$ref": "AAAAAAF5YErP9Ri4ZmE=" }, "model": { "$ref": "AAAAAAF5YErP9Ri2oM4=" }, "visible": false, "font": "Arial;13;0", "width": 10, "height": 10 }, { "_type": "UMLTemplateParameterCompartmentView", "_id": "AAAAAAF5YErP9hjBc5s=", "_parent": { "$ref": "AAAAAAF5YErP9Ri4ZmE=" }, "model": { "$ref": "AAAAAAF5YErP9Ri2oM4=" }, "visible": false, "font": "Arial;13;0", "width": 10, "height": 10 } ], "font": "Arial;13;0", "containerChangeable": true, "left": 72, "top": 392, "width": 137.1181640625, "height": 45, "nameCompartment": { "$ref": "AAAAAAF5YErP9Ri5YMs=" }, "attributeCompartment": { "$ref": "AAAAAAF5YErP9Ri+rm4=" }, "operationCompartment": { "$ref": "AAAAAAF5YErP9hi/8bA=" }, "receptionCompartment": { "$ref": "AAAAAAF5YErP9hjAH18=" }, "templateParameterCompartment": { "$ref": "AAAAAAF5YErP9hjBc5s=" } }, { "_type": "UMLClassView", "_id": "AAAAAAF5YEsIARjizao=", "_parent": { "$ref": "AAAAAAFF+qBtyKM79qY=" }, "model": { "$ref": "AAAAAAF5YEsIABjgVlY=" }, "subViews": [ { "_type": "UMLNameCompartmentView", "_id": "AAAAAAF5YEsIARjj4OU=", "_parent": { "$ref": "AAAAAAF5YEsIARjizao=" }, "model": { "$ref": "AAAAAAF5YEsIABjgVlY=" }, "subViews": [ { "_type": "LabelView", "_id": "AAAAAAF5YEsIARjkM7Y=", "_parent": { "$ref": "AAAAAAF5YEsIARjj4OU=" }, "visible": false, "font": "Arial;13;0", "left": -128, "top": -128, "height": 13 }, { "_type": "LabelView", "_id": "AAAAAAF5YEsIARjlDkU=", "_parent": { "$ref": "AAAAAAF5YEsIARjj4OU=" }, "font": "Arial;13;1", "left": 261, "top": 343, "width": 195.48876953125, "height": 13, "text": "ZSeriesBaseAcquisition" }, { "_type": "LabelView", "_id": "AAAAAAF5YEsIARjmTD0=", "_parent": { "$ref": "AAAAAAF5YEsIARjj4OU=" }, "visible": false, "font": "Arial;13;0", "left": -128, "top": -128, "width": 73.67724609375, "height": 13, "text": "(from Model)" }, { "_type": "LabelView", "_id": "AAAAAAF5YEsIARjn5nE=", "_parent": { "$ref": "AAAAAAF5YEsIARjj4OU=" }, "visible": false, "font": "Arial;13;0", "left": -128, "top": -128, "height": 13, "horizontalAlignment": 1 } ], "font": "Arial;13;0", "left": 256, "top": 336, "width": 205.48876953125, "height": 25, "stereotypeLabel": { "$ref": "AAAAAAF5YEsIARjkM7Y=" }, "nameLabel": { "$ref": "AAAAAAF5YEsIARjlDkU=" }, "namespaceLabel": { "$ref": "AAAAAAF5YEsIARjmTD0=" }, "propertyLabel": { "$ref": "AAAAAAF5YEsIARjn5nE=" } }, { "_type": "UMLAttributeCompartmentView", "_id": "AAAAAAF5YEsIARjoGG0=", "_parent": { "$ref": "AAAAAAF5YEsIARjizao=" }, "model": { "$ref": "AAAAAAF5YEsIABjgVlY=" }, "font": "Arial;13;0", "left": 256, "top": 361, "width": 205.48876953125, "height": 10 }, { "_type": "UMLOperationCompartmentView", "_id": "AAAAAAF5YEsIARjpZEU=", "_parent": { "$ref": "AAAAAAF5YEsIARjizao=" }, "model": { "$ref": "AAAAAAF5YEsIABjgVlY=" }, "subViews": [ { "_type": "UMLOperationView", "_id": "AAAAAAF5YFHDLRyRJU8=", "_parent": { "$ref": "AAAAAAF5YEsIARjpZEU=" }, "model": { "$ref": "AAAAAAF5YFHDDxyO+ko=" }, "font": "Arial;13;0", "left": 261, "top": 376, "width": 195.48876953125, "height": 13, "text": "+get_standard_sub_acquisitions()", "horizontalAlignment": 0 }, { "_type": "UMLOperationView", "_id": "AAAAAAF5YFInkRy8cT8=", "_parent": { "$ref": "AAAAAAF5YEsIARjpZEU=" }, "model": { "$ref": "AAAAAAF5YFInehy5wqs=" }, "font": "Arial;13;0", "left": 261, "top": 391, "width": 195.48876953125, "height": 13, "text": "+get_z(entry)", "horizontalAlignment": 0 } ], "font": "Arial;13;0", "left": 256, "top": 371, "width": 205.48876953125, "height": 38 }, { "_type": "UMLReceptionCompartmentView", "_id": "AAAAAAF5YEsIARjqk2o=", "_parent": { "$ref": "AAAAAAF5YEsIARjizao=" }, "model": { "$ref": "AAAAAAF5YEsIABjgVlY=" }, "visible": false, "font": "Arial;13;0", "left": -64, "top": -64, "width": 10, "height": 10 }, { "_type": "UMLTemplateParameterCompartmentView", "_id": "AAAAAAF5YEsIARjrjQI=", "_parent": { "$ref": "AAAAAAF5YEsIARjizao=" }, "model": { "$ref": "AAAAAAF5YEsIABjgVlY=" }, "visible": false, "font": "Arial;13;0", "left": -64, "top": -64, "width": 10, "height": 10 } ], "font": "Arial;13;0", "containerChangeable": true, "left": 256, "top": 336, "width": 205.48876953125, "height": 73, "nameCompartment": { "$ref": "AAAAAAF5YEsIARjj4OU=" }, "attributeCompartment": { "$ref": "AAAAAAF5YEsIARjoGG0=" }, "operationCompartment": { "$ref": "AAAAAAF5YEsIARjpZEU=" }, "receptionCompartment": { "$ref": "AAAAAAF5YEsIARjqk2o=" }, "templateParameterCompartment": { "$ref": "AAAAAAF5YEsIARjrjQI=" } }, { "_type": "UMLAssociationView", "_id": "AAAAAAF5YEtmKBl3vwU=", "_parent": { "$ref": "AAAAAAFF+qBtyKM79qY=" }, "model": { "$ref": "AAAAAAF5YEtmJxlzzzY=" }, "subViews": [ { "_type": "EdgeLabelView", "_id": "AAAAAAF5YEtmKBl4WvE=", "_parent": { "$ref": "AAAAAAF5YEtmKBl3vwU=" }, "model": { "$ref": "AAAAAAF5YEtmJxlzzzY=" }, "font": "Arial;13;0", "left": 215, "top": 375, "width": 29.275390625, "height": 13, "alpha": 1.5707963267948966, "distance": 15, "hostEdge": { "$ref": "AAAAAAF5YEtmKBl3vwU=" }, "edgePosition": 1, "text": "+1..n" }, { "_type": "EdgeLabelView", "_id": "AAAAAAF5YEtmKBl5K4w=", "_parent": { "$ref": "AAAAAAF5YEtmKBl3vwU=" }, "model": { "$ref": "AAAAAAF5YEtmJxlzzzY=" }, "visible": null, "font": "Arial;13;0", "left": 226, "top": 360, "height": 13, "alpha": 1.5707963267948966, "distance": 30, "hostEdge": { "$ref": "AAAAAAF5YEtmKBl3vwU=" }, "edgePosition": 1 }, { "_type": "EdgeLabelView", "_id": "AAAAAAF5YEtmKBl6BQo=", "_parent": { "$ref": "AAAAAAF5YEtmKBl3vwU=" }, "model": { "$ref": "AAAAAAF5YEtmJxlzzzY=" }, "visible": false, "font": "Arial;13;0", "left": 234, "top": 404, "height": 13, "alpha": -1.5707963267948966, "distance": 15, "hostEdge": { "$ref": "AAAAAAF5YEtmKBl3vwU=" }, "edgePosition": 1 }, { "_type": "EdgeLabelView", "_id": "AAAAAAF5YEtmKBl76y8=", "_parent": { "$ref": "AAAAAAF5YEtmKBl3vwU=" }, "model": { "$ref": "AAAAAAF5YEtmKBl0edM=" }, "visible": false, "font": "Arial;13;0", "left": 231, "top": 375, "height": 13, "alpha": 0.5235987755982988, "distance": 30, "hostEdge": { "$ref": "AAAAAAF5YEtmKBl3vwU=" }, "edgePosition": 2 }, { "_type": "EdgeLabelView", "_id": "AAAAAAF5YEtmKBl8Lek=", "_parent": { "$ref": "AAAAAAF5YEtmKBl3vwU=" }, "model": { "$ref": "AAAAAAF5YEtmKBl0edM=" }, "visible": false, "font": "Arial;13;0", "left": 231, "top": 361, "height": 13, "alpha": 0.7853981633974483, "distance": 40, "hostEdge": { "$ref": "AAAAAAF5YEtmKBl3vwU=" }, "edgePosition": 2 }, { "_type": "EdgeLabelView", "_id": "AAAAAAF5YEtmKBl9hJQ=", "_parent": { "$ref": "AAAAAAF5YEtmKBl3vwU=" }, "model": { "$ref": "AAAAAAF5YEtmKBl0edM=" }, "visible": false, "font": "Arial;13;0", "left": 232, "top": 403, "height": 13, "alpha": -0.5235987755982988, "distance": 25, "hostEdge": { "$ref": "AAAAAAF5YEtmKBl3vwU=" }, "edgePosition": 2 }, { "_type": "EdgeLabelView", "_id": "AAAAAAF5YEtmKBl+z08=", "_parent": { "$ref": "AAAAAAF5YEtmKBl3vwU=" }, "model": { "$ref": "AAAAAAF5YEtmKBl1IEs=" }, "visible": false, "font": "Arial;13;0", "left": 226, "top": 376, "height": 13, "alpha": -0.5235987755982988, "distance": 30, "hostEdge": { "$ref": "AAAAAAF5YEtmKBl3vwU=" } }, { "_type": "EdgeLabelView", "_id": "AAAAAAF5YEtmKBl/bkg=", "_parent": { "$ref": "AAAAAAF5YEtmKBl3vwU=" }, "model": { "$ref": "AAAAAAF5YEtmKBl1IEs=" }, "visible": false, "font": "Arial;13;0", "left": 221, "top": 363, "height": 13, "alpha": -0.7853981633974483, "distance": 40, "hostEdge": { "$ref": "AAAAAAF5YEtmKBl3vwU=" } }, { "_type": "EdgeLabelView", "_id": "AAAAAAF5YEtmKBmAywg=", "_parent": { "$ref": "AAAAAAF5YEtmKBl3vwU=" }, "model": { "$ref": "AAAAAAF5YEtmKBl1IEs=" }, "visible": false, "font": "Arial;13;0", "left": 236, "top": 402, "height": 13, "alpha": 0.5235987755982988, "distance": 25, "hostEdge": { "$ref": "AAAAAAF5YEtmKBl3vwU=" } }, { "_type": "UMLQualifierCompartmentView", "_id": "AAAAAAF5YEtmKBmB/1E=", "_parent": { "$ref": "AAAAAAF5YEtmKBl3vwU=" }, "model": { "$ref": "AAAAAAF5YEtmKBl0edM=" }, "visible": false, "font": "Arial;13;0", "width": 10, "height": 10 }, { "_type": "UMLQualifierCompartmentView", "_id": "AAAAAAF5YEtmKBmC368=", "_parent": { "$ref": "AAAAAAF5YEtmKBl3vwU=" }, "model": { "$ref": "AAAAAAF5YEtmKBl1IEs=" }, "visible": false, "font": "Arial;13;0", "width": 10, "height": 10 } ], "font": "Arial;13;0", "head": { "$ref": "AAAAAAF5YEsIARjizao=" }, "tail": { "$ref": "AAAAAAF5YErP9Ri4ZmE=" }, "lineStyle": 1, "points": "209:401;255:392", "showVisibility": true, "nameLabel": { "$ref": "AAAAAAF5YEtmKBl4WvE=" }, "stereotypeLabel": { "$ref": "AAAAAAF5YEtmKBl5K4w=" }, "propertyLabel": { "$ref": "AAAAAAF5YEtmKBl6BQo=" }, "tailRoleNameLabel": { "$ref": "AAAAAAF5YEtmKBl76y8=" }, "tailPropertyLabel": { "$ref": "AAAAAAF5YEtmKBl8Lek=" }, "tailMultiplicityLabel": { "$ref": "AAAAAAF5YEtmKBl9hJQ=" }, "headRoleNameLabel": { "$ref": "AAAAAAF5YEtmKBl+z08=" }, "headPropertyLabel": { "$ref": "AAAAAAF5YEtmKBl/bkg=" }, "headMultiplicityLabel": { "$ref": "AAAAAAF5YEtmKBmAywg=" }, "tailQualifiersCompartment": { "$ref": "AAAAAAF5YEtmKBmB/1E=" }, "headQualifiersCompartment": { "$ref": "AAAAAAF5YEtmKBmC368=" } }, { "_type": "UMLGeneralizationView", "_id": "AAAAAAF5YEuX6xnF62o=", "_parent": { "$ref": "AAAAAAFF+qBtyKM79qY=" }, "model": { "$ref": "AAAAAAF5YEuX6hnD22Y=" }, "subViews": [ { "_type": "EdgeLabelView", "_id": "AAAAAAF5YEuX6xnGWk4=", "_parent": { "$ref": "AAAAAAF5YEuX6xnF62o=" }, "model": { "$ref": "AAAAAAF5YEuX6hnD22Y=" }, "visible": false, "font": "Arial;13;0", "left": 204, "top": 305, "height": 13, "alpha": 1.5707963267948966, "distance": 15, "hostEdge": { "$ref": "AAAAAAF5YEuX6xnF62o=" }, "edgePosition": 1 }, { "_type": "EdgeLabelView", "_id": "AAAAAAF5YEuX6xnH6VM=", "_parent": { "$ref": "AAAAAAF5YEuX6xnF62o=" }, "model": { "$ref": "AAAAAAF5YEuX6hnD22Y=" }, "visible": null, "font": "Arial;13;0", "left": 192, "top": 295, "height": 13, "alpha": 1.5707963267948966, "distance": 30, "hostEdge": { "$ref": "AAAAAAF5YEuX6xnF62o=" }, "edgePosition": 1 }, { "_type": "EdgeLabelView", "_id": "AAAAAAF5YEuX6xnIWik=", "_parent": { "$ref": "AAAAAAF5YEuX6xnF62o=" }, "model": { "$ref": "AAAAAAF5YEuX6hnD22Y=" }, "visible": false, "font": "Arial;13;0", "left": 227, "top": 324, "height": 13, "alpha": -1.5707963267948966, "distance": 15, "hostEdge": { "$ref": "AAAAAAF5YEuX6xnF62o=" }, "edgePosition": 1 } ], "font": "Arial;13;0", "head": { "$ref": "AAAAAAF5YEo6dhiOqvw=" }, "tail": { "$ref": "AAAAAAF5YErP9Ri4ZmE=" }, "lineStyle": 1, "points": "159:391;273:252", "showVisibility": true, "nameLabel": { "$ref": "AAAAAAF5YEuX6xnGWk4=" }, "stereotypeLabel": { "$ref": "AAAAAAF5YEuX6xnH6VM=" }, "propertyLabel": { "$ref": "AAAAAAF5YEuX6xnIWik=" } }, { "_type": "UMLGeneralizationView", "_id": "AAAAAAF5YEvHzBnrTBQ=", "_parent": { "$ref": "AAAAAAFF+qBtyKM79qY=" }, "model": { "$ref": "AAAAAAF5YEvHzBnpWdU=" }, "subViews": [ { "_type": "EdgeLabelView", "_id": "AAAAAAF5YEvHzBnskVY=", "_parent": { "$ref": "AAAAAAF5YEvHzBnrTBQ=" }, "model": { "$ref": "AAAAAAF5YEvHzBnpWdU=" }, "visible": false, "font": "Arial;13;0", "left": 327, "top": 289, "height": 13, "alpha": 1.5707963267948966, "distance": 15, "hostEdge": { "$ref": "AAAAAAF5YEvHzBnrTBQ=" }, "edgePosition": 1 }, { "_type": "EdgeLabelView", "_id": "AAAAAAF5YEvHzBntwJU=", "_parent": { "$ref": "AAAAAAF5YEvHzBnrTBQ=" }, "model": { "$ref": "AAAAAAF5YEvHzBnpWdU=" }, "visible": null, "font": "Arial;13;0", "left": 312, "top": 292, "height": 13, "alpha": 1.5707963267948966, "distance": 30, "hostEdge": { "$ref": "AAAAAAF5YEvHzBnrTBQ=" }, "edgePosition": 1 }, { "_type": "EdgeLabelView", "_id": "AAAAAAF5YEvHzBnu60o=", "_parent": { "$ref": "AAAAAAF5YEvHzBnrTBQ=" }, "model": { "$ref": "AAAAAAF5YEvHzBnpWdU=" }, "visible": false, "font": "Arial;13;0", "left": 356, "top": 284, "height": 13, "alpha": -1.5707963267948966, "distance": 15, "hostEdge": { "$ref": "AAAAAAF5YEvHzBnrTBQ=" }, "edgePosition": 1 } ], "font": "Arial;13;0", "head": { "$ref": "AAAAAAF5YEo6dhiOqvw=" }, "tail": { "$ref": "AAAAAAF5YEsIARjizao=" }, "lineStyle": 1, "points": "351:335;334:252", "showVisibility": true, "nameLabel": { "$ref": "AAAAAAF5YEvHzBnskVY=" }, "stereotypeLabel": { "$ref": "AAAAAAF5YEvHzBntwJU=" }, "propertyLabel": { "$ref": "AAAAAAF5YEvHzBnu60o=" } }, { "_type": "UMLClassView", "_id": "AAAAAAF5YEv1xhoU1x0=", "_parent": { "$ref": "AAAAAAFF+qBtyKM79qY=" }, "model": { "$ref": "AAAAAAF5YEv1xhoSSYo=" }, "subViews": [ { "_type": "UMLNameCompartmentView", "_id": "AAAAAAF5YEv1xhoVP9s=", "_parent": { "$ref": "AAAAAAF5YEv1xhoU1x0=" }, "model": { "$ref": "AAAAAAF5YEv1xhoSSYo=" }, "subViews": [ { "_type": "LabelView", "_id": "AAAAAAF5YEv1xhoWIVY=", "_parent": { "$ref": "AAAAAAF5YEv1xhoVP9s=" }, "visible": false, "font": "Arial;13;0", "left": -112, "top": 48, "height": 13 }, { "_type": "LabelView", "_id": "AAAAAAF5YEv1xhoXi2k=", "_parent": { "$ref": "AAAAAAF5YEv1xhoVP9s=" }, "font": "Arial;13;1", "left": 21, "top": 511, "width": 114.841796875, "height": 13, "text": "XRD3DAcquisition" }, { "_type": "LabelView", "_id": "AAAAAAF5YEv1xhoYFZA=", "_parent": { "$ref": "AAAAAAF5YEv1xhoVP9s=" }, "visible": false, "font": "Arial;13;0", "left": -112, "top": 48, "width": 73.67724609375, "height": 13, "text": "(from Model)" }, { "_type": "LabelView", "_id": "AAAAAAF5YEv1xhoZ9TQ=", "_parent": { "$ref": "AAAAAAF5YEv1xhoVP9s=" }, "visible": false, "font": "Arial;13;0", "left": -112, "top": 48, "height": 13, "horizontalAlignment": 1 } ], "font": "Arial;13;0", "left": 16, "top": 504, "width": 124.841796875, "height": 25, "stereotypeLabel": { "$ref": "AAAAAAF5YEv1xhoWIVY=" }, "nameLabel": { "$ref": "AAAAAAF5YEv1xhoXi2k=" }, "namespaceLabel": { "$ref": "AAAAAAF5YEv1xhoYFZA=" }, "propertyLabel": { "$ref": "AAAAAAF5YEv1xhoZ9TQ=" } }, { "_type": "UMLAttributeCompartmentView", "_id": "AAAAAAF5YEv1xhoaVN4=", "_parent": { "$ref": "AAAAAAF5YEv1xhoU1x0=" }, "model": { "$ref": "AAAAAAF5YEv1xhoSSYo=" }, "subViews": [ { "_type": "UMLAttributeView", "_id": "AAAAAAF5YFf0qR6t5h0=", "_parent": { "$ref": "AAAAAAF5YEv1xhoaVN4=" }, "model": { "$ref": "AAAAAAF5YFf0hx6nKTk=" }, "font": "Arial;13;0", "left": 21, "top": 534, "width": 114.841796875, "height": 13, "text": "+base_tilt", "horizontalAlignment": 0 }, { "_type": "UMLAttributeView", "_id": "AAAAAAF5YFgjAR7YAK0=", "_parent": { "$ref": "AAAAAAF5YEv1xhoaVN4=" }, "model": { "$ref": "AAAAAAF5YFgi6x7SJh4=" }, "font": "Arial;13;0", "left": 21, "top": 549, "width": 114.841796875, "height": 13, "text": "+rocking", "horizontalAlignment": 0 } ], "font": "Arial;13;0", "left": 16, "top": 529, "width": 124.841796875, "height": 38 }, { "_type": "UMLOperationCompartmentView", "_id": "AAAAAAF5YEv1xhobv/Y=", "_parent": { "$ref": "AAAAAAF5YEv1xhoU1x0=" }, "model": { "$ref": "AAAAAAF5YEv1xhoSSYo=" }, "font": "Arial;13;0", "left": 16, "top": 567, "width": 124.841796875, "height": 10 }, { "_type": "UMLReceptionCompartmentView", "_id": "AAAAAAF5YEv1xhoclfw=", "_parent": { "$ref": "AAAAAAF5YEv1xhoU1x0=" }, "model": { "$ref": "AAAAAAF5YEv1xhoSSYo=" }, "visible": false, "font": "Arial;13;0", "left": -56, "top": 24, "width": 10, "height": 10 }, { "_type": "UMLTemplateParameterCompartmentView", "_id": "AAAAAAF5YEv1xxodm6E=", "_parent": { "$ref": "AAAAAAF5YEv1xhoU1x0=" }, "model": { "$ref": "AAAAAAF5YEv1xhoSSYo=" }, "visible": false, "font": "Arial;13;0", "left": -56, "top": 24, "width": 10, "height": 10 } ], "font": "Arial;13;0", "containerChangeable": true, "left": 16, "top": 504, "width": 124.841796875, "height": 73, "nameCompartment": { "$ref": "AAAAAAF5YEv1xhoVP9s=" }, "attributeCompartment": { "$ref": "AAAAAAF5YEv1xhoaVN4=" }, "operationCompartment": { "$ref": "AAAAAAF5YEv1xhobv/Y=" }, "receptionCompartment": { "$ref": "AAAAAAF5YEv1xhoclfw=" }, "templateParameterCompartment": { "$ref": "AAAAAAF5YEv1xxodm6E=" } }, { "_type": "UMLGeneralizationView", "_id": "AAAAAAF5YEwTOxpZUjY=", "_parent": { "$ref": "AAAAAAFF+qBtyKM79qY=" }, "model": { "$ref": "AAAAAAF5YEwTOxpXlwI=" }, "subViews": [ { "_type": "EdgeLabelView", "_id": "AAAAAAF5YEwTOxpaxZ4=", "_parent": { "$ref": "AAAAAAF5YEwTOxpZUjY=" }, "model": { "$ref": "AAAAAAF5YEwTOxpXlwI=" }, "visible": false, "font": "Arial;13;0", "left": 98, "top": 457, "height": 13, "alpha": 1.5707963267948966, "distance": 15, "hostEdge": { "$ref": "AAAAAAF5YEwTOxpZUjY=" }, "edgePosition": 1 }, { "_type": "EdgeLabelView", "_id": "AAAAAAF5YEwTOxpbYxc=", "_parent": { "$ref": "AAAAAAF5YEwTOxpZUjY=" }, "model": { "$ref": "AAAAAAF5YEwTOxpXlwI=" }, "visible": null, "font": "Arial;13;0", "left": 85, "top": 450, "height": 13, "alpha": 1.5707963267948966, "distance": 30, "hostEdge": { "$ref": "AAAAAAF5YEwTOxpZUjY=" }, "edgePosition": 1 }, { "_type": "EdgeLabelView", "_id": "AAAAAAF5YEwTOxpcMz8=", "_parent": { "$ref": "AAAAAAF5YEwTOxpZUjY=" }, "model": { "$ref": "AAAAAAF5YEwTOxpXlwI=" }, "visible": false, "font": "Arial;13;0", "left": 125, "top": 470, "height": 13, "alpha": -1.5707963267948966, "distance": 15, "hostEdge": { "$ref": "AAAAAAF5YEwTOxpZUjY=" }, "edgePosition": 1 } ], "font": "Arial;13;0", "head": { "$ref": "AAAAAAF5YErP9Ri4ZmE=" }, "tail": { "$ref": "AAAAAAF5YEv1xhoU1x0=" }, "lineStyle": 1, "points": "96:503;129:437", "showVisibility": true, "nameLabel": { "$ref": "AAAAAAF5YEwTOxpaxZ4=" }, "stereotypeLabel": { "$ref": "AAAAAAF5YEwTOxpbYxc=" }, "propertyLabel": { "$ref": "AAAAAAF5YEwTOxpcMz8=" } }, { "_type": "UMLClassView", "_id": "AAAAAAF5YEwzMhqCvV0=", "_parent": { "$ref": "AAAAAAFF+qBtyKM79qY=" }, "model": { "$ref": "AAAAAAF5YEwzMhqAJ4Y=" }, "subViews": [ { "_type": "UMLNameCompartmentView", "_id": "AAAAAAF5YEwzMhqDv5s=", "_parent": { "$ref": "AAAAAAF5YEwzMhqCvV0=" }, "model": { "$ref": "AAAAAAF5YEwzMhqAJ4Y=" }, "subViews": [ { "_type": "LabelView", "_id": "AAAAAAF5YEwzMhqETTE=", "_parent": { "$ref": "AAAAAAF5YEwzMhqDv5s=" }, "visible": false, "font": "Arial;13;0", "left": -240, "top": -80, "height": 13 }, { "_type": "LabelView", "_id": "AAAAAAF5YEwzMhqF8KE=", "_parent": { "$ref": "AAAAAAF5YEwzMhqDv5s=" }, "font": "Arial;13;1", "left": 213, "top": 511, "width": 114.587890625, "height": 13, "text": "XRDCTAcquisition" }, { "_type": "LabelView", "_id": "AAAAAAF5YEwzMhqGQow=", "_parent": { "$ref": "AAAAAAF5YEwzMhqDv5s=" }, "visible": false, "font": "Arial;13;0", "left": -240, "top": -80, "width": 73.67724609375, "height": 13, "text": "(from Model)" }, { "_type": "LabelView", "_id": "AAAAAAF5YEwzMhqHUvU=", "_parent": { "$ref": "AAAAAAF5YEwzMhqDv5s=" }, "visible": false, "font": "Arial;13;0", "left": -240, "top": -80, "height": 13, "horizontalAlignment": 1 } ], "font": "Arial;13;0", "left": 208, "top": 504, "width": 124.587890625, "height": 25, "stereotypeLabel": { "$ref": "AAAAAAF5YEwzMhqETTE=" }, "nameLabel": { "$ref": "AAAAAAF5YEwzMhqF8KE=" }, "namespaceLabel": { "$ref": "AAAAAAF5YEwzMhqGQow=" }, "propertyLabel": { "$ref": "AAAAAAF5YEwzMhqHUvU=" } }, { "_type": "UMLAttributeCompartmentView", "_id": "AAAAAAF5YEwzMhqITNs=", "_parent": { "$ref": "AAAAAAF5YEwzMhqCvV0=" }, "model": { "$ref": "AAAAAAF5YEwzMhqAJ4Y=" }, "font": "Arial;13;0", "left": 208, "top": 529, "width": 124.587890625, "height": 10 }, { "_type": "UMLOperationCompartmentView", "_id": "AAAAAAF5YEwzMhqJVgU=", "_parent": { "$ref": "AAAAAAF5YEwzMhqCvV0=" }, "model": { "$ref": "AAAAAAF5YEwzMhqAJ4Y=" }, "font": "Arial;13;0", "left": 208, "top": 539, "width": 124.587890625, "height": 10 }, { "_type": "UMLReceptionCompartmentView", "_id": "AAAAAAF5YEwzMhqKJn0=", "_parent": { "$ref": "AAAAAAF5YEwzMhqCvV0=" }, "model": { "$ref": "AAAAAAF5YEwzMhqAJ4Y=" }, "visible": false, "font": "Arial;13;0", "left": -120, "top": -40, "width": 10, "height": 10 }, { "_type": "UMLTemplateParameterCompartmentView", "_id": "AAAAAAF5YEwzMhqL6qI=", "_parent": { "$ref": "AAAAAAF5YEwzMhqCvV0=" }, "model": { "$ref": "AAAAAAF5YEwzMhqAJ4Y=" }, "visible": false, "font": "Arial;13;0", "left": -120, "top": -40, "width": 10, "height": 10 } ], "font": "Arial;13;0", "containerChangeable": true, "left": 208, "top": 504, "width": 124.587890625, "height": 45, "nameCompartment": { "$ref": "AAAAAAF5YEwzMhqDv5s=" }, "attributeCompartment": { "$ref": "AAAAAAF5YEwzMhqITNs=" }, "operationCompartment": { "$ref": "AAAAAAF5YEwzMhqJVgU=" }, "receptionCompartment": { "$ref": "AAAAAAF5YEwzMhqKJn0=" }, "templateParameterCompartment": { "$ref": "AAAAAAF5YEwzMhqL6qI=" } }, { "_type": "UMLGeneralizationView", "_id": "AAAAAAF5YExpLhrtC8I=", "_parent": { "$ref": "AAAAAAFF+qBtyKM79qY=" }, "model": { "$ref": "AAAAAAF5YExpLhrrLFw=" }, "subViews": [ { "_type": "EdgeLabelView", "_id": "AAAAAAF5YExpLxruTfs=", "_parent": { "$ref": "AAAAAAF5YExpLhrtC8I=" }, "model": { "$ref": "AAAAAAF5YExpLhrrLFw=" }, "visible": false, "font": "Arial;13;0", "left": 194, "top": 475, "height": 13, "alpha": 1.5707963267948966, "distance": 15, "hostEdge": { "$ref": "AAAAAAF5YExpLhrtC8I=" }, "edgePosition": 1 }, { "_type": "EdgeLabelView", "_id": "AAAAAAF5YExpLxrvFPE=", "_parent": { "$ref": "AAAAAAF5YExpLhrtC8I=" }, "model": { "$ref": "AAAAAAF5YExpLhrrLFw=" }, "visible": null, "font": "Arial;13;0", "left": 184, "top": 486, "height": 13, "alpha": 1.5707963267948966, "distance": 30, "hostEdge": { "$ref": "AAAAAAF5YExpLhrtC8I=" }, "edgePosition": 1 }, { "_type": "EdgeLabelView", "_id": "AAAAAAF5YExpLxrwMkY=", "_parent": { "$ref": "AAAAAAF5YExpLhrtC8I=" }, "model": { "$ref": "AAAAAAF5YExpLhrrLFw=" }, "visible": false, "font": "Arial;13;0", "left": 213, "top": 452, "height": 13, "alpha": -1.5707963267948966, "distance": 15, "hostEdge": { "$ref": "AAAAAAF5YExpLhrtC8I=" }, "edgePosition": 1 } ], "font": "Arial;13;0", "head": { "$ref": "AAAAAAF5YErP9Ri4ZmE=" }, "tail": { "$ref": "AAAAAAF5YEwzMhqCvV0=" }, "lineStyle": 1, "points": "243:503;166:437", "showVisibility": true, "nameLabel": { "$ref": "AAAAAAF5YExpLxruTfs=" }, "stereotypeLabel": { "$ref": "AAAAAAF5YExpLxrvFPE=" }, "propertyLabel": { "$ref": "AAAAAAF5YExpLxrwMkY=" } } ] }, { "_type": "UMLClass", "_id": "AAAAAAF5YEo6dRiMOo4=", "_parent": { "$ref": "AAAAAAFF+qBWK6M3Z8Y=" }, "name": "BaseAcquisition", "attributes": [ { "_type": "UMLAttribute", "_id": "AAAAAAF5YE3qnRuekgo=", "_parent": { "$ref": "AAAAAAF5YEo6dRiMOo4=" }, "name": "configuration", "type": "" }, { "_type": "UMLAttribute", "_id": "AAAAAAF5YE5q+RvJz2Q=", "_parent": { "$ref": "AAAAAAF5YEo6dRiMOo4=" }, "name": "root_url", "type": "" } ], "operations": [ { "_type": "UMLOperation", "_id": "AAAAAAF5YE8aAhwP3cw=", "_parent": { "$ref": "AAAAAAF5YEo6dRiMOo4=" }, "name": "register_step", "parameters": [ { "_type": "UMLParameter", "_id": "AAAAAAF5YE+VbhwxoYE=", "_parent": { "$ref": "AAAAAAF5YE8aAhwP3cw=" }, "name": "url", "type": "" }, { "_type": "UMLParameter", "_id": "AAAAAAF5YE/XThxNjbU=", "_parent": { "$ref": "AAAAAAF5YE8aAhwP3cw=" }, "name": "entry_type", "type": "" }, { "_type": "UMLParameter", "_id": "AAAAAAF5YFAaiRxp/lM=", "_parent": { "$ref": "AAAAAAF5YE8aAhwP3cw=" }, "name": "copy_frames", "type": "" } ] }, { "_type": "UMLOperation", "_id": "AAAAAAF5YFSnnB0euqk=", "_parent": { "$ref": "AAAAAAF5YEo6dRiMOo4=" }, "name": "write_as_nxtomo", "parameters": [ { "_type": "UMLParameter", "_id": "AAAAAAF5YFTj6R1Ahyo=", "_parent": { "$ref": "AAAAAAF5YFSnnB0euqk=" }, "name": "output_file", "type": "" }, { "_type": "UMLParameter", "_id": "AAAAAAF5YFUHYx1cQCk=", "_parent": { "$ref": "AAAAAAF5YFSnnB0euqk=" }, "name": "data_path", "type": "" }, { "_type": "UMLParameter", "_id": "AAAAAAF5YFVAMx14psY=", "_parent": { "$ref": "AAAAAAF5YFSnnB0euqk=" }, "name": "input_file_path", "type": "" }, { "_type": "UMLParameter", "_id": "AAAAAAF5YFVgfx2UaaU=", "_parent": { "$ref": "AAAAAAF5YFSnnB0euqk=" }, "name": "request_input", "type": "" }, { "_type": "UMLParameter", "_id": "AAAAAAF5YFWAfh2wc38=", "_parent": { "$ref": "AAAAAAF5YFSnnB0euqk=" }, "name": "plugins", "type": "" }, { "_type": "UMLParameter", "_id": "AAAAAAF5YFWerh3Mz/k=", "_parent": { "$ref": "AAAAAAF5YFSnnB0euqk=" }, "name": "input_callback", "type": "" } ] }, { "_type": "UMLOperation", "_id": "AAAAAAF5YFcTmR5gyVI=", "_parent": { "$ref": "AAAAAAF5YEo6dRiMOo4=" }, "name": "set_plugins", "parameters": [ { "_type": "UMLParameter", "_id": "AAAAAAF5YFdD6B6CL2I=", "_parent": { "$ref": "AAAAAAF5YFcTmR5gyVI=" }, "name": "plugins", "type": "" } ] } ], "isAbstract": true }, { "_type": "UMLClass", "_id": "AAAAAAF5YErP9Ri2oM4=", "_parent": { "$ref": "AAAAAAFF+qBWK6M3Z8Y=" }, "name": "StandardAcquisition", "ownedElements": [ { "_type": "UMLAssociation", "_id": "AAAAAAF5YEtmJxlzzzY=", "_parent": { "$ref": "AAAAAAF5YErP9Ri2oM4=" }, "name": "1..n", "end1": { "_type": "UMLAssociationEnd", "_id": "AAAAAAF5YEtmKBl0edM=", "_parent": { "$ref": "AAAAAAF5YEtmJxlzzzY=" }, "reference": { "$ref": "AAAAAAF5YErP9Ri2oM4=" } }, "end2": { "_type": "UMLAssociationEnd", "_id": "AAAAAAF5YEtmKBl1IEs=", "_parent": { "$ref": "AAAAAAF5YEtmJxlzzzY=" }, "reference": { "$ref": "AAAAAAF5YEsIABjgVlY=" }, "aggregation": "composite" } }, { "_type": "UMLGeneralization", "_id": "AAAAAAF5YEuX6hnD22Y=", "_parent": { "$ref": "AAAAAAF5YErP9Ri2oM4=" }, "source": { "$ref": "AAAAAAF5YErP9Ri2oM4=" }, "target": { "$ref": "AAAAAAF5YEo6dRiMOo4=" } } ] }, { "_type": "UMLClass", "_id": "AAAAAAF5YEsIABjgVlY=", "_parent": { "$ref": "AAAAAAFF+qBWK6M3Z8Y=" }, "name": "ZSeriesBaseAcquisition", "ownedElements": [ { "_type": "UMLAssociation", "_id": "AAAAAAF5YEtF6xkL5BY=", "_parent": { "$ref": "AAAAAAF5YEsIABjgVlY=" }, "end1": { "_type": "UMLAssociationEnd", "_id": "AAAAAAF5YEtF6xkMfYc=", "_parent": { "$ref": "AAAAAAF5YEtF6xkL5BY=" }, "reference": { "$ref": "AAAAAAF5YEsIABjgVlY=" } }, "end2": { "_type": "UMLAssociationEnd", "_id": "AAAAAAF5YEtF6xkNyXQ=", "_parent": { "$ref": "AAAAAAF5YEtF6xkL5BY=" }, "reference": { "$ref": "AAAAAAF5YErP9Ri2oM4=" }, "aggregation": "composite" } }, { "_type": "UMLGeneralization", "_id": "AAAAAAF5YEvHzBnpWdU=", "_parent": { "$ref": "AAAAAAF5YEsIABjgVlY=" }, "source": { "$ref": "AAAAAAF5YEsIABjgVlY=" }, "target": { "$ref": "AAAAAAF5YEo6dRiMOo4=" } } ], "operations": [ { "_type": "UMLOperation", "_id": "AAAAAAF5YFHDDxyO+ko=", "_parent": { "$ref": "AAAAAAF5YEsIABjgVlY=" }, "name": "get_standard_sub_acquisitions" }, { "_type": "UMLOperation", "_id": "AAAAAAF5YFInehy5wqs=", "_parent": { "$ref": "AAAAAAF5YEsIABjgVlY=" }, "name": "get_z", "parameters": [ { "_type": "UMLParameter", "_id": "AAAAAAF5YFJRyhzbAYE=", "_parent": { "$ref": "AAAAAAF5YFInehy5wqs=" }, "name": "entry", "type": "" } ] } ] }, { "_type": "UMLClass", "_id": "AAAAAAF5YEv1xhoSSYo=", "_parent": { "$ref": "AAAAAAFF+qBWK6M3Z8Y=" }, "name": "XRD3DAcquisition", "ownedElements": [ { "_type": "UMLGeneralization", "_id": "AAAAAAF5YEwTOxpXlwI=", "_parent": { "$ref": "AAAAAAF5YEv1xhoSSYo=" }, "source": { "$ref": "AAAAAAF5YEv1xhoSSYo=" }, "target": { "$ref": "AAAAAAF5YErP9Ri2oM4=" } } ], "attributes": [ { "_type": "UMLAttribute", "_id": "AAAAAAF5YFf0hx6nKTk=", "_parent": { "$ref": "AAAAAAF5YEv1xhoSSYo=" }, "name": "base_tilt", "type": "" }, { "_type": "UMLAttribute", "_id": "AAAAAAF5YFgi6x7SJh4=", "_parent": { "$ref": "AAAAAAF5YEv1xhoSSYo=" }, "name": "rocking", "type": "" } ] }, { "_type": "UMLClass", "_id": "AAAAAAF5YEwzMhqAJ4Y=", "_parent": { "$ref": "AAAAAAFF+qBWK6M3Z8Y=" }, "name": "XRDCTAcquisition", "ownedElements": [ { "_type": "UMLGeneralization", "_id": "AAAAAAF5YExpLhrrLFw=", "_parent": { "$ref": "AAAAAAF5YEwzMhqAJ4Y=" }, "source": { "$ref": "AAAAAAF5YEwzMhqAJ4Y=" }, "target": { "$ref": "AAAAAAF5YErP9Ri2oM4=" } } ] } ] } ] }nxtomomill-v2.0.1/doc/development/design/hdf5_converter.rst000066400000000000000000000037031511430602400241100ustar00rootroot00000000000000HDF5Converter ''''''''''''' The more it goes the more use cases the hdf5 converter has to handle. Today it handles "classical" tomography acquisition, zseries and multi-tomo. Behavior of the HDF5Converter ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ The HDF5 is applying sequentially the following taks: 1. preprocess raw data (During converter creation) browse raw data and collect information on Bliss scan sequence. During this step it will determine for each entry if the frame type are dark, flats, projections or alignment. Create an instance of Acquistion for each 'init' Bliss scan. 2. HDF5Converter.convert function 2.1 create for Acquisition instance one or several instances of :class:`NXtomo`. 2.2 Optional split of NXtomo (use case of multi-tomo) in order to obtain the final NXtomo. 2.3 save data to disk. Acquistion classes ^^^^^^^^^^^^^^^^^^ To manage all the use cases it has evolved. Each acquisition has now it's own acquisition class. All based on the :class:`nxtomomill.converter.hdf5.acquisition.baseacquisition.BaseAcquisition` - :class:`.StandardAcquisition` default acquisition. - :class:`.ZSeriesBaseAcquisition` a serie of default acquisition with a set of z values. Create one NXTomo per different z The regular conversion sequence is: .. image:: img/hdf5_sequence_diagram.png :width: 600 px :align: center Actually we also have two configuration handler classes. Both inherit from :class:`nxtomomill.io.confighandler.BaseHDF5ConfigHandler`. The configuration handler is used to make the connection between applications options and configuration (:class:`nxtomomill.io.config.TomoHDF5Config`) - :class:`.TomoHDF5ConfigHandler`: used by *h52nx* application The configuration is used to make the conversion (defines input file, output file, entry titles, urls to be converted, source format...) Actually there is also two configuration classes. - :class:`.TomoHDF5Config`: define the configuration to generate a usual "NXTomo" nxtomomill-v2.0.1/doc/development/design/hdf5_converter_seq.txt000066400000000000000000000013371511430602400247700ustar00rootroot00000000000000# done from https://sequencediagram.org/ title bliss hdf5 to nexus hdf5 converter user->ConfigHandler: provide configuration file\n and command options ConfigHandler->ConfigHandler: check options\nvalidity ConfigHandler->Configuration: generate Configuration->user:return user->converter: request conversion from a configuration converter->converter: parse input files / entries converter->converter: create `BaseAcquisition` instances\n(from titles or from urls) converter->NXtomo: generate NXtomo instances from `BaseAcquisition` instances NXtomo->converter:return converter->NXtomo: Optional: apply modification to NXtomo instances from plugins NXtomo->converter:return converter->NXtomo: write NXtomo to disk NXtomo->NXtomo: savenxtomomill-v2.0.1/doc/development/design/img/000077500000000000000000000000001511430602400212125ustar00rootroot00000000000000nxtomomill-v2.0.1/doc/development/design/img/hdf5_sequence_diagram.png000066400000000000000000001717071511430602400261370ustar00rootroot00000000000000PNG  IHDR]5 IDATxy\w-hG}} Xp!`С.wAAAN"""*F""bҢF¨QpMDHiF璜lҴ:tBi/,Ǐ!^èQRN]>},:$׫W/!}v[O =']˗_~yٳgrgA>H}5FT_ڵ{?MtYvEBDDbt!"r.ȑ#!eޗѥr~RwOct-F""2 D"|嗈;WĉzgE>̙3 JWWW/.5ذaԨQx]uLy}7ߠm۶􄷷7vGA߾}QZ5xxx 22˖-]:N%K||lРl܇bĉxOOO4oSNǏMz޴4!йsg<{ SNE ō7 ʕ+3f ֭ 777x{{]vXbdB{:Ą޽{K_|Y<˃z;6lOOOxyy!$${덞G%ÇѧOT^nnn5k x7WWW?Ǐu(8-֊.>? >>>pssCXXQ,ɓqm5 P* CRR>}*st/\Bܶw^ ЧO޽[cYv-ڶm+رcKgÆ 2ȗt~~~prrb!"z191cƠJ*ܹ3 MBwww/:3%HNNFrr26o,.Æ CΝѩS' :ׯ7yM6I[Iϗ,@ѰaC!닾}w͛ݻF?~йsgUVܹ3||| @xx8>|s;v BwވxWe?.egԩZjMٳ7l/2 _|^^^2zi~ pqqAZЭ[7s%Etwui +~m۶E֭eHNNƀ / -s̑W]z쉞={Cüyp=_WI/^Dݺu!vXnnnnܹl|sCMt5j닎;3\\\۷CުU+!uV?\z7oUVprrBŋuK(-w:uBZpU$''^zR-9`0%&&ԩ4im۶]ct!"r^^^طoɓ'K?~wmDoBtQ҅ '''ԨQCgΜAvvɯZjHMM?{ }ÇPB`ܹ~gܽhĈF^Ҹqc\p{Q>}}i;w 29Kn]Ѽys\xQ-''Ga=^UBP %%ERHIIܹs򂳳3 x9 +XDѣGC?P>{[Eivrrœ9sd m={/2ۊШQ#!j*mD @aԙyN8!I[FJn!V*tR!ЪU+[o]Ǝ_r 򐕕ip]g\sKLL[R1s[QQ222tyfgϞţGm۶I[|ѣF 6@u!.~ !!B̞=[6>}Ə!}f͚A-[} ӧOﷲ rfK"".DDBf%iv6ll)%==]dٲes˃/d4PPpwwBCy͎.ܺu z)MYeٲeBSNC{ݺuF=&]E]Q߯~EEEJ(::Zq.̍.͚5Ö-[7ǰcDGGC^y>EEE'FCѱcG[|FɓP]齏={dϞ= !wgϗ 00B:uJ~ ik㗔!t9wVVt1YΖ,Yb𹉈BD 4? m]1ץKpSCPH׭[Æ ֭[u~8? ZF~ba~v#*4?d;sţ P<ѣ)+L8B1ch<:c S] ̄M4 wvvֻ{]-D ;s˓'Oн{wi\\\{Ϥ@?!C_M6I4w1-|,]`׮]6҄_sC<4i,^B3F綶m-^bӜrۘr^?eEs?L{W.""z19cK׮]eM.p%̝;111R⏳t}\ÇGqk׮˗/J;eti?X+^Z")+h~kCslSR5EB)|y͛B 00Pcij(S_prr29fNtKzc݋?vR_^4e7w9,OtRo777(J$]xB(JQSRy `gхq19Mt裏tn+**rJm.0{l!믗9 7?ӟgر.Ϟ=Ph?!Zl0ҹɓfH9&KBBQϣQXXN:Ac׿B?yJ% SaO:ѻwo!W_59ܹsx7}Niήc9E{igW_IEcNunrX (>s}i i/_^)ɘ9`t!"r\.DDB#[ x6S˿oݻW*14N 8r6nܨ͟}֭IԨQB] v-|flڴIg^;w]vrѯݻC ;wp=kBwhSŋu&M<ݻW'lh~xjJ:GSS"饗^b^ɭ7l"m-]:-|.M̉.àAdWqL.qmKQ7ڀnƍi-駟dY]v)EIslݺU_]oOb:1Es3}gl09.F""0fTR]tABB^x! 0-h~ةjCELL j֬ !6l!C/M~}.@7J%@BB^z%( O=n8Q|`ߠ lAAAqJd4V D{#F?nΖJBLL  ___!мys[bntˢ9P5K/!>>:tF||4? BUjĉ"ݶg@PDGGK`ҤIzK!@zCG09L]^`Q|zW_}ڴi#cJtٲeVT*"""P^=NNNXΝChh((>T^0h k:ˇ9ayKaa4?Y& SJW G^6mHŋe1&Җ>={71B]fg`t!"rd.DDB󃡨-Bfjժ_~8v]=>۷G`` J%? ͓ӧG ;|||дiS7W^5b(Ż3pwwGͱxb]ߏ1c 22pqq5kqƙKI7oĐ!C >0qD4mpwwGf}Wn߾!7}/Ron۽{7:u///xzz36olE[loԨQJkFnݰi&w]믿(Jaxڷoortq>#ǻ+~g}xzzaaa4hvܩ3ay $%%I/5>|Æ CXXzIgP>1V\-[JO 61BD]х ]х ]х ]х ]х ]х ]х ]х ]х ]х ]х ]`tYr%&NĉHKKl;/\._l2fi>/_}|\ ""6k7] E$''[|Ɠ}΋)sm=ͼŜ aCDDm.bO;/\]xԅq/.DDT\be'}^XsƓ}NeIKK+׏)49.O%p}_]lѥNeat!Kbtq\%p}_]lѥNeat!Kbtq\%p}_]lѥNeat!Kbtq\%p}_]lѥNeat!Kbtq\%p}_]lѥNeat!Kbtq\%p}_]lѥNeat!Kbtq\%p}_]lѥNeat!Kbtq\%p}_]lѥNeat!Kbtq\%p}_]lѥNeat!Kbtq\%p}_]lѥNeat!Kbtq\%p}_]lѥNeat!Kbtq\%p}_]lѥNeat!Kbtq\%p}_]lѥxޗ7"<< áC m=ifΆ{\|B:tfT٣ˇ~!0o<̚5 uԱd-$$fͲdH]Y'""}btFիW1zhԩRRRPPP`x <<<0k,ܽ{=˗1tPܻwϤ;v,VN5\&F۷111PP*ǀk.<~I{쁓كɓ'ؾ};&L`*_~:Ǎ۷`ct,C-0%Xr}?vX!s[HHEEEFDD>}*oܸq۷׺]ٳrݟѥ򞑑___4h+Vq̟?ճx8p8qDѣ].] Bhlڴ G/$ԪU܏mٲe^X|?/0Xfٰ^ х, B믲JF5jޓܹ ۶m=/s빏.QQQ;vlڵk*]~:bbbWWWO?n >CԮ]nnnhԨ-Z"iXa„  D*Ugt3TZ РA4mO<ѹɓ'xSLAhh(\\\3f,Drr2^z͛ŋK˩K݋ѪU+(JԩS_}4hɓ'K]~GDEEAR [}3k֬ jӦ mVft1;ARaҤIxQzum;W6{..] Fv޽{>|8T*J%Zh-[WT:u* J[x˗E˗/GXX>n߾]>jeSM&]7]^27bi\x1݋,1o^˓'O0(J_n7{t%899luM( |wҴ6l'F"" KG  ..-[ lݺ ;v;w~[kUBR?Gaa!>3Zj5j.c.1W.={D׮]q ܸq{Ś5kcccѢE ҥKؼy3`8...r.B믿.siӦ˗/da޼y8P*,}1^!rrr7::عs'Μ9wy QT֭[QXX3g@RŋRd}L~ NNN:u*uVԯ_ܘe]捹% 0j֭ŋ~|2{ʘ!CȞ{V)z^ Y5˥KիWK.0fk׮xtlhkX* 3gɓ'Bo=ou1RDƍO>{cǎA/ʆϝ; 6"<<fѥ2|r!pR+**2(qMY~=D3g4NVVtKi[Δ{Qdɘݻ7t"/(5N IDATR0lذRX]PF2;|0駟d۴i#^* cƌ3rH[nLtٳ'z)gŊfGc˵9c%獹!h42f>Z ѨQ#ʿ{`t!""˰FtAAAx1%//P(ֽ I&hڴl!CHOw*EtYl\\\ТE HMMfe tvv#66}-j0TZSRR.׮]jnݺMKJMMB4& 6L'l@͚5u?=s E[nFdd$V&5f0i$8N*5R0w\7 ] bTtYf  ή0&L@* -֭[K׍.2elgϚ]}],9oeeҥprr2ŞG )))@Ǐ(z^n х,ZP QJ/й]ߺהϟ/:!f„ ]b>tI'ZJ]SRR0dxyyW^(**—_~ BG >/"7D-ʻ{`t!""˰Vtׯ./_&Msq{7V1bٰD c.1s]1x`ٰDRҎ; kא!6lP0,y I&Hה݋,]LٽȘr]!?K",,һwo 4H6ƍK.~'8bttk]s 6[*{fYp4Nnn.ˌ..sÇG^t拱:oeŒ34Ө^:.\(V Y5Kaa!7oRPPۣ]v(((@QQvH%{7VYхaŹs+T*,=?#._ .`ȑPTU^)))8<?Lat!sYry?x |||аaCĂ d>}:<<}n–-[Pvm+,*gb#.GeX'MZjAT"((ݏt.eݻ)iݺu(;x3qU:YF]*.TF$F:YF]*.TF$F:YF]*.TF$F:YF]*.TF$F:YF]*.TF$F:YF]*.TF$F:YF]*.TF$F:YF]*.TF$F:YF]*.TF$F:YF]*.TF$F:YF]*.TF$F:Y˦;vFʃ;/e]""",]"""lZx׷хWyat!""K6oD^]*.8r)OtᅗD^BDDm.]Ǝ(^U`Oʕ+->ɾno BT|:rQ1[O\6lWWWO\Y8UFƜ """u}߲eK!о}{O9k7]A1a[OQvZz2Z jղdYnj֬i """ 8|0'n0^.d .хq0bt!"]]H `tBDz1506F"""袋хbt!k`t!m.DDDE BB].F"ҋхх19F].D Y ict!""r.]H/FFBDD8]t1^.d .хq0BDDDDDDDbt!"""""""F""""""""+`t!"""""""F""""""""+2~x,Y֓AT)ܹqqq r0;vm=dGvڅX[OY\[OݰSFOMSF62qѺ]H/FFBDD8]t1^.d .хq0bt!"]]H `tBDz1506F"""袋хbt!k`t!m.DDDE BB].F"ҋхх19F].D Y ict!""r.]H/FFBDD8]tut?~<,Yb v܉8[O9;v` #vBll',󈊊Bnn'nut!"""""""z^1Y 0Y]GǏ#++֓a!!!5kV5 ;w)7"<< áC m=iF1}~ 78z(z#FH :zі}q ==֓Av!""rHKKd.|qaE'Ofݻw#\|CŽ{l=y2F~tXɓQNdee(((yb i[F2&m|8T*J%Zh-[ȦyZ68ڽh߾}x饗 ???/^4\bt!k`t!m.DDDEW.HJJB~~>=z~e˖RhXp!Tݻ͛(((Ǐ1m4xxx`8<,Y777̛7#xyy!%%E0`t]}ΝѠAٳ2dee…úu됕{UV-0j֭ŋ~s닭[gΜJ‚ q233!ɓ'at{.|}}1p@9sBiVtqqq|\梨| 6l؀3gٳ8q"q)龆t~_;w̙3xwP(pa>:]YtȀ;&OL:u  Annng*FFBDD8]tUi؍7䄝;w(B\rE>>>OdJ%]6l^|E[PJ?q!+SIѥ*J'9rB}^{ S_J˜1cd9{]fΜ ???8YYYprr2+Pk+ChLt̻~I6N6md3o1'C ݧիW_]хх19F]6;NZܹsGUnvHMM ߶mu_~BcƌG~~t?W^ gggx'ER ]J.] '''פRh"8IIIhݺtݘ2l0tEg:j֬iVt۷cݺu o6"##Z 777$$$H]֬YBЄ P~}1FsKPPe`ʔ)>]]H `tUiKntWVM]jWB]vɆkEvv6G4i{Ov?WZ777v/:{l&MH%??ժUèQLzܽ݋j5-[&{6mڔ]޽ !~giXaa!d>\N^T2Tdtݻ74i"jZ]]H `tUi7pi8p-Z@dd@OXAgʕ+prrBddlԩΙ3UTHwP(HJJ‰'p9|wfׯj5֯_,:ts15Y*hH7##/"<<DCGzz:.]4=>]]H `tUiK||<&N(Gz49rHH^YsB`…:iPjUԮ]׿2zÆ hݺ5͛#99כD=zIR(>)k׮ٳgGQzuСCڽ_Edd$j5Ñ8ٸwAtt4VjSFWTtiϞ=777ԩSt:rKat!k`t!m.DDDEW.drrrP(zRFFBDD8]tut9~8,.rJ߿8p'OzҨ۷֓A&''鶞 #ow EZZ'îutFۚ4ijժR v?"""""""r2YKذaTbx|w ["""""""c0T{ϣo˖-ڧ'""""""2]Gk"55֓Qn.汇R?~s̱d9vΝk ;r ̞=֓ADDDp-$''#//֓b7:X̝;򂟟zꅳgQT:u* J[x4NAA&L???xzz".. .4*YpuuEѭ[7bʔ) 0c I x̙ '^y<~7oFF.]ƍ.\Z۷qpssßg?NB&M`Ϟ=kN:pssCڵoNvI!qFtUVEXX.]*{η{aPTP*hѢl٢%''Y&U1cHڵk!]4Z޽aHNNFpp0|||ЫW/ܼyS눉\]]O?Zxh2DDD͚5 ?#p ,,LVT*|}}uV̙3PTX`4g}ooo|W̙3[f\\\0uT9s'N p-i˗Xd d 8|0n ___DGG#** {Ezz:7n~ .^|E/8x "##֭[oʼn'/#00P:g`߾}|2vڅMbРAckKfp)aƍprr±c=ߢ;w̙3xwP(pap%ԫW4-]Rɓ]vy޳gOt'N7w^YZ]]H `tb@^^J%~'iJ˜1cd9{PDժU1e8/r񠠠ժUøq^TT|'቉PTX@6f( HRRR-]_p!ʒmڴ BlڴIvE!aϨR ={Rq^z7{mڴJW_}U6T* bдiS+97n8хх19F]._FFѤIԬYj ˗/QTXh~IIIhݺ5իB`ݲq-ZTj<8<رcۯ]!mm!5Lll,  gԩ{Ǐ(.ժUo>C~Ai&tuօZ~r)oǏ`|[f  l.0aׯ/]WT5klL!pESއ88yl2Ex*E0506F"""袋͛7ѣG###׮]Cvv6VŋKT*u8lp!~8˖-+5;wBܹS(k.p͏lӥiӦN:a۷oBCsL!ץayyyBHLT*1|:u 7oޔBff&?F-{̝oW6]4h ]Ǐ]>hf]JJ ///ˮ Y ict!""r.]l޼ ҰK.AaRt&_y7+l.o:t ߊ+L.ηv/T*C6΢Ed^:aEDv!]fV4FFBDD8]t1x''')&t*U]`ĉ Ņ wFժU< yd=}4 iӧ+V(].\J+W(>gXXѥ|@@eqe\p#GJc]]H `t} EPP7nkBV]CR!((]vԩS:etJJ 6m T^: s.ɚ%??o&j֬`o^:f)g̘W_}^^^ѣ0i? Y ict!""r.]* MtiѢ~2gt!k`t!m.DDDE Q%qlڴ EEEFBB].F"ҋхх19F].D Y ict!""r.]H/FFBDD8]t1^.d .хq0bt!"]]H `tBDz1506F"""ˮڵkj ?9sz2;v sεd9qfϞm """ ugIv]W.DDDDDDDDVBDDDDDDDd.DDDDDDDDV`ʲdUJ999طo'LNNm=dGn߾"""4[O]SFOMSF62qѺ]H/FFBDD8]t1^.d .хq0bt!"]]H `tBDz1506F"""袋хbt!k`t!m.DDDE BB].F"VɓB 33ϥRxb?)^zz2х19F].Du-[ >>>zϛ7ST1]H `tBDz1X]H `tBDzY3k׮'Zj={#l߾ݻw;°tRc\ڱcy}'.҉Аy橴TEEE7o&L[o֛o2mܸ'_HXn[#G'?Zh{!ݸ8 4H6lؠTu֯_/0zj߿_d. ӓO>m۶iʕm(<Ѷm۴k.͞=[aaa*,,j|Vk"*###Tiii>_Sх) ֭u]-Ze˖ٳ֯_/rQM4I;wVdd\.飷~۳Ç5n8kNj۶ƌoD]p- 3 At"|Y КݻWahݺu;w0 mܸ1c.0#D+ [g[t;w״vѥR#FΝ;%I۷oa?q7uD]p-]̈.Gt X] Dt)//WXXrss޽[z$I+WTZZ\.Zl={꣏>z?-\P;vTttzꥏ>Eva^W_}f͚iʕ^W_)<<\~aퟦ3 At"#]4l0u?;<;vLh"h֭ڶm&Nmٲ󘆢_Wiĉڱc}]uСk:Wn6u<ڷoF+MfDb%33Ss 0&i4hP? ԢE :tȳK.zG}\>}3xn7] }z=ǟӊ.+VPDD#IQRR}Sy됴j* <8@UFFFAiiTUU졄.W˖-5oܳMC%%%E&Mz-[Vt9~:u,W;Dg=ܣx@^S^^˵w^OC lPtСy筋*]$iԩJNNVmm 3 eeey;p YW[[S. w/7|W^Ovh֬Y ך5k~^tDAѩS'CQQQ:pg}mmZnL?~\TDD)E/z_K/7;vL͛7פITQQ5׈RΝu) (}Ya{'|=zvcǎ7n tJE:qޔnu] .7HաCEDDv{=_AA ԩSep.L O>-YD111 PZL 392ڊ>3]ve=ztsV#s].l]~0zhEDDO>o=fDbEt`@ s].l]D]plEEE0&{;fDbEt`+;;[[V^^^Y.Gt X]j]3 At"EtA ]`Ft9.VD. ̈.8Ŋ@t Xtt֚5k= I***Ӄ=3x7o֌3= bM6-`Ϟ=0a>졄.p&]4&  DtCtИ..SHG. E p!]q!] Zttah x20ch).l]D]pS.ѝwQ%.0#D+ [DҵkWݻ%.0#D+ [Esl[.hfDbEt`.-D3 At"urti(]/ ̈.8ŊVvv?|bKҥKM8].Gt X]ΖRJJ_0 umCu!fDbEt`v9O/B]`Ft9.VD.D43 At  _. b_. Bt]4Z} @ttԜ9s= IZz aaVZ{!0@#(--UZZ=х)1O 4SFLmEt`@ s].l]D]p- 3 At"EtA ]`Ft9.VD. ̈.8Ŋ@t X]" .0#D+ [DfDbEt`@ s]B:dffjΜ9$^Z 00V= \edd{*--MUUUJp" .ѥHeee$UTT(???ÀTTT @ٷoUUU 0BJHG)L 392ڊ%?1htD]p-K;z]T]p-;vL>\.bcc5h ͜9S͚5.??__bcctرsVVn,Y.](>>^kN9rDO?ڷouE3g=۸\.M4I~tWKf̘/\-[Tbbnfm۶N:0 D7讻R˖-ջwo6  ̈.8Ŋ]ys9Zx˕RXX͛gUIIl٢Ç+99YUUUND 8Pرcնm[>|\JMM՚5kT^^K*11Q/gxb;vL$M:U+WTYY6mڤ*%%t;Cھ}M(mڴ) ]`Ft9.VDΦR[[8M2kw]5l0mjjj~[҉bf2 C͛70 #dG\r%.K#GlpVTT֮]Yg]^|E%''k-ܢ7 N( s].lM/aZ~ٳg{E$i;uɓ%.^S]]-0|rIܹs}>ODDq.K3f̰PC QnԦMnW_9 :4ޙEt X]:7f.mڴѸq}k.|NĜp}>ҬY޽[qqq3f _k޽.qիW.0#D+ [gSt[n\DBze].]p10 mSRR4uTN6M/}PFt X]:Åty |ƍt\yyy3fgv tB 7oJKKUTTyi„ mKQQ<K}Ufͼ۷ ]vR555:x:vK/T˗/Wyy6lؠɓ'{I.0#D+ [g[t2:!!3e)SԲeK6nܨUVQNtRѣ4i:wH\.+~EI:tPRRvlnm7lؠ=z(&&k{{U6m${..0#D.EEE*++ 0&BƏ*555p 6(''2/*((p6ٷoYo 8JyyyFH ڱc^{5/+(͙3'Cse˖0 u ]8BYYzꥸ8hBݻw=,ǫ.u p_jjj=D h.+-:ukF|@%;;[k֬ 0&H?XX|.SL7˧~3fR\\iӦ{ٳG&LÇ=х)Vtt_?YtJwEIIIz#0e4VDնm`!ӋN:i?.`!s].l]2|5],X@t At"Et?|E.]護޲\@3 At"Et?ѥ.nOt X]"uѥR3 At"Et?6lؠcK ̈.8Ŋ@t X]" .0#D+ [DfDbEt`@ s]B:.@t @] Ef/^dEt`@ s].l]D]p- 3 At"EtA ]`Ft9.VD. ̈.8Ŋ@t X]" .0#D+ [DfDbEt`@ s].l]D]pUHGlY&e{( ***Ӄ= 8͛5cƌ`!XӦM 0@#سg&LÇ{(!# p8z_5vtug8 .K&M߮]}Ւ|]UbbӵcIҮ]dv%I&LPݽ^nIg}&0kF-[Ըq㔕%ۭ%KK.Wjj]rŋرc:t ռys=*))і-[4|p%''J#]N%\x*,,u!eee)""BTee5vXmۖ .ip\9r׺t 6k]MMoKj`dY{nF-KQQʂ= rY.d9}n<~bx屰w\.W婧*x!D.EEE*++\ ӋRQQ`SQQ`!d߾}[CTUU)///)!]4_. uѥBt଍.uBY]ŗS{(]2335gΜ`hV^A{pUViBHnn222= JKK`%dttׯ|`hn = 92ڊ@t X]" .0#D+ [DfDbEt`@ s].l]D]p- 3 At"EtA ]`Ft9.VD. ̈.8Ŋ@t XttԜ9s= IZz aaVZ{!0@#(--UZZ=lEt @]!]2gch). h.8Ŋ^bccSNj֬n3NxbuQ[qF]v{h~INNԩS= aDbEtLʏ[NG>#u5o\SNՁߟu#GE^nΝ1b`ϋ\+VˆLDbEtA;v,C8-M)=z4C}vz q]{SCÈ.8ŊFA'T۶mլY3>|XGO?+&&F]t̙3uqcۧ hB[SO=QFnlGy5o]w%˥-[w1=JJJRTTnnfI}'0Yf|ϻwպukEGG /ھX/ԢE ꗿJJJ<g2 C/V>}l3b?ѣu^+;;[\sZlqջ?$Ӌ}]]~劊R֭5j(}wvduEJMMU~~}9k,˾9SNJ+ԵkWĨw*--Ֆ-[ԧOhBݺuӺu|MvءnMm۶ULL.bM>]>ȑ#?~RRR?_={rT}^rsiСr\:{ݾ;y9W_}U)))V^b  вe˼p}_~۾W\4gϞ裏ߘ7}DbEtAPddz)UUUJǏWFFRSSfkҥJLL/y 7ܠnݺi*))ȑ#wSJJj6mi&IҔ)SԮ];}'ڽ{6oެӧ{ AuAW^yrssUVVUV);;3;OӦMTXX믿^IIIfO~mٲEǏŋ͛7Kdoy#GsK/{ /P:~:0G|)33S%%%ZbYVV"""4p@UVVZcǎU۶muaDa>5n_ѣz衞={jٲe*..߮mȑ#>Ƕi&Kڴio*>>^gϮ["vmrz7c 7t:vիWk֭op}駞m\.9}ᇪ֭[r.k׮UXX{9?E]t}ѢE֭[m6M8Qڲe籾ѥ B X]h222ԱcGyf;vxm;c ]r%B 6xVrr)G_|Qɖn7$=JKKye銍UEE׿-Zx~wy^xM&I:~:v-ZHUVV^k^4?W~ymSwDݩ-YYY2 Cl{nBב.q8<ۼ;}cǎfe '?׮]뵾W^8p҃>ͨQ4`mKտ?iG߻/}3/ӟ]ZuGihKǎ5~x+0[oI:;aZ|}V_t1q{^[BL۱c /wJNNV||ڵk1螅 *<.sri̙^ی?^={'tA'Ozm۶vt1KҞ={4vXCIIIr݊=]s ]p&##CC Z7{l; ͂ lsKtY`Wt;ԫWZUUKꡇRvԡCQF;S\sk\uUـyNDs/TXXrssw^5kL~޷ג蒒uԍCɑõTNV]|!]>qXsrc .лᆱRݻW&M<̙p_.bmeuWxn]ywrt D#GZ]RSSuu)//Oڻw絭?ѥ1 B X]h~՝:T#؎ 2DCzq㼢˴i/q:tHQQQZp r*++KE: Svs?﻾r28Ӌ]Nw].2Zww]~Eۍ]=vkܹ15]8 0{*%%k[_c^ts]B:dggk͚5|gdd(!!ASii4o\.rAuQ^z/_rmذA'Oo-Iz畓m۶i׮]={<D=zVǎ5n8 4k[_c;e4%sٳG|ڷo;3Â,[3{ZNk_@EtA"ou:wqo99_@Sх pv.. sp!].L ٥b_2`h._ڵk뮻 /}ѵ^q8k{O> F˯ggUHGnA^{&LrnMqqqAt},[> M* m{p  .0@^@Et"EtA ]T/Kt9.VD.DbEt`@ 8ѥRs].l]D[lvzcK At"EtA ]{{ At"EtA ]`Ft9.VD. ̈.8* يD  B:dffjΜ9$^Z 00V= \edd{*--MUUUJ”@0e4)aƔ8SF[]" .0#D+ [DfDbEt`@ s].l]D]p- 3 At"EtA ]`Ft9.VD. ̈.8Ŋ@t X]" .0#D.3gN4IW֠A= 8̪U4x`!$77WF1bn`/EEEꪫVZIbcc&99YSN CUZZ4UUU{(!# @S2w\ :Tz_p 4h***tAIѣLFO?T:tP@_T\DSp`!`N%($%%_w3]ƏuGNGtg0 kꪫW^nƌղeK%&&oֶmۼq\4inv%$$ꫯtK.D:su5x=6??__bcctرqOEʒ֒%KԥK+55h߫aZl׺Oo0YfYXRRb䥾Ӌ9~Z۷WLLt颙3gmگ\r%z|k\.=s:t\.;<=ÞR>=;hd;ukU\\bccխ[7-]}=D8u!SN*))tZU͛7nԩZrʴi& 8P))):|g˥X-^XǎӡCC͚5ӛooF۶mSvvo.I*,,TϪD[lÕ\yv%""BTee5vXm3FkCE:#]n5tM j͚5*//ҥK9b8p6 t>sV[n>Hibbx i+=6:vZj'xBeeeڹs/_z 4tt)**RYYY4I^%h *((0BkBğgF5\qVTT֮]Yr4rH/^83kذa^jjj~@7 C۽{ Paa)`G͛70 >3f%\"o߾>絥>ͨQN[/'gZSS#˥yymi& ߿_ax}lQUU`#ttah x20 Ԕu!bƍ^g͚xB 2DݺuS6mvW_}ճҌ3O~jJ~?iϞ=|~3yduTɪe/_~J5eܹ>IDD/_~Puymg=sLǎ?^={ܮ/'g}zOZ~$iȑԍ7ިɓ'}).l]D:y饗}{I:q0O׿}Yf FK꥗^͛{n^ZK6nܨh@Ә1c<cK}UZ}jjj4sLEFFzEɓ']vںu*++.(lvMHHмyTZZ"͛7O&Lkի^|Eڒ>'H/p?^/{iN SO)??_|6mڤ޽{[ni=@].l]D:k .rٳgCJJJR׮]-`tYvҔhuI'NTmmg7jժbbbԩS'9]zJOOWBBڵk{L#F.馛s:]=I&sΊR>}<goQxx+%}otS111S=g׮][(1bDD+ [Df.UM齚^Ŋ@tkJlZjU?D+ [DfD)W@h#X]" .0 TtgŊ@t X]" .0{7ԪU+M8I-;w ]B:, {p {!d0]@LB:8 p]h:.gp]h:B:dggk͚5$if͚1cFR\\iӦ{gT{ф t`%dttah x20{7ԦM`").l]D]p- 3 At"EtA ]`Ft9.VD. ̈.8Ŋ IDAT@t X]" .0#D+ [Df]jkkpDGt8Ŋ@tѥVotݻTD+ [Df Eca2 @"X]" .0]b Ft b m. jjj-Dp!㏕`l[:w]h. jjj],.R 4]@H+# Ef/E  ً.l]D2Z_.&- Ks|!.VD. .u˝wQAt"EtA ]`v*6- 3 At"EtA ]`Ft9.VD. ̈.8Ŋ@t X]" .0#D+ [DfDb%;;[k֬ 0&HӧO007o֌3= bM6-`Ϟ=0a>졄.g+ @]!]TVVaMREE= 8LEE = }o QUU`#ttah x20ch).l]D]p- 3 At"EtA ]`Ft9.VD. ̈.8Ŋ@t?sG}a Xj^y`N Ŋ@t?}?|P:bcc~Ĉ.99YSN1Cu!]qz@t"EtA ]`2w\jժQ."ٳk#<"0kl.K=*˥;O? ~ӋvduEJMMU~~gcǎGURRv=Qןﱠ@7xvWjݺu;lsD+ [Df.TtW*77WeeeZj%].B:t ռys=*))і-[4|p%''Ji&Kڴio*>>^g#7xCeeeZ~Jedd{5GcǎiѢE Yh"h֭ڶm&NmٲVZ'PYYvܩ˗{⒑TYFZt/{iJKKts-tA }>r4h }ڰaz-Zҿx*..7|k…ב.rM7cǎZznݪ ק~rsч~ZmݺU.˲N]tUYYj;Vm۶Ç%ISLQv'hڼyOy_߆CҰaôqF_WIﰡ@p]B:, *4 {! ȿ5ӧOWll***l﯋. ,ZaÆyQBB~m?A?<~ri޼y^7m$0|!utVtt_ӧyIe֮]k͛e4c ]r%۵j߾׾K~t7L6M.K< K׮]=D}׫W/ 8srfԨQ0`4.{޽[aP(--st٩^twH=WҕW^i9B? }JyyyFH p:.ueӦM^듒,V-'Ot∐^xA{Vrrnծ];hݾ}6 Cׯ?j˦M+E^SٳGcǎU=$ۭ >ܳȑ#oQ'OVqq{5v{ֽ ڵvC=ܣ}Zֻ]ΝH'К5k~NtYp-<䓺袋<].ǏWϞ=wKbb62 C˗/tH˥d5J999^Wt;{l۶~iɟÆ>GD8wyEmڴ{L\p}]j޽4ing۶ma>㍯k=wjj:婼\{U~7#IŚ:un&5kL/$i )v/0ԬY3.0<496wy^%,,]FYotN1o< 6L-[7~:_].bmuD+w|]dukTUUiҥz衇Ԯ];ustwȟxwߡTӋe֭}>e]f`wC_Яֹ{JҲeK?~\a=*%%]NOcǎNarrr|nsN뭷g}x㍺;%5En[szl^.'[j _-1u=Rt9١C&?ﱡӋ;43*.q+99Y={Tnnvܩk7ސ;lܸQJOOWAA˕1cxPZZ爏%K(&&eV^{f'zٻ0\EADĻA-oAֶRW򲊮,p)MvQ#/D&%*h\޿?|̉sx?̙9/眏#,XL|w-9=}7? ^'wpܤIĠ 刉xy|2ѫW/.\ӧ`0ݻcҥ Q\\,^܅t%^Htvˊ+l`˖-pppǪ9d6iӦxTy 4j+VPGDD@ӡuֈWL]TT#GAAA5k&MTct>}:ڶm ^ 4H2Ð5j#""z0]]H iх䴌.6mB6m$mݺ5\UU޽ɘjb.=lѥrܸq=zt?_VVCҌiu.999pttŋBD]H .$etѣbbbį!d),,Dll,]vؿ?*++tRj ... ʕ+QUU%ާhDtt4VZ^[w^tnnn0`._lvӦMSm𡡡T˗?7F^$E:>>>ݻ7͎$** sA@@pm}K}ŋc̘1OĠAwww<ٳ~)FM6OPRR-ZMn#4b*.GE>}wwwt{c= 8.""[BD]H .$Utq7o` ''' <W\AEEnݺ˗ ۷oGNN%21Ƅ _O?7 H;v 'N@NjDO-iiipV^ NtS]vF|||1qqqyPZZRI.::FQr`;vލwƍ~:6m#==iiix7o-k~!Ξ='q0`6l؀saٲeppp@FFKtw :rss>LOz예.J.DхBrZES$. o|MɺsΕhDHHZ$/2QTT$޶m6xxx8憌.?Jz=?v 0cҺukVc.L cƌMRS3{?ՙ.k׮6uTGݻwK/"NU?L.E>V@&O  8K.U|8 P\\ Et_кukt}pI,]|`޽X~=Μ9+W )) ~~~cbt!!CH.nCDrW^Aee%n޼ &+dggc޼y8~8._tKr VYYXEDD!F"""K'O+~gDpvvq]1dxzz2ڴ]aaa5N]XXSYfpqqA`` F4C ???"44-}Ḻ4SF?^~CFu"""vaǎt-`Ĉ NC1i$ٳ(++qLDDat!"""5bQ"""i&k4`хSFY&-phjhkkYիWl2ɴDD+NBD]H .$ut!""EхT1]Hх~0(1*F 1F%F"RBZ`t!xyyaѢEԒo퇞1(1*F %$$ "RSS58F%F"RBZ`t!G"F""W.J.DхBr.DDDEхT1]Hх~0(1*F 1F%F"RBZ`t!9F"""d%>>#)##k֬0Μ9sk׮0ȆdffbBDDիXp!n߾m .DDDDхBDDD1=:] F""G BDD Y/KZtI%""MGNMd=2)ISFNBD]H .$WrՇSx.DDd]]H iх,.Ř={65j]>iх bt!-0\mѥo7n A  btQbt!"U.F3]JKKtRx{{хȶ1(1*F ɣ˯իWO[]l bt!-0)aƍh޼BDDd]]H iхブ-[[LK6m,^VF""G.J.DхBr Gbb":udQҥ RSS=btQbt!"U.F~MJ~zbtQbt!"U.FSBDDd]l:ѣ\|at!"" 4y|at!"" =L^P,BDDDDDDDF"""""""" ttA\\AHJJJBTTAv;vA6$%%F ""HZ{(6æ &NMZ$6e4=8e bt!-0 `tQbt!"U.Fct!"".J.DхBr.DDDEхT1]Hх~0(1*F 1F%F"RBZ`t!9F"""BD]H .$BDDd?]]H iх] bt!-0 `tQ8k葔(k1vXklHJJ FADDD ''(--PlMG""""""" ]4BDDDDDDD.͵0IEEE8~Av'N0Ȇ|!?àw^F"55ð)6]8e4ph&9NMT IDAT֖Apik0g4m `ݺuٳh޼9+k'MaÆ5zK/aѨuݺnNBD]H .$R?[nW_׮]!6n($v0sLzٳ'J@``MS\\I&!??… |Mݧ-8|0pQΝ;W_}vի?c ....k[nYi_|GGGtjcGbƌuVk|TYYh,Zu}h`{BD]H .$򛪪*ܽ{עu.5`,.GGG,X___\|Ya.ZuVָN])m^dwF߾}鉐;ۺu+:tWWWwFAAb} 裏бcGxzz"<<\q+W`„ hҤ \]]Ѷm[l޼Y˗?7F^b6VlWaa%`0`՘:u*<==a0w4o>>>1cj܏51E .Hn=z4z!~m)((ѣWWWj o;w`hٲ%z=:v숍7*N)**Nŋ/bڴiq߹s ,@HHt:7o^zI?7774i3ψDFF/~*~-?ݻ_@t:KN~PMGӋ*++tRj ... ʕ+%u!>K_c^5m)"""tO8֭=*~xmQ+.J.DхBr*رcqy>}}􁻻$ űcЭ[7FqN%K())A޽1zhq44jK,AVVΝ; & 88Xcll,0x`\ruӱ~z#//v킧'l"޿edjv3f`׮]pqqNE2 }hڴ)t駟F`` n޼ Ѯ];|8{,&NƍcذaΝ;e˖2[FRRΟ?^{ nܹsݻ3gN:'''ڵ /_LOXocccѣGxW۷o~j ݺuCJJ rssqANjsHHy… Xz5t:-57o`fylɾ0 0 XjΞ=CG|7 6Wmѥ h֬$2Z"33/_Ʊc7Grr2w^)ƿtRDFFh/H5j/⫯o-~gAΝq1deeaj*ȑ#r Μ95kֈߗsG/_777l߾999^OXZWjѥXms"11ϟGvv6-Zggg;wNx)t:?OFNN>>|eV]]H iхTtYj믿A.0 ضmg!K.uv#Gbu***>?A_z-wV1Eٳ'F |t~úUVP.k֬k׮M<(5E۷Cũ -Zի%9td=zxf.ٳG^۶mߵ{nvTWӋ駟ۮ\Anv@ppsE۠\t?-&LӹsgtEcS]t:\]]A0jԨZON:7T]̙3A6Xv-:t ~]YY-[bΝm:tchZiii'Oo+//Gpp}G?OT<L]xΝ+yNYZWjѥXm^Էo_v[ѭ[7 5^~  bt!-0܃.'Oƀ7kL.es˱c9**Jr?AfgҥݻwrJ 'utjG'N_|EepwwNS jeԩӧbݻ1E`N' 1pttT2gk춛.g#ׯƌM6I.j.XoMV >@^nӸq>LڶA"[/ ֯_/YgРA3gDDD8ȯrY|G gkxב,oݺ,xxx+WSNO^Ƚ{t.iii0 ӑ( ..] HNNCgk]E^c>mիWꫯ",, ^<-ƀ̟?6Ym?jEхT1]HAF*nKvvPhl:uYf;wncܬYТE ۷999(,,ŋ%oT.'xo>8::/#бcGɧ2eY  <<nnn/.[^x{6M?Z%Դ jEx-Ar̝;O>dck$&&B|,?OEl۶ ǏGƍ1l0TUUa˖-ptt5C prrGGG >GGGeΝeرҿEtٹsgJKKw^+ BVO'Aq]!St),,`k]KuP6_~HMME^^ 1x`6Y͛77dkF%F"RBZ`t!9[:\hgM,yܹ3ޏcI. 'N{),ytt5j7|S]\ Fb4kLqOHHӋ ѥSZjM}\^0uT`gdZr@XEWE5FKD~d(..FEEx!!CH.n] ۅt%^t5.{q\rIIIڵk;MQFXx1?lZr@XRR`DDD %%8tܻn֭ѥK|gɓ'tRBmKAGݻhӦK̙3q… >}: z0Ŷmې l۶ .̛7وN?0bc׮]ӧ%B߿?:wVp ˊ+(Ƶe888N]̽[bܰcǎ/k ѥׇ6WVVI&AUUgg:G'O&L@ZZ.^={ȑ#,{׶BD]H .$ >~b-[G5ILLDDDz=<<<еkW :t(_|`СhҤ Zl1cƨ+iӦpppQ.7oD@@$Z \u/0iӦxTy 4j+Vܾ}sEPP\\\3gaԨQhԨ bbbSFch֬1j(Z0qp|1P?27etpp0Nm Œׇ69raaaG֭1w\DEE9ѣGѯ_?OW6WmJJJi.J6]222ka=pqkLQQN8aa ^STTGGG?'"jh3f̐De #F("55U=Dl:5w}_} qIDFFUVs玵FD ]i4o]葰xbh:FAAED`]ɣ K1R=gj#)F"""""""R-3a%>>#)##C2-!QC8sLӑң'33ׯ.\pƖ={Z]LK~0{lܾ}b .2z8e4iSF\BBpB.\p…-_\v)e]H iхЬY3kTXrzQϞ=ikFF"RBZ`t!9F"""USt g}&YEхT1]HхvEΝ;c޽RBD]H .$BDDdGmb׮]4> bt!-0 Gpp0oߎZgtQbt!"U.Fct!""]wAYY3(1*F 1F%F"RBZ`t!9F"""BD]H .$BDDd?]]H iх]MG""""""" ]4BDDDDDDDF"""""""" ttEDًH 8{EJ.DхBr.DDDEхT1]Hх~0(1*F 1F%F"RBZ`t!9F"""BD]H .$BDDd?]]H iх] bt!-0 `tQbt!"U.Fct!"".J.DхBr.DDDEхT1]HхbݺuYMGx$''[{D Y ;s] իW[{DTwZnCF"z0^ ͰBDDDDDhDTT̙899۸sϟ-[Bףcǎظq#Adee!55UCCC|rkŋc̘1EϞ=qYݻwo߾DHHy IF`޼y(--Eii)`4da޽Æ ğUK];vލwƍbtyq9TUUappp3g}  IFnmgΜ xdݵkעC]&O,Y]#m۶<=l IF$"ҚMG Z{D"?~ ;STT'NX{dC~G шhm[l#n޼YϪEÇFVZ)͛%똢KVV>}`oiM)SF8e4qh"ۤ] kِے۲ .\ VZZ gggF";)]H iх]lZt1m۶!''ضm.\(3` >(..FEEW^yy&&LBdg]]H iх]E2,^mڴ >@\ɓ ^ĒSN!""h׮vءzMF bt!-0\]KAAf̘GEDDDBD]H .$gIt1NAеk4:""" F%F"RBZ`t!<F"""BD]H .$]F"""BD]H .$W=[]l bt!-0\BB0i$899[LKpB.\p…-/ bt!-0)X ҦM׏ .\pbcKn]d]H iх䪟^T۵\xzmEJ6]222ka=pqkLQQN8aa 5.éMBDDD.NMDDD;F"""iBDDD F"""z(KDDBDDDdF"""z|DDD8k葔(k1vXklHJJ FADDD ''(--PlMGNMd=2)IDDDpJ.DхBr.DDDEхT1]Hх~0(1*F 1F%F"RBZ`t!9F"""BD]H .$BDDd?]]H iх] bt!-0 `tQbt!"U.Fct!"".J.DхBr.DDDEɦKLL = GRRR= 3رc= !)))05DFFC6]V.DDDDDDDD`t!""""""" ]4`хSFY&-phDDDSF+1*F Ct4i f*.o[{u־}{,YDZ F oÆ ôiQg͙3gн{wzxyyY{8uApo[~gtQbt!"U.F::tC t:BCC1{lu[ѿuV[![gwukGٳgcK,Ahh(rssQRR [;vhq7#FP.yC_|oҤIѣeиqc\xQ'|GGG|̿>m##** EEE_=:2=z3fhm.J.DхBrZF7ӦMÑ#Gp9ڵ mڴA֭QTTTŃ:Fmɣܸq0zha ZDF)_ytD~н{w ѤI̝;W\_-ݻڵkW+E~zᅬ'xŠAO?Y/M͛7+4sO.44T1gc+g.oqU{ PUU[n{._>&}n۷O<t:4iӧ5n=Ò}RPPѣGhժz-5?G֮]'x7 lx -[q`0iӦ5={?cpuuEpp0^}UܸqâmRlӌ3pYx7[m~~t zzBNNΝ;} ;wѣGk܇EAKkڴixpfP˗/oߎAK>%`4dܾ}%%%ݻSX~=ӑ]v[lQ.=z3> sb{j>bc] ЩS'L2EZt={Ǐ;rrr$71sppp@LL ~R>:t(L\|ǎ/~sƁt=!!!}`7>STVV0 O<cǎ[nGm۠צжm[ 7nܰh_B㩧—_~SN!,, aaaL3sqYWowF^^bcckQtyq9TUUappp3gNvڅ˗/#;;pB]]H iх䴊.&L@&M~ރ p'((:/_X]7|Sܹsa0įF#t"Yg׮]qz-wV.ӦMßGM߾}%1 પ*oqGӟ$~yfxzzxn4"/ GGGI\۶mq\f <<<$׮]xPzjx{{T\ܹsᾢKEE|||0{lۥF/,.Z?Ԣ%St),,t ?.I}[%GؿEZtmtIYzz۷p!6_~Y1|p?+**`0m6jz k5Ν;%Y/LW:{ سgxŋ!̎˒DJxxx`ժUu&NhQtжm[^{nU .J.DхBrZEǣiӦfsNEtQMk׮`YttA@rrd? j4%Y'99 ֭[}|ʕիOOO?SӋ^W_}aaa ?z=&L ccsω_c{',aaa>9шQFIn[l~{MԩSѧOu/`ʔ)߿b??.9994] KKC>ԨEK[5գ %蒓wwwc̘1﫽>պuk,X@ί AE֭[pHNN"e9ht͚5?}vq7J~nP… fOǎhԘ.HgbɾdǏ+B>3佯>ᅲP]]H iхuzٳ 4HO?A"Ytf:4]C:AfB-o>䠰/3.ׯRSSB r mۆǣq6lS{\r9s&p%S p/T]|I?!E3iմ jEu,Tw T7۷oC|'fe{ZtqppPDɓ'+_:7o(>}H>TQQT,Xpss×_~iv;EхT1]HNr:_H`0Htzw{=QӋj;z8q}G S>R'Oܽ{?xE111iT?-!WcssWF-$QӋ,SLQQ}O/Ot QCK[5;'44/"̝;El,Y먽>1Րuv[SFFxxx ** ~-Nݻ̽>X:q0gdggnmd̙8pqL>AI91\v  S P6lGGG,X[r\m۠bɾ.5E &gϞ~:***qF^HwϞ= :;;'ݻׯǙ3gp$%%ObCatQ8 Y ;s̙ǝnE...hݺ5f͚k׮I3,ZHƳ>~A+M)\ 0tP4i-[Ę1c,:Ȓً90u֘;w.ѥt]vpqqA׮]*}jlYBΒc@}ZK~ɓBCCv,. v...Ő!Cĩv-2d<==%R07e>Ԝ>u.@cHLLDDDz=<<<еkW,\P~m۠Ծ/:ާ]0rH"((f¤IT^j7n,Sr!DFFSvbѢE4qU,\P2C֣Φ уv1Q;{3Y/Lj#jCʲ}vr0tPw}yiʔ)ӧF[ʲ,XBcƌQϞ=5dh 8PH]tѯ~+EQEQ9sFPH*..Vuu|Mi…?IDXB'Oԑ#Gbk_mۦ3ghŊJHHPyyo|IDqB.2>}:v\eiq+}eøk˟W\q,Xж7K8VUUcR$Qiic `"ʼ>RWWBcK,q:Ը:wFk˶m⎏=Z_}8ѨJJJW|]2[F[F t=[hU__u.֭s.?!}G֣>lܱ۷˲,ڵ+v,sD @.ND.0; Onѥx>}TPPJa('''v͸qt]wFPCCN:!Ch̙:}u]!8]" .?5]N8y/W.]1c_]a9RqdƍԧO 6L/3].@Etq"pEt Dص6h'mEtq"pEt Dص4hƌJJJ҈#.d.ND.0sEcKִDDt.k* Ftq"pEt D٣Kuuu].\]`vѥZ?ԩS&c #8]" /_޽{K.-+##ײX,ź_Gp8*:H$R@D"y=|N-q]}*))aX,;hŊ^W|]@t|݋@]o h/.Ύ/D^:1)+7710z HEE,Xkjj4|UNN?(–рw2&e4/_K.1ye + L @p].\]`vDDt.#D' WD@t 8]" .ʼn&]`Gt 8.ND.0; Atq"pEt D]+ L @p].\]`vD^] @t0` a"Ec"݋.\]`vDDt.#D' WD@t^~ex=ڨH:u}PHsр Dt.#?O͘1#%KԫW#FeYzٳGeiժUz;uFcӒ;o3k.Y>a.BtA].\]`vDtdgΜɓ'MDnݺ)555!{tk*11Q_c̙>}h޽cD/أKk]$D' WD@tձcԵkW\2vlʔ),K}Q5\'xBt-[LÆ SrrVZw;Jaa,ˊ[gB+kQ׮]5x`?ב#GuĈz4h =#nEf͚4jݺuԩS:tc۷K***t뢋.RJJoI֭[eYiر֭.rOu}G8p^|ŸjڴiJMMURR233o7kjjsί\R^{Էo_M>]}Yˆ͞=[iiiJIIɓE5554iԵkWeddѥZÇפItfDt.#=[ӟTZIKKS߾}/ISBB֭['I_Ǐ}AGu']󕖖˗Jׯ7 Bf1bf̘W_}U]tѮ]$5]N8L;V O~k}>S~4~xmٲE7o֭ު^eذaz7uV=޽ƍ j۶mz駕p8{ &.Ӛ5kG闿7ޛKii۷kժUu]O>J8qBIJ,}qӒРT߲e,R$ir"IFT4^:IDATt)##CGuw.ѣG/[E] J"O]sY X =nA&Mj7]o;j*Y :}zqǧLltWo=z(77k#8]" .hdYݫ ;ƍuA?5a„ٳuZ]ASkMyvt)++SBB{fӕ$硸EG=z믿>ir'X$)))).4lIҲe˔ XfϞaÆ5ޛ.]v̙wرc,KkeYO.5z.K,Q.]YfXgΜB߿:uꤥK6^Dt.#=khhP^l2M0A=N>޽{kƍ2dHtkItٱcG\Ύ.tkUUUk\Rz4qD]uU:vX5neڴi馛_y]0 7ܠG}Tt&Û|ME!ChܹqKQQk}Dz,K׿/Y[F۷OԽ{wy睱 4vX]}պ[F|?]|] U\\@x+λry=|"R_)))ںu$iҤI>}FK:~i?uzݪ٣'|nݺwD})55U=oK 2D>lܱn/JII=禭ѥۋ{MS6mw^hѢ=9V^-˲'H*33SFV[!gutWΝbʋ/Ν;+---6F˲,YFNpB%&&jΜ9Ν;[oovF{t\ݺu.gΜѷ-}_zժUJHH;;6n8u]с{ ۿǃt[]/[\\۷y&ɓ'խ[7͛7OHDmVg֎;Ao{uԳgf˃>w}W{Ѯ]4}tƾg?H7#G} mʲ,{アc;weY:ujܵ-.4sLO q Srrz#F(''ݢK}}]ϟܹ?ϔK6lؠ#G*99[F.2K/ G[rF$hSNi֬YJMMՀtm駟n6L>]W\qջwo~q;.ٷ׭ު~=LOZ=qDK]Ccti\ėut êz CD"*--z L$QYYcGYO٣˹K4UIIɅ|]2[F[F_MEٻe+ L +4kӦMDD.0˗+))EcX,u7ʲ.g#pEt D-_\*))aX,UTT2~x]l.\]`v^END.0; \tqDt.#_nѥ% WD@t:;4[].\]`vDkϞ=--.ND.0; Atq"pEt D]K8VUUcR$Qiic `"ʼ>RWW"1|"@t0`Kvv/^@fM<10WԩS>vZB!AeeF7|]2[F[FlDt.#D' WD@t 8]" .ʼn&]`Gt 8.ND.0; Atq"pEt D]+ L @p].\]`vDDt.#D'_Gl-^1i͚5}t'OnF s, 6! .GKÇ^zɲ|%ΉM.0; qrСҸڳv]@v!=c޽{\l!b Z[.-k.۷E%(.~ҥ]={4utŋ֬Yɓ'{=f՚:ucG֮]P(`_ will be an entry in a HDF5 file. Almost all datasets of the NXtomo is a copy of the bliss raw data. This allows users to modify those information without affecting the raw data. **Raw data is considered as 'the truth' and should never be modified** by the user or a data analysis software. Nethertheless we cannot copy all the frame (which represent 99.9% of the data size). Today the `h52nx` application will create a `virtual dataset `_ to provide access to the frames (instrument/detector/data field). If you want to edit / modify those frame it is recommended to use `nxtomo project `_ which will help you modify the NXtomo and will prevent as much as possible to edit the raw frames. Please when you access the raw data frame directly with h5py make sure you open then in 'read only' mode to be safer. nxtomomill-v2.0.1/doc/development/img/000077500000000000000000000000001511430602400177415ustar00rootroot00000000000000nxtomomill-v2.0.1/doc/development/img/nxtomomill_design_1.png000066400000000000000000000515241511430602400244310ustar00rootroot00000000000000PNG  IHDR"m:RsBIT|dtEXtSoftwaregnome-screenshot> IDATxy|TםL&{X’ e ; .+.V[VkZ֭UV n(vH’d23}\h{s{>iXfuDDD¥ """"Q(eDDDD2 """"bXFADDDD, """"Q(eDDDD2 """"bXFADDDD, """"Q(eDDDD2 """"bXFADDDD, """"Q(eDDDD2 """"bXFADDDD, """"Q(eDDDD2 """"bGSop?ԛ؈n2.qAd펣MϥYldSoZDDdX V7iXFADDDD, """"Q(eDDKeWzVJ",9'GX<ՍzSsIdVCD "" )$#rγ#E)y)3 撺{E!D+hzgH:IbnLJeϳnv ko~lY0;:KBht<-:IJHHU_R#sL6^FT O,?đIP~>c8]=6"?s0ZdDUeܔl_6n-oQ~fϻX*3p?k c-%kÉM!}} >c'wxZ8Xܳ [36Ýsb"7:0jFDt1<<>00 p m"hep};(-_~ 7NL]KI_Kg/&7 }͞8`w X6ȋ'гo"0ZF'_3̚B3>0bD.&;Kn%wퟘIpL $˶f'dĘ+iӪ9ٴ/>rMNxgnaY{3ID{pmƲkbf)|2@sDD_U ۃ\؉h?ä,}n.zv ~ArO1l$^;v1 :|2Bc$vlY>b'UzM~Guů5Ҳ$Hg\?V:ķas8?gjjMDMqt:O~ "rl\?tf5٥/{:/?nO"oJF ʶaWl {#o݀dv?trn$fw<:73,o c;;`9B~-BmFqr#ϳsN4#r~r|ϖ}9.&Kx?eD]UO^]Z/SʞWdތ=R:Ima~'-6݈lSnѸ˷_[mFյ@."@o͈J?v 0\mGg̽8cG#2okF_QMNaz;Rvm#X&nw)>%GulwõMD,| 48"tQOI= bx֤d[|4g6HD(f>`"s _xu_2VgG^w^J\\KBC՛$m4%6iVY|c|Y|EdL nΎfΘ'4PW1E)bر1K);c&"MJAD|qCp&# ϲ)yKf'bJLG 4HU4j" 0 WGY~'~'+ݳ(6mzLoJ#6~& %+ggw= 0֓ZX܈&<:ys yG8%AMDafHU=RHqMY ÁrVܕ! tkQ>]egAorϤ~U?E?YNO3l'f6\]__*{GV0];u B5BF "M#;voGMzژmT{N].oL*yȤt2~!rϩ }|d§Y" B?.o\*XşG溙l^O΃I9^ sIݽ"̐X{q\z4d,ίÖk$m/: ૆x( }=QZ`fǿ}tomi%R?lgNg@@5?G!1yl ˗QK6-Gھ=RoAL|#F<Kq|x|rS6e{w  w(!,kT8yw Wen\g9s2{p8ܹ'36|qw`Y[s*}I?LAy8œ,ۚcM8_Ə^dӾ<~4˻5#*,'oMqg*p=6^N7X:{1_hĽ933t7QWV1N(I'},|׆5Ď{~s6N̥phߊo~gY=?U lo~xw9St2UY[z I$vnCv~"˖pߘ^{;Űa {?zS2QW>ɘQ#i2h?̂/VQX 3 IDd m5麟2?+f6{>x} Q7]G| 'eǷX^lX(&vmOgDmaڝSA[M^/l#֓" ws[ÆClXF!:?ʞ){kf4 7?$v"22t9O0)K} 03secbQn|#c@O2ឫ(MO+? FO>,'oB(n:oE#3G s%a6'.l"YsQjrӴѥM0Vs+\ݿ9`klNx;4EF-S&!Ng(ͺe70`m'~dj?#~\ <6 HBp 8[^Jҝqqd$o ! zC,w<LiS~c')z?-Obǖ)vB[WqTlxY*[3O͠tAV_cc< I xung+u0bұ0I!™>~0r4?kLA'ӳK_ӝm[Q F G^KY! NWFf)) G8!~_U=4϶oH)õSh몬9-H}16=U:bD]9bh׽[K'' Mpm] {'s5Sm%|&tu٣fF$ cKę`#r{DTM˶aWl {8}+m%{d;T&}IFF_y(?Vi"lӍ6+bR>t{꿎IR#jz SF}xޙv0i=NY<BmYGT/{V6=r|m&Q5wBBeAaѱM%Homvv8P:p}йѭG8/,{6{Լ%sf&A|bm5Om >젌8mgo5?ר?x駟V9LƆHٵcݥ'"J*%ر-g?Tb s ʗkCTek؈iGhoi0t?.Ěb2=;A^ù:tO&+&/3;ps&ЬY5[% |3(CiO@VU`8}2uկٶ:Ob"≮r`q;a>}0˞ѺcsJ;v4пJeV|9G)6Iô3S X[b֕>ɓ'+4=okoEB \1,g [Jb#>Xf'%%dզ]kiՈg"<12\Q[[8.tQoR7t3ԽmMٿ }agt? g"<%xSl٫rj<57//ȖuVffɼJZz}x)qq- q\6y[~&f-o¥D|@[~1ɫٿk%e϶YkپS%\SWjl4XH߆qHW0V#~8~#&$r5kg F>=?='.Û=̙3DeZ9wC*|2"-5xj4v3AŔUyR5?{yGlO^ՓW|䤬bߊY|3EXKߧ\lHB]<"JEzoV).?srpu`s'YNBi&p-K%hgF?A[MV&=xx,*KaωW'O;Zݤm?^E߳y䵷Al.2RK*߰g ; *_3uO?o/v%ȶudWYco$m(79tؑ]v֒^j [?ּF M4?vs8]m~?v\9z}l;l]i٭OkilWu"m|3*l/v6v vjlk<)4Șgfn EgF3w KgZ=gLt160=6\3ٗ4#h; ;6z noWu(#{bO &)ڽz-3j4d_Oۅ{;\?`٬sD ;YZP=ok R/]|lk @H._w40#*~=<-[g ]ۏc c 6=Wq[ l_0֍ tk||?H>VY|c|Y^Ά?5S[M>&ֶSg%ߟɱ,=B0}#'`̼V(G1x\|³lJE~~.Eǒ; fi2. ܸg_{:]@ё9$Ӭ+{ЩG`6;|;cDzQrl3;?'!>caDßf|Gp1M/e|=jVMxth7Pw#ՇPL0ğ>_wve7=/c89%ڇPh&HFM-?/1HK?F⣛9W̜O~J?[)X _~"`ΎYtYSYN4j" 0 WGY~'~'+ݳ(6mzLoSmogΩ &s&6?c,ݙOu/ ).o#~t*b~ {YĐ߽JV6bFO˹[q~?ei[1ICa =_m5cGb?vŇYؿV3R iLbA1/2ۂXNlDzqy%d9KX%4l'f26\]&F@ngCϩ8{V_\/d "M͑H_N'jl\x"hD0QpxE גW$EĜ5o4-orgl6,K=aٶM4;mq7pS;$o|H") f(kWt[` IDATV3]w_hFt$1'ؽJLZD 't\3u+8>;DNBhF^Cڞ6.K'$$5;wY*ڍ7Sq\Rw$';բ=q]mަiO7A O?Q_>AW %2%B (h OubّbL{ ZWͥ};[9#=vr=eĈ/sgυlY0;ݚfq[;7 ԻA_s*}IP߰<Y7oX#tlהZ1qo.gʐ?dX& ^&917֥`mJJ;Bs#?h7x 2/j`)i),]=6̲M\iHȹϽy?~y#_qc& #6s"u "">i n}Yo`F^_z㛧K/.$]7_̲>)/y aGrW+cʿ<*#y!Ch٦tSnDD`%1OC6^Aළ޲3_Iϫnkߐ̲M\iH]i2zkFDDD~043[ MacMi^'K3""""'4#"""Q]T-Zw܁岸E"r>?{kqD|ҌXFADDDD, """"Q(eDDDD2 """"bXFADDDD, """"Q(eDDDD2 """"bXFADDDD, """"Q(eDDDD2 """"bXFADDDD, """"Q(eV7@D~n7sέ7lp̙CDDY?˨Q}"r~2L4nXvөS'=ZuL:ӧ7\D伧Ҍr?~|P;5"rPNZe[nͰa5"r!PG֭1cp\ "9)IgTPԧ<ԗ>eDDD4)Ϩ,#" ""gKyFe .eD$ ""rgT`(HjSQYFD ""MyFe T6eD$X]ҪU,nݚ#G4qD|Ru`WDDDՕgn&nTjUVQYFDFDDZgT ""ժ<4fDF3*ˈHC҈ԨbyFeiH ""ReD!4#"j*&L4(H׏fyF#""""beDDDD2 """"bGSopvm͊@txI[2& >g+v!ԛ\7?`A)ķmAMiX}ڟ5GDDDD, """"Q(eDDDD2 """"bXFADDDD, """"M8 """"bXFADDDD, """"Q&cg%i@OMM?@A^>+-msv{nyd e ==F!k<0pt)?yj;>$׹wl] >J܄W4i`M{oz?znNlc#r҈mm,J=e~/^wGvrhya}63Ԛydk>5˓Xp>ZN3j`YFgX5'ӔnaĈ{<B!sFDdlOcL*:E˸8\!&l }"RRuk>>0=֑Idwoxf scKW۪TF+.S5ӟHbLeYϰtI+9DAD=v+VĆ8>O샛w$ͼ`n1 : [VR5)Vػ\AcH+Xf㉮/ӈ?x7,Pxu?dX'xfy2_PbI9^ .^gy'K]=6"?s0y k̈́+6#Ľ,Ys7G4pv{1c7Ѭm|K2~d,Z[\3y'7TchWlZ-w ݖem$ HUS+^cgd!tu?C/Rv_cݚ;?$C~O峥> (ȹſm.]Ok!8:Ÿ<WWb}[f| 'vÃ;7)o`ܽiᬰӅe\2?>BY42N'cjylF{Ki,\9V|Dd+`LM9"7:|e`s5#"OQ ]TAú(9 Y3Żt2V7<廦x{i^\R?#[2}ʥN:3ي86Bp(IpN7bS_0gSh]nZ/L0%lx{Gs˨26KwdJdד\ۯ-AAD)=P8.>r'Pſ)fXJBt{1a"uhQzl{ O̎gA8O\ w7%u哌56-#1sh,b0 TakyzM$Fhp'Y5? %#\IVq ߿Ȧ}yiwkzGvNxgnaY{3Ig0܇y;g+3nD{~g~;txn"Ȃf3 K8sZ9 ~˗_oW:m1|/_d]nkϔ{GTl'qb$bl`as_gu֧wf}/' aq}%;f>@ ʙrN)Kq^C*)ԓgk>O1ny~;, ڪ?&:`KWٚYqfQtEF-S&!Ng(ͺe70`p77*Y=Yx)Dk.eI^nz+[tذ9cMȟ?DPGٳb5eMz cSrt"vOY~7p뉶AYʿݻs)F_߽OnfbVYGi`OMG6);Ws O؜^yܰ_k":+pu=fwYCzю10Zˡ'R "rSy 4o`M˶aWl {nHڏvʒٽP{QW3$Bu:u!o'7W2Ş'^{&мm1x_{MC ӷ[dY~qd:{sø;hg7p)uU$Ǟ*_-gశg}1}v.zf/. \GȯOCODb))5pWCBҏx{3nA|"IF$ڶ<OQɈ*c#,:(d!өy%-6hm7<Ѹ˗-saN\rblxE_WuQt+.9q촸!zs' /%Uh{ yz |)+ EFD򲔍fI}z8bGx<_O4GD)6wNM«Y5=CpopE3 J;v4tW-sVm9VUb8o-ڰwHus6Ma26|Gʮmd.?th ueF]G8_0M&#g :YͮѽY}H1yفcLYu뱷% |3(Ci#K敿)Ӭ0a9̓!'Rw "r1(̣GtaO}>nSLμ+BBV2j8"|5d[|4g613#ηN#1š?룓kvxo>^Gk-bYfbE"W9Mzu=a2kryy .e5]! _JO9#"{u^cڴoD +՝>}_~0x=r:Nd>ceXH0H";szfi=w% $\$#60 98;X1#wMwꎯRw KVI伧 "#r.x৬\J(1NT2{wf!RFpzSHȮa9[{&P3>^D !ث7)\g-=0ڎ=;66k J79R,FL]zҳفdkPLcOwFrn1xD; g?#7No1Jκi٭O)FW!Җ'=_]l{x=%8iuolVygˆ7_"Џ1BlD5WiazȚ4˒OYpl=ok R/JKg_SBpIvM6>9)ȹLj 2fxg/k)˟}%s?cƕر߰m,|'|2J{U}+|\?&^Mf (擄d%{|2}Ŧ WҀσPxd39Vnf2VY>{9 #X<6oN09G7^aE$OH ٵPT{m>*2Zuha!L6k86 p9a?}|J |N$}^F(q>D>~ޚs=nwM'YwSebi5` &Ns\h#rS+a')\.aa0<1Ӱz75޾A1dz'?޺q7?:X1?UlHb^H@zw~?<@cx:%?˗0kы&盯w5mD'ToD]w˳_F럎&D_ c~Ŝ9)6_az )=ql"h9z|$.#Vßd{I_5{6OƁrsx!bi֮; KifcDڗ牯FvV6gtkuEoބ7Ƈ85tƵ<,Itp E pGYp-yeNB[^DLD];?,K|ONIHdkwDVdDi#\gN1?51)U!p+$4p_Wo VcCfj\6.Kd5Z'0XsaO[iV U]|ܬEȏ#""""$hFDDDDB """"bXFADDDD, """"$4'FDDDD2 """"bXFADDDD, """"Q(eMAÆǚ\ "Ds{%{II9ԛimM_jsj]XDsDDDD2 """"bXFADDDD, """"Q(H|gaZ9O(eDDDD2 """"bXFADDDD, """"Q(eDDDD2 """"bXFADDDD, """"Q(eDDDD2 """"bXFADDDD, """"Q(eDDDD2 """"bXFADDDD,ciZM6{W\qK,iVN#""bAooֈȅBADDNя~ǏoȅB9)2"R)Ϩ,#" ""oyFefD4)Ϩ,#"9M}3*ˈH})YZQYFDK9K]3*ˈH04"""gKyFe TeD$*͈HjSQYFDTm3*ˈHDDJ5gT`4#"U<4H+Ϩ,#" AADDUUyFei*͈H*+Ϩ,#" E#""R3*ˈHCQYQYFDJ3"ReD!iDDDjT<4$eD!4#"v[3gM󈂈Zjj*]t󈂈XFsDDDD2 """"b7veJSo8bMQC&" b{q՗cEhC|fMi[ᬢn  qr9شX(?Ϛ#""""Q(eDDDD2 """"bXFADDDD, """"M/:Q(eDDDD2 """"bXFADDDDyƟB\X}fg.-4B oFdNuD!cڹ%vd0"iqD Fۄ""\Jsl|%Ed.=-AӷYBƜUsъiqTQ$Ms^r<̱k=Uw !(FJX*|5SGx^unB|H "DG7gPg@ix,A{8XCAJ bk,B `+j*=  ߬`ݧIv [b,3'=Ga̗ }A~&7tz-uC_/~S+[wD3 ;v߬|geWCX\Y<6 ?f b^a,_%,|hBK ql;@pM>UrﰯJWPzms#Ug8%-K u;DrD萑3)rE|YE W_ўd)VyQE5: ?{ {[Y9{rÀ!΀x]Qsh'eV zMa#ғ7`3;=PPm5t{!cC1(uެΝ=ɋfBs޻l^:?YM~Rbg tJoH`tC" k2`s|^(}os(-AC)x&ӧ>K<}0*x+*cѹgh/tI !W>̇fe@s IbV݉rΘg?GϽūd}}z<.wnznr<´O,a[z*5P2{x;v>3>XK7A+'PrCfFF8]v#amb umܬ I "UpZ+A3&/ߖ^#p~6Tg\ "W;N-dscFah,}U+?۔\Q=Byaun0AM贔X;~a4F`-YMXo|+A`{#ˀݏ1Fas20aA=:v3Eh>mb?v"ٳeZJ34M2cM-#TGnR݄$GD`è1D."{Y$?.\ ~Dr,b|;8_xӔN^Q*illª/`'4h\MxN[FfZn8#[{MԖzM[XȖRt.OK0fq{P%EϨ>rT'ʉڋת2]A FQ[^e>@߉>/#4ykg1K&j_ǿ{gbsz˪*`EolbVpG|VMN!n xg=K({C46/.ALvTm`oQpVSB?̈Q#遽E2^eAo C0me=Ġ˼_ 6HI4fC wy.54w81\װ=4;F2oJ[?&v Ss݄fQ59FYT(^=f͙A@?LX]mу)#|PШKK6fZq %Pqpzc ]׻A.R+dh\\qr z-k^mnȨŏ`} s夜8}˺b,]{FGML!:?CEv"wRqٛ<[A4lKq`QrD' j"*ZA;ʼnXuime&oΧE;OM`ųkmgu,∻ootꫴ"jlزVuBJن%|elx)׷,ҒGyW|pm恥5q܋ɩlip:(2obNZDNO`ɧ f[zjEFqgHjL>Q/h]w|>b [i.I[Q<3q\KGlз̏0nz]˦xUrM:(F|†g=&%23׶^ˉ>7Z5åV@u| ⊋-Dkj,zM"InloK0NXD V$-21.ߣfÖXu2qѨW64kĜ[hg`i#Jv1-z/k(Q}BT\F=h_L߰+H1}MhLӐx7Q4Sk16m^10muf@D(~?d_+ [JT'3(N/Ջs|c:f]3 "[iݚٷ m&zJ_g!m1bm;q \;25O[HޗUWqK`{]aSQ&㢀^dL42uUU/bx-S`*h:4yNP<_3̷a$śc "zUprmڻ[u&U3BtW2[QPKE*t|bu{wo*8ZBtA2| *st0eƣS\Ğm'r9ǻ9NEDϋ 4Pڋ;U\^dvy>DH$YQQ[cpZ6C5L0ql"N\\ ^c,,sēL j/_qǸfSr_c]>BاɒMkHbocPqYO~}* oW^ H3Kq̞~ݹnBZ.ŝ^seQFRwn$?;:, I2Vu&Qt]iZ2 ogB!쬍F#"B.%du !dDD!]H!Bt DBe$B!D@D!]KۺqB!-B!D@D!]F!Bt DBe$B!D@D!]F!Bt DBe$B!D19Y.P D 76?Ξkv)Bn&;B!D+B!D@D!]F!Bt DBe"UznIENDB`nxtomomill-v2.0.1/doc/development/img/nxtomomill_design_2.png000066400000000000000000001140401511430602400244230ustar00rootroot00000000000000PNG  IHDR`zsBIT|dtEXtSoftwaregnome-screenshot> IDATxy`=B6DvTpVmjEmk[ZmU[(RQd=$ }_:qzB2s3|9gF4MC!B!zB!B!D_"AY!B! AY!B! AY!B! AY!B! AY!B!` !B\;S{B!#(3.6,BIpr_6:goWEG(]Nz=`}2_ɓ,B!Hj!hʨ!k2GY!B! AY!B! AY!B!Du`2e!B!ƒ0;.IPB!褎<ݜuܲGw"v *TTn9M$n]u:&N ?[f+*j(] MMbRѳ.7IPB!#4[p?˳LjR `La J^E%䳂ռ+2ż"8UsX2{R_p\+FB?{e!B@k࿫67dd^^4"^ggU|Q$mv]&* +.+'n=ȫfDϻ%AY!(ǏZQcW[[CG1UKgQ Kyuw䖓Ym^,Â<ʖWo\'0=Kg{u vnfxp1iU6:q,3''fv2B6Ր`Q=S灡^pQ+yR}=G>.=KnXGjxe^~w<{0Ϝtc\^ޓǚJit0r`4${9,V\bV,}-b 6*RY66PpֱjG:?XJ;!119-Ǔ}Ԫ#|u/[\:ͽ-S|isחbE0xQ'uo9/wya @jB\6oŘ~q6)<]d;QуXF#W-ƪ Kq}_t,\l-̩̔A?f+3Cp a^-۵ kkXjf}V^(df@F*(z#=:v?Y/᷷Ji0dLExV˿8S9C;PN:̀ NMECG ?ЛW@1FQ7񃌦p}qԬj3w(Zѹaum kk~4Ʊb&z&Yٴim-twSiu d}'rCF2rx&zGݡ}YB <:;jؚYöcE}/$|>?j[q@~OeC!oqeC LУXX\k}5X=V};Rȯoύ., B!L*`EB|/#)@!*j:E (t 6L~)U,ߙOq#Th.(dڈKLPuzR#q24yiphTW},Ǿ-;]Y$\:7'3)ԈXiyp) ic6ܙ}ђvNF')u$Z{ \h(=z@4 (89i<7.qlU޽wQPVb}c6׹RvdB!D/?UN @Ǹ1^oX;Q¯}_U=[Ꮗ9ƳLԡzs 3ObDjg `G5̋#H`0IJc (Tok YNP12s2OMIloYO%Ti`'[qMQBkgp :4NfdRc(*0l,ޜè#&?OS1jC>/xzt~Lt/%l瞧5|>gC'1NØi!Ƞ'$(.oSC>0 聆\>vk<'(prN/Ʊ*/ك*)d PL\7>X/&*eߎna<{kkMbtAC]y y۠4*G(janyv53aS3DM#^>xO`'!7}%1Gtj`JBwWk kGօ_ϧErAM#̘w,u#t9xPmFkrl9~0_Aϟ?q{j6̩mw}]j/z-BjmΦF[{b-*(ju/ 1\TOQb cڀouL QRQPIAuvcNS(v˜v*33тaHlD(} IF'tvwd0A UI;Yԇ1wPsM1L3UHaUZyf*̄+)$GXAf,MsR솝І[2S"/'4'u1f0ߊBydLIeeB)`y $xva,0DE2٢pA-8K'AY!.o{YY';YK(Ȯj@4 nzDbX3栠֎ L$ 4G=[:CU۝/W?<InZ1a]Cj8k9qNPV4{b^4X!*UBNw6]ZG+Bz]Sp5ՅG)A>6 i(S\5Ng @5 Axhv3(hb ޽.u-AY!í5]B4'V9wIZ'hh1DBǕd'U;-M!l>^sPms3Z `hhv^1kř#u7ubj26Iץ,Bѫb# RJ)לd&M H'Zss{OtX$:nn‚jM}  Sь jСS-GoQw^z]{^zl}䡷GWP'̪ j6Z]BMga9]sz^B!z96>shdfpdz(}0^F 6ޤ9QQmup~ H;̚  Kc^i8;#. 5jSx5vwEufSJW{{z~t#%w0>urut>4o j92ٝ7'MORLF^.__[!BqQQb}{8l] s?XsՐlϣ tU hliAce^h9[~/Fx 1VKfgUDZQCm>o)jwZh5}d/_Ϗ.0l9\LV'Q>z0S[ U0MmVT^.ue!BަXX2m#۟⭪džFλT~ЎZê-Ucm T9\adt{ZŶ>Yv/!E=Q o᮵:'ͯ=wwtK DJȵXkA1[u&DӁ[ QZoj66zz~t!}8w ňF|6)ta1&!ȶb;Y^n{bPšޓܻץ,BŎɡ|-&{k+9ٰsWADL -!IJoyL8sHh+!1jzʽ^Aq!ެ@#Ԙ|CZaqr0|^EysG'5P1"όfU, wS9 dhgU9f 67}9Ժ|V%:ٲ?*3ؙugHPB! #g?k xU$go?{GO!ՇxcWYŕso] rLh@;[xp% Ш)M$ŚBؐ<q{O,9Y5pvܻ룆XkK;((Mr})C?omӒ 4)_{u9ؖƢlÀQq{(OyܲZ.('bxff,Q 8JpiwʊU4B^Z5gqW]p67M+9lRpPZY;_obκ*fƜgWy8u!{E-zfIdpMI!>ҏ]_nltY5[=8Lz]!0/!BBcX|}͎z'Ny-̌1_#*ç̌uN|UH~U˖ 0κ162+yG vU5]N咶= &oIF^=~C/ U,8YA*N葩l ?γGjR:qIuH*gj)Py͏%3GڨD Uܠ`6P.GcNo߮K=CB!D_?nFf\gcvj*:RÙ#b{V#$}h>aE^VjUADrdM❪<ψ v0{-׏QcǮ31#xjb  j./KՇn%*uN#0w0(4]`V9cIuO.WrN#:‚0[Ƨp ;pCD2僜J;p f<5*K~@*֞+Ώ6u K5|w'{Ҕ*RgRZ[J) <~0fy>]zi=3A!7Y ] OBt Ri`w߅O%LW:5?sB9{֘?¸ ={s^&AY!‹*:DQQ;whll7* !DXyGՉU|.R_#AY!puuuL&owqfX!DO,B~3a%!Y!D%AY!ʊ+XbEpKdM뒢B1Bѯ7ŋwiғ,Zc.72On6 K@<r !:K^%B~eȐ!}]Vd*W9g3ntPUjԨ2@ ( !Yl|.GBhd.oh($GkvIB!U,ڣ r$& 0,wB\B!DlٲNސHENp(v}]EB!*KOϙrc-D!sBow},!Y[Ϳr(>>8\םuYr>V+#كy<7@9]fQA./cM~%Gk4j:90EcވP`aџvڥb4OOgl)mQQ,ʲa*U;"Vq i<#{3_ise@/_hu+y Q)'X[PűZ?JCa^|^ݝ'dVۨt2:nǦ oZq1,B~'''_E1 p:T4tS o4,|?h מiRܱ.jEǢTְGy9i+`g5OFMvprOwu;UVJ6LPۇy"NJyW#y9<_B֍y=LED_sR*Nz2׾!~n 񬙓}xCyMӻ[Um5[Wq>r+KbGi- B!7<nh4p8Z]OB\MR};GB=Sc]Qɴ=ɡDhٽ[wQ) HKs8֟@Ewϒ]Od( r@k 6:9ŏbcmy?f"5+{2\28O$bd%e{! MǐyfGha#<1=9|(%{N/@1xh~73khTU|g?S, dוn.]{%B!0b^{5v;&|;9rչEQtM!V# *F]7wN)<<9 $ZB|rx4TX!$l߿vCdF/4=TNe͛śc`cj8F@m煽8_ b/Nt=4/ %,Ţ~•&قx"uzelklZQ¯}_~kDZ7 B!`>?O %$4*G(͊g9:;`Ùmb)V3æ=DjWfH|dS/,XSx$A${Xrj4zF͉(zAzK uOBCSu99|PUq IDAT=W %[Q̚T=hr>ײeN?KBi$ŜVZX04XDBx Qfu*:ƦVr20p[ʯN9ٿg:4k0wp4B^T@Olkpn~lNzBuٛ^e ɢ)xkmu&y"  hv^1۾7)o͟RÈԁṾ<&1^{7籧#kNIy+plEC=˷daO~wE!}Xhf'36)>ӗdo-[UW]%!Y8 ,=E{i}[ߑdʼn l*{S1}gnβD A8Ob^ ۣKw:wesBnr1(~OHPlȐ!^W. 5ΪNJ=5vLg6y¡m^-A,cAUPĊ1S %r^&<0E#al6*\u<> c#.B!Ì.kFwd!&CS^Z ZH:5=w*lowZ5)Wg"9!'$pKf&)$o8X4аtՐ^qKL_!B!u]1Y q \{]lAPUU@6aXfCnWZi46Բ3L[@bJt.NQQ~Zê8[Z]cm T9RKHPB!D,.vAC8P\|pS>ZǛ;8b}fb `h/yIY%;֐˭/f;O-eeky5U=Aft=i^-k϶2ie!B8 B41Xpxh*k=nGEϸ? 3օ3;:3w}[+:Vכ٩1{̬Qq\8tjhJ]}-ep,iJ!M3G<9+XԆB~?\IC4jj`ff9I6d$K'QB!D9BKa)|XPw_?Nnj$ɌH{g3rcwl޽+v{Y12aqP_C-.̻eyD#_W;! gZ邉5O,-/` zTy)Dď뇑ԇ_}e!Bt; BB13s2r\>˯hFt1.a OAX."?(Trށ`fHt4K&QX PzL(VQl5\u:|mE5iPE3=i\xsbd┙9ȫ J#ߟH̃# ɢM֌'_α:%B7]V\O`7FBOyc/̝|e_!}D&D7uK;5=x\sD^?EzNS"VLJ=k݄&)2&3z"IDd(sJj!huNH%( X JIW.9O3vRF 0Lr hCb'sْdqpGU%0zuAԃH)pdptI.:]~,Ӟ|Ԛ*~2Az;eS%Y !h^{_ǿ HPqRuu)%hsb)+-:)=ULMl[5%|,# ]eg .cZ %sʲ]Y7!D6zd!e!5%%hbHp`gI 3L%|ֲkUkS#)/)jR_좱zw]݄B!, B5Z=ƦW_'_` >Yخݳp~FW_#ޙ z%uB!7TNF}KPZ 4hYWȭU4aW?IhpU;Б {݄ݧ#!B#B5Jf?wXԬػT )kV'(9*($ EyvqzvM!BTsGG=ʊͶ>9:VGIN.*Ngo!POP9>M!B. AY>@T %P{tk3폤z"SaQ5}i1'ݼz:q:~Bo*ʛJnxM_&B!$( F_ðhvN{sJviħlkI.6o1[f'x:iD`*cV5]7Y9=SYѣkpOuB! HP/\ƔbQ4ԢX؟q*K28)w#aR&_}sKL|<gǞT<+E2n.Ij~K=mx{9%u!Қme ?h}̚Ъ^] ?ՓHK˦"whnB!BN4ݣ|m;NJ՝uBhXפcz*n`Oԟ5?aBU}o0O-:Wt( hORtXa&,l:\`aohDkA?|L^0Ï濷`,]wX܃D}]V]'ЍUB2j!#G@akawB Ne=w̥֬`N[iVL&-`̄d, fKd,R-"rN)b'"BWE}Mq<뮗>j q c?f#. D'(-*c #j\F\➟K݄F9 0lLloWE!Vң,Bve! ,B!Bx,B!Bx,B!Bx,B!Bx,B!Bx,B!Bx,B!Bx,B!Bx,B!Bx,ڤ|[zha#|xplwvEEڟB! B!B!CoW@}y#.]zuy>_Gye<}u9uQط_!&AYyyrEC=wPwt57#xUU8\z Ď"~B.9p3kx/IOvy{0|)Z{גs085#HҴ[;1˹;~+9NI|zo99[5 Db2rP}؆ζ.[kg):=]}kk'%%8_JtHL~yjJ6h"{Y86&2,AYKl_r`*efP^VCk ~cBe;wN:߶->~B!Of?dSz)Y=.ɦ`J2%7OܛJOagiX= pS5Uٛ8z? )5'H+;'s,qOŦVQteVM;]/Q(;%__ǟext0Wcwi`߂`ӛ:#[e.*s9S߿idO/~'n٫s)o qE/ V:(&+]}V/_G ̱_xb]S6cHkx[Y}/ym(⧬|5NwR &kU6~Lڤ8'RߎSm[[|nݽ^ׅBHP;>}饨5G\0p j-5yϲeG\౾JgXz?VL+ (56HU᧬}%'hU{9XA "f]t!&h,7ÿyu {\=2iqUiXr̾Zb1U5~-WP\_9Gn L=W79ۉ>!gI~bͷ<4Vë-GK+JgUt(:P۟AY0g1hyg6|:.?v0n|[[gÿ:Mq y~DM9]Ml Glk@uճ}_wou!BG2G#{ؚKݠ{IdС3YDj1Y[w\ߺoVlQS0<̒dD`ɰ/rq GJEU]˸ǹnBP/YO rn4npQq` U~s{QnV={3> H\? L2Wn^_w7} |:_%W@|H%Ҿo>\O`I}~]E6F1GO`җY8'N*7Bz陽S=}n?mqsBqAL{]x,o1^zmI lj=7R@COB/j  ]ǖwrU,ń[Ҭ M6qDh'0-]TdbՒϙoOJЦFviڍusrǧsQ~~#~J夌: W08[vRz4 Fxg>+|,# ]eg ㍯v>n(z텇y >,wqGoW.BqA yr̔6 { AKJZm.N/k٨))qZ|~J @]emz1y@GH@tuBrW&ү?;12f*#'0 *,4P]Z6ʧzjE!u*Ճ` nɾ&MEU;vܹvc:~|k?>S4Ό3s}Y< ZCoݾlkYݵ-UYL&o|ܶ.c]g{x6N8ҥKygYlK.+,<@E !D_esʹifJPƣFnckl /fb=)-~,cW70z]U1bOҔdU Ƨiح}XZ߷:B(xv*vs׭'ڏ/Y?V|H_=_ez썝B7Ps~>n[uWn)4b"KKl|t;y.rt;b_;ZjrkU0 b0~ҥDEEb6^*24uZR oڹ9%p,7$wFr2vw$FS;9b6|ԇ!?u=ۏoqzrH_e+!7q&#WNͷzvoq{w hb c={/,e"V%{^c(/A˅=%sӐKZ]1~gމlmMƧ.BG3lh}\5Tfoس%ڽgK(-ܷj=6kӳNsn|cou,MKۭ^;3a>xk?=';=g<ˆqc=ȼwy$GtQ]IS#2 nFAa5:ٱe9f7k"'-><{3k;5TfbKgyj @1,-@M31T/MFqeT`G6 ߒguÒ퀇5OWn>O F396h.a!ekXXeeU3?}זQaZɧ+4-~j潝O_k?*f촘W#Vna777?=Ђ Y\}:ʚ%yz#e..dq”Z"!閧(qdrdb OPCoC1Bʵm6?8 abF,X=o^ðc&f\?sx5~&EgȜoؗ_f?{BА3c^- nwu͈9pǑd~0}bo$0wY4FrѯO>XMi }k ɕ ~wr7 7-QbͼiVkoRy̛7?]nѯw_ }[RP$"<ԁYIp і"K $8q$)=GkP=IAcxDA0eTUQtaGs xw֜Ǜܟqc SQRQi %4\oElٚLjǔ~Fʲ. ("^W 'x#;צREn8}|,^ABo1|@dMO_<=_KR\0Y ?c=Kc=3t~|%%hb=ɥe?,؁189ְ6Z ]I ge%n{T۞i#<'sm|u9,<&ool)n3]6_0B{\ȀO0֫J8"n-$E70>c(*'Gr.\~G>iߖz]z#=jj96T?!::YEJܦ bOa،1=KͿiVko"ʄ :f1l0.9~8̛7~S=ϝɫ wp{P:F%˔=Z4]^ǁ2zmV58'St\ֵٔLRyj$⌱9-ur>aND}9V>3Sm+ `l-pl[,ic ""Q)(H_H1Iծg;df2M_Ȭ-WMԴYQ@YDD:Q24GY:/r'{ O kyz [")WMsƊ9-urԧ(Oka.**>}DQ5GYDeT}n 2`a &8~}/3~.9-urtr饗ޒE.F=""" mգ|a.**"))bzͲe˚ݳeC="""eT>Kqq1-ҳlh=N1գ}MM)t\3ѮehJwo,OuFGYD3el kXrHGQ_`~'O'䩧yeeΣ1AYCEDDSoH9sN .**瞫<-5 [DDe7999!gSX ww0's5H+pgԼ$}~zY{c02`=F)ݘc+qݻ\UJu((|++p8ڻ8""\PPРEڙ;_ů9T㡴AG_Hv"˖-{`v.H9gϞμy|ɰ[Ggy! wfazJ9\ ~6B>2$l;;?Mxg[!=뮉fz^.ig5f;hys&e ohi- """%9.]:mXv<|-VtU$pn|Ct+|v+vß;EhUY^wMUPPqp˸dƄ}^/9#v4tI&7Vg f `%̬% $fp8,(d偣|UAw,#OL g|xZm=iLnLN\P{}43MQP.|+]7D fo,XU "RxapnW}h(= RŎMx0a: lk<. 9R{]yDf.Ԝgm7EAYDDD&{ ax6~ρ'`b@}GXw(o6]l'wpHyGxqs:eV\Ei!,8!qޟ{r8,dS+o/BU7LGo͋{rQTObtӇ ; H;_g* 'qBYF>x3ra%Us69yam$+}LV-\;c 3ٴc7ܞŚrN Bܛ/ϕuGƵ>ʭ u {`*Zq6왳_ \'/o3Λ [2Yt}N*M ލCS`gёΌ[B믟!}.|yb#!6YH" UƂx:'>9Xrp{ڝ_:pl8kCmh3u$括"VQhŔI<8((H:tX6KxmQL0ŧdgs,,tܕe#ÕҿtJs7-k35YjׯΣd]c`x(('#Gxy[^m,tEDDKYhݺuk:jXe1?ǃ J X5w&vodƒ,L I\HaRT܍;ɖL̬6]Ga0vdM*O]O<%gjcyK6bg^\bSUƚi[T~֯Z+qacĈe\F 4d[{-6𳤫W7?^bfQ^-sŮ N9 Szܘ ą';-F\[?'7#fئ8/fOvQ}[dUdO5ظKpZ]pׁ.ThY~VEbdLʰeܻy-7?Aik:Y A&Ņy{6YH"8Ea`ܛ^5.CIsq }??\լ]JJy6/ϜÉ1lI}K2Hs<2AAڝMsloLٻ5y s@B,&%޳.?Ėt<7%ӣdon6Gee%wfܹcҤI4qcrX7ghjdɣ[C!> "\1GbmOQu)J=9F1$FX0k@Faѵ8Q;(ʮ_}u b l$óG00+pWe)`k.q1(+P%#wabΐp=c&1a .M(;!6ё11m2 3 /l6>L lsO/Jfr`q]s" ⣛GĠ!q|)8#ϻa6/gpA 0Ex*&0Dn#(0K.ßlT}">ރb-YFkXo|X}zĜÉgaclȚpt_ht {&%U.LcJ{ y&Mh5s o!zРApNZZݻ4;Jk?Q.gr: >!î'(0d|\pR Ц)Rsk™%̑=Y0&cͭ87~$DG]uk\T3ṍ_$O=lM$u0Fx="OIsܘظth:wgl1\d7$'f7nuP_7a{k${n|7̈sJh@AJ|\$4(;Pe(soq}cCSFya~eur;vUt!y=}}~PnSٹTcCx2wn0:ALT(ut6%ޡ@x*5!hv%(*>"fQ-/~]L0&r B|XmΗ6 aW^{}5v^g=d?K'?n%߯ 9QUƚ}IϱuC^ꈽuSP C57]ÝO$sO>.%{ҏ/?imY09qKs1X"mhyWVe粮gfjR"kmvF&<|ᨳYq3gty4TV18iUu_WDX+EDDD:wjQ\,ݸ ß`˩A)f][vJ&qǧk#ޯ1NlVWsA> > 69s7A !5|yJGScS_s.TO[ih ڇ6Tdlqbϑ#uG`b FW;.Z_h> 17_mS)5ᶁ!>Wn{{4dUDe ]>5Cʋn{""g5'xg*._GQsc<V\]]];5|(vk+wm=|^~ <:º /ǙN6qޭD9\TA1ɼ9sW(㗛J/{ R~\8v2, ABhPczH  &dfc.sW AAA L?$޾+kxe_%N?c>t ?0cL7.z3^s FtE eN0lDZ26%OεHg,""""{{f7}[H"]2xQP,""""""EsEE֭[O{󚈈N*LEx;ϟq """""hH`ۙ9sfQWQP"&DŽ Z4"""""H1j(tӱ-\"IAYhk 9MAY ik 9HҔv-""""r&e.)ï5ZDDDDL "]Lc_kصȹE ְks)(t1~a"""""RP2ZîEDDDD|SP2ZîEDDDD|wD~K/ i=ayڻ$""R$iXi'֭shu\\999m\"W/@VQ{C:10 L0.tPm_;/MxתGY:9:775ve{փHW9"]T}_tMm\CCE0_ï5ZDDDD~Q|~a"""""SP| ְkiH=ZîEDDDDO="]k 9?e.{]^|[YfiصHGY`ԨQw}] NA="""""""^ԣ,""""""EAYDDDDDDċ[c^p!VYDDD cxhbۻ("""QAغ'_kGDDD:*&GwIDDDZUr${4=[<"""A]69"""""""^EDDDDDD((xQP,""""""EAYDDDDDDċe/ """""""^EDDA.HQP,""""""EAYDDDDDDċe/ """ f{@DDڻ.IІ%ڽܬÔtXGt' Lf[ۻ;/&?|.F@W0u1``K[~~/ȽտMB/{{\7EۯŁQ^[ǜ~\/" )qw{Z7""""ѨGYGQ>v*_oƉ|՘.G1e9{8#6oʡ^d?^/tpxg7|""""rei;Lb&`;?OH>NQ} 3>yo%%.00lX 'Cdm9DySg&^۱QDޗwP9=YA޹Ĕu}W10_˫ o]=l}ܕ n' iӟaO7# Y^m- ;A~^unB.16a|/BvSL{woNYDegltƷ+3w ?lj 38V>\9y3WG?%/L3,$c[_Lnk8=ʴ YȚ}[([\1*/S>9J lnbD$-+uCƧJL?FY7ЧW 6M_<+EѦIxz'aǞTB/9ӧN!>&9I 7ء| n"f6|2}}ųm4Cok‚dWMFm㛖a9ns ?3~%#Cס)3[6H߸E綟 oe3n-ɨG.[JfND < z۴`֞sUf0aWfCƗo,c!`\y&+h/gT;G_;J@10=YWDW]1`lXbG?c/ٰd܃)?c( L2B1pz _37ԝEaׯ g gm'ᶟgFz&ΜC4" d6\Wm97_&{3z+Ɗqȃ2IwFt38C0b.Mʕnٿ}5c&t?[OA>lo~g{5=9x-ݒ FL=F(zb+-_os]'K]ہ뜈{55='q5&#WB`XPI -y}HR%^=|`a=ځIeQw,-~en까Pz(O֟bHls̹۟50/xF=n j|v#8vعȔ0wtz+9f,Sk^b^| W]'ϙ5)*U}hPY܅T5ѭ 7KϩմZԷ]bPdSry5uHd}v'<n<ٔy y{xm~ZZ v~ڟydDDDD$ei#*TiglBטjZ,~~ޢ7 s}/ً&}k:l4#zԟx'~Jc81JB|e,{m> ?MxSYհֿ-%pc:,'""""5XYIL<9:f|?rNS7r_~N6m-{};͙ YQ^Fl5E0H+Y˙|"ǞXz 3'=Kk\݇P[]c׳,^z7\sMo萋Jw򉈈t,mw4k߾–XxXGSFw9MK=`Dr翙1jz&c̸OS괽 ?ƄA!`1y8bt=b2[: cDI?gH #sd&!Ye:JqWr5/0C9$ODDDRP6cLdК-n|Ȧ-nq5 :&/qoq<=`\ѱã0MuXғd_F%~XOl,^yt$c֮ftgD0 Tb!Y9:Ba}RTP-щX^>IQ\}Q6=yYF1eڈKou|F͵8ᴂ:upYU&6bÿQi^>IAYڔ=L"ab.eƴo <%ݿ%8Tm5YҿzWPY>q{VoZF(!E((?7 E+X5YɰbmVUP})ڻOUO0*<[g;2FEL~BKaNU%gԍkaK^',,veoGh9|""""-#2 wJVz^OYh{pzmW^}t 1qE7L#l 졬q槲o#|2*L w02ZtvaMs-ߚ񲚯̪ f%Xx OaV9;QVCavNM0l:?@q&hxbsϳ7#fjIe*6}`^Úws#u򉈈tBZZڞ5wFDY+qPsc"v&0gBEJ>`5y+X5,'W2,ɵք7=ƭ=%oon~{gE%l ;u5K2)`ӗ3x8~OFw ؾ*>=Kpo ux&҉+Xmwc|^\я1z{HIL}n>JB Mħ( l|ϫ|0lX:Y7L~go?|"""">/I0BvϹu=dm;6}EE8. ` DD $({a&ΐعtҾ ۆXGg썌7׷0%mLމ"!N o˧m7 6e_mڟ>A_p߳rF MBЛF ?bkW>,YWާebD^ɥs" =̴ɛh6/K04n1_1|۾ZD=2G$O!jG/H'c5yxn.z$DfDDDs; G7n*MT蠬REDDDDD+Se/ """""""^EDDDDDD((xQPlF)DDDDDDD:(xQP,""""""EAYDDDDDDċe/ """""""^EDDDDDDb!fVyDDD1_V+HiTP84y+pб*H2M}cF{Ao@\24J"""q>59"""""""^EDDDDDD((xQP,""""""EAYDDDDDDċeoyaaPTTEDDDDDD((xQP,""""""EAYDDDDDDċe/ """""""^EDDDDDD((xQP,""""""EAYDDDDDDċe/ """""""^EDDDDDD((xQP,""""""EAYDDDDDDċe/ """""""^EDDDDDD((xQP,""""""EAYDDDDDDċ4M !"p84i+VhRtNQv;W^yeq-PiDDDDD:7e.on3gltnz-ixU!!̨̓ 2; jVj>cl{O{֞jk:=HAAEdBy&aȴ:/`a pb{%k_wq6׶]K$IG9,g~m۵$ItAYCj~m۵$ItRRkۮ%Ic9,!5iZ$I:AYcδڶkI$X^Ku̙_v-I$}#Rs&׶]K%16~IDAT$IePmZ$I([:tگm$I爲TNmג$IR Rukۮ%Iz-Q'kZ$I:1G:d׶]K$I'fP_v-I$RV]mג$I9,aյ_v-I$AYoZ$I:9[:.ڶkI$Qkۮ%IS3(Kۯm$INk1c0y.E$I:OM6qv$IyϠ,I$IR(K$IǠ,I$IR$I$Iq?yyoxj9GtS GCIJN0cx.Esꌂrb2fЪysU$I:OE*Jٱː$; HjjE$ǒ.As%I$IcP$I$)AY$I8eI$I%I$IcP$I$)AY$I8eI$I%Ii jI>&eI$I%I$IcP$I$)AY$I8eI$I%I$IcP$I%$I$v9+dSX}r6o`^JJ $7&i:QtؔڮcS?]Ĺ@@b/s٩2;gnHkqE)U^k=?J~EmW]B*Li 4W"7Qf(<TrYvirϝGC2΂מflj\$ItfQV_(dMѲukR b8w Lg;[PLޜGw ^ȝ>j -hFqmӃNȏQ,&g,^A&n^CJd]$IGUG(V m?g> } y֤gdָ M;_΅7>~KzLO,: CZļ&,&" .g41 w>rUOD`C (yuR$I>f(:*CV+dX3J[gb-;̟CΖM?PDE,8.GLU#V Ͱ.IDiλ,>ٻyym՞v%RQ7Zp9E~W&x/67ks\X<^%? 1j9Y9/īoiێy? vo❃WP |MkW_HEDrÖ4ПN~^a'3XL{̛>~{Lze6+B HL&!(yϕcnK$QJw/a3gVI/e=êbD"0b. ܋|e[Q1Sa,Ȁ{wN kL{i wjk =&Am$IuQVHch@l~u|drpڵjJrbHrbW]Jֽ7CvQP r]9Fw ! v1(6>%oZ5@J~\<)>uu;"T?wȫ0F$U#?缿fǫdna77_5$IT ʪ"{nIJl~j~h<&sg#S%^\ݗg[hRd c$!e;7HkH@X8kK?#m GҵemYr2k ҚF}FM i;r -(fg );i,OվL/6^ݞț51˸rdEOK7x A*Gty# u7|70ಖuDқϐ6v;Ѳk/N-M$Ià:,aܣO0dH?2R#qhR3 ?<ɲ6QI#EWZtHMoZv*/ =G!-^6-Y4ih1h"HVkخ:C^ ({6:2]c0[Ȉ@4EfL]El4=M[,%wnHqY:o=[yF1oqqlKa㚕ݝGII)Q(vL^OD#w^\uR?Fe}bəd]|#YH?~ 6/eʹ{M*d?x;S\rhퟘ؜_>4 B'q067|ݪkNvC5^,;Hy NW q̃tk ſS\[9|u>%$ҨHWOe{ fLH}d尘 ɷƊ'%5D K9'I8\&IaP'WBCuJNC>[LE̟4_^9x߱@ {d@_Haٿ˫:I=F)uVbl֗ۮnаހCq+}}wk1ٔ~sq=egQ 2{2zDs?$y\$I's%Hڌz%; eϜt A|ڶnAjr, Dl~t$ͤK;ޥt75H#^Prs+B˿e=!,dguA+FBXWf2H΀_ujW~}El#,Vq }’NJ)-|APY/M$IŠ:)vh'{tWJhI̪pzraݛ6reUc[w9jU4@gF! W #i1hޢ-DޮZN"m5J `Ϋ38D ''Q={1r# XsВ ? :7@ <\U?m$FS2G2[Wd}.ϵI$tg(_~ o>(9mw*6[HYUnfL]'[r2V,:gF Öͺ4KI9 4ڻrf6. Vl;L#&{DH۹$1$b37iW>1Ӧs![זD"%bkٺj ?{ņ䔆$յ'gt8k$I0(J[h@XȆbX>&vE zy4 =2†Wѣ4R*'pQ^tך(>|e  {7ҵiBO{ՍXƬub9d]].i|9vbVgɦA@#W nNw؟Q@=,=HqLF:7@Xi!Z9|8u*E1 7XϵI$ ʪ[U.Uy?,֬`um&'V3m €Q տjZ_9:X6/EaNr6ϰ!ab6o:XMU8n~y󳉆I1ppc/ū)*Qfv 38FH.t:Gcɽ4v4"˛ƚ 'j3?ĎI?`޺b?w t!.̌@t;'WjHm>HQNswVK| Bb/Of=2a3A Y>@ϳڻ ϵI$\ZuT Y#ŘW'=^aϲWN ~k3{~ ^X8"an*<Ĥ@,fA JrYLj#;MSᓼ9u a~d'% \ɕw'&R x9Ϳ< 6}l.kYh? xQ{K)^,ohAb2 SQqt#ts |M$If4|d?!{);oeA>e$IfЬ.M؅~_@)ϰt"PVNZ^+G\MfjObƻ ),O&dҵG#D[Ǥ~>Yj%,Hr4n  >{RlN>!Fn˿]3Wɯ qz>xm6w.MCZ {ٳI~qHRzstlOAmn=wwij"L&5-Y/۸G5?s|M$I'ax,WЦU湬I$%d'R$I:'I$IR3ʮ*I$IQ$I$)AY$I8eI$I%I$IcP$I$)tY$IQ$I$)AY$I8eI$I%I$IcP$I$)AY$I8eI$I%I$IcP$I$)N♼uf=mJ๪Am ժ0 kZw> J9Cm!I9gW-:H-PeHtNQP$I$s$I$Iq ʒ$I$1(K$IǠ,I$IR$I$Iq ʒ$I$1(K$IǠ,I$IR$I$Iq ʒ$I$5P/(RIENDB`nxtomomill-v2.0.1/doc/development/img/nxtomomill_example_data_scans.png000066400000000000000000002535641511430602400265630ustar00rootroot00000000000000PNG  IHDR|LsBIT|dtEXtSoftwaregnome-screenshot> IDATxwt\} A Q(`DeuM_ى~ss]b9ɋc߸$nr,ےeUEuzo̹ 1 H΀1sv=k{+nب#B!B!B!-.B!B!B!<B!B!B!X$#B!B!BIG!B!B!b3[:n!+ŋ4&}DMlNsM"3Zč/'Gx/^H{_|cIvxܷ(}7M6߽x?ů&.g9]ќ SxW/Hg{_c P;nxg'w}/,LP흼U T.a Cu=5(J eqtR~GzsՊj(G/ (v{'ppFeA0w,Xs>P}$h[x[ෟK?Ӧ{玬x(Q^} ?y_#br|am,VV}cfcti'o;nOoў\>./2\`e-ؤ2Dž%b,d8 ȡ}7o{ U!B!B!5#/scm8:K{R4k~xõQمOLǙɌnX~_0}~}KUafİZ}Mc4~HSR!sK[1l{yK_?Ζ_?_ g‡ZQO9ا__<&P'_.^&ne7o}_8^|R=O?̧t?xB!B!Bq5!S L^dRǮWEK8?}tc9tsTc_Ï'aeo~3yo'?=;7_z^Y< |4ov_3 =x:W,^Syy7uQu.L?~6%N3Ya)P8CXhx_ϼ|{s%7OSjW1XPTd%c?c6+$"ay~ T!B!B!ĵ>Etm0gB >!~Af. hI9y!֧Qbn{Vyͼ&+|.yQm} ?Y8vR$ŏxO:̑4O{Qp#W"d|*=T(0R3kL9Y6K}L}7cWB!B!B!K'8.~Vޢ`ӽVt[35{oh_Qy?XD nrPHu}yL E:^/-U8ͻxn\^!5 iC<°U^G;R9xp|zh3>n\B!B!B!֑˛VFfг"tkYyxʅ_1ܱoq|^v;:e_x7߳ͦ䚑{/uGyw^MRʽyϼLLS3so]v:ou'(Ad3OL-Y3^[o壯-u3rqrK|`^糷}|?B)#xߧHAlSO~},FSskKr,B!B!BNtu$_=Sno_A~" !1;? ~GEjG!B!B!D\>y~~?ey"^L9yOv^>B!B!B!E  + a\>WPB2+1*@:{+,!B!B!]W<Ӎoa%r$~ڕ)U^)B!B!B!֘|B!B!B!ֹ !B!B!BW !B!B!Bs8 uCtWiL? [}c#Ul5er W/g`-$osO7G80Jdndt2KdlV15r|+ƆSLw!B!B!.qfߓLwq[{ gt ,IΌiTe}'c3B )\Dձ֍T3:P& )?bJ#][ؽF|əi(zqWt}B!B!B(?+|t;HMdlhC{v*_ӽyNTXO0+f ýyӤS^1P\= '. _-uTLs>\3Ӂ0g߻֚ S*&OB!VF;Od<+&X]'|nuPa¶{wޥy&TRAm/Z5fa3>2 y83Rmo\sqz`Z'n .lqF'Xqg|!{F{a"(@6thX`-H b5DpΆ lBRZb"]˖+q{Ix ZNG$jKP9֪|1K:#TQ1a1+X:Lņ lwచ1P J eI<ӣBj;[Ox"Zfn2)Wj ްF G̥JsyI XB!B!B!!S L)BB8a3o6vDb A'4v!|WΈZJ hd$cj>ekMӤ3 ]Pھ]f8쎒L:nV$p쨁*ڷQh5cgA˜SXL`ă_W=ahbB!E"<B!B!B!ιOQ+]̄z69(STeu3ةZP;(7 M:.'k.ɀ0B\yJ+X. UfsΥ41P8nnEƈDK)^D]`4f!B!B!\ ja=+O0ԲͯlvIwHR74xpʙS2hQb+޹r!FCn=k|pM=H8hr2Yҍvu1NӬ4va[N8E>?k$ɳ)BQz+/-Uv4,~*Jsa\wI}}ʦݻ؊Jbw5+%2͉ӅlmocWh54uTQd1P :k)4$1^/c O_hjd3J*Ab |~\!B!B!BlaL5m q!B!B!B!ȳ>G!B!B!B\]B!B!B!XdK7!B!B!BuNV!B!B!BsB!B!B!X$#B!B!BΙV-C?Eŏz"+|o nJ3hzU_6c;l!q=~+{)8dftW,SLwrR[u !֢eajVdh!fvTQVd@c9:sp:/.beWz\mU^~v:qeWk'֒<B!BqY]'̾'6)5 _R(|TH).ơD`& 5-Q PɞM^(4Q3'z%q'{!B!B\{V!g|OgvIϞb(􇭃=; ;:BǭWmXJlBTPJ)#8k7e%R#t +(Ȥ᝞Ž>+TqXXGXy/B!Bk*>S:֙~ay-e-T=sf:(PXb\~,r6R]VDQ EMp?'g(,oZ{|;HI /(o`tD4L)cݛh4/j[qL,i#g -li9R+쨧fعֲvsgz{h5&nET.?j+nck8=TPi3E 2LMr}(mj"3 u1dn1i>^UWj[^MYnN gnK~o/?3xv+9o.gQz箙mfW}D؛cw-˚iSdJr8v j*lر fy8ZJ"'yy*qjk`jqJjKPS:u`O8~@:,AfΖjn Q%Jml1Soَ,ym!B!B,+?}1PWqxvG3恝:pݝ1⿂oy԰mlhĈ&* 0HsaW)@ IDAT$ӓsM0cn# e͎jZ+4fF;|xfw,B24=/f)9D!NP38-_ֲ_#][ؽF|əi(zqNَl뛝7,e54WPnWch:BS^" ~R25eZJk gUYo>VQ.8hn,VRBql3} 9D F@\kٍS7IIs3,A&= Rnق3Clǩ )Mrj4I1{D٢sld<6߬YK`(cCIl5lnfra5n.[V%dvB!B!DXKgAƖN~['?d #/9.ċ!Ll^b:nj0K7M:i/~;aOBxVoxK@Us-C'4 ~R1 w?mŔfqMTR10 o+6;C)f^*_TZM0Gڵ0y'Yӄ=noܵig9tڻ$TOtDb)vibk1z%Yevύ[*-fxk*(Q} __Xvln!B!B.̍]ODtml1)=XWÄ́5&8!hȮ=;[y}y-uXj7@TVJKL~K>?~݄D LTLi`.sk;b c4a>LF4v\Sи]gD! p8& 7LzY2)doXa[ۺc#K~L,n˶ͥJ{7q[բt\h-ʪ-_Qt|U\YA4DƭlQ95{ي֬T_j::UhĨzd<5,9Ҋ.\JmMۃkWu]|B!B!D6§ -F\6UL&L&0 (b4c2P5F'L`BO/ @GG97^֢|YbB!E Qroy\-J2]CUx9l˗}w7d2O-8Myohr W.[\#耲xR}vσͦS>iהS4:EbA?gV.1^wp E!B!" ?WJ3~ͥ(ťZv]K?:棔.L'<`˹F<"iF]iN(`ZI Uyلф0 A2NxjwF*ŘXv; `ZZIV:Ż&KG4d\ƦH:>^_a ?5vlQTREBMWmg+j&&]o-MP2W(e^-_Qޭ߮QC17c  ő*V?{g3O]>z3/g;*gdJE7`qڋnl˗}okoye~N[:?dWyFz̩]Քct^w*>'.ƳB!B!^./ࣔY9̠g5{&Ў}3̬UшVl sBCNX܋QP[OQf4n!in":C5I=ti\㳄kޖo,DAkG%QϹ6 h-F&=AL)JrzyV4L TB-hQ|^E+h0Ohr!}=dzCnhqj*M1Q2;ķk03Q XUclny (fhZX0?Ϯ&FA}j.QW,S=Uj1[#: oǞp|zZGln뙁7e{[Y[dd0θ!z*ρ!2J5hlkoZ)ͭSPr=,B!BW :騀1P^eGס[ȃ{/jqzS+vb+*ڷ֬4'N] z2 Hshlш!$`$%U<~B:A?d=MaQEjkC3V<@lr6ڻ6hRPt?ǟ;D2zd+`o [ڶpfd{[ړtxCSt~c-FdB!B!B!iU2qm_)^1['5fiG_rX;[H=BJn'p`"٪7 lg*띡[.M,4߼ >6j(4"L;ќnku=?v\>_YYM!B!׻|>{iqQ.nkO0f<ɳ:I_{a ɆÚ$TW{5k(ҙĨ7T,FXSW8Oe i5M 26Q0)]'t #&ru\!B!B\ 3>'3;ŤgO1X>56p|u囥Fa,D@=3,STBS=Ǚod@`rVPHI;=gWz=.~ !B!Bʀu_&b#t\%"4 KlX">?q2߽F*~eiǟ;D23_KY;:멱"~zgfl,",J;G fb.(o`sG Uv F]# 37:©[v*XYMH2fms9G8OVOJY w\̡^Sqk/~`Φrʊ(j`%Uw;7pb;kI$3)&]KD/fk$B?4' `–JJ oNAY!l]ټFfN@^`]/gӕ`kNكv@ט\[o7&u\.%D9MS e%R-wa<Ē Osϯ>w;g=!B!BE>:;(S:n{b <2_&p%g~ jT=v^۰~p i~nbTг}(TnFu2T`!":6iK_Pluܲ gN ikfMFslф=Ή  (3| &nV5aj:;*/ioı׋h:HE׍`;;j,I[v6`89u4H⠭cPpy@yG;64=jl,Tص(gp`$ʽ@b,DH,} G[S/b{ YfsD̴o!{^s{ǥͱ?kj'GOtcvBr^<߬/eJwM9EcZVN=kwn,|sB!B!'cAj c˶mcs ` RI{zd~k+!ċ!Ll^b:nj0K7M:4x IMJgFl5PT=9K's{1r=D(@1[PTMWI EX|r-cw;jŞtq6=Z]h{̑)v-Llj: ͕<=KHaE;2f#bJ &JJ) kvz@ld<褮¾ZUҀL/!|sl\ϵ~[B3Ӂ0g߻֚ S#M7Ss j^xTPʈ2es绝B!B!p$fng/&S Nس?Wa6nq7_U(a"5Ĭ+BP3a5Dp͆ k8+ O„eq$Feۍka27PbJ[il{kdbH::AohuGZt`dɏIuiR%̹`Ϲ2KJitwM8J Vow(}+3봪z\$,%abbJs[;;l!%m+U+\|%x@b~{Ҝ͡Rn.…O=r]ԙvzB!B!י<)`Cg A&J25:Ibs;nPRV!ğNq"//3rNy|8[R3( z„r;$6ñ:[hRGԨA}j19jC\뫧Ig`PHwوi v|-3&L^ A'2%UKoU"fIk0BXВ Wj]_Yi -R&sͥR>iהS4:EbA|sB!B!\HQ+]̄zvnKRүİRΎ{h0-Bv' shs)NdWZNMw)ͭtv77W% FyĦt 2A}SI f8oٚ{v10tTU[1pVg4dL*er;)턧qnQVWo+rф9T5[}sj? ׌mn )}U|sB!B!\vǾzs+L,* :_HfsVμtJG{9Ī=F#y_i4ڭQ"پlc*J 3|Pp1/S(Rэ&,&X<ٻ^ei5o 7SWgcx0s[Q0;v)/Y8Le`(N}p0L/H`II2klkD1ř ~0UrH߼#|׮?uw-0ة*72Paܜ&59q3vQWSJ5{5^o)jdiW!B!B./ࣔY9̠gCN#LzB6V'?tY@#[%|ESl 1:"9L:P#&sPRr۵[wt,EV*1rr67 UiЙ (ZNeo,z9#t6/i 7Ł{$^\&t".%:GD5Z7d#e,T=n} ]hz% ,pQ)dԓI3R A*S1T]#\h6]~޿GfVknqf[E\O 0R"sy/vdw绝9Ja9(╀B!B!^.+cꤣ [i5en&Bcˍܼ*qWѼu((=QzJhLIA49V0uXKQ'z > PCSGE# q[E+tw4}d@m33p򌍭훸9M7 5/hxz8@GS#6QR ^K2{;9x4jhĐNtMpo*)0{[Ҷޝ>q&vf@P($RKmkow_xb"_?~1m%uKjRq G꾯A,'BT!+||z7 M/i<nϚrOp9BjbEDDDDDDDXDDDDDDDDDDD-tyi[N>""""""""""""o9 __s+=}J cX/-TgVEDDDDDDDDDDDd|};Vƞ=C7x,Dz?C _/|ҕas8mtc?ɕpl.ͳF.]j?v>tEDDDDDDDDDDDdc3i:ΧN >ޮcF8-E?.b' IDAT_W9 fL ^DDDDDDDDDDD:3|L5QXf1sM9!wm&/Ẫ."""""""""""V>Iy,L/0p96t`L]%)""""""""""""Taa̬_-O>oRyi9AhWMDDDDDDDDDDDD~N>arIbnҾ&`aC;O$(3_k&h{pH%ND'f7c}v_|NY?Mqarwq.Ѻ?,*8sm;nw4߹P_( >zqVTr'gx-6ơmo:]φRos(jk7W!s5o:Y7;rF2o=8򥓔{GPujr:\DDDDDD΂ #C:cux%«N)ETϥ*_ʛchgdIT1<⫤Lyq"OڡAqדڷ>ڥSܭ}\х,K'Xjv\-\<#)/w^[ 15`s׹#lqg㧖`X9)Gw&Zi819 ~6^Zh7-;N<\-] whr&~4ӥFG=61_j~XjbGkcEVS;z#pQ:aXXKakh JN.f5à/O`%ri.o=lJkhllj9N1$SVuvvEά\F "5Zwz~d`+ 1ԘatS/i3,[9G.(;ussǶ:ݭ hyxw Q.DyTp._lfUT/[¼~Ũofd`ԣ.Y-`Yʏ ?%V5U%U+atbj[v֟DDDDDDdGufػk|޾W M|Q嫭Dΰi`!lϟ0.S.dk}|6424ٲ,Xo} ;W!AdBH6Tα1?@hW{n[AhRL )|L)f-Ɂzr{.ŋ$ * /s' z:?ha)ef·sF.7VVf=?*bX}6l g4H<{R;?ͳ^ w3OG8'䯰'RU#Zw/Yv^6t+F EqkÙ vgTTr`X_jǝs: '){^wePaRA4WBbi62ݸAVqBYF:U/VLx%HE6[eIھaX3P,< #]˄EjQ*j2ӳF@GFБjTO `qz횵8vr;iyPϭ;k""""""RU|fY^yV H:YVSL0Loޖ¿$~~&hS$ʝO ]HfKH٨TD4:paDffe9I M4;q 0 lF{t9C<E8p9_ʏr3S16s7y0yLK{B0Dpwf>N8i0G+|4$X Y.Ő>c$xV54 /+NRJUrDX% EŒIU  S˱o2ۊ3/ō))"]WMt<xM[EU9I8V9 ԿGu_U?o+#G䇥*q۵c7ރv]+p֟DDDDDD*a.=a V-_KhM4DNF_E5ݵ11PLG{jPN˲g|wNx5(()%%p8kW*jeJ{*ztJZSh=oG䇕*q۵7WNZn;W?Ȏa{Ftx2aVk_Nxv8W_oO CʲF}s;FO5uꀲ;uh8m['ķ/bN 3tד<|lSp?9pewt|f|$>ŏK,mXUi)j+[h,D ^x=q[ \JmOGlN~6]]/kPN+]_gc|n6<ˑò;p)[F/5oPz5=arR1i)`gQ%*q۵gX{_A;iZgyMDDDDDDqt^{ٙWo8q5ax|h)½|,\Cx>`OT48ռ2+)p; evl'yA63DElʹ_~hi(Oo!#fzR`_ӆݶ ޗ*Mj|}|U~ho~yB]&KmwxEJ7c3RU^k-[[ QJOg=`=W /5D&>//45;^:9@Uo/",i8dX`A¶F:vhVgx,]/6'k*B5zp&_[q VG*l'l,k1?jٮUi=X.WIkCv}PDDDDD:D'f7龰z\͔$"K(^7pZ9F]+h/O}"PzyZBJt_fGʹ=9NʾJTOxl'omB$F`p| ^Lrzroi/=XUa;ydgj~Ԯ]zα.w֟^8A7D> `Y"Y=lv4R 5֌7I?Liq߆͌', &p߹kɛ>l0I>3ILN<.;sDKܞ^3+ ɸ$EzjWezϼ\;CeR f4]h>ć/kTOR9vs8ک=XJJIkժ?KwFOmFDDmbN]F?ʁ-q-""r{Cx7:)HhGDDPI>rOy`V 9UZMDDDDDDDDDDD-g; р[N>""""""""""""o9DZeW\jzqVT;%lcj=s=d.u3t qyenqwچb\(/BrNyoٷca3tr,;>?|9F|9w'n|X3I^4tf4t11 `*-L^9I I>v &M R-5DDDDDDDDDD:7Cͥy6|ȥK 1{lgto9Xn|s߲T>. VŸTW(^]'S~T >Ӿ 95y2d7Tk\T(R6Mo~0<⫤Lyq"ď^z[<"ϖYlHn]]:Z"e_NC~ ]品4&C4:d!.H+ w&Zi819 ~6/|1[k/g94\}\x'~;&h,'vԷ29MgہQ 3=+ 뿞׾.n‹t1KI6y}`N/|P_7N~W>jw}Fx$2( k xz|:GolE.w7}v8VX?CuItƇtLO-@tD,74Ġ+fwӸ9ˏsɝzwk7>aޝB}Cn+ѝ.ɋ]ln(Fc>D0 gsu5tV_$+,f5\g(_j\_:Zme\ R:fD A4*k ~?*O,GJ4 0aH۝w&}x}>2:;gG7׹4g3P,< #]˄t29POCnϥx1 mSQ _2\$ɑ9v|]Nt =yrMRL )|LvN1vي-jm<RfÙ v>dI%\ t Y ie騧Hfx;MA s%9pl9H-|Ea' [DDDDDDDDDDΒ 8'9Ͳ0kIgB:|}ߗڛ>`I'x )RדĭObxioqPC>N8i0Gw /_˗3H蠵)"]AσcV�kxid1^nTb3]z}ʱ(]HN +&ȫn;J&%%bqb v[ i8idz'(r8r'dÄc)kL,5T0uYfV`4ru:/<"f:O/lq?28i% '.zU?f΁ vR"$9Oc"?)==%%p8YANj Y fa3)(E2vv\*Q(4:owJM33_o /$2h!s ɗo_i8EՔY]#]tdzFC{=#L:I'pӯgȁiP,nRA|n'2RV-ٚc3b8lNx ; 8m[sr7zܛYٖ x60ĥ~.> H""""""""""R'iƫfL-m%Fs+-6L:f2c<}\F4K/cGp.`EJ7c#1F9?d!ORHGGFc2Xz{zh9DO{u3 0}\6nv*dV5D&>/nlgF 33DElʹ7wr4bYg6춭E/Ġ kqh:\LtٕN흆鮙o IDATӋ3d5^ȩ8ـD'f767~^Ͽ(LkWi-L w$"vtt`8fٝ׹)f1OcLV73E&(EQą 5#"""""""""V8рc6aȁ#7e";XhR3< 6Z;)׉ikƛ$t*oan&lX8b|{!0ţyV</Lɳ%z'wdHXa؜zA2(䈇$_M}@0)-s0]7 M/i<9/Q-qn c\֏'\ +j%֧xz~7op"H'k]:G5mWq#|`|g@9$Fx\v(=F$Q38Ψv8#|cW?)WDDDDDDDDDDD"i_|DDDDDDDDDDDDrZMDDDDDDDDDDD->""""""""""""o9 4#"""""""""""s[~ͥƃƋJ|qj'։(M.ʙ(;|gx-""""""""""""ێ7S߱0|c9闟~_}>Hf6}m oXp esi=5rRG|]nC"&BW2 Oӯ\DDDDDDDDDDDD*P=|q&;MVg9{3m-ٍ5vƀ(YY%mM""""""""""""J>6:&i)=e0\6^.QNc‰H3cfb4]I fv}\֎0p\U ^DDDDDDDDDDD䗬*>Iy,L/3lzv<Օe4_wJ .=JqL"׾?,D.K~7CC>""""""""""""'sNOY-r~hc'n8oN/݉|G'G?Aʹ@CӒYþ!""""""""""""Vlha|c/f&j`YO;\0XOTD> `Y"ٌ,(F2V$s_nqwA˹Tq Ոv """"""""""""'|DDDDDDDDDDDDryiGDDDDDDDDDDD-gohӾy -579mjNo?;'?8р||Fɇ։d{l/#޿4FO}9<6q3xȠݫ+9٥. 9ƏH#O0]!&GhHXK*og͐H9nNJeV ;^3lc`}1_LrjOӞ`#sZ Iz>4#$7Ie 5o6[ڵCe竏(e|ܙe~-͛Hʷ=oԮ}ooGTwhOX=!zU)o\xTX;/)?DDDDt{I7'//}77H\+]Wxo4^5Q?מ /MybbqtsAso^]3:2c0yO|z~xijS+7 *`42]dq>~g<̮89<w_Ᏻ7&URvbD2KJB&C2[|ozިUb5ނ;;N;z~Zz=JQr`|pG;=#xv"@u#W.5ƽƃ2Lt~lK]&AFs>3?yG}Awt8}]\1 .sq_Y3?x `O+ߪqYO-O)W]|K~w ]BiRl,_|X&4Dʻx~zģ1NV%>hܙr#+nqw 87=xBeF<2:o/Y[Ń(戅֙"{}|Q',nF<s]4\ea731KoQ$`2k.ep;_L拯Yp,G+>g~Y5.{?I7vOLϛt,~H #j8g v"VI~X_HǏtuSJE{22moivC6Ԇ+_k5յx.<SS{ʽzyّ b'M^;'ᡫӋ3b-~qqupܮ?3v Y~/5bZU2hJaxwy}ZV뇻 /,K'X+%"L qbsAfl8e}%lx9I3O'O}tjt3rl߹ !ӯ^t9TҾm\$-O/BTķ6ͫyÒζ*/Y{;v_ckwckfd$ : mn:k'SSiaϦgJ5[&_^c!h!_\e-Q+`PGCfgSKmv$ _;C yVW6X\^c%9;}v+qr.+aޝB}Cn+ѭ[xTǻ= XI~X_p&gGs<]at1c#trvvEά\F{X b67Ǜ\ɔH:L6 vYKK%LO;6"KRGf$_)A_J9]\k$9+۳ ΦNFJfWK-E =hfFzIxDZuwc_|S~b$9֣ݰ6y^~ͭ1ḣn T[jbGkcEVS;z#bLXX% K,i/v´.VʁrlYX uV 3w V[ruZ*yހۗjκ/P/qQX_a5eq`=l>~̽[~վYs||ccG3kL7KڗӫGU}^EZ+q+k""""r{O!W_5馁aɮ/I nyJI>Oބf n~'pԵsj+ч3,eo'L˔ يa`!G{"x߹B3d[b03\T SE@1#=m8S{~ʒJ_/̝t~Fw2ċWBbi62ݸAVqBYF:8맵Olt)ds0ȗL(fw\x%HE6[oNZu Y2&dYOZ&(BQ&/rʑ a+];Kc-FD[=D.zq'zNIʞ׽,k5mt uӘr-j.vXlK&2z̃gjqR] S\'k;V^マ.B=V=Z-X),VsiDDDDmpg5~$?3! uL>?fm_Y$W((,E"z7b`x}3$c68͏x?+6yvwz]&+a8idz'(r+Q(`(LŭNbv;vK{B0Dpw ' FhM:x/8! xRܘbmDrHbUdSr>F i^<չ %@,I 3wU)>3bf8I{_sRIG&G=>X7HN ͇Nzmk2|eq{Bh`i:/dz<C~䫙VUP?tDaa±g@:h2̬^Yh9Y+UN}yuf*fn&?QߞGǪ ],j4.W?^/x:) |ꃕ}- ~ep65ҐK9Ily ~Z[%`*=UXӾ5T2L:b!?*_,[xy͎fR(/勔4'{$;>a?.( f VBy;=Ęu!jծV<  m4XjEԏژ`buv(#=e6RGNC'TW\(8#Q{ztzy Z9 [r;~y Fc4=CLjVdMRy9pM }V(Q>hfz^3۾yty ؚikQ\Gf4Ll Ifh׌π6_3FH4f|V}9I8_}5V>~J3nxxl2mk]O>{󱝗3 ̘E?.D^1WA:cw{H׷IXawkbq{ S@X[.T۞va;onwH۝:<_TƏj_,)N+[˺h,D ﺾ3ۉoSqc_VX?F}s;FO "&.Nu["]*asۗڿ}^EǫUAX*UWҜv|j|< M=Ί.1qqП/]kjkL*53DEl6-MEbcZ-/7$Ӈ@du@q3FKOW+=!B{z|*Obi8p\ EL׮kڳAqGLtٕTƈzz9vRڛw/^2Xl/GVU^V.KG^_ڡJ8|;ƕdϒۗ*M_ /Bf珗`Mf/qL.lf|nn3ebS'c£:L$An s؋QV"/_h)%q`F}l)w rnІQaff|2md:ѵmrF/170• 0+fB :n2$٫,^f`>2elFcÙG4aJ*CFv86s*4ȨA@C'F?Td3iw'z צ{]t|L^c[jZ/"L23w~g)Ƌ/ RKzQn8~&3Vi'q3W*@o7P&lpl~|\hS_K-,13wݐ_ 7eq^ak5F1ȥs}ei>LB_S-s[ܾUa2O?LX Hi~WXk,C INq'r$9 R`$!|}%5&4R?Y%5Vɡ9g+?P)۷}έQ٬D9jgn߷}5nݭ3ezLt[ dN _tNzz N%u$>0o7ׄ}[;91H7#94.j=ţPt>.@ @ Fl@ @ @ qt@ @ @ d B>4n2jop;&@ @ @ AksjƗ_}?g[)ǶI}G׸ϸեKZtI\17&Z1&4)V.1^&R8Y*$)S?b[JxwQdS(me>X:#:_d#$+͵)ÇГ)wW&]#L}'׆>|[Exon5ҟ}<8 VN m>گ?-vq~4:߯gs>Oj3nVC7~&5@+Mc|?|5'%l~?aĴBSWbGrQIn)O:'Z&nce8S^cxR/']nuH8rbj|F^9glRI즐r:~g%NsЃ7wv)ۦIJ@X=vѱ3v}zVJyW,ȡn.geׇֹGJ]\1ÐztIJa>'`XW߉P5:/?z7ede.X6Pk_5v1ӓ\L|I/ŭ". )|Htzgs` ?]6lfRD:&}:Qgt8Gcb*BD_W,AMsʤca-o!V&@8GKgkX}]CڤgUR?,_fy ߾|VC3g ?yntkۋ{1d?~ ~.O 4Gp//d/NcB*U6ׂxLŹU*qK f /kvrDV^hp7fjK͈T-IDYZ yy^/U*J9@>ZZ7Tzzo3L|+j&+[X*!\].OX驦sj%*^t$S/s3]W< 1} &ŵȈsvG+e#q0nae H2[Vl$z97' +3}|zy{϶ID&B|Yn:0?žb~!z{pO&e(ƾ5d[~ɵ$#Crea{FڥgUUesY؀f)=eF3= m, -^N+kwdD|seZ7V?SW=vx.`3+#~Fz\ ]J gFh+Gk\*W鸥K/^ŕBplՀPb;?鞞V[b;Ȏnѝiֳe}=|zYѳ0.̺yIݪ$0+Q~d[5 TN'R*W)4i7Cۼ"_);8q^։vui%(  ڭ}~i|ԕUIAetcYݔ4oT^j%t~F\e67rH}?giEYKA.Mى/,~hKqD5Ci\yYW wd\}ֿ࢞rbpᒗgL~@|Dzs bܻ~"}LN), A CxkQ @x쟏1 YE-j nS) QPR,"{$:M{s"\fwuQC uFߋWN0>ێ'w2эS"F"#Vdg 9Ν?̒([ڍ߱N:Fz61 q6YHۮPWt~(b3CjK5 6vY8{hIT*\mu.K!I$WCtlz¤NXd*G*Tg{qdHҹG*/d M>ao;dZr2εRyU[ Nܺ"k/6 C2xhP[/Vrr{9x4z)6mRe{7=4#@5eq?+GZB WrBǐD>8|JX^(ҋhR)b^qJ+fno70)#O:@ QʒtG]mӳ=CfHx\7\ۃG6SMgگӆz߀;Aרoj Ɨ~"i-GCs i$qK 0ǢNK,%_g"ɀDұ{i}W 2<^Q q ~*E^T!ҢqQ+ughA{\ JҰN|hݴ+MC}Zgo?%Rq Tl\b[dMRJJǭV僎]qo14S5r(Xm_*E+NT5WrҡONzdn]1=nF'BG7`RY^AGUoW 3o"{s0 "tu;ޯ-qcj[;$2Vn Tc&He<?6t钪3MnSf@1{ٚ߬kUWTL8mNwZ Y2`<ߗIK>*9=C=x Ew4/J9s|kIx~:[{˾`Ҥes87A5ˡ`Mf/qL.lf|nn3`aNl#ƎՅG%G̒ǎy!ƫ u p37hlr3jr m%afŀ6[69vllx P}u6eHsVd253CJ2MFv12jc3;lG>h%!#; }xx֦& d&V3=8YMX-fVS:z-f,oyIYzMυ~(++e)εWqK'F?T|'z צ{]t|L^c{O5j9}_PVc\:ׇo4ȴOל=4s*)EVU,R] {hJTaݩo%ziT'n?:Q^/t:CF_yR`Go?5^R7R/4*Gp捓7V⭓n&d6tyXxKetpVJMu~/׸:{/',jRmEkEO}s|}JGۤdu{g:Xs[ܾUa2O?LX Hi^ N2dyz3 "F§߃`ǁ6H5x23k\J>ei>LB_S-VϹ5??(g<_M߿VOqn`/sdZ8#t Pl_^zlSL{0ڹFN>ً(Yo q iܜgMegex r&EH}WW}j&B?^pDfa9Ʌ3u҂qEunJ@}˫%zi8LJ|T"4Q]n؄^:}=kI烝Q 4+xφf350Ǘ|LCNDԉfhy'ggx6qVY>J\С-v\6~m4u(lX0ֿN4G3PK% .FY:FG7cv6V@ H'o#&PL5?~^"6|# LiH9`:omz2,>Z!`7aLD}&gOF@ yd08zbQ/ -pkY%@ NYDM ߈#@ @ Z 0ROm( @ 8bG @ @ kw@ @ @ 'Cl@ @ @ q=>6ldlL>ʧE^w:>}~~0ScV/䎼?'!R  C vdlˡT/Q><8^(ϻ.9H&BJBӉ~:U}:_';W*C4ĥVI YF|P ! 9U}kW[oX+1SKK$u}3\oߜqv(Fr8pKyd0q[ˤ3U혺|jI62}?5B8.L5C7ȤMfs#2B\A*eBсepO]gicO&HlAgNN@^]LcYPz]G&otxuu_/gӯ;Be'hf܎wEvPb>(Mn^~ox*&r1bmYXy%|4X YU<\rP3l{F2aF >0O졧w{zu(*6g8D\PQ26[&^'8tx 0:_:N;G]'hzq)t|P A~@U'j*uYguv2wt$V =pl2[eIrj"\`vx#hzzjK}T39BA)b*D?!9yb]~'6CRv׈]PϦ^~>q.߭JE&K^lA98x3S"ׁ]6LR|"u;y)C^cby]W>a@qw\u\JEI{^MU^PLxdP`|(+ SW?s-#eyW jگ P+_Af_CQYyޙW^玫PPw*W_Q>drYͷW{Msr(՟J*RP_ahm¼ְWJ@  txKG(@{ē#==:)n7\euf|##Luds%=#cY8XO\1X q/V7 R5+!P6enmp@n{Cx5+Ls {ATS+|û{]Z vK$m.^k*w?DE[mvOS9ɣ?ddG_U ^Û#? T}Nuwj߫ɻj0xH(eH 9n&%V m O4[GURkKjZ?x,K #vňDQqS;*NUS`v9ǭFy:yJoH h?k&ǁFW PZƇ.{h.u]U@Q^SԿ+eA@ MS_#|]B_!zLAmG?g4-y,K.yI=|ZtYHԩWMn%H_)E/SF z{WFht_ͩ,G //3z5nBXAr $>L z0BzMV9+|po-ʝ_]_< ƑR9@^v|-}:P-(Y]׌5{5yWRXՁ4' !^\b)YRt.zZQ3y8*G*}CZoM%E;q늬$d"AHEj,4;UꯡUQ'ønh݀RH98<:ǑrCU}eqm|(/rh]׵o\Q^Sڿӛ @ t1^g_N#=>Q2G?n~K)^-xoNjVͲ͓zsd8 #H~Vyz7T7nigy #Y )֖) }`=O2o']lHd@Debƨ`cN5@$,S޶94Eu ?^Rŀ(z&ܸF : $ Tdw]eʸ%R]֛ǫvZi8uPb_5gv'oA+8Z^ Wj,DTd(n[x"Go8p]OJ4FtPΐILH!oUqTvєiJ爧^GO/ 3!ȪȑȼnZ,QT~6ɓWk:/idV!-y'O(t\tߕw&Fu yw;ȯTh/Vј;$2Vk)p\Kְ:;C.kl7r =^I}Ag_|]xu2ކO{J8zј6{wˉ흉qQ'_ c?ǭ2t pWXP]7.21HPe|||=V-E9{)f$OV6gʒ-t; 8f6݇c/:ѵmrF/170• 0+C?=I\?bG6bX]xYB[M|deblp&3Vi)@q3Czn'b0`ׁd?ܠJ*CFv8G,pmː<9ej6qϾ\4$dd#n=O?vf*J {hbnJ?ٌVq8*l3o7,>l)w rnІQaffgU^t #n] 0{q}* Tzy{/iw5?3ɤC/_GJ8xќvO+|W_t1~ri/=].||pPKI6Q*n,4;_?R5g7~)M g0{pV$_}D8R.*يG&{h-yS3( @ x?hr=NoeWg/]@*^&Yu \)MUxۤdu{gAjC\1"UJdQb=ƭuf]BW/skaLe r&A4H6B=ȹ8[zHoH ]g2HHrً(YO=SGMCKTs#ix8Iys16*J9GgL ɬ綸}d~,zr@:`56ko7er(׋h?{yW%O*ON4C1׵ׯ#miw{#MÓ>l&=TKk^ gwUԝZUuNOXLOAΤdu)CWY͸ߚh_c>o{ҺǙ6@ t&̹(}gCYn}LL%q?r7OY/qDlR 7,|͠ y,AkТMD}%xi|P Ag)Nǹs~+M}6C׺?? TB!BTܷ_@ Eԝ@ :]p֑p4`M0`7N9 VM) %Mc+[Ͼ b@ hQw S @ tđn@ @ @ gq@ @ @ Gl@ @ @ qĆ@ @ @ p?? ]V g˩5>I`=nW6n|1Jluy/F-;߽Yq?_Nn M \&rd;y+߻5Z^ĥz9ފc?$?nG)@  s3 ?p%s/׮ӿPo߰Vuucotu|g ?ݿndQp<`㶖Igڵ"d&LP Wr\ЃDt`1Mn(ЄNӥ@p2vL\v@ @  =cZd[_~>ƸOՁ:w~/g %۰crٱ yK*–EgMl3@ @ h'8 Zjk*uY^?o@T(!G?ݛEQlc'ɨ=>gR*2̇Ogyv)׊x97هiB_+YX$^>;0_~0O+@fSns&&a.4x98x3S"[lx_K7f2Qn|=$yK^'vٰ H6"us Km{dGf /!(䈬VQ}cJMU?S/W?ǸtVKL =^PLxdPt$S9 )x:m_l1c;4|<<_)ͻ lx2N!c*n PRq(c"돌p M<64=@ @ N}L&_3qno-J8Q[?_aod:`@o0哏'E(@{}\"&rѻ|L1x xUor12ŜJ g!vwX. :0?žb~!z{pO&=&0r(Dxr(ŎNO5 d,.ΣdfDj3ŎU)oO o6I$UGHe[VWd7X\ZTCQfk#zDS0D72R"ZGw-rx{^ƺk_ɽK/^ŕBplՀPb;Q.G{쫨:O?#2Gn :I?[p:l<[ey+7ԐfCk﫱=L]}q; EhDt= 澭+ʕ1EBMæ<(͓J󕚼`q'xxw+)afRc)CE*Km4 0u1s6fY|̓4o~݁x"0좲V^sx36so$@ @p&h wd\}#:d^|<v^8`%/X+80< :JQݢ\&#!RcJDHdpĊX:|?rB{ɐR?[<^aoݓbE2E +3`|qXHR@\[O@"GvSz:b  *=՗XNbO$VzvwξyrT4xZ;q늬$d"AӴU`m{G-:qU,j*JRXՁ4' RH\ιBj_)Ci>P'fEB 2c&0ַNlCMWY\j=.E.Nt j m?z#|?kE%q1Ԍé6}uC#sdWwtR k͊R*:xdnG sMk IDATƸsذWY?9>qB!B!D+dǛW[Sdi20yrGcEX,eS dgVbѿdelU|/Ts:&;|,rڹ sFY@^WQ]m6,vo mTU5bE.A=tWVGƯЫK݀ʊE/CBTrNXFȠ~mP[(gs`JDGuMֲBN9BgFG ^_Gg)$h'}əyZ\qV,uN t>o;lS_3cBP΍é}lʩn۰i܆z{GѨ緑߅fٱ9=ogx-]$!B!B\>mUf*ЮJI WQ-t$gfw3n\0<&[ 6szUO M|ꬠ&,TY5JsٛZO :kckvkutjHE6qO:vp*NWW7gFEA& 2w@FD` ~IY`'bס.֯,T^6x[ I,;^r=z z|ףbUk{+'߻MFP$VݱChXWgǘx}!" 7!B!B4 >Z啠ofG _(/+QQU;<;ZH.u:;Sӡ㤖{>˷Y+!hV?tg7X1;xR6 )YKb O\)YZNn@C*:R 2d=pZ씛r9ю9s&GS__g7kcܩKW9x\Ջ@?5W<}pc.*Vqz4R?NgC;O U%d]ƾ'r۔Ui#/;2oc|˱xSѰXYH:Jۡ!,bۑBk%"!B!Bd' ڟĝ0Rږ fDaځd(RhJ(qNo(/)ge!GsK) KWLrV1\c'/5AWIbJ1Ut8|G}m%T:C0&J*. @}C9YEVJ>ݣ;ѳS13+#Lq9z\26qd٨,HJ˷ҥCA(SBT'CBy\u (_ b4kֺ[LfZ0!m),Ǫ(3SUڶ7t5c.5 )BG'GG4u"2 醇BqsL+uC̦FPxޕF\8m{j:ޞ@Dx)%~O6}{IǫٝR]6wwij" ǥ_\6߅ƻ{,㦞DwB!B!5|X! ՏanrL l9sDLl|C7kY!9 iQr[LJ@[ol94hgq>H2g^jœƞ NIKc{xG{wMQUQNQA)f',yI9ڕ(nPe')к<##>G"Xh&# FNb"ztǰ!Jia'f޷}VDcHs1rD7@$[Nԝ/172$XEՊ8(5^VBܽ-.:VbK%x6uqoƎ:?;مTǵ"wpBBEwRK?oǽR8ƿ'jC,$$IwWrS.Rsoc粹.4EK'/N(KOh۾J^Q"٠B!B!D3Sz k"EB!5'<ٳF*B!B!ZzMB!B!B!L>B!B!B!tB!B!B!h$G!B!B!B!B!B!D+'!B!B!BVN6%v>G2ơ ׋nܩ2PXa5wMˏ/d SQ 0S5N%P=Ʋhx9wB!B!B*9rGGg:ֵ?va2ftqo^"NJ^tt=WZ]0^o0_!B!B!Nr2DcX7<ع3I?uwՍ4ٝ4W>G@ϰ;)!rS !B!B!hf`Ufb4TtsIʖ_A/GT[ȍ_ɇ[.<jHf=6G$Wkf^.rVYQ@{Qi;a~ۻ!=bҎgokSӢO߻G78kؖ9??囙i, bhWo*Rk/i|s;0nf/!B!B! 1棹T'*:0DzN1K>{I7 /c0\skp?lv$Ϸ|EZn6pF?K9f,Y8e(>3ߐa`{PK gy^]OΟ.1?**^ G?/LĭMp=۲-c$^^tWK3w{:ڿFtk0MFbS[D $#B!B!5s:ǒU4ƍa f"'kW{][AEw(ŷ z =1ͩ_s=%;5v)d?q]XF/X Ĕ eT"hFk<]h=]SXvofy۩)!$<9KıЩ:P _O؀=L}c0;N;Կ0hѬvRxJmJ4HK/"B!B!Bqq:chן[FF$mg #vDḒv֮:yD/WDX{ px^CGުX/ٷ(ֱ~uյe1q=tѡ*z?'FRsz;~w8RyGz6 aV;0H,H<}HS!%Gܝ1,qR5FN mQ9B!B!Bqq2Mahg嫭)4v|.r["c-d}buô)T&o{>=APqZ啠o&jZQ6Ls5/coqZ7hrN1G( DY'.@kz8z *puõmA22Dz3avǴ}cEهǨ:ΆveWM#=-tjD!B!B!*8xB.g(;q'T{hl?Q^buvUplFEa6Lr1ibPU2鹔؝i #g6"h?M偅8=;>ޟ?f$J\.xYhE)fR|`9O(%]|vwatwGo- vzv+[yht:1_yT=:RSf/;Lyqzi\Ϟ@`_\2~6[Z-A%dCՊxtw.}z:-^"e /WU;n7WY-О_0/-IѢYtZB!B!Bkא8:k^ʥҵ f7,_o!6#GƑq6QUXbiMA}Tf3#x{ssr"SڵuLbgٯt.fǜk&4@S<,A?o*jU<6S&蹥w g ;=|5!B!B!Dp2ࣣcX7[9m$te78Ar(#]j+l'KRߕ*3Y`q=}^|1g_UOK f`M3s 3*?|۲Ͻ5^ǡv?%ݛiNAiQtq+3?+\xącx6=pLSiObLݞ2)m!B!B!nN|ld$NtSRڞt:oRȯ.*BcoE-#V`d>ߟ_O.;_P;Oaђɤc4 D1pto=Y@y[c$ax+ry3'x}wfN_ʱ3]!er2O^oWͣי1F|a>t*~{"NÒ?M=3%<.qdXvp>P } -g!PIzw%XO#֔Tġz,Z7#3_Ս;^\ˋU ]53 ^h(Ul(kv=P]~a+nv m zPASGH84=k.j=Ru |t$kso$f\G9CXi>~vOn7~I '́:3EG){ 67z苦٬V[7w 9P(V94R^إ%6|Kׁ>.쪺.B!B!Bk>ڠ1n$SP4;9_ZZy.'+\s#[oaӔ7(d+DN~[RP]TQ^EfE2NW|?eƛ1B}g.;' ?˨e;a}:vNb)ĮftNeꈮtP݇яkxvΛl6eot<4cvs|Tb„2 dey$5u'G2@ x#vfZC|3b}G/eתBq0hѬ@Eye)v/9_Oٓy|2ҿ$d٦111b*bWFN'HU1xOޛv.ҥ8>Ǚ4 (!=XidĄ0.bA7u-r^g<){8oYi)X=}ZN!B!Bd疑(IYˆŭscɮ?X9~))mws[h"%sIPiסiM6`O[16-Ō䏧EUL՚J).VJI244JݣzR\ߋ(ڲ. wZC}_Gs̏[V2a:3'݌u%υm\z^WqXѥkO$:3SiZ[I)~z)POUG(Ei쩝UO|}>8w4xޙ]칐-)u™:>UƌtՔKdޟS5cUVeX ])>B!B!B4'>6vXzM6&'gp1VnnWT '0@꺵 `amZ²t!jLsaGxvWvn[cשUiWj:Aա*?//fĺP9Y>SY>{fcڮge Ǐ w^VYNyNh唕WuFJ%J9>frʼnՀߘX=@Ths4x5+i6n4. Hg]`gt`9}ZH:]yn&gcCAo%A!B!B!Dq._ŚZHq9ET@]%W'#l-(1'M651aZn*|N(X0XI2M쥹TԻcSݸ_zIBC5|-7_|]9qP|Q(-.q8V:b`.H߱/8ZyE3g%1)>qýjLǬ4=nBq{B!B!8*(|TL5s_FyYyoTU^'J`\mFr[n֓c#t/E\pq2sqz8gc+.X6dXRUn^__n&'}ڐj뛌wbORD*>Gtkr\t\jS@х.t2Yj"[yb" r`QUu\vwu/Çe.d3 9¿RR.i+B!B!8xB.g(;q'T{hl?QXQ2O_L.=͝C?qIrQ#?ʝDpaΪ=5{LL?_gm9̛fq"~h|<I)1)>7܌&gm>}b &šles)3a]Awkȸa.&Żu] IDATg^#c9]4BrV~9Jnd; (xv: [iF2kO#5ٌ6w+[)Ȯ͠Q!D[XY:*-O7z֟]ߏME;80FcYc#i-e&?  vS悪( ͅyĦ}<<']N^3e߱@GD/PE$W!B!Bqqr ;u{cdSv[%\3l/#7஽=Vʋr9bg̡:ZONldSʃL7gJز99w35)qkX35\ x%aȍx謹,5@کGH -KI^xSù0]혍$g*;Ʋf1T?kt8bX>)]҆}u$$"7RS]_!B!B!E3L{(M_L;_eg/vrm~Ew0戮|xLq'S5>A>[WN._w:vu:'wC~]oW!B!BzM !!!W \++o[R:#:^db=\LJ-ƃm}ݯ3?JTz=(ޑp`t}B!B!8Yҭi-Ϊ29=^ЂRz"Ptr3o[J|K fTUhv a݉nSciݱ8[k .Q*zB!B!B\ni k7S^B%_淉{"B!B!B\B!B!B!ĵJB!B!B!h宛sB!B!B!ĵ\|WA ;zk9Y'8kL6'|b)5.xi}kx3| S4v.gO_/.jk?y[ Q 1q|"+ݝ&b<{ޛ2jHn$ VN{*Fz~C+ݙFu[M=U&m:?/l[ƈ^ _W?fo᫬z =)Z38s0!B!B!透kML5i/[wRڎ^C3fz&kԾ8RP޳ 5 u{@1|=|^:\ܿbŔv-{tt=ǺY}¸/B!B!B2Jf` qŊ]Ӱ@ph y{9P+s%`;I#o y_<)<>a"VʚJ"fpd# v"7b+@ ɬǦpszdrt bS:wy҉;X>k,ኳY$f`7ŤĮS)ͼ]&z+5e徹g+ߠNJg⢕o)=Sv=XOFNIŬ=]>mqۘ(L gRdrVj<|_7%(ZZFb\ ~vm۵|;ķwo oCGw;ɧۋG_oDqY/>Λ?rf4mEi]w,;Wd3=$Ψ(otɲcl>wΓGݥ}q)`|,O/|>3g5Ͽ%3wV %ϟ!?-L2B΅G\y1gO3dhgӥF@%9rm,0.R&B!B!9𱑑psh8}Oa>Kk{Cs,fQ%_GyV9CncBN-O. Zn ֔>&%|.}-L_=@@gJ_oe -gU;Ff"-w8~wg;stAؗEG){ 67zv~$ ]ôom-;Pz)|^@Jv jrAP);}K?"ɍw/yĕ[Gz/;GDl5?:>:ߵK7FQ}\ff[o܅>+̹DLʑb:u 'Fii{r<`ôr.c\dRB!B!B4+3|,;XAcILhv*rYfygg]qsU0tʯN1b C16*䊋ɴ`+DN}[RP]TQ^EfE2NW|?eƛ1B}g.;' ?˨eK\ǮI 5լݳѩLѕ*00b yfM~yJLY0PPA,2/85}dҰZt7]cV=~L+p/~F̷OpZU4n-u`<轝`ΨOs2a K7z0I>f/։$r<鼠'8sFu5SvF:q{ȓYNBٟ.%zXؚ_e_g{ $a_)k$Xi"{24 /W0@w ;fc9fTLY,ȹX EϟidĄ0.bmA7u-r^g<){8ĸzDt_~]'4 bDt](B!B!(>ved$Jv֟0bqkGAjgX- g]/&܋ww]$+HQiסiM?m@5][?#Oי5R\j2%ehhGdCzQe-kgSıﴎtG"&betfN߭(RC[/J6+FUN녖scF{_k#aQ3G>iTG Eb1s*\b?|'q.~lI;1MǬ0f?5r쩦3fTZ9 \%\"ݕDaXujihڹմjvPu K>{7zhm{*kcLbLl?Bq )*) r.qq\DI7@gJ-u%,Z퀏Jϩ;TycP;:@SѥΨJhWŚ<68nG9믩<< Sc;fqH3R x*գgUb7/͎B˩9wg/aF#tvfL 6٧ӕ/vgmxT|>MZU zʄB!B!W>mUך+LU}Q)n/XW.UZxdh&[8EkxUw}buô)TGCQa*mH|5*\:TߔA"q4cIP 1j*詝=b,r`[.3h6[Ь|1/⹙vI6[97ua/-3ftKi.4 gf#Vn-+&{!hX,vp͕dvGןG]9]*'kt{q37H/`k/Çe.d3MQӿRR.i+B!B!h1N|9K;N #Um?k&O|4L ðNcVl??5eOr'Q=\XJiaM`OeYn ,f&Yh?Z=wdnRJ w 7)ə)k'aô |q*[\JLDW2x(k,oK7ټkdx,'+FHj>O#ǡW)t N?vg֘|o.=*F{؊{IS8g&6q7`*ҍ[wJI{?=M}Q Qx$䲤+>5 #ՎXLb|o8g!c#7㩢`\,qS1RNeA 5eAet^5{:K/y|1hdC-҆}u$$"B!B!Bv],z2|g읯 OH|Tr޷:z<ϜT]#ds:HЇNg߮C\W~ox !B!BکW-{42W~[x\- :^db=Ч[Yuһ_gƑVˁC? _ ?`l̸fB!B!YMI7qf SD2 6Y>D|B!B!:^>4\ㅫ k_:t?B!B!Bk#B!B!B]7+!B!B!BfqUT.Fw>DEj+lLd׬ﭣ /d#7nےʝZ_7ɟ]aʋ0ջNc2⢖5*W+⹰h_k(ޙ.T+^__wAqW1_3SzG|C#|7wջZWQ<>!z(ٿ˖gVD.>ro#z+]!/h,:7B!B!tq7 ~CX mՇ7bݸ |Rg_נ\EnnZ>g77:ik.UNZLO >j+5*`l FOy#L%W :{=J@B`;V-gc9"d;F@SwSCcϻE.e6?/L{嬨w+x"T`J,಑\1e{_B!B!;Gj@CCk:t8Iezvr >a3XxA0/ԑ|Ûq~.Ь챤>6}J,ws $ب+x"YKm?'0$]K%{/?Cuo_FݪrNuT{6{^}dV`EvưB|Sf`x>9. 94K>vO?m;Ygџpd.yc&W]Qwۋ yvf.IzYQQZ>E#_tss4;_Z 8n*ciDgm%{ o ]by :T~xqMX-"5>gJJN;Ul\Ëk[dv$ƅSS岮Y]WXyy+S#ռ˳{_Q?cDzxFq*E{ao^/r[  yܗ3e4,Mv4t'>x9V[±,=WbB!B!BÀ"ZIO/z)t 64Pg5*N= `0<j-P> IDATǙR{s#+yE>z-l D3G_ƍ 0^9EoXM'Fa7ʋbdbP:h8]:i BQ^% |StkJ;iٶ.2~`SK􄢧9NPr~G#Pռu0;"*PC9oh{cp]w?ɽ1xb4@sms%z8xp4OaJ/iCb*|<"^fj^1>lg[dXcEYyƏN'=9/sֵSH)䅇su304Vr];ODi?TV̛c?(Q}98>`c0IOK±fcIim8?LS%<7H!B!B1xn6h*ylZƮuCHJ ^M)d<3| A@G>/HLt~lhWQYnjŏ7O0PBiÿvd}Y? Ixu.^#7kZaǫشSoȬOlO( "5ٷ¤lek \?u6/s<:h#,TUQyLt]N3ɹ?|QutL-gseo1wpWشp-Fm z2dj[5m3ڱkMz>5đr ٖ`1w@T,6,hln\JqNO`O?(dmagiήsS #]2C6[dėgj7Ruq fb,UV~SVѹZ %_dUFżdOάtV48y".M$j)a |8x/(9ژϟ-QVҌ#(84E!B!B! y1DaΦ(njcy*8B|@U%՜gG@GLl J꽶߬ۯCzmxtF ȤG0 6YiYiiu܊F ; c695ll+s}H 8N8i|OU4FchY mq-]B%ldVlb>>֗%M{;pWϊaP? ( PYD#7jr7Rlvt)7 Lg)-g~5$\P _i/v eG"vKV VTvosJYU5sb *Ǣ eky8{n>K[)>v 7zg#?Fsqp?o!s|C/#bc):h6 \|O8n^a7:F_=Lwwf;4t= `hX!0  wu7b?c3~AG-J[LIJ~4jZwwGP?ҕ|#08Ij֖vb>Yͫ\0#o6u  sis XS:CG%pmpOٷb/tU|r}^NJ.gXs)23a2A۬c=O\6ooI8VSe46W,^X~E۟AvqLguλ0Oab=b>|[[M14ZMG!|4vLzL\uP~kriYA\ucIZf9rl” UUy-f~O6xtDnR חtS04=2zL4 :a̘N5[,6WVJ荽QB!B!BSƳ@YAFdaTHG&QCvHLFiMYcEhAƍ2|:đ-,Zp >yʍλDa͟@|ͣ*"ד>͏bL/wCbZLQL;KOgV !B!B!N NMj î+}02~zFo0Θ%>=X|JƝ ؑ/_>},&B!B!z|B|B!B!d!B!B!BANf!B!B!B r'1@xt\q-7r x-L#5,[oO3/Cx{?~-ngu<6x x*H}#\>N f6O?+N9IgHBOte\K? ;͸\ʵO_7]¯yx1rᒉ<>}~s.N8!B!B!d8wOln#bdr8_+))9u'C59XMco]O=7-ayqT&3|M 1 zFISLt$NOgɼ(BOToBqv.7ʹ CN;wMd嵡ulcRe "B!B!7xgzRS`Ꝕ8*Ѣvn #bTu8KNNDA`[rٯQףtQ $cr6朮M=qO76|>D&r&V}P="pe1 ,{*B!B!`wRhN;n݁ig a3XxA0/ב|ÛqZ--\YYY,]}$Ow߹=K kܼ` 6 ៼HV;frO$ ARye$yߗQ0Ųs}?$՞7v>o`EvưB|Sf`x>9. Q~poX6w?Ψ7?q "ُyh\ukzFo/*ؙx&av?O>gEGk@aټ[{i#BTj+A7|{zOgر,E\J}q^v:msv?6m+7}Zȏc|Sul*ڑBNyN)o˺bb̂\ 1&tV syl6X|{:8p\?5w60{ϋyb5 n'^ k`sa̲+//iާB@:i " dʁnM ÒW7- ۖe^plv)PĪ8t!hZ><=.cB=lkaEYN!1Ρsкiy7diot| Tk'`I81Wrr LRˋx]A{X~G2Yfry\~~m?THmFg/9 wY} E]pn~0f鿛ly*Ew8b1aAGchv;Vjۤ4յҤ4:N~Z|Th@~\â y]ߘHS ~PRٝ]%+\/gN9w]dUFżdOά(ڰtV48y".M$j)a \}s"e=Nb57h4 #=6 [B!B!n8cȅ33Pf1dL¼T>$j{#&6q^ Z̽Z?WƍLg԰Lzn#f V'-ͭhhZ s0iiZ.ζ"=gİےk[yKi]En ?տۆصH*\rFfa6.în})^ҤNQm"HE37AnjPߘ9UvoAL#:vuXwבHddZͮ/}e`8#MYel> Wb!>KY& F#MdjF.+vq `O'N'oKjm7Kn{Xܿ *yɹд#մj*FxKg%Gg@1QkB0O>p?o!s|C/#bc:h6 \|O8n{DAsAAtptGsKI)Fj?,mvZ0<޲d:#A sYGWס-8Ĉh8raf~5J)?aQ,ZQ{/m=>Zˍrv5n]-|nJbFd9k8܋r%%2[f[)PzqeN' ʄB!B!< ‰(mؙXOcXD:ZP]=$33G Aym#] 7?OwLǡ(z&[~ {Tk-7h~ 珠"~+hg1G*aqdd] %,TҹdpwI7DyuFBq0ZocӚk+ H ZvU;F-Z;m`"T)k M $B9q8)-*C,a!@ϥȼ΄n3'L=l"< 89rټg$([Mr56W,^X~E۟{qLgu1Oab=b>^qE?|*-zR[?`ǔYliqиFml,Hu4} SpVUQLJ[֑n˺)tZe4vLzL'owP~krihA\ucIZf9Ѝrvj>`9ďK)7KeE<nҧc(xg#ohƌ[XBosHj;7W!B!B~a j;m"sglo7'dǝiOݎ*?`LR( IDAT(7hiCylȢװH꼻YzIv͓ w"xcfJZDď .ړ!\'E^2g]؜ki9j`WRq sdĪ4g0s#1XLr'<}x4Uiv9j`S?^[0sh.eI֧;{oOX‡hI0joYDĆ0zB,~mٍL+>nkȸ _fY\1;۱vf 2@ŏ UA~xxv6!\0[n{~!L̄vVUNgw='.g8UUs5=fbZqݷ?`Hi4ճ$fV bbĤhSLIߠVQGVI~KOzzi^_!B!B!{4\ŧ:f<3S9hcYY47_\Űqo95_>\CZ#%WǏo7X]-y?x#ШhO~{!F~,1 ʑu2`_Uƚ׬̻OR96vRZ*y=;3͒Hn l4/{I%פqOSSϮbŁ0|;ueˋMmdN5_\s ҡwMX0Y<1ϯ>HyJkFԓ9٧pђDbÌj%~fk6@bt {ꎝ=}l1]R>}0 n;^4ֳF\FK噢v*)#B!B!(F > î+}02~zFo0B!B!B!B!B!B!J>B!B!B! vQiS>! 6s6 G>s&fBmkr66n+D^IJNoM~HB!B!B!w飇D>0Ud㦝UX KĤ Kh ٩PY{(m fĄdԐ_ڌ̸yx B!B!B!9tBW擄|{p #b}m(i)kvqUvH Z> ]i!B!B!B:=|4G):NUPLNTB{m5MgҨ4TTѪJГXQ$hun8sKH@!B!B!{< 8ۇoFɀ)$ ћw[u(⣠(=MS B9{Ha۶r> ~!B!B!Bxr+Ԙ?{!LWP4l>_{Z4kǟ*Z~ML'8))(2щ:+8; !B!B!BxKc&r Y&JMcwTb]iaDM@`8?U4s2!'B!B!B!DpOO"ƒڟF#MdjF.+v ߰&B=;(6-t/ !B!B!Bxg]8Q: 4wh4:E@mb(BX[K!Xdh+HP4ciYFB!B!B!zfZ;m`"%<F[k13wԎfH:km鯽sRT4!B!B!Bxg3|˫e촉̝}FN@|tΙFK]ZQ)Pv*-̑p4!B!B!Bx{4\ŧ:f<3S9hcYY)O _Z_N[Q܏˹M༩kB!B!B!(Fq|KWjϸ !B!B!4>2K_eT=B!B!B!8 x >gx?twZB!B!B!\M!B!B!tr-&B!B!BqB!B!B! '!B!B!BAN=$a>j 2fϙߛʄ1#F]u#=v21}\fS2ӈ hN}f!@ҿ7RnQ)Aݐ454}qS2#3DSh.:H ^L;Eխfphn_-z伾oG>R2;)zMJ/\8t8'$sT27WR}LU.kT~ FmʋQw/?u70ݿ~sO_;˙q?}0'ϵr9ٻW: L3hۗŷQ22B.6u~K"QHMXG2eorlɇ[`+[40`臔SFyju9)|FFq o CQYьc>ԟzb03d#`YZk弯r_7 >{;lW>O].'yN3~NI?y|X^}׹y %-3d.4F m Ȍ j`VsPhdN`rCH[3MPƒ լ .Ni j۩ܓM$|M9ϟ@M}I}'&KoSۮ|֟ߋ;]%b0Do/O~3J< ‰Ph/HRiuRC 9kʦ^=|$ 3gRU L,N7鄕fnS# 'r%k7CbBd&?ݲ)mlڮ/T"3kJ$Y;Ʌh <1H"(,u+됀H2Rc$π\þ}K_;{v3$m(AzF p?q# ZkGZ.i8/}EɑU--Jd0LΚ}|srV"0k C0YwSm {M.eDMa Ĥ8ײ/6Oڊk|\bӞ"s՜tP[\#^\,gXn+P0E11}CC W۟;L.H9^ĤsHМTŽÉqrw oJH"3ҔsD2i!T:k*ݴ u\z?U>w#8!iiػ/f}ڞ}~wȯKבad!kWk"3ß (<`nWt}b9x;'vBcCQ,-3"c(!j3(l:rڮ\z6/;_7/g>Wzs|箶W {yU~.v40u;P=1MOj{4Ӯ\}8ٍ|xЍ|uB: (z EUHU'*zB‚PhBSB ա66b@198-4kzBCP8xj eD:;Y(=HiiqC,l92PI|BDNx;,Laԉ T`YIKiná#26 j;O5SZPݎf $.%lTQAcC$i&[ $%3Q__ϓ!#8{LbUc#cdzZ=ϲ11l߲gc3? l0i ^\H|8FzYkbφ=j<UlYC M[Գj&oW1V͟#8YO֦"nRj}x=?W\o֞c?1 s)9HPXC(/\Ї>Rr5đIǔ۟ [~.h-*J`,S&ź75@a wޮ7o:"Gt=Ц# rSW=LGHb:21CvU{ύnԇWA`t gQY6tav_ׇa'\͇GnctfM#xd:ičv޿~]·.ϫ`xv\h/0wU{ayu]'mTէ94#-3( כ .gw^'t?_a\~E< h-X5FϑN6 * EA5:3j `. [ & $gc3hΚI3rr2{ɯWQ4B: 1$Dp3(6H]Շ@ǐXBludSc8GUc1d$r`Y=s5m?L-QvQ DaI)gn=_K9[rkhրIL$җ_MZ(ΫdyI -\]h"; 6G1@mۋX8h)-\Ίׇwfs.g%(0];ET[:P_OGu]GgUjPР4B׽\4zsTa E].vyraV!λd;Yqu}џ/zBF2-ݟ{ȩv3ӕ6k}0.9 [}> Ըz]zHr>4ufMԶbB!-n+ןWKz>^^~t-}n KG@sWۋ=<j;.*tCu4Px8r>ar8~k_U_u>9QÀO=f&L9PЈ9sAU548Nsac밣v iZ[t4_gNJyM fiZ-HΌ@ͧtC MwKGC#Z4a~`>aSZ_}pڻb$*)aa1PtGHNmm%НDxB{eGOrr5ÉgnhԡRiIp'|"Juf'l[RÂP]}.ׇ7s YkҬƐ>zꛬ;=KuBHkvw`'H{\?мoxrhiry)܏{;}sBpb"C"{ЏkwUsloA3zp}',g@spj8p8AGޮ<+zOq'~r{KmwUo')z??n~}j7ΙF\?ہb0Sw(r>QrUw!xݞP޹5aM腜{" wP:m*A]7qF t&|Ώ¤f;RN ư;i͵ #3g687#z zpG?d;Ug& v: +d5%l3bS8BҚ+ih]KEǠӰۏ6@u<*}NN0Y9bdӰQWa@r}x9^>7Ҟ۪ٹCGzGk{ (hpw.UyTգ\;}C h^7OoNlo.rxr9BGHVbI+䋿h>׀~w05A] 軜ɇm9T(]{rӮ<+zOq׉_wxJ|8߻ݪ~ʇOt?wۮ=_ߣ~2LI`q%Q;)h<Oخܸ~}~k~~w@/9 [mb(BX[K!Xdh`WhWhQхԅ8ihl>5]}#?J̘v?_f^SC4bi "LYC+7ο;e"*:Cm4LP V΢Ý_ޤUGguGu؊8u\Gt_8Zfа䳵8K::ܻyZG?7WS_ f3zwC\h^77oݓvRy]-ޏu*l0:5qI7sg St~U Ψm;Ȯ^˹!=>y}r9q w7뭟{_nq=ꤟw]կU'u 8w.V74{ݗQ9]|5p\<]5XKND~R;ihhj#QDsPGNUyZX2#JGTJj-Ut ٬ZN٬X@e]ݰuyn{7PV v[XᷠU磿\Xn@Tc|JEi k>7_|M]rS_]/=`;@p!ݗ9 WUi1$)&:ρvG{]TB8丐> o{}'~!}Oy}s3ti`\@y=q/|gM;BI!ap0FGUν+ߟޓOi')u;|WDdsŘHZPM%/YCWTENL94r_C箣X3t+|5X,8‚ lsX a:3fghnĒHڳ_䢡mC9fle BoLbRII6%(^ROi`&0QWEy.xj)nT@iAʐ )ڢ%|H*=H|IDAT F&$hfM$rQ(: Ϣ]WK3B;(?w[фII"53Kbb;5@Noq G׆~)g6eh,C[9fTtZl]I'ʪh¤Tiwh  &:6]] NxrQy>|??MҸ"Qބ@ɰ0rZn*Wm=z^zIie(&禓߸'/|>~UWo$sl"crJ ;N{N-i}V۹z~MP yɄinx}wra͇+?Kcsoϛq~M=W[_/wR`|j!;uLgOkR~};r~|.v>}uGwHctFAZUvVeدǸiԖ|ŖUjWa-1q8O`T $6Ps>+f;bNZ.2?6 ˆ՘ۚ}CôT->@d^)sTZvK YIN1wbjn71:9QZʁ4r.(4pQ^W{Qv #?=4LM=LxY \Mw=Ĩt&&i*XBE'LL2MOc5iB}l8vzڮ?Z]NqD:Hk(F6j)i'+3C):67sCu>|]<9o,mOrV!:ptblPg^j뽯)+]_:^5+7S盛\z?Ϯ޳T$nrih-<[|>~:xbTk JZ}ҟ߷sJ}7˅χٷ)j؁|ү~>]UC+OTsŞ.Z 'srVQԶ__39/Oy}MNnG/cdf2jb0;B!B!]GEl#uuR]ep8vj&e 91~xL㠾h on8d7sc͛|Uu*%uZ\&nKirlѶPeo' ]wo9_6vF?Iּ5/ϋEe|5]x5=OV~>AVIOU~=H&Q~:QQhu*Xasݩee`|ʎ#6o_oOm#g|;ihoU ]˯UpŜ9#=e '-JHж[(-<?I;1>-`Z#V卍mIpjKbZ~8Q:;U{+x\wB!B!B3|4h4rq9q#<2 (sRԧy'b\*O$db\>ʺmO_++#G)w%q=!<| hpQ0 yTN^i,-gfh(⟿83l9̽wľ g;IOg~?I]-?U }/0&^_OVؙs"S溓.`y{.A! Cq5Qr?{gQdKcޏVW,~@fV}ogatM_MySzPtDr N5N*\\u~3=O?VDC!B!B!%ϻ҆ɬEg DF+ w)ME=`\tƏywf>KW,b?g*Vj>}rOk-̜_`ВGN~vQ-ɋ3g+bK@#EG!Q>fV!B!B!.^|);Șc"Ghbrjt. => P~:>`f׎3ܶceanX uLґYW5x6sBDŽpM34HL#CmH0|~9S] ymgmvqyh=ܘ ʉh40=/ǻp];j"7~F6Y7S|Rhv!U z..JWN 'UnmZĘ99,6@B hlDEx~DgM+f.A:6R趔h.7ZP6߷6QTaQv^hB!B!BKK)}ņ, m40BL&sdɄ O$Qa:ʖB7:<X 7xye~ڴż{ZkQ[~>ϯ!<*bӝ4al4ckH C Bbu,K`]|9\o֯F!Mg/KyS@=;{%WyZu??mةMJDE}VNkY %#^}[ȟ־u|2{괸 K!B!B!x\ʶ֊%| h|m h⋧x圽Xp4cjWںg?1.kgR!B!B!.1|:ʹt:*hk9Nx?rbA'rr^00|Dgs`G}~N`1f0⪉Dw waj5F 'apf l(X-(0"€SlD`_ՖMyNJ\0cp88%q!EU/gݗ gҸPm:ggl#g6K-1ErO(bZt덖Xn28zZ]juhDI92wwjڎHaBqR9!B!B!OAq1?惚lNtHȞ0I:o=`B bRIdEEƊ爊JT0g /ਅ㇦odWǨTs0ar ֩!4.ӸGMǺE5[nq|]qH˄.{NjvBȜ8.IxuM赭#nm1;.n 5J lLY0ёTHG |Pc $<"GSTU>;t,k.;ZZTaL[r s =yw;irh46nK[Z'O'gBfT܏9En7şvd^FBu?6:= i1wV,ߖvѥ2w| 5 []ϓ b P8[kn Y)ڬ%*ڀFGW3Lkw&̠#84ļTޖĠcGY㤺Ja>71) ۸uNU>=me0%Xv,Ƞp ,u>zgf1ܾx7^Ep׬^-]+: P"Y6fOigﱷT8𙟱W~5>=s/#Ve?7pj$iMd{_|T.*7cOeLp +htnG1R3X<79éY_ɡS1:)R{CFcs:J[u6dd޵ N VQǗ[+I !B!B!wE FK"?lS<~C|-p|!B!B!>GNHpR2Im`B!B!Bߐ =wE1y"[B!B!B!Ԑ%G, /.|!B!B!^B!B!B!EN B!B!B!}#!B!B!B,uIENDB`nxtomomill-v2.0.1/doc/development/index.rst000066400000000000000000000001761511430602400210320ustar00rootroot00000000000000Development =========== .. toctree:: :maxdepth: 1 changelog.md design/index.rst create_your_own_sequence.rst nxtomomill-v2.0.1/doc/img/000077500000000000000000000000001511430602400154175ustar00rootroot00000000000000nxtomomill-v2.0.1/doc/img/nxtomomill.png000066400000000000000000000070721511430602400203350ustar00rootroot00000000000000PNG  IHDRkkp| sBIT|d pHYs77dtEXtSoftwarewww.inkscape.org< IDATxUU?o~2@*" MEP4R__+\[Jb.QHM`I!j 23"8ϙwc߳}{}zyߵzwg> eQFeQF#> ht9x4rs80`8$pñ~nN拃O_PYa-g>gPW\7-GݦP7߸`YkZXp|5D`";T#l |<4Yw}o2x"4s|k./GZ։>ܡɾEn!QxgݪZ?@2A.{g9~x8Z/z>6ˬ9S~{\dA|㗬;o50`?xLV}C>Q_fJ_f?S@F4Sۑ."m!*?ˈQP B69A7iIYuÑ?PEYSkfiSp$g-`؟y+ؑY`[7EdkHw}{(˚BS~Iuvd?! #.Y G=}9o"BYү[$uYNd܌7'i6ZXc4 7v_M2}#r&qV[ԎZ,'8ir-%2Z_"AM=t冠y4pwD52wÑ?B5:r +@!1;|ڹϒ nd5 *_gEA ߬u_IuF2B}Xðⱜ {c45 q=i@> rU-b:'Iց@7Y1 BB*.#G 1PNd~vqCwaE /d5iw{('l+d + cʎ ^r#~K.7l@f *M~ƃ";Edu0b-jQ2 p9+G?1ۀcɜQ} !J=iz Cy\XwP؈Xat-\Ap)lY)w9LX1@#p4񳑈(+U(C5-AH.NE88YD>XkQ*ĤQ'/AL {+v~e; y\OiFLyG!B_%5?m[TD]7rE4-S||eGBr򯦚0D 9zܰ{Yv*ɿzrnh61 Kf!3ÿF,Dk.Hd T3'4,Y S?]M.&,Rkkyud&\׉7}Y! IJzLj02`2 ,~v&oA0auv)6^~p*j L5Ȁ_ap#bFn,ةGOD4~&Ig%\,#-[4Y aa<ҵ陶MSM~(]iH4I됄 # @gs `,b8gOCʗ\x7|ʼ1BڛHpCjHeXd/GƩ#Ff9WDr 'XrZq=VKrt"%8Be VYOG8\o݀l=oadYcW͊wq1ĀZGY v'c?jf 4QHw趆̪Y_~ byE{ /GraAd!cj"Bd)]o\ÑG<+RM_!~t[5դ5'4"λ_07#M2/~"h@ݸBC|"Y^~jA|6$1 (6d^27)#R7~tW#N?fONM0sر{GͰaê++++z{{{w^:bÆ {VXѴe˖&d't 7!EC@%‹fn3[ߍ ӻ׬YoQ.[l1cIk&İujXd)BEV%vW2FZtd2SANؼy .@dHX ajǙ\d>}ܨ7-ZP-UV&O,7tDHO'׵6:qrk6cFDƍ&IVR˟5,]\L<$ZȽhl+.FtV F"ٵ3gXpD"sر%m$L_`Aa4dQFhz#Ͽ(PL$Ɋd2eS^b1`L2eaɸI돈cnR>p_[[۷gϞMqc%K!"G#l0ZZZ^4B=1?p"KMR]q3֛H0{1o޼Uq/! ٹ[I&Az.0 0*++R;q+WH(7= MMM% o Ə?IL@)BP#G}Bd$Lg7C܉ #`.$v/^|POa;_Wyy_H\(h7 Kaɛԅ [0x<7tH[sG {v;p7fv M"E'g3`ot8H&V/ntq IN"yd!!@~O}ѬwRy R2ZC?^"!ZTUȌ`A֙}Y֔ՈEՆ|PjP~דU qYIN~ljqC'"nt$4+I-1fls9RtWz)^PfYKǬfʚUB(kV a!ͪFޭ5 z  SSyiVf E굃s7)t\.df%=fEEցdҳa:k\cVT`/C̭D5P&]LG܋&a`*640ώ1PJe*!(R3xF54+jYyuPz`鍀ur %A֝8'`~;i1HFT)n0N'1Kuu8GMV##e ߳vx[L 4 dqq6FtSac#Dd#~ۭ$+7ĉ2VbuJ,7WpfMùP l5+N&2QXXTG#7p8QFeQF1˵!IENDB`nxtomomill-v2.0.1/doc/img/nxtomomill.svg000066400000000000000000000145251511430602400203510ustar00rootroot00000000000000 image/svg+xml nxtomomill-v2.0.1/doc/index.rst000066400000000000000000000014251511430602400165060ustar00rootroot00000000000000.. nxtomomill documentation .. |foo| image:: img/nxtomomill.png :scale: 22 % ============================================ |foo| Welcome to nxtomomill's documentation! ============================================ nxtomomill provide a set of applications to convert tomography acquisition made by `BLISS `_ from their original file format (.edf, .h5) to a Nexus compliant file format (using `NXtomo `_) .. deprecated:: 1.0 The `nexus` module allowing users to easily edit an NXtomo has been moved to `nxtomo project `_ .. toctree:: :maxdepth: 1 :hidden: tutorials/index.rst userguide/index.rst api.rst development/index.rst nxtomomill-v2.0.1/doc/make.bat000066400000000000000000000014371511430602400162550ustar00rootroot00000000000000@ECHO OFF pushd %~dp0 REM Command file for Sphinx documentation if "%SPHINXBUILD%" == "" ( set SPHINXBUILD=sphinx-build ) set SOURCEDIR=source set BUILDDIR=build if "%1" == "" goto help %SPHINXBUILD% >NUL 2>NUL if errorlevel 9009 ( echo. echo.The 'sphinx-build' command was not found. Make sure you have Sphinx echo.installed, then set the SPHINXBUILD environment variable to point echo.to the full path of the 'sphinx-build' executable. Alternatively you echo.may add the Sphinx directory to PATH. echo. echo.If you don't have Sphinx installed, grab it from echo.http://sphinx-doc.org/ exit /b 1 ) %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% goto end :help %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% :end popd nxtomomill-v2.0.1/doc/tutorials/000077500000000000000000000000001511430602400166715ustar00rootroot00000000000000nxtomomill-v2.0.1/doc/tutorials/copynx.rst000066400000000000000000000027251511430602400207510ustar00rootroot00000000000000.. _copynxtutorial: copy-nx tutorial ================ .. article-info:: :read-time: 5 min read :class-container: sd-p-2 sd-outline-muted sd-rounded-1 the `copy-nx` command can be used to copy one or several NXtomo contained in a file to another location. Examples -------- copy a single file (with potentially several NXtomo) ```````````````````````````````````````````````````` .. code-block:: bash nxtomomill nx-copy /path/to/nxomofile.nx . nxtomomill nx-copy /path/to/nxomofile.nx new.nx copy a single NXtomo (potentially contained in a file with several NXtomo) `````````````````````````````````````````````````````````````````````````` .. code-block:: bash nxtomomill nx-copy /path/to/nxomofile.nx --entry entry0000 . nxtomomill nx-copy /path/to/nxomofile.nx --entries entry0000,entry0001 . copy a set of file `````````````````` .. code-block:: bash for file in *.nx do nxtomomill nx-copy "$file" /new/path/"$file" done Virtual dataset behavior ------------------------ If the source file contains HDF5 virtual dataset then the virtual source will be updated to fix relative links. You can also use the 'remove-vds' option. In this case the HDF5 - virtual dataset will be replaced by a standard HDF5 dataset (getting rid of the virtual link). But be aware that with this option full dataaset must be able to fill in memory in order to modify it (at least for now). Help ---- .. program-output:: nxtomomill nx-copy --help nxtomomill-v2.0.1/doc/tutorials/dxfile2nx.rst000066400000000000000000000036431511430602400213340ustar00rootroot00000000000000.. _dxfile2nxtutorial: dxfile2nx tutorial ================== .. article-info:: :read-time: 10 min read :class-container: sd-p-2 sd-outline-muted sd-rounded-1 the `dxfile2nx` application is used to convert an acquisition stored with the `dxfile format `_ to the `NXTomo `_ format. To call this application you can call directly .. code-block:: bash nxtomomill dxfile2nx [input_file] [output_file] [[options]] You can access help by calling .. program-output:: nxtomomill dxfile2nx --help Here is an example on how to convert a dxfile. For it we downloaded the `tomo_00068 dataset `_ To convert it we simply go for .. code-block:: bash nxtomomill dxfile2nx tomo_00068.h5 tomo_00068.nx Here the conversion will set default values to parameters that are not recorded in the dxfile but that tomography software mights need. Such as beam energy ... Here is an example of providing more information to the converter and requesting the converter to overwrite the output NXtomo entry if it exists .. code-block:: bash nxtomomill dxfile2nx tomo_00068.h5 tomo_00068.nx --scan-range 0,180 --distance 0.15 --energy 14 --overwrite Here is an example of the NXTomo entry generated: .. image:: img/tomo_00068_nxtomo_entry.png :width: 600 px :align: center From it you can use several tomography tool like `nabu `_ and / or `tomwer `_. .. warning:: to have a complete usage of tomwer you will also have to provide the field of view of the detector (Full or Half) Example of the reconstruction of the middle slice using `nabu `_: .. image:: img/tomo_00068_nabu_rec.png :width: 600 px :align: center nxtomomill-v2.0.1/doc/tutorials/edf2nx.rst000066400000000000000000000125361511430602400206200ustar00rootroot00000000000000.. _edf2nxtutorial: edf2nx tutorial =============== .. article-info:: :read-time: 10 min read :class-container: sd-p-2 sd-outline-muted sd-rounded-1 the `edf2nx` application is used to convert acquisition from edf standard tomography scans to a nexus/hdf5 - NXtomo compliant format. This format will also be stored in .h5 / .hdf5 / .nx file. For comprehension we will use the nexus format (.nx) in this tutorial. To access this application you can call directly .. code-block:: bash nxtomomill edf2nx [-h] [--dataset-basename DATASET_BASENAME] [--info-file INFO_FILE] [--config CONFIG] [scan_path] [output_file] if nxtomomill has been installed in the global scope. Otherwise you can call .. code-block:: bash nxtomomill edf2nx [options] simple convertion (no configuration file) ----------------------------------------- To execute a conversion from EDF-Spec to NXtomo using the default parameters the first two parameters should be: * input folder directory aka scan_path: root directory containing all the .edf frames and the .info file of the acquisition. By default it expects this folder name to be the .edf file prefix (as `folder_name_0000.edf...`) and .info file to be named `folder_name.info`. To have advanced options see lower 'advanced convertion (using configuration file)' and on `dataset_basename` and `dataset_info_file` fields. * output_file: output filename which will contain the NXtomo created Sor for example to convert an edf-like tomography dataset `/data/idxx/inhouse/myname/sample1_` to `sample1_.nx` you should call: .. code-block:: bash nxtomomill edf2nx /data/idxx/inhouse/myname/sample1_ /data/idxx/inhouse/myname/sample1_.nx Normally the resulting file should have more or less the same size than the initial directory. You can also access the help of edf2nx by calling: .. code-block:: bash nxtomomill edf2nx --help The result can be displayed using any hdf5 display or using silx: .. code-block:: bash silx view /data/idxx/inhouse/myname/sample1_.nx All the .edf files in the origin directory are considered except those having '_slice_' in their name. The algorithm selects raw dark fields - darkendxxxx.edf - and raw flat fields refxxxx.edf. However, if processed darks (dark.edf) and refs (refHST) exist, they are stored in the destination file instead of the raw files. The names of the motors are defined to some defauls ('srot', 'somega') for the rotation, 'sx', 'sy' and 'sz' for the positioning motors. You can defined different keys from the configuration file. If you have 'redundant' keys to be used you can also let us know so we can add those as default keys. advanced convertion (using configuration file) ---------------------------------------------- The conversion is based on settings (defined in settings.py module and EDFTomoConfig class). In order to define: 1. which key of the EDF headers should be used to get rotation angle, translations 2. prefix to be used to deduce dark and flat files 3. rules to compute rotation angle if we cannot deduce them from edf headers 4. pattern to recognize some file to be ignored (like pyhst reconstruction files) All of the settings used can be defined on a configuration file. A configuration file can be created from: .. code-block:: bash nxtomomill edf-config [edf_2nx_config.cfg] [--level] For now user can get configuration file with two level of details: `required` and `advanced`. Sections and fields comments should be clear enought for you to understand the meaning of each elements (otherwise let us know). We can notice that for the previous explained case: 1. define EDF header keys to be used: this will be defined in the *EDF_KEYS_SECTION* section 2. define prefix to be used for dark and flat: this will be defined in the *DARK_AND_FLAT_SECTION* section 3. rules to compute rotation angle if we cannot deduce them from edf headers: this will be defined in the *SAMPLE_SECTION* section 4. pattern to recognize some file to be ignored (like pyhst reconstruction files): this will be defined in the *GENERAL_SECTION* section Regarding the *GENERAL_SECTION* we can also provide more hint on: * `dataset_basename`: the `dataset_basename` will be used to deduce all the information required to build a NXtomo: get projections files (like dataset_basename_XXXX.edf) and retrieve infomration like energy, sample / detector distance from the >info file. If not provided then the `dataset_basename` will be the name of the provided folder. * `dataset_info_file`: In order to retrieve scan range, energy, distance, pixel size... we use a .info file with predefined keys. If not provided the converted will look for a `dataset_basename.info` file. But you can provide provide path to another .info file to be used. Once your configuration is edited you can use it from the `nxtomomill edf2nx` using the `--config` option like: .. code-block:: bash nxtomomill edf2nx --config edf_2nx_config.cfg .. note:: to ease usage the "dataset basename" and the "info file" can also be provided from command line (`--dataset-basename` and `--info-file` options). If you provide one of those parameters from the command line option then it should not be provided from the configuration. This is the same for `scan_path` aka folder path (containing raw data / .edf) and `output_file` Help ---- .. program-output:: nxtomomill edf2nx --help nxtomomill-v2.0.1/doc/tutorials/h52nx.rst000066400000000000000000000154061511430602400203750ustar00rootroot00000000000000.. _Tomoh52nx: h52nx tutorial ============== .. article-info:: :read-time: 10 min read :class-container: sd-p-2 sd-outline-muted sd-rounded-1 the `h52nx` application can be used to convert acquisition from hdf5/bliss to a nexus - NXtomo compliant format. This format will also be stored in .h5 / .hdf5 / .nx file. For comprehension we will use the nexus format (.nx) in this tutorial. To call this application you can call directly .. code-block:: bash $ nxtomomill h52nx [options] .. tip:: You can also call it direcly from python executable using: .. code-block:: bash $ python -m nxtomomill h52nx [options] Then you can convert bliss h5 file :ref:`h52nx_without_config_file` or :ref:`h52nx_with_config_file` .. _h52nx_without_config_file: without configuration file -------------------------- The first two parameters should be: * input-file-name: bliss.hdf5 master file, containing the details of the acquisition * output-file: destination file where result will be store. Sor for example to convert a file name 'bliss.hdf5' to a 'my_nxtomo_file.nx' you should call: .. code-block:: console $ python -m nxtomomill h52nx bliss.hdf5 my_nxtomo_file.nx .. tip:: you can provide an output directory instead of a file path. Then the file basename will remane the same, only the extension will be changed to .nx .. code-block:: console $ python -m nxtomomill h52nx bliss.hdf5 . .. versionchanged:: v0.13 if the output is not provided the default behavior is to create the NXtomo to the PROCESSED_DATA directory. To overstep this you can provide a directory. You can also access the help of h52nx by calling: .. code-block:: console $ python -m nxtomomill h52nx --help By default if some information are missing the converter will ask you for missing input (it can be the case of the incoming energy for example). If you want to avoid converter for information you can add the `--no-input` option. An acquisition file can contain several sequence (so several acquisition). `h52nx` will convert them all create one file per acquisition and one file referencing all acquisitions. You can ask the converter to keep all the acquisition into a single file using the '--single-file' option. Input type ^^^^^^^^^^ z-series """""""" z-series are handled by nxtomomill since 0.4. It will create one entry per 'z' found. h52nx-settings ^^^^^^^^^^^^^^ The `h5tonx` command is using the 'h5_to_nx' function from 'converter' module. This will used by default :ref:`h52nx-settings` defined in nxtomomill.settings.py file If you want you can overwrite them from the command line. camera name """"""""""" In order to know if an NXDetector should be converted or not it relies on 'H5_VALID_CAMERA_NAMES' defined in nxtomomill.settings.py. * If the value is None (default) then we will first try to retrieve group (under instrument) that are defined as an 'NXdetector' NX_class. If none are found then we try to retrieve group that 'looks' like a detector (containing a dataset name 'data' and which is of dimension 3). * It can also be set as a tuple of string. Each string is a detector name to be handle. Those can handle Linux wildcard (like 'frelon*') * the values defined in settings can be overwrite by `--valid_camera_names` option (see help). other (rotation angle, translation...) """""""""""""""""""""""""""""""""""""" Most of the key used to retrieve other operation can be overwrite from an option from command line .. program-output:: nxtomomill h52nx --help .. _h52nx_with_config_file: with a configuration file ------------------------- Generate a default configuration file ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ You can create a default configuration file by calling .. code-block:: bash $ nxtomomill h5-config [output_conf_file.cfg] This will generate a configuration file based on the existing :ref:`Settings` that you can edit. Once the configuration file fit your needs you can execute it by calling h52nx. .. code-block:: bash $ nxtomomill h52nx input_bliss_file.h5 [output_nexus_file.nx] --config [output_conf_file.cfg] .. warning:: when you provide a configuration file to h52nx then no other input can be provided (excepted for the output file which is optional). .. note:: If you want to provide configuration from scan titles you can ignore the `FRAME_TYPE_SECTION` and generate a default configuration file with the option `--from-title-names`. On the contrary if you want to ignore the `ENTRIES_AND_TITLES_SECTION` you can use the `--from-scan-urls` option. Example of a configuration file ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ .. include:: resources/default_hdf5_config.cfg :literal: .. warning:: the "data_scans" values from the `FRAME_TYPE_SECTION` section should be provided with indentation (at least one empty space. Otherwise they will be ignored by configparser. .. versionadded:: v0.9 There is a dedicated section regarding PCO tomo. If the bliss acquisition is a detected as a multi-tomo by default it will generate `nb_tomo` * `nb_loop` NXtomo. And those will start from the first projections found. It might append you want to 'refine' NXtomo to generate and this is the goal of this section. Here is the details of the parameters: * `angle offset` (the *projections before will be ignored*. This will not affect the dark and the flat that will always be copied for pco tomo) * `tomo_n`: how many NXtomo you want to generate * `angle_interval_in_degree`: do you want to create NXtomo which will cover 180 or 360 degrees. * `shift_angles`: do you want to reset angles of the final NXtomo to 0 Constrain to the conversion --------------------------- *nxtomomill h52nx* will create an `HDF5 Virtual dataset `_ to create the /instrument/detector/data dataset. This `HDF5 Virtual dataset `_ require the creation of a `VirtualLayout `_ which force all the frame to the of the same data type (uint16, int32...) So if you have bliss scans (entries) with different data type the conversion will fail. If this happen then there is a bug wuith the bliss acquisition. Nevertheless there is a workaround for it. You can add to your ``instrument/{detector_name}`` group a dataset named ``data_cast`` near the ``data`` dataset. If it exist then it will be picked by *nxtomomill h52nx* instead of the ``data`` dataset. Of course this second dataset must have a coherent type with the rest of the acquisition. .. warning:: Be caution with the casting. .. hint:: You can find `here a script casting a dataset to another type `_ .. hint:: Consult section on :ref:`handling_h52nx_issues` in case of troubles. nxtomomill-v2.0.1/doc/tutorials/img/000077500000000000000000000000001511430602400174455ustar00rootroot00000000000000nxtomomill-v2.0.1/doc/tutorials/img/tomo_00068_nabu_rec.png000066400000000000000000016127471511430602400235460ustar00rootroot00000000000000PNG  IHDRc zsBIT|d IDATx^U&]ʃ(``bw> 7;PBQ InuwwZ;w{`fOl߶-@@@@@@@@rv.[0|@@@@@@@@pzzb l"       a@@@@@@@(Kqd/@@@@@@@29@@@@@@@"i `3.)@@@@@@@%)Nt-      .1A@@@@@@.@?      L]@3p9      2`.e      %E.Kʑd?@@@"rj[fMMA#_w@@@")(RLF@@ȳ;ϿoƎeVXJj5jPN:t@'ۺm]uS?KLL|Զa.ڵmmsgׯ QLj\@@@@0c6   %S`O?M[V-mͺu6s;[jj5j,%% )ǧr:\25 @@(n5krn7ldiӦϴ^{!]i9fB@@@ GVq&C$/ȫ$/WE>x߀՛ә b9&G@@@y6clvXBB375m$f4y@@@t(}۱c+'GϹr+W=؈syg۩'oVksURنʗ+ֻl Z6o=ooCg{7nѯaOf;ve\ﶿ/Zln۷K^gwfիV͛؅W\m{kc5-N8+e<~5r@@@+ڛn.rA`/l,zuڃC gOfe&@@@r/ФQCT/l3 /}w޷ӣ_^+. Uar6eZ_Lժ^mZgX4ql}e w?dß|wpܿSpOݶ}1cV4c]}n|W_ZڵqÆdE@@(ɶF>֨aVn:6,5)!fLjRMA@@@ e˖;oɞ{U~?iӪtUVf kWVJ%3a`";Q۹ҭKg{h -V=ZP-3]o>voS\KN^]oٴ3촓i3gYu/xֵˁ6ynΝ:f[_2n2vk;    Pjve6T_RRRl\b.1/Ɍ   P@](p1}}3GqD/<g֛cC?߽zuXsʴ\Z5u6c۶mM<ͣpxYhW߸`ټeRaR^wAit+ܪEsQ}7,*V 3   R@=]s0[xqh ؝1UK0 11   @n/qx]6J9=-7C'UիUu+W֬[&wY{gy?ϲIMz^Y-54pEKi#f=qyje˵W~wmOB 1ז,[f}7B눰6   F`аCs}MCd111   @<5OԳ-UT'aE^m;滱f'#;[bb6cи6iM1k5_Ϛ5n*g>vyck{wܿK}nF    zje ~{-mdl:ژ@@@(n? VݻϿ滀qGw OMZrr-o{Gѧʕ+bx*p,UTtcO殮hӪuܧ:yW {NxRX]7ҰA=k(XX/_򹩩g۰A}oc ߞ @@@R$0olzrK3$z3QKII% Ǥ?8\ӂ̄   @NMMM6ڒl^Pwi^HR]uoxAߓzk ^n彮g`낿٥/ڶڵkxsͷw˔EuNWov'67Mi;B/n׵ -nw; ,- .c<]}oxYگt~A@@(u7͛6f9K5MZ:Cs?ġ       P2wMSE)       @C2D@@@@@@(<ľ}Z2e o X3       @\lޔ>e;3lɒ%6g洸TL%       +Z=̦O`!      E 4pgϞ^.LA@@@@@@(f=#泹       @pӦMA@@@@@@(.˗bz l@@@@@@@ Cq30       P2t]wmF@@@@@@#)xA@@@@@@(v>       @М       @  \B$      @@@@@@@Jr @@@@@@@0       PB@@@@@@@r<^:udYf.-@_n=zs=\WlW^ye̛dС?ڴi+uo?kݺ}gX~sm?ZX*}a>@@@@@@.5~;a뮳.Uzi;wvpu)כڿltjjW^yk)))ŕ}XݴaÆjE@@@@@@"$.ۏ͛7jQFogu 弖O>ĒC;853<|iٛoiwqucSLqjetAn̙coǎ6zh/ڧ#GڦMBѾ2|A;cSOw}7CVڎz+|n޽͞=;CW_5yE>|xuwK/]h$~gCӔ],uet_\ o.\uttFa{}ZV{ @@@@@@<5jOf#i+Ы,ݕ+Wڀ+'x".Y3fpefZ=P ۷CZ \oܹVfPVnSVUW]e^{ p +l% ݎ;Fvڶl2۸qUZͧ@ VQx֬Y.+nsc誴l242uT{MPϯL^?W|kej\af;0YO>dS5^%119{']pZ_*Tq5:eǮXAʚU6C;ʔ)r=#] Hǫ(裏Xo7xCY k,dG* kC9t>v '@@@@@@(hd+sڵXQe׮Yի ֏ƣU4iӦdɒh{̭RJhIg +k?M!_-u(x_EN:NEu)₵W2tpWZch%ੲ{<ݺuK/u]e+kVIʸ}7\wJ۶m3 *p駟)[&Ye m1>P      yKpn7bŊ `uŜF׼]ݰ2|rxQW~x EZ6.]vhx~OF[.>UVΫKf\2uT[Ah% S{m7vk[j쳏[g'L`?}.ZAO<1&uZ`CV:      @[hʤTVe<}衇lvW[0sܸqsϹE]۵sΨټys`tR7MXgV~:ud~ZUj^zM]&U.Ս%y *Xck^mg~Ϛ5\㎻ۄߦM=qJKK uݯ_?6lXEPBwu>))      5-75~6-Ns\rIi}9Y (      u.l       Prײ}ͪ*m[v涆oK~գ8:ĺor-u"     \.ǖ=C@@@@@@R&@Х쀳       Prc˞!      @)4ҥKK       Lzꕌ=c/@@@@@@@R&@Х쀳       Pr͙ڻ~;ܽe@@@@@@@,ܬU-_*      l.ǗC@@@@@@R$@lv@@@@@@J}|;@@@@@@(EKfW@@@@@@@d .ǗC@@@@@@R$\]E@@J J   4Ν;g%H  \ݻܝc@@@(Ecƌ#ę3gƍmqYy*Ujժ֪UG%   P2KƎ    QpD֭[Eal6{lkݺukC@@@@@@B \(W_l׮]q٢$kҤ͝;7.Q    2Kqd/@@@&6y[W؜MK|Rk]*&Mn}Voܽ{%''ǭKRwPa@@@@@@@m]cƮes,7ydWַٱvrNŐ`7yڴio3xܸq{ŋۅ^h5* cm   P,.F@@@ f| ޼.MY0!S^-ma]>)3^z%{]&0@@QLǏ:nfk֬7X׼yLM<ٚ7on7nt `565q+oj{ '%\bV1g{oΝ;_̃%ֺt^|E7n]=P˭iӦUVs=g_|c=[͚53p0o7ߴS1oԪU+!>ƎkmڴSO=`[guԱ3ga}r)FM=')S%du]gʕ߶;.j xg!̀ @! dLㆴJ-oTcouѮhr n{*S9S٠vXr|_.%%%Yݺu~G_B^k}q`Cpia@@@([,-4itI*7LF~7IL]`/7rt #5kO<]*ԥwmz~TzUQ=֚={5hMS0G n_e1*`:nuK.u* Umnt8+u>QH>QgI9.E,ۅ @vqNHa}%~u%ڜ{hunA8ZV~E_tO>k\x#j UZ/    @xjeժkUU8 T׉O%8/V؟auݰn.S¤[RH{ Vn;j;a„Pg:l׮ͫVeͧK.vmSpKUGqۦe˖2ok!+LϺZun/ݪUrYV=ֳgϐ+WbKٜ_}7̤VPM=gU^Ϻ(gu^=TU7x#4M\e6l0⹫Zetp}:f+3oРAv(ףu6?pwENe5eGר_z>}ѽku]׫͛禛nrO?ݔCL54~.hew~(p/ UAH !RTv-ͧ86lp7p ;֥Kk9}6꼉y>t::"]GڇX3c 9)NrͲ[Cˆ_uճYAѹijvx/_}\g}5ۆL4(s}HF( hӭ&qDK3)aBK8кycbyި`iӦ}rJp⽎=0x̘1VFx   @ ~ט}zZJU֟y?MVA&43 j`5CW쫯᫺wU @z@_=pش;" ꡵W5JzLa=(ClucN5{gsε믿y3Iֻwok\ YwႩW_}[NyruYn۶mhT %m^ :Rq4=T29蠃1P֯_^{5zW\{S@S7]ع * Xc*P@#RZ^k\cDZ68M ʕ Z3e$k]cj,j|R}]~{ymVVٻ Y5DU0G=3z_CLMSFa n>+ +UoQ,^8=U ?3[(駟\/x۪QNlퟂOqݜ A k%11y(@ ju% T)-ݛ.~k;tX*1 Nۼyˆg֣4ޭTD%3w}_G99.:-[t{ R=܂Z8^mz&o R o5 K'(4]}G>VoG~/xspp}H.󡟐d ]F^ՖK\:֒~`-*7]=#/T}#;999GZH(3:uuѶKtI*]\׺m@@@x _UM? \x衇ܿ~LU{zOUm+PU}}QPA_ \GZETUQF2~T**Gل lj[>e0)O4kR`B#e_k\@YiP zkVիW4ΧK]K,q(L6e^*Pe)@Cj>ǿp݁ɢ(XFE p(^A^$u=`yl4/ ^+/jh`F*ޢmR[F$7t^}͗w(3ZETA*\cZkzש{wN<|WQ٭ j{4_wIA~(hIy? Q@gTdu>u9y٦Hb=. $w5Pر˸UP_Z4X,{>}_[ kT]%@'WGR(Wݑ'wϐ:W ;­ @ k8}NKҷ,qﶻ!vn#,iDs(mtV->_R 0}{˳8ӗdrᯮhGz/j֨GcDc|·   /z.ɤT?\f ֩eWQ~_ݬ2M,WE@oG~w=O6mBw&` _TiPSVmzy5i,Euarr 4hi,WQfRUs=WѱԏV`vNZ:X5 ,ys`cpG:o˗/e/ZFzO U ON"0Vs4\?؟sKAI-Pף5*z_wZ+`j\`~z_k?eV55n~Ϊ_jh9Lz/LzzP#y?g]n=M9;Kݠ{~Uwa ֯F~@V  ޫA%ty;j,FLJHus&X_:RͧM皺2]ٹf|?|Y .OݿGh~QJG^m;a@ XHA@ĵ 账Oٮ -A4cKX;Ӓg콾ޒb黽J{F஺wV=,Hd/?BVԕI_f/qM_Բ[ݠU+@@ȉ +e*<ͮ( W`^Z=8W֭.*(0L[e(p, [^D=)8M'kQw~efϞT+?{ןOL*oZ^Be.k!e*ZejXFu@ :Q~Q 5PJB`wW*ءF~Q`Vj VvIhn=Џ wb-kL׮ƺ|tP 6E]ԪֵqV*s FE'tM_uij.:<5Y^HT+~?{o՟{{2U8W47%v7"i hts #;,b.T%zOEՍ2ՈBdWhjХ0N @Ik8=yO7Ii5n/țkKvKBK%.e狺~w6c}IVf1=PK@Ɖy):ǭdw}u-VG-V     S=PV끪iȮ(N]+^HRPQɕ]lj~=W7U~*` š+5y| ㏮ 5$Vz4檦]{RGVZF~QWxUQ O5ƪ[xSګR .')(࢞uAK9 ?tEٳ:D􋂺o/ʊո 2E3,AA h}tG-izQ~QU#XLA:u-ZZ` S\݇REe7B#ݟZFϢ,G׽~Ϻ/j]Yj?gN '%Bֲ:?{]=XtNdWtDWD}D+!^3mhhs\ -;H>:ך'5¯KmkhEYBY:j]o8i9! Ptd ^%|ΰ+tj[z.]/K/{*Vx BpܶueKA^B-D3V'C@@# h|Oe([Jy9! .A ={~’R;wvսUW]*HAM J T{zH `^?HumCjmTG]UKԅPXˇ^7Wݴ{LLz)=Qf2$mT=j+VZQYzz~ 능. UoT8j_OVT)S##u>v׵!zwu([QG:ot^Qcek!Cؽj {>.T`t݆w{:ۚz-S0Jד?VFט|vM/R7 U= (1_u2V1Z٫\c5P# ct((P~6iiOy}ѿ__p[u?zUƍsqYTݏ+@hy?xw/TY2/B֑Ogm_j̤s@ZQ#%Hujae ]_RZc}^RePvwV烿X>guϏNQ_絺BͺfrBTth_bgD[C~]l(0K$݇xk_u}Q~P @ ȷ|~i [^vxcW:-ߎy Ʋ}u Fli!   J*Hn롻=%=c6rH% >.(~&ոz( ἂQ0gϞ.( i h]zû [XDCX3 Ê<rO=ՐCRne߷o_gqG[#uxu(_׌>C{C4}5BK㭿hD?@y@&etǏ`y|Q V@4_xպU@@PYvci沨!XTh9RQUJŵ_GNh羂~px]:VbVcُ2]뭨lӁM-UWRKQ.K"85T%cWV9$UT@@@8(Oc-ۭ,AeG)_ lS+M#,@]uq('g7oq؏%벤 .!M@]5u!/" j,MA@@^wt\n#EȰ]И9W.k׮":AVƔ%PRKQ.; IDATek@82{l;w[+E '@@@zzh" ߢE.N H A¾h VގXX vr\uYG! P0 9۵n:y@@@@@@J W_ :vj~C@@@@@@(~hƏfN0! @@@:wiťL@@@I ]@y1@ fͲ6m#VwDbc8Ll$P:޽;t&"   @uV%`/@BB:Z+LjQίqKz@ oy@@@@H .҇+(wz8%K     @d\#WT@@@,i\ࢼl   D0aB7r[ڈLD dŕ3ϕqFz?::    $е}VR%{Gn p$:KA ^Bܹsh'py.=zԦsg~Trrk>`(]vU)SZfBsbQQ\nݺe+~dž6?%h(GhI5     @ -UaÆv۠A_~3f̰ b}ݶ|Qu 2?p{nݺy_P]L+8:FQo@\s=۴iz:N{]r%>إ^jof=eVK;]uc[k6yd{3aÆ#<ӧ~`!b\tEwyǭ[CC/Mҥƃk=w[ 9 Bek۶+V1tP۷vI'k2e `޶GC݅+l슺ֹnŋC3Ʈ:w>sH׌U]^=M:u?4$#H4|m<k@@@@@T:̸nv`O<׃+qܹ.z|IZ k[׳gOkժ wN=T2 Lwq֢E 5j{'۴iB ^^ɼġ8_`֮]\@xŊAŕW^15UF޳.-nݕ__]ڟOCd58]@>p*@ӴiS8pM05Cz-1uW^[׼?x}@۬,dE7i4Xo:6/ VQ <5h̙τn׏=͚5+[!s9ZFޟ9s){[>[xg  6?>cy2Z18㺾#S55&뮻u g΁uo$   duLB?i |G.,7ԏ_9˖-ڵk;0 8+|A* t)裇Z7p޽{o?r P4kM[jܯ (,Heٖ-[MWKA385j2P˗/)իel(п (P=@ZA[T5֯j̡RӿFn~߼y huOP9\e/hU}=P7-^%ӵ@C?7xcަTe+4"yy.^pY5PM5U8k׮.ۤI3,V/c_5tԩkTc.D}GtTQ^xk 3     PlriUzH.SSSllgԭ2QQf\ BOJJr7n薙2efUA`Qfq0/(k&|#k֬qׅ2%^שS'âjrׄVAZuϬF[vTF_rʙӲ VvU^ݭG"Z.\~ڴiGA>UNz֭2÷_Gn5RѶuQ7oZXoބ ۪_{[$o՘@?wqk䠌ou ׹JY]A ܃7xLtzQoډtz+Xwp ?" U@(    PxJmu8*JeQ{`QV?nSƮ<=/ .Es=ה,~cF$ .+W׆YWDǔג2o?4iWtͪaO'dW;,Y|Z>k;"uږ?u.-ܳ*<ۢKqFW_}<`]QWয়~:Ex9ITk֬ˁz M ?4   *p0&RUÇwϣ 'TѸ~^k-laÆcU,deE1kM~^+;rH8q{O٦ Zٶ=5ՏVU&UW]U>}-^8|p.ko?pc: */i;4U VguV螢ږ ;}&SF\5nmǎ]up{&Zc"iZiEִ0W~4qkګ)=Wc }`#]W:>юoUСC]w k?(     @iu8;,e+ƞ={fU)ֲeKϮlW>k7ti,Pe*Qِ͚5vyf@8 (__+n:7QܵuU8TQm KheʩTuQ׋7UpRٍGW 4ů:pSYck椌;֍+Vp`7Zh `tOm2Q ^]+z v N꾤s=)T ? 6{?GG琲O97h\|>OU:մ~z1bDEPӘj$c*t7n\uRetz4P0^C@@@@@~VOpçin~aO>}\VkVJY .gץD _W^ӃeqM#-S[:k"ָc[mR/N*x Ϟ=e^pne_.2su(+X?jPO zt~׿>Muͫѣ]͟G!CX߾} ׹?ϥ^j~(Y:ūE?ȯ'gY+[nagmʎVz?4|o 1׹p@ټ}GBm8)P٤QU({>-S&otDSInסlE!XB%|h?2jHc7P@ 9aԩ*DQA5k buB*vV  @HغesYV㏻5j| 6\f"P(˗//.5-z9@_RJEch]j[*VlUU^˰aìf͚vUWo[cZBwr忮k of!o%a|`+^j\m?ĞyoUslvum-:CٵvvgSF7{հk%*g` jjdegXM-qOf+'[7&m,=%X9 ['|i ;_*yiyY,=|u7Wvf=Jk>5ش)+zYI5jJ9 ew]Kn- v1l6l> )b{y˖-z;ꨣg VV͞%;~cWv(\kͻL<Żms"۵ȪM:eX4k%(k;ӧi7k7ON>| w}VB Oڵ^[_Mc?[jjb/'Id6 W2Jy@(`"^x߸.*VUժ fy@@@ _Rʥ˼lO/kNSۙd^o-rVKz~ 瞲ʸ駟`_4d޽{q[G84gb}њ`e3O;N+_ ~n YO X⤇-P?bif&e?Nܡ%Tj`<o_)a;0n/5l\d 4wԬF9zxˆPMn*$SYX9~>u{3%xcv=o֭M 4ͣvTX͵jKl2v=5jvkgڼ.+c evoԭ,! Zb/tc>cY}@@%Pd ouml߱k>lծsEY=@@@R%0acAXej޶'Xץucl nDGZ&,g`;zhKNοyq|l =&?ze[TNwR0V\ KlR^IuӀ:aJK9q͜~tm^ͺk _%q-qFKR/P0tX*}m_kVZw,a/>m'iVi8ճڤ5s]OӝkX]5le֮];C /nZjլ] !8arB]/]e)DB}uUWs9\bڵl +:=䰺 N~VY8+t6t-wig7g];oˣWY,ka ߳>*G}š pz7ؽͱ,v;뾡k9hzRRR/k@(PVݘ>}/Y6g,~?[|Zje*5TRYtkyMc=֮L_T֭[g?}'ְaC;sNʴ?%#un|ԢW_FO?t;Cm۶믿nj#+2W_Zg͚5p.].}e[Z.rc…Aرcmƍv'Z>}H޳SO=4P/+Wtƍ:(R5]uUn<{׽.t\{o_]\ى&"  P~ZmX6CX0-=yߊ^Wz^OHߗl mR=c#(ث=w`8_mΝmEJUܢ}?duڿ}o)vM}kmg_?a)31c쇷9o[)Y_[:J IDAT,e,ASy7,-ce%tO3$h ?ji'MO##t0wN&UVL%KXg);R_ fʒMMM5eǣ hcL6[WXʆ9wٖܮ[UY>Ps̉=W<₹8׭\(g\*e0ջ x^y^|ѢŖl)-uV۲ij>k|WF$q_|[?8X͛gFrG̜9 i='ʪAϿ; @K۶mZ,rs,cn= jDg"\`2 @ ݭ4o,"ŗָQ۶c[C#FES V^tEr^|EBN=9Ske'| 2.tM@:3mN;}Q駟\lٲGq >ZU W7x`ָAc2~Wܺh r_t4@fN\s`}1c KJKUFi'N4`p5lnl1`Բ   DK1Al^#GtSXy/ kd/V{0\?oL۲mm͟CiU:ݺusG 4'Q۩jZ 1ܦ};hu6\uLw+Clz/e&{VvLRO}6j|>*,o-gZm-}jK{>x7Jgm/VY.Zɖ|7졁CL>IQ?YzsX3現黻p1c;UE 5v#<99my ;,u K޲6h:jUuO? *YAm}GWQwA}6XA͓cThA۽ZY{ǧ\]޹r7Yն3q5w- GSݳ+۶ڮ^'{3u6k\^S~EgqF:c}1Y3Yգ=zΕۢ)r\ϽTWF֝ckd>cZy٢E wyy]LL@@ϛ7ϗ1+עEsۻݿܔTAj0뮻쩧:ڵkW;?^^7i*Cw XkwAF܌.= Qtʔ)ָqLn:}UQVcL4)GG}Oeu?"X_U-ZdW_}U/ E-O۪/۪;87?Ӿh"{RI!BȾo!k-B$l$VEdTJ}fs9{Ν;T{yϻ|=>Ï`& uYz4?Ѕ_006mZ@U*"" " " " [Mo:T֛04 <9'VeYfU/eL$T488W;@d:u5<'qVV +e=AE 5miw>lt}[['fիk 쉿3҇窫T=f/XNNe6j=y^g|Zb'^{[ֶ23yjיt~ԾWXpCDwDb4ѳHg)^M6نlC}Rє#m}C=0>k܃<:!L1cw"ަ(D={%4^':v!?NFN c_ߘ-Cȑ##/ +cn^(ˏDz'i4> #` ,y!By}buzYٸq㸳}e?ǏTNt6(ޝ1cK&$СCO?uz衮^_ 1Ep "!-hP@?2x# .!_KDgNgSO=ͬa9x Ŝ1c|AÓ<a;tb #&bz_yfrlLB硝c8_yC.B pg(Ls9?0@%܉3GD 403m>W'hـLD"u!r"P_ܿ;$A]y=+w5^' 6ms!#_pLyQC^c)Լy#ǝY۷7ca;7r cY9Nj#^-X )S$OlMaa4$^/?6mlʖoeK.Y~g.ÖyWaMglO, Nߍngb+l&L>@O?=i7xI<`ڊCA8֪V˛D[ly/'{_oe?زOnhmY% 1 Yچ?-֞v̗Klڗ6MK+cujVt/Tu+'&d}>ӝg^{v*-] OZv:oBs>&lj ܇ㆀNm<~fba3/Ƅu"E3꠮XD'ںdgd~2׶7lV>aڪ:e兲N/gVkqMgnn԰Q#x eFS3v)qMXك\ -|f]~ϻ5޻!.Ag&g BG{vCm( D, Yٛ~|/P!!3kKpXA#ifь5,8XeBcBgݝhF;T/R gG|oDwDۇu&,,\CWE@D@D@D@D`k ~Nex^zkm%6Ol'ڜ~^ۜIS٤s  gƔ6U 5ǿo<7nz|yՕ֮v =lCGl4bgۺ7'Zfg|][Ͳ{V7gcjnc}d9 5|u챡C[˦lZF,Ra9 ]WU9{z<,RghN<ˇr5-RSI@Ot<\pޢ{xb{ꩧrKd}7jeu,jw5g=  b[nױyVe/Un>&߿ⅬhEu~5L e-w p]Qc߶kyR ; 8N"P&" " "P ?zFCO?Ŝ燀_u !^"H^vee$b#mϞ=tVi@H[DMGN8{g\haDUׅC$Z?k|EPϼw/|8B&¿_w "6 9&xNC q! =΃i\?T„瞹E^^ [<1ڵlsBs|m_nj?J~&D[D@D@D@D@D MZCb-9hܬ]mO۴>u!{ywʖ,.GsԂD:S"k:vX;C=e ћyյZZ&V'3Ƞqv߰{32n{pcYגּ5-'߉YhrJԮbew3cYYߝkE%1Vd􀕚x%FWg7=޲v?/%6gGyѧEPNFccv1}4c,+~py baOUR񫮺JLJTD:Tv'aӽ6 &ZuY*j:iyqt*Z=6RA\q!"'û\ȔݚVA+]N㕟%s)U2{Ҽ0Ye\uyK֣븎vun,xִvxhey\թHg&`uܣ2 yk{22:]D@D@D@@چ"l>Ex0&%N2Ѻvm~rk}}>.;򕌥a؍ֺAe)g_`'yxged9qÜ3a(eydw7tP?F"ŪᗉR*TDSEIyYR?~UdJHΫTnCR^k~|oc+.!" " " " EIIEчXo&w) u@D@D@D@D@D@D`;"m2'i^| ~..Î -h=y8PN9l׽dǘSm6;cs岕cٻ7-n߹1 j^%=}I+e;]j㮾,͝9D9mm滛9W.Rc?-&Xc;U-_j/pm[ik^mݲ?mUׯ6^پ~2_r1k;ёh87+[ǂ8X۬7 koƌQd`~}b""4ve." " " EAmvog,2(l˧#Oܲ-[-졇y睍IEiCzOYKs #DYˬXHF3g:w6|nƍ{vqǹP며}S.UW~5+}tdRv9ӖYz;y7/]r].}ZM6ZJȩ5;cMx'_y-vC}wj" " " "P\V" " " " " " " ;.}|1>(0UlMC%sax奞{[s6ype" " " " " RfIExd>" " " " " " " "5,m돷8akW" " " " " KԊիl*U ." " " " " "H&c*U\Œ%KZVVVhstRk~ƶΉ@%PjݺuQD@D@D@D@D@D` PjU[`5lJ*lCѶLGǣ1@(Y43 ([Xr_,WZ U鶴" " " " " " yhѢ͚5ΝkVʳ|* @F2 $" " " " Ohe8l_e*Wlk1Va!DD@D@D@D@D@D@bhٲem "]Wq^" " " " "#a,H_XgfwjiaQGzzmذ!e!&Md/}vGW\aj5+W?nիWN?tѣLe][T">|]x6e;s VZ… ݵv5G}d~U\9R^}ӭq&J; D@D@D@9[36KbF`N~\}̈yH236ܖ {챖%J(Pcǎ;|A; QO7|ӪU^fvi.؍7ewq6b;3# J;6BwFhf7p[6tPCׯ=#)3hc:iӶAj" " " " " " "P In36m Jj5ED L=e׭ZfHpQ~z[lذav)f;wl{キ9wޱ^uP[WlYۜN;Y*r D`;'жm[;h 0`@C9:ڴic~uN=Ԙi5:T{" " " " " " " rCCz޸~e ZU*[NvfLb,+#,=څ]?m޽RJ_Y͍}7vG8o??yb%Kti>WԩSNra嗈ph"_͘1VZeu52U:wȑ_tKnݺY^쫯r!l˔>vL!cǎ.zNb裏.y6fw.]:OQWn&{Ȟ{ڧmџ8s0 " " ">/lD@D@D@D@D@D@D(\EO>1G[ؖ-_ٵpU]i(zWSkРA2kvT֕!}^{ew}nݺ#%;^wִi\rᇻ{M|{+d" " " " " " " " " " " bܪU+'8hM<73}y)8iwX|]wugEa5kD[߫_?N7^۷~lԨ"n8 wo]v.\,1tR 6"B#H!bŊ*UZhaKe]lFɹO9cYݺu]4ޅiZQW.;+t^Ȉ^zP"{5j87ꋿlKP7޳džEڐg^Yss޶~ŕO[`˗ۮ];oOf_l͚5QD֮]u{pe*Ur! 4W^msu|ժUs"pe6!_ ']fM=uvN 8I\Tݻ"E}_!Џg}N>Hix" " " " " " " " " " "(0"p2olh=O3~reZ:s/e]hnonͽg9`?/o .̵S'/Q9\>}s"-9'Ol_~41%wAdUfٲeN=RYWhl\tq^uUvc=fSL1rsV&\|{9>sϐl 7p~<I7}oܰhoLz~Ch<4p }`ի8=Gؚ09󲲞pݻwo{7lĈGonذM81!W^38#sα^x!2{a0RYW^cv wû{{gmL4h]q6k,W+y'3)J߁:xÆ N5j>ꨣ"N8'\r%L_r>'uJ?|>|Mz;Slܸqcָ0?zX4.!lSpA!sr #t=Ss;nQoșKnq^nnӦ/uڣ>yN=Ӷ(xsϹM;΃x.T֕0 ։XpwQX'z0y䮻;E}_)SKVZmGGAC@|//:w?ܺuklK_>7%JY-_a&4޽?nݺ0R" ;BwE`rLD@D@D` @z` Fų\x]VZ5j("+W̳X;pN tݽ ҥK*S" " " " " " " " " " " [!רUǪTf³KOO MKK+.R?D@qcƌqk׮D!`-[v?ڌǡ/<@z#" " " " " " " " " " "4IE@D@D@D@D@D@D@D@D@D@D@D@D@D@ p3" " " " " " " " " " " " " a~^/gϞ.mj?5k۷~~ϷN; kkqu} /XFF+n&kҤI텱ApaPU" " " " " " " " " " " " " &f8puپ\|͆0\nFWkΕE֭UX.#)gee٨Ql}u{';Ac9ƮJ{gwvE U퉀 lΜ9GA:mz]uUVreO<ѭS˗ې!C쩧ڢ6l#Gژ1cm־ UVdUK.ҥKYg4 I^r_,ޕ׵r$Yvc=7ռ q>aÆ=GE%J8Ar5kUN;doQ%he˖%N:i!zvyn&8b_eaXv.=W׶7~6o}XϞ=ݍx5rII\κc=֮ꄏ a 2eʸa ZO̠aNrUwC| 78wС#CH*VSgD@D@D@D@D@D@D@D@D@D@D` 믻O~y~14dmѢEv0۷wddd_or/xkr'I3cӦiVf{wDte1׭ZfHU$Ffk^|EdɒN>3xKsλ;%6|TR[̄kߢ؎/N07q4hʎe$衇G.@6mv.]SO֋!f{5th"~ɒ%31BGs9.*+b;6j(?U5)8 ™q/nvdURs`ΛխfVFMT[rbI&Y޽]V8,:zH[̌xg@8v5׸OwuuYn!xN0X?W߈ fc0o>_ck|_=Nh'|<;]_{5w8K.ٍy ?p 6ewr׼V!|^k׺|Czz-i}B"{&qA]{x_Cz뭷\ՠ0)21NA^ѩS'~E6eNaEa;'|:G3rp"e˗juvou4n=3-;+#^ͥC$5uNP=C]XĞ_~Ŏsw~^pꫯkxq91oЀw&-0 !V\m~Uv7_;lĉ0WsrpMrn*~|~Xk_^=]8kUNba*UkXmLD@D@D@D@D@D@D@D@D@D@$0OΑ 'CcGI@6^t^"rWɻwXx'6ӧO7tD\t7xÉʳf͊ԁnQN mUT Zr9A\4OL_yJ&R:X&/oȎv'$ pwndx")wΞ72OSzr!.+x"] Ddg.~`DL W8!"."Ѝ!."rqV4{Ç;=jߵkW'Zcx:2)mEf@ݣ!r~r !." e'~\f͚mQ<9.h#G%sesSD%bIl~7'kݺuP{7!JV IDAT܏D,矝Wppv#k}K]\D@x3|o_&-޶H!}$m71"Æ Z;:O?-ѹvk}܅]}]9R˻},L`y싿lK\cW^hXes]d}/=ƾ;ﲯMrնi[zV^E ')Ɖ@FXD8۱cGݷpB7S!lUDEPB ;TV#\1 7,o6.n b.Q!gϞ=sű/. p @͛!?HYG~ۅI垞DX!<~~":Dꌾ@> /'m9D`c oh,铥(mɈ9>G+_g{巬?m~٭+g9JO\҉O`\:W&Ɇ0ko O#(^\@xZ/OޏL~xx|hLqZG`G|]B4aS5035mSOr;/l\ 2}'o^?s,#6~981C;3HB Bxl" -V^D@D@D@D@D@D@D@D@D@D@Dp0(}oxѰ#ĀnܰhoLz~ChҼˑr%6{ G3:bܭB/uG(f|Ciذar%KX <~xW/Ehk?ɭO]hQ2x1"o E<}_}ٱ a $eIXy+:1LBHr:Ph <'} X)Drp/n=7kID x>MӾۖC"" " " " " " " " " " "PvA~s4Oɶ;Wy[ ^ ;J>r͙К7innOB:ԅaF(snjv)]rjaH\rb}unC1C ;wV#jV! hڵksF“w}pӧ [8/B'(}ܸq4p5k-̰ uPD@D@D@D@D@D@D@D@D@D@D@D@$רUǪTĞD 5UC@p ˲e&v Q&" " " " " " " " " " "  5#" " " " " " " " " " " " "HKK+1j *ؖdffZwK*|yD^) E[@(|vUڕKmeTz]+Wv5N FD@D@D@D@D@D@D@D@D@D@D@D@3[c\؄_e*]lk1VaED@D@D@D@D@D@D@D@D@D@D@D@D@vd ]L~vveϵlX¾dM}gHe7mޯ_|A{ュW^6{l;Smر[o֭5kK/d~eddlÇ[ZZ}G[ h֬Ynof۷^z\e_xQ͟?:btk{~ ʲ+WZfffd+쨣? Uk_"&;;ۂKVfoXg6F]236裏g}f N;ͪW'm{kwC wWZs7p)pBׯ=#ָq -jժ!lKq " " " " " " " " " " %|["+t"(l2!fl=ͪլg%Jl#=Y׭Zf$ngϞG*޿ugLRIm۶Vre7b1U=C֣G5ۦMۭK.s~[w&L-v]}풀x6̎>h;2ηvu]N=mԨQkҤM8.rΊ=zt_F;sm \+r ь~qsOo>?־}{;ClȐ!aW;0<L쩧re?_ĸ;ϦMlM63uK!&a˗/C=Ԙ(Q-Zp1cGy}wnw&l= |.D<}wsoZ{ᅦw|P|Wo|Nd]ڣmB^Lxa+O?͝;j׮!jʉ3X׮]#)^Ÿ|h8\^=6FEةS+_^8mBh9y6h Ve˖7%M7d^z /e~;7DgBb{Ǯz(e]fo=ht֭kGq={*]vD\c_v! uɉVw$r}YRq`OBL(5_ld" " " " " " " " " " " p8_sB{wöhR̫6kB{nO2HٳgnEwX⋿ _vbخ<9s8q)RJիnf[tiUgffom:tȵ]hFӟ םm=>mܸX†k2e«#;"lEk'\.ZevDH#*lWCţ2,2v)P_nw_w.VI ac^~ǜG/)cBtGNĪy9p\ _2ףvBcD@D@D@D@D@D@D@D@D@D@D@ N@o 0Ro~?3nij޸akѬߘnۇ}=>]/5jD ʂFnWE@D@D@D@D@D@D@D@D@D@D@6P ɊFVw=Ss;nSK^%K8A TR.W1!hov#, 'SiE}Gm/vj/=NTS} {wZ޽s֧O\"0W￿=[T~x;s=L:5j-v C nW uQk.',.89_c]˄#Fزe쨣r}xodۘ~va;?Gt5nM^DG$GX~/m Gl=rz#-רUǪTTO[܍V&Lp?ڷoo_ [+0&,~%BK.VsET;UTq)HDx'VLȾ{좋.ƍ.N+:IDڵkqI'ՆL-[a~$mU7bqO՜jrZa0< M gu]L~ŎEojRD@D@ [{VeY?P=Pc@^}]kذy-֭3<5*ꫯ?i:ud~/vUe]fC qze|N>d+Y[_u=\ƍh{ ʆc֕l͚5q'Ys*s z/?jCD@vh.v_nHo" " " " " " " " ^wv:!RJn?^w\F(Óτ&fVV+G0<Xۊu̝;ׅǥ3'9q?x!o>?-vb…?)Y*" "P\Ֆ@Lol2DA\{'t pg}\>y6m rglӵCU233]OiӨQ/E|ל0tRرc D;n햫,0Ӈ¨o H{w8_ty{px~q qUVn pQג%Klw߯J^f{qlFlڣ?MǁcF_ 'oy5}9/ ݼysq7B&hs 6ӦMsC?LT`Y;}t'S!"6sNr)#ié8CWK IDATLRZ툽wjcGᮯnz6[޽X.Ue?C&'NYq|;8W:x w Კ-& 6R/gϞrx!'p9K*ro3Q?/K~W=s}GI3p/'TwV $[8Upq:RTREtBLCp e˖N SO[nEAzgJX>ʳxw:#F%IFVl﮻k_˝H eD< /B*Vuh!qG'/@aw)~~\og2"Aq%2Ǥo߾N ?3f̈OC>09zhw|}0`@39I^Rp=#GFχ? xv5DƝH6DAs7Η^x!r."FJs?( Ͱiq駟^zyFy oYu9<0fx89wu]E];d7ߴ?M&=w/0!r駟dprA&Apb !90|衇_" " ;.qF." " " " " " " Ŋ0/y'B˲e" REt "_gˏ! O9DMC0V~} ۰a6`&Z.uR\ 36XJe><,cAHe1#2vX;c*^_88닿l#d7x8hx1zxyUϝCnƇwt"x{xLxYr-e/(l s!^A~T^xӞqMX4'LDkr„ uAxs?Wopل& 5utMDH~>\ k)X[!R"Pl 0ے#fԬY3$ZȚb;uLD@D@D@D@D@D`'@(UݻGX็ܛa Xxy~}iME&0yG^~f?ls/C~)3=A1+:`H`_ފoڵ |FHC(+ u\ qܸq.1 Lh~~MM4IM'Kx|?3h=ןF\&- .uaqگwG8h9 's̼/Kh0ׄgk֬I1% .tfy!LM:f]G4Hs" F;o" y}Eޒ͚5& ~ovwY￿{ &i+WNh [E}q)FÃ$yxŴbfr! AՊvADߚ6m>"B=^t0гg6< ڷo}[#nؾ\)BO?(l1_h-:y 7?{ҥ)Gf<#7-FnZX6m\>bBR_'x"RU2=paDDD3s",4Oa4yd7i!8hnEO9;D-?DN$ċe>ԥ}D@D_XKQU!8p]xᅮV`}ّFmС9#[oͳL DxggW/yuxСf;"H3uȐ!SOu.9{I9#Gژ1c=YF%Xp.A줓Nrǎ~`ڪ,oW$M I"ő !AMSQIQQY3pRHqJLs!B~<߻9o]Yp?Z{zNw=+owq6>'Jx4w}Xb|YrQi\8s_zL`mnd2oWOn3E_pWNf܌-×*z{:@g`0pΞkw띳e_-b lj.l7_>er6~yWn~Ϟ'i47w-yf㗒xEg/ot}W.@h͎8f3ؙgln ?jffI%٨Gll6n6\ )2X;*q~6n[}[ߚm7ndCqxnxQ|ʧWy1n7+Ygeoq{E2m OyO t4O d;U6w7ڭW[}­_9뭗MOo_ٯ_ykYz\1UIʙv9~/ŦzErp$goq"Y{qYt-o {v._s68?lZ8˘3z[p*ϷYg@LJ , +v93_Sヨ 'J@=蠃ڜ%ɭxs0~9vo-Uq&32꘳+ϼ0a[Üe|>s8㛳9isk!|$ocrH;׾v;K3Pvn]?I=Y\żyV3Wis-gr1*{ ~R|/g)Ϫvc̣q7vLzmܐg<Ǘ\ yc5rs t:-ի-WeSYruV/@g`s2wr k.yGՎ[|n噩\}ՙ[ s V֌t޳jd}u#FY cÚ09ǭ)\5͚u;nb̝kat:->]!6&~r>V`J~ )V^ 6O0<+dzSas†Qj\3.&es g-zx,&Z/+Ϩk@VF^/fl9B[Zp%6~5g_)lSRog\ ini) Ws=*inoogb4ޖR|s58LnU3{^n|.坁@g3 t:w./x,R/@g`c3Ƕ駟ޮ8w_7OleNRMozaZ0螝=>/Rr#ˋҮڞ{ .<c)7u?yM6@j֟7+G>Ҟ9 9W]svg3 t:@g3m035{ t:@g36i[Z}x߀GT9ˆ#ȁ /6H*JR疩<+7eQmíZ 56Cmϝe3ꬳjylZXL>mjQY<#'>!׷`_N`ic=ڦ+\\C7qmPsjnc}jLn͉ l`r%7v^ q*G<nssᇷ[|ᗿ-68:Yn|ܦfN>[q|,nELn׾^{k-K̽<1Jl6\zC~'(N@ęr[+=@g3 t:@g3 t:@g3:[8kll|>-qǭd<7pM _W\lYG_ OzғfφK*8cڦ+)^Q{) 'teIl6S`sW7{g{ܞMj6U6(̙,l=oWlV/N8e74}[2/=ضwꩧ. WַnWϫ/'?p[?݆z}iΌ;d /lg*kn?<@vݭ t:@g3d {o2qg3 t:@g`CBGKg`[goo33p;a˶(wp%oo83 t:@g3_ O vNT3ٹ~;jI`M*W"_c?rc\CG촧?uO}io.{S/C}=%;3Ji;|PWPgyG"YWdƙUV9q}9X_Ě>WMxD.g5n{|W2Nw<_~q̧6Ab 8|/}ƫ&3%G9r 2?-b+ꈁ8_v9g˜>u|U߹My8.ā>کq̕7Æˑc_C/㨞y~sҧ}b'm.zbY@ IDAT<!O6m')?T^Xyg~CF]⟊/%bR_ލ1/Q_@_cbҎRթ3-17v:[ \{QGW/@g3 t:ŀWҕۮE>r!υAm"6rCu 0Z`(C.N\-o<"->k~Sv\]m1Smk3 ܤǺ >+v,:v|Mw91髹&ỎCy7Uce\ Z[)}n|?g\ySc6wo' /p [F^sro[e gu7wq"K3z$bM^+)yqLdH>}6WݔG7 WNc<1h'q搹Џmnz#z!&O`·zŚ8اęybLrlq<-u&Y~&5Wl(M[93<Ǐ~#>yIlMUΕ9K&$#mk:~b+Xov1P\˫M>jNX^M8~ t:@g3 t:-C\q-H\H`A'E= ?,4[u^0 T;1VŜ3yd^Y!5[![R{Q+&s9r81!᜘K\I,i3 yeu&8*i]OC棾+hc~c$GLqy[G_G㣮mrOrʥEv_s8i gJ7e楏<_U9ͻ)ĀGX B_vnNm;0_#u'.2>ҧsA/ꋣqI{Nl+_޸9/.tkcl2ObI<Ϝ9&G~m=qB=0CdN3G})K~[$CyI]1xIbG9~q qx&;q<'N~zӏmπg3 t:@g3 t6\˅C]zqυOX; W1rSG.X:65}UJ@y71)3ic].$'f8S袪9MŮd,!SXy>sN8sܦx?9O~q9XbvӶOke>b06G=scfH3T O%ya:oOc0S9[Q/uqĮ:_q٘͜-5E]jcu><+1kqU/yDOc /WG/87ulӟ،>Ɵ~:_{{D<:9VUG9G1{TFUl7X8263}:6Wk|Q򳂶sOK=yw,OYw68n( u:@g3 t:@g3XĀP .`hm=*EF6H .傟#cevEĎ/}hmcM^] y p_}[G;e.V~2K.S.*7z|X?(C<-u}ӏ:+9HHC:ژm$u2ԕ1bE YoK|2}ٯ̶΅:'Үw'u919^Q*X'qП9S˱r2c&/ 753˱hל=|hɳּYHV&/S9B73:rco^9\ˑ1w'΃<OnL駯rחXn 9_~stlĐqoO}K㡝Gc[un۸"bϱǦr%c&]Kr^d3`|O'ombSc_دfN33Q1N߉5<3)׏ >:@g3 t:@g3X'rtB\]$T@u|EG+,mrWS71KL押3_iklTg)_Zק</.&ib˼b:5?O}_mRj=NksqM/ǼAO3g,+lˇv3>,+o*VcY<ɳI 36뗣u}1Tā=QXKHw}ˣ>Ϙ2w>7g77+CX饝 u9u3lip𻩦tKn|i/ z^Gu|J <T_&N9N@mnj<=F2~}SF1yjMTc֧SL s Եoډszs>$zYř1/WC}QhSs@g`I t:@g3 t:@g3 pE\sR=,R\t̅>e.jou5=B22ՅP7[[u[.*k?y[?8eΉY]c1/*o/\=]lr#gO^9&q|q =J>K8*7ƥqy_i_g|ŜFv)7VCO_rq 7G73Oa\sut2#[ro\q41*݄ůrm9'a+nuŦ\8fmhN㘟Sh^$8[LȲ_-sw׿?cl/cQLiO|1~o1X|*^BO}'7Ʀ+2)-|[^.F_n_r~r&ck[?g.k_bwHڹ86y&丷JQW?chQz#}>%[ǹQ?1٧3VA~c'0d\pp//-p~'.6>z׻w@g3 tRrsA.e:.ƺy (}?1C_g[6y>9e,&bkbT26ԢWl7N%3.ēq1Pl_k Rq3FƭU="wW㑘Sml+s{2'eik9qo.g5zwm,~y7M>7_H)3vr R2Ts6Gd)+Oa'ucf6Gy+Ā^ŝ}8n mc蟣k Sg 'ښCKr$gPmN{ oΉ;e#9&}/.1(qУ G|1яzw^<{p23Nbǎ#˜s[ʟ~͉v&m$&N?3O(ϼclbJgw6!Vjۯ^)S]3__}C[򖋐7h>{xc;tIrWbS0\JWM+_-__nMnmo;{weM} Z֨+t:@g3 t.' }]tNxB^]Wc  5wQb~U+eo2fѿ9R5Gl3+lW\f>YH<*/:&OKrA5V3cqhSX<ư %~?|ŤK|闺:Sn8&N ;bWڠr9}g<9C/wHgTz:5__[J^ŕe:>|PWj|x=5\\8+}8s^/>n <|[͹#FQӎc;~z':YFNN@?B"_97FO+ٟ9Wƭ1S_6x'̧<-6}т =cyOH\V,ƬhgsC>XOγ8{ ; ӟKyk_(O?}җ4Vx3VwuEI _6);Rjm t:@g3m2~p=9\:ico}[ӟݥ7~7u|w9Æk^o?>-nAww]vexv׵х)[]]K.*G.M~wڋ/}hk|<[Ɯ"sCũ/-Nq17\#ueݘƨvģ^d*7/~j76rs}9K?}5qUɋbC3~!g99ѯKQ!7N+iuX2窓XFK^bs}|"\rL_ףo1V,gNԕO|AF,W.末r&~c6cQn,qSrMʍWmms1IT؉Kđr}O]^g~$4ҥS{'!m3>^3o<_b878K]C_]C\ OU}q7yէg#qq{ȫbcs4qXܩ'K{gtg.y+ʙxQ=}'Qf|~]Ēzg`[go+rn׽n8Ç[__ ԧ\}ns /2w?qqg x=s8#o~ |;'4g6/cbԧ6Y|{^yC|3Uzpݭa{7|mvw]ڳmJ˖l3ox+1;]m?k3\6em?6[YT?v o~󛇃:h1yo}kᑏ|dg/~pE-я~'R_ꩧ.;tpg%1 EA.lf7kos9g8lSϼ'nnLg?٭*oNC9ۂoeƁ8#~Tx12oOzғgeV? t:@g`]Q,|xK^2iwc~[ _Bʆ}OO' ,_bG>2r)'!]s1.,Z 2&S`%O9 8zn:υK:䘿o%O_5vKn;Q;.Wꉕ1˜3c/#CqؠX [|~9Ori;}C1Sw>(2|OŁu1Oxqi"G.Go꫇Xɕy'S16c)LP,mKck/UrbnU[bQr*޴<ґ]-ޯ;/d s\9Foo#z0<♗&0,$/lg76;i‚UzNk',p ?wnSGmz6f99yZŇ1R6ِ `կ>h2˛|{qV;lv6e x#ǶX؜};k|H׾vۈ\7 ϘZo6OF>c9s:ۿᕯ|墐LJ{JW TFɃ?Ko>UlHh׺ֵ4Wf'g; بЇ>ԞXӟpݮmsc\⛼6>}p"J>w7NnN|'Gm~Xncs t:@g`3+~ɟɤs6l>_;דּj|$C+ۍ99}cM=yOm&2';̦rA?Ÿrԅw/|b:Uժ\LXW.`bL;q4Vŗ1E&#t]D5~ڧh);⤾B|MɒOK,7)̅9}SrLxoΓ3&z57(qzň<S~WVK̵O@G/gKr8ՏzM9X7OcF'}nPn̺i#tvrk<9>&^8W^Ĝs+c+b2:b_b7iG=EE711N|ȉc,(nUi/11=!?uXUvӆcߚ?#W_r}qJ|~S_$*M~Crd>V~sоʁ9Z^]P}GK/zY / ~.*cYޘ1//_=o_D D7nal`Xجbs­Ş'~+qٰLr6Wr#_(,M`xf&W@@}noG}tۈ|_oot6UG兼;ζm E~{ L51^}pO#lrzF>ԹR l,Sظ6k+)ıʷvFc[6|׮Fme)9 W}Px/qr\??f;sϲvaXq^{C IDATۉPAY.8@g3 t:ɸڗ(qB(ws('R>O;=ۿ(&|wSNZCm%.%;dpEGs|NIzt2c6(^2&^d,s {n6vbӆ>N-v_b~(/|%qhC r7V&/}r+WS8}fN9>6WŒ76ɵ91h㆏Lz)~kJR`Nb3omqa =ϘŤ)7Oei.կKھgW|WnnqBN[;3Oe.q`ē\VХ+ڥ_eQ7mױ|ք[kntgm[ʦ/B9S>lu0]L&W>T'w巎q*'[hc|/,tWk_^#q~nkw{Q7[o|]ia#VonuxT-lp׾]o6;S9 0ys-cm>YLC:j܎#m^K*Jyz]F__܎ܢMr9mV/\׼fgne)9<_8]zWB(۲>cX1vg3 t:ua;p/w\BN Ql8 |E;P~O~Һbt.NN-hGEƺ8I~\Ԧ.B~lsL=qei]?c1.nBv5[~rNbL\. #\oN9jFDڪ91K#qlc e9wCݔQ'.3vck.Mseā1'r u%9ܨ'ԇ1/\G3C|s0N ::u\=LsS7:Y.s8uѸ Ft3Wjlb!\DL%dtȧޏ#um,FtKfU/9|㠍˱w/Fk8𘹈7?䯎e}_&v|o.:GnA_9RK椮̥bI~<+S='V9gڣ_9<ȏ9f_gnV&~\] 0>6+{\h׽uxT7G9bmЛՍ v?D =W+~l N5_9+39=4\L}8LɖF7 rpܮg6^% Yfc?Wav+[Wr+b~8sS=m:RV$p"76o)9;*;$pbW0ƽt:@g3`(97{;lx[Em ;sA調$fu^ 8υse.՟m7"z+BS7s.u\ {xx^l슍vC7ĀM-ԫocp2i/Fu<˜69Џ2SͩYsT]93Ĉ,Q=lO!7>79>2g~Աb_稹OvJ|CW7ܱ —tE7\c$U6{Y?ɭv~~0K_ԑOᶥYxԭo}빮Eؕ|^ gpK<,sľխn՞/˭8ucW>_ʸ^ng>$juSNiWn9=uAP\?ܞ Mas]4:4rms _FYjKeǏ??>.R_4shn%R˺~Ϲ['T,u5{۝@g3 t61>y|=yvp 'Br${֓1߇}E#x/Ba.Ĺ@>r տi]۩k&yL,k/xB_S&_| 16팧)LOHr<93gUУS\,36~re{ EOrL򌁾:ؕkr3R.MLeCGǀOTǗ/d!qCOY1tÞ]k%sP$I?Ȁy^#~ z`aƕlgXE)WՇg>lrn-ƸR7#"6py&~=蠃 ngqp'B6{Y7ؔbL٠g3­xwE>O 1nO s\  N}s Ǖ\% *Mҗ%Mgq50s(simWs܆g`mec=ڦoomtx,eΊ")YGqF-7'qNs%6_:}' P>񏷱bA3^\uWrQt:@g31¼]DG^{|5 Y@ș]v٥nwy'}(o,,@H܅Nl ⪋.i T_Ĵ@z.W"Nl2'cBv`^#73(Sԩ|jSs^g$3cAܰ0t2O5OqЧdѯ:#968%h,c[_c>69H6dXdom*oA$6Ip|1k'n*91ԯT|\}s< + GSp,b,UFLҏ_,Fy0Ow 10v憌snsm hڠ6'y:GFz K]_mc<؋VO\ܰ'X)v$M1K.qB$?ONGڈ X5/sHlcxm2) O鸁B9&cbWXs&ubO]׷G26ԯq7ߊG~\= tG6@< o}kgx`[eSO=v*k^Dzr -ggr[S6×*lY&y7w WvXgs_6"YLVdKx_ElreI'4aG# IrH\fÒoa__w޹u]zs3~=atx7'~ oO[@cÑ8=O4n60<^~_ޚߧvZ|1W,Õۼ$[K Ϝ ɽt:@g3ؔ ?]}kç?v'Qwǵ9NF74hr7~o=QjJ.2l-Mޕ› .tj.ƮdAO\D\3.1s1A[O]4k'W\$3^]7\qbLGTj;%W$'[]ĄcV<.rd'}ҟ2n|qNrCj uknƐ?A}&sw\O7RzrI=XI9W5zr-bY䈸f@O`/q&^%9SGꦺ|j>xzb5W'!ׯ8M;VƲmwG

3sOZ;̋>܉۩1>1ʗobƼ+v;ݰm{ԯqkn9G'uCYbJYM,Mr\C(/qL_d)gB<'9J:99\bsɼ|W~^Y c7VqorΞ6>/R}p衇nrD\5̙'x&\(Æ>e!+>E?:o[/~6`/@g3 twGtd\M1u]mJeNZ8a3 :?$6.vG7 6~/]t0'dxWU UQ5~m;p᛻P✋~r@u0SsѴ. ?؟\D7!$]8Ek?MLw壑Kƙ#inЎT::S8ɉ0/ZRc7&v/榍 59G;uQb:Ss&qC.ԓ+sM q9f:o*F x|dnH˺\xd1?q9i&BjQ7^։c, =7A~l`5ډ×`'G}ʹX;}q#.x2udq3:;X\2gddb͋>z3@e9w߸+xq~9e \OߎU9.ƚAs-rd8ŗ>9fqr|%#%|ӱHlGb 8#rnx&0<@'}x'v:}Η7$vSmc)1Ϛo'O)Sҗ9LIB}fi/ a|gC\,i{\ga.X3;w~x.[\QWorv:WqUp/=)Oig.O}Yϛ'e)=; Un?<@t:@g3 ly q|WS9`KncV|ʔ.|;Yb 8p0}L_in.#}ƮU{s4G1v10im1ۅЩ5gc1/u4}f%\lvqUq/9o\qg< s8GǨrS}VWQ[ȱ@5ږ'3wb9\ u$ŎWoYG$w%j6ѵ8d,$7իxd!>m92GM]?Gη̅:'$nynb\:MȊ92Ȓp9Krz%vQ[bQ7I}zʿK;دo71G38OyKүv9Wy9V|}<D/sQ#r9R8O)6tjMcb˹CI|1+М\M,39^c9ȗUqN~)ǎk(*5Ė)@g`|C'3> WUַMozqm6nǫm~6wq59~nKg3 t:@g`s0b,L0`\\dTN](O.ZwA!˒mcgۅ%U5ɓ.ځYm+7J!!W_e>˭~Bꈗc x\7֙wSkOpO+N7"&97}G)is憮kB.g\_cF?Ưar^#*띁 a lfm6ECn^:WuQKg3 t:@g`s3b~h"}( .v!\ (S?10xW7N5K{p ع%e`/b∝}k}ic>}o⢛Ak?>f~1oG7&2'96o\J+Kć픽b'wpFH?,FN692 9/5m7w.98/"K;9ᦧ1ZNsނ37&P#q,b+'KqcE,yϜ=Ҧn.=3nr.>I~r^eDOޑQ5mȒ{#WQAuL)FdbObm B./b'X,A_:|s4c_c#I_'OYEʘ+61ҷcvg[,c!n|բLhSѓӱO>L~E.O,@%SOy֓[uLm ^^AILYׇ=&į~?veږw:@g3 t:@g3,؆r]:]`t\s/gdZ*/WYFWOrg"83ĢO9cb?ckLSYNNelcfԍ'uG]X6W}ʵm",%q/R* lrNӦxL\ŤȫNg2[>2#!Ƨ&>hDZOіwObWK179pNܼ̋+mRO橞5kJT.P.bё#ixBXbN|)S>KK{fb/rl3.}ǾĞd")ɋ\'O|SR<'gC_Է.?uO^)&O[owe<=@g3 t:@g3 湨 ꫛt.b."sQP] X5FbğxOؖZ$5.ǚ1jLd.l'6hCbY!Ouysu髋gDZi_3_q.׺'pQ[?mmSg,їJlgm IDAT%;eyTb^,SϺq/Fu=7;όsۜ*?9GrBf^ڂg8˥>9l1~Ί<,V92GNǕü&ѧ^C&r,?r&cP7GsFlx8~lbv.vUrv%/恽D9Z=X9"fr%?a|{5mr`^lRw S?+81A]|Or<;b҄k䎝}^C<|ѱҧg'nʫrub8>~Csu/#uPeA:ډ~lWʴE.FNЁɹh~Ma,Ϭk/ٯO_ؔˇJI>3㍎U?Sc2Ё7aG]2ޔ_s=8%GF_r|[H[=uژg?vu>z@g3 t:@g3XG\ttζn:b zCJ?#}_MO-V)[̉GsU_S8lMۺ U?cMܨ碳C;K-Ws\Wg*^nOqc..Wğ371|io\7VÇn֡L]Jr'1=u,VF[e71R~З7u=9"G\iGlpOAS:ϒ׏>ul˹d|SG,.G 6V9,.>fĀos8e]dʵI*W8&ɛ)掎sY3fvK;{U%㠟%WSGQ;2^- r.y\]yUC t:@g3 t: хJ\ǺIM):|&/ eӅBb8a_1ť:Ĩ9|쫼֧~y.6m=]$#i=q83g1/Ŷcv1yk1 O#KnT%~=uy^M875s8l9&mٟ:~T7L\X'1s1#2ʻ1~j\l, ~gȽS[p:Vg袋=//ZS+?]|L>!sԗsHΜgr? L9_w֜X{dG~u09K .g^klkJb1wbSysȔ㋺q2&ĐSc,GĭOS/9H=Aa&qC#Wc|Vċ1]ʎ9LG9hӇ-2K'}Hl ǯeƤŧur~Is3191n._}xs1Lo߇ "_╇߾=+oe{wg3 t:@g3 t6/.j"(rq6Gb^]0\T~ѭ S8F_.ؙOXjxy~rj,s/sqZ;P\I3'qڗmsQXW|'<]2<\pW^4QsO78;}'ظqWkĆ̟c>>tK2n 8ccUu/Cq1M_Ny~#9NWs.f_s6dʝǟ>1Ol"}fEҎc9ibu>ү1ԭ5gl%6yGA9oJkɿ חu1iE_cw5'ꔴuoj<(J^ۏ [@7>.z]iGœ{V `|lg\pAKg`kdկ~ugyZ}_l}/2r{xk^av? wLW!? y t:@g3T yo]̣#Stra>]4mi;2*^0V28*W6G_sYKo&G+?`>gnW/1`S;:ʋ.#Ƭ~NϗvMg} c._Ň~3;qi긡`0'0W9&Nx?S*%u0/Џ<7jٗp\ę/u6l3a 6wkuIԖAMdn69ĠЗ6W ~aա]]w|e #&kOU}k*k|W(va7g091tO=2bx8nKk͸h >Ujz15#DW2w˩ةg^9Vb|'؈cb,c4r/n}%.}+ヷI[u\7\p< ۩$>؉>;ʀvS7JJXL:|`y~6\'_dTmo;oL,0ˇ6w '>m/}޹;y7ؓ7Gʵr̸Bn^2oǃcn8(m|=HN '%9*s@_5frO7Tcܴп_}d=ri1M=BF[?]'9N|.Vo}ӹ 6ZUn賘gr[^őv+:B>C"1 M_||aW.ˍyqo|Ȉ`4g1/$/>XĈ`O5ñT]m8f\qaűpiyY׎6&oԝW9s?1mڧNzłc>Xcq#39Fn>mrn]yƗ/-2rom"w{b9߾ğ\zg`[fo eKwpիz{pk_e;˰Uwf?hsWI:Fg`b7pի^u8餓{]vOzғU w8`I t:@g3XM L-Xsoj1ﰩ:z#rSN=ڧ.B?WbC15ٯ}U~o@X55¥\}|LO>lI9Gūz |!חo,y=͙7wr>?sO-7? BN 2rg6%1eNdL>G:rX9DB6l+qs=!'6r+m9dn63mͯTh; 2yS]dz'i mc?S92/n5 WYć/q z;3:GGMrs2}f8R '+qaonO^+f/}덆k|a*>^;_yak4W𳟜d׿~xS:瞭NᖴGyp[b8蠃}c ~u) w݇=cxԣ5|m~'?ɋb7N?E2sW/k]cɿomR}d@V7Xț9ʗz2ݴ@?se=-^lߨt3ŭ}r/95.r7RɃO{Glj\ϼӿ:c|͕~NQN?&~LҸyeQ'wk GS2TNycKco,ȈK/O5%%~W{qCWǧ}AWPϛ⸡Vb7hvI>7}.w}'Rx>\ׯ;z5gM%Kێtdcm7MÝ|v×w}hn6<=ymCkw򕯴׾=qCv ׾nڧ?/}]AKg`Keg Gqpgt _Jx/_N oH x\sks){P=CP[tpG'?A{)ڸmv]wGKm"}3 t:@g3. :hSF'Q<#sk]b:o|΃DuRη8rLŕCGm/>c/kh|1Moc,ņ/+'3sgrvk_ks|,_^}p>l[<<0eldu5O c'XRGNk׉_<=>)rV[m5[@!㓵<=ڠK\ͽ.k4clCj1ϵ62zU\[$.Fw\%y/69W}51y݋\ɥ9[n͓b7kګr?p>ҧrs934y2>=k_n?1?q1'r?8M_N~k2z:Ň<1K\'uku9QyL^oyG^+$&G]Gls3uGvc3B\67us&wkGF|NnҾ;CG@._ |m[6pße5ח_Q7]4Yh <//m?<2Ӟ痯y{^N믿~]X'=~|;Y^W׾F[gd|Yg;n;a| d?<8Yxm}l坁@g3 t:CQ^E?uBm`B.Q8mlm&~%ϼyh3mq s/bhozK(}xmg,[2.;OL|rژU!6O5V/PWY طk=sg-E6/7?z]{ý6|>\{gaǵ7y/Kb83FW.kjc(X!'5#hkmћJ˼8Č/|K?b$Xq뭷cglis$c!_d1t/3^ѧ{voSiK9ip4zqZ.ry2'9'-b3H<~kt/][捅9Ͼelؘ6s,NiPnǹr/nwXӯs~r| 듾yoz53ߗNg S ?Xߩ8L1;=o/'|f9`=W^; gw %/)W\q7̓*wޗeO.Sjr\SGydԧ>U9;J{ <8#[;>mi˻G??f=w Of"}3 t:@g`f<@.C?YCg~:o1kl1WbVGzbσrz_L>xy gg i>3ٮE˩h=h67o9fY=q IDATM-'H|Daɵky0_)s7sY[6db|ϼq2댭2}r܈)c12Q>An̉E,%Gš,2\#6w>c X; #F9wRXe\q{1^n\7t<{Y$ԯRϒ%K/qヾv`Fr+forq_9slliW Wkɇxbd/'Hпz}uyR|3^3f3[,Gӿ= d@/1GxH}ĕq0'oi'1A;'G_;uNM.8:Nʍmy 7GyDcu[ā6)&}62#;1z菀@;"0˝Sq5\߶tʙen-/[ǮelzԀ;sG?Z\sHGi_͞=[Vs坁 oxGA]f~;wny_<Sv_ -,y]m\,^%NÏl09?\P4Hltڢ{g?=?mڴLJelQC_أu~?y6r$y/rkcOa TB]rFUqi#~,rA&7j/J9vػ"fWD1 \\Ǽ[V<`u=oU.s3=c_=m:z) c=wW|N*~O|byS2%gK/] K!u ' ] ُ 7P8׽'-:Bz\'~ w)^ 8Wzx4:wG=Q3Lk~_c tAe"5@g3 t: ݨCDttP/Cyy𙇜bps#~ҧ6Vb~XT3.r6fګcGYr6c>LյxIեk|xwb~0ccn+;rĜbc!5Q⯹,[9ᬏ 2/9G^Y4rt\NM؈ښ#7.94s3vɷ>Ƶ[s0vdr!>tOA cf OWi|DGc;|Fɉ[Ŧ#g89ZZ>^חX9aNI>{=*7{Bk1L=垒7۽g>SS\cZ"Z>r8[5>R'm33 0o|jDOzaG?1B|/qe'8z) (/viBֳXL{ғT>ϖ}cgcG`=yOYxqyX򖷔GHW]y3I@]m[g)_oۇn5Y8T&Ym@g3 t:ud60c;Syȇy#}:V8iy8^{Щ<ćf|g{*] 'Y'e,d>5UYGnXQ륿ͼ)-}]y׼5ymsDQ?rG3s7 ^/s+yX~1Ӊ9#V^X?掝{JV<:\~17E\w>+c2uI.>{|㋹CNi]ߵ 'n -k%OjtCeLc 7>TSny-W̩˘&.凱gꤽ8 [q 2ۻw)^N;z̉'Fe~ħ-}!unqmk&K~+2ug9Oro>Ez/u6Ř1rqz6\9x3~Tϼ3ǝMN?-F# ԧ>uBn ܰKkximeye]Ov]3 t:@g3x3|g|̖t10ޫbf6,X0Q/g[np(j˃<@̃p<0CҜ}Z!v1V'7}Lkg~ꢧx%W%?ʒ_q8X^ǃ:}e.+s<8<{Xb[i1On[_V`>=w-?ryk5)\nd܌8'.EIӜ(.˖-:nݯcOs= qmf.'~ѵ?CLr-~˓1scT~͘fޮ64mU7; 3:v]ק䳍u \ʍ{>|3#ْ<|,99Gzǵeu!Nʵ2l ;s_ k3<暡%OB9[wDȇYilĦ8NS搸/\'gΥrD tW3p`o]r%Zmynr6޿1{VoYH˜W=ry=Ig3 t:@g3 t֛9hqۃ <;@1m>y\[Y{*b}hgbX#}!WCgc%v7c( g\OybWˇ:Wr%&דkb@=@o{@n9sFNHNbgLC9OtoqقjƳ@{tp؟a` s2V22`p=uEeO=rs=82cɭƗ;iK }qTK)K؉\Ċ5~륌^x qiLN`ls 1GqCcu51W/v-yȓ6Y,SG<#sJHĘw?團x[@C;t,X 5F] E\^_>؁?޽_򇝸(yƢwc>O4,\`tyjk>3GO^=z[&kKZ̕F_Ēc/06xI~˵Ź̵#^x3_i33 2] !t:@g3 t:@g` x8bmکcN \$z-pjyOvm.\qy|eSbk9K?Mm<[K+ֻ~Urua惎=s^rz[l(XlLbk"7=5>eCoױx,l!:3wצ͸r297(6f|qG †6V]/wOlS`cxˇ1zD\''b@rõEy͏9`\(fޕ,=<2Z6'z_%6=5>2?-r]L^#.{`b}p?spbw=׮%q 3?9CO Kye i Bx#!3=7JWĈ~ȬM'qc0oKoȫyL6㓏{+60/FkW?s5DG~Ӹ{\3nbR>6'ry?חJ#yh9ͽ#_;Oe33(=ng3 t:@g3 t:Ipĥ9lTlr5_ȸ"#r9f?n>-H2k%\Ӱ1~{Ƃ_nsOoޙDm29GcN?⠷S8ݛ޼#G1܈!YՇrMc+v>xq^>9 =ƙO믾EV|ɭX%:Q+:{|y+c\GaPN}&v[GN]Z~Oc+u@/o; t:@g3 t:ȀlyX36XsZKlc~#.XeDڈWNN|'G[d9dɒalQԻ[68`gT[>g̘QM6͝ s`-pt̻F`·| v6V~õTEG΁;}9H;:\O#sȵtw]k,lKKɩ>3kA36?q[ܣ؊#ɛ3>cĤO \wؤL蓿wUBk W.y/׽}1u+3GY|Ҟ9l(\@-%wQwx-aN^7[¨P&Omn1r4{ؙ-:^Gݣ{thz`ݿ̡GƲ0cɇs1wZS/s\bEs;nqmO?3:ٻ18rL/ă껯vꘋ:kdf@fy#s)OWb旘ż:|k1g|_g;zkeo>sGXh#9G6NOQ[g`C3sCM[o\3p璛<@6R+]S&n#; t:@g3 `CWĐpA<֗Nnl<=}$-v][q==1k'~q%/bLڹNcv=Č-vr=u2gtɅͨ9s4+ĢqMSůMaAϢlbwp㣐Y9;a)r )yr,s,w҂!p;&.w_Zb LeL×g氧]J/VL#3k[LL1ԇkk&f>ROG=e5vk:%^?:ӏؙ>soW{sO\e~kkuN/.z9rĵ%b65ڵPaĖ3k2JL۟e-?M/| ='Ɖ)T}f3֏Fą/3Gz}2t85@g``?o2&uy@8]u{@g3 t:@gb<0<0σAl=p@T=9!K;;S\bi^#/.{D̡壍Mb`zxΐDm2u}_KoWɯӏ^1k׌G.9o-/c}? =b6Aķb>3|'P1G8R&%^sH])%g~ߑ"29e8^\cS~R9×1b+E_qa?zG,eqZrsYX'~= mC9F'!?x)c 9dQX]Ĝ LrO{[K1_i6(y`^=uw!ˋ wgA/nz˱˷94_W{C3q-g.^[G7q0<1<*Z{dٌCֵ IDATrd~1ryX;sDn1#^zsVױ688Ζc'Oy~'ő|Řz_t֓ٯ_OHɬ|z+~KEnҴ1+60kGtۮ\|mtˌYemv,Sf1J:@g3 t:a7<uyd;Uxj}recjخ|&)3>Z xɃc/ϼKꛣ{߻Nk1" v.⋵&qB>o s@/-Z0;dʵwKȒ J] ssf"EM0#Akq/w6|9kbN?1ONml\O\/l?lZaN~{uAǸIȉ+gCW9z"K͕>'1ȫza'dٴѯu['-qg|q#SGbc\gO䂱~,Z,%Џ~Oy9ׂXͼOC\صS?e^rc=1ZI>3>mgw6Fzx#XuK/^ndzYyǰl12u2mޒ2iM6) _]>kHfo9T`٪Lhe*2>"zVsto_ܾm|:yMˋؾe[~οbz)ʷ_s0y˥7,-+˯X=7{,oy>卟?['R/s8}sW' {۾q'/(yE+O`=i*|G4>;(3n^-YQsu\ ;O+/;rn2yM˯燗ݧ l`'teOW>θ~xܱԲ|唋wryo~y3=yʪwc??ԥ]ze[o]Q}k2gI寎WeX\M5hq^Y+]fǑqWKʻpYxuc2\7}<L 73Rk曕W~ 4g_+3>lkNprUe'YG<|O{#v)_zSi-|frۖ=oQԩu7efeSʑUɶ߾|邛{v#VϜ>~]yN\wUλ~ܵkFw6SI,,+k].yeR-̽gW7/Gs\y5eʱT}s>}VY1g5:7mry(6sFyq-SQ~zӋn,۪BF}ܔ '}"g[o-'aʓ 2bWs5rMs]Cǧir>\&ڸ+~c̜ .ǵ=cnl n}='==?1ܸf>2PaM\1>yr[>m'Bodž[$ĦMLRr-.tXS|҆|j; '4M82XmvOV^> {?m^c{EX}&7ݹ?-M6snRMV/ZfOr ;xtsi޺莎qIc;)Җ\tzCVҳjQxv|9ew~2cJwϧ.dn*}oۺ|W<^zU8V_߯ˏ/EYbɸVv^l^ sUKU{P/S{qO_=xM.=l?cJ..խ~m1ȝʗN]PW7㫿\X>sy+. t:@gĀ|kꡛ(ڵ+yȗsxiC,6Uo!1m,l313 տMˊqϓ(/|[[ Ocet]eeEp]d[qUH~xu}vx k!eUx蟺یۏܡv2y&OtA},(̞1R.WpZms❿<;_^ܱY}7 15隲Kʖw_k5UszW [W=Bgߗ~yNʏ~ݽTy|wNGlS{묩eIKc}Mqo@/ܯ7wUN`=㣧+ ~C-=y5_pi//=V][.~ݿSxޟY2bLwR䵵knY5U}C}›ʉ/.lQkݶbk痯_v-z̜rC-EXs}.ԞTa\!]Q t:@g@8#hzA{y]<vW;罦osgqo[d᱇;rü1XF##k풳v07^98oobss.6;hoS0.9s'{񘃸ڻdM] #= _؃¤yi7z`栮8NYyEYE>Xn/E7 #qk˫?ݺqM]G^3ݸetGH?:rou k3.1)#qu |s2 \4x$\+cXQ߱1_z9Fm.QǹĤLAT[z>~/MN}ɓk 8iX.s-.s11>s:rvI\U5r3!!}&}o(]wlZnƊVo[yZ כtsVח#j!8+άqg~}ɛ߶ԂNq-Nź/rU]u/Z V.me)+-[ܱLz<:m9^m&*Z9dK}1X?\0;z-6_e6 }e˧]5);pj{8\yޢok~a,(C|S-8vngNJo:˷>i8^ӆO(|n]%ۅþ^w޼rl}\{_;Uߙ\}ev/65 |Yߑ|3ˌ6NSk7c 'o8g]S.Z49sZ'VuΚ>s]/wsZ=9UTYהv߶;ˇOZuwuc=cwn_&U ţچswZÃvkw3V(n^;٫*_2rskͩ@g3 t \a% a]{Ǽ˨kcI0u1;r~]cDk/&27!6Y[seTw .[x_%06|lpwY-SEN^4b >WF 1o>GWr>c摁kOdc/1sk|qi5<ˇs>N'Fؼӧ(;mf#\2W1͵6-8r?c׭ū֯r}'Q|$}Xl)o2-''}J-}rr^zjWŚw>eؓ띙/ZQsf囗ݼN6gŝrU[;Il5e_*_2uޣH}r[xM1XT&3ѻIow>z-V^3Ϯ[+n^Xh6-3wQ-/;@m;9_[/(݃gmYNaͻ9Wmc_Tw& k-v-x'~LYXLJ>rYpӪ|Z-־QmZX籽?kyAOd^mW0U*s3UoՂ9W..'V_;}oڪ㦟Rߏ}#+݆Pѥ ǭ,K߭L䕻G+wTېyim]Ooi}{9zP(/~e7'U>QaNC,?{I±Ge@g3 < ECAcd=M;}iCχ.%FxzX+{ȯ OŖ:큥:K`'1%6u236c|?k ߣđ|'v1}152\#ysym^R}Ʃ+9Nϱx#@1@ wb1 n9 1N9 hň 2;'8g50KN(z?-嚈UM|8fǂ)1d"W`ӧO<"+jN -*>\' 4l-V`e}l]#0ts˵EG[=4?imCzgl5^}&fzb+d䀾N[lڨDW%V9'͸y ^~\Gtg@b9O_)bNo\ K^,C/b93/k؈?Fɧ؍r>굜u2v*OM}wfcdǮʙWwWVr~v(Mnej-17>:'6kA]˼;\,wXZ]ua֛w~Ey;:w(oQS띌 ]mgieY-[ey;ׂ+#y.V/Νkk>fvyl-ZsjCV rvNSˤ zG[_|ݒ[}|5rO)skL//{n}/ 0egSn^o읭&~]Xz עǿ|غ^}Ϗe<ǯVzT-0咡gY.( (eG/N;K?G) oYQuֵwoZz?[|*.ES؇#=ǪDy`W'^XRx^e7ԧB*u:@g3e>soE礼lqcs6L@WnS}>dT,Ӟ8ZGr;\]s:s|>NMbmy1W>Axqޱ/:rFe9cJy3Bi# 5w΂uB51h"+:q|\+y@7Y4GaAN ޼ v\;V}5- ;:rgjc|KS{`cƗk]/ cY[<8~>/c /x[V|P}~~u/LSy)UBw/*C?۔z%f=o. jI-O.OsP~?Xy~h}-ϝhs'\P[~b}~.bw?n9Qq}2W/O!6\[Ev%tel?ç'yiٱ! {@g3 t x X=,`C<D?3mh3G׮qG\{]%&%mܠuشg9c!v~(ŘE a)RXYfiӦ Yd(eAb$DX˵dy0`7cƌ!\GG]Z#qAޑ+?\v Y.1 _uoב9s%6>g>'&nM:lma`53qŅcg>:k~rOk`rۘ{Y2&~^W6/2B;dMX{mˏyp늞׎>Y;=M"EmX\[@v؇3csw1-Cn\s6?5G1kev__b^'r3gMW˯{Ѽ57/sOǝ^XW~]濍~~pzcdϝQο鳮/߿]c?}ؘuIݍo||tx ,K.8L㟱s2ڬMJ嘆.[1ѴUz21߰zr쮫2E-Gϭ9וOG#)6Tvp}wu~㒛˵a;=)NwZ^~rBk[Ooʴ[cMw*׻ Iܻ@h@=jZ|^>;_wCvZ~o} 7|鼲ك;u6WMQ-\1+brN?vjNn<~rnu~WN>F}}8xϼwJ :@g3xX0[.sƢ!Io1 _9׾-`cfvKn톢)F&#s1؃e|z|q+vr͑BN\uD\uQƤwς1ؘĈxH\!l`>?9.]t\S49s٬ܐ59(r9Cʽ2փX~F~b&gmBSϵW9'qkF\K!Or®5v1u8ִq ŏP˜%ksO|\-/Kr\E ƧGZ.IqM 1/9s5߁/ō9fo l G@o+= -cv>Vȼn[$R,+<>5篩6{q^}Я^LdsnSfQݲrUe 9O%7iy3 ;\]+7ϭ}.]Y93>ݷZ9>^{Ϊt+獌_.,/Xݥ_Pyb};W9؊M}tLwaV!?Q.,Z>x®Z4>˿]]}KYX99cbKT=ʁ#uF-nEᯏWܷkO8%:ϪxZX}^?8WM\/.՟4Qʩվm<>}w.^N{ݫ]}K?'z>GA9L׽3||S t:@g@syAyPg.cz@遠赇8>㨟7Hďzic'sAOGkΉŜ*W-?s>qT^əXK^j98y\ȳi\r|'!c:k/H נ=\GEN˂Ar-\0-,VSĢ`r|k,9'1Yd!ͻOc2`<93-e~\]7X<32r3u_\]?FΜ{13/f@nq9{-2~-##y=%1ytNS|a.m/##zcgCO c ]ʉZ~}?G3wz5wlӷy 㛱'g\Ԇ*WW(NJb0/yşv361QĠzWn2/MlחON4*O9Wy:b@Fkm`~Q?sa Ce ;suueŒUwwro+ӶZRnn5,߮Gyg9tzwڹnknpJ#1;j^-Xit~}jӮ.;myycxqf<%m|JYt {wb2eeoj J(.2gNH){{}ytZIeN8|UD.U>Zq?iAvG۪~}szgO.XX;̿>UG//~ny7]UDC̩Ŵ=q0ZT}^RG [^N p&eM]8> ߺu}\ϝ](J&KT5/m;?w l_=ZY})o8fް=tU__?nn-Ϭk_X+<?ջ~AxmCAQ8}|Rv;~xG7OQOϸo@g3 l| ɂv1`a!c{ز:;A=LuS/})w0z0@:q8vOΐy-e yQOlڶ[;.?X5ǴwOҿcq]0jzB>Kv]l(zdo.~+r'O'3\ǵSzp 6ouQ =r`lq/q-ixT1L)fa;yX>\\š]?i`ҿ\#~įI\9 ^O!ĊiħFș=v[tzCڐ'b>f.A/ws7/E16r\Əko9[cfb#7~^262} }6b8Z>)g =LnԧFO3u2sLԧvɩ~\Q/}5mV/sa[g3P~wuׁSN9vđr':':EWx k7_qEɛQo2iee;xoN^ᆱIfͶQ:jcwo|u@g3 t:v[L-F#&{ b`ãO=wOcl=E=VIps7DZ]*䄹CxjsƃTc#cOuZ_K-flYɵJ\p2~[~\9S6.zm3j}g/Gg0>|X s"g)v?(y3ĕ|g5&yB6,PԢg>-ĩ<)ցk FF0`G՜s(p[&NUC|lI=1ӛ0Y;I{lbU_$wNз]k9Y̖/}>;1u}~ Nx0b>ů.3!ڒ+]_N}ĞkyK2_{7G_hc99uuQ=cak067q#sNm97|\ye5oH> s=w]TKg[]ŋ׼Aoxwba&4OtǠ>~S 3>,rkmW_0LwN?A=@g3 t:@g@{y'<Nk)rj6{?dQGϴŮL|TL=Ԧص@ѥETx2S!'. [9yd}z}1j`>6ӦM[]%ɂ|13<[N('`g/3:b'}҃ȯcțb&_1V}S\bO!-O}<9f4uds9 \0fN^Ƈw˕؈Ae羶lڛRoec.?iQWO$v<:!8yRW*@_`Llʰ3Ė90f9B:7/ʸ6{uǸK2Vy@?}ckL2Sf\/Oz-.1fL}=wx?sQeN] rًܸi<~ODG>Z> /qɁ<߼[ܛS[r+dՋCEu+G()b@:SpB>b]_,y =1(qK94 #8Yv |49w%wy> c=x>W}J!0^+p#怜1rFFQ9>G| R4Ek=Гs=6.ɋӎ|}e>hعAs78 ╳S0c+6 p^+O_&ѷ/z͙\ql_ "3Fr`3;-+˻ߑj ,B hhT:BhDZ;1lA!D **`"HCbokϩSU3nsZOyj,ek)A' 8&c'g’cNM\X }b`1X ,b`1p%谯I:Лjuا~{_0#ti(

Ʀm&7>:G0y G]uu wJJ5MbHvos݊~dޒJlväL\SwN.yo"FolQh;~=JYx73Vg&萉5T7qHH҅ss\oу%[ٯa[vpH 39,1DպZ ̫]~KN6yRWKI7GmW{LLqTN=yƃ 6rj׾)(pg}⽄ic{>x"g}~=%ܟqY?lwaQ#=89 ޒi _%^>rzg8|k5+.?16Z\BAgzjr g+mbqM La{+K:3Oza0>צxaNwՋkއJnڡ>;,b`1X ,:¥]|ug?ioNd:Hqb@/y3>c73TN.ͦXM9/},UǗ׸O'9q,LZ;hL.|vGHWֹ~Y[=Ű 5>e;1e?%~8⻘oںU[u$}L_cXœ?udZS d'+/bT'S2Z/}Jh`.92ڒ}\ٌ+ڒ3K8hٞ{~7JN_*8ۈ=)gL3)G&S@~ed;Y{|%g0jGdq1>UX)6n j ?|f{fl^ n8n& 5 K7|Ł+=ӏQؓmR?t͵F<0淽vm IDAT\q[)H>l>3zlͱ+jebMg%i\TRqM6$_Ix#|O8R6Nj/.TVB]s]#5yc?E2X ,b`1X ,'Vσy8 KyV1vndjП|95?yz?s\7ӛxwő܌ca;SvXLA^<%WV᧓ m+exlf+EܙK^{W؛+_`KZCsmէ+KD 3ٔp*ٝu~n a&W0j=qm>LtqM']Al%Sݟbsu 3 'q'ѽ%Pað7}yK+%Q?u\c>c6lg8Ï#Ie~s+<\8#Uح8|ا,p/_?yls+cl^6,֙15^f\\lb]%uM'O>©nZ7~:ob2q~xacs;^cꉝ ݩ(ތM{o6Joy;}yX[%{3xW&76WUO.;F Əɔ^oߜ{]b2~v>\W8G| X ,b`1X ,K_ːXӋ+@!u(8:#Ӂ<țSy\pog@;q5V٘w`t?=W'g_wqQLﴷ秹ٚVaxXq8>v1eX~|[G6=\?K8&و~%Ϙae|15aNeƔb95{]㩄ZOlVŻ5MVA&D`_ҧ20:݁f g)7FXI%(&fc ]msD}]w拎$D[]\O\Lɶ⛉'O'#͕S3ո\i?e'p|X.YYp%ZGKŇl_bNp0W9MuqOf/9_#2zE_jLnrz{41aOkʜi177+[tJ/\Ʋnukd~e4?}7?w1Q|L[a7Sd)2Jҋa<)švqL|i24&|c#b`1>b`1X ,b`1X ,.\]CD c/[ɤ!_r"tծ ˱y8X:4?< SxWdfPv1uw?}xbu8a1ο=2!zqcXgӍK£?Ī] Kc͇7?4׸n恷x7̵C%$¹qVv6NyKzWlfnFUSV2.;)/kH֕=>zv#NŲb|2`]J_Woa_ 0׽u7 _|7 ?b`1X ,b@!;L};3Alցh:t(`{L=Zc|q9u;¶Y{aqx9ĵ9inui1jC6dx0VSئ8ʜG; 9c7W\6Gx≽zmo^{uosrZo{}mO}2׶#h.}c5tz=LL7ړaMXLvȄior;)xcjn>i囿7Ͷ<\IOz>~U"{ȇv<򖷼g=p{k[˯9/̛wwrnwmw|kkekb`1X ,15֡dN̓ym(־v?m}-CCKsvǖ83ϘHw1cMd\Ǟ<7eݷ2}kr:ҙ̓=tgrFPnJo,u_.I `Er|L(Ge_:6VKis䔸kdmJvi+ZQfoNhXW\wIf54g5~0fE%y?$͊ᗼz7qXbVZ։<Wc;fkƝmd C_pc?ct'7ؼF 1lA&n`e#䤴w -vcG]9A=fऴ38g͹ed~[zWg⫤_[ qE^C~fl'df.1"X\7}1̵&7>ޏ+=j/.d/Չ>>Rozӛn׼5=a?{ý}헇zԣo|__qx_M,$v˾8$/p{p;w>w=y3>3__xCzVx_xssN{A/ ~1X ,b3C7u'c|Ls<8v^o̗C)>w`l|h<hy~8L7Kq7γ7cK~>51K/ s0ܡrv}#xjKXd*LsM#{o O %L]wZ榯l.)6y>f %5N>Lvss8|66WH#S)4U=y-w?ז`>.M+\|I[%4ב^6g'!j-aa}X-pKE ^9)%]/[يhsl4m][L^< z-<9z>S7q5WݾX|TQg!-Xnv̽ 5b/+lǯBe_A1dڭMwi_ɴ^fo]~ol48_13C{x6Ov#=S^䳧/ͷT82_l[aag/67cc皱*8ukn6'5,'1 b 2puS+|~]3̫e ]ߵ%=yگ=/|S?Sխ5?2|K^rx;q_>= Ori _-N{wэnt ࢋ.:Nw>,b`1X ,> tX7F\ۡ] _{3cCHS?\s~/xt;ح.tԵ=⩗=?w )ס-eOM~kĆ'wv1onC/=6t0M1=N2L ٘ ek?yشL%;bKo"l p! )%27_=pc[1WJ╝ƜL?h$g~#-[r,Nf"oɕ+W 앀7}1W♎QY~5FxHFwuQkЍ1%7bP#{{\-8Skו1yc=:=`Tc7i G*dv|(Ŝ9]a5+f,ٝ5_GkdNgvi?%qp`5d0q[rq=,yrO>M<]jqڝ5Ӯx\8 w|dbd/lexVor1O6bBg`=*Ï})gz[_;}??l/ϟΗ=//<<>7>_#>vdckGn I}_}:W_Շַ>8Yχ~ۣq۴^vn{n>]n'?ɛG~Gn PX?S>eoo.eҝzЃu3*.ۿOov}97??qW'ww]SWi_"ٝҐ 3yC5,%@jqh>>֙5x^w5q4ؽxGwYwwpv:TG5a/3+ dgw~'':Gcg3bNgӮpg7ksŒ?cgoƺG&󠹘77yM2nƜ_vl^]]m=GX~z{fWFo'%Slh\\L^a],큉qb..ux#C%iWZ4^]kqѺMތc6 =飵O6w{p|͜yw&V2)H꾇CK&Q&IM[w7?[bDⴿ+ ǒ~$^ﹰ"*qO?9؏KK_E_t?Suw8ɟf߬yW.Mt/ÿ~{ nq??oչ2~?3?=}'̓௹}o3w-c0; ۱6kwH_x&tgacanze 1/N5PU1mnk~3igG'Wͩ3aG'onl2q3{~UŘRDtNv窐~\k"fٔdKI's5| e4aV5YӦ$%}-1zoZ_tnDLa>ՙZ|k뜔8gu ;vuhɪ.%zȗys.O+q(ӣOl/v [:qG|ė|Yc%lasG=8FI\kOӱ.Gl '0)q11$>ȉۘ׌3lvE{Q~8yc'o~롽ԟZ֞|sWi~kgubXQ|{Mم|{|~fg–WZ0M_Ʀ~[(#|==^B)g{O F(׺ֵ{J׭ס!w~0b`1X \B ü{*& rhabxu`Ǔ1cuڛ;&}sr3mXjGvg|hz~(=9_Lt):3Vf\d&К^~gLƲx=>1&;咙Mx痍{G_ IDAT~=յá.|OW+plaw /փm?ƕ#GMVI/l;qVd}XX$ =%#k>.j7=J& yg5e G}JO ǙK ;#%Qۓ_\{\wm-kS댹"pMI7YcV?62+| ^}/[^g}gmohlEI%΅6IdcKK(? lZAƝ1W&<@?V1Ox%bvRϱCw3ZO'8g!{6ʞc1_l=yGuˈG{]^:f;v4____mӾt+>0zGv{OO71WRb`1X , hAmxR䙛}0,_{mv*ކ~ò?-xIyNo,1&7Ivwƽϟzbvs`|xOki=X6z4lgX8}ąqL\8fas97_hmȱ$)bGؚ'pY'?bac^'#ΒtكFȵ}/+cl}tG:Hkԭ?ydҏKc}Rts=.nu[لƎbV=ko팅5kk?|tf;yuܐvL>Qt╭^'k\1[է c4^=*9UCz?n{"k/3>c2xK6Z [ S]rͽOr|5W~f>lj+au'ozw<~և5rx8!#I=[Vz_vJrSq8ҟo-c>JIxq{<uLxg]np=)f;eKw|wlw)'̐sbxtK8YS+>kK;ş'nI^?nun ʳ=]?#?`}=S#G;q,b`1X hm>9~Aa5מSӟ6acv8kbcgN{|0S͸j"[} 0vOc|NN>Ůί9~,F~F_5և1}_}.2b_2َֆmJON$g7ڃc,<փ=VqZrO h GwvFc'[k"$rKzyt“Ni|_mמkR N=j|oǼkkOb} Ut'\#x ;~/*O{Ӷuj͔WOַesd1>psAK _s|n1bgzO1XsQխ_U_w {%6[kn1X ,5bPSsH&Nug:,̎yH<$Ms|_},c3L=^<ÜX?:`N"sZ&0u:n<_)W5$|n0$o㓻}3Ή7;L0O%mwficKwRWgkMNřRҐ=('zJf6}>VIn^(0))%q*G YғPě}ǿ‡43g -F|c܅'.%.f\n҇%\%G' ־5#bwXK39Z?z%OJ|t =C3"\⛯"_lèOO|lbssZ' Y8-OC{ME_ ok\&vu'\R7=v`[ɘ8lڍWJkzm?%WgXRJW™C}#%?S5W.z__MX:4x~&Q-.٦9{Nծ|?!?J~>Ò{ʰgX:SW&9ڋ+|\;.ݕp1~%||s(|h=y#yI*E~ի^=Y,gwY(%%[ })?ǖ*jͿ7[br~эn?l_cc_.h/Wq^o_S{LA{ղr4wnx?ys Yzn-%?[_}W-Vb`1X ,C>;1|mlqbמ:>9>esx>fyLŞM<:djJfd~{cϋ&55ПaMN?ܓcvs Cy?&ǟZ)9V!9#;Oskh~6ܩ$^'DJ1Ti}'m 'e2kr =V^%S:9T ϩ_SmH$Ȅxb)IK{[\%ӓås-Qlę}lj3&)RR|j#D\e?6֭1oa &aS:yc{Dvw䚷%[{X|^;%8s֊“#>\Jktve6a#%`g=^bXg?ʞ>Ycl˜ c{Ck K8?G񦟍YOo}r~8[Ar3TO2ŭ1⨘'|5WzǸ=n{t,'MTI%g=YG=QۇIcoЗ2<:~?O=oݒp*>ĶLK{Ӿ0ny[Y?s?w{N>ۯ*g╌d_w>)OْƸ|VsRb-\;Wܭ;sӟs+-1gr{ļL^e1X ,b vZ ~-qk}(nzJ7_{N[j1:u.|equvု-{JWmliLStη^0W°8O~ښkҺ/ba+{df+vX>V1p2浯nriosϾL.%u25??{ɝG2_#G_}4wTuۀ8=bZ[㧍_ -ƅw1X ,b3җǓY]T%gy qp;p ~~2y;б?Cmǧ=;dc(ceorƓ'3F'S}96u1m=1!׮'wذc|̄\:TǓ}\3^2:P|?L^;얬'و7zFf{#]%"#.K%lJO_$S*ޒPoi8c⌧b%98 WʹOs AI-ZIXw)W|cOi lxN`qWl'%yKT..*j?̵̹F+-RJiՍ|ăV9^ kl1l9erd= [֛~#!zsk.n=yφ{@O|kx i|7:tn/vx' zℝ'чoŜ\lϷYZgj+Zk2q>Ѹy:(l'lm|ol3vIcٚ+L qTnzbn1u Slӌ#{S<o~k€=)&c^~|=|6՚^?\~+_ypool^|cd~Vlb`1X ,Z.\m;֟hᱹc;lfou)c,3)\6:\Ns|Ldi|b.9OGO8(<(N6i#OdKLs-&7߱u>~v^K-tfag5]qL'luvИf|O8Lt%6K _x=u>' ,y8?(vu١W/bE16p/~R\O㊌y9ĭ98W֝<55`w%ʧĤ]ljoKV)cmsa*Vf#%Jɕ|ů߰3ًװ Ð/KN^Pbr^k+knk9q8U֗jcSbq<S:a,F%.uku~%95;g1W/βJ\&dclo3|IܬO'S/ۚ~V{18y"a1z3خUb`1X ,ypABw8|ޑ0+}:6jgXf<4st:ݏgko]{afo۪xs|?j7m{__=3Y ÝlL{a?G2ɞ=މح<})JLЫ ٜ%$)Kss-Y_wk\/1O",lK<,0Ll _Ȼ`*føǰ)0kF9gx׹u6^pC7 ⶘0(|؀^rɪn~a8gnspOLkh۞~׻޵Z'K<9IrkLfXqRzͩ;?0pGDxo1-G[&o^\{hWW&7%d`7L0q)[?1eKw_oqk %yv8)dk^qJ^[l: Krå_TƶKJW:x"xϱlv~!b?'RoW?S@Kh1X ,b`1X ,8tJvpou!gJ56m%ܜާgy 1؟S'qS2CҤAc#ì/!>%ܽTNOq:|fb{ 3^GNbk]%J 1>K"s-ǭ$=Jő䃭+2tB :O=^̒%U'ogW7ق>?lk%-Kӝ|fBc8燮.`\_ci3Zg]}W^j2p+'qԅ]z۝lh^%_m3xӏnplIh` '[g{)0Etllϧ~Xٙd:SW{.ɷ7^lF\َ\x&)Cg ?ceXXcu-޽+֭~5s/1LlNxV8 ?y&[+|&fb`1X ,b`1X ,Gp87j;Oݡ~~r^{95pX0}9:\XjNrΤ݉#fcq0y3?섫~I)tao|Cd'l5HX:ok}nMJͽHD#{|mfIX Grً IDAT|Glwj b1fWǹٌ|Goﱓ?₫6ڛ#H7QӶs]㟼=2y4gwnR~ٯ IU~[ q^X\-6j\n ~pxv; 8'$qO8;>zo7\WKCՏ$`=Sf뢋.:5ߞcW,eg▮.~ly ?31:1k.&|G)n {UۗkpA7.-`7k%PLqEĝ` őMd>dճ[5YW6~FZs}|5O{-~ٷ6Rlߘv|׌ōhX+.t+L3|N11OSN'{sȤg~b`] Ysb`1X ,b`1X E:v8:s~dw97~*Nuش<b֞ylN P5v>ON\1aDc,M;d*ę؉Z1'GSƾ?9eq,a$_$ f7]$ݰ)RrADRIBI >5.֤>J/LsfLkSglrYF,^J s8KfK0Á7z0.U{T]]kdŇ6dKOkd>sػc=֙,LI yjk?oɸ`=zX-v\t`R68`/d{$_b i_7. ,A^[_x3>m\F6;* r8fSzsɷnNfNNa8_9e<q ߍE|g/oNJhL;qdeگnXZiXq˖v6Ĩ@mB,  b`1X ,b`1X \.A E;ԛuutAh2pV:vİL[8'L[ӟSngt@Kfö_뜛r[bߴܔetPI=醙4!ӡlM\>Nuk=wk~bdx'7IԙjϦuLy.Xo}ld [AFHlO\a 3y2uk+ao&ⴵ#]mj^D:lœW2d8鑉bW611跧٤CW %[R)85^w(__,%ͧ~lvm>ۃ\hӇ+K6ރTu)Ůop)3Lj&&wWFҾ/q7_ڙ|Kw}ž,i/O\ꇟh]jk)ŗ |a'_ k?fn:Г"~;|Mn{?;օ4m7cӏgO0K3٦tRүJ_;`ſX ,b`1X ,+@pu0=fAbnSܱJS~ r>\l]kx;\tz&k8,X&o0x|WO|0179Krd{b^kDk/tX$+l/%Ux~ҙxjO?aD;Ifd!923޸FLd/nu70?9fg|I>Jf&LNx G). 1aaG0u-:ƻ&rQ5,X%jHMFVJj5#O6#YV;eɼ6χ㷱*ߊ~q;u1d%iƿv>቗r'z7^'ٹokh-C&}[3pW7!Q sܶv_,q@ZXkOWr4qh+l)qm𤴆I^M6;4}M9/.>X|ž|_|S>T/n:FqdC^r3=lKNlʴ?)S ؛JB;'Db`1pUd77{ox7/p_WE b`1X ,b`1p c`HNtX{v OoCxVoubpq*4qM{Cfs9mX ˌ~10đ^:Xmx;N7N' ÙdXDZ53s6-0{'^lv']k?%6|O=?Ӟ9}8.q'}\k&d͜1ln<)d[-M>_+l(q:EtgLœ\=Oͱ"S'8k96S11ܞ|-O쫽Xw_ȫck_{]rly;ƀ7>>--oo>f7ƾ;1؋_?SoM~>ᕯ|^oo=ַtR2o~߽v'=IۇYp{?.^CeZp=D__/G,b`1X 9ho9uWzng?u;8M~#?Q]㳭i>Yd8jrŖiwm9G>4MnK'Ź?o<p77{;b9?gesb;ckuO KĘnX?n4ϙ(ITwgJ)NDVRi Q"q%AΗv6<%K1.lίsx/蹃QLb l<;soأv4?ai7a)W56o=8%K,K!%viS;JKgGk<;+mN׿>E|E6\ۗaCx铅+lۏsM`دs|[/ۭo6{b3L1cnZk%U鵏W&^l9'>'la$_4uŅgukWlSc?m'!p=}l͊{hwƟ n%9u/r{lSW~Ͼ_u~ԓgsq58峻_e1p>XyxV3+|Vza asytc__ywӝto|Moz-}ٗ}a{9$kП:<8|~~6?w{ ZWb'>[ GI`F?3>cЇ>t[mpvV.|(>s>gKK9%X ,b`10?찮A`cc1?<@qtSa.dĐ]ca7ցt6ӏ7?1tU܌Ld]c%z׸ɮS$kni+[OۍkA1=1ï.^:{v#>≝ 8)8ȓ<=796%&WJ>5vaؚ2.nqꌣdk>f(*!ꬅRvT^|Z71gG\,JvǛt⦵ Fwz@\3nvݩ` b.gNp5@ >A6%?wweݧM=|k/rdwȢb֝~Eh ZiúnMɷNH>փL~ycqfgOS;uqHngoX̆k&Ș|dX/S-_+~2Ô3™>ƺڭmՓ77k-Ӝ6/'-狁[VOO8_&30]]>KI/<ٟz=R3|𒗼u_u[R{{WWO_U_ux%_d|@//>^: ȞUhg>[~b`1X , ~ֱ>g_:`?3?s{Oza>oKdK848ø𚟇F0洓8ٚMfL\mh,5y͞tM;(lO|Ml3=:wM~O}ML(\zOO{nvLsuCx×/v;#}?/ajM'/~ {N2ūtO s |m2qg| ۙ$t{݇mbI#nD [|g\_f36sɳokY%LJvtdO} 䍷lkGRF;9> y֋< ;]e[m~qCt{tԭ_ki: kv踔g<-8{#%:'*N=%k|rk/ Xh%Oз'l}wr~}^vOiy%U'/]uEln137V39g8I>ᛘáIĘLxjr0T_ G{]2s~љgssb`1p)V*!Ï}-HY~p;8=oslH Ox?ӷ{߾}@_|>S>e#oo&1o~'~>ӶG(KmP?Wc?=>&7/2}Q{랶ʦ=ؒ__&Yэnv4NE/2flȞl}}e'=2/S5?ݒʹ6^l?Z-r&ˈ;֭x#qx޶zzqK(yzcOO=}c//˟ɟ|x+^q?cӻxJ//o=}oO 8]g޾=9ϹB!;vw꾣C6M>Scd{E|k|ب?#9>=:7nv|/ÚK|L\׹.f~mfAlA& O>|KL`_O*tOxz;JY w %=%k]2ž6+x C~ʆ,!b<5c`g̎dS!9zj x F?vkYrIÜkIȘ7ο '1=%ڊwp叏c[JITcsO;ŋ ;dM}ފ|w7 9z$u{a&s/-.6;2VR\ųzR6fMB-Jnƴooos#~_qc ͵]JXd{{rM2u̾n]KWߏ>ה?+9zoΉA^&|q&Sw[E¥o.xx$G3N%ʊub$,m =sJ)L&*qƎyW*2ɧkLcO#/5.㾓.ɸ$U,x\ɟO~0S5+gyjfշ%o]?Ac1lHB8 Dar9~حc_q~2RkL66(^5alNNZOc'.ɲKF7Ϥ3v,}_Lqf1}A}vK˼1'osgl͇c#a' ua'_٭G&_)t]kq7<wuX18^:Ŗ#}?(.2]ZiX|19~eƟch_>빽o(Plo~>h_]^Õ~'~WW7W>*n_e2p \.O[>w8?%Tr)5O yԷ6KgSZw6?K!υַ߫FuGEގv^ 拎L8)۸]lҭ^d{umNn/x!6s[kw}7K8ظ[W }W^ ?;gǜ~OLoAvE.+.=3b5!*ܤSz8SyrҞ_̷pV<>/bRE ؉~.S1uZnO)r8gM֚<n.)_bU$b`$h3&`J=~_|kI[nbObWћo8Y{x/\r$k/|#,qC֌ūJn'] }~]^9O3nlMcLJ's_塝8UCGqvrmacCEg+?Ei{9tR>Gop|ŧ<ù}ؐtXAS{u{ox-Bͭ9qVe֖7<+GsXڃbm^agq"|mg.źe}OSkG T??O}ѫ~ Om󴢯'{Fwh}Sw\ } y}?z 8_'J:|xW{JeKIm~ռ8?Cמ~{]^~} 1\zc[b}Ib" >=u?Ïs꩘^|~*{xƹe2p \.wɟ!7OwϽS<~|+gz[|/>.{[Ovm{)ٹ~4wgֺH^1mpo/e^qKnO2/ |)}<ڟl9hO/*X@r`~n[ s8'XQo=*҅,x*.l( O jlN}hExGSq)&Z: {=KFEˉ3ל}g|<Ġ^ݜM5:+o㱁9MsĆ8xV~\!;uGd~Ya`/wX/s_|i\Ey%]?'+Q'B+N[g^Z\gF|ӗڷOph8 fΜ/d\aYe4k9uy9v8.g/f/r,_O|Uzo} 1~4} «}?  g~//~O0~Srl@ѵ\֮/}SQ>忍.|尯sϽ]逾[iI3`=~π_X+me2p \~5<3=~^o|Zyط˜m. @k9EL'?tdlL%^,vi])y/4){rvq]>*pC_86LLN)xꓬCtN>as*FĶbGsOrN7U+^.w n/6|V|m}l ^0~_^ams>l_߹Ys}:?d2vwc5[㶢{3pOs>x7;~~'+|_sNkc-W}9S08*$3?{Js2s+;xX"9s 'ODyy3f{vW1|iLӠ2Y~zo4^Z'߿;^g.-Y,&k|wvgcqNkqO/|>ٟ8|Zz]u^˻U?Ӯo~B3ن'ذ3 d[b>\?Ζ𩝺8 KőL^ܝa9vI_ gwمh4^+nTXw5Y9aq-X9vŀqqrM)b.]/5v6;p%__j5fX϶8Er@d1yjqddɳ_2~0g+x 3s#䏼>^_ӽ~a zW{U?~wegIniԟzoJq/>u?rJWRgy}~Y[Cp[o}̷>k?/?F߇Gom?+ w@LTN^eyE3?ygTE邰 LkWWN/.*{E~s9c׌Aeoz^s|}M9O-c\tke2v'5v~_,pop=.ؓ/ ɇ^c}6v^GŠuc<^[/;L^ja>ť"6Z{ y1v/ŏ;m7p ECܽ7xXM0Sz1eq(Q{gc+KroIWt*ʱլStT`o>k,V93hſޘƲ_{ؕ3\{S{/ȣ}[m=.z-^ #|[ctv 7d8L?>*Xmpgob>u\u>{]>,RG\wnTLx媕;"cmַqtzg\+ϸዿs[& ?% F7wD,nxN ga:bwp !v"{]įž"?G&oC?g87[Qt\}wW{ )6k_T`~W|.<''ki~a_!M۾OKoo}4]Fԃw}`>Lz|_tfU{g؞OgyywC ~ տWx?2gbO? /{/> >I.e2p \>p1ƾjooy;3~O>l}>$ù-Ϭw[wVϼ.GvڗoByfScZ,~۫}X?k~k?[5./tE׫]Kʃocس ss㰝x1ou6n_> ??IRK ~ݿwN].fĿkw~\77?#?f].e2pz.wʞcjB+{POPw\Z;;ߨg}T@!^E;<.WjmLJxD>e'L//z|Υ^{^ua(bo/ۇ#.,=q-őltޖ^Ȓg(uٜ{ͷϻ{.W9suϧ_C~֋tlb [6 !/FjLgqW>$19_dg1_|W)Vlلc/Τ^vŜp7l0)+TiܹCي nt+J'}>̾Ƶr 0k{%o4hs;yƈ0Wr5n/i,^{yW{Ǐ|~)k|Kɛ˩sOgg3ũƧq~y\c[pTC_N˟z╿aΟME/Fv_־>_qrwcԒ6rG9eZ3'_yU_NVoۧ/V6tdoC1;/60?rh=8'x뮓UާX;~Ÿ0\}:ַ{Uګ~ϿO2e(??@_O}55 tqPsy \.e2p d૞m]fUH~UZE[ ,+ۋ t肓Nk]8&#˿|yc=iqq}]}y={wa 3.z zŻtBznqq7ra=\־U'ϝΙϵ֥{0l!]E;c "QOufر8mn{`5[~S֯q~ۓrHﹽ>* ǎx 'W7-C3k+=h=* #}=mbTWaW|A{S #MDsd5]+RWB]煿rY߰Ϗ\dK4 ˶ YJ? bg[{Gu6 'Z{uʋXa~ X K{k,5fG|/Y˿W6ML6{護zضN?\i2_+]Ȏ,1^!ndb#w52q=0_vٲ[A;_YxCX9y3se2o%2pfWk{b/D~I~>>ye2p \.è .ߌ.o/軌^]^0. {AT>~w8_]ޞg/__s6oqo/?9̰xyw^͗xllOł|Wwz͇\os8ZϾ +|T$˸Xo V>ۗݵpo3.+ɽd}ڽg{e\Q&ZWRb/+hm/qpe pj 0gEYy(]^W04nگg y! q-w OǓ7|bVK3GS{68{yo}㬷/vTL:xK5qIYEr0 ;-6t4&<6lHGCz_.=k}my䫘 ̇}L1S|Kqc|>u:Qߘl57O'_+/ޫ_-?ƽ \.wŀ_雾?'䅯x&me2p \._IˑW]u߫tRo/$d]tɸkL:{m: .(m˩Ks\<źCX=gs꯭=;=^Vr4kuw xj\0.{=?ޞtz1a-ɻT IDAT 8 ,v6Z~w[?mngqr-s}SzBabmp\.[[8y 7=k/Kzx+>{NߞØ.)-ğe/|0trF|LIb}ܷ'يna'N~ocvō5 7^ƝŌ̿<:{)v?šiŧ5V&9.뜕<5 ]bO' B%YgMXl\|koSӋqiuaӬj/z9^^aYom /e2p \.e2 \c>^u؅󳗌].ÝNqqs[' |~N\B>o-L{xrrZx.ϼo_ۅ²v⇍nx*,MU-&=sr_o/]#|Ŵʝ 0{ƞkNSl:+>mZm¶{JKwaE0~_+srZ8Ϝ?:ēukC ٵŌ|.+f1.O}g|ӜN/8>!~An8;)s/6qUl1Z0Э[l}_lw| <;vI \.e2p \.e2& tƸ.b/d.1u.wqj-ۇ#|kg~aS]Rgϸ ٓk'gd.V+ݧ]ԋw9:{aewipGg=)ֲ/-&t3n\3_^qSkq%Qn;nb O_GO  ^/6$g{ {U ӧ[9{ߝs: g2SAYAǛ^! .hp3o/^~#yя~';)2Nj?k?'IJnn=x*'>P~^t׼;#gg^0jщw }s Gy쾉Ξ>[a'xS+Oc_Ǭomחh|/t3^3N={C EdbIzݼ>JǼ;Wዣ_kvO(Fy|c[0gqȍ,g:Ťg'3xJMkʧXuDt.OG>A|[~-=w2p \.e2p \..%\v]<D|Յ]>5?K. 7b킳<6?V7G.Cf/Ek#VN]Z[br^:b=.n\gJӍ#/v}.bgޘ2~;o d*hVkan6F-ZIY>IٟD))W0ل,{)f㹼1sFi ? U|*[oЇ>(|:gk7_ƽ³ wK6szq/y<-qܕgUA^gBiXibxu1hi\CaG,op`]_|]tsktᚎgX?-t[kq78Z8y)^N?Z Y|m@O- ܚž`t5_x=AgpW<1&s1Xѵ=Wˆ dWeˆo9-ɥ/~1}5g XWۏ_F7n/˅#|6^\%ޒw Wtt*^}A/6 ʡ=ѷ޹.Gt՟g%tqx+ˡ<X7Bm[M/tWNks/ۿН\> xoꗁe2p \.e2pT]y]E^K. sWmXty{٥QI?13~q|-Eѯnrb{O1rOxm" g\۽(VqPcqnsً-N-u#'}5]b'\dqz{ڷ7:ǰ^mW8/V}_ |X&N>Y׊pGOVD+W>O EU:lbWtX;f*Rҏ**Bu\lodG`÷3W<~:Aq[Ͽ^\͸8:7qGlogvpx+Ocˇsx蜵ys ꇓ}itYr2krܔW}{瓞8G9__|m=&=]6ŌtuX|!_1ZgI=[k3Nzb'}>0Keܶ1xjtgO&~{W銱 [~ֲ[ty%/dqt{kceZ{-;_+Wl KW2p \.e2p \.O`)7?/ L~^:'~KqqtqWIX-]mާ ۉMU#(^6 |%vy'꯬\6&0rHf|W.tYZyY3\{iiyhg}g=/ sg<˳V.&51aÆ/>Yߺ뛬~m|uҧ"y Ok'WdWdY묭^xyןqdS>l}W^&{[2p \.e2p \.7b%^26ߋ^Bes6{1gz^;/ K|7_lkyژ^뻱|[O?wkuv=ll[kʷsԘ0ߝl*LIklN%ߞ 6b4<y=2VW|ѓKaT1v}8*"x[({X+dn`n}x{/+_/yU~|t7aG~[/BmDZx8,_Oc6;oLwrYS!=-}2y~x <ʛ~ S|=Ǽ6#m٧ -x \.e2p \.ɀ˷TRuj܅_}]^g& K5bύO-濼]=={<_.LO]ކ1tmc5^b9};J&>igpsy,dζc-8k|)[E='n'Wڵt`[ִhs.p=n>.+^V>)?qӃqLKF[3ߧㄯ́9^|\1NO'WؑY_XlO⚷~EIξ9֞ozѭJΈU1.\!viu4c3']rs\:q sOSXj+\tn -')F:Sl3;NAq$LOsz{Olb-a]Z/|Xoξu㸰>omq4n9[Ӯ yMZyc6[ O,_&/x2px0p \.e2p \.e2 ty%^u!Wo0 %i6lw |;ݍ%>Z3-]S|bNH H}}Ά A:*b}˾\6_l0kva2="WxY--<|jS * ǹoW.qU5\_ 0Zz`rZ'[ϰÐ CEzr+}"'j~Ix)wY"/ғW+?X1?>C /Fb}y7bʋF56t-!qOqָ_y\ڿ,yyiܓ9+~^Z?\_^^x¼3jȵr6nHy9^W9y.n:K_/b#.i>mM7wz8Ɲ|99S˃Xgs*okͮG{Ge{.e2p \.e2px#bKw)ץ^H {ܤ+v{!~nqN|3~S\ݸѯt/~ݾN.@|/.&]2'μB8.rXۍk ͯq'akyg8x= +c"3↵8,3B7ܿ_O{ rK'+\3>+rg|p^OkTYެƝaSq~(;~a?蒕{˿WGaav^R0vG9tpc^`{ʞ|b;Jʽ"ˇ^燍5  B5M1_m(֛$,fAX򩇫BD1ؒwsz?Y_>VAZ8篵Z Ŷ|_iȶ"/^GϙR$K/\/tU>z#*"|d^\WۛrO>]oL$I߸b[LxK W8f[a0Y29H>c-ŋ5zW譬/V֜ 95W7Ksu6T.ָkM>оwsc0y+/6qũ.u~ۇ+W^~#>6G1?m}s1boxٍxY_٬Na[.Lm/3p 4\.e2p \.e2F xK\]Нb|]&˾W.TW/.f[K^0wKOL?gRs/; ż{mC/<dړ'Γ= ł8qAqre1y>Wx&d;2 =[y)>)hTܽ1^lZxY>Za{ۚyӿ ql a[<ٮ|cBPvzX|V{.O_mӄ*USz{ņ䯧TKϋ-iK*T$p-Ƹna5o9S!5N鷏KWX^5vdVN/?k/9|qߞ1>G=Y5^NY];­{7YXK~ GXk+Ay(~>$nc;oL+fZ۳ u~p]nϗ:n?) ^Yq=8z[TX6e2p \.e2p3{kp]̝]Хc.Q.N^2.< _g6y%#V񒭿?/m鷞z6֋%i3nuqztÙNkxqa[޲q{\ 2ߞbv-g6n8xw9xAW,/v`Z*fTth{*=a\Q&[\~ aѳGz8־-#B6镛W2*ls8>܊UN~P$p~WR|2Kv[`(nK:5ec s_ӛO>5s>o֋//x{*=)ݿXgCWO'?d+Gntsfş4xLgz^I.GX3$[^ܙN|jqax./},]šlqgzgz1i~~{,0o <. e2p \.e2p \ބ.ٺ|._\,?w/;+;q,3[]aZKdijs!OnXɍƼ;y[O}$>_,qFM|}v7;c~,(ַP9?.VPB%o9.a{#7k/YcX8q\$d Og~^!62?qºy/zT|Yƞ\7|o1*47^1lgL|żVDtsg[a`*׋q+iƯZ&ή"&f<#}q*z6X++c#w:Xm<|f}yv*Zrrgҏ,.c}CdS :}ćhCׇl-ï?zX.9Ǹl~OˇtĤ-}#_ٳq>Ȍ|5~N/8i4/7~qU4o _3Swt[Ɋ} {/; \ne2p \.e2p \ސLu M.G^'GW-F;89ac-ŭh[ ~a/C\熭u{B9p1%xև7b҇3V:GK^~ܓ&|h̽HSK>G_ 2$<EX3o3_@"3}x2L9=yx<~[3.M||Ôm{b.۫l[eW|k{֗94VJ2=;Gl;+.OG>A|k鹋e2p \.e2p <@!vѺ<}v, ~hZOΎ 싯8 X.v/h򛮘{]xo|u\u.Z w~)sq G{Bct㘿"MpF|/VϞ~.}ͯM93|K'1=YgqynHF1cXu~O”~vx.'H'ݍ񐏸s^502Ag?' \.e2p \.O\K."w]tUy9~]Ook7nzwe;x.fz]^+x/[G5EN >Zrr\lj/oUT>r[+۽.+Y ;]b/$|g:%N܇¾~L.nT<*œ>Bo푸h-= 6q$'WjO϶xv˯O9wa_d腷& s?sdd~pzzYdW%V!ؚs'ϯ'3 L6'/~k;7.[s|uxדc= /~>׷֙|\ߙs_MqӍbyr2{6ia gkU*[āΎ.`/#u:8/مz0/脑kΞ,?=?sH7|lj/@y&}C7?bg,~glˉy}:A?7e2p \.e2p | t)t/\,vk ƽ,sԱ#b׋>vإa}_6Fzg.(.VW_Uxح^'Y sYo0Z qEtXL+t^Άo↷y9aIi`dPz,x9] $q{G'Gb{bQ =u r*nB^~Y}3.t炯WO9i V8*:;# W旭57`a[Sp哞=c6}E7歷zy|]^5n^gߚbqhOS/fX<`z;:x[B8pt6ؔ;V6"{9~{oWW;zv.İί Q,~k/M u5pC<6o̍SCv}|#6bAÁƇfcŵVlsky9dG51Gv8dޙNOoqc-Ƹ ۸僞12>1{ѿO__K] \.e2p \.ed]P.Nv/SBs/NʶKz{mB2sy]̏$n٥$fץjfҚ^6oq`)r\tػ/F>ʉ=w.rm嚌-x-w:YLxU3*ּo^>>F^:݃0Uh7t6_vN஠UL>/~9?CMǯWWRh?{ȸK!2eXǎNΤb^җvyjEϼ}yzjb&<5S;+2{?;+z:9z6&EE/qQ!1h㹽ғȗWxOΗzоSW{b.ԓUsaߛlΖܞy{ٵOƝM}FL/vtˣB\ꮌ9V=rm}6⽾Q68;^)LLq~3>W& | _.e2p \.e 6^].d!^va-u ]Jv1z^nƋ\낿\ê/^c.a2wc,'|'N}8|jk'h.^ӋY>NS,_ť/8ÓG^ Ls{foqŢ9`ka(o(7Z l⒟ dv&(HUL{& cT<ίSt*'Wr\,}7n?}Xco_'rr o#?0E)~Yhhq CzrH/b5e2{ѿWl KW2p \.e2p \. ee½ $d/7L̦F. Ύ[_Ƨo.;8L_lN>N؟\X#rv}+̋qS}8_~X}8O1wlB:dZ8vnɳ;(nM87-S+rN|WP,H'Olp?{6tJw=^mcc=ͩ8dhbL/pVP{&~|[O\)o}[* a 8|!k^S SO+:'|*^Tj9kPH,_rㆎd)~Aο r׳ϗ>|m_iE8pr[E>[lوͷwᒫ`W_lM[ko[HWŀE'N@ǎ>Ԓ~na.nX`WVF}kaޫ`,n}x&Lt_qc \g}m<{эװC!Ϗ1{7qW,t⎟G馟OZ=)a)_r2-텞,h}Kq6e2~$\.e2p \.e2pxdx^vX.$7ڵi^>\Ngܓ.xWA"{J<}{|1F2u񤟬K_=vһe>7'1O8×8 >{k=>]~qXbY!]WY+yЇx_1×9~;Y+k^cM*qR!nq ~m7&WRTI+&*Ri?Yӗkq529ty)7?LVӋ7~̭s;}DY5F ʎ/+8Z[;u9Sؼq}`ce}M ť.}nȥ}ͶSS<^/f =7>p{l=bD3{;ag[w=;1:wq6a&<py}}Y<{Ɋ05R|rח{D8m CGoѨӋݟ[]>/^/V o9wSМnm.\ѫ=X힃Acf{,#/ŏ>ƥ_oMWYk}(Vdp[oXV^Z:_"}8gOz*ww⯼˝ =ʃsa<:a0'whnyg N'~`ʻ 6A1y{Ns*.m}@ ~gjd|9?nvߋǞ8{ȴZ<)=s?s_3>1{w?+i [{ќ|A/X< y=}Y;\G#v׉/bƺ{e`ߋׯ}W2p \.e2p \. t)t|/TLPw z]le t^vf[sX;1/]K.FN>[K>̩xސz3wp+rhO^‘]~/[ˋ}><_SUٸM6go9ԳW@(Ll&X{EAxbQdt7>(<>ٸtO6tLG|i>+m *"b݇اFwV+*7߼??/>%Wt3|cMr^t ㊺|㢂4{1м3V?˗qc+I.=vqOmmi`:[HXń->ZkiqhE/?v*ֹ*?]GZo㷇;Ovqc狳5>gי+.N?0fWn9dH,Ze2o%2p \.e2p \.0e^Rv/M&]^]<>7y:.,G]DF}nn{yssCy^ŞO2v6/yҙ匹z ?=7S/lTt9؟1|X+exm>z{a]=kaƎ,z~`OgI^Ykܣ1~٢'g@T*A(@eVُٟ5x/ſx|<꫟}=Ȧ#}FpU4|Om+szW·ڲ3^FYcSRb /n@WSb仧u_v=W+3K<.00tr1̻L7_]s7Nw}1(-@B2lY}>vle?qZ̧ZśO}ˑb]b r\)&gK8ڽ*4X_O! SA< ¿.w_#ǃۏt:zzR|X3f&K/=Xf0D\q,˭bW>+F1d|(Tzr:~OO?/lbǫ*B)ocS80rJK{>|ty% kyGāM3ސ?g{ otU˫nxeW,wX;Y.r_:b.lˍ܇4m0 IDAT}h7Oi焿0!>,ao/}z26^θs^+G=yAhl5kh?/,| kk&<qBNv ||/['쏘N?.Ub>K1_Gg&Om}}.|/>e?1_}~qfƻ \.e2p \.g8{awey{.\RvX{98O^Jnak216ų3\'.skdt!d/U>ِ1x %^EI|}CzOԋx?/z2Xa}؇EŠr.~MNٛ}9gf_y8f^gsHòu|+8Z+1y)LW!__i|{)-‍q/©EC؉Kq#W5: kql3{ۛ߯lòx:*^c^Ɋ_<|h7n}+|Ӿ/2E`~z8/ca-^Kfi[>Ӈ!|,w˙uZvɴ^~|mqY9-t ՙ;uU6ᨧOXcxeߊKe2p \.e2p \.7a[LBn}uu^ݎ;/ %c}|/h}/ygۭ*"/ ""/-$hh%Ʀ1M naMLZnے*Z*`((K^-;yó".Xekrs1qc̱/WZOp\6^SspU=ox֬?6ł'>ͭ|e ]=Gy.xTKtSz6a|w.SB\xG+*nfwӚ<=Jwŧq.;˹|rQY^s*2|}^W]|_\zQTV-;>`8SY8֤pkG8x[?u_uW'pӼgoX6{sLr}4G>z8;Rߞ?=1Wrycl; zl~`q||UcW{kI9moӅ~l>arŰ^kk-]fcn8w3~⧓?XZէwq&xt(*й)?Q(p8 GQ(p8 ^b/D܊sl41[]7o9=>6O;6W'M9k{o,sZxn͉=;yY+FV k)Iû3QѦ}&q-4cA+JPz[r>ŷOi5Xr[>\6Ws+Κ30_z'1WAS 7ֵ=;?-℧y~]ٛlWϊ1}+`ŋ E{{W~vE[냛~&lAzuo{o|ԧ\/FgM3<ɥ5p)mqiwZa_^->X6\-n'l'U^ KGQ(p8 GQ(<m/lE@lm/?نblu]^]V|vkB|ey_Nl]Z[=elYxyϡX}7^9wwiW[(b9~{8r,7-.}̷3Xbx.n{6$~ZxTh.˧<Ҿ|7kgYB[8iw\È||vwz~g.]?я>/UXnq}-?I/Ώ jW,z4Wᴂ*r)n)$CҼ|X]⬏W9VoS+b ?ݛA{=0pri&54ݿlѲ7^?q.:7 Fk;Sxl\8u`6Ŏ|·t7=“ Nޛ{Okzz;,~֦ؾAQ]7z'6Gl|{>_oo_E>Wm? GcoNtټ-8EKv;S0QLe_&A6VܤƧg|{Nj7p)Χ2f>^X8_>so59e6Gl1=K{;X_ޘNxaqK|ݿ;`9ocR^r/'~?O|3>bo^k|^\-ɮ0=A_ھF6 | ]8qk?׾qqYasu _MWS(p8 GQ(p8 _E^ԭs=%saZ-w0>υyӢ5Ϳ\~=}1B7抑xq%~/`Tͥx.pkqo>no\ w9]I=G<<5qۼG1P@U֝{u3pꗛxiB){saYݿwsOzxqq+O6j956x}+^qoo=/ßɟ\6驘 U7'~8}9~Wx2ϮbZaZ^(jś4L*UdW<ؤ?El{e)\lq"/^∅yr68 >ޞƑ`Y1h{]gBu笂ކ6g3NC4//6bZl1<xٖ{n͸Qndmܮks(~?7;l}ɚz){Ͼjo.'ݏ/ZZu.h K9Չm훹. 'نX铿3/NsiG)p8 GQ(p8 GRKwZy@\]y=. x񚃓f=ǻKν˷+jm|=n7%rֺܲ5Wly׿8{{ w\,pZ}ho-ɡ t,83g<`Cϛ,ϊvU>ū+᪱_܊V+_vˇ➢z}{?|s{{]o5K!^odL/^ʊ3$OǸz5 A59dWΥg Z ڸVTg5K<O'Ν)'pE&Z1z׳=so+y۩콑˾jǻbosZk/ʣ^h=Nΐyec\Nv~i6+Yχvgg3s/}?rzăolac3k-k>֍os`!V0[V.͙w\OkYKp<yߵz w}ڷp4qZw{aC;|vvd]+&b57۸ۜ;Q+UJMOoұ9 GQ(p8 GQY鲭duɷt-^{(sxaim KaXN]h?x|y{.<+lZc"6Wn/oh~. XcZswޜᬖ.+X( )x+fy҅]XSg~c)_w?k."V 6& )lZ>>k[+*YWX3-_EO}S8_v t9^=xO|(hiNqL9⭧!{xx՜އ^NIКlk(+nގ<,Yy+t\8C{c8k*WtEYXVX o)wfm{sZѳI81gg9ޔuZ42ֳO|*~Xi?Wq2|/Ω4O/zǹ:'Vl͕:.٘';G}3<}0㕞͋њcs哶-*rn_0:[~ɿ*?>_q[/~\x|+ {Z+%ֿقa>8OgiVygK|7s-R8xs:Ozɡ C׼' :O 7UYk4Gᵢ0ߊ֋+>q^yOZ9azYQNoIWG;׸oq`[Klk|aYYX}/'cMGcߏrj?vFDZ/p:=t/]h󄵗5Eb7Er6[KX`|:?ٮ99+5{\&o~+l`׬K8m{!vʿgkxoµR|ኗ>l=SU?׾A{*_!¨5s;ϫ)8ɥ|ٙ؇u>ecx͋4}CoZS䇃,4ޏxހ͎o{Y6n?ty%yq_S8'|l|``;W믳M煍>0-ő[{nse|~E6K~Z<~a3 v07lۋ+VO+/,~3{__yW_=><ÿ[O?yx{{;/Ds^^*RQ(p8 GQ(p8 ^ K˼.8?}.3?^&8en.ߵBq^ͅ\yg4~/Wnl9[_ӥ/!>My/^aֻhf(U"E#aWNcnr`C{rZ a(d2ڦQѼ"fO*rK9Ǎ}? T ަTP݇}coy/*hQ^ \sW^R//^]Mg5'Gc*ųb_lؚWԬ+~8<]_-6b؏ү<żM:R7aaU{7+XWWe/{{Wo++bvӱB:.*BNz]<;Ƶanyֹ 'lrz?;̷٤=|{o@1 OߙU |=!~ak|(ȭ M|gLf9[p/{v IDAT բ۳jQ'X,fk?~lyXkS@O[?O_ z׻??C_?|}÷|˷Elc5]nυwt^f{6.q1O*? F9>>l4msq >nNũ6ͧb^c_{WcSEr<X(ϊIl7}7]`*xuV3S4*r(Lqؔ<soӊ9 .,xeOb%Lq*OqiYlh7\z+z׿ߛrH_oˇAokpZZYi͇qg<;Gr-wav:ii9ņO3gyud\Jok׼fZ+3ܰe.'m>YO;ag/tƷ_(r;l6>+~χ}dSͅ]~q#{ʧZoa7{oz;7ɏMZgklSaew] _S?׼5ϚMoz>$|5:ெ'Q(p8 GQ(p8 )EBKRnSߋ.'./b^¶ƾO|7.Y.,7||_V1 s>\6-n+l5hNն9}vY"FW۵cs'߰ړ.Sſ]ﲛqqOT}wڇw7֋gy/c+ҥM [GʮXWŵ֛Їd_UxME0g>̞]N嚖Ű rXW y t`3g!PƸ`g^b?{S(g͘y[}*?SQ]X3W*>ZS?+?{M_Ej͊ƸWg?{bcNcG\aw)2lXiޫo0S_3Φ=3˟og<gw=,8<g~;nLa.}Z|d9+-Ą}aG[|ҖΚW[׌̷\^K0ҬOsES?lÁvz}ow1C[xiPo~9ܟx>O\qWf(p8 GQ(p8 G 2>]w׽kJ{ٸ_˼zֵ]A VL/g=uv>V qk"5}W"2\wozR8l*Y|%MÊG&g(Eo*ycȿ-_ u凓|**-T\Ѓ 7t^ }_MSS={<+Ž7GlW#rroO?w^^xbiq1~sϔ˵BaEYWskWڿo{LQ9ç3?-+;_[ibǛhƝWK<9iNV<1Uv~u|i!Cc#zyYoKcw wy8>pņ>gz>Z߸sm\OZ|a{?sFcײ˶u6m8sz]{f06F4뼵Z# tϮCzX}=Ώ]-EqO[/ûo? |;~>3yb=/)5T>1GQ(p8 GQ(wP.Bf=vV~/04 ܘ7^==̖MZֲ]Kvx_f.gvyis51]2rNpܮ-f07|w}EP}wlo}4'b.]=^=[8'ëXUS Ko5r4{~ZѾXб7ᙏs^qXl*&T{EE8:Ho[߮1~L;98ps_--`w*2].!o{v,q F6i8(jhXp:lo؛0-׵ΎOÇ)wpĩjǚO||uW39^ΉΉ73CoK#n&a큳{Ȏ͟^yggV-i4z3ۇO'ϰ__l Ǎ ssY.'Ғo!gGv]IĊs=t(gqoNgs= OăϽ|sW)eQ(p8 GQ(p8 Ro/VuإavK纴P6´xŸlKM}ń.Ƕak{1Q굻JVͮ7j8bćzZ}<Ҭx})Όy1\?c0=+ث-@ua#gOĂ ŧ|ov f*bU/?6 Fbnl*vz瞽%O*>pg(*Nƕ~b{)ʅ<;ik̟ Kk~68H39*.[8|زz=WƸkM0[y)wa+}WW1^qL#F{8-NJ姗N=W w5Ƨy1{k?_Z C.GiO}ř3(m~Gʋ4֭k9/nɽ99ii0p\.[lfci<9>*r=F\ʙMXa̧QӚV~Wv6=XoNfs~g|rL~/);pGQ(p8 GQkPl{axtkn]Ron1כ;x֥7.ŋ[x=!>]yW=aٯMqtݘ\)n]w7X Vhʘ=NY=' ]`gig]'.>#ޮayL캘lSkaWDg~yϯE>Wi +]sKF–BQVgjs.N={}A xb['Wg?{ͧ=~*naM4(N 7#,X{o*p3VUt9i}8荛~뷞}Cu(|OҜbjgO=-|V"8ٙW9 av{ }i:ΗC 5gᄫkxЧq lk|yK7I3E˞?6ךX֖z:aZ[ ;>+6Ag,o5z1f-<ʯ>S'4~ l|oc\)xU^,5^ժ¼\O8 |QS>'(p8 GQ(p8 G^%\{cAu~& ccEnakq؋h~.V߹|x8g8ů=Lc:>GQ̴IW뭹֊cE}_x^̛_$W<("2Mke\+)׬w*^y<,Mqj^k?>nC~rӇVlO귿Wmgⶾ¢Z\H:(ZOaJ!W3󙫈 G߇+pY,zAS?v,+öOr{'\lt+n`cbMp3>l\._1>kosp'fizyw'+V,chvV{6[ѯU~ڗ5~Nu>ƞ/Ns+rh-\i/|Ѱެ^Iq_{R_ ỌQ(p8 GQ(p8 |+e[i/H2 ^vi9llN]2[{mv/K/llȬ_NauZEs/}.s͇Yuk' [ #ӷC~{&akj^ŕ U>+-.>9=a/{Kt {si][FZ[BȂBK98L{Sts]~WbCspp56w\=ߊX-a_QMQW/_q^38-p$'K?U`(HK+o4"e|,Miݞ_QU|xe+^Yn]>`k8xW538lų/ҥr3cwv:Dɳsg41:rskM3ofvhDscs}on YQv?qc}6g]t5G+-}kN\o0f/g߳\Mhme.o-^iξI1ⰸr,|a=;ݯ-pj-Ց}(V_›R? GQ(p8 G@xk^&a6{Q^ Zf/77"۾b,1.b^&kcq6cagUrwɺ{miR'|am];~EZgW{6_~t[`gE^ ܋qSs=l_yr(lٞkU܊=߽CzU<`îq9#l+QyWP!v>ͿVϹHZvVqg?W/b)رũ]ST8d맰[{W~ToIPۺ/b]n|`G4Vo"wKW ®8bO~7ٷ+W,woK]jAwEa`h>^|88k|p]Z%n磷=v;z1`{g~ (q;__]7v].ii"a3C&]Xx遣5|`# :C-M0ӌջVu.:뫙񮥫XӓVh>a4WSO8s GQ(p8 GQ(px t p@rc{!x|`~]|ޅ^D6Mq=|ʱ .1X3.lÝog]+rbAbɥܽz)l_Ex]v+enz\'{{%wwvoMթk9Jw'}2q]{Ikz;+ST1)PپW~WzT'­ GE@p -L+2V MN&ű)/{i*^T,f'&?G?zWeb)CW^ĭoxp=S >/{>\cE^}9\\z/Wx ·g~ 4Wkp~+zگ÷}۷]{mO= *ZU;lAoQ+LbQ@KRt3Wۡ<74ŇRêSqS.w7`(DGͳcqO 㳾l+GxV\}7~xn-ߊN[6k弗x3=qɷgY>̇SYذw>c?cW!=y6bYٖ LbmN[׹Z.^a۵ "£1,䁋7Vqٟm~k^s` sʊrGKcOoo]1\0qV(-6=˧qEux lX\6rߛl3Y6{v>[Q??k~^ڿL'o9~=-7cc]`[WLvn;30-o_N0;b\Q}Otnp\=]X[~q=Zv:}ioQQ3 7"8!GQ(p8 GQ(pQ w9%frK{Ƌks_|†흛lg/]o J1]>mo0zkcqYoŭK-w)r%vyܥ >./<.ʋS)nzoF8{Qs<91ۣ.M V>]ΧWl6s)7-W8Ҝ +0p8Ϙ]9|~G~~ ”^E*n+δWÜY-oyx[zL=WO+NjXU)m *#*—;R\5V5VR4 +L1~J/X'FnR>pf\, "L1SgަOhsg 0K<:/:v6Р75a3`qM̱]߼4ܽ,/Ó4nSgs(n6ᆥOkmr7k{hC| F C^|m>v~hڮ}cqXb+c9x_˭>ٷa\{m= _+y~=yOx+p /p? GQ(p8 GTuk>v{)bu)~볗/.N=/|~yK2~ae];RO{R018o|cam\~wM;,vq^/N}?Sz Σr.Iqq r)^`{yla8NYly -b~ImPbl޺}hoB«`k]1=k(+) cT`Af oUpT4G?ze9gwy[UW*/…`\h})v&Ao b1wOSl۹}s:5Zվtfmg?;Wh5>V$6Fpe>(Q@W51Ы0?oDolAKfqPq}×}GʩB^ fl]>M;?5 1]bIZg:.kWd_Ν&ng>K頽\6ᆹ8ir^{}:Gi/ܴ.&ޕ'6;9̸٥<\/)GQ(p8 GQ(pT`/BJs{W]{is`|ř6>]K>z{ycmqӯ_pҠ]7uo/8p;SjaVsvbTCp*QD(.|x|la+ *zV`[IL?LnEFoz{[[/}~FɴWo.^o?7tq?X&HL~rZ 88,EN{XB(>-e;/ѳ|;Cp|:+e~bWܘ{ѹۢP9Sьbgۿ4dN<)ZSZoa8#*d[ch|⁃u:x.ü<][*W> <ۿ4355Xˮ=OkǷ Nf|9sg8,s>Jx+xʼnC{=5*weo^|[s9g痽x6nm}:?agw^j㦳5g1'v7YI9?썹g|x)+p /?GQ(p8 GQT`/"˽.{iXy\D'> bWϯxv\뷹zZq;;^g1xAwm3y#w=k/6aOw0}*$+U bqU8YU Z6~?7rOu_ۜ7;~8>[krmOQdқ/nkhV\}q,?}^i[l8;g};;:beONTވU)7W!O1+"7h)jMo "_}J&rcVlqc}wEkEho:gƭg+>^^϶g5:{_of{AA׽uWz׻ߜ^ލ/n\|9HQؼ¹ΦB}=^AVL[nǥ$JZW }gv7cqg{SjO\V8ó7_j8|z֋ߍxyo[saxm9w"]jqGckX>~s·|v1/W' ܞĵ*ڭ.\Ocl| GRQ(p8 GQ(p8  % .+cquمgbz_C\w.@?ͳ|VK0Ϸ{˥-5ϛX~]6Fͧ9~[{{AnhbiK;C:.]j]6{HkW|65N\WKW6O U妷0U4kOWjeǾʞm{|=<ƜP=WSRU嫘(B}C8|eh˗>~Zbb5e j__3Я{RGѲe_{rH7䡐 xrp|l5\xxwaC>`S:{:E`o.~ZS_:WVXᑶrE ѥ=k3@?, i)o^Jx84}_~aLZOx3'yfI[<>F^/3},ך?Nh#l# jlsp/^C= \J0wv:qlP^.ie8{;Gt\g9n]|[k8,֋} +.}6.VN<̷w8̰ӎ/ u _UOQ(p8 GQ(p8 P`/ۺk˒4iVo/ 1b gwebk|š{.Rg{0s%ŹX*8sǷKl}W0ggKtgW۽q9mN{sbhfEa>yk>7VК/5[y균~-\+)NU\V /iG,,{ҳ}3Tbn=]藛yZ=`}Œ^qFq))hlGB,6VT'EooͰ3}4޹ C%*pV@L:nb5yiEhso*PjGlh>aSAιsPᔟ<ɏ0X-^훳#$m[}/h޷?o7~W?~|3!|+o0M*Pw|3]q*/:Lxi=87욃yW1 -6{s}&v{m3n}_e[,q .ܻ>*v3, tVy#{NoZoFåX#߰I7qml8a[k3n٧[ #Yq`wu>%+ӎGKS>(p8 GQ(p8 G祀K6y˷..o/aX[/d>eou/)2~YNOs|M)ǻ\vlv.Gq]ƥq9r60`6i~saྺ<0hz,y VZ٬n]+>ٰ/vM9y`x{#Ԝzl}w|w\;*kAm[ 42?ყ9yGq}8}`ϏV4,_ΰsFjΏ31]9ɳXGbhlǞFϧ|p^qaW3H~b?N7[~)^r5z9i  ˿}ּ1rk?^mkWkqY^^OZ_XkyngtlʼnO_dxQQk\ sQ(p8 GQ(p8 ^R tu]텥x$Ry+X{^/=m]4⭝qrq%Pv˱ 06~s].8X/$P忶xlgٽ.7>.Nwm_\ʭIl*tɿvi;(1]#mv[d|{i|s]^xc&(fĨhjg6ME<[8(v xRi?eE.50㛦X^axVlE8 0qc(O~ך#n|+t{}QLC+t∝.VFwq߾xr?~U/OŶ(Z×?y] >gboϭñX~YO9P{^w~w^qt[~.>͋gsBqv}{晽59/ bdk]N.ή}n^ mp6|ߣ3qGs}_>ly=1aFïŽ 6X!;9Z|c.ί\Xy8  GQ(p8 GQ()t.pl ʍpnso>﹋.p\^]%.y*co.M.].]ƍs1w9?w,] xk=  bc{?=w ^uy6'bd.웫|6fk v*y=Ůf_ĺ+831peo\~3 pӸ<ڃl;ܹo#.f?QMִ[|Zs>QcΚ9rKgvi'1ϷbL= \xzo5p|y5KWsޞ"e|-ڃ,qp'Nmmcy/]+^)2XO9;|/ΛO:gNGf̎r {ⳅ,mM G߼0W(l`0pc- WrQ`WU(88ؿ[lQzk_{+ +^kMB"? avכl+ VӷGiH\6-n∭O|vi_/]4( h|)j ~jY\pb)*J[y#<65E-U9V;=hx+_MyS%ho[w8䋣W [gd?1q/}ՊlOgXذ53:IͷYㅋ:/)ΰ\\_3õt]|-.6^؛#9}}`k{*lZ]W'>ΧVqv1bK=o8=V˫X役sQ(W 9 GQ(p8 GQ(p8 {6/\iQopXXD縥GڳvNwο="nE&㵚,_74ۧuLz< 5qWFJ> %4QAt-ezvʲߟ#Bm@) T &&DF%7 1-1qa l*ZAJiy)j1%7_ ֺΑ\sq1Ƽڌcnܳ7q'n_?LԈuj3ƉO?j!]8#j>Otq= ګO.}o¨S|zÖk6ln'=.ޯ~8DUb(.A䆍+^D| >]vwv }&<j48'%1,q'>Vor-G^x+ &|#'Qqp!5^x{? >kQݮ[^y/{d1g϶&6捹Vgkֺ傇tiz>Ň{Xغp?jj\*/ZV'0&u 8jf\<_`Wq<0W81׸ll֌5>4/'b/_szmVOq|eny[19V1_nVVYW~^e`Xe`Xe`X|:L,7z<@Μg.&gz!_99crqxxy>O;1}71园M"E7ioC/x;r}\כ|qvS;8ɏ~K1/qG'?ySʽʗ ;1O3/wGp${衇x{VQ㓐.F786x'RCDj|@E |#lpۇ~So+ꃃz)i8Y>z͹͵#O|s\ڏ^م{y6ZGqlĔXʼnQ,ܿmMsԜ<6wp`+{c239g4/j^^xm柵om\g܋e;|n=72 ,2 ,2 ,2 p(7!: <١cy$Cy/\w1q@l3QwzjPig7Ǧ3GWq\'s;?įxq^𛛵\Sqi7\y3wU>ygꀡq;@o㘟sA2eϺOiW)rO+~c'ZToxeLƞ rg?{ފ5%v 6O<āhɮ7{*oo@~6Lq*>,xDb>x ~c`\WrSr˛-Z"7c V5^%|oϸ\ƈUes 7dk,=r}ǫM{;kqZ803kmZkh~=?zp?]VK7j5Fiε;/{fpT3?Z5>f]\ߑ|k3 }yjmug+~^Ϣ<?Ywݳ%FA45ǽx9'7ӦGq?-?{5׳g;}_j \u { 7o2 ,2 ,2 ,@vaE}9vܙ|U9a<cƙ 0-#vxX!1<㮦j0Zf?}3&5300?sT>q·g,V#[U‡ku>ST|6쨸D!|:ܟĘ[BG/NO |՗{uӚO7эs\z0 j)PD$P}}3#J9aG9V/}K{ucG9Ta7'|&w}*,.&ٛX%=:6 c&㖰^5&*w9`2<{9o-7.&$wq /ujYAM@_?-vyo)ZKr'g& S?׬>umb>ă&k\%Z:\7f}c0h{8w-80f]jk辜}֣7;abi|f8UGȇzon0g}"9|KY޸4^]ֈXڣZ[1588n.[{ 7cs]Ӹ{ֲ溏3y枚1f1^μaϜbղkO_gǖoj;1OO72 ,2 ,2 ,2yxD:ý9fþ<$<۹ug<6WE9myv3G|8ϸ' usq>Dm݉eM>On; 7srVs\ZyhO )l\Cva(>oN~/俱cƊ\|LN;1< XuYW>>|&s-a/Htɇ>}#)V|NkE*^Bo.—g DasNH'q&sŗK~q$B014;b֊W8z38G#2S߳6cl+rA$hkZAnfN9\!k1a_πxU?0و;{yNo/|7ll=NjgsDWxzɫDQZ01͉Q[)p̸; a?udQZ~b_{ݚs=Gm~c-/q+Qھ o8"{.1& ~{ 'E^؍ϘVbbnXŜ[ĔyGnلM06x\Ϧ[ck_zf~g[a^859۳f' >Okd\<䦼}ǘ.93?,u.؄p~躜ceӸkx]߼z\l ʦl , ,2 ,2 ,2 ,/,'.w_/=ꆀ~~y,?s?wD;0{{ 807N_q5߁_t8c.vo~ [6Z,3Pv>t?1c|:fMguXͯ8 C:x-ƯZ9ſܜzyCLL3Aq|q1;ňO\ s[Ss۽6cಕ;v?[a=ٌ%+Ny⬜'^c?)/ٶ̳7O}*;*;bNj^C̼;ޮ0,]#Q6Nͱ#O8o^֖pLm]ݬ&oZ jP_3vjf;֜+MozŝwyΧ/Z><_0%l/p~Wb@M8&R8ǣ<>pكִ"|6Yclھj_l`5sƞ?gcZޜS{$a/NdZ{{cy s1b7y06kf+/.Nz+vۛ qp`ݷ:p0P|ɾCzcPCÉ+,ŝՁ`1/9g&#u<)Nͼg}x~7wW<7략4>mlM#YcP o6Bu./5g ?~Wrw\AsuN/sM6',\w?וs[s?;å覆53ߺ0h#5@?c[5vbrQnsoܞx#.q|(V IDAT ?:E=GqU8I2&^ 9Ohwћo9DͩJ򪑘)>!W|x6&L ~p?r $7~M,R_o#930G0YKZju5mÛC`𮉫~?fy{u3&EVKs1V>'f}`o2 _Oºqk-qehֱgL|ssS`W|ug1g}v%`^>W=ȶ{ĚX¾+mX5ag>>/yxgx ުs5qh?saϺ|ir k{xꝘkz-:vsf:ͻ6_rL>g[0Mg2 '+nXe`Xe`Xe`X^`~5Bʯ\OO C1sm斯r~8~Mq UeS8p8bޜK$bdC9]6>|]fc؉QL.fY[9${#L+ bu? M`G0 bRQ}Eᔗ1FO}xUNy"bq8jcj௎~%3kfN5& /X>͜&[r|,<ƍsM\& iGr@pvZO,<o}{FxZS6W?|{N3{|OZ\$uhi6ris®`gq؄yl-QZ~=:\nu3^ᾏ0S_Œ-O[6TKيh>7GO|Ƴ/uxxg}SS05o|6o,S_IJ$,2 ,2 ,2 ,nnoo9Tyo:0{衇.~~8;q~GzjvXS#`:o͝76i? waa3U:0,Aeqyh:?wPYU@7/~*Ovů׼\j~C3uk={ y]^]1]17.3qu`<؄%ny@bBLb?akb NMWbNF ƫ}ZE" a? }l]-L6n,\_ض&0/=/ nŅhjqM_=RxW\VkXNxzռ5 w dqu5yśCmރ7]D+.jEDCN=u꒯7h5SALMܞV-<ӗ?Oxۃ_sjFbٜ|mc?[݄`3=OSKknX<'y|5z\kgz9CxaȭP\'̈6ukե}?|"|ŵyyZW;+lmًݞ8כ5xSĠ16g|~/ Aҵ-㰘cm5ى>l/S<1?û2\^m-e`Xe`Xe`X"ð__^gcڜFlJ}O76`vș듯1qχsi;08gl˶:\Vc3fe#e3alœ0&5i6^]ߌe'Nq#}ۡb{q8,hfCYK~>O:A;a|bjWW|lu wyOD~FD8bk1^W8 -~5{)ъ(+CĪR},>B_c_+Z&9ZL}o&9=,*)1M`h'(GUgx `A$JT7dq/ _&ܙs-w]4f} 9md@S|8v 80[ܱ5+w?~â.܋?Ꙓ'3{ 8bf63~ﻃ-iGxe!nxvaPxqpdz.6h}WUga 1ve#ai}O;^kW:{fbZr̘l=fL5-:]<֊؜;폮~rs.vyk~]g2p3Ne`Xe`Xe`Xe xOO.8,{>oo}l6:c{#l:t-Ns)vͳ:v0?q:LdmO5 }.v|p-iq\qϱ g㫃Yo5gߞiCtW}UY󜃻Ws̽?4q׍klf9kwňKy`>`e׉{ëpā\%A=#f9A6 ب8l zY"W_OpEq 8s <#qg}|-kZ=Ldk 'ʶg`g~b? 0n!Ί#Sכ.>z0?{KN`ߘ8ݻf¤[ljiiW{X3Ӹ޽}3.oX'.7dS ]Xը)gzӊ1'Oa>]볝X֣xx!6QU|%M2.o6 1luj }w^uaGcru//u]'zփ8D`RGjByKPUӗ֑k)W{yڶ^oLDD]Q ZU;-oxv`y_Ǜ޸6SJNb<'Q3oy5 QJ/|j>ZW/vǶT,cx la/j7xzžS c{n]k&>gOٹqW{um#Z\ij{xŁ136qp(>^sL>q/͹.Z+1/ C|yߞ 9x᮶0069ݘG+.;yΘxU_}η<3w1/ιig؛e90bS+?_e`Xe`Xe`Xe5~{ӓwx8soo~n4w2@sj"N1> Wq"$:o\QqG\;x؅7bSm˦(>!o|εȗ3oFH4G!$Vz*)>81O/"org>bq ^ϸ"WK|jF 'kbEw6oL y5 j=k82+0;r%'`8b|[G6|ŀ36cG ]Fc}uٲg7e_#AeG>OBa6vD}klab/~׭y$p8D¤6}%Ⱥm>klGXM|nh;]AgFso>nVu>ָ`1M"N8k-L0xvڟa {-^|p/{-3gg77觥Y;7ý_qÿ/1;g`˫gz{kw@;g_N?l`,"smjg;pƯnϮqrсw '{Q1dW2Gc6lkXw7/vx=b1\Zp+^kE=B?Flψ+Q_"ku9 kQ ˝(/`J07?<&1lM@o1w=sO},ٛ'i6 W%{b8j]Ro&I.^rVg`Db6NqZ⥹D<|`xfO+Ƨc8r^3\m5ˇSâ.6rA/?^#?ýVnq ~o\?a=߄ő'nO8jݿկ?!n㗭=V>Qi/mafl{aǧy߾1g^6zf|5Xg3j-<9sk 6s/qSŒ[5On~O7>Zmj7?y4>fbO.& nlMLNcs7ܶe;ܧ߉xw7oXe`Xe`Xe`81o|kkЇ>tqw\Ż;(ۿ ⲷ{92쐯^6}vӶs/'Ft8Ü|Ϙf[3xy:y9ew`{١j(9o\?yt]w?gr;ol;x'Lh_k]'C,7>kxل=&O6GB|O /bfor&vku[dr8'}DzF†\qo}q/3_.sį;ސ RgD Wȕ8K #ET{ _XOo|/,h4r泥Ŗ#qr܋G$&3o5kl^>M}6sl`Y{XL5ca3s#&ANL vc|F~oX]/Dqگpn$k޼56CԼ=b^M{9VqŒODn[}7J~>0h_`L,ɞ mo0al[̷0 =Xj򛓫{{X1#> j)j' e>)1'ւ}[z؄|xĝ{- ߳;[a3ˣ*f>7_LZfމX℅m6Ŝf|M۫rT|3־ ,2 ,2 ,2𿂁~D87ɇ?ĩ%\[r:+f];l|P:cF}!7;3vy~t䩱왟߉ywc]3={xַbi皀aHQYoμ;s7: 7OԊ8aO"ABBK/gs퍳nYOoPDK¢7kD;ssZ{ċ=EMZ\q|x/u~K^r_ V9/!&8vgbA>1j5cQ_~Ƽ3?3O=fzHO4 cq!̉#Vkn$`jb2>p"i&z{[׸q-F>aMo^l}⾽L87O #+_A%z ox!Z[}o[߇N4ՃTU,^8f_ٸ;ţ1k~qu~{5?68maŀYm&Ó8>3 w/m?S_M=3zj4V\e}\V a56|߸ ^Z5;y'V}7Qsխ/l “5un_s}X*UsgWg|ӎmkZÝ7nvڸo]@ۖe``2 ,2 ,2 ,2-1p>`Ny`9th^@o76mgi뀳siAea1ӡ]c|_08?XbG~xxϿzşb/⹟1cfc>xڸnk(n=HD L؇1{o>cGTxzkh(8lw*ke\DQ"qx?1p^D9O, _bQ L$~]{c}JC8Zuҷa? uد~vo#O'u9᪽5Y%|yX&~5>+j{">O1y0.ր|>|+6_g IďFckm?+^÷?Pfp+XlowZ3\:@L<{Vش0q߾]Á}x5ޘaa .j{Xp[xbFYl457ƺ& Lւf+\j15[vp+p+^O1{Xb'O/>\2g<0ߵM+i|r)H 3Օ{}srW98 qgly5_O6Śy[m3un42 e`Xe`Xe`Xed<{PAa5?yhXb\u*J6 b:윇5&ƫiB7{6C5q}l՝o6_/36񆱱lcvay߽9c䨆b{ )ɝoka|5|{}1mؙg3aaL8a:z QDT$.j TĵnmnÖ`L#8yu"ZCJpV[Srs3Gl%h'U\{`G u84=_3 G^xQDWo`GxG0Vb}ӼZkX4>滚O|Noq".M\QޞVq ^%`N9 ^}~6oӗ>?j W~fں'8 6~mb5-fςp˷H /ǑDǰ;*{{A&Mf:ЏH sS8A~uk:hUWrĻX4sy-LT9߽k9+{g]bu4gm؆ENQru>b3g*^q&&y4TO)֬ج<]k̿ƯSW> 䦼~x ߟ~|2 ,2 ,2 ,:T qUj( e;gAӦ\*N[ vبyXKeo5j8cθ3߁朏̍?,gxZφos{953cD?tؔ;Ʋi<.ks5fӞNgbf# `,Nr'(̿ a#=蕯|!wXKh2Fy^vy[8g=7<="L̄OD\^%LTa8oGzx#{6<_o}qir=a ?{7Z9j΋;|#' Zglx83"w}}?8χX<欓Źgulʸp~*fouqկ~DR4Nhj˜`HFwogښ'V K5+zܳb1Z39ۻıq슡g`O}vcwo 5kΧOkuxa03ޞK𰇭+|H/k0MrWG;5WZl&sslb#}mOya:&ƮM 7e`Xe`Xe`Xo;!\C !^iNc;;'=w=(u5t8q`[3ƴ+^Ʈt;c\cr2m7k=_cO1}a4fow!=(gg,+O(Ƙ䉷fWcreC쫵giTwh>ECܹ?ĎɯC~" .1wx7 'u1F؞b;$1{U,1w(y"YXI|#~8fmkVbY됐~K~_&>Dފ7͇ mVy%mL|gNKSCB!!I,68!ʋGuaFᗾX&W9'܇7PD $aRNyK;'Xpo]Q'j׿~m\¼xrԍXz.l}nzm='kX&}Os kuV &>/W-'iݛEi|_ٚ#0hl`׳Y Wr'n0N;qu"ƾ*lit?1÷ax L^fk/[_>N%e85i_}q`N|/yTxpxx|5&0f[?=o[w#,2 ,2 ,2 ,nLv}>;'C0h6z-_|xv:̿\羺Xܟ1L,{ƒ﬷y@[gN:\Qi?sw|9߸+GƧq9]y̧w؊=Ы9ᡜl;w>XkĖXs!k5KAy|#__ .6ׄ:sl ޔv>1{&}H|Ĭo^FsvdL VOE4kݍ\a5&6aHi\k g{Ixv7S]?ǧ5 AO!ULa#'͵Wi]'#9Cax~_~__u_bib7q"{qw$Jl&o'?ypji?alߵu#b[ =!~_%[Vqk$GKX k->1nMՒO p_|vrY=S>mx5GW,Q,yYv-o8d _g/.Ԫu\65w_}z]g! JuqO̽f5&_qAcw9>|[IlaԇŜxգ=Zf'n6م;7b?٘7.V=!'qb|06O[g2\h=8/ /wXe`Xe`Xe`x3A;Pugl3m3ߍjɧCrIc9c3rfai!,5}[Yg~zN=bX}pse['ga{WpCms6;nMsb?1 ookWiZ+{1;&d-&\'>b'v~6]~DD0T^&fr!/UqKnp%AJle.`􁁭&.x?!^GGOp]a'8Z"!v8SnxZbiNxc'kzZ'_W_|Nj[Ukr=,|Y]k '?든N1Jp(fM-qG~Θyk|>|9>OM r%y|h}V\>Wz6{Ä7cp،m)Œ<=sbx`﹛񍻯vq9q^DQKov#<֊s u׼MsZ-jg،Z.-~6r:fGflƙ9pi/q0}eff`ye`Xe`Xe`Xe`C8Ǯ򙇇B;m;,^sGk-|3~vv?}vpys0|9Uuw=9v=}>qR^Un<޵OmUɋ_#–O:d.;5[k#=rbvM\Y&Gn%`&a%t ߚsF-H`͈4Opq-777=A0}F&FR\cg's=KbV;->x}1ç5:{*9/73+̫/2 ,2 ,2 ,.<:P1{C@}1fabCb6?$M>n,{;46E+^C8)~`x|5ڸwڝyo>pLryΚf|`0\ր]5M;쪥^saJ%{.ZI=|zK8 >8''8Dm?ncޚ~/>&r5y|\Y~]\{CߛԄ7RkB_~o$6'gYQu6'gjzSu͉%krnq=F<~?pb?h9X6k|=?}Yc֮Zh}[,qǘ|>j~؛WLa4^L{>p[_lZ֨4l}<lGm/aGpo.9]0lpxƦ #>~Z/vw~=^,߁P/H_72 ,2 ,2 ,2f`ԩ|Uuu2UyýC7훃ݗwV9׼W?VӌgCi_48(fyWU8򟜲qMj͹gxv^rwC "s_hm:a= a=lsݺ9눻3oKnڌͮ[mxZYq>V,9'3DK"ާ{S|)OӇ{bD >a𪁨hGH$#sh5~b%.$51?QV] ƈ^.W'^XͱK4;_jΗ=cG}DC<[WՇN.M,n/=;7oc5ij/fp~%7y_!39RZ՘n}ۓ֫gUfrQk*O؞h˿5W{s7?F`9||̵cӁrou=1Vb'>;ysv]uw0ANj}qk8^[>Ƿ j1V.8F~x&Xپk?1Ʈ5;MM _&{[1vHn0tcoۏV?p}FxNX.6=zxZs傳.qM\X8Zy4]Lx~jo˟|59cio }o{OV$usmOq_L|UCXU٪8tߚֲw/Negk.9#G9\l_-ωs :e`Xe`Xe`X t%uh2i[1;4 Ͻ{͜a(V=൜ݗCmY_.}-NCiSxb߁3>(U:#W\Շ=\ +֏Ϭ{cq0moV/~3|Ր]ҳIW;x߸0W'\gVp Cx/.L{_B@G +ŧ7jarLБ`T㲱𸏳$Jn'q5h}V&܋pNꔧ)VAlGKX އFtJtn'jV8[l 4n*X~h+f\4\[S,Z|(?1 f1VL`2%8^ڇM ẵjoscNrذ9r+}8[1|-.?,'[8OKⷽO\_W_t/^[Gqj<.\aZ?1t ^y>z+gKl}t-v2p e`Xe`Xe`Xe[fyw>,t50AߌY C (w}e#f;srgwU=ӬjkXcO\W糏|;.W=,ٯx+N6 WO>m{VX IDAT0y+͗ӽ8sO K1kS˩nb4Zy W DpHF0"RCi¯^DB<{B 7ahO/&9>L=G__Z߽ ̱ 7e`Xe`Xe`X@pu(d?]Ř/N3δw*mbjW9Ŗ]1wX\-ӦXg6Pkƍ0 13|_q`X~ŇnH|5 W}9M='kգN_4D _8 ޺%g8%$f?Qf"N$ZX0'rG"Vy3_ⱞwu7$.q9b)^̵:pN=pkS!h [8]TƏ1OtߏuU#l`'-zB\uU[oϖxG 9zQ{yƾ/o1!̋KL3~:G`o?3Bu'`5p…'k58Zg˟ljgk.buY !{.Y]ۋq()?b=_j4'f>rߞγҳ'1n喋;XKkⳃоk_g<~z?gg^-bnz->̷!NϓI1yOLi3/^ϩqk{~3֮kŁaiGs84_+F{'-0Okȿ}kG~091=9V5w΅wcc< }Z8^q=z=S^|C]yص}'EɎ}Z=k޸O:s/jr]' /'l}yɶpooҗe`Xe`Xe`XʀCI?tg*xSw8v;_8N3@HH|  @X6I;3 rocUҭڻj ׺R]Cy7؝v7OXv9͜嘱',N08s΅CKД Kq#S=w-tx?q]A;,4??'fNcr%̹?c/xb[o>n'sS,qz[}4:3]g˜p{rXE}"x#>K$>iY9BjG\DjMZux뾷x1.-P>5+$& CkJS6 {y噼s/7&='l ?l$ "QAI(6is?b11>o==[{>"jpUBAۘ9xw o$L؈aVqNwxM\5[a[_{p]\ǩlC-ǭ:mxoɿ+w~^ vvjH 1]"-۾n{߱꒣=:~۸I=c=|j߰w̅4o4u2uϖ.~ +ߞ j bka8;B߾@x`+W^avg/`u=쮵_o |Wۼ>nfqUXm#f­Qe`Xe`Xe`X:;!cr@y9muq̞,ǹ 뀷Ca'g^6Ʀ-VGaņ}V[;h.83Og?l3/`a7vIW1̵^a\ 9A|6a*~9Wm r;s|H% 4OLH \q>O_B/|yoABbH3f<D/eK#j%XI_1/OMr4aSc‚g>[k [+~5XIIƬZ̩[H"|W?/qX>1'6o7dG]V,%$?Q76alm͈M"Brȡ ?L$M]o ˩VTU>ޠm/hϵv5>[]\|qj喫88~Y,pO}z7.5"b#k773m쩧"@;=S#>qe޽|"9ܾ%|&ȶg5x=3sxf<^=;b qkFߍZ{:1>G3G|7y}QCm^srI[ck8㷜̗'\a_k/a(o5>kήǬqc G~Xn̒ ,2 ,2 ,2 ,{e`8v : ;9]'F}]ra,NXgxay_+rt(>9lj='yk3FkoaJx&]9M8zw`˧s c[ ɓZ|XsSmay~N4,>{$bOBxBQq5aݻ`E|#H%GcO30#<]";-QSBW,vZ>>UvL5S\3'XD:Ǖ{B|! ^4~l0%%T6q^/>AH<6Zo`{p@w=-7_ ̽A+, &samU{"Fcf#{;fr'`ɟ-bCh[>؊gKH#[oJ}WaYhϊcĮ}mX>h3h:n#NeglXݷWq>E\6b6Sirg޳-Nug}1 tĩ5c+V挝ǬaX aXe`Xe`Xe`X:1y`u}bP_?Θߜ307fg_\Fhx?OW\va7Χxz>;puY. :;c dŊ36}@C+<Qq7$MKj'|YxSMR{ߤU5Ə|lĎ~bS>CG\oЊ?~g偑_2;/Q>pZ{8|ZωleZ֥=Xۺ}饗7o\L 9}eoKˣ.uk/ Wr>;;sk_ڝW_}h?&ęw7^UL\k[3ke jRqg=>sZ'c|{ZWpǟMo5416j 8[k}W/f֖/۾Cn= bC>Wlk[jck^~UboU[v.1lcrT+o͊ϜcmX #e`Xe`Xe`Xe=10̓?z:<~ q1m: o#"]g47=γ+39g9kPo+b8PΗM4}593YuXaluYkx':_l:<=bΚT}ćP+&$1װ&Zx*>Z3 o}/5Oz[ouO!%&HaDS㣹8Sc/\7-^ĉD8?$G_- O}뢶DO~q+9X?i4kӞ`ݺ#idd \':ZだyPo5'%~YſUcGoK03Fk#&1uYsWƧ{ß_worR Ci' Dr6|`/||Kѳ$7uS}1/+㿽޵P{Ɗ ?6; 6sO#?=;=3G,MroCh5q}ӸXy|EϦGת=\3&~qtmq۪QlYq2'c~ƙ\ܳncۖG}Oxac-2 ,2 ,2 ,o0u9 1}sM :'0>{cPSyĭ–>|X:,mvrnO<Õ:ɮCړ8%ͱ+v<'ްΜƲqϮ؍pỼ \oSً55#ϘXBܚD9c/~1AL ጠ$Q#Q%S 1Ԣr q*n5^mq'n6Y*no^ ۽kB0N*[\kǚur\k$o{<<~7޸~šsg䇯⪡=wYy>897p>ƭ)n|[+57H$ Sk?>a}Xb?6l բ晛MuV2nD>W17lEU8>֜>.Ĩ;m'g? c e`Xe`Xe`Xy0ס]v0v\/L?f a0ցfRv`yƟzb϶Cه'{}׬e}؊gO1ĖϘG=omğG> qSO]=)A\0єm$ĪC^3`*g oFq=ۄQb=sV u/:QlX}a&Lbڛm=p-y\iyT[1*6-MN9{NcZ.qص&!Ƴ:3sY. ٱQ ǜ69xoiz6,G8 G8;kl<8ֲu߿W⠜-~7l]R헁@F~OW62 ,2 ,2 ,2 tupd{d?ya<ߡyءw*3CCON+>i><9 [7u}O\O9:Ls]\|:d?-[:WyO!NEU,9Yqū'>׃m@7Ax=8K4~C^␷='ΪoB :?LT#cN|W##^!QD,2Vr#g}㥵2Z806[{I\F_}r$49M.q>\%~yOp&(ƽ}uO2F9[CyÛPm^^؉? Z1eex_>lƝ{⤘ԡ>8;]9`|o}e~߼# ?qY j3^6yŴ0Zo9Z38~H\7ߞq-^pwIv[ϞW_bvjELhej,b5W.9&O+w g6=op7V swߜ8n*OϨn>5y kZSX|'ve/~滖uZ|  w _Řm⌇bt_uĨe[=? ,!7Eߒe`Xe`Xe`XˀC6pyvv ρ]k+Fl|γV}ap|8y⌟7,=k*Na`s8 gq/_8@x(vM\On\w TS\8Z3s{GNȧNg;.W)N/Oڡ~B]fD o*D&4&:s 7V?0# iޘ/qO'z9_"pǕ 8MgךV"bgLk q' A|n=># IDATixmgĮpƗO{")gq[l s0+l9ٙ8,bXݛq~u_ {V<k|޽+-;X̹ۜã9.˯}e?Z= pӳ"X^﹌=,[v>ӧ('.f_˧{s=ou{b6ϧGo_?_NUɑlR]ňwiuto\{}:}~S c317w%߶ ,w 2 ,2 ,2 ,2pAyH7`%5>?3\껞o,̍p{~iڝyOr'k6Z1f0\ 1qc-[ְ$ܹ_u/||`I#.Soag@ Gnb\˵Kkj/n(Umm^{S>s셹#'ĊO;˫Fk;13.Q֩9\|Ͻ(G-/ՊxĄ9qXةIEs kx\p%Ugż:ægR|#;sgyrֳQl_y?hi8B_i])u4嬦|`#V cΧ:.-.g]Mxm}4>l%ʦrܶ <"~{׏⻆7ߕ\e`Xe`Xe`Xwb}k$g5bOC\%&|C^^f?ܶ~_xGǼ7S$ á]ϰՄ3!ͷo&f &ة5\= 򫻵>,yud~7yT ,V}Ԣo}2i۳o/Za0C>8|`':wc6av ol]wߡgwuC0;ʽ3_5*f15y\p!&δqзό!B^ٙO7?9vboܴ&~պS<󑏐3ֽM=Bxل-[x&W0\l!*Ŗ+0CT*Mr!?oe] DDJj/z6:Xk^M l.A5ѓTvbJLdMxc 7AGFR`$֞Apws+֞}gocO~;εo  @nsiK>q:͵bi_|Pfq |A zFxpkr[y&>k q>Wl~@GLXő='|=p*?-{&[Oӳ͛r|#q6Z5_ٺ Qak22 {`Xe`Xe`Xe`X#01 Pp󀑏yx91n-Z>jf;Ё.bƟoz U<6u؜YOwU={뚃7~?y*^pƚXbb A9 qziaa}B^>\LKCIL*o5ױ#Kp)X &ą#Df"zȑW#of័Z8ETI4g60?&9rO,B85G(׼ML@룉)/̱OUʏ- P/έ^9" [p/ӚZ5,>r/w"[8Wθa=uZ󭉺O}SWl"zķ8OI"GlЄ>Y|peo-8~KC>8ѵ|l?珝{Gr=&b₯OB9{[_ oύ]wyq&qpгsz 7=~ue،YM+[`&/]jg[15anͪ}vkÇGlq[ְیq^oZ'xѫer&mkP\dx>Ɗk,\ٹ߶ faٺRan; /MnXpBc, G\E|%F`QXԈ8{ۗ*OWϼظ.NaI<ѿ|8Omrᚯ=o֌w?Quċ=Қ5{Mʷyj0NAkZ:3:/3Џo2 ,2 ,2 ,Oyءf.O3G7<=mq 3c(͗M<'LחOKdCwv_]L˜@?ª7[%XZ6q}y;7gC"QJ-C7CL7>D-{6D>"!!!}q8{&_%j®_q!?&{IJ7H&DWu~.!Om^mx܈:'+5$b3̅md|Q?lgb>^.~YSw> E?/,n[ɘ(G,k cb(Na:_GGrk823}FLu%=im+.M\o1ĂE ϭya>E<次x`Ylyw޺ݻwubb\~14y~fطΆ_w8Fq:!yîXg'>uD==S+Ӻ 8sC9ǃ=5O][xՊ s|*leW7su,v5+,œ_6+M?tr9$|*v_/}>h?h72 ,2 ,2 ,2!gA<s;?߁=yߡ9Vp3y3MTev3N635/On=~w_}Ū7n>;ףX53>EjiYnZc;c>>}9o̸<ߡx;\S=?/%X: aqrO S'&AWO$''W1E`F9"Rc8O'`57NX 3\k-H\`oHfĽKD@\5hFnkkN.V-'3q"6ᨿk 7;FL%@ 8כ JF?-s9^ ^b&?<>a|g`O\lֹqą5y5^0^-xB_>eXCQ zqgٽ3`?˥Vo[<]}p3'?0'L:۳jvsjsƛ}+O)qv= رq] ?㞫6s(qpVo^6bu-z#&X\Ñ/,l|fݭYK qXLq5y晓Mxи>VK19ߞ03q۶ ,og`2 ,2 ,2 ,289wv:dv CCy!ĕŚO,fsbUs3W89Cqkauӧ<و7cYy8C<׆Py[xbWn_8Nۋş͹8sp?pb3Ob.1ucT߿D9/]//]B 2Mzy'h 7G#"a/vk;wەb 3A/'\('H1˼Fuxs՞#u&$կ^ƿ_o?ÝDs__^Dޢz| iQbV6p~Qsv>EqMmb.fxB}Xȗ=;\x7ͱ7W>0ɍ{}iclժNh:aO>yU9]\Dw䰞XO}HN}58f﹨n4׺z_Kk?X;b7yFysLs\}ons}Y_6rN G<9p;{큾+`a3W~l#\~>'h/Ӛ<3V'Fk W&Ku41M|ai||XqϽZg\O+e`ؿaXe`Xe`Xe`X:̓;:P`϶CioCgͯG.nswy[3n84[O.؞w Ptf␶z4Aq&up_ˉab qǁZ8K&3w7/G9gonҞ/݁{~ +܄Xb E׾va!`;\OHWBa<6aB/o=Ńp ĬDKm9&!@ʈbB3?73QQ'"5.p'+qb^G,͜u0f~-βxB{k7]1ikL,|4" *<_QZԼ~mr;Q> Vou͵zwP_V&qĻ),8^{EpҘz&~ + UKX"H3ѻWLqOQ W>.09o7! .[8 9>zKMuMG8Ç>6qo"L>,be>GܰU'DPlp$e8<4q{b%Y!˃os'φqN 5\gPzWW/}-XbRLՆOxq'V[GCk+cE)z{7˿\|쭧+_ʝ8ه .xԳj=gjhÑ]-W#;֧=0铭k.q_p?$5G v=`3sxl}iizlnp`3.+w5\k5Z\iO57xu_68Ȯ>qgs=2pgz72 ,2 ,2 ,2 g:PC0nσK~  de3muhz*;}'w*~ oqgƦ?qM}ծ7i>Ř̓Xc>3~0i ě5[s]sͺßz΄xg\:kcǴ)VP^8Dʛoy>4o/Q@Lyk$AL3A% P12M,qv/P$0-ƭ*vjLK(nljc;K.'!U hDm {HSnsS.xq?k1n{??10-_Y>r~h5#ӗyX95o˙a+O`䯜ͻL3n`~5Y+r!y~%6PO|Vfg]Ūa|>O^o^p_㍱M(LJX#'A5.6w%T86bBx]D˜x⺆Kv6Z'Hnp Am1N$APꏓ"܄6h +x^TO=C񕨕S h6]?{͆ɞaZVCoOYel%'<9&Oyٙ'N [ˎ0g`j&l֞5C?$Ra&vo|לaz[/Zk(GW9Fÿ/qggc ~?QO-|wűɼ5j((zw ZĆ_coG]>5rYi|`lQLv8'{6'n5mo {߉;˷3>io{ qar\msQ {|Xp6l&~ͤEc8-eqd`qշe`Xe`Xe`Xe}0࠯þf9QqZoB}wh8;\3wN|M\p2ɬ)NnO,3G׿5Tr/rنi;~)ˑPpđYSMYMaI0/)˾xd/F\= ge<;'i$'&r:G.~D*sI?1p 51j*8;x&BKGj.^ÍyEtl팷gH?K&ag52%foI5 __TdSr%7G30bS+;cȁ iz6|5Xͫ=e?ßfO9mO‡aj^!jŶO'e>bxmWvCm /µ5>ץCo\nqas1o~i_g~uYX?f0%?1z1+>Q1ص10=ݺط}ޜ\ xukac2}/_Ƨ}{`x o³؇^͉=js~r&riqGcNs94xƾGn~Ǹ1~^z&̫I|6]guӋo}"=8e zV1ŚqZS=&+3~MNcq7c5Q޸b-^jl˗0Wb.0lk1_=ǛNq'!#ALNB7%23Vq~r5QG΄Ö^{ΜGtsp;r8Z"KrG|`?ri-[z. W ~0^ "{{=6"|s|s§!XmDbΆ5&G#çZsĐ[/'\}ϙ#!1~·8H(~pcO4 3D.[cocW5hmOgޭz[xd׺|(>o/ZĄWçza1&_=_ޘb]Z>jޘ [|}\!5&_o |6~g:ٯ߉cX_[GpOi8CuĪ7xg?+Fdu7@_o\B{k}abɛDǗ^W^˯zuo}azꚓ?[ s[G6>u_dlI\Ve;La:Oج_T{K%jr Z/0)cF8fO \bKx6j{SA#DN .M|9~4xkGN#^{=^'.3m=21 wkUǞ3̵'ޘ>r9koU9NZo61|b?xx뾵2^a(uU6[WV~\W~^e`Xe`Xe`X~Bu7"< ̶yX vX: 'l|i3='6<6rjcyu5wy^ Ug]'s?-0ߡs3/qe N1"0Wqao~D bObUgu8ie"9?b[0~pݫ_M_6V˧~ج#qQ-bn'5bG.D\bf-6_zZos k扒Z{6K8z;2dDN e ޘD05؝] /Mr?a2g\.,KՒ-Txz]kg=-bg/!ߞq©o߽{ju[}о%|a>o3 8~wgC.7]x#~ _/F3bHUzz{&{wZb{֮Jd5n1Zo9 _8ۚ&Y/_k_ANMqŵ&l}!>lɼr-1ܽXxZ6zZbj(F]O9js6U>5Ϊmfx | s2 ,2 ,2 ,21́de;0/wy;su8/7#3NXm;4k.blhx0!k 3p:8NjL']Zu&^Fgۼi}^4c_p%@ʼ g˻7bXt be}"k9!l{ $ -0%uuo.a |'ZU|~n ⴟb"Q]QĚ8O6׸":0A *qm,$K /~׮fښY'+<z7')]1p.7xG|I,#F #Fuǟ-Tkducđ#nM{O.f{Z?5gjS p{v5)~kc#_kܟ^q;.{{5?$w"s·{"o aM_UW⢽Ɇ|roXuЋ!_#_N1jV\s] pm\}Rcނ?;TKg8[Z0|ͷW`dZ[8|Xk0_ްX#o?\8מm~rzض ,wVM ,2 ,2 ,2 ,lnXCî`u{R5Ug毨bv[waPžM1':p y0:1cXσÖ~9;m쬩I6j9qnO [;\ӎ}{\ak|ⷮn<[joxglpE"_6"c<^hM]96a$PE$%y A~.<y#Ԫյ7gL}3(o}sWl$!_BƭF|WTT/0_Uqv;{A7q"f|hMsC\:aP+=p˵+go~gu5zY]FϕÍ޼'~#x zjv {%Aav&Q1bwSC:w=;[G\"qq4mʉg[;}WLcG|q:dp./fT~l/1^m>Ř}627F3<\ n{-v#c}~V t2 ,2 ,2 ,Ou@P=Ǧ-mMǧ3ogc席/< [)ly0}<8b swއ_Μ N'Ovާ9cK :7507+D| Su>pF x@Pj򑿾:g~(q F C-bTkVGy-`P|DP0:!ÜXDCB.jbq#ƈz:|16ހĩUJ:ao{&ʵzb~3 LU&?_B'l5I>^==DXD\O7g}9[Zw14yw_~+*X܄HshO<ĵ^z饋??[O>7b{COKS) ʥh"._ygORV&@*:Q|jL&~7ܳe[ ysz]z5_{P<Y1|>w]l}߁{i\;@ װ =ߞ%\33pxCl}^LPMp}13l>^̳/ŷ>aQ?}Yqkj^r[g3.b}pc3/滞co7c o2 ,2 ,2 ,Ov@!^ps <,Ws洟>WÒ]Vssb|3[0Wɷ{0g\i:T.B>ʿ⸛9J.g]Odڱo_O wG}xͧ1}Oa_DOb%Hy#7}1 7!$تCr8aF`C2N|!:cx rXvՕx2VCηz&(17IՅޖy`F__]95~JW 8Ԉ>:ZGœK$W&p^تEXYo DŽgMXX]֔)obݛ4o"7A>v7y[.Bx+ooœظHD哿 } _ւ/n&p{ 8Y<Ɂ+>;ך8>Ɖ?"1ڃSk=,=W歧W^ϵV}Ԍy1nٳu cu/zwꂣ~'{36p90g|{;] ~Z_UW|}+;w㞷pN<>ᩦS=ǩZ v1:'~vx^xqXn^W~ηWl2 \ aXe`Xe`Xe`XqruhgCi7_@ߴk%9qL 3mxa<15;dx|&W2.gx\jg_aqM.X1.:Nכ됹|Onv[VgAq/FvgɫfE1uJ`~f?DKSmq18][?Ǎ|'1b3[q"qS;*&"v8J4#Hg{qg&~׽9Q=2yFb1,q/qЬVAbGjjO% +I~Okpۻq>&^]G֐k͜*&8Ski.1ƽ!++D' IDAT5xI^u[j#qUDyk>rRGd_ lf#^nypԾŧupoG#,Qݟg/lޞg?8Q=r{@2~?m-e`Xe`Xe`Xe`01<>gw|bSn8?zcD=o'隘 0' rO{Nn_|!|eGmas!bģf_"0qOO.q裆֔Hhڿe]F^s7Oy6<֟f/qG:{\m8ę&1؛jOe'qgsvx5+uo=Y8T1';1Zo6_L=CrMoɗ{;b=?/{=\\k3'P&]7g=^r⾱x 6?kFcZ'k.^^pO]8y﹘yzxXq[2 ,2 ,2 ,20yס];l{s̃Hcu1g뀒RVXoNonq]O[q0:.b᪖|_69 \M18|5u/Vx7[9oԭfyrnr8Nn߸ ˙0$x.{]ⷦb'GR#!7 Ht!IJf||1V-^7Zf^ Lz-q0V /yMhZƬ=8q۾v/l-N0'|]ϟVU#ll5a''.r֍X駋*6~<DNZ{C>s֗zŒX eoX/9a 5Yqm z^zWg>sRGW+<+ƿ_wA g= {pyw};wo~Z93ޢ5/bE-do'hΟٟ?ܿDl3c5N>3f=8{M7S~M?ZL85#j=3.ٴPM0,Z?7ظߞ񜵯̷8 qϧZ=[~]Ϻs8S|ۛ|سxks$ۛkXe`Xe`Xe`)a恟y0h9_.`q"46X7?7dIzOg|s8-gsAk9&GSl(W(3^$"WAzb̃ueˬ_7Ƹ q9|_o?;lj*׉Ll>c#vW[=s]J+&||ǟ^#8UM ; 6D0`':ŏ{~[O9-:9VU;D`B1cbD6V7; 5rH zֈztxxj/^)))?["K/t mpu]nx^&h'?X؊A^o|l嚿Z\_S<}{N ЄD z.4ށյ.Tഖbed/31~"ڏbSkD ,|gk&=K朚ez?~Ų^&֔_L|\~\C{{?*EkwdCi-Z4 *(x午z!Exwz%U&4{^˼7#gnvcb58|gݛ1gxj߳c>h>n.Yֺ6yvω-_ٴYv;-ɻ/2 ,2 ,2 ,_l5 :Lؕk.˙<'KBNKf`l=?^8<و/q漹ekzG~J $H+xQT@SBQQbbOcx 0*L a"O&b [d٩p؞ežbBu ᭀ6qiۗ`ƫX拙 jd`S8q0@SI:dǼSs1/lى5_ n<у/.įh;$Wd;WpMftb/ᤣ9@~Χ5U 4HN{3.1UO+>g<zt$pUgq .1v6؀x9klp+yk/=3p' `cX}{Ds3\{I޳ W=\ՙGfvF?}=9X7fﳏgsSs`|x5]w d|->znŤ'd{vqiMs_Ӈoam-7]{3/fz^xJ|>lXg=Xg 7;1L;^t+/2 ,2 ,2 ,_ qxt3QzvC s5:ӞUJ $𱕏Ol%0;gl%YÙDj\f?0Mps6-!8'_d=qUbl{V ;o񗏒%f<$*]sgo H(ɟQQԗ(J6n"ء8X1\ ڼuT|y>_*v9gҽ90W#x/c2QO_l=+6{xWϜN6qL©ৰkk5o/Q! _z2_LqRq=e>ЉB1`M[8{+S'G?⏃ ֊R ЁXӼ3*K ή<<>7o?\<h=R+ΜX 0؅o$0 3BMmo)nbQV S l[b%_ #67.l믋!@ ?b陀ۙp?|2}M;ld w={a:}n!6Zsb/4*wWt;N3}sc;=vړ>ukƦ6(dX|7^zl#g>WBJ&?:S$p$oܛ8)xr)|O 0= 3޳-rԓk:ĭtK8/; jO*rXgOJСW1~`<)쐷86و͕w:I)2*Dڳ }ac}v}g 5E$7ߌ<9dTͻVR4Ʒ>-,{1*⮽ 3; vFoH:i:[t>?'6_'l0[\lf l=t3F珮y<}E|bl/\~ 9! g"κM1~?3GI>y>Oqu^4 t^Llk-OKtqug;xqzlw֊Z1ew-e^OΜ+e 7~OŖakqO6[n2po!Xe`Xe`Xe`X[b[tI:FZ+їLTדLN7K s±X)|XO.g%录pC.f]W0A\1}s(oO㢳0V7[%b5E>g7>0zPFƛ0ؘp]W\|V񶥷=Ͻ{GE,=_bE: ζ A3Ϯsk=UPXǡ"5n oDV<krt`tٙT׼Oa૵٠g/D3WwML'9 $Ӿc"*;pa߰}#uwqsA3u,Y¹{g6p/oqsF/k̎,/dϞ==N`7l~gI|,5m`dj?lK;\ cx)qtVUz&k 3af dZ䋃d/9k쵞lv?孅='&əwc67m}n/2 ,2 ,2 ,@I7zZɹzdK;'eY GsMƗԙ1Qq&JNJiڟ3.Q1Yѝq|1|K6~w&c8wܻ1ɬٟ{-%׭ ē_(t+r)]O͹K+(da~XzW"E?EwTéP\zE ^=`DWud⩂,GS'zوC}Oc:l(T, *)ZQ \lU`BB"78[ۙ[鋭"X&ڃ&E0)ɧ=z.o-l~Cljyafs#sh=dϐdWO=QT\wz;q<uM/ڷptN:FGqI ޳ßo=stY0s &>e׋<<͍goؾv[\ZgX|=S}L?,mz=yK.Ol/{aNuٞf~=thzg]ae~g,]<+l5bN6L Q2SvO6=}1vFf Oz1.d%?٘ٹe0й|=6H-e`Xe`Xe`XeML3gm&KJ3'%1gM~ea8Ǘ||ULv7x͑/qI'lĐ8330O/ 5[$9ɗLNj#Xܴ?m||>N_YӬ) (;[ {6͵VOJVNEG>pl^a.BQ|e 13EM̹j&L{B&'Ύ}o)Wyllb _Ǘ??g|Ű9 l^S7_ly۶9_d``J0)*h9 777يtfGї,,<"8[~؇O zpq?䓇7칲/촗+} Kr8B7/ׯ08zƾlU< ~'\xv⧽霺Vgϐ5;C+^硋.Mtttfs>՞Ż?pd l~ }eO%`}XRl*_.:qgbq8{>C6L^La#~z&ɱi G!w}<<\>JYv,R褫inrglb5WزOq&m}eNg`N?2 ,2 ,2 ,2p H0-DɹzdɻvO8%JNهc& e38׬},bד ϼ&2fո{ |'Wwbj\Lq=ėSIysUJDKDӋ٧3,ğ1a32noW([%㌃,d%?Ͽ\Ba=7xw8HMTCzl;/+c B 7tg;6*Wgs@3/5~:tk,u;Ld%'>5S(%[ kޔՓ`ŷ{kb=AByla8bp+qgGt5ƾg; :d>lN؍.gUlל`SG1Z+mb [%p}-{S.}s0y9)Nn&㇬1_>I7VH &0󡰠-Gy(.&" =:SR %0UAEsC-0ѭEY{FǸyXM۰Yc[!l Aq?;p}t Gn?Z`Ŵ{׸q6ܷq9rtWr؛^\z wgd¦Ö7n}#yVtusn~vA1t`38?aW_%@ڹF_6̳=$ ]3ܳӊlVpG߽n珯?h=uQ IDATÃ*d;bP 2>O\=#lw_Ǝ>o<,p8g,\1{)x7Gf}^07d>=٢oN# Was=Vk3vkd\LauMJz}ojݰw7wWX?ű;?G8>~X.?㖄e`Xe`Xe`Xe`Xn%KܕKI(֒IPlO|%$D$lxdk~=_Շ3SlX*{? L’^βY,*Bu}D)T1'>= /bġ`axV { R >x+؞x[ڵk[`BE`$.=8gr;wdq <g;/Q@ULNv_wX`+]u[9vb5;)¯5oЋ>,l{62S>ߜ1ވ<;ml\!Ob,[~Șo_wL Llƽ=l/΅i_|Zܱm\Ϗ{x`f/qğ5s)6glu{W:kaiys"=\|o.` * O_lÌ/Np/ybb(l6>~%1gܲ=n/w*[Sw~^e`Xe`Xe`Xn#3XbNdL ԟ{`bɽ欟%‘ߙ4 OɟKNIi\?qq140N'nJT$~x}xuZ%aaG_ٚ1љ.iɅ,'7O17qSbK1;ɖdg#(E:X/SEqFAHnX\|(|W}Q@UDQXR{'YDފ.6+z?.>>q71-6.>7dVn_oL&l;  6jeފZ^!X1Ox!gX 7?}=2gIYZ{1׍u?{.&'6 x7م77#:^-s\qY'ٛ\εl)0 ,_ Զ|Kt2 ,2 ,2 ,2 @I8}\I)X"Oeɗͳ~`ݗ &7mg7/QN~fKM f]pd?g _"7^e_goaIb#G|Ư>38ior=ȗx؇Cmlv?ϧyOW.2d=g8b.yb vot]nV ;|ŭ1z̻3qGc5qc' b0S1jgⲮ|cfo<*h pY NQ8TUSRP֋W78g{qf-nȹ#ߤWg|+YGgmq^gOχ觵>RVs~g?(ك\O/eO.r]dp!\ ~3pcoP\˞{VѸs&=~i!|ɿyzj.&?.h?/krO澰/d:0kZ#מ#9cylu?,6kf\dXG3Sz2]a>[ys&]؋|k͞d8,zhtW̹:ai+f[X^+w, 2 ,2 ,2 ,2 VJdԕ ;k͕Iƹ6978g{K˘-K[ؓID崕̜%>Mƚ+%ZxF6+|O*kabCcb|\>K5_򾤵>\А=mp[>a=?9+jhَ?\bGCa֛*N$6+&(уW!NF1桇:U0"__yp۟y<ƒ7viEⰢ lO|F`e d37 @bʖ") z t`#>de1?yl,~-&㣳<vqLO՗^z-0GZw.{覠אּ[dq+p.2o tȋW9[сsJ^8!os&Fq"sAl.v`m =ob&]G <}gE|2]E|:gg>.{|1g8(- K'kZz5-F\e`Xe`Xe`X Ld` s%oL9W5Wlcoz{Q0/\|Ž#+$v:]漑ܾ?fե ?ڋ0;C#[Ɉx=pW,$9eýX4~s1(fRf>.xvsEOگu"*W'W?9la34gt G ql3".`C3=܋˞weςiqÑbF7>/'Oc-_Z%k5=lϚYQ dc'{J.:_8̈c76%pg`2$o=V2ۧ3o ,)K2 ,2 ,2 ,2 ,@ 9zq%K+QyN +a?eW_2p&KqL8w3Yɗ YvYٜ>)l^gOqOφ>9:*qM<٥ő%Go7vog؉b1?s7* |1E! k2+:ۥ؛dy8Uĩy1yUQ^W_j+sRpM__ˎ/|OO7.si?UXYQ+/z؋_cv߰„2vo>\3sP1NRv&`Q;#:#{d?ɱ=|v1t5u G2a֞Z3^s>l,̾L?co·/g&6؛Ns9m1iŊ' o/b=g0;a|2`ɧg/}ᙾՇm)&ކ+ܝ7p$v9NrX湞gq&(g2٠SbܴZ0̘Xgg*Z4f}}+?B}|m{19Y 0}Ck0s .(2%%[1@k=aGE!CA|EũSDc}5+917vyڵXǯb"|![fQ~>ɳ羂NE)!>㥳HWsφg/1裏%EֲƮBToB7[#V+TUXo->c>zqYJ~Ë̜; Gt+[{8ͱgShcɉÅ&;'qrs1Fg3._xuv+=W}Z>?s㲳? z8_Ǐ}~o]v6= Uqo?ɓs~qsgn뼝WJ^?Og|}ĕu|W|yH^|賬s]yC1ُ6?Yn8#e;{ljN٤k=l=jz0aC2>9g߳{0's|C|1_&/yo/}K[e`Xe`Xe`X?b@"n&J srp&SΉ~(+Ii>+Ys+Y8q5N.^3mDhg\/9xr_Kc.􍿫b?8OO?aKDτt8W񮏧lJ,8|تPSL+v57%KЫPcjr)|VQ` '{>ޒu(D'|)JH# .'u*L^6c|F/`yxx?vdaWÎTЄ[=:qWpu}<)ذx[w(UqJ0xK[MbPP@L<'>3ܻb"c G?a쬳uo*k;x?=0د0ɳo=aoz^{p9btQD{t<|lYs_o33l+[ 7;poO瞯C甜{Q1<_i1I?>$vvΗ8l9as ==o-q3N社8 \cȇt966khl-Uv/2 ,2 ,2 ,LJĕ+yN3W2ț233Qh ika9O>$i~韽xz<3w[ g8$gLF}LlgE%̋"OL)'|^tMȏw)(}ػ(xN!{ dr q.jgB( s*~*Tv ziO*Z Pd֛ ^n\}c<+|h|*bU&8Ʀ—7+k\\KΛ9{[z|U|g {ٹ켉/in6aHў=ivL;Gl|#69[=k9+T|upHcy뚮Ƈ8p L٫PKV{h6vīLbp^Wqn</]M ;aϪ3SW 9\‹:Dl y'ldt&;:lbÉ5^Mg-Gg,Yl']}~#m!Y'O>e[:=zd9) >{5r0dO s]ٙrŞs,a2p1;m7e`Xe`Xe`Xeu2P`I|% rLܴϙYB2%>'`o_rq9Jfx'L6f.fr|sxc8aO^O\S؉0L%MS=diݽ$y:~|(11H_ń':* )<*((wI+ džOEd|*T8`V{7<+.ŬgpPqf|(zě>/E75_+vgeq=z6"9lV,g0KW0?L^+)곉ao}Q|{E":0Vt5Vs6~ogc΢dوqNEM8" 9:Ki?C޽ś]WK=63[dk]4.ɈI3@:lg?àA>[ٳ,Ù:lv`s2_s^gs[t35^9_n?&~2\l&S)Os{@g6zCLl }.2 ,2 ,2 ,onfrPr<,ib5s3Y737.NY Nߓѳ?b37%g|_ٛs k3)j$%^b7[3Smf3QXxM3'q>{Ie |O W4ˌfa 3a߾Q̾X@`NNY1(bā984JQP߸?]@ 6#_>:3ovzEEW£{8561O Zb1!W{'3qN{5_#㈯/Se?Q @6Wq}+ΝObbf DOi' |K7|D[C@ êCkC= .֫19*v8V%#ފu߹"Þ蹱.^|_cjѸʜgVgϰo6?s?rΜ=gX>/ z8qo.~<;[>~?w醋.ٙg>&lo.-N>؅| Nsbˮ9cd\\>$N.]Ԓ?ねfěf3N|le?\c IDATo2 \lx2 ,2 ,2 ,2 %o˘}%ΎJܙOv0l=}-KDOSvʛ'on&;͟’Ό3 C'o%ɧ[:\);(8g ~%ٔܭjOƓK%b K熾@ɕ(4}&W<'tVb"=UPđW$BC{@bB+:Qbo69ofzkOF5*EN"l=?w"܋]c4`mMo*)$Yg;޸ӵB&/ׯȳc?!VXmM2֜ =(:so3ӭ@K?{Wd'Eӟ ~_Xq;f{{)^Q85'x\ SI6el6:=wsC!k> Yso4CYjg-l]KOٞdkbӼfu~\X~6\6eɴGpv~ Sƃ')헁<ΫGKy}-2 ,2 ,2 ,IrbI΂+y"!HDnK`k鹟 ]| |\\/꾋>Kj+\/l2>Pִ9}еw%Xۛ)cl]|'Zm6gx:+ӮDtq /gRܼ5[soGuqo+ďr"Mx*hw{Gl3f'c9.vzU=8{~ŋ9zkVWx?\sYsj55谏/.Y<;zot1_0hlk٢kNC ._<<َ+vc}[C9YYo1dzpog}tw?ÆygV8'}kl_-kg.ba/ohŝ]s5=ٛ\zɑm9kُ|7&lOLdk곟2p"K2 ,2 ,2 ,2 ,@xK 6LJ6$1[%JNISg|>1-ĉys_W 1XA❾BG\ңpd`FⓂ\E-kހf%z-H+>Z `/bgۂ}¯5 Sj,q`ťF"HO 2TW.<äU4jlOs'>+^bU?i\x{S5x8R V<6|s2b dcC_vz\pO+*ky'wߞ۹2ZgB'9B~ws~G=Ή{K' [bWO==Ϟ=8_5}{q2n`vpK2]qh G3+>+?ooN1q7'ԇ}eNg`Oƿ ,2 ,2 ,2 "3QjIotS"R?|%^zh D:Ϲk% 1_.J"&Yv&=υ)8<ߟ%Wo\D:%^Zsccr2m$\:,sf⛬W\KlwN͟ya3?I.%+~PRq@_Ua*.>_S6|SPLR "KAK10Qd15t?} ;}r*|-QRuϮg}z^_ /nG7p능/]}%x`|}Y+vfNQQV|dPɶ' =84{=[ًOoo!\h!Gao>_a<ŵ˷)| ~#lW|l؏=KBAZqREŏSlb0F ~X?nOgl+]: 8b1+~~lϬU,8ɵ?Όu՞j u[pʗw#vynq+ dq( {__mzn`8C}ίL͞1t}CAS:lc;?=ۇ6.z』PӾ/{Zǽ>m9'r~'9.ܟ{tIgqU kV7wN*nZE=s_{bjlvccׅdyI|HmdukTъvQ@!UpPQlob)b;@^qe#1}1`vw-?8*%o*V! DQ46'[RކTSkoU㆞*Ƶw@NYG1 vr{}%x^lbQ$f#ә1v) 8qGEwE`83Ʒ"^\.ax.?ǎ- upUOW}1e'^cWԜ8+rm0= ٶ9oP+4!:=?=г6. 0Y"uf=SM/~;-lxg.9k푹7.}7q_sd|+ll#Z ϗ1 _i=ixm2p0oAXe`Xe`Xe`X`o⻾뻎?$g~⡇:d~GHϦ #?#G[o GV__P3I@Iv֯JM؛g`ٜ S,^2+ KJ Kyga GwDUlqĒw1N{|'U@*%{[?%l87&Upv:CPMfra1-sŐ0*B~[wxc_O'G"/l70X>gV02}}g?W)$|'1W,/qʆuk >ɈCOskbOx͛/yΧ">O/賕\8Zӊόag>Γ[`k65y?Gx黿_%+G.}۷O|kv ?| L<'J\f+Y%s%ɞޔ閬o|KNs|a?/F3nxLwu733g+KF/>m=i<adjrPs{|鋡ɲ;UX$OcXn>̑P_dc<=sz1U!_l4b,.>W#v SQ4s'۹xSüZ%gy~醗M\IGZNVSt#\bԣ ]* zRa~7m_8Aq¦h* <;}kq~Ml|ջӁ\ܱfg^bs U/]~U7ew^хYh&Eg-9ɒNyu M[1+2pug:t:,Uߺb,wED=qˎb?x&ىOțSgEyp;|xHVg?G|E,jWxqįu֊zsctpe /|d'C>e:aqܓk?䊡18sq|49wXX'knbGvwW;;ebz2 ,2 ,2 ,23lЇ.oM{//. oiۿ}~GGG>x//BUs%f"L 36t|̞\ItJ8ob;''7妿LP6WB&?a|=K|=i39}Svbʧ>%|&o77=#o~L|{z4O^:MV* xU\|Ioqөn/ߚW<;a۫ }l<eK# 3 rע7ŭ deVQ+G9zljSZbki07טl|v>3w_̳>͑osa-\<4vE?7I/vR?y>a=M׎;-'`_e`Xe`Xe`%T& +9@M㊿ 9_7}7HԒo__9*&U%=K>sgyw3g.~wMh4 ǧ Kǁ  >#^'lQ2,;|;?Oq(*0*~9=]Ūx&ХTFo%O{Q||' T\>Mzwճ2&VF2g>;v&gs-`f+~V x W"='獾 s@鱥nM_~~awHvs_w8,[}gXƇ39`Onw{W[ ;xdl\h3ɶƦ^疬k>enkX ((*Sc S7+B@4|M`Q۹DGڵkG*UčKd/( 5"/?9kYt:Ȓls5*Tu9J< /DFkYvܸ?^ξ=09~*tdܓg^E!Jaҹ{嗏y6Ǯoon΃9kxKBk_g(z]6k{FpIW\xXhݿGbr~`qSvWk⯨KFc߿uȈ=\:._Zcoslp oƻ%tzN_0Ʊ \@F;Xg=jΟ(o[go6dfo 2|y>}1{M2Ћ+s݇!M.k'F*8CᲉ/b<'Y|M~LN9m Cqvɛg1۶ ,f` ,2 ,2 ,2 Kt?zGK$x?ɯًٟ韾G"LH&`N"~͕F.}%f?%Dj~&|3ٳđ37w5U1q&31`uNVKU y_xJZ˦$=3 T$1a Kُ T`<Ӊ?t_~:Z{|6= {E׭zOGT*[c˘|>t^>Uy{{|{gxc͘y*)<7 YE_oj.gl RQkL?dtVx軧ů^僌1 IDAT'ٸտ5|sok**ؽ +&*fz?U(؊GGP?/]rzxf'>t~n59ɋ퇞}_&e{_:.=S{f z҅\_->!]g)=lY6 ^e.{}Ϡ1}߸ҷlӏ|0JK}28B\d{n̮F1\20|:ٽt| Xe`Xe`XeɟɋO~GR[1o(-+eNUw6Ȓ(H=d(?]e%'1[IJ%gl3ٗN17㙘K,N|ei㜐<8kz[-]WA/鋎sC8&Vk8cƛ\-]8sLN'5v_stfeYgL𻟗5q՞Wκ+)(V8 ) _A9}O }p¡ճ%>ܽ9W9}j}uEbXgXǥžiMǸ=֓i=6y^gS|>+a"s'p"q(]SKE)F3bS#;̑o:3 }7ž_|[b4c[ARq{9b5glY[)1| zd3ÿ3=Ϛ1޾#>OﬢfϫKQšU>\8B'װ~#1Y;k%| 'wIno=+ w;0g[rc+E.:#M#Wd'f>le;7uɞul̑v4~kt5G :䲕l,uXYu~T2 ,2 ,2 ,2 H~Ї>t}}GBd=䓷. H_}HHz G9F~^tVHWnJ_uD^>:[ $[B`,q9g:gNIc~&fsgӷ3Gf;m{l?y-}ٝ{G`ZFN砘3f+;%%';cw/yoJ_gG%G"KGeG؛jqF_Lz!'bu`:Ⴥo(_) szx8ث]; R󌅅.C$rR[>q7X뼊!? }6+OK#lt*"kphǯ"7JeE8d*Qρ9]1pEVa/<>a=W? gV,٩(~z{5O8i7>sum9Cd{szcΡ#~ѳ<,|MY`{v~WxdKklįOC6_*|ɵg谧``\qd<i:+y\| $𯨡7.aon Ţp'ŜϚBHS'f)#Vwɉ" gz.>42Z2,xwqP˯tS籢 숵jq90?U{ -EWSmNLb1ǿU,d*ng:f8_ z[zOoF+X~ŧ>,e5v4/=whY>ŧ}ơuE% A,y#ZOp.vq;\yPϋp^=[7G})v7{ s|?H8mO–C޾[g_s±X#=;Z{+v?:qdN ÏqOg~q:cK_FYk&=Ɉ%{mwOnƗs’am\g .^9ך0i=_l$[<ӎ<7 ,2 ,2 ,2f__8&4UD<=EPDAHlQJA[*yh&1iwqu{?5CӴϺ`g_>s]_oOO^#9(%?O_񖰄\f%亗 <';'ȔߔOF>iݸ~6~71=ӿ8ϘUInr}7&wNֲwN7q1_տbFXJJ#k䱵i+4q'wE=L S g%%c"8WCf]yskvcߊz-E&Ev`UWU^q9-xP M"(*̸ae6eÏ}-pW`æ9: Tm T_Ep;Fbgػ[)L~cEuk0;dUKv= f?^~öz%S}bf_ͯK_< x^?VDXX\x'x'#oĒ0٣zz],ŀ# ޮY@f S\+IvB..aUV (9Μx\T9_ fg`qC8q9&Æ/&L+.X|8'dq<Ð/Y7g3[OeG\әa˞gq>?c NX4k,Z+ǽsg pNם6{_rɚonreΚ85ϴ8avsrls/a£|ȧl~83|d'gm2p-aXe`Xe`Xe`X^޴~5hX.3ivFkzYƾ2bIsT"N.ֵtn}&vOL6U"1Ӿ3Mg|ߌ)瘳Yr7ĕ\67{ǔ_)̘߰.qE/N7l |;ɻә2m/ [S\<և7ʊ)[WV]rں@[I؝ 9:bP?y ;{5aήUpWP,ٛ+>a*x,b%väi{™Bߨ [`k;k/nX!CƜ84rlɎ9'9'qү E-_x; &]X?`*`'|XWtOo(Gv%م/S T\$;>~zoֳsǦ>Clg~b9|k';虲ÆF2*b}&5y׶!<;GtpA^G»QF0 lV7yc&sWVCٟ=Έ7[g--gqw}gbp;דsC<*NЏSc4i_ܳf5p<{{O:ЃqJ>-lٜf 02(Vg'ٙ\7n-a9lYњV39?9h=ݽgṀmo|^) G X/~_ ` [޼ ,2 ,2 ,23 OIЛII]+Ag䠹3g%ζ 亲U+yHL|6.AJf5.73bs'd?q1>s0;g]|9J\Dy{_K>Ɲ8K^K^R_&IQs U4QPȤ(C Sg۽`Q M+.YV'*(:Dm1q~_COS<ȟ"SQ=",8gb\as/6W xwh{}}!t=`ϸwޝO/8a‰>JL{ng58 0_aA(Oq/80hoشt|m]wq󷪽VV/VzY2 ,2 ,2 ,w ~f.%gE_3)X2L)W2qw>gr&0C?frr֒Fi?'lGv}  t7sK6?Ø̌Ivi\º쌭*\=%%޷No<}}ܕCIϹ]@:%Uص>)$U y07o;k0(tw'̇|$ V큸f G^|N+7_Bu*tyʞBX{F.Yn׾5{%ajoQ-{{.:3ިC~+rph׳+S˼O=Řdފe3_硽8mx?|oaߛBÕ~o1mk|6_!Ԙ|tg']a.79l]~鐟m!y-lil~Zs4x͸kgސ]t "Fr0Lg2ѽg8Le'NN[{0,Cs?: ы˿<>þs+1UV1~e`Xe`Xe`XJ ~Il4'1\dy iP4'zn [KwUKlN[W٢fyLnהa"8N{o3["xʷgdpr̆=QPcWSpRh#k%ت'm>2a&Ӽ貛> o!O}I1dyV{7F`o^{ӥR1T͝_ܰkgV1Zݛ 7Nw8g'>Z`#&zގnOp߹6Y\boq錙 =6 d(~K_:puoyp¾u66oUL }]sk}Ϟwz؛|#F=\0W=vO6oU,lX9Ǚ5_Ή]|dC+Z:}k|]\.5Ǯ/w=WWhNsYYt|ܶglrb\1^K. |ǵɀgG[/~~G9z<|Ї>tL8ߪ뭈o`y},2 ,2 ,2 ,E `<,9I¹~~i33h$gUXfr/M MbJwڰVB$0l洑H#xWz>49hlZqVÔc+9cN&OviDzS'C^#>ҟyr`s16"c;بw<Fa? NNEi弥hjO" t;g˶X` cvTAԚ"8e,'w2Y{\ lxc5p܊t!}sΠ)f{KF< 8bf ?yb񓿸9oʱO+;y~vn^8lxfU,kOoPÆ_q 3S|bs,n,e]3f={=ljbll#cML\|f̧;]sdٞ0'枧=+q=?:[b3/{3섹;#8&wS'O_v`;<>_=[>OO1uϞo/ٳa` okeXe`Xe`Xe`)*&a7y ?͝Ug2$?\ǩODF\ Cr%Kg39c>’y|8Ƀm lcBG5.iwݓE1t>YVzПM^g{$%_;v]hSgD^1l %VJ{챣- x:oJ}+E/-@13 od=N _8p[_ٛae:|y|CEFS$S򶧂\E?wIYE#119X}O'#D`͘shm|zI_'< _ QF9oD{[OaUuEd~z guĤ(I- ŝ`5$L IDATsZ_vh왳BE ks1[hݭ1cmSvfS~ꫯ~pG1+|^>`왜/p۹lzn5}k}]3p圊9|]<.b!nUΚ?8g.f/K ٴ|cؼoGqͽ=YkdɱW޳d<Ƌ9Wg>g%2/bPh~ s?ֲMn] 'u tVEYt*ZI>UUCM,fvk~"RE"/u* ^Śl#KO.aMWZ饗n|ƭ9.iu32>,æ-]-z˯b,y$ GN?Oo*~:?pYg-oC_k;%  欑-T+<)+PWaᆙMg_p`O9*zR]\pV>}hEμXa{٦S* ΃Ng<)`; <Ufs#{o%g] Ɵ]t;l% љo.1&k-x3uF:{}V!6{߳OA74Z_E, G0viϳm1gN\[i.6=g7ٰvg GaWҳΔv1ζ|ȿ# Gt|F?{wgbkel嗁e`Xe`Xe`Xezn&$JCU2gJ}Sv&CK; U~ؘǫb(8Og֦< aLĘLzdϾ˿uW xO 3)y=1sb/J6%[PA&ř~>J[O-q"_iN߅3Ί{ŻL`GqDKY y 6l6g<sR4s݋ٙn!W#_$*G!{q'^2a:G0O_o,Foh¥`ȦqȇAO}SǛz׻wE Wa=ŏȏ`_i{#W{v~Gg#uNtF5HW֝_{C=qN={gppXcbXԊ|o٘s:e<3a62b$>ac퇻&\5usx_0?hpw\ͻMz{|pbcqfH|\Mpe?\cK,Lzޞ w}gy7󸶽 lt-.2 ,2 ,2 , ܜI@9xN;D=!_IAsd씛™6'#xO8ɝ>Ʌ>L>gv/iδb:J𺬻f*7\KVawN|&Ӿ;YG2ߚ]Z 5r' K ‚~oUQ\pIVH9/U!Vg8$ET67ICJEslijtcE?I ~~~h&daɶyc6T\jOTS$7)=U^qo {zv7X}* 1jOIwc,~naVϰF|/(Zv/yg6?ǹtߺ߁ĕ4YzqLt^*.?۾SEOCarPŠ7}1lz*v`po/ٚ:mKs1x9;;%K0+di~EYފxKԛϞ~8ŞOޚľbUQTK!#/5iB^r˛>9vm^QZ񉧷 Jɹ3TO W!˿FB]wu{Þ|= crlz[[MaϚش߸9=l,pk Wl0VT$~ᇳ+S[aF:?+ڣo6+]~s^_x+@zYT ׺ y|Bn `bSdBmogVOqt&[u3VܑU,`aWc zl[Kv|t+-c6|mԀm,~#Vtp\t=GAT<`C\==tȘwv[_kޙ3f">'&͉g U豑lg|{PLp H|o.Njg-_ɲE6=0"[3+Y{]>42+;9-e+*._apWauozzW:b8IQ1o˾i|U8l?+*͖ux!PJsb5N*>~[V!ܺsu.OVܽMl浰s쇳9%C}Wq׌qG}[PӞW19guC־Ї.ϰ~|Oɛ)i,q˯f>_O#:d'9O}{LglNg:5W- lk_#y>;lO'w쇏^6'vzwst,K<1L>pι3b\\'z;2p0_aXe`Xe`Xe`X^%(I038S [vëtfr& 39i]4_3oh8;G->Lc SoMRAе<{3b4GLd[\E]b<يthabx^+:{TDPmxZˮ{E w:b-"b D>fq\W¦ daߥHT =dfX\bŜSź .a /Tl*6¡[mvTDK1~aS'>quVX8x__1vzk3;ǦsF6uqsOOq+X{V!֕ng8'-c>?0uQ/~? ȸGo\1_γ-kE{J/~99v)<ڏ>!/68Z1[_w{gllEooGܳI=/p >`2osɿ}7SȅYїm<-qn 9kN^΍(+n-مI8j=5zY'qJ.du=챝-}\ p+pwvwl]:i ;͑yc38A!a>ɰ>>hO9ganؙ1%e|)?eⰸ庑,owrw-e`Xe`Xe`Xe@ L/qg\S"ݒgܼ'=eDU>s|u'L(%'Kf&gN;ۛ\X~O*^iC?e~ݸ^:g_W%99# d+$[5}_Z~{LàPBD1M4 hқ U%/FQvsXL_)Pk]qGAmr lXKpNNሌbJI{\)ßݟ}cMVAg9Ǿ'qEx&dpMQX1bu'~.6Z]cWB*{O?uۙ8 ]XK!oo;U+_!k;c0=]Ç¡i/sZqM6[n9 gȾi?m}wg6]k0Wu1830Zg1̰8`Kx&mۗ_~:0/Θ}r,9gZ6>oz8uဌ¯gJO_sNKg⇮XO4G&^ܵ sWrtͳދϼHOFZ'kޥˏFkds^7OX:w'c=̇=,>VK9ԝ=L8mv*?; ܌ lfyXe`Xe`Xe`x HIkܝik3qi7r=ܛɾ3r,I8L6|LMg7cO\&֓F~m%;ɶ>x2g^f|lH>ܝ{\Zs,ƭ"~1̎u),َZW8KohnlUtЩY_*CBV~2ŭhcR%_L'[qTQšΝwy`- P[aVtg̛ܰ&L2 2X{;WZ}¡<GE&<'|-ъqQ 3VlǫKQ UR%0V!voD/v.Wg_`na&R;dpBZB*CCpЇ|~̿)~^z饣CxqwCl~.^f3`,vdµ7~Eb&oػ{L቏] eùvG8‡̶l;kQ\ f>ۓ7:=CpYY|9;~=WߺI CS?_VǺs/=>,{}>S1 vYk0YoicO)0ͱ:kflz㷖8sܳ;Nۇ)Wyvk|žlx*&rۖ-'`_e`Xe`Xe`X^#%dLevʟJ[R{ f}ʞol~^v&0$&[3a$?e4]~*-d\rl嫄xSq!}r3&zS6lm8㟸>clLn_EsST$Hn68[d\t`#')*)BwcE-Vi[{^y6ţ{8m_T0z; V c63L|f?~aTNsQ 8>w0諑 Mmq_BSeWa]F\9 bg|ůگvpm'< bg Tgbzoz/&/ֹ_Dy`A,qjokw\8cW:>[0S|NZx~wΝ9v\0w͋O9-}pXKF>뇽{ݘGc;pך_~UsFw | tY~W Ye`Xe`Xe`X-\@/9v%FgV}k]W @_ϼd9qh\l>imbf|$/ ;[s*3:ɤ_q x7[dr{<(l!9gcHVz1uO>qk>*pŧ y٦_BG:/.cpb^qGAGIv+v*^Z'R@\~v;r:Vɿ{ܶd`Rh2Vb_:_>ń/p]W4)n-GHE3_ 쫰7]0^8\LenM.{SҺq7}ǯb%{ (:ߠ ;`P(}"q|xꩧP|0g)r՛|ΚfN̰zQg<O?[K?\x.{<}=4 C3;S=3t!;G_m]#þ{%}Ɇl{,q C2ưgȺMf>|s3n|{exgş 8םf{OZL6g=no^}Úbevkoyn7oؗe`Xe`Xe`X@%gaIs3I7W%&KFcw&S3_6UIðHXf1{&%'ZFɆZqٜ|Xc0~ZK ԟ'aI63_Usk%}ï-uf\̸ްWr^Z+QB\7MBKqdϾuUG~'N\88 v.pqgCEbM+B:7p8ϞS_Î-{|#G;[5o[~ ojtqW+8Nn;`K{Kfm⩘'Gy;8ά=Z'K؜5}Yx7'l~{̮Y1g^`m^>3+~[&1A&LYYs_Iwlu g=Ř_2?q_qY\r|.p v鲭59kwv?e׼V=l# Ɔ#am=ߍxg|Ol'wuw _Ǧ2 ,2 ,2 ,2p3pU.N$K\3I8gR1%*g2&%ܔ-jrj'&k7}N'٘~,|%e_{ڙgLqd;grv'Fv*^gKv'qOoL O^$oqZ IҧbW:l[_|BEX3%*oV0|h?5K\|($)2]<]~] 㐟p2wŬ)\c0Vc!Gs'!>E:\W$[_}|-l6+X!:< p;GZ76 o鲡@s2axqE s Omxvl%;+\ ?|y9Kx>vyG|YKTן+4_xW=:gt# v5s\Ux '[K8?ly&p✉"00uFX`ǝ>g7zaF˳8$_jlOAg# >sasysak>Gtڛko{وx'˟/ɰm!yk w>iڦ56^ո+,:lό=Y_a/ߓ88˒bU%.ͥ< Dz<ٝ_ M i0*/ʝ}W,>|x:K*92o)"WU57`埬qE ]@7;olߙW{,]_uY{gpf&*^E޺".po(C\ l{oWP$W:M,ٳ'=x\G;C;Uy×=s1@#.ə8Ƈu|Uf I¤ٿ=ZW[1_oeO?}1w|~,=}N\+6lC=ɱg=Zl8zfSφ+_aW>qӎ9tJ1)Q9\1]Y7^'^qp1glϽfq<(icY\s<`VKAra_zagIϾI֒^AbmNq]R}wCE1PEb{E}v^zc]E΂"5_Lw*_HGQJ#xY{ ^`p|/~GŞol_:q*~VkWSsgq;ɺ֛v0W*l<<r΁y~XOT5?ab_v]an~7>Xqo- d{}NC-yܹ |+\vk,Ulnohyσdz>{56֞I {|h'ώs/?=ב<`C~=\%̰bχ?_r\bܾp#bJ=L7n^F|>>ko,O\MZ1Ɠ+~)tB[l|V܉#s3YSޜmHEh2ڣ3 ҹGE%؀7t*41TTdWM{.fzkFQ!S!Q 㫝ܞ:8?o98qu&=z>}뭙79 l|뵞A>x~{=i?z3scܾa|gq9'{guunE^2t33C>wV:W_<'WL;|Ͽ ͓su݋3;9ZMysdCc><|𥑋0/6ߜ;{dsMKcPX2p3l2 ,2 ,2 ,ke,8픠+h_3g|dn<'~N<qnpKqNCX;غxǯ{#bRܱ%p33DVE=r0ޞK\e\W\VN茚MLT:\88vıOBx?0:Oe,6}M?\dd=,ǽ8VpkqA&Ioܐؼ퇏~|yVܶFo=e{Ε@nLt#:a4Obyv*Jšu6+H͘_Xۊ K"K2ge7$]b_qEqQAq v+Ul1G"U_˞fqɸ؍*Ú]oA*RoVX"G5 ZyYxh_ v+}m;;lWb=*WXRDC~ÊYda+4zqY!Zg|m?qP~ Ox~1k:bWs{==3RJoŒGbS\ɐAsg8gYGg}uOog1y*¬ħy{=g+[L*Zs1Ƒ;ql)";:=ǰCG%um{϶{|pG.:p[wXϾ;LYkܴiro-:qs׽.e?ؙ2\ga oXXK9gڝؓ,gk _=kle/olj[~_2 ,2 ,2 ,w8%fc&3))쒒8l ai*B+t+y&̷v4XRM??"w +xX#S v+)P**mc@?E/vN)ߝ7+0y0ivŰ|'CQ+0,{r) ksNZÑqŅeSԥNn?< v.80lq}ܓ )h-:~ ؜[2r׊r-Ǽۿb% l<|Φx{>NWlۯ|{^Lly}y.y#3J,bMkw⟏ dC?lvsQ\akƮlG \\ۜ;}YbL~+z+֊ǘv`> \\|?;.n}w_xS=MqL{qb4~ىSdi?ةxwiOp)Ʃde?:7B֞v6*'lZÏ=Usdzܝoglk8Oxf|Y\t%5֊_6f +.z~`qo;+f+§뼰wQ?*unх#+3/tny};Wd$o|"wS,3|)j;>̽b-XgDϦ= g{h,&ݹ/,[Ǚb^ iIvoOE|Efci.qnepOV܌Xt^~@'qPo߳Gfr1Mrg.er7\g6{_(7jۥowXe`Xe`Xe`eF 9_P%͗LS8,):g?$^r5Oې%PU٘3Ncg|xʞ\\-uWI*ɾQEۉ9ȧC͸7/UL3OW2(|[#+NE)~YĽlV`& d% =xT*Dly7 &k!Yq)`# _|+Wd#>s>6t[o_~æbjNid+< wlk~U`cOodqͳޘt_$Nvʄ;숑-0K1GdP|we|{ӽ}] ~ї{UF<:Cu ĊbP=k+t巚l8پ̞d{^3w|3GO:0k֧!tG E:op6ng&]M{h/̵?7O?k>я)I&iMc~87mo=g;^nf|3ƾ ,2 ,2 ,2 J 2So& NZrs>_r0%!鞓ɘ\KgÕ5mO8ێi1SoxO  Lٝ 0%_9.M0YS C 욯?lGrٟt7_|c<' Ak #squ㙼Cqqk`els_ @o ΂y뮸oo`g_`gu ?EWE4'v`O;%}쒏#L]a=v_ʖ+]UhSf~e#. 5_k&K_QfW~a cys~T8aNNQocI0׏ӊ9*)zW\'rێQg@ᅮSE#~Q`aN &>W7l`3Oqa6޲',lg>;>;,ˆ_0gI>쉅N}ܙéy{(SoV" ᡟ| L:_Y\tW|O3s\g mb 3{ Ɲ{7ۋ)V6n֣O\<~oCb8A3ut뮋=_ IDATx#[T]8+|x?^޳i:oI:qokg@,cab[įxgK=\̆{sk-œ\vOxL6)ܵg`7n?2~2 ,2 ,2 ,2 |0pN0(Oaw7lKBv&fg+yX’Nʙx:n>s \xg2]sO᝼W3]JTϵ37$wؓka&GGGưH)uEy1CLgm}sf??f:|.l''zc=;֝| GGqu+y#}vysBW|J&s $3~3|ϓZ$7vg=X=g{{Oz3(T'nEŁ+CтM"6tʆ7U4GF*?(}eo&ǟR>ވVL7J(S_Ǖ+(u`ϼQZ-~fqЬ+Vw:0On O1.wv/bfKaZ1߉U[}gO}bE'߳#ZL.1STHwg>caK_s<>/5ȸgqe OlY`~>G8qey6k[Xp}b, o+~{{+_O6zmvQfW%z=_Q2W<+3.in>{\уz85yI?^m}o4?emMRq/2 ,2 ,2 ,(V2q&J/L[LMi߼5qĐd'gf/Iiv&v9{9iSRn2ϸgmf81;+vWq:%_]i$w:|GOn·'麳7H8[?[ q⨨w>ݳQ.lw(yow 9VqxͿ5+ܐ 2ΥBY6-DZkϞ9d&6} #"g?p% L̄5Ī88L>}u-r{! o R f0#sܓ=mY1ŗ lt5{ [a_pnB|o.ϊpd!|k3vƖ>x58g_3&%YݨS܃lf?[|asp6Ώ%.=)V:t% wxͯ{83%_%ѭ)" aƷYUQBBK8}RDf {.zoA_3DGk'?5ن]S cwq[Qִ욏}E0J[Ӊ'18{ӱ}VOx8͹ˇ9{;7s=w`PЅWq9E`xq,(dž"1Ǐ"qb8ͳ]9O1g(5n1f`3 "3\),Mqt^!t 8]{D=qߙrw3wψysb8½~љoկ:CqYOt/2JO/Ƹ?Bp^Yw;쑭0ߝ)}>lglzF<>+}k"~q{> \le<0t~LdwokԯO6]Wf=W"܋gr~:gbծD6 ;aKvϗ{17gJ$,|}s+<]s?d 7r s`ޘl }6*,Y}^Za\k³uk}fww=W`*m>X5ZFjq膯m5FA* `iUQtu5<5̬Y9YqΏϭ߱7qa~dë+&;P/-恃mvݿEN k`ſtg/Ĭgg=4'g_cO^Lsg[k}Jt4>kwS>bfœqݼ\E V(G|P)'+N$n<l^R[WKV:HG&EVsg3SR): baWpXWU=[q %d g>& /pɑXCΰu欷7x P=[3VVt.)uְjCr&7va3=ck=p]>dt-brcrnn'h98K+F[ҝÇ=sv} 7zn|'x#<7dLƉm/JL!{W{j·xk tɾ3I+ƇOaצMfW.lC1N>Mno2pK2 ,2 ,2 ,2 ,y9إ\]s^΋0`.3:3L'rbg^r)\#63ogy<қO^'_.Z'inX^+[63 3/Ӛ| GqO.pZ/}.0g\k]`ӯ2}7CEf BY' xx}E??\o*X y`+|0l/g/ sz&~ۿWJ8P¦"\lt,^ĂzgⱞL!ɺr+ _g1&ewM7/+XWPc]_<>7k g(0ʩP}',*b[?E7!}΂7/x,ɩ]Ν\5\ź}Zx"B4+ufʱXg<+EfN\ [qOO!Yg?x{Za3.W2C8;ǰu=lsv{qO(T۷7x5]}EhcZ;1?$˿ߚ?? Wxfy~YaZ:}q8 ng=79>s;q7ƺ&~8=a%onrشK2~q^|~%!9z,!y賙 {C71N*ͼˋNkWb7̣i?}gS]P/2 ,2 ,2 ,.K.~^wIظ>[yQq^ vhmh|[.P'"9\"5.ffdڌ5]2>]?/'dt㩋iwg;a^\ޚϵr60tE>Y8<}8/^s9 >(0O-SРxx}*>y}ǯV;Ś &Dɼ9XoVlⓢ׿ŇGLaH_^+ R:~WW>+ fm}'yɉaS%rߚ\1+u*tN燏ZHk獍<*S~WA^ïq_~+6I̊5ɽ7??ә[':?4.>?8?5O\`n6t+> L7 gZϨ9s>ؑe)'8>7|U(>k IX?2 ,2 ,2 ,]s1r*ɼH’_ME M#'Nk-3w{ G'z.^1_S6OνʷeykbN|.{zŝ7|řtm[ z!-xOؠ69 n*[~SDqNLƸb 8rPۓb*Py{~T=3bg.?\f[~0!(?z~`-eerKWlbo|A>NEGδ(Y\\JA+W|<YG.헽v>f3*VSϺ8{+V޹0_OϽc_Z_^S/1p,ov~W c58uj s:a;xΟɟ\ |i0Wo]꼈mOgLΪ>|iK6|wVٰ\o7Ɖ{6\?iמC^KXg{_‡ .CA瞻 l+&V*_k$o9?e{_>jqU|˝OrcE$[0j,WzMDY-|2CG?`p*,5{(gćOr{oK?S7q9s>?(iOZ0ă*S΀raӿ/"5- Ӈ7w||3F>EN6b3ӿ9dй;W^0kPWNx<pkL7`a#v%9ab'F#6}^ɚ5v6vm͍[_{G~/?v=0N?aaWK/ل%œlc013qҟ@_&kXe`Xe`Xe`?@!.,rn^J߼#kٱm+XK޸qglMsr1+&t. n`U~p?>}5bgW@N_aMy KO-;塐^O7߼ j@ Hl&C0Ӎ-7GovkLglb ]ÈED2s<ȗ_E7%LxēPo_osq:;bO\G5xy㩧8 mj:)zshdz+?;l*RD*pg. [_ZVN>zM{ֺƟq;*`ſZk0ɵ+ӕ3_L lM,o|g/On1ʗ_r|푯uO"I;HBLpF~ᆥq>JAz =?B̪ IDAT> =l^cZ#5>jd|پw&1c[/c>fLg+׼SkdkK>fN30MN~9?2 ;$,2 ,2 ,2 ,2nwl}]&wi .]dֺhlm^r~+n“͜cbl[=N,VL{8Yܝ]ʉ.6xmrdgxpju6zk|[|d|Y7ro0L\]{g4v?{X:ll{߅Siݜ}~!|&E!g )_o$.F,>8ku^la{:Kws%.81On q{8"N|C9 2pg1x/{-{KKn_8]}'7rÜ]ab;ymXg`Xe`Xe`Xe`X#.΋.]dҳ =!;[4 ! g] YaV}L<'bq:|5$!3Ckay/z[Wm> #x(Ukp䓭⑂ l}饗.NNA"򇑭,(im|WpHH7#:c.<7bh|GƳPh`O|\0p-YMѐҕm\Gbf0=8*Ptٷ'՛a>YEbvabo//֓O>vVA(j}5g~OY^b~߸ J w3Lr7Ωr&pOs19Ό~oa/bf־o>lSzpgt|;c10ΣC\pҹh_vv`A?;q{xo&N/d^~е[ks3o{Ӻ^c[[7/ԋo|ҙf;;~oۿxߙ:1Ӌ簆eb42N\GkĘ2i7ׇ}rz>•nN_;^G[2 ,2 ,2 ,Kyݔuaҥ <.Oͧ^L.(GWз>/7o;&Oy1jK&_3V8ɧ7ūMeyOB,X1+$_8x{`'-YY ɼ]pW"S0)&~|]/zP?v`(T!L+<gT_E|ҩP{C<QCa~.EUp__7H2/nk_Uo)w=6f/ٗ[θbw;V>0Ι8ƚuϣ={d0gB#=|S~owĘBЙ3bx/YkY99]qgOZ|t&˾=ŕܴЙ9bv^N1;sl'gz=s|N.9m~dz6I,\O2 {e`Xe`Xe`Xe]2҅ .e^/k-uh|#N?ear\t)9Zg_L>WOrc~^ڣa6'V]X1< \şMYռ8x&2b\ lOUvbXű>M\5|".|ռ>jUS͌覸ȫcR*zj|8IQ |+б^Gbg )e>:tvN8d39+WݼQ|zTRS׏._]^;pyǮB0=EkG4vt샽S^nŠE3g繄}gfz">[1⇓?x?ps'ϪV|o6֌k1^>gq$FB(7_gKwK|N><\n{Pna묉3 ٭)_yÓ^l :{o6ii>qō3헁}n}Xe`Xe`Xe`x tȴ .>.+|SAlf֋EGu9:/25~f}Ɲ|7}O-t['t;/sW_yLޅ|>* i/Ƽr%6ovy>9c+YTS.ŊWk+܆LʼnD_jSO]= ?D r((&+Z&>z#W*25H(V 9bӕWN)SޜYqMS;rQx[N H SB3o/)$5UVVx#=-gE',+l5}8hP^f ?N;'=zMëP'8MWx/w/k~=?C> &7x}\{6,A>9ۿac]y4د6q!G޴3ܸ4%qZEL{ўRLϾτqy|'[xˉΌ>~G__ztf,>Lg3Θ#+_jO.,5ϙSg|O^,?q1xx? s~|X[߸2 ,2 ,2 ,Gyָ̙\KN1.7˘9)R6,ޅ,=)s19}w)״"K\4/b]pϋZ=Đ,,97c,/>]\oƠk:nVVq}~6әOS ⇞o}|euO?i1滂%zx:ȚEk?Ťϖ8@kӥg>Ϧ|P>[>.Yo\&!osӁ &」\'31oVA8o,&'߹FnYyx:C+ Gy+?>mvd[>%'_eלљϰd9yEn(g3nwܗe`Xe`Xe`Xh⼜1HO] |Oٹ.:b.._>l}boe{ @-N@Q: CqS|ѷo l; ˻#<+dƣs#_0˗5;tSqCKV.ͧ7p|륗^|l,~5l÷'>+P#8љ/|v:+L+|:UG>9lL_> 'ӭ yO6M~&ѱ^^=r}[S֞z[ًe>Ň8\{c&сOEOba7 '87.!)Y.dp7@8t/^|u'Dgc%Ϙ_cm8g~ņ7N>e\+hme_dSCds'r_U+жe``~f>wt?_z ,2 ,2 ,2 ,Ek\?`_nt.'~igN.m\ė-i:?gNaY6NӇroa6o-0μX@0P!dץә+@$9!.̧'fE &|o*()(䅍 r{?>5l6ͭŝ b=Žl;; r>GY!Gpq[0X[ bP!?v|̆?m/Ody<ѥW!ƙh n `#mOm,k\hgt>''Ź:p`=~= Gs=_3&;;0Ǽ v{G#OSq"]]|g.L.mbX£/iW3&]ŹK.<П oW\Y#˷1WO~7gI_rWO~q7_btvGf^Q5+9{zp[ƏX@=wq/sho9S?`sO\'AZq'wXti|ᮽ6[~77'?c]|u6&>e)'Ƹ'|GW/gv>01qiY y偗!';0t[㛽gM8e.3}8M9⦉c1~gᚘZ_.Nl-aIg4ytAmsv l.?2 ,2 ,2 ,2nr˿.Hbt]6[vꜗ]8_lk<}L2Kljո.)ϋ]a,+|&7 ZZo:.ㆭKly>&WWO>,~X>s8 E.[cTM,E6m*>LtU@S,TQUSۀVSRt񵬽\A+U,#A= 0Qr,AF_N)>>UL_(-6ySDG*VP5|)vy[zVыx3q1 y DQ8jd笸}U4u:CSqN|M銕 iJO Tz*ZlW~o9Y"6.=2QEazoٴot{~ qz9na|Mǜ^篳Y3XÙ>;ˡ{Lr#ƞL,O[> <_>7Ϟ\frg |CSdbL}6}ML-Ng7s"MZM8Lwۼ|sOWȹ7na_il+~\/y ~xaG?ڟ^Osj9#VtQ5\0y *K>xcqd֙jg.b㇮p><8l;l>#0v߹x:ٽ޴P,vv~ S0{rMV3^cCΏZmGˡ*}~''0Y\YO7of6^N7uilfÚN9ӎ ~N-`_e`Xe`Xe`X-e\]ʝ}yy8/z} Dk]Npz?.xaZy'tx3b״R{ϼFKyYFٗK6Y/..NCŶuc8.M&VeWerBׅ{bNqʷⱧ.,p(+d( IDAT^{UN+LZVd_>= mNgA"ku4Ozk/8ş5yї3?rֳ (V>^~k ʭMWo|ʿ7g+qx`0۞)ʯ9%Ρ|#ll/<8}Ş^_k &Γ|5 ]o{zӃ[g^5; rY}&5W.LJ=v|͵wV?~~a~JWY!6r>`oܑcġޘ~TvkG#v|mγۋI^yҕ#x>?lt~߿Гy!/q/m 'zX kzvKӿetӚ> |7Jg^\hK8_8f^]t0mf}<% K=ԺH䵜tY聡"q2yUXQ|MJF1FQ FIWCoEIO7$ wo|TԀlYd;aH&Y05Wo]t%W0of/¹RQKnhh?fM> q5b}Ow.h9_z1[CnYΞ)xw.>ie@XO}*RßB(ņKo gƞVQfÇ'`,<0^ȿ3?[AFq(8U|䧳!~>|95ߚ59?>]2Oÿ7=}5'緳V.|[k]y܄ٜxhOوiY/G1gozc3>0g Oc6&3oL.1åg_|6y {{0cNM5L/__>/g헁-ͻ/2 ,2 ,2 ,qڼkN%伈s!8蟗z]žx]*u阍~>KGϋ^%=qOrOrav^hӋq9ei}wr}^d̋߉'?|t=r̓m{&my 1A's0EFwٕ7$aȏBYE:}E.},E dbkxoǍ|H"}LF\=T@DoU,$Pېݼ J7 ao~pP۝jg.))W$҇[bw~ۋW_B"[V=NFXVS٧okt3m]Y;]|c6cr\t|A?qY|kO;rMGO{w',3Z5> ?m/c- 2 ,2 ,2 ,2 ]u r^Zx]8v9/9/[]Jv!X?/9ecg.Oq2ξXCJ<.ga*cß+]KeM3|9<3Ξu|:;.7}\XKE}xkk3~r+)A*U၎5/k -z矿ʝl*F\!7*Ţa gg(YƧ1EƊbB~~Koo\EE N)"Hxӱs?||xba<-ouqPaOEY| nY0Sg姵 e}20toC}.2 ,2 ,2 ,] .${sK.kce^45oX]Jy8]@ {1}"2b䄯8Ȯ|g?1c:GzɺTo=ZOӆnإ|˱=I>8^-Ɠ0<8|O2 m.W<5[5SŎC8gU=m/ ӯx-ė헁e`3 ,2 ,2 ,2 ,o]͋se\0z~o?q]VZxS:S<]kZꓷ6}9/Yd3}?\m[ ٸ$/|ԙ]v+X(L3F&ǜO=>f_Z>OR|XsEWozl[.'Xtp<*&(Qio)P?<]䋩k|Dk^ޛr60uo{ h Uq W\(*DžXlz+56gMS1&_O|쩘k;a|ß5U<3~M ]cC_k.NupmZ!.>TE|nZU<19;k'oz [Ξ`֘<ơ7sRѲb8qnmg>2>7^*R#z69aK _<^L\14xq|W뷀qyq C/zɳ]\6r }W~7Ssy۸va9A8wgf\j~0cO|>sS'=~nۏ2,|7 ,2 ,2 ,2 \ĝ~]q \>GzbcyQ8N?s\>d#L]vkVnZ1N|\Mge/gKwʺ@qt\Yk٬KrKęߓ 'W{ƞ<39|拟.߹3N/)nO+ tf1)<)L)V_I␼ߚ=5~Fta&>"/׸ƁuzQNbȋnfnxW@^qo3ƟNwxwzZcM^kMΡ\+vr&sڃy}8,'z3kp?Z۱ΠF0.aG>~YC*FZ=qg3 }v}.,a&xXmc\W|*ۓoxyupgMП=8U$wVE1A_{Ϟ?ygqsxa^Y fg/=psOW{o Kk|#ygE}v{XqkM 쳱6Xl|wMy>C+te<[5ř>:|dNzT~ 'y\qX8Û6soS~)/~^Wm@rKw_:pXe`Xe`Xe`h3%ۼ" "n^"k`N/a']3q8N9.G;\#;c zb&+ǸN.K9/.T}kBֻ &>ڷيU?WGOod;9'˦*VT0R{᛭3+09px1Ht`ވˇ95y:O哎q=X=pA\k'>xd[ 3?r<ߌ&8=[Vnb1|+g~x"xw|ݦomVg+𗏾|t2 -iXe`Xe`Xe`Xw.n̋Byl<}pƚز) Hw+9YO忋8g]NOR6ˉ}{7f鋬s|_k%9z*F\nW/9/qS-fKSqHO<U*nUB"b /pt;op\3VLQ\!8rEbw6rwZa+rªGKV|SDzsњ2E>8K}E2_ {㰒<=8  sE/p$/{LWѿ8tWQM&G= u^yk[օ%ۊΠxhͺ0kҊB*yI`(`b|*q⒭=1dxo~,\fE_{Ϸ~:O?N*n _dLr)~p s#6aؿCi0sHt :}^ʷu8x4}<X&8od{c-2 ,2 ,2 ,l]t Erƭ˻y2>/ܸv^Xeˮ3c+pyӗ5/ns.ӣ|=e-. ]x0Mx3pNd∩sN!+X5s>䣑)r+Vla^"Yfn(TtytpfAK1ž30!wK*W &1V|T.b5YerO ȇ5xk})],:7S?o._oؒ+n-[Ěg/>sXQ<qVVę5<΋8 &Ft->|cαT+ܰ:(p:'r謊Q|~ԷB'lqwۙ!s+og_}%&Wo*[w֜wq/=߰kNr{Wo/K|oEN/2].igM٦O^ Wqx=oʧ–_2K}6:Cqx37Τpޘu~<Oxn^<~XtW:KWlgKn\e&sV}fϼҿ oZ&?DΜ=u)IV.7s~_?0[e`Xe`Xe`Xe`x t1n^wY\:.$.s|p9/ C13|%;/%*+q?;xp3>mMӝ0=sQNWvC.~Ep-<WN,嬟wC>/1P1J%+~/._ `+)沯dNŅ^o*BqS1V蠧)+0Wʇ 9Ɲ56ł?s:3fz j ~ \'/98VpCU @bj^1_XxyF1;c}Sfv'?y*?{-q?sn=طq|xMfkqWnaqD_M g}ך#>ing4=0}o/\ls'8׬ߛ< IDATKl~}6<+YfO3ڿ}w= '߉Ro_؋l8gzoyC'2{A3B扗`9?|k}Y^\|m%?|w鈫ŸFΎFl;׳ _5<ə ٵfgRc^8,FN]>a)VMoxx 3^}|v^e`Xe`Xe`X> tzm^љ'uhm7{:Jr-sϵKݖK.#( 7|t䥵"[S']+3.f;H]V'6.O\dOY71c8<+<S/|8Cҝ?OT߾{;z| #_Z2_W.cp4NF|To&εS2y ujC0;=2:r@~)v};߹LivL[z Oްf7ShOcvN_K/t1 ?N{45tq)r>7Θ}g>\N.BvL8pύoY?> 1qBW~?: ۰'Yb1;;-K޼~w&a8=Zg.8{8/qٟX.7-?s8r[z2Ź9g3uw w e`Xe`Xe`Xwɀ .2BK.]v:.qҳJor0]m- g>s㴙g>M|6q̸7c^NeG'3Nss|',ֺ7onrIƾ}3*o}S`p9ޜBHEb].qpF _ޢb- kw| pI?o)P%~+Ryk&Um[ b5ckp$paF+>#[S㫯+{CV,1z=(t9.-wQgG_ NJRo^Q.kzqʋoqil?{;/~__a{3|?9}5^ٖ4Ϙק{`:ٓ5v; |3di5=|&__☞w^MOȑO;8fak,F [XMWWʧ}H/>Okzls_Vtӏyvw }ra"onmƹզSf~M2}}m/2 ,2 ,2 ,wt1u.c*z.e28. n>&baNiOyƉW1 \egC&y6;d,N s*LyGɦmka杫bd;y%k?&>Og-: /*҅nX-SEFYELLs}}fC ~3W|STTԄg<)egL!JcΟ"rSw=a늨|*Ǖ?5>! |` !=ո0oy)k|jxˊUaQH֯U욷*Wylt.5YC~n=NO^r%_rŽ)ݏ}c.>ޢv$r'ոE]:#o CK̺_ coy9nSvWc| mj}+'0@_@|;ߞr!̿_,V?^ 13(&_~Yotz1pmLu}^% [Gk;GW>BRn=ۙx%]'KrCyfWNgrL?^k<7g֒O;^f y7e`Xe`Xe`Xe=0("5nɵy]6A8/tej:ŝ]BO{.A<]gG k9̡s5ۇl<C>'i-ݸ;]ksOØ]z|j''be6Ys).ċQl\}O5X'yga[aIQʚbX(2㊬@Ifh8ʯ1r+yR MG!zx*Ut\G [r2N8Y2EO}SWDdiƤ\3έY0F8O?:?ckO;\8POGa&^o/>5`kᄹNyi?Ι6>{[.1ZgO qB/xlu#oMW_}1Ć1}~>O_l{K)gkgw8 ps!ÀrU.)kSnrEP&nLz޿ I}\Gњ.͜~O6qMS0"[=Pd~5lYcK>˯>嗜bi&}>󋗹Ϗ>b,Vx3vCϺ؝Ecm>׋SX 3m2p蒰 ,2 ,2 ,2 ,a[l˺k^˺΋GyYyH6140<ŷV܉crg.]Н;9|&bқum>^ \ۥxys3&g^ĝ/"kq.%=WqAYh}W'|zQſV[y5,]sO>@G'Q\2%o ܣئ(sϽۺ˛^ gȑr1kb۾g.{ųz*zkRßVG{QpcoI91 d U@[EU8M&V猞y>i Oia' {rQSp>)j۾ھ_J7M,:z-̚#l^^NUVOgg3b7^[<2KW^y +<ŗ}wtVo|qs`"hț~GWƞs|;Ǘ;Otضa3?>j/.{b}(l`I u>ɣ}`Noks:ag}Y6 ]c1#^wcq:sI_68eNɕuw ]ٸ87>3;yx<l6Sқe~~O/2 ,2 ,2 ,w.Ẁbd^v7/k^zN.f8/ !?sYa4͸ؤ[9EG1_ 5)԰^Q5|v`M;'b*lM,Vlr)h) EFQΛpxP1NaGc_AF 7Y[b¦wNaMcr8EB2sE1_na/Fx'~Wv2#&X{3}GL?M1) +V|'6q+{lܙMip쐩x^,Ykqypl6'2X'_=aO#o9:/8To^Y lEQ~vFM>,:>*Cƽ7:(5 0+,{&Ga֜_pOγ|yFQ!>ٚ?;+擟{YڞteM+1y4G&4Cs:ve7_`sԷ569׾8)=ylɷ6&sM~r`~7?|5;Xn`|o ,2 ,2 ,2 _ D vl@l,)i*Q'+jL|~K n\ [ssa Oh6?c?0NמtO߸*_kri~WONqКgGcra&[Q8i]k%S>KzCϊ ; d53 3[\ F <;  FgBqDcÆKΛe=ϛ}ӟk| l@FP O 8U8XgF_پ%}}/x"T[ I'=x#^gHNKct%{_kV`b)_BY\p\q߹F"XfzO_y#U@X\΄y}w&N ;]&i^bJ&q{Wfٳ*β<|?a}3ypD>ۃ@|grp[ona*ʶ5k_.`Xe`Xe`Xe`XbΉKĕLdqr9*G>Jdv?mΤ\vg5?gٛxf$8O%Ogu:+\1MLdӛ'=ْ VOSs.8K>T/@?daܼ7W̚uW39xēn܈?# _gݽ1̓_+)=+4.sLеQf/y>Y#le:6,Ί~ fvHo&;sɇbw{ {t^g;8ޤStj΅Sr|N_d;ɺFsy5߳4%o.}xOnA6]x<=Θ̚L9˖MXrb&zcxnM7[h|s;'2a9r?ײUvwnj׋ ,2 ,2 ,C{ދzZdᜟ<%fp&USt&gJ {Dc1bvɞ/ϙ68Z ɆU|OtJ|V+·6(l8`km^|YK$o<[5ה O.?|d|MŦ9赧}ų/!H[iǿ9&t+YH"0W־3f5+\EZPE<@_lG>`WݙǼ6oÏg$< s^Ϫ(&Wr y}U~g[n9VEb~6ZbO_s-g_u6lj8 poo+5t0Id5?Z1lN|nuM:ǧ1:{q9ɸٲ4W,őwtg q?zg7qfG6y&%d;pq8Ú|l \:e`Xe`Xe`XB$-דɐ9UbQ?%'3p'7me#yk%7D IkqIڙlÞ3ҙGkư3@}gM?Eҏ~GA-_{~OY|g!= lkKE8%|*Џ+z}◾{o΅~{@_ӳϦwx7Q;[ l_}(=l);+~kar/s֝ӛne{ƘMio?B7^>s IDAT1q)^1vx?'>GX!..j5=91vBOy=Cltg=Y6#os ~SnlkaaCZg{6 wvL3b=Ü{}>aڴ;Xze#$?mY;"2p2w7e`Xe`Xe`AدA6L+!Zr|?w%bAO0~&ġdLgS]%b'>g(KSvUZX&6>L_g>ݳi심ĺqG|'GZ2}q`-~ؙIi7~FጻWvs *f̵wU|}݇b^;/ D_3=E*EϊIc;.[G6=];*F2Lt5Eᇍ>q< a+XW {\k%]6+&W|3WTl, ^$gTUgVώxpg.[Ϙ7jq v{o{'oϬ;B|o)׳e/g7iȷFnωyy-xmz̛se+*~|[2ҁS<\ 0Z3G&ۧ 6ɴm5{6K\g=,%>c}g%u~HXÒ8t&wzX9i0pxH2]l/)2cpvfLsZ'^i#/7*_dQ߸e`Xe`Xe`د~o[֋eG!F$k_. Ǜy'~'U8xի^u$gЇ>tH?S$LI%ɗ3&W𜌜vdJȚfp✾lsS7ٟ Fr%$z\L6ܼZ+ZxWfgv:e9JQv%ənK6}ئb\y|MLה~:wt pϝτ;m0UeXk¬#o_C0?>s9! owg\礸U̵=w]츌h=eg!>7dy|5:a4G= 5:o=5rqNg6,x5;Xl^uJ ,2 ,2 ,2f@rεˀ?+A*Ծux_wH()~#x|Gկ~5{/yKf^ş^&KIi%Jv<'ϲτ+XrOT^~4~Igb<˓ W>gW3_֦l3a|$J-n& fgmd+{t_Rv}>wMgK68ޒ8 gI谕w?0NxIvblUlF<dm\$ V,b->Pq%{X'?y|d~a4\٨6yɊb>替&[̖9f|s?=g\.]&r.V\69xݘBt5r0şa\ |j"(qgOxԧ^|7Q`u +W]Nma'.kaqsyd;Kl?g3+ECc6]8f*Ga??cӶVlW!?yϻƕu{wG^d=|9ּUz|.[y\{<>gc>ط8w?vO8,,q=MN\':كaΑ o>l[nk S129vkJ;Xn`+oЗe`Xe`Xe``??xẪIlů^!GB6|+/>oy~!/}K/~~x oxQ?#z뭷^y%0<؜dLKg&J[+7lJ"ftdiZWܗ<'+c􊽵s J櫞;q:%̌k7cWs\&ws`|UhF]3|LN2!?<;%ӳo%؛6Hjǝ+} O3v[gs& kgsS15Vn q+ Q4G<s޲ӛ ~꧎b|5+o|'Sχkc |Wg*xeK/ya<ýX<6=Y6\|OEgV6x[w\p۵|kxg Y ŏy]/Z'GO go϶;Y\3^vi|0xcGOG U,ήyZPϗ?v12޺{sH^gW*-y&p⧳Ϳ} <+ x7st{N2jpwp).gg<#?ecuogGlllt󭏛dg}配|$F|8:׾x{9aW["[M_dIѓ[v/vīMŤM~s5&G&Yײol-9}2h)?풝_%W@+y8X,2 ,2 ,2 | د\5X;)z_s%Ƽ8yđI.zcKy,IX5%-yJdݔF32}db|v'-_}v`*q=k͝&S;mL8Wy#∜Dm+?%X%}؞eo-}>ӝq-,qi/_d \L㪵i'[ɫfƧH&?Ukaד Co*fGA!ɛӥhW\Wh(F}!qX,p)bh0e;t>M^ltlYL\1u:'g/y3N{=X_{Œ=gEc؃r~`zs'˾ a3qn ^-tN/|,~e|{l"{狎X:;֓7oR'}i/NOM/gO\|mj6'8&OkzM\i\LΦ³>=­[ު"??\|<).a#sA2lX Y]R6J$K앴\;c&ogEdj:3i8}姹bp/g Ot %*ܗMzfdss6Z~z"n&&Wm$= [ƓUoza-m>urM.[+ܙ3}>fSQóFŒɻ9Er.*>tئs.9%,W*tt),)*[-&Z)N)PZEg*oWZgŇ&^/ 9ɆEw7amTŴYO>;ڟۣ dĿxĿo\ kͼfϷQzgƞ'l'>:]E. q{cS,ڃzk=g;#7=1׹o[cv:ׂkEl%x}2 wR{:a߼C_(bESLl^6sldNe^~{^JGcW!{9gzo$M7(KWbloA䀹FPM\un_do= ¸n0Lv>~Εs8?=D,y9Le_3q)x%ŞOz|M :;֋E/f|`u?aokgglXq7}cOil}v[ǔ9nj;{1ۖ#\m:5 ,2 ,2 ,Õ ᒘjo㖌l}2_xySyoiv3yLUa9 /89Sxd LvfBsfo6%n<'=M>Vq=yDl}1Wr"E|UX*ns3>kgL%7?%7%G/Vo͜q6ꋃ-slYOƺƒ\T"*b1nHN=qL_ᗼ1\Œ?UHld CoJƱޙ;6<.f\E chza}gW'YM L+¸ _sR1s%{{lџER/s|[.:p/͛#ˇ;Gi<p3{z{d0[϶9c>y OaOSq<>|>9Ϲx3y~xU1q{ܵߧ>4u<[]el9;gxwGk/[qbjcS#՞8璯tQ{#rBҾM.W:=Cv1 i_8XMlβ;Ɉ;d§͚9vV|u^6>8O|e 9s>&g+>槝0cƐO|7&c>)fzs瞒y;8qxQ2 ,2 ,2 0 Hm{2p7EV۾??{=H}xֳu̽-o9e~+8__9%7_r-G+i3XcIKewڟX |sKθ;ᙼ-:MvZW_–c/zgrո$wXǙ 5@ |7q7W\69j%_a.#?وxeW#そ䳥s=>/!wsم܇bߺ6bu}=qo+MGSmXKKy8*(UJ,W1[OE~8 ca'ـ g~<*%xf۟Le[1a&~EI `5z٩'W`+{ z{xB6q O,{e.i~>qй&O]XpG`_o(J:>\<5L3C} clξospř\izNkLϽuو>y7癫'?(ץEG8˻ceyλu\jO8;-|xs-9?d;,asAΘɹKK6םlg/zpA9u~kgs|;yW|X+eG|'N#r\v=8\aV||Y񲾽}O6w.8K*.l&Y1( Vx|ᳯnO}1YLŊ`.:Z+fM6ߝSxkQĶo3O_⃎qv1\&d+P O[k ήy=4l/bp oF08/Ok}8ax*z;yY|lGs?YN̋5v]qYg[}s?k|ԊOM\qq\kn>w^'Ϲ5yևhk^ GZq'ۇ˖_c1fcW-õgaHoڞ5qyܷqu0we1H?[[nGa -2 ,2 ,2d`/"|\ww'/~HZկ>8~#]{p}H؏^kozӛ.~~xKX1൯}a+m%fB9tJ/7gK(*931 %KZև1og'C뒴B] IDAT0L,>zvճowrz2FJOȵޔF/L_XObdW+ncv'9v#S3Mz% Us)l)86+kŠm1 gGQg |]>pu]Lq|1 ٫M [Zņ鯂*F4+ܓa"VOgjCE>rlXWut bB{lSsـgV كs9<;ϼo|EM>l {F.vN3_Osqk &U5=Q: XAaRhل >O~읽z}۷yÿyNg][吉xt&ګ,{ Oxµ}r?9=|I?4xc-oɑ^hl:r.s)^S0G ;IϦ= xٳHF{|>o0le^vs~A8~gLgg9mfd\8~fq>sdj-.7Nvw]=ss1<'bI?ZE%s.s VP P`Ƈ^79~K՜[dhgEG1b#G?ыooV@P4Ca=T|"7 apJ^▘]|C{7^䊍^1*@jlhv;|T"/62&.rq%{7K9\Ll3~UH}Ӟv@9Ź[Ϋ5@?kiox"O?\ppC^ow>ūG.]slkɶCs=G[pC̟ԧ.Vә9?c0/N|{n;y}{ƭw&ޞ]5K.~q֞a`Ϋ 6;z,<=#oOX e3O&yxf\ݷ'wxӹ亇%S3qYoǓq<'2m~6K?[L欷sb`e??:Pz]_2 ,2 ,2 ,.۶ \Of]̵l3ɖ_Il6$,KN< C:%80LIg3|䣄aiw&g'GٜOj.y q> 3⣹8p϶]u:Gbk{"ˎSl̟ŏ?T@ cZ$gdYeN1Q&8U1-Xo{ Τu#y:Z'ϗg}ĿG*dfy=\Qq"~;rL*i~9A۸_uO}-1j qf-;wg ?l=k٫h۾PX?]kgv{^|c˗K.z#޹lOxwqs1lt٘{D6..Y痝qCf'Kѽ}dit5xٳ>qg.[q5f^'llAXL>o|SLֳ(,8؎ -aXe`Xe`Xop+7^ 7 ss&JOfx&7g~|qPt/yng8W%Q?3|6wndfdnKO?KdӼ:1;N[vXg;l{܇%NvfQb9[3pɾĴ~䣽p`.*yI|-<1{龳\mEkTRsM7]}݇n|G|X|ƭEl'.ڴG|V1o~ɱɞqV̑vMQۤĜO_q7~4Mva!#dӅCQVvy.*D|Nvf:ɴۓ0o}]ӥ~i&Lr=“>0}ebg>󙣀[f5|ir4Eo>og}f´B{g58"c͸%a=?;X;3^\}csCUbjN]:w/6+y8yV}W;gl]lp7Y-^|wA{|g 9iͼ~1yx+o`~u^=0%cwg\kcxٙ2fv5'xǜoM?#}eQ2 ,2 ,2 |c1 qm Hir3xI>m&!M~&1d?P|U s2vxf²De)W,3L;3|n,.~3Lf?{vG>''L^Wleٚrԟ1қ k]t`ň۴l1۳RvTA=upg%/yɁQ;[%(k}[D1*(eȗM?Y*=S;on[dK1Xs_$گCuN9gϜK|)x|]\^qͿ*GNpOf5ėWحP7(! ++>O=~>}  t<.Y}a(&/}{i_@L~8>s~Np {8`қR1RO#ȵ4|bǓ{Exa?R`ۥe9ÃxzSۺwg PXL=;lXض |-}-l?6P>e`Xe`Xe`Xo JͤJ$^IomQdeɖ؛fb3y;ߧNQ3{~7hY~Ɩ3q_LܶtVZ{N&\Irc696?4]ݟp?6_|;.K5;fr}&&ҟuW/'UU+x}j((n]o*&))4yQ<ܳoE oOXq}oΌByW37RaNX*g\Fъ[o(ѧWL;kZRNX)źG?Go'g3xBb^M͏f .aGźqwķ?Pt{/9ٱ#;ΧK#pY;y!K72>b' o'WTok竞"7mb&o^I|zkOݓ6(af3ɗy8kpƝ7Z0S7}:TqWƮɵ!3 {6s]v`?0?'>c#;W 4=-3/dge`Xe`Xe`XeA(wUr^kݟ5VIϙ#_bx1?%y7v'o LJ󌋿{XΉВXȇ<# 1u K{䨘%+.M;g3Qga%/tϏys%g?sWƒ0ƥ'{;^A+dʎm5sj|qk t߼y'8}~LZMqO+</6?M79?<чI&~2&3p[Wl5xQpWīu6١;q.`ᗜ9Z79`}iO*":O|ǛT6->e%jyz.z=dYߛ_|JVvOާ.a7y<7iC Y{r76g~ϟ9TX` 6YcYW/nsFeo:d=v]fLz'Uӏ3εl=،3G&>pesf=g#/w{m{}p?7˟qkwϾܻҡ8CO|ܘk-%9kcXfodiΙO-M_͘\N8n[Zg`ebe`Xe`Xe`Xea@ҭ\ɾq%GJI>:sy 9Uxl<7=LC~R?g\Jb ى}ٴg?q;%S-0^eg\k.J5Y :oti;pg9:3泍orYW_yvW,)9.~Gc {(W((Wϊ^';|(`)ȣxcͰ3 yz2V1}2@}E>~]h |GNc'zE!3t/9+NK,[no^gngzW=&qNឺuF!7a"9[׾YvvOcxⱞܜϴvvۖe`` {e`Xe`Xe`Xe@ɶzdshL6|M3=f1 5>/i@XI6%0Ý? 0LL3gO'b; Όx&lU1 w1Dw4w>e/ پ.!'o(*WNoz[ ՛nOfljOT}B9~rI:VXzDa/^QLufWOWi+*<҅C!k>B |)sW10\ŮH8;Ob+ _ٜ nXy g>_AXq1YY spاxSlFϺ> y\8w⠂{X7:]/ |aU7ּ)mhO!_q_+TgMϥ=kqqgUW ?k;olXã^ cʜ8ϧ3l~~vnElq>aOܒ{ppnEjL?sv\NŔovnҝ/a}mޛϗu:S3\2}k)þ$ m>o55MMq%ge`"{e`Xe`Xe`Xe+b9IH v_|Rx'?WaMe:i1ϤdILqLl~OM8f:泉θc?ll3Z+!/-Yϖd{^}ܟ{rSb;zS';%Iw?cs/[9+vcaMV"Ž"Kv c$Lcb\ D 9U\q( `oeVW( #ۙ-^( }.r+Bä0 5Hjv*˜|Um[T4Csteg {M-\]ƺyv/[tDlfVW&gzROyS~3NEaJ {0}{cG8kk_(cg:kjx/zSL⹬y>;GϽ}gXk;G[wYbղil-nڏsf>țsNx{K7MNslk#=sQlcb|#O7{N;dzҍyĽ{kwv2p=p>m [2 ,2 ,2 ,׉z}Iٟ'%KY/yO]3aLhUg"1WlnHwHХ{i5{r29ysOg7scv%'Q[Q@@M2[$&?޻5k<9k..J| f vXkrTZₘ5>Ms|o[Xq擮5zq𢘤ઘZX1j:᳇íǾxP1LE[ހw(,mGvF6v0Ū`d|sqɈnX,.=` S֬)dQQ3o=+}w髣ZqDd{̾1;kʵ'ql1=w:lc[ox{{c^qU'\g=[?>P}c{poSwEz<ޣ?Zp?+['2 ;q%5v,] (å(l)d8b9d%coUӵ`wرΖF7L zֳy>^*!?`3dZ4g=qKqugw p3bν^gll7=^9c;'?.֊''qgF{}_ksbinb י37}5=3g3_nD|#Ƽ ,2 ,2 ,2 | ̤99XD]Id\I9WZJԳ_,GwV8 CxJJ',s~x9)wIP]8l3}/ǫKzi1*eSR"I i^pO͸&GOUq>43 f]A] +xi" 'b'tacw|(|ZWTDWEm=\ハ/~78= LqYo mWgQ'^ IDATߊ[x3n?R-V\P[EO>K&zÛ=Cl87Z7O=q9 yl^xWxg1ό{g@co?ⴿa~g2p ;t_?-HeM_|>7`Zk8{֋} aϸgL+>OxD}ks3kx?7+f瑌1d9j{Wg)`(8鼐an)IXs!~ZgX|X1Ya@3KLV כg=sY"# 83ƧuN_jLglYk =|.ɦ`woΘ|o5WkwFuUݜX@Ƽ~o~grc,7;dlʷ>1$>4OXkx_ZGr#y2 ,2 ,2 ,2u`3IZ`H)YWy?}NH0;)ǖ{q}%KBggbm.^|vpgONg-Qo@1U^%j%ۓGr%'/؞2 {և!=X@^@^"?<>α֙qē|ͼ %-S<⠧HVbLq+(b)WRQտNpb' Ylo^;YoV2oL7P+WxU&zi++ģlglIs䟬5Udy;xl_*}= }(~7oo䞵 V×Uyㅽ\l&o*Ysݞ+t>×-z͑e+~Sg2p#3y7e`Xe`Xe`XeĠD97&kdJ'nW%&kcrf sqbLlb(qT"vn|]%u pUOoBm5k!ȵ7Śn~ƣbEg~r#/|)n\Of|Ev‘ k_ 7–77PP3XP{7_(HGF{k؄{qpI]_ac^1Ӛ IkVk0?CßWthխx s܃O~l[@'oy[.?(<* )3wV3̋~'U7yٽ"X3ǿapM7t`VX96O(,I7K?):gOcgqUsE{›tf\xSC>g =a+{@}͘[3qgOcerg5v㱸ڟw#2n=i9g3Mι7oq7+ÌiXfֳMsvV1ܧM5cqz|kqǻҾo{>r8O.S9 $fO̭6߹?[3ݜ=x y-Ʉqrj.?'bϽkb=ǝm茄lxӔnئ9oؗe`Xe`Xe`Xmu_MF%gěKg7 8݇qL`f|Oi$boy} sX›?se-K&lӮ$zEE+v+1q30sO|(Ǔ>!+lLqsHfL7.V?%W"J+~W󶨱"Rq:^otyەla{; }-6ȇQIflo\ezzlSsbj@;cai v׹ WmAEQ ZArdI" *ZzQt=qG?#kXHng{ngq*Ћw12n zqiM=e͗o;o~kZaј}v*v3S_|]w+ rvyۏ.'mJLϸc^\:|_|>[goʱ͙B8 s5Xjor>^\vmFl87 oXcsb.Ng\qd=^Ȉz1[xzewcd,lK^K֘{2솧9Z}!t٬ͱ{s-,=W)j/wF2~qmJx5f/ -.݉1]-Ͷe`e`Xe`Xe`Xe`(IWm&;Kfp&%;glaθ$U6ؚ|=Kds/a?')ח:ki]:Z r(Y}qD\\χBGO&L; Ndfpcs9~U}!g~O/pȉ=gA_+bWb/xat̐׾\:x<ڴS}g+GXՙ%452b/v'^E&9>}? |1du-r׺5Ɓgn\tlfly1+Nkbs8c,eãv:su2&t”_yb`e󡴳 ,2 ,2 ,2 |00q%+WRUa䞄h9'[I8<~l~KLfwb9۟O{5N@  ƄĘشiφ' 5{;x sS坷? ֜܍51~lXgE ޚ5{ɱg~k?gc-߫$.t~kyJtب9(<;q=*],۸w-Z>lϸ99=lv Q_~_ao$٭` ۊbXo]{p+$GaV+\XsGE=3*$+L=2eۣu;k Xau:}0 |鼐e3lsF3KG7<QO:r /&6w~W >l.N}ـs [?ao}]cZgBs[a-mr叏 /Lt{ʻݳgk}Z_.K'rjc\o\|\ز?=ro^C{pgz;icܳonSgpx8g8'a0p8 a0p8 |/ tHKB."Kd^B&|/E%bڋ0ig6#>㍧.[ǰ?L_Nn&^VNa:D6?l/z:‚|ݹf#tDD[ݍ5c[Ț/)|¬/r/'z[V}oj C &_ׯUW* |xWoU`ǫIWL X :iu;֌go\|ٷ-O/]Lkb?xt$F?>++rP$UdV|v+7WHJm,aGowy_xt΋0u?pG8 Yd׸uy[N9{b t^xiq^L=a}xa@#C%˯π<#k=y}\_El/]~~'>'7u#qG\t.oe[y++8ė[ '9ϏZ+{x}9Or +b692–_̖ߛ C;ŗM Z[m҃oVorte9.g{k[^t򑟸x̷5Lyn̝iW=S}a0p8 aG.D~^m],ZByzK.Ò߽ \]\knލaŴcvⱽpg/_^bm,kLO";d5&*[xӭHpG698+gqK1;k.o7.9bc48g_?Ez=6 ;IS`Em"л SE`vUhWH_W_/]QB{{7~}8°X~``>Mj*hYd7^:x!#~&pӓNj~|3O@ *pG/ Γ.y~>~C?. 38'/|3ʯ'ّ7j?rAς'ye^8izEYg??HoN9xƽ¼π?xy=|[cslGj,Vv鬍^n< #_;p[C/c9_~'90^;d+Y\sx2ٳsp懁gS~b? a0p8 @]fuQ]%B%]ѕ[;Ώ0/[G}v6^=MY.wMc>|o̎0cfw~E;0Wߘqy9H~Z3ӅAyawrl/ǽБt+2ťtÙ| s^|>^M׿*)<<'oo]X1)*ѹc|YWn+\ⵂb>9ak \/rqg՞>˓ asg@|;rf\l>'RT+bSgy _g+_ӷ٨{p_8!_E ֊ VV1s>a7!NWDVpl3u-ba=v;Oo_C{wx箯ck -d>\oo*Vv=;3!s_۳n1e\W+߭u&g񞝰b[姽p{1xɬ|=2y0>b+?pe{a#p3N7-bhmw02p jO܇a0p8 a0p蒎^^u^6vy.}l%o%j_/`X]߾ű|$w]lqO{.OSk+zegmyS8j%.î(gBI\/o ]/⍟|l)>6#>YM>ɱh -Y uxnGvR@f"o2t7__@ Co*/6,T(SDc_MQ7a“GXɲ,8:7s1Iڣ_N9k0k,ߊ}pq\Nج`/b矾 毷_qo*r] ޮ296;7 l3P$.噝bI=S#^{lz\#0<n}s-W`tfK9rV6v+%/tU|MwwγMozӥ?c甾x)"Iٳ/6!^IL ggsV1^X1ƏWêk᫢7o1TǭϪؼi "0[E^[>~jƸZ _y Oӱ37aYdŬa0p8 a0}0%crw]/V$%o2ͻ4f]_wd\veu1Y9n9OyG1o}۪H9{1/qE鰣W|yEbBUEK1z^E!co**))&))ё;a1+()cSrUTTc+m n|Ň1َor|x:ۓRέǛ5Y3z6b[bď"^8[dM{ 3{a!99 ˶Y\ZSE'2 g"gX 7qϊ}TEK`TWd5:d؏s|Gh^>gMx[o'iُC=_}6a7E}|C\Z`uF:x 7_gǿb/?~y.v~|7^o@g>l>q[guY63ќ6g'^짟=}6&mxs {唬>{\,ݘʑ~6~:?lzbY{9;|r~G6v1g00F"{ La0p8 a0pa ۥ뒰RwixlNg/$u.g^,!ŒX{i%g?,N>6v?=8xX;~<.9]+^Vayl./@ضX,M>aw=7f>A'ztB| r| 7ٍ#/1}co\ A/cmhyXVNθ[- lzs_򅅍nZS0T@AG pp /<}7;=}=כԧ稳"7bMh~b7wNsg=1ab|y ߞ_ o7>DYsBg:=q/Δ5>c\X~aCO+{>U~S`/E€{5|wv&cvu6M-xI>_S¦gp~Zz\-Nue8^_WXGgrx8g8'a0p8 a0p8 |/ t{z8 ^&% .NxYW}rz;'?؅fka{\ſž|:WYܭ?r]+(Ex6|[ i(AnK&d$ֳLvpOHi;7v UAC!@~0W\#bbH1Ma?K@<=XUaJq۸x-gaTp?Wд(tvyG]:|EKs:wlx4tKN{ěu6ŧ0.~;a=xxd՜<]M<-cy&g~ڇ?-y1ѓOvW߹Z߮Gey6'LqfY>YlǷ5żs|}[\ճu5ݻ\>^ ? | ^m0N}8 a0p8 ar~hL\z\8f7{azkdӱpuz]\v^8b_HetxKpLrܭLw'ɮl^XP8+{"+w嵱0Mvz[}ܿ};b{'sm<4cO0Ewsa3l?lY1ol^kp9}-+wm׾y|ė}6_9.﮿9,vS:v=kv8 mg=N02p hO؇a0p8 a0p^±K.?Rﱋ]vv!Ylw^󱗌 ._quŗ f{ZegcHO\ٍiw|0M /(;:LyJݏ Vko[+8谟~+ht>& cD_Azxf6M@eۑ^K mQZE(>⪼@_rqhߞ\ŮRT2WxSTS,b=46`ーƵuECי-o'l9Vպyyqܽ8~UT.|ég Wx7Wya}ywؐ+d[оG7𧈫@o8v&0?P\}5x}7*hdm>*>⅞߻/ pTmg}~ؒO~M2V7R_|#}o5r_gwn./5k/Gc#||kMsOyc-sԧh$c-4?N2)wv/Y g;֭iqaw{dh}meü=z-X֞:3)?8 a0p8 l]ȹ˻Kýs/\ivaZ6('9ƕ/5;=d/;YۮڸnOw؎;޵18_ödŲ[K|{^]t .󛝵 bZ>ӽT``'ʧ9WhGZ.dM:lMFE.v5Ҋgb^βNފϾ |(RY"2\bbFqa {ɛgy%ozfNx=8 d'׳8̽AWhGXOo;cۃ _zC9->{*(*EZG>;~sW\>ctW0G̞8/?y;lwfpqX[_9kaߛ԰E>#ʎM`tNIme'Lk!:؃3I@>oo3^v2;;?l!<{n6¿L3[\IOXشN.d~1_Xk7Lw iOG|wmaa~?vPb9 a0p8  ] g/Fvqy5K׽Գ\{ɸ4f/w bI;V J>?"4[C2X=lq9~2٭_fbuaŹ5r}XN.٧ƾ74zL|:qXymۧ^;b~s׹n.\oVa߼” Q| YP_݊aa.ۛuTRf/LQ$ t+NlT=|38'9lO{Gjx5(0>PTFhV+BV?sluƭk}nF/ql̟]΅sc_k)Uv2}%2;(|!LNx?FO+7qo:q,֝6"*x_SY}Z?,):p:?x`㻷˕ω3N&~Tk]VSnҩ٧cÛNqu;}6bL/ɆZ6.i’1teσy2/olur~O'5_ 1>—\۳YL/Uecc)eNY0p8 a0p8 {9x]v9NK~w%3lt؅^&݋tLJ,|5|߹8&ocG>rPŮw_ yz~Ol(ZxnqV [dG|4O'>~p^k/?鬮1| 5ld̬ϸeӾ֋)?==>f-L BF K_ׯyomڇ h_QJAF+~bO+֍VRHc7 Wo#=asrt5mKٯ0L@v\chܐA,a$0Wd8k]-KcrHN{t^ܳp/l** ¾s)җ{_s?\o%]8Ї5bo/xjߦ#&~#x NJʊ׾/m,7Ϊ||esGg<]?oqcO0( {dq'^`xo[;7 g}؁mϣY;>Md#;zkZc{? p~盾}ȶN\ƻ/>tzǝ~}g;k7b_[~mmY C68iz:־qqܜb1rs '|a0p8 a0p8 ]uI{I. ^Nb!d fa9-Ū׿o*JhW8{┟lx|1Y>!\d)!+|F1W-o.&64rZ;پFg]\ٶfW5lzS՛h}8&Vr,ËakM_|rkjaQc{ZQ*]yPeNg.+ngGi}sF>~}zQ||+_\\}vL NW`}X;O}w)1 g6鑵o/o=}pF+ֵ5ӏ۸ky7g.g䚋sC+:/גϿZ\>ɷÑ-ąƚokų1aaG\{8 a0p8 g.ۺ3ߋ[УG$6{-Nnæp rv"pVn<_ΐ];nXBb\o`V<ܼ÷&w!W|I.pſ0V'> ;o xȷXaҜ7yE+bbC6.Y_/~ɧ? [Ŭdy Wo#rgJ/΀V.YC_1Ϻrk}fh| YQaݜ. }U_g#+볱:zvV7wz/Źq4֯l ŹV7Vf]z..r=zz r>U%oMc‵]O6{NҍCӵ5?GWֲ {dTHޚbrfcr#<, XCx![a{`}K3Ww LqA.]{ӋsT*0.gXg|şH^O[wY~.g^~)8 a0p8 @n]z^uaxLg/,]4lm/[[_ .[՛UHU#6EQѩ^ǻ=pl 'PeU/qK\ܱY0 O sq,KƘ/_5 8W1z17les.1lh?kpѕfg׾l΃ 2>nKo__ֺ1,뻽>kye߹!S̍=엿b/~7n-z!UL(ot¸gVW {~i ?ۧY\[qYPK߼_~V 6p;)?Y?1a0p8 a`:&\j] vix7:9:K|ec!8=k[ K>ڳ^ah{p፟in_FWWqF1N1[ln-'YҸ%ZbW&OH'&{) *hzh}îf&ZNtTk{鲳d<κ"-9_ዷpg!.8};Ї'P3KHf_\'6{ ߎ}wѹ?ʑo> o"Wr^p rg +9rr-ʃ}rI#Z[3 pN };;k3w6SqG,{%oݓ}{ed>|oٵWw^^d/^xZ[Ͽ3%]/5$rˏ5-dmsu׶l-dp {uNY?'a0p8 a0p8 | t˸`.{6~:^ L<]|.p;ٝq-5q-.G:ke70ZONí__>c|xLvY~7z #]j?5c1^,_;_֖y\<CGG6yU1ͣ賹b~\/rVϺ—7 *U_]E\6HV^ؤ)&_aϞV,~EaT(xxUr\opx&3ek?*e/[rH^q7]{.gL<3A[7/]\7WM7` ŋ1]E~Mg~{vg__ѓEh=;}`m-{_t,o)>}6Zr0Wu|U;[ygY!S\f9×,dWld'jo*f}C6/wt1t[ph^ Z9Xx _lv>*v|[ߺ~cכ l vŮ+*7'[k]!S#E܊L~~S.+>3QuG6Toc+g!~+-[Iv*&{[/^˾Lx&WE_sؾ X+DulL\[R|T3YEQWklKqӕ q? =cCNu^ሷ> 5czgK|+iɇuzO}]|jvHb|ob.~ljq;<59)~r|:;|a#K/=_<-ұ־yNoNl-5gX7ή>|zp.{+|M6;j?.g}7'ۼ>o=a0Ca0p8 a0p8 .$.q/Rp/c'_t7.tI Ž].wIKwh-Ok3>d C1YO|l^Knl!%?$qe?nmb =]ɓ2z$:qÀa]_? }/}2=4+)_6aWUqW7F󣰧{8tg^~şo2O?ؼ "׿Ư9}1yXoHzuj 1| ot_XBe**:?Ɖ Cu˯7nBg7n?_oO}SWJ"zklaMѺ])hW:sFwlX >p!-,xˁ8ʧ}lo.rִNu=)@.,w?t_|g]2Ƹ1f[OʗkaO>[eq\g9∋lx׷5-b3Nɦ2a`N.ִi -_K9mŔ_{res&}+-bׇkH>+,to_f=8]/ ͊sTCķX1>aI<"_>n׾v^+x/rJ^b{^wŪ.[dW0JN^\omh\=:9;V+ۛž'nӇ/wxz?a˼#}&;<)kλ 8^>/CF<_ɶ_<K<1k_ګ-_|((MN]}#x o)Ght7 ]8z[0ϯ59S9OJ-ܓÞW<&v|m}Z?]83)?8 a0p8 q]E;{)Ea~#w.}9k7<^n,{_\87q?s|d)0Tt=>6 ^bx9 ~sr9/[]F ɗ5 z۷²1'>G݊.zݚ{T/?ܞTms9L?z"+Pa1𣘤X^VG>X>Z aBU<8=vqz[\rtaRtWHu6aQEw] {SXи &pXg߳Do̲%:g|[aкB9b|St//]kwݗ"1\x=n9V~u= :<[  lbvY|Ѳz~y8)>_C>ᣯ~oٳߞۘm<fУ/ΟYlܘMx?_ua=y4\YQZcd<.¼2/tl4:)&r\l ku*8f]! ٦_|\m-;q+Oltio︛oo\쉳Xr>MhLJa0p8 a0pe`/\E.υ"o/ۯ]b^&ꬭJfb1EA>v{nYk=[{yʟy#[|l8 GAbV𘓃8'/b܅5}a}].;ſ7$o[<΁d+"!0#]2&6o6z9|bGXSB,zgg=y[ΰW.W?+Wca̧yw384yf2Q|f"X*`[8/>b ]{p!ܛg }73O硜[c?.+Zy[׊|//zhpf&7~Zv(b?-p[bNn{ɶ흳iqC~{l+E&NW?L73[Ř䲳-wd< e.%tyfgyuV]}`_+\v޶|\Y 򥱣2=a Lkʯʥp??-ħKNj%sh\:N;/Sr|h1I&\O쭝_yrkb`5xh-r2,g~8 a0p8 ."K=fK.ۿ_e^~r,܋I:]8%DN'゗cen'cqux}8 b^Ύ7ֽ_xk˃'z qXg'^bsN}c4^;)~8ON6d1;bH홌GztW#/6_UW+N*6֡ᝣQP0Na1dhE`x>;I֣Ys((**ߛlҚa`ܖe*U/OՋ}v灞u?rW!W- N}V|7aCZKn^ظ3k?.o295sgAΞ 7~[L˥Msӷ.HP^o9ܖwv` '8(6cq-{in9csTC+vlWQ]Ζ:lN[`߳퍙kGvg3Y]G{RP|QULˣ wr%oaXtVPX.͇V%lCn6FX+bRO1Cg?;Ϋ`*?\YE&m=S,X'w`WS gWhv̲'>7LɟKfEm>fn\AW%O*wNP,q[W_7yj.;:%O/; <(mckdp_1= &-^6k2}x}B,_fY_WH+wŪP9Ǘ9+t?rn:_W˃ƞ={YP<x+}&co{Ӎ?جiw˼yaK9KNal3;,<) {@Z`ho:M KY`lo, %\klX>覷aql'C{&;lAyMk+3> )p8 a0p8 wczA2 ]rνp4nea>"t_L:2]|Gc#LCKn9c,ݥ-{g]m.{|]/gws Ÿr X.+c(lx!Xpվ|mObݽ\3wg}^xpEW`[cqE _ aC;oV>ŮV=[v۹dOaV<m.J^d;))]c z[&FOTa%>khaӾ1}zګL-8?4k3ſrHzEoۗ+9cCΡ5 IDATw~caв^rxq+^H\bc{|ײ_$-^v:;^s}6J8۫ a WXf%}>1s:Sn}v6kqqKNx%JA84a0p8 a0p8 ~ .~yrȋ. [5ߋ.WgRvb_H S2Ⱦq1i8e8ߋ0[󽬿.e(ℽ r_8)kwi)ַ߱ڎx|ZO`d}]LɊ'džY~#dk^Y7EC2ȘZǞG=园mC/Ύ$O&|~Wo~~2!ֺOw-k sZo_gTN4LqE b)ob"!_ ѳg4~,\|Ίx0\˦Zg[A_ NJ5{3e]eOO\gC^Pف>1#Ű\YMxhtʕ#\yZ6[K9aspЬ`1[cG_33{Aw|KҽK.ݽl_.䯽.hӿ_ubV=.g'.ҷwu/.+.u{ٶ"\BNydݥ9q-oP̻F'{4uxݳT.YL{Y=7*0'ي-[ oxÓ-W~}K_ s|( ٶ+z`h'Fx壼T`Y=_ ,>kTb ?AFޠ%op$k83|dœ<E\:$?lÜNǯ}kW19 Wli}.:?|bUi ΦqRO&_Z?a|d^+yg_KYv5=zEc_ O<\?~=50t]l;;qK''raFPEt C6(:'q߾1c_XU1<4.(0km> ^U |}s>W0ca0p8 a0p8 ~P "n/02/|{Ax/VncW]Fwa%2rcKՍ{jb;< 7W1smc}7CysEV.{bAI/N[L:ֻno~|raN"ĉuOx |ܚ}ţ Pl(fo"[p)8+m+\G?ۭ ۭ[ "/>[ -?'v>]8+ {o6f,^Sx[E #x07.7]q}6g<zS\ߝ a͸F~X!o_MZ牾\Sx+WW:} CX,o}UlS7wQx7ͫK'.3Kx`0./Qy!o?@-m}{uI&p3{tπ5$S5O/NvNCyɇLӞG7xwnY,\~G<skf,Mgl <[ߘri>ɴ~~h<•콏/,씷xI><>??{>8qg#j9WcVa0p8 a0p8 0p{rK.tV8FŠ.ORg/ӯ ³?7{YEf{;llt/CtK`cу}_enڏ ;kmg#NɲjsjLVo+~{Òt!ٰQŇxF*<9ڳХG6{|_#+yaV$¨°b{A'p_X[Xw&Y~vU.Sg>[e[}k[WrMWT _''Eno*Ư]?)L.~xm@l._aϥt{yf1=s"~Mab& &NW>-;oa[1Cō>߫cln9Nt+O> ´zŘ||[r瘭9mcU9 ͸g[Wc0p8 a0p8 .D,_HZe^|Xhpײ^B?rrHL{/O[5|7[.@e ̞]'dcfb:yr\{x.rڃ+kX+UL 0mŷmWAWR,SLSȒC-qo@b] ðYU~Α5髐o\w~o+}ӟΟj61_}~ɻ,vongvagpBk.WZ]:Wぞ.W F'_g Xz;x-^Z+Fq{G ᳇6sN6T5T_] 1]c:qʧ5!d#r=?S?uab"0\xi;xĹ9ؤ}zUߙԓGGo-Yz\OȦ=K2}i+3`|O7{WH$ +ZpDV[\ cÞ ~NOy2On_= *woey}W)sa0p8 a^nn/qsw.Temz>r]] v霽]BDⵋcu\^2}e7ɇ\bһ1LW곓b$/;wdŨТb t 0MqTx*`q/g͗ϝ|b_q~-lS+~ǵ=-\(c~;O[]8Xd_,etz˳5 `tGků䬋v]L+}c=X:/\ozӛxa >ħy=Sal7+}|!&=~\oB*Rvӳp -瓼+pX2RsDf|&Go>iNWXL6{b5fw1u}ڋ/h]ɒcq$s'?0&Ģ|-~8 a0p8 ;쒳.^\ֺ[{n?4sP̧KYh=,#{ɻ0kd-/gǸɇ~m,eh^-fI%>Y:w֊W!DqțAaS|;>K|g{NQ˸Xbw1sp{Wmy0V K>:;K/ҙĽ8n_rTMTE4ӊ!v l&k}[X+ C^YD+)Eƾ}6UAdi/[O|wOw\~;؜]QopĶqg@$/S~<7+nA~^}g{*'=W+tzaTx |Z;eί3$?0Яf + *aME_|_cFac;xUe?2 UXo_:Csȗç-dm8#{acgw|n76o=]^Y{[WЇ ??zu1Fq ?=a0p8 a.x^}{K;_e;l80eKƻu{so{љ~ЉύZ{k~hͻ5b6|X0r957M|yb0w^܏k1ނ \ {>7gk™[+^PvC =ԼC >w}3\r7**ZSQURWozaWlf]cX8\tڇ}|mBY~`WZ_7X+l]=ˏ8꯮8???x=͇BLÉw6!a.|(tS9:}+bf˼o_Ɏߖ5zқ϶Yg=a%t-(m% F AbĒјƸF4 F cJK)](\>)h{H9V81g&^moyd?#^ot̶<0vΊ?-sV6*M;̭_Xwl_/uĬ֋_rx\Lokx38/> ~<9= {x~gܚyd3]Պ< ٶ׽I>>kɀq?8Yݟ403Ik}֋~SX+Lg'|_: 2_-߿^/ρ_˧%Da0p8 a0p8 [M>VRndJ~^wtVne?|%KBNq,!dc1.?>7Iym1%xU.V>[]0ߓ<%yӃQ⟌K6!/\hW,<ƽמ+ZYo>^j_ycœzW[ ގc_gwn[ޔU$QtZܝu~Nim;sL7erᇝFs{4kE|yߺv8pshmgLF_l6SXC>L">{Qf>oKI_|%czooc8ɆcmOfXu~ߓ|u=3? < z'a0p8 a0p8 <6!Wҭd['7 hoyAZV1M>,p#k\.{h\f⌞52 (e<|~w}w]k90w5 im\SƷb+_뜌w6LT`5Y#w;=+zC\qC g +΁, ortaU(:ЙsE^|n`g@΢f{ ͞ψ}s--.oEWۻnfq[o;}}-nv;ռQ,̐bK.\Zحݏp cX C11|h/}96΂EYSFX̍[cjN?~ӵ{M~qa3[p a0p8 a0pf۽+)s"Q^c+Iɒa~-J@|Khwqm8[ZN  gv-cI~wa[1|H+(Tldwg*zi̞uvl)*^)F񊟝yW5+Ƚ%/lãgE:n-k ~a({o}տ+$o**#_ N8_k}*_lW*ؤ a>G.^<059_{,Lְ(&@7Y;g ݊:laWɿ o8g9я~}orSoV>y^w8?u0Z/b.&1F}rk㶂v/蜲+{-l9O1/<5>xҊǘ8fLLbY3X޲ű뷅V|tG^}k/!~;grxJ8O؇a0p8 a0p2 ɧݓ] Ȓ%JnX/m1%7&Xot ñήًh狍qmvlpyXo^Oq{|η+iȭbZ?UQk&7/Fw5N b7|ljZvވ3n5Y8Et(П/90T,[!caۓ ;Z[4rǛ9ƙ~9knc>O|iSEZW5EH#.}͛0V󶪢:Yz]Sq\"5aJ:)Goc{`+Ί*6L\%kG}7}o咇'"I}aGl>tG dG )ywͻusVdV|'+Δ;x'f8-zAzv8V}~'[Ï{AϣY0޵dw,[_×3-g{W8ތY'ؒ1#ʟV\Y8–0b/پߝ:0_wwϴL=Ǹmxbo ]oiac>ݯEKQO`: a0p8 8+!yOL+!WBDw Mu]R3|c2ڤOek䓍8Ky{aɞ3ykdmn+ݒÛ;x=i$oI<KWTɆ}Ř;sa퍭|˶f,1kk=θHl,׮q8c'|b3Ua'LS]?a+}+WqoUb-Q}s+R:_M0\ݫM}MV1St8zBۻΕO e㾻K g~U\!`I ֊qMZQaۧzo\+-f{޲|žbbyOqc{'㎌"aYgfdN/&f%wgĖqE| [ڼ~ 篞O<__|6v"?CzOEY{u=uZ8YZgºlI62O82LF}8/o3\zH/%n}rri<q6wF/揝bdgoԙ-C<횯5lqG6~sn-ټſ02p Oɟa0p8 a0dh+ᶉ_odowæx;>b Xy\Â}bVA`KϏ{%ʙowֹ[xzOO]l/<0&ݧ |~=}~;-쐃Zf>N-[W|ޜڳL:۵g\LV߹O~moK֜NCŮZ'~:ߚOzN|޽a/a_n;egk}qO.ݵݚ^ _S>70p8 a0p8 /wMDɼJK[޽G$\'+'fl mBw_L^DO)AnsbO/CƓNw3^t=%?*)$U@Ty1>\ |>_*bI3pNm~eBPdQ~iBO{s[_m(#-cHgW-`1_ t=Y{R|vhz~X˃0u>֍+ "=6kVM^ 9aPIV8'|8EI_׿z`S0/6'Ƒ7q vXc+.=svŢWGSg7i,0d&N^c6ս]^yY]1Xaq;.>'|w˼l3Iu+-ه5o=>ˎ!xK lO_"o?x6Is_,ٶ윥1<&Wظ윟p]ׇx[gvo^ڸxmcw [W|gmiFnyw;o[*hSH͍%)b{Q%- $v~;Gű8 _/''9kyt>c=VV0b~_p{{CU(-)b5TU| _웩+SLS\uK?2V~7~U@SܴWPѫsw!W<[Ş< [/zы7t(}u0? p݊VXb$̸s]Ԝ7Ugg^\gƶ};b RzYoá0 .uwW*ǎ5$.'P,;sSsb&b`aojwV"㞿/ qhErb}tnUloغoŲ{46+,EGט>z =ioҵo-3}]ucI̋g1;{\Z˦cT|3@e\_,l }=lOԌ\Ű2)O,K'Y=w^'gl_coy1bXOc:G|U21*h*ئMJs>qd̯M ~ot+N=.Y> Zq_a-[·ms}\+ti'?n4󲗽{Y:;۾Z{?+52SQOΙU#?__?я^\;3E@o`:= [}ϡf/wKX,9lzMzd5쯅g,[#.~4}>'~kZnݼ>shز|w={ I M2n)>a0p8 a0/6WRT,ٚ~e/ɿeDdk%W3k/]\r$d~֧O\B_{K.仗UkɥoaϦ oq'kgnX>۳Gk>{O{c)6魽9[g|χ;g_܇vgeN+S2oXw'\O,ָnzɳ 0J|Fc!;83VYgC}.x푁16_ݷ̋^+&Y[V>lƐ?{-yd3amߺ'b,r|:vkx9!c-;׍?s$Wi/3p 6a0p8 a01PoJW.&}L'MLf^% <\̛PdDf2WʵF {؋>gqxzn=MW∗d_<;bԗgGEa[c#[q!\%gZ[k#?kw+~±;.|S1C\ٳ/Ğ ],þwyAit?ԽUlc oo~x_}gQȊ/[|wNwvsnτ{ Ks-wЇ.{ >7}}k׺|iέ3e?-lĝXȎg{W58y>%]Jwuصo^sk7[|XϿ3sa^g bm]nºΝOvұ\ow;qo},k}i@b0p8 a0p8 _ <'JnD&7񊖒ӻde#왗4m/ltOaȇ>rr1񆧤|>b5peg9OGvoxr}rGS뽹>G.{zP̞8lfi=ov-):Xg1UDU·>?qR-LNCǘ/s Xh'oNW8{xZ j|"7Qك۷ԧY\..*nZq_ε̍39']E?q6n%v;95lҥc=}?nb-Eqovb|׻uw0nf;gtpG4rΖ}o« 3Ѭipt;72|+z꫾:_{Elwb)p'X/KGOd'+~~3_º] z{%^gix޸gc=1X=2ɯ^G-bl]lQ]\n|{&xq$8n|:t]7O_~H8 a0p8 a02PS4K&&[_3xK% Y3_qW}vKv/nr|u^5_wNnpXs%v-x {>ƚ7ƛ޻3kwWc;:2x{W ن7'}{Zggwlc6:1&.k5[Ww7򽉋 o≜ɄN 'tEV3yg3īSvr)Y<3M:Y>y۾ۮWC3OaKqssA&|Oxy"cN=ǥǾyXcsfpKl>a2<3/Oٳz(^+9E/m`EZEf=ڸQo *VG1R=:@s׽ulϺx`.\6nVHWl#SgcEҋ[c}*T}x:zAwƟƋ gq&|k+}2?*^|ua컗q&^xqW;n`P;Ǯ7{['|]S6:qCb쳗E/zƆ |ܶ]y=dI~޹X[0[-?{l,w_;w,Lwq^>[_ Sb~|a0p8 a HD@$PJJЕxm[dlIFr^>cdNd'wo\qm>=p%?}xV1_L.۝$lqX_CM1d V<0 1Tgcye[wޯm=,͋Ϝڊ-fc^_."hً|4n^qH1kc|6;ׯst3y᫜a7~[ۛ;Xq/F|<^YA:Ok Dkţx''LN/~dɇB&,ǥ5w=Ł׸gX>O\?k^ ++:8 b?c>4z|8[=<`ؚa򖶯VlqDΥ=䜥ƶbu{ҙjN_;_ ӯa-Nudk}֒f絺Y'HNO6;8n7ޒs!ד^d#ۯN>ßxN^LaVָy!LJgMka]q]V/zw"|fwy($^8brPayc2qؒݽa_̏\01i{8 a0p8 쒞)IW/LdKҕ(ڄ!id$eoм*ٻILk3]W1ޓ&Krfgse,t=ٴF}puNҵ{Ko)&- dY^p+:!˧^a8X&OsSa׀>Za?`?:f}#O⎉ZĜ-ª<:{x'oMena㻻V,t$Ӝ?6kߝbxm]4rX+r[ギGp+cZ3q/[+ۋ/0#7\̵{ڳ}mmlVwC8??[nY0<;=~~M _MG0p8 a0p8 (1;M줳<%I|{پ'V&'o8lV'lWvMO/YZ|ƓN 0= b*J.{v앴5^;[癭d7N~+ُ&ۏ/(XW>v*N;M̯wX:%>l4v^b`Y_q|rALUé@"&|O'.*Źvb5~+9 iwuG?TxFQ\f~oS R#7DOgY!]ywy辰ѝ{p7*Lòw}+NJގt,[ȧ+΄}é5ww*bw~a@ٛUƊΓmx- vΛ8b{':Ys0Lfߚ9׹dzgI/ Y6;.$ #wGXCK/>iŗNk᠟_![g^ Y'=8yڝ[}|>x]&wӍ37v{?nkqlw;'3? < 7֓?qa0p8  moy[^\oe/{[o]ɷ]E2S~Jt?g̕B_%Jʕ$\Zɽ/ND#al|mnr2gsxP3%x[1ߵZ[@ykʳIMK2X⧸ѹjwVa,vW>.Yos;*7NX}+aaǣy,dEa):c }X*g#__;пwWxv}}f=}5vNεxdmn_o7n.]kzjxݱ$gV֮qwrÕ^wnq-ƅxwpw>N~Ui-+8;O^wD==b^dVkdvl/w7w'?pH8 a0p8 aK?z%~O?u9~~KFCo~+?؏L@ ? 7c}ܒɤc}@ӗ,},ɷ8 u/%$d8WָcLd1/aZxqǓœn++v6Yn;z2%-xXEDCۉ{U!9q` ٍ~gC뛬7gkϑuvO#k-.[ٱYYIcL8y۔^M^E<_ "`o**G3}{C WWag~g-C GU|M>+dZSl/*.ۋ;1, :ze[\:X+}xӛt}qHNV|&/}+XlpZWu o~N6;?`d?G~ ^c/~>o.qG`nO{mxg {]=iIltY/N{ iߚw6\4׊#,mƻf'5k=36?p݁yυuW;#2X[lk}eX“Ųkt[,N>'=._wa0p8 a0p8 0wỿ뻾' /{k.9?KNg]ʼno~>|#y^KGGC+MޖD=9I>kONIk\{%M{}dW,a&b*0orcلf6Ő][lkk#|y+M/gB/ K;%O_Ng.p9أp@eoܘG6Xklk[H1'q3a_{FK5=kZuM9@.Lxd9 o);2-N'a`;y^-6~~cwҚd;[[5g\D=idأ-.-ƍ;.~IݹJ͍eZ;b-tȦk?+ڹǰw&vNi'a0p8 a0pg?~?ggwc&>t9gj-N[\ ۰ZmEx{v|`3]r+y g컫J|xpe˙[3.͠ӘlMdw/:#ca4K^ҳ*pZ^ƮgN~xbǏ9pԘx|{.8\er}8q\|WO~ bNkZc+Sm͕.qVg?U=&s{<;Mxqc1>W^/)O? a0p8 WU%;W:kԚd6$6%O&រ"D$7gIQɽw}DKǕ'7xO6(Mj/| Io %Mvl^ +kN~ٰok6^'ó1k_8,k.g+saN"JX%y*:x'%WHx{٦bopWrmq6Nr^o_]7qiza վW-+&7|u}{;3.Ħ0bF£oUv؂~ XٚbxدllۃOLgtuxt{^daF8?_o\gXl7}2>'Fx?]qk u}5ɦ/|x?p(0/;#~ 7M\8=Y؀Qc#;di/Y}|gl<^9fsn{ooL cd1xJggn:{\a-'9|!-d-YVbIg1w/8f~ֶjʵV|}>_6s:ǰˉ'歑;0p a0p8 a0pg_/ſ~okioG험\IMQ.)I{d"{Z%$nI6{%Ǎ79O]Sw?}o{|"+mWa_L N= (7W|6zo>O^s< q]ѓ7Z4᲏MA;>d^qN޴wג>꫾ꊭ7sE{® #eIC"&۽Ϸ9}ٿŧ7z)w؀o 6kiqnώ5$}bY쳫Y7}kM^I8g>Ft¼xwߪl,7!8#;m.Գ]q_'\gŏ5zqYK6ᴟb*;'/wltK~k[\ÓrxޕacNO!0p8 a0p8 /m_Uk^+@ZX}5%Mד+6L_k/g}}J\s%Kڕ(,1Jd`ɒdw?&çDmI8ӵI>/'KrϏ~o{t7lx,Ż1dδoY(8=Klx`JwςX33/ 0W_㤸g'l݋{;K=l9F>- s sgg"W`c"YE^ݺQAP l+R_6eU' me:8KPaT,|Bk,6>:;8?+W>Qmcaw>&{0k cg jpE8`{?Kܒs'nTNWncdOL>b9Χ{@J2ֲOlvJ,kcөcLzHz8NؓٚN1^vVY?Ř?6:/Oske1.GNwemk^ 텱86'WkwŜ^LW]kwxa;|l|1uO}8 a0p8 _ %/!h*JO_IKܟTۿJmoo/Wk5E Kޓ!%ɔKKf{㒉%w-%J}qXRf 6)kWz{&}g@x5ξ>ls+k*.OWߒXW ;~– ֊3xҳwuzҊeRj=ɛ?qTgfs;pja؏ 35lys rt?K: l){|7~MozzXijon|W6o Vc\E~p؅&θ߾TﻇުO>x{طC0eU IDAT' ;3}9m ZM:''pc~Y۟볈>)+E3‹9 8q+8LktۚQO6W1'nkkwlvWhtevw)%KW |t^~“M"&dvNNI&7In7յu_~fX'c^,-d)n;չ$#2ٮ1goLnCB$7 öxӝ/hLv͸ݍ~}Ϙk= 2w|=d6t<\B5b牧po|l'_m{rΙ=E_ m^ H {zVR!Sa/}Tuvc8l̗uv{3Z!1(v+|14s?s~q[5_5ל]_v`-Rv*wG:f!E]܈pɆ7m_W\=":_8#g~lMܛ}5y~}a{ozӛV'O>3xk,Do;E _ 3._m?鷷o.ݸ[;{ኣ(m'r$0zoW,UL{$43aVf?`QY5+-\{jagǕlˇb]Ex޻ i y|pfOTثVEW_ۯ g~kqEӽ"]M|`}g-66c_q.k\k|*qXŽ"¹tf4WT&뷇ljݥ>Cz;oL^a} I1^lqod[|eo^ز>g ggK~[ kL\/⯿ ,F[1]yb8WOsɶG/?g1%'_y/͵;d)኏/,N竵1L@c㋩{ _La0p8 a0p8 _s5I7__ zJƷ2ӛ(6*`*+UTU(VPΧ⦢%|fpg( cS ウe_<[g^a+?Í毿{v݉w*xmjb \lA\~}ŷuƇbsg_>yxѽͶ8Cbޝ'2lKw{w/^{z`–0Ko#][uֳenkR᰿:b_+ƍNd;3? < x'a0p8 a0p8 6Ny8 a0p8 /c6zO f~@kD M.E$,ɘ0M4cXcэM bn8]Lc6_~w> ?%jw=,~\end@vؽ.Q0%糟|y}oNE6lP=%7=S{ZgW׿zc7YKxؾB6n7V#b>}G1[}O׸8.lXZ'^1ZEN6ysv= M9 ʴ y磸Y]&OO7L{>lZ<.7{wo<#;p-O5G߳*2֜xes(},)?}g~"> a0p8 b@ҭ[I{kmKz)Mc\{df~N6õ[ӗl7%an[[N1.Y[[`㞤ϋxq|Ưy4ı-dyCP[!/\d;({3;'X:-#\<]A. ߱C}6>;Ul[WPS?Y\ƭo.Wᒬ{<*TzVnFV'n&(Uoև|#S+X*RVV GXwl]k@LF ~3̵Q?+sE>Ɋ9NaaR$K_?3 |WaW\k}7}7]޾VkM_8־i"u7܉ b{}&6|mHlOkimqN6;$w>ً=gr~Ϧ1>0ٙd|4|[}dù?7yãj}.l{sɏbZ^iO[l<8rֵqƇS~O~8 a0p8 @Gft% s oBouJf_B{_ Mdqo46㵾{a|,\~ Gvb{g!YkK oq\{]x)k.T 8]seo;?M}6(FDGK:{26ҳV(:s{q}{%׋3|ޑa=ָl֙'~{!"-[ilS\☹8WQ\b_L6}uonоG!QqVj<(>_DFQRܛ3 6_?WqӛeX7eE~27'~'7{8mnXȋ'M| b3~_}+~qxLƊs秸'o44}09kb# ӹw{]N><;z{5U:iݹIko{g>QKVE.aZg1/_غsŗxxsb wÑVlk7ٰ^f~9|ma0ؽ|>~uO0p8 a0p8 _ l2X"n~%Jߓ'%%yzZ+6ih^cvWfLDck1ZȔ<[KInyg)xˎ\'K>ź|ؾyfwOk*}H=w%v8JB/w<9Yz8zCy6Ρ@'#[_x&\8Ñ}|GYԛ ~kyj)Q,7nVUdۿٛΊz)P+=d~+^qv"CpmO|Gx>gSb5C2Ǽ{ִ3T\;ޘݷ߽yh.Oy6S?A_Z_{h`og/^]km;ο~/ ?p L>wXc+nhL|ճmL>sYq-N^Xcv$;y c61~Y<ͻƼr{r)?78 a0p8 /wImo%:%ߓ\DeIl|eM(ZkhژLxZrh~؛/;拁͞x/yllœ>g^{pf],qdž"b7 )Qxwid>4bN&%|tn䲷=yufvˣ([p'^30W\_өLc)|@'{ k7Wpw:[ rO p˧>k]>7[ jf\G/ };s#}/ }9IqtccZٵV,ҵn6VbOXNox`}15;cv;pMx^& {xvw#;E'O6Ž'֋~<=q| >iE.B~e_nknxxu뫿W860|rS|Na0p8 a)dCIIIdV^&Jm'g܄p IA볣+}MزmLn5ݱ[tx/r%?~%624q]|:@ xB._w Yiʲ\H"bƜc9"֮RDj/ZZ--{onƗy\jK.$ ~8|OWZY~[򑇦=q~7f/2ö)' Cu/c5[/8U_sgoj)K6|&pغl?xǣ9ScK]`FJ˿|\ovQվbo.Tv7t)KK~r8sۯgkM.q~]~U-K8ታ´O󄫽k{G,_2p~䗽W 4yg^mF˼X./u~~54ƕc?X;mW+{.!q^ԮƯrxiYͷgsЌy8Ĉٶ|qݚn*}WU*p \WU* pZ=0囍!t퟇~Py6kr=Xd[Z`1wro@9Zv} :ܵ^A87>Ո7'/寮|{so'/c?˱G q5toLٻ,bv-N|iPrZVsmW [Ngi^I7͋e?~o~UmO֢r,^97\ڨ- IDATN8ҹyOo޽N3{ ZUG.hEn]vӯ</4?7ym.˯ ?xׅ /q-g\.W֯pFŷ.o .wx{QdϔǺc}we'=_L8٧oR7c7_|{po\<kӁ`owꜫX9;sf5Ҟ9l=j{{?i^9^# >l~~6>qk^X?ַ-NՁm9g OnӸ>w17_s0gƥ1]ĩiʼݫoL{7[eoU*p \WU*p |؃8}rAesvksڡ);4|v;CÓg.~Γߙ$Kv>u:S_Gͽ6ꈣ9gv\~⛍8ްK>4]M|}ZGR~źu1֜z[_&x҄6hmQr݋4ʗ6VW.e]Oco1v%on.BK޾5 o~nmcuMOݾUFշj7YLd]VFoq]HA9&q.`iNy-ޞ_|ǚֻkt ~˾9^{4?0wiN:=8}Z廵Uʥ/=wO\1tZw7WxnL}nҬ=A{j_g▟6b򕇶ms6z-ﳏr_s}׷;/Y޷x ׸U*p \WU*p C@p។e΃;Я>[rCgr3k9n>at0shL3߽ЀinƗ4Nc.99䛟Xqy:({~4b.RZ{ws=H<_|6[gn4囿\.LS˛͑iih}jârϥŕ%t5n8sk/7 Տ_<2q.[fZWK|[.OlGP.OpìK.WboM޾[uoVW=]:_G/"?÷Yr_aݯ~b.qөO>7<=.HִZǫGmW}c.?7[o%aoqOuԧ;J|ٿx~w,G7 W޵ᕷyV7> <ףˍڇZG0#Og:rC~Ӱ0jR^?p w[+[Tܮ-O^Z<z6NqOY8ٱgVyU*򏘮WU*p \WU*p \W=Ma_sa;؃ <=ay *7y(q:pz#۩֋ǯSڅ[[kcNZ#5{/xCb]϶~k- {mz%X|=g>xZ:v_/^4uӦ05__gcɻpܚhV[,7Do9boKƫEcy@#m\6jH;?~Ƹhn>R'{~Kͳ[j>ԃiv0u߸_ItWOO L*p \WU*p \_8uq*p!޳<:5><߃=T4!Η ^仹W |oMS==s?^kWwYs{Ѽ~mLq'wowq*\{G.v{nͅb#; \7Oc>[f5~cMol݊F\ {1յX]ҩ\|Z0QkUg~"9]o%zMlfo]7x=oV~]9^˷oooWS8|9ɯ͕K]"QW{Pηrĉք0qɳŗ?-|˚>ȽWC{|7U*p \WU*p \HAC4o::t|6_{й*ۜzax9|߳ZڋX}Քa91X~;kmo85cצ=o7.<]u7-w~eMzԼ=M?KZ/gNZ}u/V |،ϼ4K9飮]ojӜ|M֠K.Z\PG~cI8O6Opnv]u~.ox۸~wQ˾]j+o;SK߽ͧ\.75$W-?pѩ6::~>ָ8w1\x-wm:]dOZZq -|擭5cnǻ77jHW_s=<;{ct{[|%yz7j?}8zt8L%\5Y]2uu-w0g>xvk;Xp<ʧGC-m^4[<&;\o{ |ֽ;_WU*p \WU)Aú=퐲-wb`C@1YX}'X876̟}q?mam5Q٫}g$*]6֛奎s}(ձ{kھu,iS;1<o|߻5ϳDoֺ ^jxEx.^\a KrᄁmxO/?6(ًl%AlZ~u3cl)W^xݸ'ej78NјO}m E<{݋x]g0gϋQs>=t,n˧3ϔ.cj'WpmqXqTjg֋.ɉa[/i|->[gO؞vɧ6lN>'r}ڮY{W~ \WU*p \WPeݸ;Xrњk|a#bjwp?q{pwPσ0j,;{Kss/x4.ofKfeOqOj˖_v [W[Zo.pckgI>aЃi{/ܸo6ewifͬGnO}uqtދϴ襻k:kظ>;ݲl拵}X>/>Iw G;/eOdQ[#o]0`ɯ.\d_~Roజť9{",o\F:-֡כϔ]/Osǡ'y׷ƵxawZnA>>Ǘgq䭵[E.cug7b䷼vXw?*pU*p \WU*p \>9LL;|v~`}u5.n=0|:7ہ:ړr/+/n!YCy^m]O۩Kӳ|¢!njVb]t}za6y\D=orCW8d[onʉS6zv9(:ܐ'?{gs7,s8 ,Gz9\ñ8Y>emհ]Z9gmWzUO-])-]Xrk#կ^aMgqs `xU_yqʯl.iYjvk*GX4̧yX|WsklMuuƥK?>j_\ƫmǓԄo!&ANX;>6?xюY'zɻ0Np7_.G{ɏ=:;mkggy}VigLm[;n_=3rl5=X |J)~uo«U*p \WU*p |pAC=,qw85C=΃nvbb:7_7F^+l0-n]ϿGMγC38saoo}V{sta5_.ëEBA]wW3ްp/=۷\Ck_98WxGe|ԏj!֥AKEb\b7 ۮ5k^Nxͯ.+x7O]lpv8mдϢ5?i況5GV#^|_] x1|{{jq}ElÓG3ZplÅG!ՇSlqs=xv~V]ՆI'c/8w\gׁ-<_>ikşo _?jq[^ɹ{DՀ?j8öY^w\WU*p \WU* ؃GA8tW`AF:X,ց:L4ϧzg`1yW|m9@ރ7rxy48}q['O<㳟ݚ%ְ%ً h"&^alkj;q`^M?裏n՗\|%ϽGsrv^[>@GKo1݁g'.gyBʹ<[_:\ͳyl0͟XƋov|&'Z5w.tw-#~oǭ>]Ժupp]<C-=bm\8ԆkG1ݚ'<ʫ6o1햿O<0_^j#0C]|?nmzVC!_N>_h<[|Q<\s-ֿ\n]?jʃ\8;GCsqmciZPG F.&W6-4G7zo宖Ooū6aW'=~ʽzϽ[e[a\s}&ah& o~<_bWcY|s+.m/ n֢]m|?-|ԅCk~5msohlV8g(.GQkhZZg534a?A/dD]j c_Yλ(>{yu7'|g-Z}뷷?|MjF~<[;ZnuW5oSSϮrY.k˜xq^}`X.j$Pϛ\ vkU*p \WU*p |B@{м 9_t۝w0|ð>ye{SkJm5kx\ٳ0-b(~k<ɏ˅8˅s*YG=_-e5rC|(ַ>~m^5o霟ukp:/'}ZG.ZxdgR*q4sZ ׸x}ӷ ]QZ8fSkߢ?Y7O#yO ^c+.]]Wjn./gyuk ]ZIQ<#Nt!Z>~+cs[W޸,v}{F]jݵCkG5o)1[ss8oB˩W淶O~~Ƿ~s_Kbl>faո_3]wn5<-.Q\;ϯso'է_1}U{ZW} \WU*p \WOy`jKN<<=̶>@|s{ȘVZjl;aeNM}5^ۜ:;x<'z˫ZrEͷO-/=XΟn֠lƟ=c~ƵbH~6Ne]f5yhx?wA4.*:#.k{jO/lm5 O|[=O3s8KrʋߩSe I??5XyƫٍG[ nABA'z]Bʗ{0ђqh}kSk<Һ1~ńG|zA/6nl?o`7U3f1~crͿE&~ŅEt[-tl\r:~X-fCm9v^.|on[lhV.7s8,Ֆ]}>mM٣^ &~Xƽ,T9WU*p \WU*9W`m*1? pIVpr5&7=Hⳇ͉=ki_.CyV=U/=a7~͵1Omb<__<Z”gŋuYT &?tL嗫ma-n|i޾WV-.QpR?zg'\js{mҽ^_ la.o1wBo_~l֏uy-&\kLZ7^qVW3,4H|kkF=[}QSq`<9¾OuʹV>}\6 O8n=vթcO-˳F8n}A- ;Ag՘.s[3~[Tzw1pR3߭kk8s’sq`m8l81*qoWU*p \WU*p aP;s nG>njq6!Y1I7Z:<\N,.a}riO wMr^/Y^CskR-F5|yzϷ֓_MӾƫ\/7j߷`ki f5 ߯׾zY.fm58=.P ohP ԑ˖wkh>]yhheQ]+amKŇ ˧8֥p>F.up95=c-WW.hÅ!'O]}xypgGS_} {Y1x_/|wm|;?_~xnj\dSzAڲ\jxm't,o=ҡ1گ裏黱Zݸ~аօEs{1ſv/K=8<oӳiZKnb{oހ}7F-&nmׯ\06*nomUyhkȵid?^A}޸w>Y||XaXj9G.q#Gy{Vzl4pӟ[c9\wi W^ߚ56Ο ꑏl}1cO "{}itj-n>gMo}~G|?Gc_{Q|V~3$'9cQ=>j^bWgk_1p[0 þ1쟵\\W;s m\/?׮WU*p \WU*p \>پ{<|;^{ؘ@Y;syl@w\7\bŭmۜn|W[r;X~5>[+׎[~Xp1>jgӈm?5o/-O~]w1TX.p. Í]=˶͇Pg~=k^µ-ͅ#NX|]\\|jj|v]lm>{Vo5g_oGwU8r>@^w im0z/޺w>v!ލ7|[^?aåSX]_d׃_q9vjqErQv͊QgjV8ͩ^'7<15uƯV|aюmc w?yoNzU[c}ˇ?[0;NqU}׉oB޺]p1WU*p \WU*0dD:Dtxp᠃=tdKg|:x)nHW=^bwиXSj>[Y뻱Y{#NsN5Ȼciu;vq-z&8UoǴZS=[; ~ >pڻp~KWڞ{[K>  3yŬsro \is6еuIvыn77cծ\ՇSqȎzɯ]C\6?<և_g//pwAŇM\784u1Op,h/=򩡶sx<ƫz_멏|l;'~S[lW}|qƾw+fC~zFlw:^oYGՀN \ꯍC-^%j'WPVtȆ\ m6[H^Z,31ywmWת~+ \WU*p \W{*zc6ⲟ{ǟ.ayca.nZ V}gɽ7b~s֓0h v/5腧f|!oFr.g}oV嶚W_c}k僵5f3""÷կ&p|a1]p-'{cX0S6_:˕{M>9K0|>'9z+4 qoů߼zVpk״Օ;;raٯ+ Ik0h6pϸX|קïx{5h/SۅazVl|!Ro-͛6ovNL6sv]6^=Fxnv?GgsW[[fn?ƒDֻߠ7g6q=ߔ>gm`IZl%L/.~z]c{pz-p\˧Xrq(F|/}%2"pPݾv;{OW\|4{yO-mMjo_qƻp\-&|qz{yκwrof{r~OńkWzj꒸ǷV8avkZ,xӱX {cx6p6ƫe1_؛O}|}XUz)'go\6W?{W\fϷ6ӇeWe6;kc\`v[ք~V.mN jxVHk g& sGd)mgU>X)9颾ڭ3>9]pqׇA \kN-|6:dU7=m ߶7U*p \WU*p \P`4yb8(fzdlC5A{ jnG[^.{X):10g;vAl8✜9\~ W{XÁg ֑ωY[baךNx/>oZ|_~]/:ë CﷆڰÉwϩig7ůK.354 #?yN{թ8>VmM8) ksɎ=k^Nz-]x?~թ \ok+NL\Y]4XOOkWԇcʿb/h,~5_ ZǶl3/Lgv]ꛧ ټ'^q [m ,yVK[u4ػ6kT@3|arƏⲼy\LEGl||տ:!ť1]y%qC|t ??\kGn {skV.~<ƩhN~QO.$zyцVj=:of_1hָɻN0Z~z`klM=8iw4o6f}n*~; \WU*p \WOCw>;廇tT=$t!c{`__߁y ui>KrA.?w^-ȷV'|sƭ9}>3\z^k8v擗Nd[8e7G݅oyWAG| _x7{K:'\ߦoS[ckf-vo淚7'](g[c.U-w]hk-zwFI}o4A7fzYsY0kM컏h˿Or&oyljF3voIn)7]Zi<OsENࡶ]=ljޜ]7|{CspCnW7&q?|ZkSTNxmn55POx[c~0׾8ky/^bq%;krtj>Z:/ٰ IDATs_MoU+p/__WU*p \WU*;x;uhyV6v O~$zy2;'\Q]b/?]7{=~/ckeq95ZMjwwy5b^~4ks-?}\x}_zq{$}Ss{9:}ƊYȣ^qG.V\5նȧbܟ|5k/]ItWxRԘ~{9<W^u)6{xٌ2Z횈-vXS:LJo~źw^cշG¿9pYNsjvXjK|:ƈ=_, γ?W?Ư-?:kgĉxqq89,vPV̉iϜ{lǗY.bˣ-n|~Xrak99U*b}W@ߜWU*p \WU*p+WC=ti; #=4\qXM1^rc|a=\_;aˇ꺱qmv”ij\ f_ ߽$9kl|.=N=\V3?zsou%H8|6>|GbpX OxEc>\[qeN% Srem.]L9w̶kFTgsA}'gX=[ }|9fN9ڇ]ԇGi=zӆ[av| ̭|4,ϊ[>?[5}p\rgl,]yum=V\WxvmK?xMlpP3/s]sε˯kҩ)vA}gīG+v\a_iդ~O5cN'ϭWjXMO_9o{x op \WU*p \W{(o9ܹ, u7 @a,'<6\.Iv։;b7SQ_=Iq^>y\%o-MVuֳ8X:y(ޥ.O[gsqR{c]njM~k /詞]յoeggq{4g`v[rVy~ZrVOa˱p~y?ju 뒭~1ك.W_<`<oƋVo6yϴym.88|69q-፹Y_q7+g9[|;YA07?K>wVwa-~vyh\96/^kxz{hM.8+4] {o/=>c~ŷNcsqr57֖M2wƝ:-7/n\妕vkV4g/Y;zisU}'CUWU*p \WU*9R`zJ1ayu璡͵9 \nZ.Ǎ9}՞Ltr =/;M̎Oq.zV˻”S-ƛ7?^a6ib^bk=3 WlgVWOqyW|X f], .m=.G\8>Tjgyq ;]+|m-˻.qoLltv:z܋9uUϓ-jY+|kSKmmrį?{>~|X# tF{qqÆY6lypY?5Z~ͮOwշ86^ >9iax}$q7fpj哧13; ڈy{Z,廜O_^[U*p \WU*p \C8;L nypA _6,kT\bАsa=qF!wB$;ƫyrXd,Ʃj= 5MHq>:8:vvmԁgQߜ~>ZrExoj ?>w6&[iwi.GX=-O}ZMs}qe1Im֯9~_Qs[jDž.a@_.dWwmzkNݸ v:~\> {5[MKa՚7lָ6CbWjhL_M+-gdE?6zɻ8<˃_>[[~=gxa6G?z\-y^ͼqW_`5-ufsBOFrwu=G/b!q71-,Wa֪uqbր6wbԼ\¾U+pkU*p \WU*p |B:`;|:sXJlp@s~߁+g~x,D0js|CMԿ]>vHM|4*<}ɳv+sdGա8OI>zyjnqq~s'}s)n/>>P}ڨ7۵{|r:5s0ivַ\rX4οy>pƷ쾥.ypO]8チS7;kMjZ^yʫ/Ǯhb,w͇w?7bղ Oh xT}pcYW[ ~G-/p6W~Guid/~^C/ڜF'O':nϚ f6{ַ8s1a[Ug~ ,g1O X |ZȧmoWU*p \WU*9W`u+y :/.wW.=W~3?6\q/nk=y/_c97~5T:K|zVS˫y ^vaX ڬ~[xX0mxoK\l>>8zqin|j{lZu5ﮇXֿ>}&:Z. Yۚ<[.]~vz?ᆃ}ͧ*u#c8p5ӣxy[ք ǘVsǚKݚrticA{s\g>Õ0uٓ#"r:7-ɑ.q-V-^4 [?>|Go~y4Y?_Ϯ͝lbq[|w>biK]6u^,msֲVLOYk:YMu~N3OW[ꅇvʯGMõW63Zf ~ع]0=ƥy߱y5^hOҷ_gy[]6P'tzb:Ϛ_MX fk^[U*p \WU*p \Cu@;tPyŜu{,>k;7/XT3֮^Q>ʱzoͫ\vsŭv/6ߓj|#nXep{g=3MHE׾.}c4lˁڇ/~tljUzӹ9:,`Nˣ977FPbs NxPo˥|7~18?tO+}=b[*> bk}KHvCKjq3/6>Q6yտxqX=lRc-)/NîA=[ guZ,bzX?/|;^[q>;g?'zeun^Մm,ޡÉ~owK>{RGnֽs\[N[k\V[q,kNuzWl0oT`?3 3& \WU*p \W 8txUþz=<<{G.!i16osw_'ӆwݳ-KSq4s]:xe]|UUc>\Ք=-5z75fw`=q'[ש1onkU3Npx|[z_?|j?1j&u?zdx٭gî5 5©jY#󳸜Ò';>ٽgO;rZZp9Zx7>p{E0^[yt5K{>µyyKˇ~8M'34],k\ xp:U3gmBz/VM8W};N 3_r~ݚi]rŮN;3L]|_}{IMaZ?Z!͗%,f6T_9TԾ>/˭Yx5.ͷ1.oM޵]=F ˽kp'?>_g#ׇo?ŦOn6W_L9hտskcǗW/uTo[7xZUs o}gkS,-|y}淾禘w$oad~Ν~glfڟ鈋8]U}7W \WU*p \WLA{|>*9ϕ>{w:DVFϳ={ᬘ${,~MunN}93i~w1偡.kc=ٜP囟a>lq1l8[ͻ'ԡqX_+'8o^#|7fyK k1]q]^Tg*\ث#=u;sx)?ZRL1p8r'fNeυb+N7jա8_tKq>@^&yGƬM^Z?5e[pu%_oCsIaZkw[öcQ-aek~.ˉ~ւ^;W:xyjY~_7Ʈs|ߥshs֩QSU5_WU*p \WU*y֡۳=wء,0;ՇW3gc?cArP[V;`ˏ>^1^V:Z45>jbX|ֶs͟kuܫr5;6g]յ5uǝn+wt/ҥowo~q/}Sc /6թ~ܞ*o/ wUj5]ͫ˟_{tG/d~=>9C؛R_5/^vE<9O[V.jVm=٭zgt8`9I ޻X>Ȏcsr/&_O~}-1npi>9g~0hC?gS~%}X{~݄޿g66j+"8P:SPDPPXDt"*(( NO3OEIj<5ILyzeiN7Ƴ>ֺ}_u_k}Zş,=IpzO*jF[!'ߢzyf6Χjֶbⴹy Zw#(W2O+9i\a)˹;OqD׭ 1[Y3:]|>]}kvszFK[mg\`v95G:[Zr_r&k{VsśۘVRCqkASLm_|_/wQ^WU*p \WU*7): <ql$s({8t@{xy<|y>!6X/_~יo:W͵j Mgki|K̛l g̮^usւ;;er^6o>9=^9cŞk8<˩o.} _o/}uş&33riY';inu;\.E>s]\.vvqꆫ-#”ϸx|>Vc7{Ę.ߖ5P=y[Kl ք5ֆϚF{p|[kNy__?n}|6kqrW+U*p \WU*p \WW=P iyl ps&ڞ07ab:\qlu8{3{{.|p]ԭS_S+G-a-NZc/ֵ_=XZ:Nv1rZ74upP=,9/rS\wiCǷVBrR_]@s-bχGȫgy>V;%.̍+0Y88=uܵrh37\Bݚ{e5wqȎ3m=5*n9r֖wu;3 c Ϋ5g{VM].qnkeׯ ү ߋaWVm 98Ys9`֞O}n˷99|մs8竭3њ#ZZ\'nyw-#xNw=5'm廁?P \WU*p \W# 8 |vX`Ρ`%;O<4|y[=+홶Og}Ԅc};?v  -vl07_-ƛ{9|wx0WIܗ'_ \7 \WU*p \WUm*~u׫=tρgi;l<@tȷ1x8v`<<}:h.?@S]qom}Æ7~Eɧj:CYWr#rX><9?nY^qzo2~)ûS߯rcz]t|۾^ۭbҐO~|z=uQXϊՀ0é^gkw=ʵAk吿{hY\zZks*{p/_\+w~reʫVjZo{NŶ1[僵x4(n[K) 1aU}{Mõix9g;?x7o˧[KpS[7aEl= |߫оPu<|“Mp[ՠ~3g6Gu]4حGCWU*p \WU*p \ގC0:\4ٳud9Cj;v9kQקwy<|w^ d|՜Cp5Xw!6,ml^ڵ[o ۧS-W>g=Yi;yCj<϶?6p˾ OOs0Cfc^9+fqV4ȇ>r˧pvќx:m&3͖ R϶p*^'?cNvO#|_/WҐ~n뗭_>Jv$^=+8Ȼlnb6齄  -]SܛY)aWţmTS{og>$i~_rQ_m8j<Py>nlaZSӖG=ˋn{8Psh ԎOcS>6kouN_>ٺa}}k/n[p1^;y{۫;U`߃vrZע \WU*p \WU+8oC0pb=\H"{x<,.gӜK75&x4קY;%=XjXͫ>R3mK9v4ד}j-̉7L]ܺٵis Zw5~S8OvRL|K%%^mچp'ZW+9{t^6\aVW 8'g8ֆ!_}s]sO|~G]ޓuZou>ŗWƋKl8~ҧϋo]/{'g^?˃N)nko-j_bé1&\á6}^VZ[>9->|[]ĖCGX}vpi OWw>n*^Wk^W \WU*p \Wפy`n<C~(k;`\P;`-O`ɓY+ [<]l^5/+_ ZևZ;'/ǎ+^'6Ϯg&r5:^?jٺ47?xS's|noe _=^5I~4(>=1wiTl8 7b'zm1N>o='.,~VY-٭ϛIźѨ.-]`ʻ8saҨ<ˑ}[5ǡv/&bu)-L988o{|-\6zy.SX\Zs^_uʧU,,C~4:lNxZwgE6 v>.nl|䐻1鵾4^MQC}ϝ<$zhpOfc݂G~v{7>t_?ɓbao3.g=ukN۫{] _WU*p \WUT` ;Nk0^T$aEd`=ucyo[Cϭq_bƎw`ϼ.lIܖbo?gl^lqv΋/J|}>vag=-wg|S18[ЫGog?Xw;pw} #}{}cת7I:ᫎ8sDK?S[|߭\ʫZ1[i͍Wg򹐽'[[-/?W3qf({nug żz5r7_Mڷ/'L18/|K~b|SzZ@|a\ng=eq̩z*Q5y5/G9n}V1r/,yj{]_jo-Ngg]z5a2ӟZ]̅~6w{۫׫? ෣ \WU*p \WUŁ=,tLH֜p0ѼZCb!h6B77sY+W>8tx uT0dzr}sL3i\j ՅƆ+[ӉWxۼZ=7b酫K@\igX>;*KbݸoOo/ʗ.skc۹/~}~g~X <[e O5pffn )~rI-వcV7:ǁ]Vnc|V;q wbn.'|W? _5Woߟxlz٭MsZp/ Oxrnq>n϶\%׏NŘϿ>NDn]>OW|jW7N |7^rZx}W/+pU*p \WU*p \ޖ{(`.l8ۃ<$6k0p5e_rV\g1:,-<ĭ.b'7{x^?jq.jdrw,ꋭ][csquZ[jtcTGsO 97l=T%K]~ŖOO>OAsq>j ze '?x[ؾ_>}岾_Y!g9-ZohPX5473㊇L|WMWlŝi9P bpP7 y-~0N͟/{g=Zvg?܋[\8bsnԀGsigckoXhi^p{z*n*pwg~gB*p \WU*p \* Apa1AC]~:w.քQ=b~ oIڳ^uN$Ϧݺ쥀FZǭquzVC."Nڼ̷9_ߺ䣦ƻ/7e&Q9xWzSzA]|+ EPqvt֊r_ou)_qi}wz{1lXg^8 ʩba_ö.j]1M=bib.b9/xYpZ\3? KzO;ǜg`,Y=hb˻gO)?C|# }&>#O.{ؽ޴`ASNySC1;vgjm_}1?AC?vn 9ńu:Znͳ[ϼw|x/*p^\[U*p \WU*p \ޡ^:`ᜇ4tm'6?b0p#jx{||v[ZƟ~,<L૎LKb~uuGn~_ɛ}u{!՞ض&\`UzuNϦ\.hrOϿv)|M.z4\>*r{- Kϰ|Zlxٷf~U})_X)N~^8oܜ=9bg믿9Ĝ,yqW-vĉ15n.^xG;?uټx[̹A5W\˾?c? rds-|ϮWs=g|U}[ ܖ-O>޻X ֗ƚ[Mq]m-|u9l}bmY\`wjm ~S<[ssU཮^W \WU*p \WO9:3>0t&=ht-[{Aqk!ճ`?Yk[x Wa9pk)fxk98]jW Pr&W=Xᨥovs۾j}GG/6_y²r{`=qu]ny 'gkO}_H}?p?7Z D{϶rǜV3r?Yl$MӞX*n+{\ g\U*p \WU*p \_'=<=Db9t`hn} ?{ZAP6/W5'yck0O'KmgYrǿ?c̫?5ދ=^[ywl-]l~8G\[ŵpornƫe\#ufgi7/qظy j+C},v?`f˿y|t5X}ZOO|SzԑW>=r{B7./ kwm"ڈۚEm'c篖]<'h:!_jvjS>x+6ń,'}\6-Ȯ6{K ipz[փ\q \^#+U*p \WU*p \WC={v0>Y].v{Xߡ6y@\m5S?(j†t~˽lnxvQʹ {Fs_~''5fk0Xvq[7}~y y_ Q涶Ů\\'ߍ m˷~>.k}񕷘Ťͩ5UxktI8Samէb6OS{sQˮx\|l|+{ŪzVrƽ]²ԙ\;?݃33G]kë6Q1lz~bki}gLr1QK6Oc,W>o~h7&t;Wwaa}aTnU*p \WU*p \8@t$>;Ws8{x`/^{ 6/ŸKudoc㺼Վ*/o\X`6\W֍iFZU>8-GYLt;RGauKM߹ֻxrڲUS>jֳՠvs],Zq95KG1i9.|۷~OG??A0U^sg V.Z决5j_=9yFgW^@C \=|{8bيùyEpz?[u\ňw-wx_k=wu槻xo(|\=u[r{%D3>;s5>1=, O涭.% IDATmu1ʫv n^BXr|ZNlcZ*L6xyWU5_WU*p \WU*:+|A!^>x ~p 屹7<\x|a/Y=Z\Ϝ[{\Ww#q xѕoNwMNN>k uefs\Xۧˇ)>ͧmӏ[۽Z](ɝ-hЇ /}qAVnr9_g\8W|m!ᓟ8X⦎&œGrl/^|Lz㭭^?lp~~hOʣ1g=[rm_X y[>5^trʳy+3^_={goRޭ'<߾G?ᨷira~mWU*p \WU*p | a\灱{qQux(FG<]q(7ACPw糶RC~e]k̏NټgyL?4W=Nrk-l'?~m'ZwłaAՃw1S!fvGε˻ћzo&u͏ª͇RSׇ?ᗟ닄.ٿ\.\z7>_%jۺ3]o=Y\>qi>u>ڋgc\pdaʻu~Ys,dIxVi(_Fk(bUl.?{>A}@|\6oG1\cLCxl/n)G5ۜ'\}Yjf3Ԡq8N~lo9 ˅7=_N{@Kx2^eO(~18**`WU*p \WU*MnK ~:C=s(uco9|κbp~lֽ\qZ~ӟ}}bm-4G8ǟ_ juȏ?/F>q\&^G.4R|}%ζ{o<䒟Fjyo|kc7/vl/.F'?6'h%^!,93֭=i_i wOקˡz`Z㛮jG塟|●lj>0pڛSzO=Njx/V˧3nq}kg_|t{YMl˓wRK|κ_U?5m4GٽVN|lfz~kuy${\7~495Ug)~qVg>_LU56/=~4ZzԽZyÑ=Įp s <*p \WU*p \Wt@/=,ցJyv('|&/=Ԥ־<7f>ow58 O,WiECL={0傋+az4#?_5[S_1:ϧW</{o*N= iK!㙖j5/yor4K.{`Ǜ]|xz`:y1 \;*p \WU*p \W&o9,m;<̯;1ZWX~r˱ao`xF.kqE &.`5.0CWqbn^|՞/WWqxc~:Nivs=俗R:i Kt)vZaSjXZ89{N=ۺ;;_|_|p=ċVzWꔯZ֞_l=vj{k7'[{|?՗?b1~k ]:o>}gn}wO63ec<\󴩥֖OjMΧ9>®~1xyF}18Y-(' pwo^|~]|L7<7Ri a-|ƨuk|?L:)Zn/d9}MҼ:٬)?>].z_nݾv~<n{x7=nLcܟV \WU*p \W7{8pH.C@| sg9>6t[\ypr(_sxs=UMsgMӲX|p9建9k+ϩqh9OOjg}oac7qgoMƭY{W.mzK3z.&>0O~ƧKeN~އtu-~8S_Y.{i}cNJ)wV7jS=^h yz%i9}֕_\8?ksbwsQNYA|<ݾ g}o?Ԋ|W ^~I}lon֜[O}bhսoO=Szɟך(vyV}U}ogZ:;8:,Zp[շ(s8kcsOڭMu WM]Be%~֠xsbO.]V|SׅU4>Ƿ`ИVa/G[bqxgl-.|Z5{ mg0hDj_07,ӟj.VͿ1wP[&^\{ؓ?^މ; Uw \WU*p \Wo}{{×~K/=.S</|/^>??~^mWq$a=[9p!b`{؁;1ţjk*>y p||iZԃ}&8僱ø!14~W5V^,a.gcc߭vb?ǮE~űYK6bo-.$->L6c g.Yb? |oSk¦ԧtϏajfqm?чgZ\4~SbOmFr9_^>X?]6r1 gs׮/|>g?/?8[z5>Hbz~g??v7^c?연^XYPukz5UW/+p WU*p \WU*p\?/EoG>~~?o[/?~_~~_~_~o//ŗ~u::Y_/^{MXbz/?p/inŸ()y2[/bovկ`>3 cq\x-}pN'6-F~qTc>,~t5/{klF߭m1wv  xH 8KXďK6Y]͗_=Op7렆~V?OӏoL^ū+<8]'sp}惿ⲟsE~Y;זwk4֓36:4.n9nyM{~dm\[^[~g0WCk#Nk|6؍3s4f_v9mO&e7˫k \WU*p \Wo K~w~w_+__믹_? >{>_k_?Sm-.ߡY~;Cb: a_cxULoAn#=<)ܲ_kc>y6^ _ j:Оg9B|wyɧSq8n~˱j/u֞/v:89,]7=Іq;vY^6>}c7y.i}7!wٮVGkPSgƧ.]q->p?/0؅ک N0`X<~3PmS,Żdׁ?ckհsW]ּg5g59D=է6łW34˷^Yiw%p? jMW~r0\ln5(ituص+gv|ݺͳv.N;~[G8'\4~)1tܜtCׇ#='Ϯr n6<<$Z5a}o[l>?uol]0?|b{o}-5|%x3cqsob8q5&jsj@sMNSƮ.t}m^Z5_fg--9|$z L[Cx4:sj%W8.{iNps_;㑧K))…VK߹>;߭SmɺXbv^\y91[87W_mc84Vk5u8y8j;䃱5ַuj)NK潿痭}]-?jȧqT~rW; VuۚyѡXٹ '-q\stX6-ݧjǷVņ~t϶ry >~Xz5g/,I>g]? ֎<5wOb7|i6r۫@{f^*p \WU*p \ |w}/e+j?_ ~>>WwOW?oze6~{ |{9}v{L̯~[Z@}s.;a_Wƪ-[kJdhbqQmcm ;5<}^~?Y g=߼9uoB~|]^g]W3>^mV=eac_=ھ\uj>4ۺw9I|s|wc,-6>m-~뉇&{str8O~ayPּ|Wѿk}[r1WK/?suY j:v5Xng4[įr\^|Ɨ¾xE C-;e9mC߭Ylk~57k+~ \WU*p \Wo:{?/#+~ůxo?~ݯ{}o˯Ua>[Ϟfa(\1{ab9htpP6[yz esYN{ |y|6֏.5SAb;.ꕷZ/&\>~{bsr̝]b5nŰNihͺpg@>lyao݃9[^m,fe?[loj.;n .[o-©3]+yijZSQb #3qnyUCam-a5W[[;?Ҏ֞= IDATNTE'|O'f,w6kExrn|v~c!69|]3c:,gY=YK`y6H>NONa{oˇmuO3儳)m)uܷx/+p/˫k \WU*p \Wo #/O@_/ǫ?˟3w?BO?/wO-巼?a{{~o)gt=?w]8\931'LPY'X'{q->{jZ j& 7i/M1]p:s]s[8? S9u}"|q/i_^ Yh@K|V[1繟߭zey|{Wo_={O%owq4/_1SZ_=W'zԊ /.N5ֺyl=V;^Ƹ'LXIqw >ȇF{YO+|Ǯ=5S^힡wzC-yu N4uO/ZSP~y>˯Fk\5ؽZ ~|jgZro×kp2wq蹼s-zr4~:paYl˱ ;k[̳—xPuxߩrr^}Tݫ/ߜWU*p \WU*px _k^dO?.x~^?G_>Ͻ|㛾7zyo7>~/??6|K=sq;8_{$wvp+.=tl<6]͝9\g?soNa{e_oܾ?~[wėo c ZS;mO~yO=V:dsyvm6~Q}[Džc)fye5hiW[~5>;Og,S΋ Fm C?_M|-m#['?5M>L7/WؽV.~??z(&_rn]ᩉͳ5o߸]c9 ?k&oo &ꘝrȾ4Ǐm>}ȶ}vZ>Q9j.aGwNzmy,^97v5'ե^\l-'; U5_WU*p \W7^oz:rכ/7MvpqPa]~p$ׁ ?gyHRnⰗR-guIu|s) vfc6 տ5nݻ%?9|){`guϡXihckWxH~tguTbXl8onlx6>9-=,fko.uvVܹv]ǫ { v55oղt_ƫfrmy{Ա5GTk럾boz Gl8W&Uo6E8{85^S/GlZ[jluO=gխ刃Zgqv>k@v;Xmu=EmchՅֱy|ů>V-W:O,uWƝ'`-=+r9A1aZ=n?'f9Әik/?翗 =[k+r-?|ͩvmԲil}iIk9μ4-m]7?я||y i:myq^O-k51[7=L^?<4f;awa3/(ym v35nk{)u5gNV^p&zo蠖Z'?X, vy|o?=ĝ-~?o^[~pO[y[3ŧΫ]'; |= ؋_O 1-cvU*p \WU*p \oWC=@;x +}{pp0?1;.h:y&^:7Z●SO ?˽YͿO #ֶ֞4)|Tc\}jW5IdWˎM俘O>Zz_ƭ֐}ujL?1t?5Ͼof⑭ Oӏ]?|+l\M48V,VOfm~4j S?G?]ނ#'ꬕ֮olO߭5\tCֻ;g?\u^_gO:X^ޫꧾmՌ緭|3Wߕ8<4NOu[8J];qG e;׶qOi[˗ϮjCKįlb6ym=8mݵ3噝`}o*^W[?A \WU*p \WU**y``w}ׁE~{Fgp#3yX>g {⇃^"'N;&4֎0{Nx/}1rg/_X"p{Av/LyʻyWsQ̷ J7\Z5y]At֍{k[}v'[ܾ/=\Y=)7ωS.ҽ;,z~k5b3O눏:j7=qm?xao(XX}rVti@':OA11hg:_-Ͼmxdkܳu,/#O>4jgo5piO_ûZn]'ܬ- vyͱ7e6^greoFb ^xzlM\k_}+ex7˳￰յ1OrS{Ic9s۫ޏƷ|>3 \WU*p \WU ȾCT?aCa$2oO.|a:`zfg.~{Z/8y]|bzD峇m,,wquᱶLwAn\䅿|9Oރvlx51_Tϟ˭Vxo}^]KbߜҊ}ښ˞B֞~&K^]l=zksu ~['0pTr:>'k=9# fÑQzEڭc[>/7&ls>9=1kqMkg{:7m_#. 0\>Kkn O}v/kkali: O쮋<8c.OnVָzg6y6 7;nV?5kG[|.068k߼rg?m_^{9V'8 c/z6ԧM}qxȷ&7Zac/j!rXc.\|3|\/|e|Fpm& */ {/kQȾ[+>X7ϴ3 /|Խ7~xx6Οal]oݿ˿Dq^g)Q[1=9^Ov|Ӥ}~C-uܗ3'X[VS|iwfϏ֓jv]s?s'~i}gc9}-paO{YMkcnXZ>iY~av?UཨX*p \WU*p \WK@px0NbN\0߃<OO^rw9Agqws_禎p;w kukw[W֯(`ׁoܳӄzom&8bQ6vyhS0Xε簗ˡgXΙk1un fZ<`l8dTG~~kggm1b8[[^ۜxpZ-V|8צ>8w?Գ~xk(On]ϴ(~qX^7ElkٺvNt׷%!Vۓ[<̝8օ ]~_M\˕O6{9j_ć{wk[diezv{`gyY~N{٣whWkZ-|EMx.u_ _\;U|?gR*p \WU*p \(s@ ǗCW@ǵN>{lԽ9債^=5za1>E-8,ϾknlbW38&mwM9u [[¡>6FXNj|e5ۣ't_{j:54xo=!LٽsO/s0gEL)Vh^]׷A],5ק:O~=ן]=-6NΝc&C&\rig=MW7|{>>)~ Zn=]_Z8Qb,\rֹk ƛ?MҜj2W[#L1]\VݺO[2V\7O<ܵ˹m7冀=?9ZvC~{H?vQjM>qg1|d3okv]vj +E|6[y4n[h^o|9-.=1}N^8 u {ٶo pǛlZv0W.ӳw:â>-.{qŧ}{]~#y?痏~sVc8ȩo,Muw}{WwQ/f'XuoXak'-ꐃ&/3ήk171uֱ>x ا/Q߼WU*p \WU*p&U`Cau1/8 ,^.? 1yq=|9,mμ%za;^\Խif oփ`k, ĞX4xv^^hyy7^gF?z֮۷7E;XnmrXݻl3rl>e @n+ ^rV{ *GY;Sa[j'tZ= RYxVY4'aJ~cg>܇>vn=aXk}^~4_>j #Fq|Oi=[j7f&r.&mZ੾Jlb\5 g֠.:+1^[N'1kۚ o_K0W^Vrȿ{kN|Ws)敏^?*p \WU*p \W;Qlq>b8ͯx(.<6w}/XrO<79P+gW [=Ň &:YAx€?=$_=,nrm|&c;i ^.- Ω_cq&Ol8v9O;\0[kI3w9mg}vbrmq/v1ޚץp]rz5c=k]^j.>{/11O.{yc/{b?>k _ W~pjf>]s{ҳowћ'yͯ_Y>>7\o9}tfm?l+.?{Ac<<Ok>`ɳ\TK36fkw4£P pWnWU*p \WU*p:ph1l{gn:.8lKl#9$rj ;jxj. x.ԗj)eɎw^-:|g_ʹioqg綻lηhȯo!pv^4(&r\45w`R_1YTPFELs:'y|{"{޸I~%ws%}ۥ垴blu G)mT6~~wɾLEQ^mU/.ۮG_<θm'~Ug\)>1evޯN˼-y у$ϱn) IDAT+?z勭z|h5Woqɾ$6%kZx{}ծßde'Uvx{y'pj2V;<[zB uZuǖU{,ի]V٤v\=WZǣ} Wf ˼-ZmգۮGG㯵:c'-r&o{S&kuq6^ˍ:zM/Gq<=q?ܗ{[7c{T?yۓu.hxܗgk!ٖxy\'-\y{1^|￸>?>xyۼZ'ُci Ÿc;i.{ﶥZʆ b+ٳh߃E+D "\;f!   P: B=WOF?m|I\˵N&z9ux۔?WZL'}]g_|u)zz%^ʋj _*#^k='}}\xx֓q6iq>Mᅲq^'^/<^6\=vy̸,yǸ}*s=z~qݞ'vju<;vV*_ek[oS?=u.9uy<׭G)ק#2?_ P@WIx?kh=vk[q^ߟgߏzt7/Ϸӣ[k^<^{}sm?%m?x96N&w/ M'_^ۮ<ޟ1ק<.˗{ݾ../m>zYzT6d_qse7{6{YzTYmdQ:V I~T.=C@@@(Q?q'duDz'Ĝ,Oz~?'xD?) T J(~ey;|?ZQueFѿv=*%~t/z2\. T2˽nezAlC?.Cez;vޗnE2Oh@:6ܗѲ+[=O/+.3nGroVcŏ?oq|/nU˒]mjy<q=/n{c~CM{@>?vN=ןPq}>xyyzuO|!G.[9|UYq6ƥSm;/Gǖ/x_&ˍzlcf?|O\K6ɺ^rez6yzu?^=ےַO.O{yzGϯd~Mro2Py<>v|}n7)y9~ds1:TV<_vyx,=]H~2,c#<~LW-СM8o59   ?AlvBOI$y%yb6~Q^QDeq^>IĬ/<LՈ7O|Y\OoqZץ Q]ˍm.{z[y L%mzm=PjWVNxb_ˏ:kOy:v:zG/+nG쳶Od>xq`J˼Ͼm⏱G[y1.#_}MⲒmNZi}Mzr\W=h(MOy4JJSY*SʣG*Yʫ?=֤6)_H2{Vevu>}yzTqd;~yYj{ڏ_u-me>oʉۡxxqYɺ|r7S;=0q'˓U>u2m玾2^Gi}6d;}\m:weA?އ2/7۱ߧqi}8&Obs݇xlu  :/ #|g̘a?}{ew߅ 0r[!   ۜ9s2'BuM㓕~΁|}IE?ie^VmC|}|B'ko\g|Bї{_|Nۜ^-W9ן{Qe ݤmyq^/nk\o>rm͓u瞪4^ooyIqӷKǮS=??Zdq\o>kmz%k;c?}$=<ݴ\uh[o|y'7g.&[bxy&$qZ{ ?n~kI*_Oy*nS\wVyq[|V/6xb#rգGߗ*/nץL?*7^me6y;Tǚ%nZ.'_'_2H.W;}{LxoIy=^K۷ͶmӶoyx\y>k'덷sP^MUe۷c-lȘN^?w//?k8/OOҥer-.TjժUSNW_|`+=vGZ5 "   Аg}!u" ^UT-aر6zp!AlȐ!!|}ٰaLc7޸t;R-#\;!   P*;wZj2#=Jmڎ3g:c&M֭[j3L,ޮ²x* O<+P86g{_ {TSO ɴzȑ#y] @c@@@Nh 7R?sȬ)ڴiS*gVJ-IJRl973'MVY)a 4] ߬,@]ω+y6C@@@@@@(/h        P%#   ZhagyTuuJI&Nh]+4aӧO|d@@@qYnT˲]v @@@z/ШQ#СC믿B3fۆ\3t^:2       uC)~       PЭfT FW @@@@@@@`piZ      (Zħj@@@@6>c-l[o={3M 38ÖYfw'iil6mXvlȐ!/Fwj۰alWV{t3.zmve]5jdG.XnmfB Y좋.ޠ:<Ϲs' /p0ooC=4W\q XyǦglwmۆcK.W_eJ3gq{kE_6aUVKډ'hҢ(Z{9p Ej^DZ@5 0t5S<   /ӧ&lb͚5&Md^z-袙*@vUW&L'c:aIߞz);vl8/zs_o\s)7k,` k͚?lno_lguxY ={b=>C!ȡ[\5j!Lx)Rޱ}m馶Zkd@Ǫ.tژ1cLJvmz]n/q![yqOQ>Oʟ>F 7ԳgOmq;c/7ƳrGCtnּyLC4]_5!~)L[R!k<:b)ؾNv8\$9>裶k&xJx{IWXa2K|ԣ4j]U4%}v=;J#[}4sQ}EA|p*wX@ p==G@@@J|aW_=L=xaGTRW)>}u>NM6 'L=O)b{gTj+5rS9َxTu{+M{4(=kkw}3[*sOpÇvmgO>azgMǦ.[mP }u`USb<[\dtرVO]믿nkhMp:    Pa??lezp߿5 x1bs=aOų}@] BFP^y~EMgϯRN}|[TF?|ϭP(QnH4 \mG@@@XfeȠ8W_ELuzԨ8͛7ϦMSfe=~_}Ք b\pxVnMd;}Sͭ<=Sѿ}^X'?VZiI<_|{k˼K-&=.v_};#-}YK2&e`4v0]FN{{V뚁e˖e+9@@@@ hjC4N7F )!pzo="=DFyzgL#tƆfϞ}&M %<+w4MǪF)I5\]t5nO>Ğ~i[l5Bw}7?!Z_ (K.~/mFa{Icp)^z\9$˅n\}/׾IGe8Ji|~ͣV3tM(S   NٳgZN8nLu>::lذ0u.hİ)xA):uY'Pu/\kHI#;0PS@[agAO3&OiFMuQ!xֽ{gm 袋}駇[hWc|6[Yt/;3|7Y[8<ֺu碎9;CkN:)>PYxGI&So㏥O/z@@@JU@%˶,GI _ }b=ձcTxjJe6J^MZ*zSVeL:55hР",JOoK͜93,GzttcIZ(*2=3NOO-wrؠXnNjM7 r->|xy 8T)@[*}aAuxQޱ`rKjV lAje,S?Tz~UV]w5L/"շoT:pJO-JOɝJ_u<2xT(!u饗TOx);Hn[ioJ_p~+u㑶EI I_O.~#=+Vvs*͚O>ea   @- h4_.,[۵km1@@@'}!gS\O󖟼w{ r=i @@@@@@4      #P꩹828      ԺZ4@@@@@@(S@ǑR@@@@@@@0t=؉t@@@@@@L]"}@@@@@@@La      S@s(     ?vm2y'С :4od@@ 0t>!#    EhҤ~O؈#2%uQ־}{;3P E  @U4ڶBg Bȃ    A`5ְÇ[mM8>{׬yE"@@ nGG@@@@PwԨQ{ggqm5*C@+@[z    %(ШQ#m׶[N9l%MB@ h纜p]{@@@@jZɓ'7|S'@@@4~U     PO^~emرֵkW2dFtW-@* R5L     @Z`6x`;ìW^v-ĉn@(0R +? IDAT    _SO #2Jv%I'd_|Eȁ       @1Əo^{vm9ٳ'SA2@(@uM\S|hZ@@@@@#[ؼy7n\,D@**@@@@@@(QF莡Y       P55Usu%K       @ 0@@@@@@JWoh       РAn:      Y)ޥo       P!@@@@@@@K%K       P\v F@@@@@@@-z      4<nx#      @b@@@@@@@` 6@@@@@@@h)@@@@@@S@׵=F{@@@@@@@z*zc      T\7c @@@@@@@jh\ eR$       @-0t-S%      S@~U       @`pt@@@@@@r 08 @@@@@@@jTqFe       &FK       P1ڋ      S;n!      @` 膷1      ` 0,F@@@@@@YF׬7!      @ 0wMC@@@@@@@Y      g{!      @+Ɍ      Y`u{       P,>WXG       ,@w#       \,IA@@@@@@jYp-G@@@@@@%@X      ԲZT      Kp$)@@@@@@e@@@@@@(bIR      3g~;zgG}} lGى'hmy;,'`ɶ{ڕW^\e3f̰ .6l34h=#e{vZ߾}34iR<M;찃y6wL?Fm nݺٰa\-չpuR6      PvĈo[~7| ^xGnm ݻd9sf vꩧ p~B;<]|֬Y3;#Oӧ[޽MAe%hJ$+-ܒ);Aw=QY5TEԃ       [`֬Yv9?nm]P`w喳7xÖ_~kۙgi5mƦMf\stM v=ؿ/:t}Q:u=C֪UEvYg",b ު.OZ+?o+6qD[r%3y~'C c=n6u]CM7:wlF8 ]u>apuR6      d4ib-XIY-a_}!0ƍ7ߜuJepdUUy鮻;z0OCY)굦vVP}aqWfS}w2,+-袶>tM%Fה4       Zh!l=/2GW}5w7:?CxzQJSL%X"0'oWXa0o} z֥K{CUT%ƍi+8wu_O'pB>ŚZꪫK/mVٕλؕ. @@@@@@@#<]w5\X^~pO<1;udW\q 0 d}xy7k6|sdT 2']q{mw]tE6cƌ̶nW_ݎ;2ejk_ zuo=󵻲 WV@@@@@@@ q2=pDUQz\T 9233fό=u5N9sf&gرmFeF*ۢE0K'Oۦl       t5UUi)5U'zfr-m„ CJ ᆭQZ@ɫiү-Ú[nC9$0zoYfڦ^{mkݺ}WayV9sGF0+8Geګ.5\3ԫinm#b(R      Pnn$֭[[g}uztѽz ^W4:M8piĭқoi_~]r%!۾}Uyذaaj/LԼys+G Ykm y&MdwqGx)XCXy>V@_~x.a5h4}ꔔ;qDڽGjaӧO|d@@@ST˲N@@@@{jEp.1#      uLpa4@@@@@@%@8 @@@@@@@:&@0       K       @ \vE@@@@@@r %r@@@@@@@ c;"      a9       PױFs@@@@@@@\sɰ@@@@@@c       @.V@@@a ̘1av^#   P\v"]@@@@@@@$@@@@@@@'Ɏ     @) 3ϔRh   Bpt@@(\`̘1袋ZϞ=~l;k}ׅ%k:u*歷 'L~c=BN>d裏עB?~}ѶV[ygSL)l}4i]|6pWn%?~{9s}u/R&L`[n+mgΜi?y֣G}qc)WzGCLv-7ҥtA*"l*K/Զ~{vmK.YR ~e>va:t=6+NR37-x dT]vY8v<@{n]_ڐ!C/l|O?8=?Oֺx㍐O ]}զRue-Ac^nfQ7ߴO?=?\lܹe~{֭[7g}4?==s\z%?gϞm7x/o3CݖJ; @jUpS9   Pz ^|`7tShN\/BСC >$yJGtM3YN4(WloUhlo\?zƌYՇʚ5kN*8we8#uhiZ/k)cB~袋l3]' l+Z7|3cɓCDA_><駓YU+vXCA>>ꨣl޼y_=tAfV\qp/l]w]8&Y I جJc!U$8 |޽^s5Ww)`C6lK>; Wiժ 6^x:V_vi'[jBO@復MRWy}`_֭[ `{o[zkN/~gLt, .vovyިQ'#8"|/hZk>W`VWzoSO=L937u1 +`;O3索3WY P:)@@@Zhܸr)!H:I{{g8Z?I'F(`'?)&dN5RE;ݻwX]k۶.ЬB_C?SVI'^{sFIbmӷo_[C@H'IhIkN;l֬Ya䫒FKjR>3i52N@'=mֶKcWZh:9pL?eQ-nK(GùY^~ax?~u1wRCC=>sӨ7R (pӱcGgg.hѲ?P@ ƍӧ]wJ-Z/TM|Ǜ#G K]+ٙ_ejJꋂk~q.:B[{B&Oԕ,/+ }BgYI5K:4f{Ј[]-Uw߰ZC4ۃUPX3q!`*<+{Dmي,SW6F+7TB~Tv %! 4@@(-[.43 w4I&aNkd_^P5DejT\ NĬJ۷'W=M:զMsdQ!hTF|ĭmۆv&GC2 Fij`R1 Fd)F[iҿpJq:_/@˖-m7\})}^)0цn>c4Žf2(eY&}jTi4.[l2:5 X򵳐>ڙ' uVuvp@@@ztR'a0PRBaO *?Mخ]r4vpFNn*:6G褫N.ӧO`:-M2p(p5ׄ{xz;_NjJR,ӨbT#U]@2]p1DŽ /FPI|ԈuZs5˭4W7C'uNhڣ{1Bս05%mKH6M-F;*W\qi&oϺlq7TW^/>ͽ>34@<\ʣJ{ 8)cnBy}i\r%Sbo>;}+8Fg~д ihNJg G+ H~`}WQAdOU@wĉaFeլ  fD+^{[Bz轪#F(hzt_{귉>u3< f+yW *-o]|opBߺ݅>I& B~kgF{>   @(^yn4ZNz*P٦U{*O=T8Q (Nj*P)@ɑ1Pm86}VpF1,W#I||tQ.WN@#4EF)^~"[ǁ8HN\4CWh'ѝEF.+`g )W+{Y*)HѣG\M`y- PPI[/3]UީSpq\ 4GjTf%PH#(%X1`x{Wx+3翷-9ݿO>˺xBL[xC?]櫫zSm|̗שw}7E\A[(تpb IDAT}io]}IW SN yo]죠:KXA[]|F+Pڤ@>E1B<@sW_ mшdNTh;+J^@~cj#A@@aGRtIBQЪiܸqĭi~/@ 7{ = RS2jNj4)jlmtUZ"]t w*mx(=#408> D-N-9SS lU@DZOCGWAV/ -wԨQ!Q )luAME+YgFi}%8 H4U.1IuܳgF+Ȧif+{U9*u&?=81?> ޫz#4TJ~{9ɺy^11_~QY7^fvhwpU 2Iǻ|x{j -S:zySt^s\'TQaBQNv󤓽 ZkaBe J?d0>8^ ҉wVҳG *-"!ؔ m,睂=o)vۅQEdžN L! U׽u~ִw3UqeB?;}B]Ӟ{^](+#j'|e2`\|ɨct.S@UIc4RbWM~/kzu]EAO4 ]h'գ..Ҩ"1IwgXw2kg!}euvf2@!uc?J@@@4JJ'$ 5\3ܯN jDFOi~>P8M,(!۱cLvHuY4RM'֨%4zM'o5"NSE~'aEtXj^MY6*GMլ`+oq2 N+kF*rL޻2L*OթSrWV̕R΃Cz} ijdMZ 46>vU+{F*hPQ]:֕4F?ޤuN8vqP΄ ” 6*)44]*UTWk@Jۆ$~wL.:? Iǐ3Tc^x뮻>uuJo:MǼOW}}];b̘1efЬ {ON}_i V]<w/!}~ixgoY;@AbM+i4S>/4WǗ ]p h}]cYIu|3Vh4ޫBhkt1n64qnciVͶ4][oK|,O!er@!@n'Z  Ԙ:1TF(0箂Ú+[h8I{,=z3'5 }O'9u7       @QB@@@@@@@ \       E \F A@@@@@@j_pZ      EpQ)@@@@@@}h       PEa@@@@@@@>       @QB@@@@@@@ \       E \F A@@@*ڢ.j}Qv'|]oS[n{(f͚eK|A;m޼yEB@@@(bR   P>S{C^x LAຜJ;st.ڎ   P@² @@@! <3nY׮]ڿo֤I5.(oaw7@@@(UFꞡ]   @- hvqGѣfZug;`g˔vqa|i' ֲ.]ءj#GC:>SgϞ6h yUQlmƮi -$˴ /n0СCCwqN*4"X#կ7܆ f|Mf/h￿1"5Efq[y   @1C2@@@z(ooݻw^V[m5S1c3g AQ%t?c;Ӭqn;;[^뮳[l~m۶mjK.i͛7yoV}x|uYkj|ǎk'tmveYΝ+ `/RfK=P*(L>=sԩYK'hK-Ti4y[m7lLK,D<:Y;k֬5\F*uͶn0Jln:6m]q&!   PLԤ,@@@/ad>cz:um&v 'ؾkgyf*:[rkwe߿mV/uUV ;k5eoF~믿 z &S`kܴךYu/Bajj9JRZ#ϟ$@@@)Ԥ,@@@?i{.[.^dEr^y{1{74Y5}r< EvW?T7tSfѣtq4#s4iR$Ν)'A\QdX   P \ E@@@1cl t#Fz{wyafΕ4US$랻^{m, ըڃ:(L)SOٻk._s׬Y͗ߧy%~˶*,[~e˖{R0X ^sn @@@SթK   @;ﴑ#G.z+WU@vֲO>9;mƌ}g /pm5W^wu}u}Yp }W_vֽ[!Ⱥ +guVw j )St`MkF5jXW鵶U^{m[uU+^{{>m2fϞԺoic   5!@&@@C/B_oh >3!ة8#'|n;l`ɓ^z٭޺=p~5:X#}tbF #tmۆr.0Rxԥŧr}w kD=ua%q;./2} Rn߾=6m4ݻw`7@@@" 4>uJ˛8qu#o&L>}G@@@PpV-I%N@@@*|\UYG@@@@@@JDp      TUpU@@@@@@(%#h       PUUd{@@@@@@@DȎ       @UVG@@g϶)SX&M @@@W`_ZjUW @@@+?Z֭e˖֨QGB@@@HR9s'_q+]Jӱ!   P6m6n֬Y$=C@@@tfJUc[@@@ 0s=ݱt @@@?94@@}wjE@@h_p=~9      3l@@@@@@h       P׳Jw@@@@@@@ n#      @= \v(A@@@@@@+дv#   P>s_lԩ[ڴic+R  T~j3gδ3fToEU(]vֶm[[eUP "P}H@@@7-ܲdzoɓmW.6@@:jwU͚5 7ܰ:iR~~gꪫ< :՛ڬz4,&,A@@@$4w-T*U2ܹ?dCC@@ _p߀6޽{TTO駟Rl\ }Tr=)@@@ hg+Rh # t\M&U6.i틺l}jXͺkr2& @@@* *v@@:'t-~5}~cj@@@@@@z"PAXͺ%"Prm]SkJz@@@(@)hԨQzE1  @(ٳg[f_Uuj#@cY#[l6yOֺupbd(3_ @@@I|'JJ|uM!;mڴVZ@@@gkժywB6cݺ+kbilcN`wZhQckh{㏲W]z~fmM]kZ@@@z(0b;c2=dMsζ6ۘ{1cr!vGfmV#G'|Ү:kڴzm?6nܸ۷~ּy4jmҤ+oQoM:裏 oV_~vL'ߪ . 0`@8gqFA'ܰ˳_MS邀BڼN&j;  P~x~}zy욇_~jzd\e&z]}1sLs=?SO=5\ܩ0$YU5%˸Ϋj /~,~ e\Y2:V,}M챉Z߃O\vMsKyrg)Wn?^~M'`t깔aC@@@*]v4#>5Gֻwo[tE(.8W]飏>+^y[}C5[`[n~RL%RS@"'=_۵lYZӨ+MA@Z{,.y3'1ծi{mƜҋӁL*RuѧnC=묳B #`;XIDATeח[n%>Å[nem}7gF2|uTf}cY;gBvIQMGo>*?/hԤ4;dл:xK[b# +#'b(R   @VMQvaM7mW '6|s2eJ+ :vXI T)xv|!K/NhkImȜ ?=쳶fZzƏ_k`{㦖y޳gO=z+*߂ki*C@*"ߖo߾f=qx֬/_O~t55mW To|u -vqٚf.{O>v[lEEk%owFI&ߢ믿~d޼yv=؄ m:(СChs\Kr4TѣG:߿ѹv :6`24mܸ㎶{~9eOǀ3Y㖶PE)k&Vϰ}XǖZ;O`MM&^ѾEkYeYeFWUe%\5A@@(X@S`sy?5gpn:u/N\x6w\;餓v)*ONsw>UUwmK@3  Tӎ?9bt#&/#H\bPQ(Hm $lkh#CD;r{n_ﻯߓ49s?sQCA8dXj,X@V^?cE )Fmҥ:ݝwީ)S͛[\p(s5ȃ>C#eg^d=* <|Ax}mYp}CMsߠA ? @Ot ~&G7CW|GWV{ >i>9sLo߾ssssj_x^ÈB `D5» 2ԩS2|7ސ^>b]TBhvӧM1jmDye^3"WOT$m<'un ɍK7pqz8nb 9$IWQyK$@$@$@$@$@݄90o^^^ҧwnuu@;0g9dNGdM6ijjjH$`YRR=K ^XWp5K}x,#5)X}]]̛7M2?nwAxl0ڊZցG= пSk{ʏl[p0,$@$@$@$MŶϏCK˩rlDV/Ob77! "V|w:igyF~lƖؓ\> wQ{ 2DVXa ^0_j- : MV8 )1*awZym%>lc[o-;D\L$֍2Pk)UqeX]{W_ȴWwImI" 7W{13s)ryXFOL \yp耰8k+kQΟ?B5@5:s x -&ܻM2}G^N]|!m{0gXC3w +s~VxΈɍ)=<)؆HHH N>'CKsy%X%In\g! AHRaeG㹋K| k-#M Ab!sӾ;vL"=Q:7cw*5 #uuzXr$IX;ړ^U`]7JXGfdO}%֬1)̏0r 3,VcɞHHHHHHطovn+IwB-{+kx#O&g;d͚5:$+"mn5"4d*ofs090uI&`w0'a΁c toMGFGHcs0Ѕ)sk(gCd?5Ĕȍm~ 7 xS=4?e֭:jBBcULxg")_g^nuAa$~~JKhV{+ 'r^/&Pa(Wy#4p4'*UCdo{)|z4u6 ǛIHHHHH@E4vU fB7\{p3S80 rIHHH;*w*y od,391OIHHHHH@'Cxp :Aтn:Yt>A>-?#E8%KȄ >g,q0vZ1ϒq/O=>ABIϚ5K~r&!Í:04~}ĈwvȉfV @HlTP~~TfN*cGVn?`T:ة1ﲲ2y뭷lSM!GFMMM:ȑ#gF7ne˖iCɓ'˽ޫ;ͨQRF ^'|RƎKaI~ K2c|%dDE<07R}zI\tYu֑ /B6~=}>ST׽l^qZ9޽[Fc /,$@$@$@$@$@$@Tq,xs_K5f-EN({='o80h7oqyq 7(=@֞$@$@$@$ ,Zt0.JjF~z:JVzwQN"      }Pb{@=IHHHٷo @1.Jnll{Juuu #%%%~yuS6"      6U=IHHHJ 0p=F6Э[JKKK֞60<!֞#28cEpX9 @;V] LЀ;  jAקOIENDB`nxtomomill-v2.0.1/doc/tutorials/img/tomo_00068_nxtomo_entry.png000066400000000000000000020400231511430602400245140ustar00rootroot00000000000000PNG  IHDRzv:\"sBIT|dtEXtSoftwaregnome-screenshot> IDATxyX' ;k#nj[i暹}4-w-=3\37Pr+(q]s=s̙y`H`H: I4[ezEDDDDDDDDDDDDD۷z ezEDDDDDDDDDDDDD%Ju """"""""""""""YfMDDDDDDDDDDDDDD$ l4qH:b Y^:~ﴎCDDDDDDDDDDDDDD;i$nIgIgIgIgIg=y$nnn.v޽{jՊpu?R<ܸq3gҡC֭˧~J@@@뉉!$$hcݓ͍G>-""%K Ç3|DUfMVZR:qٲe̙b]1͘L&fs{ՋzѮ];^zL8>}$2ep!/ǗXʕXb۷ *Xl;~8!!!FU?‰rʑ?x}/Pr0h v튕5Ã:PR%*Vm%:t`ʕߟ 2vEǎɒ% SNM0EDDDDDDDDDDDD;zqssc޽,3w\6lH֭Iv[nW hժ-2ֵhтkSZ5zی7(3gEƌfee;޽{-ƍtL2yR~}.\@lٌd~ɛ7Sfƌî]۷/d˖- <d[p!Ό7VZѽ{w<>M4VZ|W4lؐY&|xxxXOرcPrxyVdʔ:O?O8ٳgٴiqwwL2^ٳgӫW/fΜI Ȝ9s '^͛7-ЧO5jĜ9sȚ5+]t!444'L@tt4GA)Hr͝:u={vÇٵkO6<૯u/ϟ| nϑ#/aaa0x`ڵkΝcڵ9bŊPHu:u˗s raT1BwޡW^xzzr!O58|ȃZ䄝]ō!o޼lݺGw^4iBv=ztKiР'44ʖ-`OƌС'{*U мysKѢE_f|74n8YǕXvvvʕӨQ#mq}V,/=[paBCC \r)Z/_~%K._~278t>nݲT\QQQ YP!;˗qwwƍ;vE&:v[[[<<>>+W.^Zښzq z;wxqN>7NNNHz'OJҥ۷/~!#G$w̙3&Vl٘1czŋԩSL2q)&Owƚ2e Δ/_:Ą ͛!O}U Aqa뇝9se>pGח[n1qD-[RI 7dW_}ży;v,ݺucɒ%t֍]vak29ڵk}vg7|Ì3 M6OoӦ O?ƍlٲx߮~~~8qÇbfϞM52e sLJq%:fWWWoƌC/IΜ9ر#+Vdɒ-})R%JG$jՊ\]])Zh'"""""ٜ!H S7={H .uʼ*d2qe)VX)"""""""""""""%zHXX ʊwww.\"A#zEDDDDDD^?~#oq 2S9d15KBBBN$/@LL )6R4{-2p "n-/X+d?5kgY tԉQ"""""hD'7yr>\59Xg w,<0=X:iVDE!Sgܾ})رc_6b]v͛7[$z7l@.]8q"seʔ)jGDDDDDDDDDDDDDҧIxL~LvTv+BP0Dq(<;ndq͈lƌE>[nQX1ݻ`L&֭KLTT[~|Ims`de柲o>ƍK=zD>}}m}={v2c…l߾yҺuk5jBuL&~vɁ(^8͛7uXYYC֮]˦Mr ={DzTRSGƍٲe oVe2غu+#Gl6c2sx5J,ѣGG/u8QVr~#nRc3zg+ńm4p"Ȓ!=}v .l$z+///}b77nܘe˖1tP*UDN8z=**o-ZPB ?W^dɒTPGGGΝ;۷qttd 8N8uP9;vq_>aiӆ5k2c :!)-ZhѢE-ZRn+#z) d88G5f_6*gR@=/ Gq\&aÆoܸapwwk׮Fl#(Zlɂ ҥK#"""""_;t7 cС8::b69s uaܸq4oޜ_ooovIʕ1̈́~zt邝ͣdɒ{t֍(>s=zĨQX~=SLatؑ5kаaCvIl7ȕ+Yd87xٳg oFΝ-ȑ7ob6]޽{[.+V`˖-ҦMڷo̘1.]pׯς 9r$ʕ{wl?zj:wL׮]Yx1 `͚54jԈ6m0~x/^ٳg1bwՕs/c6{gx<2ĉ*U qaÆ|4oޜ/ҬY3vaٴi 8ƍGLL C Iz10ydtBƌquuqƌ3x?z聃/_Dʊ2epljL9sZ!}f͚u;vƍcccc}(g4k֌]vѲeK8@~gcJ9rPlY>#rkРAd"""""%;kœ` bT(c `z0L GGGzɥKfmmÇ)Ɠ^/o>Μ9Ctt4K.ޞ֭[5kVL&Fb#JDDDDD$> .]yw-VVnݺxG9fΜbׯ_Ç\zWRxqc=>>>:u*#]\\1L?x '{dʔ,5L\~Yb6U #F`Ĉu}t+++}rApp0+VVZ|wTR%~޿LΜ9:u*ڵQFtxرc#SwV\ɲe,b8pSFa`` {~J^mޞݻi&|}}9x j2Fȑ{=UOhh(yl6dGrk!8z19}vvvϟߘYfٓ[n닷19ѹ=I\Bdd$ժUI'G=bwhCDW@}CXd6byx'|DUܦ L4 R`AmK,a„ FJqww_eÆ DEE1i$\]]?2\D IDATǎ`ѢEbʐ!NNNw'=oGeggGƌ_Μ9 Ν;_Nʕ&C xyyqe2w%22x뭷Ȑ!E%KIpp0 4|[o1yd6m̙3}ln:n9~Ѥk׎ӧO[, Jq7oCF} 0iW`` tE7oZlOɺΞ=;oFRjUر#L2I/VRG>2eM#'"""""d_VSDUU#l$ïGao$ɭ$j)GrWhӦ ק~94ib|K6\^ f͚EٲeiРxٳgST)VX{GXXXCDDDDD,f"ۛ_բ܉'hРAϏ'O4pA~DS|yϮ]u|ƺm۲zjcZ\o߳[WNXnƺ[n+W.gn7nښ… #F"""42$$>c-Ǎ/*UX[oq!ț7ľ61^hѢ4jԈe˖n:cnZjgOc]`` {f͚D|vؼy4OWWɓ4nW_|Ck$***Qu\zg Oy_lEyf:wlϑ#6>kÂȳ|-[ߧJ];wgϞB~3LСCTPL֬Yɟ??6m"::&LݝѣGS^=Kw?C ?>{ooŋٷo2dLJVZ1zh.] UT1ɓȚ5kc񄇇具wbgP_\VVVL4e˖Gƌi׮۷(WL6n=z4իW￧hѢ)^׭[8p3fHO\زe 'NٙFѽ{w2e g Wsf͚5̜9M6QN:udL75k k֬aڴi/^ &zA^g5d`1c:_~+\[[[9B:uR^I-9pM4w۪UĪ<_-ڵkeپ};F_}| Ç=u #z%q\\\0L"u`2pqqIDDDDDDR[j J0ҽ;wgM֡zjt'y(h___%yEDDDD$]74˗qwwO/&K."H˗-N .\` :4MHnݺ?%zHѢE … H...ZHDDDDDl6uʬ$oZwB{nǑX@UDDDDDIJbŊu"""""""""""""YyϴADDDDDDDDDDDDDD7LTATEDDDDDDuVDDDDDDk׮ZqH:q9/ax5_$tHJ۹sgcbb^r$"""""""ڒկE۬0z"&$y?!H SWDDIWդ~ҵ""""""""""/*I^I{o?ƿkD$FZ&$yQJH()jQ/XVDDDDDDDDDEY˖-?-00]D,]|||5jTφitܙDƒf!C0gΜ1 2Ģ/\&N!gL&~}ʗ/7ÇСCܾ}8RIk˖-|n{gҬYرcJ'"""""""""d6l۶mr".PK.l6s~w|||:t(~)/dřP U#FUVO>{һwo[@,Y^x^gNfJ.3cȑOE^OO> 0Rvm^z/=6mЦM}'[ӧOOr/[ll%K4PlY ,TYI}:22QFR~}c{}5>Lt]~5._|L4 &pȑ˝9sqQ~}<<<3f 7o4Ϛ5 &0ydj֬IVظq#.]bرT^S\ƺvFÃnݺiKW `ƺ3k,֭K-Xn111}ѽ{w~mzڵkmQQQXVZQreF۷sٳɓ'YdERm֬YL:3gRvmZl͛GժU9s ]vё 6н{wK?LdԨQϚ5?S퍯/+dD\b)S߿?M4aժU1zɓ'-ٛ4< 22_ڵ+˗磏>ƶ]v1h <==_>'22;w;wno{Gʕ5jkA}:L͛7ӭ[x'-ZRJ_EDDDDDDDD$$)wTHlҢlٲ?#Gr4\v ''' ùpd2e\]]cǎ4jԈE2ydʔ)C͹tf۷oӠA .ܹs_>M6?x*N-ZRBvAdd$ 0[n1aüyXnfǏӠAY`5"k֬F]ǏgӦMٓ &₽=fgHF(UÇ@ƍGTTcǎaÆ{f-Zog}?իW1bM6eĉܽ{ pmqvvfiӆΝ;{~={vZiѢQnر.]iӦ7o^j׮1}Օ3`+Ν;g'ȑ#mۖ3grM @TTfgRdI>3:vȒ%KXxhh(3f̠yL<+Wжm["""w}իiӦMM۰aGf.\8͏E-Gn;.111{,Zc22111ԪUZj˗Zj#GL&5jԠG㑀۶mJ*m *0|9B|Xf իWK.+W?m۶QD5111O=  PΟ?/™3gȘ1#xyxw9{,I&dʔ *9s/3g6ذak׶+iӦ <///""">>|x*UpA;3>+{LJ1cpy .Ç1TT(7dٳgYjFҳ7O>ul2>#իG=QF١CRR%VW\ݝ^zY֭[G޽njCʕ(U+VdԫWO} J3x&Ŗ4h+Vnb{SNlڴ~;EDDDDDDDD$xw]NݺuY&vvvƶe޽Ϝ;w[θ'O666lgʕ,[""YE[;_3gN2ٳgl6Iܹiڴ)-[VZ+V sQtirT;3gܸ->-[#FD,Y,1cz*W^xF{{{|||8uTϳ\rq6%?^xqԩþ}(T1oƌVVV;ܹSD_lllˋ?_~Ν;ӭ[7&Nﵓ9sfcoή]tgϞX],Y… WTRO}uqFĉY[DDDDDDDD5DTT#F0fƎkmʕ\QFѥKn޼IƍYS묭oiWWWڵk'|bQ&vcǎQvmqrr"s۷ 2ebyuV=ʞ={hҤ ڵc̘1RR3%)#vvvv|sow]5j [lZw}9ruH"""""""""^xDo|Pömۘ5kE[KjF%fҥ:u*$Ǟ'y̙C@@fB JPPʕ^*WLʕX"-[o߾˗?ׯS72ÇFbx{{hd}Qˇ'N wiݺuϳQr=ބ <۷o_+ʗ/Ob,ܼy⳿?ʕӳ7OLgʔ,Y`ooo w?Ӛ&wxhh(K}J35}u}1eڶmСC?>)y("""""""""RlD/<.pĉ4jb}RXv-Eޞ%K$ƍxb>Cz쉝Gv,X0Hⅇsf3?`ܹ,YEPtiˇ~ȑ#ɝ;7'9sFݻST)ؽ{7˗Օ*UPZ5FA߾}رct)Iq8pѣG˭[8q4UTa֭(Q''')b+cƌaСDDD'Oo;K$K,ٓ)STrʔ)8;;S|y9t&L}nݢ]v4k֌… ȕ+W>}:~)=gggʕ+Dze0L0{ 6PÚ5k\2UTwIxx8ź9sRP3k֬|W4i҄S2zhRDDDDDDDDD$Iw1c1w<|O>EҲeK;਷Ĵ)S&V^Ǐ'<<///jժI"$W^nݺ888PfMʕ+ǎ;(TE1cưvZ,YB`` +Vcǎf\\\x"k֬!,, oooƆŋ70b2fH͚5yaFzɔ)Çt,]*Ue>C&OL֭ ~=zڵk vڬZ_JJ=^zu.\H՟ަMN>ʕ+^:[l1ޥg=/ >;wuVP3g4|̟?ӧ3x`TB>}3g>]wҰaC>3Wϳ 6X7ez~ ]7O>)t!ՎMDDDDDDDDDRՐ!ͣGynA*WBW5Ν3F'ѣu()&91qDBCC}RJ6E^&W"""""X7֟d2>[[[Sp2kܹ{lYs$pL&Ωp*ٹs'Vɓԯ_#1߻w[/_>ڵkG&M^.ƍٱcdɒlْceeŃXz5ׯԪU>}PdT="""""~PP!~)mŋ_J{"/+W_N *H"PXYYϻ%)SVةbӦMlOOOƎ˹shٲ%F0ڶmٳg9r$իWiӦ\2u0d>S3g-[ҥKXYY0i$V\I.]8q">A\{ammMLLKk3&&kk6˦JDDDDDK_Vlll(\J=GOI/CDDGfnjժQR%lBΝݻwٰa2eޞO?MvҥK)PS13~xmF!o޼=z|yMK1ff̓;LvD;wW?;: o7[{B!!Ђ{ދT#R,H3HS(* (RE"()J @$!u|DVք >93w}g63{o@NhӦ ιyyΝ;He9.8T :b L&֮]Ktttʕ+G޽{@ !BR\R7䭷࣏>b̘1̙3HϟϞ={W#FqRJ-Zڵk9}4۷W^ìYӧO^~e˰Њm6K"##i۶- ˗q_8}4m۶,sww',,+WZ˗/ uR IDATg$/@ǎy8z(Zw] ,`ȑ9&y!+ũS,x)[l=/f͚5;v:ꫯu4hǏJ*|ر|3!B!(hZI-]YܢbZ9aԺ\Wc_.]N#88@puu%99_HHH`ڵtܹXWsնgA1](gSPõkXf #FȵiԨQ'BodD޽{q7͛7W=FCV/ .UTݲy>>9::o*V{go>jU7n.EQصk:ubɒ%_$ @XXz{{{>KTT]ve޼yL:w_YbCeذa,XÇazy7Xb;JW!Btns;nyZ=yyJ:[~a UjjH*S&NQ }=V>*C09ج^{sff&ΝܹsynNǎQm?dҥKV*Wlv$-3sQjAbtk׮YjF!d$[Z5"""Zĉ]ɘͽlzRLz=jղ,3g:#GUVܺu{5kXʍ?WѰi&N?5/Nxx8SL!55 &xbڴi@ӦMi޼9W?bbbr|ՕDRRR0L$&&dUFby 6?ueddÙ4i//_fĉܹs1cйsg˼y7o|G9|W_CwwwԩիW f4i҄Kzj>5j$B!(yYfL&bHVx':&GOR=,Syx">z`mqq49=hb6NL?fVQ.m$n]+T%9^b|lhȼ^~/[T ADi`H!%%%z(nUp٬]vq̙BovIm]6oa]xf=uf2 ؼ-(ڹQ:u۷oۣP w$ٲԩSlٳIHHॗ^B[;r?Çۛϳm6¨Tzi!B[ݓ7'NJMz{{9+y{dggG1͉hdjy֭ׯ9nwiffe'׮]###WE5,ҥ 'Nw|\pEQ)))l9>wܱl6s5KԥdL6SZ(¸q9r$*gy???HHH`4hЀ 6ЬY|훻U|Gу={9B!4/{EQ(ZшT+ukN᫕+Ͷf2.L|0,!ˁ:t@ӦMIJJb֭DFFZW^T\H֮]Kjj*_|ڵ+P\q;1 ЖUz$]|ޅ:og=IĉddgʘΣpP^O%؝;*- wP~2[uw(Lf͚ƍo9-S&릿^!BQi FT`Ô o{- ^?oHBBk:\}lݺy͚5<3ћl(Vs^|\ gx3fҤ)L%h& NĴeh&j7Pѵ5Tws/oEGH*r(wyJwߢ:0 L+:<bLgȡ-Kz8imN~&z333`駟Xh>eyDDO?eXݐvEr,eiժ?s1D6tľAXٝ^O*d-t#`DYk])]5еkWoNPPP :-BQl:doaIRni߾㮣GңGKׂ ё_ղh4gԩz6lHPP۶m,KMM믿f cٲe$''[޽PիWʊ+nnܸ? Y7VR Vu$ m^~e"""ؿ?+WB!Bw!=Խv|^S۷'>>.]еkWbcci<seFQ#óSN+>+cwٳghhذ!-[dذa 6 wwwΞ={LJf=O 8LZFd%N/]ȵk44ZTj5iiiIɉfm;\֭[qvV]V3d"##ȑ#V/]̌3 ٳg>}%Yl41L4i$߿%ԩSVZ•]믴lҪ[prrk׮9'BQ*=+h"7/?8.\`4mڔ мys~'>S.]JV8p@~/$..Ν;yf\\\?|\e˖̭+Vbҥt҅ٳg[ꁬ׭[LJ~ɓY&_u9lS7ofܹL4`f̘An݀uW^͒%KX`׮]QF,[m`ooOݺu ױڼy3رchZv?~<|%Y-B!(rU{oϞHOYx fE m:(fHM =-7|?;wEt\IOOg޽6?yh:f)Uy%Z.I-5:ݙQYڬ݁izC4~vWoG*MG iTCIGuDs 2!d38G)Pԧ1==mZ6r8{yyl2:uġC5j'N`|4j&ߏlN}ndK<&:IN(v9bcccǎ,\P2331bNNN,Y ^z%݉aԩٳXνSz Wl͛Ԭi un+ j|1\pIzu)דh -= ^ɑ2~~T*BSw=䓅.99[orjYjׯ筷/a_EN?,z=ڵ-ZPR%HOO'!!BT&zϞ=k5E@@ёS2u-ZE9n폒Zje[֡CeA1hР=q=X6mdIՋ^zȚvƌy>9s&3gq/Z띜XfsN0((ȲVZVOT*͛kB!B4mh>/z{{nR6ԪU WWWjРcL2OOOpa69uAWWaCRVӉExx>!!Ue6lX7n(jՊ(ЅUN4P?ydyРA4k򰷢(竮v>f~e?U;?Doa۩&cu7:w/Nˑ#3Z.ƛ싼3g0LF@׮] ~^vIIIc0hԨh4݆PĹM~M#qFKڻ<==?~|h4Z%nHLLʕ+fI !HU1XaAWƿB!B{{3gPF5uquw^`2(8zcR9W?VVNAc!Ͷd2VsV$/\$///ƏϻkIMu~ٳg1;^...t:C5tT*4M֜Z- /B)UC7תYZ5ktB!B!rDEʲ^&g/*prv&.&ќ;qW$RܡFb(CB0M׳7z=&MbuV -տT*+W,KMML2tܙYfg[y)q-Ow@&'0if:#ncmy֛XTi7}Q?qMyV◎YsͪT:lNB^W u1 ПFa>lHպLO<ƍO0l޼ٲ$&<< XC7.[իWGRݷLU>tP^~˩T*W^Qszࣰ snw@w-N.{ 2kѮCgv*OE͛8;;˗ ,N{lC7=zzz2n8{=ƍg_ѐZ$pSSStFdLJ KiBQJ !B!w)Pj\iF|ʔHs~oD5Q9h(P fl۶-_r5 k֬cǎEnG_?23ҹxw4g ey7hLG0OFmι:͋*$v>߾x]תCs[ V$ ?BǓLT+_;?>-b̙:t(gTskLDw*C0睇!!!;w.cС޷UjL֮h6:||[)f`l|@[9S_u(K&9G^/r}ɭ{ɍ{]ƍ/J*h,SBB&==tFk9{,ߘ;Z!(M$+B!l5ej!Tmύy][÷INJ")1B)@`t6MBяc@  FN3Oݺu\-9Uxm IDATWff&&{ .Pϯg}6[o!Cs Wtt4 .ꝯRxg նJ«B5ʆ6lhc\|SNgÁ ]E9$+,~7[W߷5ٹc6n+9yd.-7nlٙ^z D\\\/)SLuԃ}WլYM2tP$p`` }K?0#BE"B!Prc𕊢X/+|"GbmRөR&z;}cΏ;wSOY>~zڴiSRh0`|e<U8,ǿzrݭ'J%l>fNJTĠ: ;spw>(oD~nfOϮt Ʀoh<U-EmY\miXf ~!ތ X"*.s'9z3 D^qӭ[76md%_y˹V%((K.н{wܮ-/&U?dn_9TEI&P[%p.h146Q}5o۬XFo( L< Mbb"G77nJݺu)WcǎߟN:A+T*5kﻛnnnc"B:{v qV\ax?B!B`}qqtrbQܼ?sNENر#۶m/$/8QUݒ3\#|-n`?TJV1C'+a1MHOkf%yhUqJTNGb8&fM)SB:PrHh\1~ Sܛ4+ {{{ d\2p@}?[|5kv"%%EQ9sfs޺u Țx̙W~V#aRaz sn+G =7J -L`z֧eD]8OR՚TbUVh.7nMVK݁^;u綍7YfdQ=wB!P'zqqWiTk GgJ!B!GiӦ IRѮ{giSM:k,~Gt:8th )kQJE'zq!&_'>-%yPkl~]uPy/\!x#&1zUT΃!Q|뢔,'C.9z33ФǡKHsp怀Ν?([bF#AAADGG3w\lGI+mCqvݩ,lȑ#7S -Cg3py :͙3gU3gm֬uĉ6K;!B<ԉ^ل!#jѐ0L9n]6*u:k\w~ܹ'NЩS'^~e|}}ܾ}e߿?=z(ֺ%c :ݻwӪU+uΝZj\rƍǞ={quu[lcǎȑ#9&[_BQzLBְG bm&QzSUr)YKfR犡('vpǮl0e%3o]jݫx4"hqBso^'4Q߹ʳ SO=E\\'Ns<V޺u+̜9j- 4K9r fo3u 5{sFy;MN 䇳m8{,{m۶ ><ײ:?>ؤ}'Bq?unO ۷ӽ{wL&jHu_{ѫW/-[O<ƍ 99~QL&N˗ٳ'_~QKQƎˮ];wJb„ lڴO> &p^}U>$y=!B}iQԹǎV-9K8:8:ু+ŭ2MFuBVY!+wnkZ'wK >>jWg-#0baRS^+nYz 9jժ1rH/^c֯_O֭mr$Đ@Fm$Rn9V,I,SoǑtYEamЉk70᭍|3 uoZB ݽfju X"f&CZwB!ozf^*<|,I((b2RSS֭[ӷoB!D`/{e%H0gdرBc+j=F3Dc=ɓ'Y~==.] 88.]гgOt荞ƍqH1/h4S^+6JZK:!BQJ3s|ݛ#p2eH7odF6lG}d }sI۵k#<ɓyꩧÖ{m۶ԪUhUXb20L۷@2~~~ܸqu !JzqrOIV3eN8A߾}TUuk߾=m۶eǎ{}:4B!BDkI$yKS B!BүTV9I3q|աB1#O ~Ko3*Wɓ-^}U7n dSW^%88 *PVlsUnݺ;Z>lٲ֭[W_1dj5111/_>[$&&d"11'''2PPKQzlܸO?4ʕ+G/hРպ}{g1cFBղcFB!0s,a6d2CTa6qss+u(WB!B(=z8qSw;WRJ~`Ռ>עco8w KWmԙ?rTY"**Tj׮+gA6ܹs'Ϙܬ>?,^$.\w}g^à .dJIIggg˜ll6s5|||l^pqqaΜ97.[O{ܹӥK&OLO׮];._1 ,Y>}YB!J'wwwPF@^pe$B,{ 6~T4iRBQBBB8w.\ 11憻;!!!=!49B!PwPW/ݿ%-=}OP iT D!DU*r9 `._Lv md5`4t1uq|M˺ Ķmx駁_kf)|#pqq`݄R^=%(}Fƍyw5k<3w\ FfͨZ<ȑ#/TB!(B!BD/sNXX111;EY'KQӪU+vb;vr?Slذp5ly~BB!)B!B>[jŤ0iP˲-Z޷nZ?6ᛓz /_oMٳgU˗/gܹ+W>($i*UhԨV.,, WWWVZҥKҥ g϶dݺu|L<5k_gg˺O˖-Ŀucƌ!""*jGYt)Ç=NG>}(_<ժU+N !¦ZnMxx8AAAE!B!B!B!DSt޽{Y~}{Gu^oٛMaVpU.:t^zGa2h޼9W^ 666m… ֭:uԩSoԨQYf1x`.\g}fU'|BOAt֍O>d BFFFB!B!B!l6vZNyYbsO?J7u%ܹsտc @ժU߿?۷oVDDDТE ƍo ?ѣZjDDDoˋ'E!ֵa#R/>zh|IZhsXb&L૯u 6 r);jժŨQdʕ+eÆ o߾ԨQ{ &Ю]; TZ5_1 !B!B!BQ\L&ƍcΝ?>}pM._$]bb"j[Vdb͚554ibUcĈL6s:tΝ;iӦ ׏2e0qD._LϞ=/8p9Hz{{3qDd$wQ5(&CV ~Z-]v?~%Kh"r*lڴ۷sE"##s/WWW-HII)pB!B!B!BΝ;Yt)'O$0002 ԪUMf[wV\ڵk4iRVbذa ::puvaI~w^gڴiggg[n'n^^^J4Y]x R.,szxxƕ+W^ ܹ 2mڴ#n9-B!B!B!,X#G7 pMKYn͛7GΞرcdffYDGFFRBK˗3p@6:vѣG gI$yTjbh4Z+޽{_>>(Ņ[^ܺu/ҨQ#*UKB!B!B!0ڵ`,YBϞ=yXpe]IP=z?&11RON ޻^z%?N޽ٲe cƌˋ~YCG۷/[7nz ?1t K֠`=_9""///پ};ǎc޼y<|'9_~VÇԩ*T~,\۷={vM!B!B!B֭[$&&2|p&M /˗8q"wa̘1L&,X@ٲeqvvŋ̙3ݻwb z}m2rHٲe vLDbb"NNNVi4BBBHHH(ϧ\(Tvv0ϛM>5(ڷoϮ]w̟?۲eKEΎ˗3c FA3f${B!B!B!ĿiӘ:ue(7#Gboo-{1֭Kݺu=z4͛7ϵٳgm6~|||Xz5ݺucټ wܹclڵk\^IDo>i1L(&b6*Fkm۶YooFuժUcV|5 X-+[leB!B!B!BGGGj׮Vkg %55ʖ-m5jyÄ ذa`ܸqx{{3x`뇧'-[ŋVőQvSs?c2eb622v7?dB!B!B!B:t(+VQ{ ͱ+Wεnd7((j}XX˖-#99Rf݄Z%EzSHy51qh;GgwiԡB!B!B!Էo_,Y?30bbb;v,-޾$$zٝƝ/0B!B!B!Oa̝;I&̌3֭*Tu|7:u:usOXzӇK.믿2n8"""W֭ԩSԪUg}>}Xbdݺu|L<5k_Ӯ]@A5嗕ogCѱcB!Bضm5ZvСl &!gs?I$!fDCXI5Zj^Z[PjƨU|K_j|5j=E B2m4x>9:Gu>ȫΝ;ϤG?_y(+"""""""""""""QWDDDDDDDDDDDDD%@K&[f@D IDAT N81*UeC71ӇiӦ=-[F6mMx\\u=DFFr޽g֟{9r$/_ݝ>}лwoܸ|2g={Ů]RIݺu:tN!K0\ дiSCM>}(W7f@b FVZE˖-dIm۶ݻtl,\~///&MDdd$,;v, ݻN:ၿ?.]2kצO>}""""""""""ʟ#( JGO57 6zf{ԩcڭMpp0ׯToĈ͛0 >S+f ~ ;WV5jrJz聯ooo_3ŋ믿cǎ4hЀEѰaC83ӦM3j}U\rӧOӼys<==͎?~'B"E O<|禺kfʔ)S^$I}ċU͛GٟOɖ><`͚5Y>ݻwSlW`A8s aaaؘɟ?s[[[_άN9L?-[???pssݻwOrޣiӦ/0iҤgkݻ7K,1}%BBBtwښFFnʆ 8y$N0Nb+V Uƍ裏صk˖-cΝjJ*EǎywLA*U`iiɟIMAf8 ċP-_| 򊈈d @Z7[02>+<::t]~Ճ=~!ƍ3+C6<9rP`A Bf[S >z۶mGGG-[ݻwIHHHiԨgϦiӦy}/phib gH ~紶N::t~;պEƆxӓcǒ'O'ii0xAXXXиqc>>1_veRlYZhA…9|0...?~$zW*UD˖-zDDDDDDDDDD^EJ, Х{SP!*U籱᭷2gggѣyf:-Z0ݻ7ٳggꚮ񸹹1oWΩSXd  0qppgϞ|gEC@@RJtЁ3gкuk;t1>###9y)AΜ9)Yd HrSIѣG9z(?={E"EXv-ݣE/_KRtio{~tTPƍG^Օ͛7 dߟ9sRD,]xjժaooρx,X:uзo_`tܙ%JqF-Z+6l07wīGr|DDDDDDDDDD$PWhܸ1+V`ѢE|T\9}nٳ',ZЪU+:t`:;wn֯_Ϝ9s2dԪU+CWJhтsѵkWU__ŭ[_tiy8x gϦcǎ˗ tz!1ҥK;w.'UTsΩQAAA|,^???j֬c֬YY6ׯF///jԨ,^ӧ6=ڵ+AAAԩSYЭ[7zȑ#Yd ={KrMz-֭[G x go(ogo휋W#(`æWuZvmε~ [.boG1[>m:^`^*' sAb݇u6)휇}ouDnKvK -., 6|ìNj;<["b"}7H:G\lC%r&YS:Y07`Hcw46o`I3h+G@F 3C(Z'{OU|G w^Mo lM@_dgY;N/oXu6}esy$":<΃DmxˢQ0ߠ=d II7t:W^g 7'i l w^Py)7GZDDDDDDDDDDcva7;yIL?祀S> t":&|vDRG`sI(mDGo?\>iTt9_uH(l}52ݻNF!W+ȿh4>Gfzev>9 yrce@ܹYi'o/;׵rv2ocLӴ4 '} "uv.L `PbϧooӱV >W 7#r#W{^^$ytZx4'WgrÝgލݗZYyΔ)/֟ft<_ r|oor;&>Czש+Qq?r EB_ rH.fS""""""̞=>d^@Ӱ0`L02rM?{V*Ių6%WS\~l G2m*JNx)5k8屢v'jqDh8x1v)_ygXg; oJsӏKU,@";uYѮZ1Ŋ? ދM`ʖsLhY\|$֩/s;*S\Frt:%""""""ٳqttTWDDD2Чwoc1iVܷo/ T<=ڗ)1Xdcx.ļ;x.UT1+۷o_'eg4DDDѣG:l߾ LDDg0ws?O<Ν;|vӵ(w&ݠ`4rX>qȿkL…ʔ)1b󚢈H[+,,Ү"""""""""""lڴf͚fu 򆇇sMj׮ O\VVV o||qSڴiSfADDDsQ^=ʔ)Ô)S(Z(9rSN%!!!>Əψ#ԩcǎ%_|XXXЯ_tdͺGH:899amm͙3g;uk͚5T IDATkR<%ig0=׏?7+sO:BDw&MicI#XZZի|)eְwp$}ʕ;w${w\+WPTg2y)Л=!fe٭l(FclrˤQHJظq#5k֤GHr8v 6$G4i҄]yȟ??W^5ˋ9s0n8J*E[.7f̘1tؑjժ̲e(YdfL[DD䥖US7[d^e FG=G&x ~[?3ǙÛ3|8 ֭KWd"|Rܹsg֧l vؑةS0 `4߿?^^^ܽ{׬w}#.\Hd˳i&9r$ , ͛긻?b08p _5]to߾I9r$5/M63C2`6mD߾}9tׯhѢ\EDDŦ,!!bS9lB&M x8qe>Hzߟm۶ak|:xg}ڵk={6}/_fРAL6%J<>_؍Np|898щ<9w+վF#Ve˖xzz2|$uBBB8p mۖ{aӕ S&OL͚5yY|9o?ZjԨQ'r%/@r0 :u4΍7Ҿ}{*V ۷oZjlݺVZQF$eE$yE嫯bԨQ߿?պo6| }?#?Ǐ'{:i+'N`ɐ!CvZIR7?ݻwsfjQF}ܺ~BRP1 1MB|i!!,Y &0fXz7nPn] DV8{,~~~ڵ oooݻ7G1XK.)RI&ΨQ]vDEEѬY3ϹuW\@lܸ???VZ>"+V`ر;;l2֭˯ƍsX[[7o4_[Iń 0`7n$W\ֳ`ȑx{{Ӻuk/^Y֕Pr>˗/[nZ*u%6^d""""""""""""I)ʔ)Ä  3+wpp`Ȑ!À(2HAdd$Æ c4lիhSNt7_>̴ؼy3!!!X[[p}NJvXj|df*S K6ݣ7""> Pn]N+WңGS%KPHP"ٓ5k0w\b… ӰaC/^oav,=늯/...x{{su]Wj#"""""""""",dvE'Ȑ!Ĉ`HܞιގN:Ucx/GYڤghh(*$J}ԑ#GXp!~YAR̙3acccV?~:DZyS¥KimmMF8zY]yryaʔ)ԯ_++dmݺիWӨQ#FoJݜuh4uV6lɓ'M)ڣSۓDGzuppx #9IN++*Rzez }Gx #aL=*W&reSl'Olmm9wY!3..άʴcXDU2zhLTTTaaaۗI&1k,oΒ%KLӳ,\7nԩS?~z6"""""""""""H;z`ß3€G6p)" u?Ү0X R[hQlll8x^g<==;v,y.\qqqȑ777ùu+WN^cƍť76+V :dJǎ;h߾}'ү_?֬Y×_~ĉ)R|9sdt҅իue͚5hɯ+iyYpguN  Lه3/VcmRΎӣG"##qqqafuZhٳ޽;{&{߿???\]])V,|?KB xzzO p?~gggիGV3f FEqv튓...SHٳ'QQQ)Rkr=Zh䯩$֖SRvmm۶1i$>LΜ9hժ?3~H׺R|y.]Jҥo1;ܺVyqa (󑐐שXbo!)*з!YXX`72̿^Mou%9[[[-Zăhժ:t0ϝ;7ׯgΜ9 2jժeÃ%K0}t .L`` Ee„ ,]sRJ:wN{%ػw/3f̠m۶,YM ˗/' >sOϞ=qpp`ҥܼyzu֑'O'~ME$ej2_3a*T`={vƏOʕYh]vMs]ׯ 8wwwڴiMǓ[Wj#""""/ÇvEg`= Oƀ1cҬo>|}}!n&1jga_9LDDDDD^V6mJ*feKR._ `=ɢ^7.]2eI/͛7[J }ΝgrG?_;z31 ߺ-S晏GDDDDDD$k.X EшHVۅS78/}'j={g<y(ЛI,,,adiI2w""""""""""""""/n,KEDDDDDDDDDDDDD䅠"""""""""""""eiGΞK2{* b̙f͚EPPPfKDDDDDDDDDDDRxx8GbŊhт3gyf|}}iڴ)7տs~)+V?'** 8̙C*Uҥ MJ}9zgaXܠxq︸8 ֭KWHݻt'B\\#G˸ӧOz/_& tZjTVܿ`z-Z?3fp =88t^2-[F6mMWxܹC\\\c{ˋ]v%9Yxyԩ{yRzEDDDDDDD`4ի*T`Ϟ=|W8::r)NԩS TjUك% .`ժU\v 5kF~0G=2zs̃C‘#?p %&=}7=&NȰaÞ$SO-С 4x,Y ҡC4iB~HHHHHpwwgԨQtԉ-[СC@LS~}ÓJq۶m4iWWWFcWi\rwyJ,= ooooǝ;w:u*uƆ7|r4h kfʕFc$̤{f!]ؘeΰl6w)YӘ1cܹ3Fe˖Ѯ];ӱ+VPR%*Uhdر5*ضm6xzzRBSyٲeV{G~8p ?39s|3}ky.]̙3ܹZj%9>oka4Yr%'_|@b6?n:wYfo_>UT`֭,Zcǎ'#c f޼y9sRJ~!㏩TR~rwwaÆlذ뛽n}RJlٲ%~DDDDDDDDDK.C߾}ٵk:tW^\rʕ+)S7or- , رcpa (@\R oN/IJ;zO=lAP^b7[j|yM!(ȪUhٲ% >'>|8>>>xzz2dӮ1c0vX1 ?6 Օ *FMD_N۶mM[///\\\MS*>}:kצv|fzAŊa̙ܿx? IR?S@鸯/ϧo߾xxxкukS/$$ŋP\9S?:/-n߾ͰaMW]j%f; PBfu9ĉ0a%J0;F`` caaA˖-oqFs1ydƍG||d0h׮/6K̑#G]6`0֘ɓ'3zhϟOڵӧOkU>}9ryK,ʕ+u{4mwww:uSh4biiɈ#غu+۷o_~@fx"֖WR/߼yJ*#Gʖ-k/!ž$)͢#y p(Hw4-Y &0fXzPr>˗/[nZ>+Wp= BܹlgE'NY~==z`ݺu/_޴ ٙ'OvgϞ%$$iӦ1{lS15J֖ .$9hߟaÆnj3S{=6oLƍ9tNNNlܸ???VZ+Edysܹ?~<֭`t__mN/^9s2{lZnͥKx5kF2j-""}hz 4+/\0 CE4o͛3gbee9$*UlNb޽KK$mZ>> 4:Η_~=]ve4i҄۷ӵkWZn͛R {y.\8žFfX~^}܍O622aÆ1|6l@ 45 ޸)V  tqvv~J9{,5w5oޜuѣG֬Y$kS4i|4xzz&ѣ̝;+W& bӦM:}1k%?]twN}zjk+W.FmvqȀ޽;!1ZYYh"NcǎƦqZk׮9ӧOsYjԨj|yFpp0k֬\p$""""""""jժf͚$Nr'''OWRM>6rssٳ]t>>>ʕCJҥ={6{$[l8;;ӱcG;ҥK ;w0\]]ѣ9rO>ё͛7y|MN 1xDF5DhbIkDcmQTpE[.b*$"uN #?'3Zxqﰲ:곿ŋ=i5 EQR]h3 xyyvGWqt!6mO?Dڵ]6 .ݻ̛7 60zhΟ?#mnn_SJ_KJJ5;w7nIII[KrVVff&'Oݻ״… [njժtޝ9s0mڴtu\p!+Wү_?ptt4R}i-,,xsMYݻWzjբG|fK.{p]ɉ4,--Yjf?,XXׯ  ૯2=;F-[kܹs$&&j*UF˖-ٸq#s!55H",,׳ajժe*^zoٲ-[Kٲe lذhݩU_~%7n4OՋ3nܸC !**۷o]z[[[jԨQ1>k. @Z*C 믿fժUy.Hn*kkk^9q77>A+z%_ք1rHRSSg׮]fu4h5kpvvt,]Ԭɉe˖SL<<< #lllb^ձ端ԪUrGrr20nݺTZR m۶%!!?ӧO?)>sRSS^nݚ̟?> sieeYY`` >>>T\mr6nh5nܘhJ*e:KMi(>>III!&&RJŋ1cs>|J*/Ã/Ot-SժUٹss|9#ӧtؑcǎ^z%*T@޽dСJhh(>%J(oݺEhh(m۶ʊ~)Si.]CZKZnmڴnݺ>|ؔ;vb پ}Y*UbjvEDDDDDDDD䯣@o1j/1qmTz57QT+V ==7|Sy`` L8ѣGM~~~\paÆѼys>ϑgԩS h4A˖-ٿYcǎr\s'GGGF;Xd ʕE=z^zɬnpp0^M6 0??<$""pO-MWfΜ9 >-[d}1c`4 ޞ޽{w=t{e˖/$44}1gOÆ `߾}mOOO[-[FPP͚5ɓ~$%%{֯_Odd$~)xyyjժB}QF޽fΝyNnA܂|ܹϞtЁڴ:vҤIfjEE]8qm۶5+;<...T^[h"Ls̡s΅cff&UT'--6mp4iR+-N]ծ];ϸ>ݻwgĈs:vŋ bŊ~^8x Ǐg888Ξ=O?h^z&"""""""""""R,(uYff&[pI|}}xbfddgQLOΥKpqqaر3'''.]Dhh(/R)))̝;VZѷo_ <ƍL>Ν;?v!ʕ+4hdХKAqIPPC aÆfeuĉhѢBoݺE>}ƤI_ڵ+k֬_~saƍѣGyӁlٲ5k,\v| ƍc͚5,Y{{G^ɓ'ѣݺucXZZ۷sĉqcJ,ƍ{'= yf͚Ő!C0DEEb׮]Kƍiܸ1Fٳg3cƌܶmlܸ-]43gΤk׮/_>9k׮eر$$$cVvڴiCɒ%eOSNѶm[^}տ{1i$̔)S0 k_l*T+(""""""""""RlhIe1w6,ݔs#為B1lڴ=zԩScbb0 \zׯҪU+7oΔ)S|2۷o7:{,SNwww&Mdj `̜907ok׮5Ž{K.1a~7n߾iժ:_~eKۇ# 6$99Ry'%%[o~s]z53[}KBBǏӯ_?rݹs5j ɓ0a_|)5ujjjzF;vп5jĉ|<2g9&&;wҷo_ӱ2o<ΝS'ѥK}]SaIII,Z.]9uT}}|rڴiرcyȟCbmhe4pիW3yd HKK˷9}4fbΜ9Ԯ]+Ixx89}4/_'2m4>|Y wTPyѯ_?޽{MӦMcÆ 9ʕ+SL߿O@@III; _BHΞ= /7ժUc۶mlݺ P^=S}m#G\v Y~ܿu&LݝΝ;g۷7n㩑o5}ÇoK~ݼy~~ Pre:wl"*4441cЫWd""""""""""Oh~$n.~O=N·ʔ)SX|9:tE d|'x{{N:rIRR)׮];ƌ@Vٿ?۷~`޼y\|ZjбcG{v˗)S [`A{J%$$ZZZҽ{wnȑ#gϞf L}MnܸAr̎(Qggg_^q۳l2llls۷oYf|7f[[[^[un߾矛;7oޜ-[~zFYs۳|4h>>>X:cz.\hZ_?Iqww7+VB̙cۦM>CΜ9CvrSiYlY^."""""""""Pք^ݻw|$$$+ӸqcΞ=gHBCCܹ3 ,`?o޼IVVV*T@Jp.]2䵿<'''L'O#fprrʷ͒%KҺuk~'IKKm0hѢ~JLK6|D߿CdmmW\޽{=SF ?3ݺu`0p5ͧJҥY|9?Z*AAAt҅|aǏ}ԯ_ٳge˖|yߛHq̩TNN فJֆe˖㦽4 sү_?zMNؾ};/)@IRϴ޽{E=?9V999q _N&MԮ+.]2}.Qݻw'""3gUդy0` .dTP/ar"={,f_(>޽{=v 4vw6l ~cDDD`ee~ʌ38ru!>>III!&&RJquu͍%Kra,Y¡CEyTVSNq) cԨQQV-l­[(YZ*=z4iii8::k.ʖ-KNȱ?- VyT\ʖ-K:u;Xbl߾ݬ)Ώ9K/Syv(+5j*UbŊokݻwӧqvvfѢEzj-ZD͚5 '00 &N }ѣk׮eѢE72ecJO;w\֬Yg}bȐ!~aԩS h4A˖-ٿYcǎrJ-ZT6T¦M裏6m/_~%VZ4mԴ722s===С;vߟ͛7a Zĉ<x >ѣG3}tV^ͨQZ*k֬7:ub֭==vvv;TRugKtt4+V|RS*,Y%JO?ѬY3vލ@ﰎ;xbéX"~~~f_///BBB b޼y>ga1Y x|}}!ȳ*++ٳgcoo NNNdeeիWILLdʔ)9'sN̎9r$DZemmXڑ#11;;;._c EGa0駟֭[mOSRRK|‚3fpسgյPEDDDDDDDDDDD` cצMڴi!"""""""""""Rl)+"""""""""""""VaR+?Q3F^gR7H""""""""""""""TPWDDDDDDDDDDDDD"۷gO?a4Wx{{?顉sFd4;v, .|Cyd|'OzZff&ӧOҥK0vXƌ.]"44̓w}gv|  ʕ[n_1 nZ<̚5 ___6mJpp0?su㱶f5A^szz:+Vsθ1vXΝ;g;]`<<<=z4Nz'O^)Pq<^燿?>>>ԩSuҡCy7 $++nݺŬYhٲ%'Nȵ?dǎۗK.ѰaC6lg{n*W\9<)E}fɓ'iӦ )))=Pʗ/۷sĉq#*~;w.QQQFFF:u"111G?/ʕ#,,rѡC{EDDDDDDDDD!n| ß00k, h$**~֮]Kƍiܸ1Fٳg3cƌ|ۻx"|n::kצyϽzI& 2ԩX,y\{4ifʔ) ^{\/[ *iذ!IJJ⭷2 =<</rJ =  6mʵhdӦMwwwNjV~YN7L4W}vx ,--Ms'!gvƍ[⋌7δR49İsNk:Xܹ͛sqppg~jժ9st,99d^x]dڵwfaaA=ڵkDDDDDDDDDDIQ:L^/ ]<,͸ ﭂_o7n/d,Zԩ޽{:t(aaal޼فǏŋk׮=*F#AAAݻ)S0~x6nhC[nDDDPzuzPib߿̙39w˖-bŊ\vW_}ggg-[FΝر#gΜ͍vڱgSqqq|ٳ7ժUc۶mlݺ P^=S} l¢(+++Zn?kի!))aÆoݺń pww' ǾofܸqcggNleeLHHGf0c 4ibWе5;^fM?S@XεacˎuGf ױhL8f͚PD lŒ3LBBB>}:K>0ñc8tZ"66K믿/;w43o<&OL.]hҤ QQQо}Bh4pBkPF ڶmK@@9sm۶3gcǎdɒٳ#F<L)}---޽;[neȑDGGӳgOc| IDAT޽O-q<55)S|r:t@-Rkloo'IIIؘAw46=4jԈ˗/뜮\b:oРAb :t@LLi^ .lԩ{fɒ%,YVSt_@2e̎/_@'siY)[= BҰ] >3 k \2w5R dddPL\Btt4_~̙3ԭ[x<==RZZZNtt4 8Ьi kԨQ8p,ill,DDD} :X6lHdd$ .,Rϓ^zѥKٳg_իWiذa㉉$&&j:{ _{e?05^^^\r{1zhΟ?O50  ~gu`0s-;t6m"88ڵkӰaC.\HϞ=7o^u_OJJ0e0A+z 4{pwdsݻV}ёӧOssoaaAΝ9~8G.T*//2u8993{l*T`Zzy&#FQܸr%%Jн{w"""9sfW^5[UF֬YCɒ%֭[r;;;ʖ-M{g7::???/"у/33tt.9yyyQF d/\@~dժU9ԩ!!!x{{cooM~J] ( {0`_}mڴȖ-[ Ԋ^yn)u<ժW]>'˄Y/0'|׌f͚C\\Wf„ 4mڔ7|wyXtYAAA4i҄:uХKn޼Ç?~Yи0<<LVVIIIݻIrrimmMXX#G$55{{{veVAYgggJ.^|xݺu8;;Ӱay6UѣG#vlٲt seeeCeϳgOOOZnM@@X[[syϟG}Tk+Wm۶9r7y.""""""""""Ńϑq}`T/ȸ u֩S;vMǎ6miIKKK.]Jxx8ӧOZjxxx^|E?̠AR >>>ST)r]Eiaak ݻ{ocoo۶mO>aҤIܾ}֭[}ãTR ;;;ׯg\gԩS h4A˖-ٿYcǎrJ-ZT`{IIIxzz>6 }Ѷm[ ;erdd$oOOOfϞQFQR%VXAzz:o&@2228q"...ۗGXz5-f͚x.9;PJ֭[g}/Ѧ` ׯ_Odd$~)xyyj*ڵ+&5bDDD7;wťEDDDDDDDDDb;f1t֬+9r__߿`H"k.ȥK͋oRٳeLlْ@X,n={6DVV \zDLbVDDDD;wevȑ#9=9Wbb"vvv\|Zj=ለH1wʣ0 ;wOk_,Tj蔔;_+z婰w^^Jݺuy&aaa| IZҘ9bZM*9YXX0c 8@\\{^zYDDDDDDDDDDDD G^y* vѣGiԨ_ENlذlllPzmڴM6Oz""""""""""""ŖTxWyW0}:.]Ņc2ftܿHm>>\~ŋlٲ?gr""""""""""O⺨Q+z@g޼y\v kkk+틛Ǐŋ_޽{ӨQ#¸uw/o߾еkWHKKcѢEiӆ۳tR֭ŋMu>qss#((Ȭ222:t(]v͛@vډ'[oÇ8ulE\VVZbժU޽{DFFҥKܘ0ai6lyA֭[L0wwwr{mƍGxx8vvvĞ1yVVV|DŽ0zhoN@@3f̠I&z]_5;^fM?HzI"""p6668;;S~}ƌ+@v3f ZgoWWWʔ)Cڵqss3{fV _Q$$$RZZZҽ{wnȑ#gϞfٽ{w),[[[ߟxjj*SLat}}}5}ӓ$lllLLyYkkk5j˗qppuNW\17h |||Xb:t &&4 fz6tԉ޽{d,Yf ׯ_L2f˗/EDDDDDDDDDzgX(Sֿ _1|Fzr "=Z+WݻcUT!11 ʔ)Õ+Wf/9suYRKKKӟӉ&:: ra5 =%22oߞCKÆ d…4h ^?iҤY@Ņ듐g֖Zc(zE.]HNNfϞ=fQ\z 8Hbb"c {e?0(QݻA^rԿz٪h4fJ,InrQlY?nvEGG燷77 ifffy6'///jԨ7|c:v].\#ZX[[[,X@llCOϛ{{ZXXDZZZ  ૯2}h4eWDDo /`0h؃}VZaeeޟ>7y<^wȞp/f-sQ_35kqqq^ &дiS|MyBBBbҥfmѤIԩC.]y&fE^5k???1bcƌdɒ=z;蘽'*T#..H}hтm2n8ѣ 2///zСC dɒ,^q^>6oK/D r}0UTݻwkۛPN8Aƍ'>>l6cHLLdРAJ\\)=s簲nݺtԉ>|,ػw/$22rhښ0FIjj*ڵˬN Xf Δ.]:ǽ upvvaÆmNUV;G&-- GGGvEٲeԩޟVVV888}w?{zzҺukښ3|>"]@|||\2m۶ȑ#lܸlsy6˵Nǎ4}.]Yy~׿ݻw " 3uЁ駟r5DDDDDDP92w\:uc>Cر#ӦM3#iiiҥK gTV S/4hUTLJtJ*E\\iUYXX·{{mO4ioߦuf=K*E^3[x]Ν;+VVVV]E1n8ʔ)wޥB DDDdBCC\2ݻw.u:u*3gd ><@o% W^3s#ENN'O$>>8FDkgΝ;_...ҏ7|gz_~o\]]iѢQO?%<<˗/deeaggر#/yBoڴ-ZxWS?QR%bcc(ij)duU"9))Pw5W 7n$>>jժ2e]%y,ݻٹs14W2e7Ao6[l!:: 67cf;Z"44OOON:ň#hݺ5 BnnnzOrm%z3220^_zOMDDDDDDP0LY]vQ^=|;)}(0DDDDD PNؼy=Op/&L@dd_DDDDDA(蕇B-hѢEQq[SL)DDDDDD TZ5J.ɓ'ygzIc IDATpww… ydeeh|yݝsevA^!,,/|NLLdĉ<4i҄#G HD+ga2-7n ''e˖ѡC5jɓz_Qi&[..]{}IHH^0~קJ*lٲ}>FnOIIaԩ<Ӵmۖ/B[mI^zѸqc^^|#GҧO#1b|{4555_;̚5k޽;cذaѣ5@|}}0F3^g}KKK޽kҵkWcݭ8pI&1a<== j'N`Ȑ!ĉ˙,X|||… $$$EHHjբW^/}9soo|ǩ[><?&MЩS'֮]{ګJxx8mڴ1ޫvvv-[pBCC2e hт/#Æ cЯ_?f3 6d̙CwUo߾;v,ϱSRR}:k׎ .xݷ\lݺҹsEDDDDDDDDDl~`KQRblz8^o>WWW>7oNʕ:t(~-eʔX"""ѣ͚5,Y<,,,q,\˗I1b!!!4hЀK{nZlyO1ye/_G}dܘc׮],YDۍҿ˗z=د:yQҒ;rJ^{5VXs=_*/n:bcc׮]cw,}Y4hodgPPg&22-[ЪU+6oL@@}UROmRRRx뭷OM4iӦ,_^{`۰aCpwwjժ oAϞ= b…_b>0F>.~wN8AǎgD#G0i$bccUV899{m6mgw;7-[EDDDDDDDDDѥDQ@@F"44 &дi{2&[,_<3g$$$ӯ_?Yb+V_γOrr=`eeŤI߿?vUVRD[sEԩSѼgϞ%--ukbΝ;¥Kذa'O.cbbty֗-[1z27{{{IJJgrrd˝7vժUFe˖>}E=ԉ޻ٳglM6FY`ƍYz5FႸ0|*W @FUk֬_f֭,]~鞪bYf'Oܻw gggN:ZݻQN'̳PjU<±cnj{0waL&?_~3[[[c ]vL>߱y ?L )[13gС&;vi>Tlmm?>?J",,HNN{e˖Ԯ]q8sDDDDDDDDDDZYb-9#n,XwTsvW2h &OLXX/_fȐ!\,22%KҰaC~g~gL]ي+?wNFY&aaa4hЀUBrr2;v`С9p6l"""hڴ)*Tŋ߿Ak{yH>}0LDGG7ʠ3qD틵5ٳ\x^zuzTɾ}GХKFd21|pqssy$OKj֬Iݺu)U. -- ///֭[["vvvxzzXhM4Օݻw@ժU)_<͛7gaaa]ݻ7Y K(7sN|;7)>zɡ :UVň#&333~_|E:Ă  dӦM[rJ>#ߏ5m۶[n 6/'x{2{lz쉛AAAcccCΝٶmCeXYYXO?СCԬY(ڶm9{,3f`ƌO>W^y߸xb}]x ڷoo6j( l~4mڔ͛7ig>3_BBBѳ*6myL0%K0g??{c&(>>cyѣGv 6䧟~bܹѨQ#ߟ}(ׯ `̘1,^_RJdZnʕ+C !##aÆQV-vʮ]|||Xx1QQQTX3gRR%郛_~%s!88+VPdɢ:5VVVL8ӧOӨQ#֯_'˖-#**cooOPPׯ_UV̚53gRdIBCCW>|8aaaL4MDDDDDDDDDqT%Ӡ͑cޱΝ; BGIpp0֭˷ۘ688:ԩS6lX~7T7GT0n8T;5j '''OO\\#GBUEDDDkgΝ;_...ҏqqqTTXc*¦;0LܹW䤤B9׈^K֮][!Vxx8Ec‚~-[͆ ^:w,,""""""""""""wG^)tu""""""""""""Ŷt=~EDDDDDDDDDDDD5(SDDDDDDDDDDDDD"""""""""""""Rl+"""""""""""""QWDDDDDDDDDDDDD"""""""""""""RlJH۴iGӘfW7EHR._L֭ٴi_k՘L&222 !2ؿ?={P+3f ΝVZ 4RF Ν;Gdd$mۨ_>[n-8,X@hh(AAA̚5LJJ ~!?,yԩ:t`XZZ{nRRRaørJ'""""""""""aA%zȹf͚@~~~_x$;޽{c6Yt)ݺu3-[ ???0͌7 ܳgO?Df -h&LcǨ]6իWA___Ν5K.BA:x ͛7Eq333W^9HiӦs QDDDDDDDDDDPbJ |͍Juw++YvZVJjj]1uTƏɓiڴ)ڵoOb2ظqQo5߲e &76 ___^xvq'v,>3ڶm˓O>_ؽ{7&xUTa˖-}5j(p{JJ SN駟m۶|wUzݺumHԫWƍqFF9#GҧO#1b|{Ϫlf͚5tޝz1l0bccGeԨQKDD1y<XZZvݬ]]nL4 &YXP;qC LJ&Nh\dCxx8.\ !!(BBBUz<x;̟? [DDDDDDDDDD %z+)^c~.]0o<.\`[bDwȑ##$$_|o&_5j0dIJJbС|ԭ[/ҢE jܹ֬si׮Zvb7Yh> `ܹ4lgg|UơC%22#}vzA7oeʔ{,=z *HҥYj+WN:T^hNttUylKBB'OfРA||w̙3x{{;Nݺupttd4i҄N:v;_^}UiӦ^nٲeʔ)SHLLE\x86lG&&&~a6iذ!3gn$l:`\W/}رcy9s&*Uz㡑ij> s̡_~+W[[[F_kɓquuޞd.\/quu]vy7}֭[8p ;w[_DDDDDDDDD(z)W?Lhh(.]bFbnuԉvƍcǎv֍~ &L5ի9͛7o߾4lؐÇj*|||)\# VZa2hҤI휜c1w\I1b!!!4hЀK{nZly_=*N}z9{, 4ȷ͍#ٳd˖-jՊ͛7p߱?hUT|3[oŧ~jwnҤ M6ek߆ ;UVH&x zIPP ._w>9q;v4ʃ:r&M"66Zjx{FۀMÇygN e˖Firyt)+eooO~ꫯ e֭_=ciiu 2dFZ[[3agFB,Xy7޸stޝ:t@pp0e˖elF?/X+V/i|߱=:wLHH.]bÆ Ls :td29V̟?~RJFHHIII$''S~{e˖Ԯ]q8sDDDDDDDDDD;nG#[r3H:;{?jբ}̝;K2mڴ{… \~H>|`HMM6''wyzdyf͚7'''c`aprrm۶mۖ *өS|N:ŠA5kժU3[XXЮ];ˀr"Q͹sVVVtؑy;ko{QF \Bbbbe7o~ˇkPP>|:upq6l`;ANo 2shذ}]rIʕqpp`߾}(Ҭ,lB݁sq@fffrIVVlٲF?ׯ_ԩSt KKK-Z/֭[3|pR8doooBCCX"J*9r|zoߎ<};7)>-oipd⥗^SN SSSҥKvvvTV ___߿?TX#G^༥7W_gϞ1}"""]6p=xtssyM6eyٳ>;g쫯 Mh޼9&L`ɒ%̙3ǏO޽دd"**O>?{{{^~ez葧]Æ 駟;w.aaa4jԈy_?Jǀ3f /_TR,Y?֭[rJc~!CaèU]ve׮]F>>>,^(*V̙3T}͍/9s̊+(YdQC+++&NӧiԨׯQ>**cooOPPׯ_UV̚53gRdIBCCW̻}+.^g@7G%++spB|}}yw<Ϻu:u*5'7ngΜ^k׮Lub4p9r;6ܹs'CH0:t(X̺u`L:]vlٲ-\7o6JOv戊ƍG*UpwwFpI㉋cȑ""""R֮]:Ν; Xs? s)T),G֭9|p˗/ȑ#l޼ l2N8!CXt)/_M6l߾5kƒ%K(W{&<<&Mܲ?Oy/hOry@$%%nw-rW/^+' 7~TӹIޢ;WkA‚~OOOΜ9G}Ĝ9s8{,^^^=ZI^GTFFnnn@@@˗/'66 6кukaÆܹSBjժ [6ު?UKqrG'N`5CGD@@EtCժUg4i҄S\9J(?/_Ak? 33Zj+UdLCw*W|qׄrG5kKBFDDDDDDDDDDDDB5HHHښ#GLtt4ޜ?5jÇ)UʕٳקDy?SR%[%ѕ+WHHHl6#t҅u֑Ύ;ر#+V$005k֐H\\{ߟur9;Fjj*6lUVKӈ^XMΝ;iܸ1| JK.|}駘L&lْoۯd"66СC_>y-ZҥKs̙+{zjL&EHPY2ɸJYDSCʕ+n2x뭷HNNfsow*""""""""""%%zANNׯߴd\)Z's9X lRqED Q:u(Y$QQQmWlYMFDD$''H7oN.]hEDDDDDDDDDD^){23HOJvvXreQՕҮqs*ER)[ N6^Mm'N`Ȑ!ĉ !!(BBBUz~7oc޼ynݚ&M0uT㉊"((@,YBNN/^Ņ~_| F||mceذa /cǎ~QE䁩TSLf׮]m۶m[^yz-+kƏVϩk׮ӵkWjժEYb<Yd Cח^z<co \~#17*D׌:~(9';tIII<쳸0gGr尵%99 ./2}t\]]i׮.\0믉a >?˓ΨQӧ*k֬1r 'OsL>sʵk ŋhт5k2w\ڵkGV8|]];)g„ zv3ҥKf̘v) Kpp0f"00۳uVcyNDDDˌ3iӦxzNȭVEzIڵ0a.]ʳTRDDDU&Ldea]{Ή'ر#yըQ{xi8|0<~ȑ,Y~7|ؾc6nH6muǏqԭ[ڵk?Ҷm|1~4oޜ}аaC>̪U !"E_gŊ̙3C޲]Ŋ׿g}?t9CL'5K~=uVrI0nsիWgϞoߞHoNvvü{tЁ&Mrm&&-/}s\KiӦ8y01LL81qDĴiСClqF[ڴiѣ49uy>cy5j7|s2LwsJDDDDDDDDDD7E&{'k|#yRr,{?Հ׆O`΃HkԨÇ'---K.1x`&Ȯ~͛YxvϩDڶmKZZÇ/K.~S899M)n.r&(ϳJgmA<*#Z5*cț;7SfL&x{{JŊٿ?GϏN:aW~cѢE4iWWWvލUV%555kְpBXz'ʕ+xxxxb4iSO=3ׯFRgߟbmmͮ]hժ^^^<8L><G&OQo;sLfń###)Y$ 66vtt`Ν4sMl\ yuL&VVV`2~L&flL&q|KKKVVVdeer疖ʸ& ,㙓;XFM s1Ckvs_瞯%&7rùn ''ׯM[n72Ldggs?:Ͻ}}s 䐝[yY[[ޛSvvqKKK_yɍ/Wqgfw76,Wsrͽ;;;(QLlllpuu"ssYYY*U4^JVVvvvy}vv6YYYFL >>x>r'55{{{vZkHVV׮]ʕ+8::bggǩS^:fٸ.\IMM%11ѸwիWg3.]Ņ'NꊭsWrr2e˖Ύ8>rf1T\L,--v_7/YYKLkgJH$ !a  v!"/! 9C - ,#dhFLoյ^9n-BHDtHSy͛sjdP0 /G~vc4)J(ou?TlPB %PB %N\.hVbf jNT* "*A`(HZb~~h4 b\.&F#V+(˅Zj|>Zv30( v20CZQQ`ZaZ!"4 F# rAJBRFnG\FXD*,t:Zp8 HFzt8, 4V+, <*j5L&l6x2tVfbj(Jl(p:0ͨT*jx< ;bp8NC@׃V^G(BݻFr9LOO3IuP* |hZr/z=bӧOhP(P(`8rAӨVp /SBpann.\nG6eP bϜ9cjB1F8vt:N'|>Ν;ǰxjj0>VWW9!h|>Dxqd2=vl61??l6Cajj X v~6 NAc4ez\55NN~%&t:-h4 Z-$INC,cEQVTO IDATQ8;Nt:EFTUTUT*X,F#$ :L:ϋ5"h?'($hs9r>0{k=4ZfCU堜}rVBF#?G4&䀓~/rJǢ"-;a!jmـ3Sj;]OjNԮ)۴O:G O'P\6`MΗΑvC:F)gNcK$-?tB#V-A!$A8 &=fQTZC^GۅbFs4|jZh4V }4{0n#; OFFdK0j||=fff`Xx?(r"]2DTkj9IbjT*q"Z@^ED0乽lB#͢V!A$ F`tqt:Gto*㷇@*J(J(J(+c0^h4"Jbuub!&\.J4 f34 L&&/ʖJ% 8VȐZ)"z2DQ^bD2ZlFZ@݆ZcZ-LNNep8Yl6rPVQL 8NF#qFm Cڵ zRahD"`,-Z,Yt:$ 'OD*E$`qqcu$wffA)9g݆`$Ip8HӬrpPT + :V+2A`EifnHi.*?$r1P!GFJ j\p  &.fggJ0aZa0@BբRn#'Dۅf:VVVP*0== ɄUXVlll'n`)-Rwuu`0@,ff`0\.jazz|dǃ\.jЕzaL&d2T*,..b<].hp8P,qap`0kfs ئZ, Qt:1Q(`6 dRDX\SnVD" # 3,l4$ hd@l2 WN;A@.ch4t:h4nY)n JJ>gIγh* 0 'PlJY9$db 6A;*Rr,0!Wȕgr볩3mSrBݩ+hrI-\ +ߟؽ^orK&?>)e{+i^gp&W9kK0UR%]il6Hl6բtsT1M@3}!F?s"Y@%yFN$hӉ~x<ΉGR(qnm2tPV199sd7b0 LbbbW,9a`s8qRh4Dh4P(`||jf, V+>!"'j5~7 & ^/v;$6, h͆RFϷ-IdwJ(3CJ(J(J(J(įF6 Vo<ÈFl v;CXZԗ$ $a8p J-Cx^}mfvh4d2Zhr ZVZh: yɦl|N'/-FĹsh4`4fdu]^fH&#ej循 nc8"#4 (p\/\`X_^/j[@ŭJ@Ra(FQ(Xk6A^x< Hv!_^ݻw\.C$:kkkN/HUJ099Va}}툫*x8F.CZe@p8k.LLL"6 pT*6H`zz_~96Ξ=X,SNlvsB)~?fff066\.Njآ(Q(x2qܹs0 j5&''vz=HQYj<䓘aŚ(8x "l;wZ\ZZ‘#G`6VL&raZFa4Q*`0뱺t: Iz8z(+2L&J[gZlmm(Y~S_J%d2nt:T*,--34\.K}OvmLҙ*%xT**e#F3 h%kU rXHYnL6I.A4:&m nsTn~H]JA@Q$-t|(ĝ_ZYuT+di;\+o+yA|r7=:NÖ3尔9S"daO 'i@'H^͕`GsΕ`-=_dKH%9LV1o6jQ1a٠Vmnsd^w[9xmmvL&t:ϝlLwXt:z= n7}rp\Ln9R066Ɖp. O='mPbV+@ ʓ~?VVV+NB*+Jv!݃J(bCR^%PB %PB %PB %P *p8@J|hϟ(l\Vyss* \KKK&[p/~FeZ-H !n7[ѪT*R)EDQx^%N@RNCZE\bauZRsl6v @ q][ϷMR5  w\B8F\F^hڲV$[=R~`0`Zn# rb Á|>9ZF<EP /T .,<%R~zͤ$"nQ.166 LFDQVz Nt8< ' \.4 x^N,X,V(Vl]( "ߏgD~hZe~ $ y6 ZJڂj<l"Ja޽x%E]>ɓ't:6taeVEvT*l6ˋFt~(˘4N8ǃcǎZ;14.֛6Ͱd2^^^fff0d&V!Bd2ayy Z͊ u@<<|(Jl%OtvJ%uV84L HF\Ʉjf jFQQTƛ 4YΒ}02'0 ly M*f,=r 0CE&*X&K@+udwc9t& Gm: rkb 悝T@R[ttQ\5*9ĥ p!o|lRʯ %AAdPRM9R=[yP*`J"LLگ<9a41% P.vئF~gy$`n%~VAg2R[AFӁ`5~41Dwݜ,C!Z<..J\.[OnlDSvFN+++hZjHA}[,!82 iyhZXVTUZ-B!t:fLLLpbTR blla:ꛜ0ф] %xf(W %PB %PB %PB %"ϣj!J!<~uB!jy4MRTK{p8d VReC$t]T*LNN2%[Yjb&UR{nmt .N>r!-)Lڑ[iQXӱGbccᐭjE*pQu]r9h4b1`റBVI~$InvdH$XC H\ob{Zz PTp!LPvnravvFxnm2j5jF#VQB!`lzdJXHr_Z AXg0⯯h4jؽ{7z=* ,* sXf`=`Zy>b0X,kEfrӉ%rj0V9(!Ξ=cHd%$$<l6&N`%9I9VYEFP5 EEq[-K*mz=2 `6H$RXL}g2Na6aZjo>XV={:=򋬥%Ibh4fLMMm5d2 A`pGjZ`@($IX,jrx`( 5HtLki eWV*w繑ҙˡ4%rc94'yE4WN^Jg%I0)zG}C5VO9"4g:t}iaS"ӱk|nt~~mO@X^Y'B}A0jճ.1NJDF(j( |MEQdEqՂj RZ,q|zHEV$]=|>~fʓ;6#G033ARbRY!, s`ccoll@EA@&aKz. ш-8'߭r9'| gbzzǏfWM*ok(W %PB %PB %PB %BRh4"h\FP^ tX^^FAD:F8FFRat:lD"t:\l6Xr"(Vb0\$+-[Vϳ*jEh4D"L&$IU`nZVk0P*p f1-sDZc0;;  F ՊJd2^FZ#Jaii |na<0/\uO%IZNL$VbϞ=EV dnkF"`my@QJ`4t:qyjwh8s ,ZB!6mu+&9Ԓ$^^B.C^lqL`T kkkp8PhZ0((H$rlY.zomma~~n w* PTx,}4 hZLMM3'pU( l69@V#ɠZn&^{Z"n#JqDRFV% m~8NNDhZ PZ-_NL&Vu{ױg̙LNbnn7 +LmL%'@lfhL&hp 5.[F#ykl6F| B|rGjov:%F(Е-AGRgydWd.r^yY>9&'W|< CMre9xNH&yޔYym!:Yw]v##mtl:6;kRҸw]s'A%r"+W,M}As\M`0`hFPλ^s{%ulVCVv,Cȥ, eOz= VWWa28166?Zl6۶PR^'|^d2 I;4sY;]Al6fh4j5 R@VJRkjP. &''a6177Q9)fvv^z)z<_r% سgçk+į=^%P%'x}C'?+,%PB %PB %P7& >pF.  )=2.R^uݘSO=Ŋt:5:6. ϳ,%kP.xvcbbx|,RPYFT*P(r|>*NI099j͆%$ fb1|>T*CEpJ:0L8<E;0>>V6 \!Ձk.e$܄V2R6-)lvp8ljB[[[p\8{,á-R bFGt:ĴhM [[[x<xT@Th4"AR1dMOJLÁ1e$U:n7A@"hٳgp8P(PVQ*PVfz>d2! ns-C9 vj|>FIV^\6 .綶P*0??O" C$\.T*R)vv:|ΝccFӉ@ v>|>n7*jb}r,cl6qi- , ("JA! BQ#UhD(BݻR^ ^|>*g:& jk=E?YF5? i,F|>?^T*! !ϣhpm^}뱲D$Fz FT HlN-t8l6 +L&N[[[p:h4H$BlI @ z^~D &-oۅJ<  … jd2%A0bssn!$IطoDQzɝNvwfxa0:Nft:x^J%8NAt](˜a4jp8D@>Dw:8Nn Nj zVUT*0rBuI [.YKv*՞SMiZ0 ϨK\1 < A bN`"NU"rTUtn y]ڔ+x)XnMA@nSvi?r2mOԕ^Mך='ATN*K^r{cy[$~kBp\Nc2(s%oؤh'* vj'Ҙ%P)|v}}Vg=zd]Ezؑ/ȹ,zl66Z:ǑL&aX8Ijqbv$ }Av` f[Zhus!r2`]8yn^ciiʢ(r"A[[[8q Z+yGؔN5Ϟ=J( U _>G}|6r-k0>>뮻r 7 Z {/^WѣϺ-܂C?n&lnnb޽8~UokN|cw/h/e@sss `jj l jXZZb@MVӵZm )"ݬdbū(( ZO3gΰ.6f~?4 r677.Y(m%T*|>שv+-T'N`+IJez\ rzp8,% L:WċT۰^X,2Z &E@l)H5 `ejTU(OvcccZl60=_h4 5yV4IAz1>>^ÊY8'0f۱bjUZ0W_}5Z-ZN,J, N'T*2JۂWUb:5sܶl65 AÇ1̰Z>3$l6СCh47ѱcǰ57z=C.S. A" T*xpz^cۑJR "|>^ b4՗Fð!?͢P(D`Y;`+WC9C`'J]Un,rI^y{/[ꕃН?9Lu/3)4ȁ~T 3) `bEZ^`/Mt;rU4=(mizn_+9jd'e-z1Fd\زM~lgfNv,'| >55v4gz~Zr3Oϻ g?g&+߷8~8^W㵯}z^}c?c|7 Ϻ׾5r6ύF{/{1u\wux'|Cn?iT*\{(x}7srJ(J(l+tIJd2z,DpEх  It:v1 P*nY)V& UDcccllQaZd QP(\Tұϟ?X,ƀd2%PS.177^fG46AQ͆UH͆xzǏ8L&oVߏu48<"[c2Rn#Ҋ,%6A(ԯd;66u{ Zpl6<7x LOO30 zHӨT*8w IZ-|>ARAZ$I0LGلBZ& l-Pz=, NR(JXXX`zr9a.NmG0&.'S`ɄSNa޽x< }H@ P%H jRL&cjr7MJ%jf1;;*Lhտ,0??(jT*0h6ͱ@ 4 >|>FIMN4HK ce۶kƒN'`\1JCGr6rI͡M&'ئ vbvtms'vtr/~>oK햟RM-˭IMsܒZo{:/:wGTǙIEvQ)X0Zi3?9+P?m ΩjxܙV%Q_ѶV]HO \T7M:l69+q@d,8BèT*fVp7 ~&S"HT$ RC4M.@vшh."7łBD"ݻwvhhp_ve0L J%IB6E݆lFل(XYYA<e] ӉR3g`qqj`4'9NRr!c8ԩS$bIP(J'I &e1YaK.bVwAT %^Pqg>,..r9|3e]-xG%{>9<PTЇ>XZZ'> 5={} tysaT*ގscaaz+x ].$Iww]ÝwJxއE5>$R|_ǝwމÇ#LϱgoWڵ99#-oy .r|K__.O~LLL`޽( dw_ߏh4ϻݻw[^p;j5wcll uh}{g|=7^r=܃ /봘 h曱ÈCtM^ܟ-z)| _?y]ϴptIP(P( lP(nn#L %PB %PbF?~Z Ft-nd2P]*X;D055u/iaude:??QQTz~NÇd2l6\.#sYdz e nپ`0@$9s6 gΜA\fŐ$IIknnrF$AE4Mx<}dY\.VJݎcǎAEl>SfGEۅh4b;D" FPصk/<8Np\.^cll ("Jbp?T#HQ,*iRavAb18VF#l65Xt: Vǣ h4Fbj˅X,ƶPTVhZX__GрFaX*뱹d2 ͆Fz[[[:du4MnuHva9rzkح-$ vlll8l6^㗿%z=VWWqWCl\jӨjB f3v;,=p\$n7bj5^/n7ۍS)V jZ,--1/h6~0 Cgl>: `0050 H]W(XMjnGe50v!XH5?IOoKrZ-8ȥ:t@0,AcPmcR( jNڎݛ 4 J:j5+I. J܎w' %-])5K`!%\q\+-\*:},=9P79T\KѝX3F\K׍C`}ʌ#{fjWOԟrKj9D91vt#?]WW/WqxE\ \RS;'М@sh4bdfcSZx<IՏmd26r0 hZlM5_&}$ jŵTY8-ϣcnnJ.\'N`$h4jp\( P.8~8>'j5l0J, HrT*\>d2!NsBEc)LsT^1J<3["pCxQbV7c8S8Z7h;.;pM7ĉx\0>ObeehKKKꪫk.<W\_MR?o_Wd/$N>y| _ފ}km<+կ~333xșĕJ1 77/~yկ~|_q)|}ť/w,--1zk^˅<裸K099|>>}WW^?ㅆhW_SN=snF_S<ߘKN8+r{^u]xހ'Nĉ<yu]xކ/(xk_0Z {mZzwy'xVn>sgш|+G>o=n6|ӟƁx_ol} |:%PB %PBgl `ѣGjRբ^# %Ih{ )>6>n7fgg`2011VjNX, Ѹ~,Ant:ѤjRꑽ!zW\pjob7[Ɏ^"x0f!L2` rZh48rb۵u>eVF$IE,,,h4"ϣB$;v |$b0 ../,,vcrrJvNjnJ"N'/2t:x<"0>y$b666P*0>>} "DQdM}rj2\c^/2 dv:~Ru$Ip\v\TkzZ-V*j5"2 N<$J*J9\q?8"6Uf< ]v\.C$\qːΒb9c0 J! ajj:AOINvZrf& ~. vO6*T* nPdb XG>j:F:l6* N> Fõ,z[7 iVX*v v֕@.oaXAFvUT" -)I)Jv4wнC;_+>  ըq(!DX6*0@R+폞3դʗ)tq'+Wgr`Km+W\d)7cK | rrtMF*:'_àCߓ借·JmkKV4 FS2AX{tz81@כ FARA߇ng0 P_ҜO@ %ՊVZAŁJ t:ev&(BfܦhŎ4yv)H&Z(S.zL&6©SJ`0 j݆NC>G0D @i s"VEVVWWy]F\:<~??^d'*hP(P ⶄ0%x1bm_+u+7o.~ӿvOlz?zs67M8w}Pոk Oz׻Dza0߻q(.Rd2˿Ks=?35\RO}ݸ$aqqxmv;3?p78GE0w]n_t焽bGcϞ=.I|߇8}o߶o??y?q|_u]ꪫZ?}>bzaa_zw^ΈFHy_u|7 w+W wEC2p^~ߍk7quoMux_wx衇Call(~UE3eaXJ(J(E$XVvz {FX^^F(hD\b[UqIAVzsPAn!V,+Zu^T*zr`@^ګrhx$IE_Ų T*>FcFY$I*@:B&Ẩ{h4bKuy|u"pi$ ˰X, mΖTGD&9\.r9VNMMaccbLvBVlt:Q,h4p8nl6r>&. -Ÿő#GVr&.VIJ]k 6\.!N8#@RqHdZQT)4Mlmmavv#[T*ŊfR~nN0xk_JJ€! 1 HpQB!*V+<ftp\ X[["f3666??:\RWUaunc4`O=. / bl5p8XL[ۑHU(bii  :<9VhZVt:Zb8\vY~%7 H IIzIZ- $z5i$mZRz|>Z-V'fG|ZA7KִE 'rڞ@ItW?RRz"EJH5]pR!h*WA4͛r[a}ڬr js IDAT.vbU~rz``p.;]򾐟;E-Rj)slWRQ^ 8?r =a6m#O1tIyNOT)AIU4~!U/%[QʹlD^vm!sJ@(BlAםjgS@s>(qvF#r9vt:( @Bs z=D"x<Z8w*v;'>crrٕJzPgΜ`J"B!zRՐH$NqWVqbJr8rPTnHHLjn dE GzJ(3C`;_?~)͘a;_o);kf[6ցPT Iu?{ O2~U}<裸馛}W7xsS_ql__XZZzF#"`8q~YDh6p! cf ?ƗtY0??ϟ_^7xzh4 PQ J"cG'D  nTh4ʵe2͆@ Z|>׋nM~=::2J(O8RǃP($*)2}QD"ceeJ6 /^D0 s/6ƑHW^E,E/.KTΦirp# ;3#\.pRV+pZ 9|FfPrY M:Ԕ.H$CajjJ`x],--l ,x<̓H$Jq(JDĒ=kМ?Opttx<.[[[3Zj<:l r:yzJ@m/R}k}Ǟv3XFvY\߭N<xߌ{3J`\׿uCK{~l?c8q'O<}>tC}W5+^V /Awq&prxaDkeƹsJq]wիbh׽uh4^>9nF||Ǎ]EdYL&8NQb14z z[[[+WDL&qiɊv0LXXX P*$Kg̥h46099)l6#1'7[f3nf4MQxch `}}z;5!ё@+Áv-Th HH>h4&j8ժ &&&Pvjj 6 zۨFH$fhۘD\N:*ߩT .\@(h4l6+JpnI2.l6EGht:Yt:}eFtZ΍Fh X[[f)~t@@\Z sssX,9]dYx<Aj5iw^ǃRX,C!fVq*k`0 . :RDz\x= <2 (n7ͦϨ(Am+X3/..b4Ivݖ6TUqU»h ĉp\0 bLh;G٬g潨XM4ndSUg0D`6Q.D1W*DaFHx~0a2pYL&J%QJ%vvloo#jh4Vp<[,qE 4%lR<>x1jLq{xiGnaXvQץAhHF0tih0Lx}_򗏍A8D"q[0"_WWя~k3151>O`0u{ݳH$`ZOk8I3?-cjœO>) 7e/lΝ;`0o|^' NO|xꩧD"Ї>z ?i-ew)KՊ;npۿ~Ǻ/|UK-RK-~h4PT@Xrr2QK$z r({*XJ%0fe"@* zQ*.ɩ)  j9 dTj5 CĵðbLl6nFlmmR1VE2D**4nr9|>b1X,hZ2KE̕+Wj0p8rD:;;+҅BGGGXXX<&bbl6l6nXF"Ą(R9 @rC666DɻFojn[`0n+yGGGD"jޖs{ۍ] nZhX$dYc8N( 8q::$E]pA"մsPZXYY VӉ]mHuf6:b 077'.K@u:Ąp cB!LNNbww^@~zx衇$rĄd&ni}]*n׫Ts9ce]K @A:/EM>vm,Y;de3a.Nf3z=O.o6l6fMAuf4PF06Mrc 4gZ5{{{̱+6l"=#fffx DvR XXX`@^ \.<2l6P,%S N4Z?Z7*7|y F{x׻ޅ7(x{ރ~by177> , n&9sԧpIL&~\_w}7~^z+.]˿?r:q}a4R[oETB.IZv`0.0: ੧ 7Fłj|>D"f)^Pf3aXĪ=m\c`0(Ϝ9fZ&:VfR E&kPrrrRhKz.VfW^^xχ@ rT*Áp8'|SSSj*ٽbV0LT6LhZxK^l6+n\+_׋L&b`0&J`ۍ~Z@-մ@lKKK(b=11f)jxf/r9P:V+nf <8uj21x\m&Y/}K:L&ُir9t],--\.lJGvwwD#v-P2H>@F\.'Tn nZJmg ].:z<#H҉P6yr|  BIIhLT^Y:bh4?<˩2jV?aܜ3_^IX"l˗D`0"NBP@8F"@.C&ɓ'E5Fh4=fsΡV!AH̀VE<h*r@†Zii֩jt:~///˳d2!J9X, vjqpp%QV 333b}eƩZ?O*z?@k@/@P(^]w݅~c=ַo}+>`8|'^W;җַpmoŋ|y):R]v@'h𶷽 _k xzy?ҭ}{C>73 xq~2Tɟ ~Q˾/m~y{s}lr ^ַ[nŋe/~70??o|5sG@ 7M旖O~{l9?Po{ېNZ oԧ>V_E B~~^c^{]ngWGp]w'> qk;׿rw(WSN=RK-RK-~"hY^^f1U .'Ѩ@h4VBz.P6rxB @2N.,, AcffF} x\svBB(Jl9l6JDal6cQvF#l" \.jcBGjXJPT4u0LbH=)nbNO@ qёRe$Wt3;$6 LbffbT H@pP>fgΜG4ťKҕNGSVU Vjŭ*333h40 E<>Z<2hDZŃ>(v^WZ-]2gϢnK&2XVH&;FQ,Vfa_pٔ . cjj zp\p8xeL1ŢdYjZ3'a"xPj011!sETSj111ݎZ&h4C^G,dj|>4(P.eccnFC˕Y$OnK2!e,FXk(>d!IOЎ'rl tDL/'X@r}J;cfϸ)բHy뭣̍UUl&r_#%I IDAT)A _e6~ZM_ 6 =yn5ncCR9KqXQLVr02 ̦^T+fr. F#JvF#|8.~_Ϋޚ8uvN9 ҼFd2v . J&bsU?UJwvv$g\Jf3Պx<.`u8bzzsssx(~~A,g26J!@!ɠldNle׋]D"cV!q9Z-4 z]VGAӑ* FL&S~9ʜGZ\V%̳*hjazzZkP \"ԋ/BcnnNlc4W*h4lNQ0Wyй\6MwTUUB+pf3`0b<#L"jr 0ZT*|j*y n D"h6A"&BB*ظl6yRv`0HgEՂs9J`6Ł@ n#CtBףP(H4cxPLe/F[3~Pn<ͦK6"p}9*n%$##H53FꄨJE0/T 0zǬ~+ y<<׫ujTZ+풯<BDs96)4Vf2E~<_߇lnR m5湥\/F>ǔ/oh4gArܹ2Y`0Ԕ1\.|>٬/A36ktZ.!;լ~^jzZ. vO=u1f)v4fyT*a<DnQTh4xdy// {Yt]93хv̼ii|f yi0IdRF_~_q7?h)WAZj)F,.wӧһT}X\\^𞩥ZjZjl zqA/Yn7J,n8|F2\X,& BjQF"4 R)t(JX^^^X+vZH&Ø7 ͥw0rɓ'apU |>lllaeeE&DKEM^GVC8Ov-VnV8::z˅d2)& R pX%f1;;+fSrI͉n+0VիW$VUT{MOOGE0[DJǀ`H$"f r*6 FC :V >GGGM)'If*1glVV lPHz]knW9U̼V*A zd]PL1׭t#IpzWZ)e s獲)*?OЬTi@ ͦJޯR,155bEeq c lBK%~_(J~#5*bǃ\./r. \.8j5 c4aggX |^95¦,pr0jy_P(  Jc 0^4EQmD"iy]f~*UFYjx~7ocii'kjZjZj} zqA_jLMMh4bss_np8Nl6 2qʉl6+6z^r: ^NƘO" Zz3 ٬Xr:8L\.'ZzfSe2d2vP0JXD(Bp8D8FjEF\lF5'-0099bnT*3gΈ#ϣZ"@_2q<#0HRx_f]B!Qn4ZP(n{ 7p NqppH$h4*d2)pz]քlH&v'n#KSAVdB& b(*@ N#qOv:8<<FC)h~ fzsF#PrR@P(7tZGm%,4 ٬@z*NSOu-hD)l6e 8F4@|ۍ~\.'VlFQ4岀C8N͡\.K.$q8s Z-֠j qy8qB5BjVM|>2Z-jŚ^jnjarm~lll@{l6˸Qqh4BJf&p ў`Y%WTZ sY^J%'[ڎ|RHJl`DŅ!tw}fR<::\.,>v;z=199)16Mu}>]#NdNv&IJL&vl6Fq=R$VFHE<t0888@(lF&Áfux^$ g}<fmB!y/gPAZ/~A꟪ZUo~/} W\ߩW-RK-RK-RQ13jL*~L~8N4zXF|>8N EA<D.~?l6~ӒQL&J055ݎMt]x^bh6D6`؉ bjF6L&(c6N䁎cLNN ͢bffb=DQ,.. |8{,F0 QTWb #  ĉx+_T*f\zFX F> !N'$L&Vo:lJRף\.jm J(|uiyloo j6888hDR|,,,0@iL{T* ߏ5iD"hZ Z"k涶133#`0bjR|?y$nc*e=L&<N'2M. KKK̻jEՒD"q ”T& ra0pIt:F#LMMԩS(J(˘B^GADPp8=In6jFCjnpJC{{{(p\B F#r. 4NJ%B!Q`P,yb="WfF#"5ʙJ}Z3gS Nݮ@եwJ*` ISիJȪFZ? &TZ&+툕s Z@:cZ4sJ4-DžsDqDEDh4=5 &2 9(Jbf>l& NS^/kJuO|V@ebz|l6g5D񻻻+M57|(,:-vgΜA0~^jֳKwܹ~Ϲ`2aRK-RK-RK-z!d2Cx焳Zjj>aii 6 f{{{2M-`/ bǩSDeaZqy\.t:4M3E!Uf.s?{Bn7\N#F,Z X\.<|nzqxx(Tvd2[e  bZbvvbj/~ 裏… HӢzxaqyXV(www$U)G\Ą@F zTH9t:n{{{wz=.]%0wwwxFqppߏFh4r|Lu,.. :NR)~DQOFXD^ K.aqqQj>zbQLE/dx8"ϣ\.Ke1+*666p7bmm n.K@4+| L&aX Q,a0Do2h4 cV˾n'1d[lVjE&jN6Zw @@^G B:Kp8DYt$+MT TO6M~XV3%U)A,QdfJU/2t<˳TK%.a#l3q ݔ.J|# J/ǜPp9\]n_U+SKHT2^3}V$s\7-yh6&hs`+2m 뇟Snr@n3\^}%CϩbAݖgrTsg09tJ"ݮ^p8eXhCannNV bzlq8j.z_p:h6lf`X>XYY: fff!o4jp":馛pUD"8a6׆H$|;Ҕt:vH$P*`2qϟǻ.y[??3=r|=_jZjZjZ?#^~E{뭷2$bzz~?}NS&: QVxt:-U&2zJjQ(pttӧOĵ>ԳX,rf:^R Z FQ L/^X҆9fvW^h4tĖRln SP(UpD& $l6pE,,,,t:p:HrX,v[͆}ueesV+Ξ=+J@ Gy3330Dj0==-fS& gtHR(IJ Z|fkk x\&p8X,lnnbyyVU@?EETu]~8q\NrcZ y-3cwffےH0J)혽^/&''h4pV(tbYV%M Lr •d\ =/WikLIN諴& Ǡp Jkd^JB @3_ BL,ZmZz9HgJbҾ[/JmF8Ǭse^r4Cu3?<Ә\h&D}2>k 8^>KLeN<.Cшry |>Ql6QZ,Qz^^ D"j qp\xGp8F%%rYޯllz<cvvBCL&j+UpX%-HDb8 5޾| zzA^W-RK-RK-R맽TЫ֏z8^ܕea%-a6f2D`4tP0DBS!hr`6Z7aooO锜WVv^c}}GFLz=$ h4ۃdB2_rZ-Юjd2zt.q9Ej55*@_TI]F#q4 eD" TU$2q{Q1󉒚h4RV ǃvJIT*nDQ$ ^2Mrl6ŚT\B!9H$" vE9i}ttyQQcQuRa'jx,%,p\F<PTDnf!)ZΔ?B6L4MDdB(`0@P@ ݆(2 IDATnǗ%LNN!NҥK8q._ ӉEic=&YǥR >^v]+9FQlY=v,ˈFb{fQ,1;;jz,#rcc'N{YlxDE5h4F#N'ѨLnsa0b2K0 O^c}}]zfHR0hp8p\Bz(X8P uX]]mχd2)`raX $jRp($!n|ƩS$l H3P Ԕ'&&B!QNMMR@YSfffp8-jf|>z)D"5LxfF@ J"Jj1==->99]DTe0'wwwaTt]|>f#H>%mVjXZZ•+WPT똞 677Nӆ)?l*PZTXchk?SOs0糷T*sg˗=p {\3Icymnx<h4b4!JtDZꪪUՏZ//uZjZjZjZ?XWw.//ˢF$u:2a4 Qdt-2U,昣Gە+W0;;,,,n#V lDA$\:ۍr Պt:v Ʉ`0(j3+'NE&`0,z!h4BT¹shP(t:dp n7n6id2fLLL`ssBs?{y^m9 420<r*\K\ֹT EC3pd9眾usc\ wUSD;<;5^kmld2Ii8p8DVT*v- ʤEM6`Z r9|>fL&O!`Tc{<8NT*mg&dm݆jwssu Cu\CKvBF^.HeŅ SˉzfCL&50bIMFb&,KxB2+@k*L өkP@er*ģGPVH$0 `ZD$ƃx<.2z3ZzxGۅt:|^'`0ʽ% ~_`(CGgBW@^/IBJb өm*ye3GiSToJ.AA)OmUfl**3> @MH`IHHJh4P.SZ uc&~O4ק/VBlW'ө8O=iKmLXqR1P56G}S]>nJ={.]64@m}6I iBl嵣qzz]fi{ a$ 4 F#Z-uD"h4z=iBu&6FYvhZb1d2mѐc|uu%V;;;8t:hh4kG]^^"N`0 pT2gi\t$2ͨj2fVUZ8diQvx?ypOB0`0V,Vxϱ*9VSqq7rX[[Cߗg[K3|>D.CRA4EلE`0,t^'|vA, 8 LJ[.C*ݻx zN [$U3zRK-RK-RK-RKV1@^d0Q.H$l6d`2Dh4p8K83!_|G  X,bjqpp׋b(ИҴeO~X,Nd2Fłd2 χd2 χFfBh4x}!!͢l" "Rp>#LP(@!! 縸p8Y/BTt*V҆p>OiCΠD zp8PP*jGGG駟b>#a0`\wFqvvǃZJp8,Oiy&jX]]__!!b @ŕF  |WQ嗒 @@nNjfxd#Sp8#Z-\]]a}}]^/L&_}>JOSϧOj# Z ̼īWl6Ӊ+x^$ILS<{ ^lrV/mi 1Njxr[oa}} j0b P,Qհ5w\fjd2ZCP@&Ë/_ 8L&%6L^N~_KE2D,~lkkkjժJڹmL$mQSaFEZTBA"@@t:|zxd2)洊vDmn9Nh`0ٌ`sK-nvD|[ Je)yNM&dYQt:Qn&D[.:4 (Lpe>df4{*lὑV[7 a~ʼOV`RףK%- < d9~oZ*:*Rqe)yJ+΄BZo*]-,CXJ "[G0C6:SE}i0b|Hv #D"i_:T X F_}D#}lnnxLH'''H$r  ĭP($ v/^@\66>82B!<|PUYѫ^RK-RK-RK?RAZw>xN777b2N',(.ZáEY*7Nf}W*"٬(ik ggg(B"dǮ/ϡdn`0nC Nn[iKf0-WWWbbt:t:e,, YӡZx=!^,@3 IDATH$x0nqzz x^4 \.L&\__#bZ,4l6|RTf3boo777ZFC dNtTGn[? h6jFTKw\"b8l6n-W 1d\ ?>St:DQv|VT*lڏ*Aᐼr ۍd"JFۍ#QBQ}aZX,tPTp]c"C6v;Z؊hZf&88<<{'kbH^ՐJP*`Q%_wmZeD"\]]IpbD @$‘JH$.tJFA d2:ǃ\.?EJgٰ\.nEuf3L%3t:jD"bM;wfR{4ݲl9T%BJx @,kyNRAHHHOu-X+-0S)ʼXi^nfV0g2ܲ-V)+!-.ץ_ep@%~s.Bh. W27VM74\ǀ 9JxkmWf貑T>Ŷs<X&DUll \~ȎcQYT9`~5Q*(v]`df~z›M@bZJ|DەccA݆fg I :<|>V+zN'VWWs}x<[l y|g}No6á4gFv, yr܇!b9SDQ|>ڒZ0t5N-L&Jxt:-5plϟ?WAZߩ~`7պY-RK-RK-RK-...pxx@  bggGlE=`4̹b(jע6P`fC(B.OSLS|h48>>rDV^ǝ;wH$db`0LFCZ h6b+lZ'pիWZ,1xzE9[*p}}p8 -JGǃ;w`>cmm 6Mm6JZ6Ml6aQbqɜL&H&#ST tZ4E55?{ {{{jHӰVba0e*F^JW^ٳgrxxG"!$w0 jU~z=*v(J%c6!LbssVA`2#lll Ll\OdJEb"[oR$IkmшT*h!ϗw(pzz*쫫pΎ4I CG *Yn#&miO8K6fr--bUoℸo*>GB{f*3yPlQ/FKr''CL-TBecڗs\6鮠lPWT @Dݖ G+ \oXeIxpss^/+w V/^= tZTnVxLw:Z-K*@^ WWW;#R$`իW0LX__GVC\ƻヒd͆5 |x$G"?N'Kan~\^^ 7Rp8dӅB|^w0j/KQ*b@C,t:EX`lFX;afs|. FCv;6DM^ʊXTrNOOnbiN.fAew t:xnooKlA{03\.t]QQd x]\\ mkk떂Zenܻw777s4 `۱bvJ.z'O !l6KvFAߗksVF!~_TFQ, 0&!9(n7jz=r><<__`X裏p}}-pd6 fnX LjFX[[NïkR)$ LS4 QBWUv'խfv]NTflN#^B2D<^zFd2hNP(p8,J6ڲڣ5y6g'W*Q1WhT*rxMt:4Mz6l*f $$t=Lk- ܔjP2h?;NT#2^w)Du @X {i3"(UR< VG%<dST*`9a<.J HBxd"fPГle3b`:29>go;o*ynTs@P*`fi L^%Vr|kp~񈢛klbX,p8`0(у>z>Aχr ӉN`t*!tNNSK\.XWVV`4;F,`@\`nl^nNG ˗/޽{Wv^ot n[F%8& Ҽ y$It:\__c}}:U;ںRѫ^RK-RK-RK?RAZw?If Ӊ5n tq6a\t:>~/٣D"]FUmXVXVL&~l6j5h4~ CX,"c8bww`WWWBp\hZvV0 x~AZ-} QzVS( 2Mb@ד~n ͆` +Rn^ÇDJxOb8Ji*rD>fXz}$I\.ٞp8,9TZvee6 >F@@ Sb- =zlp8 ɄSQF)Q8y`}}]W %Pxt:DP*P ^st]F`P\:fx<.1%y4EEՂD׃jt:El6B @f1N 8N.u]^c0 a4a0`<6 `Z H,KF?~ Պ5dYL&B!s<~~~l/^~ϟKcWk>sQv:\.|0L&qpp z6!Ɉ9z@sc#|>H$^l6  ,0 ^jauu^>OQ3jvb!*h\d"v0̾%TRfRKIH~|2 |[J0JJ`e,.7Zi!L J>a6c *Uzvsϼ~i3V 9oBn*z7\PTZ s}3qt48#ie4C3VEney.;FFݮGذL\./4 DLSqh4|ts`ĺ96 hFQ. ؐ90P(`0`uuvF\T z?F48vQpxxD"!L&X,&f2R wܑ5^' u֫ժ8( ,MhN~1zG|r*U;u zRK-RK-RK-HJj}EX,Inшh (5ڪ6Yx#-J*tBB,HT*6+++3B!bJ@Uj> }b1t:eQJ%8B!<}ZMr8># Aբl - fb}>> dB~>_&ik0P{I6l41 BX,2>X >D$uZ[1c:"l6o5ͦh$]EMߏ` Pv y<9JU-W V8d"N rܕNg6[jc~s߹<*ɕjoͲ1\{JmYvz^rӕPLj0d26BG.`0Hv7U@,iLHhM鼥P&:^W^m,Kn>88;wpqq!1GGG " ^_[[y\r<^g6 >`0dB"@BW_}%x\Cá<C-4 (>Z0F! `|>lrePDh4j}RA/TЫZjZjZjTЫ]/]YY@vE1Qp||,YlWZ sO*f3Jzt*jl}ye8Qʵmv\]]I`@XD<%hD(BRVh@ ^L&5-M&lnnJCɧup:V 6ժ>sQ_^^Roo0QTdb=Ⱥ|NC. ^O,V+24(Kvww%Gt2ݻdv;9...`6%oee@@oonn$X`dBՂ`@$ L&p8,YFzF븹%BRq]5A>j3f3QsEe1VVVp~~ pX8q `ZQk4lnn'''bLĉSbZl6尲z.vzzlmmV˗HRh4t:X,zzt:?<^E&eCLu,D&x<ƽ{`0P*`4Da>LxĮX,r9C gϞHKyfІj8 h4K5L{rN'E%~_`l6ktp(vŴRUBE%#$TffюMaڻrJ̢%dx*Y>O0d*\X+!oJ8{%Ǔc@er쨬V2Q*}&~TfqL0Ͳ<\>&ǚjqzוйnpu9ǘN&Jm3-SZ,+- ,㽩 J63cD8[ r+++888Z-*x<x<9& +++^g[,^5OSi, EZ-i Qvt] CyQ,q`Ft:E^x4_tCS \i"F<lp8 10NVx<.<˅r^'T0b6ammMԖx<.`0͆^ T*t:~Z-$].x뭷Ztb677x[oFQn0vHRp8t:F\.bmZ F&/22믿/Kx<XV |b8W_}VZvk\$6e NSrPapuud2nkɢ>J"rpuuVd2 Ӊl6+|ggG2:rZ-00̱X Z|XviQu݈bC4X, ߏt:| χ\.ӉMb0 8<<*>|縸nя~3d2L&|>b1ܻw6 HD;w`2W_ŋF݆|dd2)h4 Á]`eooV zX__Gxdo IDATp8Ma\IՂV&ܹ#n /^\t:-lx<TUR2ɤXw|nTUZMt:EVx_2ۗёNL&x<(UF\.'}hja4aooO{BV`0@ v-z*Un7%~"5Dͬh4*h4L&uix0LP(l6l6Vl6 Ʉj ۍs?FT\_Wxw1LPՐH$_ϱrO>CRA.C\t:H~?2 ~BբZBʸv:W&_,p8899bt:49vn^O`4mXij`4p8DhZ^b ^9&y^:NʜYLky;e3T3JJfv.M-Eh0UҮ|WJ6'ӹ V*R0x3O ]j [8fJI[i>1V6P +džnU?S=L_RrTRi<<˥aR]e#˴-;ߴR_^Ca( ^\.h4L&1a2$.GGGX,HRpFсSG*xŋx뭷0LPVF}<\.c>#|OgϞݲ{zLyχJ|.x ^/PhZFx`s˅/^s<VT*!LzrYcj899Qj}C+zjZjZjZjJj}E`x&LRB(K6fCP@\nG:-{l6FEi6qssX,ٌr,ل'''0p\rwr>|t:ZχVr??t:x::~A?2p#"9jc,K\.Kr6VE:g} J_sVVVqrGZƃkl6b1Q|>t]yB!~d2a{{[Td>Or4ZN14Ɉ2yR& PHp1FhllFBF8֖Z>_]]Ν;`0/L&qtt }NRfÁ@bQ,?[L25 "Ja{{z]7Uvggg8==| ^,C^^z%Ts.YnZb[MtӉdz.`XFQ΋p8,6fYh4ewPaO9 Z^/-$UJxJHHH`bKk6TPվ~SLKh$"䲔v&U%~ѺX *pKQj%%xR}s{J'UZ*- [*КtD@ /)h:Y'cZ8S]X,$Ϛcݮ(lFЦc~p2 %s,xnL&z=iX,2T/sV+(L&F4i4\__e1p8t:D\"dyǖ⫫+ܿN/_k26 (x sx<ߏ/Dn~_1 P.tJdlt{EXD,`H+ϑ籹N# j*U뻖 z^RK-RK-RK^"}L&R@)ш= ɟ z4 =^|x,W˅O"j^߇].K}/DvvV%v tnAA8FZ.߿ Z-j pg} \__W"! +++bH$Vn\.dgggp܄E>p8 םN@|.\n`PR0t:>cc^K'r$vgggjNXYYN^X,xة2̩֠V Ѩ(}ZXFd)[,loovK1ݻ'yXLbfJxt:6*4;N'42jt:E&7vqrrb-ɓ'X.K L&z<EZhJEr4:.NdSdž+PH!J1-vnC c>c\X,"ltʑ9TƞPfR )b <E\Ns9f*ՙXU*Ti'jZ`DX~OಔN6Z=+ji[fr|nJ2%!~*suG*XJ7-|Mu/H>ƌߔ*_[5յJ%6٨\.SݭEycF. DŽz$*w&Js?jBi?@cR״&rhg+ C lp8x;H$"& ;;;8>>FG$ՙMT kZx^e rV{X,AQ)jRjX >H9Izqss#5 P*P }dYJ%qB!}Z-\.u\X,΃xwyG̡PVlJtvfNS,%X,2rP kyIF4' n}>?&VpXTPFVV llX[[t:rC0gϐN%7ncggpp\0bN=iɔOp8$+Va0H#`:X ;NF\\\?FAN@lߏJ"VT}L&!ˉhRA4E>Ţ@w]6L>O f F\hTԧTJUqAOە``P($x ͆KDQEB!L&Ol{g ^qA C$x"RR$Dt:݀LJ)3\Wvf@a=똖^P.ٳ/@XBl&„J@K5RLt:oJ 10ٴ\r[Th{FB b7Ӑ#͌FҎV*|䰇.C֮Wj&p9#HDNVs3ڃT t?Ӭw_]LJJJhlVjL69fZ)MԔML&X,=XeVxS<:S6ApkXJ8ktқA!aNfuZ?zl6DQfzmhd:NQTx9.&?#2F#&, p+++h|bsse/$˩+ʚ޽&;Ssn\z(t:=Ԋ`0bD"JɄp8pZP(n~OjWUzJ+J+J+KZEg?$ISAzBiv;DQ>t:<L&:~?>B6WOOOzl6Q.błX,ƛ. ~V|t:X,Vo~IX%qyy YL&y<c6<B?|P(Īt(X,Zbkk B7Kk)NNNxsrjr9"Jcئ>[R)L*۷o#J1:T*aX֭[)n4,//#LboobaEe;b2 /&.//a0 h4Y6M~f3J%|7l͙d"6MV8ɒ }B:憀vwa6͛7ZƠ1:lkntL&9VeV~bpN5Qd2Y-W,xX,ؖ6!g[It|ϧQ-nX%8KƋԗb^13tIUKq6qe@`0p`0`>VԐAMV@J_inZV"P: qbɅƖ@:sA]nGZf2rs5c/tth}rsɕ2X(Rl`05d2  ©Ώ{z&[SXuL&gEQ3!7ѳn#J1!޽#9 ^QVWW( F^/vww9T*|>oՊB!znZ-ΐ'i˅f EQh^ǏkWUzJ+J+J+KZE޽{zH$t:8<<ĻwpXVE0B MN'n7罎cFd2}|ۻR Y-,l)I?#uQf$-Zu:\.qf3<~`Ə?__r9aHR(JEnoEa+NlvUFZ X3^/, oʓtssn!#YIyh;w8wvvl6pm4VWWa6zJ:Hh H*KmLX:R3wؤcQRS Xs9t,j\Ziժe& , +AGRNjƁ#)tHEL?S+zp4t 4TqMDl2a0`XTt vt tkN'{:7tje(Kti|YT'jb"t:PV1p8zqvv|ߏH$r Fbb0.;l`gpXZZB"p8dN=5P>:5lmms@qcd2_|V""F qZ-uHRL@ Vu>)Ðepϟ?͛7!2^zb;w{lF6 t:7/5*:$IW_}^W׿Ư~ jVZiVZiVZi+ jC^ʫ3ͼ!G"Ͳ8t:t:$6&fϡp8Nx6`0l6 EQxS{&vww YJ Üɪ 0 nEQ $It:kt @envVC`xԧ^})tii hհi^VDcܽ{pbz}"C. r9$HÁ=evloo믿F dB,cu3 ^/[mʬT*xG<G` B[[[8;; 2[D0\s\<~zv!rRt:2, ΐH$0ϱn PLh %#Y 9hZ dm n7f>gz2vP(vcyy+DQĻwX,jpZ:Jbn#NV!h4"`<r:`0 b =فaJ%TUer9x^mV۽{|'''X]]E>g%l6CEZ2CoNx)VVV8Ö@ nl6T*V_R7*/ h(lKuii zs78+jtłB@Nn AXA빙jETbE $DКlpdKjHLVj [cUWRY UN&yjcD%Ifٴ4j.zLΛ1XDזq$8I=l2}J6ޤU+ !'|kH HdӭB1gs1A+++naXP,zvnt!E0doH ~=ёz^@VZiVZiVZiV@V?t+}t:t]Έl$ /_r%Ahj8rnE^C$ܻwn>EIJv~f:D&3gf3L&>BKKKZkY)nx $A'O0@&v1ZVh4Zjn#Znܸ|Ǐ3F3r10.//a2q}" oA,cn^9޾}j̻w\.T*9rYyh4f l63" " 2LsogYLS\kR$Ȫ`dxP(D"f V.)rz==߻wZ Bsv;C|Ӊh4 EQwB3'( 71<{ N3)ϙ=޼yAt:YM6n'''l\,}MPԅ\M4YW_^^rD.n߾v IFAX,BAeK|f8<Z7nDQd% t:mۜ{ F958Pvp+LmuIa^^52$Uʟ&F]5$`K!򖾟Ttt|t޴Njkiu./YՒ=5}'iR6RƫUԨxoZpy$1Ȥven>Y, j>l6>ӳK$8, kkklY%\.[D"^3DQ1ݎO?/_īW ;нI5S$C(/^T*s%F<g?xkkkFp:\kM۸\. aaߏ`Kܸq=N'r^xd2 jJ@VZiVZiVZi՟{iW?F^ |0X^^F6NC4)1677YKЊ@efXc[ZE<(jdB0 `0cHv>EQXpppۍ/_e_|X,ƛo_zd2JQ @ j5xMDԱXmX>... NNN`6tp8Sev(bgg7ol8KPtb4t7Q3 bDQDRa5hG8*޼yt: / ZN~IH$ŋldv1X aZμ|ˈVlJċ>o߾E2p8D:)F#BH sr!N`6nfv& T=Y%z|ydKeI86ACEw5PdK2yM&$?0bj<EapI9+p1pW* >/'^`mmtNɄ[lCDQXV8FVцafv;VVVLИv]z=vYVɦyC -^/f3 ۼ"͛v  v]x^z\]]AHz|nt@x2`0u m0II AI09SW1&Gֽ^ 5~}U+|  $HG߫ V{&J["U=.:6ХR_{+:VmM,\ScsRCbjhNYjOEmM CT~ߧ44^KJTjRcNZ<KKKո_iX,܈y\u:[$5tшyu7DEЗ@ pjrb(p8`As\\.H)L| qciiAd<#Nٳgtmi=sZp\hp:x5~ӟ@:fl6 ÁxfɶzVtO5^YYVVVp8P,1 믱;w]g~FZ ?FcB.^|^Wi@VZiVZiVZi՟{iWԠ7N38::GTBXD `@ٳgT*fjX,oxo,M&߿ pacczN7I}:8V1s85R tnjg&%`0,ˬTeInٖr%q׃tA}%޽BZ޾}jhlK֘d8p8`0P.a2Nխx.pYaee3wi4`%+yٌ Vo3[ӱ5<5(h4d;M*v ͆Nnt ncgg("Jl6מb1R)4M|WgaH$(IV+*9pzz~nT zFG\b,x [S ,)<f[lxg8: 87d2pR`2@$\\\r1xjh4rY a6q~~soPb1fl"IxnLFf%В-#$y$I\.AEDQV)Y,l8??G6,d2X^^~;s(8<bfHNngA^͆Ǐ?E>|>G4E@2:X9Z,y?{m3f3*jb{{FbUF^ 86n7n`R#Nc<'7om H$xXt:( [s'eJ޺u GGG HI'*nܸV qM$ImZE:f@ 3:==dB6, u:"dd`YYKjݳDZ hZP~h#+0z=2l67(VMy~~^OjVŀd5 K@>8(;F#,)w(!YҽG``:TdoX8 ^R`p"KR2)O*]z=W;d?L@ "gB0)\ F}CV,bq:n_S3k`]j%2Wji0&xMcFWf4whv;I X,8Zzf=сT4Ytn@aш$DQdMd:=>i,.8J.n7vh4"r;= ,l6.//ap81x^ kd/IdY ( 7v~.ƳJ^p8 Id0N9wiiP( Jp b`kk F^7)l6#a$h4B$a8~6d2 ͆lSb1f//ۈD"lIJ9nGGGFploJ4$W ֓(;Bgggu~ߡE.e잝q^gXbeɄh4gϞrqii L+++(XZZB$;wpyyslmmAQT*A, 4 CEdZ9Lb2`gg+++l;Ijd("|>>|FX,łj~B pgggFxdPWEVUL&V~B!z Bdp:f  qyy EQ \R.Q.EaH$嗬<'nZ$IHR"K_?k4!LÇ!Gq/)- ^/ە~vSLMݎV>>\vGGGl뱼O<|>LX,S _%F#* 677quuwM;b1DQ|G}Ķ.~9߿?T _~%[Vl"auurB$aee<(5?n7,& 0F#^~ QH$XDt``qvvt k)^)Z$ |>n !Hv^JfZn׃jERa{|pQd*V9dp&Vm)Υa:OrQʤ], $l F#ɄժjdTLTCNWz Sc W:W} jeRX;h_RI\kA'l&U4}6t- 0]/skjfc! Bd21EN]5)Zm>L)- =F#n fb Y٥`Xv3@(n\.bH$FI#L-&X,X,|hH&d+Ir9Bl6*7oDA ?^/, Q,a 8XQlnn`0pf}(h r& |\.^H$'O0w:s޾}YzӧO!"rQp:/5Ы_o~^J+J+J+w^~"'r7ol6C.9Wl6޽y $ 63q X-FJłL&mR)V߭p8L!0@"RR>^&{ne@N&ܼy\D*^/_ @0MflfC\V> nkkkX,VX]]ETcdY◿%DQD$a2Ͱtz|b{{sYVWWqzz ͆gϞVᣏ>B&``0h4XtxxJ`2˗09H$`6Q*R=[0B!r9 -m4S6fhf3(gגU(hZ8;;hD*x2+]m6,j0ͬjWF&HT)>+EII7j%)Aru C&DS`0t:Ov;+ - V|4TgS>.?N&XKS \ն%L ^XI*NZ5]l'MLwKmUM9BV:림T7RS&1E IDAT`+ES%z?][Rm,c d2iL)Vki95{kS <5x^~PcFрx͆= CaR)LSB$$ VSFjE1. X,dt1u=>*z=lmmuߋ"?_UUv&h40xll6+)ѣG{WZiVZiVZiVZKZEwkkr L&aZa6!2b[ʲ̖hz(/rXw_7n i<h N`bcP.`ۡ( oL&a )d%tY?f3DQ5L 01<@R.--P<ݻwnt 2}|#]Va0^r%ut]V&R )I!h0XSi4MF#4M,//cooPxlш(pݜ-[*01ՊNVϔ'Nqyy2_www0LV_ zt:dW^ }L&2A`hFGGGj 2^/[(t@?lr24MvAj5L&R)4T*( 2Ri#,7663t DwwwZ. IU+uwvvpXV|>`|>϶kQ|~?Ftmm zwY^^B<lώ=ath4hŋO1nmzx<^__۷fnDQH\.m@4ekۅc+c7 㸺b)HVfCP@Ղfs%bcZfZp8zh4lL)oSN1)d ]V5$)bFO;AbeMXq m*Y5xS0e#@Hje)ASNz=>L$XM*Y隩u ʩ%;`AV9%@K^Йi>=)xI |XV[PLV.9) zL&m'`L`?uA\ZLԹ\sN }6gNS<53PFݻwb2PVYPT0A$ Z-TUA4MLSȲך), qܽ{ WWWp\jl8992>|ȿfg24 m$ f/^gnnhRwww!I^|5X,4 nHχsBQȲ_}dQq||7ohWUzJ+J+J+KZE1yr8Zbmm ?ft:j5 |hPF眉'2& P.txp@׳Bh4b{{:njr9\\\`ss )J6.%JAף^3x<NK9)dYp8~y·nc4NzX]]$I(ef. $R`X L\.##N˗H&T*B"X,rq&uՂh,f vww|JnK8sEX,~ YwKfɖZ zlnnb>ün6Z@ pVqcsJ%x^B!n<`0Su6V+(, [HSpۅjfc?F7Ae~X,+~?Rf:>sR) J%\.Ah4By<͛70<h xb!RRA$Wppp XV(B,ڴh4"X^Vt:a20ˁ& dVjn Fj NfL*bu=ԑIЕ*9P:ewpƔu-dcM.auұ)hqh40LZc#Ux<#%2baLcBtFcuU5cIJן>)AXj _44Y@{8l6<[4RSbg, It>g 5t:Fm Cfx^`@L$54 `>nC\.fDzt(JcOMO5LzT*6|>:"t`69c<cd2(J|l6x,x!n߾R'h40|$I0{vAX9NZ}Ҭ^J+J+J?@V?tȲ̪v IxӐ* }jP(V^H$ٌ/_BWmHV ϟ?g}CL&nB&HшjVŐ2{A z%X,v|㏱/_vc4n]-)!J~frsdYlf3F3^S/N\.իW ~HUl(2=z_0Z]]LD"fn˟X,drfY!j0>!79YC!bt]\.OUӚL&4Mt:$ z=Ev", ?,%=zho6@So?ݎxG d29\.V+h4P}ΔNXYY|>ϱ`0npX,EQn P*pvv˅7n+]S5`!hD(b+vx<::yG7Ln3(d2H$PDO>$Ipt:v!5Y89>CL&AAQiHZ(J(pݘf䣣#&XU~Ç%Kl`*wu.$IlJ|SH1,#)fI;Q1ӽMV%Gm:򛀗YN%J7|Z' $V\mtKV%-iP+^ 3Yp1% L@5V?KE^"$qf>[L2uRkFy+7h͆xNs4JwjO85IjLʩ% as4Gh\;5[lRkL&l_=`1)lIaLslEjng^TAT*׊C,5|m60pX,| 2Q Ja4,ZxcofNCPϟw׃(E)kPe$X ɞ֭[Py[oJnR)ZZ6gggy&x~V^^P(O?e@f'pv4:6~_RQlmmAttVfl2ۍ\.^Ϫf _@3T})A&ju(V,T*T*akkig2\]]vﳪV0%666t:a6q||=V͑vjEDB<Gp8t:EXlƻwPVymQeRjn0 boo:nX BwV N,C$6۷e*XZK(f1#Rb^v~[ +>6ͬ.&U()hII(NԟfXsRҿ s20#e%n6!Nc5 5 an9:WEQ8jW̍XvX,2zc8:ǏkWUzJ+J+J+KZEWNOO/m`i޼ T*bU\.e,MRh&UY `yp'Yclnnh4p@EEn*Gӱ}^G4OR$ERs\].ׯ_CAV;%ZT><5eT~KKKAJJpdV^B zdYܸqUCd,IXHn(" q*<?K$C <b.// (ӉMt]}f3y.~ omy^]8fdW ޽bajł\./^@$|嗸w& &C>3yf3N'Jn7gOcXr>dh fX,믿F0d0 Q, ( tfzvj<.//ǃ6z-Bzx؎g2_rڞN888t:$Ipݸ>666 !.74Mx^L&L&gǙR kk7},)E,K-;7hPhM-mr[EE&M&F8lY(R܆Y9sf/7C477iErx,s|~s3r֑ӟbi"L&Zf)7 bb)"qɩKjxAG+AdJWp&C8\t')w"Z.uwm4ӲnnfQii(Ba5Or6SD6]w?tvҺSlJbGoY4cH=E龿3U_\H]TM'Ф@<̦IT4FU*'ucAЕ&d2vu8 t:e$ bjT@q6779]"əMIJjjp8P8nsC,CZEƑ#GtH$%,//p`nnx $ jW|{߃G$(d2pQL&qZ-lllpD2~plNnW\AR84yܶ>\&ȉJ ٮh0==4Jq2K,CPVVP(p% fַzk $H A $H Aź|2^ynD"|;9^??Jٳgy5\˿ ^ш__CR9e<3P*zo$fff`0 x<d2qLNNB&a{{V BXH$db433γZnQd^V rt122X76ϣ^ɓ'Pըb `Fl6vm[h6p:D"(JKGɍXTV199 FCZGuvvSSSh۸z*R)">νF/^DX3lwT*FNtff׿lbee6 <(D"N<~j^Vx1$&''xX,dV ǃd2T*jaqqsss0LfX\\I- H3\V  `>`鸧"e}t:!ɐfQ*K#ܧO\Ѝ(IKS 8;5́WrS/(`$MOMPbr9G HhZoCݭ~7l?w+/MnUEǪ?h%*б("a/O 7L;+ZZ~Jb)Z[*r;rCmaV{8#ߥ,Hx{gNrk4uTUvt:0,!rm$*}VXgHzL&R ǃ!v:N,//#H0DW\1;;I38u&GV4 2 b1渧v#Lh4rAPp2ӉE?~7\r'O: v-t uu:;[nass333X[[ŋ133c&kЄH$^x脸v IDATtZ x14 "?DD"d2ɟhF#1<<\.׋@ yiloocddpR)8$G%8z $H A $DGhZ?ū E|K_q|K_g>v|cC07 +ܻwgJ%,..b~~O9o&l6=c'9z0>><6̧R)t:t:t:$ vbe8B"A@͛P(h4p\ufffa6 PL&CTB(`:z=\.2\.;^/?`o4H$ uJP*FȓRP 8 nP(`qqbB{{{l( l d2lmmT*رc~SNbj"LrA&!t+ l6J%DVӧo~FJbύj/r lllX,r19m6wb5 (t:@+tbgg^A͛7q1>W! `Zj0009NNwӉr۷oرc0Fp\fÍ7pVx<XVd2h4(J>^/vww}CpF&ba6x?R&, tKF#r9?2 vGZV9ֶX,r-^Eu4zBpw&LYrB;"veJ  Qg~ | }ދ~/0 $@Lq%PK" |T*7v}k pfl=LPrVU /"i? o[Q|?#ZrVN~FýԻ,HPVrĩZj $ Ff)Jܿ:mO>^3oKKK8y$/_:8_*ZSSSxW~W^^G׮]v;$|>^/,&t:hV!˱1cmm SSSE8믿}cHH$ܻL&QV9Bpa>d+J($J8&8z=w rEuE\r9b1Z-vd2v,d2annR&='v00QVD~??9V\C bozqX G (J beer=ZTP(87P(СCFqL&$FGGݹZ [[[8tZV*yAD"zn6 @T^H$hdR>vfbb4 Ǐ#իr |>nݺn ÁX,R'Onc}}a:&&&j#|X,F$:jOp\xXc}}>d2A&CWFVBǏh JQ(V,p8ٖJZJ%r JTj5zz=lmm}J%p8R`Q,9:WɝR8>v;{F#6`8FNn B Pt0S)9Q@l2!'9kߛ: J﫥qB4;pR3Q~KNbѴ}Dp\.G^FaJ 욦#Lۿ. hB1tU*Z-ItNR_0_Ze2@rT*]ZwV*yI1N4>r@OZ7봭}F׉'=ԍ^>?i2J6}~-E욷l0Lj9X,ѣD"f"[naii >NwޅT*h4hZZ-4 W*B! }~AD;;;@.MFJ8s vwwL&T*SOARamm ^0Bv|_vHRlp8VVjP(Vh4 Hx>l 8RO~$']_@ A $H A ?H;;;ٳggKKK~:0XXXל={b7oל:uG"-}W,ĉC B`{{rD fv0܄hDC0DP(@z݉'n;w`kk ZLJVrsssT*E<D"A<?])aZIO911\qƭ[d`Q(0>>Ύ9V ˅A4 L&H$ܹsvxwUlll@׳3hll T*looVAp_bdY,--AT"s_|rR NZܹox<|,[pjqj`gϞɓ'L&sϡ^f8kXhQ.ykkk[~~ Z ;;;݆B\.ǃjp1a``JPCCCq^pc2):,OZXX$-CCCH$>} :`||'NfC4E>^gPHMD"x< $ GI)ΝT*:ML B\Qa0PVq%S4~,ãG؉-H@ A $H A ?H{{{qG.~GOOa0krrإr뱰v;;;UTp Hp=m!ԽK@d2ɓ'vr "T*VݎP('Nph^gXEqx& SSSaD1N8zn`\.c}}Bj 5J\.R0$N$X,Ÿ{.:Μ9z O?4y$ލ7 o`zzTAfJZbkk{iS;]z@{|f1??^z N| @݆h``0h4fbncjj zR {{{ |U*T*l60l6|>w=VUf\xXe)TK}Y~_D0 h;y$(^/޽@·-v k4~MngΜ\.GX"T*|H#GѣG5N(`ooP*< 2 ^/2 wx``R4N'n߾gϢVȑ#0P( s> fhDP,yЄ łl6m[xh RP.96BN|ק\.X,3.$ C܁3dVTPTv9B ER?5jg'?MffA9 *#A"w W h5&-ܸm \ b{@09E64 p=w &hNN_Txm[,qRS'(SGǁ]+ |L6Lz-g@K&X\V H\.1/d ZvVZ$ O)t.r9XVS{P3ӉB1lFrV+"O1L>_ pn8~8Nٌnd2 Z ^2LYT*iLNNvcss+++ѣG144d2ɓE $avt:B ߏ ( \.|>\|/^ZF&AV:RP 1;;OH&PTZH$0L|_R(Y"i"9m6d2"&''jI $H A $HKb_~{ ˅t:`0ZͰctt"h۰Z'?y&PBb8{,;r9f3b>h4h6r)H$B&9ml6niLxo E0k4 M^ߟl 0j~rӸ[j1$XHJ\R3# L8&I[}%V*J]\ f44VTSK0\j]n;|q \.Q({x8bGz[LQB׻lrg.IwqV^:Q46O s t:U( i|b AuJ XKؕH$8Z\^c:H6 z$ ~?d2rR&&&jw};)j5n `eeBzNB@8FE6e\P`q \xFcccxrI^G\lRD>l68CzLX,D"Im$I,//cqqT*xܻwkkk0Lzp\ܫƟ-N:M ͂ $H A $Hv돠)L(*_h~Y"xV*8r9#"HZÈF؀RdB&hnG"ם b||l B޾}V v\XXXGۅD8Ɲ;wl6055bgg?cلjE&3<Vvo}[# vjdeh4jzxwQհjD"\~+++:00jk׮c믿1+Ws$u&`͛7ob`` jJÁ~ːJVw4 ޽l6 JZqBT `2l6fFq`aa5.ɠVa۱\.ٌ]qvsR'OFSZ*"bttZ b@laG@ڵk|%+H%N+m6F#:n76*;)[O]o'W.E2bNzR)ޙ@t RRdJ^w峙 IDATaLPCNǀv~"zLheL< Kldk5`>)+<>nN'rv9?LѴҶҺjJ] E.V%XM1]R.\3 ;?)mST1Ҹ_GO1M=. jr ݻv N|>}?UUb1Z-<$wR)HR! ɓvH$UL&y瞃db1|ѣGvŰ#G\.Cr$ǏCP JA,cuu7nZD"a@*{zwwN^bF]s=;Wp89rvX ޿LhZpc,q!mhng@ HHR8s l6`0!sԷF;w(y>|NΝb1 ;qwvvp!+|F۷h40;;ץ0h4 }~J%k\r:&IܹsD,z2?~>,:;H#p,dV*r'(^%|0|MjvSZC8 J4>WTٳd2_Vquzn=z{{{fp:8<߿A!Ancss;;;0 jHR(JD(X,فlFB4EE<GRxP(|>i:t ,tT*RWUX, "JgFUj^Ǫ AJ3g/b/X%A $H A Yb؏'9@E=|+_Gm _~j T ͆7ok_9P(|3x1 ?~1;=|ѣG,J :u J^{V\.cllD" X,bcc"6 X `jBRX,bppr_Wy(RWӡ^3y4vC,#Ƴ6"j5Z8Z\.chh;Z <̌F#" Ŏ_×/_S4h\%4 vf2J%|CBB0.QrZ-l6 FDQxqh4H&VL&es pn7 \. :NA pT*!0'js.='(v/Uӱ tߏbx<?NdY vR)8ND"Ball f8z(, "^/ZN'.^*)%籶Let:Ffr N<2ǃn Jt:z͆Az=qTUE%Hׯ_?A0Aٻx!(?nn~݌vrtaɍ7cd2x^lnnB&axx\& fX]]$;m6ܹ\O<NaZҤIop\ z@P2p  trjZel6Np[%P ͆5&whw7^,-v;j;׫*SD+94_,OqHTw&'W*x= l#j CF!oCZqFpt%1Gr;o%KLrҶh]7;^kr S/uФ7}OpܾfO.PuYr۴oi} Ҷ3"FXh:^#xҺs&u5+; jXuyvydP(`2t:Q8].ƍr0( 9p/%{\&]ш^V^WӜ 1::RD.I^dpȑ#>GA:* ;wSSS씦sޛ&?a_v0L(X\\g2 F(rpvlIVai78w~~'Z<>̖>5$H A $H G׮]|o+_ <>駟FV>9|_׾58^/[n//?뿆\.|կ~G+Ww~w?z h0jmzRdbp)HRh4њJf /@Tr䇞cccw`0y& dm624 4?Xx<ܾ}zaMр(Y7oބf)K777W^y. hX ( qz)uzV0L(d2v@2ԵHVufr9&''Fa0P(099t:q* B l4oL73go@Aբlrlk8Fh۷MfRaV*p8 bssfx f!077ٌl6wbff׵Z-WtM| A $H A 7X^͛7g?FOe9~~D?? .D"V+CGaqq|Z~{{{@okk ^ <VVVQ,1==\.H$‘"& xe :SƣG p!ܺu xV HjQ(1199zF|!Ja6{Vƍ8q?d.FGGWrRnǽ{`ٰqzXV$Ij5n4MDQz(Jev]F"`"#eO8t:͑)RwށF4 NP(#GB=z j^NѣG! ɰDe(n~@O=4JP177Rx?Aat:l6A<3|f0͸t:HL&'unr9` 0??Xh49RWٟ"Om"w$?S:p}6U ^A $H A 9_-r8qF8|0;2#:"N'r9BZ-&''vQq-d2D"hpmRA6zB;, N<䈥@|^r*c/]qbE=X[[A4 h4x!!ɰzA1@ @6jE(D"nJNl6ˀl6t2{.YzdB"lF:T*6b!=94 R)._^z D"GPCk1vܸqO*=z`0Çl6css#wwwi D{aii |b[[[0V0͸x"L&<?|jh4x7X,j5T*$ x<  loogT*0"L qDtVe]v O=CU˅!t:t:X,J%r4K.AP`rr#)\2tܳ(011(J P( p'Opy>|FX ޛu}嬜!9\%Q6Kr:i  zPEAzFvYeєDj2ξ0]9Bs8L&CVNiCp4qT "vww1??t:v~ٌcv.,,`4Ν;X^^FX^5 (p:t: 9 >Q79:&ǜyc^5 jwVnll@9I(l6"&cр^.ut/#Ib>!__m S󤋒du:a ~`\?rȣ>ay z;OFJh44Mx%:  $ۑ֛:Ci}"'5'3uiP(y8h9K:3 x&irh1ny x<}hJXϹIGw4 EK8hH"hЀ9ur4 d>P Psh4r,}l6F#$ P*F888k… p||9rސJvp\ zr`P,Qxz1>|K.AP cvv_|Νŋ9j:#c~~B;;;zl<< HUK`&{ ^zR`0@VqǮT*,jEr8d2l{.2 PTPՠT*a2矋^QJ/?1Y(QD%J(QDeAZz_K.qnXER9H$`XL&Qq$t9K;\˅Ct:|x'Jv;z^vq>#e yt:X,ߴlDh4H&h6SP(`6fVUrcnn<.R,pF0`0@*>ߏr ǃr )ZP|p@R[d2! rG+BIr l6X,|PV` IDAT}e4 31`X[[;5nHxp nX,PTsdT KKKhۨVx7#UsGvc<Kd2!xp͆\.FÁZJZ@ ?p:x ȩrI\.t]^CڮR+WzD"l opo߾E 899F`lǏETUFtX,h6<`6L&p8hh4qU;l0߇dXwVX/m;w%E=w0p_D"hDə^zΐ BJBNJ `HP9jZ]Znc5 fL+v{KnbK=)KD1  &7&Ev%qx{ҾS.o''Рe<ٟ; SK 3YIK1jdt19+'߃O R5mEO ЁZ>LSԳB`x#ܹCz$ LOOdjP(=D>._XXX@0DRVnA(QD%J(QD%^Qߵ;!rttivh4`@2ZF$\.G:F @E, wpLR«WD0`4ve2?|)^ DSfs@ >v; r~?CzNQo޼Wpϟ?g e4l6+W`4!Ç ;unn;1M&JEQpu. >K$0Lh4d2 +DxKKK p88:D 6?}@HBw:ud2HR|X[[.?o6A20onnb}}/T*!ˑJ؝MWAPEP9ŋX]]Ž{Vqy|Rqa 8ϟ?N'C ˅Bшh4CL@6_~ktr^zl6jx9\qJV1-aHNf9T*A1p~m4Mcmm <`0qB Rq|%z=Irz=\z^GGGlt:܇רL&Ǐap9|gĵk`Z1ymj5zb\T*nBrQh4B6E2D8Ƌ/077RB\.=߭Vh`FzSt0]+BR Zsw}ܷԭKP h pKsL.5C$0dzIJiJo˘$#pIN`p(Rd/9W't&H ?5%J' LS1O:7 ~Q-u:yd4AnQɱKI-ezO.:'\.3x%MB}8Qk>1 S+3x)iRɽ&NNIL0^&qD6}- |xd2#\{{{h4lj<@ԻaQ,Q.gZ"ѣGZt(4d0`0P(8*υaNv:3bfNdϞ=ùs/JFs(, Nz-..vT*X,pV+N'NNNSSS|o <7;;vIjtPnB-r\x* >|(^QJ"zE%J(QD%]"]@k d0T*VH$An#T W^ś7o`9QPr' bÁj Tʝ6̒DKKKx"677HN>zΝ;xwwollʕ+@kvXR> xoƍ7it:aٸv4l6hl"anno0Ptn/fFl6T*U|'{..^vS|>$ ( LMMA|'p: 2: 0,cůRh䭭-UTO?۷rkih4I8fᅬnGP@ZgpT*q}vj1Fx%Bpmt%}v. L&r1رX ^V 7;;;|]}vjZv:5MDQr9rx^4 hZ$IHRϣjk;zAL&\.ztj0}tFݎxH$32 \fT FDZ2~?G\D"T*-NNUh4~:={Ɲ9f!LWUd2DQz~mJ%4M㣏>L&C$p8DT*Rvvv p88pdłz=:8bBm6:2bn7A˗/|(hVl6`I7smvsZeYL}TܝNRԿ8š0W 6Q.u˒ 7u)z3B!F< LE_OBzrO:nJ%bz=9Ui{-)x-Ah[ءID0u^L9j52^lSTUN)E%S4񚄼)r? !LFwS-m}v |>$䈥n˱b:':֙LD"^rR  `  *.NOO4zd: `O:- $I<}j(gfgg H>xPVBt;;;l4V P(ARAj:;ok;\.&;$֠Vy6t8\~A >K NPc!n޼ѣ_}BRj5<n72 bgtX__G}U:0L|rV rqo`0~zah dtO<vgaqq2 xsNZJq.aNH\.`֓*"(CVnsu r9} h "`0@PhFARcjŎ|vz7LVPA40j O.C׃hD݆Fsܵxwj1l6r:AӧOa6d0??ݿ~pR/\p泆\tzu*f xSܹs(>SVA/D+J(QD%J(QAZz{=$ CRd2$IL&vtU*L&F#vX JVA&8?h7oބB[P(0055zݎv7Jq* ;sk(N1t:z=0Z-&Q|zzh U7 Ft:RcuugXXX_~ɝ 믿L&C"e] /;@1== \D"aT*C2a'7XTtp]x^L&v>&7766pxx=N( 9b h4b~~o nݺD"r~|tR),vvvp5DQz=avvd2h4 լP(P(vXV<{Z'(~ncggF/^͛7BFa8sXV]8Qt:aXd\d"uq !vwwhpy`zzA>`CdtH8<?@B6>.--1HX,Nŋ܍hP,Uy|~5o22vRPVJ  pppT}{㏡hzaXoV!ϟ>̧Or  ,QI^x0 x`0 Cpi P5J40"!EM* #X&W,sZ-Z-C(vf!uߒ#XT띉&PMM,h4g0T%d+9x N u 9KO_4q^YF)"' LO^4PPM~_>ִqCM}p#qR0RM@+}@@tLFxS0{}Nb'&cx7LQL=tot: Uj`2`T*йX,Bp9mJfK PrxZ.^=rv޽{t(P(p*X*P(x# qr쪥/^@P`0`ssf;=89z> !ɠX,:)If?sB'3?dT*qEv8oll`ccCD %J(QD%J_D+ƣG ,,,ŋ8yR`6 fAN'mjXp"h4\.DZ~T*uvyΝ[bvr8_& Pj]j|4l6ܽ{3338>>lF$Ƶ5r9! rQ*zplZg}eA|WZċFx<(zd2 |lAW_}NCBJkE}{]c?JB<N'8;e2V+l6j5יLN>x <F=^8B@рd (JBH0 0LT* !\.Z>Vewu:hhd0~rr ZD"C=V frlaj4up>88Xpf&;a*=ӵN ^3$H3u`@)Jh4xxr2A?LPΙX^}^#88@@wȵh]&d%&i2@139~'#i'$]r&7 jthZ Q?6>L!S1APZ;'rj5J wR5u#0KJ`vc@t drkSrm ]Er\~}k_Rɀ^K^z>Z?orT*]:SSSs.A`X߿r@$ 9\ĩTPX^^FXDXD0D4*:2̙f ɄsA.^sH:FRh4ba(L@41Q.>t:͝;;;P*E2NSLv(xi8<8(_lV/~gW(QD%J(QDeAZzt:5@jL& *fgg^Z^Ág9vJ#x<c3|DVKKKH$H$T*5LHRkZQ+!^z@OЎ)9! |>d2v\.#rY, j5VVVVA1͵t:l6B!b1F#ebss?O8uvvF݃,L&r4 L&$rVj5~J Gg2 R ˱χVJݧ 6J"\. *v\ K&?Db?c`<1݃T*ŵkאGկ~Álnnh4rW+rx<~C IDATFqߥNb@ N"d2?D"ϱ~_|wb~~''' 2WVVpzzz7n@*rlL&c0KFnt:>KKK(J(p0L899atppAjxP(LKKKh4| |zz@CѠjq/Zx</jPT씾pG B!$ rdr*B:5&1V+*N'F#Rr 2jAHfh68::b0H5rR^\g[j ;z=d2G  ~ `Ӱ:c'-FV{cK4 + `0`K%jvai4! q Ν;C܄Z椆^vx<N7oի|0|Z||T*z @zpvtv^bR6hy w^/OOOG"t]yFHR\.|0pݨT*ۃjL&cPrFŰ^C83i* Xh)v;nGeXp8PTjqrrud2ezj0pm1zz pHѲzz7n`HMN[n!ckk BQVj4vx[4<z<ۃJ~;w`uuvz'''0f '|'fW%unF#LOOC&!affBF1yyv¶Z31fRw:v;>ܹp81 dqD49 i4jb{{nbN?h4¹sJNk"r9/Ũ`w$v0|jXXX@EVg:W&wf2v Zh4 шRz.`nf6R4A=L=ͫVQ0d24;RnyPp8dG.Fj`HL`0uCܦ&ݡm4}:jW:SO,}z+9B Vr3uҚX&nb$5}ٴN]:trz4@Oj^CEKzz4t `@Ze?Y&7+Mo&Fcv8vN1<S mj54 v0+ nh4 t]@p8Q(g}K$|Z|^wꌧN|>zPTd2X[[T#ppmw@.CT`4r8[,$I\.BX J%$0FyT*8N C#L" ZV<>sVA/D+J(QD%J(QAZzo޼V͆St:B!Ej5xx<^k`Z;tkk ljs\fXYYLpF Z͑*_~ ׋ŋX,OqU|WX*f#b!=ŷ2M$ Ãt:hH$T*njAӡP(20؀`W_}^Cl6cssnX X,HD"jfr}K.mg?.Jl6v }L.r J۷os3=H'gjeNqpp+W  qnp8lF sq/nq-eB!,--a{{RGX"6NNN Jq-R)\t FΝc"H$Žׯ_bP(`00dx"AZCCJϯ^jEP@ŋy;/_" 2 jh| (H&P(D"h4ꫯja4v,krwiJ*PTg ۙh\FT26޼yni08Ζ!9pz=WrN&5A9J] F RR+ABrN6 _Xꝥ>gT&PK➩" Kxt5 LNbrN7겥א['~>BǕb c~^C`9K`֕hr dsFHhi6wҺL'T^<@t8zyE!]t<']NkB= Xf\bpFn:ǝӰAՂBL&`xyyV?wij޼yٌ^CQt6L}'''|ߖ|N^'''<@ r"j5_gϹs R)PV92v8"vpwkܼyr4 4Mx^vCnh4BCP`eeV |D"쎤hg\P(~x<ή!AP,z96޼yk׮A=Bh~?b>#LOOcggSSS<ɠlbffz5K.]Rit:EJ%x< t:( lmm!Hh4F#T*%ٌhD"ɄRJJχm}nD;;; \IFHRtx%h4w}N{i`\gԽ<==gϞtT*!caajz=X,md2XVpo}{ ?>\ٌFF9vz=Z-\v .]B2~!͢j!Cբ\.C"`ffa =4loo#p4< ,r9FJ%PT*ppp3pr@wqqܕLFT D|* -C#Hp9E>N3d:99[o`w2,|Z_40z7C2 ,hzt,J?:КL&8vұ^a^nd2' )*}tF&'9A:_2'  ˑ~zu d`D:  0RG,AaNv͎eyO%HGC#B%WpZeH@ $FgZb82p7 i߸z=_rFuV <'2} ߃␩b)Zף(\&4 :5ϿRNpt hHʓ= Q\Rt8<'OUH&qR`0xȍ v:\.(J|ӘG\, 2 P*L&!?zr6 J{=jVWW8ZӧP(l]x *60?BC\.#NcggW^E4(J~ Swss+++( |2!j:ZA8U`155u%x looCV^SeynnF^3 D+Wb 3Lt{GJn)oB~V:c~J%vvvp8rjQ(!J 8&{ t|)ʕ+hZh4xc;z=?OKu:ܫIJ|:cR)0w9믿ٳghxvx)k,c ٌd2pɄF$===^` ݎ/_ƍvqm6x>}C<D">^/\.J2L&Rt;/$~s4ũFA4 <~@B]䔴F0\,H$xP(uE>h |RO>* Skҡ8 0 ΞjRpmZEFRaH}pzz m(i꾝tR<9YU*~fC`/v:'GKCzHk֗ON[P(8Λ֟LCII7,g`8H] )~ܯ4@ԟKߧNZ:Vztu:ޟV.M:o(zo5 . pC=tѹGCRoV#ZRzlFNJ:㎥s)&fxe :Z0nd w]l6Ev3wp0mFD"J%v6ÁN]rJPN޽ Áj`016 ^Q^;nrP(p?Er9އt:`zzd`Z$hD2mT*d2':u/S<9 ӣGT*{ovC4 v^QV"zE%J(QD%]"]@~3t]vLRO`@\FA @E6`0@XdHQZ!nw.--qԳhd 9fv;2 wL&}.wsZ,ܽ{/_o[R)G?fCfggñBfv;A6L&Zk&|><:jܹsx &\F.NrQnr9vV*-x/_b4aqqJaF_LCl6fLMMqD"ATB>T*ãG077i^kͮbȱ֭V L=L"*`40{{HfΝ;PhZ Q ro۳zo~y2 1 NNN8 fE\QRZB.۩nst~?jCnvi\.vFQ\rl1;;n^͆V bnno/biԵ??%ǩ`lF*B&͛71 t{={R ӉB=tn6wz-vrhR.39URqd#^n͛.p9-JZ8>>hXTR@ C 3"W_jbgg. fafA/4+rҽ;ɉ7e].+NN%IXrjvR4Wr~$rq^`{h=WRqrm#~%I@Z N%W-605 %O+9fiJQޓrJntܦZneKL&N+DzKp3G =PV'GRRDjP  DZ^gL}֓ b k4t:vrh4׌F#\.vs:fxjBNF#r9k͆\.iu4PDNׯ_sT p||cvwwnPP*jr7omF6˅#~Mnυr9᭷B\fWp^G&\.n@ +H0==T*AP(Fq-%g`@.C:;j5~?AnR˅JrU ?lw>|ϳ۸|2>ABj2 L{^vz"ʕ+{ V+|>\cx<vK}4MD"f  l6X,Qr9N\z jq X$h4{<<18A"ER%ŖK]vR"YdTVYere,Rd*gǔ#ɢDE` $@ 4=sB<ޯJϋ{X%@v=#677Q(+\.V! A,#N`p8|>vA7q IDAT P*N9z=N'[ZT*FN:t:X__^Y?z=|> Z'I>&H$x뭷f qq׋bL&~tѣG x(ܹBL9Lbqq;:͎Z˗/\.l6C.cyyZ& oF<;#L&`rVVEfX,0p:H$xr̮R>p\( p\H$D" VP,Rqttbj6Պ^k t:CGr6 'JXA`4pmM g;vww1:: F\jʉ&tlןRD.W6R.(+JDQ4M p4 D1y&w!FFFP,fq= Zv W A $H A%^Aߴ^|,Sc.h\s:C>?VP(j J=4 $ x<vPaDqJo:666@OV l>?OF?JRl6sgl6N'v;wzE|>8Nv{^<~8~)rJ\.!^ʱ& | J%rst:X,4M elll0|hh48</i?i[H]jH$ Ov$cI),=NvR|0E!A Q5E3f;un)⚆h8^iJ$j5h4zv@1t:h6t:vMmx_[, p1 |.cyMz?_Nj08Ʉd2!!([!l69a}})ҝ9hhthX,rWs{j" ˅l6l6 vN|###HӐd|Zvp\D"Jxp8xp)yfG<GTÇ+kI@ A $H AKidx"_\1r2@\YYA$a7 N' vvvhEIrFz^~Nngg& |rhd#?x+ "DZk׮A.##`0 re(FFFD"!0:: ^0 cll Ϟ=Y?zo=j\pj>D(b#d2ܕNqyR)|>t:t. vbPHRF2M&8<>F 71p\ܓl6q-^__ ^xcloos|j 2vvv055t:5|! "(Sw#9Tu:( _'|ӉZ/[zgggl6Q,Ac J06j  ^ϐlrrO<` 8F@h4"!cuudc dCFT³gP*tp||.b#HR4 fX'#J% "P}RAeGBbzH$FJ^3NYNVuj5^Ô(jE|[.K=Q0)`$3,r__ѕJhۧ]侥  {Vd/9s) \:vA' hZ Lj4S~ifici@0lD')CZRM$|'< `.c/ Lx t:_`Z3WTR79i{d2Vk@рFJkbe2v;b1=zN~^b1wkZ0LPըT*]ٍF"zc"f3_& ^V R) LrZ PD"A.ógpU( J%z=;wZ l^l , 'fX,loopv;Rl2  Q*l6111 Ej5D"RdD" r5 !:D"j Ӊv"z}- @ A $H AKip~rAP@&R C{r{PӧONa6!Hpv g?j534 (Jp:ժP(ɑЅBU"vIRL&z C5 ܓG9>>QL&i(Jx<u,..b||T*(̀b ɠR  h4"aggckÕEA݃NaǃJ`R`rrF & ftT*<~׮]C\D"";ZR)VVVT*!P.9 X =FV J|gΜ"珏 ŽFnr7n~r/R)b:Փ 䪦Hur aYԻ'ॗ^h`rvs$sF*hZ@ R7obzz㱫*=뱿vTR9Ec\,CP0t:zH$x7X<hxiv Ei$JpɩK +ɠh Nk< 6 ~MCѰӓ$9z $t̪j~OؕL8c{K1JI(_Zkt^u(-RLǗ`8m#3Yr8ܦnV'{ pcANpܯ4tR3ALjm}rWќ?5}҅Mޓb G!#>#e2o+EJSW`ϋT*V@:hRbP(&gr @RAZp8!, $FGGiOik>;~jfh4 YC״VٳgzP՘v8X__+kI@ A $H AKi?7 V+1??Ϯ[0|HRvdB@>GՂhĿ7߄L&c7dB0ē'O JaZV\αVdٳga2 шjʮ1˿ V+סP099w2 hnv@ ]v;R)FjJ%Ez=t]_>aK=jHrALQ* t@HV @( D t:n«c\|rltW\`@^Zƭ[0==jjy|p8@F&A0D<D"gϰX3B׃`@ZN[xd099}Z-ae|LjFZx IZ b/2尰ǃt?]Fnlll;133yex1fggkz8fAӡAR!ѣGz*<?~H$ۍ\.F7n܀Rd0Rn1::i h68{,:n7{c~~~+f9sJp!n݆T*`` P(ǡR`0p=A*L&c jP0I.$kBBӉB6#x,&.rZ* tܓJnyrP܍{ T8=>>FJHh( 8T:OKALhrC-5^:tt`)[rԥS(T*%MKN`爜{'$ȍLNJ]ɭjمMn5s ]NǞ\f2rw@a0#u:ݩdn j066v*4C)6 fxt2;|t"h4:-\a6a6QVRd`0x #L… x!FFF=Hn!0O^G\ZF\B@X^sJD6E"dF\.# Cj! nѣGFAP`i4a966z`VA?``'}`6EdY|;ABXNOZ ӉϟsJr9D"ahZVtv&''L&&mTU9sE|R7VtDRp8/Kvvqi={?Opxx mZFs\. 4 GA2X__GGT™3g@fiuzdŹ9y( fjh`3 IDATT*w T( ;;UR1vl@PcF`-{QVEMN`!m_A?umKpo.J­V VW>t˔ .m  OvӜޏ\OF)S`J-;ٳK`L&!o'_HOyUq:VP*x\GGGT*Lp:- 9M^,^C"p:]wT@Ljf3"N'@Z8,F#2PTd2}]\|BZE#/ngpN:t z=|'^@ A $H A Iiyn# ٳgJ(JX,Dx!;?r$f#"rP(h4h4ʐ㷿-n7J^t N[ ՊybEXăo} FAZE$dnG*nGBPF6H磣#>{, kO<;n6?t.?Z rp|lv裏_~%FFF1ZEjz<{f;O&!qt:!ɰ P7c0fˌܹ DQnKT*L&q||Nt:~˅_~YLFDx뭷tzqML&x^JB,C(677W^!rL&0*9orx) jGt:Jq^hSV.P"nZ-K$J%t:|7NXrW*vmZ- S^oz 'G.s:&&5zZ>oǦWr1rR*s "$4 Jx|ϥd:4x@TUrvEӹjٝLk(Z&(ns= Gf&9O +JC]\[ZNrR.vrRRD0{`0t><~ӟR^RP(vkkkH$0 Dx$, , F#ǝRls^n. l6"C<r-M,CR2ٳgH$ H0775jX,Bᥗ^B>ZV&033JFq C8Nupvz~JΝCҙjG>GTB4fhh4clll coo&kZjaii ϟ2MB!L&r9ߥOF @2#a}>1 p9j5R)T*~d24 X,|gg躳iԮjQχ~P(T*%~4Mb1(J~j5FUJb0LP*Nv8<.2z=gYaWh`ts,Jh4;Mvu:C?r{ r9&R1[P(nM'!]&Gp3]rShSLd.95g7A铝ǝNA3J (JRI,KGNir33 |bR&y2RY$lBT2%J_Ȧ"th؅KM뀎=9"鳙 nӱ%`L]*jAR)~dT*k+/&N}wy냎\.G*}T*ߏpȠ l攎^p8hJ.vBR.`H$`aٰp8`993221R;k;t:h4DP*P8pV 尸F@CjTJB &6fggP(ҋD"|>z=dYfH$e=V!p`mm w@%B $H A . EիE٩H.|>1LLLh4ŋzXXXLb6;ȉ2??T*vX,~d2 JQR)~hJwE׃hdgX믿μrpAbcc@|>RZ hZl N'D"ft: P.qt:<{{p d2V+Z-;=5 D ?FFF٥P(Pܻw B쐺p둳.A&d2q!QQT0;;i#JH$H0( 0LxWPTFh4l6!JqppBŎJeLOOٳgd:jB*bee;[ \.(vwwQ9fnt: rރfϡP(166pL& dYEH$#k\.իۃV`0_~)Fv$~瘙??b@󻿿ϽW-Pccc<L&Vdt`6|@^G*`0`B} l6yv} Ƭj ;ɵH$8btyya`W8_. NP(@.# \.@x]JT}ʵZ jfc AHJkf1STh8>>ngw; ZPN;_K'W5A>rz=h[H.eR D&9)\~땶 Y e~tYeO;S:A\t ȩt3) V= tXS/-$jt~ vދ =<פmTʰ"u: zn St1U9`^^[r* v-_.uȥx =A^;uC 4D`X_pd}H&0ٳgxحVX,?l. 2 drHJ|Ν; .~_crrBVwԍbT* x&N'r9n7"Z-?IM}VN~!<^~e CR)HR|>ja2p~z=0ʾxt:F`2p|| %LLLr9W|G$D7C $H A . E?.? ^LvЗFN:>}!qppd2 Ǒw-v$iAzbj5O VWWɝNq||vn{FW\A2nGjr;hۈFX[[f3VVV`8T*ass9v;666o:+0LbFB#!Ncqq?K9 H BRMn7oBRbA6EmB Ӊ}yvRǶN!Qcvo) x^ܻwߛ@3U*_Kl%wI@F1665iT*At:evpooo#jIXd^V!022BF'cv)*y0\.sϐhuu^n7[rR 5E8T*$.J.[ť.f]ra6 XfKIx5MjzzWR˖D"9@S* ɠT*_"IK.V]%J*􇜲@ 7(%0=Dt\)Md2u7 v:&J0:P> !8uv]NJ 4,f\΀icJJO9IMNCt  JKAw:zta S!~?D"8( \.?m<@Z f:lNLϟ?wg###d2 X3(Jb1b0P(`0påK`2N׋ׯ6FGGP(t:q||_~d[[[0 0LF {||?2;ZP*`4P(055j|H$|2&?̥& zZlfHP*x< Ϊ*?gΜ:.\͆]ܾ}o&P(Fqxx^\?L&q%ܸq!rV+2 vrPVQ(0::fRݎ?@DjWiXۋ:z=vwwH$ ܹsHhۨD"똜D߇v;GDdxl6*(Z-jV+VVVgt}q9Fg?C(B `:/^@h4 `}}Jhjn›ow}R; zj|H&}m\v Z-FaXP*h`6[Y&Z*bii . kkk|VꌮT*vl|~v;F#HLD Z !QT8܃^T 6 f"ZV-Dp8F[G]6_\^ϑ¤R)po*9>k\ Nl&pM[.Yr&&`)I_/k.Z=|ґL@]!o+S1Z*i)I@*JQסT*h4x\;-LdpN]ղӒzO78%RdW1c:P?0t5 .wP :K=9b1Zk$'y:v:jR&RZ>>>F.CP{uy1LК3'? '3,..r%C*eӉW]A#\.#B& ݎǏs-^'OpO_|HFTi@ׯcnnoٳg>J%hwjVrCfAP  L"@P h)Zϟ3@+.ti8ooo&`kk WR4V bϟ͛˿KD$яc;@g $H A $H Anf)S Bpi"@0D"2F#?$siܽ{B[[[D0 xW1 ˅d2PݻHfO>ŝ;wa0P(|̻ IDAT*022`0`uuLT^T*qa R>}]=<|\.`0BfCRq߮Vef3^{5HXYYA׃jE\Cn5܉ݥ^x׮]*  R) CөncbbrrP!x"fggQՐf% V+/^ǏS| D"Zx), ~?b1L&0>>9B,… t:x7PTptt~8֭[ܛKP(`0`oo@G<j2cyy8ǒyb1>NgϞOOoT*qξ ə6$ED$R i$n uO"EESh@[$MSI1Ԏmɒ-k$[(:䐜}wμo޼yyk!;=R)t:8NTU H 1hD"p:xgOԧ>Bd2ɐD"6Lvr9J%02 L&f3 S(5x ߇N,h4V\|>P,n\0n2&;4,g__;ɉ.2#1ANZ~rYP(@sFf̤EPRA׳c`9mi}w```X,gG&kj5R)Vrr~>%K t:AOr3\; a#m\.3 hiZS:&XIItH$0~M.im[.V_1g~=mFH$켤c!tfA4 W*00u J:"b~w|6MiXV|>accVh4h4`0h4L&l6ykgϞݻwaX8=~ lll@Vg~P(nl6!cxx^݅nJB86&''h4t:T*cwwǏTP(7u1<L&$IX,x^>OW^:>OOkᥗ^{ ;v W^X,??fm=(JR~*K $H A $HOp6n߾ \N ,z=jD"v9lp8p:HӸp?^YYnA8qf`իW#ӟ4|>0.yZ-u΢VAӱ3wccz8R*.z\.ysX,2* dz2z뭷P,qAn,//#A&! ޽{n>z=z=4q^z%t:(JܿZ L߇`@?2 099 ^x<Ӊ-LNNĉkpapnG ^uhZT*e ݻd8qr9`0  B.'hRAܻw?\\.zRbsssX,D"H$ $;C=Jv;~?d24GjS_*{=ߏzΉ <`4OsJ™3gP(0;;ːF<#GKKKx7۷r0:: ~ΝCP׿up$߹s`2 \z:Dn]`nfffnx`6dOBRT*\.C.# beezRx<Η.]RBe|򓟄``ht:a4k2l6)alnntCxrCwX~nӧqE3_YYA:<677ٱ,`Z#P(Zիj} @hdvf7b+ , k###rVHRMӰX,!gp%o:Jf3ܹ^Nh4 "J… _5DQr9nFh4d2P(e iXGBGa4 \.vQj(fEJ%x<n%cZRdrp\  0??n:|<( NT*1ZKǃNl6 Ā) r/熢q\b(Jl@hZT*kWt@znrHPIVrR+4'G*uFFP\1vH$w \bBROh4 N Q0E "+Zer`MXLzj/MkG}%`K_Kk@tjP(vR'/u?:tXj5vJq%ALj#_rᒛ`.9h)2?&hL5i1O֕X,9iТl+,HxE`Blxi\j!B*RlB.?ԋT*!Jl0L&9`0pG׃dNl6^/,,b1 ?~Vt*;l6X,P(bwKVpVRr̉N& j5666P(8R@b7n{VEDXNyV <bXt:蓃["p" VE*Drg2Dl_җ77d2Bx'Z1t7o,^|E|_kOOꫯbll `l[?k,H A $H A _. fffpH$,,,fn޽{0FLݎ%d28NZC۵5qHRlmmرcH$szy1rh6~:vwwJ|p… 8}4Zx vdYs {gVݎIAv^ #Jajj ^/$ ]QR)L&d46x AT*,,,1,T*zt:݅je(=::`0q:t]9뿎L&D'O"H@a}}* sssDX\\۷ h4`ꫯbtt=T*\.;2m6T*&''4PZ.\˅}\~av$CL&RX,'xXYYARhd2}t( ܄QV9rX,"h4"L"cnnO=޽Yx<ܸq  p8`٠P(%LNN0>>'O>v?)b?M̠hbǎC@<RDXDV'?Ie|NbDqA,..… @<bD"<>!`bb|d#?U |Y]]Eۅoo޽{ gvwwFZ5ܽ{ƽWN{^rA"GAF>ɓ'g0l6R)?ԇi28֭[X__G6E0d'B"Z-FFFP,P(W{hh-j5vvv8jbttd2D@\ DZm\.:U(A. ;6r9R&Mn9rd2q_kVL&h|0ZHTJ CR/W yXt: ]r9GE,fdV Q}&`̮L&chtx&.BOBj}}}QK2}6* 2GLSnW&XNۦYr?'MNV" ܒ+@));9K&nT*مKNTf6e7D"NF)tN}*E=C-:אm62wr9 vb>zFtVUacKB^kZvr +6JvcppD˫٭F+l<ߏ|>-v8{܄X,F0ӧl7 ܽ{SSSt:;677Q!zIԏ=00}ާN۷1==zjbׯܯt:nDl|?kEoooҥKVE4Hmao6(Jh4< C_pe:u _o3330LXZZK$<ǑfL ðlG ^A $H A 9zR)q%j5Eu hplcK"0;;}ZPZ^PVN|B^"jf3d2?7a4T*ߏI|;f34]gyb0 888p8~hdu 4 FGGAZ8J͆'OrǑJfh40<<̱V KKKBV4Z5 HP(tqq2 Gl~r^{ & wE"#G>@ %ĉtHR kJ峷Պ\.rmE" "f3j5 Z,8:{d2h6[YY'onnbzzPTA<GZz(J{uLOOcnnW^Żヒ'xgϞzl6ٍtx/zF)O5$ Lb1:0ĥirS{} W(EZS5vDӾc_~7 h4`.'R) eL&X,|^Fp{,( |?'PMW*<#j0Z-~`bbCCC<8̃cz&;T LOOܹsjh6|x<@рV 'bu&D"]YYFnG<˗177䎞BVD"CTBT!R z.8zIt~wNK/Ʉ~8}4T*og144U @?q1BR"_*/rۢ*Uv W A $H A~E$^AhWNٌ{nCVG?$ FGGl6aa4Fu[o3 فg,666T h6|l6#pTG>:z)v! >T*fC>^666066n۷ocii /~vR/jD(pYdyN IDAT2ϳȑ#8w>O H0dxW055D x<r9J% v>/|>Zrl6:t:}ĥRyF:F׃NCXEPヒi={ݟ@n݂fzUbQבH$I& tr/2(JjX,8|>~O\*L& aِNQ9b`zzKKKP՘E^kիWRߺZ"7wTu!q(WUTUFzXXX>1z Qlfqqݥ/r,*Ed2x<(Ja l6j5r9 hZtUT0ͨV|2g:N|>o 9rPR)9i0hp0ES-AvrQT/%*N_X Vj \DQPTD"Wz-* N:Fcw &4v ɕL>+vcKGQf%G+ ѐ(\ԗ\xU Ad&NkQ`D4E<h.V59L`td#TͧfۣMf0xt:~_bt\h[Va=YRq(pT kV.aZ!a!JVh4`XpmfFh4fz( @SGL&|>D"Fvb?@C s1LC4+"_2moost\.ǩS|7G pQ|şٟall /$ v;t:/^׾5|v[?=^{kW~z?~Ӎ7O\v@ A $H AuY=zqUA:D"zGt:l6xb w]|>d2>pV+ǭX[\Ήn^ԉ byyb^6 7n@Xr<ԐJ` dALMM!J!Lb@sf{b?Lpp:L&Q*8nfT*AP`pp~@ץT* b1,//v|DmO}gϞEZlbaaFcD2H$fCX͛7/|*|> 9/}K D"o /NBz$H A $H_ ^Ah0Fde(JR  rtv1jzf"uF"t:u8èjކ(aZ"`||PJZF$۷ٌiB!R);mtt>~?"ǡT*RH$`X qXVx^Aܹs;ܹߏRܹs. Sa۱DQvjr9 d2!@V3%322]?^|V [[[(hRD2Z׋zC Z-479 `0>@^b@2"?Á\.Vpɓx122rBN͆n F$I1bI[.Q,aXx֐\/5 t::nu5'W3Ktt:FT*rtܥ XRlNB@@$0v;4088j5v6zL&s+J ÁbsaffZ Z fs7>A .!flr/2wrf.]*'FA.C*B>gt*|> r1==>R)t]U(Ðd|e"L"ŋWϤ7$H A $H_ W/Zz/BP`aa-cR,d2Jgf#H`uu`zL/^ERXн=\.( lmmA.s/E]B&"## v#lbggjΝ!aXPTp%nt:b1q8D";nܸ.!r?U*[\^^VšC ?Oqa\p:^"|&&&Gtss N'|>FsϡT*d2j""arrzr@F,cOTRD#qm6Rqvꛛ;ڵk(BFӉd28r"8Z-Z==v8 \.qyjhxw044LLL8FbV+f3GrZ-d2 ˡP(rA,cmm ccc o}LLLܹs|>v* ,..p(i{\assfd"%R);iZTh48Vb}}ZnJLQT000ۍh4n MD"jd2r9:u >̙3fpfnC+WncvvO~E$ aJ%XVQP2 D~@Z<044rJ B$3x&Nfp8`Zd6 {x@ }uC,VauuubWUev[,V… XYYtT*h`rr]M&vww188>Vvېp~?j5ݻh0::-vR4~]z8N$aggJ08x kZ ǃFyT*n}i6l pa H&P(JoZxp"v;N'"jqE bll X 2 W^egH$}AV3d~wp8[[[FVJo}[lfgXDa&VVVpv( P(`cc>$Jvt:P|0h4X\\(wiԥJ̠?Sd<9/i`GRahOi{b1Z\.<'Wz Ʌ-9f[*XVCрBNCZ}!:jp8jرmXRP/`Z¯Vl6J86lBP0T*j8vr4N' '8NY%Jarrp:GL&C4NuD"X,_ocbbprO>$4 FA`qq*# 0CV:6t:^|ErLNN޽{( 8~8000O}S؀X,F?NMM!͢hT*r1|lpHRfL&0^/^/ 133ɓX]]<h4H$xNyf3:.]zJKKKVp:%n1;;0L&T*,//#L" wa7g.Bo6 h48r2 * N'wfd2X,qlnnkr ҥKx뭷pQjq<\.G*B `H`0tPVY͆|> d2!C6EP`/{V {{{`4nܸchhvFlX\\O~vu # a}}Ŀۿl6L&fggQ,ׇcǎ!LB&~?^/H|>d2K`w)$ e߿9h4hQ̠lZd2q:8}Qd2b1vv,--AP:Az2\.30?|0:t:{ JfsssxwPT`4!JQ*066_cc&rvww9Njaee<4FFFP(JpM Kʕ+õk׸9ϣX,BVcwwCЌ"6(noosrC\fo(H^v#G/ѻR&hrR>trS-JC`ޓB`JQ?+CbjN`~ɕJ%Kz=D"* ;imh(l!:E6h[Z9%=hZZ RD4}=Ab/sAnώV0[4@k%Q,y*zMjypVqV塒|>G:G(遆aCVd2=4@dD\J\.UC IDATZ ,..BTBpsلB41::ʯJ`Zv>bbbf]8Z-v;4 :V+J%"q=3H$p:Hkٌ͛YT*aX`Z,Ӊp8}jXZYYsa&z^h0??} rt:={x055p8/7oD(< :,jR)w,+d2s•+WpU $^W A $H A~%^Ah?va8\.#cjj D>lfggaZ/cppo# , G9͛7122NIvAx<lmmA$arr2 W^ /=t\FP( F \.t]Kp``:ABP@\^Ǖ+Wz!!jXG\F4ᅬ3g bdd}}}ַJnd22|d~nYVq6~dYmѷN0 x"{1D"d2R)^a0puܺu ( xGꫯbhhF(޽˝@eoݺE9/T*SOAP`eeV w܁H$n]ǃX^^F L&b(n޼dAݻwQ( !pu|>v2b~?D"*ltf]A\~###xꩧL&]~fNYvIC$_;եV 0>>fMT*v)yLMMڵk"GT*P*z;93\R"%Ӳ,ˊ(I,8 U_@ ql+*JQl,]r:{wAcl6FFF`0U[o7߄b."T*r<2 lwww155[n!cdd{|> rJ%ǒCh4r4ŗR) vp:ܵT*Q./\vj\H$z@pj4L&4 4 ( WJ %PVmP(gILNZ}&G?'(E"L=Sw/}VRdy)J1nd݆B~eB%xQV|%3䣎e:nԧK]Jv#7ALrjOz-E;~lB׳h<6u+NFfNXץ&+ G@ÿA,P(099]@p8|>|f#b1CJ ^/" AףZBVT*q/ARFχFL& VvCt"Z._ ݎhZ;, 84Z-V+b1 2 "(9= Bŋy&x P*NK.r?믿Ӊ-Fk׮azz;:>xpjjqbNa{pBJÃ"zAO~???{ .W A $H A$^Alg,,, {bl6VR@, HۘD x:Q`>b1, ݻǑfA^G2A:Bz L&?CJ%f(  :u {x v:޿_z ۳gbkk#u:"6 pq~8N˅g<}>wRo߆FA*g}nP1sFGG9zrr{{{X,NaZ+?؟j%)q7@.åK022/ٳzzfO]v>DTz{94MR)vbr9}8Nq CDQLOOcoofcccx0.D"U*/Cboo@^ i{Μ9=nF+++JD"v_jH$X\\DF*$F#.Jjxz=L&HRdYXVv4U( D"hOٌqh4\.Jh4 D'Op@$;QV+t:zq)r9|( >;(t*J%'OnV!N#  xP(Pס#vR3kF=P(x$ , _O:rډD"dYN0?Ml@@لFAaV Fn_'k(^0nsluz P'.?q$ Z-73UL.MT R\8h0L>|^27*i%WB8L.pkVc}t(`3|2&KC 4\Ei @'g66 >/4ChaZͰpQBmjKKCD:Vp\VĠ[VNq8FلfFT* fܹs[G, fggt:qO p-r9K/JۃJGXu:#H@V3ԥԏ^ΑǏ8|?''.y):s@WRZhp!ЀD^Q(pxxߙLZ Rcifa0pܸ`@^,^A$H A $H Ak W[z_u$ H$RX#{=R077rB9 Ci|>HRB&1%A:=b\.ÇQ.l6199- 9s#8v;* 1].L_v v^_~%>|Q6 2 b>FRaL&96VtrondZj7Cz8b1t:?r..\͆@ wp@&CvχO>?GFFp)|KKK&Μ9NF/]^z ~# >B\p|'h4GTD"dbב^gdHpJ@ 3ݎJL&M[@XV'}/B* n݂NC:,D"bN>0o߾%m\v ccc 0GHb8N\~b6 N?ƥK/C*޽{X,{.B9:nh4"Ja82`r P+JܑZVkRTbnn~ta7jq(FB`_Uh4ONǸnD@ï@1?% ;l˖bY rr9>AJk%7 (,JP* {+)\F!ӟ2c)ָZ2n%w!AaG9|ԵL[Kiiy;h|6m/9ɅL_ϩ0ԦsJk:G,y[賎FGbs默5M} 69iH h& Z-ndQK\.6;U z=zA4T.Z #C`ggObwmV,){jp@. 9ciP,@ӡRpI,Z͛7 ׍7pI6T*Clll`0j kk `Xx JR`||& ;;;0 <666yNa2'`zzsssH$z*Μ9/̙3hꫯrm^d7|hpL&bF#b R~?Z-nܸI$ h4Z;;;>Bvww/[svvbhPfZ/c||* O<2 G@$@ 0 ׋Vx{h4 NannB9D"nݺ^Nv͠#`||.]h41VMZH$ݎ?+шbpz:F(B:JbǏ2"coo+++H&H$Çva6hP,z=.666rcNl6 -4DRɮ7. RZ{[)ޚbəN";G:!1PMU\ tD"~OWHR /KbX,/Dj -FZSQ',9i)E-t:&)JK)9)ƙ'>b5hu'g/ dЧ?AK)ƛ ES?Jw'L>~I[-P(|(chK]Lt̩oIj`mMneDT* FTApVL&X ~B!O]NRS?8nt]>ݳDZ.jzQVQTD`ZÀ")ƙl9KJRoL&^8siRe{R& 0 e2ä^1%G.]:.֤]z/=`rh4~J%>%L@r{:Cѣ~#v]\)\\[?Nz9|i@BVKR,@\>4 F}JQڴt +tӺGǜ>z{ qĈfםNj^#ɽ<qpp̎bL:<&J 9[,ާHt ^m߻w ld^X[[ ܸq/PTJlqmCP֭[p8 i,ps<vS[ `4!;˭V~?>CB!FNx!~?o~L&aHR{ǃVR-ܸqC $H A $H?u W[z_x>@VoV1h6X]]d<b?;nnnC?9.\fm\pJ-XVG=vf3b1|>?:j5ǧD" ܃Gn=~N;>cvgϞ˗ T\|X__@ f  BR<77X,W_};;;X,IIqp9ve* \zӈp8uEx<zcvv2 >4 N85@8F.L&Ë/TǏ#KHr VˎZ׋rFbSNx7111L&;v 0Lp:d2XXX666`0H$P,92 " H`ll +++( PT؀JnGAB6ŕ+WPV /޽{H$8y$uc}$ IDAT qquvOQ|J,P1== B;wl6f10Lކl 8Fv>2jH$˿&*&&&0fqF#aZqYDQzLMM!X]]r$?W* f3._CZ?lF8D"ATq vgYI~:&&&0 vp8p |># ˱>loo3rg}FolB~`]>}]dw"677F111`0ӛ<* wpV*C-JVlnnBRhHC= L&Xgcg*WeoZ ~u:Z;ipHJ@NHVc&Avrm6P*J$>Gc>JT*SF O]& i;JNPwT%J@"% V $ȥar"0VܝL@1z_GN[X 4T^{i6;u[p%._ npɄBx^w)M1tHv}">S$FyuR\iZ,X]G鸐ÛJn{~t\łm-p\LktZE߇fD"A2 ^pkkkp: \D-> F ݻwP.q1>:R8)BG6E&B4"ow6XxJJ|!L&Q\t O<;\.H$̠R`sst |h4g}ND;J)71== W$B $H A . c@˿Br&f3Q|o,`0!N8D"F-,//㣏>c ~x<99}!`0 X,bii tbX*v`@PZ4*"  mT*(JT*TU|hp D"{066yv`& Xl6\rSSS"BTD"]u <<<yilnnb0`aa|vކRGw:R 'Np8o~|>zdYt:n`~~hcN>F0 0 xw8ߺu R dj h4p)!`ii * 7n` \.#H`qqX X P{u].l6?; ,,,p ؁L&QVT*JVDLSSSv r962 ݻqjy*`2# 9Z-f3Ǿ`j{iL}L[Tʠ8~@բj1 5 x{$(?LB}Dt-0 ؕKNUrȒ4WXRRwZ\G :tP/r`- 黔`5]G LZR`EBjNwnA|>f36* Bw8߿V`0jfpٌj1V#o߾͟k6p8<155ŐT*p88vvq>}R do߆L&`@6^G,T*G}D\~RH$ [*ڵk<,e4h4066Rx: >"aDx(!JAn#H#Xq = iZ$ ?~8u!~_ W'xŋ$H A $H A; W[zO8f\pV ?Dž p}ncww:?u:<\.Cx<Α`d2l8y$Y p\t:իW~/`XqDQ"sf6`^GRBNí[s>&2 ?tq!h4nj{:?`0.6nfp`}}T*$ %(aX#j5;55˗/sG.EE+ ijjayy.[ꯥP(۷oC.%}>|J'N ϣVaaa1G)AvMOOC,cki0115޶r|nnF|ssshۈd2Õ+WP*pqFt],,,P(h4T*! Ν;y&~WFGG177|>>p82 *PT qaaׯ_G믿e2j5R)" /  ,bV!riVC0D:F6eYTzQ,l6X&l6O?bJ2 ~رc(( |%ܻwOF"@:kz Ff:jbA,\.8"j5l^*f3f3t:GFT*m4ϸ遧|>vvv0fvZW*L&X,T*rL&L&4 ?3}ͽ^݌ZX7ovs̽X,fKVA*"pp8Dkz=uS:X2 }G.#Ͳwe(JryH%Ct:7;@I)oJ &+9H ҚP(qJ%Сf Z8m4vYմmUX")Fm * rR`05jA@dݻ ħsGg䈦8trk:*)~\.шfGF^O \.ehMуӏw݆N۷oCѠ\.3tݳNMNNl6#Jd2T*alljA.݇fj|\VZz=G?B>^"0~_CTpLLL@RtwQ,c0O8Ht:D"X ǯja~~jc- J%'wx<f&Ws#hp: eLNNwA |Yv|Fym!{ʕ++^A $H A zu; pppÁD"u( ~hvJ{oC"#ڵkX^^j FGG"sWWW!؅hbJ~}~B]~n czppU>}zQwʼn'8Z* X Z^HrNR II"EB< .wyo6Pj^cffZ X ~F#|'''aXL&À\.X,xڕTp8`H$b*z=pvJ\.DQ C|7X]]E(©SfرcH` 4L"#LO>l6"6\.;fff0 P(P/h۰X,èVvL&aِN111P(\.D"Ӊx/"F#+d2LMMB0DV~?b_?;*Ο?3gΠ^C&aooVv/^~{|n<066BNUlnnvh<|wN_~%n7FFFv}ܽ{gϞft:0wnuqܸqx1N'rayy]tR?Ǭ똛`rU*x l6!JL&-l6'`0pVh ĉX]]}'Hj52޽IDQ,,,ptnZEP^BhspweT*QבJ`=?H$xv>KT꘥Q(nZOMP,ňvuf0Zuva$'rfs;MNQ&8irokZ%MY, *9` ܪ䂦Ҵ kV Fj3ph|2ף̥:kMm$NCxbz;9pdQPo6wpe O{ l,^/d2< ommq|b`w\.nsKboCjFO;~M^RP(Ʉ|>T* X\\j'x< (2:jSv;J%'d卍 gaFJ6f3.^7orT*l6߻wbmm 'N@\FVp8cH$xx}{{nT*Ň~(^A  $H A $HП+-pz0h4B,sgbp8Nxn7$kkkp8ISSSgDZ JuF\. Cx^D"s=Ǯ#HG`0ncv<|XZZp8L&CR* 6b._}{ fxE"y,//`uut:a0L2 BpNhpw0}\v ?p8x<5D",--q'זNzKiD"zH$h4|<Ǒa2H$QLGa{{'N@.ãGN122ݎyXVlooCRB M&2 <r"0??)`0`ee|x<^333H$8<<^~3~|8wIIittB_|8z\h} ö ^XXfC"">C@Rf_wvv0;;5L&,//#`gg^ﱻy!k2t:199rW_}nǽ{[[[Z,,,@Tⷿ-;H$jaZ޽{|ppzQayTU"2zOa00>>n2裏022nqz=q]n>LRF#U ߿|_ZZbd4q)(J$In^v;CZ3R, $ q5 IIG6]˅ r5L \R)ZjwRWD"A"u3VVV1\!K Ĭj캣h~jЌyQNIPT*`0p'{ ^clrp^gW.M*PTY`QMI^'B\!R\k& _P?JW]LHnhNȴtRJR( t(ҚX,FZ}ƑLNvrz"-EeHJН5MlLf]r ^U*$2y )qR0jR V1::ʉj=B"s}ZW_}nr V+V+kvQ8qVׯ_G>n4^s|L&EB>! :0;;˃Q(Jwà t?F4l6>4 v;V+($ ^/?~zݎH$Ll6 i1Eevc8buu& /+  $H A $HП+- χǏh4 J½F. L|~|^`P*FPNÅ W_133p:<8vwwJ0??N>@/^AVCѠZ"J13xRP*VA19 GT*v;v V#A4wS"GR"94 z=;!h4 J 0MOOc{{NB@$^Wzl6<}XR ַgp8`ddO<۷1;; LF Rt: Z zF|2Ν;'O@Tv#ϳqtt^, JPTj`X011j8y$D"vvvL&155h4 ǃi$+]g6 lt;m6L&>|5,LD"8N1top8$ x<7<V+ #zdx1b1<NOOCp;w011 zz*{9LNNbooXYY:?TjH&崭Im^G(V!&&&8^ckk ΝCbL&r̝MuDݎ_Bܧ[t:xL&Q9`0`ooF& d2\.BQ>q4 b1k$IvvsNLL`ddkkkjIN.ZƽʭV ZgM`M&=xH@ pJJ°@\.GPkZ#Qmd&wRDZ^3,`rtLJNBFîVbh4ܿm08jY$1Y^NrM~nɣ.Ur Ah:5 t~WVC(!xZnAp0a3g:d%29S2f2a)jAq7ӗKSMt|  <@D`8AҵFNP:i?@d2u), RvnCB*bkk R Vvpppeh4d2 R)f3:.vj ˡP(pxxՊpX,BBl6k׮\.sB %GݻwaZa2 Jhx͆Cܽ{N;].Z-y8v'NnB&GPÇaX:N|gxd_C`llF~?f3`ddRP(FIU IDATł7I@ A $H AK"/C,j`||W\ZVǏP(pIvflSX__H$7pP*(ːH$X__GVC./MJ9;;jʠX,Ν;A.cj2BR!d21j|20Ο?^5 $ZX$i6a2j˗/cll㣋">Sax^d2b>2$ 2F#  9vJd2$ B!N8Ʉd2/'Oį~+_ǽ{8Zɓ'.H$dp%y^T W\-D"|D n] DL-h4T*X[[;>XV8qv 5 "pw-9&''1==j!X,(dskP*077׋\.Zwyr94M@TP(cdd9?p:8qj6U,..h4Rjr/1xxR*'x<DQrx.Jp% G<_\\dG"&^z%\rgffpxxwG0D\F\b]R 2<fa0CfrXxB~?D"?~h4bqq8@բnɓ't:lEVCVG"]x9":VVVpuR)b1_WPb]Oy=y`{[]F dVq4vLrrLn2c[).8*M.R$ щ{/ Nh|(}ݝsF hyRX,lb6T*?n}ԣMa"D"|F#n߾;wtzjFNCrz}% $H A $H?v W-Wۍׯɓ' q!un4MT*|gjx^ܿc:mn~<99 ߏ'O `0… H&0h4HRwT*133Rѕpj!LVb V!ll6cppLccch4BN!JNFBZhrlF^&JQ.9pnRp Zχ5|GX,DZ}*|px?A\FVF8Ʒmd2,,,bt͛PR*h4X__dNVT N4ML&bܾ}nDCF#N'677j144f vrjhAlnnl"LcǎX,bccw3x^~Dp->}>}^~9Bxj10lXXX@ ǏcǎA#n۷|T xG``(rH$b+^#coo\ -`ttDP`7s:xpMqDQT*ff,//33L|2t:&''J:h4`0jܹswm lmmٳ`r`Zp@rv0D"a T_F?{ xH&T*8{,wj5,,,p_0J%.\~BlJ%'S_29"4l6?g5LHR쮣bJ.F0ܚ)`?jT p"G0N<4v0LRmZt.V+t:>n2 /f3GӁfp8L&p8?4p&JLoo/._~C*"JݻEVhX,?JTt:B*b v㼳>\zNd TbW\\.{ Jя~W_}/^(^A $H A z}"G c荌wl6L&VWW1>> RGiVE$A.zzzؕVoرc~r :R NZ?l]/l6cwfuF,AVC6HirO>NB Jmb1X,0 Hn}Ç1::=`WrDR/dP@>Vŝ;w Hpe(Jl6H$ɓ'XYY' NNL&~.MӐBjťK077DǏCrqTfՂwT*ԍ x<=kѣGxtoop?DE:>3h4T*1wt022Mj5! h4ѣd2bpݸw 1;;l6 ~Ž{RT*v9zwwA(r9"(fggqM,//7 GZ-|>^{5y<|>Zl|H$ف\.G?={|>QEZOF^LJ~~D"nLNNbwwd[[[[rΜ9_|H$B$A?FGGqm?s~C,l6;cZ-Z-\.Ǐ#cii CCCXYYNc^^)?)N8RR Nd2nC&Ap?z H؀N㟡5鰴T Ʉ]lmmW_E$lFJjܱ7vzk:c'pll!H$" ^|Er9L&Eܽ{>pj6jZ[gϞ۷T* /v?L6d2VB0DR$L&Fa2t:;ٳ8y$L&l6.^Qbkkkx^u2:FFFKXBTgFmrD"ޖ}( `ww@F5-q%kh4fXXXo{@]b( z:GmmdY x<<}& zG:F$Ph4i|H$8}4L"#H8u wߺu & ΝC";h`XpyloocnnƒRp !LqYm0?iGq숣hY\/*dYZ-lllp J/Az{{VT0Lu 4MvR|Tt:]ZR VRP(`0gHİzG ҵmِd`4QYQ*G/g`W(A1= cBBNgrhӿhZ]>]%8N~\_/NFÐSP@&>ٴ}%G= bK_T*e^b: 6Mv6S|6?wq޴ra5 9iQ:z>t^Zn"ti^Djr i* azz V DNB2{Xx<<`0 NUT CCC|^6x b* X At$.N'.vvvvف@ ^`0Vz L׹ӧO!8BF#q= C!H\.cyygϞCzÁx<BZt:>fq_9>N7H@*ruE\F?_ǏVagg+++8s n޼ɃcTA߳^A_Uz $H A $]u@o۷oCRq'n(B `לmH&H$J§~ T Xt:L&j~PM.qb1d2ahhb Rr9rrffff9:1N#T*w||KKKzw[o!˱h42HzuTRd>* N':L&6S|19VVV`08oݺGBT"l6# \.c||vP099`0+Wĉt:l6va0pm,t4 fgg Fj088Z)ivIj5loofl6㣏>^|E>D"oI&>FFFPT033DDQT*J`Va KKKlt:BDA$6E&PlmeX|kQ,DPK/X,h֭[xӃR) x<|ŰJ/~nBz| &&&T*Rm-( ?022LÁ]~X,t:z*={z 0ܿdp8tff³gP*p.٩뱺SNAVc||O>C6uFװӧn@ ˗/j2p8X,PT;wp){BÇs!J\.h4H7( x)pTJ\GKL& cxxt!&xSg:RA*orܼF'O ɨP(jv<<+fFd2 b6F#0<L`VBNMr9v:g}ꐥ(dS&7$kT`>Tf=K5&A8I)䢤$\V!]Xd70AKr4S.CRp1 о5 4CNRF2 bJ!2g:i/]L sX| ˅n_WNC>5fzQVğ}J&,l6qA^_^^FVC>ŋ++Inz $H A $]u@3go~!Ad2\.mlnnxDl6r_ `'ORVǏqy;v .]0^/Bpilmm!@*"H@g?zzz0<<6]OJ\ztǡh8tiipW\bii lGA2w>mnhZarr;f38jrL&cM`p066)|j5\.8RjbccfV尸J%T*#Ncyy:4hCۭVwo:fG'l6 шx<EM`E"{G"F9Fh; ӵr-|>bL&iyt\.wRZbccRc \.dYacrrrJ 윝p\t`ggKjvcj5 R bssn6 SSSſ⥗^ŋaXrP,uGo0P({?gxv_ٳRD"i$ 8x^$Iq$IAڵk|>ɡa2rX^^kw#ϣR!N#JK'? yx<q7k^p8 T Xh4QfEq caaF8f74z*Jv?Sp*BR<,(~?iL@t<vH%`O.it떢r9, 9#L\ZtT*VP|m%ST2ܦ{.L&f30oūkZ^|DlϡVL&a2j-^,aZa0v|فmGst{{{H$8}4G7p }ѣx1D"H$xn#N#ajj jXX,Át: Vaddx#{=|{c3bgϞ Q>bd2|v]_QrDݎw@$B $H A . EW^011cffVb1T*"r `ss@fDnX\\DV^gg4xP.9Q.#baa/|A'q ?~2 p6;QG?AC.swvB!~Mw\.>|ȀnpppXZZ\.\.8N͐L&~\.$ m΢T*ǘ¡C\z'N@4E>^P(qŭV }6z-VpUJ4l6aِPQ,9sp\8~8r9,x X,<|Z [[[ u`^.ڵkq^yloocsssrrp]h4fZ-lnnB"СCV %9])\Rh&N'4|>V+Q,Q*044p8n NQCCCف^G8l2 0?h4D[ D-(J8NyE,--6ZBPra{{V ׮]ٳg!Q.A\zχV}H$$ B!bii]li`gB^gvk_t g.GZF㊩U*ܩvݲ lZP*T*tB䊯T*vhZϹxvv˘6Drұz~OC|>YHnh5l0 됋"kGZg2]c'\]E[Lb\bj1t &(gv-S=_=}v+HNǂ39 :v?먇LT*vOT*vjb,-.xd a,A(ZBj#YvxP_ vczzj)ھBV7776 kkk44 'uP=L<{ 0'fN^f* *MRȡhzaU?11ÁBbNMc~~&nܸR ٌl6 WPDL&C"qXEDQnALLL l6?2P*xwz155z_ַ^?@ A $H A Ink|'Dݻw/ߐH$088Ƞ`ii sss@ _, 8F#V+* v;(T*ۍI nzV }}}h4;;; ٌMHRvzT*nqb1plK3rA?GǏqI$Ib1zt:#a``xΝt:HR+1RD;^j ʕ+pX__GE"B 'N`qqFZ dT zHR DXcǎ[ݻ } FGGu;116l(Jj0LXXXn*P(l6 ˅}\p^N<^}U|'7Nd2 >:bkkkCVqa\|O>ew2dױZfhctt xW JD( <@rqk{P(0==iptpU|gpP(011O>oƙ3gP.ϔblIL&Q(l6199H$t:qNR}_Σd(;766t:QT` Hcll l6111fX,ɄAkZ | ,z-ɰ̀rnۘB.vfloosܽfSrD#G>000J@,l6B.>ݵ5yJcp8 yD" vhʕ+\3d2.y׋u$ r`rrvvvxxEZ&n6S[x)L&R)V*\Q(pEܽ{W$H A $H Ak W-7!`01448v=?rVgffPVBVfncnnR `cccE0fD4g}Uva1= vm,HDjp8`2 ]uC 2 T*|>,--1qD"V+vww!p`ccX ^L2 (|>azzܝӃd2 K0:: F-H$( ؾ>v4jZ NӘC."w]XVr,,, ȑ#q*1??A8F.VFVf.DrĆB!XVujv+r׋UR)~'@nGGGQVRCӧ s. 677R 0;;B׋n>B!KhZ@r?H$ݻwy8rr9;~ p3<3XYYQ.xR\t & SSSpp:BP*H$hZB{.d2, QfVV8:2ZsŠV T*8DQ'O|nz'-9龧h81baaSB08H$8@*l6#Lⷿz}% $H A $H?v W-044FP(Q>@~)D"FFFL&!pah4 $xcۑJxPTP]eXfl69nC~$IJ%v׿R`bG eEx<yj5auu}}}F0 Vlf`"߿C#G?GƎcqq> $ QVQ*033}HNܑHpl=H&VJǃ5=z\E-X,x<ܻ| TUb1CPK/)Å EJP*bfqrDH5d2;wjzz*>={/177,H@&axxM>G__666<  J1:: X̱ZnBD V 9NaݎwXXX8r9.^UkJ܄bq5 eGGG96"oܸf'Iv A*r>C:͖H$|2Gil64M^b bfT*b`ooG< 8|MR)yj\t]gΜA::N';={m ijgx _zj5FQ*p FR)&JբP(  ׌fcPOݶɅ'q-r9LOO#s5L&Kg4QVQ199 NjZ}v\xGQt̥R) gd2dYl6rèT*g;= Dx< YR!CT`L&lxwDRO?Nfx5r9X,ŭ[~`Z188r󓓓]^h4Ba}}ϱjdl6R@v 9Zf χx<BVT*˅`0 !YW  $H A $H+7x2 (^v >'OD(G}ybeld2p\DކRD(8IwRF|W\{_&0 h4RlvbR@PߡP( Á-x<,..r4aZA?j/>`iiR6 eh4 ɑJ@ɓ'122u buu;V(.;Ʉw%Ҥi IDATR jA CTΝ;Dz@Ղl>6771:: LBTq;qPױÇ# #J!h4"?9v;G_;W$&1[ fffVDhdxPuDQ׿5z{{x2ѣGR?~ hZz_~W^__#LbxxgϞE8FVU>}d2x1^z%?J(!p>r݅A0dzt:5LOOl6sǏ144h4 ͆h4 X V@ L&wbff* R`0X__GXķ-<}\r8(BlP*H$h4666 /pp_VE:fWuOObzT*J%hZJ%cll #GvCP@#addF4ӧp8}GѣxN'z{{L&1acL&&~^/UZVx<^OxW-RL&h]JP,t:q}FbR Hk4D"쪤ph4d2nM.dZ|>t;z.BOrD@$LnR=ԁKRW1t\8`t2@(d2ϤJ\>ɕKh;rGu}!ߋD"=jԩL\t.c2&Kȁ|p"MDZ^34' OǗk'xРVACR\zt:R)/XVzC n===tXkǓ BrW*aP(_@&ѣGB@2DOO~?_r9ݛ~? a4ajP*hT*L&!ˑNɑb~j_D6EÇr:A&7 N:J@ \pgΜ@.C>8(r?XX,D"ppp& \ϟh{7uS:Ο?/^A_Iz $H A $]u@K/R ӉN>F#>c<}?jVdA b2 "z=$$ n)4b?[h6YBaEHR A&!A.nӧ1==Fk׮sl,j5硡!g\.GP@>^lF.c'kkJjt:BqXV\|KKKx71<< ш:~  / />L&N1wJ%|>s1d CӉE9sRv7T*D"}t055A8ND"m;wJ7nzzzH5::R?cv{5 LMMn#HnCӡT*h4l6ƍtx Z07 r>? 4hI)YlKI>&NK/W_EׇF!JRW lZXXj288Z F0$˱OPjsssJ%N R&V6V>>aqr}y XV.!VVVfi|>qR5 KVz DBT*%P9L T?Օp8t |U fq9*ZuuuQ(puNu*ײrfV+}{qG`~Y5|w}@*TUY[TZM?{KU,/J. pZՀX,r+gNSf58`zV+|>l&J4ZoW<J"._UNqIfNDfL&CfK>fhv<|~d2*R)U`Ç JAi x^bF#V`ttx^1 |_grI{D׋˸d2IZ%?CKbC8&KBF7L2aٸr \aWGGG~akk #׬~ [Jl6n7׮]c^xRdtt#Zd^{o}[d2rJk zi޶jjjvAo[i){\vz Fs=G2T*?ϝ;wxꩧlMNNl6 b1Z-===QQLE~G\xQ"'wvvzurAqijAy??h4H6p\9s1 kkkPl299 tϳ&. nbxxh4JZ% uxxXrg?~^<99ga||  ~k_rq||,?~ܸqYܹ祏j )!Z-<d2D"X__ghhl<+++uz{{Y188(S=,_]]ŋfcJ333DQRBD"~?}}}$I&&&.GGGf Vcn7V$Lrplmm |8N666dwwF$FvCCCJ%H|LOO ```eFFFBb1b  <44(tT*E(ի 1LUX,q0L;looKioo/VR$v]===o*9H{{{d2V+]]]8NjQ*8JQ,1r9PC0تx\[jқljh4Qw8f U1]\VI$TUeyUVE6N[귅S- 8(U@T?㝱  A޿SEh40L'Xm.lXd2SҏVcͦ"[Yi. }  ۢ@oV{UJhW*'P G@:lV:].BZFgg5\l6f2zU,QWݿj?԰ncjdK%3cb5 GfY`%jR,) 2Xr0 =tZ咁rL__Ħt:666X,a"HD:;~zzRDPn7댎J<|F222B$\.x~:onݻ'& 9s6{{{b28>>ׇnLMMqttDGGsss\zٌRL&F|'$`-I|X__p}M~[E^8{{{l6fggq6 #iΞ=+F^{ z׼jAo[mV[mV[mV[2Ao[i=<22Bdxxzݻwaz jƗ%={R)|>SSS|'Jjl60Lj5666$ ʃeߏn_Fp d2277Faccy@z677ŵyxxzj<|N0GGG#qlnnR*u(hlmmax<7Jff t&6FA*G#tA\³>&8w'''(TLÁl6Kfh4*L&r9\.׮]cff=FZ-%١grpp;#`||d2I,crrE$T*9::P(L$zH ߩ`^^\.ocdh˗/3i}}]^ymyW>7npjsss,//-L:N6&k088Ȼ+ X,)4 T*=zD(bmmAN'}?<^ ^K/D,ϭE RL&b~~ˍ7 nfy!0v3wyttf,,,d2I@1f3/">^z%>#:;;iX0 p)~:lmmC/T*N'C23338N"8~_b%XښUZ*@A/5Pϗ%8h"V%| z)} t*bV85UĮ)F[Tij%Wstww`0Pft'@uVUZժ=MTzr,ItvvZCEZ,UbP,ŭ]k8(Ǫr2nFA\jId6XV96jzL&Dyh@ԶOUOAKN'YAU*r^c6jt:@mju~_>WE+zX nt:U \6M"Umr+W\.l͛7q:LMMq]n7tvvrm9H$"tuul6eڒt:-m, r> ډV%}}}aI2RԩSr||$INNNXXXfbbJ"b |#Z-nDnk'SVl8N1L_i2jnqypX9czz!>#yPF jD"To1<<,kggjJ\g?_988Hi\.|>> 666_hCp8G}$P*899x":NNNbj5Bt:rf2qHGVNp8,.ifrrRDܤR.K"XXXZ ?88W^`Cudfttۿ[&Iz%WVV\.z)^*voIT*tww399I6%p8bd2399ɇ~Hww7|J™3g$wwB!DNbacczsQդShM<իB!,$ ^u*G2_W$Iv;dD"O?c}}f)d>4X \@z\.8nz^\Znv]nUdH]TPIJ8VNN$&XV<5uEU`Vp8t㫺j*J:qshᮮ.- O0fO|fWW&l6bcD$ %Y}rt:3W ʥ\bnzU ZӔRd qz.XɀXu6r97?Aӏ;tu:&eq*jh4WPtU?.rT̸Vj*7|)P\.KLVII.r,NcUvT*F!qZʊ|ߩ^du=vt:F|>/>dll @[V# 7;  OĥommٳgX,f666xv:r5<@l6`ǝ;wpeU>SGGGX,tSh4x``7x9cccL&q֭6mRmV[mV[mV[j޶~RϱX,Z-׿΃|i>|H$BPոp r9>3q:WN^{qJ%|>>dppP۷`T*y9<<ͩSzh4p:Fx< j6Okkktww_ə3g$I<D&j4B>Oz[ DQX,A-oݺ%P`aaA\SSSϳ)=Z}]$>vvv{q `P10lJ)Jzq8|g˃=Μ9#*:lJ;YCCCg͛r9B^cXVq:lnnrm(˜:uJFCl6&''U:33#>q-moo{rLXc&  899nO<guuy._LX&FabLLLH\7M, |8\h4r4 !oͿ!ovq\:::8::^VܻwG|_gpp!8ˬj4\.f0<<,̞}|><`ff@IjJ%1::*b㡧D";,R C(mo߾o ܤ'Cfb1y1r]zl6L&8Z-`0.FG"ǝhf{{<ĊK IDATr{xީZIɤ8^[U%XEkU7.YZSUrvttT`L9UoVZ\.'U9 =&l4r02`Z%Wmr&gwt,\.˱Z‮T*9:NȀCVrQ,/~{p1P:XtHN8zUgnZJLhvD5,\ fkA] +gʱǣ.8Z cmZ{)Ǯfkę+xΣr*'*BV9c,//c6pHzdjJ V.vt:"ɐl|>r$ vV1Z$Iz'''2tTygD"r~Z-nݢxyh6J%T*fiiIK*'''ܾ}[ߗnd5K/&ׯ_IdYF#?9wfy&;C6n(8N~ 6fhhGZA_Gm֯vt3mV[mV[mV[mۮ6m7-zZqq呑Y__䄉 |>lmmL&#JqttD$1/"HZ-PR=bll ߐf @,p֖+6ģG$޽{ONNX__WbZE&''cuuD"tF!l2??O:ҍ _!A"٬l6pL?\.@oo)aP366> `(ˤi>Sn"<8|{;?Op\~-FFFd25یh$Wq\y, ~i~OO\ݻCd\( zu4==M^D[rh]__RD*brr^RLMM6ܿg}&hNfh099n^^VWW)<3dYăܺuZ‚>S Vin7?Ol2r[[[d2bT]`.677) Z-.\"/2n?''''\pA\*J8\J%, '''bxy7W"1kkkLOOc0'3gxݥT*I!~^CvVVVwKR!py.]$}333TwX$322™3gbZ-<|^@h~Aű15룫~06MJV+?ϥ:#ߗr͛L&`5zɤϵZ DPXaVK40ݶU`3͘fq5:NNNNT*XV @U`YB^ Zu+X>F#QN'T^@:Zn&I\* i0+!l6/yww\ *oZaՏOW]u:Ij8SU4WTF +t:z8E WAZx,t"8m@`]]]l0cXm 28'Ūh4+`pȹ28&Uڀo x^`ttFC&!t:)x<I߰qW "]]]HKW!LLLp||&>YI Q.E0~l6K   !"CCCF8< Ij6hKR!rÌ999!JqYzR.~:}}}p||2>,h|>@qt VVVxV UN~677)`~~^dZ^{O>D۷ B:u}NNNX^^fC0X,Fq8N..//s%üt|]\.QL&[[[x^t:dǥx||^:J)FWUn޼s=ι9F#6Mm ay i6Etba!Z+.cy/ O=Bm*"Y_˿Az|>Ϲs|nAa$FP($`0H h˳>+.Z~Tq?JtZ>Wb^zRF  Jdj^9>>lJ}ZŋuF޽{|>qR)OclJlrT* RIZF:b Td,D6fZjUּt:-=p8:::fVC骮A(.HwZx:ejl48ϋ[UWN'f~(7iWW\cxt2TF#f4_W%%(`h(eq2UOvTzoWAǝ+:Qr]t:^SV玎qSmPqŏr6'b:wvv@G6xyTh^^j%v]wÁ`P(ȚxXYYZh6|zi6T*z@FhnvE"1ZDV֭[~>fgg1 _ ֖tC,ceeTq\r x\.|ؘ8dtvwws}FGGd2|駄B!q9N9{,p `ggJ"`ŋ|?S=zb+8Jqn76m~?xbr PdYx^^{{_ᥗ^byyodY88Zrpp NMB.}}}D"˼Z-X%L&|)n'HOڵk={9$ 211!A}6 88(Qug?cbb'\xfCшW9j{'N{łjf"_xwghhHq\r: &9>~^F||%D0 АwJP̙3?h޶~-_5_ʕ+mV[mV[mV[mֿLmoZ f'''tvvb28::BCNr9k4b4yWh`XXZZҥK=q =zWU`H$;j} ҥKQ.?}{QV9<<I|;lmm0>> lVCB@RPzk0ccclllHi>_2d2=bhhH`rl6{d2r+׹y&'gwwgyFettT8*rʕ+A%M&tf8TwkX0 zUt7o$0<T Ý;wKﲱAGGy_4/^$Nspp<[[[B!P*xpN[o@8l6L&999rvsB!V8Nz>WQb<>>\ra0KKKhZi4R){ݕ^X@J]]]DQv;===˭_5 :;;ZX,qjZI( 9 N1ҋ R& ,.l6+af El6fHgTUxǰrL\.`5h>C9bVqΪV9RWlO@PvT=*NYjBU=*YABfF!ۣNV@z..] @?U0hHz>TulfyU4 @RȀrݪkbPTE9jG`0`0$[.j5ls6wjD F9.Kt:-[OOë5q'r5r9z=wޕAwt:2Tp" JnKRVu8a8^glXի={V\6\xQ:(|^԰.Zku:x~ ___#PVt:VWWIwoo/z~ZZ&!@'l6/oN'233C<RԎnnֿ zjjj zMK޿ _>N*DZLY;==-.R.ͩSwLMMIdsS񭳳R)~ӟrf3=X,299IXdoo8N^/cttT#SSSt:/_fjj ^ϣGjB!VVV8s Z#Z-333 DQ@Xd}}9]F!pi>i6IkƟٟVR(H$|e<DSNɉD"!nL&z6NSt~RfX,&|EӍbT*899?gzzZeZp8Ncqq.z{{899!pi|?h4ħk4z=\O?@W'@D.'xH&TU\.Ej%n pkZkz[z8$Uv+++2j. t:qWNP-2jT*HӘL&V_4"UjMP`Tr>.^#\U*bp*HŢD$e5 sqggTTVpTyPhzƏdV.T*VT*u&X쉴r,*.]l6+_gn˵_ cT, ÔeeCu—evwwҗģGĝ`bo5\ʧN"`0dhJG"^gYN@ bass$]P.I(j!N'GGG&''١j#7jXO-DXV*kkk,l $u(BP(PՊNΝ;;wblfzzX,Nc``@~ܾ} |t?R]duuz{{etկ~mZj^ڠjjj]moZ 1Vūʅ xXXXbo}H IDAT&J1??OGG;;;l6}]F#tuuɃUM[*fqu^\.G"R`L&zzz0 ﳴ$1^J$T*'ɵkA'LRvF9sDFONNp82qRJ(z-ˌFj<|PbXm6T*ؘܹV.Zc?G%QT*qEN'$I|>8:`B . cr|ҏ8;;+0BiZ<:w}W"auu`0+\m:::`|bHGGpǙܹsx١V=;SO{/p8tlmm177G d2qrr|*g͛KOc.A0E|>z_]*WVVh4zy{.ַ|>lmmOS.bdr;(P_g}zNwwEz)RHOOP} +++<Dxp0>>NGGDry5V{{{<3cXx!F"չͨ?я!3`999_G?ܽ{^ztSNIpvZčH$Zq;N4 hD",Vwyp8gooR$*5N qdZq8:I~m~? (.\j&kZ2`A2$([[[j9Isww a F LGgg8ӿ۷o-E,f) ܾ}[WU(HDO~L&Y[[T), \J,../p8( $I:;;tU6O~;y<BD"&2hj5.]PI2h4w [[[0==M<'EV#q8LLLPTXYYApi9n;zuՎn zjjj~m@׿u(| _1|>V`0ښtmp8L$!ɐfnnnrxx8DP(D>H@V  B,..RV9utq}>#fgglnnZܺuӧOL9LLLLDtD* q8 p\z='''҇h䩧booO"\"|IPhpyq^B!}Ygիj6h4aX(Jңz#NW^ym67n`xxZÇ)LLL{{}K,IdKc'E!A,k˖-E)bYR:N.;ߥ8`Vg J—_~{D\^6 6 X l#T L&v95Ns,033\.r ӉL&Bwy7L&D͆ZqqZ-n)]]]RvL&nyJ%eA8lnnrTB@(;w,Jj|b18q  R($ qttX,+(L&շT*v:N3u_=NWz=;>2T*J%,G*JFvj4r$4AINSo0P*8 jcI6L Ko Xw撃Zp@e0J0JT*Aq1u)S*R//HFliZ/J'jT7j8m- N1ϭhj;Lhq^hv 5 W*4M>Rcr2c&߷Nu.ZꍧnrSevSOqn^F=X,bǮXoFFF}fii$?<l6DQicbb?ŰZbrmh4<~nVb?}gO$BHR7MT*! {{{SžvG>3> V+0z=j5~ WП$B $H A . s@= 8wvv`Xva4!Hncf3"Ξ= F\.7xgΜaW,#Jall Ojclll888`G_|FK.lO>﹇^ԩSXXX0 G"x^lnn2P=88F&nGZEV 2 wQJ%x<BVR&޽P(PǛoz*N8^ٌ9vz) b{{s`4q} Aӡr|@6eD"Jۍp8d2 V \Νr8ץ%\x;3E"G2T*\.>;ҩG5!`hhpFSSS{ttQEPVd2 0OpIt:x^ܻwt7+byy:X =6LbppzZL0+JL~VTUjbin{ Bxgppp\.NfSN!`ee& ˡRPT)](ꫯ`0uL&43J 0,e|gPTh6_h4WvZS^'*>R)d2.rC?r\ĥXdZ Br#*)J%* r9b Z*Jj4".b12 T*V+" 2 l6ZZV})NJSVLD{K~\@g+ TU|&J&^zwSt^gIй\.3k8thZz4duYM ֟ u:H$( FFxq@ Za8vrE>`B@P\Rtmj z=4R{V+`ggOr<Ӱrv; vEvh8}4pxxsO?籾`^# ѣGtzB&FFFFJzq fLNNr2R4$ 9s*P(p 4M@VhSP(@,c{{( HRp8APf% 8rT*APbϥRy^/H$z=666ؕvaZ\Պh4z^{"bpE^jNC.cĿrϱRĭ[pHr?177"_(,5mo9z=h4u>sx^x^D"# ?o T u|gexHd2|l6sN:W^yj1Kxt:qxxnsgeP~?>s lmmq?$EgY#rWk zrtgW_48~ajj . JoΝ;d2j077ۍz]?*vwwaPTh50  2"G}?99FRvH$D"9 'XVpZjavvXũZ bsssh6H,x[rRD1uLT*ݥ?D<r9;% DܿhP*v `I\RK``$9)JmjxF,d2 0;a}}PGGGzx‹/d2ɮEhZ=::^a0pppy3 B(C__1~4 d2177? JR~hKn⍍ LLLp`wwjX[[ӧ駟B$p`yy6 cccd2BzXZZbdO~^u d2APf3JmEH۷ocbb. +++(8y$wz^fv1Pq\h4J :4 a۹Xnnckk 13)uVjvo.ZFd (dDٌ@ ZU8qNٳgF LR f^ZŰX,|>Zfɑxo,fggQ(8޺Z7M8<4vwwٱJp%h4yuv|>$ r24. H$X,x`0L&qĿ˿@*jh J633۷ockk D<'yTB__ y`700 >lmm!NNbx<͆fpp.92 <8G S$6W%Д6Mm8Nj5dYd2%צhE"ND>g`ӁZxWɄN|>J‘rX)Jv,H]m S\, pv_C %INh4ܔ8@/vfjv蜑Ow8'L`\wPD^GCXNcMzH.v0t%(v1r|}1 xX,x+jګ S/tZ@Q̍FϟRNНN#̛&V+ 4 vWnyOшUL&vܚL&Z-8|>b1,--!H`nnn[[[|Ft8}4PX,@.J|vH$( :}]fz2 q7sٌbpp˘T*VRvV+N:X,1bH>_|p%t]㘜:jbss U*[[[L&NC*r9^{5 @ A $H A _K:ׇAeSp8099 TRZP(۷ocll < 2 n7 rfff8:b@.cjj R z'Oěo HT*<~'ODZE6E J%JǃC\|{a6P(NpDQ\pDZ &nܸɝX x饗jFq]cmm KKKp^ L& j5> … h4svL&tro".:u o+& j?)<`Xsycgg+++O9r9, wQk"`7j5<Rt:!H8\eD{l6&&&u~Lq*˨T*luQ6>>L&шfph4"X,`0}YhZvݸqj\.^J"zo޼dFE*b* Q뱵Պ@ l6}. ^]j#H$d2p\x<ߏJxa0 ǃeu8Nx<T*,-g4 IDAT-XYY\%vѭ͛GRZÇEXܹ16 . x|~LC!j5 8֨W{zA2ëFl6uv,D";wj|GGGP(ZBtx0>>Hϟ8fvC*ƍfÚ@ Bt:Ch2 N!l6 ^@#FmLh4ÓMmOnd2 ׋NJse2GS: VZ-T*hZT*mH$Hҧju\bZF6彟r).`@>vR0fɯCnUшt:*?-P ZJZJHĐJ et.˙q lD]MܪmZ Mk6;5i-ic/wW d\lM&6ҵC:FHSR ߏ x<t:_?LB7n`@Kv||v8}4*0??FO?㡃 t:n߾ 6 D000t:X,=b(4b1|> ^{5|߅f<p=$I<3X__G`;??="cggqyP;zM={:;K`0.MDt:z .\ "n޼'NnQ.ڪjܿbFjt]X,0tAt`ZRK1}>";~?J*?d2p8ކng[Vy=jr_TFdX,шӧOVhphj: 677Qp)Ǒfvq(J e HhZPT ٌVn䭭-~|>TU|gG鰻_|?OшH$V Nɗ^z ;;;8<<`ssW\A>68666ؽN0jFy=\. ѣGvCTj^ǣ#$Swvvn|dj9W$! bdd=nݺ`N׮]YRLۍmHRavI$LOOckk }}}VX,fh6 )taa* |gϞV{{{(xgqttX L% >%gM<믿1#&͢Vʕ+%T*@˅hpp0L[(1;; ۍܸqOng.'O ajj W^ř3gN!HhZ~Kn>b ڻwd2 H$0<<@ ܀Odsx<|PTY^^44~?$G?_W;h6rH$cfC,QE1d2"FFF J~$!2 0Պz (X__8j`6L&'N^(_R W^qIL&0LňT*P(8::B,ӧ!p80::7oB,# 2|x0L|GIbsc@O? 2^/:Gl6ܸq~}6n޼ ʀ' onnrq__r100T=d;!HzJNtpmm8TcEN|>В SG9suNZ&./7'GRZrL5QMNKr @\X,F WA^Jzj)=yՂba+p^{Jjzt.(j=KtT|{E:JbG=Mh4sTRݭ?Y=u#M48pܕLkGMܥR]JD_YV5(tj](fnsdh4X,ف\.n7l6QVa6h4p qT'Oxkmim\.( nz 2 ^/' D-ˈnGXd88r011D"V ш_WB`JZp8-qRbA^8fggQTx@Db088C$pH$Pn7^/,[nÁ W. ?P$ $H A $Ht WП[z׾1B_|~?;!;wpppAO`pB :|'d2lH$X^^ŋ!t:ƠO?E `'5=d2r Z- h43Xb`pIJ%T*|[BXD.Å e8dF{arr^1zj=*"K="r 000Mf3n7q \x###G2D40|>r9~m7׾5j5ܽ{t/^>V+f30J;7u:wv( ԩS>ȁ֭[EDPS q=fx^i`nnfr׮]^GPA׃lfWL&<0eY> g|gp0 p8HӈF8q^/h4/0vRqvNNNrar~nll`cc!P"D"Iɓw'^]X,nn?(JZ-f3F#b ?8{,6,j5ߗV}}}pPTX__G,C @$AՂ}EbssJJ.Z`bHthZ(˼ +bj1@8`@d\?t:4  /텽^ZԋJ{9F)r\*#~G6h%?u6k3U'l*fdr6J$J%dSiȃDkQ/~Mꂥe\.P5n˝|>. %`F\f#i(^,3%`Miu [i&SD"F#SY,T* `KnNb^Y\. v[[[X,XYY˗y t_SWd:$ ~?"0pnT*`"'zXV8~:BL&vvvxߤyTnRǃSNql6xo{`49^[͛DHӰZ8wF#0F#")z=cxxFk3{Jn~!j!ɠP(T*ZM/5 ѱX FNf;;;/1991pVSSS n@?Iz $H A $/]K.:X,FÑ" X\\ fffP(pu իpPWՊ|>]4 ?D__Gr9~HCN[ԑH$`6qmx";"/vZ Nov;RBcAl6d2XcTgr$u<ŋ/.]FEKPըjVBj믿FA 2N .y^>n:^RD?K8q?{gT*>\~(pݘAXD2DÍ7pkZl"O(F΃|>8qh~!N8uDQ `ll ݅F"`T*p \|:ۿ V+:F%p`0#>GFR#0#Ccgg^@Vt|>>C͛cqܻwbcU*!˱ű[[[C(%vlnnrGj$B'vjt:vollNX,VqZR nh6)>"M&(w6Mbhh(Jp88<sv;h4dH&p\T*A:fF*B6J%BXij>J#\. |Gfxl6 ?<]Ln:^|155TѺ^RB{D)v}}0225fB!\.,--6( ޽AR)DQj5x< Z8~ |gXYY/l6 ݎSN{|ggw\.,*.^ł>N]Iq8P(0X,FT0zVVV؉j011}ăbwV+ \.q2>(fffFb qttl6)x^hZܼy`AXD, 0bbP*p5ܼynun4z!9~#^z%j55055^͆|>Bm0QbbaWݻwt:t:9Bj"hh4r/q^g'G}AAzܾ}##qlllpuP@"@(NV¥.͙4Mjt舝|)yd2LLLXV#l6c$qkZ|$P*}r9h4j1BJ\Rtr ߓJ\RutK';\ 5 *w$WD"AZr'y|>gY&8t8JY$AVs_,)\n? RSr6lrS8tr z r|V|o:% #)~iH&k ESGqVcrV^g`H^rҰqh^y?-04TBFVhd׶FfssNh;i||,Qb, ܹ'Osn#H2rQ8v~M#Z8T*V+?~ \~4M`0|O]Jnn#rCCCP*8::7077eH$ssE2dB*o~+':|>$ `2xmb1j"fffpUhZ,o  IDATY~>}V H8s jP(z Sb ;z- @ A $H A _K"~rRF1М^TP(0 0>>T*J1~E"B a4aX"x !V*l6Q*^D"A`vj5x v.R)A 4N'Q.>r,CL& T*q'aلEӁ`[rp=J%Z-xj"H`wwa,&&&VV /`wwr <.`0$l6sb,ưZu"p ""ҥKbBGɒRns˗aX,Ǐ1::Vi#z!6 < @!AP```bjNbt:hp] !ɠT*A L$Pr155EZh4؀B[T*ٕLP!LPfggyOaG[*b`C]^"dX@ t4n79Y!@p8H$t:9"razzHf{p\v ܗ[.-//C&nchhZ{|_ːFT*9R pGRą p8 JBp8ZۍL&X,aTUJ%,//3܄FX,F6L&C0DB$T*^G>B!TUd2h48q& zp8(lfOJ%uEXVz2 j{E"wjT*q+ES1EKRvŒ"fX,l6C"0@l( h4 W)t?d2 ՊCgRT*1Ǐ*νT*! 2n6P0ͨ[(Pr^'g)uS$59v:^X_ B9uϒs *9E" ST}&Xc]?Ocbg-I S0;XRT*A*T*&{ih TҽRy BѠV!JA"@TB,?m4NyQh`4Әr < eAiv aPNJ~:&&&ɝ4PTRp88Jjvy8>mooZ177j BvĔq)8y$|>\v 0 hZzP(|>}<(H$Ř^G$x<QT 77BFZ t{i-H@$! WП$B $H A . s@ /X.\.d2 ˮPVjl6=qt:vnnnhkk z~\nJ촱X,Orb###ffqD"l6 j5~?cnrɓ'HRr 6FFF{all .^Nt: ͆'Oh4T*tB&/qvC #Jaii H$H$dv_ np~Z JsR G8l6AT*011f5|>,--q'AT*\._z8::bgr9^/C~q{X*8wN8/c81777vxVEJjW\BS1LN\X{zcdd@ 8{9ܿ]ԴN8=rޛ} ;@X"Vjmu;LjTir!T*IruW؎K"嶭W"E\@b#}_}o=5]ss1-Q;>cEa_frrRbСCt:hшg?\.*9 b=XXXv366pT*E:DQ^/vfI^Wz9, PZexxX~T*Μ9CP`ggiT`0rq&/^IҡdB\|Bjh4  jd2IoOO{{{A.\ ]cfggfooOI4t&OLL`4r r:tghZr|U_t:%zr9 |8^[[#LRVq\edee/F`\oo/F\JɄ餧<`ZMʭj888`ppf) `WP:LbXWE+T'7|NV*j4V5 yt:y]Vi}DfFWr+C#cWAW@mHW׸K"2NGWWt3' t:D.#WWղ/mjAgшdnr_Wۯ"nYtppΎ[rFyL&Ţ  DV篂\a666( \.A6T*%nfӉb!NS.RA8*>[[[,,, H&L&z> pT*(RMfZ-fffaaaAj n߾dccC1^_v-u8CCCz[Y Te2l6aq7hZ&D"iZaV+wppibZVӗ_~/$055{{{L& 2de4I$<99f&>V v~z;ԉnz;ꨣ:ꨣ:ꨣz;cKG~rJhx<h4|LOOKߡ3顯a6z{{댏۷9O2gggg˅gee`0ȡC$2h4r/Q;;;EN'\.vwwYYYx<ӧfl6t:VWW).ghZT*Q*w Vn駟O8fiiI\L[;FT7˼曤i&''jFb7U쯂euHZ%JrF!˱''N N|B@T%ӉD^O `kkKYOOD~z^Y`V,--q)q.//3<y K6ڵk2T099>Z-hT'''X,<~pe</dMOztf~OSZ-T !w,bN., BϟKjb`0dUuQGuQGuQG@wNb_ݻB,&~Irx<)LLLK-133éSp`X~3==ŋl62| ΝH$b ?vjKww7HT*ER>G' v]:VH,cqq/^pinݺhF#GH&v;KKK:tCFr4M~9P߿dwZ`jt:q:DQ<ׯ_ZѣG1{ys!x)TURsoݺE8x022FG |i866Z~v;$ .\355E\_|NWLSb10 <~‘#Gĥ^WDw FCsZO vX,&>dll s6~ozΒL&X,f`0ĉqppٳg%\.͑#GdA9tZ-׮]O?ennL ˡCFC,0>>.PWR,7o/ϥKbgg@V?fddDv 8^V+?) >|Xdl6ZV)i0oqEoǏgdd|>/0C*R.F\r IRtwwsE, D˅d`0hZ>S"8VUhKKKXVhZ1Q'|Ԕh4h$In7;;;$YEZ,h4J%֭T*vl6D( \.FeTw~~]3 LTnKgVt@bFCkjt:%Zҥ%E\.VZ-6 r*:^>Udl"Z-b(ʪ;iսn VH}UXu#_ڪKVX{qv[*A9v;A7ճK ,ch4j Eo+:&|":{}f.FR$JňnoOOtx|6EU}SSSlooH⣏>ގ u@/QGuQGuQGu][ ZV?.0`PWWCCC|W̊D"ܽ{|>O__^{'Or!Kqf2HV?v,--q ^u< V*<9/_gfcuuvM:fdd+W099I$a{{[6l68[7771Lq}Z-Gv8z(`gϞQyD6Mq)WTWW׮]ԩSlnnP_Vl6+|;;;ܺuǏSX,Jr)jx}*΢L&0bqqD$ի"D9y$f`0byyp^x!dYݻG8&HH=}4Jٷ$IΞ=n'a0T*\r+=`0fIj5pxr,n<_ER!/q*ƍ+yVt䎎;vcc7|?O>E0<z"?effJ`cxxXz+ X,zDӪn?NcvvbH2d2Iǟ?|bH 믿&266X,rI|Mğ*|wNxSjsss(>v:Gq!FFFx뭷>f^/;;;v ԮT*\v .񰷷l%JZ,--ŋّsewwazJ IDATE^{5/AB, Ξ=KoajjJKH&h4 1HDj5lA{.G?'ccc|?hS ˑNy v'OrMr@XR>tS+b^'Jh~:FP($R@ۍDqm~_>czzB ncǎ144ē'Ol6Jf_ATU<j2===A"DB`ښ?Ox"wի_288H2h4211AVX,255bl6t;vLUnXK<ӧ1 , ===̰l&͒Jh4A$d>VVV )tPdv#fY9ZKS b*58N8S@r8筭-v@\.' r*r{Z.W U\(u T'r*@ 5|}F :tww @SCCZVܭ~{pp \u+7q6g,pR9J_vpV MydWfTbmVl6z=&Iz rzq}+7:U/rFfjh"7*WrmR$ ZMs\FP.A^/.ey=>VuҪg2}ܼ &===r8l*ZuwuuIt~7L<{|>υ l6_HNM^$\:[[[?^ć~d_D/..rEX^^n3<<(tuuH$U|5>~򓟐L&|`_RQbzw~ܹC_>Ol&{xxKTb}}??B!vwwiZB!2~avuQGuQGuQG@`Fww,0wwwB&all~o >B3gp<G,-- ~{ix*3P(dAZ]jZ^weffNǭ[T,j5߿O,ٳ t'088Č?rQ٬D+``ttTOӧOFi>|X===066emm J<oo$a,Rp8bd21;;+nP($.L&C nk󱴴s`0nŋ9s<~2 z .N>z>|f !ϳ㡯RO h4ߗxRD4ehhOc{{[b@ݻǑ#G|<~J4~L&ùs|ܹsFéSV[vp8Zlood29uxQ&|!;u'N rD"ܻwOƆLOOS*x)p&0V ~:b= |]ze"r x<m俉DNގ u@/QGuQGuQGu][ _%@mqܽ{Wz{zzh6҅ X,hMUx1RB@ootA~'Ʉp_ӧO3??ϿWJH4ԩSE!裏h6T*.]D<X ( 7kͦDxP($ҭ-&''iDQxr .]@?޽{v/d'NpUz=H'OP*ŋdYfffrTUI#Gjd2BHX,X,?~̷~˗z^q^~Bvs5_sss YcqqvBX,F$t2NѫԴl,--a69vX^^&K'OFh4gqyq5~?T*EXv! F|A B$I}SRv94pO>js@r$r3kkkZ-z{{r•X,s=*|oA<ɓ'LLL0>>.d2XN˅N>  Ol6Cgqq q###:uJş& ( LLL`%ѣG8w/E*?رcJ%, LOOc0uSSSr^侾>LՊ^l6###w#K6<}T *l6x<L&I$~S@y&:N΋L&` S,1v2[M=N9bsN$M&8N0>O"[E:z=V`eGUTAjljJp6EHgr>Q.J)uϫ/K sP\.se)(%B6ndjlT.tYsY~V@XmKVP(l6)x<|xKWF![E ZjUvf8gRxl"vKrfF#NΎ_3ewNhp5evS*djr.hSu (ɫ JEb  S*j8N tW[,n|qׯQTǼjp8cxxIRX,lmmaXя~$.poqQ&H`69~8׿ns9Kt>]+333T*r%ܹ tZIe>99)稺AIsgwwWLJ'JE(b}}fggb,,,e8DE<l{w`0t;Չnz;ꨣ:ꨣ:ꨣz;cKSNdRPV E~IٔRF w!Hp!uݘL&z{{Ǐt|L&#Zr̡Cţy&>ÁYYv8pjjr0.UH 2`nJoٳg4 ~cXh 'zcccǎW_qyΞ=}}}pUngDv |=zd2I4`0G8},u8z{{%Zȑ#i~~?üxBU4ꎽq;vL`ݻhZ nGq N===hZf3h}N8_M.wk,nSY^^f``˅jnz(Jd2"9snJvsŨLOO9H$x^ F8 ViRl)^/NWNX,ŭa6NGXxdL&et:v]wzD H;h4J䭊 Vv^/Ah@Tu;N/fp8؛9U\~__TJUk"+l6/q}TVvww)WIAeh(JL&U(dr^O\f_ܳ*p|/N[FAѐt *誎] 6 EZPq='V[_ Uǫnh4R,ʼn%j%``0(Ϫ[ :hrlܾ}/.❝z=SSSb1n7t:޽墧}===N@sU^TI2ȑ#0ou8LNNzy1~.rܹsh4'JqUx9SSSz988\]]% b<~!2 vbݻwR|/ZlR(x@tBٌvDZcLOOKij7z 6y)lSN 1Lg?#`0$6qeeH$‹/h6|T*v;>O ) hوFY^^R066F?<>uvww m6771 ~'f~\.ogΜattUԍFtwwrV=KKKhZ^_Gl6HRW^aaaX,bf(kkke3_ $Ϥr_Eoo/z677)Jizo$t>j\,eR$կ?K. \.ܻw@ F۷osI~_qqiJNА8U oZ,#Fi4jy&`In7p@ ~\.Ϟ=//jbwwwŁ>::J:淿-^fI8&lLMMZ=j ÄaFrzGxrbz ܔN`0` ۋojxJngppD" |MjJ%<Ⱦ>x ܣƵZMUj߾ܛhGV.{5U@q5 t*Ln4X,;sTRH f)=*jv\߻/^000햸f{$H~?J k4$ llnnJxXjqqnܸ!!C"`U>' %ɓ4M1Ln^/zgoBjjItuuFrI3g$&X,rUBtZGay LOOh2xC{C#T űc$lR,'1??ϡCh4loo37782U(Va?88Mm^/ovvzގ:ꨣ:ꨣ:ꨣގRwllsI7ʊVϞ=B@ @ Nsiqݾ}maV+r^/}ܼySbrϜ9# "v]"S^v F Z Ӊ͛sjE cccܾ}yR)qϟ?nK2rQ(avvl6KXdjjVŻ(333R)~_kJgxx1߿/eppG t:`qq9XXX kggjʫ۷iXVF# TvT*0===[LNNx X^^Fh42>>W_}EOOd2E^'si9';Ç\|2ǎncXp\lllh/j5~syzzzz*BH$"1MYe8DQ$Hu666$\. z*_ Jf>}CjHtww*0nfoԩSߏ HX UF">LP@ӱG8qMZemmӧOdDB!q;*˱j={N>(c4YZZ"Jt:)  ?iܹŋq}v!N᫺zO8``<Fɓ'iZ#s ~?[[[ خjYZZ" "i,SQRIஊ&a4xrs*:_ӑNbP()r*x:]]]fV=LFD* U |^bU *XܼʙTl6K^9s6WnHz?׫/hrP*x@hX[V95Lneة\.˽B ]]]Ezt*WjsC] IDAT3Z *NR$]ū@`Av)W\. qϪ'֡Pz22??/=jHRH*Z oߦV"oܸAjJt__FIĈ{^~\?BEIzh4tww322"׺hfŤ~AEJ%JB@Xl6cXp8d2 >۷oK4l&''1LDQP.ɮk׮qe~122"ڢjrH$4M&''IR$IYZZP(;?H+׮]ގ:ꨣ:ꨣ:ꨣ~ގR$qzzD)|{{{x<HӴm~344DTUZrmn7VEX瑊J'Ja,jAXbH>?RSj'O)jMYTV7x_]w&088(}BӧOepH4hHԣVeppϟ׫Wrt:\ ~_V,9t||>رc3h6uΝcccϟKh4*۹\T*ٳgܹszΙ3gH&OKׯ_'$wѣ###Э'''y ZMΕnOZq$p)zzzvcXHTU4,,,p8D"qhEL&ǎ/HՉ vvvh6BJ%4 Ǐ@wE&DB(׮]#sIbǎӧX,#FX[[b믿& I$8o~Fp%ÇܺuU^}U>|g}Ƒ#GxWt:?0xt:MRtD?###_ l66 ߋN|2|^bs6v288HX Z\.=J.j%Nf d2(KDh7d$!Nc٘#Ͳ%.AA0$`3NaI&]]]dYݻ m ۔e, >FC ZN*+VuJ%~*p8Ϫq\.e(8-@_r,pK snfōkd8HY@|^bͦW;U^@ww_{=-L&:===pVUm?FQ:S}9_U_Egr8dez櫞Ϫ]mS*\. Tjd*Z^/ij@^df_E k6)Jq໸B " lgvVjFWJʆ`h4q tq2sss|TU$]\.jxǏtT*\z˅h~k<_^^h4X,^xAOOxG288.kkkLLLI6%c :<7Lf^/>$0::ʓ'O# dEp!I|ciivx<\.6RD^' QVb{zz;?HЎ:ꨣ:ꨣ:-z_}9Μ9éSZqqAp8Ν;w osss (|>qOt:(jDQ,8lܹÍ7H$p{{[^/Hf6{{{\tǏ @ Ok6IRܸq歷ޒms\R)j6D"!u5zeeeE`kkkLLLP,qX,?ӧOk666h6LMM{9}t:hwtbZ% T*qZ`]|qݸ\.:;;A3߻warL&z8(v'OʣG8{,X|>n߾M\fssf)ջ a0d2An޼)Ç|>v;RT*wfHQaCtvvhrqE #jC:tx<.Px#HikkǏA`I x300 <5ADX__TS5`4VV?sssJ%^}Unܸ^u>*b锄J7MAJx\FAoo/[[[Z-󱿿g}&}v/I{(}zl67odppx<.ҥKbqI?~JB&/^wߥ>>sIk|nnV̙3re1Iz=6Mۭ-A(W=;;;U&J*CVߕLD$k^O*zWu*CW h4$m#TW]8l6*MYVVZTbVe{JE^U27Kw- d՛^fV `e4('{Uf{RoU_fzTo"O(Wדǣz tDNr"UG*;?U=ldUcVS*0 V=ЕJE,da$L2l P(B7n0LA^/dRp_{Yk1CCC2P>p8,}BA>[www>nT*Ɔ?Hj*uNGL\.ǥKptttHX,_3>>NVT*aČVםS9|0d2+Μ9ãGrJB( /_nmZj޶jjjv޶~RFljD"IP)|tww3;;K _H?fw$6w tL&GaW266&wﲳõk8z(b1^$1v,//.ttN(KKK4 \q<geea1*ZFۍM `_N:&2==͡CF3<djIP+] ]e(GjVHpNRV)`SB<k>tJo6>Vel!uZkZV7AnZj En7r%V+ߑ}>fSvJ}}}r~l6nѣGs>22B^O?ON8bΝ;B!Y{S>F×_~sd2H$tp'n줿j'0 uoo9z(.-Z?/^lmZjimV[mV[mV[jm޿P(`KB&0::*H$lŸOO\.y&T ߏ^'r6" Yy1TZ5LuNz-19֘a||p8‚6 Iǎ㗿%NbppN,..rF#hZ"wO?ܹs,//bZ b4y!|嗜:uJP4 Jz{abii FCww7E 0Lh4x1gΜL&#:tb̌V+dX__gkk'N '#GHJ3F!H000` 8N>3:$f+tpy"9rDk~~!x ^y:::288ȕ+Wp\YZZbnn>,;;;|qzzzG?O\.8|0lyz{{D"+i`0&Xd2F$)vqxt:+++LMMKZVǥZa[ r9* ~_:uU_x:<~>VgGG$TɤzR]$>l*81z*CUU*Wf4Q}>4JR.KOb)f46n6VǠH@ 'TY"WW)R! g*MߧT*Id(FJ.W*;;;VʸWHR9JDUװ}Viq׋``0`XsXe5pFQjՁL|Zf"tu;NN'fM.3CXdqqa}4fCa>> ҡ[o'e _j%Ȧmgg'yIC8fnn :r9BXۋl&,f3119u>fccs}NlJ%0͒Tf$u:>X,[oő#Gz*F&}}}rM6eee1&''on+3Nzgzz5N<ɛoI ̙3looW_}ŋ/4V.I R.y1'OQ1u>677r }}}F1Lbuڢ\.lL&2zxg888t:N8!x~ܹڵk?dqq+B ]tPK$LNNxxgy&~)L:HRbR)eaaӧOĉp88|0do6ÄaIJf<kkkD"yFQwwOFjñ^ǹv'Oƍt:\x.xgY]]7|Sp 4 sTǏ'gq8?GK4cl6p8accF*\*+www}{t:$PH0#>LWWn?P(ĕ+WСCk-9rDpn[rL4\l,|~* _|fc``5V+GX,p8`P(jtuu===j5B%˿v;V˅bass&&&$$!Hpe^~eRI{aX|+!M`J$X֧> >=== &ϻdqq`0ȋ/HZT*f BbV0LLL055EV?RĩSX]]mwkm6zjjj~6zMK_ B!2i]bKWj2dggP($jl6۷?cHWW9ZV&''bبQ8ޓ>(RL&C>gbbl6V}*-W,rq=B?&LRdbb^z%2 2 IDAT ^K`0)֖ÁdCa6ŀu~==-Ie1LMM1::J0$5 <T/Ȉ`U“*?GJ: :u7o J‰'=zHp|{{1==?0kkkbXVߓĬJ߹sL&#^F#(l›oɓ'X,k$h42::<ȑ#TU1L-. -f}8ڵkR)b*i-O" 144D\vKJ˅VPQƩZZwfSG풼V~kҫʄm4t:V+t|>/k?|j4 *&IK ATJE1Bh4/?P \Uʼl64 I Z-鶭b*Y_RvhUOUYB9+IUIBgc2g>Yu>Fl6$U:~]t:dV.X,f e;NH߭γjX,ʟSvV󱽽bknww!\.0LL&\.BA* OjCk4$pF!LJU½{rq0XV}xgp$I>,J OxիkΝ;yv;xL&Ç6M6v8}4_5TǏ,XL6qzzzq6vwwqx\L=Ǐ7dggh4J,T*Q*XYYp8Ԕ (n[R6wo*-\._tffFBrk  Q,I&  O9<9NSL7oru{9JZv;~7lnnr7䥗^\8'f2 sR ={?'{{{:tG"I% 8l6ِd2vҥK Npt:$I CCCܺuZF,RJ#I˄B!:vU^y 4MNt~GV":.tt*;#ƃd⫯b``L&C:h4.)4e7MFGGYYYarrRQ'[GaaaNfffF^ƒl68TJLxsbiA_t˿hӟ@ f?9.\ ɈYǥYZKFnV+=R vYZZb_"6 ׮]c||ښ C2 h4b[[[|GtuuI'j4EѰ:@@QvwwfDH$ݕīJ7M\.*Ĺ`t|*Ju AZh~Pg2eWUW%TbX kT*It:Ik|>/C J\.z]L=WVXf՗UStUIPU:V+hjwwL>ĭT*ttt1Z-AD2Wz=V F#_e+B9[VIl?VNe4zNn4/edjRUX䝝fUW=l5UHRxbV*9j)L<-:;;ŸW?hp8rpN>gss'Oru:$Vh4JZeww~鴐D B 1Lnn,E122B"> 2??/(/Doo/sssJ%{dbddmN' J%8]]]\z˗/zFF岜l6KDV|'< =v]# qr|>d2t;B!FFFcnnnbz]>ܹ!םdbjj|;V#Do[_'zFo[mV[mV[mV[jmol+ &I\< Moo/XL&l?Z tccCZ&ׯKbb 233}*KKKx^Μ9Z^"b>-,,`۹|D:D"wE^|EIt:2 Z@ @¥KjcF####,..JT*?^z%XT* rccCF Қ| ׮]^xʔ`O?VE8LnaTZL& 377GP@0>>2'N7|r}b{{{#Zըjzb||wd2q RtqTJLJlF!8D"bllLj,N4 pahhH$":O}wqBj/^x/h4b |>f|>O  H`4D"2̌ 4 }Y1_S(8r䈤7].:1|>bl~VWW)d\m/399>[[[p%<=+++.Z-餛abbNbf~s155^S*z$P,cۙhp8j5vvvX,z֤wvv!6׋VnD믉D"ttt`XXYYAb6b4 jIzzz8v|W/1A677O>axxF*`0l6p8uK/jX\\dddrL"^ΊW/~F? pIX$ 0*:p=߿ W۷ q666X^^0v |G>}p8LOOlmmNx^2 zrL0믿O>{$=4\^,$N'|򉠁v;.`04[[[ s}Asss:uJТjxl6IӄB!VbH  HWB믳ŋ>3Z^/8NI[T\\Ԧ2>h4WE1z X,t:J^/^{5P=88g?b0V*&r*b~~e^y 2___gyyd2IGGLNN/ɾeYrygg3r9tZb^'r 3Xu+^ѣGbf)R&FAP2\Ug;HZIZ@ҍ(V8_e)sLNSKvtt l6KW2SfBDW*UZWaU`c4 okt~ft!Vha}ҼWh)Sc~V'SO~P&hbqVU+̶JAll5 u5*EI_ "C mަ^d0L <|fnE*BIX988իfv: K^z%߿O^SB&ׯ_vce`C  _~m6NU&&&(yj.UF6ի;vp8&6.~?T\.'qoCCC\r_~YJ^/Ca:?^_οW… t:L&jرct:$5'4 , &VNŋm_Kmt3mjjj]mߴ~WTUBzI)Q%Z;;;F\.ɤUj l6ynǏfc? PJt:nNGZexxX6'&&H&<~p8,+++ /$Ǐl6366F8N?twws R7j w===nnݺĄ(J I_LOMM y&.\r ^իj) tvv\ n2kn!4 h1RܹرcGK/Ij^sy"׮]ȑ#ҥ yk0;vM666(;Fl+unnN:ǏgwwWLd2ngdd1?fllL0al6+&JjZرc,,,N9}4[ w#Y__t+8_]855  (rшnp8,f#rpp L,ceew}|X,͛7i6b1z=BA*QvIjGs Jtr >crNx<$ԦqgϞ~wappAR.Vdvv.Ib{e\~]0 m0xB|gb2* s__d#GHgq⭷"pQIچafq}VWWcX00??Y^^v)Iqzz.Zh4*f t2nH$Ғ ~j*5l$ÅBAp olX$5AXrIR\*X*Ws*Zub+`0?NcZ/znRY*QjT*UQTd UB _5hg㑞eF#\.'OdRHH\.R*o8vjm'NcccSVqzjHDko҇Rwavv 333A6eppO>DT" M"s}O2dffF_ (4իAI|>Z-ݴZ-~N;RZD"A\O[[[~.*>g2왙yjO>mkm6zjjj~6zMKOٔ+WŞB<*lb3O2NCVKHtȈ$U4NdnnNPXVn޼VGoN#ߦ4< EFGG/~AOOt莍K^\.Sfo[?9~8@Ǐs}:;;%9jX( rzjhp=4 l6- j5FGGrv᭮b2d2\pQ~-J̠X, HR"T +++R)zzz{.;;;p)$fgϲ%B/juA_jÇvAٳh4Ξ=K"Ry!VW>ZF<'N95::7|ǏKܜSR)FFFQyY96r\.$ CCCܾ}.z{{%q~-I*C?`0;jZ) v8D"1<###j1116;;;8oӧOH$e~~Bh$1>>N `mm ۍ L_>Μ9tGrx!h4_gbbB/sssb1BW\ʕ+ɟ Vnr,}kZ$ &@@ 0GOoo/FD"ATb``!ݛ&MZ 9$I9{,NR`Cr IDAT7oޤRp)(D^:::xq:18BVRg0z`ժuzi0ufD"A>Q(QFBgY1$Shp"kvJrޞdpVRX,8I=ٳk2(zf)$]tUpGGtʪ]URƥNl6P.1t{sQVÀo^o4VlJo1 4)SVu*SjJJ5^aE(T*N';;;vI_+h`0`V&J9+YťRI0Vd2)k 2UW4UM2qkXX]]ԯ2}C[[[|>gV K|>/k׮/GVVVX\\2$ȵRchhN>|l6ˠzX]]S0zʵkפ<PX,l6y!}}}8)WiH$ ~g}V>&''T*b///Dd@S }pp hiKPtNV阞A0P(^_HAբT*L&=]VlFi6D"N'>O[\.nt:{カ֯KmjjmWm7-ekT*ÇtT}_Rq]A^J'|BwwtU*xpp rÇ}6ӧO~: ԩSf$xe~_pIt\z??m6aggIdbb2\xS8qdb(:nݒ^er4M]ad}}z0L\x>Nڪ*jk׮Z駟 }6J%vwwh4d2VVV_244|pI~?Oufvwwܤhfgg]VWWcRDOD"ܾ}[^/z}6>M, .\`vv-1-:DX}lg2";vt:MbԩSIO+ :;wtH$F8:;;e{iimX]]T^g``!ܹ#={{{C 2anne=j^P(D^4Nt\v eAz///Dr d2F'Ɉ;gg\tcǎh4N׮.dZF&ɓߧfr̹sH$TUb{O& lǏ'˱8Vnj|>޽To0F榜yB-vwwDQ2ժ[[[TUuVKR@766Q,~rW)E`Pʕ+x^9B8bQzZ!sYkvv]ʜbȺ\.1FiL&WVIRSOFl6+fB [,2 n["t:VUҚʘUjR(ZkD"Uded)b *Ĵ2TێI*$zu AΥJ+$tRZ&[@P^UZVR*)αJX5d6q:O*PVD=z|TJ\tz.nRakkK ˬ3͂VIaYh *Qv)JL&n7&I4:,--为ަb(5 6uU}kv뒸|b&FazzѣGF RIUwB/--qe B$ eC8NH$\/, hbH&lJAww7{{{یHjra*CTG  jCŨjr9E*|n...G47HrHB].Rѐ4OӶ֯__|EmjjjNmߴGGnqf3VnNSΝrIjqq.~, R?ÇK?[:fxx$d^{MRz*Xj&;wN*8117z 6?].sss\.:jcM:_q-;ŵ*$ٲ,rO3v{2h$ @ $߃: `;wvK"ɲ%[dT^,wB`|VF.# H$3t:9::[MN@]Vn'=3.>$R,J%qEi-Jx^ʛ͛x^2x<q:٬gZ,r1;;% I}R]]]LMMqmvvvRooY[[.*O^ӧOcXzM^z%|>l6~Je>vczz_2<sʍF@ZF^Z*]]73Nu=kZz{{y8dc4|^z. J%ĕzpp }ߥR χ?fpp*P]uE+K:`0X\\IϻY\\cH$|tuu$IB [nq JE\.y9tH$BѐA."kkkD"z=8Nv;O>w^tbX(ˬ088(ޅz{{yĨ2>>榸wG5bd2\z zm޶jjjm@C|>& ~3rx<f3׮]Cptnllk1JfoZEדJ ?1_K\.L5y L&~_&&&d3;cPmz(QS.ܹ#8888xjFqB!vww)˲9??^gvvhT_K<T~l6J)];;;X,$ZbZFx<?~, | &tZHzL&C2$KdRK.qio6L={sϞ=#5IX'ڴ͛8NΝ;'P"H2ǣNXt R]] Er7rJ笊5L h4x ^W6 *H9}4^u\.1z^:bUlnnzT*r9Z-}}}y$lllnfI|?l6SbP,@W`Z`0HVb+W_%Hpppɓ'ggg???Hlww7SSSJ% fYi3L&/2loo+ʕ+>}t:-Ig'{qE8Ϟ=fmm @<Ν;bX.+++l6<KKKcby)J=l69ڵk\pĜF waddwr}N>MR! \S'L&I# KP`bb}q*vAaCȀUU&Ubl6+qlnnz# Bߏl&sxxHVXe$ZuҖJ%l6 VK*\9yF\]UWATͫ`JP WuFH@rXhN'C4V9\Uol6WATYzr&+PX9Uc>ߝ^`rXVUy> *(2d`r9u:~j@rIx*b^ΩFeHcmmMTV0mm'Nn,򫯾;wdgssNdNkF#r[naٰZy8id2 $ÇB!}N:%}###e9'O|bHբF/~ D2dooQt8Abh4qt:IRSVؐXML&hR 'OkA.-߿ORwVxlّCh4XVΟ?h$Jj8::bvvV*BR088 v]ĉb{)?@XdllL3 333?&͊;Ν;b15L&阝V.W_}UN'n|>OP`||V*n<sss|LMMq5N>MZ%9y$$It6M`B8믿j!ЭR)*ncf3NT*Q*oooc0$&62ӧYZZd2+?.ޝEDꫯr2QTpSpGu1yg*vΎuYʝZPPC NVjdggFޞ8eQ]:Nj[ f,?gh4h4ۨn{tt$'/s[B ?f)=< ш.Rl_Fe!0uUardhP(*'GP0<99ennf2Bx?}{rz{{r F~FQ$ycvvǏ+`q88N!?r:V;v<aI?88HPvtJEbSzχb͛;wN:~ڠJfڠjjj]σf&z\bb}>zC2oDFǏyw( A@@6Նm>giioMOO\d2)-娽p;;;$Iq-//388HV\.h4XXX <===<{Lb- ;;;P(P9wvt:M8櫯Ν;W$hH(TD"L"lmmp8hjJX V1JE7*|;annNޛ"͒nX}6###Z-qrZ-q:Nvww*ǧ 33SΜ9ٳg&znܸėfY@<a\OOOcZF YZZ" ꫯ(Jܺu~}W_eaa}bpX `oo7n`XczzzV^uR| ϟx%3~?]]]ܺuKEٔ qz=l6666^+\N6o?OdsSժSNa2M?f6668>>H$D",--N) x^Pl'OJlk<jQ*裏x"gϞ%Jq}^}U={10;;^g``c._̅ M\&c2xD r9,6 4fg꒎h'000gvv\.ȈL7MhZ|嗜8q ]]]j5Z\Lgg'J%ٵZ[nqYVr( ҁ€@eTaxUi@_VZcUԵjB'X,/B ,@nc4%\>ooot:Ž*BKR*^Wbu:oUK6hN';u^:::j|>q$lmm1==˗ul68*l<V@dx<.br}C"~r }$qIᰬX,FVÇ8NzzzT* SC[ A {UW5'(J/ĵbh6XVq.WUIjT* HwGGnݒ.sbsSSSeֈbD"S)#NK߽`p8(JdYj޶~kA/mV[mV[mV[mAoOO@*z?DŽB!F#nۍqM IDATfxBPz=_~sbQBO>%JQVYZZ?r9\.f=BB7n0::N  q.gƝ;w8<

JOr8?DVbssl6gq9 kkkܼywyEΝ; H!RIy7D"loosUΞ=+qFE\&S,gii IZ'? hp8LXDq d2 _sttD<XAUrCCCx^_'h!^/T ٳgoorI~!wi4ѩ2/]D,m0.FQVtvvDX__磏>" M$ܺu!vGGG\z}*V W_}``llLbq}###J%8::7ud2t:hV.UrЙfP*\駟r9pEj) 8N6d2Iww7ryw*=@zLUr`rj>jtttHez^<{VS*hჃ^D+tVGU[=OGG$(IPzN5hWݪfY ꘨[fj^WX\LлOGGAjp^UU.KགmXdh`xVZHgn[m^j@%v߫*`Tcr *Mu*fSH*`0H\Kwjd𥫫K˂ ###d2qZ-X5>/].ĩ_z@ pHųgϸ{.z 666xrHWj޶~+A/mV[mV[mV[mAlG7)z?Gqonn{ IRLOOc6eZ9bDE~399I__lzKVK>'Dx)h4N'wazz7nl6T*l6Ngii @nwwVUc ˭-y1V O^g}}P zɄfp8dŋfB=udR\LF(yf#l6&ǏD$V5 bZT*d2b|>) )Sոph-L&}}}t,--_gr= /\lnn SD"u/j#ZWWbQI `wwW5iZn7z]B H|o\H~gKDa+/~Y!FC&IbUdv]ֱYo;VjZĹއjX긩ϝC~>Y9iVC=fYt$c[ٔX]{(T \f5pR諠}m*s!P}^v9|tt$.ݎ \(JYESs"P3  n^V TovS,|cf>W0O>ejj B"vT*x^=zf#Ė÷ =S*$`0(i!J%YYY*t ֭[ b6p* ZV[(^n|2r>}%.\:rN˸\.YϥRgϞcſd2vy d2)TE(.Ao[/^{o޼mV[mV[mV[mߩZ ^xI TNO>7|/3==󌎎Jol&~uΜ9,~i>gΜ͛zbL&C$ĉ{tt@p8L*G1;;w]"t͆jeii ͆cyyUx>Γ'Oā/H$B\ݻ={Vō7$p8Z2>>NcyyY\?~X,F\V&NYvvvU6YTHR4ML&7odrrsI01åKd2.һj0XYY! 1::%O>ᣏ>"jo<|P\oD"A  NH&6zXVYhZv\xu4h^&Q\hO>X,rm&&&.wW^A`0A8??P(IRyj\^^fff20ᥗ^f"k믿&JaZJą],(xP`ccl6K^gaa333e* 'O̙3Q8s rU7  >#lT*teJ%'2>>NVc{{,ZGjgmm=Yd}buuRA+T*YLv8X,l4LLLP,y>b;;;r[jx<|t\J%HPH-vL^r>(cJfa4h4DӉ]># X,jQ9N{C6h4d`^^LN&zURHTFy=[U1n/DK+( ZV"EAd2X,,ZouV.RDgg7C:v0VC\iDVW*\Aj=:ܕYJ;X9::" IǽrЪe5tyh4l6Y3:Nܛ +g:fSfGGb1DQn߾-N>IR\.2 B .AZkkkXVYYYa>| C7o2044$ussSTn7.$ RS166ӧOr Νp8$aa4fVVVrw1l۷o/px<R+lN&''[\%8j5!#I8y$@B===<}TzUjH|>/Ru^mVvAo[mV[mV[mV[jG7?ũ"?3?',../n=4LN /~j8N^}U~pm~H$qOӧd2bSt:-y8g].^=qC>.I i4000@"Jz TZF<p:є(r̤R)*NWE> aY[[F?~̟ɟS ɻbaaSNQTjw3V89::"LrM(SSSyzU7+WhZ P.yISդGO>^zI\r`0(qXqFFFܔ~LTɓ'~@*bcc .pM \~]N>-Mfyw9DvuuCGG. & Ԩngkk FC8fmm Áenn+tvvJr&W_|>O:vxx%6zbb5N'N#(333 2>>ΣG\sp8̇~@  ~!'Ov388HXth4zܻw|;#ϋVqM6]FFFT*loosppE5,//Mww7###i2ccc%}ooR9(H?tbllnonJ"C%  z' Z h4Dh4 smBeݻĄ@MG Nr(ȐF6SV)lZHowٔZ@zߪXV+ZTEyZ-6M\J"jxp3n `h4J/*YEb*FN'Q}W9 rcm`&WpQu* ]Vߗ@VQ2UGU0`@r+;ɹ8::cz+ s?^ձUP_JGޞ Duy*l6+UE+ֆr-Jl6z{{)XVq#÷@r i4Aq+p8d2rZ2TSG״,//#T[,RDNF#hÁN# K4nK7^jR,z& joY6668q͡[>cXEG[,N8tH__y\.G0`uu{qY?fqqDTBiZ)C}^@l60zWeh zAo[mV[mV[mmpjD"L&"ҽ"KGkb7Z|_~Y\4F>q:T*^ʗ_~?mF .Uo\Xdjj5z{{^_ht:M<gwwzNRRx"rBfɓ'xJJ888 L&%BKB\n裏6>}ZMhZ _wD"y fQ(0 z"Xk׮DWWPH6u=z[oE.p022;wĥىlb6v3::J6cjdYl6/^ETF"Ѩ@}{dYq*,--D0,,,`6yccc|DQ4 ͪjappN) tww3==M$aaaz8ݤR)OFy^ۿess .Bײyt:y" Su/j5n#ULLLPTBvr~J\ҥKAN>˗%\rxii g}k&nܔf&gϢjX,~El66 JhP.1 \v`0H&Ir`}}+W033Ve~~tvvRgΜ.i}H ⦬jg?cjjb u:ܹs^{ Z4M,Oh$fq||իW֖qZ- X[[#je ```ׯp8$N^!bP(ijgψD"D"Rm4 (ZV CIӮqgg u(hU ,W*z}U=}k2Z ^J"}*]+Ȩ 4[,q*w V*\.* :NLV{UZFxUVվС u"k[h4$A3^ j P6 n6UFzrsWdf8Ujrlrl6ZMQF"U\Z^zhVWW' ɹ2aXp`0X]]effF@lFq]DT X,zY__đ=z=Ϟ=cxxf)F:R#ǥs=LCX kZbCCC$ ye]iZhzN6Ec0jtW=*QB]j%l|>Z 2??| ^uoW^yE8f~~%FTR|~^$E$[9ǍF#n[LV[mV[mV[mZ G?_b! (:88X,1dz*p8$^Vq84 bb8{{{tuuɦI`2sM Nstto_|N^a||C0 n<g":33#<~XV\qppT*Q*vjcood2I jqmljD",//sy4 [[[ttt3J[heeVE,cssUϙW^[\\p˗/h4XZZi&tMqSSShh4JGG|ᇌ%Ǹ\.>|?S, <}L&#}b1* w'pnݺٳg) xZ/,L1 |^z3@b(pd2`p^~|ܹs$I|>UN8A,cwwT*믿`nnnn>跿4}P<>Y]]פSDL&Ûo4KKK\xfI.col6c6I$ܿGڟX,b4vkkk!KKKɻFWWn߿$Hp||Lwwaz) TU]]]/ D>y$ij IDAT\!<W\! 8<<$իWd2ajAΟ?Ͻ{m+whD$2V_O8ur9 w7Ç!_h4*CCC$I`0077'pXzfͦĿ+d00,..0AV#L0LZPO⡍F#}qz*PV%U=V^U=FCk^>|^UJ ^DFQzeձV(VRY%zU8<XABXVX"Uz.5HPTYl4988>]ZVS=? 45ǵZMz}kQ R)q w檮T՛ztt$1owu΍F#:|>/]ӯ8<<^/g5v1Ld2q:dYJEzKzWD.Kdbii ɄbT*I4Vʕ+:u 6322>jP(Dgg'+++|={#vwwakk|>O^v/KD8FR,exF;5ꫯz!NT'r6JBTĉҥjT*|>{=تs}dd=b~:N Wq* fS&FQvwwryJoo?y@TANfff8<<^۷otR[ zi޶jjjw]K޿K/%ˑdhZX,j\|RD A.Ѩl^qd2~tX,|N%nYm.//L&9qDԔlTFQfggD"twwjOדH$888@sKΝ;GdwwA 2==-0bH*"000.N >  Y^^C2֭[nv;Z*?O8q->}w]4|^w><& ~3 ===y7gddJ*rJ^$qt:DB3~?f1bI$;wa٬noo#NMF,^}ww}\.VD"!񠀬r>zp݌- ( x$ZXEb1I&K!be4vb?{|ee˅햎a{z=Hp8, zkkX,F:Fq"XVb*>spp餯FGGeCW^d2L&YZZbxxL&CTd2IjTl6ta0o,SX{4QFzsUV+FyW]\__ =`0CU%4e=D@ Xd2)`Zv SA0u/P.`!R*gr6M\+g&WR(er$nYj* Ԫޫ\&na;::c]9S \(JlPq *V^zUMZ>ˉC$h4tJ*?Htuul6p8@ qŪX,FP DX__z_[V) |g vܔ|>/5jL׳tѨ { 77vww)$Ib8bpyxיdwwMEՊcjg,'OfqUn7hC(J{mo6 zjjj~Վn_5¿7GyױZ_sR).^(_@k?V%1J%t:ܚL&Dj4B7ofI/Kz{{888`0O~Ξ=uC$ ~?]nܸjbZI&011Fjeee,L&X,̩S.xjezzaXT" o8q>|0dm=*Ea2Uk0v333#I^b< Fߢ%566ƣ>*ƻ2rTbdR,//g  Hb"R[nL]]766trx^n߾M]]z^y:::x<,// `00==MRa߾}?l6Vdgg ~?:q:;;YZZ>E +hzzLKK X łfCBK?Z$)J~鴘~\ .^YYpHꊽv)n< I&M$8N8%J2 qunA*p&IJ& NG&w}}@?e)L^3Vx\ X%UOޖ}ŢZpfhZl6h3梁mUV*0*ak2UFB+J+Ey]%U!*Ѭq)^.LUxeuLB ҆# * FQ^h4U'U:\.cdBgRjWp5\j\.'veyf3ZV *j2(Jb1z=DBj@'J:\f!iZ. El`V׼zMI' ][[:QN<ɍ7_L&lv~PMW*C: R*H&e666Btwwc4fX$b}]G*A|>/p\eqΟ?c=F]]3l6&_ZN$ҹDڵkkvn7\+WPVP(0>>q:tttf?sv;>g-cttTz PצNp0;;˙3gjFoM?^5jjjj<:{,X۷oOO[[466o7||[⥗^G? ĭ^z%N>Jxۡ&rabbd2) 3Pj!e۬3<`0ȫezzM<>O?E,// 3 |A8t#&11z=NNU^[[7nJ艹љ:::`yyK0S__ϱcǤ2r%ɰgn7Xd2IKK =niVVVjD"؟H$ؿ?ofhh]|>:t. %V, H&^+WP(tuua۹sgϞstL&O~Bkk+{%'pae+bP,b/fرcTUhllcPࡇNNŅ 0͂. ~.^i&xinn&Lq\En7rP(ݻL&h4(c8sL ƩJBޟ(MRb:[eB 1sUV!T-l6I`0(TU1UgR?mz[o"|&"WqtVԊJ"Xፍ A*ڿjUJ׋~zXuj&\B{WUNĶV%N Ya+ v,r|>n`U~Wr,g5rYZfR|^Ϊ]%ӯ]ܜ<R\. hllRBss3DB 뼸(]:::,//-7eTV* ǎ̙3B!h4K}D\ƍ|K_tv455yU'^t~[YYU1Ue믿1==M6;v{1==MWWz^j{OS<GeeeEv;'O$2~7ByꞠ1 DoM?~މޚ[SM5TSM5TSM5T[5t?={~iIm~W~g}{ ǩS_/|VΜ9'Eww7MMM!;#ɥC ellxk׮q5{1~DOOls裏2gh4pU>L,IP@ՒdXXXnS,X,122 ɫŬp׮]W_EKj}}j$m6kkk\.Ay&͸n`{{, 'HL&A LOO@kk+[]]sԄjvbDQv;@??~᭭-#ZܺuK^zNKSSsWU>c<.K.zv횤kM&/OfggFvvvessS"W^B RH_rnN`nUߤ` ndd;wp1L&FG}Rxhhh`uu}IZ^)n7t&1*_팍Ig&… >}V/lll#LΞ={eJ󬬬0??OKK r9L&I[424 L&z-< U tDp8b%I, +++r_S*D0LhZb(fBߏfXVqUcjUr.jUҝVu*)=*!\VŌTcTFI*U yLh ]j@*zFAfW kWB@sUݤʜUAP(ĸU|>/G/|>cX%UWUzF}R eLۺ:1M&[[[rFp?qxަT* y{{~~e[,4n[eV|2VU*d(JC sqnn 0.q5dammM+]]]bƫa15MMMIK/V"@Fr^!EArIC Ν;r9FGGyꩧxѣx  d8~8rׯt:y׾Fgg'\xJjweeEE46.--ꫯ244$BL&)Pu|AgR-K譩jjj]5t?={7xgygyF2ŋyyg嘹\.yZ[[9~8WE2駟t:|+_ ^} lV{c< GѨ` XVI8h4h4:;;I&z*yIevvvH$xx Z[[q݌<k2XXXfffD">}*fի9ryrU ]]]1??Ͻ{󱱱!IЍ , x{ǎCzilll6ӧbLOOS(8x$SO>aIR|>^yzqŬ$NL&ùsطoXL^Lҽ{쬜[* h4ٳ|>F199)C O,|ŋ,ljjƍ>|l6*J͛7b p8hiiAvsSSS8qy;Vl63;;SO=%}vwwż+\pv^uz$'|l驒*v),--<XVfIR=H`8x ^yL&~wyG gϞѣavwwOwnngϲo>, wޥ&V"ҵ6 ^O?P N'ׯr:VI!r9  $A IDATjUNam7661feF#RII tUhb")Vե HUlhhTaM&$UJ`f20L vvvUUVS^F#ʬV=cij~r*>RƩL&l\%9 s\.ZV:sտq8V^cߛJ$!~]] Ԩs6czTR4]__l69_(rL&AW__Oss|;"(\w&Ez|>pX, mmmX,;Vn'HЀjZr9iooZʽd2L&YXXjY__l6ˁH&y&&&6LH[[Xz2 wСCb̫-q^LF wf133t:[aoܸ!Nڢ[&l6 \)IKKlOwwRn]N8!p>#|>~_Y׮]dV޻w/FQOnܸ!avww\3zkl݋wjjjj?F׮]`0CoFM?'=z^xwy??caaY7 v`cc?|_}#VU`zzF޽[oE[[---{,Jx<Z-7oh4p8r]:[Cx?ӧOSWWʊt)񰳳ϟ''M"`||H$accg}d2$ZVҔaVWW|hZYU~>(Fy't߾},//sM#.j*Aŋ]A7t:nݺE8G>}fffv裏|5ٷ*yۋn_~>N:fllǏ˶t:ZZZX\\ }].\@[[pB333p8bzzD"y Q*T*.J¥K`޽\rE(eƷq9N8Akk[r90. Պ`~~۷oߏ!s1>֖$Rr>!H$a޽,--<[[[tuuQ.{.+++ Dkk+}}}pt>䓼+H7FP( RT*㑾Ꞟ}]VWWx<ҧ|ErBh4' r), ǽ{b@2٭APrZr X{B×J%-n:N4l~]PVJ+cZߟ6lnn"n,V:1RH V%'@ @.#HP__wvvfssNǥK_Ppf\.`Y[[#Jj\rEC{{;l6IDb1Ǿ}Fؘt,v"X,,zPT*r:^b'N|JzFx<N::p7o~ X]]<ngnn%N>?yVWWRI&2dJ^Ͻ{d@ l6 644P(RQSM5}V5jjjP'Nބ~׿uyGxI&G?7FLNNoX^qv;XVIEP($)Ư~\tIaFGG7A&!r!z*:V$r "> nP(DGGEV+ַ:tP($CѸ.]nݢT*D8fŕ+Wx7c4v NE*bvv('r9A755nǏ$޿?kkkM>'LJb6H1Lܾ}}q1VWW|2$o~ݎ`qqzzzsr͋/qbtvv+iN'Ilii/}Kb)t8;;;aYYYC_'L<=CCC_t*?\xqV+ "$T;;;X,<?3inllL7OaeeT*Ņ XYY!3<<˗Y]]&"rիW DQA[6\ #j-:::hhhBOS^z%& 癟cX{U'044FҥK|_K.t+W |_T**(էzk׮L&èbH(f /+Xo~f"AI577J%VWWԿuFb+̭ny&ajjJ瓓\~'|R lZYXX`ggyZ[[IR u:22"gaܤ'tէ}}}H׷J֪Bece)1QT$Y%)؈` ˑf rުx#S]jFayLl6+ T@}mZt#TJ*/ Y^{5BzKk9x ~&]TU<rY1*8|>BARIz^ir%?p=% K$aff~N,jf1~岼:*b|ƴnS.h4s( tuufrbpIn1î]'|BOOD-1::bƍқi6w\uvwwsAGwuu111 / Ntܽ{@L^{ӧOs1Ν;^_*1O=dYopAN:E$accHBf2"p:N؅$"TUN{L&d2ĵZt:yddb(=ͼFN'@:LMMhshz\)ǴnVVVd}}^ ~9|>Fa41&h1D"\BKK LLL01126b_Uv,,,`ʹs8y$`qM5RjjjDw^vP(r2 ;x /_A+{Z[[|{_! ?9ͩS(lnn rX,J=zTgUjrt:toooS*rr9A'*n*"ҒMMMIZkss5~:===jU. ;;;tvvoܹs(lmmގj`0PTg\XXn㏓T*bz%V.y衇b q* h4JT" ,dY"JQWW']~>NٷoyVVV8y$HLCC:]B_NOOKzh4ߏh?D  a0(˴JX\\*fZCCKKK 4 ,,,.--C`0L&iii9\.M>$Hj[%a6Z477.>"BIRWrewwX,& t:M8aX]]E222KKK,_FqŢtE:NA:")jioo'c0I7C6R K]]~H$"T*%hreL)̫Jo*#PyU"Uf=NRDžBAzS$Z5x<{nZ%ɩ0SݺʄT _ս+㝝N$wՠB*Su*԰JRez=DBWe2+ltb[,$[T$T__/+el*߭r9V Zeߟ"tr Ok4$B:F9dD8VB%u@zl6K4vҘ*Z kkkV4*WN~*mZlAI\.)fy(BKK leZZZRr_Y^^ Á̽{# vdYhB!H,..b FGG֭[W\_2ܻw[nLss3f( duM 8p<.]nWWr9L$r9Ξ=KSS\Z&X,l6w5dFZZZh4BGijj`0H$d2ZGoM?~?XGo譩jjjFo(ze!yrro}[D"p8X__OO&H?'ttt/388ȭ[ͷ-y䑿v(СC Ijii #O(n@ ϑ#G(˄aq1Vك^g}}]R@v|>W^W^ȑ#3::Jkk+tZ%[ZZx8N46VV=zQ|E666]j0b>3b창# 4H0==MCChBn'bZ1LbrT^O.… 9rtq:JT*`kk>zzzOJTB f6I677cۙJ) qQfff0D"|H& |2i,r))J8Nn7[[[\~^H$hf<#2QWW'h69ơPFpǎwR!H~yyZ-p^Ϟ={0L\rEmnnwo~B  lV{4 d2C*BFh4R)|M{l6Μ9S3zk|׌ޚjjj޺UW5_WJooX>}E~~{a/Rz5 =o6{Ǜo7M@q.RF׾5qVVVXXXl6(o-I&y&6MvsV* & #磏>A&!322+"Vݎbwapp)&Ι3ghiia޽R)hhhjTϳg$\.tvhoo'J+c:BCR?l6S,GR__&^ {%t: BI?6<׮]zAY|>vvvVY]]ecc^ϫʯz2@"=9|0?9O(R w_~~E(OOxfٳgq;vl6+ U/Ok#bv۹u#J9sAo={`۹q |ɓp51h666p\AOOx))0u=I$b$ \Z~,JeF#gΜ}WW v_{5n7^_T*ŋ/АUrvvVp*ͬhess|>Zrr{b" tuuq%nܸO>UʈUש=uNޖdJRI2djjJCrT ɄdtR,ŴU`^YY42~UTBY9UwlZJ"f b1I "tB6CU+BanYbrBVH˪wV}B@}}̉D&V}j*߷lT*IkRվĬ, ҝh4 ]=TrXj{mSWۦҭ`S" ^^t8Ţt a1v(<2f3tZLy5`B ZH$"X,&Xu5eX,dhkk#$.d2uUjE07^ |Xŋ-ff3r)&: }}},--q vIRB!"|I yjل+dVt\.'(xՙcH$tttx,,,HJ?dnnǏKWzGG<&&&hhh0E/(D (fggX,Y% B(D"$I9uK5IFoo+Wꭩjjj_9sG~kW\f:5huu@ oIKK .]I8& t:|2v$ͮ.1n1fggqLNNo> o&G\ng||!XE Bv;|-, W^E244Dww$|>o*> x 1F%ݻC=$2 !IR0gv;X[[[loov{.?0f??'daa~E114 o6k~~mTi-&ǎ#HLX?ϱN^0??ftd8<?0np8FJuNs TW©SPVR؁zh40;;\.bp|H$B?;C6vwwaqMLMMd2a~~tVWWwE\pz=wZ,\.mFx"L__* GFFP*p[.q ?@P>0R ~!o@*jxpy`rrJbV j݄KKK *\.* JP*unܽ{{6WWWQ׹y~~nvϟ? J9<cxxH$0LxV~d#t~ə-J*`70vlDz ;uc*d2GS$tAǒ|ssZ#ɵNR떢F#2 2' /SBRa'98+R@Vs9R4sZ7n7rz<|V O&zNH$tjÁjL&s(2 /!iH&wXZZ¥Kx*R opQڭjp80LdD"\@rc D"GB̙3D"Oyȃ#rVϞ=Coo/~?:!`XS `0X,`ӧOh4j9=nsx(T*e7D"A,B@ GVonn G";{|>^|+H A $H AEG/Ip ]o ^j>X,ȑ# <:ht:lllp? 9h|$<í-x^_5F`kcV+t:`dd>J‘#Gxd20C\ƃ0<<̰O0R)ˊ6E)PPZBTB,#C$axxq===X,* n7$$ d{@ 7~+˼{Q?F0D\FP42ְܹ,ԩSXXX`fC^g1== ш'OP(T*p[f2H>V+b1,  vV!Nc||T bnO.Ͻ=\.lmmarrX T 'Ovπl6coo.8ƛbN8ÁYB!r"ܹsDP*hpF JZN>r9 |>666f ׋\.B $c<G\.#q,, v; vۼ/[oɓD"__Z"yww===MN98Jh4BjRpg__!QVa2j099=L&}WbbeeJ===tVWWqm-'ŋvH&Kp]h& A"ٳgG$A"@\H\d) zpfbh6t( \\\F8$R)R)`X8wooRL1Ar w:vNSo. (%'`t} F$'.v 52w@huj!@&q+ݣfAJJ$%pKQd s Fu]v"k4FiDMR|ɍKIOjT.Xz^:t(JW( Xixby#K1/jp9wf. U*( vh4f&pT|MihӧpF?٬Qבf}g2sOwP`NsyyZ|{/A}\)p( (J@&A>^$:bt:O%9_v x< L&9EenL&hZׯ_ӧOoBP ^h Zsh͛8}4G\ eFHR,//&T*E6E$a&|>j5R0>>Ʉcǎa{{ΝC(24 rɑjf~R7\ɝvH$xw{{ ׿݁N"P(ĽǎC*ǏVڂfn, N8RGV.r@ ξ~|ZX__gH#H188TUz|ǰlP*P*y=ܻw zZ-4MLNNbssǏ3@t:P*( ׇjfRdH ܸqZ{oܸqXVl6vSm?Z0666^9|0_wΝ;BD$`@&A(&Μ9@0j6 VL&vfbbRcɓ'r <|>TJ%;Ch`aarxWQ,H$#8!N#Hh6@,H$ѣGQ,*߻wF^ӧfQ*rI<~Q4 6 6 T Ϟ=Ù3gx|>JχJ-sb AT*ǛkZj488q.EvQVL&l6aZT*fa0P(2B2 J| Nv3VjʝNCd2153 T'ϳ2pJ_n!+Ef;|hpO0E7b~/_t EL=6b1'2둋D `jB>#5xRp3v\kF;X$Aq0 \Ev|-~hbvzT*H$j%NGKݻtt:RD*N n N'*\..n7;z}}`z\.LOO#s<4rr9\.#Nd2amm 3 ^j5~?޽rZ<aٰ HxBP!`wwv?6 * b###D"X[[0ưl6r TF)9^CTɓ'Zt:~:PRİvz]G7 W A $H A%^Aow Dݎ[nakk ǏZ:;l6|>>s-0 __R===XZZZ-"&;9NqID" :vDžZ- `mm * @o߆X,BZ&xS:ÁH$Nu\.󛛛hD8F2ŋaXʛV;Sv;0?bqӛp8  L&#L(X\\D"Hίjs&YꑍJ?9N:ʮ]H$ecpp;;;hrx<[* h8v~?* gϞtҥKx;P*022n#G'? v;P(d8{ff:7o*n7677 χt:x<0i~~gϞ2j5 :TU$ ~~vit:@>n߾xZ ^ԙ8NH$! xd2iHfT*… p8Ǒd'OÇqux^vw] ꫯ"lB"똚B:666044h4 BϟcxxM&677  =* 7n܀EVZfۋ@ _MFGbkk3[FJjxz3<>>Ju ĉrg:Q*`6hP,-JX,$I( vo \.edYfj5t]vfYlnnĉT*M{ez yt]&Ipccrx&`0n9;> |7Md2\Zɱ]*d2( r9Nd6+f)#xt:6{ bh4| ^|[K"P?.]Gz/bN[rFvPjt~_tR%inǾ耮T*|)(PD>]#HyP`39M* &9@ gT*zɦkDa=uk4D"$IL&^V+(>̮\Z- `۱RNIZ+ :`0L&X .ZxR0x< YTx_*۔`0ZƱHDX]]eF3bmm |CCCPTT*E bӉX,Ni\.$ jd2H$l6Q MHb1v;^{frw^}Ulnn"ܹs(JB>Zr-.."ͲSm6xQ.gɓr4&H$ fcpp">Vœ'O000X,J˗/ԩS*Z-޽}t:J%A"LB,vcuub`2 hnN333Jh4[+G?pa/2Ӄ> zCCCxoďO?`lHUFX:F&Rd#|>v4all [[[^ ~ٌUAvәfHR8Ny~͇ܹs0Fx!^~e0;;-{{{ܓ尾 $uІB!GV0v;p=zvM߽{Ol!T*ۍnER>DA(b L&QVQ`3S+qmv|>Fb1t]v^v 7| }K=F* RB;;;H8vOA`eeCCC;00rrQ rF]'Qa49t" L@ȵkpQ(J[rZVj5R)Hqgh4ʴ>Xf*jZk [T@"T*q+ T*h4v;D$l*4N.br _nkD"A6NkMΔ*@ 4$D_MNRJFr= ^|~Z? i%&(FC.`ꁤOzΎJBn[V *2 FFR\tѱS,4kiL1/:;W.sƋq:rbZ <$ `X7h4M  ^#LjùX,j~%blxfA l6d2R 8{,0vwwc.~?v9nj`0v3dljETBƍ7088neT*?~xzǎC.C:d a2D~z^YYR)t]ܼy7G  q1_ub}}}+跒  $H A $H+-]yq?#o#ѣG7on5vő4<< Z "P.166Wsd2aqqvr9o6 aqq{?)Z6MV+v;b(t:nݺRcjjMz=n޼]bW\ J%(Ξ=zpnb|%:00n085H$p\V~Q BP(ZZ0jX\\ѣG9wkk j& +++ŋCvKzHqppώ_f& RYߏ|>=?~7WVVd066X9,lmm!Ncttr}>[o`0࣏>fLD"H$Ӊn!Aob+W`jj nnjfKkӉt:TUE;v N K.>$Ph0::ʼn'jǏ 0gϞA! "?1, CfY<*?9֐H$faqq^vcb1[4X]]4:n7fff0??͑zZ |ϟbA\F>ٳg N͆> 6;N'wed2 B?v(zq'Of5HӸu&&&p}BR|vܿ|#:Dϟsl8 G뱵[`2矣\zXZZNd~;(˰ld2xV+\. <9ED,ɓ'8y$r9W#D"|yxxH}NO̙3H&jj ih4P(xpzV@/+H A $H A@߷9s.]b[o!N##ᥗ^:шT*|>T*Kxpb1x<X,yܿ|2χR޴dyva0L&bddAu:loo#nh4bnnjB2>$~?ofR^^Ǚ3gQ`6ZgϞE >Hv9Yڨ܄nGZETãGl6a177=wׯ_gjxhӟgΜa.^O?}}}tx!H0<< H]B!"0,/ P*x<(rzӧ:&R,|Q<r91RpnT*lmm!JlF4T*(`0{8~8: p}3 =66`ddlnsss|D>}B6E>ɠVŋH$B RÇQY_2:N:O>VdO<z=(| fffH$6ZߏzzN@ F|[߂lƓ'O@`H$8ySL0Ev ʕ+x7Q*J:NEft:i$ LLL@R!byyra:;;ˑ/{wwfX]]J^gW0"GRױH$D"T*E*nFaj&ХP(xȠ^CV#q7ZR )ҙc2l%XVDrV^gZבdH"'/J$̾6d:NĝNA%9_y&3EYKxNX,aZw *  M`J%\.NNt\rUNt.鵫*ǨSl6sZA ԓ,؍OZR\.Dr"S5>*c~?F#J%vqa !s4j5F#Ccl00 djA,R p,]nݹ'.L&qt:a122|>M(J,,,@,z{f>đ#GVh43P(8T9NVհX,D"<c6h8&Z.wb&X,<vׇH$p݂T*E$FAoo/4 w'Ij5\zU~+ $H A $H?t W[/:zu:\.Q,L&qYDQt:"gۋ|>t: =@Lc_7-rj4lool6cqqBFZeZVhUɷIfx^˿ N<ɠ6gffv92 wŪT*h4nckk NJ]Iuh4jp8qRL7fffP.wM&0 l6zzzȑ#t:jQױH$fǣGP[o!頿:/_kJŎ`0t: \ݎnׯcxxrL8w066۷oc}}wdjrDpQ( Uոx"P(ɓͫT*qIJ%#Zs7 >|D>\D"t:,..rF_|[nH&<| Ο?.qt:u3<4  ?~ D!d2,..bppP1{zzv LD8rbb1ŧ)t\ _gYjAv AnP(X,СC~:GD"n9曰réSzQVQT`099V~x^DQT* !J[qxx?OQ.qYR)h4H^r9|A^GD*JRXꩩ)$=v)VUB2 ܏r  r\RaNND0^C$t"H#`a`8MI7%%%d,v<<88`JJ0VbT*`0|c+ b1 gYg/#9qk]$(iGOv%OU4g(.rW P}1Y"qr̮Vjch888`5z[`b^_]sK0K)ϩT*e+`6t:Fp^znB]4\ v'"[Vya:i].100v|>^kjZFPA/IR>\`2Q,ӃZ\.Z ǃBP(ĎaVI|#u:.˗/ctt^[["ZC\nZ p\X^^Ǐꫯ"akk ^z% loos69)aŽ{p}bpp2FHj ÁMv7 Fqr4MɥR +++p\sy(u:"ܹ#^A~̙3矞@ A $H A I~"}ف@KKKX__G(B$>&, 6iK$ t:vwwɓ'PTX,^~emܼyݺrP(P(˅Mm2b5 <(ٝI.wށfC0fÝ;w099ɛF?F@8frGT*l6& X dBA,FÆzxFɄzuvF"<}ܵGYL&#dYuH$ܵXVVqq,..B,# 1Md2}B!#.\n"ro VsV.cii h@x1z{{xJ5ԩSt:t8p\x1ooBX,d2fp8|p\H$x1.]vR _~8t^ Պ!$ID"ۃGXⱱ1B"@V|ǭ[0::ʝ? ?~\.X)l6R)XVcvvNfGVXđ#GtP*{g٠j|CCC .<2 [Vloo3ϰ@ zRzŭ[zVP(P(xRP,7xW^8|"d &Ν;1loo>믿]N/P,hZ#LkT*8;pJ% R)* z}nFcc;)RRya:hh Jlr\+Mb1(4 z=wmiz( d;Mk);Lƃyj5G4 ^CqJEV*Z-jj5>된ԇKpnQr_"cK~wzh ſW*܏t[z:t R5R\4Dr"fBMAkVr0LX,&]V;%]дyt:h4$ vD"N kD23t*f3"ǙSlӧOzp:qB<= ^nz1z=P,a4|111DngJ2 KKK9NcՊF}}}rhp8;&''H$dxJ`6P.>t:H&0 p2 `U.퉉 Eܹsl0͜BIB `x0D\RD&A2D VE^G.۷++^A $H A ?zEwjj . gϞEXD*N* "<G~z<\.ڵkݻw!1>>HD"JB\~cLRJT*űc0??Rqϟ,8:pcJTBףjq$Błe4dH$BXhbrRdRѣG1::p8 LgϢjq'9(D^>Zp7N'"vwwvJ8*JFfYl6d2 IDAT2ﺹ\.χVB .0Сcjy&666[oq)V+a<{ 0Lp:p8n4y8@I2RG*B2b,z=t:trvDh4XZZj\.W& ARa~~]"kkk0L x >jlll@&q\{XUxv{Q :4MerRd2A"UrR'N^G]xLRw0W(Rr0  z=;O)_:X_% AL&cZLU^YrT*1,DK0`(9 ~S̳RhdM i R9SaTpb Q<3{rcZT*4j䦦AS*@ x^ϟFx^sT*!JםZS"96z{{a9@H$͆zyl6_:cyxh4rO/ul 5 B#Hj@Vcii /^d׿FD"A$&&&nR Ñr333pǫ[,t:,,,@bddfHt:<8&&&qi jbX, vV+yx<8NjH$b1vgS(Z-בL&r8oo}}}(wjd2vww ;`ooܱxH$aX8"[.C?@Jz $H A $]x"T*Mղ3#pZF,Coo/^~etX^^FV8/~c8t]m40߿Bqf?fõk6&@x ,//s\+`0X,N׿ӧk2 B&Bv@*br`QTu{{șLNftj#e2:FrF1 TfPT}wo4fggQT!N@nll0trzN>b5EFQb``KKK888b/~ $ :u * r6ES38L&t]T*HӰP(H&oZ߇gh݆fc7'SKpSբhT*hZD \r9J qjriu z%GnXhdHV.Sn]6!&P؅KKt 󒫕6[zoKJ%J%)ژ\ &8BP&L+k^cr<X O?џQ2=?]辠zgzzx<" S_Q||f_Nkt-l6J<z{{!Cl"s<xx<νy=DFҔPn Jϐl6s<L& 2 F `6qM<'mswj5yNP(D\F?} Át:n vx;~mzZ(bB+N랞4 $  t]X,|8GEOOoy?'@9n7L&64 J%t:v_7X,F}OOPVq |G8vn7n7xe nj0 N ??jvTU baa^TZ & wa<~?4=\pmd2! jVRvC*"Hd2`0pN(w=zN\j.;"Gmb1l6t:8uǏ bA8ei&<T*{WwUWU{6 "9$")Y`CQqB!r|0 H$P +ŀ9,Q")ٷ}ar}Hb4V:Y>sqIY'gǏdHRE=77b7|ybfyo&QTHR$IvvvߧS:;}>ēB!' /`Zioow% 288(?)hT\tߩsYvu\\(zrPHbsYcW1*pHlPLS.1 nU +"UuRT-_kvcd„r+hh]db!Jhhooebhd6? П===tttL&Y___VqeyyxW ȑ#|l6FFF$.\tl6}?·~믿.@SԩA(fGgg'lF{{;xA=zDXT*QVdY\׾5,;;;\.giiI yjjm~OGG<3S,7V9"ղp2`l6ݪNS *PgXh4rX,eX,Vj]TdHG9~UWݶ *Ч6 @,Z $j}UUCFjUUJlh4~)@r}*Wfy|>xۍl&J=NnloRj?t:CшWkC"Xx& ooo344D$!ى^'JT0R+ԽDQ2JQ\NG:hiiIz~{^:::Hl6( 2 (J&j~_5땔eBmmmܿ3<ʕ+?NXO>l)d2JdhB}RDBb$ ZM⪯\BP`uuǏjF9sH$ǏD"XV~?.gϞP(-/z[jZjZjz[UKϳ+H$"ݥRY]]lbB\~F訸qOO8q%߿ϗ%U񤳳LNNbۉbL&޽KOOz^zL^/D"looX,J6 ?& 䁣FA*`0x,,, h4$ ;6lɄ% v\ѣGj5FtuttM6ޣGFrj⤉D"f$H*0U8|0x7np:::?'N`kk Fؘtz^&Fabb5fgg9q yq7A`0E?T=̇xjreW'|±c}B ߝG9y$xvN>-ݶ>>jܾ}v4|kkkJd*bxxXzpݼtwwsY&;;;ܹ۷yv|0d\.G{{;^Wo zL&'Nt| `gNt:`'իWe}}z.g+NrJ%|yFGGA\FxsDaZV_UFv;z|> zwwWzUFX,bZe )\XVO[T*%rX(+Zwx<r9Udu :p8gk6q\Ehf)W`y߅BAZVzUtjժD,hfUvTlb2p\{ϲhP(p:2qsP*A@m>!NJ`0TC4tߏf#S*H$tuuI,rz^qWY v/ BUQ.lZh44Mf:uJmf~~L[[[[[DQoB\| :D(4j(\.s i4b1.](FC">,*PH: ///  ,--q!fff8rr-f3@@S&G·y\.\=mmm20XPJGG<4 O[_H9{_zz[jZjZjZ-@kFoo/ËP(fa2(JtwwcXHLOOz] /\.orBbp8ax<2 7n@׳Ë/HZ}{tuua`ssS\lɳ>ˣGx<|>>cB@T*%iVWWbtvv0<<,j;VGQ,q Yӟ!~ar)UQFȈK/č7( lmml6sI* ۜ={AP  O"qGβBD"Agg'>zJxt:MggqXl6+3 :N~#~~O~mmm033ùsh6e=zD$fQ(Z&QF׋l&㡻ZF&!rz)2 FSNNP`ddj.}rr@ Fa``M_`llL`OΝ^d2hZ666'r3==-777|8No>z(+++\pÇn7LD".`uf3~!{i_N$! 111NO>ajjk׮ܜ8UOꡌb2G_UА@u<X,0l%?Oh4ŋ9vK\fnn'R=H:ׯb! H$F UV.^ǹy&͛lmmIgs6E ռK2 2XR,) 6|yH$WcZ%fI fL&#=n|>/ɫ h:q[QF2_ .f"j"U|fTj^UUڇ5p8ّXj*U5$,V0E=ً~WuuShc6?UdUjJ8N-L&Z&Z LzL&tu|fQ>s9sRֆFсfcwwW:Հ:nkk#HPTpr9`.4M6\.R|>/eH ~tR|i^/7n… LMM@oKW;z[ZjZjZj_Z-үZ NMMh&H'8{,W\X,2>>N\&Lz$ q]vnݺА8e"8j>```^>cn.ceeE~ĵ R1dDZ^r AnݺEP^>I4˪VqE\.ǏjJ7d{{;dYUPgemmd2ngffh4̙3y߿OTbqq'Nh4$v4F%FXp8,@9TO_$T077'}۸nF#u3`#4 f384 LJ".ea;::+++8qf<$ 8~x1;CZve]r.n@?/>6668|0SSSqgT^^eEښ tvvNph4XZZl$5355E2dmmp8͛7'rUN:%PmeeET áCؐcxlloozfn płB&uT`EMt:tb1 _(ͬOC) j5(~U]|86c,m6Itb#OG0}2.T*I<1 |>O{{8iooZ STz# E;V0W-W_ xJ"&r,`TJSQ[@ @VɪWXY Xyh4$}ooO>R|ZVH7Z-x\.Na:|Z-NSj <h4VH.#N]DHX,4 wޱEoo/`}uvvl6 B\xvO"?!:bH$B[[Dl6FGGY[[޽{ 9~8-/z[jZjZjz[UKo|J( 4 Ē6 顷[nF(Z^rDZZJwd^R|.tttD.Hl4^Ȯ. x{ŵkp:looK?0rY{\t^\.Hn{=F#988`}}cǎJfp<}u~3<|'N066ܜ<@U*FU=xq=cǎywx7h4dY/=nc٤SAح-~?ccc,--QTͪ"޽{9z(ߗ}{c6YZZfq5&&&SNggҒʝ:00?@:Q8nuܼyS\R1]]]J% ~sn[I$w}Ӊl0dXZZbbbl6h,###R)YXX`uu9ol6 z {~˿K^{5VWWY]]u_\\dzr2z8SL&#٪:\.388&VNjBA`\&A T,38 `r:7#ooo4Q׊%V[*X,VduQ}. aVH$$*[TSmV@LAO\zf3tZ`kR:_k@lFj$P)um.Jh4FCjxf VQjZ&`\ Vۡzl t*7V8jJ6p(U>٬ʍj}>RLJTb_\ ܫ\.Ko\^O&n>S箶_]u< HV͓Ngա ZU:6V;;;Iyj|\*Pd2)&Mp\ZG__wޥVN_ZZcU ~)`\n[OP(DTkǛo{vI{bfaXFqnݺEGG~mٿv-N'ߗFOOioo'h6rR)f3/^_fkkKz+ HQͦThkkҥK @oo}[\~]z??ϭ[8v ,..o~5f#G#RǏ__D'G7kZRK-RK-RK-RK-V||~ |>{{{,--Y^^lJ{%O>'&"},IfaaJG?TCq-n޼ɱc9fx3w.KzT'&&gmm1uvvvzJ%"_ygh4E[Tl+J ;ly6J$VW tjU:{3GLp8, )ثb t* ,`u8t:_jGN}ժ fjy`V*XaD97 *E^/jJP@c0dYjCAmլrN VjUQ*]A9v"bHdw&d2Iz_ *X*ŢUırjZh4g\>:%J|lll6@@ݛrL? ՐS.β)]^ݻws]]]m jJ%Ο?0{{{r>,rD"RQ333F垣cǎaٸu 1T*ױl8NN'@6" (7n NH&|駼LOOă!H^7nhL&zD"Μ9>x\UGqteh_^^f~~GT*>ǏBJ#.LLL ^KGKt:{qm?$/|ߔ_:S}z-]qi>St:w?K ~O6G?}7x饗~RK-RK-RK-/>SN}nܸ?/*˩SX,qb]]] TiZ9;whp%v?YVWW_5et%z)yi4 ޽KTرcR*xǯc0j5xwi677ikk# FΟ?/=NG&M~?jL&:l6%Z=]ZZ" bd2B! |d2).ܩ)ql6ٿW\! cWxD"f\.333j5Sݥ^sq27oV/2<>.n5]Cr\v:;;flnn__022$|M*HDkZ}q$Z,L&D"}Yt:o&>g}2~hZ~7~EbF^zI hxT * \.0Kq;::(X,:AF188H$aX,|>b^ݻw9z(RX,ƕ+WdxA,`?9ǏzT*dHuUY9.NH6%J(x^, Dm:::޽{_~{I~*F9SyFxkt+@"L@ T*I䶂Z}|>F:cUdLrTQL<>^STVP`0HDV%v]Y"UNAF*nZF!]!^~Vbd۴ZtHG r}/}~}v=+TrXhveCKeE7Yv{Zn(\oYT.eN'{U`Zu1[V W׭Znȑ#k=f]t:^(&IhD"hnT*E^' RVܠXVd].LF>y~~L&C䣏>bllL+<88(C]W^>~X\\djjf2:G.coo`0H `qqQsTVNLL__tA6DH$X,D"ׯsYvvvd*e}%Βdᩧl6d2a/0::ѣGF2$t:v%J_U TU4 kkkT*?'lmm%˩T*o3;;կ~!ΝX,b`{{/.?'7!Nx}6__h]5 ַ:7sW_^ՓrRK-RK-RK-RKCm4 pX,&ѷ<o__[[[|>&''D"D"^~ef3###zqr}<mmmj5N:' h$2j/|Ǐc2__pرc<ӬF/ryN>M,7jG066ƭ[p<|R$NNz`klt: ,,,peR===\p]~?gΜa{{[\TCCCܹsGlutt7o2<<, IիWj҇l2`vv-Vt211!`h4ʩScuu^{5.\Vѣd2+n6|(i9A6668}4Ν#K/KE|aY[[ܹslnn PO$rccC\ ɐ Nx<v]<8|ҕ0oKKKn9hqu~0==̓x78}4]`0lL,JNF!6l6G}$VfdRYNTzjORߖO˅ngoo6ܹ P/" i6LD"UDؠp8,}}}hXYY>xFG266&n[{=zzzjAVI}}}2TxT*z-l6O?4\NǏ _|^nF= IDAT'OAU ϳ!͛7(fO>fo|}kܻwvIid2$ FFFw!N d8 __FN@oK-RK-RK-RK-KIA~3ѨD*gFa_:DH1??>ZbayyYJĩS.fyX\\yM"[*k6ܺuK.a2p9={Ç)|k_֭[wdWj6FLMMl̙3a]%zd2155%1t#GH$XYYb ݫݬaww]qn\+9::*62 ;|h4677蠻+W`X\./255,f \>ygeN'T -@ N.g?h4*JGۋ^gnn=?.Ѳ'Odttt:͡C$L +++Dx9y$bp8Ǚ%H0::t6 4 <z~VU܁EJdh Nsiy9r###ql:::p\A"Νcoo˅bww@R'p__,//z'ϳ(񬳳">VҒD:ty򗿌dbeeCW^ݿ)k|ǜ>}A>guuǏSrx^FFFb099).@ ϟՅpb[wvvx<8߿O__]]]X[[cggGN( D"Ξ=`٨j\.>#ܹ#Q\p_~\.G4% tQ=sssH~OIRټ^/v8Zt:LP - q{{[ϟ?ꪜlVxϹISh4R,y%NRL&988`ccr,}٩:ᰀ[Ip86JQVi6iUlb0iOE7 JJ)X-Cu?UnzFvKryjZqkZj+Vx\dE%&(Pp8*UVV] L+8``0`0(d(誖P(t: XuU+ O᳸j9hZFQT*Sg:>88A;A^'N@r֪h$%]*@9TǧnKgdԩSrFQq5c\E*߸qC:ժO /A4FCoo/: &&&by"ҏ aBe묭E0Bdv V+qL&### vww|VKGG봵122իWd2|>zzzx@sm~ō79VE2DU霎w;R_|C|>^E4%JL&9z(F\VKtN{ #UjZjZjZjcDڢ͔<^u{E@%^8>> }}}}gWE^vyz=]]]T@nspp m=z$n3S($Ǹ\.&&&bY__GSTCӉfZ/D" x%RYE9lnnGGG?OaddDS$^388(Q &V*0$ R<|>۴ 8RBqJO>#G˙3g?3ϐfrQz(՝C^gaaZFgg* &!|۷?~I0$000 0z{{IbP(DT"HGe>7b.q?~̩S( ub񰺺VU"uZ\p'Or~!.kZr\.QLFױX,ܸqp8L8fooQ4 #cBN|>O4%Lt:Sd2LMMFaxx/F?| :t^יڹ9(amm |vz*Vl6`x]y~}$\Yy.zSKT&Ufu@f Cb1v;JR ZzPd2q}}}. 7n܀`DX'n#D"|>a< }],,,`vvSԵH$`4qqT*N07M8vvwwyB8NCÇ9 JOP(jV! Ç( R>vX! RPPסR q%ȑ#y&xj0Pոx"w|>d2Fn9d,PTx~:?0c677NYU_Wa00??g͛h4 ɄT*|7M!LB,cttկp);v ^g}޾W^ygΜ6sssQ7;FaFaFaFa~YYYA>>|Ç`0R`aa~/2b1+ :pYLNNr?*r?CN SLT˸v21 Fa4qeA,bcc׮],t:F|< axxX nS+F6&?V шz }D"N'0 |>_Zz f.] /t:xqi~ 4 rh44 L&zAToD˅~H$χ3b|2-b1|Fpݨj8s &&&{B<`0Z8" >7 9r uo6޽. !͢jVWWf111~h4 Ʉx<^xv=rݻV ߏE$IN_|ccc fn޼ FB}O?X,JՊ`bbd{{{j0 裏`2 D"ߏx+ڦRҢѩT*D|šLϤ?IMLRZ@GIMN!)IRR+uNŃ/FB3F|/)h4IHT\,j5JJǔ̥D"JvX ^ϝÃ{+ ?.J @RrDR^\fjl6J>j5* |Jt&6A^i{?~T*h4K}V R"b^//Z|r|HRH$ =h4]|vZ;񴐃B$… p\bie2 k?9DQ-$t$ fff`0_~gϞk쫯Ɨ% 8~8ooq5:u fַg>U, ^_xg/IaFaFaF3N<NF}x^?3>.<8z=>ܹbfݻPTB"@RiAqNZ-r9LMMARb?~1z)Z-yr A.fq1lVX,Wt:x$n޼o|HRxw3h4~p4ŗe?a$ ,--Vd x뭷vQVQ(155ENì… ,Jχ`cjjӖ(JD"lXZZJB"@0 T*:qjj CCCFp:H&H$x< :cddbW\SO=XZ^J׋uNd2֭[ (I0'J%t:b1~?_=Z$w-D" 'D".:No@ iaf!qF)mNt>}JSzq:noFǹR*"LBrO`0@&ѣGV B`eeq~Fp\؀bL&\.ᅬ3gT*J^{ Νc-'uQ\.R)\.g?×eܾ}lفj^GnoofaddD@rp@.0 ?95 *vwwv9e|E(J/fȑ#jBXZZB(J6scffQY,l6h4bqqnwѣGyؑ#Gԓ;HmߛP(fgg H b>)'|'Oaggp?dYNK$R)( ^djN@UWh4f,?#~?{|{u0#0#0#/"?n?BH7n X,Ưk\pf{+r9J%&&&PTvuRA<hw6Z-;x&&&j8My=iX,( \r^^"L ӵzU*+WƐ>{VVVxAHiM)pm6(z0YNtFV[)KzG%=44HpJ tvHRh4 J%~ mH$bZF.J8 RP(xA]HĚj U*R) |QҾjq/t<./+ ԒVT^v,HP*P(XMp?T6HMl}/ 7 ^T@p iN(Tg!~L_.n * ӱ) >M9ՊRaҗ}xdd|!;7[[[B 2I`̠lh4B)zۘj*J)z=abb\`4و rP.Yռ'N @,f!@ѰZg"@^G8rR êtBF Z'Nr!{RV& IDATbb"nZt" Zɓ\.x<B .@,p8TyTnq%|~l{{{D&* lNMMȑ#ڂL&C:殯ViB*ph0p8݅ZFe(H$鰹 χmy>}KFR&Rjv… H&0LlDzX\\ěoCA,cuu^:{{{GrF#(IHѣGj8ӧYt i(J}lll cjj{LF#^{5<BX[[D";w077NbQ 0͜b8y$ v;~m~4 8B$fggPTXCL&Ne~We|GWp{{:bZ >kkkP՜Th4فNCP`0IXǘf1B<J~_駟F>G6šC8fZQ,1ZcJ6 Z-vb^0nchॗ^f`0@"ٳgX,$ ÇZ"Jd2͛H&Px'_r5"?8?B0D,C*^48wf$ # `0\.jB`ss###|ZV8{,O`ooX,Ca}}tJ߇T*Ņ pM?,z=J%w*Jt{qqǎVEP@P;w8Avhh4auuz&*l6l6Vl6dp88JZj LN^=F>-6NpX,r/1AF` HL]tOP( :D"))H:oJRJ%@(L7 hZ=l2%,MKhD"/AUs4iYI)F)K RRdm}4MK+Z Ĕ-_)Ml*{I?Mc:>J3) \$5s݁S6l6b~& 69t@r:g`l6=^d>x\Bml3rZxF]R | f3f3+i}O=RzZȑ#vvv`aٸX,r$ <> :</.dtb裏p!vp8z0L&B2><<^۷o`wwFqۅRF^zNJed2B!d26~n0nݺ׋L&~XPUӟ⩧.~?677YtDqob^<R&''9acl6b1lP(ft$ F\Ga4i*HÁRp8Id2\v ʺRJXYVZԩS02D"cեL&$bX v@lbb/"a0pY!bii X^^O<Fl6X,۷otf0Lx7կ~^\k0 zNZ- ~Nz<ܸqj~b8uCZH>^CCCѣG0==L& "j4|j{{{Wz8}4QG0RÇf?\.s21<<̵R!$dxapk=R fB6 @zr&cT ?8KKKxgFjI޽{D7 $I}|8Μ9Ë&rNRh4}kVǃ|>ncaaVVfO>S055\l6D"]B6f(R"nݺLL&NP{mm uT įtۍJwsvW#0#0#0#^aCw||SSSHRj܄򗿄5TO>V(˰X,nzNhd2APŋfc%rA"`ttJupR\.cvv### tx|jT*8t`61::jL&)>V*Z-ܸq69Z-kpl6t:pmm Pd&L&{1yt:8hRb p ׋mNe2]f`tӆ+++f8w}T*:unG\f6,,,@,AaaN޹s333 ar9nܸ0\.snCŋh4v.-n7,wnlllرcn{`0p8 ӉI r\zPAG)i2`6h4q93ZXX@рhD:Ƒ#G8iV@ t: ׋juNimXV( D"NiFQT*8NN\.ܿL L2w\8{,P,9mx Q(D3 LMӸ|2Μ9vl6M!pH$B4ZjE"^ct:x7PT ND,H$B0{eE8N}b1|>NI$A>!ݻddd`6Q*pU|_T'uWR J<6"'sIil4H$ Jh4#`7A5S֎FHRh$*v L&úZBZNvVcG1ڥC>JSSG7i`) Jϔ %G}&cuRl6sr%KJdpP`8KIU5 I-J9 rZ:F!JX/MRrtO =u ?z]rD&p(p3i Sʚ ;nJů v.;J:D"Eh4j5::g^O\q8bao!ɰχ)j;B|n>~}B=N\.$Ih48Nv}:u ljjHӸ>ӎvp| >U,aJJr9l6  BŸ}6B Jv] bR)C,0wZ,qr AN8pf8|0b1 H$!Ră t7M~`;66;w@RtX,tB!2(`.v;C,#ŋjXZZvww155Vٌt: Llmm"s_dB oyLNNr!Lb{{L8t^nb1%խRRDZFdJw :ny n6>|YMR8s 666tQhff{ k.0;;ˉJݎE X,4 ևRon翷Z-VhR:LvCT24~TiXhPVT7|ì>&ŵT*E$jEVÝ;wx Ś^Rt:T*D"HR~>U*0" 9D'Or'(;\j"JaccSvӬv0LT*8pkۥR)C$_{{{PTŋQ(c\.nc?. [^^D"A(y>|յ>fdB.`00`XPTP(vp8T*j ^J=HGnV>yh0 xd҂E~VjZܡmJ@ }@r Z SG`aрFRdCbX[FpJrZt`49-LY: ӦKtX,`@^gE5An^ϠiOt'KgD"Z-Z-4BXJdD5iܬjQ2D1nɴ@J_RaHjZ.% ,&''JPxVWWb@TV!AVuaii p:È㜔h4q <$ `2 ~}}>-Z-(*T*vwwRxC:FRc1:: ZV&w}Z^ToB.2.T*k_6*Jx^z;E"wᇘF. [>C FP7C#0#0#0G@>).z8 pd2N;r9\.~JÇF.R`4!P.Q,h4୷ނjdB2D,f L& tΝ;X^^ƙ3gPV9'81jXp8pmd2:u{Hm6CV[[[lT*tZ-v;V+VWWaۡT*x|>,--!L`0p2abb.\R ǃh4Z 333ڂFjÇ!p8zQ(XIHzYBA$D"\.ϽXzjNtj5LMMA #jbggfjRuݼB$!HҥK÷-|+_a;=xVWWt:a6Y?Dt뜠  b Q޽{Vx.Jfph4 4Rw^gpHۦP(Ûp Qz%YNz`:gJ%VJRʖ%=iNh#4dB Rjq:Fjg@%%IҦtt+骩ÕgRWSo6}/W  &0O#J9k!-5mER ӹP(P.1$+m/)zئ%X].7”@&jeAfuA677a4a61 PV>MǚoBVqu֐vbp:zood]boo F#B$FFF077)i I(Jvp N8ÁNæDQVrSR Z@^w}/24~?nݺq4M(JN}^s-ev3ywcjj_B4QTpM~_7@0k  #0#0#0W Gi)lΝ;7orb5Hh4^vz^bcb~~###8rR).]Ǐ8{,>^}Uarϣg@166L&VP(`[narrB.CрE4TdBD>JFثWh4BTrϣE$AǗe2wЩT*p\AK}|f& VwޅáCd0??ǏVayy. VVV /^F1<< Bt: шBRayy;rqTU?@ccc0.{R P{b>T*ٌL&Qyt]N;w8x !\9x^m,--qr9YaMrLNf}Ĕ\__M fUU4MlnnBf!qwj@>Vŕ+W0>>ΚE̙3B$Rć~FD ǃV^ϋb1#S~X,T ݎjÇqǃ^ RH$p8g}?яOt~3UӡT*qBۻ.@ b7/m6jV+ 4&<Rva۱I>^bR ^M?zgH8u (   IDATh4w* $N`ll DT ÈFX,8Gr_`tnIKb& 7n܀JB nX,łbNPS:R* "ФqJ\R:%)!ZV1 x{ jm(Jv@'0Qe2kZT*ol6!Hd&> ~ WRs^ AJ)w4IWL[V0j&50 }Tk,HG5i:CJjrC0:t DpIi\Ǵ7`a5S ӂRFydF!O6 0KRph@V3Bv}ii }pp霣@}ccctx뭷p!8NN6 X,aj^cxxkkkUn4 F"m^EM x2ufH$X,Ŝj|5x ^\QnihZC z=`+ Ek3T*8x r9n޼ ͆2h4BP`0p8>H|@/+0#0#0_}+zj|l( Z9nqvvr9Ez(ɸppC&R_c=RZVO>$סVY*t:qUcnnu}6>'vww/bzzfF.r9F#bgGLiF&333HӼ$ ޽n`0T*bǏ'? fff85{ b#066ՊyjʠXPܹs v j5Faoov QnJs+JVǫp%~bCA*2vA^G(BBd2J%k6"j p)O~rf3B:f9GՂB:^GE(B.C߇FbA2dx<d4+SFFFo_D0L&22 |E($b14'$ F#rJQ*bȺv χZƽ`׮]ѣGP(8}L~шz kkkx'HXV\x |PՐL&a28Y,z0P(^sگZrW8%:<:kC$V+fffL&qy%y)9hzɴA=H$szaK2F#l6wϓvNhV(@PgFaJVA&jB,Rd2X,"LbffKKKX__G.Y[MI`H)Zr9 ^xqu$ 9rzT n?gY5_9A=Yt" sG4cB2& ^Dn72 jX__Gل`OjǶ{gΜE]j6 kkkhZ\.ī*^a>׼+8yϟ@0#0#0#0W?=u^/"R&&&gΜ0pU|K_BX6~_Cӱ R\:B5 ;!Hh4 rݵbb'NM&G8F^4 \|RQV cccH$x7011-~دT*1>>L&R Nw@@bXl6K/hpɓ9L]@2i"4 z= Bl6Up!W_ř3g`Xx<ΉcJ?T*La0 bqq###HRH$ j5h`08 P( JqULOOsR c|>\sq1) 4_uF,,,  T*!s_m,c!JiH$T*܄B@2n`2 0==*rF3333v\|L(N7n@ `6<G&a8 p b1~~ORWJb*)W24d6L&fR,~Fr EI;Lu:|)G}Ť)4^nYҌ ,D( h4 $HInMZd2 T*Z-Z*j5^ZCjYJa6<%NZn 2˱ʺjr6ch_Ѣ~RZ T*E/ARʤ_)nn Fݸ2ar9:g:ޤiV( P;t$NIIyHĉi9"Zo߱L`^?J>S3-0i(AL=Ƥh&xMIwz|>mZ-b&X łn J͆zEt[:LB*q JZ +++ )O䳳0Qv8HRhZ0X]]>o^Eun+_ R^/N*_[n1l4h6zQՐ T*F QJ*vvvR\.j" ^#cY"n3P!/%E`n3X~xw1P.Q,sO>_LL&888v PNϟ?G,GWUt]q F#}]f7oD.ŋj?>k?FZ,2 f3d2}k ; 4VVVp%vR/5^w!xPgg\}D䌍!J! \ZF<g477/h;E"V+j5G?{ F~E"vwwVnAcuu-9U[g2t]vr0 p: ;*wRʣ(c^ϐzv=InZt:?==Urlj5~ftT*a8jZ;t Cutj6F lF#v-R/EJ$mQnw]N*vN_%L ZNs AMz r1NX,cH0uҹA=,wpl2N0S.CPp1}fr6cI.pGM,\?OZt:* y"(nJQ\'OLfB>ҹ@st[`NqJMrv}!Aף LB"`bbp\pH&tX,|4L`0f>'y/j5<l6^/80d2bΝ;łx<^ÁnC8N (N|w:a4qF ^t:XVLLLNCC4T#HRyBt{{{켦zV+V+!ư[na||fn_~z}# $H A $H?w Wз-"8>>FZN)`2P-VaZYB.H|>Y7nZ Z SSS8==~VX,l(Jd2B!_|z xnpx~333~)ի}IX," 2te=q9TU6V <(/4ONNN.]~T*xW^: J%H$<{ `X ~Zc+ /}v j\.b@P`cco~1k6x<]155T*vquDQ?F&D*>&&&pET*yfӧp8ܱ#q^ӧOl6JovzlH$BR@2dV>prrnrB}"DHK w]nY%Hn69T*a0"Hx9v;#f<}.\`)l6?Tᣣ#{avvxT ?я. c̠T*xىvf Áb/Z+++~:Ã~wBc||c, R)&&&* tNOOed2nDQd2!H bzzB]w@j5w:d2q>JqԍF(a_40h40prr`ĥrW*jta!A^~ijvL&$Id &I8vR0pA#RZ-d2GG"ߖ\A}FZ X0$Kpj|cFp.*%+~{wb^ju hZrth6NtnҾkUTGLN` mXPՐH$8v/DxDl6S[C$Z>S(zi w\4|311Un؝N'N' xV_a).]B"qj5 `ww+TUiLMMl6coowvvNR`DP(`ۑJχNh4@ 'tFt]rѣGx<LBrF#frQ*xRM~шvL&jp B߿hx͛$D7C $H A . EP`Z_c||8}`@ .q4Mr9x^f$Iܽ{>*z=!vٕH\.d2 <{ sD"ptt}f^/={ٌJ|瘙  H. ϟG&A4:rNOOqR Jd2;5:|>jpMB!cll HxJ.~`0pTZF2hd21={E$It:zb1VVV4bjb4q7v$a X Jűb?fw)KNf麽 R{yM&r9677(lJ,dv;b1* <6660==r9)V+VJ H8BBDIh48\"pD>Ac$Pf3 :;jf3?¦.Yh2DJ@br1 겥AK"R|h@,sh4b(MJ KY,s. kfrf?Z-; tҠL&}v>;HRܠ_> _.LNj#iVd2q$=Edjι]TY@N^NCPVEX6;^/ߛ ?tP(R-T*akk R Fs,Ν;ce\.yhZÁunv*<1+tBRAr9%$T*MjFz Z=V n+9Ii"#t:c Wq.{H$B6ZTh4 .T*RjBբT*yxzz W7z!^A $H A sz}:za`Xt:p8h4ő|Jhٸz:Cd2nc8\.sD4unR)qINx<VIvl6F(MpDHh4p" _byy.xrTU,..X,"@P`rrHX dӋ" ELNNr5ŗV*( (Jd2<~""_zV կ~\.sΡpiXD׃nGanndϟN^x IDATEmx^R)b1DQz=asf34 Nqxxl4v Rp8BTl6p8Jr976 fX\\d8v!HNq9F#dYB!jj7dDN<, `nnfB'}uv@тn7@T\.C"`0O?e8K='''xٕfZvfd21L><ﱻwyX[[ݻwyB׍7`4j{A.Rx0ǃj@ p z=wRlXdG._r9bERAtna㥘^J3(WR ߏB"ӧ| o:wxlZeَirmz< j|NQnX\_^.)f")r9vOIrR4:'pGݨ4p9:g{Rt2=7(>um6导T*EZemg0Ix z=~MQ#.?3 r#MHJ߃`-4. h6ݐ`ZFezZǙ<(FZ`0l6lr3c^hf3NOO{^w@o$B $H A . EGPJ`Çp8aT*y!`0zݻ^UJJ[naii f6 ^_5:.\5nt:DQ~|駐J1;;z>LJ~Ѳ>..^vͱ@dX\\~X.Dۍm8N~ܺu _}~{FWZ>JڇFVT*w6}Z-Z(JDH&0Lh iaXH$Nv34M\~366|pBWH3^L&z]HrfffFqrrߏpݴ2 Prjfj݆4tP*v^yvss:#Q!f}GϝN&)K1g| j5>WU]V*P(h4 7'H1/K.ӳh4`6ءJ,rS_(PJR3Rdr(hu"7.9+XSL]i).p8d-JQInvͰv0{(JvE(S5??Pc@L&CYM\,\.vsG39̳,T*W:&KG6E\.Brzc \.D"~ w29GV+"* Oo60|rl6( $ L&FOq%lllp@&!Al6F1??σT 888RG*vHRvWnz8Vd2 T[[[Rd=110$, RJ%&&& Jvsα3bܫ*x ~T*dlp:^o@o_|?~X $H A $' Ewa˃!vQ(Izz]GD]{jjn•+Wx^zP,y1`0R`ssR?MCNNNb}}ӟ.\$?t:ͮFq=888~?_ut:T*v\20t:^xS.bii oY^(JD"0 z(˸>Qj5v;(N'G}vD"L& x`HRFYr`Z9&?G>Gp:/ȅ=66Ё bA6B[YYngpV͛D" >5޸qB3330 |>$ 155 Wχh41vG}ݎ)t]Ktgg;;;p:H StG"z=FDQsP,v &͛7P( L]ﻻ5LN@ ͆\.)a4bbbA$ Q8v Rӡ^h4BTBVCVh42h4h4f0LxjKKK*Fa\.^*χh0b0.hZ~fr]IRr9@X,|o=L"bS2EŊb>n8.XKPW  k_DM(um>ڑq0]r*L&$h"m4!J9@NprE&%W=Nv`]P\.禨f:cIFc)JCd2lr5AםLM N :m+u>O+7prvr!d>2 D"N'J`+.0Jԝqz-x ^^`0X,5iCVcggnR PB| l6Z-ߧXIR|Wh4P(pݼ_kNNN8B[P #"Lh4H$½{fs9T*$Iup2PD s@З A&b\Nn@ A $H A _Km~)bffE7or0)^n`0b>*OCn ΎPT r9&wZb~;\ lyj'Hn1.섦ZWAd:'0Kyn{3A86Jv;;o:,ɠhةK@1=gyL!4|DRd'5pjAHR0LC,J_q_S6ch4F"//1>_f3 9Nl6uy$I\r~T шݰ>bs`bb۸r &?|>wyZ P;;;RD:\.GA.ՊT*۷oczzViptb004'~"@Ǐ :D@9::sJ<Fx<-@ FH$A&aee*h`W\AрfiJzeH  $H A $HП+޷~D rp<== ċxv-ȪjܸqVS\.r9n7R?,AR R x!b1B(P ӯ8x<8\.G3F\./VU\x~r:lrjP@8;(VqKprrN˗/NBFa G^sI0`o4|X__;Z|v Nb`gg;4- 2 ={7|6sn{k0 8bs/_FF˅h4NPf{{JRocja~!$ ˑj`4q  F#&L&޽MA8Nrd2A&!Ja8"VN{tzJp~hVN`>:H( OPUy_R p) Bn`%6>>#vAw:J%LOO|00(0(X__^h^>x<R)vkի0L8<~N~Ivzrq>bAb jǝNpYjZJ*ũR=Eגsee;5b1"F#;affR'''0L;W8 R)hZb1t:v<fꯕJ^vFvt:vR0Ojr~hziPj1hnrF$`JǕ\ )f٥P(t#pIT*)n@+iFܳK{)W$qr\jy$˹? 7/m9iX\MН\*#VVP|0pYK[6}MNF|Fvs "v;Cmr{bin`004Fx4 WPZ-zhZAr9uLMMΝ;UlXB EXlF:f׺`^cee;4j5-J%ܼyronfd2Д\H [[[FBh4X__G ۍZ|>R Xcx<G0 s CN bAV4 '-6Fܻw6 XZZ!'t~}} 0># F!^A $H A sz}"~~nbמfhWD@(66[\.x^X%3 뱻G;* ZwY/gggqbbkkkP0 *J%u ܾ}.c *R)6:/Bx߇n38Nt:,--999}u^VLvT^ҏa4#fX,?3w>ykkkr 6V+Gnnnn\.^s&E$'Iz\.dYp)CCvu:l6jh4z!|>>nȯ >:={)(Jr9H$l6b1lmm! ff?ӟ XVlnnjr4zP@8lmm \LlZ(XZZbӧO`r x<vl6lllV!r1%(Z*fvJ%uFEhZ%HGZVl6ˀn~0V\.cnn+++z@Ysߏbr .W:z C\"(K @.EjZv*Jo4,ŰX,t:vZ-/t|ȅl6'ٮ_r8,$FKuցJar2+Jn6KU*frVz2`ihc` f~j5o#=in^n0\{Z-u g0/^u:COڴ?-sSn\.F<>3arp^gG;~z&(z}J%nsoVNxq׋p8ϟsF$,rmx<H?XZZ`Ãjd2mnqxx`0j5^x NR E`}_yHVŝ;wSL&hDTB>,vvv033Zƃ l@PqBto,B $H A . Eooh4  xD"۷yO 3|JЏ:>} Jǃ\.Ӊk׮!`4q-L&`t1oz=;F2 Ο?yv6 fZnZc&|>߉D"` IDATP(fX]]n΅!3$^ub1aܾ}? DjEj :y0pX,Ưk-4 G.--!d2pqe* V/vlllX,jvۘG$aC5 ^b0L 777t:N % v̊D"|X,p\즣YFbJ혞H$xl6&''qzzhUm0p v;~VqttL&Ӊ\. "T x'''p:0F( e6qrrv}{P*FDVO`6T*hfqa})W%,[[[V@*N~ҹ\r]M&68::BPkT*| NOO199 r.]BC,w^шENLj52J~m.{.^/?~QnFL~8rp2JGV F@Q, .,2 cJЋz@_&^gpUVvQ,!x{)z4&S)]{RuD"PIQM=:d'@xgYLK%XL.JJ1 0j%cG}j$.sx#!7 R ^Cx lj9}i~(Ӝ5M%`h4C:QTS3 P}KgcdSW4Eow]:FtNb1ԩNA *6.GuuP.z NVvfn6|iPJ&''@2T*r>SsW_}p%C";ŋH$VOoٸ* B8ACRw˗/T*l6^T Ӊ>v;!z3mۘF2DVGommqrF4n^dzJ{sF>G4L&9N@o$B $H A . E.,;xr9߿jz/rcGXBja٠jzD" Lϟ# `0`mmncsyy>L&8?ϡV`rr!ŋQt`!P(`Zyq~~Z8NOO9.tyyD;;;G0>>fɟ)T*`0`ss@w.pȋB^vA*]|}iR)<::;/^NHHċ0LfX\\)V+d22z=߿`0ǃx<Z ѩJVLf#:L&D"2|>fffvqzzt: ˮR#_:* J"U*nb1v1J%D" 2zfӟVdSSSfj  H$( x)sKR$ 8mdY@82v R2177Jݗ:[[[|2Ν;Bf .[@"d2* p?A$Vq\?~ ՊH$}{._.>T1(U@sdaZ  MӐH$h4 8y^x(X^^F:^g%ud24My8N &p8xȅb&% arrh4b 9ɭGvD.*wDWY,VT):Y,3 !)7b0؉M@nXĮy!f3%nA'F`< /$Ienr9fvmJ 0GǮniLLL NT*qW;9mucE~Fp8P( \Q(x@1{=$ #`yy^r  prrI `Po63<,BfN#( X*JQTINC|P;>AtΞvݐbblqt:{Z@"`}}`p JXH$<:/+闿%.]O<@ A $H A ImO n%;X,lpFVp8 Jx^?F @"rZ qxx^N/. ~PTAT>.~t2=>>믿Vd2ӧ`0#LFVVVPT`۹jl63T*x w& ?^N{^/?azzKiصKmD.2:Bai۱PnX," ^#ϳkMPÝ;wp%E4M ?Kn۷os'D"x<FgN#y;]LYX2 DJ ~rOr"ndD"&''n133RrhSvcb1y%Vr //,,`mm p줣drZ#\.c||dPBXhc<{sAvR d2t"`}}~* |fwA^j5ܹeArx<}6V+<FL&*$ ^ub1E\t 0Iwll zlKKKHR $I C\xhGGG| #u9ֽ1R(lD( d8::⁒VNI$Invwvv7gk>f! awwFPr b1r.^pvl6#sRZ~o.b"*  yH$v^?`'i>U.{ ED"U.H8ʜ H^xaNrsxX~F]M )l.E"3m~﨧#b(J%79!=@]r8JB4>B7J KMFT{@R:zZt:z4 +i[^` Nwt:H$X,|]& 8<4Sw^g{Bd?h4x<(gV뱲)l6R)={p5Gj5bFFñԇ|pH.9Պn j ܸqAd2ATwXVhZ\|ϟ?B'OP(ölNKVTL&Cf;w#o;lrec~ܻwO<:G#VWW;hX[[C2n!6pzzErRZB>r98,,,`wwːH$899A(NNNPF׮]C^JbA&A&\.je'L&je_gp8*~{p8;;CӁfbajQ*h^4M}w5fX\\DӁB`INRj|=]wޅp8Z-r9j;Bu:a4E,JAdYh48N( =>>FT2srB R) 8N; B:V+v; d2~?T*4{` pT*>.}J$|ouvPp'wM:Fp<v0@!ؓKbzSu$S7.20N: d2vGӳ~ "/Z6OF ,nv^Z4c69`<"`J?c' G[Ӑ``xM'HPTA1/vROloDxx ZI2&J9*@7 v\&7EPMtgE2 '(M \n ^ϐz<#v28lAR F{\jbVVVp}~D"(J{6 |XXX{;p sv_zNrT*a6]u~~C]Z?[-.."`8r0 MrWh4݅f˗/H$b81t:8==ehNq:+3.]qN_ Ndfzf`V  Zrwޅ/--}T*N9LST*L&T*vsΝ;t:XXX@Xċ/fcDQ b1z`1޽`0lшhıۯ_DXDR*;0.j[^qrdl6P(ĮybdNk"Ljr4^GX6(6 gd fja}}* | T >d2JR}N~\:FRBJ7|}fF#L&~~l6T*R˅{^#VauudD"#Lb}}1^/|>b}LBn fF۷oC@}Cp~~d2!z |}] @ A $H AKi]-xV/8<C~&uttd2 ɄBEvϣC6cZP(Rp%$ 4Mj5D"~ ݈񘝡"z=J%:4 D"rd2|>Q*H}Ddl7yppXwy}M&peCV#b6akk PTp:HRItǏC*nsg4J2=zo}[W3hu8|>zjbgg2 @bdAw.d׳X,꽽=\v bfH^n#- b;a8^^qx^ǣGFy8C,l"C"0LjVd2910y@daa)r96zږ a(ij!Hʕ+ۃhv`x^ x^Ax![]FEF# z\VJ1z=Tn.H8h4 !" %`HP\f(:[ɡJדB_rF#'M\r;R0ҿYԗK3u .^SD59ᩇ `bpieme'xZP?t.d2_ Ȓh@.3hw阓EN~]9v^=K:Թj4TDxrƷ9 n7NY,F#C&pQMQӳ +++p:(JǡT*amm jx%|>, 9vnVj5?Srv-kcDQd2zR)hZ˗FNjfnV\4'jhuܸq0ͨV899[t:E\hH$9 G}|>dYz=Nz hD" IDATl6h4  $H A $H_+ޟgDh ܦ)s% v;^xV ˅V~:Jl6p]g?C\.SZ}wj,//c2@ @*"  k%l"%W)v_-̸nHRh4}k,0 HR ^N5N@??X,"g}j?z9??JcZh^ZZp8h4)t:/tt`ZBx<VCONsfa2x89jr9'|Yd2??Zl6d21d V˃fBn0Zb1Uh4`<ceerd2C8rĻ\.dX,x3 Z@/+H A $H A@oZzסjxxQp:"HB-BRgϠ鰸mvR}b1 ,//=v;d8R2/7 \.^<6jh4ZW^t2`#ЫRp~~'OtBTbww#rU*2 289!H`ee'{=\|_FӁ@XH$<0"4 rf3R)^~#X,,--qh4B";w">EV*|;A*{gD"^e c~~* JdgggF B uڵkhzۍnf Iq'|l6c'w]ܺu jf;fWV . NV+!0H$B\T*E6C:[nd2Rh4byy#t ~_2=??dYJ%jX,h4z ^Fy&VVVؽ)`2 J @Z"._D"R ˅FFp8̝Уk7 ~n[d2 HE4MA)\.Fl6$ "RË/0x0Nh4:M&xf{{{tR)r&VǏcccNɄ{ɁP(dnQ(n{<vrB~蹹9 ~~ZP(7obuuN1FͿwVbaժRvz=~>F ( 8d2d R PZ-LS Cb1|>v.Sk.CGXY}Oy`Tw:2T*JL$0 x]rl%O,h4+tC˝N& * (Jv1Ck䌜L&  NRl2[_tLQ)"$D`o캦cvӗY/^?t' \rWFjo%]5m=(EARqBPt:Z%@h2`0x5 &j5l6 p|| `0`fCO>w/_niG"9}>J%GJt:yz ~ȹ\1Lt)r ^^Fpb& RWH$hZt:e(LrG]4Ay8~j5UKnT*JQt}L&b} v)"U*o;u F#sG=f&HHQ"!x<(m1MkZ-e%;8N\ǧ)f3uNvd2H$|]P1xT łV\.ǵZ\.NNN LtN2N'actKr@R^EJ%\0 ̦{Vr8;;>* ARqϴdB6۷vqzzU~W9&`o4 Hbsss|n^xh4 N N9}C&@cww~܏LNp:h48<<= -PAG$A߇H$bwoGX,X,}r9ަ?\$H A $H Ak W7-ׯ_L&j[Q5pxx]& gggkU_Wt:*rt:t3d2\~} HzdfJU$n0fD 0& n nx<Νbܧ';i<s$E}lll@$* :n{ruvXb1^dHHՊ`0JxNf9 ٌ#`oo`RbL&<z#cR);}>^/d2 <J%>s SE"t:d2JB2|/^*C%|dVHӐH$(J( 裏fqrrˋt: d2ɰT*!?D"9,]nzϖl6lnn\.#qlTB^xZ-޼yC.L&(Jf899nghbAZE"W{jN6hX,T*=y6* =z$^A_Kz $H A $/]M@??A&m620R)MrMSIX ~ccn72\.$n߾h4v (bdR)\.LSwJ*dž@"$H3_ABwr?L&V1;֢(PrjJB^:f3$!KH$xl6KlllphÇ9l6Cٳgc|z?/Fr$ A^$Q\b1V+\ru>f`gXnqzz B^ώ-Ʉ#mt:*Fd2hZ|26wڣG8RV\ַp-zbXYYa`:F2 "L&f\fp:t"?E8Pٳg0M].l6QTsL&T*4MHRj0 chp8f٭j2P(`1 tWVP@VZfWJ|>.A꣌D"T* ]?`mm |zHR|'0 8??gЧP(j!pXVR)lmmA.#Jd2m<{>} \JJ.`:" b4R9L&< B1Ə~#z=ncppp=>ءfXMj9t:TUnvЛL&zc Q( ?( T*>v* |kZ yNԇKRrCI z "u:'(J}} nUzH$J=.UydP*grbT*PhZf%d2Jc!؉z1.a{x<c0@2N" _%H$Bl̦\rf3v=S3]/M=ԑLǜ`11NU&2kx)7E,ӱ0hZV7Lp3JA*BRT*a8h4rj\.AӉ|> J89n>1p:VH$XZZ>8~\$!# 'j=b!\.]~6 jF(102pKzɁK:aV qUNNxD"n7;ijj@9i" b0p0E_ 34OOOh]FQ\~Jv'OWג  $H A $H_+P(/_ta2Cc40H&IP(v1H$& C|{߃V˗/ P.f_" Jq̢@Vc@ǡRxQÇFj6w}RɡahZJ%t]DQ d2|>dYux<mz & ϟ?G4eGZ^C*|\.=m69ʰRcD/_~;\z;G_|H$#L&jd2A&p` P/n?j h4P*J`00PVa6=yV>?ƕ+W2÷lb00(T*HXYY j5 ,8j5~?~vZ-H$\r{{{< rPF#\.C\~v d1c0/bVK.ŋZ?p]T0??H$DfBX,,,,`8`Jl6 ߏ_Wp8D"t:lVjxd2 ;Y t:GmR&s`0t:+GGGO~\}vXVz=Zz~u:NOOt:aXN\GB!B@ Z.# !H Lb(Jp:hZn6!(ԍFd׮]C(bT*y@NBRL&C.&w,7MbnnDm6.u:޼yp8N\΃|=Tx<1f"% LSX,t:ܩ!a6R>n ~XX Vf]NZBvnk4 U*>f/ ҿ' ughZ锡R YGL)޹h0^D&> RK-lTʿV\OǒɡK&dw:2N^rCD!Z.MNǁM]qK`, k4dYvdY(#O@t:}so;}p\0r tnrAT"3E$a{{sss{ ^ M7 IDATJ]jHt~]frLN *Gj5_s;;;lnVt:aprr^yO;#UvWU :hAף^N^SVF,Fj5.z\ov JũߓO>@%B $H A . E }ՂFA{D"ܹs`aA \sss0899^~͋z]1>v]bp8fCP@\F(bG`0@Ӂjnܸ_#g+-bv: ܻwַVq}ڵk8==hFA*ZRD6p8`0ٌ3qmlnnB" A"  "HjBբZh`<ڵko9lll`2App ƎۍX, ^O??OFNqttulootnn0gV+J!wz^T*l6B!>fD"0={SV d>1(T*wSįfD"A:F @,N\.ZhE|Rd& frD25J%.˗ 0~_"0XPՈHӈF(P(8v<CѠ\.c0`qqr uV*x^F4 uA<}zpx&9*%ƎV ]ƷoF8f7ūWv]`0N|>ӟp8R۸Z`0d2a:tj|P*Aq/^` y, hZr9zlmma4C͛DF T*X[[8llۃng'.EM`6T*v`eXFdX̀Q&Rw XVJ# b+m Cl6c0`0=NN r9D"r9;2D"F;X/J7L0+2ɕJF Z C݋ˋ !JL&~?X,0TTJI+F\.|B}ŘH$GjVnCÇ_rF#L&_z޸qohƽ&f0/! !p5$t:qvvfh4`@*@db'V&b18B޽Q0߇RH^ۍ/_c8nc<wŃ J177|;>j5"4\z(J(h4T*&~ d`2P*j`4!pUr9<~~>/sss|Z,Ev_' |>H$F>|p8hzd8X,B>uZz=zGz<vZ- ,//֭[ BJS<1"zl6 J HI[aXP(WފlFB j5GAb1vG޼y?xPn9Zb8^s:ŭOjJzvGv]1.t:(w6+@S\lEVQTW#/[\ΝtS?.u`(9|&Ҷ#JyJWrCoOb 3\?3 = r9eDq{K@TѠ1K-PTޥsM\rS:mkZ4HD4n4P* λ.J%ٌ<GcvK]5Ne<';WU}fp\~zz ٌǏst:J0rX* Nf՟9^/oS׃ZFRwhlf'd2t:oPo9R).RB!v6onnrG~ՊS~oېdx77˅|>~ǃcd2v7{1pxx`ǝbGGG|Pո|2rGnC"b:d2!NCayyFBbD-XӁD"dBe^Ǐt: ݎ3f3F F#rAP`oo~!V+ONN`Z J@r5M<|>"Gnw:'''D"X,S/rWN< R Z j2h4jx!>ߊ 5OS)>l6vx<-QVwt<ʕ+(J(JtGp"ǭZRK<ßZ0U/aIG!EyJKj%wdH.q!&J\$)]~&G(Ixclh4r41b/:V'%P(Xx&: 8b RWE.>u>H^g{B0" ϤEP\-::},&LRfWNOU8>>~Ojh~Fv'@K=rkӀ9J5 _Q%!l6muvh4d2ll<( pxx1RNlV kkk|OXVb1cX,t:vuK$F<}x@PBI@RaǮ'z[ǁ  CT*FaӧtRWD"R)WTaX H`0P8U%oo뱴dRV3f 4MN4h4 a4駟 WW $H A $- E1(͗/_߇nG.L&%vUB!^^\\`GEvHTb}}R X__Ǜ7opzzh4 \B FR Պb\.ՊW^!pr2Vew7o9bj!Jauu2 ;+i[P;wJh4p\fTUv͡ZncuuDz"^~t:Нvr=<F*J'ײRHEe0f}y/XcЗJE>U,I1˿{9i|=NV79V&7 n7V+>}rmT*>WFQcv:^?{^/S|'x9  DQDƻヒFP:hW׹'w2@'bXr:jT* 2b\d܃<בh=2xB![[[Vη7n  r<7VVqr9T*jnnvT*,//C x<{LN]J]`0><)VVVjq~~Vq6!JqZ(VÑ8^/"1J{˽H 3l6J %FQL&fZ+].'>T 2 rz ANZRɮinzb$[ S ^W:@c{* =+ hZ$IhZjvPSps Dv:E;>h?¥LQ,A[rtS0`_$Fm-bz=%Z@r#)fWp\䰦3ھ%*̔@}d:Z Z~iLm:J} P/:לN't:[k ffs R0= V?O?E4h4.>S~𺸸gϞ!Jaqq#Z-~?Cr׋NVB-b<v`0KhkXa}9VZy|P/1z˅@ ~Fv\ס뱽Ka8xZ X t(hH0Lh4H&XXXǏ 0Napyy ww<\19Cl6hqD^Ϟ=XoQܸqlNrMn0r\]]xKסL&cGFtl] n8::dD8T.cuu㣭V+1F;\:kD"vQ{4 >s\.E,,,h4бr:x5;yAP # hh4"# ꊓ4b T`cɍMqzLoFP`'Af=tUKѻ4A]?j1h48W*va0;r{^i:jV=5K ^A8>,666.Pwr(7d2裏 ɐNqpp^X,p fZXv;(/_ftÎnrS6݇m6NOOm/^b4 5RxgX]]EfJN ,>n7l6<_/42C JA!^I$I$I$I$IKz%"fp:'jˮX,r&A@QQ(F1ϑNaXJp}DQL&4 |XZZB</_g}Ʊ'''KRHR:?<==BCÇFL&Q( C1Jfgj5d2n7~c{ҥH}PD"( ^P(mh4aiZW_} Պr^~tz`UsqfR)8N\.q^~ L@ Lł> 2F>\.G(B6x<8[b~Y׋Sb{{n. WWW 9fb>|O>n(ppnra8^c4vceer| vvvP(8z=B!cub00FFx<p8HЍZ . f](v#J1\j7"HP(rߣbt:_^^^"2 BH&O!9b^s^( vjxwJ`2\m4Mvl6@|OOO/x3PGvG8l6A@"xh(h)ZE-󑶁9}.ǥ:VZ-w`D S.}y0@VCRq6J V\k`XJ 8::B vZ!FPd`2[,_Zcww:{)Fd IDATwj5Z-?ʀd}zh4 ݎho?3(JiZH$xhz...>@u>j 7v\\\p:z  Uh8MVfnc<#Ap9e2l...`ZhPVtq~~C JA@/$+I$I$I$I$I@[zoݺݎ`łVŮ1rd2,//s Q*صFRJ \]]1(r_|M CTUχr\. \t:.Io}9,>#r9nvt]yfXXX@X\.ZT*j677!QVa pxx`0JuEfn h4vhl6nf</@ELS666P00Ͱh'Opf&`Q9繿]> "& <{ C2S rj5cz=APV cwCFQ~>==E*9V+N'j%g2XVL&7JKP`ccbNlrc6~X__ VD" \ZZB>dd2 ^ZcDbvBnB:FVC0ķ~KBs;w0 ,}F#Gz=T*hZM @;EQp8hp8d0I]DQaRnV=tɪREJPE`/l6Z$IE1!G4EΒ["Y3 '.BӺr7Е%'(3MА;7L]%}~|9E: S?3X9C]рNÃ9im6Ĕ@nb\דcA='q bCш#`\@ NB |>o&/`08xbJǡT*ΩIOk Et`}>j5tݝ3 666`XPVaZڣ2|FL严pQ 8zyA\`ZN|Vp NoF~$ J$I$I$I$I_$+O?* ՊB0Zb8l6 ~bdfRC @E\_Q8Xbssștj2P*8.ZVfh4"J1xuDQ> rwEVhJBARᇺh4݅h4.W^rf!Jj5I}nT p\xou~: @EF#J%p|| R χ?]8v;;;ncw&;' *&3 F.//1  |b36,,,,w:v]\\`{{$- d2x<j\\\`0pܯR^VЎY% "ZNNNP(p-cq|>z=p]@2^F;{p`2sGD8);P([5᷿-&}]J%XVzDQD6( oNk4frh4lKB{{{X]]hٌh4U( iޮvb7o:#JO>L&CP>BZ&^|`0{K8yrTX\\dd0PV1l6G"X,>n {H$8[Vɓ'JDŽ\:)fzN3 ǴmI6z0|(&lBvC.[#Żd$h:R`EvL& V& "ntKn`Or`w=A~ϐ񘝇I|D%g=z6$P;L*5JPpW0]OKY}R8vhK.NU(t줦h' F[jzi|ȽLgz]U&Mq侦B}OW`0@р(4,Dkp&Jhh4BDJi$xkkkLǏr/ŷݬhMh6j^o60 x!z=\.֡A;W>nbQ9osyyyɄ;s^/S_ gK J$I$I$I$I$+ޟvqzXYYHIRɱX j#wOOO3%RvQVT*a2؁H]|p81H!ՊSvix^L&wy. j[[[ܧJFamm t~8NTテl^ۥ%TU ΥnR-R). fZ5 μ[nq0.^xn@ .GB@ V4F}-\(r ^ljLlaf*L&1LX(0ϱ7XYY xnݺŽJ&N 8 ZQ@GGGI+˱p8Ǐs#A5>oub1EF#|'xL&9W&tCcܼyNLB[[[ R b1X^^SL> Z KKKpHP0LX,hNrd`<|^frDkkkk,^/J& Al6)9=R89ŋp:p: |>A?__NW_}hh4 ǃfpp8>#Cjrl6nGRXTm4P*(Jp:oAL&7o2Phf:ϨKܡ=ܹ׉lsJ߿lH$]2Al)*VVVNa0LGE~j5*Gժjv]RVeBN`JX?V;2;[i;y- | n`zȯMKb]T*0`"KX K1ɉJ.RJ K:v)w(ZCszu9A\EZ&F|ݽ|=xGM/]` L%p>0#GP@9m +%,ɐL&t:%(.u^^?/O:[l60Bs[:_&d2\=Ul6c45K6 ~6;U*6<fǜS&;J>fTAJ#T*hpL&C\D"5 C\.Pl6CP  P(R`uu6 Rxϟ?nG,ׯZ8;;beyܸqz&Z>E%5uR24 R׹W1wobQOOON X,x^ t:DQP(l(Jd2H.>!VVVؙ,--\.st:EvsB!T*^64 jx2d2ns8JcX,X,F#Z-l61Lpx<Ύ X]]`0bMiB!l6}ax~ VAEڭV &丣7o ^@b2 vh42xdB@&ag#MI Lp }6rR5 ds8 [&3삥Qb⤸jL?kZKn^w!L2S-msJt:x<8gmt^`;z7Z&AEvkZiZo XFMo6qd3Pߡ| ~o^^]/' GvZ-7`4B| _4LJ`Zd2T*p8#m2`xP,9q϶L&l%Jf~rr_}0 v)9 r|=wZd2 z@L歁rJݎKL"70d2a6l=Zs|>`ccQM`0LPvϟvsE^{- Ex^r9NӘNf&?Ʉn ӉnNÑ V`I!h8fA"h4r\ݎt:D"JHhɄ>\|;;;HF n[1 H5" 0x c_Ν;H|tt ͆+vKjvr9mvxeYTUB!nzdX tt+ G\R)vsFP(h4T*H Z$ (<.rA'OvvN^ggg|nv:t] @2'T,jCR!gT*n#PiXbaa#\ zt:2T*|Ecg*<==e@=z^M~Z j^ajE.,,,@E_ZVa4jZv; 9"ݿv=Nj B J {{{L&O!;Sޯzn{əvyP(VFƴcl6d2vבY JZ #4DQjE*bk"@4> ш]?9OM/ǷR+NR-9m6|ٌN[}JuxLA)M( Xjv`:h4uv K b=%Dr>P(h?4|CATV0 :#)5Y7MtS/ ZQ0ҾAr# (c; `20Nvy?|:"=ΟpF uM&!l6^/F#NNNh[]ףhhR0vIX\\d:x\Οv} |m}rcc(^cqq. N]4Bg`X`8rt:eg>V+?tJo~?Cy\Ή#* n'''COlr6& M)J#O("^Elnn^p:N' N1[,aZ!P*Dgn3dl{ w0d2!ruH$jL&! vs<1|W0HRqt:z0.{:N' w7 dYiܻw X,B.tI^/kyn( r9+^V BJ`(ٳg|zX__Gc$D >v\չ@ JrD"^@ .=r?==ZV+_:88jeb6qqqA7߰u's&A\J%* T*LS$dB<B!K~!Nz\6Fz=ܺu fLh`Cap|6^h~t-G"'Js`wrς )PTjaZ9>; F#Z;ZEQdNn\6E5އ/#OS^"RBJ b8GUvF+W\.&`K[ٮC|Mr9]etu#*=P2hr>L&VXNr)ScE.dM_jٝRP׹ ރ"v;C{ꇦr^hZ(JnǼ^l<d6aX]yŞ}8dYۃh!r^\F=LaXvQ*P*Nr ^C{vh4X^^kKagub\.s29lX\\kZ>fVVV1P.ܒ+_Eep8jơ5aZOJWz!^I$I$I$I$IKz%غKnfZ9 IDATV@ Z L&BB=nh4eXV?>~?:Z-mggguZ!#B&gjEZE.xB A@,hVXYY~ ׋/^`0l6sL%Jp8T*Z ^ cݎxX) !JAR+lqq9t:...Qh4pxx%>~?^xz.ʐdo4(תP(ZEt:8 fbF~nfPz pw-٬V+;ίwPR&,J(|ZF[,}G%n`"#& Z8k4N hıh0 E?ޟS29I3e@v ^*ʷ0 5B@')* ~|v ł`Z C(mzrLҾ!@LA+ T*J?u_&'mcLiJ%lN\\Rɟ2m/ svd{-(b `hM7z~\frbwv:p8E^ggg>A_&hN'f3.//yb2pL3E*7MP(<1ϱpNJs:\.^|HFmht2l`4Xzחy:)9T*qxxh4ZPīWv1x8TcZ-F#AR(U-|~~S JA@/$+I$I$I$I$I@[zzx!_~!Z-,V+Cۍ~VD"tNRxRj3T*L'u:b{{d ׿5n޼]z.;֌F#].ܹT*Z-w _^^`;F#0, rܼy`p8L&8<C|>* N&"ɓ'WWWp\PT A=G]e_|>?~xD" %0L@XD8FӁL&͛71 O8 }wZ,vz,..BCEx<r#j"Lb:|\o O>t:A`wkP`h4T*v0"u: 8raZOz< . Z &CV#cii Dg<͐L&q eفDVx<2L1䠭jVzP(iZ8Nu,,,04LHRh4X,PTpݨV0L ؏\.~AP*#j @GلjbǝZpp.FGGG|Hp\EUr\$w$llPThZ yܠGѱS>*z=kA@d na4t&WZfDbe No)aHdz/'#L=ԇKYr+z``4M0LP'\.gpFJdp8&owZ}Y0Lcr QZi_t]^- )wuLS1&GC7tT*v\ iZvӹJ`6R8H JF@;gkb; j4<FNLi#wнb@VT*!Vh4b2p*r q4q=:ϱ=|. '''0L U* "Znc4^fass zD9f!d2NOOt'p}P;  |_ o?{} J$I$I$I$I$+ޟ8<<*?y&w.--a:B.cuuc)Vxmmsܾ}Z vc)WV/d&?7CN{{@=Pz/ F1a0L&qxx\.Nr̮rUz= ,.."LT*qO +;ŨjN??T*AVNdT*@%i$ᇦO<**ȍ |>|P & A`VZ^χl6qKKKzh4X[[`@>6w\^^rglۅd2h4D(%ǯv]ܽ{ATU\\\`kkz* kkkd2Bq+++T*ocx^|>r9'vvv8:`n70={ɄX,npC(JDQ trlp8D2D:OS|~>Rv/..UGo߾rn#J˗\Ё@GGGz_~%B1qvvNt:vfQQNC2& WᅦM`Bn |?#dY JϟP(޽{d2 X";_&Hh4q<CVc8wEXD$a@DLrgn6EE,%z=B1DQDV H$z0L< A۞dp8J{aۅL&h4 QVފfp8DRFV+wO QO*a% s&7jnYrQ×Q:I.%&*2%/9 pR<[_&; h4ycZ"JP&}ݥKtr9?JŃ h0=B}G^'vޗiuMOdr0T%0tVRs:i`6!H`ii-IC.l6T*9.&ȕlۑJv2h49rpvvChZ w܁ xinC !3toG6L&w9R\ZEݻw93A˗X__Gb7e߇BngX7L؅J?NbZ -fq=v H$N'GIR)T*>Xd12׋'O  ˹X,."F#B!.aZqzzn#>hb. T{Z-B^|S|M}tP_?fCeEӉ] CܺuhSN"ZNNNpmDQi~`L.Ał/[!"Cb~NnL trEAZnGZ%GOhnlt"`6acc>\z"בNd(Kv)"AnGAل`"wRl4FLw}v!4]glhܐf3p8ՊxbȀ"=u:&|d2 ϐW_}-ou\v5n9:LBPt"Ab66 N`Z R d )Mp8|8<6%}odk伦cf0xS,3Ur3AnrV`zb&xh4j:R<4z˅xGF4nL&C.hċ/0jr8h4f!A&qZ.brԎCX,0z1N9Im4y}k4|^\\ƍzn; Cp1}lh4 bAP}fÐfp\888ٙz% I$I$I$I$ItIWҏ-w}{d2ܽ{)J |f Z"HjsD.t:͠ٳgN(B^xEP;#ܽ{Wra iܽ{jGGG}6=z$08 v0G6&26=z$Jj͛VjrnnnL& JL&Y2 oV@`> 'h899Ğ^AHV1x~_'X3?6Lڵkh4L&L&T*( bhvl6 nQ1bj 5L b:Ldv:+Xd"FnF2;NpG`*6&@6'AUTr QRJ?+,Cf_V^z&$ZϽˬٴT0F#:4nߡDJ%03 gݮicG{d91d<[hILe)Qtt?9h[Ϯ0p:rECjqqq!v<$ztb1Qe:<<Kpyy,-- l؇xf Áf)YGh4\.Rv|?O0p8PTP.zdm1L(ppPELjFl/%b!`<p36i+mY]]9BV &^@39QTrD]\*h$|&~{\Si_.^WPAZjZjZj_zWz_!8;;lW^ƍBHXYY^CyjX[[CӁjfh4B0nG2ed2n$ ܻwnB2ׯ %/!y T*8A<{ h^rfVKTCD60srt:?hBAT/rt:hݎ{l6677aZŞ1P(f^@݆fk]Պ;wZF7 0:?͛8>>F @^5ٙ(`i+\U79f=z$`CƔcF4 %Z%Ffnu QARl6CZE w}|.*SjRUg8jō7l6vW<tD"~/*p(x fy(W^ ކdB>GTq*h[[[Jjᣏ>B\FXč7x<Ӊ;dY CJ%<~j0mgg׮]q"Rd2]ɺV؀F@A R4@TUZ-{8>>jQd2wO>AA2XFy\G,dB\ ~L'-x"^vjf`@߿b hZ0j_Պn+p TJr,d2p`c{pV]%ac + U%p$QАV i0WTA@sVEW/岂O˟i4 CQP^7%,44*d;cz)%?ϗٽA1t$'cO g* ҩyq.F#nY(j9N:Z VUM*f3N2jǏۍ/^W~T{Qz%{aaAb8V+RX~LFr(fs[z=B4L&뱸L&p{]h4@u-P(-zNJ3p8p7`qqQ>|eZ-c~>X LF@j\'Rǃx1ݮ¨j}RA/TЫZjZjZj^*U.> Lׯ_CT VVV`6a6Ŗbٳ f IDATg97|=TpSj$HW~ӟ"HP(j'?q.--Çp8bQ|vvcuqz=<{ | J0z=qnd29D"qH$J"%믿??jX$_h`@8JłL&gϞ]b.K6O;S\.0v!t*hKR,CZlL\0DVbuZ(Ņ r9ܿD gggp:tHRޖ f)V{{{0 X\\DZܛ7ojIf 6M'''zFp\b p8rJqqq!xnSF#b1 C4M`\__v V+899h4X䮮Z 899AӁd2o[Fx^L&En>p84x>{ pNX,b<믿dbAZ½{`4aXjN"#Nc8-y>G^| Պ%|bunq~~[n!NAQ@QV˗2sMl6ղш3l+6& ;;;XYYU -}EA*%- A@X2LpqqEQL&FڍF#,VKaVl6`0EnGՒ%]XXx=w>1<.FE:󩎼l4@Cm\Za.>^5bQLn ؔD4?\l&23T ]9Ne у`&P%D#H_XXb6z+6FQt%;k<K<(A:%ǖK%P$r~w *5m,kW H$"1 8==榀~6cn7> i+nrf1Nl6垪VrF#$IY{\7[*www( JdRqde) XYY(T*|0ͨT*s}B!XVX,i8PIj VrcͦvBp(XKKKWɷQPT`ZeҴuys#Jt:|vp\.cooHD: <*U{ z^RK-RK-RKRAZ?t?͛77ڵkX\\LǃCQhy^ Ff3j5" h X]]Tfw20J}Ubii v[TZ_FA ^. rvwwq-ofDFœQM\D>^\.ckk p|BFQo .%sQɓ'혿c* ABh4b( X,&ﰱL&'vtt׮]C^GV $qmm /qM:/w}& hNmA"? ŋ*U{s;k^RK-RK-RK-g^~"'}+pxnjMֳ38QV*x^l6dY TUy<ep86J1fA|裏 Z,\\\> (di5N ݎ7o(bdt:LSx
@l- ~?4 P,hs766D %[IFk{x>d2kL^(X[[ý{dSukk R Hd2):T*ByDQ4M QD&I8N` fʮh`mm rY,JOJ^l$AG2LpUQgA%'A.ạd2f YwzߟR| ei7N[fRs %3WL;Qrgl x<kw?հ̉3{pxiwM0 @3y9.K@-my^O.TQ ?.Jl.}ֳrn2+v\`X,ybAדKvhny*̣(, >U - &Z`2p~~.Ŵav8"vv]G|>ZqNF~}7]0ؐ@ Zh6b!3ܼyN+++҈(شw]L&Y2, ^|h4pJ"ۅBH$ (N|ّnD"^N# X>W*ϋʘf׮]NãGTЫmE zRK-RK-RK- zbIMHn&Rt:eSj <„ôA.x<Ɠ'OÁ7o 0h47 VWWQ.l6}j$\T*h6888P0D5 vQ,q+|p\b Q.p8+3Zt>@ T=qwX]]d1@v=T nGr>=HVm$' 677PV%ﱳpnTUV^~fNzbbh6qvv-6^χ`{Z*^~`0%kFA>^G0W_}h4 <|ϟ?UncyyY@h4‹/d@"(baLк3{Vb@6 {{{T*e-S8nZO>A*iL{,99l6XC awwkkkdh 6jժ@<l6$8t: %KW H ϣX,ʽFmǃX,&@ǹ\\NԒ LpFj.C=X ХRv`0 ˉ7! !Ɉl<K x:O6M h4rakk FbPFCRc:BQEfqrX \FR z. J`nR F7n@5N111ŦfTu:d]jjCy3~{PH~9FC,a5@5fZT(RKgl|>GM[˹lf tX0c|TS& c* z/cc[WTl^󵴋fv0 Bө8o/5XSva3+jgN'A !`sf/CQ8Nfz= qM5-hh42l6lV`$$%D}9-çө3VPHdP@$ /tboo bl6EYˬ_H>AI)TUQ;J%s|> Fp =Thrx?L+&(6*XWUe|G>U0 n74VVVL&t:uO*(>:Dc6cm|>lllVp8F^ ^Պz.Yv;;;""xFK+4- |V޼yEQ`XDArDEɄl6+ݣ#X,v jBb"|^WN>F#̑HHlddh4evfw$*}]T*Ɉ Fp8u.+j5$ ~ǼqdRY jaZEvblllǸqV+,[ZD9Ħx:B|* N'6?.͓Dg+ `4q||,V̨5X]]EzRA<FhJ;^ǃyɼt]Z-t:(˒5!χnJ+Pn>r 2 r N՛&>} TUx<|bn`c+Ǟfa< lfsj ֗T^e~b+cVsݵZMzh4 n'BFf s*L&4 e%k0ŒJ`eAZ.tw,dƟM&lPE 1r׻K ٲIK(χjZ:-\h4v J9GJYakӑ&pm3bq<a1LE7ϝh4 eF/4<7dYf(N#cp`@Zd2 N#{6bjIx:Jf.n 5.sn7\[^nE2etV‚4@y^}iNb0YBh|ߏN ?tshlB׋J&Fbjf!v yN&ɷw8T*r߇G~xR5M=q`s‚(in6 jUl zb JrX]]E`0E^jEh4BӁ8qqqh4@ ct:m4l0 zqjۡh#lmmtBӡlbaa\N,&ݻ |ǘNb'p8d3Z x\3VfT*8<<7HŠdEm5CӪXBX,&{{{jl(f9*2\.vJy^hZvc:J)/=??fC4E2pfQ,Q.m (D"!N#9ZW>n4b %rRpCQh4|>hZ\\\@buuUl m\.={&vtpfXl`  P(@j NFfl~?ΰOb}6t"oӦszKKKG"*0B56|>_~%& n޼d2s,-- ,ZoA<Gꪨ˨N"c2 H3_xPq||,@P("VQZ-x^,,,BG f4 oh4@T* uX,p\ٸp8$SlV 8o$n{_W٫$C]f&h- ݮ@:jU6rݡ“j`0yRQKU#)A T;J%ZNep<|>~V8/mP 92[eaF# lf N'k0C9d01LJUMδ#XQj]ZOS4MJ%q`6oە&J"yrdN_~gSJ*h4*L&Z-2";qW0J4Pp8 nwaii fn[/{4jRHcOZ6Т1FۈD"qrr"F(hZx^t]QyZD bQ8G9x0cfЫRK-RK-RK-RKw8X__l dшS"nܸ~/zLTrR d[[[o~#ʡr, rssp7o!J>sR)| NF!uv>>>EVl6د~dYh4r9٨&h68::BCZׯjsիWbuh40PTvEjQV1`ii +++kJ%m899dxDP*$;rsj "0JRH&7oJ^j|GB^vQ*0_Z,LF#ᰀRB nxq}FJ%|>4 J%d2QX,Jp8D4EXD*y IDAT׃VE.C*B߇fC*k~ML&ÈD"8??NC8F:F^h4˗/a0(X~rSR@Q,--^KbKEeVGEQl6ee% $jb/tCDQQj1yشN]]]ŃnEK^0|>N N'*B4 _^W^ <::]ZE<1mb2vwwx0NfvJppl6}VVVpzzl6O>SD"+|nHT Nkkk0Lw!:49l6QV% HӒ*ׯ_y? SGpzzZ :XL!.^~-ym{F @Tep8t:嘍F#u]]Nk0ZV.t:˙1!(m  N YE9/t22߱X,"l6_9^(K)ȼPi}Y) k>%\ 9^HA͜T6PsdC \˶`h$yOgR\"ZS9@:T|>~?z8ij!L?)+vl0hZYpXp`nW/aZQ*$ t*{DZłx~/߃(㏡hP{ f13P| J48Nqg`ֶnG<G4EC(t:J(1l.K8E5e8NO$HRrv0 b~-x^`0ӑ^;w`<#/|UKUK-RK-RK-TEZ?tQ c<ƲjūWdp㘙|́:m RQ. O>l6%g@hߗ|fl6`vH$h4x5ϑH$PT˗|BdFGlF6Miz=x^`&A4ś7op]ټJ -KbJI& Ja2ՌX*FF#l6T~ L@D0Emp8 T`߻w}l6@ QLdN fh4BTfH[TӉf>dR/rݒD0PV$W@ףX,T*rˢht}h2DmXP(E1׋J" Xڗ*=~Am~Qz?g0l6óg6z^f,S*Πp||E%{w9aooW_~!~?>|-itjFF#v;߿ɿ%fud/_" e:"HH6Q>j8edTvvv"X,Jf-Nvey_  HDpil6ŐE:%onLFHv;.^/Z1B()޼y#N f^WᕜpLZF%p!r?R:E(BZy5(\O9^f2E3He Uz^vXjiLe66VPV˄z^~EcfSNSK#hPJ*mu:.`2Tgu-Xp~PuMkıq8Bt v8==|Z{&[ᅅqt6"<P<ϥ#͊mdw6КhHL, r(rl6ir>0fXS!h}4M}FPpX 8ײVi>HRduy^f3zppC^G(lFVC^'`χd2)Mv'''~6Mw \.D2%LcggGU?9nݺ/^PRK-RK-RK- zO?eո5ۃ^SBՊd"h4 4r0+`nĚfh4z=E͛bLEMF.nx{vvՊ`0(JT*h4?\ɤxFX*fQݎcE8\\\e-7_z`0^/TC`># f3sr8ݻP(ƍja4z1 n0p8Gjnd~F| R Kn7M GpX, hW=ϱ;HDK@@6ikj`4l6H'Oxt:a2Zhh6nƞj֭[( RH$$s~_ԠH(ڂN×_~wj' (JfX]]ETX[.R`6a{{b0 jކ(G Q(d`*NHPh4x/^ d2rPFBX͛7%T* 4lBsS Zbqq.K̯nܿnv]l$r\rxz ׯ_k60L |eX,ɚu\%QOSDQ<|6M@n^d*;Niafg^F!̗z|y܌BnEaLi&bn&jK(`0"$̣pcHGxl8F" VBSf_V@: ,PiŭhRLL9&r&DcDM+NGԼTbl6F#&lh6uKrz^2rrY}$+jڛdz=QZ,%ͱ|.{- `0@ل`@LnV1n x0/^pE u$IXVmaD@%8h^T`6(Hχt:-Y,nn[29kkk(JFFfF5vK  )`8JCz^6rfŅ\s6?R)q/m6p:bOB`0F!0^r!bggFׯ_qp>,8Q?}Tj}߶nVAZjZjZjZRAZ?t޽{W6bkl3h4b>\.tbaaBVHz?KpdDٌ~D"bL&p8,LBh4oi\T–0oT xN,6`Ls^x!j[n᫯`:Lb:\r<+B!DQ C4*U.Kr~?vwwa0zT*AI;@բ^lQá(/ $n#t 0yKP1Nzp)ɛbkX~s|>D"QR |ߏJׯ_ X,\.F#2 |><NNNFvQ,5Qf2F#v| j~?޼y^> zp@NǢvf0 B8==E(B8p8DPE.^@*bF`0@KZBBڢj^*۷oK"3yHGN¹cXPVBbjZfmfNTz.0l0@O1'O`4r `( h%)s?t:%[VFDBXVy&n4d\v gggðX,x (\GBjF!k*#|n[rq3 j|>@ nK|l6 ,jvکXPQQ_`1OKZ-i" e,v{=L&޹ $y}1\3a6}d~ 96dsgv+f|>& fCE6\~ g\V8S}L;btR$@ss\x-v;ZX\.y\\\Ν;b(zu^J`6 NG銢jF,CGZN@ jtX,*b(/6M^FaQ*YjVw:aivB\.x<.tٳgT9vrpO>C VEՒ@ f^p8,|v CJ%zx<p  s.lÇ@: 6l6 WU* zRK-RK-RK-KjuF"P^/ -\.ݻw( vvvDmex<DZ%z$ +`P-VWWjDIs~~ٴ=~3t:|>d2t:Fzft:ϟ#J%Ja}}Z V@6/L&xP.EuD8oa-]pL&dǃr,jN#v899Aꪨu' p50z8;;;#pEQ\sxYi0L&#@vceerYp\zXXXopM}v( CD"|WPEiݻ۷o,GGG=>>Ms,9X 碸|,a4Q0LSd2vLS|>QqNt]h`H@, ZW)iz bh4x%ϱ,0p/_ĭ[0͐J䚳YbB>h_KKKbKEE"Zl@Գo)m=iFj`0hm{^c׋FtfVu^w8vj. JElm6ZwEل(ik@;+7Mx^i8NR)oTb1J% Z: r$UUvPZ gggb;K2L.'N4m%ljEʌNT@ ʿ@*bmt@$ZUEQVBgy$ F6?PEs3k:JS(rNG.l" cM%,BrQ1JrfQ{Mלpv l ༺|fcCՂNZEQ ^\\ `0H\xi:8N`0`aaA\*[gv+qQbF{ssShT1lx>c Cɤ}捸(Bpš6̸T*NBϣ\.Z믱,NlaCgTЫ*պ*UK-RK-RK-K/CA{VT*$J Z_,WTM =99|>G>AT֖Z҂7NKV%-8zn. F+++8;;(j`D"VF%dYt]l6Vb|'J{Vਢ+bLPyqqL&#ٺ|r,ܼ2y<zwAT`0@8 & B=zL&۷o?k_n駟b_F Mt^^eTZ4pHu\d+KՊBFx<.f=F'UtfbQ̜eՊ;wjj LFp:FGC<GX(X,\^/^zZMQ_vMMx<v1h4 TgfmRJ0 t: uܾ}\B`0f9&(1nk*iNs8h4|>uHףVaww`FQ԰JBX(fx<l6KsWWWd\.it:FBD"N#`lLIul^e'm93*r Vw^n+ EW 3x( 4ղʖaK1z=ycFXc{xSmLqi63ȄĴ*FQ|>+|ZgS)K(f+U%ahh4SMXna2vL&E CizFK(f.#!F4yw}p8~f `\I$( :Ix^ɻf\ė_~l6+Md|PFC{T*ܻwnX nװn6za^2(2(2ʨ?2@Q?v~gbwuu{I"s0j@ EQDj`2p=XV u\jb7Mu*1N1q}}QZV ~n~_r!%޼y#֜|^`j(h\.|>.//FcEL&d2KF|L&QFRtnlVRI2 i7Ml6NOOEy8h4jRLy.x<ƿӧv; <؅J%F#Q-KQ RIKAD"h6a\`6qxxh4lMQȾxl&nnW6D*R'X,iPUU{{rrNɄF!6ЇrY+b1 C9'G*<Z( jc|>ժlXV+Q)7obH*`0@2DTUՕNOO DZze.+?w 8In}clmm 4H$d*ɤǓ^'j[edYiat]۾B:h6w]դQU//_į~+$ |ƫWi2 n7DshlS-A{Nkdp*u]^Y\[8Oig!9 _ IDAT <y !iL5,ܲZnb1fSQްnLQ:Z-T\. qH&w\=өX3kZ?66l6#H  b0sVQf0;]Eyh4( B|bs@u8ptʜfjt:`0@V;}n=>ݻo=o"1kN'4Mh4l奬~vT*ѣG4g.H$vbiXlo k*FeQFeQFeQe^~"}VREBl=ګcz=Q:N4M\__Zb>f5*RG,}>df2zaQ.E9n6qggG6JP.v#f`P,Q 9!ZrX`Ru4Naۡij92lZs3@uB!t:x^ BPg\^^B4b4a\jaccCITz=QMCB! ɖ 0L(zT*DX,&v?G(l`0Zpvvłp8,LR Hpr}x< V%1fChv MtZQVzR]*Fx8 \%HUjE\&)Eqpzz\.'0d2b(¼Tq׿TUH$X,Ė r0nG߇X 9>|D"hv-777bK(H$p8ki.!P(`4Ȧ~ل8<~/T2˖{d2Az_K @SiX,p82fA'.TSu:N]~Ul[*o hv]%Pm3|վt`V*a4[p8M͆l&`( B5:2u:zroy޴fS-y_TӪ#66j5$ V+ LSv峄t0ͨbrYfa4s;d2A$Aߗgo4L&sd2aiZ5NhۈD"( H$@Nz' [TSgS~'I8Yvvv0na6zQq~~d2h4*5I]1Z^x)vT'IuassZ ޽Cev| øF 9t:5**+ ޾}k^~PǏFeQFeQFeQFie^~"h4*Iut8PUH_MHD"dj*PV pXrerxt]G6nd2kQPL&v1ϱ+VU2 ) ÁT*z]Q.\.FE!id|>`P&vww\.l6:PIGCZTB ZB(,NNNvi+<~nW1u\888`*7LbA<خil&KUUL&T*" KW ޼y)֛TZ1W\.g\"JhR ~_i}~~.řLϞ=ib_ @G ¶l`RvhT~^OT"ɠjɓ'X,vg3^jE&Zhz~~.J~Sqx^iNBPt:QT0H$"vgrD2DדF^CfB!!XTEϟ?G(Bݖ5?nx!~?(l:0lJ*ݻ'̹\xrAd2Çh4nQ*nB_M뺀2*ݹl6#K,sݮ +V+x< @“6Xf3 ;38/UT>n2v6M91Hx+N#hiz;Ҽt F899ޞ4ssMӤB4TUE(bSdY"Y4B!4 XVilbUfS|BV%yT)JlmmjTڹb1e"F岨-///a6i^/N'$r61 bȵdeZE"@Rr5烦i8>>ёdJ%QhR|B!+ tZl~?</rNGGGz4 WWWǢE5 ?`gg.K;!N(l6cj T3g˗x!V(\.Nj0zFj`0W^d2!jlt"N#HHT*B\.6qsiFI2 AjV^W<Ձ~a_am48%4F;M%#mW}>9D=l%-\x<aP*c;!mi[L8NLNS^"@~F@~c 2x/@ ϋ]eӑ\_VIiZE ]Jr39 khh4-nooFrpssɄX,&~-B~Z,ulmm/ёw*6l6dX,jX"r @wccV x)z(i3LZ|۷oaXDkZv-VU6Bpm]v[ɳ DZ hT777 ׁ/ȹr9Eq'''x)NNN rP( `0R麎d2Y3=X.fYf+T6^(|.ݜô nZn [mi Ӻ 9Q\T0TU 8sq9gCUUV/Ce/)޳g:ʳM2TR @@*a0 "eB\g0܁lq罥KׯA7ݺU3vv]9lFIXMs :稺\.q7@:i(ི K7TCoooN8t2}$ M,GjbsMv% Z`ccBUUZ-B!޽͍zAeX7FeQFeQFeQe^~"'ɓu:888@,KA=4d6& n}>(l6vwww!Jfe^R)RR*Ɉ%sZœ'OX,:\@ v taV\"ph40_PHDTA`J>O,k:$өnRvz"D"|D"/` @aX7ߠVѣGPTdR% wh6~&9TV+yF UXuptt\.Z=L nd2 HzA`kkKFZŻwp~~Of;UWfS,I xb"R)Gf^p8+jp8T*%h]G%CTB2D\FRg}gϞ \gm݆b^P.vP(@UUQhFFT*͆~^`0(.K8 DA8 L&*پ(JrMiXAI[VQ|>z=IvWx@ %WWW0Lrv) @ n+Є㋟S!vQBr-Kx^h'lwz˥rLO&Q^Y2i = h7r)jd#%a-ʝJS_Ϻ˜Ў`Pl6H3b@Kku+d?-(h@d= 7-<9.L&l?og~lp\yo].& @x,cO)>3PnZƲX,bssS̘Nh4^ߗGkNt6װ{{{bO?#AUU~H$εbnP(68>>(xd 6p8c B}6܁@c2NOOfVz]a>K1-glnKp6n}v{F2@/ kQFeQFeQF^5.ޏ>^~xx\2hipyy)FCaXd2Bߏ` ÁgϞawwWd2h6^xP(+y& > < vd2xlf*jdJP(tX2, l=LST*f3b1XVt]}DQQ fQB!\__u1H>2|>l{^D"|(`0{<Qi Ew6 9߼y/|+i~HL&~p8d"PՐX,vźXu C;twp8D2|x)К|ZxN'rExvvQ`0O~a4MB!AEaC ~ <( FD_bww_F8( Z,A}V욦LE8!:~L&?5~ӟ"T*a>#Jŋ(OaXr^.KR>;!nC4E,~~H$ݎf)N#l @F2f3U{:h4:NUUU Qs L&4 NtZr5MuQQv4 lnnJ -BТ:J(U_޵l(˒M\ .Pp  9H+a-+AXt @O(Ge7-+)-V~_T+N&q1#;_C˿I$- 9x] u]KK0G 3Ǵ7^VBA&&r)KP(nwZvMux<&T*T/X,Xkx/=!{>9 " XF<  HDe~:s r?(uW،3|v;t]G0D:Ǔ^'je3G76 | t:`ADžh ix3X,#{Tx<Ӡ&fze\[Q?Nqzz .5za^2(2(2ʨ?2@Q?vVnL&j5٠TU^pX`cVC,C(ċ TAlll&*m]ΰ-T"V+BTET8Z"H]< X%` hMXx=h4p8ʌśq?88'|"٫|v(˒QJёd^}x<9*vP. _|^suuGFdh4BVA"p8ƣGP(STU\^^b0D!zV4& z؉R%xggMڌ3ۓ*@ ]B5f36|>,R8NXVT*|7BySAGHLxh4p:k9>Ll6H$h4d.0w2 HnbZE,@].(M 9hX,ık)+U~_@d25Bd23\Tl % Zf# -Ja | $&z\.eQrq]uKh̥2|bDXFv˄<+!3hڛ|=!(f iMiu+cu_P;_ڥ3Oy7\ y-8&!sZ- i⚦i(5MCD"! ]*___KCN8F(L|E"H>|>\G=\ChsJpzz* yy J=Z$ KcXd}!fCٔ&`0 OJ@ǟKsT^i4M9']D"}n^O8ɤ{GGG(5/K|kpp`^2(2(2(2@Q?v>~C6ͦ؄:ʥJT*%Jxb(FQ CX,8N9\NT*a:ʦd\FVý{0E-J t: ˅hl6fSr9lmm+UrDRAjB2fC݆(X.x\.q}} Áx<ݎj*@n6O$od2agg|Pr/oX8::nG @X>St:J%$ AeB49&DT*F0( HR%onnĢJ#Zf31,Rȵx*)NNN˅T*z.GGG7H:YQF#t:|D"NBp2٬Nv>css}ؒ޿_6B(_ZcQz=VN LF.K iZ@bJ?Pr 7ͦ*Z|> m4vf3$IsQrMzx@|>P~r!|>(P($^£G0JDn Պ׿\KT*iM/lNNXL@EQTv _ *^{ȲeVK2=ic>SvE bd"c0pݪz]:0d{<QYt} `C`A Q"gRi#xpbf`)^>GhsNu1-qiMU.x<>6 LԞ ZF^~.39VL'u\bl~/ך?< ٌBx:FaۍN#pl$cH,ZySDD˹@[cy}>񴒦j!͊ڝv M fE N %NxT*IDt!&4h4 MPV% ]hT = xiDwרTۊ^eQFeQFeQF'z)ժl1vT`+ j x#`v6M !K]Zr9dc6x^FC6MGdOed2TU9EQp8`ZL&j޴m, bXT*@$LfF=99A$jBD2`0@Vt:G8=z$Zٌ-|vx!v;fdNjdvquu%VnE1uy? F\Vh$Ɗ F UBzx 666 vݻwFP1f2 B(0pEQPTd{ss;;;z-JJ`ߏVuG jl2lh^l"ˡlիWx1vwwa2`ۑH$oob9l6D(@ \NlZUUuttj%:Zs<FH|(( ͍@lX,rfp8D"!yՓ[[[N]. ]ץ/@.kv-6Nn_(Jvqpp v-0 lĈD"T*~?v;.TUE$A\__Kdz^yea [T9mAQz^V+4 6͒˻X,$G6M8FXVwiom6vjZЮEf\. QR=_J54՛NSY~' *D]ס({B1RTRJ`M+s*B †6+.K4"?9xw1|[u0<+/ a;չW/2D>Tӊ*|_,2DVC0ˑH_5p=ȵZ bHDΓb!ļvZ3\. f:-yt 4Mݞgx#NŶx8R~Zg/ ig6-N'* ~?BFF]U;O< ӷlBB8Ldc <|P,3 zN'nnn8NLS4MvzAe^(2(2(2Ͻ kԏ]܈~+llld2DO%W@jHʍ_"֮$/x\,d2jFF$AE<5OSf3}aL&rP(mb @V54MC^(ż|.JEz_|@ MyQ;0PtfZ6vlBUUf3lnnb0F0LPUV ;;;WK RBn ߏ+Q <*ո^Vq||H$"yŪP hV T `P6iIvj Ϩx!l* qAF_EQpvvMP ܿ\ԾFCޗpT/ Z-4_*UUWWWbb( vww%[rFx!E44 "@TD쟩\VT*xRIX37͛7buJZ WcQ2ws֩VUUB^VMt:l6徜!˽;??֖ElcB-qXx!$f޷bFaXh4pss#j2`ssnfj`0 {ʣ#椂Zbkk0TZ,Q;w:i(E2Dӑlv-7Tvc:/YL&p:JxH'a!,hzkFX.OiKZms>g2<b*y{lҡj_%H g uyT SLu'զc vZc#aSyMO0j,{]} g.տyg$Z=C{"e>|uFq6 R)Q 3Õ b;l4MjE,AVCGTbHFvvXVfh4P`ۑL&ew<5M lllH'!gT+l&VT׸\.GjO ǃH$@ r,$"Mxd CGfCXlFXS*|>GTdBP@.0NN( <@ۍ#ɜL&x-ɤXrj" 0XVt:p8d2p8h4vL&1Nd ~pR hlJ& @^w& ANL&aF#.nWޞN*Zs;yFzE"Q˅V%6 nWԛ<&6=YpI{3V+\^^ɓ'X.% b@vfCTŅlSi&XvltPHk:lj‘uZpd2)TkO&O?*G 5lXrt]G x<ƽ{pyy)`~V+Xd0rvt:X*rm:< P-d2T c.3wii2P.J3f4\]]Au?h4RQt:  Tl=^zUUaX$7 jpٳgaB: B jX,l]בfp8_fJԾ6 zrYr6޾}v-'HaZf2DF ؟'/_^`bi&޾} 'dL&ͦ~ۍ{aZ l'l{@J-b1H'''H&>9{<X,mw.O>& 2 ::^/n7^z|>>ffYG6Dӑ1Ca0Ft:q~~ÁD"!l?990n%^ z5"áO]ޢhn #\z)Jl⟜pvrX,fPUv0fjGnooFzL&((X,(ˢhu/^i؀fý{0qxxP(FZJox4MrkR>ɟ}!p777BxXsp>+^r)V̐'tt:x<L( 677GGGx$ t: UUq}ɵف9UTwɵaE)":z ~4Q]\\T-@H޽FL&T*3? P(Aߗ6IJ4P i8;;C^f`n;QF EQFeQFeQFe%e(zޟg(8;;Ǐ:h40r8;6nzd>Ol }%R l~Q rd2njAQf3ZF׃ALs@EifJDDDj Z-@ lh4*f /fZb<4 jUw&9UʕJtfS@J%r/_Ç|b( 4MMs)>TU۷o  О늫r]e`ٔ ?JP($`V BTTZZ-, .䴺d#7y__*%[b`8k_VM`r)˺#JK+UKVH-S@h}|^.%cBdZ<`gggggy:Nwt:k|ǢdnG:FX]-z=P?==W&va<ZR,Q5zvv&`0p([#ھD2|^ &6^^^Jk#Tz=l6Y@p!yTREHC'@ ]Wa"3OݖLu'D{z=x<;&|%ZWsav:AJXBu;c*<QZΆP4x_xy̚_2UUJ y {yxt &e3:#qu_(ŢN#T~q8 X=(7EIV.F%KC4&tN'?>LEO\T6almmI^l6,~ !\.\\,VF! fY [~\Z @X.t:b]xx< 8==.өA6w\.i*a.b@@D"@V|>r)_ߏp8,v" a8Bu|>,KQs9h&ܼt:q@v*ŢQBbCu|\x<~9t]6pCN3BU-WxK@X,b}KKI M5e4t@A[YZI9 >z<(Դfku$Iˌ_ MT1ڼYP3#3f׊=g?ЭVD&8N{. GGGX,Hz4MhT뒗 ( ޽{'\x혻ZPVeܲy-9v~6r:MDQ\p!~ %$Jp:`:"9s1vWtNbq86?lEd2 ='||>WrJT fj<\a33)Á}sj5!nCQt!jH&j3)8"\.jkȵ  .K{(%`9O6{_ںve]ve]ve]ve^EW_!r!X,JUKpzzϟKj!`=eGpCyߗEq>e%UxiP(4M\."J%l[yϟ?a>tiǏrRd^P(nKCCv2#t70 Cp>OVB[R*n5& * O֪ hiJc&~At:M&ǁ@kGAQ׋V%E"Ph>G8D"4 t:}SFj^|>CB]8F"R)] Uw:8NGr9y}~өL& E^l6C&xssp8,z--U'̽fh4@(˜j`3,Kb1BF##=Bz'XZ Cٹq2 TJvl eUљJ(???5cX(ÚLd\S,Y+*ښ_K5)g4g#Sht"VI`^T瘳P&|>קB^:6XUTZ5t*D8NN+j*~9,ך|kx4۞fj~ÆsFs0sy>+`ϚOp8  BRac)Y,Egxt:E.CZj=ggaH7MSM%\OJElRJ9-y/Ţr ї˥.c& C^h4w ?|\.hCpN3N X{@*>Ob~D^;;; C{NLF{/v}A/lk]ve]ve]v{٠׮v>~XyWWW:TUCa zh^O9~_Yo3d24M8N<}n^OYrRvf|WWWʡ8;;C*B @Akċ/p8tH_RX,ʶ1(GV!J۷R_RQ|yylb2ٳgŌ_$@`\.'t:g"LA./j^zx<.{fÁ>fF#F &BrjD"!O*zt:xvww& ÐztJ*o'*լEtgC|x<.u5t:aXjI%vvv&vQ*pppl6\]f@V+|tR pz]&fH$SF:ywwW@@V; wt: e9i+L&^|@ [o_}&>~k᜞fFp8pXM _Vo:h4V+J`0(%h RQAh4d^.qyyGrX,h`4!cX( BJB+;;H$眧rl rt:Bh4Rf2A)i*KGTAJh1#\t:R`F"WZ'񀀖t. $d&.L3DV%TBjCU֬\zfܳ"j xbfڭD9!ބ'Xf ӖV|.VS; Z={ 3{x!VlsϧЁ@@vϜN^iΞ!\`3b:نm{ Rz\F=м6 kfVkqx^V|>DQ5|>BнPfSN&Ħsy_8gerHӘfZVGBhRD4`JtZ6\ 糈kD/Fw٠׮Uu3lk]ve]ve]v{٠׮vYA/)5 Iu}}cw!-@v[YuHJ)ΨD1ZAV |^6H *C n LSf3r& ݮTlVJl6hX,ۍh4*xKj'O, *bB8xY^^^"Lʲ f 4 C50I-6aNNN-̵w&,A @#޼yJ}l6j5S-nQ,Qդ#>5:NT*)aTu>t:x* :A,b˼PNl6u dL1 x^z="ѣG(Jrh8;;>ժ{T\B!a ,S7LdSαFl(J0 PVNtrDi"LT*Im Yf ne|嗂zT>x@@X,bH^h4#?%޿/4-l\ՐpxxH$ǃJvph]AJy~/kOZt?~ p8TEz򗿔"4M4 V+TU* FF Taf.<|>l h)H$h4|t:-v1j0NH$Gim n+̌D"ʙI9l< l YZR@@Ѫ†[[@b . n`PPjt*%z\`4)뛠0^'cBp/fwSLK_ 6cyTRm=#$cc $Cd^9ձlҠ:LFT>_ߪN&>FN'.ө-TҶM\l xjD"z{<)4\Njj0w u&A&539o>P.$ܦRu `ZV'fBA{aG}mZÇQհ\.Z{ŋT*#ݮr Px<۷oҭ;;;t:rJ w\ё,&7s})?~t11777X.b@8j]ke+z...FV.*zBd^! 0;KŔ?h4e_ҺxbVdT*%G(B4\.>|(K`0(N*|x Jl6quuTQFx4M<~J9vfp:}x^iig;t:/JL&( t:RA1wwwW9xrF@j*06TR<ͤA$ *~ӟ?R*pyyy/bQ60P(b@2 n789NOO1L{%U̞bv̂$0ur2r*D!*樾oZHR?</ t:k$n0MZM#DBPת g>Ul f0MSM4~_.(dRYeih( H$^WJmy].^3ŅZ a^ZMV̻4bÁx|q6a0`^JEǏH$0 V ;;;xЋLzܓDǏvKMeV`˗p: . ʎ]V(~0Ze*_yOfb~wd2t:iP\Nn7>|T*;i) ϹF) Fk`B )L&zj^a_bGV?я|BEr"ڟ?Vi.lKwwweJxC U/l{NhsAdC:/ qZ{pөv%L=-ʹP g66%m(@ 67@K.ժTKr t^y hnPL VKh+ <}"`wggGI)zOVHXkVhY[mL8j4?5RZ ;riOPNG039^O@NS 0nWJrB!Y3_8L pZKXǕBkrq^1jNE35@/ D"}wFzFA=La!rDXTcŅYsNGeV{<;rp}٠׮Uife]ve]ve]vg zkA_~)69D_r8booOh4sqz XHn4 jUT6 "PH*a޳P(/_b:j!>|^HB `Ad"6\.1ey}}D"!0DvaB:FVeJӸA<i( ]טL&xhTP}6)_pX?bTV+r9媆aZT*%zdl6QT0LtXm8r:ϣVt"L0 x<eD" ߏ#U'''xq>ǻwL&P IDAT<==ʌy>o§p8,7Am Gܖ%zQױlsA}PxW^,sPH̦D"K>jU9d; @EbfmQ]F5땽6UlV*7滻;Ts x-)d_NtdgDx>>XVqss#, ueh$A&kө>#f^I8 @pHIk$"dfC OVxH"L d2A8ּl[ITZ9 h@cUZ|>ShKȿ ?t:z'~y.ljHs'6U֜dz!3`#\'kibh4*r*|\ldj (Jrt:X.RVs3>߳]&56pMyc~±ٚ]M7tP($0ϟaiF R)U4NhLj%^'%7ǀR*minFaᰜ ,tZXvXMbn߿A]߫Ak]ve]ve]ve]vY`0rׯ_c:ScL&<~P>OX aP1\.Ǐh6h6۷oesn}w]\.$ |Q v_RuNSHHu,M;Rt`Pρ@TJf;b2JAK[`0χVS&E0DP xp8d_  NGdLi2N'^xDfZ3  zR +mxbnoo[JPבt:^vF(J:&xk"fiꫯJEE@Fp: 0P5WhD3x^dY3P(LFvT mZR ___vcww^fS .˅T*\.4a&Խ?0 t:f+SI~xxy*GVzDB^|Y,F)ZV>|Ja)aW_! d:"JUF@W2ߡhH-W.Ciɤg & &Bo~t]f3)ƛGRIvx<`0^ϟ#Iz JKNj*u \.?nJdR9/R WTnz&VX,&'a%Ua)ߏ@ޚKR?τkb1 =CGcv6-#s-&l+U0UЄtϼ/ܗx]TZ횙mkT%[TAso6UBJ#%:N&Tz=l[F#] ] HeuL4A`jJnc/\g/of#6 1\.#dn5TRAş=r95\J'fN&e5J^r~qvkh4vnrT*01NkZw4\. Q9ҦiRXen7޼y@ g#5b={oáuuuDtMa6Y|]v9替?/ve]ve]ve]veןU<Y6u)$!jRoU*|?OP d a&6 01 ۍo*cwwwꫯ^p!HI&d2h4XVd2fH H&L&pR a`ww^{{{FRhV+r9 G">ssKW`d2˗/unҡtgggpTSp8Jg\ZR) CYfz=A^˅Jh4l6+pf1͐H$LKe.p8`Am0Db@F^r\ft:ʧ:?!F r~$ " I,ŏ?b\ . ;;;/v\Hao0onn; o[T*o )7oRɓ'b4MNG.ZfYӨbN4dMCA[j! \l6T*%-Ah4;?? ҕNL&P(b\.c2`:^ŋjR;Z-ujSZso_|!D.C^Gp8PRAHesˌx 4폄t:T*%%7 xD"h6>tڞSǦ!EA*!p`C9d̸5!|\iJAJJ@ k80Wu8 fU33`YZ<՜_V1x|lc VX P۪n4zmv@ fX,Μz)ef  K2 U@t !e3 hjjU `0Fzt:P(ÁzjRvh4eu7uNk&4F69 hT:Y,K뉮B(n[kb6! a6i78!bgg߿> ˗/Fj*+7ϧajd2X,brpr.{ve]ve]ve]#e[7.Z7F0 J{n[!`FL&b:LfRHxT,1:~(#ET`P1n}VWZTSAʌD"|>` x|X:o4eO{{{+%+~ER YT;6? \.ˊЈ f00DP*+[a̷Z-({p]618Nŷp8A{hP`wtfcYY"R}t\phj 0.#l}CLjD"xf쾓ɤ$a03+|d3R ~BnWNG <6+$ B0TR `&Ƙ0ak>|((z3^WKdYAN_~{^ΐJ^-Fg KcZ{kVpZlVvBVTц~XvyTro33k٪l&=4!"(ՒR`sZ Z,RKf 9|X3+&̣Rpzhm.*+y]VZ&p-[LVu9FVaQ,߼O漰Bk^3,dž|o*;˥.qb{˥y?OW1ɤB%6rolP.eOQT4\>͐f@[d2 ۭY5q6:F#5ac5Q9^x9Ga{N}H$^'ͱUግžV،`vA6E<Ǐh4f9TMqMrjtooo:wOOOmfW?boge]ve]ve]vg zkAg}x<gah*5a@l6z-vh(T^]] `8ZÇR@r!"ͦ, iH(V,:$zH&0MB~~@@;t:E>W>&a۷oQ(^nTJTo>8<N`0x<t]dYB!}, eR. $I)s&,Xi;v#Yi̱s 2c 93afs%a^k|v~}EhFT+tʚ]J}6P=iy Oϧ?slH z éZlf5YT}ZV/mY3f*{kq3?3?'zcA8Xa[+ȧ2g6NiJfSuf^tZlVq"D&h"V\jUjc^+s өsc5r"5;X^OMD&˽6{Vy"b8jt:P:֚$@mDZ2' ~/ Bx_ssQJv}gϞ^ᄈA]ve]ve]ve]^٠׮v?zFVi"b"٣ǨjR0Yy,xivqxx(hX*t~_LZ. ٍBq(+Nt:hZ8==E>G<G^h  4Mz`0b#ٙl)>|p8d2)W&:r`fSb>Z pHDv!|>Ҫ62߫+YR)LS;|wHR899@ZEXE<b@ T*ϟKFe1z1 *pHIUG2c=>1 rYBhǃh4h4nx<0 CJ333ӑ*h6)w )noo^'<Ne*W*{PiSs-0ِ. ~ztۭ[>wwwM'Xi-Y[) À(0 F|^JEَ`X DV@GRn1x턛Tw7MR)]A?@N x/$fjbݝ2o2 :nh4.9{擲ye4!p`6 %Fhx4_v@z*gJpk+sQ9&*X?6PKrNp=37֪%0&&|.rxе:[7~"SxKh|a*;h'ME3Atx-V-m)k2UtD01I69p ,v:]6=DQT[k^r!kj8( NʢzjJblZ X nXL[6x^{o4E(Bvww$K %1R~booOpziѣG tDctt29Ǚ JE[f3yTUgT*|X,~/űaA:FTǏ1 0NQTd4V*Cmk%3z...ˮ?<<: fA`.U%TWz=c jh4M \]]a6IGēkP\.NNNX,H$ I]BCBKzFr,u_*,hQZ&  >kP(`gg?%.tHZ_ RIjVlopFT0=NK\a~_jV%%>""=qf-~GGG{JT tGGG-v~=:riimLkѐ5m g,I%wyyoF,@~ggJ̡1@<Avc|>|~=X|ttSeoF777x<ۃa8mLS)6~f|D"K4M 4n( XF0 CJ2ǃzz;|X XL i3'x`8"J! tJ1  P(V׋pzo*DzR(4M)9^~-E42}>Ծ ZMc=0a0G? Z;)ۍ{ֵ^ypp  & ~?~=eug" [t:0MS {UNʘ`@ x\ .{6\\K|@@ MN6laM6PJ5t:U]Z>w#%4A:?3o9g@M3>dKɬٝ5Q%XلTQZhLv;ߓy;:wZ1`ZN>Ѩ&s:rl r%MZ\vɜ'H|^N#6-"z9n#"Lb J!n;C4E>WSC4<#g1L=!Hh:R$ |h6R7 aRci8==U0mwwwl߽{,`6^{{{j$bVɋC6xa6ӧV =\fo]v...)[k_ۓtJnvuʟ3 `&Oʾׯ_SQBu)+̽!|>Ǔ'Or p8 tH$L&qyy@ xF!Tz=a)Wh{zz*x̃ce O˱X Bvxbp~~h4lp!8pvgϞI7Na޽{'{k}Dzx\9<5%r7`6 r@pp8L5YTJ>XLmk"sX,6FY"j: ze/G"{Ӵuf3ѨMVȖ央c3UR`a PCdYJ%=ZvҢ7H`2(`T*aooOo8! C$ h4s)=z4qrrh4t*,X ~jx2."j߽{ߏ\.p fi^__K) @dH$")F>}*z|j0fL&ܴ_,mrDZjvۓj5ރZ{h4糪S."V*8GfP^_OS+vT+*n7܃. IDAT4Y38%ՄV5Uj ZwhZԵ#x2X~*rVgUIM+eZfT8[_`^yMϴAS#tW61pli\.5'pu],j{{{(vԋB9l"M8$E"ut`Y8rpww`0(> w]=өl*iSQ*ܭVRQyf\.Nh˥H`0\v~g-va&~\.:VB_T7G{6֜AH,j5{߲u...."f:"JbsZT* b1, B!$ صmI6H$)x?;e8 v,JL&ݕʗ̱e{pp`0(Ppppd`0k@s\u奀(!zԚ:0NOZPVezѨ}>ZziȃJOwHz]. DLFY[TQ!Ctڮf3Ax ONNnar: C+Wښ9kh4cfwvPV1NL&eK,׫+n )ߘ gZ\B!e~lbggϟ?zVC&<Ǹ`0MSUߏh{)ۨ|M$SE.cT*})* x<-{qAbX˗fH$C^CPd2|`0a65XdY4 n$I' e2 2l"_~}f#Es4>ͦl wqvv&`0`0ёa|T1@=\ hTvV<e*\* 62ϹssHu4݃BLMV+ek),?D2 m9&Tdn0{j[>n7|>@:sפRe^kkn2퐭mhhHc#S3\}Xd?0{l4UEVCZ6p@`K{/vpr9C*į@SبAzR$0If)wvkZj\ h(˲N>1Z6qqq!wb0v*n6իWw\3[쨾fXh6jz^vx<& kѭZ \3aM̪-%י]vulk]ve]ve]ve]__pppχ'_c6 62g?F> h6^QוǕƫMLz-XDt:Jn7:6%`. h38Nq{{rd2y2 I)W,whTjU٭JE ej,CTvhZZ{J7>xGGGzT8N!ek"BpRptZCvT]ӚrDnZ HJE*`ڮki),2->bXZ?ŤOR3tnP(Hl6e7KH˜|yy }OЉDV }n d^ZM/8#a&& !&nnnt:Q*PՐH$/0MSje#NSV Xv.y^z%pB@Q^a#xG:FX`0\8??rD$A(bm1u:5,KL&z=<~xu:5Nm6m}F¼\FTp=e58.R*C}'[E$i(j4Xpnnn]p |6>\.nkNYl6|BXVԏFx8g|>GAXax k4)ͩTJ1ˮ^6...ˮ'?3[|Ge7ޞ`!$fnnnKcD"l[m Nc^0@6"B9B`0(@Ui'riEK5x<ƻwl*8>>Vg,eQLh h4nQ( QuFt,d!z)fpyyl6rn TEs5e+sa-˜eTSCd^s\F(ёD礭fÇa&R=^'{zV%gZW~x oө{ urR2f2t:Y~.Kia1Lpttz=>JRTJ٫V fLjcq/c@h)o*p|d2k'J0 $ eBÁ=)NLx'*&4FVnnnn f]Vo\HD09+k   a8b: Ar͌r^+չo@vndjR`V,3. vl6h4R2 M&LfNZN&{6 fCh4n0FZ{fL3?Zf?*M;T pxOpRh_؀e76DPm:N4~fP^׺FCl øF*5Fb9=1fXHRx3i5OP[VlK5ky=6 :|>LT%P]Vxػ 2OΥtNN:7b PBT ; F Ewfa-f*Bֈ2Xn:%ܓNIIBPlpЗ|>Uo9yOts|SfݺueJǀx;_ _O#M7ݔ?xM)ܰaCXOn߾=iii󶷽-6l( E^v1,$s=[f֭~Ņ-[.]ve۶m6lXLRv-ݹEԩSޞ+W1WNMMMYD9˟QSSkfȐ!9裓,bQEqOdذae񫶶6cƌ)U뗉'fȐ!4hPY+/f:th֯_c=6+ViӦKU9G.'EѣGgΝٰaCƍ;wCYT2dHjժ^Ea"lkk˲e2qĴ?hР8;nܸtttdٲeikkTWWQ?||rP[[ŋ,{իSSSSvsssoߞUVۢ㴱1Ç϶mʂlA\YYl۶*KxߊN"V(L 6,ٻwoZ[[n"S\[[[iӦeƍiii);ZZZʹү_û̻ZKqI =SL)޶m[ybt{tvvf۶m9c< *ǏueSO=U[a;6W. w..9rd_kmۖɓ'gǎ(8qb9e{{{YpO#<2ijjJ[[[&Lgy&,,>KGqD9FGΒ%K̚5kbŊsrEOMMMyC˰a2f̘r~󪪪2/n)"jjj2`l߾,7n\*jWSt$kPDSСCSUUUFܢZW1EgqQt-:"i_$-g(MGM(";w,o`)Ea(&_l{_SR5Ea,nb)s.nN(ޫ\E+/-(Dr$9Z[[ ЕB/vڕEe̙~e̙Y`kVqnᩫ͛ˎ#oxѢcr,p)/]{W.;dWε5k֔_;;;nݺdܸq5jTEqTTTdȐ!2N:-:E!Yn]#ȶmOgܸqeGKuuu:::vf͚5,Ez2dHUёI&#2dH-[˗gҤI|Eq1^gggVZU^+W&y e˖-oX"vʚ5kۢiӦ$/ 1c$>2pL4)uuu7n\5suݴiSy1~ʕ:ujŋŅrljjjw2.zСYvmY`BKUUUiӦ˭(D޽;HKKK[[tM0!Vښ]veС4iRoߞ7f̘1ehbݻw+oxߞƲxUd4iRjkkS]]] ё<{lVZ򆂚YtD|xX^17sQiii) <;v( +++#wؑ۷-:Wscggg!\̋,EŢp\t>b. E\j1~E7j7\:pKvb⻱]IʟܷE(vtt>$97rQ/ge"sU|EQyME!u)i. EjD%]t?oQ. }PDH(""!`Νioo/o1bD6lP&.;v(E皚xyϋb#GfժUttt 4Ez ]y/~eڵ{Qt)EqcccF2A"cǎ?{^^+JEw1{q+R a ]tVTT7Օ?۷/7o.oLkmm͠A^0zk87TTTd˖-eJ@1Cq,nV?G_nЭ?5|+_9`w?C5k2a„ qt!aÆ3E_/}ӳ5ߌ]E(!2|r޽{sgƌ/"xskd>.'>] '뮻.'?ݻ+PCgM6+HSSSwfihh](!/_ݻptwb^^F:@ IDAT Yzz@%AxEK.ܹsw#gܜ$ٺuk.{W۷'Iٓo1ӧOϜ9snݺr[sOf͚#X O<1SNMuuu:ر#[n͵^SN9%UUU9sfn$ɏ_e?wygV^/~zF)SN… _v[7b6^Uȍ7ޘ:+[lرcS[[$;vlxڵ+=X$CҒkf84778B/l߾=\rI.s1wq̑GYٙ%KdԨQihhʕ+mX"/?fcǎ\zy{ޓ:+I2jԨ|ӟ<;wȧ?$g{'y8ƍ>̟??[lɚ5k裏f/- n-s嗧! W_w9ٙ;/ ޺͝wޙo|GYgK.$?2bĈ$ymqݽ@w /<`СCs2˼yx3gqѣG|ezz@%A Yzz@%A Yzz@%AG/gGPeD7}fz^^Ft3gnGPeD7}fz^^Ft3gnGPeD7}fz^^Ft3gnGPeD7}fz^^Ft3gnGPeD7}fz^^Ft3gnGPeD7}fz^^Ft3gnGPeD7}fz^^Ft3gnGPeD7}fz^^Ft3gnGPeD7}fz^^Ft3gnGPeD7}fz^^Ft3gnGPeD7}fz^^Ft3gnGPeD7}fz^^Ft3gnGPeD7}fz^^Ft3gnGPeD7}fz^e;TTTty?Sux≜tI9Yfeҥiii)י9sfmΚ5+ ,xɟsδ7xt@/s%ȑ#CK_R֯_o}[IL#|_LEE>y$e]SN9%G}t.|wܹ㗾t@}.ˮ󖷼EO>={Ɋ+2uԌ3&6lNb^ߗZMAeРA^v8xz=ztF^x/I3f{ 80Irfԩ1bDߟK/νޛ3fGгn݂ ru?̼y*RYY . O=T\|o~3rWdΜ9:y{ro1'O:; vޝo=={o3gΜ[|{ɬYyG>l=ܼ}^vnݺ5_~y=|_ߐ޷oA{t'Ћwqy_q>:^v|#G>F~YlY֯__.뮻Ԕ_yr饗;ȳ>k6?яҒC=|??!\pA;찼=UW]N:)_o;rKΝRQQfvxEw^>OuY8ϝwޙիW駟#Gfʔ)9p;sO:;;pƍ|?~lْ5kGw3VO?w}={vFOys<?݇ף)[oͱ$ihh… sg.}{swF***rGrYgK.~1"_kfϞ38#z׻$W_}u~OLggg;7djtn@p?`y2o޼q93X>mڴ'?9`ѣs׿!z( X>Xt2 f @ ˈn, f @x]ݛ$Y~}7 }Yf'^uYzuN=PaÆL8w^F7+$ɓO>nyc-ӛ|޽ٰaC=Qze.+L0!Ç`kkkK7x"ǘZN^8B/g~ݽ6 2hР|_ΠA{WxC1?C>s{*.g}媫^qŅ f֬Yo.z}ݙ>}ze .<`U[[l,uUVh[!?o7G/gAQ^n!vXӧG]u+SQQqGwvvfܹ5jT :+6l貍UV`R__ٳgϛ}(?1O}+رc3dȐ̜93e˖|S[[ . ۶mOgyf.]e7\ˠAַ5z><īSN9^e͡h߾}ѝz;e]/cɬYq5^wY~}y;3onݺM?~ٵkWzvm[sWtǡ":::r1nxvn,\0CͬYY,^8^ziϹ=ԽNO}S]߈ao3z.\Yf qwg]-\eWmmzӧ~w&Iݛ &3L/vZ\yOŋ\kkkFo=?$3[. In)_~y6mڔ7xxy뮻rg&y1I^z9sO#̿]zWdY&ijj*_b~ӟg鞃%Ɂcѻu:} Ƽw۴iS&'|v._"O>d9lݺ5cN^};ߙ뮻E_c9UTTdmk֬yU[n}C~xvڕEe̙~e̙Y`A7466-oyK>gժUIEe]#ĉ˱^`AMV^(NYf-O=ԛ{ f˗/OSSS1ӻqmmmYK3g_~Ypa'ܥ?k֬,]4---oZ<ԩSsEesƼwkmmM92w._`Am߽t ?SWW:*_җ}9͡F7֟*y\L]\s뭷fԩY~}䤓NʓO>Yvi7CCC$MMM/Y(g+p12r.LL4)y'rgҥ_%1(N?>L>=&Mʿ˿ }9SirGgʔ)yrꩧv;wn|.swx?i2v؜zYlYLf&pn^?6l|Æ 3fL7o~y2f̘ڵ뀹1cƼgx>3&7n={e>-oyKs%1_e7\R >܍Ax7jtB/UVV_.ۻwo̘17¶m۲lٲ;6|e.]UVc=cƌ,YKQ{sGk3y3e…]x֭YhQί~ݻ,̘1#o{r{7SN Y&7oرcf߾}s]wWoԹ|ƌ]Qw,^8I|Ǎ7 kre~n<ӹ袋ёO~ݽkF7YbEz|Nя~4555 re׿u-ZO~1cFN<$i#<2{n}̝;7 #yxxB˳xZ*K~,Y$w^sg&Igԧ>Gy$>`.s9illL| SO;_.ێPrcm۶|?+Vg5kVc̝;7o=ijjJSSSvؑ$oع /| _3<}{̛7ێPJlٲ\}YhQVXg9r'裏Nb1G/}ٴiS455|gϟ5^5kh6oޜѣG}{_~=:Irצ_~9묳s̚5+??sEeƌ:th>O䪫CO<>P(}ȭޚ/| Ȝ9suּ}3x5?sSO-?kjjr=dܹ9SWW+yȼyrg7x񮬬}ݗ뮻.0aB:?cPK%̾?.\K Ѓ}eba…,{jkkߐ/?֯_ :_neD7}V_n (2>Kt3=B/@/#D7#(2>Kt3=B/@/#D7#(2>Kt3=B/@/#D7#(2>Kt3=B/@/#D7#, f @ ˈn, f @ ˈn, f @ ˈn, f @ ˈn, f @ ˈn, f @ ˈn, f @wޅBj7ob^^GQezz^^FQezz^w}\wuzw_>eԩG?{キv:;;s뭷CPMK.$O?}8;F;vYtmN:$/~\xᅹ+ssi__$I|xN=r_3<33w}5jT9$/ʕ+~07pC/]zWwJ^۽{wK.СCGuT.| _ȿ&I~_e:~z>p 2y|;IEEś~ i=P,X3</G}4+W7|C$Ν;O|"#Gʕ+cCPwC)[n%g3a„rƍ/~1I}s矟s9'?xĉ3q\wuٵkW~] ]>ln<#]ٳ'Iү_I&޽u뗷myn"^O~N;-;6g3o޼lܸ1'Oիs-.K}}}N8ᄜ|ə3gN̙<37otD/P|[:w+**r5]wݕ%Kdڴiۿ|#I;-ܒn)k֬?̩TR#F%2dH=ܜ{/N}}}.w\VlTG*9sB\{樴4͙`nG(-gA*/é)(TA+R|<|}u]:sM|b7""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""nwyM҈^|F^|F^?Ϻ"""""""""-ݻwO<ʕ#,,,Ge;v1cN׮]믉DvoIӦM2e nʲ޻w2gZjE߾}ٵkW٥7lذTΑ#GR-c͚5t 777|}}9|ɞΝ;cggS-qU[bE̙3m֭[ox4cl۶S^=vʚ5k3nsYfΜ32pTu֌=@=zr{nn^4iիWOkIHH`-[T߸q';C```˗[vݻwԩ;w&448YV|H{# ÇSn]zņ HJJ2nү_?j׮M߾}9x`2bccYd ۷s|<|0w˖-O>oa9k׎I&Y1Y7@DDDDDD֭[ӫW/VZҥKy!'O`.ݛC+W'k׮e…`iiʕ+ѣׯH"{=z->|8у%K ` ͟?r-XE1ut3g?#o&޽ooo)UT#ڨQ8pӧOM6+V ;vpqw[NI&7=z___;Wu=zm۶1rHéUq]f}_% ///:vȯʴiӸz*>>>@r߭P}%66@:t@`` >9;;caa??EСϧPBYK/@xx8y8̙3'M?y[nҸܽ{GGg\=zĘ1c8tC }ܼyPLLLسgÇgȑtޝcǎѩS'mFFHLLdԨQܺuŌ3_5kdv~XXXбcG,--Yf ݻwgΝ-Z4}AAAԮ];ADDO 􊈈H Fɾ}2e -Z۷^{v娼J*~/n\ŗ_~I޽\CF… iӦ 4jԈo^z[ݻ}6+W NJV1zk֬IJҔƥKعs'ZJ.663fa<==bŊ8qBA\VfM … Yb>111̚5 {/˪ߙK/Dݺu-c 0~cǎeX׃ظq#+VH3*>VvmԩcܴiS/CҒŋ:-[RjU~zfVW^/uV^y>gݼTk׮Z_~#G—_~oj]`` 1c`ff|ǴlْB eYYy)Su3]:Ě5k8|0NNN閳yfz믿$޿qQF={~gʔ)@ҥiڴ)oժU˴_ǏTRk~jըP?sYv--[1IEDDDDD$^xѣG3~x3f  }eܹeaa*#QQQ$%%e?$**%J)R];%źu6y!9PveN8oDWbE/^LٲeӬ3 sY㲈"##ȥAaooϤI e;wQFGdoڴnݺe8~FbN<oo O VZ+CG4<sstIHH ..;;L$yʊZj_BEbb"SN{龸/as;ҥK)S333&MjfU?Dczhbbbc֬Y,_N:Ѿ}{VZExx8gˋnݺq!c7n`jՊF1bc'N͞={2mcv˖-0 /}dB9=?͛7OJo}ʃ .<駟HHHH0x*)䇪k{&**ڵkӾ}{Zha,36ZZZ2{lCXX͛7gҥRFl}%Kd 69sDRi7O8}4/_>G_޻w;wsNpss70> Ϩo'm*'$$?s1w\-[jXΜ9Cdd$6lC4i$mQ|y6lHPP׮]p¼{L`` kѵkW֯_@npvv@̞='ORV-csgabb>7¤IRRܿ{֩S={RfM ZdI֭[ǽ{)S)ݬʗ}> RFw}ߧpDEEg~G^z%~m9B˖-)Y$;wK.ڵ}affi <~AAAxzzzjvAtt4]t{ƗkGϞ=ի}ѩS'+WPjU obŊ5SLqxyyʌ3hР]v\\\˒;w.fADDO 􊈈H(P͛),Sԭ[yO8e 0PB=??? J"((ٳgm2J5ܹ$\pqӧ4h|CX<==ԩ_|_|̘1#G)%gYf +V`ѢE&&&gϞ4i^_>nnnիi۶-;w[Ss_3g#,,,Ry~~~<[e˖%(((TbLnffFhh(իWߟ~O>]`ekbkk5111lذq䔣>/7n&mػw/l߾ fZSLaȑx{{W_}`>M{gԩS899j*Jzꫯ(P'O>ѣp*T… #11XN.]sN߾}(|@߾}ߙ8q"111 8H[o1~x )\0'>>>{ 6mo|qjժL21c;pAve̘,^_=ըĖ-[27c""""""#2e /6lmm;vlʼ~:cǎM8l{{{ڴiY`A&S111'&&r5cJD'''za\A4hoF츸8 @%8}4ofժUxxxf͚ ucee;wrt!&Mx4kFm۶in֬"F|7XbZ7/Att4'N`DFF2a„4)=zѣG߀LRR‚[wqN:$N0a#FrnWV-jժeܬY3YxqX'''o… =z47fĉxxx?MkH%011W_p 4>#"":u͛)S ;wk׮cnn/s)79___III=z KKK>S)V[n[nL8}bnn… Yx1'OzٓT)޽{0`[nM5=Ev|`\Ò%KҼADDOWDDDDDD-))3greΝK֭ٸq#6mz2#""0`e˖eРA9ZoiiI~ d͚5у+WY333ܸrJ:߿)}oݺرCD*U:u*۷g޼y+O'K/aeeTee/Wpa\]] {:/}8p z-7fܸq|'ܽ{ooÇ)]4J~uڵm2q,x}a„ 2b5kDٽ{77o>0ΛڳgOBCC3@+֭ѣG5Rr,Z)SS8g4iN˗_|v7WLMMYd {Ϗ~nݺ駟>Q3[YYQJ4Pۄ3a}]Wfٌ1۷oD? ʩS/ Rͫgqe5kf<̙Ü9scٲe>y(+""""""ٶ~.];ݻw71Bbڴilذ!򢣣6l̞=;M٬Uhh1ea⌟_{5kݻg\va*VHjRLqe|H`0PLbcc3WNPPǏ7*LV#..ӧOg{ߧ)}q&&&XZZgmcjj}䍄Ο?ϨQ>1?~?-ZйsTZ!9]~J'::HᆪJ*4i$MP(}f͚HPPFҿ.;wOJJbƍhMn/χ|n|2UVeС,X?\t1cp,믳qT@n޼#ŋ'!!H;OuJ5e_8pHZha\qFN:G}ȑ#bݻ߿?˗/A39ޫW/zEPP-ZxcgOEDDDDD$[nݺG}رc|L2ѣG3n8-[cf͚ec 1{t}ɒ%5k...-Zׯ3o<L`€y&7oԔ6mڰrJNnݸuGfXZZo+ ʕ+̜9sbmmMll,ϟ'""cii35k͍aÆѧO .ŋYp!ӦMd)%ӧyxw26~immM^ٙm6LLL)/]DTT׮]ݻ?~333^y+Nʾ}?N8ARR\zǏcooO3c;v ((OOO'00KKK<<<ӓݻS\9 ,իW={6cǎ5s{XXXj///SrT}3x{{b zEŊ5jŋt֍0a}GŅykмys~T>ғ5C+Vug1yd;v,͛7ϷSzu8{,*UW^oH[Hϟ_>;vF@r#Gһwouz4hЀF ϯ+Wr}R d;rvADDO&C I_^""""""ٽ{7uM,8882ɟ""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""n<.^HTTo~Mɐ<2zIDAT-ɦ--eʔyu_x PFj;wMPP3ji6l@߾}IHHȳ:=zիի;vdҥ<|07 `0ФIt&MĥKu&))){_nd?oi9C;u![ef=Muaj֬o(6n[oEʕiݺ5˗/=v}J* 2SN)ٳ :*UвeK+6c f-#&&{=qy aܸq3bΜ9X~=e˖%222պ[n1{lZjf֭[gϞe̙x{{憯/ϟOUFbb";v7ߤiӦL2%UN ufmЇ?uvĄAK2Сgˎhׇ5Qwh&NH 8vXugΜaӦM4mڔ9s 1cqǏSfM^}Uf͚98q¸˗D̝;3m4>!;-O>aҤIO]n/ҩS';v,666t)ޫW2p@&}]>|8 2?>fffۗ#Fp:t@hhk2e:vȈ#8{,=zΝ;-;0`珧hq5ΣTF2t8ɕ snSf֭kܺuk<<>>XZZxb|||8p ^^^3oAyyC^yjua,Yf͚[nqܺu5k2|8{,M4al۶K.Ѿ}{O7Ήl۶7rMڶm[oEe.X۷s)4iСC*n߾ٻw/ 4jԈ޽{䄿?7n$ H9߿?ׯܹs4nܘҥKb۶mܼyN:ѷo_|'Z*kСCYzw=ϧO~'wSg0}=PᆪPBy2'*X ʕ>zcbٱ;Mgy7 BGaZGqɎr ܼy3ͺÆ _LSƌ39s&K,aL26l-[ aÆx{{SvmBBBeرT^=U"##Yd ;v0`m۶x{{3n8-1vCҬ35M^tx}~ aggITT%J0nSHvʆ *Л< VXGL]#GzJpK>Sȑ#Xl`τ P|M+{=֬Y)G&>>>H~Qpt۷yff͚ŨQٳ'6l]vݻe8;;3dbcc4i{i B  TBXXX'0Ae+C"""zos@Ņ"EM… x$p[o"11XaafX4[ ܝe3) .mYw_ٳg:u*֭3SjU=j)`qppM6YM2fn޼ڵkٸq7KNǎٹs'4jԈʕ+4h7n4Sу7np= _5 .dҤI*T+WI"##9pOӖpc;EJP֭[Yz39UTӫW/>C^|Ef̘yR3r"##ٰa:tI&ƀ ]6~'anjYA,l07ʪ ~ŗjR 7W[sH_l.\L]PWDDDDDDrEC,--fȐ!ԪU H~<00___weܸqϞ=ѯԨQ#FP@j,֖Fq59sаaC*&&???,X`L?Wn]Zh-[۷q#FGEFF2o< m0jԨ.;;;.]Ih,H73v)ͱȳv?׽{id5zʜ3<ν(b 3K X`afߠ 1qї#zc_wNZ8WmZRo-ZL2q9cw`oo/9sPl\ozMǎ(_< ,`ḻ<%վNNN/^sss*Ud\~Ihڴ)&&&S{RUJ9::`0Я_?mƸqҥ K]DD͛77~3B 1zh0 *U fϞ $gڦ[vF07_sS M11|>7nҩٍ)g'm4@9 4[^ ,-, yse""""""+"##qvvΎg}ʨqu>|HXXO78ܔ111(P???}~g<==iذai^JXX˗7.Yf/.\ѣGiT/QA㢣y2,3Eq{[n'虁s!9sHNd*+O*\V/66 h>3S3,-1{`ߑ6X$.!ܞv<ʃsbĉ5k׮_ШQ#N8/۷ogҥر6mڰcZnMطogNɓ'Yt)fffYo/աCyiݺ5cccS-OɠY ܹ3.]v,^vڱn:˸/"DGGs Ndd$&L`=mڴa,X lJ&`nf9}F Iپo ӂXyc%acUn\8o""""""?~MZ깔֭[Ӽ-njGv8t+Wd̘1X+.>>GZnaa{q!.\e˖5wVDDуh_1 ĉWrp”+W.LMpcMrܓď'17MUk fk4 (C_wNYZZRlY&Nƍٳg/2&&&mۖm]~=֔,YiӦѿڴi$E\2իWo߾4h OڜׯOpp0?{e|穎?=)nܸ~(enbŊ=UlBBׯSvm6lȖ-[0`q[-7fܸqӇÇckk%_~GkK&&&X¬q4Ys Sm[|9koAs3ЛXY'zM-05RbAZ4+Fr""""""Ԏ;Ƨ~ʏ?ke*U gggBCCiܸSD vJΝҥ Kurrʊ'Ozk׮OT;~~~={ʕ+pE[f̘^U-""OǍyf%9fksYS^^jU,ٰ11+`ߑo04ꎏO3KK _eҥQ`A㲔ZR$  ///Yl1cL%Jkm6ׯ` ))@}ݧ&+ K6f]Jijj}YBCCYx1۷oc#y'g)<;bm7aNS6' lj0B[wL?gE^ɑ=JRRw̙3,X_~9걶f„ 6 333VJXXnݢW^Y#&N+DDDp)f"E0n8>Cbcc)Uw޽{YijժѧO |IDD[[lpN 4ꃏ3zU;ʹTc.kC,l0+`ey d`n gϞ%""_KKKʔ)֭[9x -[dɒquq=ʔ)۷YbXСC޽;Ԯ]w}vݩYfq ŋ>}:Ք-lٲX#G`eeEYl tl'*#+m۶˼ԩS9sH@@1|֭y뭷938PjUlŠ+;v, 6dӦM^wrQziܱcG 9PŊٹs'+WdԩԫW;wR|y6mwfDFFRfMf͚EÆ HLLёkr=z4ZrJGf03QLIє_ҍ`_ here: https://tomotools.gitlab-pages.esrf.fr/nxtomo/tutorials/index.html nxtomomill-v2.0.1/doc/tutorials/patch_nx.rst000066400000000000000000000140371511430602400212340ustar00rootroot00000000000000patch-nx tutorial ================= .. article-info:: :read-time: 5 min read :class-container: sd-p-2 sd-outline-muted sd-rounded-1 the `patch-nx` application is used to patch an existing NXTomo entry in a nexus file. For now there is two main 'features': * adding dark series and / or flatfield series to an existing NXTomo entry * modifying frame type (projection, dark, flatfield, projection, alignment, invalid) .. note:: You can do both within only one call to the function. In this case we will first update frame type prior to add dark / flatfield series. This choice has been made to avoid confusion on frame indices. And to fit the use case users want to modify dark and / or flat. But a good practice would be to call twice the command. Adding dark and / or flatfield serie ------------------------------------ This can be used for example if you want to add some dark of flat series as post processing. .. warning:: this will modify **inplace** datasets contained in the target entry. If you wan't to save original data then you should first copy the file prior to calling this function. .. warning:: Any dataset you want to insert must be store into a 'permanant' relative position to the file you modify. This is needed to insure virtual dataset consistency. This is also why you should provide urls. .. note:: HDF5 virtual dataset are managed by creating a new data set from original sources and adding a new virtual source pointing to the provided url. To call this application you can call directly .. code-block:: bash nxtomomill patch-nx [my_file my_entry] [[--darks-at-start url1 --flats-at-dark url2 --darks-at-end url3 --flats-at-end url4]] .. note:: urls can be provided two ways: * `official silx way (recommended) `_ * tomwer way: data_path@file_path This is an example on how to add: * a serie of dark before projections contained in a dataset name 'data2' at the root node of the 'dark.hdf5' file * a serie of flats between the added dark and the original set of projections contained in the 'flat' dataset inside the my_nxtomo.nx file and only picking slices 1 and 2 .. code-block:: bash nxtomomill patch-nx /tmp/tmpzk3v37h_/simple_case/my_nxtomo.nx entry --darks-at-start data2@/tmp/tmpzk3v37h_/dark.hdf5 --flats-at-start silx:///tmp/tmpzk3v37h_/simple_case/simple_case.h5?path=flat&slice=1,2 Modifying frame type -------------------- From the command line you can define a set of frame for which you would like to change the type. The API to modify frame type is the following: .. code-block:: bash nxtomomill patch-nx [my_file my_entry] [[--invalid-frames frames1 update-to-projection frames2 --update-to-dark frames3 --update-to-flat frames4 --update-to-alignment frames5]] Those are some examples of usage: .. code-block:: bash # force frames 26 and 27 to be dark nxtomomill patch-nx --update-to-dark 26:28 my_nxtomo.nx entry # force all projections to be dark nxtomomill patch-nx --update-to-dark projection my_nxtomo.nx entry # force frames 10, 13, 16 and 19 to be flatfield nxtomomill patch-nx --update-to-flat 10:20:3 my_nxtomo.nx entry # force frame 0, 1 and to 4 to be projections nxtomomill patch-nx --update-to-proj 0,1,4 my_nxtomo.nx entry # force first frame to be alignment projection nxtomomill patch-nx --update-to-alignment 0 my_nxtomo.nx entry # force all frames to be invalid nxtomomill patch-nx --invalid-frames : my_nxtomo.nx entry # force all "projection" frame to be invalid nxtomomill patch-nx --invalid-frames projection my_nxtomo.nx entry .. note:: If you try to modify several frame type at the same type you should know that the order of resolution is: * modify frame to `alignment` if any * modify frame to `projection` if any * modify frame to `flatfield` if any * modify frame to `dark` type if any * modify frame to `invalid` if any Concrete example of modifying flat field on an .nx file ------------------------------------------------------- In this example flat field frames at from acquisition A has been mess up. NXTomo file is name original_acquiA.nx. Entry name is entry000A And we want to replace them by flat field from acquisition B (master file is acquiB.nx). Entry name is entry000B This is one way to proceed to replace flat field frames: 1. go to acquisition A folder containing the acquiA.nx file .. code-block:: bash cd [path_to_acquisitionA_folder]/acquisitionA 2. copy the original .nx file. Modification are in place. This is safer to copy the file. .. code-block:: bash cp original_acquiA.nx acquiA.nx 3. invalidate the flat field frames made at start. In the case let say that we want to invalid frames from 20 to 40 included: .. code-block:: bash nxtomomill patch-nx set83_tomo_black_drum_LPJ01_6p5p_20N_0001_0000_patch.nx entry0000 --invalid-frames 20:41 4. check that the invalidation of frames worked properly using silx view for example 5. get the silx url you want to link as the new flat field. Syntax is `silx://[file_path]?path=[data_path]&slice=[slices]` for example here we want to link frames 2000 to 2021 from acquiB.nx So the url looks like: silx://[folder_to_acquiB]/acquiB.nx?path=/entry000B/instrument/detector/data&slices=2000:2021 6. then patch flat from patch-nx command and the 'flats-at-start' option: .. code-block:: bash nxtomomill patch-nx acquiA.nx entry000A --flats-at-start "silx://[folder_to_acquiB]/acquiB.nx?path=/entry000B/instrument/detector/data&slices=2000:2021" .. warning:: when you provide the url make sure you use `"` or `'` characters. Otherwise in this case the command will be executed as a background task and slices will be ignored. It will also try to link it with the full dataset at `/entry000B/instrument/detector/data` 7. check that your dataset is complete (using silx view for example) Help ---- .. program-output:: nxtomomill patch-nx --help nxtomomill-v2.0.1/doc/tutorials/resources/000077500000000000000000000000001511430602400207035ustar00rootroot00000000000000nxtomomill-v2.0.1/doc/tutorials/resources/default_hdf5_config.cfg000066400000000000000000000142531511430602400252500ustar00rootroot00000000000000[GENERAL_SECTION] # general information. # input file if not provided must be provided from the command line input_file = # output file name. if not provided will use the input file basename and the file extension output_file = # overwrite output files if exists without asking overwrite = False # file extension. ignored if the output file is provided and contains an extension file_extension = .nx # log level. valid levels are "debug", "info", "warning" and "error" log_level = warning # raise an error when met one. otherwise continue and display an error log raises_error = False # ask or not the user for any inputs (if missing information) no_input = False # if true then will create a single file for all found sequences. if false create one nexus file per sequence and one master file with links to each sequence single_file = False # acquisition type. if not provided will try to guess it. valid values are "standard", "xrd-ct" and "" if undetermined format = standard # force output to be a `full` or a `half` acquisition. if not provided we parse raw data to try to find this information. field_of_view = [KEYS_SECTION] # identify specific path and datasets names to retrieve information from the bliss file. # nxtomomill will try to deduce cameras from dataset metadata and shape if none provided (default).if provided take the one requested. unix shell-style wildcards are managed valid_camera_names = # list of key to look for in order to find rotation angle rotation_angle_keys = ('rotm', 'mhsrot', 'hsrot', 'mrsrot', 'hrsrot', 'srot', 'srot_eh2', 'diffrz', 'hrrz_trig', 'rot') # list of keys / paths to look for in order to find translation in x x_translation_keys = ('sx', 'd3tx', 'tfx', 'px') # list of keys / paths to look for in order to find translation in y y_translation_keys = ('sy', 'd3ty', 'hry', 'py') # list of /paths keys to look for in order to find translation in z z_translation_keys = ('sz', 'd3tz', 'hrz', 'pz', 'mrsz') # key used to deduce the estimated center of rotation for half acquisition y_rot_keys = instrument/positioners/yrot # list of keys to look for diode (if any) diode_keys = ('fpico3',) # list of keys to look for the exposure time exposure_time_keys = ('acq_expo_time',) # list of keys / paths to look for the x pixel size x_pixel_keys = ('technique/optic/sample_pixel_size ', 'technique/optic/sample_pixel_size', 'technique/detector/pixel_size', 'hry_step_size') # list of keys / paths to look for the y pixel size y_pixel_keys = ('technique/optic/sample_pixel_size ', 'technique/optic/sample_pixel_size', 'technique/detector/pixel_size', 'hry_step_size') # list of keys / paths to look for sample to detector distance sample_detector_distance = ('technique/scan/sample_detector_distance',) [ENTRIES_AND_TITLES_SECTION] # optional section # define titles meaning. titles allows frame type deduction for each group. # list of root entries (sequence initialization) to convert. if not provided will convert all root entries entries = # list of sub entries (non-root) to ignore sub_entries_to_ignore = # list of title to consider the group/entry as a initialization (sequence start). ignored if dark_groups, flat_groups, projection_groups ... are provided. init_titles = ('tomo:basic', 'tomo:fullturn', 'sequence_of_scans', 'tomo:halfturn', 'tomo:multiturn') # list of title to consider the group/entry as a zserie initialization (sequence start). ignored if dark_groups, flat_groups, projection_groups ... are provided. zserie_init_titles = ('tomo:zseries',) # list of title to consider the group/entry as a dark. ignored if dark_groups, flat_groups, projection_groups ... are provided. dark_titles = ('dark images', 'dark') # list of title to consider the group/entry as a reference / flat. ignored if dark_groups, flat_groups, projection_groups ... are provided. flat_titles = ('flat', 'reference images', 'ref', 'refend') # list of title to consider the group/entry as a projection. ignored if dark_groups, flat_groups, projection_groups ... are provided. proj_titles = ('projections', 'ascan rot 0 ', 'ascan diffrz 0 180 1600 0.1') # list of title to consider the group/entry as an alignment. ignored if dark_groups, flat_groups, projection_groups ... are provided. alignment_titles = ('static images', 'ascan diffrz 180 0 4 0.1') [FRAME_TYPE_SECTION] # optional section # allows to define scan to be used for nxtomo conversion # the sequence order will follow the order provided. # list of scans to be converted. frame type should be provided for each scan. # expected format is: # data_scans = ( # (frame_type=projections, entry=silx:///path/to/file?/path/to/scan/node, copy=true), # (frame_type=projections, entry=/path_relative_to_file), # ) # * `frame_type` (mandatory): values can be `projection`, `flat`, `dark`, `alignment` or `init`. # * `entry` (mandatory): dataurl with path to the scan to integrate. if the scan is contained in the input_file then you can only provide path/name of the scan. # * copy (optional): you can provide a different behavior for the this scan (should we duplicate data or not) # more details are available here: todo: provide link data_scans = # you can duplicate data inside the input file or create a link to the original frames. in this case you should keep the relative position of the files default_data_copy = False [PCOTOMO_SECTION] # pcotomo specific section # if provided then acquisition parameters `nb_loop` and `nb_tomo` will be ignored. instead `tomo_n` nxtomo will be created from pcotomo. all angles before `start_angle_offset_in_degree` will be ignored start_angle_offset_in_degree = None # if 'start_angle_offset_in_degree' provided then specify the number of nxtomo to create. if -1 provided then will create as much nxtomo as possible tomo_n = -1 # angle interval - range to create if 'start_angle_offset_in_degree' is provided. 180 or 360 is expected angle_interval_in_degree = 360 # shift all angle nxtomo angle to `angle_interval_in_degree` interval by shifting them of start_angle_offset_in_degree + angle_interval_in_degree shift_angles = False [EXTRA_PARAMS_SECTION] # optional section # you can predefined values which are missing in the input .h5 file # handled parameters are ('energy_kev', 'x_pixel_size_m', 'y_pixel_size_m', 'detector_sample_distance_m') nxtomomill-v2.0.1/doc/userguide/000077500000000000000000000000001511430602400166375ustar00rootroot00000000000000nxtomomill-v2.0.1/doc/userguide/batch_processing.rst000066400000000000000000000030151511430602400227050ustar00rootroot00000000000000Batch processing ================ It can be convenient to convert several file in a row. Here are two small examples around h52nx that can be reused for other nxtomomill applications. using python script ^^^^^^^^^^^^^^^^^^^ In this example we show how to retrieve a list of file to be converted and them convert them one by one .. code-block:: python from glob import glob from nxtomomill.converter import from_h5_to_nx from nxtomomill.io.config import TomoHDF5Config from tqdm import tqdm bliss_files=glob("/home/payno/Datasets/tomography/tomwer/tomwerExtraDatasets/bamboo_hercules//*.h5") for file_path in bliss_files: print(f"start conversion of {file_path}") config = TomoHDF5Config() config.input_file = file_path config.output_file = file_path.replace(".h5", ".nx") from_h5_to_nx( configuration=config, # you can tune the conversion using this class progress=tqdm(desc="h52nx"), # used to display advancement ) using bash ^^^^^^^^^^ In this example we show how to retrieve a list of file to be converted and them convert them one by one .. code-block:: bash # source tomotools environment source /scisoft/tomotools/activate dev # list all bliss file to be converted bliss_files=$(ls -d /data/visitor/{xxx}/RAW_DATA/*/*/*.h5) for file_path in $bliss_files do echo "start conversion of $file_path" nxtomomill h52nx $file_path # + possible options like config file or inline options done nxtomomill-v2.0.1/doc/userguide/handling_h52nx_issues.rst000066400000000000000000000141601511430602400235760ustar00rootroot00000000000000.. _handling_h52nx_issues: How to handle `h52nx` conversion issues ======================================= This section present ways to go around some potential issues during conversion from bliss-scan to NXtomo like: * some bliss entry is skipped / unrecognized * some mandatory information are missing * specify some field values * provide a `h52nx` configuration file to tomwer Some bliss entry is skipped / unrecognized """""""""""""""""""""""""""""""""""""""""" Currently the deduction of a bliss scan type (dark, flat, projection...) is done by: * looking at the 'technique' group (image_key dataset) * else look at the title. Title mapping is defined in settings.py file of nxtomomill. If the titles have a specific naming convention then you can provide updated information to one of the following: * modify it from the settings.py file (if this is a local installation * provide different name to be used * from the CLI * from a configuration file (see later) * from the python API .. hint:: You can 'ignore' the bliss 'technique' dataset (aka bliss tomo config) by using a configuration and turning the `ignore_bliss_tomo_config` to True From the CLI without a configuration file ''''''''''''''''''''''''''''''''''''''''' If you look at the help you can see how to redefine title names. .. code-block:: nxtomomill h52nx --help --init_titles: mark the beginning of a Bliss sequence (eq acquisition). Use for example to retrieve energy. --dark_titles: specify that this Bliss entry is relative to dark field --flat_titles: specify that this Bliss entry is relative to flat field --proj_titles: specify that this Bliss entry is relative to projection --align_titles: specify that this Bliss entry is relative to alignment (some time called 'return') --init_zserie_titles, --init-multitomo-titles same as init-titles but dedicated to zseries and multi-tomo (behavior of NXtomo creation is a bit different) From the CLI with the configuration file '''''''''''''''''''''''''''''''''''''''' The same information can be provided to `ENTRIES_AND_TITLES_SECTION` section .. image:: img/handling_h52nx_issues/nxtomomill_config_titles_section.png From the python API ''''''''''''''''''' you can also provide this information to the `TomoHDF5Config` class like: .. code-block:: python configuration = TomoHDF5Config() configuration.init_titles = ("mytomo:basic", "mytomo:fullturn") Some mandatory information are missing """""""""""""""""""""""""""""""""""""" From the CLI without a configuration file ''''''''''''''''''''''''''''''''''''''''' There is a limited number of information that the user can provide manually like energy or pixel size. Those can be provided from the set-params option like: In this case you can provide it from the --set-params option from the CLI like: nxtomomill h52nx ... --set-params energy 0.5 .. warning:: the `--set-params` option should always be put at the end of the command. Because it can take a full list of sub-options From the CLI with a configuration file '''''''''''''''''''''''''''''''''''''' You can also provide this information to the configuration file under the `EXTRA_PARAMS_SECTION` section like: .. image:: img/handling_h52nx_issues/nxtomomill_configuration_file_extra_params.png From the python API ''''''''''''''''''' Or provide this from a python script when defining the configuration .. code-block:: python configuration = TomoHDF5Config() configuration.param_already_defined = { "energy_kev": 19.2, } Specifying field values """"""""""""""""""""""" For specific fields ("detector name", "translation_x", "translation_y", "translation_z", and "rotation"), we attempt to extract this information from the 'technique' dataset. If the data is not available there, we revert to the generic behavior. Generic behavior '''''''''''''''' The generic behavior involves searching for each field in a set of predefined locations or paths. If the dataset's structure matches the expected format, the field value is retrieved from the corresponding location. Customizing locations ''''''''''''''''''''' You can customize these locations (similar to titles) using the following methods: * `settings.py file `_: modify this file to change the default locations parsed. * command line options. * `h52nx` configuration file: overwrite the locations from a configuration file. From the CLI without a configuration file ''''''''''''''''''''''''''''''''''''''''' For the CLI we can get them using ̀`nxtomomill h52nx --help` again: * `--x_trans_keys`: x translation key in bliss HDF5 file. * `--y_trans_keys`: y translation key in bliss HDF5 file. * `--z_trans_keys`: z translation key in bliss HDF5 file. * `--sample-detector-distance`: sample detector distance. * `--valid_camera_names`: Valid NXDetector dataset name to be considered. If None will try to deduce the NXdetector from attributes and shape of the dataset * `--rot_angle_keys`: Valid dataset name for rotation angle. If None, look name from "technique/scan/motor" * `--acq_expo_time_keys`: acquisition exposure time in bliss HDF5 file. * `--x_pixel_size_key` x pixel size key in bliss HDF5 file. * `--y_pixel_size_key` y pixel size key in bliss HDF5 file. From the CLI with a configuration file '''''''''''''''''''''''''''''''''''''' the same field are available on the `KEYS_SECTION` of the configuration file. .. image:: img/handling_h52nx_issues/nxtomomill_config_keys_section.png dataset discovery is done as follow: * converter will first look at `positioner` group to resolve key then at `root` group aka bliss entry*. Collect is done by a look like code: .. code-block:: python for parse_group in (positioner, root_aka_bliss_entry): # first step: look for existing dataset with expected number of elmt for key_checked in parse_group: if find_dataset_with_expected_nb_elment: return it # second step: look for existing dataset and adapt it if not enought elmt for key_checked in parse_group: if find_dataset: return it return None nxtomomill-v2.0.1/doc/userguide/img/000077500000000000000000000000001511430602400174135ustar00rootroot00000000000000nxtomomill-v2.0.1/doc/userguide/img/handling_h52nx_issues/000077500000000000000000000000001511430602400236165ustar00rootroot00000000000000nxtomomill-v2.0.1/doc/userguide/img/handling_h52nx_issues/nxtomomill_config_keys_section.png000066400000000000000000003433151511430602400326430ustar00rootroot00000000000000PNG  IHDR|sBIT|dtEXtSoftwaregnome-screenshot> IDATxwx\W{,[!vBH# Bh &X @XB٥.,Kh K K[j Ď^nKi\$͹Kv>ɓDs>GwXK.Xh#""""""""""b3s6EDDDDDDDDDĘ6EDDDDDDDDDĘ6EDDDDDDDDDĘ6EDDDDDDDDDĘ6EDDDDDDDDDĘ6EDDDDDDDDDĘ6EDDDDDDDDDĘ6EDDDDDDDDDĘ6EDDDDDDDDDʗ, 5sODX*~>AA_tw|A>rצM[;vkxO>Pïn-86./}7?ﶁ,z+&.YTEv|C}'鳝IzAIUWy!6W\?c)K?{InS{]wQ}=W-%gm5tE|Ճ/㏌L~_Yocs'so m&/~LMG _|^][đ;-|W1ЏwW3yK_@/]>O(i8^JwIT,`wB#^>.nmI&;o9oMlYd?;FN>;YvW'7]|*߾WtMβ_y_zw NS6k^T/|m*zn7W=k>vv0{?Nsӿۮ k(Ȓ_xz֪oMʛ =6J|+' e饋-Vn=ܜ`3!yW#vm??z;zM`j~[Xg>i~kfs}-ܼ|i۱W >غWpC﹯{GCvñ3_#"""""""""5TTq?} 5"'\o# {՛ GM>z O MxqM7Ҕ}?>;{ͯvtYy!""""""""""9;bZٗkwI9A ?Kv^9qC|/wž~~8 rcڱ%?t8~HazP^ Cɧ[@fddtj$G`I:K6흼bY̫S\T+g)vn-=O?97y5F-{37}O~ooj|KukWaKuF;8ҕ"IVU$;8_ۮ%.j^wsiAf9TՕUļ<``gG^Ϳ#u9^yںMg~׻i~X3"""""""""r {^]z7~I~gp)l;7讠qV |'1~3j׿ {SM`>[̽? Q̚ko7o~5$d7%ex|˩~ \O2ώGНcK/~?E2Ռ=dۙ|N_>)8ʙ<dyc__8_7}4?_sgG˯\ɛa~'>|/rxnWXo$~ Ͽu1c?Z|n{]w>zuO໽=m9٣]lyח~x_<.aDDDDDDDDDZrB—_8{V+U7߾/GLeZ<Vg>Qck2YO*[yԗbvbA39crJ(kvDDDDDDDDD2T>gq]7<ͣ[~gFJDDDDDDDDDto*k,""""""""""iTcފʲͮ,gK) ?Ǣ篡>:":]83z_h#4"z>neŒ&/ Eϰ9?׸]~\75E9W׍;_/~x~|:g<7c+‡D|<49M|iW1r9wa˧87F8x2LɅdAu=y6]Q߯2[⥵ dö!MldE~]~s]rdz9/ΗrDx q^X̖\}E1:n95K.ȹxS9((X%t =e`[z) qdK%Yv/JDd/q<9s17%cM|iW\DDDDEEy$# ېSO hy'7ٻ785)Yl ga&IoAZ+#J0cvzFO,Wpe;QRMEp;q tV)^Ogj-=,\եk.Nҹy=lEwl硽CtӰz5+sx.gik5Yx$#C:\ GsЦBP=5<&![EU'˞C<2@h<5)fq$tpߦ6ͬDl 5NgePD~Q}n;#/vڋ}MS%2(FWЉWtԓ=;(❶_S]ΦbLysSiA{cwG!+Og0BA`\r!㋻񚖳ʦ clAr"D6W]Xnfn߃wxcxh`h/f(iXnmC%H:L yڽ|4>Q&sA92_Aay?t˾@ Sri=Gpqk/M{AםF2To""""6`d('7Da"%z"Sef|J,ƺœ89@pTou>'bC :B'6SD&}ks* ,5!vH G]m>6zfj{dhEk 34<}a~36Lxܢ(+%[#>‡2t`)7uy,,Qg6'6Zl:z&rDiyy!OLCT2I Q晴;I&!LT%I$ƿ4LY\6㟈 y*c S;RmZw_@o/?ML<ᙲX|_'m#9#1~N-z{km&rF3uc.%.mfъ"8>SVO %GsPX, *2tgS[Kka7vL W-nr9#қzg)gs36۳.*GK3Hri f\6c9/Vg~‰x7㟈<לyS-Ϯ`rBpd45d@;12=`Ԧ(l~Eqq>&#KW~xz&:EIE1YgH?iu==yr6Y;H]v{8^Ӗ=J(SVBwY+-Jv2^n.WK02[̬9;~)KNkQ">%dwl̓M[.37>5UcCtdH|=\7LoCQfZ`'ɞsIK@^XQw-k`I]6LDJлXϾ YyɶޙN$߲v9I$-$mmZ̒KֲÃC&9}scMr6Oϴ9ҵG5rTi9|R,mb<vַun3㩳$VWMX]9 +^lPl w_>Ź1‘D&EAu=y6]Qι1M|td4 %FMy ~6) 2i.1\ϟCR/%o  mb#se*x=;s=rLnD9ʰ~Vak~*xƬ0cl;r%7N|o|C3rVQ>9Q J (pg6)tB~3 M˜%yֲs% v/juqOQ;y_M~ȳ3&Kl ƍ;_1sw\3T(*#f؆|$0yW6c-T{IؿsBGz%˲{ybW:.`x6 ^B:|a.c'SVTfMzhkb5*ƺ()Št˂ݲTfҳYsb̠e˨{Oɿqb6r%5$PDKM!y^l8€՗/c;T6~VfeN?a~\Yl gaJoAZ+#J0cvz|,`>X1ѳa;CQȢ2JXq"~oi0onPUG^+1N8˞=3_LHNS;*ڕd|dC=S+rQK\Jsz6(e؏LxMWi?2R~s~g^sR.CY^TGu3B^8ž65MeSucxj¼~NsԛkLV_M<~x|rid VY9/AN*p7vh\3]w1yp4HwֺRvg~giP; <4,bݒ<wdsc\570Gfȭld**rĢvXg'k6Nƛv]簽9IBщ60WGe-tcF,J!YW[I:9eԗ[;;9ِrk<ƞ1|,n-ȔZ.^;vo;İC&V_eÓ $gKe#x́Qlj(ov?OȾ]#p0]FoFin ߜ_bv^r1dthlwJ/gAUϬd.j ?Hsn3&u0Hyz;y&;̊2whe?4x#;a{G9: vqȌqcJ( 7ӿq渡E[G`t;[e l>wy8'淉N>Fo6.bѢ*re8^uv? ڕѸfꄃʱ>ŗS mˑZ/js`'m1?fAWS cu<`ێb^pa wYHˆ]I)cı%mH%=q)ޑB)6gvH G]m>6zft G$|J,ƺ“4$@pTou>'w,Bpbaq>VG4=%[#9{[X~ca]1Ź~| , '+kr'7_f40T 3$fh,yip{w;('޸@?2Iҭ20;RC#'F8qr';`t )`jȀpa^2R~m3jr8ph ȥ:D~U΁u 㧫$p5 Jb("7¥o;R3I(h\̺")T,swoȴRR%:^6`Yr/υ~ᾨ)OqVoF%ZTZ'A(B ƣ dxS:倓$] k0c`zy!OLJ&, }^&>QP6'cgfG#5Ƌ)0T\B'Jnq6p\IMA1:;#.6;@{N%13?'--'πNjcO$C'YuKnjeaIg3>/L}(iY&10B,U'k☮:h7yX\IZrd}\6cI{1rfTa ҭ20;R)SדM%IN7I" >eVG];\oK/S嗮ޘ;W ݁Vwrj 9_98s8a“9I9m }PUDKG{+=]92Md^|>xY8s_:d/jʭSۙK*c S;Q+7p2.d^?sgZՑo`A88rMLv^IxٞqaR\@]P>5 (._IWcPHn9y#3Y".Tb"=gircoy1McY9!vn=YNlznG ExJJ(tdR+A8kӛD2mݹnz1Bq+i, IEC%13_8N87AIf{_rvQܢ|Т8c`\QOg<,= O`{}dMr :)x(LΣ4%4^_OTEIE1Y8f>uCV69Dwe;x3,s~|g;9SH*,̟"9<3P7971/OmuU2:aF6[1~nJA.JZW.gi`?Lk? |X> k?'g]A<\v|ԕQQZDem#˚ H w2lKuOq97׈xoqiŭS^Ysݟg. `Poƛ ՟8=S2[95>e|J O6ݎÐ~nIZӚm3cˮSy릳4Ff?G$VWMX]}f^s{詛.aA?7^wexRU%$0fyP;k-냑\ nc,Cs8̹sb~sp;Xh5b{`i I:\ץ~yfZ OqRo֒ ΁"6hW&6#"""@Q˅!z;rrq6>~-yUpr?m1IMb7q a9/)"4oN>e\pA }^"""""shWrk3y'2t"""39wX5-M~b ;s'DDDDD{)? ?1fu1,RClnrȹJ_1!""""""""""ƴ,""""""""""ƴ,""""""""""UXM&y\7{Sbr oM=apv3W^DCe!.v> !r3|/p}#\3zʅ9}|o`y%m5_Y[|x۫beS h{6o^(x=o?~9`uk>_yy?cobn)E9΃S.~p3;>6{х43ls><4{|\z+Y7#O rC康𾷿KW߾Gy>#̠iLE_g5WQoMzQ vO(c?]~w{?#5—r+d7(y7g>(䪻?Hr?מqy3.{+?ӏzy)~~/<Oxneyz7yu_{6Ň- sdFsW|'׏ =l?r.XGw&kQ/yo3Zg@DDDDDDDDo ZYKm=(I⺒%YxZzrE)P0&:{$CllFG!7< -eE=E&# zyV|'1%?q6? D}vft<4K))&f\/2^Ӈxfn3s{ی6tEDDDDDDDD?y㟯~kfaG)_ۿWߘ_w>Rx_Ͽ}:|0- ?&N93w$!L6- K !ӝ$^>o>z2))R{x&{ʝ,To!>lq!$'g϶Oo}o6oˍy0txom 9 t=ͰI2EDDDDDDD9h!QF>s W7ǯGI%\C3߼SqEvؙQa?7wgDCQRV8 V"Lh(?w蔧kS}"3~35咤o'U7p]wr'/""""""""""gf:xts\{Up̆Wd#褧iXvZJOf3::!y=;Swp wq8g/^odǛFbO=3h+grf.o;rIе|/%ﬥIyOO_#OG\;yՂA'gyb}+ IDAT0IdF y;^ʺU+XI[YRW[MqX9%RW[KU >t.z V9\ryYa`J4|}.b/|\q {o?^Kx\7&Yrok^.+?馫t.}m|[O}{zV=||UMl]tǏߔM  }-+;H>û>~#<!] c" ~o븝kKukƛN|n}c*'N0;>vf뾦l.ռ]7QW.v>UsOFs䂅r#69hSY?Gtg;"""""""""r3ae|,Oٟ<;}*}lEcO)_w L_1m*13嬹r>㛞aG(El;]M[Ê{C5KsY_UO?¡(NrYU7<Rtm^Ϧi?UA_ٻ8٢[]cB ZBCI-/$@b @ WrUZiw*C-ْvFs${[f̝,e+Yۤchlo^!B!^'h';U0Yta{t&碶l*|-Qqwwp iYGԏ$1DfNܪp= 8]s㾽_ շ>g^!B!A'b-Dt;y@&VuR۩n7o^wJLxQ&/ֆ&Zm2-h87a/4?icCz:hVPHo?ҕ!G_ہJk}L0Ԅ(M(^lMl68j>_j)t DgN`Za Ķ0FcˠhB!B!yR9<q~8#kﱜ r>[rv6}rLִ]D*3f`il^XH0ӗ/k@!y E6b}}wspbYjٴΎ+,\RX{@y%*#RƖ6P~B9&yQn̆޽X2{dFnĩFV0YsZV頺ͼwvH$+IE2芳zӺ~{>^@bq6EFBݳvZ%)zo`ER&NM5:S 8rrJ4`JȠ8#]HW4WnZCCStԇi!ZuPgEF~.sYiv~:5'sdu-8SN/ϖTU{uIk/8֛gN[{M ,-oV{pvGCm0=l ΪvǑ ]02'$Y&*`59iMW22U ہCk?Tk k$un5ڟ8lZ˫502&$YW[es3hB!BFTV=tuz0DqPOImS'1HYr]GOIg73R"5u`p箧5ph SʼX+gVZ3X; ,(}Wfjf&bniM<4k - Uݣ_?r Ԏޯ }.L@WSoc߲^ڬMڏ4h!4Ǩ?LRn.qE1P 0^s:~ Dk8[;e FB!Bh=6!bu>S#nd:%)M vvn`jd3x\O!d$H ęhl"5cEv*lr 3AXDd~(& п.guuvKSo&"h 6wg*ݾz[~#d".W0A7(** r\^A7pHՏ4p4ǨTB|d`igkZpKGHEJg9/uo;0a2=TMB!BgJ"N*%˴猯 ȱemZ8M2k=` 2>LU%̨֠xxhhlPx|4>Z+LiLͥuE%m{*Bu8*X]=$A'=MM4EWdQ]ԴsRBk=G1X'G-yg4 lSLہ~7݀ 8 ZcAXHU_ݶJ86 !k.σǫ`o`6 !B!fs5Xo77d{Tld|: e׋0hfG‰t> F#9W{0Ǔ8ZSB =ؗqPصt&컖Ku%2&s&4y)4# [},ˆqbFlWZ՝??h!oc,UhwDZطЊB\eIe(BՏƔx7ÀwS!>9aphwDv8@bB`>W!B!g#{f'5hvG_cWcPBr|4q 86È'22 0'beP|GAf" d0eB4dOхjI 3XK$Q`Ns fdGJf.''cr4R6v'm6̙LMsfig-9̛58dLKۿ}ֵKAm./JkhkZe`.p%d1)+ b ")孷Z?mZ#O}NZҹk#_nD|]H#80#xzhfնFzRw]%c (*LIAQ;e95:JUrELRʊ:c|yW.LgZlU+eP4%JIn7\G3x8-~#+L@ M/-ub<L .{ Ro? h@KSE8HŽ-MO28v[wj  MTlgp|Cu7}B!BΔ㎝4BBJl.Kþa-C}-qNBJ䄩P͚/w,B!B1n_AVb(NDYk'B| b˷:p0G'QMW]2,B!L*)N( f\+[jhw!):"|]Vr94 !B!"j*koE![kgǺu>B!BeB!B!BheuB!B!B: PgB!B!B!ġ!DW*pҕč.|n%+=Ȁ}u~Sc'r_zJ7s8jRi}H$Tȡ^JPP~񗞦25k~u,} 9I}PxQ;q/ L?o~> 8oC+Oq%ǣlt*7s>!BfMKX]1$裭Jv[fdZo3$HӤZ^{B6y=f-$;Eˎ7XtS0: nSBXO_ӏI1q<9+FM榾_]K?? 'p5rܬSb0tֳ7yWXӽ?̧i}~~''VC900ኗxpO G]{bXK:upZʫ+ Ϻ;9)д-)4Zj7?cYp-/5vsʆ*uSpɱIdDVwma]L$##fۚJ^xq;:wdJ玿4˞65Wwj)U̥uI$aڵg«XǶAA9b'rמ^[%\wIGGucm`ٛey~ kGλbdGS'`1qE%lcm~<6pfCj02''ps.~`% *z \h!G'\sDb#ɽ.>'7঵/]=Kxu|O lq3[dߘ̬tڟ1'N,*7mh~97q J!p?m,qpFNWE6Mg_vAγw*XѾt~Kh#^od\֯ɉgeX۟nO,:fZ)>#L{iٍ[Uu|`in_^G'qhۓ1fpAIo,!//HFCCS"8lsS.P؎;T8ao;JM% X^-Bӱ -a s<)s 6y49K)̽7<lRV,_ҿ\D)xݳtYN*vuwI,Y^Oyk,e$3ͫWo3'VmL7'okYȤiţ\HyǠ<8hBY_N8 m>oVo6vx&p/~OCqʭlcq) 3N6j )(:JMrI>C4f\-lpsS 0,٭=*NBc)M;}rta'}[o͂+𽣒)H6ۡ@|C# 4}>Iwp3ՌmM +ީgkcCBiIź/2"6QX&*njţ aF'886B?1IZ6Z178OšD!Di Tw MLt;'-{8 @m aL)$/\çb ֈ7w** Y^隢TqMp&QoG|s盳߂b0(fg$ْ}\xӓZ5)NڙQG ZIo +{QYշ&k:ЧmJ/tѤW%T(\ IDATajH1cn{pt!bT?|yW}} !9Ak9TPe6M8?QHp5kyeJ;GRX#$~簈LE!x FVź%w۹JWF30t-ƭ,[C3N[_v Cvh&:sӍ[8}xOEdR`E ㄎ LgU'.̀XbT:;+di xFVg SBT0" t9 ty=8bbOGDc-#?[_dg}!l\/I?u/f՞>tɛ|u}Ȫ~./+_ޕ/! _q}?a͹YGPqv!Wht*UуO7]NFlvDl~|4 mdOm$cjo 1>Lwp؝*KWr{>=\Dž:{ Tyt@x{MFL$`p/fq{7w<]K%.Z ec$=x25b<ڳVq<ݏi& 7緡 qx-[M}9b>ӌvSq\Araaew ,)I_(z뇷nRKuQB;ď6]W %iEx ҆qFPq⋱>܌u :&4kLMeŶaF {Xve6Y=>M[*OmHѻxC>$"qp$~P=[6tRS7:+$MќqQ\#盃o_]*s@QI( %#uX 1`o?625ܽuebڴoEIP(BK:w˹fZlIGr K9n>|-;m6h S{ZRG .꿕TSͿZǧpeD[eR]iG=1¿+zqrcVv5PS8\el=z!q_>#;8G\|z<8?>ji$!=I3 j-3lȟMsa dp\,h,odU{Z.u1מ_8Z5'Ea`&H#P‰ |aFэCkR2/핬Yk}Ѯp? N7ijj_S%wsu.̎+0EfFJJzmY!Ę ǽ 7rqcW֯ec/QqJۋä;g9;ƌѽ{slſ_T}?HLY*O?qٷo52jݯ<]{;Oɽκ7Z^wxW.wn:ز v/5~GyNnwUz~͗]>i<ޚ+?{7Efl)~G_*YR_̩0k !\ʹ^d0W7Ws1DMl\|1f1`4󇫖~?H5on2`1;+۔|N,83t9X^կPx"1H}9\pEB}[Tlh]:i8 `|a>uWng)tn4wG*̼D:-|y ^|~/ Mdv,f?80g%l~V_`׹;qEDh\_^kWWG>B}<",yӍT~P?aBN8pMQȼ޺[XWA:LSnϣsNoA'[Ϫô:x^X兛Z[MclHEHoRǡ/cy'\C<[T iC̛7߶PFաĕ&~ty 5ejv]x2\1w'XxRTF)ŹE[oОPht+;g?8u+ibir%q@clHEHoR!'*?MV>, Pv0 %ɠ|,BRJUl͜;-0\Yy/KZ c_ %b>^+u"vRggy+4B!B"䖩^CJW5nOl;$Zͮc !ƄL*i q!S\ ޻UOŰ;d|+B!xνƑ7smBޯNqMO\sϟB!"4O*Zv`MꤶSK7[{o@ƙL^ MIA~5:3bX87a2in)x?!B!D(hTVkoBD0g;vos$1R2}zVJ2<1iĄaTvu\M ӓJ3OHYG'.Ά/˩qbDbuakmOW'>lKk>ؓX%̛E-{1:L2S<8lZCcwDf[B\&9 $3ٳf1=%j7Σf&3?[-y:h&bn Ę}twz}F&RZNjBQ&O/&mPoۋBXBJ2I1uvsvu nii5ov~5JLOVք ehbêjdPgɟ=lCJQO@LVS f*!;^JW#/@{}3~M>nsB1)ΚΔ&fM y3'疍'c !B-7i?i3sȎ5bM(p e3!-Oq'yiv^ꛨnBΡHeΑy\QKU D=g[.jJ+mttiaX:RQKMRT98کiE%)# ,gwU/> 2!'wS N19%̟Eͬo[Œ IYVQMeCrszqGfZ޹&MLrl^q%&%*vP${xb:ɱl\WN=w+LvPLUM#uRse_oKXBy⢡ݍݘKUh:Ӟ?T/Wkk_ꉟDŽX,Dw7txltʤ/,!XmIy %% QkN)iɨMUlD; &iٷ*q]?~\l;Zh7m㳎vs7~@ج.b'L 7N][/>swOT#\yXb3PF{ڭVjۗ7 . >= ĸZXUVKN'ff@Yk_Un/`<^{1#1+efNH/&širc)H ֵN%;<[SITmll7U-Σa"-/ -yPSQKk tR䒟VCk}ߪ5ցor`uE:7l^Mvta%ujIտI@5i`iMw4k_&zO촏"ƿP.;ɟ>i9!ZV7Pk'It ]?~\d;ZZ:ri7ǭ?ԯ@kno+7ELn5lUQ*B!'U] DyT8GipRԉ},~v9qR.ɣƊ椳܇(ǚ:wС&֐sJ ulbM`JkBaP3)LҊ(23,x+hp!WJa-vg>';qbUHBU1KafqfL&,Lє׉վӋHjZ՝??>TD}l'>|؝ty`4 ty =6-ډ/4h;㕦t-Fځ*?g|>7M/P^NZɆi̛>,ݫhlh!B!DPi:M`{EY@I~ ES30w=Xg FLF=qzqbLF{f &Dcs(Sax0eNdAw*kPWg4Vj"RȊvPacPgy4Q}Cy L=_4#'hYŚ.\>3PF(TTVS͟vr zkoV}-3D$51Tx^CJC^#@k}Hs|:@륽lbuB!BgJ"N*%˴猠 OӲ6ou-k=ez@d|2œ)JA9400T< _֊-3S8w%Ssi]QI۞Pݸ<*Φ VWN7~}1X'G-yg4 RLہo>@G  wCv8Lڟ^hdid3#et=U!B\ßK6յtx\%vP륥b#KQO({^Tń< GwG3;NT01IM݃!>DHP<[Ůմ34T{Dw2w;5Ms (VߌmijTqO!>7$O+/C )*D#n`!FC_G0 H{9p_F_WkZ ty.*S!.βߤ*BՏƔg}#. piI*)0-fKa1cB!Ÿ>{f'5hvG~cWcPBr|4q 82È'22Sp"\dH瘙yd&KJFS&D~@T:]$ 2cDb ō>Z40mF6YIqd2{r2&G#mcw-ll sfS {hYK{tfM 7523 8{|O;ㇶ=\ڟva1Xڝln5?-Ԡ}(B!yĔ&:TJh58(+`zQGf06ʖl@!dGfwڒ *6NTՉ4rS 3jVmkiul-t2&E`×Ըx>[5+\Y|.-5;U-)ә>[=l`J7EMI'¨fRe?$T[i$L`gbr2I>]^2K)WhfCy-{/TQA|&?xpcK#EӆeK5Ak#_Lj~} @ݲ[Z0S&+m쪲S0NU(@k}qRqwq-sID ''C}qҰoX˺]:< S3SJ|&4h&v˝JB!B!"hb2FX"8KweAPOG!B!Bp2;'q9l)].B!B!,!B!B!B3C3 B!B!!B!B!B!4Ie!B!B!hR .488J >,=cqÊGO'2I'r̿q561&?sČm'=5ϝ;>e_SYۓ+gO˲ z9Gp٩׃^<~˫q4|kAh%$gSy]ʥ_ߗo`n~ 'p}ڻ,MM3; O^+ٿsB2*ۻ|l%+)n?=wTukʟO]zc/ λ_z%VwxsD2hqp g2pQӹ%YcBNE>F@؞eJ|9ߞh\:uX IDATshس?r?9O4If3}nv8\B%'r_J ,Ʊ'a9!B!ck+3E{ ~x5qp=ɝYƜGfK 8cҸ|"Yͣo?tc2}EK:\՗8ܝ)̈l"$s5i?#s~r7WVҠ۞5Ii-/Pr.ʈ!=S<[Uֿ%. 1Y\0]1~TQcޗ=U߰S9yʷX7U^`%{.~p.s l˄`*«ϋǧ}] ]V=hW<؆.]n/,XƮ Z| -ݔH/zg잟qG &ac9g?evrC|aXꭐm\~ xeSs8{ZYp&˹7:yrG7d[GFem]\]7Ɯ:wdg_6mh8`9ZcOlijwQ<#DrY$Vmgwl}*b{c2=aYβ/ 7?[?<c st-W5 XŒ(&cwR{$N&kɼU9 Z%k7meWQv'be\{߿;׬`t&M°t׾+4Eg?1Ν<8hB0Mfޜhl_~7Tg]˚F.VƠPSϻ{FMew 2kX=Mvvh潗q^=K 'ڡy̱Q/MocT=nv Py=^<( Q!B!M+ ǵ?;*~gvstTG_,k͚ty3?fm`HR0O8 ꡥ9-ztf庻ne}Fc? TnC4twux_S'}{=+gn&6!tJg竏XMu;PZ{|͡>} 5xoػ|kn G ?FPҽ3<;]?+o-\^S }+5Cp<.z!$tqC-q8[U'Mj*33L(;z$ӽ%DRg$*{CJʫ1UB!B! |WlW3ڟqb$Uq9JȤg0fy'.'4l_DK%8*=nT%O9@.;+Y2"[|X;='(N|$vWy[7kS:le‹~MkT$ֿF}:F2Zz"n˫V1f|UddחriB ? r\m'*7~ge{(sƯSt2&m%4TRSIӵy"r D3'gr3]"?"u\ARo1"^a@yy??_}NhƫKoYu+Zdtnֽ,8psÏ"|r3T=Ծ3~37w޿q߾~_?΍lud۽#>nq]5[x`dX{όn%XO;CգK7d~P>;8~{x%72H`N`&IQJQJVTOp*^z,<72d gqW[pԾ_m?bzc\,Dz {t  KwXXg^+ss(v ,/xV,SaC(B!BO=g 12üxRyAo’>?}{n(f[4WŸ/Uy PL?R#;wi<)3]KytFvB!B!&)DS XY/|<mg:_:%_??*yC 7Y(f֜J;xWB!B!Y,0\uzp}<_LEԜ,K{L0QW*Gs;@(-B!BLʆy\|Al_?xߕC?H !B!B!B!B!B!4=B!B!Bh&Ae!B!B!xK\g}fOXB-ټ>$gbqX5?gajAe9!n&WGxR:Y1`;%瓧`Nb<ϞIt" 4 V2GÝ X}LBښ;7/9sIvG?OdIew tyb:4 vnMB?#x״TS=B!B`k{T0js 4=㳵GC t* vj+f(/mH5cR8[ؽiX*躝|k $# |yßA0!?!B!grH2+OECbL*xDx+$.A=v0Mb|V6n f2@P@5eK1ձs )L-VXT`tF9&(/MT#g䳢ƾlyQ&vơ3Kl pwRbEj"q1q+npRDॱyrIc[gtx˜1K Z%,N&.TL-ճZ*}4y 5Q53#/مRC硊g?ߴ;:.}ϟ\)ۺDlr8>G8rg4PZ\Bj&{~O#ט8ԋ!6ht9ʇt5 8|^s?L:vi}([~?hl_烠C|.B!BAegBX({ oLt˩luM.8:/>@53%k4^?5N2?k~wq7M_1]KB!J2*6N/#1U4Q]G~v"8i?cЃǃ˫i7ZIJInJ렜(FL0fmw*g@E쥾F^A"!jCIpXieHu$d§MW"N׃gH{p{`< 1sY>SGlt0eB^/!AEEzU?՚CGzO?WǠSq_D!BT_I*q,>tá+<֮=^diû[!@GXLysɟAiȕ~Ro0T@ŋFf-O~a&մjՅӭh䳚'q 67ӒKFxj; Cj}O#|>Pz! ҿJ\NjJΆ /Е0S?՚C=8a~.Cuq 6Ck6l,](=Tŀ8KOg {` !ty*&sis!N?kCl4-˸U\.7/#$JՁCXdjwǰafjȡ Մe0~=O!&3x~/]$D&f>xѭC_5Pus^ C[+W.B!B8)KL mX0HiE( rsXCZ)PNMB|BgXͩd*]wў.T݅df%b6Ggk [v7jʢr-KAAQ;)ZP$ g:RTY?n? X0'Vk%ujdKTrWq`moȋPNK_Fv9#$)\m!-J*z8᡹^qt#o5c_N&#{ 8>jOQy91Wikcv"su򷠍1hm_g}~>|9F\"B!.eYrC@JoKGaeId.JrqRa-BLkGsB!>t IDATgGb҇e6BxO3pdOG!JP>!B!|&A50ӈNyi-pu!B|.B!b/B!B!Bh vB!B!BLTB!B!BB!B!Bh]7.\Htp209J"?UD?38r0saHOtMo|r(]㲥|5}w'rO;w}>?}*^ ǘq|>W7z;jߌN.`=uKPӍg;yB!B! dv5]VZmͻ.ʧvہ}Սa?ȓ},Y;3(̖k'>#e<|k똱2+5!1uwyEYB!BL3DjBt7ҍ6gd)u1QV~1]xF/7r_L=Gϛ u 1ʞw{A_Zy!!B!BGEEc92˗VƓنmo/s"0ο׷Bb\;y9c6~#EnbYdʍ=oǟnf{_cYy_ٲ\~6n/>FQ%+ME}>cyV愖ukʟ/b<_~o#ilشO?|?k yzr. &M1̹g݇>h㻼-1>?[Dv򍍴M20r x}l rr@y.ٯŢNk~p.<\0VH>}!?uox|NNV8bTlV*:L!E>|~ڈz!~!|g{昪?9^>W]WfM.k6ϼ|6w͙8ku՟>M\/Ce{㧏Ц%sJVĢ_9ԋcl~sĵkM~۫G~mbq-7#F~3^&&Y?ȓf}'ospK3%%ᯩfpU7cfn6wSkzЏVP9!5o^>sR/t46ҡKLn|ݩ%\oܷϧzF%?[2or;Qow߻>]|gJ+?Q]۞QWz9͆b>0 фT7Z'>䙺ذv4K{ԯEeoof0¯?ans3;; ĝq&8 I_M|j֋҃ =\XZi>#w_>e^JS+oN`q VEOTnn}-;tQwrVv/Y:x=\ ~$U=`M~*|z !B!A1]o+6V]_8{.]ÎEl2ڟ,\uJfnңt QY5dvuv\btpp=no?b]ov0.}ɟ3;'O)utŜ6XA^|=ē˖}P sY4Go'յockJ+n?K-aPr0[3>`ڝ+Ѷ=T[Gg oSRM\G1{Vj:-%O 9N^Ì$sE}h9&w';Gk76"Pm c^V*ʯZ:ݡcUn,xly}ؕ8 !B! [úWb8@oPdHm?+țĥ>9מXLC_,g*38}F/kfQ5荄osӸ;ymfW tcR}H}Yuicc^\nzzp<zxP0%B!BT^7yksO_Cz:^+~X05k|f~ʑ!I<ဨ縷9ЙI[ﻖ9W,eH3Hm򣺳x\tBHе=ZpNTffPv}M 8uԋb"jD3=!}%PUŒqaw*e!B!ӇЦ‹~MkTNn*.BB ,&t샶W01cї땨q$@эD^<(eǴ`%K+$/_G_Cz[V*ʍQf5.*pO$׋):xE''TzwW+-v7æ_f_?a_e7\G/:Z]?݉?^ZP|'~˟_́.#9?dB{3?p`ykIbdGf)POsrt>Qd\OS [( \x^,ΊRʏh^߫|[G~Xqgź#^>z˿za\wViGYȶ]X i& }U%lTNK[yM{S/!4/`zQV]s )wFpeaMß'??jcE-B!sMcPܻ>I5MvEJk9wqmOr^k~[ys/r/8yjwP|/t.iq`o#/0BUsYs<~QW?;z^jHƫKo_ܠ%w;7-=7}n;v}$<,OڗoN?lqo4[._BRΦ]G[xАǙKb1loc =V9DAge*F}MYrI8PG>R;]$JGY+~X9[Q w>1oe/ݠYȿ^o$a/8(N4.\JYK?#cr5'&\¯Ⱥa^r,ԋ3/8\Rq-#lbg=o5 zB!B1)r3̻|!w^t!Xgğ=7(-7叿b&BKUu/Le>N5m}M1i _n O7m?m>B!BqԌRٜZ@^f.k2_澏w@OL/bk~o/Rg8_%FũTCc7q@T }% U̬9?w6B!BL3 _$(r!}}fߞ'~]A_>q}o?5IV/*od!1mgu6g&%BCr&fU3ϝA~vqTzjf{u'v_r>y ,.cJ'@cG6$b3 ik7e ϞϙKV㇢iSh&GNd\I-p?;M\<1[˃&oh՟?n^^/S}O !B!q6@ ZqKC8aN69+NB9>[{T\==ty~QHN'ϬPo=MVŖ]Tg?.BJVE5ЗAϕ]S\ƛs^#T}ԼKL?!B!T9l 'ǎ JLQ./C<>z\Z뢡SbBB1<76ccWJvzvTy44?ymTϟB!b*TV25P!4*'9SYrzizwN Kgn^2&.Zx0̪IJhl2wdzmV?|TA,ƥ3P'VK :Vx|Nɏ&ܨE4U*)34z鴰 A `V If!wVK3w*`Meq~ɑ+f(/mH5cR8[ؽmac|vўA+d,^̂6l;|c=k5݉ov—5J VLZZ%v&) ^͔l͗ j³sJv/e;{IM&!\?8ʿEaN1!Be2B7 Hs{?4OA>h Hũ1v?nߋ"B!!ɬƌy,\΍UX^*aDvj6uͤfglMbU)ǦLfI &LG|VQwxJx*˖ghb׎jj(93YTϖOw'5M.V&k GzBց}9YYk7N,VZǛKx[#e[ӣM稝y GJK(Qd/8risX>7g}5[z1ĦRQ!M͇t5 8<hmM&t4g|(~?jl_`C7FR&?*B!𿱃6J71&`Q;PAҙSlp4^}kjJx$Ѻ^jharBQ릷m#qf VEd7RjϴqnGO?j@rV2f[GY1R[YG@Id'bilm&;0œQ>ןy^vxN:uQ$Fcm"Df"?֝e6 LH mU4l |U69)uX\VId-SShxˡ?ҚD7VimM$:6V8lȡy0ʾd/:wyML f*ޅ׼)uiMW#?j"˱MyFkyyPK6h_>43ɉjk5->C!BtcU7]nta!v*[b9k66UؽIdg[NAW'Qˆ2v>(SM :*,YkbNbVWytׁO!%ʌ`#H|f&Ң3b)(&Io+Uwo.0vSQ(`NBW,.Y(uU[ u4falLZj8J\?q-XB>"oA xn#M]^%(+{hk›b:MS6q-Z#i7_3@MWJW3%y#{ !B!EgTl:[^0FGgFWDvىww#PK_q*D2OGBV*ѽ-|4Ƴ/B!:FTX|ZCWy]{h8VӲw{(mot$77Dz㰠ހQvxjh"R i}1 .rW)* ;% P%RYz#'0juPՅӭh䳚mq p67ӒKFxj;OӚ݂V\0??^7$tP۸]q6j]Cuq 6Ck6l,](=Tŀ8KOg {` !ty*&sis!N?kCl4-˸b2ӈw[_?N:HeP&f>xC_)5piC<e5t ]Lu`݃1oXAm9?g0 IDAT6 }T1N=9.si7 WîN_*DG *8 XoƟw|#:'1=.=/B!GzFx_-qDT7ڙӆM`fn" =UݶtF!zcj[Mt7Jh4Y3#tVs6}*]nԴxrҬTu>ǽ4ӕ}T &;/KLJV@xwzR pJxo+-PLI#a:"SfRnDevhtw 'ɡRfy_3Y޺Nn&sq69fջiUÈ8CowcriB!Ǯq6\N1۰cҊP,02(SOT=] +Kl҃n=ՔE[0 vRQ^<^&Q8׉3H: rkBTz:[(8@ሀJ*fg3{r&r$xH՟6u)O\99Z+oW#[\妒;/Pk{;lG^īvZȴ4}*x7Ԟd7R[pƯy]^W~ʳ()䌹 N[3tm߂6~=|x|wǜ}12 ș9Sw Q!B!D(̒!Ӆ!'S \E. ;E"-O}9iqR^T> Ͽǻ>ן>{Hgic8>+Ό|5EoE?r|:E%dpϳgE\!B!OzN.\Dt\+]prS;q>ԋڰvIӋ>{׬rK؂cB\ffK ~Ws{25xeuXk≚:ǻ^μ",B!B!Y"5!^F3Hy^(+M]U#_JcPΣ:hVބNeϻ /-C<ΐB!BٸAe3W0C?R~sU_ѣ'Bn89q`Uß7+*6 C;R+Y~݅Yg=1S Qsf+8!˄ k˚C?ay\*԰gᆪ*·6iw{$ئlB(!PJB  B^Z%B$j!T`\d[re˶z]Me}lI]i"?s|{g晙ggWW?dBeN^|#礓e[f>jLT{ՖFVL~$͇)4:UDݶgB.[v{z!el"zѣ<{[k{]71lȿ>#vSG<޼m.U̹hv}e"y|,jLqEgr xƍ Ri݄Wl4f`}E!B!_S_g܋Y-roNFGS#wuHHﺕbU jj*K>_n_; Xߞ #j n\ԐJ\iol]6a渋/]nO(aSg:o-_)f /,aս1z>?^9ֱnu;yV =[}̓] ^M&foF(>da lїC! AUhzٹ~G덝ͥoXg(d5 yM|5ȑ˝8#9,s系6u:x}7<5qU?zY N[Ntttaky>G/ۖ L)cnNb^l,}-; (nƺfhvp33| kJm 5tZsÄB!B*=-se|*~W؆{XK窳aZFgmj0.C_s1=羚8`_ wj6տNC\By/م % /b*?zJvB omWp'|8s[|vk{tS1BRL8O 48ʙDَl{b >3I-gqu=lr䰍6WOk'NR`uA6jBԇ}\w_u] 1?w w{=$R}օB!B'N}r;PwY?S쏏vDw yt _]UQQ 8Nmx{n+/N {~d9FbE"].~q)GE1(]!.1R87OVo5oԦ E? 8kpE1 6GcDt*N793hxJ(_x>z8sGjn~ WåLe<ͼz^2tu%mׁ7b5rC$Xiشurl.'7q>?@>RP˪dOaՀ''  B!B!ġI[Rِ3zW~aKfφ˘Zּk^Kpߧl'2b(&~\`#b6tՏ߉? .g~n?5V0i?ћ8{i>KȬy7m$ *JU@,{l6i`{Vξevsunyw7DD^;nc4L2le/ 9IDx.2AUF,py !B!8"iHmF2j~ y塨^$b\;@0/">;K;iϽSNqT]OeRr%3ٸwP^wn^,Zfx6":%,ǶF=LiAV|ecW%)Ύ[^9s\CkXNc6"OQm> qO 6̃yV1מjAǀ> QT4~G! ]]RSD=% Ni半A?n<}]3)7&S\Ԡ9]lFqeSɶ5aEOQQ,T1Ά!B!ШIekTҍ\;~|exv,1ƷwqrvoCl|'<ӟoqAy_~k @&ʕ Mg`W[W>-O!w}rwf 5yu,-.,{0`?_B{no<;|ei7~ont;6*_f6~pe\.&%GGS5[|Oz}ʖI|P` V5cXsQF;ەfrIf±gb?]IohGX\ KI6+ e!B!Y,a<qjyTXLL'odO q8:+c9kiޯfko'B!BO*fr]1kݙ{J_VpnzdB!B!B +B!B!Bh&{* !B!B!LB!B!B!4'%'ܧD2&Z{,7BZsC+1*NNF9шt~|&49)Ɓ%Ğ pc"w ּٜ>?G0=?S^q)n@3c2Ç%5 =m S3r5tLpPoso0;l̸vrC3.'T&<B!bbr~JTn Vb^NDMBTj&zY^k:#pZM)m7k7*C%8-t 2c4<刈@Z7B!LsRc%˅J\1:- [Ji8KD$C{c36^;oo]>mJ|.@ l/%"!$GЫz:ݴTfscsڎ8Dg2+?hEA {)u4{)Ks{lrjG&d2}O;^&**h{b-#q @\v6E [(V*2OL+ 5@έk &!OuHͬz.;HVvZ^CrI4B!8PFN*Gr̉SI{;霙14r>1Ư.bqۖ-6=bMg\bmԡX8zA*elmSw<+:$bc"6lsilA1.XM꾷߄;kQe|2'D_5?X?z=РIajF,f#Na29H6N]8=~ǕVc*cp_dvLD95b;5&~@X][(4OuL\IGQ݅/7nBq^48B!a)YGGDh׻3ѱZ:IIBA ǀ,LX1Au;),N&=Fmd2Q^+t5qd fg;;TR:=7P'^cǠ? c /m8q1 (i!oHougiX *3R={+`;)u t?&8UM}ƕVcΘ0_dv~aϚ؎p_a}F>~J{4]c0(?A|=PGB!B%yim!K9.dh7C[VJL]8Y9VWѶD_\! JXt77RX@VNs"N!}l}n gn{e@9.y*j]p&C1 ]nz50zWڎ~y0ídqp_mq(Vxٸ۱1?c,wC#׫10/YC#[XZiB4]a?0CJC萟GB!B˫.eź::]Tm,eŪJzVlbŗY>ƕ(*>Nvm="9>mF_> c_;~)N7hh F8{x}~TӀwCL# BeLDvT1V"S!6:r0^n]t8H!>xŏrt혀h7_j!=}s] U7 3jC]?b1 8rGU~#Ϗ0 luo PF0iGl q{RƸ8WZzLG<B!" F;@.kO; -\jQz.N\]_z)Ϭ=4ﬣ=*󦐓KbB32((jSU֠5(Haּ,tA/ZTjo{\4hW9fg17LjioeG]BZZ~ZϧukkBb\e|:OTܻ6w_\QA<9R9C>cq=N'фzu7r=Ϸ+~HYy$s YC:([ YJ4^-U]Qux6vlo!Fizajz= uHGB!B2mIÉSqgCAV!∥NEIJֳnn,B! c^2$B!B!bIe)7ňegkY-MB!B!BLIB!B!B;B!B!BqB!B!B$,B!B!BIT1ԫys~W׬ӌa٤Dr򟻳ݧ|z!1M{lkˑ9B!B!đhrie_'<}I. 44xmoбs\3c7v2Zm!^:%=;ޚ__3KwO;?=Oc_aL.;!ts{n~l{(aU_gRYxv.mE'SOİkO`kryѯ큽嚧d 9eV4qzue5\9+{MS\BloN9/ {N3FI'?:H˶:|\7xT%-rY%U<J;6jJ{5?ej_64oaך=XH+v1gKq`h5~cB!B!,FN*Wm<}%ԯsbƜd$w+p~obؐ)}G~y`']u3X}bқ/$r]\߼dǮ MIUoßuz|dnKOQPsdh D2XTecGV\\{4fcȹh.='77 g3n7f5A+_0g3>? ߯b3pq[|s[K)=X0_SDt6 Wl|wߣ;+@x ǟvh#?`4n=1Fߞ22Yr-z0J4oT!B!(_=[}̓] ^MǾAl_PMMeI߃fB :Ī.6ux?^9ֱnu;yVJ(bzD |o|\ޛڸ3CՍQ|:Aٰ!%Z8Z;q(A\ю`M ;͜$#덝ͥg:m]8χWSej[( +QK/29%n_EjFvq1s{jRp(;_`Ҏ1#}+<7!u*Λgfc) YtvjZ^M]^+`Jh76^BiioG#e=~{ԿAJ?~k{P_fvVv}~Z7q*B!Bd+;ۢ9sƙ3p6kk ?Y>}3G.Yf,gdN䝿g|<A e0 p7_ \g_On=gk_z;)}-|Zя>e;_r0dϭNv֪~ب,vA?W[&dSټ/a6O tx<~T+x2(e{sm) FC\B˜wձ sArU[u1]Fl#lnoo& vG 6BhU;Z1N;9jV 5~B!B!8b~` Uµiorf~+._g<%]x%h t\ ZSȏBϰ¹|b}fyfY E? 8kZ@`$z6=FpÕgrp__s%vg/BK$U@Pڼ_BǕp-SYfO}3=zt8BcX6APTUNtFb q{ nN#h3%4z;ngأTE VkoB3ZׇVޭ&!v{O0 -~7棏l|NHヤlN:RBB!B!\'QiMw4%dּObiV=:IM*nh.^V?v?~'+7{V+t|tZž 7-k^5/Bܥ\{OSrr6'/MVVJHw͕Sz&65/:ž,={XW0%ANJݗk=~@Fr$es&6#s1'HLtvFs>I#Q&l0A!C(/i_|uMcj鍵|u+H wB!B1iiH*N<nIdKQ1ɣ@5[0} -勾%wGl$U|^/DDDiGgo۾W/gN,_m;G vҰ-{tReG׻EͰgtSt/yk8/O| YE55%;q`?_ϳhl6lthig02޹X0}{sro5]>LScgMߖ3Y)L{YUCQ-`΄Ŀy\A[5TvM>/|R!B!mIeYԟ{%ky䃚 ^)ŪW`׿i[#qݴKşxu|woE%\͵Ɏ7Љ8+z<z?r 9.|D"s3}]OeRr%3ٸP*.)5|EI0+Dm&R,ttc󄶆Ѹh.dW6vxQ9xeloJML.87*z؛ؿr;i}^ ߰3cQ{/+_g#[OfQiDoKUC{c3v]# 5hk!r[Q~Tm5>"[y~#o^ !B!1 mr+壦tsm=~y5Dm,{T=Zz]\umD[g7u3Ao2?|vmRU̲m˸]LJj׷ퟠ-~M+"# |[ ; G}Y1`湼\ HsՋ!%v+y|lbb~ 5?SSr5dzĪCoWCތlx]<~rb^:;@Ö^G7&v>J+.-isQQȻƲ\U?c&5jUwϿ+qEJKy5j3k|^Ν)bL.v+S9 oB/WCpҿMT]CTPy)B!BË2mTM)K̛x\= ?64~b>Ž Kڸ; 4o?C \*_ZߧnB!B!a5JeKz19,wȩVJB4?v&] 1QB_sr,1|y>MU,_' e!B!BLQ. _3zG~H'( SYoϊY3ErB!B!84o1,L."f)}GYwC~B!B!BqRY!B!B!Cw+ B!B!!Ie!B!B!铒vd"OABG=O!-9!gb 9avEqní9ǥtaM$7\./|,,WiS(I&Oℓi1leɄ[K.3n(y܉'eXvv<,?2qf\s\Jti~WG;Fe}^0B!B!9l%.Co]>ѰK:DDb176csHV` }&͸$=)XԎ^c{'E8qN ?2ہB!B!OcRY!&BوG+&O;LIdid{ 4oÍ6*2"!$GЫz:ݴTfscw߹9idgr)ZoULdr^&**hp.klN*޻yucPo\Æ&tDex[н$ 3Ȉ`Rx-lVKӞwBu|{I#Y1'n1j ?y"h-wlߴe\i Ǥ(ۊ-~ iX Az\͔-\\f'-T((<a~1d=rVhloدoa@5}׫nl& $[+WtitN0#6BB!B!-#'#R9ĩ$tNYCO]9mWv6,\19{yXm5pуkl,ɑF{:z*PH6FVol[AcHNbY8?c[78b/amԡX8zA*elmSw "[mBic~P,\Km𨑤Oa=kڍ;i8=ad !-q*pZ7|iu\^? XR.D|!#0&hF"*J1ħS\jZۡ\Mc $1EEc{}PV* IDATT5Mx}F y9,\gW ZmH f3mV&/jP Wn Yq$hB!B!am䤲U. I9*M;銢ptRaP,P񍐀P꺩@wv;u݃o@mDG4-5G;+jGW ~o]መBEҺy eM HR~7;9zBiu|@{}+ik5-gg8rRM;mj,a4ey+ߍ v&2$cMy3. Z mcsZX+wpGhk) LDҢjqMZZ;n'oN,fm@nW!X c !B!bF|TĭD`hn 0<6q:qw%gjWLVQ.SRcԇ2tAG[wС ]iqи&&+I|6sc^'vB\uoTg uN nLӀŖGTrF鷃ՎATڛ:-\+q1 7e fwGrC!VHx}j+FLeѱ9|pt̊ipzǀƠzx1a+B!B!&=c916:QW7(K`V:zwsK%s>dU /mMN\^IGtNRҧPP1&LX1A>;),N&=Fmd2Q^+==⬵P7Jv zwt*>_`й^?A@c;(aA fhՔ:k:GMD7ǫ jq3GB!B!8 Qwj1=B.ݳh',"вnh=7]7Pfot(G,>!ٝ$:?0I p6ErmOG>~Osj؞3(aLKaYi:͉X:mԶzIکmOq GCwg8k5bL`GZ ~[qvR1X`C!6:XnC{Ey:u,tu+B\R,6F%ݸ;uB!B!9]Nf=4xpD]46p:quk'N5)$Ekf֞: sdf { r:]rK~FI1$g3sJUOD&CՌbê 5͸̞Efb,9̟DUہgZ4R݁1#Y{2Y~wySI%1! f̝}=F98 -Κk?JL\GZ{p;XKcuLgZ0*:"vh-7]O V⢇Ju-Tm~,idXuNgVqGnka\B!B!c|hf:T$aua GO#e)gQlVjz(8Yy|R޵wQ`d&c1CGk k75D]UlɧxYErjCLt5dFA e zqֲ^Ys3=VGut7v‚t fWvw۩vLTvZz2q4Dn(8k5ZLXGOp;X׺5[s?P:Um':vh+7nѓAVRJkW ʧ1EʭMIיGn쥋&%A֎k\% !B!PL>U,Pbr8qq*lhڄ8gpR~%+w:ɧ!Sf|7B!B!8|c7\1#'$3}z֮l< &j6H^v,>v݈ kJ:$,B!BL o',&)7ňegkY-n;h!&훫b5aċ`'BnMmUlu!!Bٻ;-eIH%zVpQdkakXWeݵoݵbH @ ˤLf&v$@$ ~!s9;gBqt/B!B!BLB!B!B3 B!B!BLBe!B!B!>P6U8]7tAy-oXӧ釬_>=3nͿ%!Hnټ~Ah?L!<9|_%yI??7=O8/Kd|D$;poS!B!B:Fn5e"o<:iFrL'579?Ђŭb<ՋBy?0M Xo%4-sMxi(Z?Wfe|6!B!B!|5K[|9#O#+>1DIun6 F*?wPRT )Y桭rJer,ʄB!B!| 8Nv6b!SpeV@?;[w~gj>Jt.'d`hh»llRX/;nrfMM#Jgewr>o\>tAH?IޕŖ'n)zCNOdGWq[f$g)ƾK`_b_ɝ X=V+q3^2Ǿ#k:7So)+붧Ő\qq(AdnKf7WBX_`'+<̝29쀆dg|U7զct@}|SԹW'' c/[u[X[BrF8A.3ud^utPޅKUi7۩o%fUXb6֫@ _ùhǬxjSzN~V7Sfo عi[vsߒrL9Yv_:LMڪf97FC||}T*w[qɊuh!B!B!Ŭtqc{kp@\&3ft(΂ul/F[z=&A֫!WO׼2-gph-i-a]'~,O]OC9yoФsƜ1} ߵ;ճ{;~;8m*Fj;ܦ]AbiPn4ST/UMloѐw![cfŻ''քYHA3l[)r0q<|*n_VEۄqT'"bl, e &alȵ[f?B 8ToT u0=a!`wP۬.T%0A B=)'vz-eԩ$P {z(AzqaI,B!B!^ٰg;[=1jÔ4zfճg7V `JJ~w~9ZXUTݙSZN]=mK/V3Ļq՝[G>-a(~3J vj6͙7#_n N8҆F6}S&1R*FJc9ifۊyFF.ÙT͆Z3Zdm+OWT\5"zL4I7kdfU$;UTGQQ+B!B!z}j+kgJ^gǣ/T62,X #'xpWx_0;KnMpxCop7ڋzvLLgq![yAu}޻~Yw)WPWP=Jƹsxꋊ-՟t/o6QZci\tX:,!֊7Bդ2:]Ê͇f1c E3/SK{#>4 B @`xl혺VzkykE O?'K8׊̸R'^<2Yx7?3a˒U]׭Ƃ&\R˦]ml 28kpe%| ~10ŊeUj?-YYPQJ B!B!įZXx[[}׽Z˻k(fms{7Yq7+9[- ;Xt95t4Pz)%.W{v\U7.ZvS{>,PxxŬ)X:z?RbJuWz* (REB5~_75GToy j29}J$M{aԶɗeQ* Ey&]q O 6&/UZ|7[EwsAi^% ɝۋq .I?ecp:رootE{llXL± 3=`6},h_7Mt05*- Ø>AK5THB!B!į2j#('OxB!B!BPY!B!B!Ġ=B!B!BLBe!B!B!>FD=ۑØrZh&)d@GuRde +-kYS?/z<9ؽ:IՉ?-?x LQ)dL$ZCbByhm5{Ў@ qy5D_cxN#b~^!B!CѦHxguN7Ӡ,KA=BHl"*5V\]d'6\kLx dyu7qg ^!B!CPf$a%"0FUqvpzmy]& `zLfKN8%{!B!(cRM0#[ LƔӲIvS8PQɌ%&ԀVao]eE2Q$镮3R9evj_-PD&F%2zO'fS%%54u+˘6Bn^98T[6`@ !IYLLKa![j~\MVf 7WRSYWmmVI< 5Z[w{~W~/<|w`}|ϼ}JfΈÜ_)2Pu^:Tk LJ 6TRv~<%P i.2Fc+,`cMa8qe*q)Q8ZMl/Mg:oƴ`wr%2S.g3~g7&rqim',. RCaXBVvӼ{!B!bCXff${_79{)*mQes)R$GfogMҁcx81z;ۥٶf%(DZos/L-8 g&3mš X:^Qw80&Pq;>P`L۶W۱wc<ӎM%VGqA965&OՒ pRQbz| Q;Q$Sh޸oa_W~<|w`}|}2306ղ-w@d냿6L;3"3Qi=O>vPLh n}šNAW~ëشa#- Ffp)ԕ<yj?3|}+"㓘lh ?;!YdDPߌgC!B!?wl"]`4&EZ GSS*ًu0G_8;W+$c(v*jt~rwBQѵ €hI#HN~ NOZMV=.@ 8Ǵ&g2=+ƭȯLH"H2]kqfqU\FlII6)Ͼu<|ki__;@ڧAoaMQ#ߎg/K)mj>.aAj糯(DpNbLq#IR)jcbv*K09TNL&-Scpgo{T'&Lf; 8 ͍T7[1D6!B!BifM mцţ#Hc N[PzS6 RI #4PעQ5VZUaCBFd3}TԖu`R1e2=;qZhnS7#@EGlBzI7nJ_U?ZguRt(yh{;pà߻|?s9 IDATɫ4a<cݔm}^-n#nѹu~o}`rzp{TΎt=բU\B!BӞa!h,UzAJHC:QǖbOvn+e>Es\W!T ]`!4\G])dTY(鶗( LEQP;thkv-df`20'%fzޯ}3n:z|s(7K?O(({?.CKMmIZMT6f^~e>wyHʾɫC!B!e=TV|j6=N&g24`F57"F$,yHΰH/k:W J ɭ3rx%cZ_NӞP]8*6V`{wG@ ₱ #nٯ}3n:z:^c|4!dM bjLbbj k?VGGwv/>o*s孢#(`r\B!Bq={Pc*Z=ʷjm)5K Xf (=TE^?ȋj v?~灯{B!B!أ=5DńiUaMc@&dslj&U! ò&rlǺ84@mW?hC62"3`6VuT-,1$t>xleLpMNBubidCI5Cj10:Fs Um%ǧx2Uq;077@E4Б@g~!>L4npAσR^` DCM [N w3# c̬3w\M򐐙Mf4vK}Wo: L))^l- (k$|L}s @!B!BtRFym*=%,LŒ ˘ĉIV6PY!B!B!$T>hH9*;g"pX:Mf䵼a-O?M9ql(Fr_ B&=8.N}P<\Dh9_"xY8I*B!Bꈢ%ԫI72s.vZ7-hFrL'~Xp7F@ÈY\6kaޗ^, ]reO!B!B,EjˇnO#+~蕓f+)vdp7H!B!GBq͗3kjQ:+s٥|[<@y/.pUReԻx3e,Z^-nwwq%'6sK|^ѭV ç_›儬 W_xjb_ɝ X=V+q3wocSKAdʚxƌ1$Dl;vecg3&рfgg~x{hn,Z,yo ogC_R JIK39e\(ZUFFCcNr?xx^{(ʥ7(d^r*aapv<^[FOKR96-CZ];Y=8x\_]<{VOdC?rD݊$fɘVy0tvz.&4w3>mCHmeα\vj,K7`B!BKu⛏rvdo 7*B+u> ?F]÷C@ $-)q5z<6w/-dq1772_x Y[KW- WbВo:_(UEX\AԛJ"ο|Y_ʵ%ɜ9x;9̙39/a_`#7nO棕&ΘdþRrt[>{6@OqL2XR_9knmvR7:&]5篍Cůl'1)zF9`Dxd/R8d&ҍcOͼ~\c <4)]ymK1Ʋ4~oz^&4 y/k=n3Fk Ub7'fn[d+yv ^%7mKK_=ojP+yd:>}n#5Eqٱ{agMuF8?\!B!.,azuLi_]=l,ԏdG_ģxoɃ,\xޏohڿXoWָ_YN}u[ ˹K=gȚeqoo:WmY=l(UO}FbT/頵V4UpZP;-(n>ӧ_n.x!Q9ͤNR@w%mɬ}Jc_`'+<̝29쀆dg|U7yW'' c/[u[Xr@3 r)X';7V]w~1+MYBX"'&}WghSzN~B&r* L;3R)@~MQz@^LG D.t8M_uUx;ײ7Oaרm{1Uɯ h9vnܷ&,/s&`&"W/ Z\F;fŋn%t"+:DrnMa*>}T6vbB!B!{n ӧ`^9?u_׼2-gH[W?σଧȩK\g1;Ej}E&=c;&3ft(΂ul鶯C[z=&qo=SNaӦkD_ŖoWa:зBayAffb9 (`!-i-a]'~,O]OC9yoФsƜ1} uKsܦ]Abi`2i$~q\lV0's 'oil_TS+[4ddu&(薹ڶ6R6at m}5+6v8ptOUv(!juXCB^@`sዸ7v;=nڳM-~9TTuU4Z4 Cxz2'F[\^Sr%ƞɜFZ>;lW:z U<iu xT> />i;gI߼rj*EJY5+w8qW]nW `4=asNm.H59Qo[q}*h"/t:ެE!B!2}n3mn*E5xcˈ+HiU?rݛJ`8KE0lmBaCPh(nFAmP e &alȵ[殓UPzJ:}ˑjȮpק\׷Y-| 9{V]UJ㭏0wv:ՉHX'[:;˷yB {~+2N5<ƽnצ6 U $* 0_Оf\V*F=Ɓ^J՚ s.&G8)VyvSXlp 䧮CxUEvj6͙77b1[P‰0 3F9/Pq9HXR9djFux8n#c疰a%}ĽtnQ*^\3*mxk$H!o4h"WC" )夙|nAFvnQӅal c 5{*9nV6ms4#Z* Lb*G7ܢ lz];Mq:65a^JwXn6Jv0΄j6t}P8n8N֖㫸\^0h9$dfU$;UTQvd\0A^* !B!~4_7o#yn~C7o)wF΍GyxE^(?vہ@pڢǨTμR?㹽:X+S M%&oa*UI.n5fEc-lɡH/A[[q˝f6?ʶ{,.9341U5cAQnAB|<H՟u.G_>{]&Tm'Tϻqgᠯ'ʁ6?xBդ2:]ÊL =f6;;s$~ǷuRgSq=֖֊:Oq!q'O YQNʼn,߾jś̕ppcxU6-qGkyam<1綰Ҥ~W'oG][XmTCM. !~HF*XvZm9+LLX"b8$wq@Yr )J#)u\׎H}hQA (8P1[@(ǜ¤&+UdeCE)B!B!VU]mpgxֺn;]M\-镦'iؓX7l̺R "kc_ean:{_W/ޡ[,H9r QFPntq?C7<'9}s?}@KO%<;>Zz<||cms{7Yq7+9\Gw*ϟܳ5X^]V7\JLr:7oy j29}J$MŚ7 3j[--E)f?v4.{49(ͫ}!(qy&`Yͻͅ ݥwo;]E$:d$:kbsAy熃csXĂg0ك7?pqerYJCn~f+9~^Ľ{XpI )E,~{; q2r"_sz + 8ft/'/UۗMt05*-)0ORy RZ!B!Ge葇0r2xY4EG:B!B!# B!B!BLTB!B!B3 B!B!BLoGc)cj#㧐IUv5r4fO&+cYiAX˚yQY9oaHjXC_Bpl*S'g2~T # Pg󳘐$f4h[ {ERhq[өbLS07s8ܿd ݗCH\2ká^!B!~m%$pƎ6tFƒ-n?sR &fq\/稸߿\}/~yA%"M Ÿ euB!B!.}wM IDATHo4s=Sf'QUZ#j [WRI?ގ #0*a=pb(wK5m&JP5AROܽ8iF0mJ"*n4̴ k6Tc{E1jP8fj,| T@<@9tDӳ1mF^mqPL;60[@bSM6 bl&N2L])d$DRQZ݅&>-iS]WY '+SbNLfԸDuO f>_`ށy+_Kۧ!*3cS-rwP Da/C0|zܾ*=#Tb`8t)3Pˇק ~oP>|9ZYe<?fYAvں͗vB!B!:}&ZF3aR;p95ͮBJx](΋PqO1i:KZگ,W{.n]g 1)q:ɯ`ɓIVKѪDžhxG`LgҸuu݃5qIDyTF}M0L#= S \݈-i8IvfcpT3u_y tĦbճ TTar@5$LZl%δDm$IVA 6JV{(^귗RZiՄ1|\8Âk}._HH4m5)jifu=|}DpNbLq#IR)jfo~nVcrӰ7(>|+B!{6%`F 6,V;mCJN۰zHJ%%6@_D ""LJsJ#<,`@Ȉl2)F#uL&gN m ƽ}ZMB@0 F u[6d<~լF[!6[mgXH=n :vgL'Ha0 m4[\0w:zPiox:??pɫ4a<cݔm}p+]}}C/6bS2C=+B!Ӟa!h,UzAJHCQǖbOvn+e>˫EsWW!T ]!4\G])dTY(1wwTAQNy(:4е/g;2c0QCb3=zoHYCtj< r^~x(Dd ;wkx8G׋DPQQzo:z|s(7?_xhi-9P~Anߐy`d Rr=^!B!GCe%ɧfۓLd2{9+ OVnnpo\C~s !("1idOr$ ;'\CQq=C/Br6qɘ֗Ӵg TN؞#8h!3`Aly@4x;jz՚n#qA8kRf(;K~=zm_?]GOG|LB,$& { UhПJ 2Hֱa0_B!BK_fjc[X%ZC`@i ^Z4"Z-n4Di}YNle*.T;UDŽHvj!(4fO~z7#Ɏ7`i|a])D$'0mbW &nb1ۻ ZM}"eA迮j>__}vz^ˆJ2򂘗 W e4Ύc =Gʚ0X4xx<&mU66ڨ+=: ǬqSgx!1ړ j\/(O}\[JQJ1#֢cm dbjRc4hf2dS 1&ecvgL[A&?nClm4Nlr>ZtT[*4k,/"+ ԉ$$Qj!5kIc<#DKm)rˏs*i/+ ;l*SmΥlme&9)IxieLF(n9p 4PE\k U֔UQeμ{XJN%STCbj{*pVss94! sg3 ZdW45|S,_\?-O={W3eR+C"=|Maf#_ TA+E(u>xk%qV}CdoˤOW@ @ ~HGG 9ᴤc˖.iR *?Ȗ4y1!: @ @ X8X,8zCdrj2QMFҿv@ @ E5R9}Sk4o@ @ @ !_@ @ @16@ @ @ ',@ @ ň@ @ @ 3RW/_0u`/ Rx+Nj]<E`h_.+LRXz~q1K4W?Rbc1xԮg yܖ~9Qܿnpcݦ˻~ޟYÙ!.}+ݑjW#~3tM)B}:Xb&G]{\2FNMF0.]hӠV}>MޤWɲ *[qz>yt1^x1^? 'r˙&6_IqIC.C졑mԴPTkv .'#ܘuW_!wK_q_F,#mP gޏ(|T"~z? h];\gQ0.]hӦrk/M`0ȗO]͗ҖJڑ4,- t̽,>/ur1ݟ㻑6i\OR7=Ɉ:&> oޏ|\Nq.p7'F^zoR N&N=QuDuq:)>Fs#bz>kYաC?֑yg">D7-,̿n>)f*?/Ͽ煝 ?Ϯ?;{%tNtwu fkxǬq=ߖ dO<S8̝VjKx}f#X8[Oە;;{"x,_;oϚ{Í'sdf2JxClɃҸqQ$=(o簳ρ%dN_9yl6\a`ݫNf u\KyYyU i!PS›;%ݑ_:Ќ^J m4CB^x<ǬC,B'N:cyO%-ܼ` x00?K8_{N䚱4黅כ0oIV˽w4.Z_b\^S+7'Y?k/ õ=g1埍)45dUJJ[C']F2Y v  kߝʻkWq{ٱI.}=C𕿲dڍ,\s{4VV(Yh" _̎xps|=90sa(C&85k+*}}Fq?{K;!vE̴`W=YmdSv]m|V{e8?O䗧b&/(u/hcIhْ5 Wm%#fؿROzsf^\.y#fd2浲3JٜQG6cJ0~Jq&>e&7uad&wdlgY3X _+h9M~(@i(H^<+S:xuf X;z/^уW k7\CHq/d6a9g8HvU[0qm%L~ksuvj۫ uCtѹ6:TGyj-(|=gA&cYA4._qt2橩Vɛ!W}:B[ _NB|rMy?N#FYJ6n?n:nti U]wtzlO9Փ<-o Vpˇ{y~Sc¾dSE\PWk/Ex'd kxڊӫyco͢N` F*chJwӷAVNg?=le3AXw RVfq'gv3QE\2cq")%+mf̲q:?T[yxB'աT:Omml\ 7qfH_r^loƙP\ڿ*͕S +:MeEeIR"rpdMmCΆX $Г(IH/Wd6 %I2 ]z;_Z΃Y7Ya-ޕPeLq.Y@yK>'/+56Ԯ!jBn!Xs+{e u263xр71i1ȔUgV3EHĝ;"1GGl$_={%qBc,D@6cho~b }/Z%OvjA6 Tӿ@.k3FGnc M%}y&@gOƀqDtYTw +uF'/?v+W2Z\r 52jmD]z޻][ǭf:ӫ>uHy= ;1eInDAm+fY,<ӟ"+K"" Wt|y*yLa8Vn*SWᦈН<4wqU̙[ԥŒ YG+r ORZDQQߟfdvdɏ?'}샍fdayU#YZ~ߴ0yܽ` 9|BPS)9$pwo R6_"X^<Ⱦ~So(W=o!zQ{eBSic]BՖؒ ALt:!hșl: kʬO`2K&r$&6- LbݏZbekݦ#4.Ye5pxO4=OՌ Qm{Wzߛ H7i}v':nQݨ̋8#FAK yi~zGggM̝YM{t8g%監* ܽ2[i&=g\Yͭ67Sy?Ę\Itjfpޖ S ިiC)_e~4xXZ}=8! hhq)T쫥]GٓyhI~2[S%HchQ_I*fSf'<ȗR'iAy`=f*LJ2ۧ߂eymoZh->![pCCE9@^&pj>B@O= a3鴹ߜ?e+'p#?|ZC\z/^{W񟱎[2Xk_b!h>p]>/`vGtB ҅إ(B'NC<@ٓs/%QS;!g@y5zzU~0yOf>elTO0C5㋐?y翮HO}}'\jkoXn Q~ {2OǬǺ_-~>{Pe,S Geuܶ:nՌ2TULy?}l-Fg*}=+fXki&+ =aPT-ɜ?3ߨzs&%N[`oϺ*e.>£t$;PO-uWqm: *ӸT~sZ#饼[vy?7ߘ(yn؄ 0$!ǞМ_;os>g5sx+Y%lOw9NY\$%3W/Zo;#qx*&#78ү 6Y"Ss_8&Wyr I\4@=ݓo;{{&%cQ˄;;Lg߱D!}t7zrrYctb~<~E`58@?)6<8L8u;,_@tUkheC$\IW?0AO0t\2t:~5Kx;@ b|1fI М:)x^؋J!DML,I\ULvPL?o.'3]hX< ڡ3N<={sW sӿ@'7'K,~@ @ @ @ 88>D @ @ .DQY @ @ цVvf3*j;}H9}&P֢fƝr*gN#%)xoZ <=ݫwl4ム4Hg֌dLGjb4~TT/3N#PArts8<뫩ikp/S8fE Dw o>;3~]EM9\;/(ikD @ @ q^$??%dmŢ9eрWt +yhjqi h}d*[^jIO}!302vQœ )j-q|Qpq9o$qѐ@ @ Q\TJ ?dem?ZMJk"+ -2:$K'u|dFJ:&P7[]?)Sw]<f\*O̹4gdS<|t6:[I]BRݔ@)*uC ǯyA$nT&yV}W Gà B1dgwE%@<(g(|~-ޗF_@ @ \ ]T gY2+Cv2 c1A?69>ee^o$O"  1KYK%UfD[ !zDC^moaHID !Fez C2C՛IL H޾ɭaFǩlQJ|,QRO͇8&La[ MGOi:gyR}B$koFكp__ 7"=PPh^;)*$tnӒؖۂEDtyd]5(RέH=y'rRi˗W @ 'CMdloE!4eGqh ma3_Aqpl::+$_5VPZcCeeČ*BN!l\ZvgQkh鑌SIF]ѲՌ h-c+"?.9)^"ʾ#")k-{Rm|ό'!23`CXbS_eoXBf2/J :LJk@q.};(-(mT5/R*%rC){Jɛ!¯VP/#iM4i;91ԽBеSj4V7dY9 =PRP燂s]6bNaZƒ}W!.JqhbG6%+@ E{*i)@:[i֪8_CJ|I#҂#itwy`5[jbI +xiQUq$lxDM`aTwJܩCrvP^Brjuzg2>sK5$U.X!gbt++GC"(i"i/fo];&)}u\m6}AFF{U*mW}~OL&wn6?O(EnUƊzbo%. qx3yJuG]7oG_@ @ t ^TBqn*Ѻd.P1OJUmw5#>]W*@)uǯII"ѫlLj`s".#5o9FC`B2e|]y @ %Cla֎Fk m㭧ZhcF{lňz{੕@w^ 2aXQ+{w4V:;1+@ `8SYCH?ֆjdok=uXg$SA#7%N1)Ә{pP<ώd 2p2`kopbЂvUaYcyY $N$F'!MpR5kIc<#DKm)rˏs*i/+ ;l*SmΥlme&9)IxieLF(n9p 4PE\k UԆQeμ{XJN%STCbj{*pVss94! sg3 ZdW45|S,_\?-O={W3eRH7Tp*˜:1*r9sDyvasS:]CW2=P>|}hKHZޖIA{n@ @ &%-@ÏYsiI~-]0T~-E% i:bZC>u"aU!Ư@ @ 'V* Nv^axTw@FD7@ @ DQgDTN@vF)`Hn@ @sGl!@ @ hF@ @ @ 8@ @ @ #@ @ @ P/*Ka\.v|ց~v/wJM⭼k;ϟw':3wB@ ׯ2icOKa!.99\Kɋſ[̗SY3q[Dq1~Kw.y6gGgޏqPwG]ÏQh5mOb]ӟ:Mhvq!trx:6!Føt 9ObX7}xf_%+ (lO\x,{ fw'@-g~%-u& sSX` ?FRBQy,<|>u5_K[6*iGӰ(1xsrd wF x%rEZ'_?UJݨ>o;4&#FV0y?q91ƹݜy0zO=#kJz/881Dյ Y@0y*s",NgV.*dze‡7q埲Xs6*m ߀2Β|^h2s|^{,ǘ]BsC?֑yg">y>Ip]7`VSEϿrz*vlW rxf8tQzo=4g8iiܳt3"6#=Lٮ6V-Nʻkf]q\zs$+XR̎£H"6y^;uZ3.$FMNo~oʺUӿ㇊Ui),n<{m5P3xmK[xDDxN $*DNbxD8궴xtYnOw?y0*So_.~l *G9=]LŸWP{V}c쉵P

)gH?=?Pa">a#%?+G w'%MgU4~WRx~4}YN OrJG r@[BM$g~އr;/r./AŬ}iqןµڊ aTL8B}Jf?e0륙P?+޶.<EƋM-8k*Jw݉>z/^QT+t\x7CZi} =̍o`燵xɛ䫟+ؾ׬Ү>MLg }|sKqMN5AolvH%mnѱ&jAK44A6mѦgթL,e՚tԑ^y룛&r<7_G?WדM꺤=#T̈>&۹*ZT͒gfFyԋo*WdS̺_:@Hq闍'gw 2xZytm.;wG|aaO 4CքeS׿ml0m|A>[Fcd WIoc̋%{ی\޼Vv_@pF)3ƒ9~@QO)٧2.䎌9k bk=G(b2QY~؋qeJV]w"B ?z3 :AuM朋c)ڀ&,1 gUڮy @Pʿ@e}e:)tRU yj#}s) ƱpzvwU2ZK[Upɘ2ŭX|I r8ْܾ1rYhXtDھ{L9d-Շ8T'ubS<7m}U7&]*o&2g[?g*}-Ԓ6%͛Kk_ŋ<ۧ>a-es:o_JO>ϯd:uf0oc]0&i[.Iobf~|mEA0ji@~Uvs ݟ:?&$1֒#h2 8sGE\zLǯNӰ; ]ei`-,$'S3 { kԐt:BU_ZLŏ&qi$WOW^—_gO' QQ,:d  2`4ZK9]?'@.ߖ=7sMA]~(@q84!bC 2j>z,Bj0!Q`m ,̤6W矻j筮V8P7ĩIgJ pւsyh8o~0lmMCUo.G'c:izէS8Vg\ya_)".L嫵۝ޟC4bd#Fp&Hd3E4ߚw] A~Z/}m%[?:pSN$O -9^^ا?y^O/G|xo?7,!IPyn: 2xFODy9}h5=q#&[ nC "Yy2A{G}?P#B+C0 t|`\Z=jRf,{GC;@lّg:)tRJupsgEh,vjť\9N<ڰ"I0 TP!?X% (^@tg|t`ptH'Q 0_v$n}r){82&~{ܶ!4fd<^1If3kCobKypS0&˟7eѻSVjvm]ACԴCܱV.^~/d mfobd3b)i>i=&β'mfdɋ;wnE6c0Hz|ѻ/KZ * rY|'l~S^J vm L]f 1pݛJL2-6۟x}ҍ㸉̃a(A\wW2c (N_VqVe(h}k.ueڈ;z{!w;Bǻ~w[ʹuW}:Oq됑_V{@v bB'݈!xnxNf.f:D5ޏSWᦈН<4w=r ORZwqTǿMeջ %MLK0@x!_ !`  !@HЫwM-.V6mIr9V3w=ٙ;}7nFn@UB چk1;N!y7+DJ$Fn P3 X{=&=~jevqf5V*lD =a:+R!Qlj)/T!gb4>Z,T^Fu^t;7)J _y;11~wc'~Kˤ0ECnG) OD0WdcqțKpL?]ЁƛWy02Ӱ1]=+byfJ3ɗN8Гw,kǶqۃF߼a[kI:d2k#ۑ^d9Z`i\7+)mbCțb" (94Ee}Llҡ_梶9:\&^f#f%̛M< -mRNn`FxlgT|g '.*X&͹lӍMtbstOQn[@,83eBpt)c3x\xb|η%}Whr8u9L =3ΖV:}*fq\tq}2x`e _w!ieq?ˋgj E T+T.O.3I@Z';.yFNSOHsJi  cܔDξhx11\tan:<bRc}d~UɚO~f~uqʾ,f}Z_g "6DHH NIc M| t8<Եun&'xhI9a,7ECW{XS߽=gW3LeE]esCb~Dž$nY<8ȇ&f#'ɏ,3.Ef\H@!zr )/}Cb< ncg^ii_m 1 24>/I&?o :gqTol@Ʃ(BL*%+XUJX4Fŧli71qvYe s^r|as:8T&vReiiP?7Ҕgڞch)8 Y~K<l"79D 4rroWO"2|/{ǏyE̜y)65E lm/;JmZ߷)1?7oi̓2OzG<ΘB7)/޿{g>󐿎SL;w"Qɋ5z5];+-zχ[|/ؘs&Oz_ YTK\ڎxL'p=0'_D ΢Y|m#7,5wqNZkرkyR7Y,73apso:xAt%z'ת^opWl@ o91QfT㻧Q}>(j)feuİi}pu'3ǬCkWaϭRβ"Wds-YxhoP_ŽsofWǯ1)ʯґ*(Lt Sx)xGaKUܧmORƥ٢ǟgyn_u⢐yf',2]7qo09^_wQԯUk5/' JGƺ_ֿڏ].%nWK?4DyCLNe{⡫=d^{e|//y}m>e|o/Rh-Ր~IGzտCכi3<)semc61e|8F[C9#'O1E3sҏ{=|ud)cei*1&[/ϣ XO)z,gG !!Sxb+KmfKut9xeY ~“[Gs~f&\+Z(?g4ͬ?;'%iީ|ʋ䭪nͱA{?~<~y8w8ytVKMfC^GNj6sn9UwGvHw4G;\|j!?S jydGαvBĤIa|آcm;֎W>d?*ɼvhHG<N0N2R? bHJ0sO*>;Del wx(ڊK#vGW#>0c?`!+?>v3BHq!ܑ>O*d?' RS>ܼG>lP 5/qP \zL<ݕ]Lcxb/},d~9vO|xzaX;^!XBo`* H'TºVC/ M%.q|Pϗ{q΃}\j5ZqB!Bq\T6 [hsBjiхwToϧ=f 6i)>tdD:Hh7F0''G]#Q@!B!@ aakfdk=-S9'ywnnnd]D%1>'PzMW'H>d>7)}wnmVk[IoN"@uSy-[̱}CbØ`L [s=vVPk٧!ir 72ԀʞEViӑuy7Rkf@ۖ4FJCu4yښEK>88:k: KCifG̼xB=vdO[w4#/ 9i5y#4)1D@gk= x<3BnJFW s߼|e3dt}['Ę}_zm !B! ^Tgq~8;i8}n*]| (F&M$` cʴ̍lm #pb(tvKmc3v jX=8|(2Rq\ƦJmja '3'V' ;FŽ&Pq9_PMef^0۷_ӵs"3NH'Vέ@3Ә6]ϺJhtVkL%ju`I ͻ%>RBȌP_7udUqzZALcqXUBcyd&DP^TӉj &1#6ENnBy*=2vbߗӺ70Ӛuyq}tDdcnab쪉x3V1_43Jl3nR2Llٲ\"(Ҽ aiij qTCd"y9x87R 4UюK@TB1!Uw#?8 EvJ5[6瓯1)-ǑB!BEeG[V[00yj[wSn!gz6{8WP<8.@9ȕN9p]'%Զw[Ks3‰íEmMK -4iupDңkh.<х@mgKmšd lXB}M`AfB%NCsU8hjn9!(-ξ0Va^vRQXI]f)dWX}֭6W/Yhv7# ,u(vQZiՅ71 h7~Zۧ=OK~}i`yqKtmլ*h{tb{>-ACU#m057PNHc'A/4#mCZVޛCuXTي'8'rZËǑx+]Ы;Gks Ѯ :SB H!B!cB@Bat6*무ߪ6,8shYh IDAT=BJazÅ CM6ZH!B!L-,2]{%0eeUײy܌X'&WG {Q2R1t=tqzB z|,Oj#4@m}qidWS֫%Lc2wLTˀznw浪X%)AMa}7"qHxg=?J+wq` 9!MGݞ264vD'TT;T~my?j8~Xl^SxvJ5kGy5^ǠSq:#o>^/g4Pp VG)~w|P=xM6jH!B!w35%igd{ܹ{W.b֜dlS_gs5[A1 wR2ߖ<""dNo pdX UFfL"wb*kJiKVW'qu9>'`: hbKV5|}:0sĄ l.iW0QkA Njݯ"G8WL̗ƑV^C)*ݾ=?^/tP1`Q{qomƑB!B jj;r}%n;Y.7 [Yj3lrQF{k=EU608y<*StQEDφp"m#}$09/}Tm4 BcկlEDJ&y&ڪ>pCyh cv5R\xB!Jh8QFm=':+BDL8l/rV- ־Aǯzݾ!F}u|>5r7xx9yAE%(LT7+*{9[hSIL4>5o Jk}6omǑB!B >&6j?Rq9x RX@)QE}G3xO%épFg4h2`4H# -NTǯz>MyAǥwSIKsc1Maca *90{=eN q;hhyHCn:A$gFՍ):I94Vx?/a"#=h' $L =/y>hFn !B!p RTNlG5I#BAa/@\JYi %al 38NLfZ<1A HǐH-5w+*F£HGzJy!ƥ7gQY[hgoC^o!u{:Zi dLf:sH2;"2ROsb;qhޞ&gQ %m`oo;Jy-Σ@梲~q$B!á%T~<3l L9%mmaS9%o(-Od !B!B .@3a$$`ױ\v'f nϰyc`GQ\7>0{A&>_ B!1CGs (ƈB v3D'^AΝ|T~XO1_=_v3` ;Rj`q_>CȘ7l*?+.ݣ !B![:bg.WqR^*`ƛb ֽϳ=˗\H21o5xsC߸6W*?fm8H&_K~yiLN@ge׺xZꈛ;n'bꬣ໷yXܷܡϽ7_Knbݴ\wT4_<ͺ.!i _k+xwtW}8To>`dǫ54Z3$pk'2Ǽ?&&vYc׮ö/JK+s8}b(z'[ywbbD':س5}^Hظ,~0pT3xk8\t*/^l'my:ӓt : cݛ39qC8$Om Ҙ?p鍉_ OnU{l`uϥ yxVsݹ?OK%wXXˢDIsM++٢8YxY'gLeb7{}>]R.6rudy.73 9x8D2C=EoWR{߿ѓ,4qQи((rr1Srq~:'db겳kC)/־ե?fR4agE䡹GM^}1Κ E\Fd\jwQkXg ΈٍF|B!B,*+t~?O>'rbB$:zr6ܡٜ&T.xǐy_m%Kȸ!2ožB\{& .rRGO+x_[6Oz9[Yt0^5֡[i}?' oA0{߯s /=(FfKƼMwMV/@3O]¦7}K&1mymZW5M%~B<H{ /4r5/{~I_54ϻ {gyB?ٞ|". 4dswpskpɟٶu ?糪' AQqcS>~8!& 9n uv(yOxIgqG\1w=\J nzaH'UY‹JOྥU>;b9e/Z|J*ǧXثX^nSW(g]5ȿu;;{(D柵~cp~vL_u:X 0W?fAa˃wgf=ߊ;5kN%N㲟>3Kces*1}86nO5Ӭ );cM b⬋ '^/Ɂ'$S.Cv_4ǡ}z3)j!|/o#X^y뮌%Hx^ט=~KhNC.j<Vǭ|lßnȄQ!Z ME\?.^`Plf#xnL 7 !B1!̉ (// 6ε9\_6%Y/PS}ۊqDn"}j9WNJx*yl+ _q/v gȚwoNgskyV'27W+$zkͿlT;A~[ ˂ (flv:س-hoӎV;N' FN,)]|4_ )N.1kY򏶞87]꺢s+vPG)oaZ>zU*q:7m ),S=3 ,bDST$̗'jKхT HrLVV`[~hkNoKl"=LBvJBK{米J6첰L"TFR7Xhfq:(ߛ_:hvs_ 7U,_wJLT.yxk+,=ٗպ_='Af{./칓{0+d,=aUX RY~3h7ָ\j7_y_TYpMi!B!"Ce(ryFÿa㶝80},3.k=c"1(:}+QQ  k ]rd¨u49c|&7hr0Ch#l(s 6x&:J†5;p͎j^>^ʣOeP[`s+ְu'[U !'\}i4VZ’{r`e<a:艞~s^9],֗?U:)ԸbbPh=R">Hۺ RJr0|ډ KߵOOje[:h'z_lgOq놢 alGa;Tlm}^E\5!)c_J3@߹(u2w>eVJ]\%9κ@7O#'>QEwCsT'6 ul.TnwCtQPjc+ZtCe9ɇ>Y _6/$s Fn#5~ !/{mUmkBNFKmumOAGZǥ~w^څE!.J2Y\!B YTV混h\{ ֲ`_ذwo|o~(Ӄ.rl }SQU՞tzt]vT;v pmǃe8!z"{rxB1'!ThvwQռ=ЈPtj+-~T !4K. BfHˢ=T;-^sݟ8{A[>kM.'ݦ(o+:إt 7(Vqsr0BON:<}q 5ZRMW (b=} b W>T;tG/Mϴ m*^^+u,(+nbqتx뙍}c? t|6nڥKw'}^oi{TU{n`6Ё; gĩ*v{#4~wٜ}pbuBx Ek0˚ZRKMۃ/B!8hxP+ 3f\~q]l`9\a,sf+k{nS>XJ%0=(a)ج|#V7K8Y|gb>'Ơ~nOjU H0BM*VWB {qCq|yW# Cw{u_V E\.maAP*[Xt&sՍXx{њ⬏Y}e4wh H/DX Ug$,:.JX^NC~Xy5[+V 1Ay`Ψ5^6.5o+)AFqbIAY!B6zx9D0N>at0ǙrgZERQ:PB- XRqU|MpowbN7gKzHބ8$`y=,ffkRIW+{;Ȅ`<6k,B! YT6z/6RT3 tm^Ζvzm+yg~Uɚ%FT|8V; Uoߕ97'Ձk߲BZ\m|śʬF"2tҏ VK5|\B8?? xuv~">NodW1s0uz%y\;æ9` ':XDnD@XS>k佶~s>++;TθLnnu@$.>w px\4vb8Bx<?>¦'2!qSv5OM)/}ƲOBB)yTAD)j<&₱uh^w݇wxRSY8'W4[yN3TJ1=.糃)9( S?IDATx'VHq%&yE(Ž^gw88oVg|ϗ:g3q>?c$;bv8B=?r]}`AѺ=X.J͗n5Ŗʽ_M *9&F<~qF0>No;ZwYRJOgi?[F&q:oh2yϽriLmrJh?=P^N!B!#C?b+\x˹2=mO}pXf] 1j~S&K{x εe|o/Rh-Ր~IG zCױ{f4nM& X.Fp^mq>ohZ2y{bөlo9YOGս(B!O;.0O؛ⵟE)B)t6wqt9xeY ~“[G`6'gf2˕\e5RPșibuY5PRҼSy o_[L鬸=nY]B!83!Р||L.>ڽ{8tl<݊;ZCG&&M 囵?HA%9'btIAY!BAIQY1y휗;^#B ?> ȇx\B!GtQ`^!]o߅1N,> e2V?;'mC-!qB!BC,B!B!B3/B!B!B!IQY!B!B!fRTB!B!BB!B!Bh&Ee!B!B!IQY!B!B!fĪ&IENDB`nxtomomill-v2.0.1/doc/userguide/img/handling_h52nx_issues/nxtomomill_config_titles_section.png000066400000000000000000002703261511430602400331750ustar00rootroot00000000000000PNG  IHDRQ|&sBIT|dtEXtSoftwaregnome-screenshot> IDATxyxAoEەt,[nlM y!2v`–!a / <@! ! Mɖ-k?dY*ʒr٫Ꜫs4eH(""""""""""""EhEDDDDDDDDDDDMIk}u!"""""""""b&QDDDDDDDDDDDD$HD)B(""""""""""""E:]_e@aoo ^-A>ܷfn^z_?}A3ɯ?&~ayc_翽ۋq|'oz?=NeG8?rE|;nZ/-䳝r-𕟚wqYnV³?e{~o`o W>gOWj~?3=T$'9| \8jq_OZH?/> |5e7EDDDDDDDDD6ߚ(dO3Wl<;w/osKG /.8yzOOo 5~ >~=?>w_S2|?j~L.=|w -/|嫈VbmLdv#f_V7]%{x+WM.<dznӝ_S|lXx7r|?{w߿1 y|x_^*UZ0:xFsL/$?˟UTZ ຏ'm$y>cG,NX++.¿+Y_gi~y6j5R6+l/<[v Bpr?GYpK 9J_n}p}3&/vA޻|oX@:T31""""""""""'J>p4]!hql =3Mpr!dCΝGy#˩)_?_`㏱H֕I%5ӑcj'cMem?F9y+wG;|K2/oWRDy˹f;5gFs\apw 99ҕqN:{ח m""""""""""[Oi&Q{> /\ZX{?{N7Tz~}d 䯇9f:|/{/sq~}UD^t/cҸ'm*όF֭)쮗֊zf(@h>:)jI"d5$WW|~WDDDDDDDDDd'ZrG~.03tf }S|XN[v#|ŏ8>̏tFGOGz+O?yΦG9^jC/J#?޲o~u9Rn³>"۞luD1㌝;n, Ƌ5b|oo6{OH'z/]t E_"u>c:wG9PWqQ{2o?/5px}mB oB̅ u 9`vٕTUU.`sVᩄt^᷾>fkܽ(x3Kv?1=˜7W?G}ɳⷿ';{/ËA{/\_}jj_Wx۟ĸXcmO>֗~#ADVzW귾p>ukqƞ T%`bֳa^#~{S#YsLO܍N~/{wvꇕVfQN*{_=Q~/.yDdxF#EDDDDDDDDD kn,/"R*X^DDDDDDDDD%D)±F7bేy}f֎Q=>;6 f>N|?o8ݙ{~yU|̑pTS2g|&FF%u. %+,!Bfr/r/ FLѤmUoDn;~I"%mB 7۶Ëҍaa0-9Dhqb]d}~|KwD vz=!L]k$<8r\6(G9vQqlpʉEI24%91C0C~"cƁraD/O= Į1L[SA.e#GOxǕ o-+)\`t9pa<Qy^HJw3UeNȥ F9>4Mrq)NmT8M2$@둥yrb]Ȧ|\ݚ1[~z i|*;L?G w|rGso{ff fAUv8Ͼ=Iuty!kIqm:V:F3ұzxaXo\7.ՔvH7hZroep$w7f]{ک1I&Ql$L@Juk'U&QV0~$;NVWVt^һ~NWjڑȽkIZ(uԚqc[!*W j2y4w-b4g$[,IzeRVzIM]ʹg#{(U#SMPɯTj:wGu 28k`9Y@U9Y.1\dɁ~.3ĩcq9_;geoD`jxؒ2qu+&yqb@mAv'f@@2gu&*Y:R_7%yWyI60}$BKtQ@wUn\HǕqfSa,ӎ,7Jy]F.v tZ㺣|s]N """"""$Jy+i'M7o\{-O?E|Hfb)\$8CGSf;z8/_c%%1jZ_8Å90e r'hܷUsIΜ:i}ؽFY?K0_|A v`_a~5Va'<,G9s9J7 7)Kx̹$P,g Q?@_O#gmϨPmF2MNv,@xeG8|dp(LdX dRi$֋}f1| 0׃X YS J 7#Ϻa'<+u6td=7N0l,c"ڟJ f8Xz(L%-5V=F8k(m*/\>C<Ծ Bvl՗qFM3Br:) BqQMc_Ems <j87(yXwOEߍ Xd1<\@`GB.38 4ImQLVó]ݕĮpfs>ߒRWM XkG6wwVkoluR`•`rҲw=j`@DDDDDD(fD,\A_He831"vW1<&,ng.Zv\SϏoQ&B fm$%;29r(s#* IDATlLĉZNj,@ 'A^L0&+\N04n/J7.RVv|UZ^UQ_븥}1 n{gQI]B8D`ȹ`لIIWuUK3F Ϟ PL_ovg~1r>?(u*#G:р'KrL']Tpڠ c2YLVó]k$<8`vS f{t_Wmo!F xT.&K|^uu[WyXV`?DxW㈌*[Cu:Jp=O'.I6|.QXM :vL Lp@uK,PXjbb`,*79ͩzNr p%)źb8vbƍ-ϠoGz _?Axy@b<vR4VloIͪ6K8q9!-h j\N FۨuzEysD G K.ʓܮGϥ6Bi/3VLgu0r!O~Y'F:Z w ,i;1m祾.:~J{hʷP ,u]WW˿kv q눇7ӵ3,(o"?{:8t;/ ;=.͍U0dr&A^-\X.[=~f3$gʺ&2 #6o3ora*T%,t^V_3r8:Z \qqL2:NZmӬ/~w nJzQs@DDDDDD['Q0^9py5:;hC^#8̥O QZĘ_ `ɫf0OWM%f"H|=An` N%zjx|p.6Hf2BW99p: ]<ĠKD63N0RK_ 8n0C[w&Ɣ/a=j:g& Er8hpXs\^3!NSL?s Õb:|׽Rrlu`쒍pdF5^"čI_l UQSEAPC{WO+Rr{$ -ߒXuu𬗇z,U뾈XWd~LbfG#;:\ eɛy҉͇d{X;PEO_3$93O"$cKG;Mf#D9 /{ɇFn@q|s9u19DAM[{;K *u?}:9x_K u 4;n;=TGF`__VO Fgv@Q2^zpE[[M'N"vvP}6]ZTvy ?N0nymW?q|%]xR>..\ KR_`Ͷ^b-`+& pw7Gd4R6y~{z{ *R]]\B}6 ;hh!!dBUc;x1ʱ ٻc/ocqzY,Ħ8~,K_;}Q4ɤG4H{ɲ c};PTٳv$90Rɨƛn%r5NOmAvE90򽈬j+]W;n%UZzDSՍV_("[CmUmx3oZi"몈&QD\MvvSvPH'rl8zD2ڞ]<& pA5Zͧ몈)±ي4""""""""""""R&QDDDDDDDDDDDD$HD)B(""""""""""""EhEDDDDDDDDDDDMI"4""""""""""""R&QDDDDDDDDDDDD$HD)B(""""""""""""EhEDDDDDDDDDDDMI"4""""""""""""R&QDDDDDDDDDDDD$HD)B(""""""""""""E8>~z i|*;L?G 6NSFσľm u3[I1 XKk Lg+:F3؉3jٷ];;N2c֔>~Ix-lrKG답RoqnTO):l4gz7x~hmw,ǠnZ~:#-߭ޯ1DDDD6kjF1\‘d?sYTth1&ܠTv_e29eß#rֳkO;Us8~6F4$[M?K6"~è`W[`єX N/E6nM9Lth[~|XOnZ-Vȭv{{VޘG""""eIZ(uԚq1ϤIdn󧙴}-*W n7[9iފ'<ؽ F ׏oFL3Dz:8V|wUTJmZ~ؿV}xk""""֘D1""nBExH~cd4r-p3?3N)vN\S̓NĘƹԍ9nƍlk!.2ŊN[8o>C?$sKɁ~.3ĩc`t#m\_C_[ U 2W?KmP92#G<0Х1,mǓ)ΟKJI>b늟Q,p$8vX=tT6wwJ'vi5揝/$~ Uܨ60}$BZ7f*7F.M$8Kl,X_,Jxv\mWQUȥ g(euyHc\Y&lo#z $LEzvNtc,Zx%L]_Ooiky]07:hvfGhwk&m= Ը $C~.^g2|RdMvAMvhRYg Vv`>[oUF {OYmo?<_`HG""""wC=Q[y8MWgm3}h*QQGo_=1丽-6晾i&gm1`ؚ-6͈ϤS8ɵSLG3^[Om-Y. 2<"p Hgi;i)ed:e&C$n<շ;йa.1;hw ߘ8?+gYMzja$bXo}DyCۚj:=9fS7{{ MXYl?nc0gs Aٵ2#&V֖Yζ$S[I+YCtdi97ӽqkSuG%?w8HG""""wA/Q2sy)hzp\`@!ڠT0ɮ2ixj:R^d:2` 4Y 2y 9R6j2>Ǘʕv/YgIgˤIJcI^q#"،J.h85f睋^v_ofz^+!`[-\YOzj_׃'(k5[7QR<\GK]vuWzg|Kµy^KWa)<;Z'K%FڪGH:K!K"mvb%gX_z?I+(MbVFf:J9fCܿ_)U6R 2_]O;_PFoYu$~&c$G:r]oZZDylcg@ F0-=j}B8쨦1ࢶdXGM 5fp񖯃Al=HDDDdc9̑%TD.*q|31">)8B%]iiJj]!Ks!Ӆ|cgJᡩEǷ0!3za!I$䟙9\襤W?lccAEEdo3NfVaTRWM"ހz5LM/,t- %biɑG7?;P;?g]7^`pŻϛ?-d2d(|w{kWRq'V[7_W;/ufB`t('_VHgᡮ  `@ly|Ep*]~b%g"Jh];󑞏DDDD6֪{W㈌*[Cu:Jp2$9u@o3}{۩pB.d׵¼N\Ngsy n'JVI0ܔc7t/Lpfa2$&&eIS04o_FVk9N\.˭s/w ,u잷6>A]8 Lƻz#]?-2P{Km#/d_`kU#$i3s%zfylP!O~YyrypMXV9&l~Y\V,2B6j]8^jcb:Q*%5[;,󑞏DDDD6֭OVF޼NX?< <.IPK(L`pPYD^uxaꅤI%{؎_eC{^oĦhJR{۠ˇx*wȊ77:cT40_b/)ͺXIۮ~)J|\;o3k?a;3ʹΆ.vw^Z Ϻh0F~{m &Tegd?t֤jmWz>z<lۼ$頡|` UuxsKE|3+߷#]f3_`4 `8p#]%ԓIY aZo 4!(LJI9mrb;۵.a8El`£;Y`O_ {u(d868Ad |zl5~vQMqXvoiI% \P4Q,n=ʌ1c.zˣZNGx wrLYviX~4g/y߷{ ă\ûnyJ^ 68͍M dҞz}٨VV338ݽ~raţZ~婢z}iǒ Q.|,W:W|QCKX`6$ MtRf2xkJݿfG</7If9} mcgܲ;=Wٱjx%%l6 5g:D /nzXqkRVuW"z>zc<l$c};пUٽ'w9a|֖#MO9-^,b 0y;Ѯ(ǞƯKZuTg,Ho2PNWOq5""""""""iE^0tnYؗSDJ]F?©[RGy/QDDDDDDDDDDgޫU,=S)l$ZHHwI,Y@(6KvBK!vܫ\լ+ޫvlƀ- IDAT\ϵd|y3cO}?F*xY ȧ>z ,*&-u7|-lT|S{=?)=ƃ_&M .v}0{/6N|绸;\u;"@x]{57|쿸ӏq80Fd9HzݣJG]t5o~#>x0]Brσ[s`\\ (,SfeȈO9Dʏ|Gg/Xx|sX<<".*w91^|&tut>̄Lvt'l0JGFT;^wr$?O; uO 8?F+H1ݳ&y0GDDDDDDDDDDD'Qz㛸\㍓76ac3,e8"""""""""""rL>d /w_64Flфg͌!l#JG{#JlU|/o6^`DDDDDDDDDDD~SLfݦa޴h֧igMIC!HN!LL [0͏x+WVJ^yx OyCshz-e%&_Yj+ʡ Ǐ/Omb _u-cl}l؍e#`C"""""""""""2L1?玛{GMnR t6keRa{i֥NknNUbe,*мwfcg1^B}?\%%&xځ0Y"""""""""g'QI 0#',s8:FCc~6LέNiIN!E.sξWnIluG$n&m]ʨ`[\g (YYiD NP?hsdskQ1ڵ?me$YZ_Lafn3pC=\V] q\qUcl}f7!YL+gt!{;illo¾kVpYC汗 f[ֳʌ=n/?I\qa6k`b.Jde&ڂ]TvgP_FYnIF!_7{9aɤs(u p`~DfV\zq vћ[E]I&ic.nh/Rys(tӿbg?KsWQ_ˮ>>[OVk-m" I.v'bQ _΁#k9W`=o"""""""""aw$s+YU_IEwV!u$Jɡ z_,fQswESK/B>LJ&m4wx5pj^n; Qku>bqeUr;FkJ M.觭4|;~w;; DMRy!)Mc:1Rs)K%&K- 5scwa ) 9bfBb>69ŕI 0,8ArJ]8nFr6UYge1͞ 0r 2~-Z\Y-]PβRhߎoX?qjuvXi]VPc-ci]WW0ImÝlߓyu*P74ԝF|de&X(`0u-+!maPgF\Y, 1TFEAF_ of+NΟrXW&Eyn>gtm=Ć&Vqawx't:o8Xd*"GV.16>wؒI,OQW,#Ԫ\^;gЭy i~aI6ZԔ&vVd*6'PX#㢣񾖯td7Ol/a(Z{3_{w<:^z ~z0ϰꑴK 9~|$V~OrջʳMGKl}mRbDDDDDDDDDDl5(F۸[|Ayp+}\,KrqB,2OY}dowxxvfk>Eáxߤ)Ve7νq{e]piw>—|w]7aGk}/~1,)\p=T{_򵋎9_MosWO&-߹&2~r܋xxᦇz|gΗ}w𮸖[o4LW9o_[7M)'L QV)'QܵY?([=+_sz6w=n,ȇ8|{y2/_ {uc4sg gʕ{-8z>B;YexBaѲz{,e >OoY١E8O,rNma#d itI&/eױ#͒"/:77"<3o\嘦} ^p؟8ckfc||7q46?_ot}&o ypx'>bxĺ2L_$Lxftf|3o7HH{ 3 F'/ ݦ 3$@0yn?SSnh yYޙ8dt?4V^Ň?Yn| '\x'qRpJAf: D'~*AZr'e) u3jgq1OV9!Ð Yd& ODF 23OH ׶Uir^""""""""""sUbôo5?~\}[)ǮD1I ;mIY*?7|VٳsfGvn0oZH4ۚI8x1e{5Cy ‹9?< /fϳ'f%a"[HcՅK.7W˔˳xzIHD^9~0yj"nc[cۄ[6ۼwb6doS.4}4k>g7[G*#7PG>/7Scm|\~:^rkYqɫIyvxڊrhAbo~=znMlARv1i. #$092D=t"a=-/8{y_{>vi?Tb=?pn+_绿=gŻ{O,t9?#FfDDDDDDDDDD$ѦDngco,/zA>hXo4pGou_\{N[to=wow$qs/{MQJfvؿ?Τ7%Odzq_7v;{7=w?ywwQtyv}rOz Hfs?o?Loyw^};6|ߞK)!_`gˑXdaܗEhokBDDDDDDDDDD;QDδ |q;Dc14"sS,FZRDw͡ș0w,_uƶ>ʽ ٪?SsgEDDDDDDDDDDDdqvDDDDDDDDDDDD"ML]Pw[sKg,WO=ݴc6I‹t*iI%pϼhCÜcET@ NOAZq5g*-,'#NP|{^k7i7g^uWa{e9tuNx-}kVl!#',WVEXb{8{ttfaՙDsSl5=,0[t 96oj9g32ͨ˖P0NstqQzzE93=z~'E5n.^fx8dC I';5Ġ?b3m6킔.hāAFq9i&qhi)i}ذ=H4 [";R3/vs,ۜ1[Ծg^2x4Q9NN?l#:XFfJ"4>+Vk1PWpI} | f#rĴqqfd@Bgl6m~'E=:.'QI 0#',s8DCc~6LέNiIN!Eo/ZNLo793!g+072:ۙ9Ϳq\ϟb[i=4UCx))"eCSRfXjsY3tt/J.\YͻE44jw~mF,>9=޵M?!GwF:ns޷/ӏ#ƕp4O"ݹcf)M>cۋ<)*hɠ*\w 'ÈL|23vrN}>NWxt0 #Gª2C=lFO@$WR=ai3f$ #ĀHhxkN9ԟ⡤h7+ P[Fodz{XVĢTбzz&|9JVz9Oy>Z j&'Bt8+#;Olv|X_\VbR_ ^Y#NkX8}>r~kxg< `z2N/gmd?csޟƕp6'r )ɶN(i%iǞF('+ρfnfz]"2'Qd"{+W8#?+l]gp,'g'ϫ0+A~+Ǘ)'3OXf2;KZ {mٗдe+Ͼ(=y-<J$4{O*S*gKaMƱ|͈$d#^BC҄)i$MV?L '&?8<kH/Ou+BNe-Jlq\]wPO$M<.Nwr}׋sٵ,a).o GLzڿ-ov9ǎ-L'գ~2Aϫ;M\?d'Ӓ0JpxNܛNIyinVg88,#QlGK8oQNO4xȓeul( f1O:+lXGLpV'帷8y5QY"}tG)-Σ(PO$/q<3I}E H%}]C2phplx߬"fa#&6.$H.)7`dS]At3``,ڲA QƆC6'bt,.c1/h/g.o]_o`(")-<ܝO̔p7EYwM=#ԾfBҵ7DGsYR`G( %oLTYqt9t4RUKiƕSJC+;uKYɂlz13XZiNrdQ=CTԗ>î &峰<S;Sg+F^1˹!z",()]dT ;8U{(zqj]4M{1S8Z(cMȟSoX!4h;֎ˑ򖔱bmCE4T$ccX'ЯY8C=DdgAi5RhXZI _p7oY;foո:5ˑxD%,.;8ƓCT ˊв3t-JqDXclXG+Y><"]g :H{O jH?FsϩqhI'yD}] Cz^'&:ض;jYSe]7dMww5\~Y `JlaKXW,O}asL_gmLn va}Rꖕ6 0^GxHuɨ]«k'?`98 So>:ʨ( KVKl [ Vԗ|!/vS4Ą-kx~j2)sh81nY+?k>Xg牆<[RK`#m6es/M+nNՃ{rYE qY xb[7yGkD!Kvg9#лq;բ.OQW,c7Us/\+K8W:v9Dq0[.5.ZLs!%n2۴lt(/!}mq->*"vZVzt@NW2Uyv6&Py_s2<1H&P)JtR\TdA B֝q/"&QDDNf'JR.bc5@u/% */]Z9YI;JOK(η*M"Ė9x.y_ᜌm6٘Kg! &b1}Apu69'^D;-%""""""2#%o`GмXJU&8s^DD(""""""""""""qvDDDDDDDDDDDD"MLbIkm=FRH\T]yf%NigN>l<說ZidۯW.#kMܹ|oޙ9mV^^7~渪w^~~>k=#6B)Nb9 J_oM Ɠ`t?'gϿ]^~tY. g;֞\T^Li9Ȍ4>^6%ZKmӷGL6 do _oinw]K)qS3?-'T>j.I%51ogSƗ~˓ KeHbpJYZkhχw-(^*~?!XǼoufرzG/o#/FƷu/;g/`OBlԳoټl4C|v9ttC|!. *wmL'>K/ZU ômk?s')nPf _Kq-|ד<}tmZyr\Z=ھVyN{X\5qYKO7$B^'qj']G%vxwƳ+vޓX5$J9v͛N;w\w}|쮛#bcuw|O? cg?S{=?)=ƃ_&M .v}0{/ConsYb..ŅIM)52dDL,+aUR5:f!l95nw^'ٯ5af ky/_l s>K]F-;Jo~7?/4s7_f{/}R>pM'i#/Yc=?b/(ꈏ!ed/_'>G5 K(ߴȯϕLy]&aLL4}yݧ^ln{{t{.rMnt߿{UR=w&#Q϶n{Tn;wFw4eƜFJ)y>_F$Z>xG[oɞoœqS|m зKX?MGry;HY 0c5];Zatx?[a=Gh$r]7S$7cvVX{3;WbVƻ1"V$p7 IDATҹ,f9Q;:7" 403~Q`=\ŎC@+cM=oEv7ʏ|Gg/Xx,Inu{˺y"T* }hvt esmCx90=tRnּZ3 IryoӑFX66 Zrm,k3(rAKlUKgȣ#@ۻXt"]ɦȶ=c?RΫ42 ?xvld(= 1jn~S-=~1HhZpM:6]%ҹ/1F 6MF+[8n H[ɏA Yz]3.=\SǯW{+Zҗ|W5Of;Ϧ\8n5ݣfnktgbtx?[wGh+23tјTŷn`f^Α8]ZHFYx6imIϦ^j>,ł&qz~ v`6r71AAa*gʻHY؎'L0@K$@KB %%J a+Pڷ )RhCBCaIHĎ7ٖ-[}4f43lC]h$~>|;gy<9{.+q<%Z;!Xcd:CӏzaA#@04Tt p[b=n<>LL=cIc-}o!Eh6k̋IE{hTr][~3#q4Zx y3޺ZeI/O/@,B.}'MVkM.JזîvtƉ s[] ~9AĒ SkpsE Cɿw9fꟕ(Ǫf([/?KlfDk+teMħSA:  s6Q"a3c7]GG[Nv,ӭpWى d`"| {z3| Sys&9xu6 :5;.' l0avٙ9=M@ק:X4,ZzFC>>6*m^q>ctRRy 댆X ?!{v4=ȥeVX#z,#-LGcJ[CFV;.7\ήj!O ҟ{>P/]~TU@=?V5k(sn:nt&LkD{L=GcDѰ  *ŜMaQ i#cG|A>yO32I>I8)~xc=agb'8/x6+͈9lvS R4Fzʌ5;Ωblߓد8eǼ6q7hS+Xlm,l9z1<}FƖ|mFgCjVb_ tϧ~7.qAi?e5J&_g%-ߺ`\_EDgaI_:  JJa BF)w Z: |9ˇ5Vj}ΡEަ {<_-%<~8}|`b=1cIN'x1HױqZ΅Ii, t>Nc ҋvoo9yEkcU>oAZz =?k'?zq;9elC|WS)?co o;EWs[<(MFo|1䲸PͦZ Oԥ*v/2n;[& "x'v_]ϟyƛ],y?n2,%u&*[s/myU9qb/L7\tWBܲgQ~ozfx}a}vb]?EaZN] أ]8fJs]X{/sÕ9cs_͍. _x>0N㟍rﻫ_6 r敓|ӴƑ^{}5Ƀ.t%a=2'W/!OtoVJ+VÛ8?-/Fhýv5|(':y?iӂ>)ݾdK?wrv6Zd:Uh塏Opu }.4rO|^󉿲0vKWtV:j|/UW.3DʸT*/CKxW%gR +?sEAA6%xa=Py#Xm7n +B٭:??X=0Q-#|| BYHXN]<@*>{8٥AAl$7`p[b0'.VH|^Ξ7OURJ<^{$VmG5*#5 -ow-gb2!(jrt3Wf:[j/"\c]t䣹:Fg{oU)y}4WV;K|PX,+ Ft‹.b&/Dt!-Eo4% Z8/Y(26QU2g}˃Z;nܮM7UO' /bZn 1ijq}Cƅ\yz#9AA].+Q(g[97:[  el';&Fi]nZF{-fwNu8m1&}=ɰ߰4RrJPJQmQ?,VmC٩` hw32P^Vλ-W0ͣSf5NWausdNiM!icݴy_b?%=legew,)߹+zync8riPLag -2ɘ{ӧ ~|UвyR\s;vG 80^S6{\UzQWYW*K]05=+#G݌6i8 *PHJLw͹GQ|O-n2w6y`-8젇>o941̿a ē*񕚮b3g|H=ad#uZ?qq"\`|fRٽ-9 \`qq8b]œ5IU3BS0:flhGSO%m6QRȿMߙ콩PJ^vWtts$bM%'trQNj5qk:gPZvo# l9%4gaevtc,sr\BFg͢^,1'g{~ P_KV1 uju -W:+! QLm~/c~9."tܳ'iixN>YsH}t۝Tr.+/Ř~iv^~~BSy# 3X2*WUb^TUՕR`_Xsrȳ97HdrdUpRC>9^HCC!-evSE&m g32<\0~E54)x<8+ոl:4PS<  \}G& C&69NTl]>+":0CA7Iב[_ÞM>+(ݯMWMaACmE8;.*ؕKqFG%i6QB4Ö;r=v`:k#NTLjtKL 4WY :zN}6='$a4BQb&ӏƅb2BChf( e4vP=|=&b@$4Ip"^̶rrC<@˰\{j-mT򩷇2B+;Ɓ wU  ͘GWtC1vPsEyjzBiU>?9O<ĜWxu}'k?]ƨ`HZӭĀQF-ni?BIU>i=\]oKt SjԁqORJjpumUpdhĆ췬/K/gy!y MU\H>ƱE\t.zf~rPxu]>UL,L pm4\5fMvPG"2;6\U+zQg]3o * ީMdt^]cf{&mq+8{DOKcdEWO ϑj ZB"Ξhu$Q' [Y!v(F*XvYy4;)6H)Ms4IXK3Ư|YfzXYtكX4!6nQBDib1 =~fbf G/%6]7bQ~%}nzKȚm~*z6rK}c%n (K2ǹu]ealB1UU/kn+%ʁsU,c UUʗ /MoVl6m~}#ޙ7%'?' Mײ+xhY-agU]ūz$J2l(,KXw88;)f\t`V~}IR<)_<&j cGQ.^yGh5>W1{3A_j|}#eJ禛ΟX˞ˉ%0 XpdSmx~׎'!;S"jnщDRaB@ t.p[d2A `G8A'T;сizEgr¬yy4nb"Q bԺv1`΋4Zmg%8u7L$]6c')2O 0X_Gy#gMHwEIPI] T-/ 櫞n[({]/[dUUG2JZ#QY on7%'?5 +qC_]ΪRW;Hc[9cghZv.8{#L&YTӘb? j/fxRt61{\M7) PSGz ee0{3Acz(Ͻh4HK"@ IDAT0x8||}2ɾs4r2ϱ7%_< Y%٤:S1x`g⟡Hb~v(=;Hj1W4,zɬ8{& q- [ސ`5/dSt`|,b15O^κWE51%1y\MT\$v3qQRKIА{٫k:f<^|\? zT ѣABӭjj@OY8Rz$DdBKA ו z#hiYTWmgݠ3 Q[:Fhe22csRmG(5|lnڇس|=Tyng]5;vXHq:̱xl\AZ ]nڇ{n=!Y}Cu SMe>N`-gci*0D*fKM2vl s7%Kw%<2W: w8zz!+\V[:(/51DkSPEd_p q/Ηs~#TR{*ٙ{@qƼ|UT։4CyEL>?*|d囨LЋY񕒮 Ϭ%qLSHvFߦ[\ʶmt i(OEIƏ{)y@RkDVYN\--{ ⾩M%/RCUWq:f3j(#azEMyͦZ8<`Ď'6'Z7 ݩd|\T3t2T"Y8Bx2d ٺK,w-=_؜қ>S_WBݖbҬ: cg~{Dn9KJ+^iG O qfK>WM:jn卛5BauxȨWvSŴ~ir=6ٺJ!82@ؘJ,'U)WmdoU gچڼ Ü:T GOvzWFE)x8ҽ {= NR?=2ɍ5lv7W?2Dks?uWͺ8cV\N+鵛xm̂h~a I7Q3Nǡ+5]/YK0}|>)hþyZ2ƺ8pTc[};9sz͹3V?q;0?;KV|]| dC)J5.VqFz$N3 t TX gCZZiL| d|`x(Odg8a(n)ٳ^/{;k    !#k>dC3##ސIZ+ظk(;uwːoMqIvZVm;h $    kE7Ql? /M|wdWl GpF} uwO~،:=wՐkq| ݳI6NN|O9~{;(wj}?uL7}S~3OuQW |-Tmz__Ts+Tg0|踱3 7gh<|`>>{#u0^27Z/dsn;c8=[1}ZntVS7pϻ]Fdy3R7R^?vjj"w/o•ūvgW||mݸo%=˿](o{j8a<}&uTg a:yⱓ8x@zyo4;|{|G'0BK@z(?%t+X=N'_YHR̎I x gWBo_?';xPE^&7߿4S|%s w{Q-D١|;Ǹo{ 2xgog1/lֹĕ,kHaiulvap|:^eܶwLEOD9? ̍7Y)~d4YKMT+^s.I۲O#gEuż<| 1{͹Z~ޒCd͓m* ]~+9lQL^/  T2RX ŅND|\a~_Eo! :[9vɛx    p&z%v'U_ۮ]8fJs]3nx~}7wwQfSFWm{|θA~@oۿvhMLz:}?y/lz?Mnɰc ȧ>~4i%K/q勵}{>7gnWm7U%X{/sÕ9_{x97l]A}<=@:}6ʽHJ(șWN'NGf(ϤnWm3x}}Sy8;+(βCO+'o?ӿt':nScx>ZsFA{ymbt>,w?9J@0%IEgKU3%k^:WIAAAAAXh7mXEK͂~)zkm<|\0M٭:??X1\R+W>AAAAAX(AKcWP]W9)lۖȳ-t 4'7_eEAAAA !O [ʼ.py7s.     -dŶw>N.v)&~GNVRAAAAAAX%&      ;QAAAAAA@6QAAAAAA_Wq34UWR ޘlRjle^=WNRsQ >_#%t4EY϶U4n(#K/xVN/v3jxqë``p r Wf2Aމn }u'ߕgEyz9{DU1͍#@~93=_,ŧ2n sӫʈ\l:eH&DZWR7 e[" 2RQ& qozyPaލ۵_T?c#}$]!EL-c|!2<#-W6oȸҙ+@sd? LC~ 5=RJshAN?hpk|*a 4T㴣E&zh=geLpuE们DmnlU/g0?,dU6TePCwzi칶f9UgŘ p@'êw֟FJN ;J)ʰ-j?*m( ;La[cqvƑ/vQKW~yT\yt̺"6\21nN׿XC))$m6kT짤g,@Vq=3>.;;wrEZ/ϽmG.)qLE&sp|/@s \Hs3Gz'{ ?ύqHj~Mߛi?S#Xz^?㇩OuŢKH̴R{'[r1Q=\f^a<ݯI09/K?H)=[cΆV~}=X& ²,Zĵ@oedMLv?pmJbץ}g.w'Mg|LbǕEArcw47ng 0u@PV p7{ dQ[_;5;m@)~gAssծ"M4N$djFbEspj2:֎_Ox  O=Ա׸ݕ6<qXS-.$? x\*zi:rmlZNF_gPZvo# l9%4gae,xYȭ5lj)0՟5zΎtǜTWγ5ޒOC}A,Y4eϳ])`ϣC_a$bK#ۯо6 {bjcDi IDAT~C[Nq%?5%虖KYs5tGgk?0IIM%WK]f竈r?7~n!kOhd0PF7ϗjTՁ351Pծu3~T1[WJzI@\,:0CqYa-`ɯa&ESf y"2`ACmE8;.*ؕKqFG%i~nz' ,›(a^aK;Cg0]UX5 '*tK2ȲLKw9i'& 0~?.,T@S7CQ/T^>S֣aQ!DB'ⵘzl(+'7:Cm  {nFNV-z{(,c8pЌtb(0َj"1l|PZOx7O81*y]i=\]oKt SjԁqORJjpΝ(.ܙA-xR_^Hy^CSrqr40^GqzcOW[ɨ皆4l~P?_V/u`̎s֋aS.RՋq 9/$XBQ`MqҼy5DksUe @([Q4ZzQڹI<  o,T-C~l?xWSz/VHyC5]7= %dM R6j?=f9]M㥾T7]ikøȮ* g;8ڞ~i$_Ty\s Y6TQe,oB1~KY_*`^ƏfcUEt`qw+f?qڼl0OQAtF1y\MԺ@2Ax>wE8{ QeuzA=7Rf;?bsMOveύD𿇇X2 O/M^#;54l+v< ɘ9Նݢ;TBZxsc" @DG8A'T;сizEg *5‹Zmil"D93:~s7y!fY8_ Db˒V3},>$9X(/vp cSsQR {#mc&PZ*(/ ,<|Z wK3{lz [+qpU߸UV;ElX8dA5Ι` nRWM櫢jIQGn:k(- NzA}>Ʃ+݌F9s/;e;^H^ojx_1Ȥ_=^+!S?W=pYhqGjR(V0ײ_}MBL2ônz{pEAY[DFU6bU3.Hڋc`"بbxh&4$764$rRJck %$ܝUtqaSœžN'nM 5t'1g^?6&`QA籏CkiMϛut,bqfubw %6>cSۛ})BEmrXGUk/Nc0q5RjQ_:hnp~:3mh``z:oZU/>' Kg ' =bR)J4xqvtaU3ʉƲ5"0@FRb76%0VZmO~z0֪ZFz˃BOz:1j;}8jEVJi {1t3agػ~Fc=}Z#%b8eF-%lmJHE33VXhZ{GeJbZ}.jv[ӓڣO[ā~&jx_qu 1iݨt"P큻V s 3;ie=F?7q!F6Nca{#%;C[Ǣ e(cQy븹vj.]5QOO7]$YHX\tְiW}bҨ< $äIV{jظ̂$fZƊ#?Ӗ 7t7i‚T fbTqNX|i(e}O28Wt߯x~~l,avlΜశQYNdgu޴ҭ^|N>6fav4SӃxib{y8s 4ۃZ3L&(tV8`wVadfInozKҟF&PMsE;2EŅ=eMMz~SWIY\ȣ: T=ךz~^Jdظf/ZpS,NFLis)QY|uq2-l!|6깘:&R\i~ %NÐ8"{!BdE!B!B!b@g@!B!B!b"I!B!B!Bax5b^3noŐظ0*٧_w/GX*\F6<}?m[ p&<3'޶ޕFHc.oFjB xoqyN!k$c\ Ÿ_do7Pq8b2X2~0ߤW6B!B!9FD1sx|:bEWC%Q>7Fzj!{>Q}1soو5Й(T+UNGg=ò㼶B!B!4yeTixio5Y8빤^L|'ΆBTv=rtB!B!rFD ;;˙-oܚ#77dM7^W+"#1CweyMϐmE7kR-l:n7=P{d卫7\>ɻQmUWyg8/R;x{>ouLߓ"SXL+3f fā7((meHIr9qf4iqf *Tʫvع0Lʣ:kW.'5C{>Q[2N4۟>{f)XiAtą18 E7\Hb!)W;~mm<Qr ;)g{B2K%Cur49>f}~gfٝ_ꑶ3Vol8UO}IU dZ@`.I)?Ć(L>L<۩s%JY̖Ŗý }K)OvC0I,L :bT+RN5X{M!B!B?~EI˗%n}q_ Եy4'[ґŒ/ci ,?lxP &{,TSg0y,==~At65҉VivTF:]#DzQr|߻ܽ1>|ɗ{fًlxclfG;tۆOyۇǝAPmm|6h uvQ4ֶ~Y?PA\.{ngg>Lফ.ŃmnO/>c.#K,dm$ƣx^ABn~Jg2>mߚ9iy7J|n.:- 3č0m/kQT+R[mMQ*'B!B!B͆D12{mp-1.;iQ LNbw 388Qr/^s', ie0dּ烿޾\3[w..okزfONe+5GJ> (f: (+X>nmN!OI&nV@Q0D t|Yg/9 x>Zǯ|@ޫ.}*v uCi9^c43 ~Js}{B˂f6ղ{P^(S( }E+΢&JvwReqMeqǦ$@|H!B!B= ?b ި:teݴl|6 0-ᦟ}bo9/{BMN 8d2`#y2pH>ʁ|SsB!x#_:\IWpvNS4΋ǩxכpYaٷOY\)!'+ALjj*iDCZj*i$E>JyB!B!B23]kև0бc5ݐ4hn֬_VST;./5pk'6or~’sXp\!sg3-?[M>~Zqh++pm~ Xvr:4{5aQtU#Nׇ)!'g֌߿;Y{:SF;NOo?q2zK$"% @ak" I,J:/>^1?2s8֌yvP""VVNt.8+JBҋ8)Wߨ8{{vt8(D$SPoc\c,fֶMۻq*_țR~cySۄg9/Lmwtnto]'C @X~vM qM>^1^ !4LEkJLQn ^ֿuPZL{`C3iolA)?ϛS~qeI%7If }ΌvݚZtocc:-= 0:#K!Ę( QQaU 'ށ)4Ҍt7mep\:3 I ¨馥j;W(_: /rX[zAq}ۍFoZڏmjiǽ+ =ޛ빞{i=/I%"2X<=25yz}hkGÛY~L?=6>Neϑy?}<ڏ׻~z%q’<{EvC)8q}{xeBbIdNj> ?;?sKg_K;M`g٩I3‘ Ibq[j(m3$QȗE[@!a<6a[3o{p0`1ձcGP4yYp/r>i4pA2T@Լ9-rQS9aQQ&ʷWaWCHF6߇śȟ󡝑iXem^\`RHO~8 2(VLFiLN5tNK4(GzBVIWBÉPmiD5 [_KRzmz~^n7t?@k;w1hM+]h??t:M;!o{sO/_b 2°Ru;>e:_ߴ*āU~nJ ,a6kz{uh7i#q?9}]KyG;I#PǑ页В{ѽ'(6J0$0w~wSAl eT{8uT<Gmf_MցuSwXZξ~(8*x\='StV7NW0O%'@U^7`t\~z|-1oCK].&R 2sy>Vڬ$:,uެ<5χfaIL r;>:~}΍&>:_@ٌ}~HN ;@{7Y2x;_Λ/v2Fnz1x]{:4D4{pAf']ʚ;8NHoF+?) UE^親SMpxDrN2fľW7f<ҾV~[g$_M[@Y$vulnPN'%.OK}Ovһ;؎n -v#Ɵv3|i{sO~޷Z%F" Y\BRJ@Jo6%5]xՏկ\~\D}HOV^WEsS'fǓxmx\2sz~EuLFEkh ;uX'[j'jiJ(1Q&<]jW]t DGeb 5 %XV + (u~xkMCzzzpDY9šikƓbMv렛~*?O #6pDuwIQ_NmQWH Ci=ް$ 5T!7z\ian$p\-4db&>+hC͘ ( IPhwйCikxo+|ju{Λ:NK"28. wç T QvM@cdzho+q?iy]W9FԤ*PRpYhm_JO)hZGёDQ6mD fbWO{K(NЎۍӣi22ņ!Q (m:0χ9ȄQrϐL((3 FLshgp\}:on~i!}X&uX9x}b F/?7! $a^+R8-` !G8h^!`& 4fOexKoQOwԎ5F} m;>f\ҎZ%NK DFhj!)5:+]><<[5}hnϋF>T IJ몿ρh4Z )ᤦhɶ}];J0Э^z_oϯOS;oo{Nqc7,el7fgaPE72S0Q: T&qt)?-ho[#$''?=pԁ{^D:;T 1Y.7::ݽTm+f=4i/Yy(.U1axlaO exT01MZCЏM*.MG!Qϛj&42}I5~8.0RSÏ{ϣR{谩F|(DGvL~NCH\4e .6·NpP&MoD4\"IMكgPL!&+x2P[j]d1-5?nhxBiH9%D4eyGcBvq+Os5Dfby6.i?NqTq:]PYZCGp sŎL`o(g0(p/h]ā>quFA&xıZiݨt1@큫B+8RBqZ\NJWilbuD#>n[jZqLO o^RpXmtpRЃ!&DOvigGV5h,Q#Nj$eZ!vc#P2 S keжAcjea꯷U/-_gk%r73g*8mTV77t:ӲM*YT``<5^z^܂,`haVg 6+ݤN0XC>FlÌRek9p\QL?qsqaheOYsluFM;܎_oFN{z_}xq T;5=zDh;i'{*)KyvUPd)!!1#`k:āiYk5qu/k<5uHK&#?!^3ҪBٳ9-koD 2R ne)ӈZ? 7%*.NZx_L6Mv\L)4|3S |v*;H>0̙ kʨ ۱¿ !&Y.O!g"'7G [sD<Wc,o?]\NTX)6SbBoSThsLƕظ0+ 1$1(.(q/B"(Bq8L gb6RWƽ#I=dq)+7pҎ 8&Eݖ :$UWrCFF䘌{!YK!BqQ"ȞB2fFveJ@$B$B!B!B!0 ΀B!B!B!D$(B!B!B!0b"}-lfȥod֭3d+E#9oL>P_[LfNm9+61]ߌ#ROS;:<~x_(!\ {s_5s'qY;c?AH2~ _?\ƽ0vK-W:]dB!B1i=~*[{;~ZJEgswj!H1Ze$1,3.*챍1[L|nX{mbpK[K/;TM \ョ>`}B♋7vgQ2cS=X[T9SԏMrN㙫xdU,B!B!ݘ( 2zXW<zAOYoeTiLrĉ_%j~NZkQ^zlnc1> t6_H(bυ\2/!!B!B&Q d_ o>Pyf#v޽s)|z qӍH@oo^ce<3;x{>ouxSo3{v{煰sęlTo= .cΛ/iɄm>0a"SXL+ŬFŧ5qtƏ/dG;bCJ*^ˉ3I3cPW^Ͷ̔8̓]x3Lʣ:kW.'5C{>Q[fgpU,II]I /=)В?%v LCo;z7~^7_PBN;YV vOvH@Տn"빑3fҁ4v,>=4`?B!B190?2/嗫/WF' !ubۿP҅;ӯ_&~.U~*f:[zz@?^ԛE~942]3/IѾ7Y㧨dpu0n16tl$_U7n5?_]L1'rbJ,DQDaqw7vō!dV lo7Cv>|a3{{F )|e.uJIW4߸c1YQDm|hLlW9h;8d0^X\43`.I)?Ć(L&T ٭Uro)?n& APKrRϣ[~ IDATk W%^K;aScES!B!5r^nF;8Pmoɓ\M-Y,2r7OgS#iQaGIF:]#蝿(Nw9]^Zrǽ\E6 1o63k䣝#E6|>լ? Gjn3m3U*&EUX:e CsÍr~Z2'm[#8tNɪ`W xZzbTKkͣvUDbji[9iy7J|n.:- +|hOO W*Xv3ߝq)P 9\n5o~bEkxS >ǘtХx6eÇulecyĖa\8$x4Ƴ 2H͏VWRwL_ç [ǩ0m/kL(neCG?Mrѹ+,=/]>y흸2,Tjm£)JD!B!׋1.;iQ LNbvI4 "Z~ˡ} fN!Zq]q]N_װeG9v|YݩbW‡tA!!1LD[.s$ 7+(b9c]ØbY0LצZ6vyWu EaOI>cVziN  /*bf" ]ف A990wxCubU>݅ !Dij1Z?iี;prh^*W˯EۻMTx 5cRX|ǭ L':p5W#rtcSHSZ%B!B i%7fA|_rJ:<r/Ǘai]ML ~ QLf<͑6ɏk.0W^ ]G]Jyy8Fnׄ[20㊯>c+W1d{1NS]tv0i0sIt 3 x,/?yXܩ2(#"(f;Бg.U?TUAl4n琥NI/Wm#,=`onIx֏dcs n'N'Qp{p`2B!B!Ԧ$ʨC 霵f9&nf }lĤC{ É.(B!B!ıBI^{ATq mөA:yj8!XA@7FvaKNU)Zа|$qh-RWksf8)Tꦮb.Uκ\|#Wg&&lfͪ{a?A-£;|}T'guw=fuKǒ͜Sfp7bi-dEH\&%.Hr+(B!B!1ED~5#ӭv!U\yl)$1Ҁ_h 9oyVSftfB!B1&$i)K< &2B!B!B!D&$B!B!B!8o!B!B!B C&QgmދU;6͡$MB*!RnH'tɽ!Tn%wɖlٲv]mrQ]ͬg|>ϣwgy@ @ @ Y<6_AA oaz=1ɘz;UPWACo&J9QL jF&0hQQoUC,#94FX -r(|K޴$Ѳ} Q OF:t'[9~HlM88ʫ5 iN 8E\]%?.," uv)?]D1eZIxqG@ʶ)S& ߆oi_uJt,(΁!I6S_oa.&'L>Ό`Y ;yTb.7?yWk/e:f] وx.!ڥ@ 8gE"3B3OL+& O49FJ̖dZn(0c wvpx+VRnN+gEsG bznN v Ȥ{YkqYCƙlr9JV1RS%K]|K)ͱ`";`@Oeԇ: dWTPKň qi?g:8uE[D}hmTUϫgP>tdU64mXBqz2*qa1V9Ug`1zh͈ҝk'a)aSC)E9a+b݊ 0:ıaiu@Qjz vN)+2fm ?:dr0:8v~\atw/Ι("=yeӎzI;o~vثXs.+)̱`I3 EC;6o(jIHW jtN!mXs*żpyT.ZAWO *JR)Ӌ4ש) Xa+o`J VL݅X13Ol>B@ $(iE\y*^Svߙ\qM%#<2{dlZ. *:i>%kVF$'%iٵ6$Wn`y1t|PV .\q]C.u4lIPkpcA-Vd83uTtaeI"+z/jϢ^1 3짩c~ b@]V1 u3.0Q[{qETidf18P(6?@Ќ˨|zNȿı_d0K =1vv0=< 1s%쥻}xh yjۥAiԁ.W]㠪̏ qA_SP^~-Ei|~TCPudT4тjl^)ZJ^0:Yj\~iu\z ,?9pְ(KY[s)Έat=r/E f_D Cz>72z8[Нl#NV@28[$kYq;L|r:vx0 S'b2Bv4bxqhvLdZ D}  'j15Pb?bܨ}; xuG Ž8z՜4C1B+̌<Ğ31GI*ӕ9փDYaTW _&JIe!2#f̖%Ro:~v3Geߘg,*I12)\;~ZDIU>c}uM\P2o% : ]G){8hViyg>;BOZ\H`p{1}ɟxq^GwSRtTi$$BQ`IRwԡ,.k7OJ4im?uˡo(z2*ސp ̓PS+ΚRB ,,x.tT6j==ɅGY6blEydF\rd@=/ct4,/mvs6fޡ1YHVs !1YV$<0fg%9>x3Wn,gX(B&t殚Y)1닢8Lj\d\C.O[1I ag$ 9s9Lj̾C/]7b>\IJgZ=Sk)dE{]lj@ꉯ+3LX(-nc0c$#y("lĠ@IaL$KBR*tjMF_ %Ei|[)j$NK*ٞ }G8 *^d\ gMS@D&aq7LW  iZYǠ v~M>z -;b1S =~ZOg G/%6=o$XED`~?%d,Vk?%zV|C}c%iz (hsKd׭f[]s k [7%]IW u0݂T]I_̨k7џR(oHo(:2 ),C;)^d\5I3!t "'HE㸚yTGEX:(EjU?z>bgǑrte#ek9}a.;.':|Җqg?~@9;54+NIYɘ^z`7`D"xF<0oݳ &aQPiF`сfzE&MdIݙ ۢӸED%+:~gcoTE#+.XGJ/w2c+4&Yqf>AIQ<_hW5J?bt0q(Y|r %L#9r;2 y/bo;]y3H4,0εA#!bFi)Yߴ`WIPQ7nBIuF83%:_q2L+igsOVִ2%ޔMԸ'Fzn3ґcKB/pYo4iy;=Âi62(tC)#\c ?*]YJ^ɾ9t4p'"vtu;)? SߝcL8nD گЕ";EύV (,q)kJH/JbGq97 # 9ҍ+9s6)^NF7dbM,^/ZÄ4Ё牑{(i}]\4GDF GRRh&dw"{%j<Ho1g%0'b3c hb{r\NU]9 t*&HƉwszQ@L\+2a@)=*N\3̘׏\Gm0Q9JR=XQ)6iJˢ>ΑdO:_SzįC'zqmd&#":L39y[94m_?L[O!jVMK=@XFnIZrRUgebke*r}n9+KӀ'ޔUԺam*c0GKh(%M^.7,cȶBVWXYE7)/:nuxҵM&0:Lӑ)/˨('{㿈2UW-pJ0 @6aXON{ u닦\6h*@)5źRwOULu<q!U~MEi|S~V(+G4'i-XUm%^Rz:]_r폫׋VqZ@xq5)㨔#.BE?GhĐVZd  Urym}ϝ`8Kqɬȥ^v?w\ܐÆK;$M.4Rf%^i?~AlZ-\+ v@+_,'븬x>:H} 6$Qo@x t TX 9xcmyuX*Mz)չ}Lb!:Wt+:$L.RWrr- (,IDބ;ޥRo_\H~mƉ;9aIa_'LxfD>~8_gܾ?izX4+.jr?>nҹp{`ɞrRUK@W:U@Zy9F^JzGG M- 27}ӾT$J.Y'o`m .?$_yyŕ^h{S&z .[ r.~▞Wo,g}E8-gGy/gp[WTr|w| Ok22]=]탑2%@ XOȾ!Ne4|mS}{x65_yY9ߵOچ6`3?{j@F)S?ˑ"a_?㪛j^=|@ @ XZ,'Q"{Mo[T] ot~~$vꥈ GkҞޏ4pGSTgC 8Jl9WaNs?LNi.GOu~}=7lNoO7pQc{7_ϱ1\HIx E+yZ>%/6#^~wjiu+o8f 7ȣ7>ʁSXjդc 8zI[x׮է&3"Nx sU97+fO|ՆdbVhW??Pݿu_a?C]-F+S8an{e>XFG4!zT?ʣM4Z#yx'pӕE<1h@ @ HOHٯ]﹖P ~]K3Fel~|3gë|kZ~[xɹoycny }GKtᗹ-5.}?}1 ɓ^w<^sB ;xߤ3Ve|{wcIEsQ|&F9T sСrE֓~γd(&/:8}Csa(%oΝhFҳ[h>if8ΚKOV6toeoV>7k5_Ɖ3 {Nq:}P/m+z;x[Y5|w3WZ|{2fel#aNϖ^ Ou2 r3"C|燺4+i)./ p㫏s7},>SkR~5w޽-|N4 Z`9! 7)̓0ꈓ1[&i_/=3~HyyԯtAY}FFJY(ɀ!!40FLGi#(}~-ܳs ̒w Zߓ/{jx9LS}Boѓԇ:vtR{1>.1g o(x$Oʩ͟JmWKw^̽SHz0˟HYMB_4au~^:@ $8X/y??w;oGif4:->.o{3uw/q_oK-Q|#\'0_g8\RN5SB&W?<$^ޣP;yq;ڵJ~+ydb2O j7hgxÆ?X/:¾gU'w'cv>[C϶jtqmrp$`ᑻkyܿt%Ght # >&^_Ixh=oC+=ؿoܧޏ3BӞntgVbx< UWQv?n)wN)/}2`MgS1=UEMc'yb*{~Bt#Gɟq˸c''ʦWy_Ѥ}ѩtl?L8/woE$V tK$3WܮcP1(+k  &,ˌ 5^B!μ9\uxs8sh %T|O|],!2cvxʟD p@/?c&׼s-;WE.G ,JLO5x+@ eOEQ?EU:+WbiO*9Nf=Z_UIO{ÑWju]'̑@aX-6&fug_cxNv=zG9>!lLKǸCIN#3N[3gQ-9qvrOvr̥cG]&g&Xk$fФa|TP_gE߫\5IGhx[C&l hԹyr,-oÉv{&&ehH%׼{ #O||NGdY]:[ۯ(( QI y2U\ Շz}$B4X o,Jdg\!xNYgCAKҔgA>a^Ͻ+_Dל]|97}J_2w?$ [wFhuYs__t1(YӺK_Fx| s%Z@ @E9 0y@-6sǝOtF^ԉoXfCGrΆ^ȶywyuSHl(G_{vX }jsL s De"pdQQ`E9¨{ -Ɉ_x_(XUH5i~E&]'{>>G+EJȪ#򃓧!`~[_ǟ *r_v-ޭrԱ† QӺK_EcD0e] @ N,̚IVo?ːL{D9#ۑII?t^LC׈ѱO'~9㩒oxҋu ~/~tJ7\}~d|ca0h*&Ȝ,LvAr̶z2%'ӻk7;z󆑥tr3U/>.Mq4ss; ,]m8t}6.Sц<0\VT^E*$C)1=2b:8~_KlbtW\8 36iSy41,}Fo序lpw?؋=R{J{,;cC ="_OP l!IH̙ޔi]%$ a>"@ Zl|vаM!Zսte¡|~dFm#GxyJ"=tvNdOO??!PdTv9ޯ?!uum!!/25VcMuXX&fj5ISY0L8LGθN~O7l 5}h3wRNYOEa2N섏LqKq*](TE#逋hY W6ŔD\mIh:G͑2+=y]i|V>;~ABt 11WfQ*ʞtyI=dp.M7c9y?4gm_[?o1LOONq6z t' Zzo*;2;=Wqv~$w2c\R& fBoONBym~e/989B/ :L4ߴ ^QƑ0O>KR_u9u;Vp3M<3zj>ze:ayMWQH75:UJ3x֏ob~|Y_1B+5&tĊe%賳+ 17O=?=slټzj|biJt?}n+{7dqk(u4ZCt%l?̡32GOCCtsh@ eN ާy>7M;8W [I s?-8q1 #3o򉇸.È>"_<0_s'=s'N BO͛2ئwXoj.|Cx7_~mKx_o0=`cGΖE{qD(͵#ha\9ÁuOLCG\~6nʕ>׿q=s<б#(|S(GO77ws;xV4ON>+O ]nք_vΜz;n:a{.tHl :mN7obt?<2J;;'Ǥ^}SYFhg~9OoP'j9\4 /}z-Ctˇ:x'\JWyB^TcC<}޻w~4CO6+D/n#oc4@'=¯ sy&F7\̏oEC|PyCuqmxsG~6FS'Qd:;C:>u }8Hϑ.#<9 )0zj2s/ݓV?ˑ*Ŕz:ObX@ erՊY@Q~&Kd{3Hj POk.'7q'$oӷdoʋ: <1<,wp?}ɣI} @ П@ XnXoᑏ/,@ &( \kޱT{z ({vn$I\͊<=RW]m:Wezn7؂GO ,3$ W)"@ Q Խ^]dѧ8`itp]+8-EHԼ+h}|'-&R yП@ Xfd <ןtq@ @ 9ZyMkKb1o%XF:sh,'Z"5Pb-g5 IDATHiI mѣeH~4ʫ1tNrD.-R;KAS}$Ӵ_'"_ ~MPȑ 1-jZ@v)?]D1eZIxqG@ʶ)S& ߆oi_uJt,(΁!I6S_oa.&'L>Ό`qesCR֯,kѕ SM X|v)?YD̴ !=ӊMb<6RCA%[" 2L(AÝ85=(񊕔Sk'szB곘[ƪFC32^֚u\֐q39/@wp7U ԔCd-RJs,>0ǎ0P9S?%4Tc1"Exvڏ2/;ĜKb s,X H nǎڥZW*v2.q23RSuD/IkB炚ii?Hf&R`K 8韙GyQZkQ^^WBK$U-ǡ)B.Q&l o癃'R6qO `E".| ϼv~);L`n=qD2Rn-s]^ȆMX4Ĉ5+t#ݴO+7<x:>F(]fl.8B,j+`Į}xN}gtHbn)L DB*JH.VMo:Vi=/uPs)k\öJή^:Diocx\V!~6$[Yʵe8_fuJ ۇmuopCN Y:*R_o:r밎 в$DNUgP/uޘT1mkw .Sju -ef*4a3vue(eSciheTo>='_/2J`%;; KY]يM>obgǑrte#ek9}a.;.':|Җqg?~@9;54+NIYɘ^z`7`M=P0oݳ &aQPiF`сfzE&MdIݙ ۢӸED%+:~gcoTEHœb2e!Deg?!')ۓt4)G[o+9ÌM &x/ u-Xg43;Sُ+cδv6deM+SM)ڴK!pybfa;/9:i* GL3,f.I725:ٕECjj ]QKc wCؑ5NYe+$NkݎTr=RQ`Y<{RהOQ)oHߐ # 9ҍ+9s6)^NF7dbM,?׋V0 t:s*q$|ֽR..d;S"##VJr))4;fyt5H`3?]rtx9~HxԃG. : tFiz $ވ9=( GBG&.pQO Euh'^fG.ͣMh%\ abuuy eQSKH'/R)i[U}Q|&\J6/ m5+٦ O#S -9)*ճ 15B2Vy9?>iUoJѪ]j]0]6qU~t%4ɦ\/a\Aeu1d[!+,"zpE͔ח`:Z JK( !ZK9)}O[~o`+dgh7Jeyyy$9k}s~X9t G\AQ^G>jgkI `]u(ʢ$${lߴry0&J׈,ӽ+6GV4k{N lkÔ|jI'X Б*eJ>!&}iu?_{EydWZ PJV|<:өutǂ)?cOо{sR9rˁr9H%韃M6,ڎ\7J8ت;;#04;W祽{ 7T*?l8&q8P?{i^Xϯָ9sAl끍' lk 2.:#B8F9JR!{ۮܮݛKuiߓ~Do>NnmWpC3z_~4 ०rS6aoKkD֔#!!FS+M ƀp8LJ"q13F)|F9v5ĽhXoBtBq>aTS\nՂ^'{ 4Zٴkz7O|<<gYm+.+m;rC[#yM傕ii ᪌+ i{vhJ$.C ޡ6'9?Ʈ䪌{!%y !B!@m?~JlO3][EHĽB! (B!B!B!55vB!B!B!"DB!B!B!M(~4o+jߎE~9vd::?M\O~Hߙ@|jKo#x;eF2Hܼ*?ōQG`ܜT&Uncqr4r/#k{73._jSڿuwyOM-5%í\n/06zBo?ʷ_5] kp03—dê[X2) ǫ00ᵑN8EyUyJ !B!WQO~(c8ߏY{,f$Y|N;Smecec+=Ʃ{SmGC<&mԼZ0xp+ZpD;ӏ~+W*Xo^ Z~Nίۺ1ʋ9x] Y# !B!Fi^&(PT=>~EΎ@; :|SJ~{7~jT̓@7+$+!o.D? h3?4ʮ{@Zy正l͗^O0>v쮺fdpbI*MFDrεz4yǸ ~HT?Ma/?'qlT<=V')B!B\jD̴r5FWxٙV^UVXn=e$t#bl?|;][AVW[KʱYdtSRyT6>aޔle YSRu aY(r~H#r_0Z/xp]Dc'XLogڌ+bw~{ǯi`h+M VNo6]m,:uiigg@G>3}4wlC^>9w:?]gLN^JL?[eXm):^?ח!+k?Y:e ~0pF܈TJdV>ʾ}fG~y>|j;bgϊjvf\ЙJ9GA "yPrqc Gy#cn/o#V]y`:7|ݍ9SX3kM6:07~z3i)\~?Ne@{qEk!w`i0z8'Î~KWf)ܡLr>jb0jf]takm+{t-p+Odo%;ueK`-KgUQʫ-XTyͅ4C~mg&mqכ;gw6"b^4LfgxԆd`j2B!B+I(廟]ջ=&=[o39d$4PZ0P-ɼb->cL/" wc*7st՜ѣ0&$+ΓkZ 6й0X,Tfa䁎B灳;Ž['UȪ٘zH.Z/jMxO7 _m MB!B;:b>swVe/{C lCZQN~f&ھ+m>?uʰxҫ|ٚl5[ҵ\c![[vX\{ a àG_7YKT]!'8x;Qn'2)H^ Edef=ihgO{uk __UоƁRjO~׺׼dfGuQmZiF8[W ..<|LA3VƎi>Rh\Bwpw/Z Ŧ]7 eQ^@ 4n^#HT{>%$@{ݹ_<{~Z;^Nj+ǫ5^[W9|Mw%a{Tqg`'IqXkS[YYe31*入de_xiMwz|ҏ0ߩPBZre_dBYڅqloq}Y'DEU)?³::^ R3myk[()xzkh01njE_m<9WnZ/*X<} ^1!B!V3Qv4&KfxQt: 5P#];nghLy/Lo>Nل )! ?g6PMl()RR\J =PC}zyQ{V{7oLú+{5G8fɈ',nV0K;ٜRNBV(EY\v֌B,6 '7 hIǣDF75ڸiT@Q0+󹸺+uY yՌL,UL& *PVf7ii/#tC:[r2 HT@+~*ggocWZWs"~^ Խ^\z:E(CʙٗFW\6yJ;uHh +8N;<3+Bʼ1t\'=2C?N23+Z))h|WLFNm+-i\_B!B!jDqe,߅M۞@~ ]8^y00xR\tŘab\wgzj|ll~`b0bg'Q4{Ԍc+'ExJ)|rDg;/@oPbȱ%5+_1P?,w0g;{{<^c'Y ]H B߀mDQ+o-jgvʞ>UURujm/70r01&QtF|<n ^-x6ڿE{\i=^mb_7J̘&6Z((0!9۵.`ʩؑΙn©m O/[ fTaĘʋZ!dg \xN9 =]ET߉̖^HEG`/aH="PGOx^jߥ)- t%Ĺ8o7eaҺw#)6B!BQZW@iQqrhV JU w6xZ8fg(d9CSCn""p:K\Y n:epm?[){b7#6#6F) JZ ۭ{II|K('xMU6pRc:\Uw[ֱW+ja+9C 隂jw+tjnU/b-q$TwY֭?d)` {b$Uۿvl}C 5eѪSw7{:W[8XŲr(AeP͕|rժrGbrY=tfzbo~4sPΰURPꋿ. %ԠR_qjX'pCa&} e-5 NP}>= jͱdR[yҿ^El.Ԯ#8=pHɔI!B!>5OXgtw/}!X0f恊īs#H_DDOEi (V?(,%9d[@53 =}<͡gfhWoY=+fXn%F7L#V$`>dM\RrӲ)qhP4qS,ifo{8Qmeܸ.T[J}bv)Ŏ4drNذlu=-6l%!e+FhFXVZ%%u`cE|6 y'1k!b+?A a= ZS0z]okP=u-36eɘW)u뀶m[k#j]r6}Ow`JZc;&֝wx%|ȗxW>)Tft?#ؖ'lXF>ގzxvӘL$on2޵#@w$Suu46OBOJ^F)EvO/- vS wA(D%Usؙl[!?o#Yʇ 0yq+-EgΧ_Ⱦ;J> U0Gxq86Nͯ\71*y?n [?]&L*"5CDFB1B!B! e%J ͿcGxN- y9xg%~%a, ç6^]ͫ=I$`!I@2ka*'|s\pVc2kJ.~?nf N$s0zk;:R^yj%iRD&m[cus@ q/=X`#ef&.85 jaߙٙo߇7q,&lVvۥdw,٘}_B$IccƷgctQ[L|L&&=_t-=hS?c~ڏWKRSv{vBAb&}I=Rs~4`Z c’{36Y::?h#1u[2+*Ideg^? R%P̒,-MC0;RqvG8h9맩?҂>$}AJScL!B!hN#uMǯ૩)̽c!_Bء`L csS4z+ǫOtj٧gcdcF!B!hjY҈z3nb$ycIȮ+Eo-0?Ø&>(h]w?< IDATxwty].: N(ˊmKqK{KMsĎ%-*ɒe[VD $@H,]bIP|>H؝}Ν;3{GiYTc6~qj̧-`Ƕf>N`ؾ@ [پ?9rU4=/WܤP1rÖxnc{'xgÓ|cɶi:Vz%?9SJ-B'^ n1v|O>&b*v:fgYxgӔo+oZ2qe{MZͪw??bXf,oyv7pz/>4E"*`_H<-OoI򻸫G|Re9I WcB!B! c E+7hO1#r7 o3>"^J _skuG'[n<`ZbM3!Yz gΦ[c x6'%n^~ެ<h6a<=?%m 37~3$q~v{NB!B!YrL1b4 ~K i*3}.HKiF2qa-2? ?ļ/mj|髻vڪ R9:{ꖲ}k#{9ᕽav_eư k<ٵ!@z3d*j!E+ZJ֭ZWOtz|@+m C 9ȣ=\hB!BlX#E5c5i[V ޼-CMM*N96/5o}9C4&}tQZEzW_!fSkLj1;߰|%FVBoUȶz8~ܻeǖ|~/-|k )Tˊa-N]puP}K9?Ki˄9}L޺w}s$!y(˝-^kE p72*TsI4Q^{dϯy{GOZ!B!  14#Uᢱ 4S+5nacg92rWyI Mj)FKc@eC71t~3|I ];;J>ٸ4=_s?M~iVzI l@ޣ3/3oo~5ܰe-6g?Ɛm}|k͔wг$+җyۚޱm| fa^|QoidS?Y|ÄM[讈XYUT𜞣h 6˜|axx7]#z +B!BC1clE滸u~okY=A=yB캹 (ĕ#_[.5Qes* Ps;w.R Xy}<Qj?sSȖ+M ;Xg=z6tC6MKz1m ܶQz[NL *,WΟ`-ټa=׭nuoIgxֳEJY sKu|_dXB!B!EeB<$MkijfUuNv?ށm睔ŢWn2qg~YO8;xL?|->.>':63Cg; ̎aO>M1ty9F&<h(\-cu=R__˺I~Ս|'c2c,~|qFwnƶ{<5o|7) +ٱ%TA鵓hgaj60#@1\mp{y ̳^W)OYOqNe.W_=+߿{FW|3_m&p'MZ?f tod>¡?Ѕinx%"^mu9 !B!J;Yc&e**zv73 nx [*$cXoi$[ymˀiav~rHu^3_}o"own=~>vfڽK㜎yQk%zVŷ;{`-0q]ƶ}p&+5'>|pv1Gn\W=Ń|+gvd=S?s#eO1k7+aƬw .Q)^>UA#A#J&/5|k7X"EgsS]n !B!i2OiAOPT ]2opl08h2( j Yq]/,䘂jo:[埓x'&2yoy?wA2S;b^Bo~:;hپ2ӱt bx"~ۊ3J%8Okٵn-1|;<ҚM&^M'ǀ|wom7~>Tg .ZClW&r?߽[ް?g_ROD ÿ?^!B!B!DveKon*WJw~$$XDtXNz|j.pB!B!.CVO|Е/KbtKyB!B!s +Rؕq1^ azjmg1Nmg< !B!ӮB!B!~#ƇlDz1~ϟyDEL!B!xUcB!B!%=^ %l llRomWR^ɫUB!BUrLͫbuT`7A,0@!Ɵ6LqֳaM#b 6LjPBmF&v2B+]aUYo{\TƖ7Sm>~t)oj &B!BdԳhʋ3A/%ozlrIWȚ Bc#D:l,];5{xL\1 ;(@Q#2'x5Yh /_iE -G1~@a{&2u 7!B!&Bʍ4S :3k?v͛X[S]w{)6Fv5c]q<[Md3,{LjA9g"ݣ xW; )oB!B!arJˉrq2}N}xQ[_s{biAzI5ԑoC!vR*F (Ab5/N%E<#iP*qXtOZO)Z®NPIs.I;F^gJBÖ58ՙWgH1zl?/Izי5bjq&#|I1VtLgNQ% 㔛+=mN=J"A8`lh?awUX[Lq B ;{9߸_JʦM"J;A_ 8~B!B!c%#Fh0췴`_\L/+5eWbyrjMkM)hJ9 '|@uy cK{gQȫidch͈Fyy9k6`I#1;;M%= ΖYě6)t[J,`%l(xFrдf)'?;kt Y(+cs}{yNFy9jD<#$&* ;[t4,g 8²VncqOZl\R;=DI&KXQ8kq !B!ױ̒cZhՌgRlyXPlvl*Ӻ\ܘ7,i$> OGf+{c'SUU4'i?1J3U-5h$cj~%-.#Nv![.UBWZa`2Fh}+qC1NHdrtt& !/="Ilْs闻'WqS=hI8>Ĭ;FyCK]8#8,7= J{Ko6bMPw{۝g͍-5ҳ=†5&:+WB~z~B!B!cIbhGJuTa3+(PAɸ kYb˛Ry~}+zo?:{xNghDU7<{x r`!gt:a1f4(:!27!a0Tȩl⍍џw { j*6j1&$h@ h yv 94PTдsQH]PXYM?F50$PN Ӌ\& s;-1řdB!Bqp1clE滸u~okY=A=y&%m6PV Zش} = +=Kg#?&aǡDgSL($9[CՊUPZB#4` j(Elzlav񆃑Ys`i _5•oƠ*i NE&RFTհu/IyL8z]>+odyedņ1?Ԇ7!B!zar u쯒6U &;xwRr1}Ǭ kٹ+WM!B!e;Kmcn\؆EQJzMu$C%KQF8#Y…EXW7[`0VbnS 6[qX@ =Nf#mhbaSТa 0MjA4oNv ^CUEA) 04kI ?bʰٱp|&B!Bq}b!6y +عc3O3R IU)CL^8ޑ #1#Gg( &%HjtG IDATdI"(-r6`*Dzj"(K\T[g&'HKjƘyx |$R`ʣ mi\RB"-F0B1_S;s*lD!I4~& 1>0FP3QTEjppf>~R~`-cY gkb !B!׷{)q70)S.Wf%H׳iY@lߵ׃ ),sQ`UIyo,-tBc`/rbN ]3"ֺm(R+ݜM#3Ӳi%n}r'昇ç}Ijщ$%lXa`'iDUz:ZK g{v5m;?yy]x]MZeV*eֳ6 J1뛒Wڕ.l zO@ I]Yt)oj$~rx'v)YI<-&k_9o !^eS(ZF{C~N;7=իEپBG-@APT` 36Q zZw _ZRŰ/C ] r&6ǡ9Δ!eRrtO:jK=08(%o u4Y3b4Ы b ,V3|ZR#1zQh޺K/t3;kJv6[>|cN54T 32L)7I:n"~љ7,l^OgR^_I]E(}]gc-)āflTIxGG8fHK)H}c"1h ֗8y.C%UNl$!'{SGڍ)g=$kjiaRI ttpl8vvYbHNKwN`Rf5'.XZTU`KLxV?9Ƙ9*k6uflFlOz7}rɪ˩gXBaZnNa:dT'3A=槎5%  064H{Os(vo_I] bVt\^w0z6y%ŐCڇyZĺ]M8R踝++(83=:2Գy/vǐG]ST[$r|@UhByK\aaZicW3==.N*Lo-\Y,~\.BqeSM„҂BJ~q1y eX_楧-@YP4oYN)*i54yM0o X&fe̸\y1Lf|Jֺ ļEQlTVձ%޽݌_k Xjg(ID~I+`;kt Y(+cs}{u `(]>>.WMk}gtUȫidch͈Fyy9k6`Iϛ!QشJ>*|%#p  ٴF7@ojXrs&iee8ardA [qW$W q5Ki5M #SHL]vrh9o)r2߾U9SjhBʬ'_Qp} ɚe\'s!2Cy9jD<#$&* $SP!r;=%:7 a!z6߶Lb@aۇ J obkyQr(sN@f"zɲ9jw 7,gyBpl. Kq%O%J^Z1I,m*mp&9geesl7W2;:{ MŘ$Cx#|宝B!̒cZhՌĬ;aŖUfǦ֍.~I{(\39v1U^UO}1SRCց0hIN<6ŷW ň Bd`k5X'0u GoJ7XV= ]Y2RD1v9{nЩ>K~id e,u 0ڗ~`%98ӛ=?ϐQ0#FtX#;n(q߹~)ȋMG[ShbMPw{>=Κ,gz\d1vONW(MWhd Ne}rK#S}G3J0@_Ԋ #Q+$SCǧ^9zA[a m}X!Sɱ>*S F{8C)Ym#ߦM4}I=˰N1VX# }S.nK.+9x/(& !/0~L̲8w_T6+CQE344=z#Yrz0?B!ĕvg\L!)EcSAڇTl@EoH˸߲1^\ۗ#t$q7N88>ox8/^ jJ)ͼl/+ơDr\9J 3:Иw3Tpc8Aawhq&ᙋL-䙛NEEo0`4PB+hFO˒ƽ5LbЈџvsqX!Xg̍oBae 6O@htdē@u8)4:k&g+OJ_og_23U3eXC#˨ -}(*iܼ%"&cD bݦ,R Q# ܬ̤-U Yևk2HZ4JTӈh@*#1fRrP'ggcfoڒĻb-3fzv̳!:N _7boaJͪj UBq2sLc~:wѰ.uߛZVO}AaO.Ŷf37.˴:y0 ;%BrѲb#IPd{DTl66dp"? eV+VB9p 1?1•oƠ*iqBzGp02+%$.\,0RdܡSZ aiףY}#+ηHʀMج*$tgBDqfދlϾU9d(5{4NME!ef7@RVb`̐3QgT'Z8`d,AeY [72n9IIN=Wd ȳ$%j >do*:?{:qJ3g93Գtݖd,W笅Z;,1rN[(b$tپImެ׻SB!._1 ɳJ۴fVU'dvIY,:uԹX Csқ!Nb+ J^>$XflEv 09DGFkΧUax F] IssqZDRNɌס%/Tg W1x0FBtNVW`WWrG0;oEdRCCE㜋/MCWТ㴵sih Q@Qѩ $3sF2Irp {\r_2652ZUHw_ %]iC *}g1p E,m.AHB wpt yDoPaD"+>N NT~[ |lfҖ*lÂ٘[Xsz*J6LYd\QrՖd.WZ;}-ɾhLObsXW"B!*wnh\m؋/);w90Mp_(L~&ɶr-gz{x{e%Ŕ ;ha֕)L,hCP%$A6*e8Iktf C6t:WRs SwӒ$R&^6E2Q)HfE W吅gX)1`#Lp0m7Nxf'B.o'"{x&vpRA0ӧyi}A*+e[JuQB|,@.@mI䪍֎seRNf"J 8Ֆ8,04-@[XL0kNܝ΂۰)HtyZB!e1<냆R1O[#"/8Km#$ш prJLxG&pPx0##ӓ.~ 91PDJK {'G=D0P^[s3Z8L(|lg5QQ]rS~`-cYm/L$ K)k4.)pciqK\[ 2MADuSsĊ1 SȦWlTUgKͣʁd W名:嵥#sʲNj1F&;>V;s[وBh<60$|^T rSɳnKr%WmԵvY=Q#^J5_+5 Җ,fyl`{zgAw6ռ斍vC)Z9 YPÓsK7:)B_=︛fmqݴ缫BBc񩡎"'感,IP[o00|\k) LJXZ¶MN'*Jts/CXj6o2K`/@ L2@8ue,i\Β =Q:#B <፠bPiMV`scU=ǟrb:LiDcfZ6#UptqJls2<'P09()avԣ}tz-cu J)ňkכ ⤢8l90Ie~:alhEr!U(  F'Ӳ0 M#4?xh4mZ3cd"H8|*JhNdx v3}~@bG'!Y̬ͷ-ɕܵQ:3ѾȤelڝH|״<{25LT͹7+IF'TTaA1u[ܝ Eh4/wȣ5IB0pL~ "H>ys1B!B<9"}O?Td\ٖJڨǙh_dR2X6gPZSe,-LgU-h)-ܵو3pk*U*1Oɛk?wgs )n7n]>vEyvT^QͫN ! EiYt1糮9ܼ<#808g(EKصO>B+S} xŝyDɹ[!ג,XvM00*1!Bt :1y%1&B!{ZuVRWւbt3"B!\% ᄊ2#N~rB!PiZZ(^s !WWߨ`ʺ":&qd&B!Rd1!B!Bqݒ9DŽB!B!uKU.%7dwzhbfǖ*Ӈy3B~ lL/KL^x1Pf kn;@@*Xiq :7/ЍGc(a Od+.Rbђb\ۋ[mIK_iw1*-k;:>)AH ")R]rdkwugKcfo6^o;^;v\&KlQE")`*`?@$ 0 @> 3Ν{7V8b/%WpL-gllQ#8G(;3E}'{"(U@- F]604rLePpWCV?P'WCV?i+i5+0] yH_rgyɭWƆ1NcyʤB'k.~@OKL)1ԇRB&fbGEcU?{38fD͌}:{4ǺoGVO <ńQUPLeT!ccCSf | W ŵwT>}^SN4$= g6;u^:nl"t,pw7~w%xmUkzx`˖Zʌ &Osx0\JzFf0?cVSxzGZ~2ryo*1ϸg_6m*Q}[֡]v1Kkۯ16mq?HsY%M(gch`fVv4F -TVXa? 8Um@-:z調z\υzV8. t.VX*j.鿧@1-A<jVd {16 ` ݾˉ}O\ox1jQfRUqԷ꘢딇76Rp 46SӼIΕKVnosRSidhvi*ȩ| y]5x5huL)=nS;Ѿ=庛6ҼD  Eix,LnUddp&Ҭ|1 g8lttHLϑ jB6;vѠ|Ly<3(Ro 04zΛrPJ@9h1w.y˻rp:TXp GdE~s ULU l(3Wr6Zh]HXU.WɘV-]qNe$@~׸@~JcjhOZۂ_d\6T {yB՝ޛ9Ojħh% =4;iaYil-EdUa:(*60^J $)Gvz.PL?j>'Y9]3K++ &P]mE&ʸg %62S>Jڅ5.WɘV10djKۀA4s) uzކ rߒe Vw yozr?.~RU3-tǀtCOq(WU*IRSPTuQJ|s;u ʾzjm ԋr`3T*)Q8ygZ曬!l05B~-M*AQ/粉(sAA!Lմ˴B7=B^IE:P-Ÿh]_M}G+ѽg_[7[2I<`iZ}ȧILQ]YAKNӣKҧ p4J5-4on`>J.=U2RiRd\xA^23ah9kA \zԆ tߒeY՝ߛE.~d6IyξZؔ:SE1*pSS+0k Ц}4G eLqi:_05Ctgt~9_yct:p=$N8 e+EEik*"`,-$zhD"q4Ŋ>]@Z, s6Tq,roQ{ rgXq8)7yg;\N,<&ghhU5m7l>$FnhPƉ=ϔll-6WV2$HjkO11#^Ym{b4gL0վvԱ'!")Kq .;@/O<5>mvs[䅞H?^WbuR[fJI]ljgbZg~_ uO)tQ2~laÅ Εjv!biT3_c 3g-ĺ [Sc'm0S*i|y>TKkJxl# 1< tg1M? e=P[|K2| [z6)EJڅbB~iS!Nx`KMn' FHƙ s/@&<$=vt 45Вŷ=t!,۲ojWW-vvI{U9%D Cqa_aqS\{GᓯR'.ޚ[W6[IC&RL H\Wfaz EH_-nu죤][BĪ-4EϲDӚB*!\j0*K)D ,0Wl44`H\1@urdBLʍhGf5 !XLU54ئ?ՉU !VI+kLc}4@7e]5L]wj3la/g[Y2q G e$cenGUJBq9*%JQK$%8& ws=~"uN] N1z!\wA*W.kgot6XUd>JVrB056rl ~?1Y⊐cbMSHENkS8M2n/ȹq5 El\Ww3V\HpLDŽB!B}^6t#N !B!*WCٰe3MeL X &G#mmWb3A2?񣽄ykRC3`r66Zi|w*x;x2 ҹ=2HO>VTF~ۿ0!B!B!t+ghKl8MQy=unU2_#eFVnu3e$4Fzz X 3;ʻ)|SxlK]_ ?P^#ۚ:Vh~N>ҁ)Nvs*nas%o[ؽ0YB!BsVl| 3Ŏ1~njF{ Gf}hA6ɩ=x'0J`LZCG?= Mz7tpoUc~<2ES<;}_8##|o!B34egn;7&B!BҹW53@l` ^Bw]D Ui*zyn` o=ʾ^~O v]Jֻ:޽7|yd//OFלIwF1oK~ess{?˷K^dϿv_8d _/3ywg~| ? #|/4uts:Jv|>{gy]r0'}{/.F<׺K3|C'+/P,l\!{gc j>|S۸gk V\D@K<m70Ih=ŧyo'CdeϼH0î7_L<{/ms+imHJ|?So'O(_??0w>sEϽ6} x}K/Cw/?/> Sq}qu|;Nh>T4|MsWhI"(/Y+RAu٨PadYbYfv*ǘppHR!B!5;EF$`3խ-x.fd 5o1$6p;֯20;|M\.\ ]p8iӍ\_}L3?Q4/HHCS̒Ƽҝ(q^_< nfS8?+~G~ yoyh;`m*-G:ӏ:0ˏm?|?^ggB_9I󳨌lsZ/7vhܥ5e5pfX`2;&m,;Y'zS#P,)][x.gO.yB!BΙc 7D{5D8ZG _Fsx!gjNyvRԈ g"6ZvG~zOS씥4Cd1ʄk ne&WNҁBGZCK-|{zdn~m?ć?u;gG.V'6jixIM -a>7oڸz:OVVOz"|4z?owƗ69cyb¦u|C IV!B! t.MxKʹD|wSJh?E*hIM^ab'*LRJ1Bfi>Yx^&LRs@B3L@K/1Gb*h(Ţ8lrc7qRD6Ͻ'8ħj| O}wF~9&z#wZKOdzWO1~zCT~V:%/'V+a9Y!B!B\Lp D90'hjFB:w"Kɲiii@5l} 3P*c |Rt4ڥ$ZhLC){ՆUcs!n/wӨu'^8_&<{a8oݻȣ?sxR_PvO_U/5T^Ы˅s(QG{v+OM-m*>۹_e9B!Be{hdQM A>B(.Eņͪ@@?ވ|~(<qG3"G6wc!lhlO⡊ <ҷ)B!B"ynjXm9soŔwbcCx*(3\xQ:jXI/ :5Z o/u}2o޴B犧;͡/4lodMm-1_";{'w#Oe-4-_:8Mÿ=|aW~y;!Z0]yΪJ'x GCCV;#Uf<> ]CZ mo~ M/ln%uzkeB!BDzJ[Y O1pb&H+ dox:nw14$]䢶Kb'GXđv>}^a0bznqeG{)B<߬OSsmÓ^AZCc #kFXi3z.8q1;0' wz:Pk}5U#f3e!B!ڥ~n7BAw_ cS^Uz[xoO潻X~'pNB!Ba6bYiI^>r*(WI`L!B!r/+G":H*eNj]궿3t}<YB!B!X$8E93 >xox=8|/[' O0G?vrEv B!B)7`!n4$O~|kn/"{ !B!̱kI 柯t>B!B!V 9&B!B!YySٶs;*qA"e: ʦ7= K⑟+9!B!BqY[='ws,Z̺QSFitԋPլu% 1!B!Bq )d#˿#~2trf<оzyv8$9PVL=l.B!B!u]53N4Ǻckik-3{B!B!/81}Ij*֊ /uv 3טB!B!Ǵ8([ܗ{16%S֌%3A?B!B!WeQƂdT7ʲа2^o"5MO(< !B!B,tnȯ=IOݴ 2Hcn,¸EČ0vjI{,CB!B!X 3ǀh:φ0״ukU?Ci -'qJ9mmAbKθB!B!Ks،tCOq(W.J ] /~]!B!B%XZ3eؗ8p{heB!B!BU*%t<$PLQnw%BZ`}kK;E݌/R!B!B+ApL0rZ܍1#'x+,J0e|^IB!B!B`_: .. code-block:: bash pip install git+https://gitlab.esrf.fr/tomotools/nxtomomill.git Building doc '''''''''''' In order to build documentation make sure you have installed the 'doc' extra dependancies .. code-block:: bash pip install .[doc] Then you can build the documentation using sphinx: .. code-block:: bash sphinx-build doc html nxtomomill-v2.0.1/doc/userguide/settings.rst000066400000000000000000000013331511430602400212310ustar00rootroot00000000000000Settings ======== nxtomomill needs to get several information in order to complete translation from EDF or (bliss) HDF5 to nexus-compliant HDF5 file. This information * is a set of key values for EDF * is a set of dataset location and name for (bliss) HDF5 All are stored in nxtomomill.settings.py file. For `tomoh52nx` you can generally overwrite from calling the appropriate command option (see :ref:`Tomoh52nx`). But for scripting are when used from a dependency (like tomwer) it can be convenient to modify some of them. For example if you cannot retrieve rotation angle because this information is stored at a different location than the 'default' one knows by nxtomomill you can change `ROT_ANGLE_KEYS` parameter. nxtomomill-v2.0.1/nxtomomill/000077500000000000000000000000001511430602400163005ustar00rootroot00000000000000nxtomomill-v2.0.1/nxtomomill/__init__.py000066400000000000000000000004261511430602400204130ustar00rootroot00000000000000"""a set of applications and tools around the `NXtomo `_ format defined by the `NeXus community `_""" from .version import version as __version __version__ = __version nxtomomill-v2.0.1/nxtomomill/__main__.py000066400000000000000000000172521511430602400204010ustar00rootroot00000000000000#!/usr/bin/env python """This module describe nxtomomill applications which are available through the silx launcher. Your environment should provide a command `nxtomomill`. You can reach help with `tomwer --help`, and check the version with `nxtomomill --version`. """ import logging import sys from collections import namedtuple from silx.utils.launcher import Launcher as _Launcher import nxtomomill.version from nxtomomill.utils.io import deprecated_warning logging.basicConfig() DeprecationWarning = namedtuple( "DeprecationWarning", ["since", "reason", "replacement"] ) class Launcher(_Launcher): """ Manage launch of module. Provides an API to describe available commands and feature to display help and execute the commands. """ def __init__( self, prog=None, usage=None, description=None, epilog=None, version=None ): super().__init__( prog=prog, usage=usage, description=description, epilog=epilog, version=version, ) self._deprecations = {} "deprecations with prog names as key and deprecation info as values" def add_command( self, name=None, module_name=None, description=None, command=None, deprecated=False, deprecated_since_version=None, deprecated_reason=None, deprecated_replacement=None, ): super().add_command( name=name, module_name=module_name, description=description, command=command ) if deprecated: self._deprecations[name] = DeprecationWarning( since=deprecated_since_version, reason=deprecated_reason, replacement=deprecated_replacement, ) def execute(self, argv=None): if argv is None: argv = sys.argv if len(argv) <= 1: self.print_help() return 0 command_name = argv[1] if command_name in self._deprecations: deprecation_info = self._deprecations[command_name] deprecated_warning( type_="application", name=command_name, reason=deprecation_info.reason, replacement=deprecation_info.replacement, since_version=deprecation_info.since, ) super().execute(argv=argv) def print_help(self): """Print the help to stdout.""" usage = self.usage if usage is None: usage = "usage: {0.prog} [--version|--help] []" description = self.description epilog = self.epilog if epilog is None: epilog = "See '{0.prog} help ' to read about a specific subcommand" print(usage.format(self)) print("") if description is not None: print(description) print("") print("The {0.prog} commands are:".format(self)) commands = sorted(self._commands.keys()) for command in commands: command = self._commands[command] print(" {:15} {:s}".format(command.name, command.description)) print("") print(epilog.format(self)) def main(): """Main function of the launcher This function is referenced in the pyproject.toml file, to create a launcher script generated by setuptools. :returns: The execution status """ _version = nxtomomill.version.version launcher = Launcher(prog="nxtomomill", version=_version) launcher.add_command( "nx-copy", module_name="nxtomomill.app.nxcopy", description="Copy one or several NXtomo to another location", ) launcher.add_command( "dxfile2nx", module_name="nxtomomill.app.dxfile2nx", description="Convert dx file to NXTomo", ) launcher.add_command( "patch-nx", module_name="nxtomomill.app.patch_nx", description="allow to patch an NXTomo entry", ) launcher.add_command( "tomoedf2nx", module_name="nxtomomill.app.edf2nx", description="convert spec-edf acquisition to nexus - hdf5", deprecated=True, deprecated_reason="remove `tomo` repetition. The shorter the better", deprecated_replacement="edf2nx", deprecated_since_version=0.5, ) launcher.add_command( "edf2nx", module_name="nxtomomill.app.edf2nx", description="convert spec-edf acquisition to nexus - hdf5" "format to nx compliant file format", ) launcher.add_command( "edf2nx-check", module_name="nxtomomill.app.edf2nx_check", description="check a conversion from EDF to Nxtomo and optionally remove EDF files", ) launcher.add_command( "edf-quick-start", module_name="nxtomomill.app.edfconfig", description="create a configuration file to convert from spec-edf to NXtomo", deprecated_replacement="edf-config", deprecated_since_version=0.13, ) launcher.add_command( "edf-config", module_name="nxtomomill.app.edfconfig", description="create a configuration file to convert from spec-edf to NXtomo", ) launcher.add_command( "fluo-config", module_name="nxtomomill.app.fluoconfig", description="create a configuration file to convert from fluo-tomo to NXtomo", ) launcher.add_command( "fluo2nx", module_name="nxtomomill.app.fluo2nx", description="Converts from XRFCT data to NXtomo", ) launcher.add_command( "blissfluo-config", module_name="nxtomomill.app.blissfluoconfig", description="create a configuration file to convert from blissfluo-tomo to NXtomo", ) launcher.add_command( "blissfluo2nx", module_name="nxtomomill.app.blissfluo2nx", description="Converts from (Bliss) XRFCT data to NXtomo", ) launcher.add_command( "tomoh52nx", module_name="nxtomomill.app.h52nx", description="convert bliss hdf5 to nexus hdf5", deprecated=True, deprecated_reason="remove `tomo` repetition. The shorter the better", deprecated_replacement="h52nx", deprecated_since_version=0.5, ) launcher.add_command( "h52nx", module_name="nxtomomill.app.h52nx", description="convert bliss hdf5 to nexus hdf5", ) launcher.add_command( "z-concatenate-scans", module_name="nxtomomill.app.z_concatenate_scans", description="Build a concatenation nexus file from a z-series", ) launcher.add_command( "zstages2nxs", module_name="nxtomomill.app.zstages2nxs", description="""creates all the target NXTomos for all the stages. Possibly reducing also reference scans for obtaining flats/dark""", ) launcher.add_command( "h5-quick-start", module_name="nxtomomill.app.h5config", description="Create a default configuration file", deprecated_replacement="h5-config", deprecated_since_version=0.13, ) launcher.add_command( "h5-config", module_name="nxtomomill.app.h5config", description="Create a default configuration file", ) launcher.add_command( "split-nxfile", module_name="nxtomomill.app.split_nxfile", description="Split a file containing several NXtomo into a one file per NXtomo", ) launcher.add_command( "fscan2nx", module_name="nxtomomill.app.fscan2nx", description="convert fscan hdf5 to nexus hdf5", ) status = launcher.execute(sys.argv) return status if __name__ == "__main__": # executed when using python -m PROJECT_NAME status = main() sys.exit(status) nxtomomill-v2.0.1/nxtomomill/app/000077500000000000000000000000001511430602400170605ustar00rootroot00000000000000nxtomomill-v2.0.1/nxtomomill/app/__init__.py000066400000000000000000000000361511430602400211700ustar00rootroot00000000000000"""nxtomomill applications""" nxtomomill-v2.0.1/nxtomomill/app/blissfluo2nx.py000066400000000000000000000061221511430602400220650ustar00rootroot00000000000000# coding: utf-8 """ Application to convert a fluo-tomo dataset, after PyMCA (https://www.silx.org/doc/PyMca/dev/index.html) fit, into an hdf5/nexus file. .. program-output:: nxtomomill fluo2nx --help """ import argparse import logging from nxtomomill import converter from nxtomomill.models.blissfluo2nx import BlissFluo2nxModel from tqdm import tqdm logging.basicConfig(level=logging.INFO) def main(argv): """ """ parser = argparse.ArgumentParser( description="Converts Bliss fluo-tomo data (after PyMca fit) " "to hdf5 - nexus compliant file format." ) parser.add_argument( "ewoksfluo_filename", help="Path to the ewoksfluo-generated (h5) filename taht contains fitted XRF data.", ) parser.add_argument( "output_file", help="File produced by the converter. '.nx' extension recommended.", ) parser.add_argument( "--detectors", nargs="*", default=list(), help="Define a list of (real or virtual) detector names used for the exp (space separated values - no comma). E.g. 'falcon xmap'. If not specified, all detectors are processed.", ) parser.add_argument( "--dimension", help="2 for 2D XRFCT, 3 for 3D XRFCT. Default is 3.", default=3, ) parser.add_argument( "--config", "--configuration-file", "--configuration", default=None, help="file containing the full configuration to convert from (PyMCA-computed) fluo projections to nexus. " "Default configuration file can be created from `nxtomomill fluo-config` command", ) parser.add_argument( "--overwrite", default=False, action="store_true", help="If the output file exists then overwrite.", ) options = parser.parse_args(argv[1:]) config = BlissFluo2nxModel() if options.config is not None: config = config.from_cfg_file(options.config) check_input = { "ewoksfluo_filename": ( options.ewoksfluo_filename, config.general_section.ewoksfluo_filename, ), "output file": (options.output_file, config.general_section.output_file), } for input_name, (opt_value, config_value) in check_input.items(): if ( opt_value is not None and config_value is not None and opt_value != config_value ): raise ValueError( f"two values for {input_name} are provided from arguments and configuration file ({opt_value, config_value})" ) if options.ewoksfluo_filename is not None: config.general_section.ewoksfluo_filename = options.ewoksfluo_filename if options.output_file is not None: config.general_section.output_file = options.output_file if options.detectors is not None: config.general_section.detector_names = options.detectors config.general_section.dimension = options.dimension config.general_section.overwrite = options.overwrite converter.from_blissfluo_to_nx( configuration=config, progress=tqdm("blissfluo2nx"), ) nxtomomill-v2.0.1/nxtomomill/app/blissfluoconfig.py000066400000000000000000000015571511430602400226320ustar00rootroot00000000000000# coding: utf-8 """ Application to create a default configuration file to be used by blissfluo2nx application. .. program-output:: nxtomomill blissfluo-config --help """ import argparse import logging from nxtomomill.models.blissfluo2nx import BlissFluo2nxModel logging.basicConfig(level=logging.INFO) _logger = logging.getLogger(__name__) def main(argv): """ """ parser = argparse.ArgumentParser(description="Create a default configuration file") parser.add_argument("output_file", help="output .cfg file") parser.add_argument( "--level", "--option-level", help="Level of options to embed in the configuration file. Can be 'required' or 'advanced'.", default="required", ) options = parser.parse_args(argv[1:]) configuration = BlissFluo2nxModel() configuration.to_cfg_file(file_path=options.output_file) nxtomomill-v2.0.1/nxtomomill/app/dxfile2nx.py000066400000000000000000000124411511430602400213370ustar00rootroot00000000000000# coding: utf-8 """ Application to convert from dx file (HDF5) to NXTomo (HDF5) file .. program-output:: nxtomomill dxfile2nx --help For a complete tutorial you can have a look at :ref:`dxfile2nxtutorial` """ import argparse import logging import pint from nxtomomill import utils from nxtomomill.converter.dxfile.dxfileconverter import from_dx_to_nx _logger = logging.getLogger(__name__) _ureg = pint.get_application_registry() def convert_2elmts__tuple_to_float(input_str) -> tuple: if input_str == (None, None): return input_str try: tmp_str = input_str.replace(" ", "") tmp_str = tmp_str.replace(";", ",") if tmp_str.startswith("("): tmp_str = tmp_str[1:] if tmp_str.startswith(")"): tmp_str = tmp_str[:-1] v1, v2 = tmp_str.split(",") except Exception as e: raise ValueError( f"Unable to convert {input_str} to a tuple of two float. Reason is {e}" ) else: return float(v1), float(v2) def _get_pixel_size(input_str) -> tuple: try: values = convert_2elmts__tuple_to_float(input_str) except ValueError: try: value = float(input_str) except Exception: raise ValueError( f"Unable to convert {input_str} to pixel size." f"Should be provided as a tuple (sample x_pixel_size, sample y_pixel_size) " f"or as a single value pixel_size" ) else: return value, value else: return values def main(argv): """ """ parser = argparse.ArgumentParser( description="convert acquisition contained in provided dx file to " "NXTomo entry" ) parser.add_argument("input_file", help="master file of the " "acquisition") parser.add_argument("output_file", help="output .nx or .h5 file", default=None) # usually it looks like the dxfile contains only one acquisition at the # root level parser.add_argument( "--input_entry", help="h5py group path to be converted", default="/", ) parser.add_argument( "--output_entry", help="h5py group path to store the NXTomo", default="entry0000", ) parser.add_argument( "--file_extension", default=".nx", help="extension of the output file. Valid values are " "" + "/".join([item.value for item in utils.FileExtension]), ) # as scan range or rotation angle are not stored in dxfile we # provide the default value which is the "standard" case parser.add_argument( "--scan-range", default="0,180", help="scan range of the projections. Dark and flat will always be " "considered with rotation angle == scan_range[0]", ) # as pixel sizes are not stored in the dxfile and requested for paganin # we provide some default value parser.add_argument( "--pixel-size", default=(None, None), help="pixel size in meter as (x pixel size, y pixel size) or as a single value", ) parser.add_argument( "--fov", "--field-of-view", default=None, help="field of view. Can be Full or Half", dest="field_of_view", ) # as distance is not stored in the dxfile and requested for paganin # we provide some default value parser.add_argument( "--distance", "--detector-sample-distance", default=None, help="sample to detector distance (in meter)", dest="distance", ) parser.add_argument( "--energy", "--incident-beam-energy", default=None, help="incident beam energy in keV", dest="energy", ) parser.add_argument( "--overwrite", help="Do not ask for user permission to overwrite output files", action="store_true", default=False, ) parser.add_argument( "--data-copy", help="Force data duplication of frames. This will permit to have an " "'all-embed' file. Otherwise we will have link between the dxfile " "and the NXTomo.", action="store_true", dest="duplicate_data", default=False, ) options = parser.parse_args(argv[1:]) distance = options.distance if distance is not None: distance = float(options.distance) energy = options.energy if energy is not None: energy = float(options.energy) * _ureg.keV if options.duplicate_data is False: _logger.warning( "Generated file will contain relative links to the " "(dxfile) source file. You should insure the " "relative position of the input file and the output " "stay constant to protect links. (use --copy-data) " "option to avoid links" ) from_dx_to_nx( input_file=options.input_file, output_file=options.output_file, file_extension=options.file_extension, input_entry=options.input_entry, output_entry=options.output_entry, scan_range=convert_2elmts__tuple_to_float(options.scan_range), pixel_size=_get_pixel_size(options.pixel_size), field_of_view=options.field_of_view, distance=distance, overwrite=options.overwrite, duplicate_data=options.duplicate_data, energy=energy, ) nxtomomill-v2.0.1/nxtomomill/app/edf2nx.py000066400000000000000000000103251511430602400206210ustar00rootroot00000000000000# coding: utf-8 """ Application to convert a tomo dataset written in edf into and hdf5/nexus file. .. program-output:: nxtomomill edf2nx --help For a complete tutorial you can have a look at :ref:`edf2nxtutorial` """ import argparse import logging from nxtomomill import converter from nxtomomill.models.utils import convert_str_to_tuple from nxtomomill.io.config.edfconfig import TomoEDFConfig from nxtomomill.settings import Tomo from tqdm import tqdm logging.basicConfig(level=logging.INFO) fabio_logger = logging.getLogger("fabio.edfimage") fabio_logger.setLevel(logging.ERROR) def main(argv): """ """ parser = argparse.ArgumentParser( description="convert data acquired as " "edf to hdf5 - nexus " "compliant file format." ) parser.add_argument("scan_path", help="folder containing the edf files", nargs="?") parser.add_argument("output_file", help="foutput .h5 file", nargs="?") parser.add_argument( "--dataset-basename", "--file-prefix", default=None, help="file prefix to be used to deduce projections", ) parser.add_argument( "--info-file", default=None, help=".info file containing acquisition information (ScanRange, Energy, TOMO_N...)", ) parser.add_argument( "--config", "--configuration-file", "--configuration", default=None, help="file containing the full configuration to convert from SPEC-EDF to bliss to nexus. " "Default configuration file can be created from `nxtomomill edf-config` command", ) parser.add_argument( "--delete-edf", "--delete-edf-source", "--delete-edf-source-files", default=False, action="store_true", help="if you are duplicating data (default behavior) you can ask to delete used edf files to free space", ) parser.add_argument( "--output-checks", default=tuple(), help="Define list of check to be run once the conversion is finished (raise an error if check fails). This is done before removing edf if asked. So if check fails source edf files won't be removed", ) parser.add_argument( "--use-existing-angles", "--no-angle-recalculcation", default=None, action="store_true", help=f"By default the angle will be recomputed to have equally spaced angles from the min and the max angles. If this option is set then angles defined in edf headers ({','.join(Tomo.EDF.ROT_ANGLE)}) will be used.", ) options = parser.parse_args(argv[1:]) config = TomoEDFConfig() if options.config is not None: config = config.from_cfg_file(options.config) check_input = { "dataset basename": (options.dataset_basename, config.dataset_basename), "scan path": (options.scan_path, config.input_folder), "output file": (options.output_file, config.output_file), "info file": (options.info_file, config.dataset_info_file), } for input_name, (opt_value, config_value) in check_input.items(): if ( opt_value is not None and config_value is not None and opt_value != config_value ): raise ValueError( f"two values for {input_name} are provided from arguments and configuration file ({opt_value, config_value})" ) if options.dataset_basename is not None: config.dataset_basename = options.dataset_basename if options.info_file is not None: config.dataset_info_file = options.info_file if options.scan_path is not None: config.input_folder = options.scan_path if options.output_file is not None: config.output_file = options.output_file config.delete_edf_source_files = options.delete_edf if options.use_existing_angles is not None: config.force_angle_calculation = not options.use_existing_angles config.output_checks = convert_str_to_tuple(options.output_checks) assert isinstance( config.output_checks, tuple ), f"config.output_checks is expected to be a tuple. Gets {type(config.output_checks)}" converter.from_edf_to_nx( configuration=config, progress=tqdm(desc="edf2nx", bar_format="{l_bar}{bar}{postfix}"), ) nxtomomill-v2.0.1/nxtomomill/app/edf2nx_check.py000066400000000000000000000074671511430602400217730ustar00rootroot00000000000000""" Application to check the conversion done of an EDF folder to a .nx file. .. program-output:: nxtomomill edf2nx-check --help """ import argparse import logging from nxtomomill.converter.edf.edfconverter import post_processing_check from nxtomomill.models.utils import convert_str_to_tuple from nxtomomill.io.config.edfconfig import TomoEDFConfig from nxtomomill.converter.edf.checks import OUPUT_CHECK logging.basicConfig(level=logging.INFO) def main(argv): """ check a conversion made. To keep it coherent and safe we must keep the same inputs as for the EDF to NXtomo conversion. The only difference is that this one has some output check defined by default """ parser = argparse.ArgumentParser( description="check a conversion from edf to NXtomo went's well" ) parser.add_argument("scan_path", help="folder containing the edf files", nargs="?") parser.add_argument("output_file", help="foutput .h5 file", nargs="?") parser.add_argument( "--dataset-basename", "--file-prefix", default=None, help="file prefix to be used to deduce projections", ) parser.add_argument( "--info-file", default=None, help=".info file containing acquisition information (ScanRange, Energy, TOMO_N...)", ) parser.add_argument( "--config", "--configuration-file", "--configuration", default=None, help="file containing the full configuration to convert from SPEC-EDF to bliss to nexus. " "Default configuration file can be created from `nxtomomill edf-config` command", ) parser.add_argument( "--delete-edf", "--delete-edf-source", "--delete-edf-source-files", default=False, action="store_true", help="if you are duplicating data (default behavior) you can ask to delete used edf files to free space", ) parser.add_argument( "--output-checks", default=(OUPUT_CHECK.COMPARE_VOLUME,), help="Define list of check to be run once the conversion is finished (raise an error if check fails). This is done before removing edf if asked. So if check fails source edf files won't be removed", ) options = parser.parse_args(argv[1:]) config = TomoEDFConfig() if options.config is not None: config = config.from_cfg_file(options.config) check_input = { "dataset basename": (options.dataset_basename, config.dataset_basename), "scan path": (options.scan_path, config.input_folder), "output file": (options.output_file, config.output_file), "info file": (options.info_file, config.dataset_info_file), } for input_name, (opt_value, config_value) in check_input.items(): if ( opt_value is not None and config_value is not None and opt_value != config_value ): raise ValueError( f"two values for {input_name} are provided from arguments and configuration file ({opt_value, config_value})" ) if options.dataset_basename is not None: config.dataset_basename = options.dataset_basename if options.info_file is not None: config.dataset_info_file = options.info_file if options.scan_path is not None: config.input_folder = options.scan_path if options.output_file is not None: config.output_file = options.output_file config.delete_edf_source_files = options.delete_edf config.output_checks = convert_str_to_tuple(options.output_checks) if not len(config.output_checks) > 0: raise ValueError("not check on conversion selected") assert isinstance( config.output_checks, tuple ), f"config.output_checks is expected to be a tuple. Gets {type(config.output_checks)}" post_processing_check( configuration=config, ) nxtomomill-v2.0.1/nxtomomill/app/edfconfig.py000066400000000000000000000017751511430602400213700ustar00rootroot00000000000000# coding: utf-8 """ Application to create a default configuration file to be used by edf2nx application. .. program-output:: nxtomomill edf-config --help For a complete tutorial you can have a look at :ref:`edf2nxtutorial` """ import argparse import logging from nxtomomill.io import TomoEDFConfig logging.basicConfig(level=logging.INFO) _logger = logging.getLogger(__name__) def main(argv): """ """ parser = argparse.ArgumentParser(description="Create a default configuration file") parser.add_argument("output_file", help="output .cfg file") parser.add_argument( "--level", "--option-level", help="Level of options to embed in the configuration file. Can be 'required' or 'advanced'.", default="required", ) options = parser.parse_args(argv[1:]) if options.level != "required": _logger.warning("Level option has been removed. Will be ignored") configuration = TomoEDFConfig() configuration.to_cfg_file(file_path=options.output_file) nxtomomill-v2.0.1/nxtomomill/app/fluo2nx.py000066400000000000000000000106061511430602400210320ustar00rootroot00000000000000# coding: utf-8 """ Application to convert a fluo-tomo dataset, after PyMCA (https://www.silx.org/doc/PyMca/dev/index.html) fit, into an hdf5/nexus file. .. program-output:: nxtomomill fluo2nx --help """ import argparse import logging import os from nxtomomill import utils from nxtomomill import converter from nxtomomill.io.config.fluoconfig import TomoFluoConfig from tqdm import tqdm logging.basicConfig(level=logging.INFO) def main(argv): """ """ parser = argparse.ArgumentParser( description="Converts fluo-tomo data (after PyMca fit) " "to hdf5 - nexus compliant file format." ) parser.add_argument( "scan_path", help="Path to the folder containing the raw data folder and the fluofit/ subfolder.", nargs=1, ) parser.add_argument( "output_file", help="File produced by the converter. '.nx' extension recommended.", nargs=1, ) parser.add_argument( "dataset_basename", help="In 2D, the exact full name of the folder. In 3D, the folder name prefix (the program will search for folders named _projection_XXX where XXX is a nmber.)", nargs=1, ) parser.add_argument( "--detectors", nargs="*", default=list(), help="Define a list of (real or virtual) detector names used for the exp (space separated values - no comma). E.g. 'falcon xmap'. If not specified, all detectors are processed.", ) parser.add_argument( "--dimension", help="2 for 2D XRFCT, 3 for 3D XRFCT. Default is 3.", default=3, ) parser.add_argument( "--info-file", default=None, help=".info file containing acquisition information (ScanRange, Energy, TOMO_N...)", ) parser.add_argument( "--config", "--configuration-file", "--configuration", default=None, help="file containing the full configuration to convert from (PyMCA-computed) fluo projections to nexus. " "Default configuration file can be created from `nxtomomill fluo-config` command", ) parser.add_argument( "--mode", default="newfile", help="What to do if file exists:" "- 'newfile': program will fail if file already exists." "- 'overwrite': program will overwrite existing file or existing entries in file.", ) options = parser.parse_args(argv[1:]) config = TomoFluoConfig() if options.config is not None: config = config.from_cfg_file(options.config) check_input = { "dataset basename": (options.dataset_basename, config.dataset_basename), "scan path": (options.scan_path, config.input_folder), "output file": (options.output_file, config.output_file), "info file": (options.info_file, config.dataset_info_file), } for input_name, (opt_value, config_value) in check_input.items(): if ( opt_value is not None and config_value is not None and opt_value != config_value ): raise ValueError( f"two values for {input_name} are provided from arguments and configuration file ({opt_value, config_value})" ) if options.dataset_basename is not None: config.dataset_basename = options.dataset_basename[0] if options.info_file is not None: config.dataset_info_file = options.info_file if options.scan_path is not None: config.input_folder = options.scan_path[0] if options.output_file is not None: config.output_file = options.output_file[0] if options.detectors is not None: config.detector_names = options.detectors config.dimension = options.dimension fileout_h5 = utils.get_file_name( file_name=config.output_file, extension=config.file_extension, check=True, ) if os.path.exists(fileout_h5): if options.mode == "newfile": raise FileExistsError( f"The file {fileout_h5} already exists. Please remove it before running the program." ) elif options.mode == "overwrite": config.overwrite = True else: raise RuntimeError( f"The value entered for --mode is not valid. Should be either 'newfile' or 'overwrite'. {options.mode} was given." ) converter.from_fluo_to_nx( configuration=config, progress=tqdm("fluo2nx"), ) nxtomomill-v2.0.1/nxtomomill/app/fluoconfig.py000066400000000000000000000016641511430602400215740ustar00rootroot00000000000000# coding: utf-8 """ Application to create a default configuration file to be used by fluo2nx application. .. program-output:: nxtomomill fluo-config --help """ import argparse import logging from nxtomomill.io import TomoFluoConfig logging.basicConfig(level=logging.INFO) _logger = logging.getLogger(__name__) def main(argv): """ """ parser = argparse.ArgumentParser(description="Create a default configuration file") parser.add_argument("output_file", help="output .cfg file") parser.add_argument( "--level", "--option-level", help="Level of options to embed in the configuration file. Can be 'required' or 'advanced'.", default=None, ) options = parser.parse_args(argv[1:]) if options.level is not None: _logger.warning("level option has been removed. Will be ignored") configuration = TomoFluoConfig() configuration.to_cfg_file(file_path=options.output_file) nxtomomill-v2.0.1/nxtomomill/app/fscan2nx.py000066400000000000000000000044741511430602400211650ustar00rootroot00000000000000import sys import logging import argparse from nxtomomill.converter.fscan.fscanconverter import from_fscan_to_nx _logger = logging.getLogger(__name__) def main(argv): parser = argparse.ArgumentParser( description="convert fscan dataset to nexus format. " ) parser.add_argument( "input_file", help="Path to the master file of the acquisition, containing entries '1.1', '2.1', etc", default=None, nargs="?", ) parser.add_argument( "output_file", help="Path to the output NX file", default=None, nargs="?", ) parser.add_argument( "--halftomo", help="Whether to enable extended-field-of-view mode (half-tomography). Default is False.", dest="halftomo", action="store_true", default=False, ) parser.add_argument( "--ignore_last_n_projections", "--ignore-last-n-projections", default=0, help="Number of projections to ignore in the end of each series of projections", ) parser.add_argument( "--detector-name", "--detector_name", default="pcoedgehs", help="Detector name. Default is pcoedgehs.", ) parser.add_argument( "--rot_motor_name", "--rot-motor-name", default="hrrz", help="Rotation motor name. Default is hrrz.", ) parser.add_argument("--energy", default=None, help="Incident beam energy in keV") parser.add_argument( "--distance", default=None, help="Sample-detector distance in meters" ) options = parser.parse_args(argv[1:]) if options.input_file is None or options.output_file is None: _logger.error("Need input file and output file. Type --help to see options") sys.exit(0) energy = options.energy distance = options.distance if energy is not None: energy = float(energy) if distance is not None: distance = float(distance) from_fscan_to_nx( options.input_file, options.output_file, detector_name=options.detector_name, rotation_motor_name=options.rot_motor_name, halftomo=options.halftomo, ignore_last_n_projections=options.ignore_last_n_projections, energy=energy, distance=distance, ) if __name__ == "__main__": main(sys.argv) nxtomomill-v2.0.1/nxtomomill/app/h52nx.py000066400000000000000000000235131511430602400204020ustar00rootroot00000000000000# coding: utf-8 """ Application to convert a bliss-hdf5 tomography dataset to Nexus - NXtomo (hdf5) format .. program-output:: nxtomomill h52nx --help For a complete tutorial you can have a look at: :ref:`Tomoh52nx` """ import argparse import logging from tqdm import tqdm from nxtomomill import utils from nxtomomill.converter import from_h5_to_nx from nxtomomill.io.config.confighandler import ( SETTABLE_PARAMETERS_UNITS, TomoHDF5ConfigHandler, ) logging.basicConfig(level=logging.INFO) _logger = logging.getLogger(__name__) def _getPossibleInputParams(): """ :return: string with param1 (expected unit) ... """ res = [] for key, value in SETTABLE_PARAMETERS_UNITS.items(): res.append(f"{key} ({value})") return ", ".join(res) def _ask_for_selecting_detector(det_grps: list[str] | tuple[str]) -> str | None: res = input( "Several detector found. Only one detector is managed at the " "time. Please enter the name of the detector you want to use " f"or 'Cancel' to stop translation ({det_grps})" ) if res == "Cancel": return None elif res in det_grps: return res else: # warning: this is not a debug log !!! print("detector name not recognized.") return _ask_for_selecting_detector(det_grps) def main(argv): """ """ parser = argparse.ArgumentParser( description="convert data acquired as " "hdf5 from bliss to nexus " "`NXtomo` classes. For `zseries` it will create one entry per `z`" ) parser.add_argument( "input_file", help="master file of the " "acquisition", default=None, nargs="?" ) parser.add_argument( "output_file", help="output .nx or .h5 file", default=None, nargs="?" ) parser.add_argument( "--file-extension", "--file_extension", default=None, help="extension of the output file. Valid values are " "" + "/".join([item.value for item in utils.FileExtension]), ) parser.add_argument( "--single-file", help="merge all scan sequence to the same output file. " "By default create one file per sequence and " "group all sequence in the output file", dest="single_file", action="store_true", default=None, # default is None and not False for the ConfigHandler so it can knows when set by the user or not ) parser.add_argument( "--with-master-file", help="Creates a master file linking all .nx files", dest="no_master_file", action="store_false", default=None, ) parser.add_argument( "--overwrite", help="Do not ask for user permission to overwrite output files", action="store_true", default=None, # default is None and not False for the ConfigHandler so it can knows when set by the user or not ) parser.add_argument( "--debug", help="Set logs to debug mode", action="store_true", default=None, # default is None and not False for the ConfigHandler so it can knows when set by the user or not ) parser.add_argument( "--entries", help="Specify (root) entries to be converted. By default it will try " "to convert all existing entries.", default=None, ) parser.add_argument( "--sub-entries-to-ignore", "--ignore-sub-entries", help="Specify (none-root) sub entries to ignore.", default=None, ) parser.add_argument( "--raises-error", help="Raise errors if some data are not met instead of providing some" " default values", action="store_true", default=None, # default is None and not False for the ConfigHandler so it can knows when set by the user or not ) parser.add_argument( "--field-of-view", help="Force the output to be `Half` or `Full` acquisition. Otherwise " "parse raw data to find this information.", default=None, ) parser.add_argument( "--no-input", "--no-input-for-missing-information", help="The user won't be ask for any inputs", dest="request_input", action="store_false", default=None, # default is None and not False for the ConfigHandler so it can knows when set by the user or not ) parser.add_argument( "--data-copy", "--copy-data", help="Force data duplication of frames. This will permit to have an " "'all-embed' file. Otherwise the detector/data dataset will haves " "links to other files.", action="store_true", dest="default_data_copy", default=None, # default is None and not False for the ConfigHandler so it can knows when set by the user or not ) parser.add_argument( "--sample_x_keys", "--sample-x-keys", default=None, help="x translation key in bliss HDF5 file", ) parser.add_argument( "--sample_y_keys", "--sample-y-keys", default=None, help="y translation key in bliss HDF5 file", ) parser.add_argument( "--translation_z_keys", "--translation-z-keys", default=None, help="z translation key in bliss HDF5 file", ) parser.add_argument( "--sample-detector-distance-keys", "--sample-detector-distance-paths", "--distance-paths", default=None, help="sample detector distance paths", ) parser.add_argument( "--valid_camera_names", "--valid-camera-names", default=None, help="Valid NXDetector dataset name to be considered. Otherwise will" "try to deduce them from NX_class attibute (value should be" "NXdetector) or from instrument group child structure.", ) parser.add_argument( "--rotation_angle_keys", "--rotation-angle-keys", "--rot-angle-keys", "--rot_angle_keys", default=None, help="Valid dataset name for rotation angle", ) parser.add_argument( "--exposure_time_keys", "--exposure-time-keys", "--acq_expo_time_keys", "--acq-expo-time-keys", default=None, help="Valid dataset name for acquisition exposure time", ) parser.add_argument( "--sample-x-pixel-size_keys", "--sample_x_pixel_size_keys", default=None, help="X pixel size key to read", ) parser.add_argument( "--sample-y-pixel-size_keys", "--sample_y_pixel_size_keys", default=None, help="Y pixel size key to read", ) # scan titles parser.add_argument( "--init_titles", "--init-titles", default=None, help="Titles corresponding to init scans", ) parser.add_argument( "--zseries-init-titles", "--zseries_init_titles", "--z_series_init_titles", "--z-series_init_titles", "--init_zserie_titles", "--init-zserie-titles", default=None, help="Titles corresponding to zserie init scans", ) parser.add_argument( "--multitomo-init-titles", "--multitomo_init_titles", "--multi-tomo-init-titles", "--multi_tomo_init_titles", "--init-multi-tomo-titles", "--init-pcotomo-titles", default=None, help="Titles corresponding to multi-tomo init scans", ) parser.add_argument( "--back-and-forth-init-titles", "--back_and_forth_init_titles", "--init-back-and-forth-titles", "--init_back_and_forth_titles", default=None, help="Titles corresponding to back-and-forth init scans", ) parser.add_argument( "--dark-titles", "--dark_titles", default=None, help="Titles corresponding to dark scans", ) parser.add_argument( "--flat-titles", "--flat_titles", "--ref_titles", "--ref-titles", default=None, help="Titles corresponding to ref scans", dest="flat_titles", ) parser.add_argument( "--projection_titles", "--proj_titles", "--proj-titles", default=None, help="Titles corresponding to projection scans", ) parser.add_argument( "--alignment-titles", "--alignment_titles", "--align_titles", "--align-titles", default=None, help="Titles corresponding to alignment scans", ) parser.add_argument( "--set-params", default=None, nargs="*", help="Allow manual definition of some parameters. " "Valid parameters (and expected input unit) " f"are: {_getPossibleInputParams()}. Should be added at the end of the command line because " "will try to cover all text set after this option.", ) # config file parser.add_argument( "--config-file", "--config", "--configuration", "--configuration-file", default=None, help="file containing the full configuration to convert from h5 " "bliss to nexus", ) options = parser.parse_args(argv[1:]) if options.request_input: callback_det_sel = _ask_for_selecting_detector else: callback_det_sel = None try: configuration_handler = TomoHDF5ConfigHandler(options) except Exception: _logger.error("Fail to initiate the configuration.", exc_info=True) return else: for title in configuration_handler.configuration.init_titles: assert title != "" logging.getLogger("nxtomomill").setLevel( configuration_handler.configuration.log_level ) from_h5_to_nx( configuration=configuration_handler.configuration, progress=tqdm(desc="h52nx", delay=1, bar_format="{l_bar}{bar}"), input_callback=None, detector_sel_callback=callback_det_sel, ) nxtomomill-v2.0.1/nxtomomill/app/h5config.py000066400000000000000000000027671511430602400211500ustar00rootroot00000000000000# coding: utf-8 """ Application to create a default configuration file to be used by h52nx application. .. program-output:: nxtomomill h5-config --help For a complete tutorial you can have a look at: :ref:`Tomoh52nx` """ import argparse import logging from nxtomomill.io import TomoHDF5Config logging.basicConfig(level=logging.INFO) def main(argv): """ """ parser = argparse.ArgumentParser(description="Create a default configuration file") parser.add_argument("output_file", help="output .cfg file") parser.add_argument( "--from-title-names", help="Provide minimalistic configuration to make a conversion from " "titles names. (FRAME TYPE section is ignored). \n" "Exclusive with `from-scan-urls` option", action="store_true", default=False, ) parser.add_argument( "--from-scan-urls", help="Provide minimalistic configuration to make a conversion from " "scan urls. (ENTRIES and TITLES section is ignored).\n" "Exclusive with `from-title-names` option", action="store_true", default=False, ) options = parser.parse_args(argv[1:]) if options.from_title_names: filter_sections = ("frame_type_section",) elif options.from_scan_urls: filter_sections = ("entries_and_titles_section",) else: filter_sections = () configuration = TomoHDF5Config() configuration.to_cfg_file( file_path=options.output_file, filter_sections=filter_sections ) nxtomomill-v2.0.1/nxtomomill/app/nxcopy.py000066400000000000000000000046241511430602400207600ustar00rootroot00000000000000"""application to copy NXtomo(s) from one file to another""" import argparse import os from nxtomo.application.nxtomo import copy_nxtomo_file as copy_nxtomo from nxtomomill.models.utils import convert_str_to_tuple def main(argv): """ """ parser = argparse.ArgumentParser( description="copy one or several NXtomo to another location" ) parser.add_argument( "nexus_file", help="file path to the nexus file containing NXtomo", nargs="?" ) parser.add_argument("output_file", help="output nexus file", nargs="?") parser.add_argument( "--entry", "--entries", default=None, help="NXtomo path(s) to be copied. If none provided then all NXtomo entries will be copied", dest="entries", ) parser.add_argument( "--overwrite", help="Do not ask for user permission to overwrite output files", action="store_true", default=False, ) parser.add_argument( "--debug", help="Set logs to debug mode", action="store_true", default=False, ) parser.add_argument( "--remove-vds", help="Remove any Virtual dataset to the resulting NXtomo (duplicate detector data - warning: all data will be load in memory before dumping it)", action="store_true", default=False, ) options = parser.parse_args(argv[1:]) copy_nxtomo( input_file=options.nexus_file, output_file=get_output_file(options.output_file, options.nexus_file), entries=( convert_str_to_tuple(options.entries) if options.entries is not None else None ), overwrite=options.overwrite, vds_resolution="remove" if options.remove_vds else "update", ) def get_output_file(output_file_or_folder, input_file) -> str: """compute output file for copying NXtomo from input file or folder""" def get_output_file_from_folder(): return os.path.join( output_file_or_folder, os.path.basename(os.path.abspath(input_file)) ) if os.path.isdir(output_file_or_folder): output_file = get_output_file_from_folder() elif ( os.path.isfile(output_file_or_folder) or os.path.splitext(output_file_or_folder)[-1] != "" ): output_file = output_file_or_folder else: output_file = get_output_file_from_folder() return os.path.abspath(output_file) nxtomomill-v2.0.1/nxtomomill/app/patch_nx.py000066400000000000000000000230271511430602400212420ustar00rootroot00000000000000# coding: utf-8 """ Application to patch a NXtomo entry. invalidating some frame or adding some. .. program-output:: patch-nx --help """ import argparse import logging import os from silx.io.url import DataUrl from silx.utils.enum import Enum as _Enum from tomoscan.esrf.scan.nxtomoscan import NXtomoScan from nxtomomill import utils from nxtomo.nxobject.nxdetector import ImageKey logging.basicConfig(level=logging.DEBUG) _logger = logging.getLogger(__name__) _SILX_DATA_URL = "www.silx.org/doc/silx/latest/modules/io/url.html?highlight=dataurl#silx.io.url.DataUrl" _INFO_URL = ( 'url should be providing the "default?silx way": ' "silx:///data/image.edf?path=/scan_0/detector/data " f"(see {_SILX_DATA_URL})" "of just by giving dataset_path@file_path" ) class _ImageKeyName(_Enum): ALIGNMENT = "alignment" PROJECTION = "projection" FLAT_FIELD = "flat" DARK_FIELD = "dark" INVALID = "invalid" @staticmethod def to_image_key(image_key) -> ImageKey: image_key = _ImageKeyName(image_key.lower()) if image_key is _ImageKeyName.ALIGNMENT: return ImageKey.ALIGNMENT elif image_key is _ImageKeyName.PROJECTION: return ImageKey.PROJECTION elif image_key is _ImageKeyName.DARK_FIELD: return ImageKey.DARK_FIELD elif image_key is _ImageKeyName.FLAT_FIELD: return ImageKey.FLAT_FIELD elif image_key is _ImageKeyName.INVALID: return ImageKey.INVALID else: raise ValueError(f"{image_key} not handled") _INFO_FRAME_INPUT = ( "Frames can be provided three ways: \n" "- as a list: frame_index_1,frame_index_2\n" "- as a python slice: from:to:step\n" f"- as an image key value. Valid values are {[item.value for item in _ImageKeyName]}\n" ) def _extract_data_url(url_as_a_str): """ Extract url from a string """ if url_as_a_str is None: return None elif "@" in url_as_a_str: try: entry, file_path = url_as_a_str.split("@") except Exception: _logger.error(f"Fail to create an url from {url_as_a_str}. {_INFO_URL}") return None else: url = DataUrl(file_path=file_path, data_path=entry, scheme="silx") return url else: try: url = DataUrl(path=url_as_a_str) except Exception as e: _logger.error( f"Fail to create an url from {url_as_a_str}." f"Reason is {e}. For more information see {_SILX_DATA_URL}" ) return None else: return url def _get_slice_to_modify(slice_as_str, master_file, entry): """ Return a list of int or a `slice` from slice_as_str :param slice_as_str: :return: slice to be modify on the image_key and image_key_control dataset """ if slice_as_str is None: return None elif slice_as_str.lower() in [item.value for item in _ImageKeyName]: image_key = _ImageKeyName.to_image_key(slice_as_str) scan = NXtomoScan(master_file, entry) frames = scan.frames slices = [] for frame in frames: if frame.image_key is image_key: slices.append(frame.index) return slices elif ":" in slice_as_str: elmts = slice_as_str.split(":") def get_value(index): if index >= len(elmts): return None elif elmts[index] == "": return None else: return int(elmts[index]) from_ = get_value(0) to_ = get_value(1) step = get_value(2) return slice(from_, to_, step) else: return [int(index) for index in slice_as_str.split(",")] def main(argv): """ """ parser = argparse.ArgumentParser( description="Insert dark and / or flat frames and metadata into an" "existing NXTomo file from url(s)." ) parser.add_argument("file_path", help="NXTomo file to patch") parser.add_argument("entry", help="entry in the provided file") # dark and flat options parser.add_argument( "--darks-at-start", "--darks-start", default=None, help="url to the dataset containing darks to be store at" "the beginning. " + _INFO_URL, ) parser.add_argument( "--darks-at-end", "--darks-end", default=None, help="url to the dataset containing darks to be store at" "the end." + _INFO_URL, ) parser.add_argument( "--flats-at-start", "--flats-start", default=None, help="url to the dataset containing flats to be store at" "the beginning of the acquisition sequence (made before " "projections acquisition). " + _INFO_URL, ) parser.add_argument( "--flats-at-end", "--flats-end", default=None, help="url to the dataset containing flats to be store at" "the beginning of end of the sequence (made before " "projections acquisition). " + _INFO_URL, ) # modify frame type option parser.add_argument( "--invalid-frames", default=None, help="Define the set of frames to be mark as invalid. " + _INFO_FRAME_INPUT, ) parser.add_argument( "--update-to-projection", "--update-to-proj", default=None, help="Define the set of frames to be mark as projection. " "" + _INFO_FRAME_INPUT, ) parser.add_argument( "--update-to-dark", default=None, help="Define the set of frames to be mark as dark. " + _INFO_FRAME_INPUT, ) parser.add_argument( "--update-to-flat", default=None, help="Define the set of frames to be mark as flat. " + _INFO_FRAME_INPUT, ) parser.add_argument( "--update-to-alignment", default=None, help="Define the set of frames to be mark as alignment. " "" + _INFO_FRAME_INPUT, ) parser.add_argument( "--embed-data", default=False, action="store_true", help="Embed data from url in the file if not already inside", ) options = parser.parse_args(argv[1:]) # get information for adding dark / flat darks_start_url = _extract_data_url(options.darks_at_start) darks_end_url = _extract_data_url(options.darks_at_end) flat_start_url = _extract_data_url(options.flats_at_start) flat_end_url = _extract_data_url(options.flats_at_end) patch_det_data = darks_start_url or flat_start_url or flat_end_url or darks_end_url # get information for modifying image_key slice_to_update_to_dark = _get_slice_to_modify( options.update_to_dark, master_file=options.file_path, entry=options.entry ) slice_to_update_to_flat = _get_slice_to_modify( options.update_to_flat, master_file=options.file_path, entry=options.entry ) slice_to_update_to_projection = _get_slice_to_modify( options.update_to_projection, master_file=options.file_path, entry=options.entry ) slice_to_update_to_alignment = _get_slice_to_modify( options.update_to_alignment, master_file=options.file_path, entry=options.entry ) slice_to_invalid = _get_slice_to_modify( options.invalid_frames, master_file=options.file_path, entry=options.entry ) patch_image_key = ( slice_to_update_to_dark or slice_to_update_to_flat or slice_to_update_to_projection or slice_to_update_to_alignment or slice_to_invalid ) if patch_det_data and patch_image_key: _logger.info( "Both adding dark / flat and modifying `image_key` / " "`image_key_control` are requested. Will first add " "dark / flat then modify `image_key` / " "`image_key_control`." ) elif not (patch_det_data or patch_image_key): _logger.warning( "No url provided for dark or flats or frame type to " "modify. Nothing to be done." ) elif not utils.is_nx_tomo_entry(file_path=options.file_path, entry=options.entry): _logger.error( f"{options.entry}@{options.file_path} is not recognized as a valid NXTomo entry." ) elif not os.access(options.file_path, os.W_OK): _logger.error(f"You don't have rights to write on {options.file_path}.") else: slices_patch = { ImageKey.ALIGNMENT: slice_to_update_to_alignment, ImageKey.PROJECTION: slice_to_update_to_projection, ImageKey.FLAT_FIELD: slice_to_update_to_flat, ImageKey.DARK_FIELD: slice_to_update_to_dark, ImageKey.INVALID: slice_to_invalid, } if patch_image_key: _logger.info("start updating frames") for image_key_type, frames_to_update in slices_patch.items(): if frames_to_update is None: continue utils.change_image_key_control( file_path=options.file_path, entry=options.entry, frames_indexes=frames_to_update, image_key_control_value=image_key_type, logger=_logger, ) if patch_det_data: _logger.info("start adding dark and flat field") utils.add_dark_flat_nx_file( file_path=options.file_path, entry=options.entry, darks_start=darks_start_url, flats_start=flat_start_url, darks_end=darks_end_url, flats_end=flat_end_url, embed_data=True, logger=_logger, ) nxtomomill-v2.0.1/nxtomomill/app/split_nxfile.py000066400000000000000000000104141511430602400221320ustar00rootroot00000000000000""" Application to split a file containing several NXtomo entries into several files containing each a single NXtomo .. program-output:: split-nxfile --help """ import os import argparse import logging import string from nxtomo.application.nxtomo import NXtomo from tomoscan.esrf.scan.nxtomoscan import NXtomoScan from silx.io.utils import open as open_hdf5 logging.basicConfig(level=logging.INFO) _logger = logging.getLogger(__name__) def main(argv): """ """ parser = argparse.ArgumentParser( description="split a file containing several NXtomo (at root level) into multiple files containing each a single NXtomo" ) parser.add_argument( "input_file", help="File containing NXtomo to be split into several files", nargs="?", ) parser.add_argument( "output_file_pattern", help="output file pattern. Must contain `{entry_name}` or `{index}` pattern to make sure it is unique", default="{input_file_name}_{entry_name}.nx", nargs="?", ) parser.add_argument( "--overwrite", help="Do not ask for user permission to overwrite output files", action="store_true", default=False, ) parser.add_argument( "--duplicate-data", help="Make all NXtomo free of any external link. As a result this will duplicate data", nargs="?", ) options = parser.parse_args(argv[1:]) output_file_pattern = options.output_file_pattern if "{input_file_name}" in output_file_pattern: output_file_pattern = output_file_pattern.format( { "output_file_pattern": os.path.splitext(options.input_file), } ) split( input_file=options.input_file, output_file_pattern=output_file_pattern, overwrite=options.overwrite, ) def split( input_file: str, output_file_pattern: str, overwrite: bool = False, duplicate_data: bool = False, ) -> tuple: """ :param input_file: path to the file to be splitted :param output_file_pattern: pattern of the file to create. Must contain either `{entry_name}` or `{index}` :param overwrite: can we overwrite output file if alread exists :return: tuple of identifier of all the NXtomo generated """ def get_output_file(index: int, entry_name: str, pattern: str) -> str: """ treat 'pattern' to create the expected output file """ keywords = { "entry_name": entry_name, "index": index, } # filter necessary keywords def get_necessary_keywords(): formatter = string.Formatter() return [field for _, field, _, _ in formatter.parse(pattern) if field] requested_keywords = get_necessary_keywords() def keyword_needed(pair): keyword, _ = pair return keyword in requested_keywords keywords = dict(filter(keyword_needed, keywords.items())) if len(keywords) == 0: raise ValueError( "pattern should at least contains keywords '{index}' or '{entry_name}' to be format. Else unable to create a unique file per NXtomo" ) return os.path.abspath(pattern.format(**keywords)) if duplicate_data: detector_data_as = "as_numpy_array" else: detector_data_as = "as_data_url" result = [] with open_hdf5(input_file) as h5f: for i_entry, entry in enumerate(h5f.keys()): try: nx_tomo = NXtomo().load( input_file, entry, detector_data_as=detector_data_as ) except Exception as e: _logger.error( f"Fail to treat entry {entry}. Error is {e}. Is this a valid Nxtomo ?" ) else: output_file = get_output_file( index=i_entry, entry_name=entry, pattern=output_file_pattern ) dirname = os.path.dirname(output_file) if dirname and not os.path.exists(dirname): os.makedirs(dirname) nx_tomo.save( file_path=output_file, data_path=entry, overwrite=overwrite ) result.append(NXtomoScan(output_file, entry)) return tuple(result) nxtomomill-v2.0.1/nxtomomill/app/tests/000077500000000000000000000000001511430602400202225ustar00rootroot00000000000000nxtomomill-v2.0.1/nxtomomill/app/tests/test_bliss_fluo2nx_app.py000066400000000000000000000104321511430602400252640ustar00rootroot00000000000000# coding: utf-8 import os import logging import numpy as np import h5py try: from nxtomomill.tests.datasets import GitlabDataset except ImportError: raise ImportError("Unable to import GitlabDataset") from nxtomomill.app.blissfluo2nx import main logging.disable(logging.INFO) def test_blissfluo2nx_application_all_dets(): """test nxtomomill fluo2nx CLI with default parameters (handle all detectors found)""" scan_dir = GitlabDataset.get_dataset("blissfluo_datasets3D") output_file = os.path.join(scan_dir, "test_Siemens.nx") truth_file = os.path.join(scan_dir, "Siemens.nx") input_file = os.path.join(scan_dir, "Siemens_raw.h5") main( [ "blissfluo2nx", input_file, output_file, "--overwrite", ] ) assert os.path.exists(output_file), "output_file doesn't exists" with h5py.File(output_file, "r") as f_test, h5py.File(truth_file, "r") as f_true: assert len(list(f_test.keys())) == len( list(f_true.keys()) ), f"Number of entries ({len(list(f_test.keys()))}) not as expected ({len(list(f_true.keys()))})." assert np.allclose( f_test["/grid_Ardesia_ng_mm2_Al_K/data/data"][()], f_true["/grid_Ardesia_ng_mm2_Al_K/data/data"][()], ), "Discrepancy in grid_Ardesia_Al_K projection data." assert np.allclose( f_test["/grid_Ardesia_ng_mm2_Al_K/data/rotation_angle"][()], f_true["/grid_Ardesia_ng_mm2_Al_K/data/rotation_angle"][()], ), "Discrepancy in grid_Ardesia_Al_K rotation angles." def test_blissfluo2nx_application_single_det(): """test nxtomomill fluo2nx CLI targetting a single existing detector""" scan_dir = GitlabDataset.get_dataset("blissfluo_datasets3D") input_file = os.path.join(scan_dir, "Siemens_raw.h5") truth_file = os.path.join(scan_dir, "Siemens_1det_2.nx") output_file = os.path.join(scan_dir, "test_Siemens_1det.nx") main( [ "blissfluo2nx", input_file, output_file, "--detectors", "grid_Ardesia_ng_mm2", "--overwrite", ] ) assert os.path.exists(output_file), "output_file doesn't exists" with h5py.File(output_file, "r") as f_test, h5py.File(truth_file, "r") as f_true: assert len(list(f_test.keys())) == len( list(f_true.keys()) ), f"Number of entries ({len(list(f_test.keys()))}) not as expected ({len(list(f_true.keys()))})." assert np.allclose( f_test["/grid_Ardesia_ng_mm2_Al_K/data/data"][()], f_true["/grid_Ardesia_ng_mm2_Al_K/data/data"][()], ), "Discrepancy in grid_Ardesia_Al_K projection data." assert np.allclose( f_test["/grid_Ardesia_ng_mm2_Al_K/data/rotation_angle"][()], f_true["/grid_Ardesia_ng_mm2_Al_K/data/rotation_angle"][()], ), "Discrepancy in grid_Ardesia_Al_K rotation angles." def test_blissfluo2nx_application_two_det(): """test nxtomomill fluo2nx CLI targetting two existing detector""" scan_dir = GitlabDataset.get_dataset("blissfluo_datasets3D") input_file = os.path.join(scan_dir, "Siemens_raw.h5") truth_file = os.path.join(scan_dir, "Siemens_2det_2.nx") output_file = os.path.join(scan_dir, "test_Siemens_2det.nx") main( [ "blissfluo2nx", input_file, output_file, "--detectors", "grid_Ardesia_ng_mm2", "grid_weighted_ng_mm2", "--overwrite", ] ) assert os.path.exists(output_file), "output_file doesn't exists" with h5py.File(output_file, "r") as f_test, h5py.File(truth_file, "r") as f_true: assert len(list(f_test.keys())) == len( list(f_true.keys()) ), f"Number of entries ({len(list(f_test.keys()))}) not as expected ({len(list(f_true.keys()))})." assert np.allclose( f_test["/grid_Ardesia_ng_mm2_Al_K/data/data"][()], f_true["/grid_Ardesia_ng_mm2_Al_K/data/data"][()], ), "Discrepancy in grid_Ardesia_Al_K projection data." assert np.allclose( f_test["/grid_Ardesia_ng_mm2_Al_K/data/rotation_angle"][()], f_true["/grid_Ardesia_ng_mm2_Al_K/data/rotation_angle"][()], ), "Discrepancy in grid_Ardesia_Al_K rotation angles." nxtomomill-v2.0.1/nxtomomill/app/tests/test_dxfile2nx_app.py000066400000000000000000000015041511430602400243760ustar00rootroot00000000000000# coding: utf-8 import os import tempfile from nxtomomill.app.dxfile2nx import main from nxtomomill.tests.utils.dxfile import MockDxFile def test_dxfile2nx_application(): """test nxtomomill dxfile2nx input_file output_file""" with tempfile.TemporaryDirectory() as folder: dx_file_path = os.path.join(folder, "dxfile.dx") nx_file_path = os.path.join(folder, "dxfile.nx") n_projections = 50 n_darks = 2 n_flats = 4 MockDxFile( file_path=dx_file_path, n_projection=n_projections, n_darks=n_darks, n_flats=n_flats, ) assert not os.path.exists(nx_file_path), "outputfile exists already" main(["dxfile2nx", dx_file_path, nx_file_path]) assert os.path.exists(nx_file_path), "outputfile doesn't exists" nxtomomill-v2.0.1/nxtomomill/app/tests/test_edf2nx_app.py000066400000000000000000000013351511430602400236630ustar00rootroot00000000000000# coding: utf-8 import os import tempfile from tomoscan.esrf.mock import MockEDF from nxtomomill.app.edf2nx import main def test_edf2nx_application(): """test nxtomomill edf2nx input_file output_file""" with tempfile.TemporaryDirectory() as folder: edf_acq_path = os.path.join(folder, "acquisition") n_proj = 10 MockEDF( scan_path=edf_acq_path, n_radio=n_proj, n_ini_radio=n_proj, ) output_file = os.path.join(edf_acq_path, "nexus_file.nx") assert not os.path.exists(output_file), "output_file exists already" main(["edf2nx", edf_acq_path, output_file]) assert os.path.exists(output_file), "output_file doesn't exists" nxtomomill-v2.0.1/nxtomomill/app/tests/test_edf_config_app.py000066400000000000000000000006121511430602400245550ustar00rootroot00000000000000import os import tempfile from nxtomomill.app.h5config import main def test_edf_config_application(): """test nxtomomill edf-config output_file""" with tempfile.TemporaryDirectory() as folder: output_file = os.path.join(folder, "config.cfg") assert not os.path.exists(output_file) main(["edf-config", output_file]) assert os.path.exists(output_file) nxtomomill-v2.0.1/nxtomomill/app/tests/test_fluo2nx_app.py000066400000000000000000000074361511430602400241020ustar00rootroot00000000000000# coding: utf-8 import os import logging import h5py from tomoscan.tests.datasets import GitlabDataset from nxtomomill.app.fluo2nx import main logging.disable(logging.INFO) def test_fluo2nx_application_all_dets(tmp_path): """test nxtomomill fluo2nx CLI with default parameters (handle all detectors found)""" scan_dir = GitlabDataset.get_dataset("fluo_datasets") output_file = os.path.join(tmp_path, "nexus_file.nx") assert not os.path.exists(output_file), "output_file exists already" main(["fluo2nx", scan_dir, output_file, "CP1_XRD_insitu_top_ft_100nm"]) assert os.path.exists(output_file), "output_file doesn't exists" with h5py.File(output_file, "r") as f: assert ( len(list(f.keys())) == 45 ), f"Number of entries ({len(list(f.keys()))}) not as expected (45)." def test_fluo2nx_application_single_det(tmp_path): """test nxtomomill fluo2nx CLI targetting a single existing detector""" scan_dir = GitlabDataset.get_dataset("fluo_datasets") output_file = os.path.join(tmp_path, "nexus_file.nx") assert not os.path.exists(output_file), "output_file exists already" main( [ "fluo2nx", scan_dir, output_file, "CP1_XRD_insitu_top_ft_100nm", "--detectors", "xmap", ] ) assert os.path.exists(output_file), "output_file doesn't exists" with h5py.File(output_file, "r") as f: assert ( len(list(f.keys())) == 15 ), f"Number of entries ({len(list(f.keys()))}) not as expected (15)." def test_fluo2nx_application_two_dets(tmp_path): """test nxtomomill fluo2nx CLI targetting two existing detectors""" scan_dir = GitlabDataset.get_dataset("fluo_datasets") output_file = os.path.join(tmp_path, "nexus_file.nx") assert not os.path.exists(output_file), "output_file exists already" main( [ "fluo2nx", scan_dir, output_file, "CP1_XRD_insitu_top_ft_100nm", "--detectors", "xmap", "falcon", ] ) assert os.path.exists(output_file), "output_file doesn't exists" with h5py.File(output_file, "r") as f: assert ( len(list(f.keys())) == 30 ), f"Number of entries ({len(list(f.keys()))}) not as expected (30)." def test_fluo2nx_application_2D(tmp_path): scan_dir = GitlabDataset.get_dataset("fluo_datasets2D") output_file = os.path.join(tmp_path, "nexus_file_2d.nx") assert not os.path.exists(output_file), "output_file exists already" main( [ "fluo2nx", scan_dir, output_file, "CONT2_p2_600nm_FT02_slice_0", "--dimension", "2", ] ) assert os.path.exists(output_file), "output_file doesn't exists" with h5py.File(output_file, "r") as f: assert ( len(list(f.keys())) == 4 ), f"Number of entries ({len(list(f.keys()))}) not as expected (4)." def test_fluo2nx_application_2D_single_det(tmp_path): """test nxtomomill fluo2nx CLI targetting a single existing detector""" scan_dir = GitlabDataset.get_dataset("fluo_datasets2D") output_file = os.path.join(tmp_path, "nexus_file_2D_singledet.nx") assert not os.path.exists(output_file), "output_file exists already" main( [ "fluo2nx", scan_dir, output_file, "CONT2_p2_600nm_FT02_slice_0", "--detectors", "corrweighted", "--dimension", "2", ] ) assert os.path.exists(output_file), "output_file doesn't exists" with h5py.File(output_file, "r") as f: assert ( len(list(f.keys())) == 2 ), f"Number of entries ({len(list(f.keys()))}) not as expected (2)." nxtomomill-v2.0.1/nxtomomill/app/tests/test_h52nx_app.py000066400000000000000000000015571511430602400234470ustar00rootroot00000000000000# coding: utf-8 import os from nxtomomill.app.h52nx import main from nxtomomill.tests.utils.bliss import MockBlissAcquisition def test_h52nx_application(tmp_path): """test nxtomomill h52nx input_file output_file --single-file""" folder = tmp_path / "test_h52nx_application" folder.mkdir() nx_file_path = os.path.join(folder, "acquisition.nx") bliss_mock = MockBlissAcquisition( n_sample=1, n_sequence=1, n_scan_per_sequence=10, n_darks=5, n_flats=5, with_nx_detector_attr=True, output_dir=folder, detector_name="pcolinux", ) h5_file_path = bliss_mock.samples[0].sample_file assert not os.path.exists(nx_file_path), "outputfile exists already" main(["h52nx", h5_file_path, nx_file_path, "--single-file"]) assert os.path.exists(nx_file_path), "outputfile doesn't exists" nxtomomill-v2.0.1/nxtomomill/app/tests/test_h5_config_app.py000066400000000000000000000006301511430602400243330ustar00rootroot00000000000000# coding: utf-8 import os import tempfile from nxtomomill.app.h5config import main def test_h5_config_application(): """test nxtomomill h5-config output_file""" with tempfile.TemporaryDirectory() as folder: output_file = os.path.join(folder, "config.cfg") assert not os.path.exists(output_file) main(["h5-config", output_file]) assert os.path.exists(output_file) nxtomomill-v2.0.1/nxtomomill/app/tests/test_nx_copy.py000066400000000000000000000026041511430602400233140ustar00rootroot00000000000000import os from nxtomo.application.nxtomo import NXtomo from nxtomomill.app.nxcopy import get_output_file from nxtomomill.app.nxcopy import main def test_nxcopy_get_output_file(): """dummy test for 'get_output_file'""" assert get_output_file("output.nx", "input.nx") == os.path.abspath("output.nx") assert ( get_output_file("/file1/to/", "/file2/to/input/input.nx") == "/file1/to/input.nx" ) def test_copy(tmp_path): """test 'copy' application""" input_folder = tmp_path / "input" input_folder.mkdir() input_nx_tomo_file = os.path.join(input_folder, "nexus.nx") output_folder = tmp_path / "output" output_folder.mkdir() output_folder = str(output_folder) nx_tomo = NXtomo() nx_tomo.save(input_nx_tomo_file, "/entry0000") nx_tomo.save(input_nx_tomo_file, "/entry0002") nx_tomo.save(input_nx_tomo_file, "/entry0004") main(["nx-copy", input_nx_tomo_file, output_folder, "--entry", "entry0002"]) output_file = os.path.join(output_folder, "nexus.nx") assert os.path.exists(output_file) assert len(NXtomo.get_valid_entries(output_file)) == 1 main(["nx-copy", input_nx_tomo_file, output_folder, "--overwrite"]) assert len(NXtomo.get_valid_entries(output_file)) == 3 main(["nx-copy", input_nx_tomo_file, output_folder, "--overwrite", "--remove-vds"]) assert len(NXtomo.get_valid_entries(output_file)) == 3 nxtomomill-v2.0.1/nxtomomill/app/tests/test_patch_nx_app.py000066400000000000000000000016701511430602400243030ustar00rootroot00000000000000# coding: utf-8 import os import tempfile from tomoscan.esrf.mock import MockNXtomo from nxtomomill.app.patch_nx import main def test_patch_nx_application(): """test nxtomomill patch-nx input_file entry --invalid-frames XX:YY""" with tempfile.TemporaryDirectory() as folder: nx_path = os.path.join(folder, "nexus_file.nx") dim = 55 nproj = 20 scan = MockNXtomo( scan_path=nx_path, n_proj=nproj, n_ini_proj=nproj, create_ini_dark=False, create_ini_flat=False, create_final_flat=False, dim=dim, ).scan main( [ "patch-nx", scan.master_file, "entry", "--invalid-frames", "0:12", ] ) scan.clear_cache() assert len(scan.projections) == 8, "Scan is expected to have 8 projections now" nxtomomill-v2.0.1/nxtomomill/app/tests/test_split_nxfile_app.py000066400000000000000000000045261511430602400252020ustar00rootroot00000000000000import os import numpy import pytest import pint from tomoscan.io import HDF5File, get_swmr_mode from nxtomo.application.nxtomo import NXtomo from nxtomomill.app.split_nxfile import split _ureg = pint.get_application_registry() @pytest.mark.parametrize("duplicate_data", (True, False)) def test_split_nxfile(tmp_path, duplicate_data): """ test execution of split_nxfile One file contains """ nx_tomo = NXtomo() n_frames = 20 nx_tomo.instrument.detector.data = numpy.random.random( 100 * 100 * n_frames ).reshape([n_frames, 100, 100]) nx_tomo.sample.rotation_angle = [0, 12] * _ureg.degree output_dir = tmp_path / "test_split_nx_file" output_dir.mkdir() output_dir = str(output_dir) output_file = os.path.join(output_dir, "input_file.nx") nx_tomo.save(output_file, "entry0000") nx_tomo.save(output_file, "entry0001") nx_tomo.save(output_file, "entry0002") # test split with pytest.raises(ValueError): # make sure if no '{index}' or '{entry_name}' are provided this raises an error split( output_file, output_file_pattern=os.sep.join([output_dir, "output_dir_1", "part_.nx"]), ) split( output_file, output_file_pattern=os.sep.join( [output_dir, "output_dir_1", "part_{index}.nx"] ), duplicate_data=duplicate_data, ) for i_file in range(3): assert os.path.exists( tmp_path / f"test_split_nx_file/output_dir_1/part_{i_file}.nx" ) split( output_file, output_file_pattern=os.sep.join( [output_dir, "output_dir_2", "{entry_name}.nx"] ), duplicate_data=duplicate_data, ) for output in ("entry0000", "entry0001", "entry0002"): assert os.path.exists(tmp_path / f"test_split_nx_file/output_dir_2/{output}.nx") # test overwrite with pytest.raises(KeyError): split( output_file, output_file_pattern=os.sep.join( [output_dir, "output_dir_2", "{entry_name}.nx"] ), duplicate_data=duplicate_data, ) with HDF5File( tmp_path / "test_split_nx_file/output_dir_2/entry0002.nx", mode="r", swmr=get_swmr_mode(), ) as h5f: assert "entry0002" in h5f.keys() assert "entry0000" not in h5f.keys() nxtomomill-v2.0.1/nxtomomill/app/z_concatenate_scans.py000066400000000000000000000412051511430602400234400ustar00rootroot00000000000000""" Application to concatenate several scans; each corresponding to a z-stage, into one nexus scan for nabu-helical """ import argparse import os import re import json import pint import numpy as np from silx.io.url import DataUrl from tomoscan.esrf.scan.nxtomoscan import NXtomoScan from tomoscan.scanbase import ReducedFramesInfos from tomoscan.io import HDF5File, get_swmr_mode from nxtomo.application.nxtomo import NXtomo from nxtomo.nxobject.utils import concatenate as nx_concatenate from nxtomo.nxobject.nxdetector import ImageKey from nxtomomill.utils.hdf5 import DatasetReader _ureg = pint.get_application_registry() def str2bool(v): if isinstance(v, bool): return v if v.lower() in ("yes", "true", "t", "y", "1"): return True elif v.lower() in ("no", "false", "f", "n", "0"): return False else: raise argparse.ArgumentTypeError("Boolean value expected.") def get_arguments(argv): parser = argparse.ArgumentParser(description="") parser.add_argument( "--filename_template", required=True, help="""The filename template. It must contain a segment equal to "X"*ndigits which will be replaced by the stage number """, ) parser.add_argument( "--target_file", required=True, help="The new nexus filename that we are going to create ", ) parser.add_argument( "--entry_name", required=False, help="Optional.Output data path aka entry_name. Its default is entry0000", default="entry0000", ) parser.add_argument( "--total_nstages", type=int, required=True, help="The total number of stages" ) parser.add_argument( "--first_stage", type=int, default=0, required=False, help="Optional. Defaults to zero. The number of the first considered stage. Use this to do a smaller sequence", ) parser.add_argument( "--last_stage", type=int, default=-1, required=False, help="Optional. Defaults to total_nstages-1. The number of the last considered stage. Use this to do a smaller sequence", ) parser.add_argument( "--cors_file", required=False, type=str, help="Optional. If given, it is a txt file with a column of centers of rotation. We expect one COR per stage. They are use to set the x_translation.\n" "This file can be either a one column text file, or a json file. In this case the key rotation_axis_position_list must be present and give a list", ) parser.add_argument( "--pixel_size_m", required=False, default=None, help="Optional. The pixel size in meters. If given it will overwrite the nexus one, in the final nexus file", ) parser.add_argument( "--neutral_flat", type=str2bool, required=False, default=False, help="Optional. Its default is False. If true then it will set flats to arrays filled with ones and darks with zeroes", ) parser.add_argument( "--flats_from_reduced", type=str2bool, required=False, default=False, help="Optional. Its default is False. If true then the flats will be set from the reduced flats of the original scans", ) parser.add_argument( "--flats_from_before_after", type=str2bool, required=False, default=False, help="""Optional. Its default is False. If true set flats from the reduced flats of "before and after" scans.\n" "If given then you have to provide also the names these two scans, using parameters '--scan_before' and '--scan_after'""", ) parser.add_argument( "--scan_before", required=False, type=str, help="""Optional. To be used with "flats_from_before_after selected". The name of the scan before all the scans. From this the flat/dark has been extracted""", ) parser.add_argument( "--scan_after", required=False, type=str, help="""Optional. To be used with "flats_from_before_after selected". The name of the scan after all the scans. From this the flat/dark has been extracted""", ) args = parser.parse_args(argv) sum_bool = ( args.neutral_flat + args.flats_from_reduced + args.flats_from_before_after ) if (sum_bool) not in [0, 1]: message = f""" Only one or none of neutral_flat, flats_from_reduced, flats_from_before_after options can be selected. You selected {sum_bool} of them """ raise ValueError(message) if args.last_stage == -1: args.last_stage = args.total_nstages - 1 args.nstages = args.last_stage + 1 - args.first_stage if args.cors_file is None: args.cors = np.zeros([args.nstages], "f") else: try: # check if it is json with open(args.cors_file, "r") as fj: json_dict = json.load(fj) except ValueError: args.cors = np.loadtxt(args.cors_file) else: args.cors = json_dict["rotation_axis_position_list"] pattern = re.compile("[X]+") # X represent the variable part of the 'template' # for example if we want to treat scans HA_2000_sample_0000.nx, ..., HA_2000_sample_9999.nx then # we expect the template to be HA_2000_sample_XXXX.nx # warning: If the dataset base names contains several X substrings the longest ones will be taken. ps = pattern.findall(args.filename_template) ls = list(map(len, ps)) if len(ls) < 1: args.name_template = args.filename_template if args.first_stage != args.last_stage: message = f" The argument for filename_template , which was '{args.filename_template}' does not seem to contain a pattern with multiple X for the numerical part" raise ValueError(message) else: idx = np.argmax(ls) if len(ps[idx]) < 2: message = f""" The argument filename_template should contain a substring formed by at least two 'X' The filename_template was {args.filename_template} """ raise ValueError(message) args.name_template = args.filename_template.replace( ps[idx], "{i_stage:" + "0" + str(ls[idx]) + "d}" ) args.file_list = [] for i_stage in range(args.first_stage, args.last_stage + 1): args.file_list.append(args.name_template.format(i_stage=i_stage)) args.used_current = None if args.pixel_size_m is not None: args.overwrite_pixel_size = True args.pixel_size_m = float(args.pixel_size_m) else: args.overwrite_pixel_size = False with HDF5File(args.file_list[0], "r", swmr=get_swmr_mode()) as h5f: args.pixel_size_m = h5f[ os.path.join(args.entry_name, "instrument", "detector", "x_pixel_size") ][()] return args def main(argv): args = get_arguments(argv[1:]) output_file = args.target_file dummy_frame = None """the dummy frame is a frame fill with ones. It will be insert between two stages to ensure series from the twos stages will be perceived as 'uncontiguous'. Separates flats at the end of a stage and at the beginning. """ nxt_z_list = [] npoints_list = [] for i_stage in range(args.first_stage, args.last_stage + 1): filename = args.file_list[i_stage - args.first_stage] # step 1: load the stage nxt = NXtomo() nxt.load(filename, data_path=args.entry_name, detector_data_as="as_data_url") nxt_z_list.append(nxt) scan = NXtomoScan(filename, args.entry_name) npoints_list.append(len(scan.image_key_control)) if dummy_frame is None: if len(nxt.instrument.detector.data) > 0: # create empty frame to give it to the 'invalid_nxt' NXtomo later with DatasetReader(nxt.instrument.detector.data[0]) as raw_data: data_type = raw_data.dtype # FIXME: avoid keeping some file open. not clear why this is needed raw_data = None args.img_shape = (scan.dim_2, scan.dim_1) with HDF5File(output_file, mode="w") as h5s: h5s["empty_frame"] = np.ones( (1, scan.dim_2, scan.dim_1), dtype=data_type ) dummy_frame = DataUrl( file_path=output_file, data_path="empty_frame" ) else: message = """The first nx file must have some data""" raise ValueError(message) # step 2: insert single 'empty' frame between the two stages (aka dummy frame) invalid_nxt = NXtomo(args.entry_name) invalid_nxt.instrument.detector.image_key_control = [ImageKey.INVALID] invalid_nxt.energy = nxt.energy.value invalid_nxt.control.data = np.ones(1, "f") invalid_nxt.sample.rotation_angle = [0.0] * _ureg.degree invalid_nxt.instrument.detector.field_of_view = ( nxt.instrument.detector.field_of_view.value ) invalid_nxt.instrument.detector.x_pixel_size = ( nxt.instrument.detector.x_pixel_size.value ) invalid_nxt.instrument.detector.y_pixel_size = ( nxt.instrument.detector.y_pixel_size.value ) invalid_nxt.instrument.detector.distance = ( nxt.instrument.detector.distance.value ) invalid_nxt.sample.x_translation = np.zeros([1], "f") invalid_nxt.sample.y_translation = np.zeros([1], "f") invalid_nxt.sample.z_translation = np.zeros([1], "f") invalid_nxt.instrument.detector.data = (dummy_frame,) nxt_z_list.append(invalid_nxt) nx_concatenated = nx_concatenate(nxt_z_list) nx_concatenated.save( file_path=output_file, data_path=args.entry_name, overwrite=True ) with HDF5File(output_file, "r+") as output_target: N_total = output_target[ os.path.join(args.entry_name, "sample", "x_translation") ].shape[0] if args.neutral_flat: # will set flats to arrays filled with ones and darks with zeroes original_currents = output_target[ os.path.join(args.entry_name, "control", "data") ][()] original_currents[:] = 1 del output_target[os.path.join(args.entry_name, "control", "data")] output_target[os.path.join(args.entry_name, "control", "data")] = ( original_currents ) do_flat = ( args.flats_from_reduced or args.flats_from_before_after or args.neutral_flat ) if do_flat or (args.cors_file is not None): one_for_invalid = 1 actual_pos = 0 old_pos = actual_pos i_stage = args.first_stage if do_flat: darks_dictionary = {} flats_dictionary = {} darks_infos_dictionary = {"count_time": []} flats_infos_dictionary = {"machine_current": [], "count_time": []} add_dark_to_dict( args, darks_dictionary, darks_infos_dictionary, i_stage, actual_pos ) for i_stage in range(args.first_stage, args.last_stage + 1): if do_flat: add_flat_to_dict( args, flats_dictionary, flats_infos_dictionary, i_stage, actual_pos, ) actual_pos += npoints_list[i_stage - args.first_stage] if do_flat: add_flat_to_dict( args, flats_dictionary, flats_infos_dictionary, i_stage, actual_pos, position_in_list=1, ) # if i_stage != args.last_stage: actual_pos += one_for_invalid if args.cors_file is not None: output_target[ os.path.join(args.entry_name, "sample", "x_translation") ][old_pos:actual_pos] = ( -args.cors[i_stage - args.first_stage] + args.cors[0] ) * args.pixel_size_m old_pos = actual_pos assert actual_pos == N_total if args.overwrite_pixel_size: g = output_target[ os.path.join(args.entry_name, "instrument", "detector") ] g["x_pixel_size"][()] = args.pixel_size_m g["y_pixel_size"][()] = args.pixel_size_m if do_flat: # darks metadata meta_darks = ReducedFramesInfos() meta_darks.count_time.extend(darks_infos_dictionary["count_time"][:1]) # flats metadata meta_flats = ReducedFramesInfos() meta_flats.machine_current.extend( flats_infos_dictionary["machine_current"] ) meta_flats.count_time.extend(flats_infos_dictionary["count_time"]) # save metadata scan = NXtomoScan(output_file, args.entry_name) scan.save_reduced_flats( flats_dictionary, flats_infos=meta_flats, overwrite=True ) scan.save_reduced_darks( darks_dictionary, darks_infos=meta_darks, overwrite=True ) def add_flat_to_dict( args, flats_dictionary, flats_infos_dictionary, i_stage, actual_pos, position_in_list=0, ): if args.flats_from_reduced: filename = args.file_list[i_stage - args.first_stage] scan = NXtomoScan(filename, args.entry_name) reduced_flats, metadata_flats = scan.load_reduced_flats(return_info=True) position_in_list = min(position_in_list, len(list(reduced_flats.keys())) - 1) my_flat = reduced_flats[list(reduced_flats.keys())[position_in_list]] my_current = metadata_flats.machine_current[position_in_list] my_count_time = metadata_flats.count_time[position_in_list] elif args.neutral_flat: my_flat = np.ones(args.img_shape, "f") my_current = 1.0 my_count_time = 1.0 elif args.flats_from_before_after: scan = NXtomoScan(args.scan_before, args.entry_name) reduced_flats_b, metadata_flats_b = scan.load_reduced_flats(return_info=True) reduced_darks_b, _ = scan.load_reduced_darks(return_info=True) scan = NXtomoScan(args.scan_after, args.entry_name) reduced_flats_e, metadata_flats_e = scan.load_reduced_flats(return_info=True) reduced_darks_e, _ = scan.load_reduced_darks(return_info=True) flat_b = reduced_flats_b[list(reduced_flats_b.keys())[0]] flat_e = reduced_flats_e[list(reduced_flats_e.keys())[0]] dark_b = reduced_darks_b[list(reduced_darks_b.keys())[0]] dark_e = reduced_darks_b[list(reduced_darks_e.keys())[0]] current_b = metadata_flats_b.machine_current[0] current_e = metadata_flats_e.machine_current[0] if args.used_current is None: args.used_current = current_b factor = (i_stage) / (args.total_nstages) my_flat = args.used_dark + ( (flat_b - dark_b) * (args.used_current / current_b) * (1 - factor) + (flat_e - dark_e) * (args.used_current / current_e) * factor ) my_current = args.used_current my_count_time = args.used_count_time else: raise ValueError( "one of the option must be activated in[neutral_flat, flats_from_reduced, flats_from_before_after]" ) flats_dictionary[actual_pos] = my_flat flats_infos_dictionary["machine_current"].append(my_current) flats_infos_dictionary["count_time"].append(my_count_time) def add_dark_to_dict( args, darks_dictionary, darks_infos_dictionary, i_stage, actual_pos ): if args.flats_from_reduced: filename = args.file_list[i_stage - args.first_stage] elif args.flats_from_before_after: filename = args.scan_before else: assert args.neutral_flat filename = None if filename is not None: scan = NXtomoScan(filename, args.entry_name) reduced_darks, metadata_darks = scan.load_reduced_darks(return_info=True) my_dark = reduced_darks[list(reduced_darks.keys())[0]] my_count_time = metadata_darks.count_time[0] else: my_dark = np.zeros(args.img_shape, "f") my_count_time = 1.0 darks_dictionary[actual_pos] = my_dark darks_infos_dictionary["count_time"].append(my_count_time) args.used_count_time = my_count_time args.used_dark = darks_dictionary[actual_pos] nxtomomill-v2.0.1/nxtomomill/app/zstages2nxs.py000066400000000000000000000225741511430602400217370ustar00rootroot00000000000000"""Application to concatenate a serie of scan with different z to a single NXtomo""" import argparse import logging import os import re import numpy as np from tomoscan.framereducer.method import ReduceMethod from nxtomomill.converter import from_h5_to_nx from nxtomomill.io.config import TomoHDF5Config from ..utils.flat_reducer import flat_reducer from ..utils.utils import strip_extension logging.basicConfig(level=logging.INFO) _logger = logging.getLogger(__name__) def str2bool(v): if isinstance(v, bool): return v if v.lower() in ("yes", "true", "t", "y", "1"): return True elif v.lower() in ("no", "false", "f", "n", "0"): return False else: raise argparse.ArgumentTypeError("Boolean value expected.") def get_arguments(user_args): parser = argparse.ArgumentParser( description=""" creates all the target nxs for all the stages. If the postfixes for reference scans are give (the one before and another after the measurements) the reduced flats dark are also created. The references scans are expected to contains projections to be 'interpreted' as flats """ ) parser.add_argument( "--filename_template", required=False, default=None, help="""The filename template. To be used for multiples zstages it must contain one or more segments equal to "X"*ndigits which will be replaced by the stage number, for the scans, and, for the reference scans, by the begin/end prefixes""", ) parser.add_argument( "--filename", required=False, default=None, help="""The filename. To be used for a single scan""", ) parser.add_argument( "--output_filename_template", required=False, default=None, help="""Optional, default to a name deduced from filename_template. The output filename template. It must contain one or more segments equal to "X"*ndigits which will be replaced by the stage number""", ) parser.add_argument( "--entry_name", required=False, help="entry_name", default="entry0000" ) parser.add_argument( "--total_nstages", type=int, default=None, required=True, help="How many stages. Example: from 0 to 43 -> --total_nstages 44. ", ) parser.add_argument( "--first_stage", type=int, default=0, required=False, help="Optional. Defaults to zero. The number of the first considered stage. Use this to do a smaller sequence", ) parser.add_argument( "--last_stage", type=int, default=-1, required=False, help="Optional. Defaults to total_nstages-1. The number of the last considered stage. Use this to do a smaller sequence", ) parser.add_argument( "--do_references", type=str2bool, default=False, required=False, help="Optional. If given the reference scans are used for the extraction of the flats/dark. The reference scans are obtained using the ref postfixes", ) parser.add_argument( "--extracted_reference_target_dir", type=str, default=None, required=None, help="Optional. By default the extracted reference will be written in the same directory as the nexus scan. As the extraction procedure is time consuming they can be written instead to a common directory", ) parser.add_argument( "--ref_scan_begin", type=str, default=None, required=False, help="""used when "do_reference" is True. It is optional. It is the reference scan. """, ) parser.add_argument( "--ref_scan_end", type=str, default=None, required=False, help="""used when "do_reference" is True. It is optional. It is the end for the reference scan . """, ) parser.add_argument( "--target_directory", type=str, default="./", required=False, help="""Where files are written. Optional, defaults to current directory""", ) parser.add_argument( "--median_or_mean", type=str, choices=[ReduceMethod.MEAN.value, ReduceMethod.MEDIAN.value], default=ReduceMethod.MEAN.value, required=False, help="""Choose betwen median or mean. Optional. Default is mean""", ) parser.add_argument( "--voxel_size", type=float, default=None, required=False, help="""Defaults to zero. If set to a different value the bliss generated nexus files will be corrected. Units are micron""", ) parser.add_argument( "--dark_default_value", type=float, default=300, required=False, help="""The dark value that is used for scans without dark""", ) args = parser.parse_args(user_args) if args.last_stage == -1: args.last_stage = args.total_nstages - 1 return args def _convert_bliss2nx(bliss_ref_name, nexus_name, corrections_dict={}): config = TomoHDF5Config() config.overwrite = True config.no_master_file = False config.input_file = bliss_ref_name config.output_file = nexus_name for corr_name, corr_value in corrections_dict.items(): setattr(config, corr_name, corr_value) from_h5_to_nx(config) def main(argv): args = get_arguments(argv[1:]) if args.filename_template is not None: name_template_for_numeric = template_to_format_string(args.filename_template) else: name_template_for_numeric = args.filename if name_template_for_numeric is None: raise ValueError( " Either filename_template of filename must be given as arguments" ) if args.output_filename_template is not None: args.output_refname_template = None if args.filename is None: if args.extracted_reference_target_dir is None: args.output_refname_template = template_to_format_string( args.output_filename_template, literal=True ) else: args.output_refname_template = None args.output_filename_template = template_to_format_string( args.output_filename_template, literal=False ) else: # will be deduced at output time args.output_refname_template = None extra_dict = {} if args.voxel_size: extra_dict.update( { "x_pixel_size": args.voxel_size * 1.0e-6, "y_pixel_size": args.voxel_size * 1.0e-6, } ) if args.do_references: refs_nexus_names = [] for bliss_ref_name, what in ( (args.ref_scan_begin, "begin"), (args.ref_scan_end, "end"), ): if args.output_refname_template is None: if args.extracted_reference_target_dir is None: extraction_target_dir = args.target_directory else: extraction_target_dir = args.extracted_reference_target_dir nexus_name = os.path.join( extraction_target_dir, strip_extension(os.path.basename(bliss_ref_name), _logger) + ".nx", ) else: nexus_name = args.output_refname_template.format(what=what) _convert_bliss2nx(bliss_ref_name, nexus_name, extra_dict) refs_nexus_names.append(nexus_name) for iz in range(args.first_stage, args.last_stage + 1): bliss_name = name_template_for_numeric.format(i_stage=iz) if args.output_filename_template is None: nexus_name = os.path.join( args.target_directory, strip_extension(os.path.basename(bliss_name), _logger) + ".nx", ) else: nexus_name = args.output_filename_template.format(i_stage=iz) _convert_bliss2nx(bliss_name, nexus_name, extra_dict) if args.do_references: factor = (iz + 1) / (args.total_nstages) flat_reducer( nexus_name, ref_start_filename=refs_nexus_names[0], ref_end_filename=refs_nexus_names[1], mixing_factor=factor, entry_name=args.entry_name, median_or_mean=args.median_or_mean, save_intermediated=False, reuse_intermediated=True, dark_default_value=args.dark_default_value, ) return 0 def template_to_format_string(template, literal=False): pattern = re.compile("[X]+") # X represent the variable part of the 'template' # for example if we want to treat scans HA_2000_sample_0000.nx, ..., HA_2000_sample_9999.nx then # we expect the template to be HA_2000_sample_XXXX.nx # warning: If the dataset base names contains several X substrings the longest ones will be taken. ps = pattern.findall(template) ls = list(map(len, ps)) if len(ls) == 0: raise ValueError("The template argument does not contain XX.. segments") idx = np.argmax(ls) if len(ps[idx]) < 2: message = f""" The argument template should contain a substring formed by at least two 'X' The template was {template} """ raise ValueError(message) if not literal: template = template.replace(ps[idx], "{i_stage:" + "0" + str(ls[idx]) + "d}") else: template = template.replace(ps[idx], "{what}") return template nxtomomill-v2.0.1/nxtomomill/converter/000077500000000000000000000000001511430602400203075ustar00rootroot00000000000000nxtomomill-v2.0.1/nxtomomill/converter/__init__.py000066400000000000000000000011431511430602400224170ustar00rootroot00000000000000"""python API to convert from several format to `NXtomo `_""" from .dxfile.dxfileconverter import from_dx_to_nx # noqa F401 from .edf.edfconverter import EDFFileKeys # noqa F401 from .edf.edfconverter import edf_to_nx # noqa F401 from .edf.edfconverter import from_edf_to_nx # noqa F401 from .hdf5.hdf5converter import from_h5_to_nx # noqa F401 from .hdf5.hdf5converter import get_bliss_tomo_entries # noqa F401 from .fluo.fluoconverter import from_fluo_to_nx # noqa F401 from .fluo.fluoconverter import from_blissfluo_to_nx # noqa F401 nxtomomill-v2.0.1/nxtomomill/converter/baseconverter.py000066400000000000000000000002751511430602400235270ustar00rootroot00000000000000"""Contain base class of a converter""" class BaseConverter: """ Interface of a converter """ def convert(self) -> tuple: raise NotImplementedError("Base class") nxtomomill-v2.0.1/nxtomomill/converter/dxfile/000077500000000000000000000000001511430602400215625ustar00rootroot00000000000000nxtomomill-v2.0.1/nxtomomill/converter/dxfile/__init__.py000066400000000000000000000003551511430602400236760ustar00rootroot00000000000000""" module to convert from `dxchange `_ (.dx) to `NXtomo `_ (.nx) """ from .dxfileconverter import from_dx_to_nx # noqa F401 nxtomomill-v2.0.1/nxtomomill/converter/dxfile/dxfileconverter.py000066400000000000000000000574331511430602400253530ustar00rootroot00000000000000# coding: utf-8 """ module to convert from dx file (hdf5) to nexus tomo compliant .nx (hdf5) """ from __future__ import annotations import logging import os import pint import h5py import numpy from silx.io.url import DataUrl from silx.io.utils import get_data, h5py_read_dataset from tomoscan.io import HDF5File from nxtomo.nxobject.nxdetector import FieldOfView, ImageKey from nxtomo.utils.frameappender import FrameAppender from nxtomomill.converter.baseconverter import BaseConverter from nxtomomill.converter.version import version as converter_version from nxtomomill.models.dx2nx import DX2nxModel from nxtomomill.utils.hdf5 import DatasetReader, EntryReader, get_dataset_unit from silx.utils.deprecation import deprecated _ureg = pint.get_application_registry() _logger = logging.getLogger(__name__) __all__ = ["from_dx_to_nx", "from_dx_config_to_nx"] # dev note: to ensure backward compatibility we needed to keep from_dx_to_nx. And we created from_dx_config_to_nx to handle the configuration file. # but with time we expect to remove `from_dx_to_nx` and them rename from_dx_config_to_nx to `from_dx_to_nx` in order to get something coherent. @deprecated( replacement="from_dx_config_to_nx", since_version="0.13", reason="Some configuration dedicated classes exists for tuning the configuration directly instead of provided n parameter to the function.", ) def from_dx_to_nx( input_file: str, output_file: str | None = None, file_extension: str = ".nx", duplicate_data: bool = True, input_entry: str = "/", output_entry: str = "entry0000", scan_range: tuple = (0.0, 180.0), pixel_size=(None, None), field_of_view: FieldOfView | str | None = None, distance: float | None = None, overwrite: bool = True, energy: float | None = None, ) -> tuple: """ :param input_file: dxfile to convert :param output_file: output file to save :param file_extension: file extension to give if the output file is not provided. :param duplicate_data: if True frames will be duplicated. Otherwise we will create (relative) link to the input file. :param input_entry: path to the HDF5 group to convert. For now it looks each file can only contain one dataset. Just to ensure any future compatibility if it evolve with time. :param output_entry: path to store the NxTomo created. :param scan_range: tuple of two elements with the minimum scan range. Projections are expected to be taken with equal angular spaces. :param pixel_size: pixel size can be provided (in meter and as x_pizel_size, y_pixel_size) :param field_of_view: field of view :param distance: sample / detector distance in meter :param overwrite: if True and if the entry already exists in the output file then will overwrite it. :return: tuple of (output_file, entry) created. For now the list should contain at most one of those tuple """ configuration = DX2nxModel(input_file=input_file, output_file=output_file) configuration.file_extension = file_extension configuration.copy_data = duplicate_data configuration.input_entry = input_entry configuration.output_entry = output_entry configuration.scan_range = scan_range configuration.pixel_size = pixel_size configuration.field_of_view = field_of_view configuration.sample_detector_distance = distance configuration.overwrite = overwrite configuration.energy = energy return from_dx_config_to_nx(configuration=configuration) def from_dx_config_to_nx( configuration: DX2nxModel, ): """ Convert from dxfile to NXtomo. Dark and flats will be store at the beginning and we consider they are take at start so rotation angle will set to scan_range[0]. Projection rotation angle will be interpolated from scan_range and with equality space distribution. We do not expect any alignment projection. """ converter = _DxFileToNxConverter(configuration=configuration) return converter.convert() class _PathDoesNotExistsInExchange(Exception): pass class _DxFileToNxConverter(BaseConverter): """ Convert from dxfile to NXtomo. Dark and flats will be store at the beginning and we consider they are take at start so rotation angle will set to scan_range[0]. Projection rotation angle will be interpolated from scan_range and with equality space distribution. We do not expect any alignment projection. """ DEFAULT_SAMPLE_DETECTOR_DISTANCE_VALUE: float = 1.0 DEFAULT_PIXEL_VALUE: float = 1.0 DEFAULT_BEAM_ENERGY: float = 1.0 def __init__( self, configuration: DX2nxModel, ): self._configuration = configuration if not len(self.scan_range) == 2: raise ValueError("scan_range expects to be a tuple with two elements") input_file = os.path.abspath(self.input_file) if self.output_file is None: input_file_basename, _ = os.path.splitext(input_file) if not self._configuration.file_extension.startswith("."): self._configuration.file_extension = ( "." + self._configuration.file_extension ) output_file = os.path.join( os.path.dirname(input_file), os.path.basename(input_file_basename) + self._configuration.file_extension, ) else: output_file = os.path.abspath(self.output_file) self._configuration.output_file = output_file if self.input_entry == "/": self._configuration._input_entry = "" else: self._configuration._input_entry = self.input_entry if self.field_of_view is not None: fov = self.field_of_view if isinstance(fov, str): fov = fov.title() self._configuration.field_of_view = FieldOfView(fov) self._n_frames = 0 self._data_proj_url = None self._data_darks_url = None self._data_flats_url = None self._input_root_url = DataUrl( file_path=self.input_file, data_path=self.input_entry, scheme="silx", ) @property def input_file(self): return self._configuration.input_file @property def input_entry(self): return self._configuration.input_entry @property def output_file(self): return self._configuration.output_file @property def output_entry(self): return self._configuration.output_entry @property def scan_range(self): return self._configuration.scan_range @property def copy_data(self): return self._configuration.copy_data @property def overwrite(self): return self._configuration.overwrite @property def field_of_view(self) -> FieldOfView | None: return self._configuration.field_of_view @property def energy(self) -> float | None: return self._configuration.energy @property def input_root_url(self): return self._input_root_url def convert(self): """ do conversion from dxfile to NXtomo :return: tuple of (output_file, entry) created. For now the list should contain at most one of those tuple """ with HDF5File(self.output_file, mode="a") as h5f: if self.output_entry in h5f: if self.overwrite: del h5f[self.output_entry] else: raise OSError( "{} already exists cannot create requested NXtomo entry. Won't overwrite it as not requested" ) self._n_frames = 0 self._data_proj_url = DataUrl( file_path=self.input_file, data_path="/".join((self.input_entry, "exchange", "data")), scheme="silx", ) self._data_darks_url = DataUrl( file_path=self.input_file, data_path="/".join((self.input_entry, "exchange", "data_dark")), scheme="silx", ) self._data_flats_url = DataUrl( file_path=self.input_file, data_path="/".join((self.input_entry, "exchange", "data_white")), scheme="silx", ) # convert frames if self.copy_data: self._convert_frames_with_duplication() else: self._convert_frames_without_duplication() # convert detector extra information # x pixel size x_pixel_size, y_pixel_size = self._configuration.pixel_size if x_pixel_size is None: try: x_pixel_size = self._read_x_pixel_size().to_base_units().magnitude except Exception: x_pixel_size = self.DEFAULT_PIXEL_VALUE _logger.warning( "No x pixel size found or provided. Set the it to the default value" ) with HDF5File(self.output_file, mode="a") as h5f: root_grp = h5f.require_group(self.output_entry) instrument_grp = root_grp.require_group("instrument") detector_grp = instrument_grp.require_group("detector") detector_grp["x_pixel_size"] = x_pixel_size detector_grp["x_pixel_size"].attrs["units"] = "m" # y pixel size if y_pixel_size is None: try: y_pixel_size = self._read_y_pixel_size().to_base_units().magnitude except Exception: y_pixel_size = self.DEFAULT_PIXEL_VALUE _logger.warning( "No y pixel size found or provided. Set the it to the default value" ) with HDF5File(self.output_file, mode="a") as h5f: root_grp = h5f.require_group(self.output_entry) instrument_grp = root_grp.require_group("instrument") detector_grp = instrument_grp.require_group("detector") detector_grp["y_pixel_size"] = y_pixel_size detector_grp["y_pixel_size"].attrs["units"] = "m" # field of view if self.field_of_view is not None: with HDF5File(self.output_file, mode="a") as h5f: root_grp = h5f.require_group(self.output_entry) instrument_grp = root_grp.require_group("instrument") detector_grp = instrument_grp.require_group("detector") detector_grp["field_of_view"] = self.field_of_view.value # distance if self._configuration.sample_detector_distance is None: # if value not set by the user let read it try: self._configuration.sample_detector_distance = ( self._read_sample_detector_distance().to_base_units().magnitude ) except Exception: self._configuration.sample_detector_distance = ( self.DEFAULT_SAMPLE_DETECTOR_DISTANCE_VALUE ) _logger.warning( "No detector / sample distance found or provided. " "Set the it the default value" ) with HDF5File(self.output_file, mode="a") as h5f: root_grp = h5f.require_group(self.output_entry) instrument_grp = root_grp.require_group("instrument") detector_grp = instrument_grp.require_group("detector") detector_grp["distance"] = self._configuration.sample_detector_distance detector_grp["distance"].attrs["units"] = str(_ureg.meter) # energy if self.energy is None: try: self._configuration.energy = self._read_energy().to(_ureg.keV).magnitude except Exception: self._configuration.energy = self.DEFAULT_BEAM_ENERGY _logger.warning( "No energy found or provided. " "Set the it to the default value" ) with HDF5File(self.output_file, mode="a") as h5f: root_grp = h5f.require_group(self.output_entry) beam_grp = root_grp.require_group("beam") beam_grp["incident_energy"] = self._configuration.energy beam_grp["incident_energy"].attrs["units"] = str(_ureg.keV) # count_time self._copy_count_time() # start time with EntryReader(self.input_root_url) as input_entry: if "file_creation_datetime" in input_entry: with HDF5File(self.output_file, mode="a") as h5f: root_grp = h5f.require_group(self.output_entry) root_grp["start_time"] = h5py_read_dataset( input_entry["file_creation_datetime"] ) return ((self.output_file, self.output_entry),) def _copy_count_time(self): """Try to read and copy count_time / exposure_period""" def read_and_write_count_time(dataset: h5py.Dataset): count_time = h5py_read_dataset(dataset) if count_time == "Error: Unknown Attribute": _logger.info( f"Count time not stored on {self.input_entry}@{self.input_file}" ) else: try: if len(count_time) == self._n_frames: with HDF5File(self.output_file, mode="a") as h5f: root_grp = h5f.require_group(self.input_entry) instrument_grp = root_grp.require_group("instrument") detector_grp = instrument_grp.require_group("detector") detector_grp["count_time"] = count_time else: _logger.warning( f"exposure period and data frame have an incoherent size ({len(count_time)} vs {self._n_frames})" ) except Exception as e: _logger.error( f"Failed to get 'count_time' / 'exposure_period'. reason is {e}" ) with EntryReader(self.input_root_url) as input_entry: if "measurement" in input_entry: measurement_grp = input_entry["measurement"] if "detector" in measurement_grp: detector_grp = measurement_grp["detector"] if "exposure_period" in detector_grp: read_and_write_count_time(detector_grp["exposure_period"]) def _convert_frames_with_duplication(self): image_key = [] image_key_control = [] rotation_angle = [] data = None # handle darks try: data_dark = get_data(self._data_darks_url) except _PathDoesNotExistsInExchange: _logger.warning(f"No darks found in {self.input_entry}@{self.input_file}") else: image_key.extend([ImageKey.DARK_FIELD.value] * len(data_dark)) image_key_control.extend([ImageKey.DARK_FIELD.value] * len(data_dark)) rotation_angle.extend([self.scan_range[0]] * len(data_dark)) if data is None: data = data_dark else: data = numpy.concatenate((data, data_dark), axis=0) # handle flats try: data_flat = get_data(self._data_flats_url) except _PathDoesNotExistsInExchange: _logger.warning(f"No flats found in {self.input_entry}@{self.input_file}") else: image_key.extend([ImageKey.FLAT_FIELD.value] * len(data_flat)) image_key_control.extend([ImageKey.FLAT_FIELD.value] * len(data_flat)) rotation_angle.extend([self.scan_range[0]] * len(data_flat)) if data is None: data = data_flat else: data = numpy.concatenate((data, data_flat), axis=0) # handle projections data_proj = get_data(self._data_proj_url) image_key.extend([ImageKey.PROJECTION.value] * len(data_proj)) image_key_control.extend([ImageKey.PROJECTION.value] * len(data_proj)) assert ( self.scan_range[0] is not None ), "scan range is expected to be a tuple of float" assert ( self.scan_range[1] is not None ), "scan range is expected to be a tuple of float" rotation_angle.extend( numpy.linspace( self.scan_range[0], self.scan_range[1], num=len(data_proj), endpoint=True, ) ) if data is None: data = data_proj else: data = numpy.concatenate((data, data_proj), axis=0) self._n_frames = len(data) with HDF5File(self.output_file, mode="a") as h5f: root_grp = h5f.require_group(self.output_entry) instrument_grp = root_grp.require_group("instrument") detector_grp = instrument_grp.require_group("detector") detector_grp["data"] = data detector_grp["image_key"] = image_key detector_grp["image_key_control"] = image_key_control sample_grp = root_grp.require_group("sample") sample_grp["rotation_angle"] = rotation_angle sample_grp["rotation_angle"].attrs["units"] = str(_ureg.degree) self._path_nxtomo_attrs(root_grp) def _path_nxtomo_attrs(self, root_grp): root_grp.attrs["NX_class"] = "NXentry" root_grp.attrs["definition"] = "NXtomo" root_grp.attrs["version"] = converter_version() root_grp.attrs["default"] = "instrument/detector" instrument_grp = root_grp.require_group("instrument") instrument_grp.attrs["NX_class"] = "NXinstrument" instrument_grp.attrs["default"] = "detector" detector_grp = instrument_grp.require_group("detector") detector_grp.attrs["NX_class"] = "NXdetector" detector_grp.attrs["NX_class"] = "NXdata" detector_grp.attrs["signal"] = "data" detector_grp.attrs["SILX_style/axis_scale_types"] = ["linear", "linear"] if "data" in detector_grp: detector_grp["data"].attrs["interpretation"] = "image" sample_node = root_grp.require_group("sample") sample_node.attrs["NX_class"] = "NXsample" def _convert_frames_without_duplication(self): image_key = [] image_key_control = [] rotation_angle = [] dataset_path = "/".join((self.output_entry, "instrument", "detector", "data")) n_dark = 0 n_flat = 0 n_proj = 0 # handle darks try: with DatasetReader(self._data_darks_url) as dark_dataset: n_dark = dark_dataset.shape[0] # FIXME: avoid keeping some file open. not clear why this is needed dark_dataset = None FrameAppender( self._data_darks_url, self.output_file, data_path=dataset_path, where="end", logger=_logger, ).process() except Exception: _logger.error( f"No darks found in {self._data_darks_url.path()} or unable to add them to the dataset" ) else: image_key.extend([ImageKey.DARK_FIELD.value] * n_dark) image_key_control.extend([ImageKey.DARK_FIELD.value] * n_dark) rotation_angle.extend([self.scan_range[0]] * n_dark) # handle flats try: with DatasetReader(self._data_flats_url) as flat_dataset: n_flat = flat_dataset.shape[0] # FIXME: avoid keeping some file open. not clear why this is needed flat_dataset = None FrameAppender( self._data_flats_url, self.output_file, data_path=dataset_path, where="end", logger=_logger, ).process() except Exception: _logger.warning( f"No flats found in {self._data_flats_url.path()} or unable to add them to the dataset" ) else: image_key.extend([ImageKey.FLAT_FIELD.value] * n_flat) image_key_control.extend([ImageKey.FLAT_FIELD.value] * n_flat) rotation_angle.extend([self.scan_range[0]] * n_flat) # handle projections try: with DatasetReader(self._data_proj_url) as proj_dataset: n_proj = proj_dataset.shape[0] # FIXME: avoid keeping some file open. not clear why this is needed proj_dataset = None FrameAppender( self._data_proj_url, self.output_file, data_path=dataset_path, where="end", logger=_logger, ).process() except Exception: _logger.warning( f"No projections found in {self._data_proj_url.path()} or unable to add them to the dataset" ) else: image_key.extend([ImageKey.PROJECTION.value] * n_proj) image_key_control.extend([ImageKey.PROJECTION.value] * n_proj) rotation_angle.extend( numpy.linspace( self.scan_range[0], self.scan_range[1], num=n_proj, endpoint=True, ) ) self._n_frames = n_flat + n_dark + n_proj with HDF5File(self.output_file, mode="a") as h5f: root_grp = h5f.require_group(self.output_entry) instrument_grp = root_grp.require_group("instrument") detector_grp = instrument_grp.require_group("detector") detector_grp["image_key"] = image_key detector_grp["image_key_control"] = image_key_control sample_grp = root_grp.require_group("sample") sample_grp["rotation_angle"] = rotation_angle sample_grp["rotation_angle"].attrs["unit"] = "degree" self._path_nxtomo_attrs(root_grp) def _read_sample_detector_distance(self): with EntryReader(self.input_root_url) as input_entry: path = "/".join( ("measurement", "instrument", "sample", "setup", "detector_distance") ) dataset = input_entry[path] distance = h5py_read_dataset(dataset) unit = get_dataset_unit( dataset, default=_ureg.meter, from_dataset=f"{dataset.path} (reading sample-detector distance)", ) return distance * unit def _read_energy(self): with EntryReader(self.input_root_url) as input_entry: path = "/".join(("measurement", "instrument", "source", "energy")) dataset = input_entry[path] energy = float(h5py_read_dataset(dataset)) unit = get_dataset_unit( dataset=dataset, default=_ureg.keV, from_dataset=f"{dataset.name} (from reading energy)", ) return energy * unit def _read_x_pixel_size(self): with EntryReader(self.input_root_url) as input_entry: path = "/".join( ("measurement", "instrument", "detector", "actual_pixel_size_x") ) dataset = input_entry[path] pixel_size = float(h5py_read_dataset(dataset)) unit = get_dataset_unit( dataset=dataset, default=_ureg.meter, from_dataset=f"{dataset.name} (from reading x pixel size)", ) return pixel_size * unit def _read_y_pixel_size(self): with EntryReader(self.input_root_url) as input_entry: path = "/".join( ("measurement", "instrument", "detector", "actual_pixel_size_y") ) dataset = input_entry[path] pixel_size = float(h5py_read_dataset(dataset)) unit = get_dataset_unit( dataset=dataset, default=_ureg.meter, from_dataset=f"{dataset.name} (from reading y pixel size)", ) return pixel_size * unit nxtomomill-v2.0.1/nxtomomill/converter/dxfile/tests/000077500000000000000000000000001511430602400227245ustar00rootroot00000000000000nxtomomill-v2.0.1/nxtomomill/converter/dxfile/tests/test_dxfile.py000066400000000000000000000046661511430602400256240ustar00rootroot00000000000000# coding: utf-8 import os import pytest import numpy from nxtomomill import converter from nxtomomill.tests.utils.dxfile import MockDxFile from tomoscan.esrf.scan.nxtomoscan import NXtomoScan from tomoscan.validator import is_valid_for_reconstruction from silx.io.utils import get_data @pytest.mark.parametrize("duplicate_data", (True, False)) def test_dx_file_conversion(tmp_path, duplicate_data): folder = tmp_path / "test_dx_file_conversion" folder.mkdir() dxfile_path = os.path.join(folder, "dxfile.h5") n_projections = 50 n_darks = 2 n_flats = 4 mock = MockDxFile( file_path=dxfile_path, n_projection=n_projections, n_darks=n_darks, n_flats=n_flats, ) output_file = os.path.join(folder, "dxfile.nx") results = converter.from_dx_to_nx( input_file=dxfile_path, output_file=output_file, duplicate_data=duplicate_data, ) assert len(results) == 1 assert os.path.exists(output_file) _, entry = results[0] scan = NXtomoScan(output_file, entry) assert len(scan.projections) == n_projections assert len(scan.darks) == n_darks assert len(scan.flats) == n_flats assert numpy.array(scan.rotation_angle).min() == 0 assert numpy.array(scan.rotation_angle).max() == 180 assert is_valid_for_reconstruction(scan) # check arrays are correctly copied from mock numpy.testing.assert_array_equal(mock.data_dark[0], get_data(scan.darks[0])) numpy.testing.assert_array_equal(mock.data_flat[1], get_data(scan.flats[3])) idx_last_proj = n_projections + n_flats + n_darks - 1 numpy.testing.assert_array_equal( mock.data_proj[-1], get_data(scan.projections[idx_last_proj]) ) assert scan.rotation_angle[0] == 0 # pylint: disable=E1136 assert scan.rotation_angle[5] == 0 # pylint: disable=E1136 assert scan.rotation_angle[6] == 0 # pylint: disable=E1136 assert scan.rotation_angle[-1] == 180 # pylint: disable=E1136 # if overwrite not requested should fail on reprocessing with pytest.raises(OSError): converter.from_dx_to_nx( input_file=dxfile_path, output_file=output_file, duplicate_data=duplicate_data, overwrite=False, ) # if overwrite requested should succeed converter.from_dx_to_nx( input_file=dxfile_path, output_file=output_file, overwrite=True, duplicate_data=duplicate_data, ) nxtomomill-v2.0.1/nxtomomill/converter/edf/000077500000000000000000000000001511430602400210455ustar00rootroot00000000000000nxtomomill-v2.0.1/nxtomomill/converter/edf/__init__.py000066400000000000000000000001651511430602400231600ustar00rootroot00000000000000"""module to convert from spec-EDF to `NXtomo `_""" nxtomomill-v2.0.1/nxtomomill/converter/edf/checks.py000066400000000000000000000045231511430602400226630ustar00rootroot00000000000000import numpy import logging from silx.utils.enum import Enum as _Enum from silx.io.utils import open as open_hdf5 from silx.io.utils import get_data from tomoscan.esrf.scan.nxtomoscan import NXtomoScan from nxtomo.paths.nxtomo import get_paths as get_nexus_paths _logger = logging.getLogger(__name__) __all__ = [ "OUPUT_CHECK", "compare_volumes", ] class OUPUT_CHECK(_Enum): COMPARE_VOLUME = "compare-output-volume" def compare_volumes(edf_volume_as_urls: tuple, hdf5_scan: NXtomoScan) -> tuple: """ :param tuple edf_volume_as_urls: ordered list of the expected volume. contains (DataUrl, bool). first element is the location of the orginal frame, second is a boolean notifying if the frame has been modified or not. If yes then we cannot do the comparaison. :param NXtomoScan hdf5_scan: NXtomo that we want to check the construction. On this function we compare a final volume and it's construction. The idea here is more to prevent from some 'external' issues such as a file behing not accessible or some 'rock in the shoe' """ issues = set() with open_hdf5(hdf5_scan.master_file) as h5f: entry_node = h5f[hdf5_scan.entry] nexus_path_version = entry_node.attrs.get("version", None) nexus_paths = get_nexus_paths(nexus_path_version) detector_dataset = entry_node[nexus_paths.PROJ_PATH] for i_frame, ((url_original, modified), output_frame) in enumerate( zip(edf_volume_as_urls, detector_dataset) ): original_data = get_data(url_original) if modified: _logger.info( f"skip comparaison of frame {i_frame}, expected to be different" ) elif original_data.dtype != detector_dataset.dtype: # TODO: in this case maybe we want to be more 'accommodating' issues.add( f"orignial data and new dataset have different data type ({original_data.dtype} vs {detector_dataset.dtype})" ) elif not numpy.allclose(original_data, output_frame): issues.add( f"difference found on frame {i_frame}. Orignal url is {url_original.path()}" ) return tuple(issues) nxtomomill-v2.0.1/nxtomomill/converter/edf/edfconverter.py000066400000000000000000001321721511430602400241130ustar00rootroot00000000000000# coding: utf-8 """ module to convert from edf to (nexus tomo compliant) .nx """ from __future__ import annotations import logging import os from collections import namedtuple import fabio import h5py import numpy import pint from tqdm import tqdm from silx.io.dictdump import dicttoh5 from silx.utils.deprecation import deprecated from tomoscan.esrf.scan.edfscan import EDFTomoScan from tomoscan.esrf.scan.nxtomoscan import NXtomoScan from tomoscan.esrf.scan.utils import get_parameters_frm_par_or_info from tomoscan.io import HDF5File from nxtomo.paths.nxtomo import get_paths as get_nexus_paths from nxtomo.nxobject.nxdetector import FieldOfView, ImageKey from nxtomo.nxobject.nxsource import SourceType from nxtomomill import utils from nxtomomill.converter.version import LATEST_VERSION from nxtomomill.converter.version import version as converter_version from nxtomomill.models.utils import PathType from nxtomomill.models.edf2nx import EDF2nxModel from nxtomomill.utils.nexus import create_nx_data_group, link_nxbeam_to_root from nxtomomill.settings import Tomo __all__ = [ "EDFFileKeys", "DEFAULT_EDF_KEYS", "edf_to_nx", "from_edf_to_nx", "post_processing_check", "get_byte_order", ] EDF_MOTOR_POS = Tomo.EDF.MOTOR_POS EDF_MOTOR_MNE = Tomo.EDF.MOTOR_MNE EDF_REFS_NAMES = Tomo.EDF.REFS_NAMES EDF_TO_IGNORE = Tomo.EDF.TO_IGNORE EDF_ROT_ANGLE = Tomo.EDF.ROT_ANGLE EDF_DARK_NAMES = Tomo.EDF.DARK_NAMES EDF_X_TRANS = Tomo.EDF.X_TRANS EDF_Y_TRANS = Tomo.EDF.Y_TRANS EDF_Z_TRANS = Tomo.EDF.Z_TRANS EDF_MACHINE_CURRENT = Tomo.EDF.MACHINE_CURRENT _ureg = pint.get_application_registry() _logger = logging.getLogger(__name__) EDFFileKeys = namedtuple( "EDFFileKeys", [ "motor_pos_keys", "motor_mne_keys", "rotation_angle_keys", "x_translation_keys", "y_translation_keys", "z_translation_keys", "to_ignore", "dark_names", "flat_names", "machine_current_keys", ], ) DEFAULT_EDF_KEYS = EDFFileKeys( EDF_MOTOR_POS, EDF_MOTOR_MNE, EDF_ROT_ANGLE, EDF_X_TRANS, EDF_Y_TRANS, EDF_Z_TRANS, EDF_TO_IGNORE, EDF_DARK_NAMES, EDF_REFS_NAMES, EDF_MACHINE_CURRENT, ) _ESTIMATED_PERCENTAGE_TIME_TO_READ_METADATA = 20 # as at the moment we cannot provide a good estimation of how much it takes to read metadata let's estimate it from experience @deprecated(replacement="from_edf_to_nx", since_version="0.9.0") def edf_to_nx( scan: EDFTomoScan, output_file: str, file_extension: str, file_keys: EDFFileKeys = DEFAULT_EDF_KEYS, progress=None, sample_name: str | None = None, title: str | None = None, instrument_name: str | None = None, source_name: str | None = None, source_type: SourceType | None = None, ) -> tuple: """ Convert an edf file to a nexus file. For now duplicate data. :param scan: :param output_file: :param file_extension: :param file_keys: :param progress: :param sample_name: name of the sample :param title: dataset title :param instrument_name: name of the instrument used :param source_name: name of the source (most likely ESRF) :param source_type: type of the source (most likely "Synchrotron X-ray Source") :return: (nexus_file, entry) """ if not isinstance(scan, EDFTomoScan): raise TypeError("scan is expected to be an instance of EDFTomoScan") config = EDF2nxModel( input_folder=scan.path, dataset_basename=scan.dataset_basename, output_file=output_file, file_extension=file_extension, sample_name=sample_name, title=title, instrument_name=instrument_name, source_name=source_name, source_type=source_type, # handle file_keys motor_position_keys=file_keys.motor_pos_keys, motor_mne_keys=file_keys.motor_mne_keys, rotation_angle_keys=file_keys.rotation_angle_keys, x_translation_keys=file_keys.x_translation_keys, y_translation_keys=file_keys.y_translation_keys, z_translation_keys=file_keys.z_translation_keys, machine_current_keys=file_keys.machine_current_keys, patterns_to_ignores=file_keys.to_ignore, dark_names_prefix=file_keys.dark_names, flat_names_prefix=file_keys.flat_names, ) return from_edf_to_nx(config, progress=progress) def from_edf_to_nx(configuration: EDF2nxModel, progress: tqdm | None = None) -> tuple: """ Convert an edf file to a nexus file. For now duplicate data. :param configuration: configuration to use to process the data :param progress: if provided then will be updated with conversion progress :return: (nexus_file, entry) """ if configuration.input_folder is None: raise ValueError("input_folder should be provided") if not os.path.isdir(configuration.input_folder): raise OSError(f"{configuration.input_folder} is not a valid folder path") if configuration.output_file is None: raise ValueError("output_file should be provided") # if we don't duplicate data then we can't delete sources EDF files if not configuration.duplicate_data and configuration.delete_edf_source_files: raise ValueError( "You asked for avoiding data duplication and to delete edf source files. " "Those two options are not compatible. Avoiding data duplication will " "lead to create HDF5Virtual dataset pointing to the edf source files" ) fileout_h5 = utils.get_file_name( file_name=configuration.output_file, extension=configuration.file_extension, check=True, ) with HDF5File(fileout_h5, "w") as h5d: return __process( configuration=configuration, output_grp=h5d, fileout_h5=fileout_h5, progress=progress, ) def post_processing_check(configuration: EDF2nxModel): """ check that conversion made contains the same information as a folder and optionnaly delete edf files """ if configuration.input_folder is None: raise ValueError("input_folder should be provided") if not os.path.isdir(configuration.input_folder): raise OSError(f"{configuration.input_folder} is not a valid folder path") if configuration.output_file is None: raise ValueError("output_file should be provided") if ( configuration.delete_edf_source_files is True and len(configuration.output_checks) == 0 ): raise ValueError( "requested to remove edf files without righting an NXtomo and not doing any check on an existing NXtomo. This is a non sense." ) # if we don't duplicate data then we can't delete sources EDF files if not configuration.duplicate_data and configuration.delete_edf_source_files: raise ValueError( "You asked for avoiding data duplication and to delete edf source files. " "Those two options are not compatible. Avoiding data duplication will " "lead to create HDF5Virtual dataset pointing to the edf source files" ) fileout_h5 = utils.get_file_name( file_name=configuration.output_file, extension=configuration.file_extension, check=True, ) return __process( configuration=configuration, fileout_h5=fileout_h5, output_grp=None, progress=None, ) def __process( configuration: EDF2nxModel, output_grp: h5py.Group | None, fileout_h5: str, progress: tqdm | None, ): """ This function is used to insure processing is exactly the same: * if we want to convert edf to .nx * if we want to check a conversion from edf to edf (and optionally remove edf afterwards) Not very nice but the simpler for making evolve some legacy code... """ if progress is not None: progress.set_postfix_str( "preprocessing - retrieve all metadata (can take a few seconds - cannot display real advancement)" ) progress.total = 100 progress.n = _ESTIMATED_PERCENTAGE_TIME_TO_READ_METADATA progress.refresh() if configuration.dataset_info_file is not None: if not os.path.isfile(configuration.dataset_info_file): raise ValueError(f"{configuration.dataset_info_file} is not a file") else: scan_info = get_parameters_frm_par_or_info(configuration.dataset_info_file) else: scan_info = None scan = EDFTomoScan( scan=configuration.input_folder, dataset_basename=configuration.dataset_basename, scan_info=scan_info, # TODO: add n frames ? ) _logger.info(f"Output file will be {fileout_h5}") if scan.dim_2 is None or scan.dim_1 is None: if scan_info is not None: raise ValueError("Please provide scan 'dim_1' and 'dim_2' with scan info") else: info_file = os.path.join( configuration.input_folder, f"{configuration.dataset_basename}.info" ) raise ValueError( f"Unable to deduce scan dimension from scan metadata (used file for metadata: {info_file})" ) default_current = scan.retrieve_information( scan=scan.path, dataset_basename=scan.dataset_basename, ref_file=None, key="SrCurrent", key_aliases=["SRCUR", "machineCurrentStart"], type_=float, scan_info=scan.scan_info, ) if default_current is None: _logger.warning("Unable to find machine current default current. Take 0.0") default_current = 0.0 output_data_path = "entry" # with today design we can simply remove the files of the url. We don't expect those files # to contain other type of data edf_source_files = dict() # for each file register a tuple (existing_frame, set(url)) so at the end we can make sure all the frame have been used before # removing the file detector_data_urls = [] # store the url in the order they are used and if it has been modified or not (for output checks). def update_edf_source_files(url, n_frames): if configuration.delete_edf_source_files: # edf_source files is only used for deleting edf files edf_file_path = url.file_path() file_source_info = edf_source_files.get(edf_file_path, (n_frames, set())) urls_used = file_source_info[1] urls_used.add(url.path()) edf_source_files[edf_file_path] = ( file_source_info[0], urls_used, ) DARK_ACCUM_FACT = True metadata = [] proj_urls = scan.get_proj_urls( scan=scan.path, dataset_basename=scan.dataset_basename ) for dark_to_find in configuration.dark_names_prefix: dk_urls = scan.get_darks_url(scan_path=scan.path, prefix=dark_to_find) if len(dk_urls) > 0: if dark_to_find == "dark": DARK_ACCUM_FACT = False break _edf_to_ignore = list(configuration.patterns_to_ignores) for refs_to_find in configuration.flat_names_prefix: if refs_to_find == "ref": _edf_to_ignore.append("HST") elif "HST" in _edf_to_ignore: _edf_to_ignore.remove("HST") refs_urls = scan.get_flats_url( scan_path=scan.path, prefix=refs_to_find, ignore=_edf_to_ignore, dataset_basename=scan.dataset_basename, ) if len(refs_urls) > 0: break n_frames = len(proj_urls) + len(refs_urls) + len(dk_urls) n_darks = len(dk_urls) ( frame_type, rot_angle_index, x_trans_index, y_trans_index, z_trans_index, srcur_index, ) = _getExtraInfo(scan=scan, configuration=configuration) if rot_angle_index == -1 and configuration.force_angle_calculation is False: _logger.warning( f"Unable to find one of the defined key for rotation in header ({configuration.rotation_angle_keys}). Will force angle calculation" ) configuration.force_angle_calculation = True if not output_grp: # in the case we don't intend to write the NXtomo but only check it output_grp = None ext_datasets_grp = None dark_dataset = None data_dataset = None keys_dataset = None keys_control_dataset = None x_dataset = y_dataset = z_dataset = None rotation_dataset = None machine_current_dataset = None else: if configuration.duplicate_data is True: data_dataset = output_grp.create_dataset( "/entry/instrument/detector/data", shape=(n_frames, scan.dim_2, scan.dim_1), dtype=frame_type, ) ext_datasets_grp = None dark_dataset = None else: _logger.warning( "No data duplication requested. Will fail if your dataset contains compressed data" ) # if necessary create external datasets groups # note: for now we are force to create one dataset per frame because # the byte order can be modified from one frame to the other. ext_datasets_grp = output_grp.create_group("external_datasets") data_dataset = None # we must duplicate darks not matter what because exposure time is not handled # at nabu side for the moment dark_dataset = ext_datasets_grp.create_dataset( "darks", shape=(n_darks, scan.dim_2, scan.dim_1), dtype=frame_type, ) keys_dataset = output_grp.create_dataset( "/entry/instrument/detector/image_key", shape=(n_frames,), dtype=numpy.int32 ) keys_control_dataset = output_grp.create_dataset( "/entry/instrument/detector/image_key_control", shape=(n_frames,), dtype=numpy.int32, ) title = configuration.title if title is None: title = os.path.basename(scan.path) output_grp["/entry/title"] = title sample_name = configuration.sample_name if configuration.sample_name is None: # try to deduce sample name from scan path. try: sample_name = os.path.abspath(scan.path).split(os.sep)[-3:] sample_name = os.sep.join(sample_name) except Exception: sample_name = "unknow" output_grp["/entry/sample/name"] = sample_name if configuration.instrument_name is not None: instrument_grp = output_grp["/entry"].require_group("instrument") instrument_grp["name"] = configuration.instrument_name if configuration.source_name is not None: source_grp = output_grp["/entry/instrument"].require_group("source") source_grp["name"] = configuration.source_name if configuration.source_type is not None: source_grp = output_grp["/entry/instrument"].require_group("source") source_grp["type"] = configuration.source_type.value if configuration.source_probe is not None: source_grp = output_grp["/entry/instrument"].require_group("source") source_grp["probe"] = configuration.source_probe.value if scan.scan_range is None or scan.tomo_n is None: raise ValueError( f"Cannot find scan_range ({scan.scan_range}) and / or tomo_n ({scan.tomo_n}). Is the .info file here and / or valid ?" ) distance = scan.retrieve_information( scan=os.path.abspath(scan.path), dataset_basename=scan.dataset_basename, ref_file=None, key="Distance", type_=float, key_aliases=["distance"], scan_info=scan.scan_info, ) if distance is not None: output_grp["/entry/instrument/detector/distance"] = ( (distance * configuration.sample_detector_distance_unit) .to(_ureg.meter) .magnitude ) output_grp["/entry/instrument/detector/distance"].attrs["units"] = str( (_ureg.meter) ) pixel_size = scan.retrieve_information( scan=os.path.abspath(scan.path), dataset_basename=scan.dataset_basename, ref_file=None, key="PixelSize", type_=float, key_aliases=["pixelSize"], scan_info=scan.scan_info, ) output_grp["/entry/instrument/detector/x_pixel_size"] = ( (pixel_size * configuration.pixel_size_unit).to(_ureg.meter).magnitude ) output_grp["/entry/instrument/detector/x_pixel_size"].attrs["units"] = str( str(_ureg.meter) ) output_grp["/entry/instrument/detector/y_pixel_size"] = ( (pixel_size * configuration.pixel_size_unit).to(_ureg.meter).magnitude ) output_grp["/entry/instrument/detector/y_pixel_size"].attrs["units"] = str( str(_ureg.meter) ) energy = scan.retrieve_information( scan=os.path.abspath(scan.path), dataset_basename=scan.dataset_basename, ref_file=None, key="Energy", type_=float, key_aliases=["energy"], scan_info=scan.scan_info, ) if energy is not None: energy = energy * configuration.energy_unit output_grp["/entry/instrument/beam/incident_energy"] = energy.to( _ureg.keV ).magnitude output_grp["/entry/instrument/beam/incident_energy"].attrs["units"] = str( _ureg.keV ) # rotations values rotation_dataset = output_grp.create_dataset( "/entry/sample/rotation_angle", shape=(n_frames,), dtype=numpy.float32 ) output_grp["/entry/sample/rotation_angle"].attrs["units"] = str(_ureg.degree) # machine current nexus_paths = get_nexus_paths(LATEST_VERSION) machine_current_path = "/".join(["entry", nexus_paths.ELECTRIC_CURRENT_PATH]) machine_current_dataset = output_grp.create_dataset( machine_current_path, shape=(n_frames,), dtype=numpy.float32, ) output_grp[machine_current_path].attrs["units"] = str(_ureg.ampere) # provision for centering motors # warning: x_dataset, y_dataset and z_dataset are from the esrf reference. # from NXtomo reference this is z_translation, x_translation and y_translation x_dataset = output_grp.create_dataset( "/entry/sample/z_translation", shape=(n_frames,), dtype=numpy.float32 ) output_grp["/entry/sample/z_translation"].attrs["units"] = str(_ureg.meter) y_dataset = output_grp.create_dataset( "/entry/sample/x_translation", shape=(n_frames,), dtype=numpy.float32 ) output_grp["/entry/sample/x_translation"].attrs["units"] = str(_ureg.meter) z_dataset = output_grp.create_dataset( "/entry/sample/y_translation", shape=(n_frames,), dtype=numpy.float32 ) output_grp["/entry/sample/y_translation"].attrs["units"] = str(_ureg.meter) # ---------> and now fill all datasets! nf = 0 external_datasets = [] # collect all urls created progress_v = 0 if progress is not None: progress.set_postfix_str("write dark") def ignore(file_name): for forbid in _edf_to_ignore: if forbid in file_name: return True return False # darks # dark in acumulation mode? norm_dark = 1.0 if scan.dark_n > 0 and DARK_ACCUM_FACT is True: norm_dark = len(dk_urls) / scan.dark_n dk_indexes = sorted(dk_urls.keys()) for dk_index in dk_indexes: dk_url = dk_urls[dk_index] if ignore(os.path.basename(dk_url.file_path())): _logger.info("ignore " + dk_url.file_path()) continue data, header, external_dataset, n_frames_in_file = _read_url( url=dk_url, i_frame=dk_index, h5group_to_dump=None, # never create external dataset for dark frame_prefix="dark", configuration=configuration, ) assert header is not None metadata.append(header) update_edf_source_files(url=dk_url, n_frames=n_frames_in_file) detector_data_urls.append((dk_url, True)) if output_grp: if configuration.duplicate_data: data_dataset[nf, :, :] = data * norm_dark else: dark_dataset[nf, :, :] = data * norm_dark keys_dataset[nf] = ImageKey.DARK_FIELD.value keys_control_dataset[nf] = ImageKey.DARK_FIELD.value motor_pos_key = _get_valid_key(header, configuration.motor_position_keys) if motor_pos_key: str_mot_val = header[motor_pos_key].split(" ") if rot_angle_index == -1 or configuration.force_angle_calculation: rotation_dataset[nf] = 0.0 else: rotation_dataset[nf] = float(str_mot_val[rot_angle_index]) if x_trans_index == -1: x_dataset[nf] = 0.0 else: x_dataset[nf] = ( ( float(str_mot_val[x_trans_index]) * configuration.x_translation_unit ) .to(_ureg.meter) .magnitude ) if y_trans_index == -1: y_dataset[nf] = 0.0 else: y_dataset[nf] = ( ( float(str_mot_val[y_trans_index]) * configuration.y_translation_unit ) .to(_ureg.meter) .magnitude ) if z_trans_index == -1: z_dataset[nf] = 0.0 else: z_dataset[nf] = ( ( float(str_mot_val[z_trans_index]) * configuration.z_translation_unit ) .to(_ureg.meter) .magnitude ) if srcur_index == -1: machine_current_dataset[nf] = ( (default_current * configuration.machine_current_unit) .to(_ureg.ampere) .magnitude ) else: try: str_counter_val = header.get("counter_pos", "").split(" ") machine_current_dataset[nf] = ( ( float(str_counter_val[srcur_index]) * configuration.machine_current_unit ) .to(_ureg.ampere) .magnitude ) except IndexError: machine_current_dataset[nf] = ( (default_current * configuration.machine_current_unit) .to(_ureg.ampere) .magnitude ) nf += 1 if progress is not None: progress_v += 1 / (n_frames - 1) progress.n = ( _ESTIMATED_PERCENTAGE_TIME_TO_READ_METADATA + progress_v * (100 - _ESTIMATED_PERCENTAGE_TIME_TO_READ_METADATA) ) progress.refresh() ref_indexes = sorted(refs_urls.keys()) ref_projs = [] for irf in ref_indexes: pjnum = int(irf) if pjnum not in ref_projs: ref_projs.append(pjnum) # refs def store_refs( refIndexes, projnum, refUrls, nF, dataDataset, keysDataset, keysCDataset, xDataset, yDataset, zDataset, rotationDataset, raix, xtix, ytix, ztix, ): nfr = nF for ref_index in refIndexes: int_rf = int(ref_index) if int_rf == projnum: refUrl = refUrls[ref_index] if ignore(os.path.basename(refUrl.file_path())): _logger.info("ignore " + refUrl.file_path()) continue data, header, external_data, n_frames_in_file = _read_url( url=refUrl, i_frame=ref_index, h5group_to_dump=ext_datasets_grp, frame_prefix="flat", configuration=configuration, ) update_edf_source_files(url=refUrl, n_frames=n_frames_in_file) if external_data is not None: external_datasets.append(external_data) metadata.append(header) detector_data_urls.append((refUrl, False)) if output_grp is None: continue if configuration.duplicate_data and dataDataset: dataDataset[nfr, :, :] = data keysDataset[nfr] = ImageKey.FLAT_FIELD.value keysCDataset[nfr] = ImageKey.FLAT_FIELD.value motor_pos_key = _get_valid_key( header, configuration.motor_position_keys ) if motor_pos_key in header: str_mot_val = header[motor_pos_key].split(" ") if raix == -1 or configuration.force_angle_calculation: rotationDataset[nfr] = 0.0 else: rotationDataset[nfr] = float(str_mot_val[raix]) if xtix == -1: xDataset[nfr] = 0.0 else: xDataset[nfr] = ( ( float(str_mot_val[xtix]) * configuration.x_translation_unit ) .to(_ureg.meter) .magnitude ) if ytix == -1: yDataset[nfr] = 0.0 else: yDataset[nfr] = ( ( float(str_mot_val[ytix]) * configuration.y_translation_unit ) .to(_ureg.meter) .magnitude ) if ztix == -1: zDataset[nfr] = 0.0 else: zDataset[nfr] = ( ( float(str_mot_val[ztix]) * configuration.z_translation_unit ) .to(_ureg.meter) .magnitude ) if srcur_index == -1: machine_current_dataset[nfr] = ( (default_current * configuration.machine_current_unit) .to(_ureg.ampere) .magnitude ) else: str_counter_val = header.get("counter_pos", "").split(" ") try: machine_current_dataset[nfr] = ( ( float(str_counter_val[srcur_index]) * configuration.machine_current_unit ) .to(_ureg.ampere) .magnitude ) except IndexError: machine_current_dataset[nfr] = ( (default_current * configuration.machine_current_unit) .to(_ureg.ampere) .magnitude ) nfr += 1 return nfr # projections proj_indexes = sorted(proj_urls.keys()) if progress is not None: progress.set_postfix_str("write projections and flats") nproj = 0 iref_pj = 0 if configuration.force_angle_calculation: if configuration.angle_calculation_rev_neg_scan_range and scan.scan_range < 0: proj_angles = numpy.linspace( 0, scan.scan_range, scan.tomo_n, endpoint=configuration.angle_calculation_endpoint, ) else: proj_angles = numpy.linspace( min(0, scan.scan_range), max(0, scan.scan_range), scan.tomo_n, endpoint=configuration.angle_calculation_endpoint, ) revert_angles_in_nx = ( configuration.angle_calculation_rev_neg_scan_range and (scan.scan_range < 0) ) if revert_angles_in_nx: proj_angles = proj_angles[::-1] alignment_indices = [] for i_proj, proj_index in enumerate(proj_indexes): proj_url = proj_urls[proj_index] if ignore(os.path.basename(proj_url.file_path())): _logger.info("ignore " + proj_url.file_path()) continue # store refs if the ref serial number is = projection number if iref_pj < len(ref_projs) and ref_projs[iref_pj] == nproj: nf = store_refs( ref_indexes, ref_projs[iref_pj], refs_urls, nf, data_dataset, keys_dataset, keys_control_dataset, x_dataset, y_dataset, z_dataset, rotation_dataset, rot_angle_index, x_trans_index, y_trans_index, z_trans_index, ) iref_pj += 1 data, header, external_data, n_frames_in_file = _read_url( proj_url, i_frame=proj_index, h5group_to_dump=ext_datasets_grp, frame_prefix="proj", configuration=configuration, ) update_edf_source_files(url=proj_url, n_frames=n_frames_in_file) detector_data_urls.append((proj_url, False)) if output_grp: if external_data is not None: external_datasets.append(external_data) metadata.append(header) if configuration.duplicate_data: data_dataset[nf, :, :] = data keys_dataset[nf] = ImageKey.PROJECTION.value keys_control_dataset[nf] = ImageKey.PROJECTION.value if nproj >= scan.tomo_n: keys_control_dataset[nf] = ImageKey.ALIGNMENT.value motor_pos_key = _get_valid_key(header, configuration.motor_position_keys) if configuration.force_angle_calculation: if i_proj < len(proj_angles): rotation_dataset[nf] = proj_angles[i_proj] else: # case of return / control projection rotation_dataset[nf] = 0.0 if motor_pos_key in header: str_mot_val = header[motor_pos_key].split(" ") # continuous scan - rot angle is unknown. Compute it if not configuration.force_angle_calculation: rotation_dataset[nf] = float(str_mot_val[rot_angle_index]) if x_trans_index == -1: x_dataset[nf] = 0.0 else: x_dataset[nf] = ( ( float(str_mot_val[x_trans_index]) * configuration.x_translation_unit ) .to(_ureg.meter) .magnitude ) if y_trans_index == -1: y_dataset[nf] = 0.0 else: y_dataset[nf] = ( ( float(str_mot_val[y_trans_index]) * configuration.y_translation_unit ) .to(_ureg.meter) .magnitude ) if z_trans_index == -1: z_dataset[nf] = 0.0 else: z_dataset[nf] = ( ( float(str_mot_val[z_trans_index]) * configuration.z_translation_unit ) .to(_ureg.meter) .magnitude ) if srcur_index == -1: machine_current_dataset[nf] = ( (default_current * configuration.machine_current_unit) .to(_ureg.ampere) .magnitude ) else: try: str_counter_val = header.get("counter_pos", "").split(" ") machine_current_dataset[nf] = ( ( float(str_counter_val[srcur_index]) * configuration.machine_current_unit ) .to(_ureg.ampere) .magnitude ) except IndexError: machine_current_dataset[nf] = ( (default_current * configuration.machine_current_unit) .to(_ureg.ampere) .magnitude ) nf += 1 nproj += 1 if progress is not None: progress_v += 1 / (n_frames - 1) progress.n = _ESTIMATED_PERCENTAGE_TIME_TO_READ_METADATA + progress_v * ( 100 - _ESTIMATED_PERCENTAGE_TIME_TO_READ_METADATA ) progress.refresh() # we need to update alignement angles values. I wanted to avoid to redo all the previous existing processing. n_alignment_angles = len(alignment_indices) if n_alignment_angles == 3: alignments_angles = numpy.linspace( scan.scan_range, 0, n_alignment_angles, endpoint=(n_alignment_angles % 2) == 0, ) for index, angle in zip(alignment_indices, alignments_angles): rotation_dataset[index] = angle # store last flat if any remaining in the list if iref_pj < len(ref_projs) and output_grp: nf = store_refs( ref_indexes, ref_projs[iref_pj], refs_urls, nf, data_dataset, keys_dataset, keys_control_dataset, x_dataset, y_dataset, z_dataset, rotation_dataset, rot_angle_index, x_trans_index, y_trans_index, z_trans_index, ) if output_grp: # if we avoided data duplication: create the virtual dataset on top of the external datasets if not configuration.duplicate_data: virtual_layout = h5py.VirtualLayout( shape=(n_frames, scan.dim_2, scan.dim_1), dtype=frame_type, ) virtual_layout[0:n_darks] = h5py.VirtualSource(dark_dataset) for i_ext, external_dataset in enumerate(external_datasets): assert isinstance(external_dataset, h5py.Dataset) virtual_layout[i_ext + n_darks] = h5py.VirtualSource(external_dataset) output_grp.create_virtual_dataset( "/entry/instrument/detector/data", virtual_layout, ) # we can add some more NeXus look and feel output_grp["/entry"].attrs["NX_class"] = "NXentry" output_grp["/entry"].attrs["definition"] = "NXtomo" output_grp["/entry"].attrs["version"] = converter_version() output_grp["/entry/instrument"].attrs["NX_class"] = "NXinstrument" output_grp["/entry/instrument/detector"].attrs["NX_class"] = "NXdetector" output_grp["/entry/instrument/detector/data"].attrs["interpretation"] = "image" if configuration.field_of_view is not None: field_of_view = configuration.field_of_view elif abs(scan.scan_range) == 180: field_of_view = "Full" elif abs(scan.scan_range) == 360: field_of_view = "Half" if field_of_view is not None: field_of_view = FieldOfView(field_of_view) output_grp["/entry/instrument/detector/field_of_view"] = field_of_view.value output_grp["/entry/sample"].attrs["NX_class"] = "NXsample" output_grp["/entry/definition"] = "NXtomo" source_grp = output_grp["/entry/instrument"].get("source", None) if source_grp is not None and "NX_class" not in source_grp.attrs: source_grp.attrs["NX_class"] = "NXsource" output_grp.flush() for i_meta, meta in enumerate(metadata): # save metadata dicttoh5( meta, fileout_h5, h5path=f"/entry/instrument/positioners/{i_meta}", update_mode="replace", mode="a", ) try: create_nx_data_group( file_path=fileout_h5, entry_path=output_data_path, axis_scale=["linear", "linear"], ) except Exception as e: _logger.error(f"Fail to create NXdata group. Reason is {e}") # create beam group at root for compatibility try: link_nxbeam_to_root(file_path=fileout_h5, entry_path=output_data_path) except Exception as e: _logger.error(f"Fail to link nx beam. Error is {e}") if progress is not None: print("\nconversion finished\n") if output_grp is not None: # if the output group is provided then close it so we can safely read it back # it should be done outside this function but this is legacy code that won't evolve output_grp.close() # do some check on the conversion issues_discovered = [] for output_check in configuration.output_checks: print("\nstart output checks\n") from nxtomomill.converter.edf import checks as _checks_utils output_check = _checks_utils.OUPUT_CHECK(output_check) if output_check is _checks_utils.OUPUT_CHECK.COMPARE_VOLUME: issues = _checks_utils.compare_volumes( edf_volume_as_urls=detector_data_urls, hdf5_scan=NXtomoScan(fileout_h5, output_data_path), ) else: raise ValueError(f"{output_check} is not handled") issues_discovered.extend(issues) if len(issues_discovered) > 0: raise ValueError("Seems the conversion failed. Detected issues are {errs}") elif len(configuration.output_checks) > 0: print("\noutput checks done, no issues detected\n") if configuration.delete_edf_source_files: print("start edf source files removal") assert ( configuration.duplicate_data ), "if data is not duplicated this make no sense to remove the edf files" for file_path, (n_frames, used_urls) in edf_source_files.items(): if n_frames > len(used_urls): _logger.warning( f"Will not delete {file_path}. Only {len(used_urls)} used on the {n_frames} contained" ) else: try: os.remove(file_path) except OSError as e: _logger.error(f"Issue when removing {file_path}. Error is {e}") return fileout_h5, output_data_path def _get_valid_key(header: dict, keys: tuple) -> str | None: """Return the first existing key in header""" for key in keys: if key in header: return key else: return None def _get_valid_key_index(motors: list, keys: tuple) -> int | None: for key in keys: if key in motors: return motors.index(key) else: return -1 def _read_url( url, h5group_to_dump: h5py.Group, i_frame: int, frame_prefix: str, configuration: EDF2nxModel, ) -> tuple: """ if h5group_to_dump is provided then it will create a dataset with external data to the EDF file under the name `frame_{i}` This way we will be able to create a virtual """ data_slice = url.data_slice() if data_slice is None: data_slice = (0,) if data_slice is None or len(data_slice) != 1: raise ValueError(f"Fabio slice expect a single frame but {data_slice} found") index = data_slice[0] if not isinstance(index, int): raise ValueError(f"Fabio slice expect a single integer but {data_slice} found") try: fabio_file = fabio.open(url.file_path()) except Exception: _logger.debug( f"Error while opening {url.file_path()} with fabio", exc_info=True ) raise IOError( f"Error while opening {url.path()} with fabio (use debug for more information)" ) try: n_frames_in_file = fabio_file.nframes if n_frames_in_file == 1: if index != 0: raise ValueError( f"Only a single frame available. Slice {index} out of range" ) it = fabio.edfimage.EdfImage.lazy_iterator(url.file_path()) frame = next(it) else: frame = fabio_file.getframe(index) data = frame.data header = frame.header frame_start_offset = frame.start blobsize = frame.blobsize byte_order = get_byte_order(frame.header) except Exception as e: _logger.error(e) data = None header = None frame_start_offset = None blobsize = None byte_order = None finally: fabio_file.close() fabio_file = None if data is not None and byte_order is None: raise ValueError("Unable to get ByteOrder for file_path") if h5group_to_dump is not None and data is not None: frame_start = frame_start_offset + blobsize - (data.size * data.dtype.itemsize) frame_end = frame_start + blobsize dataset_name = f"{frame_prefix}_{i_frame}" data_type = numpy.dtype(byte_order + data.dtype.char) if configuration.external_link_type is PathType.ABSOLUTE: file_path = os.path.abspath(url.file_path()) elif configuration.external_link_type is PathType.RELATIVE: file_path = os.path.abspath(url.file_path()) file_path = os.path.relpath( os.path.abspath(file_path), os.path.abspath(os.path.dirname(configuration.output_file)), ) file_path = "./" + file_path else: raise NotImplementedError external_dataset = h5group_to_dump.create_dataset( name=dataset_name, shape=data.shape, dtype=data_type, external=[ (file_path, frame_start, frame_end), ], ) else: external_dataset = None return data, header, external_dataset, n_frames_in_file def _getExtraInfo(scan, configuration): assert isinstance(scan, EDFTomoScan) projections_urls = scan.projections if len(projections_urls) == 0: raise ValueError( f"No projections found in {scan.path} with dataset basename: {configuration.dataset_basename if configuration.dataset_basename is not None else 'Default'} and dataset info file: {configuration.dataset_info_file if configuration.dataset_info_file is not None else 'Default'}. " ) indexes = sorted(projections_urls.keys()) first_proj_file = projections_urls[indexes[0]] fid = fabio.open(first_proj_file.file_path()) rotangle_index = -1 xtrans_index = -1 ytrans_index = -1 ztrans_index = -1 srcur_index = -1 frame_type = None try: if hasattr(fid, "header"): hd = fid.header else: hd = fid.getHeader() motor_mne_key = _get_valid_key(hd, configuration.motor_mne_keys) motors = hd.get(motor_mne_key, "").split(" ") counters = hd.get("counter_mne", "").split(" ") rotangle_index = _get_valid_key_index(motors, configuration.rotation_angle_keys) xtrans_index = _get_valid_key_index(motors, configuration.x_translation_keys) ytrans_index = _get_valid_key_index(motors, configuration.y_translation_keys) ztrans_index = _get_valid_key_index(motors, configuration.z_translation_keys) srcur_index = _get_valid_key_index(counters, configuration.machine_current_keys) if hasattr(fid, "bytecode"): frame_type = fid.bytecode else: frame_type = fid.getByteCode() finally: fid.close() fid = None return ( frame_type, rotangle_index, xtrans_index, ytrans_index, ztrans_index, srcur_index, ) def get_byte_order(header): """ byte_order (as a str compatible with numpy data types) """ byte_order = header.get("ByteOrder", None) if byte_order is None: pass elif byte_order.lower() == "highbytefirst": byte_order = ">" elif byte_order.lower() == "lowbytefirst": byte_order = "<" return byte_order nxtomomill-v2.0.1/nxtomomill/converter/edf/tests/000077500000000000000000000000001511430602400222075ustar00rootroot00000000000000nxtomomill-v2.0.1/nxtomomill/converter/edf/tests/test_checks.py000066400000000000000000000050351511430602400250630ustar00rootroot00000000000000import os import numpy from fabio.edfimage import EdfImage from silx.io.url import DataUrl from nxtomomill.converter.edf.checks import compare_volumes from tomoscan.esrf.scan.nxtomoscan import NXtomoScan from tomoscan.io import HDF5File def test_compare_volumes(tmp_path): """test the compare_volumes function""" input_folder = tmp_path / "input" input_folder.mkdir() output_folder = tmp_path / "output" output_folder.mkdir() frame_1 = ( numpy.arange(10, 110, dtype=numpy.float32) .reshape(1, 10, 10) .astype(numpy.float32) ) frame_2 = ( numpy.arange(30, 130, dtype=numpy.float32) .reshape(1, 10, 10) .astype(numpy.float32) ) frame_3 = ( numpy.arange(100, 200, dtype=numpy.float32) .reshape(1, 10, 10) .astype(numpy.float32) ) edf_frame_files = [] edf_frame_urls = [] for i_frame, frame in enumerate((frame_1, frame_2, frame_3)): edf_writer = EdfImage( data=frame[0], # [0]: easy way to concatenate the frame later header={}, ) edf_frame_file = os.path.join(input_folder, f"frame_{i_frame}.edf") edf_writer.write(edf_frame_file) edf_frame_files.append(edf_frame_file) edf_frame_urls.append( [ DataUrl(file_path=edf_frame_file, scheme="fabio"), False, ] ) scan_0_file = os.path.join(output_folder, "nx_tomo.nx") with HDF5File(scan_0_file, mode="w") as h5f: h5f["entry/instrument/detector/data"] = numpy.vstack( [frame_1, frame_2, frame_3] ) assert len(compare_volumes(edf_frame_urls, NXtomoScan(scan_0_file, "entry"))) == 0 # test inverting a frame scan_1_file = os.path.join(output_folder, "toto_tomo.nx") with HDF5File(scan_1_file, mode="w") as h5f: h5f["entry/instrument/detector/data"] = numpy.vstack( [frame_1, frame_1, frame_3] ) assert len(compare_volumes(edf_frame_urls, NXtomoScan(scan_1_file, "entry"))) == 1 # then ignore second frame edf_frame_urls[1][1] = True assert len(compare_volumes(edf_frame_urls, NXtomoScan(scan_1_file, "entry"))) == 0 # test modifying the type scan_2_file = os.path.join(output_folder, "nx_tomo32.nx") with HDF5File(scan_2_file, mode="w") as h5f: h5f["entry/instrument/detector/data"] = numpy.vstack( [frame_1, frame_2, frame_3] ).astype(numpy.uint16) assert len(compare_volumes(edf_frame_urls, NXtomoScan(scan_2_file, "entry"))) == 1 nxtomomill-v2.0.1/nxtomomill/converter/edf/tests/test_edf2nx.py000066400000000000000000000366601511430602400250210ustar00rootroot00000000000000# coding: utf-8 import os import shutil import tempfile from glob import glob import numpy import pytest from tqdm import tqdm from tomoscan import version from tomoscan.esrf.mock import MockEDF from tomoscan.esrf.scan.edfscan import EDFTomoScan from tomoscan.esrf.scan.nxtomoscan import NXtomoScan from tomoscan.esrf.scan.utils import cwd_context, dump_info_file from tomoscan.validator import is_valid_for_reconstruction from tomoscan.io import HDF5File, get_swmr_mode from nxtomomill import converter from nxtomomill.converter.edf.checks import OUPUT_CHECK from nxtomomill.models.edf2nx import EDF2nxModel @pytest.mark.parametrize( "duplicate_data, external_link_type", ((False, "absolute"), (False, "relative"), (True, "absolute")), ) @pytest.mark.parametrize("progress", (None, tqdm(desc="conversion from edf"))) @pytest.mark.skipif( condition=(version.MINOR < 7 and version.MAJOR == 0), reason="dark_n and ref_n from EDFTomoScan where not existing", ) def test_edf_to_nx_converter(tmp_path, progress, duplicate_data, external_link_type): folder = tmp_path scan_path = os.path.join(folder, "myscan") n_proj = 120 n_alignment_proj = 5 dim = 100 MockEDF( scan_path=scan_path, n_radio=n_proj, n_ini_radio=n_proj, n_extra_radio=n_alignment_proj, dim=dim, dark_n=1, flat_n=1, distance=2.3, ) scan = EDFTomoScan(scan_path) assert scan.dark_n == 1 output_file = os.path.join(folder, "nexus_file.nx") config = EDF2nxModel() config.input_folder = scan_path config.output_file = output_file config.duplicate_data = duplicate_data config.external_link_type = external_link_type config.x_translation_unit = "cm" config.y_translation_unit = "cm" config.z_translation_unit = "m" config.sample_detector_distance_unit = "cm" nx_file, nx_entry = converter.from_edf_to_nx( configuration=config, progress=progress, ) hdf5_scan = NXtomoScan(scan=nx_file, entry=nx_entry) assert len(hdf5_scan.projections) == n_proj assert len(hdf5_scan.alignment_projections) == n_alignment_proj assert hdf5_scan.dim_1 == dim assert hdf5_scan.dim_2 == dim assert is_valid_for_reconstruction(hdf5_scan) # insure links are valid in case of external # for relative links it looks we still need to be on the file working directory to solve links # see policy details here: https://docs.hdfgroup.org/hdf5/v1_12/group___h5_l.html#title5 # this will work with the tomotools suite as tomoscan will automatically create a working dircetory context when reading the file. # but this can be an issue when sharing data or reading data outside the tomotools suite with cwd_context(output_file): with HDF5File(output_file, mode="r", swmr=get_swmr_mode()) as h5f: detector_data_path = f"/{nx_entry}/instrument/detector/data" assert ( f"/{nx_entry}/instrument/detector/data" in h5f ), f"/{nx_entry}/instrument/detector/data doesn't exists" assert h5f[detector_data_path].is_virtual == (not duplicate_data) assert ( h5f[detector_data_path][()].min() != h5f[detector_data_path][()].max() ) numpy.testing.assert_almost_equal( h5f[f"/{nx_entry}/sample/z_translation"][0], 0.0 ) numpy.testing.assert_almost_equal( h5f[f"/{nx_entry}/sample/x_translation"][0], 1.0 / 100.0 ) numpy.testing.assert_almost_equal( h5f[f"/{nx_entry}/sample/y_translation"][0], 2.0 ) # x, y and z are set manually on MockEDF. n_frames = 120 + 5 + 1 + 1 numpy.testing.assert_array_almost_equal( hdf5_scan.x_translation, numpy.array([1.0 / 100.0] * n_frames), ) numpy.testing.assert_array_almost_equal( hdf5_scan.y_translation, numpy.array([2.0] * n_frames) ) numpy.testing.assert_almost_equal(hdf5_scan.distance, 2.3 / 100.0) @pytest.mark.parametrize("scan_range", (-180, 180, 360)) @pytest.mark.parametrize("endpoint", (True, False)) @pytest.mark.parametrize("revert", (True, False)) @pytest.mark.parametrize("force_angles", (True, False)) def test_rotation_angle_infos(scan_range, endpoint, revert, force_angles): """test conversion fits EDF2nxModel parameters regarding the rotation angle calculation options""" with tempfile.TemporaryDirectory() as data_folder: scan_path = os.path.join(data_folder, "myscan") n_proj = 12 n_alignment_proj = 5 dim = 4 MockEDF( scan_path=scan_path, n_radio=n_proj, n_ini_radio=n_proj, n_extra_radio=n_alignment_proj, dim=dim, dark_n=1, ref_n=1, scan_range=scan_range, rotation_angle_endpoint=endpoint, ) output_file = os.path.join(data_folder, "nexus_file.nx") config = EDF2nxModel() config.input_folder = scan_path config.output_file = output_file config.force_angle_calculation = force_angles config.angle_calculation_endpoint = endpoint config.angle_calculation_rev_neg_scan_range = revert nx_file, nx_entry = converter.from_edf_to_nx( configuration=config, ) hdf5_scan = NXtomoScan(scan=nx_file, entry=nx_entry) # compute expected rotation angles raw_rotation_angles = numpy.asarray(hdf5_scan.rotation_angle) converted_rotation_angles = raw_rotation_angles[ hdf5_scan.image_key_control == 0 ] if force_angles and revert and scan_range < 0 and not endpoint: expected_angles = numpy.linspace(0, scan_range, n_proj, endpoint=endpoint) else: expected_angles = numpy.linspace( min(0, scan_range), max(0, scan_range), n_proj, endpoint=endpoint ) revert_angles_in_nx = force_angles and revert and (scan_range < 0) if revert_angles_in_nx: expected_angles = expected_angles[::-1] numpy.testing.assert_almost_equal( converted_rotation_angles, expected_angles, decimal=3 ) # test alignment projection and flat are contained in the projections range dark_angles = raw_rotation_angles[hdf5_scan.image_key_control == 2] for angle in dark_angles: assert angle == 0 or min(converted_rotation_angles) <= angle <= max( converted_rotation_angles ) flat_angles = raw_rotation_angles[hdf5_scan.image_key_control == 1] for angle in flat_angles: assert angle == 0 or min(converted_rotation_angles) <= angle <= max( converted_rotation_angles ) alignment_angles = raw_rotation_angles[hdf5_scan.image_key_control == -1] for angle in alignment_angles: assert angle == 0 or min(converted_rotation_angles) <= angle <= max( converted_rotation_angles ) def test_rot_angle_key_does_not_exists(): """test conversion fits EDF2nxModel parameters regarding the rotation angle calculation options""" with tempfile.TemporaryDirectory() as data_folder: scan_path = os.path.join(data_folder, "myscan") n_proj = 12 n_alignment_proj = 5 dim = 4 MockEDF( scan_path=scan_path, n_radio=n_proj, n_ini_radio=n_proj, n_extra_radio=n_alignment_proj, dim=dim, dark_n=1, ref_n=1, scan_range=180, ) output_file = os.path.join(data_folder, "nexus_file.nx") config = EDF2nxModel() config.input_folder = scan_path config.output_file = output_file config.force_angle_calculation = False config.rotation_angle_keys = tuple() nx_file, nx_entry = converter.from_edf_to_nx( configuration=config, ) hdf5_scan = NXtomoScan(scan=nx_file, entry=nx_entry) # compute expected rotation angles raw_rotation_angles = numpy.asarray(hdf5_scan.rotation_angle) converted_rotation_angles = raw_rotation_angles[ hdf5_scan.image_key_control == 0 ] expected_angles = numpy.linspace( 0, 180, n_proj, endpoint=config.angle_calculation_endpoint ) numpy.testing.assert_almost_equal( converted_rotation_angles, expected_angles, decimal=3 ) def test_different_info_file(): """insure providing a different spec info file will be taken into account""" with tempfile.TemporaryDirectory() as data_folder: scan_path = os.path.join(data_folder, "myscan") n_proj = 12 n_alignment_proj = 0 dim = 4 flat_n = 1 dark_n = 1 original_scan_range = 180 original_energy = 2.3 original_pixel_size = 0.03 original_distance = 0.36 new_scan_range = -180 new_energy = 6.6 new_pixel_size = 0.002 new_distance = 0.458 new_n_proj = n_proj - 2 other_info_file = os.path.join(data_folder, "new_info_file.info") dump_info_file( file_path=other_info_file, tomo_n=new_n_proj, scan_range=new_scan_range, flat_n=flat_n, flat_on=new_n_proj, dark_n=dark_n, dim_1=dim, dim_2=dim, col_beg=0, col_end=dim, row_beg=0, row_end=dim, pixel_size=new_pixel_size, distance=new_distance, energy=new_energy, ) assert os.path.exists(other_info_file) MockEDF( scan_path=scan_path, n_radio=n_proj, n_ini_radio=n_proj, n_extra_radio=n_alignment_proj, dim=dim, dark_n=dark_n, flat_n=flat_n, scan_range=original_scan_range, energy=original_energy, pixel_size=original_pixel_size, distance=original_distance, ) output_file = os.path.join(data_folder, "nexus_file.nx") config = EDF2nxModel() config.input_folder = scan_path config.output_file = output_file config.force_angle_calculation = True config.pixel_size_unit = "m" config.distance_unit = "m" config.energy_unit = "keV" # test step 1: check scan info is correctly read from the original parameters nx_file, nx_entry = converter.from_edf_to_nx( configuration=config, ) hdf5_scan_original_info_file = NXtomoScan(scan=nx_file, entry=nx_entry) assert numpy.isclose(hdf5_scan_original_info_file.energy, original_energy) assert hdf5_scan_original_info_file.scan_range == original_scan_range assert hdf5_scan_original_info_file.pixel_size == original_pixel_size assert hdf5_scan_original_info_file.distance == original_distance assert len(hdf5_scan_original_info_file.projections) == n_proj # test step 2: check scan info is correctly read from new parameters config.dataset_info_file = other_info_file config.overwrite = True nx_file, nx_entry = converter.from_edf_to_nx( configuration=config, ) hdf5_scan_new_info_file = NXtomoScan(scan=nx_file, entry=nx_entry) assert numpy.isclose(hdf5_scan_new_info_file.energy, new_energy) # for now NXtomoScan expect range to be 180 or 360. There is no such -180 as in EDF assert abs(hdf5_scan_new_info_file.scan_range) == abs(new_scan_range) assert hdf5_scan_new_info_file.pixel_size == new_pixel_size assert hdf5_scan_new_info_file.distance == new_distance assert len(hdf5_scan_new_info_file.projections) == new_n_proj def test_different_dataset_basename(): """test conversion succeed if we provide a dataset with a different basename""" with tempfile.TemporaryDirectory() as data_folder: original_scan_path = os.path.join(data_folder, "myscan") new_scan_path = os.path.join(data_folder, "myscan_435") n_proj = 12 n_alignment_proj = 5 dim = 4 energy = 12.35 n_darks = 2 n_flats = 1 MockEDF( scan_path=original_scan_path, n_radio=n_proj, n_ini_radio=n_proj, n_extra_radio=n_alignment_proj, dim=dim, dark_n=n_darks, flat_n=n_flats, energy=energy, ) shutil.move( original_scan_path, new_scan_path, ) output_file = os.path.join(data_folder, "nexus_file.nx") config = EDF2nxModel() config.input_folder = new_scan_path config.output_file = output_file config.dataset_basename = "myscan" nx_file, nx_entry = converter.from_edf_to_nx( configuration=config, ) hdf5_scan = NXtomoScan(scan=nx_file, entry=nx_entry) assert len(hdf5_scan.projections) == n_proj assert len(hdf5_scan.alignment_projections) == n_alignment_proj assert len(hdf5_scan.darks) == n_darks assert len(hdf5_scan.flats) == n_flats assert hdf5_scan.energy == energy def test_delete_edf_input_files(): """ test `delete_edf_source_files` option """ def get_n_edf_file(path): return len(glob(os.path.join(path, "*.edf"))) with tempfile.TemporaryDirectory() as data_folder: input_scan_path = os.path.join(data_folder, "input_scan") MockEDF( scan_path=input_scan_path, n_radio=10, n_ini_radio=10, n_extra_radio=2, dim=25, dark_n=2, flat_n=2, ) assert get_n_edf_file(input_scan_path) == 16 info_file = os.path.join(input_scan_path, "input_scan.info") assert os.path.exists(info_file) config = EDF2nxModel() nx_tomo_path = os.path.join(data_folder, "nxtomo.nx") config.input_folder = input_scan_path config.output_file = nx_tomo_path config.delete_edf_source_files = True config.duplicate_data = False # make sure if we ask for no data duplication and removing source files we raise an error (incoherent settings) with pytest.raises(ValueError): converter.from_edf_to_nx( configuration=config, ) config.duplicate_data = True nx_file, _ = converter.from_edf_to_nx( configuration=config, ) # check conversion went well assert os.path.exists(nx_file) # make sure there is no edf left assert get_n_edf_file(input_scan_path) == 0 # make sure the .info file is still there to assert os.path.exists(info_file) def test_output_checks(): """ test some conversion calling some check at the end of the conversion """ with tempfile.TemporaryDirectory() as data_folder: input_scan_path = os.path.join(data_folder, "input_scan") MockEDF( scan_path=input_scan_path, n_radio=10, n_ini_radio=10, n_extra_radio=2, dim=25, dark_n=2, flat_n=2, ) config = EDF2nxModel() nx_tomo_path = os.path.join(data_folder, "nxtomo.nx") config.input_folder = input_scan_path config.output_file = nx_tomo_path config._output_checks = (OUPUT_CHECK.COMPARE_VOLUME,) converter.from_edf_to_nx( configuration=config, ) nxtomomill-v2.0.1/nxtomomill/converter/edf/tests/test_edf2nx_check.py000066400000000000000000000051051511430602400261440ustar00rootroot00000000000000import os import pytest from glob import glob from tomoscan.esrf.mock import MockEDF from tomoscan.esrf.scan.edfscan import EDFTomoScan from nxtomomill.converter.edf.checks import OUPUT_CHECK from nxtomomill.models.edf2nx import EDF2nxModel from nxtomomill.converter.edf.edfconverter import from_edf_to_nx, post_processing_check def test_edf2nx_check(tmp_path): """ make sure checking an conversion in post processing works """ folder = tmp_path / "test_edf2nx_check" folder.mkdir() scans = [] for scan_name, n_proj in zip(["scanA", "scanB"], [10, 11]): scan_path = os.path.join(folder, scan_name) MockEDF( scan_path=scan_path, n_radio=n_proj, n_ini_radio=n_proj, n_extra_radio=0, dim=20, dark_n=1, flat_n=1, distance=2.3, ) scans.append(EDFTomoScan(scan_path)) # do the conversion of the two files config_1 = EDF2nxModel() config_1.input_folder = scans[0].path config_1.output_file = scans[0].path + ".nx" from_edf_to_nx(config_1) assert os.path.exists(config_1.output_file) config_2 = EDF2nxModel() config_2.input_folder = scans[1].path config_2.output_file = scans[1].path + ".nx" from_edf_to_nx(config_2) assert os.path.exists(config_2.output_file) # check post_processing_check(configuration=config_1) post_processing_check(configuration=config_2) assert os.path.exists(scans[0].path) config_test = EDF2nxModel() config_test.output_checks = (OUPUT_CHECK.COMPARE_VOLUME,) config_test.delete_edf_source_files = True config_test.input_folder = scans[0].path config_test.output_file = scans[1].path + ".nx" assert _get_nb_edf_files(scans[0].path) == 12 assert _get_nb_edf_files(scans[1].path) == 13 with pytest.raises(ValueError): post_processing_check(configuration=config_test) # make sure no edf file have been removed assert _get_nb_edf_files(scans[0].path) == 12 assert _get_nb_edf_files(scans[1].path) == 13 config_test.input_folder = scans[0].path config_test.output_file = scans[0].path + ".nx" post_processing_check(configuration=config_test) assert _get_nb_edf_files(scans[0].path) == 0 assert _get_nb_edf_files(scans[1].path) == 13 config_test.input_folder = scans[1].path config_test.output_file = scans[1].path + ".nx" post_processing_check(configuration=config_test) assert _get_nb_edf_files(scans[1].path) == 0 def _get_nb_edf_files(folder): return len(glob(os.path.join(folder, "*.edf"))) nxtomomill-v2.0.1/nxtomomill/converter/fluo/000077500000000000000000000000001511430602400212545ustar00rootroot00000000000000nxtomomill-v2.0.1/nxtomomill/converter/fluo/__init__.py000066400000000000000000000002031511430602400233600ustar00rootroot00000000000000"""module to convert from Fluo-Tomo (tiff files) to `NXtomo `_""" nxtomomill-v2.0.1/nxtomomill/converter/fluo/blissfluoscan.py000066400000000000000000000140501511430602400244750ustar00rootroot00000000000000"""Scan dedicated for bliss fluo-data format - based on h5 files generated by ewoksfluo""" from __future__ import annotations import logging import pint import numpy from dataclasses import dataclass, field import numpy as np from numpy.typing import NDArray, DTypeLike import h5py _ureg = pint.get_application_registry() _logger = logging.getLogger(__name__) try: import tifffile # noqa #F401 needed for later possible lazy loading except ImportError: has_tifffile = False else: has_tifffile = True __all__ = ["BlissFluoTomoScanBase", "BlissFluoTomoScan3D", "BlissFluoTomoScan2D"] @dataclass class BlissFluoTomoScanBase: """Base class to read XRF data fitted with ewoksfluo.""" ewoksfluo_filename: str detectors: tuple = () dtype: DTypeLike = numpy.float32 verbose: bool = False angles_deg: list[dtype] | None = None lines: dict[str, list[str]] = field(default_factory=dict) pixel_size: float | None = None energy: float | None = None @property def rot_angles_deg(self) -> NDArray: return self.angles_deg @property def rot_angles_rad(self) -> NDArray: return self.rot_angles_deg.to(_ureg.rad) @dataclass class BlissFluoTomoScan3D(BlissFluoTomoScanBase): """Base class to read XRF data fitted with ewoksfluo.""" ewoksfluo_filename: str detectors: tuple = () dtype: DTypeLike = numpy.float32 verbose: bool = False angles_deg: list[dtype] | None = None lines: list[str] = field(default_factory=list) pixel_size: float | None = None energy: float | None = None def _check_entry(self, entry, f): data_shape = f[ f"{entry}/grid/{self.detectors[0]}/results/massfractions/{self.lines[0]}" ][()].shape flag = self.slow_npoints * self.fast_npoints == data_shape[0] * data_shape[1] if not flag: msg = f"Entry {entry} has unexpected data shape {data_shape}, expected {(self.slow_npoints, self.fast_npoints)}. It has been ignored (i.e. not converted)." _logger.warning(msg) return flag def __post_init__(self): try: with h5py.File(self.ewoksfluo_filename, "r") as f: self.entries = sorted(list(f.keys()), key=lambda x: float(x)) entry0 = self.entries[0] # Get detector names if len(self.detectors) == 0: self.detectors = list(f[f"{entry0}/grid"].keys()) # Get fitted line names detector0 = self.detectors[0] self.lines = [] for name, ds in f[ f"{entry0}/grid/{detector0}/results/massfractions" ].items(): if ds.ndim == 2: self.lines.append(name) # Get projections dimensions self.slow_npoints = float( f[f"{entry0}/instrument/fscan_parameters/slow_npoints"][()] ) self.fast_npoints = float( f[f"{entry0}/instrument/fscan_parameters/fast_npoints"][()] ) # Filter out interrupted scans: self.entries = [ entry for entry in self.entries if self._check_entry(entry, f) ] # Get rotation angles angles_tmp = [] for entry in self.entries: angles_tmp.append(f[f"{entry}/instrument/positioners/somega"][()]) self.angles_deg = np.array(angles_tmp, dtype=self.dtype) * _ureg.deg # Get pixel size fast_pixel_size = float( f[f"{entry0}/instrument/fscan_parameters/fast_step_size"][()] ) slow_pixel_size = float( f[f"{entry0}/instrument/fscan_parameters/slow_step_size"][()] ) if slow_pixel_size == fast_pixel_size: self.pixel_size = slow_pixel_size * _ureg.um else: msg = f"Found slow_pixel_size ({slow_pixel_size}) different from fast_pixel_size ({fast_pixel_size})" _logger.info(msg) raise ValueError(msg) # Get energy self.energy = ( float( f[ f"{entry0}/instrument/metadata/InstrumentMonochromator_energy" ][()] ) * _ureg.keV ) except FileNotFoundError: self.entries = None _logger.info( f"Detected {len(self.entries)} projections in {self.ewoksfluo_filename}." ) def load_data(self, det, line): if self.entries is None: _logger.info(f"No entry was detected in {self.ewoksfluo_filename}.") else: with h5py.File(self.ewoksfluo_filename, "r") as f: projs_tmp = [] angles_tmp = [] for entry in self.entries: data = f[f"{entry}/grid/{det}/results/massfractions/{line}"][()] projs_tmp.append(numpy.nan_to_num(data).astype(self.dtype)) angles_tmp.append(f[f"{entry}/instrument/positioners/somega"][()]) projs_tmp = np.array(projs_tmp) angles_tmp = np.array(angles_tmp).astype(self.dtype) if np.allclose(angles_tmp * _ureg.deg, self.angles_deg): return np.ascontiguousarray(projs_tmp) else: msg = f"Angles found for {det}/{line} do not match the angles found at the class level." _logger.info(msg) raise ValueError(msg) @dataclass class BlissFluoTomoScan2D(BlissFluoTomoScanBase): """Base class to read XRF data fitted with ewoksfluo.""" ewoksfluo_filename: str detectors: tuple = () dtype: DTypeLike = numpy.float32 verbose: bool = False angles_deg: list[dtype] | None = None lines: list[str] = field(default_factory=list) pixel_size: float | None = None energy: float | None = None nxtomomill-v2.0.1/nxtomomill/converter/fluo/fluoconverter.py000066400000000000000000000202451511430602400245260ustar00rootroot00000000000000# coding: utf-8 """ module to convert fluo-tomo files (after PyMCA fit, tif files) to (nexus tomo compliant) .nx """ from __future__ import annotations import pint import logging import os from tqdm import tqdm from .fluoscan import FluoTomoScan2D, FluoTomoScan3D from .blissfluoscan import BlissFluoTomoScan3D from nxtomomill.models.fluo2nx import Fluo2nxModel from nxtomo import NXtomo from nxtomo.nxobject.nxdetector import ImageKey from nxtomomill import utils _ureg = pint.get_application_registry() _logger = logging.getLogger(__name__) __all__ = ["from_fluo_to_nx", "from_blissfluo_to_nx"] def from_fluo_to_nx(configuration: Fluo2nxModel, progress: tqdm | None = None) -> tuple: """ Converts an fluo-tomo tiff files to a nexus file. For now duplicates data. :param configuration: configuration to use to process the data :param progress: if provided then will be updated with conversion progress :return: (nexus_file, entry) """ if configuration.input_folder is None: raise ValueError("input_folder should be provided") if not os.path.isdir(configuration.input_folder): raise OSError(f"{configuration.input_folder} is not a valid folder path") if configuration.output_file is None: raise ValueError("output_file should be provided") if configuration.detector_names is None: raise ValueError("Detector names should be provided.") fileout_h5 = utils.get_file_name( file_name=configuration.output_file, extension=configuration.file_extension, check=True, ) if configuration.dimension == 2: scan = FluoTomoScan2D( scan=configuration.input_folder, dataset_basename=configuration.dataset_basename, detectors=configuration.detector_names, ) elif configuration.dimension == 3: scan = FluoTomoScan3D( scan=configuration.input_folder, dataset_basename=configuration.dataset_basename, detectors=configuration.detector_names, ) else: raise ValueError(f"Dimension should be 2 or 3 not {configuration.dimension}.") if progress is not None: progress.set_description("fluo2nx") progress.total = len(scan.el_lines) progress.n = 0 _logger.info(f"Fluo lines preset in dataset are {scan.el_lines}") entry_list = [] for element, lines in scan.el_lines.items(): if progress is not None: progress.set_postfix_str(f"elmt - {element}") line_progress = tqdm( desc=f"elmt: {element} - line: ", position=1, leave=False ) line_progress.total = len(lines) else: line_progress = None for i_line, line in enumerate(lines): if line_progress is not None: line_progress.set_postfix_str(f"elmt: {element} - line: {line}") for det in scan.detectors: elmt_line_data = scan.load_data(det, element=element, line_ind=i_line) if configuration.dimension == 2: elmt_line_data = elmt_line_data.swapaxes(0, 1).copy() # Otherwise, it's 3D case, and the structure of elmt_line_data is OK. # my_nxtomo = NXtomo() my_nxtomo.instrument.detector.data = elmt_line_data my_nxtomo.instrument.detector.image_key_control = [ ImageKey.PROJECTION ] * elmt_line_data.shape[0] my_nxtomo.sample.rotation_angle = scan.rot_angles_deg * _ureg.degree my_nxtomo.instrument.detector.x_pixel_size = scan.pixel_size * _ureg( "m" ) my_nxtomo.instrument.detector.y_pixel_size = scan.pixel_size * _ureg( "m" ) # define a value to sample-detector and source-sample distance. To be set to the real value in the future my_nxtomo.instrument.detector.distance = 1.0 * _ureg.meter my_nxtomo.instrument.source.distance = 1.0 * _ureg.meter my_nxtomo.energy = scan.energy * _ureg.keV data_path = f"{det}_{element}_{line}" my_nxtomo.save( file_path=fileout_h5, data_path=data_path, overwrite=configuration.overwrite, ) entry_list.append((fileout_h5, data_path)) if line_progress is not None: line_progress.update() if progress is not None: progress.update() return tuple(entry_list) def from_blissfluo_to_nx( configuration: Fluo2nxModel, progress: tqdm | None = None ) -> tuple: """ Converts an Bliss fluo-tomo h5 file to a nexus file. For now duplicates data. :param configuration: configuration to use to process the data :param progress: if provided then will be updated with conversion progress :return: (nexus_file, entry) """ if configuration.general_section.ewoksfluo_filename is None: raise ValueError("ewoksfluo_filename should be provided") if not os.path.isfile(configuration.general_section.ewoksfluo_filename): raise FileNotFoundError( f"{configuration.general_section.ewoksfluo_filename} is not a valid filename path" ) if configuration.general_section.output_file is None: raise ValueError("output_file should be provided") if configuration.general_section.detector_names is None: raise ValueError("Detector names should be provided.") fileout_h5 = utils.get_file_name( file_name=configuration.general_section.output_file, extension=configuration.general_section.file_extension, check=True, ) if configuration.general_section.dimension == 2: raise NotImplementedError("Only available for 3D at the time") elif configuration.general_section.dimension == 3: scan = BlissFluoTomoScan3D( ewoksfluo_filename=configuration.general_section.ewoksfluo_filename, detectors=configuration.general_section.detector_names, ) else: raise ValueError( f"Dimension should be 2 or 3 not {configuration.general_section.dimension}." ) if progress is not None: progress.set_description("blissfluo2nx") progress.total = len(scan.lines) * len(scan.detectors) progress.n = 0 _logger.info(f"Fluo lines preset in dataset are {scan.lines}") entry_list = [] for line in scan.lines: for det in scan.detectors: line_data = scan.load_data(det, line=line) # my_nxtomo = NXtomo() my_nxtomo.instrument.detector.data = line_data my_nxtomo.instrument.detector.image_key_control = [ ImageKey.PROJECTION ] * line_data.shape[0] my_nxtomo.sample.rotation_angle = scan.rot_angles_deg # TODO: should the pixel_size be in a more readable unit (convert to micrometer ?) my_nxtomo.instrument.detector.x_pixel_size = scan.pixel_size.to(_ureg.meter) my_nxtomo.instrument.detector.y_pixel_size = scan.pixel_size.to(_ureg.meter) # define a value to sample-detector and source-sample distance. To be set to the real value in the future my_nxtomo.instrument.detector.distance = 1.0 * _ureg.meter my_nxtomo.instrument.source.distance = 1.0 * _ureg.meter my_nxtomo.instrument.source.name = configuration.source_section.source_name my_nxtomo.instrument.source.type = configuration.source_section.source_type my_nxtomo.instrument.source.probe = ( configuration.source_section.source_probe ) my_nxtomo.instrument.name = configuration.instrument_section.instrument_name my_nxtomo.energy = scan.energy data_path = f"{det}_{line}" my_nxtomo.save( file_path=fileout_h5, data_path=data_path, overwrite=configuration.general_section.overwrite, ) entry_list.append((fileout_h5, data_path)) if progress is not None: progress.update() return tuple(entry_list) nxtomomill-v2.0.1/nxtomomill/converter/fluo/fluoscan.py000066400000000000000000000320701511430602400234420ustar00rootroot00000000000000"""Scan dedicated for bliss format - based on EDF files""" from __future__ import annotations import logging import os import glob import pint import numpy from tomoscan.identifier import ScanIdentifier from tomoscan.scanbase import TomoScanBase from tomoscan.utils import docstring from dataclasses import dataclass, field import numpy as np from numpy.typing import NDArray, DTypeLike from silx.io.utils import h5py_read_dataset try: from tqdm.auto import tqdm except ImportError: has_tqdm = False else: has_tqdm = True import h5py _ureg = pint.get_application_registry() _logger = logging.getLogger(__name__) try: import tifffile # noqa #F401 needed for later possible lazy loading except ImportError: has_tifffile = False else: has_tifffile = True __all__ = ["FluoTomoScanBase", "FluoTomoScan3D", "FluoTomoScan2D"] @dataclass class FluoTomoScanBase: """Dataset manipulation class.""" scan: str dataset_basename: str detectors: tuple = () dtype: DTypeLike = numpy.float32 verbose: bool = False angles: float | None = None """rotation angles in degree""" el_lines: dict[str, list[str]] = field(default_factory=dict) pixel_size: float | None = None """pixel size in meter""" energy: float | None = None """energy in keV""" detected_folders: list[str] = field(default_factory=list) def __post_init__(self): self.detected_folders = self.detect_folders() self.detected_detectors = tuple(self.detect_detectors()) self.get_metadata_from_h5_file() _logger.info(f"Detectors: {self.detected_detectors}") if len(self.detectors) == 0: self.detectors = self.detected_detectors for det in self.detectors: if det not in self.detected_detectors: raise ValueError( f"The detector should be in {self.detected_detectors} and is {self.detectors}" ) self.detect_elements() if self.angles is None: self.angles = self.detect_rot_angles() def detect_folders(self) -> list[str]: """List all folders to process.""" raise NotImplementedError("Base class") @property def rot_angles_deg(self) -> NDArray: if self.angles is None: raise ValueError("Rotation angles not initialized") return self.angles * _ureg.deg @property def rot_angles_rad(self) -> NDArray: return self.rot_angles_deg.to(_ureg.rad) def detect_rot_angles(self) -> NDArray: """Build rotation angles list.""" raise NotImplementedError("Base class") def _check_ready_to_load_data(self, det): if not has_tifffile: raise RuntimeError("tiffile not install. Cannot load data.") if det not in self.detectors: raise RuntimeError( f"The detector {det} is invalid. Valid ones are {self.detectors}" ) if self.angles is None: raise RuntimeError("Rotation angles not initialized") if self.detectors is None: raise RuntimeError("Detectors not initialized") def get_metadata_from_h5_file(self): if len(self.detected_folders) == 0: raise ValueError("No folder found, unable to load metadata") h5_path = os.path.join(self.scan, self.detected_folders[0]) h5_files = glob.glob1(h5_path, "*.h5") if len(h5_files) > 1: raise ValueError( "More than one hdf5 file in scan directory. Expect only ONE to pick pixel size." ) elif len(h5_files) == 0: pattern = os.path.join(h5_path, "*.h5") raise ValueError( f"Unable to find the hdf5 file in scan directory to pick pixel size. RegExp used is {pattern}" ) else: with h5py.File(os.path.join(h5_path, h5_files[0]), "r") as f: if len(list(f.keys())) != 1: raise ValueError( f"H5 file should contain only one entry, found {len(list(f.keys()))}" ) else: entry_name = list(f.keys())[0] self.pixel_size = ( ( h5py_read_dataset(f[entry_name]["FLUO"]["pixelSize"]) * _ureg.micrometer ) .to(_ureg.meter) .magnitude ) self.energy = float( h5py_read_dataset( f[entry_name]["instrument"]["monochromator"]["energy"] ) ) def detect_detectors(self): if len(self.detected_folders) == 0: raise ValueError("No folder found, unable to detect detectors") proj_1_dir = os.path.join(self.scan, "fluofit", self.detected_folders[0]) detected_detectors = [] file_names = glob.glob1(proj_1_dir, "IMG_*area_density_ngmm2.tif") for file in file_names: det = file.split("_")[1] if det not in detected_detectors: detected_detectors.append(det) if "" in detected_detectors: raise ValueError( f"Suspicious detector! Detected detectors are {detected_detectors}. Please use --detectors ... where det1 are valid detector name." ) return detected_detectors def detect_elements(self): if len(self.detected_folders) == 0: raise ValueError("No folder found, unable to detect elements") proj_1_dir = os.path.join(self.scan, "fluofit", self.detected_folders[0]) detector = self.detectors[0] file_names = glob.glob1(proj_1_dir, f"IMG_{detector}*area_density_ngmm2.tif") for file in file_names: el_str = file.split("_")[2] element, line = el_str.split("-") try: if line not in self.el_lines[element]: self.el_lines[element].append(line) self.el_lines[element] = sorted(self.el_lines[element]) except KeyError: self.el_lines[element] = [line] def load_data(self, det: str, element: str, line_ind: int = 0) -> NDArray: """Main function of class to load data.""" raise NotImplementedError("Base class") @staticmethod def from_identifier(identifier): """Return the Dataset from a identifier""" raise NotImplementedError("Not implemented for fluo-tomo yet.") @docstring(TomoScanBase) def get_identifier(self) -> ScanIdentifier: raise NotImplementedError("Not implemented for fluo-tomo yet.") @dataclass class FluoTomoScan3D(FluoTomoScanBase): """Dataset manipulation class.""" def detect_folders(self): fit_folders = glob.glob1( os.path.join(self.scan, "fluofit"), rf"{self.dataset_basename}_projection*" ) fit_folders.sort() proj_indices = [int(f[-3:]) for f in fit_folders] if len(fit_folders) < proj_indices[-1]: _logger.debug( f"The number of projections ({len(fit_folders)}) is lower than the largest projection index {proj_indices[-1]}. Some projections are probably missing." ) if len(fit_folders) == 0: raise FileNotFoundError( "No projection was found in the fluofit folder. The searched for pattern is /fluofit/_projection*'." ) elif not os.path.isdir(os.path.join(self.scan, fit_folders[0])): raise FileNotFoundError( "Found fitted data folders but not the corresponding raw data folder." ) else: return fit_folders def detect_rot_angles(self) -> NDArray: tmp_angles = [] for f in self.detected_folders: prj_dir = os.path.join(self.scan, "fluofit", f) info_file = os.path.join(prj_dir, "info.txt") try: with open(info_file, "r") as f: info_str = f.read() tmp_angles.append(float(info_str.split(" ")[2])) except FileNotFoundError: _logger.debug( f"{info_file} doesn't exist, while expected to be present in each projection folder." ) raise FileNotFoundError( f"{info_file} doesn't exist, while expected to be present in each projection folder." ) _logger.info(f"Correctly found all {len(tmp_angles)} rotation angles.") return numpy.array(tmp_angles, ndmin=1, dtype=numpy.float32) def load_data(self, det: str, element: str, line_ind: int = 0) -> NDArray: self._check_ready_to_load_data(det) line = self.el_lines[element][line_ind] data_det = [] description = f"Loading images of {element}-{line} ({det}): " if has_tqdm: proj_iterator = tqdm( range(len(self.detected_folders)), disable=self.verbose, desc=description, ) else: proj_iterator = range(len(self.detected_folders)) for ii_i in proj_iterator: proj_dir = os.path.join( self.scan, "fluofit", self.detected_folders[ii_i], ) img_path = os.path.join( proj_dir, f"IMG_{det}_{element}-{line}_area_density_ngmm2.tif" ) if self.verbose: _logger.info(f"Loading {ii_i + 1} / {len(self.angles)}: {img_path}") img = tifffile.imread(img_path) data_det.append(numpy.nan_to_num(numpy.array(img, dtype=self.dtype))) data = numpy.array(data_det) return numpy.ascontiguousarray(data) @dataclass class FluoTomoScan2D(FluoTomoScanBase): """Dataset manipulation class.""" def get_metadata_from_h5_file(self): super().get_metadata_from_h5_file() h5_path = os.path.join(self.scan, self.detected_folders[0]) h5_files = glob.glob1(h5_path, "*.h5") if len(h5_files) > 1: raise ValueError( "More than one hdf5 file in scan directory. Expect only ONE to pick pixel size." ) elif len(h5_files) == 0: pattern = os.path.join(h5_path, "*.h5") raise ValueError( f"Unable to find the hdf5 file in scan directory to pick pixel size. RegExp used is {pattern}" ) else: with h5py.File(os.path.join(h5_path, h5_files[0]), "r") as f: if len(list(f.keys())) != 1: raise ValueError( f"H5 file should contain only one entry, found {len(list(f.keys()))}" ) else: entry_name = list(f.keys())[0] self.scanRange_2 = float( h5py_read_dataset(f[entry_name]["FLUO"]["scanRange_2"]) ) self.scanDim_2 = int( h5py_read_dataset(f[entry_name]["FLUO"]["scanDim_2"]) ) def detect_rot_angles(self) -> NDArray: nb_projs = self.scanDim_2 angular_coverage = self.scanRange_2 return np.linspace(0, angular_coverage, nb_projs, endpoint=True) def detect_folders(self): fit_folder = os.path.join(self.scan, "fluofit", self.dataset_basename) if not os.path.isdir(fit_folder): raise FileNotFoundError( f"No folder {fit_folder} was found in the fluofit folder. The searched for pattern is /fluofit/'." ) elif not os.path.isdir(os.path.join(self.scan, self.dataset_basename)): raise FileNotFoundError( "Found fitted data folders but not the corresponding raw data folder." ) else: return [ self.dataset_basename, ] def load_data(self, det: str, element: str, line_ind: int = 0) -> NDArray: self._check_ready_to_load_data(det) line = self.el_lines[element][line_ind] data_det = [] description = f"Loading images of {element}-{line} ({det}): " if has_tqdm: slice_iterator = tqdm( range(len(self.detected_folders)), disable=self.verbose, desc=description, ) else: slice_iterator = range(len(self.detected_folders)) for ii_i in slice_iterator: proj_dir = os.path.join( self.scan, "fluofit", self.dataset_basename, # WARNING: dataset_basename is ONE SINGLE sinogram. ) img_path = os.path.join( proj_dir, f"IMG_{det}_{element}-{line}_area_density_ngmm2.tif" ) if self.verbose: _logger.info(f"Loading {ii_i + 1} / {len(self.angles)}: {img_path}") img = tifffile.imread(img_path) data_det.append(numpy.nan_to_num(numpy.array(img, dtype=self.dtype))) data = numpy.array(data_det) return numpy.ascontiguousarray(data) nxtomomill-v2.0.1/nxtomomill/converter/fluo/tests/000077500000000000000000000000001511430602400224165ustar00rootroot00000000000000nxtomomill-v2.0.1/nxtomomill/converter/fluo/tests/__init__.py000066400000000000000000000000001511430602400245150ustar00rootroot00000000000000nxtomomill-v2.0.1/nxtomomill/converter/fluo/tests/test_fluoscan2D.py000066400000000000000000000045431511430602400260350ustar00rootroot00000000000000# coding: utf-8 import logging import pytest import numpy from nxtomomill.converter.fluo.fluoscan import FluoTomoScan2D from nxtomomill.tests.datasets import GitlabDataset logging.disable(logging.INFO) @pytest.fixture(scope="class") def fluodata2D(request): cls = request.cls cls.scan_dir = GitlabDataset.get_dataset("fluo_datasets2D") cls.dataset_basename = "CONT2_p2_600nm_FT02_slice_0" cls.scan = FluoTomoScan2D( scan=cls.scan_dir, dataset_basename=cls.dataset_basename, detectors=(), ) @pytest.mark.usefixtures("fluodata2D") class TestFluo2D: def test_all_detectors(self): assert ( len(self.scan.el_lines) == 2 # pylint: disable=E1101 ), f"Number of elements found should be 2 and is {len(self.scan.el_lines)}." # pylint: disable=E1101 assert set(self.scan.detectors) == set( # pylint: disable=E1101 ["fluo1", "corrweighted"] ), f"There should be 2 'detectors' (fluo1 and corrweighted), {len(self.detectors)} were found." # pylint: disable=E1101 def test_one_detector(self): scan = FluoTomoScan2D( scan=self.scan_dir, # pylint: disable=E1101 dataset_basename=self.dataset_basename, # pylint: disable=E1101 detectors=("corrweighted",), ) assert ( len(scan.el_lines) == 2 ), f"Number of elements found should be 2 and is {len(scan.el_lines)}." assert ( len(scan.detectors) == 1 ), f"There should be 1 detector (corrweighted), {len(scan.detectors)} were found." # One ghost detector (no corresponding files) # test general section setters with pytest.raises(ValueError): scan = FluoTomoScan2D( scan=self.scan_dir, # pylint: disable=E1101 dataset_basename=self.dataset_basename, # pylint: disable=E1101 detectors=("toto",), ) def test_load_data(self): data = self.scan.load_data("corrweighted", "Ca", 0) # pylint: disable=E1101 assert data.shape == (1, 251, 1000) def test_load_energy_and_pixel_size(self): assert self.scan.energy == 17.1 # pylint: disable=E1101 assert numpy.allclose( self.scan.pixel_size, 6e-10, atol=1e-4 # pylint: disable=E1101 ) # Tolerance:0.1nm (since pixel_size is expected in um). nxtomomill-v2.0.1/nxtomomill/converter/fluo/tests/test_fluoscan3D.py000066400000000000000000000043371511430602400260370ustar00rootroot00000000000000# coding: utf-8 import logging import pytest from nxtomomill.converter.fluo.fluoscan import FluoTomoScan3D from nxtomomill.tests.datasets import GitlabDataset logging.disable(logging.INFO) @pytest.fixture(scope="class") def fluodata3D(request): cls = request.cls cls.scan_dir = GitlabDataset.get_dataset("fluo_datasets") cls.dataset_basename = "CP1_XRD_insitu_top_ft_100nm" cls.scan = FluoTomoScan3D( scan=cls.scan_dir, dataset_basename=cls.dataset_basename, detectors=(), ) @pytest.mark.usefixtures("fluodata3D") class TestFluo3D: def test_all_detectors(self): assert ( len(self.scan.el_lines) == 14 # pylint: disable=E1101 ), f"Number of elements found should be 14 and is {len(self.scan.el_lines)}." # pylint: disable=E1101 assert set(self.scan.detectors) == set( # pylint: disable=E1101 ["falcon", "weighted", "xmap"] ), f"There should be 3 'detectors' (xmap, falcon and weighted), {len(self.detectors)} were found." # pylint: disable=E1101 def test_one_detector(self): scan = FluoTomoScan3D( scan=self.scan_dir, # pylint: disable=E1101 dataset_basename=self.dataset_basename, # pylint: disable=E1101 detectors=("xmap",), ) assert ( len(scan.el_lines) == 14 ), f"Number of elements found should be 14 and is {len(scan.el_lines)}." assert ( len(scan.detectors) == 1 ), f"There should be 1 detector (xmap), {len(scan.detectors)} were found." # One ghost detector (no corresponding files) # test general section setters with pytest.raises(ValueError): scan = FluoTomoScan3D( scan=self.scan_dir, # pylint: disable=E1101 dataset_basename=self.dataset_basename, # pylint: disable=E1101 detectors=("toto",), ) def test_load_data(self): data = self.scan.load_data("xmap", "Ti", 0) # pylint: disable=E1101 assert data.shape == (2, 51, 280) def test_load_energy_and_pixel_size(self): assert self.scan.pixel_size == 1.3e-6 # pylint: disable=E1101 assert self.scan.energy == 17.1 # pylint: disable=E1101 nxtomomill-v2.0.1/nxtomomill/converter/fscan/000077500000000000000000000000001511430602400214015ustar00rootroot00000000000000nxtomomill-v2.0.1/nxtomomill/converter/fscan/__init__.py000066400000000000000000000000001511430602400235000ustar00rootroot00000000000000nxtomomill-v2.0.1/nxtomomill/converter/fscan/fscanconverter.py000066400000000000000000000123631511430602400250020ustar00rootroot00000000000000import logging from posixpath import join import numpy as np import pint from tomoscan.esrf.scan.h5utils import get_h5_value from nxtomo.application.nxtomo import NXtomo from nxtomo.nxobject.nxdetector import ImageKey from tomoscan.esrf.scan.fscan import FscanDataset, list_datasets from silx.utils.deprecation import deprecated _logger = logging.getLogger(__name__) _ureg = pint.get_application_registry() __all__ = [ "from_fscan_to_nx", ] @deprecated(reason="Removed as unused", since_version="2.0") def from_fscan_to_nx( fname, output_fname, detector_name="pcoedgehs", rotation_motor_name="hrrz", halftomo=False, ignore_last_n_projections=0, energy=None, distance=None, ): entries = list_datasets(fname) if len(entries) < 3: _logger.error( "Error: Expected at least three datasets, got only %d" % (len(entries)) ) return do_360 = False if len(entries) == 6: # 360 degrees scans are done in two parts: [1.1, 2.1, 3.1] and then [4.1, 5.1] do_360 = True projs = FscanDataset(fname, detector_name=detector_name, entry="1.1") flats = FscanDataset(fname, detector_name=detector_name, entry="2.1") darks = FscanDataset(fname, detector_name=detector_name, entry="3.1") if do_360: projs2 = FscanDataset(fname, detector_name=detector_name, entry="4.1") flats2 = FscanDataset(fname, detector_name=detector_name, entry="5.1") # Some datasets don't have the 6.1 entry. # Anyway nabu does not support several series of darks. # For now it's safer to ignore the second series of darks try: darks2 = FscanDataset(fname, detector_name=detector_name, entry="6.1") except ValueError: _logger.error("Could not find entry 6.1, proceeding") darks2 = None if darks2 is not None: _logger.warning( "Discarding entry 6.1 as nabu does not support several series of darks yet" ) my_nxtomo = NXtomo() data_urls = [darks.dataset_hdf5_url, flats.dataset_hdf5_url, projs.dataset_hdf5_url] if do_360: data_urls.extend( [ # darks2.dataset_hdf5_url, flats2.dataset_hdf5_url, projs2.dataset_hdf5_url, ] ) my_nxtomo.instrument.detector.data = data_urls img_keys = [ [ImageKey.DARK_FIELD] * darks.data_shape[0], [ImageKey.FLAT_FIELD] * flats.data_shape[0], [ImageKey.PROJECTION] * (projs.data_shape[0] - ignore_last_n_projections), [ImageKey.ALIGNMENT] * ignore_last_n_projections, ] if do_360: img_keys.extend( [ # [ImageKey.DARK_FIELD] * darks2.data_shape[0], [ImageKey.FLAT_FIELD] * flats2.data_shape[0], [ImageKey.PROJECTION] * (projs2.data_shape[0] - ignore_last_n_projections), [ImageKey.ALIGNMENT] * ignore_last_n_projections, ] ) my_nxtomo.instrument.detector.image_key_control = np.concatenate(img_keys) rotation_angles_path = join(projs.entry, f"instrument/{rotation_motor_name}/data") rotation_angles = get_h5_value(projs.fname, rotation_angles_path) # Sometimes values are in /data, sometimes in /value ... who needs a stable format anyway ? if rotation_angles is None: _logger.error( f"Could not find the rotation angles in {rotation_angles_path}, trying in /values" ) rotation_angles_path = rotation_angles_path.replace("/data", "/value") # --- rotation_angles = get_h5_value(projs.fname, rotation_angles_path) if rotation_angles is not None: last_idx = None if ignore_last_n_projections > 0: last_idx = -ignore_last_n_projections rotation_angles = [ [0] * darks.data_shape[0], [0] * flats.data_shape[0], rotation_angles[:last_idx], [0] * ignore_last_n_projections, ] if do_360: rotation_angles2 = get_h5_value( projs2.fname, join(projs2.entry, f"instrument/{rotation_motor_name}/data"), ) rotation_angles.extend( [ # [0] * darks2.data_shape[0], [0] * flats2.data_shape[0], rotation_angles2[:last_idx], [0] * ignore_last_n_projections, ] ) my_nxtomo.sample.rotation_angle = np.concatenate(rotation_angles) * _ureg.degree my_nxtomo.instrument.detector.field_of_view = ( "Half" if (halftomo and do_360) else "Full" ) my_nxtomo.instrument.detector.x_pixel_size = ( my_nxtomo.instrument.detector.y_pixel_size ) = (6.5 * 1e-6) n_frames = len(my_nxtomo.sample.rotation_angle) my_nxtomo.instrument.detector.sequence_number = np.linspace( start=0, stop=n_frames, num=n_frames, dtype=np.uint32, endpoint=False ) if energy is not None: my_nxtomo.energy = energy # in keV by default if distance is not None: my_nxtomo.instrument.detector.distance = distance # in meter my_nxtomo.save( file_path=output_fname, data_path="entry", overwrite=True, nexus_path_version=1.1, ) nxtomomill-v2.0.1/nxtomomill/converter/hdf5/000077500000000000000000000000001511430602400211355ustar00rootroot00000000000000nxtomomill-v2.0.1/nxtomomill/converter/hdf5/__init__.py000066400000000000000000000001321511430602400232420ustar00rootroot00000000000000# coding: utf-8 """ module to convert from (bliss) .h5 to (nexus tomo compliant) .nx """ nxtomomill-v2.0.1/nxtomomill/converter/hdf5/acquisition/000077500000000000000000000000001511430602400234655ustar00rootroot00000000000000nxtomomill-v2.0.1/nxtomomill/converter/hdf5/acquisition/__init__.py000066400000000000000000000000001511430602400255640ustar00rootroot00000000000000nxtomomill-v2.0.1/nxtomomill/converter/hdf5/acquisition/backandforth.py000066400000000000000000000012451511430602400264670ustar00rootroot00000000000000from __future__ import annotations from .multitomo import MultiTomoAcquisition class BackAndForthAcquisition(MultiTomoAcquisition): """ Back and forth acquisition is very similar to multi-tomo. In multi-tomo, the acquisition of projections begins when the rotation speed has reached an 'optimal' value. value. In back-and-forth acquisition, projections will be acquired in one rotation direction, then in the opposite direction, and so on. From the nxtomomill perspective, the constraints and processing remain the same. Nevertheless for clarity (and possible future evolution) a dedicated class has been created. """ nxtomomill-v2.0.1/nxtomomill/converter/hdf5/acquisition/baseacquisition.py000066400000000000000000000705601511430602400272320ustar00rootroot00000000000000# coding: utf-8 """ Base class for tomography acquisition (defined by Bliss) """ from __future__ import annotations import os import pint import h5py from nxtomo.application.nxtomo import NXtomo from nxtomomill.utils.h5pyutils import from_data_url_to_virtual_source from nxtomomill.utils.hdf5 import EntryReader, get_dataset_unit from nxtomomill.utils.utils import embed_url, get_file_name # noqa F401 try: import hdf5plugin # noqa F401 except ImportError: pass import logging from collections import OrderedDict import numpy from silx.io.url import DataUrl from silx.io.utils import h5py_read_dataset from nxtomomill.io.config import TomoHDF5Config _ureg = pint.get_application_registry() _logger = logging.getLogger(__name__) __all__ = ["BaseAcquisition", "get_dataset_name_from_motor"] def _ask_for_file_removal(file_path): res = input(f"Overwrite {file_path} ? (y/N)").lower() return res == "y" class BaseAcquisition: """ Util class to group several hdf5 group together and to write the data Nexus / NXtomo compliant """ _ENERGY_PATH = "technique/scan/energy" _DATASET_NAME_PATH = ("technique/scan/name",) _GRP_SIZE_PATH = ("technique/scan/nb_scans",) _SAMPLE_NAME_PATH = ("sample/name",) TITLE_PATHS = ( "technique/scan/sequence", "title", ) _INSTRUMENT_NAME_PATH = ( "technique/saving/beamline", "instrument/title", ) _FOV_PATH = "technique/scan/field_of_view" _NB_LOOP_PATH = ("technique/scan/nb_loop", "technique/proj/nb_loop") _NB_TOMO_PATH = ("technique/scan/nb_tomo", "technique/proj/nb_tomo") _NB_TURNS_PATH = ( "technique/proj/nb_turns", "technique/scan/nb_turns", ) _TOMO_N_PATH = ( "technique/proj/tomo_n", "technique/scan/tomo_n", "technique/proj/proj_n", "technique/scan/proj_n", ) _START_TIME_PATH = ("start_time",) _END_TIME_PATH = ("end_time",) _TECHNIQUE_MOTOR_PATHS = ("technique/scan/motor", "technique/proj/motor") _DETECTOR_ROI = ("technique/detector/roi",) _SOURCE_NAME = ("instrument/machine/name",) _SOURCE_TYPE = ("instrument/machine/type",) _FRAME_FLIP_PATHS = ( "technique/detector/{detector_name}/flipping", "technique/detector/flipping", ) _PROPAGATION_DISTANCE_PATHS = ( "technique/scan/effective_propagation_distance", "technique/scan/propagation_distance", ) def __init__( self, root_url: DataUrl | None, configuration: TomoHDF5Config, detector_sel_callback, start_index: int, ): self._root_url = root_url self._detector_sel_callback = detector_sel_callback self._registered_entries = OrderedDict() self._copy_frames = OrderedDict() # key is the entry, value his type self._entries_o_path = dict() # key is the entry, value his the entry path from the original file. # this value is different from `.name`. Don't know if this is a bug ? """user can have defined already some parameter values as energy. The idea is to avoid asking him if """ self._configuration = configuration self._start_index = start_index # set of properties when an acquisition needs to copy dark / flat (used in the case of z-series version 3) self._dark_at_start = None self._dark_at_end = None self._flat_at_start = None self._flat_at_end = None @property def configuration(self): return self._configuration @property def start_index(self) -> int: return self._start_index def write_as_nxtomo( self, shift_entry: int, input_file_path: str, request_input: bool, divide_into_sub_files: bool, input_callback=None, ) -> tuple: """ This function will dump the acquisition to disk as an NXtomo :param shift_entry: index of the entry to start saving new nxtomos. :param input_file_path: output file path :param request_input: if True the conversion can ask user some missing metadata :param divide_into_sub_files: if True then create one file per NXtomo :param input_callback: function to call for users to provide missing metadata """ nx_tomos = self.to_NXtomos( request_input=request_input, input_callback=input_callback, check_tomo_n=True, ) # preprocessing to define output file name possible_extensions = (".hdf5", ".h5", ".nx", ".nexus") output_file_basename = os.path.basename(self.configuration.output_file) file_extension_ = None for possible_extension in possible_extensions: if output_file_basename.endswith(possible_extension): output_file_basename.rstrip(possible_extension) file_extension_ = possible_extension def get_file_name_and_entry(index, divide_sub_files): entry = "entry" + str(index).zfill(4) if self.configuration.single_file or not divide_sub_files: en_output_file = self.configuration.output_file else: ext = file_extension_ or self.configuration.file_extension file_name = ( os.path.splitext(output_file_basename)[0] + "_" + str(index).zfill(4) + ext ) en_output_file = os.path.join( os.path.dirname(self.configuration.output_file), file_name ) if os.path.exists(en_output_file): if self.configuration.overwrite is True: _logger.warning(en_output_file + " will be removed") _logger.info("remove " + en_output_file) os.remove(en_output_file) elif _ask_for_file_removal(en_output_file) is False: raise OSError(f"unable to overwrite {en_output_file}, exit") else: os.remove(en_output_file) return en_output_file, entry result = [] for i_nx_tomo, nx_tomo in enumerate(nx_tomos): output_file, data_path = get_file_name_and_entry( (shift_entry + i_nx_tomo), divide_sub_files=divide_into_sub_files ) output_file = os.path.abspath(os.path.relpath(output_file, os.getcwd())) output_file = os.path.abspath(output_file) # For multi-tomo for example the data path is modified in order to handle with the splitting # that does not fit at 100 % with the current API. for now there is # no convenient way to handle this in a better way assert isinstance(nx_tomo, NXtomo) # embed data if requested if len(nx_tomo.instrument.detector.data) == 0: _logger.warning( f"No frame found for NXtomo number {i_nx_tomo}. Won't write any output for it" ) continue vs_0 = nx_tomo.instrument.detector.data[0] if nx_tomo.detector_data_is_defined_by_url(): new_urls = [] for url in nx_tomo.instrument.detector.data: if self._copy_frames[url.path()]: created_url = embed_url( url=url, output_file=output_file, ) new_urls.append(created_url) else: new_urls.append(url) nx_tomo.instrument.detector.data = new_urls elif nx_tomo.detector_data_is_defined_by_virtual_source(): new_vs = [] for vs in nx_tomo.instrument.detector.data: assert isinstance(vs, h5py.VirtualSource) assert isinstance(vs_0, h5py.VirtualSource) url = DataUrl(file_path=vs.path, data_path=vs.name, scheme="silx") if ( url.path() in self._copy_frames and self._copy_frames[url.path()] ): new_url = embed_url(url, output_file=output_file) n_vs, _, _ = from_data_url_to_virtual_source(new_url) new_vs.append(n_vs) else: new_vs.append(vs) nx_tomo.instrument.detector.data = new_vs # provide some extra data like origin of the input_file if input_file_path is not None: nx_tomo.bliss_original_files = (os.path.abspath(input_file_path),) # save data nx_tomo.save(file_path=output_file, data_path=data_path) # check rotation angle if nx_tomo.sample.rotation_angle is not None: unique_angles = numpy.unique( nx_tomo.sample.rotation_angle.to(_ureg.degree).magnitude ) if len(unique_angles) == 1: _logger.warning( f"NXtomo {data_path}@{output_file} seems to have a single value ({unique_angles}) for rotation angle. Seems it fails to find correct path to the rotation angle dataset" ) result.append((output_file, data_path)) return tuple(result) def to_NXtomos(self, request_input, input_callback, check_tomo_n=True) -> tuple: raise NotImplementedError("Base class") @property def raise_error_if_issue(self): """ Should we raise an error if we encounter or an issue or should we just log an error message """ return self.configuration.raises_error @property def root_url(self): return self._root_url def get_expected_nx_tomo(self): """ Return the expected number of nxtomo created for this acquisition. This is required to get consistent entry and file name. At lest for automation """ raise NotImplementedError("Base class") def read_entry(self): return EntryReader(self._root_url) def is_different_sequence(self, entry): """ Can we have several entries 1.1, 1.2, 1.3... to consider. """ raise ValueError("Base class") def is_part_of_same_series(self, other: BaseAcquisition) -> bool: if not isinstance(other, BaseAcquisition): raise TypeError("Can only compar two z-series") if self.root_url is None or other.root_url is None: return False def read_sample_node(url: DataUrl) -> str | None: with EntryReader(url=url) as entry: sample_node = self._get_sample_node(entry) if sample_node is None: return None sample_name = sample_node.get("name", None) if sample_name is not None: return h5py_read_dataset(sample_name) return None self_sample_name = read_sample_node(self.root_url) other_sample_name = read_sample_node(other.root_url) if self_sample_name is None or other_sample_name is None: return False def filter_scan_index(sample_name: str) -> str: # for z_series the sample name is post pone with _{sample_name} that we need to filter parts = sample_name.split("_") if len(parts) > 1 and numpy.char.isnumeric(parts[-1]): return "_".join(parts[:-1]) return sample_name return filter_scan_index(self_sample_name) == filter_scan_index( other_sample_name ) @staticmethod def _get_node_values_for_frame_array( node: h5py.Group, n_frame: int | None, keys: list | tuple, info_retrieve: str, expected_unit: pint.Unit, ) -> tuple[numpy.ndarray | None, str]: assert isinstance(expected_unit, pint.Unit), "Invalid expected unit" def get_values(): # this is a two step process: first step we parse all the # the keys until we found one with the expected length # if first iteration fails then we return the first existing key for respect_length in (True, False): for possible_key in keys: if possible_key in node and isinstance( node[possible_key], h5py.Dataset ): values = h5py_read_dataset(node[possible_key]) unit = get_dataset_unit( dataset=node[possible_key], default=expected_unit, from_dataset=f"{node[possible_key].name} (reading {info_retrieve})", ) # skip values containing '*DIS*' if isinstance(values, str) and values == "*DIS*": continue if n_frame is not None and respect_length is True: if numpy.isscalar(values): length = 1 else: length = len(values) if length in (n_frame, n_frame + 1): return values, unit else: return values, unit return None, None values, unit = get_values() if values is None: raise ValueError( f"Unable to retrieve {info_retrieve} for {node.name}. Was looking for {keys} datasets" ) elif n_frame is None: return values, unit elif numpy.isscalar(values): return numpy.array([values] * n_frame), unit elif len(values) == n_frame: return values.tolist(), unit elif len(values) == (n_frame + 1): # for now we can have one extra position for rotation, # translation_x... # because saved after the last projection. It is recording the # motor position. For example in this case: 1 is the motor movement # (saved) and 2 is the acquisition # # 1 2 1 2 1 # ----- ----- # ----- ----- ----- # return values[:-1].tolist(), unit elif len(values) > n_frame: _logger.warning( f"Incoherent number of values found for {info_retrieve}. Can come from an acquisition canceled. Else please investigate." ) # in this case only get the values which have a frame return values[0:n_frame], unit elif len(values) < n_frame: _logger.warning( f"Incoherent number of values found for {info_retrieve}. Can come from an acquisition canceled. Else please investigate." ) # in this case append 0 to existing values. Maybe -1 would be better ? return list(values) + [0] * (n_frame - values), unit elif len(values) == 1: return numpy.array([values[0]] * n_frame), unit else: raise ValueError("incoherent number of angle position vs number of frame") def register_step(self, url: DataUrl, entry_type, copy_frames) -> None: """ Add a bliss entry to the acquisition :param url: :param entry_type: """ raise NotImplementedError("Base class") @staticmethod def _get_instrument_node(entry_node: h5py.Group) -> h5py.Group: if not isinstance(entry_node, h5py.Group): raise TypeError("entry_node: h5py.group expected") return entry_node["instrument"] @staticmethod def _get_positioners_node(entry_node: h5py.Group) -> h5py.Group | None: if not isinstance(entry_node, h5py.Group): raise TypeError("entry_node is expected to be a h5py.Group") parent_node = BaseAcquisition._get_instrument_node(entry_node) if "positioners" in parent_node: return parent_node["positioners"] else: return None @staticmethod def _get_measurement_node(entry_node: h5py.Group) -> h5py.Group | None: if not isinstance(entry_node, h5py.Group): raise TypeError("entry_node is expected to be a h5py.Group") if "measurement" in entry_node: return entry_node["measurement"] else: return None @staticmethod def _get_machine_node(entry_node: h5py.Group) -> h5py.Group | None: if not isinstance(entry_node, h5py.Group): raise TypeError("entry_node is expected to be a h5py.Group") if "instrument/machine" in entry_node: return entry_node["instrument/machine"] else: return None @staticmethod def _get_sample_node(entry_node: h5py.Group) -> h5py.Group | None: if not isinstance(entry_node, h5py.Group): raise TypeError("entry_node is expected to be a h5py.Group") if "sample" in entry_node: return entry_node["sample"] else: return None @staticmethod def _get_scan_flags(entry_node: h5py.Group) -> h5py.Group | None: if not isinstance(entry_node, h5py.Group): raise TypeError("entry_node is expected to be a h5py.Group") if "technique/scan_flags" in entry_node: return entry_node["technique/scan_flags"] else: return None def _read_rotation_motor_name(self) -> str | None: """read rotation motor from root_url/technique/scan/motor :return: name of the motor used for rotation. None if cannot find """ if self._root_url is None: _logger.warning("no root url. Unable to read rotation motor") return None else: with EntryReader(self._root_url) as entry: for motor_path in self._TECHNIQUE_MOTOR_PATHS: if motor_path in entry: try: rotation_motor = get_dataset_name_from_motor( motors=h5py_read_dataset( numpy.asarray(entry[motor_path]) ), motor_name="rotation", ) except Exception as e: _logger.error(e) else: return rotation_motor else: _logger.warning( f"{motor_path} unable to find rotation motor from {self._root_url}" ) return None def _get_machine_current(self, root_node) -> None | pint.Quantity: """retrieve electric current provide a time stamp for each of them""" if root_node is None: _logger.warning("no root url. Unable to read electric current") return None else: grps = [ root_node, ] measurement_node = self._get_measurement_node(root_node) if measurement_node is not None: grps.append(measurement_node) machine_node = self._get_machine_node(root_node) if machine_node is not None: grps.append(machine_node) for grp in grps: try: mach_current, unit = self._get_node_values_for_frame_array( node=grp, keys=self.configuration.machine_current_keys, info_retrieve="machine current", expected_unit=_ureg.mA, n_frame=None, ) except (ValueError, KeyError): pass else: # handle case where mach_current is a scalar. Cast it to list before return if numpy.isscalar(mach_current): mach_current = [ mach_current, ] return mach_current * unit else: _logger.warning( f"Unable to retrieve machine current for {root_node.name}" ) return None def _get_rotation_angle(self, root_node: h5py.Group, n_frame: int) -> pint.Quantity: """ return the list of rotation angle for each frame. If unable to find it will either: * raise an error (if 'raise_error_if_issue' is True) * return a zeros for all required angles (kwnow from n_frame) """ if not isinstance(root_node, h5py.Group): raise TypeError("root_node is expected to be a h5py.Group") for grp in ( self._get_positioners_node(root_node), root_node, self._get_measurement_node(root_node), ): try: angles, unit = self._get_node_values_for_frame_array( node=grp, n_frame=n_frame, keys=self.configuration.rotation_angle_keys, info_retrieve="rotation angle", expected_unit=_ureg.degree, ) except (ValueError, KeyError): pass else: return angles * unit mess = f"Unable to find rotation angle for {root_node.name}" if self.raise_error_if_issue: raise ValueError(mess) else: mess += "default value will be set. (0)" _logger.warning(mess) return ([0] * n_frame) * _ureg.degree def _get_sample_x(self, root_node, n_frame) -> pint.Quantity: """return the list of translation for each frame""" for grp in self._get_positioners_node(root_node), root_node: try: x_tr, unit = self._get_node_values_for_frame_array( node=grp, n_frame=n_frame, keys=self.configuration.sample_x_keys, info_retrieve="sample x translation", expected_unit=_ureg.mm, ) x_tr = numpy.asarray(x_tr) except (ValueError, KeyError): pass else: return x_tr * unit mess = f"Unable to find sample x for {root_node.name}" if self.raise_error_if_issue: raise ValueError(mess) else: mess += "default value will be set. (0)" _logger.warning(mess) return ([0] * n_frame) * _ureg.meter def _get_sample_y(self, root_node, n_frame) -> pint.Quantity: """return the list of translation for each frame""" for grp in self._get_positioners_node(root_node), root_node: try: y_tr, unit = self._get_node_values_for_frame_array( node=grp, n_frame=n_frame, keys=self.configuration.sample_y_keys, info_retrieve="sample y translation", expected_unit=_ureg.mm, ) y_tr = numpy.asarray(y_tr) except (ValueError, KeyError): pass else: return y_tr * unit mess = f"Unable to find sample y for {root_node.name}" if self.raise_error_if_issue: raise ValueError(mess) else: mess += "default value will be set. (0)" _logger.warning(mess) return ([0] * n_frame) * _ureg.meter def _get_translation_y(self, root_node, n_frame) -> pint.quantity: """return the list of translation for each frame""" for grp in self._get_positioners_node(root_node), root_node: try: y_tr, unit = self._get_node_values_for_frame_array( node=grp, n_frame=n_frame, keys=self.configuration.translation_y_keys, info_retrieve="y base translation", expected_unit=_ureg.mm, ) y_tr = numpy.asarray(y_tr) except (ValueError, KeyError): pass else: return y_tr * unit mess = f"Unable to find translation y for {root_node.name}" if self.raise_error_if_issue: raise ValueError(mess) else: mess += "default value will be set. (0)" _logger.warning(mess) return ([0] * n_frame) * _ureg.meter @staticmethod def get_translation_z_frm( root_node, n_frame: int, configuration: TomoHDF5Config ) -> pint.Quantity: for grp in BaseAcquisition._get_positioners_node(root_node), root_node: try: z_tr, unit = BaseAcquisition._get_node_values_for_frame_array( node=grp, n_frame=n_frame, keys=configuration.translation_z_keys, info_retrieve="z translation", expected_unit=_ureg.mm, ) z_tr = numpy.asarray(z_tr) except (ValueError, KeyError): pass else: return z_tr * unit mess = f"Unable to find translation z on node {root_node.name}" if configuration.raises_error: raise ValueError(mess) else: mess += "default value will be set. (0)" _logger.warning(mess) return ([0] * n_frame) * _ureg.meter def _get_translation_z(self, root_node, n_frame) -> pint.Quantity: """return the list of translation z for each frame""" return self.get_translation_z_frm( root_node=root_node, n_frame=n_frame, configuration=self.configuration, ) def _get_expo_time(self, root_node, n_frame, detector_node) -> pint.Quantity: """return expo time for each frame""" for grp in (detector_node.get("acq_parameters", None), root_node): if grp is None: continue try: expo, unit = self._get_node_values_for_frame_array( node=grp, n_frame=n_frame, keys=self.configuration.exposure_time_keys, info_retrieve="exposure time", expected_unit=_ureg.second, ) except (ValueError, KeyError): pass else: return expo * unit mess = f"Unable to find frame exposure time on entry {self.root_url.path()}" if self.raise_error_if_issue: raise ValueError(mess) else: mess += "default value will be set. (0)" _logger.warning(mess) return 0 * _ureg.second def get_axis_scale_types(self): """ Return axis display for the detector data to be used by silx view """ return ["linear", "linear"] def __str__(self): if self.root_url is None: return "NXTomo" else: return self.root_url.path() def get_detector_roi(self): if self._root_url is None: _logger.warning("no root url. Unable to read detector roi") return None else: with EntryReader(self._root_url) as entry: for roi_path in self._DETECTOR_ROI: if roi_path in entry: try: roi = h5py_read_dataset(numpy.asarray(entry[roi_path])) except Exception as e: _logger.error(e) else: return roi else: _logger.warning( f"{roi_path} unable to find detector roi from {self._root_url}" ) return None def get_dataset_name_from_motor(motors, motor_name): motors = numpy.asarray(motors) indexes = numpy.where(motors == motor_name)[0] if len(indexes) == 0: return None elif len(indexes) == 1: index = indexes[0] index_dataset_id = index + 1 if index_dataset_id < len(motors): return motors[index_dataset_id] else: raise ValueError( f"{motor_name} found but unable to find dataset name from {motors}" ) else: raise ValueError(f"More than one instance of {motor_name} as been found.") nxtomomill-v2.0.1/nxtomomill/converter/hdf5/acquisition/blisstomoconfig.py000066400000000000000000000154321511430602400272450ustar00rootroot00000000000000from __future__ import annotations import numpy import h5py from silx.io.utils import h5py_read_dataset from nxtomomill.models.utils import convert_str_to_tuple, convert_str_to_bool __all__ = [ "TomoConfig", ] class TomoConfig: """ hold motor used for tomography acquisition according to https://tomo.gitlab-pages.esrf.fr/bliss-tomo/master/modelization_sample_stage.html convension """ def __init__(self) -> None: self._rotation: None | tuple[str, ...] = None self._sample_u: None | tuple[str, ...] = None self._sample_v: None | tuple[str, ...] = None self._sample_x: None | tuple[str, ...] = None self._sample_y: None | tuple[str, ...] = None self._translation_x: None | tuple[str, ...] = None self._translation_y: None | tuple[str, ...] = None self._translation_z: None | tuple[str, ...] = None self._tomo_detector: None | tuple[str, ...] = None self._rotation_is_clockwise: None | bool = None @property def rotation(self) -> tuple[str, ...] | None: return self._rotation @rotation.setter def rotation(self, motor: tuple[str, ...] | None): self._rotation = motor @property def sample_u(self) -> tuple[str, ...] | None: return self._sample_u @sample_u.setter def sample_u(self, motor: tuple[str, ...] | None): self._sample_u = motor @property def sample_v(self) -> tuple[str, ...] | None: return self._sample_v @sample_v.setter def sample_v(self, motor: tuple[str, ...] | None): self._sample_v = motor @property def sample_x(self) -> tuple[str, ...] | None: return self._sample_x @sample_x.setter def sample_x(self, motor: tuple[str, ...] | None): self._sample_x = motor @property def sample_y(self) -> tuple[str, ...] | None: return self._sample_y @sample_y.setter def sample_y(self, motor: tuple[str, ...] | None): self._sample_y = motor @property def translation_x(self) -> tuple[str, ...] | None: return self._translation_x @translation_x.setter def translation_x(self, motor: tuple[str, ...] | None): self._translation_x = motor @property def translation_y(self) -> tuple[str, ...] | None: return self._translation_y @translation_y.setter def translation_y(self, motor: tuple[str, ...] | None): self._translation_y = motor @property def translation_z(self) -> tuple[str, ...] | None: return self._translation_z @translation_z.setter def translation_z(self, motor: tuple[str, ...] | None): self._translation_z = motor @property def tomo_detector(self) -> tuple[str, ...] | None: return self._tomo_detector @tomo_detector.setter def tomo_detector(self, detector_name: tuple[str, ...]): self._tomo_detector = detector_name @property def rotation_is_clockwise(self) -> bool | None: return self._rotation_is_clockwise @rotation_is_clockwise.setter def rotation_is_clockwise(self, is_clockwise: bool | None) -> None: if is_clockwise is None: self._rotation_is_clockwise = is_clockwise elif isinstance(is_clockwise, (bool, numpy.bool_)): self._rotation_is_clockwise = bool(is_clockwise) else: raise TypeError( f"'is_clockwise' should be a bool or Not. Got {type(is_clockwise)}" ) def __str__(self) -> str: return "tomo_config:" + " ; ".join( [ f"rotation={', '.join(self.rotation)}", f"rotation_is_clockwise={self.rotation_is_clockwise}", f"sample_u={', '.join(self.sample_u)}", f"sample_v={', '.join(self.sample_v)}", f"sample_x={', '.join(self.sample_x)}", f"sample_y={', '.join(self.sample_y)}", f"tomo_detector={', '.join(self.tomo_detector)}", f"translation_x={', '.join(self.translation_x)}", f"translation_y={', '.join(self.translation_y)}", f"translation_z={', '.join(self.translation_z)}", ] ) @staticmethod def from_technique_group(technique_group: h5py.Group): """ get rotation motor and thinks like this from the 'tomoconfig'. This can retrieve one or several dataset name or a single one. In the case of several dataset name we get (real_motor_name, bliss_alias) If the motor moves then this is pretty simple the real motor_name dataset exists. But if the motors does not move during the bliss scan (scalar value) then the real_motor_name dataset doesn't exists and the bliss alias does. This is why we need to keep both and check both during the 'standard process'... """ if not isinstance(technique_group, h5py.Group): raise TypeError( f"instrument_group is expected to be an instance of {h5py.Group}. {type(technique_group)} provided" ) if "tomoconfig" not in technique_group: raise KeyError("could find 'tomoconfig' key") else: tomo_config_group = technique_group.get("tomoconfig") def get_dataset(group, dataset_name, default, dtype=tuple): if dataset_name not in group: return default dataset = h5py_read_dataset(group[dataset_name]) # note: the dataset are read as string '["detector"]' and must be converted to dtype if dtype is tuple: return convert_str_to_tuple(tuple(dataset)) elif dtype is bool: return convert_str_to_bool(dataset) else: raise TypeError(f"invalid dtype: {dtype}") tomo_config = TomoConfig() tomo_config.rotation = get_dataset(tomo_config_group, "rotation", None) tomo_config.rotation_is_clockwise = get_dataset( tomo_config_group, "rotation_is_clockwise", default=None, dtype=bool ) tomo_config.sample_u = get_dataset(tomo_config_group, "sample_u", None) tomo_config.sample_v = get_dataset(tomo_config_group, "sample_v", None) tomo_config.sample_x = get_dataset(tomo_config_group, "sample_x", None) tomo_config.sample_y = get_dataset(tomo_config_group, "sample_y", None) tomo_config.tomo_detector = get_dataset(tomo_config_group, "detector", None) # translation x == sample_u tomo_config.translation_x = get_dataset( tomo_config_group, "sample_u", None ) # Needs translation X mapping tomo_config.translation_y = get_dataset( tomo_config_group, "translation_y", None ) tomo_config.translation_z = get_dataset( tomo_config_group, "translation_z", None ) return tomo_config nxtomomill-v2.0.1/nxtomomill/converter/hdf5/acquisition/multitomo.py000066400000000000000000000367471511430602400261110ustar00rootroot00000000000000# coding: utf-8 """ module to define a multi-tomo acquistion (also know as pco-tomo) """ from __future__ import annotations import logging from silx.io.url import DataUrl from silx.utils.proxy import docstring from silx.utils.deprecation import deprecated from nxtomo.application.nxtomo import NXtomo try: from nxtomo.utils.NXtomoSplitter import NXtomoSplitter except ImportError: from nxtomo.utils.detectorsplitter import ( NXtomoDetectorDataSplitter as NXtomoSplitter, ) from nxtomo.nxobject.nxdetector import ImageKey from nxtomomill.converter.hdf5.acquisition.baseacquisition import EntryReader from nxtomomill.converter.hdf5.acquisition.standardacquisition import ( StandardAcquisition, ) from nxtomomill.models.h52nx._acquisitionstep import AcquisitionStep from nxtomomill.io.config import TomoHDF5Config _logger = logging.getLogger(__name__) __all__ = [ "MultiTomoAcquisition", ] class MultiTomoAcquisition(StandardAcquisition): """ A multitomo is an acquisition that we want to split into several NXtomos. It is contain in a bliss scan and looks like the following at bliss side: * scan "description", title can be tomo:pcotomo ro tomo:multitomo for example * Optional scan dark * Optional scan flat * scan "projections" * Optional scan flat The scan "projections" contains several "tomo" with a parameter evolving with time like heat or pressure. The idea is that we want to split those into several NXtomo. Those NXtomo must duplicate dark and flat scans For example if scan "projections" contains nb_loop = 2 and nb_loop = 3 we must create nb_loop*nb_loop == 6 NXtomo as output. Split of those can are done in postprocessing on the to_NXtomos function """ def __init__( self, root_url: DataUrl | None, configuration: TomoHDF5Config, detector_sel_callback, start_index, ): super().__init__( root_url=root_url, configuration=configuration, detector_sel_callback=detector_sel_callback, start_index=start_index, ) self._nb_loop = None self._nb_tomo = None self._nb_turns = None self._entries_to_split = set() # set of URL as str def _preprocess_registered_entries(self): # those must be defined before calling super because the super will call then `_preprocess_registered_entry` self._nb_loop = None self._nb_tomo = None self._nb_turns = None self._entries_to_split = set() super()._preprocess_registered_entries() def _read_nb_loop(self, entry_url: DataUrl): return self.get_nb_loop(entry_url) or self.get_nb_loop(self.root_url) def _read_nb_tomo(self, entry_url: DataUrl): return self.get_nb_tomo(entry_url) or self.get_nb_tomo(self.root_url) def _read_nb_turns(self, entry_url: DataUrl): return self.get_nb_turns(entry_url) or self.get_nb_turns(self.root_url) def _preprocess_registered_entry(self, entry_url, type_): super()._preprocess_registered_entry(entry_url=entry_url, type_=type_) if type_ is AcquisitionStep.PROJECTION: # nb loop parameter must be present only on projection entries nb_loop = self._read_nb_loop(entry_url=entry_url) if ( nb_loop is not None ): # at this moment 02/2022 nb_loop is only defined on projection type if self._nb_loop is None or self._nb_loop == nb_loop: self._nb_loop = nb_loop self._entries_to_split.add(entry_url.path()) else: _logger.error( f"Found entries with a different number of nb_loop: {entry_url.path()}" ) nb_tomo = self._read_nb_tomo(entry_url=entry_url) if ( nb_tomo is not None ): # at this moment 02/2022 nb_loop is only defined on projection type if self._nb_tomo is None or self._nb_tomo == nb_tomo: self._nb_tomo = nb_tomo self._entries_to_split.add(entry_url.path()) else: _logger.error( f"Found entries with a different number of _nb_tomo: {entry_url.path()}" ) nb_turns = self._read_nb_turns(entry_url=entry_url) if nb_turns is not None: if self._nb_turns is None or self._nb_turns == nb_turns: self._nb_turns = nb_turns self._entries_to_split.add(entry_url.path()) else: _logger.error( f"Found entries with a different number of nb_turns: {entry_url.path()}" ) def get_nb_loop(self, url) -> int | None: with EntryReader(url) as entry: for path in self._NB_LOOP_PATH: if path in entry: return entry[path][()] return None def get_nb_tomo(self, url) -> int | None: with EntryReader(url) as entry: for path in self._NB_TOMO_PATH: if path in entry: return entry[path][()] return None def get_nb_turns(self, url) -> int | None: with EntryReader(url) as entry: for path in self._NB_TURNS_PATH: if path in entry: return entry[path][()] return None def get_expected_nx_tomo(self): # the number of expected NXtomo is saved with projection # and not with the init title. This is why it but be computed later return 0 @deprecated( replacement="get_multitomo_version", reason="rename pcotomo to multitomo", since_version="1.2", ) def get_pcotomo_version(self, url: DataUrl): return self.get_multitomo_version(url=url) def get_multitomo_version(self, url: DataUrl): """ return the multitomo version according to the provider version (aka bliss) """ with EntryReader( url=DataUrl(file_path=url.file_path(), data_path="/", scheme=url.scheme()) ) as entry: if "creator_version" in entry.attrs: creator_version = entry.attrs["creator_version"] try: full_version = creator_version.split("-")[0].split("+")[0] major, minor, _ = full_version.split(".") except Exception as e: _logger.warning( f"Fail to convert creator_version ({creator_version}) to a valid version number. Error is {e}" ) return None else: if major == "0" or (major == "1" and int(minor) < 9): return 1 else: return 2 return None @docstring(StandardAcquisition) def to_NXtomos( self, request_input, input_callback, check_tomo_n: bool = True ) -> tuple: multitomo_version = self.get_multitomo_version(url=self.root_url) if multitomo_version is None: multitomo_version = 2 _logger.warning( f"No multi-tomo (also know as pco-tomo) version found. Try to convert it using the latest version (v{multitomo_version})" ) if multitomo_version == 1: return self.to_NXtomos_multitomo_v1( request_input=request_input, input_callback=input_callback, check_tomo_n=check_tomo_n, ) elif multitomo_version == 2: return self.to_NXtomos_multitomo_v2( request_input=request_input, input_callback=input_callback, check_tomo_n=check_tomo_n, ) else: raise ValueError( f"{multitomo_version} version of the multi-tomo found. No handling defined for it" ) @deprecated( replacement="to_NXtomos_multitomo_v2", reason="rename pcotomo to multitomo", since_version="1.2", ) def to_NXtomos_pcotomo_v2( self, request_input, input_callback, check_tomo_n: bool = True ) -> tuple: return self.to_NXtomos_multitomo_v2( request_input=request_input, input_callback=input_callback, check_tomo_n=check_tomo_n, ) def to_NXtomos_multitomo_v2( self, request_input, input_callback, check_tomo_n: bool = True ) -> tuple: """ The second version of 'to_NXtomos' for the second way of storing multi-tomo information. In this version we expect to have: * nb_turns: number of NXtomo to generate * proj_n: number of projections per NXtomo * scan_range: rotation angle scope """ nx_tomos = super().to_NXtomos(request_input, input_callback, check_tomo_n=False) if len(nx_tomos) == 0: return nx_tomos if self._nb_turns is None: error_msg = "unable to find nb_turns information to split the NXtomo" _logger.error(error_msg) raise ValueError(error_msg) # if we want to split the NXtomo from `nb_tomo` and `nb_loop` _logger.info( "apply split from `nb_turns` and `scan_range`. `start_angle_offset`, `angle_interval` and `n_tomo` will be ignored" ) results = [] for nx_tomo in nx_tomos: splitter = NXtomoSplitter(nx_tomo) projections_slices = self._get_projections_slices(nx_tomo) if len(projections_slices) > 1: # insure projections are contiguous otherwise we don't know how to split it. # not defined on the current design from bliss. should never happen raise ValueError("Expect all projections to be contiguous") elif len(projections_slices) == 0: raise ValueError("No projection found") else: results.extend( splitter.split( projections_slices[0], nb_part=int(self._nb_turns), tomo_n=self._get_tomo_n(), ) ) if check_tomo_n: # if angle offset provided there is an hight probably this will mess up with the tomo_n, which is expected... self.check_tomo_n() return tuple(results) @deprecated( replacement="to_NXtomos_multitomo_v1", reason="rename pcotomo to multitomo", since_version="1.2", ) def to_NXtomos_pcotomo_v1( self, request_input, input_callback, check_tomo_n: bool = True ) -> tuple: return self.to_NXtomos_multitomo_v1( request_input=request_input, input_callback=input_callback, check_tomo_n=check_tomo_n, ) def to_NXtomos_multitomo_v1( self, request_input, input_callback, check_tomo_n: bool = True ) -> tuple: """ The first version of 'to_NXtomos' for the first way of storing multitomo information. In this version we expect to have: * `nb_loop`: define the number of turn * `nb_tomo`: define the number of 'sequence' per turn. One sequence will generate one NXtomo """ nx_tomos = super().to_NXtomos(request_input, input_callback, check_tomo_n=False) if len(nx_tomos) == 0: return nx_tomos # apply multitomo tomo_n, angle_interval and start_angle_offset # this will select if necessary a subset of the NXtomo to apply user requested offset angle_offset = self.configuration.multitomo_start_angle_offset scan_range = self.configuration.multitomo_scan_range if scan_range not in (180, 360): _logger.warning( f"strange angle_interval provided: {scan_range} when 180 or 360 expected" ) results = [] if angle_offset is not None: # if we want to split the NXtomo from angles and settings provided by the user: _logger.info( "apply filtering from `start_angle_offset`, `angle_interval` and `n_tomo`. `nb_tomo` and `nb_loop` will be ignored" ) for nx_tomo in nx_tomos: for i_part in range(self.configuration.multitomo_n_nxtomo): start_angle_offset = (angle_offset or 0) + (i_part * scan_range) results.append( NXtomo.sub_select_from_angle_offset( nx_tomo=nx_tomo, start_angle_offset=start_angle_offset, angle_interval=scan_range, shift_angles=False, copy=True, ) ) else: # if we want to split the NXtomo from `nb_tomo` and `nb_loop` _logger.info( "apply split from `nb_tomo` and `nb_loop`. `start_angle_offset`, `angle_interval` and `n_tomo` will be ignored" ) for nx_tomo in nx_tomos: splitter = NXtomoSplitter(nx_tomo) projections_slices = self._get_projections_slices(nx_tomo) if len(projections_slices) > 1: # insure projections are contiguous otherwise we don't know how to split it. # not defined on the current design from bliss. should never happen raise ValueError("Expect all projections to be contiguous") elif len(projections_slices) == 0: raise ValueError("No projection found") else: results.extend( splitter.split( projections_slices[0], nb_part=int(self._nb_loop * self._nb_tomo), ) ) if self.configuration.multitomo_shift_angles: _logger.info(f"will shift angle to 0-{scan_range}") results = [ NXtomo.clamp_angles( nx_tomo, angle_range=scan_range, offset=angle_offset or 0, copy=False, image_keys=(ImageKey.PROJECTION, ImageKey.ALIGNMENT), ) for nx_tomo in results ] if angle_offset is not None and check_tomo_n: # if angle offset provided there is an hight probably this will mess up with the tomo_n, which is expected... self.check_tomo_n() return tuple(results) @staticmethod def _get_projections_slices(nx_tomo: NXtomo) -> tuple: """Return a tuple of slices for each group of contiguous projections""" if nx_tomo.instrument.detector.image_key_control is None: return () res = [] start_pos = -1 browsing_projection = False for i_frame, image_key in enumerate( nx_tomo.instrument.detector.image_key_control ): image_key_value = ImageKey(image_key) if image_key_value is ImageKey.PROJECTION and not browsing_projection: browsing_projection = True start_pos = i_frame elif browsing_projection and image_key_value is not ImageKey.PROJECTION: res.append(slice(start_pos, i_frame, 1)) start_pos = -1 browsing_projection = False else: if browsing_projection is True: res.append(slice(start_pos, i_frame + 1, 1)) return tuple(res) nxtomomill-v2.0.1/nxtomomill/converter/hdf5/acquisition/pcotomoacquisition.py000066400000000000000000000011531511430602400277700ustar00rootroot00000000000000from .multitomo import MultiTomoAcquisition from nxtomomill.utils.io import deprecated_warning class PCOTomoAcquisition(MultiTomoAcquisition): def __init__(self, root_url, configuration, detector_sel_callback, start_index): deprecated_warning( type_="class", name=PCOTomoAcquisition, reason="rename PCOTomoAcquisition to MultiTomoAcquisition", replacement="nxtomomill.converter.hdf5.acquisition.multitomo.MultiTomoAcquisition", since_version="1.2", ) super().__init__(root_url, configuration, detector_sel_callback, start_index) nxtomomill-v2.0.1/nxtomomill/converter/hdf5/acquisition/standardacquisition.py000066400000000000000000001503571511430602400301230ustar00rootroot00000000000000# coding: utf-8 """ module to define a standard tomography acquisition (made by bliss) """ from __future__ import annotations import pint from datetime import datetime import h5py from silx.io.url import DataUrl from silx.io.utils import h5py_read_dataset from nxtomo.utils.transformation import DetXFlipTransformation, DetYFlipTransformation from nxtomo.nxobject.nxsource import SourceType from nxtomo.nxobject.nxdetector import ImageKey from nxtomo.nxobject.utils.concatenate import ( concatenate_pint_quantities as _concatenate_pint_quantities, ) from nxtomomill.models.h52nx._acquisitionstep import AcquisitionStep from nxtomomill.utils.utils import str_datetime_to_numpy_datetime64 from nxtomomill.utils.io import deprecated from nxtomomill.utils.hdf5 import get_dataset_unit from .baseacquisition import BaseAcquisition, EntryReader from .utils import ( deduce_machine_current, get_bliss_scan_type, get_nx_detectors, guess_nx_detector, ) try: import hdf5plugin # noqa F401 except ImportError: pass import fnmatch import logging import os import numpy from nxtomomill.converter.hdf5.acquisition.blisstomoconfig import ( TomoConfig as BlissTomoConfig, ) from nxtomomill.io.config import TomoHDF5Config from nxtomo.application.nxtomo import NXtomo _ureg = pint.get_application_registry() _logger = logging.getLogger(__name__) __all__ = ["StandardAcquisition"] class StandardAcquisition(BaseAcquisition): """ Class to collect information from a bliss - hdf scan (see https://bliss.gitlab-pages.esrf.fr/fscan). Once all data is collected a set of NXtomo will be created. Then NXtomo instances will be saved to disk. :param root_url: url of the acquisition. Can be None if this is the initialization entry :param configuration: configuration to use to collect raw data and generate outputs :param detector_sel_callback: possible callback to retrieve missing information """ def __init__( self, root_url: DataUrl | None, configuration: TomoHDF5Config, detector_sel_callback, start_index, parent=None, ): super().__init__( root_url=root_url, configuration=configuration, detector_sel_callback=detector_sel_callback, start_index=start_index, ) self._parent = parent # possible parent. Like for z series self._nx_tomos = [NXtomo()] self._image_key_control = None self._rotation_angle: pint.Quantity | None = None self._sample_x: pint.Quantity | None = None self._sample_y: pint.Quantity | None = None self._translation_y: pint.Quantity | None = None self._translation_z: pint.Quantity | None = None self._lr_flipped: bool | None = None self._ud_flipped: bool | None = None self._unique_detector_names = list() # register names self._virtual_sources = None self._acq_expo_time: pint.Quantity | None = None self._copied_dataset = {} "register dataset copied. Key if the original location as" "DataUrl.path. Value is the DataUrl it has been moved to" self._known_machine_current_am: None | dict = None # store all registered machine current self._frames_timestamp = None # try to deduce time stamp of each frame def parent_root_url(self) -> DataUrl | None: if self._parent is not None: return self._parent.root_url else: return None def get_expected_nx_tomo(self): return 1 @property def image_key_control(self): return self._image_key_control @property def rotation_angle(self): return self._rotation_angle @property def translation_y(self): return self._translation_y @property def translation_z(self): return self._translation_z @property def lr_flipped(self): return self._lr_flipped @deprecated( replacement="lr_flipped", reason="renamed", since_version="2.0", ) @property def x_flipped(self): return self.lr_flipped @property def ud_flipped(self): return self._ud_flipped @deprecated( replacement="ud_flipped", reason="renamed", since_version="2.0", ) @property def y_flipped(self): return self.ud_flipped @property def n_frames(self): return self._n_frames @property def n_frames_actual_bliss_scan(self): return self._n_frames_actual_bliss_scan @property def dim_1(self): return self._dim_1 @property def dim_2(self): return self._dim_2 @property def data_type(self): return self._data_type @property def expo_time(self): return self._acq_expo_time @property def known_machine_current(self) -> dict | None: """ Return the dict of all known machine currents. Key is the time stamp, value is the machine current """ return self._known_machine_current_am @property def sample_x(self): """Return the '_sample_x' attribute. In **ESRF coordinate**""" return self._sample_x @property def sample_y(self): """Return the '_sample_y' attribute. In **ESRF coordinate**""" return self._sample_y def register_step( self, url: DataUrl, entry_type: AcquisitionStep | None = None, copy_frames=False ) -> None: """ :param url: entry to be registered and contained in the acquisition :param entry_type: type of the entry if know. Overwise will be 'evaluated' """ if entry_type is None: entry_type = get_bliss_scan_type(url=url, configuration=self.configuration) assert ( entry_type is not AcquisitionStep.INITIALIZATION ), "Initialization are root node of a new sequence and not a scan of a sequence" if entry_type is None: _logger.warning(f"{url} not recognized, skip it") else: self._registered_entries[url.path()] = entry_type self._copy_frames[url.path()] = copy_frames self._entries_o_path[url.path()] = url.data_path() # path from the original file. Haven't found another way to get it ?! def _get_valid_camera_names(self, instrument_grp: h5py.Group): # 1: try to get detector from nx property detectors = get_nx_detectors(instrument_grp) detectors = [grp.name.split("/")[-1] for grp in detectors] def filter_detectors(det_grps): if len(det_grps) > 0: _logger.info(f"{len(det_grps)} detector found from NX_class attribute") if len(det_grps) > 1: # if an option: pick the first one once orderered # else ask user if self._detector_sel_callback is None: sel_det = det_grps[0] _logger.warning( f"several detector found. Only one is managed for now. Will pick {sel_det}" ) else: sel_det = self._detector_sel_callback(det_grps) if sel_det is None: _logger.warning("no detector given, avoid conversion") det_grps = (sel_det,) return det_grps return None detectors = filter_detectors(det_grps=detectors) if detectors is not None: return detectors # 2: get nx detector from shape... detectors = guess_nx_detector(instrument_grp) detectors = [grp.name.split("/")[-1] for grp in detectors] return filter_detectors(det_grps=detectors) @staticmethod def concatenate_pint_quantities(quantities: tuple[pint.Quantity | None]): """ concatenation dedicated to acquisition. quantities items can be None or a quantity """ return _concatenate_pint_quantities( tuple( filter( lambda a: a is not None and len(a) > 0, quantities, ) ) ) def __get_data_from_camera( self, data_dataset: h5py.Dataset, data_name, frame_type, entry, entry_path, camera_dataset_url, ): if data_dataset.ndim == 2: shape = (1, data_dataset.shape[0], data_dataset.shape[1]) elif data_dataset.ndim != 3: err = f"dataset {data_name} is expected to be 3D when {data_dataset.ndim}D found." if data_dataset.ndim == 1: err = "\n".join( [ err, "This might be a bliss-EDF dataset. Those are not handled by nxtomomill", ] ) _logger.error(err) return 0 else: shape = data_dataset.shape n_frame = shape[0] self._n_frames += n_frame self._n_frames_actual_bliss_scan = n_frame if self.dim_1 is None: self._dim_2 = shape[1] self._dim_1 = shape[2] else: if self._dim_1 != shape[2] or self._dim_2 != shape[1]: raise ValueError("Inconsistency in detector shapes") if self._data_type is None: self._data_type = data_dataset.dtype elif self._data_type != data_dataset.dtype: raise ValueError("detector frames have incoherent " "data types") # update image_key and image_key_control # Note: for now there is no image_key on the master file # should be added later. image_key_control = frame_type.to_image_key_control() self._image_key_control.extend([image_key_control.value] * n_frame) data_dataset_path = data_dataset.name.replace(entry.name, entry_path, 1) # replace data_dataset name by the original entry_path. # this is a workaround to use the dataset path on the # "treated file". Because .name if the name on the 'target' # file of the virtual dataset v_source = h5py.VirtualSource( camera_dataset_url.file_path(), data_dataset_path, data_dataset.shape, dtype=self._data_type, ) self._virtual_sources.append(v_source) self._virtual_sources_len.append(n_frame) return n_frame def _treate_valid_camera( self, detector_node, entry, frame_type, input_file_path, entry_path, entry_url ) -> bool: """ treate a dataset considered as a 'camera' dataset. """ if "data_cast" in detector_node: _logger.warning( f"!!! looks like this data has been cast. Take cast data for {detector_node}!!!" ) data_dataset = detector_node["data_cast"] data_name = "/".join((detector_node.name, "data_cast")) elif "data" in detector_node: data_dataset = detector_node["data"] data_name = "/".join((detector_node.name, "data")) else: raise KeyError(f"Unable to find camera dataset in {detector_node.name}") camera_dataset_url = DataUrl( file_path=entry_url.file_path(), data_path=data_name, scheme="silx" ) n_frame = self.__get_data_from_camera( data_dataset, data_name=data_name, frame_type=frame_type, entry=entry, entry_path=entry_path, camera_dataset_url=camera_dataset_url, ) # save information if this url must be embed / copy or not. Will be used later at nxtomo side self._copy_frames[camera_dataset_url.path()] = self._copy_frames[ entry_url.path() ] soft_lr_flip, soft_ud_flip = self._get_soft_flip_frame() # handle None flip values soft_lr_flip = soft_lr_flip or False soft_ud_flip = soft_ud_flip or False # get the final flip value (soft + mechanical) lr_flipped = soft_lr_flip ^ self.configuration.mechanical_lr_flip ud_flipped = soft_ud_flip ^ self.configuration.mechanical_ud_flip if self._lr_flipped is None and self._ud_flipped is None: # if this is the first scan then define frame flips self._lr_flipped, self._ud_flipped = bool(lr_flipped), bool(ud_flipped) elif lr_flipped != self._lr_flipped or ud_flipped != self._ud_flipped: # else check it is consistent with already existing flips raise ValueError( f"Found different detector flips inside the same sequence on {entry}. Unable to handle it." ) # store rotation rots = self._get_rotation_angle(root_node=entry, n_frame=n_frame) # handle rotation_is_clockwise. It None or False consider it is counter-clockwise # NXtomo coordinate system uses MCstas that uses counter-clockwise rotation direction. # https://manual.nexusformat.org/design.html#the-nexus-coordinate-system if self.configuration.rotation_is_clockwise is True: rots *= -1.0 self._rotation_angle = StandardAcquisition.concatenate_pint_quantities( ( self._rotation_angle, rots, ), ) self._sample_x = StandardAcquisition.concatenate_pint_quantities( ( self._sample_x, self._get_sample_x(root_node=entry, n_frame=n_frame), ), ) self._sample_y = StandardAcquisition.concatenate_pint_quantities( ( self._sample_y, self._get_sample_y(root_node=entry, n_frame=n_frame), ), ) self._translation_y = StandardAcquisition.concatenate_pint_quantities( ( self._translation_y, self._get_translation_y(root_node=entry, n_frame=n_frame), ), ) self._translation_z = StandardAcquisition.concatenate_pint_quantities( ( self._translation_z, self._get_translation_z(root_node=entry, n_frame=n_frame), ), ) # store acquisition time self._acq_expo_time = StandardAcquisition.concatenate_pint_quantities( ( self._acq_expo_time, self._get_expo_time( root_node=entry, detector_node=detector_node, n_frame=n_frame, ), ) ) self._current_scan_n_frame = n_frame def camera_is_valid(self, det_name): assert isinstance(det_name, str) if len(self.configuration.valid_camera_names) == 0: return True for vcm in self.configuration.valid_camera_names: if fnmatch.fnmatch(det_name, vcm): return True return False def _preprocess_registered_entry(self, entry_url, type_): with EntryReader(entry_url) as entry: entry_path = self._entries_o_path[entry_url.path()] input_file_path = entry_url.file_path() input_file_path = os.path.abspath( os.path.relpath(input_file_path, os.getcwd()) ) input_file_path = os.path.abspath(input_file_path) if type_ is AcquisitionStep.INITIALIZATION: raise RuntimeError( "no initialization should be registered." "There should be only one per acquisition." ) if "instrument" not in entry: _logger.error( f"no instrument group found in {entry.name}, unable to retrieve frames" ) return instrument_grp = entry["instrument"] # if we don't get a valid camera (not provided by the user or not found on the bliss tomo metadata) if len(self.configuration.valid_camera_names) == 0: # if we need to guess detector name(s) # ignore in case we read information from bliss config det_grps = self._get_valid_camera_names(instrument_grp) # update valid camera names self.configuration.valid_camera_names = det_grps has_frames = False for key, _ in instrument_grp.items(): if ( "NX_class" in instrument_grp[key].attrs and instrument_grp[key].attrs["NX_class"] == "NXdetector" ): _logger.debug(f"Found one detector at {key} for {entry.name}.") if not self.camera_is_valid(key): _logger.debug(f"ignore {key}, not a `valid` camera name") continue else: detector_node = instrument_grp[key] if key not in self._unique_detector_names: self._unique_detector_names.append(key) try: self._treate_valid_camera( detector_node, entry=entry, frame_type=type_, input_file_path=input_file_path, entry_path=entry_path, entry_url=entry_url, ) except KeyError as e: if self.configuration.raises_error: raise e has_frames = False else: has_frames = True # try to get some other metadata # handle frame time stamp start_time = self._get_start_time(entry) if start_time is not None: start_time = datetime.fromisoformat(start_time) end_time = self._get_end_time(entry) if end_time is not None: end_time = datetime.fromisoformat(end_time) if has_frames: self._register_frame_timestamp(entry, start_time, end_time) # handle machine current. Can retrieve some current even on bliss scan entry not containing directly frames self._register_machine_current(entry, start_time, end_time) def _register_machine_current(self, entry: h5py.Group, start_time, end_time): """Update machine electric current for provided entry (bliss scan""" machine_currents: pint.Quantity | None = self._get_machine_current( root_node=entry ) # electric current will be saved as Ampere if machine_currents is not None and len(machine_currents) > 0: machine_currents = machine_currents.to(_ureg.ampere).magnitude new_know_machine_currents_am = {} if start_time is None or end_time is None: if start_time != end_time: _logger.warning( f"Unable to find {'start_time' if start_time is None else 'end_time'}. Will pick the first available machine_current for the frame" ) t_time = start_time or end_time # if at least one can find out new_know_machine_currents_am[ str_datetime_to_numpy_datetime64(t_time) ] = machine_currents[0] else: _logger.error( "Unable to find start_time and end_time. Will not register any machine current" ) elif len(machine_currents) == 1: # if we have only one value, consider the machine current is constant during this time # might be improved later if we can know if current is determine at the # beginning or the end. But should have no impact # as the time slot is short new_know_machine_currents_am[ str_datetime_to_numpy_datetime64(start_time) ] = machine_currents[0] else: # linspace from datetime within ms precision. # see https://stackoverflow.com/questions/37964100/creating-numpy-linspace-out-of-datetime timestamps = numpy.linspace( start=str_datetime_to_numpy_datetime64(start_time).astype( numpy.float128 ), stop=str_datetime_to_numpy_datetime64(end_time).astype( numpy.float128 ), num=len(machine_currents), endpoint=True, dtype=" tuple: values, unit = self._get_node_values_for_frame_array( node=root_node["measurement"], n_frame=n_frame, keys=self.configuration.diode_keys, info_retrieve="diode", expected_unit=_ureg.volt, ) return values, unit def _generic_path_getter(self, paths: tuple, message, level="warning", entry=None): """ :param level: level can be logging.level values : "warning", "error", "info" :param H5group entry: user can provide directly an entry to be used as an open h5Group """ if not isinstance(paths, tuple): raise TypeError url = self.parent_root_url() or self.root_url if url is not None: self._check_has_metadata(url) def process(h5_group): for path in paths: if h5_group is not None and path in h5_group: return h5py_read_dataset(h5_group[path]) if message is not None: getattr(_logger, level)(message) if entry is None: if url is None: return None with EntryReader(url) as h5_group: return process(h5_group) else: return process(entry) def _get_source_name(self): """ """ return self._generic_path_getter( paths=self._SOURCE_NAME, message="Unable to find source name", level="info" ) def _get_source_type(self): """ """ return self._generic_path_getter( paths=self._SOURCE_TYPE, message="Unable to find source type", level="info" ) def _get_title(self): """return acquisition title""" return self._generic_path_getter( paths=self.TITLE_PATHS, message="Unable to find title" ) def _get_instrument_name(self): """:return instrument instrument name (aka beamline name)""" name = self._generic_path_getter( paths=self._INSTRUMENT_NAME_PATH, message="Unable to find instrument name", level="info", ) # on some path / old hdf5 the name is prefixed by "ESRF:". clean those if name is not None and name.startswith("ESRF:"): name = name.replace("ESRF:", "") return name def _get_dataset_name(self): """return name of the acquisition""" return self._generic_path_getter( paths=self._DATASET_NAME_PATH, message="No name describing the acquisition has been " "found, Name dataset will be skip", ) def _get_sample_name(self): """return sample name""" return self._generic_path_getter( paths=self._SAMPLE_NAME_PATH, message="No sample name has been " "found, Sample name dataset will be skip", ) def _get_grp_size(self): """return the nb_scans composing the zseries if is part of a group of sequence""" return self._generic_path_getter(paths=self._GRP_SIZE_PATH, message=None) def _get_tomo_n(self): return self._generic_path_getter( paths=self._TOMO_N_PATH, message="unable to find information regarding tomo_n", ) def _get_start_time(self, entry=None): return self._generic_path_getter( paths=self._START_TIME_PATH, message="Unable to find start time", level="info", entry=entry, ) def _get_end_time(self, entry=None): return self._generic_path_getter( paths=self._END_TIME_PATH, message="Unable to find end time", level="info", entry=entry, ) def _get_soft_flip_frame(self): """ Retrieve registered frame flip (know by bliss-tomo) """ url = self.parent_root_url() or self.root_url if url is None: return None, None self._check_has_metadata(url) with EntryReader(url) as entry: for flip_path in self._FRAME_FLIP_PATHS: if len(self._unique_detector_names) > 0: key = flip_path.format(detector_name=self._unique_detector_names[0]) else: key = flip_path if key in entry: return h5py_read_dataset(entry[key]) else: return None, None def _get_propagation_distance(self) -> pint.Quantity: url = self.parent_root_url() or self.root_url if url is None: return None self._check_has_metadata(url) with EntryReader(url) as entry: for prog_dst_path in self._PROPAGATION_DISTANCE_PATHS: if prog_dst_path in entry: dataset = entry[prog_dst_path] prog_dst = h5py_read_dataset(dataset) unit = get_dataset_unit( dataset, default=_ureg.mm, from_dataset=f"{dataset.name} (reading propagation distance)", ) return prog_dst * unit else: return None def _get_energy(self, ask_if_0, input_callback) -> pint.Quantity | None: """Try to read the energy from root url. If fails and if a input_callback given then execute this fallback """ url = self.parent_root_url() or self.root_url if url is None: return None self._check_has_metadata() with EntryReader(url) as entry: if self._ENERGY_PATH in entry: dataset = entry[self._ENERGY_PATH] energy = h5py_read_dataset(dataset) unit = get_dataset_unit( dataset, default=_ureg.keV, from_dataset=f"{dataset.name} (from reading energy)", ) if energy == 0 and ask_if_0: desc = ( "Energy has not been registered. Please enter " "incoming beam energy (in kev):" ) if input_callback is None: en = input(desc) else: en = input_callback("energy", desc) if energy is not None: energy = float(en) return energy * unit else: mess = f"unable to find energy for entry {entry}." if self.raise_error_if_issue: raise ValueError(mess) else: mess += " Default value will be set (19kev)" _logger.warning(mess) return 19.0 * _ureg.keV def _get_sample_detector_distance(self) -> pint.Quantity | None: """return tuple(distance, unit)""" url = self.parent_root_url() or self.root_url if url is None: return None self._check_has_metadata(url) with EntryReader(url) as entry: for key in self.configuration.sample_detector_distance_keys: if key in entry: dataset = entry[key] distance = h5py_read_dataset(dataset) unit = get_dataset_unit( dataset, default=_ureg.mm, from_dataset=f"{dataset.name} (from reading sample-detector distance)", ) # convert to meter return distance * unit mess = f"unable to find distance for entry {entry}." if self.raise_error_if_issue: raise ValueError(mess) else: mess += "Default value will be set (1m)" _logger.warning(mess) return 1.0 * _ureg.meter def _get_source_sample_distance(self) -> pint.Quantity | None: """return tuple(distance, unit)""" url = self.parent_root_url() or self.root_url if url is None: return None self._check_has_metadata(url) with EntryReader(url) as entry: for key in self.configuration.source_sample_distance_keys: if key in entry: dataset = entry[key] distance = h5py_read_dataset(dataset) unit = get_dataset_unit( dataset, default=_ureg.mm, from_dataset=f"{dataset.name} (from reading source-sample distance)", ) # convert to meter return distance * unit mess = f"unable to find distance for entry {entry}." if self.raise_error_if_issue: raise ValueError(mess) else: mess += "Default value will be set (1m)" _logger.warning(mess) return 1.0 * _ureg.meter def _get_sample_pixel_size(self, axis) -> pint.Quantity | None: """read **sample** pixel size from predefined set of path""" return self._get_pixel_size( axis=axis, x_pixel_size_paths=self.configuration.sample_x_pixel_size_keys, y_pixel_size_paths=self.configuration.sample_y_pixel_size_keys, ) def _get_detector_pixel_size(self, axis) -> pint.Quantity | None: """read **detector** pixel size from predefined set of path""" return self._get_pixel_size( axis=axis, x_pixel_size_paths=self.configuration.detector_x_pixel_size_keys, y_pixel_size_paths=self.configuration.detector_y_pixel_size_keys, ) def _get_pixel_size(self, axis, x_pixel_size_paths, y_pixel_size_paths): url = self.parent_root_url() or self.root_url if url is None: return None if axis not in ("x", "y"): raise ValueError self._check_has_metadata() if axis == "x": keys = x_pixel_size_paths elif axis == "y": keys = y_pixel_size_paths else: raise ValueError(f"axis {axis} is invalid") # solve according to detector name if len(self._unique_detector_names) > 1: _logger.warning( f"More than one detector found. Will pick the first one found ({self._unique_detector_names[0]})" ) if len(self._unique_detector_names) > 0: keys = [ key.format(detector_name=self._unique_detector_names[0]) for key in keys ] self._unique_detector_names with EntryReader(url) as entry: for key in keys: if key in entry: dataset = entry[key] dataset_value = h5py_read_dataset(dataset) # if the pixel size is provided as x, y if isinstance(dataset_value, numpy.ndarray): if len(dataset_value) > 1 and axis == "y": size_ = dataset_value[1] else: size_ = dataset_value[0] # if this is a single value else: size_ = dataset_value unit = get_dataset_unit( dataset, default=_ureg.micrometer, from_dataset=f"{dataset.name} (from reading pixel size)", ) # convert to meter return size_ * unit mess = f"unable to find {axis} sample pixel size for entry {entry}" if self.raise_error_if_issue: raise ValueError(mess) else: mess += "Value will be set to default (10-6m)" _logger.warning(mess) return 10e-6 * _ureg.meter def _get_field_of_view(self) -> str: if self.configuration.field_of_view is not None: return self.configuration.field_of_view.value url = self.parent_root_url() or self.root_url if url is None: return None with EntryReader(url) as entry: if self._FOV_PATH in entry: return h5py_read_dataset(entry[self._FOV_PATH]) else: # FOV is optional: don't raise an error _logger.warning( f"unable to find information regarding field of view for entry {entry}. set it to default value (Full)" ) return "Full" def _update_configuration_from_tomo_config(self): """ force some values from EBS tomo 'tomoconfig' group to make sure correct dataset are read """ if self.configuration.ignore_bliss_tomo_config: return url = self.parent_root_url() or self.root_url if url is None: # case of entries are made manually and user do not provide an init node. return with EntryReader(url) as entry: technique_grp = entry.get("technique", None) if technique_grp is None: _logger.warning( f"Unable to find a technique group in {entry}. Unable to reach EBStomo metadata" ) return bliss_tomo_version = technique_grp.attrs.get("tomo_version", None) # read metadata try: bliss_metadata = BlissTomoConfig.from_technique_group( technique_group=technique_grp ) except KeyError: if bliss_tomo_version is not None: _logger.warning( f"Unable to find bliss 'tomo_config' when expected (tomo_version={bliss_tomo_version}). Fallback to conversion based on list of paths to check" ) else: # check if some metadata are missing metadata_values = { "detector": bliss_metadata.tomo_detector, "sample_x": bliss_metadata.sample_x, "sample_y": bliss_metadata.sample_y, "translation_z": bliss_metadata.translation_z, "rotation": bliss_metadata.rotation, "rotation_is_clockwise": bliss_metadata.rotation_is_clockwise, } missing_metadata = list( [k for k, v in metadata_values.items() if v is None] ) _logger.info(f"read tomo config from bliss. Get {metadata_values}") if len(missing_metadata) > 0: _logger.warning( f"couldn't find {missing_metadata} in bliss 'technique/tomoconfig' dataset" ) if bliss_metadata.tomo_detector is not None: self.configuration.valid_camera_names = bliss_metadata.tomo_detector if bliss_metadata.sample_x is not None: self.configuration.sample_x_keys = bliss_metadata.sample_x if bliss_metadata.sample_y is not None: self.configuration.sample_y_keys = bliss_metadata.sample_y if bliss_metadata.translation_y is not None: self.configuration.translation_y_keys = bliss_metadata.translation_y if bliss_metadata.translation_z is not None: self.configuration.translation_z_keys = bliss_metadata.translation_z if bliss_metadata.rotation is not None: self.configuration.rotation_angle_keys = bliss_metadata.rotation # if the 'rotation_is_clockwise' has not been defined by the user and if we can access the # metadata from blisstomo take it. if ( bliss_metadata.rotation_is_clockwise is not None and self.configuration.rotation_is_clockwise is None ): self.configuration.rotation_is_clockwise = ( bliss_metadata.rotation_is_clockwise ) def to_NXtomos(self, request_input, input_callback, check_tomo_n=True) -> tuple: self._update_configuration_from_tomo_config() self._preprocess_registered_entries() nx_tomo = NXtomo() # 1. root level information # start and end time nx_tomo.start_time = self._get_start_time() nx_tomo.end_time = self._get_end_time() # title nx_tomo.title = self._get_dataset_name() # group size nx_tomo.group_size = self._get_grp_size() # 2. define beam try: energy: pint.Quantity = self._get_user_settable_parameter( param_key="energy_kev", fallback_fct=self._get_energy, input_callback=input_callback, ask_if_0=request_input, ) except TypeError as e: # if the path lead to unexpected dataset (group instead of a dataset or a broken folder) _logger.error("Fail to get energy. Error is %s", e, exc_info=e) energy = None if energy is not None: # TODO: better management of energy ? might be energy.beam or energy.instrument.beam ? nx_tomo.energy = energy # 3. define instrument nx_tomo.instrument.name = self._get_instrument_name() nx_tomo.instrument.detector.data = self._virtual_sources nx_tomo.instrument.detector.image_key_control = self.image_key_control nx_tomo.instrument.detector.count_time = self._acq_expo_time nx_tomo.instrument.detector.roi = self.get_detector_roi() if self.image_key_control is None: _logger.warning( "No image key defined. Unable to determine the number of frame and the type. This is a mandatory field" ) else: n_frames = len(self.image_key_control) nx_tomo.instrument.detector.sequence_number = numpy.linspace( start=0, stop=n_frames, num=n_frames, dtype=numpy.uint32, endpoint=False ) # sample - detector distance try: sample_detector_distance = self._get_user_settable_parameter( param_key="detector_sample_distance_m", fallback_fct=self._get_sample_detector_distance, ) except TypeError: # if the path lead to unexpected dataset (group instead of a dataset or a broken folder) _logger.error("Fail to get sample/detector distance") sample_detector_distance = None if sample_detector_distance is not None: nx_tomo.instrument.detector.distance = sample_detector_distance # source - sample detector distance try: source_sample_distance: pint.Quantity = self._get_user_settable_parameter( param_key="source_sample_distance_m", fallback_fct=self._get_source_sample_distance, ) except TypeError: # if the path lead to unexpected dataset (group instead of a dataset or a broken folder) _logger.error("Fail to get source/sample distance") source_sample_distance = None if source_sample_distance is not None: # source_sample_distance is positive when the NXtomo application states it should be negative. So # let's make sure this will be negative source_sample_distance = -numpy.abs(source_sample_distance) nx_tomo.instrument.source.distance = source_sample_distance # handle detector & sample ; x & y pixel size for (nx_obj, attr), params in { (nx_tomo.sample, "x_pixel_size"): { "param_key": "x_sample_pixel_size_m", "fallback_fct": self._get_sample_pixel_size, "axis": "x", }, (nx_tomo.sample, "y_pixel_size"): { "param_key": "y_sample_pixel_size_m", "fallback_fct": self._get_sample_pixel_size, "axis": "y", }, (nx_tomo.instrument.detector, "x_pixel_size"): { "param_key": "x_detector_pixel_size_m", "fallback_fct": self._get_detector_pixel_size, "axis": "x", }, (nx_tomo.instrument.detector, "y_pixel_size"): { "param_key": "y_detector_pixel_size_m", "fallback_fct": self._get_detector_pixel_size, "axis": "y", }, }.items(): try: pixel_size = self._get_user_settable_parameter(**params) except TypeError: # if the path lead to unexpected dataset (group instead of a dataset or a broken folder) _logger.error(f"Fail to get {attr} of {nx_obj}") pixel_size = None else: nx_obj.__setattr__(attr, pixel_size) # flips nx_tomo.instrument.detector.transformations.add_transformation( DetYFlipTransformation(flip=self.lr_flipped) ) nx_tomo.instrument.detector.transformations.add_transformation( DetXFlipTransformation(flip=self.ud_flipped) ) # fov fov = self._get_field_of_view() if fov is not None: nx_tomo.instrument.detector.field_of_view = fov # x_rotation_axis_pixel_position # TODO Missing calibration mechanism from Bliss if self.translation_y is None or len(self.translation_y) < 1: _logger.warning("Unable to find translation_y") elif nx_tomo.sample.x_pixel_size is not None: x_sample_pixel_size = nx_tomo.sample.x_pixel_size translation_y_m = ( self.translation_y[0].to(_ureg.meter).magnitude ) # This is very fragile, no idea how to make it better x_sample_pixel_size = x_sample_pixel_size.to(_ureg.meter).magnitude nx_tomo.instrument.detector.x_rotation_axis_pixel_position = ( translation_y_m / x_sample_pixel_size ) # define tomo_n nx_tomo.instrument.detector.tomo_n = self._get_tomo_n() # 4. define nx source source_name = self._get_source_name() nx_tomo.instrument.source.name = source_name source_type = self._get_source_type() if source_type is not None: if "synchrotron" in source_type.lower(): source_type = SourceType.SYNCHROTRON_X_RAY_SOURCE.value # drop a warning if the source type is invalid if source_type not in SourceType.values(): _logger.warning( f"Source type ({source_type}) is not a 'standard value'" ) nx_tomo.instrument.source.type = source_type # 5. define sample # warning: the NXtomo XYZ reference (see https://manual.nexusformat.org/_static/NeXusManual.pdf - p 26) # # # Y axis # ^ X axis # | / # x-ray | / # --------> ------> Z axis # # is not the same as the ESRF XYZ (see https://tomo.gitlab-pages.esrf.fr/bliss-tomo/master/modelization_sample_stage.html) # # Z axis # ^ Y axis # | / # x-ray |/ # --------> ------> X axis # # nx_tomo.sample.name = self._get_sample_name() assert isinstance(self.rotation_angle, (pint.Quantity, type(None))) nx_tomo.sample.rotation_angle = self.rotation_angle assert isinstance(self.sample_x, (pint.Quantity, type(None))) nx_tomo.sample.z_translation = self.sample_x assert isinstance(self.translation_z, (pint.Quantity, type(None))) nx_tomo.sample.y_translation = self.translation_z assert isinstance(self.sample_y, (pint.Quantity, type(None))) nx_tomo.sample.x_translation = self.sample_y z1 = nx_tomo.instrument.source.distance z2 = nx_tomo.instrument.detector.distance propagation_distance: pint.Quantity | None = self._get_propagation_distance() if propagation_distance is not None: _logger.debug("set propagation distance from bliss metadata") nx_tomo.sample.propagation_distance = propagation_distance elif z1 is not None and z2 is not None: _logger.debug( "compute propagation distance from sample-source and sample-detector distances" ) nx_tomo.sample.propagation_distance = (-z1 * z2) / (-z1 + z2) else: distances = { "sample-detector distance": z1, "source-sample distance": z2, } _logger.warning( "Unable to define propagation distance. %s missing", [key for key, value in distances.items() if value is not None], ) # 6. define control if ( self.configuration.create_control_data and self.known_machine_current not in (None, dict()) ): nx_tomo.control.data = ( deduce_machine_current( timestamps=self._frames_timestamp, known_machine_current=self._known_machine_current_am, ) * _ureg.ampere ) types = set() if nx_tomo.control.data is not None: for d in nx_tomo.control.data: types.add(type(d)) if check_tomo_n: self.check_tomo_n() return (nx_tomo,) def check_tomo_n(self): # check scan is complete tomo_n = self._get_tomo_n() if self.configuration.check_tomo_n and tomo_n is not None: image_key_control = numpy.asarray(self._image_key_control) proj_found = len( image_key_control[image_key_control == ImageKey.PROJECTION.value] ) if proj_found < tomo_n: mess = f"Incomplete scan. Expect {tomo_n} projection but only {proj_found} found" if self.configuration.raises_error is True: raise ValueError(mess) else: _logger.error(mess) def _check_has_metadata(self, url: DataUrl | None = None): url = url or self.root_url if url is None: raise ValueError( "no initialization entry specify, unable to" "retrieve energy" ) def _get_user_settable_parameter( self, param_key, fallback_fct, *fallback_args, **fallback_kwargs, ) -> pint.Quantity | None: """ return value, unit :param fallback_fct: callback function to retrieve the value. Must return a quantity """ value = getattr(self.configuration, param_key, None) if value is not None: units = { "energy_kev": _ureg.keV, "x_sample_pixel_size_m": _ureg.meter, "y_sample_pixel_size_m": _ureg.meter, "x_detector_pixel_size_m": _ureg.meter, "y_detector_pixel_size_m": _ureg.meter, "detector_sample_distance_m": _ureg.meter, "source_sample_distance_m": _ureg.meter, } if param_key not in units: raise ValueError(f"key {param_key} is not handled") unit = units[param_key] value = value * unit else: value = fallback_fct(*fallback_args, **fallback_kwargs) assert value is None or isinstance( value, pint.Quantity ), "fallback must return a pint.Quantity" return value nxtomomill-v2.0.1/nxtomomill/converter/hdf5/acquisition/tests/000077500000000000000000000000001511430602400246275ustar00rootroot00000000000000nxtomomill-v2.0.1/nxtomomill/converter/hdf5/acquisition/tests/bliss_tomo_datasets.py000066400000000000000000000015531511430602400312470ustar00rootroot00000000000000"""Util to load dataset from gitlab.esrf.fr:tomo/tomo_test_data""" import os import pytest import shutil from tomoscan.tests.datasets import GitlabProject as _GitlabProject _GitlabBlissTomoHolotomoDataset = _GitlabProject( branch_name="holotomo2", host="https://gitlab.esrf.fr", cache_dir=os.path.join( os.path.dirname(__file__), "__archive__", ), token=None, project_id=4166, # https://gitlab.esrf.fr/tomo/tomo_test_data ) @pytest.fixture def holotomo2_simu_dataset(tmp_path): """ Return a bliss-tomo simulated dataset with latest holo-tomo sequence. """ src_folder = _GitlabBlissTomoHolotomoDataset.get_dataset( os.path.join("simu", "bliss-2.3.0.dev0-tomo-2.9.0", "holotomo2") ) dst_folder = os.path.join(tmp_path, "holotomo2") shutil.copytree(src_folder, dst_folder) return dst_folder nxtomomill-v2.0.1/nxtomomill/converter/hdf5/acquisition/tests/test_acquisition.py000066400000000000000000000027651511430602400306020ustar00rootroot00000000000000# coding: utf-8 import os import tempfile import pytest from silx.io.url import DataUrl from tomoscan.io import HDF5File from nxtomomill.converter.hdf5.acquisition.baseacquisition import ( BaseAcquisition, get_dataset_name_from_motor, ) from nxtomomill.io.config import TomoHDF5Config def test_BaseAquisition(): """simple test of the BaseAcquisition class""" with tempfile.TemporaryDirectory() as folder: file_path = os.path.join(folder, "test.h5") with HDF5File(file_path, mode="w") as h5f: h5f["/data/toto/dataset"] = 12 url = DataUrl(file_path=file_path, data_path="/data/toto", scheme="silx") std_acq = BaseAcquisition( root_url=url, configuration=TomoHDF5Config(), detector_sel_callback=None, start_index=0, ) with std_acq.read_entry() as entry: assert "dataset" in entry def test_get_dataset_name_from_motor(): """test get_dataset_name_from_motor function""" set_1 = ["rotation", "test1", "alias"] assert get_dataset_name_from_motor(set_1, "rotation") == "test1" assert get_dataset_name_from_motor(set_1, "my motor") is None set_2 = ["rotation", "test1", "alias", "x translation", "m2", "test1"] assert get_dataset_name_from_motor(set_2, "rotation") == "test1" assert get_dataset_name_from_motor(set_2, "x translation") == "m2" set_3 = ["rotation"] with pytest.raises(ValueError): get_dataset_name_from_motor(set_3, "rotation") nxtomomill-v2.0.1/nxtomomill/converter/hdf5/acquisition/tests/test_acquisition_utils.py000066400000000000000000000217721511430602400320210ustar00rootroot00000000000000# coding: utf-8 import os import numpy import pytest import h5py from silx.io.url import DataUrl from tomoscan.io import HDF5File from nxtomo.nxobject.nxdetector import ImageKey from nxtomomill.io.config import TomoHDF5Config from nxtomomill.models.h52nx._acquisitionstep import AcquisitionStep from nxtomomill.converter.hdf5.acquisition.utils import ( deduce_machine_current, split_timestamps, get_bliss_scan_type, group_series, ) from nxtomomill.converter.hdf5.acquisition.zseriesacquisition import ( ZSeriesBaseAcquisition, ) from nxtomomill.utils.utils import str_datetime_to_numpy_datetime64 from nxtomomill.converter.hdf5.acquisitionConstructor import ( is_multitomo_sequence, is_back_and_forth_sequence, ) from nxtomomill.converter.hdf5.acquisition.tests.bliss_tomo_datasets import ( # noqa F401 holotomo2_simu_dataset, ) from nxtomomill.converter.hdf5.acquisition.utils._scan_type_finder import ScanTypeFinder def test_deduce_machine_current(): """ Test `deduce_current` function. Base function to compute current for each frame according to it's timestamp """ current_datetimes = { "2022-01-15T21:07:58.360095+02:00": 1.1, "2022-04-15T21:07:58.360095+02:00": 121.1, "2022-04-15T21:09:58.360095+02:00": 123.3, "2022-04-15T21:11:58.360095+02:00": 523.3, "2022-12-15T21:07:58.360095+02:00": 1000.3, } with pytest.raises(ValueError): deduce_machine_current(tuple(), {}) with pytest.raises(TypeError): deduce_machine_current(12, 2) with pytest.raises(TypeError): deduce_machine_current( [ 12, ], 2, ) with pytest.raises(TypeError): deduce_machine_current( [ 2, ], current_datetimes, ) converted_currents = {} for elec_cur_datetime_str, elect_cur in current_datetimes.items(): datetime_as_datetime = str_datetime_to_numpy_datetime64(elec_cur_datetime_str) converted_currents[datetime_as_datetime] = elect_cur # check exacts values, left and right bounds assert deduce_machine_current( (str_datetime_to_numpy_datetime64("2022-01-15T21:07:58.360095+02:00"),), converted_currents, ) == (1.1,) assert deduce_machine_current( (str_datetime_to_numpy_datetime64("2022-01-14T21:07:58.360095+02:00"),), converted_currents, ) == (1.1,) assert deduce_machine_current( (str_datetime_to_numpy_datetime64("2022-12-15T21:07:58.360095+02:00"),), converted_currents, ) == (1000.3,) assert deduce_machine_current( (str_datetime_to_numpy_datetime64("2022-12-16T21:07:58.360095+02:00"),), converted_currents, ) == (1000.3,) # check interpolated values numpy.testing.assert_almost_equal( deduce_machine_current( (str_datetime_to_numpy_datetime64("2022-04-15T21:08:58.360095+02:00"),), converted_currents, )[0], (122.2,), ) numpy.testing.assert_almost_equal( deduce_machine_current( (str_datetime_to_numpy_datetime64("2022-04-15T21:10:28.360095+02:00"),), converted_currents, )[0], (223.3,), ) # test several call and insure keep order numpy.testing.assert_almost_equal( deduce_machine_current( ( str_datetime_to_numpy_datetime64("2022-01-15T21:07:58.360095+02:00"), str_datetime_to_numpy_datetime64("2022-04-15T21:10:28.360095+02:00"), str_datetime_to_numpy_datetime64("2022-04-15T21:08:58.360095+02:00"), ), converted_currents, ), (1.1, 223.3, 122.2), ) @pytest.mark.parametrize("n_part", (1, 3, 4, 7, 9, 12)) @pytest.mark.parametrize( "array_to_test", (numpy.arange(-15, 23, dtype=numpy.float32), numpy.ones(57, dtype=numpy.uint8)), ) def test_split_timestamps(n_part, array_to_test): split_arrays = tuple( split_timestamps( my_array=array_to_test, n_part=n_part, ) ) assert len(split_arrays) == n_part assert split_arrays[0].dtype == array_to_test.dtype numpy.testing.assert_array_equal( numpy.concatenate([numpy.array(part) for part in split_arrays]), numpy.array(array_to_test), ) def test_get_entry_type(tmp_path): """test the get_entry_type function""" test_folder = tmp_path / "test_get_entry_type" test_folder.mkdir() test_file = os.path.join(test_folder, "test.hdf5") default_configuration = TomoHDF5Config() with HDF5File(test_file, mode="w") as h5f: h5f["group1/title"] = "darks" url_case1_darks = DataUrl(file_path=test_file, data_path="group1") h5f["group2/title"] = "flats" url_case1_flats = DataUrl(file_path=test_file, data_path="group2") h5f["group3/title"] = "darks" h5f["group3/technique/image_key"] = ImageKey.PROJECTION.value url_case2_proj = DataUrl(file_path=test_file, data_path="group3") h5f["group4/title"] = "darks" h5f["group4/technique/image_key"] = ImageKey.ALIGNMENT.value url_case2_alignment = DataUrl(file_path=test_file, data_path="group4") h5f["group5/technique/image_key"] = ImageKey.INVALID.value url_case3_invalid = DataUrl(file_path=test_file, data_path="group5") h5f["group6/technique/image_key"] = ImageKey.PROJECTION.value url_case3_proj = DataUrl(file_path=test_file, data_path="group6") # make sure if title is provided this is still working assert ( get_bliss_scan_type(url_case1_darks, default_configuration) == AcquisitionStep.DARK ) assert ( get_bliss_scan_type(url_case1_flats, default_configuration) == AcquisitionStep.FLAT ) # make sure the 'technique/image_key' get priority other the 'title' assert ( get_bliss_scan_type(url_case2_proj, default_configuration) == AcquisitionStep.PROJECTION ) assert ( get_bliss_scan_type(url_case2_alignment, default_configuration) == AcquisitionStep.ALIGNMENT ) # make sure if title is no more present then 'image_key' will be picked anyway assert get_bliss_scan_type(url_case3_invalid, default_configuration) is None assert ( get_bliss_scan_type(url_case3_proj, default_configuration) == AcquisitionStep.PROJECTION ) def test_group_z_series(tmp_path): input_file = os.path.join(tmp_path, "test_z_series.h5") z_acquisitions = [] n_series = 3 n_scan_per_series = 4 with h5py.File(input_file, mode="w") as h5f: i_scan = 0 for i_series in range(n_series): for _ in range(n_scan_per_series): h5f[f"{i_scan}.1/sample/name"] = ( f"my_serie_{str(i_series).zfill(4)}_{str(i_scan).zfill(4)}" ) z_acquisitions.append( ZSeriesBaseAcquisition( root_url=DataUrl( file_path=input_file, data_path=f"{i_scan}.1", scheme="silx", ), configuration={}, detector_sel_callback=None, start_index=i_scan, ) ) i_scan += 1 series_grouped = [] for acquisition in z_acquisitions: series_grouped = group_series( acquisition=acquisition, list_of_series=series_grouped ) assert len(series_grouped) == n_series for i in range(n_series): assert series_grouped[i] == list( [ z_acquisitions[i * n_scan_per_series + i_scan] for i_scan in range(n_scan_per_series) ] ) @pytest.mark.parametrize( "bliss_scan_path, scan_type", { "1.1": AcquisitionStep.INITIALIZATION, # for now this is detected as an 'initialization' but in the absolute it should be a sequence of sequence. "2.1": AcquisitionStep.INITIALIZATION, "3.1": AcquisitionStep.PROJECTION, }.items(), ) def test_holotomo2_simu_and_acquisition_type( holotomo2_simu_dataset, bliss_scan_path, scan_type # noqa F811 ): file_path = os.path.join( holotomo2_simu_dataset, "sample", "sample_holotomo2", "sample_holotomo2.h5" ) configuration = TomoHDF5Config() scan_type_finder = ScanTypeFinder(configuration) with h5py.File(file_path, mode="r") as h5f: entry = h5f[bliss_scan_path] ScanTypeFinder # dummy test of 'is_multitomo_sequence' assert ( is_multitomo_sequence( entry, configuration=configuration, fallback_on_title=False, ) is False ) assert ( is_back_and_forth_sequence( entry, configuration=configuration, fallback_on_title=False, ) is False ) assert scan_type_finder.find(entry=entry) is scan_type nxtomomill-v2.0.1/nxtomomill/converter/hdf5/acquisition/tests/test_back_and_forth.py000066400000000000000000000106411511430602400311660ustar00rootroot00000000000000from __future__ import annotations import os import pint import numpy from nxtomo.application.nxtomo import NXtomo from nxtomo.nxobject.nxdetector import ImageKey from nxtomomill.tests.datasets import GitlabDataset from nxtomomill import converter from nxtomomill.io.config import TomoHDF5Config _ureg = pint.get_application_registry() def test_back_and_forth_conversion(tmp_path): """test nxtomomill fluo2nx CLI with default parameters (handle all detectors found)""" scan_dir = GitlabDataset.get_dataset("h5_datasets") assert scan_dir is not None input_file = os.path.join(scan_dir, "back_and_forth/bliss_tomo_2025_06_17.h5") output_file = os.path.join(tmp_path, "nexus_file.nx") assert not os.path.exists(output_file), "output_file exists already" configuration = TomoHDF5Config() configuration.output_file = output_file configuration.input_file = input_file configuration.no_input = True configuration.raises_error = False configuration.no_master_file = False # Step 1: make sure conversion is not done if we mess with tiles (and make sure this is back-and-forth which is recognized and not another like multi-tomo) tmp_back_and_forth_titles = configuration.back_and_forth_init_titles configuration.back_and_forth_init_titles = tuple() result = converter.from_h5_to_nx(configuration=configuration) assert not os.path.exists(output_file) # Step 2: test the real conversion using default inputs configuration.back_and_forth_init_titles = tmp_back_and_forth_titles result = converter.from_h5_to_nx(configuration=configuration) assert os.path.exists(output_file) # check we have our 3 NXtomo are converted with each 101 projections, a flat and a dark at the beginning and a flat at the end. assert len(result) == 3 nx_tomos = [NXtomo().load(file_path, entry) for (file_path, entry) in result] n_proj_per_nx_tomo = 101 for i_nx_tomo, nx_tomo in enumerate(nx_tomos): # check instrument assert len(nx_tomo.instrument.detector.image_key) == n_proj_per_nx_tomo + 40 * 2 # image key numpy.testing.assert_array_equal( nx_tomo.instrument.detector.image_key, numpy.concatenate( ( [ImageKey.DARK_FIELD] * 20, [ImageKey.FLAT_FIELD] * 20, [ImageKey.PROJECTION] * n_proj_per_nx_tomo, [ImageKey.FLAT_FIELD] * 20, [ImageKey.DARK_FIELD] * 20, ) ), ) # sequence number proj_seq_start = n_proj_per_nx_tomo * i_nx_tomo + 40 second_flat_start_index = 40 + 3 * n_proj_per_nx_tomo numpy.testing.assert_array_equal( nx_tomo.instrument.detector.sequence_number, numpy.concatenate( ( numpy.arange(0, 20, dtype=numpy.uint32), # dark numpy.arange(20, 40, dtype=numpy.uint32), # flat numpy.arange( proj_seq_start, proj_seq_start + n_proj_per_nx_tomo, dtype=numpy.uint32, ), # projections numpy.arange( second_flat_start_index, second_flat_start_index + 20, dtype=numpy.uint32, ), # flat numpy.arange( second_flat_start_index + 20, second_flat_start_index + 40, dtype=numpy.uint32, ), # dark ) ), ) # detector pixel size numpy.testing.assert_almost_equal( nx_tomo.instrument.detector.x_pixel_size.magnitude, (2.55135 * _ureg.micrometer).magnitude, decimal=5, ) numpy.testing.assert_almost_equal( nx_tomo.instrument.detector.y_pixel_size.magnitude, (2.55135 * _ureg.micrometer).magnitude, decimal=5, ) assert nx_tomo.instrument.detector.distance == 3500 * _ureg.millimeter # check energy assert nx_tomo.energy == 10.0 * _ureg.keV # check sample assert nx_tomo.sample.x_pixel_size == 2.4 * _ureg.micrometer assert nx_tomo.sample.y_pixel_size == 2.4 * _ureg.micrometer # check source assert nx_tomo.instrument.source.distance == -55500 * _ureg.millimeter nxtomomill-v2.0.1/nxtomomill/converter/hdf5/acquisition/tests/test_bliss_tomo_config.py000066400000000000000000000063331511430602400317440ustar00rootroot00000000000000import pytest import os import numpy from tomoscan.io import HDF5File, get_swmr_mode from nxtomomill.converter.hdf5.acquisition.blisstomoconfig import TomoConfig def test_tomo_config(tmp_path): """ test a configuration from bliss can be read from nxtomomill """ file_path = os.path.join(tmp_path, "test_bliss_tomo_config.hdf5") with HDF5File(file_path, mode="w") as h5s: tomo_config_group = h5s.create_group("/0/test/instrument/tomoconfig") tomo_config_group["rotation"] = ("my_rot", "rot") tomo_config_group["sample_u"] = numpy.array( [ "alias", "motor_name", ], dtype=object, ) tomo_config_group["sample_v"] = numpy.array( [ "sample_v_motor", ], dtype=object, ) tomo_config_group["sample_x"] = [ "sample_x_motor", ] tomo_config_group["sample_y"] = numpy.array( [ "sample_y_motor", ], dtype=object, ) tomo_config_group["detector"] = ["alias", "frelon"] tomo_config_group["translation_y"] = numpy.array( [ "ty", ], dtype=object, ) tomo_config_group["translation_z"] = numpy.array( [ "tz", ], dtype=object, ) tomo_config_group["rotation_is_clockwise"] = True with pytest.raises(TypeError): TomoConfig.from_technique_group(file_path) with pytest.raises(KeyError): TomoConfig.from_technique_group(h5s) with HDF5File(file_path, mode="r", swmr=get_swmr_mode()) as h5s: tomo_config = TomoConfig.from_technique_group(h5s["/0/test/instrument"]) numpy.testing.assert_array_equal( tomo_config.rotation, numpy.array(["my_rot", "rot"], dtype=object) ) numpy.testing.assert_array_equal( tomo_config.sample_u, numpy.array(["alias", "motor_name"], dtype=object) ) numpy.testing.assert_array_equal( tomo_config.sample_v, numpy.array( [ "sample_v_motor", ], dtype=object, ), ) numpy.testing.assert_array_equal( tomo_config.sample_x, numpy.array( [ "sample_x_motor", ], dtype=object, ), ) numpy.testing.assert_array_equal( tomo_config.sample_y, numpy.array( [ "sample_y_motor", ], dtype=object, ), ) numpy.testing.assert_array_equal( tomo_config.tomo_detector, numpy.array( [ "alias", "frelon", ], dtype=object, ), ) numpy.testing.assert_array_equal( tomo_config.translation_y, numpy.array( [ "ty", ], dtype=object, ), ) numpy.testing.assert_array_equal( tomo_config.translation_z, numpy.array( [ "tz", ], dtype=object, ), ) assert tomo_config.rotation_is_clockwise is True nxtomomill-v2.0.1/nxtomomill/converter/hdf5/acquisition/tests/test_legacy.py000066400000000000000000000126541511430602400275140ustar00rootroot00000000000000from __future__ import annotations import os from pathlib import Path import numpy import pint import pytest from nxtomo.application.nxtomo import NXtomo from nxtomo.nxobject.nxdetector import ImageKey from nxtomomill import converter from nxtomomill.io.config import TomoHDF5Config from nxtomomill.tests.datasets import GitlabDataset _ureg = pint.get_application_registry() @pytest.fixture(scope="class") def S59_68p41b_16mm_6p5mm_F8_0001_Dataset() -> Path: scan_dir = GitlabDataset.get_dataset("h5_datasets") assert scan_dir is not None return Path(os.path.join(scan_dir, "legacy", "S59_68p41b_16mm_6p5mm_F8_0001.h5")) @pytest.fixture(scope="class") def WGN_01_0000_P_110_8128_D_129_Dataset(): scan_dir = GitlabDataset.get_dataset("h5_datasets") assert scan_dir is not None return Path(os.path.join(scan_dir, "legacy", "WGN_01_0000_P_110_8128_D_129.h5")) def test_S59_68p41b_16mm_6p5mm_F8_0001_dataset( S59_68p41b_16mm_6p5mm_F8_0001_Dataset, tmp_path ): """test 'S59_68p41b_16mm_6p5mm_F8_0001.h5' legacy dataset""" output_file = tmp_path / "S59_68p41b_16mm_6p5mm_F8_0001.nx" assert not os.path.exists(output_file), "output_file exists already" configuration = TomoHDF5Config() configuration.output_file = str(output_file) configuration.input_file = str(S59_68p41b_16mm_6p5mm_F8_0001_Dataset) configuration.raises_error = False configuration.no_master_file = False configuration.single_file = True result = converter.from_h5_to_nx(configuration=configuration) assert len(result) == 1 assert os.path.exists(output_file) nxtomo = NXtomo().load(*result[0]) nxtomo.energy = 70.0 * _ureg.keV n_frames = 10252 assert len(nxtomo.sample.x_translation) == n_frames assert len(nxtomo.sample.y_translation) == n_frames assert len(nxtomo.sample.z_translation) == n_frames assert len(nxtomo.instrument.detector.count_time) == n_frames assert len(nxtomo.instrument.detector.image_key) == n_frames assert len(nxtomo.instrument.detector.image_key_control) == n_frames assert nxtomo.instrument.detector.field_of_view.value == "Full" N_PROJ = 10000 # check image keys expected_image_key = numpy.concatenate( [ [ImageKey.DARK_FIELD] * 50, [ImageKey.FLAT_FIELD] * 101, [ImageKey.PROJECTION] * N_PROJ, [ImageKey.FLAT_FIELD] * 101, ] ) numpy.testing.assert_array_equal( nxtomo.instrument.detector.image_key_control, expected_image_key ) # check angles expected_angles = numpy.concatenate( [ [0] * 151, numpy.linspace(0, 360, N_PROJ, endpoint=False), [0] * 101, ] ) assert len(expected_angles) == n_frames numpy.testing.assert_almost_equal( nxtomo.sample.rotation_angle.to(_ureg.degree).magnitude, expected_angles, decimal=1, ) assert nxtomo.instrument.detector.distance == 105 * _ureg.millimeter assert nxtomo.instrument.source.distance == -145000 * _ureg.millimeter assert nxtomo.instrument.detector.x_pixel_size == 6.5 * _ureg.micrometer assert nxtomo.instrument.detector.y_pixel_size == 6.5 * _ureg.micrometer def test_WGN_01_0000_P_110_8128_D_129(WGN_01_0000_P_110_8128_D_129_Dataset, tmp_path): """test 'WGN_01_0000_P_110_8128_D_129' legacy dataset""" output_file = tmp_path / "WGN_01_0000_P_110_8128_D_129_Dataset.nx" assert not os.path.exists(output_file), "output_file exists already" configuration = TomoHDF5Config() configuration.output_file = str(output_file) configuration.input_file = str(WGN_01_0000_P_110_8128_D_129_Dataset) configuration.raises_error = False configuration.no_master_file = False configuration.single_file = True result = converter.from_h5_to_nx(configuration=configuration) assert len(result) == 3 assert os.path.exists(output_file) nxtomo = NXtomo().load(*result[0]) nxtomo.energy = 80.0 * _ureg.keV n_frames = 4602 assert len(nxtomo.sample.x_translation) == n_frames assert len(nxtomo.sample.y_translation) == n_frames assert len(nxtomo.sample.z_translation) == n_frames assert len(nxtomo.instrument.detector.count_time) == n_frames assert len(nxtomo.instrument.detector.image_key) == n_frames assert len(nxtomo.instrument.detector.image_key_control) == n_frames assert nxtomo.instrument.detector.field_of_view.value == "Full" N_PROJ = 4500 # check image keys expected_image_key = numpy.concatenate( [ [ImageKey.DARK_FIELD] * 100, [ImageKey.FLAT_FIELD] * 2, [ImageKey.PROJECTION] * N_PROJ, ] ) numpy.testing.assert_array_equal( nxtomo.instrument.detector.image_key_control, expected_image_key ) # check angles expected_angles = numpy.concatenate( [ [0] * 102, numpy.linspace(0, 180, N_PROJ, endpoint=False), ] ) assert len(expected_angles) == n_frames numpy.testing.assert_almost_equal( nxtomo.sample.rotation_angle.to(_ureg.degree).magnitude, expected_angles, decimal=1, ) # note: saved value is indeed 0... assert nxtomo.instrument.detector.distance == 0 * _ureg.millimeter assert nxtomo.instrument.source.distance == -20000 * _ureg.millimeter assert nxtomo.instrument.detector.x_pixel_size == 6.5 * _ureg.micrometer assert nxtomo.instrument.detector.y_pixel_size == 6.5 * _ureg.micrometer nxtomomill-v2.0.1/nxtomomill/converter/hdf5/acquisition/tests/test_multitomoacquisition.py000066400000000000000000000164711511430602400325530ustar00rootroot00000000000000# coding: utf-8 import os import numpy import pytest from nxtomo.application.nxtomo import NXtomo from nxtomo.nxobject.nxdetector import ImageKey from tomoscan.io import HDF5File, get_swmr_mode from tomoscan.esrf.scan.nxtomoscan import NXtomoScan from nxtomomill import converter from nxtomomill.converter.hdf5.acquisition.multitomo import MultiTomoAcquisition from nxtomomill.io.config import TomoHDF5Config from nxtomomill.tests.utils.bliss import MockBlissAcquisition configs = ( {"nb_tomo": 1, "nb_loop": 5, "nb_turns": 5}, {"nb_tomo": 5, "nb_loop": 1, "nb_turns": 5}, {"nb_tomo": 2, "nb_loop": 3, "nb_turns": 6}, ) @pytest.mark.parametrize("multitomo_version", (1, 2)) @pytest.mark.parametrize("config", configs) def test_multitomo_conversion(tmp_path, config: dict, multitomo_version: int): n_darks = 10 n_flats = 10 bliss_scan_dir = str(tmp_path / "my_acquisition") nb_tomo = config["nb_tomo"] nb_loop = config["nb_loop"] nb_turns = config["nb_turns"] bliss_mock = MockBlissAcquisition( n_sample=1, n_sequence=1, n_scan_per_sequence=1, n_darks=n_darks, n_flats=n_flats, output_dir=bliss_scan_dir, acqui_type="multitomo", with_rotation_motor_info=True, nb_tomo=nb_tomo if multitomo_version == 1 else None, nb_loop=nb_loop if multitomo_version == 1 else None, nb_turns=nb_turns if multitomo_version == 2 else None, ) sample_file = bliss_mock.samples[0].sample_file configuration = TomoHDF5Config() output_nx_file = os.path.join(str(tmp_path), "nexus_scans.nx") assert not os.path.exists(output_nx_file) configuration.output_file = output_nx_file configuration.input_file = sample_file configuration.no_input = True configuration.raises_error = False configuration.no_master_file = False result = converter.from_h5_to_nx(configuration=configuration) assert os.path.exists(output_nx_file) # check we have our 5 NXtomo converted assert len(result) == nb_loop * nb_tomo n_projs = 10 * nb_loop * nb_tomo # check saved datasets with HDF5File(sample_file, mode="r", swmr=get_swmr_mode()) as h5f: darks = h5f["2.1/instrument/pcolinux/data"][()] assert len(darks) == n_darks flats_1 = h5f["3.1/instrument/pcolinux/data"][()] assert len(flats_1) == n_flats projections = h5f["4.1/instrument/pcolinux/data"][()] assert len(projections) == n_projs flats_2 = h5f["5.1/instrument/pcolinux/data"][()] assert len(flats_2) == n_flats for i_nx_tomo, (file_path, data_path) in enumerate(result): with HDF5File(file_path, mode="r", swmr=get_swmr_mode()) as h5f: detector_path = "/".join([data_path, "instrument", "detector", "data"]) detector_data = h5f[detector_path][()] expected_data = numpy.concatenate( [ darks, flats_1, projections[(10 * i_nx_tomo) : (10 * (i_nx_tomo + 1))], flats_2, ] ) numpy.testing.assert_array_equal(detector_data, expected_data) sequence_number_path = "/".join( [data_path, "instrument", "detector", "sequence_number"] ) sequence_number = h5f[sequence_number_path] numpy.testing.assert_array_equal( sequence_number, numpy.concatenate( ( numpy.linspace( start=0, stop=n_darks, num=n_darks, endpoint=False, dtype=numpy.uint32, ), # darks numpy.linspace( start=n_darks, stop=n_darks + n_flats, num=n_flats, endpoint=False, dtype=numpy.uint32, ), # flats 1 numpy.linspace( start=i_nx_tomo * 10 + n_darks + n_flats, stop=(i_nx_tomo + 1) * 10 + n_darks + n_flats, num=10, endpoint=False, dtype=numpy.uint32, ), numpy.linspace( start=n_darks + n_flats + n_projs, stop=n_darks + 2 * n_flats + n_projs, num=n_flats, endpoint=False, dtype=numpy.uint32, ), # flats 2 ) ), ) def test_multitomo_conversion_with_angle_subselection(tmp_path): bliss_scan_dir = str(tmp_path / "my_acquisition") nb_tomo = 1 nb_loop = 1 bliss_mock = MockBlissAcquisition( n_sample=1, n_sequence=1, n_scan_per_sequence=1, n_darks=1, n_flats=1, output_dir=bliss_scan_dir, acqui_type="multitomo", with_rotation_motor_info=True, nb_tomo=nb_tomo, nb_loop=nb_loop, ) sample_file = bliss_mock.samples[0].sample_file configuration = TomoHDF5Config() output_nx_file = os.path.join(str(tmp_path), "nexus_scans.nx") assert not os.path.exists(output_nx_file) configuration.output_file = output_nx_file configuration.input_file = sample_file configuration.no_input = True configuration.raises_error = False configuration.multitomo_scan_range = 90 configuration.multitomo_start_angle_offset = 10 configuration.multitomo_n_nxtomo = 2 configuration.raises_error = True configuration.multitomo_shift_angles = False file_path, data_path = converter.from_h5_to_nx(configuration=configuration)[0] scan = NXtomoScan(file_path, data_path) is_proj_mask = scan.image_key == ImageKey.PROJECTION.value remaining_proj_angle = numpy.array(scan.rotation_angle)[is_proj_mask] assert remaining_proj_angle.min() >= 10 assert remaining_proj_angle.max() <= 100 configuration.multitomo_shift_angles = True configuration.overwrite = True file_path, data_path = converter.from_h5_to_nx(configuration=configuration)[0] scan = NXtomoScan(file_path, data_path) is_proj_mask = scan.image_key == ImageKey.PROJECTION.value remaining_proj_angle = numpy.array(scan.rotation_angle)[is_proj_mask] assert remaining_proj_angle.min() >= 0 assert remaining_proj_angle.max() <= 90 def test_get_projections_slices(): """test the _get_projections_slices function""" nx_tomo = NXtomo() nx_tomo.instrument.detector.image_key_control = [2, 1, 0, 0, 0, 2] assert MultiTomoAcquisition._get_projections_slices(nx_tomo) == (slice(2, 5, 1),) nx_tomo.instrument.detector.image_key_control = [2, 1, 0, 0, 0, 2] assert MultiTomoAcquisition._get_projections_slices(nx_tomo) == (slice(2, 5, 1),) nx_tomo.instrument.detector.image_key_control = [2, 1] assert MultiTomoAcquisition._get_projections_slices(nx_tomo) == () nx_tomo.instrument.detector.image_key_control = [0, 0, 2, 1, 0, 0, 1, 0, 0] assert MultiTomoAcquisition._get_projections_slices(nx_tomo) == ( slice(0, 2, 1), slice(4, 6, 1), slice(7, 9, 1), ) nxtomomill-v2.0.1/nxtomomill/converter/hdf5/acquisition/utils/000077500000000000000000000000001511430602400246255ustar00rootroot00000000000000nxtomomill-v2.0.1/nxtomomill/converter/hdf5/acquisition/utils/__init__.py000066400000000000000000000006731511430602400267440ustar00rootroot00000000000000from .detector import has_valid_detector # noqa F401 from .detector import get_nx_detectors # noqa F401 from .detector import guess_nx_detector # noqa F401 from .bliss_scan_type import get_bliss_scan_type # noqa F401 from .bliss_scan_type import get_entry_type # noqa F401 from .machine_current import deduce_machine_current # noqa F401 from .timestamps import split_timestamps # noqa F401 from .series import group_series # noqa F401 nxtomomill-v2.0.1/nxtomomill/converter/hdf5/acquisition/utils/_scan_type_finder.py000066400000000000000000000071571511430602400306640ustar00rootroot00000000000000from __future__ import annotations import logging import h5py from silx.io.utils import h5py_read_dataset from nxtomo.nxobject.nxdetector import ImageKey from nxtomomill.models.h52nx._acquisitionstep import AcquisitionStep from nxtomomill.io.config import TomoHDF5Config _logger = logging.getLogger(__name__) class ScanTypeFinder: """Util class to find the type of a bliss scan (entry)""" def __init__(self, configuration: TomoHDF5Config): self._configuration = configuration def find(self, entry: h5py.Group): if not isinstance(entry, h5py.Group): raise ValueError( f"Expected path is not related to a h5py.Group ({entry}) when expect to target a bliss entry." ) return ( self.get_entry_type_from_technique_img_key(entry) or self.get_entry_type_from_scan_category(entry) or self.get_entry_type_from_title(entry) ) def get_entry_type_from_title(self, entry: h5py.Group): """ try to determine the entry type from the title """ try: title = h5py_read_dataset(entry["title"]) except Exception: _logger.error(f"fail to find title for {entry.name}, skip this group") return None else: init_titles = list(self._configuration.init_titles) init_titles.extend(self._configuration.zseries_init_titles) init_titles.extend(self._configuration.multitomo_init_titles) init_titles.extend(self._configuration.back_and_forth_init_titles) step_titles = { AcquisitionStep.INITIALIZATION: init_titles, AcquisitionStep.DARK: self._configuration.dark_titles, AcquisitionStep.FLAT: self._configuration.flat_titles, AcquisitionStep.PROJECTION: self._configuration.projection_titles, AcquisitionStep.ALIGNMENT: self._configuration.alignment_titles, } for step, titles in step_titles.items(): for title_start in titles: if title.startswith(title_start): return step return None def get_entry_type_from_technique_img_key( self, entry: h5py.Group ) -> AcquisitionStep | None: """ try to determine entry type from the scan/technique sub groups. If this is a flat then we expect to have a "flat" group. If this is a set of projection we expect to have a "proj" group. For now alignment / return are unfilled """ group_technique = entry.get("technique", dict()) if "image_key" not in group_technique: return None image_key = h5py_read_dataset(group_technique["image_key"]) if image_key is None: return None else: try: image_key = ImageKey(image_key) except ValueError: _logger.error(f"unrecognized image key: '{image_key}'") return None else: connections = { ImageKey.DARK_FIELD: AcquisitionStep.DARK, ImageKey.FLAT_FIELD: AcquisitionStep.FLAT, ImageKey.PROJECTION: AcquisitionStep.PROJECTION, ImageKey.ALIGNMENT: AcquisitionStep.ALIGNMENT, } return connections.get(image_key, None) def get_entry_type_from_scan_category( self, entry: h5py.Group ) -> AcquisitionStep | None: if "scan_category" in entry.get("technique", {}): return AcquisitionStep.INITIALIZATION else: return None nxtomomill-v2.0.1/nxtomomill/converter/hdf5/acquisition/utils/bliss_scan_type.py000066400000000000000000000024071511430602400303630ustar00rootroot00000000000000from __future__ import annotations from silx.io.url import DataUrl from silx.io.utils import open as open_hdf5 from nxtomomill.io.config import TomoHDF5Config from nxtomomill.models.h52nx._acquisitionstep import AcquisitionStep from nxtomomill.utils.io import deprecated from ._scan_type_finder import ScanTypeFinder @deprecated(reason="renamed", replacement="get_bliss_scan_type", since_version="2.0") def get_entry_type(*args, **kwargs): return get_bliss_scan_type(*args, **kwargs) def get_bliss_scan_type( url: DataUrl, configuration: TomoHDF5Config ) -> AcquisitionStep | None: """ :param url: bliss scan url to type :return: return the step of the acquisition or None if cannot find it. """ if not isinstance(url, DataUrl): raise TypeError(f"DataUrl is expected. Not {type(url)}") if url.data_slice() is not None: raise ValueError( "url is expected to reference a Bliss scan entry (no slices supported)" ) scan_type_finder = ScanTypeFinder(configuration=configuration) with open_hdf5(url.file_path()) as h5f: if url.data_path() not in h5f: raise ValueError(f"Provided path does not exist: {url}") entry = h5f[url.data_path()] return scan_type_finder.find(entry) nxtomomill-v2.0.1/nxtomomill/converter/hdf5/acquisition/utils/detector.py000066400000000000000000000036061511430602400270150ustar00rootroot00000000000000from __future__ import annotations import h5py def has_valid_detector(node, detectors_names): """ :return True if the node looks like a valid nx detector """ for key in node.keys(): if ( "NX_class" in node[key].attrs and node[key].attrs["NX_class"] == "NXdetector" ): if detectors_names is None or key in detectors_names: return True return False def get_nx_detectors(node: h5py.Group) -> tuple: """ :param node: node to inspect :return: tuple of NXdetector (h5py.Group) contained in `node` (expected to be the `instrument` group) """ if not isinstance(node, h5py.Group): raise TypeError("node should be an instance of h5py.Group") nx_detectors = [] for _, subnode in node.items(): if isinstance(subnode, h5py.Group) and "NX_class" in subnode.attrs: if subnode.attrs["NX_class"] == "NXdetector": if "data" in subnode and hasattr(subnode["data"], "ndim"): if subnode["data"].ndim == 3: nx_detectors.append(subnode) nx_detectors = sorted(nx_detectors, key=lambda det: det.name) return tuple(nx_detectors) def guess_nx_detector(node: h5py.Group) -> tuple: """ Try to guess what can be an nx_detector without using the "NXdetector" NX_class attribute. Expect to find a 3D dataset named 'data' under a subnode """ if not isinstance(node, h5py.Group): raise TypeError("node should be an instance of h5py.Group") nx_detectors = [] for _, subnode in node.items(): if isinstance(subnode, h5py.Group) and "data" in subnode: if isinstance(subnode["data"], h5py.Dataset) and subnode["data"].ndim == 3: nx_detectors.append(subnode) nx_detectors = sorted(nx_detectors, key=lambda det: det.name) return tuple(nx_detectors) nxtomomill-v2.0.1/nxtomomill/converter/hdf5/acquisition/utils/machine_current.py000066400000000000000000000050131511430602400303440ustar00rootroot00000000000000import numpy def deduce_machine_current(timestamps: tuple, known_machine_current: dict) -> tuple: """ :param known_machine_current: keys are timestamp. Value is machine current :param timestamps: timestamp for which we want to get machine current """ if not isinstance(known_machine_current, dict): raise TypeError("known_machine_current is expected to be a dict") for elmt in timestamps: if not isinstance(elmt, numpy.datetime64): raise TypeError( f"Elements of timestamps are expected to be {numpy.datetime64} and not {type(elmt)}" ) if len(known_machine_current) == 0: raise ValueError("known_machine_current should contain at least one element") for key, value in known_machine_current.items(): if not isinstance(key, numpy.datetime64): raise TypeError( f"known_machine_current keys are expected to be instances of {numpy.datetime64} and not {type(key)}" ) if not isinstance(value, (float, numpy.number)): raise TypeError( "known_machine_current values are expected to be instances of float" ) # 1. Order **known** machine current by time stamps (key) known_machine_current = dict(sorted(known_machine_current.items())) known_timestamps = numpy.fromiter( known_machine_current.keys(), dtype="datetime64[ns]", count=len(known_machine_current), ) known_machine_current_values = numpy.fromiter( known_machine_current.values(), dtype="float64", count=len(known_machine_current), ) # 2. Sort the supplied timestamps timestamps = numpy.array(timestamps, dtype="datetime64[ns]") timestamp_input_ordering = numpy.argsort(timestamps) timestamps_sorted = numpy.take_along_axis( timestamps, indices=timestamp_input_ordering, axis=0 ) # 3. Convert to float for numpy.interp known_timestamps_float = known_timestamps.astype("float64") timestamps_float = timestamps_sorted.astype("float64") # 4. Interpolate the values interpolated_values = numpy.interp( timestamps_float, known_timestamps_float, known_machine_current_values ) # 5. Reorder interpolated values to match the order of original timestamps ordered_interpolated_values = numpy.zeros_like(interpolated_values) for i, o_pos in enumerate(timestamp_input_ordering): ordered_interpolated_values[o_pos] = interpolated_values[i] return tuple(ordered_interpolated_values) nxtomomill-v2.0.1/nxtomomill/converter/hdf5/acquisition/utils/series.py000066400000000000000000000011661511430602400264750ustar00rootroot00000000000000from __future__ import annotations def group_series(acquisition, list_of_series: list) -> list: """ :param ZSeriesBaseAcquisition acquisition: z-series version 2 and 3 are all defined in a separate sequence. So we need to aggregate for post processing based on their names. post-processing can be dark / flat copy to others NXtomo """ for series in list_of_series: if series[0].is_part_of_same_series(acquisition): series.append(acquisition) return list_of_series list_of_series.append( [ acquisition, ] ) return list_of_series nxtomomill-v2.0.1/nxtomomill/converter/hdf5/acquisition/utils/timestamps.py000066400000000000000000000007151511430602400273700ustar00rootroot00000000000000from __future__ import annotations def split_timestamps(my_array, n_part: int): """ split given array into n_part (as equal as possible) :param sequence my_array: """ array_size = len(my_array) if array_size < n_part: yield my_array else: start = 0 for _ in range(n_part): end = max(start + int(array_size / n_part) + 1, array_size) yield my_array[start:end] start = end nxtomomill-v2.0.1/nxtomomill/converter/hdf5/acquisition/zseriesacquisition.py000066400000000000000000000241771511430602400300070ustar00rootroot00000000000000# coding: utf-8 """ module to define a tomography z-series acquisition (made by bliss) """ from __future__ import annotations import pint import h5py import numpy from silx.io.url import DataUrl from silx.io.utils import h5py_read_dataset from silx.utils.proxy import docstring from nxtomomill.io.config import TomoHDF5Config from nxtomomill.utils.io import deprecated from nxtomomill.models.utils import _get_title_dataset from .baseacquisition import BaseAcquisition, EntryReader from .standardacquisition import StandardAcquisition try: import hdf5plugin # noqa F401 except ImportError: pass _ureg = pint.get_application_registry() __all__ = [ "is_z_series_sequence", "is_pcotomo_frm_titles", "is_z_series_frm_translation_z", "ZSeriesBaseAcquisition", ] def is_z_series_sequence( entry: h5py.Group, configuration: TomoHDF5Config, fallback_on_title: bool = True ) -> bool: """ Check if the provided h5py.Group must be consider as an "initialization" of a sequence. And if so then if it is a back-and-forth sequence. It will first check for value contained in technique/scan_category else for the title name (legacy) """ # check 'technique/scan_category' first # warning: checking the 'scan_category' is more for legacy. Today the z-series sequence has been replace by a set of standard acquisition sequences. scan_category = entry.get("technique/scan_category", None) if scan_category is not None: category_name = h5py_read_dataset(scan_category) elif fallback_on_title: # else fallback on title check category_name = _get_title_dataset( entry=entry, title_paths=BaseAcquisition.TITLE_PATHS ) else: category_name = None if category_name is None: return False for init_title in configuration.zseries_init_titles: if category_name.startswith(init_title): return True return False @deprecated( since_version="1.2", reason="rename pcotomo to multitomo", replacement="nxtomomill.converter.hdf5.acquisition.acquisitionConstructor.is_multitomo_frm_titles", ) def is_pcotomo_frm_titles(entry: h5py.Group, configuration: TomoHDF5Config) -> bool: from ..acquisitionConstructor import is_multitomo_sequence return is_multitomo_sequence(entry=entry, configuration=configuration) def is_multitomo_frm_titles(entry: h5py.Group, configuration: TomoHDF5Config) -> bool: """ if the provided h5py.Group must be consider as an "initialization" entry/scan of a multitomo acquisition """ title = _get_title_dataset(entry=entry, title_paths=BaseAcquisition.TITLE_PATHS) if title is None: return False for multitomo_init_title in configuration.multitomo_init_titles: if title.startswith(multitomo_init_title): return True return False def is_z_series_frm_translation_z(projection_urls, configuration: TomoHDF5Config): """ :param projection_urls: list of DataUrl pointing to projection nodes. :return: True if the set of projections should be considered as a z-series (according to version 1 of z-series) """ z_values = set() for url in projection_urls: with EntryReader(url) as entry: z_values_tmp = BaseAcquisition.get_translation_z_frm( entry, n_frame=None, configuration=configuration ) if z_values_tmp is not None: magnitudes = numpy.asarray(z_values_tmp.to(_ureg.meter).magnitude) if magnitudes.ndim == 0: z_values.add(magnitudes.item()) else: z_values.update(magnitudes.flatten().tolist()) return len(z_values) > 1 class ZSeriesBaseAcquisition(BaseAcquisition): """ A 'z series acquisition' is considered as a series of _StandardAcquisition. Registered scan can be split according to translation_z value. At the moment there is three version of z-series: #. **version 1**: each z is part of the same sequence. bliss .h5 will look like: * 1.1 tomo:zserie -> define the beginning of the sequence * 2.1 reference images1 (flats) -> start of the first z level * 3.1 projections 1 -7000 (flats) * 4.1 static images (alignment / return projections) * 5.1 reference images1 (flats) * 6.1 dark images * 7.1 reference images1 (flats) -> start of the second z level. using `get_z` to know the different levels * 8.1 projections 1 -7000 (flats) * ... in this case an instance of ZSeriesBaseAcquisition will create N NXtomo (one nxtomo per sequence) #. **version 2**: each z is part of a new sequence. So each sequence will instantiate ZSeriesBaseAcquisition and each with a single z. * 1.1 tomo:zserie -> define the beginning of the sequence * 2.1 reference images1 (flats) -> start of the first z level * 3.1 projections 1 -7000 (flats) * 4.1 static images (alignment / return projections) * 5.1 reference images1 (flats) * 6.1 dark images * 7.1 tomo:zserie -> define the beginning of the sequence * 8.1 reference images1 (flats) -> start of the second z level. using `get_z` to know the different levels * 9.1 projections 1 -7000 (flats) * ... in this case an instance of ZSeriesBaseAcquisition will create one NXtomo (one NXtomo per sequence) #. **version 3**: same as version 2 but dark / flat can only be done in a at the beginning or at the end of the series. And we want to copy those. To keep compatibility and design this part is done in post-processing. in this case an instance of ZSeriesBaseAcquisition will also create one NXtomo (one NXtomo per sequence) The goal of this class is mostly to handle the version 1. For version 2 and 3 it will be instantiated but `_acquisition` will contain a single acquisition. But to manipulate the series in the case of version 2 and especially version 3 the converter will group them inside `_z_series_v2_v3` """ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._acquisitions: tuple[float, StandardAcquisition] = {} """key is z value and value is StandardAcquisition""" ( self._dark_at_start, self._dark_at_end, self._flat_at_start, self._flat_at_end, ) = self.get_dark_flat_pos_info() def get_dark_flat_pos_info(self) -> tuple: # on latest z-series dark can be saved only at the beginning / end # so we need to copy those in post processing if self.root_url is None: return None, None, None, None with EntryReader(self.root_url) as entry: scan_flags_grp = self._get_scan_flags(entry_node=entry) or {} dark_at_start_dataset = scan_flags_grp.get("dark_images_at_start", None) if dark_at_start_dataset is not None: dark_at_start_dataset = h5py_read_dataset(dark_at_start_dataset) dark_at_end_dataset = scan_flags_grp.get("dark_images_at_end", None) if dark_at_end_dataset is not None: dark_at_end_dataset = h5py_read_dataset(dark_at_end_dataset) flat_at_start_dataset = scan_flags_grp.get( "ref_images_at_start", scan_flags_grp.get("flat_images_at_start", None) ) if flat_at_start_dataset is not None: flat_at_start_dataset = h5py_read_dataset(flat_at_start_dataset) flat_at_end_dataset = scan_flags_grp.get( "ref_images_at_end", scan_flags_grp.get("flat_images_at_end", None) ) if flat_at_end_dataset is not None: flat_at_end_dataset = h5py_read_dataset(flat_at_end_dataset) return ( dark_at_start_dataset, dark_at_end_dataset, flat_at_start_dataset, flat_at_end_dataset, ) def get_expected_nx_tomo(self): return 1 def get_standard_sub_acquisitions(self) -> tuple: """ Return the tuple of all :class:`.StandardAcquisition` composing _acquisitions """ return tuple(self._acquisitions.values()) def get_z(self, entry): if not isinstance(entry, h5py.Group): raise TypeError("entry: expected h5py.Group") z_array: pint.Quantity | None = self._get_translation_z(entry, n_frame=None) if z_array is None: raise ValueError(f"No z found for scan {entry.name}") z_array = z_array.to_base_units().magnitude if isinstance(z_array, numpy.ndarray): z_array = set(z_array) else: z_array = set((z_array,)) # might need an epsilon here ? if len(z_array) > 1: raise ValueError(f"More than one value of z found for {entry.name}") else: return z_array.pop() @docstring(BaseAcquisition.register_step) def register_step( self, url: DataUrl, entry_type, copy_frames: bool = False ) -> None: """ :param url: """ with EntryReader(url) as entry: z = self.get_z(entry) if z not in self._acquisitions: new_acquisition = StandardAcquisition( root_url=url, configuration=self.configuration, detector_sel_callback=self._detector_sel_callback, start_index=self.start_index + len(self._acquisitions), parent=self, ) new_acquisition._dark_at_start = self._dark_at_start new_acquisition._flat_at_start = self._flat_at_start new_acquisition._dark_at_end = self._dark_at_end new_acquisition._flat_at_end = self._flat_at_end self._acquisitions[z] = new_acquisition self._acquisitions[z].register_step( url=url, entry_type=entry_type, copy_frames=copy_frames ) nxtomomill-v2.0.1/nxtomomill/converter/hdf5/acquisitionConstructor.py000066400000000000000000000542001511430602400263060ustar00rootroot00000000000000from __future__ import annotations import os import h5py import numpy import logging from tqdm import tqdm from silx.io.utils import open as open_hdf5 from silx.io.utils import h5py_read_dataset from silx.io.url import DataUrl from nxtomomill.utils.utils import str_datetime_to_numpy_datetime64 from nxtomomill.models.h52nx.FrameGroup import ( FrameGroup, filter_acqui_frame_type, ) from nxtomomill.models.h52nx._acquisitionstep import AcquisitionStep from nxtomomill.io.config import TomoHDF5Config from nxtomomill.models.utils import _get_title_dataset from nxtomomill.utils.hdf5 import EntryReader from nxtomomill.converter.hdf5.acquisition.multitomo import MultiTomoAcquisition from nxtomomill.converter.hdf5.acquisition.backandforth import BackAndForthAcquisition from .acquisition.utils.bliss_scan_type import get_bliss_scan_type from .acquisition.baseacquisition import BaseAcquisition from .acquisition.standardacquisition import StandardAcquisition from .acquisition.zseriesacquisition import ( ZSeriesBaseAcquisition, is_z_series_sequence, is_z_series_frm_translation_z, ) try: import hdf5plugin # noqa F401 except ImportError: pass _logger = logging.getLogger(__name__) def is_multitomo_sequence( entry: h5py.Group, configuration: TomoHDF5Config, fallback_on_title: bool = True ) -> bool: """ Check if the provided h5py.Group must be consider as an "initialization" of a sequence. And if so then if it is a multi-tomo sequence. It will first check for value contained in technique/scan_category else for the title name (legacy) """ # check 'technique/scan_category' first scan_category = entry.get("technique/scan_category", None) if scan_category is not None: category_name = h5py_read_dataset(scan_category) elif fallback_on_title: # else fallback on title check category_name = _get_title_dataset( entry=entry, title_paths=BaseAcquisition.TITLE_PATHS ) else: category_name = None if category_name is None: return False for multitomo_init_title in configuration.multitomo_init_titles: if category_name.startswith(multitomo_init_title): return True return False def is_back_and_forth_sequence( entry: h5py.Group, configuration: TomoHDF5Config, fallback_on_title: bool = True ) -> bool: """ Check if the provided h5py.Group must be consider as an "initialization" of a sequence. And if so then if it is a back-and-forth sequence. It will first check for value contained in technique/scan_category else for the title name (legacy) """ # check 'technique/scan_category' first scan_category = entry.get("technique/scan_category", None) if scan_category is not None: category_name = h5py_read_dataset(scan_category) elif fallback_on_title: # else fallback on title check category_name = _get_title_dataset( entry=entry, title_paths=BaseAcquisition.TITLE_PATHS ) else: category_name = None if category_name is None: return False for back_and_forth_init_title in configuration.back_and_forth_init_titles: if category_name.startswith(back_and_forth_init_title): return True return False class _AcquisitionConstructorBase: """ Base class of an acquisition constructor. The role of an acquisition constructor is to create instances of nxtomomill 'BaseAcquisition'. Then associate each bliss scan to his role (dark, flat, projs) and his (nxtomomill) acquisition. """ def __init__( self, configuration: TomoHDF5Config, progress: tqdm | None, detector_sel_callback, ): """Constructor that build nxtomomill acquisition sequence from a set of entry""" self.configuration = configuration self.progress = progress self.detector_sel_callback = detector_sel_callback self._acquisitions = [] self._current_acquisition = None """Pointer to the `BaseAcquisition` instance to be populated (we expect to add darks, flat and projections to it when iterating on bliss scans)""" self._start_index = 0 """Counter on the index of the NXtomo to be created. This allow us to know from which nxtomo index a new `BaseAcquisition` must start from""" self._need_to_split_to_several_nxtomo = False """ Flag required by multitomo and back-and-forth sequences. We need to wait until all bliss scans are registered to the nxtomomill Acquisition to update 'start_index'. It true at the start of a new the sequence we increase the start index by (nb_loop * nb_tomo) == expected of NXtomo generated by the previous sequence """ def clear_acquisitions(self): self._acquisitions.clear() def build_sequence(self) -> tuple[BaseAcquisition]: raise NotImplementedError("Base class") @staticmethod def sort_fct(node_name: str, h5d: h5py.File): """ sort the scan according to the 'start_time parameter'. If fails keep the original order. If a node has the 'is_rearranged' attribute then skip sort and keep the original sequence. """ # node_link_to_treat = h5d.get(node_name, getlink=True) note_to_treat = h5d.get(node_name) is_rearranged = note_to_treat is not None and note_to_treat.attrs.get( "is_rearranged", False ) # in some case the user might want to keep the order of the original sequence. # in this case we expect some preprocessing to be done and which has tag the node with the 'is_rearranged' attribute if is_rearranged: return False else: node = h5d.get(node_name) if node is not None: # node can be None in the case of a broken link start_time = node.get("start_time", None) else: _logger.warning(f"Broken link at {node_name}") start_time = None if start_time is not None: start_time = h5py_read_dataset(start_time) return str_datetime_to_numpy_datetime64(start_time) elif isinstance(node_link_to_treat, (h5py.ExternalLink, h5py.SoftLink)): return float(node_link_to_treat.path.split("/")[-1]) else: # we expect to have node names like (1.1, 2.1...) return float(node_name) def build_progress(self, n: int) -> tqdm | None: """create tqdm progress bar""" if self.progress is not None: progress_read = tqdm(desc="read sequences") progress_read.total = n return progress_read def _update_start_index_for_acquisitions_splitting_nxtomos( self, url: DataUrl ) -> None: """ Util called when processing `BaseAcquisition` instance splitting the NXtomo generated to update the '_start_index' This is the case for multi-tomo and back-and-forth tomo """ nb_loop = self._current_acquisition.get_nb_loop(url) nb_tomo = self._current_acquisition.get_nb_tomo(url) if nb_loop is not None and nb_tomo is not None: self._start_index += int(nb_loop) * int(nb_tomo) self._need_to_split_to_several_nxtomo = False def _update_start_index_for_zseries(self, url: DataUrl) -> None: """ Util called when processing `ZSeriesBaseAcquisition` instance splitting the NXtomo generated to update the '_start_index' """ with EntryReader(url) as entry: z = self._current_acquisition.get_z(entry) if z not in self._acquisitions: self._start_index += 1 class _AcquisitionConstructorFromUrls(_AcquisitionConstructorBase): """ Create (nxtomomill) acquisitions from a set of urls and roles (dark, flat, proj) provided by the users """ def build_sequence(self) -> tuple[BaseAcquisition]: """ Build acquisitions classes from the url definition :return: """ self.clear_acquisitions() # when building from urls `tomo_n` has no meaning if self.configuration.check_tomo_n is None: self.configuration.check_tomo_n = False if self.configuration.is_using_titles: raise ValueError("Configuration specify that titles should be used") assert self.configuration.output_file is not None, "output_file requested" data_scans = self.configuration.data_scans # step 0: copy some urls instead if needed # update copy parameter for frame_grp in data_scans: if frame_grp.copy is None: frame_grp.copy = self.configuration.default_data_copy # step 1: if there is no init FrameGroup create an empty one because # this is requested if len(data_scans) == 0: return elif data_scans[0].frame_type is not AcquisitionStep.INITIALIZATION: data_scans = [ FrameGroup(frame_type=AcquisitionStep.INITIALIZATION, url=None), ] data_scans.extend(self.configuration.data_scans) self.configuration.data_scans = tuple(data_scans) # step 2: treat FrameGroups progress_read = self.build_progress(len(data_scans)) for frame_grp in data_scans: self.treat_bliss_scan(frame_grp, progress_read=progress_read) return tuple(self._acquisitions) def treat_bliss_scan(self, frame_grp: FrameGroup, progress_read: tqdm | None): """ Treat a bliss scan from an instance of `FrameGroup`. This instance contains information provided by the user from the configuration file (url, frame_type and copy) :param frame_grp: Group to treat with (url, frame type and copy) :param progress_read: tqdm progress bar """ if progress_read is not None: progress_read.update() # handle frame_type == init if frame_grp.frame_type is AcquisitionStep.INITIALIZATION: # re-init the constructor if self._need_to_split_to_several_nxtomo is True: _logger.warning( f"Fail to retrieve expected number of nxtomo for {self._current_acquisition}" ) self._need_to_split_to_several_nxtomo = False self._current_acquisition = self._get_current_acquisition( frame_grp=frame_grp ) if isinstance( self._current_acquisition, (MultiTomoAcquisition, BackAndForthAcquisition), ): self._start_index += 0 self._need_to_split_to_several_nxtomo = True elif isinstance(self._current_acquisition, StandardAcquisition): self._start_index += self._current_acquisition.get_expected_nx_tomo() self._need_to_split_to_several_nxtomo = False elif isinstance(self._current_acquisition, ZSeriesBaseAcquisition): self._need_to_split_to_several_nxtomo = False self._start_index += 0 self._acquisitions.append(self._current_acquisition) # handle frame_type != init else: if self._current_acquisition is None: raise RuntimeError( "processing error. Was not able to find acquisition initialization scan. Is it a bliss-tomo acquisition ?" ) self._current_acquisition.register_step( url=frame_grp.url, entry_type=frame_grp.frame_type, copy_frames=frame_grp.copy_data, ) # in case of z we append an index according to if # is already registered or not if isinstance(self._current_acquisition, ZSeriesBaseAcquisition): self._update_start_index_for_zseries(url=frame_grp.url) if self._need_to_split_to_several_nxtomo: if frame_grp.frame_type is AcquisitionStep.PROJECTION: self._update_start_index_for_acquisitions_splitting_nxtomos( url=frame_grp.url ) def _get_current_acquisition(self, frame_grp: FrameGroup): """Deduce the acquisition type from the frame group""" acqui_projs_fg = filter_acqui_frame_type( init=frame_grp, sequences=self.configuration.data_scans, frame_type=AcquisitionStep.PROJECTION, ) acqui_projs_urls = tuple([acqui_proj.url for acqui_proj in acqui_projs_fg]) h5f = None try: # open the initialization url if frame_grp.url is not None: h5f = h5py.File(frame_grp.url.file_path(), mode="r") init_entry = h5f.get(frame_grp.url.data_path()) else: init_entry = None if is_z_series_frm_translation_z(acqui_projs_urls, self.configuration): return ZSeriesBaseAcquisition( root_url=frame_grp.url, configuration=self.configuration, detector_sel_callback=self.detector_sel_callback, start_index=self._start_index, ) elif (init_entry is not None) and is_multitomo_sequence( init_entry, self.configuration ): return MultiTomoAcquisition( root_url=frame_grp.url, configuration=self.configuration, detector_sel_callback=self.detector_sel_callback, start_index=self._start_index, ) elif (init_entry is not None) and is_back_and_forth_sequence( init_entry, self.configuration ): return BackAndForthAcquisition( root_url=frame_grp.url, configuration=self.configuration, detector_sel_callback=self.detector_sel_callback, start_index=self._start_index, ) else: return StandardAcquisition( root_url=frame_grp.url, configuration=self.configuration, detector_sel_callback=self.detector_sel_callback, start_index=self._start_index, ) finally: init_entry = None if h5f is not None: h5f.close() class _AcquisitionConstructorFromTitles(_AcquisitionConstructorBase): """ determine bliss scan role (dark, flat, proj, init) from titles. Create and populate instance of BaseAcquisition according to those roles. """ def build_sequence(self) -> tuple[BaseAcquisition]: self.clear_acquisitions() if self.configuration.check_tomo_n is None: self.configuration.check_tomo_n = True with open_hdf5(self.configuration.input_file) as h5d: groups = list(h5d.keys()) try: def sort(name): return self.sort_fct(name, h5d=h5d) groups.sort(key=sort) except numpy.core._exceptions._UFuncNoLoopError: raise ValueError( "Fail to order according to 'start_time'. Probably not all scans have a 'start_time' dataset" ) # step 1: pre processing: group scan together progress_read = self.build_progress(len(groups)) for group_name in groups: self.treat_bliss_scan( group_name=group_name, h5d=h5d, progress_read=progress_read ) return self._acquisitions def treat_bliss_scan(self, group_name: h5py.Group, h5d, progress_read: tqdm | None): """ Treat a bliss scan (given as a group). Either create a new 'current acquisition' or register it to the previous (except if the scan is asked to be ignored by the use) :param group_name: group to be treat :param scan_group: h5d - h5py.File (open) containing the group. :param progress_read: optional progress to provide feedback """ _logger.debug(f"parse {group_name}") if progress_read is not None: progress_read.update() try: entry = h5d[group_name] except KeyError: # case the key doesn't exist. Usual use case is that a bliss scan has been canceled _logger.warning( f"Unable to open {group_name} from {h5d.name}. Did the scan was canceled ? (Most likely). Skip this entry" ) return # improve handling of External (this is the case of proposal files) if isinstance(h5d.get(group_name, getlink=True), h5py.ExternalLink): external_link = h5d.get(group_name, getlink=True) file_path = external_link.filename data_path = external_link.path if not os.path.isabs(file_path): file_path = os.path.abspath( os.path.join( os.path.dirname(self.configuration.input_file), file_path, ) ) else: file_path = self.configuration.input_file data_path = entry.name url = DataUrl( file_path=file_path, data_path=data_path, scheme="silx", data_slice=None, ) entry_type = get_bliss_scan_type(url=url, configuration=self.configuration) if entry_type is AcquisitionStep.INITIALIZATION: if self._need_to_split_to_several_nxtomo is True: _logger.warning( f"Fail to retrieve expected number of nxtomo for {self._current_acquisition}" ) try: if is_z_series_sequence(entry=entry, configuration=self.configuration): self._current_acquisition = ZSeriesBaseAcquisition( root_url=url, configuration=self.configuration, detector_sel_callback=self.detector_sel_callback, start_index=self._start_index, ) self._start_index += ( self._current_acquisition.get_expected_nx_tomo() ) elif is_multitomo_sequence( entry=entry, configuration=self.configuration ): self._current_acquisition = MultiTomoAcquisition( root_url=url, configuration=self.configuration, detector_sel_callback=self.detector_sel_callback, start_index=self._start_index, ) self._start_index += 0 self._need_to_split_to_several_nxtomo = True elif is_back_and_forth_sequence( entry=entry, configuration=self.configuration ): self._current_acquisition = BackAndForthAcquisition( root_url=url, configuration=self.configuration, detector_sel_callback=self.detector_sel_callback, start_index=self._start_index, ) self._start_index += 0 self._need_to_split_to_several_nxtomo = True else: self._current_acquisition = StandardAcquisition( root_url=url, configuration=self.configuration, detector_sel_callback=self.detector_sel_callback, start_index=self._start_index, ) self._start_index += ( self._current_acquisition.get_expected_nx_tomo() ) except Exception as e: if self._ignore_entry(group_name): return else: raise e if self._ignore_entry(group_name): self._current_acquisition = None return self._acquisitions.append(self._current_acquisition) # continue "standard" tomo dataset handling elif self._current_acquisition is not None and not self._ignore_sub_entry(url): self._current_acquisition.register_step( url=url, entry_type=entry_type, copy_frames=self.configuration.default_data_copy, ) # in case of z we append an index according to if # is already registered or not if isinstance(self._current_acquisition, ZSeriesBaseAcquisition): self._update_start_index_for_zseries(url=url) if self._need_to_split_to_several_nxtomo: if entry_type is AcquisitionStep.PROJECTION: self._update_start_index_for_acquisitions_splitting_nxtomos(url=url) else: _logger.info(f"ignore entry {entry}") def _ignore_entry(self, group_name) -> bool: """check if the entry is part of the configuration entries (if provided)""" if len(self.configuration.entries) == 0: return False else: if not group_name.startswith("/"): group_name = "/" + group_name for entry in self.configuration.entries: if group_name == entry.data_path(): return False return True def _ignore_sub_entry(self, sub_entry_url: DataUrl | None) -> bool: """ :return: True if the provided sub_entry should be ignored """ if sub_entry_url is None: return False if not isinstance(sub_entry_url, DataUrl): raise TypeError( f"sub_entry_url is expected to be a DataUrl not {type(sub_entry_url)}" ) if self.configuration.sub_entries_to_ignore is None: return False sub_entry_fp = sub_entry_url.file_path() sub_entry_dp = sub_entry_url.data_path() for entry in self.configuration.sub_entries_to_ignore: assert isinstance(entry, DataUrl) if entry.file_path() == sub_entry_fp and entry.data_path() == sub_entry_dp: return True return False nxtomomill-v2.0.1/nxtomomill/converter/hdf5/hdf5converter.py000066400000000000000000000504411511430602400242710ustar00rootroot00000000000000# coding: utf-8 """ module to convert from (bliss) .h5 to (nexus tomo compliant) .nx """ from __future__ import annotations import logging import os import sys import h5py from tqdm import tqdm from silx.io.url import DataUrl from silx.io.utils import open as open_hdf5 from tomoscan.io import HDF5File from nxtomomill.converter.hdf5.acquisition.utils import group_series from nxtomomill.converter.baseconverter import BaseConverter from nxtomomill.converter.hdf5.acquisition.baseacquisition import _ask_for_file_removal from nxtomomill.converter.hdf5.acquisition.multitomo import MultiTomoAcquisition from nxtomomill.models.h52nx._acquisitionstep import AcquisitionStep from nxtomomill.models.h52nx import H52nxModel from tomoscan.utils.io import filter_esrf_mounting_points from .acquisition.baseacquisition import BaseAcquisition from .acquisition.standardacquisition import StandardAcquisition from .acquisition.utils import get_bliss_scan_type from .acquisition.zseriesacquisition import ( ZSeriesBaseAcquisition, ) from .post_processing.dark_flat_copy import ZSeriesDarkFlatCopy from .acquisitionConstructor import ( _AcquisitionConstructorFromTitles, _AcquisitionConstructorFromUrls, ) try: import hdf5plugin # noqa F401 except ImportError: pass # import that should be removed when h5_to_nx will be removed from nxtomomill.converter.hdf5.utils import H5FileKeys, H5ScanTitles from nxtomomill.settings import Tomo H5_ROT_ANGLE_KEYS = Tomo.H5.ROT_ANGLE_KEYS H5_VALID_CAMERA_NAMES = Tomo.H5.VALID_CAMERA_NAMES H5_SAMPLE_X_KEYS = Tomo.H5.SAMPLE_X_KEYS H5_SAMPLE_Y_KEYS = Tomo.H5.SAMPLE_Y_KEYS H5_TRANSLATION_Z_KEYS = Tomo.H5.TRANSLATION_Z_KEYS H5_ALIGNMENT_TITLES = Tomo.H5.ALIGNMENT_TITLES H5_ACQ_EXPO_TIME_KEYS = Tomo.H5.ACQ_EXPO_TIME_KEYS H5_SAMPLE_X_PIXEL_SIZE = Tomo.H5.SAMPLE_X_PIXEL_SIZE_KEYS H5_SAMPLE_Y_PIXEL_SIZE = Tomo.H5.SAMPLE_Y_PIXEL_SIZE_KEYS H5_DETECTOR_X_PIXEL_SIZE = Tomo.H5.DETECTOR_X_PIXEL_SIZE_KEYS H5_DETECTOR_Y_PIXEL_SIZE = Tomo.H5.DETECTOR_Y_PIXEL_SIZE_KEYS H5_DARK_TITLES = Tomo.H5.DARK_TITLES H5_INIT_TITLES = Tomo.H5.INIT_TITLES H5_MULTITOMO_INIT_TITLES = Tomo.H5.MULTITOMO_INIT_TITLES H5_BACK_AND_FORTH_INIT_TITLES = Tomo.H5.BACK_AND_FORTH_INIT_TITLES H5_ZSERIE_INIT_TITLES = Tomo.H5.ZSERIE_INIT_TITLES H5_PROJ_TITLES = Tomo.H5.PROJ_TITLES H5_FLAT_TITLES = Tomo.H5.FLAT_TITLES H5_REF_TITLES = H5_FLAT_TITLES H5_TRANSLATION_Y_KEYS = Tomo.H5.TRANSLATION_Y_KEYS H5_DIODE_KEYS = Tomo.H5.DIODE_KEYS # deprecated variables H5_PCOTOMO_INIT_TITLES = H5_MULTITOMO_INIT_TITLES DEFAULT_SCAN_TITLES = H5ScanTitles( H5_INIT_TITLES, H5_ZSERIE_INIT_TITLES, H5_MULTITOMO_INIT_TITLES, H5_BACK_AND_FORTH_INIT_TITLES, H5_DARK_TITLES, H5_FLAT_TITLES, H5_PROJ_TITLES, H5_ALIGNMENT_TITLES, ) DEFAULT_H5_KEYS = H5FileKeys( H5_ACQ_EXPO_TIME_KEYS, H5_ROT_ANGLE_KEYS, H5_VALID_CAMERA_NAMES, H5_SAMPLE_X_KEYS, H5_SAMPLE_Y_KEYS, H5_TRANSLATION_Z_KEYS, H5_TRANSLATION_Y_KEYS, H5_SAMPLE_X_PIXEL_SIZE, H5_SAMPLE_Y_PIXEL_SIZE, H5_DETECTOR_X_PIXEL_SIZE, H5_DETECTOR_Y_PIXEL_SIZE, H5_DIODE_KEYS, ) _logger = logging.getLogger(__name__) class _H5ToNxConverter(BaseConverter): """ Class used to convert a HDF5Config to one or several NXTomoEntry. :param configuration: configuration for the translation. such as the input and output file, keys... :param input_callback: possible callback in case of missing information :param progress: progress bar to be updated if provided :param detector_sel_callback: callback for the detector selection if any Conversion is a two step process: step 1: preprocessing * insure configuration is valid and that we don't have "unsafe" or "opposite" request / rules * normalize input URL (complete data_file if not provided) * copy some frame group if requested * create instances of BaseAcquisition classes that will be used to write NXTomo entries * handle z series specific case step 2: write NXTomo entries to the output file """ def __init__( self, configuration: H52nxModel, input_callback=None, progress: tqdm | None = None, detector_sel_callback=None, ): if not isinstance(configuration, H52nxModel): raise TypeError( f"configuration should be an instance of HDFConfig not {type(configuration)}" ) self._configuration = configuration self._progress = progress self._input_callback = input_callback self._detector_sel_callback = detector_sel_callback self._acquisitions = [] self._entries_created = [] self._z_series_v2_v3: list[list[ZSeriesBaseAcquisition]] = [] # bliss z-series for version 2 and 3. Can be used for post-processing self.preprocess() @property def configuration(self): return self._configuration @property def progress(self): return self._progress @property def input_callback(self): return self._input_callback @property def detector_sel_callback(self): return self._detector_sel_callback @property def entries_created(self) -> tuple: """tuple of entries created. Each element is provided as (output_file, entry)""" return tuple(self._entries_created) @property def acquisitions(self): return self._acquisitions def preprocess(self): # clean path if self._configuration.input_file is not None: self._configuration.input_file = filter_esrf_mounting_points( self._configuration.input_file ) if self._configuration.output_file is not None: self._configuration.output_file = filter_esrf_mounting_points( self._configuration.output_file ) self._preprocess_urls() self._check_conversion_is_possible() if self.configuration.is_using_titles: self._convert_entries_and_sub_entries_to_urls() acquisition_builder = _AcquisitionConstructorFromTitles( configuration=self.configuration, progress=self.progress, detector_sel_callback=self.detector_sel_callback, ) self._acquisitions = acquisition_builder.build_sequence() else: self.configuration.clear_entries_and_subentries() acquisition_builder = _AcquisitionConstructorFromUrls( configuration=self.configuration, progress=self.progress, detector_sel_callback=self.detector_sel_callback, ) self._acquisitions = acquisition_builder.build_sequence() self._z_series_v2_v3 = self._handle_zseries() def _handle_zseries(self): # for z series we have a "master" acquisition of type # ZSeriesBaseAcquisition. But this is used only to build # the acquisition sequence. To write we use the z series # "sub_acquisitions" which are instances of "StandardAcquisition" acquisitions = [] z_series_v2_to_v3 = [] for acquisition in self.acquisitions: if isinstance(acquisition, StandardAcquisition): acquisitions.append(acquisition) elif isinstance(acquisition, ZSeriesBaseAcquisition): sub_acquisitions = acquisition.get_standard_sub_acquisitions() acquisitions.extend(sub_acquisitions) for sub_acquisition in sub_acquisitions: z_series_v2_to_v3 = group_series( acquisition=sub_acquisition, list_of_series=z_series_v2_to_v3 ) else: raise TypeError(f"Acquisition type {type(acquisition)} not handled") self._acquisitions = acquisitions return z_series_v2_to_v3 def convert(self): mess_conversion = f"start conversion from {self.configuration.input_file} to {self.configuration.output_file}" if self.progress is not None: # in the case we want to print progress sys.stdout.write(mess_conversion) sys.stdout.flush() else: _logger.info(mess_conversion) self._entries_created = self.write() return self._entries_created def _ignore_sub_entry(self, sub_entry_url: DataUrl | None): """ :return: True if the provided sub_entry should be ignored """ if sub_entry_url is None: return False if not isinstance(sub_entry_url, DataUrl): raise TypeError( f"sub_entry_url is expected to be a DataUrl not {type(sub_entry_url)}" ) if self.configuration.sub_entries_to_ignore is None: return False sub_entry_fp = sub_entry_url.file_path() sub_entry_dp = sub_entry_url.data_path() for entry in self.configuration.sub_entries_to_ignore: assert isinstance(entry, DataUrl) if entry.file_path() == sub_entry_fp and entry.data_path() == sub_entry_dp: return True return False def write(self): res = [] acq_str = [str(acq) for acq in self.acquisitions] acq_str.insert( 0, f"parsing finished. {len(self.acquisitions)} acquisitions found" ) _logger.debug("\n - ".join(acq_str)) if len(self.acquisitions) == 0: _logger.warning( "No valid acquisitions have been found. Most likely " "no init titles have been found. You can provide more valid entries from CLI or configuration file." ) if self.progress is not None: progress_write = tqdm(desc="write NXtomos") progress_write.total = len(self.acquisitions) else: progress_write = None # write nx_tomo per acquisition has_single_acquisition_in_file = len(self.acquisitions) == 1 and isinstance( self.acquisitions, MultiTomoAcquisition ) divide_into_sub_files = not ( self.configuration.single_file is False and has_single_acquisition_in_file ) acquisition_to_nxtomo: dict[ZSeriesBaseAcquisition, tuple[str] | None] = {} for acquisition in self.acquisitions: if self._ignore_sub_entry(acquisition.root_url): acquisition_to_nxtomo[acquisition] = None continue try: new_entries = acquisition.write_as_nxtomo( shift_entry=acquisition.start_index, input_file_path=self.configuration.input_file, request_input=self.configuration.request_input, input_callback=self.input_callback, divide_into_sub_files=divide_into_sub_files, ) except Exception as e: if self.configuration.raises_error: raise e else: root_location = ( acquisition.root_url.path() if acquisition.root_url is not None else "" ) _logger.error( f"Fail to convert '{root_location}' sequence. Error is {str(e)}", exc_info=e, ) acquisition_to_nxtomo[acquisition] = None else: res.extend(new_entries) acquisition_to_nxtomo[acquisition] = new_entries if progress_write is not None: progress_write.update() # post processing on nxtomos for series in self._z_series_v2_v3: self._post_process_series(series, acquisition_to_nxtomo) # if we created one file per entry then create a master file with link to those entries if ( self.configuration.single_file is False and divide_into_sub_files ) and not self.configuration.no_master_file: _logger.info(f"create link in {self.configuration.output_file}") for en_output_file, entry in res: with HDF5File(self.configuration.output_file, "a") as master_file: link_file = os.path.relpath( en_output_file, os.path.dirname(self.configuration.output_file), ) master_file[entry] = h5py.ExternalLink(link_file, entry) return tuple(res) def _check_conversion_is_possible(self): """Insure minimalistic information are provided""" if self.configuration.is_using_titles: if self.configuration.input_file is None: raise ValueError("input file should be provided") if not os.path.isfile(self.configuration.input_file): raise ValueError( f"Given input file does not exists: {self.configuration.input_file}" ) if not h5py.is_hdf5(self.configuration.input_file): raise ValueError("Given input file is not an hdf5 file") if self.configuration.input_file == self.configuration.output_file: raise ValueError("input and output file are the same") output_file = self.configuration.output_file dir_name = os.path.dirname(os.path.abspath(output_file)) if not os.path.exists(dir_name): os.makedirs(os.path.dirname(os.path.abspath(output_file))) elif os.path.exists(output_file): if self.configuration.overwrite is True: _logger.warning(f"{output_file} will be removed") _logger.info(f"remove {output_file}") os.remove(output_file) elif not _ask_for_file_removal(output_file): raise OSError(f"unable to overwrite {output_file}, exit") else: os.remove(output_file) if not os.access(dir_name, os.W_OK): raise OSError(f"You don't have rights to write on {dir_name}") def _convert_entries_and_sub_entries_to_urls(self): if len(self.configuration.entries) > 0: urls = self.configuration.entries entries = self._upgrade_urls( urls=urls, input_file=self.configuration.input_file ) self.configuration.entries = entries if self.configuration.sub_entries_to_ignore is not None: urls = self.configuration.sub_entries_to_ignore entries = self._upgrade_urls( urls=urls, input_file=self.configuration.input_file ) self.configuration.sub_entries_to_ignore = entries def _preprocess_urls(self): """ Update darks, flats, projections and alignments urls if no file path is provided """ self.configuration.data_scans = self._upgrade_frame_grp_urls( frame_grps=self.configuration.data_scans, input_file=self.configuration.input_file, default_copy=self.configuration.default_data_copy, ) def _post_process_series( self, series: list[BaseAcquisition], acquisition_to_nxtomo: dict[BaseAcquisition, tuple | None], ): dark_flat_copy = ZSeriesDarkFlatCopy( series=series, acquisition_to_nxtomo=acquisition_to_nxtomo ) dark_flat_copy.run() @staticmethod def _upgarde_url(url: DataUrl, input_file: str) -> DataUrl: if url is not None and url.file_path() in (None, ""): if input_file in (None, str): raise ValueError( f"file_path for url {url.path()} is not provided and no input_file provided either." ) else: return DataUrl( file_path=input_file, scheme="silx", data_slice=url.data_slice(), data_path=url.data_path(), ) else: return url @staticmethod def _upgrade_frame_grp_urls( frame_grps: tuple, input_file: str | None, default_copy: bool ) -> tuple: """ Upgrade all Frame Group DataUrl which did not contain a file_path to reference the input_file """ if input_file is not None and not h5py.is_hdf5(input_file): raise ValueError(f"{input_file} is not a h5py file") for frame_grp in frame_grps: frame_grp.url = _H5ToNxConverter._upgarde_url(frame_grp.url, input_file) if frame_grp.copy_data is None: frame_grp.copy_data = default_copy return frame_grps @staticmethod def _upgrade_urls(urls: tuple, input_file: str | None) -> tuple: """ Upgrade all DataUrl which did not contain a file_path to reference the input_file """ if input_file is not None and not h5py.is_hdf5(input_file): raise ValueError(f"{input_file} is not a h5py file") return tuple([_H5ToNxConverter._upgarde_url(url, input_file) for url in urls]) def from_h5_to_nx( configuration: H52nxModel, input_callback=None, progress: tqdm | None = None, detector_sel_callback=None, ): """ convert a bliss file to a set of NXtomo :param configuration: configuration for the translation. such as the input and output file, keys... :param input_callback: possible callback in case of missing information :param progress: progress bar to be updated if provided :param detector_sel_callback: callback for the detector selection if any :return: tuple of created NXtomo as (output_file, data_path) """ converter = _H5ToNxConverter( configuration=configuration, input_callback=input_callback, progress=progress, detector_sel_callback=detector_sel_callback, ) return converter.convert() def get_bliss_tomo_entries(input_file_path: str, configuration: H52nxModel): """. Return the set of entries at root that match bliss entries. Used by tomwer for example. :param input_file_path: path of the file to browse :param configuration: configuration of the conversion. This way user can define title to be used or frame groups Warning: entries can be external links (in the case of the file being a proposal file) """ if not isinstance(configuration, H52nxModel): raise TypeError("configuration is expected to be a HDF5Config") with open_hdf5(input_file_path) as h5d: acquisitions = [] for group_name in h5d.keys(): _logger.debug(f"parse {group_name}") entry = h5d[group_name] # improve handling of External (this is the case of proposal files) if isinstance(h5d.get(group_name, getlink=True), h5py.ExternalLink): external_link = h5d.get(group_name, getlink=True) file_path = external_link.filename data_path = external_link.path else: file_path = input_file_path data_path = entry.name if not data_path.startswith("/"): data_path = "/" + data_path url = DataUrl(file_path=file_path, data_path=data_path) if configuration.is_using_titles: # if use title take the ones corresponding to init entry_type = get_bliss_scan_type(url=url, configuration=configuration) if entry_type is AcquisitionStep.INITIALIZATION: acquisitions.append(group_name) else: # check if the entry fit one of the data_scans # with an init status possible_url_file_path = [ os.path.abspath(url.file_path()), url.file_path(), ] if configuration.output_file not in ("", None): possible_url_file_path.append( os.path.relpath( url.file_path(), os.path.dirname(configuration.output_file) ) ) for frame_grp in configuration.data_scans: if frame_grp.frame_type is AcquisitionStep.INITIALIZATION: if ( frame_grp.url.file_path() in possible_url_file_path and frame_grp.data_path() == url.data_path() ): acquisitions.append(entry.name) return acquisitions nxtomomill-v2.0.1/nxtomomill/converter/hdf5/post_processing/000077500000000000000000000000001511430602400243565ustar00rootroot00000000000000nxtomomill-v2.0.1/nxtomomill/converter/hdf5/post_processing/__init__.py000066400000000000000000000000001511430602400264550ustar00rootroot00000000000000nxtomomill-v2.0.1/nxtomomill/converter/hdf5/post_processing/dark_flat_copy.py000066400000000000000000000224711511430602400277170ustar00rootroot00000000000000from __future__ import annotations import numpy import logging from silx.io.url import DataUrl from tomoscan.esrf.scan.utils import get_series_slice, get_n_series from nxtomo.application.nxtomo import NXtomo from nxtomo.nxobject.nxdetector import ImageKey from nxtomo.paths.nxtomo import get_paths as get_nexus_paths from nxtomomill.converter.hdf5.acquisition.baseacquisition import BaseAcquisition from nxtomomill.utils.utils import add_dark_flat_nx_file _logger = logging.getLogger(__name__) __all__ = [ "ZSeriesDarkFlatCopy", ] class ZSeriesDarkFlatCopy: """ z-series version 3 can require to reuse dark-flat from a bliss scan done at the start or at the end of the series. This class is an helper to do this processing. """ def __init__( self, series: list[BaseAcquisition], acquisition_to_nxtomo: dict[BaseAcquisition, tuple | None], ) -> None: """ :param series: list of acquisition part of the series. **warning**: we expect this list to be ordered. :param acquisition_to_nxtomo: for each acquisition in "series" this dict can provide the nxtomo created during conversion. The value is expected to the None if the conversion failed or (file_path, data_path) .. warning:: even if list contains BaseAcquisition; those are sub_acquisitions of ZSeriesBaseAcquisition """ for elmt in series: if not isinstance(elmt, BaseAcquisition): raise TypeError( f"elmt is expected to be an instance of {BaseAcquisition}. Got {type(elmt)}" ) self.series = series self.acquisition_to_nxtomo = acquisition_to_nxtomo @property def first_acquisition(self) -> BaseAcquisition: return self.series[0] @property def last_acquisition(self) -> BaseAcquisition: return self.series[-1] def run(self) -> None: darks_start_urls, flats_start_urls, darks_end_urls, flats_end_urls = ( self.build_mapping_to_dark_flat_source() ) edition_to_do = self.build_edition_to_do( darks_start_urls=darks_start_urls, flats_start_urls=flats_start_urls, darks_end_urls=darks_end_urls, flats_end_urls=flats_end_urls, ) self.process_edition(editions_to_do=edition_to_do) def build_mapping_to_dark_flat_source( self, ) -> dict[tuple, dict[ImageKey, list[DataUrl]]]: """ Build the list of DataUrl that can be used for copy. Output looks like: { file_path_first_nxtomo, data_path_first_nxtomo: { ImageKey.DARK_FIELD: (DataUrlSeries1, DataUrlSeries2), ImageKey.FLAT_FIELD: (DataUrlSeries1, ...), }, file_path_last_nxtomo, data_path_last_nxtomo: { ImageKey.DARK_FIELD: (DataUrlSeries1, DataUrlSeries2), ImageKey.FLAT_FIELD: (DataUrlSeries1, ...), }, } """ darks_start_urls = [] flats_start_urls = [] darks_end_urls = [] flats_end_urls = [] # create darks_start_url and flats_start_url if possible first_nxtomos_infos = self.acquisition_to_nxtomo[self.first_acquisition][0] last_nxtomos_infos = self.acquisition_to_nxtomo[self.last_acquisition][-1] url_mapping = {} if first_nxtomos_infos is not None: url_mapping[first_nxtomos_infos] = { ImageKey.DARK_FIELD: darks_start_urls, ImageKey.FLAT_FIELD: flats_start_urls, } if last_nxtomos_infos is not None: url_mapping[last_nxtomos_infos] = { ImageKey.DARK_FIELD: darks_end_urls, ImageKey.FLAT_FIELD: flats_end_urls, } # build darks / flats urls for (file_path, data_path), image_key_to_urls in url_mapping.items(): nxtomo_source = NXtomo().load( file_path=file_path, data_path=data_path, ) for image_key, urls in image_key_to_urls.items(): n_series = get_n_series( image_key_values=nxtomo_source.instrument.detector.image_key_control, image_key_type=image_key, ) for i_series in range(n_series): slice_source = get_series_slice( image_key_values=nxtomo_source.instrument.detector.image_key_control, image_key_type=image_key, series_index=i_series, ) if slice_source is None: continue detector_dataset_path = get_nexus_paths(version=None) detector_data_path = "/".join( ( data_path, nxtomo_source.instrument.detector.path, detector_dataset_path.nx_detector_paths.DATA, ) ) urls.append( DataUrl( file_path=file_path, data_path=detector_data_path, data_slice=slice_source, scheme="silx", ) ) return ( tuple(darks_start_urls), tuple(flats_start_urls), tuple(darks_end_urls), tuple(flats_end_urls), ) def build_edition_to_do( self, darks_start_urls: tuple[DataUrl], flats_start_urls: tuple[DataUrl], darks_end_urls: tuple[DataUrl], flats_end_urls: tuple[DataUrl], ) -> dict[tuple, dict]: """ build all the edition to do to complete the dark and flat copy according to z-series v3 configuration. """ # for each (final) acquisition register the different operation (edition) that needs to be done. editions_to_do: {tuple, dict} = {} # for each new entry (as a tuple of file_path, data_path) list the operations to execute for acquisition in self.series: if acquisition is None: # if conversion failed continue if acquisition._dark_at_start and ( acquisition is not self.first_acquisition ): concatenate_dict( editions_to_do, { nx_tomo: {"darks_start": darks_start_urls} for nx_tomo in self.acquisition_to_nxtomo[acquisition] }, ) if acquisition._flat_at_start and ( acquisition is not self.first_acquisition ): concatenate_dict( editions_to_do, { nx_tomo: {"flats_start": flats_start_urls} for nx_tomo in self.acquisition_to_nxtomo[acquisition] }, ) if acquisition._dark_at_end and (acquisition is not self.last_acquisition): concatenate_dict( editions_to_do, { nx_tomo: {"darks_end": darks_end_urls} for nx_tomo in self.acquisition_to_nxtomo[acquisition] }, ) if acquisition._flat_at_end and (acquisition is not self.last_acquisition): concatenate_dict( editions_to_do, { nx_tomo: {"flats_end": flats_end_urls} for nx_tomo in self.acquisition_to_nxtomo[acquisition] }, ) return editions_to_do def process_edition( self, editions_to_do: dict[tuple, dict], embed_dark_flat: bool = True ): """ do edition """ for nxtomo_to_edit, params_to_urls in editions_to_do.items(): for param_name, urls in params_to_urls.items(): edited_nxtomo_file_path, edited_nxtomo_data_path = nxtomo_to_edit for url in urls: # if the file already contains the dark / flat if ( url.file_path() == edited_nxtomo_file_path and url.data_path() == edited_nxtomo_data_path ): continue add_dark_flat_nx_file( **{ param_name: url, "file_path": edited_nxtomo_file_path, "entry": edited_nxtomo_data_path, "embed_data": embed_dark_flat, "logger": _logger, } ) def concatenate_dict(dict_1: dict, dict_2: dict) -> None: """ concatenate two dicts into dict_1 """ assert isinstance(dict_1, dict) assert isinstance(dict_2, dict) for key in dict_2.keys(): if key in dict_1.keys(): if isinstance(dict_1[key], dict) and isinstance(dict_2[key], dict): concatenate_dict(dict_1=dict_1[key], dict_2=dict_2[key]) else: dict_1[key] = numpy.concatenate((dict_1[key], dict_2[key])) else: dict_1[key] = dict_2[key] nxtomomill-v2.0.1/nxtomomill/converter/hdf5/tests/000077500000000000000000000000001511430602400222775ustar00rootroot00000000000000nxtomomill-v2.0.1/nxtomomill/converter/hdf5/tests/test_frame_flip.py000066400000000000000000000037621511430602400260240ustar00rootroot00000000000000from __future__ import annotations import os import shutil import pytest import h5py import numpy from nxtomo import NXtomo from nxtomo.utils.transformation import DetXFlipTransformation, DetYFlipTransformation from nxtomomill.models.h52nx import H52nxModel from nxtomomill.converter.hdf5.hdf5converter import from_h5_to_nx from nxtomomill.tests.datasets import GitlabDataset @pytest.fixture() def dataset_id16a(tmp_path): source_file = GitlabDataset.get_dataset( "h5_datasets/holotomo/id16a/Atomium_S2_holo4_HT_010nm_0001.h5" ) return shutil.copyfile( source_file, os.path.join(tmp_path, "Atomium_S2_holo1_HT_010nm_500proj_0003.h5"), ) @pytest.mark.parametrize("soft_flip", ((True, False), (False, True))) @pytest.mark.parametrize("mechanical_flip", ((False, False), (True, True))) def test_frame_flips(tmp_path, dataset_id16a, soft_flip, mechanical_flip): # edit with h5py.File(dataset_id16a, mode="a") as h5f: tomo_config = h5f["1.1/technique/detector/balor"] del tomo_config["flipping"] tomo_config["flipping"] = numpy.array(soft_flip) output_file = os.path.join(tmp_path, "my_nx.nx") assert not os.path.exists(output_file) # convert model = H52nxModel( input_file=dataset_id16a, output_file=output_file, mechanical_lr_flip=mechanical_flip[0], mechanical_ud_flip=mechanical_flip[1], single_file=True, ) # check transformations matrix from_h5_to_nx(configuration=model) assert os.path.exists(output_file) nxtomo = NXtomo().load(file_path=output_file, data_path="entry0000") transformations = nxtomo.instrument.detector.transformations.transformations final_lr_flip = soft_flip[0] ^ mechanical_flip[0] final_ud_flip = soft_flip[1] ^ mechanical_flip[1] lr_flip_mat = DetXFlipTransformation(flip=final_ud_flip) ud_flip_mat = DetYFlipTransformation(flip=final_lr_flip) assert lr_flip_mat in transformations assert ud_flip_mat in transformations nxtomomill-v2.0.1/nxtomomill/converter/hdf5/tests/test_h52nx_utils.py000066400000000000000000000037141511430602400261010ustar00rootroot00000000000000from nxtomomill.converter.hdf5.utils import ( get_default_output_file, PROCESSED_DATA_DIR_NAME, RAW_DATA_DIR_NAME, ) def test_get_default_output_file(): """ test the get_default_output_file function """ # 1. simple test assert get_default_output_file("/my_tmp/path/file.h5") == "/my_tmp/path/file.nx" assert ( get_default_output_file(f"/my_tmp/{RAW_DATA_DIR_NAME}/file.h5") == f"/my_tmp/{PROCESSED_DATA_DIR_NAME}/file.nx" ) assert ( get_default_output_file(f"/my_tmp/path/{RAW_DATA_DIR_NAME}/toto/file.h5") == f"/my_tmp/path/{PROCESSED_DATA_DIR_NAME}/toto/file.nx" ) # note: _RAW_DATA_DIR_NAME part of the path but not a folder assert ( get_default_output_file(f"/my_tmp/path_{RAW_DATA_DIR_NAME}/toto/file.h5") == f"/my_tmp/path_{RAW_DATA_DIR_NAME}/toto/file.nx" ) # 2. advance test # 2.1 use case: '_RAW_DATA_DIR_NAME' is present twice in the path -> replace the deeper one assert ( get_default_output_file( f"/my_tmp/{RAW_DATA_DIR_NAME}/path/{RAW_DATA_DIR_NAME}/toto/file.h5" ) == f"/my_tmp/{RAW_DATA_DIR_NAME}/path/{PROCESSED_DATA_DIR_NAME}/toto/file.nx" ) # 2.2 use case: contains both '_RAW_DATA_DIR_NAME' and '_PROCESSED_DATA_DIR_NAME' in the path assert ( get_default_output_file( f"/my_tmp/{RAW_DATA_DIR_NAME}/path/{PROCESSED_DATA_DIR_NAME}/toto/file.h5" ) == f"/my_tmp/{RAW_DATA_DIR_NAME}/path/{PROCESSED_DATA_DIR_NAME}/toto/file.nx" ) assert ( get_default_output_file( f"/my_tmp/{PROCESSED_DATA_DIR_NAME}/path/{RAW_DATA_DIR_NAME}/toto/file.h5" ) == f"/my_tmp/{PROCESSED_DATA_DIR_NAME}/path/{PROCESSED_DATA_DIR_NAME}/toto/file.nx" ) # 2.3 use case: expected output file is the input file. Make sure append '_nxtomo' assert ( get_default_output_file("/my_tmp/path/file.nx") == "/my_tmp/path/file_nxtomo.nx" ) nxtomomill-v2.0.1/nxtomomill/converter/hdf5/tests/test_hdf5converter.py000066400000000000000000001425171511430602400265000ustar00rootroot00000000000000# coding: utf-8 import os import sys import tempfile import h5py import numpy import pint from tomoscan.esrf.scan.utils import cwd_context from nxtomomill import converter from nxtomomill.tests.datasets import GitlabDataset from nxtomomill.converter.hdf5.acquisition.utils import ( get_nx_detectors, guess_nx_detector, ) from nxtomomill.io.config import TomoHDF5Config from nxtomomill.tests.utils.bliss import _BlissSample from nxtomomill.utils.hdf5 import DatasetReader, EntryReader from nxtomomill.converter.hdf5.utils import ( PROCESSED_DATA_DIR_NAME, RAW_DATA_DIR_NAME, get_default_output_file, ) from nxtomomill.models.h52nx import H52nxModel from tomoscan.io import HDF5File, get_swmr_mode try: from tomoscan.esrf.scan.hdf5scan import NXtomoScan except ImportError: from tomoscan.esrf.hdf5scan import NXtomoScan import subprocess # nosec B404 from glob import glob import pytest from silx.io.url import DataUrl from silx.io.utils import get_data from tomoscan.validator import is_valid_for_reconstruction from nxtomo.application.nxtomo import NXtomo from nxtomo.nxobject.nxdetector import ImageKey from nxtomomill.models.h52nx.FrameGroup import FrameGroup from nxtomomill.tests.utils.bliss import MockBlissAcquisition _ureg = pint.get_application_registry() def url_has_been_copied(file_path: str, url: DataUrl): """util function to parse the `duplicate_data` folder and insure the copy of the dataset has been done""" duplicate_data_url = DataUrl( file_path=file_path, data_path="/duplicate_data", scheme="silx" ) url_path = url.path() with EntryReader(duplicate_data_url) as duplicate_data_node: for _, dataset in duplicate_data_node.items(): if "original_url" in dataset.attrs: original_url = dataset.attrs["original_url"] # the full dataset is registered in the attributes. # Here we only check the scan entry name if original_url.startswith(url_path): return True return False def test_simple_converter_with_nx_detector_attr(tmp_path): """ Test a simple conversion when NX_class is defined """ folder = tmp_path / "output_test_simple_converter_with_nx_detector_attr" folder.mkdir() config = TomoHDF5Config() config.no_master_file = False bliss_mock = MockBlissAcquisition( n_sample=2, n_sequence=1, n_scan_per_sequence=10, n_darks=5, n_flats=5, with_nx_detector_attr=True, output_dir=folder, detector_name="pcolinux", create_tomo_config=False, ) for sample in bliss_mock.samples: assert os.path.exists(sample.sample_file) config.output_file = sample.sample_file.replace(".h5", ".nx") config.input_file = sample.sample_file config.raises_error = True config.sample_x_keys = ("sx",) config.sample_y_keys = ("sy",) assert len(converter.get_bliss_tomo_entries(sample.sample_file, config)) == 1 converter.from_h5_to_nx( configuration=config, ) # insure only one file is generated assert os.path.exists(config.output_file) # insure data is here with HDF5File(config.output_file, mode="r", swmr=get_swmr_mode()) as h5s: for _, entry_node in h5s.items(): assert "instrument/detector/data" in entry_node dataset = entry_node["instrument/detector/data"] # check virtual dataset are relative and valid assert dataset.is_virtual for vs in dataset.virtual_sources(): assert not os.path.isabs(vs.file_name) # insure connection is valid. There is no # 'VirtualSource.is_valid' like function assert not (dataset[()].min() == 0 and dataset[()].max() == 0) instrument_grp = entry_node.require_group("instrument") assert "beam" in instrument_grp def test_invalid_tomo_n(tmp_path): """Test translation fails if no detector can be found""" folder = tmp_path / "output_test_test_invalid_tomo_n" folder.mkdir() config = TomoHDF5Config() bliss_mock = MockBlissAcquisition( n_sample=1, n_sequence=1, n_scan_per_sequence=10, n_darks=5, n_flats=5, with_nx_detector_attr=True, output_dir=folder, create_tomo_config=False, ) assert len(bliss_mock.samples) == 1 sample = bliss_mock.samples[0] assert os.path.exists(sample.sample_file) output_file = sample.sample_file.replace(".h5", ".nx") # rewrite tomo_n with HDF5File(sample.sample_file, mode="a") as h5s: for _, entry_node in h5s.items(): if "technique/scan/tomo_n" in entry_node: del entry_node["technique/scan/tomo_n"] entry_node["technique/scan/tomo_n"] = 9999 with pytest.raises(ValueError): config.input_file = sample.sample_file config.output_file = output_file config.single_file = True config.no_input = True config.raises_error = True converter.from_h5_to_nx(configuration=config) @pytest.mark.parametrize("create_tomo_config", (True, False)) def test_simple_converter_without_nx_detector_attr(tmp_path, create_tomo_config: bool): """ Test a simple conversion when no NX_class is defined """ folder = tmp_path / "output_test_simple_converter_without_nx_detector_attr" folder.mkdir() config = TomoHDF5Config() bliss_mock = MockBlissAcquisition( n_sample=1, n_sequence=3, n_scan_per_sequence=10, n_darks=5, n_flats=5, with_nx_detector_attr=True, output_dir=folder, detector_name="tata_detector", create_tomo_config=create_tomo_config, ) assert len(bliss_mock.samples) == 1 sample = bliss_mock.samples[0] assert os.path.exists(sample.sample_file) output_file = sample.sample_file.replace(".h5", ".nx") config.input_file = sample.sample_file config.output_file = output_file config.single_file = True config.no_input = True converter.from_h5_to_nx(configuration=config) # insure only one file is generated assert os.path.exists(output_file) # insure data is here with HDF5File(output_file, mode="r", swmr=get_swmr_mode()) as h5s: for _, entry_node in h5s.items(): assert "instrument/detector/data" in entry_node dataset = entry_node["instrument/detector/data"] # check virtual dataset are relative and valid assert dataset.is_virtual for vs in dataset.virtual_sources(): assert not os.path.isabs(vs.file_name) # insure connection is valid. There is no # 'VirtualSource.is_valid' like function assert not (dataset[()].min() == 0 and dataset[()].max() == 0) # check NXdata group assert "data/data" in entry_node assert not ( entry_node["data/data"][()].min() == 0 and entry_node["data/data"][()].max() == 0 ) assert "data/rotation_angle" in entry_node assert "data/image_key" in entry_node def test_providing_existing_camera_name(tmp_path): """Test that detector can be provided to the h5_to_nx function and using linux wildcard""" folder = tmp_path / "output_test_providing_existing_camera_name" folder.mkdir() config = TomoHDF5Config() bliss_mock = MockBlissAcquisition( n_sample=1, n_sequence=3, n_scan_per_sequence=10, n_darks=5, n_flats=5, with_nx_detector_attr=True, output_dir=folder, detector_name="my_detector", create_tomo_config=False, ) assert len(bliss_mock.samples) == 1 sample = bliss_mock.samples[0] assert os.path.exists(sample.sample_file) config.output_file = sample.sample_file.replace(".h5", ".nx") config.valid_camera_names = ("my_detec*",) config.input_file = sample.sample_file config.single_file = True config.no_input = True config.raises_error = True config.rotation_angle_keys = ("hrsrot",) config.sample_x_keys = ("sx",) config.sample_y_keys = ("sy",) converter.from_h5_to_nx(configuration=config) # insure only one file is generated assert os.path.exists(config.output_file) # insure data is here with HDF5File(config.output_file, mode="r", swmr=get_swmr_mode()) as h5s: for _, entry_node in h5s.items(): assert "instrument/detector/data" in entry_node dataset = entry_node["instrument/detector/data"] # check virtual dataset are relative and valid assert dataset.is_virtual for vs in dataset.virtual_sources(): assert not os.path.isabs(vs.file_name) # insure connection is valid. There is no # 'VirtualSource.is_valid' like function assert not (dataset[()].min() == 0 and dataset[()].max() == 0) def test_providing_non_existing_camera_name_no_tomo_config(tmp_path): """Test translation fails if no detector can be found""" folder = tmp_path / "output_test_providing_non_existing_camera_name_no_tomo_config" folder.mkdir() config = TomoHDF5Config() bliss_mock = MockBlissAcquisition( n_sample=1, n_sequence=3, n_scan_per_sequence=10, n_darks=5, n_flats=5, with_nx_detector_attr=True, output_dir=folder, detector_name="toto_detector", create_tomo_config=False, z_series_v_3_options={ "dark_at_start": True, "flat_at_start": True, "dark_at_end": False, "flat_at_end": False, }, ) assert len(bliss_mock.samples) == 1 sample = bliss_mock.samples[0] assert os.path.exists(sample.sample_file) config.input_file = sample.sample_file config.output_file = sample.sample_file.replace(".h5", ".nx") config.valid_camera_names = ("my_detec",) config.raises_error = True with pytest.raises(ValueError): converter.from_h5_to_nx(configuration=config) @pytest.mark.parametrize("z_series_version", ("z-series-v1", "z-series-v3")) def test_z_series_conversion_no_tomo_config(tmp_path, z_series_version: str): """Test conversion of a zseries bliss (mock) acquisition""" folder = tmp_path / "output_test_z_series_conversion_no_tomo_config" folder.mkdir() config = TomoHDF5Config() bliss_mock = MockBlissAcquisition( n_sample=1, n_sequence=1, n_scan_per_sequence=10, n_darks=5, n_flats=5, with_nx_detector_attr=True, output_dir=folder, detector_name="frelon1", acqui_type=z_series_version, z_values=(1, 2, 3), create_tomo_config=False, z_series_v_3_options={ "dark_at_start": True, "flat_at_start": True, "dark_at_end": False, "flat_at_end": False, }, ) assert len(bliss_mock.samples) == 1 sample = bliss_mock.samples[0] assert os.path.exists(sample.sample_file) config.input_file = sample.sample_file config.output_file = sample.sample_file.replace(".h5", ".nx") res = converter.from_h5_to_nx(configuration=config) # insure the 3 files are generated: one per z files = glob(os.path.dirname(sample.sample_file) + "/*.nx") assert len(files) == 3 # try to create NXtomoScan from those to insure this is valid # and check z values for example for res_tuple in res: scan = NXtomoScan(scan=res_tuple[0], entry=res_tuple[1]) if hasattr(scan, "translation_z"): assert scan.translation_z is not None assert is_valid_for_reconstruction(scan) @pytest.mark.parametrize("z_series_version", ("z-series-v1", "z-series-v3")) def test_z_series_conversion(tmp_path, z_series_version): """Test conversion of a zseries bliss (mock) acquisition""" folder = tmp_path / "output_test_z_series_conversion" folder.mkdir() config = TomoHDF5Config() bliss_mock = MockBlissAcquisition( n_sample=1, n_sequence=1, n_scan_per_sequence=10, n_darks=5, n_flats=5, with_nx_detector_attr=True, output_dir=folder, detector_name="frelon1", acqui_type=z_series_version, z_values=(1, 2, 3), create_tomo_config=True, ebs_tomo_version="2.1.0", z_series_v_3_options={ "dark_at_start": True, "flat_at_start": True, "dark_at_end": False, "flat_at_end": False, }, ) assert len(bliss_mock.samples) == 1 sample = bliss_mock.samples[0] assert os.path.exists(sample.sample_file) config.input_file = sample.sample_file config.output_file = sample.sample_file.replace(".h5", ".nx") res = converter.from_h5_to_nx(configuration=config) # insure the 3 files are generated: one per z files = glob(os.path.dirname(sample.sample_file) + "/*.nx") assert len(files) == 3 # try to create NXtomoScan from those to insure this is valid # and check z values for example for res_tuple in res: scan = NXtomoScan(scan=res_tuple[0], entry=res_tuple[1]) if hasattr(scan, "translation_z"): assert scan.translation_z is not None assert is_valid_for_reconstruction(scan) def test_ignore_sub_entries(tmp_path): """ Test we can ignore some sub entries """ folder = tmp_path / "output_test_ignore_sub_entries" folder.mkdir() config = TomoHDF5Config() from nxtomomill.tests.utils.bliss import MockBlissAcquisition bliss_mock = MockBlissAcquisition( n_sample=1, n_sequence=1, n_scan_per_sequence=10, n_darks=0, n_flats=0, with_nx_detector_attr=True, output_dir=folder, detector_name="pcolinux", ) for sample in bliss_mock.samples: assert os.path.exists(sample.sample_file) config.output_file = sample.sample_file.replace(".h5", ".nx") config.input_file = sample.sample_file config.single_file = True config.sub_entries_to_ignore = ("6.1", "7.1") config.no_input = True converter.from_h5_to_nx(configuration=config) # insure only one file is generated assert os.path.exists(config.output_file) # insure data is here with HDF5File(config.output_file, mode="r", swmr=get_swmr_mode()) as h5s: for _, entry_node in h5s.items(): assert "instrument/detector/data" in entry_node dataset = entry_node["instrument/detector/data"] # check virtual dataset are relative and valid assert dataset.is_virtual assert dataset.shape == ( 10 * (10 - len(config.sub_entries_to_ignore)), 64, 64, ) for vs in dataset.virtual_sources(): assert not os.path.isabs(vs.file_name) # insure connection is valid. There is no # 'VirtualSource.is_valid' like function assert not (dataset[()].min() == 0 and dataset[()].max() == 0) def create_nx_detector(node: h5py.Group, name, with_nx_class): det_node = node.require_group(name) data = numpy.random.random(10 * 10 * 10).reshape(10, 10, 10) det_node["data"] = data if with_nx_class: if "NX_class" not in det_node.attrs: det_node.attrs["NX_class"] = "NXdetector" return def test_get_nx_detectors(tmp_path): """test get_nx_detectors function""" folder = tmp_path / "output_test_get_nx_detectors" folder.mkdir() h5file = os.path.join(folder, "h5file.hdf5") with HDF5File(h5file, mode="w") as h5s: create_nx_detector(node=h5s, name="det1", with_nx_class=True) create_nx_detector(node=h5s, name="det2", with_nx_class=False) with HDF5File(h5file, mode="r", swmr=get_swmr_mode()) as h5s: dets = get_nx_detectors(h5s) assert len(dets) == 1 assert dets[0].name == "/det1" assert len(guess_nx_detector(h5s)) == 2 with HDF5File(h5file, mode="a") as h5s: create_nx_detector(node=h5s, name="det3", with_nx_class=True) create_nx_detector(node=h5s, name="det4", with_nx_class=True) with HDF5File(h5file, mode="r", swmr=get_swmr_mode()) as h5s: dets = get_nx_detectors(h5s) assert len(dets) == 3 def test_guess_nx_detector(tmp_path): """test guess_nx_detector function""" folder = tmp_path / "output_test_guess_nx_detector" folder.mkdir() h5file = os.path.join(folder, "h5file.hdf5") with HDF5File(h5file, mode="w") as h5s: create_nx_detector(node=h5s, name="det2", with_nx_class=False) with HDF5File(h5file, mode="r", swmr=get_swmr_mode()) as h5s: dets = get_nx_detectors(h5s) assert len(dets) == 0 dets = guess_nx_detector(h5s) assert dets[0].name == "/det2" with HDF5File(h5file, mode="w") as h5s: create_nx_detector(node=h5s, name="det3", with_nx_class=False) create_nx_detector(node=h5s, name="det4", with_nx_class=True) with HDF5File(h5file, mode="a") as h5s: dets = guess_nx_detector(h5s) assert len(dets) == 2 def create_scan(n_projection_scans, n_flats, n_darks, output_dir, frame_data_type): """ :param int n_projection_scans: number of scans beeing projections :param int n_flats: number of frame per flats :param int n_darks: number of frame per dark """ bliss_mock = MockBlissAcquisition( n_sample=1, n_sequence=1, n_scan_per_sequence=n_projection_scans, n_darks=n_darks, n_flats=n_flats, with_nx_detector_attr=True, output_dir=output_dir, detector_name="pcolinux", frame_data_type=frame_data_type, ) return bliss_mock.samples[0].sample_file def test_dataset_1(tmp_path): """test a conversion where projections are contained in the input_file. Dark and flats are on a different file""" frame_data_type = numpy.uint16 folder = tmp_path / "output_test_dataset_1" folder.mkdir() config = TomoHDF5Config() config.no_master_file = False config.output_file = os.path.join(folder, "output.nx") config.rotation_angle_keys = ("hrsrot",) config.sample_x_keys = ("sx",) config.sample_y_keys = ("sy",) folder_1 = os.path.join(folder, "acqui_1") input_file = create_scan( n_projection_scans=6, n_flats=0, n_darks=0, output_dir=folder_1, frame_data_type=frame_data_type, ) folder_2 = os.path.join(folder, "acqui_2") dark_flat_file = create_scan( n_projection_scans=0, n_flats=1, n_darks=1, output_dir=folder_2, frame_data_type=frame_data_type, ) config.input_file = input_file # we want to take two scan of projections from the input file: 5.1 # and 6.1. As the input file is provided we don't need to # specify it config.data_scans = ( FrameGroup(frame_type="proj", url=DataUrl(data_path="/5.1", scheme="silx")), FrameGroup(frame_type="proj", url=DataUrl(data_path="/6.1", scheme="silx")), FrameGroup( frame_type="flat", url=DataUrl(file_path=dark_flat_file, data_path="/2.1", scheme="silx"), ), FrameGroup( frame_type="dark", url=DataUrl(file_path=dark_flat_file, data_path="/3.1", scheme="silx"), ), ) converter.from_h5_to_nx( configuration=config, ) assert os.path.exists(config.output_file), "output file does not exists" with HDF5File(config.output_file, mode="r", swmr=get_swmr_mode()) as h5s: assert len(h5s.items()) == 1 assert "entry0000" in h5s scan = NXtomoScan(scan=config.output_file, entry="entry0000") assert is_valid_for_reconstruction(scan) # check the `data`has been created assert len(scan.projections) == 20 assert len(scan.darks) == 10 # check data is a virtual dataset with HDF5File(config.output_file, mode="r", swmr=get_swmr_mode()) as h5f: dataset = h5f["entry0000/instrument/detector/data"] assert dataset.is_virtual assert dataset.dtype == frame_data_type # check the `data` virtual dataset is valid # if the link fail then all values are zeros url = tuple(scan.projections.values())[0] proj_data = get_data(url) assert proj_data.min() != proj_data.max() url = tuple(scan.darks.values())[0] dark_data = get_data(url) assert dark_data.min() != dark_data.max() assert len(scan.flats) == 10 url = tuple(scan.flats.values())[0] flat_data = get_data(url) assert flat_data.min() != flat_data.max() def test_dataset_2(tmp_path): """test a conversion where no input file is provided and where we have 2 projections in a file, 3 in an other. Flat and darks are also in another file. No flat provided. """ folder = tmp_path / "output_test_dataset_2" folder.mkdir() frame_data_type = numpy.uint16 config = TomoHDF5Config() config.no_master_file = False config.output_file = os.path.join(folder, "output.nx") config.rotation_angle_keys = ("hrsrot",) config.sample_x_keys = ("sx",) config.sample_y_keys = ("sy",) folder_1 = os.path.join(folder, "acqui_1") file_1 = create_scan( n_projection_scans=6, n_flats=0, n_darks=0, output_dir=folder_1, frame_data_type=frame_data_type, ) folder_2 = os.path.join(folder, "acqui_2") file_2 = create_scan( n_projection_scans=6, n_flats=0, n_darks=0, output_dir=folder_2, frame_data_type=frame_data_type, ) folder_3 = os.path.join(folder, "acqui_3") file_3 = create_scan( n_projection_scans=0, n_flats=0, n_darks=1, output_dir=folder_3, frame_data_type=frame_data_type, ) folder_4 = os.path.join(folder, "acqui_4") file_4 = create_scan( n_projection_scans=0, n_flats=1, n_darks=0, output_dir=folder_4, frame_data_type=frame_data_type, ) # we want to take two scan of projections from the input file: 5.1 # and 6.1. As the input file is provided we don't need to # specify it dark_url_1 = DataUrl(file_path=file_3, data_path="/2.1", scheme="silx") flat_url_1 = DataUrl(file_path=file_4, data_path="/2.1", scheme="silx") proj_url_1 = DataUrl(file_path=file_1, data_path="/5.1", scheme="silx") proj_url_2 = DataUrl(file_path=file_1, data_path="/6.1", scheme="silx") proj_url_3 = DataUrl(file_path=file_2, data_path="/4.1", scheme="silx") proj_url_4 = DataUrl(file_path=file_2, data_path="/2.1", scheme="silx") config.default_data_copy = True config.data_scans = ( FrameGroup(frame_type="dark", url=dark_url_1), FrameGroup(frame_type="flat", url=flat_url_1), FrameGroup(frame_type="proj", url=proj_url_1, copy_data=False), FrameGroup(frame_type="proj", url=proj_url_2, copy_data=False), FrameGroup(frame_type="proj", url=proj_url_3), FrameGroup(frame_type="proj", url=proj_url_4), ) urls_copied = (dark_url_1, flat_url_1, proj_url_3, proj_url_4) urls_not_copied = (proj_url_1, proj_url_2) config.raises_error = True converter.from_h5_to_nx( configuration=config, ) assert os.path.exists(config.output_file) with HDF5File(config.output_file, mode="r", swmr=get_swmr_mode()) as h5s: assert "entry0000" in h5s assert len(h5s.items()) == 1 with HDF5File( config.output_file.replace(".nx", "_0000.nx"), mode="r", swmr=get_swmr_mode(), ) as h5s: assert "entry0000" in h5s assert "duplicate_data" in h5s assert len(h5s.items()) == 2 detector_url = DataUrl( file_path=config.output_file, data_path="/entry0000/instrument/detector/data", scheme="silx", ) with DatasetReader(detector_url) as detector_dataset: assert detector_dataset.is_virtual for i_vs, vs in enumerate(detector_dataset.virtual_sources()): assert not os.path.isabs(vs.file_name) if i_vs in (0, 1, 4, 5): assert vs.file_name == "." else: assert vs.file_name == "./acqui_1/sample_0/sample_0.h5" # FIXME: avoid keeping some file open. not clear why this is needed detector_dataset = None scan = NXtomoScan(scan=config.output_file, entry="entry0000") assert is_valid_for_reconstruction(scan) # check the `data`has been created assert len(scan.projections) == 40 assert len(scan.darks) == 10 # check the `data` virtual dataset is valid # if the link fail then all values are zeros url = tuple(scan.projections.values())[0] proj_data = get_data(url) assert proj_data.min() != proj_data.max() url = tuple(scan.darks.values())[0] dark_data = get_data(url) assert dark_data.min() != dark_data.max() assert len(scan.flats) == 10 url = tuple(scan.flats.values())[0] flat_data = get_data(url) assert flat_data.min() != flat_data.max() with HDF5File(config.output_file, mode="r", swmr=get_swmr_mode()) as h5f: dataset = h5f["entry0000/instrument/detector/data"][()] assert dataset.shape[0] == 60 with EntryReader(dark_url_1) as dark_entry: numpy.testing.assert_array_equal( dark_entry["instrument/pcolinux/data"], dataset[0:10] ) with EntryReader(flat_url_1) as flat_entry: numpy.testing.assert_array_equal( flat_entry["instrument/pcolinux/data"], dataset[10:20] ) with EntryReader(proj_url_1) as proj_entry_1: numpy.testing.assert_array_equal( proj_entry_1["instrument/pcolinux/data"], dataset[20:30] ) with EntryReader(proj_url_2) as proj_entry_2: numpy.testing.assert_array_equal( proj_entry_2["instrument/pcolinux/data"], dataset[30:40] ) with EntryReader(proj_url_3) as proj_entry_3: numpy.testing.assert_array_equal( proj_entry_3["instrument/pcolinux/data"], dataset[40:50] ) with EntryReader(proj_url_4) as proj_entry_4: numpy.testing.assert_array_equal( proj_entry_4["instrument/pcolinux/data"], dataset[50:60] ) for url in urls_copied: assert url_has_been_copied( file_path=config.output_file.replace(".nx", "_0000.nx"), url=url, ) for url in urls_not_copied: assert not url_has_been_copied( file_path=config.output_file.replace(".nx", "_0000.nx"), url=url, ) # test with some extra parameters config.energy_kev = 12.2 config.x_sample_pixel_size_m = 2.6 * 10e-6 config.y_sample_pixel_size_m = 2.7 * 10e-6 config.overwrite = True config.field_of_view = "Half" init_url_1 = DataUrl(file_path=file_1, data_path="/1.1", scheme="silx") config.default_data_copy = True config.data_scans = ( FrameGroup(frame_type="init", url=init_url_1), FrameGroup(frame_type="dark", url=dark_url_1), FrameGroup(frame_type="flat", url=flat_url_1), FrameGroup(frame_type="proj", url=proj_url_1, copy=False), FrameGroup(frame_type="proj", url=proj_url_2, copy=False), FrameGroup(frame_type="proj", url=proj_url_3), FrameGroup(frame_type="proj", url=proj_url_4), ) converter.from_h5_to_nx( configuration=config, ) scan.clear_cache() energy = scan.energy assert numpy.isclose(energy, 12.2) assert scan.sample_x_pixel_size is not None assert numpy.isclose(scan.sample_x_pixel_size, 2.6 * 10e-6) assert scan.sample_y_pixel_size is not None assert numpy.isclose(scan.sample_y_pixel_size, 2.7 * 10e-6) with EntryReader( DataUrl(file_path=scan.master_file, data_path=scan.entry, scheme="h5py") ) as entry: assert "instrument/detector" in entry assert "instrument/diode" not in entry @pytest.mark.parametrize("z_series_version", ("z-series-v1",)) def test_z_series_conversion_with_external_urls(tmp_path, z_series_version: str): """ test conversion of a z-series using configuration """ folder = tmp_path / "test_z_series_conversion_with_external_urls" folder = tempfile.mkdtemp() frame_data_type = numpy.uint64 config = TomoHDF5Config() config.output_file = os.path.join(folder, "output.nx") # dataset init camera_name = "frelon" bliss_mock = MockBlissAcquisition( n_sample=1, n_sequence=1, n_scan_per_sequence=2, n_darks=1, n_flats=0, with_nx_detector_attr=True, output_dir=os.path.join(folder, "seq_1"), detector_name=camera_name, acqui_type=z_series_version, z_values=(1, 2, 3), frame_data_type=frame_data_type, ) zseries_1_file = bliss_mock.samples[0].sample_file bliss_mock = MockBlissAcquisition( n_sample=1, n_sequence=1, n_scan_per_sequence=2, n_darks=0, n_flats=1, with_nx_detector_attr=True, output_dir=os.path.join(folder, "seq_2"), detector_name=camera_name, acqui_type=z_series_version, z_values=(4, 5, 6), frame_data_type=frame_data_type, ) zseries_2_file = bliss_mock.samples[0].sample_file dark_url_1 = DataUrl(file_path=zseries_1_file, data_path="/5.1", scheme="silx") proj_url_1 = DataUrl(file_path=zseries_1_file, data_path="/6.1", scheme="silx") proj_url_2 = DataUrl(file_path=zseries_1_file, data_path="/7.1", scheme="silx") proj_url_3 = DataUrl(file_path=zseries_1_file, data_path="/9.1", scheme="silx") proj_url_4 = DataUrl(file_path=zseries_1_file, data_path="/10.1", scheme="silx") proj_url_5 = DataUrl(file_path=zseries_2_file, data_path="/3.1", scheme="silx") proj_url_6 = DataUrl(file_path=zseries_2_file, data_path="/4.1", scheme="silx") flat_url_1 = DataUrl(file_path=zseries_2_file, data_path="/2.1", scheme="silx") config.default_data_copy = True config.single_file = True config.data_scans = ( FrameGroup(frame_type="dark", url=dark_url_1, copy_data=False), FrameGroup(frame_type="flat", url=flat_url_1, copy_data=False), FrameGroup(frame_type="proj", url=proj_url_1), FrameGroup(frame_type="proj", url=proj_url_2), FrameGroup(frame_type="proj", url=proj_url_3), FrameGroup(frame_type="proj", url=proj_url_4), FrameGroup(frame_type="proj", url=proj_url_5), FrameGroup(frame_type="proj", url=proj_url_6), ) urls_copied = ( proj_url_1, proj_url_2, proj_url_3, proj_url_4, proj_url_5, proj_url_6, ) urls_not_copied = (flat_url_1, dark_url_1) # do conversion new_scans = converter.from_h5_to_nx( configuration=config, ) assert len(new_scans) == 3 assert os.path.exists(config.output_file) with HDF5File(config.output_file, mode="r", swmr=get_swmr_mode()) as h5f: assert "entry0000" in h5f assert "entry0001" in h5f assert "entry0002" in h5f scan_z0 = NXtomoScan(scan=config.output_file, entry="entry0000") scan_z1 = NXtomoScan(scan=config.output_file, entry="entry0001") scan_z2 = NXtomoScan(scan=config.output_file, entry="entry0002") # check the `data`has been created assert len(scan_z0.projections) == 20 assert len(scan_z1.projections) == 20 assert len(scan_z2.projections) == 20 for url in urls_copied: assert url_has_been_copied(file_path=config.output_file, url=url) for url in urls_not_copied: assert not url_has_been_copied(file_path=config.output_file, url=url) # test a few slices with HDF5File(config.output_file, mode="r", swmr=get_swmr_mode()) as h5f: dataset = h5f["entry0000/instrument/detector/data"] assert dataset.shape[0] == 30 with EntryReader(proj_url_1) as proj_entry_1: numpy.testing.assert_array_equal( proj_entry_1["instrument/frelon/data"], dataset[10:20] ) with EntryReader(proj_url_2) as proj_entry_2: numpy.testing.assert_array_equal( proj_entry_2["instrument/frelon/data"], dataset[20:30] ) assert dataset.dtype == frame_data_type @pytest.mark.parametrize("z_series_version", ("z-series-v1", "z-series-v3")) @pytest.mark.parametrize( "dark_flat_config", ( { "dark_at_start": True, "flat_at_start": True, "dark_at_end": False, "flat_at_end": False, }, { "dark_at_start": False, "flat_at_start": False, "dark_at_end": True, "flat_at_end": True, }, ), ) def test_z_series_dark_flat_copy(tmp_path, z_series_version: str, dark_flat_config): """In z-series version 3""" folder = tmp_path / "h52nx_from_command_line" folder.mkdir() bliss_mock = MockBlissAcquisition( n_sample=1, n_sequence=1, n_scan_per_sequence=2, n_darks=1, n_flats=1, acqui_type=z_series_version, with_nx_detector_attr=True, output_dir=folder, detector_name="pcolinux", frame_data_type=numpy.uint32, z_values=(0.0, 1.0, 2.0), z_series_v_3_options=dark_flat_config, ) # launch conversion sample = bliss_mock.samples[0] input_file = sample.sample_file assert os.path.exists(input_file) config = TomoHDF5Config() config.output_file = get_default_output_file(sample.sample_file) config.valid_camera_names = ("pcolinux",) config.input_file = sample.sample_file config.single_file = True config.no_input = True config.raises_error = True config.rotation_angle_keys = ("hrsrot",) new_entries = converter.from_h5_to_nx(configuration=config) # insure only one file is generated assert os.path.exists(config.output_file) # insure data is here for i_nx_tomo, (file_path, data_path) in enumerate(new_entries): nx_tomo = NXtomo().load(file_path=file_path, data_path=data_path) assert ( len( numpy.where( nx_tomo.instrument.detector.image_key_control == ImageKey.DARK_FIELD )[0] ) == 10 ), f"NXtomo {i_nx_tomo} doesn't have the expected number of dark" assert ( len( numpy.where( nx_tomo.instrument.detector.image_key_control == ImageKey.FLAT_FIELD )[0] ) == 10 ), f"NXtomo {i_nx_tomo} doesn't have the expected number of flat" assert ( len( numpy.where( nx_tomo.instrument.detector.image_key_control == ImageKey.PROJECTION )[0] ) == 20 ), f"NXtomo {i_nx_tomo} doesn't have the expected number of projection" def test_h52nx_from_command_line(tmp_path): folder = tmp_path / "h52nx_from_command_line" folder.mkdir() bliss_mock = MockBlissAcquisition( n_sample=1, n_sequence=1, n_scan_per_sequence=2, n_darks=1, n_flats=1, with_nx_detector_attr=True, output_dir=folder, detector_name="pcolinux", frame_data_type=numpy.uint32, ) sample = bliss_mock.samples[0] output_file = sample.sample_file.replace(".h5", ".nx") assert os.path.exists(sample.sample_file) with cwd_context(os.path.dirname(sample.sample_file)): input_file = os.path.basename(sample.sample_file) assert os.path.exists(input_file) output_file = os.path.basename(output_file) assert not os.path.exists(output_file) cmd = ( sys.executable, "-m", "nxtomomill", "h52nx", input_file, output_file, "--copy-data", "--raises-error", "--single-file", ) subprocess.call(cmd, cwd=os.path.dirname(sample.sample_file)) # nosec B603 assert os.path.exists(output_file) # insure all link are connected to one file: the internal one frame_dataset_url = DataUrl( file_path=output_file, data_path="/entry0000/instrument/detector/data", scheme="silx", ) with DatasetReader(frame_dataset_url) as dataset: assert dataset.is_virtual for vs_info in dataset.virtual_sources(): assert dataset.is_virtual assert vs_info.file_name == "." assert dataset.dtype == numpy.uint32 # FIXME: avoid keeping some file open. not clear why this is needed dataset = None with HDF5File(output_file, "r", swmr=get_swmr_mode()) as h5f: assert "/entry0000/instrument/diode" not in h5f # insure an nxtomo can be created from it nx_tomo = NXtomo().load(output_file, "entry0000") assert nx_tomo.energy is not None z1 = -52.0 * _ureg.meter z2 = 0.1 * _ureg.meter assert nx_tomo.instrument.detector.distance == z2 assert nx_tomo.instrument.source.distance == z1 propa_dist_read = nx_tomo.sample.propagation_distance.to_base_units().magnitude propa_dist_expected = ((-z1 * z2) / (-z1 + z2)).to_base_units().magnitude assert numpy.isclose( propa_dist_read, propa_dist_expected, ) numpy.testing.assert_array_equal( nx_tomo.instrument.detector.sequence_number, numpy.linspace(0, 40, 40, endpoint=False, dtype=numpy.uint32), ) input_types = ( numpy.uint8, numpy.uint16, numpy.uint32, numpy.uint64, numpy.float16, numpy.float32, numpy.int16, numpy.int32, numpy.int64, ) @pytest.mark.parametrize("rotation_is_clockwise", (True, False)) @pytest.mark.parametrize("input_type", input_types) def test_simple_conversion(input_type, tmp_path, rotation_is_clockwise: bool): """test simple conversion from different frame data type and handling of RAW_DATA with PROCESSED_DATA""" input_path = tmp_path / "test" / RAW_DATA_DIR_NAME / "dataset" os.makedirs(input_path) bliss_mock = MockBlissAcquisition( n_sample=1, n_sequence=1, n_scan_per_sequence=10, n_darks=5, n_flats=5, with_nx_detector_attr=True, output_dir=input_path, detector_name="my_detector", frame_data_type=input_type, rotation_is_clockwise=rotation_is_clockwise, create_tomo_config=True, ) sample = bliss_mock.samples[0] assert os.path.exists(sample.sample_file) config = TomoHDF5Config() config.output_file = get_default_output_file(sample.sample_file) assert PROCESSED_DATA_DIR_NAME in config.output_file config.valid_camera_names = ("my_detec*",) config.input_file = sample.sample_file config.single_file = True config.no_input = True config.raises_error = True config.rotation_angle_keys = ("hrsrot",) converter.from_h5_to_nx(configuration=config) # insure only one file is generated assert os.path.exists(config.output_file) # insure data is here with HDF5File(config.output_file, mode="r", swmr=get_swmr_mode()) as h5s: for _, entry_node in h5s.items(): assert "instrument/detector/data" in entry_node dataset = entry_node["instrument/detector/data"] assert dataset.dtype == input_type assert "control" not in entry_node if rotation_is_clockwise: assert numpy.all( entry_node["data/rotation_angle"][()] <= 0.0 ), "tomoconfig 'rotation_is_clockwise' doesn't seems to be properly read" else: assert numpy.all( entry_node["data/rotation_angle"][()] >= 0.0 ), "tomoconfig 'rotation_is_clockwise' doesn't seems to be properly read" def test_machine_current(): """Test machine current is handle by the convertor""" with tempfile.TemporaryDirectory() as root_dir: bliss_mock = MockBlissAcquisition( n_sample=1, n_sequence=1, n_scan_per_sequence=2, n_darks=5, n_flats=5, with_nx_detector_attr=True, output_dir=root_dir, detector_name="my_detector", ) sample = bliss_mock.samples[0] assert os.path.exists(sample.sample_file) # append current to the bliss file # from the example file I had it looks like this information can be saved at different location # and can be either a number (for dark and flat for example) or a list (for projections) with HDF5File(sample.sample_file, mode="a") as h5f: # overwrite start_time to made ordering work del h5f["1.1"]["start_time"] h5f["1.1"]["start_time"] = "2022-01-15T21:05:58.360095+02:00" with HDF5File(sample.sample_file, mode="a") as h5f: node_names = ("2.1", "3.1") machine_current = (602, 589) # those are in ma start_times = ( "2022-01-15T21:07:58.360095+02:00", "2022-01-15T21:07:59.360095+02:00", ) for node_name, machine_current_ma, st in zip( node_names, machine_current, start_times ): h5f[f"{node_name}/instrument/machine/current"] = machine_current_ma h5f[f"{node_name}/instrument/machine/current"].attrs["units"] = str( _ureg.milliampere ) del h5f[node_name]["start_time"] h5f[node_name]["start_time"] = st assert "4.1" in h5f assert "5.1" in h5f assert ( "6.1" not in h5f ) # this is because n_scan_per_sequence == 2 in MockBlissAcquisition # create some X.2 for machine current as this is done in Bliss for node_name in ("4.2", "5.2"): h5f.require_group(node_name)["title"] = _BlissSample.get_title( "projection" ) current_monitor_dataset = h5f.require_dataset( f"{node_name}/measurement/current", shape=(5), dtype=numpy.float32 ) current_monitor_dataset[:] = numpy.linspace( 0.9, 0.96, 5, dtype=numpy.float32, endpoint=True ) # add a nan to make sure this is properly handled current_monitor_dataset[1] = numpy.nan current_monitor_dataset.attrs["units"] = str(_ureg.ampere) # define start_time and end_time to insure conversion is correct # start_time and end_time is required for both: # * from X.1 to create frame time stamp # * from X.2 to get machine current time stamp start_times = ( "2022-01-15T21:08:58.360095+02:00", "2022-01-15T21:10:58.360095+02:00", ) end_times = ( "2022-01-15T21:09:58.360095+02:00", "2022-01-15T21:11:58.360095+02:00", ) tuple_node_names = (["4.1", "4.2"], ["5.1", "5.2"]) for node_names, st, et in zip(tuple_node_names, start_times, end_times): for node_name in node_names: if "start_time" in h5f[node_name]: del h5f[node_name]["start_time"] h5f[node_name]["start_time"] = st h5f[node_name]["end_time"] = et h5f[node_name].require_group("instrument") # convert the file config = TomoHDF5Config() config.output_file = sample.sample_file.replace(".h5", ".nx") config.single_file = True config.no_input = True config.raises_error = True with pytest.raises(ValueError): converter.from_h5_to_nx(configuration=config) config.input_file = sample.sample_file converter.from_h5_to_nx(configuration=config) # insure only one file is generated assert os.path.exists(config.output_file) # insure data is here nx_tomo = NXtomo().load(config.output_file, "entry0000") expected_results = numpy.concatenate( [ [602 / 1000] * 10, [589 / 1000] * 10, numpy.linspace(0.9, 0.96, 10, dtype=numpy.float32), numpy.linspace(0.9, 0.96, 10, dtype=numpy.float32), ] ) n_frames = ( 10 * 4 ) # there is 10 frames per scan. One dark, one flat and two projections scans assert len(nx_tomo.control.data) == n_frames numpy.testing.assert_allclose( nx_tomo.control.data, expected_results, rtol=0.001 ) # test also from tomoscan scan = NXtomoScan(scan=config.output_file, entry="entry0000") # check getting the projections machine current assert ( len( scan.machine_current[ scan.image_key_control == ImageKey.PROJECTION.value ] ) == 20 ) assert ( len( scan.machine_current[ scan.image_key_control == ImageKey.DARK_FIELD.value ] ) == 10 ) assert ( len( scan.machine_current[ scan.image_key_control == ImageKey.FLAT_FIELD.value ] ) == 10 ) files_to_expected_result: dict[str, tuple[str]] = { "h5_datasets/multitomo/id19/20250713/034_oak_1__0001.h5": ( [f"034_oak_1__0001_{str(i).zfill(4)}.nx" for i in range(140)] ), "h5_datasets/holotomo/id16b/20250711/B3P7_sponge_pco2_ht_25nm.h5": ( "B3P7_sponge_pco2_ht_25nm_0000.nx", "B3P7_sponge_pco2_ht_25nm_0001.nx", "B3P7_sponge_pco2_ht_25nm_0002.nx", "B3P7_sponge_pco2_ht_25nm_0003.nx", ), "h5_datasets/holotomo/id16b/20250325/needle_test_alu_17p4_daiquiri_50nm_ht_0004.h5": ( "needle_test_alu_17p4_daiquiri_50nm_ht_0004_0000.nx", "needle_test_alu_17p4_daiquiri_50nm_ht_0004_0001.nx", "needle_test_alu_17p4_daiquiri_50nm_ht_0004_0002.nx", "needle_test_alu_17p4_daiquiri_50nm_ht_0004_0003.nx", ), } @pytest.mark.parametrize( "input_file,expected_result_file_names", files_to_expected_result.items() ) def test_output_file_indexing( input_file: str, expected_result_file_names: tuple[tuple[str, str]], tmp_path ): """test nxtomo indexing policy""" scan_dir = GitlabDataset.get_dataset(os.path.dirname(input_file)) bliss_file = os.path.join(scan_dir, os.path.basename(input_file)) output_file = ( tmp_path / "output" / os.path.basename(input_file).replace(".h5", ".nx") ) config = TomoHDF5Config() config.input_file = bliss_file config.output_file = str(output_file) converter.from_h5_to_nx( configuration=config, ) assert tuple(sorted(os.listdir(tmp_path / "output"))) == tuple( sorted(expected_result_file_names) ) def test_id16a_use_case(tmp_path): """ Dummy test to match the ID16a use case: providing the input and output file. Check that the rotation_is_clockwise works. Check the sample and detector pixel size. """ output_file = tmp_path / "output.nx" config = H52nxModel() config.single_file = True # add noise to the input and output file to make sure they are overwritten config.input_file = "toto" config.output_file = "tata" input_file = GitlabDataset.get_dataset( "h5_datasets/holotomo/id16a/Atomium_S2_holo4_HT_010nm_0001.h5" ) config_file = tmp_path / "config.cfg" config.to_cfg_file(file_path=str(config_file)) cmd = ( sys.executable, "-m", "nxtomomill", "h52nx", input_file, output_file, "--config", config_file, ) subprocess.call(cmd, cwd=tmp_path) # nosec B603 assert os.path.exists(output_file) # check 4 NXtomo exists nxtomos = [ NXtomo().load( file_path=output_file, data_path=f"entry000{i}", ) for i in range(4) ] assert len(nxtomos) == 4 nxtomo_d0 = nxtomos[0] assert len(nxtomo_d0.sample.rotation_angle) == 2064 # check rotation_is_clockwise raw_rot_angles = numpy.concatenate( [ [0.0] * 20, # dark [0.0] * 20, # flat numpy.linspace(0.0, 180.0, 2001, endpoint=True), # proj [180.0, 90.0, 0.0], # alignment proj [0.0] * 20, # flat ] ) numpy.testing.assert_almost_equal( nxtomo_d0.sample.rotation_angle.to("degree").magnitude, raw_rot_angles, decimal=2, ) # check detector pixel size numpy.testing.assert_almost_equal( nxtomo_d0.instrument.detector.x_pixel_size.to(_ureg.micrometer).magnitude, 2.95203, decimal=4, ) numpy.testing.assert_almost_equal( nxtomo_d0.instrument.detector.y_pixel_size.to(_ureg.micrometer).magnitude, 2.95203, decimal=4, ) # check sample pixel size numpy.testing.assert_almost_equal( nxtomo_d0.sample.x_pixel_size.to(_ureg.micrometer).magnitude, 0.00999999, decimal=4, ) numpy.testing.assert_almost_equal( nxtomo_d0.sample.y_pixel_size.to(_ureg.micrometer).magnitude, 0.00999999, decimal=4, ) # check detector flips transformations = { t.axis_name: t for t in nxtomo_d0.instrument.detector.transformations.transformations } for transformation_name in ("rx", "ry"): transformation = transformations[transformation_name] transformation.transformation_values.magnitude == numpy.array([0.0]) nxtomomill-v2.0.1/nxtomomill/converter/hdf5/utils.py000066400000000000000000000067341511430602400226610ustar00rootroot00000000000000# coding: utf-8 """ Utils related to bliss-HDF5 """ from collections import namedtuple import os from pathlib import Path H5FileKeys = namedtuple( "H5FileKeys", [ "acq_expo_time_keys", "rot_angle_keys", "valid_camera_names", "sample_x_keys", "sample_y_keys", "translation_z_keys", "translation_y_keys", "x_sample_pixel_size", "y_sample_pixel_size", "x_detector_pixel_size", "y_detector_pixel_size", "diode_keys", ], ) H5ScanTitles = namedtuple( "H5ScanTitles", [ "init_titles", "zseries_init_titles", "multitomo_init_titles", "back_and_forth_init_titles", "dark_titles", "flat_titles", "projection_titles", "align_titles", ], ) PROCESSED_DATA_DIR_NAME = "PROCESSED_DATA" RAW_DATA_DIR_NAME = "RAW_DATA" def get_default_output_file(input_file: str, output_file_extension: str = ".nx") -> str: """ Policy: look for any 'RAW_DATA' in file directory. If find any (before any 'PROCESSED_DATA' directory) replace it "RAW_DATA". Then replace input_file by the expected file_extension and make sure the output file is different than the input file. Else append _nxtomo to it. :param input_file: file to be converted from bliss to NXtomo :param output_file_extension: :return: default output file according to policy """ if isinstance(input_file, Path): input_file = str(input_file) if not isinstance(input_file, str): raise TypeError( f"input_file is expected to be an instance of str. {type(input_file)} provided" ) if not isinstance(output_file_extension, str): raise TypeError("output_file_extension is expected to be a str") if not output_file_extension.startswith("."): output_file_extension = "." + output_file_extension input_file = os.path.abspath(input_file) input_file_no_ext, _ = os.path.splitext(input_file) def from_raw_data_path_to_process_data_path(file_path: str): split_path = file_path.split(os.sep) # reverse it to find the lower level value of '_RAW_DATA_DIR_NAME' if by any 'chance' has several in the path # in this case this is most likely what we want split_path = split_path[::-1] # check if already contain in a "PROCESSED_DATA" directory try: index_processed_data = split_path.index(PROCESSED_DATA_DIR_NAME) except ValueError: index_processed_data = None try: index_raw_data = split_path.index(RAW_DATA_DIR_NAME) except ValueError: # if the value is not in the list pass else: if index_processed_data is None or index_raw_data < index_processed_data: # make sure we are not already in a 'PROCESSED_DATA' directory. Not sure it will never happen but safer split_path[index_raw_data] = PROCESSED_DATA_DIR_NAME # reorder path to original split_path = split_path[::-1] return os.sep.join(split_path) output_path = from_raw_data_path_to_process_data_path(input_file_no_ext) output_file = output_path + output_file_extension if output_file == input_file: # to be safer if the default output file is the same as the input file (if the input file has a .nx extension and not in any 'RAw_DATA' directory) return output_path + "_nxtomo" + output_file_extension else: return output_file nxtomomill-v2.0.1/nxtomomill/converter/version.py000066400000000000000000000030651511430602400223520ustar00rootroot00000000000000"""module to get NXtomo versionning as it can evolve with time""" LATEST_VERSION = 1.2 CURRENT_OUTPUT_VERSION = LATEST_VERSION def version(): return CURRENT_OUTPUT_VERSION # Information regarding Format # Format 1.0 # NXtomo entry (one per acquisition) # |-> beam # |-> incident energy (optional 0D) # |-> instrument (NXinstrument) # |-> detector (NXdetector) # |-> count_time (optional 1D dataset) # |-> data (mandatory 3D dataset) # |-> distance (optional 0D dataset) # |-> field_of_fiew (optional str) # |-> image_key (madatory 1D dataset) # |-> image_key_control (optional 1D dataset) # |-> x_pixel_size (float) # |-> y_pixel_size (float) # |-> sample (NXsample) # |-> name (optional) # |-> rotation_angle - mandatory (1D dataset in degree) # |-> x_translation - optional (1D dataset) # |-> y_translation - optional (1D dataset) # |-> z_translation - optional (1D dataset) # # Format 1.1: # * move beam to the NXinstrument # * Keep compatibility by providing a link to beam at the root level # * add optional dataset instrument/name # * sample/name is moved to title # * sample/sample_name is moved to sample/name # * add NXsource under instrument: # |-> instrument (NXinstrument) # |-> source (optional NXsource) # |-> name (optional) # |-> type (optional) nxtomomill-v2.0.1/nxtomomill/io/000077500000000000000000000000001511430602400167075ustar00rootroot00000000000000nxtomomill-v2.0.1/nxtomomill/io/__init__.py000066400000000000000000000001331511430602400210150ustar00rootroot00000000000000"""dedicated module to handle input and output""" from .config import * # noqa F401,F403 nxtomomill-v2.0.1/nxtomomill/io/config/000077500000000000000000000000001511430602400201545ustar00rootroot00000000000000nxtomomill-v2.0.1/nxtomomill/io/config/__init__.py000066400000000000000000000005341511430602400222670ustar00rootroot00000000000000# coding: utf-8 from .edfconfig import TomoEDFConfig, generate_default_edf_config # noqa F401,F403 from .hdf5config import ( # noqa F401,F403 TomoHDF5Config, generate_default_h5_config, ) from .dxconfig import DXFileConfiguration # noqa F401,F403 from .fluoconfig import TomoFluoConfig, generate_default_fluo_config # noqa F401,F403 nxtomomill-v2.0.1/nxtomomill/io/config/configbase.py000066400000000000000000000010531511430602400226250ustar00rootroot00000000000000# coding: utf-8 from __future__ import annotations from nxtomomill.utils.io import deprecated_warning from nxtomomill.models.base.ConfigBase import ConfigBase as _ConfigBase __all__ = ["ConfigBase"] class ConfigBase(_ConfigBase): def __init__(self, *args, **kwargs): deprecated_warning( type_="class", name="ConfigBase", replacement="nxtomomill.models.base.ConfigBase", reason="moved to models module", since_version="2.0", ) super().__init__(*args, **kwargs) nxtomomill-v2.0.1/nxtomomill/io/config/confighandler.py000066400000000000000000000140721511430602400233350ustar00rootroot00000000000000# coding: utf-8 """ contains the HDF5ConfigHandler """ from __future__ import annotations import logging import os from nxtomomill.converter.hdf5.utils import get_default_output_file from .hdf5config import TomoHDF5Config _logger = logging.getLogger(__name__) __all__ = [ "SETTABLE_PARAMETERS_UNITS", "SETTABLE_PARAMETERS_TYPE", "SETTABLE_PARAMETERS", "TomoHDF5ConfigHandler", ] SETTABLE_PARAMETERS_UNITS = { "energy": "kev", "x_pixel_size": "m", "y_pixel_size": "m", "detector_sample_distance": "m", } SETTABLE_PARAMETERS_TYPE = { "energy": float, "x_pixel_size": float, "y_pixel_size": float, "detector_sample_distance": float, } SETTABLE_PARAMETERS = SETTABLE_PARAMETERS_UNITS.keys() def _extract_param_value(key_values): """extract all the key / values elements from the str_list. Expected format is `param_1_name param_1_value param_2_name param_2_value ...` :param str_list: raw input string as `param_1_name param_1_value param_2_name param_2_value ...` :return: dict of tuple (param_name, param_value) """ if len(key_values) % 2 != 0: raise ValueError( "Expect a pair `param_name, param_value` for each " "parameters" ) def pairwise(it): it = iter(it) while True: try: yield next(it), next(it) except StopIteration: # no more elements in the iterator return res = {} for name, value in pairwise(key_values): if name not in SETTABLE_PARAMETERS: raise ValueError(f"parameters {name} is not managed") if name in SETTABLE_PARAMETERS_TYPE: type_ = SETTABLE_PARAMETERS_TYPE[name] if type_ is not None: res[name] = type_(value) continue res[name] = value return res class TomoHDF5ConfigHandler: """ Create a TomoHDF5Config from argparse option provided from the CLI. ..warning:: If a configuration file (--config_file) is given then other argparse options will be ignored. """ def __init__(self, argparse_options): self._argparse_options = argparse_options self._config = None self.build_configuration() @property def configuration(self) -> TomoHDF5Config | None: return self._config @property def argparse_options(self): return self._argparse_options def build_configuration(self): if self.argparse_options.config_file is not None: assert isinstance( self.argparse_options.config_file, str ), f"{self.argparse_options.config_file}, {type(self.argparse_options.config_file)}" # from the configuration file _logger.warning( "A configuration has been given. All other options at the exception of the input and output file will be ignored" ) config = TomoHDF5Config.from_cfg_file(self.argparse_options.config_file) if self.argparse_options.input_file is not None: config.input_file = self.argparse_options.input_file if self.argparse_options.output_file is not None: config.output_file = self.argparse_options.output_file else: # from argparse config = TomoHDF5Config() options = self.argparse_options # check input and output file # propagate options defined by the user to the config file. # policy: all options when not defined have a default value set to None # So we don't have to set twice the default value from the source code (class and argparse) for opt_name in ( "input_file", "output_file", "file_extension", "single_file", "no_master_file", "overwrite", "entries", "sub_entries_to_ignore", "raises_error", "field_of_view", "request_input", "default_data_copy", "sample_x_keys", "sample_y_keys", "translation_z_keys", "sample_detector_distance_keys", "valid_camera_names", "rotation_angle_keys", "exposure_time_keys", "sample_x_pixel_size_keys", "sample_y_pixel_size_keys", "init_titles", "zseries_init_titles", "multitomo_init_titles", "back_and_forth_init_titles", "dark_titles", "flat_titles", "projection_titles", "alignment_titles", ): opt_value = getattr(options, opt_name) if opt_value is not None: setattr(config, opt_name, opt_value) # handle specific use cases if options.debug is not None: config.log_level = logging.DEBUG if options.set_params is not None: extra_params = _extract_param_value(options.set_params) for param, param_value in extra_params.items(): setattr(config, param, param_value) if config.input_file is None: raise ValueError("No input file provided") if config.output_file is None: if config.file_extension is None: err = "If no output file provided you should provide the extension" raise ValueError(err) config.output_file = get_default_output_file( input_file=config.input_file, output_file_extension=config.file_extension.value, # pylint: disable=E1101 ) elif os.path.isdir(config.output_file): # if the output file is only a directory: consider we want the same file basename with default extension # in this directory input_file, _ = os.path.splitext(config.input_file) config.output_file = os.path.join( os.path.abspath(config.output_file), os.path.basename(input_file) + config.file_extension.value, # pylint: disable=E1101 ) self._config = config nxtomomill-v2.0.1/nxtomomill/io/config/dxconfig.py000066400000000000000000000011171511430602400223270ustar00rootroot00000000000000# coding: utf-8 from __future__ import annotations from nxtomomill.utils.io import deprecated_warning from nxtomomill.models.dx2nx import DX2nxModel as _DXFileConfiguration __all__ = ["DXFileConfiguration"] class DXFileConfiguration(_DXFileConfiguration): def __init__(self, *args, **kwargs): deprecated_warning( type_="class", name="DXFileConfiguration", replacement="nxtomomill.models.dx2nx.DX2nxModel", reason="moved to models module", since_version="2.0", ) super().__init__(*args, **kwargs) nxtomomill-v2.0.1/nxtomomill/io/config/edfconfig.py000066400000000000000000000016201511430602400224510ustar00rootroot00000000000000# coding: utf-8 from __future__ import annotations from nxtomomill.utils.io import deprecated, deprecated_warning from nxtomomill.models.edf2nx import EDF2nxModel as _TomoEDFConfig from nxtomomill.models.edf2nx import ( generate_default_edf_config as _generate_default_edf_config, ) __all__ = ["TomoEDFConfig", "generate_default_edf_config"] class TomoEDFConfig(_TomoEDFConfig): def __init__(self, *args, **kwargs): deprecated_warning( type_="class", name="TomoEDFConfig", replacement="nxtomomill.models.edf2nx.EDF2nxModel", reason="moved to models module", since_version="2.0", ) super().__init__(*args, **kwargs) @deprecated(reason="moved to from nxtomomill.models.edf2nx module", since_version="2.0") def generate_default_edf_config(*args, **kwargs): return _generate_default_edf_config(*args, **kwargs) nxtomomill-v2.0.1/nxtomomill/io/config/fluoconfig.py000066400000000000000000000016461511430602400226700ustar00rootroot00000000000000# coding: utf-8 from __future__ import annotations from nxtomomill.utils.io import deprecated, deprecated_warning from nxtomomill.models.fluo2nx import Fluo2nxModel as _TomoFluoConfig from nxtomomill.models.fluo2nx import ( generate_default_fluo_config as _generate_default_fluo_config, ) __all__ = ["TomoFluoConfig", "generate_default_fluo_config"] class TomoFluoConfig(_TomoFluoConfig): def __init__(self, *args, **kwargs): deprecated_warning( type_="class", name="TomoFluoConfig", replacement="nxtomomill.models.fluo2nx.Fluo2nxModel", reason="moved to models module", since_version="2.0", ) super().__init__(*args, **kwargs) @deprecated( reason="moved to from nxtomomill.models.fluo2nx module", since_version="2.0" ) def generate_default_fluo_config(*args, **kwargs): return _generate_default_fluo_config(*args, **kwargs) nxtomomill-v2.0.1/nxtomomill/io/config/hdf5config.py000066400000000000000000000016251511430602400225460ustar00rootroot00000000000000# coding: utf-8 from __future__ import annotations from nxtomomill.utils.io import deprecated, deprecated_warning from nxtomomill.models.h52nx import H52nxModel as _TomoHDF5Config from nxtomomill.models.h52nx import ( generate_default_h5_config as _generate_default_h5_config, ) __all__ = [ "TomoHDF5Config", "generate_default_h5_config", ] class TomoHDF5Config(_TomoHDF5Config): def __init__(self, *args, **kwargs): deprecated_warning( type_="class", name="TomoHDF5Config", replacement="nxtomomill.models.dx2nx.H52nxModel", reason="moved to models module", since_version="2.0", ) super().__init__(*args, **kwargs) @deprecated(reason="moved to from nxtomomill.models.h52nx module", since_version="2.0") def generate_default_h5_config(*args, **kwargs): return _generate_default_h5_config(*args, **kwargs) nxtomomill-v2.0.1/nxtomomill/io/config/tests/000077500000000000000000000000001511430602400213165ustar00rootroot00000000000000nxtomomill-v2.0.1/nxtomomill/io/config/tests/__init__.py000066400000000000000000000000001511430602400234150ustar00rootroot00000000000000nxtomomill-v2.0.1/nxtomomill/io/config/tests/test_confighandler.py000066400000000000000000000160411511430602400255340ustar00rootroot00000000000000# coding: utf-8 import os import pytest from nxtomomill.io.config.confighandler import TomoHDF5ConfigHandler from nxtomomill.io.config.hdf5config import TomoHDF5Config from nxtomomill.models.h52nx.FrameGroup import FrameGroup class _ARParseMock(object): def __init__(self): self.config_file = None self.input_file = None self.output_file = None self.set_params = None self.alignment_titles = None self.projection_titles = None self.flat_titles = None self.dark_titles = None self.zseries_init_titles = None self.multitomo_init_titles = None self.init_titles = None self.back_and_forth_init_titles = None self.sample_x_pixel_size_keys = None self.sample_y_pixel_size_keys = None self.exposure_time_keys = None self.rotation_angle_keys = None self.valid_camera_names = None self.translation_z_keys = None self.sample_y_keys = None self.sample_x_keys = None self.request_input = False self.raises_error = False self.default_data_copy = None self.sub_entries_to_ignore = None self.entries = None self.debug = False self.overwrite = False self.single_file = False self.no_master_file = False self.file_extension = None self.field_of_view = None self.sample_detector_distance_keys = None def test_creation_from_config_file(tmp_path): options = _ARParseMock() with pytest.raises(ValueError): TomoHDF5ConfigHandler(argparse_options=options) input_file_path = os.path.join(tmp_path, "output_file.cfg") input_config = TomoHDF5Config() input_config.to_cfg_file(input_file_path) options.config_file = input_file_path # insure this is valid if we add an input file and an output file # from the command line options.input_file = "toto.h5" options.output_file = "toto.nx" TomoHDF5ConfigHandler(argparse_options=options) # or if we provide them from the configuration file options.input_file = None options.output_file = None input_config.input_file = "toto.h5" input_config.output_file = "toto.nx" input_config.to_cfg_file(input_file_path) TomoHDF5ConfigHandler(argparse_options=options) def write_and_read_configuration(folder, data_urls): configuration = TomoHDF5Config() configuration.data_scans = data_urls configuration.input_file = "toto.h5" configuration.output_file = "toto.nx" file_path = os.path.join(folder, "h52nx.cfg") configuration.to_cfg_file(file_path=file_path) options = _ARParseMock() options.config_file = file_path config_handler = TomoHDF5ConfigHandler(options) return config_handler.configuration def test_local_paths(tmp_path): res_config = write_and_read_configuration( folder=tmp_path, data_urls=( FrameGroup(url="/entry0000/data/flats", frame_type="flat"), FrameGroup(url="/entry0000/data/projection1", frame_type="projection"), FrameGroup(url="/entry0000/data/projection2", frame_type="projection"), FrameGroup(url="/entry0000/data/alignment1", frame_type="alignment"), FrameGroup(url="/entry0000/data/alignment2", frame_type="alignment"), FrameGroup(url="/entry0000/data/darks", frame_type="dark"), ), ) assert res_config is not None assert len(res_config.data_scans) == 6 # check flats flat_url = res_config.data_scans[0].url assert flat_url.file_path() in (None, "") assert flat_url.data_path() == "/entry0000/data/flats" assert flat_url.data_slice() in (None, "") # check projections projection_url_0 = res_config.data_scans[1].url projection_url_1 = res_config.data_scans[2].url assert projection_url_0.file_path() in (None, "") assert projection_url_0.data_path() == "/entry0000/data/projection1" assert projection_url_0.data_slice() is None assert projection_url_1.file_path() in (None, "") assert projection_url_1.data_path() == "/entry0000/data/projection2" assert projection_url_1.data_slice() is None # check darks dark_url = res_config.data_scans[-1].url assert dark_url.file_path() in (None, "") assert dark_url.data_path(), "/entry0000/data/darks" assert dark_url.data_slice() is None # check alignments alignment_url_0 = res_config.data_scans[3].url alignment_url_1 = res_config.data_scans[4].url assert alignment_url_0.file_path() in (None, "") assert alignment_url_0.data_path(), "/entry0000/data/alignment1" assert alignment_url_0.data_slice() is None assert alignment_url_1.data_path() == "/entry0000/data/alignment2" def test_external_paths(tmp_path): """ Check that DataUrl are handled. Warning: this also check data_slices but those are not handled. """ res_config = write_and_read_configuration( folder=tmp_path, data_urls=( FrameGroup( frame_type="flat", url="silx:///myfile.h5?path=/entry0000/data/flats", ), FrameGroup( frame_type="dark", url="h5py:///data/file2.hdf5?path=/entry0000/data/darks", ), FrameGroup(frame_type="proj", url="h5py:///data/file3.hdf5?path=/data"), FrameGroup( frame_type="proj", url="silx:///data/file2.hdf5?path=/entry0000/data/projection2&slice=5:100", ), FrameGroup( frame_type="alignment", url="silx:///myfile.h5?path=/entry0000/data/alignment&slice=5", ), ), ) assert res_config is not None assert len(res_config.data_scans) == 5 # check flats flat_url = res_config.data_scans[0] assert flat_url.url.file_path() == "/myfile.h5" assert flat_url.url.data_path() == "/entry0000/data/flats" assert flat_url.url.data_slice() is None assert flat_url.url.scheme() == "silx" # check projections projection_url_0 = res_config.data_scans[2].url projection_url_1 = res_config.data_scans[3].url assert projection_url_0.file_path() == "/data/file3.hdf5" assert projection_url_0.data_path() == "/data" assert projection_url_0.data_slice() is None assert projection_url_0.scheme() == "h5py" assert projection_url_1.file_path() == "/data/file2.hdf5" assert projection_url_1.data_path() == "/entry0000/data/projection2" assert projection_url_1.data_slice() == (slice(5, 100),) assert projection_url_1.scheme() == "silx" # check darks dark_url = res_config.data_scans[1].url assert dark_url.file_path() == "/data/file2.hdf5" assert dark_url.data_path() == "/entry0000/data/darks" assert dark_url.data_slice() is None assert dark_url.scheme() == "h5py" # check alignments alignment_url_0 = res_config.data_scans[4].url assert alignment_url_0.file_path() == "/myfile.h5" assert alignment_url_0.data_path() == "/entry0000/data/alignment" assert alignment_url_0.data_slice() == (5,) assert alignment_url_0.scheme() == "silx" nxtomomill-v2.0.1/nxtomomill/io/tests/000077500000000000000000000000001511430602400200515ustar00rootroot00000000000000nxtomomill-v2.0.1/nxtomomill/io/tests/test_frame_group.py000066400000000000000000000131271511430602400237740ustar00rootroot00000000000000# coding: utf-8 import unittest from silx.io.url import DataUrl from nxtomomill.models.h52nx._acquisitionstep import AcquisitionStep from nxtomomill.models.h52nx.FrameGroup import ( FrameGroup, filter_acqui_frame_type, ) class TestFrameGroupCreator(unittest.TestCase): """Test FrameGroup class""" def setUp(self) -> None: self.url1 = DataUrl( file_path="/path/to/file/my_file", data_path="/path/to/data", scheme="silx" ) self.url2 = DataUrl(file_path="my_file", data_path="/path/to/data") def test_default_constructor(self): """Test FrameGroup constructor""" frame_grp = FrameGroup(url=self.url1, frame_type="projection") self.assertEqual(frame_grp.url.path(), self.url1.path()) self.assertEqual(frame_grp.frame_type, AcquisitionStep.PROJECTION) frame_grp = FrameGroup(url=self.url2, frame_type=AcquisitionStep.ALIGNMENT) self.assertEqual(frame_grp.url.path(), self.url2.path()) self.assertEqual(frame_grp.frame_type, AcquisitionStep.ALIGNMENT) with self.assertRaises(ValueError): FrameGroup(url=self.url1, frame_type="toto") with self.assertRaises(ValueError): FrameGroup(url=self.url1, frame_type="projection", copy_data="tata") def test_constructor_frm_str(self): """Test FrameGroup.frm_str function""" grp_prefix = FrameGroup.frm_str( "frame_type=projections, " "entry=silx:///path/to/file/my_file?/path/to/data, " "copy_data=True" ) self.assertTrue(isinstance(grp_prefix, FrameGroup)) self.assertEqual(grp_prefix.copy_data, True) grp_no_prefix = FrameGroup.frm_str( "projections, silx:///path/to/file/my_file?/path/to/data, True" ) self.assertEqual(str(grp_no_prefix), str(grp_prefix)) with self.assertRaises(ValueError): FrameGroup.frm_str( "frame_type=projections, " "entry=silx:///path/to/file/my_file?/path/to/data, " "copy_data=12" ) with self.assertRaises(ValueError): FrameGroup.frm_str("frame_type=projections") class TestFilterCurrentAcquiFrameType(unittest.TestCase): """test filter_acqui_frame_type function""" def setUp(self) -> None: self.init_1 = FrameGroup(frame_type="init", url=None) self.sequence1 = ( self.init_1, FrameGroup(frame_type="dark", url="data/to/dark/1"), FrameGroup(frame_type="flat", url="data/to/flat/1"), FrameGroup(frame_type="proj", url="data/to/proj/1"), FrameGroup(frame_type="proj", url="data/to/proj/2"), ) self.init_2 = FrameGroup(frame_type="init", url="/path/to/init") self.sequence2 = ( self.init_2, FrameGroup(frame_type="dark", url="data/to/dark/2"), FrameGroup(frame_type="proj", url="data/to/proj/3"), FrameGroup(frame_type="proj", url="data/to/proj/4"), FrameGroup(frame_type="proj", url="data/to/proj/5"), FrameGroup(frame_type="proj", url="data/to/proj/6"), FrameGroup(frame_type="flat", url="data/to/flat/2"), FrameGroup(frame_type="alignment", url="data/to/alignment/1"), ) def test_search_init(self): """test filter_acqui_frame_type with init frame group""" with self.assertRaises(ValueError): filter_acqui_frame_type( init=self.init_1, sequences=self.sequence1, frame_type=AcquisitionStep.INITIALIZATION, ) def test_search_flat(self): """test filter_acqui_frame_type with flat frame group""" flat_url = filter_acqui_frame_type( init=self.init_1, sequences=self.sequence1, frame_type=AcquisitionStep.FLAT, ) self.assertEqual(len(flat_url), 1) self.assertEqual(flat_url[0].url.path(), self.sequence1[2].url.path()) with self.assertRaises(ValueError): filter_acqui_frame_type( init=self.init_1, sequences=self.sequence2, frame_type=AcquisitionStep.FLAT, ) def test_search_dark(self): """test filter_acqui_frame_type with dark frame group""" seq_sum = list(self.sequence1) seq_sum.extend(self.sequence2) dark_1 = filter_acqui_frame_type( init=self.init_1, sequences=tuple(seq_sum), frame_type=AcquisitionStep.DARK ) self.assertEqual(len(dark_1), 1) self.assertEqual(dark_1[0].url.path(), self.sequence1[1].url.path()) dark_2 = filter_acqui_frame_type( init=self.init_2, sequences=tuple(seq_sum), frame_type=AcquisitionStep.DARK ) self.assertEqual(len(dark_2), 1) self.assertEqual(dark_2[0].url.path(), self.sequence2[1].url.path()) def test_search_proj(self): """test filter_acqui_frame_type with projection frame group""" projs_1 = filter_acqui_frame_type( init=self.init_2, sequences=self.sequence2, frame_type=AcquisitionStep.PROJECTION, ) self.assertEqual(len(projs_1), 4) self.assertEqual(projs_1[2].url.path(), self.sequence2[4].url.path()) seq_sum = list(self.sequence1) seq_sum.extend(self.sequence2) projs_2 = filter_acqui_frame_type( init=self.init_2, sequences=tuple(seq_sum), frame_type=AcquisitionStep.PROJECTION, ) self.assertEqual(len(projs_2), 4) self.assertEqual(projs_1[1].url.path(), projs_2[1].url.path()) nxtomomill-v2.0.1/nxtomomill/io/tests/utils.py000066400000000000000000000036361511430602400215730ustar00rootroot00000000000000from nxtomomill.utils.io import deprecated, deprecated_warning from nxtomomill.models.utils import ( remove_parenthesis_or_brackets as _remove_parenthesis_or_brackets, ) from nxtomomill.models.utils import filter_str_def as _filter_str_def from nxtomomill.models.utils import convert_str_to_tuple as _convert_str_to_tuple from nxtomomill.models.utils import convert_str_to_bool as _convert_str_to_bool from nxtomomill.models.utils import is_url_path as _is_url_path from nxtomomill.models.utils import PathType as _PathType @deprecated( replacement="nxtomomill.models.utils.remove_parenthesis_or_brackets", since_version="2.0", reason="moved", ) def remove_parenthesis_or_brackets(*args, **kwargs): return _remove_parenthesis_or_brackets(*args, **kwargs) @deprecated( replacement="nxtomomill.models.utils.filter_str_def", since_version="2.0", reason="moved", ) def filter_str_def(*args, **kwargs): return _filter_str_def(*args, **kwargs) @deprecated( replacement="nxtomomill.models.utils.convert_str_to_tuple", since_version="2.0", reason="moved", ) def convert_str_to_tuple(*args, **kwargs): return _convert_str_to_tuple(*args, **kwargs) @deprecated( replacement="nxtomomill.models.utils.convert_str_to_bool", since_version="2.0", reason="moved", ) def convert_str_to_bool(*args, **kwargs): return _convert_str_to_bool(*args, **kwargs) @deprecated( replacement="nxtomomill.models.utils.is_url_path", since_version="2.0", reason="moved", ) def is_url_path(*args, **kwargs): return _is_url_path(*args, **kwargs) class PathType(_PathType): def __init__(self, *args, **kwargs): deprecated_warning( type_="class", name="PathType", replacement="nxtomomill.models.utils.PathType", reason="moved to models module", since_version="2.0", ) super().__init__(*args, **kwargs) nxtomomill-v2.0.1/nxtomomill/models/000077500000000000000000000000001511430602400175635ustar00rootroot00000000000000nxtomomill-v2.0.1/nxtomomill/models/__init__.py000066400000000000000000000000001511430602400216620ustar00rootroot00000000000000nxtomomill-v2.0.1/nxtomomill/models/base/000077500000000000000000000000001511430602400204755ustar00rootroot00000000000000nxtomomill-v2.0.1/nxtomomill/models/base/ConfigBase.py000066400000000000000000000017131511430602400230510ustar00rootroot00000000000000from __future__ import annotations from nxtomomill.utils.io import deprecated class ConfigBase: """ Base class for models that can produce a configuration file (.cfg) to be provided to one of the converter. The configuration is setting inputs for the different conversion functions / classes. We expect config classes / models to inherit from this class and from a pydantic model. """ def to_cfg_file(self, file_path: str, filter_sections: tuple[str] = tuple()): raise NotImplementedError @classmethod def from_cfg_file(cls, file_path: str) -> ConfigBase: raise NotImplementedError @deprecated( reason="Replaced by pydantic function 'model_dump'", since_version="2.0" ) def to_dict(self) -> dict: return self.model_dump() # pylint: disable=E1101 @staticmethod def from_dict(dict_: dict) -> None: """.. warning:: deprecated since 2.0""" raise NotImplementedError nxtomomill-v2.0.1/nxtomomill/models/base/FrmFlatToNestedBase.py000066400000000000000000000020351511430602400246430ustar00rootroot00000000000000from __future__ import annotations from nxtomomill.utils.io import deprecated from .ConfigBase import ConfigBase class FrmFlatToNestedBase(ConfigBase): """ Base class defining behavior for models needing to be dumped with sections. Internally they are represented as 'flat' but externally they are represented as nested to ease user setting values. This is the historical design. Maybe in the future we will use nested models internally as well. """ def to_cfg_file(self, file_path: str, filter_sections: tuple[str] = tuple()): nested_model = self.to_nested_model() nested_model.to_cfg_file( file_path=file_path, filter_sections=filter_sections, ) def to_nested_model(self): raise NotImplementedError @deprecated( reason="Replaced by pydantic function 'model_dump'", since_version="2.0" ) def to_dict(self) -> dict: config = self.to_nested_model() return {key.upper(): value for key, value in config.model_dump().items()} nxtomomill-v2.0.1/nxtomomill/models/base/NestedModelBase.py000066400000000000000000000033761511430602400240560ustar00rootroot00000000000000from __future__ import annotations import logging from configparser import ConfigParser from pydantic import BaseModel _logger = logging.getLogger(__name__) class NestedModelBase: @staticmethod def cast_value_to_str(value) -> str: if value in (None, tuple()): return "" else: return str(value) def to_cfg_file(self, file_path: str, filter_sections: tuple[str] = tuple()): """ Dump the model to a cfg_file. In this case the model is expected to be composed of a set of sub-model. The sub-models are expected to only contain Fields. """ if not file_path.lower().endswith((".cfg", ".config", ".conf")): _logger.warning("add a valid extension to the output file") file_path += ".cfg" config = ConfigParser(allow_no_value=True) config.optionxform = str for section_name in self.__class__.model_fields: # pylint: disable=E1101 if section_name in filter_sections: continue section_name_upper = section_name.upper() config.add_section(section_name_upper) config.set(section_name_upper, "") section: BaseModel = getattr(self, section_name) dump_model = section.model_dump(mode="python") for field_name, field_value in dump_model.items(): field_info = section.model_fields[field_name] config.set(section_name_upper, "# " + field_info.description) config.set( section_name_upper, field_name, self.cast_value_to_str(field_value), ) with open(file_path, "w") as config_file: config.write(config_file) nxtomomill-v2.0.1/nxtomomill/models/base/__init__.py000066400000000000000000000000001511430602400225740ustar00rootroot00000000000000nxtomomill-v2.0.1/nxtomomill/models/base/general_section.py000066400000000000000000000040371511430602400242140ustar00rootroot00000000000000from __future__ import annotations import logging from pydantic import BaseModel, Field, field_validator, field_serializer, ConfigDict from nxtomomill.utils import FileExtension from nxtomomill.models.utils import filter_str_def class GeneralSection(BaseModel): model_config = ConfigDict(validate_assignment=True, validate_by_name=True) output_file: str | None = Field(default=None, description="Output file name") file_extension: FileExtension = Field( default=FileExtension.NX, description="File extension if not given in the output file name", ) overwrite: bool = Field( default=False, description="Overwrite output files if exists" ) log_level: int = Field( default=logging.WARNING, description='Log level: "debug", "info", "warning", "error"', ) @field_validator( "log_level", mode="before", ) @classmethod def cast_to_log_level(cls, value: str | int) -> int: if isinstance(value, int): return value elif isinstance(value, str): value = filter_str_def(value) return getattr(logging, value.upper()) else: raise TypeError @field_validator( "file_extension", mode="plain", ) @classmethod def cast_to_file_extension(cls, value: str | FileExtension) -> FileExtension: if isinstance(value, str): value = filter_str_def(value) return FileExtension(value) @field_serializer( "log_level", when_used="always", ) @classmethod def serialize_log_level(cls, value: int) -> str: return logging.getLevelName(value).lower() @field_serializer( "file_extension", when_used="always", ) @classmethod def serialize_file_extension(cls, file_ext: FileExtension) -> str: return file_ext.value @field_validator( "output_file", ) @classmethod def cast_output_file(cls, output_file: str | None) -> str | None: return filter_str_def(output_file) nxtomomill-v2.0.1/nxtomomill/models/base/instrument_section.py000066400000000000000000000011261511430602400250030ustar00rootroot00000000000000from __future__ import annotations from pydantic import BaseModel, ConfigDict, Field, field_validator from nxtomomill.models.utils import filter_str_def class InstrumentSection(BaseModel): model_config = ConfigDict(validate_assignment=True, validate_by_name=True) instrument_name: str | None = Field( default=None, description="Name of the instrument" ) @field_validator( "instrument_name", mode="plain", ) @classmethod def cast_instrument_name(cls, value: str | None) -> str | None: res = filter_str_def(value) return res nxtomomill-v2.0.1/nxtomomill/models/base/source_section.py000066400000000000000000000040231511430602400240720ustar00rootroot00000000000000from __future__ import annotations from pydantic import BaseModel, Field, field_validator, ConfigDict, field_serializer from nxtomo.nxobject.nxsource import ProbeType, SourceType from nxtomomill.models.utils import filter_str_def class SourceSection(BaseModel): model_config = ConfigDict(validate_assignment=True, validate_by_name=True) source_name: str | None = Field( default="ESRF", description="Name of the instrument" ) source_type: SourceType | None = Field( default=SourceType.SYNCHROTRON_X_RAY_SOURCE, description="Source type" ) source_probe: ProbeType | None = Field( default=ProbeType.X_RAY, description="Probe type" ) @field_validator( "source_type", mode="plain", ) @classmethod def cast_to_source_type(cls, value: str | SourceType | None) -> SourceType: if value in (None, "", "None"): return None elif isinstance(value, str): value = filter_str_def(value) return SourceType(value) @field_serializer( "source_type", ) @classmethod def serialize_source_type(cls, source_type: SourceType | None) -> str: if source_type is None: return "" else: return source_type.value @field_validator( "source_probe", mode="plain", ) @classmethod def cast_to_source_probe(cls, value: str | ProbeType | None) -> ProbeType: if value in (None, ""): return None elif isinstance(value, str): value = filter_str_def(value) return ProbeType(value) @field_serializer( "source_probe", ) @classmethod def serialize_source_probe(cls, source_probe: ProbeType | None) -> str: if source_probe is None: return "" else: return source_probe.value @field_validator( "source_name", mode="plain", ) @classmethod def cast_to_str(cls, value: str | None) -> tuple | None: return filter_str_def(value) nxtomomill-v2.0.1/nxtomomill/models/blissfluo2nx/000077500000000000000000000000001511430602400222155ustar00rootroot00000000000000nxtomomill-v2.0.1/nxtomomill/models/blissfluo2nx/__init__.py000066400000000000000000000000761511430602400243310ustar00rootroot00000000000000from .blissfluo2nx import BlissFluo2nxModel # noqa F401,F403 nxtomomill-v2.0.1/nxtomomill/models/blissfluo2nx/blissfluo2nx.py000066400000000000000000000022111511430602400252150ustar00rootroot00000000000000# coding: utf-8 from __future__ import annotations from pydantic import BaseModel, ConfigDict from .general_section import GeneralSection from ..base.source_section import SourceSection from ..base.instrument_section import InstrumentSection from ..base.NestedModelBase import NestedModelBase __all__ = [ "BlissFluo2nxModel", ] class BlissFluo2nxModel( BaseModel, NestedModelBase, ): model_config = ConfigDict(str_to_upper=True, validate_assignment=True) general_section: GeneralSection = GeneralSection() source_section: SourceSection = SourceSection() instrument_section: InstrumentSection = InstrumentSection() def model_dump(self, *args, **kwargs) -> dict: unordered_result = super().model_dump(*args, **kwargs) orderdered_result = { "ewoksfluo_filename": unordered_result.pop("ewoksfluo_filename"), "output_file": unordered_result.pop("output_file"), "detector_names": unordered_result.pop("detector_names"), "dimension": unordered_result.pop("dimension"), } orderdered_result.update(unordered_result) return orderdered_result nxtomomill-v2.0.1/nxtomomill/models/blissfluo2nx/general_section.py000066400000000000000000000037571511430602400257440ustar00rootroot00000000000000from __future__ import annotations from pydantic import Field, field_validator, ConfigDict from collections import OrderedDict from nxtomomill.models.utils import convert_str_to_tuple, filter_str_def from ..base.general_section import GeneralSection as _GeneralSectionBase class GeneralSection(_GeneralSectionBase): model_config = ConfigDict(validate_assignment=True, validate_by_name=True) ewoksfluo_filename: str | None = Field( default=None, description="Path to the ewoksfluo-generated h5 file that contains fitted XRF data. If not provided from the configuration file must be provided from the command line.", ) dimension: int = Field( default=3, description="Dimension of the experiment. 2 for 2D XRFCT, 3 for 3D XRFCT. Default is 3.", ) detector_names: tuple[str, ...] = Field( default=tuple(), description="Define a list of (real or virtual) detector names used for the exp (space separated values - no comma). E.g. 'falcon xmap'. If not specified, all detectors are processed.", ) @field_validator( "detector_names", mode="before", ) @classmethod def cast_to_tuple(cls, value: str | None) -> tuple[str]: return convert_str_to_tuple(value) or () @field_validator( "ewoksfluo_filename", mode="before", ) @classmethod def cast_to_str(cls, value: str | None) -> str | None: return filter_str_def(value) def model_dump(self, *args, **kwargs) -> dict: unordered_result = super().model_dump(*args, **kwargs) ordered_result = OrderedDict( { "ewoksfluo_filename": unordered_result.pop("ewoksfluo_filename"), "output_file": unordered_result.pop("output_file"), "detector_names": unordered_result.pop("detector_names"), "dimension": unordered_result.pop("dimension"), } ) ordered_result.update(unordered_result) return ordered_result nxtomomill-v2.0.1/nxtomomill/models/dx2nx.py000066400000000000000000000045441511430602400212070ustar00rootroot00000000000000"""data exchange (dx) configuration module""" from __future__ import annotations from nxtomo.nxobject.nxdetector import FieldOfView from .base.general_section import GeneralSection from pydantic import Field, ConfigDict, field_serializer, field_validator __all__ = [ "DX2nxModel", ] class DX2nxModel(GeneralSection): """Configuration file to run a conversion from data-exchange file to NXtomo format""" model_config: ConfigDict = ConfigDict(validate_assignment=True) input_file: str = Field( description="Path to input file at data-file-exchange format to be converted to NXtomo format" ) copy_data: bool = Field( default=True, description="If True frames will be duplicated. Otherwise we will create (relative) link to the input file.", ) input_entry: str = Field( default="/", description="Path to the HDF5 group to convert. For now it looks each file can only contain one dataset. Just to ensure any future compatibility if it evolve with time.", ) output_entry: str = Field( "entry0000", description="Path to store the NxTomo created." ) scan_range: tuple[float, float] = Field( default=(0.0, 180.0), description="Tuple of two elements with the minimum scan range. Projections are expected to be taken with equal angular spaces.", ) pixel_size: tuple[float | None, float | None] = Field( default=(None, None), description="Pixel size can be provided - in meter - as (x_pizel_size, y_pixel_size)", ) field_of_view: FieldOfView | None = Field( default=None, description="Detector field of view" ) sample_detector_distance: float | None = Field( default=1.0, description="sample / detector distance in meter" ) energy: float | None = Field(default=None, description="Energy in keV") @field_validator( "field_of_view", ) @classmethod def cast_to_field_of_view(cls, value: str | FieldOfView) -> FieldOfView | None: if value in (None, ""): return None return FieldOfView(value) @field_serializer( "field_of_view", when_used="always", ) @classmethod def serialize_field_of_view(cls, field_of_view: FieldOfView | None) -> str: if field_of_view is None: return None else: return field_of_view.value nxtomomill-v2.0.1/nxtomomill/models/edf2nx/000077500000000000000000000000001511430602400207515ustar00rootroot00000000000000nxtomomill-v2.0.1/nxtomomill/models/edf2nx/__init__.py000066400000000000000000000001641511430602400230630ustar00rootroot00000000000000from .edf2nx import EDF2nxModel # noqa F401,F403 from .edf2nx import generate_default_edf_config # noqa F401,F403 nxtomomill-v2.0.1/nxtomomill/models/edf2nx/dark_and_flat_section.py000066400000000000000000000032571511430602400256270ustar00rootroot00000000000000from __future__ import annotations from nxtomomill.settings import Tomo from nxtomomill.models.utils import convert_str_to_tuple from nxtomomill.utils.io import deprecated from nxtomomill.models.utils import filter_str_def from pydantic import BaseModel, Field, field_validator, ConfigDict class DarkAndFlatSection(BaseModel): model_config = ConfigDict(validate_assignment=True, validate_by_name=True) dark_names_prefix: tuple[str, ...] = Field( default=Tomo.EDF.DARK_NAMES, description="Prefixes of dark field file(s)" ) flat_names_prefix: tuple[str, ...] = Field( default=Tomo.EDF.REFS_NAMES, description="Prefixes of flat field file(s)" ) @field_validator( "dark_names_prefix", "flat_names_prefix", mode="plain", ) @classmethod def cast_to_tuple(cls, value: str | tuple[str, ...]) -> tuple[str,]: if isinstance(value, str): value = filter_str_def(value) return convert_str_to_tuple(value) or () @property @deprecated(since_version="2.0", reason="renamed", replacement="dark_names_prefix") def dark_names(self): return self.dark_names_prefix @dark_names.setter @deprecated(since_version="2.0", reason="renamed", replacement="dark_names_prefix") def dark_names(self, value): self.dark_names_prefix = value @property @deprecated(since_version="2.0", reason="renamed", replacement="flat_names_prefix") def flat_names(self): return self.flat_names_prefix @flat_names.setter @deprecated(since_version="2.0", reason="renamed", replacement="flat_names_prefix") def flat_names(self, value): self.flat_names_prefix = value nxtomomill-v2.0.1/nxtomomill/models/edf2nx/detector_section.py000066400000000000000000000021551511430602400246630ustar00rootroot00000000000000from __future__ import annotations from nxtomo.nxobject.nxdetector import FieldOfView from nxtomomill.models.utils import filter_str_def from pydantic import BaseModel, Field, field_validator, ConfigDict, field_serializer class DetectorSection(BaseModel): model_config = ConfigDict(validate_assignment=True, validate_by_name=True) field_of_view: FieldOfView | None = Field( default=None, description="Detector field of view. If set must be in `Half` or `Full`", ) @field_validator( "field_of_view", mode="plain", ) @classmethod def cast_to_field_of_view(cls, value: str | FieldOfView) -> FieldOfView | None: if value in (None, ""): return None if isinstance(value, str): value = filter_str_def(value) return FieldOfView(value) @field_serializer( "field_of_view", when_used="always", ) @classmethod def serialize_field_of_view(cls, field_of_view: FieldOfView | None) -> str: if field_of_view is None: return None else: return field_of_view.value nxtomomill-v2.0.1/nxtomomill/models/edf2nx/edf2nx.py000066400000000000000000000147611511430602400225220ustar00rootroot00000000000000# coding: utf-8 from __future__ import annotations from configparser import ConfigParser from pydantic import BaseModel, ConfigDict from ..base.FrmFlatToNestedBase import FrmFlatToNestedBase from ..base.NestedModelBase import NestedModelBase from .dark_and_flat_section import DarkAndFlatSection from .general_section import GeneralSection from .keys_section import KeysSection from .sample_section import SampleSection from .unit_section import UnitSection from .detector_section import DetectorSection from ..base.source_section import SourceSection as _SourceSection from ..base.instrument_section import InstrumentSection from nxtomomill.utils.io import deprecated_warning __all__ = ["EDF2nxModel", "generate_default_edf_config"] class _LegacySourceSection(_SourceSection, InstrumentSection): """Historically the instrument name and section was melt with the source section""" pass class EDF2nxModel( FrmFlatToNestedBase, GeneralSection, KeysSection, DarkAndFlatSection, SampleSection, _LegacySourceSection, DetectorSection, UnitSection, ): """Configuration file to run a conversion from spec-edf to NXtomo format""" model_config: ConfigDict = ConfigDict(validate_assignment=True) def to_nested_model(self) -> _NestedTomoEDFConfig: return _NestedTomoEDFConfig( general_section=GeneralSection( output_file=self.output_file, overwrite=self.overwrite, log_level=self.log_level, input_folder=self.input_folder, file_extension=self.file_extension, delete_edf_source_files=self.delete_edf_source_files, output_checks=self.output_checks, dataset_basename=self.dataset_basename, dataset_info_file=self.dataset_info_file, title=self.title, patterns_to_ignores=self.patterns_to_ignores, duplicate_data=self.duplicate_data, external_link_type=self.external_link_type, ), edf_keys_section=KeysSection( motor_position_keys=self.motor_position_keys, motor_mne_keys=self.motor_mne_keys, rotation_angle_keys=self.rotation_angle_keys, x_translation_keys=self.x_translation_keys, y_translation_keys=self.y_translation_keys, z_translation_keys=self.z_translation_keys, machine_current_keys=self.machine_current_keys, ), sample_section=SampleSection( angle_calculation_endpoint=self.angle_calculation_endpoint, force_angle_calculation=self.force_angle_calculation, angle_calculation_rev_neg_scan_range=self.angle_calculation_rev_neg_scan_range, sample_name=self.sample_name, ), source_section=_LegacySourceSection( instrument_name=self.instrument_name, source_name=self.source_name, source_type=self.source_type, source_probe=self.source_probe, ), detector_section=DetectorSection( field_of_view=self.field_of_view, ), unit_section=UnitSection( pixel_size_unit=self.pixel_size_unit, sample_detector_distance_unit=self.sample_detector_distance_unit, energy_unit=self.energy_unit, machine_current_unit=self.machine_current_unit, x_translation_unit=self.x_translation_unit, y_translation_unit=self.y_translation_unit, z_translation_unit=self.z_translation_unit, ), dark_and_flat_section=DarkAndFlatSection( dark_names_prefix=self.dark_names_prefix, flat_names_prefix=self.flat_names_prefix, ), ) @classmethod def from_cfg_file(cls, file_path: str) -> EDF2nxModel: txt_parser = ConfigParser(allow_no_value=True) txt_parser.read(file_path) def get_section(name, default={}): if txt_parser.has_section(name): return txt_parser[name] else: return default return _NestedTomoEDFConfig( general_section=GeneralSection(**get_section("GENERAL_SECTION")), edf_keys_section=KeysSection(**get_section("EDF_KEYS_SECTION")), dark_and_flat_section=DarkAndFlatSection( **get_section("DARK_AND_FLAT_SECTION") ), sample_section=SampleSection(**get_section("SAMPLE_SECTION")), unit_section=UnitSection(**get_section("UNIT_SECTION")), source_section=_LegacySourceSection(**get_section("SOURCE_SECTION")), detector_section=DetectorSection(**get_section("DETECTOR_SECTION")), ).to_flatten_config() @staticmethod def from_dict(dict_: dict) -> None: deprecated_warning( type_="function", name="from_dict", reason="replaced by pydantic 'model_dump' function", replacement="model_dump", since_version="2.0", ) dict_ = {key.lower(): value for key, value in dict_.items()} config = _NestedTomoEDFConfig(**dict_) return config.to_flatten_config() class _NestedTomoEDFConfig( BaseModel, NestedModelBase, ): """ Configuration class to provide to the convert from edf to nx """ model_config = ConfigDict(str_to_upper=True) general_section: GeneralSection = GeneralSection() edf_keys_section: KeysSection = KeysSection() dark_and_flat_section: DarkAndFlatSection = DarkAndFlatSection() sample_section: SampleSection = SampleSection() source_section: _LegacySourceSection = _LegacySourceSection() detector_section: DetectorSection = DetectorSection() unit_section: UnitSection = UnitSection() def to_flatten_config(self) -> EDF2nxModel: return EDF2nxModel( **self.general_section.model_dump(), **self.edf_keys_section.model_dump(), **self.dark_and_flat_section.model_dump(), **self.sample_section.model_dump(), **self.source_section.model_dump(), **self.detector_section.model_dump(), **self.unit_section.model_dump(), ) def generate_default_edf_config() -> dict: """generate a default configuration for converting spec-edf to NXtomo""" config = EDF2nxModel() return {key.upper(): value for key, value in config.model_dump().items()} nxtomomill-v2.0.1/nxtomomill/models/edf2nx/general_section.py000066400000000000000000000121051511430602400244630ustar00rootroot00000000000000from __future__ import annotations from nxtomomill.models.utils import convert_str_to_tuple, convert_str_to_bool from nxtomomill.settings import Tomo from nxtomomill.models.utils import PathType from nxtomomill.models.utils import filter_str_def from ..base.general_section import GeneralSection as _GeneralSectionBase from pydantic import Field, field_validator, ConfigDict, field_serializer class GeneralSection(_GeneralSectionBase): model_config: ConfigDict = ConfigDict( validate_assignment=True, validate_by_name=True, ) input_folder: str | None = Field( default=None, description="Path to the folder containing .edf files (mandatory)", ) delete_edf_source_files: bool = Field( default=False, description="Remove EDF source files after successful conversion. This operation is conditional and will only execute if the 'duplicate_data' flag is set to True. By default, 'duplicate_data' is set to True, allowing this cleanup operation to proceed", alias="delete_edf_source_file", ) output_checks: tuple[str, ...] = Field( default=tuple(), description="Perform validation checks post-conversion to ensure data integrity and accuracy." "This function accepts a list of specified tests to validate the conversion process." "Currently supported test:\n" "- 'compare-output-volume': Compares the volume of the output data to ensure it matches expected values.", ) dataset_basename: str | None = Field( default=None, description="Prefix for dataset files used to identify associated projection and info files." "If this prefix is not explicitly provided, the system will default to using the name of the input folder as the prefix." "This ensures that all related files can be accurately linked and processed to", ) dataset_info_file: str | None = Field( default=None, description="Path to the .info file containing metadata about the dataset, such as Energy, ScanRange, and TOMO_N." "If this path is not provided, the system will attempt to deduce it automatically using the dataset_basename." "This ensures that necessary metadata is accessible for further processing and analysis.", ) title: str | None = Field(default=None, description="NXtomo title") patterns_to_ignores: tuple[str, ...] = Field( default=Tomo.EDF.TO_IGNORE, description="Specifies file patterns that determine which files should be ignored during processing." "Files matching these patterns will be excluded from operations. " "This is particularly useful for filtering out unnecessary or intermediate files, such as reconstructed slice files.", ) duplicate_data: bool = Field( default=True, description="Determines if we can create files with external links or not." "If False then will create embed all the data into a single file avoiding external link to other file. " "If True then the detector data will point to original tif files. In this case you must be cautious to keep relative paths valid. Warning: to read external dataset you nust be at the hdf5 file working directory. See external link resolution details: https://support.hdfgroup.org/documentation/hdf5/latest/group___h5_l.html#title5", ) external_link_type: PathType = Field( default=PathType.RELATIVE, description="Specifies the type of file path to use for linking to original files when 'duplicate_data' is set to False." "When 'duplicate_data' is False, you can choose how file paths are referenced:" "\n- 'relative': Uses relative paths for linking to the original files." "\n- 'absolute': Uses absolute paths for linking to the original files.", alias="external_link_path", ) @field_validator( "input_folder", "dataset_basename", "dataset_info_file", "title", mode="plain", ) @classmethod def cast_to_str(cls, value: str | None) -> tuple | None: return filter_str_def(value) @field_validator( "patterns_to_ignores", "output_checks", mode="plain", ) @classmethod def cast_to_tuple(cls, value: tuple[str, ...] | str | None) -> tuple[str]: if isinstance(value, tuple): return value value = filter_str_def(value) return convert_str_to_tuple(value) or () @field_validator( "external_link_type", ) @classmethod def cast_to_path_type(cls, value: str | PathType) -> PathType: if isinstance(value, str): value = filter_str_def(value).lower() return PathType(value) @field_serializer( "external_link_type", ) @classmethod def serialize_external_link_type(cls, value: PathType): return value.value @field_validator( "delete_edf_source_files", mode="plain", ) @classmethod def cast_to_bool(cls, value: bool | str) -> bool: if isinstance(value, str): value = filter_str_def(value) return convert_str_to_bool(value) nxtomomill-v2.0.1/nxtomomill/models/edf2nx/keys_section.py000066400000000000000000000040101511430602400240150ustar00rootroot00000000000000from __future__ import annotations from nxtomomill.settings import Tomo from nxtomomill.models.utils import convert_str_to_tuple from pydantic import BaseModel, Field, field_validator, ConfigDict class KeysSection(BaseModel): model_config = ConfigDict(validate_assignment=True, validate_by_name=True) motor_position_keys: tuple[str, ...] = Field( default=Tomo.EDF.MOTOR_POS, description="Motor position key", alias="motor_position_key", ) motor_mne_keys: tuple[str, ...] = Field( default=Tomo.EDF.MOTOR_MNE, description="key to used for reading indices of each motor", alias="motor_mne_key", ) rotation_angle_keys: tuple[str, ...] = Field( default=Tomo.EDF.ROT_ANGLE, description="Keys to be used for reading rotation angle", alias="rot_angle_key", ) x_translation_keys: tuple[str, ...] = Field( default=Tomo.EDF.X_TRANS, description="Keys to be used for reading x translation", alias="x_translation_key", ) y_translation_keys: tuple[str, ...] = Field( default=Tomo.EDF.Y_TRANS, description="Keys to be used for reading y translation", alias="y_translation_key", ) z_translation_keys: tuple[str, ...] = Field( default=Tomo.EDF.Z_TRANS, description="Keys to be used for reading z translation", alias="z_translation_key", ) machine_current_keys: tuple[str, ...] = Field( default=Tomo.EDF.MACHINE_CURRENT, description="Keys to be used for reading the machine current", ) @field_validator( "motor_position_keys", "motor_mne_keys", "rotation_angle_keys", "x_translation_keys", "y_translation_keys", "z_translation_keys", "machine_current_keys", mode="plain", ) @classmethod def cast_to_tuple(cls, value: str | tuple[str, ...]) -> tuple[str,]: if isinstance(value, tuple): return value return convert_str_to_tuple(value) or () nxtomomill-v2.0.1/nxtomomill/models/edf2nx/sample_section.py000066400000000000000000000031451511430602400243330ustar00rootroot00000000000000from __future__ import annotations from pydantic import BaseModel, Field, ConfigDict, field_validator from nxtomomill.models.utils import filter_str_def, convert_str_to_bool class SampleSection(BaseModel): model_config = ConfigDict(validate_assignment=True, validate_by_name=True) sample_name: str | None = Field(default=None, description="Name of the sample") force_angle_calculation: bool = Field( default=True, description="Determines the method for obtaining rotation angles." "Options:" "* True: Compute from scan range: Uses `numpy.linspace` to generate rotation angles based on the provided scan range." "* False: Load from `.edf` header: Attempts to read the rotation angles directly from the `.edf` file header.", ) angle_calculation_endpoint: bool = Field( default=False, description="Specifies the endpoint behavior for `numpy.linspace` when calculating rotation angles." "", ) angle_calculation_rev_neg_scan_range: bool = Field( default=True, description="Inverts the rotation angle values when the `ScanRange` is negative.", ) @field_validator( "sample_name", mode="plain", ) @classmethod def cast_to_sample_name(cls, value: str | None) -> str | None: return filter_str_def(value) @field_validator( "angle_calculation_endpoint", "force_angle_calculation", "angle_calculation_rev_neg_scan_range", mode="plain", ) @classmethod def cast_to_bool(cls, value: bool | str) -> bool: return convert_str_to_bool(value) nxtomomill-v2.0.1/nxtomomill/models/edf2nx/unit_section.py000066400000000000000000000074771511430602400240450ustar00rootroot00000000000000from __future__ import annotations import pint from nxtomo.nxobject.nxdetector import FieldOfView from nxtomomill.utils.io import deprecated from pydantic import BaseModel, Field, field_validator, field_serializer, ConfigDict _ureg = pint.get_application_registry() class UnitSection(BaseModel): model_config = ConfigDict( validate_assignment=True, arbitrary_types_allowed=True, validate_by_name=True ) pixel_size_unit: pint.Unit = Field( default_factory=lambda: _ureg.micrometer, description="Unit used to save pixel size. Must be in of ['nanometer', 'micrometer', 'millimeter', 'centimeter', 'meter']", alias="expected_unit_for_pixel_size", ) sample_detector_distance_unit: pint.Unit | None = Field( default_factory=lambda: _ureg.millimeter, description="Unit used by SPEC to save sample to detector distance. Must be in of ['nanometer', 'micrometer', 'millimeter', 'centimeter', 'meter']", alias="expected_unit_for_distance", ) energy_unit: pint.Unit | None = Field( default_factory=lambda: _ureg.keV, description="Unit used by SPEC to save energy. Must be in of ['joule', 'electron_volt', 'kiloelectron_volt', 'millielectron_volt', 'gigaelectron_volt', 'kilojoule']", alias="expected_unit_for_energy", ) x_translation_unit: pint.Unit | None = Field( default_factory=lambda: _ureg.millimeter, description="Unit used by SPEC to save x translation. Must be in of ['nanometer', 'micrometer', 'millimeter', 'centimeter', 'meter']", alias="expected_unit_for_x_translation", ) y_translation_unit: pint.Unit | None = Field( default_factory=lambda: _ureg.millimeter, description="Unit used by SPEC to save y translation. Must be in of ['nanometer', 'micrometer', 'millimeter', 'centimeter', 'meter']", alias="expected_unit_for_y_translation", ) z_translation_unit: pint.Unit | None = Field( default_factory=lambda: _ureg.millimeter, description="Unit used by SPEC to save z translation. Must be in of ['nanometer', 'micrometer', 'millimeter', 'centimeter', 'meter']", alias="expected_unit_for_z_translation", ) machine_current_unit: pint.Unit | None = Field( default_factory=lambda: _ureg.mA, description="Unit used by SPEC to save machine current (also aka SRcurrent). Must be in of ['ampere', 'kiloampere', 'milliampere']", alias="expected_unit_for_machine_current", ) @field_validator( "pixel_size_unit", "sample_detector_distance_unit", "energy_unit", "x_translation_unit", "y_translation_unit", "z_translation_unit", "machine_current_unit", mode="before", ) @classmethod def cast_to_pint_unit(cls, value: str | FieldOfView) -> pint.Unit | None: if value in ("kiloelectron_volt", "kev"): return _ureg.keV if value == "milliampere": return _ureg.mA return pint.Unit(value) @field_serializer( "pixel_size_unit", "sample_detector_distance_unit", "energy_unit", "x_translation_unit", "y_translation_unit", "z_translation_unit", "machine_current_unit", when_used="always", ) @classmethod def serialized_unit(cls, value) -> str: return f"{value}" @property @deprecated( reason="renamed", replacement="sample_detector_distance_unit", since_version="2.0", ) def distance_unit(self): return self.sample_detector_distance_unit @distance_unit.setter @deprecated( reason="renamed", replacement="sample_detector_distance_unit", since_version="2.0", ) def distance_unit(self, unit): self.sample_detector_distance_unit = unit UnitSection.model_rebuild() nxtomomill-v2.0.1/nxtomomill/models/fluo2nx/000077500000000000000000000000001511430602400211605ustar00rootroot00000000000000nxtomomill-v2.0.1/nxtomomill/models/fluo2nx/__init__.py000066400000000000000000000001701511430602400232670ustar00rootroot00000000000000from .fluo2nx import Fluo2nxModel # noqa F401,F403 from .fluo2nx import generate_default_fluo_config # noqa F401,F403 nxtomomill-v2.0.1/nxtomomill/models/fluo2nx/fluo2nx.py000066400000000000000000000070361511430602400231350ustar00rootroot00000000000000# coding: utf-8 from __future__ import annotations from configparser import ConfigParser from pydantic import BaseModel, ConfigDict from ..base.FrmFlatToNestedBase import FrmFlatToNestedBase from ..base.NestedModelBase import NestedModelBase from ..base.source_section import SourceSection as _SourceSection from ..base.instrument_section import InstrumentSection from .general_section import GeneralSection from nxtomomill.utils.io import deprecated_warning __all__ = ["Fluo2nxModel", "generate_default_fluo_config"] class _LegacySourceSection(_SourceSection, InstrumentSection): """Historically the instrument name and section was melt with the source section""" pass class Fluo2nxModel(FrmFlatToNestedBase, GeneralSection, _LegacySourceSection): """ Configuration class to provide to the fluo->nx converter . """ model_config = ConfigDict(validate_assignment=True) def to_nested_model(self) -> _NestedTomoFluoConfig: return _NestedTomoFluoConfig( general_section=GeneralSection( output_file=self.output_file, dataset_basename=self.dataset_basename, dataset_info_file=self.dataset_info_file, detector_names=self.detector_names, dimension=self.dimension, duplicate_data=self.duplicate_data, file_extension=self.file_extension, input_folder=self.input_folder, log_level=self.log_level, overwrite=self.overwrite, patterns_to_ignores=self.patterns_to_ignores, ), source_section=_LegacySourceSection( instrument_name=self.instrument_name, source_name=self.source_name, source_probe=self.source_probe, source_type=self.source_type, ), ) @classmethod def from_cfg_file(cls, file_path: str) -> Fluo2nxModel: txt_parser = ConfigParser(allow_no_value=True) txt_parser.read(file_path) def get_section(name, default={}): if txt_parser.has_section(name): return txt_parser[name] else: return default return _NestedTomoFluoConfig( general_section=GeneralSection(**get_section("GENERAL_SECTION")), source_section=_LegacySourceSection(**get_section("SOURCE_SECTION")), ).to_flatten_config() @staticmethod def from_dict(dict_: dict) -> None: deprecated_warning( type_="function", name="from_dict", reason="replaced by pydantic 'model_dump' function", replacement="model_dump", since_version="2.0", ) dict_ = {key.lower(): value for key, value in dict_.items()} config = _NestedTomoFluoConfig(**dict_) return config.to_flatten_config() class _NestedTomoFluoConfig(BaseModel, NestedModelBase): model_config = ConfigDict(str_to_upper=True) general_section: GeneralSection = GeneralSection() source_section: _LegacySourceSection = _LegacySourceSection() def to_flatten_config(self) -> Fluo2nxModel: return Fluo2nxModel( **self.general_section.model_dump(), **self.source_section.model_dump(), ) def generate_default_fluo_config(level: str = "required") -> dict: """generate a default configuration to convert fluo-tomo data (after PyMCA fit) to NXtomo""" config = Fluo2nxModel().to_nested_model() return {key.upper(): value for key, value in config.model_dump().items()} nxtomomill-v2.0.1/nxtomomill/models/fluo2nx/general_section.py000066400000000000000000000052631511430602400247010ustar00rootroot00000000000000from __future__ import annotations from nxtomomill.models.utils import convert_str_to_tuple, filter_str_def from ..base.general_section import GeneralSection as _GeneralSectionBase from pydantic import Field, field_validator, ConfigDict class GeneralSection(_GeneralSectionBase): model_config = ConfigDict(validate_assignment=True, validate_by_name=True) input_folder: str | None = Field( default=None, description="Path to the folder containing the raw data folder and the fluofit/ subfolder. if not provided from the configuration file must be provided from the command line", ) dimension: int = Field( default=3, description="Dimension of the experiment. 2 for 2D XRFCT, 3 for 3D XRFCT. Default is 3.", ) detector_names: tuple[str, ...] = Field( default=tuple(), description="Define a list of (real or virtual) detector names used for the exp (space separated values - no comma). E.g. 'falcon xmap'. If not specified, all detectors are processed.", ) dataset_basename: str | None = Field( default=None, description="In 2D, the exact full name of the folder. In 3D, the folder name prefix (the program will search for folders named _XXX where XXX is a nmber.) If not provided will take the name of input_folder", ) dataset_info_file: str | None = Field( default=None, description="Path to .info file containing dataset information (Energy, ScanRange, TOMO_N...). If not will deduce it from dataset_basename", ) patterns_to_ignores: tuple[str, ...] = Field( default=("_slice_",), description="Some file pattern leading to ignoring the file. Like reconstructed slice files.", ) duplicate_data: bool = Field( default=True, description="If False then will create embed all the data into a single file avoiding external link to other file. If True then the decetor data will point to original tif files. In this case you must be carreful to keep relative paths valid. Warning: to read external dataset you nust be at the hdf5 file working directory. See external link resolution details: https://support.hdfgroup.org/documentation/hdf5/latest/group___h5_l.html#title5", ) @field_validator( "detector_names", "patterns_to_ignores", mode="before", ) @classmethod def cast_to_tuple(cls, value: str | None) -> tuple[str]: return convert_str_to_tuple(value) or () @field_validator( "input_folder", "dataset_basename", "dataset_info_file", mode="plain", ) @classmethod def cast_to_str(cls, value: str | None) -> tuple | None: return filter_str_def(value) nxtomomill-v2.0.1/nxtomomill/models/h52nx/000077500000000000000000000000001511430602400205275ustar00rootroot00000000000000nxtomomill-v2.0.1/nxtomomill/models/h52nx/FrameGroup.py000066400000000000000000000222061511430602400231520ustar00rootroot00000000000000from __future__ import annotations from enum import Enum from pydantic import ( BaseModel, field_validator, field_serializer, ConfigDict, Field, ) from silx.io.url import DataUrl from ._acquisitionstep import AcquisitionStep from nxtomomill.models.utils import filter_str_def from nxtomomill.models.utils import ( remove_parenthesis_or_brackets, convert_str_to_bool, ) class FrameGroup(BaseModel): model_config = ConfigDict( arbitrary_types_allowed=True, validate_assignment=True, validate_by_name=True ) url: DataUrl | None = None frame_type: AcquisitionStep = AcquisitionStep.PROJECTION copy_data: bool | None = Field( default=None, description="Should the frame dataset be copied or not. If not set will fallback on the 'frame_type_section.default_data_copy'", ) class Info(Enum): URL_ENTRY = "entry" FRAME_TYPE = "frame_type" COPY = "copy" @field_validator( "frame_type", mode="plain", ) @classmethod def cast_to_AcquisitionStep(cls, value: str | AcquisitionStep) -> AcquisitionStep: return AcquisitionStep.from_value(value) @field_validator( "copy_data", mode="plain", ) @classmethod def cast_to_copy_data(cls, value: bool | None) -> bool | None: if value in (None, "", "None"): return None else: return convert_str_to_bool(value) @field_validator( "url", mode="plain", ) @classmethod def cast_to_url(cls, value: str | DataUrl | None) -> DataUrl: if value is None: return value elif isinstance(value, DataUrl): return value else: if "path=" not in value: # by default we expect the user to only give an entry on the file. So only the dataset path return DataUrl(data_path=value) else: return DataUrl(path=value) @field_serializer( "url", when_used="always", ) @classmethod def serialize_data_url(cls, value: DataUrl | None) -> str: if value is None: return "" elif value.file_path() is None: return value.data_path() else: return value.path() @staticmethod def frm_str(input_str: str) -> FrameGroup: """ Create an instance of FrameGroup from it string representation. """ if not isinstance(input_str, str): raise TypeError(f"{input_str} should be a string") input_str = remove_parenthesis_or_brackets(input_str) elmts = input_str.split(",") elmts = filter(None, [elmt.lstrip(" ").rstrip(" ") for elmt in elmts]) cst_inputs = {} for elmt in elmts: try: info_type, value = FrameGroup._treat_elmt(elmt) except ValueError: url_example = DataUrl( file_path="/path/to/my/file/file.h5", data_path="/data/path", scheme="h5py", ) _example_frame = FrameGroup( url=url_example, copy_data=True, frame_type="projection" ) err_msg = ( f"Unable to interpret string ('{elmt}'). Please insure this is a " "either a frame type, a boolean for copy or an entry " "(DataUrl).\n " "Please prefix the value by the information type like: " f"{_example_frame}. Invalid element is {input_str}" ) raise ValueError(err_msg) else: cst_inputs[info_type] = value inputs = {} if FrameGroup.Info.FRAME_TYPE not in cst_inputs: raise ValueError(f"Unable to find frame type from {input_str}") else: inputs["frame_type"] = cst_inputs[FrameGroup.Info.FRAME_TYPE] if FrameGroup.Info.URL_ENTRY not in cst_inputs: raise ValueError(f"Unable to find entry from {input_str}") else: inputs["url"] = cst_inputs[FrameGroup.Info.URL_ENTRY] if FrameGroup.Info.COPY in cst_inputs: inputs["copy_data"] = cst_inputs[FrameGroup.Info.COPY] if "copy_data" in inputs: inputs["copy_data"] = inputs["copy_data"] return FrameGroup(**inputs) @staticmethod def _treat_elmt(elmt: str) -> tuple[str, str]: assert isinstance(elmt, str) # try to interpret it as a Frame group info for info in FrameGroup.Info: key = f"{info.value}=" if elmt.startswith(key): return info, filter_str_def(elmt.replace(key, "", 1)) # try to interpret it as an acquisition step elmt = filter_str_def(elmt) try: acquisition_step = AcquisitionStep.from_value(elmt) except ValueError: pass else: return FrameGroup.Info.FRAME_TYPE, acquisition_step # try to interpret it as a boolean # is this a copy element if elmt.startswith(("copy=", "copy_data=")): return FrameGroup.Info.COPY, elmt.split("=")[1] if elmt in ("True", "true"): return FrameGroup.Info.COPY, True if elmt in ("False", "false"): return FrameGroup.Info.COPY, False try: elmt = filter_str_def(elmt) DataUrl(path=elmt) except ValueError: pass else: return FrameGroup.Info.URL_ENTRY, elmt raise ValueError def __str__(self) -> str: return self.str_representation( only_data_path=False, with_copy=True, with_prefix_key=True ) def str_representation( self, only_data_path: bool, with_copy: bool, with_prefix_key: bool ) -> str: """ Util function to print the possible input string for this FrameGroup. :param only_data_path: if True consider the input file frame group is contained in the input file and the string representing the url can be only the data path :param with_copy: if true display the copy information :param with_prefix_key: if true provide the string with the keys as prefix (frame_type=XXX, copy=...) """ if self.url is None: url_str = "" elif only_data_path or self.url.file_path() is None: url_str = self.url.data_path() else: url_str = self.url.path() if with_prefix_key: if with_copy: return "({ft_key}={frame_type}, {url_key}={url}, {copy_key}={copy})".format( ft_key=self.Info.FRAME_TYPE.value, frame_type=self.frame_type.value, url_key=self.Info.URL_ENTRY.value, url=url_str, copy_key=self.Info.COPY.value, copy=self.copy_data, ) else: return "({ft_key}={frame_type}, {url_key}={url})".format( ft_key=self.Info.FRAME_TYPE.value, frame_type=self.frame_type.value, url_key=self.Info.URL_ENTRY.value, url=url_str, ) else: if with_copy: return "({frame_type}, {url}, {copy})".format( frame_type=self.frame_type.value, url=url_str, copy=self.copy_data, ) else: return "({frame_type}, {url})".format( frame_type=self.frame_type.value, url=url_str, ) def filter_acqui_frame_type( init: FrameGroup, sequences: tuple, frame_type: AcquisitionStep ) -> tuple: """compute the list of urls representing projections from init until the next Initialization step :param init: frame group creating the beginning of the acquisition sequence :param sequences: list of FrameGroup representing the sequence :param frame_type: type of frame to filer (cannot be Initialization step) """ frame_type = AcquisitionStep.from_value(frame_type) if frame_type is AcquisitionStep.INITIALIZATION: raise ValueError(f"{AcquisitionStep.INITIALIZATION.value} is not handled") if init not in sequences: raise ValueError(f"{init} cannot be find in the provided sequence") frame_types = [frm_grp.frame_type for frm_grp in sequences] current_acqui_idx = sequences.index(init) if len(sequences) == current_acqui_idx - 1: # in case the initialization sequence is the last element of the # sequence (if people make strange stuff...) return () sequence_target = sequences[current_acqui_idx + 1 :] frame_types = frame_types[current_acqui_idx + 1 :] try: next_acqui = frame_types.index(AcquisitionStep.INITIALIZATION) - 1 except ValueError: next_acqui = -1 sequence_target = sequence_target[:next_acqui] filter_fct = lambda a: a.frame_type is frame_type return tuple(filter(filter_fct, sequence_target)) nxtomomill-v2.0.1/nxtomomill/models/h52nx/__init__.py000066400000000000000000000001601511430602400226350ustar00rootroot00000000000000from .h52nx import H52nxModel # noqa F401,F403 from .h52nx import generate_default_h5_config # noqa F401,F403 nxtomomill-v2.0.1/nxtomomill/models/h52nx/_acquisitionstep.py000066400000000000000000000042361511430602400244710ustar00rootroot00000000000000# coding: utf-8 """ contains the FrameGroup """ from enum import Enum as _Enum from nxtomo.nxobject.nxdetector import ImageKey __all__ = [ "AcquisitionStep", ] class AcquisitionStep(_Enum): # Warning: order of acquisition step should be same as H5ScanTitles INITIALIZATION = "initialization" DARK = "darks" FLAT = "flats" PROJECTION = "projections" ALIGNMENT = "alignment projections" @classmethod def from_value(cls, value): if isinstance(value, str): value = value.lower() if value in ("init", "initialization"): value = AcquisitionStep.INITIALIZATION elif value in ("dark", "darks"): value = AcquisitionStep.DARK elif value in ("reference", "flat", "flats", "ref", "refs", "references"): value = AcquisitionStep.FLAT elif value in ("proj", "projection", "projs", "projections"): value = AcquisitionStep.PROJECTION elif value in ( "alignment", "alignments", "alignment projection", "alignment projections", ): value = AcquisitionStep.ALIGNMENT return AcquisitionStep(value) def to_image_key(self): if self is AcquisitionStep.PROJECTION: return ImageKey.PROJECTION elif self is AcquisitionStep.ALIGNMENT: return ImageKey.PROJECTION elif self is AcquisitionStep.DARK: return ImageKey.DARK_FIELD elif self is AcquisitionStep.FLAT: return ImageKey.FLAT_FIELD else: raise ValueError(f"The step {self.value} does not fit any AcquisitionStep") def to_image_key_control(self): if self is AcquisitionStep.PROJECTION: return ImageKey.PROJECTION elif self is AcquisitionStep.ALIGNMENT: return ImageKey.ALIGNMENT elif self is AcquisitionStep.DARK: return ImageKey.DARK_FIELD elif self is AcquisitionStep.FLAT: return ImageKey.FLAT_FIELD else: raise ValueError(f"The step {self.value} does not fit any AcquisitionStep") nxtomomill-v2.0.1/nxtomomill/models/h52nx/entries_and_title_section.py000066400000000000000000000111371511430602400263240ustar00rootroot00000000000000from __future__ import annotations from pydantic import BaseModel, Field, field_validator, ConfigDict from nxtomomill.settings import Tomo from nxtomomill.models.utils import convert_str_to_tuple, is_url_path, filter_str_def from silx.io.url import DataUrl class EntriesAndTitlesSection(BaseModel): model_config = ConfigDict( validate_assignment=True, arbitrary_types_allowed=True, validate_by_name=True ) entries: tuple[DataUrl, ...] = Field( default=tuple(), description="List of root/init entries to convert" ) sub_entries_to_ignore: tuple[DataUrl, ...] = Field( default=tuple(), description="List of sub entries to ignore" ) init_titles: tuple[str, ...] = Field( default=Tomo.H5.INIT_TITLES, description="Titles for initialization" ) zseries_init_titles: tuple[str, ...] = Field( default=Tomo.H5.ZSERIE_INIT_TITLES, description="Titles for z-serie initialization", alias="zserie_init_titles", ) multitomo_init_titles: tuple[str, ...] = Field( default=Tomo.H5.MULTITOMO_INIT_TITLES, description="Titles for multi-tomo initialization", ) back_and_forth_init_titles: tuple[str, ...] = Field( default=Tomo.H5.BACK_AND_FORTH_INIT_TITLES, description="Titles for back-and-forth initialization", ) dark_titles: tuple[str, ...] = Field( default=Tomo.H5.DARK_TITLES, description="Titles to determine if the bliss scan is a set of dark", ) flat_titles: tuple[str, ...] = Field( default=Tomo.H5.FLAT_TITLES, description="Titles to determine if the bliss scan is a set of flat", ) projection_titles: tuple[str, ...] = Field( default=Tomo.H5.PROJ_TITLES, description="Titles to determine if the bliss scan is a set of projection", alias="proj_titles", ) alignment_titles: tuple[str, ...] = Field( default=Tomo.H5.ALIGNMENT_TITLES, description="Titles to determine if the bliss scan is a set of alignment projection", ) @field_validator( "init_titles", "zseries_init_titles", "multitomo_init_titles", "back_and_forth_init_titles", "dark_titles", "flat_titles", "projection_titles", "alignment_titles", mode="plain", ) @classmethod def cast_entries_or_title_to_tuple(cls, value: str | tuple[str]) -> tuple[str, ...]: if isinstance(value, str): value = filter_str_def(value) return convert_str_to_tuple(value) or () @field_validator( "entries", "sub_entries_to_ignore", mode="before", ) @classmethod def cast_entries_or_title_to_tuple_of_url( cls, value: str | tuple[DataUrl | str, ...] ) -> tuple[DataUrl, ...]: """The entries are first converted from a string to a list of string. Then this list of string will be processed later to create a list of DataUrl with relative link and with a context """ if isinstance(value, str): value = filter_str_def(value) value = convert_str_to_tuple(value) entries = EntriesAndTitlesSection.parse_frame_urls(value) return tuple( [EntriesAndTitlesSection.fix_entry_name(entry) for entry in entries] ) @staticmethod def parse_frame_urls(urls: tuple[str | DataUrl, ...]): """ Insure urls is None or a list of valid DataUrl """ if urls in ("", None): return tuple() res = [] for i_url, url in enumerate(urls): if isinstance(url, str): if url == "": continue elif is_url_path(url): url = DataUrl(path=url) else: url = DataUrl(data_path=url, scheme="silx") if not isinstance(url, DataUrl): raise ValueError( "urls tuple should contains DataUrl. " f"Not {type(url)} at index {i_url}" ) else: res.append(url) return tuple(res) @staticmethod def fix_entry_name(entry: DataUrl): """simple util function to insure the entry start by a "/""" if not isinstance(entry, DataUrl): raise TypeError("entry is expected to be a DataUrl") if not entry.data_path().startswith("/"): entry = DataUrl( scheme=entry.scheme(), data_slice=entry.data_slice(), file_path=entry.file_path(), data_path="/" + entry.data_path(), ) return entry nxtomomill-v2.0.1/nxtomomill/models/h52nx/extra_params_section.py000066400000000000000000000030131511430602400253100ustar00rootroot00000000000000from __future__ import annotations from pydantic import BaseModel, Field, field_validator, ConfigDict class ExtraParamsSection(BaseModel): model_config = ConfigDict(validate_assignment=True, validate_by_name=True) energy_kev: float | None = Field(default=None, description="Energy in keV") x_sample_pixel_size_m: float | None = Field( default=None, description="X sample pixel size in meters" ) y_sample_pixel_size_m: float | None = Field( default=None, description="Y sample pixel size in meters" ) x_detector_pixel_size_m: float | None = Field( default=None, description="X detector pixel size in meters" ) y_detector_pixel_size_m: float | None = Field( default=None, description="Y detector pixel size in meters" ) detector_sample_distance_m: float | None = Field( default=None, description="Detector sample distance in meters" ) source_sample_distance_m: float | None = Field( default=None, description="Source sample distance in meters" ) @field_validator( "energy_kev", "x_sample_pixel_size_m", "y_sample_pixel_size_m", "x_detector_pixel_size_m", "y_detector_pixel_size_m", "detector_sample_distance_m", "source_sample_distance_m", mode="plain", ) @classmethod def cast_extra_params_to_scalar_value_or_None(cls, value: str) -> float | None: if value in ("", None, "None"): return None else: return float(value) nxtomomill-v2.0.1/nxtomomill/models/h52nx/frame_type_section.py000066400000000000000000000072021511430602400247610ustar00rootroot00000000000000from __future__ import annotations import re import logging from pydantic import BaseModel, Field, field_validator, ConfigDict, field_serializer from nxtomomill.models.h52nx.FrameGroup import FrameGroup from nxtomomill.models.utils import remove_parenthesis_or_brackets from nxtomomill.utils.io import deprecated _logger = logging.getLogger(__name__) class FrameTypeSection(BaseModel): model_config = ConfigDict(validate_assignment=True, validate_by_name=True) data_scans: tuple[FrameGroup, ...] = Field( default=tuple(), description="List of scans to be converted." "Frame type should be provided for each scan. Expected format is:" " * `frame_type` (mandatory): values can be `projection`, `flat`, `dark`, `alignment` or `init`." " * `entry` (mandatory): DataUrl with path to the scan to integrate. If the scan is contained in the input_file then you can only provide path/name of the scan." " * `copy` (optional): you can provide a different behavior for the this scan (should we duplicate data or not)", ) default_data_copy: bool = Field( default=False, description="Duplicate data inside the input file or create a relative link", ) @property @deprecated(reason="renamed", since_version="2.0", replacement="data_scans") def data_frame_grps(self): return self.data_scans @data_frame_grps.setter @deprecated(reason="renamed", since_version="2.0", replacement="data_scans") def data_frame_grps(self, values) -> None: self.data_scans = values @field_validator( "data_scans", mode="plain", ) @classmethod def cast_to_data_scan( cls, value: str | tuple[FrameGroup, ...] ) -> tuple[FrameGroup, ...]: if isinstance(value, tuple): return value return convert_str_to_frame_grp(value) @field_serializer( "data_scans", when_used="always", ) @classmethod def serialize_data_scans(cls, urls: tuple[FrameGroup, ...]) -> str: if len(urls) == 0: return "" else: urls_str = ",\n".join([f"{frame_group}" for frame_group in urls]) return f"""( {urls_str} ) """ def convert_str_to_frame_grp(input_str: str) -> tuple[FrameGroup, ...]: """ Convert a list such as: .. code-block:: text urls = ( (frame_type=dark, entry="silx:///file.h5?data_path=/dark", copy=True), (frame_type=flat, entry="silx:///file.h5?data_path=/flat"), (frame_type=projection, entry="silx:///file.h5?data_path=/flat"), (frame_type=projection, entry="silx:///file.h5?data_path=/flat"), ) to a tuple of FrameGroup """ result = [] if not isinstance(input_str, str): raise TypeError( f"input_str should be an instance of str. Got {type(input_str)}" ) # remove spaces at the beginning and at the end input_str = input_str.replace("\n", "") input_str = input_str.lstrip(" ").rstrip(" ") input_str = remove_parenthesis_or_brackets(input_str) # special case when the ")" is given in a line and ignored by configparser input_str = input_str.replace("((", "(") # split sub entries re_expr = r"\([^\)]*\)" frame_grp_str_list = re.findall(re_expr, input_str) for frame_grp_str in frame_grp_str_list: try: frame_grp = FrameGroup.frm_str(frame_grp_str) except Exception as e: _logger.error( f"Unable to create a valid entry from {frame_grp_str}. Error is {e}" ) else: result.append(frame_grp) return tuple(result) nxtomomill-v2.0.1/nxtomomill/models/h52nx/general_section.py000066400000000000000000000112031511430602400242370ustar00rootroot00000000000000from __future__ import annotations from pydantic import Field, field_serializer, field_validator, ConfigDict from nxtomo.nxobject.nxdetector import FieldOfView from nxtomomill.models.utils import filter_str_def, convert_str_to_bool from ..base.general_section import GeneralSection as _GeneralSectionBase class GeneralSection(_GeneralSectionBase): model_config = ConfigDict(validate_assignment=True, validate_by_name=True) input_file: str | None = Field(default=None, description="Input file path.") raises_error: bool = Field( default=False, description="Raise an error when encountered." ) no_input: bool = Field( default=False, description="Ask for user inputs if missing information." ) single_file: bool = Field( default=False, description="Create a single file for all sequences." ) no_master_file: bool = Field( default=True, description="Avoid creating the master file." ) ignore_bliss_tomo_config: bool = Field( default=False, description="Ignore Bliss tomography group. On recent bliss file (2023) a dedicated group specify datasets to be used for tomography. Defining for example translations, rotation, etc. If True then this group will be ignored and conversion will fallback on using path list provided in the KEYS section.", ) field_of_view: FieldOfView | None = Field( default=None, description="Force output to be a 'Full' or 'Half' acquisition. Else determine from existing metadata", ) create_control_data: bool = Field( default=True, description="Generate control/data - filled with machine current." ) check_tomo_n: bool | None = Field( default=None, description="Check 'tomo_n' metadata once the conversion is done. By default will be done for all except scan build from 'scan_data'.", ) rotation_is_clockwise: bool | None = Field( default=None, description="Force rotation clockwise or not. If not set will read the information from the bliss-tomo 'tomoconfig' group. Else consider rotation is counterclockwise.", ) mechanical_lr_flip: bool = Field( default=False, description="Detector image is flipped **left-right** for mechanical reasons that are not propagated with bliss-tomo hdf5 metadata (mirror,..).", ) mechanical_ud_flip: bool = Field( default=False, description="Detector image is flipped **up-down** for mechanical reasons that are not propagated with bliss-tomo hdf5 metadata (mirror,..).", ) @property def request_input(self) -> bool: return not self.no_input @request_input.setter def request_input(self, request: bool): self.no_input = request @field_validator( "check_tomo_n", "rotation_is_clockwise", mode="before", ) @classmethod def cast_general_section_to_bool_or_none( cls, value: str | None | bool ) -> bool | None: if isinstance(value, str): value = filter_str_def(value) if value in (None, "None", ""): return None elif value in ("True", True, "1"): return True elif value in ("False", False, "0"): return False else: raise ValueError @field_serializer( "check_tomo_n", "rotation_is_clockwise", when_used="always", ) @classmethod def serialize_check_tomo_n(cls, value: None | bool) -> str: if value is None: return None else: return value @field_validator( "field_of_view", mode="plain", ) @classmethod def cast_to_field_of_view(cls, value: str | FieldOfView) -> FieldOfView | None: if value in (None, ""): return None return FieldOfView(value) @field_serializer( "field_of_view", when_used="always", ) @classmethod def serialize_field_of_view(cls, field_of_view: FieldOfView | None) -> str: if field_of_view is None: return None else: return field_of_view.value @field_validator( "input_file", mode="before", ) @classmethod def cast_general_section_to_str(cls, value: str | None) -> str | None: return filter_str_def(value) @field_validator( "no_input", "single_file", "no_master_file", "ignore_bliss_tomo_config", "create_control_data", "mechanical_lr_flip", "mechanical_ud_flip", mode="before", ) @classmethod def cast_general_section_to_bool(cls, value: str | bool | None) -> bool: return convert_str_to_bool(value) nxtomomill-v2.0.1/nxtomomill/models/h52nx/h52nx.py000066400000000000000000000203141511430602400220450ustar00rootroot00000000000000# coding: utf-8 from __future__ import annotations from pydantic import BaseModel, ConfigDict from configparser import ConfigParser from ..base.NestedModelBase import NestedModelBase from ..base.FrmFlatToNestedBase import FrmFlatToNestedBase from ..base.instrument_section import InstrumentSection from .general_section import GeneralSection from .keys_section import KeysSection from .entries_and_title_section import EntriesAndTitlesSection from .frame_type_section import FrameTypeSection from .multitomo_section import MultiTomoSection from .extra_params_section import ExtraParamsSection from nxtomomill.utils.io import deprecated_warning, deprecated __all__ = [ "H52nxModel", "generate_default_h5_config", ] class H52nxModel( FrmFlatToNestedBase, GeneralSection, KeysSection, EntriesAndTitlesSection, FrameTypeSection, MultiTomoSection, ExtraParamsSection, InstrumentSection, ): """Configuration file to run a conversion from bliss-hdf5 file to NXtomo format""" model_config: ConfigDict = ConfigDict(validate_assignment=True) def to_nested_model(self) -> _NestedTomoHDF5Config: return _NestedTomoHDF5Config( general_section=GeneralSection( output_file=self.output_file, overwrite=self.overwrite, log_level=self.log_level, input_file=self.input_file, file_extension=self.file_extension, raises_error=self.raises_error, no_input=self.no_input, single_file=self.single_file, no_master_file=self.no_master_file, ignore_bliss_tomo_config=self.ignore_bliss_tomo_config, field_of_view=self.field_of_view, rotation_is_clockwise=self.rotation_is_clockwise, create_control_data=self.create_control_data, check_tomo_n=self.check_tomo_n, mechanical_lr_flip=self.mechanical_lr_flip, mechanical_ud_flip=self.mechanical_ud_flip, ), keys_section=KeysSection( valid_camera_names=self.valid_camera_names, rotation_angle_keys=self.rotation_angle_keys, sample_x_keys=self.sample_x_keys, sample_y_keys=self.sample_y_keys, translation_y_keys=self.translation_y_keys, translation_z_keys=self.translation_z_keys, diode_keys=self.diode_keys, exposure_time_keys=self.exposure_time_keys, sample_x_pixel_size_keys=self.sample_x_pixel_size_keys, sample_y_pixel_size_keys=self.sample_y_pixel_size_keys, detector_x_pixel_size_keys=self.detector_x_pixel_size_keys, detector_y_pixel_size_keys=self.detector_y_pixel_size_keys, sample_detector_distance_keys=self.sample_detector_distance_keys, source_sample_distance_keys=self.source_sample_distance_keys, machine_current_keys=self.machine_current_keys, ), entries_and_titles_section=EntriesAndTitlesSection( entries=self.entries, sub_entries_to_ignore=self.sub_entries_to_ignore, init_titles=self.init_titles, zseries_init_titles=self.zseries_init_titles, multitomo_init_titles=self.multitomo_init_titles, dark_titles=self.dark_titles, flat_titles=self.flat_titles, projection_titles=self.projection_titles, alignment_titles=self.alignment_titles, back_and_forth_init_titles=self.back_and_forth_init_titles, ), frame_type_section=FrameTypeSection( data_scans=self.data_scans, default_data_copy=self.default_data_copy, ), multitomo_section=MultiTomoSection( start_angle_offset_in_degree=self.start_angle_offset_in_degree, n_nxtomo=self.n_nxtomo, angle_interval_in_degree=self.angle_interval_in_degree, shift_angles=self.shift_angles, ), extra_params_section=ExtraParamsSection( energy_kev=self.energy_kev, x_sample_pixel_size_m=self.x_sample_pixel_size_m, y_sample_pixel_size_m=self.y_sample_pixel_size_m, x_detector_pixel_size_m=self.x_detector_pixel_size_m, y_detector_pixel_size_m=self.y_detector_pixel_size_m, detector_sample_distance_m=self.detector_sample_distance_m, source_sample_distance_m=self.source_sample_distance_m, ), ) @property def is_using_titles(self) -> bool: return len(self.data_scans) == 0 @property def is_using_urls(self) -> bool: """ Return true if we want to use urls for darks, flats, projections instead of titles """ return len(self.data_scans) > 0 def clear_entries_and_subentries(self): self.entries = () self.sub_entries_to_ignore = () @classmethod def from_cfg_file(cls, file_path: str) -> H52nxModel: txt_parser = ConfigParser(allow_no_value=True) txt_parser.read(file_path) def get_section(name, default={}): if txt_parser.has_section(name): return txt_parser[name] else: return default return _NestedTomoHDF5Config( general_section=GeneralSection(**get_section("GENERAL_SECTION")), entries_and_titles_section=EntriesAndTitlesSection( **get_section("ENTRIES_AND_TITLES_SECTION") ), extra_params_section=ExtraParamsSection( **get_section("EXTRA_PARAMS_SECTION") ), frame_type_section=FrameTypeSection(**get_section("FRAME_TYPE_SECTION")), multitomo_section=MultiTomoSection(**get_section("MULTITOMO_SECTION")), keys_section=KeysSection(**get_section("KEYS_SECTION")), ).to_flatten_config() # deprecated function / properties from version 1 @staticmethod def from_dict(dict_: dict) -> None: deprecated_warning( type_="function", name="from_dict", reason="replaced by pydantic 'model_dump' function", replacement="model_dump", since_version="2.0", ) dict_ = {key.lower(): value for key, value in dict_.items()} config = _NestedTomoHDF5Config(**dict_) return config.to_flatten_config() @property @deprecated(since_version="2.0", reason="removed", replacement="") def bam_single_file(self): """This option has been removed. Usage of 'single_file' should be enough""" pass @bam_single_file.setter @deprecated(since_version="2.0", reason="removed", replacement="") def bam_single_file(self, bam: bool): """This option has been removed. Usage of 'single_file' should be enough""" pass class _NestedTomoHDF5Config(BaseModel, NestedModelBase): """ Nested model to dump the model to .cfg. It has historically sections when the model (config class is flat). But the parameters are the same """ model_config = ConfigDict(str_to_upper=True) general_section: GeneralSection = GeneralSection() keys_section: KeysSection = KeysSection() entries_and_titles_section: EntriesAndTitlesSection = EntriesAndTitlesSection() frame_type_section: FrameTypeSection = FrameTypeSection() multitomo_section: MultiTomoSection = MultiTomoSection() extra_params_section: ExtraParamsSection = ExtraParamsSection() def to_flatten_config(self) -> H52nxModel: return H52nxModel( **self.general_section.model_dump(), **self.keys_section.model_dump(), **self.entries_and_titles_section.model_dump(), **self.frame_type_section.model_dump(), **self.multitomo_section.model_dump(), **self.extra_params_section.model_dump(), ) def generate_default_h5_config() -> dict: """generate a default configuration for converting hdf5 to nx""" config = H52nxModel().to_nested_model() return {key.upper(): value for key, value in config.model_dump().items()} nxtomomill-v2.0.1/nxtomomill/models/h52nx/keys_section.py000066400000000000000000000064421511430602400236060ustar00rootroot00000000000000from __future__ import annotations from pydantic import BaseModel, ConfigDict, Field, field_validator from nxtomomill.models.utils import convert_str_to_tuple from nxtomomill.settings import Tomo class KeysSection(BaseModel): model_config = ConfigDict(validate_assignment=True, validate_by_name=True) valid_camera_names: tuple[str, ...] = Field( default=tuple(), description="Valid camera names." ) rotation_angle_keys: tuple[str, ...] = Field( default=Tomo.H5.ROT_ANGLE_KEYS, description="Keys for rotation angle.", ) sample_x_keys: tuple[str, ...] = Field( default=Tomo.H5.SAMPLE_X_KEYS, description="Keys for sample x translation.", ) sample_y_keys: tuple[str, ...] = Field( default=Tomo.H5.SAMPLE_Y_KEYS, description="Keys for sample y translation.", ) translation_z_keys: tuple[str, ...] = Field( default=Tomo.H5.TRANSLATION_Z_KEYS, description="Keys for translation in z.", ) translation_y_keys: tuple[str, ...] = Field( default=Tomo.H5.TRANSLATION_Y_KEYS, description="Keys for estimated center of rotation for half acquisition.", ) diode_keys: tuple[str] = Field( default=Tomo.H5.DIODE_KEYS, description="Keys for diode." ) exposure_time_keys: tuple[str, ...] = Field( default=Tomo.H5.ACQ_EXPO_TIME_KEYS, description="Keys for exposure time." ) sample_x_pixel_size_keys: tuple[str, ...] = Field( default=Tomo.H5.SAMPLE_X_PIXEL_SIZE_KEYS, description="Keys for sample x pixel size.", ) sample_y_pixel_size_keys: tuple[str, ...] = Field( default=Tomo.H5.SAMPLE_Y_PIXEL_SIZE_KEYS, description="Keys for sample y pixel size.", ) detector_x_pixel_size_keys: tuple[str, ...] = Field( default=Tomo.H5.DETECTOR_X_PIXEL_SIZE_KEYS, description="Keys for detector x pixel size.", ) detector_y_pixel_size_keys: tuple[str, ...] = Field( default=Tomo.H5.DETECTOR_Y_PIXEL_SIZE_KEYS, description="Keys for detector y pixel size.", ) sample_detector_distance_keys: tuple[str, ...] = Field( default=Tomo.H5.SAMPLE_DETECTOR_DISTANCE_KEYS, description="Keys for sample to detector distance.", alias="sample_detector_distance", ) source_sample_distance_keys: tuple[str, ...] = Field( default=Tomo.H5.SOURCE_SAMPLE_DISTANCE_KEYS, description="Keys for source to sample distance.", alias="source_sample_distance", ) machine_current_keys: tuple[str, ...] = Field( default=Tomo.H5.MACHINE_CURRENT_KEYS, description="Keys for machine current.", ) @field_validator( "valid_camera_names", "rotation_angle_keys", "sample_x_keys", "sample_y_keys", "translation_z_keys", "translation_y_keys", "diode_keys", "exposure_time_keys", "sample_x_pixel_size_keys", "sample_y_pixel_size_keys", "detector_x_pixel_size_keys", "detector_y_pixel_size_keys", "sample_detector_distance_keys", "source_sample_distance_keys", "machine_current_keys", mode="plain", ) @classmethod def cast_keys_section_to_tuple(cls, value: str) -> tuple[str, ...]: return convert_str_to_tuple(value) or () nxtomomill-v2.0.1/nxtomomill/models/h52nx/multitomo_section.py000066400000000000000000000034331511430602400246610ustar00rootroot00000000000000from __future__ import annotations from pydantic import BaseModel, Field, field_validator, ConfigDict class MultiTomoSection(BaseModel): model_config: ConfigDict = ConfigDict( validate_assignment=True, validate_by_name=True ) start_angle_offset_in_degree: float | None = Field( default=None, description="Start angle offset in degree." ) n_nxtomo: int = Field(default=-1, description="Number of NXtomo to create.") angle_interval_in_degree: int = Field( default=360, description="Angle interval to create." ) shift_angles: bool = Field( default=False, description="Shift all angle NXtomo angle." ) @field_validator( "start_angle_offset_in_degree", mode="plain", ) @classmethod def cast_multi_tomo_section_to_float(cls, value: str) -> float | None: if value in (None, "None", ""): return None return float(value) # field aliases @property def multitomo_start_angle_offset(self): return self.start_angle_offset_in_degree @multitomo_start_angle_offset.setter def multitomo_start_angle_offset(self, value): self.start_angle_offset_in_degree = value @property def multitomo_scan_range(self): return self.angle_interval_in_degree @multitomo_scan_range.setter def multitomo_scan_range(self, value): self.angle_interval_in_degree = value @property def multitomo_shift_angles(self): return self.shift_angles @multitomo_shift_angles.setter def multitomo_shift_angles(self, value): self.shift_angles = value @property def multitomo_n_nxtomo(self): return self.n_nxtomo @multitomo_n_nxtomo.setter def multitomo_n_nxtomo(self, value): self.n_nxtomo = value nxtomomill-v2.0.1/nxtomomill/models/tests/000077500000000000000000000000001511430602400207255ustar00rootroot00000000000000nxtomomill-v2.0.1/nxtomomill/models/tests/test_edf2nx_model.py000066400000000000000000000261721511430602400247140ustar00rootroot00000000000000# coding: utf-8 import os import pint from tempfile import TemporaryDirectory import pytest import logging # from pydantic import ValidateError import importlib.resources as importlib_resources from configparser import ConfigParser from nxtomo.nxobject.nxdetector import FieldOfView from nxtomo.nxobject.nxsource import ProbeType, SourceType from nxtomomill import settings from nxtomomill.models.edf2nx import EDF2nxModel, generate_default_edf_config from nxtomomill.tests.resources import edf2nx_config_files as _edf2nx_config_files from nxtomomill.utils import FileExtension from nxtomomill.models.utils import PathType _ureg = pint.get_application_registry() def test_moving_to_pydantic_default_config_file(): """ test that the default nxtomomill config generated before moving to pydantic is properly interpreted. This ignores any comments. """ resources = importlib_resources.files(_edf2nx_config_files.__name__) ref_file = os.path.join(resources, "default_file_before_moving_to_pydantic.cfg") assert os.path.exists(ref_file) txt_parser = ConfigParser(allow_no_value=True) txt_parser.read(ref_file) old_config = EDF2nxModel( general_section=txt_parser["GENERAL_SECTION"], edf_keys_section=txt_parser["EDF_KEYS_SECTION"], dark_and_flat_section=txt_parser["DARK_AND_FLAT_SECTION"], sample_section=txt_parser["SAMPLE_SECTION"], unit_section=txt_parser["UNIT_SECTION"], source_section=txt_parser["SOURCE_SECTION"], detector_section=txt_parser["DETECTOR_SECTION"], ) old_dict = {key.upper(): value for key, value in old_config.model_dump().items()} new_dict = generate_default_edf_config() assert new_dict == old_dict def test_TomoEDFConfig_default_config(): """insure default configuration generation works""" with TemporaryDirectory() as folder: file_path = os.path.join(folder, "config.cfg") assert not os.path.exists(file_path) config = generate_default_edf_config() assert isinstance(config, dict) EDF2nxModel().to_cfg_file(file_path=file_path) assert os.path.exists(file_path) def test_moving_to_pydantic_modified_config_file(): """ test that an old nxtomomill edf-config file generated before moving to pydantic is properly interpreted. This ignores any comments. """ resources = importlib_resources.files(_edf2nx_config_files.__name__) ref_file = os.path.join(resources, "fully_modified_file_before_moving.cfg") assert os.path.exists(ref_file) config: EDF2nxModel = EDF2nxModel.from_cfg_file(ref_file) assert config.input_folder == "my_folder" assert config.output_file == "my_file.nx" assert config.overwrite is True assert config.delete_edf_source_files is True assert config.output_checks == ("check_1",) assert config.file_extension == FileExtension.NXS assert config.dataset_basename == "dataset_basename" assert config.dataset_info_file == "dataset_info_file.info" assert config.log_level == logging.INFO assert config.title == "my_title" assert config.patterns_to_ignores == ("_slice_", "_pattern_") assert config.duplicate_data is False assert config.external_link_type is PathType.ABSOLUTE assert config.motor_position_keys == ("motor_pos_key",) assert config.motor_mne_keys == ("motor_mne_key",) assert config.rotation_angle_keys == ("srotatation",) assert config.x_translation_keys == ("stx",) assert config.y_translation_keys == ("sty",) assert config.z_translation_keys == ("stz",) assert config.dark_names_prefix == ("prefix_1", "prefix_2") assert config.flat_names_prefix == ("prefix_3", "prefix_4") assert config.pixel_size_unit == _ureg.meter assert config.sample_detector_distance_unit == _ureg.meter assert config.energy_unit == _ureg.GeV assert config.x_translation_unit == _ureg.meter assert config.y_translation_unit == _ureg.meter assert config.z_translation_unit == _ureg.meter assert config.machine_current_unit == _ureg.kA assert config.sample_name == "my_sample" assert config.force_angle_calculation is False assert config.angle_calculation_endpoint is True assert config.angle_calculation_rev_neg_scan_range is False assert config.instrument_name == "my_instrument" assert config.source_name == "my_source" assert config.source_type == SourceType.SPALLATION_NEUTRON assert config.source_probe == ProbeType.NEUTRON assert config.field_of_view == FieldOfView.HALF def test_TomoEDFConfig_setters(): """test the different setters and getter of the EDFTomoConfig""" config = EDF2nxModel() # try to test a new attribut (insure class is frozeen) with pytest.raises(ValueError): config.new_attrs = "toto" # test general section setters config.input_folder = "my_folder" config.output_file = "my_nx.nx" config.file_extension = ".nx" with pytest.raises(ValueError): config.dataset_basename = 12.0 config.dataset_basename = None config.dataset_basename = "test_" with pytest.raises(ValueError): config.dataset_info_file = 12.3 config.dataset_info_file = None config.dataset_info_file = "my_info_file.info" config.overwrite = True with pytest.raises(ValueError): config.title = 12.0 config.title = None config.title = "my title" with pytest.raises(ValueError): config.patterns_to_ignores = 12.0 config.patterns_to_ignores = ("toto",) # test header keys + dark and flat setters attributes_value = { "motor_position_keys": settings.Tomo.EDF.MOTOR_POS, "motor_mne_keys": settings.Tomo.EDF.MOTOR_MNE, "rotation_angle_keys": settings.Tomo.EDF.ROT_ANGLE, "dark_names": settings.Tomo.EDF.DARK_NAMES, "flat_names": settings.Tomo.EDF.REFS_NAMES, } for attr, key in attributes_value.items(): setattr(config, attr, key) with pytest.raises((TypeError, ValueError)): setattr(config, attr, "toto") with pytest.raises((TypeError, ValueError)): setattr(config, attr, 12.20) with pytest.raises((TypeError, ValueError)): setattr(config, attr, (12.20,)) # test units setters attributes_value = { "pixel_size_unit": _ureg.micrometer, "distance_unit": _ureg.meter, "energy_unit": _ureg.keV, "x_translation_unit": _ureg.meter, "y_translation_unit": _ureg.meter, "z_translation_unit": _ureg.meter, } for attr, key in attributes_value.items(): # test providing an instance of a unit setattr(config, attr, key) # test providing a sting if of a unit setattr(config, attr, str(key)) with pytest.raises(TypeError): setattr(config, attr, None) with pytest.raises(TypeError): setattr(config, attr, 12.0) # test sample setters config.sample_name = None with pytest.raises(ValueError): config.sample_name = (12,) with pytest.raises(ValueError): config.sample_name = 12.0 config.sample_name = "my sample" with pytest.raises(ValueError): config.force_angle_calculation = "toto" config.force_angle_calculation = True with pytest.raises(ValueError): config.angle_calculation_endpoint = "toto" config.angle_calculation_endpoint = True with pytest.raises(ValueError): config.angle_calculation_rev_neg_scan_range = "toto" config.angle_calculation_rev_neg_scan_range = False # test source setters config.instrument_name = None config.instrument_name = "BMXX" with pytest.raises(ValueError): config.instrument_name = 12.3 config.source_name = None config.source_name = "ESRF" with pytest.raises(ValueError): config.source_name = 12.2 config.source_name = "XFEL" config.source_type = None config.source_type = SourceType.FIXED_TUBE_X_RAY.value config.source_type = SourceType.FIXED_TUBE_X_RAY with pytest.raises(ValueError): config.source_type = "trsts" with pytest.raises(ValueError): config.source_type = 123 config.source_probe = None config.source_probe = ProbeType.ELECTRON config.source_probe = "positron" with pytest.raises(ValueError): config.source_probe = 123 # test detector setters config.field_of_view = None with pytest.raises(ValueError): config.field_of_view = 12 with pytest.raises(ValueError): config.field_of_view = "toto" config.field_of_view = FieldOfView.FULL.value config.field_of_view = FieldOfView.HALF config_dict = config.to_dict() general_section_dict = config_dict["GENERAL_SECTION"] assert general_section_dict["input_folder"] == "my_folder" assert general_section_dict["output_file"] == "my_nx.nx" assert general_section_dict["file_extension"] == ".nx" assert general_section_dict["overwrite"] is True assert general_section_dict["dataset_basename"] == "test_" assert general_section_dict["dataset_info_file"] == "my_info_file.info" assert general_section_dict["title"] == "my title" assert general_section_dict["patterns_to_ignores"] == ("toto",) edf_headers_section_dict = config_dict["EDF_KEYS_SECTION"] assert ( edf_headers_section_dict["motor_position_keys"] == settings.Tomo.EDF.MOTOR_POS ) assert edf_headers_section_dict["motor_mne_keys"] == settings.Tomo.EDF.MOTOR_MNE assert edf_headers_section_dict["x_translation_keys"] == settings.Tomo.EDF.X_TRANS assert edf_headers_section_dict["y_translation_keys"] == settings.Tomo.EDF.Y_TRANS assert edf_headers_section_dict["z_translation_keys"] == settings.Tomo.EDF.Z_TRANS assert ( edf_headers_section_dict["rotation_angle_keys"] == settings.Tomo.EDF.ROT_ANGLE ) dark_flat_section_dict = config_dict["DARK_AND_FLAT_SECTION"] assert dark_flat_section_dict["dark_names_prefix"] == settings.Tomo.EDF.DARK_NAMES assert dark_flat_section_dict["flat_names_prefix"] == settings.Tomo.EDF.REFS_NAMES units_section_dict = config_dict["UNIT_SECTION"] assert units_section_dict["pixel_size_unit"] == str(_ureg.micrometer) assert units_section_dict["sample_detector_distance_unit"] == str(_ureg.meter) assert units_section_dict["energy_unit"] == str(_ureg.keV) assert units_section_dict["x_translation_unit"] == str(_ureg.meter) assert units_section_dict["y_translation_unit"] == str(_ureg.meter) assert units_section_dict["z_translation_unit"] == str(_ureg.meter) sample_section_dict = config_dict["SAMPLE_SECTION"] assert sample_section_dict["sample_name"] == "my sample" assert sample_section_dict["force_angle_calculation"] is True assert sample_section_dict["angle_calculation_endpoint"] is True assert sample_section_dict["angle_calculation_rev_neg_scan_range"] is False source_section_dict = config_dict["SOURCE_SECTION"] assert source_section_dict["instrument_name"] == "BMXX" assert source_section_dict["source_name"] == "XFEL" assert source_section_dict["source_type"] == SourceType.FIXED_TUBE_X_RAY.value detector_section_dict = config_dict["DETECTOR_SECTION"] assert detector_section_dict["field_of_view"] == FieldOfView.HALF.value nxtomomill-v2.0.1/nxtomomill/models/tests/test_fluo2nx_model.py000066400000000000000000000121771511430602400251230ustar00rootroot00000000000000# coding: utf-8 import os import pytest import logging from configparser import ConfigParser import importlib.resources as importlib_resources from nxtomo.nxobject.nxsource import ProbeType, SourceType from nxtomomill.models.fluo2nx import Fluo2nxModel, generate_default_fluo_config from nxtomomill.tests.resources import fluo2nx_config_files as _fluo2nx_config_files from nxtomomill.utils import FileExtension def test_moving_to_pydantic_default_config_file(): """ test that the default nxtomomill config generated before moving to pydantic is properly interpreted. This ignores comments. """ resources = importlib_resources.files(_fluo2nx_config_files.__name__) ref_file = os.path.join(resources, "default_file_before_moving_to_pydantic.cfg") assert os.path.exists(ref_file) txt_parser = ConfigParser(allow_no_value=True) txt_parser.read(ref_file) old_config = Fluo2nxModel( general_section=txt_parser["GENERAL_SECTION"], source_section=txt_parser["SOURCE_SECTION"], ).to_nested_model() old_dict = {key.upper(): value for key, value in old_config.model_dump().items()} new_dict = generate_default_fluo_config() assert new_dict == old_dict def test_TomoFluoConfig_default_config(tmp_path): """insure default configuration generation works""" file_path = os.path.join(tmp_path, "config.cfg") assert not os.path.exists(file_path) Fluo2nxModel().to_cfg_file(file_path=file_path) assert os.path.exists(file_path) def test_moving_to_pydantic_modified_config_file(): """ test that an old nxtomomill fluo-config file generated before moving to pydantic is properly interpreted. This ignores any comments. """ resources = importlib_resources.files(_fluo2nx_config_files.__name__) ref_file = os.path.join(resources, "fully_modified_file_before_moving.cfg") assert os.path.exists(ref_file) config = Fluo2nxModel.from_cfg_file(file_path=ref_file) assert config.input_folder == "my_folder" assert config.output_file == "output_file.nx" assert config.dimension == 2 assert config.detector_names == ("my_detector",) assert config.overwrite is True assert config.file_extension == FileExtension.H5 assert config.dataset_basename == "dataset_basename" assert config.dataset_info_file == "dataset_info_file.info" assert config.log_level == logging.ERROR # note: 'title' is part of the configuration file but was never used in the processing assert config.patterns_to_ignores == ("_ignore_",) assert config.duplicate_data is False # note: 'external_link_path is part of the configuration file but was never used in the processing assert config.instrument_name == "my_instrument" assert config.source_name == "my_source" assert config.source_type == SourceType.PULSED_REACTOR_NEUTRON_SOURCE assert config.source_probe == ProbeType.POSITRON def test_TomoFluoConfig_setters(): """test the different setters and getter of the Fluo2nxModel""" config = Fluo2nxModel() # try to test a new attribut (insure class is frozeen) with pytest.raises(ValueError): config.new_attrs = "toto" # test general section setters with pytest.raises(ValueError): config.input_folder = 12.0 config.input_folder = "my_folder" with pytest.raises(ValueError): config.output_file = 12.0 config.output_file = "my_nx.nx" config.file_extension = ".nx" with pytest.raises(ValueError): config.detectors = 12.0 config.detector_names = () config.detector_names = ("test_",) with pytest.raises(ValueError): config.dataset_basename = 12.0 config.dataset_basename = None config.dataset_basename = "test_" config.overwrite = True # test source setters config.instrument_name = None config.instrument_name = "BMXX" with pytest.raises(ValueError): config.instrument_name = 12.3 config.source_name = None config.source_name = "ESRF" with pytest.raises(ValueError): config.source_name = 12.2 config.source_name = "XFEL" config.source_type = None config.source_type = SourceType.FIXED_TUBE_X_RAY.value config.source_type = SourceType.FIXED_TUBE_X_RAY with pytest.raises(ValueError): config.source_type = "trsts" with pytest.raises(ValueError): config.source_type = 123 config.source_probe = None config.source_probe = ProbeType.ELECTRON config.source_probe = "positron" with pytest.raises(ValueError): config.source_probe = 123 config_dict = config.to_dict() general_section_dict = config_dict["GENERAL_SECTION"] assert general_section_dict["input_folder"] == "my_folder" assert general_section_dict["output_file"] == "my_nx.nx" assert general_section_dict["file_extension"] == ".nx" assert general_section_dict["overwrite"] is True assert general_section_dict["dataset_basename"] == "test_" source_section_dict = config_dict["SOURCE_SECTION"] assert source_section_dict["instrument_name"] == "BMXX" assert source_section_dict["source_name"] == "XFEL" assert source_section_dict["source_type"] == SourceType.FIXED_TUBE_X_RAY.value nxtomomill-v2.0.1/nxtomomill/models/tests/test_h52nx_model.py000066400000000000000000000274551511430602400244770ustar00rootroot00000000000000from __future__ import annotations import os import pytest import logging import importlib.resources as importlib_resources from silx.io.url import DataUrl from nxtomo.nxobject.nxdetector import FieldOfView from nxtomomill import settings from nxtomomill.utils import FileExtension from nxtomomill.models.h52nx import H52nxModel, generate_default_h5_config from nxtomomill.models.h52nx.general_section import GeneralSection from nxtomomill.models.h52nx.keys_section import KeysSection from nxtomomill.models.h52nx.entries_and_title_section import ( EntriesAndTitlesSection, ) from nxtomomill.models.h52nx.frame_type_section import ( FrameTypeSection, FrameGroup, ) from nxtomomill.models.h52nx.multitomo_section import MultiTomoSection from nxtomomill.models.h52nx.extra_params_section import ( ExtraParamsSection, ) from nxtomomill.tests.resources import h52nx_config_files as _h52nx_config_files from configparser import ConfigParser def test_moving_to_pydantic_default_config_file(): """ test that the default nxtomomill config generated before moving to pydantic is properly interpreted. This ignores comments. """ resources = importlib_resources.files(_h52nx_config_files.__name__) ref_file = os.path.join(resources, "default_file_before_moving_to_pydantic.cfg") assert os.path.exists(ref_file) txt_parser = ConfigParser(allow_no_value=True) txt_parser.read(ref_file) old_config = H52nxModel( general_section=txt_parser["GENERAL_SECTION"], keys_section=txt_parser["KEYS_SECTION"], entries_and_titles_section=txt_parser["ENTRIES_AND_TITLES_SECTION"], frame_type_section=txt_parser["FRAME_TYPE_SECTION"], multitomo_section=txt_parser["MULTITOMO_SECTION"], extra_params_section=txt_parser["EXTRA_PARAMS_SECTION"], ).to_nested_model() old_dict = {key.upper(): value for key, value in old_config.model_dump().items()} new_dict = generate_default_h5_config() assert new_dict == old_dict def test_moving_to_pydantic_modified_config_file(): """ test that an old nxtomomill h5-config file generated before moving to pydantic is properly interpreted. This ignores any comments. """ resources = importlib_resources.files(_h52nx_config_files.__name__) ref_file = os.path.join(resources, "fully_modified_file_before_moving.cfg") assert os.path.exists(ref_file) config = H52nxModel.from_cfg_file(ref_file) # general section assert config.input_file == "input_file.h5" assert config.output_file == "output_file.nx" assert config.overwrite is True assert config.file_extension is FileExtension.H5 assert config.log_level is logging.DEBUG assert config.raises_error is True assert config.no_input is True assert config.single_file is True assert config.no_master_file is False assert config.ignore_bliss_tomo_config is True assert config.field_of_view is FieldOfView.FULL assert config.create_control_data is False # keys section assert config.valid_camera_names == ("my_camera",) assert config.rotation_angle_keys == ("rotm",) assert config.sample_x_keys == ("samx",) assert config.sample_y_keys == ("samy",) assert config.translation_y_keys == ("yrot",) assert config.translation_z_keys == ("zrot",) assert config.diode_keys == ("fpico",) assert config.exposure_time_keys == ("exposure_time",) assert config.sample_x_pixel_size_keys == ("technique/optic/my_sample_pixel_size",) assert config.sample_y_pixel_size_keys == ("technique/optic/my_sample_pixel_size2",) assert config.detector_x_pixel_size_keys == ("technique/scan/detector_pixel_size",) assert config.detector_y_pixel_size_keys == ("technique/scan/detector_pixel_size2",) assert config.sample_detector_distance_keys == ( "technique/scan/my_sample_detector_distance", ) assert config.source_sample_distance_keys == ( "technique/scan/source_my_sample_distance", ) # entries and titles section assert {url.path() for url in config.entries} == { DataUrl(data_path="/1.1", scheme="silx").path(), DataUrl(data_path="/3.1", scheme="silx").path(), } assert {url.path() for url in config.sub_entries_to_ignore} == { DataUrl(data_path="/2.1", scheme="silx").path() } assert config.init_titles == ("my_tomo:basic",) assert config.zseries_init_titles == ("my_tomo:zseries",) assert config.multitomo_init_titles == ("my_tomo:pcotomo",) assert config.dark_titles == ("my dark images",) assert config.flat_titles == ("my flat",) assert config.projection_titles == ("my projections",) assert config.alignment_titles == ("my static images",) # frame type section assert config.data_scans == () assert config.default_data_copy is True # multitomo section assert config.start_angle_offset_in_degree == 24.2 assert config.n_nxtomo == 100 assert config.angle_interval_in_degree == 180 assert config.shift_angles is True # extra params assert config.energy_kev == 12.5 assert config.y_detector_pixel_size_m == 2.3e-6 def test_moving_to_pydantic_file_with_data_scans(): resources = importlib_resources.files(_h52nx_config_files.__name__) ref_file = os.path.join(resources, "config_file_with_data_scans.cfg") assert os.path.exists(ref_file) txt_parser = ConfigParser(allow_no_value=True) txt_parser.read(ref_file) old_config = H52nxModel( general_section=txt_parser["GENERAL_SECTION"], keys_section=txt_parser["KEYS_SECTION"], entries_and_titles_section=txt_parser["ENTRIES_AND_TITLES_SECTION"], frame_type_section=txt_parser["FRAME_TYPE_SECTION"], multitomo_section=txt_parser["MULTITOMO_SECTION"], extra_params_section=txt_parser["EXTRA_PARAMS_SECTION"], ) old_dict = {key.upper(): value for key, value in old_config.model_dump().items()} config = H52nxModel( general_section=GeneralSection(), keys_section=KeysSection(), entries_and_titles_section=EntriesAndTitlesSection(), frame_type_section=FrameTypeSection( data_scans=( FrameGroup( url="silx:///path/to/file?/path/to/scan/node", frame_type="projections", ), FrameGroup( url="/path_relative_to_file", frame_type="darks", copy_data=True, ), ) ), multitomo_section=MultiTomoSection(), extra_params_section=ExtraParamsSection(), ) new_dict = {key.upper(): value for key, value in config.model_dump().items()} assert new_dict == old_dict def test_generate_default_hdf5_config(): """ Insure we can generate a default configuration """ config = H52nxModel() config.input_file = "toto.h5" config.output_file = "toto.nx" output = config.to_dict() assert isinstance(output, dict) # check titles values titles_dict = output["ENTRIES_AND_TITLES_SECTION"] assert titles_dict["init_titles"] == settings.Tomo.H5.INIT_TITLES assert titles_dict["zseries_init_titles"] == settings.Tomo.H5.ZSERIE_INIT_TITLES assert titles_dict["projection_titles"] == settings.Tomo.H5.PROJ_TITLES assert titles_dict["flat_titles"] == settings.Tomo.H5.FLAT_TITLES assert titles_dict["dark_titles"] == settings.Tomo.H5.DARK_TITLES assert titles_dict["alignment_titles"] == settings.Tomo.H5.ALIGNMENT_TITLES # check sample pixel size keys_dict = output["KEYS_SECTION"] assert ( keys_dict["sample_x_pixel_size_keys"] == settings.Tomo.H5.SAMPLE_X_PIXEL_SIZE_KEYS ) assert ( keys_dict["sample_y_pixel_size_keys"] == settings.Tomo.H5.SAMPLE_Y_PIXEL_SIZE_KEYS ) assert ( keys_dict["detector_x_pixel_size_keys"] == settings.Tomo.H5.DETECTOR_X_PIXEL_SIZE_KEYS ) assert ( keys_dict["detector_y_pixel_size_keys"] == settings.Tomo.H5.DETECTOR_Y_PIXEL_SIZE_KEYS ) # check translation assert keys_dict["sample_x_keys"] == settings.Tomo.H5.SAMPLE_X_KEYS assert keys_dict["sample_y_keys"] == settings.Tomo.H5.SAMPLE_Y_KEYS assert keys_dict["translation_z_keys"] == settings.Tomo.H5.TRANSLATION_Z_KEYS # others if len(settings.Tomo.H5.VALID_CAMERA_NAMES) == 0: assert keys_dict["valid_camera_names"] == () else: assert keys_dict["valid_camera_names"] == settings.Tomo.H5.VALID_CAMERA_NAMES assert keys_dict["rotation_angle_keys"] == settings.Tomo.H5.ROT_ANGLE_KEYS assert keys_dict["diode_keys"] == settings.Tomo.H5.DIODE_KEYS assert keys_dict["translation_y_keys"] == settings.Tomo.H5.TRANSLATION_Y_KEYS assert keys_dict["exposure_time_keys"] == settings.Tomo.H5.ACQ_EXPO_TIME_KEYS # check input and output file general_information = output["GENERAL_SECTION"] assert general_information["input_file"] == "toto.h5" assert general_information["output_file"] == "toto.nx" def test_hdf5config_to_dict(): """test the `to_dict` function""" output_dict = H52nxModel().to_nested_model().model_dump() # check sections for section in ( "general_section", "keys_section", "extra_params_section", "frame_type_section", "entries_and_titles_section", ): assert section in output_dict # check titles keys for key in ( "alignment_titles", "projection_titles", "zseries_init_titles", "init_titles", "flat_titles", "dark_titles", ): assert key in output_dict["entries_and_titles_section"] # check sample pixel size for key in ( "sample_x_pixel_size_keys", "sample_y_pixel_size_keys", "detector_x_pixel_size_keys", "detector_y_pixel_size_keys", ): assert key in output_dict["keys_section"] # translation keys for key in ( "sample_x_keys", "sample_y_keys", "translation_z_keys", ): assert key in output_dict["keys_section"] # others for key in ( "valid_camera_names", "rotation_angle_keys", "translation_y_keys", "diode_keys", "exposure_time_keys", ): assert key in output_dict["keys_section"] def test_hdf5config_from_dict(): """test the `from_dict` function""" valid_camera_names = ("frelon", "totocam") alignment_titles = ("this is an alignment",) sample_x_keys = ("tx", "x") config = H52nxModel.from_dict( { "KEYS_SECTION": { "valid_camera_names": valid_camera_names, "sample_x_keys": sample_x_keys, }, "ENTRIES_AND_TITLES_SECTION": {"alignment_titles": alignment_titles}, } ) assert config.valid_camera_names == valid_camera_names assert config.alignment_titles == alignment_titles assert config.sample_x_keys == sample_x_keys def test_hdf5config_from_dict_lowercase(): """Test `from_dict` method with lower case sections""" ref_dict = H52nxModel().to_dict() lower_dict = {k.lower(): v for k, v in ref_dict.items()} config = H52nxModel.from_dict(lower_dict) assert config.to_dict() == ref_dict def test_hdf5config_raises_errors(): """ Insure a type error is raised if an invalid type is passed to the HDF5Config class :return: """ with pytest.raises(TypeError): H52nxModel.from_dict({"ENTRIES_AND_TITLES_SECTION": {"dark_titles": 1213}}) def test_hdf5config_to_and_from_cfg_file(tmp_path): """ Insure we can dump the configuration to a .cfg file and that we can read it back """ file_path = os.path.join(tmp_path, "output_file.cfg") input_config = H52nxModel() input_config.to_cfg_file(file_path) assert os.path.exists(file_path) loaded_config = H52nxModel.from_cfg_file(file_path=file_path) assert isinstance(loaded_config, H52nxModel) nxtomomill-v2.0.1/nxtomomill/models/tests/test_io_utils.py000066400000000000000000000030501511430602400241630ustar00rootroot00000000000000# coding: utf-8 import unittest from nxtomomill.models import utils class TestConvertStrToTuple(unittest.TestCase): """ test convert_str_to_tuple function """ def testStr1(self): self.assertEqual(utils.convert_str_to_tuple("toto, tata"), ("toto", "tata")) def testStr2(self): self.assertEqual( utils.convert_str_to_tuple("'toto', \"tata\""), ("toto", "tata") ) def testStr3(self): self.assertEqual(utils.convert_str_to_tuple("test"), ("test",)) def testStr4(self): self.assertEqual( utils.convert_str_to_tuple("(this is a test)"), ("this is a test",) ) def testStr5(self): self.assertEqual( utils.convert_str_to_tuple("(this is a test, 'and another one')"), ("this is a test", "and another one"), ) class TestIsUrlPath(unittest.TestCase): """test the is_url function""" def test_invalid_url_1(self): self.assertFalse(utils.is_url_path("/toto/tata")) def test_invalid_url_2(self): self.assertFalse(utils.is_url_path("tata")) def test_valid_url_1(self): self.assertTrue(utils.is_url_path("silx:///data/image.h5?path=/scan_0/data")) def test_valid_url_2(self): self.assertTrue(utils.is_url_path("silx:///data/image.edf")) def test_valid_url_3(self): self.assertTrue(utils.is_url_path("silx://image.h5")) def test_valid_url_4(self): self.assertTrue( utils.is_url_path("silx:///data/image.h5?path=/scan_0/data&slice=1,5") ) nxtomomill-v2.0.1/nxtomomill/models/utils.py000066400000000000000000000060131511430602400212750ustar00rootroot00000000000000# coding: utf-8 """ utils functions for io """ from __future__ import annotations import logging import re import h5py import numpy from silx.utils.enum import Enum as _Enum from silx.io.utils import h5py_read_dataset _logger = logging.getLogger(__name__) __all__ = [ "remove_parenthesis_or_brackets", "filter_str_def", "convert_str_to_tuple", "convert_str_to_bool", "is_url_path", "PathType", ] def remove_parenthesis_or_brackets(input_str): if ( input_str.startswith("(") and input_str.endswith(")") or input_str.startswith("[") and input_str.endswith("]") ): input_str = input_str[1:-1] return input_str def filter_str_def(elmt): if elmt is None: return None assert isinstance(elmt, str) elmt = elmt.lstrip(" ").rstrip(" ") for character in ("'", '"'): if elmt.startswith(character) and elmt.endswith(character): elmt = elmt[1:-1] return elmt def convert_str_to_tuple(input_str: str, none_if_empty: bool = False) -> tuple | None: """ :param input_str: string to convert :param none_if_empty: if true and the conversion is an empty tuple return None instead of an empty tuple """ if isinstance(input_str, (list, set)): input_str = tuple(input_str) if isinstance(input_str, tuple): return input_str if input_str is None: input_str = "" if not isinstance(input_str, str): raise TypeError( f"input_str should be a string not {type(input_str)}, {input_str}" ) input_str = input_str.lstrip(" ").rstrip(" ") input_str = remove_parenthesis_or_brackets(input_str) elmts = input_str.split(",") elmts = [filter_str_def(elmt) for elmt in elmts] rm_empty_str = lambda a: a != "" elmts = list(filter(rm_empty_str, elmts)) if none_if_empty and len(elmts) == 0: return None else: return tuple(elmts) def convert_str_to_bool(value: str | bool | numpy.bool_): if isinstance(value, (bool, numpy.bool_)): return bool(value) elif isinstance(value, str): if value not in ("False", "True", "1", "0"): raise ValueError("value should be 'True' or 'False'") return value in ("True", "1") else: raise TypeError(f"value should be a string or a boolean. Got {type(value)}") def is_url_path(url_str: str) -> bool: """ :param url_str: url as a string :return: True if the provided string fit DataUrl pattern [scheme]:://[file_path]?[data_path] """ pattern_str_seq = "[a-zA-Z0-9]*" url_path_pattern = rf"{pattern_str_seq}\:\/\/{pattern_str_seq}" pattern = re.compile(url_path_pattern) return bool(re.match(pattern, url_str)) def _get_title_dataset(entry: h5py.Group, title_paths: tuple[str]): for title_path in title_paths: if title_path in entry: return h5py_read_dataset(entry[title_path]) return None class PathType(_Enum): ABSOLUTE = "absolute" RELATIVE = "relative" nxtomomill-v2.0.1/nxtomomill/nexus/000077500000000000000000000000001511430602400174425ustar00rootroot00000000000000nxtomomill-v2.0.1/nxtomomill/nexus/__init__.py000066400000000000000000000005551511430602400215600ustar00rootroot00000000000000"""**deprecated**, use `nxtomo `_ module instead""" from .nxdetector import NXdetector # noqa F401 from .nxobject import NXobject # noqa F401 from .nxsample import NXsample # noqa F401 from .nxsource import NXsource # noqa F401 from .nxtomo import NXtomo # noqa F401 from .utils import concatenate # noqa F401 nxtomomill-v2.0.1/nxtomomill/nexus/nxdetector.py000066400000000000000000000004511511430602400221730ustar00rootroot00000000000000from nxtomomill.utils.io import deprecated_warning from nxtomo.nxobject.nxdetector import * # noqa F401 deprecated_warning( type_="Module", name="nxtomomill.nexus.nxdetector", reason="dedicated project created", replacement="nxtomo.nxobject.nxdetector", since_version=1.0, ) nxtomomill-v2.0.1/nxtomomill/nexus/nxinstrument.py000066400000000000000000000004571511430602400226000ustar00rootroot00000000000000from nxtomomill.utils.io import deprecated_warning from nxtomo.nxobject.nxinstrument import * # noqa F401 deprecated_warning( type_="Module", name="nxtomomill.nexus.nxinstrument", reason="dedicated project created", replacement="nxtomo.nxobject.nxinstrument", since_version=1.0, ) nxtomomill-v2.0.1/nxtomomill/nexus/nxmonitor.py000066400000000000000000000004461511430602400220550ustar00rootroot00000000000000from nxtomomill.utils.io import deprecated_warning from nxtomo.nxobject.nxmonitor import * # noqa F401 deprecated_warning( type_="Module", name="nxtomomill.nexus.nxmonitor", reason="dedicated project created", replacement="nxtomo.nxobject.nxmonitor", since_version=1.0, ) nxtomomill-v2.0.1/nxtomomill/nexus/nxobject.py000066400000000000000000000004431511430602400216310ustar00rootroot00000000000000from nxtomomill.utils.io import deprecated_warning from nxtomo.nxobject.nxobject import * # noqa F401 deprecated_warning( type_="Module", name="nxtomomill.nexus.nxobject", reason="dedicated project created", replacement="nxtomo.nxobject.nxobject", since_version=1.0, ) nxtomomill-v2.0.1/nxtomomill/nexus/nxsample.py000066400000000000000000000004431511430602400216440ustar00rootroot00000000000000from nxtomomill.utils.io import deprecated_warning from nxtomo.nxobject.nxsample import * # noqa F401 deprecated_warning( type_="Module", name="nxtomomill.nexus.nxsample", reason="dedicated project created", replacement="nxtomo.nxobject.nxsample", since_version=1.0, ) nxtomomill-v2.0.1/nxtomomill/nexus/nxsource.py000066400000000000000000000004431511430602400216630ustar00rootroot00000000000000from nxtomomill.utils.io import deprecated_warning from nxtomo.nxobject.nxsource import * # noqa F401 deprecated_warning( type_="Module", name="nxtomomill.nexus.nxsource", reason="dedicated project created", replacement="nxtomo.nxobject.nxsource", since_version=1.0, ) nxtomomill-v2.0.1/nxtomomill/nexus/nxtomo.py000066400000000000000000000004431511430602400213410ustar00rootroot00000000000000from nxtomomill.utils.io import deprecated_warning from nxtomo.application.nxtomo import * # noqa F401 deprecated_warning( type_="Module", name="nxtomomill.nexus.nxtomo", reason="dedicated project created", replacement="nxtomo.application.nxtomo", since_version=1.0, ) nxtomomill-v2.0.1/nxtomomill/nexus/nxtransformations.py000066400000000000000000000004761511430602400236220ustar00rootroot00000000000000from nxtomomill.utils.io import deprecated_warning from nxtomo.nxobject.nxtransformations import * # noqa F401 deprecated_warning( type_="Module", name="nxtomomill.nexus.nxtransformations", reason="dedicated project created", replacement="nxtomo.nxobject.nxtransformations", since_version=1.0, ) nxtomomill-v2.0.1/nxtomomill/nexus/utils.py000066400000000000000000000004341511430602400211550ustar00rootroot00000000000000from nxtomomill.utils.io import deprecated_warning from nxtomomill.utils.nexus import * # noqa F401 deprecated_warning( type_="Module", name="nxtomomill.nexus.utils", reason="dedicated project created", replacement="nxtomomill.utils.nexus", since_version=1.0, ) nxtomomill-v2.0.1/nxtomomill/resources/000077500000000000000000000000001511430602400203125ustar00rootroot00000000000000nxtomomill-v2.0.1/nxtomomill/resources/nxtomomill.png000066400000000000000000000062741511430602400232330ustar00rootroot00000000000000PNG  IHDRljY?sBIT|d pHYsnu>tEXtSoftwarewww.inkscape.org< 9IDATx{U?p.+\)RA D+uDIJӬ4jB9YP(THV>(Sº&B%TAEڛswuok@t.e!afv]g pxLuEž"0K<QυQW#wW87BK7aMTm J~ov,R<_]+";4a~ MyJ J8͒V3CKw1\LR^4# a;&%qwʹGJ~Q2Sa wvX#*aM#*S@I{+/* 'Dº/tC0t3ՒC09jJBZM[ U7>눃0UK~3F( UVZ36*Y7euEm5ls2~CmU89Wnk-J84@qЎQWXr[5~!V1I^mX`MJuIW0|F3TF{mK>D{UF>um}w7a%C_Qa-KJ~WU|9pU !PÍյj< |7aG" LŨF|2껕Gb_֥Ӭx0Y d!ʌ\c/cF}ą5 YCӁU}?QCB"LoXYc֟`| iL9ZovKˁ;i60.!ly܆,%(i8ĘБV cV7ۜ4'Z6`=XrmGLj&ζDz%L0KX[=;o%aM5Ϋz!ƅmZYmz91(F`%K иC2~ޘy a ~sڡ,,0*o"09Jw6>d2Aš1 ߴg)v` .`V5J+L%{ІJp{ a_PiP̹D?aN` ϒ7cT}`dnY{kD%D H\Sm0;rKUa8F1Ĺ LR= ے GW,ʏ&d 07}S%l:H%= )- PyEKyv!8Y5 q6NM埩y֎w<Vzu:w0U{,pe3DԈ_NƉ[AP߈јK~>M 9cRF'G| 2/P^00;H痰v*Ғ&چ>{=cQn∛yK:?žĬ.tV*y-:a-!|O#\!n@*ǬUNd X[6ig\K>xKɯP~"H01x6FaTgCɘqw%j7K7OB|`Vib=2OGH3%K:Ă[6b;A&?Cn;1[cf9.( ?ڭ|6#O7*"1 nuNq̝"^PFăﴹN㹪-ukH [8%$ndW6PhB܌ 8}MGMi }^ !k1эb>i;T= sC me!do`dH/k{JY8@ٺC<+N؇L b~$7$ny ?Y7B#˭A @AX*18ϭO>lA,n̠}] o "  ;< tՉpd5{2fZxY@D&%cCeDV@"#y1a0BG[Y߇,ܮp  >烑䵪]bPw ~}AS-F |̩n/ zQy ^~هz cYpƥ|2񔕃<p'=Lޜ&L$%a\ȳABF0p:Үdsۑq;uC?APB,o&}/B; zҥ<]d[ 'FL[ፕ:50zuu{#|5gz}~;<= ;9> 9a%|L"Fw&H 5UaC0MXZ%6zXAJ,!q!7RyBxJPcX==`VuGXV5A2D4DZj=%.'BUH%V|s}Rɰa-o[!gK1G< =p?6s image/svg+xml nxtomomill-v2.0.1/nxtomomill/settings.py000066400000000000000000000142171511430602400205170ustar00rootroot00000000000000# coding: utf-8 """ Module to convert from (Bliss) .h5 to (Nexus Tomo-compliant) .nx format. """ class Tomo: class H5: """HDF5 settings for tomography""" VALID_CAMERA_NAMES: tuple[str, ...] = tuple() # Camera names are now deduced using the `get_nx_detectors` and # `guess_nx_detector` functions. Alternatively, a list of detector # names can be provided (supports Unix shell-style wildcards), such as: # ("pcolinux*", "basler", "frelon*", ...) ROT_ANGLE_KEYS = ( "rotm", "mhsrot", "hsrot", "mrsrot", "hrsrot", "srot", "srot_eh2", "diffrz", "hrrz_trig", "rot", ) """Keys used to find rotation angles.""" TRANSLATION_Y_KEYS = ("yrot", "diffty", "hry") """Keys used to find the Y translation below the center of rotation.""" TRANSLATION_Z_KEYS = ("sz", "difftz", "hrz", "pz", "ntz", "samtz", "mrsz") """Keys used to find the Z translation below or above the center of rotation.""" SAMPLE_X_KEYS = ("samx", "psx", "sax", "fake_sx") """Keys used to find the X translation above the center of rotation (direction is independent of the rotation angle).""" SAMPLE_Y_KEYS = ("samy", "psv", "say", "fake_sy") """Keys used to find the Y translation above the center of rotation (direction is independent of the rotation angle).""" SAMPLE_U_KEYS = ("sau", "sx", "px", "ntx", "shtx", "hrx", "fake_su") """Keys used to find the U translation above the center of rotation (direction is dependent of the rotation angle).""" SAMPLE_V_KEYS = ("sav", "sy", "py", "nty", "shty", "hry2", "fake_sv") """Keys used to find the V translation above the center of rotation (direction is dependent of the rotation angle).""" DIODE_KEYS = ("fpico3",) """Keys used to store diode dataset.""" ACQ_EXPO_TIME_KEYS = ("acq_expo_time",) """Keys used to store acquisition exposure time.""" INIT_TITLES = ( "tomo:basic", "tomo:fullturn", "tomo:fulltomo", "sequence_of_scans", "tomo:halfturn", "tomo:multiturn", "tomo:helical", "tomo:holotomo", "holotomo_distance", ) """Initialization scan titles. Either the value of 'technique/scan_category' if exists else the value of 'title'""" ZSERIE_INIT_TITLES = ("tomo:zseries",) """Specific titles for z-series scans.""" MULTITOMO_INIT_TITLES = ( "tomo:pcotomo", "pcotomo", "tomo:multitomo", "multitomo", "multitomo:basic", "tomo:multiturn", "multiturn", ) """Specific titles for multi-tomo scans (also known as PCO scans). Either the value of 'technique/scan_category' if exists else the value of 'title'""" PCOTOMO_INIT_TITLES = MULTITOMO_INIT_TITLES """Deprecated. Replaced by 'MULTITOMO_INIT_TITLES'""" BACK_AND_FORTH_INIT_TITLES = ( "tomo:backandforth", "backandforthtomo:basic", "backandforth", "tomo:back_and_forth", "back_and_forth", ) """Specific titles for back and forth scans. Either the value of 'technique/scan_category' if exists else the value of 'title'""" DARK_TITLES = ("dark images", "dark") """Titles to determine dark field scans if 'technique/image_key' doesn't exists.""" FLAT_TITLES = ("flat", "reference images", "ref", "refend") """Titles to determine flat field scans if 'technique/image_key' doesn't exists.""" PROJ_TITLES = ("projections", "ascan rot 0", "ascan diffrz 0 180 1600 0.1") """Titles to determine projection scans if 'technique/image_key' doesn't exists.""" ALIGNMENT_TITLES = ("static images", "ascan diffrz 180 0 4 0.1") """Titles to determine alignment scans if 'technique/image_key' doesn't exists.""" SAMPLE_X_PIXEL_SIZE_KEYS = ("technique/optic/sample_pixel_size",) """Possible paths to the pixel size along the x-axis.""" SAMPLE_Y_PIXEL_SIZE_KEYS = ("technique/optic/sample_pixel_size",) """Possible paths to the pixel size along the y-axis.""" DETECTOR_X_PIXEL_SIZE_KEYS = ( "technique/optic/optics_pixel_size", "technique/scan/detector_pixel_size", "technique/detector/{detector_name}/pixel_size", "technique/detector/pixel_size", ) DETECTOR_Y_PIXEL_SIZE_KEYS = ( "technique/optic/optics_pixel_size", "technique/scan/detector_pixel_size", "technique/detector/{detector_name}/pixel_size", "technique/detector/pixel_size", ) SAMPLE_DETECTOR_DISTANCE_KEYS = ("technique/scan/sample_detector_distance",) """Keys used to store the sample to detector distance.""" SOURCE_SAMPLE_DISTANCE_KEYS = ("technique/scan/source_sample_distance",) """Keys used to store the source to sample distance.""" MACHINE_CURRENT_KEYS = ("current",) """Keys used to store machine current values.""" class EDF: """EDF settings for tomography""" MOTOR_POS = ("motor_pos",) """Keys for motor positions.""" MOTOR_MNE = ("motor_mne",) """Keys for motor names (mnemonics).""" ROT_ANGLE = ("srot", "somega") """Keys used to find the rotation angle.""" X_TRANS = ("sx",) """Keys used to find x translation in EDF format.""" Y_TRANS = ("sy",) """Keys used to find y translation in EDF format.""" Z_TRANS = ("sz",) """Keys used to find z translation in EDF format.""" MACHINE_CURRENT = ("srcur", "srcurrent") """Keys used to store machine current values.""" TO_IGNORE = ("_slice_",) """Fields to ignore when processing EDF files.""" DARK_NAMES = ("darkend", "dark") """Names identifying dark images in EDF files.""" REFS_NAMES = ("ref", "refHST") """Names identifying reference images in EDF files.""" nxtomomill-v2.0.1/nxtomomill/test.py000066400000000000000000000003671511430602400176370ustar00rootroot00000000000000from nxtomomill.utils.io import deprecated_warning from nxtomomill.tests import * # noqa F401 deprecated_warning( type_="Module", name="nxtomomill.test", reason="renamed", replacement="nxtomomill.tests", since_version=1.1, ) nxtomomill-v2.0.1/nxtomomill/tests/000077500000000000000000000000001511430602400174425ustar00rootroot00000000000000nxtomomill-v2.0.1/nxtomomill/tests/__init__.py000066400000000000000000000000001511430602400215410ustar00rootroot00000000000000nxtomomill-v2.0.1/nxtomomill/tests/datasets.py000066400000000000000000000005221511430602400216230ustar00rootroot00000000000000import os from tomoscan.tests.datasets import GitlabProject GitlabDataset = GitlabProject( branch_name="nxtomomill", host="https://gitlab.esrf.fr", cache_dir=os.path.join( os.path.dirname(__file__), "__archive__", ), token=None, project_id=4299, # https://gitlab.esrf.fr/tomotools/ci_datasets ) nxtomomill-v2.0.1/nxtomomill/tests/resources/000077500000000000000000000000001511430602400214545ustar00rootroot00000000000000nxtomomill-v2.0.1/nxtomomill/tests/resources/__init__.py000066400000000000000000000000001511430602400235530ustar00rootroot00000000000000nxtomomill-v2.0.1/nxtomomill/tests/resources/edf2nx_config_files/000077500000000000000000000000001511430602400253515ustar00rootroot00000000000000nxtomomill-v2.0.1/nxtomomill/tests/resources/edf2nx_config_files/__init__.py000066400000000000000000000000001511430602400274500ustar00rootroot00000000000000default_file_before_moving_to_pydantic.cfg000066400000000000000000000124451511430602400357020ustar00rootroot00000000000000nxtomomill-v2.0.1/nxtomomill/tests/resources/edf2nx_config_files[GENERAL_SECTION] # general information. # Folder containing .edf files. if not provided from the configuration file must be provided from the command line input_folder = # output file name. If not provided from the configuration file must be provided from the command line output_file = # overwrite output files if exists without asking overwrite = False # remove EDF source files once the conversion is complete. Works only if 'duplicate_data' is True (this is the case by default) delete_edf_source_file = False # Once the conversion is done some checks can be done to check the validity of the conversion. Expected as a list of elements. Possibles tests are: 'compare-output-volume' output_checks = () # file extension. Ignored if the output file is provided and contains an extension file_extension = .nx # dataset file prefix. Usde to determine projections file and info file. If not provided will take the name of input_folder dataset_basename = # path to .info file containing dataset information (Energy, ScanRange, TOMO_N...). If not will deduce it from dataset_basename dataset_info_file = # Log level. Valid levels are "debug", "info", "warning" and "error" log_level = warning # NXtomo title title = # some file pattern leading to ignoring the file. Like reconstructed slice files. patterns_to_ignores = ('_slice_',) # If False then will create embed all the data into a single file avoiding external link to other file. If True then the detector data will point to original edf files. In this case you must be carreful to keep relative paths valid. Warning: to read external dataset you nust be at the hdf5 file working directory. See external link resolution details: https://support.hdfgroup.org/documentation/hdf5/latest/group___h5_l.html#title5 duplicate_data = True # If 'duplicate_data' is set to False then you can specify if you want the link to original files to be 'relative' or 'absolute' external_link_path = relative [EDF_KEYS_SECTION] # section to define EDF keys to pick from headers to deduce information like rotation angle. # motor position key motor_position_key = ('motor_pos',) # key to retrieve indices of each motor in metadata motor_mne_key = ('motor_mne',) # key to be used for rotation angle rot_angle_key = ('srot', 'somega') # key to be used for x translation x_translation_key = ('sx',) # key to be used for y translation y_translation_key = ('sy',) # key to be used for z translation z_translation_key = ('sz',) [DARK_AND_FLAT_SECTION] # section to define dark and flat detection. # prefix of dark field file(s) dark_names_prefix = ('darkend', 'dark') # prefix of flat field file(s) flat_names_prefix = ('ref', 'refHST') [UNIT_SECTION] # Details units system used on SPEC side to save data. All will ne converted to NXtomo default (SI at the exception of energy-keV) # Size used to save pixel size. Must be in of ['nanometer', 'micrometer', 'millimeter', 'centimeter', 'meter'] expected_unit_for_pixel_size = micrometer # Unit used by SPEC to save sample to detector distance. Must be in of ['nanometer', 'micrometer', 'millimeter', 'centimeter', 'meter'] expected_unit_for_distance = millimeter # Unit used by SPEC to save energy. Must be in of ['joule', 'electron_volt', 'kiloelectron_volt', 'millielectron_volt', 'gigaelectron_volt', 'kilojoule'] expected_unit_for_energy = kiloelectron_volt # Unit used by SPEC to save x translation. Must be in of ['nanometer', 'micrometer', 'millimeter', 'centimeter', 'meter'] expected_unit_for_x_translation = millimeter # Unit used by SPEC to save y translation. Must be in of ['nanometer', 'micrometer', 'millimeter', 'centimeter', 'meter'] expected_unit_for_y_translation = millimeter # Unit used by SPEC to save z translation. Must be in of ['nanometer', 'micrometer', 'millimeter', 'centimeter', 'meter'] expected_unit_for_z_translation = millimeter # Unit used by SPEC to save machine current (also aka SRcurrent). Must be in of ['ampere', 'kiloampere', 'milliampere'] expected_unit_for_machine_current = milliampere [SAMPLE_SECTION] # section dedicated to sample definition. # name of the sample sample_name = # Should the rotation angle be computed from scan range and numpy.linspace or should we try to load it from .edf header. force_angle_calculation = True # If rotation angles have to be calculated set numpy.linspace endpoint parameter to this value. If True then the rotation angle value of the last projection will be equal to the `ScanRange` value angle_calculation_endpoint = False # Invert rotation angle values in the case of negative `ScanRange` value angle_calculation_rev_neg_scan_range = True [SOURCE_SECTION] # section dedicated to source definition. # name of the instrument instrument_name = # name of the source source_name = ESRF # type of the source. Must be one of ['Spallation Neutron Source', 'Pulsed Reactor Neutron Source', 'Reactor Neutron Source', 'Synchrotron X-ray Source', 'Pulsed Muon Source', 'Rotating Anode X-ray', 'Fixed Tube X-ray', 'UV Laser', 'Free-Electron Laser', 'Optical Laser', 'Ion Source', 'UV Plasma Source', 'Metal Jet X-ray'] source_type = Synchrotron X-ray Source # source probe. Must be one of ['neutron', 'x-ray', 'muon', 'electron', 'ultraviolet', 'visible light', 'positron', 'proton'] source_probe = x-ray [DETECTOR_SECTION] # section dedicated to detector definition # Detector field of view. Must be in `Half` or `Full` field_of_view = fully_modified_file_before_moving.cfg000066400000000000000000000126511511430602400346530ustar00rootroot00000000000000nxtomomill-v2.0.1/nxtomomill/tests/resources/edf2nx_config_files[GENERAL_SECTION] # general information. # Folder containing .edf files. if not provided from the configuration file must be provided from the command line input_folder = "my_folder" # output file name. If not provided from the configuration file must be provided from the command line output_file = "my_file.nx" # overwrite output files if exists without asking overwrite = 1 # remove EDF source files once the conversion is complete. Works only if 'duplicate_data' is True (this is the case by default) delete_edf_source_file = "True" # Once the conversion is done some checks can be done to check the validity of the conversion. Expected as a list of elements. Possibles tests are: 'compare-output-volume' output_checks = ("check_1") # file extension. Ignored if the output file is provided and contains an extension file_extension = ".nxs" # dataset file prefix. Usde to determine projections file and info file. If not provided will take the name of input_folder dataset_basename = "dataset_basename" # path to .info file containing dataset information (Energy, ScanRange, TOMO_N...). If not will deduce it from dataset_basename dataset_info_file = "dataset_info_file.info" # Log level. Valid levels are "debug", "info", "warning" and "error" log_level = info # NXtomo title title = "my_title" # some file pattern leading to ignoring the file. Like reconstructed slice files. patterns_to_ignores = ('_slice_', '_pattern_') # If False then will create embed all the data into a single file avoiding external link to other file. If True then the detector data will point to original edf files. In this case you must be carreful to keep relative paths valid. Warning: to read external dataset you nust be at the hdf5 file working directory. See external link resolution details: https://support.hdfgroup.org/documentation/hdf5/latest/group___h5_l.html#title5 duplicate_data = False # If 'duplicate_data' is set to False then you can specify if you want the link to original files to be 'relative' or 'absolute' external_link_path = absolute [EDF_KEYS_SECTION] # section to define EDF keys to pick from headers to deduce information like rotation angle. # motor position key motor_position_key = ('motor_pos_key',) # key to retrieve indices of each motor in metadata motor_mne_key = ('motor_mne_key',) # key to be used for rotation angle rot_angle_key = ('srotatation', ) # key to be used for x translation x_translation_key = ('stx',) # key to be used for y translation y_translation_key = ('sty',) # key to be used for z translation z_translation_key = ('stz',) [DARK_AND_FLAT_SECTION] # section to define dark and flat detection. # prefix of dark field file(s) dark_names_prefix = ('prefix_1', 'prefix_2') # prefix of flat field file(s) flat_names_prefix = ('prefix_3', 'prefix_4') [UNIT_SECTION] # Details units system used on SPEC side to save data. All will ne converted to NXtomo default (SI at the exception of energy-keV) # Size used to save pixel size. Must be in of ['nanometer', 'micrometer', 'millimeter', 'centimeter', 'meter'] expected_unit_for_pixel_size = meter # Unit used by SPEC to save sample to detector distance. Must be in of ['nanometer', 'micrometer', 'millimeter', 'centimeter', 'meter'] expected_unit_for_distance = meter # Unit used by SPEC to save energy. Must be in of ['joule', 'electron_volt', 'kiloelectron_volt', 'millielectron_volt', 'gigaelectron_volt', 'kilojoule'] expected_unit_for_energy = gigaelectron_volt # Unit used by SPEC to save x translation. Must be in of ['nanometer', 'micrometer', 'millimeter', 'centimeter', 'meter'] expected_unit_for_x_translation = meter # Unit used by SPEC to save y translation. Must be in of ['nanometer', 'micrometer', 'millimeter', 'centimeter', 'meter'] expected_unit_for_y_translation = meter # Unit used by SPEC to save z translation. Must be in of ['nanometer', 'micrometer', 'millimeter', 'centimeter', 'meter'] expected_unit_for_z_translation = meter # Unit used by SPEC to save machine current (also aka SRcurrent). Must be in of ['ampere', 'kiloampere', 'milliampere'] expected_unit_for_machine_current = kiloampere [SAMPLE_SECTION] # section dedicated to sample definition. # name of the sample sample_name = "my_sample" # Should the rotation angle be computed from scan range and numpy.linspace or should we try to load it from .edf header. force_angle_calculation = False # If rotation angles have to be calculated set numpy.linspace endpoint parameter to this value. If True then the rotation angle value of the last projection will be equal to the `ScanRange` value angle_calculation_endpoint = True # Invert rotation angle values in the case of negative `ScanRange` value angle_calculation_rev_neg_scan_range = False [SOURCE_SECTION] # section dedicated to source definition. # name of the instrument instrument_name = "my_instrument" # name of the source source_name = "my_source" # type of the source. Must be one of ['Spallation Neutron Source', 'Pulsed Reactor Neutron Source', 'Reactor Neutron Source', 'Synchrotron X-ray Source', 'Pulsed Muon Source', 'Rotating Anode X-ray', 'Fixed Tube X-ray', 'UV Laser', 'Free-Electron Laser', 'Optical Laser', 'Ion Source', 'UV Plasma Source', 'Metal Jet X-ray'] source_type = Spallation Neutron Source # source probe. Must be one of ['neutron', 'x-ray', 'muon', 'electron', 'ultraviolet', 'visible light', 'positron', 'proton'] source_probe = neutron [DETECTOR_SECTION] # section dedicated to detector definition # Detector field of view. Must be in `Half` or `Full` field_of_view = "Half" nxtomomill-v2.0.1/nxtomomill/tests/resources/fluo2nx_config_files/000077500000000000000000000000001511430602400255605ustar00rootroot00000000000000nxtomomill-v2.0.1/nxtomomill/tests/resources/fluo2nx_config_files/__init__.py000066400000000000000000000000001511430602400276570ustar00rootroot00000000000000default_file_before_moving_to_pydantic.cfg000066400000000000000000000052301511430602400361030ustar00rootroot00000000000000nxtomomill-v2.0.1/nxtomomill/tests/resources/fluo2nx_config_files[GENERAL_SECTION] # general information. # Path to the folder containing the raw data folder and the fluofit/ subfolder. if not provided from the configuration file must be provided from the command line input_folder = # File produced by the converter. '.nx' extension recommended. If not provided from the configuration file must be provided from the command line output_file = # Dimension of the experiment. 2 for 2D XRFCT, 3 for 3D XRFCT. Default is 3. dimension = 3 # Define a list of (real or virtual) detector names used for the exp (space separated values - no comma). E.g. 'falcon xmap'. If not specified, all detectors are processed. detector_names = () # overwrite output files if exists without asking overwrite = False # file extension. Ignored if the output file is provided and contains an extension file_extension = .nx # In 2D, the exact full name of the folder. In 3D, the folder name prefix (the program will search for folders named _XXX where XXX is a nmber.) If not provided will take the name of input_folder dataset_basename = # path to .info file containing dataset information (Energy, ScanRange, TOMO_N...). If not will deduce it from dataset_basename dataset_info_file = # Log level. Valid levels are "debug", "info", "warning" and "error" log_level = warning # NXtomo title title = # some file pattern leading to ignoring the file. Like reconstructed slice files. patterns_to_ignores = ('_slice_',) # If False then will create embed all the data into a single file avoiding external link to other file. If True then the decetor data will point to original tif files. In this case you must be carreful to keep relative paths valid. Warning: to read external dataset you nust be at the hdf5 file working directory. See external link resolution details: https://support.hdfgroup.org/documentation/hdf5/latest/group___h5_l.html#title5 duplicate_data = True # If 'duplicate_data' is set to False then you can specify if you want the link to original files to be 'relative' or 'absolute' external_link_path = relative [SOURCE_SECTION] # section dedicated to source definition. # name of the instrument instrument_name = # name of the source source_name = ESRF # type of the source. Must be one of ['Spallation Neutron Source', 'Pulsed Reactor Neutron Source', 'Reactor Neutron Source', 'Synchrotron X-ray Source', 'Pulsed Muon Source', 'Rotating Anode X-ray', 'Fixed Tube X-ray', 'UV Laser', 'Free-Electron Laser', 'Optical Laser', 'Ion Source', 'UV Plasma Source', 'Metal Jet X-ray'] source_type = Synchrotron X-ray Source # source probe. Must be one of ['neutron', 'x-ray', 'muon', 'electron', 'ultraviolet', 'visible light', 'positron', 'proton'] source_probe = x-ray fully_modified_file_before_moving.cfg000066400000000000000000000054151511430602400350620ustar00rootroot00000000000000nxtomomill-v2.0.1/nxtomomill/tests/resources/fluo2nx_config_files[GENERAL_SECTION] # general information. # Path to the folder containing the raw data folder and the fluofit/ subfolder. if not provided from the configuration file must be provided from the command line input_folder = "my_folder" # File produced by the converter. '.nx' extension recommended. If not provided from the configuration file must be provided from the command line output_file = output_file.nx # Dimension of the experiment. 2 for 2D XRFCT, 3 for 3D XRFCT. Default is 3. dimension = 2 # Define a list of (real or virtual) detector names used for the exp (space separated values - no comma). E.g. 'falcon xmap'. If not specified, all detectors are processed. detector_names = ("my_detector") # overwrite output files if exists without asking overwrite = True # file extension. Ignored if the output file is provided and contains an extension file_extension = .h5 # In 2D, the exact full name of the folder. In 3D, the folder name prefix (the program will search for folders named _XXX where XXX is a nmber.) If not provided will take the name of input_folder dataset_basename = dataset_basename # path to .info file containing dataset information (Energy, ScanRange, TOMO_N...). If not will deduce it from dataset_basename dataset_info_file = dataset_info_file.info # Log level. Valid levels are "debug", "info", "warning" and "error" log_level = error # NXtomo title title = "my_title" # some file pattern leading to ignoring the file. Like reconstructed slice files. patterns_to_ignores = ('_ignore_',) # If False then will create embed all the data into a single file avoiding external link to other file. If True then the decetor data will point to original tif files. In this case you must be carreful to keep relative paths valid. Warning: to read external dataset you nust be at the hdf5 file working directory. See external link resolution details: https://support.hdfgroup.org/documentation/hdf5/latest/group___h5_l.html#title5 duplicate_data = False # If 'duplicate_data' is set to False then you can specify if you want the link to original files to be 'relative' or 'absolute' external_link_path = absolute [SOURCE_SECTION] # section dedicated to source definition. # name of the instrument instrument_name = my_instrument # name of the source source_name = "my_source" # type of the source. Must be one of ['Spallation Neutron Source', 'Pulsed Reactor Neutron Source', 'Reactor Neutron Source', 'Synchrotron X-ray Source', 'Pulsed Muon Source', 'Rotating Anode X-ray', 'Fixed Tube X-ray', 'UV Laser', 'Free-Electron Laser', 'Optical Laser', 'Ion Source', 'UV Plasma Source', 'Metal Jet X-ray'] source_type = "Pulsed Reactor Neutron Source" # source probe. Must be one of ['neutron', 'x-ray', 'muon', 'electron', 'ultraviolet', 'visible light', 'positron', 'proton'] source_probe = "positron" nxtomomill-v2.0.1/nxtomomill/tests/resources/h52nx_config_files/000077500000000000000000000000001511430602400251275ustar00rootroot00000000000000nxtomomill-v2.0.1/nxtomomill/tests/resources/h52nx_config_files/__init__.py000066400000000000000000000000001511430602400272260ustar00rootroot00000000000000nxtomomill-v2.0.1/nxtomomill/tests/resources/h52nx_config_files/config_file_with_data_scans.cfg000066400000000000000000000166251511430602400333010ustar00rootroot00000000000000[GENERAL_SECTION] # general information. # input file if not provided must be provided from the command line input_file = # output file name. If not provided will use the input file basename and the file extension output_file = # overwrite output files if exists without asking overwrite = False # file extension. Ignored if the output file is provided and contains an extension file_extension = .nx # Log level. Valid levels are "debug", "info", "warning" and "error" log_level = warning # raise an error when met one. Otherwise continue and display an error log raises_error = False # Ask or not the user for any inputs (if missing information) no_input = False # If True then will create a single file for all found sequences. If false create one nexus file per sequence and one master file with links to each sequence single_file = False # Avoid creating the master file no_master_file = True # On recent bliss file (2023) a dedicated group specify datasets to be used for tomography. Defining for example translations, rotation, etc. If True then this group will be ignored and conversion will fallback on using path list provided in the KEYS section ignore_bliss_tomo_config = False # Force output to be a `Full` or a `Half` acquisition. If not provided we parse raw data to try to find this information. field_of_view = # Generate control/data (aka machine current). This part will need to interpolate from existing values and can take time in some cases. create_control_data = True [KEYS_SECTION] # Identify specific path and datasets names to retrieve information from the bliss file. # Nxtomomill will try to deduce cameras from dataset metadata and shape if none provided (default).If provided take the one requested. unix shell-style wildcards are managed valid_camera_names = # List of key to look for in order to find rotation angle rotation_angle_keys = ('rotm', 'mhsrot', 'hsrot', 'mrsrot', 'hrsrot', 'srot', 'srot_eh2', 'diffrz', 'hrrz_trig', 'rot') # List of keys / paths to look for in order to find translation in x sample_x_keys = ('samx', 'psx', 'sax', 'fake_sx') # List of keys / paths to look for in order to find translation in y sample_y_keys = ('samy', 'psv', 'say', 'fake_sy') # List of /paths keys to look for in order to find translation in z translation_z_keys = ('sz', 'difftz', 'hrz', 'pz', 'ntz', 'samtz', 'mrsz') # Key used to deduce the estimated center of rotation for half acquisition translation_y_keys = ('yrot', 'diffty', 'hry') # List of keys to look for diode (if any) diode_keys = ('fpico3',) # List of keys to look for the exposure time exposure_time_keys = ('acq_expo_time',) # List of keys / paths to look for the x **sample** pixel size sample_x_pixel_size_keys = ('technique/optic/sample_pixel_size',) # List of keys / paths to look for the y **sample** pixel size sample_y_pixel_size_keys = ('technique/optic/sample_pixel_size',) # List of keys / paths to look for the x **detector** pixel size detector_x_pixel_size_keys = ('technique/scan/detector_pixel_size', 'technique/detector/{detector_name}/pixel_size', 'technique/detector/pixel_size') # List of keys / paths to look for the y **detector** pixel size detector_y_pixel_size_keys = ('technique/scan/detector_pixel_size', 'technique/detector/{detector_name}/pixel_size', 'technique/detector/pixel_size') # List of keys / paths to look for sample to detector distance sample_detector_distance = ('technique/scan/sample_detector_distance',) # List of keys / paths to look for source to sample distance source_sample_distance = ('technique/scan/source_sample_distance',) [ENTRIES_AND_TITLES_SECTION] # optional section # define titles meaning. Titles allows frame type deduction for each group. # List of root entries (sequence initialization) to convert. If not provided will convert all root entries entries = # List of sub entries (non-root) to ignore sub_entries_to_ignore = # List of title to consider the group/entry as a initialization (sequence start). Ignored if dark_groups, flat_groups, projection_groups ... are provided. init_titles = ('tomo:basic', 'tomo:fullturn', 'sequence_of_scans', 'tomo:halfturn', 'tomo:multiturn', 'tomo:helical', 'tomo:holotomo') # List of title to consider the group/entry as a zseries initialization (sequence start). Ignored if dark_groups, flat_groups, projection_groups ... are provided. zserie_init_titles = ('tomo:zseries',) # List of title to consider the group/entry as a multi-tomo initialization (sequence start). Ignored if dark_groups, flat_groups, projection_groups ... are provided. multitomo_init_titles = ('tomo:pcotomo', 'pcotomo', 'tomo:multitomo', 'multitomo', 'multitomo:basic', 'tomo:multiturn', 'multiturn') # List of title to consider the group/entry as a dark. Ignored if dark_groups, flat_groups, projection_groups ... are provided. dark_titles = ('dark images', 'dark') # List of title to consider the group/entry as a reference / flat. Ignored if dark_groups, flat_groups, projection_groups ... are provided. flat_titles = ('flat', 'reference images', 'ref', 'refend') # List of title to consider the group/entry as a projection. Ignored if dark_groups, flat_groups, projection_groups ... are provided. proj_titles = ('projections', 'ascan rot 0', 'ascan diffrz 0 180 1600 0.1') # List of title to consider the group/entry as an alignment. Ignored if dark_groups, flat_groups, projection_groups ... are provided. alignment_titles = ('static images', 'ascan diffrz 180 0 4 0.1') [FRAME_TYPE_SECTION] # optional section # Allows to define scan to be used for NXTomo conversion # The sequence order will follow the order provided. # list of scans to be converted. Frame type should be provided for each scan. # Expected format is: # * `frame_type` (mandatory): values can be `projection`, `flat`, `dark`, `alignment` or `init`. # * `entry` (mandatory): DataUrl with path to the scan to integrate. If the scan is contained in the input_file then you can only provide path/name of the scan. # * copy (optional): you can provide a different behavior for the this scan (should we duplicate data or not) data_scans = ( (frame_type=projections, entry=silx:///path/to/file?/path/to/scan/node,), (frame_type=darks, entry=/path_relative_to_file, copy=True), ) # You can duplicate data inside the input file or create a link to the original frames. In this case you should keep the relative position of the files default_data_copy = False [MULTITOMO_SECTION] # pcotomo specific section (handled for first version of the pcotomo: bliss < 1.9) # If provided then acquisition parameters `nb_loop` and `nb_tomo` will be ignored. Instead `tomo_n` NXtomo will be created from multi-tomo (also aka pcotomo). All angles before `start_angle_offset_in_degree` will be ignored start_angle_offset_in_degree = None # If 'start_angle_offset_in_degree' provided then specify the number of NXtomo to create. If -1 provided then will create as much NXtomo as possible n_nxtomo = -1 # Angle interval - range to create if 'start_angle_offset_in_degree' is provided. 180 or 360 is expected angle_interval_in_degree = 360 # shift all angle NXtomo angle to `angle_interval_in_degree` interval by shifting them of start_angle_offset_in_degree + angle_interval_in_degree shift_angles = False [EXTRA_PARAMS_SECTION] # optional section # you can predefined values which are missing in the input .h5 file # Handled parameters are ('energy_kev', 'x_sample_pixel_size_m', 'y_sample_pixel_size_m', 'x_detector_pixel_size_m', 'y_detector_pixel_size_m', 'detector_sample_distance_m', 'source_sample_distance_m') default_file_before_moving_to_pydantic.cfg000066400000000000000000000164031511430602400354560ustar00rootroot00000000000000nxtomomill-v2.0.1/nxtomomill/tests/resources/h52nx_config_files[GENERAL_SECTION] # general information. # input file if not provided must be provided from the command line input_file = # output file name. If not provided will use the input file basename and the file extension output_file = # overwrite output files if exists without asking overwrite = False # file extension. Ignored if the output file is provided and contains an extension file_extension = .nx # Log level. Valid levels are "debug", "info", "warning" and "error" log_level = warning # raise an error when met one. Otherwise continue and display an error log raises_error = False # Ask or not the user for any inputs (if missing information) no_input = False # If True then will create a single file for all found sequences. If false create one nexus file per sequence and one master file with links to each sequence single_file = False # Avoid creating the master file no_master_file = True # On recent bliss file (2023) a dedicated group specify datasets to be used for tomography. Defining for example translations, rotation, etc. If True then this group will be ignored and conversion will fallback on using path list provided in the KEYS section ignore_bliss_tomo_config = False # Force output to be a `Full` or a `Half` acquisition. If not provided we parse raw data to try to find this information. field_of_view = # Generate control/data (aka machine current). This part will need to interpolate from existing values and can take time in some cases. create_control_data = True [KEYS_SECTION] # Identify specific path and datasets names to retrieve information from the bliss file. # Nxtomomill will try to deduce cameras from dataset metadata and shape if none provided (default).If provided take the one requested. unix shell-style wildcards are managed valid_camera_names = # List of key to look for in order to find rotation angle rotation_angle_keys = ('rotm', 'mhsrot', 'hsrot', 'mrsrot', 'hrsrot', 'srot', 'srot_eh2', 'diffrz', 'hrrz_trig', 'rot') # List of keys / paths to look for in order to find translation in x sample_x_keys = ('samx', 'psx', 'sax', 'fake_sx') # List of keys / paths to look for in order to find translation in y sample_y_keys = ('samy', 'psv', 'say', 'fake_sy') # List of /paths keys to look for in order to find translation in z translation_z_keys = ('sz', 'difftz', 'hrz', 'pz', 'ntz', 'samtz', 'mrsz') # Key used to deduce the estimated center of rotation for half acquisition translation_y_keys = ('yrot', 'diffty', 'hry') # List of keys to look for diode (if any) diode_keys = ('fpico3',) # List of keys to look for the exposure time exposure_time_keys = ('acq_expo_time',) # List of keys / paths to look for the x **sample** pixel size sample_x_pixel_size_keys = ('technique/optic/sample_pixel_size',) # List of keys / paths to look for the y **sample** pixel size sample_y_pixel_size_keys = ('technique/optic/sample_pixel_size',) # List of keys / paths to look for the x **detector** pixel size detector_x_pixel_size_keys = ('technique/scan/detector_pixel_size', 'technique/detector/{detector_name}/pixel_size', 'technique/detector/pixel_size') # List of keys / paths to look for the y **detector** pixel size detector_y_pixel_size_keys = ('technique/scan/detector_pixel_size', 'technique/detector/{detector_name}/pixel_size', 'technique/detector/pixel_size') # List of keys / paths to look for sample to detector distance sample_detector_distance = ('technique/scan/sample_detector_distance',) # List of keys / paths to look for source to sample distance source_sample_distance = ('technique/scan/source_sample_distance',) [ENTRIES_AND_TITLES_SECTION] # optional section # define titles meaning. Titles allows frame type deduction for each group. # List of root entries (sequence initialization) to convert. If not provided will convert all root entries entries = # List of sub entries (non-root) to ignore sub_entries_to_ignore = # List of title to consider the group/entry as a initialization (sequence start). Ignored if dark_groups, flat_groups, projection_groups ... are provided. init_titles = ('tomo:basic', 'tomo:fullturn', 'sequence_of_scans', 'tomo:halfturn', 'tomo:multiturn', 'tomo:helical', 'tomo:holotomo') # List of title to consider the group/entry as a zseries initialization (sequence start). Ignored if dark_groups, flat_groups, projection_groups ... are provided. zserie_init_titles = ('tomo:zseries',) # List of title to consider the group/entry as a multi-tomo initialization (sequence start). Ignored if dark_groups, flat_groups, projection_groups ... are provided. multitomo_init_titles = ('tomo:pcotomo', 'pcotomo', 'tomo:multitomo', 'multitomo', 'multitomo:basic', 'tomo:multiturn', 'multiturn') # List of title to consider the group/entry as a dark. Ignored if dark_groups, flat_groups, projection_groups ... are provided. dark_titles = ('dark images', 'dark') # List of title to consider the group/entry as a reference / flat. Ignored if dark_groups, flat_groups, projection_groups ... are provided. flat_titles = ('flat', 'reference images', 'ref', 'refend') # List of title to consider the group/entry as a projection. Ignored if dark_groups, flat_groups, projection_groups ... are provided. proj_titles = ('projections', 'ascan rot 0', 'ascan diffrz 0 180 1600 0.1') # List of title to consider the group/entry as an alignment. Ignored if dark_groups, flat_groups, projection_groups ... are provided. alignment_titles = ('static images', 'ascan diffrz 180 0 4 0.1') [FRAME_TYPE_SECTION] # optional section # Allows to define scan to be used for NXTomo conversion # The sequence order will follow the order provided. # list of scans to be converted. Frame type should be provided for each scan. # Expected format is: # * `frame_type` (mandatory): values can be `projection`, `flat`, `dark`, `alignment` or `init`. # * `entry` (mandatory): DataUrl with path to the scan to integrate. If the scan is contained in the input_file then you can only provide path/name of the scan. # * copy (optional): you can provide a different behavior for the this scan (should we duplicate data or not) data_scans = # You can duplicate data inside the input file or create a link to the original frames. In this case you should keep the relative position of the files default_data_copy = False [MULTITOMO_SECTION] # pcotomo specific section (handled for first version of the pcotomo: bliss < 1.9) # If provided then acquisition parameters `nb_loop` and `nb_tomo` will be ignored. Instead `tomo_n` NXtomo will be created from multi-tomo (also aka pcotomo). All angles before `start_angle_offset_in_degree` will be ignored start_angle_offset_in_degree = None # If 'start_angle_offset_in_degree' provided then specify the number of NXtomo to create. If -1 provided then will create as much NXtomo as possible n_nxtomo = -1 # Angle interval - range to create if 'start_angle_offset_in_degree' is provided. 180 or 360 is expected angle_interval_in_degree = 360 # shift all angle NXtomo angle to `angle_interval_in_degree` interval by shifting them of start_angle_offset_in_degree + angle_interval_in_degree shift_angles = False [EXTRA_PARAMS_SECTION] # optional section # you can predefined values which are missing in the input .h5 file # Handled parameters are ('energy_kev', 'x_sample_pixel_size_m', 'y_sample_pixel_size_m', 'x_detector_pixel_size_m', 'y_detector_pixel_size_m', 'detector_sample_distance_m', 'source_sample_distance_m') fully_modified_file_before_moving.cfg000066400000000000000000000154371511430602400344360ustar00rootroot00000000000000nxtomomill-v2.0.1/nxtomomill/tests/resources/h52nx_config_files[GENERAL_SECTION] # general information. # input file if not provided must be provided from the command line input_file = input_file.h5 # output file name. If not provided will use the input file basename and the file extension output_file = "output_file.nx" # overwrite output files if exists without asking overwrite = True # file extension. Ignored if the output file is provided and contains an extension file_extension = .h5 # Log level. Valid levels are "debug", "info", "warning" and "error" log_level = debug # raise an error when met one. Otherwise continue and display an error log raises_error = True # Ask or not the user for any inputs (if missing information) no_input = True # If True then will create a single file for all found sequences. If false create one nexus file per sequence and one master file with links to each sequence single_file = True # Avoid creating the master file no_master_file = False # On recent bliss file (2023) a dedicated group specify datasets to be used for tomography. Defining for example translations, rotation, etc. If True then this group will be ignored and conversion will fallback on using path list provided in the KEYS section ignore_bliss_tomo_config = True # Force output to be a `Full` or a `Half` acquisition. If not provided we parse raw data to try to find this information. field_of_view = Full # Generate control/data (aka machine current). This part will need to interpolate from existing values and can take time in some cases. create_control_data = False [KEYS_SECTION] # Identify specific path and datasets names to retrieve information from the bliss file. # Nxtomomill will try to deduce cameras from dataset metadata and shape if none provided (default).If provided take the one requested. unix shell-style wildcards are managed valid_camera_names = ("my_camera", ) # List of key to look for in order to find rotation angle rotation_angle_keys = ('rotm', ) # List of keys / paths to look for in order to find translation in x sample_x_keys = ('samx', ) # List of keys / paths to look for in order to find translation in y sample_y_keys = ('samy', ) # List of /paths keys to look for in order to find translation in z translation_z_keys = ('zrot', ) # Key used to deduce the estimated center of rotation for half acquisition translation_y_keys = ('yrot', ) # List of keys to look for diode (if any) diode_keys = ('fpico',) # List of keys to look for the exposure time exposure_time_keys = ('exposure_time',) # List of keys / paths to look for the x **sample** pixel size sample_x_pixel_size_keys = ('technique/optic/my_sample_pixel_size',) # List of keys / paths to look for the y **sample** pixel size sample_y_pixel_size_keys = ('technique/optic/my_sample_pixel_size2',) # List of keys / paths to look for the x **detector** pixel size detector_x_pixel_size_keys = ('technique/scan/detector_pixel_size',) # List of keys / paths to look for the y **detector** pixel size detector_y_pixel_size_keys = ('technique/scan/detector_pixel_size2',) # List of keys / paths to look for sample to detector distance sample_detector_distance = ('technique/scan/my_sample_detector_distance',) # List of keys / paths to look for source to sample distance source_sample_distance = ('technique/scan/source_my_sample_distance',) [ENTRIES_AND_TITLES_SECTION] # optional section # define titles meaning. Titles allows frame type deduction for each group. # List of root entries (sequence initialization) to convert. If not provided will convert all root entries entries = "1.1,3.1" # List of sub entries (non-root) to ignore sub_entries_to_ignore = 2.1, # List of title to consider the group/entry as a initialization (sequence start). Ignored if dark_groups, flat_groups, projection_groups ... are provided. init_titles = ('my_tomo:basic', ) # List of title to consider the group/entry as a zseries initialization (sequence start). Ignored if dark_groups, flat_groups, projection_groups ... are provided. zserie_init_titles = ('my_tomo:zseries',) # List of title to consider the group/entry as a multi-tomo initialization (sequence start). Ignored if dark_groups, flat_groups, projection_groups ... are provided. multitomo_init_titles = ('my_tomo:pcotomo',) # List of title to consider the group/entry as a dark. Ignored if dark_groups, flat_groups, projection_groups ... are provided. dark_titles = ('my dark images', ) # List of title to consider the group/entry as a reference / flat. Ignored if dark_groups, flat_groups, projection_groups ... are provided. flat_titles = ('my flat', ) # List of title to consider the group/entry as a projection. Ignored if dark_groups, flat_groups, projection_groups ... are provided. proj_titles = ('my projections', ) # List of title to consider the group/entry as an alignment. Ignored if dark_groups, flat_groups, projection_groups ... are provided. alignment_titles = ('my static images', ) [FRAME_TYPE_SECTION] # optional section # Allows to define scan to be used for NXTomo conversion # The sequence order will follow the order provided. # list of scans to be converted. Frame type should be provided for each scan. # Expected format is: # * `frame_type` (mandatory): values can be `projection`, `flat`, `dark`, `alignment` or `init`. # * `entry` (mandatory): DataUrl with path to the scan to integrate. If the scan is contained in the input_file then you can only provide path/name of the scan. # * copy (optional): you can provide a different behavior for the this scan (should we duplicate data or not) data_scans = # note: tested on a dedicated file # You can duplicate data inside the input file or create a link to the original frames. In this case you should keep the relative position of the files default_data_copy = True [MULTITOMO_SECTION] # pcotomo specific section (handled for first version of the pcotomo: bliss < 1.9) # If provided then acquisition parameters `nb_loop` and `nb_tomo` will be ignored. Instead `tomo_n` NXtomo will be created from multi-tomo (also aka pcotomo). All angles before `start_angle_offset_in_degree` will be ignored start_angle_offset_in_degree = 24.2 # If 'start_angle_offset_in_degree' provided then specify the number of NXtomo to create. If -1 provided then will create as much NXtomo as possible n_nxtomo = 100 # Angle interval - range to create if 'start_angle_offset_in_degree' is provided. 180 or 360 is expected angle_interval_in_degree = 180 # shift all angle NXtomo angle to `angle_interval_in_degree` interval by shifting them of start_angle_offset_in_degree + angle_interval_in_degree shift_angles = True [EXTRA_PARAMS_SECTION] # optional section # you can predefined values which are missing in the input .h5 file # Handled parameters are ('energy_kev', 'x_sample_pixel_size_m', 'y_sample_pixel_size_m', 'x_detector_pixel_size_m', 'y_detector_pixel_size_m', 'detector_sample_distance_m', 'source_sample_distance_m') energy_kev = 12.5 y_detector_pixel_size_m = 2.3e-6 nxtomomill-v2.0.1/nxtomomill/tests/test_version.py000066400000000000000000000001661511430602400225430ustar00rootroot00000000000000# coding: utf-8 def test_version(): from nxtomomill import version assert isinstance(version.version, str) nxtomomill-v2.0.1/nxtomomill/tests/utils/000077500000000000000000000000001511430602400206025ustar00rootroot00000000000000nxtomomill-v2.0.1/nxtomomill/tests/utils/__init__.py000066400000000000000000000000001511430602400227010ustar00rootroot00000000000000nxtomomill-v2.0.1/nxtomomill/tests/utils/bliss.py000066400000000000000000000631141511430602400222750ustar00rootroot00000000000000# coding: utf-8 from __future__ import annotations import datetime import os import numpy import h5py from tomoscan.io import HDF5File class MockBlissAcquisition: """ :param n_sequence: number of sequence to create :param n_scan_per_sequence: number of scans (projection series) per sequence :param n_projections_per_scan: number of projection frame in a scan :param n_darks: number of dark frame in the series. Only one series at the beginning :param n_flats: number of flats to create. In this case will only create one series of n flats after dark if any :param output_dir: will contain the proposal file and one folder per sequence. :param str acqui_type: acquisition type. Can be "basic", "multitomo", "zseries-v1", "zseries-v3 :param sequence z_values: if acqui_type is zseries then users should provide the serie of values for z (one per stage) :param nb_loop: number of multitomo loop for v1 of bliss multitomo :param nb_tomo: number of tomo per loop for v1 of bliss multitomo :param nb_turns: number of turns for v2 of bliss multitomo ( <=> nb NXtomo to generate) :param file_name_prefix: bliss file prefix name :param file_name_z_fill: optional z fill for the file name index. If None then file index will not be 'z filled' :param create_tomo_config: if True create the 'tomo_config' group under instrument which contains metadata describing the acquisition (which dataset to read for rotation, translation ...) """ def __init__( self, n_sample, n_sequence, n_scan_per_sequence, n_darks, n_flats, output_dir, with_nx_detector_attr=True, detector_name="pcolinux", acqui_type="basic", z_values=None, nb_loop=None, nb_tomo=None, nb_turns=None, with_rotation_motor_info=True, frame_data_type=numpy.uint16, file_name_prefix="sample", file_name_z_fill=None, create_tomo_config: bool = True, ebs_tomo_version: str | None = None, z_series_v_3_options=None, rotation_is_clockwise: bool = False, ): self._n_darks = n_darks self._n_flats = n_flats self._n_scan_per_sequence = n_scan_per_sequence self.__folder = output_dir if not os.path.exists(output_dir): os.makedirs(output_dir) self.__proposal_file = os.path.join(self.__folder, "ihproposal_file.h5") if acqui_type not in ("pcotomo", "multitomo"): if nb_loop is not None or nb_tomo is not None: raise ValueError( "nb_loop and nb_tomo are only handled by acqui_type: `multitomo`" ) else: if not ( nb_turns is not None or (nb_loop is not None and nb_tomo is not None) ): raise ValueError( "nb_turns should be provided or nb_loop and nb_tomo must be provided" ) # create sample self.__samples = [] for sample_i in range(n_sample): if file_name_z_fill is None: dir_name = f"{file_name_prefix}_{sample_i}" else: dir_name = f"{file_name_prefix}_{str(sample_i).zfill(file_name_z_fill)}" sample_dir = os.path.join(self.path, dir_name) os.mkdir(sample_dir) sample_file = os.path.join(sample_dir, dir_name + ".h5") if acqui_type == "basic": acqui_tomo = _BlissBasicTomo( sample_dir=sample_dir, sample_file=sample_file, n_sequence=n_sequence, n_scan_per_sequence=n_scan_per_sequence, n_darks=n_darks, n_flats=n_flats, with_nx_detector_attr=with_nx_detector_attr, detector_name=detector_name, with_rotation_motor_info=with_rotation_motor_info, frame_data_type=frame_data_type, create_tomo_config=create_tomo_config, ebs_tomo_version=ebs_tomo_version, rotation_is_clockwise=rotation_is_clockwise, ) elif acqui_type in ("pcotomo", "multitomo"): acqui_tomo = _BlissMultiTomo( sample_dir=sample_dir, sample_file=sample_file, n_sequence=n_sequence, n_scan_per_sequence=n_scan_per_sequence, n_darks=n_darks, n_flats=n_flats, with_nx_detector_attr=with_nx_detector_attr, detector_name=detector_name, with_rotation_motor_info=with_rotation_motor_info, nb_loop=nb_loop, nb_tomo=nb_tomo, nb_turns=nb_turns, frame_data_type=frame_data_type, create_tomo_config=create_tomo_config, ebs_tomo_version=ebs_tomo_version, rotation_is_clockwise=rotation_is_clockwise, ) elif acqui_type in ("z-series-v1", "z-series-v3"): if z_values is None: raise ValueError("for z-series z_values should be provided") acqui_tomo = _BlissZseriesTomo( sample_dir=sample_dir, sample_file=sample_file, n_sequence=n_sequence, n_scan_per_sequence=n_scan_per_sequence, n_darks=n_darks, n_flats=n_flats, with_nx_detector_attr=with_nx_detector_attr, detector_name=detector_name, z_values=z_values, with_rotation_motor_info=with_rotation_motor_info, frame_data_type=frame_data_type, create_tomo_config=create_tomo_config, ebs_tomo_version=ebs_tomo_version, z_series_version=acqui_type.split("-")[-1], z_series_v_3_options=z_series_v_3_options, rotation_is_clockwise=rotation_is_clockwise, ) else: raise NotImplementedError("") self.__samples.append(acqui_tomo) @property def samples(self): return self.__samples @property def proposal_file(self): # for now a simple file return self.__proposal_file @property def path(self): return self.__folder class _BlissSample: """ Simple mock of a bliss sample. For now we only create the hierarchy of files. """ def __init__( self, sample_dir, sample_file, n_sequence, n_scan_per_sequence, n_darks, n_flats, detector_name, with_nx_detector_attr=True, with_rotation_motor_info=True, frame_data_type=numpy.uint16, create_tomo_config: bool = True, ebs_tomo_version: str | None = None, rotation_is_clockwise: bool = False, ): self._with_nx_detector_attr = with_nx_detector_attr self._sample_dir = sample_dir self._sample_file = sample_file self._n_sequence = n_sequence self._n_scan_per_seq = n_scan_per_sequence self._n_darks = n_darks self._n_flats = n_flats self._scan_folders = [] self._index = 1 self._detector_name = detector_name self._det_width = 64 self._det_height = 64 self._tomo_n = 10 self._energy = 19.0 self._sample_detector_distance = 100.0 # in mm self._source_sample_distance = 52000.0 # in mm self._sample_pixel_size = (0.0065, 0.0066) self._detector_pixel_size = (0.00022, 0.00022) self._with_rotation_motor_info = with_rotation_motor_info self._frame_data_type = frame_data_type self._create_tomo_config = create_tomo_config self._ebs_tomo_version = ebs_tomo_version assert isinstance(rotation_is_clockwise, bool) self._rotation_is_clockwise = rotation_is_clockwise for _ in range(n_sequence): self.add_sequence() @property def frame_data_type(self): return self._frame_data_type def get_next_free_index(self): idx = self._index self._index += 1 return idx @property def current_scan_index(self) -> int: return self._index - 1 def get_main_entry_title(self): raise NotImplementedError("Base class") @staticmethod def get_title(scan_type): if scan_type == "dark": return "dark images" elif scan_type == "flat": return "reference images 1" elif scan_type == "projection": return "projections 1 - 2000" else: raise ValueError("Not implemented") def create_entry_and_technique(self, seq_ini_index): # add sequence init information with HDF5File(self.sample_file, mode="a") as h5f: seq_node = h5f.require_group(str(seq_ini_index) + ".1") seq_node.attrs["NX_class"] = "NXentry" seq_node["title"] = self.get_main_entry_title() seq_node.require_group("instrument/positioners") # write energy seq_node["technique/scan/energy"] = self._energy seq_node["technique/scan/tomo_n"] = self._tomo_n * self._n_scan_per_seq seq_node["technique/scan/sample_detector_distance"] = ( self._sample_detector_distance ) seq_node["technique/scan/sample_detector_distance"].attrs["units"] = "mm" seq_node["technique/scan/source_sample_distance"] = ( self._source_sample_distance ) seq_node["technique/scan/source_sample_distance"].attrs["units"] = "mm" seq_node["technique/optic/sample_pixel_size"] = numpy.asarray( self._sample_pixel_size ) seq_node["technique/detector/pixel_size"] = numpy.asarray( self._detector_pixel_size ) seq_node["start_time"] = str(datetime.datetime.now()) seq_node["end_time"] = str( datetime.datetime.now() + datetime.timedelta(minutes=10) ) if self._create_tomo_config: self._add_tomo_config(seq_node) @staticmethod def get_next_group_name(seq_ini_index, scan_idx): return str(scan_idx) + ".1" def add_scan( self, scan_type, seq_ini_index, z_value, skip_title=False, nb_loop=None, nb_tomo=None, nb_turns=1, ): """ :param nb_loop: number of loop in multitomo use case. Else must be 1 :param nb_tomo: number of tomography done in multitomo 'per iteration' use case. Else must be 1 """ scan_idx = self.get_next_free_index() scan_name = str(scan_idx).zfill(4) scan_path = os.path.join(self.path, scan_name) self._scan_folders.append(_BlissScan(folder=scan_path, scan_type=scan_type)) if nb_turns is not None: nb_nxtomo = nb_turns if nb_tomo is not None or nb_loop is not None: raise ValueError( "nb_tomo and nb_loop should be provided or nb_turns. Not both" ) elif nb_loop is not None and nb_tomo is not None: nb_nxtomo = nb_loop * nb_tomo if nb_turns is not None: raise ValueError( "nb_tomo and nb_loop should be provided or nb_turns. Not both" ) else: raise ValueError( "nb_tomo and nb_loop should be provided or nb_turns. None provided" ) # register the scan information with HDF5File(self.sample_file, mode="a") as h5f: seq_node = h5f.require_group(str(scan_idx) + ".1") if "start_time" not in seq_node: seq_node["start_time"] = str(datetime.datetime.now()) # write title title = self.get_title(scan_type=scan_type) if not skip_title: seq_node["title"] = title # write data data = ( numpy.random.random( self._det_height * self._det_width * self._tomo_n * nb_nxtomo ) * 256 ) n_frames = self._tomo_n * nb_nxtomo data = data.reshape(n_frames, self._det_height, self._det_width) data = data.astype(self.frame_data_type) det_path_1 = "/".join(("instrument", self._detector_name)) det_grp = seq_node.require_group(det_path_1) det_grp["data"] = data if self._with_nx_detector_attr: det_grp.attrs["NX_class"] = "NXdetector" acq_grp = det_grp.require_group("acq_parameters") acq_grp["acq_expo_time"] = 4 det_path_2 = "/".join(("technique", "scan", self._detector_name)) seq_node[det_path_2] = data seq_node.attrs["NX_class"] = "NXentry" # write rotation angle value and translations instrument_group = seq_node.require_group("instrument") positioners_grp = instrument_group.require_group("positioners") positioners_grp["hrsrot"] = numpy.linspace( start=0.0, stop=360, num=n_frames ) positioners_grp["sx"] = numpy.array(numpy.random.random(size=n_frames)) positioners_grp["sy"] = numpy.random.random(size=n_frames) positioners_grp["sz"] = numpy.asarray([z_value] * n_frames) positioners_grp["yrot"] = numpy.random.random(size=n_frames) if self._with_rotation_motor_info: scan_node = seq_node.require_group("technique/scan") scan_node["motor"] = ("rotation", "hrsrot", "srot") if self._ebs_tomo_version is not None: technique_group = seq_node.require_group("technique") technique_group.attrs["tomo_version"] = self._ebs_tomo_version def _add_tomo_config(self, group: h5py.Group): technique_group = group.require_group("technique") tomo_config_group = technique_group.require_group("tomoconfig") tomo_config_group["rotation"] = ["hrsrot"] tomo_config_group["detector"] = [ self._detector_name, ] tomo_config_group["sample_x"] = ["sx"] tomo_config_group["sample_y"] = ["sy"] tomo_config_group["translation_z"] = ["sz"] tomo_config_group["translation_y"] = ["yrot"] tomo_config_group["rotation_is_clockwise"] = self._rotation_is_clockwise def add_sequence(self): """Add a sequence to the bliss file""" raise NotImplementedError("Base class") @property def path(self): return self._sample_dir @property def sample_directory(self): return self._sample_dir @property def sample_file(self): return self._sample_file def scans_folders(self): return self._scan_folders @property def n_darks(self): return self._n_darks @property def with_rotation_motor_info(self): return self._with_rotation_motor_info class _BlissScan: """ mock of a bliss scan """ def __init__(self, folder, scan_type: str): assert scan_type in ("dark", "flat", "projection") self.__path = folder def path(self): return self.__path class _BlissBasicTomo(_BlissSample): def get_main_entry_title(self): return "tomo:fullturn" def add_sequence(self): # reserve the index for the 'initialization' sequence. No scan folder # will be created for this one. seq_ini_index = self.get_next_free_index() self.create_entry_and_technique(seq_ini_index=seq_ini_index) if self.n_darks > 0: self.add_scan(scan_type="dark", seq_ini_index=seq_ini_index, z_value=1) if self._n_flats > 0: self.add_scan(scan_type="flat", seq_ini_index=seq_ini_index, z_value=1) for _ in range(self._n_scan_per_seq): self.add_scan( scan_type="projection", seq_ini_index=seq_ini_index, z_value=1 ) class _BlissMultiTomo(_BlissSample): def __init__( self, sample_dir, sample_file, n_sequence, n_scan_per_sequence, n_darks, n_flats, detector_name, with_nx_detector_attr=True, with_rotation_motor_info=True, nb_loop=None, nb_tomo=None, nb_turns=1, frame_data_type=numpy.uint16, create_tomo_config: bool = True, ebs_tomo_version=None, rotation_is_clockwise: bool = False, ): self.nb_loop = nb_loop self.nb_tomo = nb_tomo self.nb_turns = nb_turns super().__init__( sample_dir, sample_file, n_sequence, n_scan_per_sequence, n_darks, n_flats, detector_name, with_nx_detector_attr, with_rotation_motor_info=with_rotation_motor_info, frame_data_type=frame_data_type, create_tomo_config=create_tomo_config, ebs_tomo_version=ebs_tomo_version, rotation_is_clockwise=rotation_is_clockwise, ) if nb_loop is not None and nb_tomo is not None: if nb_turns is not None: raise ValueError( "All of nb_loop, nb_tomo and nb_turns provided. Unable to deduce the pcotomo version" ) multitomo_version = 1 elif nb_turns is not None: multitomo_version = 2 else: multitomo_version = None if multitomo_version is not None: # write Bliss version in attrs with HDF5File(self.sample_file, mode="a") as h5f: if "creator_version" not in h5f.attrs: if multitomo_version == 1: h5f.attrs["creator_version"] = "1.2.3" if multitomo_version == 2: h5f.attrs["creator_version"] = "1.10.0" def get_main_entry_title(self): return "tomo:multitomo" def add_sequence(self): # reserve the index for the 'initialization' sequence. No scan folder # will be created for this one. seq_ini_index = self.get_next_free_index() self.create_entry_and_technique(seq_ini_index=seq_ini_index) # start dark if self.n_darks > 0: self.add_scan(scan_type="dark", seq_ini_index=seq_ini_index, z_value=1) # start flat if self._n_flats > 0: self.add_scan(scan_type="flat", seq_ini_index=seq_ini_index, z_value=1) for _ in range(self._n_scan_per_seq): self.add_scan( scan_type="projection", seq_ini_index=seq_ini_index, z_value=1, nb_loop=self.nb_loop, nb_tomo=self.nb_tomo, nb_turns=self.nb_turns, ) # end flat if self._n_flats > 0: self.add_scan(scan_type="flat", seq_ini_index=seq_ini_index, z_value=1) def add_scan( self, scan_type, seq_ini_index, z_value, skip_title=False, nb_loop=None, nb_tomo=None, nb_turns=1, ): super().add_scan( scan_type, seq_ini_index, z_value, skip_title, nb_loop, nb_tomo, nb_turns ) if scan_type == "projection": # register multi-tomo specific informations (only for projections) with HDF5File(self.sample_file, mode="a") as h5f: seq_node = h5f.require_group(str(self._index - 1) + ".1") scan_grp = seq_node.require_group("technique/proj") if nb_loop is not None and "nb_loop" not in scan_grp: scan_grp["nb_loop"] = nb_loop if nb_tomo is not None and "nb_tomo" not in scan_grp: scan_grp["nb_tomo"] = nb_tomo if nb_turns is not None and "nb_turns" not in scan_grp: scan_grp["nb_turns"] = nb_turns if "tomo_n" not in scan_grp: scan_grp["tomo_n"] = self._tomo_n class _BlissZseriesTomo(_BlissSample): def __init__( self, sample_dir, sample_file, n_sequence, n_scan_per_sequence, n_darks, n_flats, detector_name, z_values, z_series_version: str, with_nx_detector_attr=True, with_rotation_motor_info=True, frame_data_type=numpy.uint16, create_tomo_config: bool = True, ebs_tomo_version: str = None, z_series_v_3_options=None, rotation_is_clockwise: bool = False, ): assert z_series_version in ("v1", "v3") self.z_series_version = z_series_version self._z_values = z_values self._z_series_v_3_options = z_series_v_3_options super().__init__( sample_dir=sample_dir, sample_file=sample_file, n_sequence=n_sequence, n_scan_per_sequence=n_scan_per_sequence, n_darks=n_darks, n_flats=n_flats, detector_name=detector_name, with_nx_detector_attr=with_nx_detector_attr, with_rotation_motor_info=with_rotation_motor_info, frame_data_type=frame_data_type, create_tomo_config=create_tomo_config, ebs_tomo_version=ebs_tomo_version, rotation_is_clockwise=rotation_is_clockwise, ) def get_main_entry_title(self): return "tomo:zseries" def create_dark_at_start(self) -> bool: if self.z_series_version == "v1": return True else: return self._z_series_v_3_options["dark_at_start"] def create_flat_at_start(self) -> bool: if self.z_series_version == "v1": return True else: return self._z_series_v_3_options["flat_at_start"] def create_dark_at_end(self) -> bool: if self.z_series_version == "v1": return True else: return self._z_series_v_3_options["dark_at_end"] def create_flat_at_end(self) -> bool: if self.z_series_version == "v1": return True else: return self._z_series_v_3_options["flat_at_end"] def create_intermediary_flat(self) -> bool: return self.z_series_version == "v1" def create_intermediary_dark(self) -> bool: return self.z_series_version == "v1" def add_sequence(self): # reserve the index for the 'initialization' sequence. No scan folder # will be created for this one. if self.z_series_version == "v1": seq_ini_index = self.get_next_free_index() self.create_entry_and_technique(seq_ini_index=seq_ini_index) for z_value in self._z_values: if self.z_series_version == "v3": seq_ini_index = self.get_next_free_index() self.create_entry_and_technique(seq_ini_index=seq_ini_index) if z_value == self._z_values[0]: create_dark = self.create_dark_at_start() create_flat = self.create_flat_at_start() elif z_value == self._z_values[-1]: create_dark = self.create_dark_at_end() create_flat = self.create_flat_at_end() else: create_dark = self.create_intermediary_dark() create_flat = self.create_intermediary_flat() if create_dark and self.n_darks > 0: self.add_scan( scan_type="dark", seq_ini_index=seq_ini_index, z_value=z_value ) if create_flat and self._n_flats > 0: self.add_scan( scan_type="flat", seq_ini_index=seq_ini_index, z_value=z_value ) for _ in range(self._n_scan_per_seq): self.add_scan( scan_type="projection", seq_ini_index=seq_ini_index, z_value=z_value ) def create_entry_and_technique(self, seq_ini_index): super().create_entry_and_technique(seq_ini_index=seq_ini_index) # add sequence init information if self.z_series_version == "v3": with HDF5File(self.sample_file, mode="a") as h5f: seq_node = h5f.require_group(str(seq_ini_index) + ".1") seq_node.attrs["NX_class"] = "NXentry" # write scab flags seq_node["technique/scan_flags/dark_images_at_start"] = ( self._z_series_v_3_options["dark_at_start"] ) seq_node["technique/scan_flags/dark_images_at_end"] = ( self._z_series_v_3_options["dark_at_end"] ) seq_node["technique/scan_flags/ref_images_at_start"] = ( self._z_series_v_3_options["flat_at_start"] ) seq_node["technique/scan_flags/ref_images_at_end"] = ( self._z_series_v_3_options["flat_at_end"] ) def add_scan( self, scan_type, seq_ini_index, z_value, skip_title=False, nb_loop=None, nb_tomo=None, nb_turns=1, ): super().add_scan( scan_type, seq_ini_index, z_value, skip_title, nb_loop, nb_tomo, nb_turns ) with HDF5File(self.sample_file, mode="a") as h5f: seq_node = h5f.require_group(str(self.current_scan_index) + ".1") seq_node["sample/name"] = "mysample_0000" nxtomomill-v2.0.1/nxtomomill/tests/utils/dxfile.py000066400000000000000000000035461511430602400224370ustar00rootroot00000000000000# coding: utf-8 from datetime import datetime import numpy.random from tomoscan.io import HDF5File class MockDxFile: """ Mock DXFile """ def __init__( self, file_path, n_projection, n_darks, n_flats, det_height=128, det_width=128, data_path="/", ): self._file_path = file_path self._n_projection = n_projection self._n_darks = n_darks self._n_flats = n_flats self._det_height = det_height self._det_width = det_width self._n_projection = n_projection self.data_flat = None self.data_proj = None self.data_dark = None with HDF5File(file_path, mode="w") as h5f: root_grp = h5f.require_group(data_path) exchange_grp = root_grp.require_group("exchange") # create data self.data_proj = self._create_random_frames(self._n_projection) exchange_grp["data"] = self.data_proj # create data_dark if self._n_darks > 0: self.data_dark = self._create_random_frames(self._n_darks) exchange_grp["data_dark"] = self.data_dark # create data_white if self._n_flats > 0: self.data_flat = self._create_random_frames(self._n_flats) exchange_grp["data_white"] = self.data_flat root_grp["file_creation_datetime"] = str(datetime.now()) # for now some information are not mock and used like # exposure_period, x_binning, roi... did not had a concrete # example about it def _create_random_frames(self, n_frames): data = numpy.random.random(self._det_height * self._det_width * n_frames) * 256 data = data.astype(numpy.uint16) return data.reshape((n_frames, self._det_height, self._det_width)) nxtomomill-v2.0.1/nxtomomill/utils/000077500000000000000000000000001511430602400174405ustar00rootroot00000000000000nxtomomill-v2.0.1/nxtomomill/utils/__init__.py000066400000000000000000000011561511430602400215540ustar00rootroot00000000000000"""module with various utils like modifying image_key values, checking if and HDF5 entry looks like an NXtomo entry...""" from .frameappender import FrameAppender # noqa F401 from .utils import FileExtension # noqa F401 from .utils import add_dark_flat_nx_file # noqa F401 from .utils import change_image_key_control # noqa F401 from .utils import embed_url # noqa F401 from .utils import get_file_name # noqa F401 from .utils import get_tuple_of_keys_from_cmd # noqa F401 from .utils import is_nx_tomo_entry # noqa F401 # expose ImageKey from nxtomo from nxtomo.nxobject.nxdetector import ImageKey # noqa F401 nxtomomill-v2.0.1/nxtomomill/utils/flat_reducer.py000066400000000000000000000273701511430602400224620ustar00rootroot00000000000000# coding: utf-8 from __future__ import annotations import logging import os import numpy from tomoscan.esrf.scan.nxtomoscan import NXtomoScan from tomoscan.esrf.scan.utils import cwd_context from tomoscan.framereducer.method import ReduceMethod from silx.io.utils import open as open_hdf5 from nxtomo.application.nxtomo import NXtomo from nxtomo.nxobject.nxdetector import ImageKey from ..utils.utils import strip_extension logging.basicConfig(level=logging.INFO) _logger = logging.getLogger(__name__) __all__ = ["flat_reducer", "extract_darks_flats"] def extract_darks_flats( dataset_file_name: str, entry_name: str, save_intermediated: bool = False, target_filename: str | None = None, target_entry_name: str | None = None, method: str = "median", reuse_intermediated: bool = False, use_projections_for_flats: bool = False, dark_default_value=None, ): dataset_file_name = os.path.abspath(dataset_file_name) target_entry_name = target_entry_name if target_entry_name else entry_name dirname = os.path.dirname(dataset_file_name) basename = os.path.basename(dataset_file_name) if not dirname: dirname = "./" if target_filename is not None: target_filename = os.path.abspath(target_filename) with cwd_context(dirname): if reuse_intermediated: scan = NXtomoScan(target_filename, target_entry_name) reduced_flats, metadata_flats = scan.load_reduced_flats(return_info=True) reduced_darks, metadata_darks = scan.load_reduced_darks(return_info=True) else: nxt = NXtomo() nxt.load(basename, data_path=entry_name) if use_projections_for_flats: where_proj = [k.value == 0 for k in nxt.instrument.detector.image_key] where_flat = [k.value == 1 for k in nxt.instrument.detector.image_key] nxt.instrument.detector.image_key_control[where_proj] = ( ImageKey.FLAT_FIELD ) nxt.instrument.detector.image_key_control[where_flat] = ImageKey.INVALID file_path = f"{basename}_edited_keys_scan.nx" if os.path.isfile(file_path): os.remove(file_path) nxt.save(file_path, entry_name) scan = NXtomoScan(file_path, entry_name) reduced_flats, metadata_flats = scan.compute_reduced_flats( method, return_info=True ) reduced_darks, metadata_darks = scan.compute_reduced_darks( return_info=True ) if len(reduced_darks) == 0: assert len(reduced_flats), " We expect to find at least some flats" dim_2, dim_1 = reduced_flats[list(reduced_flats.keys())[0]].shape _logger.warning( f" patching with a default dark of size {dim_1} for horizontal , {dim_2} for vertical and default value {dark_default_value}" ) assert ( dark_default_value is not None ) > 0, f"No raw darks found in the dataset {scan} and 'dark_default_value' not provided. Unable to get any reduced darks." reduced_darks[0] = numpy.full( (dim_2, dim_1), dark_default_value, dtype="f" ) metadata_darks = metadata_flats else: scan = NXtomoScan(basename, entry_name) reduced_flats, metadata_flats = scan.compute_reduced_flats( method, return_info=True ) reduced_darks, metadata_darks = scan.compute_reduced_darks( return_info=True ) reduced_flats, metadata_flats = scan.compute_reduced_flats( method, return_info=True ) if save_intermediated: scan = NXtomoScan(target_filename, target_entry_name) scan.save_reduced_flats( reduced_flats, flats_infos=metadata_flats, overwrite=True ) scan.save_reduced_darks( reduced_darks, darks_infos=metadata_darks, overwrite=True ) return_dict = { "flat": {"images": reduced_flats, "meta": metadata_flats}, "dark": {"images": reduced_darks, "meta": metadata_darks}, } return __RefsDarks(return_dict, entry_name), return_dict class __RefsDarks: def __init__(self, dict_or_file_name, entry_name): self.dict_or_file_name = dict_or_file_name self.entry_name = entry_name self.flat_image, self.flat_current = self._take_image_and_meta("flat") self.dark_image, self.dark_current = self._take_image_and_meta("dark") def _take_image_and_meta(self, what) -> tuple: """ :return: a tuple as (image, current:float|None) """ if isinstance(self.dict_or_file_name, dict): group = self.dict_or_file_name[what] # [self.entry_name] image = None for key in group["images"]: if ( isinstance(key, int) or (isinstance(key, str) and key.isnumeric()) or (numpy.isdtype(numpy.dtype(key), "integral")) ): if image is None: image = group["images"][key] else: _logger.warning("More than one image found.") if len(group["meta"].machine_current) > 0: current = group["meta"].machine_current[0] else: current = None else: file_name_tmp = f"{strip_extension(self.dict_or_file_name)}_{what}.h5" with open_hdf5(file_name_tmp) as f: group = f[self.entry_name] group = f[what] image = None current = group["machine_current"][()][0] for key in group: if ( isinstance(key, int) or (isinstance(key, str) and key.isnumeric()) or (numpy.isdtype(numpy.dtype(key), "integral")) ): if image is None: image = group[key][()] else: raise ValueError( f"More than one image found in {file_name_tmp}" ) return image, current def flat_reducer( scan_filename: str, ref_start_filename: str, ref_end_filename: str, mixing_factor: float, entry_name: str = "entry0000", median_or_mean: str = ReduceMethod.MEAN.value, save_intermediated: bool = False, reuse_intermediated: bool = False, overwrite: bool = True, dark_default_value=300, ): """ this method extracts first a flatfield and dark from two reference scans. After flats and darks extraction, an interpolation is done according to the mixing_factor parameter. The obtained flats and dark are then saved associating them for a given target scan_filename :param scan_filename: The target scan. A nexus filename for which we want to create reduced scan from the scans given by ref_start and ref_end parameters ( a scan at the beginning, another at the end) :param ref_start_filename: The scan with projections to be used as reference for the beginning of the measures. :param ref_end_filename: The scan with projections to be used as reference at the end of the measures. :param mixing_factor: The mixing factor giving the averaged flats as (ref_start-darkB+darkS)*(1-mixing_factor)+(ref_end-darkE+darkS)*mixing_factor :param entry_name: The entry name, it defaults to entry0000 :param median_or_mean: Either "mean" or "median". Default is "mean" :param save_intermediated: Save intermediated flats and darks corresponding to extremal reference scans (ref_start_filename, refa_filename) for later usage. Defaults to False :param use_intermediated: Save intermediated flats and darks and if already presente reuse them for mixing :param overwrite: enforce overwriting of the reduced flats/darks """ if reuse_intermediated: required_files = [ f"{strip_extension(ref_start_filename, _logger)}_darks.hdf5", f"{strip_extension(ref_start_filename, _logger)}_flats.hdf5", f"{strip_extension(ref_end_filename, _logger)}_darks.hdf5", f"{strip_extension(ref_end_filename, _logger)}_flats.hdf5", ] intermediated_are_reusable = True for fn in required_files: if not os.path.exists(fn): intermediated_are_reusable = False else: intermediated_are_reusable = False # saving the intermediae if enforced if there is a plan to use them # and they are not available yet save_intermediated = save_intermediated or ( reuse_intermediated and not intermediated_are_reusable ) if median_or_mean not in [ReduceMethod.MEAN.value, ReduceMethod.MEDIAN.value]: message = f""" the "median_or_mean" parameter must be one of {[ReduceMethod.MEAN.value, ReduceMethod.MEDIAN.value]}. It was {median_or_mean} """ raise ValueError(message) fd_start, fd_start_as_dict = extract_darks_flats( ref_start_filename, entry_name, target_filename=ref_start_filename, save_intermediated=save_intermediated, method=median_or_mean, reuse_intermediated=intermediated_are_reusable, use_projections_for_flats=True, dark_default_value=dark_default_value, ) fd_end, _ = extract_darks_flats( ref_end_filename, entry_name, target_filename=ref_end_filename, save_intermediated=save_intermediated, method=median_or_mean, reuse_intermediated=intermediated_are_reusable, use_projections_for_flats=True, dark_default_value=dark_default_value, ) fd_sample, fd_as_dict = extract_darks_flats( scan_filename, entry_name, method=median_or_mean, use_projections_for_flats=False, ) reduced_infos = fd_as_dict["flat"]["meta"] scan = NXtomoScan(scan_filename, entry_name) current = fd_sample.flat_current if current is None: # handle the case the fd_sample does not contains any flat frames. In this case get the first # current we find from the NXtomo currents = scan.machine_current if currents is not None and len(currents) > 0: current = currents[0] # pylint: disable=E1136 if current is None: raise ValueError( f"Unable to find any machine current from {scan_filename}. Unable to compute reduced darks and flats" ) # compute reduced flats and dark flat0 = ( fd_start.flat_image - fd_start.dark_image ) * current / fd_start.flat_current + fd_start.dark_image flat1 = ( fd_end.flat_image - fd_start.dark_image ) * current / fd_end.flat_current + fd_start.dark_image flat = (1 - mixing_factor) * flat0 + mixing_factor * flat1 reduced_flats = {0: flat} # save reduced flats and dark reduced_infos.machine_current = numpy.array([current]) reduced_infos.count_time = reduced_infos.count_time[:1] if current != reduced_infos.machine_current[0]: raise RuntimeError( " Coherence check failed. Total non sense: the code is broken." ) scan.save_reduced_flats( reduced_flats, flats_infos=reduced_infos, overwrite=overwrite ) reduced_darks = fd_start_as_dict["dark"]["images"] reduced_infos = fd_start_as_dict["dark"]["meta"] scan.save_reduced_darks( reduced_darks, darks_infos=reduced_infos, overwrite=overwrite ) nxtomomill-v2.0.1/nxtomomill/utils/frameappender.py000066400000000000000000000362331511430602400226320ustar00rootroot00000000000000# coding: utf-8 from __future__ import annotations import os import h5py import h5py._hl.selections as selection import numpy from h5py import h5s as h5py_h5s from silx.io.url import DataUrl from silx.io.utils import get_data, h5py_read_dataset from tomoscan.esrf.scan.utils import cwd_context from tomoscan.io import HDF5File from silx.io.utils import open as open_hdf5 from nxtomo.io import to_target_rel_path from nxtomomill.utils.h5pyutils import from_data_url_to_virtual_source from nxtomomill.utils.hdf5 import DatasetReader __all__ = [ "FrameAppender", ] class FrameAppender: """ Class to insert 2D frame(s) to an existing dataset """ def __init__( self, data: numpy.ndarray | DataUrl, file_path, data_path, where, logger=None, ): if where not in ("start", "end"): raise ValueError("`where` should be `start` or `end`") if not isinstance( data, (DataUrl, numpy.ndarray, list, tuple, h5py.VirtualSource) ): raise TypeError( f"data should be an instance of DataUrl or a numpy array not {type(data)}" ) self.data = data self.file_path = os.path.abspath(file_path) self.data_path = data_path self.where = where self.logger = logger def process(self) -> None: """ main function. Will start the insertion of frame(s) """ with HDF5File(self.file_path, mode="a") as h5s: if self.data_path in h5s: self._add_to_existing_dataset(h5s) else: self._create_new_dataset(h5s) if self.logger: self.logger.info(f"data added to {self.data_path}@{self.file_path}") def _add_to_existing_virtual_dataset(self, h5s): if ( h5py.version.hdf5_version_tuple[0] <= 1 and h5py.version.hdf5_version_tuple[1] < 12 ): if self.logger: self.logger.warning( "You are working on virtual dataset" "with a hdf5 version < 12. Frame " "you want to change might be " "modified depending on the working " "directory without notifying." "See https://github.com/silx-kit/silx/issues/3277" ) if isinstance(self.data, h5py.VirtualSource): self.__insert_virtual_source_in_vds(h5s=h5s, new_virtual_source=self.data) elif isinstance(self.data, DataUrl): if self.logger is not None: self.logger.debug( f"Update virtual dataset: {self.data_path}@{self.file_path}" ) # store DataUrl in the current virtual dataset url = self.data def check_dataset(dataset_frm_url): data_need_reshape = False """check if the dataset is valid or might need a reshape""" if dataset_frm_url.ndim not in (2, 3): raise ValueError(f"{url.path()} should point to 2D or 3D dataset ") if dataset_frm_url.ndim == 2: new_shape = 1, dataset_frm_url.shape[0], dataset_frm_url.shape[1] if self.logger is not None: self.logger.info( f"reshape provided data to 3D (from {dataset_frm_url.shape} to {new_shape})" ) data_need_reshape = True return data_need_reshape loaded_dataset = None if url.data_slice() is None: # case we can avoid to load the data in memory with DatasetReader(url) as data_frm_url: data_need_reshape = check_dataset(data_frm_url) # FIXME: avoid keeping some file open. not clear why this is needed data_frm_url = None else: data_frm_url = get_data(url) data_need_reshape = check_dataset(data_frm_url) loaded_dataset = data_frm_url if url.data_slice() is None and not data_need_reshape: # case we can avoid to load the data in memory with DatasetReader(self.data) as data_frm_url: self.__insert_url_in_vds(h5s, url, data_frm_url) # FIXME: avoid keeping some file open. not clear why this is needed data_frm_url = None else: if loaded_dataset is None: data_frm_url = get_data(url) else: data_frm_url = loaded_dataset self.__insert_url_in_vds(h5s, url, data_frm_url) else: raise TypeError( "Provided data is a numpy array when given" "dataset path is a virtual dataset. " "You must store the data somewhere else " "and provide a DataUrl" ) def __insert_url_in_vds(self, h5s, url, data_frm_url): if data_frm_url.ndim == 2: dim_2, dim_1 = data_frm_url.shape data_frm_url = data_frm_url.reshape(1, dim_2, dim_1) elif data_frm_url.ndim == 3: _, dim_2, dim_1 = data_frm_url.shape else: raise ValueError("data to had is expected to be 2 or 3 d") new_virtual_source = h5py.VirtualSource( path_or_dataset=url.file_path(), name=url.data_path(), shape=data_frm_url.shape, ) if url.data_slice() is not None: # in the case we have to process to a FancySelection with open_hdf5(os.path.abspath(url.file_path())) as h5sd: dst = h5sd[url.data_path()] sel = selection.select( h5sd[url.data_path()].shape, url.data_slice(), dst ) new_virtual_source.sel = sel self.__insert_virtual_source_in_vds( h5s=h5s, new_virtual_source=new_virtual_source, relative_path=True ) def __insert_virtual_source_in_vds( self, h5s, new_virtual_source: h5py.VirtualSource, relative_path=True ): if not isinstance(new_virtual_source, h5py.VirtualSource): raise TypeError( f"{new_virtual_source} is expected to be an instance of h5py.VirtualSource and not {type(new_virtual_source)}" ) if not len(new_virtual_source.shape) == 3: raise ValueError( f"virtual source shape is expected to be 3D and not {len(new_virtual_source.shape)}D." ) # preprocess virtualSource to insure having a relative path if relative_path: vds_file_path = to_target_rel_path(new_virtual_source.path, self.file_path) new_virtual_source_sel = new_virtual_source.sel new_virtual_source = h5py.VirtualSource( path_or_dataset=vds_file_path, name=new_virtual_source.name, shape=new_virtual_source.shape, dtype=new_virtual_source.dtype, ) new_virtual_source.sel = new_virtual_source_sel virtual_sources_len = [] virtual_sources = [] # we need to recreate the VirtualSource they are not # store or available from the API for vs_info in h5s[self.data_path].virtual_sources(): length, vs = self._recreate_vs(vs_info=vs_info, vds_file=self.file_path) virtual_sources.append(vs) virtual_sources_len.append(length) n_frames = h5s[self.data_path].shape[0] + new_virtual_source.shape[0] data_type = h5s[self.data_path].dtype if self.where == "start": virtual_sources.insert(0, new_virtual_source) virtual_sources_len.insert(0, new_virtual_source.shape[0]) else: virtual_sources.append(new_virtual_source) virtual_sources_len.append(new_virtual_source.shape[0]) # create the new virtual dataset layout = h5py.VirtualLayout( shape=( n_frames, new_virtual_source.shape[-2], new_virtual_source.shape[-1], ), dtype=data_type, ) last = 0 for v_source, vs_len in zip(virtual_sources, virtual_sources_len): layout[last : vs_len + last] = v_source last += vs_len if self.data_path in h5s: del h5s[self.data_path] h5s.create_virtual_dataset(self.data_path, layout) def _add_to_existing_none_virtual_dataset(self, h5s): """ for now when we want to add data *to a none virtual dataset* we always duplicate data if provided from a DataUrl. We could create a virtual dataset as well but seems to complicated for a use case that we don't really have at the moment. :param h5s: """ if self.logger is not None: self.logger.debug("Update dataset: {entry}@{file_path}") if isinstance(self.data, (numpy.ndarray, list, tuple)): new_data = self.data else: url = self.data new_data = get_data(url) if isinstance(new_data, numpy.ndarray): if not new_data.shape[1:] == h5s[self.data_path].shape[1:]: raise ValueError( f"Data shapes are incoherent: {new_data.shape} vs {h5s[self.data_path].shape}" ) new_shape = ( new_data.shape[0] + h5s[self.data_path].shape[0], new_data.shape[1], new_data.shape[2], ) data_to_store = numpy.empty(new_shape) if self.where == "start": data_to_store[: new_data.shape[0]] = new_data data_to_store[new_data.shape[0] :] = h5py_read_dataset( h5s[self.data_path] ) else: data_to_store[: h5s[self.data_path].shape[0]] = h5py_read_dataset( h5s[self.data_path] ) data_to_store[h5s[self.data_path].shape[0] :] = new_data else: assert isinstance( self.data, (list, tuple) ), f"Unmanaged data type {type(self.data)}" o_data = h5s[self.data_path] o_data = list(h5py_read_dataset(o_data)) if self.where == "start": new_data.extend(o_data) data_to_store = numpy.asarray(new_data) else: o_data.extend(new_data) data_to_store = numpy.asarray(o_data) del h5s[self.data_path] h5s[self.data_path] = data_to_store def _add_to_existing_dataset(self, h5s): """Add the frame to an existing dataset""" if h5s[self.data_path].is_virtual: self._add_to_existing_virtual_dataset(h5s=h5s) else: self._add_to_existing_none_virtual_dataset(h5s=h5s) def _create_new_dataset(self, h5s): """ needs to create a new dataset. In this case the policy is: - if a DataUrl is provided then we create a virtual dataset - if a numpy array is provided then we create a 'standard' dataset """ if isinstance(self.data, DataUrl): url = self.data url_file_path = to_target_rel_path(url.file_path(), self.file_path) url = DataUrl( file_path=url_file_path, data_path=url.data_path(), scheme=url.scheme(), data_slice=url.data_slice(), ) with cwd_context(os.path.dirname(self.file_path)): vs, vs_shape, data_type = from_data_url_to_virtual_source(url) layout = h5py.VirtualLayout(shape=vs_shape, dtype=data_type) layout[:] = vs h5s.create_virtual_dataset(self.data_path, layout) elif isinstance(self.data, h5py.VirtualSource): virtual_source = self.data layout = h5py.VirtualLayout( shape=virtual_source.shape, dtype=virtual_source.dtype, ) vds_file_path = to_target_rel_path(virtual_source.path, self.file_path) virtual_source_rel_path = h5py.VirtualSource( path_or_dataset=vds_file_path, name=virtual_source.name, shape=virtual_source.shape, dtype=virtual_source.dtype, ) virtual_source_rel_path.sel = virtual_source.sel layout[:] = virtual_source_rel_path # convert path to relative h5s.create_virtual_dataset(self.data_path, layout) elif not isinstance(self.data, numpy.ndarray): raise TypeError( f"self.data should be an instance of DataUrl, a numpy array or a VirtualSource. Not {type(self.data)}" ) else: h5s[self.data_path] = self.data @staticmethod def _recreate_vs(vs_info, vds_file): """Simple util to retrieve a h5py.VirtualSource from virtual source information. to understand clearly this function you might first have a look at the use case exposed in issue: https://gitlab.esrf.fr/tomotools/nxtomomill/-/issues/40 """ with cwd_context(os.path.dirname(vds_file)): dataset_file_path = vs_info.file_name # in case the virtual source is in the same file if dataset_file_path == ".": dataset_file_path = vds_file with open_hdf5(dataset_file_path) as vs_node: dataset = vs_node[vs_info.dset_name] select_bounds = vs_info.vspace.get_select_bounds() left_bound = select_bounds[0] right_bound = select_bounds[1] length = right_bound[0] - left_bound[0] + 1 # warning: for now step is not managed with virtual # dataset virtual_source = h5py.VirtualSource( vs_info.file_name, vs_info.dset_name, shape=dataset.shape, ) # here we could provide dataset but we won't to # insure file path will be relative. type_code = vs_info.src_space.get_select_type() # check for unlimited selections in case where selection is regular # hyperslab, which is the only allowed case for h5s.UNLIMITED to be # in the selection if ( type_code == h5py_h5s.SEL_HYPERSLABS and vs_info.src_space.is_regular_hyperslab() ): ( source_start, stride, count, block, ) = vs_info.src_space.get_regular_hyperslab() source_end = source_start[0] + length sel = selection.select( dataset.shape, slice(source_start[0], source_end), dataset=dataset, ) virtual_source.sel = sel return ( length, virtual_source, ) nxtomomill-v2.0.1/nxtomomill/utils/h5pyutils.py000066400000000000000000000036211511430602400217620ustar00rootroot00000000000000# coding: utf-8 """ module to define some converter utils function """ import h5py import h5py._hl.selections as selection from silx.io.url import DataUrl from silx.io.utils import open as open_hdf5 __all__ = ["from_data_url_to_virtual_source", "from_virtual_source_to_data_url"] def from_data_url_to_virtual_source(url: DataUrl) -> tuple: """ :param url: url to be converted to a virtual source. It must target a 2D detector :return: (h5py.VirtualSource, tuple(shape of the virtual source), numpy.drype: type of the dataset associated with the virtual source) """ if not isinstance(url, DataUrl): raise TypeError( f"url is expected to be an instance of DataUrl and not {type(url)}" ) with open_hdf5(url.file_path()) as o_h5s: original_data_shape = o_h5s[url.data_path()].shape data_type = o_h5s[url.data_path()].dtype if len(original_data_shape) == 2: original_data_shape = ( 1, original_data_shape[0], original_data_shape[1], ) vs_shape = original_data_shape if url.data_slice() is not None: vs_shape = ( url.data_slice().stop - url.data_slice().start, original_data_shape[-2], original_data_shape[-1], ) vs = h5py.VirtualSource( url.file_path(), url.data_path(), shape=vs_shape, dtype=data_type ) if url.data_slice() is not None: vs.sel = selection.select(original_data_shape, url.data_slice()) return vs, vs_shape, data_type def from_virtual_source_to_data_url(vs: h5py.VirtualSource) -> DataUrl: if not isinstance(vs, h5py.VirtualSource): raise TypeError( f"vs is expected to be an instance of h5py.VirtualSorce and not {type(vs)}" ) url = DataUrl(file_path=vs.path, data_path=vs.name, scheme="silx") return url nxtomomill-v2.0.1/nxtomomill/utils/hdf5.py000066400000000000000000000061311511430602400206410ustar00rootroot00000000000000# coding: utf-8 import contextlib import h5py import pint import logging from .pintutils import get_unit try: import hdf5plugin # noqa F401 except ImportError: pass from silx.io.url import DataUrl from silx.io.utils import open as open_hdf5 _logger = logging.getLogger(__name__) __all__ = ["EntryReader", "DatasetReader"] class _BaseReader(contextlib.AbstractContextManager): def __init__(self, url: DataUrl): if not isinstance(url, DataUrl): raise TypeError(f"url should be an instance of DataUrl. Not {type(url)}") if url.scheme() not in ("silx", "h5py"): raise ValueError("Valid scheme are silx and h5py") if url.data_slice() is not None: raise ValueError( "Data slices are not managed. Data path should " "point to a bliss node (h5py.Group)" ) self._url = url self._file_handler = None def __exit__(self, *exc): return self._file_handler.close() class EntryReader(_BaseReader): """Context manager used to read a bliss node""" def __enter__(self): self._file_handler = open_hdf5(filename=self._url.file_path()) if self._url.data_path() == "": entry = self._file_handler elif self._url.data_path() not in self._file_handler: raise KeyError( f"data path '{self._url.data_path()}' doesn't exists from '{self._url.file_path()}'" ) else: entry = self._file_handler[self._url.data_path()] if not isinstance(entry, h5py.Group): raise ValueError("Data path should point to a bliss node (h5py.Group)") return entry class DatasetReader(_BaseReader): """Context manager used to read a bliss node""" def __enter__(self): self._file_handler = open_hdf5(filename=self._url.file_path()) entry = self._file_handler[self._url.data_path()] if not isinstance(entry, h5py.Dataset): raise ValueError( f"Data path ({self._url.path()}) should point to a dataset (h5py.Dataset)" ) return entry def get_dataset_unit( dataset: h5py.Dataset, default: pint.Unit, from_dataset: str ) -> pint.Unit: """ Util function to return the pint Unit of a HDF5 dataset. This dataset must have `unit` or `units` defined else will fall back to the default unit :param from_dataset: information about the 'dataset' / metadata we are trying to access. For logging purpose in case of failure. """ if not isinstance(dataset, h5py.Dataset): raise TypeError( f"dataset is expected to be an instance of {h5py.Dataset}. Got {type(dataset)}." ) if "unit" in dataset.attrs: unit = dataset.attrs["unit"] elif "units" in dataset.attrs: unit = dataset.attrs["units"] else: _logger.info(f"no unit found for {from_dataset}. Take default unit: {default}") return default if hasattr(unit, "decode"): # handle Diamond dataset unit = unit.decode() return get_unit(unit=unit, default=default, from_dataset=from_dataset) nxtomomill-v2.0.1/nxtomomill/utils/io.py000066400000000000000000000061271511430602400204270ustar00rootroot00000000000000import traceback import functools import logging depreclog = logging.getLogger("nxtomomill.DEPRECATION") deprecache = set([]) def deprecated_warning( type_, name, reason=None, replacement=None, since_version=None, only_once=True, skip_backtrace_count=0, ): """ Function to log a deprecation warning :param type_: Nature of the object to be deprecated: "Module", "Function", "Class" ... :param name: Object name. :param reason: Reason for deprecating this function (e.g. "feature no longer provided", :param replacement: Name of replacement function (if the reason for deprecating was to rename the function) :param since_version: First *silx* version for which the function was deprecated (e.g. "0.5.0"). :param only_once: If true, the deprecation warning will only be generated one time for each different call locations. Default is true. :param skip_backtrace_count: Amount of last backtrace to ignore when logging the backtrace """ if not depreclog.isEnabledFor(logging.WARNING): # Avoid computation when it is not logged return msg = "%s %s is deprecated" if since_version is not None: msg += " since nxtomomill version %s" % since_version msg += "." if reason is not None: msg += " Reason: %s." % reason if replacement is not None: msg += " Use '%s' instead." % replacement msg += "\n%s" limit = 2 + skip_backtrace_count backtrace = "".join(traceback.format_stack(limit=limit)[0]) backtrace = backtrace.rstrip() if only_once: data = (msg, type_, name, backtrace) if data in deprecache: return else: deprecache.add(data) depreclog.warning(msg, type_, name, backtrace) def deprecated( func=None, reason=None, replacement=None, since_version=None, only_once=True, skip_backtrace_count=1, ): """ Decorator that deprecates the use of a function :param reason: Reason for deprecating this function (e.g. "feature no longer provided", :param replacement: Name of replacement function (if the reason for deprecating was to rename the function) :param since_version: First *silx* version for which the function was deprecated (e.g. "0.5.0"). :param only_once: If true, the deprecation warning will only be generated one time. Default is true. :param skip_backtrace_count: Amount of last backtrace to ignore when logging the backtrace """ def decorator(func): @functools.wraps(func) def wrapper(*args, **kwargs): deprecated_warning( type_="Function", name=func.__name__, reason=reason, replacement=replacement, since_version=since_version, only_once=only_once, skip_backtrace_count=skip_backtrace_count, ) return func(*args, **kwargs) return wrapper if func is not None: return decorator(func) return decorator nxtomomill-v2.0.1/nxtomomill/utils/nexus.py000066400000000000000000000103201511430602400211500ustar00rootroot00000000000000import h5py import numpy import pint from silx.io.utils import h5py_read_dataset from silx.io.utils import open as open_hdf5 from tomoscan.io import HDF5File from nxtomo.nxobject.nxobject import NXobject from nxtomomill.utils.hdf5 import get_dataset_unit __all__ = [ "cast_and_check_array_1D", "create_nx_data_group", "link_nxbeam_to_root", "get_data_and_unit", "get_data", "concatenate", ] def _is_iterable(value): if isinstance(value, (str, bytes)): return False try: iter(value) except TypeError: return False return True def cast_and_check_array_1D(array, array_name): if not (array is None or isinstance(array, numpy.ndarray) or _is_iterable(array)): raise TypeError( f"{array_name} is expected to be None, or a sequence. Not {type(array)}" ) if array is not None and not isinstance(array, numpy.ndarray): array = numpy.asarray(array) if array is not None and array.ndim > 1: raise ValueError(f"{array_name} is expected to be 0 or 1d not {array.ndim}") return array def create_nx_data_group(file_path: str, entry_path: str, axis_scale: list): """ Create the 'Nxdata' group at entry level with soft links on the NXDetector and NXsample. :param file_path: :param entry_path: :param axis_scale: :return: """ if not isinstance(file_path, str): raise TypeError("file_path is expected to be a file") if not isinstance(entry_path, str): raise TypeError("entry_path is expected to be a file") if not _is_iterable(axis_scale): raise TypeError("axis_scale is expected to be a sequence") with HDF5File(file_path, mode="a") as h5f: entry_group = h5f[entry_path] nx_data_grp = entry_group.require_group("data") # link detector datasets: if not entry_path.startswith("/"): entry_path = "/" + entry_path for dataset in ("data", "image_key", "image_key_control"): dataset_path = "/".join((entry_path, "instrument", "detector", dataset)) nx_data_grp[dataset] = h5py.SoftLink(dataset_path) # link rotation angle nx_data_grp["rotation_angle"] = h5py.SoftLink( "/".join((entry_path, "sample", "rotation_angle")) ) # write NX attributes nx_data_grp.attrs["NX_class"] = "NXdata" nx_data_grp.attrs["signal"] = "data" nx_data_grp.attrs["SILX_style/axis_scale_types"] = axis_scale nx_data_grp["data"].attrs["interpretation"] = "image" def link_nxbeam_to_root(file_path, entry_path): """ Create the 'Nxdata' group at entry level with soft links on the NXDetector and NXsample. :param file_path: :param entry_path: :return: """ if not isinstance(file_path, str): raise TypeError("file_path is expected to be a file") if not isinstance(entry_path, str): raise TypeError("entry_path is expected to be a file") if not entry_path.startswith("/"): entry_path = "/" + entry_path with HDF5File(file_path, mode="a") as h5f: entry_group = h5f[entry_path] entry_group["beam"] = h5py.SoftLink( "/".join((entry_path, "instrument", "beam")) ) def get_data_and_unit( file_path: str, data_path: str, default_unit: pint.Unit, from_dataset: str = "Unknown", ): with open_hdf5(file_path) as h5f: if data_path in h5f and isinstance(h5f[data_path], h5py.Dataset): dataset = h5f[data_path] unit = get_dataset_unit( dataset=dataset, default=default_unit, from_dataset=from_dataset ) return h5py_read_dataset(dataset), unit else: return None, default_unit def get_data(file_path, data_path): with open_hdf5(file_path) as h5f: if data_path in h5f: return h5py_read_dataset(h5f[data_path]) else: return None def concatenate(nx_objects, **kwargs): if len(nx_objects) == 0: return None else: if not isinstance(nx_objects[0], NXobject): raise TypeError("nx_objects are expected to be instances of NXobject") return type(nx_objects[0]).concatenate(nx_objects=nx_objects, **kwargs) nxtomomill-v2.0.1/nxtomomill/utils/pintutils.py000066400000000000000000000031601511430602400220450ustar00rootroot00000000000000"""pint utils""" from __future__ import annotations import pint import logging from pint.errors import UndefinedUnitError _logger = logging.getLogger(__name__) _ureg = pint.get_application_registry() VALID_CURRENT_VALUES: tuple[str] = [ str(unit) for unit in (_ureg.ampere, _ureg.kiloampere, _ureg.milliampere) ] VALID_ENERGY_VALUES: tuple[str] = [ str(unit) for unit in (_ureg.joule, _ureg.eV, _ureg.keV, _ureg.meV, _ureg.GeV, _ureg.kJ) ] VALID_METRIC_VALUES: tuple[str] = [ str(unit) for unit in ( _ureg.nanometer, _ureg.micrometer, _ureg.millimeter, _ureg.centimeter, _ureg.meter, ) ] def get_unit(unit: str, default: pint.Unit, from_dataset: str) -> pint.Unit: """ Convert given unit as an str to a pint unit. Else fallback to 'default' """ if not isinstance(unit, str): raise TypeError(f"'unit' is expected to be a {str}. Got {type(unit)}") if not isinstance(default, pint.Unit): raise TypeError( f"'default' is expected to be a {pint.Unit}. Got {type(default)}" ) # special cases (some string that can exists and that are not handled by pint) if unit.lower() in ("mev", "megaelectronvolt"): unit = _ureg.keV * 1e3 elif unit.lower() in ("gev", "gigaelectronvolt"): unit = _ureg.keV * 1e6 elif unit == "microns": unit = "um" try: unit = _ureg(unit) except UndefinedUnitError: _logger.warning( f"Undefined unit: {unit} from {from_dataset}. Fallback on default unit {default}" ) return default else: return unit nxtomomill-v2.0.1/nxtomomill/utils/tests/000077500000000000000000000000001511430602400206025ustar00rootroot00000000000000nxtomomill-v2.0.1/nxtomomill/utils/tests/test_flat_reducer.py000066400000000000000000000104261511430602400246550ustar00rootroot00000000000000import datetime import pytest from tomoscan.io import HDF5File from tomoscan.esrf.scan.nxtomoscan import NXtomoScan from nxtomomill.app.zstages2nxs import _convert_bliss2nx from nxtomomill.tests.utils.bliss import MockBlissAcquisition as _MockBlissAcquisition from nxtomomill.utils.flat_reducer import flat_reducer class MockBlissAcquisition(_MockBlissAcquisition): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) for sample in self.samples: # append machine current to the scan with HDF5File(sample.sample_file, mode="a") as h5f: for i_bliss_scan in range( 1, 1 + self._n_scan_per_sequence + self._n_darks + self._n_flats ): node_name = f"{i_bliss_scan}.1" h5f[f"{node_name}/instrument/machine/current"] = [ 12.3, ] h5f[f"{node_name}/instrument/machine/current"].attrs["units"] = "mA" if f"{node_name}/start_time" in h5f: del h5f[f"{node_name}/start_time"] h5f[f"{node_name}/start_time"] = str(datetime.datetime.now()) if f"{node_name}/end_time" in h5f: del h5f[f"{node_name}/end_time"] h5f[f"{node_name}/end_time"] = str( datetime.datetime.now() + datetime.timedelta(minutes=10) ) @pytest.mark.parametrize("stage_scan_have_flats", (True, False)) @pytest.mark.parametrize("save_intermediated", (True, False)) @pytest.mark.parametrize("reuse_intermediated", (True, False)) def test_flat_reducer( tmp_path, stage_scan_have_flats, save_intermediated, reuse_intermediated ): """ test execution of `flat_reducer` function. This function is going through a set of bliss acquisition to create corresponding NXtomo and add them dark and flats obtained from 'reference' acquisition (converting projections to reduced flats) """ raw_data_dir = tmp_path / "raw" n_stage = 5 # create bliss scan to be converted to NXtomo acqu = MockBlissAcquisition( n_sample=n_stage, n_sequence=1, n_scan_per_sequence=10, n_darks=2, n_flats=1 if stage_scan_have_flats else 0, output_dir=raw_data_dir, file_name_z_fill=4, ) stages_bliss_file = [sample.sample_file for sample in acqu.samples] # scan for flat at start ref_start = MockBlissAcquisition( n_sample=1, n_sequence=1, n_scan_per_sequence=10, n_darks=2, n_flats=0, output_dir=raw_data_dir, file_name_prefix="REF_B_0000", ) bliss_ref_start = ref_start.samples[0].sample_file nx_tomo_ref_start = bliss_ref_start.replace(".h5", ".nx") _convert_bliss2nx( bliss_ref_start, nx_tomo_ref_start, ) # scan for flat at end ref_end = MockBlissAcquisition( n_sample=1, n_sequence=1, n_scan_per_sequence=10, n_darks=2, n_flats=0, output_dir=raw_data_dir, file_name_prefix="REF_E_0000", ) bliss_ref_end = ref_end.samples[0].sample_file nx_tomo_ref_end = bliss_ref_end.replace(".h5", ".nx") _convert_bliss2nx( bliss_ref_end, nx_tomo_ref_end, ) for i_stage, bliss_file_name in enumerate(stages_bliss_file): mixing_factor = (i_stage + 1) / (n_stage) # convert from .h5 to .nx output_nx_file = bliss_file_name.replace(".h5", ".nx") _convert_bliss2nx( bliss_file_name, output_nx_file, ) # apply fly reducer flat_reducer( scan_filename=output_nx_file, ref_start_filename=nx_tomo_ref_start, ref_end_filename=nx_tomo_ref_end, mixing_factor=mixing_factor, save_intermediated=save_intermediated, reuse_intermediated=reuse_intermediated, ) # make sure there is some reduced dark and flat for the nx i_stage_scan = NXtomoScan(output_nx_file, "entry0000") i_stage_reduced_flats = i_stage_scan.load_reduced_flats() i_stage_reduced_darks = i_stage_scan.load_reduced_darks() assert len(i_stage_reduced_flats) == 1 assert len(i_stage_reduced_darks) == 1 nxtomomill-v2.0.1/nxtomomill/utils/tests/test_nexus_utils.py000066400000000000000000000000001511430602400245630ustar00rootroot00000000000000nxtomomill-v2.0.1/nxtomomill/utils/tests/test_utils.py000066400000000000000000001470611511430602400233640ustar00rootroot00000000000000# coding: utf-8 from __future__ import annotations import os import shutil import tempfile import unittest import h5py import numpy.random from silx.io.url import DataUrl from silx.io.utils import h5py_read_dataset from tomoscan.io import HDF5File, get_swmr_mode from tomoscan.esrf.mock import MockNXtomo as MockNXtomo from tomoscan.esrf.scan.nxtomoscan import NXtomoScan from nxtomo.nxobject.nxdetector import ImageKey from nxtomomill.utils import add_dark_flat_nx_file, change_image_key_control class MockNXtomoWithElecCurrent(MockNXtomo): def __init__( self, scan_path, ini_dark: numpy.array | None, ini_flats: numpy.array | None, final_flats: numpy.array | None, dim: int, n_proj: int, count_time: numpy.array | None = None, machine_current: numpy.array | None = None, ): assert ini_dark is None or ini_dark.ndim == 3, "ini_dark should be a 3d array" assert ini_flats is None or ini_flats.ndim == 3, "ini_dark should be a 3d array" assert ( final_flats is None or final_flats.ndim == 3 ), "ini_dark should be a 3d array" self._ini_darks = ini_dark self._ini_flats = ini_flats self._final_flats = final_flats self._count_time = count_time super().__init__( scan_path=scan_path, dim=dim, create_ini_dark=ini_dark is not None, create_ini_flat=ini_flats is not None, create_final_flat=final_flats is not None, n_ini_proj=n_proj, n_proj=n_proj, ) # append count_time and machine_current to the HDF5 file with HDF5File(self.scan_master_file, "a") as h5_file: entry_one = h5_file.require_group(self.scan_entry) if machine_current is not None: monitor_grp = entry_one.require_group("control") monitor_grp["data"] = machine_current # rewrite count_time if count_time is not None: instrument_grp = entry_one.require_group("instrument") detector_grp = instrument_grp.require_group("detector") if "count_time" in detector_grp: del detector_grp["count_time"] detector_grp["count_time"] = count_time def add_initial_dark(self): for frame in self._ini_darks: self._append_frame( data_=frame.reshape(1, frame.shape[0], frame.shape[1]), rotation_angle=self.rotation_angle[-1], image_key=ImageKey.DARK_FIELD.value, image_key_control=ImageKey.DARK_FIELD.value, diode_data=None, ) def add_initial_flat(self): for frame in self._ini_flats: self._append_frame( data_=frame.reshape(1, frame.shape[0], frame.shape[1]), rotation_angle=self.rotation_angle[-1], image_key=ImageKey.FLAT_FIELD.value, image_key_control=ImageKey.FLAT_FIELD.value, diode_data=None, ) def add_final_flat(self): for frame in self._final_flats: self._append_frame( data_=frame.reshape(1, frame.shape[0], frame.shape[1]), rotation_angle=self.rotation_angle[-1], image_key=ImageKey.FLAT_FIELD.value, image_key_control=ImageKey.FLAT_FIELD.value, diode_data=None, ) class BaseTestAddDarkAndFlats(unittest.TestCase): """ Unit test on nxtomomill.utils.add_dark_flat_nx_file function """ def setUp(self) -> None: self.tmpdir = tempfile.mkdtemp() simple_nx_path = os.path.join(self.tmpdir, "simple_case") self.dim = 55 self.nproj = 20 self._simple_nx = MockNXtomoWithElecCurrent( scan_path=simple_nx_path, n_proj=self.nproj, ini_dark=None, ini_flats=None, final_flats=None, dim=self.dim, machine_current=numpy.zeros(self.nproj), ).scan with HDF5File( self._simple_nx.master_file, mode="r", swmr=get_swmr_mode() ) as h5s: data_path = "/".join( (self._simple_nx.entry, "instrument", "detector", "data") ) self._raw_data = h5py_read_dataset(h5s[data_path]) nx_with_vds_path = os.path.join(self.tmpdir, "case_with_vds") self._nx_with_virtual_dataset = MockNXtomoWithElecCurrent( scan_path=nx_with_vds_path, n_proj=0, ini_dark=None, ini_flats=None, final_flats=None, dim=self.dim, machine_current=numpy.zeros(self.nproj), ).scan self._create_vds( source_file=self._simple_nx.master_file, source_data_path=self._simple_nx.entry, target_file=self._nx_with_virtual_dataset.master_file, target_data_path=self._nx_with_virtual_dataset.entry, copy_other_data=True, ) self._patch_nxtomo_flags(self._nx_with_virtual_dataset) nx_with_vds_path_and_links = os.path.join( self.tmpdir, "case_with_vds_and_links" ) self._nx_with_virtual_dataset_with_link = MockNXtomoWithElecCurrent( scan_path=nx_with_vds_path_and_links, n_proj=0, ini_dark=None, ini_flats=None, final_flats=None, dim=self.dim, machine_current=numpy.zeros(self.nproj), ).scan self._create_vds( source_file=self._simple_nx.master_file, source_data_path=self._simple_nx.entry, target_file=self._nx_with_virtual_dataset_with_link.master_file, target_data_path=self._nx_with_virtual_dataset_with_link.entry, copy_other_data=True, ) self._patch_nxtomo_flags(self._nx_with_virtual_dataset_with_link) # create dark self.start_dark = ( numpy.random.random((self.dim * self.dim)) .reshape(1, self.dim, self.dim) .astype("f") ) self.start_dark_file = os.path.join(self.tmpdir, "dark.hdf5") self.start_dark_entry = "data" self.start_dark_url = self._save_raw( data=self.start_dark, entry=self.start_dark_entry, file_path=self.start_dark_file, ) self.end_dark = ( numpy.random.random((self.dim * self.dim * 2)) .reshape(2, self.dim, self.dim) .astype("f") ) self.end_dark_file = os.path.join(self.tmpdir, "dark.hdf5") self.end_dark_entry = "data2" self.end_dark_url = self._save_raw( data=self.end_dark, entry=self.end_dark_entry, file_path=self.end_dark_file ) # create flats self.start_flat = ( numpy.random.random((self.dim * self.dim * 3)) .reshape(3, self.dim, self.dim) .astype("f") ) self.start_flat_file = os.path.join(self.tmpdir, "start_flat.hdf5") self.start_flat_entry = "/root/flat" self.start_flat_url = self._save_raw( data=self.start_flat, entry=self.start_flat_entry, file_path=self.start_flat_file, ) self.end_flat = ( numpy.random.random((self.dim * self.dim)) .reshape(1, self.dim, self.dim) .astype("f") ) # save the end flat in the simple case file to insure all cases are # consider self.end_flat_file = self._simple_nx.master_file self.end_flat_entry = "flat" self.end_flat_url = self._save_raw( data=self.end_flat, entry=self.end_flat_entry, file_path=self.end_flat_file ) def _save_raw(self, data, entry, file_path) -> DataUrl: with HDF5File(file_path, mode="a") as h5s: h5s[entry] = data return DataUrl(file_path=file_path, data_path=entry, scheme="silx") def _create_vds( self, source_file: str, source_data_path: str, target_file: str, target_data_path: str, copy_other_data: bool, ): """Create virtual dataset and links from source to target :param source_file: :param source_data_path: :param target_file: :param target_data_path: :param copy_other_data: we want to create two cases: one copying datasets 'image_key'... and the other one linking them. Might have a difference of behavior when overwriting for example """ assert source_file != target_file, "file should be different" # link data n_frames = 0 # for now we only consider the original data with HDF5File(source_file, mode="r", swmr=get_swmr_mode()) as o_h5s: old_path = os.path.join(source_data_path, "instrument", "detector", "data") n_frames += o_h5s[old_path].shape[0] shape = o_h5s[old_path].shape data_type = o_h5s[old_path].dtype layout = h5py.VirtualLayout(shape=shape, dtype=data_type) assert os.path.exists(source_file) with HDF5File(source_file, mode="r", swmr=get_swmr_mode()) as ppp: assert source_data_path in ppp layout[:] = h5py.VirtualSource(path_or_dataset=o_h5s[old_path]) det_path = os.path.join(target_data_path, "instrument", "detector") with HDF5File(target_file, mode="a") as h5s: detector_node = h5s.require_group(det_path) detector_node.create_virtual_dataset("data", layout, fillvalue=-5) for path in ( os.path.join("instrument", "detector", "image_key"), os.path.join("instrument", "detector", "image_key_control"), os.path.join("instrument", "detector", "count_time"), os.path.join("sample", "rotation_angle"), ): old_path = os.path.join(source_data_path, path) new_path = os.path.join(target_data_path, path) with HDF5File(target_file, mode="a") as h5s: if copy_other_data: with HDF5File(source_file, mode="r", swmr=get_swmr_mode()) as o_h5s: if new_path in h5s: del h5s[new_path] h5s[new_path] = h5py_read_dataset(o_h5s[old_path]) elif source_file == target_file: h5s[new_path] = h5py.SoftLink(old_path) else: relpath = os.path.relpath(source_file, os.path.dirname(target_file)) h5s[new_path] = h5py.ExternalLink(relpath, old_path) def _patch_nxtomo_flags(self, scan): """Insure necessary flags are here""" with HDF5File(scan.master_file, mode="a") as h5s: instrument_path = os.path.join(scan.entry, "instrument") instrument_node = h5s.require_group(instrument_path) if "NX_class" not in instrument_node.attrs: instrument_node.attrs["NX_class"] = "NXinstrument" detector_node = instrument_node.require_group("detector") if "NX_class" not in detector_node.attrs: detector_node.attrs["NX_class"] = "NXdetector" if "data" in instrument_node: if "interpretation" not in instrument_node.attrs: instrument_node["data"].attrs["interpretation"] = "image" sample_path = os.path.join(scan.entry, "sample") sample_node = h5s.require_group(sample_path) if "NX_class" not in sample_node: sample_node.attrs["NX_class"] = "NXsample" def tearDown(self) -> None: shutil.rmtree(self.tmpdir) class TestAddDarkAtStart(BaseTestAddDarkAndFlats): """ Make sure adding dark works """ def testAddDarkAsNumpyArray(self) -> None: """Insure adding a dark works from a numpy array""" for scan in (self._simple_nx,): with self.subTest(scan=scan): add_dark_flat_nx_file( file_path=scan.master_file, entry=scan.entry, darks_start=self.start_dark, ) # test `data` dataset with HDF5File(scan.master_file, mode="r", swmr=get_swmr_mode()) as h5s: data_path = os.path.join( scan.entry, "instrument", "detector", "data" ) self.assertEqual( h5s[data_path].shape, (self.nproj + 1, self.dim, self.dim) ) numpy.testing.assert_array_almost_equal( h5s[data_path][0], self.start_dark[0] ) numpy.testing.assert_array_almost_equal( h5s[data_path][1], self._raw_data[0] ) # insure we can still make a HDF5Scan out of it NXtomoScan(scan=scan.master_file, entry=scan.entry) # test some exception are raised if we try to add directly a numpy # array within a virtual dataset for scan in ( self._nx_with_virtual_dataset, self._nx_with_virtual_dataset_with_link, ): with self.assertRaises(TypeError): add_dark_flat_nx_file( file_path=scan.master_file, entry=scan.entry, darks_start=self.start_dark, ) def testAddDarkAsDataUrl(self) -> None: """Insure adding a dark works from a DataUrl""" for scan in (self._nx_with_virtual_dataset_with_link,): with self.subTest(scan=scan): add_dark_flat_nx_file( file_path=scan.master_file, entry=scan.entry, darks_start=self.start_dark_url, ) # test `data` dataset with HDF5File(scan.master_file, mode="r", swmr=get_swmr_mode()) as h5s: data_path = os.path.join( scan.entry, "instrument", "detector", "data" ) self.assertEqual( h5s[data_path].shape, (self.nproj + 1, self.dim, self.dim) ) numpy.testing.assert_array_almost_equal( h5s[data_path][0], self.start_dark[0] ) numpy.testing.assert_array_almost_equal( h5s[data_path][1], self._raw_data[0] ) # insure we can still make a HDF5Scan out of it NXtomoScan(scan=scan.master_file, entry=scan.entry) # test rotation angle and count_time count_time_path = os.path.join( scan.entry, "instrument", "detector", "count_time" ) numpy.testing.assert_array_equal( h5py_read_dataset(h5s[count_time_path][-1]), 1 ) self.assertEqual( len(h5s[count_time_path]), self.nproj + self.start_dark.shape[0] ) rotation_angle_path = os.path.join( scan.entry, "sample", "rotation_angle" ) numpy.testing.assert_array_equal( h5s[rotation_angle_path][0], h5s[rotation_angle_path][1] ) self.assertEqual( len(h5s[rotation_angle_path]), self.nproj + self.start_dark.shape[0], ) control_data_path = os.path.join(scan.entry, "control", "data") numpy.testing.assert_array_equal( h5s[control_data_path][0], h5s[control_data_path][1] ) self.assertEqual( len(h5s[control_data_path]), self.nproj + self.start_dark.shape[0], ) class TestAddFlatAtStart(BaseTestAddDarkAndFlats): """ Make sure adding initial flat works """ def testAddFlatStartAsNumpyArray(self) -> None: """Insure adding a dark works from a numpy array""" for scan in (self._simple_nx,): with self.subTest(scan=scan): add_dark_flat_nx_file( file_path=scan.master_file, entry=scan.entry, flats_start=self.start_flat, ) # test `data` dataset with HDF5File(scan.master_file, mode="r", swmr=get_swmr_mode()) as h5s: data_path = os.path.join( scan.entry, "instrument", "detector", "data" ) self.assertEqual( h5s[data_path].shape, (self.nproj + self.start_flat.shape[0], self.dim, self.dim), ) numpy.testing.assert_array_almost_equal( h5s[data_path][0], self.start_flat[0] ) numpy.testing.assert_array_almost_equal( h5s[data_path][2], self.start_flat[2] ) numpy.testing.assert_array_almost_equal( h5s[data_path][3], self._raw_data[0] ) # insure we can still make a HDF5Scan out of it NXtomoScan(scan=scan.master_file, entry=scan.entry) # test some exception are raised if we try to add directly a numpy # array within a virtual dataset for scan in ( self._nx_with_virtual_dataset, self._nx_with_virtual_dataset_with_link, ): with self.assertRaises(TypeError): add_dark_flat_nx_file( file_path=scan.master_file, entry=scan.entry, flats_start=self.start_flat, ) def testAddFlatStartAsDataUrl(self) -> None: """Insure adding a dark works from a DataUrl""" for scan in (self._nx_with_virtual_dataset_with_link,): with self.subTest(scan=scan): add_dark_flat_nx_file( file_path=scan.master_file, entry=scan.entry, flats_start=self.start_flat_url, ) # test `data` dataset with HDF5File(scan.master_file, mode="r", swmr=get_swmr_mode()) as h5s: data_path = os.path.join( scan.entry, "instrument", "detector", "data" ) self.assertEqual( h5s[data_path].shape, (self.nproj + self.start_flat.shape[0], self.dim, self.dim), ) numpy.testing.assert_array_almost_equal( h5s[data_path][0], self.start_flat[0] ) numpy.testing.assert_array_almost_equal( h5s[data_path][2], self.start_flat[2] ) numpy.testing.assert_array_almost_equal( h5s[data_path][3], self._raw_data[0] ) # test rotation angle and count_time count_time_path = os.path.join( scan.entry, "instrument", "detector", "count_time" ) numpy.testing.assert_array_equal( h5py_read_dataset(h5s[count_time_path][-1]), 1 ) self.assertEqual( len(h5s[count_time_path]), self.nproj + self.start_flat.shape[0] ) rotation_angle_path = os.path.join( scan.entry, "sample", "rotation_angle" ) numpy.testing.assert_array_equal( h5s[rotation_angle_path][0], h5s[rotation_angle_path][1] ) self.assertEqual( len(h5s[rotation_angle_path]), self.nproj + self.start_flat.shape[0], ) # insure we can still make a HDF5Scan out of it NXtomoScan(scan=scan.master_file, entry=scan.entry) class TestAddFlatAtEnd(BaseTestAddDarkAndFlats): """ Make sure adding final flat works """ def testAddFlatEndAsNumpyArray(self) -> None: """Insure adding a dark works from a numpy array""" for scan in (self._simple_nx,): with self.subTest(scan=scan): add_dark_flat_nx_file( file_path=scan.master_file, entry=scan.entry, flats_end=self.end_flat, ) # test `data` dataset with HDF5File(scan.master_file, mode="r", swmr=get_swmr_mode()) as h5s: data_path = os.path.join( scan.entry, "instrument", "detector", "data" ) self.assertEqual( h5s[data_path].shape, (self.nproj + self.end_flat.shape[0], self.dim, self.dim), ) numpy.testing.assert_array_almost_equal( h5s[data_path][-1], self.end_flat[-1] ) numpy.testing.assert_array_almost_equal( h5s[data_path][0], self._raw_data[0] ) # insure we can still make a HDF5Scan out of it NXtomoScan(scan=scan.master_file, entry=scan.entry) # test the 'image_key' and image_key_control dataset img_key_control_path = os.path.join( scan.entry, "instrument", "detector", "image_key_control" ) numpy.testing.assert_array_equal( h5py_read_dataset(h5s[img_key_control_path][-2:]), [0, 1] ) img_key_path = os.path.join( scan.entry, "instrument", "detector", "image_key" ) numpy.testing.assert_array_equal( h5py_read_dataset(h5s[img_key_path][-2:]), [0, 1] ) # test rotation angle and count_time count_time_path = os.path.join( scan.entry, "instrument", "detector", "count_time" ) numpy.testing.assert_array_equal( h5py_read_dataset(h5s[count_time_path][-1]), 1 ) self.assertEqual( len(h5s[count_time_path]), self.nproj + self.end_flat.shape[0] ) rotation_angle_path = os.path.join( scan.entry, "sample", "rotation_angle" ) numpy.testing.assert_array_equal( h5s[rotation_angle_path][-1], h5s[rotation_angle_path][-2] ) self.assertEqual( len(h5s[rotation_angle_path]), self.nproj + self.end_flat.shape[0], ) # test some exception are raised if we try to add directly a numpy # array within a virtual dataset for scan in ( self._nx_with_virtual_dataset, self._nx_with_virtual_dataset_with_link, ): with self.assertRaises(TypeError): add_dark_flat_nx_file( file_path=scan.master_file, entry=scan.entry, flats_end=self.end_flat, ) def testAddFlatStartAsDataUrl(self) -> None: """Insure adding a dark works from a DataUrl""" for scan in (self._nx_with_virtual_dataset_with_link,): with self.subTest(scan=scan): add_dark_flat_nx_file( file_path=scan.master_file, entry=scan.entry, flats_end=self.end_flat_url, ) # test data is correctly store with HDF5File(scan.master_file, mode="r", swmr=get_swmr_mode()) as h5s: # test the 'data' dataset data_path = os.path.join( scan.entry, "instrument", "detector", "data" ) self.assertEqual( h5s[data_path].shape, (self.nproj + self.end_flat.shape[0], self.dim, self.dim), ) numpy.testing.assert_array_almost_equal( h5s[data_path][-1], self.end_flat[-1] ) numpy.testing.assert_array_almost_equal( h5s[data_path][0], self._raw_data[0] ) NXtomoScan(scan=scan.master_file, entry=scan.entry) # test the 'image_key' and image_key_control dataset img_key_control_path = os.path.join( scan.entry, "instrument", "detector", "image_key_control" ) numpy.testing.assert_array_equal( h5py_read_dataset(h5s[img_key_control_path][-2:]), [0, 1] ) img_key_path = os.path.join( scan.entry, "instrument", "detector", "image_key" ) numpy.testing.assert_array_equal( h5py_read_dataset(h5s[img_key_path][-2:]), [0, 1] ) # test rotation angle and count_time count_time_path = os.path.join( scan.entry, "instrument", "detector", "count_time" ) numpy.testing.assert_array_equal( h5py_read_dataset(h5s[count_time_path][-1]), 1 ) self.assertEqual( len(h5s[count_time_path]), self.nproj + self.end_flat.shape[0] ) rotation_angle_path = os.path.join( scan.entry, "sample", "rotation_angle" ) numpy.testing.assert_array_equal( h5s[rotation_angle_path][-1], h5s[rotation_angle_path][-2] ) self.assertEqual( len(h5s[rotation_angle_path]), self.nproj + self.end_flat.shape[0], ) # insure we can still make a HDF5Scan out of it NXtomoScan(scan=scan.master_file, entry=scan.entry) class TestAddDarkAndFlatWithFancySelection(BaseTestAddDarkAndFlats): """Insure we can do some fancy selection with virtual dataset and data url""" def testValid(self): """ Insure virtual dataset are still correctly recreate even if using slices """ scan = self._nx_with_virtual_dataset_with_link start_flat_url = DataUrl( file_path=self.start_flat_file, data_path=self.start_flat_entry, data_slice=slice(0, 4), scheme="silx", ) end_dark_url = DataUrl( file_path=self.end_dark_file, data_path=self.end_dark_entry, data_slice=[ 1, ], scheme="silx", ) add_dark_flat_nx_file( file_path=scan.master_file, entry=scan.entry, flats_start=start_flat_url, darks_end=end_dark_url, embed_data=False, ) # test data is correctly store with HDF5File(scan.master_file, mode="r", swmr=get_swmr_mode()) as h5s: # test the 'data' dataset data_path = os.path.join(scan.entry, "instrument", "detector", "data") self.assertEqual( h5s[data_path].shape, (self.nproj + 3 + 1, self.dim, self.dim), ) numpy.testing.assert_array_almost_equal( h5s[data_path][0:3], self.start_flat[0:3] ) numpy.testing.assert_array_almost_equal( h5s[data_path][-1], self.end_dark[1] ) NXtomoScan(scan=scan.master_file, entry=scan.entry) # test the 'image_key' and image_key_control dataset img_key_control_path = os.path.join( scan.entry, "instrument", "detector", "image_key_control" ) numpy.testing.assert_array_equal( h5py_read_dataset(h5s[img_key_control_path][0:3]), [1, 1, 1] ) numpy.testing.assert_array_equal( h5py_read_dataset(h5s[img_key_control_path][-2:]), [0, 2] ) # insure we can still make a HDF5Scan out of it NXtomoScan(scan=scan.master_file, entry=scan.entry) def testInValidSlice(self): """Insure an error is raise if user try to provide a slice with step !=1. This case is not handled """ scan = self._nx_with_virtual_dataset_with_link start_flat_url = DataUrl( file_path=self.start_flat_file, data_path=self.start_flat_entry, data_slice=slice(0, 4, 2), scheme="silx", ) end_dark_url = DataUrl( file_path=self.end_dark_file, data_path=self.end_dark_entry, data_slice=[ 1, ], scheme="silx", ) with self.assertRaises(ValueError): add_dark_flat_nx_file( file_path=scan.master_file, entry=scan.entry, flats_start=start_flat_url, darks_end=end_dark_url, embed_data=False, ) class TestCompleteAddFlatAndDark(BaseTestAddDarkAndFlats): """ Complete test on adding dark and flats on a complete case """ def testWithoutExtras(self): """Insure a complete case can be handle without defining any extras parameters""" scan = self._nx_with_virtual_dataset_with_link add_dark_flat_nx_file( file_path=scan.master_file, entry=scan.entry, flats_start=self.start_flat_url, flats_end=self.end_flat_url, darks_start=self.start_dark_url, extras={}, ) with HDF5File(scan.master_file, mode="r", swmr=get_swmr_mode()) as h5s: # test the 'data' dataset data_path = os.path.join(scan.entry, "instrument", "detector", "data") th_shape = ( self.nproj + self.end_flat.shape[0] + self.start_flat.shape[0] + self.start_dark.shape[0], self.dim, self.dim, ) self.assertEqual(h5s[data_path].shape, th_shape) # test the 'image_key' and image_key_control dataset numpy.testing.assert_array_almost_equal( h5s[data_path][0], self.start_dark[0] ) numpy.testing.assert_array_almost_equal( h5s[data_path][1], self.start_flat[0] ) numpy.testing.assert_array_almost_equal( h5s[data_path][-1], self.end_flat[0] ) numpy.testing.assert_array_almost_equal( h5s[data_path][-self.end_flat.shape[0] - 1], self._raw_data[-1] ) # test image_key img_key_path = os.path.join( scan.entry, "instrument", "detector", "image_key" ) th_img_keys = [0] * self.nproj [th_img_keys.insert(0, 1) for _ in range(self.start_flat.shape[0])] [th_img_keys.insert(0, 2) for _ in range(self.start_dark.shape[0])] [th_img_keys.append(1) for _ in range(self.end_flat.shape[0])] numpy.testing.assert_array_equal(h5s[img_key_path], th_img_keys) # test rotation_angle dataset rotation_angle_dataset = os.path.join( scan.entry, "sample", "rotation_angle" ) numpy.testing.assert_array_almost_equal( h5s[rotation_angle_dataset][0], h5s[rotation_angle_dataset][1] ) numpy.testing.assert_array_almost_equal( h5s[rotation_angle_dataset][0], h5s[rotation_angle_dataset][2] ) numpy.testing.assert_array_almost_equal( h5s[rotation_angle_dataset][-2], h5s[rotation_angle_dataset][-1] ) def testWithExtras(self): """Insure a complete case can be handle providing some extras parameters""" scan = self._nx_with_virtual_dataset dark_extras = {"rotation_angle": 10.2} start_flat_extras = {"rotation_angle": [10, 11, 12]} end_flat_extras = {"count_time": 0.3} end_dark_extras = {"count_time": [0.1, 0.2], "rotation_angle": [89, 88]} extras = { "darks_start": dark_extras, "darks_end": end_dark_extras, "flats_start": start_flat_extras, "flats_end": end_flat_extras, } add_dark_flat_nx_file( file_path=scan.master_file, entry=scan.entry, flats_start=self.start_flat_url, flats_end=self.end_flat_url, darks_start=self.start_dark_url, darks_end=self.end_dark_url, extras=extras, ) with HDF5File(scan.master_file, mode="r", swmr=get_swmr_mode()) as h5s: # test the 'data' dataset data_path = os.path.join(scan.entry, "instrument", "detector", "data") th_shape = ( self.nproj + self.end_flat.shape[0] + self.start_flat.shape[0] + self.start_dark.shape[0] + self.end_dark.shape[0], self.dim, self.dim, ) self.assertEqual(h5s[data_path].shape, th_shape) # test the 'image_key' and image_key_control dataset numpy.testing.assert_array_almost_equal( h5s[data_path][0], self.start_dark[0] ) numpy.testing.assert_array_almost_equal( h5s[data_path][1], self.start_flat[0] ) numpy.testing.assert_array_almost_equal( h5s[data_path][-1], self.end_flat[0] ) numpy.testing.assert_array_almost_equal( h5s[data_path][-self.end_flat.shape[0] - self.end_dark.shape[0] - 1], self._raw_data[-1], ) numpy.testing.assert_array_almost_equal( h5s[data_path][-3], self.end_dark[-2] ) # test image_key img_key_path = os.path.join( scan.entry, "instrument", "detector", "image_key" ) th_img_keys = [0] * self.nproj [th_img_keys.insert(0, 1) for _ in range(self.start_flat.shape[0])] [th_img_keys.insert(0, 2) for _ in range(self.start_dark.shape[0])] [th_img_keys.append(2) for _ in range(self.end_dark.shape[0])] [th_img_keys.append(1) for _ in range(self.end_flat.shape[0])] numpy.testing.assert_array_equal(h5s[img_key_path], th_img_keys) # test rotation_angle dataset rotation_angle_dataset = os.path.join( scan.entry, "sample", "rotation_angle" ) numpy.testing.assert_array_almost_equal( h5s[rotation_angle_dataset][0], 10.2 ) numpy.testing.assert_array_almost_equal(h5s[rotation_angle_dataset][-3], 89) numpy.testing.assert_array_almost_equal( h5py_read_dataset(h5s[rotation_angle_dataset][1:4]), numpy.array([10, 11, 12]), ) numpy.testing.assert_array_almost_equal( h5s[rotation_angle_dataset][-2], h5s[rotation_angle_dataset][-1] ) count_time_dataset = os.path.join( scan.entry, "instrument", "detector", "count_time" ) self.assertEqual(h5s[count_time_dataset][-1], 0.3) self.assertEqual(h5s[count_time_dataset][-2], 0.2) self.assertEqual(h5s[count_time_dataset][0], 1) class TestChangeImageKeyControl(unittest.TestCase): """ Test the `change_image_key_control` function """ def setUp(self) -> None: self.tmpdir = tempfile.mkdtemp() simple_nx_path = os.path.join(self.tmpdir, "simple_case") self.dim = 55 self.nproj = 20 # this can will have one dark, then 4 flats then 20 projections # then 4 flats and 5 alignment projections at the end mock = MockNXtomo( scan_path=simple_nx_path, n_proj=self.nproj, n_ini_proj=self.nproj, create_ini_dark=True, create_ini_flat=True, create_final_flat=True, dim=self.dim, n_refs=4, ) n_alignment = 5 for i in range(n_alignment): mock.add_alignment_radio(i, angle=0) self.scan = mock.scan def tearDown(self) -> None: if os.path.exists(self.tmpdir): shutil.rmtree(self.tmpdir) def testInputType(self): """Check a TypeError is raised if input of `frames_indexes` is invalid""" with self.assertRaises(TypeError): change_image_key_control( file_path=self.scan.master_file, entry=self.scan.entry, frames_indexes=1, image_key_control_value=ImageKey.PROJECTION, ) def testUpdateToProjections(self): """Insure we can correctly set some frame as `projection`""" change_image_key_control( file_path=self.scan.master_file, entry=self.scan.entry, frames_indexes=slice(0, 3, 2), image_key_control_value=ImageKey.PROJECTION, ) with HDF5File(self.scan.master_file, mode="r", swmr=get_swmr_mode()) as h5s: image_keys_path = "/".join( (self.scan.entry, "instrument", "detector", "image_key") ) image_keys = h5s[image_keys_path] image_keys_control_path = "/".join( (self.scan.entry, "instrument", "detector", "image_key_control") ) self.assertEqual(image_keys[0], ImageKey.PROJECTION.value) self.assertEqual(image_keys[1], ImageKey.FLAT_FIELD.value) self.assertEqual(image_keys[2], ImageKey.PROJECTION.value) image_keys_control = h5s[image_keys_control_path] self.assertEqual(image_keys_control[0], ImageKey.PROJECTION.value) self.assertEqual(image_keys_control[1], ImageKey.FLAT_FIELD.value) self.assertEqual(image_keys_control[2], ImageKey.PROJECTION.value) def testUpdateToDark(self): """Insure we can correctly set some frame as `dark`""" change_image_key_control( file_path=self.scan.master_file, entry=self.scan.entry, frames_indexes=[1], image_key_control_value=ImageKey.DARK_FIELD, ) with HDF5File(self.scan.master_file, mode="r", swmr=get_swmr_mode()) as h5s: image_keys_path = "/".join( (self.scan.entry, "instrument", "detector", "image_key") ) image_keys = h5s[image_keys_path] image_keys_control_path = "/".join( (self.scan.entry, "instrument", "detector", "image_key_control") ) self.assertEqual(image_keys[0], ImageKey.DARK_FIELD.value) self.assertEqual(image_keys[1], ImageKey.DARK_FIELD.value) self.assertEqual(image_keys[2], ImageKey.FLAT_FIELD.value) image_keys_control = h5s[image_keys_control_path] self.assertEqual(image_keys_control[0], ImageKey.DARK_FIELD.value) self.assertEqual(image_keys_control[1], ImageKey.DARK_FIELD.value) self.assertEqual(image_keys_control[2], ImageKey.FLAT_FIELD.value) def testUpdateToFlat(self): """Insure we can correctly set some frame as `flatfield`""" change_image_key_control( file_path=self.scan.master_file, entry=self.scan.entry, frames_indexes=[0], image_key_control_value=ImageKey.FLAT_FIELD, ) with HDF5File(self.scan.master_file, mode="r", swmr=get_swmr_mode()) as h5s: image_keys_path = "/".join( (self.scan.entry, "instrument", "detector", "image_key") ) image_keys = h5s[image_keys_path] image_keys_control_path = "/".join( (self.scan.entry, "instrument", "detector", "image_key_control") ) self.assertEqual(image_keys[0], ImageKey.FLAT_FIELD.value) self.assertEqual(image_keys[1], ImageKey.FLAT_FIELD.value) image_keys_control = h5s[image_keys_control_path] self.assertEqual(image_keys_control[0], ImageKey.FLAT_FIELD.value) self.assertEqual(image_keys_control[1], ImageKey.FLAT_FIELD.value) def testUpdateToAlignment(self): """Insure we can correctly set some frame as `alignment`""" change_image_key_control( file_path=self.scan.master_file, entry=self.scan.entry, frames_indexes=slice(10, 20, None), image_key_control_value=ImageKey.ALIGNMENT, ) with HDF5File(self.scan.master_file, mode="r", swmr=get_swmr_mode()) as h5s: image_keys_path = "/".join( (self.scan.entry, "instrument", "detector", "image_key") ) image_keys = h5s[image_keys_path] image_keys_control_path = "/".join( (self.scan.entry, "instrument", "detector", "image_key_control") ) self.assertEqual(image_keys[10], ImageKey.PROJECTION.value) self.assertEqual(image_keys[11], ImageKey.PROJECTION.value) self.assertEqual(image_keys[20], ImageKey.PROJECTION.value) image_keys_control = h5s[image_keys_control_path] self.assertEqual(image_keys_control[9], ImageKey.PROJECTION.value) self.assertEqual(image_keys_control[10], ImageKey.ALIGNMENT.value) self.assertEqual(image_keys_control[11], ImageKey.ALIGNMENT.value) self.assertEqual(image_keys_control[21], ImageKey.PROJECTION.value) def testUpdateToInvalid(self): """Insure we can correctly set some frame as `invalid`""" change_image_key_control( file_path=self.scan.master_file, entry=self.scan.entry, frames_indexes=slice(0, 16, 5), image_key_control_value=ImageKey.INVALID, ) with HDF5File(self.scan.master_file, mode="r", swmr=get_swmr_mode()) as h5s: image_keys_path = "/".join( (self.scan.entry, "instrument", "detector", "image_key") ) image_keys = h5s[image_keys_path] image_keys_control_path = "/".join( (self.scan.entry, "instrument", "detector", "image_key_control") ) self.assertEqual(image_keys[9], ImageKey.PROJECTION.value) self.assertEqual(image_keys[10], ImageKey.INVALID.value) self.assertEqual(image_keys[11], ImageKey.PROJECTION.value) self.assertEqual(image_keys[15], ImageKey.INVALID.value) image_keys_control = h5s[image_keys_control_path] self.assertEqual(image_keys_control[9], ImageKey.PROJECTION.value) self.assertEqual(image_keys_control[10], ImageKey.INVALID.value) self.assertEqual(image_keys_control[11], ImageKey.PROJECTION.value) self.assertEqual(image_keys_control[15], ImageKey.INVALID.value) class TestAddDarkAndFlatFromADifferentFolderWithVDS(unittest.TestCase): """ Test adding dark and flat to vds pointing to vds pointing on files at at different level of the file system. root_folder _______________________|_____________ | | folder_1 folder_2 | | file created _________________________|___________________ | | | subfolder_21 subfolder_22 file with | | original start dark File containing ____|___________ VDS pointing to | | original flat File with subsubfolder_221 and dark original | start flat File with original end flat """ def setUp(self) -> None: unittest.TestCase.setUp(self) self.dim = 20 # create folder self.root_folder = tempfile.mkdtemp() self.mv_folder = tempfile.mkdtemp() self.folder_1 = os.path.join(self.root_folder, "folder_1") self.folder_2 = os.path.join(self.root_folder, "folder_2") self.subfolder_21 = os.path.join(self.root_folder, "subfolder_21") self.subfolder_22 = os.path.join(self.root_folder, "subfolder_22") self.subsubfolder_221 = os.path.join(self.root_folder, "subsubfolder_221") for folder in ( self.folder_1, self.folder_2, self.subfolder_21, self.subfolder_22, self.subsubfolder_221, ): os.makedirs(folder) # create original dark self.start_dark = ( numpy.random.random((5 * self.dim * self.dim)) .reshape(5, self.dim, self.dim) .astype("f") ) self.start_dark_file = os.path.join(self.folder_2, "original_start_dark.hdf5") with HDF5File(self.start_dark_file, mode="w") as h5s: h5s["dark"] = self.start_dark self.start_dark_url = DataUrl( file_path=self.start_dark_file, data_path="dark", scheme="silx" ) # create original flats self.start_flat = ( numpy.random.random((5 * self.dim * self.dim)) .reshape(5, self.dim, self.dim) .astype("f") ) self.start_flat_file = os.path.join(self.subfolder_22, "original_start_flat.h5") with HDF5File(self.start_flat_file, mode="w") as h5s: h5s["flat1"] = self.start_flat self.start_flat_url = DataUrl( file_path=self.start_flat_file, data_path="flat1", scheme="silx" ) self.end_flat = ( numpy.random.random((5 * self.dim * self.dim)) .reshape(5, self.dim, self.dim) .astype("f") ) self.end_flat_file = os.path.join(self.subsubfolder_221, "original_end_flat.h5") with HDF5File(self.end_flat_file, mode="w") as h5s: h5s["flat2"] = self.end_flat self.end_flat_url = DataUrl( file_path=self.end_flat_file, data_path="flat2", scheme="silx" ) def tearDown(self) -> None: for folder in (self.root_folder, self.mv_folder): if os.path.exists(folder): shutil.rmtree(folder) unittest.TestCase.tearDown(self) def test(self): # create a scan contained in subfolder21 scan_subfolder_21 = self._simple_nx = MockNXtomo( scan_path=self.subfolder_21, n_proj=10, n_ini_proj=2, create_ini_dark=False, create_ini_flat=False, create_final_flat=False, dim=self.dim, ).scan # 1. add first level of indirection for dark and flat and check the VDS add_dark_flat_nx_file( file_path=scan_subfolder_21.master_file, entry=scan_subfolder_21.entry, flats_start=self.start_flat_url, flats_end=self.end_flat_url, darks_start=self.start_dark_url, extras={ "darks_start": {"count_time": 0.3, "rotation_angle": 0.0}, "flats_start": {"count_time": 0.3, "rotation_angle": 0.0}, "flats_end": {"count_time": 0.3, "rotation_angle": 0.0}, }, ) self.check_vds_from_file( file_=scan_subfolder_21.master_file, nx_entry=scan_subfolder_21.entry ) # create a scan contained in subfolder221 scan_subfolder_221 = self._simple_nx = MockNXtomo( scan_path=self.subsubfolder_221, n_proj=10, n_ini_proj=2, create_ini_dark=False, create_ini_flat=False, create_final_flat=False, dim=self.dim, ).scan # 2. create a second level of indirection for dark and flat and check # the VDS det_data_path = "/".join( (scan_subfolder_21.entry, "instrument", "detector", "data") ) new_dark_url = DataUrl( file_path=scan_subfolder_21.master_file, data_path=det_data_path, data_slice=slice(0, 5), scheme="silx", ) new_start_flat_url = DataUrl( file_path=scan_subfolder_21.master_file, data_path=det_data_path, data_slice=slice(5, 10), scheme="silx", ) new_end_flat_url = DataUrl( file_path=scan_subfolder_21.master_file, data_path=det_data_path, data_slice=slice(12, 17), scheme="silx", ) add_dark_flat_nx_file( file_path=scan_subfolder_221.master_file, entry=scan_subfolder_221.entry, flats_start=new_start_flat_url, flats_end=new_end_flat_url, darks_start=new_dark_url, extras={ "darks_start": {"count_time": 0.3, "rotation_angle": 0.0}, "flats_start": {"count_time": 0.3, "rotation_angle": 0.0}, "flats_end": {"count_time": 0.3, "rotation_angle": 0.0}, }, ) self.check_vds_from_file( file_=scan_subfolder_221.master_file, nx_entry=scan_subfolder_221.entry ) # 3. move root folder and remove it to insure links are style valid shutil.move(src=self.folder_1, dst=self.mv_folder) shutil.move(src=self.folder_2, dst=self.mv_folder) new_scan_subfolder_221_m = scan_subfolder_221.master_file.replace( self.mv_folder, self.root_folder ) self.check_vds_from_file( file_=new_scan_subfolder_221_m, nx_entry=scan_subfolder_221.entry ) def check_vds_from_file(self, file_, nx_entry): with HDF5File(file_, mode="r", swmr=get_swmr_mode()) as h5s: entry_node = h5s[nx_entry] det_group = entry_node["instrument/detector"] det_data = det_group["data"] det_image_key = det_group["image_key"] # test dark numpy.testing.assert_array_equal( det_image_key[:5], [ImageKey.DARK_FIELD.value] * 5 ) numpy.testing.assert_array_equal(det_data[:5], self.start_dark) # test start flat numpy.testing.assert_array_equal( det_image_key[5:10], [ImageKey.FLAT_FIELD.value] * 5 ) numpy.testing.assert_array_equal(det_data[5:10], self.start_flat) # test end flat numpy.testing.assert_array_equal( det_image_key[-5:], [ImageKey.FLAT_FIELD.value] * 5 ) numpy.testing.assert_array_equal(det_data[-5:], self.end_flat) nxtomomill-v2.0.1/nxtomomill/utils/utils.py000066400000000000000000000421361511430602400211600ustar00rootroot00000000000000# coding: utf-8 """An :class:`.Enum` class with additional features.""" from __future__ import annotations import logging import os from datetime import datetime import numpy from silx.io.url import DataUrl from silx.io.utils import get_data from silx.io.utils import open as open_hdf5 from silx.utils.deprecation import deprecated from silx.utils.enum import Enum as _Enum from tomoscan.esrf.scan.utils import cwd_context from tomoscan.io import HDF5File from nxtomo.nxobject.nxdetector import ImageKey from nxtomo.utils.frameappender import FrameAppender from nxtomo.application.nxtomo import NXtomo try: import hdf5plugin # noqa F401 except ImportError: pass import uuid from silx.io.utils import h5py_read_dataset __all__ = [ "embed_url", "FileExtension", "get_file_name", "get_tuple_of_keys_from_cmd", "is_nx_tomo_entry", "add_dark_flat_nx_file", "change_image_key_control", "str_datetime_to_numpy_datetime64", "strip_extension", ] def embed_url(url: DataUrl, output_file: str) -> DataUrl: """ Create a dataset under duplicate_data and with a random name to store it :param DataUrl url: dataset to be copied :param output_file: where to store the dataset :param expected_type: some metadata to put in copied dataset attributes :param data: data loaded from url is already loaded """ if not isinstance(url, DataUrl): return url elif url.file_path() == output_file: return url else: embed_data_path = "/".join(("/duplicate_data", str(uuid.uuid1()))) with cwd_context(os.path.dirname(os.path.abspath(output_file))): with HDF5File(output_file, "a") as h5s: h5s[embed_data_path] = get_data(url) h5s[embed_data_path].attrs["original_url"] = url.path() return DataUrl( file_path=output_file, data_path=embed_data_path, scheme="silx" ) class FileExtension(_Enum): H5 = ".h5" HDF5 = ".hdf5" NX = ".nx" NXS = ".nxs" def get_file_name(file_name, extension, check=True): """ set the given extension :param file_name: name of the file :param extension: extension to give :param check: if check, already check if the file as one of the '_FileExtension' """ if isinstance(extension, str): extension = FileExtension(extension.lower()) assert isinstance(extension, FileExtension) if check: for item in FileExtension: if file_name.lower().endswith(item.value): return file_name return file_name + extension.value def get_tuple_of_keys_from_cmd(cmd_value: str) -> tuple: """Return a tuple""" return tuple(cmd_value.split(",")) def is_nx_tomo_entry(file_path, entry): """ :param file_path: hdf5 file path :param entry: entry to check :return: True if the entry is an NXTomo entry """ if not os.path.exists(file_path): return False else: with open_hdf5(file_path) as h5s: if entry not in h5s: return False node = h5s[entry] return NXtomo.node_is_nxtomo(node) def add_dark_flat_nx_file( file_path: str, entry: str, darks_start: numpy.ndarray | DataUrl | None = None, flats_start: numpy.ndarray | DataUrl | None = None, darks_end: numpy.ndarray | DataUrl | None = None, flats_end: numpy.ndarray | DataUrl | None = None, extras: dict | None = None, logger: None | logging.Logger = None, embed_data: bool = False, ): """ This will get all data from entry@input_file and patch them with provided dark and / or flat(s). We consider the sequence as: dark, start_flat, projections, end_flat. Behavior regarding data type and target dataset: * if dataset at `entry` already exists: * if dataset at `entry` is a 'standard' dataset: * data will be loaded if necessary and `enrty` will be updated * if dataset at `entry` is a virtual dataset: * if `data` is a numpy array then we raise an error: the data should already be saved somewhere and you should provide a DataUrl * if `data` is a DataUrl then the virtual dataset is updated and a virtual source pointing to the DataUrl.file_path()@DataUrl.data_path() is added to the layout * if a new dataset `entry` need to be added: * if `data` is a numpy array then we create a new 'standard' Dataset * if `data` is a DataUrl then a new virtual dataset will be created note: Datasets `image_key`, `image_key_control`, `rotation_angle` and `count_time` will be copied each time. :param file_path: NXTomo file containing data to be patched :param entry: entry to be patched :param darks_start: (3D) numpy array containing the first dark serie if any :param flats_start: (3D) numpy array containing the first flat if any :param darks_end: (3D) numpy array containing dark the second dark serie if any :param flats_end: (3D) numpy array containing the second flat if any :param extras: dictionary to specify some parameters for flats and dark like rotation angle. valid keys: 'start_dark', 'end_dark', 'start_flag', 'end_flag'. Values should be a dictionary of 'NXTomo' keys with values to be set instead of 'default values'. Possible values are: * `count_time` * `rotation_angle` :param logger: object for logs :param embed_data: if True then each external data will be copy under a 'duplicate_data' folder """ if extras is None: extras = {} else: for key in extras: valid_extra_keys = ("darks_start", "darks_end", "flats_start", "flats_end") if key not in valid_extra_keys: raise ValueError( f"{key} is not recognized. Valid values are {valid_extra_keys}" ) if embed_data is True: darks_start = embed_url(darks_start, output_file=file_path) darks_end = embed_url(darks_end, output_file=file_path) flats_start = embed_url(flats_start, output_file=file_path) flats_end = embed_url(flats_end, output_file=file_path) else: for url in (darks_start, darks_end, flats_start, flats_end): if url is not None and isinstance(url, DataUrl): if isinstance(url.data_slice(), slice): if url.data_slice().step not in (None, 1): raise ValueError( "When data is not embed slice `step`" "must be None or 1. Other values are" f"not handled. Failing url is {url}" ) # !!! warning: order of dark / flat treatments import data_names = "flats_start", "darks_end", "flats_end", "darks_start" datas = flats_start, darks_end, flats_end, darks_start keys_value = ( ImageKey.FLAT_FIELD.value, ImageKey.DARK_FIELD.value, ImageKey.FLAT_FIELD.value, ImageKey.DARK_FIELD.value, ) wheres = "start", "end", "end", "start" # warning: order import for d_n, data, key, where in zip(data_names, datas, keys_value, wheres): if data is None: continue n_frames_to_insert = 1 if isinstance(data, str): data = DataUrl(path=data) if isinstance(data, numpy.ndarray) and data.ndim == 3: n_frames_to_insert = data.shape[0] elif isinstance(data, DataUrl): with open_hdf5(data.file_path()) as h5s: if data.data_path() not in h5s: raise KeyError( f"Path given ({data.data_path()}) is not in {data.file_path}" ) data_node = get_data(data) if data_node.ndim == 3: n_frames_to_insert = data_node.shape[0] else: raise TypeError(f"{type(data)} as input is not managed") if logger is not None: logger.info(f"insert {type(data)} frame of type {key} at the {where}") # update 'data' dataset data_path = os.path.join(entry, "instrument", "detector", "data") FrameAppender( data, file_path, data_path=data_path, where=where, logger=logger ).process() # update image-key and image_key_control (we are not managing the # 'alignment projection here so values are identical') ik_path = os.path.join(entry, "instrument", "detector", "image_key") ikc_path = os.path.join(entry, "instrument", "detector", "image_key_control") for path in (ik_path, ikc_path): FrameAppender( [key] * n_frames_to_insert, file_path, data_path=path, where=where, logger=logger, ).process() # add 'other' necessaries key: count_time_path = os.path.join( entry, "instrument", "detector", "count_time", ) rotation_angle_path = os.path.join(entry, "sample", "rotation_angle") x_translation_path = os.path.join(entry, "sample", "x_translation") translation_y_path = os.path.join(entry, "sample", "translation_y") translation_z_path = os.path.join(entry, "sample", "translation_z") control_data_path = os.path.join(entry, "control", "data") data_key_paths = ( count_time_path, rotation_angle_path, x_translation_path, translation_y_path, translation_z_path, control_data_path, ) mandatory_keys = ( "count_time", "rotation_angle", ) optional_keys = ( "x_translation", "translation_y", "translation_z", "control/data", ) data_keys = tuple(list(mandatory_keys) + list(optional_keys)) for data_key, data_key_path in zip(data_keys, data_key_paths): data_to_insert = None if d_n in extras and data_key in extras[d_n]: provided_value = extras[d_n][data_key] if _is_iterable(provided_value): if len(provided_value) != n_frames_to_insert: raise ValueError( "Given value to store from extras has" f" incoherent length({len(provided_value)}) compare to " f"the number of frame to save ({n_frames_to_insert})" ) else: data_to_insert = provided_value else: try: data_to_insert = [provided_value] * n_frames_to_insert except Exception as e: logger.error(f"Fail to create data to insert. Error is {e}") return else: # get default values def get_default_value(location, where_): with open_hdf5(file_path) as h5s: if location not in h5s: return None existing_data = h5s[location] if where_ == "start": return existing_data[0] else: return existing_data[-1] try: default_value = get_default_value(data_key_path, where) except Exception: default_value = None if default_value is None: msg = f"Unable to define a default value for {data_key_path}. Location empty in {file_path}" if data_key in mandatory_keys: raise ValueError(msg) elif logger: logger.warning(msg) continue elif logger: logger.debug( f"No value(s) provided for {data_key_path}. Extract some default value ({default_value})." ) data_to_insert = [default_value] * n_frames_to_insert if data_to_insert is not None: FrameAppender( data_to_insert, file_path, data_path=data_key_path, where=where, logger=logger, ).process() @deprecated(replacement="_FrameAppender", since_version="0.5.0") def _insert_frame_data(data, file_path, data_path, where, logger=None): """ This function is used to insert some frame(s) (numpy 2D or 3D to an existing dataset. Before the existing array or After. :param data: :param file_path: :param data_path: If the path point to a virtual dataset them this one will be updated but data should be a DataUrl. Of the same shape. Else we will update the data_path by extending the dataset. :param where: :raises TypeError: In the case the data type and existing data_path are incompatible. """ fa = FrameAppender( data=data, file_path=file_path, data_path=data_path, where=where, logger=logger ) return fa.process() def change_image_key_control( file_path: str, entry: str, frames_indexes, image_key_control_value: int | ImageKey, logger=None, ): """ Will modify image_key and image_key_control values for the requested frames. :param file_path: path the nexus file :param entry: name of the entry to modify :param frames_indexes: index of the frame for which we want to modify the image key :param image_key_control_value: :param logging.Logger logger: logger """ if not isinstance(frames_indexes, slice) and not _is_iterable(frames_indexes): raise TypeError("`frame_indexes` should be a sequence or slice") if logger: logger.info( "Update frames {frames_indexes} to" "{image_key_control_value} of {entry}@{file_path}" "".format( frames_indexes=frames_indexes, image_key_control_value=image_key_control_value, entry=entry, file_path=file_path, ) ) image_key_control_value = ImageKey(image_key_control_value) with HDF5File(file_path, mode="a") as h5s: node = h5s[entry] image_keys_path = "/".join(("instrument", "detector", "image_key")) image_keys = h5py_read_dataset(node[image_keys_path]) image_keys_control_path = "/".join( ("instrument", "detector", "image_key_control") ) image_keys_control = h5py_read_dataset(node[image_keys_control_path]) # filter frame indexes if isinstance(frames_indexes, slice): step = frames_indexes.step if step is None: step = 1 stop = frames_indexes.stop if stop in (None, -1): stop = len(image_keys) frames_indexes = list(range(frames_indexes.start, stop, step)) frames_indexes = list( filter(lambda x: 0 <= x <= len(image_keys_control), frames_indexes) ) # manage image_key_control image_keys_control[frames_indexes] = image_key_control_value.value node[image_keys_control_path][:] = image_keys_control # manage image_key. In this case we should get rid of Alignment values # and replace it by Projection values image_key_value = image_key_control_value if image_key_value is ImageKey.ALIGNMENT: image_key_value = ImageKey.PROJECTION image_keys[frames_indexes] = image_key_value.value node[image_keys_path][:] = image_keys def str_datetime_to_numpy_datetime64(my_datetime: str | datetime) -> numpy.datetime64: # numpy deprecates time zone awarness conversion to numpy.datetime64. # so we remove the time zone info. if isinstance(my_datetime, str): datetime_as_datetime = datetime.fromisoformat(my_datetime) elif isinstance(my_datetime, datetime): datetime_as_datetime = my_datetime else: raise TypeError( f"my_datetime is expected to be a str or an instance of datetime. Not {type(my_datetime)}" ) datetime_as_utc_datetime = datetime_as_datetime.astimezone(None) tz_free_datetime_as_datetime = datetime_as_utc_datetime.replace(tzinfo=None) return numpy.datetime64(tz_free_datetime_as_datetime).astype("/dev/null \ || pip install --quiet --user --upgrade pre-commit # Install the pre-commit hook on the local repository pre-commit install nxtomomill-v2.0.1/pyproject.toml000066400000000000000000000050741511430602400170200ustar00rootroot00000000000000[project] name = "nxtomomill" authors = [ { name = "Henri Payno", email = "henri.payno@esrf.fr" }, { name = "Pierre Paleo", email = "pierre.paleo@esrf.fr" }, { name = "Pierre-Olivier Autran", email = "pierre-olivier.autran@esrf.fr" }, { name = "Jérôme Lesaint", email = "jerome.lesaint@esrf.fr" }, { name = "Alessandro Mirone", email = "mirone@esrf.fr" } ] dynamic = ["version"] description = "applications and library to convert raw format to NXtomo format" readme = { file = "README.md", content-type = "text/markdown" } requires-python = ">=3.10" keywords = ["NXtomo", "nexus", "tomography", "tomotools", "esrf", "bliss-tomo"] license = {file = "LICENSE"} classifiers = [ "Intended Audience :: Education", "Intended Audience :: Science/Research", "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Environment :: Console", "Environment :: X11 Applications :: Qt", "Operating System :: POSIX", "Natural Language :: English", "Topic :: Scientific/Engineering :: Physics", "Topic :: Software Development :: Libraries :: Python Modules" ] dependencies = [ "numpy", "h5py>=3.0", "silx>=2.0", "nxtomo>=3.0.0dev0", "pint", "packaging", "tomoscan[full] >=2.2.0a4", "tqdm", "pydantic", "eval_type_backport; python_version < '3.10'", ] [project.urls] Homepage = "https://gitlab.esrf.fr/tomotools/nxtomomill" Documentation = "https://tomotools.gitlab-pages.esrf.fr/nxtomomill/" Repository = "https://gitlab.esrf.fr/tomotools/nxtomomill" Changelog = "https://gitlab.esrf.fr/tomotools/nxtomomill/-/blob/master/CHANGELOG.md" [project.optional-dependencies] test = [ "pytest", "python-gitlab" ] doc = [ "pytest", "Sphinx >=4.0.0, <5.2.0", "nbsphinx", "pandoc", "ipykernel", "jupyter_client", "nbconvert", "scikit-image", "h5glance", "pydata_sphinx_theme", "sphinx-design", "sphinx-autodoc-typehints", "sphinxcontrib-programoutput", "myst-parser", ] [build-system] requires = [ "setuptools>=61.0", "wheel", "numpy" ] build-backend = "setuptools.build_meta" [project.scripts] nxtomomill = "nxtomomill.__main__:main" [tool.setuptools.dynamic] version = {attr = "nxtomomill.__version__"} [tool.setuptools.packages.find] where = ["."] # list of folders that contain the packages (["."] by default) [tool.sphinx] source-dir = "./doc"