pax_global_header00006660000000000000000000000064145603723270014523gustar00rootroot0000000000000052 comment=0747ced21fe52867c5c678213f714c1244ec0dbd libpyvinyl-1.2.0/000077500000000000000000000000001456037232700137245ustar00rootroot00000000000000libpyvinyl-1.2.0/.bumpversion.cfg000066400000000000000000000007231456037232700170360ustar00rootroot00000000000000[bumpversion] current_version = 1.2.0 commit = True tag = False parse = (?P\d+)\.(?P\d+)\.(?P\d+)((?P[a-z+])(?P\d+))? serialize = {major}.{minor}.{patch}{release}{build} {major}.{minor}.{patch} [bumpversion:part:release] first_value = a values = a b 0 [bumpversion:part:build] first_value = 1 [bumpversion:file:./libpyvinyl/__init__.py] search = __version__ = "{current_version}" replace = __version__ = "{new_version}" libpyvinyl-1.2.0/.github/000077500000000000000000000000001456037232700152645ustar00rootroot00000000000000libpyvinyl-1.2.0/.github/workflows/000077500000000000000000000000001456037232700173215ustar00rootroot00000000000000libpyvinyl-1.2.0/.github/workflows/ci.yml000066400000000000000000000030421456037232700204360ustar00rootroot00000000000000name: CI on: [push, pull_request] jobs: test: runs-on: ubuntu-20.04 strategy: matrix: python-version: ["3.6", "3.7", "3.8", "3.9", "3.10"] steps: - uses: actions/checkout@v3 - uses: mpi4py/setup-mpi@v1 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} - name: Install production dependencies run: | python -m pip install --upgrade pip pip install -r requirements.txt pip install pytest pip install . - name: Unit testing run: | pytest . - name: Test McStasScript run: | git clone https://github.com/PaNOSC-ViNYL/McStasScript.git cd McStasScript pip install -e . cd mcstasscript/tests pytest . cd - name: Test SimEx-Lite run: | # Install pysingfel backend from the simex branch pip install git+https://github.com/JunCEEE/pysingfel.git@simex # Install and test SimEx-Lite git clone --recursive https://github.com/PaNOSC-ViNYL/SimEx-Lite.git cd SimEx-Lite pip install -e . git clone https://github.com/PaNOSC-ViNYL/SimEx-Lite-testFiles testFiles pytest tests - name: Install development dependencies run: | pip install -r requirements/dev.txt - name: Code formatting run: | black --check libpyvinyl/ libpyvinyl-1.2.0/.gitignore000066400000000000000000000004031456037232700157110ustar00rootroot00000000000000*.pyc *.pyo .*.*.swp .idea *.bak *.code-workspace .ropeproject tags doc/build libpyvinyl.egg-info .#*.* .readthedocs.yaml **/notebooks/*.json **/notebooks/*.h5 **/notebooks/tmp* **/notebooks/.ipynb_checkpoints/ tests/*.json tests/*.h5 tests/tmp* dist/ build/ libpyvinyl-1.2.0/.readthedocs.yaml000066400000000000000000000010771456037232700171600ustar00rootroot00000000000000# .readthedocs.yaml # 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-22.04 tools: python: "3.8" # Build documentation in the docs/ directory with Sphinx sphinx: configuration: doc/source/conf.py # We recommend specifying your dependencies to enable reproducible builds: # https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html python: install: - requirements: doc/requirements.txt libpyvinyl-1.2.0/DEVEL.md000066400000000000000000000054171456037232700151140ustar00rootroot00000000000000# Contributing ## How to test Minimally needed: ``` pip install -e ./ cd tests/unit python Test.py ``` Recommended: A simple `pytest` command will run the unittests and integration tests. ``` pytest ./ ``` You should see a test report similar to this: ``` =============================================================== test session starts ================================================================ platform linux -- Python 3.8.10, pytest-6.2.4, py-1.10.0, pluggy-0.13.1 rootdir: /home/juncheng/Projects/libpyvinyl collected 100 items integration/plusminus/tests/test_ArrayCalculators.py . [ 1%] integration/plusminus/tests/test_Instrument.py . [ 2%] integration/plusminus/tests/test_NumberCalculators.py ... [ 5%] integration/plusminus/tests/test_NumberData.py ........... [ 16%] unit/test_BaseCalculator.py .......... [ 26%] unit/test_BaseData.py ........................... [ 53%] unit/test_Instrument.py ....... [ 60%] unit/test_Parameters.py ........................................ [100%] =============================================================== 100 passed in 0.56s ================================================================ ``` You can also run unittests only: ``` pytest tests/unit ``` Or to run integration tests only: ``` pytest tests/integration ``` ## Git workflow 1. Branch from the current `master` branch 2. Develop into the newly created branch 3. Create appropriate unit tests in [tests/unit/](https://github.com/PaNOSC-ViNYL/libpyvinyl/tree/master/tests/unit) 4. Test current development as indicated in [Testing](https://github.com/PaNOSC-ViNYL/libpyvinyl#testing). 5. `git rebase -i master` w.r.t. current master to include the latest updates and squashing commits to a minimum. See also [here](https://opensource.com/article/20/4/git-rebase-i). 6. Push your `BRANCH` to the upstream repo: `git push -f upstream BRANCH`. 7. Create a pull request (PR) to the `master` branch on the GitHub page. 8. PR should be reviewed and approved and be passing all CI tests. 9. If passing all tests, Choose `Rebase and merge` to merge the PR with no further squashing. libpyvinyl-1.2.0/INSTALL.md000066400000000000000000000013441456037232700153560ustar00rootroot00000000000000## Installation We recommend installation in a virtual environment, either `conda` or `pyenv`. ### Create a `conda` environment ``` $> conda create -n libpyvinyl ``` ### Common users ``` $> pip install libpyvinyl ``` ### Developers of libpyvinyl We provide a requirements file for developers in _requirements/dev.txt_. ``` $> conda install --file requirements/dev.txt --file requirements/prod.txt ``` **or** ``` $> pip install -r requirements/prod.txt -r requirements/dev.txt ``` Then, install `libpyvinyl` into the same environment. The `-e` flag links the installed library to the source code in the local path, such that changes in the latter are immediately effective in the installed version. ``` $> pip install -e . ``` libpyvinyl-1.2.0/LICENSE000066400000000000000000000164201456037232700147340ustar00rootroot00000000000000 GNU LESSER GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright © 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. This version of the GNU Lesser General Public License incorporates the terms and conditions of version 3 of the GNU General Public License, supplemented by the additional permissions listed below. 0. Additional Definitions. As used herein, “this License” refers to version 3 of the GNU Lesser General Public License, and the “GNU GPL” refers to version 3 of the GNU General Public License. “The Library” refers to a covered work governed by this License, other than an Application or a Combined Work as defined below. An “Application” is any work that makes use of an interface provided by the Library, but which is not otherwise based on the Library. Defining a subclass of a class defined by the Library is deemed a mode of using an interface provided by the Library. A “Combined Work” is a work produced by combining or linking an Application with the Library. The particular version of the Library with which the Combined Work was made is also called the “Linked Version”. The “Minimal Corresponding Source” for a Combined Work means the Corresponding Source for the Combined Work, excluding any source code for portions of the Combined Work that, considered in isolation, are based on the Application, and not on the Linked Version. The “Corresponding Application Code” for a Combined Work means the object code and/or source code for the Application, including any data and utility programs needed for reproducing the Combined Work from the Application, but excluding the System Libraries of the Combined Work. 1. Exception to Section 3 of the GNU GPL. You may convey a covered work under sections 3 and 4 of this License without being bound by section 3 of the GNU GPL. 2. Conveying Modified Versions. If you modify a copy of the Library, and, in your modifications, a facility refers to a function or data to be supplied by an Application that uses the facility (other than as an argument passed when the facility is invoked), then you may convey a copy of the modified version: a) under this License, provided that you make a good faith effort to ensure that, in the event an Application does not supply the function or data, the facility still operates, and performs whatever part of its purpose remains meaningful, or b) under the GNU GPL, with none of the additional permissions of this License applicable to that copy. 3. Object Code Incorporating Material from Library Header Files. The object code form of an Application may incorporate material from a header file that is part of the Library. You may convey such object code under terms of your choice, provided that, if the incorporated material is not limited to numerical parameters, data structure layouts and accessors, or small macros, inline functions and templates (ten or fewer lines in length), you do both of the following: a) Give prominent notice with each copy of the object code that the Library is used in it and that the Library and its use are covered by this License. b) Accompany the object code with a copy of the GNU GPL and this license document. 4. Combined Works. You may convey a Combined Work under terms of your choice that, taken together, effectively do not restrict modification of the portions of the Library contained in the Combined Work and reverse engineering for debugging such modifications, if you also do each of the following: a) Give prominent notice with each copy of the Combined Work that the Library is used in it and that the Library and its use are covered by this License. b) Accompany the Combined Work with a copy of the GNU GPL and this license document. c) For a Combined Work that displays copyright notices during execution, include the copyright notice for the Library among these notices, as well as a reference directing the user to the copies of the GNU GPL and this license document. d) Do one of the following: 0) Convey the Minimal Corresponding Source under the terms of this License, and the Corresponding Application Code in a form suitable for, and under terms that permit, the user to recombine or relink the Application with a modified version of the Linked Version to produce a modified Combined Work, in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source. 1) Use a suitable shared library mechanism for linking with the Library. A suitable mechanism is one that (a) uses at run time a copy of the Library already present on the user's computer system, and (b) will operate properly with a modified version of the Library that is interface-compatible with the Linked Version. e) Provide Installation Information, but only if you would otherwise be required to provide such information under section 6 of the GNU GPL, and only to the extent that such information is necessary to install and execute a modified version of the Combined Work produced by recombining or relinking the Application with a modified version of the Linked Version. (If you use option 4d0, the Installation Information must accompany the Minimal Corresponding Source and Corresponding Application Code. If you use option 4d1, you must provide the Installation Information in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source.) 5. Combined Libraries. You may place library facilities that are a work based on the Library side by side in a single library together with other library facilities that are not Applications and are not covered by this License, and convey such a combined library under terms of your choice, if you do both of the following: a) Accompany the combined library with a copy of the same work based on the Library, uncombined with any other library facilities, conveyed under the terms of this License. b) Give prominent notice with the combined library that part of it is a work based on the Library, and explaining where to find the accompanying uncombined form of the same work. 6. Revised Versions of the GNU Lesser General Public License. The Free Software Foundation may publish revised and/or new versions of the GNU Lesser General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Library as you received it specifies that a certain numbered version of the GNU Lesser General Public License “or any later version” applies to it, you have the option of following the terms and conditions either of that published version or of any later version published by the Free Software Foundation. If the Library as you received it does not specify a version number of the GNU Lesser General Public License, you may choose any version of the GNU Lesser General Public License ever published by the Free Software Foundation. If the Library as you received it specifies that a proxy can decide whether future versions of the GNU Lesser General Public License shall apply, that proxy's public statement of acceptance of any version is permanent authorization for you to choose that version for the Library. libpyvinyl-1.2.0/README.md000066400000000000000000000073741456037232700152160ustar00rootroot00000000000000# libpyvinyl - The python APIs for Virtual Neutron and x-raY Laboratory [![CI](https://github.com/PaNOSC-ViNYL/libpyvinyl/actions/workflows/ci.yml/badge.svg)](https://github.com/PaNOSC-ViNYL/libpyvinyl/actions/workflows/ci.yml) [![Documentation Status](https://readthedocs.org/projects/libpyvinyl/badge/?version=latest)](https://libpyvinyl.readthedocs.io/en/latest/?badge=latest) [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.6558164.svg)](https://doi.org/10.5281/zenodo.6558164) ## Overview [GitHub repository](https://github.com/PaNOSC-ViNYL/libpyvinyl) **Installation instructions** [here](INSTALL.md) Requires: ``` python >= 3.6 ``` Simulations provide invaluable insights to plan (before) and understand (after) experiments at neutron and X-ray facilities. A wide set of libraries and programs is already available to simulate neutron and X-ray beams, propagate them through the beamlines, interact with the sample and get data acquired by detectors. The aim of _libpyvinyl_ is to provide a high level neutron and X-ray simulation API. With this harmonized user interface we achieve seamless interoperability of individual simulations thereby facilitating the concatenation of simulation steps into a simulation pipeline. The vast differences with respect to parameter names, unit conventions, configuration syntax, i.e. the user interface, is, hence, overcome creating a `libpyvinyl` compliant API for each simulation software. ## Software specific APIs based on libpyvinyl The python package `libpyvinyl` provides a way to harmonize the user interfaces of such simulation codes. It is an object oriented library; its classes define the user interface to simulation codes, simulation parameters and simulation data. For a given simulation code, e.g. propagation of neutron or photon beams through a beamline, a new class would have to be defined that derives from the `libpyvinyl` classes. This derived class requires the implementation of certain methods meant to configure a simulation, launch the simulation code, and retrieve the output data. Since the interplay between parametrization, execution, and IO is already taken care of at the level of `libpyvinyl`'s base classes, the effort to define a specialized interface (parameters, backengine and data object) for a new simulation code is rather minimal. This structure allows integrating simulation codes into simulation pipelines in the above sense. ## What the libpyvinyl API offers This API offers a homogeneous interface to: - Configure a simulation. - Launch the simulation run. - Collect the simulation output data. - Construct a `Data` instance that represents the simulation output data. - Snapshoot a simulation by dumping the object to disk. - Reload a simulation run from disk and continue the run with optionally modified parameters. ## Who should use this library Three kind of users are the target of this package: 1. developers of packages based on libpyvinyl offering new calculators for simulations 1. users wishing to run a simulation giving some inputs and retrieving the results 1. research facility experts that what to implement detailed simulation of existing instruments at their facility or willing to design new ones ## libpyvinyl projects There are currently two projects based on libpyvinyl: - McStatsScript: [https://github.com/PaNOSC-ViNYL/McStasScript](https://github.com/PaNOSC-ViNYL/McStasScript) - SimEx-Lite:[https://github.com/PaNOSC-ViNYL/SimEx-Lite](https://github.com/PaNOSC-ViNYL/SimEx-Lite) ## Documentation Documentation can be generated as follows using sphinx: ``` cd doc/ pip install requirements.txt make html ``` ## Acknowledgement This project has received funding from the European Union's Horizon 2020 research and innovation programme under grant agreement No. 823852. libpyvinyl-1.2.0/doc/000077500000000000000000000000001456037232700144715ustar00rootroot00000000000000libpyvinyl-1.2.0/doc/Makefile000066400000000000000000000011111456037232700161230ustar00rootroot00000000000000# Minimal makefile for Sphinx documentation # # You can set these variables from the command line. 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) libpyvinyl-1.2.0/doc/make.bat000066400000000000000000000014271456037232700161020ustar00rootroot00000000000000@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% goto end :help %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% :end popd libpyvinyl-1.2.0/doc/requirements.txt000066400000000000000000000002001456037232700177450ustar00rootroot00000000000000-r ../requirements/prod.txt sphinx nbsphinx sphinx_rtd_theme sphinx_autodoc_typehints pandoc sphinx-markdown-tables myst-parser libpyvinyl-1.2.0/doc/source/000077500000000000000000000000001456037232700157715ustar00rootroot00000000000000libpyvinyl-1.2.0/doc/source/_templates/000077500000000000000000000000001456037232700201265ustar00rootroot00000000000000libpyvinyl-1.2.0/doc/source/_templates/footer.html000066400000000000000000000020551456037232700223140ustar00rootroot00000000000000{% extends "!footer.html" %} {% block extrafooter %} {{super}}

Creative Commons License This documentation is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License.

The software libpyvinyl is licensed under the LGPL version 3 or later.

This project has received funding from the European Union's Horizon 2020 research and innovation programme under grant agreement No. 823852 Photon and Neutron Open Science Cloud

{% endblock %}libpyvinyl-1.2.0/doc/source/conf.py000066400000000000000000000143461456037232700173000ustar00rootroot00000000000000# -*- coding: utf-8 -*- # # Configuration file for the Sphinx documentation builder. # # This file does only contain a selection of the most common options. For a # full list see the documentation: # http://www.sphinx-doc.org/en/master/config # -- 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, "../../libpyvinyl") sys.path.insert(0, "../../") import libpyvinyl import sphinx_rtd_theme # -- Project information ----------------------------------------------------- project = "libpyvinyl" copyright = ( "2020-2024, Carsten Fortmann-Grote, Mads Bertelsen, Juncheng E, Shervin Nourbakhsh" ) author = "Carsten Fortmann-Grote, Mads Bertelsen, Juncheng E, Shervin Nourbakhsh" # The short X.Y version version = libpyvinyl.__version__ # The full version, including alpha/beta/rc tags release = libpyvinyl.__release__ # -- General configuration --------------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. # # needs_sphinx = '1.0' # 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.doctest", "sphinx.ext.intersphinx", "sphinx.ext.todo", "sphinx.ext.coverage", "sphinx.ext.mathjax", "sphinx.ext.viewcode", "sphinx_rtd_theme", "nbsphinx", "sphinx.ext.autosummary", "sphinx_autodoc_typehints", "myst_parser", ] # Add any paths that contain templates here, relative to this directory. templates_path = ["_templates"] # The suffix(es) of source filenames. # You can specify multiple suffix as a list of string: # # source_suffix = ['.rst', '.md'] source_suffix = ".rst" # The master toctree document. master_doc = "index" # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. # # This is also used if you do content translation via gettext catalogs. # Usually you set "language" from the command line for these cases. language = None # 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 = [] # The name of the Pygments (syntax highlighting) style to use. pygments_style = None # -- 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 = "sphinx_rtd_theme" # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. # # html_theme_options = {} # 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"] # Custom sidebar templates, must be a dictionary that maps document names # to template names. # # The default sidebars (for documents that don't match any pattern) are # defined by theme itself. Builtin themes are using these templates by # default: ``['localtoc.html', 'relations.html', 'sourcelink.html', # 'searchbox.html']``. # # html_sidebars = {} # -- Options for HTMLHelp output --------------------------------------------- # Output file base name for HTML help builder. htmlhelp_basename = project + "doc" # -- Options for LaTeX output ------------------------------------------------ latex_elements = { # The paper size ('letterpaper' or 'a4paper'). # # 'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). # # 'pointsize': '10pt', # Additional stuff for the LaTeX preamble. # # 'preamble': '', # Latex figure (float) alignment # # 'figure_align': 'htbp', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ ( master_doc, project + ".tex", project + " Documentation", author, "manual", ), ] # -- Options for manual page output ------------------------------------------ # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [(master_doc, project, project + " Documentation", [author], 1)] # -- Options for Texinfo output ---------------------------------------------- # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ ( master_doc, project, project + " Documentation", author, project, "One line description of project.", "Miscellaneous", ), ] # -- Options for Epub output ------------------------------------------------- # Bibliographic Dublin Core info. epub_title = project # The unique identifier of the text. This can be a ISBN number # or the project homepage. # # epub_identifier = '' # A unique identification for the text. # # epub_uid = '' # A list of files that should not be packed into the epub file. epub_exclude_files = ["search.html"] # -- Extension configuration ------------------------------------------------- autoclass_content = "both" autodoc_default_flags = ["members", "show-inheritance"] # -- Options for intersphinx extension --------------------------------------- # Example configuration for intersphinx: refer to the Python standard library. intersphinx_mapping = {"https://docs.python.org/": None} # -- Options for todo extension ---------------------------------------------- # If true, `todo` and `todoList` produce output, else they produce nothing. todo_include_todos = True autoclass_content = "both" autodoc_default_flags = ["members", "show-inheritance"] libpyvinyl-1.2.0/doc/source/include/000077500000000000000000000000001456037232700174145ustar00rootroot00000000000000libpyvinyl-1.2.0/doc/source/include/DEVEL.md000077700000000000000000000000001456037232700226122../../../DEVEL.mdustar00rootroot00000000000000libpyvinyl-1.2.0/doc/source/include/INSTALL.md000077700000000000000000000000001456037232700233302../../../INSTALL.mdustar00rootroot00000000000000libpyvinyl-1.2.0/doc/source/include/Quickstart.ipynb000066400000000000000000000632351456037232700226220ustar00rootroot00000000000000{ "cells": [ { "cell_type": "markdown", "id": "729e0589-1610-404f-a4e9-7abf276c4af9", "metadata": {}, "source": [ "# Quickstart\n", "\n", "This is the quickstart guide for creating a simulation workflow package based on `libpyvinyl`. Please\n", "first install `libpyvinyl` following the instruction in the [Installation](INSTALL.md) section.\n", "\n", "## Introduction\n", "This section is intended to help a developer understand how a new simulation package can use libpyvinyl as a foundation. It is important to understand that libpyvinyl provides base classes from which a developer inherits more specialised classes from, and the final class then both contains the new functionality and the basic capabilities. To make a new package, a developer would have to inherit from these baseclasses:\n", "\n", "- BaseCalculator\n", "- BaseData\n", "- BaseDataFormat\n", "\n", "**Calculator**\n", "\n", "The specialised calculator that inherits from BaseCalculator is capable of performing a calculation of some sort. The calculation can depend on some data and input values for parameters specified when the calculator is built. The calculator can also return output data. The scope of a calculator is somewhat arbitrary, but the power of libpyvinyl comes from the ability to break a big calculation down into smaller parts of individual calculators. When using a calculator, it is easy for the user to understand a small number of parameters as there are less risks of ambiguity. A rich Parameter class is provided by libpyvinyl to create the necessary parameters in each calculator. When creating a parameter it is possible to set allowed intervals to avoid undefined behaviour. \n", "\n", "**Data**\n", "\n", "To create a description of some data that can be either given to or returned from a calculator one starts with the BaseData class. This data could be for example a number of particle states.\n", "\n", "**DataFormat**\n", "\n", "Each Data class will have a number of supported DataFormat which are necessary in order to save the data to disk. Our particle data from before could be saved as a json, yaml or some compressed format, and each would need a DataFormat class that contains methods to read and write such data, and make it available to a corresponding Data class.\n", "\n", "### First steps as a developer\n", "To build a simulation package in this framework, think about what calculation need to be performed and what parameters are needed to describe it. Then divide this big calculation into calculators with a limited number of parameters and clear input and output data. For example a particle source, it would need parameters describing the properties of emitted particles and then return a Data object with a large number of particle states. Then a calculator describing a piece of optics might have parameters describing its geometry, and it could have particle states as both input and output. With these kinds of considerations it becomes clear what Calculators and Data classes should be written.\n", "\n", "### Benefit of libpyvinyl\n", "When a package uses libpyvinyl as a foundation, libpyvinyl can be used to write a simulation from a series of these calculators using the Instrument class. Here is an example of a series of calculators that form a simple instrument.\n", "\n", "\n", "| Calculator | Description | Parameters | Input Data | Output Data |\n", "|:--------------|:------------------|:------------------------------:|:---------------:|:---------------:|\n", "| Source | Emits particles | size, divergence, energy | None | particle states |\n", "| Monochromator | Crystal | position, d_spacing, mosaicity | particle states | particle states |\n", "| Monochromator | Crystal | position, d_spacing, mosaicity | particle states | particle states |\n", "| Sample | Crystal sample | position, d_spacing, mosaicity | particle states | particle states |\n", "| Detector | Particle detector | position, size, sensitivity | particle states | counts in bins |\n", "\n", "This setup uses two monochromators, each with their own parameters. The user can set up a master parameter that control both, for example to ensure they have the same d_spacing. Running the instrument then corresponds to running each calculator in turn and providing the output of one to the next.\n", "\n", "## Design a minimal instrument\n", "As a minimal start, we will create an instrument with a calculator that can get the sum of two numbers.\n", "\n", "There are 3 specialized classes needed to be defined for the package:\n", "\n", "- `CalculatorClass`: a class based on `BaseCalculator` to perform the calculation.\n", "- `DataClass`: to represent the input and output data of the `CalculatorClass`.\n", "- `FormatClass`: the interface to exchange data between the memory and the file on the disk in a specific format.\n", "\n", "### Define a simple python object mapping DataClass\n", "Let's firstly define a `NumberData` class mapping the python objects in the memory. This is done by creating a mapping\n", "dictionary to connect the data (e.g. an array or a single value) in the python object to the reference variable." ] }, { "cell_type": "code", "execution_count": 1, "id": "b6f9eb6e-4bc9-480d-ad6a-63d11aa0d3d8", "metadata": {}, "outputs": [], "source": [ "from libpyvinyl.BaseData import BaseData\n", "\n", "class NumberData(BaseData):\n", " def __init__(self,key,data_dict=None,filename=None,\n", " file_format_class=None,file_format_kwargs=None):\n", "\n", " expected_data = {}\n", "\n", " ### DataClass developer's job start\n", " expected_data[\"number\"] = None\n", " ### DataClass developer's job end\n", "\n", " super().__init__(key,expected_data,data_dict,\n", " filename,file_format_class,file_format_kwargs)\n", "\n", " @classmethod\n", " def supported_formats(self):\n", " ### DataClass developer's job start\n", " format_dict = {}\n", " ### DataClass developer's job end\n", " return format_dict\n", "\n", "# Test if the definition works\n", "data = NumberData(key=\"test\")" ] }, { "cell_type": "markdown", "id": "75909323-a9d0-41fd-814f-621700669476", "metadata": {}, "source": [ "The above example shows a minimal definition of a DataClass. There are only two sections need to consider by the simulation package developers:\n", "\n", "- `expected_data`: A dictionary whose keys are the expected keys of the dictionary returned by `get_data()`, we just simply would like to get a \"number\" from a `NumberData`.\n", "- `format_dict`: A dictionary of supported format for hard disk files. Now we only need a python object mapper, so we just assign an empty dict to it for the moment.\n", "\n", "\n", "### Define a DataClass also supporting file mapping\n", "For a software writing the data to a file instead of a python object, it's necessary to have a interface between the file and the DataClass. We create a FormatClass\n", "as the interface:" ] }, { "cell_type": "code", "execution_count": 2, "id": "5c915a81-5f01-4fba-9f27-a6d7d5f7d4f9", "metadata": {}, "outputs": [], "source": [ "import numpy as np\n", "from libpyvinyl.BaseFormat import BaseFormat\n", "\n", "class TXTFormat(BaseFormat):\n", " def __init__(self) -> None:\n", " super().__init__()\n", "\n", " @classmethod\n", " def format_register(self):\n", " key = \"TXT\"\n", " desciption = \"TXT format for NumberData\"\n", " file_extension = \".txt\"\n", " read_kwargs = [\"\"]\n", " write_kwargs = [\"\"]\n", " return self._create_format_register(\n", " key, desciption, file_extension, read_kwargs, write_kwargs\n", " )\n", "\n", " @staticmethod\n", " def direct_convert_formats():\n", " return []\n", " \n", " @classmethod\n", " def convert(\n", " cls, obj: BaseData, output: str, output_format_class: str, key, **kwargs):\n", " raise NotImplementedError\n", "\n", "\n", " @classmethod\n", " def read(cls, filename: str) -> dict:\n", " \"\"\"Read the data from the file with the `filename` to\n", " a dictionary. The dictionary will be used by its corresponding data class.\"\"\"\n", " number = float(np.loadtxt(filename))\n", " data_dict = {\"number\": number}\n", " return data_dict\n", "\n", " @classmethod\n", " def write(cls, object, filename: str, key: str = None):\n", " \"\"\"Save the data with the `filename`.\"\"\"\n", " data_dict = object.get_data()\n", " arr = np.array([data_dict[\"number\"]])\n", " np.savetxt(filename, arr, fmt=\"%.3f\")\n", " if key is None:\n", " original_key = object.key\n", " key = original_key + \"_to_TXTFormat\"\n", " return object.from_file(filename, cls, key)\n", "\n", "# Test if the definition works\n", "data = TXTFormat()" ] }, { "cell_type": "markdown", "id": "5bc6eae3-c0fc-4330-bafe-b38b4ba734ea", "metadata": {}, "source": [ "In the above example, we create a `TXTFormat` class based on the `BaseFormat` abstract class. We need to provide:\n", "\n", "- The information of the `format_register` method to get registered in the `NumberData.supported_formats()` method. This will be explained later.\n", "- the `read` function to read the data from the file into the `data_dict`, which will be accessed by the `NumberData` class by `NumberData.get_data()`. The dictionary keys match those in the `expected_data` of `NumberData`.\n", "- The `write` function to write the `NumberData` object into a file in `TXTFormat`.\n", "\n", "For the other methods above, we just need to copy but don't have to touch them at this moment.\n", "\n", "Then, we just need add the `TXTFormat` to the `NumberData` created in the last section." ] }, { "cell_type": "code", "execution_count": 3, "id": "1c8ab1d7-e88a-4d64-895a-133e7d31d883", "metadata": {}, "outputs": [], "source": [ "class NumberData(BaseData):\n", " def __init__(self,key,data_dict=None,filename=None,\n", " file_format_class=None,file_format_kwargs=None):\n", "\n", " expected_data = {}\n", "\n", " ### DataClass developer's job start\n", " expected_data[\"number\"] = None\n", " ### DataClass developer's job end\n", "\n", " super().__init__(key,expected_data,data_dict,\n", " filename,file_format_class,file_format_kwargs)\n", "\n", " @classmethod\n", " def supported_formats(self):\n", " ### DataClass developer's job start\n", " format_dict = {}\n", " self._add_ioformat(format_dict, TXTFormat)\n", " ### DataClass developer's job end\n", " return format_dict" ] }, { "cell_type": "markdown", "id": "8ba80c0f-704f-474a-a2fc-f291e35aa787", "metadata": {}, "source": [ "You can list the formats it supports with:" ] }, { "cell_type": "code", "execution_count": 4, "id": "bc96109b-7b4a-4cf4-9f28-23250c6d5649", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Format class: \n", "Key: TXT\n", "Description: TXT format for NumberData\n", "File extension: .txt\n", "\n", "\n" ] } ], "source": [ "NumberData.list_formats()" ] }, { "cell_type": "markdown", "id": "2accdd52-94da-4f9a-a420-c3703cbf34c4", "metadata": {}, "source": [ "### Define a Calculator with native python object output\n", "Assuming we have a simulation code whose output is a native python object (e.g. a list or dict), we can create a CalculatorClass for the simulation code:" ] }, { "cell_type": "code", "execution_count": 5, "id": "b8bc5c15-181b-4db0-9e6b-1155ff670b36", "metadata": {}, "outputs": [], "source": [ "from typing import Union\n", "from pathlib import Path\n", "from libpyvinyl.BaseData import DataCollection\n", "from libpyvinyl.BaseCalculator import BaseCalculator, CalculatorParameters\n", "\n", "class PlusCalculator(BaseCalculator):\n", " def __init__(self, name: str, input: Union[DataCollection, list, NumberData], \n", " output_keys: Union[list, str] = [\"plus_result\"],\n", " output_data_types=[NumberData], output_filenames: Union[list, str] = [], \n", " instrument_base_dir=\"./\", calculator_base_dir=\"PlusCalculator\",\n", " parameters=None):\n", " \"\"\"A python object calculator example\"\"\"\n", " super().__init__(name, input, output_keys, output_data_types=output_data_types,\n", " output_filenames=output_filenames, instrument_base_dir=instrument_base_dir,\n", " calculator_base_dir=calculator_base_dir, parameters=parameters)\n", "\n", " def init_parameters(self):\n", " parameters = CalculatorParameters()\n", " times = parameters.new_parameter(\n", " \"plus_times\", comment=\"How many times to do the plus\"\n", " )\n", " times.value = 1\n", " self.parameters = parameters\n", "\n", " def backengine(self):\n", " Path(self.base_dir).mkdir(parents=True, exist_ok=True)\n", " input_num0 = self.input.to_list()[0].get_data()[\"number\"]\n", " input_num1 = self.input.to_list()[1].get_data()[\"number\"]\n", " output_num = float(input_num0) + float(input_num1)\n", " if self.parameters[\"plus_times\"].value > 1:\n", " for i in range(self.parameters[\"plus_times\"].value - 1):\n", " output_num += input_num1\n", " data_dict = {\"number\": output_num}\n", " key = self.output_keys[0]\n", " output_data = self.output[key]\n", " output_data.set_dict(data_dict)\n", " return self.output" ] }, { "cell_type": "markdown", "id": "cddca02c-9a4e-41b2-bb43-9d428207f441", "metadata": {}, "source": [ "In the above example, we define a `PlusCalculator` based on the `BaseCalculator`. The following needs to be provided:\n", "\n", "- Some default output-related values to initialize empty output Data containers (see [here](https://github.com/JunCEEE/libpyvinyl/blob/5d14bdc107a1536ec08a467e5b446fbddaa1b7b1/libpyvinyl/BaseCalculator.py#L344)):\n", " - output_keys: the key of each Data object in the output `DataCollection`\n", " - output_data_types: the Data type of each Data object.\n", " - output_filenames: the filenames of the output files (if any)\n", "- `init_parameters` to define the default values of the parameters need by the calculator. Range restrictions and units of values can be also set here. Details can be found in the `parameter` use guide.\n", "- `backengine` to define how to conduct the calculation. It should return a reference of the output DataCollection.\n", "\n", "The `PlusCalculator.backengine` adds two numbers enclosed in a input `DataCollection` for `PlusCalculator.parameters[\"plus_times\"].value` times. The reference dictionary\n", "of python objects `data_dict` is passed to the corresponding `NumberData` in the auto-initialized `self.output: DataCollection` by\n", "\n", "```py\n", "output_data.set_dict(data_dict)\n", "```" ] }, { "cell_type": "markdown", "id": "58bc131d-cdc5-4a11-9ffc-5198660fc93d", "metadata": { "tags": [] }, "source": [ "Let's create an instance from the class:" ] }, { "cell_type": "code", "execution_count": 6, "id": "1b49d8e6-e6dd-4410-b9ee-f8a01b46e0c3", "metadata": {}, "outputs": [], "source": [ "input1 = NumberData.from_dict({\"number\": 1}, \"input1\")\n", "input2 = NumberData.from_dict({\"number\": 1}, \"input2\")\n", "calculator_plus = PlusCalculator(name=\"test\",input=[input1,input2])" ] }, { "cell_type": "markdown", "id": "48a802d8-a0f0-40b4-b488-af470d050e6d", "metadata": { "jp-MarkdownHeadingCollapsed": true, "tags": [] }, "source": [ "Check available parameters of it:" ] }, { "cell_type": "code", "execution_count": 7, "id": "89b5a666-8bce-4af8-a2cb-7e4a98f62887", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ " - Parameters object -\n", "plus_times 1 How many times to do the plus \n", "\n" ] } ], "source": [ "print(calculator_plus.parameters)" ] }, { "cell_type": "markdown", "id": "ebe6a766-7956-4ffd-b55a-c4cd201eb1a8", "metadata": {}, "source": [ "Run the calculator with default parameters" ] }, { "cell_type": "code", "execution_count": 8, "id": "8efbe1b4-026f-41b6-8ea2-661e4529a8c8", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "{'number': 2.0}\n" ] } ], "source": [ "result = calculator_plus.backengine()\n", "print(result.get_data())" ] }, { "cell_type": "markdown", "id": "32bb6c82-19ba-4f6a-b987-0fb2e54ad6e3", "metadata": {}, "source": [ "Modify the parameter and see the difference:" ] }, { "cell_type": "code", "execution_count": 9, "id": "33117ed3-701e-4bc2-93e3-fa827dc31cd5", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "{'number': 6.0}\n" ] } ], "source": [ "calculator_plus.parameters[\"plus_times\"] = 5\n", "print(calculator_plus.backengine().get_data())" ] }, { "cell_type": "markdown", "id": "5f0f6daa-1fef-4e2d-a65d-bf30aed1ff3b", "metadata": {}, "source": [ "### Define a Calculator with native file output" ] }, { "cell_type": "code", "execution_count": 10, "id": "abecee9d-b450-469d-86c0-8e2f1e4a7049", "metadata": {}, "outputs": [], "source": [ "from typing import Union\n", "from pathlib import Path\n", "import numpy as np\n", "from libpyvinyl.BaseData import DataCollection\n", "from libpyvinyl.BaseCalculator import BaseCalculator, CalculatorParameters\n", "\n", "\n", "class MinusCalculator(BaseCalculator):\n", " def __init__(\n", " self,\n", " name: str,\n", " input: Union[DataCollection, list, NumberData],\n", " output_keys: Union[list, str] = [\"minus_result\"],\n", " output_data_types=[NumberData],\n", " output_filenames: Union[list, str] = [\"minus_result.txt\"],\n", " instrument_base_dir=\"./\",\n", " calculator_base_dir=\"MinusCalculator\",\n", " parameters=None,\n", " ):\n", " \"\"\"A python object calculator example\"\"\"\n", " super().__init__(\n", " name,\n", " input,\n", " output_keys,\n", " output_data_types=output_data_types,\n", " output_filenames=output_filenames,\n", " instrument_base_dir=instrument_base_dir,\n", " calculator_base_dir=calculator_base_dir,\n", " parameters=parameters,\n", " )\n", "\n", " def init_parameters(self):\n", " parameters = CalculatorParameters()\n", " times = parameters.new_parameter(\n", " \"minus_times\", comment=\"How many times to do the minus\"\n", " )\n", " times.value = 1\n", " self.parameters = parameters\n", "\n", " def backengine(self):\n", " Path(self.base_dir).mkdir(parents=True, exist_ok=True)\n", " input_num0 = self.input.to_list()[0].get_data()[\"number\"]\n", " input_num1 = self.input.to_list()[1].get_data()[\"number\"]\n", " output_num = float(input_num0) - float(input_num1)\n", " if self.parameters[\"minus_times\"].value > 1:\n", " for i in range(self.parameters[\"minus_times\"].value - 1):\n", " output_num -= input_num1\n", " arr = np.array([output_num])\n", " file_path = self.output_file_paths[0]\n", " np.savetxt(file_path, arr, fmt=\"%.3f\")\n", " key = self.output_keys[0]\n", " output_data = self.output[key]\n", " output_data.set_file(file_path, TXTFormat)\n", " return self.output" ] }, { "cell_type": "markdown", "id": "95eb1531-e76d-460e-bbbd-22aa5fe9a873", "metadata": {}, "source": [ "`MinusCalculator` is the similar to `PlusCalculator` except its output_data is a `NumberData` mapping to `TXTFormat` instead of python object.\n", "\n", "The simulation results can be obtained in the same way as that of `PlusCalculator`" ] }, { "cell_type": "code", "execution_count": 11, "id": "287058e2-bc00-48cc-9cce-e1a2824794b1", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "{'number': 4.0}\n" ] } ], "source": [ "input1 = NumberData.from_dict({\"number\": 5}, \"input1\")\n", "input2 = NumberData.from_dict({\"number\": 1}, \"input2\")\n", "calculator_minus = MinusCalculator(name=\"test\",input=[input1,input2])\n", "output = calculator_minus.backengine()\n", "print(output.get_data())" ] }, { "cell_type": "markdown", "id": "3a04e182-0d90-4515-af4b-df7556a63f5f", "metadata": {}, "source": [ "We can see that `output` is now mapping to a file : " ] }, { "cell_type": "code", "execution_count": 12, "id": "68bce03f-7e44-4776-b5d0-b266127edb85", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Data collection:\n", "key - mapping\n", "\n", "minus_result - : MinusCalculator/minus_result.txt\n", "\n" ] } ], "source": [ "print(output)" ] }, { "cell_type": "markdown", "id": "b9a921ad-70bc-4ee3-b972-5eb1120bcae4", "metadata": {}, "source": [ "If we read the file, we should get the same result." ] }, { "cell_type": "code", "execution_count": 13, "id": "87ba42cf-45d0-43b4-8357-ab93263da7d7", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "MinusCalculator/minus_result.txt\n", "4.000\n", "\n" ] } ], "source": [ "print(output[\"minus_result\"].filename)\n", "with open(output[\"minus_result\"].filename,'r') as fh:\n", " print(fh.read())" ] }, { "cell_type": "markdown", "id": "d66b1772-75d0-47b1-8c8b-21d1fa259e3d", "metadata": {}, "source": [ "### Define an instrument\n", "We can assmeble a single `PlusMinus` instrument from the two Calculators to sum `input1` and `input2` and then subtract the result with `input2`:" ] }, { "cell_type": "code", "execution_count": 16, "id": "1dbaf841-3e0e-466c-b126-7e8e1e9bc388", "metadata": {}, "outputs": [], "source": [ "from libpyvinyl import Instrument\n", "\n", "# Create an Instrument with the name PlusMinus\n", "calculation_instrument = Instrument(\"PlusMinus\")\n", "\n", "# Create python object data as input\n", "input1 = NumberData.from_dict({\"number\": 1}, \"input1\")\n", "input2 = NumberData.from_dict({\"number\": 2}, \"input2\")\n", "calculator_plus = PlusCalculator(name=\"Plus\",input=[input1,input2])\n", "# The the output of calculator_plus as the input of calculator_minus\n", "calculator_minus = MinusCalculator(name=\"Minus\",input=[calculator_plus.output[\"plus_result\"],input2])\n", "\n", "# Assemble the instrument\n", "calculation_instrument.add_calculator(calculator_plus)\n", "calculation_instrument.add_calculator(calculator_minus)\n", "\n", "# Set the base output path of the instrument\n", "instrument_path = \"PlusMinus\"\n", "calculation_instrument.set_instrument_base_dir(str(instrument_path))" ] }, { "cell_type": "markdown", "id": "a9c43325-b30d-4bd3-a8f5-23fc4656957d", "metadata": {}, "source": [ "### Run the instrument" ] }, { "cell_type": "code", "execution_count": 17, "id": "e9270701-b200-4192-97e8-063975f96c50", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "{'number': 1.0}" ] }, "execution_count": 17, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# 1+2-2 = 1\n", "calculation_instrument.run()\n", "calculation_instrument.calculators['Minus'].output.get_data()" ] } ], "metadata": { "kernelspec": { "display_name": "simex-lite-dev", "language": "python", "name": "simex-lite-dev" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.8.12" } }, "nbformat": 4, "nbformat_minor": 5 } libpyvinyl-1.2.0/doc/source/include/README.md000077700000000000000000000000001456037232700230062../../../README.mdustar00rootroot00000000000000libpyvinyl-1.2.0/doc/source/include/UserGuide.rst000066400000000000000000000006021456037232700220400ustar00rootroot00000000000000=========================================== User Guide =========================================== .. toctree:: :maxdepth: 2 Introduction Parameters Calculators Data Instrument libpyvinyl-1.2.0/doc/source/include/libpyvinyl.drawio.svg000077700000000000000000000000001456037232700306642../../../libpyvinyl.drawio.svgustar00rootroot00000000000000libpyvinyl-1.2.0/doc/source/include/refman.rst000066400000000000000000000015121456037232700214150ustar00rootroot00000000000000API Reference Manual ==================== .. autosummary:: :toctree: :nosignatures: libpyvinyl.BaseCalculator libpyvinyl.Parameter libpyvinyl.CalculatorParameters libpyvinyl.Instrument libpyvinyl.BaseData libpyvinyl.BaseFormat .. automodule:: libpyvinyl.BaseCalculator :members: :undoc-members: :show-inheritance: .. automodule:: libpyvinyl.Parameters.Collections :members: :undoc-members: :show-inheritance: .. automodule:: libpyvinyl.Parameters.Parameter :members: :undoc-members: :show-inheritance: .. automodule:: libpyvinyl.Instrument :members: :undoc-members: :show-inheritance: .. automodule:: libpyvinyl.BaseData :members: :undoc-members: :show-inheritance: .. automodule:: libpyvinyl.BaseFormat :members: :undoc-members: :show-inheritance: libpyvinyl-1.2.0/doc/source/include/userguide/000077500000000000000000000000001456037232700214105ustar00rootroot00000000000000libpyvinyl-1.2.0/doc/source/include/userguide/userguide_calculators.md000066400000000000000000000110101456037232700263130ustar00rootroot00000000000000 ### BaseCalculator `BaseCalculator` is an abstract class to help the developers build their own specialized Calculator within the `libpyvinyl` framework. It takes a collection of Data derived from `BaseData` as input and output and executes the operations defined in the `backegine()` method. The behavior of the specialized Calculator is controlled by the parameters initialized in the `init_parameters()` method. When you develop a specialized Calculator, consider about: - What kinds are the input and output data? - What are the parameters controlling the behavior of the backengine? And what are their default values and their limits? - How should the backengine behave? - How to install the dependencies of the backengine? #### In the __init__ method In the ``` __init__(self, name: str, input: Union[DataCollection, list, BaseData], output_keys: Union[list, str], output_data_types: Union[list, BaseData], output_filenames: Union[list, str, None] = None, instrument_base_dir: str = "./", calculator_base_dir: str = "BaseCalculator", parameters: CalculatorParameters = None,) ``` method, `self.parameters` is assigned by the input `parameters`. If the `parameters` is `None`, `self.parameters` will be initialized by the `self.init_parameters()` method. The `input` variable can be either a `DataCollection`, a list of DataClass or a single DataClass. It will be converted and treated as a `DataCollection`. In the end. The `output_keys` defines the keys of the data in the `output` DataCollection. It is suggested to be used in the `self.backengine()` method. The `output_data_types` defines the types the DataClass of the data in the `output` DataCollection. If the output types are fixed in the `backengine()` method, it can be ignored in the derived class. An example fixing the output_data_types to NumberData: ```py class ExpCalculator(BaseCalculator): def __init__( self, name: str, input: Union[DataCollection, list, NumberData], output_keys: Union[list, str] = ["plus_result"], output_filenames: Union[list, str] = [], instrument_base_dir="./", calculator_base_dir="PlusCalculator", parameters=None, ): super().__init__( name, input, output_keys, output_data_types=[NumberData], output_filenames=output_filenames, instrument_base_dir=instrument_base_dir, calculator_base_dir=calculator_base_dir, parameters=parameters, ) ``` The `output_filenames` defines the filenames of the data in the `output` DataCollection, if the data is a file_mapping. If the data is a dict_mapping, then the variable can be ignored as well. An empty output `DataCollection` container: `self.output` is created in the `__init_output()` method, which is called by the `__init__()` method. The data types and keys are taken from the `self.output_data_types` and `self.output_data_keys` parameters. `instrument_base_dir` and `calculator_base_dir` set the base path of the calculator: ``` instrument_base_dir/calculator_base_dir ``` If the calculator is added to an `Instrument` class object, the `instrument_base_dir` will be modified by the object. #### Define the CalculatorParameters The definition of the parameters and their default values are set in the `init_parameters()` method. An empty `CalculatorParameters` object is firstly created and then filled with parameters needed. In the end, the object should be assigned to `self.parameters`. Example: ```py class PlusCalculator(BaseCalculator): ... def init_parameters(self): parameters = CalculatorParameters() times = parameters.new_parameter( "plus_times", comment="How many times to do the plus" ) times.value = 1 self.parameters = parameters ... ``` The detailed `parameters` guide: link #### Define the backengine There are several variables should be used in the `backeigine()`: - `self.input`: A `DataCollection` containing `input` data. `self.input.to_list()[0].get_data()` can get the first input data. - `self.output`: This initialized variable should be mapped to either a python dictionary or a file by either `set_dict()` or `set_file()`. For example: ```py key = self.output_keys[0] output_data = self.output[key] output_data.set_dict(data_dict) ``` #### Dump the object The finial users can use`dump()` and `from_dump()` to snapshot/restore a Calculator object. No modification needed when you create a derived class. libpyvinyl-1.2.0/doc/source/include/userguide/userguide_data.md000066400000000000000000000154111456037232700247210ustar00rootroot00000000000000### Data API `libpyvinyl` provides several abstract classes to create data interfaces. #### DataCollection `DataCollection` is a thin layer interface between the Calculator and DataClass. It aggregates the input and output into a single variable, respectively. A `DataCollection` can be initialized with several DataClass instances like this: ```py collection = DataCollection(data_1, data_2, ..., data_n) ``` or with the `add_data()`: ```py collection = DataCollection() collection.add_data(data_1, data_2, ..., data_n) ``` A data can be accessed by its key: ```py data_1 = collection["data_1_key"] ``` A list of data dictionaries of the data in a `DataCollection` can be obtained: ```py collection.get_data() ``` You can also create a list of the Data objects in the `DataCollection` ```py collection.to_list() ``` To get an overview of the `DataCollection`, just print it out: ``` print(collection) ``` #### BaseData A specialized Data class can be created for a kind of data with similar attributes based on the abstract `BaseData` class. The abstract class provides useful helper functions and a template for the Data interface. A file-mapping DataClass will not read the file until the final user calls `get_data()`, which calls the `read()` method of its `file_format_class` and returns the python dictionary of the data. The `file_format_class` is defined by one of these functions: To create/set a DataClass as a python dictionary mapping: - `from_dict()`: Create a class instance mapping from a python dictionary. - `set_dict()`: Set the class as a python dictionary mapping. To create/set a DataClass as a file mapping: - `set_file()`: Set the class as a file mapping. - `from_file()`: Create a class instance mapping from a file. To write the Data class into a file in a certain file format you can: ```py data_file = data.write(filename = 'test_file', format_class=FormatClass) ``` The file can then be written into a `test_file`, with the FormatClass you specify. To list the formats supported by the Data Class: - `list_formats()`: This method prints the return of `supported_formats()`, which needs to be defined for the derived class. ##### Develop a derived DataClass A DataClass derived from the `BaseData` class only needs two pieces of information: - `expected_data`: a dictionary whose key defines the data needed. - `supported_formats()`, it returns a dictionary describing the supported formats. The information is extracted from the format class with the `_add_ioformat()` method. An example: ```py class NumberData(BaseData): def __init__( self, key, data_dict=None, filename=None, file_format_class=None, file_format_kwargs=None, ): expected_data = {} ### DataClass developer's job start expected_data["number"] = None ### DataClass developer's job end super().__init__( key, expected_data, data_dict, filename, file_format_class, file_format_kwargs, ) @classmethod def supported_formats(self): format_dict = {} ### DataClass developer's job start self._add_ioformat(format_dict, TXTFormat.TXTFormat) self._add_ioformat(format_dict, H5Format.H5Format) ### DataClass developer's job end return format_dict ``` #### BaseFormat The Format class is the interface between the exact file and the python object. For each derived FormatClass, we have to provide the content of: - `format_register()`: to provide the meta data of this format. - `read()`: how do we read the file into a python dictionary, whose keys must include the keys of the `expected_data` of the DataClass connecting to this format. - `write()`: how do we write the data of the DataClass into a file in this format. Optionally, a direct convert method can be defined to avoid reading the whole data into the memory. See: - BaseFormat.direct_convert_formats() - BaseFormat.convert() ##### read() and write() The `read()` method needs to return a python dictionary required by its corresponding Data Class. Example: ```py class NumberData(BaseData): ... expected_data = {} ### DataClass developer's job start expected_data["number"] = None ... class TXTFormat(BaseFormat): ... @classmethod def read(cls, filename: str) -> dict: """Read the data from the file with the `filename` to a dictionary. The dictionary will be used by its corresponding data class.""" number = float(np.loadtxt(filename)) data_dict = {"number": number} return data_dict ... ``` The `write()` method should call `object.get_data()`, where the `object` is an instance of the FormatClass's corresponding DataClass, and write the data to the intended file. It is recommended to return a DataClass object mapping to the newly written file. ```py class TXTFormat(BaseFormat): ... @classmethod def write(cls, object: NumberData, filename: str, key: str = None): """Save the data with the `filename`.""" data_dict = object.get_data() arr = np.array([data_dict["number"]]) np.savetxt(filename, arr, fmt="%.3f") if key is None: original_key = object.key key = original_key + "_to_TXTFormat" return object.from_file(filename, cls, key) ... ``` ##### Example of a FormatClass: ```py class TXTFormat(BaseFormat): def __init__(self) -> None: super().__init__() @classmethod def format_register(self): key = "TXT" desciption = "TXT format for NumberData" file_extension = ".txt" read_kwargs = [""] write_kwargs = [""] return self._create_format_register( key, desciption, file_extension, read_kwargs, write_kwargs ) @staticmethod def direct_convert_formats(): # Assume the format can be converted directly to the formats supported by these classes: # AFormat, BFormat # Redefine this `direct_convert_formats` for a concrete format class return [] @classmethod def read(cls, filename: str) -> dict: """Read the data from the file with the `filename` to a dictionary. The dictionary will be used by its corresponding data class.""" number = float(np.loadtxt(filename)) data_dict = {"number": number} return data_dict @classmethod def write(cls, object: NumberData, filename: str, key: str = None): """Save the data with the `filename`.""" data_dict = object.get_data() arr = np.array([data_dict["number"]]) np.savetxt(filename, arr, fmt="%.3f") if key is None: original_key = object.key key = original_key + "_to_TXTFormat" return object.from_file(filename, cls, key) ``` libpyvinyl-1.2.0/doc/source/include/userguide/userguide_instruments.md000066400000000000000000000110421456037232700263770ustar00rootroot00000000000000## Instrument libpyvinyl provides the `Instrument` class as a high level API to manage the full simulation of an entire instrument at a neutron or x-ray facility from the source, through the beamline propagation, the interaction with the sample under investigation and the detection from the detectors. The `Instrument` class is a convenience class, collecting in a sequence one or more calculators and their parameters in order to have an effective I/O chain from one calculator to the following till the end of the simulation when the data can be retrieved and saved. ### Type of parameters for an instrument A distinction between two type of parameters have been introduced in this library. 1. **Parameter** (`Parameter` class): more details about it can be found in [Parameter](parameter.md). It essentially represent any kind of parameter that is needed by a calculator. Parameters belonging to a single calculator are collected in the CalculatorParameters as a member of the calculator. 2. **MasterParameter** (`MasterParameter` class): each instrument should have a collection of master paramaters. They have two purposes: - single calculator parameters that are meant to be changed at runtime by the user and not simply for the internal functioning of the calculator. Such calculator parameters are declared as master parameters in order to let the user running the simulation to know which are the parameters that he is supposed to change without altering the functioning of the instrument. E.g. the angles of a three axis spectrometer or collimation - parameters from multiple calculators that are supposed to have the same value and meaning: the wavelenght might be used by the calculator of the source and also by the calculator of the beamline to change some optics One significant advantage in defining and using **master parameters** is that the units used by the calculator and those for the final user might be decoupled. Each calculator will have its own parameter with some units. The `pint` quantities used for physical values allow unit conversions in a transparent way for the end user if the values are assigned as a pint quantity. The master parameter can then be defined in the unit that is most convenient for the user for setting and getting values. ### How to write an instrument Firstly an instrument object has to be constructed with a name and the base path where the output of the calculators forming the instrument should go. Example: ``` myinstr = Instrument("D22_quick", instrument_base_dir=".") ``` Optionally the list of calculators can be added afterwards or given at construction as a list of calculators. Calculators should be added one by one with the `add_calculator` method providing an already instantiated calculator with its parameters set. Example: ``` calculator_1 = SpecializedCalculator(name="calc1") calculator_1.add_parameter("par1",unit="meV") myinstr.add_calculator(calculator_1) ``` Adding the calculator, all the parameters are automatically handled by the Instrument and accessed via the `myinstrument.parameters` method. Now that an instrument is fully described, it highly recommended to define the master parameters with the `add_master_parameter` method. One master parameter is defined by conveniet name and a dictionary associating the name of the calculator and the name of the parameter for the specific calculator. It is worth noting that the parameters with the same meaning my be defined with different names by different calculators. Once a master parameter is added, the user running the simulation for the instrument will not have to care about the details of the calculators forming the instrument. Example: ``` myinstr.add_master_parameter("wavelength", {"calc_1": "par_1"}, unit="GeV") myinstr.add_master_parameter("collimation", {"calc_1": "par_2"}, unit="meter") myinstr.master["wavelength"] = 4.5 * ureg.keV myinstr.master["collimation"] = 2 * ureg.meter ``` **The user running the simulation will access only to master parameters.** ### How to use an instrument The user of the instrument is supposed to have an instantiated instrument object. The user can get the list of parameters that he can modify by doing: `mymasters = myinstrument.master` where `mymasters` is of type MasterParameters (a collection of master parameters). It is possible to loop over it to access the single `MasterParameter`s or print them: `print(mymasters)`. The list of calculators can be queried and printed on screen with the `list_calculators` methd. The list of parameters can be queried and printed on screen via the `list_parameters` method. libpyvinyl-1.2.0/doc/source/include/userguide/userguide_intro.md000066400000000000000000000005111456037232700251360ustar00rootroot00000000000000## Introduction This detailed user guide will provide detailed explaination to the main components of libpyvinyl: - Calculators - BaseCalculator - CalculatorParameters - DataAPI - DataCollection - BaseData - BaseFormat - Instruments - Instrument - InstrumentParameters ![](../libpyvinyl.drawio.svg)libpyvinyl-1.2.0/doc/source/include/userguide/userguide_parameters.md000066400000000000000000000114321456037232700261520ustar00rootroot00000000000000 ### Parameter The Parameter class describes a single parameter intended to be an input for a calculator. A parameter is initialised by the calculator and subsequently made available to the user. A parameter can have a physical unit and limits on the allowed value in order to avoid unmeaningful input. It is considered good practice to add a comment to each parameter to briefly explain its purpose. #### Basic use Here basic use of the Parameter class is shown with a brief example. ``` energy_parameter = Parameter(name="energy", unit="meV", comment="Energy of emitted particles") ``` There are a number of useful things one can do with a parameter object, for example print it to see what is currently contained. To get or set the value, the *value* attribute is used directly and all checks are performed internally. ``` energy_parameter.value = 5.0 ``` We defined the default unit to be meV, but since the object is aware of the unit, it is possible to provide in another energy unit using Pint. ``` import pint ureg = pint.UnitRegistry() energy_parameter.value = 5.0*ureg.eV ``` Now the *energy_parameter* has a value of 5000.0 meV. #### Limits Many parameters have natural limits that would appear natural to the person writing a calculator, but perhaps not to a user. To transfer this knowledge, the developer can include limits in the parameter to cause an error if the value is set outside. Limits come in to forms, either as intervals or as options. ##### Intervals The first type of limit to discuss is the interval, which can be between two finite numbers or extend to infinity in either end. Multiple intervals can be specified. It is possible to declare the intervals as legal or illegal, but all added intervals have to be of the same type, so it is not possible to mix legal and illegal intervals. Below a legal interval is added to the energy parameter. ``` energy_parameter.add_interval(min_value=0.0, max_value=7000.0, intervals_are_legal=True) ``` In order to extend an interval to infinity, one provides None for either *min_value* or *max_value*. ``` energy_parameter.add_interval(min_value=8000.0, max_value=None, intervals_are_legal=True) ``` Now the energy_parameter is legal from 0 to 7000 meV and from 8000 meV to infinity. ##### Options It is also possible to define whether single values are legal or not, this is called an option, and has the same rules regarding legal / illegal as an interval, meaning all options have to be either legal or illegal. Lets make it illegal to have an energy of 0 meV. ``` energy_parameter.add_option(0, options_are_legal=False) ``` It is possible to add several options with one call using a list. We could make a few arbitrary values illegal in this way: ``` energy_parameter.add_option([5, 10], options_are_legal=False) ``` Now values of 0 meV, 5 meV and 10 meV would cause an error, even when they are all contained in a legal interval, as both the intervals and options are checked whenever a value is set. ##### Obtaining limits When printing a parameter the limits will be available in human readable format, but it is also possible to obtain them with these methods for use in for example a GUI application that want to provide a slider or dropdown menu describing the parameter. ``` energy_parameter.get_intervals() # Returns list of tuples with min / max energy_parameter.get_intervals_are_legal() # Returns True or False energy_parameter.get_options() # Returns list of values energy_parameter.get_options_are_legal() # Returns True or False ``` ##### Clearing limits It is possible to clear limits, with the *clear_intervals* and *clear_options* methods, yet doing so is not recommended, especially if the purpose is to run a simulation with a parameter value outside of what is assumed by the calculated. #### CalculatorParameters The CalculatorParameters class is a container for holding all the Parameter objects that pertain to a single Calculator. Each Calculator can have just a single container to organise all the parameters, and it provides the expected container features for convenience. A CalculatorParameters object can be made without any Parameters, but let us create a CalculatorParameters object using our existing Parameter. ``` my_parameters = CalculatorParameters(parameters=[energy_parameter]) ``` More parameters can be added using the *add* method. ``` my_parameters.add(Parameter("energy_spread", unit="meV", comment="Standard deviation of particle energies")) ``` The main purpose of the CalculatorParameters object is convenience, so it is possible to access the elements directly. ``` my_parameters["energy"].value = 7.0 my_parameters["energy_spread"].value = 0.8 ``` It is also possible to perform a loop over the contained parameters, here we check if intervals are legal on the contained parameters. ``` for par in my_parameters: print(par.get_intervals_are_legal()) ``` libpyvinyl-1.2.0/doc/source/index.rst000066400000000000000000000011751456037232700176360ustar00rootroot00000000000000.. libpyvinyl documentation master file, created by sphinx-quickstart on Wed Apr 22 05:49:04 2020. You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. Welcome to libpyvinyl's documentation! ====================================== .. toctree:: :maxdepth: 2 :caption: Contents: Introduction Installation Quickstart include/UserGuide Contributing include/refman Indices and tables ================== * :ref:`genindex` * :ref:`modindex` * :ref:`search` libpyvinyl-1.2.0/libpyvinyl.drawio.svg000066400000000000000000001421271456037232700201410ustar00rootroot00000000000000 BaseCalculator+ name: str+ parameters: CalculatorParameters+ input: DataCollection, list or BaseData+ output_keys: list or str+ output_data_types: list or BaseData+ output_filenames: list, str or None+ instrument_base_dir: str+ calculator_base_dir: str+ init_parameters()+ backengine(): DataCollection+ set_parameters(args_as_dict: bool)+ dump(fname: str): str+ from_dump(dumpfile: str): BaseCalculatorCalculatorParameters+ parameters: dict+ check_type(parameter: Parameter)+ check_list_type(parameter_list: list)+ add(parameter: Parameter)+ new_parameter(*args, **kwargs)+ print_indented(indents: int)+ from_json(fname: str)+ to_json(fname: str)+ from_dict(params_dict: dict)+ to_dict()DataCollection+ data_object_diict: dict+ get_data_object(key): dict+ add_data(*args)+ get_data(): dict+ write(filename: str or dict, format_class:  str or FormatClass, key: str or dict):DataClass or list+ to_list(): list
I/O
I/O
BaseData+ key: str+ expected_data: dict+ data_dict: dict+ filename: str+ file_format_class: BaseFormat+ file_format_kwargs: dict+ mapping_type: type+ set_dict(data_dict: dict)+ set_file(filename: str, format_class: BaseFormat)+ get_data(): dict
+ write(filename: str or dict, format_class: BaseFormat, key: str): BaseData
+ write(filename: str or dict, format_class: BaseFor...
+ supported_formats(): dict+ list_formats()- __check_consistency()
- _add_ioformat(format_dict: dict, format_class: BaseFormat)
- _add_ioformat(format_dict: dict, format_class: Bas...
Calls
Calls

<<Interface>>
BaseFormat


None


+ format_register(): 
+ read(filename: str): dict

+ write(object: BaseData, filename: str, key: str)

+ direct_convert_formats()

+ convert(obj: BaseData, output: str, output_format_class: str, key: str)

<<Interface>>...
Instrument+ name: str+ parameters: InstrumentParameters+ calculators: dict+ master: MasterParameters+ set_instrument_base_dir(base: str)+ list_calculators()+ list_parameters()+ add_master_parameter(name: str, links: dict)+ add_calculator(calculator: BaseCalculator)+ remove_calculator(calculator_name: str)+ run()InstrumentParameters+ parameters_dic: dict+ master: MasterParameters+ from_json(fname: str): InstrumentParameters+ from_dict(instrument_dict: list): InstrumentParameters+ to_dict(): dict+ to_json(fname: str)+ add(key: str, parameters: CalculatorParameters)+ add_master_parameter(name: str, links: dict)+ to_json(fname: str)+ from_dict(params_dict: dict)+ to_dict()
Aggregated
Aggregated
MasterParameters
MasterParameters
Aggregated
Aggregated
Aggregated
Aggregated
Inherited
Inherited
Parameter+ name: str+ unit: str+ comment: str or None+ value: pint.Quantity or else + pint_value: pint.Quantity+ value_no_conversion: Any+ add_interval(min_value, max_value, intervals_are legal: bool)+ add_option(option: Any, options_are_legal)+ print_parameter_constraints()
Aggregated
Aggregated
MasterParameterNone+ add_links(links: dict)
Inherited
Inherited
Aggregated
Aggregated
Text is not SVG - cannot display
libpyvinyl-1.2.0/libpyvinyl/000077500000000000000000000000001456037232700161255ustar00rootroot00000000000000libpyvinyl-1.2.0/libpyvinyl/AbstractBaseClass.py000066400000000000000000000037601456037232700220310ustar00rootroot00000000000000""" :module AbstractBaseClass: Module hosting the AbstractBaseClass and Parameters abstract classes. """ #################################################################################### # # # This file is part of libpyvinyl - The APIs for Virtual Neutron and x-raY # # Laboratory. # # # # Copyright (C) 2020 Carsten Fortmann-Grote # # # # This program is free software: you can redistribute it and/or modify it under # # the terms of the GNU Lesser General Public License as published by the Free # # Software Foundation, either version 3 of the License, or (at your option) any # # later version. # # # # This program is distributed in the hope that it will be useful, but WITHOUT ANY # # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A # # PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. # # # # You should have received a copy of the GNU Lesser General Public License along # # with this program. If not, see = 3.11 # from typing import Self logging.basicConfig( format="%(asctime)s %(levelname)s:%(message)s", level=logging.WARNING ) class BaseCalculator(AbstractBaseClass): """ Base class of all calculators. This class provides the libpyvinyl API. It defines all methods through which a user interacts with the simulation backengines. This class is to be used as a base class for all calculators that implement a special simulation module, such as a photon diffraction calculator. Such a specialized Calculator has the same interface to the simulation backengine as all other ViNYL Calculators. A complete example including a instrument and calculators can be found in ``test/integration/plusminus`` """ def __init__( self, name: str, input: Union[DataCollection, List[BaseData], BaseData], output_keys: Union[list, str], output_data_types: Union[list, BaseData], output_filenames: Union[list, str, None] = None, instrument_base_dir: str = "./", calculator_base_dir: Optional[str] = None, parameters: CalculatorParameters = None, ): """ Constructs this class. :param name: The name of this calculator. :param input: The input of this calculator. It can be a `DataCollection`, a list of `BaseData`s or a single Data Object. :param output_keys: The key(s) of this calculator's output data. :param output_data_types: The data type(s), i.e., classes, of each output. It's a list of the data classes or a single data class. The available data classes are based on `BaseData`. :param output_filenames: The name(s) of the output file(s). It can be a str of a filename or a list of filenames. If the mapping is dict mapping, the name is `None`. Defaults to None. :param instrument_base_dir: The base directory for the instrument to which this calculator belongs. The final exact output file path depends on `instrument_base_dir` and `calculator_base_dir`: `instrument_base_dir`/`calculator_base_dir`/filename :param calculator_base_dir: The base directory for this calculator. The final exact output file path depends on `instrument_base_dir` and `calculator_base_dir`: `instrument_base_dir`/`calculator_base_dir`/filename :param parameters: The parameters for this calculator. """ # Initialize the variables self.__name = None self.__instrument_base_dir = None self.__calculator_base_dir = None self.__input = None self.__output_keys = None self.__output_data_types = None self.__output_filenames = None self.__parameters = None self.__output: DataCollection = DataCollection() self.name = name self.input = input self.output_keys = output_keys self.output_data_types = output_data_types self.output_filenames = output_filenames self.instrument_base_dir = instrument_base_dir if calculator_base_dir is None: self.calculator_base_dir = name else: self.calculator_base_dir = calculator_base_dir self.parameters = parameters self.__check_consistency() # Create output data objects according to the output_data_classes self.__init_output() def __check_consistency(self): """Check the consistency of the input parameters""" if len(self.output_keys) != len(self.output_data_types): raise ValueError( f"len(output_keys) = {len(self.output_keys)} is not equal to len(output_data_types) = {len(self.output_data_types)}" ) def __check_output_filenames(self): """Since output_filenames can be None for output in dict mapping, only check output_files when necessary""" if len(self.output_data_types) != len(self.output_filenames): raise ValueError( f"len(output_filenames) = {len(self.output_filenames)} is not equal to len(output_data_types) = {len(self.output_data_types)}" ) @property def name(self) -> str: """The name of this calculator.""" return self.__name @name.setter def name(self, value): if isinstance(value, str): self.__name = value else: raise TypeError( f"Calculator: `name` is expected to be a str, not {type(value)}" ) @property def parameters(self) -> CalculatorParameters: """The parameters of this calculator.""" return self.__parameters @parameters.setter def parameters(self, value: CalculatorParameters): self.reset_parameters(value) def reset_parameters(self, value: CalculatorParameters): """Resets the calculator parameters""" if isinstance(value, CalculatorParameters): self.__parameters = value elif value is None: self.init_parameters() else: raise TypeError( f"Calculator: `parameters` is expected to be CalculatorParameters, not {type(value)}" ) def set_parameters(self, args_as_dict: bool = None, **kwargs): """ Sets parameters contained in this calculator using dict or kwargs """ if args_as_dict is not None: parameter_dict = args_as_dict else: parameter_dict = kwargs for key, parameter_value in parameter_dict.items(): self.parameters[key].value = parameter_value @property def instrument_base_dir(self) -> str: """The base directory for the instrument to which this calculator belongs.""" return self.__instrument_base_dir @instrument_base_dir.setter def instrument_base_dir(self, value): self.set_instrument_base_dir(value) def set_instrument_base_dir(self, value: str): """Set the instrument base directory""" if isinstance(value, str): self.__instrument_base_dir = value else: raise TypeError( f"Calculator: `instrument_base_dir` is expected to be a str, not {type(value)}" ) @property def calculator_base_dir(self) -> str: """The base directory for this calculator. The final exact output file path depends on `instrument_base_dir` and `calculator_base_dir`: `instrument_base_dir`/`calculator_base_dir`/filename """ return self.__calculator_base_dir @calculator_base_dir.setter def calculator_base_dir(self, value): self.set_calculator_base_dir(value) def set_calculator_base_dir(self, value: str): """Set the calculator base directory""" if isinstance(value, str): self.__calculator_base_dir = value else: raise TypeError( f"Calculator: `calculator_base_dir` is expected to be a str, not {type(value)}" ) @property def input(self) -> DataCollection: """The input of this calculator. A collection or a single Data Object(s).""" return self.__input @input.setter def input(self, value): self.set_input(value) def set_input(self, value: Union[DataCollection, list, BaseData, None]): """Set the calculator input data. It can be a DataCollection, list or BaseData object.""" if isinstance(value, (DataCollection, type(None))): self.__input = value elif isinstance(value, list): self.__input = DataCollection(*value) elif isinstance(value, BaseData): self.__input = DataCollection(value) else: raise TypeError( f"Calculator: `input` can be a DataCollection, list or BaseData object, and will be treated as a DataCollection. Your input type: {type(value)} is not accepted." ) @property def output_keys(self) -> list: """The key(s) of this calculator's output data.""" return self.__output_keys @output_keys.setter def output_keys(self, value): self.set_output_keys(value) @property def base_dir(self): """The base path for the output files of this calculator in consideration of instrument_base_dir and calculator_base_dir""" base_dir = Path(self.instrument_base_dir) / self.calculator_base_dir return str(base_dir) @property def output_file_paths(self): """The final output file paths considering base_dir""" self.__check_output_filenames() paths = [] for filename in self.output_filenames: path = Path(self.base_dir) / filename # Make sure the file directory exists path.parent.mkdir(parents=True, exist_ok=True) paths.append(str(path)) return paths def set_output_keys(self, value: Union[list, str]): """Set the calculator output keys. It can be a list of str or a single str.""" if isinstance(value, list): for item in value: assert type(item) is str self.__output_keys = value elif isinstance(value, str): self.__output_keys = [value] else: raise TypeError( f"Calculator: `output_keys` can be a list or str, and will be treated as a list. Your input type: {type(value)} is not accepted." ) @property def output_data_types(self) -> list: """The data type(s), i.e., classes, of each output.""" return self.__output_data_types @output_data_types.setter def output_data_types(self, value): self.set_output_data_types(value) def set_output_data_types(self, value: Union[list, BaseData]): """Set the calculator output data type. It can be a list of DataClass or a single DataClass.""" if isinstance(value, list): for item in value: assert issubclass(item, BaseData) self.__output_data_types = value elif issubclass(value, BaseData): self.__output_data_types = [value] else: raise TypeError( f"Calculator: `output_data_types` can be a list or a subclass of BaseData, and will be treated as a list. Your input type: {type(value)} is not accepted." ) @property def output_filenames(self) -> list: """The name(s) of the output file(s). It can be a str of a filename or a list of filenames. If the mapping is dict mapping, the name is `None`.""" return self.__output_filenames @output_filenames.setter def output_filenames(self, value): self.set_output_filenames(value) def set_output_filenames(self, value: Union[list, str, None]): """Set the calculator output filenames. It can be a list of filenames or just a single str.""" if isinstance(value, list): for item in value: assert type(item) is str or type(None) self.__output_filenames = value elif isinstance(value, (str, type(None))): self.__output_filenames = [value] else: raise TypeError( f"Calculator: `output_filenames` can be a list or just a str or None, and will be treated as a list. Your input type: {type(value)} is not accepted." ) @property def output(self): """The output of this calculator""" return self.__output @property def data(self): """The alias of output. It's not recommended to use this variable name due to it's ambiguity.""" return self.__output @abstractmethod def init_parameters(self): """Virtual method to initialize all parameters. Must be implemented on the specialized class.""" raise NotImplementedError def __init_output(self): """Create output data objects according to the output_data_types""" output = DataCollection() for i, key in enumerate(self.output_keys): output_data = self.output_data_types[i](key) output.add_data(output_data) self.__output = output def __call__(self, parameters=None, **kwargs): """The copy constructor :param parameters: The parameters for the new calculator. :type parameters: CalculatorParameters :param kwargs: key-value pairs of parameters to change in the new instance. :return: A new parameters instance with optionally changed parameters. """ new = copy.deepcopy(self) new.__dict__.update(kwargs) if parameters is None: new.parameters = copy.deepcopy(new.parameters) else: new.parameters = parameters return new @classmethod def from_dump(cls, dumpfile: str): """Load a dill dump from a dumpfile. :param dumpfile: The file name of the dumpfile. :return: The calculator object restored from the dumpfile. """ with open(dumpfile, "rb") as fhandle: try: tmp = dill.load(fhandle) except: raise IOError("Cannot load calculator from {}.".format(dumpfile)) if not isinstance(tmp, cls): raise TypeError(f"The object in the file {dumpfile} is not a {cls}") return tmp def dump(self, fname: Optional[str] = None) -> str: """ Dump class instance to file. :param fname: Filename (path) of the file to write. :return: The filename of the dumpfile """ if fname is None: _, fname = mkstemp( suffix="_dump.dill", prefix=self.__class__.__name__[-1], dir=os.getcwd(), ) with open(fname, "wb") as file_handle: dill.dump(self, file_handle) return fname @abstractmethod def backengine(self): """Execute the intended operation of this class.""" raise NotImplementedError # This project has received funding from the European Union's Horizon 2020 research and innovation programme under grant agreement No. 823852. libpyvinyl-1.2.0/libpyvinyl/BaseData.py000066400000000000000000000454051456037232700201530ustar00rootroot00000000000000""" :module BaseData: Module hosts the BaseData class.""" from typing import Union, Optional from abc import abstractmethod, ABCMeta from libpyvinyl.AbstractBaseClass import AbstractBaseClass class BaseData(AbstractBaseClass): """The abstract data class. Inheriting classes represent simulation input and/or output data and provide a harmonized user interface to simulation data of various kinds rather than a data format. Their purpose is to provide a harmonized user interface to common data operations such as reading/writing from/to disk. """ def __init__( self, key: str, expected_data: dict, data_dict: Optional[dict] = None, filename: Optional[str] = None, file_format_class=None, file_format_kwargs: Optional[dict] = None, ): """ :param key: The key to identify the Data Object. :param expected_data: A placeholder dict for expected data. The keys of this dict are expected to be found during the execution of `get_data()`. The value for each key can be `None`. :param data_dict: The dict to map by this DataClass. It has to be `None` if a file mapping was already set, defaults to None. :param filename: The filename of the file to map by this DataClass. It has to be `None` if a dict mapping was already set, defaults to None. :param file_format_class: The FormatClass to map the file by this DataClass, It has to be `None` if a dict mapping was already set, defaults to None :type file_format_class: class, optional :param file_format_kwargs: The kwargs needed to map the file, defaults to None. """ self.__key = None self.__expected_data = None self.__data_dict = None self.__filename: Optional[str] = None self.__file_format_class = None self.__file_format_kwargs = None self.key = key # Expected_data is checked when `self.get_data()` self.expected_data = expected_data # This will be always be None if the data class is mapped to a file self.data_dict = data_dict # These will be always be None if the data class is mapped to a python data dict object self.filename = filename self.file_format_class = file_format_class self.file_format_kwargs = file_format_kwargs self.__check_consistency() @property def key(self) -> str: """The key of the class instance for calculator usage""" return self.__key @key.setter def key(self, value: str): if isinstance(value, str): self.__key = value else: raise TypeError(f"Data Class: key should be a str, not {type(value)}") @property def expected_data(self): """The expected_data of the class instance for calculator usage""" return self.__expected_data @expected_data.setter def expected_data(self, value): if isinstance(value, dict): self.__expected_data = value else: raise TypeError( f"Data Class: expected_data should be a dict, not {type(value)}" ) @property def data_dict(self): """The data_dict of the class instance for calculator usage""" return self.__data_dict @data_dict.setter def data_dict(self, value): if isinstance(value, dict): self.__data_dict = value elif value is None: self.__data_dict = None else: raise TypeError( f"Data Class: data_dict should be None or a dict, not {type(value)}" ) self.__check_consistency() def set_dict(self, data_dict: dict): """Set a mapping dict for this DataClass. :param data_dict: The data dict to map :type data_dict: dict """ self.data_dict = data_dict def set_file(self, filename: str, format_class, **kwargs): """Set a mapping file for this DataClass. :param filename: The filename of the file to map. :param format_class: The FormatClass to map the file :type format_class: class """ self.filename = filename self.file_format_class = format_class self.file_format_kwargs = kwargs self.__check_consistency() @property def filename(self) -> Optional[str]: """The filename of the file to map by this DataClass.""" return self.__filename @filename.setter def filename(self, value: Optional[str]): if isinstance(value, str): self.__filename = value elif value is None: self.__filename = None else: raise TypeError( f"Data Class: filename should be None or a str, not {type(value)}" ) @property def file_format_class(self): """The FormatClass to map the file by this DataClass""" return self.__file_format_class @file_format_class.setter def file_format_class(self, value): if isinstance(value, ABCMeta): self.__file_format_class = value elif value is None: self.__file_format_class = None else: raise TypeError( f"Data Class: format_class should be None or a format class, not {type(value)}" ) @property def file_format_kwargs(self): """The kwargs needed to map the file""" return self.__file_format_kwargs @file_format_kwargs.setter def file_format_kwargs(self, value): if isinstance(value, dict): self.__file_format_kwargs = value elif value is None: self.__file_format_kwargs = None else: raise TypeError( f"Data Class: file_format_kwargs should be None or a dict, not {type(value)}" ) @property def mapping_type(self): """If this data class is a file mapping or python dict mapping.""" return self.__check_mapping_type() def __check_mapping_type(self): """Check the mapping_type of this class.""" if self.data_dict is not None: return dict elif self.filename is not None: return self.file_format_class else: raise TypeError("Neither self.__data_dict or self.__filename was found.") @property def mapping_content(self): """Returns an overview of the keys of the mapped dict or the filename of the mapped file""" if self.mapping_type == dict: return self.data_dict.keys() else: return self.filename @staticmethod def _add_ioformat(format_dict, format_class): """Register an ioformat to a `format_dict` listing the formats supported by this DataClass. :param format_dict: The dict listing the supported formats. :type format_dict: dict :param format_class: The FormatClass to add. :type format_class: class """ register = format_class.format_register() for key, val in register.items(): if key == "key": this_format = val format_dict[val] = {} else: format_dict[this_format][key] = val @classmethod @abstractmethod def supported_formats(self): format_dict = {} # Add the supported format classes when creating a concrete class. # See the example at `tests/BaseDataTest.py` self._add_ioformat(format_dict, FormatClass) return format_dict @classmethod def list_formats(self): """Print supported formats""" out_string = "" supported_formats = self.supported_formats() for key in supported_formats: dicts = supported_formats[key] format_class = dicts["format_class"] if format_class: out_string += "Format class: {}\n".format(format_class) out_string += "Key: {}\n".format(key) out_string += "Description: {}\n".format(dicts["description"]) ext = dicts["ext"] if ext != "": out_string += "File extension: {}\n".format(ext) kwargs = dicts["read_kwargs"] if kwargs != [""]: out_string += "Extra reading keywords: {}\n".format(kwargs) kwargs = dicts["write_kwargs"] if kwargs != [""]: out_string += "Extra writing keywords: {}\n".format(kwargs) out_string += "\n" print(out_string) def __check_consistency(self): # If all of the file-related parameters are set: if all([self.filename, self.file_format_class]): # If the data_dict is also set: if self.data_dict is not None: raise RuntimeError( "self.data_dict and self.filename can not be set for one data class at the same time." ) else: pass # If any one of the file-related parameters is None: elif ( self.filename is None and self.file_format_class is None and self.file_format_kwargs is None ): pass # If some of the file-related parameters is None and some is not None: else: raise RuntimeError( "self.filename, self.file_format_class, self.file_format_kwargs are not consistent." ) @classmethod def from_file(cls, filename: str, format_class, key: dict, **kwargs): """Create a Data Object mapping a file. :param filename: The filename of the file to map by this DataClass. It has to be `None` if a dict mapping was already set, defaults to None. :type filename: str, optional :param file_format_class: The FormatClass to map the file by this DataClass, It has to be `None` if a dict mapping was already set, defaults to None :type file_format_class: class, optional :param file_format_kwargs: The kwargs needed to map the file, defaults to None. :type file_format_kwargs: dict, optional :param key: The key to identify the Data Object. :type key: str :return: A Data Object :rtype: BaseData """ return cls( key, filename=filename, file_format_class=format_class, file_format_kwargs=kwargs, ) @classmethod def from_dict(cls, data_dict: dict, key: str): """Create a Data Object mapping a data dict. :param data_dict: The dict to map by this DataClass. It has to be `None` if a file mapping was already set, defaults to None. :type data_dict: dict :param key: The key to identify the Data Object. :type key: str :return: A Data Object :rtype: BaseData """ return cls(key, data_dict=data_dict) def write(self, filename: str, format_class, key: str = None, **kwargs): """Write the data mapped by the Data Object into a file and return a Data Object mapping the file. It converts either a file or a python object to a file The behavior related to a file will always be handled by the format class. If it's a python dictionary mapping, write with the specified format_class directly. :param filename: The filename of the file to be written. :type filename: str :param file_format_class: The FormatClass to write the file. :type file_format_class: class :param key: The identification key of the new Data Object. When it's `None`, a new key will be generated with a suffix added to the previous identification key by the FormatClass. Defaults to None. :type key: str, optional :return: A Data Object :rtype: BaseData """ if self.mapping_type == dict: return format_class.write(self, filename, key, **kwargs) elif format_class in self.file_format_class.direct_convert_formats(): return self.file_format_class.convert( self, filename, format_class, key, **kwargs ) # If it's a file mapping and would like to write in the same file format of the # mapping, it will let the user know that a file containing the data in the same format already existed. elif format_class == self.file_format_class: print( f"Hint: This data already existed in the file {self.__filename} in format {self.__file_format_class}. `cp {self.__filename} {filename}` could be faster." ) print( f"Will still write the data into the file {filename} in format {format_class}" ) return format_class.write(self, filename, key, **kwargs) else: return format_class.write(self, filename, key, **kwargs) def __check_for_expected_data(self, data_to_read): """Check if the `data_to_read` contains the data we have""" for key in self.expected_data.keys(): try: data_to_read[key] except KeyError: raise KeyError( f"Expected data dict key '{key}' is not found." ) from None def __get_dict_data(self): """Get the data dict from a dict mapping""" if self.__data_dict is not None: # It will automatically check the data needed to be extracted. self.__check_for_expected_data(self.__data_dict) return self.data_dict else: raise RuntimeError( "__get_dict_data() should not be called when self.__data_dict is None" ) def __get_file_data(self, **kwargs): """Get the data dict from a file mapping""" if self.__filename is not None: data_to_read = self.__file_format_class.read( self.__filename, **self.__file_format_kwargs, **kwargs ) # It will automatically check the data needed to be extracted. self.__check_for_expected_data(data_to_read) data_to_return = {} for key in data_to_read.keys(): data_to_return[key] = data_to_read[key] return data_to_return else: raise RuntimeError( "__get_file_data() should not be called when self.__filename is None" ) def get_data(self, **kwargs): """Return the data in a dictionary""" # From either a file or a python object to a python object if self.__data_dict is not None: return self.__get_dict_data() elif self.__filename is not None: return self.__get_file_data(**kwargs) else: raise RuntimeError("Cannot read the data from either a dict or a file.") def __str__(self): """Returns strings of Data objects info""" string = f"key = {self.key}\n" string += f"mapping = {self.mapping_type}: {self.mapping_content}" return string # DataCollection class class DataCollection: """A collection of Data Objects""" def __init__(self, *args): self.data_object_dict = {} self.add_data(*args) def __len__(self): return len(self.data_object_dict) def __setitem__(self, key, value): if key != value.key: print( f"Warning: the key '{key}' of this DataCollection will be replaced by the key '{value.key}' set in the input data." ) del self.data_object_dict[key] self.add_data(value) def __getitem__(self, keys): if isinstance(keys, str): return self.get_data_object(keys) elif isinstance(keys, list): subset = [] for key in keys: subset.append(self.get_data_object(key)) return DataCollection(*subset) def add_data(self, *args): """Add data objects to the data colletion""" for data in args: assert isinstance(data, BaseData) self.data_object_dict[data.key] = data def get_data(self): """Get the data of the data object(s). When there is only one item in the DataCollection, it returns the data dict, When there are more then one items, it returns a dictionary of the data dicts""" if len(self.data_object_dict) == 1: return next(iter(self.data_object_dict.values())).get_data() else: data_dicts = {} for key, obj in self.data_object_dict.items(): data_dicts[key] = obj.get_data() return data_dicts def write( self, filename: Union[str, dict], format_class, key: Union[str, dict] = None, **kwargs, ): """Write the data object(s) to the file(s). When there is only one item in the DataCollection, it returns the data object mapping the file which was wirttern, When there are more then one items, it returns a dictionary of the data objects. :param filename: The name(s) of the file(s) to write. When there are multiple items, they are expected in a dict where the keys corresponding to the data in this collection. :type filename: str or dict :param format_class: The format class of the file(s). When there are multiple items, they are expected in a dict where the keys corresponding to the data in this collection. :type format_class: class or dict :param key: The key(s) of the data object(s) mapping the written file(s), defaults to None. :type key: str or dict, optional :return: A data object or a dict of data objects. :rtype: DataClass or dict """ if len(self.data_object_dict) == 1: obj = next(iter(self.data_object_dict.values())) return obj.write(filename, format_class, key, **kwargs) else: assert isinstance(key, dict) data_dicts = {} for col_key, obj in self.data_object_dict.items(): written_data = obj.write( filename[col_key], format_class[col_key], key[col_key], **kwargs ) data_dicts[written_data.key] = written_data return data_dicts def get_data_object(self, key: str): """Get one data object by its key :param key: The key of the data object to get. :type key: str :return: A data object :rtype: DataClass """ return self.data_object_dict[key] def to_list(self): """Return a list of the data objects in the data collection""" return [value for value in self.data_object_dict.values()] def __str__(self): """Returns strings of the data object info""" string = "Data collection:\n" string += "key - mapping\n\n" for data_object in self.data_object_dict.values(): string += f"{data_object.key} - {data_object.mapping_type}: {data_object.mapping_content}\n" return string libpyvinyl-1.2.0/libpyvinyl/BaseFormat.py000066400000000000000000000076441456037232700205350ustar00rootroot00000000000000from abc import abstractmethod from libpyvinyl.AbstractBaseClass import AbstractBaseClass from libpyvinyl.BaseData import BaseData class BaseFormat(AbstractBaseClass): """ The abstract format class which serves as the common interface for derived format classes. """ def __init__(self): # Nothing needs to be done here. pass @classmethod @abstractmethod def format_register(self): # Override this `format_register` method in a concrete format class. key = "Base" desciption = "Base data format" file_extension = "base" read_kwargs = [""] write_kwargs = [""] return self._create_format_register( key, desciption, file_extension, read_kwargs, write_kwargs ) @classmethod def _create_format_register( cls, key: str, desciption: str, file_extension: str, read_kwargs=[""], write_kwargs=[""], ): format_register = { "key": key, # FORMAT KEY "description": desciption, # FORMAT DESCRIPTION "ext": file_extension, # FORMAT EXTENSION "format_class": cls, # CLASS NAME OF THE FORMAT "read_kwargs": read_kwargs, # KEYWORDS LIST NEEDED TO READ "write_kwargs": write_kwargs, # KEYWORDS LIST NEEDED TO WRITE } return format_register @classmethod @abstractmethod def read(self, filename: str, **kwargs) -> dict: """Read the data from the file with the `filename` to a dictionary. The dictionary will be used by its corresponding data class.""" # Example codes. Override this function in a concrete class. data_dict = {} with h5py.File(filename, "r") as h5: for key, val in h5.items(): data_dict[key] = val[()] return data_dict @classmethod @abstractmethod def write(cls, object: BaseData, filename: str, key: str, **kwargs): """Save the data with the `filename`.""" # Example codes. Override this function in a concrete class. data_dict = object.get_data() arr = np.array([data_dict["number"]]) np.savetxt(filename, arr, fmt="%.3f") if key is None: original_key = object.key key = original_key + "_to_TXTFormat" return object.from_file(filename, cls, key) else: return object.from_file(filename, cls, key) @staticmethod @abstractmethod def direct_convert_formats(): # Assume the format can be converted directly to the formats supported by these classes: # AFormat, BFormat # Override this `direct_convert_formats` in a concrete format class return [Aformat, BFormat] @classmethod @abstractmethod def convert( cls, obj: BaseData, output: str, output_format_class: str, key, **kwargs ): """Direct convert method, if the default converting would be too slow or not suitable for the output_format""" # If there is no direct converting supported: raise NotImplementedError if output_format_class is AFormat: return cls.convert_to_AFormat(obj.filename, output) else: raise TypeError( "Direct converting to format {} is not supported".format( output_format_class ) ) # Set the key of the returned object if key is None: original_key = obj.key key = original_key + "_from_BaseFormat" return obj.from_file(output, output_format_class, key) # Example convert_to_AFormat() # @classmethod # def convert_to_AFormat(cls, input: str, output: str): # """The engine of convert method.""" # print("Directly converting BaseFormat to AFormat") # number = float(np.loadtxt(input)) # with h5py.File(output, "w") as h5: # h5["number"] = number libpyvinyl-1.2.0/libpyvinyl/Instrument.py000066400000000000000000000135751456037232700206620ustar00rootroot00000000000000""" :module Instrument: Module hosting the Instrument class """ from libpyvinyl.Parameters.Collections import InstrumentParameters from libpyvinyl import BaseCalculator from libpyvinyl.BaseData import DataCollection # typing from libpyvinyl.Parameters.Collections import MasterParameters from typing import Union, Any, Tuple, List, Dict, Optional class Instrument: """Class collecting the parameters and calculators representing an entire instrument at a facility""" def __init__( self, name: str, calculators: Optional[Dict[str, BaseCalculator]] = None, instrument_base_dir: str = "./", ): """Instrument object initialization: :param name: The name of this instrument :param calculators: a collection of Calculator objects. """ self.__name: str = "" self.__instrument_base_dir: str = "" self.__parameters = InstrumentParameters() self.name = name self.__calculators: Dict[str, BaseCalculator] = {} if calculators is not None: for calculator in calculators: self.add_calculator(calculator) self.set_instrument_base_dir(instrument_base_dir) def add_master_parameter(self, name: str, links: Dict[str, str], **kwargs) -> None: """ Add a new parameter with the given name as master parameter. The goal is to link parameters in multiple calculators that represent the same quantity and that should be all changed at the same time when any of them is changed. This is obtained creating the link and by changing the value of the newly created master parameter. :param name: name of the master parameter :param links: dictionary with the names of the calculators and calculator parameters that represent the same quantity and hence can be changed all at once modifying the master parameter" """ self.parameters.add_master_parameter(name, links, **kwargs) @property def name(self) -> str: """The name of this instrument.""" return self.__name @name.setter def name(self, value: str) -> None: if isinstance(value, str): self.__name = value else: raise TypeError( f"Instrument: name is expecting a str rather than {type(value)}" ) @property def calculators(self) -> Dict[str, BaseCalculator]: """The list of calculators. It's modified either when constructing the class instance or using the :meth:`~libpyvinyl.Instrument.add_calculator` function. """ return self.__calculators @property def parameters(self) -> InstrumentParameters: """ The parameter collection of each calculator in the instrument. These parameters are links to the exact parameters of each calculator. """ return self.__parameters @property def master(self) -> MasterParameters: """Return the master parameters""" return self.parameters.master @property def instrument_base_dir(self) -> str: return self.__instrument_base_dir @instrument_base_dir.setter def instrument_base_dir(self, value): self.set_instrument_base_dir(value) def set_instrument_base_dir(self, base: str) -> None: """Set each calculator's `instrument_base_dir` to '`base`. Each calculator's data file ouput directory will be "`instrument_base_dir`/`calculator_base_dir`". :param base: The base directory to be set. :type base: str """ if isinstance(base, str): self.__instrument_base_dir = base for calculator in self.calculators.values(): calculator.instrument_base_dir = self.__instrument_base_dir else: raise TypeError( f"Instrument: instrument_base_dir is expecting a str rather than {type(base)}" ) def __repr_calculators(self) -> str: """ Return the list of all defined calculators for this instrument """ string = f"- Instrument: {self.name} -\n" string += "Calculators:\n" for key in self.calculators: string += f"{key}\n" return string def list_calculators(self) -> None: """ Print the list of all defined calculators for this instrument """ string = self.__repr_calculators() print(string) def list_parameters(self) -> None: """ Print the list of all calculator parameters """ print(self.parameters) def add_calculator(self, calculator: BaseCalculator) -> None: """ Append one calculator to the list of calculators. N.B. calculators are executed in the same order as they are provided :param calculator: calculator """ self.__calculators[calculator.name] = calculator self.__parameters.add(calculator.name, calculator.parameters) def remove_calculator(self, calculator_name: str) -> None: """ Remove the calculator with the given name from the list of calculators :param calculator_name: name of one calculator already added to the list """ del self.__calculators[calculator_name] del self.__parameters[calculator_name] def run(self) -> None: """ Run the entire simulation, i.e. all the calculators in the order they have been provided """ for calculator in self.calculators.values(): calculator.backengine() @property def output(self) -> DataCollection: """Return the output of the last calculator""" return list(self.__calculators.values())[-1].output def __str__(self) -> str: mystring = f"######## Instrument {self.name}\n" mystring += self.__repr_calculators() mystring += repr(self.parameters) mystring += f"############" return mystring libpyvinyl-1.2.0/libpyvinyl/Parameters/000077500000000000000000000000001456037232700202305ustar00rootroot00000000000000libpyvinyl-1.2.0/libpyvinyl/Parameters/Collections.py000066400000000000000000000305761456037232700230730ustar00rootroot00000000000000# Created by Mads Bertelsen and modified by Juncheng E import json_tricks as json from collections import OrderedDict import copy from libpyvinyl.AbstractBaseClass import AbstractBaseClass from .Parameter import Parameter from pint.quantity import Quantity from pint.unit import Unit, UnitsContainer from typing import Union def quantity_encode( obj: Union[Quantity, Unit, UnitsContainer, any], primitives: bool = False ): """ Function to encode pint.Quantity and pint.Unit objects in json It returns obj if the encoding was not possible. """ if isinstance(obj, Quantity): return {"__quantity__": str(obj)} elif isinstance(obj, Unit): return str(obj) elif isinstance(obj, UnitsContainer): return "" else: return obj def quantity_decode(dct): """ Function to decode pint.Quantity object from json """ if "__quantity__" in dct: a = dct["__quantity__"] if "inf" in a: return Quantity("inf", a.strip("inf")) else: return Quantity(dct["__quantity__"]) elif "__units__" in dct: return dct["__units__"] else: return dct class CalculatorParameters(AbstractBaseClass): """ Collection of parameters related to a single calculator Parameters are stored in a dict using their name as key """ def __init__(self, parameters=None): """ Creates a Parameters object, optionally with list of parameter objects """ self.parameters = OrderedDict() if parameters is not None: self.add(parameters) def check_type(self, parameter): """ Checks given parameter is of type Parameter """ if not isinstance(parameter, Parameter): raise RuntimeError( "Object of type Parameter expected, received {}".format(type(parameter)) ) def check_list_type(self, parameter_list): """ Checks given list of parameters is a list and contains only parameter objects """ if not isinstance(parameter_list, list): raise RuntimeError("Parameters class takes list as input.") for par in parameter_list: self.check_type(par) def add(self, parameter): """ Adds parameters to this parameters object, either single or list """ # handle case where list of parameters given if isinstance(parameter, list): self.check_list_type(parameter) for par in parameter: if par.name in self.parameters: raise RuntimeError("Duplicate parameter name in parameters!") self.parameters[par.name] = par return # handle case where single parameter is given self.check_type(parameter) if parameter in self.parameters: raise RuntimeError("Duplicate parameter name in parameters!") self.parameters[parameter.name] = parameter def new_parameter(self, *args, **kwargs): """ Creates a new parameter with given arguments and adds to this Parameters object """ new_parameter = Parameter(*args, **kwargs) self.add(new_parameter) return new_parameter def __contains__(self, key): """ Returns True if the parameter exists """ return key in self.parameters def __getitem__(self, key): """ Gets parameter with given name from internal dict """ try: return self.parameters[key] except KeyError: raise KeyError(f"{key} is not a valid parameter name.") def __setitem__(self, key, value): """ Sets value of parameter with given key to given value """ self.parameters[key].value = value def __delitem__(self, key): """ Deletes parameter with given key """ del self.parameters[key] def __iter__(self): """ Facilitates looping through the contained parameters Uses the built in iterator in the return of dict.values() so one can iterate through the parameters with a for loop. """ return self.parameters.values().__iter__() def __next__(self): """ Facilitates looping through the contained parameters Uses the built in next method in the return of dict.values() so one can iterate through the parameters with a for loop. """ return self.parameters.values().__next__() def print_indented(self, indents): """ returns string describing this object, can optionally be indented """ string = indents * " " + " - Parameters object -\n" for key in self.parameters: string += indents * " " + self.parameters[key].print_line() + "\n" return string def __repr__(self): return self.print_indented(0) @classmethod def from_json(cls, fname: str): """ Initialize an instance from a json file. :param fname: The filename (path) of the json file. :type fname: str """ with open(fname, "r") as fp: instance = cls.from_dict( json.load(fp, extra_obj_pairs_hooks=[quantity_decode]), ) return instance @classmethod def from_dict(cls, params_dict: dict): """ Initialize an instance from a dict. :param fname: The filename (path) of the json file. :type fname: str """ parameters = cls() for key in params_dict: parameters.add(Parameter.from_dict(params_dict[key])) return parameters def to_dict(self): params = {} for key in self.parameters: # Deepcopy to not modify the original parameters params[key] = copy.deepcopy(self.parameters[key].__dict__) a = params[key] if "_Parameter__value_type" in a: del a["_Parameter__value_type"] return params def to_json(self, fname: str): """ Save this parameters class to a human readable json file. :param fname: Write to this file. :type fname: str """ with open(fname, "w") as fp: json.dump( self.to_dict(), fp, indent=4, allow_nan=True, extra_obj_encoders=[quantity_encode], ) class MasterParameter(Parameter): """ The master parameters need to affect multiple Parameters objects, and for this reason it is expanded on the basis of the Parameter class. A link system is added that contains information on which Parameters objects this master parameter should control parameters from. """ def __init__(self, *args, **kwargs): """ Create MasterParameter with uninitialized links """ self.links = None super().__init__(*args, **kwargs) def add_links(self, links): """ Links is a dict with key being reference to calculator and name of parameter to overwrite """ self.links = links class MasterParameters(CalculatorParameters): """ Master Parameters object that contain all master parameters with the additional ability to set values for the other parameters this master should control. """ def __init__(self, parameters_dict, *args, **kwargs): """ Create MasterParameters object with given parameters dict The parameters_dict contains all the Parameters objects in the ParametersCollection, and provides the access for the master parameters so they can control the other Parameters objects of which they are responsible. """ self.parameters_dict = parameters_dict super().__init__(*args, **kwargs) def __setitem__(self, key, value): """ Set item that propagates change throughout all links """ master_parameter = self.parameters[key] if master_parameter.links is not None: for calculator in master_parameter.links: calculator_par_name = master_parameter.links[calculator] self.parameters_dict[calculator][calculator_par_name] = value self.parameters[key].value = value class InstrumentParameters(AbstractBaseClass): """ Object intended for use as instrument parameters This object holds Parameters objects for a number of calculators and can have master parameters which control parameters for a number of calculators at once. """ def __init__(self): """ Create an empty ParametersCollection instance """ self.parameters_dict = {} self.master = MasterParameters(self.parameters_dict) @classmethod def from_json(cls, fname: str): """ Initialize an instance from a json file. :param fname: The filename (path) of the json file. :type fname: str """ with open(fname, "r") as fp: instance = cls.from_dict( json.load(fp, extra_obj_pairs_hooks=[quantity_decode]) ) return instance @classmethod def from_dict(cls, instrument_dict: dict): """ Initialize an instance from a dict. :param fname: The filename (path) of the json file. :type fname: str """ parameters = cls() for key in instrument_dict: if key != "Master": parameters.add( key, CalculatorParameters.from_dict(instrument_dict[key]) ) if "Master" in instrument_dict.keys(): parameters.master = CalculatorParameters.from_dict( instrument_dict["Master"] ) return parameters def to_dict(self): params_collect = {} params_collect["Master"] = self.master.to_dict() for key in self.parameters_dict: params_collect[key] = self.parameters_dict[key].to_dict() return params_collect def to_json(self, fname: str): """ Save this parameters class to a human readable json file. :param fname: Write to this file. :type fname: str """ with open(fname, "w") as fp: json.dump( self.to_dict(), fp, indent=4, allow_nan=True, extra_obj_encoders=[quantity_encode], ) def add(self, key, parameters): """ Here key could be a calculator object or a reference to such an object, like its name """ if not isinstance(parameters, CalculatorParameters): raise RuntimeError( "ParametersCollection holds objects of type Parameters," + " was provided with something else." ) self.parameters_dict[key] = parameters def add_master_parameter(self, name, links, **kwargs): """ link: dict with keys and parameters for which this parameter should override """ master_parameter = MasterParameter(name, **kwargs) # Check the link keys correspond to keys in parameters_dict if not isinstance(links, dict): raise RuntimeError("links should be a dict") for link_key in links: if link_key not in self.parameters_dict: raise RuntimeError("A link had a key which was not recognized.") master_parameter.add_links(links) self.master.add(master_parameter) def __getitem__(self, key): """ Provides access to parameters of calculator with given name """ return self.parameters_dict[key] def __delitem__(self, key): """ Allows deletion of parameters of calculator with given name """ del self.parameters_dict[key] def __repr__(self): """ Prints overview of state of this ParametersCollection object """ string = "- ParametersCollection object -\n" if len(self.master.parameters) > 0: string += " Master Parameters\n" for key in self.master.parameters: string += " " + self.master.parameters[key].print_line() + "\n" string += "\n" for key in self.parameters_dict: string += 3 * " " + str(key) + "\n" string += self.parameters_dict[key].print_indented(3) string += "\n" return string libpyvinyl-1.2.0/libpyvinyl/Parameters/Parameter.py000066400000000000000000000424141456037232700225270ustar00rootroot00000000000000# Created by Mads Bertelsen and modified by Juncheng E # Further modified by Shervin Nourbakhsh import math import numpy from libpyvinyl.AbstractBaseClass import AbstractBaseClass from pint.unit import Unit from pint.quantity import Quantity import pint.errors # typing from typing import Union, Any, Tuple, List, Dict, Optional # ValueTypes: TypeAlias = [str, bool, int, float, object, pint.Quantity] ValueTypes = Union[str, bool, int, float, pint.Quantity] class Parameter(AbstractBaseClass): """ Description of a single parameter. The parameter is defined by: - name: when added to a parameter collection, it can be accessed by this name - value: can be a boolean, a string, a pint.Quantity, an int or float (the latter internally converted to pint.Quantity) - unit: a string that is internally converted into a pint.Unit - comment: a string with a brief description of the parameter and additional informations """ def __init__( self, name: str, unit: str = "", comment: Union[str, None] = None, ): """ Creates parameter with given name, optionally unit and comment :param name: name of the parameter :param unit: physical units returning the parameter value :param comment: brief description of the parameter """ self.name: str = name self.__unit: Union[str, Unit] = Unit(unit) if unit != None else "" self.comment: Union[str, None] = comment self.__value: Union[ValueTypes, None] = None self.__intervals: List[Tuple[Quantity, Quantity]] = [] self.__intervals_are_legal: Union[bool, None] = None self.__options: List = [] self.__options_are_legal: Union[bool, None] = None self.__value_type: Union[ValueTypes, None] = None @classmethod def from_dict(cls, param_dict: Dict): """ Helper class method creating a new object from a dictionary providing - name: str MANDATORY - unit: str - comment: str - ... This class method is mainly used to allow dumping and loading the class from json """ if "name" not in param_dict: raise KeyError( "name is a mandatory element of the dictionary, but has not been found" ) param = cls( param_dict["name"], param_dict["_Parameter__unit"], param_dict["comment"] ) for key in param_dict: param.__dict__[key] = param_dict[key] # set the value type, making the necessary promotions param.__set_value_type(param.value) for interval in param.__intervals: param.__set_value_type(interval[0]) param.__set_value_type(interval[1]) for option in param.__options: param.__set_value_type(option) return param @property def unit(self) -> str: """Returning the units as a string""" return str(self.__unit) @unit.setter def unit(self, uni: str) -> None: """ Assignment of the units :param uni: unit A pint.Unit is used if the string is recognized as a valid unit in the registry. It is stored as a string otherwise. """ try: self.__unit = Unit(uni) except pint.errors.UndefinedUnitError: self.__unit = uni @property def value_no_conversion(self) -> ValueTypes: """ Returning the object stored in value with no conversions """ return self.__value @property def pint_value(self) -> Quantity: """Returning the value as a pint object if available, an error otherwise""" if not isinstance(self.__value, Quantity): raise TypeError("The parameter value is not of pint.Quantity type") return self.__value @property def value(self) -> ValueTypes: """ Returns the magnitude of a Quantity or the stored value otherwise """ if isinstance(self.__value, Quantity): return self.__value.m_as(self.__unit) else: return self.__value @staticmethod def __is_type_compatible(t1: type, t2: Union[None, type]) -> bool: """ Check type compatibility :param t1: first type :type t1: type :param t2: second type :type t2: type :return: bool True if t1 and t2 are of the same type or if one is int and the other float False otherwise """ if t1 == type(None) or t2 == type(None): return True if t1 == None or t2 == None: return True # promote any int or float to pint.Quantity if t1 == float or t1 == int or t1 == numpy.float64: t1 = Quantity if t2 == float or t2 == int or t2 == numpy.float64: t2 = Quantity if "quantity" in str(t1): t1 = Quantity if "quantity" in str(t2): t2 = Quantity if t1 == t2: return True return False def __to_quantity(self, value: Any) -> Union[Quantity, Any]: """ Converts value into a pint.Quantity if this Parameter is defined to be a Quantity. It returns value unaltered otherwise. """ if self.__value_type == Quantity and not isinstance(value, Quantity): return Quantity(value, self.__unit) return value def __set_value_type(self, value: Any) -> None: """ Sets the type for the parameter. It should always be preceded by a __check_compatibility to avoid chaning the type for the Parameter :param value: a value that might be assigned as Parameter value or in an interval or option :type value: any type It will raise an exception if the type is not coherent to what previously is declared. """ if ( hasattr(value, "__iter__") and not isinstance(value, str) and not isinstance(value, Quantity) ): value = value[0] # if an integer has units, then it is a quantity -> promotion if isinstance(value, int) and self.__unit != "": self.__value_type = Quantity # if value is a float, than can be used as a quantity -> promotion elif isinstance(value, float): self.__value_type = Quantity else: # cannot be treated as a quantity self.__value_type = type(value) def __check_compatibility(self, value: Any) -> None: """ Raises an error if this parameter and the given value are not of the same type or compatible :param value: a value that might be assigned as Parameter value or in an interval or option :type value: any type It will raise an exception if the type is not coherent to what previously is declared. """ vtype = type(value) assert vtype is not None v = value # First case: value is a list, it might be good to double check # that all the members are of the same type if isinstance(value, list): vtype = type(value[0]) for v in value: if not self.__is_type_compatible(vtype, type(v)): raise TypeError( "Iterable object passed as value for the parameter, but it is made of inhomogeneous types: ", vtype, type(v), ) elif isinstance(value, dict): raise NotImplementedError("Dictionaries are not accepted") # check that the value is compatible with what previously defined if not self.__is_type_compatible(vtype, self.__value_type): raise TypeError( "New value of type {} is different from {} previously defined".format( type(value), self.__value_type ) ) @value.setter def value(self, value: ValueTypes) -> None: """ Sets value of this parameter if value is legal, an exception is raised otherwise. :param value: value :type value: str | boolean | int | float | object | pint.Quantity If value is a float, it is internally converted to a pint.Quantity """ if ( self.__unit is not None and isinstance(value, pint.Quantity) and value.check(self.__unit) is False ): raise pint.errors.DimensionalityError(value.units, self.__unit) self.__check_compatibility(value) self.__set_value_type(value) value = self.__to_quantity(value) if self.is_legal(value): self.__value = value else: raise ValueError("Value of parameter '" + self.name + "' illegal.") def add_interval( self, min_value: Union[ValueTypes, None], max_value: Union[ValueTypes, None], intervals_are_legal: bool, ) -> None: """ Sets an interval for this parameter: [min_value, max_value] The interval is closed on both sides: min_value and and max_value are included. :param min_value: minimum value of the interval, None for infinity :param max_value: maximum value of the interval, None for infinity :param intervals_are_legal: if not done previously, it defines if all the intervals of this parameter should be considered as allowed or forbidden intervals. """ if min_value is None: min_value = -math.inf if max_value is None: max_value = math.inf self.__check_compatibility(min_value) self.__check_compatibility(max_value) self.__set_value_type(min_value) # it could have been max_value if self.__intervals_are_legal is None: self.__intervals_are_legal = intervals_are_legal else: if self.__intervals_are_legal != intervals_are_legal: print("WARNING: All intervals should be either legal or illegal.") print( " Interval: [" + str(min_value) + ":" + str(max_value) + "] is declared differently w.r.t. to the previous intervals" ) # should it throw an expection? raise ValueError("Parameter", "interval", "multiple validities") self.__intervals.append( (self.__to_quantity(min_value), self.__to_quantity(max_value)) ) # if the interval has been added after assignement of the value of the parameter, # the latter should be checked if not self.value_no_conversion is None: if self.is_legal(self.value) is False: raise ValueError( "Value " + str(self.value) + " is now illegal based on the newly added interval" ) def add_option(self, option: Any, options_are_legal: bool) -> None: """ Sets allowed values for this parameter :param option: a discrete allowed or forbidden value :param options_are_legal: defines if the given option is for a legal or illegal discrete value """ if self.__options_are_legal is None: self.__options_are_legal = options_are_legal else: if self.__options_are_legal != options_are_legal: print("ERROR: All options should be either legal or illegal.") print( " This option is declared differently w.r.t. to the previous ones" ) # should it throw an expection? raise ValueError("Parameter", "options", "multiple validities") self.__check_compatibility(option) self.__set_value_type(option) # it could have been max_value if isinstance(option, list): for op in option: self.__options.append(self.__to_quantity(op)) else: self.__options.append(self.__to_quantity(option)) # if the option has been added after assignement of the value of the parameter, # the latter should be checked if not self.value_no_conversion is None: if self.is_legal(self.value) is False: raise ValueError( "Value " + str(self.value) + " is now illegal based on the newly added option" ) def get_options(self): return self.__options def get_options_are_legal(self): return self.__options_are_legal def get_intervals(self): return self.__intervals def get_intervals_are_legal(self): return self.__intervals_are_legal def is_legal(self, values: Union[ValueTypes, None] = None) -> bool: """ Checks whether or not given or contained value is legal given constraints. """ if values is None: values = self.__value if ( not hasattr(values, "__iter__") or isinstance(values, str) or isinstance(values, Quantity) ): # print(str(hasattr(values, "__iter__")) + str(values)) # first if types are compatible if self.__is_type_compatible(type(values), self.__value_type) is False: return False value = self.__to_quantity(values) # obvious, if no conditions are defined, the value is always legal if len(self.__options) == 0 and len(self.__intervals) == 0: return True # first check if the value is in any defined discrete value for option in self.__options: if option == value: return self.__options_are_legal # secondly check if it is in any defined interval for interval in self.__intervals: if interval[0] <= value <= interval[1]: return self.__intervals_are_legal # at this point the value has not been found in any interval # if intervals where defined and were forbidden intervals, the value should be accepted if len(self.__intervals) > 0: return not self.__intervals_are_legal # if there where no intervals defined, then it depends if the discrete values were forbidden or allowed return not self.__options_are_legal # else # all values have to be True for value in values: if not self.is_legal(value): return False return True def print_parameter_constraints(self) -> None: """ Print the legal and illegal intervals of this parameter. FIXME """ print(self.name) print("intervals:", self.__intervals) print("intervals are legal:", self.__intervals_are_legal) print("options", self.__options) print("options are legal:", self.__options_are_legal) def clear_intervals(self) -> None: """ Clear the intervals of this parameter. """ self.__intervals = [] def clear_options(self) -> None: """ Clear the option values of this parameter. """ self.__options = [] def print_line(self) -> str: """ returns string with one line description of parameter """ if self.__unit is None or self.__unit == Unit(""): unit_string = "" else: unit_string = "[" + str(self.__unit) + "] " if self.value_no_conversion is None: string = self.name.ljust(40) + " " else: string = self.name.ljust(35) + " " string += str(self.value).ljust(10) + " " string += unit_string.ljust(20) + " " if self.comment is not None: string += self.comment string += 3 * " " for interval in self.__intervals: legal = "L" if self.__intervals_are_legal else "I" intervalstr = legal + "[" + str(interval[0]) + ", " + str(interval[1]) + "]" string += intervalstr.ljust(10) if len(self.__options) > 0: values = "(" for option in self.__options: values += str(option) + ", " values = values.strip(", ") values += ")" string += values return string def __repr__(self) -> str: """ Returns string with thorough description of parameter """ string = "Parameter named: '" + self.name + "'" if self.value_no_conversion is None: string += " without set value.\n" else: string += " with value: " + str(self.value) + "\n" if self.__unit is not None: string += " [" + str(self.__unit) + "]\n" if self.comment is not None: string += " " + self.comment + "\n" if len(self.__intervals) > 0: string += ( " Legal intervals:\n" if self.__intervals_are_legal else " Illegal intervals:\n" ) for interval in self.__intervals: string += " [" + str(interval[0]) + "," + str(interval[1]) + "]\n" if len(self.__options) > 0: string += " Allowed values:\n" # FIXME for option in self.__options: string += " " + str(option) + "\n" return string libpyvinyl-1.2.0/libpyvinyl/Parameters/__init__.py000066400000000000000000000001661456037232700223440ustar00rootroot00000000000000from .Parameter import Parameter from .Collections import InstrumentParameters, CalculatorParameters, MasterParameter libpyvinyl-1.2.0/libpyvinyl/__init__.py000066400000000000000000000011101456037232700202270ustar00rootroot00000000000000""" :module: Exposes all user facing classes in the common libpyvinyl namespace""" __author__ = "Carsten Fortmann-Grote, Mads Bertelsen, Juncheng E, Shervin Nourbakhsh" __email__ = "carsten.grote@xfel.eu, juncheng.e@xfel.eu, Mads.Bertelsen@ess.eu, nourbakhsh@ill.fr" __version__ = "1.2.0" __release__ = __version__ from .BaseCalculator import BaseCalculator, CalculatorParameters from .BaseData import BaseData from .Parameters.Parameter import Parameter from .Instrument import Instrument # 3rd party imports from pint import UnitRegistry ureg = UnitRegistry() Q_ = ureg.Quantity libpyvinyl-1.2.0/notebooks/000077500000000000000000000000001456037232700157275ustar00rootroot00000000000000libpyvinyl-1.2.0/notebooks/demo_pyvinyl.ipynb000066400000000000000000000074441456037232700215210ustar00rootroot00000000000000{ "cells": [ { "cell_type": "markdown", "source": [ "# Demo of the pyvinyl API " ], "metadata": {} }, { "cell_type": "markdown", "source": [ "## Imports " ], "metadata": {} }, { "cell_type": "code", "execution_count": 1, "source": [ "%load_ext autoreload" ], "outputs": [], "metadata": {} }, { "cell_type": "code", "execution_count": 2, "source": [ "%autoreload 2" ], "outputs": [], "metadata": {} }, { "cell_type": "code", "execution_count": 15, "source": [ "from libpyvinyl.BaseCalculator import BaseCalculator, Parameters, SpecializedCalculator\n", "\n", "import os\n", "import h5py\n", "import numpy" ], "outputs": [], "metadata": {} }, { "cell_type": "code", "execution_count": 4, "source": [ "import sys" ], "outputs": [], "metadata": {} }, { "cell_type": "code", "execution_count": 5, "source": [ "sys.path.insert(0, '../tests/')" ], "outputs": [], "metadata": {} }, { "cell_type": "code", "execution_count": 10, "source": [ "from RandomImageCalculator import RandomImageCalculator" ], "outputs": [], "metadata": {} }, { "cell_type": "markdown", "source": [ "## User interface " ], "metadata": {} }, { "cell_type": "code", "execution_count": 11, "source": [ "calculator = RandomImageCalculator(output_path=\"out.h5\")" ], "outputs": [], "metadata": {} }, { "cell_type": "markdown", "source": [ "### Setup the parameters" ], "metadata": {} }, { "cell_type": "code", "execution_count": 18, "source": [ "### Setup the calculator params\n", "\n", "calculator.setParams()\n", "\n", "### Run the backengine\n", "\n", "calculator.backengine()\n", "\n", "### Look at the data and store as hdf5\n", "\n", "calculator.data\n", "\n", "calculator._BaseCalculator__data\n", "\n", "calculator.saveH5(calculator.output_path)\n", "\n", "### Save the parameters to a human readable json file.\n", "\n", "calculator.parameters.to_json(\"my_parameters.json\")\n", "\n", "### Save calculator to binary dump.\n", "\n", "dumpfile = calculator.dump()\n", "\n", "### Load back parameters\n", "\n", "new_parameters = Parameters.from_json(\"my_parameters.json\")\n", "\n", "print('grid_size_x:', new_parameters['grid_size_x'])\n", "print('grid_size_x:', new_parameters['grid_size_x'])\n", "\n", "reloaded_calculator = SpecializedCalculator(dumpfile=dumpfile)\n", "\n", "reloaded_calculator.data\n", "\n", "print('grid_size_x:', reloaded_calculator.parameters['grid_size_x'])\n", "print('grid_size_x:', reloaded_calculator.parameters['grid_size_x'])" ], "outputs": [ { "output_type": "stream", "name": "stdout", "text": [ "grid_size_x: Parameter named: 'grid_size_x' with value: 128\n", "\n", "grid_size_x: Parameter named: 'grid_size_x' with value: 128\n", "\n", "grid_size_x: Parameter named: 'grid_size_x' with value: 128\n", "\n", "grid_size_x: Parameter named: 'grid_size_x' with value: 128\n", "\n" ] } ], "metadata": {} } ], "metadata": { "kernelspec": { "name": "python3", "display_name": "Python 3.8.10 64-bit ('SimEx-Lite': conda)" }, "language_info": { "name": "python", "version": "3.7.8", "mimetype": "text/x-python", "codemirror_mode": { "name": "ipython", "version": 3 }, "pygments_lexer": "ipython3", "nbconvert_exporter": "python", "file_extension": ".py" }, "interpreter": { "hash": "1ebcb5ce9928518081d97f4b084470ecc3595de5a529f9bfa031f01abf4d1939" } }, "nbformat": 4, "nbformat_minor": 4 }libpyvinyl-1.2.0/notebooks/instrument.json000066400000000000000000000077761456037232700210530ustar00rootroot00000000000000{ "Master": { "absorption": { "links": { "Sample top": "absorption", "Sample bottom": "absorption" }, "name": "absorption", "unit": "barns", "comment": "Absorption cross section for both samples", "value": 3.4, "legal_intervals": [], "illegal_intervals": [], "options": [] } }, "Source": { "energy": { "name": "energy", "unit": "eV", "comment": "Source energy setting", "value": 4000, "legal_intervals": [ [ 0, 1000000.0 ] ], "illegal_intervals": [], "options": [] }, "delta_energy": { "name": "delta_energy", "unit": "eV", "comment": "Energy spread fwhm", "value": null, "legal_intervals": [ [ 0, 400 ] ], "illegal_intervals": [], "options": [] }, "position": { "name": "position", "unit": "cm", "comment": "Source center", "value": null, "legal_intervals": [ [ -1.5, 1.5 ] ], "illegal_intervals": [], "options": [] }, "gaussian": { "name": "gaussian", "unit": null, "comment": "False for flat, True for gaussian", "value": null, "legal_intervals": [], "illegal_intervals": [], "options": [ false, true ] } }, "Sample top": { "radius": { "name": "radius", "unit": "cm", "comment": "Sample radius", "value": null, "legal_intervals": [ [ 0, Infinity ] ], "illegal_intervals": [], "options": [] }, "height": { "name": "height", "unit": "cm", "comment": "Sample height", "value": null, "legal_intervals": [ [ 0, Infinity ] ], "illegal_intervals": [], "options": [] }, "absorption": { "name": "absorption", "unit": "barns", "comment": "absorption cross section", "value": 3.4, "legal_intervals": [ [ 0, Infinity ] ], "illegal_intervals": [], "options": [] } }, "Sample bottom": { "radius": { "name": "radius", "unit": "cm", "comment": "Sample radius", "value": null, "legal_intervals": [ [ 0, Infinity ] ], "illegal_intervals": [], "options": [] }, "height": { "name": "height", "unit": "cm", "comment": "Sample height", "value": null, "legal_intervals": [ [ 0, Infinity ] ], "illegal_intervals": [], "options": [] }, "absorption": { "name": "absorption", "unit": "barns", "comment": "absorption cross section", "value": 3.4, "legal_intervals": [ [ 0, Infinity ] ], "illegal_intervals": [], "options": [] } } }libpyvinyl-1.2.0/notebooks/instrument_repository_API.ipynb000066400000000000000000000045331456037232700241770ustar00rootroot00000000000000{ "cells": [ { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [], "source": [ "import sys" ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [], "source": [ "sys.path.append('../libpyvinyl/')" ] }, { "cell_type": "code", "execution_count": 6, "metadata": {}, "outputs": [], "source": [ "from Instrument import repository" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Load the default repo" ] }, { "cell_type": "code", "execution_count": 7, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "https://github.com/PaNOSC-ViNYL/instrument_database.git\n", "/tmp/tmp0xnbbivw:\n", "LICENSE\n", "README.md\n" ] } ], "source": [ "instr_repo = repository()\n", "print(instr_repo.url)\n", "instr_repo.ls_files()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Load one branch of the default repo" ] }, { "cell_type": "code", "execution_count": 8, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "/tmp/tmpnzjlvz4h:\n", "LICENSE\n", "mcstas\n", "README.md\n" ] } ], "source": [ "instr_repo_shervin = instr_repo.switch_branch('shervin')\n", "instr_repo = repository(branch='shervin')\n", "instr_repo.ls_files()" ] }, { "cell_type": "code", "execution_count": 9, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "/tmp/tmpnzjlvz4h/mcstas:\n", "ILL\n", "components\n", "scripts\n", "CMakeLists.txt\n", "requirements.txt\n" ] } ], "source": [ "instr_repo.ls_files('mcstas')" ] } ], "metadata": { "kernelspec": { "display_name": "Python [conda env:wpg3]", "language": "python", "name": "conda-env-wpg3-py" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.8.2" } }, "nbformat": 4, "nbformat_minor": 2 } libpyvinyl-1.2.0/requirements.txt000066400000000000000000000000311456037232700172020ustar00rootroot00000000000000-r requirements/prod.txt libpyvinyl-1.2.0/requirements/000077500000000000000000000000001456037232700164475ustar00rootroot00000000000000libpyvinyl-1.2.0/requirements/dev.txt000066400000000000000000000001421456037232700177630ustar00rootroot00000000000000black==22.8.0 pytest flake8 sphinx sphinx-rtd-theme sphinx-autodoc-typehints nbsphinx myst-parser libpyvinyl-1.2.0/requirements/prod.txt000066400000000000000000000000761456037232700201570ustar00rootroot00000000000000pint<=0.19.2 dill<=0.3.5.1 scipy jsons h5py json_tricks numpy libpyvinyl-1.2.0/setup.cfg000066400000000000000000000002241456037232700155430ustar00rootroot00000000000000[bumpversion] current_version = 1.1.2 commit = True tag = True [bumpversion:file:libpyvinyl/__init__.py] [metadata] description-file = README.md libpyvinyl-1.2.0/setup.py000066400000000000000000000036731456037232700154470ustar00rootroot00000000000000from setuptools import setup, find_packages import sys with open("requirements/prod.txt") as requirements_file: require = requirements_file.read() requirements = require.split() import codecs import os.path def read(rel_path): here = os.path.abspath(os.path.dirname(__file__)) with codecs.open(os.path.join(here, rel_path), "r") as fp: return fp.read() def get_from_init(rel_path, field): for line in read(rel_path).splitlines(): if line.startswith(field): delim = '"' if '"' in line else "'" return line.split(delim)[1] else: raise RuntimeError("Unable to find " + field + " string.") initfile = "libpyvinyl/__init__.py" version = get_from_init(initfile, "__version__") setup( name="libpyvinyl", packages=find_packages(include=["libpyvinyl", "libpyvinyl.*"]), version=version, license="LGPLv3", description="The python API for photon and neutron simulation codes in the Photon and Neutron Open Science Cloud (PaNOSC).", author=get_from_init(initfile, "__author__"), author_email=get_from_init(initfile, "__email__"), url="https://github.com/PaNOSC-ViNYL/libpyvinyl", download_url=f"https://github.com/PaNOSC-ViNYL/libpyvinyl/archive/v{version}.tar.gz", keywords=["photons", "neutrons", "simulations"], install_requires=requirements, classifiers=[ "Development Status :: 3 - Alpha", "Intended Audience :: Developers", "Intended Audience :: Science/Research", "Topic :: Software Development :: Libraries :: Python Modules", "License :: OSI Approved :: GNU Lesser General Public License v3 or later (LGPLv3+)", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", ], ) libpyvinyl-1.2.0/tests/000077500000000000000000000000001456037232700150665ustar00rootroot00000000000000libpyvinyl-1.2.0/tests/integration/000077500000000000000000000000001456037232700174115ustar00rootroot00000000000000libpyvinyl-1.2.0/tests/integration/plusminus/000077500000000000000000000000001456037232700214505ustar00rootroot00000000000000libpyvinyl-1.2.0/tests/integration/plusminus/.github/000077500000000000000000000000001456037232700230105ustar00rootroot00000000000000libpyvinyl-1.2.0/tests/integration/plusminus/.github/ISSUE_TEMPLATE.md000066400000000000000000000005001456037232700255100ustar00rootroot00000000000000* PlusMinus version: * Python version: * Operating System: ### Description Describe what you were trying to get done. Tell us what happened, what went wrong, and what you expected to happen. ### What I Did ``` Paste the command(s) you ran and the output. If there was a crash, please include the traceback here. ``` libpyvinyl-1.2.0/tests/integration/plusminus/.gitignore000066400000000000000000000022541456037232700234430ustar00rootroot00000000000000# Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python env/ build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ *.egg-info/ .installed.cfg *.egg # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover .hypothesis/ .pytest_cache/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder target/ # Jupyter Notebook .ipynb_checkpoints # pyenv .python-version # celery beat schedule file celerybeat-schedule # SageMath parsed files *.sage.py # dotenv .env # virtualenv .venv venv/ ENV/ # Spyder project settings .spyderproject .spyproject # Rope project settings .ropeproject # mkdocs documentation /site # mypy .mypy_cache/ # IDE settings .vscode/libpyvinyl-1.2.0/tests/integration/plusminus/README.rst000066400000000000000000000007551456037232700231460ustar00rootroot00000000000000========= PlusMinus ========= An example of a small platform implementing libpyvinyl. Data structure ############## .. image:: ./docs/01-data_structure.png Instrument example ################## .. image:: ./docs/02-instrument_example.png This package was created with Cookiecutter_ and the `audreyr/cookiecutter-pypackage`_ project template. .. _Cookiecutter: https://github.com/audreyr/cookiecutter .. _`audreyr/cookiecutter-pypackage`: https://github.com/audreyr/cookiecutter-pypackage libpyvinyl-1.2.0/tests/integration/plusminus/docs/000077500000000000000000000000001456037232700224005ustar00rootroot00000000000000libpyvinyl-1.2.0/tests/integration/plusminus/docs/01-data_structure.png000066400000000000000000006523621456037232700263730ustar00rootroot00000000000000PNG  IHDRR6wsBIT|dtEXtSoftwaregnome-screenshot>*tEXtCreation TimeSat 26 Feb 2022 13:37:48 CETE& IDATxutF6J$!xpBR*HհBjPPڷ@hP ;-I`I$l\(sّgs`0B!B!B!D(+B!B!B!ēLB!B!B!D)H]!B!B!( !B!B! v!B!B!$.B!B!BڅB!B!BR@B!B!BQ hB!B!B!JAB!B!B!D)H]!B!B!( !B!B! v!B!B!$.B!B!BIQF)v!B<5jή!B!?@͛ٺuhB!#^MB!BRvYfg;B!ʝ[la!B!VVV 8!ȁf͚g;B!ݱcP(B!H])r]!IP((4UpkB!x;ٽѶ!OY B!xT CE7A!B!څB!B!xJI2eCB!B!B<2+ !JGB!B!B!D)H]!B!BlH]!O y,V!BY!v!B<5$[G!BYaR ǀOsS 5ѿG!(!NH&)-ctn)SG.g&J JlT{SɠB!H.D٨+h_Z`BOg`Հs/1h@mWGggڵCrY?Vpp/e9Oj{BE~O9p35OW(>qㅍh©-ϼkЧJ^䙅sz2 ff4fS!B!O h9n/\p.fӔȡ2ǵ;5ڎb797À-s;l+5B!kYQ}gה,|aG [CtcBϿ<V߁%.B!c(#V]`;@%)YwLE5ѻw c]iGk:aC-5\YϷ˸CDVtP yֱ2!@X`6$eD0ZJ<oД2(*jXѤ%MZRqIxpKO&X6H!B!D@{el*Wj^7[=JqU^~;ZϾ!yaurl>nmꙃ^p'PS\F!p哭矕֧́)/g~{8J8NNȧyЦ~dIGp|nxv˷+W]F4ۈzXvN?Di ]%mg0;K,C!I$5څ(hͲŇI{QS +XS+LBIf{Y37Y;M-P?ovjέcJ3h;7 _Nr\hJؒ$Å^žAW-6)≗'Sz<_*{GwN˺=qsՋ%+3ZfT(0q{mH^x[J}Gae;YퟳmAQ~.=nx;ɮvy>AE6:,afXf~91>7W0m9Z;ĸ.vNxex=э^.XVP\Cx^X{K K ,M蹸_ t{A|76gUuk4êEݨZX9̢iQ+B} zZ>~y(=\oknZΖ}|?qk[dIv |k׀ѵg7][ >on21z >kfhTK0oPƨaĜz=7>ܕ@Ǟb㶠xi""oGX=c4:է Me[1[7GB;݆Lisz 9B?:N$/2B!d.Dxvz4ڀ^6Wx G8{4[cKKLqET)p|/Xā fjocj tuI۶4sCvcFˮ;u؍g8}f/-f-.cש3>^BHcʈLig{&u4#ΖagmS8 82XXfʾo\ %D<`W 2JbO;* 2fxl䦌Kb|~㮕 ^)xRά1ٲ\ze{\[ИᆴE?a::C%CֳUG0UQ5:ZӸVJ !B'd Q6y]ˢhn95:leʂ^R€%|ł./c?P*c ZK(]ĭ#'ϟf7c7X|+s0qm4]5®`lMH3xg\_s)ńƛqgV,<8ؼ:!jȩ7@b0 H{,C\.p+8[["[2ջ1n"<zÃB'wŝwVΆQ-_2nFY+8OT9^åoIC'pyۆ1 Ilkxs}f]P2޼ӟ>x>k!]]fβ), ޲·I~ƗO:'P||}DwuDGpwˉN!eD};g xŁAǃfCbŞdxݙ垝ޙ_:ilJ5w.…82 Aɱu9)_4vD 6>jg^;muhq# TgCUX\KLz,BïR>s̗b·yvvegi\\qNhM̖)^]9љգR-C!B!yo8TeFMzFXh$tPG=숹&ya¡3{WvmK͌TS8á5=YxҝO_Cv]8N~68>ٓɬoRdfk4y@,GT\XnXd|U7z 8Ꮋm7ϺRW? }[X1{2#}Y-FxGtpAYVcƚLa7jЅ3H›3ˋW?a\~HCA5;*F65BIUN\KC|.v9&n1[~v]h<B!*P;Y7HUtf 5x!㏴T(Z`6I智xjiؚ2n>ZO8q XѨM%:6 IUYє%K3o~}Yfʺ@̿p)0 Ǚe-纆_s|$t[0^+kkƛ%\ϒ9--SsFj.3 HHzQ XB!HJQ6LeI*??6Yǐ)=DGئyY/f@K7f)LG0"BVVw>]Ӏ-x}_ l\ZKGx}1{b.8\JFnejf/^K q*]OԴ˕{H!@2{6 r|Ot.c{sUv8)}fèǤ qv83α,`_PĢ<4Y{(:xoL>5[?㒖{Z,w&Ӵ$a.4@>Ug,[6 !x<(tZU:=1`s-^PWƋRZL縚kR ޶<̞a9lMQ.>DȜWt\qXF&~J7d{{~h))7JV%\®!B81BGhWYQ`JS\BQy=}?U } ظ?bLW+{kƘRjTjl4ørg׹ \!$RC^Kr*t~Z|~:Lb7̀r*~ #*eksd-gYx^$ ZIZҲ+nsK ?rq~j9[\߇!@f~Wť i͚ThUYKI&Xޥf &UM9c&.fF_|lGN}Lz{ܜgb!+p%.SǼdg:Q luʕPʦ>`AtRԏ_z#=SфԽ!JX2xNﲆ+4ufqvy^]OtIWPbB!hl<@; ~SQleo5Vf@EcYu?XtbB!B2X,ui,{- l񅒑+X]1sH>M ;@臭WQLBK mv*/3Ŝek{X8†}f=a:w8@Xu|@|zujRL"J/Fu mr' g[\Av04&mљV#ͽITn0)vT N!1kxa@˝}<%l{Y&tѕ=ӂg3Ճ{89dkv?X&2Fpj3ՙ\ V6L{ŶTm>>#1)t)_Ҝ>rְWxE()-_^B!xH!#h׭_ğMaK]:mxwa}/E- =\3>׍ xjC'^~o֢ߣDɴq g73rVwY0q/ӝ os܆yo:n k>ӓ7߀C=U^ߴ-shuʇD.7{>M B6&\.=~IBo;&9^ ^%.n3'`E;?Lrl$p_v BQ"zgꕙ5euW˜p #Ji)Zaz 9AZ|5}>F$F 8%rM+@<6te (^bnTǷƱt2@M1g1k0Ɋ&IXOE"^0 ;W6.q`7̚9%;5XbsoT =פ^[x#3lrRޏu~K;I-03<9JrO,hD> o5}9:f7zO>T`vT)<BTw'! zh %k}AFr]g?ˋ⣁W ~zν8hbIr.E݁.}75w}ӏ&| ?j~ԯug3y6t_6Y/1h="-Kc.3i~`pOcO묛),9@(4Zr"\eޒ{:?O !BdBrnsS`GwߠƗ]SmRDƀptZ #X{%T0/KQ h>{J-FCԫFv ΉYD\˯owxZ9w?oAє p+W!5- H![S`Yhİq:{vF}; C>ʺ2mۭ=UH 9 9r298c_$FQ?7=o7b ۜ 0X  q:6LgK5mV IDATP<8ˏ|>LծeaN.cl7OXsrd !X;kbNW }J>`S84̠oson2kq8W ^j{L<`rcy5BAOH{>=ځCmr}}]|]kjžNt9\̎6 5ً :;|n|)j#^ۨʖ0goGZY;|u x9ש4gEjqb4x {wr 5r:QC,C!BR y͛ٺu+Ş!e;_fP2ΞtnwiGYӆ)EK>]PuZiME7gYM[6> <5ÒQ[f06dq凤)b xmrl9gxGv_ƶJ>Y4\u gku[z}8sݨ_FӭJ4whVvkp/r[orv]Ve&|=Oϓŗv{rLS;|f!c?ҫ4Υ :txܞۘƇt(kqn7Y,dl5D#{X*z>[˼oczXxAf{r:u||~:ps;o?9:Mw3j^NrZj|ޝɌ B~'NQ4i+|NǽqOU K\S5)Da^ *Gg24㍥*K_̖:[O٘Ѹ%Ur=LN#~ i6w03$|o'Pb]ۂsSe!B~525Hk !k@m6Z  !B!Dh/s嗀̿M]xk &תF !B9]^C') 5,iRumOmمB!K E# ! >Lttt+"&-UYX-&7BQNܹSMx"-=S*!B!Bxj˗//ùզEO.pB!B!B!OM _2Ut3BQA(hԲ!B!cC^*DPVtB!B!Cj Q6$.B!B!BڅB!B!BRxj !B1Hz6<Ь$!er2IP]ʴ,9F%F uYS$k3~/I1xףUӹ U3 ě̵^YH<^KFY+߂B$>~zK3w-d#KdIߖh/Gn((9.Dِ@B!DP&r3(flSkUKމDr7*4Ye3;TENХ~7GӚZE7I٘ŧХpDq)aQ; o ESǻ8o)CQťHl)Spo_TΆÝ¨0T97/sC)& : ;cYx(=K X"/UHּ{+Wj::Qc_XE/oCb~)Mo49A!ls:KKL5'.t1 afsvzTl.U}dcy2#9} =xI<=.^:gRsdaOz%4iv(/3eOZ;({]{y\hQd׳4>6UNr?^N3yPyƞξ%mq+[=VSY>?noc|`8Dg9Mu@2^ĭy^\[sܒwA2~~T1]胛WIos>(91}Q>ϐSrqQ=u9}fSξ0_nQ+̸(c8? M_x_IJqq?NwǥB KnfQccyW`s~{7W/^;>IeHOzozB?|#!f f@Ydy/a\LgEUNQ(tZp1.Rs2M+: 1IJ"m&X溞íyQL|>9ϝuw]ef/w鸲?xT }^Ng?>,$c/dv>'_[p,tOE\9NFvvS懱s:E|۵(.DِRxt0gD֣iأw+uEǀy"}&Jh^E=5&&lo>@㓐OJm2#S+BvUWD&5'd8_]ufcG!ٷ-ywOx4Nju{7,$ 6$_ְC4?i8D_fvuԦw2>y&3 $|c7-M $#~<{g39g~RoWNK¾%N) `h.iX]-Na}O C`*5U%ȵC}(eߦ_w׷hcJg5ZBҏ{0Zmjp/MS2rE-wY~<("b^ %f-OdsxM*G5-Zao"k\¬-j\KS"jx'+ÔVOx@ ї5ˎ@?SBaTgR(Q6c%ґ6bY]6ϵ6$ {MWwUAaMge ,T˵9/$dbe,] !ģ&%'a{ ;5q~23v@ivŘ&Kמ"tϿr{ՇC`Gw ?'uav@Z?IJ|y&$Cr{T}UbO~_0>\a\F\mY!0 Da/;qxʋx@ľ%~ܿ$=:=YUB_Nvr=ݕp9_:tT>̬1 3qjtz}-vgp#;5!=^ٚw!$;w# $3/Pcq# ;=^DzdR%=~dY] ]Փ H'ӚE0HJx!"vW|s+>8¤١I+TF 4̾ ГݹJڴP15~6܌ɵ ²wĸE6 v3GS?ϕȹ ~]8:q"s\嘫dRIQ'sm K[@ݼOk?ӵ{4[p>o}O;Kga`74s>q]ܦޛDZi;ir~HŹ>0yb^tJY}!%j_Y w z6N@wNb>'wh_cULdl35gf2.g?`B c!zcfVܚ寧*Q>MJƶ8s3|H(PQcNC&71] nX܋0IJ`oeV9Wcl܈fW3fAϹ}١jWMV'muC:G2i}v5AƚֶLř.vY77ۂ|P@Ӄ3Fʓ0ausLkP=̙-֤S;S Lk&Q^%UFĔ&x Ugjgy h r>}}zmvNDlJnˤ.`Gr"[f;.-?EUVnhs; nrgoL2rz]Jc݂?y[T<~jvJjmgG4xf.n,AsKohۍV|5#ƧgW#bXϼˀ96+[9{ZZlܶp3{9=y{Z8yW_]VpG0Ȫc=Z"iAMW煿;hnR.pb}]Z74=Ɖ=5,\iP\c\W[F|>p|vQ "BA}}.%Esֲɋ _`Xmc;b O sO `2vNEl*A* y CkKowCvcz*8Pr9GF[Ҏ)&ux1u·ꍰdaV˽^MUim1nͪPx"ZqЫu?ݬh5Ƹ 1t޸Jʷ4y7! fg8cuaN}eQ-,ʂfgڰeU6eg=H-ﯫ7`TVGTX}=nֿ;W>Qi59p&x]c?X)?`3梙Ⱉf6SiCPr$ּy!fPz9!Ug7`[ ar*Ih7]ݓfەB2:[chJ5,쓩*OG{ gA rveF,^b8AmoŔ'}4-+ yc}Xfcpr}ܭ}Xq?H?W8.ro o>%;YsàPE4NjYKR Mn!  掮\jLQ:yLnz iz q9)Hsb$QIwLfc,[~ɿt&1E_;i,3ʯBxZ$^-#=˚d֮3r3FTx{}L+x9PV#|3d6=7&=?~lSoJ8K2z/YWq^ʹxj^*<MәCMz,c nLٙw<3Ƌ &s=nt؈]{b/W> XC7:f=J% {݄M5As׾T O;rS%$B%p彍#:kk9y++yA;[yT{7+/u?G`('_ w,"auJC"H6\^; 1Y޾9ۓVj@o7']9\G9qzj}<:ioxkח4#QaZ<)*)swOmǤ]?`j4DxK"lj %$u7g01X[$' D]Mxo^ΙƐy{ٱM d>+\)C3uZ(po(|,#mPۧ J&bF,[ lˆ?ا@ .b/v@cC+G`~jl] (!w(4yl۹)'2RC\gB4v+VY 1Ug*{ 99m%Q]>''`mPd8q&=_Pt<à@|h߷D-@ "DGIUt*Uyv-6dj}|a:'9ڷ & Q9RoyMd>g`uxvNY@&ؽy0,E؄.r>ˮzvՏGuM|ovT`ֈVa tm羽mǣ-3>9*lݵ[21G;9:GM;Vo9tH,nYFu~a!LS؀zx#m68d#wwwL4Ϳ,!OxQ2K{ @ hhtwshW%2+>lkZy˿+,4|ו_YjtyF3.nMWO*%_hdSui=0+c<28GD%>,]8~l%I;l/><-\W=;./<3E"V6g+WBEl<б'g~ .H6b.ozLIjX_/a+'EP5;cnw{rHkrƱgZ-Εkl\ :?(7[Q*(BI|;v$^Ӂ| ;@|vV%F[h6GkpKK`C>Z/h% Dk05X1&^IG;7$!!n[Zi ޔt¶T9x:9 "x]DIad/;$ijuW6@)D| !Mu'0LCkcVh?;H.#" )FF20*\~2jF֩L9\ȁn$H]c=9uhc0'N{lY ~$#:IwxiuF4r6E}#|DqMapRSd߮ɭMUrB{KF+îZTϲ=/%oW3RrR1(.96Lz*Ar8Y+t4"G34U<}^^<̂U2H( R`5yɉ}(Ej__\GQ]/pisU9\w}cG@@r ƒh2J;85| `JfΛ} )tй?VEr\*#(HY@h Ԓ,?N`c*-<&!购թˉk2mOouI W[fM48mr*|Nucu;n[y}وi'ci+xb}fÂK՝(U웚ȶ n0R%+Crd9^/AV;@VH@:p s(+Vg].\5X^ ǖ\(w6vAChW,Nk0WPK;T 7!'ϦW:B=\0#S *YG T8&,66SZ݇onڔŁ?e׾rQO_3fo(U_Yɶ 3ewE5|1OyuY5:fgF&y=vKo"9& 6h ] ek|iNqqxN[y$i2}8`Dk9,J'HkW)k3q&䍓6mW[dS06msZUl\`8_sشcٙw<ߔ6OK<^6sׄE_W:sk Sϊ o iizkS(۳"P7F?D.<@ @Ťguԗ6_Czt}9n *Tʹc˥pe 4TlHuF֖BCICv.=\Y=^2ek6xMq:н lP{zWb^THEz5B#Vd$:8{?LO^]e`8#Qڌ:,X$0ݎA7'sHWRX;p羮;t|><<&Q Iz>y!LNbk7۬H\ ?^˟=.$QVeޕ̢2UAxgzn/&ZITY 6I큫l朶> T &mANT7mVVqyL&2rX*56+q^ʫ=QU[,QJ-Dֱ O7q: ,}RRo~\;(PkEA:/7CoJ~h'Nrb#@UJvTw6d9QμZ}IV vuPX 3Bho'ΰ+NiF'\3nMiSkK:[6wݫD>*#׈3|L %QyUvduzWq"^=:d V9zK,Lzv[?/kZM|m6*j-  tI߷^I O q|VH}|nYu̽3!xqэ:'}|KXB#G C (P ~vb=o!7ǪD=( uC=IϚv;(^g5dճ옕~yg# {P1sL'`s[&-X~I(:Cu `@2@⎮-r#ɥ@ϠxiW*?'I^WCBi+ﳀ]҉oeCԖ4Ø%K״"d`#_$8}Wgm C߰hg-t IJ4XbjpLM3˙W28 Ԣ3)>?& 磺x`!oy3z<^ 9~5Dg.HF/Xq >K6j}K{M,$n-Хj"`O(A]ړyn".6~NI󜖔 aWCpڟ}a_PFْS\sc.9Q,кfY!0*Q6>eYF97y7]lp=L}4!>Ul6ۊz f;px*F_(2BƼLyʥ78KMlw%sd`ÜnG>Sk|3Y@QwD"Gu^&€Rj =g7@{?"rk95~&%?>y8& Q9@QaF|sPq%3pzc)гَr̰.K@""#j v}l 7(sv;?u{ e/;6率:@=XZ]JH&rm2].m[q?.& $H^J&N(g[x~zBS +ί0.㻟C͋&6E2晞|}O8<kRQ½C|Iv yپߊTNSip0]IO⵴QSߦ2^\I+Mz?D0TkX77P98`1+er0Z| IDATbx[&uWqUT͕I(dU*x'7q_Z'zu(+PR|0=qlZXKaC ǤI;ly&.`/?**i`ny+n1#p\ yL\;.O6ldh",^< !tI==o'Kۧ+* ?; \~YuPQ˺u*~Y1L9٧cP{N=UeC$yZsBx樃cʳs(ˡlOml܅ 5(Ld`u(|Ƴ/Gr 3J[>F;C=`pS*ods&0^X؝i'I(_x`Ȩ[ԆNaMAփ@ t IߪJrֻ]b`J D;MEn"b8pf34rO} MùSա]KbO8Zcb/m0Y\ 'Pt5wwUv109 Ww6rR/6嚘&>XZ[Mۂ`h#ZlP9W-;_-r,6l*Zʟ?_@uQqf5yr9j!<^PCIsۻx.ɯwxb%0X/_CYW'geP~Ʊ΂Q_fsq\M&Vfgv!<}wi-ﳄ4:{!,d.-w x20 B{R*K" 53aKL ֝|fo]t#`&>{2^y9)|_'l%q`Ӫ>5IKD1>]X:qW캡M"k#Ukú_v}>dņ/g x Q0g"~e!E"gC7#'l![eױzo~(HC睃y<}v~_‚/'">y5OVe]˭2wl?Z-g_vyvs\8og3@!}89v#pOC)㳿?Sm K`JWG_q ?e'$<ԛ vΙ'D#ꈰX_]/*"{J0׼=O<'Jt=AGV[ h"曳3M7D+&uL}|$;|Knw~?i:QTְQc|l"gZa78] W#T enZ!1 GRt.WbŔ8t*EZaG 5npp.><ۤ$#lĮdpU5yds@r)Ðmd:>1nKit7MVCanS_8fm7@jH\Q Uj0^GUEbZխ1U/l%|>&:z.}p-r\HbL)dhv:HG<&AA 0xU. hCacMsQ*\ahq h)1Pi彏h/w_[xx?u:9z@MFt~Ie2Md7.<;L;܌M}oۋ{i]{~6B4,!w :/sn{)$pqy!<ۿmDѕg b{@zcDg3^ÊNnzy\nGBMiõ?!LRICDi3Wle{QoƐ%*LEsVCJi;6?}}n†F0WomڞA?\<cXVmٙu$Ne`lMC;Cg` ut@zsEռ P6Ev<;]8k[w6A EQ%&wQmG辫yI lbyG_QEps8ňBQODx{a36EHmo@毗2>$ A-ҌF bX8Cd CH_9#Q̾uĄl_bkw%1;#V9y^^&H4}g oD| %LNoBW5Z];;@fu0x9S@WBܧ]=1fGnǫg$\.pX׊B`f/MV4zUf!X:д._MgYS&rJ nN SRd9i;m#T=yUf %$A1uAϻl$Ô~^(Ny}_LzNZ1w[|tFiPl>GWSHɯȊI@,KJS+- 6Z]@ }cҕ?B9<^: k,] %2džuUeɡ(dWHH .FEpL@ Bh_E$xIB3?F_C bw!>ӷFF ;DT DtV!"9r:"@rv nSi뜝k4Jz3@ 6V|[»y""8ה}zq^iׂ 'yKqS]+UOZ;M VB 4rzT6w?;7V@ oHѠ PmJ c >jN:N;-_|]-sP00KnMEHJ#IrCX2("-vQ *QNOS);(zA C7Li [v[9^pWs)@ +ʖKiT;89.d3{d΅4ܸt47v4-bWCAtVx @ @ AsG 9Q4 Ȍpc"D] @ @ !v*6<2\gB @ @ ~W1@ @ @ t! @ @ @ D@ 8ny`.>@ #.@ IFs) Gj8TbE/S?Qs3-Y&2ߵJ9vU]d> @ t! @ .LBw3sIz޸h/T;0't# 9י#\Z%R ¤+}OW%!V)}@  ]6 :32IV6t垿:+OAxr>7;MZ@+wL&,) SG 6Lqs;}.gFJ/`zF*9#@ _Nd2~<0$ F3j-`7ک @"LyHTofg> 'vJow'E$$#[Vs S:9:H,y+BpK,>M5 ?י;;hQ|ңO>͋BV0#m C_C@ !w?a#9Y0,N_ @+|È8{/8(<]RhC_h j$Uyb dwX`ku$aWHMzu ͎VM r슳r>T=Oc,>: *z1E >)bC]J _{׶4綬t=B\yo"_ݸp uf8!$a4s^z/e$@ "tL0Q ^ NQe$_/HFD"f%(" `&xBcQfeۃ{ ų-LlzBR"]O\nf+`k0(aޜ}\{Ik!,f }/]orC#.≱;^&aHYOr=ъwΥ尦jg3y.ߕC2ķnK&ú֟ӕvGt3o⯙ߍ{x"u>`>ff~L[V8Xxv1ynnjL2@ NpŁBZA 4o`XAHVV}'KfC!b>l$y/%7 ux٩_|#LShuN26;=7.:б`p0sLb#gsf2K m| +' hČ M"HqRﱾdύبR^k`Z rpel LR`4J2>P(O\KV;7!472F)tkP5UXۏC<˯[٥ ܵކ<.}U)m`aJVno`[cI쬥L2dDɯ4c,jɩeF+"([zEZADd>R[ Q7#|O>Zqa6J-%>^΍@LM|SLLl4 ־yK(צٵ%͔On#-TFm@5-u#юvQϮiD&?%/fCa&;JY4|2Ͻm˟rعVAb@"x9* ,?t\kNְs^Bzv:HMߑ2@ cC].6`x [H*aEg]O$>.o>rE*Q FѯG!ZP4'db?\b^JQEپ=n$1P߷^kgwґ:iQgg=lڵX\zG{δ)(6&$V_tJ^QUqGeV}f!,'*[RX\4#X>!D$x)=uLz>NfA8sr0o2@_ 9i8v3Ue=Z$[;;mp٣Cl%9cXu2'7r8mMB&<\NM`UDʲư+c,!BE;˻3ҙG46_DQXǽ+.f=:Ǥbnqgs\$_'#wNHCl2u͈6©h]z^%#esr`vs>U} T|[.ef`u.h[>Sdª ddfdVgD(MGu,Ϻ^5%^ٹ2@ ҷr"u$Orb }|/s'􀇣ۼHy'OS?%|exN9Os(\NU (#.K;:?<)Y_F+|bLHS =6gMSa^kak^/AfhʸOSjuy jaD4IiaEkW% # P`|h3p IDATiݣ/(XQFyf&2Ü"2Ǿb;[brdu &Y[Y/G8Vg (j-^Iqr)*jjJf&ôo9?:Ir*-(w8Lĵo/50ꮽ쯓c/Flz0=newl;`v I彁.clnĘʸ5'9q.jip>X =6̪pItcO%/V\)oR#GY[w׻==Xl'Xnqcަ,9ᗳ(̗71V\JA!L0XXMynw>,{b~ԇ[V$d>]&wNm&`f>]l^84f«9u!UJxkL hdc]>7O{^ƅTx8ԐZ[Ki %1mE$Q쵰mq}V1j$2ȇ#`BW7n(i!yxlt32ZMwQULO I J  bUwE]۪k_ݢe].(*^jHB $$3vg~̤dHysez7G*cO[JrfMB6҉OrI[@;7ŮP3m<do1^鸞+sI l6ౕ{x\/Kɽ56:^F@ ފ;H<6h0X?e?\)`dNJW.R x,0cvȃU!2 Ȩ`XEm`}dqXUtX_ w) c y蓫l`ލ5t_5/-V9¶xjb:U)X,&]'eQva:kY(~:,-,--NM r'd0>ϥmsփ6WUDmr$+`l*K%]bpA(%95ʇ]0x#Z[|4(CQTKuJھ܁+ˀf}( [FT$; CYØle8>?G"SEl8;Kᝩ0Qpü ]^77a1Uxߓ6g˨mr )τ1]y<6-,Ǟ<Ӡ[$Wa!iuP8L8h;c.R=ߔ`4ajÜZ)蓕zy[uA53sq☓|A@Uǘ1ߧrltި,2`*vi?nL;(/|#LK7SaX̷E |:@4Q *Yje\j!1΃6-D- <ڷ ?wcr` |WFM#22 6rv=-w}\AvK>!Ts3>\Xō K Y8RlHY9|yjn?KulA|Jxo?bxÃZ_~kGJ7f-0]9S8j,Z(ZΘA|UAg!Xϭͽ, l҈fido>p·NDzFH:Y6׭uѮP3v*=pgL @ z'"uLG(!̏`G$D6=|Y9 ITC]Ǡ)>/l].Ÿ~˙l@v!q-N}zX-iEL\?JGR#pj薲ToիSn)FI7aL9G᫰vyvfV pЖ΢z_Qx4]21ix+2ugM0G98O/G5ӡ+I"uEOI[tgTvpHz<ʩc!욇~_@Aئ'v3dQ:$ OgC- J; 3qLxu`C4{GZ Ts}7Qh4!p\K gS&;Rthl9&jtTDr:|; tA(#O)|s|o6;EoHOZ::|ux_Ll&hST7Oj{=aLHT1a,{$Ƈ˦l:nljP.[OW5C!i j%X\t@ ! hmHؼYqC })@W ?7\7_K5܃N0#N oWKQ~UUo2m]p2Py?KvSb4fRĒi 4 ufr}QA<Ov ݆bGD\twuSb$FQz [KᑐvT!𻮊jdQJ s~ߍ`hGz#]9$an>1WSչf~nȬE1=)sl)}:|=B Dh,z+%B{WnLd'eD  nWlsֆ-/Lrq!GsCBNe:\N(Xc)w9.[~~@fŊk6*E*zrZr" )'Q*•D ӜgGYlN[NI\/:*McrTC_<1'yKO,O|.zw"**᏷c9ǰ]*7~ 7 l!ۿ39VTrp%ovZvRvB_IQOJ]<ؑIz|lȶ=ʉ4׻&<¿;"j7|᭔a8ew^t@ !QedopMʞ>̞`̇[ڷ(ZtWF*DU>LSOӑU -hHu,(jr/g=}vλ, ~pH Վ皓!.Ӥ/rߍO$TzlpR>3SGJz!S~L"T;γЮK\@?&T*A=A}VD_?eH yϗm(a\XW܏h&Ϡ" kaAXҐ 9`]3 eua{W)پJ$ (!h//#yȧJG:؅lL5)\!;Cjrn|4!y$|k~SxnjךIQΆUlXɾt32/Բjnuajڎ—Kp~(gu[VlEKQQR V/)[[▻xr*682TYmy&e.fsBV| m!=ۭL| @ zBho'9W\b {gu)_KN`Tݕ\6$Qc ur JC9dwY}W0H'j"V ٶV*Rumr De[:lݛ"Q`QűϤ ~^#|:l!y}{_dDE{̆!@ClNJU٠XTfqF*j5l^* x΋+$?5 IXͣ@"+N[>q7fur4Sed%L\ħG8>J&qr)^g.XLSH>\V˹:myW|neC|\[˲,}96?zuM*|R y}ʝK`3H @лB{{}c/ FzTEL wMddIV])X?bؼӧτWI& {q<3wN>|e/B!6BZaGy*]Y+5ZoCѧKVV5B 0qhѽ?wwZā E}mg]>klS={˴\t֎̫32ZXBɥYWn螈N|]zui" &-Mu|zȲ6j)&Jp:F#j*&S(E_a74>@ˮ8X6Ql`lXS4=V/9czH wZ]#tRq?tZl ;N72( F|_uGp@Hw3V#7Get6XSJ~0=9LVXysWK{(5'4nr?H P:sː w]t F!U[]ɦV־tƱg;Qjsgmq7`P~K v5kp퓪c'.3L_g N )ݜ- 2f\.k*dε$+/5:LJ@  !{D|"X_wXB倮GOQ0)z*m[;4 zKS9ݳ} ~6un6W9 b. ]4kߩUTKM.J](>#@{eT s%Dx+6 gGӡLAfXɋκ6wb__]:Fb|5E|^UvG8$ Gs]ZBi{,UfJKMNְ\_|ą|_ (5DȏaD`pjEQ3?IҗgYw3ڽľ&#BwhH2Izm_s(-@ .bD|L/VgupC8Ul_% eOTš S Ե{BIPEO!⏟= uW;t5{JwuZrxp ^PaAtthEs]_ &;9`S<Ķ|V4:tgk/R]#QZN6P^9\ѭ>Izm]ec<t*nɟ" AAyy{rw3NҪU ]1;_?Go;Sv@q|3j>eτܢg"xR Kus[;e1+;|5g?)©?A%{vO,ZcI vmk!ܷ~aVXNa#A!0ճiuTH#6c}ۺ77D;_H <Յd^X %wse~ZQ{ Y"? cGg&h<\^ك0rMO&ǰXeNXg xge{-_!f0#x`hwTCǵ}zWE \|EaP@ÿTq {? gn oęZ~]K>}8O;q"@j׀!1{';?|Z]%3 /a|]Rv)j"i / k<>.hy >Ų}xEcZҏGN"Sޟ;RP-+/*8R bx/B6,Yj){  ţdcVe$@ p2]S0) <ˉqOV_Ø~;F17Uώ5Z' j;GCfcTZdrGpdގc7qC*|' e O~?gR8oLPd!m]՝qnH o vD\aKe7Zq<[|u-Na/OgU%MuJV K y6cBBJ9 _`jEЏ I+QФ7i0W4*PV{-dXoPnR1H2cݞ +`I4 ֞:@7lm@qOgah`<+-sc9ciTDH?Li\xm@WCoWgmsܛnmOa|6Z_t 0FuJʹ73IzE k=iNl7[lJ;2y&/m k"vA1}[+w0_;[^-g͔iMGUTf*6#O_%r̢Oy ;A`FVIRu#_7՛ð0 -e@=Q 1`?AHPQ'\7sHɳ+as( "HERYB(O(;?]ڟa9Hާ`FkI!z"Rwl!$IZ >/`=Ӟ!2O݃D '&޿#t޼fF@7uhUjs⟇6D? ;[ WlAAe1ңcq"]y-ں'iFzm]iljP\HsP??Ft$U5ǦQwL}۹] 4ޟ~,A^ m7a-7Q&[9a\燸kWwbW()FIx+dxue޴5pH~j'iUAj"\|ne$@ {XYM/un%Hha}{E*v<5'3QF8n]oŖgN(05ޘk.i.;D>; _ s^]HY S?'*n9˄#6 e6\Rཀྵp[pJ|dC`u*&F6< kSx"PJVVH\Jk c8s0F_ sM9%'8R#rZaee ~=ąhzX,ו+a@X3r>ɰ' u&‹]7 $ RJ,>%k xX,3|Xƾ7cPkth7v8|7b[Ά:j C7 0a[1Χ4ɀ"@+| {" ״LbW$AB ՅTڠ_:n lawbҟb *ck (Α#}1 _45?i:GokhR#{ޮ@p~PUl 2\e)XvL]p-(wrr#cY"@ @)2ުf֮]˪UΗM­J|\ڞ%6{EX-JvV`?ESJ\F ;oBh7Y1U0G-B%Ro$O~v2=|3: ?|2=J9CH Rj┗F|V-Y&{'`FqZO ߐ6CoK' gk.3cF~Я[(3ʱ%4KnW)@ 1> ]mÏpl~j;!0ԃ2JӵW0h`πg=v\D&CXk:bc s #>ouL?am#H$]JT1ੁp&D+i,+pm<dE6JDO,kjly0/r;Y_^ڟ@ӈ1Q~J[ݤFӢ ʯ]dBxFkzrSriɊo;G Nn!-XNFlv >Is]5%5C[Fݶ _oBIР=y;Lp1;ia^%C)Q0 CyjV~<dnUBd@ z._=ʶdcq?ttR 8Ha>^Šgt!/wnΕ?j}hkHJb3n<Յ>/,bR Uspπ@ @@ !Y -2P]Gк,v|RQÝÚ-,8[sߏe5tWzA10_͇=cQ#v|3WaS$\XL2lvx]vv֬Yf-zB@ ~uvRpDDO%ԁL&tWO"@ d2XvmO!` >!  s_ԣcՎMZ ր>Cz,A/\N!ױu@ m,-ť" ^ӭv7\[P+VX4 lwf;'qhF L[)8_3{<Ŧ;$ɡ>I \erH28! !֝65> h cݯ+nQG3*֛<%q 5Q|?<4ړd;hO6[{ޯM0(([ۛ _/IצA^J1Dp1siJ^ ^ΖTwJ97ҧy;[]nDN@7 ՛•m'B;FGa4ՃVv kffzEñ&CF2~}g-{҇ؾqIȒiLR=E _(p^׼ďn//͝l 9H-o^~o3zM3ω(Oh~=gAW#'q۟wq΀n<;5QL4NĨAn.7w,8ovo+;~ |Skui*|pv nlo L=#y7x0)]׎6Ls$?J䎞U|#riy3\Ӧtfae¯(F`(6PtF99 YGp'ղ}\;UGQ6\mX&J e|qPWv*{ -H响q? 3(>]fwֱIqaNO' A7k3|^r>Q[L"c!o~CȋWX+ܻ] ^p7ṛ[]nk9*&­c.v…'vVEů.Q¯m֯dhA=#l%G v} 33mvC_ J]yEoڰm=#aYG3Fz+ ڂ aO!O/Az*bCqtW06ۊa2rbQT-ކNᓊ!&2ݒvW(Goy+3ō]Y;s~֨T,c$7Y%Ҭ[(k.o瞥ŜyAɧv$qˏ6AQ s'3SX0|]\w1/qS7+_c`_޸+n,#7ȍj)# ܂&r``.bƒܛs0S*O Gz\ 0٨V{D%IEud27Ap> 2`Ew7PbC,oeRz0VF.6os61٠;7VYωTG:BL6i]@:?OSg$ oLtaYea/UʃÓxh`!m.b|7т.`6дɂQ<e*F2jeݍaYK iqO粘l`ijk=x:b|;q>1cMŬOʦ],di*)lLՎ:gA/D^x ⢊sfSq i|禱|hI]S;e;7w,?=WKQbK_Ǻ9\!85/2@~Mwq8rsxw!&l>MO Nך5joJX@f?ϳ.7ǹn|s|lømLF vu^nʱ=yWwW !{vf8ؘWzx[ 1_PnUS0bpe 00e_.]++хD‡del RRMu}*Dq0Qbv}ոUBt>pFW谿٭J%6(# 4 x-j ?Ԛ"*N@TAx3س 2G#85'Ȕ5 ߝ (=xiq}UVd1kaNrBr,Xশ٘Av%0.q,Q^1biP Fbo|Ng8<> 70d1"g:= BV[p+(=\aJ<8Ƹˑlcچ3|Y@-:9pO<ĵ|78x*Y#BjKaWWLje$}baQP+1~͏@P\. P;xco`Ç 7vu4H', 6QyROCT2_u,(5uώQ2N&dz쫆a8n X;)3)^AP(at(v(\ͳ'N vԛwn&>yCFҳ^O2<3c_.+(M:LpLʬv>k8g(<_X{ĽyrXU y\޽s"XLj!*#ɬͺntcϔ|߀|xDVq{gwM$2>FF65oYl}x<3V3z3[щS++n a1gׅ6uI<6:\>W8@`wM"K7p>.C_O+їg٘1\֛6ptw;a;g8=¶oO`BXN5L,!ՊS,9;'o9z1C/- $oGؖXfHgi7m.yK/By]FlYE,|V9-/lbS.+7fn:CY&-8W&]4s7H<2to:g<6-پ3B eGrCjCj$Nݎ{R0:Cj9$/~gBy~=Gl_]PO{S¡7!L~/n9P`ؼ^9.L&pUdg+&q=,-5ӹ7q-! Vl÷3[Y%W',#謉mU+v6rJFCD,PM͂+o;~Mw¦xGeϓGFjm< *_L 1Uf=|wyF2%~$'Ztw}(J-q7(8s8B$l¯~Nqh Ե-܍veG|U BȊj0&W5~QT@w "tyer[M~G m˜sE /`q*Pˀ?c$P/LfN1˂p_ ycEވ> T/IʵC$<ƌ w]/kd;LbYu?eꊪae:l r=* Xp? IDATi@%I&Wa䎅i|oIV8VZ\{$,IUXF(aٽ>xǫP;m$d4Xʩ˜l{# EK([IS%Yå/e燕"k2ӹtP9'HHD僗҈QW2! ?r)1^D僯 rxg2=gJddIVpF>֋ Cf5/"xs",hcr 虹8I #44M~@5JdłӖZؒG.VVGr9gF!$#ix7ݧW\<,* 󪱮 E=s ͯw{}g)MHh|5ݗɇS/C|2B01@E/MԒo e~9+ +1aΖPzAf j||֟  $d6lR2K1 P(@YWn)F (!1=]~o7?|j0hW΍rӘwXTW?Ӏ:R{I4$FMl$dS7_z)^5{b *A}`vaPbyxfΜs=5hi_rƟ39Y5χ?2g`̲2ka Y?g\xsչgIƅwg0XW:.䟷PMOmZSz4=|jjiǔ<LUΰE/89zesQpv.mNR1튨/zeB0K^͝<ϋL壽1NrǔǫyZFC])HHɷݵ6Ï6/ߨl|7NTjmjz+p <]Q342rh{ܾ8I:P]K>~hӁ7?6W}v1-Ux85n{ |dT ^_uT6ԥ|zbuIH|ڤ KdG$cxh1U)|Kj)ڈ9ٶ1"~Y h> :cb)ݳlٖ5F _eZ53x$Ne9VϜC{ز2vG?c?sy=,FK2r{vQޤ}2Js;ʮ[T=L̦d#b.?]ZBmX02}Af$sLJ\-Hue.޾7|l^rJma'mR;U|.2['?˭v c[Bt5̼mUB◎4k> 0audi<,aI0!S5VWj)J3A92mz@Ba.W'yXMj`_04w;JqXwQv-`؁iL)9o?bP4ϐMT8î(,d"_\pK_wAprٮD'#~=9\H~\i`saGwcf9bIGELqqd~'eP84\a\,d|>lpU^Dዅn!욋.u3?Ed`gAo,,z1M4+< *Ês; Sՠ qۚj._~d*|lK:zs Sc;O_k6] JEa3`{R^9'Lfy<& ~A=+mQ14N5?7 Rb8݋HaV;.tLٻ7*cOo]3zAhk4{9;9o3hڽ;o,7_+KnFl᯽LB◂!>R1q(a$Z5rSK]JG-ƿ[;t9UJߩ=Wc1Q f|;EoBV٫q͙MoJwrӧvpy0-٥ppX3vq >4abL b2W|}[<4? ^]2{#!99FDas *:2?;K 'x#>":7~ xzGqxV C+;V%$.$.#oj(or0* Oel1S2NTVpЂ{U2Y\k]2~p=Oˉ^:gci쀍5hrqT%zwp˗c@T2\$QwaAh|ȏF`4CF9'g=dT vCɣ? ="X2 /`(tLgBϿʰ 9VjcUpQdj7:c!8?q^ˢE@em#yd5+7+oՉn9Q/_Rܳ)ml|yn<'9`oդ,u AlES8P>Sm#KԧІv4U7{ M Fi!#)kya#^>§0\ȕ49_1w7X ~>g9O5uv^ّ*!q) `+sȭ3&tYЙ0?s!JJL-Z>Uwk5$^яo2!-֑ê$  85|5j)^oLURWw+`doD͐J8lnJ Zj3ȇDLڹ b8N9,CfF%v*bn2:-DXC5=dоTf_\r3T%r ]4v&X KPc"`2Pչ$ƥ5\Dv}쪁ZѺ/uU ΌrV $2nGwm|(ky5 H(յ-/@a@F 4Ltepszuǃ?gchi/{ˉ`NRbgx;' NP1j\ۆYc+料-Bl }kgG0 aI<{&#U:<# !%65bdC&O` )8ɺLFrSf_qn` ~;L5Ͽ%yf\BdWd[Lr9Lb(Î̽٩"ȌTy h(rqz׃|6uJmG ]~ (D,Sl r&#;bkU뒸uMõ]xtϢ( ~\~\>l#^/||-z][[:T;DވØO?_DF2]gBfdm#UfʉaxcU`'dAPݸk~7J7Hz (31+)52s c(ÝȄ`nryK P1r <ԏvE ՜z^~ć-ov};8#xD/ṉohkEZ:"s7zUQ[&4N`S7$a W\Ϊc3)"O+J _ dRܣzHQQ\,U+46&>-Lmܦ4RZaՉjNVAk#c2 %EϲU{uhp;*N8p._\9uC=a+ɉV s*$hTxA'9a&8A cl bp 9j uǬ0"\{PSB۬* ѹa >RP Gww W;xNEnk[d,s›rt1A;zxeG&jNOtmAe'pv ugmeL 从HvĬ:ݺyZ,yv/O:,pF/+<&J՝Na1! fOns%MY>q37Q/"^ ٞ Fвf/qSGkPp)3bAR3'#hdu40'pr=%abnzil7 j26|-/t!`Ўbe۟!`}0Z=HXk&3|N>)ɰ**L2lvQ{Lt@"vyvIe~L0}i̢!0l #U}ݫRBԦg5FI7RΘTq|0ǚy<~E۞~ jZU_T/h/{T~o U]cfN$k?/~NcAn-Jh)Li:y^=4h z`#30בr. NƌͶMٜ9iЋ,MkDLP1|`9zibBh"eڈ#Uk: ݁&l?-uoaVRaXSH*rDo7#޳h,rGUk:7Ve TmƦA{u@EoNM# +NF-E< Uf(7CTq˯\=XZڽ\>DY= "`r0L'꧇Za?U~$/fa |7ʉ7s_amis4T1-XL ڲr1e[oX5_ M^nN5ڝ,w #QV80y 5SS!%luCpxdڭk>/+P6ndVَzEV ۽x'T,>/t'`57%*Ψ"( Iu=3pzz FW_j<|&MvvEYM2T$+ FZ/=QoD,}[8 e)֓d9^5?^^D06Qg /O.g0 8(3&h42 n#9e8rS1l-okQ;l(cx?^1ʖ >?t`;m}Ne %Yֱ9uI1HƷ*+~62~L˷Y)RUC/h0\0V[DnB0g;x6RN Y96#*kB 22=)9TE;80B+%$~-Hv7!BTL) H 18˂xT(cJZj@m2 B[d7P%,2^WV\%kDv1Y՛D#/T1*} j݊˟‹]/'3efy_^ sia]3/ cҸPtPH_}8 >LBziw^0'kKD)To(;$T64!_]so{69 z }~L!}_uC(u[Sm #3GpFR:Bԙv!-QU\t ;[ *NgGy #wS05K?d6js?ɱAŠ ;oe AfGmbԨ8td#Fh}wxt_"@:z,hCEs/A{P5X}>Tz EF IDATN{j2+bض7ϕ R8.@K(4,HOY"N wQ,7K1OnoǒG(3251 bUXh2VX@Cvzv\ro.@ ~2.+*%; Xhx1y*kdV 퓥hO{8sQTx f֢cf .Y@8OdНLaY-oۓG_xȷӮIB D =sƒMBb/7|ͩ!el?EJJgUZ~Js9!]}Go<,ˆ~z*T޷% =QFO= ufMFb'Z57Kh7.CtFy v{qj(+o?`#|<~NO{4hg$$~є܇HC@~PB2Av9%)uŢ|O]gt-ؽVńaW@6Oߗv5l&"ǟsG7(Ý~]muW?7ϧǹH 0qI,Yek͈ GIRoZ彔Lq׼RB׀dhw NRVJRjA5vr|'C"*66x,DGy|PBHypIknΈNg}b[gai= .+0V7JyĞAPLPkvN2CXlbwdxv'ؽPN^[^RJk gSi^)!k@2 BP)_`H~]8*52x9zdl)(NIu"CV %tR9?ԔpU( Nߖ[hX#G< D)=筵xG^)J,mGP(}pWKhv1x67ڢXϸ',UWaȝj=opjd_Fy7ӁA̛&s²pJ7m-gW[h\NCu0@N|ޝHXCNeoY CUPj\-+ºlԕseupb_sZ Jk4+JhA j9 Q2#T@}u#; ǝGl27 ӪTV=um4-psw7Rp2^ԙg5]`3l9,%t~q geYj)){bpΰ.b^ԦKy)|f7h)[I3_?D/c#'>Uuݫ>)@;O gv5 7or[NqR'یť*Q Zv%] ]-CQ(4`P'W#^{/z<2X%?Db?;6R yV~jqΧ:ǤaSu:Ey)'bacbII9ԑt;: uɔ`”@Z {)69zv  <8]{|xc[$3 0U"Yf'CHDy'x,zU0iAK=wvhkSg:QZ ^Ѓ,V[M gå\ Yaƻ ryw)^g "#-?e-~ qWzNPzYw+6~FsI9Ilqj^Q 6ḓH}t+Y)t-3244x9j |WjC c> o@Q2=&0W ZEm7*f\ 90y ^ʀ5.Ƈ `\Y--`J1,:@sá'އeφtb]GOYaӵT3 8Kd].z.{(xz8nhx y['qaQd@L*b&~hҦ׶:*7sxI~ZQyM M1D9vןl}oC:9{#\(0I~1ֿuMhd(^])[o#Y6Yۃ<6 䭯ҩ_}M00k~cH^~할в,Fuۭ3 >NT71|L5p> r((yd$\Y Eo쭄Cp$5(d<vaf?d嶮we;C K\В'"L2pS?#,,XwCTr~u<ÄA2r3X{oޕIk(dZ&joj(ace{<ƌcu;j(*&PȚ<G1l <_>iH."hѮ-O\ۏaM}}DFGk c6kRTm&37=Mm;"r)M m,D˪JJ) ere6{هiT}B&lkr x&.o?&!^l ?kIȄ`n2>{yU>H/ ہ%t*O1LߤIr_%UY|;.RNqp =4>1lnD' E$h7C`I uݡ1 oM Jj!J\sHeA(ӐDO XB }`v0QI_P 3 DIx1&00oƬE0'} ŒH) <୑ z h1gZOjOt)..fr8g~0؋~zSR?ˁV 2x dܠJҮŶQÚ]+98^w aaL%{D#X oN(uab X,ԒZ%`ܶHDx T` LjKp<$% ⣹ԻkWu%y55'LFゖc)Z?0䥘S` `_x/9^%|DŽp~:2Uv)A(TR1ҥĕ3eJ3qJyq29c]:FPz)J `~\ MFr=ϛCedCp0T9 f L;c5~X>`X9Ӭp_D|`N@xTӱǴٝҭva7_rq?3E&c|d3kj\G>3&0kkˮ` [Vu),5xn7z1ռQDŒ͋o?;!+8Lc}RFܡ_kO@D[;edh W*bFXA޶?eoq++tf%UT@U\2*WSaStFM^dz"H '֝UT3[[FCeX[};9g.w#,߱Ͻgme^{cfضYx~ +FoLܐċ'o,_rtT5ϩ]Q25ST$,#6HGuK4 E:_Y맊U Geg>%f",elٵ-0'[tԥn.dFN|dLĠe0i۳ MǴ&se-r&2Aeǵ/s{F8&glnq|2w w+#h]Riu=<=W-j!"J3 l! P=!~`|xH0/30LE^00x} < :OO0v^)PNG`DscܧpmO QRyo2Eh,cdJd82jNLBb?6%8R _QL@LvxG :ylSo /ElK%_F xq\tPE_Ng] EϥpWZno'AbJ4DNzo-lol.\{E;wyu<\OQFv+@[͂#Կm\4Z3=1t,U yh?9OцR0m |6 xb7f?R!϶k (^q`PxH"C%Q}% /v: ÇY~4x_| =|oMQ:ؐOź|rK"޸}}_6R֘/`j"o^{1Uږ \tn]YGOb%69YBT(^v;PWGNL^zzh eż|FF<^7^՚f&Q,]p'VO$&|&NT*[x#$D|֤Ɵ@{Idn1\1}ir!:6gn}$tG|yAI8sZ/&`ݫe/~ǃ]#Ïo}]7\~s-*'ʇ{Mye[mUBRA&BK5kְn:>\y 8(-eՒ 5PPĀs: JT A}LʬcdW2 mϼ" ((mhW멾hb&CN꨾h$!ƧHm0rZ@I7"BZJJ ~v^~9:Jkj.NQEǥg /1੄i66[Pb$ߣgW멿PON@WAOzw _Ֆ4P ~Nѝ @t0m0%Di:y~sb61\-eZ ~0ػiӳZFò5w4<4t }k;p"#xiPրƟp'A^ǚ R {|umQ:#8If#AaOKnCmK4Tc1(7h-|j=51aw HII9:t2}'fۢA[HFzNHqe5z<ka@iIH;%R߆]}NeM^CWs{޳^KF~ը wL*~4))(Ə`/mZDЕQh@OXp/+V%$⹳xG1bDoW]H͹Ɋ념a Wø ݹ95ވ !};_/bDJ^=7NbL~ޫ.¸*YNF[Q!'?춷95rj{`;+Juۑvwe >eoFE9䈐p#2 QMM҉SGL+v@-W]qIaQ$vP'Xײ(7 & d(7`~FvOƴ6嗫z$$$NWSAu;mFv\5}G܎mq9K";ː{*]WJH݊'6d>$'!+`<<H`@::[j.u˒lBK 4AkEMv!s$a!!!!!y$v _5[EB~| ӓgamdK$$$ZGi aj>=`JGgϢG.8UsHHHHHH}ɧZdFA,2O%Ԕ!yKHHʑ1$Nx+$!+v$IDR޻%$$$$$<71IBBBBBHC)G9$C,ˡÛW'0S?~Lk%.h|aLf5ћu(pS(Qz+qWX+&ʲ=W/<ǻy]BBBBLԴ|;k!!!!!qҦ9((uu{e] K6 gf=Q nԩSKH<{EPruC42V3/ީD+hOY) @A< op˛9١o0a,Z : nG_G 4g ;%6dF KW/d.W01PWFj.Ŀ>u2N.նLSj:{׸i mOōE)bw{* [X{I&>yl1!ÏQ^m?q^ FOT8mà Wb$?/w|⧟dd)d[H]Dyw8A|pPbƜِuˑޮĥ"՗g 3+,z`ȸau,-?#رZnM`|2%3Aplk9{w晋TrK_x)͒2`[vIЌ[-):Y|s?{Օ>f(* QP`{I&$1fM6f7ɦlڦlͦ''DDXP*e@1Hui ɞvT~qll-QҔL'Ikir't⪒KQF"Q%7%b.&5ag)%"2}# &2R2>d):l?eRXf#-DZ(`ض$v{]neST5pf~U۽sHjNnӡ?7lJ&Yw`$srs {vЛYU2&VoOw3NgdFtүk3c?fI?>@Ӱ-\ev@ 7R@CiX]jdrLnsXg(? I+*A]Ø6 w, .6o/$xe-a͙s7gъNOGyd^ZEsY<ְ~t>޹5zҊ̊* ţb\HǑLuGL^:2F-86-[ؿ+7x6"B5r2 iYbu;,3&PiT,4[;7Ue],ґVPF@D̑7o_W5DY*Ͻ ElXkVCv,/ΟJ})l:q+>G0kx u3$sy259[>9Xe-g>9ig(SGÓyu$f cwܼ/(^zFBw|ZI!sgsPʴD0g\:`Ί},AϚ8'Ѳ܏sB2N"BUe2T'+כ?_|{H~f7=q-.UM*]OoZOHe Gn#=UH^y.GEqIq7Lb*Ze |/uyd۔14$oZdɆ@8sRw잌(/Pbu7,+Ŏ~S!s#"cXtmYk{<_\ VU?9Wg3}*0d'Q|V֪p<9 t|%UլH֟é_p@pJKL ibjx]P NGF,O=?]3gmRLM6I:>{RYkx*M5زlW>ࡒ?t Pc+5rSYh!cqI*ەUg"}.#@ixpc2÷5o8sz%"L~95<n!(}EɓzNQXgfe]')QRׇ,Y)H% <Z,,$L!N@djmX$c5!Y}wV$*sOVY1RE$vfI9vpfV52_I ~aa1ajo.fO˲|JfLÍtv+Ϯ}eUtƕ~y]6pQҧx=8NVM&g%r8+_?! c,XV@QR)c|s3R߽nSi7sJO ``7_|lfI8b$HL{q !Y/THsϫk=ƮQCrClʢǗp?ZJY {cuI$g\g$O|8;i3xKV lJ)i<<&,H_.78@G 55OEpl2gۻLٔUi&Rc伉c&f$>mܬv\c#Y#%J^0{[ɚ Ab"Y/}ȩPW̑zb׼̺8|72o8 /KqaZLUl 5n+)8C2Jye=,$|6ޱa0~s'5;F:3tRfWu22r&37aYe.qj\/I?cG@[x{F㏳)hah@ pYe-DZùC1#wx#kxqAU3HY0w2U*K6 U{~J q5xxYS53!ʱ:zfU O0tsBGz^m/a8ŲYhs9Φd;9Q5{v\C@xyթFsϪ*g"˵r]<ĤJd˻3=Alc,)A"s!a{63;7/L2Wp&6mDU~H1hTaoZ+V2*u$/zה-/5yJwĵɠxD+UQy=?J%5d$.')FCĠ;Y>7#ffϸ{1}=B5e8%mdg)P']Գ 0Gr9='ן'hQ]^0ba"[Vt3d+G4-IJST4 4 6I%ncftëZpJ[[tɛq>UZMؗ#ƔeMS)=1:eԾeЦ<5ō4%K&: 6=Ð[Qag5oPqgt[XQ4;ԗ+z%قyy$_h;-_ک&ke-t>?]jH_,狋:^{zwFj}<; ="d88ټN>A^j?kI<) #"ɲ5ee iyl&Y<1r\]:e^ze^ ySX7VWKO%_Mϧb+q݇> I#}Y -ه20{WN&x 'Ch`BL%1)):PQFD.hظ;xB[.ر)C71q'Y- mMY ǜwU ν62SI8-UAVl(_BK-l#8_Gªخ?N鲫cIDv@ e @"kÒɼAJLӇXg:leƖֳ Pun!t\VLi+oX$/d:PbI &0< z.bFvvaq4>^ldlJd߮Hgm: YǠ w},NO'4k5FU}Wݲ^⹀傱­r"(FtV[رƗ1_ `#{fډ$k7( ]elGcO8n,:Tֽ#8{ aLЮr%l!Wy<'PKYߢ"q]#ئ/q߲ ^@®-)n$@;^Jr=>Z :|aÎT.=HKFLƊًHT[ď; UouY'̀_+RTƝ1!bOY| .O7ꂳ޶aҹ3E5!ڲ*xbC`rp[qtp=DεʸAظ:9dhiX]`Xbg=ys0J!eܹeZq@RE2oPgz(*eI kCХhn;JU]nV UN$/)0WWr;uK{染<|J*,? %0 Mۥ*r,C}^Qs 7˹1QGC Үk6Ua,0$>'ٍF*^xz'$s6 (ӸAl)ù GBO@˘u'hԫ^ٛ?M m-9˹;XQ?wr.m5g6;qDu\77}BV%'քKlRWO ,}uxiʿ'> 3* ~%ؐVYȒ:3̇Ҡ3l44A|}i,؈NgbxpUܹ?c3iW˹ə|Aj|IsC#6rNO DSqK^AJ޿cĹ:)p%Na&)@Ht:mrofF 2OHhp/B| fzqب+ I7UMCxE32@qLHYA<9|,'IJ=̬})!hnr-~mYl~KKwiv|]ɯedX8Ԋ-MKزm%EԵnXqQd#-[iZB5 92(;>i(#y2\֜iCW7:.IKpY!{i=-;WCTszvkLSK`gOd ^j2IPݿ7os&(ɸFp;'Is蚦{xcEezucNarbqt<~L IDAT:vWi[uȽWKU 4e@MTHh覔]nT=|}?F0zQ@=Չ 5Փ¼ḫ$ͼ/%3O/H珲zYB<7QX(u|]˶ _RC%F/Xڠns{ZB,]0泍IQO90@7tjQJ!K?k׳r BO)fuY8׆<5"JW*t y(s ;m'ch>VfBK,Wؚ}c,oO;$5-NSx;a>QL_sfVk5=?>3|Iԃ2ۃޣ(BM02XM F(:}T, du4Gb7JWq#s7`kxbSa+9y&4ygs2a܄Xoi|{6fѥ d([DEKEBg{vWe;G@ Za`YME ]XQ`w;)('b@:B>KV%o䷬G X@̫ǁ2@L޻YUv bO; {і`REuFYN!K67[4u G:yxJ9F(^~Kil/RxFǺ3: Ϋ7]rnP^:%4x{Rǫnk<+>#,}Z^gp UL2.䲥| \ΓތARѪ,YOkxTcIYۺ4K /$E]ٸd2{B a+_y3xz;ӮlnBMo`hՎ 2x`5&f('dΣ!Qd[ af_J!V4Qm%(nl2!SU_$P 7pl7Pf#sc5MN>ijS9=4n6kt9v0"vx^ {9bkזUcv@ Ry`+/Qn`SZ#jqp.BcwƠy>|2*)o^auBV|w8ZbFɻ3b&*? p(å,nxբ2Gn>>=KoM<2e!S,@GbN6t$ȫ4w|mLhr."kC|X,hr~iR R`ŘPt2~o9U}5eCgj-#G2Y8%6}Ze19acu>ȆD,Kef#15 %pfuHk6'RytTڧ}45nP0/s]+j6ɼTN6S+v!\0j#A-ёZj.vVҴmȬq5Gr|86y;*s!ai!&OװX^ 23Gx\ǎ,{M),H}OfX@sY=ktOUѴY%QqzVeI>š,=ESTc;iK'sɝFXy. lH26ʵ6e,SCѴ6S~ș.[C'M䕍:JN<:|5~W$;,aBq?q5ؿg] * ?nJQ=hs0auZd,w咢ʝ; a4rڣ%Bŏn׈]VYb vy1 K.^ïPeRTdƎ'_f3C@>j#WsE2+(YcgȺW=9KvmqLҰQr-!0#>Jk:UI)Ŏ];9qbRqT|^Lb 5aP<%YÈ0sٟe@@È5 dƾ=XmbV~G2Gay}xH_x*W.M⫂&Ow7R >/ìGu,-Ӕ&jus*ھ+5(-\=4GV]tmJ;dhx%G͂roOf͡n't`) M%>vv WS읔p:Fl%_"*n7ʒb'c\x2o,L83˿km`݈ r\ }|cR&ѨGP BG'Y[SI|ET gp-Sxpb%R "!H?g°m'Pr2g23ՍeH\RTWh-əw׉~۞g4wpl6eĈPkmO\9ah@ hRQ ˋ #fltB&<};v+1]N2zt'') ]c5s|VVDc~ .d]VZ7~$k# b>CY:1j,G)YJzw g`8Yd"&͙= 6t9zʦ4 r#4`G'|PvF9@k?7>[u?4@I%`.>v`0$H0z`y]إeք1__V[xGk%\sЊ:kZ,)߰t`H~⹂a&U| 1ׄh~"YGRC~dcO9xҏΎ6^j3~`JTn ɮ%"*R t_)< a@h"4WSw/pGOS6ڷ^/,زm78ET_ѡc"C~~e'tbp\SHn r|;I&'C.J_촡sDy=mhehз&Tx?%yϾ %>{!uN]꿽D։ޚ' ; ^S<ɉj:=:ձߕJn|s:>@a38 ^Q<>T[wM8@ 8%(s&6{d qg6uR{%i)lj8~ǹKʂdҰ+(`3x"+^ڲy@6Uꖯ.#FDHOyp#>Gd26 ]jMߎŪ"t3WwgG[8XRKEk19)]';S9݀MYl$&f2/рcE1z\`ӑeЮ}7uYz%'qxn M|QK1˛+>E~<Yʲun=TȘYdK)C˘ 'k6#?|wTw kkyb?O,VoFY"M|u[Q6jԶTb 3;< cF0%TĎm bAw{ek WB]+^Y[cܠ(-u7鼽i/nZ'Y֦ jOG( * Es|!jd3;?~49ڗPZ@ez2Uu~p?:>w2zjc"d: F5![s҉-,9ABNf@NVYHw+"DEѝMd-C9|شQ lbG-Hc > c^sNp&;;_6@"?,:׊cah@ pYұ<v]bSsI㘁+fZf1@9ydG* <ek?D^;G bc^nZȒ?2ڍ" )ٺ+F1%ʕFXZVU7L1oơm+x"n 3yaĹ.r47%U2W{.MዼrVܿ?e&`P`‘ 7wşh7 or W 9_we-Q<::ܩS˒S;S2؇r|?2 Wwן<&;-=y}$XʐRqN,ZMPA{UerW'/uxJƛA,|8 Bys%&t,%3ܕu+Щ$=W>h~)] N@9|tj`'T@wSv &~Qg9y+Q6\MpSS# %QV]&X[^25ʘߥL7pXz%hD9Z4Zf6ޱQ|fdŒլFq*?4U{0GN3ȘGLb`*1~lA267\ XL,x*AU ϘPt].+Q_V@MMn)6}qt_$V6.z'^D(a.QMT"EVYe>e  cw M);[:y=G3_Uyo}*F4dbYPRӔYH<XOQ7ιq{iJ[a4#@$w9 ZLq;HfQ6Y̑B>/ǝX/pLAa.OayFң2-{n`J'J9a8^c0K=1Oh78f2:'NRDO?LĒ5a&Uo|]Ϟpb|0 +oYlѡ05lKɁQx$*}&96@w`w{'2t0:'IIZB! fg_bKvwq3hn`3+V8׉?>*f/s_A)[R@G"'tVຠ,w =j!bX_Ojd #;$[+$)柳w22[YkM#'5x zaǢ௡Ⱦcz0{Nnr(Wԥ'F^;@Y.PǨ5!N̶ TʗǾ}v(Fu 2tl3[/۱wz?m W7*#h ٵӡt`@S_=OG䦤cQ/ UY,S[HGO3cYJ)d1My&'#ٗ_3b|*4#c6J,'T̸FG.圠2É \XGRoc0{'rh6WsAt{5ef;Ssw؅"e䚃 gح d8tR^uègГi=$h RI*KvL&U0{z$/lQ~f`hxPz))jQ 58^y?snv?ь.;v Fx @ 8ab<=CO(K`#v v͏󮛙R=$>Q@܈;9G+J~P{BERYʞsL !c8j8'[Ȱ~-l !GG/h>7ЛjgOh*5aU7r[NȁSX~ܪJ;%֞{Н{6fҙD* |os{Iv>[ꂁ),ߓȲ)l3q7;,f330ZZ䎱{B$DNǁ!H$9d̙ө,߽9¸nm⒟^;&_vxЏ5Zɢ# O |6 J@|I|}uEsG'UE0s * xC4. u[}#%'e0b֭>XL=I6 _= ݴD͙ZOdط F2w;B(h0>YOػ`slV_9,J[xo*̕t͓ d6DswJ {1Q#!"'ۜ$k2}tj$h*?f, bul٫e2*;pƘ1ii"xrSď C)ޅǒXpG셿aM|'otVyLJ>FZΉsx q@OqFe^Q6ۻl2DDNfߞ~%}#Ihַ~X?*.4  @ mZ~Id;J븖(=eL%UKuY:bASK`0 ̫w=Gs#{Cn?ь.;v F6jq@ K<{ }7_wHytl@ntB9lĩ l݈ σ_G-ޗ3U^>~uXaDaq]Ku'K6.ڐ5> &cvV{տtLV д÷cg5rMrф j?֧g\Io50|„ǀל1J:)K6.?b3a& \5BO4f#3Vv fybo|l'q=55l!x‹AӮs-^UX7DCPYѕR$#c}]23_MAD_-f+50m+95t{@v\Ȓ3 0УC#uRfFwƄV_UI6NtF[hIK r_W>'ew6$B 矧o߾W@@  @FXRR֠]] ڈ/2$/$63ש<+hhc@ @2?ՀjFv@ 3 LNFvAd@ @ hefYi-f3?G }JLVuyuymouY1] @ -B;/g/-?@ 5F>~ahy\m] @ -1#\m1@ h5*_nqnڂ~d@ @ @ 0 @ @ @.@ @ C@ @ @ n @ @ @ j @ h zVɏ}TU;6 I{aqt*D]i" eɆAKrP&\ ^xB_"ڋ@pMQCk8#5u3;oˇ{\%!_0uLFC=׼ 4 [V3"ʈW[(@.@pRQa& (vY3Neʓ0tpL'2~ׅ0X[cTU *^m85h/2;(V2d/>aiEȈG9>'<^hK]-G$_zG<8aS/Nc+6 ! @ -Dlm߫~c#"ӛ2|DGM>£mdI?'@ˀpn(:lկG@/soe3ReO[κV95mڙ6 8^X*5hjhO] kt+7߃SsF4y}u+£] InO\JJseG>`dGyZ[۾eY9->G Piˆ2ѷa/Ye!5 n68]^`{؟-o1:ДƬu瞦<Ҷwdc 0BZF@]ս~߲U1q=KDp}my{ɮ9e?k)l3s˗#ܵObUTvkiXϗ~Z^OgGb0k(>>XL9g+- w櫩\m<E .[4lǠ`vYAFmܭ;O%>{[)&TeMX$!&KO1#b">uWIR-ע~{iamg4NlOdͿ)>޾

qzQGK|+yUYw~ ĸ/1l4UwçUu4]{Mfy/r2"d3qR;t\UA1%z9CчaMlWԩ[:HGÜ9?>i˱.͝I#*>Cs$#v[Dl>VOH LѠY>r8V ,pH͌<|Kv@o/i4BEqZJҸx1ԍ)9QkG3GO8zլ)33J4@՜IųBKJXi.ƥcC׫,m:!,}wróB4⮜b0gzl 7˗[KH95`ZGziJ!>9 a>m۹=؅f" -/;.mZ»Q$LduOPh1IW-s;Ѻ3:3 <ˮYo{Xy)|,h RӸ*YS3\l 77HN$Iڽ 9|EˁW#eop-Ųv4pfDxa6{f e[!4 !BC#hԷ1me,X*@c[F}7{\?,a)_~ZJ6SG7/ *]&8;u Gfa3#[,Ld\7qN<ҝZf- Y6uv.Ǿ/K6#f#Y*Ff'(lOs{@My,Iӱ[m"6RvgKumN:--ܡ DF-JV}>'&2)L4cQľھؽQŋW0Lᆪ'}zJ),}!M?x 5JGHO0,M g .^aгkN{;/g{6^Ԗ}nWpUgE1y0럘Ӗ - Y(}Ic37}t?tŃ0>6歆1C1Slgìzq k-׏3py!1c6Lݠwɛh}3IF¶Z[ޭ~s/ R0L׾XrQ\Bi_A!]ݱXw4%5˽qÖ8~ϡK#3~}oo:FƓ|@ ֙Olg-`-3+KHjo~n ]G)@%=A1t19A^ޭ=tk cI=8k'~ *nے@:(˙ػ/VCsّVFg㛘\.>iؗ}qsѐFzz IFz!0-=͘8AI>᡹i}O%b,VeqA=U/gX#o:4Yu]Z,JΩtMWXh'7Axd$ѹAvz]}]K?{A^G=5aXሒm=0->f@LgF0NޓTJ~> (VɗŇIF1"*߿Wˆk8uO 4{LDTD8v>ګ镽UFz !BU]a\tglj`v c=SOycPMdk7-z0خcl@˃=ke,+# qfMX4^K:\Ν~`|j'^5 _mڞ6-׽c@KnV@S/=%1*;s3҈O쎟*DbWUGhi  |yy-\%D {lZ'|"6g?/OēpqoQ|}h #"a.OF˙h@fҸRI{͠E6|Õ+ʭ~ϘWf]x.d9Ӽ y{ Y N)>GtcW?RdVRk5 P2B!DHpvBMmFܟaRtMKW"Sg؉%ѻURCMݭNK8E`;鹶'-g/$r]xZ5jf;{gg yxVs]|ཕ&|vS--kPxɛN8]xWȀfG\{Kڦ0`9g[®)8lz vO΅ןe=Ʊt:\[m5-Y;^B{U_y\;BN -ݎS#XZmZ۰ͭq\hr:Pm5Fi%jHZVORńQ)O|IUL;K])a4&IW˯ ӡWQCͮrYrf/0HH\xw1pcN^QTDK{-m4DdωH+"oҤ-vނEpO3|CgL]B>P bAE!=څB!ʉb[1jq)'{ WtT `B),\]CK\bgV(,. WSS&'-53L(hF|KќIL!&ҳ({hb]X!3*;`e-4*Qeg[jhyu?( [ӦA}=.wb{;3mHl+I9i2QyvmyF+;^B{U_YM4OHW"^>FF]֛۴c_(hq9y KpJ58jS,֥Ų~֜&09 &*y/D H]!(iquE?贖^WJOX>}oOBv-+U`Uf/tkѿOUL2g BR znԴ-:KREe͊F|10TMZ&zm @.yVKŐ>SwqռhVūT{U^YPvxѵE5J;`NkwyDIl4Ţ֙tŒ 3 ֢M5^cKJL +7)9toHQ~E0CQ [zX׻mR cvh4芪{ܗƂK4-Z!g{θ$nxrGl0B7B$.BQNTj2)lP,O 7pliB|ߙ2j7?.s%37h0r "k/] mR<Ivg>`V/ӸXQ_Zխ\*˶2JJQqDA@5'z;k"W۴jȢ23ƃgzOycWL :;c;]h|4@.NwXwͥ˙QueOnKyRu/oV(2ltN~LXf[B QrI!_@9桌OI)r 4}|RvnM@&h-=xӵ)piխ[]U20O5)$e\frŗ'U1{Nvߞz&wkȲ_W_k9|q3 )rT6u'm޶0y4*zlgѧ DNU =ݙLO&&aڽ9_Wȷ3oq Dّ/ e{?Ji_8{9\_8d5n6˙Gm/d&%}ITZ>P,_7h=dfX=KkD.ߥ,$GB!DIdѡ\NutD '07|-}Η/F$/ҭESl1} !FP4 y9^N"k{</z*yx¿fL\ La_{QCAԅ3ltp,C%)t>D3s'mՔfv&\dWD^,_wf;vt)9ħA'>hvkn0_`?'Э7>:[oq\@ .g |>,'GHv4 KV+m&гh'i՚0q$#{~;'JsPm:-69hVxZ6{v!gJZ"7u'c43AԽ<ֿ3ry#=eV{5jb"u1PN?)4_~QĽ>|fz^3}iGvExc=c ٽiӀzY[J3ώO1g?Namc_:qDΝcހ#?4[8Vl:b@ƾ_9_zjIIo& \#V[W;WBڅB!J@Qt1q~c!'3ņ-u]AʙuOg^{c DnZrg7F=[W#l <_۹xL|×FLۇƘx+n[gl3/F'@cZhço>;h6VGŲ+8Ϲ#>yon<4ݳfg|*a˙֙V>#^uTZY=5:e<3D%CWΉ\;0v6.mZ1;k̛,^B/{b-\k2i>uCUuP&D\>g.t<>L}g =MP)Eޜl͐zN?B̳{&u7o1sP҆Fu`}Wo =ͣQX|`B P,v538#!ǜߧɳ=̉܁9ԩ=grk*718r- ^[ O cb/HsVLJz;\BwIN! aԩ{_k9i]b"!&X-nxV/d5DbM\끻\ 8mYȺU1#,7;0ž+4^ΜjQWqըS/ǻw?YX m;=IOw-F݊ z=gʔ)4o޼S,hB!FyڅBʢ\B!D%P2B!B!BQhB!B!B!@B!B!B!DH]!B!B!(ۊ.B!vz2gR*BT>Z%(5IڦYk^% v!BgԫW51*$B!D:̙.BQފ.B)*B!B!BҒB!B!B!DH]!B!B!( !B!B!e v!B!B! $.B!B!BڅB!B!B2@B!B!BQ]pƌgYB! .B!BRڍF#͛7ǧ<#V)}qW OuK(KE2ngIRK[UVk9!B!JhOG9B!+VTtB!B Iv!B!B! $.B!B!BڅB!B!B2@B!B!BQhB!B!B!@B!B!B!DH]!B!B!( !B!B!e v!B!B! $.B!B!BڅB!B!B2!BM0lذ.B!Bb"څBb6QS*(B!DSTU:߉i9/rTf({QӊS>k,ʹ_ywڭ۹uZߕvp'~nT~}.sWr@B!J"!B!(ĺ5]B!B!B!*_"ThB!B!B!@B!B!B!DH]!B!B!( .I=Q:zѡ].!Ih2kиTԌ6>5sG0?nWI*Fl@:O.B)REB!B/T9r:`Wȵ;a8$={+\[4go[0E̕,y-^-YKjW0f2s}R+B񟡪jEA!B!ĿPٝ?MMD17V#̢gIO+SKcˍTBaKv(`sV;0l|ѓ6Be.X@J!N7~߬/'p)@f+Ix ^B!B!,ZJ:& #YimxY3dL㝏$%ZOzL(Ǖ+&J"4 q#˝lY\7&0Pulp>X86 j F'^x\%69k?I(i3y{>Ͱ sY[C/Sc9y=-b̜y {Wy$s!4?KX 6 F/'<'p$Sz&oU?+O?V G2zK0ѝe&u.pb6mS;][Ae+Ȍ>f[fq8>9nDqfwGp垄l$ngQ%V3gESWl# bd,x${07oN. A%Cgˠ4h=v$ %xYXm ~vyUD(]nuf4s4IHw븙VE!B!BҪj?+EL *FN~:[ޒ\!kcI 2)a *qlv ßb&y6҃Z@ Y0z6R>zsuXr&$nFI(Oe-D7p@~?#:Kpp$s aıل7˷MkCu&~?/`l> '9 dO{v.#c2~kZ0t~.]I"|,גci2!2PIfݘ *)ܟ=_$e9y%q!rc9YOi;_ u@1;WW6cvـy1BHH1W8n }k@t,u{>B!B!_Rs(8sTnyOb#@>K{l~^a_s8;(q}(L2Z_yt$Y~Yep@0~zWu'hxγNf5sWG%3/ΟzGk鷟J?}L:Hg_W<œSVYD7i;YO^K?@0W0@7?8dOs{;:*F⾜bI/& fZoTy;_| qMd5*,+lzvYb \`N)+XxT<%yU-qgvtمchҸX:vA;]Q%!B!B!#*U]w>u MK>d7ʠ~_߯9:,]4Ahْg ^t2jG@}ڛzM?³Gl4e $ *Fj=?Itxՠl䕃&ɱGOWVNܯ+\d֛|%tS )'2u ov\GyRCX'rǞVh;`&ˡEJ^oM>vYϫsUYN4bI^q`v}8%K$B!B!C ځSı祷YpnߦU[aנ&Fm F-dYs`5Jz[d|ml͟/>Y@x D/d <Y4]ܩsCaSew66xm*F! >o?xкZiJ]rvq/,9_CЮ{_ߖVfKD-¢Pg f|MW_F!B!B!D% V2/~K {@> EnK=knvx¾@8(Q\qQ1&U{rqVȵX 6 5(21 .0>8d `2祒B% {Wz8(΄Eڰs|b2³:ip3o'[DKJ4-w3G[+ 7YҼTkE^rЁ(m !=stEͺJ+qWp n_ɡ 8q}@fT>]B!B!?ځگfo-yP–3YH)r`&O@!;Lw^= Ŭdߖlq^Ow*X~ٔ..t&o"hpw A!pq %UNw˦\k9fҀBcn"Q||_܈ľ u;6`8B~?['H]!B!B[U94Oy7IM-l>V’3BI&J j@&T/<ӻ|lՃ,oY<;b ~g.ϲ4.a-Avd ֳ/)B`H(~s#{~@-ަidJ3@]OqO'"ǒN^E/jzd4X–\8:|QkX\9o!B!BQT@;MFǧ,gNg rfa$3!:B65ف[~ #p{R!˖-xqU1v\o|9=/ī>o:{LȰ@>؆`O +8qฟV+vlL%8~h=Z1p$/#嵍g'($o,jsk{CqOqˠ>- ڍei\r]B!B!>Ю@gv0r/l=1qE A3ywE0̎CNsG:7w{K41W9^k&cYKF<#&i ʉg=HYDll,V` H%^~Ma{+؛S²oǭG[R. :,f#׭͔>\xp[zgu#帟j~#{Bw?Ufݓы;Qֱ B!B!(>EZqJhPl4[`!vhdɡ7nNyT5W(J%:o[|>'5ĈQ7ShƸY=DEW}KӅL&?˼ $ϻ|s@%_}>` IDATfCV>K>dhm/fiM7N DZpOFv$E4b7yiM*vË3s4dWc]=,?G\1淓D[-[\jzP>d 5x,nyc8392}#rpЁuA~0Kƙ }!ǚwzҠiː%P~ :Ӟ%l$oA^8FyV꿳3lmE-%wAz|Ƃ /MŨCX  kFR"I}s}cL壡#Dng^m3bʶ!L o3Irp" ]t8w~lY&)Z/z1s{m n噫8u5xr ~|- 7,lnzE{ ~:Gwغ?pٟllf7a\>Zf}sl ɑ͑ҏ^o< g*~͓ݾ^G|u2>OiO3d_~Sa<](^h*c*@B!<%Dh,,gl_D/zB!HQKԩL2\ Tnnq&4$iOSP͋ڸB9K":j9yQ;l\9/$&% 45pEF_.[FB(aa:Pq3.όU:̬N^s2BCY4/2bFx(b4l֌zo)HjL2Y:2MəɨZj7~,sq2>4UG3<yi$B(q\@Mgw38ݤAg NTw*D!3<*wZf $$ ^QlUׂ*b C $$L_$3^|'/rs{ e[՞(r>U9>|mJ/vVC2*dhEJ/CW;eȃ ̻RR/GMD 1Lhc$ ~l'y2x/_"R&8Aro$ ůs'kN,.ϒxfli ISKFj,RӾ wXYZݱj̮`šӞAAw@  Bw(jP/εjw;x|RX&=8ά٧x|UfX ^V ,_])wgp< Qbmy/i퉣ۉ.\v~u2KS:ޢf[_vnS>&ܕʊ6S(ޜǒW|yo*_n9 OG <?.be'nq]"Q1d(׋[|`Ψdţ6j/)wQ ,x-CAq4clih0c뱢qޝϿo;B̀]-u37e䈸   \P.PAAO澳peOQZ˱jM.N"ve=a3Xf}6<_JtA6삲*6/-§kP^#Al7/,#Tsf+Uem)Eq]]dyލ> #fg\9ލ([o)9T~g} {?wȝe7pk-AADJAA`F+#iWVWӳ &Eۺ |fpb*"2p*o q387>2yjE$֯5c9?b` ;uP~1˝ xw7]_!ѡckL dätav O6a*h~a(/poJxlZe |,c'qpr6,e\ɄFWbh/WJ#  &fͿLꘂ***w1AA8JJJzmޚyw:Ӗ[Ӛ,⎇2ZŬ`%Hm+ 7&~~|n3 %skyaEZKM2J=[xYVA5θ28 %Zj6)Y?o_li񾳄OI^Zԛr:l0@;H t |=HE]ۼs7\Xۨ:@;$~e(T)ə/w.*c  peO>.  5)-'#Yo.~׃_ 442t7.ߗZ_ ΎAC5fmeWeX]@lpH@=%7)4TJec.C< h~- [TM.sM@nlkZ'5MGΨ0~ߑeDAAA.@;@;# dvie=X|f z^Y^)dw@pc^v$x;M WT5[@7h/ГL4s6nDcۦ#1AJ-R] <ڜ*KtvIDZFoXtj&tZ3:#q﮽`1gTrEUDΈ`F_ݥY  Rv\}AAYzyEW`;/W`sy &^?pirD y.'`YmhnoPQzOد5p$VdŲd|$S^[   ):CAAzI7[:L-តumW}K\3\h:5- ԙЗZ93Á<N[YUzN pr庉JZ޸&2DO&ͪ.Lq9]yjԧCy<Ɲ`O9*g80 wvq  '":CAAks< `bWM5fj!Xٌ^)ne8aMZ+ȷ 0 hczCSȞb?(ڛ?r6oú ]GlyC\  N!AA`&4[Rk#g>SsmRVft9)[i]n? ʕ(L1㕶F3q\m)\@zEJmJ a_R_^Y]C ".  hAAuTmAXP),Z޻ qRn0[ydu*+Us4{k_F4fϲ٢w̽Xb,gcjGT)5圩ټ03_i`gwrnLzW,;ëG0Aazĥ~W9ߚ >n`Xi?D~  …@AA2f=?`1~\=~mnБs<ïepc5!ohuvFZ$#t*jyF/5uo&ۍ3ȗoskP^ 'p Oݏf vg-ヹiq3G { on.|c{6YIfs=ZrmQ>GǖKw0$[gba}GM5&2ȪC=SSk&-'[c9D%f9USŇA[*]??ya6Mé"#>N B?A U3rr0a}멕Hqra@|pjPfpkr(KpT~JwY8PCRn7 KFJrth$RCͦAA?ukړ~|CDr>&i ¼  ?&]k Ka4_Y7s`_!nbDJ* 5F*8C'ٜ7nºcQ'X/AZ&%cRʉՅ)|kts"|cM8ӴzRA8/ZeY+[,;ߎXAA^'4[4xѩGKr`k)8EXU,;qH?`FQsZg!     %jݵmյ"ۈ#7MK%EH]5JG.㰹 ]s٢]\<pW'A ;f LF0U+WȲVZ^!o ^]Lp:jyߨxCI^G9$sAAAA{"E~/@k4bXM^Y(H,@~w@xc<:&?~ciC +W SUlFApcww  5+ٿZEc`sҚ9_8RiY6X 3H.+GbjDʁaL Yo͇6ndLQzRMM2]a2:&VKVHIκ`?:?P[^> d&8Km.};m.,uN3aa8RL-T ukO:(dhߨr+(]BYZn0n]o\a1O!πenQ+D]+%3nN     \(D Y@8V &z*ZR; Z4 rj?[uX# GSLp.&ލ?09R1y |yM\786w2Tݼ%eUy$jy6ٍ?0RpH ?I)d=,ؕ\VK&PqM>E0 :>1?Ti-0(]7fVQ@ӹObeR|o W ?a]3Yq6MӾrhsgOϳ6`,g~Ʉ[Ò~MZ@۰Dq7,?ק@    `qs$h9zVTwՖ{H'L2ݦ1NU+9=SvAv)LG"|r0!/ǏAqZmB9ݟ^4o͖#A0ݻ&ƼuAAp@ kw-|r'1-Q~HHLOT *G<$v۞d`|$|sKÈ'x8ZUpO<"t,(OFz]k֜bvAv"zl(;o;V#+*m AAAAAp]UsF 1`E/)`sC ,RۥJ)*$wgxc-0h& Q|e)쬃뛧2I%rz _U{Ș|-PZ 5p;PO]}9Dyt8C+H6o .hoŌÚB|`nbIlʶ2 (Xu=eZñ: IDAT\XwlK۫& i|Ne{UF5@X{rO߀#/uXRRȴ Ly"ݰ`>YDpߗC˼5(pAAAAsHڻA3+>. ĕBiJ#5ۊl-v/g̞Ze(noߒGg YLp^*T5GJ/^঄@K.h-U@@v??G[kDm/$~7(kHk?QdjF O9v L2T3W9acϢPrYg]xJ,/0l,6٧79 1rWSHŖհ[hR&?,ANHAAAA.<" ;"|?DBQt尳_WEk]zM[w42[^.oLkrIXo2)Q-tji 6ynp}[-8yBc:S:\A}xI;rf-G*@ܬ]>p[}    Ÿw̕K@R)`m%)Vig/D +uAͣu^NA8Qq[?\3DLW`pu03 ȤȔ2K{̉G (ub5zkFdZ o{* vn1hVnLۜM%4h*t&F3;&IoRc73 T8jN+hl]FZz |w R]=wWQ"$_q +q*aid EOAb4R!΄[ŐA*t:Ԡ8oFz7/& T\эڠʮ=}\=ƹK .τCŤWj]B{L U;˅Jb'q庫<"Azrb]: Bw@{7 'J?%֗uv3f)85J0zoa̩0vs=ͷw w7V,kh,Li9p1\x|Sbko;͵M({z#v_ؿ5c{ kOn2d*dyҽrLAzS}): Rd.=iʩ!!]G,bVHQxoAlSn/]~-H ^73רws~} P,\akwߕB ~.N)&1cT ,^)WGώoef!tTr]BY1JvH޿8?!~zTbӜcN[Ϳp_GŽCPu"M., Up҃U0u7/o/<-e=#v@B7PMlMN\x-|gNIq;!Yj$du2VU xWd m`o[-M-; _+\ G@u ޘ1JH-ϵ= J]ZbO&qfwjdN_o Q&k]d /F5v|kIܭdN!ZFs%?C5VR \̜ *rRz ~L )>TliR=& lJdӻY<%A 텓[\'ۋZE&Lr ^"v ˑrih>ѽZD~ѩJdk)hf|Ekc3ju7avM^)_2\x(`8Um,&,~њ]8D H5eL eRPxQ(YIL$"iA?Nc4t0yuD6 fD Ju7s7'O~*:4ve:CcȯБ7VC*g­"Ø}aYzg*ѓ,sIZ1gRRGn@+x8.@~1|)m6Z}oz|$A \ kUPo$Y!M/]TqI$:ԊYwBW ZfM1|å B0dxQy4~9JI'6QR_sDPx^zAdx:u2GVuS$F-hD}vKG>9vj|htSkp\ a? cз@3\k}ڍ`dOsNEnvM/]c%O>~0 oPk :+b|42}P)7!"j5oqLR]`{w̶{3QS 9cI 'Dip:6zf dKN)q:/t6 ˊWš{-*`8qŜZQh[nz \,$F-_}Ye÷,(oIeOE.y_s|:: ;4KvPE=wWh=;oΈVq}^IFL_gk9Σ{LJ۪zIЩz'ܙsSxӯ:M뢧K9wHO+19K@YCB'<v~2̽r'tl['fB];rvhKGvd?kFz^/wwχ-9%\D'qX_{=3;?þ&XvFo*1\v9ӠUpA" 㱯rz0 "Rвy˄}%W(xd -(m$qnb?ͧJo7^`x99ܮ:x:>M#^akm* q?a̽52d 2xx.28 idNXX&{{2n`^xcl7Ux]nW( wTߎWiDێu}]N2HS~s t8h İ~:.x&^GJ^R-\@ 9{2n nհfY9+ۣͅLa&Hk f[rI!;qm.>|SFNg7O0;&;X,c}\R+̍܋beN; 8nIFN}=O?C/=?WR/$F#i_gÌ܉j~c,٭ił3yn~pW+\7`]v4w}Sl}=[j;]v X̱g (LL9`vg#$ }i%F-_= 1֓vFY-7e=Ɇʵi\ǬI, w*i5/5!1i5ԇe.b{I"{Yi'!Q,x-:p*(i}:xj}a\&mP?2q5[{?ⱇlbOcLKLt51|V`ۭ-N%17a8f0&{Ͷ<N>>?49;sUz1OΈecj©8F̀]w漋xz3v۞,n#wg?[K;U3w~)-ugo=Ҕ[^uuOz(cep!]ew=pE_HlKnS:~RWS>)oy >1fi;EXl3ݯ,Ot7O=Hܼy[Wq]?o:OavU\4ObÌjf/sNoʮdSn;qls'pYl,j֕z5cÆzk/4Wr*P>9_Q Aq#YM^>xӏwc}SX)BI"CTr p#ADR!&+BD1#kXbvϏ+ K+r/pf,j$M;F1ݙ3KR(Lu44aX2Ю3T){|@sr˺ _B㖖F=K"LlFg{OQ^kRG9c6P`JZ\K]d i32O?ly .ϒ]&ZŖٖ٪0}|7C|OBYC:X)< S\)ϬX}_F@IWE%hH C:  #[?Ͷd n?,ş禕Io4:V-+嫷42VA p:h]ƷpbWl^u=vZb$3n`΍g sauAgOTD%W6R]?̍e.CF2Lyzrl{) ';^bԲ<` 6+;T-k xwc13|tVϖsׯ:˴2|l(r"Ӭ<]QkݫEAnpu"DKaYܟh gz# !dSsYkKryZ""H\KV47%59SJۜ,϶ Wx+jPry"^dxらj햞&[@! 7R@뎦#*#K`'Ք-4΃#ũl)̖b޿u W d3״]w:Bb4x1< 2ʨSXڧXGጕԇ!xKf۩ɦcۭO;f^97*(wQ::2Ju)v=4l<~_&v\|(٘ɽצ$'ąuzs T.|r7M!նʏN0˶TJ? xq[%ëmehX=0 _Rwgv{)NWp߆|8Qىu59R 6Zu3 fvq+cO2ȭZ|rl1vobT'<$'k+y(]x7#ߛͼR r6 l7cvkp·g-M{J7ERTaN 0Q|VOJ-V.HƇye5LD:>]wwVX#M< <>0#ՏS [ᘒg? 'DƹF[S!vqqvYYkw۰}0"VOdA.^r>,)f!&d ]m35Ʃ,x5qk[cHL̾#I+=?g2#¨/fPc?nqGQmMyWXN2M5]m Oaߑɜ(!~L>=I(ș3PogSy(m)G먕+$g2NcM"[t푙̖Z̺쩘iFa7ˋB޻$q]S)ȟ%,^>9<&ٰvcOh$bƐemq:7; eH_ƞdO#8\80KWxT0qdVE[֭,ݯQOt_u%I,*b>K}'N#l:[tYl!7m-Mၟ[r(/a\v2^'ZFygwkj<PKj+ %ͲǺ,i)22hg{zLPQ4ǖ0 ؐxܟRhyY9˛vA 枲<}d2pp$ ;>5l9[nW_(AT$cNƉ 7Ep3cui^Onvx[&^8yc2¿1"OX|Xo*ќ̉2{%ӣIW(y0k6i#2Mv<~ɃE%-djʍRx -%3Iɢhc9x7K;ם0ˤsH%/%Lh2fPP07,!-;I[_+[~je5y[ B;T q_!*/A jnC}vwo1mp0ę'-Av pO`˩i=;ʠw楳uAI|MU4ⓦp`#xl,_tkp·{:~$LY3(IF\ r8Ky./ו#٭\M‰ɤZEpM×.p-;hHkɓϷ\n7'|w J4T) ½15y @xf-9z류= KqGG£#SAReD~v09!Y5-6suH7t52=2GA\ryP2b@uҪV("U IDATqUBv׵Y<X{Ƶ2u{]GQ%WWkFPoN.4*=%rkwL?°jFKzI?7QU6KKJެ#V{~A|nlϛpӌ`ш3ĝ4—d_nb91m<$k8d0^nYvwٜp`GlPbR*pn?/bo p; !ark8oeJ-ᇥ>0Gy~ |ЮןcqvzďvT}!ZO#Ђ59_WkWCîa(cuk)n41rpy7r\2ȗןfUP^B#-WYu{b6,*,Obmۛɲ5%-Ĥ]=U3%Y_L}}OSl,?lkmoR*p&eZ#Rħ&n::%ȐId{øySeg8(o1[a[JebƼLN:Fw2)~2~k?N=;7RzP,sl^Rte=S+,~IJO{CgR\憎|Ri,}*b /; c]nt ͝{p<~ƛ)̙dQ>ȃ;V&) 4uҺH5x:/;pX'RG]I_rXzc7'zt8=\19pv?E f" '?SY,yE-5 ˦pjXe?o) s8OW!d>41=ņ74&W{3s@e `y̕_{{AגXgB!ho)يLL%6E?m%֪U)#͑=뵞8YdG8RK:Rr?8'A?~KfpVX^x\YEXWz \xN"#ZER<Fz4k)a29b<&ӉaޔL1^ HIn T+ߵ1J9Q2ĺڕ/ҵuR.Vɲ>-,\&%1VfAvGizh#.pjk˕tPp탾YĒIo}z7Ui ~2m ɣXxfcZqK]5L r&s U cH3XUOra\ '7B@W,KosDɦΦb8)uF2[QL`1l߻u v]UD χh.; /)`'V/o4Ϧd:)hrC]g[9,W2jQ4.2RRJd?/޼j2.7J[_'{mA¸69x[_ -]ُGt z_8G¸ osL*TfI@]?5iU%M@_laCLqJF#P./GUkkH1dhhcͺ@nye<]'r<ٲزN}xzB{w;|Ƌ _7P0P ?,dYhgv8뎃Y8K'҅o钫Jn/ LUA yQe}Fͽ;v8JB ,|-e齄ea ,.},JBH#ݩN撸nUkF#lɖ}ޛx%C sbrZw| |F0rxhv!P"HFj.Hb_>]iLMq8hUNd7%3ue;F3]?-a^wZ8>«'i·d@gBq_Ńm<(|S8NL m-(w{X$P oE@#aVЈvHk בDvP/6g]iA\q\<;2!:] 69xa!I7aN| +K 0]{Jv5uokAIʵZ_9'&㕟' dEUE1********=ɧo=ݦp4=cc9#ù|,#hDiM#6s>^Vˆ, y]D z+ѸwmM>dBJX}MPf'{ΙQz¯ͯ l˱Wnb$o.c~Smr[JSG1~4͌Xޞ+:nD0XZ/#-d{e9vJ؜hb[1`PT8T:$Ѥ\o9DFS6-PujPhBJoۻDc{95\.tpMHGɭ@NFZ8R 2 AӈLQ}f)J!$+k.Zeti-mCEks繧^-{-Σ#)xwT0@#"" RR%Z?XKh÷|#Xr~-G.)N@>T|d-Z\z/ۑhG4Kq<$O%>[32ѨU̳gOk.e{jlX-),t=.]g),0_3o8uN$sOeb:<\38QlHL@qL`Hu(|GAUTTTTTTTz@*BaӁɾ,XP˒3vnwe$b|c :﹧|YDkr.&~ >H $@/\_9r"/@71dx(QA: A:%kk٣%8]WX l{̶xF/v@z*ĪyGCfq}:RM o"ƨEk v9XDMWрµz; w^ǣñ.D'Mw>zgE a:=?"(MMu5_@C% NZvd4FbwAG m**^lf5wY37MrG LGrmHz-A]Y_>wUZ6^17WI_<^X;9ֻJ@^dZo+ yD?0yyrx x/}RcgJ"x;Áp~|Y*5U|Ɣ0N/ga.mɮUT3_********_az.u4ۭ5qao^3_'vaA,xi/]MR_nȮilJ[GrqvHgTy0_1DŽrPx E]2|%2.)ls\;zh]#I•^؁KHDž3p xLk35M`D!r Tcq:@)1" UAEEEEEEEO\} qP8.'RYrRԷnڭq=:i}E8LQkkdF;N"\[׃Z&MpL٫=8g/k&Y!f*'4 ^ͺaWuFEd~w[N |dq%*]Qke{rivsH QKV[i_F cL8ih͚#I@-^zEd |prd9%4! c]na#=J6DMr6ۺ<#.=m~O xCJ&iBx=}{ʬw۝Ѿ-s=2{FyK0rȎF㬿&ʉzeQȢ~ n^mӲگ:&uE^O5=ws\[zc]epy1&ڪ&5kDž [@ ayD])a- -\ޯR9 QvhFE=Gbמ2oiE i6^-a_"t͹fk[)~eO-|hg9Ɍp4'u >t,ge7]ض6}6}l|t-/޺n7c(2ڝ,ΝdY4BuJۥ 6mv |w0(Zy2H7VhoA|]E+g`{1stge|ب|nvq3E.ʂ X鶅5srnU| X)6XfsZyʸ,{-e{ʪ {jf<`K>TIVKIn91Sr Ib (;NwTV;;I=p kw `5`ƋVѱ={G>9vjO%!qe}h[ǽ<ޮ8N2: 슡g'ʢ ~ο:BE9zvF/)gR d·"T*/׼ͧSUhWQQQQQQQ񃐝y?_=ܐ0~'4:WxM虛I_w57Y:خhKRT#6,xE ;$r <{w6#t^Y+vY{{\KrA9?⯯vqtnHcaי .沩˹69ڀ ~\_Sk}{cA-Ͼ~|\=>Ȣcwo={Ko2wSp톔3y0G ʨ\2I-Xצ\gP~#\2Bù=<8NKY<ϤouY^Gb7]6LΪ\S˶ ^j~s=a̿0̜&A2b-5l>uUrŃ Js$ ߞNmulreφ{89xvN$.5,qX1g'4MD$Mf6m/ZSx!ro)pN.A: lYQOnX(<{‡[X4)3}g*gҼ DcA4kR"dpL=:H3^-|?w3ώfr^gfb"62^b{C 3ڬx oF_Ù#kllUNOsi\Es7Vs:8#&PG R<0@}&߻WC=ΈΝVjf'WBڻh6_>GT2c~8+l^YCV `22< 7e}b$gx3gd8JYydl+Q (3k b{cX|G#Il'HxFҁ=L4gMyZHHņwE/q!2VpZ^p ;XHpy*/œ 9:ɃYL_PI$tD{109}nf׳p8 M-I;Q\83IxS ?f9v ~x4`Y8 IDAT;g-SL]Si'3<wVyڔw{4xҘ^6J8=GF7zk}IA䲵V֟9}b!Z6հ"='ݝ룄]zFG},off'21Yko=Q,qvw* T]EEEEEEEGtu {iO7^\B017cYU h];a 0 ff5kΎ$뻌̷.[>nt[-nSiDr#yyQ(1-㺯l\ƫY1ݚ©-9[}MXfm`f ?kr5SX̄gGH@OőH%{Ov#pMzjOͭӘyAur:0#N0+Rd$c0}N :/g_u#}mUKy|k;Vb -ZEI,鉼K3\d|X*/6^aA4{8/_=ߟʷ}R#X{AHd(oƀ'dxj8ym1${6N'l^3 ̐œG`\{7QQ_S =_GҸgӯUˬNrAI`(7yLGV=IRcLGx ?9Me{^Z1Ylx3/fF~<&vBIrcX8=6x{5'?6ncPvq &n~'u沼A$M-zqʡ;aKƐp5v/CHi4 ``3Iy.]/[hKuv ㄛW:ɷwg)0@MJ _1@`;[Od<"~ca,ruO塅tgׁ93ϮpąжYx,ީf^怔0%Hb~^9gl~~<W]um--1\u0\v {y@٤㑇Ѳ&Jw5nӔ.}4G/zۡQrh$?c<P_qSβj`QCEH^>Blf̆:4АG;S2knp cZ=k&R!$&) )5Vl6*-$AtrT5 4HJO-?f7Ocqݞ[{@}_K9 g:zB74ĮQ^9N&+չfrbdG )ğv {Fir 3{f_8ax"ۯɔ|O>9U գDe6Ptɻh{$FfVc@Ce3.p#$ÄXHHC . 79V룢Gjv&D->xW֞#vXoG?Vs\ftbˆRHa\BϮv#;JhrPb2 FG؀_'w{LS_RB{/9},;_ ˜ qlWRo`0-n5"tpxudI}w(E^=]f*BQvcGşOkp A0Tj8@!/C|[EEEh yJMtw)FWw-913uPf@!xtCe=Z( |iE|6m-Wo bL0G~+ ObۅQs<9beة+5jE j6?qRLŃ(qD' poDiGcAV!Om(~*XۏeHW  CǓ;سdl? >Enc, ٣(tc5ȋυצ߇@,]ˇ0V*yva&TO^e3 VPmsj-` b}I1PxBvh8n{Ye같_r yH%UTT&sE,?{(n̖s8l9Bj,_urP1􂅙%dp>_,ƺHy=ri-!bjZ8C>|'CSNȪh=%Zi=yh}2pi`AqVWFGB{ wI7rc8>2s5dl+yAaA};ϧϬcoܘ{kc0-GC9c5NMÍc.x/  枯FqIxO@{%`0b`Mۓ>iK̞Z6dA3͞Rx:i]F?.c7_===z_.g骿UTT~?"?(ݧlKА0#]ɯc.κ|A)+6WԢhb(μ8 qӢD ?+[Mf*H:N bNH[hNcIch.%*اш/U|q%+v[)q38.4ZirB\lCv1gceRQG}zc!pu35 [(_WEMQlCFEE33F-Få[<`KLά;w<mQA JIe~ ۄ`s~= l p>eܛMS0IiJ=ۉ#x#^] /E5\`;u tL$hA1fp8e0:zoP͸1I Xl[A)^]Mc=rx 1pH8˄K6֭Vu[9^yؘ"lZLk!c>ȥ2*â` |Py1k.I.g3\]@UYNFHeǴ&Ρ`y96?]=&)'QyGM+c{>ȅK)P8C9u+ Lzl+t5T|Ƒ3gěWHE OX.G"*G%C~(: 4BJ&lۋ!sY-C|~~X3BS~aǍ{l PLJ հV'}`mZp\୩0Ub˰+NvRV?S sF-Nv+#ZMlX(!4L5;q`[BhJeԶSڄN[JUْ/HPzB-Xrp0pTaۧ$U?tt[/4eR9Nt®ZlȂ3 Y3 =pp#o%l4<~:^ޙC6iDhJhȬJ: 0Ķ[%0١\8mah&r Р+ǖa]? 0Cd^m xmk dűK׮WLmir}eZX'(kvnJӣ/`-mwv3|5S1CQ%3]QJ`C <^w9xy&EAWTEՐM\[=س]}]cV'FlYvZ"ѩVc6n;K %$Q j2Hm51mGs,^ ɀnH(u<˩r+[-E[{|~19wE٩(:$`zV wmge,6P^~\ވS0\4 {ݹMkl,"(̴+vS.шxvEg' {Khu-l*ĺfD[H/\?N=^Ȍgr|uahX֭VQMycࡡ;8]ly -n=Bv' FCY8߽pg|vDG!?vf^+79a8xehzj=߾V `ӤM;.N2ض!pj~afF>w!/ n~a1ڛkv:=9G _e׽yTXk;̈l#;OgcA߂ 90:wC[EEEEEEEEEEEEEXB N^q~~BmvG\Z =PE+ nqj\d@zhRr*@. hm& ;A"tܺn%=Aόf(.z#]bryc 1h+.I͍s_\ )0}-cX@u|R׾'Caj4L۳\W 0Tx2]&U̬C >%^˦vU G0dynC>lɈKpgF]xgkpBS,+-_V !ޚ3[t{0, ڝ~r0m5s>*%ktBg:-:kTбt\♞S.y^~r >i(bXji_c𰭑{'ym)-+dq'?k!v+.g'1f0.]ѧ󎊊ʱ*q>u0,WID͊%tEفsZc}90<'A Gh % nfTv/ONVK@R1o8pZT(ޜI\"~I\``",PjC%=ur++swJh,e_pah?$D 71hx;g ^C .DV>E){e^8'ljC`DN&j{5܆HOn}HR blu9P )۞)KewTyMKf$_>Fw $/u 6sHry:ΕGyo:7Nh9/`uV/c UhUhpvR'aa $HQl Cx=LP8F ݦ޽z<Dž{9H#hK*L rL`0@7uO֓e`$<=TovrlQ:=K&+iE"HpTzzr"S}Dp[NU0= /"uWUl>&.HM]lri 46 N X r6Wĸ^h@'uy@tc.\zç$@[ճ܉=)-k+¹xKߐ ؖkǢioA:-.}IAD***********G Uh~+`M[ɫi(moEpOb sE "Gkx鱽s:. h mef-QjiԸ$eA]SEѭ)(: IDAT0[ E3$޿&{޹\B.I綑nE2NO}+Ipv.D4rE۱(ǧ9f#h]\ZB4H> v]:AtA^:s,8k;RXJ˸w+.N)M㌇\XD4>suܗjj1H**********TgD?/&ÝDuDWUSd>]\0P -/ihmf?vvT ,C:M~Q! 3CŴ_%vh#6٩_E9!擓aG_^@ PX 4!kP+A;M[͓)Dxk[[x7E7wBG]x;i;3_GSfrNß6=$RӡpO~8X* zcR 5vPIsP}9{HlW\c՟VG-MĶeU9(mmul>w'2V_C39iAܞNs /_EEEEEEEEEEZ(*'gevqLbr:@/ Uk ~(m ?Ep&HjT~cUvKR# rqODft9kkoX[Svѯ42 0EYY2? =-."j3 qGA?ziGͰǻ7(B}*{Ldy=.^XGw2{k<;^nh0CX1:r-9e:-.6庍jPUڻ7FrS}30t>-9}\羜w-´`MylǦ:18l<_FdמHyTz&IZ)z4] |Y6W'४ͨB}U@ G>xY_,y19IEWUWVl_k6[͓=Ghҷ aEH<50Y?{nH^TE0w_6g`z/l ТroGRj(߻ aq0{hNͤ'8W@C9}‾ Oҡ?M'9 zS%W"Hd4-~.Qk+J9lGU &sgu%67ʢ*)[mIp`ķSظvC'͟VςL6x0^hy~7=Q+ܿ2@ jABݢ`rSeaF"c Hf?WϬs#e@1i ZEm+YoGHFʭ"z7Ysss6g&$; L%F ]`i-;b!=dApAkl Hgpo&`L~ѧe:jbkZCLv:X2i5y;x KM#<މ&D&ZXc'Kؤx.Rv B:9" ǁ1-Yƒ!\Fnh8^eӆrW eav!L#J~<83dJ&XL4بRMnP<#dabŏSH}FXOX&*7Ԑkw1a6o.' 2=&vXTApq#oGsscn/{ j8!8FJ~''@BװJFEgn^ SBk2d5㙓iaF*&G3]^g0c_ePNϔ`:H"B4Ԓgᆪ㪮ϨKdٲ%wWllcIHHBIB7!8(`:1qEMlٖl4*S4}ܑlY3*dɰX3{ѰϾLxp5nOki;Ͱ4 2d9RduT-wku;ʒY0Yʼn#aGPB2Alw\veTVdBL*Bʽ)]?xo:U p&XBVk.ˏ{ooפ7٩hmE?Q/)ԯy|V~Vu1%ڎg[ +1}gO$s]㨽z KQml<$x zSOO Tc\2IKjUQry 쵰pM땟cr$m:9LӉzT3O!>IE\Y+:{tHC9qa3+sʽ=̿:~D}oB#_kXQAo;kl?dv9n6CJN'MǜTmi_[Z<λFW녚#S߂=;I[Yf̏cv +K?o3?TB!B| IF{8T+2 S[/)fx;zٌ4:xSߺf\3@Ƌۉ4ZdxB|<ҀZ+ľYGO!5I?GRPaNByj61F^m64``ڪYR%VqɘY̔% h ^ e} qv+h5@ G?[E.K@ zWӸي}8xq)5rkӘ=Gd=F24~™zi+.?X2)//T 'Y jle2ݹvw.zm8e,w9*(ig*'T|j޹}FVxۓ'1o2"(YtiM "gyFV&_P܌m{vXǡ.f2I[OVд/fGcIIZhTb^6Wz&xډ̿.Y4mkA@f39LXu5؟?I: V537o4%DNF9 _\"xlc>ik8l5/d $i|'1V2YRX8F,-W0$h¹KrS w+[5mH L %?//BJ̗8jv^%zٓP+Ϫ4T]ȡxx;SqZrnQ35|)7Q{!_@ERvi (u֩'a{'3xkBXN[phnyK}|W M__P޲J&MRɢ5<.7G2KJh.d˱T,a (1_đ5kq~^SB!BCPV^ ; *7|3WxysV4xH 4i I }^9s&`cHe-M{$@vLL8Tx'@dqRUC^" w51\ 2L1ۓ)wR @&O'j}XoqBLjŇ5NrMպxԄӢKѝ._ T-nk0FY7A=LS a,`Ly;$Lgo$Bs;maH2 u:*u0dO{V؛X 05iDT;=ŨS&s}X&܄Ox!#}nh<;3O1Vx|MYClnF /]& {+cfrKR^׼< ABq5 ?=v8/wx8solaS8E38Lrd>_:mr3}?uMH;'|33GӱRʻ?(?T|X6m‚no\/mfa݁S[ZB~ &KW3v\8>h1OMK.I6q@|*.Ze`,xċ+!O.FuRy3p]>ow>e q<8˶u}5! 9o.3F(.{)nǝK!B!F7_7ɬYf'R:SAK}A )3] FtѷK]B?ZB!BsG2څB!Q#n|wqKvXsZ@7:h chq8pnr+%gBnܲk! PB!g0.hg1 UӳUA?»m(uq· ?!cKO{ک<F> !B%v!BA꿦vQ12qܓ60$F~VBa@c2d-mT81&6FcL1*cn#DٓM̊b00iUZ^骠+oxAIq̙;Slni!B!ĀH]q^ID?[/O|B Wq/y>UG8II$Lz}wzVO=7s-v6|yqA"v {A nIcKnȅ/ܯu2;5)]KtV!Bq^@⼶pGم#i];]ɷ)؇-CT meXBY>3'T(y!8⎺*E^iYzu#/p JrײL>fG ;o,ePf0o榕eJ_L,{Ajuj jnwَ28S*+z}wj_*DL*B!yAB!CFƣڣL`٨ȋyk nIF~$P=,W5 |K*(Y෍#?o,W4hP^&6MM.uj[+V֜n.| X~=k^U?䊿l&/;O֨0uTΑFx ]nTr ݖ< kU/UVb1x!B!8FB!e{?h5z:aޝʟF˱f/MdٴLJ{t _>qnH'_ųy:&Sº M`OÙrRR06cu[}T$dPƫ"1H~FRԖ8s>5z?C$d0Gh' !B1D|/'f͚nJHF'TfB>uk0-r5"!= k4H&w#5 IDATiSIz[M`oP&)@OARdF]ۅ5MaLdB!bĐP%~pITzqk$B!B!)vޫllE!B!B!B1tL?$1 Ji jRE~7`!B!B!>A$\T!v/6۱ F B!B!Ĺ$}ly04xnؗ*iqzL>ϗM^tx~vk,e2)& ԽVOs3~f<, gp_ov?Y>mlk=ttf?XFflMAЪab*|1ĚA' j<7͂ ,g^1R!\32\&z&T=wpĨ2ҌGDs!ϒǎRr=m~e%; VzzB\Gl+) Qqj<4FG_»6K`KJ(ӄq,jZ'6,҉S>;փ7C;+ 0S9@jpjwyq6`ClmG/h.bVe!݄zq;VvV_gS8'+?H#(w^.OߠE@<~v5aq|!B!B!F /:YxF+ 9!26.(not OkӰ/cHsa-j >M܀gD 2{xo&-l{]ܿ W2ÀUI9? yZlKQ o,WHj~([4Zi c3(k] 06ddA*f YI@ۗCe67$‹KaIn fa}>"TF /%qxeG͘u>x O͂KRA0Ƕ?qu(mkiGίΆ#IUQZctiWqn%g÷ iFQwIK T$݄Ipkī Wn}9߲ϿAXN asG8` w!B!B!h05I2Zu=e0QZ5{VV@KR+*)}զY.2xh!7.EhѮӥw4 aa&p[15c 3:kzLc%-4%3tEq3 `Rnp%HJf$Q٤( 4_C%P;F~8]:&wOd'}bd7[>ɑQui:wb[s.:tT>kzM퉤4܇ 6} "5dn%u" 罟."%= !B!B1$>d3Cdn?@妹Ȯd/#q(A찟֢V%cxEfIGi^j]:azwBY z6@a|E'uJ;HN8P>5,"WI c,ӢK|dj|`rROe8NAr] MXFoOkM9',8GJGɗʎ{8@>ٱ.LRV'Myi5@M |J$k]!B!B_F`Z [ion,0Zm^*ijZ@at;hSWtwn+s JpduO5XCўV^;]owNFL#J L5.5FFpJB%<zX5$ľ.=2A2a=L 2Q&>r}Zܺ#[mn 6Շi ޤek'sf2Ad'Q$.΀ B!B!#Djy)hQ &np81 aHUt_3@F*̊\UpGƴK7OUpzAֈhI%V,&uêtjB= ln;z`ڣ$g4F6?t!K/* ^; \? 4$B!B! @ ygA&Rʰviƹ7ZXT$^\f P܊5,>mȰMG:s Z љ[mAzlwUGjǝaDw* 1F3Z M0\Î &YmkmW3,v aۅB!B!"A'y >]KvЦjO-"+\ P+1:i1GЍ^Gxţ$kS9;tF^HNti9˄s Q{c+"#:!Ðs2x!_/`)q,m`57&D2 #8ihlaKB!B!bHlHLnZ:,-ϊw{ `RƉ) i-{9 \EXa5F;7+^8 2FjVDaHK%¢S T2k 'uk8~NhS#V}$ J;-Qv{=jjX,塃ֶo~5iZ%sP;N(LB!B!"bEfG8MesS7Q+6@ KӺI$s V 5(A㥣վ^?++@; ysx&gC*~] ]V3Mʋ0-0- -J} p|0LR\c5J>e=$t)MϮ{;Y?)&0ĉUȍ<^j\ps)̾2m{_:/Vg=9ôƏ2h~7PEy`aJoUA;- B!B!LjU@xo&T.Tē4pi0_Ǝ$OC:Ev%H"%uXWc9 lP`"ư*?oπLQ1|W3Iiet}BxㄍpYK$ 6B&S;ꧦajǓn'`nJۘ0&% zψa;~_8 Zgڼ$&vB!B!: B!B!B!H]!B!B!)#BOu@o !|8!ee}B!>Z,ߪÇsK ''ב@ב@%HFB!B! !B!B! v!B!B!b$.B!B!B څB!B!BP`z.Ta-OCZ`iK5Am^*ĩ"KB)n7=; Ώcߏ * .I|ɣIs-B!B12Ht,ح迲;w.caqo!fmC,!k昛 ݶw(M-!ؾt*C];Ŗk`DZG1/bu';Oi`I\vh ށq&T !B!Bt%g`.wdv.]npNN. wn#|s[aM"SCAL!h+< 9s F>oݨmQz\NGjB!B!DTh? ׯ=L:  E -06r|X-a^=WAJB?<;W4 |*{({pCW֐50P!B!; Ӵf\3ۧ˜]UJNM納[QikKtՖd08{|<>Q+>y:{ĞkB822 ܶwg$;RBF!B!#rY˩WԤ=!x0nC\}0jq#jIx "is/]|vm,ۗktּՠk0@JZE Ilz vUpS+0xVNo3 ;J]@ ak^<׬: 2bŬ|r?[uOG 3TѸ[S$xfEp$XwvT>W"pXsbTn.~7:LwVSd%MٱBwU6K&Zw»6K`U߮48d, o*Luu돱Flq,;@_+F{K Z_R['5:{h$zo {eWgIz(Uo5j*(/>ʍ\d̄3>KזSZ=%x(}5?Һ`낪f@-P~yڑxb׿Vx`BQ>v&wdۂ$UhK,Pbs;?׊NtޥV<i5@8Ѣɉ`6K"өlwՆ'*CA(uy)4$J{P9,!B!B /NYǦ&A`(Kj*^xn/!\6 OW¶K} ΢Xd2Mzxs_UHrWߜ1*VA*7'ޟ1 4\*R|_ 6VM]W˱y ŗ c~뎲u+p y91e˅uk-~V߸#|3ٰ;/sj.|l*i@I5$ξoL (n?ʞ'x J$﹚KOhk^#]YAc^8_Ixv^ԍ)h)'*.cHۿ@k5ĤbrB(k˱m%/&dc~/Z]X &m%+GcׄCd렝늊$d;^\p1.¾*.yl*I:G6k5,޾qZ5<)K;'j"jsU]w,}T償{RضGV&ۀR}cB! ˣB!`@{_ݴDsRQF;g0k,h*uE{̔7h j]39" vk߄M\<갫ժzkʂA7> ԥDGC\[ .2@?ُ~^Q:ƍNRR@n:aۿA'M?)poi%x|xd~ξAOioj60D}r ߧ@WgBƵ;r IDAT/ Doo%5S]l[Ǵп6PMl( z61+M@ GSRG3}7]9?UUcP'aMwax*hi55;..`akϮ@%9)2 +B!B!O}scjدm+RDR8]˘(k> |˺ks@68-7Կ n]A@ _c0BGMpoa|HK2ɿ?ejBDjg'BfÍr~g"|1Ĥgaq65̏=Ñ&GW޿2KBs'D> )m/j ّQ:څB!B1d2>J&تLܹmC,)6cYHAZ.g7D0/Ekzkkc0mQ4L<-6gY)2' 5J2䄻w#Z/ PCZQу屜jRGظSrhzlM|4z@ a~UI!#ۄyJ"ݠҹJdXƳ ŵwRwo-GLXKnwuhԪ!/cZ5Y-v%} W!/2uWLM>%0F(n՗9f:vu;۬Quš񑏾*F>^?!k *4Du*4=6@cDU!F!B!BDH5gi,Y]NM1L;ۯ[ hpt3TV_f Y9I\yIL^nש9Nֶ vz{C!B!bz#ߠV!+SD1BeX3h`;5/7)q%}=}(R>LZ jC@JmE6z2JVx &Qb!hJW5/_i;(ɎcկWj-$/Hg(cpuux{ }th##--w{{TFaߥY|=LK-Ҷt]XjvF>nO!B!$>dYzp,' < 82rPk J1&H8-H0YNj~|<:PΫ2/H>j$+4;}`⢊&v Ճ2wR2FZcokz,r71Nv'D?_ύdm iЪ kB޶vX7_W2RG!B!bIdēܛߛ*pྒྷN+#h5z(УO&ted deg` @̆]QQGߍ+|&Gv)jH{gA&"m~5zu?|8V9z!X|VD:~AoSGwe~%}9dh6D]"3'WsIj7ߨ`Aɓ~&S294.oQATPpLML)ER+J}/+)K{6+}KK[M--G\sQDAYU`aaa~0gsݠ\餔`Y6yَN|X];7\ms]0CR4tZ}X n @p #Q-NXs @p퐗\rV ߎ^[\G)/#Z^2DD-;ϒd_pLڃN.X kQ<5oi_;I|Vdk.tt*:)ܭl!\2WHRQi22I -icFJp-Vyx[A#I8btgL r >v.(% `_:0hZuQx4Kqlg.>T'O]E4X 4/4p!!"oNp3֮\D7ζzd,Aၦ VyaFgehﶱ'~>Oi6u'˛CW/1'uǍF nqh0Y|A WZp,!}ߘϮi0QN;$ߧ,~Ƌ+C>H.o>(\.:J9yLRm̕< ,PyZt]6N{2}x ,胷%\'mZՓNZ':I$a߰oJ{/)Ga@K󠥮$e-)^ 3.k;(:7A?TJeJ_cؚZP[9E~>5o>Y5 x}E۔9@E$9pmMŠt3:D1g?&F֯:ɿPѾU(=j>YHyx`0|>%?L<5?` ̾EUVjH6Ns3hr&pI93fP^}}2oU~z'/pjtD}KzMS5!mB)7czӯ O~ofE_eߕo5!^Wv%='yDζ X ̊dmNչHfVRkc.)/]Y]1ʱ@"uL4,V@At ٯv!yG3~oH˅_Szt%'9_ FӐniWցFx[,*: [s AZYdJ6|Ċ WBJ.,O!oEc᏾^7L7Vo]̠qam61~9~mSTxJ] %tr7 0Xݯ7c0Z_?%SiǍ OENح=_u!8J )~M#+A~l|݉wT\$]UXЯaA]Ŵ֑4iJ |0JYׯM?1Y m%.I*g$3휿F]9s.-SY[oQvMf'30o-+/ʐ[G+.0?9yNɮ O"d\d2`,F}x3QP=£MqX~pn!8ԢY.Ot!T9L:xy*sBBuьl;۾H$ ڪ@!mTe\DoxnCD DK+&URWy0g %U*C@33۩\d鮄%zɀAv^UG.Tnkw/{5_Twu K*@UBDhMٲuPJę-a]=M{!%T"A ,-to?(+L/b .?E- FwG4A_=ON&[ͪ,5=k(:(1[O^b_| 7-6)ԶPcr/{/~^[OG]ist(˥vV̪e+- k:ۃC tM3,\&˟0w,X6r0"ǏG?sOV[ekk)ʥlY( ɳ=QWa 6r5%ΞU"ix.5 r&_$痒 ikxp4#D>&RM.2qI$}R&hœOf:νdP ʱ.dfd뢔lOHJd%=Kۼ3N?kYz_7v9aJ(Ooڵ()K2Ow_ K?,W] մz Ho9MhJ=mZr7`@𹸒[!nPI p$Y}9= >b`$-lm>SXцh *W)Ʌ,J+ ͯrJ~ ̑8B -ds)͕U[r.ҺT'mfS ^~0]_ O y{gj:QbDRV؈졼)%G',_mQTZʣdmrϬG8je-3?[DR(6qW֬rQU@._|pW,34U=^Lݽk󚍖~uY2b(Fܫ*JW$W3Il9M(25uhsF-Nijj,ٵ-Z=M9kFn5STnV%m qxxBz IA6X?%Ϊ&2_sg.QяFR|+^[qr =m-SHfi#oV©8cfN]_:ς:^^pm#u3hX*̇S`~5M̙ Nn1^>PX"~&hH6݈_V} 1׾S$c\txX%79NjeE3ev&0yp^O}D7 *B͏:Wnfyf>f`M#?RڒK @&] ɸg7ogyZݟy[yא%#gn]Y{&g 7GMPϘe( e+c'_$.xM`m{w6XqOh ^E-oΊSZv\WJZ۵/ MO;K4EE9jL- c(Qm|{kRRYT"Z1g/Ƅ6bBOR5(SLo0{0~@ 4J=Y<{$MJn6c+#vWT&n3NKx{6@]h1C-)03VƜs:5GүYC3D%t-)^߫y\bϵJ.ػd}_|&g"Oo(/aߋ95bzM뻜 6KTGELd}%KbUd#'muQ*;$Ӷ(K}Qf~"/>E :rQ O-a d9hsfD{br9{Sf3lc6Ulj?1 ODWK^C_-guk[=Ȓ{֡ݪ ϭMWfu<SRb^̭p}٭McK LIl8I94/R6t M;Kow*$p[DJ1ftXܽ1# IDAT>;6H>VCSO/XȻ_,`Ja!7+m"2Nkz%eХdg9~H} *A+`w:L뀏l&'4A6 #wJ U 1"7IQc~٭ȱ1r]y!+6$2UiN-gϟ|g1\,3j쓲ʉL[X:_7~C*ޮs㐍|Jy<ɒ?~;%{ _- V/ggʲS)za~P44q`ɲ R&/S~*r(>9I\.47h}n>-6.%-[#((۹n;%W(㇌[oVԲ+2*8 @ ەZw;QeokXM79e_՟fBL w4kٜesxHx cɔ07T#RPOϮtnq'7ò_Z]nsTf4D9ꃼ{bފc Ny(4ҥ9Lx`4;1P:4FXgB-fGsO oeIȣvL.);~#b\^c$;nP^ m }ʒrP\z-dSϞ |JZhMڿNwzѣm(ZZn"?)ٕt;nHXC6ۈ$#go&ON>TZ &JvwuC{DWV*sF]i]Wttׂ K%[#~ALXGg`zU-)8boݼѻx`rHA1B]!De!@ty.(lt.*T Cas|j8 >4qOg Rn"~,|Aƨ?+KR^[? $q),fˣ]NÒq,{^nK6sYm=$ϼ/ wǓ"{yjFù:YЧ+'OzD<=%.4 5+2Xӈ{M.A 6^a-1;rL't`Mqx8D.ȱ}2i2\~n3-} 7.Ѣ`7dd[ q/aoxL07u]=a-r=gKT*6I_ў }v䷰ݶ͓=2)qϝʱ'E43^ȳTdƯ!ұլ.S1?bJT[| EyXq=ٺ;`c1t͍;d{W{`Q@M94I(dw>6.gA}0Qʻ[m]c=ȡ-K[ 6y:*i=κ27Xcz-Ɉ#;=FbD-mSϿz E~jG|̀{IΟ[z:[W@]-5lN簫DkT/OW҃_xִTx[K~qmIdf, {e5^Y70N`4MzkaG6\4EFBF>jMfc*i$.wo08ԟn?'0v+G] ~ݮ9~i /۞^~ VEA%E ~B7rek>P OX0I7A[ݎ|rdFi1Ifo^X#zACx6\5IYDq[f&F2PJnE1p16%;[XfƄLrad{D.ÉdX,aO'+s(N`酛xիFlD_x=߾c/+p[paْ$],6>}g#&=Ņ|]Yz TM};"LSPI*Uk"_+fpG%#nzOV 2li0jk R{Gt+=Hz*־3{c6p| ȮhiDVmv<^,ߴy]mQ4ⵛ:p)[3s/ fq|hHf5Ǎbq`Jd[┲ӃtAqo-4,F5[S!.]yU-|KC)y_>?JG0B)Uң37PYi[ჱ)2Y!vxN&Za$9%hPn5PZ|:.Pij'Y>m;\Cx7kV~ BZ:Օ SC$+;q,)drcA<{g"wr)Tu v9תJ*ijڵrk$"@!#!V~ڏcQJ:f^(lnՆKJZ/v!+)ofgz)E\*1P,`ⲓuGV9gis "ŅLht֣:Yg׋l ~S\ѓ6!& Eatľ$մo NZ2FRr9%eeoMb\盃APǍν7e(W٧R⣪!Ւj+RQCx3J2Np˃)P\]9{ҸܢBϑ&flRƠY0!L& %J(XLXH%Ɵ :ŴڿyMC3́Ѝ,v+ɀ=gucjy#y25,>)"㢎 =gr'M]RdlgrIw}8Cx-if)ZeW!eMI{^Mz/NQ$:n4z"־m B.\l0pᜒ dH])A%4@ NF238zZHݩU/w:*:ySy6 93'gr)u CeQ.m ]Yg/y4^!a333Aan`MR8%+6Y J"crH}"KF2Ѧ[~2slLg#󷈘: LfT2_5:` vۼbU9)9Jss68NkpO8E @"Yq8O<:[_MOhA`>f1K?MN?>=;\gz J;d(,в^WYbD˧N9(5cmν vֱuO.gi5u =>[RG%헒}Llt2OsϝOpr_.rAF7FRϕ=cecɳrPdSwG?C=MR =r@OR_z;^a2DIPl4QNʗJ5>+G$Byi`B/Od  Tᮇoc RВY"gxi-rv[MoKI|x1Mڵ,kPݕ;43lNjgkOdgLeĊ+=v}a)||~2oe9h$xḲt'R{')]ɭ`PNEn*M9$7GNEϴg1}?)L\!DX2F"uv)|̭EVx\Df}qTPD1!9Wv(sddﯫ"|cN*)\Lf|VAI#@АHf5qέ-Y% q29,y"MV{}0Xyآ|mrajJNK5u& y'-҈ᾕqŖN~=?,AQT9sj;R,CqA*[5NH5b5 ^SmGӓimء^ȟtkJ},:FvPnN{0?=.ZFYh" OX61FCg!'xL2r};݃iDqu>[BD@>%:g;zԻяBB Vk JgmqN[0Y4' Qnmz9怛e ֜ фjS^_\-=NQ\Lb hWU撮/$}Rv%^T>bs+) }nF%׍JY2:V/ň8kܸ:xr]NI-c`Qq"{iDJn]~~Q߹ ͡Sa~!!`<'u 7lJKU$E:8F}B2}Q\H`αz5G+>гm"fmy7+8IV9nz'\'P'yysmayp8>&-V/٘ x΄"}R^} PxdgOac$,WҨG̮;xF8+mȣSM~`¢gqN[L%4 {7-= Q7J^a\-unrc;c}o5]neM›1sCI*KV/byuWY_:d4;\CVލ˗2EL~6ws0Lm87hW̒dȘ-Xkk!O]=|tѱY uRi07ڧ0rПt1zpuE+d;+8f0莉,f_|4wqkoxLHE?޻sPy-OFo WOɭ6ьZx-m-m ڮn83!c^zS-}$VIp#@ Hѣy)e#S-ArkV6C/* <]޻3]^*iЛ{T@_gIl/nPW Ti9q~/Ul"ͺeJ;R c]Xf?" bĄyXm/o/ˬzs_W]ExqUC{{WO#k"Dzy3=5Oygדzwp_%f-W ݢ;>(A^J31pfj\J~TVDt_^{1GU҆4 eyhh6-9 0?wŜ IDAT*J:[DSXSmJMV 5OzcMJo@|{܂9i6OMXߙv5%GVI;{oEr혏_?e1n> jVWzE6kyۿl)G$4xj ɮQw0.ɼ)?vJ* gj7S&3O+V,w@կOfZHSj*@a3/*Kzo=5?I+jM)S*@ A}tR-[$I|s`cS)dDЈ!?-Tm0fdgToNgMlϫ/PR@DKuVRC{.PŚ5,I&$IPGlilqyuv1Q2O) vc{dfpSkhM@&({DL@ ,Y?}n0@~>NAnKH38nA錠R#QAλnקh6L#Xyٻaz-Y<>B &DN=w st?yl)l@ N@*ς #MENMsڵo:nywQ+-ʿRL;/)sddn-=#'Ol~n%B(Մ$"W08f<7bY3vʳk65qM=JS?yJBK}w.s_[=clrcduykxaCN]_P{T>}aD+[#.+y7>O׏ h@ pi'ޏ7|Z;aΥ g{Z'84klAe_c dd$6eHy9Pr3b(˿aP]ڈx൱/)FΤgM|GC!fd$h`Ű܂'$ Cdѫs=^O ˜e%y3{poA `CPy*1b] 捈/GLEotvϯ62?vcgf<?}l)<. l˶86%Jtj4D8M/[$@ 4 K*nOٟ'Edn&5VbĵFK+lv3'zVvН>w vΪxaӱέȟp5˳֕j"ڄ3g/Ƅh7a9-J̼_U :u|{TˎMOLx mےDk˓9pQgh)#k?^7=H|i;ϞhVF# iLߎ=xWJϫ C+z\;xDehq/Kn$\Ǘ|{c ؛x8v>eE3R) eV6''S;(afLPA\pW\0RKl=eiUTٞY.* 2 (22,^4Wp|5c0<:78–wxlzsi|kyexy2;CY"}C. *fȭ&Nxv^Υ(8Exʯn)Ό忇ZoʂY^+!zp)zo бx.cj:+DGFy YjSWXO|'^#K=}¸ + g1ϦrstGkCe re\ oĤSeq0O8ù+*LA~Lrׅ ӛ7,+7-3wOg9k'qT_J) |:1{lS{Mp6c:k ɓM:పSl>6ȡ]|FQd]9aEοiLDX4R^ FΟWGue(hMoS׈&sot45JPѽsSDׁfG{%.{E*"g-uYIW(uHShAFC}oi*!l$ZV^:ځDlV?ڻ}ཬJM·K#Dii$^('=:1uH$oM#.YLLdފ8 Ucm˦F{ PGWK`NwX|G CYdAs]6J#%~+F Vw mhC`(3{a4[8 }@fA|~l'б&r,DM]7?|R$HJlxl4!6m|gj 9l6W2 r;͛ 'Y:;b8椢7>׋-{f+KX͆1~ 2)s(R_<| vzWf5_ sk5+.lίe7}=Qs1Gxxbi^TtJU:&0ey@Y^}ZK#2iMQAOw'5IЂ̮![SD Б FSik3Qy՛CIOlJQ06,zl1uXW_Fd+&bgƯvpm'c΁PVx3g1p0BJVB2r!KnIAPѹ7=JɼԹ~.<Ϫ M(I9ƪ*%L qgX71lhj,y{L ;gSF_&f̀eZ~~<]X,Ɇҝ>xJ8u.S=.&JhJ\)莛'M,wNMĘt<1-;ֱB `БzEÖ| [K(f+UsOSpc*3%:r (<$yl+'hVVsG_zRBU-j-# ڮN4B~cW3V]s?(sr2?7/6L~-]` L):^b?WD{Nm!)DqDZ$yΝe?BtX{YuE:/YfQVłLb اJA\*7[]n0 o}JDUt\HOQ_|Mԙ9l}1!5s(?Qz2Õ\WK|; F&y hzP۲9f*@z|X[`U?0ld{'8:~Y^[`O.|0fns0_G `ϭQІ6 8/E@ë†MH$'2+x=(ȢAz;Cid]mɄm{StiA {TڰyLV^|e^)9YlDϟ1?Ȝa3 8K8FGt/'v:{g} 6TKbpg% ~ ٹ7=D//.0_؇y}as֚U q3 F!p!(\Dt clVp|NL_Z5ld`^X7TG<_:«+Q7_g{eu \".ݚTJ-!aN8!NR53tz&|?^zb%+8*֊n9IwVeRrdq3Xʟ/%yx)+2cV 2Bǁy"f|(?V5]]h y{Xdu{uʇgWe쓛yp}Ǯр,@& |5zVCactdN'g}5j k fM-֯ =a(oN}7AښrkqmؒfӺoyF}v+ c߸jCiYWĄLf颅;ZUXϫz3߳-lBtb~w7xkD ԹyUClKhICOEexJ?sNv vD}9]83^X+?q_սhCІ68Bڮh Q < 8qNr <po?uGۧ*{]I=lh؞.-ko@|*gE2Wɪ:AŐ93i4Yvy`97 PVv́(t-'gG# cM4~},oBqL˼Y}"0k P]T8(ho-*vͧz~լ+ZUn`${a;[W?X(^sr[zvȟ| oz1}H~\Appi t4ВzƖjږ@zTTMHF Oۑ=ZW` c3D@T2!?JI =clv%fwkTG-sCg3M,^Bl+fІ6hXG)9p8l\7wN onMΝd' OC}`Qq!Yh7u {fQ0(A?d] 5.z~柏&aRc/2P(䃸A {ycdd~!7 ,0Wb/F#ۖb IDAT1zzhvu~S5هHன#Y0 u4 [+yُkFQЧ4?ȟ0ʩtm筨!bl~:q-СoSrv5w@-onDZAn+!ʹkt5X$UqS#-;g (#  [;=ʋ`B3.b@HqNf鎛؀%2o= ՎYқg;:ΤǾ [vqV&J)󧉺!*ߧ^"K.#_}ʛW,u *;Bt"ɪ[@Bԯ|09Y@k-(ٱo< )CnaWl_Z$ ,0a_CjMW6mVS͕r(F`G.ť(sN7JOAq!Y' 9k8v zt8ngdy]΂|w V}#Rٔ\,QmRy(O:0\Y?J0րid 3;S~u%#S0Ӛ 퉋%cኲQѵZ'^"uIl;BX{ː䢫4L~(r9˨JNQlCІ4dyYty>\ZY3pfEC=cu W^. \EAWG;A4c] mU EɼoXn JwtdS;TJp)QsL@FE2IQghVH)ɇAҳNrTّ(]k;_ {Yu&2YHd~pv `=Ξ z#%po:+*h9 tLDlbҮ qD!R߉l @ts<.R s^OzuGA1^ ޽G=oa{kmu[.u lJY6:o2GʭazQ0碶~sۙ6*y{U荇r&՜mB]suGkA{y \'qʌyS^եvٰB$Ǧ`ή7+WA,Z\j:=rhD^ꍗdreĞRzmF9"}GɉlX@>'9pb/o"($Vih=y#Q5CH05<;ov*#4P#\OgoyᮤzUgN\aopVЯH(C=lu s_nHqؿ>7:ıLV[5ebުz+eJqz՚kmK*0SpypΜj.]ap(./vg E\@f{3UY/se#`3O8:l{Fb> l{.OB3?kHi0{H曊E\`=;ÏC@.0cJ\!{S;bWsρG9_9~Ssw)'  t'(^ۢ$X,:2ی ,0;Pz* mh_gp٘fK,>9]|1qJQH **c^1E5}r,2_PӡKթ9y֍tz}qW3ufy`gbf*JK&&~ /,aMm$ؤb+Xe 5;ӆw5 4NƫP~ўRztU_r: (O'`ʠZQ0r )R{HCϛnSըyѢ[띹5]i1Zu! 7^y{ŵ%l)Q(ӆ,VI]x}/Z':e r m !Cq=g8SORthB͈n 祂42Hl*L)Y44w:kF_aƻPwkbAS-PgC k9NfFM$^3jxŧx U+P%֡냩@&rU YFVS$NnN~ Υs uL{E A[ Fsq2.C0:^h(Dk|`S^rYC>ξ) {*|db6QcWTʙFv:Zuj嵶%E/A:}6$B{ڢɸE7Tx0;AGxT! ԓ;S/d,@yFT%fJJë4D To1} \2C@C3eW;񝔰7W?ρn̘~_ kS0L 'WdLyk^^vý΁zШRő)9>jj]"yy2dZX\paqچ6 q4,wÈ]}n^N# ut*(%OtpTv7r74b ߆d⢖U:yŔ 4+HkXM)i;ւ _kd^bPő^EWǮtpP^5oR\2*>} 3-SJ3B+TDkE< 5ŗg+qpY kk#Q0Ѐ Vb.ǜ2WN4,>p GC2ynD'=l(2c~P h,Z‹=އ(i}[Zw )9̒׽eZ~e'EkQn=^5<&W|ݡ67͇(9SP2=y'e@(-vfR&ѓW$B3혖gw_"}< f5cዣ 7]c v/P"PGfS1V}(A׆=!/Z30"'A *KL:{s<.P0bp,ǟ%hџHne1 4v02]Ӑ{/_R VѰڡ%% |fބ%x.J>gZwj͵ho*r.#L̷;dh˹Ps!nҵUpQ^0kBR6FA i81 =L:Suk|6!E{2ptMnp *MZhmb't9VZ_g!ގޱ9І6b 쉳x(`?#ְl6M/Xŗ d)M A]c̔ ì,2j$:<.h:-,],V~.dEC=|^`%YX(:Qf ZA)_y9*6FAnjA \Lds[Pk.S(c Zz`"H"c9Μ WmDS <>/ >S)?{!A)honM_%ݧlPG<^\dH)$,8iUPB+뎦BtfuJOQ{ܨz3zct-P1,]dlH[Nz$g?_юS1r%q(9@8qn%+ɠS U[&E17!p[n[U^FǷ IʣR:gc sK R0r*+2ڣ*}ߧ~b>S8LQ@&4}c8N܂*qk2ѧ%=kH-k,*f>9bv0ȯ"qEuƭ<5KY~rś$[H03ZǤ$-dEh)Zjf?)<4fNzժps7lB4PaW]^Yk L o~TLY ,<ìc,Rk(Nu K,}>T#wD ; d[Y#O;FZSXp(ױeFߵճ;U~ycʘsYľ`Tjm'ܾUMY]SHQ8cS5fy8t! qNtfSzOBuԏ ,8LV)d*2GR>_;fLF8w fFO ! `FutqlzpLzvE'=&T0Ͷ#߿)P0_@Fww#=qסhn=9rVD3c֟hv0^FZ$8:~ZMuciM󼙺`:szb r :9x^lєL^LYq6[?wHՂ1Rh&s\mm(Ƕ) owȋAͧMIݷt8Ȧk$joS$jR{͎ R`a7FNCLK'b_ȴ2QM:@;Pff b -x3߭G8L )xMgeUߥsOpx._yXWhk`Q1mtpc% Kܱu[7IН_P0x5 [prd[ZZhs2OF}@u*pm?& ·)0= i>!z*Dn-6Юdo#䢳 >\W)4Pqs[CTq P(cWbQQ mhC$ȲcYe'b]Tl],*n=$~ ݬv J!hr`^y 0WީaS*>Br,̜8.HNxahkt^kЌtЛ&3 )j}lHVt{R|~  \ˈamcw|ص -y++ߥ 1OJhI0OD ʢ j;(O/wB Zk6%:2/rd K؝'8 y'86 ӡ{9+fןy u^K6 Ua=T4rnh 27KV< 8quMVW,%>\}PF]F IDAT߼R4oI~gؿߊD^{ gFsuv׈V -g2+<.W6xB}S^s?,onۡOgG^<ᅢd>>|I| dXv W}O+y6't  _gP! /#“>~@_"ߢ":~<@kGXfޔxh :ߜZuAL_\P[t$?^ESbhWدj=ǧV0hJ_ YwGzu#۲k@uLSVvsqY~s rS=Q#{6LIL><7dpV]0{&dN@IeP>Uҳ9j:^U;x$F\DƼsc״['tCfl:Rؽ:ʟ ځ 9%?Gɹנ vu`ƴ mhDmR`NJC)&a#C,=qijQƣם2-fo:%d^LeץJ|fj^.7![-lOӣ=qL`БJ5W&<+<9EK }chF .^D|HXePœHXEBQ~?9^(gcoe*}Y|3tlbO NRH/6LDZK;:[z2THKQ/a-dYQcƓlԐtP+(vо[0rڅr5;ҵheNK}؝,;Ĝ,}^.#X쳝}\!/^sjb =y鹴_jC9K/}) :{ o$l>mN H^~1\LYu# PXσ. ūQ`N=kּMj/o xE,ju{,8!Iu"͘ztysœر:Lx({C \Kd2e(f5]"N~;\ 3eߍaj?rܼtIa\dUB01lb/JӠ p=Nތh~t c@0c-/X|yxG(d, ϜH _š@)(+u>$0{#ޥ꿩R#),iA$B@{x޽+، w^x~7AkU3SlY6TVa0rptQe~3m %#͵uƶeH3uhs7Jg.'U*fЋPW04PԌ0'Kp>YHŞl.oYZ`C&#\ͷ"rmg_7I&R)ڢv\a0:ƩŒ̔D 9׭}Xǘ*c eWhCІ(*0y9RA08 +V|"[Y߻j}G[C L`Q1yj^:w6V OP^Z4z# SXx5d2rcSfei9Wr5\AAp#!lW>ؼUYy?ywJmlXc1'H(ְ%Nҏ)i=i NQ{5|YtپwOp^|rr͝ySjهrl.DjRQz鎖1G*z:8@DZ㙾Sز7wܼ8?پ/3/%KƤ#NCA#(X|y}oı *Ug #=i苗Ȝ~&̍>ҪJ?Lzl/Q{*wjrX9f43lڿ{|\;Ϳ,g3?=ΟXèa8#]8#lb苹GzI Y*&?}*5 VN{} GPWNrv Mx9Dq\q0>wtX/*%` Wv3<.^3 |_KE1!_]c{GMau$D(|!ʽΉ{S(<8aUssρG9_9%Fï}6#w@³YasH>#1slկ'loX:yC66 pwJ\yӷSݎBwXWTf*EP^b&1EMsɦmM'ݔMS5MScÎ(X@e0La1Cf`@<>μ̽r9|;hI6FzZei3ȩP] vz_Ϭ4c.$)0TRZ_`czVUĆz1+Ve #3KK(16$돭@HBC#Lr,XUv۹Du815)F#.2cOhk0> wgxt`o@ #96myAz;Z]nc_(džU 0b㺦GcjYFlt<n 4L=Ea}><]m= rj*n73YafB A)]Myz4iG &6*sڞUgsn~r\|\z^'<Jti>waΈ-scϟRR=ШǧÕmc Tx˭qpF,[BOK.)7Ca~q7!F ?J[<|j2s/~5[.3CST;˚6}pA-QĢv;\xLMm22j#]vɞنJKp vz;%Nfo6k EP?. rӧLT< PkwWJ{AqkNgb>xv#z7}`w)"<@ dH#(P;D%zjbv?G,CWNғBRGhF64q] wϸW&znL`v"آb^ki~ȨglT|DǴX4n=ihLxǏE&,Rp4޿GtS_Ig}qWᙝ f\X USTCO8}81q/񪳹eB X?nNzΫ^gzv<W=ͭn[|SU] G`|?B4HUJáBH804f2c[1 JF)s6iRoX7x\/j0'"nE!kXpL |7,UGArOKqcd^h7|vj,\IbegD]v4DMĞ Vq :F1eq;mW$3,Kڏ1(Өr(ܣzЅő &D]Kel2a{LGCLdx @ @ '>"8sIu9JC=-\e'ǒڸl7M܈m{p)IqDjx S@C#dX467y m\oP>l"e%pa&W&:.XWmCm4.+aЀVPF[J~vCic$ږҲ;DϩoYG੽P Âs0M Lj/ .@ُ[#r":F @ @ .'ђTNc4Ԕ*83M-mu So &\OXvb [=aA,N}:hUAiVFvp4LfiZj`pt,Fpb!x?`59xsX,9}6|z\3^I!&B:JAVG18 x:s@B[2h^IC{#( hhFʅIf2#Ev0) ߎkSO@ @ kd}xsʃ$lb#f[e`%wY!63P@ >[B B@}NQ+( >kÝ.R (nAAzG(S*au*s` @Z6/栶z Ĵ=IrQV! 9-NyA%'{2T@ @$CWD2 t7EA`Pժ ٳCLW *u0#h.u4SS {'*B_YafB% SmNYq N;AAѤDž0+ `H'Ne(7< tቜ_|mI.M` }緜xG@ 0.ysԒ4~{ TT{ KҏdN:# •Y\VE?Hܬ~v|sVfxuȚ݋HnoW^U'&r#fU0I \4THU>dMψ&E 8񑌙e$FVfS2G9h]F՚z,YxbӶ/Z 83/ebCoȹ >t4y@p2# @ ]eގ'^!+8PD(3G%4}^}SJ0{ɍwx97.b/.YPGsuvC8"l$?ģ>ܳȣ!*KR0}[ jƒ{gH7P=l3G/CR.zB[En#+,}M15iLQ36$Kv6oΥ 6!iM8~VΌn#BdB֘D\&|H>.u?+f6?4, v~} 鉹,Y_qO<~wjkcNr[/C@ ȒгIgJ;4bt[%$2Q@bHj0(_>~e6S5avV_}=\vl 6~׏9;RlzqPc6@ODc/mO.A}U~Ӂ7]jC~C\V3k_||/13E@tϴY\Bvv]جk7kw]v|@xw~"L9d~=&iY0ۑ2P- hCU鴠~p#2\35gs#jnTg{ogn<{g5ͧB>U|G#R_V>Mp Yl^ʲyc糳^$O1Ql/OF L6 B'~-jv[{{m%}ؙ= _Ͳ̍y}NVPYtj㙖:fs}BhZ㻋GQsJ.Є&0w\qxFXyoy}!=I)ܛP3wSQ>9FM'}`LScZ̪BO 8x:0č_ruƂͼ=):Oui%zh1=[.Xs7y;>yBMlt*r.<+ݯr uD⫾4%]5frkأ˯㦺5ܴhTԡUgs0/l7峳SϳFO3&3A|2;@MM;ɡjMܶloՆ84R}9fP:grSgTP8)aXi1mI?RP{^_6_|!sK7ŗBl IDATQ,)ںG3Si%ղ[;GOv뿿 | ߻T{CMhӆM঳(?v0=i~ &3a9wp< ~XdzUO3&|g<.i@ة):oYƣ뷐q]E3fx;?VNo{qr W @e=.gF".R>mg7};EX8 _1M |z9gcuzJ4k:g-_A&n_K#nfiC{+L;i!{ɇ ǁfV"б8 ݭr~ 5)7Z L$DLǾId~6T2Р:|zH Ձټy4O~[^ͥ^B*sb&Krp؜3#\_C٫~v,- "^z9Ptv 2ϻLheas~-..QM4Qk*67Y{.S#/BElx #f07qq ?k$#\@nr vÎXztR}4˭|L?h \9 r6<;wkay[ J d sw&f{_PC\6kD/cSA>Ei~m>EAhT T(-僟Trk{M쎡R$UGZ&W`+b|NaTAAY._e\{-[`W^E6ԍ@VX jfO# ߍG9LGnR;MxxXHݗ+q>|m}ΡJJ|t4xuYf[^딚y5!Ѥ+)."ck;sMw\Fj=)/bslBIybsϔ0 %Ϲ,w__|H\{ 9GQyƁ!ls.ju(I ܰd4d.X'2豏YL>XkZ~%6TI|p} AT*3BqclE,ip }e|W2x6uer_Y2h+(1Wy9ΒΠǹĄ3$jJ+Lg|cBuݻ*[[ب(@Urݍ^T06JH]UT(\S 7'su.J.F>t4;yFGqz\ho:qpZSaHa@3+-GT&Z?:p!7&yO|J,lm!Jsgs^ɳFm?[ r"Ɠfߎ=ѩLe$g<*xs aɭ r?PYs+>gg0Ϧ0͗҂D4/nL[faOorOңl{~IL!6LK >_|eM_du $򝋘l'w~Wٛ[=F?|i7WzaZFlo3n}ნ]+8̊_zHO/9&}%~,5ˎl=Oxouÿ%A5;xK8yµ<̫Xv 8d/v9WF6˿W\ٯlnFsu݆7R3ֽzg8Ћd? uwq( (cfচu+5@>D yC'>gwinfϋ%teB{,bK.|.?.㔋i)nr[|\5B|e_wn:OnGsK󩣈w_|P Ųg!ɟ{Ũq-v-լ0o*R:]s%j,ow-K[xh&f PsuWȰ?{>ocqRxIױɌT} 19q{fvZ:w]U8:f,0QQ@y}юU:}X^%;+>UQ󎅭J xt@yJڅMVUv{)fT=W6,dɍs"5SR90ƓnqdV0+rh 7u-2|x=ޜKx5oO S3Hlw,ea z…鞆#ۏCAYfH+g7ԡÕ*-v{l@Hy|zc #;:󯼕JdLd|^3\d>en $]#;dv̓Yl&J;gXvA27$3z_IØwiKrGwu&cbV#xL LInk}qb@By .m27?씅cܩ e<)}ƑU4H0s0߾yْ&aEƴ{)Kg1pRD~?v] 26 ΋&o}tOP~)@"G>]<)}/w&6fihڝtFi╸P\XEC6j{~•<=ϝnP{^$BO7ж[1h`'///nu/s9sxO.I2W^Cѓ,5϶`^pr" ]ƅB*]XW%2(_{d~';&siaWTbˆJá?#jsd&js7yg,׉_N1(3ي?oiCmcjW׌a==tNZ]MiW.YoYo켝z _yFn@@NwұoJ ӹOg2qaUfQ/#$cº$ԠcyCd$M%)ҹ|\|ж*I36ឿSOcap $nCQz1'y9v#7yxG&#sg5yPDrdqa %P3y,{1<ʻfpRijOⓝ z2Yajpף X26%Yi)Wfg\SdNtՃ6xXu-{ҿWk$YTt (@5D OgbCg1 ePw%% \<;M8H][㻂J,z L<ů{aO&1RPXõ? 3?ghTgR=|6mݸ-1;gĴ\r2dts-Ws̓/¢xџΫ2; 3?eR1[NL)ZMô1Qsm>6xVNLpd=ڽE"ë+9P' ~G];Ss]06 p -p.p°6"$|v 7Y dƪCSHE cO[hmwN?\BPPc}p"%~؎uv:9Qbro.˫z! J8L}cSgw g']]wǖQs->XNuYx`1PShXչ}-i%C9H`p Z Kvv;Гq KLB+d!H]'>(a 9xhj!$G4ӆu ٫iߪALl'HFJ|3tϟg_ٳ3OdJ$^7{ YYHaluRjW#qCC@-{!{\7lhF{ qMdrp0@ vlmqB{IX!weE\y~ !0kūev!nNymÌav^w~J(VBn%,-E3=g>v޽鹊Z8ޞAX@q9VTPK kF` Rz!v f+Úpdy)jniqP)'cpS _ՏE~F3y5(4Nkp8X5Gu>5<Tt!v:RMMuNv4;ֽYՉ72PuG`]12Ob똹ރY6(쨃`gCu ԸE;,ՁZQj=l\j(k=6jkk  TGӰEyݘ!dTIu9fhR5l(8BAIML8@>FJLvXe[4p}cȘؽydžR jT7ر}EIEKYu>V@u}wWIwy7p|kLZ͞G'A:tۋ$I;e!KLldʊBn FWݨzF_dNͼ5#T)ۑv- "W X#Uc n#߬3p}h;vP'4϶f"U1[`w;xQ!ϟ%q+۽RE]pZDѷcty1 y{~/5\?{@h<?ÍptDI-_9f#Ho';1^p" ]isp2g@7O30re0+ 5myEÔ5)cdO'߃óD;Qcd?-HǕLS;a)xm88)Y &^{3ٴ9A{#3z&-dm2T]C-۶ 1XCq?bjA~#ܙLhY@TN[Ğ.y4Txc#+&swY,*uMlVYթ#*a.2_4P,RnޘWy ᐢƽ]bɽ?þpo|:YǦrì:%lG(íp%q»p+OI{|~H(Zq2TŢ=V>SX+oV]KS )%e=#Dx~8i=urhUPxꂟ <4A@=o2v=[\&(gI s`|t:1%2|FfyWFLRQ{cpm(R UbX0˵w0‡Vg:c (-:GY`ƱS[*T殧nw$㡫"a\Vf͞ײuH4"rPmQ)5 =}&7C{#Ug ocՄ8e0z h*Źl4ֆF;h=~"Z8Pb;?dҙ\&vwIB]]mI2,UK/rCLtn}*XܕTA7XSe]Hg]NX؞~@SeNÊ,e5a$&R TSX۝c0'TC{8g{1:h>bՄ1\N@XL<?ye65`ݼa_x"ogaod} l[3G1Tң% ZY߰Qs{cDj6+&)O 'qv n=< J <~J+h wQVYj+)u2ố'uVHMiejXs`a;J=QBZR 767TQTe x6*[Wr. =܅NS8!wCcԇuӂKab5b03Fk\V7{2;;1odU|k< I{p IDATU\ſOI!]^mP]$WMj.kΥ!Ho8Հ Dj7u8#ɧNn pdJtDN7Yvg6 id:hdPpkg%en1tM<㊶-$Lgr"IOB>^44Ճ=v7 gy ʨ x\{ץ;ߚ j YlW@C#{zqesu.u@NL< Is{7 xlbdCǨ_]uݥ~MerPvJ X +`o-m=9f j7P޼R4+}pS4 \+{Ɋq^+ InedoL'bgyuPKO17sd2|ts:^$9Ȫ<d$B0qdgO0ra[c# Y +YJcz5q>h*W}%6R:e.ӀCquMJFMw t N^OƔWP4'2tpG6>hZ sTp4^ Zi 96HE5Fjr0קY5i  g:c𔁖Б#wdz8͖6R`~#aDI.FFLrB9*zP8aQ fTN6nL/sB;r#C?T `gNt{6크VUh H@to*Z}?ؗuД@5SflR 1zfk|m nٞv$ƙx*,l!c)"gDb +̬:H8]ArI>砈m  Py* HD39=KkOM"pQ;` H|ۜ`YC,dOJR}ksi^ YafM =Bw\N ~^JϜLa{3`d0(d*Hn 7[2D\?!\r=_Y?F[CL07z/-E|1K?gjF<[Ty1j{?Wm1zv2L]߯(7~X]qM{ʆ_/C/o@.Jn> oƩQUy0L̀{+p(An/@VL8Un/18 T2967{LSNn/Nq/`ߝDX4΍t5;3Y\뷲LrϚ.QB"G؉ ˍ^E|}hBKt 4s,}'c1ey&y؊5ggc'I /|9XWkZOPst"vGW}[s%NJ>Su-om;cMlml:izcG6u@qQ;x?N,-߹Ϳ ?Q(')iiUT[YwƖ8ѡrj4I:cPٜy;a=kOrL IaHDڟY1Dt!L|\H${S3ΜJuh$TUm0ٻ;k e-Qt!lUۏlL~VbPοjjY|+x~:_I´~5`I>ܷN]7Ǣo뀖ة XP4wt"@fe\r\1M ΑujOshQ+ҳٌllޏ616YTtnIwTY{Y 1Cz9J_ Z+?|gqοtrxsX2[&3Y[~y띜=}HAq(>7)RGp 5*w,xc˾T#$Z%m]jwh%f' ,ë*8rnIቑp;tgҹq:R=NZVvT6tUn2EєjܷvRֵܼaMODwO VO<= j,%‚Tף_WJ ֕Sepy>#;t^,rk%4 -uS܋C eJqOϠOd< ؿ&'2YHP^G悵)cܑdJJ8xw5/ve!JIg n`ӿW.0 ŴaP=2Y3q<=R'<~߱ L&sG#+ؙ:μn^_ ٷ?`P ̝ nܧ-=)JN$^kXyF}sx$g( ︑Oƫ VMRC{ "G 8;U] Hx= ,\-⼴D"1r  VbG^Śn&_~&9u|Æy`U<ηqXqrǬ(t܆8r P͌1ml|f (W1i )-/$#,޻l6e[3YnF8GlzCR. Xem㭡LУְ`,WD=nτ,ƵFό1IxyNY?ߣx!C6VbwQ_gBXH" Z\ZZVm~]k].Z.ڻxomjqD! d! I&l3dgd`ǃ>||sl^*dyܨEoY_Oq$;Y\{ |w%}?V5#]/7(fÝ9$ LnkXV3aC JCh߭W,἟bg,؜x9WspyHnOmtu%? y{ ʄ-աⱏY(*` bG{IG.pc=rSVCֵʃ7}Das %enrK/=`Rtʓ[!-O(XOt3s$TZ{Ggr/edy{x%~XƉ{58@{y_~nI 1pJIF]=t]Ix>CKxx=^uaihYuֵ<l߽W6/;)8hJٛx<[ ^[##k/=d'o,m.חd 4Pt, Z7:H5 ,KRwv 4kPoe X 9N ȖG/Hfr*TnⅥxQK=I1ΎgԺh6|l 78|~\e#Mp;7^N3;B]t ?><9W݇o :o WlT4ѤLۦ*շ+6:Z Dvnw. ˅ X\h^DJZO>̽ {FV}ӪU{xyv TwJtMìoW`kmҭJAZ?{S22H^&f(@osZ crNOb&2$=~zޤ#ɰ&_Y)d]o]7[RhS4`AI5q8oY{CZ%G;ih:WO%>TMDn}9Rw;lm; ih=],8 R`XaAZ+|< wӝ>; 02?yLTFCGT{)?2DǓ 5d4%k1m:RX:5.#jӒ*3=:h2|=R8i^ę#{(CL. v=T 3oU#!erG:SD[pbR)yq`𿥦qڊZ2jܞNM|2=wz644ĵvK-hdh5$.|',!nO I?nYº2m邇v+i,4z0l:k3Fupُ:dW`o=K$!FEtU'%-8txj)#dxK0tM;`M2iI79ׂ֮G28WÊ*j?>DaX?^CQlat[7TʽxO6!Hw]3|a;I$YZ tA1뎖ɡìG&an؉~O|n' gsɃWeq$%bǵO:)(=\Hw`Q6Ic0٨kDAێzO J"-,,NSTKJX:"tx !B!g W3aq-$ZofRT@ûfUf;F휔!zMN!B!8Өg:ţ> ?,]wܼsh؜~Mv=}0a~, 3Y >/y]4:08$I׏:[hckpqR5r?VV wn,g~wzfAY`vsFKc|ՑN+OPUጦ^nzy'B!B)$B!B!B1hB!B!B!AB!B!B!8Hv!3#R5z6靅B!B!; !h߶n !Av!B!B1$uB!B!B1hB!B!B!AB!B!B!8H]!B!B! !B!B!㠝)b<.valR Y(>bfҠ[~ I] !B!B!“b2i7^TGL6kTG!B!B1I:z!B!B!IRD٤L ') -78B!B!BqƑ@{Դ&|^H(] !B!B!Ę}"ӉqD}&@'qퟌṈ}1_xط^B!B!Bhf3ѫ:>F͹?8Ⱦ?o,+YV5'PO[H0LaN;*ÀYclmj_7g~~6<q͝ ,wUM8x] j6Κ.ew+:R r~ø}|ÉkYdkʚhyO-t;1~>n.5I![NWV&v>-ބ5:^XĪ-~qc'a}$2?FZz6e\a#cTkԐ_΃R'b<.g5i=FyIi&8U7Rus;o,aZ?s}+nU2XԉZww]Ս-& )2 !D8W"!S&߇B!(\\%jlu\>U3(=Xb-Nݰ+!OӷU҉7~}G}l$jܸvS_0(U@?>( GI'^g*SLv\&Ł+vTR/G1bw1-1Ai;z-\;`k/CZe+DeDw C9't8ֈ8ѥEMΉ Wq/?ŎGUip; u\>!th=@qN~T&7Z /_-b#.`QGI?Ϟ,x?<7'[X:Xy6J*p'C혿^ WV5`fGOܯh<^c=\8d"%s9H]md 7 nua y )[ Y]RG˝k;+EzF IíШ!M n>ٗ̂,<=]]ތ7' !'.7!B!i_,6lۜsXɵӨgC#q>^*/~1^N{IE7+95Vh6NLx88K»V<Π[JNZl~Fvl^v0#@'ِ:7;VOTr!.z@>&أ1?tcesh׍P0Ka0)[ $-SYfPEA"$ٗ@|R'J`k+B!B!hH CG>h c牢azR0Ji"Nk'9=F 1#\I>muaX:IiՇ~O]GK?u{!hbHE!9*  haY k8.B!B!i"?nfc>ݘIˏ6'/3/8xJ¾W- =_E-PҢl9d%w!B!B1 H}:k| QLqJHDIGD ן| F]?Ծ$xx%<襨G>2^jeC{xr֨-B!B!ڧ:UQһk?!4> >w>OQ։Y#+0n5O=3a4Zg@qK:z {u+p]6KSB!B!b pvP&hۺu[4zcpnUf)=P'Xg^-TFfsiMc79N!B!B!'"dg3BJcG,IզFAacJ08]:}aYeSE)Lԉ JBzOcP6΄cƭ Sut ڭB!B!b$>fxf\QvjAf\adS!M4Qhhb *l70_Lv-^^ׂ ,36ZcadUsX8G~s8prt>ck+xްX b`eC[Y }]= !B!B!BӆydPzXHBcN7YpKҠ>l6SKI?<)HhN1~B·q߀ɋb>MfU8ɇ'|_%C{&ҴkyNku飍x ?)D>-7+(%:§2`M*2nl /MUYf ;9$XM|Ӎ`+Ts>,]N!B!B!H}:Y˼?Qj^ XAl9p>U6iN U_Nn;V(v԰n:ض$ő 귶cބeTPCa<<j:V?6ZmWÒ /B!B!U STGM뮻 n޹pzM ϨًaS&;'Z#gU΍HQ̭SVAqM?uvjZ=xzCSm,uVEKkrFS7#VW@rc ^F !D8iI~K/4EB!B9!#ڧXȊy*b -f GSS=7~ M5a!c.¤W׻B!B!b"dB!B!B!8H]!B!B! !B!B! 9ڧXAG~57B!B!BI}kOu1B!B!BB!B!B!AB!B!B!8H]!B!B! !B!B! v!B!B!bS]3٢ GZk@3%]XۢH>s][{00/~*s }.cTgE =84z|>_ʼnc3rЖvaCuYDHB!B!9if͂]_uO_.bfIn槨hNZOu M]rS;K.kaGa|NwvnAA{w :LyrlwG ?wc(t@LKh Rᮚ׎B!B!"$1a}8p m}KVp^ >yw>';>"]1M}‹{i~g` 2/X6i\#l:/x: ϶MUB!B!BgPN^>-HJGOCWm q>+ٗ V }2;Ͼˠl#lOMFg`GĦBxu5W -hb9ePzܕ5I-φd,7M~']luMuQyExK/U13a$ӵ |ZHE)z?LQ B!B!@(-h7u8|jfZ5X52t^pKմ& ^wFNt8i$0=7OVy;i8v0-iJ]$L܍>}a$ӸF(;+sYqelۮzR>x#`+g[F X[LǿWSi'v1L\fU`U I} 7kϾzGj@~U n1$ML\gQF3&RO9ajG%;m<>/k^&=`t6LrCVԵ Z>nGi-',>[O=U'p-`'C=me6 305SO%^{W@qu=.ȇ}=sn6^XAӖn|o!B!Dng[U\:hv1=|^Ȅ.K>bmGhaf,Q*4NPoK S GY]8Juvn\K:nN\^5XHgI,QЅis=lx {q%h4xTuѽk[#]nt{t8|ֆ6x=jU> _$q}C|YdvqTh5 !o$ÔGUb;*ZTYDǻz𖘠~T@eUٿ#i5t` &UVY=.i%:ԁX>u_9$*#5~;j j@_}BwPFHA'ÆR{Ҵw+!Dy=ݺٕb5nܻ`W3l/_&z:gwVan;7[Y+«3}Z15Yq8Aƒ9H8ТɎ [ON3<7_K{7O;qᯅJ)z}=!ac-t3A<-/g !B!bjI=b~q9?BU7WF>}s0z6Eoa*ܽ+)+nwwPh~-ͩs{XCk Q='d/΅B.x`kwW`:wLI ^új⸵n‡5;NPr:po9|{&lIw,B!B!2 0Rn:ʂ#^JE 5wd&Ԏg3QeJ0n?ڴS|o$`$4hڏ&,~ #GMh"3B.-J@n0N2;Hs3<>:`;'Zؘrrț5hrD؜b ZEp{&xǟ ؛f})iaUoIw!יTj#|-HCf]X $;lg=,B!B7 Gɉ7ޙ3t>śb܏Q4j `DGQ{r;[ai򍻚@ _ylQd2@ ]1uc=[4 ;Vl0~~10/ݱz~pN5zXCHG|~0eOcFiO.ounʽ0dhR&<YqqmI6̓kCLL:UZiPs=%yAxUwMmz$Ē2'x:'"(1d(ltK]!B!SNrG(փh0I1Wļ{jlᱜD-iptP1f&_5Z?3Cn ?+ 5}}MJ;?%:L< O0.GN7U+xo )\YҢH'1Sͤ >~%ՀO YtiD%khu'40!̲t 7xOsz.?~!B!B>hۧ h jl}\ocpU:a;k%ШUXfGA@&݆lbG0 TX-nTFGw[nAI;8煒6"h.?C<~ݾGw^ h} 6}O Ns{KFagwTۍWFkkSB3e4`R`J !B!B$t=cfvꀟUߗ pF /;fCa^/;j'4^ZĥPwg ?hv .6 NMMmM7M↍4N(5\ܚ:& IgH0Ov`ehFP+!0rGxx?F.ڸi9NW3.B!B%J<yfP4Fb($i:RgmQB|vNլWσGrCzl1$Yǧfh3tm1) u(`]zA#Lt$fՐ=EX>/ѣWj-č.HsxO)ƭoǏ4tthS4ڃyO-kO"Z ~?{09T\`rao@la{˖SIN}6{Yo7Yo9!B!B7:ʹQu| gihsp܇!Dν)"/PA9AqpUn@Z0A%oo[5K@e)~ndd2w^n;Fn;bNC҆LVaoOcPvs[CG3lwEJS@ǘkC5QYAw\ԞDy`[{u65QS?& 2HQt&:jOUy <!1^Q&.s&j(nH_pꩧ }M.J@ g'}ޖETաgiXI#iJ^W bNDB!B!@{4ѤS<Zȸ7D5CCbh#  Jʌ>~+1_zt>sU6*`o (icȆs d}7#*x4$XVO۔v]5sTw=ة}\|~_ 'wMLU؆ FQ?0Tp7/ / ӝXf>lѢ VBs߅7`dٗFy| >]^‰{ӕ#|c7(e!ppWQV叫rsG\hܒGhi 7Pxҿl&pOC= |6xsmRbc߲X2+UӁffpS[e<h S#^!B!b 1 l{lyL9rf?@#ߚI]8jN]&x<;[!mk3)Ka|XE4 #tpNxw#*{ESxmTqpUC߼~1,x-aY"q~blmRS 4@Ӌv up& N7Vj6M<ɋb>MfU8ɇ͵Mûƒwz`sI.iI4-&;GZpSu]|h#^Q+cX@EgPH>!ݏ &[ܸVԐPʖ=7|I' vb9CenKchqC/,z9\Nv>ل2[IQ]IIo;O?zCQlat[7TJ {O!Hw]3|a;I$YZ tA1뎖ɡ#-ۆʚQʰ&q-B!B1dD(t^Ey_Q~76\R \&ZM:w/_BR E|4P ϟ2GdQbmHi=2K3p踎~y.>ONj-j-]]ig·7ρ_oے\R^;)k< j>(̀_GsAjZn/peԯob<8IN,aPBQVPѴ]ڊv`hjn*U64bVڡbSPۡS*CVC!5!iR$v88ywڮ@l؁.z_?~7~~L,Cz?O6+#E?W4%'[iV65Cz:>^db]ZO\7C# 63o3v:}wDͿAgTIS>{VSq!v~n ѪYR/p|iv\!26~v7 e7{N>^(4ޖ ]V 0e <œt5 l6.Z}4I'4GIgH2rޱ@ KL%RM2^wIg$9aMKu,M=)z+>n} v/P ^ =w|(puѬK\Z thުy ;/1|!hBlq3|j+e-kvՖI&*#,~;}Np4 5_"Ձ .x{|t4 _K}h&E':9Www.ydXGGI$ITSUCԽ^[(5.q1ꦧ$!X+oxvʥq[ǥhn6v(4^Y2"2gGq\ڷ_ri;__,Mt)˹>![[f}t@M R%I$IK5}p5 OiYإA%{׸rvr?eb  $I$I"gHмy8*vAҲQyn/2s@cuq77B$I$iL]0 ?r\Tukbp3DkYI$IaОȑTo_vUE lMor{-l,]*tEXtCreation TimeSat 26 Feb 2022 12:08:25 CETJY IDATxw|SwnZhjeI+CQNW*:\ɸpePBeh m+I~ xxsC8|_f paFg\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ .7yyyJIIQjjRRRaÆ9,.iZ@ Ն ~z9s`!CXB-ݫ+W*22RԽ{waÆ@jհ|r-]TE}U>}jU4sLmذA-ZЈ#ԹsggeP O>D6l-ܢGyp#*iƍZf^xP+]p)믿jȐ!Z\DZ@%,\Paaa6lKV!*h:xn&g@CTİ8PT=d2]PǏ$uɕP;j$Ijܸ+v"*󓿿KV"*ԩSjԨ"* //O.ZP .P .P .P .P .P .P .P .P .P .>3;veKT4l0g8zjb~7g.a{uv AO-\U1ydg\0### gB-sƟ}~>%Ae¢zq<BA:|F XTZΘ%,PɬeS4~\%Qeд}q&>[/kvu\̩ 2 ہo:t -'ݤ8E8KjbޔZ3EcY>B- c ٬{n~:1]J.sKRz2E>p=2sbV-DFoIK>R%+F;jvϕi5OGҏ$5b^/=}]Puw/=ZQ5acP~sc/X@eԲ[9run(m:bn+jUli g:M[ο^YfeWթc4C6a3YZ~M=BK5$g$TS#^1)KŸrQK{#l F˒NRVhe:UzyV熪Oh| -w^YG/N{_n%5Fu u๊v\ךЭԮ >cokum+u,ꝗRE zA4|ٴi k]ڎSuD=?LM 2x_s0:A'H%dғm:ުעn{d)?4;Bo?qz4.խE7 |tVvXĢtX]{Coƶ2p.@>ςWZu]I;2Jnc^-_'>Ne}׶V}/6V_ҧ۫Py 7oRJә5ɡzQYkƩߝ!:-C5P:ؿZ3=\C7(&b6ۉ5U6G蘤TRYW_qF8l_ /Ui/EZ5|6HkjD=Rz ׼K.g;6^}B.NvbbbS-Sz¦CZ>|F|%*&&J,R뢟5DpSV!B4pzO ~kG[ɦDEN'~6!^e)H7NXczocj~WZ=k9cJ۵XTh^*|LhzQ w(i6mذMSt!%'Tt= حcȫeV:CڟP'5ե s:udU)Xnc[${t=vWsR4%I9++K۴萾Y2%i͞4@M+4쥧]s cRg~IRk*b q IB['.Q#qEqZ͔dWƨOs8&x{RCR:|}hJ{P꫗Rz/+p+c^-kz'#K*pl=<-}k \S Wk;I2M?2Z%Ycd:$IY҆cڡ:>فѥ6*jDMy}u*^nMܗ1wvw9Z{|@ YxHԪ$\a[a/܌2|Ko- hYYePd)ݻ`쬒[tH_HgYEk،&YQ?l/ț^,nԣsPC%)B~]mןEIF6WPNVzgѐ' ~NiQZ +5}M0V:UcR/wgK_/+sJ ljm"NӾ?ւ)]5(eJ j>2FW;t<:{[$)i<}y9utտ4InW;I9JTbT=/yKJS#>׮gKJ-XOAzv=~mӡbU}9glmgwOח/'a~uU#Y\jUX;?WՕMf:F\R-NGjr=1+)Ml2%z@%5K., םTgI)Zigj\WZ2U-^CT1)A'(bv"]t˞zr2%[&^]ՙ t̩e(} >TK&ُwϙA#_GZczsMu"R|\eҫ~~%痆ꩨnu$= ]}zjV(QKRq ϡVa=F/| qA[gEko:Kϭ'Giƽ:j~G?;Cꉗ mrBm 'fjDm8 8_A %&4sQ|HS1/?{׬kSA!,X85\Ħ(u{͹y* ݲ M}&ٮ]ofנ.!j`Hѡ] S'MZ?V~jaHվ?õv{&^]9rNݦ\]]Z-wtS{__*Jdz@`}ljP[y^`Zuk =#ЭWxs*SXG$_qB͚<@e[ Q@ ͙3&^zt 7N&dT/+e<_5+Ɛn5w4*/)]M\5\s;J =A^ o~eΚ ϿRcxw{eQ53rz^],_ ii~y{XUZ5i&zkgmY_D>x?^~kz /uyGE o;cey?:X9YֺH~Ԧw5a ->(\7D7ޜ2F}T(VZ`ɒ':#4d 6٥YS3F$/5bdd_2s6NL=X CKns2F{*KچMÒA[D%y+8`3qJ<"w@uJ:)ʴy+88TW8<;=t kyZsL'71N{)%[LrFO-P>Z\umARyQ2+GPunT~ nMt W?DWe0eǮ*%4ԨMW5jSݚKЊopcr>A!*1@%](``R".сpA̩m;SmNg8 b6g]l5;R l6pIA?~UW{٥FM]xX\N TB-\Хx.@ 񫺒8Kog\0̩GT%PP @.šP/B-\#EWBRdgQ+.a/(B-s:Z6Z P i@tS]emաZ}hD̩d%\xj@5LZm| <\.f}j(C\ \R1*jj9Zb>sgQk[cƌqv"P*222$IZ]J ֦ehu+'IN*55F PZJePnjuK۵%Z9$m9Wu7GͪO#Lj5_(EO97Xm9:}peJ6ߖ2)p$MIOEkƺ\e[T_Dv /O>{qaXoRCK?N$(3TەzJnRuu=MԿ bh<ȉLЄQyc P.=XNѻtSjcΦ+;4Œ^Wp*g_Vt۠&O҆ ֪'#Z삟%WۦR;@>Gf$; TEE'"Sl^>']k;nQW\|}hI5:U ?QZɬi`}ێ'1|v"}M^c ǖqhЧQ5C~8f9 ɨv>Ov ,JEiԭ-^=$ɸ#QS}'psMCM Vij8UûE6yhse/֕OUW%+h#.aaX=xBaHsR ](ªCj%%H}ZwvR‘6yȯLvkf8=ekCZasoJS@%;Ij*`9饛~F[bՖkg_}:*qC\לWhͪQQw_jofUgN2%hk Jrk{vuE/_{]߬E.8w>u;܅V} ZΪ-KAY95q;G4ÂuT}4&Q=4>T}q "Pg5mZ7{ :/[ ߒj:gϾr1y>Ǐ`5КvSlM>l>Ynр[t}Vr-}'TȂZO:Yyos6ʓdўwb4ZF^krN>nR{jV~u gJmתZ5Z.4ri)T0Xr{A=wV//+tɍ  .|}D2(y_^ 6I6Ҙ=jiEiWoۤ. mxIK mՀ[*sV,xEʷQcl;] WnR_*ӻs^t{MDpn1y>浨"ׇ3Uo\wBGg :T ϵj.6垗$\xhz_:^kVa7+˿8x$ectj]Xl3W)-gzgSx!Bly8JbyW`1ӻ6-z'"o}dzGEZ0gϷў=S٨Fyi<ԂQzn(Xл>yGԔILꠧ~Yr?99Ih_1:%ͺ-]X"C:+Wm[^[WO\Vo]N֩DGfht_u4ꂽsNm߾][nURRf͚P n 4%IX}tU_E`Ӊg%tC)7}Mgڷ.M-zt6[ͿjTd\l*kթ Iz _59[;{PCv[VHfz_G;,ϵ*}g>uFK#iϧբ><RڝQx]§)|IөlYE᫄zѣ|n쪌fTP}~Jr67QC 7gj=[Y;KЎNq۝ݗu6ȵ(nI2ddr5K^B@?X$Tt?W_HRvVW6 {n*&25gG2:Q4=|VytLg69K YV_D/hO Vͬķ4| 8uC7.Zϯ[ S4}Q^{.`wV)]_rg]^ԨwkA|afh*n9NN('ͪ326-K2;ٽ-)PSv $*"ps;( 4lT\Grqz,f}2hkanRЃ-P}Ij+;oғsiñl4i: .2U ^㎕e6wX<@7wh'/Wz/=w?^Ok;hs Gk{ڼ^tFLV ,G㞩Wڴ2EmTЍ^ջ𥁞W{U`sTŬoh{o3Mx&?ݠڐǢk|7$ww5h@)#MΗup%'{ UrUxC`Y|Vm7IbOʤz߳{CI&kzvyxGcP(1zORNǘ5kǵ}vm߾]w$ yyy9DT٪~-4Xݾgoym/q<oTZs~ty,Jpy!IUoQ7QOZ68B8wZN+oJёkΨinW5?vޡ'[&ɨ)SVV5ӆ@(SioFs6 pL0ΎT}v\p>>YW=ۨH(վ^rUZrB?0ˢ[*rAWWյfP+rdKosԈWhzkZk{+|orojw=]<]kjdӃGp #W̽G[l[WȒ:`Mx6hO=(KxmA%I_=;%s+&4ei}㻊Py)>V%?_iBUԽyߘGM?Wq\@Su(h AEl}KC)xdv\K6=I[]xwZU|kl6믿=wl!I^ SA%WzoUǵs*5 k$OZx`ht{PlI86m*`0j-\EGG+,,`Ytttq\jP=RkkO'Pc3y[oIϭ`iP7Af@])fHD)sl?ay~ٹ9j)cNoqX9[ڔzhF]4>ЏTMd>rwvW{II*1#z.5ҭ+ǁLEtp\ڟUjSӨn~6iӋ,{ޯJUOF.yg ԰ru/Z_z2{ř3pV=SNa魞Ի.H-9G 4(<55U?zhQ!p) g1`}"oU'I2hmVZ߫,I׽@iYʝSbb֬Ypedd`0ۓd2o)5E/cw_ݲubyyK>>>ׯ_)))ZnZYYY W@@z}ؔ;+$_ 3XItuSg\._y _M'(+[A!putjIn:lQjlT (T͔rT_2{ ߯Ç[2j޼y`),,L/.u-I:||}}U~"Ϝ9oF/|ƌz,'<4x-&Ϛ('VM2xd(7CiWz!)pY7sUgѱRB-I_LiY>!~i[ut .F>M5uh:?P/mPg6mb类Qn%{6V,q7o6AގwclJ3ϕ]lE3RiSGQX嘑Vw;4͸}^;[O8,kˮ,ҕhΜ9%'''kѢEz׋,ߴiΞ=M!L5s[Y=S->҂~wޓi u祽٨cF%Q^OܯW8/pB-pc(o?|fQvR~=/y kZf"""c4Soufذa.ӧSbyz4qD?Xy>yxJcIEz#Vsj  V@*VsV;9&MxIEf >h׳k_ ~;,;_~~ά=EozG^i!]kͬywhv5ڿmLujzL-y%^SUΐz lS`m_~0ns/pD`'f i#yTC9|Nl7Ozn ޹':0@p.=TrtFcZF溢.ݽD%ISΝK|9v&O\"ۿAUz5l0 2ZX9z42OyЃ7=7L/Mο&xfʨZxXoճx Ej?Xcv>e\N0v@ _oN*MgaNVpAI2n:οy|Tdm2a "påuZjkZoZk-պ_*.VH"jQv}_2 }2! { ~gyΐɜ$nf^{5.233jul}zAtM74|Μ9>5յ.+Wdn{L@lI͍|cӅŔwq;ώ ^cs#+岡#!lZ={#?z01_w=vnj`^˴ ZN~vGҵz? ?U7lvkճC\Ǣw|~1[ ?ϺW`/;zz{ '$}T3b c\ksRNYhaw6W5Y@DBCC1>!QQQ|㏹{`]" aO埼\_:Z>:jE}tEDD(3KJkd'=ǹc,z縂 gq:+WE'_tpn6m;Gl䳹en_<M0'GSێk$59nTO:LV㤨CBr4sP0k!ɯO"g@SNMdjWPWǼ;m_r+rr.eQhnnۡ |kAU% Q+6p i?CN'%G=|]nCFq3A lw^>Ӯ5,M03gt0arOsKssaN̮=ndK+r-G 5LulnNG`a )H"""}oUT4o\4QSTDQA1DuX$BNٰ KŪJ.?A"]|8j{i K#Oyn/q$5|:*f%1e~ɱh HW%ZE)㴣SJ`z^3h2-~7gV8)?Di@WMp0ܦ FO>>Z۶qI ojbɟ Xrĸ)\Qzs^s691$b$&s;GSRŽ0.k Gä8~??_)^O'G=m@Gvu$֧|ܹޫСC8(].M6aoqM7yB^Z ۨi5ۮtW==vcuY>u+ƢXc ;Zò,&g䣁۱';M K8ggy,}sZScA 1FwwDDD[1-Z ,]Yڈu/?Sac0/YH+:O>U^xbL@qwÙy?~̦O?u.7GljmP4B2~]q"]ケ?t5xl8vѵ&3l z~xGݠ@.J}9OgOK n:bx&k8;>w `^V|A~k a^42헃hռjv;Q=7t1܏i3Xx'֖BL<}ƒg}˨*MĶ] w}ӻ3nSw=ɫW:GՍjyo/?{ۇZΞo7 9!^·r ;C'p\λ/0b8f6< Owx~@ Ġ>/_#; ۦf|s\8{O1vb~0o(fk:޹r#\@7MנqwtZZ^v>GX[nH֑ˍﯢ "[I vcyv r]OtQc Q^+#PGY~\"+׭:f. d&Ь p˗H]~~PDď LI6@rcqKOpۿoWPy7XZ{Nq%{pyaqɳ X7>zhOf𫄷71oEN=19Қܸ"|{$›KqGOuvT]a yWeQ#`V~cXY} epa,3of0;PqÙxc*}![c:¿ۙ.G0vɊyd2/O9LD= ߏ$mbi3WdP/1?l/{\E3xugZ18TԴ+H "!|~GskrN'FR`KM&DꕩϲqT0[GG0X'8NVg3q1dQa)?9Ɍ>1;;?Ec`^BhnI+mn&2 W^y?oCń!W,!%}Ve:j\­hn>0g}Q?Dzٳg3H.7wbsh<~Ŭj7H@ &c͠dU|wlLOհ1Ƽyǹ=þv [8&_2g_o|[bgn$w#_n[zٳ|nVs~Z xN؃Luvm@p.H87c aO I bo>g QĠawahlv[`H@=8}K(R;9cE?ճ'E:B} Bυ .*ԳI@RCr5d|ﻶ߃P!38oLia y=[V8"97.:<CX^u7崭^^zF\.3g$ _</",yncȅ &|wK <͗?uvֿz;6txP&>;c\ոϞy"ԺԟJPr_6_?\}Xaa̿'7;ٺUHo "s}uݔWS A[$-=O?԰ɿaw z[oVGY-Ȳu؜ "SF"]D֘sg8<3pHܐHƟ3~Ǹ9~M&̣b 1g@_`v1 sNOٙr\2Jcxҳ..o( f1 H;ASቦvs7fq1o<dmYwC8AfAy_;W{(Pn?ݕ}GUB&nVk~~ S]K<-̻t*!0~0-?3""""""""m UECq+P|uƑSZUq-{\[瞕53D DDDDDDD3#}y ^rNcδA 90M,6ձ7'ƼNEu;Hn]~;F7H+M?("""""""}ў̘qjZTH:">ZFTs'a$3}͔,Q|]:^{,G 1\r}ʜ;A f>##)**:n}+bC{"""7%"""""""}+ /fYnS?x+:(\zõ O|Ķ#/" \s]cF 4Ǿ}׹iJI%5s*W=[FHH-}_ _o{kY̤ADqdN6w2x0b4Nv?/1jpq`fr@;Sw_pd%Nr֧̬<Ȭ}&>;4 """kii8fyޘ>\ײ=ۗ$hSL\A\\KhSӭ_Drt ̛i%yQWƓaElޱU45fz)>\Fl6bO , 鞯ŝձEDDDDDDB-v; zEDDğjHW__^N痑aü־s˻WBB 9ҫȑ#>[γ= 'X,$&&)HEDDDoP%""""}JaaOxUZZj/'##Պb`0սKZZiiiL0Sr{nV\5ua=PKDDDDKfyjllԱX, 43fxF_@233*ohhjaKOOtfBCC{4DDDDDDS%""""RWXVN5}`@.[XXf^啕ҥKijjINN]Gwƞ> W3ZZZ:]S'-- .OxVKgbcceĈ^^l66mvu IIIiHR%""""RyyOxUPPjeҤIV+AAA~ld3fv{BСCYS'88g.٬$EDDDDD# DDDDįNOxe٨INNjrEaXZӇ Oh^SS'jp֭,[S'**k ö+<Ueeg{BBVQFy«4?XQQQdggU^VVVWAA;vpxꤥyB.PDDDDDDONG_9NL&V#Fx}K_@BBF*/((j ֮] ڏJHHSj)+(( =cccZ\uUb"'g61L0Sp8Fu{nVZ3azz:QQQ8 B- O# `jbX111~nH 33LzOX~=_|Nll'j?+$$OCDDDDD伢PKDDD+..}U\\jeƌjb4jgȐ! 2ī]_|'HII ڂ/ӧ!"""""rNR%"""G477wUcclfL6^"熸8>|WyQQZ]lڴ zl?}adHPKDDD>ȑ#=lcE\d61L0Stl6vʕ+=uýBGTT?NCDDDDDćB-.VWWUۿ[ZZ0LXVbXq,"%00L233|0\v-K,ԉYbӧ!"""""}B-PXX3u`iig{tt4\q^ 2!CxWVVzgɒ%vOdϨ_EDDDDDB-SW͞:Aq饗z«X?ZDˈ#ʋkdcED_JJ )))7SrlxV^b\mWLL?NCDDDDDQ DDDϲ흎jhh1dff2uTV+?ZDw1dddU3͛YlNTT'j f3=|"""""r.P%"""}ByyOxUPPFFF'O}VBBB8p *]mV\ա 11+j 'H_DDDt:;}U[[멓j墋.WZEDgDGGͰaüKJJ0ll۶ c6Fuӧ """""~PKDDDYЪWmZ?kUhh[-""IJJ"))QFyw Xvg{``W6KSŊA IDATj9WUUU퉉XVƌ RSSb fUf}0ܹs'+Vԉ ӧ """""]D*555j~* Fjb`ZsEDѿU^WWG~~'g͚5yzj5EDDDDz?Z"""7>=cccZ\uUWŏ-,""!C0d ) Xd vS'%%r > 9Z"""뽂Xm7 Vlr9Dž^U^XXx_޳h4z/l?+))OADDDDDP%"""]gUIIg{tt4V.+ROxi2n8O0''S'$$kTWۿcbbq"""""}B-9#MMM>U^^MMM:2c OxVd"## F+??͛7tROhӧ!""""r^R%"""'UZZ^zGDD`Z:u@?ZDD넆2p@U^UU3illIJJl6{^ӧ!""""rNS%"""---IMMj2ydOxVOLL 111\p^奥Q]m_mۆVWzz:=} """"" Z"""ҥK6mZ^{bZ4ink"11DFUq Ç_{y|): DDDz={`$r\INNj2aOx|omU{vFAA'ڱc+Vԉ -|7 Z"""Duu5 ,/d2tVGanjeرbէ#"""(((0`:) k,Yr:Ս,Y˗sM7qמDDDDDB-^O>avN'yyy'|Nǩ򨬬lOHHb0rHOe6>x.B#GxMahX~gd4JJJڏfs2j(nF}''""""rw|D"r\sGsKD|QXXȂ Xf FiHjkk=zWmttzM_6S'44+ڽ{77n>if˹>N)P%"]i,\>ә2e'Vg |:tv1 @:`fꫯ񶋈HߥEDDzի?>' F# \s5=<C4h *9mnweٲe̚5w{EDDDDjծj>j質r+)Xq#uxm3 ][.݇vv=H4&""Zhf Sa:27Tk1rH222zҞaNDDDD# DD*wEWRm @d#EMz?M`pBsu3|6J, "">n\M%o_Sf u}s6w3DDDDB-fvcv@u*"J .4Pbb@Ay DDDzX7ynv .3E"""""""""r{\'$""""""""rS%""""""""""""B-jHPKDDDDDDDDDDDDz=Z"""""""""""")^Oz DDDDDDDDDDDDS%""""""""""""B-jHPKDDDDDDDDDDDDz=Z"""""""""""")^O:Sw~8S م !awur-LkدH:qۊzjnSnhv99ovV:q,3%:>=A/K6vw]QC֫`Y[&""""J5ol;xb0!_x{y W1_3Va+3`puH=_^[G#x:jNU>qvc) )\;Mh 7Ac ˩S6! ~D.TOa0a9E7}۶ nz zG \pd_= b ϱOvm&;hfK1u-@mV |?d}63INoG2CPCDcvܑmᑋa?B%g=esfrrЕA4>F\ocg5T#&u~RF?{kp{^e\T|oB*YHyA !{0&^Kzl58nӣ o?TMꂃtFϝgHDDDDNB-9C崼&Jr#J qJ]`olf$9D-Oy$:{ C44Cn7abiC C# /@<5X,4$f$j$7Óa։o?6t`v1z^o00hykMVgc!&b-TX#4 >h&?" l01nIXXOmAc#uSJG ..uԗ8<7"r:qKX&EQ$5cc UoaM/8%[XBEڤ?LMih jhvR{\*|;~4XǞ ɯr׹D)9.8~ƴOiكǡ[N&e[0gd}|HÏ@:1),ELDžV.y ipKg٘rJ\Dh \ՆrjMskgf>s_tÓ;1EY66dgdʢ\~^AuXG.,#䌰lᢅ9 Qp]?F[Ǥu8+.9^yS1<530 lк6k93,IJKcsVgXCfxsm  [!mRrw:45п4& M'.phܟLe¨N(reS] OD(/OV2ggP?X4E;S@ [y`K04igTZ4~9&S8_0\=BӥZQx}8Lw<^""r^X[xw;?ah ܺV5{( lz%vp* yl. ne=u.{n=TقJpBL[92v(-6: @k li=6SAR7U_vIո2?ACkGNLCw玵+'ilނ3."ZgUbi u>mXHyP ZJ`hõ87Dqp:N:NzifA+j~x2~xE.HYsn^>{:HR%""^#r%`ō7g x=kJ)q z??M3g-|Z%G(puO{J^="H+ wO 6bOpҮ+Kt6RQ 麝zq^G9pwf+v.G7_DDg|a7Qν)sC#/^v2'+3w0&>m>A{$h#p֎%WNxJYIEBm,)$י=NN;G7KSdX|#)/e`/޵0aܟ~QZ>ַJq̂[_1Rq~\$a_κ5NxZK)L 9a#^ DDaxT2ƈ%ΣAȁQ<"vw8RoF,.:90u,jp4šR0 vaсUN(.ol #&y֨&C35; 5>kcp Fs"""dpGVu0aDRB#k-w$qpC##AH$=h,3cplmYH#'ЉÜ ߉=)± Hg' iSQw"yE %MF`8+Odg%LIMYNO-""""ҭ:өp2c66`nĔQ&LPTvf7J¢se ;/Noʝ%놻͞-5cg?[|{@g AF8h Εn%':8*5>` ;p)EDDPUd7 Lɋ Ph=5N⚝̭4ky.Q#.Om MG;Dp⽏'8a* u $u@i50À iHMe6`by-gq]\9M DDK]d˟Q?? F#.O} Ɠvq_€)H%3tI!""c);]|me&APl}uZ]A;Yy*`- 1Rww89NLGuґ6,`7p\ v F0?h>N?Ɉ.݁ePHi1l GI¸]^wa[\c_p4$ *Qo;4CkM1h G?}  `pz,S[r-S@8g +CDDD%0+x/|oINҵI<[c:)W1Tt˽IӉT@g/9Odl}9BLDDDD:ҝ)R'5>}G'gJu1_;*`6xKoS{I2|Tl>{pSh*bS9UأF" 6'p14zJ&"'ਂ|Ƹb^x9c +mu~0+k[P^= g]c&sܭt˞'MG;$w7](zԡYM N m*x< moKRaိ*m5PkR%""]*dB[Or>91+P32XTiM ]q3U<{ҏϰt.v4, ́p s8pY =p'>''5~l/pkZ=U~u+Z 1{Ƹ šRoMnxg /Wt鬿a #6kp*ƾzƙH/6'OP{-m4:ur S H߄@XYDˢ#;[UNA9~UJcbl}5䮬|N8άnr rC+˱}u9(Ca l{mzk돕ÑtZ""ҥ> OȲp_ hyҿAz_O)P|\80H],݋%^v)L]ƶr8oI$O~ ;SOV_x GȺp2XŦeO&-pR+CMp7|g-wrQIۓuM0an_Pp 6o g#3 ">,u 0fo`72[P,UDJ J%` {/1&uvzM&w:-ˆ׊pSpVǒi"[Uݳ}YQvh 8.w 1h \8c׊NIg׎xv`ˈlt w$-~ >.e}y+T u:H2n99s裏%"gUM=fVeqn8]M+KSj7NU. (o6@jgc%C3ap0?{wuuI&{B6  jWז^kmVֶZZ[ojֶzj`A–a L2L23LLBH{|s! 2# LhhtbS%EcyzF9ݴ:^9~I5v1DDd?ͽyxgd_̿Cs`}}򮡍bypK"kւ۔.V}#揧_' umnvswPb/zu~C3:7-;/FGuS +.ވm!^+w~t5Z꯽΋!et>,r:̀GüDJ9C*!w! 5_\_)'EVW|H-,9mz54|7xGGmpŤ:|šq1dϯqRVk3!տ ƛvpq;noS24D@A-9׍FP+5. j6ͩmyFA-8L5'٧9DDDDDDDDDDDDdSPKDDDDDDDDDDDD<DDDDDDDDDDDDdSPKDDDDDDDDDDDD<DDDDDDDDDDDDdSPKDDDDDDDDDDDD<DDDDDDDDDDDDdSPKDDDDDDDDDDDD<DDDDDDDDDDDDdSPKDDDDDDDDDDDD<DDDDDDDDDDDDdSPKDDDDDDDDDDDD p -qd {.tb*9K q7)OB~EKIDPWebᵥu.mX&GؔHL&EVZ =:Z;yz ,<ʶ5Sm!"":+D발"fŹ|"auLҚmi!ylq-vx:6oL?.-s9mVDDDDFA-Qx R\3zxܬX‰jτֽtm2"X#;cgooHK_5X2 {igq$n=JЉ7it,>T;s҈%,u+;> tS;^ {f²,˫ v5Z(OL䎃daM쫢 jw4a  a}69ܑU?LJ LSO΅qKj)"eAo<>J[ [4#g<29Ioo'TnR؊x#) &õS`]R'msRV耴IPy@DW}'taf$ZjQmU7fbO7P{3^lD~s- n^ПeRj?LIYUcp7#yVSFÀ}:#לc8JDDDD>^d*ijnÚV_Kq 6)yz/F$/TWNA ÜSj&9D`}E ,K1y5'kzewA^o4l?k{ﯵT–jxk&<: [l_<>z\7:0R e*q9p {u&jrʿ~U}Z:`SlUnocG^ST4|^+ 8GERDD%)D]Ꭴ ɛK"ys ^kvn $w1,Uý{̯aT6ں2`A|).c bjެbdqp5 CW2lWm=ew5ݦhܿX0JXOӱDxX@vwbZ[D; a8<ϗQaMaMHi)8W򭣁#fM\}y[~nN~ Z"ҋ }|vӇ;Gw#'.O"!HTwȇKaVn LcƟޝ {m8̦NF@f;'(\v;~0~Pgݽ`m IL[먯^x0DɃZp u2 NTQ+g&C \S-[s_Oesv Hr:0lx}G ^ VE5u_SXH9㘴";*=4W;D\ A<<'q fMZ /`߱D ךK NŃ}o3NR=LvyLh"GEwd[{prj)G\ƦN33?XKƅܳz?haK_[._Z\Hhq{rx|1\sh>6I_<8A-`?MEKju;{[/*xbX/_ɉ{&UpT@˕/b';$ ^)_?ܣμf]>l [~Tgu nEDDDDDĭ gǜ!߮>p"Êhdla1x;ZXg9 h?w2DV?¬?HWc*O=WEuwUw$R lL6WK`'oxoL_顠n[?yi?9}ۻxibࠋgV&·!d;ꐈ d\NGnM!-T DL0o7h4ρcH!2Mgs:kJx o h$Hqm8ީ'Xp~ظR7MKw=Ss~}КN&ن2j|DgͶXhu7E]@O07ZA+rO3csN pu\Ŵf_'|Xt1ǘ vlǃjDgW}u8 AZM+[iw?/@)d]ل)DBQtYz;~Y:<.Gs{-K]t~wx ?вdx ؿU#8AfJdwۤ7o z! +mHNm0L3od`j eBO pR"N*ᖄn/ns:~a s"" NdL Յf]]=!3RWKQ%d}~;6w6f6SJZqK%San:( |OM%kvb<ƅQ [ '˟tԧVyPyd@ nk9]UπgץGlp[";3\r;X~{RB1ȱZ1E.P.(]מ; ׏\ճ*c(Lr)7g Z"N;|e3A֞F̘^`EeBLLMtX!>#Y}nלpOl9SB6^B':y 'Z̓O#C|Frb7|ViQ*5 a'3{^n/O#FhxhTeu)=Sv7̆/tѴmtuyV :^dIdfH79 sݤ 0~x.c.$݃IkRRHϴ707&*`v(-hDs |'3d?E+#GX uLœCS7)crNfH{y ^8Vúк[j|Bg+~[hg|@kTDDDDDDK^huŲw.f1F*(Mɰ2 ̈́Fzvf@+-^)Gw˃es+A{y1aQ0;Y/#i6;@Ü* j wp @""r~`Z gQwN lnuXi @Ɉ$6udr߰É`ðWq0/J$p0:Iq|,\>&"sˏ4mOp N1pt:07B}/&F~:d(L%!W 1Ww_qI͇+xbq;3z 6; YkE}׉*&Ő';uwL>n""""" j 1aRPÊGCWÇl*Nd*[5P ?sɡR:g%k ̻iOhqJ3WCRw [):nˁeIngև0 atx9">?cYg?a^h螃芌3 }c@rYeK{']QpsWOp'aO qC 'qf ݽӂ gL:ck!ky| _[KFtv?~W9NF6 j5f}CwhwXob`5t 2k25`LG?̓.0=+uT-if^M \! he&O6bά/`w$oa2g)a=p0~wZM['@Hp#iTË+Eo!Pˁj 3mz^lzS]M8CuRc8x$r93hng Ll<?%e| 0$oWA֠% 0'4ؒmkjOP/0/xN유I=.eԿ 7!0kGXHƒ3o;p~zhTPsGd{/-H!6n4uTOz>#!t$2Ɛzy`~ oeWV ;-j{@f-L#T=u -{aև2\rv<.x U 4ooC".^R9wn߇<^kZ(x"DX30N$qub'#Xg3y {̶Y(yBbv>\p̌#eC#~}0ڎ503\X@9bRkiq ]oˢzv3q7P}xwz|bqNjoWCCY;XvuA+_6{v" ]:y6'KJ 47tX5I̘e1]Q6t&?<__Tƽ4i3Z +=N/e-35Ġ qmz=*W$р)*腚V .+{'%!8cHV"OKX~I=dl X$qX;Gq#=ocß%{yP Q03t(UMTW] 82;ȃB6,=L,%2kZ'!'ĄaLCp J2sÎ>T1o 'B42)j[il'vR֔d4Qvk~=q,3uPd^n̂q9]d7ѳm /NJbsiJ5,Vx`7<$Gaq҉gCIV:<2}̅0{[O%yIy#q?퀿V@L2LxNi\qm^ dV yps:㣼$_ΖA-;NOwvxmz>j%1dX`ǕL|Đ9 ;[;{3)$:i-1w:_9B wI0 WNςh)pHOo,ϢhzkYg2m&Clw \=c;nM7W1Ա b+;f=vvvf\>ͭ|;zm/π'ܐ0f񡣡Cv?SG%zM%o5 M/+خ)q$Сj0%0#v0HN!4fTveL]hsc="vz=ILzʻz> 3j9|P}MCDDDDdSPKDΨ㒹t*bW4DC(z?x_ޟ'{T!] `V:` <~E 9 u"./oX b 543{"V %0# IDAT3o\t2dr+YLZBf̄_ h'欈 0G0~ &=x~' "t#Uo*YHkz˸h59B9 mA3,~)))6KߗnQ1 [ ]q5I \:析nΜ x!K8\6 oH\T?MrWRL~_M;sZ5 64~9;og _c{kiC*VZ75Qӣ( JlJfW3(5zFv [tHrY2%'(:k`Ylk~g])g0e}C;}˅\6q2-Ao=s`]<]f.je׻7uD|>@Iм tl~hqco8ՁBMoctQo4´)mEg_B8LkEK0۟}dn^fqލ3&f 9ݴ:!:R깎㭴4Ę!+nTx3L'7D7GZjJju qc/[)y;M4k2v7ĘaJɹDDW}`"=3/hEDDFzjRc 1zh8'xB$ 4Փ#Yfd luθ!|ӔThpf€7wl ;b#2B r"GD"""""""rQPKeV#v7CnDȖfBs݇3&gjIܨ&sVq6uz݁7/LˆYf|cW PZǿSHa1v^ ) XMc-fGd=^X(%ņ<-:DQɂ-8 ItYlѐ߁sݩηHRPK3}JgSG7tN Lׄv7$vQ.H{0˖j$3)e{(<Z#[3t1F#ٛ̂& m:9|͐ S sZ"X+9{:.la\ԓ$a?{bdA_<.nvՂ#m rZ;NyEDDFZ"籘WʩrlŸ+ᦄ3jgw-`r8Huf,G˚if&Ds}i6yTۖCwǏq X7kV[O?K k;c>{IwsߔD X}ib`i<ŌfqeNuEDDDSi͔1>Nj5 w('{a`2x&,_K.9TKǃ8;w9cwK8LXtcPc{^jxeFqx&$cQG`yoOrr*:Jiԝ};/M0L$FXs| UupAfΕأĞO_У :I!I;tO xn wU78G| OZDB\^%@l|q*/{^:f_.<iלtKQxۈwzx9<^hNo|p3c8.P3;G| wӁ%cHMkqeH>LP"ba|w9;̶|<a#rϐs wd-6Tm-ڿC\ ^ ._._2,;>%Oar^54fBbޑ.+OeL/:@т}NT]Mغ+2`^2|&և;v3VY #DF\˺4犩zvNdx|2Ϲ=miDZ0eSjLէn:G[;hsF|#"#1e"";$Ksrj_>k}`~ \4cӐcgo5n64uwGF:t9Y})wPX|]D>ǔ@sa\k㣏8O&>SNc =0u4e^ǫRk7PU-DqU.ܖ<=uVX\F<\wOw{1.[L͛4i`$yA%OPZ-|N<KƗ g.hRE4qܻU۽e8. _K;NY'NP~Ɉksۓ rl6~~w ŏeCek1U5[Z;pzݏصrߧ/Lb_JitG}v x݊_?,Xəݿ7ZBի4ذSdl!t^ . ,L}Fx3?Ϧc\ aх ~~''z9d35$f2']{ơoW|]Zcf=6†HpZ8NW!:eeb&0H3_;eգ0w 9[THvX %-cj8Qv6/`c < |K꨿u7K'DGp6tyl_7w7қqI{<5s< O=>ZMr1E+m#`x{xưVyݫiQ`kwhiq _ˮ2y2b1dkmwq ZZ +BaD G^+D۹z=EKr77pPߍGOcb=҄󠇮+-8<> SG^;R|;&1G:p6uzr,ٲ'^h]s:a}?>h(r mvx^tQMG^]dca?҄Hddշvk?Xb]ӆ6x6]rցf>Sw+t_q9$Ύ#v S5*hL58kl0~,7~"ƌHbҜݰ +!wpA|-sS>2T4`{+8ik^`[`AJe7kCb=TIMov]{SNZ֦ZwZ\HʨHs _ MRqY#J `R<\ _1L?<'I>ٕ`7E~|s=a\o K-Jp,%,.kmkR鿆&9e3zG6{hr$obx810?}XUDՖ&ZHMfuu2AKgw%D` 埵y-@l ˆ.﭂i0whHWR߉ڻcʀco+gߝ4 0)0 ؏4&/.TiG'kH3cMg ^(7kwaqSڌ# T<;>6F{^q!68g{y9N}G [A4ṞAȱ3Ds#g͎~ |LmXWj8Cg04Vr*KL|F4Aa}Pނ#}6,uO%=cq?>Nr 9i$Kyvu!~Y#2ӔXb[iVwZe>?Dsw@DDdhN-Lo׉AF>I8R x퟇l82qm*u: yW.+a 6Uo xlap^ bX58 #G90foenv uZs3EpYcMbK:JL_φI`܀{❒' Yf!m6s!˯\ɠ=$1V͂N j0}U~smN/. #")~.Ӓܰ7_v1y.Ëw\_"( .oW >gܖҐT?:c\#pPw ILZJN3 a5Ӹ@^A+U-g֞FU6B'c?N8CK'0kß뇸r'=k/=?f|Z3'W}UNF6Ҩr\d{l,!%嗐B !d ɦR7 ۸]lh4h{GX3*$ x?`{ϝ={و 8cfi*!zʿV[(ڊh^YF=[3>UK9xqkYLtyW_s%Pnk߿_PH5wneѫK=SV5gCVM> ZJ'WZ*Ȣb;hrӛ JYs'g.^Y e}9$9][[QՀ©9s=a?x* '8__h|B30Y]wŪġĞF%um}zVT[Mq8IJE Um[|خ,~f8hO\;Ҕs"Bfqc3՟UZs&[zYGѵ|wgVv> = qV%p@VVP,vxe+TmOIP=PJBLX7x~ {c7ED^ Pb{ځP|-:v=GZ,}7i) u='{'h _nqIwk@!-}f+X>?WM oFi{ۡ6dFd J3V/'wlsM$7 F&eLXؿ)Dʃ\~$z /_^9Zg4WmжtAOkʾNȁ-y0@v5GnSF| mMT-( $耎V827Ѥ5G"v >8`b-SafO ofW-JfA6l|^_KB=C}cEE卯,JiŘ= Kg'Ʋ7LpБ9:~ә;|x@+N N_V:\3*W|(m{t6śiF{1>l=@Yr잿bML7uJwb/Sy)#옅fP}ܡZmU7بr.P>}0,^TͤMo엿#ω=l-4f!<8 aaʰ{LE4=?/HMwv&U,$]I=2,==fMr粕c\%T RZyOot)d͝xىLCFG,YX'.hu,PKMN(B!Ƃx?8iK.DD&}8CyIQ:*񆑽Ԙ `H` IDATg%wqZ}1eٻ-Ƀm>ptxrhƪu@1`#B_ F~FÙaZ#7H#Œ&}\4%6M=Ucp,Z(cWϽY:@@qخLC3qJm-|Ǹ,9&?+mS I>&kq| Ie\Hq'Evx+Y6{YмE:KaR-8Ǥ^1#][ m7ObhW#Z,cdă$\ ~otRڣlsnĹX,9P8.Ż!'c|h&Fkhid$#d)R6\5xt5 B!ؐ9xvCd7Exd&N+o&xoDߗT[q?WKӉUNV/>=23TGsz[ ޸Ufz8 2Fzأsm%@HJl WkR? [KǢT154&6բKQ]YF` F^\Y[Zh&=%b녿G <'@OGGDĆ4~p^eTҙ[yiC)csԿ EA6%lб^{Y1SgPd {?. =+(;Myt~bO(;X93tTc(J>&$Af $B}Vyt7GFGrЌM+u(Z%RV#-5!bHPK޿tZx $~x)hJ#sW0bp˽w}(ǣO\Nve!0xzzbKOI%!&C  :-61+ PF4pC/:- th݄hJHAzZ{ј.v}鮍ݞVp8GǞ^ͪU.;o >y^0:='ڧpc>3ZkP+]ZZ|Zn?-9xkI&3RJ՚q 'zXtIUW+#VeYA-Pco@U;;+!皔Fx/iyC96IY@-P%~/Rz0%) גfRn^P06?&2EFG=YjH9eB1V$%ČkxՆ2IWbw9l#lѦdGk(|.d-:Vw{~j|x;B1ʓ{ ]1CN`ZȍlR7)}yon'ja7>?(5$2WUu^9zZw>zhIZulɌ*E65.ͥnJ}5U8rgĞ\_zsٱPVe~ )^yOSokN>?W!$b 5C?LGX$2M=.hwB"A-!ڠ4x,wM}w˚eZ_r̮Yذnx^φ .SWB;PꅟMt]PkҳSV6w/]<1⅒ìlS0G P& $Ej(OfMG|(s,<&~G<; ^+f%Avf2gܐ91A='m#'#˨Ge P)3IE'71Eƚ.ٱ`@I;sn!}7f/NOk Q2]`*wjո0@j nTlHtڵXNzAoUqa2`K&F~vyKj;j2̐{g딞}ٙp.Ti̭m^ۓѿQmlpyzY-ttڿ5+uS#^^^:)-nFz>9O3z=]iqk@ZeUa?sxbϻ(d0>'1#أ'Ƥڼчryp66Lc,oۗH9eB1V$BmE z%wH 咥B 3CYdt4x,nrIځ$3|.3L;?#VЛ O3x@y3DjSc&.Dzb!z!^`-)Pc@Y}-mH WW7QuLmؐ5_K2M"hP%S̰jc&]c1/zU]X@Y:nNx%uX u6-,@:Vef#ё%E>fg2(ZCl'#Pd}]K~9<i3_f= yּ*,=p†{M>`aZXcۮ)dg笰M~sA6d|6x9ʳZK!Ce(ߥ&ܔ7~S*Ɍz pgMbzʲOxS:!cy']5ꅸ,=5駽mȽNH5eW4"Ο$^t0h*&

GןoXre>?U٢Ʃ-]5%sqicI8q :ũ  )zZ૥D겺FZF٥)=5bs"Y~=eԊnR(?mW0 T&O@UAD#lШ\ x5DYPiD{{~8js;7!TlCT&11O ڲ+)t/J#U[j6^}Yv+M?PS媝\Jݿ.~L{F/'X7D<swXr~0{F4d%cݠV`tsɪ~.3+' ENI.4ۈR)G̣P(B j 7@ C7UnVV A84`nh9lKT&~bغ-H a,]6Hi(HU^>xewgI87KIr?UVO/f kۗZ|M%38NGrÜgjcxBir5>\ ~ey)!6IbYƨlQ,HU]~|lD&qo7yfՍ}'yçɹ(`#uZ\6(.ԯV=pQc~$9K٦9ዓÖ͹M'V7|1o[(,7gAOH[N:Oy@ /}Jڔ"fAn뽦%gvU }j.l2FXxtPw\[yCW k㰶{>|LT>n17k~CR.T:ZL|ltH֠?[B}c':3lMX3eœ;T:Ќus&|,jq')yJw lJ`Y}h!M o,a '9VKZ OdYF`õNɞ`B OJmty߹r:H(ݎE@A'}3adft<^GG947CǾfR*WCCq7<7>:pI+]?5Oyj=[il6‡Iˎ6t ^HP#t 1rVeܟh =Mf\`aƤlQptVh?;Yx,k 5* ^H"Լֆ}w@ Yp ƚk/Uᨬtª`(RTw\x899ë́|#v'=T4"'MY3h*s,Vx\1j' lQ(=8C=c$g3u|"yp |FWy6*3)zm]5· Ier>?kpT6vXJ :uց,uquz^x?G' %2JG;@VvWy->D1F4\Z,͕ pM+,N!!YC|ޤ&RǦ̣yB!Ƃ& ʠ+B G"ۿ[nq1 bTL: Թ84'!7CP(BA#y0K)v!LptQBF\:tك40k(=IFlCUh颤=Ɇ=N1Ne^(pכ\堥ӫk}}L,l lR=`h1܁QΝ)IxO /h(e<?0s̹u^?wg&=b R}dܞ4âxP&ښ/N,Lpl7GNJbN 9@WkwXa鵻8R;&\k^ ԗ]{gA+=\BeLGUO0G1#jy=,7(=]r Hײj0 ͋:T<5}eτq?NjeNC Ƃt;C](K)t~n =Ϫ=}6ޓ) QMٜ·;Ҹo?}8<9o b+=- P`mƳ~-<#\D~M~Xs'˱ TUX2J6?{? ?pLx` [ =_;y[{ZܬJ;N6tcEۇ;gI {Z&&BA-:R?=ۘekMFgԀNI= eJC]z: z:gB9w9驟uil\u=D&#k&oK:6)7ayt;B!hAÅB!B!B!aB!B!B! ?(B\!I.bQ ƜUvZ[t)B1m)%k1˕.e;up:ctiFJ/e1p #B!$%} ~F}34F~%y%b]βKM\B!1 5$%{{I.B!]=4"*$'}΁z>cӍkQ<4U 4C6p,ؚu~j 9OV뼠”T4k]:ǏW$xtjĢہ v>Bo{#Vۿh>?n#> ~;m(zW+~SpWvІ~>N-gW'yN\9=[Zq}w!N|jf (,6VI/;pd> 8"~/[;V2`$w:dGmHX^id$c$_< |с[c=zq1*^ogGN&'LH,8 !>( K;]"C%_j[XF??B!C!A-!|~[pU a49p=e=Dx,^|G,p ~',piwZ0|l$nܸ*68\O qqC0C2n4OIMpod'\r)on o恱fQbXoN']>@g{&.~ʼnBL*RWIpo>-ۥ'XqrOq.a :KT]iJb؇>HPJ3a}.|i4J ]4+F 2>3kDWvv†@ &·%=ɺ'NQڻ㍰-.|hzʎŋ-xs)-eB!b$%N[Zyr &kV|q*< m^OCOU#3f `0IwNpށ˄./BU!e7f˅ B A'y'oa&LclE*82#zBy ?;axkzJ)'PN[sS':p|}%ܞG.r> \l{8Ҩ﯂~/kXBj565i~N{ ·ɄV3TV7½๹JN l-%o` tڞFs~0$|c S{;%~ՈB!_zKB,],Z4ĵ fkĠvw͡ -Z l7!&).~ R3Ί#Yb\ ,{҇O#p~y:AqUaY;` v|jBLlK7NxVIyvIسYPt:bu1~?]Gq`SJE]D!K'9ԏ21Өwg+¥'X,_ֆv;'k+L#qWwj6NoNJ8wp‹ePWG RS}^՞հ~6%\T8/b 6%fD(nev\e5|z;#`OSdwn7pw=Dt;ka\T%B!F !m4 +wp~.s3SWvD;^?]u|֜2ofn@()ۉݖ ? Sz2R|RϦ48cж6y+npGFOc@Er"'MSQzUp?nNmHŰ, 5;7#qibMgR)`<y f @ʻ?tV S&@U"m utFRb/G.@B1Met\:Y,n` KS=p%mXl!V)7-4yhx?&~i!tIL1Ç"%Hq :~6NLҦ>(Wdš@YIj:-<_iZk5?:y"S|p7(B ۝L63<"ǧD}j7,Zp2^EHtB!ɜZBE!!l|-L'-~:B >lŋŌ]4HO%8XrSl6P`#3P9D]ɩ4d\܎em-U8y: *TX+3lD!V@i;iqĊ-,3.cDh2zotJ`ΒvwLj2L^{JcBO>J}rF$$K<P^`" $U@)Ę\ъL.Q ?ޮ(DWE>?*M?TO$ȉj\6V!sV "UWm-|JȽB!IB(.wfCfAۻQv9w鄁֠C ju+ u]}鞙jk Osf39]?Ql^(mS;oO/N0(B\.͆ ~ߎhx)Ctm}*,cWv AH^:[񡅵jKW `PVQK\YF` G6ru2%b4,,Q@3J#j!4YF0D,K\CWVӠk3D]|o/(Z FH\n=6eޮrae:L! B!bI-S12|уU:~h#9%@Mh?rLqZRvn >yؤ0:ڸ7<8,̃{1Jp [ۆ ? ;O$%b^ɤw( ͫ5OrKA3At.ytvx Xק  5+,{%$ '!FQ58 tSuhih'd&)+FëYڿVPԅb|RG2 yh!B'A-!Ȱz6 QktzH^0O8kn-@{98?JCCsl 9ipk!wl:o|31nS8]L{R_Bq &,t:ּ&nXdגȌ:|l.U'Z dP} jх9&@;@O g<% 3!FAcũyyTA1^xq|1-5~}-X->xß l !B!o#B]Q?w*Z#L j4t{iIpѕJ pD qG{c(mS3ha:.=%p≍x`>oB!xё6=e^-8Cz*lHӠ4k p [XJO\&6Iz 3!(ߥxw5Cz|$* _ w28uS5/ú+k~0d ODU!Bᒠbdm{4P/=mJ`*7&n;Z N_6n_mo[3Cc1 PUy(+V8y &@L, M\#uKR Eh/pi=41kB+BJd8h=>G5=lZ؊=1Yct%z[b @2GA&<8"w ,vGe]`ew@mb8鸠.7Q푥%ФTj; ӳ٬#CB!%A-!Ĉt57!,gQ| hდ:)U9'.J[64_EM9h Pp=t} &F{lhNV)46Jj8fr`jA9p4fO@glSze/M6x}֦ՔΙe} l0%XpTGB OG֮L4wŽfj&Si%>;orXe [oCiZgmQZK÷s 1!1p!ҏ|C}iLMNx%X u6ؐj}6xu7X _: ;;UVs>NG>zLSkqB!z/Zwb,k`U d [سKD3N [0( `GL# c&|' 'JpixcĈTj%vz, xrs\nJiv*3dɴtqFV#P%xL-X/Ή]mػT3|yZooGD1uՍ}'JG;@VvW¶ͻ \Ј2X6%D 6>ޅ,ɃF'B~עK>(:OV֦Xv$-KC:|WE XC6?[ : ~/weAeX'rex4F'os!7ض'Ĩnm"C5>'<]{;Ob (xt`za +Pէ{>˷w`1tq Ѡ {²siFce0i7kV.[-:!1lK[};ħ׹ΙbXz |~8ߵFZ^nKS?{^eyq}F^$! !CT֭?ֺjb-ںjUQ"F#'g8$'u]^ssޣ^Ƕ+?g`5 `^KH("oO|_U*%@ 8׊ ?vshMMr[` `=PA ǽM;/.`YXQtC{1?6.,tY&v ȹԵIB֎RlߗbJGt F{jADD}`[ lJs6P9[aB-%17RةlD!trQƇ68(1){nY u^WT&i 8.cv@ձ\tW0)F Ӗ<gɍlhAyj <FWAG4H`ՑjTY=ޫ7, R rRk;<︺2m;~!+eK34*y~0. +/xdG{OșˆW6??7!wŬscUNiz5fধY;笼 J-X 0V1Mܗ'y)gw= ڽU=xj S ;w4^o&IqO+'9ς/$5}]ymזFae۷̺>%U8I9 YO߭%QVj%͹^CX]u,,XN#!،,}kJ+/""rbMi}Tb! Fg )F7|?asG e8{HCѲLXah86V IDAT.l=yT}vg?:ĸqќ㹍RZ vGj`cєNIẹa;Vǎr}dR r k~ }{I<ؒGdp}<ӦNDyD~)ryUpx8 n'""""r  Mx!5͘1ӧo54ѥT70:= 7qjY-$"""m6<@w>}:tu8@vz'Tc0;h7 ,gX?b *8璥lLp/NC @dSby5jBl%}J(I==N`ՕX K,AnkH?;iY`ly"qVWV~K>C9MYj&g>sW""".Z""""""HRoko&3N!`MCۖ,I rq0qYQalPP6`$=b=$p\\DDDDN=@DDDDDDDDDDDD<%DDDDDDDDDDDD"fUnx n`pu8""""""""""rSRKDlGX̽aH[ ۆ%mat%DN?{pCDDDMY-s+ci~~a:0f39hwH\1h̘\H8pC4#'jX-)㬮C% D{: )`ь=a)^B]FO)a ]bT\Kӯ?^PFW """"""""""""%DDDDDDDDDDDDSRKDDDDDDDDDDDD<%DDDDDDDDDDDDSRKDDDDDDDDDDDD<%DDDDDDDDDDDDSRKDDDDDDDDDDDD<%DDDDDDDDDDDDSRKDDDDDDDDDDDD<%DDDDDDDDDDDDSRKDDDDDDDDDDDD<%DDDDDDDDDDDDSRKDDDDDDDDDDDD<%DDDDDDDDDDDDSRKDDDDDDDDDDDD<%DDDDDDDDDDDDSRKDDDDDDDDDDDD<%DD˹`w6o%BաHj_XEȉe e``V g}A͑+ǥq̮@DDx;)~9b!pxJ´`p39ap}2ӳɽz)@;\z\ %""""%"".5uf+޶Sxᢨ:FcqlLo)gֹ""" $3%*CFx<~ \yڶGEDDDNu%"".djCTp8 `/F׎a)#g}kgֹ""" ?ಘ:JgA.WZ85;H98TAw<:ydVn3S(>{NZJ9XYQ^~+-JV2/ұl+n~b eֻW'z82iA*I`s9V }# g-UD~Fßw_9H~D\;ۘ|'rc 7~˚bzGTVdl`!jA֍粝s8@ƶZE (2`:~Rc?O%Ԏ 7n? 8>;HHBO?1m3/&c3\;ǑkعʨW0+ױ'=MA5&ül^kbk dzK<3b/0qH,jX^5. O޼7~U6Sn-Ԅ3PVBSr [ÏƘiKr~WVB׷Gt ɝ[H'"""jIRȻy+3X ە ;)7ƍ,( `# ꕰOK- *ˁH-L 󨘝)V2 fr97Re ts>&.O _eA>F\SW%[zqnL} wd;2,`CUl΢꽃eo^L x}3fd wZ:&aW^Ǖfc rn%&VPnJV@tHa)&-l^Ůr)",@ԇ[ ,ǵ ̎d\(1j[`fk^DDD\ü>;N614 )nY ;4˗r-& >t/, p8Hklu"+-X?H5oh^>9/&P ų6㺍un6goΖsr6>rl(G7!H{Á*fgAukjh{$*A_ƹWS06ZCg}a_";*(8,ǐXԺX˩ȵ]ÒZmmV݇Ї|n."""4 `I0ĝR`h^ג!C(!Tg\`dv'pVF N=#,H9RBlʄ~3,K=+۷iG/_LɄPss0 TLvb6kl$SkjNScj!32;͵HgPMJ0@i*7,0C +(>䀱:!9 쩀4 iLS?|l7#" pۈ4d 4| 71 O,"""^=g{%yoHZƤ`Ȳ1Ez 7;1nR*&~:p+) ;srȌ5fLqxmXU>̈́3u $PӹnVn1/`w f{փulsΰelZWKDDNM#8 פr?XAIzFXȰZR[GGmX֦5u7p9:^bFez5 ymU`!D5jMyXKY=o(fTR'sk!أ䢈7v}2{~9qKL>DM ] 4\U/R6>jp{sm Bfk1XZX=\g2,9h+~+Uؽګ`qaM*Kd@q>˛#Mtmnc\o _{a-ߍ~Y (*hrٌڞt#Le){ 9Z\^Nn*L9+6J~x+*~X׸v$ÞyR  1^\?fԬ̍ku!0dV7⻖4Vnݏ 1Z[˸7vm'""""PRKDDڕ0 dOc#VA 0l ڒf]85(|-Py?fد`~x:vAZ)CHUitC2[HמM]>U#s_¡Sh>ESZ""Ү/n sy"LYBekٴLD_, x9LD_4>p:lϤB3WFFH:eE/fX,g ChaX#ϭrZ6.(x1zDVs`2>]⩁Aݫы0`.8۴oVv= *ˇVly- cPsZ 둺+19ONܛ1IV*|kA70ʍկ˸[C׬$31m^DDD*//GJ`)Od弄gqՁ7%!~_E͡#rW jq:c0|1߱ _Λ,ꎍ/dn95S~arWS;=^ZwF¨#+~lCz`t\k6ܝ19v@GMu%blgETazd \+'F#șp8ڵk(ش\eۗ~f̘OZ"Pb/!Zf 5 ǼUٝk)xVb!b0 f'$Dzw#Yi2voH;q$ ڻٞ\ް<2rV^% cYZERqA`^%jӫ(kjMM!߁cFOI/DDNwsq]XJ1;_j -j\*J{AXKo)_qpܽyR6{pH] ݗt amVbߚvcǹǻ?䒲EDDD:KP6J pF~Л]S^cǝ,hߖkF;nMlF[YVK a-\d&_+ny5u~Yl!g$gwß\>^Zxζ`004ӾkDںSE٪:J ԍ.OI-4 xv<;2WqƩ@B"C rq4ghSCp]2 yvɠ{6z:$ jʋk%kUv8pաcs]3fU-&&8zE\\{VEDD$ 1f:usf㪫bڹ0Џ:9y)Oc_^Uv儆ry:9kQ^^NRRɤcVXdKpIrEEE8rƭZs璝͸qktuX""""rSRjmJ9 uպ/Yz5wqޮGDN 6aÆm˫Kr%%%j*-Zw]ȨPW/"""¾}g׮]ۗ;3!C:,9CVI-'nm֕'XƚjwmC IDATawOmųٲZ)%%1c0eW#"0=ztݶC%oKrEDD RXXȜ9sXlAAA7aҤIKDDDD0^R|>_d x˅M)p ƨoeT#46~.?`Uc1ޓ~#Ņ7icydCYzYࠚ5OLE w%>Ϋg?Xxv/*=m^2+}I8oǯrd:{ {lˋemZ = ;OSu/bz\Գu[46~siVfOcF3۸7049f?6lO,Wrr`V\\̟'BBB/&&&LfJNNfƍuGEE5Xd2*|9̛7xV+W^y%ӦMͭ{o4Kj9L%dm.rwLe9BcHyvY/cUxʛ7jyY7c~~{Pj ew%6+  \s[ڐCDfiœQ zTK~o܀ښR7n`Mf egy}ҽ,!"=\/>S~w_R@XB9䗦cגb{^dq3 mwRL20k~Wd ;|Orp߽p"cWFzaV++{oʺL[nW^!<<_idO>ӧn[EEE]+99={j* C$WLLSի'++c2m4wDDDD v &)&ysǹ?ybHF6էsޝu' j`׾3.鉧ùϚc𢡊 ]s7d5LA^0+9~5y?>p!=|7}s;778. p??>gWoO`/![ݫc &>x^lLhy1_=Z7%;C_Oxn cK }T͘[&\3+s<`[ɂ+ j]v46mĜ9s2e wqG "r"|||2dH5, ϵvZ/^ guEDD ۿ?seǎӇ'|ޚ"""""r%pPBJ7w 1˰Or{71-w^8۰*G;z.Sx?!þ*Ѻy!G|˟K/c{q6shR6]}5b77H zqr>-!i6xº _-e˟5&`C\Dfo_g~Y`$sy _V~!#Sl6rssIIIaٲe޽wwwz&"rBBB sΩۖQo ` ݻ7~~~ _DDD\9stR뮻]HS6Э5vew`MIAv0X9՝5?nLCӥƘ ~Kha\՗7L(E ۰IC3~čtuli9Tpe.aɳA8s${xag DnhP_so{68XN}Fڨ^{\r 70n8BCYMDETT>޴IIIl޼zI8!""r?>W\qE|̙3nݬkNPDDDDuN٤OkT0 qrˀۍ  in{',co;sbNg@ yN':rt#L5=vn$Lvn`qǚ1ʽal~xv0Po$XgϏJF#uwsshLHa0ݻ7{VUUU/uV^]W^\]DDD`ڵ|F.׬YÜ9sb̘1L6(D*""""ҲS6cY}n{2p?{˝ޛ//#hQc'0m{L$o 7FyG+.hM#̌&dPC*50lax5r7@Lﴴ4V\I||<_5'O:NODŠA4hPݶz7tREDD `Lﻯ@BBl߾޽{Opg2\I6[O^/emR )+X?by hK:\/~#O5VW׍feogCvةRx6#NFll,v\r +V >>SO:4 bȑytجz~  mVmmCi"|M B||<\s sasw2ydW+""""*g^R pxrݳ8YT`ӊZM7p2M<ȓv<'GfJ)3r<Sv0:|||Ox]HIdd$ƍۖRo}[ o>ɮ͑ŋH3fΜIaa!m-bٲe2uTMuDDDDrF&{n 3f-'=>\מRnEMwHMsDǶni:֥^ʰax믿DD#mM4 zYvm񱱱\z:򒒒Xz5ׯo.k9ݻvшO?4111.LDDDDIz1Ye:#=`y m8}bW׽{wz-~>#W$"r` 0n[II IIIunpss䊋#22매`4Z|lܸo~u+VZU7v;ddd(%""""3+ޜ-5F7[׬{5~mFt-UHCjh `e|!|%O07o7f4_z;SG{,m?+7_C9e0bFQ-;;ԅK,׷^+.. nNbb"_˸ޖі.]ʼy$0 |733CigTRZBҜX:?q nxOX0IG׍Ҳ8םrPMTF.ǐ4cR~uwKHu7E7Wݞ/l~7>زKHݼB9cڧێ0|pZ,X^HXXC9mDDDرc붥[kܹuѣ8 g9~`͚5r-9Sl߾ٳg7g 33sWtf"'W_e˖-CDD0ydNWΨ-nyz;_Yί<1Aj)HMX!8gݹ K iܘ~6ݩytZUY_ϵW_琔 wQ'XLږJ`'/[QKF9~V}ͫWd޸ #lm] xwL--_5ڣn;9C||<ׯgԩ.EDtKll,]tu A;^II fͪ0""bq |MF#v^d2uT=z ''\ f_ |]4`蟍mk˟>8r5q콝5::q[{/-DT\qf[>vkuΨI}qWǬؾ cCW>r=T;^yyE%o\A1Z_6 g626|BɽP5r߭xmPKĕ_xxOB{^>=MP;%}궣2h ֭[H'sss_~ue͝;LqƺĖz鮦7|)zFhbcc&&&(uENՑ\HR.Wa &nիPn7=/!Le9$Q^V#E5K`)!35R[ !ѱlcU)iU9/=v)0XJJOAneGc}{Vk֬a̙<3 <9C}7ANk׮%;;5Be0p81h .[G:qjk׮@?A% #GWK0͘1ǡL#KgN^uގ4\ȩk߾}ٳa!EEE/GDIX||C!3Z""""gtRK5wq$"Cs?pNKNdd$DDDNDD.ToFI3^ĥqnfW """g %DZ`2 URKDąRRR>}Wr1x9t%DZ[n: 3ѣ]DI-p8\t}""""""CI-.OI-̮@DDDKٔ ^9Gvneٶ" LN lj2V!ܻ`@W$""ff=wDΉrn>Q5_ όcqScru@"""""GI-95T}s 7;ܼqG@Dt™T5;3b " y"&ޗof9D7>wLgK^`s0s/Q@;O3ub?DDT\۽^ؽ< WL^7K6;VaӁdTQ30^2zXwPEsyY'W[{On3Yvbrόڟ^`sR:"s,M>oh0ጝ8 wѡ!Yrxq4$yg17eSqAQ1eŔT[X`%DDړb<>~69c&r7s.gOekp !L)X{取ԌǸyq%)ͫHq%""srz;V~h:wŀg cbA yUS>eCW2pលe`8Ɵ*z1xsP͢;zУ۵#>y4"v۵{܋#q{FX9i*w*Q" GUrYۇZ Zn7<ۛvXzlv{;O;,ΰut KsS2sH)(-9iR xrKXDd}5E{YC<_{&oofߒgmsk8)[Pqme#ﯿ[Sw?Ͽ,k8|Gxqe6i[с~7ϰ+v²&މ񈈜Țxwk\rk5w,eo1svVƱ'e~^LuP͖:,a`g.0^\ԛmz#Ϭ55ۘ4 ]{zu`t]ujv3{vb3KDDNgAp˟a/[_Dq3Z=Uծ_ f:"m*ɢMh+w\ϽpPp$n!'i9*gC͆~9Jfʑ@sXb(*R/.ǽ2/}^^rq3s|=+{^>61DS&JS^1?e5 S_{}$JUU_gk-z2Ms:lLgC!B!rB!H*4SӃ9J2g&/hk] Bn֯T!U!%.SA:3]_E"4oEű=QԬm+0g3иy (B+0+ײsKpSw$^<@ ǁ,8򲌣|߳;4u:{Ӽ;g׺pv[̊1ͯԩ0WEUUSyʟKC Zm+`I[9;U 1l]2㍠|>ȁ[t8K@-zU wq05u2KY2.$$TK_ZTԋ<U 竣dƅ v7:P-,>nR\q_Q A_gtB~{Wit{!'%~};#7=ݩdDqϹ۾s_:ѝ pƉ+F8[?7:2կXBKayy%w2LckRGsb_ OF)QtѦ*;QXӜie>jHި3/s% WLN@0fL3YX9Qͳ!$s!$)v|B!%#B<ێ  6\ʵ,k7=aꐫZu8 N$q=:#G8{9+ksޚ¡n0e-y v_S R' #Z5\^gґ4xi<ǯ= -4g?&q\,ɱ5zNl&L%xQgv( F%߆KV2/$ #BG2t5ةB!( j !x`{:DIH`qlͳ]5ز_J#a+6#4W'ϛϕgqXϯsꥐgpߦwz~å4|n9k&ikjmylJeE\(My?񿙡CiZ<ٸu?0 dKߜ3 =!d9';ӷAεdsW p@Y33(Pw6 5Ūo:cĜ[ $͆[1GW2ȼN˩NeUe53g?f]_wF3ojߧQY4*,yIy!~@`s)WyN4zG6Ma:lGo p?5oUƝv[μ^UP\֖rޫ2kd[Y啛M B!eKZB!h^85S\LO#|8?r. --2#2e4Y(_-vi>B]H,s&1LU\w,l|Zatb}TVrCnFϥPteZ4bjǏmwF4y[w Y(jOVL9YvWFv& `Cs{~$}1u, K1LlQsEՂM23**[BQrnԭm~DrkA+x}r,Gc޳8Z5?-LA3w(MĭAOUWP8Ț8>.{[^sHc Yŭ n*x򓯙6 >ӓz%*Pvt?lZ+H# nU!B}2BƶuFeZ*|ª*MfM &N{wb>O3o^.T$=!h ÞpA~=`* _1?\ &au7ut0!߮hd /:H;}frѾ-q8 S+rZ\;ZSS5ӹD4!( v(?mݩXR윀40 [%n?sFIimJPn>e /mW4ڵzV;'Pt.W/5 )w8}KP|ʲLC6.YB:K_o!ݓr%#9x=CB!*B<H2:j,RГ_vp(QQ\G'-WʟD@6:BةXb *vhՀSJQb1QQ輾i\I2]=K4C /^kKe,[<+lFUګ(DK$F:s"{#xa,|ˑBV5h>xf"]@=C†\B(*bV"(V6ث@k뀻+x`NQ99usQ,F\<$p0!>Ǖ\S.Ff[9/&X_ݣl ~{M#{zۢŧ]SH07ҼEkmCA/o]Zvb(U,a;gG6+v8;AQ<"G<=+O-oBofyR1=&U}r6o(3k(U3˗f__Vp;-Z5Nˣ'!W :խ_lk~ lSп#~e6?Ee`ӢYk|RvB.5h\*e߮hNx{} Q erssͭw+B1u<ԋT?z[,耚!XjFROg90mNVLM&}Tݛ&B{Y#&)~Hƞ4S w X.c5mk.K֩O{:t=y(?2e%ǝWHD՘dr A#qŚݭBH-jqH6q(_][{zsߥoPkv]&I$Iؑ_qRց-a RP)68YWsȓ6@\*9:zPqWsP|S$]ΚPfdp(}w9C{͓h=4bbuTe3᮱|6U\-da.nva*^<ɜZB!Lipo QD^5j-v($34$0GI Z/lQ8t=72w{zp1;xElM[ā9zp8Ӯ)MB9 2 /[l2/Z|6~%^}hBa(p9: HqJߨiMhCOġ#lq_jQN4]ӪabI*U)'W SW2:s=*::my們ux{J0ViONt= lv6/L"6W ~u>[qCBYЪY}ۨ{{ry6ܦ=>+ðo`Qʹg |-wM3՘%,Dހm12Stn@)ᶷvQDո3rD> '_LQ<>>7H`[؍bI2J|^Uf&:>W:OzÞ}?bE"e1zҭ4N\'S7s>]x2ldO1Uܺ~Risoagf7xUU(:gSC1׿ Jb#pq qB-L0g ͹L˽bf!)yBf0c)/Sz 3d'1QaChsF׷0uNt jZ?2(6u,Yx`pa%tyiPN4H$}fr`/O vCfq5?Pũ'}Zm$M?aY=S^1R:T*,xzg^7J/L'h|;_pV|1-wy-3-u5,or!(!d"kdG1vI/1Г䛗1hV?,-{꿌7eH| QHؔ4W^ӘʴjYmXΌ򊑃|[NvfN ,{>5^:޾egj5xxaJ8K;ZW:6G;_?C,7oFZKXq1Q.ǙVkptOou ?_âcYJY^2БE/&so o9@.:91u!nQk՚q#}y:GqH˗~g_lݮ%:9|j ͤYd\δi9sURq.?ʮKDL7+-j5O.qaOgطpEv` ~̨`k͹tv(L̟eT<)'o;OzU!gxB!I>C['iV'BKY_:ECz W,bյ\sqdzF>A ^ٙ: Jjx!/`޼e]2`zLŞ*pV>`TC:š˵^FA6}>)*kآLmh>适ӟ޷3-֔|養KSsߌey {2ύY*6sN*̀$V2ۮ`4\ZFO~*MՐ@ =S: N1kÎyq&3[( &Rۆ|mǍ`+-ⵇ~FI"𛰼ߛGYbO8ڙښeUDk,Us<ۗEƍ>V2/ Dc2gv.bR?? 2=ʼ_sx WDޢ5\c%S|VGː|n%&%(Wgh>R='bl{S-oǨ ,=ElG=535|yEA^4𣶒Fn`O:1u_x ̸Q܈]ϬQ5 49+Tàw2}L6]r*]Zl+9Qn#~ m3I;R5 Vf\A-Ux( ,Y3O WyyuMuz% ?ܼ}9.} !;myis擛ٜ!Z063ݎ6?$du-Z]S}G^S +3b. IDAT9˽ {W+e3tm3A?G!M?oft3_ɽ!Z3\4laqݲ˽*@ZB!ͦrYw@>cW2z.s:hxѴ~c-g^IJT鄝:g.s*:xU)CȥϾÏg9GuvÆ5?} l/'Sa7;I yfB2EGJ2d98Qۛ _~~_'s>,+I휩^ah0epsPi}5e-=z.sqI;<<9WRT~?̋ᜌ iTr]UhykgGq\2ƃj o]6|A5%&c\'GtgD^bRGbn70ȵU*xjlٗ;ѕp""|W#)|t9vNx8,{t*[b(Ny{g_m?Gi|p"/빩ggo6pkVc+Z ^g~jNq9l:U/o[oc[FsN\dVuAsZ/\LXݛ:5ML3l 7Q^ E|wcyd&*3o6 ֟wNg"N~^b^Nd4fKi͈?1 Fܶh +?ӑyF*<YkBG9Vʷ=К;UF{C\'؜؋&UTo:=WuDCf#c]c-/gN=ľgj>y4q:R '*\?I!5(6yv8:RR_F6,7GF2#B' 5@A^CojѐvZ/f݆Yug^]rlhń!~ymyHJ)EXY쁻)ۖsǟsz.CxƘ?0?3nkBj#/,/_r)jyܫB<$%MDU:NTmyC }2Z*m"mJ-bVl\i}vv_ 92BKT[jHo,7-Ky`S!ͪu w<,-M[Q`EJVRpoAI-9Ej>Ts/DɨX|GmlOקIW^5ImN@;*d۲)5rreHVknQ:̍q,*i@9\weiy^ gIa_20 {P&[fؠruִν0W6gg5w~Γ'x,ױ;0p7J!lRFtl;k FXG;.BjCזUYhݾSBt<>pjh -.Σr B!B!O$gdB` ,cyʌC.yҩ +M?cGy<(w < ~nM) \O4S˵n6@dKE\Ɲlt+%-;e 'Fp([cNI%)#}f&zŀr.'86i$gW2!@7/W\8%"olĮ:ܝR_K :q"W'{j(96D+,T?\&żwC!7C١9'hgN{R1=IQ:kyڻXL=X*ăBZB!DnըQ"11ۯ(/IV Z_>TΒ^BUq[9Ќq<j[(@rd 9;W.+wmƝr b8zQO|fR,̚&k޹C!@):nLirKSy/p:ܛߡ>+*>ʭRBxcӱ >@ 裳\5wćAUnTuH=I {N&yZ)VjeZX j !E6mWBܗ&MTUnݺ. *ُww4JtD2Feǻfe8s0lۺu;<ғG̩erlTsۙu)t ^ܺRҀ^) to}1Y$;0X|̲ŭ#5b[=i_1* z*H Is'XnHɝў}Igkn 52s)ZxdykYk RM f+UkWV@O#)kDf9(EA̵ɹK_hHvŭ[\SCU*k.u|?U*:ңli'T6?}X/*ˁ4e܆$)n_MkG6>dkh-+Aֽ{q$B{|6 | BKRY"8u~{'Nk=8>ojKʃDff|sfoz7tu9C/f ا2QHCsb*EáS9sB)Ð}s煊XcK:ZsOj(){h9x#>ɀg˦h)6{xfi~#ߺu߳*{ ]:x$PH`ikW%{sͫf$K}]l bM'O!s؄:WYvȱzNp|jYו&]Ÿ dՃoz@4yn7Қ|};ca1_TΎ㕀4P~ 9Ey}t{iZPj/_H׆xHPK! EQh]QbNcN!qҾ `nRLX=X>U!L $D*?6\A-#߫|6  ɕzJV[XF|ٕ/q_ ǖ+_Ƽ:tꎈ4)!|6-pGOVoD`S' ^ggmQE;6}~Z1Hܵc dT7f(Mek0*EOKϻTF^d,cz)`̵ŁDzC̐Wу+B!B<Eh/,U+/_1{ YD/3{4>^X}a>L2m];xU˭h֤+s~΅#rWx Lۺ5̽!DߡMB©SVąI&ѨQZ֚5kXnm+B!B!BQsSRWtĝHPdi*G5jVEW"q?B!B!OK$F:lp鉟TS:bNqNqGUA[wR-JA҅_3 [\S5w(:p*OkFc8z&:lstmb{ sDU%j{zR~艿>_6[ý#^{P_%!B!B! :_ .usbLRu+֑|` p;GaD Kn1Hi@bsE 'ۊMPT#iC}ޟyb_ i̞hNAJ˫o_Vv/+ޕuзy/5ϐ4əu6(Bj͚5]͛S~B!Bd  vR"z޴/Im֭t#4 [ <9Kʹ)KV㻍ܣqJ*љp ]+Q>]vޯ#C늏Sg^B\. 9Ϸf򰁌q/[20aO{7m%uIPKaњ5kXn]EW?EZB!BQׇ C3s,s7{Iں85ܷ(-Q|9Ґ`ݐz4@劮HRDŽЀ l .ltĝGHZkOTq~ٿO]$J͎%շ]U )*OBӎ#* v/*PT]!B!nK~os~J3cͽ=șڧ߃ Zu6)ֶzр#@J}Ҝ3V{BO#; j (6exՐ0U q`&Kן魾-{Y_Yo[NϭlZO_ͳwl1TvuV%\T־|@݈4UѮo/A-!B!Btb.v2a%VFB aIh7m|zqe><ӱ#< lt{[Z] ',Þgr_ c{iYM6.uXq+m~ ٺ@jihpր>St WN|1tS6[[W|4Ǻ0k<4uB:0环piu3$[0OSyߺqKvO-G u\ф9tHD=?BBBBBBBBB{NCRտM@."0T=ii4{Ei_,:,dlFep)#R9p.QVQݵXN\)ű$ {bFX,9Е.BQǯ>'K7]iOGg7xT==qVJ3D_9gϹTʔ89{4Wť,.Şa|[j#73r"@ICy싍l-1=]T,.e{f<ێsMlȉ+&.|8ȭڳud(;ͽ4)#=gOkbЁ7<Еu%vjAC{XC >ߗ?'x uthQL-{۫ĖXN䀳cot8 O 0TUr%|N o͉|hec|k`(rTj /\M^CXQT3jXwiE8>cVX(dy*1;ѳVå$:yr\Y CƯ'yRo$+?`w>XjVnzā$DY/IDzns9ђO",BWըqH}M֞k$QKBBBBBBBBBBBBBBBB⦐W_}t___nz[ f Ǐ)#;"B?{M<ʺ̳5FQe_0?[w5e#*M<߬9y5/,`EJú dO<xx ~ pr5ogW * k^w ]BwCL< K 6ӛjD( c5l=#&[lG;gnLU?_ZBwn⣾ID0xʒw ~dJFGֱ w,\7d* məߘi/Dz2keͷyv.:WH:0 IDATNeӴ)g voGO\%!a  f c&wE|w}E-"v]Ay3'E,}kSguld@^9Wz_ eXm盕DSz5òL2V#*ܲH̰͌ߢLˉm3L_""G[k!vɕ5.MOM˽q@EuZ '>K-XLy{<} \bwӻ60>5 f1.߿e *Kbէ3'WOF߬k߿m摇~xB <,31 WzSTb&zԌs&,hX o%5s],έVcj&эW?O. 9_3ec(Ӳ+%̙, /~ I|>l]2_<hK̳-QoOc"o&8~cj"?o1[hT u9'߹ܛ/!!!!!qvgUoÊFIHEJJ Geݺu{̜9_~+Vcoj\o[O 8 azJ#AJfڎ+f~3Y?:c /[BI:K3hs?[ϴN~ kY0,H* ! >)CgRmI]au Q%s*ע},~;h lB`??{<#I}(Obfuk?_3 Z$fnwN(#*%1L9|N>ou/J##9iC =&d@@Mۯd }fQhNs wmv7PLǓ.P8*J~-ӻUΞ$h%v @"39, иpozA@0y1o4;yNW?"Z.ݹ3 R6g Z*of=4݂|=}|+E$_t4"GL eAD:L6~m?&OFYj : [tZZ%5H-0 (&_^[ ,ySu.JtHSKBBBBBb"夆Xm[=TH7(ą~^̸,  an﻽$ҭ֠~QI s=` 5t= Ĭn@>"ѓιWM6`]|Fp|ôGp0PR\gl5#*)])cKX(O$)}:ѯ~#ѫK+-@gH$$T͈2xrT:KJUEll4q6`؋┠1{y)cLZ7ԩ0׃ *7f_G߅2,%]|JwrςyKN-d O-y|0cJrra,^lc2Яbԋ9hJﻍ}C[R1gaqپ/e-k~_@!bM+رcYp!b~aMb6}C[T(`" Z YwU$=MaHNRV>E:t(6-f~ՓD- [טۓco~Bl2まRt:"^@}Ӟ,8)sMCf"s3%CF_%@0!^Œˎ.NfoqD^V iyU&iz]X޻Rvb0OG>҃LXI79;v~ѭ[rOOOjGEA@&1yd&MRټ0!חsg@rJMۮó7țnMޯI5P!VUR%4HRt(,$O*WG_ߘ˒OK'DboۘqKTTxhPRA8ՀZtU/j2tXJ-B_ WͿ;ѱr)es"&F\[5D- [Ai1߸1]WC[MqCш(Φ C%cuhr3LKޝ%@[j>c8b TQR s6) ;+dF W;| +$60BSlb8\U%bȆ>ꞣe>IVyU><  MpD0M_bΦ61u?^Sh<~_AJPmoOq/ey(,vjq7[F+[nͥKׯC رcՎ#0L=ɓ'Z"*ش9e'&!}JfDgZwc`"zJCm3 MB^@؛OaNg7+#(ObKiؕyPbK_EE_RL2/ëʐt9| w|7ƟghFEs,0[ͩ^ekfٯQjSd%u=`4d(k,` 8iΫUp:P@+ Y]j EE3W]YG'q ۃ6yS:p~ǂh-e:e7[yP.DZ!t%|eqlpr{ًr< \R3v#Wg|*ci&,=[-HM]tEfCȩ4X:W9iދ5Lk5c*&ǞBLY*{Q 0瀱"~>ޘKd Z)i.m)ܓo]b9b܎w>:v/cYI0Upĸ>3uǰ,b˗l5Kzro5b7<ۆ*Qy8 D'9>7g'N'6mҁ'{r2P/o^ԒZρwW|xŞ>ۧ[pBe8 *JeRLQ6x槒%7O8\Uyy9/w-/>5^es])tqb䫾xҍ6ȊzKeQq'f7@\'_cBXjK۷qDƮTZ#Df =pW|x{޶^f짹hD]?7u|6<֑g\x?5hٙ;3yRaw ^Y#W<v-{s9SbfVpfO^~-+'vL\KI R#Qs#$`R80n<]GSi9rޢ׍ӜbC}^$9,+KZnYY֫]]]YhQxyYݔFɓiƗ!K8Fc~: ӊқtMxZ(.'UTK?+#sA#j>V@f䕎jn#P *r(FCzMGeɧYS6 ɪ(m`1;5= g>2_ؠհl8wcD"\z~>zZu40Ruh &{jeNGœsDǞ:TզO7#T|Z=l,)}v5!M-JuӞZr],<-9r*Y"ha*겅LF'<)sucq˘EVD*ALiۃ; Пw íHD=BKGxDFXj h3"g5i)s~솫}q؈ Ic=aQky>\Mx0H`xNZ_#%УU ΅,pZ.?2(-'*itbm}]/r9v!]ؑ;<ɠABHHHHHHCtd3.\O故7=X(%>v~twz3+Y2#29z8erb\`B0^<̍6/&Xe0 A~;lm KT~kBS.ZU2fݜj`g\'%~g朲ᅥ|n{ƴ)+3a%(aG*\-\\mߑոzQgc[Z2oϛuasnt*?aO^䝆3o0J~O$$$*JIڎxdêBxD.8*VbeX1DYǼ&d^vVXRhaZtj.oG6O ';mҰ*~UL:ӧ[Mv xїEv{i]a| I lnTx8Уc Nߑv7d-m!ÖB0磪|ڛ={6aaaVe̞=wwwrkr[===㭷޺%X{uwLD4{ճ_/XJۈ$~H)1uCP/ Ģ"6tZ kyu9ĕd$ d-!;w#)KUBIq͛\O6H'5=SW~ʜs(vh 7~EVe`T3np'T4 WjVd+Vq1x$f S1UeJ;1I$.^b=*?FE/CI@Z&co:KSkNTƼU("TC/xJd= ?]1[5wV\Lo l^qA&h-MEHڢ/W 3֡V]t܌`KB$?bᇋ[d+kȓ֜H `…,\rq]ބax>h s/ߢZl IDAT~^ݧw'R4K ގC<{jgr%/ZlRXxk19wVҁ|;bSJ_0L8f?*liwYOۖq9+0M*o`K@N{ti'AE+kMf˾xo‚{<\[)l86LḎk2ҖMa `66}s =RkYҟ}R*RWTZy|v ,8ԅIY4Vx塜:7kGk{Z6oFά@.$ءxzaYQקs<&zug>D1Ru?* ^\{}_0o>ݎGfwcY_[MZ@*^NL_ד19ڍŝog^T|[ͮď_a|>,`6rX!eBphB6,xFcuZF #bp򯯘jdlshRP>TI5[,, / /5_|9*ǏaD*߯*ųx1gL.0޿Ox D%{a#pP fŃkp#$ȫj5X@áֲ]qDz{bc [(p(m߫ݼKk]yOLFZ ϪfDƌ۩|siߩQKZ_^qXƿQf76Yy)Ih's$P2xޛ-BlU<;xbtA @IK\ԃ b꣏2! q)&$!`OH@j۴х8(Kq"G%QQI3x.b KK t>i;ϳ,&rܝtٜ#Ћoi;8 \i]I{d ~ɰ ?&r 2@&2o`+1]0sʂ,%U?ïtYr5%YXq`Fq:|ޭ{5L)[ëTIct+{I#639Fj4kAFx{Me%$GEX 6]ۮCĿUUCsQV\iUVQTzD@߆O_PUڿ^}^y+N6VHqGGr2[{dmiϙRa2ɭ۬zέy;|Wn+lgxuxvk YBF>`VNҎa5ZҬDhU/06pQw^oz}]n_-$d Ϙ=muVrѵ r*F2Ee-( &2 Epes:Ӣ D"?H%e*IDUP=_>`\6jmh,vvU 1ʓٹV ~3t[Xf粶e91[sέV6bĈ[В⑲pJz\c0Lrtp8!gc8ƹ.JQyv-H㿯B/H!b12%; p%L+߰T!\vTs@ĝ[Kq\ž ɋ'x?edzҞ@a̟2/L~E֯[^`mYd9g Ϙ{AnXtbn|&,ͣ! vwy.]G  a؉<⍽w>O򥮘x2PhCx?vJ2Τr<8toWOFt-E!6T]$` d8XgFu _Wo6܏_6@e&M]rW.x@IGf5o`~CIk,o{6m3J<څ)qdǀ~x$g.cB϶~L?W#Sl7;1wZcYyf68P3T>|`8ͬٺf'{ It A2GQėfLcIԒ dFW()s$ FJ[ #XAְa9IO)w }k*qh4dggkUpB4 /*/))!&&կ_?Wy Ǿ> PZL̫  ʭX+ʿ<#õ=P[{92 uZ$2HA_U怭qoDⴜ/ǞgK%i( }2+Ek4Q)N8#ͨ~L$, Rsۇ:3VEj yktKoԾQ#KVHyA-YI_.Yɗ ̘3r%5"P:}&}zi%58iR^`TԒƲj5䜺<2F ~,ZE%^;ZEt9x=Zb=@FFW (lR{ھVst[:0EFtQ՝'cXsujQIyDݞEtNxxq)M%#Z76{W#Dxr&! @$fs rrr7nU͛ aOii| ӦX5`KY{Kq^9FTkKc7;奀uN)ànkcѳU*]+*CzQN_܈#qE(qќ㼥F毢g)3xpuy,pW%uo~*Pjrw0CmطnTrV2HU]\OV A 5bs4tC :\\!P9*|˳,j|s]vql9y8pOy)Xˢ,J9gTWZ*V*F|՝v*!!!!!!!!Qa/2KKW H"yLIԒA\~Y Ǘo/t2įaF'ؔ/"=V;Ҝ=L8£[qFڛAƈJBI E*r~ ^믿&##u v܉Ngɱ̝wIvvUyСNN5ňQr8V\cc Xz"usedtiI`oUY*t5.Ba_ R`*.9w_dewiW֞O[&edk#<4C%- &Ykm(`OV&~w&d (̟fxMq崮2GVd?O,5썪>'ʲ;w?yX-HԂI].3|/-P(ߚs=9#weʁrQ3;Wqb[+ypZ;Mfڟb%r#{sd)jH~?X"d6o.M4/jKM~Z6}[T>$bÝ+y+|_t1:0>*BLЃ4ú2+sY KHI>;q*ۨx]Ғh1䮾-70AuNTbF10}9h,Q- Iy1bjH 24M۴崙Zf.8p sߟszu%XWN4RG}h lm*8%k|rf7l۵;;ٽ}30Fr s͚5|deeaV hᠸ"7o /w޽{FQQQP+((sM5dȐV}R1(7/7?`X¯z ~e2~5e|]9WugwpNmh>i&ZWr{r]?W3W 㷺jܹ"}AE,z>j;=֙0{w'.1IÃqN's7b bY>l5xX]Kyc}H򧻷gd\~̺&$3|rPP7Pzy >[Ky}le԰jV[]}¯c>c=c)|j>V\U͎aKrnM,#¸',8AW}u@RCf- 2!m~. NqR̎#^B<A+ەt?XɪsxqS}P޼ܗp||$eOquqV7 p$gu zpu? zܛJ̽-H>~wC aCw2~f- RgP XД<> (_³:k:w1IսqM:M {8w09m$Gt~ Dܤ|8cdT/~b/0'KB\ y}^.dss]={x)+Hg_fsżaZD \f+99Elv6.է~ʰa9rq[qq12 IDAT={Q۟s9G6|p~퍃/ifM7yKyx|.7xns)n.uO}7)MB*̍>Maǣ{Vs&5wˣYz{_jb2&~/jXTk:V/ dh ǒԍ?C#i?gL~i.e5wF#szvуIi&]p.ꨙpu1D/?(a/P^Gw0`mkp9\;aWٱUG~f5{-vQܨ }Eu?cN0eo?hT9qoX-D?fe`{wUs<͟o_>z{_.~ӭc婛YD;)Wފ>jͮ6E}1|hX`b8^+_ᨥ?;K!\,oFs.;{);l֦c!^OLĬ/]r䇠".-N]ڴ+B!udLR;*/?>GGӃz= !< 8 ^Noc=xLfuu?01,!ۂ cƿc{4PDDy wxbrѯ.~i(JDPFS-7ɻP?&8oBBu/1cэ{c_᳿36?ͣHE0?%ٵ+×}<:/or K++ȓ' Ήp<șyqjx|"`#_?!5~:A;{l-y/a{ot/hwXcWCrL2(k0ALzt YٸAL@D#_Ga9~6mA27=@el(Oû*)6E߆_f"E|oO|̦V:Ʃ)ڟi[ DE۲ز(v&%N;`gck7/_Oo3pk`o5W3~?b'։0 L&Yj=13߯mڈ߾8Nu^D-ىΕb-ǩITpǻNDlv3Q}OQ:$v'ƿG6d57)O""""(ifoZ Ul\ɏ/.捿,b{N)20< .+aP˜_N[py eԩڵ[uV2220!lXck߾}ODXK=kNH;2۲Xa9U>\r#>3B-v`›ae2y`6w;KǮxγʉtpz{r2C8}zE^8())1BLj)S쭷""rqzxw\0S őKan:iiaTRC"$\9T^Ȗ2V‚#Pヷ\=֏#ypoc |սS}9ط'Zޡą"rvӨc)v~)l:.4Lܮqղ2j-]f >Z#䪭59ϱ>U.œsK+U#Ns>~%JQ5=5 ۝[SHOp.Ea͹{,:`מgNve[qeQkgOws^Eh DJe JNc Imkq&o!:OOC@qű==u ! ;'m[vPuiQ8-C~ 0l$WR˺49X3ǫmX,%_`.rB58-;YZøk̍569Yirt6*/݇fpa[ao `Y_L51;d-~Anҽ캶>$[]Kg<˻6иt`eX<ȁ;"2o0~ #ZOy,[?M5xLn=suIC\]T,.>z:|,]_?B˘LL8Q֯5{<߼KyHfeei;MmȤ ~}<@5YJ)w0ϱ/.nPɖVާ -cI4f*=H1`&}:EtϭHgPK 8îO YLuJnS3=\t_h[d [X@fųr6|4x)MLb*,O,`}〪f'}EWB^!vy2+yHg`*I鐃C''691fiiQ[U9kq?s~+G9k=t<*:75 s>+N:668=ts^woK="i&+fR7o`YD G namNIkypܶ]sWӿ7rdmbm 1!^=73yjj7-JcۮL mYl=&B{ѱ`b>|g&[+ZIDDDDDD:T7[:A./w>?11FG-Kynޱ]^^ 5FNajN͛k׷"8[ooB'/kg2w@!O{gsrs~tĊ<.ld";0O:y|^uyvG<6*l%O ջW1o.PF;/%+Yk/>q1 \}$f mcT=_qK΋dpߞC>ݿAf<6^`%K8۹I\亿ڣƨAC>SXpEr~o.Ӝown9w iyߗݎrf *c\l.I䯽~LNs̝k|G䝃I[$+<5+_i,ٓ:_&rՄ-2c4>Ob-lwMw0-ɾݸs|:6nd:G49]W{H&`Ps${G!)[NH[9 /ї?یv[yx*Bs0M.m[=kWa,&g5?]y9ZΓ¯Xu3k“cW3Ͱo//Wװ&ԍ0K:_(""""""U8UܙN ؒ`t8,z%&Zj׍AA ⃩},(f5Y<tͦ}9arպBCjboQya6-—`ŗPKɯ,dP;\dݪqx繫-HpBj*),dݖe۲o&/&F,aGc2T:edߝ& 5 7; \24:22 rfu.߮N7ŋ G*qes}>X}l[__L՗D;J[Rkxt~~6% :%y./tX.ͯ){IGt3zd~1$֦l_6'>3fUNv1;ve#_ O?\[z2G >22t}֯wuT 7ܼf+Ǫlx| rng5+S?l3t "^[DY\%.z >DBI!rY XN=5xuor4έHgPK-yD3pog|JJ+$ہ)I<ΤdA '&: b5e$tjcs;TOpڝʀky1QƏ_M˷}ܲ05DwZX&J  &d >ʸxf|\`3~5~p8]D$'~OGͼ?Qk͝kl^yy>C57Yެܽ3=?qs)e|mnFELRA`gbʭg^Y1 G7 ͎gח󸫸*wL޹ `T?^*X| xT_z_3-bj 䂋gjz‹0$7z]]Nx |럡2ҸXι)6Jw.ι)e,#.cX ><槨2ǘtK]ޠ.WO~k;+.to4T04fN*X=yYTx)Lj廕^LWpZ>@3xRzgkt^r#U9 LA:gÁ@P9r͊;y8"p|,RG~"\?/_Ӫc:cs\fqf_{ '| 49кTVwOoŒFarN*ؒՕGﺊK\;19D)ېa1oL}M`kwM5;u/Qh8WDDDDDDDDz-|ǒ8zS/]DP&>Oo&S+1L~ju7G8|,b6ɯƌdrWldc3kH{̄ݧW}ǘ ͜s{*81Մr_'$Tk,u,,jَHkYXmS6x&pk|W֧& \~uDmZQܺR gTOjfkhpU5fs ݔ%lKא Xc}L8Kt=XX}`c=:x ՞Mb $?z^8P6sq};u/Q(޾ w쵊}8qVnDS |cӲcSM(\m(l: "{{ _f΁Ӄ.3TȢo aAҐt49XJ\!FqLMPC>+[1 Q=ɜ~tqV>O@V.d֒cuu5tj՝`DЄqmYĵo(}/xy԰s[:ݭb>}TzkWI+ki0|"3b&_0S:s`6si}UGZuX[|OtE:Z""""""""rsZӺk$S.YL*NhAjiJ\*M)J7{3ohie}mENfc{$MyYffE'e,|k=~%hZ>jspe=~ۮ:u/Q(^XX|6QM%{8TrR=ĵ֎`TQCm OnRLs ²"!"i~R|&Bm=i>H[;BmA&(uR];$C8dd-w,͢@.*:[ıƄN[٪|*Py)y&蕪_B `‡.gNա{$s|"g]s0K\xxXZD(cFI}$]u'1vﴔKdr֞" IDAT{6%;۷fV PK.e8-<|D\Æ[1H}?nhZLu Hp|ՐKcobJ$Cb:u/ґ('5r>ं?irliـ`kڒ'}пmMLIc^NSLBU/ɬAU3e;= y{RRUn&Hd w@ѮfYnf@J @HH6+IζT5e2u f/X x ڬӓL^}e.ʌRs Yz_Kfֆ£†}[><+^YW:OXPV.ێXˑ߬ɥ@.f7/,feܭ1 ?{Mc˵2wjW|~j|srv9)c[o ).rDLyNos+ґiM-9+XN2)w!o(nfn6;;/eOoMSbB;fgPF唔/d“ Wͅ<7=\с{&;>1EC],2Ȉ=QfȄxrl YfգZ]:,9݃ O =ӝNݹjtbN@qqvn!5Xĵ?$&g(7/D}_ IFyp)bM,,3vph?(X-Ͳ<5|ˀ1=u2浴ߟxiO1א `5ׯݬ0}ge]Y\uaZf?Hݾ ie g䟕ؗ> ;O?qb%?K ]@ռ|`g27^HbV5;+ؖNj?gF|od5M+{ظe4243(Ђָ܊tP& {E:9s0{l7W~>c>SpKu Jwf:Ќ3]ٳgߦh}{q}f̘!7\ҹ9NL&Yg׿9sÃ< K38MI8A'{tGNu!wRYĐ:RU sTDtXxSp,}TyX=>G3xh}q8M'B49ط':Chs\]ANa%fCB:tVWރwLtZ}{[t}#9w"9{\w!"m <<%MG e#??L&򤝄/8obrZ dd 1};v,vX7W(""""zjq+w H4"m׮]bqw9"ɤwRVVϏ0"## 7W*""""ҺjHHOOwwg`wt=RUUEYY  c̿.nww ҆9JJJ]truuus˩d2@nCKg.CDDD2z+"ӧO\HPCə ;;RRR!??XDEEDf͚***"55$&&XDDDD: DDDCLL W^y%{1??={bIJJ.WDDB;w4gϞ\zv\{)N=ѣ^z)Ť_|_|A׮]!C\ĶlbYw`\s5l6u4PKDDDj!!!L4I&QQQArr2))),]ovOk騭m2`Úl6.F׮]\șI2""""|}}7nƍ֘0%%&KIp8f]OH3, Çglذ֬Y!C+44ԝ劈&//6m@9 <t< DDDDZ` .WD+++3BTjjjfneH3j!+ ??>>""".""ryyyFyf˜4i 2HK(93eLBII ɤh"-ZDHHrED8[n5ڱc}a$&& EDDDDd)93qD&NHUUɤO?xb|}}nEDbݺuFGVaa! tMl6\Z""""7cƌa̘1)))O&W``+i;AVee%^^^$&&rWbww"""""Jjt` ͛kڵCanݺTVQXXhX֭K.=͆nd2JiK[K]B-N">>xnu~m~mg`.WDvaYYYYՋiӦH\\+@ [(99t:]șnΜ9̞=͕ݻw\DFFW~\6olYyyy 0Nbby͛]HڄB-P%""EQQp5LJRRvB9[4Y Ʉf3Lq#M?("""rڵ+\p\p4%K7olvw"҉AVjj*555!neBYϏq1n8jkkIII10:v;~~~.YD:={!V4baaaL4 ͦQ9&Z""""ba >]\W`СFյkWw+"LVVsNbbb>}:67W("""""i%""gLc0z׮] 0z ELn:㵣S BhjB-\c¬,zm\nPDܥXxzzbۍ eHPKjXkƍk]u\#Zn!!!Fe0LnRDDDDD: Z"-PKDDʌiRRR#88.QDZɎ;HII!55،$11΀\tV.@DDDD:@&L 6­UVd\.YDN͛III!--={ǵ^nW^nPDDDDD DDDDUyzz2j(F@ZZrM`w+"p8njj*eeeL&v;SLfѥKw)"""""gM?(~PDDupo E^%%%MZX.SDDDDDb DZ@H۾}}vbbbHJJfE{1ܼy3aaal6v; nPDDDDD0Z"-PKDDmז-[ѣ1Ea\\+>m߾jAnPDDDDDcS% DDD2c d$%% ,qFHII!??8<]fgϞnPDDDDDpw"""""M`` &L`„ TWW\+Wdɒ%\v///w,pbYbԩSlLNIidь=Tc+V`2\AAAnXNVII>Vjj*uuubٌ,2EDDDDD:=Z"""""an-[W4 207W+Dz{n#JOO ""/ƠA\Gkjӵ}v#ھ};:\QQQnP233N\sdوvo""""""g9Z"-PKDDDZS^^WCP^  *..`Ȑ!v޽+ DZ@b+-- ]S:v.ld:t eH30i$&MDEEp|wnjg߾}F~z7DDDDDḐPKDDDD رc;v,NӘ0%%˗cXp+)) w|ھ}>Vvv6QQQ\vev EDDDDDd)9L& ưaشik׮%%%_~8n'44ԝ6nhY{`\$&&ҳgO7W(""""""Ckj3IVV)))$'' @W^n}TWWj;pfL+2EDDDDD(iZ"""rڵk1Eaff&f!۷Is…L6KmbڵFؿ>Vjj*uuu6 HEDDDD:^zѫW/MFaap}|tZϵkײn:nvZ\^y"##[jڵJOO ""/N|||(""""""g&Z"""""Dhh(^x!^x!}_5$%%!d:jEEEdeea6̝wɈ#Z˗+t:ٷoq0)UW]f#::Ujw|Uٓ@3a(CAÁV;ml\V VDe8 H؛$MIMq3"a%xxs}>WD~8j2eLBMMM}ٰa>>>ѣ`׮]8V+/2so<{^r2dNc~"F=z4aS%""""#?~<5jcƌa۶mFuXLvnn.-">>ɺ۷SPPPdYV|}}öQFՇm"""""""g_"Ң 0u+;;m¸<7֭[y7px3  ]vFy"""""""?n D@ VXG} o3gz>+Wxɓ'""""""r"""""@FF& {=222{1=(,,dѢE۷MmZEDDDDDaHdzlڵ@ B-M61o+ܻjlN!E `!73ml=l~\y+ ɽ(. hڷoRپJahdVr *mbB>0WfJC )*F6Ic=Fx{Avo+_J&.2~M#ug6UC@m U{1!apOG.y] rGI$ͥfM@#fa>{9ՔGœT7HaO(r6ej ᒞpu㎐YY%ؚN4w6%9Cv;t1з&>FrF=}&Of?`·P-Lu=ˆ` 0a6Y9V䫿>vc{;+\qT^Uځg ^A tCY^G 2Yqi|<,׌Ε fal#Y{@fEz! /BUSDDDDDDN*DDDDD\{?<#XU}Eα17S"6eM+Ze"amǚ/k#u6yF{@0~EeyN#& n-_L 쬂F=溟[clknXJs $;ajc_?n ?|j>ͤp[n"a@֟N""""""MZ"""""axpe&a A ȯ2*kYXUۉkl۔tK\?Ue km> ]seU*f2 =Xm`9>?L^jAύm#UnC=i:AMEv6X);\nMkWtxK ;٤PKDDDD8Q7o>+b6c鋗IM5+C9,Rĕq ⱚ2+ثa&8T' Ӊtلw\Y.lrV\4= #TNu IDDDDDӟl"""""m`X081_Ճ&P gQ ApmL&0'n;`sag.4mhwPꐈL^Uv""""""rh`xWB"!)SIII|>&U.'sMA G0;Pap5StY8<< =}j_6!r_|Lȯl.kkC"| ji<X @NYPKDDDD 6l{鏝pF!m L`Q22 pmɑt58`eNݧ$8 ZY`c )Fu+Ì0p [P* -O|պ!D-R k"tb7̶XS-i^zbWU٦PKDDDDL~t%yLH`Ў\{$qPaKm%0.UyJ[=|ߗ( =='#; O$r(c$RKէ8{GW3/ՅUl2xBsG (̅S! $_"0oC\>d=%vKFw]{8lo^cnϐXgbF,Ob$JBg(lʁřDDDDDD4S%""""fFr 0{9J¿.S Qw$܊thgc5bɴ´h82ճ`_6cIdng?4~a+\ nEb~0ynT{&`~ @֜4w'sX7`yܲ )iOcܬ |'UHa';*i*%8w'0|gwÜM0GB"q5~Q *s=-l""""""rzNgL,òpBϟ=|oObPcg$9!cn rLӰ,j{tͩjh҇3~g_,&}*I/0W!Aj }<_{VS]`?X) psۄWRga2iGwBDDDDDk/4iUR?7]^;?Ļ醧dg;-]W{ p͓۰II_DDDDDDNNOtz~PDDDDDO9SjHɎ\R""""""14tz DDDDDDDDDDDDS%""""""""""""B-jHPKDDDDDDDDDDDD:=Z"""""""""""")NOtz DDDDDDDDDDDDS%""""""""""""B-኏.pĜ > Kgн{wBCC;""""#PKDDDNJ||< .nqr}oZrZKtҎ)Sx_eGwADDVwtf"k\p6}s 7ttD(S']tEZG;""fС :!"@PWD)_DQ%""""""""""""B-jHg[J2~, сۧFgp4粡]!Lr)NghQɩUT80`+z֥΀rJdTWg/aLH}+JGv`pNJ%H|'3}\PKDDD:X6-hBi[:7BiߣC{yBֳ=L(SdHDDD:%>K. >u.tSIq|>\1<ܼa_GwGDD͜]WWy,78O(QQy6n!\Ua#,,:m]Mgu0O03_ex k7Xqo羷uL{,|utDD䜢PKDDD:R6]52Cs^ôgw|,lkIU.0>+]n} _ћ꤄ėg3ꂻysWmϹyﺕSfۙVܽ:lzDDs[UMMG:%y/|LUZ""r֩RKDDD:PQABRVV<~'h۲i}^`2FR@c,Su {5T3BfCm"B&|qY] sa 较I+9"ek FGŧ΄U~M2Jk 0cRl? %طvR3Hwq az{h+}ic:2m\><?}}7of'?΅SocCCe:LQȵݾgg59)Odp(w7??"Nڹ6鰎vYv4&]4O⦉}!/#f=[{a!EDDZ"""iP2G1j cd?>](.P F# A~I!>f@%d/vkߝCHJKpe8ߏ} *+`>;ݮw:*`m!<  Y*;wFF Sƾknaza~FDDYcZAv6XƄrޯnۣ0)pk%Q\ ث6nihɞP|k an?c#n^~u4a|7 )|S1V\|0{C]9Ӽg.K?o)ݰ]n`7ZX TFz]}º5Y|EmUl;qdY8HWr 2(zM7[ Ii5UO㛅c<+VO]/D}fD wnIf9)ag28O(Gw yI ݸ}Wx{mn߬"^Gӛ2kl_5NX]S\5>!Q[Icʗرr ?~˶䆷m*ؚ]%4;n*ԤSdW{!݇0rT(4XחpM^KƇ I %$͡b\]=˪+UݍMĹz`TIs>j7+\^AQ?q`7R)̉(a3FeC[XKm/f4\Gݗb2naxet-$mz; 6~"Ɨ!3^a}f1Y]e' 3_s=ν!ݽ{궁w[WG7^dIy;!W! ǻ25m\65'U6}lx19˗0&|??<l$OyĊ4Ox&߅WW "Vf/  Cۀ/1ļ\ 쟭g)WDD~jHdzWűh-5 =% _<1-qUVs|ְfWmו{Sp{,yR]oFq7"«!Pu^ùGFTv[>۷@s;Rc}4@k;{9vq` pFV={ \ gc1Ɲ`bK[iK99* `wqս_m.i UYtba?{WkXrN(is?I IDAT&bk f>V_ѯIs ӗ.7Jhb{K +]ˀw3CX/#- Ky?َPCͶg$}=Prnq;:)aү(¸5}`\_st?[K (n嗷wiv;[HN,l鸕_gF"2a`m{REDJsjHP.;›K=74;Ϗ7pc 8Ak-0s#* *qXN6X8[h(\Ow}#y_yVy PieTPSinc8Uqr_I~ۂ~SǿWu&L^n+GG\Sh5wy]9-ywx-BRWon>!QD8~@8,V4Jk$> fV*ƍab!}g2+߼/3ɓX Iܜ qpԖMv{j̝q/sMt>,""r(ЦU?,}g)oMr JtX@',qUON.M?7ʘ򻧳xNs.PDi޺nt2{PEƟt䒛Kޥ}$SD䜧_"""ҡ|K\S𸣹l<}Nuz/_Zw>K \#g UƅS#a&`fp,Jꎽ{7|xtO4 \!ttz04M`ONvsϢC䣧3y.?D,Oݹt(d\CE_WoG>!Ƶ>O,LxxOWxgyЧK,1b #苯9Pt~gUs8磴TJjJ[+=%$oZOr9^RǪ{Lߩ lTam}3‰B-PWl97' J`U <{IX 4~~d z4&?nkv8>k挞D!Uegd``suݯJA`H5ހ ⦇?!>IS\s^P? khŧ̝12Ǘofzv ПU'C&\75,TU>-mh.1|[{Tؿ_okXD_-TnnَfדIi8&3T|' M3w.sNxq_EmA&PچʛzQ7RZ#X>a0 w17tmX`gڹ0# R]Z k(`Rqm? }uQ_a#ܞLavk桫 3 CG>&#f,3q6E.no ^mތj,锜՟Wa&?=v96r _L@ KO"ޯXy-WJ(.haӸޢYLǾ_dᆀ&2;brZ\Cma̅u=\٨KlLbFU{$)T>WuLװ^ʆ"""P%"""fHqsKFe쿃}#;mܽùjvcCGصgQ|~P/ QjJ10dЭ[nInܲV1RnN1LY4 WQi ++Þ(״#U <++!C:pAkfbX>KK^8ľ̶3pG+\hn fR{<phcюs9;㫻[1ur3k[(['vZb @W&04'gc {(d)/>Mӹ=-y?ʊq"F`!-`FB{׽@L+7ˆaٻ5,x-Lr<4тXV5N:?jrXVy.KB ].{y9W(Q>' 8(kpݝXظs21a7K~~IˡSZ/f2mPKDDD:k~?w~0 :tYwz kB>)"{֎lXO;rsQ׸ '2&8)_ach< Ϋ޸h\ ,=n8~ޏg+YExr^J!' %O5cS~}9p콇k==0Kx g>#5+xlos a Yu\Lj8a'%x1>кuĉiЅsoouI[ҭ'b  ?]CDD@Humnci%iDZc [NŚ;G1fO/PJUiC|p.|mBa=au$(y˰6 D1puӧ(G2NR>LZ {/I7ٹl-] ZyP4C??L9xLa]psx}º1vD;mXDь,ɡUn#iHȅZA֞㞡#6_DDt:^O0(l&4]?_2GX˿ TEn/o#afw']g1-}a\֐Gw#pݖ̽f.~vB<}(͎gqdTMW_D,/qsj tz4 =>JHWW1MO19%T6˴߿Dz&^]¢-Sue:3}I,] %پ/Q ŋ&5_’͗pݒDvR[3{ҌݬfG XА\˟W>_d-~1t5/Lè. K0K__7|~ u,F&!}+7ϕq_΍70i\5֮`!K[#""'t:8^ȹk…̟?{""ydzpBdO9r;a7J'ֈ@4 /&|!Azׅr X56m났6 YXA>>oUX2,dU b tHǺb}XtiGwPw6|zX#AOLd&zW j}2?~Jq?g{.Br۳\Ӭ=fŀ/w&o߮"}PfhjwW0Gyy]ZCŐٗ1w1k/Kof2zU5ڮ:}(j^_h.6Ci圝cX%|9Oz`<9GZv~*k=O]~̌zr͛EsB'%$'^\gJ<Y 1O=~-}E&}O䦇w6jS\wBrE,?K>qorx承{(v/ eSrK=vy損iSf%I<G s)s?e;ȏB-6P%"9jJcߵȫ+x03C$""rj]  ̱ ϐ!tjBQ7ZW^фk:/*|?~MC'\Bz4 ! ׾s>pJCzYe%p(=KMU`.eȀZ{D6獦6;牭if9= K_:kŠqQ :EDDPKDDD [HC\mLKDDDDDDDDh< {ut/DDDDDDDD~0T%"""r2HB١""A[?Z""""gRw{_""""""""""""RKDDDDDDDDΨ7|nn9BQyyyԘi讈itЇGtt7DPKDDDDDDDDΊǟ,.it߭9hN-T%"""r9T!׻C{hR P6+'8dX3gI"""""PKDDD:@k5]isf{9 39DE_lF<ڍ>Mu~Z)*፹PӣBAL|x/I_j1NsDXڳB!BCZB!u=e9_(aL}-&F~N+hjr%LtN6;*K+EU[79 ģHo:8ºϵXHoB!B"2B!z3_a^JpTEV\=O*5Kcs%$R!9Oth.]qa➭~K⛿Kv@+s\7uv8Zc$OOWL!BыHB!z$G [z5\7*;^zQ5\:tzA5휵9Oew83F޷CcHqe]R33dA=]nfF{xtMu]k+B! 餤t {gG1aBыQ~M/}^eZ6?Ȧڀ]ឰ&Us & {8r /iB{ |:wZk 0ѣ~pٌhS@:mv0; kZy_ƨ[)hgbAEK:Rt9rm?Z}_w]Wjk_m/UqΑѣN!#%%5kt5P$%$A-!Be[(aaaloҿ1pphCi璲b`7ŲELJy3GoUJY(<ˌ 08΍+lbV)ORm_10+3ﹽlO6c p[%2r>֗ǝQF3lVf=ɯ ЧW Iap}󎙙M9RԁԼ9 ĬRLhG)1Rð`X 3;rЁ/1ݎ#8"Nk}bkޙI֢qpS!lV9Iޚ|JS algge@:%VLMq;L ˆז(*TY'D q9/&1ai3~:E{ٔ0<pPխuk> oc cf0#nqp{OO`c*pFR*muM nsǎ՘}az1O)RfB  Yxc}r JO0q8\?tؗkp7ws)Ȫqn?4 `Bʞ;N/:ѰdDHB!gU{p򞮂$%B^cІ6Pwq4ߦc؟aTxͥPz-Z9͌Pn?B@ 1~hJ1AZ/[@tخ)~*#_19P:_m0FUr9cBJݦӛ *0%W:џ_P烗ƄObxk̪ uuZ2aqWAa>B=Wcc]N"$jlip^xxAn k8 lr̘r ˢ[]-O)| Io*w+5X&,qe7?Y; 6q#e_vْ嘔NWَ鎺 yp IDATu?ֵdPh?IY΅H?UTSÚ5y)01ə~ ۖkykN c%@3{p)GO3ڄk!ߎ!B!B!D`3Ryu^ә/[UYYyКOL>\~~Ҏh_aO]ᢏXUpX{1%x/Nr|^8 C ~!Sr)8{/V%1A&̌= /Cήl;l~}m/9؀*`|txv% j rL"3| ;V7[ec? _D;WQP; ֝) #|A>}}FX6+*fzQ}3͟;/?^I73Ӛ{[*ݱY6 g&ɧ8zN'BzO`w gs!0Xg"CUAX, Y9lݕ3*SMWbppaJ/؋\ ?5 |t@xu %%D|< ;ސf)X 4xJXцxNN%n? E;ڕ8h%>C`\P$m13#ë]ma4ڨ/gt `7Ym(߅*W~~5ÒzC *u &<7@^1h춺5'N%6OC`N,yYŐpB5{)D ܃)kAJ}nH' 7az >ĵel6rk$l}ʹ~ mg|ym;pytmi_gp Tlj|_IPV +twvЄqX1,W:Ǵ<ÓꆗUvPX!B!"A-!B6lf#`rÈD1Db඄,j7 m}&Ms1 =溛OI`g%v74Tyn۹~LGHhD]wr)3ߥgّ# Z %Ev@= *nRGVN[Ŷ8=!NU~$ %O- =MFBgXH~"2Z asqH ņ+U1`!psc !B!BZB!qKJ9 P%Q: wƆ3d`swB6óJ-tJI^x뫖斃s*(-P¨&"gh@OmjjJMmvH˜^AIm['7?B?Q~A0ک/gSa ;P;3| Knq_'؟iU0PCݟ}*/%ꚛXl{ڡ_B!BWx!B +sف0XMp3!eɢU܏s"Pլ:{H+a6?\9̖/n;k g9Uxgv(1ӚaRݶԔM|](n+G3hQnl‚Ih."kӳZIUSiڰWu0Ion&JMn}<,μnرΦ q} 5-<@[*?b#"~L).!s_ xe|<'}=h pJu'B!JB!D)EJt\MM *MÝ,ap۾y&H 83vuNDVgi&ֲ1cxy~tVQu MtM9ep :a^6H ;5^\EZkMwk~J(hz9w%4:~ZCھ)-hUu7@{YTr-}mukS\ A#ݥu{VY[g׹8*-f"'L!$>Vt@Ǫ j !B["A-!Bdp>y+wxgyjo}d@78;ah15t8$+7dU%grxcm&M{losA߸w;\K 0?wj;TvmeKZr+k8Ԯ ;1AQ[fa[ݶ`28JiKFѸs:XDf`cI}!u_; 2n;tm٭Ʋ̠JsPֈn19ZN_w;6wvSκE3x7,! XQő]Ryrv9R-n:+șQpNx6S!BzB!DX*^U|1%Zrv|Ѷu׏fANcypf6֏8}bߎRl"+]MPW}7BӶf_A5879)E$ʎÿ᪾ ჆A*HMHpiW UL[mY eW7謯U渱Cڙg*T7g}гrLgp| $z ;7&aY^SÍm5~*g6tw!~t'agoM'u-@Jò-*瀇*Hi~>HtgO5A0W6N(sQGF#zO:z_Qe4*tRZow+wԫit꩹apu(B!SK!=b\6Vow4#}U3tTCy5O&2+ Vsṃ(|(BTxiͤK^Lzs9 [L 'sJDkI:lzHXKBt <!0~?i s-gKO90ޟ l] UҒ >2LXbp]iLQFSl~7| kR!)fD9ɻuT0xqḨ1&o1VÅ% tjGk0- yb0⵫Y5n s֙ߡ|ØUoiAxgS1GSٞ8%*x06Ym0*/*~zfl{ވnk!vtW_3/ɩk~y} q'eӰRD+|vlZUo[GΈa| gڝf; 'B~#p^J` J>g<B!oB!5T``^[J*֙+ Um۵ʃuSym$}œ_+3FRô~tX5a@eݥ L{d}C\D2M@p,OQoIJ6>I'6AZkM6*at$w\`"@?°D?a ̡lcr"3c=QL^ջIf-T~MV1i0NéElIFPCK_Y/3GwgG2`u>ˤSlQkGד! "ZW f…' XBQ]Pjh_dukǀ2~OA F`I{\X]?)Kb XvMJlwFon':zF^J7:<)L{m/^OYCpE`͍mbQ pn6g3a4>( *BJx/[.zK:|_ק# אJ_OzC`2%+Tp7MukLIZl6JI^dֲSKH_Y*P [ˎnʺyjHho`%v6Q83[ vo}ߓ%h eJFys]27y6m\ֳ+KLjyeL>w6yWv`jbB!BF5}0u(w,rDŽ_b̶Hkdp-%Tæ3H/ú4;Fn=- |#ݑB6 cC;yN1,,nS^FT ^^'e[I]] !B!zK?ca<'N #pZ0I}LNBŬp_U$xdWсuKX0d6מ{"e> Y~ !B!tDw2 caElOWC!N;qsH;_3Y$b m3NpT+PhmFjʑ*;Nv/o )zqVR{c,ʟyBݥ'- {;>Yu}QԮm)ܱP}>Z./wTK)Sa{Y(>{/Owa6iI&{w cqR'rv`J~YՏA&ۯ$f'^A3SP01:Y}zo:Dul׶lggI9&xgBn f5]Ͽ*w%c k׃sFWefimR7]"RtąTXY+Iz`fgU0::]Kr:gfHT֐s}ê8(;ٜpQ T*V^Ѥ¹e'ж-,[3#)8Teh\¿dl<;U:6Q1K7T$̝fHPsj!DGF6mĦMgL:.ݯC;a n@&-aў^efl=^LWQTQpR˲bΎLāߓ|,HÕa~CTTTիYz5Ç'11DZ޸wC)ydXvz:3%^u"f-2'YpeO<(9Ȼ/M+9{>3ۮGN^t<=s9ͯ]!Au}P1[zj.^JB!B!!vv}Pф?\x6u뱢#4gvJ'92k?ǧk\uV nbTO&Tv5y8>zڋIGp8Xn_|*ۡpus*xuݙ3;+_ù7t$o@A gI`lz)ˣ'zRs:XAa7Eү~Cٙ4IxC)τcǎW^ySrxhYa Eg>Y ?YeZL\lBl|̓-:7︒р|}a'w ?fCgw{ -7$SK!B!Bf V|Zo\srGpPi_|ד̎^<-?]d̆Ѽ5v*ƑZ6Q*h, 2Oʽq=]+"A-!B!Ba[g* o0Ԁ?ǐds>'[5+?/g>-</'jhUȺVj\Տ1 YVTNn,%Kd07_um/enRȺS_#ԁJgh`s"M5!gtW9X˺ckYVֆtFv{7UN[7U~kFCI:b qݓNM媊pUnt|,7TZ"y5lg-a6+,>!\н<&SB!:ZN#`WOWHtz_ SB!DhkA< }j($w- KCYRgl@+DJ~Z:owTkʎldu`i6Bżc55վXUp+Gkصft$~Ll05*ؗaFRmc690m־؇:fb c5Z˃.w<((M[Obb"SNۛUVZ- I iSR]m[/ Q~FA\ cWB!DgUdLউ_OHtvOwu19B!Dw,(*x:j Od4ǵ<– mm7^4ο=_9JeA R;|^LqڌlX[ЋNiuC М|S-E~v>hh:,NJxxx0;<4 /hul܊rE3=͋PMJrp|r]Yv-1ߍ?\ Gobo{&A-!B0;hmQOxZQC Jt7B!D1|رk5IՉtLLCQ9\›v7ӖY T-MPiV?~52|^9?VR{0.fbyP*8+pEG>FI?Mc bm9JN@@ӦM#11!CLXi\cܖr5 BՅ;!>'|.^&Wu;_ϣ311F4[HTpgYP,@XZ-B!zԼ9^B_/*?N]wVA?S4'ĵBcjX}5Ejw;8T ;Ŷ 9?Vġ:sHkC/m64#+zӪaz?- y}~âz͊0^ MݱZwC( NԩS8qbGP }~ʒ2˛b!X(i3;ᇻ0k9' :0/%fxFH]~G=.B!- <\NwjIoؙz2JWB!RhbA 9$+Zw\fΓyJq{[/q62l |`C1. ߝۋɯ_ $NlS X 4;~p7%wX k*-Ek'qWh"8=[-Pja{B!D8c}%NHtM 1_dRjDx̻{mf<0=45ZslDKEH9nt4v~TB:Gw_MRԷ%ݦt8ۋKve|AǬ|FkRw%oO>Ɖ/)ϬqE݃ ~{n/ۓ͘3֗A#">ycߕڝ|B8ܓ5s6$, R l]4敷4>'a0'|N9k)>ϣ}`F<g5 c֋)~Oy3:ZL ,:XaǦR19 dl ! [5~X/@7. :!kżYڟG[jqSB[YQ.`;YrxO g2xg'9׻y !v̍⺾d#\t@+旟]"!(i0[00`^sྉ6xXm`4ףȀ)Y^&nMĭG)QD`RP5;+ǔ圽8_zgI5=|rhaD K ˱TPylT%c)ALTծTm$#&i6jʱ:LPPe{ٱ|G}&X=AȖY) PPyONÙ;%II6c(®<8dSW? 1(V7QQd:=ΉG S>Ks/&AY'+F9^CPt=u2k8gSr%!y5ezkhc=Xeau6<> nB!zǦV-އ"Pp^͕u󰯈;TL\^X4|{Q‹~O!oM7qyRs֡ ۨȱ2sMޗd`iuE< qⵌK8Ud8@0OKXOuHFhY"ғ# s8&wW 97t*S|fSn䦨2l1g@K $B]{69<Rc:|0y ᩬN f^W9~*Rn{ ʎLl.$\e}OYʆ7[B}b<ֹ0*R^(D<ɳ>sf)jhk:?8iωiy|sgfag -'7 סvv`to0VpḂvoWgWe`?>_Ψ׏p BCs e>֟fcھag1OAmx0uVBU ۙFfZz尢#]85ȟv^un?%pU/w55*ɨ{@έן{@PC*̓ӂhqFCԜ>? 7͍[8/{i4 v/]?6:eB(<}ccPZ*<;bz;a}Xgݓ1=W7_x9)8a<<{F]B"0Xq:6Nl$qf+77mr48urI$gM lY $$,EhA`xGϣs983~)oͥ5|枫5۴M ?ʒ'3o\Ģ˙T<;gҁovq`O-Iw~z 3C>\7iɺM qG߲UM@(}SlYs (Xr(<u#{຋*+`OW-W+w6CMt@o#vйt1YΑRSXܾg n?:& bpSle6aMܝ1vNqRK9 3=^{^㲘VXS.p=hO4'P30TհZ 0`[1[֗|/Y:tAv^s1|Ck:9:{&8ph-rYаu4"{0bDt. ro'ʶ|!3mg6cGևGtP;zhO 1,&Ze=gR@ɩӅ=0шde]_|mکɘɥG?RQW:i =>cZ<W%JJ^ Sf}H+ȇ]5l:@sM⧏k˂=Gm7y[OO^vͦ` e0/vԵA0 Kg-Wxu8]Zr%)sQjIgq!C oᲣ-+ U) MK=NC4GPa 93Ђ3rlV=rg7id& (/o v ^!4 *)yA[`.X$1@P>{L䱅þVtLJ.[nbc 7cO-Ib,́`__Ս90>HZO1jMY}-a`^$\B&hA^Eb+PsowQb{| ԛ<0u CT\֙#m @({ kqH/>M\GQ"{5wYp< C`e|&lO#BIw3Ҩ~2x2.l rh]^OKG'|o#m^f%IR(_#b\`)$w?B{I?އ(?meѧʢӮnlEo˃`o:IDAT?qF+C-IS)6~?VyC Ov$B" nͧeM#Z``7DvvєNI!(Jo6hz{/c"s48CCII2@"0!rqF>ȇb|Gf?ZMƽ)@7"Iθ??*$ WqEP'~ ܉sdlJ&0lC-Iȹo^Ȝ[8X: U\A_P>,J9Zn(L:MB'`fZZ ʐ!Żx%FiGOJ?dNzNpg]G/rƮ7[ ~96 6'Я: kklȠrku,_N$I҅%@.o}l%ow!gY B Ǧ$Ia7 ) ~aʂDz\Əh9\Qo. ah~viϡ!A@r6\}4 % ]C^Z:@:<<]ABy}鄞?ug6 &7u 7O] o>npi:+8$I$IF$]pBA2ө-C~4)ek&Mt7fQᠹ~;9Qxd zґ^nzmA0>4ENV0ݸy/~ p[Kr( j}{s oō1e `Zⱷn~y.jFK%Iű3Hm0mlV)5-{+y;`y#ΉM n'!]Z@>S[>;iվ^h$I$%)I'J~y& ;kq,xq:I,ʅoX)m(H|BO@ꆡSKi<.ٵ75{bN~{'+X/[[s]lnʇkZX />=m@bj 7ߑ_òp~M>܁xd> ;$_G:tŸ.',qEg<[qTf~}7E_VsPFAKkL/*  @+_κhH b*^ ])Y&F^^7 'Uӊy__kz5xz;&BVO/iXH!I$Iҙd%I(aWټ6A.Nn."{}=.V| 9 aз9 ) \o[4p^z[̂;p\:+>+5 K<%^Ui:v7^߻ :<:^TwqsU3z;Cfpwa֒TV^[k \?|)7t2 G넛#1]1  "hx#?O‡ԯupBtd8>= ]E"ϴZ(ͅwOJOa$I$Ň$I¯mTv(r'9`״QsK~6|lI;i.-fC)eŚr3>;} 9ㄻQ߼Nb;`|LTˢ+1 =h;FwqL=8t$9K(:Em+a{tOݻzF u4W}Q֑kd9aS/>q-|brٴvm$e 8_yI @MF5=mCAxLs&e=ti=wOȀm?Ȳo~K$IZ$)16)Yk;rʓ:#ZFSۋ2(3~©I US1/ 9$I$%W$I$I$IPK$I$I$I %I$I$I76%H,C-ID GJFE$Ix?>[nw.\2 $IirW %2sx֒sc.A&K$iӦMcɒ%.CjI2x RwR\$rŻ I$I: $I))//jJ_#eHq[n歋nw'Nw $I:jISV^^$5UHQR\Ly%I$.@$I$I$I:C-I$I$I$%<$I$IY˟Xl[G7ǪK(r$]H $I$ItFems]$I:jI$I$z衇]Β^>ǻIy9$I$I$UVU]$/$m<޲e K.CqJt2Ԓ$I߬՜"|nI%^j=@kY.t:?9mth-΂'í9kDaVB3<}d3ʆ4&=n?_z´g%pOɠgFKY?Gn"ܙI+:Y]̾,Sǁײip]ڰ:?Hwkؾ".'vw[i)p"#_]Owk;o2>nn}q7++&vૻXH8y3IYD>5֕Dg" 7;ǹ6;H/>=M5i-d05$̐$IS)I$]"a_dޒ ^:3SuVg-::m/H4&mW{pvhQBTš6 q D}*vxҙ{Kf@kY&:jxh9P|7VV/_,drU- =+xaPg.5洴X%f{'S>>RydEu-t9j:J[]ȞD";8\Q5f zQ&u ߷5;8ov"~5Q B^H$IN=$I$MY;m.z $AW߶_ft~M;s>{Ri 7 .o?om'R|n,=fIgif;y.k#M4}L }u*kG aA@3QmTaXc!g{ky-.r^~n;X 7~wI?˹k! >:t{:WO!_6 햇7lEK@KO;zE$ItJ%I$sᓥ Y$`u?J?_OC1ER:y7C It<$X8`k˓W=uTcLI³t&yG'Qtd2McdV >8q@ dy-]Ax6t |QZ"K`GJ&9|r2',@$It%I$g„jI˸"GY0!̬ hEGGH]otA$ 'iA,o/""nt <wLmh~g ʓC M Bf{}S7Y !?yD.4Wlrо (X6Tvܒd2fþvvK 3׋WMƼ\I$IZ$IRq-/VuVެ,N'PBR4s](ea3)@Uӕ^wfC}ظ=h!h> K 6 DJV!1==Z3IcNB6KI")6|Wp$It6jI$I4 >Y+^'!§եEܴ(js2t5Nb ]Sr.MzCIA?)?D/ G G{pGKDLF/f `m"#`_[8$I39$IQ*[}}MAbaDR';'zp39gB~* H§.#3) Kٷr;%b({b^9hI$I $IQ: (mP¸.VJI' +렡N0?ӻO6vcmY6ꁻ&Aʱk$l&g'_J7q߮ FxRr(jހumPƼ=_ԿtrɆNJ:+<ߙEq$F<ւ.h6zM^}LV1qZWorUd|d#w$RC@ |@Et 9k=~{C)&oc/M웚ݷҰ͕dbJ;|r-<] [y;uP>O=y$I$Ԓ$IFiԉ.ﭡ4faܲZ^Lav~6=<szVbAk?g8wv%I:[laҥ,Yx#I:AI$$`FZAy u9)s*aaAO՚ۋ2htXG:oncgi֨jM K303s H$I҅9$I$I$I $I$I$I~P$Il/AFjK$I$ $I$I!K}!I$I?(I$I$Ig%I$I$Ig%I$I$Ig%I$I$Ig%I$I$Ig%I$I$Ig%I$I$Ig%Bff&.C$I$I 4 Ż I$I$I.XZ(ASSSK$I$I$d%BAAqD$I$I 4 ӦM###5kĻI$I$I.HZ(h"***hkkw9$I$I$]pB.@:W,^{ nx#Is{jIK eT1÷$I3w(͚5 ORVVƴi]$Io_w - [Xt)K,||yˑ$I$~P: WE(Gw)$I$I$]P PTTă>Ⱦ}x]$I$I$I C-$]qw},[~8H$I$ItApN-u]/~={pS^$I$I$)ԒNٳ׿NNNK.k_+W7ޥI$I$Itޱty衇xgYbNqq13f̠B ;wnK$I$I$f%dnnVXʕ+ٱcs=Z$I$I$&C-i -^ŋyH}}}+$I$I$`%AyyyŻ I$I$Iyx I$I$I$$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I^(H$I:mٲ-[Ļ I$IyPK$Ii{ꩧ]$IJ yyy.At bX$I$I$IFZ$I$I$IJxZ$I$I$IJxZ$I$I$IJxZ$I$I$IJxZ$I$I$IJx*'@IENDB`libpyvinyl-1.2.0/tests/integration/plusminus/plusminus/000077500000000000000000000000001456037232700235075ustar00rootroot00000000000000libpyvinyl-1.2.0/tests/integration/plusminus/plusminus/ArrayCalculators/000077500000000000000000000000001456037232700267625ustar00rootroot00000000000000libpyvinyl-1.2.0/tests/integration/plusminus/plusminus/ArrayCalculators/ArrayCalculator.py000066400000000000000000000040761456037232700324330ustar00rootroot00000000000000from typing import Union from pathlib import Path import numpy as np from libpyvinyl.BaseData import DataCollection from libpyvinyl.BaseCalculator import BaseCalculator, CalculatorParameters from plusminus.NumberData import NumberData from plusminus.ArrayData import ArrayData class ArrayCalculator(BaseCalculator): def __init__( self, name: str, input: Union[DataCollection, list, NumberData], output_keys: Union[list, str] = ["array_result"], output_data_types=[ArrayData], output_filenames=[], instrument_base_dir="./", calculator_base_dir="ArrayCalculator", parameters: CalculatorParameters = None, ): """A python dict calculator to create an array from two inputs.""" super().__init__( name, input, output_keys, output_data_types=output_data_types, output_filenames=output_filenames, instrument_base_dir=instrument_base_dir, calculator_base_dir=calculator_base_dir, parameters=parameters, ) def init_parameters(self): parameters = CalculatorParameters() # Calculator developer edit multiply = parameters.new_parameter( "multiply", comment="Multiply the array by a value" ) multiply.value = 1 # Calculator developer end self.parameters = parameters def backengine(self): Path(self.base_dir).mkdir(parents=True, exist_ok=True) input_data0 = self.input.to_list()[0] assert type(input_data0) is NumberData input_num0 = input_data0.get_data()["number"] input_data1 = self.input.to_list()[1] assert type(input_data1) is NumberData input_num1 = input_data1.get_data()["number"] output_arr = ( np.array([input_num0, input_num1]) * self.parameters["multiply"].value ) data_dict = {"array": output_arr} key = self.output_keys[0] output_data = self.output[key] output_data.set_dict(data_dict) return self.output libpyvinyl-1.2.0/tests/integration/plusminus/plusminus/ArrayCalculators/__init__.py000066400000000000000000000000551456037232700310730ustar00rootroot00000000000000from .ArrayCalculator import ArrayCalculator libpyvinyl-1.2.0/tests/integration/plusminus/plusminus/ArrayData/000077500000000000000000000000001456037232700253575ustar00rootroot00000000000000libpyvinyl-1.2.0/tests/integration/plusminus/plusminus/ArrayData/ArrayData.py000066400000000000000000000016241456037232700276040ustar00rootroot00000000000000from libpyvinyl.BaseData import BaseData from plusminus.ArrayData import TXTFormat, H5Format class ArrayData(BaseData): def __init__( self, key, data_dict=None, filename=None, file_format_class=None, file_format_kwargs=None, ): ### DataClass developer's job start expected_data = {} expected_data["array"] = None ### DataClass developer's job end super().__init__( key, expected_data, data_dict, filename, file_format_class, file_format_kwargs, ) @classmethod def supported_formats(self): format_dict = {} ### DataClass developer's job start self._add_ioformat(format_dict, TXTFormat) self._add_ioformat(format_dict, H5Format) ### DataClass developer's job end return format_dict libpyvinyl-1.2.0/tests/integration/plusminus/plusminus/ArrayData/H5Format.py000066400000000000000000000030641456037232700273610ustar00rootroot00000000000000import h5py from libpyvinyl.BaseFormat import BaseFormat from plusminus.ArrayData import ArrayData class H5Format(BaseFormat): def __init__(self) -> None: super().__init__() @classmethod def format_register(self): key = "H5" desciption = "H5 format for ArrayData" file_extension = ".h5" read_kwargs = [""] write_kwargs = [""] return self._create_format_register( key, desciption, file_extension, read_kwargs, write_kwargs ) @staticmethod def direct_convert_formats(): # Assume the format can be converted directly to the formats supported by these classes: # AFormat, BFormat # Redefine this `direct_convert_formats` for a concrete format class return [] @classmethod def read(cls, filename: str) -> dict: """Read the data from the file with the `filename` to a dictionary. The dictionary will be used by its corresponding data class.""" with h5py.File(filename, "r") as h5: array = h5["array"][()] data_dict = {"array": array} return data_dict @classmethod def write(cls, object: ArrayData, filename: str, key: str = None): """Save the data with the `filename`.""" data_dict = object.get_data() array = data_dict["array"] with h5py.File(filename, "w") as h5: h5["array"] = array if key is None: original_key = object.key key = original_key + "_to_H5Format" return object.from_file(filename, cls, key) libpyvinyl-1.2.0/tests/integration/plusminus/plusminus/ArrayData/TXTFormat.py000066400000000000000000000027631456037232700275710ustar00rootroot00000000000000import numpy as np from libpyvinyl.BaseFormat import BaseFormat from plusminus.ArrayData import ArrayData class TXTFormat(BaseFormat): def __init__(self) -> None: super().__init__() @classmethod def format_register(self): key = "TXT" desciption = "TXT format for ArrayData" file_extension = ".txt" read_kwargs = [""] write_kwargs = [""] return self._create_format_register( key, desciption, file_extension, read_kwargs, write_kwargs ) @staticmethod def direct_convert_formats(): # Assume the format can be converted directly to the formats supported by these classes: # AFormat, BFormat # Redefine this `direct_convert_formats` for a concrete format class return [] @classmethod def read(cls, filename: str) -> dict: """Read the data from the file with the `filename` to a dictionary. The dictionary will be used by its corresponding data class.""" array = np.loadtxt(filename) data_dict = {"array": array} return data_dict @classmethod def write(cls, object: ArrayData, filename: str, key: str = None): """Save the data with the `filename`.""" data_dict = object.get_data() arr = data_dict["array"] np.savetxt(filename, arr, fmt="%.3f") if key is None: original_key = object.key key = original_key + "_to_TXTFormat" return object.from_file(filename, cls, key) libpyvinyl-1.2.0/tests/integration/plusminus/plusminus/ArrayData/__init__.py000066400000000000000000000001411456037232700274640ustar00rootroot00000000000000from .ArrayData import ArrayData from .H5Format import H5Format from .TXTFormat import TXTFormat libpyvinyl-1.2.0/tests/integration/plusminus/plusminus/BaseCalculator.py000066400000000000000000000175141456037232700267550ustar00rootroot00000000000000""" :module BaseCalculator: Module hosts the BaseData class.""" from abc import abstractmethod, ABCMeta from typing import Union from pathlib import Path from libpyvinyl.AbstractBaseClass import AbstractBaseClass from libpyvinyl.BaseData import BaseData, DataCollection from libpyvinyl.Parameters import CalculatorParameters class BaseCalculator(AbstractBaseClass): def __init__( self, name: str, input: Union[DataCollection, list, BaseData], output_keys: Union[list, str], output_data_types: list, output_filenames: Union[list, str], instrument_base_dir="./", calculator_base_dir="BaseCalculator", parameters: CalculatorParameters = None, ): """A python object calculator example""" # Initialize properties self.__name = None self.__instrument_base_dir = None self.__calculator_base_dir = None self.__input = None self.__input_keys = None self.__output_keys = None self.__output_data_types = None self.__output_filenames = None self.__parameters = None self.name = name self.input = input self.output_keys = output_keys self.output_data_types = output_data_types self.output_filenames = output_filenames self.instrument_base_dir = instrument_base_dir self.calculator_base_dir = calculator_base_dir self.parameters = parameters self.__init_output() @abstractmethod def init_parameters(self): raise NotImplementedError def __init_output(self): output = DataCollection() for i, key in enumerate(self.output_keys): output_data = self.output_data_types[i](key) output.add_data(output_data) self.output = output @property def name(self): return self.__name @name.setter def name(self, value): if isinstance(value, str): self.__name = value else: raise TypeError( f"Calculator: `name` is expected to be a str, not {type(value)}" ) @property def parameters(self): return self.__parameters @parameters.setter def parameters(self, value): if isinstance(value, CalculatorParameters): self.__parameters = value elif value is None: self.init_parameters() else: raise TypeError( f"Calculator: `parameters` is expected to be CalculatorParameters, not {type(value)}" ) @property def input(self): return self.__input @input.setter def input(self, value): self.set_input(value) def set_input(self, value: Union[DataCollection, list, BaseData]): if isinstance(value, DataCollection): self.__input = value elif isinstance(value, list): self.__input = DataCollection(*value) elif isinstance(value, BaseData): self.__input = DataCollection(value) else: raise TypeError( f"Calculator: `input` can be a DataCollection, list or BaseData object, and will be treated as a DataCollection, but not {type(value)}" ) @property def input_keys(self): return self.__input_keys @input_keys.setter def input_keys(self, value): self.set_input_keys(value) def set_input_keys(self, value: Union[list, str]): if isinstance(value, list): for item in value: assert type(item) is str self.__input_keys = value elif isinstance(value, str): self.__input_keys = [value] else: raise TypeError( f"Calculator: `input_keys` can be a list or a string, and will be treated as a list, but not {type(value)}" ) @property def output_keys(self): return self.__output_keys @output_keys.setter def output_keys(self, value): self.set_output_keys(value) def set_output_keys(self, value: Union[list, str]): if isinstance(value, list): for item in value: assert type(item) is str self.__output_keys = value elif isinstance(value, str): self.__output_keys = [value] else: raise TypeError( f"Calculator: `output_keys` can be a list or a string, and will be treated as a list, but not {type(value)}" ) @property def output_data_types(self): return self.__output_data_types @output_data_types.setter def output_data_types(self, value): self.set_output_data_types(value) def set_output_data_types(self, value): if isinstance(value, list): for item in value: assert type(item) is ABCMeta self.__output_data_types = value elif isinstance(value, ABCMeta): self.__output_data_types = [value] else: raise TypeError( f"Calculator: `output_data_types` can be a list or a DataClass, and will be treated as a list, but not {type(value)}" ) @property def output_filenames(self): """Native calculator file names""" return self.__output_filenames @output_filenames.setter def output_filenames(self, value): self.set_output_filenames(value) def set_output_filenames(self, value: Union[list, str]): if isinstance(value, str): self.__output_filenames = [value] elif isinstance(value, list): self.__output_filenames = value else: raise TypeError( f"Calculator: `output_filenames` can to be a str or a list, and will be treated as a list, but not {type(value)}" ) @property def instrument_base_dir(self): return self.__instrument_base_dir @instrument_base_dir.setter def instrument_base_dir(self, value): self.set_instrument_base_dir(value) def set_instrument_base_dir(self, value: str): if isinstance(value, str): self.__instrument_base_dir = value else: raise TypeError( f"Calculator: `instrument_base_dir` is expected to be a str, not {type(value)}" ) @property def calculator_base_dir(self): return self.__calculator_base_dir @calculator_base_dir.setter def calculator_base_dir(self, value): self.set_calculator_base_dir(value) def set_calculator_base_dir(self, value: str): if isinstance(value, str): self.__calculator_base_dir = value else: raise TypeError( f"Calculator: `calculator_base_dir` is expected to be a str, not {type(value)}" ) @property def base_dir(self): base_dir = Path(self.instrument_base_dir) / self.calculator_base_dir return str(base_dir) @property def output_file_paths(self): paths = [] for filename in self.output_filenames: path = Path(self.base_dir) / filename # Make sure the file directory exists path.parent.mkdir(parents=True, exist_ok=True) paths.append(str(path)) return paths @abstractmethod def backengine(self): Path(self.base_dir).mkdir(parents=True, exist_ok=True) input_num0 = self.input[self.input_keys[0]].get_data()["number"] input_num1 = self.input[self.input_keys[1]].get_data()["number"] output_num = float(input_num0) + float(input_num1) if self.parameters["plus_times"].value > 1: for i in range(self.parameters["plus_times"].value - 1): output_num += input_num1 data_dict = {"number": output_num} key = self.output_keys[0] output_data = NumberData.from_dict(data_dict, key) self.output = DataCollection(output_data) return self.output libpyvinyl-1.2.0/tests/integration/plusminus/plusminus/NumberCalculators/000077500000000000000000000000001456037232700271345ustar00rootroot00000000000000libpyvinyl-1.2.0/tests/integration/plusminus/plusminus/NumberCalculators/MinusCalculator.py000066400000000000000000000037671456037232700326300ustar00rootroot00000000000000from typing import Union from pathlib import Path import numpy as np from libpyvinyl.BaseData import DataCollection from plusminus.NumberData import NumberData, TXTFormat from libpyvinyl.BaseCalculator import BaseCalculator, CalculatorParameters class MinusCalculator(BaseCalculator): def __init__( self, name: str, input: Union[DataCollection, list, NumberData], output_keys: Union[list, str] = ["minus_result"], output_data_types=[NumberData], output_filenames: Union[list, str] = ["minus_result.txt"], instrument_base_dir="./", calculator_base_dir="MinusCalculator", parameters=None, ): """A python object calculator example""" super().__init__( name, input, output_keys, output_data_types=output_data_types, output_filenames=output_filenames, instrument_base_dir=instrument_base_dir, calculator_base_dir=calculator_base_dir, parameters=parameters, ) def init_parameters(self): parameters = CalculatorParameters() times = parameters.new_parameter( "minus_times", comment="How many times to do the minus" ) times.value = 1 self.parameters = parameters def backengine(self): Path(self.base_dir).mkdir(parents=True, exist_ok=True) input_num0 = self.input.to_list()[0].get_data()["number"] input_num1 = self.input.to_list()[1].get_data()["number"] output_num = float(input_num0) - float(input_num1) if self.parameters["minus_times"].value > 1: for i in range(self.parameters["minus_times"].value - 1): output_num -= input_num1 arr = np.array([output_num]) file_path = self.output_file_paths[0] np.savetxt(file_path, arr, fmt="%.3f") key = self.output_keys[0] output_data = self.output[key] output_data.set_file(file_path, TXTFormat) return self.output libpyvinyl-1.2.0/tests/integration/plusminus/plusminus/NumberCalculators/PlusCalculator.py000066400000000000000000000035361456037232700324520ustar00rootroot00000000000000from typing import Union from pathlib import Path from libpyvinyl.BaseData import DataCollection from plusminus.NumberData import NumberData from libpyvinyl.BaseCalculator import BaseCalculator, CalculatorParameters class PlusCalculator(BaseCalculator): def __init__( self, name: str, input: Union[DataCollection, list, NumberData], output_keys: Union[list, str] = ["plus_result"], output_data_types=[NumberData], output_filenames: Union[list, str] = [], instrument_base_dir="./", calculator_base_dir="PlusCalculator", parameters=None, ): """A python object calculator example""" super().__init__( name, input, output_keys, output_data_types=output_data_types, output_filenames=output_filenames, instrument_base_dir=instrument_base_dir, calculator_base_dir=calculator_base_dir, parameters=parameters, ) def init_parameters(self): parameters = CalculatorParameters() times = parameters.new_parameter( "plus_times", comment="How many times to do the plus" ) times.value = 1 self.parameters = parameters def backengine(self): Path(self.base_dir).mkdir(parents=True, exist_ok=True) input_num0 = self.input.to_list()[0].get_data()["number"] input_num1 = self.input.to_list()[1].get_data()["number"] output_num = float(input_num0) + float(input_num1) if self.parameters["plus_times"].value > 1: for i in range(self.parameters["plus_times"].value - 1): output_num += input_num1 data_dict = {"number": output_num} key = self.output_keys[0] output_data = self.output[key] output_data.set_dict(data_dict) return self.output libpyvinyl-1.2.0/tests/integration/plusminus/plusminus/NumberCalculators/__init__.py000066400000000000000000000001301456037232700312370ustar00rootroot00000000000000from .MinusCalculator import MinusCalculator from .PlusCalculator import PlusCalculator libpyvinyl-1.2.0/tests/integration/plusminus/plusminus/NumberData/000077500000000000000000000000001456037232700255315ustar00rootroot00000000000000libpyvinyl-1.2.0/tests/integration/plusminus/plusminus/NumberData/H5Format.py000066400000000000000000000031001456037232700275220ustar00rootroot00000000000000import h5py from libpyvinyl.BaseFormat import BaseFormat from plusminus.NumberData import NumberData class H5Format(BaseFormat): def __init__(self) -> None: super().__init__() @classmethod def format_register(self): key = "H5" desciption = "H5 format for NumberData" file_extension = ".h5" read_kwargs = [""] write_kwargs = [""] return self._create_format_register( key, desciption, file_extension, read_kwargs, write_kwargs ) @staticmethod def direct_convert_formats(): # Assume the format can be converted directly to the formats supported by these classes: # AFormat, BFormat # Redefine this `direct_convert_formats` for a concrete format class return [] @classmethod def read(cls, filename: str) -> dict: """Read the data from the file with the `filename` to a dictionary. The dictionary will be used by its corresponding data class.""" with h5py.File(filename, "r") as h5: number = h5["number"][()] data_dict = {"number": number} return data_dict @classmethod def write(cls, object: NumberData, filename: str, key: str = None): """Save the data with the `filename`.""" data_dict = object.get_data() number = data_dict["number"] with h5py.File(filename, "w") as h5: h5["number"] = number if key is None: original_key = object.key key = original_key + "_to_H5Format" return object.from_file(filename, cls, key) libpyvinyl-1.2.0/tests/integration/plusminus/plusminus/NumberData/NumberData.py000066400000000000000000000026061456037232700301310ustar00rootroot00000000000000from libpyvinyl.BaseData import BaseData from plusminus.NumberData import TXTFormat, H5Format class NumberData(BaseData): def __init__( self, key, data_dict=None, filename=None, file_format_class=None, file_format_kwargs=None, ): expected_data = {} ### DataClass developer's job start expected_data["number"] = None ### DataClass developer's job end super().__init__( key, expected_data, data_dict, filename, file_format_class, file_format_kwargs, ) @classmethod def supported_formats(self): format_dict = {} ### DataClass developer's job start self._add_ioformat(format_dict, TXTFormat.TXTFormat) self._add_ioformat(format_dict, H5Format.H5Format) ### DataClass developer's job end return format_dict @classmethod def from_file(cls, filename: str, format_class, key, **kwargs): """Create the data class by the file in the `format`.""" return cls( key, filename=filename, file_format_class=format_class, file_format_kwargs=kwargs, ) @classmethod def from_dict(cls, data_dict, key): """Create the data class by a python dictionary.""" return cls(key, data_dict=data_dict) libpyvinyl-1.2.0/tests/integration/plusminus/plusminus/NumberData/TXTFormat.py000066400000000000000000000030161456037232700277330ustar00rootroot00000000000000import numpy as np from libpyvinyl.BaseFormat import BaseFormat from plusminus.NumberData import NumberData class TXTFormat(BaseFormat): def __init__(self) -> None: super().__init__() @classmethod def format_register(self): key = "TXT" desciption = "TXT format for NumberData" file_extension = ".txt" read_kwargs = [""] write_kwargs = [""] return self._create_format_register( key, desciption, file_extension, read_kwargs, write_kwargs ) @staticmethod def direct_convert_formats(): # Assume the format can be converted directly to the formats supported by these classes: # AFormat, BFormat # Redefine this `direct_convert_formats` for a concrete format class return [] @classmethod def read(cls, filename: str) -> dict: """Read the data from the file with the `filename` to a dictionary. The dictionary will be used by its corresponding data class.""" number = float(np.loadtxt(filename)) data_dict = {"number": number} return data_dict @classmethod def write(cls, object: NumberData, filename: str, key: str = None): """Save the data with the `filename`.""" data_dict = object.get_data() arr = np.array([data_dict["number"]]) np.savetxt(filename, arr, fmt="%.3f") if key is None: original_key = object.key key = original_key + "_to_TXTFormat" return object.from_file(filename, cls, key) libpyvinyl-1.2.0/tests/integration/plusminus/plusminus/NumberData/__init__.py000066400000000000000000000001431456037232700276400ustar00rootroot00000000000000from .H5Format import H5Format from .NumberData import NumberData from .TXTFormat import TXTFormat libpyvinyl-1.2.0/tests/integration/plusminus/plusminus/__init__.py000066400000000000000000000002561456037232700256230ustar00rootroot00000000000000"""Top-level package for PlusMinus.""" __author__ = """Juncheng E""" __email__ = "juncheng.e@xfel.eu" __version__ = "0.1.0" from libpyvinyl.BaseData import DataCollection libpyvinyl-1.2.0/tests/integration/plusminus/plusminus/plusminus.py000066400000000000000000000000231456037232700261130ustar00rootroot00000000000000"""Main module.""" libpyvinyl-1.2.0/tests/integration/plusminus/requirements.txt000066400000000000000000000000001456037232700247220ustar00rootroot00000000000000libpyvinyl-1.2.0/tests/integration/plusminus/requirements_dev.txt000066400000000000000000000002511456037232700255700ustar00rootroot00000000000000pip pytest pytest-runner bump2version==0.5.11 wheel==0.33.6 watchdog==0.9.0 flake8==3.7.8 tox==3.14.0 coverage==4.5.4 Sphinx==3.5.2 twine==1.14.0 sphinx_rtd_theme==0.5.1libpyvinyl-1.2.0/tests/integration/plusminus/setup.cfg000066400000000000000000000006271456037232700232760ustar00rootroot00000000000000[bumpversion] current_version = 0.1.0 commit = True tag = True [bumpversion:file:setup.py] search = version='{current_version}' replace = version='{new_version}' [bumpversion:file:plusminus/__init__.py] search = __version__ = '{current_version}' replace = __version__ = '{new_version}' [bdist_wheel] universal = 1 [flake8] exclude = docs [aliases] # Define setup.py command aliases here test = pytest libpyvinyl-1.2.0/tests/integration/plusminus/setup.py000066400000000000000000000027031456037232700231640ustar00rootroot00000000000000#!/usr/bin/env python """The setup script.""" from setuptools import setup, find_packages with open("README.rst") as readme_file: readme = readme_file.read() with open("HISTORY.rst") as history_file: history = history_file.read() with open("requirements.txt") as requirements_file: require = requirements_file.read() requirements = require.split() setup_requirements = [ "pytest-runner", ] test_requirements = [ "pytest>=3", ] setup( author="Juncheng E", author_email="juncheng.e@xfel.eu", python_requires=">=3.6", classifiers=[ "Development Status :: 2 - Pre-Alpha", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Natural Language :: English", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", ], description="An example of a small platform implementing libpynyl", install_requires=requirements, license="MIT license", long_description=readme + "\n\n" + history, include_package_data=True, keywords="PlusMinus", name="PlusMinus", packages=find_packages(include=["plusminus", "plusminus.*"]), setup_requires=setup_requirements, test_suite="tests", tests_require=test_requirements, url="https://github.com/JunCEEE/PlusMinus", version="0.1.0", zip_safe=False, ) libpyvinyl-1.2.0/tests/integration/plusminus/tests/000077500000000000000000000000001456037232700226125ustar00rootroot00000000000000libpyvinyl-1.2.0/tests/integration/plusminus/tests/__init__.py000066400000000000000000000000471456037232700247240ustar00rootroot00000000000000"""Unit test package for plusminus.""" libpyvinyl-1.2.0/tests/integration/plusminus/tests/test_ArrayCalculators.py000066400000000000000000000022171456037232700275000ustar00rootroot00000000000000#!/usr/bin/env python """Tests for `plusminus.NumberCalculators` package.""" import pytest from plusminus.ArrayCalculators import ArrayCalculator from plusminus.NumberData import NumberData from plusminus.ArrayData import TXTFormat from plusminus import DataCollection def test_ArrayCalculator(tmpdir): """PlusCalculator test function, the native output of ArrayCalculator is a python dictionary""" input1 = NumberData.from_dict({"number": 1}, "input1") input2 = NumberData.from_dict({"number": 2}, "input2") input_data = [input1, input2] # This could also be allowed. input_data = DataCollection(input1, input2) calculator = ArrayCalculator("plus", input_data) calculator.set_instrument_base_dir(str(tmpdir)) output = calculator.backengine() assert output.get_data()["array"][0] == 1 assert output.get_data()["array"][1] == 2 calculator.parameters["multiply"] = 5 output = calculator.backengine() file_output = output.write( calculator.base_dir + "/array_5.txt", TXTFormat, key="file_output" ) assert file_output.get_data()["array"][0] == 5 assert file_output.get_data()["array"][1] == 10 libpyvinyl-1.2.0/tests/integration/plusminus/tests/test_Instrument.py000066400000000000000000000036331456037232700264000ustar00rootroot00000000000000import pytest from libpyvinyl.Instrument import Instrument from plusminus.ArrayCalculators import ArrayCalculator from plusminus.NumberCalculators import PlusCalculator, MinusCalculator from plusminus.NumberData import NumberData import plusminus.ArrayData as AD from plusminus import DataCollection def test_Instrument_base_dir(tmpdir): my_instr = Instrument("my_instr") my_instr.set_instrument_base_dir(str(tmpdir)) print(my_instr.instrument_base_dir) my_instr.instrument_base_dir = "./test" print(my_instr.instrument_base_dir) def test_CalculationInstrument(tmpdir): """PlusCalculator test function, the native output of MinusCalculator is a python dictionary""" input1 = NumberData.from_dict({"number": 1}, "input1") input2 = NumberData.from_dict({"number": 2}, "input2") input_collection = [input1, input2] # This could also be allowed. input_collection = DataCollection(input1, input2) calculator1 = PlusCalculator("plus", input_collection, output_keys=["plus_result"]) calculator2 = MinusCalculator( "minus", input_collection, output_keys=["minus_result"] ) input_collection = DataCollection( calculator1.output["plus_result"], calculator2.output["minus_result"] ) calculator3 = ArrayCalculator( "array", input_collection, output_keys=["array_result"] ) calculation_instrument = Instrument("calculation_instrument") instrument_path = tmpdir / "calculation_instrument" calculation_instrument.add_calculator(calculator1) calculation_instrument.add_calculator(calculator2) calculation_instrument.add_calculator(calculator3) calculation_instrument.set_instrument_base_dir(str(instrument_path)) calculation_instrument.run() print(calculator3.output.get_data()) calculator3.output.write(str(tmpdir / "final_result.txt"), AD.TXTFormat) calculator3.output.write(str(tmpdir / "final_result.h5"), AD.H5Format) libpyvinyl-1.2.0/tests/integration/plusminus/tests/test_NumberCalculators.py000066400000000000000000000042041456037232700276500ustar00rootroot00000000000000#!/usr/bin/env python """Tests for `plusminus.NumberCalculators` package.""" import pytest from plusminus.NumberCalculators import PlusCalculator, MinusCalculator from plusminus.NumberData import NumberData, TXTFormat from plusminus import DataCollection def test_PlusCalculator(tmpdir): """PlusCalculator test function, the native output of PlusCalculator is a python dictionary""" input1 = NumberData.from_dict({"number": 1}, "input1") input2 = NumberData.from_dict({"number": 1}, "input2") input_data = [input1, input2] # This could also be allowed. input_data = DataCollection(input1, input2) plus = PlusCalculator("plus", input_data) plus.set_instrument_base_dir(str(tmpdir)) plus_output = plus.backengine() assert plus_output.get_data()["number"] == 2 plus_output.write(plus.base_dir + "/1_time.txt", TXTFormat) plus.parameters["plus_times"] = 5 plus_output = plus.backengine() file_output = plus_output.write( plus.base_dir + "/5_time.txt", TXTFormat, key="file_output" ) assert file_output.get_data()["number"] == 6 def test_MinusCalculator(tmpdir): """MinusCalculator test function. The native output of MinusCalculator is a txt file""" input1 = NumberData.from_dict({"number": 1}, "input1") input2 = NumberData.from_dict({"number": 1}, "input2") input_data = DataCollection(input1, input2) calculator = MinusCalculator("minus", input_data) calculator.set_instrument_base_dir(str(tmpdir)) assert "MinusCalculator" in calculator.base_dir calculator.set_output_filenames("minus_res.txt") output = calculator.backengine() assert output.get_data()["number"] == 0 calculator.parameters["minus_times"] = 5 plus_output = calculator.backengine() assert plus_output.get_data()["number"] == -4 def test_DataCollection_multiple(): """PlusCalculator test function""" input1 = NumberData.from_dict({"number": 1}, "input1") input2 = NumberData.from_dict({"number": 1}, "input2") input_data = DataCollection(input1, input2) data = input_data.get_data() assert data["input1"]["number"] == 1 assert data["input2"]["number"] == 1 libpyvinyl-1.2.0/tests/integration/plusminus/tests/test_NumberData.py000066400000000000000000000071111456037232700262450ustar00rootroot00000000000000#!/usr/bin/env python """Tests for `plusminus.NumberCalculators` package.""" import pytest import h5py from plusminus.NumberData import NumberData, TXTFormat, H5Format def test_construct_NumberData(): """Test the construction of NumberData""" my_data = NumberData.from_dict({"number": 1}, "input1") def test_list_formats(): """Test the construction of NumberData""" my_data = NumberData.from_dict({"number": 1}, "input1") my_data.list_formats() def test_write_read_txt(tmpdir): """Test writing to a txt file""" my_data = NumberData.from_dict({"number": 1}, "input1") file_name = str(tmpdir / "test.txt") my_data.write(file_name, TXTFormat) with open(file_name, "r") as f: assert float(f.read()) == 1 read_data = NumberData.from_file(file_name, TXTFormat, "read_data") assert read_data.get_data()["number"] == 1 def test_write_read_h5(tmpdir): """Test writing to a h5 file""" my_data = NumberData.from_dict({"number": 1}, "input1") file_name = str(tmpdir / "test.h5") my_data.write(file_name, H5Format) with h5py.File(file_name, "r") as h5: assert h5["number"][()] == 1 read_data = NumberData.from_file(file_name, H5Format, "read_data") assert read_data.get_data()["number"] == 1 def test_read_txt_write_h5(tmpdir): """Test read a txt file and write to a h5 file""" my_data = NumberData.from_dict({"number": 1}, "input1") file_name = str(tmpdir / "test.txt") my_data.write(file_name, TXTFormat) read_data = NumberData.from_file(file_name, TXTFormat, "read_data") file_name = str(tmpdir / "test.h5") read_data.write(file_name, H5Format) with h5py.File(file_name, "r") as h5: assert h5["number"][()] == 1 def test_txt_file_write_h5(tmpdir): """Test write a txt file and write to a h5 file""" my_data = NumberData.from_dict({"number": 1}, "input1") file_name = str(tmpdir / "test.txt") read_data = my_data.write(file_name, TXTFormat, "read_data") file_name = str(tmpdir / "test.h5") read_data.write(file_name, H5Format) with h5py.File(file_name, "r") as h5: assert h5["number"][()] == 1 def test_set_dict(tmpdir): """Test setting a dict mapping""" my_data = NumberData("input1") my_data.set_dict({"number": 1}) file_name = str(tmpdir / "test.txt") my_data.write(file_name, TXTFormat) def test_set_file_(tmpdir): """Test setting a file mapping""" my_data = NumberData.from_dict({"number": 1}, "input1") file_name = str(tmpdir / "test.txt") my_data.write(file_name, TXTFormat) new_data = NumberData("new_data") new_data.set_file(file_name, TXTFormat) assert new_data.get_data()["number"] == 1 def test_set_file_report_double_setting(tmpdir): """Test write a txt file and write to a h5 file""" my_data = NumberData.from_dict({"number": 1}, "input1") file_name = str(tmpdir / "test.txt") my_data.write(file_name, TXTFormat) with pytest.raises(RuntimeError): my_data.set_file(file_name, TXTFormat) def test_return_object_without_key(tmpdir): """Test write a txt file and write to a h5 file""" my_data = NumberData.from_dict({"number": 1}, "input1") file_name = str(tmpdir / "test.txt") new_data = my_data.write(file_name, TXTFormat) assert new_data.key == "input1_to_TXTFormat" def test_return_object_with_key(tmpdir): """Test write a txt file and write to a h5 file""" my_data = NumberData.from_dict({"number": 1}, "input1") file_name = str(tmpdir / "test.txt") key = "test" new_data = my_data.write(file_name, TXTFormat, key) assert new_data.key == key libpyvinyl-1.2.0/tests/integration/plusminus/tox.ini000066400000000000000000000010341456037232700227610ustar00rootroot00000000000000[tox] envlist = py36, py37, py38, flake8 [travis] python = 3.8: py38 3.7: py37 3.6: py36 [testenv:flake8] basepython = python deps = flake8 commands = flake8 plusminus tests [testenv] setenv = PYTHONPATH = {toxinidir} deps = -r{toxinidir}/requirements_dev.txt ; If you want to make tox run the tests with the same versions, create a ; requirements.txt with the pinned versions and uncomment the following line: ; -r{toxinidir}/requirements.txt commands = pip install -U pip pytest --basetemp={envtmpdir} libpyvinyl-1.2.0/tests/unit/000077500000000000000000000000001456037232700160455ustar00rootroot00000000000000libpyvinyl-1.2.0/tests/unit/Test.py000066400000000000000000000051031456037232700173350ustar00rootroot00000000000000#! /usr/bin/env python3 """ :module Test: Top level test module hosting all unittest suites. """ #################################################################################### # # # This file is part of libpyvinyl - The APIs for Virtual Neutron and x-raY # # Laboratory. # # # # Copyright (C) 2020 Carsten Fortmann-Grote # # # # This program is free software: you can redistribute it and/or modify it under # # the terms of the GNU Lesser General Public License as published by the Free # # Software Foundation, either version 3 of the License, or (at your option) any # # later version. # # # # This program is distributed in the hope that it will be useful, but WITHOUT ANY # # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A # # PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. # # # # You should have received a copy of the GNU Lesser General Public License along # # with this program. If not, see OK <---") sys.exit(0) sys.exit(1) libpyvinyl-1.2.0/tests/unit/test_BaseCalculator.py000066400000000000000000000260471456037232700223530ustar00rootroot00000000000000import unittest import pytest import os import shutil from typing import Union from pathlib import Path from libpyvinyl.BaseCalculator import BaseCalculator from libpyvinyl.BaseData import BaseData, DataCollection from libpyvinyl.Parameters import CalculatorParameters from libpyvinyl.AbstractBaseClass import AbstractBaseClass class NumberData(BaseData): """Example dict mapping data""" def __init__( self, key, data_dict=None, filename=None, file_format_class=None, file_format_kwargs=None, ): expected_data = {} # DataClass developer's job start expected_data["number"] = None # DataClass developer's job end super().__init__( key, expected_data, data_dict, filename, file_format_class, file_format_kwargs, ) @classmethod def supported_formats(self): return {} @classmethod def from_file(cls, filename: str, format_class, key, **kwargs): raise NotImplementedError() @classmethod def from_dict(cls, data_dict, key): """Create the data class by a python dictionary.""" return cls(key, data_dict=data_dict) class PlusCalculator(BaseCalculator): """:class: Specialized calculator, calculates the sum of two datasets.""" def __init__( self, name: str, input: Union[DataCollection, list, NumberData], output_keys: Union[list, str] = ["plus_result"], output_data_types=[NumberData], output_filenames: Union[list, str] = [], instrument_base_dir="./", calculator_base_dir="PlusCalculator", parameters=None, ): """A python object calculator example""" super().__init__( name, input, output_keys, output_data_types=output_data_types, output_filenames=output_filenames, instrument_base_dir=instrument_base_dir, calculator_base_dir=calculator_base_dir, parameters=parameters, ) def init_parameters(self): parameters = CalculatorParameters() times = parameters.new_parameter( "plus_times", comment="How many times to do the plus" ) # Set defaults times.value = 1 self.parameters = parameters def backengine(self): Path(self.base_dir).mkdir(parents=True, exist_ok=True) input_num0 = self.input.to_list()[0].get_data()["number"] input_num1 = self.input.to_list()[1].get_data()["number"] output_num = float(input_num0) + float(input_num1) if self.parameters["plus_times"].value > 1: for i in range(self.parameters["plus_times"].value - 1): output_num += input_num1 data_dict = {"number": output_num} key = self.output_keys[0] output_data = self.output[key] output_data.set_dict(data_dict) return self.output class BaseCalculatorTest(unittest.TestCase): """ Test class for the BaseCalculator class. """ @classmethod def setUpClass(cls): """Setting up the test class.""" input1 = NumberData.from_dict({"number": 1}, "input1") input2 = NumberData.from_dict({"number": 1}, "input2") input_data = [input1, input2] plus = PlusCalculator("plus", input_data) cls.__default_calculator = plus cls.__default_input = input_data @classmethod def tearDownClass(cls): """Tearing down the test class.""" del cls.__default_calculator del cls.__default_input def setUp(self): """Setting up a test.""" self.__files_to_remove = [] self.__dirs_to_remove = [] def tearDown(self): """Tearing down a test.""" for f in self.__files_to_remove: if os.path.isfile(f): os.remove(f) for d in self.__dirs_to_remove: if os.path.isdir(d): shutil.rmtree(d) def test_base_class_constructor_raises(self): """Test that we cannot construct instances of the base class.""" self.assertRaises(TypeError, BaseCalculator, "name") def test_default_construction(self): """Testing the default construction of the class.""" # Test positional arguments calculator = PlusCalculator("test", self.__default_input) self.assertIsInstance(calculator, PlusCalculator) self.assertIsInstance(calculator, BaseCalculator) self.assertIsInstance(calculator, AbstractBaseClass) def test_deep_copy(self): """Test the copy constructor behaves as expected.""" # Parameters are not deepcopied by itself calculator_copy = self.__default_calculator() self.assertEqual(calculator_copy.parameters["plus_times"].value, 1) new_parameters = calculator_copy.parameters new_parameters["plus_times"] = 5 self.assertEqual(new_parameters["plus_times"].value, 5) self.assertEqual(calculator_copy.parameters["plus_times"].value, 5) # Parameters are deepcopied when copy the calculator calculator_copy = self.__default_calculator() self.assertEqual(calculator_copy.parameters["plus_times"].value, 1) calculator_copy.parameters["plus_times"] = 10 self.assertEqual(calculator_copy.parameters["plus_times"].value, 10) self.assertEqual(self.__default_calculator.parameters["plus_times"].value, 1) calculator_copy.input["input1"] = NumberData.from_dict({"number": 5}, "input1") self.assertEqual(calculator_copy.input["input1"].get_data()["number"], 5) self.assertEqual( self.__default_calculator.input["input1"].get_data()["number"], 1 ) # Calculator reference self.assertEqual(calculator_copy.parameters["plus_times"].value, 10) calculator_reference = calculator_copy self.assertEqual(calculator_reference.parameters["plus_times"].value, 10) calculator_reference.parameters["plus_times"] = 3 self.assertEqual(calculator_reference.parameters["plus_times"].value, 3) self.assertEqual(calculator_copy.parameters["plus_times"].value, 3) # New parameters can be set while caculator deepcopy new_parameters = CalculatorParameters() times = new_parameters.new_parameter( "plus_times", comment="How many times to do the plus" ) times.value = 1 new_parameters["plus_times"].value = 5 new_calculator = self.__default_calculator(parameters=new_parameters) self.assertIsInstance(new_calculator, PlusCalculator) self.assertIsInstance(new_calculator, BaseCalculator) self.assertIsInstance(new_calculator, AbstractBaseClass) self.assertEqual(new_calculator.parameters["plus_times"].value, 5) self.assertEqual(self.__default_calculator.parameters["plus_times"].value, 1) def test_dump(self): """Test dumping to file.""" calculator = self.__default_calculator self.__files_to_remove.append(calculator.dump()) self.__files_to_remove.append(calculator.dump("dump.dill")) def test_parameters_in_copied_calculator(self): """Test parameters in a copied calculator""" calculator = self.__default_calculator self.assertEqual(calculator.parameters["plus_times"].value, 1) calculator.parameters["plus_times"] = 5 self.assertEqual(self.__default_calculator.parameters["plus_times"].value, 5) calculator.parameters["plus_times"] = 1 self.assertEqual(self.__default_calculator.parameters["plus_times"].value, 1) def test_resurrect_from_dump(self): """Test loading from dumpfile.""" calculator = self.__default_calculator() self.assertEqual(calculator.parameters["plus_times"].value, 1) output = calculator.backengine() self.assertEqual(output.get_data()["number"], 2) self.__dirs_to_remove.append("PlusCalculator") # dump dump = calculator.dump() self.__files_to_remove.append(dump) del calculator calculator = PlusCalculator.from_dump(dump) self.assertEqual( calculator.input.get_data(), self.__default_calculator.input.get_data(), ) calculator.parameters.to_dict() self.assertEqual( calculator.parameters.to_dict(), self.__default_calculator.parameters.to_dict(), ) calculator.parameters["plus_times"] = 5 self.assertNotEqual( calculator.parameters.to_dict(), self.__default_calculator.parameters.to_dict(), ) self.assertIsNotNone(calculator.data) def test_attributes(self): """Test that all required attributes are present.""" calculator = self.__default_calculator self.assertTrue(hasattr(calculator, "name")) self.assertTrue(hasattr(calculator, "input")) self.assertTrue(hasattr(calculator, "output")) self.assertTrue(hasattr(calculator, "parameters")) self.assertTrue(hasattr(calculator, "instrument_base_dir")) self.assertTrue(hasattr(calculator, "calculator_base_dir")) self.assertTrue(hasattr(calculator, "base_dir")) self.assertTrue(hasattr(calculator, "backengine")) self.assertTrue(hasattr(calculator, "data")) self.assertTrue(hasattr(calculator, "dump")) self.assertTrue(hasattr(calculator, "from_dump")) def test_set_param_values(self): calculator = self.__default_calculator calculator.parameters["plus_times"] = 5 self.assertEqual(calculator.parameters["plus_times"].value, 5) def test_set_param_values_with_set_parameters(self): calculator = self.__default_calculator calculator.set_parameters(plus_times=7) self.assertEqual(calculator.parameters["plus_times"].value, 7) def test_set_param_values_with_set_parameters_with_dict(self): calculator = self.__default_calculator calculator.set_parameters({"plus_times": 9}) self.assertEqual(calculator.parameters["plus_times"].value, 9) def test_collection_get_data(self): calculator = self.__default_calculator print(calculator.input) input_dict = calculator.input.get_data() self.assertEqual(input_dict["input1"]["number"], 1) self.assertEqual(input_dict["input2"]["number"], 1) def test_output_file_paths(self): calculator = self.__default_calculator with self.assertRaises(ValueError) as exception: calculator.output_file_paths calculator.output_filenames = "bingo.txt" self.assertEqual(calculator.output_file_paths[0], "PlusCalculator/bingo.txt") self.__dirs_to_remove.append("PlusCalculator") def test_calculator_output_set_inconsistent(self): input1 = NumberData.from_dict({"number": 1}, "input1") with self.assertRaises(ValueError) as exception: calculator = PlusCalculator( "test", input1, output_keys=["result"], output_data_types=[] ) if __name__ == "__main__": unittest.main() libpyvinyl-1.2.0/tests/unit/test_BaseData.py000066400000000000000000000356441456037232700211360ustar00rootroot00000000000000import pytest import numpy as np import h5py from libpyvinyl.BaseData import BaseData, DataCollection from libpyvinyl.BaseFormat import BaseFormat class NumberData(BaseData): def __init__( self, key, data_dict=None, filename=None, file_format_class=None, file_format_kwargs=None, ): ### DataClass developer's job start expected_data = {} expected_data["number"] = None ### DataClass developer's job end super().__init__( key, expected_data, data_dict, filename, file_format_class, file_format_kwargs, ) @classmethod def supported_formats(self): format_dict = {} ### DataClass developer's job start self._add_ioformat(format_dict, TXTFormat) self._add_ioformat(format_dict, H5Format) ### DataClass developer's job end return format_dict class TXTFormat(BaseFormat): def __init__(self) -> None: super().__init__() @classmethod def format_register(self): key = "TXT" desciption = "TXT format for NumberData" file_extension = ".txt" read_kwargs = [""] write_kwargs = [""] return self._create_format_register( key, desciption, file_extension, read_kwargs, write_kwargs ) @classmethod def read(cls, filename: str) -> dict: """Read the data from the file with the `filename` to a dictionary. The dictionary will be used by its corresponding data class.""" number = float(np.loadtxt(filename)) data_dict = {"number": number} return data_dict @classmethod def write(cls, object: NumberData, filename: str, key: str = None): """Save the data with the `filename`.""" data_dict = object.get_data() arr = np.array([data_dict["number"]]) np.savetxt(filename, arr, fmt="%.3f") if key is None: original_key = object.key key = original_key + "_to_TXTFormat" return object.from_file(filename, cls, key) else: return object.from_file(filename, cls, key) @staticmethod def direct_convert_formats(): # Assume the format can be converted directly to the formats supported by these classes: # AFormat, BFormat # Redefine this `direct_convert_formats` for a concrete format class return [H5Format] @classmethod def convert( cls, obj: NumberData, output: str, output_format_class: str, key=None, **kwargs ): """Direct convert method, if the default converting would be too slow or not suitable for the output_format""" if output_format_class is H5Format: cls.convert_to_H5Format(obj.filename, output) else: raise TypeError( "Direct converting to format {} is not supported".format( output_format_class ) ) # Set the key of the returned object if key is None: original_key = obj.key key = original_key + "_from_TXTFormat" return obj.from_file(output, output_format_class, key) else: return obj.from_file(output, output_format_class, key) @classmethod def convert_to_H5Format(cls, input: str, output: str): """The engine of convert method.""" print("Directly converting TXTFormat to H5Format") number = float(np.loadtxt(input)) with h5py.File(output, "w") as h5: h5["number"] = number class H5Format(BaseFormat): def __init__(self) -> None: super().__init__() @classmethod def format_register(self): key = "H5" desciption = "H5 format for NumberData" file_extension = ".h5" read_kwargs = [""] write_kwargs = [""] return self._create_format_register( key, desciption, file_extension, read_kwargs, write_kwargs ) @classmethod def read(cls, filename: str) -> dict: """Read the data from the file with the `filename` to a dictionary. The dictionary will be used by its corresponding data class.""" with h5py.File(filename, "r") as h5: number = h5["number"][()] data_dict = {"number": number} return data_dict @classmethod def write(cls, object: NumberData, filename: str, key: str = None): """Save the data with the `filename`.""" data_dict = object.get_data() number = data_dict["number"] with h5py.File(filename, "w") as h5: h5["number"] = number if key is None: original_key = object.key key = original_key + "_to_H5Format" return object.from_file(filename, cls, key) else: return object.from_file(filename, cls, key) @staticmethod def direct_convert_formats(): # Assume the format can be converted directly to the formats supported by these classes: # AFormat, BFormat # Redefine this `direct_convert_formats` for a concrete format class return [] @pytest.fixture() def txt_file(tmp_path_factory): fn_path = tmp_path_factory.mktemp("test_data") / "test.txt" txt_file = str(fn_path) with open(txt_file, "w") as f: f.write("4") return txt_file # Data class section def test_list_formats(capsys): """Test listing registered format classes""" NumberData.list_formats() captured = capsys.readouterr() assert "Key: TXT" in captured.out assert "Key: H5" in captured.out def test_create_empty_data_instance(): """Test creating an empty data instance""" with pytest.raises(TypeError): number_data = NumberData() test_data = NumberData(key="test_data") assert isinstance(test_data, NumberData) def test_create_data_with_set_dict(): """Test set dict after in an empty data instance""" test_data = NumberData(key="test_data") my_dict = {"number": 4} test_data.set_dict(my_dict) assert test_data.get_data()["number"] == 4 def test_create_data_with_set_file(txt_file): """Test set file after in an empty data instance""" test_data = NumberData(key="test_data") test_data.set_file(txt_file, TXTFormat) assert test_data.get_data()["number"] == 4 def test_create_data_with_set_file_inconsistensy(txt_file): """Test set dict and file for one data object: expecting an error""" test_data = NumberData(key="test_data") my_dict = {"number": 4} test_data.set_dict(my_dict) with pytest.raises(RuntimeError): test_data.set_file(txt_file, TXTFormat) def test_create_data_with_set_file_wrong_param(txt_file): """Test set file after in an empty data instance with wrong `format_class` param""" test_data = NumberData(key="test_data") with pytest.raises(TypeError): test_data.set_file(txt_file, "txt") def test_create_data_with_set_file_wrong_format(txt_file): """Test set file after in an empty data instance with wrong `format_class`""" test_data = NumberData(key="test_data") test_data.set_file(txt_file, H5Format) with pytest.raises(OSError): test_data.get_data() def test_create_data_with_file(): """Test set dict after in an empty data instance""" test_data = NumberData(key="test_data") assert isinstance(test_data, NumberData) my_dict = {"number": 4} test_data.set_dict(my_dict) assert test_data.get_data()["number"] == 4 def test_create_data_from_dict(): """Test creating a data instance from a dict""" my_dict = {"number": 4} test_data = NumberData.from_dict(my_dict, "test_data") def test_check_key_from_dict(): """Test checking expected data key from dict""" my_dict = {"number": 4} test_data = NumberData.from_dict(my_dict, "test_data") test_data.get_data() my_dict = {"numberr": 4} test_data = NumberData.from_dict(my_dict, "test_data") with pytest.raises(KeyError): test_data.get_data() def test_create_data_from_file_wrong_param(txt_file): """Test creating a data instance from a file in a wrong file format type""" with pytest.raises(TypeError): test_data = NumberData.from_file(txt_file, "txt", "test_data") def test_create_data_from_TXTFormat(txt_file): """Test creating a data instance from a file in TXTFormat""" test_data = NumberData.from_file(txt_file, TXTFormat, "test_data") assert test_data.get_data()["number"] == 4 def test_create_data_from_wrong_format(txt_file): """Test creating a data instance from a file in TXTFormat""" test_data = NumberData.from_file(txt_file, H5Format, "test_data") with pytest.raises(OSError): test_data.get_data() def test_duplicate_data_TXTFormat(txt_file, tmpdir, capsys): """Test creating a data instance from a file in TXTFormat""" test_data = NumberData.from_file(txt_file, TXTFormat, "test_data") test_data.write(str(tmpdir / "new_data.txt"), TXTFormat) captured = capsys.readouterr() assert "data already existed" in captured.out def test_save_dict_data_in_TXTFormat(tmpdir): """Test saving a dict data in TXTFormat""" my_dict = {"number": 4} test_data = NumberData.from_dict(my_dict, "test_data") fn = str(tmpdir / "test.txt") test_data.write(fn, TXTFormat) read_data = NumberData.from_file(fn, TXTFormat, "read_data") assert read_data.get_data()["number"] == 4 def test_save_dict_data_in_TXTFormat_return_data_object(tmpdir): """Test saving a dict data in TXTFormat returning data object with default key""" my_dict = {"number": 4} test_data = NumberData.from_dict(my_dict, "test_data") fn = str(tmpdir / "test.txt") return_data = test_data.write(fn, TXTFormat) assert return_data.get_data()["number"] == 4 assert return_data.key == "test_data_to_TXTFormat" def test_save_dict_data_in_TXTFormat_return_data_object_key(tmpdir): """Test saving a dict data in TXTFormat returning data object with custom key""" my_dict = {"number": 4} test_data = NumberData.from_dict(my_dict, "test_data") print(test_data) # assert False fn = str(tmpdir / "test.txt") return_data = test_data.write(fn, TXTFormat, "custom") assert return_data.get_data()["number"] == 4 assert return_data.key == "custom" def test_save_file_data_in_another_format_direct(txt_file, tmpdir, capsys): """Test directly converting a TXTFormat data to H5Format""" test_data = NumberData.from_file(txt_file, TXTFormat, "test_data") # print(test_data) fn = str(tmpdir / "test.h5") return_data = test_data.write(fn, H5Format) captured = capsys.readouterr() assert "Directly converting TXTFormat to H5Format" in captured.out assert return_data.get_data()["number"] == 4 assert return_data.key == "test_data_from_TXTFormat" return_data = test_data.write(fn, H5Format, "txt2h5") assert return_data.key == "txt2h5" # print(return_data) # assert False def test_save_file_data_in_another_format_indirect(tmpdir): """Test directly converting a TXTFormat data to H5Format""" my_dict = {"number": 4} test_data = NumberData.from_dict(my_dict, "test_data") fn = str(tmpdir / "test.h5") h5_data = test_data.write(fn, H5Format, "test_data") fn = str(tmpdir / "test.txt") return_data = h5_data.write(fn, TXTFormat) print(return_data) assert return_data.get_data()["number"] == 4 assert return_data.key == "test_data_to_TXTFormat" return_data = test_data.write(fn, H5Format, "txt2h5") assert return_data.key == "txt2h5" # print(return_data) # assert False # Data collection section def test_DataCollection_instance(): """Test creating a DataCollection instance""" collection = DataCollection() assert isinstance(collection, DataCollection) def test_DataCollection_one_data(txt_file): """Test a DataCollection instance with one dataset""" test_data = NumberData.from_file(txt_file, TXTFormat, "test_data") collection = DataCollection(test_data) data_in_collection = collection["test_data"] assert collection.get_data() == data_in_collection.get_data() def test_DataCollection_one_data_write(txt_file, tmpdir): """Test a DataCollection instance with one dataset""" test_data = NumberData.from_file(txt_file, TXTFormat, "test_data") collection = DataCollection(test_data) fn = str(tmpdir / "data.h5") written_data = collection.write(fn, H5Format) assert written_data.mapping_type == H5Format assert written_data.get_data()["number"] == 4 def test_DataCollection_two_data(txt_file): """Test creating a DataCollection instance with two datasets""" my_dict = {"number": 5} test_data_txt = NumberData.from_file(txt_file, TXTFormat, "test_txt") test_data_dict = NumberData.from_dict(my_dict, "test_dict") collection = DataCollection(test_data_txt, test_data_dict) assert collection["test_dict"].get_data()["number"] == 5 assert collection["test_txt"].get_data()["number"] == 4 value_collection = collection.get_data() assert value_collection["test_dict"]["number"] == 5 assert value_collection["test_txt"]["number"] == 4 def test_DataCollection_two_data_write(txt_file, tmpdir): """Test writing a DataCollection instance with two datasets""" my_dict = {"number": 5} test_data_txt = NumberData.from_file(txt_file, TXTFormat, "test_txt") test_data_dict = NumberData.from_dict(my_dict, "test_dict") collection = DataCollection(test_data_txt, test_data_dict) fn_txt = str(tmpdir / "data_new.txt") fn_h5 = str(tmpdir / "data_new.h5") filenames = {"test_txt": fn_h5, "test_dict": fn_txt} format_classes = {"test_txt": H5Format, "test_dict": TXTFormat} keys = {"test_txt": None, "test_dict": None} written_collection = collection.write(filenames, format_classes, keys) # Create a new data collection from the collection dict new_collection = DataCollection(*written_collection.values()) assert new_collection["test_dict_to_TXTFormat"].get_data()["number"] == 5 def test_DataCollection_add_data(txt_file): """Test adding data to a DataCollection instance""" my_dict = {"number": 5} test_data_dict = NumberData.from_dict(my_dict, "test_dict") test_data_txt = NumberData.from_file(txt_file, TXTFormat, "test_txt") collection = DataCollection() collection.add_data(test_data_dict, test_data_txt) print(collection) def test_DataCollection_add_wrong_data_type(): """Test adding data in wrong type to a DataCollection instance""" collection = DataCollection() with pytest.raises(AssertionError): collection.add_data(0) def test_DataCollection_to_list(txt_file): """Test returning a DataCollection as a list""" my_dict = {"number": 5} test_data_dict = NumberData.from_dict(my_dict, "test_dict") test_data_txt = NumberData.from_file(txt_file, TXTFormat, "test_txt") collection = DataCollection(test_data_dict, test_data_txt) my_list = collection.to_list() assert my_list[0].get_data()["number"] == 5 assert my_list[1].get_data()["number"] == 4 libpyvinyl-1.2.0/tests/unit/test_Instrument.py000066400000000000000000000100741456037232700216300ustar00rootroot00000000000000import unittest import os import shutil from test_BaseCalculator import PlusCalculator, NumberData from libpyvinyl.Instrument import Instrument class InstrumentTest(unittest.TestCase): """ Test class for the Detector class. """ @classmethod def setUpClass(cls): """Setting up the test class.""" input1 = NumberData.from_dict({"number": 1}, "input1") input2 = NumberData.from_dict({"number": 1}, "input2") calculator1 = PlusCalculator("test1", [input1, input2]) cls.calculator1 = calculator1 calculator2 = PlusCalculator("test2", [input1, input2]) calculator2.parameters["plus_times"] = 12 cls.calculator2 = calculator2 @classmethod def tearDownClass(cls): """Tearing down the test class.""" pass def setUp(self): """Setting up a test.""" self.__files_to_remove = [] self.__dirs_to_remove = [] def tearDown(self): """Tearing down a test.""" for f in self.__files_to_remove: if os.path.isfile(f): os.remove(f) for d in self.__dirs_to_remove: if os.path.isdir(d): shutil.rmtree(d) def testInstrumentConstruction(self): """Testing the default construction of the class.""" # Construct the object. my_instrument = Instrument("myInstrument") my_instrument.add_calculator(self.calculator1) my_instrument.add_calculator(self.calculator2) def testListCalculator(self): """Testing list calculators""" # Construct the object. my_instrument = Instrument("myInstrument") my_instrument.add_calculator(self.calculator1) my_instrument.add_calculator(self.calculator2) my_instrument.list_calculators() def testListParams(self): """Testing listing parameters""" my_instrument = Instrument("myInstrument") my_instrument.add_calculator(self.calculator1) my_instrument.add_calculator(self.calculator2) my_instrument.list_parameters() def testRemoveCalculator(self): """Testing remove calculator""" my_instrument = Instrument("myInstrument") my_instrument.add_calculator(self.calculator1) my_instrument.add_calculator(self.calculator2) self.assertEqual(len(my_instrument.calculators), 2) my_instrument.remove_calculator(self.calculator1.name) self.assertEqual(len(my_instrument.calculators), 1) def testEditCalculator(self): """Testing edit calculator""" my_instrument = Instrument("myInstrument") my_instrument.add_calculator(self.calculator1) my_instrument.parameters["test1"]["plus_times"] = 10 my_instrument.parameters["test1"]["plus_times"] = 15 energy1 = my_instrument.calculators["test1"].parameters["plus_times"].value self.assertEqual(energy1, 15) def testAddMaster(self): """Testing remove calculator""" my_instrument = Instrument("myInstrument") my_instrument.add_calculator(self.calculator1) my_instrument.add_calculator(self.calculator2) links = {"test1": "plus_times", "test2": "plus_times"} my_instrument.add_master_parameter("plus_times", links) my_instrument.master["plus_times"] = 10 tims1 = my_instrument.calculators["test1"].parameters["plus_times"].value tims2 = my_instrument.calculators["test2"].parameters["plus_times"].value self.assertEqual(tims1, 10) self.assertEqual(tims2, 10) def testSetBasePath(self): """Testing setup base path for calculators""" my_instrument = Instrument("myInstrument") my_instrument.add_calculator(self.calculator1) my_instrument.add_calculator(self.calculator2) my_instrument.set_instrument_base_dir("test") self.assertEqual( my_instrument.calculators["test1"].base_dir, "test/PlusCalculator" ) self.assertEqual( my_instrument.calculators["test2"].base_dir, "test/PlusCalculator" ) if __name__ == "__main__": unittest.main() libpyvinyl-1.2.0/tests/unit/test_Parameters.py000066400000000000000000000552371456037232700215750ustar00rootroot00000000000000import unittest import numpy import pytest import os import tempfile from pint.quantity import Quantity from pint.unit import Unit from libpyvinyl.Parameters import Parameter from libpyvinyl.Parameters import CalculatorParameters from libpyvinyl.Parameters import InstrumentParameters class Test_Parameter(unittest.TestCase): def test_initialize_parameter_simple(self): par = Parameter("test") self.assertEqual(par.name, "test") def test_initialize_parameter_complex(self): par = Parameter("test", unit="cm", comment="comment string") self.assertEqual(par.name, "test") assert par.unit == str(Unit("cm")) self.assertEqual(par.comment, "comment string") def test_units_assignment(self): par = Parameter("test", unit="kg") assert par.unit == Unit("kg") par.unit = "cm" assert par.unit == Unit("cm") par.unit = "meter" assert par.unit == Unit("m") assert par.unit == str(Unit("m")) assert par.unit == "meter" par.unit = "nounit" assert par.unit == "nounit" def test_check_value_type(self): par = Parameter("test") v = 1 par._Parameter__check_compatibility(v) par._Parameter__set_value_type(v) assert par._Parameter__value_type == int v = 1.0 par._Parameter__check_compatibility(v) par._Parameter__set_value_type(v) assert par._Parameter__value_type == Quantity v = "string" with pytest.raises(TypeError): par._Parameter__check_compatibility(v) par._Parameter__set_value_type(v) assert par._Parameter__value_type == str v = True par = Parameter("test") par._Parameter__set_value_type("string") assert par._Parameter__value_type == str with pytest.raises(TypeError): par._Parameter__check_compatibility(v) par = Parameter("test") par._Parameter__set_value_type(True) assert par._Parameter__value_type == bool v = ["ciao", True] par = Parameter("test") with pytest.raises(TypeError): par._Parameter__check_compatibility(v) par._Parameter__set_value_type([False, True]) v = {"ciao": True, "bye": "string"} par = Parameter("test") with pytest.raises(NotImplementedError): par._Parameter__check_compatibility(v) v = {"ciao": True, "bye": False} with pytest.raises(NotImplementedError): par._Parameter__check_compatibility(v) v = numpy.random.uniform(0, 1, 10) par = Parameter("test") par._Parameter__check_compatibility(v) par._Parameter__set_value_type(v) assert par._Parameter__value_type == Quantity # no conditions def test_parameter_no_legal_conditions(self): par = Parameter("test") self.assertTrue(par.is_legal(None)) # FIXME how is this supposed to work? self.assertTrue(par.is_legal(-999)) self.assertTrue(par.is_legal(-1)) self.assertTrue(par.is_legal(0)) self.assertTrue(par.is_legal(1)) self.assertTrue(par.is_legal("This is a string")) self.assertTrue(par.is_legal(True)) self.assertTrue(par.is_legal(False)) self.assertTrue(par.is_legal([0, "A", True])) # case 1: only legal interval def test_parameter_legal_interval(self): par = Parameter("test") par.add_interval(3, 4.5, True) self.assertTrue(par.is_legal(3.5)) self.assertFalse(par.is_legal(1.0)) # case 2: only illegal interval def test_parameter_illegal_interval(self): par = Parameter("test") par.add_interval(3, 4.5, False) self.assertFalse(par.is_legal(3.5)) self.assertTrue(par.is_legal(1.0)) def test_parameter_multiple_intervals(self): par = Parameter("test") par.add_interval(None, 8.5, True) # minus infinite to 8.5 self.assertRaises(ValueError, par.add_interval, 3, 4.5, False) self.assertTrue(par.is_legal(-831.0)) self.assertTrue(par.is_legal(3.5)) self.assertTrue(par.is_legal(5.0)) self.assertFalse(par.is_legal(10.0)) def test_values_different_types(self): par = Parameter("test") par.add_option(9.8, True) with pytest.raises(TypeError): par.add_option(True, True) def test_values_different_units(self): par = Parameter("energy", unit="meV", comment="Energy of emitted particles") import pint ureg = pint.UnitRegistry() with pytest.raises(pint.errors.DimensionalityError): thisunit = Unit("meter") par.value = 5 * thisunit # case 1: only legal option def test_parameter_legal_option_float(self): par = Parameter("test") par.add_option(9.8, True) self.assertFalse(par.is_legal(10)) self.assertTrue(par.is_legal(9.8)) self.assertFalse(par.is_legal(True)) self.assertFalse(par.is_legal("A")) self.assertFalse(par.is_legal(38)) # case 1: only legal option def test_parameter_legal_option_bool(self): par = Parameter("test") par.add_option(True, True) self.assertFalse(par.is_legal(10)) self.assertFalse(par.is_legal(9.8)) self.assertTrue(par.is_legal(True)) self.assertFalse(par.is_legal("A")) self.assertFalse(par.is_legal(38)) # case 1: only legal option def test_parameter_legal_option_float_and_int(self): par = Parameter("test") par.add_option(9.8, True) par.add_option(38, True) self.assertFalse(par.is_legal(10)) self.assertTrue(par.is_legal(9.8)) self.assertFalse(par.is_legal(True)) self.assertFalse(par.is_legal("A")) self.assertTrue(par.is_legal(38)) # case 1: only legal option def test_parameter_legal_option_int_and_float(self): par = Parameter("test") par.add_option(38, True) par.add_option(9.8, True) self.assertFalse(par.is_legal(10)) self.assertTrue(par.is_legal(9.8)) self.assertFalse(par.is_legal(True)) self.assertFalse(par.is_legal("A")) self.assertTrue(par.is_legal(38)) # case 1: only legal option def test_parameter_legal_option_fromlist(self): par = Parameter("test") par.add_option([9, 8, 38], True) self.assertFalse(par.is_legal(10)) self.assertFalse(par.is_legal(9.8)) self.assertFalse(par.is_legal(True)) self.assertFalse(par.is_legal("A")) self.assertTrue(par.is_legal(38)) self.assertTrue(par.is_legal(38.0)) self.assertTrue(par.is_legal(8)) # case 1: only legal option def test_parameter_legal_option_string(self): par = Parameter("test") par.add_option(["B", "A"], True) self.assertFalse(par.is_legal(10)) self.assertFalse(par.is_legal(9.8)) self.assertFalse(par.is_legal(True)) self.assertTrue(par.is_legal("A")) self.assertTrue(par.is_legal("B")) self.assertFalse(par.is_legal("C")) self.assertFalse(par.is_legal(38)) def test_parameter_multiple_options(self): par = Parameter("test") par.add_option(9.8, True) self.assertRaises(ValueError, par.add_option, 3, False) self.assertFalse(par.is_legal(-831.0)) self.assertTrue(par.is_legal(9.8)) self.assertFalse(par.is_legal(3)) # case 1: legal interval + legal option def test_parameter_legal_interval_plus_legal_option(self): par = Parameter("test") par.add_interval(None, 8.5, True) # minus infinite to 8.5 par.add_option(5, True) # this is stupid, already accounted in the interval par.add_option(11, True) self.assertTrue(par.is_legal(-831.0)) self.assertTrue(par.is_legal(8.5)) self.assertTrue(par.is_legal(5.0)) self.assertFalse(par.is_legal(10.0)) self.assertTrue(par.is_legal(11.0)) # case 2: illegal interval + illegal option def test_parameter_illegal_interval_plus_illegal_option(self): par = Parameter("test") par.add_interval(None, 8.5, False) # minus infinite to 8.5 par.add_option(5, False) # this is stupid, already accounted in the interval par.add_option(11, False) self.assertFalse(par.is_legal(-831.0)) self.assertFalse(par.is_legal(8.5)) # illegal because closed interval self.assertFalse(par.is_legal(5.0)) self.assertTrue(par.is_legal(10.0)) self.assertFalse(par.is_legal(11.0)) # case 3: legal interval + illegal option def test_parameter_legal_interval_plus_illegal_option(self): par = Parameter("test") par.add_interval(None, 8.5, True) # minus infinite to 8.5 par.add_option(5, False) self.assertTrue(par.is_legal(-831.0)) self.assertTrue(par.is_legal(8.5)) self.assertFalse(par.is_legal(5.0)) self.assertFalse(par.is_legal(10.0)) self.assertFalse(par.is_legal(11.0)) # case 4: illegal interval + legal option def test_parameter_illegal_interval_plus_legal_option(self): par = Parameter("test") par.add_interval(None, 8.5, False) # minus infinite to 8.5 par.add_option(5, True) self.assertFalse(par.is_legal(-831.0)) self.assertFalse(par.is_legal(8.5)) self.assertTrue(par.is_legal(5.0)) self.assertTrue(par.is_legal(10.0)) self.assertTrue(par.is_legal(11.0)) # case 2: illegal interval + illegal option def test_parameter_get_options(self): """ Ensure get_options returns the options as required """ par = Parameter("test") par.add_interval(None, 8.5, False) # minus infinite to 8.5 par.add_option(5, True) # this is stupid, already accounted in the interval par.add_option(11, True) retrieved_options = par.get_options() self.assertEqual(len(retrieved_options), 2) self.assertEqual(retrieved_options[0], 5.0) self.assertEqual(retrieved_options[1], 11.0) self.assertTrue(par.get_options_are_legal()) def test_parameter_value_type(self): par = Parameter("test") par.value = 4.0 assert par._Parameter__value_type == Quantity par1 = Parameter("test") par1.value = 4 assert par1._Parameter__value_type == int par2 = Parameter("test", unit="meV") par2.value = 4 assert par2._Parameter__value_type == Quantity par3 = Parameter("test", unit="meV") par3.add_interval(0, 1e6, True) assert par3._Parameter__value_type == Quantity def test_parameter_set_value(self): par = Parameter("test") par.add_interval(3, 4.5, True) par.value = 4.0 self.assertEqual(par.value, 4.0) with self.assertRaises(ValueError): par.value = 5.0 # Should throw an error and be ignored self.assertEqual(par.value, 4.0) def test_add_interval_after_value(self): par = Parameter("test") par.value = 4.0 par.add_interval(3, 4.5, True) par.clear_intervals() par.value = 5.0 with self.assertRaises(ValueError): par.add_interval(3, 4.5, True) def test_parameter_from_dict(self): par = Parameter("test") par.add_interval(3, 4.5, True) par.value = 4.0 par_from_dict = Parameter.from_dict(par.__dict__) self.assertEqual(par_from_dict.value, 4.0) def test_print_legal_interval(self): par = Parameter("test") par.add_interval(3, 4.5, True) par.add_option(9.8, True) par.print_parameter_constraints() def test_clear_intervals(self): # FIXME par = Parameter("test") par.add_interval(3, 4.5, True) # self.assertEqual(par.__intervals, [[3, 4.5]]) #FIXME par.clear_intervals() par.add_option(9.7, True) # self.assertEqual(par.__options, [9.7]) par.clear_options() # self.assertEqual(par.__options, []) def test_print_line(self): par = Parameter("test") par.add_interval(3, 4.5, True) par.add_option(9.8, True) par.print_line() def test_print(self): par = Parameter("test") par.add_interval(3, 4.5, True) par.add_option(9.8, True) print(par) def test_parameter_iterable(self): par = Parameter("test") par.add_interval(3, 4.5, True) par.add_option(7, True) self.assertFalse(par.is_legal([0.5, 3.2, 5.0])) self.assertTrue(par.is_legal([3.1, 4.2, 4.4])) self.assertTrue(par.is_legal([3.1, 4.2, 4.4, 7])) def test_get_intervals(self): par = Parameter("test") par.add_interval(3, 4.5, True) par.add_interval(8, 10, True) retrived_intervals = par.get_intervals() self.assertEqual(len(retrived_intervals), 2) self.assertEqual(retrived_intervals[0][0], 3) self.assertEqual(retrived_intervals[0][1], 4.5) self.assertEqual(retrived_intervals[1][0], 8) self.assertEqual(retrived_intervals[1][1], 10) self.assertTrue(par.get_intervals_are_legal()) def test_parameters_with_quantity(self): """Test if we can construct and use a Parameter instance passing pint.Quantity and pint.Unit objects to the constructor and interval setter.""" # Define the base unit of my parameter object. meter = Unit("meter") self.assertIsInstance(meter, Unit) minimum_undulator_length = 10.0 * meter undulator_length = Parameter("undulator_length", meter) self.assertIsInstance(undulator_length, Parameter) self.assertEqual(undulator_length.unit, Unit("meter")) undulator_length.add_interval( min_value=minimum_undulator_length, max_value=numpy.inf * meter, intervals_are_legal=True, ) self.assertTrue(undulator_length.is_legal(10.1 * meter)) self.assertFalse(undulator_length.is_legal(9.0 * meter)) self.assertTrue(undulator_length.is_legal(5.5e4 * Unit("centimeter"))) def test_parameter_set_numpy_value(self): par = Parameter("test", unit="eV") par.value = 1e-4 par.value = numpy.log(10) def test_parameters_with_quantity_powers(self): """Test if we can construct and use a Parameter instance passing pint.Quantity and pint.Unit objects to the constructor and interval setter. Use different powers of 10 in parameter initialization and value assignment.""" # Define the base unit of my parameter object. meter = Unit("meter") centimeter = Unit("centimeter") self.assertIsInstance(meter, Unit) minimum_undulator_length = 10.0 * meter undulator_length = Parameter("undulator_length", centimeter) self.assertIsInstance(undulator_length, Parameter) self.assertEqual(undulator_length.unit, Unit("centimeter")) undulator_length.add_interval( min_value=minimum_undulator_length, max_value=numpy.inf * meter, intervals_are_legal=True, ) print(undulator_length) self.assertTrue(undulator_length.is_legal(10.1 * meter)) self.assertFalse(undulator_length.is_legal(9.0 * centimeter)) self.assertTrue(undulator_length.is_legal(5.5e4 * Unit("centimeter"))) class Test_Parameters(unittest.TestCase): def test_initialize_parameters_from_list(self): par1 = Parameter("test") par1.value = 8 par2 = Parameter("test2", unit="meV") parameters = CalculatorParameters([par1, par2]) self.assertEqual(parameters["test"].value, 8) def test_initialize_parameters_from_add(self): par1 = Parameter("test") par1.value = 8 par2 = Parameter("test2", unit="meV") par2.value = 10 parameters = CalculatorParameters() parameters.add(par1) parameters.add(par2) self.assertEqual(parameters["test"].value, 8) self.assertEqual(parameters["test2"].value, 10) def test_print_parameters(self): par1 = Parameter("test") par1.value = 8 par2 = Parameter("test2", unit="meV") par2.value = 10 parameters = CalculatorParameters() parameters.add(par1) parameters.add(par2) print(parameters) def test_json(self): par1 = Parameter("test") par1.value = 8.0 par2 = Parameter("test2", unit="meV") par2.value = 10 parameters = CalculatorParameters() parameters.add(par1) parameters.add(par2) with tempfile.TemporaryDirectory() as d: tmp_file = os.path.join(d, "test.json") parameters.to_json(tmp_file) params_json = CalculatorParameters.from_json(tmp_file) self.assertEqual(params_json["test2"].value, 10) assert params_json["test2"].value_no_conversion == Quantity(10, "meV") with pytest.raises(TypeError): params_json["test2"].value = "A" def test_json_with_objects(self): par1 = Parameter("test") par1.value = 8 par2 = Parameter("test2", unit="meV") par2.value = 10 par3 = Parameter("test3", unit="meV") par3.value = 3.14 parameters = CalculatorParameters() parameters.add(par1) parameters.add(par2) parameters.add(par3) with tempfile.TemporaryDirectory() as d: tmp_file = os.path.join(d, "test.json") tmp_file = "/tmp/test.json" parameters.to_json(tmp_file) params_json = CalculatorParameters.from_json(tmp_file) self.assertEqual(params_json["test2"].value, 10) print(params_json["test3"]) assert params_json["test3"].value == par3.value assert params_json["test3"].value == 3.14 assert params_json["test3"].value_no_conversion == Quantity(3.14, "meV") def test_get_item(self): par1 = Parameter("test") par1.value = 8 par2 = Parameter("test2", unit="meV") par2.value = 10 parameters = CalculatorParameters() self.assertRaises(KeyError, parameters.__getitem__, "test3") def test_containment(self): par1 = Parameter("test") par1.value = 8 par2 = Parameter("test2", unit="meV") par2.value = 10 parameters = CalculatorParameters() parameters.add(par1) parameters.add(par2) assert parameters.__contains__("test") == True assert parameters.__contains__("test3") == False def source_calculator(): """ Little dummy calculator that sets up a parameters object for a source """ parameters = CalculatorParameters() parameters.new_parameter("energy", unit="eV", comment="Source energy setting") parameters["energy"].add_interval(0, 1e6, True) parameters["energy"].value = 4000 parameters.new_parameter("delta_energy", unit="eV", comment="Energy spread fwhm") parameters["delta_energy"].add_interval(0, 400, True) parameters.new_parameter("position", unit="cm", comment="Source center") parameters["position"].add_interval(-1.5, 1.5, True) parameters.new_parameter("gaussian", comment="False for flat, True for gaussian") parameters["gaussian"].add_option([False, True], True) return parameters def sample_calculator(): """ Little dummy calculator that sets up a parameters object for a sample """ parameters = CalculatorParameters() parameters.new_parameter("radius", unit="cm", comment="Sample radius") parameters["radius"].add_interval(0, None, True) # To infinite parameters.new_parameter("height", unit="cm", comment="Sample height") parameters["height"].add_interval(0, None, True) absporption = parameters.new_parameter( "absorption", unit="barns", comment="absorption cross section" ) absporption.add_interval(0, None, True) return parameters class Test_Instruments(unittest.TestCase): @classmethod def setUpClass(cls): """Setting up the test class.""" cls.d = tempfile.TemporaryDirectory() def setUp(self): # We start creating our instrument with a InstrumentParameters self.instr_parameters = InstrumentParameters() # We insert a source and get some parameters out source_pars = source_calculator() # These are added to the instr_parameters so they can be controlled self.instr_parameters.add("Source", source_pars) # We also add a few sample objects with their parameter objects top_sample_pars = sample_calculator() self.instr_parameters.add("Sample top", top_sample_pars) bottom_sample_pars = sample_calculator() self.instr_parameters.add("Sample bottom", bottom_sample_pars) @classmethod def tearDownClass(cls): """Tearing down the test class.""" cls.d.cleanup() def test_link(self): description = "Absorption cross section for both samples" links = {"Sample top": "absorption", "Sample bottom": "absorption"} master_value = 3.4 self.instr_parameters.add_master_parameter( "absorption", links, unit="barns", comment=description ) self.instr_parameters.master["absorption"] = master_value top_value = self.instr_parameters["Sample top"]["absorption"].value bottom_value = self.instr_parameters["Sample bottom"]["absorption"].value self.assertEqual(top_value, master_value) self.assertEqual(bottom_value, master_value) master_params = self.instr_parameters.master.parameters self.assertIn("absorption", master_params.keys()) self.assertEqual(master_value, master_params["absorption"].value) self.assertEqual(self.instr_parameters.master["absorption"].links, links) def test_print(self): print(self.instr_parameters) def test_json(self): description = "Absorption cross section for both samples" links = {"Sample top": "absorption", "Sample bottom": "absorption"} master_value = 3.4 self.instr_parameters.add_master_parameter( "absorption", links, unit="barns", comment=description ) self.instr_parameters.master["absorption"] = master_value temp_file = os.path.join(self.d.name, "test.json") self.instr_parameters.to_json(temp_file) print(self.instr_parameters) # From json instr_json = InstrumentParameters.from_json(temp_file) self.assertEqual(instr_json["Source"]["energy"].value, 4000) master_params = instr_json.master.parameters self.assertIn("absorption", master_params.keys()) self.assertEqual(master_value, master_params["absorption"].value) self.assertEqual(self.instr_parameters.master["absorption"].links, links) if __name__ == "__main__": unittest.main()