pax_global_header00006660000000000000000000000064147646114170014525gustar00rootroot0000000000000052 comment=72d7cec14cd26e9dd6b032773842affb179af1f0 unyt-3.0.4/000077500000000000000000000000001476461141700125305ustar00rootroot00000000000000unyt-3.0.4/.editorconfig000066400000000000000000000004441476461141700152070ustar00rootroot00000000000000# http://editorconfig.org root = true [*] indent_style = space indent_size = 4 trim_trailing_whitespace = true insert_final_newline = true charset = utf-8 end_of_line = lf [*.bat] indent_style = tab end_of_line = crlf [LICENSE] insert_final_newline = false [Makefile] indent_style = tab unyt-3.0.4/.git-blame-ignore-revs000066400000000000000000000003031476461141700166240ustar00rootroot00000000000000# automated linting (black + isort + blacken-docs) 982fc992fa6cb636d87ccc445e695cb5f964eb12 # flynt + pyupgrade 381650d795922aeba997cbe46685d3ca21ccba51 d707555f9386c5b7fe8f50b12d52257375f9840a unyt-3.0.4/.gitattributes000066400000000000000000000000361476461141700154220ustar00rootroot00000000000000unyt/_version.py export-subst unyt-3.0.4/.github/000077500000000000000000000000001476461141700140705ustar00rootroot00000000000000unyt-3.0.4/.github/ISSUE_TEMPLATE.md000066400000000000000000000004731476461141700166010ustar00rootroot00000000000000* unyt 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. ``` unyt-3.0.4/.github/dependabot.yml000066400000000000000000000002601476461141700167160ustar00rootroot00000000000000version: 2 updates: - package-ecosystem: github-actions directory: /.github/workflows schedule: interval: monthly groups: actions: patterns: - '*' unyt-3.0.4/.github/workflows/000077500000000000000000000000001476461141700161255ustar00rootroot00000000000000unyt-3.0.4/.github/workflows/bleeding-edge.yaml000066400000000000000000000027441476461141700214730ustar00rootroot00000000000000name: CI (bleeding edge) # this workflow is heavily inspired from pandas, see # https://github.com/pandas-dev/pandas/blob/master/.github/workflows/python-dev.yml # check stability against dev version of Python, numpy, and matplotlib # and sympy pre-releases (but avoid pre-releases of sympy's dependencies) on: push: branches: - master pull_request: paths: - .github/workflows/bleeding-edge.yaml # Run every Wednesday at 0:01 UTC schedule: - cron: 1 0 * * 3 workflow_dispatch: jobs: build: runs-on: ubuntu-latest name: Python3.13-dev timeout-minutes: 60 concurrency: group: ${{ github.ref }}-dev cancel-in-progress: true steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - name: Set up Python Dev Version uses: actions/setup-python@v5 with: python-version: '3.13-dev' - name: Install dependencies run: | python -m pip install --upgrade pip python -m pip install --upgrade setuptools wheel setuptools_scm python -m pip install --pre numpy --only-binary ":all:" --extra-index \ https://pypi.anaconda.org/scientific-python-nightly-wheels/simple python -m pip install pytest python -m pip install sympy python -m pip install --no-deps --upgrade --pre sympy - name: Build unyt run: python -m pip install --no-build-isolation . - run: python -m pip list - name: Run Tests run: pytest -vvv unyt/ unyt-3.0.4/.github/workflows/ci.yml000066400000000000000000000021641476461141700172460ustar00rootroot00000000000000name: CI on: push: branches: - master pull_request: schedule: - cron: '1 0 * * *' workflow_dispatch: jobs: test: runs-on: ${{ matrix.os }} strategy: matrix: os: [ubuntu-latest] python-version: - '3.9' - '3.10' - '3.11' - '3.12' - '3.13' # Test all on ubuntu, test ends on macos and windows include: - os: macos-13 # pin macos-13 (x86) because Python 3.9 is broken in the arm64 image python-version: '3.9' - os: windows-latest python-version: '3.9' - os: macos-latest python-version: '3.13' - os: windows-latest python-version: '3.13' steps: - uses: actions/checkout@v4 - name: Setup Python uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Install Tox and any other packages run: | python -m pip install --upgrade pip python -m pip install tox tox-gh-actions - name: Test run: tox -vvv unyt-3.0.4/.github/workflows/publish.yml000066400000000000000000000014651476461141700203240ustar00rootroot00000000000000name: Publish to PyPI on: push: tags: v* pull_request: paths: - .github/workflows/publish.yml jobs: pypi-publish: name: Upload release to PyPI runs-on: ubuntu-latest environment: name: pypi url: https://pypi.org/p/unyt permissions: id-token: write steps: - name: Checkout Source uses: actions/checkout@v4 - name: Setup Python uses: actions/setup-python@v5 with: python-version: 3.x - name: Install build dependencies run: python -m pip install build wheel - name: Build distributions shell: bash -l {0} run: python -m build - name: Publish package distributions to PyPI if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') uses: pypa/gh-action-pypi-publish@release/v1 unyt-3.0.4/.gitignore000066400000000000000000000026631476461141700145270ustar00rootroot00000000000000# Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # setuptools_scm generated file unyt/_version.py # Distribution / packaging .Python env/ build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ *.egg-info/ .installed.cfg *.egg # this can probably be removed at some point # https://github.com/pypa/pip/issues/6213 pip-wheel-metadata/ # 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/ # Autogenerated API Documentation docs/modules/ # benchmark outputs benchmarks/*.json benchmarks/*.png # Pycharm config .idea/ unyt-3.0.4/.mailmap000066400000000000000000000010301476461141700141430ustar00rootroot00000000000000Nathan Goldbaum Nathan Goldbaum Nathan Goldbaum Nathan Goldbaum Kacper Kowalik Kacper Kowalik (Xarthisius) Kyle Sunden ksunden Kyle Sunden Kyle Sunden John ZuHone jzuhone Ricarda Beckmann RicardaBeckmann unyt-3.0.4/.pre-commit-config.yaml000066400000000000000000000024041476461141700170110ustar00rootroot00000000000000minimum_pre_commit_version: 1.15.0 ci: autofix_prs: false autoupdate_schedule: monthly exclude: "^(\ paper/.*.py\ )" repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v5.0.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer - id: no-commit-to-branch - id: check-shebang-scripts-are-executable - id: check-executables-have-shebangs - id: check-yaml - id: check-toml # TODO: replace this with ruff when it supports embedded python blocks # see https://github.com/astral-sh/ruff/issues/8237 - repo: https://github.com/adamchainz/blacken-docs rev: 1.19.1 hooks: - id: blacken-docs additional_dependencies: [black==24.10.0] - repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.9.9 hooks: - id: ruff-format - id: ruff args: [--fix, --show-fixes] - repo: https://github.com/pre-commit/pygrep-hooks rev: v1.10.0 hooks: - id: rst-backticks - repo: https://github.com/codespell-project/codespell rev: v2.4.1 hooks: - id: codespell args: [--skip="*.pdf", -L, 'smoot,copin,celcius,ue,bu,dne,ond'] unyt-3.0.4/.readthedocs.yaml000066400000000000000000000014141476461141700157570ustar00rootroot00000000000000# .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.9" # You can also specify other tool versions: # nodejs: "16" # rust: "1.55" # golang: "1.17" # Build documentation in the docs/ directory with Sphinx sphinx: configuration: docs/conf.py # If using Sphinx, optionally build your docs in additional formats such as PDF # formats: # - pdf # Optionally declare the Python requirements required to build your docs python: install: - method: pip path: . - requirements: docs/requirements.txt unyt-3.0.4/AUTHORS.rst000066400000000000000000000024051476461141700144100ustar00rootroot00000000000000======= Credits ======= Contributors ------------ * Daniel Bates * Ricarda Beckmann * Andrei Berceanu * Josh Borrow * Yi-Hao Chen * Bili Dong * Sam Dotson * Nathan Goldbaum * David Hannasch * Chris Havlin * Peter Hayman * Cameron Hummels * Thomas Hisch * Paul Ivanov * Suoqing Ji * Lee Johnston * Ben Kimock * Kacper Kowalik * Nathan Musoke * Andrew Myers * Kyle Oman * Osnippet * Clément Robert * Simon Schopferer * Sam Skillman * Britton Smith * Josh Soref * Kyle Sunden * Ben Thompson * Matthew Turk * Miguel de Val-Borro * Jeroen Van Goey * Mike Zingale * John ZuHone This library was adapted from `dimensionful `_ which was written by Casey Stark . unyt-3.0.4/CONTRIBUTING.rst000066400000000000000000000204111476461141700151670ustar00rootroot00000000000000.. highlight:: shell ============ Contributing ============ Contributions are welcome, and they are greatly appreciated! Every little bit helps, and credit will always be given. Code of Conduct --------------- The development of ``unyt`` happens in the context of the `yt community code of conduct `_. If for any reason you feel that the code of conduct has been violated in the context of ``unyt`` development, please send an e-mail to confidential@yt-project.org with details describing the incident. All emails sent to this address will be treated with the strictest confidence by an individual who does not normally participate in yt development. Types of Contributions ---------------------- You can contribute in many ways: Report Bugs ~~~~~~~~~~~ Report bugs at https://github.com/yt-project/unyt/issues. If you are reporting a bug, please include: * Your operating system name and version. * Any details about your local setup that might be helpful in troubleshooting. This includes things like Python version and versions of any libraries being used, including unyt. * If possible, detailed steps to reproduce the bug. Fix Bugs ~~~~~~~~ Look through the GitHub issues for bugs. Anything tagged with "bug" and "help wanted" is open to whoever wants to implement it. Implement Features ~~~~~~~~~~~~~~~~~~ Look through the GitHub issues for features. Anything tagged with "enhancement" and "help wanted" is open to whoever wants to implement it. Write Documentation ~~~~~~~~~~~~~~~~~~~ unyt could always use more documentation, whether as part of the official unyt docs, in docstrings, or even on the web in blog posts, articles, and such. Submit Feedback ~~~~~~~~~~~~~~~ The best way to send feedback is to file an issue at https://github.com/yt-project/unyt/issues. If you are proposing a feature: * Explain in detail how it would work. * Keep the scope as narrow as possible, to make it easier to implement. * Remember that this is a volunteer-driven project, and that contributions are welcome :) Get Started! ------------ Ready to contribute? Here's how to set up ``unyt`` for local development. The ``unyt`` test suite makes use of the ``tox`` test runner, which makes it easy to run tests on multiple python versions. However, this means that if all of the python versions needed by ``tox`` are not available, many of the ``tox`` tests will fail with errors about missing python executables. This guide makes use of ``pyenv`` to set up all of the Python versions used in the unyt test suite. You do not have to use ``pyenv`` if you have other ways of managing your python environment using your operating system's package manager or ``conda``. 1. Fork the ``unyt`` repo on GitHub. 2. Clone your fork locally:: $ git clone git@github.com:your_name_here/unyt.git 3. Install ``pyenv``:: $ git clone https://github.com/pyenv/pyenv.git $HOME/.pyenv $ export PYENV_ROOT="$HOME/.pyenv" $ export PATH="$HOME/.pyenv/bin:$PATH $ eval "$(pyenv init -)" $ pyenv install -s 3.9 $ pyenv install -s 3.10 $ pyenv install -s 3.11 $ pyenv install -s 3.12 $ pip install tox 4. Install your local copy into a virtualenv or conda environment. You can also use one of the python interpreters we installed using ``pyenv``:: $ cd unyt/ $ pyenv local 3.10 $ python -m pip install -e 5. Create a branch for local development:: $ git checkout -b name-of-your-bugfix-or-feature 6. Edit files in the ``unyt`` repository, using your local python installation to test your edits. 7. When you're done making changes, check that your changes pass linting, and run the tests, including testing several Python versions with ``tox``:: $ pre-commit run --all-files $ pytest --doctest-modules --doctest-rst --doctest-plus $ pyenv local 3.9 3.10 3.11 3.12 $ tox $ pyenv local 3.10 To get ``pre-commit``, ``pytest``, ``pytest-doctestplus``, and ``tox``, just ``pip`` or ``conda`` install them into your python environment, as appropriate. For a ``pyenv`` environment you would use ``pip``. 8. Commit your changes and push your branch to GitHub:: $ git add . $ git commit -m "Your detailed description of your changes." $ git push origin name-of-your-bugfix-or-feature 9. Submit a pull request through the GitHub website. Testing unyt ------------ We use the ``pytest`` test runner as well as the ``tox`` test wrapper to manage running tests on various versions of python. To run the tests on your copy of the ``unyt`` repository using your current python environment, run ``pytest`` in the root of the repository using the following arguments:: $ cd unyt/ $ pytest --doctest-modules --doctest-rst --doctest-plus These enable testing the docstrings and doctest examples scattered throughout the unyt and its documentation. You will need to install ``pytest`` and ``pytest-doctestplus`` to run this command. Some tests depend on ``h5py``, ``Pint``, ``astropy``, ``matplotlib`` and ``dask`` being installed. If you would like to run the tests on multiple python versions, first ensure that you have multiple python versions visible on your ``$PATH``, then simply execute ``tox`` in the root of the ``unyt`` repository. For example, using the ``pyenv`` environment we set up above:: $ cd unyt $ pyenv local 3.9 3.10 3.11 3.12 $ tox The ``tox`` package itself can be installed using the ``pip`` associated with one of the python installations. See the ``tox.ini`` file in the root of the repository for more details about our ``tox`` setup. Note that you do not need to install anything besides ``tox`` and the ``python`` versions needed by ``tox`` for this to work, ``tox`` will handle setting up the test environment, including installing any necessary dependencies via ``pip``. Pull Request Guidelines ----------------------- Before you submit a pull request, check that it meets these guidelines: 1. The pull request should include tests for functionality that is not already tested. We strive for 100% test coverage and pull requests should not add any new untested code. You can generate coverage reports locally by running the ``tox`` tests. 2. If the pull request adds functionality the docs should be updated. If your new functionality adds new functions or classes to the public API, please add docstrings. If you modified an existing function or class in the public API, please update the existing docstrings. If you modify private implementation details, please use your judgment on documenting it with comments or docstrings. 3. The pull request should work for Python 3.8, 3.9 and 3.10. Check in the GitHub interface for your pull request and make sure that the tests pass for all supported Python versions. Deploying --------- A reminder for the maintainers on how to deploy. Make sure all your changes are committed (including an entry in HISTORY.rst and adding any new contributors to AUTHORS.rst). If doing a bugfix release, you may need to create a - or checkout an existing - backport branch named ``vX.Y.x`` where ``X`` and ``Y`` represent the relevant major and minor version numbers, and the lowercase ``x`` is literal. Otherwise you may just release from the development branch. Once you are ready, create a tag: $ git tag vX.Y.Z # where X, Y and Z should be meaningful major, minor and micro version numbers If the tests pass you can then subsequently manually do a test publication:: $ python -m pip install --upgrade pip $ python -m pip install --upgrade build twine $ twine check dist/* $ twine upload dist/* --repository-url https://test.pypi.org/legacy/ Then, using a fresh environment here, and from outside the repository, test the result:: $ python -m pip install pytest $ python -m pip install --index-url https://test.pypi.org/simple/ unyt --extra-index-url https://pypi.org/simple --force-reinstall $ python -c "import unyt; unyt.test()" $ python -m pip install --index-url https://test.pypi.org/simple/ unyt --extra-index-url https://pypi.org/simple --no-binary unyt --force-reinstall $ python -c "import unyt; unyt.test()" Finally, if everything works well, push the tag to the upstream repository:: $ git push upstream --tag # assuming the mother repo yt-project/unyt is set as a remote under the name "upstream" unyt-3.0.4/HISTORY.rst000066400000000000000000001177211476461141700144340ustar00rootroot00000000000000======= History ======= 3.0.4 (2025-03-13) ------------------ This new bugfix release of ``unyt`` fixes bugs discovered since the v3.0.3 release. It also adds support for foe and bethe units: * Add foe and bethe units (`PR #521 `_). Thank you to Michael Zingale (@zingale on GitHub) for the contribution. Following is a list of all bug fixes and documentation fixes included in the release. * Fix incompatibilities with numpy 2.1 (`PR #512 `_). Thank you to Clément Robert (@neutrinoceros on GitHub) for the contribution. * Fix stephan-boltzmann constant accuracy and add radiation constant (`PR #520 `_). Thank you to Mike Zingale (@zingale on GitHub) for the contribution. * Fix raising a unyt array to an array power in sensible cases (`PR #524 `_). Thank you to Clément Robert (@neutrinoceros on GitHub) for the contribution. * Typo fixes and documentation fix (`PR #529 `_). Thank you to Jeroen Van Goey (@BioGeek on GitHub) for the contribution. * Fix return units from numpy.ftt functions (`PR #531 `_). Thank you to Josh Borrow (@JBorrow on GitHub) for the contribution. * Fix incorrect output unit for ``np.prod`` with an axis argument (`PR #537 `_). Thank you to Kyle Oman (@kyleoman on GitHub) for the contribution. * ``np.histogram*`` functions give correct units when weights and/or density are set (`PR #539 `_). Thank you to Kyle Oman (@kyleoman on GitHub) for the contribution. * Fix an issue where ``np.histogramdd`` could create infinite recursion on some inputs (`PR #541 `_). Thank you to Clément Robert (@neutrinoceros on GitHub) for the contribution. * ``linspace`` and ``logspace`` give incorrect results or crash with some inputs (`PR #544 `_). Thank you to Kyle Oman (@kyleoman on GitHub) for the contribution. * Fix typo in array function implementations (ftt -> fft) (`PR #547 `_). Thank you to Clément Robert (@neutrinoceros on GitHub) for the contribution. * Apply_over_axes no longer assumes user-supplied function preserves units (`PR #548 `_). Thank you to Kyle Oman (@kyleoman on GitHub) for the contribution. * Allow subclassing in ``unyt_array.__array_func__`` (`PR #550 `_). Thank you to Kyle Oman (@kyleoman on GitHub) for the contribution. * Fix unit handling for ``np.take`` and ``unyt_array.take`` (`PR #551 `_). Thank you to Kyle Oman (@kyleoman on GitHub) for the contribution. * Fix a regression where ``np.linspace`` 's num argument would be ignored for ``unyt_array`` instances (`PR #553 `_). Thank you to Clément Robert (@neutrinoceros on GitHub) for the contribution. * Handle ``np.vecdot`` as a ufunc rather than an arrayfunc (`PR #557 `_). Thank you to Kyle Oman (@kyleoman on GitHub) for the contribution. * Fix an issue where hdf5 io wouldn't roundtrip properly for a ``unyt_quantity`` object (`PR #560 `_). Thank you to Clément Robert (@neutrinoceros on GitHub) for the contribution. * Make ``test_unique_values`` order-agnostic and fix testing against numpy 2.3 dev (`PR #565 `_). Thank you to Clément Robert (@neutrinoceros on GitHub) for the contribution. 3.0.3 (2024-07-02) ------------------ This new bugfix release of ``unyt`` fixes bugs discovered since the v3.0.2 release. * Fix defects when running test suite in isolation from the project's pytest configuration (`PR #495 `_). Thank you to Clément Robert (@neutrinoceros on GitHub) for the contribution. * Drop test case for unpickling old pickle files as too sensitive to upstream changes (`PR #498 `_). Thank you to Clément Robert (@neutrinoceros on GitHub) for the contribution. * Fix signature incompatibilities in nep 18 wrapped functions (`PR #500 `_). Thank you to Clément Robert (@neutrinoceros on GitHub) for the contribution. * Fix an incompatibility with sympy 1.13.0rc1 (`PR #504 `_). Thank you to Clément Robert (@neutrinoceros on GitHub) for the contribution. * Adjust doctests to changes in array repr from numpy 2.0 (`PR #506 `_). Thank you to Clément Robert (@neutrinoceros on GitHub) for the contribution. * TST: declare np.unstack as subclass-safe (fix incompatibility with Numpy 2.1) (`PR #509 `_). Thank you to Clément Robert (@neutrinoceros on GitHub) for the contribution. 3.0.2 (2024-03-13) ------------------ This new bugfix release of ``unyt`` fixes bugs discovered since the v3.0.1 release, and is intended to be compatible with the upcoming NumPy 2.0 release. * Fix minimal requirement on setuptools_scm (`PR #471 `_). Thank you to Clément Robert (@neutrinoceros on GitHub) for the contribution. * Explicitly forbid destructive edits to the default unit registry (`PR #475 `_). Thank you to Clément Robert (@neutrinoceros on GitHub) for the contribution. * Fix an issue where array functions would raise ``UnitInconsistencyError`` when operands' units differ by some dimensionless factor (`PR #478 `_). Thank you to Clément Robert (@neutrinoceros on GitHub) for the contribution. * Implement and test array functions new in numpy 2.0 (`PR #483 `_). Thank you to Clément Robert (@neutrinoceros on GitHub) for the contribution. * Fix compat with numpy dev for ``np.trapezoid`` (previously named np.trapz) (`PR #486 `_). Thank you to Clément Robert (@neutrinoceros on GitHub) for the contribution. * Implement missing support for ``np.cbrt`` (`PR #491 `_). Thank you to @yuyttenhove for the contribution. * Fix compatibility with numpy 2.0 copy semantics (`PR #492 `_). Thank you to Clément Robert (@neutrinoceros on GitHub) for the contribution. 3.0.1 (2023-11-02) ------------------ This new bugfix release of ``unyt`` fixes a few bugs since the v3.0.0 release. * Fix an issue where array functions would raise ``UnitConsistencyError`` on ``unyt_array`` objects using non-default unit registries (`PR #463 `_). Thank you to Clément Robert (@neutrinoceros on GitHub) for the contribution. * Fix an issue where array functions would crash (``AttributeError``) when passed non-``ndarray`` array-like objects (e.g. Python lists) (`PR #464 `_). Thank you to Clément Robert (@neutrinoceros on GitHub) for the contribution. * Fix backward compatibility for calling ``numpy.histogram`` with implicit range units (`PR #466 `_). Thank you to Clément Robert (@neutrinoceros on GitHub) for the contribution. 3.0.0 (2023-11-01) ------------------ This new major release of ``unyt`` fixes a number of issues and adds a number of new features. Major contributions include: * Support for Python 3.8 has been dropped. * Support for Python 3.12 has been added. * Support for NumPy <1.19.3 has been dropped. * Support for SymPy <1.7 has been dropped. * A new ``unyt_dask_array`` class, which implements a subclass of standard `dask arrays `_ with units attached, has been added (`PR #185 `_). See :ref:`dask` for more details. Thank you to Chris Havlin (@chrishavlin on Github) for the contribution. * A number of new metric and non-metric units have been added in `PR #441 `_. Thank you to John ZuHone (@jzuhone on GitHub) for the contribution. * A number of common values for the solar metallicity found in the literature have been added as new metallicity units (`PR #315 `_). See :ref:`metal_conversions` for more details. Thank you to John ZuHone (@jzuhone on GitHub) for the contribution. * The "liter" unit has been added (`PR #305 `_). Thank you to Nathan Goldbaum (@ngoldbaum on GitHub) for the contribution. * The following common systems engineering units for energy have been added: ``MMBTU``, ``therm``, ``quad``, and ``Wh`` (`PR #294 `_). Thank you to Sam Dotson (@samgdotson on GitHub) for the contribution. * The ``@returns`` decorator (documented in :ref:`checking_units`) now allows dimension-checking of multiple return values (`PR #435 `_). Thank you to Daniel Bates (@db434 on GitHub) for the contribution. * A number of PRs to support `NEP 18 `_, including the following (thank you to Clément Robert, @neutrinoceros on GitHub, and Nathan Goldbaum, @ngoldbaum on Github, for the contributions): - `PR #200 `_. - `PR #293 `_. - `PR #295 `_. - `PR #304 `_. - `PR #309 `_. - `PR #313 `_. - `PR #316 `_. - `PR #317 `_. - `PR #319 `_. - `PR #320 `_. - `PR #324 `_. - `PR #325 `_. - `PR #329 `_. - `PR #338 `_. - `PR #348 `_. - `PR #351 `_. - `PR #352 `_. - `PR #388 `_. - `PR #394 `_. - `PR #395 `_. - `PR #396 `_. - `PR #397 `_. - `PR #398 `_. * A fix for for the LaTeX representation of Planck units (`PR #379 `_). Thank you to Peter Hayman (@haymanpf on GitHub) for the contribution. * A fix for a bug that prevented the conversion of dimensionless arrays to their corresponding `AstroPy Quantities `_ (`PR #437 `_). Thank you to Clément Robert (@neutrinoceros on GitHub) for the contribution. * A fix for a bug in subtraction of temperature quantities that resulted in ``degC`` units being returned instead of ``delta_degC`` units (`PR #413 `_). Thank you to Clément Robert (@neutrinoceros on GitHub) for the contribution. * Fixes for issues with the comparison of temperature quantities (`PR #408 `_ and `PR #412 `_). Thank you to Clément Robert (@neutrinoceros on GitHub) for the contribution. * Support for versions of NumPy < 1.19 has been dropped in this version (`PR #403 `_). Thank you to Clément Robert (@neutrinoceros on GitHub) for the contribution. * A number of PRs to support NumPy 2.0, thank you to Clément Robert (@neutrinoceros on GitHub) for the contributions: - `PR #434 `_. - `PR #442 `_. - `PR #443 `_. - `PR #445 `_. - `PR #448 `_. - `PR #455 `_. - `PR #456 `_. 2.9.5 (2023-02-22) ------------------ * Fix a regression where arrays elements with dtype ``'int8'`` would not compare to floats as intended. See `PR #371 `_. Thank you to Clément Robert (@neutrinoceros on GitHub) and Nathan Goldbaum (@ngoldbaum on GitHub) for the contribution. * Raise an error in case an array element is assigned to a new value with incompatible units. See `PR #375 `_ and `PR #376 `_. Thank you to Nathan Goldbaum (@ngoldbaum on GitHub) for the contribution. 2.9.4 (2023-02-06) ------------------ * Make ``unyt_quantity.from_string`` parse ints. See `PR #278 `_. Thank you to Nathan Goldbaum (@ngoldbaum on GitHub) for the contribution. * TST: migrate from tox-pyenv to tox-gh-actions #344 See `PR #344 `_. Thank you to Clément Robert (@neutrinoceros on GitHub) for the contribution. * Correctly test string comparison depending on numpy version #358 See `PR #358 `_. Thank you to Clément Robert (@neutrinoceros on GitHub) for the contribution. * Multiple fixes for ``unyt_quantity.from_string`` - fix a bug where ``unyt_quantity.from_string`` would drop part of the unit expression - fix a bug where ``unyt_quantity.from_string`` would choke on unit expressions starting with ``'*'`` or ``'/'`` - fix a bug where ``unyt_quantity.from_string`` would choke on space-separated unit expressions - fix roundtrip for ``unyt_quantity.from_string`` and ``unyt_quantity.to_string`` methods - simplify unit regexp (``'**/2'`` isn't a valid exponent) - fix a bug where malformed string input would be incorrectly parsed by ``unyt_quantity.from_string`` See `PR #362 `_. Thank you to Clément Robert (@neutrinoceros on GitHub) for the contribution, and to Chris Byrohl (@cbyrohl on GitHub) for the report. 2.9.3 (2022-12-07) ------------------ * Fix a future incompatibility with numpy 1.25 (unreleased) where comparing ``unyt_array`` objects to non-numeric objects (e.g. strings) would cause a crash. See `PR #333 `_. Thank you to Clément Robert (@neutrinoceros on GitHub) and Nathan Goldbaum (@ngoldbaum on GitHub) for the contribution. 2.9.2 (2022-07-20) ------------------ * Fix an issue where taking powers of units was backwards-incompatible with previous versions of ``unyt`` when the exponent is not zero. See `PR #249 `_. Thank you to Clément Robert (@neutrinoceros on GitHub) for the contribution. * The import time for ``unyt`` has been reduced by skipping version checking of other packages. See `PR #251 `_. Thank you to Clément Robert (@neutrinoceros on GitHub) for the contribution. 2.9.0 (2022-07-14) ------------------ * Dropped support for Python 3.6 and 3.7. * Added support for Python 3.8, 3.9 and 3.10. * Fix an issue where SI prefixes of the ``degC`` units would give incorrect values in conversions. See `PR #176 `_. Thank you to Lee Johnston (@l-johnston on GitHub) for the contribution. * Fix an issue when using ``matplotlib_support``, plot an empty unyt array, would result in an error when changing units. See `PR #180 `_. Thank you to Josh Borrow (@JBorrow on GitHub) for the contribution. * Fix an issue where units would be printed twice in formatted strings with an ``unyt_array`` embedded. See `PR #188 `_. Thank you to Clément Robert (@neutrinoceros on GitHub) for the contribution. * Add a method to parse a ``unyt_quantity`` from a string expression. See `PR #191 `_. Thank you to Clément Robert (@neutrinoceros on GitHub) for the contribution. * Fix an issue where a ``unyt_array`` with dtype int8 could not be converted to a different unit. See `PR #197 `_. Thank you to Clément Robert (@neutrinoceros on GitHub) for the contribution. * The import time for ``unyt`` has been reduced. See `PR #199 `_. Thank you to Clément Robert (@neutrinoceros on GitHub) for the contribution. * Fix an issue where taking an ``unyt_array`` or ``unyt_quantity`` to a zero power would retain the units of the original array or quantity instead of converting to a dimensionless array. See `PR #204 `_. Thank you to Josh Borrow (@JBorrow on GitHub) for the contribution. * Add support for coercing iterables of ``unyt_array`` objects with nonuniform dimensionally equivalent units to a single ``unyt_array``. See `PR #211 `_. Thank you to Nathan Goldbaum (@ngoldbaum on GitHub) for the contribution. * Add the civil engineering units ``pli``, ``plf``, ``psf``, ``kli``, ``klf``, and ``ksf``. See `PR #217 `_. Thank you to @osnippet on GitHub for the contribution. * Fix typos in constants and unit prefixes. See `PR #218 `_. Thank you to Clément Robert (@neutrinoceros on GitHub) for the contribution. * Fix an issue where multiplying a 1-element ``unyt_array`` would return a ``unyt_quantity``. See `PR #225 `_. Thank you to Clément Robert (@neutrinoceros on GitHub) for the contribution. * Add the Rydberg constant ``R_∞`` and unit ``Ry``, add the dimension ``angular_frequency`` and the unit ``rpm``, and increase the precision of Avogadro's number. See `PR #228 `_. * Fix an issue where ``np.divide.reduce`` would return incorrect units for ``unyt_array`` instances. See `PR #230 `_. Thank you to Kyle Oman (@kyleaoman on GitHub) for the contribution. 2.8.0 (2020-10-05) ------------------ * Dropped support for Python 3.5. * Add ``delta_degC`` and ``delta_degF`` units to support temperature difference arithmetic. See `PR #152 `_. Thank you to Lee Johnston (@l-johnston on GitHub) for the contribution. * Fix an issue where a subsequent load of the unit registry with units that are equal but not identical leads to a crash. See `PR #158 `_. Thank you to Matthew Turk (@matthewturk on GitHub) for the initial bug report and fix. * Add force unit ``kip`` and pressure unit ``psi``. Thank you to P. Talley (@otaithleigh on GitHub) for the contribution. See `PR #162 `_. * Fix an issue where arithmetic operations on units defined in different registries and having the conversion defined in one direction would lead to a crash. See `PR #164 `_. Thank you to Clément Robert (@neutrinoceros on GitHub) for the initial bug report and fix. 2.7.2 (2020-06-29) ------------------ * The ``unyt.returns`` and ``unyt.accepts`` decorators now work correctly for functions that accept or return data with dimensionless units. See `PR #146 `_. Thank you to Simon Schopferer (@simfinite on GitHub) for the initial bug report and fix. * Data used in the tests are packaged with the source distribution and ``unyt.test()`` is now itself run as part of unyt's continuous integration tests. See `PR #149 `_ and `PR #150 `_. Thank you to Miguel de Val-Borro (@migueldvb on GitHub) for the initial bug report and fix. * The ``degC`` and ``degF`` units now render as ``°C`` and ``°F`` by default, ``°C`` and ``°F`` are now recognized as valid unit names as well. Thank you to Lee Johnston (@l-johnston on GitHub) for the contribution. * Use a more canonical representation of the micro symbol when printing units with the micro prefix, avoiding issues with displaying unit names in Matplotlib plot labels. See `PR #153 `_. Thank you to Matthew Turk (@matthewturk on GitHub) for the bug report and fix. * Add more alternative spellings for solar units. See `PR #155 `_. Thank you to Clément Robert (@neutrinoceros on GitHub) for the initial bug report. 2.7.1 (2020-02-17) ------------------ * Fix compatibility with ``unyt_array`` subclasses that do not have the new ``name`` argument in their initializer. See `PR #140 `_. * Fix an issue where custom units added to a unit registry were not restored correctly when reloading a unit registry from a JSON or pickle representation. See `PR #140 `_. 2.7.0 (2020-02-06) ------------------ * The ``unyt_array`` and ``unyt_quantity`` classes now have a new, optional ``name`` attribute. The primary purpose of this attribute is to enable automatic generation of matplotlib plot labels. The ``name`` attribute is propagated through unit conversions and copies but is not propagated through mathematical operations. See `PR #129 `_ and the documentation for details. * Add support for the ``Neper`` and ``Bel`` units with logarithmic dimensions. This includes support for the ``decibel`` unit. Note that logarithmic units can only be used with other logarithmic units and must be applied and stripped manually. See `PR #133 `_ and `PR #134 `_. * Add support for the SI unit of inductance, ``H``. See `PR #135 `_. * Fix formatting of error message produced when raising a quantity to a power with units. See `PR #131 `_. Thank you to Lee Johnston (@l-johnston on GitHub) for all of the above contributions. * Fix incorrect unit metadata when loading a pickled array saved by ``yt.units``. See `PR #137 `_. 2.6.0 (2020-01-22) ------------------ * Matplotlib support is no longer enabled by importing ``unyt``. Instead, it is now necessary to use the ``unyt.matplotlib_support`` context manager in code where you want unyt to automatically generate plot labels. Enabling Matplotlib support by default in the previous release caused crashes in previously working code for some users so we have decided to make the plotting support optional. See the documentation for more details. We are sorry for introducing a new feature that broke some user's code. See `PR #126 `_. Thank you to Lee Johnston (@l-johnston on GitHub) for the contribution. * Updated the contribution guide to include more details about setting up multiple Python versions for the ``tox`` tests. 2.5.0 (2020-01-20) ------------------ * Importing unyt now registers unyt with Matplotlib's interface for handling units. See the `Matplotlib `_ and `unyt `_ documentation for more details. See `PR #122 `_ and `PR #124 `_. Thank you to Lee Johnston (@l-johnston on GitHub) for the contribution. * Updated the LaTeX formatting of solar units so they do not get rendered italicized. See `PR #120 `_. Thank you to Josh Borrow (@JBorrow on GitHub) for the contribution. * Reduce floating point round-off error when data are converted from integer to float dtypes. See `PR #119 `_. 2.4.1 (2020-01-10) ------------------ * Add support for the latest releases of h5py, sympy, NumPy, and PyTest. See `PR #115 `_. * Fix the hash implementation so that identical units cannot have distinct hashes. See `PR #114 `_ and `PR #117 `_. Thank you to Ben Kimock (@saethlin on GitHub) for the contribution. 2.4.0 (2019-10-25) ------------------ * Improve performance for creating quantities or small arrays via multiplication with a unit object. Creating an array or quantity from data that does not have a numeric dtype will now raise ``UnitOperationError`` instead of ``UnitDtypeError``, which has been removed. See `PR #111 `_. * Comparing data with units that have different dimensions using the ``==`` and ``!=`` operators will no longer raise an error. Other comparison operators will continue to raise errors. See `PR #109 `_. * Fixed a corner case in the implementation of ``clip``. See `PR #108 `_. Thank you to Matthew Turk (@matthewturk on GitHub) for the contribution. * Added ``%`` as a valid dimensionless unit with a value of ``0.01``, also available under the name ``percent``. See `PR #106 `_. Thank you to Thomas Hisch for the contribution. * Added ``bar`` to the default unit lookup table. See `PR #103 `_. Thank you to Thomas Hisch (@thisch on GitHub) for the contribution. 2.3.1 (2019-08-21) ------------------ * Added support for the ``clip`` ufunc added in NumPy 1.17. See `PR #102 `_. 2.3.0 (2019-08-14) ------------------ * Added ``unyt.dimensions.accepts`` and ``unyt.dimensions.returns``, decorators that can be used to ensure that data passed into a decorated function has units that are dimensionally consistent with the function's expected inputs. See `PR #98 `_. Thank you to Andrei Berceanu (@berceanu on GitHub) for the contribution. * Added ``unyt.allclose_units`` and improved documentation for writing tests for code that uses ``unyt``. This is a wrapper for ``numpy.allclose`` that also checks the units of the input arrays. See `PR #94 `_. Thank you to Andrei Berceanu (@berceanu on GitHub) for the contribution. 2.2.2 (2019-07-03) ------------------ * Fix erroneous conversions of E&M units to their "native" unit system, for example, converting Gauss to CGS units would return Tesla and converting Tesla to MKS units would return Gauss. See `PR #96 `_. 2.2.1 (2019-07-02) ------------------ * Add support for loading JSON unit registries saved by ``yt.units``. See `PR #93 `_. * Correct the value of the ``light_year`` unit. See `PR #93 `_. * It is now possible to define a ``UnitSystem`` object with a quantity. See `PR #86 `_. * Incorrect units for Planck units have been fixed. See `PR #85 `_. Thank you to Nathan Musoke (@musoke on GitHub) for the contribution. * Updated value of Newton's constant to latest CODATA value. See `PR #84 `_. 2.2.0 (2019-04-03) ------------------ * Several performance optimizations. This includes a slight change to the behavior of MKS/CGS E&M unit conversions that makes the conversion rules slightly more relaxed. See `PR #82 `_. 2.1.1 (2019-03-27) ------------------ * Fixed an issue with restoring unit registries from JSON output. See `PR #81 `_. 2.1.0 (2019-03-26) ------------------ This release includes a few minor new features and bugfixes for the 2.0.0 release. * Added support for the matmul ``@`` operator. See `PR #80 `_. * Allow defining unit systems using ``Unit`` instances instead of string unit names. See `PR #71 `_. Thank you to Josh Borrow (@JBorrow on GitHub) for the contribution. * Fix incorrect behavior when ``uhstack`` is called with the ``axis`` argument. See `PR #73 `_. * Add ``"rsun"``, ``"lsun"``, and ``"au"`` as alternate spellings for the ``"Rsun"``, ``"Lsun"``, and ``"AU"`` units. See `PR #77 `_. * Improvements for working with code unit systems. See `PR #78 `_. * Reduce impact of floating point round-off noise on unit comparisons. See `PR #79 `_. 2.0.0 (2019-03-08) ------------------ ``unyt`` 2.0.0 includes a number of exciting new features as well as some bugfixes. There are some small backwards incompatible changes in this release related to automatic unit simplification and handling of dtypes. Please see the release notes below for more details. If you are upgrading from ``unyt 1.x`` we suggest testing to make sure these changes do not significantly impact you. If you run into issues please let us know by `opening an issue on GitHub `_. * Dropped support for Python 2.7 and Python 3.4. Added support for Python 3.7. * Added ``Unit.simplify()``, which cancels pairs of terms in a unit expression that have inverse dimensions and made it so the results of ``unyt_array`` multiplication and division will automatically simplify units. This means operations that combine distinct dimensionally equivalent units will cancel in many situations. For example .. code-block:: >>> from unyt import kg, g >>> print((12 * kg) / (4 * g)) 3000.0 dimensionless older versions of ``unyt`` would have returned ``4.0 kg/g``. See `PR #58 `_ for more details. This change may cause the units of operations to have different, equivalent simplified units than they did with older versions of ``unyt``. * Added the ability to resolve non-canonical unit names to the equivalent canonical unit names. This means it is now possible to refer to a unit name using an alternative non-canonical unit name when importing the unit from the ``unyt`` namespace as well as when a unit name is passed as a string to ``unyt``. For example: .. code-block:: >>> from unyt import meter, second >>> data = 1000.0 * meter / second >>> data.to("kilometer/second") unyt_quantity(1., 'km/s') >>> data.to("metre/s") unyt_quantity(1000., 'm/s') The documentation now has a table of units recognized by ``unyt`` along with known alternative spellings for each unit. * Added support for unicode unit names, including ``μm`` for micrometer and ``Ω`` for ohm. See `PR #59 `_. * Substantially improved support for data that does not have a ``float64`` dtype. Rather than coercing all data to ``float64`` ``unyt`` will now preserve the dtype of data. Data that is not already a numpy array will be coerced to a dtype by calling ``np.array`` internally. Converting integer data to a new unit will convert the data to floats, if this causes a loss of precision then a warning message will be printed. See `PR #55 `_ for details. This change may cause data to be loaded into ``unyt`` with a different dtype. On Windows the default integer dtype is ``int32``, so data may begin to be recognized as ``int32`` or converted to ``float32`` where before it was interpreted as ``float64`` by default. * Unit registries are now associated with a unit system. This means that it's possible to create a unit registry that is associated with a non-MKS unit system so that conversions to "base" units will end up in that non-MKS system. For example: .. code-block:: >>> from unyt import UnitRegistry, unyt_quantity >>> ureg = UnitRegistry(unit_system="cgs") >>> data = unyt_quantity(12, "N", registry=ureg) >>> data.in_base() unyt_quantity(1200000., 'dyn') See `PR #62 `_ for details. * Added two new utility functions, ``unyt.unit_systems.add_constants`` and ``unyt.unit_systems.add_symbols`` that can populate a namespace with a set of unit symbols in the same way that the top-level ``unyt`` namespace is populated. For example, the author of a library making use of ``unyt`` could create an object that users can use to access unit data like this: .. code-block:: >>> from unyt.unit_systems import add_symbols >>> from unyt.unit_registry import UnitRegistry >>> class UnitContainer: ... def __init__(self): ... add_symbols(vars(self), registry=UnitRegistry()) ... >>> units = UnitContainer() >>> units.kilometer km >>> units.microsecond μs See `PR #68 `_. * The ``unyt`` codebase is now automatically formatted by `black `_. See `PR #57 `_. * Add missing "microsecond" name from top-level ``unyt`` namespace. See `PR #48 `_. * Add support for ``numpy.argsort`` by defining ``unyt_array.argsort``. See `PR #52 `_. * Add Farad unit and fix issues with conversions between MKS and CGS electromagnetic units. See `PR #54 `_. * Fixed incorrect conversions between inverse velocities and ``statohm``. See `PR #61 `_. * Fixed issues with installing ``unyt`` from source with newer versions of ``pip``. See `PR #63 `_. * Fixed bug when using ``define_unit`` that caused crashes when using a custom unit registry. Thank you to Bili Dong (@qobilidob on GitHub) for the pull request. See `PR #64 `_. We would also like to thank Daniel Gomez (@dangom), Britton Smith (@brittonsmith), Lee Johnston (@l-johnston), Meagan Lang (@langmm), Eric Chen (@ericchen), Justin Gilmer (@justinGilmer), and Andy Perez (@sharkweek) for reporting issues. 1.0.7 (2018-08-13) ------------------ Trigger zenodo archiving. 1.0.6 (2018-08-13) ------------------ Minor paper updates to finalize JOSS submission. 1.0.5 (2018-08-03) ------------------ ``unyt`` 1.0.5 includes changes that reflect the peew review process for the JOSS method paper. The peer reviewers were Stuart Mumfork (`@cadair `_), Trevor Bekolay (`@tbekolay `_), and Yan Grange (`@ygrange `_). The editor was Kyle Niemeyer (`@kyleniemeyer `_). The ``unyt`` development team thank our reviewers and editor for their help getting the ``unyt`` paper out the door as well as for the numerous comments and suggestions that improved the paper and package as a whole. In addition we'd like to thank Mike Zingale, Meagan Lang, Maksin Ratkin, DougAJ4, Ma Jianjun, Paul Ivanov, and Stephan Hoyer for reporting issues. * Added docstrings for the custom exception classes defined by ``unyt``. See `PR #44 `_. * Added improved documentation to the contributor guide on how to run the tests and what the PR review guidelines are. See `PR #43 `_. * Updates to the text of the method paper in response to reviewer suggestions. See `PR #42 `_. * It is now possible to run the tests on an installed copy of ``unyt`` by executing ``unyt.test()``. See `PR #41 `_. * Minor edit to LICENSE file so GitHub recognizes it. See `PR #40 `_. Thank you to Kyle Sunden (`@ksunden `_) for the contribution. * Add spatial frequency as a dimension and added support in the ``spectral`` equivalence for the spatial frequency dimension. See `PR #38 `_ Thank you to Kyle Sunden (`@ksunden `_) for the contribution. * Add support for Python 3.7. See `PR #37 `_. * Importing ``unyt`` will now fail if ``numpy`` and ``sympy`` are not installed. See `PR #35 `_ * Testing whether a unit name is contained in a unit registry using the Python ``in`` keyword will now work correctly for all unit names. See `PR #31 `_. * The aliases for megagram in the top-level unyt namespace were incorrectly set to reference kilogram and now have the correct value. See `PR #29 `_. * Make it possible to take scalars to dimensionless array powers with a properly broadcasted result without raising an error about units. See `PR #23 `_. * Whether or not a unit is allowed to be SI-prefixable (for example, meter is SI-prefixable to form centimeter, kilometer, and many other units) is now stored as metadata in the unit registry rather than as global state inside ``unyt``. See `PR #21 `_. * Made adjustments to the rules for converting between CGS and MKS E&M units so that errors are only raised when going between unit systems and not merely when doing a complicated unit conversion involving E&M units. See `PR #20 `_. * ``round(q)`` where ``q`` is a ``unyt_quantity`` instance will no longer raise an error and will now return the nearest rounded float. See `PR #19 `_. * Fixed a typo in the readme. Thank you to Paul Ivanov (`@ivanov `_) for `the fix `_. * Added smoot as a unit. See `PR #14 `_. 1.0.4 (2018-06-08) ------------------ * Expand installation instructions * Mention paper and arxiv submission in the readme. 1.0.3 (2018-06-06) ------------------ * Fix readme rendering on pypi 1.0.2 (2018-06-06) ------------------ * Added a paper to be submitted to the Journal of Open Source Software. * Tweaks for the readme 1.0.1 (2018-05-24) ------------------ * Don't use setup_requires in setup.py 1.0.0 (2018-05-24) ------------------ * First release on PyPI. * unyt began life as a submodule of yt named yt.units. * It was separated from yt.units as its own package in 2018. unyt-3.0.4/LICENSE000066400000000000000000000027611476461141700135430ustar00rootroot00000000000000 BSD 3-Clause License Copyright (c) 2018-, yt Development Team All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. unyt-3.0.4/MANIFEST.in000066400000000000000000000004171476461141700142700ustar00rootroot00000000000000include AUTHORS.rst include CONTRIBUTING.rst include HISTORY.rst include LICENSE include README.rst recursive-exclude * __pycache__ recursive-exclude * *.py[co] recursive-include docs *.rst conf.py Makefile make.bat *.jpg *.png *.gif recursive-include unyt/tests/ *.py unyt-3.0.4/README.rst000066400000000000000000000072671476461141700142330ustar00rootroot00000000000000==== unyt ==== .. image:: https://img.shields.io/pypi/v/unyt.svg :target: https://pypi.python.org/pypi/unyt .. image:: https://img.shields.io/conda/vn/conda-forge/unyt.svg :target: https://anaconda.org/conda-forge/unyt :alt: conda-forge .. image:: https://github.com/yt-project/unyt/actions/workflows/ci.yml/badge.svg?branch=main :target: https://github.com/yt-project/unyt/actions/workflows/ci.yml .. image:: https://github.com/yt-project/unyt/actions/workflows/bleeding-edge.yaml/badge.svg?branch=main :target: https://github.com/yt-project/unyt/actions/workflows/bleeding-edge.yaml .. image:: https://readthedocs.org/projects/unyt/badge/?version=latest :target: https://unyt.readthedocs.io/en/latest/?badge=latest :alt: Documentation Status .. image:: http://joss.theoj.org/papers/dbc27acb614dd33eb02b029ef20e7fe7/status.svg :target: http://joss.theoj.org/papers/dbc27acb614dd33eb02b029ef20e7fe7 :alt: Code Paper | .. image:: https://raw.githubusercontent.com/yt-project/unyt/master/docs/_static/yt_logo_small.png :target: https://yt-project.org :alt: The yt Project A package for handling numpy arrays with units. Often writing code that deals with data that has units can be confusing. A function might return an array but at least with plain NumPy arrays, there is no way to easily tell what the units of the data are without somehow knowing *a priori*. The ``unyt`` package (pronounced like "unit") provides a subclass of NumPy's ``ndarray`` class that knows about units. For example, one could do: >>> import unyt as u >>> distance_traveled = [3.4, 5.8, 7.2] * u.mile >>> print(distance_traveled.to('km')) [ 5.4717696 9.3341952 11.5872768] km And a whole lot more! See `the documentation `_ for installation instructions, more examples, and full API reference. This package only depends on ``numpy`` and ``sympy``. Notably, it does *not* depend on ``yt`` and it is written in pure Python. Code of Conduct --------------- The ``unyt`` package is part of `The yt Project `_. Participating in ``unyt`` development therefore happens under the auspices of the `yt community code of conduct `_. If for any reason you feel that the code of conduct has been violated, please send an e-mail to confidential@yt-project.org with details describing the incident. All emails sent to this address will be treated with the strictest confidence by an individual who does not normally participate in yt development. License ------- The unyt package is licensed under the BSD 3-clause license. Citation -------- If you make use of unyt in work that leads to a publication we would appreciate a mention in the text of the paper or in the acknowledgements along with a citation to our `paper `_ in the Journal of Open Source Software. You can use the following BibTeX:: @article{Goldbaum2018, doi = {10.21105/joss.00809}, url = {https://doi.org/10.21105/joss.00809}, year = {2018}, month = {aug}, publisher = {The Open Journal}, volume = {3}, number = {28}, pages = {809}, author = {Nathan J. Goldbaum and John A. ZuHone and Matthew J. Turk and Kacper Kowalik and Anna L. Rosen}, title = {unyt: Handle, manipulate, and convert data with units in Python}, journal = {Journal of Open Source Software} } Or the following citation format: Goldbaum et al., (2018). unyt: Handle, manipulate, and convert data with units in Python . Journal of Open Source Software, 3(28), 809, https://doi.org/10.21105/joss.00809 unyt-3.0.4/benchmarks/000077500000000000000000000000001476461141700146455ustar00rootroot00000000000000unyt-3.0.4/benchmarks/README.txt000066400000000000000000000002331476461141700163410ustar00rootroot00000000000000The data for a benchmark performed with unyt 1.0.1, Pint 0.8.1 and astropy 3.0.1 are available on figshare: https://doi.org/10.6084/m9.figshare.6399875.v1 unyt-3.0.4/benchmarks/bench.py000066400000000000000000000167601476461141700163100ustar00rootroot00000000000000import matplotlib matplotlib.use("agg") from collections import OrderedDict import contextlib import io from matplotlib import pyplot as plt import numpy as np import os import pyperf import subprocess import sys @contextlib.contextmanager def stdoutIO(stdout=None): old = sys.stdout if stdout is None: stdout = io.StringIO() sys.stdout = stdout yield stdout sys.stdout = old def run_perf(args, json_name): if os.path.exists(json_name): return args = args + ["-o", json_name] print(args) p = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE) out, err = p.communicate() print(out.decode()) print(err.decode()) def make_plot(extension): ratios = OrderedDict() stddevs = OrderedDict() benchmarks = OrderedDict() np_bench = pyperf.Benchmark.load(open("{}_{}".format("numpy", extension))) np_mean = np_bench.mean() np_stddev = np_bench.stdev() for package in setup: if package == "numpy": continue benchmarks[package] = pyperf.Benchmark.load(open(f"{package}_{extension}")) mean = benchmarks[package].mean() stddev = benchmarks[package].stdev() ratios[package] = mean / np_mean stddevs[package] = ratios[package] * np.sqrt( (np_stddev / np_mean) ** 2 + (stddev / mean) ** 2 ) fig, ax = plt.subplots() packages = list(ratios.keys()) ax.bar(packages, ratios.values(), yerr=stddevs.values()) fig.suptitle(extension.replace(".json", "").replace("_", " ").title()) ax.set_ylabel("numpy overhead (x time for numpy); lower is better") plt.savefig(extension.replace(".json", ".png")) plt.close(fig) if ratios["unyt"] != min(ratios.values()): rvalues = list(ratios.values()) svalues = list(stddevs.values()) unyt_index = packages.index("unyt") min_index = rvalues.index(min(rvalues)) if ratios["unyt"] > 3 * svalues[min_index] + rvalues[min_index]: for package in ratios: script = get_script(benchmarks, package) with stdoutIO() as s: exec(script) res = s.getvalue().replace("\n", "") print( "{}: {} +- {} ({})".format( package, ratios[package], stddevs[package], res ) ) print(get_script(benchmarks, "unyt")) def get_script(benchmarks, package): meta = benchmarks[package].get_metadata() setup_s = meta["timeit_setup"][1:-1] bench_s = "print(" + meta["timeit_stmt"][1:-1] + ")" script = setup_s + "; " + bench_s script = script.replace("; ", "\n") return script setup = OrderedDict( [ ("numpy", "import numpy as np"), ("pint", "from pint import UnitRegistry; u = UnitRegistry()"), ("astropy", "import astropy.units as u"), ("unyt", "import unyt as u"), ("quantities", "import quantities as u"), ] ) base_args = ["python", "-m", "pyperf", "timeit"] shared_setup = "import numpy as np; import operator" base_setups = OrderedDict( [ ("small_list", "data = [1., 2., 3.]"), ("small_tuple", "data = (1., 2., 3.)"), ("small_array", "data = np.array([1., 2., 3.])"), ("big_list", "data = (np.arange(1e6)+1).tolist()"), ("big_array", "data = (np.arange(1e6)+1)"), ] ) op_ufuncs = OrderedDict( [ ("operator.add", "np.add"), ("operator.sub", "np.subtract"), ("operator.mul", "np.multiply"), ("operator.truediv", "np.true_divide"), ("operator.eq", "np.equal"), ] ) for bs in base_setups: for package in sorted(setup): print(package) setup_s = "; ".join([shared_setup, setup[package], base_setups[bs]]) args = base_args + ["-s", setup_s + " "] if package == "numpy": args.append("np.array(data)") else: args.append("data*u.g") json_name = f"{package}_{bs}_create.json" run_perf(args, json_name) if "list" in bs or "tuple" in bs: continue args = base_args + ["-s", setup_s + "; data=np.asarray(data); out=data.copy()"] if package == "numpy": args[-1] += "; " else: if package != "pint": args[-1] += "*u.g" args[-1] += "; data = data*u.g " args.append("data**2") json_name = f"{package}_{bs}_square.json" run_perf(args, json_name) args[-1] = "np.power(data, 2)" json_name = f"{package}_{bs}_npsquare.json" run_perf(args, json_name) args[-1] = "np.power(data, 2, out=out)" json_name = f"{package}_{bs}_npsquareout.json" run_perf(args, json_name) args[-1] = "data**0.5" json_name = f"{package}_{bs}_sqrt.json" run_perf(args, json_name) args[-1] = "np.sqrt(data)" json_name = f"{package}_{bs}_npsqrt.json" run_perf(args, json_name) args[-1] = "np.sqrt(data, out=out)" json_name = f"{package}_{bs}_npsqrtout.json" run_perf(args, json_name) make_plot(f"{bs}_create.json") if "list" not in bs and "tuple" not in bs: make_plot(f"{bs}_square.json") make_plot(f"{bs}_npsquare.json") make_plot(f"{bs}_npsquareout.json") make_plot(f"{bs}_sqrt.json") make_plot(f"{bs}_npsqrt.json") make_plot(f"{bs}_npsqrtout.json") for bs in base_setups: if "list" in bs or "tuple" in bs: continue for op, ufunc in op_ufuncs.items(): for bench, bench_name in [ (op + r"(data1, data2)", op + "12.json"), (op + r"(data2, data1)", op + "21.json"), (ufunc + r"(data1, data2)", ufunc + "12.json"), (ufunc + r"(data2, data1)", ufunc + "21.json"), (ufunc + r"(data1, data2, out=out)", ufunc + "12out.json"), (ufunc + r"(data2, data1, out=out)", ufunc + "21out.json"), ]: for unit_choice in [("g", "g"), ("kg", "g")]: for package in sorted(setup): print(package) setup_s = ( "; ".join([shared_setup, setup[package], base_setups[bs]]) + "; " ) if "out" in bench: if package not in ("pint", "numpy") and "equal" not in bench: setup_s += f"out=data*u.{unit_choice[0]}; " else: setup_s += "out=np.array(data); " if package == "numpy": setup_s += "; ".join( [r"data1 = np.array(data)", r"data2 = np.array(data)"] ) if unit_choice[0] != unit_choice[1]: _bench = bench.replace("data1", ".001*data1") else: setup_s += "; ".join( [ f"data1 = data*u.{unit_choice[0]}", f"data2 = data*u.{unit_choice[1]}", ] ) _bench = bench args = base_args + ["-s", setup_s + " "] json_name = "{}_{}_{}{}".format( package, bs, unit_choice[0], unit_choice[1] ) run_perf(args + [_bench], json_name + "_" + bench_name) make_plot(f"{bs}_{unit_choice[0]}{unit_choice[1]}_{bench_name}") unyt-3.0.4/codemeta.json000066400000000000000000000030321476461141700152020ustar00rootroot00000000000000{ "@context": "https://raw.githubusercontent.com/codemeta/codemeta/master/codemeta.jsonld", "@type": "Code", "author": [ { "@id": "0000-0001-5557-267X", "@type": "Person", "email": "ngoldbau@illinois.edu", "name": "Nathan Goldbaum", "affiliation": "University of Illinois at Urbana Champaign" }, { "@id": "0000-0003-3175-2347", "@type": "Person", "email": "jzuhone@cfa.harvard.edu", "name": "John ZuHone", "affiliation": "Harvard-Smithsonian Center for Astrophysics" }, { "@id": "0000-0002-5294-0198", "@type": "Person", "email": "mjturk@illinois.edu", "name": "Matthew Turk", "affiliation": "University of Illinois at Urbana Champaign" }, { "@id": "0000-0003-1709-3744", "@type": "Person", "email": "kowalikk@illinois.edu", "name": "Kacper Kowalik", "affiliation": "University of Illinois at Urbana Champaign" }, { "@id": "0000-0003-4423-0660", "@type": "Person", "email": "anna.rosen@cfa.harvard.edu", "name": "Anna Rosen", "affiliation": "Harvard-Smithsonian Center for Astrophysics" } ], "identifier": "", "codeRepository": "https://github.com/yt-project/unyt", "datePublished": "2018-08-13", "dateModified": "2018-08-13", "dateCreated": "2018-08-13", "description": "Handle, manipulate, and convert data with units in Python", "keywords": "units, python, numpy, sympy, quantities", "license": "BSD", "title": "unyt", "version": "v1.0.7" } unyt-3.0.4/docs/000077500000000000000000000000001476461141700134605ustar00rootroot00000000000000unyt-3.0.4/docs/Makefile000066400000000000000000000012171476461141700151210ustar00rootroot00000000000000# Minimal makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = -W SPHINXBUILD = python -msphinx SPHINXPROJ = unyt SOURCEDIR = . 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). .DEFAULT: @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) clean: -rm -rf $(BUILDDIR)/* -rm -rf modules/ unyt-3.0.4/docs/_static/000077500000000000000000000000001476461141700151065ustar00rootroot00000000000000unyt-3.0.4/docs/_static/mpl_fig1.png000066400000000000000000000405131476461141700173150ustar00rootroot00000000000000PNG  IHDRh7( sBIT|dsRGBgAMA a pHYs+8tEXtSoftwarematplotlib version3.1.2, http://matplotlib.org/%J@IDATx^|g+FHPBBRjU{5U^#Rڭo !%BD" nO598;s{<&&&طo3DDdh ,w}ÓXbюPk}vdxpzzvH8 HDDDz&""00LDD `"""=`QJLNCd\vw1(D'ײ?o1&&kg&0?Dal?uKPA71HFd\ޮ|&&Z5ob΄F%bj_LV -Pxݐ } ""Cq,3rh# ljU^ C?iHIIA"EP^=u>11/^D\\Pxqu^ݻWm/LMMaee*U _uو900jc}aÆ077d `""JKπZ?4^+5]״~[SUHΞ=;wČ3p!̟?#GT رڳPBؼy3\\\}vU"ۤITPe˖Err2f͚O&"ҝxLrvm /jU_-SH WZ&@2eʨV!CЬY31BUF L4 ZYZ/LDBci7nIwjU5~IXEETT=9s୷ނ- .jҭ?wBBBBN~jR`www$%=0lٲE={ԿMDDݔ4L LًJ1,耀L:K,A6m<=yd >\;zŋqx{{ZaׯW!aÆDD$⣘7]*ahWT az]reZJ늾}]ҥ`iix5yYV^+V`ڙ'c4ѫq00D8,[C\h.nݓwjGn"e +]̗/_VfUU///TTI;"""]xI 7t5:m7.]RAz5c$^>vJ, WWW5J.Irܻwo;ǏGϞ=.orssj%' m--ga ߻t3z?oUÔ5񚞖4s7Ԭg|}}9 - 2Ju+-a,g`̌﯂XfYWZU"#מk]*60 `"l?[Ϣ5V jC?,h""2\nG1uy|ѭ~4W1` PM\4xAx-0ۉ)p_wo~wu+ S5 `""rD,</cT7֯YDD8}<[Xyj0Q8M=TЪ+ `"Z'W.ЙfZ N (PڴiI6m}?,0o<|8wva055رcQxq|7ԪOg Dd@DǟWbѫ~Tfbi L2ZHKKn=УGj S-\G fffbŊ UqF@Rt ÌuQ}3ǷU p׮]U`JK֭[Z;?ĉ߫ѽ{wb_m}ܼySNcq!3V<=:f9KJB,-Yi}v ŊVW˟K+799)))ڙ{dԄ/_^~<& CqH8eЙ1i˸L2+BfDxt5>j*$&&"<<={T , 3Fݷzj2ܹsL<nnn^kҰs 4U4%x]]]akkf)Ĩ>>>R#dԩSOV-_UTQ_tR5JH(\iI{{{5j C91ܴ2\8q7$""=:W">9 Vǘ8yq7$""Wf DׅG/ ~ՔǰLDd%o4*"&"54n?B}o&"!m>ˎ!6!Ll 7g[ByH.G5;brdUTR^&"ҡuǯCvk4Ƭu u `""3V4?qQJe,h"WZ,x/GcB[;LZKЫYDDt߮CsKc1^/G%vZMD$ $:nM+baZt]DDyF4t܀KύLD/ W4>շ*D&"zAFݯG+ kU&"zÈÚ`JZ0Ӥ-gњ?Q.g+9XkYDDA|xl_Z􉳠&P=0SesC/2 `"'/YrahWlXA~Vu[n!={6v܉3fСC?>F/?-Z7Pz XBg'ۤITPe˖Err2f͚g/&~w]Ea`V+U(c?pUjbb )SFb f͚aĈ.-吐T|o ~\r*Dǎ}vXYYigZ{ 65KcQ\V!C`:.Z(pQ̙3olmmQpaU˗/jf)X uOg|ZT+IIIZaaaaزe ={CN8r 1o2|)|V@@N%KM6{k :T;ŋqx{{ZaҢ^~ 6LQ^0 6qXZV:۷/wAg֪t?_zUtejxڙ'c4QV7d~8l@, )õ[Hݻw{]'~ Ǐ _b|j5XqttVPR%툈aax}̯g(uF0|It7nH.Av`uiq1,Y/Ռfo:u%K;wqq~~~&777X[[#>>^}Ǐ-`E&Y}?*9وNX.#Yrys2ZfjjliHj2J.Ck{ըQC}? YUV}4 `o8s-/5Ud<LdDeǐ|T*dL KQQ]t%M}TS/Z `"2 Iiy o=wvS#@[ ޮ3'}o#^ HO3hEt]tMd0Dd0'?T;F])J1 b|? ?ŋjU"&"r4RF5Ő79˙ 'ah5jvWko EEDcO_C[qfiMÜdDM|FjUk *h"&\)6!ݗUc+#fYKJd8LD!?3ߡn ѱV!zcDD,'9sul,r _2V `"$7_G6CLFLDz{@8S10)s3KƍLDzs75 9TWQ.dU'a^8M+a@'31ѪDIXDD/h1 >~pk<LD9}I|/U.]]ѡV![D#$ØuCVƯ#\=&"(611QLu,!ܨQ#9Rũorr:>~8F}hѢŰapQaӦM6m{< p"zn'rӨ*%v^%?͛"--MuϩSPti*?XYY+VDhh7൰@R"҅d؏y.հ _ywUq>}p-lq޽{w}{ܼySѫ%?9fUItSN:Xb*j?rttDxxvDGG֫[Y]2\|yuYrǝ%c//c\ZV%i{zz",, )))jsj2#Z|q^kkkL4IСCѢE u>((cƌQ^5w\ғ'OD3.ǩM<c|j3X֪D,: ` ^WWWڪY111궏K,A:upi5.\@@@0`O'&&"88XJKpwwW(g8m|ݥg9 EDV\ =ٰJVhEa `"z.n܆/|·cv?'鏡|G.HHJ)m1u5/Kbݼ}C~Z/~==*0תDKMG?KQah# njU gAAIO=(T ?g(k{p  c;fnvD3+{4lPknnJ*eg""2 gb:s?9=`vEʹ*崧r:u*Z**Tm(wJNM×qZř)ocLjZ婓N8BTRڙ=y֭ZƆ(/ !?aMiTևYAK'a999~n֬/Q^}1zc n D3ǀӎINNF͚5??_Dѧ›GJzn uU(xfIXGxo6mT/^X#!(2ì xOh͹$Qn\ q\x۷ǪU0a51kݺuZհp վ/`fx~yBd~!P .] ggg(PK,+WT+)5S~=GbpuVg/xf xРA8{,,--U.]iii`xzzj4 l1 q7ƶN, ԪDϟQQQWeUzz:KO…QdIU3 `22˹ǒs7sz*kԩS011Aڵ3E9oUܥH8UHԫWOFѢEQN,w^[VVVR {[.-pY$""B}R\&Cwn sDZX> e- kUèxϞ=Xp ƍV7pIE;vn֭Z~Hgxs0|p5JiԒ ͛۷k &MEBʖ-.5klll{< Yx|napJ߻nYKyQo>/pT}Q$..Nճ+$&&\v eʔQRF ur~3H˻cǎ*"dvBHT<pBF QdnݺرcE{nZZVYZҊٳ:2d\W^8s ,.]`̙m[ @xxV%dakPT*Q|?2%Բ\Զm[1a)-`iYg7yd582n,ȥP,Uc˖-ذacC( v.:#kx/y 8;AijWZOj'If_gYz5VXkg]d(6ӍpV<7o?~8\]]ձֲLr˗/Z2:::Z^^^jb"cN|o8} s0|N[ƍåKT$+{{{,X@͂nڴ%]Æ 銖_|ջwouNHHK O]kkkJO@2ۺrg_0f'0q?ޭѭr#'$˕uptt*fe 뀥u+٩SY$,3 cږ_`Sn5yf>G'_a@D `&2> 7ޗͩĨX`D,~YDdbi v4̙KG0doXAcL ?0QKNMCGklK#z^Wtt,U(` h8cjWZN"!a/JHOqZ N" ?Ðx 1:3|LK#a7֟~NXA#.X@Q^&ґuǯ#M1EǗwWZL"z,}KkMk*= `WJp|ݹ&VjĵDqDI–1cMBDO %%$ˢ G;k6V!"]eHDy#t szasc _<:t{B 8*D&cnE1~it[Qs;3|1^lnax]JX> hU"&zN\D}p;1ݛa`JZq "0bs>jUH8Ldds۹pj fpd+&z9 DG ]Dl^vA=":>m=z/5Ck"MMdDvיqV%"i=p:]HHT+"44T~Cm۾sWC&3֕KD9JܵkWĽ{VAY@OҝbĉU޽;/;w͛Zkh6 m>16hPV%":`iJSN%&N>}ZGb@*U~MNNFJJvܺuKՄ/_^~kKpm3mBbr*<0|`0`}/5X`hWiTQ~&#|)yV[Q^.hh0{P҈KDLC+ -ѡvYBDdx,o.8yk77ժDDL}|og`pgkS]60Zןj{ {ƺ])BDd8 r#0x $bFZٲV!"z^ !Ddk|E7k :)UIA~GF Ʒv5 ѓ %,:p fCe2ܙKDy#O8V(,g"SFfo#OhZ(o`SvG.>bMmdS|fBD0)GcJ_LzUKa4-U΂瑒"E^zZ Epp0-:uhgK.͛5kR<@DDDaÆ077d?߫8j"u[n!IJHΞ=ׯ_ sa5jAho 7n`Ŋ8; &MB ([,1k,hx2pI.;| c֝M1LV5JkU"~8*511QwLL u⫯BbbϵkPLrCr0c u%22;vaee}> AXsWP(w0"Wu:,QQQ8z(̙6mڨ.={>S)\j~$lZm=/z 0 i""2s\F$:] 8::j{c2:`iJliHƊe\${ըQC}? cZ~q'1: ~!Фfu +BZHdk[WFDd&h0M؎I8Ekc%",N>\}=o`h4RBQn>x j׮rʡI&*)SyWWW=III֭/9shg)""ʖ-ŋkg4_rSM,_/U(icڵr %fΜ5k =z@RvVUVaĉZXl8{,<<<`jd~.(V} _SSIX&&&Sc8::K֭[ѰaC)R1j(۷E>Æ ѣGaffM6aڴid* ѝ| C^77ժDDy 'a=͛ުU|ƍ+jԨP՚0"pHHT+7൰xmHNĨ5~>/:c& _""ڵ L ޽{,PV}W8޽>sn޼U ԝ?wܼ}mkW8˙Ƞ<˧Z:uR-a5q=Ƃ˗/kghznݺU-c#{_D|>rW -Q䃮x""2\: `OOO!%%۶mS#Ft- f EP%-ckkkL4I:t(Zh0f$$$zjܹsUHO<nnnڿjDa,7 O,U4%x2#[[[5K9&&F̈́OгgO2K:{ TX ӧ󉉉V+c^^^jLj1HO}=(?Lo\hU""2\2rk}bLj}u`q }48vzlO޶*DD8:EO)aT*a_f `=ˊ/3v&s MDDƉ'\T k:{;b(b|GcHNKC̎Z p Ca.`xfߒ(΂!Gk =o.""z<΂JIK׿ŷP %"".q=m2[KkU""#\nKvb !.  삦' y>.oi%"`BWnţ#X|>jQE-QxJDDsyH|? G>nODD~I'?Y~D~+jU""cmQp8Tǡq-P4V%""z2΂ O!82ǸV!"Yy̝)I {13×^5~8}۞y3,̴*cBT8VjkgB9KDDǀ q ^Ht-]HJJ믿{{{rٳgq-TRۢ/22 ,|@_ϒwLL LLL-[j'c4q0sQԩS9r$>CU=z Z `L6 8qkXXX 556lܹs;|AX, `""~K.533S-2e >>%J`˖-Xx1vܩgiڴ)vڅʕQѣի^y> `""`:e _!]ҪciJ+VLcǎƍ8˼yP^677WӶ* S}vٳ *DDD#! 3gD.]ԹΝ;_ 3gơC [i1/]TQ=QB}3iUd}4F،唝?G4ly5<1 <6y؟| |dns5kjGG&c)H]_ܘsAÖ^σcDDDz.h"""=`ǀ_ƍMrZZI6Xn~GuͶ)U6YdJYfY$#-zuUdoWEFmH~u,,aY+}j3YlٲnhdZ\Ee1,&jk(de˖KqzJ*i'1+WPBݤ&d+V eW߬]V. xYH~nԨvx.\V%[!7fyS ZdݩzI6ׯV1L ~I"0V\9` l@ҭ[7I,;LܖMMvkhdYC\ިK,VדE|ĤIƍ ҳgϪضmZi;6X<ש}+7Eda]͟?%U*\9͛BޏbM!cZň$,z>- ???u; <#E6Anddۢ@/ȰDEEedz3-ZѱcGu. ;cȐ!궐U#Ñ!"#3x22z.8p@=,32CX;2駟jG5Z궼}֖:6&LOǿOu[oRR̆@Fbb:έ=RFB1e~`T8Բ#_,yݡ,&S+HwP%kp˧U%Xlh2СW'qYUmڴQooo_'NT-^@zKV.i|vd8+kַo_ԩ^V`YK5ף,ӛE^{sܹs]+ ҥӮYrS{!sn=YkZkD&vMDBJnu!/y\ TBƜY/Ruxyeqc7,2k jJt2'Oޜ՛J<60}^^wL>^Y[~6xH7Eּ6`?'yKI܄ɓ?&J*jǪcǪci)ʟ6! mqNY2P~O}͛UkW&dlihCIʕZaaa꼐&MS8sLبհ޸w-GCH'+G0-G꜐`'c`&@0=|!d8::f:uJ&"OŋgdmoooQ~}u^&gY5w܌ hGjrL7hg Kf6#+#*FVP2[ 8p`82!#3<Ī&"3[Rθ|:oU^oY5w^U{n߾}jV3~팈d801zxxdd~W jժe)RD<.\3.EIDD&""00LDD `"""=`̙3G8jYkr0;}7ZrlMHD͞=[-'Ev9tZP%wP&܃Ldd;:Y9kCY ˖-ɓo&۷O&܁Ld&1b^w:/ܾ}[&܁Ld$|e7-&Mqơ]vjW2>,~D;0 LV{kٲYBXZ窐ֲ0 `"&)RDm/n݊sΩ }6qҲ. `"뀅LիژZjXh:/?X&܁8y {ؼyva7nP=a/^\;KD&2+Z&Z=*55wUD{0cDDDz&""00LDD2PhNIENDB`unyt-3.0.4/docs/_static/mpl_fig2.png000066400000000000000000000362311476461141700173200ustar00rootroot00000000000000PNG  IHDRh7( sBIT|dsRGBgAMA a pHYs+8tEXtSoftwarematplotlib version3.1.2, http://matplotlib.org/%J;IDATx^\UqlQ_5MΩ3Ys֌)f &D\k*pÇ{{aý98@DDDI*DDDF&""200LDDd `"""#`DDDF&""200LDDd `"""#`DDDF&""200LDDd `"""#`DDDF&""200LDDdV>|8 . ::ZsΡcǎȝ;7֭K.i̙3pvvF-%""kfB テb޽jyxxN>ڵkƍpssC%ʓҥ J,DDD6:=9t KOOOLRc^`ggH`ګMLp UVE>}%DDdz 8**J}Q|'Xr%֭[4i]ޅ?~BXY;NK, /bȞ=:rn߾^SBԶOyfT ;wV?KDD:V)RP+_}ѢEw/%;8Ԇ/yAN`888hKߟ9sӑfϞԩSFDDɪgA 'a Zf)JJM"""$[K*aÆiK̏Zޱc|EDDI^غuwd1ӀPLמ'+ rή\YǵGƌ'YDDd2dȠoL"0 6k.uE.z쩞A.O0'au{Fbs 9rx;wڵko>lڴ ӧǔ)S*i3jK`fɒE]uc۷oWX|ʍ ւ왛7ob꒑[lQ7X ""26\Jg̘pSNlٲi'߿sbtSK` TZꊐe\&R~--[ZW}K9ʬɘgrذ&o޼>2L,c7o'`I0Z=Z|>\#4ik6A<<<ń ԍ́XN%SҥKZr iJx{ y3gά^#߅pvvVbíLIZ-#+\tIݝHƋ .]Z4$""`sȸ"bl>lRύzY(|::FHD:1(I^-Էρ_<0(qUَGQ B\Z0(Fua^;א(%VnEDD.>ӾXeUtu,U'a='6N߶\D.Ur\40(\sbઓآ 6{`b4%h1}/FbBWVI&""8:מF[QˎrCW **YDDd"cb1bi4o.F_K&":w!7k?lQmb]oG˜F҇`4] £b1.'.h""(#8jO؃ 3&&LDDO C0o//j!wv9'0)N݂T/x>n0CBLDdt0OߋLv +ЪXDDVl@\f_1YiT)x9$YDDVj)=xA9y,h""2+a1(؏r2fLDdE{ǞСV,v10ɛh: n`ٗ0QIJ\RRcYCpKfǕ1h[)V%caY֮^ϱι4Vil DDjN]blZLDEǢ}0sGԖhQJLDdA>@㩞|ƵqNZL /ADd!s=!8" {EBYe8ȨâiA\|uJeGLDdv^fӽQRK{T幽fLDd:x gǍGXC2JLDdfn=fCкB^m粹* K.gϞӖ=*m6ڵ ^:ڵkjAAA;w.\dɒ!$$K.U7$,"2g=@GofZ$e˖Ŗ-[;Ǐc…Bƌ#FJ,988X>3'_LDj\w)`k5.]X'Er0g|wXbrʥUSL7._RJƍGŵWMxd \f|eZU:rH*~oZ___5+c/UBy&"Sw+$c;Va~)O/z[jThh(;f80^rڨYƊUng \!?'cm۶Uω7< {.[IkM"z,޻w类hJӧUxСȒ% rȁ͛7cgΜ9  }*{-Z92>sᑱط>^H%2Ӑ\^wѧN1LXAЛ {h4n>vVLDd$Ռ}H:r&&"JbAQh4EFGUB]DDoN]z?TdZ10Q)t㍐GQ8]=tQX5b%2pҷz'mkӪdDDu8OـXZ ڕ*dDDd|aMkM|Zـ.>CAҤ$ƴ*U(!q4=-ua[1 _z-0Q~Y}9ӧ†~éV!/vA}[nEcA(.h""+DTluxcEa< U̯UގLDubL돴 vAY|Q}NdJk_UeRaiƗK";.h&"w7%oĆӷ0N=42jU&"6eE4}/d2گMO DLDV$ψ-T"V!J|MDVXx!1wbZ gA4OإN_Uc0:tp]|B9ԅ5Z|Yd<&"wfjN؅d6&0DE]DD&]PK%rÚގ _2lr}:nXQ` 񹋼7;mj3|0ȢpgGV;[=j*DLD!2&/ƈ5gT&ZT(V%2=&"-X|1ѩ:U-UȒ$.]BϞ=agg-m6ڵ ^:ڵkU0;ر#ʗ/U^Ld9t Yy d/ˢnZ,'a%,Z?2** ȓ' ׉'֪@͚5L2puxN_GJ×̊Q[(W̙+V@\cʔ)Ƽy0sL=zTՆ ,YD=̟=q72ŏõ6'ZY#?СCꊢE.י5k^`~'˨\z,6lmlllGDdnΣޤݨ^4+<:2|l-wءZnnnE4i=oVX1/^͛7ղ32v={fժU*t3f̈#GvQ`ؚ3p.j2g93Z:u k׮U(λwF>Q^ZիW_e2.| X\pAڶm ePܽ{WTaXZT)kU"d߿?U(;vLMS3W\{I5V,/_yǍ7֭[UJtMO&};8se~оrBdތ-lhJ(JJW[ЧO`q,Y͛7V[2!`Ț5+jԨ֭[GQh;{yo('V%2UGc=[e4/q4QINr<3I2|R1$K ^~ź>59˙,n;-Nc׎cإɲ1hb3h:Su5o[ m+תD(bbPs.x_ DwBӵJu8 nk( y*3|0(Is?jf*DօLDIVpNXvDlMdm8LDn>_-?;XٳjMQq 5BG{30Ȇai&D-hU#F%^?\?}oR?t`LD aD48ֳcBZ siU"z(AGD8 cRZ(q +zkP/Q<0EFǡC~ s`s'8ϮUMD^. S7QX7.z@i* ǀMFԵ?ʗI%xoW ,0|[D/o?Dqֹ/x#2-RC6Qd n K0|>^p]|/ǏvWƃ,Mf%z? ֞Rp_?8 cӝA!2.\3f mڴڒg$tL2Xr%~U#GСCpwwG,5ӧ1Ga[[n8|0RH@&Km^z>7Oh]1V!LVѮ];իWזݻѱcG5{zŊȝ;Vn޼ߕ5kVԫWmڴ˗/dɒ^? `41q~X}U?KJd}}W^Űapmmɋnݺ &}(]cǎ͋ϛ7jKf˖ ={ĉ''NĤIԩPDh7{?~XZ> _"3,-TzQjU|?~< T\9rP-L2i?j#FO?ٳc9M,Y&yСCϞ9sF'2W2˹]VIO^"sA]7nĞ={T`OTǬY_cƌQTU4iŋ1Wܹ3ɣZ[j– ieʕ .]ҪD'q4F;)}\67v<%1n:,^XH$,^L.]*U*̝;W[_DXXmɫe̘!!!3"rA8Xx>@0CRub'Ղ$ %|[k׮^ā&fڵ 4PFEj1JPV!NV7 +1x\UKN'S].y)Ϟ='OtҡiӦgtYu)=4+o}(}K4OdLZ2|,I[dN<է- n\ ##5L 0\}߮:±m*D$MD *<2nv^8ɀ[2|,Ȅiyӫ,Dl8^8~=U1I)޻Ȃ1~x$z, -Ê~Ҥ$YҲ˙0da?4nG5_9BDրLĢbbtAjiм\JDւL^ R61Z /ADz]ȵg( {jEj"zQ>p? ׅQd6Osa(1fb?޹4)ShU"f `D~Z؇kñ#[ɓ#GDOpo@#nts-ձMѴlnJD(f9op?ϫrDJ `2*O߇{XݫF6/U!}GQ1h9s?vgX߷V!Ӑץ{nەgQL&nq. 6;.f*D&2Ce} *C/30;P7͟慝aa*۵%ѪDD&Y/0" ѻ0[\Ö3ѫN1V!"c0YMx`LjWKD Le>H>51a'".hG'޶ak5BD]Ddp 7<Dh&ҌX{w\]7. gKULd~u.s؂KDLVma?uS!XZS>-Uք-u7R&Nh[)V!"J|&s> K@|ۤ8Ƶ.UȜp Ȍl9{x/03;%"aquYl_M|TD%=vAŻ0u~݃YBݥV!"s.h"Q8ۍ, ×L,VeGCHfÁ]er&"a\lYOڒ'ѱcG̙Yd n߾U#M4H.t-%kwCTi+컁 KgLSΔFIPhQTRɒ:\… *SLcǪɓ'k׮طoV^jdEi^0zVÏ- i1j{{{cݺu9s&"""dʔ ͛7G̙Zj_=޲e 5jZǩSȑ#1}tU#4ri\r2V7'4-[t%شiRJɓn׹x"f̘C]?1ҥK׼͛eЖ.k,G0aEbC!_FJDd-?b*|8\]]@bp!c5jժ [CppV!sAΕܜбjABDdLBhӦ <==ƍkք]F:E1GZ /đwjֳlО={QTΝ;q 3666R o _2_Cq;>u Ve(lE5Vy3bb."k0Q"YΥFlh%"&6}%4q:]w9br[q\uefNS2V!"2&=t_x7#0MYW\=1`5u-4ɱ̵*×,LWKèY,6ՂKZ |@(jNwΥ  }5@l鰺w /Y 0Mw1+ YDdU|!8pfv;A)*u`Sey£#]TY0%8t_xלSԿΪUgASz/>1iy|VV!"zMqc?y `J0:6_@_wrS8˙8 L`p)˿T)k"IXd6 @s'K{TŚDDo2li|:EaCߚPV!"7a{ F0vy,WCLZކcVWKA9V}{(8LV#*&^D?lЯ&×=1)^BE>xu?/kGߚk970ծ wZk`fH$" `zȘ8ZwxCnlp9˙(!pR\ljN܅WѵZVɓhU""$,8+!<5{U/Qc ܏P`Vlt8˙(10Iӽ0metw,u}Q4{:JDD cE}&W7QS2V!"2]&֟CQD6l6×(lnGC~2Z9`T3{ [dv֝|xڃplTcZe%1 }+O}jltsBZÈ(8ߍS!W|ZA'sߟD!((eʔA/vƍER`ggGEEŋxlllc4nXxU|JN>Fٵ b'9s"UTF/vN26l۱y!++ܽ{wDDDߴi5ptlzuKonYeɔF7С ={`ٲeȕ+VyBZYd=M /_FҥǗ{ap]r;f1:ϗRܨu+:`:7&&NNN*/]ZR262 ?Oĕ?|RFYsߟJXfF\S`ߧP`f1*z2SȚh]B#i79Vb×L1)`40=ܿvZ  xl݇qg u.LDd&VHէѹZxs{,'ac9ѣVQBDD(A0}%T)'q _"" 6!7|ou-MKcCpȓQ%a8i^_Z9``Z^]Abb1,߂BYpjTC/`Vv|]FZ(;V%""K6 kl:s-ݫ m JDDlWD WbZ8 ].1qEևzȞwp"""YId.c5ѮR>BDD鍢cc̘ ~m%""pbp;fg1<-߂Y*Y3vA'xɟk"Z1jہeą0|? pv_AEՅ5eIUa'-gny^??*" oODD@q:먺]ԪDDDo=D1*4.Au6V%""z=΂~Ok'q~86 %΂2q3d/3;sz;KOȖ>V%""?;K fϫ`=Rx/Y0300LDDd `"""#`DDDF&""2 )RhȜh̓]R`3gFLL̙wE\\p=^dɐ#Gܹsz&4i UT 1uFllz# >}zjL3q&,sY͛7ܹs%i^8p=i2֖yasiq=i2Dxxtq=Kzc'{{{)SF{f 4azmd=˧-1M\O1`"""#`40]ϑIǕ+Wкuk899ic߾}]6ڶm-:mرFǎQ|y޶2pΝT6m:uZPP/?+ayxK.U xN4I]{(VVyǘ={6.\ÇfkkǏW;eʔ6l<{,̙XBt 7n-… cСO&Ga˖-ؿ?>ckɓ'תVGRlLڸq#թH:t@RT-ƒ%Ky329RmWYybĈ} 6mڤ-yڵk\'>uV,_SV3eʤUVU׫[l-Z%$ZJg-Zמ={fY\ڴirƋ-‰'ЬY34iD-Dlk8gϞj!oΝ;Z1cƨTFàf͚*oի#**J$?c+TzsˇOܻwƧ~Fqjybl!)+V*DDD`OԺȗa&\Z5=eM YϬYA$F\xQ~>!ܺu 6l1}t3}ʺ(P@=P˓j{g=wڅyʕU0ڞ̙3 gerwHTRi^ZG} Vbz߻^z=ھ}{u/_V+ 9I OVX=zhݻ]tQ09'W^Z4h,Xi޽vZbL:ݱcteʔA=`uzl+Y.9ӇzoUGupԞ%f͚svvV/]ӷFt7oe_~`g:]6mt uxOtQ7dNoaoBz>O Q`۶m9r>WjKo?Ӗ<9rʥ=OzTM2}(~w82p@ً3?(]rE1m [ty'78 jtJۯ[O{/:___]ҥն9]"EcKFHꒈzl @3fTݏxOc.]97hذ:bMhYϗIwRZr,ݏ_|:N,Me [Ɨ.S9ڭQKpРAX g '>ŋ^ ÇVҏ?UǛΟ#yuܹsqRmO)]GUSHud8De=dhM<./Qz,tҩDbyz>O>Ғ47Z/&򲏔v3 9ȾL.Izm?a+9ckdjBKdG! ]4mIM#dl-1N4z>oʔ)з"vyw2"ewV݅(Ava2x6Gjo߮^#a,d SvhrE|@dշo_m ؟6.<|VMZ^x}(j2'A{')绒_3)mO'Oe.#+WT"{ݰ ~X/[Xn/ȥ~'5NXdA{U^~{Qޟ >Vo%bkϏ?}X\c1ˇ0V%㩆֚V d NZ|`ƌ5kjH][BƱ)d|||gd dnݺm!G3r$,;Ä4$;S)WK,K8vϓ B׮]Ӟ=igϞ]=cRlO7LARoO!Cø3c $Zj X! \>_Mi {? "B_Kf:tEі(wOlƁa+lٲII Pk׮U[h6lzb 5ԩ0aZ. 4PO8Ν;:}B ?޶!!!ԩSu*UR'c@2%d,Syxx M5ƁǏ<ҥKkG:}(NiӦ)UJ՝:uJR|ӠSNoV{>Xqϔ)S;h<1^ulCzCUo5<&M'4xzw/BM,pss{alU}!p]XƀamBy|RuO oZO! *ڒ'vޭԾA^Ν;<==%PBk֬ѕ-[Vmc V\Y7o31#&Gߺ-[tքN߂Н3>xb\#C @!I?ϯg,Yt 0@oũj}2eʤ˙3NZW˅L]ⳞB&O5kTYLDILd[Yf3رC=1 2iӦU]ɒ=(ϯ~e|O*z,cBN=2tG &IYD8 H+̙38p$a,j5n8m %0S2f̨=Ke,3(i1cDDDF&""200LDD3L]IENDB`unyt-3.0.4/docs/_static/mpl_fig3.png000066400000000000000000000404751476461141700173260ustar00rootroot00000000000000PNG  IHDRh7( sBIT|dsRGBgAMA a pHYs+8tEXtSoftwarematplotlib version3.1.2, http://matplotlib.org/%J@~IDATx^ \WKuD.5m-~c1lgas澙or f.FBJt^=<:T;9|?, ""\e}$""\&""200LDDd `"""#`DDDF&""200LDDd `"""#`DDDF&""200LDDd `"""#`DDDF&""200LDDd `"""#`DDDF&""200LDDd `"""#`DDDF&""2,|_~D>{DD&%==5k'|џ|fffسg3DD7 o.^8bcc#""ҫ^z nҎ'ߍgddhLDDd `"""#`DDDF&""20媤tD'kGrMLB z/>K".)E;?1(W~9<bkhXօ iLDD47$FMiLDD+0 nQ?n%j-톤3hM /EcԢVy1znùs琚EaÆ|RR.\x888Dؽ{0_XXXժUƺlHpԤIXYYix<0agdAp?,áKZa?͛~H!!!*$l߾3f0o<5J_SNj… cƍpuu֭[U=oQbE+W)))5k{<pBSXw,w'kD1 b1@Z*5cpp0ʖ-ZCE˖-1rHUXZ0i$_;sWTT j fi!?0a Ey>q;_w 5aܠ+h>|gk*UH"&ʏg"44/+_n] @r/ǦMTXڵK}o""zqcϧQnT/] S<|MgAbԩXhڵkIɓ'cĈу$.\˗/Ԫ_v u_&"Zl/Xw4Íٝs=|MA[ǏŋU^z?4I=RJMMK$eGJcЫW/˛aggZHf[K i&".ވǿ9]j`JxHKJܠ|u5Y./:/?0???uNfCLciJ@Ku$,3 YիWWg&"{v\è'p-lpE嵊q0uLD|g1uqÊMQe_Qsς&"":~ ,oΝZAx뭷P\9,\P;KDd!&!FZȠleeիW˘2e J,dappp+W{%ݸq#bbbk۴i枞?V%"2- ix}xhV$fvƫ5JiU+3ԩ ]vk-зo_u">>sW_}gjg 7%J_~ ///df@D:?Zw/~ ZҚ%0ۇej ==]u_Ϟ=Ѷm[|BCCakk KKKu\R%[~ ]X[[tin:MD0c9t0ZcdZL[n*0%{Mhҝ{aĉU?ѣGb_m}mܸqCMiިb&&&"##CXIII@^dB !88cǎUY5g9'ODDz^싄tl튁+k2E ` ^777TRERU}}}եF2yѢE_>N:.=Zd U>SrHH ^˕ѣGMpd<ޏQ?UWY&!HkRq'Z=+DDDˬA!- [DDLVzc EFXnSBσ-`""zfN]E˯",& hDD䓍gQ%6pwU(?bإezg/C]+Ӫ_1 hͱ00g}B@m~(c]ղD8  ?DZK1vUE,h""gǙkxc\E2|DD/gǢ#(cmmx׭V!+vACbmFZ ]DDzPፔ ҘKόLD79}4*DO&"zNgB/v˘}vŋhUg&"z͹y;7ǔ. a=INU|Vkw87tcUgA=H4Cе,ji2&΂&"2aƜHIYM0cܺ?w/ ;ǸWZc4CBb^Iè0zZvA.to/jk%ai,9a+arQLhuiϠ]qqq8wRSSQhQ4lPOJJ… (QB/^č7 [k׆V/==AAAT@&M`eeMD;~)}"*>ިU(/AxR?| oߎ3f7oF/?֭[W^Qzu,[L$ۼysTXʕCJJ f͚{/&vwuX:1j*1@Z*533S188e˖UءCe˖9rKK944U«(_ 뜢ЩS'lݺg&lW]2XЯ*UHzn1bŊ!::ٳkJ*(RnB _@t6<׭[W=<ְ|8> KKKlذӦMvdzQND/ޭ]s~}V]Z]cOqFC}HOOnuI)SF; cy844*|EJn?l*%xQ4ǟ !6)3b Xn3/{,UMfD˘|LLLDZZ絳äITP6 [V烃1vXuߚ5k\sQ!=ydKDBDDp&ﻠMJDOcussC*U,XuW E~8u#00P8ӧOW瓒Wr%GGGxxxZD; Bwx |ѵ.g9='EDf|2Ĭ]Aդ"]m9ъC `"z&߂|Eh[̹_ > oK6$&ؔz /?&ǺqpaXVw74RB?.h"z+7ɝjΎZ(o`4}Uo;JߤD&{Sw/9UqhPeJD/WbaA?tCV%LDֳpn xڛ߾&g9(W_`K%|JZiU"2$΂&ʧDh,CZT*DYD+[ @7DFlu,XP;Wj"A/LxitZjқPV%?6eujG'[~Qtx:;qɘv] `"=3.h٧wĈj_;w´ (%-m9)PŶNOyK<,ZjigJr ի$V7зi,o/mˆiiisvA/%O|.D}̽ ×(y,7]v}>mۦQ^?{} ܪi"+~)J.Cjg#GOM"%8*ŬA|/[q9Ihڴ)v 333,\#=0=1b Q-v Z4\ۦMc&SiMѦi E*2^t)Ν&[%$$ V"##j00q)l*>숏9p!JdL:eU@BZ"55UVULBf9\tavxD75ڙٳgѼys̃.uHX-Z 6*P xH%ׯ__; ޽wƳ-Uv e2T+\MA&M`e]\w_0 tk%QΆ]Δl۷cǎ󑒒.B ȑ#8v옺\Q6o ///Cop5@FGǎUtuuuu֭[x~ŊQ\97k,kx<0YkqĐ1Oܷ' SNP½ndF۷1p@L:U; %RzրРA|HJJRz*ʖ-MVᒵﯝ+** :uR,-&q:ƭ X2}V*DrM?GŮ]sN,?f͚aܸqի:2d޽{Ӫ%.H͜9)<<\}oΖYbH/d@0W. PW"{?q!!00P-Z۫23,5yd5(2n,#_|>>>V}Phh(֮]M6aݺu i*-]Y '0|L[9U^K,Q2~\ XبKdӬ\˖-޽{3.hҋ ahI\skwl?]2f,c\rr榎e9g.]&jɬjY$$7*W[BrƬ9|PKݕKd ?~<.^T&Y9::Y2 Ej"tQ>s+Z~eU>}9!!--duy;T+Ylg.-`ˎDc{]cVFDIXO xYprr*wfe 뀥u+22q*ef|=YZXƏk[~ςLyնSW ,=W*D( `aS^Oa%tw.1*^;=_ > /j:=6 wa `"#Y7ܵ8ysL(w{/BnTNQ~&E!1yN]nuj"o8 (,9? :1?jfJj";8 (6+p.=-bvaȐE\=ݦ(RV%Ld k]Ao!$*GPO9"D/$\t}k};O{oֵӪDDw1^ jo"EX>)r&Gb KӼCM`rZx? ;0- /C"6ݳ>×[D]0}9g>F6Zr[D;2 &l8n * zN/=70sM;=XK9L=;03Zz}[IZT*DDϏcDOǮ3~WmZcD&Lf9s'bVO'/0 `ǘ tQ-1u"]DIHA{ϻ*`fZvAgm^\E1|` GzP|y4oZ2e:榝^C2e/Ν;ʃ"##[o\rXpvϧ#>m0eBD4zj\|Ynɒ%3gbժU DϞ=Qtiu^}7n܈}Z徸8i...8s <==1|J|dVY;cp*:3ӪDDcfffm'''u^ZׯWa;;;~ڵ+}rqaXZZbÆ 6m{< p"/S |7z8k"8Қ|||TWԪU !!!궴۶m sssݷo:Shh(lmmUJ*!,,L~m߾uEf5΍KDܭ[7}QAY`AWҝ{aĉU?ѣGH#_Zoƍ7 WWr7>xc_SSڡqZ(w<%+Ν;p@@8u)Y/\I+7%%ڙqMU2n\BuiUW]ƜN‚ 1=1DDɠ&RInٲE3FZw/3%eWZ+V@RR"""ЫW/5٪PBرc5k"99sQ&O www=JZY`GK sUrAXWJ*jrll p Y2>dTVM}w}/R$eXWr՘Lѣq+ >݉wCg} W"? o| E*Q&ޜ{n%cfkBD0dL4Gٗ c7YN=E9ZqYgG|_/%"IXDF0sI)iro%"`n}Zc7mZI}&ݑ]ދTߪftU(?a4Q.Zq8.ӽa] m%"bnȢVGqtWN<\;ɫk(aeULyڼ=<fdboWS1kOdu˿]ʡV!"?΂DݧH"e~u"11%JЎ377W?uVñi&}ae>zh3/C > +00SNŢEо}{1\KKKP-` ۜ: Æ 'pB\|>>>V}Phh(֮]Bxݺu|_z2z9^4R[F}r&"z\]zXd :v숛7o f͚aܸqի:֪t?_rEtfʕjx޽ڙc~ .a`YV!"zڭn߾_uL>]cǎW_U?#&L&j _ߥK,UUoooT\Y;ܶU "ncջMyd /S<~xuY\$1|-ZPzjWZBW.K]tATHK _];;;$$$w@2:b}4{5Zc@gԩP\_ rjtUn뀥++"+IVr̖UV-d|XXƏk;gkI/T8b`ʘգl kU""c ۔t3%"9gYϣw[xKDKΧc~;ytO;:r#"Iw~#>؂ 1v%"e |4|$.8M+b7cU(7 :pj<ڼ9h""}b4yKԸ-4˄GDDy؄g~:Ǝ1nhUV%""cbx</a5qx +^Dq 6~_*DDcgD'dIx9NvQk/Q3ZX|ݷ>xw0""0CP㭸ßv _"<cu j vqEj%*e ^z(_<7ohL2EwssܕݻD={v"##[o\rXpv6ID{,O;ƒAQZ:V^˗/-Ynl̙XjѳgO.}5jQQQXb&NUC6m3gתMg<*O܆E 'm[*EN233C|| m۶I/S 6oތ&MhѢHIIQ;ѣGcϞ=(V~~~>|8> KKKlذӦMvdzQGRޏ|"xB/7nT?$ VUTkV8tCU+*U>Q֯__ֶ U kt,vvĚaDD:f֭ L >},XV}8ѣ:}6nܸUc@4nܺ _uuLDk`sssܹj SN5\BԩSY &&F^r͛7uU|Z\tUā۠j]DD_ `///#55[lQ쌑#Gt- f EP%-c;;;L4I6lZn1vX$&&f͚j9sTHO<w5 A,'j/&)UH rQ*U,X5?D^T,ϟW-b8p O'%%!$$Dz{{1eGGGxxxZ ## OwPsVӪDDd*er%:V^k7k5[DDY <"j:=kq[|V!"G{|=YJZ:l gPN}:×(`\VcyW1p{p7&q:Cų]MDD6 O{~W  \KbcDVІ)΂%~ǹX=m.""z4΂'JMOW{P!%""!~=-ԭd|+ҪDD @\VR*fU 삦$$ueԫ`si%"`@o&뷇pEߺZXBZ> ut< Q~3ks3nODD223ޏj#+UҪDDDB`0>nWƷFaZ8 o|" =vu ΂gnI؟NmO8ڽ]DDA( # x/=&""]0=70 Z_~Z3g͛V*TE_TT*T(X ճ%&& 333M6ZMDdzn=z4U|{ƕ+W3φLDdzn1`Ze,Z9~UM>=zׯ_WΝ %|ꞮR <==ʃUoݺvB"E eHqqqpvv̙3ѵkWuK.jW?qVZ}kĠbŊ8q4iW^q/fϞZƊ=wvA?ƍ㭷ɓ3@jj G 2e`Ĉ_T :u \bQQMDd21Jf>_K0JmTxf6lhGwIXl!WDDDyA[rY޽{1d5\D |駪G2XbŽpWj _QB\&_ٳ+WTlmmqU5믿j|<֎HlllTcL 2+;w ##CMVL|2BCCյNNNj6t)HpN׮]SeWH$.ii5O#rY!/o nܸ'C`jO剛 #0NSKVWsPsP~F)G=ʕ1`S#miw}ڷo1-;wĆ d1Q>-?<ݗ)VEKS$[N9(ς7;o!:\c4e7砾(0%v7׮][;2=2NL7EFԷ|&""2vA8֯_vn֭'b͚55(ZVZdѢEj+ɖ-[jgIFZ{84hn]_~%95kXbȲ?:y%,kϟ?_mF" ֔+WNFUdY+^j2V~B1XxG\VyN_|9 .Mj YA6YlZP{oRכիWea/S6m5]l?oV%K|Wjc=fyQ Zdݩ飂I6߿V'y ÇǏ?eyq+_ZB~z$t]q 3K򎔔;=eSnݺ,1+{ uRz4izѬY3_)=szmٲkesyo"Y4Iռy]R%СChժ:/C-$o2&LULL¢gBW^3$꣩ bu>3O zhG\`AfNԹ̡CB+VЎ#MDfVdf=o>ea aH?B'>Ҏ2s.+my S2ZYY-uq~=y→x{ff52q^%Yo2jg$) miiI-)b 9ȒgRj2^LtU[_UZz7;>}eumѮ];θ`ڵ8qj9ed]rI˥K#ծi[Z~ꫯsΪ{]Z҃!de,YVoGY7<ճg^w 'Hn^OW~dMybksn#z|y g$zִ׈M"y,{'B4 *9|Ћ>Luxxeqcl2ׅڶmdDdd7琐b,})M ;4磬ÏW^<F_`:~Ԛz} g$z7!clǟs VڱjܸqXZ/{MmM.)Ko߾ظqjʤc-1FDȜ!\yQQQ iaX4o?>L:U=ƙ3g^-ky>m"?l^9g<3ڿDDDDDDF&""200LDDd `"""#`DDDF&""200LDDd `"""#`DDDF&""200LDDd `"""#`DDDF&""200LDDd `"""#`DDDF&""200LDDd `"""#`DDDFK{&tfffHcOȤDGGlٲoIs.]:߿aaaZ ML#44T+џ49sDPPvDDDzÇڑ1p0LDDd `"""#`DDDF&" (bQ ħǒJ&0n3v&"K&&mbQ~jOa>,iiw@8<׾4Bٴڴ-톤3<|@m/ռz'kʕ+BlPZ5Uׯ#$$666ȝ;*S̙3%KD;QZ`nn00Q1/w|u5aSaƖ-[l2CR!9c ڵ SNő#G0g 2D/AڦMgo,Yi&ԯ_;vPq$x֭"E`amm00QŘm|ߴ(3k5+t@ VlQ^=  ˘4i.\͛kO=~x 4H;z pMĉprrj_$!n:ׯ6LDDNP8:.8ѯ~1怒!LKY%J+Ը޽;-] 9r@hh+Wbҥ8xV0vA\yݫZM`kܻwO{TXX?~=J?9%㇯t1{zzV̪ jXb%Ϣ/C:ܟ6$k xԨQq R___/_sU&Iɓ'7o^888VrK̖]j_ 8utWWWu{S>}`eeZHf[K ;1lC֋-hZUFZRRd wYr{*WviU&A&Tɱn%%>8T,ҥK_DR0^;,>fPVc `a'0iE4l}kp.Qyς&"",8I`BNJ:>U)` ^}tYtp 絜[DDdRGqY40ͅZ5 `""z/o.ڤv9ևrN `""RvN8KUJ `"4.&ƀ~N4ό`RVKɅLDݼXu&m]F5DƌYDDi30jy~}#i|Z>p4JH Y.r^wk DDi)|4{Ua`]6/0QZvy`P>Y Z-40}B9G&/nLjN5Z20 smEؖnrdcsj&"2Qgt9 SNJZ  `""(NѺRAR 0 9͝`=Lh}#JmXt&nHjL "" D%'03hX6/f75 `"";pZq&WoY?{{uLDSNxp+ [ˆ>]:R;0ᓅGs)|\<jZ 0zN8x?vҾR>J*PB[.TŋѲeKX[[c۶m,NӦM?~ʕK}ޞ={ᣏ>B`t->rmE`h$6Ǹңd `sssY7oĉ'ODDD 88={ nݺi&-Zjoܸ1T;99aܹZ-i x3N< jI5kԨ[[[d̘͛7We֭;fٳgcɸtV"wwwdΜ#F@ܹ?bٲeZ"ґ藃f>?k5w>p YYZСC(PVDGGkܹ34iӫ|n߾ KKKE[=~ن TȑȗN>Lsõpj${wAzb ޽{ҥl)S&xxx`ܲe˪\fR3~xG}]""w:>]|5>z-Ր)Jupp@,堠 ĉV#pBTRϟW-Y%KTh"5JH({yy\iI|pttСCyDDz3cgϾMnlvgO#42cZhT }0(ҧOCi [DD)LVpBXym[DDd;E=3|00Q fyt]|a8>1j(-b%3OP8ĶzYfj)b%n#p}5`z*Z u `"dӮ42gղDq8 =s Fg ƴz8 }>ywcaj _J=n:-<fX;j]DDNP8rǾ(uPrb4Q6@dt V$cA\{idΐG4'խ1%߇>~&Fh+?UάZ-Q0#h5~"a`]LlWA!z3 `"$\ Ŷvp@K[+q4Q"\wiyb[kYN!c,h""rF\K (#nO3]jj^ 4*IWj(5a4 Y~u@llDDKNbJWXэҶVC%ktpp0\(d˖ ժUS~:BBB`ccܹsrq ܿUBȑCy.::SĠVZ077Heg<m D12Z fz'koٲ˖-S?$///3f]0uT9rs!ClԨJ*ݻXt:Onݺ(R ,HL>։/&f}Sv_EwM8ɫPj~ iJ4]tX@T+Wꥥ|m(t =zPB*G6mcXZZjI&8Cל܃мB~^%fjH~=O1ٳ# ǎ̙3ѴiS/^YfUuӧW-82ez",,8yXj;::"""B}6oެz޽{QvمG=1[5ހK).'a]|&M… Ѽys5  J^`n޼ 8qNNNZ틤En:ׯVCDiǽzgɈ?A^"J))znWXݻYtiJ[TK71+WTcJ.h4*R+~s@JVXܫ& ȢU 5ݻ=zJ?~=JVZѣGΝ;/t1{zzVj5PX1툈En>5|+6㍟:Vdt5jHnAE1w\ukҠApI͛Ԍf9J*% vڡw8utWWWu{S>}`eePHf[?N[DiLJˎi*s Q;G#|Z-Q&"99?ګV}Vej!1`ooo9BB2dȠWLugڞ+a%*?R 59*<BudÇkG&ү&3Voj0˔Ad'aϟ_r_q5l"/ ۂCmlh%K0W^=JBDDnV*]t.5h5D`׬YS*KФI D0ue|Rv shD`_z-[TǷ|r :Yp+/"JFovU?lZKւVKd^۴i{U+&e?"7!IN{]=*9rm*jLKKQN0GW_}֭[-/gANzch2j|s3nH 3Yвrfͪw׮]j_wwwUOD>},sAX?UcK,c:t@\1cFQQQ –-[`cclDcҎ˘2ZZQ'ZQz.hOOO(Gʟ 1. 6^kkk TC%|7nܨMDtkOBa[C>5v)Jggg5SrȁzZ>Ld\kⷣ]<~]-͵7c]вBٲeH ((I`ָ-,oR `S&Jyˎzbj(Yk֬QIO8yQ={oݺT2eTaرZs7 s'''̝;W%"c7ڊ۱!oL#_{[eD%)%@W,ґjԨ[[[u/qU,aoonݺJ ;w0ydu߱|σd%̙3cĈ?bٲeZn"Jã0bY|Sk97+_@% `___Ս̙3j ȗwIJȦM@C8q {9wϯA{y۰Q- oooer?6%h'Sb٬ V^%fje^r SBK7vr}V1ASNHGp}ޗGn ov 88!wQB `YY%|`!s=Nt%Km۶ș30:+Wƽ{zt|2yK|:WVYTH?}Q_R{!htˎyat2 ȪQb.QF2˗ϐ-[6C Z-ZԆB 5kfmMǏ2fh(Y!j(]UՅ:t`m-Oŋ ڵ3:!Um(Pa,),,,GDj0rzvùہZ)Q<ѕd iJkTϸ{v_R|lbhEơy,^^^IMDo݇4N\=jIidHx~=Ot X4hu]!2fKDi(;f'"Ѭ ×%ۏ̙RJ!oާ喞͛7-`1yK=0~ Lh[rq&-ʪWqBf5,hY\CoDIsA(<~!ߦk[^!JL>eort?˪RXoD{IUx5PH.(0۷@Se Y?Dɒ%LH|E*kȑU=]6|_v ᪵+!,[ رVRO^oDvV\㊓qEІ(U3~> S o%[㯑(\!J~=OtWZKх R[nʼn't Ar+&lvE2×($nժxMeMf7X&zW> oj}qHL ɓ]vl {ʥ `Jbb p:p #c[mayߚ(kD&ݠAs| 툈BI\/F.DF` X6_f aU.]fBz0U|v3GYÛjd[pqqQ9s8uZKcEOݶ{}%*R򄥥Z|yW QRYCZȿAAAٳ'Vnm W@8ʕøqC|hӦ fz `ҫbq; Kz@E8LԃpU޽{g,?:u`Ĉҥ:2dEO?.\Po ڷoiӦ=z-rn^p=(vŒ#Y,\6D \NHmH)C&MPti5,ǣFR3˘4i.\-Ze,e 7~XJ 8,,L;zj n/]5.-R'NV"YBsݺuؼy3֯_ʐ&J.>bQOU9%25j ֭:uBXZoBB|ɒ%jWZ M9TĬ\RmxA$a&ꍯ7kq(3JK4~ħļ#͚5Ô)STLmeRWi-+yzz1hU-q9pOF~"5)&&aծ][0]hf&{xxt-,,^_fe u+بSq$,oIK׶"Lt7GUtYD!WIGhR.bSj7OtQXb3w0"JL`|An1\[[[t]\ F Jb'{~=Ot֡C)P%Ov[{%m+w: _4&N% Y-|՝MS3-`2'IltA2yqZ  oleY1>Ym6g J\;{dҚD[sQ[m? >Ν;{4h6V X8{`nVoy"z&de٘_ٕ{wٳ$~;O%fcћ݆$ YY #)%røpfft #C| X|̗/v1#k֬jE*"JS~Qxb { nTKD$z5Uf͚`Qzu "7(G'о<~jVZ" `YoaC&A7,翯*`y\˙^)1`ر7jfզMp ov>^ӿ6{ZADBDXv8SLY6Aزe0AoE>Ap="JN&Ʊc^X K>9_è p#Cj(z+ыf9_1DF \rծdֳnSEwgxFv-Dd\vAX+eVnݐ'Va4 4_&Z]sVCobZ 465 `z[ ]ǨVo`ihPǀѣ7o^ܹSm/ ((H%2m~!htWuV _"zgO?۷o%|-,,pQxxxhg.b+c^jZ ѻI4ΝkעB jQ-[6Dj_h7(dfzLQhW\V%xes~iK4) Dc̦hYMjskDDG|||Gݻ(RH?vpΞw1j9?ѯ^qR%\t%+am޼3g'| ~z=i9WTI}u" @&N6m֜~Ϟ=Z͋ bZ) 8Ķ1×_e?X;z;O6\pe RiiPT)CppaΜ9yr!ѫlmm ?uB{D_ l0 [{V+%N[3f|6dؠT_y,44NNNظq>2dȑ#^&w͞='OƥKTݑ9sf1sViDR"m}y`ѬOh5DD+ W/V]ʕ{CN(_i&...077W2+|M///shҤ ҧO;;;:tH'㖖033SDzQz 6Е|x&=+$2eH#ЩVCD%; 3F>%r_ZbdJXZV_TRj98lyثW/c a^hݫO>&ݻwO,Iie`_7uҘ7D&; ɓ'(YxAZ?ZjҒk۶aSϟΈ ^br###w%zɂc;aC  :,ZPD CB ͚5SĆJ jKy2e 6mj /ڵkgP7Ķ  0̟?_%gA=w e6drhDwzs7$2i\1f<+sMN"2&;LwCָSH΀c0|(Uaɹ|!>,;z _5+I`3VKD:0ɤVqa6 j5DD L?/`JW s@VZ QIX{7ي3|'ߵ-oZjȔqmgAx,SKD&z +`ЭvQ(c4鎗(|QaILXI!]D)h1/M9,m/tC 1ѺRAH,s-| 3kDDTmkh3Q1ldQj5DDTkغbwj􏳠)q~'(S;VĐƥ"8 =ZyK5s_KD&L+ϠӨWG*"".h2˾Pa06۲VCD0vAn(a1|(`twCEr׍8˙08gw?Xގ0{5!gɨ `JQw^FG3kflb/j0[~\C|=>jϣ=: :88W\ATTeˆjժi57={vTRE+nܸC*T9rh5EGG~~~꼘ԪU  ,蔷 =~9VA/Z WXQվ7oVa-W%^a~Xӿ×%> ˘4i.\-Zɓ'j L;,at_aZs ,͛7'NI}۷n:ׯe=W藃Y< ×LDJ)eҥdn`iJliHƊe܆$k+WN}= cڎZ~(< 8ѫn1LT YZ"_[ܷBs.EII$]EE'P`t%"zK `JHt ޯמŀ%mh=ZDDoL:x>rgoAv߽:q@"wE<|aХf'a+DǠ޴8u`IZȐDD'aY6,mHllej3|30`ڳj̓]Mer&"J `RgK`{̛]%"c<_d͜Am&VCDzq t+$ ~ }F2{d/Q a 8 G9_|ZFD+llGn׃PWd0p|?OkahakQJbt0<So0¬Oj5DD.hJ8{ m03 DD؄EE1tY4)9aZ-Dy3~sĈfei= ̪q 9p 7EflRUsi5DDcjF98=pF _"T-`q#Ʊ+s٬ w0""0ǼPf _Q\9x{{֬q醖/o߾ KKKhѢ_eÆ *%x-,,^hmAja?NƄv@]/%{wAq׮]UPf̘Q}8ԩ:}߿Ǥ]Q}߸1On,LDkӧW϶m۪8uy … J@zϋ\xaz\#i+wѫq4VTt4~F#3xҖKDD t#t^xmsø-\%"]GaUп~ vA B#n~T*W×|bQIFٴZ""ދw.k߽:ҧDDj wc0Uj#s =j^.y7gi^GF5BZ""q[r#ם( VZ ΂Nc=?c'~31|1߀㷓p T¦Av0j̳dDo-eyd^""z;&""]010$kt #""rB嵚.^dɒ(\})S&O3f8aaak!]tFƍڄ 4zgggu~߾}XM 40_ƍ*`T˶@ E̙U ޽{ȓ'6oތ `׮]<|=vލ%^QرcOq-$iDDAde _!]ҪciJ̙Sɿ'OݻwqٳgZj _!Amnn/'''E>>>*w؁{"k֬Z mHQM۫vک_ .ڵk8r a+-Es_&_300EٳgQV-\zJxĉ9s k+믿޸LDDf͚裏0~xR(!?~ 4[nU*$mmmٳ'ʔ)xvAvALϟ}Q~EsNq+ ӎV,]BρPJuLDD%k Xn :x ƀs΍ &_fHLGaŊ(]ːr2jXr_޾}vf! @;"""ʑ#jUJ+LjQ*n޼۷o{}+WhHw5Zy_9G]&qIK[NmQoC>_@ܿ_='S#O"ߟ- 7F`R˧20נkP~F)BBBԿz"cFZj7U9>hB+1-{ƍd(_^I(߂2eҪ{wi乙b)>SkPk0)oy Os=&є(_^Iab,1%7WPA;2=2NL7EFԷL LDDd~C6lP;7k+VJI6Xv-~wu϶ e˦B-lpBdzR}_U=ϪU2٭QlYdϞ],*XY²Vܹsf$`MUr,#kb:q-[dVꅬcxbu+[X1&םڄ<$pNY2P~OݺuæMTkW&l7&B$iW_U.!czSn]9r$&Mi`mmVÊpZ^zH' d 陒ky"|卑Q-^z/I!{9/>b0 >l(02y3!]z5СCҧ>g !]cl@z+dtWʋ^Z޽{Uמfֿ1-ݸ&Dou/B$YYq٨%i yNz6cXz5w(dvv82N&-իWӧRJ#!ô}չsgU&FWX&I; Jrʆsi5$@O w܆ؖz իWW29Z~͚5PfMȠ&y~?V/-[C{2+Wΰ}vƠ&Ŷz Yd1;Oy@gkkk !bXɄؖz=ƶ \U^oqܷoKu~5I+SL?դ>2N&NNN7걥kؐ6)SƐ-[6z*7e\MDDd `"""#`DDDF&""20Q*%+=ɂ ɱޯT3`g ;$D_~#Gh%D0L˓'X@Dzl֨Qt~_d,YrZɫ۫}iSwDz&22YP6-=PeEy,˒,A(kҒ4,/)Hvqz:n3 em^Aٍ#kJy~~~Lڕ[,nz fv+WˆNNNjK3g 6mڨ PH/?#ٳ1eE'yD0RR_٢NZT{&K,Q-b3WRe I>W51S;Ҭ_[n}K %0V{2>,*d;M҂Λ7/_\vM|eG$KKK*Nlv/;?C> H',`![҅-V+sٳ,c}QD|DhГcV%l cLҥ 5~,ewƁ&߿_ef;Dž=%0Q*HVf)z,cBnY*Uz,jԨ[jGeYfF;Eם0a꺖 ޵k׳۟GD&J:wcԩh׮V_2S-3fhD>0 rN+={W PK8LDDd `"""#`DDD)+%?)E+IENDB`unyt-3.0.4/docs/_static/mpl_fig5.png000066400000000000000000000406341476461141700173250ustar00rootroot00000000000000PNG  IHDRh7( sBIT|dsRGBgAMA a pHYs+8tEXtSoftwarematplotlib version3.1.2, http://matplotlib.org/%J@IDATx^ \W.\r -/a3emff 0˦\B$0RRE61lNp)\YjQʥKDDDYLDDd `"""#`DDDF&""200LDDd `"""#`DDDF&""200LDDd `"""#`DDDF&""200LDDd `"""#`DDDF&""200LDDd `"""#`DDDF&""200LDDdfi9B>}`aaLJrr2UO>D;?9.̰o>igHo͋wyq\P!DEEiGDDW޽ONIIn'aDDDF&""200LDDd `""RIЎr.0eD(.;Dl&",q2(ahe_VjLDDV?4wD%V523Ӫ9 zD<m~"}eЪ9[ Igű;/^ߩVy5znƅ  ^z|||<._TZEQ޽{Ն666TrzX/]0pԨQ#XZZjȰSR%_&J` QkZa?͛I*$[ܹ3g1|9R_Cj|aƍpvvU=oӦMQlY*U ={6{dLDd8x?m⓶QV} -\iio@@J,Z Bf0bUR1ydW;Pxx j fi! 0ap^V hA'a,X8r̙֭[B ȟ?Irfy-|||kժZHHxE!!!شi ={^IɘYԙ6_S`Y6m,Y6mhgzV{ʔ)>|v8 ŋ#((^^^V}uT_e""nFţ#v% t.]]P)-ttŊrJ5+GGǿuAK5bccYj/_OOOLMDj׵'3 JZ%k BCC[î䌳IkYB:cJիWUYfUGFFj˗׎ƭ?Y- &ʕ+*Hoܸ{{{,X@]$ǎCb&[%I2[Z{}&ٳ'|}}M j%; m--a 蟻r;n9_U0s f%%znu떚,YYYAo:'K Ur,[ h c8ef|>???2˺r"DD.߸MZV10^NƒdwyL|-l(SWೠH·Ż` [-l-LDDOump{>xk9[lI׵7P\a\6ۅ)`_Š*Fz;]a LDDʎ37]`^Z LDåb:B!KsDuEφe* ( cA}uy0gAPO\Ä_ NkՋk},h""ҕ=CBEwk DD94 7bЦ(bh_: `"b=΄Dc&VUXɭU)1L\8:?VUʴvN0L_ZkW./x2UǺ..LDdd^rczZZ0ILJFEk1T.a.h_VLDdB|AjbXB  "2[#*>&@EiBDDdT1C<Պ!j^_S&"1 h?6^ǢcymN0t'@Eap' k:riUDD:wˎ㝺quz{eUI/DD:xmBZ?hsnK8pkFҥѴiSDDDΝCv`gg[sZn%JpvޭUN:TRXxvt-=xFdl"6 wZȠlii5k ((SNEѢEhZ*]!ƍmV<"߲eK8::0wss *iMxǰ'ѤbQ\oT)UI  4@͚5'OiF`urrB޽ak1̛7_5Ο?}ܥK`nncǢH"ꫯU͌HG\ɠ(Wp*w8֬\,-~,YRڭGzVZ!W\+`B+Wׯ_WaְBta C[`DZLk׮*0%{tҝ>ٳl7o^`̘1ժUSΝ>fʔ)0`DDz2[؄d~Mk2E ` ^TPARRեF2yɒ%SΜ9.=Zl*U>Sr`` ^˕QFM@X 9?@vp,gݐ0gbqjӚ^wC""dK0r2öQN -`",&+Y5 \BvXacB/-`""za;@oKv:CZMD/݌GGpd<8 r pؐK/LD QXABzs,X8r̙֭[B ȟ?ʕKt͛qqquOo|ZT+ Zq!!!شi ={rPX~*c`R3$,L6 K,A6m=-~~~}.tZk׮ZJ{zzjg2.h*n*_p(kbi(iO+ BCC[I]7~ 'N͛7 _bzj5XqddV<<|8;bŊ,SdIBs߿??={W]4`"66VǏ30Q">ĬlBznˈdֳ\keeu^a>>>̆/Aj̖K&2$vX>Wa be]rZ&9:/Ž7ѢZq:I` tCr U{|\I)9$"2myZc('/e[ `"2 1|r5 }]D{ć?@l,S}󪈜]DDFEua-RG81|I7DK0'_۠ vvFŵ*Q&"9p\`0|߯~O)`ULDr~4T[G9aLIXDݯZ]4EZr"N""2gn͸r;k5/C{ `"&m+Ů.h_V, `"7Cq*b'TԪD9ǀ_ynΕo/{&"ĽI|9 \Ul19×Hc>pj׮ҥKiӦPϝ;v[nU%$$[n(R̙0t JŋD]\GƳm{l_ӪDdĚ5kShѢ*`jժvvT·cʕ4iVyD>e˖pttTa hU"2'nޏ[]j"$(KǀpM,YRK7n#GT111囘?QFa߾}(X:'|||0tP9r_0}tjȜ p"z?rݸ,\=zJ~7o9WdCOF%#0O~accW+Wׯ_WaVVV(^OD癞2ƽY?l%zp׮]U`Jw**ݻwWGZ۷ *K^AqsBsQGQf rRώ;PB*ks?rpp@hhvDFF֫3[]2\Luyǝ%$`i_Ǹr1JDbvwwGHHlϣA&3eWÃ8-&OrhѢ:1cƨVVMM;w )S`Qָ6Qp?m`0GίUy ...Pn{{{^dԩSgΜQsE~aƌ|||<UX`ooWWW5YƼ}P=3,?{ _vYD/+a 1{%lT3ީ6hEơs0b/۾ ڪ2rFdO?U;9}- †5fPX7תD=u X66X"<== 6Lf%+[ǀmQsAZ(21`YWYv2dZ}jܸqjk@YLǍx [϶Cr×(N_cSBz=jFgϞU{իWW;v~kƪU{rTyqi-Uc QLj$X6Η]$te#}٩(%%E-\07oSɵo:_SYM~:ЪDblS Fp+:3ީ1iLgz=BD&.A2>zSMA×(`лxwQu chgT.aUȘ˗/g7-y]G^w z9`FM󉲋pRRTf<G[niU">I)ɸNRIQvIX.\}c o-[Dҥ9$,2e1W?>ѭ*5a?V%2-9fkp%\R]`-ZTL>[\ ƑN2Z46 `25 R0}y|,ZֲŊaW$V%2]z=,h"u=wjבN _"` Hd0:YEB3.P5iӦj&rɬ ^zZᖇRQN,w^5K2666TZ3]rr |DfF`i]\w'-~%ee5[’%Kkkk888fff=npwwW$ [ o7oބ cj%R*TƍveHW -[JBbb"fϞ ;;;cމVw lV{M;!Nf?j e*'$e&&&FJӃZ0ݖ֭/>7n@ɒ%.L'OF߾}3C*x `ҫ]goa?eWrZ(g,6L\+-`!ngΝ;x";={`ݪYi]M4رcѳgOu,e,-f 'IKS[&͛]tY|[S]Dz!]eа|QxMj8YW\A^Tn۶M,ȑɮǴiTv۶mU+Zp3ⴣLFƍ/^ xyynnnZqXn6mڄ?5+w΢p?|vu%2cccѢE `UVU-iO|-㷍7FΝ{ʕ+cٲejWZƙ5 gժUjjOOOLMzu|4ⷱ.hm QNg-`ޕ,߾};#???2~,]x `ʮv>iuѣaYBDOcN&PɄ(\+3[bSvI֠ĨPGo>L>E.)SF0);+}qFFQ 3}KDDϖ#ؔ0)X7ATl"V1:8*D"zݐXd[ E-9+×(be!Hs;gnbZך}ZrvAee0W?D%GѤRQBD虢0hިjyD&2~֝Ƃ ȟ7V%Ld k_CÝ0E%ŧ=WWFTW_}j2N,+cjLY&ibԨQ~DO 7/O6>+aI{o<)z4D+"SYD58rV _"VdroEkf\6l תDDL^WCy76]jj"L&cʯg1t/J{GZZ($,=ߠHg ߌ_L'afA'> _" 0G@QvvA Q.h>OD'aXJ٭V!]DYh@8Uűk3U(Ur΂ƅ  ^zZ~:Q`AԩSG; \ro߆|Y5jԀVy$99.]BXX_JJ 5jKKK,謷M A? ZAxpwwW$ [ o7oބ c5jFh [na8# ަMlٲ(U1{li8뤤 1kOuVKhU" 111Ujffߨ([. ǫܸq%KD@@*V߷o_.]3gTѡCl߾666 zWr߫FDܠcҽ#G`Μ9hӦR޳g>uqz8)RD;z$W\_V- vuuEBBV}\HH6mڤZ_Ȱ|#Qu3<.aQX3 × iӦaɒ%h۶-.88֭S!~_zR|Z +֑YDDOKQV\˖-CqΝ°I&;v,z쩎*׮]S-Yj+d]І6:)7EZc3jw~71c uN?7xCO8q1|~W^UdVuddV<<rmD1z6,U$,z4 oR4B\&"8 Lwbu#6fb `z赧q4*W;G;g9xt]@2 ԪDDq UsMW-U/n$<cU`esx X~Og [;[Aq[x'&4Ǵ.DDYܻןFE^9mkjU""J!'e~ זc{u > ePlV _"ll’SJZ{ ch4RL11MT@X ںbUqJʯU8l{9l:v 0ei{kO-|ԂKDMl" 8r!qoVFDd&H |wgm0MU/Q6ֱ{ Z]ѴRQJDDٙA]6J.M"""BSNU]\\3%$$[n(R̙0t Jŋ984f9ֱo2 hU"" Xf T-u6k,^ѣO*UPjUcʕ4iVy$::-[#Ν;777,X@6E OZN5*EN233CLL ;vA/Q6oތF@HLLT?QFa߾}(X>>>:t(9 />}:|}}{dZ݋O|T:Զ7@aKsJDpslܸQ}Zŷn+|Equ՚0N'oppp0lllTrʩ 6൲z'g?gvpS/zݻ۷okw#g98W\ٱcGSΜ9j,L2Y&BCC@ddjedgg;wU-c/" {/>t}[bG]DD_ `www )) [nU۠A1BLkYf0K(ʄ*ibѢE 51c ..ժUSΝBzʔ)0`ߠvK}@#ZӪDDw ` ^̨B jrTT =~xS̒-|E"~aƌ|||<UXSe RRR1ߟh͝ ggÇΕ* .E\m:50*j(fV/}48vftZߪUiEϖ S=(_g>{KD0H.+(fMchgT}|7&u]MDD6N7HLNFȬuBDD98 ao1<-tMlYDD9gAgncꓸp= F4C/r=gA3%%'m~h>@:2|lHnC%|&wi-rE-*d6ے#9 Z^vAcb`?8cjƅe0_;0ᅡa-*5)Ua"{B;C5>ˌ1T Wm`isorZͻ~8T -|vڅQȑ#xp5̋at X%|tKV*_P!U;[ntCzBRuOWPnnnZq!!!*oߎ={ Zx2h4hfB.]ԹΝ;_ سg? [i1wOeԩShԨ.^_]Cޘ3g k+^LDD_@Æ ѩS'L2E;$%%p.Q-[@5kT^ᅬ*U<< LA&F? F Vggg"ޱc _! ׯl!ԩ; OOO 8P)R}j~GjtwV\W_|+WVܹ-(!.ۇUV666q_޶mv^Y[[Ƙ^4elWZGJJ%T`uM'] 2>͛7,j>2KZr8\%O۷odj/P0EyfLTUԿ|eֶMՇ~w}m۶Θݻw_~e˴3GԷ|\w_LZ.M<6Sl9ed꿣|[Nx?`%ts=&є(/"4mzA2\F d8y2㒟ߓ닛SsPrsEp MDDd `"""#KڰaڹI_UvVdkTlWZ ЪP ,YDm%٬Y3>Hgݺu9٭뫯ѣGQZ5,XPYU6#_ձ\'/e HdRJr,#kb:UMf^:K.U˗*?1+V _|Oݤ&d˗ eW~'e~yYf]2?uptfk}eGܔ!-f68ʺO{I0=xeonE㧭yٗ~A /'yq2&7ЫJ*Ǝ(ڄ$yᒝdz7֮L 8&*- SoMIʕZ!!!꼐MӦM?cܸq6mzf͂Z +[~|D:y]"lѣ:'F?^39x&G_Դ'BCӧ>(RHjZIҪWerѣG5wԆ jGjrLW_ig%e•bZzm۶iT5,՛/_g ;O#j֬@LK bU i-}|Lk)^zUZW{U̞wS͛[oiG&Ʉ-5퍾mcckN``NRJjŋ)RDDDF.h"""#`DDDF&""200tFV:uv9YGWKDHgd |_ɓe7 `"PϞ=6`xֆ,(;e$KʒpѢEDd `" e >YZzLppZ_6-(}w;uꤎ(1td߾}j?PH7#!DQ>D* E-A%4JiN&]qI3sιq<~EbKG}tٿ_{nԟxȍ4qA( 40n ;[ ? yA>8 _6ܕ*AMmX>j@ & @M@ ܙ߇@f e|h#9젭W@A~ [E":'Dԁ.px]V2gA`5'ޚhPMhiϸkTG&N4:6 <h[̓kv&D5~<{#՝[V`s pO= fN~d:yeIJ}Gfn g'I ngR`#*Io81$^36sO]ϩtnj@ cyZb_ _hS_ynЇN6XD nq9_u'q<؞5/<>[Dq g4뼺K/=5IŢ)|y1ך+)OCR?# (ĉSk֝$tzGxrYDΫm_D k+A ^5]8aOdX*uC׋ a@x,ƸRI q~91D|=۝% $caUh YB7ZI!/7ScU~zkURYTׄWZ{nich;ۉQ Q"ŕ̿g@QTs%u8Ddta*B+(Wg*"Bއn mW}mѹ\ `- vQ=d!{hf1"_;7;.ş6۟9|bYH3N EܩEODPU6lfp!{}/6NQT_zmnmۨ'>h{ߧ׊ E_9MSgߑS(1̥H}x0EToLDDq w;Nz>VۍXEԔIDAʕȵˆ!ph0qߞ<;WvUJ8B9I]u- &9?mF/&w~hU63S]Äx${ .憝8q"E`@rUoVN;9*IUv&ٔ\]2cUac`5w183<;R뽹U~,'qQsO|^(R~pzl3 _){Rjs(O S9t!I,Px\?0 ?k;L-5_U"`FG K6_2䪜̶CGK nZѴhHõ/)gɉIiK=^q( Av*m97/4Z5w*Ei;`Tĭ2҆D3wQP \wli%TKM{(w̡fTQ.%w6"WXI"Y;TX<g_[jB FH&,Xk;td?y[';N*Q1}43wks\HeiE0sVT>Cq\ArU;k]g](Yr@@rbɀBʯ[Re$xӥp2Ci8.*"FcN)4]˫270 zYv~8R@=lTЩmbR11~tKɢ)*֐_@#xlm,n.͕ya5l7^GvX\E+QE'%L\ 8wM坱`j9^U,\YyKC,'Kduy*0w37OKYLqCȽy7]d>SM]+xd^Cd P>"sdG? Ob_6/ZϑAqrAeA`P _+/%tEXtdate:create2016-06-14T20:46:12-05:00 Lh%tEXtdate:modify2016-06-14T20:46:12-05:00QtEXtSoftwareAdobe ImageReadyqe<IENDB`unyt-3.0.4/docs/_static/yt_logo.png000066400000000000000000001651611476461141700173020ustar00rootroot00000000000000PNG  IHDR i1bKGD pHYs B(xtIME 2X IDATxw}/9HZUTPolD/Ƹ`bmvI+=ʽ}\ -ۡju+ ]>;[9NJ`;;;<륗(<3DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD"X"""* y'}`)#ec % ""B> !nrg^f'K@DDDDD DDDDDBDDDDDBDDDDD DDDDDD DDDDDBDDDDD DDDDDD DDDDDBDDDDDBDDDDD DDDDDBDDDDDBDDDDD DDDDDDb lYp/|eAJC{p!T6!<ߍ\Ɇ4ZgOdZK0T eBWB4:Hcߔ Dih@Z! iv'aZ)G=L ˲~Ȧ'b!;ZŅYV-LBA:(Qz@T@28fu,9eBF`7b*Ń@@@ -Up[M p""P2aF!Eiŗo101kORSLD3 ,̥ʄ %Iv>9nPc =E&D;h;\aq?35JԱ(%! ۡ>yܻ2ϚmRL+ΪS4jqJ9k۸2101Pi᳍BPr" RWt8 KOocat-!B ,# 8 O.Nr'"";kx /aS9@3P$< @BD D 4zWA%˂EY@Ǡsqܟ@gf(p^J C:9$D DB/kB WCBZǡqޣ܏@lͶzAep 4 @!|nTZ>} 0wk(D D TN^ JT9@pA|8BBBv̀S_p>y_#¢101ՍP&.K6CpP-""*EOnZB AetJmx #D D 4~l#A,7Ї 䛜NBBV7Ak 0* !֛8VR OBzx5S)!䛸#a nop=`1101YlhӀb1C`7U[ϳD D 4ZKb{TTXba!b[ƃ\@aADEV&7+,101ХA჈.OB;,ba!b>Mh@o*W""\Qz sVY@ \9cOD4Wm@ap9@@أ@OXАb.X xxmB;KP@?-,V0cJq"b)5Ȥ ܅O@@Jd @kl~"kA[0WX>VϢQɅvVoDTbWx [:X*$zbL(BQѐr1X@, khg F-U0d "{\5W΢"bk|dGAb3mba!b՛nM,ٛ;pһKH-o@I u? =xhy!"RV7}߆\ޒpa/v|i@@ \e."*[%Ґ3:nb+۱j%W]ca!b_ Xј{>yةAs"!q0lƉa*Nba!b!Yc S?ԲoJwton$+R[8zxZtŴA>:k>wL"{ru0],<Ddyz>8KBzsǂ $9Nhs>ۛ,|?c|4uힲ4~fԚǧ)!t"VhRՊáIqkgN D ؆;y Q|SVN5 t/{,41FpJܩ,QXr`a!b)on0>x\Z{_m.4lyiXGZ+GvTKJŪ˝IkHy^n D iq+ Qyܷ BbOl#Xb։}}&֥Rnh`bsL!d8{`a!b)m?<9< /27o5?j]$ _PsYY[_E.+f101'6^àgzn*HeB OVZ=7!Z6A*'301n ~HNLlݱ~~OB,VI}2>{W/BR|?~iA6}hd3ANV%rν1V) 0v߄{?f=@@@ >Kg1^w8jZ׆]ntXw$ัV,zQI 77Ͽ8MѯЇM[v]K"\7d1@@@0x o>! vT\[3ɹ"%J=ˍ @@@M_,6]7{)]]t[o'wP1ŊPk@Cl`-~/W8ca!b)5[BȚ-$~dv~kD#L\3E=W4u'9"QzlՀ8}d>z==d% #u?WٕGX "{|ס0~n?Xms%7$*w&TK6\n D fWhal}i[YA4<kqxVi B|0z001 Ϗ/4c_oO|YHe=Q$\:i"@" W!!X~hc |Z@A4>BCI_4^q8W* P[ϳ D m*^-l6 *Z o׾fn3²^Zܻ201ǶN_gsߝ= *=ݞĶ9//NK7Fáe{OىU DrwBl<-uk D%,vTn~IG4Yq "\OgX "b[-hfs0/~! D+s?j %Z7^p,"rxyQ\-6θf$V~,!NXgR:9=m6[;]/-fDPCٺe8a;[O30o7_v}S*̌ *=Uuuqj ) ' D' K+[3|@CǗ=Ҋ=E K/ax5Vp`"rŒBK%Tpc_r7 d6;|,8u; /pgS @\vpzΒ jŵw5W\ێ@,wJ<|ɥXQ%3yH[e;5ߤ~?|yx C[kCbۆ]bV-Z{;T~DJW42Ms'4+1!0(ݧ;¾f3_9P.gzLb ?#-Ƶ+>1'x+py`sR[,Mpe(`,gR=[Ki xo5 WZ%]RRj%RJHeIiAJ(!PJRbXZJmIhH5 5,C(HM)% CB(g%vnuq_&."" ;bZ Sw~][ge4ppnӒXJ,p-mZ4,0,% Jsyf--iXya ò\2a܆e +q9*~\$8;93=)/:j.ȂĹDG=Or nSL;oܦX6m)} ¥,˴\ )yan32̬e=.3qNuǧ0*ks6EҕW 3O>d>w.WAWq|u&g:wdN,p7@"" _v7hN6f%OEZ9-†lHg=l ƚes_6穾,a=\LɏGl{ïDD ãA8banl۾ \v$ys+zټ'z<ټateLJe ,a%L"$%{Ou[L'o(*"":~AqӋ'Zw,џ}pg{La__6 `*^#lHQO+ \"KlNksD"ϣd7fU wq;ǎWO c9O8Y_(@iɪO6=5}Je? I/OWu8{?WDD lb*Pl[.䣽o/_tSi_(3Ya1p8jZDE@r`(O㝕ɬDZř8 Fy1q|&p]6&χzyȏ -7xCɴߓܬ yUWd??nDD #cYI)OE^R~ocpQH+Y_ oGc+K9wחַ~8""g@;n)R|5dGQHCɴ?yo?_̹/:qܓހ#F5&])(^WQ)C9Fh}"v& Ud ?>#'<:5PJ3+ź5Km3g;H oZ}&熔Ӣ?ϵ+Mruu<[.DD PJXJDlJl^;MS3S:fҫx~ ^|XP)~  .40`P1  s y' ׆zʹy}ʁ?kx94`;7+/,8%2ƞjof|hP J ^ B^ ,#5@߅14=-/]p2Q4/s^7U}%04wc>!/Pu  ~L M/UX8V|g$V4[$0[m(Җc_- ft7&"yR_$^p #g *W$wwaO| xO9{<|JYm7-[/LhODd8⹬OJ3mۉRiUU[Wahc/urhGr*Kϼ睹A㲮WnKj l~SoȞJGN/rm zPC4VʛMwI tpU{aKıïU~>й;fwS'"]p/_ S Y.#i(%kcS s;Z9S{;>yƹtnZ< sJ/O?ò@`ڂ.[64Dcan6h+/rg^tnnj ii|^Ck?6Z#"IWkOM7 oX%q~KKÉRi%6s7,q]|8e%@J0Kuk굪L{R1rh*'{qK/yTy\֩i'а^Wyvr,Eo:>zcز*Ϸ! mQc ํ~|lrmzF@me"w1`h-82cO\qDD%@h!?8k_&Ezs˟7K?8 8}.m8p'ԧW[K8~R}sr],z@@X3;vY[¶2+UKVȡ Btڙpvjg4Dsdv QJvR֩nUJDTJ@?; }8ʖ+[ѻYƺztԆ_MXU}_,̣pk%o;8{ǟ\4~!"*Կtz#mG$̻uiJsJBj~oߙ}nkĪ컯Z4DcU A>p_ИKsj$%"* GoIx}DŽ=3 JpTz=ױwOKG}_!4%5~Sh`EJ]#ƧG$R L&6֥]Μdl{һ}ʉJ-0wPQFD glp(fz%U %E3">r4Vd'=j8f Q1G̯Y`ױDd%1o@)])g8g E6mW7_Vpp6 ڍ~ K5ɰ@G}_߲ۗ|;\]ՉcdY}LƐ;_uX~S)nGd l@ZoBw/֩kK{6))p ޷b3mi?ĀHCeR^@2,D[cu?!~[T mWi{3YUY|Tq"80̓as|kd3~s.eRJ'( /4<ϼql'֦4d)2XuiyW|hkDD do8AĎM.m;W"+v-@3'JlYB.Пjc_jIO>əT H[d_jl60K`e 4|^Җ#믪(cdhRwC. 'H1X%mL[)-%u Dkf{ns_s煝>aͣ !Ps{BOXe=ݾuߜ3|&Vj 烈'gpټac la}0[Yk@k+P1 .Y ٞ0:a1S}K~e]I'FG`2$N}ݵ9uϬ6X"bJH)s'i-ko+OnrdW'/lkY%Kd;DiZ\v wm|.$VRfE>Sy3{c')V@ / #.Ѓ]ֲyaZ{02RyBHI@20)K.GzEIXi~aOH"}=!%WW^JWOl^HͪH!oJx2G|7 &*Q2y Ġi i. *GވxYG֛TU$Q#;+cE[#{B>[+ys)~|ͱ)1V@ Spr׶.=KqNޏqis rF`@4`p[sY&(Q+c C4b?9nʩ|ɴX"}SJ= ]̱*I'geI1>q7/S tg8c{o뻪/1?Χ=$|2^GBtԀd~8?0G m9 I=#ē˯dE0Zw| mY7%yҟG5k^܅q0H+aon+˙߃sGåOv%[a# !S:\i)߷nX^r+BD dNo yn2ZA.!= j&71.E9>lB8"f37]d.Ĉ@s` C%'~7d*V@F49KXA&v<9 ^ j7 tg80 슆q*C,/]Osv%]Nlۮ̈́)M8TƒIBD } Gޚsm3$2,)< \jJ\ʋףlBkdUO4YweXH&uFB.~ސ?lYL+BD #`YJ2ڧum@:gyVcK_J5GO1)vsBCQmbT!dJgo |j̤Ok7M"R3rzC#hz:cϓw2Qh0g`̏M=h3q2to岖#zﹽ;mO]# &2ɝZ75Z>Բy0+BD @)CQGT*/|w:ks9N& cG43I/r_WoגF-b0Li 2uD>/Owk0%Kl_b/mxVʹ\ !s-Zc~lđ UHO ?zc]E"ё@*c^C\NdenDD áGVow#liɹ ҹ{xg^(͚_i;u٧nOhFuD#Քՙ}sWdg'1|nO:g.1a;zvJwƍ]!슆DY ot%CӃc^֏.9Ԏz8VpėC75&e>V@@ޝJ::gҊO6=D;˙K_hA]wxě`;FBH2*ҽU9_jZJZ^[ǀ8K=@yעA / O6G~BnU3R6[̑M~=u7MN` 3O==&ł@U9ě/D<YMN#gΊ4p$ö D<.I| #Ozٗ]ݜZ>=FwMZwU-od(Iu}S˯<\jȻmBqt>Ss?6Otaџ37 )Pv7;F+VNJ@VI?XQ%zG6-H!dfWg7/$"N%y.4Y +'ru5p8h:9wܚlmc֙`[o}"Ou ! F}o=Բ+BD X 'rjک{)|eYڐ0W $^ TǕF>woWґߠXC7Ӗ=}aGB 2&wD .Ds?;vgvObW1\ w-L#$5 +_~͟$v/}Gͩ+%5%|Y !%'GC錗GZɻzS?[ؔ`M߇^q2Y.h2 PJ^'7h;3 ڄSƸ,+%vxl8rk@WփŘP'OH`{uKh}E"- dZ*R?V"a:_5r R?7X%ǂM ?#H|(-WBr xcw/S+4 tV}abCPZLꫫ%< ç;w\g[9~)D@Eշfs3ATJvrCoC;Kg۲7]r~`y G#_ZP_ZW q2Q΅j|\#Zkn (/39d%kxl|L݂=Pt{+9 7ʣ'|st$9fōG`őoǺcJ4vE}4^^85fgg[S% y/|o *Xubn6ouN?}x^gymy[׌jfT[ڋc_HCñ*FpKgF@ ah-n;8 W{WJ߂7|?hJ!D@Zut+&bӗ'|lx!1 ` pRW9jiQ{sn쌆)4 ñFяQHtFkb)Hߞf5B##UhYa7ؾ}# ˞$rdQGnv-csJվ0zc\0q/[im OBŹAI&k$`Y3;:88mBpfF ʛmцP[ٻtћ݅OlӉL[[S))jjݴ끊1 =|Uhwu8rLOJU_ah1n `C,9RX89@[= ~]bJ)'ެ61 b#IK"ɡupN9nF-ҏ4enЪmcBp͊I`*CO-,1[9EX9J v{zA4l ɰns3{& ljG8IQ<*--+KKX b JUش 6. xVH,B Ӧ8ǑïVВcB0ot١" NmI.KE}pkX b>9Uz -Wΰ]I<=fdq^$WƮ_.k99ef @<d!: \5 r>.KE!]w w IDAT b/zkgvx}`nソ*H'$,އ iGZqCKjÑ'E|Z*kP !orO?i dr\Xv%e<(7":~ +Ξ')k#@#A =j;=I5|$6&=d1B| *Q,5`1=R_HqS=*.%fu< ZGB eKWm}_KRC&>lnf1fBNn\qp׬t^WNH)6+(q^n)j3nR <(Ri$0> hƪtFkTG d1În|ޓyg5 g<.3sgų:KpSYP&b9ǍgkQoVi{;H1BH>nѽL(wsr:_>bMXNn`֥Q;MF?QҠiuW\7bTԌewz'SCf/1B<*_Ž׬?m'tÒD?#Oz}Z1[X‡"Ѕ2yt'+o# UTgƸ7>rZb)ue=ɍغia:fk-1Z7b)m~םMe~*XB;ʓKi鯙FE~laM+=b)qYF&or_hU(aU/1XƮM>U7wdBp7,TJD:;S1i4! YUt8X, `"~3]Kw+ʖ,b%H"Ō0$H9$p熹9tWD f0=7t'}V )Ktߜ|eт c҈+*z/mE:w \_CB ÔD(!'<x9pڍ[p2bOn7;^>uC/aV+0ݺV\(#PH,@1DD6Bt])'{zA_89YaX~N'KpVj_`Z澕n뚚D3?\g4 !J .ش R!cߐy/^&xyp;o0Ry"DeB©CT ޶ꖖ,SmvhkQ7 !u W~r:g`7Jzz;JWN Hݓ`X[?IHEcy 0)qpGtW°z}KV[E ^x/ ,Wۇj^yÅx{1 ALmP/,L xN@&r3Qlv_V 9XT+Qܮ72L;&REZJ%Y!9<ҽƂ Rf8bh}إPHȗ0;pHH;$9cɹoofsJSg̃^Х`jϰi RbTU}Pf9O?sgvr!De=^Q Hѭ^"UM:p<(AT |cuM8}bAhQY/jp6&btYcbBT!JoDg{K W֚em8uC%<Sܦw0sG]fG TDCA)!ol=Ϋ6%p.ɸyG+g@3UbUD-9'\boă\WW-Ycu=q8|4 ze?W !P .(pBIHoq[hN>,]Ս9Ja WXBOxS}Qoic|L\ӿY f9 gkh0 3O<ܘ`MЋjTVJ=A0*DKνaC޽fjL٤ݟZp1z2 EStbƾ0 8=<} AdXοVtRrHp4U q+vٌM=+pemNz~p׀SpzXqS3%O )J\! M.c>חʺY%fH0">Ƕ} 2 L͇/Uu 9upK΁#{ Z)kwZiOT%~ *nTnsFTY!DAdƝp J@_1:W+jU6r6{^t!#d™-Jw͆VJ>7b|le4$h\j;%J!BI #H'[e5>+X_=v;FBdj1K+ 97瀄sgcs^\N!^gs8QB݄Ǻ?A" 2Co=zڏ878!}P u]ʒL=.dVJ>!S 'Nĝ>`Aw"/Z,4Č'?  b4u8,#?Oav@r0#_.ȑbAmK4R7k^D>iLt{h?ZOy!ČYiA~$Ape``?fܱE8irR:9[75vKX]a/b2{w !f\(+Dv:m#HjHXߠ-}43(0[ƄC \](59`_ M tOB!l1$B F !1DTi3.E+VZ+*Gcƒ;K>5?PkȗSn M ǝ(yK6SכʺgS^ȌҥCc F=8!@(-@}0 #CF.0`}A4Mg_v\J{Ƽ { `k뼐TAjiriVAdxRVɌs5f>.Y _: /x$\<h1FWȅcnh~',S)/u[5MY5|~rnz%#3Jg'rrWׁթLJJ1;% {ƼȔ:/ŀOBc{29r u5vB†,m_MT1i0{n0](`]M`Uk"=©Vρա]_ݰ5$5;Ǽɕ8Ip~rs}hΟPOQ`4{ѧ_kϙl䃟Zi%!&B`'.}5tv)h EbP𔆉e*WIBx$\<mXm[ Tazt`̍2D.S DuN7WfK ̇ki!BL;#C_(_}vKʹV X7 `yE+=s)ꫴfCkvcr'/CHNtxszH$ sm}/5](?ݐ1 v1^VP%36Rq `2J Tѵ:PΆ]c^Kb}*P2 1 `yV**`ϸe')Ux6ω$+a0[⯅"4D%Re3+dR} V ⽉xLVف]Ǔ 76!?r~q GW>ԡ \+K;_ω]jZXm`,&Rxx_\8ˠ@Q?6ϥ@x8Х4>-;0!bV3GB5a(t2,Q2}U M> 1 .8/qr6XG"q?l^*}YnB@_[f B.j<۵ ~ry&@oC0YUc,zipe584]3)ܯN?)vrzުe94h-n c5-HB|UZGq`gc=3sBnQxOch F 8\>fQ/'UmKW7i$JBSg̋X(ǔDEaKoI$BPzZ$@w1GV֧ s؊u5u/utKƲ7R\ySSjW)&\CNNod'TGsPiB?ڱةZiE@x'I+@.Y < yqӘ` /kE,|ҥ0\"_Q91}8q7%K|$p 4Q^H%\DgX/\Ho3t {`w`a WS5cy՘, m0+.Mt{mHY fE*y3 ށpY$B*Q|npc1Z; B`[ώ~к\QQMtq0b/ĭr,|Y IDAT"XV{qÆ @6쏺 .XB":!gF.yDgp>!hA!@G Ki|>dl W lnbQ JJ?* !mZ3[wN=>_ +X')m,`H>_%O[QD`x'iIo'l+]o̜L4) ؤʗq]Mɥccv]뚳\bfHU!kPC.]$DEO [FDZbvQÒcv/kX\c|dT. .J>'f]cndJ|ةPJ!?r`#ѸO0z޲fpο@v{~8.?hBb/fL$ xDH+Vq0=ޤTKnN?AN;q*Kg"MsPQa|<|x*ӥP?/wKv{iӊx?_cPŢ6#`=|"diM>1zR%}ZJ>'J.NXJ!=:Ò0+8<~fpο&wu&xkpievg\PmMܐ ')]a@W 4w M#Ri\kKa8gH:i+U|dߗ'%/> 9!Ţ=uO?h\YM{S>1E^*_R[XQ36'340\¯)NTD=s=,jVif<$GtOl}lh3N[ΐKS+j\MAe&@"Oy:V;?Iȉ/8%/c~5険U1.DcmDƜ\ <9ڊSNcYқqBݸN?!`D;z`Ʊcr4+Աr?uK'"+1Mibo oI5Ơ0cuJg XNw.YZ>wI \ވJ!(wEN၉Ux;iIHv6߆#ޱCee?{Ƙ1^ǡ))$E6$U՚Z3 '"gw/=z6$xrJy 2ϲ϶w?m^g'd?qFk}Tl@'*7SNP\!}qhǒ[bg!o̓ɩt=e̫E `CM,r<$Uƚ͡qT\8˴o-CQ-\Jq44/ dy;yyN3OD.?(h;;g% y{ )(k F v1Ip|/ܗ3Z޻NȥbSl߃6ϥY'" >(lۯ^od&XĽh;nr+䜨i:;upD mtADQkr:9|Mc?OҬM68&<\8nM7!x}ɘmL~ ͡Ƞ=*JǴts|NboċN<̯-6TlKuIH3M) Jr`1_LC2XcJQwI_O ^ Y$wAf _|fS/PP|64$@L?DU,׉ː_1I{wOXRukEN[ސ #'S6Yk9aJ\kQIHMk>Yɓ8f#f |WJ ;{%WũyY!C`v̰!p"/:D ؘ9,|N!N"DzGBB@5xq w:9W2m]Qa/Ә/74A=Bv biDK#R%sBZԃXSdfY+=[P c,'bv~(RȿviKKZW+[3~qH^c@s^D$Bd ͟aj#6iI74﷬?3&UKa+7Ƅb@*E>Q!kyzT!ٳJc$BdS?zE YsA+8 33`˝8~}ov^ ($]`]0jHt 33:.q,sXu IEgܻ"(pE&bMlSԊOj66q+cXDyfkؙ/>yMY]sBZr h^ c޳Ĺ *<ҽf9!&Fvbo4/`p0- ~ŀ!p0iQ`>=m4$@eVՇ4\׬'^[a󆳜wR4X=Nc{vv\ŌE Pkc#$Dblus&@a/hIMӶ8"(䯺.fw8FMaӘcM/ȕ| !s}vOK[L}?N$@3u) Ϥ we̎!zr?fn|gpA+ T֬1_y}C9ɤM1$ֻ( }3zÍ&T?s]4$@F .@dͷ]2Uj{ºES,t o򥱞v ꑭ@ >]lдF",xi s W?]>ט"վtVh2͢6ºN?AWW\/<&M/.s I 4c{GMwl1~"ːINߠßqY&B8lž@ )`[zE㶑<%o_ߒe*mAu?l]\0q}tϧ&b*8_k|ɞ٢ʫőQm FbzxS)[) bxy]~~J0E|1ge4}yT-w\l˺X.gn!3'E|o7Zb\MI} B_Za 僐1>(HHٵ}*1E|s6V鋐u K=-(4nkoo i 3bN(Yw4 Ӡiv~m۞Zo?Wv251$B'"o\wX@S(TA1-n9^obxb'09yќ|<޽f)XgZ~5+zݧdmR6/{-~@ܞ,k6R9AL}qiǏviΘ h8ۿ vMǙPNs`f'briwh,@#1uERnS(<}4 Ԝ?@X6߆#}܀ot|%c905b_TsU bӞȸFbX~e`uA-c5HzB l/VNOw((VDH {=㶑lAel( k.&5(f"̗RÙzxIT!)ța5ks6di <3&My,_u &bɚ@2:2 1i(e正Ҽ$@}Yu=^ȖuiM䄼IH,_TvĥV: \XЛ:h4Νӝbg͙ԯ$@j2]L2<|KqO(-+g"ᴈk( ihȕ+Xi0g_ڵ5oƦ  o<$@]HKgx.b[~>y@V";c|r+sLjmsd[2Ƥ3<2ڀ=[[Ԉ,2ӊzyZyBSO@؊cpVU>*7_hؑ&j_.$@Ad4u?eԵN[R7|Vo3=fs\}[~1#8{P_7ނ  %c$@s]9OsqlQ\_K+j$dQbpy| #b𤲮pFbjXe%̈́c, BT+aJgF^o)BK qV[WܜXUJ>'bF SxC#1qο:t d "b"8_Da'`'.E2n5n;>3je^OѤH\n (q?uHLRub>~QB<#Յ=#/VwL+6iaw\+ uM JFCLN15>:vxT^q#eG fԄ?2m=oEE[G16gh `mÑZGA`LUg,z`4X$@~^Nl F_=IpM<`k0`差 R9A%Ǣ }zL@1r롡#. DVok&.jpqЊ2 {I>Cu2hw!X518V˨GȔXt؉zƱOn$BTgn6Ӊfq~qmk5sdtr/VEa]Yƀ,{rQrقe$0o^s!n%x`CH2ζrL]:dt.ś[aa1E XIdݵ$v^eE%:7!Opc ct93c |#g쁤py;jӝ4mAL 0|XMe4#&4<ެFX/YkܓKgYlɄaXӽa`4a[oyďG7)AW TB> 9r+ +zlֲtTF X*B37tء+Q{ u|DY5t>\y_tL!bf\(ÑZ+Uƚ4}.t4mHF:-7qِ tfW乢0aX,Lܻ^+M$<QH$XQE68VPeI/o7_i^av19{bvg쯙7|}/+h c|m6)T&Qg+҆B՜ʼnP_*cM>=Qg*BQ ڸ$xZ:GGLв3 Γ_ݷ~JTWAWC}.ҠN?~m~c~\tXl  vO,yCCT;M"L/AWaq FjU5i ۜ3Ytw}0]ןpZ6}ˑNzV?;?~rKU?7TOl2;n5VCTC\*U"}(5lE:LI_w/0˙M&cY]; RO9&Q=$yq7զ7_K ŀ)tUIbP,!ǣ;f!]0$l%mzv~DZtߘ_u0өu]5|NP  ޓlLX},UMe4𡹍f {UÓ}232`? ޓ딉\h^()}8xQ*ւ&BT n홮RWs_[*[kgH>w:;kg\ʟB%x/nUΜU](Ñ©Sd i~7~jOHG f40gKjn gjܞ I ǭdE %TBA `م-_RIs~=mS# IDATk帹6 *d.jp1T'=4Wǣm_9dmxbWUP rr/ g,A|,h_=XS)c+j #A}|w񺅞HNOAAe 086dsvԫ*qiy^<Q=s6וԲ90NrMa?l%d0CA 0k{t`yvM*+N DpH].ZْCzfΓR_yؘ 䰇e™au :6v6blP5]mhB[Mc` *H@DbEyP:S r}<;ϝI6NU8&8PhzGSPt ~10zcFWਏ|4WɎ|rMWCZ!\77WxZRm帹6{A.0Ruϛ|a\sLe!D$@$1-oš 弅"SS O9(EAwajo@|"Ģ\LS^Ao=ܮEhR wfj׏1O1቗W!]ן'~W+}E5czR}jy s|_J&<ŁEM 'n3%S}(A:ntV D5P oZw#X?u~䰭$_=#Ey?_&l6i!t2`A .Ҡ?bm& 5o$BT0.ѺʞJũO/,rMFy!!=]>qr6gCL{s!n"?\ $@`"(G]LR 8"6Of>cCM11%æ 6(t`C\eWpp,XamQpοp,ye]lW턭?5#^޳R_:=4 xJbeN7B/ 䥉+yWdtKp4@,O5,K1=;$BM?MtWTňA?ΏOYv8vEN:)/_( !iA WI\}|9q9s ḅQ󯡬@d{VTGKєb~!ohy DyE[XWD $Dp1]!2p V+Ӛ>9QonJ^6Džݲͺ몴{~mGe 8 5ȅ)J2~#i.ʟ0`?5YP^Z㺤 6 2NއO2MY^Ȥ*TU}H:r͹p뜳! 8 u7+hQaLܾ|ֈa<-u _CvW7Bѥ͵勶&z0ފoT) h~o6ՎyOmqhFbP(a/_r Z^븥6Iz B6UBji'^;}Zb@]f :+cߐaϸun<~否Iߕ[119ٟO9-j~@DT9Bys֢{E] ( DU9e[brb9^ϸGSiH~u뒎T9E08KN?/u Q1A[`PJh<@ެHh;II!,5 {er c #zhii cYG䚦XQDoSh,ƙŒc6bxjtA 0`q LYPhf;ISeڳ||?.9j0GcwvR~t1I4"\aƨ͢"( X@6 t,W3*XŊB^|qU4M{1_*3it˃6ԡ#RT5sŬIz\Km #y!d(:JP'ӡ87Nt9}3z87!of0Um}eKgAd7.m M;m9nr&_54zs3 D'ڍVSY'7$.6pr7&$BL ]7(ѣMؿD]05v _ֆ mZ4W@漕u0`k'kzW.l\W?>lk7eIe3m}zNA8pfQљM aTr`^@!^+͵Iv4 ̀K6ۊ>.ζ:y$Qp @D&م{[7Gی\-u#U R XJ¥iAT=+5BI+W7/Xk05j V^$@,\a?S+ԑ2Ld;OV<0qs詌TuT  <`ͣU{7W~n,s h oꭀgzpl5:gw9{h\*Ŕ~sX,7o bxB* y{*,EխKåL&qy!Dާt Yc[(b}m-r1ERzS c3#4䃑-氄YQ.K bp:emd~=?sYKbϝV7\/deQV2XHA bDrw(… f~v8Ԗ\j DE,GB՗:*RmbHн-c^qf.6r+F;X_DrCb2 +g)?sr9o9B3GI^:Β B\B3g+}];WtU}"zk#7Θ!Y*+$ˢ,g6H&0T&05iߥ7ax= Z'l9ڷHc?1V=k=͡n1;,uZuQ'+Pd`{gDaR\wӓg``,@dke-˖{v׻zz_^uAWeda I A0 ss?zPy9/ٶm~ê%3f}Ńlog*fN6PC}"pqcBYhTp8|IUZz]r>+u4N oDO*w KBO~AY^݀v7H#Z9V<(rh< U_|W1rqmT,$IYs Q$k r$ϙ4Kjx5en+Lh4yaXL+^ZQ6rԀ~9ԯGfݴ -a^W$$ (%owkt[)yz]gDyA埓h*!ighZ:ݳ WT)! {If;6/F6S_p@'M*5~l5 0_,' yόYй(sjZjSɐߛ)M%$k,5pus y(MVx&tl8H[5*@*R! *pREעtl  D `Icf<\Y/H*bεw7퍱p 6`yPaL˚S92:~=PH o aq|{D*X$@#' GBϾueO n|~*ĂyP I@6Y\ٔDfo^q$:UOV"qiTO t]BGHu٣NGW=l&ۇ_m)CS#SZG"ce$t\d7t \Ҙő.!RG=Kbt zYi9neH<\bQ=HF N0۽}{)^݀v P = `1XپWnKfł^^s X{ct rCt힖$1*fz@s$I.@ YGٕls3U>GZFb $e>lC탓% 䱼%9~Ԥ 9"D.VN]*M *ɻ$saXRT$Q 2!`Y]h_gḬ̌Y8,MđQӥ^J}C^moI!2;P1qd3Z'\V=|^s d(ݕ!BT˲Ic'>lpk@ػۥX,]rduOpWp2~]%~σSh֤ ճoRgT& "G{A 1{_0/=_~ $@j c1ߎ/^;K0Ȏ-`vGfg,9}Q˥0JސP5.1o"4k@9".Mqd%ѡ*.Vh̥W5-ũ{E科!dYtАopl=~N$bic;ΞjNϥZȰpss9\ے mЙy -Cn75U,N]IO . DR#\N[W"_<sO[:]39A[<Xk$j*?id`rR< jjpnW Tw\d=0'?bb< D1ܡ(KIܺ(x+wsnkv|}Gw?G:!g{چ2'W8(~70W$rYJ45ޤP7aV Ūaټ۩`Z~o0֎?Vy$BTqw9l-g`xW0]=D rL?z푻ۮop: SUMci^$pO S!&͠RA" R>1R?^]!q%8N D"J5v-?mfX݀|۶Wr{ >'|sڇv$T=O?: QO h1hA姩g:y=5Nz{v@0Wt|/WKU!QwҀ9_y⺽~& #v#}鿟|4:2\!oD)Iwpr,4 Yh-55tQzوz^NGQGBC)xW!f\!!ķ6L}OXy*1 '6<⣽8,%U{eIŘR>%oHF*eJxLvo%2?Hy"堩g9ݵtJ+%FޝI'B=ۤ4^s_o>}0}0 ,po|[EM]3E.壠Ζи7O IDATCY%},kIH³FyURھ;ޟve*ҵ#;O!@1)pΝ"ޫmJ­?[r6Oxr?ٟv2N;iCi2NmaiC״$1ӟFYMsn:Z['^^>vfVG`ߍ! D]cYc:hEQn}Ͽ|Xtn=u" PUX{Xk$*^x4 x7NWXђP .)t|/{ Xi:qF>eQgQط5`)g4G+r o}f2U?Z3RHVI졮ߓ؇NPu8E\֔MIt e՚~A4UH;'Hx쳘Y@HeYk;hUU]7"e)ݽ9/qRhV_f|~w8Ib7EOE,B> OaEK4d(W=FvMkϾ÷@™ +g#U6? b21a+Wa_j^b@ i֧v/Wv=whjPTCrZ )!e-RTA Zuӥ)T7dk'D‡qaa3mR!lN/cCW/+n2polDA9EQ>s=2#] ^0_тN'?p9X^(xIk,OT;*qac+[XʢE3l >B%X雇׷^y1LMQOYPm3(Ky@8Jy1Wc2J)9>Go{T<' 63ZHS AU"2.Kp0]R(=!0-p]:uݾgA"+q{UAo.kK j"a+. ז8+T|U̻P爯97kIvao1[@(.sȗCbq9's"⒂<>;0&~o>u\ASs$ F$Ȫvv2HL_ . * &QGP_E; ̸t*NX(\|޼Sݘz50=np 6:"AC^AKoOTs{-٭#3þ3U~.3`zf@8fnjv*11W@ۀHXPz:*;FݴsWP%׆7@2 .$i֐1ny=D BG9d7ܒ' 8)cmŠ ~=I2UhV w ^Yp1!D4_|]\L 9:xt|:wⶸ1sj\J"#j14&m#@ G}$@Yy%lp55|}wGa3Kx*pټ[+zX ˒(U+KL:*7#"tUAP#^K2ɤ"]ش5f``$,fUg%Z!9#Im δc;H:J1!F?GtͬF753B?_taY J9" &渺E"K#n$YF;ٶ+ .n(*$yd*f)8anԖЬ-Yܳa7cLLQDŽ"1xJސW|G!3Gސuw8MQZ# ̳p7 VOE@Iݪ+('}8ԛu=%0Pl!JĭB}Y'k'z.HR݌ :Wz .pƆ>Yݹ0wNIPL\ޭ W:)DIYxf ND4 U@)YUlbt(hϽc1Cy1oce͛UC;#@\BH0Use T~'aXHvkp9QTS~韞^vcs3AUJmfM5ÁPf!r@rCI TUf)4j$*)aaX5r"l|0%۸ys'^4dxUJαHx@ zLPzGnTsNg}=9mЙG -2:s?+`<9wHa937ܐʑ/y ӅJ 1el{%Q61)ϣS&)H`an"GyfWǪdIWO+(}x<ݷi_ϟ|uƮ#a* $>HǪ }_&jpnMnUOd\ri 9c_<윙ّ ZUֆXȗibBdl 8p"€K zg`Ț9#k)ȚyP8 Ыd uE?UG _᯻S:+(DrM; 5988r#CK~bkm$cR20vVQ!vPE< ǯnmMVv7rٺ(R h#SZFBm$VMϢ4?I`ǀ>m\­Yhm/ dI! Q$^Xxn%&/nOo.햏ex딌O@XV[ Q0_4{|dxVb.֭jL$@Zfrcl5NK]ǰ!ptKh_xnɣ &B$|z.7Ӿh2EB2^V 8q*wQFN&{g?Ԑ#rfd?{1nLꖼݤF͝C=hÑa% <C~l5RJ ê\ҿ< oyBZ89'7 ^TȊ]LCC5J}DH  @#gk5hu3a!`&;C7=o3gR@!l{sEMGK,!Ni}JȦЬަmajIi5j`n,ᰝi1`0lfB4H%{+9)7{rR\Y `xBdD術X؛sgI ث{RG嵂~̄<ڧ}e@j2nTc=gLut8ʝ~'i6 /6UIzGÊ}[JkD 7r%~ڹȆE8+KAӌΦ@:i%BZ#Ry,{%fXKDpӇ[sw?Br a->9HAFHLUK.daqåVE RN_n; ٫㻚=$@Dz0QuKF<#fܴؾO o|گY_c<R :{ CUn: ϽEy[_yN ˪`s81[c`(U))K $DƀOК 5XQy|cƨt"{=欀ǜ!975.QߣԹ0ȸcih"Ifh" '2XZͺ(4 qwR9[P otl̏n ψ1;),.omkP8[jzD,df0S]3࣫?[OL*;d Wȸ3. Ͻ -^ khD%ZVEGT\TXi;B g YbUڧiKT` GcMæ|QMM%W0D. nu/wG26 eyH'5">ڻcqSV<޴dC/t>LV,yM5ڛ|Qk&BzP(uX<SEr[? ?YYKc[{{%@$"@QW!JZkwJ)vh Yd=Zyxgh횜r'óX#V)*̞=)t_'l!9k!F7H)'3_FdwЬͻmo.=UEHI֑M'D$> K:ߧțnh͑%b\e\O;"+<,`747|iZ2ܺȂcdju!wk'(UϺ6"g?ё Y'<eʲxtHFdI1)i7pOm&TOt hܮ"xDXfh~L0țohɒ%;qۚݠ8y@gCM '1&ǀ.|s ;Z0Mq;:<5pL<*#&-S2 .a;'@`Q\,=6%Ό->wu\oܥS@'[H$(h QgG t.-$ {S{6\*%GfTaܶ~gѯ̹c˺ffUC§zZOG&5G$9*hH\*3֒1k*%~w8U =zRR D/%N@/Vo|L% [a^%W_t 4{j 8X#uΫ9| Ds&}Sʳ}vc,*2E0Sh+ن=G% gym񗂳̴V@Ejz6!Ǵ\5`,vRsj l{ڹ/,PdY8%13 ]rdD ٯ5g'Q!~RQx_0&pۊ 3@PrY0 uO=QHyb_{cpgp̀H\*ZOW~H[Hy=uI{]uew.Ky@z'|2EQ+B#֯me,v4|xhyCfCVy_UD2~=H܆bFQ@ aF&zA*˗_MiE̻y@vF^ ͎nXoY А{=iҸ}/q(ΎɯԎ_t^wD f"գfkim?)3&chv&3h. Dݱ?-jˁ)ly!@R<2EE/^n«F"d3Gd74/HsO+{"TL0w^?s,ɸ Y=O5r2@jmSz8L%>}>|Qx*د:>jˁ2ya.Go[sƘ d򿥔[zŇNBz`b&$#oHniYq<"վ ɌG+tN \6LB)\=+Ky ܯ7sn&{0CyODp"k&\iO`=v9Z:rΏh&Sb }o S$:e VQHt{}S<"4H"kHf)W(却hc@c !W_=аeG3Q[_=e~k.Goa2|!Sk+ _-7lPm=   Q[T|eFb R ʹ7JR(%d)]1\yQ1O*m甘tNz@ wC޶UX&]LK!^,NB\`[Ӎ8$ˏK FUjo5SP:9H0 @3*uhNdscIW{˗ݜ۶v YܶU-ˣgh6$B2YCJ~اLQE9囪fW3?>"*VүaFZBQ-P8(KK_qE ( pV@.1K7PBb2Y8%U{m g,%IDJ1eP30/RW&ׯnނ"B)4"y"{bLf=1eO aL+R00JwHDqyԅIT64NIC@2mH}B:YLd{wʚoXV1rֽsc 85.o!3?BM|msXX+ 8yn+ЍW 75&wT#/iQ7Jո|y3C$0O}a}$n$@[(&)dCbw? E| MY,fNol IDAT}uL_KMwv\L缁T֧ZK:D".򺛞.}o~>Cysa߬/=);锤^9F`qSxvOVLL.JX(.j.Cai \,rlK}w9wW%w.ak,zs9dSg5$΋{МZ ˨z87徯>VH)z;4a˫IDr&A=Y!^05G勼 _ۑaGOiY…Pa_%.Ms%N̚O5p{65U걶f?ݸd0>քz4c<$PP(tƀw)rhMzck;O\"WC87(62-!oc 4VaSٺb|ԈEj OjLV #K,??qۑpƤfD_9qʊNby,'S}T9Ѻ3[l3nAeUtSA';1 VFYNz*"{ P yrB<#Pᅑ;mXt? *]= LJ_+ryZ9CpΏRR>%\e}Cbl0doK : z rz :3-^ OSMNz@8k\˓p 9Xqm1uwe# `) rhVlSüAUV]¢L aH&<#=kh fZ lRRWycWcΔ툥BQ^lb>kW!D7 ۻ1$`ndk*(/SVC&.)زM{vƩSu C7{ž}F^Ij_{Hh`( 0)}(Y{-AcX@xߕR>cˑ['~q| O| 1x[4ŃVNi2:#!1skF"碑@: \FbVuF cpHg 챥D׽\QC+ppηzNBpCp{n 3 tǼ7n4ubL=z. 7lkDH`w򖠳qGu[_⎕ ɦMG8EQ6J)&KLf|DJy !hUP}|"rU@Gk1I!|۟˻}kzn~x~e9 ̍jHk9$"Ds!]zY@gʻ"ux+?w빂[ݪY?d8_p"+oWQwy˗ THJpϿ+IDPsR_%5!7 t#0UcWvvg8hAݮ:$;bi׿ goVT!\@~QD89?60 rx}r*LCMƘ\fǼ/N{Aީ:G{y7WԼ K )cc_ ZqreO~~;['3ۯ;L *&X'V al cr9~y`{G!N=Erށ;o|L͓kSFr !zGwEqb r%;'r;Zs6ߌ}U¹4pΏpas~i2dX"_ɛ Lel_7yEwł˝74`CsEwfYȨwMϰ/Z !b{WGnۃPCϼ_AT]!f\BT1KUA&7 ~u@+bw`ҊKRMo!${?uϰP2Gng_tJG6[K!72 d)6Mm.W%1 SYN90B2OP fZfjsK^Mj7ܒoTkU?;*gD2ܾjT G[AC !<#-4EM\sαki(P݆LC KqZ2ق1,%s3+O^`Z'IgCmxA 8{1+LB> L\(6RzVj02NL,հE W$bYTĊ汤Q;OM kH w5 Zþ{^ wU#c՟~H8 !wԭF+@_f~D_DBX ]?к(}E&No T!j $c *$B0&`Z1}cp`/kXtO$ZþҙmW̟~ŸEU6F;7qV w.3ID57cO0V;hc9~y`pBJS4-h0&o\%qN.nM?= a y>N"M*`r;l|Y#*l9r$H mvMb_%bO!Ѹgc##CawlP8SBٝ s5NkI]{Vs2?\ŰCy;լ|E p rF A_E~P+<_BBvY>X6uqEE**@Tl@_D; TEtӳ"s^uJ"wY?\gshVAnvߤZʄCi9(,M= 9YEk3.)h!:$Ѥ>KEzId[?No gMgGr ]8Nn>_gzq۵ ApIcB\`;$s׳mPUB20A3^?xAHHDb;8%;>Nx&Cp"K53sa=d67fxԫB3$@db 91|ٯa>A]K 9%֑*yiyj1$c0.~rᡇ"k >{֚B4øs!~P~kdavnXr@bZ'DW}tܻ_no1 o Q0c]1 !C_g\B\ 0/8';4~iQzDu8}kkIGo9H媁J4+X9I1eYW1wu ARzHlZLύ:AqXZ.'ky| 8k|Qn,746gU0J@i$'3#qVpG?z^OScSbٲ !\>Q ƄSPs;v|x h!/A3^89?47;q K!ĩa1.6DME&SջMk)(8w B)~By5Fo0C 秎6 Ff {Ы7c١#g}6A؟@wk^ID܉i0ua' 7-=hӒaAx8zqxOp5i*߃4f)z)hO7[W}Y%C%c١W'p1?c+zթ|dy;uZ$@9? QeL9+L#MCydX2py9$Nݶ9sSGA 'y|M=$}cնZ$@Ŀ 6}Bs:8 ] .MoiЄA-w^zd{;A8-2Xֿ% 4s{8 ?'=Ove8ѽ߆RsS} ss)XEe~:cX i^r왆cdD#?/0H$9>qذSww:j0m OdN:CZ0xOGRpk7q_vظ'[~Y-b2Hq1uXw>W'2} s3 ŤGɦO 'k:(YLOR~O]{~As N]UP (WBdZHi{ ݹPUj`pyi J./|L4k 9 Q*K)?AO1~fR,(c?78sik1ղhzx&ӲcEپPhzn4hdt#!ގ PrshІsGcU1s?_~$Uډ u1ځeYki#th^  Bh]k(H&(W,r;rK $(.=m2bxv«yj¼~Wf/BL5$&5\ 2qdFnXG^L4s&CTg{}Lg}á%l@ &B j)~,@B,d!NUTfFQ}Jq\o'|-ŤN+tSGkd̥SZq TL=BtU@!ʌһrqLxiOL{frnYrzdf yBQ%}HoMzs&STk<%7MHA B:"`D VV)h"h;N۩Z(A*A0E *(&A K 79BQAB{~0a!=:}MY4}ءþ3P׸*v>}h]nC; m\9ihsgOms縲 JrC1v5n_1oU甾g:&p+U#(]hy*F =n2Z&IdapUh7M*{Ǖ=վ}lYm늡hKj\6fJk<3l9W4Y :}-M~ctѰ)PWR."`!-yY3SUz/jt`ё+jEgT )mLZ(#BڣVA$S,Rn9`m梣6hme_^Q[_+T{;U{+hZ)cnkڷ(AuCh1_CL4٥]7$-^>BfWE?nH$}{f^b\tTuY iZĐk:Gg1h@ovۥ<,"J1MkCRhKF([GpNAp%m?2hu@ k"}g[S@P3U?W?ҸE @ӭs.9mjȣ,t;%s23L~eh]<loV}\-~c\R9""J[x Vty@J\g-ZKTw@n-U73d.fP*} vwcòVAAq3peD/L`tnjr'+6K y4Dp_"[!:uѳAAv[%9l%aJ\F˹Wi3 +˪ I~, 򠀠qf-3;(Oԑ7Iwx3`j:W*t>IhuD#(:uThR h5Yn7Ew׃,Y쬲,F@n#iQXE!ŝwN9@( h@EQߔDIGI? 4%K\{/.uSݮ[uqBA+>C @Z[emlY4v`V1(+nUW?gMϓt4^dD̑(l)$?}-xfyqvCxDҗ"2m3-o#x1ݼ,fs-iBAxHQzS@vJ-3}Eė$!72by! u}$-u gA^)V?WɌ;]( ;;䍛ݝWFU,^IIıeY^LfH1C7RJ0_)g Ȉq%rxȕeY~G5?*lt%3oc~$e3T*b"g(`9rPhP2&vRj IDAT˲"(Nۉcl%(4z1M_:ggi2Mg;nU)ٓxL/;Vi53%>=?2̼1"ntVts$+i7>.A3;,$ -+;I4))4MTTtOO?&i(y-{$=;$'H:A|D_f^l[PmdUS%U^]%*6sPүJe9UhX'<tݿ(< 8H[ԃK/T1^&H9^VWF*{&Rj|A 2_u:U|E6 2*dw3Th Ո8LD hBCccrVUce֥NYtIꔲKi]RtI)YoϪ/Ӏ2%gU RFU/h”:~a~ &4GZЬPoW6W֦:V-۔,*VUZTU֔VA5U:J]mx b'^ T*)h0Od) X@ʲ0Ã2֖ fv,ÃFafI$%qeY^B 7w ?zwÃY呒x>:V+ EQN@x,wS>^]q%ghP#pIXy+ / ~C,WO`MwUԱ/GE`wu8]\iw_Iʐed(PG~GJZM;֨^qR>PgA F HWHڝa@]|.8IkI`+ EQz˲\Dh?3I9 Ⱦf6(KIgH"2*$h]CfvnYOJK%E{$F#kor?|` )Q@Lҟ;FqUD(c%Caf8SR?q݄盱#q`d(=syD|Hf#Y$ر=of' rE# ̼̺$៏puD33# 32f_K:KRCyY@+3̼%'!.2AoHEQL^{ʲ<_ҏHRYGu1JLj\dI:S8mSsS 1 L'Y@IDATxy.xsO<ȉEDH+ϒlckZ;o-I?o٫%[$QْALCu~߭AϠg{0j +D-(P@5 (P@5 (P@sʜ+Q@5 G0Kfۙ j?jQ@m Y#,M)L. ioxjQQU/p8Rsg}m][83=, h)P-j(`qV'~Wsɿz:{ phw!`|&-@pʸGH[^VCD^}5VjP ^[K-^(r2xP*|tt`p)`8CN6$ȗis]qLEg%0E!7P#b+Fc\"c8:DF79 \qDƁ##=KG촴|Gc3+G`pqP RG) *vGpp {#Sa WSOyEMJ;b4 EmB?~̉ϯf+Nj4T9J/% % NYuH|eG qP\Q&\N_JH#?jȧƤYD:v#M NS2,B`0dqRsUTc*@2?LG; 0qAP][|օG&̊Q;GQG7[6pX SAnGӇE8A@8j3Cs\lkKBp"0΍ 9̄Se 4d,s#.C&]HXUeqHq/#1C~@hD[E4`~q|DP"N*3.qo?Bht]DsM&2-Y;wcmtM@.8CJU9 5aeȕ=xv sT|×bҌ߯61R-8nA5;WL^2e\rԫOO!5LQD} |+ӿJP8sZcqͺ/pWA C=,wAa$jοk\- NULN"+h`@KD%`ث~˚ZD$@ 3=Nd/xSg [*e~l&8}}7}`ǩ%a7)`~9a-WpXTrPhCswB5U7ׇS/4+`,sļZڕIS) '(XOC.(c0!|4#ɣO< $*q&8J@ ] >mɹ\w{u}TmɊf?w@ #&H @~ڔ*-Ư ۿ{}m#^ >^j_$8\`|1\Gl8ylƀjH)<5<p8+JI#drZͲućq9X5njg\]఺Itha4&hBG&hZ6h0[gSTܵ0r/NuzIG]lYT#icqSc']AͥМHБ! i$T']7 ܃g&I! -B' *K:XXǤ@Ú|YT)׀=4Y P4Q5\AN9vk`bΟ:e45B35D2# WpxM%]!>zwq}\jr`b6 ˇ&PG)P!pmr@ fԆ _+궇.6/ _l`2a|cy IGFsʩmNKÐkz9IQ dD-nA t$vd33 @\C5u]&F_}s 2g8 EX \F ` NMh_>նidGk|[(4!d(v,dMm4( |}RjHH]CC޺԰-ru%Iwhg$P-t .W0ҔNSZ@Ű5B98nnVl~_Q}OXj >76 A]LOsSvjҒ28%1tk#gZ Zg]pYv@Yu6Bor D Ħ)$pog|mӇ J)M#6'2!Lc("QI -G)>=2j Pǣr@>x鹫Q >20b4坑P@*#dB(b^ɕiG{ls{L!~%_2rn.G%' R DH ~x7D4CALYWjP uIV,pFV<M6 H B舦,rW6ʨ>= 0+Ր*S> i^D_}0xӰs=؀?8$P;sL 2@Hm@[x}PD9qVέvSr~!>;PtBSSNM\M?]x~ᷗ@e .Qldż,' 2"5ɄMˁm;F/lOy[auKF !ۀ~Ђ>KB(WlƧ×MtأCS@X TddO.zi ǝD/n Y#Pki 9r9:zEdPz SгDTG߆fEA~ٛnx us?kܴTT&Z1z`ᒱÝwx5}5b-OVa;tPTU 9KN ӵI<"څ(* pc QUDѴ u7oI1mig2B7ux Np`52dtj OmRk@N x[܎ ,4 {1*PdSbř MtBZAd9kzb+,ȟ]~yuSJcZK9BDɔ,e!mXm6: 5Xջ_%onO){~QS׋tfx7d=%$?~y c|-+m~QIVg.jaD+bRh"I q5 '\K6N^5oc%L&?V򕫕̓{Pv~۠w*:@?8 "E}*䋐EqTЕΞKnkRp7UaS&jgk/  TBy,.ŝw*b-!Ivn.d 8ȘS%2HJ T< Zmg+ܒ/l"t=+ܢcQ3\4hi,]5`|O~){dK>&ـG!sOoZ:ҙ6^KvPb$=vj. 1gUpM+Vp&!M`g26(U L_@R.Ӑ0('X&ny zJypk?(_>Ӷ rM9R.PoY`lVqwCNgO:p\;L`anFL@/rI'v9SiScNx, A `UN.rW/V eKm+a'''lR&=HQ35Rz_lL}!X>^k34fj޺J({ԶdE_2pڔl18}?Hj.GZIؚg&#uqq&.ĀK"2)htX{ٴzȉNxW}fJl,,dP&]ǏJ0)0BrKf<4uH*rYpn!{x|6ִ&ğ"MEЦq)?5ǒP_GbH } Nuc$/{tNublh U2S9*D\K[6N4_}vj.7lH1D?mh] ̫%MFiX~*Y, V]o[~wIx44%k@x١suiTט*=D@%p}=#*/D5*e|=u˂ɿr}ZJ|`df?d)G} +B2yB@Ibl oqb- EY9 $fo |+Zu,<2! Pdfe,$;,3%% |_+ONG]6XDKf870mV uʺns?=%*3Rr;(0yg# ۯ]ڔ)wm4qPs ,IB;/5Ǖ]j&BSs,\ֹI7OUrz,E2̊0&-(gay0.ṙd!bp}&TFnˡ147뗶 xuQm=ϭm/}+xS?B`1|+Õ*u٠d`l! \RƦfj/pаf]:$"!dRUO擱D/5)fq?Ϥ-cp a'߆\04م?0aVg'(&Fg[z䙹7Ry0XydM@4=߰5D›Q^M-/mfLg'o]jk G(ШXƵëh~ᩆ#+ЂZ lժR(P8,B&OV3.3_ 5|z'_D{]F™(fN.ßL9;Ƃ 2+dz4d͒H5UEB3r}pg}#@؆`f7 9gލxf)C(c!2f$Ķ:lXZ(P8kunX: β|qqu7_(#[ 0{݌i;zЎ P p,r!Uqt"B'M8̕,4?Y|deY,%.8f6VfQNՈ$u֕m4 %/MK5`_>zh# nݶ͗{ s5 kӗ<˩\?=\ 5>taAodĈ+jS8=i w 7SLHb`BD.N3VX GAn"N5"8v`ȋ/EgZ*Z巓ޅ|K, 4ñP]ړM:7ZNh7cO6L_ȘQ[ f(\ Ƿs99\*qBd,L,CnN+9hGNi[eՐk)Ky! ϡJ܂B`rѢUb4 مݎl5)6Cw_yf?ŖOa Gb"IU5,+( 5$OXg^:|879Bqx/>J%G1M,Ř׷(1JR~eqGRϤaEU֍"|X Vh 4Iª6>k`|{P#]oJѐ)=(Ptk}Eá%}1 k D/+eUi!:"gZ>s+ps`,nP*6@ (?MгBH|f}*癀_1eK\O:[v" rP{m8ydW h:PK7cfa0:Rf I -Mce.9OR0{ph7/: 4.htteYڐG{P+WZct+PVsշAUo?J4 ܆h^ a 1+m;iӃ~:d~p+g\ŕ=lWmWp9b aYd=n9*J,Z~4_u/E/Cama_0>NrmCXW56 ֆ1jq&P^`e˙<3W7\NL:~LE J}4% 4־ hٜ`NK3X 18m<\b9_2c;-| I4*6#B<1| qΠ 5Q~q&ax0Ov9dx4tE XqHK -ȉ/^ܸ ~՛JA TNcS_đg:p\[*)8H0~8~qLN~u]{*"tE4GXfh\* iޣƤ`%f*v0+ 1raxSJ 2f Yo\sf{Bt9U`qF`z:Eue*OvMuIX<#8‡=oy^\$hΦ{_\Ow3 \ȼS㍇{+{a-\Km%&:{k&3)znykF12y*2n4d~b\7p;՟l̮RGDBa-9K;ԋׇā1pJРQ0.&`ivn>_Ϟ0-EF7_HUS'[z~mǍ|k\ ~ dMvQ+ECZCH^DJ@!sv:F6_|aSs4{^ !뒫.eVU8R.2ĩZ:mĩXc> 8Q/$  RX a_MKcjzH%qfPd̗>Բޔ&%KҋA0kEAh`h]92>=rwpa!QD2mnKw,8XP6#= K!a.Q[_a8stA ZK$.n|X{e6B/^ y =bRdJR(% HJ̢d|uKgFs#8, Si-6ZApKF8#M+y0qÔ}캿=M[w JT:p57׾+]9OMՒmceSF:<,7EE;jL9 dv>BcBCm619}rMԩnU >88R',΢P =:so6dj e3K#.ePLHN&'<>hL:C%&xp++Ty% 8tOd׆NbOX07u:E=yZR.#/Ee7"%:~ t߅i/W?6 ᆅ#'SwbG&u'-G{3ƄR 7q0D]ϵY9?q& D/ sX |d]+ƴ? 5޸{5ܡ6.Wn(E8 Aƕns{0²Kܻ6{Ht) qIm*u'Z3y͒S7\Ij?%\xS(%bm$i9M(;4GG B Xۜ`t + Lhp($0 qG#݃ΌJb$>1ª\4QvpoŖ= k?4fK!M ͵D #3Tcj4sL p7'pq\^W殍!q5"5+\IK*vv$# }&:(1pYJ)-**5D܇>MnIX^j{?,P(|HQ8J3+]߁Ǎ!\piY|KwTZO6K tih#I|jjSFg(gBQ'?*=ʇσi OJ84EK̪ q8؍sQpSf1?+- {Ӌ^aqAi0V$j +>`cLF`.n_ZeXx4R~?$(mDFʪL҆$AFz<d29YoFΝQ铕  NO鎥x9Hc+| .LA}X1VԵȱ4(a2TO-v~ȡ!9t:6fYAS+FNm/QS=\e 7t^pRE(f 3e?e'5($ļ8;~{Y*Kg.e|61_+$Cr(Ab9l`xaŕVO:qw=@s"@aG@] #7ΟJ !T7s&Žng/c4ϴ',wS"%A!):Y&XB-~~Rsy<U(eN In[Ee@2p&nK{D0)M <c"VǿCU! x4иcڥa:OU|[d2_?/~sdb.9cuK&O^z׎m]x{R/d2A]8z _ccV`C[/Ē)Ga^12 d!MyYb}O AENBꜺߕ3eDʙPB=~骦f,e) ,oAQIA` zQB)`mYq͛Ljl,ɓD@'^TV8؇,귶_08?3p%,NHYGNQ./ 4;էqmcʾlѼsWnA[oǴ$xs2i2%9e-~ca=l28Zce ]Mt8J:P7Qs8XquD{)됹J# r.h %ol.hKD 1wܷ4~z3Ջ'AFt?x`+CBԸ<dd"?h gdPT2.M'lysvl\EѿVy _`a1׷T 84Am'˝mBڑ-Xd0ӂq,3qA)5?/[(*"u;+G`|!xYfPQp\І=(@ptכ69Edb[?J)~xH/z\?3j$$gN+SY4UDZ!TclU[ ~Q-`ېF[j1|KR0993p AǕx@$ǤLvtxa ~N |IHIM{$m}R( MVNJ>P4{:RZ_@yX' ,дj[j\H#>A |)-K^:hcU{nrX޷W3Iu;rGEXJHt xM&x<2_Rr˹{E`vRgo܊p%-7h9eQXW ?ٖYBNY^aKB~p,Y<\𮵠m+DlȁZI1zx^E!ѵ[<EXB8Pמ $ ~*~R¿˃m_o)+Ζڑ?܆PrA0ux셭´noxVǤjQ ]Yj0jGƧ,\tK%.ߋ [rW[LZo ڜnEKX.ZKQ T*m';B=a'0S޹=2׹4 TN `,GGHӒa9sGui1Չl/ܒ[2,,#]4vyM[ ;`OVjC¾MD ; pRQiD(qXgpP+syR`t:0x\S4+7EIdt҇C)ǁ7.SQu);Btnwq1n_N,p,ob{<&DLz(A^c(eWڰ⦪KYS-lG#= WTYqEÝxT)$ug*TV"O] \K:]% *,HZk,9Eb7fK@IDATЮÛ=iA~%ಧk}TH 8 lXZ((M`kH^֑%^zfVRau!.o)2l̨w R?] ?ع+ aQS `+!_*u.& E9%aŖKa'~,d/5LWZ#iX*Y|4kB]?H%Z;5 qiEI )&=kAR({dDŽߎT<#nТTK{=Qi~m*սc{Sۇmޅ?S6r f)82n:EXFc) f}ĬXPExuq/|ux;96 vr8i2ʗsږ9n1I Діv` c|~gv{3w|cGX'@= c_ NYe8 xx ΃s3uLz'1l<5 J% Y|}=n]K ,7]H\^IC \ r{QנҼ4nS|] :?s~V`ֳsvSg涭L崅Fb#ď_0Wc֛oT&YTRK qE#j{2Yt@'*Ÿ!MYW2ZP41Ķ۱OaYC׹Pr9{0h\$G{Lr#t*'D݊< ׄ4•Muca.{˫ٰ0sqі;/Y8@Y{Wkh(ָ Aai 8^xPšMnh.ғr>TxUah+h 2즜sd^ \ w,TGxIA͑gگ qz灵}^V\3Y 6leK;Hds,V*!<<7wuׯ21g9S&ĥp><, z+kAN:x4,jOPFBQ?z2g'-t%uQ$8r5u4˅]3:5BU;me:ȇnÔF>zUv/VTOb|AeY<ƀ,t6uR)s<~߃VM8z,ոs.;yh~-[}, K7p󒩧_o>4\NJ9C}c熡4^IX;Ҍ\$92$ΔjZ8٢մ{0E(D8{1z1Gc&"1sn%p|mzoXr e}A@ I&&_-^FuPKOo%1\-ra1dhCΖz +p::s9J!'N'a֐O6*GY:SQcMO6~ёEkk78gi{t-h_Mk-/xiÓƺ=Fϫ)r(6 6ݶ 97 RLxnCZ8Mx^W%qM7$_I8z1gTɁ@ ? a\~):XUoWY<lI;\oݚW*Z~`ѪQ_wiR|ݬp`2?r<^vP3j$:GJ]=)-G~,Z^)~%oT%QŔ\Sx4vdτA'X).nO ZI:G`ࡶ{$] gK7M//hdcCsrm/㮟v\DPpf[jO-m|]:sI/ MT2K` >7z'BY4k#}xp*%'8 s 8up,2:jii;on8z_;]:岦G\L䲴Da8"K!9"4CC0ͷ-n:-{ߛ0Δvh!`@w/=8̤M. @b(aJF>1u=K/'q=ÚY:5,;׈+Mp\z*U_07rIMGNXҽgJj#*øg=;k2a.O<{lr3W_0vSx(H7}- xMY+b#s}mS{VJ1_: ldeYKiW>ܐMF"6`?iE:s!"[$dW |Gf3Q>}Fh%{#AxF͊q]u2\M0 3Xwqמ[&ΜZD,L iK#4-ׂΟj@nSP<}BhMΫA,_ڛ݂i#ʶ@\P|z#xIG 12DX sܖlFGP~P1U@+4Ke){eq,.[ O'XpAR5[/?_RA) QWXIհב a/ 7x⠻BX4 T2fُ%z qqk0`rŁe. s{c%&smkl<ʛN)087rWs۔w SU)صJ-sRAJOUy%@HV.\DLv/_W.R5uPb}}+xvYA>i z|ՠē#ݺ% r;Q6.1tt{Wn6洅RnX|Sugn|gG=0 +y!e, &:k5g|b'p[e?T,JW Z1P݀9I=(۰ihS%AO:A;%`D L*1*oqMCr{FAT>L 3LX;q;caH-&Zvɤa#}!KҢg[!}9\Giɔgٕ&,iK~+A0幟Wiꀬcﶬ}eGpE͞;z;%x`{cu0+م,~q'͓&3AM!I0/P!E[dҢaYIy*\niY_aO_9UJG!(-f80MWwL跣0_Uem\qf렚E:xF<:qwZ2@ <(Mt9Mᗙ1Ad>9Mz }K[ Nkm@%!Ry ͙7CdkG'[xwMw"qN0vE2-*|ҿo))(-YEK(~s,\ \Q!*yM=Nu']x/_ۆW 3?BfCapㄗ0\ S8 z?bE(̬xj" N]r (H#ҏ-r).އ~r\88t]i]VLbr>6n)~!C: k`sC{pO@sYamcૃa#ט^6M RY<^[hM] bA[4ť)b8 nڰX+]జld1%x QwX*)|ĩ ŝPٮAV9O 1dXD) VL-=FZ+{["z{l]ͺiD-*) E:pEH~\Cbt$0DN.$ʾYT䍿d6=N,m8P9K'ա͛bgQFaT')nY^ZٛaIfQ .&jMqTD iq ihQ q^c`PBcј 3g3V193I kaf!\{tl;\_|3s,v3(q \tz)c)H4m# R2LnPBN$VK^- *;{mAe]%gV.+2뛣U9JVfOޓyll] q{l/G 9z x;båoutՙ2rHˊ$5m0vN4"Wgw&[vWJHlcr73#XNn+Yrlsuĕc8T7}*@OTJJw jl^?A>3טm¸T̓ZbL!1)ђܶo&Q]<(f]vf]!4RJS܇ֻ\e(OdrGoS9}AFS%#ӠZێecɖD,s*B@AUޭ,׈Cr+6,g$=ցC\3^rbCg;dx:_؅}b3?fٔ(VX鼯-.vr HoAi6 % w=bM5C9qũ+XIM1yW *4a=v4uт?S!ޟA#[ j@/-qAˈ+]!1{Sv /=q>⊭҂($@]oe yVjj{W:ډc)U1D|e:21żmO 1֤(CCWt(M[Z(*@ig"#ȅ|}\3&_0Qꕥb]8yIԇ]>D.(Eq iAU`O>Li[h.E\F\MH&KR+##MyWuPu(<x;W h|jSૣ6X1e[ JG7L5mmhC v$!g=z%K15ѐ]GCD9pF"B³̾{3IX ( LQ.{n4ac}{R0 TʉX$"JfB1؅AG9~gP~k j c[/ ,Ͼ>,PgŪ~?,-d_9VHR>TnWjStY8gRbo ==:T?7V'sύ^'=dzDr"N7#4#tV3ie@֙5H`ռyD$7;jAFa߮'㑜O37b>qjY܇tZƮj*lF@4}s@ }D@/Nr[g-yybuDg ,T.e2djC94ʡ|>Ԯp h.+Dq8vI>wPZ·> ]u /)Q~&爵j98KOoyobۡ+/⼱݉uZN(S}ʟg r.u'7 `l<;wzMZi{PqE9}A+AWC\0%ry3mRv9*mL絙q8,v)T}Z> ,3rXNBeRכvU6F p-.;b߮wz43]Mqc*Lei,Jo5^6uhxI'K̘h ί8{c/~)E~~ToG]El?~( f!s4"Ktz;F\3J[(>4(߅|8 bMA"D^Ǯ鹘h(T9TwjץCaʹ`HQ\]ɢ_{)QUPrX3-Kf˃H:iaofp\bpW_fbGMQx5o_Դ~$$Obin߼[,c *E HZ8{R+uڡ;;iك8 -VDqoƨ}t4rQUS9˙>  QeFV-뉙Cz멀s ڇF]__Wh)sSt*z qW&t\iz ^nɎ5ؘ3cp*9&PW:#Op.77d;q7xBz>Mm?9Q˪M"qSvŞ?pu+%^ ~عn9lҕ~sM/@F?c}t4$#Z,eb=z8Fzݖ>D(&NQ2{KM I p tR4dDk"I7T$tG~?bΌ$gQ_Y73]={P )j*y_goY72TͷF8PWedP[Ӿo~KEhvI{<׉ fN7hkDhu8^3C!I岗Vxyݍ<ڍ&\OW^+?\:К 52Swӿ`j@}؊\Jm*E.r-~aRx\pnO[Y/svFkbđ(q;5sC^gw(ˋy:/ux8# q"4ծ~]y+)j{/7 P"[/A pvJ #u:ބC 8:]?sd]ɸ5`ݯd>n9F#31wW rk3MAdR! eW4g5X]]c2YOx-z.TVj[o@@D1x\3\(L[4+#zǂ\FxͬwƊml-_C5AcDf֖_8Ee*yRfQNdr4RR f v稞̙|`!n9Pw[ޛ'WNթK,>YOϯ1DMj?JT31Y{_r.<"KZYQ^^_ nŊ]=QEL2?5si6Xc|gȧlWHDLմZ)&~s閍oUȖc rRt#$4s]Z<,şZ@1d!a=7ݳc"^^t VAÑ/j/X+@AiK[D6s^>{4ZT"35.-{З*rp`}xct巾2=EeX{`/vgke?'2]qp_S큪ٗ,: dq~CcxTsmV4 b < f/XY(.&0T?l{͒S '`ӷy6rL;FcVYJ: Uy5=+\"a:BE&,!8ph(p(X=Ͼku_DU*ؚa׆y80: Q !;wa TC`-{+¯UjʏRL+G؇>)(>œOII/.)CR\5O7}%ė 4^i#% %/0Y0"ʹrHcD5A?rHԞ%.DK{+P*eeWwȖemc&> QR(~%HWGBNWGHa`0A_83իBCRWe͗NDrIлa o,.ŁtWVU!w+g j#!9OsL›}&cfC |tEG|} ;'p8oRt$`zv݉׼ r9~~J[@O`I#q8٢a,v'['ZLߖ1m:nd γD<(GVMmYJ:klgdn{\_5Pze]{:GwdL=ϾQEXvǞN{zKۻhھ٧zQr+D(%RL OrJ^r`,=8Ρ>m0f-jVJWUiI(tO`JxtB^?jt6ntCZ:Ors'EJ`1c7վ?kj4u+]^6IӽH%(QesX0^C}I$ i|p{bHhoY{:{ [{:C-zWw3[NK"19PJe4 1<|dL.ԖHNZۇp Z-o!8 Q. UgGnj_syW&3,}jWzt׮7X˿E$Z>=^Z-EE<P(xmR? HIKHk ʙ$PU}?v#Y98KQ}L;]L GYNL ;)t^g=N68HvlbNU#N,ݑW.8j6iCya&D "5_eou$ن QPH(gY7I#¹5Ʌp5rПܫ/N 8Gà%Ȃ5*RPLF["H֌ПGʾdz~EVg91T'Keåςl9*HVԩfF"2qXo[@\I<=B8ch F(3t>`#rOېA! l̻ 2G8՗([ш'K8k^ݑ)j4OZYaėe8s5YQȴ__Dw%yE}z-Yցߨ/{8 ImNt5H)PE;b2E F =>א!;P G::A)"iKu)!P楂gmgVv߶o F?yB| A;;7J ybBC|-)g#9Vz s(Lw%R& G 6bTs:/2f0{S=" U ?ceGۚ'Rn &"t=#AƤPoW*C?ҹ Bіx&4.= ɈLSTh 4_$j=vױ "~Q%WTaқW: $[IDATp{"[3O} u2/IbEcE[0N?l Iqdp3oNRGg1P$ nT] +{.Z5O];UጸӺGtjs&92g2{fJ̈/jG"0ϧ Gpw3oBWۙOH|}y&ܖպ[0q.tjHbCŢLu` JLQ30)&Kَpwh 8:Cg"U[X~xQSDĶZ8M;n V8SԞ#N\÷lcu޶p"O9E`|Ř19Wc; oםp^<*PU88UWGmƤ^U 4Qs~~s9|;\s*Pd_F~U?XsԲjhɳPEb9|m1 "”G1nJ2 R T(lՙe~cV(mQ3a@D_Oϓ$+ΪD6ST|=ǚx7~1'@Dؾgb MZ h*28*1 >}*yf&,7Wlrng=~ns /4 hٰ,M?q3uy,Xsa 9N l̐"qGў:ƁL._y#0;=e  o}!)r$=Ɯ~JR3`M&89JS7fص'c۔}zx buXK3_Sv?fzN\QߝO^{!bux_0I2qs\=f-dOM5a^e[Tc֐?y)JuWa]$#Ѿ+ F5.Fv^~Ii7' Cq Y*+T, ,$G1&ˠHemD(&qe43=]>RC҃F7{R苊7*j"p ߹k J,&BѠ^[_'X=""xmmV1D%ЪL}t/ґ͵/FQW[^y/@:o*cb&$o<<~NǥCϟz߅D< D h=#[{׏lYޑ++qTsx ^%&D: b'By} _޶r0'چrEc'[ Xt~-*}ѺF;JLB|99nTQŒT܀t8lkޏ~o8 ߴt FxsO]CsDw9# ;( H wFwttuʹV^b*֦LW T9ij{D=܂.8 ̫HS $JCTє'\ oN7ov!f| f`8KǗ_UŲɢsy?[9@Z*WJeU V`r}form\DV>s_w+WК bÅhvFeh͉r/Omp쵾c[{!&XS]\DZh`QgcrayV4q#S~WJtp<=Q2;HLs">xhp jXIn(iس ,߇gם_t܆ZƦD`De 0`J[ARu;;7/a2v-FY9Trv'@rTTu_@8KB '^;Wcrd| \ }.q`'M+3x(('!J$|?Js ϓ-_{v_=|_?sKh\P2S$(3?F-n95:~`8'UX^P1}b}Fm(גDG#*||3gneq:H:PbL;"{a˅r7mmD@ 訌 ڏAo1tM$0i:.ħMd&s`í[vo=)HŸL\pdLPXG=ڛ[wҲ]Tw[ n~(`xE_D`,\|;O\{ll}t~#BJo$Wa FǙI}m[tBUY| +tTt 0 ,Ixف!ʜ>7cLl%bp"*gm|,:næ;Ǐ;H4҂jpqqJY L`@fcG Uq 8X%ʬOD9c=\H Dljh)ygP\,`(JΥOLǗ)9ftJ=l.qIQ&z:!$R8*[)ŢbA E4}wx O;2f׻N]̆&Hnޠxͣ3ͤq rEhuׂƁRt/ Ypj4FߣIĭÙ;X  |ZHdzY(]A+ٳ  18/7}ĞS;;3:qF:erhTw)J004jZ IG{T\`ZoY7xʍ[~N(BrP>Mk[_QrXykS5y_}sUKloxy4¼Wˁ|j'uYgUMe>ܖIb?bؙΗI,)Ě|*r%:@'i2dtZvٰDEw.}'0MYDϘkzfd:(g`kwUyOa0'sE!zrD#BJo"W):ĂGG^L!idώ|9H h=>W"YVM'">? BR@f4H{vk4^R͞VEsc"]TLY6P,wξn Κt]'s*uD ZrqA F{MQ0@.ff :[p-4lG '<\]3y@dX(VɽT\ٹm'eںS{Z O-&e!`NgzlR^y%I pqGz-~!mZYo[w{=рr>K"FD܆"fS|iqSLMBo;#A"Q&E`R[4uzM]4Zq&;ېh'. rBm'RHK?bq1Lc>QfJ#NI7e}<Ѿ~wZI릮5ijK[k$o՚'HWLىXYmh-| K)j~PDUoj_u> ,biBTR~xNv"jZtaqѹ6d]Mn#7R :MhHdf. {TSѤ E̔Ȥ܅e[7jώVmo]5t_)CE%P'pbP}N1ΧWD7L$1t4h|2.q U8mԵA&Z@m?XB0(@\eBC#A[jt#NN@bq,㝟Ċ?Qxka".G1haRH* %pUR;xwp 7#JV))d2%Sm|8< Ƌ@<gG}h{'*mM@\5Ikɇ"Mb%cAe XAs>Cu $$z Qڦ?ڂ~;*6s ӧhrD/OSACOC,f0Dq! 0O J_;u:P :wXbVue4r/僱(;aㇽxa{DETEOqMIy4" _i/_>NÖD'"9~h9KF6A@\8_wahj6lBJQ&Y-fiE)]KP*8;R" FR}u61k5;&q <;RSKٗiW~=N)eSרN*eszdZ5^g?8;6vuZ]]wEi4N]nj. lću*c;h4JlufEi>F:7SgFjU%HT 2+'0k}Ens~5(ix}sO&T4.$=D;qz~zfWv Yd!.d ݱC| RSEz*D/%H_R | x/a8pr +t> p)t$\a 2?OjBLD8[?]s:bwq|EkZRC9&9c3z |koU  'Xƛ<|!| wJBN'ZJO(:qbhNotyV\ЈtԩfPԑ P̿y/ъ+t KIIU|fq"_?s'Vt-\E1̀ƐV]t&G Px͕1CNOZ` ^mFu;nxP\̵x{yf# 'rӻZVk!*:IWR@B !1?)]v(=q'ӆF˨Caf!\$|*uu*UT rjPS8.Du1Tbm!ʽӰLJ'CNPSSYcd y T,Me':Vm$sTXB bptN؝!tVqJ!Q~*aR+}eFj+Q 2ΦNDk{ אw/;;8+y5 B:f8: U|3z6đ`|f$ߊ5dL8mt$K9+|M#c-2'H\؂Ǯyc2?,Dz;!#;A:]${\ԂH-= (m8҂#jق;X"^arY!1ŷ,;'%0~Mj3KY 5>Di/oޤBW/pР&y$ 0 iV= ฝىOljBTJUAӨZD P6=sYj^DJvqvc@>KR SǪ?t>EĆ־ɹ`*CN?\6xNc#*MN +P tu2%w0'Xv%M]NB'uL"،ei!fLhh9pP]:Ф@mjij<.- VmShGr{$@461[#Yf^M ,y ̵K,eo@T'< : hHФ@5 !ph_pԼ\<O4aߣMNcf&5g_ 3MԛhFk>"S@#LABƙ<~K ~DbS<ФL0S"yR;ֹ !0|и@CfhRI(0pXn$ rMR/>C\U%`wm{pDs|(P.sO&VGR;E4 y;@r!5E "<p݃84A M$EI|ÖBXQZM U::zWr\NgśL+A`d )=~ُ@b7e$VagM 4)  8j=G3\R-/_: @,xpj@CAfP$S+̖T65)Ф@X ryͷs[P^M΢&՝y:,ytG5t>7M 4)Ф@M 4)Ф@M 4)Ф@M 4)Ф>]uKIENDB`unyt-3.0.4/docs/authors.rst000066400000000000000000000000341476461141700156740ustar00rootroot00000000000000.. include:: ../AUTHORS.rst unyt-3.0.4/docs/citation.rst000066400000000000000000000020421476461141700160220ustar00rootroot00000000000000================== Citing :mod:`unyt` ================== If you make use of unyt in work that leads to a publication we would appreciate a mention in the text of the paper or in the acknowledgements along with a citation to our `paper `_ in the Journal of Open Source Software. You can use the following BibTeX:: @article{Goldbaum2018, doi = {10.21105/joss.00809}, url = {https://doi.org/10.21105/joss.00809}, year = {2018}, month = {aug}, publisher = {The Open Journal}, volume = {3}, number = {28}, pages = {809}, author = {Nathan J. Goldbaum and John A. ZuHone and Matthew J. Turk and Kacper Kowalik and Anna L. Rosen}, title = {unyt: Handle, manipulate, and convert data with units in Python}, journal = {Journal of Open Source Software} } Or the following citation format: Goldbaum et al., (2018). unyt: Handle, manipulate, and convert data with units in Python . Journal of Open Source Software, 3(28), 809, https://doi.org/10.21105/joss.00809 unyt-3.0.4/docs/conf.py000077500000000000000000000131111476461141700147570ustar00rootroot00000000000000#!/usr/bin/env python # # unyt documentation build configuration file, created by # sphinx-quickstart on Fri Jun 9 13:47:02 2017. # # This file is execfile()d with the current directory set to its # containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. import os import sys import unyt # 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. # sys.path.insert(0, os.curdir + os.sep + "extensions") # -- 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.viewcode", "sphinx.ext.doctest", "sphinx.ext.napoleon", "sphinx.ext.mathjax", "show_all_units", ] # 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" # General information about the project. project = "unyt" copyright = "2022, The yt Project" author = "The yt Project" # The version info for the project you're documenting, acts as replacement # for |version| and |release|, also used in various other places throughout # the built documents. # # The short X.Y version. version = unyt.__version__ # The full version, including alpha/beta/rc tags. release = unyt.__version__ # 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 = "en" # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This patterns also effect to html_static_path and html_extra_path exclude_patterns = ["_build", "Thumbs.db", ".DS_Store", "modules/modules.rst"] # The name of the Pygments (syntax highlighting) style to use. pygments_style = "sphinx" # If true, `todo` and `todoList` produce output, else they produce nothing. todo_include_todos = False # -- 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 = "alabaster" # 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"] # The name of an image file (relative to this directory) to place at the top # of the sidebar. html_logo = "_static/yt_icon.png" # -- Options for HTMLHelp output --------------------------------------- # Output file base name for HTML help builder. htmlhelp_basename = "unytdoc" # -- 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, "unyt.tex", "unyt Documentation", "The yt Project", "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, "unyt", "unyt 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, "unyt", "unyt Documentation", author, "unyt", "One line description of project.", "Miscellaneous", ) ] autodoc_member_order = "bysource" def run_apidoc(_): try: from sphinx.ext.apidoc import main except ImportError: from sphinx.apidoc import main sys.path.append(os.path.join(os.path.dirname(__file__), "..")) cur_dir = os.path.abspath(os.path.dirname(__file__)) api_doc_dir = os.path.join(cur_dir, "modules") module = os.path.join(cur_dir, "..", "unyt") ignore = os.path.join(cur_dir, "..", "unyt", "tests") os.environ["SPHINX_APIDOC_OPTIONS"] = "members,undoc-members,show-inheritance" main(["-M", "-f", "-e", "-T", "-d 0", "-o", api_doc_dir, module, ignore]) def setup(app): app.connect("builder-inited", run_apidoc) unyt-3.0.4/docs/contributing.rst000066400000000000000000000000411476461141700167140ustar00rootroot00000000000000.. include:: ../CONTRIBUTING.rst unyt-3.0.4/docs/extensions/000077500000000000000000000000001476461141700156575ustar00rootroot00000000000000unyt-3.0.4/docs/extensions/show_all_units.py000066400000000000000000000145521476461141700212720ustar00rootroot00000000000000from docutils.parsers.rst import Directive try: maketrans = "".maketrans except AttributeError: # python2 fallback from string import maketrans import numpy as np from sympy.core.mul import Mul from sympy.core.symbol import Symbol import unyt import unyt.dimensions as dims from unyt import Unit from unyt._unit_lookup_table import name_alternatives, physical_constants from unyt.exceptions import UnitsNotReducible from unyt.unit_registry import default_unit_registry from unyt.unit_systems import _split_prefix all_dims = {} for d in dims.__dict__.keys(): if isinstance(getattr(dims, d), (Symbol, Mul)): if d == "dimensionless": continue all_dims[getattr(dims, d)] = d def setup(app): app.add_directive("show_all_units", ShowAllUnits) app.add_directive("show_all_constants", ShowAllConstants) setup.app = app setup.config = app.config setup.confdir = app.confdir retdict = {"version": "0.1"} return retdict class ShowAllUnits(Directive): required_arguments = 0 optional_arguments = 0 def run(self): lines = [] for name, alt_names in name_alternatives.items(): prefix, base = _split_prefix(name, default_unit_registry.lut) if prefix != "": continue lut_entry = default_unit_registry[name] u = Unit(name) try: dimensions = all_dims[u.dimensions] except KeyError: if u.is_dimensionless: dimensions = "dimensionless" else: dimensions = u.dimensions try: mks_value = (1 * u).in_mks() except UnitsNotReducible: mks_value = "N/A" try: cgs_value = (1 * u).in_cgs() except UnitsNotReducible: cgs_value = "N/A" def generate_table_value(value): if value == "N/A": return value approx_string = f"{value:.4e}" real_string = str(value) fv = value.value close_value = float(f"{fv:.4e}") if (close_value - fv) / fv < 1e-6 and len(str(fv)) > 8: return approx_string if fv < 1e-4 or fv > 1e4 or len(str(fv)) > 8: return approx_string return real_string latex_repr = "``" + u.latex_repr + "``" if latex_repr == "````": latex_repr = "" with np.printoptions(precision=4, suppress=False, floatmode="maxprec"): lines.append( ( name, str(dimensions), generate_table_value(mks_value), generate_table_value(cgs_value), latex_repr, str(lut_entry[4]), ", ".join([a for a in alt_names if a != name]), ) ) lines.insert( 0, [ "Unit Name", "Dimensions", "MKS value", "CGS Value", "LaTeX Representation", "SI Prefixable?", "Alternate Names", ], ) lines = as_rest_table(lines, full=False).split("\n") rst_file = self.state_machine.document.attributes["source"] self.state_machine.insert_input(lines, rst_file) return [] class ShowAllConstants(Directive): required_arguments = 0 optional_arguments = 0 def run(self): lines = [] for name, (_value, _unit, alternate_names) in physical_constants.items(): val = getattr(unyt.physical_constants, name) if val > 1e4 or val < 1e-4: default_value = f"{val:.4e}" else: default_value = str(val) lines.append((name, default_value, ", ".join(alternate_names))) lines.insert(0, ["Constant Name", "Value", "Alternate Names"]) lines = as_rest_table(lines, full=False).split("\n") rst_file = self.state_machine.document.attributes["source"] self.state_machine.insert_input(lines, rst_file) return [] def as_rest_table(data, full=False): """ Originally from ActiveState recipes, copy/pasted from GitHub where it is listed with an MIT license. https://github.com/ActiveState/code/tree/master/recipes/Python/579054_Generate_Sphinx_table """ data = data if data else [["No Data"]] table = [] # max size of each column sizes = list(map(max, zip(*[[len(str(elt)) for elt in member] for member in data]))) num_elts = len(sizes) if full: start_of_line = "| " vertical_separator = " | " end_of_line = " |" line_marker = "-" else: start_of_line = "" vertical_separator = " " end_of_line = "" line_marker = "=" meta_template = vertical_separator.join( [f"{{{{{i}:{{{i}}}}}}}" for i in range(num_elts)] ) template = f"{start_of_line}{meta_template.format(*sizes)}{end_of_line}" # determine top/bottom borders if full: to_separator = maketrans("| ", "+-") else: to_separator = maketrans("|", "+") start_of_line = start_of_line.translate(to_separator) vertical_separator = vertical_separator.translate(to_separator) end_of_line = end_of_line.translate(to_separator) separator = f"{start_of_line}{vertical_separator.join([x * line_marker for x in sizes])}{end_of_line}" # determine header separator th_separator_tr = maketrans("-", "=") start_of_line = start_of_line.translate(th_separator_tr) line_marker = line_marker.translate(th_separator_tr) vertical_separator = vertical_separator.translate(th_separator_tr) end_of_line = end_of_line.translate(th_separator_tr) th_separator = f"{start_of_line}{vertical_separator.join([x * line_marker for x in sizes])}{end_of_line}" # prepare result table.append(separator) # set table header titles = data[0] table.append(template.format(*titles)) table.append(th_separator) for d in data[1:-1]: table.append(template.format(*d)) if full: table.append(separator) table.append(template.format(*data[-1])) table.append(separator) return "\n".join(table) unyt-3.0.4/docs/history.rst000066400000000000000000000000341476461141700157100ustar00rootroot00000000000000.. include:: ../HISTORY.rst unyt-3.0.4/docs/index.rst000066400000000000000000000020761476461141700153260ustar00rootroot00000000000000unyt ==== This is the documentation for ``unyt`` (pronounced like "unit"). ``unyt`` is a Python library for working with data that has physical units. It was originally developed as part of `the yt Project `_ and was split out as an independent project so that other Python projects can easily make use of it. The unyt library defines the :class:`unyt.array.unyt_array` and :class:`unyt.array.unyt_quantity` classes for handling arrays and scalars with units, respectively. In addition, ``unyt`` provides a number of predefined units and physical constants that can be directly imported from from the ``unyt`` namespace: .. doctest:: >>> from unyt import G, Mearth, Rearth >>> v_esc = (2 * G * Mearth / Rearth) ** (1.0 / 2) >>> print(v_esc.to("km/s")) 11.254544657958482 km/s .. toctree:: :maxdepth: 3 :caption: Contents: installation usage unit_listing API Documentation contributing citation authors history Indices and tables ================== * :ref:`genindex` * :ref:`modindex` * :ref:`search` unyt-3.0.4/docs/installation.rst000066400000000000000000000043251476461141700167170ustar00rootroot00000000000000.. highlight:: shell ============ Installation ============ Stable release -------------- To install :mod:`unyt`, run this command in your terminal: .. code-block:: console $ python -m pip install unyt If you have a C compiler available, we also suggest installing `fastcache`_, which will improve the performance of `SymPy`_. If you don't have `pip`_ installed, this `Python installation guide`_ can guide you through the process. If you use `conda`_, :mod:`unyt` is available via `conda-forge`_: .. code-block:: console $ conda install -c conda-forge unyt It is not necessary to explicitly install ``fastcache`` if you use ``conda`` because it will be installed automatically as a dependency of ``SymPy``. .. _pip: https://pip.pypa.io .. _Python installation guide: http://docs.python-guide.org/en/latest/starting/installation/ .. _fastcache: https://github.com/pbrady/fastcache .. _SymPy: http://sympy.org/ .. _conda: https://conda.io/ .. _conda-forge: https://conda-forge.org/ From source ----------- The sources for :mod:`unyt` can be downloaded from the `Github repo`_. If you don't need an editable install, the following line is all you need to run .. code-block:: console $ python -m pip install git+https//github.com/yt-project/unyt.git If you would like to make an "editable" where you can directly edit the Python source files of the installed version of ``unyt``, then you can download the latest version from Github either with ``git`` .. code-block:: console $ git clone git://github.com/yt-project/unyt or download the `tarball`_: .. code-block:: console $ curl -OL https://github.com/yt-project/unyt/tarball/main Once you have a copy of the source, you can install it by navigating to the root of the installation and issuing the following command: .. code-block:: console $ python -m pip install -e . .. _Github repo: https://github.com/yt-project/unyt .. _tarball: https://github.com/yt-project/unyt/tarball/main Running the tests ----------------- You can check that :mod:`unyt` is working properly by running the unit tests on your installed copy: .. doctest:: >>> import unyt >>> unyt.test() # doctest: +SKIP Note that you'll need ``pytest`` installed for this function to run. unyt-3.0.4/docs/make.bat000066400000000000000000000013761476461141700150740ustar00rootroot00000000000000@ECHO OFF pushd %~dp0 REM Command file for Sphinx documentation if "%SPHINXBUILD%" == "" ( set SPHINXBUILD=python -msphinx ) set SOURCEDIR=. set BUILDDIR=_build set SPHINXPROJ=unyt if "%1" == "" goto help %SPHINXBUILD% >NUL 2>NUL if errorlevel 9009 ( echo. echo.The Sphinx module was not found. Make sure you have Sphinx installed, echo.then set the SPHINXBUILD environment variable to point to the full echo.path of the 'sphinx-build' executable. Alternatively you may add the echo.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 unyt-3.0.4/docs/requirements.txt000066400000000000000000000000361476461141700167430ustar00rootroot00000000000000numpy sympy matplotlib sphinx unyt-3.0.4/docs/unit_listing.rst000066400000000000000000000002671476461141700167270ustar00rootroot00000000000000.. _unit-listing: Listing of Units ================ .. show_all_units:: .. _constant-listing: Listing of Physical Constants ============================= .. show_all_constants:: unyt-3.0.4/docs/usage.rst000066400000000000000000001674311476461141700153320ustar00rootroot00000000000000======================== Working with :mod:`unyt` ======================== Basic Usage +++++++++++ To use unyt in a project:: >>> import unyt The top-level :mod:`unyt` namespace defines both a number of useful functions as well as a number of units and physical constants populated from the :mod:`unyt.unit_symbols` and :mod:`unyt.physical_constants` namespaces you can use to attach units to NumPy arrays and other common Python data container types like ``list`` and ``tuple``. For an exhaustive listing of units and physical constants defined in :mod:`unyt`, see :ref:`unit-listing`. .. warning:: Both unit symbols and physical constants are defined in the top-level :mod:`unyt` namespace. Some names occur as both unit symbols and physical constants, e.g. ``"me"``, ``"mp"``, ``"E_pl"``, etc. In such cases, the top-level namespace defaults to exporting the physical constant with the duplicate name. This means that there may be a (very rare) case where an :class:`Unit ` object that at one point can be imported from the top-level namespace may at a later point be only imported as an :class:`unyt_quantity ` object with the same name. As described below, there are other ways to import the unit symbols and/or physical constants besides the top-level namespace. An Example from High School Physics ----------------------------------- To see how you might use :mod:`unyt` to solve a problem where units might be a headache, let's estimate the orbital periods of Jupiter's Galilean moons, assuming they have circular orbits and their masses are negligible compared to Jupiter. Under these assumptions, the orbital period is .. math:: T = 2\pi\left( \frac{r^3}{GM}\right)^{1/2}. For this exercise let's calculate the orbital period in days. While it's possible to do this using plain old floating point numbers (you probably had to do something similar on a calculator in a high school physics class, looking up and plugging in conversion factors by hand), it's much easier to do this sort of thing symbolically and let :mod:`unyt` handle the unit conversions. To do this we'll need to know the mass of Jupiter (fortunately that is built into :mod:`unyt`) and the semi-major axis of the orbits of Jupiter's moons, which we can look up from `Wikipedia `_ and enter by hand:: >>> from unyt import Mjup, G, AU >>> from math import pi ... >>> moons = ['Io', 'Europa', 'Ganymede', 'Callisto'] >>> semimajor_axis = [.002819, .0044856, .00715526, .01258513]*AU ... >>> period = 2*pi*(semimajor_axis**3/(G*Mjup))**0.5 >>> period = period.to('d') ... >>> for moon, period in zip(moons, period): ... print('{}: {:04.2f}'.format(moon, period)) Io: 1.77 day Europa: 3.55 day Ganymede: 7.15 day Callisto: 16.69 day Let's break up this example into a few components so you can see what's going on. First, we import the unit symbols we need from the :mod:`unyt` namespace:: >>> from unyt import Mjup, G, AU The :mod:`unyt` namespace has a large number of units and physical constants you can import to apply units to data in your own code. You can see how that works in the example:: >>> semimajor_axis = [.002819, .0044856, .00715526, .01258513]*AU >>> semimajor_axis unyt_array([0.002819 , 0.0044856 , 0.00715526, 0.01258513], 'AU') By multiplying by ``AU``, we converted the Python list into a :class:`unyt.unyt_array ` instance. This is a class that's built into :mod:`unyt`, has units attached to it, and knows how to convert itself into different dimensionally equivalent units:: >>> semimajor_axis.value array([0.002819 , 0.0044856 , 0.00715526, 0.01258513]) >>> semimajor_axis.units AU >>> print(semimajor_axis.to('km')) [ 421716.39764641 671036.20903964 1070411.66066813 1882708.6511216 ] km Next, we calculated the orbital period by translating the orbital period formula to Python and then converting the answer to the units we want in the end, days:: >>> period = 2*pi*(semimajor_axis**3/(G*Mjup))**0.5 >>> period unyt_array([ 152864.59689789, 306828.08975058, 618162.17963649, 1441952.18891597], 's') >>> period.to('d') unyt_array([ 1.76926617, 3.55125104, 7.15465486, 16.68926145], 'day') Note that we haven't added any conversion factors between different units, that's all handled internally by :mod:`unyt`. Also note how the :meth:`unyt_array.to ` method was able to automatically handle the conversion from seconds to days and how the shorthand ``"d"`` was automatically interpreted as ``"day"``. Arithmetic and units -------------------- The real power of working with :mod:`unyt` is its ability to add, subtract, multiply, and divide quantities and arrays with units in mathematical formulas while automatically handling unit conversions and detecting when you have made a mistake in your units in a mathematical formula. To see what I mean by that, let's take a look at the following examples:: >>> from unyt import cm, m, ft, yard >>> print(3.*cm + 4.*m - 5.*ft + 6.*yard) 799.24 cm Despite the fact that the four unit symbols used in the above example correspond to four different units, :mod:`unyt` is able to automatically convert the value of all three units into a common unit and return the result in those units. Note that for expressions where the return units are ambiguous, :mod:`unyt` always returns data in the units of the leftmost object in an expression:: >>> print(4*m + 3*cm - 5*ft + 6*yard) # doctest: +FLOAT_CMP 7.9924 m One can also form more complex units out of atomic unit symbols. For example, here is how we'd create an array with units of meters per second and print out the values in the array in miles per hour:: >>> from unyt import m, s >>> velocities = [20., 22., 25.]*m/s >>> print(velocities.to('mile/hr')) [44.73872584 49.21259843 55.9234073 ] mile/hr Similarly one can multiply two units together to create new compound units:: >>> from unyt import N, m >>> energy = 3*N * 4*m >>> print(energy) 12 N*m >>> print(energy.to('erg')) 120000000.0 erg In general, one can multiply or divide by an arbitrary rational power of a unit symbol. Most commonly this shows up in mathematical formulas in terms of square roots. For example, let's calculate the gravitational free-fall time for a person to fall from the surface of the Earth through to a hole dug all the way to the center of the Earth. It turns out that this time `is given by `_: .. math:: t_{\rm ff} = \sqrt{\frac{3\pi}{32 G \rho}} where :math:`\rho` is the average density of the Earth. >>> from unyt import G, Mearth, Rearth >>> from math import pi >>> import numpy as np ... >>> rho = Mearth / (4./3 * pi* Rearth**3) >>> print(rho.to('g/cm**3')) 5.581225129861083 g/cm**3 >>> tff = np.sqrt(3*pi/(32*G*rho)) >>> print(tff.to('min')) 14.820022043294829 min If you make a mistake by adding two things that have different dimensions, :mod:`unyt` will raise an error to let you know that you have a bug in your code: >>> from unyt import kg, m >>> 3*kg + 5*m # doctest: +ELLIPSIS +NORMALIZE_WHITESPACE +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): ... unyt.exceptions.UnitOperationError: The operator for unyt_arrays with units "kg" (dimensions "(mass)") and "m" (dimensions "(length)") is not well defined. while this example is trivial when one writes more complicated formulae it can be easy to accidentally write expressions that are not dimensionally sound. Sometimes this can be annoying to deal with, particularly if one is mixing data that has units attached with data from some outside source with no units. To quickly patch over this lack of unit metadata (which could be applied by explicitly attaching units at I/O time), one can use the ``units`` attribute of the :class:`unyt.unyt_array ` class to quickly apply units to a scalar, list, or array: >>> from unyt import cm, s >>> velocities = [10, 20, 30] * cm/s >>> velocities + 12 # doctest: +ELLIPSIS +NORMALIZE_WHITESPACE +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): ... unyt.exceptions.UnitOperationError: The operator for unyt_arrays with units "cm/s" (dimensions "(length)/(time)") and "dimensionless" (dimensions "1") is not well defined. >>> velocities + 12*velocities.units unyt_array([22, 32, 42], 'cm/s') Powers, Logarithms, Exponentials, and Trigonometric Functions ------------------------------------------------------------- The :mod:`unyt` library represents powers using standard Python syntax. This means you must use ``**`` and not ``^``, even when writing a unit as a string: >>> from unyt import kg, m >>> print((10.*kg/m**3).to('g/cm**3')) 0.01 g/cm**3 Formally it does not make sense to exponentiate, take the logarithm of, or apply a transcendental function to a quantity with units. However, the :mod:`unyt` library makes the practical affordance to allow this, simply ignoring the units present and returning a result without units. This makes it easy to work with data that has units both in linear space and in log space: >>> from unyt import g, cm >>> import numpy as np >>> print(np.log10(1e-23*g/cm**3)) -23.0 The one exception to this rule is for trigonometric functions applied to data with angular units: >>> from unyt import degree, radian >>> import numpy as np >>> np.sin(np.pi/4*radian) array(0.70710678) >>> np.sin(45.*degree) array(0.70710678) Logarithmic Quantities and Units ******************************** The logarithmic quantities level-of-power and level-of-field and the units neper and bel are supported. In the next example, we represent the power measurements, ``p``, as a logarithmic quantity at reference level, ``p_ref``, in the units decibel. >>> import numpy as np >>> from unyt import dB, mW >>> dB.dimensions (logarithmic) >>> p = [1, 100]*mW >>> p_ref = 1*mW >>> level_of_power = 10*np.log10(p/p_ref)*dB >>> level_of_power unyt_array([ 0., 20.], 'dB') You can convert the logarithmic quantity back to physical units through exponentiation, just remember to remove the units using the :meth:`unyt_array.v ` property. >>> 10**(level_of_power.v/10)*p_ref unyt_array([ 1., 100.], 'mW') Printing Units -------------- The print formatting of :class:`unyt_array ` can be controlled identically to NumPy arrays, using ``numpy.setprintoptions``: >>> import numpy as np >>> import unyt as u ... >>> with np.printoptions(precision=4): ... print([1.123456789]*u.km) [1.1235] km Print a :math:`\rm{\LaTeX}` representation of a set of units using the :meth:`unyt.unit_object.Unit.latex_representation` function or :attr:`unyt.unit_object.Unit.latex_repr` attribute: >>> from unyt import g, cm >>> (g/cm**3).units.latex_representation() '\\frac{\\rm{g}}{\\rm{cm}^{3}}' >>> (g/cm**3).units.latex_repr '\\frac{\\rm{g}}{\\rm{cm}^{3}}' Simplifying Units ----------------- Unit expressions can often be simplified to cancel pairs of factors with compatible dimensions. For example, we can form a unit with dimensions of length by dividing a unit with dimensions of length squared by another unit with dimensions of length:: >>> from unyt import m, cm >>> m**2/cm m**2/cm The :class:`Unit ` class has a :meth:`simplify() ` method that we can call to create a new unit object to that includes the dimensionless ratio ``m/cm`` as a constant coefficient:: >>> (m**2/cm).simplify() 100*m This will also work for units that are the reciprocals of each other, for example: >>> from unyt import s, Hz >>> (s*Hz).simplify() (dimensionless) Products and quotients of unit objects will not be simplified unless ``simplify()`` is called explicitly. However, products and quotients of arrays and quantities will be simplified to make interactive work more intuitive:: >>> from unyt import erg, minute, hour >>> power = [20, 40, 80] * erg / minute >>> elapsed_time = 3*hour >>> print(power*elapsed_time) [ 3600. 7200. 14400.] erg .. _checking_units: Checking Units -------------- If you write a function that accepts data with units as an argument or returns data with units, you can ensure the dimensional correctness of the inputs or outputs using the :meth:`@accepts ` and :meth:`@returns ` decorators:: >>> from unyt.dimensions import length, time >>> from unyt import accepts, returns >>> import unyt as u >>> @returns(length) ... @accepts(a=time, v=length/time) ... def foo(a, v): ... return a * v ... >>> res = foo(a=2*u.s, v=3*u.m/u.s) >>> print(res) 6 m :meth:`@accepts ` can specify the dimensions of any subset of inputs and :meth:`@returns ` must always describe all outputs. >>> @returns(length, length/time**2) ... @accepts(v=length/time) ... def bar(a, v): ... return a * v, v / a ... >>> res = bar(a=2*u.s, v=3*u.m/u.s) >>> print(*res) 6 m 1.5 m/s**2 .. note:: Using these decorators may incur some performance overhead, especially for small arrays. Temperature Units ----------------- The temperature unit degree Celsius has the symbol ``°C``, but since the degree character is an invalid Python identifier, :mod:`unyt` uses the symbol ``degC``. Printing a degree Celsius quantity will show the correct symbol. >>> from unyt import degC >>> Ta = 23*degC >>> print(Ta) 23 °C The ``degC`` symbol has alternative names ``degree_Celsius``, ``Celsius`` and ``°C``. >>> from unyt import degree_Celsius, unyt_array >>> Ta = 23*degree_Celsius >>> print(Ta) 23 °C >>> Ta = unyt_array([-40, 23, 70], '°C') >>> print(Ta) [-40 23 70] °C These comments also apply to degree Fahrenheit. Performing arithmetic with temperature quantities can be ambiguous. To clarify intent, :mod:`unyt` has the convenience units ``delta_degC`` and ``delta_degF``. >>> from unyt import degC, delta_degC, V >>> t1 = 23*degC >>> t2 = 1*delta_degC >>> print(t1 + t2) 24.0 °C >>> print(t2 - t1) -22.0 °C >>> tempco = 10.0*V/delta_degC >>> print(tempco*2*delta_degC) 20.0 V Unit Conversions and Unit Systems +++++++++++++++++++++++++++++++++ Converting Data to Arbitrary Units ---------------------------------- If you have some data that you want to convert to a different set of units and you know which units you would like to convert it to, you can make use of the :meth:`unyt_array.to ` function: >>> from unyt import mile >>> (1.0*mile).to('ft') unyt_quantity(5280., 'ft') If you try to convert to a unit with different dimensions, :mod:`unyt` will raise an error: >>> from unyt import mile >>> (1.0*mile).to('lb') # doctest: +ELLIPSIS +NORMALIZE_WHITESPACE +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): ... unyt.exceptions.UnitConversionError: Cannot convert between 'mile' (dim '(length)') and 'lb' (dim '(mass)'). While we recommend using :meth:`unyt_array.to ` in most cases to convert arrays or quantities to different units, if you would like to explicitly emphasize that this operation has to do with units, we also provide the more verbose name :meth:`unyt_array.in_units ` which behaves identically to :meth:`unyt_array.to `. Converting Units In-Place ------------------------- The :meth:`unyt_array.to ` method makes a copy of the array data. For most cases this is fine, but when dealing with big arrays, or when performance is a concern, it sometimes is preferable to convert the data in an array in-place, without copying the data to a new array. This can be accomplished with the :meth:`unyt_array.convert_to_units ` function: >>> from unyt import mile >>> data = [1., 2., 3.]*mile >>> data unyt_array([1., 2., 3.], 'mile') >>> data.convert_to_units('km') >>> data unyt_array([1.609344, 3.218688, 4.828032], 'km') Converting to MKS and CGS Base Units ------------------------------------ If you don't necessarily know the units you want to convert data to ahead of time, it's often convenient to specify a unit system to convert to. The :class:`unyt_array ` has built-in conversion methods for the two most popular unit systems, MKS (meter kilogram second) and CGS (centimeter gram second). For CGS these are :meth:`unyt_array.in_cgs ` and :meth:`unyt_array.convert_to_cgs `. These functions create a new copy of an array in CGS units and convert an array in-place to CGS respectively. For MKS, there are the :meth:`unyt_array.in_mks ` and :meth:`unyt_array.convert_to_mks ` methods, which play analogous roles. See below for details on CGS and MKS electromagnetic units. .. _metal_conversions: Metallicity Unit Conversions ---------------------------- In the astrophysical context, "metals" are all of the elements that have atomic numbers greater than 2, i.e. everything heavier than helium. The "solar metallicity" is the mass fraction of metals in the solar atmosphere, and is used in a variety of contexts. Often, the metallicity of other astrophysical objects is expressed in terms of the solar metallicity, given by the unit :math:`Z_\odot`. The default mass fraction corresponding to :math:`Z_\odot` in :mod:`unyt` is 0.01295, corresponding to the value used in the `Cloudy Code `_. Metal mass fractions (by definition dimensionless) can be converted to :math:`Z_\odot` (and vice versa): >>> from unyt import dimensionless >>> M_Z = 0.0259*dimensionless >>> M_Z unyt_quantity(0.0259, 'dimensionless') >>> M_Z.convert_to_units("Z_sun") >>> M_Z unyt_quantity(2., 'Zsun') However, the value of this mass fraction conversion must be measured, and various estimates of it disagree somewhat. Different sub-disciplines of astronomy often use different estimates in the literature. :mod:`unyt` provides other metallicity unit conversions to several typical values in use. The available units (and their mass fraction conversion factors) are: * ``"Zsun_angr"``: 0.01937, from `Anders E. & Grevesse N. (1989, Geochimica et Cosmochimica Acta 53, 197) `_ * ``"Zsun_aspl"``: 0.01337, from `Asplund M., Grevesse N., Sauval A.J. & Scott P. (2009, ARAA, 47, 481) `_ * ``"Zsun_feld"``: 0.01909, from `Feldman U. (1992, Physica Scripta, 46, 202) `_ * ``"Zsun_lodd"``: 0.01321, from `Lodders, K (2003, ApJ 591, 1220) `_ These can be used in the same way as above: >>> from unyt import dimensionless >>> M_Z = 0.0259*dimensionless >>> M_Z unyt_quantity(0.0259, 'dimensionless') >>> M_Z.convert_to_units("Zsun_angr") >>> M_Z unyt_quantity(1.33711926, 'Zsun_angr') Other Unit Systems ------------------ The :mod:`unyt` library currently has built-in support for a number of unit systems, as detailed in the table below. Note that all unit systems currently use "radian" as the base angle unit. If a unit system in the table below has "Other Units" specified, this is a mapping from dimension to a unit name. These units override the unit system's default unit for that dimension. If no unit is explicitly specified of a dimension then the base unit for that dimension is calculated at runtime by combining the base units for the unit system into the appropriate dimension. +--------------+--------------------+--------------------------+ | Unit system | Base Units | Other Units | +==============+====================+==========================+ | cgs | cm, g, s | * Energy: erg | | | | * Specific Energy: erg/g | | | | * Pressure: dyne/cm**2 | | | | * Force: dyne | | | | * Power: erg/s | | | | * Magnetic Field: G | | | | * Charge: esu | | | | * Current: statA | +--------------+--------------------+--------------------------+ | mks | m, kg, s | * Energy: J | | | | * Specific Energy: J/kg | | | | * Pressure: Pa | | | | * Force: N | | | | * Power: W | | | | * Magnetic Field: T | | | | * Charge: C | +--------------+--------------------+--------------------------+ | imperial | ft, lb, s | * Energy: ft*lbf | | | | * Temperature: R | | | | * Pressure: lbf/ft**2 | | | | * Force: lbf | | | | * Power: hp | +--------------+--------------------+--------------------------+ | galactic | kpc, Msun, kyr | * Energy: kev | | | | * Magnetic Field: uG | +--------------+--------------------+--------------------------+ | solar | AU, Mearth, yr | | +--------------+--------------------+--------------------------+ Note that in MKS units the current unit, Ampere, is a base unit in the unit system. In CGS units the electromagnetic units like Gauss and statA are decomposable in terms of the base mass, length, and time units in the unit system. For this reason quantities defined in E&M units in CGS units are not readily convertible to MKS units and vice versa since the units are not dimensionally equivalent. The :mod:`unyt` library does have limited support for converting electromagnetic units between MKS and CGS, however only simple conversions of data with a single specific unit are supported and no conversions are allowed for complex combinations of units. For example converting between Gauss and Tesla is supported: >>> from unyt import T >>> (1.0*T).to('G') unyt_quantity(10000., 'G') But converting a more complicated compound unit will raise an error: >>> from unyt import C, T, V >>> (1.0*C*T*V).in_cgs() # doctest: +ELLIPSIS +NORMALIZE_WHITESPACE +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): ... unyt.exceptions.UnitsNotReducible: The unit "C*T*V" (dimensions "(length)**2*(mass)**2/((current_mks)*(time)**4)") cannot be reduced to an expression within the cgs system of units. If you need to work with complex expressions involving electromagnetic units, we suggest sticking to either CGS or SI units for the full calculation. There is no general way to convert an arbitrary quantity between CGS and SI units if the quantity involves electromagnetic units. Instead, it is necessary to do the conversion on the equations under consideration, and then recompute the necessary quantity in the transformed set of equations. This requires understanding the context for a calculation, which unfortunately is beyond the scope of a library like :mod:`unyt`. You can convert data to a unit system :mod:`unyt` knows about using the :meth:`unyt_array.in_base ` and :meth:`unyt_array.convert_to_base ` methods: >>> from unyt import g, cm, horsepower >>> (1e-9*g/cm**2).in_base('galactic') unyt_quantity(4.78843804, 'Msun/kpc**2') >>> data = [100., 500., 700.]*horsepower >>> data unyt_array([100., 500., 700.], 'hp') >>> data.convert_to_base('mks') >>> data unyt_array([ 74569.98715823, 372849.93579114, 521989.91010759], 'W') Defining and Using New Unit Systems *********************************** To define a new custom unit system, one need only create a new instance of the :class:`unyt.UnitSystem ` class. The class initializer accepts a set of base units to define the unit system. If you would like to additionally customize any derived units in the unit system, you can do this using item setting. As an example, let's define an atomic unit system based on typical scales for atoms and molecules: >>> from unyt import UnitSystem >>> atomic_unit_system = UnitSystem('atomic', 'nm', 'mp', 'fs', 'nK', 'rad') >>> atomic_unit_system['energy'] = 'eV' >>> atomic_unit_system atomic Unit System Base Units: length: nm mass: mp time: fs temperature: nK angle: rad current_mks: A luminous_intensity: cd logarithmic: Np Other Units: energy: eV >>> print(atomic_unit_system) atomic >>> atomic_unit_system['number_density'] nm**(-3) >>> atomic_unit_system['angular_momentum'] mp*nm**2/fs It is also legal to define a unit system using :class:`unyt.Unit ` instances: >>> from unyt.unit_symbols import Msun, second, megaparsec >>> UnitSystem('cosmological', megaparsec, Msun, second) cosmological Unit System Base Units: length: Mpc mass: Msun time: s temperature: K angle: rad current_mks: A luminous_intensity: cd logarithmic: Np Other Units: Or with a quantity: >>> UnitSystem('quasmological', 3*megaparsec, .8*Msun, 42*second) quasmological Unit System Base Units: length: 3*Mpc mass: 0.8*Msun time: 42*s temperature: K angle: rad current_mks: A luminous_intensity: cd logarithmic: Np Other Units: Once you have defined a new unit system that will register the new system with a global registry of unit systems known to the :mod:`unyt` library. That means you will immediately be able to use it just like the built-in unit systems: >>> from unyt import W >>> (1.0*W).in_base('atomic') unyt_quantity(0.59746607, 'mp*nm**2/fs**3') If you would like your unit system to include an MKS current unit (e.g. something that is directly convertible to the MKS Ampere unit), then specify a ``current_mks_unit`` in the :class:`UnitSystem ` initializer. Equivalencies +++++++++++++ An equivalency is a way to define a mapping to convert from one unit to another even if the two units are not dimensionally equivalent. This usually involves some sort of shorthand or heuristic understanding of the problem under consideration. Only use one of these equivalencies if it makes sense to use it for the problem you are working on. The :mod:`unyt` library implements the following equivalencies: * ``"thermal"``: conversions between temperature and energy (:math:`E = k_BT`) * ``"spectral"``: conversions between wavelength, spatial frequency, frequency, and energy for photons (:math:`E = h\nu = hc/\lambda`, :math:`c = \lambda\nu`) * ``"mass_energy"``: conversions between mass and energy (:math:`E = mc^2`) * ``"lorentz"``: conversions between velocity and Lorentz factor (:math:`\gamma = 1/\sqrt{1-(v/c)^2}`) * ``"schwarzschild"``: conversions between mass and Schwarzschild radius (:math:`R_S = 2GM/c^2`) * ``"compton"``: conversions between mass and Compton wavelength (:math:`\lambda = h/mc`) You can convert data to a specific set of units via an equivalency appropriate for the units of the data. To see the equivalencies that are available for an array, use the :meth:`unit_array.list_equivalencies ` method: >>> from unyt import gram, km >>> gram.list_equivalencies() mass_energy: mass <-> energy schwarzschild: mass <-> length compton: mass <-> length >>> km.list_equivalencies() spectral: length <-> spatial_frequency <-> frequency <-> energy schwarzschild: mass <-> length compton: mass <-> length All of the unit conversion methods described above have an ``equivalence`` keyword argument that allows one to optionally specify an equivalence to use for the unit conversion operation. For example, let's use the ``schwarzschild`` equivalence to calculate the mass of a black hole with a radius of one AU: >>> from unyt import AU >>> (1.0*AU).to('Msun', equivalence='schwarzschild') unyt_quantity(50656851.7815179, 'Msun') Both the methods that convert data in-place and the ones that return a copy support optionally specifying equivalence. In addition to the methods described above, :mod:`unyt` also supplies two more conversion methods that *require* an equivalence to be specified: :meth:`unyt_array.to_equivalent ` and :meth:`unyt_array.convert_to_equivalent `. These are identical to their counterparts described above, except that equivalence is a required argument to the function rather than an optional keyword argument. Use these functions when you want to emphasize that an equivalence is being used. If the equivalence has optional keyword arguments, these can be passed to the unit conversion function. For example, here's an example where we specify a custom mean molecular weight (``mu``) for the ``number_density`` equivalence: >>> from unyt import g, cm >>> rho = 1e-23 * g/cm**3 >>> rho.to('cm**-3', equivalence='number_density', mu=1.4) unyt_quantity(4.26761476, 'cm**(-3)') For full API documentation and an autogenerated listing of the built-in equivalencies in :mod:`unyt` as well as a short usage example for each, see the :mod:`unyt.equivalencies` API listing. Dealing with code that doesn't use :mod:`unyt` ++++++++++++++++++++++++++++++++++++++++++++++ Optimally, a function will work the same irrespective of whether the data passed in has units attached or not: >>> from unyt import cm >>> def square(x): ... return x**2 >>> print(square(3.)) 9.0 >>> print(square(3.*cm)) 9.0 cm**2 However in the real world that is not always the case. In this section we describe strategies for dealing with that situation. Stripping units off of data --------------------------- The :mod:`unyt` library provides a number of ways to convert :class:`unyt_quantity ` instances into floats and :class:`unyt_array ` instances into NumPy arrays. These methods either return a copy of the data as a NumPy array or return a view onto the underlying array data owned by a :class:`unyt_array ` instance. To obtain a new array containing a copy of the original data, use either the :meth:`unyt_array.to_value ` function or the :attr:`unyt_array.value ` or :attr:`unyt_array.v ` properties. All of these are equivalent to passing a :class:`unyt_array ` to the ``numpy.array()`` function: >>> from unyt import g >>> import numpy as np >>> data = [1., 2., 3.]*g >>> data unyt_array([1., 2., 3.], 'g') >>> np.array(data) array([1., 2., 3.]) >>> data.to_value('kg') array([0.001, 0.002, 0.003]) >>> data.value array([1., 2., 3.]) >>> data.v array([1., 2., 3.]) Similarly, to obtain a ndarray containing a view of the data in the original array, use either the :attr:`unyt_array.ndview ` property (or :attr:`unyt_array.d ` for shorts): >>> data.view(np.ndarray) array([1., 2., 3.]) >>> data.ndview array([1., 2., 3.]) >>> data.d array([1., 2., 3.]) Applying units to data ---------------------- .. note:: A NumPy array that shares memory with another NumPy array points to the array that owns the data with the ``base`` attribute. If ``arr1.base is arr2`` is ``True`` then ``arr1`` is a view onto ``arr2`` and ``arr2.base`` will be ``None``. When a :class:`unyt_array ` instance is created from a NumPy array and a :class:`Unit `, data from the NumPy array will be copied: >>> from unyt import g >>> data = np.random.random((100, 100)) >>> data_with_units = data*g >>> data_with_units.base is data False If you would like to create a view rather than a copy, you can apply units like this: >>> from unyt import unyt_array >>> data_with_units = unyt_array(data, g) >>> data_with_units.base is data True Any set of units can be used for either of these operations. For example, if you already have an existing array, you could do this to create a new array with the same units: >>> more_data = [4, 5, 6]*data_with_units.units >>> more_data unyt_array([4, 5, 6], 'g') Working with code that uses ``astropy.units`` --------------------------------------------- The :mod:`unyt` library can convert data contained inside of an Astropy ``Quantity`` instance. It can also produce a ``Quantity`` from an existing :class:`unyt_array ` instance. To convert data from ``astropy.units`` to :mod:`unyt` use the :func:`unyt_array.from_astropy ` function: >>> from astropy.units import km >>> from unyt import unyt_quantity >>> unyt_quantity.from_astropy(km) unyt_quantity(1., 'km') >>> a = [1, 2, 3]*km >>> a >>> unyt_array.from_astropy(a) unyt_array([1., 2., 3.], 'km') To convert data *to* ``astropy.units`` use the :meth:`unyt_array.to_astropy ` method: >>> from unyt import g, cm >>> data = [3, 4, 5]*g/cm**3 >>> data.to_astropy() >>> (4*cm).to_astropy() Working with code that uses ``Pint`` ------------------------------------ The :mod:`unyt` library can also convert data contained in ``Pint`` ``Quantity`` instances. To convert data from ``Pint`` to :mod:`unyt`, use the :func:`unyt_array.from_pint ` function: >>> from pint import UnitRegistry >>> import numpy as np >>> ureg = UnitRegistry() >>> a = np.arange(4) >>> b = ureg.Quantity(a, "erg/cm**3") >>> b >>> c = unyt_array.from_pint(b) >>> c unyt_array([0, 1, 2, 3], 'erg/cm**3') And to convert data contained in a :class:`unyt_array ` instance, use the :meth:`unyt_array.to_pint ` method: >>> from unyt import cm, s >>> a = 4*cm**2/s >>> print(a) 4 cm**2/s >>> a.to_pint() >>> b = [1, 2, 3]*cm >>> b.to_pint() Reading quantities from text ---------------------------- Quantities can also be parsed from strings with the :func:`unyt_quantity.from_string ` function: >>> from unyt import unyt_quantity >>> unyt_quantity.from_string("1 cm") unyt_quantity(1, 'cm') >>> unyt_quantity.from_string("1e3 Msun") unyt_quantity(1000., 'Msun') >>> unyt_quantity.from_string("1e-3 g/cm**3") unyt_quantity(0.001, 'g/cm**3') This method is helpful to read data from text files, for instance configuration files. It is intended to be as flexible as possible on the string format, though it requires that the numerical value and the unit name be separated with some kind of whitespace. User-Defined Units ++++++++++++++++++ Often it is convenient to define new custom units. This can happen when you need to make use of a unit that the :mod:`unyt` library does not have a definition for already. It can also happen when dealing with data that uses a custom unit system or when writing software that needs to deal with such data in a flexible way, particularly when the units might change from dataset to dataset. This comes up often when modeling a physical system since it is often convenient to rescale data from a physical unit system to an internal "code" unit system in which the values of the variables under consideration are close to unity. This approach can help minimize floating point round-off error but is often done for convenience or to non-dimensionalize the problem under consideration. The :mod:`unyt` library provides two approaches for dealing with this problem. For more toy one-off use-cases, we suggest using :func:`unyt.define_unit ` which allows defining a new unit name in the global, default unit system that :mod:`unyt` ships with by default. This function makes it possible to easily define a new unit that is unknown to the :mod:`unyt` library: >>> import unyt as u >>> ninety_pounds = 90.0*u.lb >>> one_pound = 1.0*u.lb >>> u.define_unit("firkin", ninety_pounds) >>> print((3*u.firkin)/one_pound) 270.0 dimensionless This is primarily useful for one-off definitions of units that the :mod:`unyt` library does not already have predefined. For more complex uses cases that need more flexibility, it is possible to use a custom unit system by ensuring that the data you are working with makes use of a :class:`UnitRegistry ` customized for your use case, as described below. Dealing with data types +++++++++++++++++++++++ The :mod:`unyt` library supports creating :class:`unyt.unyt_array ` and :class:`unyt.unyt_quantity ` instances with arbitrary integer or floating point data types: >>> import numpy as np >>> from unyt import km ... >>> int_data = [1, 2, 3]*km >>> int_data unyt_array([1, 2, 3], 'km') >>> float32_data = np.array([1, 2, 3], dtype='float32')*km >>> float32_data unyt_array([1., 2., 3.], dtype=float32, units='km') The ``dtype`` of a ``unyt_array`` instance created by multiplying an iterable by a unit will be the same as passing the iterable to ``np.array()``. You can also manually specify the ``dtype`` by calling ``np.array()`` yourself or by using the ``unyt_array`` initializer directly: >>> np.array([1, 2, 3], dtype='float64')*km unyt_array([1., 2., 3.], 'km') Operations that convert an integer array to a new unit will convert the array to the floating point type with an equivalent size. For example, Calling ``in_units`` on a 32 bit integer array with units of kilometers will return a 32 bit floating point array. >>> data = np.array([1, 2, 3], dtype='int32')*km >>> data.in_units('mile') unyt_array([0.6213712, 1.2427424, 1.8641136], dtype=float32, units='mile') In-place operations will also mutate the dtype from float to integer in these cases, again in a way that will preserve the byte size of the data. >>> data.convert_to_units('mile') >>> data unyt_array([0.6213712, 1.2427424, 1.8641136], dtype=float32, units='mile') It is possible that arrays containing large integers (16777217 for 32 bit and 9007199254740993 for 64 bit) will lose precision when converting data to a different unit. In these cases a warning message will be printed. Integrating :mod:`unyt` Into a Python Library +++++++++++++++++++++++++++++++++++++++++++++ The :mod:`unyt` library began life as the unit system for the ``yt`` data analysis and visualization package, in the form of ``yt.units``. In this role, :mod:`unyt` was deeply integrated into a larger Python library. Due to these origins, it is straightforward to build applications that ensure unit consistency by making use of :mod:`unyt`. Below we discuss a few topics that most often come up when integrating :mod:`unyt` into a new or existing Python library. Unit registries --------------- It is also possible to define a custom database of units completely independent of the global default unit database exposed by the :mod:`unyt` namespace or to create namespaces in your own package that expose listings of units. In these cases it becomes important to understand how ``unyt`` stores unit metadata in an internal database, how to add custom entries to the database, how to modify them, and how to persist custom units. In practice, the unit metadata for a unit object is contained in an instance of the :class:`UnitRegistry ` class. Every :class:`Unit ` instance contains a reference to a :class:`UnitRegistry ` instance: >>> from unyt import g >>> g.registry # doctest: +ELLIPSIS All the unit objects in the :mod:`unyt` namespace make use of the default unit registry, importable as :data:`unyt.unit_registry.default_unit_registry`. This registry object contains all of the real-world physical units that the :mod:`unyt` library ships with out of the box. The unit registry itself contains a look-up table that maps from unit names to the metadata necessary to construct a unit. Note that the unit registry only contains metadata for "base" units, and not, for example, SI-prefixed units like centimeter of kilogram, it will instead only contain entries for meter and gram. Sometimes it is convenient to create a unit registry containing new units that are not available in the default unit registry. A common example would be adding a ``code_length`` unit that corresponds to the scaling to from physical lengths to an internal unit system. In practice, this value is arbitrary, but will be fixed for a given problem. Let's create a unit registry and a custom ``"code_length"`` unit to it, and then create a ``"code_length"`` unit and a quantity with units of ``"code_length"``. For the sake of example, let's set the value of ``"code_length"`` equal to 10 meters. >>> from unyt import UnitRegistry, Unit >>> from unyt.dimensions import length >>> reg = UnitRegistry() >>> reg.add("code_length", base_value=10.0, dimensions=length, ... tex_repr=r"\rm{Code Length}") >>> 'code_length' in reg True >>> u = Unit('code_length', registry=reg) >>> data = 3*u >>> print(data) 3 code_length As you can see, you can test whether a unit name is in a registry using the Python ``in`` operator. In an application that depends on ``unyt``, it is often convenient to define methods or functions to automatically attach the correct unit registry to unit objects associated with an object. For example, consider a ``Simulation`` class. Let's give this class two methods named ``array`` and ``quantity`` to create new :mod:`unyt_array ` and :mod:`unyt_quantity ` instances, respectively: >>> class Simulation: ... def __init__(self, registry): ... self.registry = registry ... ... def array(self, value, units): ... return unyt_array(value, units, registry=self.registry) ... ... def quantity(self, value, units): ... return unyt_quantity(value, units, registry=self.registry) ... >>> registry = UnitRegistry() >>> registry.add("code_length", base_value=3.2, dimensions=length) >>> s = Simulation(registry) >>> s.array([1, 2, 3], 'code_length') unyt_array([1, 2, 3], 'code_length') We can create an array with ``"code_length"`` here because ``s.registry``, the ``UnitRegistry`` instance associated with our Simulation instance has a ``"code_length"`` unit defined. As for arrays with different units, for operations between arrays created with different unit registries, the result of the operation will use the same unit registry as the leftmost unit. This can sometimes lead to surprising behaviors where data will seem to "forget" about custom units. In this situation it is important to make sure ahead of time that all data are created with units using the same unit registry. If for some reason that is not possible (for example, when comparing data from two different simulations with different internal units), then care must be taken when working with custom units. To avoid these sorts of ambiguities it is best to do work in physical units as much as possible. When writing tests, it is convenient to use :mod:`unyt.testing`. In particular, :func:`assert_allclose_units ` can be used to check for floating-point equality. >>> from unyt import assert_allclose_units, m >>> import numpy as np >>> actual = [1e-5, 1e-3, 1e-1] * m >>> desired = actual.to("cm") >>> assert_allclose_units(actual, desired) Custom Unit Systems ------------------- By default :mod:`unyt` uses the SI MKS unit system. However, libraries can create a unit registry using another unit system to expose that unit system to their users by creating a unit registry with a custom unit system. For example, to make CGS units the default unit for all operations, one might use a CGS ``UnitRegistry`` to instancitate the ``Simulation`` class like so:: >>> class Simulation: ... def __init__(self, registry): ... self.registry = registry ... ... def array(self, value, units): ... return unyt_array(value, units, registry=self.registry) ... ... def quantity(self, value, units): ... return unyt_quantity(value, units, registry=self.registry) ... >>> registry = UnitRegistry(unit_system='cgs') >>> registry.add("code_length", base_value=3.2, dimensions=length) >>> s_cgs = Simulation(registry) >>> data = s_cgs.array([1, 2, 3], 'code_length') >>> data unyt_array([1, 2, 3], 'code_length') >>> data.in_base() unyt_array([320., 640., 960.], 'cm') Note that the ``base_value`` parameter of :meth:`UnitRegistry.add ` must be specified in MKS units. All unit data are stored internally in :mod:`unyt` in MKS units. You can also use two helper functions provided by :mod:`unyt`, :func:`unyt.unit_systems.add_constants` and :func:`unyt.unit_systems.add_symbols`, to populate a namespace with a set of predefined unit symbols or physical constants. This namespace could correspond to the names importable from a module or the names of attributes of an object, or any other generic dictionary. One example of doing this would be to make a ``UnitContainer`` class that contains units that are compatible with the ``Simulation`` instance we named ``s_cgs`` in the example above:: >>> from unyt.unit_systems import add_symbols >>> class UnitContainer: ... def __init__(self, registry): ... add_symbols(vars(self), registry) >>> units = UnitContainer(s_cgs.registry) >>> units.kilometer km >>> units.code_length code_length >>> (10.0 * units.kilometer).in_base() unyt_quantity(1000000., 'cm') >>> (10.0 * units.kilometer).in_units('code_length') unyt_quantity(3125., 'code_length') Note how the result of the call to ``in_base()`` comes out in centimeters because of the the CGS unit system used by the :class:`UnitRegistry ` instance associated with the ``Simulation``. Writing Data with Units to Disk ------------------------------- The :mod:`unyt` library has support for serializing data stored in a :class:`unyt.unyt_array ` instance to HDF5 files, text files, and via the Python pickle protocol. We give brief examples below, but first describe how to handle saving units manually as string metadata. Dealing with units as strings ***************************** If all you want to do is save data to disk in a physical unit or you are working in a physical unit system, then you only need to save the unit name as a string and treat the array data you are trying to save as a regular NumPy array, as in this example: >>> import numpy as np >>> import os >>> from unyt import cm ... >>> data = [1, 2, 3]*cm >>> np.save('my_data_cm.npy', data) >>> new_data = np.load('my_data_cm.npy') >>> new_data array([1, 2, 3]) >>> new_data_with_units = new_data * cm >>> os.remove('my_data_cm.npy') Of course in this example using ``numpy.save`` we need to hard-code the units because the ``.npy`` format doesn't have a way to store metadata along with the array data. We could have stored metadata in a sidecar file, but this is much more natural with ``hdf5`` via ``h5py``: >>> import h5py >>> import os >>> from unyt import cm, unyt_array ... >>> data = [1, 2, 3]*cm ... >>> with h5py.File('my_data.h5', 'a') as f: ... d = f.create_dataset('my_data', data=data) ... f['my_data'].attrs['units'] = str(data.units) ... >>> with h5py.File('my_data.h5', 'r') as f: ... new_data = f['my_data'][:] ... unit_str = f['my_data'].attrs['units'] ... >>> new_data = unyt_array(new_data, unit_str) >>> new_data unyt_array([1, 2, 3], 'cm') >>> os.remove('my_data.h5') HDF5 Files ********** The :mod:`unyt` library provides a hook for writing data both to a new HDF5 file and an existing file and then subsequently reading that data back in to restore the array. This works via the :meth:`unyt_array.write_hdf5 ` and :meth:`unyt_array.from_hdf5 ` methods. The simplest way to use these functions is to write data to a file that does not exist yet: >>> from unyt import cm, unyt_array >>> import os >>> data = [1, 2, 3]*cm >>> data.write_hdf5('my_data.h5') ... >>> unyt_array.from_hdf5('my_data.h5') unyt_array([1, 2, 3], 'cm') >>> os.remove('my_data.h5') By default the data will be written to the root group of the HDF5 file in a dataset named ``'array_data'``. You can also specify that you would like the data to be saved in a particular group or dataset in the file: >>> data.write_hdf5('my_data.h5', dataset_name='my_special_data', ... group_name='my_special_group') >>> unyt_array.from_hdf5('my_data.h5', dataset_name='my_special_data', ... group_name='my_special_group') unyt_array([1, 2, 3], 'cm') >>> os.remove('my_data.h5') You can even write to files and groups that already exist: >>> with h5py.File('my_data.h5', 'w') as f: ... g = f.create_group('my_custom_group') ... >>> data.write_hdf5('my_data.h5', group_name='my_custom_group') ... >>> with h5py.File('my_data.h5') as f: ... print(f['my_custom_group/array_data'][:]) [1 2 3] >>> os.remove('my_data.h5') If the dataset that you would like to write to already exists, :mod:`unyt` will clobber that dataset. Note that with this method of saving data to HDF5 files, the :class:`unyt.UnitRegistry ` instance associated with the units of the data will be saved in the HDF5 file. This means that if you create custom units and save a unit to disk, you will be able to convert data to those custom units even if you are dealing with those units later after restoring the data from disk. Here is a short example illustrating this: >>> import os >>> from unyt import UnitRegistry >>> reg = UnitRegistry() >>> reg.add("code_length", base_value=10.0, dimensions=length, ... tex_repr=r"\rm{Code Length}") >>> u = Unit('cm', registry=reg) >>> data = [1., 2., 3.]*u >>> data.write_hdf5('my_code_data.h5') >>> read_data = data.from_hdf5('my_code_data.h5') >>> read_data unyt_array([1., 2., 3.], 'cm') >>> read_data.to('code_length') unyt_array([0.001, 0.002, 0.003], 'code_length') >>> os.remove('my_code_data.h5') Text Files ********** The :mod:`unyt` library also has wrappers around ``numpy.savetxt`` and ``numpy.loadtxt`` for saving data as an ASCII table. For example: >>> import unyt as u >>> import os >>> data = [[1, 2, 3]*u.cm, [4, 5, 6]*u.kg] >>> u.savetxt('my_data.txt', data) >>> with open('my_data.txt') as f: ... print("".join(f.readlines())) # doctest: +NORMALIZE_WHITESPACE # Units # cm kg 1.000000000000000000e+00 4.000000000000000000e+00 2.000000000000000000e+00 5.000000000000000000e+00 3.000000000000000000e+00 6.000000000000000000e+00 >>> os.remove('my_data.txt') Pickles ******* .. note:: Pickle files are great for serializing data to disk or over a network for internal usage by a package. They are ill-suited for long-term data storage or for communicating data between different Python installations. If you want to use pickle files for data storage, consider using a format designed for long-term data storage, like HDF5. Both :class:`unyt.unyt_array ` and :class:`unyt.Unit ` instances can be saved using the pickle protocol: >>> from unyt import kg >>> import pickle >>> import numpy as np ... >>> assert kg == pickle.loads(pickle.dumps(kg)) >>> data = [1, 2, 3]*kg >>> reloaded_data = pickle.loads(pickle.dumps(data)) >>> assert np.array_equal(data.value, reloaded_data.value) >>> assert data.units == reloaded_data.units As for HDF5 data, the unit registry associated with the unit object is saved to the pickle. If you have custom units defined, the reloaded data will know about your custom unit and be able to convert data to and from the custom unit. Handling errors from :mod:`unyt` -------------------------------- :mod:`unyt` sometimes raises exceptions with unique exception types, e.g., to signal invalid operations, like summation of quantities with different dimensions. It is possible to catch any exceptions from unyt as >>> from unyt import cm, s >>> from unyt.exceptions import UnytError >>> a = 1 * cm >>> b = 1 / s >>> try: ... a + b ... except UnytError: ... pass However, it is in general advised to only catch specific exceptions types that are known-possible outcomes. All custom exceptions types live in the :mod:`unyt.exceptions` module and may be imported from there. Performance Considerations -------------------------- Tracking units in an application will inevitably add overhead. Judging where overhead is important or not depends on what real-world workflows look like. Ultimately, profiling code is the best way to find out whether handling units is a performance bottleneck. Optimally handling units will be amortized over the cost of an operation. While this is true for large arrays (bigger than about one million elements), this is *not* true for small arrays that contain only a few elements. In addition, it is sometimes easy to write code that needlessly checks unit consistency when we know ahead of time that data are already in the correct units. Often we can get away with only checking unit consistency once and then stripping units after that. A good rule of thumb is that units should be checked on input, stripped off of data during a calculation, and then re-applied when returning data from a function. In other words, apply or check units at interfaces, but during an internal calculation it is often worth stripping units, especially if the calculation involves many operations on arrays with only a few elements. :class:`unyt_array.name ` attribute +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ The unyt_array has a name attribute for use in structured-data applications or similar applications that require labeled data. For example, Numpy has record arrays and when constructed as shown below, it is possible to retain the units while taking advantage of the labeled record fields. >>> import numpy as np >>> from unyt import unyt_array >>> x = unyt_array([0, 1, 2], "s", name="time") >>> y = unyt_array([3, 4, 5], "m", name="distance") >>> data = (x, y) >>> dt = [(a.name, "O") for a in data] >>> data_points = np.array(list(zip(*data)), dtype=dt).view(np.recarray) >>> data_points[0].time unyt_quantity(0, 's') >>> data_points[0].distance unyt_quantity(3, 'm') .. note:: The name attribute does not propagate through mathematical operations. Other operations such as indexing, copying, and unit conversion, will preserve the name attribute where the semantic meaning of the quantity remains the same. Plotting with Matplotlib ++++++++++++++++++++++++ .. note:: - This is an experimental feature. Please report issues. - This feature works in Matplotlib versions 2.2.4 and above - Matplotlib is not a dependency of Unyt Matplotlib is Unyt aware. After enabling support in :mod:`unyt` using the :class:`unyt.matplotlib_support ` context manager, Matplotlib will label the x and y axes with the units. >>> import matplotlib.pyplot as plt >>> from unyt import matplotlib_support, s, K >>> x = [0.0, 60.0, 120.0]*s >>> y = [298.15, 308.15, 318.15]*K >>> with matplotlib_support: ... plt.plot(x, y) ... plt.show() [] .. image:: _static/mpl_fig1.png You can change the plotted units without affecting the original data. >>> with matplotlib_support: ... plt.plot(x, y, xunits="min", yunits=("J", "thermal")) ... plt.show() [] .. image:: _static/mpl_fig2.png It is also possible to set the label style; the choices ``"()"``, ``"[]"`` and ``"/"`` are supported. >>> matplotlib_support.label_style = "[]" >>> with matplotlib_support: ... plt.plot(x, y) ... plt.show() [] .. image:: _static/mpl_fig3.png The axis label will include the unyt_array.name attribute if set. >>> x.name = "Time" >>> y.name = "Temperature" >>> with matplotlib_support: ... plt.plot(x, y) ... plt.show() [] .. image:: _static/mpl_fig4.png With label_style set to "/", the axis label conforms to the SI standard where the axis label is a mathematical expression rather than a caption. In this case, set the unyt_array.name attribute to the latex expression for the physical quantity symbol. >>> x.name = "$t$" >>> y.name = "" >>> matplotlib_support.label_style = "/" >>> with matplotlib_support: ... plt.plot(x, y) ... plt.show() [] .. image:: _static/mpl_fig5.png There are three ways to use the context manager: 1. As a conventional context manager in a ``with`` statement as shown above 2. As a feature toggle in an interactive session: >>> import matplotlib.pyplot as plt >>> from unyt import s, K, matplotlib_support >>> matplotlib_support.enable() >>> plt.plot([0, 1, 2]*s, [3, 4, 5]*K) [] >>> plt.show() >>> matplotlib_support.disable() 3. As an enable for a complete session: >>> import unyt >>> unyt.matplotlib_support() >>> import matplotlib.pyplot as plt .. _dask: Working with Dask arrays ++++++++++++++++++++++++ :mod:`unyt` provides the ability to wrap dask arrays with :mod:`unyt` behavior. The main access point is the :mod:`unyt.dask_array.unyt_from_dask` function, which allows you to build a :mod:`unyt_dask_array` from a plain dask array analogous to the creation of a :mod:`unyt_array` from a plain :mod:`numpy.ndarray`: >>> from unyt import dask_array as uda >>> import dask.array as da >>> x = da.arange(10000, chunks=(1000,)) >>> x_da = uda.unyt_from_dask(x, 'm') Methods that hang off of a :mod:`unyt_dask_array` object and operations on :mod:`unyt_dask_array` objects will generally preserve units: >>> x_da.sum().compute() unyt_quantity(49995000, 'm') >>> (x_da[:5000] * x_da[5000:]).compute()[:5] unyt_array([ 0, 5001, 10004, 15009, 20016], 'm**2') One important caveat is that using Dask array functions may strip units: >>> da.sum(x_da).compute() np.int64(49995000) For simple reductions, you can use the :mod:`reduce_with_units` function: >>> result = uda.reduce_with_units(da.sum, x_da) >>> result.compute() unyt_quantity(49995000, 'm') But more complex operations may require more careful management of units. Note that :mod:`reduce_with_units` will accept any of the positional or keyword arguments for the array function: >>> import numpy as np >>> x = da.ones((10000, 3), chunks=(1000, 1000)) >>> x[:,0] = np.nan >>> x_da = uda.unyt_from_dask(x, 'm') >>> result = uda.reduce_with_units(da.nansum, x_da, axis=1) >>> result.compute()[:5] unyt_array([2., 2., 2., 2., 2.], 'm') As a final note: the initial Dask array provided to :mod:`dask_array.unyt_from_dask` can be constructed in any of the usual ways of constructing Dask arrays -- from :mod:`NumPy`-like array instantiation as in the above examples to reading from file or delayed operations. For more on creating arrays, check out the `Dask documentation `_. unyt-3.0.4/paper/000077500000000000000000000000001476461141700136375ustar00rootroot00000000000000unyt-3.0.4/paper/apply.png000066400000000000000000002041011476461141700154700ustar00rootroot00000000000000PNG  IHDR^sBIT|d pHYs.#.#x?v9tEXtSoftwarematplotlib version 2.2.2, http://matplotlib.org/ IDATxytչ?'!a"AJgE::ՊŢ +j^-^T8+GDB:CQT@@f2?\'' Nr|Zɉi{~$I/?nr Gr Gr Gr Gr Gr Gr Gr Gr Gr Gr Gr Gr GXYYYL0!l80P+IV[m v)7|=Pp Ѿ}w}?vY+Wh[n1f̘lD[xq;6;?r֋l4,?||'_D֭]ROcG$. :4n戈N;E׮]\ԩS瞋~:&L+V:(˕`VnᆈXdI92.,WTѢEe]?e˖e$V3qĴʕ+w爕+WF׮]/v)Yhf̘6mB ؈#<՞5kV;6_p@Kǀ8vw_t֭VsL<[ *((͛oPVVqrKVuYkӦMKc…1}ׯ_5kVӧ;O=T}.ck׮]Kp%?2'j dO81|,TT׭\RZZ{l̟??ۥʬYbQyyy.!͸qkI;E./@xq!d\6l.֫ĉ]F1}]K}q 'D$.5zW?VZ<;wN>f̘/TԭQFu]2jNI&e ޢEXU 1p륥1K|8Ӳ]F]r%1jԨl[tiyf`RmcED8S.+Vvm__QTTN]xq<_:Zl/--^{-M}]lf{[lQ/נv^6mvmΜ9K/ٳe˖N;Ş{ZVV5ȑ#3ό?"bɒ%oԩSc…ѬY޽{'n]X|y{q-yΝ&Myw}ZM74ktMZ5wkgώe˖onz?{xc֬Yx(..:D^m۶k/c9&^uR_oV̘1#~hѢEl&ѳgر:Y @5.$"_i H""릛nIk'Jr'&͛7O""ϒ$Io[feI޽)Sԋ5Vx3<䓕UI&U&TzATZW7gΜjkus9s]wU[o{ɦnDDr饗͞=;o4j(cwܱVI$ԩS.,fm*}-I$w\ޗU_Y|yr'-[LkݺuRPPDDR\\\xUW]ӧV2K~ܻwj+--MdwLHZjKvmGԧ~7iܸqIFVZ%&gqF2N*cǎMv}}͚5KX??r_=hj^^/~{ƍ{ꕼ曵I`J+Vo="U≮{k#,^};vlt„ N;ѣ[k=#>|x̞=;z.]cvW\'N;$w9:묵UVqƻ>`}FO>ѣG?{<=X̚51ƍ?>.\Xw}7~_o:tr3fѣ㡇ںb}^z'|Æ K~7wI&Ŧn7pCuQѢE())_~9.:thDD{sIIIIxqSO=ֽ{8ofU5eʔ8cʔ)Ѷm۸[cVZEIII?>ꪘ0aB&Gص5rK._W?)shԨQ|q]wO}W_;sԩS㢋.'|2^}g}b̘1sl'@5jԨ$":$%%%wu׌lO=Tָ{mݶ]'NLu9hР /L?𤰰0c|ӦM7xcQU4I~ܽYz?;,k׮ŋk|_uu$IL0F;o;L~W#N5jl&矟qvg߿<#?F}+:묳R~J,Y$Uۚv֥^Zݾ{S;j;v|畎+++K.袴uuL>}t_}j.qnҺuRRR;5v}M3-Y$r-S:u,XںWwvM?uϖ[n,[,cLYYYҫWԸ-[&3gά@Ue]H.J Wz . c:$=zH^}Ռ_|E{gܳ6$+V6l馛2yǫgu[oumV{䫯H$iѢE÷$Im&m{4j(93%K-++K60o2׿ɔ)SAxI&MHzjN<9iԨQ ?䓴_{5S=zdg?#<:4]ѣk}ic/*ǎ1"mu]Wm-kWʼn'Xاz*mgܖ4iRQXXzjc=،^_|裏jc=6s3gk:t_|1~ߩ:餓]vi}ٮG}SN͛ouV&lVmF6ۤ'MÆ n)ҮǰaҎ/))W^y%cޝv)vafmꫯFժ1iҤXbEDDlٲڱ?#j,>݇vXs 6,ɓsYgu͟??N?H$k]1G}ttҥk9kf6m2h޼y4m4フ@[okiӦqg=>z(""~TZj+?6ρhѢEZ_~>øcǎvۥEEEת_rkt_6mK.I뮻bڴi뤮?O1g}G5ow˖-Kkϙ3ʱ?P3Vа ͋1cDDęgY38##$SMmT6V>E7IbƌYY  NY_*M6]=ݺuKk7kI&+l-j*ҥKkouگk&^{?> R6lOiqGqD/}GW9ŋkNM7ml4,` í+W=zD޽۹s8C/_rK]c|F6m29rd,Y{|͘>}zt1~_z곊_u?iJ oƸ袋M6zj}Ws;3j7k,:uT۵kgu5*uvďGzׅ?\pA<3q 'u^>J\pƥ^zE )))IgqF4hPFM7+W\U75""=(,,L,Xwyg|1Z#;˳TI֭[;CYzǏOkve3jt|yu^xᅴv|9cСqWz=Ix . zCVU&MT3 ׆_;vl̙3'ZhQjΝ:x}hѢEt9opkDDt)cJ=ztD4sUW])SD޽NH{Np}0}vm;d͜9smK8qbZ?cȐ!ѭ[8ckt=x≌q&L*vi^'P{EEEF=2s=[-Rov-W|~Ej=fmykcܹϳ]ԩSSNN:}]vY|W??뭆}qyEǎcСqܹs=0 w}7^u:߄ |kҲe˴v]\kDD#9䐴aÆe[uݿO7ވ:*gqF>urxmU|~%KjNg-_|ko3*ua̙ѿѣG7.nciv^_ϟ nݺw]vYL6- l0n!""z-~1\TTh <8ꫯ;3jرc h3fLw}馛f\7b]vG}4 mڴIk'Isέ?lkl z?y<^{| X#ׅqV[m#F-"y8s + wۭMzgF-jugOČ3kxs3sϴ뮻.s=}]|Yt?8qŋ裏z*KE?~aXlY歩vN:u[ǩ˖->}h׮]&#F#8"СCKѭ[4` "~|h߾}yk5EkTqw{ X]]G*""z衈psCQ\\C So~keeeqgGYYYVj~3>ZQqnf~s+=|չ뮻S믿>7o^'kċ/g}vQZZ#FN;mˋ;/;~'Xj} r~[%%%q 7IJe֭[^5P;cƌSN9;wcƭޚv3_^%f{」0_w?陹:tx?UeҥilѫWuNM%IguV*-((9U#fϞqi?c_-ZwqOwM͛q75*ˋ.(nz(utO F$kSRR*Ɏ}饗8Ns=7oڴik]WuTs??^~okG}>cj^`%2@?SLIV#jwҥ5Κދ_=&m۶v7O=` vXlf?i&MD3:=K}߲e8餓߿t!cD7n֞3gN.]ϯ˒ZŰlMYp53sx뭷83HkWo֬Y]}oď}WU|/* ToŊ>b=?~Hp `VZZk׮dm65j׿5-[Vjrߟ o8p`O3Ν[VwGqqqZ~6g#<{oƸ cĈq衇]jX`AZ{Ŋkϳb[ђ%K249sft5չb;n{YoFqV;w^ti<#i}cƌxnT{޼yqWq/?u7"iӦ1|s9'J?1o޼8C3o=}ј>}z|y睗ꪫbȑiK.M +[0aB̞=;m??J뉈8*]w_;oTYH ʕ+SN9%СCܳgN^uQG% .>,c|f͒{ʵ%-ZH""e] T[X<929#oM9眴M櫩_#H \}'?kI&_^^t)m;XmM˖-KZlv/~j{3jٳgRRRR}{w=yyyi5*9͛'W]uUj?m&;wn󖕕%rHjwYmhѢI&iul2˓;39W_͸wI^?\ZCM=ÒKױJt <89rdrm%gqFR\\t!9*'":5?Jɐ!Co=ӟt5iѢE+$Z}曓?>9餓j2޽{r'7tS{$:tHϟ$I|Iaaa=[mUrW$wygre%m]RPPdM72xdM7MN>䣏>Jp{^KJKK$I'%WJ5Yk<_dm^_n7I?Lg}:}OWO>d$IFW\|駩yWX\z饕w5$+VȨeѢEU^{m|x +~U 5*~ IDAT)++XutIIyyyjpD$;vLOyw_W1_~:yVwgTYs~~~rwTyni!g|ǩ1SLIN<ߎs9'κVVV 8po>yw2~SO=5y72ׯ|S^8#+Ӹq˨Ei&yw袋Xo7.I$9s+u]u60ǏQrGjޑ#Ghտ*qr'M6M,/~5u[mU^hܸq-ܲVW_}uƮUaQG|gdOF|7 <8f͚%zhꞁ&͚5O$IzQdٲe-Z$^{mZ$_[N} ǝBumI޽3jҥKϮ;/5kz:*5Ts%;s?K͛$I\zI^^^ҧO{vgҥKO>9Ϙm۶ŋNڶms}W*#"g}>JYYYr%T-[&~zjp$_񡞶m&cƌY5Ѐ%I@=0s|>ҥK|71~ϣ<:tm]ln2ufmI&1whݺz~ǴibʕѩSӧOt!mڵۯ|ǏK[l|plٲIĂ bƌ駟ܹsS ~Ko]㥗^{/b_>:ӦM_1gΜXbEk.:u={MQUbĉ1gΜˋN:E޽]v1ovm6tRy_z믣m۶ѭ[իWf;wn7.,X=z8 {ɒ%1nܸ1cFl2{qgΜ9 /ė_~Z.]ľEEEcxbܹ[fO Յ i2tи袋裏ѣGgN}qꩧf4jL<9v)0@=$I <8""=rE@}%&M78N>lc`(+++))ipkVyyy 80ZlmYwq1~XdIp q%DDW\͚5j@VV3gNFߗ_~ݻwoPk֘1c[oŋC==Pژ]w5N;l4 v=ܓw75jk޼y^h| T//I$E_~9y晘4iRccիWo>cc=?Ō3bvEe\ԩSS<`4mڴGV́{C-"z葥j|h׮]<6mŊlZ #5i$.6 syyy|9B #93s˗/Ç=b.r#r Gr GrDA 'I(//$I] %yyyyyy.u@ Xpa,\0JJJ@Eaaaj*Zj.$ Ċ+b޼ydɒl3Iʕ+c1h޼yl&ѤIlF-y0lJJJb֬Y_F,Yf͊lB- @kԨQim֟={vf)--ٳgGΝwD @+,,_.`5gΜXbEZ_~~~GqqqF~CCaCU^^%%%??k+V9sDǎX!!VVV-Jkܸqt9 7n͛778>XreڢE.y U|o^^^tI T :uyyyip U[TT7R5@Cиq(**J[xq|)_Ŕ-]P ee1kT?:Z:z.ZlJEi? :WZZo|YnX P$f͚e!,$83`>Po rT$}v5QߊPr Gr Gd~*/O%+]FNY&2"*.#}bM]C #9ԹqQ'G]IxQ^^W^yew͙3'Fkg `zX&5906hT{wqƥrꩧO$&NX霛oy/?~|+W#<;Clvkru0}׮]ګѣGZ]tQ$IbU}gbq 'Դd`e˖i%KT9vv:(վ{ot>v :wL} Syyy~ٲeq-dYdI5i$?O?t++Āj4G^^^u^#d`׼y~i&ONjZFqqqc9&"~\M6M}߮!Km6O<ӧO3y8EsV:_Mw /]4]@8jѧOԩSСCO>$-.--zƵ@}"O}|իW}{ /sN-:w{-?R}1T{U;cƌw}W>(((HkzsDDϞ=Sߗ1z˱WnC}'r)qQG_}Ux≱;Ɓ<@<ѯ_-Z<@L0!ʕ+S &g}f$qק=u!<ȈhѢEkX~/R3oc=Ҟ1 `Go9vmh޼yh"n+bʔ)q衇FDDn .'7|>huY{'pB ,:z'NwoV1rȴ3f̈Ν;!+VQ:ꨣҞe&EEEǁEEE,:xbȐ!5ꛂ5 Ӟ[<0<Uujj֬YsMlsϭ:']ƾR^Ϛ7VgR{?k$%4hSN7|3E^^^ˁzA T*??/6n4e@EEEq)d7 @ꫯƨQOmf7?n(..?<=Oo߾ѦM_^P5o2fѢEq'FiiiDD<1hР~86hl#5k-[ڐ!C_~"K1nܸs=(ڴirHL0!^r4^{oFˀC @+-Y|T{_X&0u<9= { #9dqjayOIj^4mhw]lW4v0@0@0@䈂l@ˋ'5i\a.r#r GrgP$ڭZE^^^+$έXY}2վD&X5G,.U䶢" ;`rK[mLh6U9GJr Gr GrzgQVV2)vh_W~JtRgdSiiiuQq}dhP $I_Zghp ?;=0 iD,(EWDTBD FK@HHH1DEEذ5*c,š0  G\Zֹׂ%?~cҤI1lذT,;pUj͘1#z`*\ffhT=qǪURYjU|ױ{*)G@%%%1y8묳iӦQzɉFEc„ [ܝrӟGydQJH+[k׮|?I&zޜ9sk^z%fΜw\Ԯ];w3f&}뮋#8"6l999Q^ˋoU+/aÆ#qmѣGo"7775jvZ euꩧ6)+X.***sA;G @̛7/?޽{.^~?~|h"&Ozj# \O?֭[5\M6qo=XcիW?!igs&~:wq 6,ڶmZaƅ #"bܸqѡCxg ϟw^Y&i%Kĩmڴz*.x瞋/<ϟÇ6mD^T3gΌ>:ty ,Ν;GcĉgŚ5k?GN.+#GFNNNx-bʔ)3ϔ#333x㍸c o;'}9+B.;裏/24h/RԬY3""[nѱcxGo1 Vj3<3>ݻw=:1~衇F׮]n(FD4l0&MDƍ&LO>d̟??|ͤk/R\pqgī""b͚5rDD.]O>SN9%~ol֡C Nz+|x7gF%-\08tazaÆqF˖-cŊ1iҤۣ(""F1|ĽuԉK.$ Ow%~k۶mm?bKzP֮-fNqhde#6ָQM\hS*#Fė_~O"]/###x#"bԨQ1xMϏӧGDD|oۘ0aڣGnC=4#"bq?mڴG>;~EDķ~ݺuO>$5jGN kРAL:5Zl/{/N>xSN9%""uM6MC]wKZcǎ_"N8.#FD裏N{饗ưabٲeLj#"##c͓O>Tu1cuũn 9s?24k,yʕG%];wnrcz,AM6QV=ztĨQ|ADDW5jlr=#i믿zkyoRݧORz:u'꒒2dHy5k֌_Wz޼y1uM1vc=N<қ;c^zV IDATR&[bժUesꩧFժU$ժUK|SN~雝dɒ;qguY{$?OށT>]/z%꧟~:,YR #FlrYfܹs㬳*kbܸqqwƔ)Sꞵk&sLb7:u}OzmyUfG:uj^:QoyS^tih||ɉ$q|4h}IO?tn ;6""i>Ȉ={\Pj_}.C ծ];(3g! k~+I:ޔv%eꨣJ?2 0 To]?pj*v7vm`رccnݺEzbܸq[q5$kãyq= w/21;Ep/5m4z葨njSӧOO>_"B )**o=4i7pC\r%{UW] 4ت5w_ԪU+1W_E~}1k֬jVXTow=x[nk6yqw$];vlTZ5z}~ť^ڵ8ӒZsN{ѷoߤ_{8.ַZ:uMŮWT۶m8cQb͚5nݺ7n\wqѰa]{wwy'8x衇J۪aÆq=D~~~r)8Ѷ~T[u;7nýlm۶u%>/Z(ƌ/B|W&A PGϞ=W_]jw׿K|1a„xꩧvQRRC ٱƷCǎ^zi[paRݩSe޼yύ5͛ov~ΝM6zĈϵk׎N:i{"=*g}6{DݪU2畔lqzj׺t.kmFFFRnݺ->s[t)]Ə>hݽeٰvE&Mv3g&>_x[uφ~w'i;A P'Ջ/.sg}TK/gu- pwsҥ\k{W_WZ?xɓ}#"nݺqo}=zH ;XlI"dǠKHd%lvܸq\2~$mG|Kx|Vaaa|wqGިQzIO< %>_wK.8c7zGDL6-^{8k׮[|FY͆X`ADDyQnݭi=VEiӦdrTb~{\qK/̙3cԨQѦMhӦMdee%92O#G)S$޸|mK]oӦMTV-Q4(~5kVp 1dȐYfܹs-Z|1nܸˋo&zQQ .L֭[vm[\?"e^1cF 0 ""?D=js9t?O Pk.)D,..[o5'ڷo_}\}1bĈرcbެY⨣o9>5zϏUVEzYfqG޽c:=p8묳bȐ!sŢEVZѤI8餓΋ lqMycc9&Ml rwމ-[К$۱BTW\qE\qwkzjǾ:th :#G#Gskݺu.k޼y=׍;O PV)11Q_w)Žv]SL5jlr5(n?b*n?JubxGO>QFT([Fi9rdY&V-oݻv(,,LZg:DNbСp˜;wn?:tOzs4/kڵLu6XzuL jv R{TN4gΜz֬Y;g-^x!ĵCFfvh}қ>x衇O>IO?t\}sE~~~[R~2/꫷_*;`;qtR%%%q--ѪU^wܸqqnj3""?q?w^>,YR!tAOWڤ?G@ 0@p4jժTO @ʊnҞ#҄ M҄wPኊbىCL@yT6mZ>P &iB &iB &iB &i"3 ?L%%%QXX6Zvvvddd 2Ɣ)SRFZ֭[䤺 p999qy%ϟ_xQQQ7nQvTڦ%%% ٳuQvꪫRT*`ե^gώ1bD9{NիgϞdɒT;]T7eݺuѻw1cF[k׮?-***5RPP={{+FoiӢwqDnnnѵkxGf~~~xѼyx790~Kb DDDIIIL81N=^~馛zy'N:+Q1|Xxq,[lϯ[n\uUѪUsF#"boǣ>yyy9Ǐ/geeE-E]vbEѸq-9 ܹst9~_Oo'qiGx'N<@}Ѯ]h׮]D|G}1]tQ꫉O?=~Gc[:Dfbҥq]wiV;*jժEzuN⦛nJ kժUoK2dHy*ޜ=36lXR^:uSOM3f̈~T`*\*UI&*U?$>lvirH~wҜvme˖+Wof#pqI' dddgW_}5ڶm[ϮYfRrʨ[n?v%`M:ꨤO?MIl<0oS 10dwO7>H­[.>DGժUS#23#=#EP֮]>h +((H۶mN9m2o޼眜8ꨣR !0d̙Ͻz5j222׭[N`MƎkJ]/..NKJJvJ_&>/]t>RM @g}OvDbŊ4iV?v5`^x4hP]߿sύ.(2ט={vR=k֬M>/H-[ɹ꯿y:tH|^xq'^{xǣC;}G2L`Zjv[|1rxWcƌ1dȐ?1㎤w.Y$x㍸{nHZs%~]EEE$3fLҎx衇J-˅^kNcǎ#8"N:8O/v1`ڶmѹs>|x?N8!Ə\rIquוo[k.=ܘ?~ҵo6ˋ;""hܸq=sTJaG'˼v==SL;5`p%%űrI5ZoF[*#҄ M҄ M҄ M҄ M Medd+))IA'XYÓ 2"vݤWJ}`k׮tJJ{ ?<`*\լhTPdddDvvv&VX[ v+VHE լY3.((p 4Y%%%QPP4%p  mڬ]6.\(TRR .k&תU+E i,777œ˗G}jՊ5jDffw{B%V\\EEEbŊ((((feeENNNc[ p%űf7:f?;EFFF^g%]vm,]4.]xC @+^6>69Q8WT_,[ᆬB`Ȉ}7vmT6 *!pVVV[vYYY]PInѤIXfMˣ0m?QfͨUV8y%`r'9޸qi`׏(..v44TbQJoy7ymeȈU ʉw 0@ 0@T7ěO?|'v?,`*\(`GTp7n@˨R5v߷YiB &i;pEEED#J?w*\IɺXهzV?9 M҄ M҄ M҄ M҄ Md*Y٩nҞ W5+'Zxvh4!H`4T(.*LU2####@zs Ѳn ͓u)HkvS?0H`4!H`4!H`4!H`4!H`4HU3c=j pU$@ŨtG@}1hРTP*U[o/wqG^:JEŘ1cR @4%K⡇J#FHa7KѺ6OѺTiwuWұs̉S#ʣh]FpI[T)ࢢ;J JO81,X]tԩSwMagR_#"⦛nN:)]@H7ߌW^y%Zl?O⢋.JŋS@In /0"":u͛7O\_zu5j-..{.rKb|̘1իWx뭷.]K/*T7QQ,YOddd{DD >< נAO#''gkk1nܸ7n\̟??"" +"n֤;vSO?ǏGy$/_/B??>^y啤 6lX 4(+nݺEVVV.VX5k֌I&ENZfM<_*sDDn֭[!~䈈yŴi /8餓bĈo{*Os̉VZ%w"ZN1 ͓u)`בq}|O>+Nl2T7PXzu7FD7nXzuDD|W1f̘ׯfC=4"ZXX?,f͚%>Ϛ5k_{ʼn|SƎ}< 6":DȺޟ^Zj]t٪nxODD~Jwwc-ZGDDFFFvaeΫ^z~;5XresbѺuhݺfR/-~7W^իG^69 /K.$QO:5Ν[jl~VZ1s̘8-޳v^k3όo&ޤag}6,Y_.ٳ63o޼޽{|9YU$@KkDDyQVM;k+VDDDIII1"뮝jܸq\~W^'O/꒒ͮ׳gϸ?[o5.HCEfffuY^;####r ]#OŮw^#n ŋ㡇M^Z;a׻cɒ%_y7o^\uUѸqxG㪫38cZj 0 Q1eʔ9WGy$t 4(6OePy]|wƚ5k""}ٟI^:TEs΍={A~m1f̘8cJmOy{쑨Gt'ӧOT:(EFFF=f̘͛7?~kٳgs=W^mn-UVWRR]_y啑1r;vl4o<K|.,,{իW;gvAi1y䈈8䓷{O?=~'7(snQQQw}Uox޷a=}?~9>h⋉z0xk 0 o&֭[s6.Z*.D]z^iӦ.XfM7|_~e~ףxϝ;79sUVEΝ㡇z+zׯ_\qѳgļ _6mZhٲeұ;v}w?,ltҸ#3f${O>d̙3gYfM̞=;&MTϯzk.ޘ9sfs=qqźus>ٳg3<3ڴi'pB,X ^~˒+?iҽs饗&>g~X2JJJJR{s $ݔ~;իW+Wnqއ~Mdɒc=xOFbe^;wn 80X|yqGF޽OL̻[b|ˋ_ѽ{[.U_nup3̙3'Zjh)V ͓NUS@9:lR;D˖-SQLu۫o߾ѷo^իtA1nܸ-0`@ 0`_ڵk3Ae|M2%""N%&""F-Zc9&խi=K/̙3cԨQnHSUdaTO\ɭ^:.hذ㟁 Y%o'$>߿,]4?ׯ_4j(N=:rrrR&u]=XDDL>=nĵO<19T*n~ey1f̘>N\I+WƔ)SbŊqF޽K/TuőٲD}ȾG* 7p%Qf͸SPI-*I}[U;'PlH`4hv9<=s@5kbA̽ωv&fHuFڷoH`4!H`4!H`4!Hn'|rR ?0J*QnTiiB &i;pc%瑝Ž = p%%%_'@s4@ 0@ 0@ 0@Lu+/J}'7n\v8Sj^IIIw%UR%6lTO @ʊ] H{` 0@ W_}4hU7HmĉR  vx9999)`5lذXƍw~#ۯ҄ M҄ M҄ M҄ M҄ Md_ffft9ʟUZ57n6 9 M҄ Mx0nڵ1mڴDݱcJ]CTOj9 M҄ MDfmպuhٲeի^HGqD榰#HOv 0@ 0@Lugc{-[etޮ nPu]mܒB0ʬ,GY"j 1KIfɢeN4m7c0Vd҆Kֵ;=s#y4y@~J%,Y24'\Nٲ;Jr4@0@0@0#fOF̞GF/{#HIxs'D<'veo) IIIIIIIIII^s@Kk&u?~ށxZŗDDĆ J^r4@0@0@0śF= !w$` םǣߝWb-J@N0"ע$w9G@$!$!$!$!$!$!$!$!$!$Q/{GܱalhF[RĒWh$`$`$<vGڃWJr(\qt?4h$`$ @ӃZRvNT /Y5XC8     z _1+_xqǓ#Pf׺GQZ$h$`$`$<KQVJr(\g./3o3(` DȯYimshFOpj%ZT^s4@0@0@ @~œkѬ{&0̂7Z5Sn+{07yk4Oy񶷽HIIIIIIIII^*J,YdhFOpF#lR# ` رcybb"U&Pn>`޺uk47~ D\tV5 %_~7 G@$!$!$Vf`` WbժUe P]w] %袋[VyJ wPn=`^n]~yzk׮-oHIIIIIIIII^sC({HOpf3ַh$`$`$<t:hDR)q#IpN'v=nfč 'G@$!$!$,8[l/5S033 7%n9      z _^zhF;0P G@$!$!$?<7mA0j}pn7za h$`$`$`$`$`$`$`$`$`$e/@~ccc}=U*/{ HIIIx0;w;30bff =G@$!$!$!$!$!$!$!$!$Q/{jvڡ=5غukk@zHBHBH3(\<䓃y͚5QJr(\Ӊݻw橩) h$`$`$`$`$`$`$`$`$`$e/@~f3 ,8_}+^8=X?նu+^N# ` iX@N0q 9         z _ZO hտ5 =`$!$!$};U:U&Pn=p`zm/q#I =8L_?oZ#N @w\ ? ?#Λu` q/uZ#7G@$` W7[(\VW5 =G@$!$!$i7n`߸6f@N0zC30zHBHBHBHBHBHBHBHBH^WT 7e9  G@\|*.Ak} ~Z` ؉/JRF @f۝yy-7Xč 'HBHBHBHBHBHBHBHBHBH^WM(\^ []# ` too7h4Q;⟞`z(qIIIx0̣>7}F_j|J0̣U׼o IDATq4@0@0@0@0@ fw^W/r5g~ R`^}.:k# ` 73ێ?/c7 DȯVƆ]64'PFnZW[ ` ţ{ .$Z@N0Şu0Z%@`򊘘X])//q# &''^sg$!$!$!$!$!$!$!$!$Q/{oyhFOpJ%&&&^s4@0@0@ @fgg{z׻bllč '8~ ##N=oZ#s 9oƍe~X ;(\Z׾C30z0k6񖷼5 =`$!$hy7Fr˗:p` 777>`^jUj7bŊ\grrr$%PN`~/@<          z _ٌm۶ ZƊ+^s4@0@0@ @v޽{0oݺ5f@N0z9   fcrr5S033/̓1>>^F;}_b⟾_)ӏ=7+q_ݼX`A%' hk~Kk@z gP~'g0AT~ FMpNKywD:VFۯUkx7yUXKh$`$`$<8?k~=u?~:4x===VeX@8z|o?^+o^%mBh$`$`$`$`$e/?o|CP p8U[Vy`9  G@0/jf+@z05bwh$`$`$<uۃZoFR)q#Ipn;guZcč 'G@$!$!$!$!$!$!$!$!$!$Q/{Tklf`` W㢩OΈ̂GB\xIkZuy1II8uzӇ󦩋^;H0j0㡽.]Z @RnHBHBHBHBHBHBHB^vK.׼5}u|vJgϞ^}C?}x{3 |nxG{+e?n/͛O{zů^^ ѲebٲeqUWڵkc͚5CǮ]F.Ҹ뮻cX,]4>O?Cy5}뚡=+"-Z43|#Gc˖-#OO-2/ ȯRx6ST^ RG ./’6'NĶmɓexᓴkZI~7tS<3e"fȝw_^HG׋~_*HRj}I L}ǣn`ڵkiί<5WbpOKNMӉcǎűcǢ鈿gJF#&&&bbb"F+q|;cݺu?zk\vehѢT*/>nƍ_}͒%KFo30;;}ő#Gng~v;9lkq|t:>җǎ+Wԯ?zk87XFߏw#DD3eӇazj.;:N8p n٫pN<VnF>Ks=1==v{{{vq}ś߭[.nظqcn7&&&O~yz*>G9ٷo_AE8tK VxXxq4V=l7;;7T oE !?W/M6Gk׮]я~knn.N81f+Wz[gfw^,[,{hۃ;qD͹ x+V ei#"bff&yoG;y\TbŊ/QcŊQT>KNޝxgOHˠbWZFV"kKRnV:TjmMPZZBKkطڃX 9CNy}u9I}O5j(ݻw/]Ç^zYB""EJsVWlHPPf͚={d2iĉZhL]Zf7oެkȐ!Oi^>> n*<~gw`~I7nO?mONN7|gyFOb {;)id2H1(2y<~3))){n\uڿϟjժOkN yrȎ?a?fKuر._lN>O:E=R/(Mo%IM41f͚ѣvءM6uֺrJd=WbRֳh 7nh/\PWڵUX1+::Z/_Ӎ~0ɓ'ue]6]ܣG >\uUժU?<<\СCբE رc%ŋг>kR`IIIڶmfr!0}tM4I/^tcǎرcZn-~| ڵKO OTTFm+:uh֬Y۷3)RD6l}vaÆz/&I{QhhBCC~[ɓ'5g_^.\Pllʔ)#???J]ϟo 6hÆ ٳgczXC0` `{:u*焆ɽkԨɵa5J'NX L29֔)S4bĈlM<9Ù jȐ!v횵Ky} }~sZxϟW^y%j6m9s& zXX2d6oެÇŋ TbbbkҤI\1///3Fk׮Uppp8 Txxdrlŋ{lٲ?6mv|_~:~N>'|2_ Ke˖&Ou;{Zz5bٳG%K$iݺu;vڵk'oo<ggge˖S˱I8pL&ڴi#{"*Uhҥ*W*T`YF*RTJ;_ܹ|K Nׯ_v (ݻw[`@bggyqfݫ~Y*vqqq.̭[ԭ[7-Yڥ,^Xx9;hܴb =f,XI&=ҵL";;t^{ͬ_LLL4vLPQF[niԩzTti)RD4iͮa2fCUT*Tƍ~&I!!!jӦJ,)WWW((((W+L&-]TzR*UThQyzzWC4ӂ Զm[ӸqTJCLk۶m2dׯҥKYeʔѳ>O>DOܹsM&qO?0ܹӧ_T ҥKOŦNLLԼyԹsgUTInnnR+<<kN#GŋU^=7.ݹB`)SF6lؠdƪM63fΞ=;wh͚56mZcZgϞUVԼys۷O~6lؠ 6(88XUV- #>e˖gϞV۶mwJJJ҅ Ժukjڵzt! 6LJ(QB:v>7˗/7;/>>^;vT^ӧ?Ж-[4|pcz{n.UN-ZH:u2ҔS{Qs}MoooiF 4UNթSG5k^׮]Sddj׮.Й3gΝ;+ @jRrrLŋKVXE|ҥ5j(UXQњ3gΝ+I:~&N/"G788XlYL&]zլٳu-dxu֪V~w-Z8'???lRIII3f̂^{MZnn߾m{vK?vڥ;***J _-;,7T6miӦ֮]keŋxޣTRUC՗_~E oX˖-q)**JZjڶm SNԮ];XW5Ν;z뭷_sQyzzdɒJII1߻wO:u޽{5|pjJ˗שS4x`+%%EoڶmO>D˖-|MSǎѣuqIĉsg_.\Ν;ǎS~TV--YDիW5f۷Od-^Xݻw7srr2jMyų;7o,WWW /VZC}ꫯ( ,L-///.]:G}+WCjرԩ?""B'O־}xb+jР5j N:FQFK.Zd{kNj֬~Gg޼yשm߾]+Wѣ˗/ʕ+:uziwŚ9sfk*00P7nмyZhf͚?\7 ,ЬY4c >s.]Z,YPUZU+V|ϟ?Ν;+**JԩSWEc;G48p@o|||3>Æ S"E$ݟo߾&qDDw.'''͞=,} 88lFs߿ϟ/cW_} *ٳ8q^|Esf~ՠAL IDATڿF{NkV``6nh)))駟2|/y _x5nXy=zȑ#]nݺرe5kV͍͞=[Զm[vکo߾|/lo0`پG>rhQVvVjl'%%iƍ]vիѾx&MVZex>}ۗ.]ҙ3g_gdɒf5.\0]#zǏשSԪU,GAKvJJJ>fׯk̙m߾N:駟5h caÆesYFeW\iOoodJ#o޼Ѿ|V^H6lnܸ=zDWݻw`t/n6o\f͒[5R5vddV\iv*T0~=-[֬=i$ 4(]%Jk׮F{fFddFqLLL ؀ƒ)Q(C+Vvr.KEa-w[]+oti[طrJ3&ug%mUbL DUiߴƞ:u,lΈY0ԩSf?^-[fw1Fg3i믿n6=8|VXa_|l߿ %&&J?"GyRONرC%ڵKup9oj\%m O>'NH<==UTL֪UxߧNJwY> &盙+Wkj;yvsgZ0mmQ/^\M6t-[jڵ_^( ŊShh}Y]zU/zfk9Ǖ@.]4rH}c{~_7x-eǎv`.+5ƍ]㓗e*]zʗ/s/'ոqѴι>zy)5k]rE۷WN4ulG ؔJ*iŊ7;w( @Z,faZ/7oJcƶw׏+Wwy5k4k_v-jN㓮okv -QFZhzeǽrJ_^>#[JXS@lγ>ywuݘ9cgggN(66ؾ}vK; unGe[~ٱ/۷FƾxM0A5kҥKXԽ{w7l_XXn k^;746(QK.輢E[79/Ykvl齤ְaC8p@Ǐ7ҥKڵyRlѣէO}_ϟo Dc;t uܹhBJyGKʷG{f?cg\]]ٳ6l\]]c3gL6{l=f~7+US0EFF/: >u6nܘk*(((o mrt^DD쬦MeYYJ|CBB_·~ߏ|߰09>o'|Pk=CKT۶mҔ)StթS?u*G!F%%%>>ki?W^y%@ԩSjݺ4h6mX4ʕ+7|hkŊYիWK5z d~XAAAfԩS3k/[L 6իOzƍF)&&F3fHw5߻w/_|jժe?ҥKO?tbbbyPT|yf nݺ?0L&=zTS@@j׮Çgxʹ_h[777}fMVZi֭fwxx ^xA/3o. u3h/g˗/olf?X-_ܬ }o޼)Ǿ9`q*W?? w޽kٺ~aٲeK}w9&I:tӵo>mݺUԩ>} xb͝;WK-|…LX:vY;TRָM-!!AAAA:qza`+Ir>p^z%yxxG*[ƌ8zپayxxhʕ$>}ZСC%Io>Lo>]/R 40믿QF*V*W"EWsO˗/ӆ9sƬ}ƍ 5kؾ~g^Z͚5K0`@ݻsNIi}]cǎk"\~˔ 򎃃jԨa]UBkj)))//G2޽{oÒOzQխ[7ð); 4ݻwsN?ӾUVխ[{nIR||֬Y3gjܹڻw+ 0@ ,0vtt۷e23ݻ:p~}f8~ʕ+dyyy)))IGݻ5n8EFF=ŋArrrґ#Gb =,:t<==%J#6m̙3Ӽyt5%''Ν; շo_:tH&LɓͦMCwvmJ%''͛ƺժUբE dҾ}/hѺ}{wP?ﴼԫW/?~\'NБ#Gj*d2̙33gx 9::jʔ)7n:QGզM4j({/_sQwޕklvԳgO߿?z 1FiFVRŊ+͞=?y{{+))IŊ3>'vvv֭Ο?#8oذBCCFK={hذafahxx\]]")RD:rBBB?}~wy{{K󋋋5c ͝;׬TT)%''35jN>mKNN/<^A}-j5kٳߓÇk G}`I3ydIlذAJIIщ'O?wU||>S 6,ݽrR>Wҥ0gg`9zhϟ?_UTbEqgB-))):իW28ʿ/;6QsSW^u))) TݺuqR/ӵ~%&&RJر J*Iz-999K.zܹsշo,QjU:uJ/^0|Jk۶mj֬ի.K<իW+,,LgϞյkהҥKW۷׫9֬Yo)""Bqqq*UyuU=z9FPŋ9%'mۦk֭PBB~̘1#(釱vZ/ڹs^*sydkjO3HΝ;?j˖-|UL5nXzRN2ﯭ[fy>LG'dU~,VTɘ6>}|,Xhe˪I&Zl$sڼyY@j; g٣Cj޽s=>@>'.]Z2d7nɅ@LU#9E X@#.Xl_X\JJ1$)S׉,.11QW6}I xt A 6l0Gk<˗/ϝ;gɿb0 .]dyd2S%)F A 6lZnmyX|||]l2vT x @U\96_6@a`#F04e/'''"X\JJ 14``#F h@2LJHHv6Yvvv.2D IHHʕ+]ԩ\\\]d)F0`qvvvrww7k ڵ%،&MX0✝ճgOkؼӧOk׮]|RRRT\95iDիWvi 0@ݺud2 D8pedk1boߞqM>]k7` '''`5sL 0@&ISʕҥK-[qgX Ytyɓڷo-[5kŋ?%Iwѫ+W g2tme2]PEFF_~0aBBBTJvvvzW4k,cߙ3giӦ.@>"X\BBBBB0ĉէO5*~={TR#G,]+b `&%%Eʕ+kggjժ)22RfX0 ueȅ8m޼h?ruubE@foo[[$ a:w$I&jҤu `Q6lѢE{J.~0`u%}ǪUTZ5kX8qB]v՝;wz%!oĉՠA:tH111jذ~mݺu0Fyj|eȍiAi+lwi…d͚5K7oVXX*Vh*X0 U TzuEDDhϞ=ڵkRRR>'OT=}v3Q,`ls={l߅ Cs5ڵK˗/W.]Bv,AzڵkvёbŊ5uT .RE,`yRj󓟟0φVZ={XD Pؾv+`I`JX0@!ml-[֊$aX^rt֭$jJNNNV. (\޽kl ,ϔ"]=ݻH |vQc{V%14@!tRISO=^z`իZf._m߳gj̙rrrҜ9sdoOD*HLLv ܮ_'|RrttT5~xyxx.](..NSƍP1B @!pk<hIRRRMKjzUbE]~]3f<<CرC.]RRRJ,)oooU\Yg_"(T*UX `l:ud2lKL`cb2V`# >^2L1cj֬)IZ~^~e}ZhڷooJKIݻ{n%%%Y"h„ JLLThhJR۶mկ_?ݾ}[=zЕ+WX%`,?uiݺu@!3odB^zrt$ȭ$߿edd2)&&F:z6nܨKʕ+uׯO>JNNVɒ%kŋl㛝ŋk޽FK,QլY3͙3G*Txj͛}JW_ռy;VQQQ go:uꤥKjZxw{٩{:wF0 IDAT *UtYGUDDo>M2EE?7x#gI6mZ@޽eggW^{(pL.@.%exi;JNvws3  %JDyre˖)..N驧z*M41wܩ'}RJ:wl٢#G7p#( s1^.|xovݺu_dIc^ɤueoΝPjԨdاL2ѣGsS64ggg'O^vv`54kԨsWnl8q"""tI͝;7>w5K,{0@!ӧO휆e6Xbʕ+w鎛L&]xQnr኏79]WؾpB}:v(777]VgҌHpF)۫}+@0@!v$oN%&&]]]+RC`f*g bŊ=y3ThѼ* C"(*UdNIIyƶW`9r+VѾuVKרQ#{j׮mrt^DDM <MǏ϶BBOSOY.C h>x`;$ݲeK wgo'*`]:t I҉'t,YخUׯo 0Գrv$7nnnڵ ɲy[.C I| GGGIԩSaM6֩SG˷d$''޽{F9>VZ%IQQQ4hP>7nЀ$I 1Bco#@RRK(疐Ç+..N:~~W]vرcjҤ*T UPAe˖|L&FYW^UULd~]9@ NF^rg`~k t钞y,ܹsG3fЌ3}cƌرc3=gȑ Ԍ3޽{+..N%KTڵ{魷R"E#PHNɾ'? I|\|||] 9L&\Zj?ik`($*k 71Gdc 4ib#1={˰Y.2E SbŬ] l0B`w;>ɤ$w:.CSU`($|JXo҇D<] |+y)'14`L 8gzfmgoo' S@ A 65d҂CF:NrvdM`0RLҹ6{L 6l0``#F A |1cƨ^zz.]ĉ. .`줲fm޽{թS'խ[W+VPJ$IׯWvsN-[U0\N#gkΝ;۫nݺZrc6mRTTΝQFYJ#E( @qqqHM4Ityk `8}c+Z ;?$$Ia5 l_QAZ\ZEBKUp҂`EŶZYJ1 k !deB?q: 3x=sw'Z7=z_CYYYòPsE,Y7Q-EGGo7|;KARӦMe8qBï<쳒#FTfٴbŊ`Y$p8c5j_Hď?ݻwK.R/GX}x\IIV^mx;$5O?~)&&F!!!u]zKvݥ~͛7w}nAݻwWbb&OtOPu֩q?Q붟~ڵk3gaÆ7o֯_^{Mݻ+--ƾve.oݺU'|TرC'OTnݴzZ "RXX?Ϻ馛tiժ_|nM9990aV^ѣG_~?~6lؠCj߾}4h~j;~ &TgW^05Õ'|1pFOjҤ:to]7nԝwީ:7k,eeeI~_aÆ֋7,Io\ee_}u:v(Ik}2g$ȑ#k.9rDv]5{lEDDԺlo3sO'Md.U֋6۵kWi&MHJJJyfP oiܸ7n얾-[&.IKTm+\޴i~gjժB-[U%CBB匌Z w@_~|eXI&JHHtY?+׳gOsa.+ W$TsC.K-[TZkQ@ WXXX} `zp(--,&oMKIIN\\(Iڽ{wu>,IիڶmҾT0@=wA]9::\>tPx Ig}VkJf̘~T0շo_'00!(ѣN刈ڕJtM뮻;8i,I+W͛u}[ne v T6m*;]jb.;T9}75h@\s~ajJ[nբE'0ܑ$^e`;ڻw6mڤ#G_~3Ԍ0@=WXXTp)a8>G*;vTǎ]@ `z*..v*#;WRRm۶^z)(!ujwYrÆ  op8tܣG:;s挹lᮐS@s[v*;egg.?;p08qºwKRbb -[*88|onnK&;t `_]c0.B$uK.JII$eeeȑ#2 `7p>tW{쩱ٳgw^䑸 `k5SSSk{n뮻#q)IVmUV˝:uRϞ==א4hѣG?{,.C\AAA_~YOW_}e>[n'xBYYYz衇pB:NүkIRdd>C3i x7.`Ӵi 7oΞ=;wn+33S{'NuOݻw+TBB"##f͚U?.0SOiѢE<8_TZZZh+Vsp0BBBt뭷z; r=޽{W[@sܹsu3gԬYlOhԨQ;w֬YKv]M4Q.]4uTM2E:nBO%&&0 ݾ}{͙3#}op`L 80T\\leټEGGĉòT0㊋|r|-($$ċX[@@bcc ,0X ``$"HE  A,0XDX_``vTGq3,0-l6[uax!r8U9C\\\Hº>{x0JKKgܩS'z1" 6M!!!:{.??_^ PN吐$w]z; `۵{nj߾3.gXc8d||_X58|oƳ͗>U_3:._ժ'p,0X ``A'NԠA_}п>+jʯ/X;/Z8jϪ/x[}~ܪ=ft\U3OWa?()),ܹS]vbDj16 `g`$"HE  j̙Ne16 `gg3 v `$"HE  A,0X ``$"HEv@UJJJ+55Ujڴ:u꤫J6iiizgteGSv]6mݻxuMzrsWǩ|_^iii*,,T˖-ճgOuܹ}O}N:u*..V֭u+11}*)--/-[ꫯּyqF=쳺kԡC-Za4M4ICO?VN`>}}9sF3fPf4x`-\P||I[={_|VTvvNXvmJNNW_}zH]tрֱK}:KM6/K}/4ydkNÇ׏?X>}ӧO4$ڵkmØ?lH2^{pYZZ1i$#((Ȑd̜9!?O?w}OmcFRR!h׮}vs[qq f̚5G*_RRRVZ}iii|4$!!!ƻ[xSk֬14ibH2jddd233oݐdDEE+Wt}_Yf]l3P ݮnA7nf?O 8nԣG5kL+WԶmTXX{1jIOOc={W۶m$ 4H np;vLӦM|֭[+##CN2;Fffﯽ{*<<\6lP׮]kT\\ 6믿Vll.rw"|}ڷocǎyڰa!!!:tۧ۷?W޽աCڿSǩM6iȐ!Snݴzj]r%pzZn~G-_\C Q||K/c hG}Լm̘1߿&O=zHfϞ+V\رcկ~_Wjܸ%.giٚ;wn0a$I>`4͛KymݺX/S3f233%Iݗ^zI 4НwީC8S;vΜ9#IzՠA ꫯf̙3[_ ۷owy衇kiӦģw~8u֊3m۶S?+VЪU$x)SHJJJ4mڴ:`}{b(d IDATH:9sTY_O,0$I͛7וW^Ym#G*((Hҹi}u2P]>W\-ZT[ڵkf͚:P8UZZ_|,>\.pB۷ϥTvv͛gGUm={N>]c_F^˗fUۦq޽Y^hpek>zFXc$EGGe?q/ѣGkg9 eJNNnUe;}w***2˽zr]z6lpʬ_ީGϞ=˗;].gTTڵkg?CXSus OC^:;uR.]C|[`=QJJ[@8>٣'O`]2NqjڴiCfyÆ K `x]svEzn 5qe ͛ 6TTTT䏟0N?S۷W@@@ҧ?" KJJr*=zԥvG(/8Uv.߱(ƩPo߾1Aj۶m} `xu]T޷oKֳlq}5.$όSsj޼K/T>O<7 @">}I&fy۶m.ۼyܻwoq;]c r{lxb馛ҧnΝ;]`m2N0 +;;> `x]``~_o6Ne@Ubbb4bGzzT`\<T=teժoV%%%U e6lbcck P360|¯~+l6IRZZo^m>̜>**J&Mx0׭[j'''۷wql+VpyM}^{޽{qBBB4ydW_>Ǐ@G$:u,73f(22cMNr*#њ ^P%I ,pŋkƍ귿E@@u/_TodKmyEGGK^z%8pzgVzzsӊ=_`Iz)5}tH|Iϥ%\w}L8UQQQzCoTZZ*IzWզMJ+C=jːd֭saaa$cȐ!ӧ)ژ1c!ꪫ )))1RSS/˜:uC1j(cٲeƶmیj۽{ѦMCq饗)))R^3l6!ɘ0aQTTC16nh~?nXOε;XH}>#<<ܐd10&L0$AAAƛoYcf.󁋤@5w\.SVw^㏊c=iӦ)00Diivڥ#G?ܹsͿ:oԨQ;֭[I&^Z=饗^c=VcӧOעET\\>}iӦڱc<͜9QT_ƩhڴiO>}(**J۶mSFF:u^r}|_}RSS裏j͚5P߾}-[(;;[}՜9sԿu$ a޽[jԨt5(88'Lxӧ~z߿_gΜQƍճgO]q<u222aXӧzV/qF:tHC͛7וW^;ֹO_E,?c  A,0X ``$"HE  A,0X ``$"HE  A,0X ``$"HE  A?w lR˖-v(/# [d>Ro2c233!џEyP&K,QddFQv)?'|BIRf4i$_P@={y\l;}tM4I 4$!ݮǏkڵz饗o O??~_qN@A|Lrr$n￯ VY7//\׌3*M^z߿.r :TԩS'7GoXd$iܸqUf dffyfUYD\r $I]w֬YS>:t蠣G*//cǎ)(ysN@C>w߭1b&$ꫯvim۶UÆ IY@jj~&%)o5vXhѢƺ6mr*3 z-bŒWYs ڥ)w%iƍr@@R'N誫SahҥjժUZ)|'|xߥeLJJRF\j?slڴIwQ˜SPoݥ:tH2ˮN՛SNcǎuaɒ%qUYs #t]wTj-((PΝI}GܹzQe=)x0m۶\Z%דu ڵkWYfN8SV[s ?Tnx%&&z/\t/Tϵ9@LAARRR̲wjv/_}}ns 0^%%%fd]g)77׭wrNu?Sg,Y;m}rNu?SYѣŔUViw[S`A:0ߚ+BAAֻs8r曽-[Lv]Ǐw[+:#vӧͲU]~Zuax)߱xbhԨQn볾SP_04SJM 4Hv]߽W_}!Cqn뷾SP_?qFs9 @W^yP3adjܸqn>SP?Rnͮ]QF^x,YDnr};H8yg8p[9Rns 0 Z-YD4~x9\Z'>U].//O[nթSl{jԨnݪoVv]^z TΝ;G'OTBBn&ƺ۵uVeffy0`]n_cǴj*eddUV6l4i,ZJGш#ԮH_~ݫ]ve0`t /Vtt\UsX))):y3uTÇբE 6L111nݕ;vL-[e-((ʕ+uAhȐ!jѢE}}w({fզMٳG'Nѣ5ydM6M5tP{R``MD-\~8AgUAAZj+!!A>#==]_FLnZz4l0eggt<d̟f͚inOu릌 kN7nT֭ujذa:zVXC:KNNVvxb0 M8Q]v~;wjΝ=zkݾ:rN;vL аa4a=c:s/^'̙3ٳUV9?pv믿6={VIIIjԨ}%\^ZҺu$Iu-ɓ… նm[͟?1^fܹJHHp:O###ެsQEEEۃtwWTTd IѣoѵkWc׮]Nsssݻ_~Bƿo!ɸ;Ν;Mڵ3N:m„ $cԩڝgyƐdp ͛?oH2͛~ر$/0 0>74l61sLvǏ7⌸8ɓU2ӧO7$_uĝT@@!p8}hBÇ$cڵ$cuf͌͛7zqY#""PرcUㅼoϟo?|pN25kfHOO4p `6m2HW^y}/t-[ IFppⴭA$cNKKK-[7V賨L\ #FToYFn݌Yf9%ӧͶYYYN+BT#88؈1Μ9Sac=ViUiӦPq֕;ϩxCѯ_?nWަMCW_UGe `8̯*\o߾u]gH2,XP1^pm۶5$K.}ǎ;wVpS@(?}UW]u}k׮{V~T\\g}i۽ޫ(u]Nԯ_?Iw}WϥK*55U111L^{M7xz 7ܠSj̙l֫k6l0nҤӶmJ:7u^^Ӷ~Z?~*{?N;vlYW>^]vqP oSN@%%%*,,Tvv֯__~٩^rrոqc5h@AAAjԨǍ7ިN+VPaa%KԾk9_'77¶>@ԧOV~w1Ci2eLRmݺƚ/I&'v ٳgZr$sΕ}*//O/I/$?ƺUTϞ=+]"I5>wBմ?74i$͜9Sk֬Qzz{=M< Q\\֭[ ̙3GsqZ7n83ax:v(I***ҁԥK999OjǎÇoǎVZ9{oԡCb =#.[X$I'OTIIw霑!tI^x͛7Ҿ ^cTرzUqVbSeP=_u͛kZb׿g$^u` h˓auqWW4h`.yyyo~8=sڵϟT|Uw ְa+!!A֭o]mƚkVC[nu_JyK/۷Wsq=zT5W_} 7ƺUs>}s޽+IZ`JKK%zȑqu >]߿ۧ{Go9unMZh4:uN񄆆꣏>R^C'Ј#P*>{z衇|r5kL_x ~6mSw6YW,YD.(MYC QBB>/B#F{W3@mp0M6Iw﮸8I̙3o>uEͫ1gϒAIRSSmS]԰aCs!Chĉ}Wi U.\ŋkذa=zw{No|9ݻK_\Iiٲeݻ:tPc}+Xeo ԤI${]p8iH@=U\\\l\Rׯ_/I{^$ҥKW_IL"ͦ;v觟~tꪫW^yEZj> /$VIII9s6nܨ;vh?~|1>cTZ0 p NSFWfժUɩ7wIGmYYYJOO!U?33;SH@=x@999$ݮ3f7ԭުdl6̙3dm6;Vo$i֭Mܯ_?K/)Shɒ%z衇/\wղeO;xϼ<;vi,~NS+_hGմiSuI:uRRR1c^p2Zl6u-Ї~h&Ϟ=_]V2x^^^>s_-Z/6_ v\䪦~:s挤sI>@asΒ9s֮][WT6jɓ%{sDDDڰax;s>9kwQaavڶm_WikϞ=z嗵fiӦj߾f͚:^ /]ѣ+Kovܩ5h@v}QJl٢;S攻JLLŋէOW[l1 W_5k\p?*_PkCl>S͝;W[">IDATRpp5fM:)Zp&L~[SL7=zT_wo8QFw;Ug#YYY4qD=Nۂh[ PϞ=rJ%OSPJJ"%&&j;մiSo rf…zWT\\=z_<0XS@E  A,0X ``$ڳAJ#`BL`  0!&0`BL`  0!&0`BL`  0!&0`BL`  0!&0`BL`  0!&0`BL`  0!&0`BL`  0!&0`BL`  0!&0`BL`  0!&0`BL`  0!&0`BL`  0!&0`BL`  0!&0D1 &IENDB`unyt-3.0.4/paper/benchmark_plot.py000066400000000000000000000140511476461141700172020ustar00rootroot00000000000000from collections import OrderedDict import numpy as np import perf from matplotlib import pyplot as plt from matplotlib.patches import Patch ALPHA_MAP = {"small": 0.3333, "medium": 0.666, "big": 1.0} COLOR_MAP = {"unyt": "C0", "astropy": "C1", "pint": "C2"} SIZE_LABELS = {"small": "3", "medium": "$10^3$", "big": "$10^6$"} def make_plot(benchmark_name, benchmarks, fig_filename): plt.style.use("tableau-colorblind10") plt.rc("font", family="stixgeneral") plt.rc("mathtext", fontset="cm") fig, ax = plt.subplots() ratios = OrderedDict() stddevs = OrderedDict() width = 0.1 packages = ["pint", "astropy", "unyt"] package_offsets = [-width, 0, width] size_offsets = [-width * 3, 0, width * 3] static_offset = 0 sizes = ["big", "medium", "small"] yticks = [] all_yticks = [] for ind, benchmark in enumerate(benchmarks): if "create" not in benchmark: benchmark = "array_" + benchmark for size, size_offset in zip(sizes, size_offsets): np_fname = "../benchmarks/numpy_{}_{}.json".format(size, benchmark) with open(np_fname, "r") as f: np_bench = perf.Benchmark.load(f) np_mean = np_bench.mean() np_stddev = np_bench.stdev() for package, package_offset in zip(packages, package_offsets): fname = "../benchmarks/{}_{}_{}.json".format(package, size, benchmark) with open(fname, "r") as f: pbench = perf.Benchmark.load(f) mean = pbench.mean() stddev = pbench.stdev() ratios[package] = mean / np_mean stddevs[package] = ratios[package] * np.sqrt( (np_stddev / np_mean) ** 2 + (stddev / mean) ** 2 ) ytick = ind + 1 + size_offset + package_offset + static_offset ax.barh( ytick, ratios[package], width, xerr=stddevs[package], color=COLOR_MAP[package], label=" ".join([package, SIZE_LABELS[size]]), alpha=ALPHA_MAP[size], ) all_yticks.append(ytick) if package == "astropy" and size == "medium": yticks.append(ytick) static_offset += 0.01 legend_patches = [Patch(color=c) for c in COLOR_MAP.values()] leg = ax.legend(legend_patches, COLOR_MAP.keys(), loc=1) size_patches = [Patch(color="k", alpha=a) for a in ALPHA_MAP.values()] size_labels = [SIZE_LABELS[l] for l in ALPHA_MAP.keys()] ax.legend(size_patches, size_labels, loc=4, title="Number of elements") ax.add_artist(leg) ax.set_xlabel(r"$T_{\rm package} / T_{\rm numpy}$") ax.set_xscale("symlog", linthresh=1) ax.set_xlim(0, 1000) ax.set_yticks(yticks) ax.set_xticks( [ 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 2, 3, 4, 5, 6, 7, 8, 9, 20, 30, 40, 50, 60, 70, 80, 90, 200, 300, 400, 500, 600, 700, 800, 900, ], minor=True, ) ax.set_xticks([0, 1, 10, 100, 1000]) ax.set_xticklabels(["0", "1", "10", "100", "1000"]) ax.set_yticklabels(benchmarks.values()) ax.plot([1, 1], [0, len(benchmarks) + 1], "--", color="k", lw=0.75, alpha=0.5) spacing = len(benchmarks) // 2 + 1 ax.set_ylim(all_yticks[0] - 0.05 * spacing, all_yticks[-1] + 0.05 * spacing) fig.suptitle(benchmark_name) plt.tight_layout(rect=[0, 0.03, 1, 0.95]) plt.savefig(fig_filename, dpi=300) plt.close(fig) make_plot( benchmark_name="Apply units to data", benchmarks={"list_create": "List", "array_create": "Array"}, fig_filename="apply.png", ) make_plot( benchmark_name="Unary operations", benchmarks={"sqrt": r"$\mathtt{data**0.5}$", "square": r"$\mathtt{data**2}$"}, fig_filename="unary.png", ) make_plot( benchmark_name="Binary operations, different units", benchmarks={ "kgg_operator.add12": r"$\mathtt{a + b}$", "kgg_operator.sub12": r"$\mathtt{a - b}$", "kgg_operator.mul12": r"$\mathtt{a * b}$", "kgg_operator.truediv12": r"$\mathtt{a / b}$", "kgg_operator.eq12": r"$\mathtt{a == b}$", }, fig_filename="binary_different_units.png", ) make_plot( benchmark_name="Binary operations, same units", benchmarks={ "gg_operator.add12": r"$\mathtt{a + b}$", "gg_operator.sub12": r"$\mathtt{a - b}$", "gg_operator.mul12": r"$\mathtt{a * b}$", "gg_operator.truediv12": r"$\mathtt{a / b}$", "gg_operator.eq12": r"$\mathtt{a == b}$", }, fig_filename="binary_same_units.png", ) make_plot( benchmark_name="NumPy ufunc", benchmarks={ "kgg_np.add12": r"$\mathtt{np.add(a, b)}$", "kgg_np.subtract12": r"$\mathtt{np.subtract(a, b)}$", "kgg_np.multiply12": r"$\mathtt{np.multiply(a, b)}$", "kgg_np.true_divide12": r"$\mathtt{np.divide(a, b)}$", "kgg_np.equal12": r"$\mathtt{np.equal(a, b)}$", "npsqrt": r"$\mathtt{np.sqrt(data)}$", "npsquare": r"$\mathtt{np.power(data, 2)}$", }, fig_filename="ufunc.png", ) make_plot( benchmark_name="In-place ufunc", benchmarks={ "kgg_np.add12out": r"$\mathtt{np.add(a, b, out=out)}$", "kgg_np.subtract12out": r"$\mathtt{np.subtract(a, b, out=out)}$", "kgg_np.multiply12out": r"$\mathtt{np.multiply(a, b, out=out)}$", "kgg_np.true_divide12out": r"$\mathtt{np.divide(a, b, out=out)}$", "kgg_np.equal12out": r"$\mathtt{np.equal(a, b, out=out)}$", "npsqrtout": r"$\mathtt{np.sqrt(data, out=out)}$", "npsquareout": r"$\mathtt{np.power(data, 2, out=out)}$", }, fig_filename="ufuncout.png", ) unyt-3.0.4/paper/binary_different_units.png000066400000000000000000002365401476461141700211130ustar00rootroot00000000000000PNG  IHDR^sBIT|d pHYs.#.#x?v9tEXtSoftwarematplotlib version 2.2.2, http://matplotlib.org/ IDATxy|LgOI%֠BDTR;v[j*TQZp[XڻTR-ZmkT"؉؂$d~|sLfML_cusNƼuRJ y08 `p$I 'A N08 `p$I 'A N08 `p$I 'A N08 `ps)))2rH)SԨQC;GӧO *#<"-rt9c3fL<9ׯ5kJҥ%$$Ds?~4jH*U$Rre ?Pnݺeu$Yv[G%00Pz%?(r\.]$;w%JHȑ#. NE"ڡCA9ZKDRjUUZ sNiժ*B^zo?˖-[urJ˝={V$%%E;s玌3FVZ%۷]ٳg/{i*T ۶mڵkػw 2DΝ;'!!!Rzu__յ{dy(z)ߵ@9u+3swt???%7o;wشlRR$%%͛7ܹs&""ԭ[W&M$={SRR$22x""#~[-_ۄ=gݻwKnԞ߿kN߿/""׮]3g7|#""Bڵk'IIIwI^DDdرG,X_N:U'_LO7oJٲeT<ʖ-+L6MV^-ǎ2e)RD֮]+!;w͛7˲edĈkqryjjԨaO>^z!IIIRZ5-H6m85 T\Yzi߰aCݻ4nX7n,ZΝ;KHH,^XΟ?/?떋&MuI&ɠAdɒ ruIis]xMm,ƍ2eHʕ?֭[so!u֕Aٵl͚5eŊRjUaÆɤIl^>::Z,YM;!_狿<3_`\rnnnK=_N*m۶FѣG]B|riӦx{{Kfdݺuv [q`'FXiFoߞ7oޔ:_߰aCٷosaӧOҥӊ+䥗^Ҧpn?^ϟMgdd˧Kʕڵk""2h YjU׉Yx8p@/_RڦMduVG#Pɒ%slٲe%44T<<Ԍ3c=mfmrrڼyׯ*V2wVDDΝ;6umo+j}*UJk%KToJJJ[naÆ̙<<]{n'4+U _Ǘ_~ׯ}Ξ~ԩV^mS_j˖-_T֞Pniii>Suz{{+///mM >>Uԩ:|oD֚;wUVg}V 6lsl}djӦMu;vLSyyy)UT)>>>^S6OVoRDշo_]nnn~מ={SO=ݕaUӧOW)))V\:t}YZJUVyxx~b'N0\N?K֬YcvU1ٳW?222TXX5j5&666דa˗U޽ (%"zj޼yjРApN(/`s?vf*Ttt={KK~_~.3sLGlk?g[߮]TJE>Hݾ}[)Խ{Ԋ+tfǎս{u}n:իW/CY bcc󕟟6jҤIj…_ϲeԐ!CtED5jHmܸ1-RJDT۶m۵ƍj…\r ޳_ttTft<322Ԕ)SED 0m۶JD/ 8ڵkw*55l_j„ G1x۷d>LC˗/-ZhϏ1B;wN) ?TPP|ժUӧֵuVdd-| 4H{.[ʕ+O>QZPUFF:u*RիW/uRҥKj֬YZ""xV_տoă>s>h R VOKKS?ypr~"<<\ۆDDUVͦ ڵK 炂ThhvJLLTk֬QS=6u j͚5G2QQQmlرt;|dڰan~u7o{"Eڵk-[hnjX5c -$UF uQ\~]-]TjpR߅֭[|ICjE={hϝ?^꫺>5jddw%K~ZLk{}[{L:k?~]ÚSN{Ool}|7j֬Y7w\\?ݫƎkaΥKK.5k46l0ҨQ#m6zp;Ʌ8_aݻ[]fݺuI& O>]}f0?THHUn];凞+W3g M?35:tH<-_l]ѣ m<ׯZj3]tհaC]8qRAQreV_OVݺuS"Ffr^j2dcbb?{ /ݻwU:uT``nuGDD/_^ 0@M|UT)md΍7toܸŋ07ǒ~]v1\RԃP>;Qw\-XussS2 *]jt4u,Uʾѣ***J Tm]6ӈ#O=T=j%[j\]]Ռ3 xRRJ*TPiiiבJ{{{[N_J(Q(gj!NŊK,9w}7ƪ;w'NuiW7stU_%''FwE&61ydZh H]XG_fWD u]v0wԨQ7fVRq2""wܸqWիW۽DDDv GAvroZÇ[my0!pBMSNQQQ*""B[NM2EըQ_]VPϖG^G[lon޽{Zߦ!vhhŶ@K]V.--M^Z}G:߿ozeqY9rD r޽{uKpuuU;v>}oE\{V1{lgߤIm-ZdkHNN6uԱW^1|]t13W{o֬nYfY\w}k;rHŞTem]ִ?Ӧ̜9ٟ ˗p[nٶ7i+bJ {1P:uꔮ]\\ZhZrH-5w S:uҭH"CÇjWՐyذaMTuV3g+?2;Y,;9 N0aնbŊNU~Y$M4={{'""ҹsgٿү_?qqqk]Ŋ]cuӳgϖ-[Zlf&%%RJ7/&&beʔM_rb[WWWCmJ޽uܤo߾2f)[9///yt6l /^L|󍈈SmoiӦIzz*UJvjrzi[oYl_ti-ZI&IӦMu"""$22R7/55Uf͚%""پú=*ǎ3۾\r鴴4Ytay'dĉҬY3j̭jժV)|aÆ6-;qDݱe1_={ZlߢE ͳtvׯɓ'>dիW˶mDqw6-רQ#e6QX̛7On߾-""}ɶ}6m BCCͶ5EDƍ'y>>>2j(3i-UT۽NK6oެU]˛~_rUN:F"Ya???tRs#lܧL"V &Hxx]aa'"i&tn4s]W&[AAA~'8po֭ySdDDDi[G]Sl߾]qQJe{xxFؾؾOիVD C?ywIֶ} kN7=h Yz[3fsU(hSV./}'Oõˊ<AҸqcٰaM 騝 meҤIV}^Z֬Yom$`^;v,ZH4۷e2zh3߿.iI_8:2;UT___m߾ꥠ.;voVw ]ݻwk^bnD=2Tٸq,ZH7v[iР۷>[2qD;v6믿0ϓzoY*) 痣~cZ[TTTܨakOaԩS'iРvK{Ie͚5pBwl(yG_C?mݻw瞓ݻwKݺuXaŋڵkVZ=@󃟟OVZeԨQ7##C?e^^YDAg=x ٲe6}ܹܖePn]]ƍremo߾ү_ IDATjժ9/2.\ ˖-/BmL#9sF7m-5kԅ^Yw/JϞ=K.pB^Cj3ezoO{wgw!˗˗稟uٳsO^E+ wj7n([nS>0pwwKʎ;ŋx5jرÁ坌 Y~̛7O//IƍeƌsN5a/?J=tv!W\ Jڵ{Gʕuݳڵkא""">K&MdٲeҵkWٹs[ܾ}p)n+YVl*tp۴i2e8qfwېGVTBUC:udƍsF߿_|MꫯdņEyp`mvTiXX9rQի'/+VL9"k׮ƍ;4StE7?4˼s^10("[,YR71/fQYG< lavҥ4mT"##eÆ g޽ᾤ{^mVŋ_UFix[oOٳGqsQQQҾ}{y饗rt@G ,7e}6ҫW/޽;vL{=ٺuk-{T=Ǘ5tҺ阘0 g˖-)VnL2ڿM/QQJ;# 4͛7KϞ=寿2Pn'"w[_~0\DOŋP#>ݻw%%%Łed2uT4::Zڴi#ϟw@u/H"fa.׿pf͚6d: 844T\"""|EDׯz#LG(6j(u2زeK*T=wСBw˷~[,]]]eɒ%rϖn+vj[ĶS2qD9}7xCܹSu, 30ݻ>kmjO }Kzt]vMM`իWί\rW{m6mW^>h{$((HNMM eÆ #=\]vy]}9s&>stcu.]ZWM'%%ڵksk"-t钼tӦMu>,L?ׄٻw]}ĶHǎ->_\9YlOK޽{WBCC Do߾НH'Z*GܺuKw.yQ]mOηzW[l)2tPիW0,ڵkM4)Jr/M[mo"'NOoy//t~kWG/z_zIu̙#IIIv… 2bSN 5cʕP)ɼ z Vݻ1&76ya%=&K*6\}wٺ{_rE.\huLY{HdLjLK,ɶ+WN~GͶ3gT^]~B5 \"C*)dպy.\v}99{dodڇ-˻+w)իM%Kdʔ)2l0}Eoeʔ}ܹSBBBl 'O,˖-SJJl-;'_Dr[lbTvߋ =3݇DߏLh鼸87~~~ڿ]]] /"pB.Gj{IHH+$$0ҜDyn-#rn߾naZZa=~`1޷xÆ eCXҥ\~]7ݺffFF$+"%K8p`oa~jjŋtRyp믾J|}}m^mr ={< !Em)dС2j(!jXX4o\n*oᒭLhGFn׮]~.99YFe|ɓ'u۝6XfM tYrr_+:ϟ?nmԨ̙3G7޳xL׋HѢE%44T-j-#2m۶!7nhٳer9INNÇK߾}-LϗbŊ}ҧO#%$$Dvءk.Yp;vL.]d}wmw ԇO0Aul"͚5]&66V &#GG}T>M?{lU/_.:tΞ=+=z9sH֭efۙ'"+UkԩS>Ç[]ovJ*yӦM#Gڞ>}Z:t nݒ\\xxLH|=Ynnw}']v5,og[ӱcǴ?RlYQp*IIIZjJDtW_ 2?46m5|W>ZjRSS-.si2cǎp2ٶ#F0uuuUC QK,Q|:t*ZjժZrՂ Tնm۴KCGyDݸq,;cƌ<;;}rqq\bm||j޼ݬX"۴icx̶OLLT:tP":&NhX(թS'5zh5e/ ޽{t}Ĩʕ+\fM(ڵkԡC5|駟?bŊbŊjΝ۷W/VݺuS3fg7jH-]T}Gvڪ^zzjԨ 5n8r-ZPw1ի* @V2/^4]mX:y~>}6 0+VLݺujXrn|*UJ +V)Sի+www[oMDTrÇu눋Se˖յsssSRRRTƍ 6lͯ?4[gxxM۷O/^l?zՔ)SԘ1cTT"Eʕ+(}߆tl~m6lPfkk֬ QozWUppruuU"j׮bcc-۰aZf͚eXᄈԩS+VT|TNT%ԉ'l~?,y *Z>|O̙3UΝT:}jٲa9rOԅ tL/a7x2^%o/_)´eFӧ^ׯ_W~~~ 0HNNV/_V7nTڵ~f͚_-[VQ榺vjӏWVݻVZjҤIjʕPƍ~3sΩ&M)Yڲe!p!0xUBBU^^^fԯ__;wN͘1C5kb_˗W|nݚȑ#Z+'֬Y||||}}{)))TkV"*U6mdSߦp5 T+VPqqq:6oެׯ4x A8qwҥwުXb}gՌ3… -֓Zhm- m:?>>^UPbժUSΝSJ)qFAY ˪Tƌ>sou-X@Pu}RS_R}~v jذafիW ݮ_tbX]}׆e"J.,X^9y8p_6{RRjO?Y}Zrv[PVR۶m3']vUJIIQJ),XFDŋAEhԃcՂ 믿j֬i qƩ+ UgVWfT?>}OԵk,?vhڴod{n믫ի[\6((H;V}֭[gXbѩS'uM=zTըQð\ݺu-;w4ԫWO?~:wlNOOlOUrrMG}T;V tjSW|z뭷T۶m-ݦM5k,ز ]͛Y%''+o飏>͛7s@B 𐋌ckoooUR%ՠA5tPxb]:uص0M6G@e>ʖ-ym]׷Zg>ʕ+֮]Xg uΝkh׫W/uMf^s!ҥKsWN\vM5*UJ]m޼Y 1\\\Tҥy̍=Z#vڥ|IAٳ؇hܼyS=sfSn X #63رC)SwuZ(RgϞU Ѕ...G{Ԃ ђX<#QdI+ht(Y?P[4(Q*Q6]JlGǫru=N8wnxڲeKuڵk>@3wXdRJ ڵQFݨQ#* 9r*Y>ԩ-[fq+bWM\A'(v޼ysjsΙ ~MUVU?RJÇUfg֭kh_^l?{ǫ!C_X15aU ZlUզMYׯ۵4kkOHH0{ ipuuUeʔѝ,ұcGuر<Pظ(e '!;wJJJTRE4h k6~޽{nvc=V?%=xzzիW!u<={$::Z^*E___Sᾋh۶ӧO3fHZZڵK>,R|y֭[g^CLLKLL$$$J [.G0___U3gd˖-$ 6֭[žoݺ%7nK.I2e]vRV|9$ ˵kפxRbEiРެRJܹsrIrKFF.]Zի'nWiiiҹsgeΝҪU|?..No..]DyGI&x\rE5k/5f)))&gϞׯKѢERJҴiS phmcpܺuKJ*ҢE = _رc)M6`Ǥܸ|reqssG}Tڷo/%Jе FwQ222d֭r!qqqK2r 9yDGGˍ7.+V֭[K5 U sĉ_PG,iݺ9rDbcctrΪUDDdذa@N>|Xeȑ|E raaarQ ;:vH@@L2}7-PR2i$;vlW@ytM._,;wt{ /Fܹse޽R|yy];۷O4i"ׯ_;wJڵ]p >|(QB*W,}m۶Ibb,\P&O,""3gΔE:NMMuP%@ߤwr!_@qQJ)GSJ>}iҤݻW\]@֦,+Wt\A1 xHZ}L2vZ wޭ ED6o,qqq`Μ9#O<M6?Oll޽{eݺudhSbE֭ԨQC*T Cu@`"""7ߔHJJԬYS^x3f/^]z)իW"A N9o$@$I 'A N08 `p$I 'A N08 `p$I 'A N08 `p$I 'A N08 `p$I 'A N08 `p$I 'A N08 `p$I;aqmٱc6'ᒜ,/^Ԧ۴i#>>>6ڱce8~Aw2 'A NK@6Mjd'--M=MשSGti-7M6MJ:uT g#@[ IDAT$%%={+2_{\08 `p$I 'A N08 wG@~_]7 #8%qt(. N)$##CR.8`aRSSΝ;rIMM% ...RH)U*UJ)蒐C,K,ѦG!񒓓%66V] BF)%)))ru~x{{/>rK.2X$Ӧ<'^+8܋ 9JMM .HZZKHLL .?2.tx"/ŋ%==ѥHrrn,YRJ,)EWWƌT*wޕwJFF\rrH*UX!A KOOx<VxC|ryIIIўtqsss`u...G ,www|c /`{|5932uH$QI9$%9oj*()%F:IH,gjad(f]?\}ny?y7u._'( |||6lR_XBW<_u!!!|^@@II&n??h40W>\WnF.KncŊ3) r\FX0iBMf?+20.'`-n5VG`I^>j~cX `%L_;ԫOI0 `%Vr!!U) u8.8bv K+[.1X `%9uFtC5yxz9q&E~lv KKf`!4Wux 8r ``4"V}v=p^| '>!1W||/ / 8P6-_te˖Ll6͝;7kڵKZh &4HjRbTR%=:p@׹馛fSV2g߾}Yfu8jѢE~6TR90߆,C!f$Uk?rr9 ѣGرcO?e9oԖ-[~;V۶mtݻjڳg1ޮ];9N7N/RSSsGܹsj*^5?s͚5Kcǎu-[TTTTTjUmܸQSNՒ%K3fK.т rԧOm޼YԴiS}5<== `"rʪ\<ݮ aÆ*QL祥GWTT1=zhݺu0aZjt͟?X{Q 8P111n׬Pta;x.]oY]tђ%K#hYNI1,?0a h"ojժeynݺjܸ}YӦMɓ'oij֬n͘1C3}:x`=z۪+V\5U$](Jha6k&;[#{{{L2FݤI\mG}8v\ڰaC׬Vuf_vܙeáe˖QFjРU3VB`IT+"򖷷q\zuRuuZ,IF2].MUVɓ۷ov#A`INK t e3gd9QFj߾Q:~xs-Z$///=C(D;+듵g}(bN?3$;wNfҘ1c9sF+Vw߭< T4c!WSHH ;CM6͛%I3fШQctYF(4yҢE/ZHKVǎ;P @ѩS'ծ]ۨNj?Ճ>(nF}u fzF,^f d:f+wQ*D&&¡Cf'PU\"x:s$i޽Y۾}[r4ו 0@ƍSRR\.w.'NȓX Pկ_8ϴ so߮Ç믿2^vW ={֭ICD0`Q'&&;P*U`xϞ=nMഴ4-^8Y0l6ʗ/o.~ϟmݦyi˖-Zf ͛kZ1w֬YZtmfرcF},wr 4HWVշo_kraPб4X+@\Nf62zr `K0;//gv WJ"X ,Oo#h,ۮ{{v%,€0`e:A@3"h9v͎:4rb\.ّdSZZ"""`vKr9+fQSvȮ1chݺur:fG VO5qDc `~Aln޽{ 7nTϞ=ypb͚5:vXkΜ9 ߶S?V̎ `341l<UV{:{QٳgrfGA `'|R Rս{ws|riz衇TfM/^\v] |9sF&M-ܢRJ#Cu{k:nժUs{ߧ~~۷o3<={c6mRvTdIp Z`Ayl٢ѣGoVPPvʕ+ƍkĈڲeK>GUPPK˗/WDDV*___[n3V.]2|^2k,e:_jvX `7ŋxb۷O͚5Sxxvء_~Y.]P-_\]tQDDG?հaC5J5kTttl٢+W*"""{S;v+6_jΝƫm۶?C'NT&MT^=M8Q hjJk֬QRR~t颰0}z'kڵ2d<ɓ'+,,L={TRRRܛ6mUJjJs1:tHm۶Uxx-[(%%EҥKզM 4(CC}ٚ>}zSCCCb Y&C///O4h1/jY~.(>[oU*Tu) @ԸqcuQ[w}O>Dcƌĉ3\Gڿy}\L ԡC=sd.UF }Zj<_~ܺuc?]3gM={}WΝdyy6kժ{1s=_pB//lKHHPUZ5-]hjJAAA;w֭d}駚1c$IӧO&Oltzꩧxu}e.jҤ\.|Mu]r.EԩS_ITb4/l6lQϜ9Sϟw 6H3رcUN9s(66V7Ə;{L_}>CӧOL2SNcǎWpp͛B U%IvRNܝ;wСC5w\.^X/6lؠ}ꦛnR֭5m4ǘ;uT[.RJ~WZeE `"n޽{3SV-̙3ڿ;w'O^^^-7hرCQQQ $(QBSRRfΜi̋Ҟ={$IO<~V:o޼Yzy7p[ݷo_/kiF/Q\.7.üۧ,JҢET|ys=Wkl^ըQCʕnW@@BCC#hŊWGbb^z%5o\eʔT=zdt+>19W||o7/6P/WxqԩSnqkٳ^K.֨n+f.]ZݻwǏ6v]= X&Mnu*Ux!C\rFzj?~}M\bboߞ׾}r򇗧MV4^^W~@QfԵkW=cV޽[V KYdI7mڤkĈJJJʻ9~zggiӦng_-ZgF![zY^dիƍ_w6n\|_Vooo}_^QQQZvΟ?=z~SjղuzJm۶)]v{W2^T\9޽C,55Ufȑ#NÇ7 s=fΜc^u\^j]1B)--MK.լYt 0@j޼7x`%$$MMMɓ5|?^W]/ٳj_911\~˷Ծf͚ҥK%I ,+ⶐnÆ פI; ?K6_~e>|X z3۲eK^ZڵڵkuY[nݮ v\U2Jn:{{{kРA^~9N͙3G={m@okĉԄ ԥKyxx(>>>[xWU^= 8X{QOf;C "knuRRRJrzqN\~+xg[o)**8h"yzzf{nX%ZKAYO˹M4ѝwi3g4͙hkNAAAם %Wgriڻw߯>}Z1̙3&&,Ԥg;#IZvo)SOk׮9zϾ}~ԲeK%%%oŋ34(s*((HF^˖-$9_UVukj懪U[l,\r8$$亳\\&M\qѣvZI^`맯ZGU߾}; `өO?T}֮]+#\uNHHׯ8]wu t; %7 1s':L rT׮]ȑ#Gi޽zg?VllwSNriܸqnݺsr׭['|KHHp۴isYgvW߶m[_~$M:UӢETdI=ם `ҥK;wf͚-\.-ݥoek@,Nk/Ԯ]^zs\W IDAT_||Qtal6[~{D6mTB=zTg)99YYwi֦MFםeӦM?<3޽ W\e˖[nLK.>|uQI&?מ={tI}&&ENd3_]tرL8pv8[NGرJ,))M'Ndyk뫑#Gٳg5gΜoqTTuHKKҥK%Ie˖*dIpk>?#:y$?Í111:uQs=o>1b^ժUK%K'̖y{5+WWq(R._uaΙ3g /].,z饗йs$Izkn ꘘ=׽!Co7^z)˦$}7%IWz>K;:t$Vٲe]z駍:11Q5kT˖-~ k/Z8ׯbbbԴi\ɓ!͖@X2$Ysvܩ۷KS3fСCn:mڴI3gTXXm̛>}6lؠӧkŊܹsdz7|SCv]2 SbŌ_Ԓ%KuV=s7nyƍKEDD(999܄cumǎo^hٙ۸qF!IzWk^0ݻw3/Gǎ:?553h u]r:(Sv{W2JһᆱN:]qΥ{!iڴz?Pt:_$,YR۷ nݪ-Z(88X~~W㚑C=*Uȑ#ܹsUdI-\P5ʐOԤI$I'Oԃ>(IjܸV\$}Zp~kԨQ UUTĹ4p@}Zn6l~Z͚5Szz֭['*99Y/ᑽaaa2dy=WVX_]Ŋӌ3gz*VSͦ>}65/|ڵ k4hpM/!$}Olϙ3G/tak1cg<,oܹ ӻᆱ={O5jPxx{1Jf͚~X7oVٲe(+WέrpB-\P҅EժU5x`=W|+"ͦ>@NR5ԫW/ 4hϘ1#nݺU={$͞=[>;w y/s=gϪ\rU>}RJ9<^͟?_ƍ /D(QB5j /GyD*T5/u}iҤIrׁ5YܣGM>]҅- Og;}Ν7n,X`;wNiiiREFFO۷7Ǝ?kʔ)ؘ1c [+v*͎ ))):TCT5TbFE͛77;0C^q^fzK.r\םK^}LO>円 "rKڵ5o޼\3/nO \r-ZGٳr4~xM6M5RPPk.ڗڶm/eO=5-G%%%VUF 9rD۶ms{`{=ϛ|+Vvjv@kK믿.өsiÆ mԨ|}}p8tIҤIhOOOf֐!Ct9)..mƎg}֤PDF7;1; G}+dCSLQdd̙5kJOO7W馛6mܹs4iVX]Ota[bH{ԣ>{GSLQLLOyzzf͚j׮ jC*^.1}thfGAeE 4oqy~~~RTTT>$͙3Gs1ʕ+kʔ)y>ٳG&LPxxG{v /8ܹsu5khرQիW/%%%PZZ^}>0l6v1۷ownz]ի%I_Fm{WUV>0@uȑ cBBB> @!~m޼Yի5rH} T:urKa``ԓO>#G^wnX `"sٚr8 @t7ĉ].^{5k8իW/׍֐!CqFIM7ݤ+<<x 85v\m(t\& oyֲm65lP%K͎)4m۶)))ISNW_}ev$Ƞ \VH6 @:uBCCUD 2DwyiY9bڽ3xҬY3ܹUtt}]0ޝc\WWR^ԡC0~SHHiQϫk׮:~Q|G`M^R;k>}hƍfGLavC*^ߗIMMMKKkf&))I]vҥK~@Ao-[hذa رcoF}Q뫊+COz͸83Fk<áիW멧RժUu!IҲetbŊ*V4h)SdPNLLTXXXl͓f3^5J Pm߾]~>c۷ɓ'5`-YmѣQLL:w nwءh-YDv-[hZJIIIxZZPc0@tUTInǎwܡ8[ڰa{=*^1oٲez衇z͚ Zh2/nyOcT&(ŨGOib*Vʕ+glܦM=έWf͚|7nz6UWRbEzĉ0`@yKV.]oK6n()6."-TrŹݺuSz׮]]ӽ+7֭k9sFO4m6m$-R@@[}̙|/P@h­?Mt:M/P@ԩSǭ>uII\0rTRn[30K'ϻ$]؂nKBBBLL~^^-˛h,M34s\f,!))ɭnҤII\-#3vZhab ٴiqܳgO&#f4aBJ-Z$QFe8t:~,q|ĉ|7`6~3$/"(t8p111ڸq$iܸqSN9Ob}lg;{[Uc9888駟jO}O __|1*^³~aIҀ4|Lm6z֭Yȑ#nɓ'wZje;vL}Տ??\Z@~2;y!쑡q z7ŋ5p@Ck׮դIkn;~o^p;7ydu֪]$]X;| ,P6~Q-^8&Md3<6mN:%–Ջ-MN:]ۇXZHi  kM4Q\\ڶmɓ';=ܣKꩧҎ;24%i֬Yjڴ~a}z3f3&'N'fk˳}݋5k}@aG`ItiQv8kb" `)ucvCdfn< ,5Z]eRp8cϧFl2Ɂ.]pΝ31 `"ĉ:zQoݺ4 `"Z|:u$i޽[ӪUm6%''@N4@ԩS'ϙ{KVX/?4~[@E` h%yzze˖n5V!!!ph,Sf(4z,Ϲ\|Ll A,0XXRjj~Gn֬ML@ޣ $˥l= p:20Ќ3{פ$ 7yxd|djj*; cLAC(00Э 1'U6M>>>r8XrrLL0HNNv}||X\HЦꤤ$pE.KIIInc,AE i2rT%J9Xݚ9OUD ˋg{ETZZ-nR: `,fRJ:p۪T8qB'N01[xu,O7p 9bt 7(Vר]v[1dڳgQwY&&0&Ç3l VJhB4("TF ())IO0;G*Qv;R4dTT)~O+___rt=@bQ-0vjv f\av@ A,g,rԩSF]dIl6h,)%%EӦM3ѣGD= A,0X ``4"hE ev򂗗nKR͎@b h`l \#`aU͎ `%덺^Ӗ61y0tըӤ$k`4"hE `&lύ/ּkL fn6\k[@E A,0X >v_u{|1 +e{nz0 h7d{nHH3L繾7u0@"<$minE?iaw7}tJ @ZZZ+V{܁QZZa"r#M^tN @ZZ[ P$$9xƌQQQa"} `cÆ :G@$`@.]S105j$G@$B 0@"P$4_~QUUu MMM1dȐ¸**++3LO @cر:0tz1>c5j}/d#b;@"P$3HR56qMaǾUQT\a"} `o{]a<+߈0s4@"[N:r=80ݲxY΍|/P65P IDAT$B 0@"(:W~`s>L(t.]5jT,\0(V\Rɧe z Ě5k5(VT\H";q ,=#N;G /Ĵibv: ( `zԂ "":$0x0=j>Ĕ)SnW1ngtbKKkx1$D0@=|W Mg<|S__+V3gFilE44~n s4=f1}sK.ÇGQQQ?3f,0="Gmmm_uLׯp澈sN/=bҥqUWEQQQ.({hll|0,'LLX`ADDTWWos~ذaqm/ZZZ>8&LuzXCL4tz5 ۧꢱ1.⬣ F_p۸1?RmϏΏ566?{V^Kig(+DmmmǪUߏ7Fyyy92&OӦMN;-^SSS,Z(8891wXxqkľsL|[ߊ?Y?;믿G?Nŋcԩan˖-jժXjUs=1a„;⨣֚>hlذ[ΝgώM6f͚8{n)2o޼vĉcȑQQQcҗ>`L:k.X 㬳g\veova;ģ>{G>[1NeeeOw(K}٘3gNESSSTWWcџz 6ŋcV}'n8묳8""ns΍+2㷿m\~1o޼nqEřg޳z6mZFEYI\xAY>N 7N]6me˖EDڵkl]sXUϚ6>*K2LO/<~6f̘8c}s771|_򗻴NCCCo1n喘0aB1"6ljY{c{ǠAbqv矏z+(**6lO>9ƏcƌuE}}}l޼pOuuu\{=.Dszk͛&Lx#"b˖-nݺ馛Ytӻ$Z[[(_%͛cٲelٲv_W_}u9:!q7/\rI;6 1lذ4iR\|űdɒx饗.8qbQZZQSS[[[㡇:(?ng . ^{⠃x I&).\vX'zөǒ%KO+8@1b6r S?pDuuuQS---qWլ;AL!x|et9E @J#&NYu3 0@" `ҔGnC@R?lXo.fψ9sfTTTsܧ[v `.nïޗ维d3H  `D(Qu)3~o.s؈9sfTTTB0u( tlg""b^G@$B G@⒈q }~@K"F~P@hD ]|bܰc]s:@ vxg c9&JK@$$o~xҤI `hD(,L`hs6.6tz= @oP[?x.>H 0@"P$B Ҭ{Nj. D =qA.91k֬vcH$Eeee1Oy0@"P$3HRSSSr-^$O @}J ]ā7QA}_:@ `D(H  `Df566vZ.0H $w|>Iz#a0I*)) &@$,Mu SH ww|%$K $orBjmme˖Ǐ @S[ƢE c*Hg$B 0@"(:W'xb7.P`0@"P$B 0@"J<.vcH$~u SH  `Dx0Ijnn{キ0>s"D$)vcH#H  `D(H  `D(Qu őڍ u `ݳlu|._a`gyYG~v(D(Hgxk }~ )HR-_0V33+*3LO @i|7|.40}ӿ?~A !$B 0@"P$4 ?;!U}Ҥ Q۝$&G@$`TZZO<R I%%1rY>hD(HgCT_1⃬cزuk,|)GFyYY⥬Y@ Immx5>47 0@"P$4 , k;T.,0}jIv}|@z 0@" `TTT2R Ieqӳ}J @zO~  RЧr#og2g9 EYg @|qSaL(HRkkk˅QGFt-H  `D(H Yjchjj!CUUUQYYa"}v$`b }OY:Ѻ9^} e?{&J*2L@ЧH$"}c@  `D(@wmwa\$uksxW%e&7,uvG|Ȟ#H  `D(Qug}@5x۝r}`hD(Hg{̸vcH$~u S `ҥKcڴi~v9眸2J=773Y`{+@o8Y'ey0}'PFDܜA"H.({hll|0糊IQ' v[a7x0c_60Ijiyemagy&9숈Ƙ;wn,^8^{b}c9&okkjh`743r\doŋcԩan˖-jժXjUs=1a„;⨣ ÇܹscٱiӦv׬YQ[[g}v{QYYk}߰=QkWN81F?9V^ 7_yҗ>`L:G.+w}ƈwy'}c=fccc_Sg^zxYVVO?t"g9sD]]]455Euuu,__ :4~EuuuKWU]woNOdɒXreL>c̙/k|s'x"=شiS̜93N?X~}ov1{hmmO?=.]C nO:S/o.st?cժUok׮7ƛoYʝ2eJL2%?#nXpa?>fϞoW_}ulڴ)jjj⪫>[ \}qwFDD]]]Oګk^tEqg~^:M5)XhQ򗿌hllu8x衇7ߌo9f͚W]uU|ߎK/4F#k\.Ǐe˖Ekkk1eʔ.9|3`O oC ~9sʕ+lغuk[l)ҁ˖-k@RQVvcwpCOrG~O\~ww1cy7tS[ 3 ~`0>S믏#8bty~:SO=vZ}ݱzꈈ8㢾>9/_j\{qwi͖_/Z2; ^wyxNXrep SOnwqG׿Ç5F*%%%hѢ8ꨣbƍ]Z'ܩl+V^S^駟nڥuFƍI&K/3g,l8fϞ>l7.<4hPֽwxwSN9%ڥ eƌSx]WWӦM?x2dH,[lsqc]Z#=X<#}mwuWFć[_s5ZOXU<((O~~{ IDATqW{+W??o1n喘0aB1"6ljY{c{ǠAbq~ ׯomذ!N>?~|3&֭[y=q'Ν]vYWyxb…dɒv &-[bݺuoM7ԥ/{lcX;v-""-[-^xP?~}@G ~8nxK.cƠA" &M/8,Y/Ru]'N=3JKK4jjj7n~jjj(jjj .^{-.8蠃2zh\yꫯW_#@ʊ|>X"9 `U577}gϘ `}cܸq&J(:Zn]s ۼz' `zgIEuH  Iű@$,Lu SC$`z… ;;0 `z͈#:r}0Ijmm~05jTdz$Ē%K 3f(H^q `D(HDi:CcܸqY:O?ta|GFeee ;ޣ{u !odG 0@" `T^^555:0I*..\.u S ```ذå@.pi6H+@ϰ$mٲ%ϟ_O>=3LO @=繭-0G 0@" 3~[ߝAޣ!:\r}9  `D(H Yj^7ĸa%YǠ;xW~n S }P$B  ImmmX><iS-[ܹs YfEeee)mL4tz5 @OR[?ĭ;\x"@"P$B 0@"/{weUu*IJQ! %hTVD#FEmMqhgqj0(*$iEQ2(5wqpys9罧{离%+>p- N᎕L~PXXu6S@d#Hyyyѷoߔ6d:0)'''wtB !L o9ۧ Љ'jTRR&LH:KcEPd>}z}Gx` !K,h7WXXX04hP%:n C2 C2Dn RPP^ =ܳѾ#<2 P bȐ!.njj}_@0@dS@D%Ɗ HEEEq-$C 4VuB !L ] H2 C2 C2 C2 C䦻 ѻw6d:  #mB !dxד^zEnl~@F*--vϞ=d2>(j2JًW$N[ X@:{tV` ! #5ȏ.Hi@ vB !L [%_.U),,jؚ HEbӏ$Gv5mօG>ոN"QuH M @*..Jw P !B !"7vU稽C%lkOaaz)2 C25H9Qg6d:0)7//~~.6 >]f jq #Y C2)Heee1v=~999iTRZOz+k[0`bɦ;0 ku-:?HR%S@d0@d0@dtd^<~ ZWضU  /7~w)mt~@FΎ;2^ej C25HE%s&gwH7KcEPjW4k)eXn]|jjI(((HcEtWl]v.Z` h !` !` hѢM/--Xaaa-W@]v7D"QPL !dh׮]J2ԠA8]+á2x76yM6T uI ہoxvT^^_|EݶmζM @F*..1c$4VuP( !` a `L[vڥ ֭M&ڵ0` a0l˟}7M6e(+)-Hg>z姱"<tl` !` !.BvNny6d:  #eeD6]+0I-vW٧pjL @!TCH$]`2RyiI|&ٹyiH RL !$y˨j生a%mݷ 6)2 C2 CsoFj*R^gqFK|J,]tO>d1"PՖڴy*QTRa}A"?/'@3"T]:7pobɒ%1jԨD" %e@SZlwuW~ kd0@04)HwkJf"+++@cȑLJ~}]\24hm۶8 o2:XdI1q裏bժU.D^΋>8%۠cݢAN+j%i&J-5ĉcLJ~~a<#ƽoJW_}uY&e#G#GƩ>hJ`kcTED"QKG&=PJG۶m#???/_͋ӧ!Fc9&M׍.,.~ǒ%K""bȑ_I&r%KҥKktμyjPWi~'O(ܜ2eJ\{1iҤXn]/f͚k}[vy뮻SN숈())?(**/<zZ_}\ .`{u 7D>}scU8_~9k֬KS͚5^{- #~/xgG?T)liҡCs̉N;~q$n}WonܹsǏ:8so>m)"D">O>/"V\~iիXaT*|4iR|N;m}'tRΙ7o^w uIf3<&M%KǎvZuwƫY6hР>ѭ[9sfŻzfGZVZm5WB`+%N1cĵ^s̩{7f͚U.Rg3gΌ/l [5F:V4oOױc8蠃?it%chժU__Wu}kJ$.z48qbJ_:ѣGsrrꫴzWݑqr]vuUli%}g?jW^;n}̙gώJrdMi@Nwۣo&}GWٿ.IÆ Kn׿f͚eI@Ɏ{Lrs@4رcr{ҤIU饗겜0aBΝy6h Zh5::*VdeUgnݢcǎxxcڵc'x"rrr̞̍=;kdvԶ YfE׮]XQɜaۘ?7nwygڵk7߬1^\\/[ou srr,K.$֮]3gΌ3gˋ믿>4U lJKKwI{Q'@E&1|6mZ\tEѹshԨQG˖-gϞqxbҤIqu;cFnnn 0 o YYY1`8s㣏>.,:u;>W^ye̝;W l={vUVVΙаm)**'x">3"???6S@=#2 C䦻k?~M1l?H$`K C2 C2 C䦻 q9礻 &;_=Wl)0vM6/,,B-f h !` a `2RQQQ<gi`LOt5eXn]L<9/~i`L{8>tT8#Nw%Po` !` !.ҚSVQ&VX ,j^XXX{@ =qfz/;yDb b h !` !` Jg^sJ"fMK\pAraukڲG7͛GDD )2 C2 C䦻D"#HOVVVdggGVVVKS%%%ww}%%%_PVVVEf͢YfL`Lv!Iw@E.֭[ܲ ݕv(XzuK D"cҥtab]vtF YZWRR .lV^ .tB UeeehѢ(--Mw)lXhQjdKS@XxqΎMFӦM#///OZGIII|GyyyXQQQ,^8ڵk 0)+++7o2`SVV+WLנAСC4֪A;D˖-cQ\\R[}o\jWišȆ5j 4HS5l DFRBUVEӦMX%`띫j~ҝD"QpX&MT [qlL @H$QVVaÆi-ѨQvYY40|㥜rrrP [?+sZQȰ4To# ` !.6KKgFQQQw}]\pAbeU+q3&(,uE͓:tXl}L !B ۡ?}zKX{oޱ`cĈѦMXpa=:>x뵦+:v:t1jԨX|yԟD"Hw-={v^ã}iO<1%@F+--?8e{u*Ν&L;#JKKSp1y˫ZK.iӦŌ3>,yEqFϞ=c-[nV[(//7x#8())3<3{AFzjt!>䠄O?4XbEDD~O[]k֬XhQ79s$}gQXXXo?a2k֬ڵ]TCUjO( IDATݺu;n)ǧL^xaղe8+~;viʊoquGoDD 9֭[HV.(Dz)3nF6m]v1hРzQF[~zojf. `קOK6x۝w99昈8g?YԸhҤIKHO>d,Y$""ver!_ƢEgϞ]^REux饗bС. `аa _r%kZ?O#"L;J7n\r;'''l\?O?=] @ @F*++/"']tIiĉ'-57o_RgNw l֭['tR,[,ݥ!d2eJ%qWI'oɒ%ѷoXvmѠA˫>-]4% *++ԩS] D 6%+++ދs9'MU[n]K`I'cƌIw)d0ۜ%KTZti^:Zti̟?_֥aÆ /O~O?tޢkvmk)VXQa*~qqq?>=zHcqAEVQFѵkEEE)H$1q8Sbvh׮] 4(D"FC=4vi(((߿WN$F~bvv!7o]v.Z$]6~8Ó^vm 6,vmhܸquQ| kq饗~Z ./kO>s{챔y"HlիW=vXk.UVѳgϸ馛l ᒒxc:DÆ E~W_ ,_}U}q-ܒ?cƌ⋣K.ѤIhѢEWɓ'ot˗]wzJ?ҽ{xᇫ^/ :?|ǎGydn:6l{w >7F&9rd׿]v%rss7Utۏt55hР3qĸK7:n:Əp@\2}'?ͺgOz+TگiӦ… n]zu;6^|x饗ᅬ:DD+*> ~o>lG}w^L_ĽjL:55kVv O<O8sW_<0s=cŊ1qĸ{ߎ[n%zkrwqѳgd?ir{РAqϣ2&L??{袋0.]h}[tM:5 ;c\~q 7w}'NÇ;o==ܔs?3fL?>z(//C9$JKK㪫Ç'hҤI1y9rdrz*.ҍiӦ9|I&˖<ñcѣk߈XhQ 0 ^}ՔsfΜSѣS1~ӧO|ѷo߸曓 R+**N8!&L={{'v}bq]wEqq&O 6[n1r8c,""SO~;c_e˖q衇Fݣe˖~1<;;;uݺuN:m^K,e˖E.]O9c}FΝ,ndh /ȑ#m۶q 'C {,""Νӟ ~tg&֭[G":ψ#ᅬQFUx?~_Nc={={&Ck6JKKcذa> ƍ0ǟ5kOg}6o6oG|M\|qwDVVVx޽sύC=4,XwuW׿54i׬YJCݝw9:wYU'ĠA;L9v!D˖-cذa7 '/fb}&Mc9&85jT~_ƞ{g}v^:;ɉ>;>͛N;]69x+>}D۶mc޼yq%Ă -Z_-7x#Μ9q9DΝc̘1{… kiӦEDĘ1cbq''KֺZ͚5g뮋 &DNb)A}􉣎:>mڑG~{ʾ+VD߾}SĚjѢEjժZ}wu*_vequ1_ĭӦMѣGiݻw=zO?ݺuK2dH1f̘x衇#]F^GM }jhǸqbС7ŋ㫯yũwltUV '~m<)>իW}M7ӓ~:|Rλ馛UV;$Gv1ڷoY=/\0=obÇ?c=⥗^2eJ~i%nƌ6 *3xhԨQD0]Ygkq'G^^^1gyf4]tQ,^8>ccSOENNNrڵ>,?a]vc=6e=˗GwމӧǕW^]tN8!^ydZ^^?S{gxA~r)cҤIq'>G}tL4)ZhVxߚ(//#FDD#8":-60ۼ.he>-* X*~U;&KKKW^.]Tx~%۟y>}Tx38#_Ƨ~Ze-|I<3q 7N;R_®?G|7ƼyO>&]^^^ậOtx#y… cm/KFߦ~n:u!C$ƍhpGqqqt MƁl/^8&LE|x%KرcիW'GUdOiٜ})nYf /lvaU3ptMRKuM2%z뭈[] ,gyV϶]v͛;\i?}knyt]vMn^:/_W%???3&̙Sa.]ܛF+e7ޘZnK6mZO?TChɓ''_}}6 ן*2͛7Ou6k,ZlY9ZJ gΜϯ&7ߜrnuoeƍ\SO}?Z6s_g>]p"~DfQ>Ck}|-۶ǹV{OIIIa{l]l6mǏ__GDD"{쑲 ;6컥k㏏ٳۯzr믏}VM[WL0۔=z+lQXXXUj;a„h۶mm֬Y>ikjs?} +S@G0*W^Q\\_}Uyq1Sg&`ӪU*;llٲaW^޽{'իWG߾}wIY6mرc4k,o,XnٲF&_}Ur{͚5>SN)%KZMUYnTK}ܷm۶[ϵ*zuG1rׯ_r=qK/_~yƍJdݻwU(E<2#">8䓓SR=YYY)7h [i][jUr{ʕ>oikot=mZLz/?:_=3(nԩS6lXʾI&W\]ɺڹ>;cr$j,sU\|kU2齬g?Y̘1#nƔ)Ϳ8w/l65t83RqSOmSIIIr{ןN{ҤI5իСCJ{:oQˌm-))I}#wtU CAAA\}g y 0l?d#F/˔}TͶiٲe;,~j{w5\Y zuaٲeqoEqmŌ3[nÇߪxm#JKKkt^ 瞋;!ִmɊ+S)ԁ/*Gw]ֺ)w\'_"HTy#=hԨQWʕ++ycƌx N?-o"VYM2%86u!]ϰ&si_{JGK/%-_jƯ7 e[u"vXt5e(..~:>X P~ =6:Zh&L={Ɗ+}~Ǖ={vJ:aٶbر3lذ5U-~ӟƜ9s""bܹqiŨQRo޼y_*wzhݿӦM8s  /}0aBDDdggСC+W^^߿L~G+.Jy3ΈC{\}v)ۈxWK/;#%\oG}tGti[ kbvxg3Ϭx6mO>1nܸ:P&3 @F*//'_n }~gm:ug}6}ө;1eʔ|qgF͓Jnݺ Goj [QU֭?OqYgU8nHi;6~ń ROcذaѣGXbEw}]kÚ׮][{Ν;'W\qFm}Oׯ_mD 'T3f̈7ѤI(,,͛G֭kuŨQb==[ss5i$ƍ'ęgYa:k֬""Ϗ?^wڴi)7|j[{)yѣG4m4vuhԨQt5~O~cǎMUΝ[aK/4<Ȕ}'O޽{G&Mb]wƍGaaa ><5j?|G˖-%K$ۛox|ٲe~F#gxfOS?P^K.3<3~0aBk?AU(O:5x㍈AGʹKw-Xti? ZJcE믿lիiQ^^QѢE޲H^{1k֬a> g æt=֬YoF;z]iߎ;SN8qb<c{~{ 4(~4ѹrH$ǚ5kbƌ#C=rsF6m,Zh1{:uj 6,-[,5k999Ѽyˋٳg /CM fΜ͛7q#8xO?u?K,Xzu,X 8묳b̙q7ǭ޺߭uQvژ:ujJ(UVV}]r]w=8}DL6-|:th\2=]6rssh~jѢE/Ν}Q̞=;^|hذa$Ov[ 6l/_|A1dȐ{/^ϏXfMTV%///N=Ԙ>}FNJ+R=x})VXn~1bĈcnl2JKKiӦIVVVtIp˜1cF9%%%bŊdp,Ə2b>0ߎ ,ϏhԨQ,[,fϞFGy$s;D˖-#⇟ߺu_/{챔3f̈w9ʒ3wC O>$,;I__yaRQNbĈOf͚#FQFtk֬[o5"~_(//>('.(**믿>ѽjLԽD&-Duhٱ^{%۳f2ul֭[2̐!C6@nN{_*5ԩ&L}٧F.//N8!g8ѣG=ӧOСC}1hРСCDDwy޽YU{0AjoweI1@DKoY'15)N *A@涟?:͌g,˗goe6qGyd}w矿s;|XtV˧O={vq1x- OO7o^<1sxcժUQ]]ݺuĈ#3l3P_y啸'e˖EEEEq)~zYzlQ={W,]Yc.~XlYTVVF׮]sΩskO3y-n3foۘ3gN\2"vaq91u\rItM;<=3gN~SOŊ+&c8C3Έ#GnQFD ><~?q\}$K/C ^zeoI /^ܽ{6lX|%f͚UX>}⢋.ڢ_]vY⋱뮻a_~ykv~i&uÇ|;q衇ni1}K)@E /WeOzy0@J(RB  `PDA {uv*Jol IDAT `R8.袤c@r hP %<TUVeݺu| *++cʔ)qm۶ &s)@J(RB  `PDAzl򘿹C1m\Xwmrԃz%)H 0@J(R qc R~6Qc4تKfqL旕5 `hA.3 d2QYYt***clTy2;>舣0Dͧ2z衤cȑ#8[ jkkb `M )s%!5 tR@ róā9H@khѢxbŊQ[[{g 6,oD -H=WisFaGd5TUUU̝;7;%3<Çn)ɀ@aa8~__L&OTTTܹscSOšO9.\ӧҥKw]t!"">832@.)ZիWǘ1c뮋iӦž[g^^^|k_o9[oO>dsGOg}v7nկ꫹$3Zژ7o^o99XdI)\s hRiseU\?xܘѶ(Dx٪-h^`gaÆ8蠃be˖_x碶6;g…q3<4RqpvXvau^[dIGqמ{xb͜hٳg~1q:O(k `cɎ׿&%0N`رUV%%0N`С6m$%0Nk׮ݻ'ȥۏMi؂&;q'E 4Z^,'9رgO;;/""_җNZreL>=VXùov׿¸[#?_EiRA61svfUUUIGhZk֬;vl'?-斗ѣ"8CH 40TP&:pvsM:Mhٲef͚I&}'tR3xGbQRRf͊#8"@l,_CQQQ92UTTtm'Ruuu@d26>4hP; _}'R͛OL&'O?""{8餓w}w1"뢪*ylqqŘ1cbq{w`JH09uwƓO>^x>GDĆ hh: `rjQ]]~رcv{Æ  R3vr^}/澲@d2~cVIII%UꪫkO>9tRgߒ%Koh: `R)/M~uMw63n2L֯_cƌI:FjM:Uظqbܸٳ#"b/~ RI @*^C>t h㦛nӧǛo֭N:ŠASN . ڶm{}1u(,,lR@=sIGHaÆ%A~5jTL:5N>}衇sΉ?09昘2eJ[o&M/8nᆘ1cFݻQ{8ӣCqw E6n&M &͛<3fѣ&=ܸ۳<:thz|%f̘Ç~:wz?:*jjjbѢEǴi#lN ;GAZ㥗^J:6e2Xvm^:ϟ?xw}{5x~}QSS;wo1[~5ҥKs>wG:u/b61bDqq뭷Fiiir'=w*7$ YBwygbvqgƝwWn&Lq9D:G1rȸg{'N;+///N;Xxq\qq!D^'0T[SdϽjӶm$@C}+_GN{{o 5kmݖ_x۝}&Mjp:+ƍ+W;/f͚ըɵ}m+++k @nn5ɱ]w5Æ nϙ3'}{|=zD^bSOū hqO:{'xw9L&ӟ:oΜ9QZZ}snu{ݞ?~Cb ͛۷o~~s~ӟƲeb…qwluƍ۝;w׹SjkkcѢEq}KOս;v}':-g2XtiDD.1dȐzɽ;y츾ҥKv{ɒ%[/9ve1cF|[gc͚5o~)gѣGg̙ݺu:}Mv/_^gܾ}z伕+WFMMsN=8bԨQu2/ˈ8ꨣkHl`+ @OxI￿ocԩQ\\$in;:t `**k◳q%UѶMԤةw&Z 6k]QQQv6>-ˋiӦu]~x|K_aÆEqqq3gW_Qۦ&Ǝjժ6mZv_&I*𿚢ma O=TQSS{nw:4.809׵kט4iRvHƍO_oMvbĈ1bĈ3vr۶mqRQA~\tuֵk׮Q*++ $Z;6jݦMyyyѾ}4ˡK/ŸqbĈѻw}ݣ8JJJ_~q?fժU(;wٳgq1k֬@իθ^֬YҥK +2}9rdl2^xnǯ~6lXIskʔ)qUWmpKww}w|_os`'xՆ\՗rҨgϞQXX}up߾}s hpBn:{Wǚ5k7ߌ/R}1mڴ9rdBs.n1dȐyŪU""˗#<%%%IE%o{6nJДڴisFDDyyy-[,AO1k֬-g}6Ə3gΌ83W^}٧&MO?={k2eJ|ߏ͛7wݸ[䜫VAk|&97<6WDqa@vg ^{m?l@)8/_˖-kfW~?9=ؘ9sflܸ1&O?H:uٳgG~^XXַb}#GFmmmL:5<>|g>~\L{k7vQGů~7o/X 㣏>:gـ$+xX`A|k_7 \[t'Ƙ1c뮻9bNO^oĢE;~Ő!CrW L&^Xpa,Z(-[ׯz+;磏>J0a^+2n戈9sf|ѹs\G.rJ~1mڴ~wygvK/y>~ x ޘ9sfZɎ_"E=msuK,4hPQSS/B|_ly>mرqꩧ6h͛oFL/+cĉ1vu]Of;hР:wN?~|,X 'ǿcz2eJ׿G}4zQu;wnp}_~9""-[׭[֭g><޾qw @MMMlڴ);^~}/ڸ+#"$M*'h)jLw/~Q޽{ǑGp@?o֭[N{ '>mڴihͮ_ .n*@jKIG /_|v|G1y9&O3gΌ:+***sѿKFk׮ D ݾ [oM0M8餓;gꫯFDDaa6rٔ.}#WQ'رc1H8!SL:/˨M6s=չm_5*++cŊkup6m&׿u\z饱iӦx_30~ƕW^PZh=3!1qַ_~Ѯ](..]СCK.'x"^|Ř9sfL0!>mݢ K4yyyyqy׿x7.?ڶm۷<0kzrp_%&Myڵ!UnM:γ'JKKcĉu 4T+Wf{G:hG͎>(..N0:Fk֬+(@Xb/^eeeMhR `8nH:CGL3L%! %)H 0@J$uow!C) ;=sʚ' %\ @*:cH;@*im4@J(RB  tƀlCeeeѣ(D{ `R6^{:c yk֬n!|믏}&Z=04<奤c[̤#[/ȑ#|0z=X1gΜ޽{)u `rn1bĈ0`@}z曱nݺԩS 4(N9唸 m۶;h~5jTL:5N>}衇sΉ?09昘2eJ[o&M/8nᆘ1cF{ǚ?~v^)S())Yg ?/~Q'H7_w\]6֯_ߠ3f̈ѣGLJ~{nk:uJ3o޼v߾}fKFD?#ѣs6ns[ 6o7pCv9oܹ[{đGf͊ lD>蠃b}mHtS@yؼysv\0w%dɒmλ+""GYfED5\Sۦ-_θ}Zy+W;⬳ΊoΕ?poQF509in ۰aCqaaaekkk?oe]#K/4{x⮻k6~4 Y `\Sۜ[XX7|s1gΜXlY :4z?s104+I:5?Qxfs+N-ƍL&Sgo ;O_[_UUUum۶m8gɵk׮Q*++;tq@ رcmڴ)۷oH@#)vrz3׺5kdtRgW!NgϞQXX[^>Y۷s WtȅD?`[ڴisFDDyyy-[,*,^S[|RݻGDҥKWVV믿80gـS* [},4|JuQ_*""͛ ,裏Y6<8$""xXhvO>=ݯ_2dHN ve8SiӦmwwޙݾKs h0qGA? :qXvV=4(ƌlSHMMMlڴ);^~}/ڈ(//o[/6mZ4W#@+UYYJTTTի^{7VZsWƂ bذaQZZ%%%QZZݻw1/d2qW]w+Wc{͋ghѢk8.P `Z/Р31ʈc#"f"$fqowG}'Oɓ'g_?~|L0ak8c1s8묳":wK.$oDvMD @P[ÞZPQ]EkDD+ڶmhd299v>}?yN g %RK.jժcV.]M `Hڵk1H[@+DZgϤcPYYx b(SNI*+RRQQQviIf `P %<VkժU^xF-++kZ%0_y3L"q hP %)ImZ&I Ummm<-OAnm?#r vF[^sTUUUhxmk?iyZݺuh߾}vܵk(**ʎr vJyyyQTT6lڵK0aÆ:㢢"WjzLIIIud2nݺ:};-&邠*-[h%2L,[,ޱcDŽPn @i۶m)֯_-;Fs$Zڨ 6ĺu( 8t4TϏۯȽѣGu(//O0 um=RAAA 2$Sj׮][.yyy{Gv풎B(=0`@1E^|+,,=z([!09Ѯ]ݻwl޼9֭[ׯʤc EEEQRR;vb}nL^^^m6ڶmݺuL&n ЂE~~7%Ruuuݯ|SC=-YxԨQ %H%KAtTq hk& U/MO nݺ#4!Rg|=Dw^6~&_?ZJ9Z?3OϪ5wUeߧISKɘD}RQ?yL&th ^}8p`v+ĀLl_@S ZWtr-RB  `P % E׮]cu@,-d2Cٹ4@J(RB  `P %)H 0@J(RB  `(H:t׿5͛k֬nݺE~MdѢE$&-O?NTUUE^CFr0lCMMML81z~xL2%}ٸk#}]wݕtL`-Z~p IGZz+.ׯ_qvcӦMq5Dc73~8cȐ!1cƌ5kAk֬K.$vG{'|OOrLAK,:+usN믿ڵٳgǀ#={v۷o? 4gΜ8c1hРxc]wo׮]tIOo<@ѣGz}-[@ç|{>N[7f̘9co6u𯱱AgQGŋ'I~}OTTTd շ`X?_ޙg`ۊuYs=7555>a P޶nt޽ܷo&s}eرIq СCN9$IMMM=&m֭%A]w]$o#G68f^rG&I͛~Mzlls[Iw1C ip=# +lZa?яREgϞ'>~x,Ү]Fy뭷W_mT <͞=;7pCi#Xx+ygΜ9kӒ Ͻ[ZSQQ>[mUXZ[}i0+n$ԧgܹsiM>标z(ӦM[1W쬶6կ֭AcƌIuuZb ɋZ#~,Y{6ߊx:Yn{Qz|9XsPSԩSvaw٨:hZԔ1wul O=T]v٥Q>яOSЫW:+_&!/ܽ{f hyڶm.]W>&X153Ս @i-sym90|`v<}F[ݮl5-ӊՙ;wn9~`]5Աc:73Ս @yj sP~RYY`  Zo}A1殻={6SkVsƌ>S^+=>ҦMFk;[ouikT &^@|3a@S9TUU5{m1}Ӷ1cVWWgɥ;Quz9h1_5ke mڴW5Ypa_F+W.]2lذrcLRbh1mj̧z*555 @i-sСCӵk׵'(=n׮]<5i}kHyWmz:uȑ#{@pi?c5kjۏ3_~u>8>Ǽ|1?Ogk֯5AlIN:Վ#F F`X.SO=|׮5\Sz|d7_o{Wg\C9$CMo￟o|%!9Ʈ%A#G,5sq 9k֬=:]|ōo}[իWdĉ= _$o .MXɥ^~%In:צ_ѨQ2~$ɧ>|YS6>{o?O)Q}sI.,SN_)S$Y~yÇ7`FKڶm[n%niϯs/wy;wn??2hРF @QsЖ[nF,YJ|_ϲe˒$?O;fR6mZ{I]=Xwyg馛I!Cs@ $ۯe-@MMM1iҤw]qmڴwC#(+f͚o}q'-[ꪫ"IqqK,Y/5AǏ/tR$) T/.TyXX{СC6lX1}Һsw\*5ZTE#|,\0rgmݲW^s9srM6\-[,/R~Tl|Z4uԜ{{RUU;:us=ӧg]v%\ kc&Mo~7n\:v}'UUUyg2{>+2xF a -Z'x"߲`l0`@i۶.`m͙3'?x^{,^8[mUc z>ӧ'Ȕ)Stt5{wcf֬AoFƏ7|3ٳg>Odwn-Lc2!(`2!(`2!(`2!(`2!(`2!(`2!(`2!(`2!(`2!(`2!(`2!(`2!*yTQIDAT(` ?|$w}.u 2z[Ylن.u (C3gLǎSQQ^n Ri&GoaÆջ>:Tmh~sO-Z$ѣGFO>رc*+?￟>;<@y睗#G}I(R]]w}7}zyTU)e{IUUU~1H8qb)KO~F߾}f I/SO7MSI.5f̘uQfm'ذ( -FJ16)ed̙yGu$?~|qeee Ԩ~?~5FZ(rwdmm0O.(#sOz7gk0 [lEpfx'o裏n)E PFƌ/}Ko7|KΞ=;^vy&I0z$1`@"(#fʗFmZ.\]wUX䮻ʮw߽v)=s=+ޫ5i|X׻wKkU-ϸq3~j٧ZglV<[W^ӧφ+QF%Yז} `l.\'{&塺:{ogׯYƴO `_Ԕu7o^khޫ0zTVV樣j1S-`#Z;tw}VÿҼy2v|SJ^m\@P _(TWWgĈ6ƲO62/R̙SZ.K>?]繢(6P5-ǨQ&#h17} 5p h̊M7;R]]_Wcy3dȐlV6ƲO`K+++O|bV~k.Gy.3fL-[c9Yݘ)N Ylg-؀լEF;67} %lDf̘W_}RS橧:4۸)E q֍ѣ$#FhqS-`lDVWkvaٳ3{|b-橧Juuuvqr!i߾jǛϦk׮ϳ>3ggϞӧOF_R;3cfvm3tl֙5kVƎ~;Æ ˀ[dIz衼+n?mڴik5jT:wC9I.ҥK3q̘1u駧]v[om&CM.]lwG>vaԩS… 7H.]2dȐl6l7k `Ȋgk3o}93|tI9s׿5zhnt)mڴɹ瞛>}[owS,\0nm;7t1eʔ|33gfٳsieС={v^_TUUգGL0j~}c>}zva?>m]N> :4ӦM}ݗC=N1cdvȨQҽ{E?>[^|FM<9'OӶm۵:O;3twq9sx5*|/^Ν;/O>}2v:Ncǎᣏ>ZZgb-Jo喵[nK.$nm{$|ό3ҽ{z۷onU^㺾oݻ~;NʹiҩS{챍9e`dɒ]vE"I1|&4F^zy,?OV?xk׮:_yE裏npۓ'O.uV{WgqW$)N?U}X."IqA&LhZ/"Iq 7y*(.8k(.:}ݢ{E݋3f4wyEG]~kҜTeeeۋSO=-W$EΝE*ڶm[$)yUϛ7ڵk曛=z&L(/~;vXk׮xw|KzUֿ{E=w߽2eJ50F',HI+c}8C=TgyHRm۶8qbu۷/vX-[V|#)Ǐ_e%Kx`mY6lXh޼y>??}Q9sJ}g͚Ugo~"IoҤIE۶m.]/^e9So ~޽{7:9^zIAի~$?\oE8O?t,Z(:tH<ꪫay歲oOiӦ*O8ᄜp k7!SN9%rj6 $IAnQyK/Z(<@d]wwK?~6|֟$FJ1bmܧczdMd]^Wk֯3m7I2r\x7n\LR~7tSN:u6PedҥnJ\q+O6>lu=PϮEJ/<~6mZvg9cض!-i4'$[lٲ$ϼ>=8b{~x3x9suו.&lM^{M]v뮻{fM7͡o6lXzJu5Y~3̽ޛ=zG5\/}K9s_G1F$UfEC I޽[ow] nil< zO&Iݻ'I.¼G?na/I8 I2iҤ$lJA2dH?̛7/_Wm.&ɭޚQFeС>|x/8]w]ƌʺݻw$j՘|ɒ%^{evZcrbڒYQ6m2r$K/tq0Mtz={vx$gQzOtAuC;#?pSNIEEE^xw˖-~`]++ӫW;6~*ץd  /0Ǐ /?1bD5}zE:%3v̝;w9wM7M֮n֬Y2eʿfe'xb*++梋.*] .zw dȐ!9Kw-I2cƌUkyG_W[[m&I>笳Jmmm?z{7gȐ!>[O~+Rk<+ $r'Cͻロ;67xc䷿m:U* hYfNܹs$չ r _BƌR /0niƌS?mN˝wY \};vlϟ|06lڶ-ٳK_}ե"7tyo}[i׮]~_dI~{Ȯ$+(|*c5\ɓ'cǎi߾}/fڵk$ygrfʔ)Km6}ɨQ{g}3vi9C /K.ҥK/#hf92?~6&LsҥK oN}YҥKӭ[{vy Xk+Y}ٰŴ@zkկ?qF6,IRSSsfԩ?~|.L6-&L{l?C3nܸK5fԩ9#Jg|h2 Yf_j?quUUUsܹskc=4[GR]]ڶ7tSdȐ!Ͷ7~xɆ3vؼ R[[$2bĈl:%V^yɓ'oJZR]]+wyg,XPZ_[[ &W>5<̼رcɺkf矟;o|c} ( ]-3}DFFJzzK,-]T&M5 0Trw!ݺu3Qn۷˰a<]}  ?"m}uQQQ@v9sFyIJJt)8ԩ\|ӥB ^Mbcc=P w]pA~a9s挧K,%$$ȣ>*{t)@z +F (Q"<5u\\,ZHڷo/ʕ3<~ٰa\tIJ.-m۶5jdݻw˞={… *%KZjI-/["+V;s,_\bccQFvGX*;wGΝ;'"aaaҪU+ݱa-[?c9u-ZTj֬)[tQQQcDPKŋСCa]"˖-CIٲe[nRH=zTo9{$$$HťRJҺuk}A֭??? {GJ,Çe#Rtii֬ԫW/[%''ˎ;d߾}r%)UԫWO7n,>>}ƍ%!!A>$Yn;vL.]$ʕ_*TിGʦM̙3RpaiРDDDbV}*۷oÇKttI Yf>oGOe˖s=gX.6l;vHbb< J]CΜ9#={7f }nQ ͛7OEhӦ[}L4СC:f]Yiۤ$]|>ZP!66mҖ-[j޽^z庮\o-[Gb׺\hQ}w411ѥ>/_~6jT~ cpߐ<._|Q˔)"ZhQ~)zҨ(5jѣzw}ʕ/W_is@@ߺuw}R_iiib }5 @k}n۶mZjUCUV{ֶAAAh].PWϜ9㰞 j]:DD͛簯'OĉI&:|ޏ Gվ}jppk… .Q RiiivZ0`8p@z!]vN7L8QK.m>dÁsӵf͚n/y寿f͚Y "EX[j6i$ks}YD488X}||!!!:m4MOOw'NʥKtZX1g秣Fo-Zׯ__:RmtZbEX,Zxq-PO{uns=w;669]rek<3/rʦ~wqݸqC'޽>CvlܸQK(5+߇ry*7`{a+_FEEq[VO?ccG_eYƍ\r*"Z`Aʕ+zyBkBBݾnܸ?v"fk٭Wϝ;'OPmtĈ:m4>~bn׸qc]ti;OAAA*"ڶm[?ŋuڴiZT)CX8/**J'L͚53Ԟzz92]DW^.}.\жm۪O?mo߮ڵ3ۻwoMIIΝ;^ *lb <33g !ĉz3\z֪U|JѣvZjΜ9SgΜi Uc}.a={VN-[4|.΂R???֭n߾>}Z{=n۶MT:~x瞜K,Ѱ0k~~~NXfZfMn'%%ԩS͛wL^vMgΜhq;{[N_ORR.YDz)CAnje8 ѡC5k۶m>ry*~kھK.N矵iӦ4vѣuΜ9ںuk6Whxx:|;v?wZ] ܹs? ⋦6;vkDD̯w֬Y*"ڨQ#C8x񢪪;wN˗/ozݎtIEn0*rא!C5awy5k״nݺZzuèѣuРA*"Ztiի51dܹsѵF%&&j Cwm۷קOhɒ%5j0`;YȺvZm۶ݕ8))I{16Fmޜ3fݶ&Mҹsj.]靖h*Uy k7t&OlmnTܹðV:uuwΝ[`Kof8V^mݜ9s ɓ{駵Cvσ}U4((H}||4""By;wN0 ];111ڠA.] /ɓ' D_~,ǿ: ʕ+2\-YxS{ǰc^bcciӦjX)SytС|}}}\vMo5kiӦoAVrry*pzzΘ14]q oݹs8p@l7u&&&ƴMDD̙3M/]իW7oԨ}؎eiO0Oϝ;gx>sb S'ObŊŋU(oҤ3f#DgϞ6?~:uhrrѦi{7C#DD+T:i$))):vXӨaŢ֭t{]w9-pB?dl`͛kO8(P@wew1lle?ö۷Cf8p}ݗe%g?994xDD1cLxbbi&2ehjj}dAAAN .h"E<זU;ijլ(P{jBTRN iiiԫWOÇ9u36UT .8̙3kNټyA9oܹۡs cz ӳg:g5ϲ7xy9#-uVQQV-iڴtU>zHn*\5kfX rn9Ure۪U4jȥmnVܸqyc{x饗k׮۷hB ]#Ο?o Nkׯo5oذipiҤmE=m&+WN}۴o޴nԩ.Kݺu o={䧟~6mHٲeӤI5~2m..r9$"`)Wml:tHRSSǝkHN90nklR:v(/VnocEv``ŋv/^&)SFZnr-Nk5,wi{{^x×󩩩2mڴ,>ٳg\Һܸqcwy}׭['۶m6pDdX,vC 'a+^xuVmm? JŊG?qf{L;ҥK-[ܰs<{X,RjU:G׈bŊ 4ث[TVZg̘!ǎsu)<<\j׮m]U;lX@ò+bMl?'Nm駟Zݶm,-P-""Ν7mo#<55U^t7@v90n6mڈ޼ᑒ"Ǐ~Azi RSSeٲe#H˖-ȑ#~نׯ_wvѢEo-۷o2eʸF;*T[En>a5mݺUbŊr=OG/_nXY[ێ~rT|4z6ӧ|]>rH/^ 6{Le˖sz2zUjժ)2ڵkgիW}Q5a'k׮ѣG{'7n2,/ǢEvqqqv-Z2eʸt}Pcn;;/n<"mW¤gϞ? __ҤI?=S #Fرc%44n+WȌ37ϭ)>/s:lQSO=CYfavtdV*V(!!!u} ˛6m{޼yW_5ʕ+Z{"} &8:%%E.\(a:&caÆc77W>>>2|púoVj׮mY~2daJǏ]w%Æ ҥK˘1c74ͮ!&&F&L a}^]BBѣG 0CƔљvao԰Rn@Nf;CV\)oƺڵkҹsgٴiiAm۶Ɍ3dRfM y!44T|I/>4h)$KOOHEz? gR}oWXa]v4 kNˏ?h]ŋrOdSRM'O>L 5?va;媻JPPԨQű!CȯjԩSҵkWСL6MTP̜9pɓe'8VV3gʒ%KYfRL<unVڵ˰ȝwޙ6lsB ۚ̚5K֮]+NAڵk=X]IOO ʤId֭cɺuI&2fY~Gz䫯%K#lÙ)Ra9ÙGm?޽{Cr]wΝ;e_I.]=n^wXqU…eʕ /qㆼҠAٲeoȐ!h"6.&MryfUw!O?ԩSG9"gvϷv}s}A |m۶vg7nݺI.]ȪUr}mN1°n:ٱcu9%%Eo5vxh gK,)Ta9Sf{jTiVTU}]iذ,_\v*w6vd{ܾNJ;O?~4\Drwˌ3uuE |!$$B'ϟVZY;vʾiѢaݔ)S^b\|Y|A)]t߶q>sC)EDjԨawpp9'QFIZZ<)AAA.% 2뱒<۷OƏo].rG 3gz:KddYF5jdz~~XRRRnaiڴu/B훧5{J~>}9= @gw=?󃴴4yǭT5n7(H9{"7?ԯ_߰9QV7q]\bXnժeʔ1|8[} >\6lؐeMy9E |aƍv7mWs_|a9,,i{wG0"Ç7={/ݻw7QSNGؿAAAyrOEW4o|˖- ~$&&'Onݺ/ԎKZZu9M=i;JBB?~ܺ\R%ʦM<>:t 'N4;zh^吪ʪU e˖Wo~FDJB ucƌq%K̙3%<<<*{TƮ\rry*7OII1""e˖{<۷ϰtΝ3,{j맞zR\|Y^z%ɻEn~ζry߽{DGG[}Y)QDg=tPSzʰ|1yw]GƴUVu::4;7,3M;oÆ 3[`$%%+rƍ/dg5h;e#wcrϞ=NCS{VfuL+V̰l;]swܸq{={VMt ڵkˀkD3gf&00p_;n?PT"R~}sʕ+MShjmq~9"wȨQlrʙqz{N<)g68D\D̯!7 7 OٛՑ 2`׀L#~\tɰΕQJ!DV#SSSM9}/^,+V0;wtA.\`Xi&~]f]NOO7ՑK>sRhQú޽{ZFϞ=4r֩Sd֬Y"rs믿ZBBB\ի-vE7nܜ^zܹc_-Sҿ4hjݺuҼysYj릩3؞WQQQ.v=&ϟ?/]RR 442ÆQayņQoIIISO_mkXmܸ|ᇆu|gؾ},\PDD ,(R`AݽFea͚5v ]xz]̝;Wx 9q$%%ɞ={'pcJMM7|3vO+YiYdϞ=~9">iokdBB)40ˍs D\aX믿t6)))zA裏n뫣FrGDD8ѣm^~eٰaim;p@S[ׯΜ9SNׂ jDDΟ?)Shu͚5־+S *ŋ]~ϲ2tPCG͵bzzYmyf޼yYߦM׿MLLzh``Yc}>/9R~iU=zhZZ>|>,QU]`ݚ^&&FV"AAAo9?>>^ ?N9utnm۶Y۔-[V>vv[P!|Ӻ]Un]kSNunƍ"5k7n|w֬YڣG-Tm~a3fN6a=ڢE,k_5lذ6k.CqqqZLV\YO8K.k>}pvWXQsѸ8kʔ)szi \oՊ+Z߲e.>>^}Yw}g7pv~m2"7ÿ)ShLLÇw6O?ը_~uUKDW^[kvΜ9S-"UV5ر5/aXtvrd;w9z]})SF 9sFUU:SL^xpͱ='z-oϙ3G{Eqx2D'Ol s "ZX1r3u1VuI4%%ڻwo Zj?حgvWncg~)R,VX1qqqڻwo-U?WGg_K(a=/tY3WPSE[nk׮5󚐐*TpʘUa޼y.oSn]k-BG{#H3y;pz̼g@-ֈ5TPAgϞm 83+[l1(ĉv_GJ_UU={hJ&,,Lt2b}v[9bjUo  Weǎ,+V J:u߼ylڴI]vRv[\MڵkK@@HppGyſK$&&F ,(!!!Rn];m۶vZѣe̘1*7n={H\\.]Z¤u-6lh`^4ot[an:Yl};&+VDiԨnZ,þ/_,K.ӧOK%]vRfͼ|9VaRpa)[4lPW~KjpJ\\8qB>,gϞ8IOOŋKze˖VC!ׯ{JJJhXIJJ JXXs=RdIJNNիWEUbŊҼys ?;wJppRr\{]~ٹsHZZ,YRåQF{%祽{ʦMŋRpaiРDDDHmTU{^/ׯҥKرcRHiժ4l6yE -'NÇK%22*G0mZn-{s 'n/RDD}YW ;#6l^x0[N/UV{n^zIժUeȑ.ܻQ,U#F/^n?.]N:ə3gdy\2qDټy.]Z~iO [lMʅ dRNO#DD$==]y)R/_^x Yf\~]M&o񆈈;V ,jFZZa9%%C=zȮ]YTU=]ϋ={:mӴiSټyxoIW.QQQ}=W[n;wK(! ,wӦMWDd롊`""r1iРř e˖ɝwιs$22R6o,?$&&ڔ-[V:u$ժU2eH=P)kXm۶M|Mپ}$''K5CJ…=]^?s}HHaE9C ^;oA%K /A ^0x `%K /A ^0x `%K /A ^0x `%K /A ^0x `%K /A ^0x `%K /A ^0x `%Kz r劬]ֺ* IJJSNY۴i#.Zv<#.k,ZHt2 S@ /ЀBCC ˋ-ի{YIMM[֭+O:zᖛ roW.uP5( ..wU=Xmc h%K /A ^0x `%|=]y! @^z%2ގ,{ n)/`TUEU=] X,#ӥ "W^WJJJ /X,'ŊbŊKB6RRR̜9Ӻ>>RhQ)ZcƀtIIIk׮ɵk$==\RRDGGKŊ=X!A KKK8:\_tJrr8IKK xBŮ_nXX,J .___ bXo{-/f;PBjRP!úxUw@v)۞(. F5Q{EP%… 0 _nۺѯa1JU%--Ͱ`@~b;8--MT454n?L JOO7+P*ػVػC 7 ޵x%VͰ #x%_?i@gO-p(kYr<)r۰yX IOWt=exAct0+IrO t=Iʼ?OΏ&z ^RSSd_ˏ(/0x `.ȯ? u=]%Y.w %tޥR\9O F?k3x ?)..Nw.ׯ_t)@!J~ȑ#.U6lϞ.u.U_3fx)J IDAT O0|#X~ 'Μwq~,3fݮ]춳X,2|}:tHƏ/-[?а_5jH|ҿ9y~4ipE"""L=zaҲeKSvZC`L6·#ax |I'cᆪ*v&UQd ($1%$(v bM&`F ֠KTD",;?92,e̾^9s3}39.d?<waW_}ukk$f̘~a\}[oqܻw}w<{%w9b?9V\6o޼9rd}z 2$Ǝ1lذѣG<D"N>xW#"]vq&Vz>=4FEFaÆѦMϏVZEVv_zJJJ㎋5kƀW\w\L:5رcߟ\_F~bҤI)s1`;LѣZsƁ=zcƩ}Eǽ;""W<2fҥ1q8â~[,+TV@Ra Yak_2ڵkjDD 6,.KyGbٲeʲ4ی.,y_ѣS=:֭]vѠR`qQG^{핬 ŶJ`rI'E 7o{ǁhv$lԵ)????d=iҤxggO>Bsdee%KKK7{FHtm5[F:{;˗/gFAAAl^5Jw2H5bҥuܬYRD"EsK߾}cQTTD"z߯U_p- `*n}I?#3k֬ f͚s_|Je˖P]v7Yϟ??~_Fƍ+ &{t&pIII3Y2 #eeeN;| O>˗{bƌOy۷뮻.4i;|?~|[s%%%W_%E}QJ_oPs=7rr~?GDt1y\ZZz_|1~ԩZPi㤓NJ *N;-w?ݻwn::wFѣGGϞ=S޷x5jTð-Ⲳbܸqq5jԈ5kƾ\sM&""5k_|qL6-,X׿sΉ)SDSNI{ѢEѼyӦMWq]w裏I&qGƊ+*\ݻw5jT\z2eJt9W;ct%&O_}&gCtѷoߔ?;wΝ;W^OE}'^{ɓ7K,`5qhڴir!dX`AW^dgP}[RRR᱅ b1?S ctGئ;+u]YYY08d}YgEAAA_ȼDb dةߩPy <8WvZ@a)ۜ>F^z0<tPt%  .ӧG=nݺѿtGJTJ/޽{GIIIDDL2%뗼>va*%+nU]ѳgϭ*? `*xG㠃իGݺu#{..tǃJPIK۠A-*:lпJqۨQ-m-20@ @F͍.]4HժU-Z;lUvehԨQc@gd+a#]Qg2ԟ;hJJbiɺ>Evl~@FJ$Jc'']Z y0@2y}Μ9kVXXyi &]kw|fN-2%"}Y0f˾YSd?Q/) +% `2VܼtGJT-7?qbcVscy}XR 42@z yp320W_} `2R"ҕ+D"H@Dcܹ #;H͏8!1PQSFYYY6 `*z( fx饗~:餓 $O'pG&4[O<_}U1"mYo`}ck4ϟ/2 1Nm9t'*^zŲee,[,hԨQPEYVqYg9z^z\-D8 uov^x!Ə͛7~8zݻw5/Zj^zi1nܸ1cF<ѽ{N~;ڔO>d̞=;ԩSDD|1hРh۶ml2 s΍qEǎ'OSO+V̽`ѣGi&Yg=XXbE|1~8Cs)P뮻bСvOm޼yL81xr9rrr^s9'yOS{k\>~_|E1uԨUVDDn:vrH<+ *7q~a|q=?>Ze}#<2rMވ $_j֬Y{O?4x㍔kSN38#=x饗⣏>+VҥKM{/ts̉>:Ǝ99?:vgqF~'FoF<Ѱa丹sFӦMcwO6#ow14h#G-ZĒ%K⡇aÆEIIIDD :4⦛nJnݺqgGQQQ_|.]7Mm۶D"׿u 0`clVTqC /"""7nl +++.d}mS̜93Mݺu[}{7)kcĈ1yg}83駟/'jՊ3<3vaok׮1gΜhذas=)1yi""wމ:*}G#GLy1c⪫iӦ)r!q-ɓ#///9vȐ!1urׯ_l[ۏ?xT]UܬYKƇ~r}Eqͺ-O~o 6mDDDڵ{ے ^DDyQf͵οN;t~W[o-7nvKO9ӧxҤI6JKK]+..w}7}_vmqA!)Ç+_}Y3&ƌ{lH!v7asݗ4^zi4m48*+WԿ/f͚dɒxC{ܖOyRvZɓ'lˋnݺ]w .^z)9䐍ZPPGuTrD"ӦM]ySNI6Lf͊-Zsc\|]w4۶mocN:<,|ƌq衇#,jYqH]jTeYYYq1gQnJ8#ԩ/O\|QTTo_|1^YkӮ]go:8/[zk[.a6mW_}&T^9ղ;lJJJb1|7o^7ƍ۷_=ܘ;wn+Wn)w]%})e*^}79sc޽{?>""xkSM6-̙\l2?9>;wn\yk)SDΝ㩧e˖ߞ uϏ}wsG߬#hԩѼy󹹹q9DӦM㷿mň#Nؤm5+));# ժU믿>z1gΜ q 7D˖-_~ɕ_~e'wygjj )nɒ%)uQQQo}Jo\W.,^|y~1`ѣGGj*}7-#nҤI<]Mvvvy_X__#<2N;d}umXPѺuׯ_k.fΜ={L~N:xww)W3zխ[7^۶˫[}[zmru;ZǶm6~_%n-h7n\t94hɹe 5I$~a̝;7/^}QrҥKӘZW;z*a6guVs1> uo|QTTx`3\rC5h %\W^yeL0!""N&M45&M3f(,5Y}paa&gYm۶]/`L<9z~m$8pVor!)ɝ:ujuY}ܹsSC=t| ^{:u6moC >}ѣN:n32<~0`@̞=;QLc9sfDA D<;d~ϝ"ZJaPUr@eO;3[lqDbs.A.]eeeԥ8Ccw/2""yXdIԬjvEf69/<P\veѫW~>&L={FAA&g"3dR /0zm֬Y'=X{hѢx'Ә QX |Wk'L:5>ޫk׮QNXssw&… :(((K.$Y/[,F=lr?~|DDZѽ{󩧞-3)2W^> #=3ԩժYn%%%'5j@qƕtҸꪫRέ~15\{w}?]oذaJ=mڴzҤI)MU{ ?̻>w^H\sZ>l+qiőG{Y՝w}YDDq;V$zTV-.d=c=2}bҤIѮ]~n)#++kׯ+VȌOsN׿za"??߷-d㨣ZU-t'6Mv⤓N/""[o[o5""ԩ7xcvi1k֬®7|3:t 6{.$ݻwL<9?hܸq̛7/ƍ#G:uĨQb+zѯ_#8"8p`\uU1]v4k,8Scwޠ9W&?ϣp!3e\㎋CF[ԪU+.r{Ϟ=;<@w}%%%qK׻w^zt%ynqu7ߜ<׿Jō;+Vܗ_ntFTTn۷Owegg:5m4{5^ѣG$MΒ7pCp k>td/hshժUJ/bsk{FUe\Cq]wD"[b DQQQ;kꭷފ5jĎ;իW+b޳ኊ}'5k͋z+w\\yiL ~'N5k1(TB~5kƭeeewŴi8v>}zDDǼy""b[Hjբ4;;/9sf̜93elnnn\}?1Mi;D\aSd;;ۘo&y8Sf͚C% 曣w1bĈx'O>t-=b1q裏bl{ѻw8o9&MZj{DΝo߾ѴҝXСCcŊq; TF6_^z 0  R1bĈ1bDnԨQ|[d}L{_ݺuC% `8餓Rj-86i'x"ˋ.]ĉ'EEE.JJJnؤldOw 5kVJon|'xb̟??""y晸˓ns=7i~20ּy;fΜ9SXXaVƫ^{m)S%\vXԯ_?{ ڥ~:문K697MJ裏,$2l#l !420@"'`[ժUhѢEckrou"777` #Fn* `H-hĎ3LCOHwd !42-Lee ?ޱqD=4Le%c]ADv^lD;FFˋt20F1qth]vt20USW)9sDž[7 l&TM]RJH$v,[oEVN:q;T)lVz+bȐ!O;TlV)Ǯ֣ f5dȐh޼yԮ];;կ~,K۽!rrٳ#&Oƍ;Q`j:iHJ5>8#Lwت4jR'-_<9X`AV @fW5JKKOӧ; EvQc_YYre|Ǜ}ޒ>1Ǐ*o3f\ 6:ygO>96m.đG׿;̙3^{ /q1eʔ8쳣I&gEDD" &GKlv7߼ƆM6s=|~X`d?L*h֬Yq}Ń>|AʵEE߾}cر)˘4iRL4)>5jT'1nܸ;vl; bܸqGQQQ|III|ѻwxS3s̸袋b1nܸku֍G}4""~_ݺu뮻.9`} lӬ͛k4oٳgg?K6x'LxѮ]h׮]D|{/~񋈈۷oKz/q&ϵj*:v{g,\0""ٳf$ hF;UKw `-L"Y_? cuiJvmKn|衇ƕW^Ʊ-[ÇDZ<7p8Sƭx]vezРAѷor֭=z;#""Oeeem[X"ULnܸ:3gd;믿2zuu7[h<^ti|7TTXVVV HyKUVJtҭr_ؖhA:tRKK*3 `6{RiJN ۧԫo ON4盲9s""0Y`[biҔX0U.uH$iNۦm۶iJl> y:tHc`Ul_~9y| 'D͚5Әqiiig h2RvVD)5y=:"_{饗^VVRoDZ$.\U f0)/'+zKrt">u^4iRL>=""{w1/^g g[lYJrÆ ǯZ\2Y/Y$z 5$=3?*ވ>""^xx뭷R7|s7o^JhѢ裏R꯿z:v<ꫯSNW^y%{رV_ [0UՌ?8>tGJe˖-~ӟСC㥗^ӧcoOy ^#GW\2M7rK̘1#eoIII)cx?/c̘1ƮSN=ztx.N;8ꨣ*6F*nvFaaa@Զmۘ9sft)n/~x?>>x//ÇGv}|)׾۸ u="".(,,)S?[}sb=s禌2dHqM7oРAL2%:t.G/bo>rD|۸V'+=*b]wCСC+G+<~РA1hР }+<8x7} #FQ/ ik]vMw@(+++ `3L a0l){9f7nZ='ۏiJŶGU'4&6U… /LofJ O:*ʒ}ӧO<[oŒ%KҘP:xxG'o~ Uk` h  !l @FVZ|)5`7gΜ YXXqa6# `2RjբaÆA%ugo9f`C Chd ` d+W+8Mc"4HD"ΝRT5YYY= Peeeέg 0P 6B8-O\rҀ*)))wnM?S|4*~WXXeyyyQ\\g:n4> ` muٺĺU-20@B C;l y;1tu^?e 3hiS^Xd+% B Cx0xxhɺ[3#/ `˳ CX K"Ickg}LŪ^y7koo\.W4@Is1;|>_$@P$$5x̙QSSa"(>0I~.cH#a0Nj΍1IB =ztsF*ap4@"P$B 0@"*>F:Ѝ2dHaN|#H  `D(QuS6o?%2xۜr l=zt,X (V^Q'WTe `c͚5ёu`++/I;sϏv-N;Oc]v: ( `#".$0x0}j^{Ŕ)SRt?SY#=Fa<1Q]Ya"@+Y?:0}fɒ%|袋luvF45e s4}f޼y1cƌ-/^Ç.fΜY,0}"G}}}|ӟɓ'o1Oĺu붸Gkkk)"@KXxqG\uUQVV_n477GDă>X% )S'ϟuuu[6lXvm `o(x &d1T.,CL566Fsss\r%YGc% ƜW1@ڼy"wFͱnݺի81/rǒ%Kbʕ{ņ :F'Oӧieee^%.\GqD=3gN444ĪUߏ;=W'pB~w7xc~'dM6mZtttl1yXre\2瞘0aBqqGjG}4֯_߫ݿs̉ٳgƍ_fMG}}}s9qFmmm@ ;wnwĉ1rȨ?Ozhjjw s|0M5ϟqg;.{vXǒ%K9""㭷ފG}4vm ee|j.cH8!/F⩧ڢ,}g믏hiiXlY3f[~}444qѫ{WvmqgGyyyDDŜ9s+_~y̝;WkED\|qYggV^ӧO@iTUצu ()pBnx뭷bڵqumuɓSFccclܸ1~w^o3fUCo~7n\UUUq饗~ӦMθ뮻_rZs1|^}d+]`NȾ-㢋.ƈxz޼y󢺺:05 IDATGw-ߏ;S .;3""n^ﺍ\r;p|\2^}xbڵaÆx |;'SO=5v}^eܞ\sMnll?{j]H8Ap~k׷ÇFyyYy믏+VucڴiE]XtiDD]dΘDmuE ~{cǎ;.<8蠃//c.>hxgcƌ1hР]|H.'7xcq[}ϟ޾]Ͻ꫅QF7>*e#"?hhhyŞ{SN{'>+V#"h?gv'w-\~|[[[YlY,[,կFUUU}/}i|,\|1t>Y7>gdžeFkkkz_aQSSa" e&M:D;vlằ/٣u͛uuu=dɒV0aBs=7o~;""n喭x≘:ujp qg'˗/>0~b0-q}3gΌ ;öJeٲe1~ lGć*++㳟l1@_839,m85,X'N:vY 8'jhh{_)D͋8slmڰaCvH};H  `D(Qu(/\P0p Vr"q4@"p4i*y`1$N @+"Fu ()G@$`e{9R/>!;a0IjQ銨(0$wF04"*@ 0@" h\.Y 0ޘDDD>0 #H  `D(Qu_Ëo,6$TF\}lMqwr\qC@)HRYYYVeJ;H  @Z6ߘTUe&S|CS=:@76ojaaQ]]3<Ա  `D(H YRinn2njj2r E`ۿm%p4@" I]t./wH$UVVƑGu ()ۡH#0n.SN9%$P `YӟÇwy&˕8$uttoxȑQQQa"(>H  `D(Qu详NǏ:ЍXjUa|gDmmmH  `D(HDek_Z1N @c}:#H  @Z[[{-;H0$e s4@"P$B 0@"P$B 0@"*P^^\R忿޺k1mN(\˲3L:@0@"`~ma>#{ m `޶9~g]5&Sh~no\H%Ol>?2DD(H  `DTfvԃG0 @6@2nr}K H$*&O=R I1rYr4@"P$;P1c x0>cQQ]Ua"2Ŭ;_:@)HRgg>^ycGfJP$B 0@"*PJMk>1rdk ``@g|=G@$B 0@"$ŧ 2)HRMuU|cYRBiPr#w3+ib(:}`  㡻 @)HR}(0#H#~1tYk/ &FuHP$B 0@":x7koo\.W40(3s&! L `TYY|1oHReee~Y*:}C 0@"$O?]{QUUa"/^܈#J&0I舗^z0>PGvnԨQ%L#H  `D(HDeg}bԨQYC )G& vC]bs:Ѝx(QQUa"?z|&CH$&c@I 0@"`y{z?/ $u+ NrTTdؙ5\}Vz"s4@"P$B 0@"*Y;;ngY$; `{\.W KH  `Dx0I*+=ǎ2)HRyeesQYRS/ӧǺu?s(UܺcOe(:Pjg$+LI<[fc0%qǻ|V ) `Jbذaqm/3~ڻgTV8)HR{G>k ="@PB$Bv;cv?(@7ݏQ__K,+W{6l9rdL<9OvZO>dOǹۧs̉XjU{DZ_WN5mL6mv.+Jk ~!M[m޼9V\+W{'&LwqG}}a""b}3gN̞=;6n5k>sΉ{7jkk|}``sɂ'EJ}KO̝;K;q9rdğXzu4P}s\<1mڴ>Ye]V{XdI477GDD}}}[裏n5cݺu;իWz](&p?ҳ*z-gy&hlle˖Ř1ck 6ı{'ƍW8cƨϷ^{mg}vj궶3gN\y/sh;oΤ<ln)N<7n\?VwN<98#"bƍxx7cɒ%ĉcΜ9޻?;;;vL<9/_+WW_}5^{Xvmlذ!^3;+wʔ)1eʔorK,X 9䐘={vDDk\s5qƘ5kV\uUqVk&Έhll?{x͋/8:իc=^Mtvv… g?Y466FsssQ9#⡇_=nָ#"⪫~o|#F'kUWW3\.9Xtittt /SLÇ}Ə~.y믏+Vl͎hkkxͅq)t҈XvmƩuDDﺍ\r@1mx?qt)ǎ~r-cŪUbѢE^w]uY1nܸw?OcYf%[ S ʲEgp? O>9n8#|EEEzꩧ[V<qiwW?>,Y{n'?/~18{f{{v=ꫯGգ ev/\m+VO<1|e];_1|»cх".\G}tlذG>ەmQUUU;p?O?O|G92Ə&M_|1..,//ٳg3<Ǐ:* ԣuO|fOCh-H#cc|e2dH,]tsqGyd,[G}Gy$/muxkWCk[GESGqnW\qE][bE?C<{6mݿꏊߏvYfŠA _B;7|sz뭅{^{Ns0zchmmeύ555&M4)QsL\x1wq7~0aB1"֯_JYf.Y${g 4(fϞwgXn]I'rH;6~XdIlڴL]]]\wuEYRĜ9s.mڴ)}X`A,ZK;a„.zokrK?kwqA[EŇǰŝwKDD,]4.\?|o1o޼-)[㥗^K/4ƍ 6lXL4).XhQ7pCL81v}ʘ5kVֿ bÆ QWW'?ϬY,f͚^xaZ*.86zh\y+5\'@|>,_<>`Y};g`w}˲eb&J:~{{MMM[}67 @@ѝqU#H#HRyyy]:0I)SdJv(D ݂ wae m `FŽ\.W Sx7 ѣGGEEE$=-ZTϜ9S @ʳ@P$B 0@"*աǏ:Ѝxꩧ 㣎:*jkk3Lg0@":[Fr1oxqIaxf  `D(2bg @i(HSYyĐaayC/H_ŭ>力JH @zl[cr/""; 0H  `D(Qus3p땇~555S!"7G *w}6PP|H  `D(HDeZM1~XE1nmq? ,׿WTP\ `TSY_;:PRH IDAT  `Dx0IGx(//0$mͅDs/HOL4mv.+Z(0Ƙ|>r&@"P$B 0@"*w76#{7_r%hD @*"2)HREyYL``q4@"p4+F:gyf($=^|J m~@:;;00aBvaD  `D($>(\[.""#\Dx$a[P$B 0@":Cuuu~[;SJJC @\AD 0@" @|UUUQVVa"(>0Ijkk ӧO @9  `D8zhԩ1~chiiUVgqFf`D({c?>҉j˯6!#9d a2$& f22fuMR1% J%Q{iծv^Z=~~{cu]@d0@d0@dt5!777:wLdu` C2 CXTVVzݩS=2߄W^I;v(  C2 C2 C2 C䦻Vz.؀U+c͊4V8}t2 C2)Hyѭׅ)mt`2RVV5 C2)*+b&)T JW5>s P .:bUZ*%@0@d0@dtl;~M9vP UVdVP 14@d0@0);;'; N @Fˋ~qTZ%-tTѶ`in+v=!8kd0@04<|2;nǏ"'''@V3cLڵg h a0@-9dvYjL`ZQlyD+2)2 C2 C2Dn ^|QSy&:J@rSJi@p2Rvvv4kcZ` ! #*]d4V5O [zDÆm]+WNLoi-Ze h !` !` ˜9s6zlNJ'2~->7HTc%a h ! #eggG-Rd8S]*á2@Ƙ0aF.T @zѲeˍoѢE-U`2REEE̛7/n޼ydg[& #ƈ#>}Daaa+g(@d0@0lf͚E-]+W $-Z0` a0l˟~'N&ePzU|d{ʣGN^A+bo`f0@d0@dt5!;'7Z% oHY9`V.jl*9s&o]1ԸZ~tD"@NwT#Heclhhd楱"y`2R"QKJi@34@dS@P㎹eXsA5\ d605n&T_qqqB !B !Ը &DQQQdeey. 2Jn K`?1x(((HCU[oPmF.؀U㾝>J/lh失"JF Nw?\'? q}􉯿:JJJ""bذacD"]e /'.=vtJLkڴiu]0P} !dD"._l7YYYij8&MC?>ocɒ%͛7C9$u3:())?cҥ;GN΋_.Z]w ߭]礱"YtfnR .p<ѵk(//_Xiii|GG_{ifqWSϝ;7CN;-y(,,LS5o׋Fnæ}C=xѼy(((EŌ3b=iҤ8cذaѵk4U\3.;p@%%%1t/bQ~joIII,X`Ι1cFj8M2bܸq녛Ǐk6Ǝ+W=zԩSc]wrkT&M⮻SO=5#"bqǕW^V/<zj_}\ ~(]n.]^{>h#[9?]t˗}Wۥ֨ k=zH߇_|q<W^y%M8MZj׿bqo_vvv\x;S՚[n%k ?cw7\e6? D">OO?yŒ%K>KYlY+~ ׸ꪫc7|;VݷO>q'o93f̈nݺm}& Ӭ"yxꩧbرQRRR9rd~QVVV]ۡ/>Ѯ]2eJ;Gqo(**ڪkT>q)m8F^{mL>ѭ[:uj;s]{w=LͫԶzP\+um_~{ʾ֭[a?c=#wܢdgg^%U"Hw 'N>%կ~_}tС999UZO>$ݢE*i4>sNc5K/ '>ӧOiӦEDD^^rd7Oi@Nwۣ:}qmիkJ=p~hذaMdܜgOn6< Zn;v&5YNZ3&ocСT `c 4ѣG}q=D_~Ѹq~ӧOn)|+VDYYYf[׫W[nuQ} .o9 7`ϓKZ2jֱct[,+H$]΋z(ٮSN.w}~a̝;sqQnݸϮEVVpk.ZnϏ?VX<֣Gx#''\ϴib}I|m70d0@Y7o:ujm6eF^zqwFEEEX"xJQXX'NҘ?~DDz\呕<@w+VĔ)Sbʔ))}믏*M۪x:d PkIvvv 4({クcuFAAA4m4:v]tQ_w}7Ǝ]w]x;FnnnF^R6[^"+++z{n|qeE6m0vao+?P l6mZ򧼼<%@34T)a۲jժxǓ3<3 X04@1t3 C2Dn 49sWo1PlWN<->7HTc%PL !B !B !r]Ԅݻw~Fp@-UO ve]v)j)2 C25HV<>3 @3 C [hmۦ `V\ƍK>(,,LcEPF' `CJ#}?ټ˿E'ۉF !B !B !r]ԪDD̙3#"8}@5#v=eH$j21`2SNnD]*kd0@dk*#J>vѮ9jTQ1{#"T0ۗM#"8u@ 0@d0@dt#HDEEE$tdeeEvvvdeezooիW ~"///6l 6t ѶiN6`DRwUݿɿ0oo̪U⫯e˖6!HDiii,X ,X;CQPPLڭ^:fϞ-F-[,fϞWNw)l&0ժ<̙eee.PVVs̉tf04 7Ҏ)mv̟??VZ/;;;4h 46> "V^}]|wQQQ]7.Bخ/ZS@d0@vhܹo Ҥ,ƍ+VHw)@5v Hěo\sMpѲe˸]VFOqD&M0Zjzj+ik׿5N;(**?<ꫴ 0);+b&9ɟtWlGVVV?-[ܬnjh֬Yt1nƘ4iRKX{o1k֬c.ٳc_" R5u]ѺuhժUaÆŢEj䦻Vsbz˖.؈o{ZO:Z|^zi}1f̘;,ylܹѽ{7n\mZguV1y?Okܸq\tEѱc8k䵤[N &GWNwIiqEEV'qŋ#"^O?ݻw\x1gΜӧO{`~5k;w[o5DAAAE]Tk5m4<ȸ+㭷ފv)""G\wuqGGӦMuGaaa~Ѯ]t.]_|q$8#oDnO?t.ѢE۷oVnˋv-8Z7g. K.n>fcۤIڵkDDzZjVԯ_?%d'x"JJJ""bw^_|E̙3':vX%UV? /b 0 elԩSwkmֵ~GDD۶m.5!Ic%Cm{6w83<ݥd7o^? s)իWI's̩55j_ZӦMKw lV\'|r,\0ݥ!d?~|G +O>9e_IIIt-VXQkGDD^^^i.mPyyy3&NR `)YYYcAwߍ޽{*w+WLw lc8cĈ. #`QRR%%%1s̘9sfB:ugG) _ڷv[deeӫW~/_VVV\wu~iii=:z:tHbРAqaEQQQԭ[7ڶm_bժU)H$ǩ[F-o߾7lkN$1lذ8#bv(..={nʉD"~ѣGn;DFm۶qeUi+VĐ!C⨣J{Ŋ1pmݢ^zqƢE6u^K/48(**yk&> 裏D"ygK,[,8#EQPPEEEѱcǸ馛jl իWc=|j*ԩ78 5k&_wzhr-'OK.${_~4n8~_ƸqֻƢE⮻N:%o>~*}3ZsF#G1͚5:uľ SRR۷){챔޹D"կbw>o#7@m۷oJ{۞f͚ѣC%K$_uU~QGE=묳b[o5^z kРA1{W^`e˖ȑ#瞋_|1hժUDDKѳg/SCL0!~ɉ?8;xWSΛ7/xcĉѰa*+Vo~Yf??x;pǙgK,?ѿXbE7.n喸;k; IDAT&?&T1bDOW]uU,]4>?~|?wuW\{Uzϣw/ǡW]uU瞱xx{z+nӧOzi8cǎv߾}㢋.J_cCƌ~qGqqq,X z꩸7ߌo=y8^k8qb+vqǸno64hPL4)θsM9O>#Fѣ7ߌ8ã,4hPrcǎqСCӽ?q饗о{ѻwO6Z92/BoDDYYY̙3'z/r9SL~ĉc)vq=ztDDt%⋈֭[|~)Z*wcƌ;={|1z뮻tρ'`ծ]:tht5#""N;x뭷b=k6m48h߾}4mt_"ΎvEvM6WIII,\0{xgR=ѧO8[n^{EyyyvmgCFOݻGe˖hѢxG?0?o2 [㬳J͚5D"_}UJw}Æ ~򗿌w=~a:v;vL~^{m}zՋ)arqI'O?|?*uqq_\rIq<޹s8s#Yf]wN Q~d n0mҤI^[T_exիW7Δc~x4m4_ut={-Ǝ]v>: {9˖-;/rrrsIQFN;+V]ƻW\qEt%7o3f̈w1k֬??:꨸kbȑ>9hԨQL>= ~aDD={no3lܸq\~1gΜ0aBrӣwޱ^{ň#b=ٳgǵ^^DD1"rJ򼼼dkEްaÍ~v3fLi&ƍvaѥK8c7x>S@M;coOٷx֭[J7nEEEU뮻ne]]w]t5޼yq뭷{ÇO?=ڷo:t!CDv}'xb1"z8裣m۶ѩSxGRB{J1jԨ0`@|1/cƌqi>|x<]cҥѽ{oK /~:uJo馘4iR=dȐxK9鍊nx#I[n-[ܢgϞ||qĠAR5cx ""ƏgqFZf묳Rac}'|2rrrohѢE|?9X}7GEEE <8"":FGD}qgoض ]|-luhWYRíMiݺur,^z{+f=s_ҥK=3_|E|gO?z*niRj^?ioƘ1cFte_'+***]ui#",X<@s11cƌ={vロ|}Kb}෱M6ѿd{ԨQM[?(--Aq衇&Ϗ1cl}|Mzꩱ;V';;;9d{1dȐ5l0% =C:u׷C{& .QFF-s֫gجY_K;I'lO8q7… S`/^x.JC @F;裏N7jԨ-^'zlr} Dw} ]w3flmۦ4hPϒ9rdlٲൃx)7xc>>s9'ezܭ1uxg#쓲oL -% 4rߵק56E|ڭ;zܸq_~9yDnn מ*xC5j>Uv۰ahڴ&)**J L3gά6e>ߛo9ܪ< 5jTrM38c=kl羲|ẵE|o !߯| /Pi'xbŶq瞈iiҠA=zt|W_EDD"={{쑲 ;kq'd{ڴi_=}Gnݪ| M[SƏ^7ۘ:K/lQ\\\mw̘1Ѽy*۰ajC=tPϭlZ͵>z)#ߩS(--/29ڵk 4(ezyo`EEEt =Zj>lt9-[,uovZlk>^[֭aÆFD_<6k֬vӦM}C_&/_^ڴi.))6e[\\^-q͛MI3):tCF=q5*^|ŸcԫW/Un #FΝ?R >xǒeFD|gq)$jR)On֮oZӖ.]^dI[w VnJ&5N8xc=L[jU|ѦMxX?`2Rvvv%6g 3r)1p}cǎ+"Mm^us}m%vqի/y;CJ{S&Wt=m}ݔLz-kO~'Oo1eJ/"N:餸 |e;dęg;'|2MmV^^{:൧;vf_wٲe[WfhժUJ{ҤIU:o<իWou߈%)z0ϣ_~QXX<`2?yʾ_if۴pG^; 1cFKUѣgϞS`t9kUy%Cβ6j;lذ7媫;v裏|w\sݷ&ք QGƍm'Ov%4S{l֌+++۬{nzִ-Yxqr]w5%:CS^r%jժM^O>={\Pn 'H$}ƏW^yewy[tߚg9{fS6k9{Ƌ/|ҥ1cƌ+mF9#m۶.؀2dH}G~~~+֚c)yq1f̘رc,^筽'|~ӦMKiW%,V92zOӧO?0N?6lXJ͘1#~_F#K{qwGDĬYgnݺm3fLDDdggǀ*WQQٳgOJ~G}'. sřg HYxst-vio""_K/4㎔rmoVwq&ڴiE zc75ByŊSOYgU]v%tFMd&#H1sOuH5P?-:M6OG^^^Y{:շ~;Ə^sYg5J/7x͕+W7;xck{ݪJTPrg}v!oᆔȑ#'?I3&eԯ*:tŋ}ݷ޵֭yŊ| bJ/"{}ߺ_*XhQ5TѼy S/N>ܗH$>޽{Gnb+kָSN\})ҥK)k5kV/;차W^r-w缩ڟM}u?z͛7OnNyO.]|x<{;ZxΜ9֞x]kF᮱I&?e۵FϞ=?=zTFDt}'O~Q\\5f͚ŵ^+WaÆ{oVz-U~5jTGDħ~guV!ԩS.???n^J7ިoh߾}ʾz*:t 4]w5֭m۶~8~ȑ#SFn?~^zis1)ƍ;wǮՋ4hPԭ[7y晔 .d{cu/\pш}Ο??>쳔ԩSr{qYg[ocƌN:GѷoJ'Ƅ ""o4+v*뮻.E`)0ӧO"`c_O;uiQQQ^Ѹq޺Hn{1uԈ~> oJæMi߾},_<&L;wΝ;oo֭ヒ'FDĪU矏x }xwo}Ɛ!CDƒ%K"Hā˗/ɓ'_x衇R.Ѹq(++iӦĉcp”gѰaɉFE^^^L6-}0`@J5eʔhԨQTTTĎ;|!}Y\2{())XlY̚5+y8cʔ)q7ǭ޺߭{챱bŊ8qbJ(U^^~mr]w=y/~H$ދ'x" K,Iy +VXjjܸq#>cڴisE:u"Hg}?pܸbM}hѢ_L;9sfˣpi&MZo=XxqrTG=\l2ŋwމo=rwމMFYYY4h 9ʊO>9fϞ'ON9gձxdp$F2b>0ފ~f͊((([n,\0MÆ )~;6m߿+W_=xGSN<94isF|;ǧ~W^^'pBꫯK.I<)khѦMޣu1cƌ;wnӺ^{ԩStZ3rsm~3&ƎyDYYYE۶mc7ԩS_1o޼Xre4i$<8餓SOs.\ 6y1w*RZ<ꫯƼy󢴴46m|={L{mw}w\|U}ݷ(- /O<&L*".?ϢgϞѥKJϻ袋{+DDL0!yxWbQ^^;stAѣGڵzhDDΝW_=o0`@xq5l?#&M8`}[j6~m[ ǐ!C*ܬYO#G?>ƍ\^8[oe]n4j(~8}6g%'''sq饗AT w'*l[Pj*, ]` !` !.jBVVV4k, N @F*(( . e@24@d0@0"JJJ'2TZZݿ(,,LcEP ` !` !.U/~E|^6tʘ6q{"0d۷Jw B !BޝWU A("ZulVZiס:U-[/ZS*b:*Z+U 8b$CCC=5ILyk>AɇI_rx>0׮]1$t59JJJ) [ ly_֏alvEb>?{+ elTTT$# #$`Hxeӹ> &h>C%#F۷O:f)J5517r 0 /t5jTIت; lU_1k֬clӋ/]tQL>}9䐸c͜ hn (o8s#ãQ^^f͊EED_㩧#G&hJyIaNg}v1w?<裱`:uk) 0@+|;vlL0!LkL&o7ߜ{wߍ~HvZ\z[o|#vq\_oh@ ٳ㡇L&[,_<""n$H7bH5I<*bnŹgF &H_I:A5两T^^^<ujժ\{ĈM h!H-_ذaC̛7/""FFJ6ФH ұc{bݺuѣG뮻44-Z_~y 4(M[ґ& `zNk#<CM: \ "W>ī+W/=VZt<)Jl67T侲lґI͞=;>ӧO\z饱vܱc'hjn @*mk&ߟ_:vLth_`"ڢy n}@y[=\RRhY[fM>… o[ QSS3w8c鑗:AH#`&?dp@ϟ?O;= /<cƌi@sO;Ro߾qu]W? %0@]x/9`))s IDATNtMI 9rdݮ]MI ޽{ݳgM)?i>` "֭[kj` ]H=1YrDDI'`)4@0uԈ:th|_M8 TZ%Kģ>/{/~FAAAz뭑DiR~vqeIGhZmٲe{Ɗ+"???9~ś-++1cDyyyy睱'h. R~~8dF̚5+4 Ɗ+""*nᆘ:ujgwqѷoXlY<#1nܸ(..gy&:蠄Mcر[pa\xQRRڵA#<]w]꫊FNCcH: ЧO#@5jTh"rK\veǢE*vݻwLj# m0@+뮻Ʈt Q {/|!-{@PXXGN:Fj&`&T^x!1b0fd2h߾}1H0@J&:񪪪-+))i4GH8fFLм %RB %RB %f̘zj$KTj߾}s9}>}4S @*e2СC1Y %RB % @*ǭޚuY H 6$04PϞ=O>I<:w=R@?rlIrC?sW '{bq0M.~8bذa1`1bD\s5QVVtfzcVqqqd2c*_~y\}qGnj?~=x @*e2b]vZG/=yc_yl#HիWرcZ'Ou*v饗ƥn{.""ve @*#t hoG}4yXjUl1lذ8;Nts=|SNl{1y8uC=㣏>/1iҤӧOq 7Ĺ_}^y\?qGAzkӧAN@*ek52yL|НwgyfD|\2dHr)qw=㣬,""oGN6;w1z:ujL>=8餓\L&N:餘7o^\r%~E~`R*_갸.NZ173$_zGϞ=#///""zWXv[YgmugSNnOzq饗ƒ%K3ΈgyASuܱ6ǔ4}ht]ti(//v!Fʵg̘~.~޽{G~b޼y%^2dH>tSO{6w5go6|f̘}ƬY6;fvʵx6 @̞=;8p`}s^zic~… cܹqwlv̺ur]鹁Shjjj4ׯktuoΝ#"qꩧnr<Ƃ ""b#F97)`6glذ!ׯݺu˵ϟ1_b{,wM?bŊWL-ӧO1c6yo 3f̈=zD&ui5s hѢZ;ώ[dITWWo2O:*=ZW׿uDDz1a„6#?,O=TO> JJJe'|2-[bѾ}Fyv]1I:ijiqet(l`"89ˉw&Vl͚5u:0׮>h+x3LL2%&Lx`|_QFEcL6-O~RL&w9DYYY,]4""L;f|1 ;ǕW^?я/K[Q]]~zN:5,8 `\n?[nݺZ"^Gxs裏>~:0@uUYYYߡCƈ| 6AUTT껕3$OTDK: ;wnyׯϵ3Ltر" r͜93.8c;F8 guV<Û??m.]?Ocȑѵk(,,}7xgׯ_Z:bŊ\[nu;h:Nȣ>GMUTT[ooVvm1|馛bԨQ $mZ&M/|,XsOs=o|#nvmҼk8p6|,W߾} Utg ll@('[nUw}cwNJ+wމy|x9sfva1eʔ=ztBƅ^kN1bĈٳgҥK#"{E#<IEHD:Khvc֬YQVVV.\k+Cˠ2 g٤ǸqbڴiQ^^7^7w&; 7'|r1iҤ袋bÆ ׿5~-(ϹtXlYyw幁污:n_otMqUW}y+`Z}`=и馛""b?gΜ;&ԝү_xꩧbΜ9o}k??r_~95kfgs11v\„  ڜc9&wo[裏ڃ #F4i>n\܂dx뭷bܹQZZ .իWǻロv6?ue7ӦM?0vMnNo=""L?8;̵/&ԍpjjj/MK.m_ŏ~6ྦྷgG1(,,yu_RRÆ W_}5_lsΉO<^;q~/;뮋s9'vaM=ӹ Vn@?1ƍsi=555u>gҤI6?G޽|^׮]]v-^}ՈXp瞯GѣG=@S{7c6_6mZX~}z:;hРK.wuW1~a{Q\\SL|%'h)mLկ~U{g <8v}ѣGK|QXX{WٳgDD <8JJJM!W1AINuXҤ9֯*RKUTTk|x7u.,̙F>}Dqqq'W_l6⊸cɒ%q9N;gώk6JKKcw|0sz@('GU=ꨣ⪫/| ߮]pΝ;7ӧOSH3g&&hѢw}:fڵ1qĘ8qbqx%\|L81MzjG׮]cqy(**j4ȵĭޚ`d=qqmu̜9s_-)))ihn_jZ_׶9)$[of?i}QGoԱRcԨQIG%-0`@=mڴm'2N?ǟ-l@  %RB %RB %-߿#Fh$P z񒒒  ) `R)???@m8Ԯ];gq hPH `04{C I:qcƌ@S jjj7߬bŊ=#8p`SG"L:%Uu) W^ѣG{>`/""x8#cƌѳgτSBf`ܼy裏!CC=+FD=o$CR,ztZtit=W__W79dlHZy Mߎ9sDDpH?`چ#V-yżyN-^YYY?#GƎ;ѽ{8⦛n:Oڽz_|1^{%%%1vxe@ @]qѿ߿i=ѥK:uj}衇b1nܸԩSL4)׿ƯXn]{^{Eii6z7r_~9&MӟbꫯƲebذaO;')`Yn]\s5qGʕ+c:1cG}~z=-ق͎c=""@m ׿x7b…Q^^?EtXX"n\3<3׾8[n6;k׮QUUؼ0]t.]4\GD;CQF3f̈?veM7Ra0^dIr_ExK: 8O=T{os|׮]O>^ď?fǍ1"Ҿl6n6U S6aw=JJJ$t̞=;8p`}s^zic:$]<`ҩ2bL:X555QZZ'|vܬY6;fv>8""̙1 ,}'vu:=7y m?Zgmukϟ?.䒈xG6{g+N l0@hѢZ;ώ[dITWWovGzj|͵4x{{{Ll,?$k͚5u:0׮>hb:(.e]_;(qQ(//؂㭷ފ3f… cȑqya`^_1{c[P^kgɥ;> 2W>J(<֭˫.lVo{{Gb`6n+y몲VCڸWQQQߩSƈ| m\ΝtsL&;vlH@)qկy+VȵuV罃o!@׷o(((WZU>[8p`/?"-Vؼvc֬YQVVV.\k+Cˠ @*Ⱦ.sV|ry""B@ v o6WTT[o:ɲuz(ڔ^Wѿ@q衇ڳg9sDUUUa5I.~c9&#"ߎҭGsAň#4P7 vm'pB?eʔ;s .r @*UVgUlґŻ#???""Xrf=ӹ cǎm)J5/.}U$Guuu_>_zu4hP\}QVV7ƹ1eʔ\HT\u IDATEEEkQ^^˗/7|3Xtine]s̉QFE>}8={_|qdٸ+%K9;S̞=;(--w9|s=u @>nd@#XhQ[vژ8qbL81ظqb[&NӦMSO=5ˣk׮1x8{^5K0mJIOv?()I44fM2n/dn %R- euK.M:Fju-[ )ݻwO: p hp04_1oߤc[PQQo@c0""N8ᄤb@r0@JT*,,N:)Ь\  ) XtiDD̛7/""JJJ M@Tx ?csϭ5&6w,hRn  )  ) @d2脓@:mzQeee$ϣj6>Oˣ @ѣG())I6T&¨=f͚(**J0 f͚ZBW4ZUV 4@+fcժUKFq2.\ JdXpaTVVzs %FӡC(((U8XzuFΝSNoI&b͚5jժMѾ}Q_ R^^^@d2ѻwxk][YYeeeQVV`:u R~~~1"&.I%.EEEIG?2dH1EhѢMn% @WPP{VmhEEE1`ذaCZ*V^I` 8:w۷wVJ&dCѡCѣGd٨qkh$D^^oJ(JUUUK/_[ iL&ڵkt H-   @%ơ  )  )  )  ) Iо}~Pi @*e2a-RB %hÆ NBIZ+_ϟ_& ϟO1R-V\tTQi| PGVJ:@4>:ojg/R~qw߲͟wy֭xmm~i4{N{Z^SK99k^?_rZ~Zj)Uki~R2&W9o'tRqOsε!C4st14un5@h4Hs45s4ռ~hZ{@kUkx]-%*y#WmQKyh-j dL"Gk]2>?H `PH `O:/gqFr!QRRҪ6rsH uUkyM-%gshkh)f.߫ZJFkU6~j dL"Gk]Zʟuf٤C@kСCs^{- `"`k/h Wt֪)  )  )th-wƍZ.g Z-el6|~n  )  )  )  )  )  )th骪o[̞=;VX=zAL&x@#)--g{~Q^^3f̈9sĪUw1lذg}9-iRנ5k_(--uE߾}cĈ{6xNZ}?Ϩ~%%% r0lAuuu\wuѷo8cҤIW_tP 80cPZZgyf\IGZw}7ĠA;+W{ǕW^={?<뮘>}z?}7F=X5kkЊ+΋ݻǘ1c{~:K_R_r5hqꩧF=vlDdu}grjjj~{ ~UZZ=3وȎ7.H@ f~^?7١Cf#";`̙3s*++&LFD6dǏȯ֨%Af.و~ _Ȗ掭Y&{g#"[XX[䵵5hڴiٮ]f#"{QGe,Y;|'lΝ?p_}n㛭 @yyy|+_>2L|x3~xWbݺuq'{wW^8CCI6",^8.~%Kć~;^/__㭷ފxbȐ!yyyqAEeeeXxq+{ӧOxaaauQ1wܘ9sf}7Xoͪ-A3f̈#<2V^Æ '|2va񢢢8g~;x8#wu>dn я~}I'_7;nر1|/~>`e6x8ώ.]rqTTTUW]O>hD|آUUUqE59hZtM7̙3#"8<-ٻw8餓""bժUA֠`}##F/}K~/8Pϸk"FDD^bԨQ[=#yMؼh{x„ /vy筎?rgy&M֠ =ZT]]^{m1D<]wsέSvZ4A+VI&Vx_rmӒ)'V^<@/})2VҥK^wdi0u[o嶎8yСC[n4DSAO=T,Z^s~wg555~к5{z1k֬U/jv>/Ɔ r}٧N}vs=W Hu0bĈ\)֠ٹs0`@?eʔ:j-kPCs=C[QO  yεkjjb鍚 hyG}fj\_SA1o˖-yNe ]t=zls8p`s mmAfϞhkP6;u;wnМa@5hٲe{;c׮]u>QO,\VW^u:oqzeZ~~(((wܱNkG}k֬=޳g:g-a jhgksF +VwԩNml?PM5wb[i-kuc u鼍֮]h"|~i5Ⱥ@]5Ⱥ1`DQQQ~eeexku h-kuc Z:ҥ67) uZ {6[~Zvک2wq^u_3@(ܑ&bjJlV CZbe5\ ,MM-ZMW LE\d0 04̙{ygΗw^s>v>ڵKn*oL9>?=v9h['9(rr^{j}pF{{8jvN~5WS:uTZ9hn߶ \g{/ov||~sƏ_{F_кsP3psv! h؝ꨣ6j6(йszMZ#AM1޽{7j-SK9me|~1bDڶmPn;я~NYSSsV;F@RAYE~oq{.˖-kpΖF զM|3l׿իWh@yu-Gl7üy,)bG:x[5磏>|Z9hԨQ޽VWo>'xHUUU^Ys?~9{>`,YSN<8p`_砍;b}Ksy2dk w93+?O6;7n\%Fs9뮻n/8 1.]Zg?ܒ#FdԨQIp }7r7V/TWHLWZxqLK,ɓޥ^ڨyկ~5}I̚5kW?կ~$mr7طi/$7|s76iҤ̘1#I8_f5[Zgu_(F]&I̟?~w2o޼$o.O6f̘m/ؙAڵ7ߜ]v%IrEչ.,_ktw;v,G..\Xi[|yqꩧIm]wk)q>2W7p YjU}z:uC=4m۶c=e˖C͕W^Ç7zΝ`͚5y?1VJ.]2x]ڵk#^{|p_vqZpa~̛7/֭K3t|M\=-YK9 1cF^|lذ!{?~O??뮻6wʕ;v_\ow;Ç{I߿+9L<9I2v8Z(.x;sLTUU5ط6{V^$9ꨣ2}-c}͂ rO0x,\0JڶoS-+J;H۶m`AڬY*A]|Cj> ]wUPWgΓO>s97qL$]Mkԩ9餓{n#^vL,`:uj>O5>/b^|vc]lY.]od0y$رchY%dɒ|ӟnTmWիsZvm9rA51в0@<ZƇu}SO=Uu>}zl?@ `Vj57_1M4)Z)'hV^YfU{&PSSg9C3p&1sBR[[[ֵ.?ϳbŊ&1sBzVaɩI'ds:v`Vh{v1tP3VҊ+2mڴ|N>}l^ΡmsVQyG+Ҷm>nذ!K,}jv~{jjj2nܸ&S-OcSO=^{]֥zzyu+yL4)x;r '4ٜh , lToRް#HMMMKi,\0w_FwM6ok9Z0@+3cƌ|ljvOl2vSN3v&5S;;0@+՚ J.]ϽhɓөS&rk;vf`VdѢEyg+ۖm=ϟG}4|:vd:v.`VĽZ[ɓ''Iƍפ:v.hE6Wkuaʕ+gҥYlY-[:+]t?G}4555yߝ#FCoܹoEo߾G?ݻ7?ydzx;~x[c{j}W2mڴ,\0˨QǬ]5IDAT{dɒ%6mZ^~=:3nڵ{3Ϥ:x`?if^äIҵk׌1b7d{ue֬YYhQK+ߟ^z){gFnݺ5i+w]9ҹsիW /nݺeȑs=7GSoWk;{w>92f̘y晹 >{lnt9mڴ\[nw#ȷ^:C=}+_J֭[:͛|#9x^YlY=܌5*˖-k~mۦիW̜9jַYpag̘1#{W:묌5* ,ȝwޙc=θSf}ɤIҳgEN;-x`|F͝;7s͘1cҮ]9sL+93jԨz9gҤI9믧k׮wgڴiuϟ?? JN*?xodҥKoުү_>!dذaYn]oi;묳ҹs|ӟ|uuu $o~ɜSLٳӭ[MLN:sL.-ֽx}92a„TUUo[k}+u %W\Yf֭[qe]vd޷^mݶ77o^N:_jcꭥGoޫW$o.w\cKGQh{$y7d7UUU93$7t&cgΜ>}dРA[?PnmҥKzf͚5YlYz|߭oԩYn]wCi۶mtΝ;7IsL~;̚5kұc$oW_}ucbŊMn$СCӦMMO?~[_̈#rglmuժUIRoV[Exk֬wߝ$9$/BV\vm'ɤI$ƍb߆->w$ﻼ-o3 od0aBOyչM7ݔ3cj9li;ͽoz;.wyg~K.I^:/s5%Ү]\2EQlWSIҡCիWW\2_җҳg\z4hP&NٳgcXk̻Fk&}̓>oq#̆ i{$_ɛW^^q>`^o]p ><>l8\sd=?KnS=۷mݖ}e]rk_ZF}n{jM\>zȐ!?gիWx\{ԧ> .Nln}dcI]0[o66r7/R~_dѹ馛 @ `vGy$I2dȐ3I2a„EQ裏dt}M˗we$Ɇ 6i[dI͛.A[y38#չ{r%THlK/YjUF38|=$-ds=x x6lHv$/|!6lE]T~'N#GVln!ɛ}{=&oW]uUdC8{yW3eʔzx9ꨣұc5iҤTWWOn[A%I/^IUW]=#K9mks1tI;T͖,Ys=7˗/O/u]O|:uj*'L]v%SNܟ6Ix≜tIk$?x7{6lXW\>;'O?Z*ɛ~~7YƷ\re^y:mw\:u%KST1cdĉѣG?'y3[SE8$ɕW^߿M}?f־osg&yΝ:u>֣(ey+7o^|Ϛ5kRSSsb=wӧgѣGw}s/ϠA/|!cƌdɵ^sSNСC>O+_Jڷo$yr)d޼y%w۵kgҤI:th=}vOfYlYꫯ>EQ䮻 7ܐ_|1ڵKǎs'ΫwIr-9Ss7l`|# /P mү_|k_9眓As%O޽{O%\Rg'|2_~yf͚mۦSN9sfĈ3gNunݺ哟dN8m{[V>яf̙u~ݺuѣ3q:m֭[=zWUyl7o1;[n%g?~|vu$Immm/_gƌw d̙9l{lOW_}1ͯ)7 'P-a`,Y$gsis\mۦk׮ڵk9~9sw7YhѢ{9r~Oo2{@zSSSSIW_}m6cǎm9i>3dȐ| _d?>:uj9~>|xsE:wuWL/;L(v~=XƎ秶6IRUU~ki jkks]w?if͚TWWgڵ߿FSN9%=zhRف}s-䪫ʺurAkqgA`4@IJB P`$%!( 0@IJB P`$%!( 0@IJB P`$%!( 0@IJB P`$%!( 0@IJB P`$%!( 0@IJB P`$%!( 0@IJB P`$%!( 0@IJB P`$%!( 0@IJB P`$%!( 0@IJB P`$%!( 0@IJB P`$%!( 0@IJB P`$%!( 0@IJB P`bwɔg@IENDB`unyt-3.0.4/paper/paper-final.pdf000066400000000000000000024325161476461141700165450ustar00rootroot00000000000000%PDF-1.5 % 5 0 obj <>/Filter/FlateDecode/Length 15191>> stream x]w@I߄A XQ ]@{vywyyw{ X(RIQNODII&!HNjlffv7oރ 9INr$'9+' w^˻AN%-q~}wRelI4&0Pp~q OSGOO[*rSEUEeEu9TE_U]E]¬ii/+.d&zdRG_~uUh+.?+'_í`ڻ̆rr>~NuV kQWKWޮ )pqǘ|YyMqhoE/mśK}_{&sB/ffxKiCUm&CMkoob" ڻ/TmqM ?~8?~) 0t2ACu"*Ps4={4)G"M"ZiτNu7ݧ )q+Ia/v$v3ӦmR_mʋ .fi%#$8FoYá66XpVlFp׋D(F7K ]-/Ֆ뚱*3Xun׉DͧTpiJbZ< v~L /$~Hk+|q0kc_}RM_F Nkh'S 5aPPx$[ 6ת$vu֧ ů.Ө 6+Կ_I]/v 2R#w@ƌ'H'E?9+V=? 0e܇/ LE&0aRNN"5 &k`NdMSKKYGxO% /υ(.ӟ\B S'Hdso=J|JmwIY,~'I?R}unƵ86JFL%ߙA782E6 I.'ӋY#FQ';`G"ő{ף؋Pzycѐj܀vcݪ4=y@lviA58|Bq$Y*pqEc.nzM/AK|Շa-vv+w{!j#qY$5Jv/29]tɦ6<ՇH=RXʊd=4BIce<"ϻAk8H*ӮRCҥӸK.sV|GTsJ{RF"7BZe580##@FbT])#ڳ7Hf|jwI[S+K{{=*'|sHX?܋i~RG"sd(` $#{J$se+Di$ZP:s.n$ŭ['zFۥD(Lp$itM*daPxr ВC9g(W' j#q:@b}6HIp^G;lKr)M l@Ij[`MHq`:^ Qۂ\vtoM?wHJ`O; )ptߦwvTG┫2WH-0CIpwՑO9`r%}AJ(`s#=&}$GVL&5Uity r 2Z`iEIUIh<H yos***[DSkiꙙt0`MȎAZlO+k$*(jjk21Ý\8 Av\T/E_şSٮ_wa/k1lLK-b>iu2j[X1en>ݶ'^nH 95xzztC#87o:{r; '"tj#Q$SPZ@C$,J=60H%nsv'9$̑- B#*= Jo, ~-i]+8&Q2Oz2>hdQh|O9Lĝ4SZ2D'R]8D8t(^dP /JjFХW_ (6Rs$[鯮]=xf[>@"m7$͕CzKpms)^Id~HT<@Αn8$څx7 6<=RNy$BUW %VڎFZS;.^yMu$BS{ H4[=?}7yO,O3Dhm5jфxzsǃ*)DCbb? V!AGEtcDhJ hHϽP [vވP/ozO5$F "EhVCl(LƩME`wP~>C1$&SEn0]P2W>#R k*"CQ ۰mdխr !Q%?2=D2 O OcB(5SZ) ^=^iFB5T 9w&#P"SX:eAr*"6t&E$liT̴\j  =7&XBSe ]0.Hb8<0R`-"R\J ˴l}cnRmqFL$>NE$T>tEx)(y#gN @$OE$zFp[4Q f2qhEkwyH<@lr׼cqIx8xpb dyd-Śt'"dmv?g*!ckG}DbqifN0ğZx4z2lu$NNA$&;o2f D8{lwܻlG"| "l$+\1Ǚwٚ^_m$Ug??Bm"'cj}~L#qeP4Ц1th9ޏ0i%~v&FCDQ:D  TB@Wj"I?#.aGFNg< !1n>Q![G*ՂQPӴ0mTک&I |gz?8EFH߷2+K8lnlJܳ8Yrױ*e tJ#wb,I]u 21fD"cgR J9}LxT+H?8۠l!|@PI!OdӴ(DhjܧYŮkCGğ':g\:Tg .[OSۯ"N`c "1pp'NVy ~;IM)UbFy$B?MH&kiS{3I>g?-?${H웬N}$$* !nsdDbx[CH$OvϕHĝHl K#9<%@ !(^l.Ir$bv+]ZH}7kH~G7mHQ>O/ y`>ڐw($}sz;;^%abW5|C&=LV C$7a.ZkՁxkjw0R  DBk,zO=*4ٝ\`]J 3c.ɚVXJ17Q [0rPΒH3WFSDHh uD~?L(-.qBX\N.Hm^!XU4Hi08锊u7׮'R%kϓt+ _E~Wv( c?&yLNi2bI Yzmi bM`'mNt*'BZ,a2J#B_ 鲣1FaI+#u>R cd#!{a˺عz[Sc_wI3#( H s]!?(0UC]BDyI_ƀ(!#>پڱ24{RpH{L"z셋l;J;&M9Q`$LΣX&,gĨ}&ib/H1羴s@#HL^+ŭ RHz̝&Y@ʮ :w/,\scs-tn8| rT ]`Rzn2 DRqW`4 KX8Oڝ#~m@4Q>RFHeI+o!Dy2}<|L.?*uH8~}s-d0I4B?n8傓2d?Кrssg E2 ?'U@ TF|T,fߠ+m>`)okVGgpX5*."Idn=Pya}Ԟse$\G{a%$NX4-5Vw9 $2#:slMX 9H~JPNk'ٹ[e ޼/I6#1FQ|o3I/Z2L8ܒ~QjyCTfa09"?/ó_K[9|7N9y9 r?77TR13enn&  a.+'&!yef1i^mRMy(qm+ذ Smf3YQQBA(M ~=JtG ^'})9:ncŋ7ODﰎnI9yfG#CM/H=ҭ浑2Ċ ĈT?TW4 UۭOvfUvYp]N^AqΧڪ6&!^^}mݻ6Ue,ZO D?0[RSCEe]]}c]c-@A]M]CS@wEl &cnaSMK 2GiijJv:}@)<B栖)f/eH>zJ)s+*PبRfVH&WLĝ*Ԭy"Xf !nD`Ӏ`RfVa#4z>tK=28P8 n)K7oJ&e%#p.K+3D_n8#f Zƪ2l*@*Kz&kK(?ۦ3/{b*$`ݒ&fO*b۵VXa՗fq)s a^ $24\G(LM Hӭ[Dy!]l$մT ^in@{xҡTOQP/ #D1$iJǰf+LB5QB兵T9uu3LFj!w` xN!E)27SEi2X+؇W7'yL<S'V=[jl]1"͞R>&X3z)D@n&*$U aŕ"ɋT0Uo2wh$"D{%rzc]\dQX&_2S\0Nz(i"s6F4S1ٸKkigdeIrA_!PR7 8+%Np}BI񽠫Yg4tŋث>)|S5̥ 0 Ru@iO'kwɓFl Ԅ{XOkO7CS';Xy3NRyʭ%- \#ʭxBbYG3Jce\9=udb80}æCy? s;nR2|cT~u^?E0ZNRW[j.#1TTtZpu،AbE ׌w &Yˠg݌{\)D߶k?xKVb8GP pmOQAQ_sqAQSQoLHM%ku$38{Ò)s4C{~8o.,E]wGX %oiO vόٽ\>HQ.>& 1L5I˼U=TɊQ!ฌs=[s/ʁBfnzL 湒/q˅Zy~xL&|x'Hzng?g,%Eic ;xGשةUeAw44<&XBX=$MR h):N$Nb~p1S6GM!LKyI.tri!H).a>$mrP)L0Nօxd5z"c}ԅe8n6ZmfttT!$ $xw=eG"#Z;>_+.2ؔ|;-lct-ʎҋt>yQh=+C#[_ 6}w(wлog##--!5SQ~fۢ HHoӡR`GղDm0ާGgel\2Iİg9>JzZjJt̀Zj*?WUմ@H(.WZэ,-lz,yQM0jVYӘMHcBT =^-]hzXtdBaTW}LyNW261mPWSt겢_S IinZfDk4 -=f]ut5TTU[qfscc}C]UeeEJ9 ZMhkki*[ƆJ|)كN;Ҵk񨨨hjkM YaQuu5-Mr|P,9C)yIjl;~=(*I3$]Rr1,w$'9INr.{'n6ޑu6 zX>y]^U_IN2#U]xiްSb ~@_73zDuhFN&L"*jc; sm%,LqY۠{U uJ>KcDHDDW)R0Zkr(|HAHAbJwH\~ïh5y.Gy47_8=D5| gpb`E0 4ȧQu $@⒣sBQ"ī&EǷM왛-x_6ͥ+x-sȜp ?(\RC|h҉%L0 y@H\NkqM-$N ?/m@3|V˟Ѳfa|T:ɗ32ŗ Z q c\1b ۯd"QP~_,m۟QW:3qe%A&[ho.A6ǝBD6P n4$.Ng}^_ؽLΨ>02G=!9͑ř."oswpe:XOfJ 1H8hՈL(la#?MݓL(TbF"͸R}>tmM Ze%בUCK[]=p괆O"fO+n"1$ZF_ldGoHԹD/GUCGKcU.ԡ¨(i.a2Q˻o@V0T!dwn@XYU;;ljHo߇y&d{L~JܨlbP˻u[0pYх{!?)h*1 PAԤWU򖡮uUR>m 1X<^v;.Tp,׉R~z d2!u|ϡ*T[%TG]T1i+As:Db%#4GqÚix*!4OΠz!PD\j d]GS]";"8yl*zx:ꤩĨ)K8Pp]GϞWj.I9q3Ы&1 z&Y]s~0l1kʳ!P j(yu,Sj(9&>f І?}̹rNQ$ԣ-~Fbv6_ً.dYE&]!]tXkIiG84TV~;*S[Z2N]F2~$r 8 >wU~{=vC$_'á'ADzmm~Fgv+X}czG qo`$6Ȧ&˷| 'b+um7Y:( d]yhै+TE5?ٶzy)d< x~Nݍh{~[_i{ f]WC"&Ȧw >H9`Bx]xU#ਲ਼gsV@=\#6D_8oNo!97w8W8T?c,T 5@YA{bG:{+|yԷAb=o~cgڽ7%j?ܱlCݝnqfΗkqϭ,|9yWUb*mX%ran 2-+XsX?W+jOIh$oI]\x\:B1@ w+exH?cد}@b>UbFp?J>L?A"oND2vAx|)ɗÛöCkl7be}yHDC/]K% -ֱqG!f)|B3/&up(,*HO5`ƕ&8Ըdp*m$)Ybҧre6tp[g=\b+NEwI3~ғ!8uB>lH9Y0XqˉFvs!Xĩ|7zVZr\gbTqܹS[s!,a# 5ASp`jgHtmW] ΘG?6W}LdWOwzt@$“0"nn;OQ!"w2\dL 7itME7͍̺C`$%=drs=bgLLŧ: $` ;7̠lΝ#\$zAЫ^=9<-` z1#$"bIUWS\Y2VBYjC+zBt>r[kb$@&spK3H3Ow f,6: l_!vmPH|ӏ3i%) == ~1@ТDh 2D|Du9-l50j7ɰyX~#@$ g38H<6JJ?./p_6F<=woM$ ތ EPX ox6Ju%o/pb;gbdɠ$[;ifG4i7'fD6wjo?e [`?t^}ljr=HD$W;Pee2Ks*vu* $o+|gm{6"1֕g-gHAF]#KԜ˚ QB*·^ˏa=&c@QS36$z9RbSdÛs"lƀćp_:%'"inXˢNoN4OA"Nn3VXBlXxr.z.{jIȖ7G {T6!$D,XHD0,zjŇhKts.5WDxsj=?4:y5#q[\j6 / Hs{C&[z:ɛaBxްxڪca־V !01lZڿX-~#q^ud5*0e1XE{_C;ׅxmUZ#trU._u:ՙ5I  ,Xg۲c ئHs򐨙ҋcHњ-&( P--7x)T06,G ߙ<QQ$g :ʀPS1]A=2݉K3`9vMDH߈dRй՜M*>}m׳أ}[:Csv!5^ngC"}Gs\7%ٖ#<Q:$%TAB$*?Fs3o{Tf{CL{u `/b 4q}8qjDE iQbcv (̆g qajY9}.!!|}"x률Sg{&\rG g+;F- %F[TڏW%PH j{v?<{&DF&5%/mlh @"4*"-v;"J]hG鉚|a.llqD#N6sd#A5Ğ|f;rdžDh-=7Wԝ~ݲc$@aHBbO}P3/,Ch|,Ą!qE /=Il?HmQHkȪj]+4l{se?}d.h#I6^DbzЊA.jWL!HD4NN;MEoS^PJ<@l.Tr8}ΠOd3QIЃLrlAy]l'lIrB*l %{uE oJ]z5^o d|H̜} FnWk&nबI8=< C+3j3*zTv.虷l!V`px 9vO5{qش n28^aV<'{QFG ).1!=~d gQKL3,  Bav$2gxfN̡eXI}F krdo€% zn2tdjۣq֙3h8zke5sCѶ3[#7Vtul,ԡ7Ay>uz' n`%R.>FWX]9η?;Fͻ/ZXߛ4h_N9,cW]6ĭq%-`݌Qw// cDxnluH:}u0G-KfXi}t$=s.ۣdb 5ԕ4 OQw#LU!ÄlINr$ID9$G%es򞐓tvi>/&'9} $< endstream endobj 6 0 obj <> stream 2 5 300 1 300 650 1 249 2017-10-24T06:10:68 Pixelmator 3.7 endstream endobj 7 0 obj <>]/SMask 5 0 R/Metadata 6 0 R/Type/XObject/Subtype/Image/Width 650/Height 249/BitsPerComponent 8/DecodeParms<>/Filter/FlateDecode/Length 66092>> stream xgdu.vrowN` Y$JH%~*%ÿTg^IvDS|Y( 1 09;'wz4&a03s}O޽[kEQH H H H H H H rwMȣ!l6p@*Li4uظgɞtІ+)\5(ǭ-('ݫ`F QBO^}ՠnSȢ 6uŤd_ BV c͞!2P233Št bYv21q^# e Z2tW'xj Lz]b8qM sρ\ᑑݳ[5sΝz00eYa/KW@2U`ӪI<LLL돱B<7[h"ѣ/N;^)K(B5͇qqlQggq{$m4:>zZe=)5Co `߱kf8M& SEQ`i^{bٽCm`Ja>8t1T bhp8=?.ԙ!ԹO񺵵u Fi&}H$eM< {xWUhvhD˩T*01'#Ac>M5=?: by^&#Ih7\ 1Cvvv͡0t|&Kuꅠ(r̙j4؆0b۫81 NS,LzIpjP1?X.G>!1yi\beNaŞzK.KR"!!jzyձ:]Mu]2[#}Y Cq܎X߱X (`{~8$OIͽ(gDdJѼ46|Ӷ] T (Ɋqk8< ;q?FƟ-@kIhGi!c%<' gDA`5I?L[w@Hb귾Gԧņa@C9`E(- ,T$7 !96*Sȵ;596"F2r|ּ.$I ͻCA30u;{lyjJPko=cў:9ݩjVyyRT\zɹD"wII4tTn<8FV 9{#xDBc$k@)1x5:V(= "+(Rg0E#}P,n&I&DG,hߥHD+E˃κc!nt1qu0A`3b}ć{PJP9AQjj*夈VZVR͝rvXnCݥx D+ j{ vTШT30E^QXMa*h2Is5`+:_{7M-,EC1)6ĉ!a1éUln;EFg^^֏H$JoĠJO/~jGk NJ}5???ZP7L^0?6ȵG}eYAB3|+ ѴnvC|rWVsM-6v82<- @ > KZqߍv. ql*ʒ`(/| _򗯹J~_(,6EߧcoKĨch fQ+x |1z]k+sVHs5L`FgFD Py<=`,j#ؽZ NpjP:oVBp8Lέ& >0&aUsT+>]TJbJ*'(SfigXƩS'o'x uQ2dVVV,7"v?DVSh/2 ש9F˱tVPHgP1m l1XW-CM+j8]u;U8`s<+$!+NbHBܞӱ-nXY %q=<'wWrr|Dn-@/PxWj4: 6h 4׾a2קMY}#ikie h%A9A9)ezZyUMf\ -7N9i Yzq5ep? }~L6>5{8?7#XW,6\%،a|!Bcu4pf:1 'G<h5ÉܼcwO~)2_ %Y^j$o*݅dfXXX5>"7o_s^hdhA&VW!.D-cei53 JlmeQ' 3wì+!E"zNb˜fr9{(D*-͠=@6Joq(VCd^rD 'pֱ:6'(-Jhf12DY %™Y}uຒ/X箮lgTfqOUU={v/7NEISd9+r|7{h(3 ޼s꟭vs,<<ե UR㞥[2 ipiUU[(:rEo=g@]|6s t{%H A_݌:<ɪKJbUr"Zq]Zᥰcb(upnrYQ'9B%؛"SSSx;vIveY޽L,=S, l"1(uvp+Bq}ĪAq'+{f9&Mv](X־OS8a H}C0/_{ρ &X3O׮[ռ;&HsQ J*`zuhT6yB1g>kiՍc=&Zt1Q+`#I%Q(7NH+rHKoP9ͭ3`ϰ9>9Qߡ{^6r(, xǵU?]44Ʃ.&mhAFy'HNhDU-2=H;n=Tc,GiBo|8 әLf-P4j7fJpJM>PvFI!z[]䈟LB  ^^\m&ɣR$umϵhot{C5I&}Fr3뺮Z-{ #Gf~/ `'ha<&nM?)7a2fh5k|VZ"x"}WVd4v"# ؝^]hk,Ne֛b8NQcF;z]Lδ)I{+0 )&@yz7 >Qܼs ,ðKv e#y)> 6JDkk'ڥ%AMn4 `%&V|Sb(ekUV bdh/TSS*☎c5/BG^J%FR ipkq ZC[Ν#gARp8 &ڻ0M{ (ɬUi.%VͲBY]y?2WMO%X J$iu~nY:ekxpN8ɂXaE=ڴ50bRu8Aw &us}$۽CaU:'Dv9s6[ڝ:0pT^J-MCy2=l/rR8X`f|kuXؕ^_?!FRS߷=od< !(+eA?"k’=qa>B_s2V2ËTCN}SOY@A/C)J|epBƒo9s~rKMQs1^= FF@S/ 9gZtv;UIցVr͂s9f6}gcC`2oeEH&'y(3pݩn.PJ1VǮ3  Jsz3`/2,o5vk+yc\TQ2jݮ_9Ql%5Ɋ!`4h55fѧ gP몣Vh\ZuܼsW&X Bvwv wL=LaP%5&iU@sb gF(F2 '^bY++qp 󯭼>/}kKN]_)ȕk7{/0$93}m?BkWQUPzeWb6o2mV>h/Ս߳o(qPjh)q)kl0h< 퉍`nz$p a!bOޓxp{س+"C-e^O3{):#iY.0 Lu=0?O{ *A?&ȢRt6GP(?2v\\~LC}rC\.?s~]YYOպad4ta~ FS4o=+ų?9%={Hֵuba99x%-]Kc%TV5 RA6~pr>ީnr]V?pj?i`5]"*0 OBr,v@LNNVxɏmCZRf:u1]ː#$`~jfXu^]gD5iwꬨ#Ii@8.=GG7퍷ޣ+S._O2@ּ8pfԍbl$>ӫ!JhZ Um}qsg?@b4p(;$&`QN3 EzeԀ~3`4ц=b4 E^{r_; ACۡ)A=!k!$^:&bᕽ*$:p1Ұ8~Ia:0>(IG}W;F;GJ%`<VƞPG"3ω>tp qHurm6Xdi5Nsjjg,Z:0T`C*k>y^U<}3N[ BE!;Qo/">uv=9LX2L^jNEnNTs$1h}i3ihqs +r| WV˗߂Q^mFYvi?=q-{깎(<;iZUmωWәL9Kc xhxO l&U 333xO{*< lOkzHzywуI~Hw=erjm‰ sk+v.&ՄcHWbglD4Wӓ"rjZVyAFm P֘,|j{gWUA.*ɾv`l;66VH/ 845o~Su2XTU%}r6x~y#9[&*V ԰S]  -SZ PϔXVbb8MxiZiE+/:3$S$1?DB$%#(DMboJMbl= +H|_*~e$۸熎☺)eh*J\嚚MKsxpըVv穮cCh:^MrlXiͼ?#bChOϵkss@e%!ԴJg8S|Pq7Q$l *1J};$AОx ~9ON//f=\ #"5^tLxs^ZZlv%2?%X^)Pj<[:5`g܋x&Sb9wfVw$I4 ځ[<;&F8f0 z Çϐ7xfBׯ~uoꝶ,+TכBzb([D V ctSo*Gr^nG%a34cu3,/R|MNЌ@ʦiNRvܙc[ LzTi7ut`Gvcܺ8dc4nV^>_~充k|H[Uv@GbMV%QhnMS4=A͂VZe tl?fu=6vo|Ċ2եy%ʫgFC~UI7`w} ^$~S[F Ԁֆ9Pl`v*Eɼ2PqAx 8|eyy6 r"8Jll_dEMCVu}Smʉpn>:rdPn\֋Lz`#Dd*ezu'ܮw=ʱ .Qh qй޷oס4gffQloF]kqۯNbby17 rB$GAp"jW6.=7wΣA|W?&V # ϶BfbGhv0+=-``kG̈-p, B_}ټ 񃜱;YlX>}o8ā)> lUm4X.6~c8h0d J(I @Zh'xfO 74ML&#[dj|yUUDZ\ڶh| P[[o2 /3Jb_eؘmlcz6ˉVE]:1ZV^W0Y8IE LҰb4SY9oОx̳(⣏|wSIػ[P|28σUpjOPG|% {tyEI;X&F~ُeuKMC?ɻPL|Ҝ5]BU۠MkMe0Zs<_ e%FQ=lnf()r*\'S{[<B*:&3:C>\:*\6 ?9L[v?޳GþӋʫ׶XAMX pj3`~M8&[ shF=)twF$ڸ->y_aHxkǿs ,)?2YS.9_,g)CDy`Օc i/'in vf;|MY,anbR+-U&h _C  /i{r^˜vq;lK] .- I/V: JhCϛ;uT2x $!iyg䤩UlT+.F g]s& :3'B,8ZeqG0+^^A`e)™A齯YBYVƟlc#**a|kxdX$gxhO[(-1qv*؁܁ Ⱦ=K<r_~1cxiٯavZ"8g6 gY^"8XC=ѫJrRIM+-$wʅّ"-W&A_0iQ& DPfo%\,p~*̬΀බϋ1C~ʬ=`mrK+Kq1>·szFvza|$'DG^yUXtlY\?KhU6^(:K55ھ$EǔƏ%a(GQUNT&ETu&ױ[VӊW,lXvPӤ3{_h}}L ?m<fuP1_9 X sǕHӃ3P)Ix~K]x^QSə@Ak+ɹ"/G[gv^Nu=9)lw<}``~w*bXecfds`r|Dd*\OuM_k۽#ՕS9b4km`'+F[$fvV*@[M%2_ps2L= arLgiIhl/>Wܮ#4C3'z254e9Gy~oVrWS?us)Ҕ։*ŇG\)8#ͷ/SO5σ°ppG~pCd&$N'"B#W6[Ub|а0B/ `o0~|?m1" VPԩ#ʉ{;TT>k$UIs .j2K7/Er][Zo:+':gԩP.JKo1R8:+eJ`n¼]6\Vtj {w;zSIO2 ]r $uZ -/;?Jt՘DQb%Ehf۝?l6+~\OI0?ojv2z?_{{UGF̩'Fk/\xPRuĉzϖˍ} ,ۮ :׶O_8f&ixez;`൞ca{!|8aX<E[F8 Wu]g:b֪+܋+ mXٙz4Czůξe4 x}$'K5+q?&3 {rb"Iw+áBA(rpN?~U5hD"F~JLLDF'=U]( `ƞ3(F8u,]\Vѳ SO $v Mz6ZF7vN}\1vM3{YNf񖮍DύN="s5B[ٳ?<-˲?>111::777z)@*~~^sVtJ#GUJB1ollz JWHc;v^XZ\,O>d?kǎqwDvnO.ePnJYZqJ/+ʪ^©ڇf\~gr(v,&Mix_oSj#B+ԋuYelxy K{wirr 18rs_ {~rw+wV*›ꆚğ?g(3=蟥vrvnD'3{03|NFM&TQg~^0F67;3֖u-ɑɹ4{G7OY'>y9QeQ?E[׳3O=/ÇOs!9smʣ{חp+0PZWNMm9F~͉'\,$ݘ=u%vwhg~OD Y1g7z췷FPAUڶٶ4BQ_\^Vbvg=6sC~iFu7NnBir{*>D"j'eσ/ ؀8oC`=)PQ:r3psݫ(EQD?H5& 5Jpł ˋ sb&R(3DX!}⟾ӭRˎ$IMOCC{Css}f>tg!Q kK+Z9pb(%l7cc OGVf7sd964Vڎ%O/˿;|BoCڳYVijVubWӜ(HR6 \Z[g0O{H$OT*jDӑŌ~~ 64i' Iw"zK9|~\__}IG=zHg42 ˽<4A֏&na333PķXT*H)bâݨ&TFK;g̼N">6S޳}FlYVEQe;69;G\AQV'XVU->Lms5rYXfɡGOO]ǿnz҇OP\< ^x [`^zC/6 Z9qTj4zkknN 9~kk+lmvԎv\pk} O}bE}ndFK\F}ڐ 1,z4'w&YY= C6a Ȧ` vdp\1>aV h?m$8͔)?b EٹO$ l0BR Od+b:W6esRsᕨծ{鉗̾Q^(&J#~t\wfb;;L<Ļ BJL6v5ps\cT׭m-o|Vj|B,pm^ʅ)aᅧ{q|kXpx ػz%/\0::JH$Zރ xXT\|WHzɬK3;ޒt"V(*'$\. =!;;şhy_'(v,??~7gRȲ8s`V/P[p& z% >һLook|OTֺ/}q/8Qh*Gͧ?qԩ'OO|&YY1rPХmJQY\yoZ-+ZQϯV?:YY7o/.Fufws_Z=X\7+Vmv ?K/(H)x0F=vt6GX™3ȨoB"pt{ѺgDEBAEGО]Uo~̏sv0jƓ(3ai }R4 _rC7895بʗ_^?Jw]Rwo#6^L̼ԥ)6)FrF~%>"𡎓iR9),k3Q"2$!uyy l^XX@S_ QR)NYo^_<*ņfQ*#`sL$ 軡PdC4r;`Uw_WΝ;{%`?!FF=<Cw:K/l$AQwg B4Pv\15͆N5;v(=< iiŗ8>69^XbY331N :sffaAYf.w~rdk8- vq) i휓Yes96u,[ f#:?,E28_DlnR|Y>6{;[Oj?`Ë56 +++K rgb.xBߙ?rijso/[ZU+-t ܦUx:z ?s,!# pQq뭾V0N9rgG^m*w1<C %{PLf|||jjjzztR#4NTͣ^J<qTO-kntt_~zAGNc]ǐ"i VZR4ֵ:xm lۜdW7>Lͳt1; > %YEePʑGUo_'11?{B\nkZDk[ѣ%zݳ4zvFm1INI]8@h4 EО>,H8&^i~vE"q2xX tXD` &b@| ( sST W^I&ȀjL<rvQ ^EQ(KDPn51b7 p8*k|z_jYŊ'SϹ%JqyJgqqR-qO?=~T:gֶJ2S~b()9& L1CfQO(5` 5[Fh&=Sg[qx Hp!q"%C1ɲ6d7 \YYf]oQ=Ozj+K|̐o4aţ^ksDbFT& A)sG}isR\Q(hfffhg*F+2ryjmi퉣zhGoiUS/6C=Z}pnEmK-J,[ze<[Q^K7N=wPU}QIuL܀_"Y5gs%/y~O,//,<867\gr ~(2P^zO4HLl6aLQQɝdv|PT6ث7c$hj)J&+oVɟ "AhXxA{RsDk q̖[(|L+.4wϮka2|>F\#c0V `r3zpxYVXiԝf+pp88ݳ2g2l'ۅxRjbv+vnn8& 5x Ћdu^J+foԞ87:ma91,g86Zyq!V*}UcXS^UJ'WdbPx{{epT.>466u?T8IvK(^;N$ # 1{~Y0r$EmmmT&dnD}O f.^ G?k{~ ^Z(}]ĝ ٸ vh,zVtλy׸9ɔJf6,@Ò;'gA-k{ɑwMU7ޠvcIV(R!ō;VXQ*.EShy[]jER$RpwY35FOС8VWyЅn,֡m |/ի-c X(-7=Gs}~^BũE³L] /u6I_^qG)I^/ς#B nP XOV ˙G#5R\8#74]l[FK0Λ`=!n8pqCifTO~X] EB?@D+7o3)Jt^j?62w_S9\Zo\Ԙq_ҊԳy/Ӽi#m;en',k"-/эͿfHnZ`z)!4cVPI`jAF𩵬R^ Cx VRC|x*uC۳i!U1"^bg؉ ?,P=opzh֭[d0pSf~4L+P:1+7/| 8{d҅].ŨbzT!?|Myvz)\If#Pp_z"q8R8+^W2˒Ɨ^"@?X몝cc*M$fsyQ\$҉B)!ӧ` ajNM|>l0Fi%_@?^WFDד:6#3Wsb V3J,~~ g A2LӖQ/"50 4{ZWoNI9wBt "[! Hw ?O|mx\|%o~饗ՙEqeѻU4Lە3 ]wA2%a\+]x~ YE(yA.z >;4o^… @fggv5b]g6x7 F=!ɇ3/CCa,ei)E)Tnրh|W)'̫aVoT*jTH]5u -=Mb67A|!(5,9!K1\X DK葾r&&P,Kgqp`tlZ䒋C:fwbۘ __ƚ B7oDڬ9M @Y^^~dwg1(a{&5Yp_U![s{wB!*\ZZ|I¢1)0s'[mpʯkBԞh"}vgy5)9H;ZGia&xr%4z;l͊_{czqf Lkɶ `%8 b} tr} rI{4|e2! W#'9eE`k.>111aW\*Aч/_| 9=` ;tc5vP ،m =om抷I[!tGL='{^ӻ ϻ:k']dDh}pev&?я~m>|iPn G҂R =h,!}+Rv^1ud2V"SiiVdak-ײRec8&l D@W7$0Vpet_e-$x=<ۖ^[} IzzzUO`#f14A?/)_||/\p…H$@[XV/}0B!{a¸ ،JU=u&x95Sc74L44l׸}_T`/HgmYg.]%ܦe2,d&6bB?4cFwFIԎH֑Ȣ $*DۇeiwdZ4s.NavN|220# Ohɧ fr^ eSҙ̉Lkp1_x83B%=??fXwQۀCs;[{@Gu c 4]twMYbp-Xn"<3th;jhvTѪ=v$Bd*φCJz\%VjL8heM1-hzע#GIXrB1,{, 1 2mulG.,6 ,O2B "cPzf6~/-.9snwpoOb77X,|RRg???G@z7{QhTiaNQmoc]xJR׷/t%'~ezӂWwhtv}Ć`EV5=zeN%"BoVj)XB| 40Vŧ* cF y bc$sxx; tm=_Va)<ДaG ?L<{uX G85"2`ثW8<<[vҾoo&wf]6N|ҒC T1K x)ƸxxowEۉqf^,nmS}FʒpMG M=1}+V.5or`//\>#@E7ҷZ9Eb0  _:[5$0\m pq7buj&PA Bec^[!HY.μ+TjQu1K={ BG=|p1n?BoŠFcas:6@Fp?XK}cu e-N\u^' RuPn{K?k׮}C:ޘwG_[[G?=_re>zgΌe$ /.-ڦ6P7rM?vl7m,\a<#OAŷh=Į ),˛ (d m-1I6[%40m(1-z3TUW sGڌ$?zW_}@`u)]'?ی!ҥK>,nQw Bw|ry2Gszs> FA+;qM@F䨿s= C)>\]xe ,HN2EN64bC[ߊbK;cn^¿c}򓟄ö3>9Ik^|p$)*0rEE rűM1|R,J3Kf{z{^x_r.ssۆg8j5l~ ?ܞݹhw٫٩}^eǪ+oosO5nmml@yqfgPjs-'Qҝ޿y4z$PdGjӚ'O G¡K !<~#f6YxLq<9Cj\tR ߴ%u^C5d>Y_Yliti*uNb(2(R0_3nz'ՖJ157|zk_R)1b3>f>Ӈ?W=eC#G{Z~~Zis{6tQc W+++LfsVi'p)˟g/u\ZTpQxdTF4 +pm۰ &#Q`sUW'-\rybbbph4cWo,SI+_WF[(,)С}mP4<Ȳ u- mL Pc`L+a e]=rZݲNXN'y/)VI; /&A?yr.%ϿU DZbyXWx)N3(*Ӯȴ@R4^9+ꪘ-9oL`@6+bNb֭vhShe2tmɘޱA ,_[Yzw=xo{ߟ(Yymr(_Iж7?~3~y6- 8MSET _)eW?V!/|0d>oy^S.!kj@n XhRTl_{;Y+q<-"M59W/6dx{d8+ 7*GkmztDMb.WnNYrɇ&+ӫB(Ԗt}P9˥˵RI鵌I`i=T6 'uǘ(;[:li-w_aD\\^',abR ZZb`+WVVVq*HAkI`٣¶ CLe??ATSt߾S.PFW<˸Cc=?9;቎vIw7F6M+he^Z~ab۪'+g%SP ɥS噋G*-2J=ߪV=aKkڦ9KQ_^CвMܭyѣ f1k7$q%1E(B5 –aؓeIRkY&~֛nmo|cԔe,xϦgND 1+$+@/phP(mp+z&,؜z"萒ĭT*N20Z Lh\. YJM%WGЀ쫧+,މ pl\[<~pɓ'f3W?w7&ȤAsx$'vgxo7&;:1ϰ#"D%#_x.j  މ7;v6N*ݙ0&{5n&bFjruJh\ܿltY "4 }?~wuKrhAԚ~ C~#~F.e8WcܬxWR@%IѪkZj. wB֣/ph wk+t6vKOJp;4؟Z^9J&P('&m y&`T56j3/YOiqY@ZߠEAZ5˘g.n$hv#0p7p= :F#9@2-K##Hjd2lgxS-)P?Cx+,%j,S/V9 yv咭:|4l6}tʾ{)O볉9 5sTWʅEF<<οXˤ(:B2ՙʿ?|UZӛl" 3|d`^18ئ} K`[|ٱA1'A7"67#޴Sۘ01]ng-䍎3/#BʟYGwn?bǷOBd[ v۰R8ݶlE#bd4?Ĺ;Swz{z!f&L^yMFjÎmHU!7Q:C &4Π%pe 0 N>N't6Q@%6 V43az:yk'@d/sC޹SXj̙3??Yԧ>y/{2\ssRO,^Ӹ9 #VxKW"g7M!YS}k _x3gᐾDl$$m1Rp,I3nr#Sy`?[#ǥL* -Skfb8tX`flޡF裏>s`ю$づ0J?Z=kl( RRpaۙkw>Nߣw- CVbu 腛@  [IGmS7<<2\+ ¶މJ9'͒\\vl+wiY8K1@dZF*&W wxG6=?}4zmܫ~זɀ`k)ֽhߐ K訥+nsj VH+KD܅# SkzKua`0C łGEJ%}ݮ`]hy'~ =Y?H෍0?65y[^^)m[a?BCwwqc8y675:'*+Rll쿕 15||w sb\kAܷ(::?D#I)͵j(^}!W s'E|DRsw3[x ݒ;Cs"Fp;6:wzE uKk)5//^ ڞr'@wg_~Yt/'oMM'{i5! .١G &n[iVZP䝿{L-R$mȵZ#Ɣ+UЇ)6I'Hn0q[.j,u8 7Fۢ.mJ yƒþ>*ͽ`SULZFJЄ$WL4Yod%Jz8Y|ef=9eWBfӼH;y/PSkdI,Fѱ -ju{Iq̙{=ѣG#HG,% h@Ø%fՀe]2pEV Q)Ϳx9˶_|饁Xlǃ'2.),y\.g2][:NODeij-%Y fv0S$Aᡟ*7^`v#:G>ҥϲ;oݷho#-I BCy_p3ft[!K'pIFϑu֖EqlLv9I% <T9GMU'gV eM?rH7ዅkB*-JIV3h[ѵi`>C$@c3Qa4HU(_P(e KKKE!, p n1FlpREUL86hr0- Ւw* >y Zp I֗&b$I Sׯ$JR3>Vw 3((J:vc5E <934(vNX0鶥 ʙ}Z{zYX˦q-X Km|5SM?L.{ X) tD' -SXUZcn3鿦{NF)WrpwK`i&SL wUay \^}x!v,|^]|f|}Q nh|.gU Kj-K1cۭ j5LCGf4x#H5UAmW*0 @tS?E1gi މҥεOwy,C1 2wKYw9֖}̮dˆ{?m(\#;bڵ4p+P}IZ_ fCZKv)a Vi7]$dxvSDQ4@lppm #*=t\[)*\@ucytp! iKoRnژ^JzBf75qkC;w56pح[@ah-<?-V[Ք6| СCD>3@(\ǍS` Iڪ hVO,"\  dϟ?<6nf2s|8 1 (~W=ˤ8&HQi3z4 ,UT1 k yُި\^5{aRY 9 ;fE\Xh377 Z=T?mFէo98"ꧨK~/ߪxg 1''H1\iEV \fj] P4zZ!y髌KFZIug^6xӰ/%KΆ}}t<ı:ɩ c}}do/qkk+Jũ71(T~mcH q}{+[V1q7/x?Ӏkkkx5Lk*f [gDס;ޡpH$\.fa f]+%!AXu+lv3)D# KggKeQئ^]xR\`@AFO!F˳/|y)pH147յψ~̜m K3n rd9yCSBM˱6#t'-nhgy8KZzn6EZ.yfPJˎPKjw} w(E5B\c } ~_?sLò`p@U&h5a; r~-*7-&,WON,v_q =P0h[Kc-oP\S,6C@V &HBoG<: yI9r=8*DgBgYGLjjT-SL] ,i'ܝS:GB( FlfFn'ܒhۜڳӫmX{PK?ρJ;bu2Yٶ1d*u}Ҩ%C'4j*o@`R ^SrBDa\5sGߛM]ig[G8P@vWKеK Od@)|=5sJÀ~g*3yRt0۲MVӷ9 4we2hf%KOT,_v.&A|Kr=b9 L˴.GC}Z3Ӏ/b-]؆Ɗn)KBv#.\NLƔRXW,- Uaqhxa:o&i22mOҬj.uVP8 ^kHй\R{ϧ͒78[I"0^TLݑ5`kb1e`^,])/^ `(nfhFGkv][Uq74ٱLDzZŵ| FQX|' ` ytwy7Ϻi-VJ^ F )L Pt)yey x2<|JS_V֊-&+=!XR$? --[75#:oÑ_t,-8K«+P o*S +7wWuw6p,;T]qU Sk *jD5U&(;xw 3{6pC)>:ɕȾs7QTT oQը&odi2Lc[:$/EN95T"ŶHښ[V2NS(׷W}TŘv{0AzK{q~Ooø:Ot?02| jCkkɛO723JyEffY) JEU-_!;W RpSy+uVSp5Cg' g&lTH?1L&KR<Kg `ެ'r R1Sc!N-)4\c'/f!\0s/іS( `-JNt&n0n^jCC QYWL]]|O8xs;e^[3\ӱg/MՄCp-9hui4\'pZ uN@nYtcZݨ,]nPvi-&6s[Ud5Ls7KaQyڦޖݰ>-lS[914I{{ M [deJ}14ss@aJ-Cr 'x!6K aY?n*`<Vq=뉌HL\H y9QtH,À\.X %^:0bU*4%Barv:~0| "c~/x`sv6G#7p@04UÉ)[b}|, -.-\r"dZ_,KS,CY`x *Ёaް;YtgAQY`XҚEOx%mj[0㖮뭊\X$in.=,h)x?h5IÕJeKא=9;g׋Xo4y)$h e:Va9Y4,Eq"PjbR%]ZzYON}8?$pJhd#; X=jp`fFS3~}ى}]ŚMymei$%C.β0rVȢgCmΟD t@#B:}fG/95 q: uoo(yJJُ1ڦoPCv 栝)B\z'Un HawϬZZIaХZ_⍍نꍍB1ƦyFj&.<2xFތH,ﳴfSǵbU\@y-Fx)Qmh AP בCCj(;֬dN..~{x@S7C~xW֦,z]^Е =9AUnr`Vk/Dseq1 .FK b %_tSb p#Kn4I `FIrIuПe/ޙMK:x\\uc2T U 05O@y]lnu ˄?:mcFp٠QD v|jirda@2Z@z" Д(.Iٲv4O> ўdZ?d-=@G]z$]xMAԂRR c\Le=AP@1'# ?ntXovm+OxPfZ$MڂmGo/M {d2ȴ޳?ҡ1\잡ۻpwyx~@&3@pŀۺZY@˥fFï͸;n\)Y?b[5!YmfDY Qb0.w3Bj:s/K44<2<4W^S`?/EP?[Y Vh+cGrF3o2cLM.z:Iu$i&Pp(phPwB'\j:j6&~ _ZXX_"{׳Lnǚh/ S7E n7\ĆIn8Ivdt=- i[pV5kcX!ЫTZMnѻ7w؛6X9$n"xFL& {Couy3RlD̑M W.~Ş2#lTrw7/ %d}'~Vgͺ˱ o|vյRR4yC`?-yzIjqv@Œ ߸vP+| CMI ᖣεJ+nW9Sn04N0jK^^ <2_x {Xa 30anDtѪz'y'sx ƵXCvg2OXDZKH2iક `=}PNb8O37OLzdm0S O.BPwœc\ tP@j봖4bɓgV%8nbD[P;Tw%{)a]x5{=ѱGFکU À(I \͞Q2T ٹ)`cm/n*gT_c!rYp=8L_'2]=}+/y%1@F|ټכ v #Hj5 ɺ"*Ce2c,0!`BN{* )ҩQX0| }4xsCz<>% kL9;?7"!ʝ>C*9IMEyʵĸz#,~z#9^^n6w` 0YF ʐJ2t) ^a_Ff uN5)M`M}{s _8wKXo^:٠bc\m]iwwxGrr b#[u9$uk*H z f fAYz r%ENR7| * />L%)9)D$L%7ŰRkVa1d{/=<#U ]w/ {ՏCA(uyws0‡#.DZ#:-va*k@6ilD([F$i@`Y4{ݲ / Rk/bra4ùb|Q*>u.B_rm֭J\KnJm0K ǖb\Qs};{!0å`;q0b{iI0FdM3%  !4rlƁc|~쩇ΔV;mL}rsf\9D&(s=%145˜q ,sjjMТ\ӷ.g"J5퉎Z872Ĉ~@e!w#ڡv 0Jg|0iuиڻr' ]WVd'.<x2 VUnfT80<.] *$'mK8?HAO"CQn DԳU\7 BjJQrS\meo>#ya2IQ9B6Ho}AYU]ՒV0 >?meUW4M>UFvb k%pd^lF7ug̤ э7pL<>߻Z^pѳpj3; cB 'βڎi@?`vj*DtS%Pb"_۩x"c4@ )aVcFfZoA@)HhFR FhN?/GGZZ+0@%\GJ-y JWmxZ?S AS"`f1 i`hAt4t'|b-5܍i|Oa=ƀʠwN>j@4s|1siT_$h'p}Rw@3bB53mc[j5Ӫ$fr 1u&{=՜o MJ>ΓgZ5hj۩l4cýdfD?0>R=뙐z嶪i1pc|Cc clxӶ)~4r,0h`lBtVi:|"2vnM`  ':TFQReCOd,%ހ'< ->! 8wiWj,sz.ƲAruРKufMZrbx]R/HBp2٪:2gaD 74wW}И+$,?TWº[ f͕v"zp@@%Rki͓`z^`o@8RZ!WV@fEsn61x\Ypx+.0>{u2Cz6S``p`J%sho/Յ<2hfeNm[#( psW),2KǿENh]luE뢨t:w#Vt"sQMwMsM[V$NR=>i;Ou-=7,27Ϟ:eİc(Oŷ<;h]}!s\3A3tV)'[8?o33S{Hvۓ9/h qi۠9SiE\PZnsbmh'h"kc&օ!/xߊN͘d Y=9<͎ʄ4ƚ@ ]VtmCUqTb@^Y ݱre٬\{6f2-\l};ɬna%_f~~Ȉ{$Ikp"{ƨ6rM2hڼ3c{*4|C</qel &5ێK.p:C=p4rx!וouAxQz\lZ&+z=^x@@ά5}mldDQUaҳt&/˗/ڏךz5՚E؊~Od kF7ss_o\!(ѵ%lӴ.܄RD t[P Vsei=ʹ{goF(?<7H6?]Lǽ&3f ~{ժ: 9% anZIztQ-E6'd5rǵ9oKZJVPlSmqj vԀeSznvo!$Uh(dj/B4=^O1_(ҹL./ A]yeFNe=S4súg=JsA7"gs`3 I3#iFJ(:H^ZOVYksU^ۻ'[O+9eYZ~,{30 c9h|/n9$$! wW_ZT+^TP|xMNHHn,E)$o5J]V~ \/"~tFT\N\X) ޳?0-+CM^|\Pq❣KTTxO=5XpA[]-Xg-%Jy^zX.zSW6JQ oUD1\Hn#M)3OS6Pa,^e#-TTLxPt]"C@ctG%$cDϘAiH6C3VQGX7A 䡼'ҔW iF,8xc xmZ&b^J Ф49V/+իT-/e2vwCVKqVk6,R).-/?eY5zڣ^;U;uTdAmeVp~+|@'lj20Xr=7Ce\(URS6+<nsr0uN\|կ>qVl@Z+N=9˚0j#/_;3z=t1=\-`JaR^WlZ^dxi64R!T3B2RNU0DaR6ȉa#< 6D*v Aj {LdY~Ǘ'%w AravO}Լ~Du3x&I * Zj{0'";U̗V.Sw+͋;fhr:]IN5#IMRs̀ :{D!vY6qPRZZ1wO ţG>F(rcJU*ž(/h(2k{8޿/Z^O˴EORQ)Vn18X)k^-JM/l'zEY/hzC0dc \LAaUƌG#3\ /=1.TY:Ow.\(AΡ~}JU r혧;z3[GTfb |#3JnѬK^So,z|-n @unEf5Ji3A٬$H).j]MIKZ>KL جM|n eh#mee%sl6{u*]Az{m;oSWW({^}tP oWғI:ceꥴa bӤ[q^$# !Vϕ˼p83/_|tb1aG{=9*V*x  +tApjuW2Nf/6:mXU-6VY p\Z̑ȺchdPF0A=LKF:0+.yi2zuadD:G{y+<,t`t̋k#]UuU2F:~X7; hџC@VKa{Tb5(ikʦn o2yɸJU)G>\LŽAP,d~_{hRZ]%_\ EjiҪIvjBש&HtGnj$s) `O ɮ$Jh~o|A-VbEN1 GF bxu䷕2 s(N*$jy1`h9x4:<֊dh gA:#ށ+yr)ł\4@N*8c+ :Z|AW/JjQ`'=V!n'7_I3!PVk9M$qQv]`zr%zh?p[.ah_MJiw!<=K Cl$m4I%A Ll6kj|m&}Tp`b(]mkRSv)srbJ8!gx0-֖މR68NZZ 'QiC,+ra9Gԥq6XPS{'w kW_/;<.m~W(z% ۵W M`Fh)'&û=8F2N#$AQ*╔7d~3)m[Z%<}p8$c Ʈ]2 Y$6Ԇꯤl %PT3+ǠjUg6#!+īVG$zAvaߐ"Zl W&Ym{vr]q@0aFC9}RTH)Ղբ#WJ*hx r@%5S_=QGx,ޘj+.]r޹2~>РFkxv>G(ʪ5k㇔{ᇧϤğ~/p s3kT~zhDZFsSp $mU:qŌ}q] q#o*sƋIEoeuF6[bIiڰf50et#I"O4\&.`tOؚfQ0?m<EQVAbFfʂ'&x{99CR5:Zf<;0hÈj6?iM7L΅6crFu/ _Gڿ|չK~G{O#{g}v| Eތ@M\_{Rd9iWˢR4Lsb91^IXW?P%5mzncICk#L&SvN}qÿ́g=8qbqqRUU\.5j(7gt[2=J@3<;1 q.'ge0q yD¹='?I^K?Ю(/%^ఖ^h|.>6#Bqy&66{T5}a~z=;qgȗ.Sn_^?cޏ>`o,Tp/r "=˗&1Y4Qrq=X.vqbh咱! r I`Ұ`HRr8?M5|+~-1\,fL13?vMxnp^1 sG8a*)DUuMa*;{>-٬TqFt%yܮh|U*9B+di6*JYt^z0˷qZzlt>68?[^^TF^(L6[O$|,M̮ i)_֥ȡ}bPXtOo__,|`8؃}?@I,۲G,ףҙLbB[ʹ\鷍].d UU?}%gC6/~875nϗ^zi6ڌ@)n4V`'4 0Шg9(\6j I3ٙU,\*rtL*[ N 0r|q>- ӹ}S6n;xc3bJkgvG+?l,6vo(NM>SϘ'Άg-,%zD}޻wCYz;kw,?PVGiY33:X}o?~|t-K~x䚰KNLJ5ȡ \Bf ,Qմ€?cۮ].s?|e)ejr-us_bg}zB7T:QhW\h4V K-̻;e_.R17sF5OSmB٬)tCt`t_?oqI q~)Ds44V9^9£yyBJ1KOA,o'oɮ߂;BN//mN`\K/rh'ĄϞbj5pxn߿Ojҫ)}Oˉwk3SDž 6t_Gㆻ$Ϩ8{Lj3dT5PՍ^ZI#'d 9~Ј.^[U\.Ww6e[ik_~nP߀(TP̻Hq" U7qʙᚚZj>7w&72[PVBEQ\x3 "o@ m}rj_/}ؽyELjNF>t`Hnl,gfs~b"h4x>N?ФVu,f{{{5||X,f#Gh?kXw9_U`h!KS_<~kk_=cc[t/,gy;QHe(V"gpG`j6N+!)jRVXfxGaTLuצ}?H7t}h۾6UM]+.]!P,*m\AV*U*1@}u.1Gɮ3 pè6Pa/y|^ g/󮐑Q5tr}!. TE{{1NnsZ^xb#wIh[z<K_;ۿgO`ɊRH$FFҩ?J\DGKɩۈO;^8w}3 ]CI7l.UYfo<\.v^ 9^GxUÚ=?NOZu˿KNiI0nHf~IRuDqN?74E*XZfҬ&W @h`i'UNJQ0Ƌ} @j/>ӂ Zai*0+R%s|\_}|ʚ7"ؚQ;O΂4rm`=~T2Xs Kf|mr!.,x̗^xܬ{a<󤋶@B@oojyXhϓ #VisUKSCC[4n/ů@HNך`\reqqѣm F;w=yϣ> =33C}%:7>tOҒ!o|S*"jr9= f9ꕆd&_h 4D"A#.qLh1Itdv.ZmUq4Ɛ7*'=Xg|DU8Հpb?I}9ͶǗ$ *tը2v:mLH8qZ΀X,6==J0|ωlFj*b-;쾨-V.]t8Hͻ#E'f/<:Xժ"XHF- ̿=S5WxW7L^~:~ក$a'_aO]t k<#\T@1 IP,IݪgͦkZOS*&xW"(xjPZbjj"8xYwۇY%FORvOV={IKXnXP=+t!H w;t/M}\6anP4uW5kQm}vW~~xz_:>,O5lddfYgZ_iI&N&lJ)+_=8;ʵsybw$Ƨf+%KZ>7_]\XgTb+9?{o$@RB+҆OVr΀FKEk C1* af5>__oxSoL˿t\kPjK6X;+ Jr^\l6}>,??T7@wom $פ8&/.Q-Zpr)g@% #b(Τ&NUgX;H|: XF_2EAA0ig;8 MۡC`^|y X,߸ժM](V+qN]/_ݕ)zmH*Vx{@{|>m_ۍ"7tM%f|] OȬ[-°ΡOlӝFmx<.Z\|b/ NlaUK9oxBofanqM@58P9q;/U sk_Jfi s=&Gl nPINTHм3l LMc*;dY-ɛjp- ֌FInfYӃr[L^{qM2 qNܡ "w``{uE 9YȊ*D 6[xy0w!h; vXiCC67jz.eVjiU-slaW̽6j5rz`1V 7㥤%x)NLM<ߩRQ)TNLTRST.&S*Ӄn5^X1}sidr}A{ k<$f 698kf ja.ZcY;m ^ ]q`7]3Fɛ#2bXo'Eʧa-PSKaIM\FzlllMpMJ0l5;{Zݝ E)"%8zSl'ߞ4D:/ lrͪJC0[[SVâ_+WuÂ܆y  8BCR SKx_8#Wĕ&E6II({X$';[|>cP+ᛸӧ7xd|][rg:-!g>CtB.ܟ0* ;v;Y8.m&ĝӵ7;no)1] Lr;hSؓx!G#ݒ^Oڪ]Q3 7RmoFUo|>->M5_H,‚7 ̶m#X-Vb9C{ NeQ ULuNDXclva۷VE[zK^WYrMշpꌬn#qeC׷sI@͉8 pNpK/!{5u$صl uH1ؙ$7"ݾ)ɍжI޿}JZ &E.¼As<:6 ڬXd[e/b8ym6C9nvv|L%Tk9:0br*A7uU\TWBl3< zT2Or)/@7DNxÊz[z3|ʕxKZ10@3ЬЮB4h EM)3H @2rA-s{.ܐC{6 F#P 'TwWqowNe7`yn}XelĮd#ٺIBrm"+1o'arr~뷞}Y=䩁O- cXNl5:,91qV͹Bbښq]ţp 9Û#nO-@!oRvg""?fISۜǛ??җ/#5$WOv[>;#woDՁq SvRN|S8qۙUmu`n7spْA$x1???::af}{ fz5 q|ӆVUI|n]]NM75ED@535ƠYfqӼjhNT9\{(0uPUYF]6K?ڵk"yWկ?PQ$#f׮poW+ΐzW5,3NGWI!:30DʘC ###d2yi0γUݿmF̀%` |S-SErljRbc6, bpBѕ&yWuՆV FB)uZJZE 6U)(زBoH^n߬_|/u׾~\K( d 'Yt͇D!&(o1%wA377{effN0k@h|w,a\koweXf,Ѯc#Gm3iJߩUE. c/S$=66l} I%qڵ|>/|o ~I . $? TeJV7i@SԬ4S˭׈sEhN=ĻB4Q|\W4BB-G{(FV+%,ͰhZH2#؂U.zӝ|+++xf¹gǥr5$KOڰ[Žݣo캀,--`&l'ͮ#Lbmu.WrرcdrbT*o~M̽$5 L:Y6uiO^5(mcxQX|MЎVAIUD5.[ě^$WSvEsR1[qMVNr?#^X8c2 vd1}jxboop>Bҥ;$~5[Ibw|ɉ wwo;ruJdn)w~w}gmr<׬5 _əR&I0K2QC䬑Ż'>3 vx+O.CqX(W+4{n_:JbJ%. ށO}] b82EV_c)#Mce3Gad5y(+p<_S{ofJImjݕ6-]j73 תپ#2ui]˵/Ad$ׄoM+qj&%oCY>}j >ǻ"+TkhJzYM/hY9gJ1ԌLj>ܤ' ~V/S4;2owGyOT.%yg|Q BSzT![Č I6ސ-R% Z|~Wba{wntbX`[*CWXY ?5Y˥U@):3xn6)kuUóvͻELq;Ab 6i`VۣkRi\4QWhfv À3_z"`ϝɇλP'7.P伀ۻP Ї ;c7uCeL@eddd6l%-6qA3':p~sѷRV9z9Tk5uFs|w)WXQ/htUJ IZp"C J@ќ#<)5z)m@;GߦI|{ɥZ+'c87-xwQ{]{N49ڪ[ yWH͌Lh[,kl%L0_̮́)EEwkޔ1ۻ( üMM۰Q}[$f2N'dL8$0YV `TjDu_{X3x;`Wr6Qhp֋Z»""ީf-8ZP9j-4+~ӜfSC>@{-ȹœ+$]չ^_IQoRr."gcv6)zح9p @W٠?k۷HZKȲL7\Ke낏'6u8$o)~^z2K J1r =$09=GV H%Y åť @bL"͆=RbA+*喝110ڭ 1 Eki"nn].S#kӧdruuRqCnO%R/v?m( LaEcng\۽mޑr~6|w'Ki6a%~fS o\z5͂[R.xMrUn꺦T}JiV}[.bRI0`]d&`R jfi |=Rkǝ}'R4'VF^n#5h5Jj0 ;#d :_Äj/9{W[U.˸6t04Ѭښ*M@+dY)6XdG ̕+Wvׂq7( av,L);<ʂ ܂mDmv9r;q\fZ(\.g }%H$.\@buߡhHpI(ko+lQON]F,+\ ha5W㔔^/8gr!TsF0NJ%'xcF*k㊁\P*(;#{ {zdjAK+Wܽp9Ht9e]Q-)uӞTq[n^i:J\@襥%#m?"wܹiMt n[%[jnjxL»N)3ppkޑ y.@(hEݠȘX@A86ITi3i=?CFA1(\L4b.J8ω~㼽&K%|$jV)''32F]W%) B7u nh]Ťhw vvf][(w&tUu`<ɥ1'WwfYuYPX=pԽCÖn0)Y}^8cZf.;J{{ϟ|}l+ûsXN R4TdX*$.V. lfx]Ʌ#Znќ_u Ӂ]WJ ߄* 1+Z`D54.X)@tڪ[tAnatKW#s5a{[QNv7N l&Ks,߬ v$ki1vk`&[J\4E 'J5M.K:£Hx \lucnV4 P.$b<=ίF׋jfb+B_ d~V"O++\)~UϬ͂} ]uFJRrB.gjqv=hX@hpzO1(OȀn-bZW1,aD"y&$9]wRk|.ɮmFQYǎd2梽ڷo=8R Lvky1$e ` ^hz)MQ*ҵ@}Nb5\Ydzhr˻Q*9wڂ' NLqv0rU*K)`T*l(tUCӜڽ֌ Jl zF .CJs5FILsb99ZBLyu0{:]+]0_Ⱦ2.RMϮ__3J-֥Ruu1(zoz79)L&4w7q |g~WܲINٻ֛ +0 Uopp=iB S|T|nX{4]m}`j f30Gp, ќ\;##^sGk#d#OG.˗~w2VKql*FW V%#Xr >0:6Zd++&-Źb ^4dSa0񥬙=DG ݾMlpv  -v@DECeIG]ljݹ}uq(P(bBtKpb|~Jr\<LYĴUUZ 9V(j^Y|[6YןO>MrܚD@{xz(Ɨg̴q%(΃pVsnrwI0dYkC,Xlxa)`aյ^!`m! BphvRa=9KόeY7%"-ve+H~_7b'|駱 I2v[.$~(pq0Hn܄gS>x<80uyL3f,osWe<6$[ O 4z?$)%T.I!.C*8W]&ñSmY.~3z-NܰHuǏt̴$}~~%־)u]-NSLe=] &}Svh.~Ӕ7 [XNwbɭjޏxHA!`Kc˜ $F }jaَ$8`x0a‚xœJCMvƷ;)HG?zS{.ʄ <ntPM ?FAoO]t*&99C<v;:ɓ'?Nqƃөׯ_ӹ@Pq~~~uuc0%4͑~'j>O=F-<ˁe)/..u<=wbl\__ 6PIf(LXH*~ACrtTO>}S%+ J1űA;S$XpH=sRh?2s؄.r\U9Z:pT=8N46Dj}3SU8c̦TU툆źPL{MiXO- Ƚ7}. $ӧԱ"GĠ߸z[_nG0TxpXga 5DMdsT$OB7Տղh!էdzaQiV2}Ȟ z\pűt3/CMFhd&<fqP:$xZWkք(*6W[.}kfYu-I(yěź|,&.3]sJV]xri&8jc@Ŭ9kl4ӋNuOۈMPX*0򡺧S3|:=eoʃiA栂C0C#C]T99"+gd<ַl $8ϦB~]+2M&^]]Qm:'o|A)44яU[!VZT\7eS8\IuЮtT:DKVV)H`LBO0R١Gs;Ww:,.r)ieI[NNta&8X3Td)(o իW+U&ggd1"Â],X 4gmЂRIX *0Ug=MMwjh2Euȷ|{YnxhEjz +8^|17YՏW,P-jz#B RR{ڝ36Œ ]X-&8s~BUkadS!}HK;UV5ѱڒSZƝls>ϥ>Yk dMN0}ʩk'AK ]j?5нϬ.HcH4J%Mlr~yϥí)YDA'Pŭs:hM% P:Qtnp\+a?Gr8c<\M-Kc,U$X,=r!KJܿ0s}- DDfyAYA(#\ 9JE!f^*ERHH;2K7M Xs kNaRLKR֚r*Sp(q!.L ez`LypRX,LzJ%CZE ;)jyv9݊>Gɕ:xҧ:|DۚJeq  X.fBLgWIAE =P7r){lbQaWwͯҊܥQwbn-F;7Khv4a>=y1-)= q7uڨ`(8ҳSȤr6G0|ҳ:t?rt$1 ={MtG%_^^)EMigq*nev.{sh_wNW\"o`beEvs'=G>/WuHcGɝzNͩH_>FSk=Գ.Pҽ#[TV?CM;\umkR|JCm|9G(!l//^j&%˗/ix`c!Ro߾=0Jw8 d?|#i>|v o޼Y-l`߅WEDs#f`xkb J n4;S˄7|qz-{.70ALObu>}^CxsJ*0n0\:0?~4tswbCwzD)V.j4t_2,۩ ` ؚlgI$>p x қ~1M4 |HRap.F$:IvPPy8rc4\T|qsx`j8ah{pM&!&0 亥 MںqpX:1cʞ< v̇=T/j~o2JO0ÒE.P+J5IA)h,͕Fzhtld O8(Y;mhĭUPGVti-혺VfaԨ UKI qހ7en06º*q2+i3sFNs|h2)) f7=\9qMGT^&6:z61Ji.do掵SrW]zss㣈Fcv m"`OYDG,(+ |AV xR P E"Mg.XpU+DadI })6_$JJptQswo1ŝ)@/-QRob]5sU5i#H;0,D3TYtb=BqK;8!Bm(X$s҄t]1S ͣ:EZM]Ln$O`Bi9p֠U_VDTr} WkWQe{y\[ 6f'8$t",gW%Or Y+[&L0a„ &L0a„ &L0a„ &L0a„ &L0a„ &L0a„ &L-w\ endstream endobj 33 0 obj <> stream x<Ɏ$qwE@p_ OС xF|`0LfvL\#_6Ool1=V߳6chۋ{nۭ}Io:F[v{ n{yW:mTxWʕy-OJ7|j36S}*=\y]/a{o_ZL2xlZ5a -Г`mq__ ^.$x۷lHR"I聿=(;l?cӞC1p pMD2A N4]QWundS<&ݨ I72]Yz__irz?:Ks_H Cc O#wMXt zo+&&7Gt!T|p ;myӗGRNPHP _2sA$߹*@,I%/zT+ j n0KueHoTx\E6 RB_W'`+Voj30`+scT@HM@P E1kF.n,3%HeDcED3ϢH=Q;IkFAeUig ɣ}c׭6/PN!@eo߷謼_৮MЭ^-Ԧ߶?jweWQvw~B|7Etֺo[IM߷>snǡ0˸wD0L n۬?a8<Ʈen 6~gvF?*c }~TGW]@3l*m9c8+>$_Y69eʫlWWeY=x`Yk_ganS7<(r+E\BڥW]2H+rݮbvǠ]=xTSjٕsnD  B+!L= !NZ}`Z#(,i]M^A\NakO.4Gc. (=+g>ǵE)v*"zR,wAz#|k̃=@([l"$]&[!w=) <ʸB/x_c3k̲~>|Lz3`tE WE;Z1V P闞gyKMע\"ƄIhсUT&헟5LgҞ[wm [ݭomqBd ;i$Bb d+ Ӳs -۬܍Hz0;28bB4Lq"=Pݝ)`N=hVPe]ü'b$(X13nMO{D1_qQ ܃([_h&(m} vCx&-.qm$Af0H-J&8Ii呑cv))N<1)pg- qdvpyKF[ZOx)j̍@Hˍ[s;%'KMr]6e. T:]=YS8S/dIiTTD77hG_Yh4#x( D@f$HI05*RŌsӟ&3IĖq];)Cݏ^ <(uzRȒqA)<d݀8j5zM7jYT]L)q;%NޅbzR'z+9!9߅ѐ}^Lj^;ft?2D/}\ ɥg͆I^7H %Xf~"ZI]X>f! Kg?![%T GY\KX!4mz}N n Sҭp _ 5n&2J Q$Un˸z\*-#~*-D'8lN8:׹P8̘ęa!8r|"7aVwREYWUAZ SQA=&qاއyɜi.u A*98!n N>aD>t&#(I TdE,YdiY+kgA-O^1 iqrv5F)-X'7t߃pߍB { LD(Uz)tt)ʈBv˜tCo? `M:0$'ONlʕ9R9˽ԜI4=K밗T\L N:8m. Sm4>^]JCuDZ NǼj<~'mrhD72\&\Щ  a2W8~d=pr]Zp{Ag8χ.镼a'?9(@1^v<(ue*FA| pjqS!&},4놶uq=pֳ6cħhsN+}Z`i}8Waf1ZK(>M7e*WAh=8Xy/nǾPi$63TL<;yGh&O0͓CV*Kt#92VF3guF7Ꞹ TI=X`G?D}u؋EG =@p ,JC1Lcϓh{<,ݙ.mڃ2'\~Jeɇ=> "q 1]qw\O+&%3WS|ddW'#Qئrtٸ5KǑA15JJNV~2 LgUϐ{DGI'3GK *~BR1y (i*)Tf;4,͜1ʄq!RQ$@"/w (o((;)G8e">%_S:WWVSƯ{:+B+"/JPƚg\^_-+qS2[؝VWGN؇HxltL=dG;=7}$zcg3~gO8!>ɥ= $uH2aۺx+b hF+Nc57ee T&́άg)dlS.f`{qc9ÍNvfu^쀝`]cb΄ LG%\|Cjs:l'޺q`:Tvp?mU5Esv6)1vCJLlƭ jfm۽vYb- %os9<)۶@ /p{0z*1\e"Y[>+EAk;K8 !z>#}sq0hYk N[j}xG-2 ww+ϻ ǐ=]eSXP&%U gK#Y3,|N zj-Qu!q-@GDO=#6.ԟmEl@]ǃo^ϻ5faVp0>- ~;8wqt X@nci)deϪ"}t\߶Io= 祈®eAI1;SWZe">~^yb$ R4! r$t镤VsԘpGc{j.L3ɑ&ո>P薤(Orxk;c$R"CJLD.Hޥ'I4_ n$a4Tgݽ!4JrLm/vO*!Xte}(P=Q{U(cߚѭXMS~dwkɡ6)$=$:`SD' ܞK0c)ΟÀ(sBJra ѬT@*W7-P\ɉ=xűⵙW.V(2NhDO pk7W"YE~`+47FM33/6iD?? d8sAZXK\k{Z8,awcUΡYgk'1a&#D$m$LqO聯;'ݓsG}0ꙀσW}Qfhe7ةVS*>/|R+xy Ͽl CA`gVҌZa;76V-#W[N |ҫ7^y ʟpx -pD; cuPԈOOoUe1 k= |/n\_|yX̡}=ݝUh!F_aej{E-W- endstream endobj 42 0 obj <> stream xڭ<Ɏ$qwECQdpsɞԴtЃ _&`̮  o:K0yt"b*n*h}V*{wSq_v_o7*T vQo_3rٕwyçȿlo˟g3=UnY:J*pU(c Atf(6Aޕ'=X:zr /h5/0rovg7m~Բwl?LaqFg?ݙ[Y3f:37  }^EOyT-/ $zs:u^K) Z@ۖcN{?'&:%ypK)*t˳cEc*h7sV`2| [N}~g!egqoǵ̍4]BP@T0N`ǼT*r|[!㯩Bb7׸Li'Eȋ/dAd^,z!]M0)ݕaHg(jh7|vsڠv;Bm4m_?#3rT"tXbA7)2jg3@{ʍL=2dg3_F\#xy)ISaG,HM,|KvgIDuϝ [0lϡ0=_@Gxd^2 %CZFB B[Y~BTV-QE!`9-F݈dE{M;}"  9g[Y2n7t!|tN<^cG?D}3T3Nh>r>u@IhR>VnZ1( yCo3@y.h@m%A6y3f ~VvrXypWj8ؗPYG T_"1}<#,RlՇ~ݦؙڣ)0@#"D1:zĉ7*7 }`C-]6BIH/L]x K0.u/є\bD%ʢ-M9VM/x/YgU f>yB6<"1]XUR'эު0j׊bBv$( /&|ZZyCUpSH@ =Ls od$tbךQfn1|Vא HTwؤ"A&xvn"e t0k+)!ףn6*F髮e6n)|Τ cp+^OMHrd 02:?f8!dl*,d2Tv(Y5x{ $d-^%3ǵH|>U|~h1Cͱ 25`8`9b>Cx2Rf;@?`Ȱ$Fa/yu8&4lE=?ItFzAIkGJ|;wsTyֶGsbIV -W嚸f70V4:h )wf}kYc޻}Ȟ>gCv \d}TH H zZ+89K BTOݛM]aft@!A)(F[S'tu㧺d5ɬ~81s_Cv?e m mdv=jpZ'2;us2mꘫݓrI%Q-|&[MQ^Ӎ1NՊf=46,Nd#Q##6g hGn'Jlob~[#iG7Hʱ&Y% z\T]?!e^ K(ZԒh#E_fJ+8sx|k4}R\Ӄ9N&Z•u"ˑ)Tiþ~0Ia؁  Q$ӪK 쌌6$#$27d1P3 q2!Ǝptk)!WD)Ok\7' Er]*m%8CNش^+mt&}9Vf:x(3w,Ԧyimpl['<OLy4CI]LM|KRWZ\(n6עe6^sns\bb h4S _-l߈ڻ Rǫ'l< ۘpS܅s U+KR2TbE2q Ay|yw4y_"/'-Gs^i?;l'4d lV$@'Z+~)b#;E46y6ڧxurAy0܈Ul: P$(i,{>kS-ϕL\̩|> )v,oh\^T|2G Y5ݏZ?wQ#edqdUnx1Y 1rEuVڝYIڷ˄;#<Ɯ=w &Yve4ZAЯVݚ5;(xwȠ }=vJ3Irq4&o|{qkp $ƽ+ ٞ5(ќѲҤof2O g_K6j]FAv ~E}բ/دi_5ZE-Y.?DY*8tLwxv'-`7O8hBŵú-_L1brg%3G] ʣJ:}#q[pH}^튓[D7oB~Yrve#8`b)p%*x%(wvsƣ>oEz]L-qK]N'-s=E]i;.f|gMcwZ.e ׄ{8nVtM'A% i)'nSFr7?Q{R[3H-~@h|-cwG-U;c7MvrWY_ƞ*mO> U3c 6юjp!@ }sm?e u?? endstream endobj 46 0 obj <> stream xڵ\ˮ#)]::(%$8(J6( n3'MTQ.xϦ6]!n_o鲵"*(mjلy/.{//p[с۾ܷhDpfh/ۗߤVI饔tUJ?9dLV &ϻ[鳡j_G:_IdHgigؿP4A jWkʫ <1+=vJm:7S3kuv( qW.٭ZV}\BG]JFB\\[PK`~ݔ$ibn]7`>u{[[PKn"EkFM 2+I/n o$Vem3Ctyxx]/XoO$әo84=\ ,?̺78M#Ȣzo@u(ʆW]$:.Z0.hBHnp|C"lbR1ۅ@Vn6C:t !pɥKKGY`ǫPP cdzA,W^CߖP7yϓͨK!w wz RYֵmW+J~w2# JG@n8:voX&W-g UIR}+npQE!LcP{F6oA]Lr iGVmGZCw*LVbm7Tfŵ4;(U;x]Lffc<3FWV$`ttHTN#Cln+ɳ\IqYؽuTV\]T\{TӼp r_ |o$ Ed#KCdE73JcW'4GfLBޡa!OJv-˸2R6MUÂ|X.Ow@$w|uAIAE4|CNZM^bszbGb_$#yIWLX=lE/ i7 HsX׻HhxazYd)G/E{s,#sCwLxŇ ,!&#wrGK 1_6LOZ{2o C[9ɂA1&^e iO<2&uBs!A`4.E.pZXh& glXdJ(^H|&<.qa0*j+AxA gbńG2ˉGLkV&|2XM| ="E'4-V}PR9ig4%.?durn}"alsC9#Dk+"?L.&I'+ _J'DvqFpǟ [@ Bx016{w0e£M`g@޺o͝"&eLc ]u஺2I3Aq:ZQX9PSEHi5iL,#+[324K.}" %IjdkzD$AWF-h1,`!~/qKb<5!i;愲ie9qŦeTChBa}g}I(˶̚DzK<q4 >,5x)0 r0@Z,T>঎S9R,#\`1`9;B"WԒD'jCz@)e ,.I]Yd5+ BkP)fʧ"ʍxQr4{Abdڝ' _g_3´J tSt([b$Xo#&18m?PN޼UŦF9R :vqh51bx njYMW3}'pu>%hk2d-Rk(âC) z>>OqE$<ɻ^ڄr˃ (Rf'/eGLp M|0ncܒKXa߰φ /aצ ]eTV_V=\VxC|*Lmo0ϛ(3TNfkp.0DNa{Of*A=sxhz1ei&2FST6=_1"I5ZW^{1vGj#)A]VC<p2[ke֞$ K o ($Npg&x^Νr>NʱPpZs38u  vW qkI[KY%\0LӟB> )#w;1+o^ڥ^BWUZX0̓|ȴ3ǰ`Z2ɫ)> KDbjCj#(l7jKE_{R/F0TNbf`J֒P1. #dJTفIkYI{E׽:Lڮ+?c STópȽe%wy~DyuTN:Dղ ϫ?_J84437S'~יSF|f'P,aΑCQx8QE%s4> L~RPwye_5^ Й)lW3¹ 8p 4gv%&4ks NYM.vZqP0Sg[I/|k_Fz@>?Ȳ\lߛ4|@C2߷K3 %D哇(zTuֵP(Uo5沦7Lo$~1\- +;Ů)va愯tGwlvhD^5 ;ԄnDuv'Yl{X{c)&XvIC2v4 TÑ8&hup>UK wǤK*u\?4U0/8Egy`H&?PSޚSo,y[{,7vα 4Z=<Ƙru.},HW)SI[+ߩfe_~?_$ endstream endobj 49 0 obj <>/Filter/FlateDecode/Length 4539>> stream x1 ð'ݱQ$E____________________%___________ ____________W____________________%___________ ____________W____________________%___________ ____________W____________________%___________ ____________W____________________%___________ ____________W____________________%___________ ____________W____________________%___________ ____________W____________________%___________ ____________W______________F=̚ endstream endobj 50 0 obj <>/Filter/FlateDecode/Length 59041>> stream xTWq:H b]ѼX5],Q&#bIb AF,`hDQbC Jo]@@Vy읻r̳;*JG @ZAZAZAZAZAZAZAZAZAZAZAZAZAR@WRTu裏܊߻{vg9|pvIII0a󝜜w^޽{ۗ-(@ھ}__ʪxÙΟ?Rʹ|r<{lРAM4^:iђ{ڵ+88899YtܙE(@ҥKτUV}wM;::^v˗\\:q={6܊>xT(@BxxݻmҤI}͝;W :vgqsiӦ;wbCCFm022HNN ޽{RR9xBWpwwȐw7GqsҲz6lQ+_r"23000033^<==ڵk:Rĵk*[/^ܯ_йgjjnݺGGGwܹpƒu9}6mӹ^3raÆXeˉ'N:UEǿ %--m111>w:;;i}cڱcǂ ?z)ȁ駟*-ZDf...ʇ?}+;w 2DR B [Gxbҥ7n ߿ g^^^ ., #1bɓ'"9֭ezE [gs#GҤ]@vFQF2m4///H{uERPx!@ݻm6=Z>|̙3+W>}i>zߺukϞ=-,,cǎݼyӧ*UjѢE͚5pW%Azz}ѣG[I&"cyZqXָjժo&555OKHH {yRjժվ}{CRRĉ=< vvvmڴ8Wttӧ_xann^|֭[W\4@֖/_ޡC:uM_^ٳu֍5*TGzyym޼9!!iii ,XtÇmV wvWcǎnݺe8))IB QQQʖΝ;ٳ'7XfÇ۵k'oN0aɒ%yzz:T̙37nϘ1C*@߿eӦMG5nv횯xe˔(NvZQ[m3gʼnM++xqA---kkk:t(O.::SNϟW6+G"#GhV$DnnnΝC-]\>f͚M:W^qyiSRR͟?nhh8|9lٲ~ qRD]]]k׮͕7fe7=#.=9{lSdŋҮ6mڈHH(@d!99y:o ’%KFٲG غuZiݻrq&M)U?/^|=e{ժU?8qbݺuRiӦcǎJ.-NwYe]X~BCC7nܸm6];v}:22R>`=z\xqNNNm۶ d.u֎;xWj=ZjUyԴE  JV˓4mڴgϞ׫W~ԩu2󆆆t{RNVU*UZjս{wS8oFMqU3008{خ]w~gÇСógܩSW\944TdDO5x*"""İvjmm:K=:55uʔ)R++}Zn'Z]bG۷O"ʕT+w/RzSZF@@;wƁ"'JR/鮫]_ K; qE?b"Pl0 3Ga`?^ouE}'9wy&|w ,۝rZIIIÆ ;wT,TWIL:uT33|o߾Ǐamڴ177r/ĉW^w)KP^{ر"]Ҧߒ%Kʖ-+wrJcce˖#(D"SNӦMyyy5hSQSJϟ%oݺս{w-Y֗rΝ͛y@FD~s?7nڵS16l(\zUZ DK]_|akk+o*W#$fff6p|w֭u)% ._ _T*=lĨs̳k.9N$M  nҥ:4&&F|A΋h WK9~o)^9y  ?u?4j(em[*^[Qլnի\… Z*OF-WrҤJ}s"ښ J5jԐǏgwqA tRO?eyLǎ]=.-[˫Ӥ+KSJq=r>XAkt7 [rIM((@+Vԭ[m۶YеkWy3))ãrũSBWeIVZy8ŭ[*WܡC+ YײeK6eʔTcLLLvܩaDFF?R\TV)U~@%///iM^s ھ}ɓw=dȐNKK{s}G1cVP P?R5yԨQ96feRRR v$:uRNsvZQJ8qƮ]V̚`("sW322F)o6jHzݛta9'eQsX%KZj[޽^.]U*8ɓ-y6mڈs-.9y%h#ss׎yy322RZq1888țϞ=+ ] )K,Q+ԊM???F7gW\i۶!C߀[nqn5j܌O8!oW111ͫQƀrXw@Źׯ__r@f@ dnnn:˶ZZZ酋/.zk͚5o߾-ʼnE+[~4֭[={?|I&wTnٳ磏>|(24gΜ#FrJ*e+EFF*G.pXX؂ 6l 3gѣGH.\fkkɓ':ӓ@BΞ={|6888eB_shv%ԭ[O?ݱch"eZZyRbŐ/2 @n|٨Qϵk*gkrkgdd%MRRROf|||AM|}}˕+r?<!&&f֬YV2119sرc3@ƿkҥg۶mjzyAys[6552e$...r=?HϑKMM 000P{!^{ɑr{HH{꥽[[[˱JPIPlܟNmulllW^=nܸ/_nZ܇˘={{֨Q{ K 'OHSwo=keS +I֭[h!o.\P .]+W* t5}ݹsΫv.^ʗ|2r&>>Ͱ "i_}UÆ ٣곻{=={fggwA9 իW'%%UX1Lt\8##?(!qVHW29{1밴7o^XXX޽qiv 6Tn^p/Qb.UTV.}o&chff tq-goΙ3B ܴ9 .#F~W]]I&)[֮]ŋ<-wީXbJֽ{:uHqjjҥK_|}v+++*b y CiiZ24478ʗ(h8S!n|bb*ThӦJ;vT}+= 3 :7ow#+W[fM )**JS,|E+%]])Sț+WS-2T*UvRSS=eJj 9qDy͛Z'|"o:tH9.]$ͣ\%44ʕ+~S[lyWZ5Ouq,Z\hWWW[n|ccA)[Y'a۶mR`aa_Ԉngg'O>0aNadd$Ǐ=Ę7|(+U?]oOnQX}HcP~qƜݳgTPaȐ!yU~96mZ+c|)K?β(wz4:sp:4(v!-A8pr3""BZ,]IT*Պ+xVVVZ*))ITtxz[.K\-$ cf͚NgyX.ߔZ*|cBBB(xΞZQrٝ^k׮4*Gr-S˛-[T:={쒩Hsx޼yg)z 00PZGStt}ͻw O>Y~!aÆ,.QEn!k\,[y.f5 J2 DKKK^V^==ԯ_____O?|/aooo&ޱcǑ#Gj7n(7###skKKKy?/-[Oϟw޵kWʲ+c;+J )"""ѓ&MR~ :ݮZJzY{=|s \j_ZxzzN>]yCBB4PrϜ9,)55uJuiٲe^;\p%666,,K.jBE%KԫWO~qrrJIIqttܹsI +JryeK1***44T\fM2e UPaڵ֭[ϝ;'~Q#G:u/==رc1/_tҽ{+W,Sχ444r-줤I&tIzzz| njӱcG͛3229".>bww͛#˗\ݻw]]]իW;88HJ*չs۷Kk}?~w߭[Z ,pss\fʧIE~/Ht!ѡ"cvo_\M96m?v-(ZLMMŻp%R}ڵmEĝ#N'>Dѣ<K;vHLJ+Wnҥr\ٳg"HqNJNĩ8ĹD<==WZU(@ѣ:$75k̹Wz{{L0As+,,FRZոZj˖-tRJ?~>dȐ?x֭Y@ ܰar}T<+W, Ѳeɓ'e;qrڵ{8p… FFF7o֭prZwKK˪UK𔔔_UYH GGGuqǎ+Wu2W~]5i@lk6|,ݻwݿ_T 4Օw˛mݽ{8L|#&.n1B\O<9|ȧȪ1Eի׍7N<)#q5jį_PPݸqcj~zѥK{8~ĉs8 22r͚5[֜@*ٱcŋJ*%=^z'OӲjʔ)׮]rWH&H3Ls&"NR˗_իUV[:t0x]JRԩS5!i駟:vui.ߊ^qٳg+KJ"A onn>k,q(Բdee%)RNWWWB zr\,zxxtY*U|Ν ;; 䦮W]w}822K̟/^v͔{{ۋ7(=Eʅ k֬٥K cĿcccoܸ!(U˖-شiS=AHۮ&!!A#RZjܸqܿJ.-֮];SSSv۶m'ZJ@ hdccӷoߢUHwﮥNB6lX/qL9kmm4QI...`WyRzgΜIIIU͹rR`@+(@4@+(@4@+(@4@+(@4@+(@4@+(@4@+(@4@+(@4@+(@4@+(@4@+(@4@+(@4@+(@4@+(@4@+(@4@+(@4@+(@4@+(@4@+(@4@+(@4@+(@4@+(@4@+(@4@+(@4@+(@4@+ HA,x}ݓmZYYTP.{Iٶm[=Jg VPhKp\rm۶5k풓W^-ÇgesP(n޼)/~•@K.emf͚u%'x%%%J{gbbBN@*3X̀P|O:UI@ e %8ZAZŋK XpQP$%%"%8ZAZAZa@ J2J!~ Xzzz'(pJD@544,I$P(YH ST)))1ʗ/ollLZ^k@ wޥ @|@D|XHc4C__QFrLB@Mzz{H31* 4аgϞѣdySOO2)X/2Xj\2yJ8yOYrܹ"50 u@\YWWޞ3F|(GC|@ (@ϦFFF$>"oǓ\+$2 *52=6q mʉ$Ȏy(@(>RO'ŕ0 JJOO7K*EN(g@(@@񗑑j@> j hOR)7 @APhhVPhhVP=IH&BY3c==h{l;-<s0!@@+(@%8hF cP(@(>ll@ }8Yfׯ^ϕ47gLnϪ ʙ4iظa&KKˏ>HmoZZg}fnn>c %55U;vl޼y[NOO߰aJFTTԘ1cv%|>\ryCC_ڴiӰaF);⧩):upA4P}ݜVfcݺuz͝;7))Iċ/nݺKʔ)#ӢE kڵ"޳gϽ{핝L:u͚5" 0aBؽ{N!\,) I䰨\nnڴ;MW_}%*ĉj/VZ~W^RSK޾MKec3660aڵH&gg^+\;d4P̤ƍ{/_,*J{9rtdNx)x Y KA``[._|޽{DáCJ666>>>]v知7vСAbS))} Rpp۷U&wRys׮]򋕕U#ի͛7s;w`&kIBrJOO˗/ի;999\r5j|}i޼޿۷o顡r'uyϟߢE  *wpphݺ-[L׮]7o,%LSSS1c_Jrtt4iRNQ LYݾ}ZKL9tCo9{9dȐ\v{rM ȂWޮ];R h+kf=yRQoXXةSΝ uzz,LC jjj:|pReB Gmll 3h:uc@,--w>`7n899Y[[$4CO_߮ZM+ :4--M3fԾiӦ2eʐJRJYXX5Λ7_~$@c``cǎɓ'?ظe˖SLiӦ Ai(ZlBUi)HBD@񑑑 CZ hѕo$9cqru{qr hVPhhVPhhVPhhVņE@ald8n` VPhhV4CR=K<pQP|$.3HmblDN T:OIÿL貊O!΂_oِ ZAZA%&&&==< 0 ՛"VJE\ZZZ>}6lPJm RWRH;A*PRl۶~#xc FҺN<9h ^oh7Cд{'&&Q<}r\4P$TM6:uѣGiii666͚5{e˖mٲի/^Pn?~ŋxSLQVoݺgϞ˗ׯ_}oooO?K;wAp9__Ç߽{ӧ;vKHF1`pђ _޹s爈޽{oڴ࿕֭[W}gϞݸqPY~AիU&o޼)^"Unm۶KUo777##]:oy\IcڵfJ6mR.]G͘1{Fjjڞ1  -Z){{{,՝8q/[,))Iu'N@zd֬Y3$g{_3bĈCgϞuvv5jT2eϟw-""nݺurYR|yUrD֫W/y{0a|{2dcv/^,^.O9v옲1cXYYIΝ;{ݻ hGzFץDLBo˗/K7vժUK nݺ%_zU bcc^b``0nܸwߕ+W̘1I&"\n݋/-[&_.QFkS\9yӧ,Y[J92dȗ_~:RRfϞ+¥͛{a|`4PȤdff&ϟ?c)X`Abbګ1RJIu<+VN:eIЂkJJK@{{,_>~x)޷o8^9-|^*Ơ4BaB6e??+VpXjjmVz|ʕ:\vMynrXZZʏSw^yI y&###yy'Oi&&&zbJ%7"+_!Cx߾}rNjE(d}1brk׾{yrNfVK.-/LZ~ɓ'x r\ZSfX-[;wWOMIЛ6mW^ƍ hIKKi۶mnlll;rܸqSLvڞ=HKh|bttt^O|rI͚5#O8EAZZڊ+~7}}y999EDD׫Wo̘1稨/cʕ 68X7IVVVrs/,...7oARRg̘!41(|׮]kܸ1c5kv~K? :t|S7ovZ- Z5WPR("?Q0 ovXG>x@;tݼyS j׮1~a&MXz@= @a:p@XX׫WOmoOܳgfcΝO)V+pʗ//Łij4k֬Fy=Qhh|9".]ڱcǖ-[gbb­UP ŋ8&&Fmݻw8%%Eرco[n$h*&ɓ|XwIqbbի;rR ?0Ҥ -[v9٧O=lذX(:(@(>Lf~3@O$VPNLSJHH>},FdN5;LIIy>e8qBw%ףҤ@!;Ǐo۶1hͅ#G:uJÇOJMV\y}Xlٲ9 F__o5kjՊ &e}„ ǎ ]lYL^77'NAAAR:\tiJJl]*UJgΜiӦg϶گ^*ٍϯq"~Y>}x4 [nb<9}1UV5>~۶miii>}9"IMM-Ufff zϝ;w6m$սwsll8\@WoM ׯ_/``` umwwQF3.[lرɡ8Ǐ[XvhѶ%dpyyy5id͚5ׯ_755QFϞ=G!.=<<ӧO-[VϘ1ٳkU*L"622rpp7nܷ~[re͙3GWWwϟ?g\7sss1'[?Cbb[Z&M\RWO>dÆ gϞ>}ztt &?07vڦMUrGB7!S{WRz"2o 6\pڵk[5s(% yHy!%GPPy߾}IEBLLE yϞ= 2d9(R(@@ i-99yܸq#^ׯ_7o^Ϟ={=Q0"/_k?k,##Ν;<ŋ=zHKK s?־¤vg2EgHѸ+cr$1#8d^.a8T82^s"EJ88Ea2Ր!#BWv{?ӍZ~~pvC^zVZl-}sΉ'(4|ロnZ[۷ʕ+`4u555={VsP{ort:->kw„ vgcVf̘qΜ9>jڴi.].>}l ,xGƏ=t}UUU&OOΚ5ul8w5kִm۶C ))ߴpݺu{ws+oם:u'$x7|{lѢEիW8Gɭ|'S79Szx7?~|[oO81駟^vmN,Yҿ>ۂ ٳgG;5kOFűGEE޽[ׯ@@~,[UV۷+V\UU5zΝ;L>n۰aCM~{,_}ջᄏ~WsϡC.Y䥗^W.\xF8a„m~7p믿mVƏ{FE+  ??yӦMS׿kSTϏ~O?'3s&6|ꩧnG}tt9p\}g˿ː!C⫝:u:#۶mjժ=ܓ [K vm+9뮻.>:vxwx㍵~Y-ZտssYfvZ\Ϟ=;xF }}DPW\[O?ӟԠAyzH;t6l<|O'Jy ,K]t޸q\aÆ=S8Cnݺ?нK u@vr5k4:A u@ӦMsuP̀ދ6m QRQ= nnH'x" $ I֮]o :`ѢEqQ^^^ 3gN\}ٍ5eJ⢦Ƴ@@tUW]U{=l6obժUv |SΞ=;*nv޴nݺ9tzw-#}'.^{^~I<_|%4^xaOji:tf7?~7oޖ\lY^z;o՟~f[<ȸXby+C{[neƍo sʕ/_roƍwlРAM?3&رcO<'|]tR!]tE#G\fMMc@"J>}<_|%4.]L>n[? \rɅ^ئM{}]wf7_f_wqꪇzhҥ}љ3gq'N|/B#F2exe˖wרQ#]N@ɑIW0?zÏ+*) Y>}?mDŔ)S~uHl6a\!^{MH63B @h@ ! 4A/Jվf%l) IP}\9EE4RmJyC@r}T*UVVVUU_]~} *z oBxиq\vZS8`F NUWW/]T [#zV4i-߄ /--%k֭{w4iҨQSn.d2tzk׮>Gor&@rd3>R~y%Jj>ȝ\]]j́me @rdjߝ19:?Q 4ojgVR:9|tiiVDo- 4qƵk׮[JO RVVָq&Mm@6\ŋ7[+ Jߤyl6wQTT$tA _:- |9 yRTqq>! @h@ QG\3ih 9K9\!쐆{rBg` hH7ݯ>3B @h0HL:lq#J|tHlfu/#8B @h@ ! 4AD $).-!}s@@h@@rdL*JR/Mu]eU&-a_V'|SXp=i#8B @h@ ! 4AB @%Z$8սC\!%V\ Ȼ:9㨣>|'ս_9sѣ+++=з~{t|cz V W\9nܸ1b-]c=ܓoL>Sk謆W] ѣk8 `եzĉ~a=JKKӧ曞ETO>9$hTgsΚ5C?я[3f̊+<<;#袋c9|L^x>+2Z;vFljvr!_|?~_{GuTt[n/_~G3Yf~mO[o]QFe]֪U;>wy]=#g͚3Xm[[]B۩^/u#^rqvms=7J~_!rdÆ +//_y,Y$՗]v٨Q?:t<̄ uE\s{왋NzGfN/w/o~w}I9ަM/8~ q:uk?oƌ]tQ&9䓍`KmOoz@ԍCeee6mD^-F'|2vl?Yfb7묳>r\5mРA}-ZԻwohXsWз~{tyYg5id˭sW_~z{Ĉs.{l^ziɓ'?>l۷/{/G5hРT*:nܸg;owP>_V|Gw)[Y=Vp+V7n\߈5i$>8\2/uѢECx'3W^ye\;SLm|{{{-m4 <*3?lܸ1*6jb\YY9z뮻nW>ȅ ^&Mׯkֺuz?|+VD#~ӟSN]v wߝJM֪UyuM4)뮫l~W3̶p\=gΜm|qǍ7_6mڀ.첾}[.]:r3f 2d[СCnHo?K J 硬ZjɣF?~ү_A~:t֭6n׿ofc7^}C=蠃|ͻ){׷o??:r5f_{nȑ<-Zٳg˖-kx_~~:x㏟9sftԩSW^ynx׭[עE#8_~zj[n[Mû+z{=JKKN=T/J  (Ɏުsl^{9蠃ƏWn5lٲ:QF^@2iA!2eJt9`C]}Z$Fϙ3箻 R+_<4hP˖-߀oa@@Rސ!CVZua 0`}.$z6lؓO>/Q^z{$/!ܥ7oG1vTʰZ iKtM6l2e۵kׯ_ |+j2kZ\IK5n{:qݾq @h0/kqƺ}N6n疗 9guH׮]5! 4AB @h@DQZZڧO\!%h=aAB @f@QUUOO~򓲲2=#4lO?_Fp ! 4AB @%ZlٲW/^\jEE@EN9lfZ #e˖ZCK $Gii'! 4A $G&Oz=fĉr!z&s=\O J˖-k_H6 ! 4AB @h@ JH'ZIq5 4@rWTT@0 ! 3䨮1cF\w޽TOH $G&ys! 4A—N:uA*++_xᅸ>ׯ'y h@ KHRM6_h_D^gk@@h@Hl џիWg@ RS]o޴ȑ#7nܨ% 4AB @h@ ! 4AhEٳGI7< RT\Ϯ]j@@h@HtEMV]]Kf}ϧMjӦMv  $Fj߾N ! 4AD AK/ r48 6m@~ B @f@IH+wة s4AB @h@ cџÇWVV@~ B @h@ ! 4AB @%Z$GD?vZ\\%={ 3 ͗o:|ϼW6_ab΀ g@ɑɾQM\C┞Ht޴Eܢ\2 ! 4AB @h@ Q@bԻky $4@rRi@0 ! 3ؘΎ~*/aYyIJOH $G6[oue6W_Fp ngM?W+**`@ f/j_ @h@ Q@b߫8W޻ї?.[?HԙnXlQQ?! 4A $GM&ΪL\ݳ8EHzRÎ,/xx ! 4AB @h@ Q@b߹,W_h 9RRP 4A h 9هT.-+1 |~p}j_'uAwۣݿi"#8B @h@ ! 4AD HRM6_h 9JKKO8}(Fp a4Lf͚5q76|@ɑN}ٸݻwYYB @h@ J0-_<./^!hu%lfC 4A h 9j /4mڴ!(Poz .@͛EEEnu/! g@ɑNΝׇzhI8|ɑd/^ם;w2 ! 4AB @h@ JHRM(h 9@ ̀#VWWuiii*<@Q]]=iҤݻwYY! 4A͗N8:u]eeoקrJ  @h@ ! -VRҽ{\!%V+ 4A h 9̙3#=%#NϘ1#v*/#8B @h@ ! 4AB @%Z$FyyarHT*U~}}(Fp a4#G.(-kČ 4A h 96ĄO @ fW_Fp ! 4AB @h@DQTTԼe\!%VZ}@8C ! 3d2+?^{HtuՌ)u<@-hNjڿBH*4lڙ[lVH*RB @h@ J):cky}wVkČ g@ D+) 'j /4E9@ ! 4A $Gjcz @ d2K5 4AB @h@ ! -#J5l$Wk@~ (-+?g@0 a_Qf-xcn~Cfk}׻7n{K $ƪNaRLO h@ػxqj)&B "*b"Z1̤ԌoS~-:"2%zIMh>3|g>g^sg{=뾽@B(@ $4 @B(@ $4 @BK EO.UP,Z$tW.  @B(@ $4 @B(@ $4 @BK4J,yG-h yTPA !Hh@ضm0꫽h)@#_>%hP !HhBP !ҥp=*''gתUK :QbT$@HMM=cXB4<233/y8@!@B(@`oM2eV@rSGnnO?ƵkNKKF{Uե4ďYYYwy瞽l}r0q&;;{] P}vKJJJFFF|A !۶m>3̚5k'N\hі-[W}嗍5*_}>G>-A_vl.ښ+ P<|r/UX,ZC9#J,)-{Ozegg{[onݺ`I͚5w^馛ʕ+yAs72dHŻvgKN9W_}5\{@R Ѽkժ%-$_,X#>iӦP轧۞zyeee睙5k֍7oj*\xI'uO={v|zeʔ AطjfI .\PطJlY{Iz+ 6moً/t *DP?3w+VXr/63g nҥ۶mϗ슼8\<['Lgo(@Rone˖ҥKǯjѢ){gz!7aÆkfff͚5ӕ;<@9*U̟??+++\g ЉrC=Իw-[įիWFd F2edff 'IӍ7[DڵAM{aÆ 0.ѣGǎٜ9sڵkNpX\9 eF轤(o9W__ϟ߱cVZ=S'pBB}x>(১otI F_ `ŏ3&~Rv)82eW_-8qbƍs_GN80`_)<)a(@'Vٲe_}{O >S'|ܹs)jU b +'6ݐy=sΙ>}!Cԩ.ڴi%e; @} .`_tEK.\y_.3gΰadHV лjoӦ.bn%K|'۷M>=~m4? X޳Yoֵk7~?;W_.]ABsR`պ&p+ 6DqNN/bw}СC<]aI&}/_n]q˃ՠ34Sܼ~ZV Ћ/^vm>(ƍG٤I'x"zǯ9sfYf…;Cn ڶmO?͚5h" rss;whѢŋviRQ$rrr>裦M.]Z6HVť3 :;Xb>zgv=+W;͛`Ç2+ܷ8pSO=?|xpj֬9dȐp71|ظqy{r… /]R(nVڲe%KON87z뭱Xsύ^uG9ZjիWW>^2e222tŏXt$tuַ~{A=savg٤I˕+WokNER`ҕ>{ɒ% .lڴi^L/qQҥKXrGArx0=ztAj-ҥKnnTtJwqQ}e-\M*V}rЙ5k$,nڱcUVIHD^:v}]bEvlR&g!o,@rJI8(vuԩRqRϻ_ySO=5Z_t]f֭[%~;1B*^R$ԛϫҥK[vڂ %?6lnw2`{'~In^~0^vZ`|o߾fee_>|7|gȯ <88`ƍk׮o/YdI,7n+_dav%<Õ*U ͛Vzgp gqF[57O/_qGwyݻw[l?d>}_gŊguk}~ G9?\xqZjp5s1k ,|(.ڴiSIpkfϞr *}]tM7܅A' <} Eeʔ9ꨣڶm{7֬Ys,[,# ;X2cƌ^x_pafffƍVZEY{t߇I&yɓ'W^=XuuٲeQٳg{SL9rdZZ7~Qŋ?|ԩS+TP9oٲ媫?-?u瞋 F#^}Aݻw'>O>O?}?Q㰞>bĈ(annܸM6'Oی;v>.~ N/3<3۱v`'<̴iӂ3ѣGp#™RB^ziӦM8|dgϞo6j{cƌ馛N<[oVZ+W|7׿~_^z饋.hzcp㮹C=z׭[\O<1}z* 7578n^^^ 2l. M0!SÆ 2dw?_~$vܹ<|qƅ9… K ]쫯KG k(Zndɒ h׮]~µJZn۶C7u ly-h: v#(.iӦխ[wW6Rʹ{I'A,+655|WXjժ;7ߌ =zKڵkW~ zlWZ58˗G~_&3g| W]s5e˖}w~͛7_ve#G sz]M|E]۞|ɔpy˖-o̟?ǍW\pm|vuaYݳkֳgϧz*Zd ܔ~{0aB ۴i$?gq^wu6m҂8\5{+VT)5oٲ%_|ѫW֭[dΜ9~{M7?jԨw͛76o`tu%W\Zp)S%QwyĈO _%AԩS2###E'8n8YܹsÆ  >Ͼ馛M6nءC9sT}j*8t ھ}F'|i&0lY[o6v YYY8 .իW7n'(piu?~|Æ m6y.];1cƌ믿VZ?S`w=`͛7ggg_{?cp_N:edd }zI cvZ[~u|Yg7|sTܹ#<|w*3A_? 4(*@TD_ A N?X+BH([oٳgu]t~Ds! /8y4qNN{_}7+xѢE?x|9tW%K~ǝhܹoFTz7Gmxm۶E_y9sG /0 ^xUѬ+W>aڶmp'xn 6/_a&իN=ztM7ݔuo?EI<0^ti]\?ܹsgNMM ƛ7o+ *D UCrʱƫV U^e˖aGKZj5>˼xԩUJhwuW"~3QGOprԩrf(z꩹sѣG?ќ_7Q ⋹uپALs| D%xO<رcÑ+V5jTX޴i?_"OO>dguuE*얯 ;ogz?9;;DhΝ;4yO?*@Ϛ5+ =Pv 5ɓ'/N})rm:Ø1c<o]pBac&q\NQ"~--[ֶmۋ/8XQg ;(e˖a1nӦMڵϢ {D9 *[.ĕȟ " TrkYlYl޼Ջ+VӈPV=yE{SN6lW\NV>zu]ʖ-9V0v+ԩS8IBr .9~:zΚ5k ƍ`Æ ջ+qy( t'K?۶mׯ_zF!V~AN_?8a„^z f[f՘7o^ҡK,)!;z%.E"M41c#<M.o.1:`)@>}Ŀ'2dH_uvvvDsADsL0aWiӦ}{J5k֌ӧ2+]"ֈ}{|s *U駟 >@b?S%>3~_U =gΜ;;tuߞR˖->*ŋ 33y4<;y„ }]፟~t]??~Iʕ 0cƌ}O<~S4P4srr~eff?c9fWv=ڵkj׮<̨mm~ᇮ]|.Ғ%KСC yb4>K.)S̾=(6l{ i9bĈK.zQN<987޸C.yv6G}Xc=vL,7n3g]Pn?]C!*W}Xxu5K/Oo}W{տ]vd|wܱ2mڴ.誫W_M[ly7v[Znu " k ߟ~i׫Woȑ7g}6yU-֭[T^loݺ5wX/p=2zg`{,CY~(5jT&MƌM|?񏧜rڵk{-[_~+J#W's'X[Yf#[w}wǎ?(g޽{v;_Y s%.]8u~aT(?~p:lٲ>ltv6=z\/c3t9|76~֭+?u ݡ뮻nذa%i\pO?}׿m3qpÛ5kְa?>k^pQGU`(>}zp (޾~a̙3/^BYYY]vﯸE@nh3f\x%KZڵkz_R֭[wg駟W3wKrF}y͛7oܹݺu{%_yApM7~_~e'{v>ɓs\e2nZF A;&L0v|eʔ9W\+UoF1jժ+VlCvs{N%/]41ڵkǯmѢE'|0jwsРA|SN2%yLO"ַo_Y(O}"Ȟhii KիW@?G_+W#Qn믿.OgϞݰa9餓6o/rpп/={?ΑaÆX,v'1cƋ/-շ~[Zsrrf͚c`*TTbŌ[oէOW_ ͛79s?uW^yeŊ7m47|k 6ׯ_Y]p[l N#lnݺpJ:u;iժU*h_k)WR`&xA* 3}povҥW /\ d)f͚.{&sXtyAKs2yO>=~rᬬ s=785jDkU0xhaJ*&˗Np;v\`Af&M;(iӦ}Q%7WLz׃ݵ>,8tϠ'|=\K w'a=ßϝ;7*^zi_ۗ/_~mE}C)Rz {W֭[_#5A׿\uՃꫯr-V=Px/v!-xDo/2dG-'kڴ$֭R_`ycƌiذ/$//CAB >g>}zvvv͚5/袞={Aoh߾Yg/|pWs̜9s-ZT>jѢEF &N_.9sfp&L駟VXoРA۶m__4_x֭[;찓O>.ܹs#m .jApC s ׮]駟[ s=oM7nk6eʔ˗Wt'>o |饗&Mt#8SO+.㮂 $p?p>}y￿ƍAcÙjNªU6klԨQ%_\xT}թS'UiӦy_|Ep[c= `A~;?/[R'Q.(4p @/ s~1$8!(Z @_!g`3<P !HhKZtiϛ7/kժ%9.hڷoXLr] !HhBB hsNK@RGZZZZƨQqPZjQ7:%s@ $)8䑝=iҤ0nٲeFF!h y͟??%hP !HhBP !ҥ b,yLIIYVVѣ/.Y<~f h y-[6% '|Rh ydff^~bܹ|ҥKU֬YuJ Ŋ4@1ҰaÌp3f(O:{ ,oٲ3yHgϞXQFk޺u3,Y4iҩ7mT(`9r7ܩS~}X,6tЛniƍ6mꪫL")@#<<˖-=gV޽{~z<+W^~_>M62FK ideeXB{챫@9rvaaJ*%'7Ǐ/àq2Fm6o޼ hOB(`6l͛?W_}U6(&L dɒ~#Ffg4$ߦMM6 :㏗# Q6l>Û6m 6i߿%@8wJT.rJ|1'En̙>СCׯ_-4hĉ'LPF Y"陂7tIhW^y>qƍ|M)b?S$jժ^ziˇƍ,mذAʕ+y}OСC+.\w~ 6lн{wyy͚5+s۵k7x:bѣGwuݺu[_Zzq={|'Ǎw1q-[ֹse˾ˆ?S$ Q6o'O^fMr-F@)@@[dIr!._<777~Uǎߴk.LzꩧVZOϼ Hrb}ƷY246n uŏhNIIy׼y /Yf%K1bĄ gHh yb%nE1,غukw=iҤ.77[nC )[$S$m޼9SS 577*62eʴ'9s@@\(.UqS"VL]l& E|r˖-ar!H8h(b5k֌⼼BZY& *W\lp GըQ####ׯ__H˨}>h(biiiw\^/h P7o~dee}wa|K>h(zZ 3g7|g}qS艖RaiA  .\rAϝ;wmƎoܸqSGfz_{^E6 HG@EAW;MDETU,]u] ڱ]ADP uYA"5H͗ϟ@IBrߗ׹3Μg&8ӶB_K{*U7'L6GkF#(@@p 7$$$C=nݺN6-m۶\rtGPdffnٲ%7nܘ-[u]A2dȐܳ~*'L֩s!Gӈl۶mܹiiik֬Y`ĉWZκ馛ϟ߹s&M$&& 4nn!;;[ny饗ϙ3_dIƍ'OܪU+g2{ h^ԉ BIʕ+;tYro 6|n3<3h6uծ]uփ4hPժU%=4P͊n[3-XR.[V߼ys@͕hLJhH覢G%.ܹ$EC;Jg}V@4k֬޽{kn͚5 {ݺu4h ?%hwy@Ylٲ=zk7,_|8qڴi)))/7)()@@)էO>V}^f?Qn#~'U߾} (5RRRN:~7Ǎ7{kqƹwޟ~9L)@f)))ՓUVխ[]w+ĸɓ';( @鑕~0YfY?ϟ??8٠PJ?SGJIIy'Lx 6ԬYm۶}袋*W{0hذ_~y-Z*XOw_)@@>>}{-ڵ?e0x| ^njSr`K.uD(b Ph׮]hѢɓ'O\p;w{5ksӦM !8Ќ=zrJ|yLYfMA>|xJJJ^vs5jԻwI&}IIIۮA| 'l7렃 ^'NOwFCPBsi͛7f͚ 4&NXڵk0wf&M {,oN:awڵk3f8&4Z9va^{----k6mMΝ?6m{>ժUn+ArrcDQ24> ڵk6knҤI}zaw}Zqv49sO8 fΜݬ>:#H:͛7dPeee-Y$w&6wͪ_nO?valS-_<6nFdĞ4bŊsonӃ[oU)b @Q|N'%=ʕ+cqGSN9eO?t~[o5cƌ.O>RMK(_+q6m+TOˊ+AVVWR裏k6m_Kwuwy)@@1ۅt ---o`~z…?|G|+ QbyXߨٱ86lt^X1徯9鱸rRG hȘ5kVva 8 UV-`m۶իK% Pzdee-[,۷o/!)jԨQ[l jժI%!85k,geerڵaPNGVZm6@>*TwްaC>-ch(~UT۷oO0amF\sGPnHHHzhݺu͝6mZ8t۶m/b O>nݺO$DAH(@ h" @$4P  DBH$HPj/_P#wbg" rΖPdee(@{_Y#==]NBOob h\`AlstXm·6mZ/Obqϸzw(@?N>dnHKK?~vi+W(SRRxÆ uU'vSvvvПgdw{S. FvGЇ=I!}jԨ!-r*T6nܸdɒ5jT^=!!@eeeeddlڴiÆ ASR% *..QF?cJc`{U6mT(\A-m=[XPT"OT !8=^ժU[nذaƍ۶mXbbbb5*UUh4^zYYY~Wt4Pŕ/_^140PzTTo[, #G0P  D@鑖#СC PR%--MJCp h" @$4P  RPfmݺ5/^XB(gUx޼y*U家•A e׊+bq>}$RfԨQ+Vvaev Qv[N Re ]vmذA Re gcǎ8))uֻ /p̙ _|٤ZO $)*,^86oѢE o܍,OJW.w?Io7uvoal@ emy/sW 貫Fu|.ZjS7IrBUoa ]=q##V(+]߰F}џ,[X4PBY[lO*\d" @$4B ^صk͛M5RE}PI ߰F}Qa7KP֦o}qٲP6}wmڴ szN1j]o7.Cp h" @$4H2nݺÆ |+.;; @3P  DBH(@ h" @$4P  DBH$HAW_͙3gڵkٲQG'3%Kyڵ:thIKKϟaÆFmI@M6}n޼y}9C[j@zo}ӛ5kv7o޼$gXl|GG믿WM47oҥK[hqmRD?|KÆ +"[lGС{7߬\}ݻ*kڵzs=ݩSU~)))]t ~q kŊ7tT,L9sfjjjn~SĉvL NѣG4(==K/}' B)>|P@K. Dƌ|S UѣܹsI&k׮\ο6{qqqrH/mΜ9z?ةS~`ّ#GVX'K@۴i:~{/bz)))W\qūZFW^yW^%0hʈO<> }c=6m߾} zYfm޼O.o^zipgeeS~/r׿͚5KNNUf͚.],\jժ|N?Ӄ~aݺu?py~E?HOoذaвI&+v=7߼;:t(z>nݺmܸm۶^{N6O裏7x#hӨQs_A~^r%۷x`ŞqyZj1(mv{ШQ \dɒ N曃_A0tЯZ_ ===Mf͚ ֭]#FTR%++[b qgoٲ%e uTxyqϞ=+Ucƌ ~::@֮]Og--[d٩Vmܸ7c9&..njժu!K/$oo . 7 W6mԩ >+W߭YYYeKJJJKKc6m5ߗ_~u0>i'0 j=0~.fWkԨ (_oժUʕKf]}e˖lݺudee}RPXWׯ={eۅ,Xzjr[VZٲ G#~'`%t7rF4ϧes̑:WeggϘ1#Rz5jpq(˽ի.]Z 55uɒ%%' Х?6lOs;B {キ+@7mNiРA~/S[vm,^z>-s]f䕕aÆ0VZ]_z+/S'+etL FCmݺu1r}b_<_)<K͛7+4MMM:ݹr}bSZj,NOOϧe`.\_z؂:O@Wf;O/1112K W_lA'+tרQXlׯ/uy~y'W/ޯLuޯu Х߁i{nV 뫴׻~UV-VFoѢE||y*@~mڴ+W̧?åp}_,Xn]w<_TJZhQνx*Uo@㏏ŋ-ʧeln\\\5jtIꀲZjذe-z^xqtڵ|%' Х_ǎk׮Ƴfʧ3 CؙN8!vM_뫓O>9!!Aꀲr),6w0ٳe-wg/^xڵ%T.???ӝ5KMM {ݫW߽ZtiB~۷o׮._ddd<_Gu?0TR~JT˄/<...,Y7oCSFJ@>0裏RRRv&))) ZhP{'OYvqw!9P{+^r%a} @ל9sL:Zj:u V2s̵k>إKsh͟|7mTf6m}*T7u}Nj/޲eKZ=#8¸π/ӥKW;v /˗/VXհaΝ;tA%9] DMDBH(@ h" @$4P  DBH(@ h" @$4P  DBH(@ h" @$4P  DBH(@@ijժ}^ 46ƍ駟233 ܚ5k5kyBYۘ1c?|Y-tƍKLLի@Rxe إK͛WV->iݶmۆ[oo馠M*U8;;;--_>}#6mDJ ?3P38cիW`ƍaPj[ouХK?{ۖ-[Ji7n\z:|;c@PPk֬w>q/322f̘Gyd;pCݺu;U֤IY-tƍ{O:$bh _OHHxgv`٩a?)U~իWzes﮸H9|;w@PPIIIg}vƍw?W0 cw( (p.(6}^ey\jA$+;;{M6[>v( 믿ިQ.]&v m6mj֬O-.9眼c8| h $))鬳g98֮]oЍ7.x=s>Jh $%%e4(©ZR,\j;| R@A̚5+^I&͓5uUV <p4#v mF7o.!Elرv2@qQΞ=;(:ujѢ@ɡ @!ꫯ222X6l۟>" @!(Daܸqg}@ @! \jKHQڰaÔ)S=F9|( Rn/#HH(YYY){Okֿpس`w͛7oݺua\p㏏;==cǎXgY{Cpb8*]vMKK{WJG򓓓M֭[ZjE{]|||ΝKWT_~#III{n9|A][h>5k\jF:7n\jvg$=P`^zѢEal"lٲ/NZ@ n1p17n\ڿ)A qƯ~[K/Yf0/HKK;N>*Upٹs?Yzu&MN9唺uY|M5k4lci޼nڀ_~eʔ)M6ѣGڵSRR)?s^ڴiku>`…ڵ 6|ܤc֩S'HZٳg?TR_?Sƍ={wycY}٧gϞ5j[˗/>[n֙@!R`nmذ \x?cvvvcǎw_;w~4hĈs/lٲ``AM~_| *K^r%/+5kW^yVZ;۶ꨣ ֯_ԩ.l?G}W\q93gΜ38cO<Ċ+–IIIAc=GV >%11q„ |f~n.l_9-[o$L4iܸqzj:ux ;p={ ^7o>}z׮]`۶mvX 6o_x`_^{u= wp;8L>3mԨQwu?^!C)Arʖ-[nܸ1˗/g'?|*?|pY`l۶ꫯݺuׯ_7ns^xav:wIIIO=T͚5z=;Sm۶=SEwte˖Yfxmڴ9#'jo=x]V|iӦKM0ᡇT6{[~k6^z͙3祗^zg >b޽{'⋃f#G|s=|q{ }/ܱc:tIƼ{:ÚxW]uU/feeAA9ټys>}ڷo_1wڵk7x#X`po%\Rz];:utu7y?lUX_~GnѢ2g hv_u0.mŁG}UV鉉?|xs̉M9sիƱ)C g}Ə{r9ZG}IܐVmܸq„ Çݱcܳ ׯ;ଳΊM a{=X|^{m:uZ֯_Сw1&Ms1E4h??NoѢž_n߸qvݲe?_s}c}9#yvL o Os'8j֬/@.e4nʔ)MСÑG~KkԨ1`*6>>h:Ǐ?gΜ;8G=餓n喝mɚ5kN<6,V܅ Olٲ^{ծ];61i~W0;#W\9-WO]K.=nm۫WJ*W\ߵkF_~@cϴ >l3fhԨQAE`;ƀ~Ԍ͛7]?~衇bskժUJ5kƞtI_~ɓO x4vY 1/r{E9v+V8aoMʦa9;;{۶m`z }[nE7nܘm;vlڿb<|zvS*VX.Yrl;K.irnQ6lԩS.]gK@IOOo֬YX`ݡsޞ{a:蠃׭[.[uֱׯ7o6駼צM.\xI'XbCl@6mիWgdd$$vrrrwԩNYti8}Onǧ$+T劣>wgZÆ { =ܝwLIMM @TP!ѩRJAvۨQw_wu-Z Gݲ֭˽ѣc=vWGO=_M7M6wӿ#8"C5ۼys1"c ״iӒx yϴХ^:y_|o/_|RRi]1(Yb#72'''wG0`ٳpjܸq& Y*UzW3ϔy$]\S ӧ9κcmbh3lq>ƍW.&egK!i1ݺukҤIph3QdσC9$|ܰa-ZԺu'|27 rOΙ3gͶ5Pzpnݺ]x6l˶k6 0f̘cѣo߾^}?r?p/ص`V,^x+̧u^{Cx~>ϴ8\Ny޼yYYY*hMާխ]6|:W_N㏃O<16r(wrӦM A}?vk<ꨣy>?ܨQ)SO2CP.g|6m 6> 8qbFFv ӱ` ׯ_g>1+++ĔK3-/8>>w޹;/RLbswo7n۴iSn.pJz>sكY& Kk.#w[fN:餌~ (6)))W^y8--[o}O?Ͱa*WL >̚59rdr8#ù#F4hPxȐ!6m*S~g^{;#ί7n_b{YZ`: t /ԫWe6mw}ņ;O>A&LmcM2%o4l;ӥKpbv09K,==8y*Usmٲ\N_^[j}ӧpM\lYgR3m;{npJSeqe,Pԗ9UKN>駟޼ysZZ~wWz,XCM:533^z-Z>|x_>k۷oE{q#GΝ;ZjUT93TT)5s;/pX *4o|ر;vԩS07\COSt.]]6^|Gs3\曣FZbE%UVׯ#f̘1\pSO=5hР?j+W</_֑Դio18jA~ǰhذE]twƖ jsι+O>ov1:3 ƍ8)2cƌX^ص3-`v_b `K~3.Xz`JFF-[g=+W1cơk޽ԩSp ;ӂCvgw #%%. HHHSN offfϾWN9"s]tEp,Y$---xw_ݵ{Tr!\sMv֭ڀd`7)@@h۶m ^z饴6xgnݺ322oN;M ݔ)S 5k֔ݤ @Q[pa̝;4W*U}ݴ:L0!ļ3f\p~O<ѻw][!C,YRZ5Pi]t[7|s]w>!̜9s]lYFFF5ml ڛo9iҤٳgغukO>;^zNy3NOOo߾}@BHH(@ h" @$4P  DBH(@ h" @$4P  DBH(@ h" @$4P  DBH(@ h" @$4P  DBH(@ h" @$4P  DBH(@ h" @$4P  DBH(@ h" @$4P  DBH(@ h" @$4P  DBH(@ h" @$4P  DBH(@ h" @$4P  DBH(@ h" @$4P  DBH(@ h" @$4P  DBH(@ h" @$ݥ endstream endobj 56 0 obj <> stream xڥXI\5+|cWy> -07az: $Hq\^$ChUZ>e)VEP1dz|Re{-muQmuNiD{Q_La2VHY;DTv:6v66l_ݫ߂J:Bsi?|01ɘp.:K)jRi5$Fgy$c[8apɓ,2$V1M"H DG\憎dW]:cKz;ΡDkuαe4/=nC%xj%>hQvPҀ07ĩk.OM!׵Lv h.grjJPJڠKg؃{gPyGyy}:nd:<{nR8cACQ8URO(YmVw~ ûw;RWSϗΩ :TI_O矻[/hQ a'BB71@Ă< &Pf]-nJFֳyקeF}q,n͎#)˺`Tx jrq':49`5YO2.i"3^K2D="#Ղ vvMB#(]31M N)qfFZTP׆S]<(-_eDϦ!uv⥧O|K a斤yc]>;B]\cRY*<]7y,5UiSK<p Y}g[`ZʥäfW&*Գכx2VU*7m uk""%xd%iM!UuhEr5iבܸfIUdis| Ro.kJ鋄i%Pݣfn1pcІ:uPYM3sӯ5U$-L0[ǎVeҌii)b2u-Dx6YYU:8PDfܾЖ1rzAΰ4.ϝ͐|L L BsBt꽚iOwt+]8 +֤rN{fcA `CLZ"ҨIM*1it.#F^8Fw'e6cQ m a(ʨbqp)?E1<7"[w@My(Ikʐo'TU,XD |hW<E1ػØMnr8©efZsY\B|)F{;zO77h)bYx"UZ  1(1WptވKoeCW׮@вK-pScڨm `k1 Fy&ȸ32+~?cBP'$ 40`]01sc M4'kD4+2} 31^,o5 endstream endobj 59 0 obj <>/Filter/FlateDecode/Length 4539>> stream x1 ð'ݱQ$E____________________%___________ ____________W____________________%___________ ____________W____________________%___________ ____________W____________________%___________ ____________W____________________%___________ ____________W____________________%___________ ____________W____________________%___________ ____________W____________________%___________ ____________W______________F=̚ endstream endobj 60 0 obj <>/Filter/FlateDecode/Length 62181>> stream xXWqzU@[-*آ"5v Xb X+EQ DQ, `A:߾͝{(#=>}JJ @NQ8tuuK*[W>/^<@=򒑑ihhH*8055T@=B w^.]IK;<ǧ˗/_p!::Dڵ+]t6޸qŋbʪVZӧO׭[W<ݻǎkѢEƍOޒ߿%b *fEnkԨ!zj```XXe͚54i^HӔqdJ*es'ODFF%-߿?44ήwu+W-Z|yq3ۋlfXG|Ç_ūV*fw^q@ygϞu֭+VCdyF$GզM… @mCmٲELLLꫯO~ 1c,Y#""͛'khaa[r666?Eӧ7o^~}pp͓N/[|9nܸRr堠 嚸nj.$$dܹ>>>L Ҽ{NThϜ9SrGqF{nqثW*[/_^d{YwIjŊnnnwW\SiSƍa۷oپ 8|LRѣQQQR&M={VYcʕbE333jq(_F5{SǏUlӦr׷u*;޹sK!V"┏9" ]KJ,4m4,rIq'ݻWAG*@?^ܐ}Yn]GF- q$%%]}XHǏ:uH9E ?JHHܳgѣG媜$11q+nnno޼ټysfqtt|ƍkbb"׭[?FFF7. Ce(tttw˖-'NP%SRR $lh"??W^-]˗MիWݻwٳg)Kkڵk7x`'R1s̅ \ۋSWvXXEV\iӦ˗;99~ƍ]injaÆңG3f̛7/^ݿ_8ԩS'NXxq= &\vMcǎ ,P9m۶yyyIeg$p™3gڶm+-޾}[|bPPرc+&rr!q"kjee9xU6͝;ww!;vDEE:u*1111~E" O*ڴjՊKj?# jpwwسg:u {vQ\K??5j[1c 6lM67nܠAUvMH}-Z+W.g=zGqqqZi{%644G+Y}#5򎢱իWϝ;,wN8qѢEk׮mԨ۷oqZiU jJ|ƍA­[vZvmOׯݻԩSj\э^Ztc̙V&633kٲÞ={EKI9V7((HtŋwU[n,S8BǎK.JVV%VL͛7n_~_VRs֭KNN,2h ͜9s..."C 6mɓ'šNNN2@_=iX~FrhѢ:th׮Ν;ccc9֫WOyoooȈS> ,߫W. (w3v*+"n44Z655mѢE իn:yիWm۶͛7;~W۲e\ kT>(%%sRRWWw*lԮ]{ڵ$I*T0jԨÇ8p@E_d̙3`___ٽ{}͛7Νw4hP^$&&O6lWﺌ7nӦM"h߾}:uqF~eNl3fĉEEDNBBBVZ%).Vvz=~x˗/Μ9#/N\qqq[UcllsQiɓJ*UJZ9raÆzj/s~w{/\ *TUnS8{^\ ru``[D_+'P) ֔]ףG &ŋY7JݧO'No9sf6eʔiܸ4_ܷςFrL.]@^)ΡCVX!.P˖-mժՐ!Cv-DT>Nz^T~x֬Y_PP)ϤѬ?W^ʕvvv'O?r)NNNNr9]] Hϕs˳Cˏ?# }XnݪUsL8P^JÙ9z۷o˖-ۮ];ҵk.)VB%ѣU; ]vU}.!!Arecc+W*kqqq?ʡ~i3fd؟k믿$ʪYfJq;v;w*x3g,ZHxRC 4UvZi\]tG4PH:}Z+Z'4@R522ʰY啋%5jԨZWRE,fY\*+wS:u<իW6{Ձn߾-x //n۶-}Vº g,/&&&*'*Yn4V9tzڴ8•+WH~۶m ͂6Lk׮j\RuІ @666._\$SѰP?R+⫯`! @阘gΜ5kovܹ̪u23cbbU\믿-[~a޽kڴڳ?Ks;rV:_맬V+gC޶',YIT$_r+O~%\zU@@@FfҹȘ(;#@锔m3{uDDĦM?.ɰFiӦu=}v\ |}}X֚uȹ:}r_K;wHٴMf6Dnڹs+4t3$1H\@_~e.􏆅ŨQ)>\]]l``Я_??rYMMM+V(CCCss5jԐ Я^7oi'L+fgn߾-%ZꉈP>T>wEQ_V#J,)#z\3UFݺu˫ĉǏСî]ڶm~Of|اOÇ)S|n.]j֬(-.YDtCޚ7T}mv233㸸ܜ`…X_B ^g:;;?nݺΝkڴit#/PJ(?lذ]v+=z͛7lؠ1IM)k٪UC>}T%r!ԩSVVV]tQ*C fg/e---ssv-Z?V$&&~_~Epż> Afff;wܲeKŕ4h Ob %K*t888aÆ~~~"^~!C>q_HqBB;v$'' nF^Zgrlmm*V(ʐOmqj:tXr.Q%eܹ3m4###ݧO{ (@RJEƷmLLL^?~,6m 4gLOOoի򢧧VߐԮ][oܸIQ3ׯ_?7k9n޼(_a痒ɲ=jԨӧOk/^ϋu%Eeق z-LNN?~ɯ> *߿#{޽{"Y{޺uKWI1b<=ŋ/6m#*US mڴQ… EyՔ!((?iժW^]+<,,ɓ<ŋwءvgΟ?/r+ŝ:uRΤ}vxxxSRR>|.Pfv)nmmmwڵfun9ur44h9'11dɒժUtoׯ_/vvvQ=9bjj:vXyqҥ۷o;xAnll/tM@'rٳgg'ό3>}(?}ΝY{n||ʳb`ȑ&Mo}Ԥ"""5y!{ę)S1;ƍgll,AAAQ[nE///|ǏEɓ'f`y (֭P*wUeqGYʳhgXѣG*UEx96mK.eurA@}'O655Ǎ1 bߛ7o/^|ʔ)YӸ,*'2=z_|$$$4++aÆɋo߾mݺr\rqqk˗/Ϣlܸqfܽ{X^f͚tuqerr{DDDxxӧ.]:v/rРA'N'%%]xqZiS6ʇ;991sE"E\"!b1e}<4i-W+m諸4}(ܹWLMMEޔR+;(ֺukkŋ.]T-ZԩS Zie "ҾlٲK. X\ɓ'^۷SLU vŧO;'9K)8p@ٳg󏇇СCEf6lذm6GԩSGZ,y9qW=zT%J1c ooo(.kYv%eC|XB}|fv(ebŊ{ ..n׮]Pⵁ={ U>^9͛:u-[\vݻ?VZ[ۇ>x͚5A͚5׬Y#@h ~M/R$n yȘ+ʕ+'cccqʼnN.\P9KɣGDD+:[dO{jjH‘#Gʔ)cgg'6rHWs/44_~ٻwŀ8Yss7oވVREرc/KN{ƍ"z8T%F1c d̜9ʕ+*žQ+>K?cf8߅ϝ;\YrS}TXXXgPC-ݺu(@|d111ׯ_{ׯ -ZH*UTZ5opԩ7o.]I&vvv*mN8q :j'Gܹs%?~2wهFEE*Txu֭PBvU)@Kuؿ"i޼yNO{C5$t҅ ޽{Wd5jԯ__7--n fxվ},PxF?d9NLL9EbŊ]viUJ#oݻwNy={&/qMb hDddd˕+nݺooxyqΝE١;{l#%%EFǏɷ qww_h… SSSϞ=۶mݻwoݺjժqb hD&M<8gΜ@mmի7}~~)S2t}7774nޞK |rX s@44@#(@44@#(@44@#(@44@#(@44@#(@44@#(@44@#(@44@#(@44@#(@44@#(@44@#(@44@#(@44@#(@44@#(@44@#(@44@#(@44@#(@44@#(@44@#(@44@#(@44@#(@44@#(@44@#(@44@#(@44@#(@4By3gL2B||ǏUVm*(@j:sLϞ=wޯ=}hhF02e޽{+T@Nǯ[NGd|ݓnU>C5) y*T^:9A>WX1)V9SG1@# 嘄O4dL Sp44@#(@49 [tO8D 8Sp44@#(@44@#(@4B|RSSSRRORϖI*>: 7iD@7O#PGFFĐ @,!i=$&&>z3D<," n:u䘄Ǐ'%% k1-7K ٳ'y yi||cF>[)))oӈXZ)Ȕ.]hBrrrtth```kkGuajjjmmÄixdă -ςrgmm2eP}TB<(@|ßMLL x4"/{_s[ C)ɏI3֧-IϜr g…IB \ YRRǤ]y *55599Y^466&'@f#Ń#(RRRT Ȃ G(@|EsYPy@ThFPhhFPh)1A45ᝍ(@@Ջb?"Bua#)8ALArLBD 2 b hhS Qk׮1< C YbBRܦG}CrSdIR Q,55grLB:޽Ka :uݻh90iҤ+W d47Ǝ\3k֬ٳgKիW֭ƍk|cq#G}ʕw?xիő75o|֬Y6 ,Y奣```uVKD nڴIEWW+ hL7eJ)UTzo544̬}*[ PPYfIk?? h"99y˖-,cǎ=t萼1<<|ڵ߸qckFW\Y411EPJ.(ң 䥪UfݠlٲWV^]QK\\.]ڢE7n-ZTjӴiSiÆ ">rǏ˔)<ׯ_,O81>|X+m.rJy(YLj/ׯyQJAjjUv/[l߾}?"!!auԩU 9E//ʕc4PZqpvvԥKopׯ_2TC prA:u:v(ś7o~J۷}7$j N?"&!9KIIpiӤWVnٿN+F^BA}|>VlSP6m6lx%9;;HPh(VLfl̴iD}v,"EtޝA=sls׫WJ*I%KիWG߿!)z(@;~A]v)ڵ+!!7[@Xٻ&\D )^xV+Wnܸ1Ɂ(@@ApzN鏈IKJʏpĉR|СiE[[[ vh JݻwoyjF233>_~Ziâhodd$/^""CׯTҸq 6mtՋ/VN*}}[[-Z޽0tmǎ䙩3cbb"1nܸg7h`:tBF{:::dGeM4Y0eѣG!CӸRJG!߿\r[&=F@@a=yRYoPPŋ]\\= Pm]؈<|\]]MLLFA*q~wYYY |[^OyrLB[X̬G{C"E~g4d:V NNNIII">vرc^^^E%?X.\Xe Kr18p`ԩ׮]344l֬s˖- >F Pf44dI Hq㶝 d(@@A"$|J>#ȇ>b{ëFL{0AAAz 0mmm 3S9&!S L ,55Mt6=S Y|BxF@hž$ ä6j tb_j^4!ZVP44@#(@44@#(@44yyYrr2y'G PhXTkMCӯ#]}III[l/44 ?*qT6[.Ⱦ/%%T` swE|2[^WSrLBsv…G)Q[^OIp~^|t\4/۷ŋO>MJJjذ{~+W޽o߾Un0aҥKwvvVn-[ٳgϞ͛77mɓ[ŀ_~E477wqq8pJ^ѣ/_.\L2_}ՠAԩٙ{{{O4ib˗/GFFZZZ6o|mڴQk׮,U谼(Rl0k,SQ޽{ pڵkϝ;J*?^lO^zyzz(wyW_}2hР9s/^\q%ځvmϞ=3f̐֜8qTRR, @~;_تU+[월$o>PFڽ{wZ&MTfX___?cVZeff&$ `֭b0y'ON<) ߑfر\|믿ڴi3eʔxiHk竧'2|r8{qq ԑ(ÿˈ??… u޽{֭={*0`@HHVZ]v駟ϟlYה/_N^ܵkÇ?~,U%'#G߿Śܱc޽{{yyآE WΝ\m6q@e;,,\re˖ W^ݻw{uss+V000?E߾}+WEu>OMM]|yg͚=WtH`IG+1 EJOF`ɒ%"(ST}hkkO4IW\'o <gH̙Sr^doonݺG֬YSZٳ#G:ujW\qtt=ztѢE7ot=44TRrYbcc#emm-⠠^z=ݻĉƞ3g2dȐ nzҥbwyHcǎfZÇKh(ȒSR.\ @tM){ʦ+JALLLHHRk]Ə^OV*nݚ5kVzDlffە+WJ[Ŧ`=PBcmm-׾tҲe˔[ 92dȰaTvo۶*ũSnJR|޽Gfx.۷oܹ37X 1ieA**7oH ~Xtuu艱)R_~y5kCoA &˔)&LcǎU*g`ɒ%wv۷ETFO4ǜ׬Y,%&&qVǷnj۶;w-+&ϛ333*=*O!N@ŋ9ꀑQ^855UoDfcc3d)>v<\}v]y4Ǵ3rHx;w̘1CVcsssyb5kN:۷Νc5lPU*ѬY39~Vq::)oUFurw- @>}VZu;Ǐwvv?RJ7n|DI?B)wWN?"PoݺUyχ29?#@~f͚E.XAGG'444].\XFcJc### zk֮][C|bg]~ʴiv!UV͚5KZ}vÁrS9F@yΝ;u;vlÆ +/999/^بQ 6hE3c()g(}zz;|V~R+)^re||;tPD AK)))}?rov\SZ]v9r\+퍅[ ?֭[˱_-m۶9{IAR*UavիWOWn߾]d믿(@yĉAAAR\F YQȑ#WvqR2|ׯ# KDLB RLVn9s,Fk _0!!"/K*%ϟC褤$) &jJEͅӧO_xQ#FڵkfR9)ڵk}Z'118ʦ y𡗗8Ǐ===_~-:/>B.+S lڴIAOOO|փF->qʕƍNG<\ ,,,>)SXwի~`sȑRrCt钥X?k,+++WHnK#b[[O2t?hڛ6mzA7N|رcT_vm"믿Fh =4ilj'~Xѷ+N<ѱdɒ<.]lٲe޼y3gΌ233WfGn\]][liggP򘎎4n-Wܙ3gTV:88QY"z]ŋss*Uey`~C vKcN*w.ٔF܂x5qC5#0ht0*.[4F=*AQFQ@5a5"NWR@V7sHnݺ}WV9d'֭[O>QlΏ}5ڵk_|ŋ/nݺҨQl#b`On޼yРAiE>쳻ᄏW^۷FM h JJJRMv鱯mݖI'wgyfYYٶHM*uݎ "j'ԲeK!@͛7/5Ϛ5k۷oߕ+WFôiM6@,\>;S[&O_OnҤ!#D% 蔫*:lk&4&GuԚ5kܒH$~W!̙ӱcaܸqW_}{͇~Az%K իڵk7ydI)E@ h@&9R@tR@d=z 4A(lrs飉deeeoFrn߾H'Kp  4A(B @ h[I$%%%rb19Q@'!rv EIKS@,YRm7I J(2Y,kРk׮;s"@P@l]tѢE.aÆ %[8pٳׯ_?|S E {l!)j4@&ۼyfdÇm۶~W_} '-_\*k@dD"~,sGΟ??OyWǍ裏 ]2+ެY3Zti߾}O;4Q@+޲eKgբ`7X\yy??{`(F(--/~ʪ|֯_ߧOg}SnT3g=:S{ho1jԨ~jذa.].޽{W9s;vܸq#F޽VDG{^zwye˖DOȑ#?oM64hPn+Wڵ… S[FWH·zhtΞ8Ǽyx~֭0`رcS[VX1YgO$ƍO*={-eeeK,ׯԩSS͙3{Sg^z)N8ᄯ*zu]w%-,,3P=/_޼ymۦ UVѣܹsyy:lذo6P hyum5k,Y2}_veO|g۴ix[n/KtWeܸq{nrϼhҠAy h'V6lXRRm4iүn)%9c=c=6Y4?'۷*P>+=q}0`@>nA%ozݺukӦ͚5k#FHP%|!Tk}M!CRsRǎy;c{M{'>'>sNr~g*w!P3+sԩSrOf̘N:;y:tH~ڵk=T!Kpd3m{uVZu~嗣!&j;>cƌ8pO~)S{{7o뮻sՋ-;w7ߜ8tzuSNVO>djzNKmYbEܡK.[W\q}}7YˀDrrr-{|PStW_;W_թSUVzenz=y!Clo旿e4<~e˖vx~ǏnyӦMrÇO8q]w]rKf&O>jذa׮]uQ,v@ӦM[l)j'GPc5o ; ; ;^ؙS:#yO  h!`)2Iw&`Wb9? egg h>?{.=V@*))I6mć@uZf͊+YB&q4T%K|GC-~%\ҧO-Zxu֕{44@&.~|Q'fݻ3 0qĞ={J= %oW/OQ>C!٬ @ hP@  4A(B bؖ7L`{^>\d_2 4JZ+;\XZZ'TYY^>4@&?rbX~~~IIIƍԩ#TI ?VW^j^~U8RK#zTa7(j-{e˖a+ы"ziD/Ԗ凰@PXXj6lذp׭[777*fxlƍׯ߲}^2!,oް69'/Pb͛/^8usii d,)2Yt/$綧^_@VN8`T,^,>s)@-D@UK:u:͛7_~Æ %%%2H~~~zׯ_PP`卪v,Zh-EEE` +4i$HqrP_J#Ֆփc`rrr` hP@  \zح#;C ɲsr[vljn{f[m)** e@&e4<ja hP@  ee缛uiv} }4,(_ɹi#d P@  4A(B @ h@fP-,'}T Kp a hLH$e%9;7?H4TauPK0'9թ0?G&Uّ2`g jg P@  4A(B @ hP@D2]>'ֽC, .*L>9rԩS-[lٲQGէO={fg[^=ҦM^{m#F_:jԨ'+tԩ)x՛?~5[n 4s?駟>E]zkݼysfaa}wWH&'L=/[oo-6lXIIE]ԪU?\bfe?Ymcu }4{RsV%GKM9wݶsҐ!C4hULGШo:'rw}_|y#%t9z޻s7n|UfΜ٧O~UϖWI7k̓@[rϒYh[EEEROw3fx^KF7[lv:svI&q[n,))¨Q:wC?cǎnܸ_!37n}mҤI'x<$9ggggWrUV}n ,իO:餏>(ys}رcFfΜhѢM6=;xTۥK-Z]v*{8رc8㌭y̘1:uj<6muֽvF Q@:/_&K/t7x衇n6:+kyr[ٰaCn{QFm6qۚ%\FGٙ wDO7ߜ7n|{@uɮͿ#<2eʔhk&O|5$H,;{uoN8ᄶm>c[6c9&:fto4w?pߢ aɒ%g~wtܸ~;3:yyov'O2ܑ2bĈ3gFCč;6ąNW@y׿>'t\o Zz׷wovvW^g̘sh߾ɓ7|3z/]}I |;~N:x7|??~]ڵ w>}DoNNΨQ.ΰIdX,V#x_g[ϟ_Ks(///--&璒\|73~&L7U{ D"wQkvᩛtq׮]۷iӦI&~' qC~ߎ?qsW_nx/2ByĈwy~+t٫j[@7k,9,\pnI&SN9n;#''j^W }ӦMۧN={|G,Xݻw={v߾}G㏟yC O~RGꪫZhyۋ׬Yw^Z%M89_sj{AT{ ]&_~9HbJw4i3$K.G wK.v͚5O1rvM6['L%zzUg̘q6o:vmڴi-w^` :u*V?$ܹsf֯_',]tˇϞ={nԨQ:u~/\p͚5vޤIy} GU9T*y mw7ׯ[oSe@zd߿}oGp—^ziʔ)CwYfln>Hn|G~_fgOC}ǿsG}tr.))Y|y#mϤpĉZ?lc|J*w7nܓO>9k֬UV5hРK.w^߾}ȑ#\?qjaÆ/^[xqyyy?^z6m{m GEWz&ڵ~<0~ n޼E.TE=m]V!Dwo*`7̛7cǎy3:P&@%wOBܫSa~L3ZP]Oq4wnjmV' 4AX d:(5 H'4@&ur%8B @ h4@&+,^;0'ۿwG J89m '_$@ a a@ y H﷼@&T>Zl+B @ hP@  \d޽{f餀dٍ5P-,@ hP@52YIIK/{왟/ m,HoK 4A(B @ hP@D|J/ZEEEB@ |:kO$B@ ɲ5k virB @ h4@&+VH!`?vp4@&+--}嗓E]TPP vOf*^TT$ǥp 4A(B @ hP@  \d|n'2  4A(7Hݻw˓ 6 hLǿ, ,@ hP@ |!TC=Cr*..6mZr>ꨣ e+B @ hP@!dXaÆY @:) tF3@M8_ %8B @ hP@dD"kџu% 4k+џoI @ hP@  4A(B @ h@&:'=H+e@FjG- ,@ hP@ e}9++H'B֯Z<;+%[.((梢"ihO_~WH$%8B @ hP@ rE.'k=^y"F UP'ްaVZHKp ,oYG} +B @ hP@ dϭZ\\,  4A(B @ hP@  \dXv~?}999I rrZ}?䓅%8P7[܀[P\ @de*O·7͉H4@&+g,9wnk ,@ hP@  4A(B @ h@+ͺ NL%zX  k@de?(IW_ 6 hLHd+NftA(ݢLߋp hڮ>:=ah*X  4A("W,;vߜ\/9 } ST$1 Y  k@dxkɹM}!@ hLVZ5fnirܭ *  4A(B @ hP@ + կs~jH'4@&ΎP-,@ hP@52YIY9NyփG U`3@ TRR2 о}Y2 4A(B @ hP@ + 6l M6f餀dyyy?ϣsΑf 4A(B _u4 H'B^{hXl e+B @ hP@ lժUVXnݗ_~) rE|' i hP@  \dSOf hL֤IPTT$ ,@deee}viyyy2,/0sr餀 4A(B @ hP@  \dkaa(4S@d޽{G9# ,@ hP@ % , |!@&+--0aB4|g.,, 6 4A(P~wA@ş}j hP@  4A ssw2 ( 4AX ɹ[n>H'M@&+++{7G 4A(B @ hP@  \df餀dXP@A(B @րdw_rk    4A(B @ hP@  \dΝ;f餀dyyyzP-,@ hP@52Yyy9ssN|!N hLVZZ:a„ܶm[4N 4A(B @ hP@  4A 0 5 H'4TW?u@MjKp a hLVZyڋ&g/ 6 hLH$֭Y%8B @ hP@  4A("W,;;IY @:)2]>/}T W a hLW,9۴!I JKޘ\rBij_-Շ,Qb/**3@P h&x]?H (  4Av!ul)7E DM→)*j%7 h~;秧f餀d99-ZX@A(B @րde%O}%9uɹy2F E S@tA(B N?cf5"HSiCڳ(E6STZړ%PiCICd);co0gk^׹9s|u . hBP . h"Q ŋ)W>KP\~<ą4qa g&xD Pf~+P8_ț4@Qv(Pn?! 4@Q%-7ޏ]ڕ*,'@14q @\(@ ą4q @\(@ ą4q @\$J@Q˗(ƱXB P%4/@0q @\(@(ӷrF<$}De_L͏?2IFd BP . ؅eLg)@Ι{YYY 2q @\(@ ą4q(EXɽ_*c =Sț4?RjTݕ)2@L@\(@ ą9-i=+U2YNc4q @\(@ ą4q @\(@ ą4q @\$J@V"!ICcI(KJJlu1 hB04@Quk3FqGXBN PmhwQܨAh )@C>*ԗ@J}K[T)9}Y͚5%(H . hBP . hBP .+YW_%(H EY+T @0q @\(@(ʶloDqǎ=(H EYVVֺubą4q @\(@ ą4q(Eŋxw֭y"@>R(:N:餿zVVąEYBBBݺucI(KNNnӦ<h8B0@1eʔ<ޭ^I(X`AשSDrE[Zxf͚R$h,==}ȑQܭ[7h P . hBP . hBP .KNN袋bI(KHHU<ą4q @\(KKK1bDms Peff.Y$KPL@\(@ ą9!TVf͚(55|QRJ P` . hBB=}S?=PF%mRr_}$@ P/Ur, )@e%6o#@04q @\(@(ʲ23_%_<@Q(23}^78}rC hBP . hBP . h"Q 5%(HEY'@0q @\(@(2n]Q\ nge.\p SRR .*EYVVƚEsڑ'{=/va۽eI]L@\(@ ą4q @\$JGmKN((Le”"ą4qa "DR$B(+T( . hEYVVVִ(NHL.^F(ܚ跢K$P . hBP . hBP .+PJƱXB P%Hڠ<BeQ<@_w9j . Spe[32'"OnP5 PmtoQܤ^RC hBP . hV'N\n] n:vLB(sύ=zܹ֭PQGu^uUJw9ߦM3<\hQX\@S&^F٩Sk׶l?O͚5ϟ3t֭7aÆmO>D )8?]vk׮ܹ'|r6i/:9s4o|ہr 8SOu.(@V\ٱcnjJ*=ŋԿsɒ%;w4klڴisho_~=zزeKTRO=T׮]=A޽tԩlٲ֭[6lҤIw{CGu!w-_<33f͚͚5k߾g,@dRB϶Gb  Z~b 55u {իW+Q|Wo͕W^9lذ<3}1Ǽkry(^x裏V^=55uŊ&LK233t֭^z&MvT/1Ç%+V<#ۦiӦQ0eʔEtAE߱7nk 8/-0`ӦMӧO2e&cǎ MJj֬>HHJa{3fDGz-Y$_^+o䣬{)(^xBBB)Nv7o^WT)wO.o?iO1o9pAPg`/^<))BHH~I݋/X\7|sO_~=rϣeʕ`N"UjZFG*(o-;wʕ+T]!N'^3oyƎpuY7pß8mڴΘ1^v듓kԨѬYm۞{;Zۿvm\lٰ{\pA?~ &MԡCntX\L2eʼ+ 4n[&:uUW]uw+yӦM9c9oޔ:! 2$Wx/رc׬YӡC~ܥ`ϗ~LNN]vbb*@ʔ)SJ_~%---Z0qXo7.Ko喨\,x饗:4Tz衖-[6h^Y 5k .>q:|܇ŋϘ1??cCrQu5ngΜ9y7NW95-===*Uj7;iҤmϑ+o>k֬|/^VZ_!Nm;_ac)cl֭?p?rʶmqĻ{ӦMW\qŝwyaK;iܼys/^L2ZjEAzzaîڝ_[n]tџ6;wn۶mD Q_scVlٜh +IM6y|3mfff1bСƍ[bEN?x?={ Kή]r-5j(Ğk׮hz(\r< sjiӦҺU;P(<_/ {YnN;;իWjկ{\$Vydž^J{矿ݘ#_zp\ӧOGh~bU)9sfy;o۲eˎE_ T@Q+DOKI d-@W^= ͛Ghv=:V=묳wٶ}nÄ 3~{/ܹsCܼy3ftaz6m؃ f͚E~iGm~(>#w5۫V}AJJLh{P8=;餓`ԨQy|0z;DUW]m[/K,IIIPfjٲK.ݯ_jժьgZ"r%FѴiӜ.E3֭[E:(11\r=zm. O>G ޞ;YfUTYr%K~ofo֎zMqyMsgnNMMȑ#YGѮ]C58∲e˾ 4._<3!!{=ӯJ*.z>sʕ+~ٳgϛ7nݺ۶}mոq]64i2;[ꫯ+iG ޞ[.YM7t}o?餓ԩW_r+}7m۶m3f̘ܟ5jD̙3sM[|q݇)ǖp ?.e;{.]^ꫯxРAs϶m 7|v;y[rOl,|̕W^ٻwC^tE]}cǎ^z-[G ^žsv[Ȼ~y9 Wرロ]vYlWT );Y&׻Ǐ&>ꨣt~iϞ=CfϞ#k֬ys%Klw mѣ9i dx 'P+]СC[h|Ejժnݺv͛L~l7n|qUZu̙Ϗ\wu\k>lW4XƏ9D3f`SN ,Yd֬Y_n]FRRRBpFFƆ MDEҜO?>`ڵ_54 e}o֙]k֬0a7xQGoJ͚5+]~:mڴ?|ѣG_qs g'>z$D3x˞޶M8_~y0z}ˌ3{nݺmٲ{yWv~ݰr~Eq3WP =֌I?D:'780D򐙙^V\9!xwMTYTίl[N8ZjIIIQn/O[ULwCoN>= *{챗\rIпr!ÕO\q'|hѢˇw\۶m[hy}_~}X+vaaC>o޼-[ԨQ#ХK?~G.]wu<ƍ g*55RJ~ 7pQ>SB6}pàA>裗^zF2e4hУG۵kvmeeeM:lӦMܹ'/'dΜ9/1c/^aÆ{nk۶ 7ܰr[ou…[o_{g~؄"v{?~8#d&}-ҪUQF=S<Ų磸 &L|;w>裧O/X ZXrnI&{~?)SOOO/Wvz+`!p׬Y?0 ]t/^|Κ5'}}Nj՚7o޷oߏ>dɒ'OXJ*gqw1uJ*~4TwޭZ \T-Z:6lN?|aêW^f͝|:.oV:tc-[& {h`ղe˛o9_|8֭[SO=/W>~|h*\hbҥ/nҤIL/lט1cz%{h`Vt\Kn>OW<}yj׽ȑ#DBxXNltdɒ:dddH^G/ŋJŊc?̜9S]tѪUbo Po袋b/WXѶm͛7Jrrr ٓ\R :u/b/ ?Kp'pBlɷ~ۥK)RSS%aϷnݺ.hȐ!RJ",913b <.];-Z-y6lxw|'?wܑsIΝ_{(^fkxw޹}ǃ5kW_}U,׿aɆ ԩzK,0 _tЦMzRJaݰ7|iӦjժz\s)ZÇ:t_~|򤤤5jq]t؛7o~wN+t^7gŊvڛom~g 6lĉkޔW\qEݺus5+s-G힯qưJ85?ʕ++Tpwy]v g\" I6mZH~wA}^{mڵoH8YϞ=Òӧcǎ]xqrrrƍ޿oUV^oUfϞ. tꫯC~'OYfx•.ovڵ 즛nJLre8N:i޼y%Eql  9gՒ P%$V<NV|ЬYGK y;CΝC}~'۾[|3f,ZOmqÇcƌYn]XCo:ub-ǻkʔ)Æ +Qٳډ'?~_|QBS>}wG!CD ݰaCV&O=z3<d,Xv/੧z;C׬Y:yN[nDD3D?&MDq ɠ{7pCM޹֨QvzGx)))+W:t/'|W;ݻÉ+=z>uW-[>Unڴ /6lX}'O~y7tSO=Uxhyͯp ~gyo˕+[![8ݻZ|phݻwc Cv/7n\[jSN9Cꪍ7^{%Jq֏?XbJ*j͛7o=|~{˖-E2wܛo9d&ڵkثowygffիWfԩӶC\r=/^'|n^Ym۶_|E*W\j՜\ԩzk~aÆܷo5{g͚߇gϞ7n2dH˖-c N>+W5X?c%\&Mdɒ^zFư7/8p``-Zڵkl 6\pG9V}o }8l]v5v8lժUX1jޭ]{WDѣGlۋ-jӦ޸q'x"סիWo̘1 6ܲeɓ;t0bĈ_1}:%%_Wn|ӦMW^ysS_|III5sν{f:ի8$$l߾}N5|WtGqE^c Wi*Qpx!z.vO;뮋/>}>_Lk&l4_wRՐy晹.3_Z!2(@n7xcc/:l;9yK(n'>G_veQdɒY}t1 .]:mh޼yC9Ut;su[lӧܹs;sύ̗_~9[Y/+W Q>E>M7V/]n&ׯM92k׮iiiW_}S/_SOe˖Ep\%\ !]QiӦSBX59/>G?C=4WZkYfͣ>˯CVZ,k yF_|s*[}Gg4.Y_`y`eM+0gQbQx͛GE/G攈uZjm g1CٶAΙ Ν"GqDdO<1zh+7n /˞b{챱?A]uUyv?{Egfݺu{ӋeKM*@XFUFoÝ^kC6us)8e?~>uaɎFSv(zP^ͣbƍ۶mW_fL+[n ֮]qŲ'*Uy,[lڴ)׏+V݈!%%%5jJ ?~]vY4Yȑ#njӣG{lٲcCr'0 _|$ E[8ӱ'ʬ^:WP6lׯϻeY>Z◇B^c?ҤIG,nٲG_a 4s>~pܸq~pc-fXpaK.ͣe2eb񎦺mCgx۱;n}M /1c)@^z|"SO=[oNOO\Gƍ3=lܸ1wvڱxڴiy.=kD.!=[-u?m֭[wgݞwfsn͚5d uԉN=Xn)zv͙3SN]w]_d(~wxbl|t6moݍX֯_wrȐ!/Bvphݺu'O6}COӎf϶k衇3&eÆ su+ >-[圮!+W5jTΧHl9sl̙3cq5|h|M6=â~˷[L;wc9O߽^5\ſ{キfL !!W^n:uɫOl߱cǻ+6uj۶mlr-۽BNzyo߾~s=Tl2޼yСC{l2vI h` ]`N_aÒn_}ɓsdɒΝ;Ǫؿ۶s};rpwX W^k(C=~qǍ5*6|?~͚5?yͻ}~ DߞHcxag>\}ūWލ}Q_{ym]tE_u,?c.]ڶm{oʜ_x+t{o3<Ӳeˉ' KiVlG}tG}Gg6vv4=r:?ytr8bĈXGy$--m۞׮][,{#8P$J@PzX,!˦MynjlnѢE~bp몫8p`j9NZnիׄ !nڴiÆ }[djժY&*Uw߭Wގv/Y%ʕ9rgpyu7?p뭷k׮s϶=|w?߽xG'OF/f Vr~-*㦤 ><6w駟r%[n7nѣ~UV]reT8T҈#b1jժ+Vl{l =^99myٲe8g?f?SNwO>ɇ ݻwcu_|Ŕ)SBЧOdwBQw޲!¿b+˗(qb cլYSvUff{Yr]u^s5?C?6l0WEoG9M6M2ymԭ[wݺu_|El2z^x^ }'woGs$&&_>++c }N>W^y饗b]OիWǸu֙3gn|C*TXbRRRh{+V[!cרP۬Y3f̟??55u+Voܸ_~1bĕW^VyGu!s9ga7bUk׮>CB'ߢB7 4KyXe'sSHe]23{p~ҥÆW_}uYY\zu8Ǐٳgl2e˖-\0/m'!dK/6mZɅBڣ~zرZj o}7߿laXRJJ壳.ZhQb;>/]):umSd/\-!Q WW}WaQ>Õ0iҤ>\a '7j\,{sHyb矟W˗tMkL2*X~ {:w\,kpF?8uÅ_a!3|{Y8֭[ժU{u=v!Åvo,'nkҤ$쪭[^^z-n׿޽{,0ԨQcԨQ 6N233/2}~M^v;{!o]{IIIڵ;b{k]ynݺs]dI^N}'ܨQ˜&L\>cƌpƍ[`+BJVzG}۷ӱV?Ë/8v_5558c/K.$b#mv r|ĉa†ԩӶSu?7xc=?7ߜ2eˣ#:餓f~r ꫯ~˖-8O8.uֹCC=ԫW>}w}y\Z*mܸv+6f7;|ɹZjM6>|xGDU#rH89SN[pZöB'xbXOCZn喐Dr'e_Dv4v_c>PEYfff}6mCaQ(ʒ/Ry(ϗ-[YzM֫WOZا(@C6l/izz k_|w1iҤ\˛7os~.E lW/Bݳ5jTNӧ/]4駟p cǎmҤD/P|3lذ뮻/~G>haVV;ӵk 6lܸ}fJNN.<h,'&˖-9qjժ.]<#={̹<:vG??UV2F EXZZڠ+W=XǎsUc.8 gΜ)]  dffΘ1o߾;jPxC9$K.-c LaÆGq<JMM0aBx≥J8IHH3fLm֭[71υ@زe… C4/P0pM6UZ_ [t۠A!Cf"hٳgjjƍΑG)!;xY~}~z衍7wW_ݷoKhGXinz$I3fׯ;SnݺŒ_|q„ ƍU,Q䙂߆ 9[om֬YB̙s%dffEN{x^^~#Fh׮DQ)@|bbVbZjG}t=%4Eb@'&6iDzQƍSJE9t- VX!y PpbQDA J*QPZ5٠34@Q,l2))IN צM3{(@e_|E7o\QpE)8 6,<#=\٠S|ѣG/[lG ,X /$%% Js}؇;8Yraznݺӧ\r9ڵKMM0` 'jd_ >}$ɯzlݺg6l~תUkʕ|BAFtҿ寿zkWXyOhРM6wժURD Sgo4@7n|p%ةSuͯQ N[.;;gMKKLLMM ͛7K%,//oӦM򐒒_o;{U˗/-[:(a lӦM cFnc7+4O> ^7n|ꩧ:(a [3f,ZhƍիW?{}WT7jժUӦMPرcW)@>}wرԷᣏ>ٳرc{Ưڅ^aÆO>y̘15GuW>#oVf͊V۷oj՞{9?S* Zn:jԨ#Fdffg0z0`>[ݡCs9笳 v>:蠃XɆ N<`%/o&O| ' h}H۶mPvv7|S򟛗~u͛7wߝ6mڪUڵk/Zj=cѴiӟyλRz:ܘ^z[nwO?ݨQ#?J4>$))@Q5nܸ/j[8ˎ1"---.jժ۠A3(h=wQ5Y};vy!Qhx2vXy PYBBBjj<D4Qa=jQa333GC INN ֌#i,QF0nҤIJ5P * *VxEEb J4@}znnnNNΡz=r!2C Sg Ջ/<6mL>I&wyK.f͊\$BPgɗ_~<;.]ڭ[6mڼk˗'~iii=܍7(E$h={xg"uw}AO?I%L6~޽{Kw榥CڵkN8q6lXy晟~H)@ij:uÚ5kwygڣGOT(@ij+3}_Y? b4@<۾}1coTľ|ƌ-ڸqc?޽{_|E\ٽ;aP~_r˚5kO9唛oXJ4DG}Գgϱck_{ /pÆ '|1c5j?5+|Gzf͚tyW_},+lڴ~xSO/(a/6D֭[O?}6mmzްaÀ}޽{wС_~|I׮].\عsqˮZ* |1cƤq:~J*]d=B S&///==}…ӧO:thfn,v . ''VZ=XBBBdV Ǝ wǎ Qzݻo޼D 35ƍ8p`qV?`ʺu늳#҂ /VZ 483M駟N2O>D'|rYzh:u'|23J45guּyW^^3ԩSSNOOgK.iN6-Fcva ѵj ^⋓N:n(@^S3,/gddA5Zn6;v YfO7.8UZbŊazjd h(}^ifWmjժըQr#Mۅk. vr:h{V= oΜ9ap!/Ь;.#H[g)@@)]xq5Jdٳ ͪ[ '/4^<ȃ:H)I Pʖ-[7Q2"O\|so7(4}̙뭷*Ք0!g*Tٳg$شrH# ]zuNNN =O:Yf_/"WPbYp"Ҷm[yq7oE?Tb0ݰaÎu<3+W>㮹ƍW?w׿U)y PرAO>`YfXC7|sJC(@@)ۺuk$NL,myyy82l':!RVeeeEJ*I1,8/y7ø{CTR̖۷oժU:b4@Cا \yewGYdbD[l9{ NKK++@2]P=0wf ,֭[KOJ߉'sUgggI'$i>h(}ݻwOII ~a;m3cƌ0hѢEv$ا rʽ{ɓ'͸qk1h _}RRR> $] IRP|?xT߭[ѣG!CDm۶0޴iSэ[hqwp iiiW_}#~+2RRR&O֩!9R~:uDuM6ݻ+ȨW^7iҤRJv"@Tm߾}ܹغuSY&uM7͟?cǎ5JII ^#l_}^^-ϯ^+[9sŋ7lpv<xPb۷o7o\Ji~?A?C%N;UV@ øC ę0޸qNNyyyAR@YU0b #AGЇ=I!}RSSp+PVUTB RѦM/^ZZ$öŔy7>=Lrr @YРA~)rsVVVZ>Jc`ɿeX*U7nB]A-m)@e[XPT{EП>-ʼ*U4k,33sƍ6mھ}UŊSRRRSSE թS'///777204n;DEhPMBBBP * ϒoH,!@IRgcX( * h,##G aÆ(I q.##CRaBP * hBH3xѢEBk֬ y%'' D[bQ">H#qϞ=%7zhI|#}q">HPjjj$nٲeVl=UV _Kq %+z`ۻ[+ו~,FNJc~Wl~E}!8 hBP *<JE]ԹsMbg%K4VBO{ ['K~O ӏuhٲeYYYM4дiXΰ4999#GWZtL57oޒ%K7o~m]pRD5 Aggg>8l۶zG ~|ʕ+۶mLڵeJOO[~鼼>J*_}UZZZNc9NzO|niҤI+W|[lҥ?|!fohذaYgGծ]{ԩ;w&q <8++K/3fLbmݿ#F2mɒ%A>~c{Uu6wf͚M6M6S<\$$$ '@̙sgOG}I:`-[e}ъ+> wS}sίڵk{N:Ĵ!CK/b=b0#8222N9>,M~N8!m۶mz^z֭vt#MK/ `J|2PF/]w /ФIիWv֭ԩӂ T'j*xqeee?<eZpap|z5 WXk׮o7l߾}詊f͚եKM6~n5O裏~W_}5hӠAX˹[5X>}M j۶m<ӧO.Jj֬@P|۷oy|c_p/j뙛o9 aÆ}W *+++L[n]vmʕsss?˗M*s=Uzzz߾}mw_вܤGy$!!!h?iͱshؽo&%]};6~\sM_wue)I&u ?lg̘K;6R4F2S.ق %%e;.ޠA>}ƍo6 Te?ܥKo׮_.|0r w=ׯ_cǎ;msYg%%z.\pܸqFk @RgN}ap14lpmz3g|(C=UNN}ݻwONN.zlPz1cƄq^vxVjPM6a|'$$Y͚580~2`p @ ;jݺuڵezV\#r/e2eJFFnڼys+ _WfffyEO"= @iwHܢE"Zl2 rss?Sĭ fϞ-u@Y`~ڵ2*x͚5ԩe"FO±dc4F?YΙ3G1pJjRSSiJOv%K 6زeŋc' а+V/e}1rSBi g=Ն 6oNW^bOUT,wt а鑸ZjE,8wݺuR܍7qժUn4(=U/tt@⣣S(SSF&R~E8hOU5PF{a7n+VX-[(0Nc2S耸⣣SݨRJ$*es1@tt@T)@nD⌌"Z[p)X>qўJG}O4F "qm۶EuJP >Y nz_2SGG q!DիWѲ;L9ذa zUFJ3E_2S5o<11wt а[+W,+vhT`EߙbOܼy|t+W|ANa7N:Hp"ZF&$$\ Ƥ}Rj?ׯ/@Y"]-Z;w._|$Pv㨣UVE/ }J'(ii$u@YN?H"Ș;wnw]ځ2SE]-hѢa7. .O?tWͶlD"J~ףGݞ,Y$6ej۶m6mvbOխ['aܧOJ4_/ov7IMM8p+>(--mmL͛7\\*#]-~'qrbŊ Sj #/R а{-Z2dH?c;m裏ޚ"i@믑Gٜviݺu Rg;6ؾ}O<w}w1e8p`xku&Miii'N,[.(s=_ߠA ={No} HII[c-/X=\d@ &|gAp '\{EYk @ꫯF"$//O>dڵ_ti<%KO޽{-Pz *i*U [n%2jM7>kT5jxꩧ¢^Ypnnn眜 ~G*T 3_S?Em۶f͚ڵ;c SOzzmɒ%YYYYQGAVx꩖-[g-_<77~;vN:ڵkERM81x=>g hǺu>î]N:u/";t.]|UVmԨu'NoN=T +$%%=S ;m0{-[[Ī:jժj=9s|wC  @2eJ߾}6lf͊EW0  &L^g _|qm>{ءC"ZYCս+//oҤI7ޱ|l%h+ҠAN:&r m֭W^D˟~-{]e˖{;a%hĔ)S9"]/!==_u ^7qγ ĉѠ#oٲSܻ_z m۶IR@|믋nA*57o]5kj; "r m 6m*!%l„ v1'lٲea\DCFFƫzG7ow(@O*%7ظqlQ ?00qľ}}h TҶm[ )I7n1c 'РA`$Ľ?<9昤> M+3,322/> r@7o8pO<ۼ &TXW^ Cp"8+ Ν;gddիW]tY澰(Hy]bbbǎcɟ2eJNNy睷>"[h[jUz2q3ĉV{F.4qnڵ . c8K~guV*U>}4q¥hĉk~>}S"#+FsӦM_}կK/^z0?8O;ʕ+tٹs^vmFN?߿o5[~|ӦM7M/̘1cՍ7֭[Z҂)+VѣG֭#-333{ $&&i&s&LPv i%fϞ~aÆ7oxcX8{a-[˖- >K.# Q En_Xt.駟GuԽ۴iӎ;fee]wu/‚K-]4X*XЬq~%\2t{B ;~ʒ%K hѢ+I&W`/Xf]m?999ۺuG}l#G^{͚5 2sϝ3gg=}|a)SN8nݺY&ɓ'jj.6Dc/m·AM6q38v<@%wnݺ5x;sΝ;ۏ< 7n >/z5jԸL 38N<`7;f̘6z;sŊjժ]} ^re-6m˗۷op 1#dx}*,vҥO>EoذaúuSOo˧~.jӦMǎ`ʔ)O Xj=Pߴs->Gsyǎ|#jԨѵkנ?~%}#8"?~|r:?#~>7w엛o{^ Rg GuTO=`cy`aMӂ ի  Kx6cƌH?.ެYBSڷoߡCpХ^ڿw4 BI&͙3g o19r䩧z-jK֭[w) :tᑊl'|m۶5jԪU+21i? qo f~*UTp ᖿKE.%Kwǭ-{葜\pzX8^reĝ_jzܹs֭F#-Hfxرc 6/4hPqQJ1-[nݚ?CSLʪYfʕWy^orꩧ_>}z)0#j x[9~'._No 7o^ Ms^^)W{׃m͛˖-۴iSJJʮy„ k~Jqk׮Д+^9v]Mwr>_dIdPc4H hDVVV&MN=/ աfff.]e˖6lx>o6 f7.-XSO]|Ӈ V ـ֭[k׮NJW.nڵ)K, Ǿ{njSh%uMLLU:''gʔ)Az6o.7n\eׯ__p ѭ[QF]uU}Osu]iGa0qr7);ZѥKF歷*g )@o3k֬#,7| lr̘1P;r ^̙fnt TV-ŢK.]tƍ/Bm~Ə?a„nݺ;wu?)S|^ՂYAhѢzff/ܾ}C9WXGZDX.|g}lɻᄏG_pԩمf` 7lPnØ[pbZZڒ%KbH+K.ILL|7K/OY KwuW)v͛tr%SԩS.~-Z?,ߚfZB p5ooBk~gՆvFG.?Xsoڀr~PuGڵU&MThOܹsVZr>/Krrr0/<ePBӦM'LpQG}p Ac=6ߺ}]Nw:GYpz^=z˃-RJ>}Zh0'իW1߰aÁ?ӿHgݳ#~@W+@NN[lڴi<}_vewULްaҥK?x`ʕ_|Evl]v}WZ^A`+ fiii]vE]tUW@RRRڵ۷oWk999ڵk{O?]<_|<1N/5ީ<0x]j՞^>z?Gq5ׄo333_~ `_q׫Wic]tٳgggtAguTu3foO ~U^]f -X ΝO߫rovFFF'Oyb^nn_|1`o?3lW_}ŋVGZNo^4iҵ^+@B;oҥ׸qQFqM6_^6mٳeff6mN;ԩ0#m?pVVV۶mhD4Q @T(@ D4Q @T(@ D4Q @T(@ D4Q @T(@ D4Q @T(@ D4Q @T(@ D4Q @T(@ D4Q @T(@ D4Q @T(@ ر Q+`!Xh`!Xh`!Xh`!Xh`!Xh`!Xh`!Xh`!Xh`!Xh`!Xh`!Xh`!Xh`!Xh`!Xh`!Xh`!Xh0/ endstream endobj 64 0 obj <> stream xڥZK WG }$MյK撿Q)ʖg1hLY(ym)`Iq[_pfۚ \[9/,. zl^6藋q>e@|ny}ί1.3&˟)],oc󊘇5'_!*VVT~{ Q_B!w߾@&>ʒ&XnVdʤmλG>#j4w܉9xT2p8qks"Q;MN!w+(b|r3^4%]) r U\8 }^ܶ}phDYd4lTSd%Ϋ܅& zrn%`W9!nSIz%_ VK)޺/1HP(@Fi?h4s83l f||8y@{ފre xq!~-Ђb i>'DZTѡ5^AGͅL<ρɶ1MDʫ' k<=?[ +c{ɽ؈}_UF@~pbf1U{ei)K |[$41DoQm{;̍xLiu)$D ՅQ|5 UPHU@Լ*s2J%Tv ]9?59@4p GZu-oMdn'_vps'2J ͉Z9RgkoYMW?AЎBQG,lƧj&\ =* T(ċiw~ g2 [Ԗcl>&?g.6!]4n\3'"g'S7ķiK DRFZYLT0I_{µzgQ#>w@XM4l1_65|̮y+<cX}6odvF5ucR `:WJ1CiT4<ȴ .u@+/*c8C"q#,9P-S]:-Ǭ1U-3$aa# jB$6z:[:x}D|hn*CV-% v5)5<خ+vf͒Y>Q̘&Zr 7Xi߹Ph?n~|6ڝ漘AM#oάȃ*ZSzӃBV^ŷC.GV|v3?7vofjH'襏vL;}\ʬ5x MX:d !u>ՕeF (/D99pvLA$?D F[\}jYА]ucA~+mkq]9DT4lѨԳ;l7%=\>s7$}bZ`:LGIz}e;R;8))pN;wwJBټh̑q5k xgn_ZꕖYqm HlvDfV=t V5 뼰H00C6]D3ozWx|/CTjٳڧ559 $mN+ k|9*uu*jdv + uBF6:n:WpfĖq F+!HXP ێ Npg-؀ENM]紹[Io) j>~ە']ށRY+;' lDO$RFHyΘ .m$n (֛5Mji]U7t%o&Y9s{>I)#f"5:BXiqqhQJ:]i܂C#nԇNt0C7Z&h(/\$E&S)]Idۙ}NR oQIӵ1V>cAY)KAa9n 9f/uX&}oϿ$umYgo׸ 1l(jM/O!R2dl䒥׫.4D¥T_5ݲ-њ*vPʲ{RU$׾\roejTYI۹ r[~JKƻ'l3;!?݅#)PCmG rs$Y-[*xJE[7 M4"W"U_=b5j:[v&\M97L>s8͟50܆ofš5EG endstream endobj 67 0 obj <>/Filter/FlateDecode/Length 4539>> stream x1 ð'ݱQ$E____________________%___________ ____________W____________________%___________ ____________W____________________%___________ ____________W____________________%___________ ____________W____________________%___________ ____________W____________________%___________ ____________W____________________%___________ ____________W____________________%___________ ____________W______________F=̚ endstream endobj 68 0 obj <>/Filter/FlateDecode/Length 71189>> stream xTgqQw؍5&Xb-5Fh4ޱ {+6DP,HǼywaA`;e>ytt͂@ ]4@$   h.H@tA ]4@$   h.H@tA ]4@$   h.H@tA ]ŠQ 0`ڵkNڪU+bرc͛&M#Fۗ'22ѣ6msÇeM~+6lذ'$r u9,,$gq)ɫM6ׯOgϞUJqw5h7޲eᆱY۷oe9((h„ k׮U_%JOaÆ-[ʚ3fP6ٳ~ ܼyS3gLX G~)<(^2P&M+W*9А .l۶Mm._ܡC3fxxxΝʔ)}vuJ}X\9bV^-]o۶m^jz%KZZZFFF&8 rqY4hB g֭V}Jf͚Ig(}}VgϞ=tKwױB+> 7nQ@'ҥys5ʕ?+Wˇ 0ɓʖVj֬*T}v9ΝW_vZ/_ ʐ!C\,YիWׯ_wtt_۴iCВ_|!qV':(Ut5K'OgϚwaҤIVVƯu9**JgΜ.]:}e͚5KSLٺu,ԨQ]vlܸlڷo~Ӻr'yiժUx'Oά\2[ls440}Lu8>ssMVZv[Yvڝ:uDnٲeݻ7lذcǎqm`aaG b&:H>͛,0(vMddܹs>s1 ڙ3g/^I^]޼ycƌ}Q1HR%D̙ݭ5ΝSg>js̑ gnnǏw>$yS4hHq޽;ԉ4| (UF|Μ93x` {[%ϟ'nŋ[~m۶eN$Huԩ}GLQ~fȚ}/EDDj}=zSʑ#GbJ,>yǧjժ۷˚lٲl2]t͛Rd}R-HUURu>," \ W!JĹ%^zuȑYdɝ;TŸj|!;wlԨF1)hssڵkKcǎI:7o^iA cr~GuMTT ҄:;;׫WO.$wET敼/qU9 ϟ'ݻw_~!CŋW;MH:7o>tss%ׯ__vuΝ+7 ۷oʕ+7o͛( 'N <ڍ[n}/MhѢ@y(#}DDY}aÆϟYF^~99%=u1cƨ9)9ի7n8CCC Vf˖ѣG5۵kWC.~YYYٽ|Ry*]tM6?~=ܻwoŊ˖-qرcCF}R(?Y㏋/JP-Ztȑ~m\/,YѮ];%}̙m۪?8q 22R^2k֬˗/C{{{YᅲRڷo[.1ԇΝ;]}vt(===czJy*SLrRfj" G]jAGI@_vMΝ;׬YSj6cƌJ˗*mkk+RxٳgLO"M*޳gd֯__0WIQJqȑc…ڧ.]-M- FGGAI7~Y XC\hFciӪè3fرcI&988-r)倀|?֭tGR7n('O$U Iٳ4uJ%SVNXM@/_\vx]uKYi__߯Zq⊕1yP*Ut/ƛݾ} dV)UnÆ [JaI7y8xv 8PJVhK-F'O>}x{{¸q ٴia6m֭[k׮5S۷o7ĉ#wTTˇ[X>[[jôiΜ9sJmݺuرc9e@Н;wZj۶m:m4!CY>Ҷm[uܴք N8l2%RL~)e65jԐPWOYfO{l4޽{줖Jm7xjѢE}1Z.Sbu{{{󉌌&3f(ˑ_`l#KnDԄw*Th5^ TrWnzRn*Z /͛,UO 4]vʲuߊ+_;]5yd^YNi&R|r,͂/)bj%Jk=v옶RI7>|X[hR 3g\tkrf1*I%+=z$]thrP7!!!Ie:#S{2\>>~WS"SK,iѢ*UHs5[lInĉR%&`gg׮];҄˥矷mۦ4s P2hJИ ud uWqD5|S(QBJ _|񅔬~iYl0t&a4~AbUR%mv?22Rt1 :N\fϞ-}2ٳg$j eRj`3gd|󍣣Y9=X*TH}IW7"F˖- Ԓea„ &"mJYLn_~W4Ksf.HUGKڵkjʗ//ʒ .h[...7C{Sce۷M4߿ӧsGרQt 4>}oǎsYx|/UT;ui&5Qx}t߸qCY5kVƌϺʞdej+k֬ru?CrV\DGvww1cF׮]Dٲeb&|Ss QFGM0ɓ'SNU+&>i$Y(R`4ppph޼\ׯP-ݻw~6m?i7o;vlʔ)k׮mݺuڵe,M4ߌK֪U`D ]*_+ 83̖-ۺu딇gΜ)V:os͚5j~M6XpaʕO?۷Obii)̋"P~ K$W&>tss+\q0gΜl֬LC^ٵ3cX!'|rY[Tc dR~%K(3Hxɣݛ\p˖-ǧOnǎ+W- PRJn d<~S 4$$De JG66mHFK)Aj5Qs,X@Y| ju|R(۶mSlf|\ yfwn3ǐ!C$w6H.B*]vMz*))e鈺w.|Q%NNN6l5Æ .E:.ev:|jO"dtiuB RoMqUw.՗_~|M Q5j2Ez$tJ/{C$1\mܸ1,=Pj6"ߣGikW^w^'&u[ۚNZH9sfGGG)1XR۷ooj\~̙3#G̟?޽#U]@>.\xԩ R֬Y73||,72&h;wӧ͓B BxxvW5aWNr .TEfΜy޽W9sv.]bO5qru>>>:5N:/('Nkvlٲi 0avdɒڜg?k'ȶ=zom v5w°abz 3j7u+7n(_hr2>{Ԩcǎ73ܱcG,f{^H2e,XP®}Y\LbbکQz~w͗/kcǎ~ tPP2\O]nggkdɒ%otԜ~A9f̘|;wnu1ikUV-P@͵? e_Yhذa%7֦o.]Acp-`wޭ .{Μ9K.&l Ν;Z-j׮>~ÇƕP˗Oꫯ+˖-Oddbbt<3Y_j֬)rss5jT2^׍Uv|ӧOϟ||J/W\whggdpX988&O\Jm{j`Po؍-JA]ΛmԹ jԨYSα^ڵk-Z$z1c(3dШQ#[vaf1# Gm|5GGGY ᡎg:z peNZv{n+W^jʒ%L [*v{-W+wܱH%5P,*Tt&6:tٳ[ʵH t4 uy$b&;Θ1S}ieʔnN$--PN:w'իo)g0}F9ܶ.&6MZ|ys鬔 1h+Yl|`„ oU* ;%֭[k.[^uM_]$f7`|3I_rj~?{8@?<Ν;+WܨQ#yiMЎM`6m2f̨'1)bIVV􁴷3̯.7n8!߻wo51{츶T;vL\x{{#˔)(Z4Ç'&zL yU6e6lPfTS2jg1[7rR53~/m۶32Kcwo-b4 Fc\MC;c}bѢE Ә씻>@8V$v/z͒%Ke$W号W*5HUC7{rGS'S$&c4)z),br N1ǨTҲeLNvځlʄ6oKmSŸezzz͑ٔk6lX ^z5k5MΙ3GQk?6ѿܘ;5.9s̚5G4^ASʍĒ ܡ:eDRC\cɥ m&%Sq'WIeP+PЦbС>>)pPұx_]8YPzykٳgܭz(P`Ϟ=]tYr˗M49~xbR{yy͝;wݺu ̚5Gqvvn߾:YM4GEE%a ɩ TRwI5JA hu('O)ۄ\1W\zĽ{,Xh"+++֭[U@WI, sNu-Z4lpyIhgMHjci |Uo&9y*C'R VXQrey3Ul۶m߾}F|ҥK>>j؍D# @jd<:CjҲe˦M^z~۷o_RF/ ԇVqF++P+GGGuY\Y$:L{cL2_liF'رE/^~´A (^]ŵk׾꫹s0O>oeXXX 4[PPӧO5F~ɒ%K!D! ʚ5uF.((J*ʬ &L1bD @ժU+U>1c{gϞկ_))׾<߾}AvLs3+STۏ3&22^zv[HbuSjbӧϼyt:)%}>Ç/[[ۉ'^ze˖ҩV^ݻ 5J&Pdddv픬_S>vww,],o%Kˁ yZL];pJ*_|pBJ`|J`-,,͛sn.˗/+X%СCo޼٣G  =}O? _=hTTعnذAʴDʠ1ctB(00`6w-[~td&M *,Ϟ=۷3fs{Rݻ7ޗܺu+/UuWyQCCC׭[ɖfJL4IY._z6¯^:ydJP\Μ9SNYdY`֭[|Rڑ砝 ̶ -[W={iӦ)yЛ7o&;0$}\x}*Ut颮Yz  5:vrʥ[h6 DCU.\pڵ^jݺиqcN֯_K\,'q~duJ%JTXQ]_reuyʔ) ޽{zUhDҥK]rwVZi[@o޼Qmȕ+vuEjŴiԇ7o!ݻwV4Cm ٳ'!ѣGt%j7!<o}vss[pݺu޽3ý{AYYYT'߾}uָZ;%H2jժ~Tl"`'QQQ%&Q' }5q])Sxxx 2$lѢZ{o~tR>}t`Վe]]\LA5%<qݻRK==0իWhk&ioe"/s>\]2ŋܳg=RY[[+Ig˗/]]]V,6zhd &xB֬YnzϦr/Vr*Uܹz͛W}8ydHkڴ믤D^[ޱy?o739qLH}WIٳR@ &i(L`6K,r 2eJߌMW9sCҦM.'iߙٍB[JӰnѢ))#Fѽ[o?S_m۶C(ҫhwرٳg_z5  ):i^~=޳վC ɗ/P\BӧOl&]P{]@DOH#Q)ڵk۷5k&Zj6QLT0 kӶ7nh/̙={|3gN1cj#+=LՕqݏ1Y_ZjӦM 64jH]qN_ko q:}fw޼y󼹹cʕ+;t~uըQpww~ رA,&3}tGzĉյA <Ϝ9>\z;w ,޽_~)]l޼YQ7nӧdɒ|wޱM̙S]syI-J(z)9rΝ;¬XQF(T&!/0>nxxl)APHkܸS=wv&Wu֍k4iH]V,Y:+)ֳgJ' LjJ/k.ub9:ǺY5{K.Hw'Sv.E)4iСC}||)"m0g˖-CƂ kժUοVZ[J*2dȠm6)8iR˗/_lOJYTRϥH :uD[>鸔8 |- :9_rk:yJQ[R ĦM&-ʝ;߱ctr͚53(nu]tyIӈn# @=:|p>}.\`𬇇|l^x9sd_9r,g5Umiiٰa 6i&CYf֭*޽{/Q|i^|}}.]T|yݻwvM!)SF6S˗ h۷ܹS7o.,_@9sF.-[lrf>|6h4_d߽{'O:gΜʸʕ+=L+,Xp߾}Ϟ=[rc|lllwѮ]gϞ]uqGΛ7֮]>}z[[[՞={ڷoĉ/r&[Hyӧ9"' !CHX L6o߾#6ke/_6z*Th޽R5JrJꞜ\ۖ-[񛲁ԇ)SH++ (h">ܶmۮ]do3gl޼y…FNsoR>Nhː!áCIJݓ**#"3^q<I*PTHm$ݺuqㆺ֭[[fjիWRUT888 VHeibv\BjEժUgHRJIՒΜ={V Sɛ7o_LE"֠Aɓ'KH |߿vdw)irYd fժU 0WΝz"r 4-ϟ?l0ɮU׮]Jgoo}6G5jԐKPT{SNIݓPT-\ RWXUE\R(עN%=@儵*Q)/a_Ti=\zUFTNLѣG%뒾f͚;̔)ZRӤyʹ9lڴB [U%ҿ \Ik'GQ/z.UWM@IFb%a)[2GvH+]&LP~ %(xib7 Q7d3s͚5S+ h[[F\Vxj#5MzX<Sg… nnn*>}lٲ*U\r-Z0=Cb}u[[5j;v,A3g<`QF$.] YdTw .k׮*Tx;wFyfevZЋ/rd=zȩFkuaÆ NJ+Q2JƍƛɁƍE~i(a!9s8q 9޽{ܹS{5kִk.wСU_JYf)S5k,RBj޼y={3gTxOO>NR9Ϟ=KYLS+WS-گ_nݺrfpK,h6md\[*V( w֮]`ZU\ׯ/Tvqqٷo6Xn]mD-5V !uOJpNL:+;UtתUK7ҹ%JyIsN&#f-_}Tf•ԩ_.\^ hޢ9r93gN777_|r \A֭[w -]\_ųg϶m)S&NDhrhݻwe&R;^2eȟRw}Ec%$hUӾOXo)EvAׯsQ\95/ga|ʕ+u!|RO hQ6l, 80SӧOիw-[vD"c4mڴ'O:99uڕhcwԩoȑ#Ʒ>^$@ջwUVKz=z(_ŋG!N0!mڴ"##pۦMѣG3 U۰a dիWkcO+WG>|.߿2QF|j=u}L֭[ga9~pǎϟ?X|&,ǍG@dɒ0Jgg{,X0՞G#FDDD߼yl2__߫W޸q͍" c ۷oȑgΜ ˟?VOҥKGfڥK'<::(s@tA ]4@$   h.H@tA ]4@$   h.H@tA ]4@$   h.H@tA ]4@$   h.H@tA ]4@$   h.H@tA ]4@$   h.H@tA ]X*88СCʲ 1ݻwr3fن4uСf͚|6oܴi ]0.o޼9_|+W(EܼySV Fw̗/_ѢE >:r-lmm H m~3]4@$   h.H@tA ]Ši666 P @xg̘8/訨(Pennnaa!dGs",}ӤI!,Bݻy)˽z>>9=z5Taaacg͚ @ ꃫ>:aB],;cmcKLsֺtn߻w/""Pqy4B'7!\DFF}%D4BD\<|ݻwC 1ҤI#1dYY)ELΜ9OR,DFFsmeEvaootݰ0e4i8'j ೠ령3g4 i@bm8Hg;;;kkkb! D}+b|"du Arځҥ# @\B:H@DZ}tt4AfV>L6-1- GvREEEirS5b|^H@| ~pNoݒ)8 9\ H@@|e*P>. b#oX,'4/tuKnDEE?}86ܳH$_]VcaiIL|~ňAMnΖ8C~lVeY^$tA ]肛"ԳZqG',Ce[kb+REu=|0{ .^ح[7XH!!![~5@H@HΝ;߸qP .$$7mD`7!~8w\BH@@>>14i2|pev׮]?~|\*[ٳgOJ*GU޼y3'о}ʕ+k_~!ssse9C .dR .9 ^nM >~Ǐ?s p႟lm-Z뫬믣&M4qpeÇ.]sǏ/߾}eUT;vv;w'O֭wVWZ%/رlV\˗+/d" |9cȑtؔ>}u<:88;VY.k92eʔUFFF\R%ѣG}ݱcY |ѢEuz;w40 dkkkzZIFM7)Sf۶mCEݕ'N"ҥS_~mARի,Xɓ'Yʪ}@jq{XGd@uav- o߾?Qԧ^~u@H-\rŅ]լY\r^^^:Zyj˖-o޼a $SpaÆ) k֬Q˲cƍ 4k޼y?SYxݻ۵kgccC8$ϝŐ!C/߿_6nHR":t={veyƌf1o,XB FH-"""2 0@Yޱc;v4ssse!22CRLC  |tBCC_ڞRҗ%&@ 9s{{ׯ_7r劺isҤI/_m,fXO>+F@Xŕ-[irʠA5=>͛7rSY Ysȡ,9h9kRPx;/E$~AY Vڲe.\wWXqɹsV6?뽽?V|-uٳgI~goU* m۶=vԩc%g)8 666p@um7ntYY%K5k4jh„ ʚիWתUDP”:t6mڌ7ݻ/, 6LK  |` .\dɕ+W䡫kVz$͛oiٲe-,{3f̩Sԝ*TB SNTg̘T֭[sZMlll:F_^eooo4~駣GMlٲ )PH@E>i&vhboݻwOԩSB6.Pݻ)>Y ֭[]]]kԨA(txDEE=yDYΒ%C'?m"""LoB$lo4%qPBY]ӧ'OlnnN#H@@GӦM֭@t&8|ҥK,Y $ F@UV͙3'}M4i׮ݍ7Zj8j(B섄tYXϞ=}U֯[.SLɅ)8NڴiӥKgrʔ)mڴ!8HF>;VVV۶mwܡ2f@QzUVEF7a`ܹ}$z/Gρ'|rܸqbŀ՞nYDzXŒd=غ:gflzڸqc:|F@{*.[fMnnnPO:5uw}wŊEEEuСYg%&&n 63f .ˋzw/]w]&MDgy&;;;p_.]:qo; >:mڴEEEAw%3W8pӏ8∫u7n={vpwyѣQFLy睉'OYlYdpҥ5kVddOhҤIe˖5mڴI&zѢESs{'Zl~g}ZZڝwy"Izpgy-m߾}(N9#Fx.IZ]w+(7nNp=f̘͛7G6͟?3nC=tN7n̙3[nYjoW_}u„ }\ؠA`ڵku떓ӰaG[nP=zYguW>LrM7˅^xQGu4gΜ9b }V^C.]lP}VZԣFꪫ"ظq>}СVPKAQNwuWxbuPvie',\0ح*{=cƌݻG|Qk۶1c~_2vnaƍKN)uy=㑟VPȶ 6m?۷ #_Zg'x㣗ݨW^dSFm9sfdI-egg?#Af͚zsα@FFF=O/oݭ[_x^Z`A˖->y]WewX$}n߾}Æ ӃErJg|lԩݻw/PG}|,Yg|@dcٮ]y&|^x!ziG|1LNH!tsN/b'NDK5kּ[N֭ rM7ըQ#goFc:o]tcǎe$N7n\֩Sjժ]v]We#ٳgG7#f͚y޽{M4)P_~'~G_n|:u{スzJJ߅cǎk_:Iݘ1cnf!I⭨詧;v+8mڴc9f{2dٲeᥖ '|rȑ뢵%mܸ :zRvxBv~z#a|g*'NxmիW/5jnW[neieʝ:uz饗t2k֬F5ߪ#8b|J6gΜ-ZDFRSS⊦Mv޽dܸq}_@s=CwqGrr޳gϤF[j5x༼ʕ+/c>mڴy_t[N]VHBlWpy|pĈɓ'=,=vJ~~t+p>}l쓒"\έ ~ZEgqƀȑ#5 `7/ڵk7x:̟?s -{??}ݣ>ө~z˕1EA11JIٺunCO:p=f̘(..6mZ.]4h}ۥT BG񫯾Zxe֭[_7mذܪfa_?f{em2і-Z*O?ԩS^^/)SDhР?~>|߱`tP$݉F?cǎ3ztVVPxZwXϰaf͚Pĉ/ٳg\+@<߂7,KKK֦cߺurJ쇭WJ{O?ymQ/%9IOvo?wj ')))9sa^c##~O?=s^z]6 z;wY^yΜ9]vٶ\lY>-Z.6lx!luO>#JoExO}瞛UTꫯ뮻"a~͛7W';aفaV:C.Y$RDo3g7|ӤI2֭[͚5׮][&ed׬Yp8^{[ 7+=\0vEEE ֮]Iց>;^/^ԗ\rɏ?hʣR!3"s׮]}E=Oo޼yAJN6j9[pѨQ#'&O6-zӆ n0:tf-XPPiӦ86l~H=cƌ5kքH>¶ :9Zj}^{w}7( pglPe~xҥACծ]INNkunnܩS'JRπN?p-Z6mk6{>pssssrruvG}?'|x8F?*J@G~MO+SN)))YƗ< #"ڵk͚5}v:&LH(!=f͚_ `_~9O:vذa}Q9sf>}7nbŊiӦ=s'MԶm<]tQ,= v⋃o-11LU24f ['tx#8b|#/3_fff6k,;;mzcǎ袋{ڵk#FSN$BJ%PtА!C暭.zm%&&]vm*+o߾A# 0`ˣUV-8󫮺j 7ܰqܚ7o~W_px`駟Oz7tSnnn5 K.~35jԯ,L*uݻw?OW|lW{Ypa8qbMRRR\-?9rꩧ^zȑG?wk].UN~~Қ;Ώs=1 {+KmukӦMٳRe/c'ߦMHдc9גvʦRǵ;vKy4rȻᄏm۶ 4kG̛7o}]vff 7DбN;u͚5[bEЫ*{>|P=ժU;sRǎo瞒M6Eߝ3m۶sKgX"(F%NNN...NLL|衇to~7|_ckO_x7B2tRRѣ?nܸ_~yɒ%`:Ot2pM-lMbR¾u2dVT6Uc#8{\hJF.-ao=;;׍-*vGj  RGU_~o~ꩧ///7MQQіwV2@T 111==]`gY`AO ٳg6,PՅ 4q! .,P5Tn1@T(k  C ---11Q`(ׇ@nv *֭ YYYP4@ ͌M&].CC Uؼy?K.dBPu  UfΜ9mڴG}T.hqOE`t~qZ@ =z,`ys9gZ@T$9%I`Q\\|̝;W+1$&%[K¥K6idPTT?3. ;L <~'~O}z?񏼼HQQw}׿W_}5暹s;Go^x!(N:˗Evvȑ#[322\>b!cŊx`-"UN8ᄂ|m۶3g=z J>}rrK/VZȌ3?x㍉}:utwqyO>d~:J(|go|uW^ӟ ~ئMc=yk֬ >Ñv 4ΧҲn>Pl. ݞ5FCعg:uꄗ8^fVZ; ?[Z8 Rq޺~ s疔$%%(,LEhܸVw8s[n?> )333ƃ'l#nٲeذa?r KLL̀D)WËM"*cǎoӫh5;C=4R]VCPԪU+RG/Y͡?qJOO,=`WJIOWn] jo SfPHOؕ"u5*P,Z(\Gߐ*34TN۷ojv&&&bW%ebBV-?-&OP:뮋/))X12###\YU`{T*ؿ]> ,Yd3f̘;wnPz뭇zhum(***E7n6l.p~g}Bh,fϞ?!zRsB]tQP 8ꫯ.yEO>dcX"R[_Gc=6\Z /|w_|`0h@,Z֕iժ}7eʔyIIIf5jT~~ȑ# s999~M7;W޹sC9$333tO>aĉgqFjjjddʕEо}S~ww]6t@rrr0ңG׋ b_韌deiӾ}3gvmwy333:/|M4sرÇ/k^uUAq} <뮛4iҲe";o{7~_M>=xf wu]?H ^z~aZ:vE,() -Y_$)EсxwTQ{XN裏~7]v O|\Dn @֭[7}B` (+111==]Ją:/|ƍ*/ ӧOgϞ.:gشin'1v5k֬\2\'ž h=?;,)) |_|9ӰaæMVVM{>(3xx:55UOb!@(ZlY*JLL,/=m))))v@DO?]C`ϓ_ Nؖr>>l4 ՋYYY{Ĵgffj lU CxW^z˳ lU> [{pٲe2h(#|#Q+dddFu-^FժUKII-{EG&==]~4^!11\dIdsaaRXg-Eff/~ T(̀fҥK˖-*g_~e>묳222ˣxB wl֬Y~~~^^޺u iiiիWQFzz?Y'VZZC=_:ի JJJ" CHJJx@T ==+`O@< !q! .ą5* ֮]k֬1@T ??Æ XX@hB @\ 4q! .ą@)ZPʔN;-Rk@$)LI9c`{Y@`י@vΑi1@T ׭:tJIMX*PRRIH@ą@ր#wwYYYz@ݎ`rB!= ą@hB @\h@3U^甿1@Qu.#KphB @\X}pPZz h hvS>k^r6'|"\qFzW[T"-{P @\ 4q! .ETf9rmߜmm:`@TjMN BZv .̀@rR1m#4@RSSN;H}^h73ns!P *PTTE$ +e5mYTfh -bd hB @\ 4q! .R]_Zլy>Pl޼9/pF =`֨Q#M`0@hB @\ !@/H!1@T 11f͚+gkQQѶ6eee@^cǎ;P({ą@'L?|!P(///Rk@,@\ {z 4"v 4u7n\Fi;%8 4q! .ąHv8թ6o\Fp322 h $%%h"Rk@:W?A͹USM^TN.9N1@h[4\W(1)YOb!@Iqw -N,%8 3b6^`NNΖYYY*fư^[6=Cvnd B @\ 4q! .Rۧn9@u*4U̾ul93PXjmܸӵ295SZCbdt6yKkObRVP>3ɓ'W^3 @Wa_;3>Pɓ'_{@%d *lu9S t\8Io|$-%YO*px= vj޼y ,4hPJUR>R#eU5iҤk>}zUVEF7av=k@WIPhʔ)/:u=/Gρ'|2??_] *魷o.l͚5A=u`(1`@WI'Oݻxݺup v tSTTSOvam۶ 'rިGYYY@9UϬYrssU(4iRrssp-ZIG)S͛_]vݺuiii 6ԩSvvgYؼy>ۡC>8g;vƌ_~ׯ=v3f7*"3ft޽8zR=X۶mnj_j /mcp 7n,]tJ;pN/Nڽ{;y䤤^z*\ԯ_]vEEE FL|^xz.lKbbH/H755u o9bĈYfm޼wޟ~i&Mb9f^^ތ3?{oNo30`?#G=^Vj[ ei @"P/ꫯ/^lٲu}M6l+W|W8o^_}85kR"g}o۬YaӦM+..ݻ>1--m[Zn=|)>ȡܼ^$7":z#F,\pmowI|e˖dUE%c_"\n'@W_}]wE6k??͛׫W?ch=cڵk͚5ݖO赮qSo5'w's1h1̀f͚Yfmk3gxI&_{c'/>3[7nʔ) x㍮s{^5\Svօ z'N ?ܴiSQQQJ6Ŕ)S5j_zϧzjիG9zoN';^z饏c;ˌkN[`!FVZ+vi￿n{ cX2zꩧz@%6mF+ѐRTif@h:mڴiٲ>Pl޼yeFF h hvVz!@STE T+ol/4@?)w6Nlk@hB @\X"PBq͛2===11QKb!ny*ާ0YÆ} JOO/,}H@{ WŸ=f=Tg(Wk@'hB @\ 4q^*ާ0YCOh JOO6X[STYV<蠃222  .ą%8*Ν;fTY0]oiY7Yvk h h cuBQ*PTTkc9F #KphCC?Ή~EThbܳ$Q("* KphB @\ 4qP|3=rP9 p!+KO-==}ذaZCb$@bbbFF>l/k@hB @\X7oW^i=h *yfM^hveaS̬ @\ 4q! .E@U0'''aVVPʸ JąHNNn۶m *ąT<@O?]OPeԫW/aVV׭[vB *PXXφ-Zbd hB @\ 4q&:]tiٲ>^ hB @\ 4q! .R|iii#4@8}^ .ą4@p}E @,B׿"ą@hB @\ 4q! .E/)))+++Rk@:3?[Y~5}*s #ؕL .ą4@JJJVkYs@C!EE "#6=T4ذXX ^ *EAD4 X(ԭ?s–,VwNV* *PT$DB @$DB @$<`4Rz߸/)461'92+Sp 4@ s@T %%Q$I P̌+>A6`ѥzUI jVZoLe!D㿬Z֯9I@T 7/^ &0@ SpPuۆ wSj^^`,5@vv"P>w@ 4@ 4@JoѢEwlŔ 4P<0ɑx\H@Tt][n$IPхv[6 4@ s@T ?? |pzzy7lٲP>4@ ?䓰}nzl| h"! h"! h"Q[ 2-ZVj7h lpVV$DT?jO@S GJKT=Cq @$ PN'DB @$DȚ?RcՖ_Lz"lw8ijDg3Ko;x\jDB @$DB @$DTTw;j4@5Rwb1jSp 4@ s@T %%IN$I Pڵ[vuTmobŊDO޽{jy#Ԡc5`wj16eʔ)%?2@s6_~?˃SO=vqzҴiQFD l¢1lۮI4;4@ $lw5$Iq@WkqjSpl3f;v̙_~/jժVZtA999|JJf^[z[oջw-˗=zҤI_}U͛|^xP/_]Ƃe}$@ %"&M: Kv}Y졇ҥ=sl6m6klK6rу ZvmgcᇳJF&ZP-$}٧UV+W;w;8f̘ѽ{zO܌W]uUh޼^{UPP0s˗=cǎ]tK/T~M]m+VT8,(O-mӧMV2Ɲ>}!CN~^z͞=m۶jժU|pݺuz:矿N;kNKKKr;F:3SS3w~~ѣ x6u瞡C:x曏8:<#D|A+Ҡv{キU-Zh̙>3z谳yעj?ӧY&ɍlذo٫W0}U_~Ǐ{ƌ;@riӦͫZK.dԩA>*U;v|Wn7x#ĉzЋ-ݻN8nhРAym%n.۷ ֭[7Dǿ˯oYdɪU͛.J壊}Ç0aBΝ _ڵksuUguVYՆԩSƍ'~~;wnNNsCJPTTs=3SN %o~z޼y#F8p`su]tEW^yeV6ce-b;w5kVaaG}tQG%f}6ɷwR[q=-5nܸ!C̙3rW[XX]ڷo?k֬dGJzqm-kV1Ȁvva{ǎwef͚}Gy&3 &4m{o?{Yg?iS(_<w|)))f@oI&%={:t`LZZZ+6m-k?;wn̙֭3{裏8餓nᆽ;믿َ)P-{'?iҤM2gΜ#8^a_|Yfk֭x:--;VZ䚧LRΗ~gA#==} `|?c8ZjթSz=:t;kjjA:?quIr>'\ꢛn)lٳaÆ)PЛ]vacԩ9991cƎ[x;P{_2l_rnij ի]w4_}M4I,3gέOo׭[WPPP<;D>}ԩs1DŽoaÆ1"|;xઙgj3'77wvTk׮[zx\p<LJ vwvҥe˖_|x⒃gΜYn&MԩSgРAw^2_bŊ6G;wn׮ݲe˂Yn]W^7p T:=ztzFYTTnݺwyg]tzwv^^޲e˂Ó <ȉ'vamiii)))w_͛U,1 ==}С_DAERSSGѧO1cL2e… 4h׮ݾӽ{u >_7oުUjOۼm 塇 ^/={9iҤ o裏wل@TЕ`=5jTYKԩ3Xo՘ba;;;{D1 2J@ |ϟAO,S+2hrJ2Z@1@ SpT 55M6$I P:J6[;L0aνKe`+@-hٲƝXLe`+@TpѢEauiiij 4@ ^}հ}g !DB @$DB @$<{vIq֯_?mڴgee @2 @$M-/j2C*Q?(Ֆ;H9*ѧOD[A$@jjj,SM%%͟?[!)ՊjǮ*}UC;*3luYC$4@mmH)8Hj(ۛrjK 5J%b1%2@ 4@ 4Tٍo4M*~zD$%%E$I P̋/X6)8H9*PTT|ݬYTHyyyGRdFh%b1U`@Cv F-.[LHHDm%6rPFџ/b@b "h $ $4@ҺtDB @$LAysZVui)Փ'N w$I P3f=zdh"! h"aha/_^KbJ@5!K/-gi<W" Sp 4@ 4Beee)@C sw|{ǪIԲJ 5LfJbj@dh"! @< ۙ)))j 4@rsso=p,5H)8HsGwI`;h"! h"! h"! h"Q[ *P֮ݭ[D[A$IBY" y h"! @AA[o>`!Hzڵ I h"! h"! h"! *3Sj-9ldAFfPwE`s4@ SpT =#3%$I P>lSp 40T˗.*kb1j*~? Ǟu>.;2x>"! h"ahN- /<bHFn~ѝ?s:ge 6fe/_jw,S7L&M: Kv}Y졇ҥ=sT=zA֮]Yxb~ᬬ,εlBX *$.~g̘1 T:w@M6jYKSSS/䒩S>m֡C;}A{ذaݺus%NV</ofɒ%V7o^h͚5v:묲]a=uԟ~qƛ~~;wnNNC =s<ԩSgm &uY[IzkbΝ;Ϛ5>:ꨣ6i3sۀo:ef@gܸqC 3gΖ*''gٕ@7o޼v}f K,qVY6L ;o۵kwa;ve]5kyI-55}m DD]I&%={:t`LZZZߑFvvT.t|ɰq?5wGLr'hΜ9}YHOO8^BңKD[A 'P֘#7tSٳgÆ t(KԮ6  Iҵk.lL:1'O;2qg}v1cƌ;VD"vJg 뮻 w}w&M$Ι3[o}'·֭+((]O>u9c·?ðaF|xu SRR?Xb@zzСCz*TPPۯ@u#F)Sjj#3f̘)S,\Aڵw}srrwnݺÇ[jU.C\pAϞ=4i҂ ڷoG_|;l6 HJo> {キ Ib F*ki:un=XQR40V>Ҽo<,PSN9%ax\DB @$DB @$D/33o߾Dg„ ^l[Y˖-7blL@$DB @$ P{,l}ٙj w@ w@SusN:5M,5H;;:ZMf5OA^g9v$ h"! h"! ~"arAsbJ PK l*#gNȑJ PDD @$DB @$DۯHjXa~S!h^r%* 4k4ay24j(hi&++Ka "! h"! BlkxQQQ@2RRRRSSWth`K!}6IJJJzzzbACA*3͝5̐_֯_m T~͚5JlxyW._9{ꜜkŊW]u.\O?guV~ݨQF9ox~߆M4˺v>TvO~O~:8pmڴy'ğ9h˕@K.Yh?ghѢ[nÇ׿>}e]V6mzG_{~ƍ`%7xc=|322w޹sXz_Ǐ:_Ǐoٲevv^Z_ZN[zfXl"8h`uG߿dVvO z[v~éH6t{K.]hQ׮]gzT'O|)S6^ڠA3g.\o7f͚ &⋓'O_0g v9|w~{iii_}Յ^o$,Y{y}݆ ֭&z,XX}{%3>Pf 80Xɴin;$yqo kv=z>}zQF 2$R;x衇ku]`%w}lI~Δ:䓻vgH?^ve1jc'N袋c/<Xg=z{ww<'p杍ӧώ;8`o_~ nĈ3f9rd . 1믿=ݻw/((*|$ M:58Rcǎ y'sKN'o׿n.O?/s تEİYf]}.{48∥KaÆK#sssO=`Â۷|68sϕm@Ν;;O ,**.|iӦGu{46%%55svۭ._~رs=|wI't!ذo=`k[ju)zlݺʕ+~JC{n>h">h̘1SO=GfΜyGo>[nٵX{2$x馛E}W޿DvڵvZIwyN_qwygJJJ߭[ . 8 ,5jG}/ׯ_?\ڰinv饗92T 8_|1_`$L:58 {? C9d]w=׬YspިQƍ'uk㏯#8"8IΝۿ2.(تna„ ]wA||Μ9Ιis9l\M&M 0`ѢEov|K߾}57.WplOEAOpbqp3ԃu7N81g5mڴD0}a;~qǹm]hc;?&r 5iҤYf% m۶*h̞={od>|'|ұcİs̙5kV8p5kƍwG$|+V8qbY nDDzkŃ7o ގ;6Ow.J|dէzO?9>?ૃuKO9.]#`82XڦM>--mҤIAt{…'tҏ?^{1b]e]&O{N>w=\Y%>?X7XtW~k׮?͛Wv 3HOO3f{7xc8K_޳gϖ-[  w} ΢v!lN+|ʕ{wpΘ1#H8 8ׯ/**?.6c;/Ngy-ܒ 4ߟljPq1lP 6?tYۊ<`C_~yə>޽{o${iii<1qAA)SJ{/^d:ҥK͛W}7l=%%fU/ñp…{G2+*=KnNxJP.袼?OQҠAC=4l/[,M@駟<̍gNMM ׮]& 6L9~a~֭[~7+k[hhk"}yiw}!XmtQ J1r=z$޾ OOJ憭[x@0}(9SܹsN:%"F&l/_|„ a{͚5w_.J]>h;e矟Wa̞=G}tYXZhա| ,xg6{͉"gggęӨQR8s68mdKRH<28V\I"333Ǎ7gΜ vرGbR5LPzꠃ={vs-tSNנA0;CCFsK/OSvڴi=ׯZp oòeZԨQDܰaæM5Yfݺu 9k֬bJ"?CJP~r갱^x!Cwމ+e͛zlpDk֬gBZn]z衞={㏻mEhZ+>:W 4hॗ^ٜ9眳.tyo[jjj֯__q)$>,l;S6aM>=lT8%~еoȭ:Q'jժ?05?|`=s68m lo?c=O zذu Yfe- xFӦM+\nݺaܚ5krrr>Č5&^Ю] /A;|@\ "?}ڵkn%˗/H!!U[jU*\oc+^K-u%O4˖- :xF$#G +~СCǏ?gΜ?S (!zǏғN:):u<ӧOB[&$$-Z(j#cFtEy=nݺu` ͛7?E\{3vj޼y^zW_]ꬳJLL 7x+F>3UVo#7||}ً/9ش}_~eW]uUv5ƤIv~ÑX6m4|iZnݺmV(s233_0 %ʙnԩS?<#֭[Wpq? 0YG۽spYxUȑ#w|`O8ᄃ>O,wհa+򩧞 ŋ;G;6 Tm߿vsssswݥ^z]wHx饗ڵkw7߿K7PԮ]~ >#؈3θ袋9Ʌ ;n;kǣk׮^]6w@"8Yٽ4 o]paF/Y/2KիVZ+WqH u{w:r5p׿wz}EѣGw? jժ{C]nݐ!Cvnݺۻg֭{-1bDtAx7k"tM{~̙3ӣG6mnUy睑_ܵk׉'F.H/^ܱ[Fx`W}W{6v5ۭ?Mv ›C;{01cƲe"Pff楗^:w .`"rgϞW_}udӧ~鉉 4Xn]XRʻ۪U]KRR{׭[E-XW^j? O7pC/;v\?N{o/w [:uV\qSRRFw/:{u׏?~ܸqVZz֬Yk׮=f̘~ ]v;!;=r;m7yŊx=/D~y}vw ;i3gΜ~=sꫯL?x{D[;Vøo߾AG)'9ٓ'O|eSSNW^O?U3gl߾v]9l2eʔ.ؠE6lꫯ8##cܸq>/c=֯__=#ظqc^^!s/?Yٳ6llcp?{#O Yfʕ+תU+>>>h0vG*}?c0+^{j:3f/Ç_zu͛7/^x̘1{4hC=ӋN;m֭ۈܬ,~pH-[+9YA~W_ R!XyHs_ . ܹs}VPE/W\>n bŊEW٦!iӦ\8333H{x'MF6lXdb0nݺ" 믿CYVVVxxoN>]Un)R ve-TV-8ZD928"{o ^:gp$C H {'عa:H "YgVk#Fշ{d,<lrwcn;.8P`C=80^yk&X{ 2S݋XDLf!"H :@Y|G),UWnժUy'ƍ۷/t%={ Zp{[oOO6-++YfgqF~ uUWǟ}{l䝿˽{Zh1KnWoҤIG}t.0aBr3?~… W^^zzE:ops}˖- .{C9s9# N۰|SAƒ ^{mĉ{ ^(|^zidzo[k2dn<5>zWLjժp< tu_aWnuK+rrrׯa]pݻw_  Ij0`@Vpvq #6f76[ny׃b :w3"PK.v:{?tQ(@@1͝1cC=111-[ U1)9'%lI(s+eΫ"{&Nh.JoYvPll\p 6Aǎe@pq- H4@!$&lyz7lR^W^yE6  /_~wnߎ 垇@t͝;O޼yoѮ]; p4Dƍyo:u+zdɡ"P(D^^^FfV'& 1c3<olذ!21''瞛0a6m*K{ z`0UlѲՅ7)))@EiӦ>UV˖-믧NΚ7o?yXR)@Ck˓F*#dɒ_:u1c>l|7MK>hdo!-{ PBnᆮ]_-!{ PrW =h(9GqDT\Y6(ԭ[7 4h {qRh$ ve˖-a1hwHi\R$ ve̙apy!85*ٮ]O?]6(xZjܸq+VU >/Bl!8 (+WrhH,!@eKd͚5p@ZZZ\\\߾}$%%ozg>s4R(@QUrX;yʇӧKB,[,---} lٲ ZjuG=T@5լ<KeH&M$ sΒm?_._<;;vum޼yǎLŤ fmB oCR@ْн{wy PQ =u0ر4P$&&GzP *#9֒%K m]U'^ [Gydq*//O>2Q @T(@ D4Q'WSL)MÆ % D}P4mڴ6M4(hBTRE~/c@ D4Qa hB a|W ! 4%AM4ʜ0aw@1 w@Srn]ͯ~ʜ/_\9>QNJ[W# P)@@kk֬ٲeKrrW֩SGr(y PNddd3//oȐ!0?>묳曧zQŒ ĠA>8餓q?ʕRD Srb}W\156m4f))@@9v^z)55521999 6m$E0c@m^blj-qbJJtqFyHJJ)yw={%KA6m 0hBTOH,!nm8m;~.6nاOy6lXFrm8iҤ>s':(a kyĤ>ƍ? j֬ys9_~y*U~תV\9jԨaÆ? P!L:bnxΝ{8qb= ֳgB{^zv:t&M/ׯ?GhѢr?F/۟# Zl2x;##(qGg}vNNN^^zp#8s=3]t8q[J֯_q+Y`{=rc=־`P@:tW!*BӦM+[nڵkgΜ'5jʕE\v͚5\rINNNڵzOM HIIYti^awf͚}f~SO=裏~4i#@ S(!7g_+ɾ1CaqqqÇݻҶq۶m/`ڵkwߝ^zi5ۨQݻ5joyW cbbf-[;uYf%H@rsXNjOehv#w Py3gάYf bcc)NQ iii/b_q;mӻwQFR]|ŷv۪U. &G$h{7qbJJe^ƂGNOOZjknm"T2eʯ>e͍5j֬٢E>m۶v%&V `Oà}jSvp缼͝2eJ0w>}vׯ3gΔjJ4y3fۯfZ ovY׿-[6o޼_~yY[l ڵkK5%I q5osNNN~6o/?=//oҥAPjՎ;6%IŋgddqCHשS' ,Yݬ38jժ}!_~\s;)aBXL^}aÆ%&& |ƨɵhʜ̜ ے$T=~mڴ)2!!! rssׯ_昘#G4訣:;wo?wuWkhP.?훚z pb^^rQo77..n7t>gΜ^zk5jԐdnݺHo˖-86Ƕ02lvUv6!忯`YYYx$%^mH,!jժefff$64@!bccԪ*DOrrr[nݺ5 bbbW.ur 6mڛo9cƌ9s_~ƍ 7>ꨣzqҿW:tqΝiӦ}W]uqg͚5ĹLKK :u:t7ߜ_x?nvk֬)KuY9/M#q-+  гgUø]vFKw˗-[6`?ݺu?~-[ έYIZnm}޽{nna..]5?3sㄊ /u[#1Dq33vf֬Ya|K.5kO?kfAwߕڭxW#N;>}|x\wu2F]1cƼ;Ǐݷ{챛nϮ)p[dIBBB- hr999}݉'6o߾{nϟߣG@n#Fdgg?}UV}Y8اOLP.o=pYfׯk׮7:t>6jԨk.JP-[+{|jAi :{'SO{Df⒓u>Xhƍ7nݺ뭷^{Jdo֯_ HJJ9rd\e#un{,k-=8M6ZW޷~{ 'eU tP4h ֟R2ۘyj@ŖRΦ Xv4J233kΞ=wމW>k֬Ν;7i$)))[?bĈUV۷~3fx,Xиqcpd 7n\|)s=zvm*W\ֻyAPL6MjrNgm޼y6wlv뭷3h6~/8==vmڴ_zUWUVM)Cw7/^('uY;5k֬9w,RRR?lG FysapgMVVVߐ^xaW{7 N9唚5kPuY+;բE0?~|ǥC>w}wÆ {7+mH8Ps\pSO=O?tRRM7TNYfZ֭[KG/jժtIk׮4hУ>ڿo@bcUl5IIIÆ %Oz<+4h?ޡC nذaK.xƌիWSNj޽{m '|hbŊݺuk8 .0`@ t_GeQ|/W_{G! @񊉉INN`PޥC֨Q'ݺuԩSkСC*U|$33sŊACtʕsrr/g} Ds=~ D.>裗]vٰa>_5'''99E:uѣuևz_6nXixo_ ~^y啧rJ!ƍ[xqʕ[l٭[z}݌ Dt!:jnjnSm¸I&nco%IV z!@(@"111@鑕%\('E]#--qR^-ZhSRR$ (sK?gIPP * hB7zNر4аaÝNOII0Qh:ʸN8!K@r`7P * c@Srڷo߶m[y=zt} rP ͝={v$J|رlNN~2C SLʖ.ʖ{۷;vlf͂)'A믿J%L{zO9#8 /4i)2o޼.]/;]vʕa# :4)))J*U.\)@@KKK7oرc׿ho(ʲk֬Krrrj׮SODf6,t^ztHsGYiiӦ;{]i[ɸm۶]tQ0eڵEYNMM K/FmԨQG5yzۮAܵkfwy  3Ϝ9sf͚54h?;E)@a|WM޽Gޱ]N0qڵk??n(@@kXpAPVvMΝ`ʔ)>nӦMàz-V(I @tcEl@ Oà}jSv&M,]4//UW]nǎ ###111}^qv4P!%-bÔKz5k(.3fۯfZZti|>F埵e˖4@!2220?GEnn 8yW"sOݬ{&L5kvf}ᄇMIس/^y%Kv{뭷??O0!9`)@|H#]jUNNvsO:餋/瞋Ox替/=zx%mڴ)2!!! rssׯ_x/bժU>뮻n}FqqLS=l7 Ё?ss̙2eʲeˎ8;Sq!z)9?վm@sOJYy&gTrd%[lı#Fhm$=忯`YYYJ*RG) {XjՊ2333רQC(`KNN.b˭[ALLLեRNf͚EZA:u -J(aM6 62Ro/N (c+2'OU\M6ӧOZ.[, )TR mQZnQ5[A XtiSRR$먣 гgU9sqv$OК?Mh^Ćyyy uq=3A0cƌ]5kVvvvFg hN; ;w vfܸqaкu;J4yUV=sxȑ;m3|0d2AJ[n%.}G׭[>,ӧtQ&(@@lݺ57nXp֭[Azo/F֩s@駟׮];{wygo}֬Y;wnҤIRRRA-~-ĈV۷og̘.Xqcǎ=䙲B^W6yCͬtD k ]Y|!Y7oMz={ ?/NOO]v6m׿^uUժUdhBK٫ҳjUTV7k֬J*.4SRJZl#H&1 hY:u"y(y Ylllݺu3] }6ʜ̙sT93GN?s$ * P;O~/w@ D4Qa hz7^hQH/P(@~Eiyyy T4 * hBPʤz7P\rss m] 駟qbVV0֭[|||z2{Pllvu?";;ކEԫWoljիWu&$$qJJgᯛ6mV\ПD⠟qxUIIIxÆ F' 3h_Zl4G}HГIdJrrʪ*UGJE7n\`Arrr5 QnnnvvM6lؐ0G(@"8gmժU$(=bbb5j믿Fn|JFrba )@QuQtV>쓿 }Gq ДnݺmV(^a zq`7jHX(@e^jZhaÆ7fff {%$$$%%%'''&&y(@ALLLmիkPHlls4(@";;o N:9\<q(:tP(X)  hBP * hBP *8)(Xbb_% SV-y @T(@ 222"%r/,}R&,Y{!!T(K,9+Zn$PaU4ѵaIª1Cp]:uoV6mv{U]vٷ~_޳+)둜]?~d1cƴlG2>V6,yhJ1j{w܃>P`ugΜyyqXMt%''G6mڴmvWUz_Iy]Ep\,=x?6lyhJ1ꏯn|:RNue,w>VP * hC)3..]HHm`9~{%xb\_ ,P}$uq1yyy@ڵ ~#q{DN} 0Q @T(@ D4Q'DUݺu%1G:#&//@3Q @T(@ D4Q @T(@ D4Q @T(@ DE%_3ի׺u#<2&&Ffri?ڷoq)S̚5kÆ 5:>`tqM6}nٲiӦ;v<@&Nx⬬f͚va)))9 'x⡇ZremҤ̙3.\زe˻K."*/|G1";;{EYd֭?|C{ァM|:O9Y*l6`^x!//CVw}z1~T.nɒ%~oYj``ʷ~yO>oJgcuQ֯_gN8N:N.]a6|𫮺*+++:thl_(_Hw}Zpa+ᔢ|SWzO?ԢEQFo߾Ҷ{ᇃ+H/P3fq롇o͛7> O?t>}vq}ٹor)/rz\sok~0ەUR(F'p—_~|{:4hlҭ[7L|!+6777e`Ŋ7|󫯾ڬYUVWB)֮]{1̙3Zj&Mj۶m8=66裏 &~u=P]ܼy6llҤI8=!!SN N6?7x|IZ9묳&N8w1cm5jTr.TM77;3>}t!~cJ mkR>{Dff='6C )zZ`Ao; I8]\VVVp!v 뮻֭]zjժ]tђ%K&tqiii֭[ Z :M6+@SlM^{;6> ]w]|͑q@ѬYqE1vqƅEˎ UvUWU<{g HJJݻ7j 6u]vPq{wҥAЪUO>y;v?ҶAByҖsh<)ްaΝ;͙g?7o%r@T[ 4( ?ƍ9&L?~yNKLL,xW^y%wť :4{칫wY^n]ʳCbq1cƄ6k:01bQξs + DvթSPO?t.}פ][o^Y6mJ UW_}|p-#s'M{_|ŎgD;ձc0."X帋+-Z#GJ;P".~TRt SF֭[вM6a;yd/|iӧO:wqٳ׬Y#@"^ղqqqG#N4)&PxDҶq h3p| ]\^^7|NQFrrr':wqk֬YpaQ^:͛7/X$PlٲHܰaZ?KPΗ{oK..֯_iӦpJ ~!=$PJsMHKK5j(ek׮:"ݰaCW^Η rR T.| Ȑ;n P[.2_'KΗrR T.| -[D℄"~6o,u%K@E.| jժE⬬Z柛)'KΗ !]\!)III8==_ ɒ%vqzH@W{HhGF"q֭[#q?3=%@WI=$PѺC*@S &99wޒT(} '[o-[\..رc#%]-~qtArT..!!O>a;F #RBbӺuk&z꩝y'`III[$.2ݺu;S 2dȎ 233}04hP律ݻwxڵk|OMM}7*mv Pq[nQFA0}={O>$ P쌖+|4/&_/c@4f̘H\瞫SNGUjU颬2d_|_6@E?Ox"|!@Q[/?֭[k;~}tqUVW . ;uAV]\`_~%Krss6lعs߿4Kp{Q @T(@ D4Q @T(@ D4Q {L~$( 䖮T ZJ3[jH YM3k*]LQ 1IpBnhBkuuq+wpp{~g|g٤^?zS HJKKo ! V6lQ &p FХ-[M6B$ ^/b_tIݺukҤI͚5K.o]v 0`ƌQ?ONɒ%C9'''33s͚5s:tmۢF in!y@a@ky2e]a֭QTRw}w SOmҤ\вe˰{gB)))e hbaÆze˖?>ˬGƍnEzjҥUfT ]ZZډ'جY3@41z+6f̘"E¢E222%\R@Sk.S-^?ݻw)@41JOOرcժUWwM N0srr,԰ܹ KM7T@sirrƍ nݺO?ݨI&U^} ˋ/XJ&MP'mzʕ+W@+VxЅ_|yN^ Kzzz Xwe\6mĐYf[o߾x#&m*Uj֬i@Ԥa(hEE傟%/RFԩc#>ʊ_޲e!?l8h_A8%''w4GFbRJկ_߀N[l9se]VJ@|yE /X={ḷm۶GO:533sοK#O>O~/O㟚z1\wuG@b!lڴiffs=kΞ=E/a8W%''_tE?(QC?OOONIILG# mݺu˕+wTgqZZZҥJGpD8֯__De 8f˖-7oW_]T)@nV>¶sΦà!0K t ֭[.\wmգGrʅ#zׯ__Z+WUZްaCʕ/Қ5k+=^z̙k׮^zVN8ᄍ7#~m6mիs7|?ONN>sCW-z]JMMPB3}w^h۷o%oVO<~ϓO>uw\T9##cƌ˗/glѢE8{ai-mʕkժUp>k׮+V 62dH͚5/ݻwq={:t~ZlYVh?T^[oս{}>Cŋ,K.替>}ԨQcڵ?s\|nJf̘ѨQC<0|r)r޽;uxkvڴiO<ʕ+e]֪Uuօ-[vu(W^ه8o՗^z;0/BZZUW]UB~8sp-kݺun߾=Ι3iӦk׮?? –-[Å~׿ _I۶m4>Sݺu;;mԨQ_hL2GByժUgq֭[ChѢ;v 7?|k8lvկ_(mѢE _jjj̘1awƍڵ{EBzzѣ˕+vj۶~:xΝ;}W]uUٲeO/]vN7idǎ~W\Q^ƍ?|FFF" ᄚM}+Zٳ÷&O裏&=I&]8)7Qܹu{챼; IЫ[N<{}6l͛Ph߾}`FIwnF K,+T?1QRJ Xv!C~p٫Uv饗;餓B6lX"EuԩUƍ{シV'LgqY&%%駟N<ڹ Åk?Nk׮ Nv½T\'\jh3g&_|~SNwA7޽{wAGw\.]orr I&-^O1N>|xfdÆ W\qE߾} H<wQ|N8!q04?h.s{lO2 ]tiǎa(mӦM%UVJߴi|+MWT)l h3zyرy+ϟ?J*. ϛ5w}}M6ۏ>hݻw%K,VXr;(͚5{M-C1SN._|֭e˖_SSSösGp;|G9昤w w<1|%>>hРYf-]4(رcof@ݻwרQ# X\ݔ($=X~zܹsٲeguV͛O>o޼%K7|wGa[z?ެY+WN6m++X>_vmRӻ*T,]4ZbȐ!O=TF*U:;;;=== W~awwZʕ[nnq <8xףS~ĥxyWOɒ%BFFFT{FUV}8N:]v}g}hժՈ#kѷruջ̙p /0:oնok ٳ׮]ۻwE"=z6m?hѢW_}u~ Q/p ڵk4hcueѢEzkH>UZ5)wn);-EժU S3y8a{9Do4h_|qYg=SR{VXz]x>{5(SLE-ve˖^zsP&LڪUvڅx|/ > /r ܹsԩ 48ӎuGNK(ZhnݒrO>dϞ=*8M6Eoׯ_t+"r$ov9iҤٳgBϞ=)dɒ:__|q6lX*UfΜ0:>Fz 4wГ7xc .QNNN8]b|B7o|t=qϞ=ynܸqҥ?;-ݻ'''+ѣ?UɃ>=ܳm۶-Zt=:RbŤ_[I4öxpvmaCQ}*_Qrk.TraÆ [.e˖k֬4iRF_zKMMMNNԩQ4u 6=FNHGN˫F͚5z;vO "hq>}l޼9333mzzz"E: :cÑh?رȑ#Cy…Ikn4n8tС={ m۶tx̘1SNݺuի[n]tЫ'8kn+V<#WzG:t2dHb \`hk #0y(޵k׈#fΜw` ~W4iv}Dlڴ)*0ђ{+?Dƍ۱cGRnF?qİ=3#{Gg͚]b:u{ァvڐ\uֽڵk+ȑ#?ҥK,Yk_Dт phYŋ׬Y355aÆ5 F-ʗ\rI8v?nҤɦM.3~O>}ԨQ+W =)UTo &x㍣Gٳ៵UV7Y|y#i^wf- Š+?\M74xwðZhQ0):uӧO͗,Yrb0G]w_jnݺ=W^yiӦN+axpc+4G/ł5k9]ׄ z[neʔ G6o޼lٲs>ëV?ywhlr֬Yk֬ ;-Lu]=@plܸW^]vׯ_>ŊPB ?}w`oW^)}> ƍwM7p|WѺTV]f͡?eʔC]>s9nvw9u.]pĜ}'tҳ> cǎ -ZZYYYk׾ u9s%K|mйsr>Gdɒjfff &Oxcޞ={ϟ7w}OiҤ@| ,[,+++)qիW1b!g?AҦO /,Z(9Ν;k֬ټy믿bŊn6a„aÆ޽~&  hb! hb! hb! hb! hb! hb! hb! hb! hb! hb! hb! hb! hb! hb! hb! hb! hb! hb! hb! hb! hb! hb! hb! hb! hb! hb! hb! hb! hb! hb! hb! hb! hb! hb! hb! hb! hb! hb! hb! hb! hb! hb! hb! hb! hb! hb! hb! hb! hb! hb! JP endstream endobj 73 0 obj <> stream xڭˎ$9WqPTEp[5EBv8Y]hY~Mm2SכwQҰ"*uQxo' "Z 7~L; 2B١C@-Npm~}AoADּ~`q{,%8)NR[/ӷ6iֽI/yC:̰ÌlOQɄ4\N:@:Ԥu:>w/:ږ4cfZUOvx^DYGʺc3k<.j>'w%΋R s%uMA,}x? p@!np+Vդxxe|M iPItΦ̔滋2>SZ1+NPq(&Czqkv'!R,zc񃓡Xiti  뚽ȂD髫5E$ 2hKb\,Ȓ+Ӻd7< g^iF҉d,aNojfj ޭzH.|OJ`wNZ,$Và-:s;l0!͐f75H< 9c3EnC0zH!:,jFc-"$Ũ6.F7ߐ)(Ƕ-= -%0@1qD>d:8 4=K2X$k&UylQ+`6`LAMUL zWtanXF>x!U/634Ңr;D[~31bhE i)Dg9j#'9i=ےoO'Hǂ;w!"Nqȓ,cm ] Z@ۘQGmf>ŢΙmqO;{ʉ tr6.g3=d(9KY~kȄWkVl^\9d޵x2N}fCyY Uﲿ'򊔽Rm P)ήOX_,@ږnRFOf4w5{vs4fvUzFl,lMKP'/֞{C5]qSG=пgv/gN# ;%olݛnyZD!QT\E30,IK]peCOJ:gE^9@Y!C?kB*K[fd k`nM LnJ฾5HJL=u"J煥K3 fuNFUKB'I *0a-ad1aQ%CYXb ٪r0\fѤd!OOy^D, 5iQ mAg+H]\{8=SO E.] 2QxleUaJh]Iٷj\ͬDgاF!{3aG71.U|d&XiwI']|D>s=3JI΃ Br+˚澌ǔeŚ& ^Q] ]Qiy>p%iRH3•ꙷ=^Z/Oڇ -XHM3V텟ckUƆ{8|R9i~uHRd6UKrĆJ/%1Ԗa]ͳz⾆[gr{鶗FK[P'9klj˳i*NwJ8> })ȇa-k֦Hmdh#;u,-{{7#o`t7j< wp>ݹӴܟl|˛]؅ֱ{@'ī)X1UJ8Kb̪Z`a6Wj fP岥!^z낁8 E>4x>!6w?k RXdlu_t$&pY_q1&B :NE,ZN5?y] endstream endobj 76 0 obj <>/Filter/FlateDecode/Length 4539>> stream x1 ð'ݱQ$E____________________%___________ ____________W____________________%___________ ____________W____________________%___________ ____________W____________________%___________ ____________W____________________%___________ ____________W____________________%___________ ____________W____________________%___________ ____________W____________________%___________ ____________W______________F=̚ endstream endobj 77 0 obj <>/Filter/FlateDecode/Length 69774>> stream xXWaP{K{4Kb,XРE2ywaYU}:;;;s9ٳgcbb,H@L4$H@L4$H@L4$H@L4$H@L4$H@L4$H@L4$H@L4$H@L4$H@L4$H@L4$H@L4$H@L4$H@LŠcڵ'NlѢ1A6rȹsZ[[:O>02Rn޼`c :}tɒ%!gΜ (Zhc&M4w\)x]vJrssLZj̘1J,QDʕ &M<\ׯO.… 0 h+ӧ_|yǎ/_4sXgϞݺuO۷H"HxUÇSNFѣ$\d!CLH@\e̘o߾JEfW\BBB޽{iooo)(?CSLٴiS\ t۷oW^D ɗg˲e|988_% ,GEH]vIuKٲeN-VΜ9Wt ߿dɒG V|ѪU4i1cF(_{n޼={9sTRիǏK./+W8::Z"hJ:~xxxxÆ gΜ:ujzjL)yLL Q 0uԟ~I}YZ7={VV3gΨ[J*uQ++~/A֭4hбcGB|* 1b |ҦMk3fJ*ɓ @6k,фT'O0f$bXZZo޼vaDr0p@|Boԩ \k־~:1Av&M{PJHHHÆ ) hRlٲi_x zI߿O(O͛7M6=w*4@ʥ3Q4C 6**!!!7o]v֬YՍ.\8xgϜW/_>Ow̙g>y$222cƌ PG8qℳs9߱cNj/J*3ӊ9uԕ+W¤ή*U.؝ EŊ-r'O޽{7mڴ Z{ruÇJٳg/RHōӧO/_\reeDDeK,Y7o秮]&M7o8::̙Sl`K.ą=ѣ7nx[|y)[b( ny}=z$+DFرcϟ,cAJ͚5et|NxMbŊpZr r[2eϖ.]Z:G8tt:uh'm$ǔ7|#6n/w{{{Rsssj@@\ B.߻wO?n_T]!#lݺBGڵkڗ%KL#W\Yv9sWCxxʕ+7o͛}) Ç8_s˖-.\h/_4iҢE=z$/ӥKişy666}ŋk֬vd%si御):t }||dܗ/.A@$rjK/pVҥ=zHt6ʭU_J?+W[noJ@'U˗/*1T,#Z6 ۷KرCWK})3jԨՅ@rAkƍڗZo7nxyy]̙3~M6m߾]Q;vԨQJjIkݺu?޻wo|[hԩSO6sJӳ_~^9rn*;h?sUVy{{L4hФI[ȿ_%wjruumݺ:oZk̘1^lҥK㏉nٳg<844z*UHL={f͚ѣG?}tʕ]8tPرcڧ~Iyذa&LЏk׮|9i˖-_S>qJ>x ŋ矔O^Kg5 ^ZԤRs6jJNgϞ\rI߫W˗/Kmaɣ=rڴiL"~MuرB >ڲ\WK᧥ɻ#F8qbDDDͥ J*enn.tqI̙|3g꧹#m'Tz[-ݻr;;;DEEIp :u1}(iq%)3 Do۶m22Fg(P@b"#H* 4-[HΘ1%K|pB oPPG[ny;͚5wvi5JvUz.P[:u;99Wsit!ݻwwշo_4}yxi&sU\>3fHG9n!!!r"IVN-Tv۵]J7VժU?\իW 4o}ʔ)HF,uӧl٢tuuOi^zƍ2DM6ui rp9vO//%J>Yu &L}z)˕GZldɒo߾|R޵kWm^Rv(kܹ֭[,/'AرCe4e|^mv޼y#F)$JӦM+unѢ ).oLܼy?44TX3J9sӧO.]$ߗx*;HA%_iӦ)W|PCr+I঻HʐKnԨ4 tI ,~8ofSܾ}{ӦMtlll*T ݺuһhobp…gϞi{EݺuvQR%҅pUezYJ۷ K+Hv-UT]U9ro! P"oiiY@"2ڵK"PH Jƈ4;Ν\U|ymݼySٮrWRB.Juԑ˗?XKߔ~תU+Zұ{)x9#dϞ]kԿ<<"qVpyaɒ% *iӦ 8sC^9~֬Yݫn7 +_uԩSG2e[%K,7wڣɅK7o.X39ȑ#e*U֭J͌_r姟~ҟ. "uٲe:#;K=eNڤ hJGi-\T7z)_:Et.x['OҮ5,=S 2%jSҜ9s䰛6mRVԑK<۵k'G9WZfJr *@TgϞUve9~؟>>]pŖ.]6MX dǂEz5 ?M6oի2TRVڽ{wWv(QDB&Nhc֜3<o=UAO<<Ln8uKLLs9s۷O sknNhPk a E 8ĉң:>POW_.]@v1#5;wR0ajFٳ݉J7o޼Ǐ[nȑگ$>چ~ӧusuem4i:2eKq=FR[rTTÇuvPӧO#t3gO\S'''8 .o/ͥ7y8W'Oyi%ѕu[+kI!ūW;VJFvt@jԨ?5kjGB ! رc4 ,Xlf͚7NԫWO9qℇ'}ҤIc`]T-{{{<~J*S~}KsZaaaeyTflLvիk׹l۶m~y[[۾}/l٢*ڵkOj|1Bx.]:J:tPX1=|p}O'u.P #FgϞ-={ƗPm]+=ToGJAEEuEXX'//Θ=>U hRVVZnأGe_q2elٲN;5/::Z;;իWYFݒgf͊PεxbJ ڵ3ȑ9((HyƹAyʕ+[ʃĒXFP\rioooi5j%KJgP;4PІbРAEV>}zӦM?o߾gV7o,W\~Fe`)?zhƬ\7oo]vM]/{Ɵ=ߍ!V]$%@߽{w.]V\lyUFEs,Ps|k&Dze˔ӧOݻ6e7b&Y,vdJ{VTO*3}W̙3)Ν;_hvC,1 dgg/_>52o߾;wTo{nf4h0sܹsdϞ}ܹj믿d``sn* 9swd`B7n\ѢE<^  hR?͛~!{!!!{-am7m4ycǎhϯL2F:p/,_\}y[6iD}WRJ}W>vy_)[lq~<k=tss3\D:~)^lKM7n*]RIl ,,,6l 1ю۷ݻ_4hP:txEIk޼yzf͚]Rـ+WN2ڵk;vЃAxȐ!;vȓ'ϖ-[>I5t_ dݻ 0gu۷oz%K| }uuuҥYN)2f̘񘘘e˖5-[O|a\|҅@raAH}VWx4o޼q/^7n>++PqK\ hR4ggg-Ǐ+U0f̘C~ T\B )S(ϟ׭[))~ܹs|۷q~<Y˗/RH>QDG1bĈ(www///;;O d$'N}LbDDD޽Νqb-[6i}*UJ}׮]WXlYeEu3O~qBo hR4kkk-uo?6m(Y|}B; Ky0ڵk͒(^Z~13+K.J*)̙3O>mѣZXX̝;wBm?^]4lll tڵݻk+2d˗/?ZWtL2<>TڴiS勮5joOjJ.h=٢}'̆S͛7TF,XP)GDD̜9۷[lI>ăרQCrϞ= ~ƍ}}]|YPsVaaaFM>}z+s=u-rʩu~#G>f' ·eʔi۶mQzJƑShQi5:u:v… ̘1ŋJu>.ÁF~pРA/ۅ@CСC:[ʖ-9TlѢEj5}D R_.X`ڵ_nٲK jnݺ?r`gg%+_+V'L`ܹ̓ӳg… '2K.ͭ0Zh}`y͛J9gΜ[>,EzŤIԗ׮]3Q9|}}|y *:uL{̞=rԨQ|j֭s-RH[ba 0]$;$B$"eM,YԪU˘S:CtyAAAjt+kN==fb %ܹ?s(^}r`"sjƍcǎ5|(eE778~N*ZNpsɒ%ԩ|ٳGMOVVV&o߾ݶm[|JPOm^-}I&ukPC}jy͚5w6˗etw]~Kx[>2 0~& 4_ׯ_GNTL~/_?`eTR)IgūW*WAZmj~J2f;/_\)8;;2𑣣 xb%ZBΝ;krwwϓ'r˖-37n|ԩ3f$%>{ly^:5A|5Fg|u̜9Ә*TPM440hkk.?~w'L;wn٧xʢۉ{n#:\\\]y<}??:17[m/ϮJjQhݺ3g;իWԩccc3bĈD6k֬CL҂ .g_Nх@rD/Ώ ϥ}MuE;OGhhgԗ&isLMPh޲eݠ Th'/X@;Xݻ3]dŊΆw(>}tfx,^A<P})ҥK޽~~~_!C wׯ2mnѣJ/ h-m|i:qR }[=۷ϥK0arܸq˦+6m$ԩS{yyɟf2P۷OgPhWRj֬-iCϞ=۶m[o'c~]ug91cƌҥO]znݺYЖPhWo߾5vdƥ;?{,O~9[[m۶+WN8s޾}]ӦM?v׬Y3x`i'OjjΜ9nf8p -Xpׯ_<ӧϟsuҥK۷o_dCLLtc޻wz/^夻w=Bws5rYfg|EU}r!b'JWQ^=9 tR :)G9R})=CÃo)?7Θ1~gef jA֔)St~~~z3]n44T|:J*/PtK5얖I +E.X*Uڷo/W %OH򋁈_rHlQ2gά_OPډޯ^u :rir Kʕ/Jy)uرcٳO<)ٷoUVI?>} F(k<|PnerKx&L(QMb֭R+W*UJ;wvu:uHQf͚_}>r PWMWByM]GBm|~cHpHH@:nڴiʔ)/5k~~M>>>jҲAׯoժUXfͶmS'NԮp//{ݿXbJٳg={ Tw;w\rܴǼ}v׮] /_\tiMc1/_Nl ܾ}Ν;дiSs\bdɀ,YHO>dm,6idޕ+W޽{;wnC#Ge^pŊa믿.PoXXW\7o^%զMSNe͚Ue˖GΓ'Ͼ}֮]6mZWWW[[[9ݻ=<<>-[uXN:"m:**ڵkǎ;pTӧEΛ?I3cZz|Vpyw#IrM=\{ɒ%nݪNՔ?<~x„ /ZH]Ç޻vJ&-cƌ͛7/X`ذaڹ/^&ZD>.]KgJJߓZYYYIHKI UOҀ$rJ=jnݮ^nqV~t {{{rR!_|1 cGYlVrʉX@Ŧ8_.}LZANΝ;KgI(Z-[[)?~\ZŋzNWHetHѣ4Ŀ9QF3gN+V;V;oZx˖-ґ-j}ԩS%niҬW6tt<%)JCKmF&2X -]E@⟔{4T^JTeI<(aÆ7nA]]^fΜYOAڵk|i!ǔxjg FJ*1IT/Mt 2$82~݅@c=c%K|O٥MV?De˖m֬)[[[ի:t('e̘Æ ܹsqP)Sٳggzxx].yĈé&O/6o|wN'۝:uZt{&<<|޼y?)ɓ'G^vDtBBB$Vr ˕*yH߿_)9|?ӧOի(GGۏ3Ƙg2:bjԨСC˔) g˞?ޥKlM%J>OAj~x+VP҈*??Mjߍ7?Ͽ۷ޥK ,֭[Ç߼yޮDU˗r9ݥZ:tX~}`$[6lrQF)kA2L9ϑ#رcuVH{sN֬YӦMwTRI +q֜8qbl߾}ҟՕ`VRE]jUet] $ )SRN-W]|#΃@-0[…ntS^lߴԱcGEի}>ٳ֭DLnlٲInZz}۷kժ}ޣVΜ9eDԭ[Wn2"9:\]]}}}O'VB'!U"P2`*UץB ! I& 4e˖^^^m%4 Zs炂>xPY-[L  gϞ=x`^>H@$… nnnk&o^J4`R$ 8ݫWUV988TV{ʕ[xСC1cƤN3(ABߞ={Zj5|p>  ׯ?^~6Vٲew_ÇiS6lA 2xzzZX|j~rǎ/^Y|ZF" NNNK, ntqqٳgO>jIg̘1tHu7o-[xūW,Y&okѢE~?j;wo߾qѣ[ ]t||111D4$H@L4$H@L4$H@L4$H@L4$H@L4$H@L4$H@L4$H@L4$H@L4$H@L4$H@L4$H@L4$H@L4$H@L4$H@L4$H@L4$H@L4$H@L4$H@L4$H@L4$H@L4$H@L4$H@L4$H@L4$H@L4$L*88xJƆݻw*jժO>ņ4LkM4!H6oܸq{,0 ` Z޼ys޼y .(… [Yq\vM]VKHƠ7o… M6)f͚`$m~,b I hI hI hI hI!lllﯖ H@@ӧOOKpL)ZLLLttI(b[XXȟ# EDD%πuXR  hHwΝ{s|J(ULLLxxXvvvd>x|.>Ξ8 v-tV*)\R sNdd$*ՕICRw}$D BD̀H)>|;EXR&>H#""^ŒQ 9r  "DEE/SJ+W.++CG۷Õ2ddXZZD-AAsssπ24d9p$R4iҤJdhQ_~$_s񃻆w!Ntpp @|B:H@O* bbb#s#=EEE/SNMLhg@]%8|ڗ00@g` HEv$$7nH`!~ 0gBH@@RY[unRS- awg-#G5jR>}tɒ%u>dɒΝ;k\|yKF 2D"ݵk׽{G3gNʔ)s *UTk׮˗O+V~|J9]t4.dR 3@J۽GC0ސ!C֭;z耀w *tׯgϞyʕ+-ڲeK``o?Ǝl|ҥKw~|͓#}WTiȑ}D\92uTOOOٳgh"UTVv[ٲe/_|Ғ4i䈕={RJkccSx`ʖ-Zں|򖧧g׮]{ ?ӤIBiP# |J 2Cܹ7.\X,R¤C >*T/^,]vݽ{E{!C,Z(**J۶m߿عsY!4ޗ!>![[[;XZM7.][}BLLÇu>;wVZ)}]tI7n,QDbh,/*Z|&ZV)wABLL̴iwعsgpppN54$ ::R& dA-PD wwwbŊOf+++D` hug^{w'J$_qnGEҥ+@섅=-mkkKL9r);;P)\zU .嘘UGǫW,[6m`={F#"Ǹ>E*[^~]օ ~'{{{Gt>kx~7oriC)?~F...Ϟ=R TsRkҦP;E|@}R ZeN>gϞ~}Ǐϕ+üy֭[wYedd'Orppoܸ?`MkeU !*WZn}С{֮][?K%8 666p@ugÆ R~AΝ2eZfͷ~;feKHHիk֬YX1y)p߼y3wacbbf̘ܱcرcӧOo&ٳgoڴu7onڭZ5j۷K VגH@ׂ ,Yry֢E=z899yW^˕+gaU v=bĈG .X`'NXByM2E7rUr7Wovݺur.;;;N&TUʙ2eR~w];DFFՕ0A E˗/;vl>4$ <<|ʕJW^ITX1G!#YX;9?)ѤIҤIӭ[7Bs3!ҥKL2 |̀RUV͚5+mڴ5jӦիW[h8l04℄tYyݻl̐!@:uj&Lhժ h ű8p3glll*V8hРU|FdYf%Jx+VL@;ȑ(  0 ` hHZ& F" ,\0qx_$dɒ')k@Lx~p"ݑ̫k$ ёVYV `(;Wr"s5&A`$&JB>s[71Dގ!ܞ{HcbbEb I0zb<' &98D" =oMCcu3LH3 a֩" )X=qRgg.Gp4>G;V)YZ@5lذDGG |̀R͛7 3(M4y䉺}+V 28rHvX^ /ʞ={grʅ |+LP8mz NXxsJyCm*Kbn-3s֭[y泪9r䠁T$(zzǏlK-d!&&f֭ǎ{adddLʖ-ۡCf͚$44tΜ97ntҫW~M6M)O\L.۵kSӧO]v߾}wyKZڷo_Dҥ^^^ 5jlܲe7 (((cƌ*Uٳg5lѢņ ={vRBgmmaȑY𑑀899͘1C) h;<=а) Vк`pCpCZ\(ZBUh+FJ@!B 2u yfΜ=3P͛7^Nnv-\;[}N0!###)|G138cĈKGydO?y_8K/5k, _O>9q> xx!_'M<$ ;3O>9~'O>\z;vg̘:z;ׯ?~'xE8}Ȓ%K,7dȐ;3?v_^XX5jT֭ߴƍ뮻.bﱭB [ْ%K>_M4yի vҥwއz諯3\{??묳ƍi}㎻kn=H֭~ . Pp&oN;7|󫯾 F 0/>輼Oھ}իW?sOz}zk;pEē?[ڭ[X,v]wuQÇZRn_WPh"LCɗ^ziX{k׮o={[ogF{VN:餱cN6cǎ߼ˏ=؇~xYg'v֕+W;//Yfƍϡ&My睃>۷oO^>}Cۃǧ{y?dȐ ?h;{mEh̙_~:ڶm?s ~ :td O?t]v 7{ ⋠ ֭qvy|w};Hܺn>s]~ 7ֱX즛nJFa=o޼iӦUѫW/o,b>k֬?|ʕVh֬Y=rrr? _'Q~~1cN\Xzu&M_Xgw./6U8+8Cah+WYYYa_~uI|'?yd v!8l|XsC0`W^|5jԥ^Ԝx-ZTaÆZZA /N;픸n 'V1?ss7B@O:O>EEE{;w|Wr-7d̘1\s͏?Yhфrv#<i@յ2 oVZ}w~Fv+..ׇrHݺuW^駟~ƍK\vqu7M74sڴi%5ח+|C='շo_z޽{Ƿ6is^xaΜ9۷O|znnnRjﮭ;,>w֭YfGq޼yySK>6q>}lPG~|,Xg|@|c٥KYf'dɒ{.qi* WnjZ25k `KNN>SǍOG tMÆ ~cǎCׯپz~&ȁzZt{7l~l-++K*=vĭ'NСC.]@3fH|CoÇO>}ڵOZlC]tEGyd,va*F[C;ӝwٯ_]\\wq #GjVweȐ!ٳO=w+ug~g;?pD;WHx[VQZЭ[#8" (--4iRϞ=6m}եծӍbG/?ŋWZW_~Bfӧq~ /p7o޼W`#>=zp&LH ^iӦ<W\quM[" 4hw糛QpxGu}C{&Εή ӺCݺupjIK|<{3-[5Dʞ}٧z*x3mb1۸ m >;v={viiQGUi/ۂҲ?'A > ݪ9@r)azWT1}~ꩧ˫:>{?ӦMׯʕ+cM7E}衇ƗW~. xx}W͚͛5c=*#ڵ|T~+s=777A'pVMP '|rsuЦMٳgHTR{eOwW8iZd7 ٚTK/gu6I CG}c=֧OɉLXZZ'|7idٲeA}uVW ~[ av#ׯ_RD{nɧzjVVVMP.n? ަr^{{m۶qޑGBn` g/_{&n]`A.**Jk}-[\瀽{nРʕ+I3+VlB"?W\.ւcǎK*o[XoXu%%% 5jIցN:)^ϟwoF"ԩSs^yy=Ccm۶mAJM5F~aѼys'&O4)q?p&I3 oXTTf͚8Ǜ5kzx=u+VuG=:u]EEEǻvvۅ]v٥]v&MzWf̘_7???//oCwLKK x1|}]pSNM\c4޽{糑o~??qp̙ o98x=8pu]gy7UQ?o ~/>ꨣ6UKz "իW |3|ǒoHxGnР_A͙3_ F>ݻ7k~>}ӦM0`@-.]:iҤG}4x;wBu2dHxN .]<%ec0tuذa<\СC)Sns9*= v>so-99y8FehO+'tUhVFKgyf zر&LH*%kև`kyGvC}uiݺuNNo~ƍ[njs95 ƇN;X,6\Ry :/p՛o999ʕ+W93.0{?>Ӄ4hG[np^zq^zk έm۶]vYg뮻V{챏?M7tׯ_?8>4iRF5Iw'AzMj/s ޗOkoUV.NaaϬPKxla)))pkV3x'oŠiii.6;u6{1nܸp-iijt\۽{? >K#G;wܴiӂ>,\Pwaݽ{t= 4@%srrl9\R_ݧk: @$D)+KZQtP%hʔ$}OMR2ŋbEEEHNN4hUTT4e};33S`6Ux@l.-b@@-6k֬3<3//=zPh^C 5kVAAm/k5jxmeHj4bv[vׯ?lذ#8"ZtnS]րZl;wnԯ2mڴI&=CN6ՙ˯o~-ZtgwqZAu 6Uevv~vSN9o 62iIo یҳ:k̙ZT&9%i@Ԋ-ZԲeM>BIIIZfK 3ϸ4l24le}Ѹqxc=G F^y啇~__dIÆ u6hР}VٳgO8qҤI<֢hߞ{7xyXg;v|rʶm8pСu~~~ϟW.;u Gu̙3{͛=z8S>0h6 4[';~T}Y[SRaX_詬 _3lmN;.sq_wuСC1cƜviÛn3ΈotV.o'naN>?gΜYVV⢰x3V_Epꩧv1??N:Uwy(f[c jzXݤKf] & 2k׮)))ӧO5jTaaȑ#~m^^'|rozz{G:uʧ??x;.===>lٲw֭[)W\T H 555۷EՖCJRv>umڴi7|޺dɒ:u]tѠAZl1curK/4(!C\yǏ_xq|z_կ~5y '0cƌą;n)S dMj{Ś5kt_3`kZbŲe?~Ìc…|ZVV|{)ҬYVZխ[Wu_grA1eʔ?^u{/5HX=zRCodk^^Ɵ54@%RSS5k5E]sf "! h"a hJ;a @U*/^5/= )++LJj@P+s=zj$JI? Nؐ||4Rƍ75;;[ QrrrFFFQQQpuP CxMW^.(( T(h ?8l46!1G+..^x *Gׯ-?%8 YYYdmժUϯ_~ݺuҬr˶dsԟC MHNNu],X\\\>2N:@ *|L{u|f@ٳ}@v/"O<Ĭ,=j0^dI:@]wUY-uiݺuaaaAAUի_~ff_\/aÆZC_dkܸq,+++/ '"%%GA PK.D_T}&DB @$Db+W  #@ *QXXxUW]'Ua "! h"! h"! h"! h"! iZPʴc9&^k@IR*Av@uYH%8r}º@k/%şFXwدGZzTee_0v`RJ @$DB @$ D( vU8ih BCz\ba "! h"! h"uo/4wOy:4_<4ƻp<;{wų@ 44@% >}a3,= 3l9Gg|>s>PM*2uiXQVff0=}_=Df@ 4@ 4@4-*[mz6E U?XLئXH PԔ;5HOO;栮P] 4TllGk$@T9zmR`ݬF7gk$@T^0;k)"k@ 4@ 4@4-`W :k׮-z;'@7o ̀ h"! h"&<ⵆT 4 … ,))Yg$;;[mںw^b^2k@ 4@% {챰>3݇bZCD hj{7lڴ$@T[-l޼$@ 4@ 4H]vm֮][~YYYzPhJk.^k@ r.{3N?&?^(;X2HHXUu&͓SR*(+-Ynwh0ZoV8`vv@M zU [^U`,1 ,@$DB @$DB @$Ҵ?Mp~kXhjwjZxvv@e -??E'O ejzv^ZC -77wѢEZINIд;6 -77^zV5ꫯ~휜N7F]_@ d Z,77wv:ꨣj t ]8jj'5ȓcqzlеլY̙s*]VǏ0 q7Y|y|3xǴ ]+b &n=zH?^XXcgto~7W^yerrr^bŊ8qb8t еRnnnwygXhB]<{WΝujhcl е P2ъ-l#е*Zc _熇7oK&t> &̚5?_rU2225k֣G?~[Vڵk}ۯM6UV~~1cN_^I&to~;l{wĈ2!L:O>EEE{;w|Wa{jM3f5\?G-Z4i#dee^f'ʃ>Ou֬Yn޼yy7!裏?'ӧMIIׯ_՟r饗E&MtRRR2k֬`d„ K,yի$''px @ tg̘Ç>}ڵk'lٲ*,((:u!뮻Vdvi;S_~)))1c\qe]V O9Jw7o^NNZfz{@u r7.Yd]w:{ /s?}w-T嘓'O^vu& 4xڵkIOO[jէOcǞ~zh۸ q_}bݪ36Kw^֔ .`AWǏ8餓u&ӟ5 ȑ#@n\lY-$}_~/^jժ*?T˖-{嗏;vءZ/W_}u@O>;J:Zeee>SO=5}lҤI3226);;cǎguQ.TA RR܈Jz'>wr߾O>$۴i3{XxK**)a}UNǬT= tT.n-uևr^{{m۶qޑGYţ[ S=X,bQ@GbԩW^#Fo'5(sss6Fld_~͛7w(E Łs=wԩ5~Fs/nhܹs̙?$+VE޽7OqqqIp2=P߾}+t 7E^4hu[>ޥߪ=u 5x,n~fff3x Hn:,O}MVţ?>ڿM;gY|ر&LH*_ ku H{^z_~yF[Ν{M7=5k֔mZL0y7|XN>:|ߎ9rkf'D?|09oܹsӦM >E%u9明;nzҥ+Y&Կ뮻%6;tTƌSn;l͚5o:;t9++k̙A]TTtҠ5j:tƀ{aÂs].Czz#j :*)))G8pرc_| ֯_uoNNf͚QFM2嫯ZjURZ 2iҤ=ܳk׮=P?pիWp2SNoRSS۴iӳgj喀@Tg} mSr_^l\ /=\X7o|t9W`-.///avv$rssʗi ؚ?(wB`#N<člbZ/$*,,dIII5QIIIVN8j/KpDCP @$Dl9:uj߾>P]vƌa}dee @U @$̀f}>%EI MJݧ F 5]K POʌn} @$DB @$ PX,:5=)9YKB PTwʰ lMX4XLWg h"! h"! h"sm_ߐ!;fggkFXH%8*lT2 0-ol qy8|$2H P JRR*()Kz%$lf!@ Kp\yߕ|jyqGK,%8HHDL_?.LKx$;[ * f h"! h"a hJ->(/90#+=YOB P%1M.4[ mnۢ>P}ųa=yO>Y6DB @$DB @$D"M %???^lH$.X,9,@$̀DJJJ|y*@T2-m2H%8{{l ,vۭqM>4@%JKK}ݰn֬YjjT5HH ![NϞ=۷o:k׮/O0HHHiedd <8^k@ *.@uYHHX>YuuwM-L h"! րDYYٷZ;}H PW<9/@5Tpli@ 5 n`X,c5LHHD5S^[u@5T][? f@T2-GZCH Pf-@uYHHX-Ogj/'u@FV5=4hJ>zk@YHH@퓷8ݡ-;;[؊P֭O87%-CO(bೋLi12hT@hB @BHT-mtLk `[سq)HD[$@ƀ(CRRJmcI PF%ީ;YYYWMӧO|͛7 z2p~iժUA=a„pb4`@Tׯ?jԨ6蛟RS@\e͋sn%qq !|Zj=ZT9Vg?~9s_uڵM4ܹsVVI'+׭[og[6rժUcǎ:u_aÆGqE]t18VXj]qSD vem2uSN9%// <#۷=zau_~ڠAmȱc2dÆ )˗/_ã>PLbb4+ؕ C=K>&MdddYf… ~?{]N0SNي6\sMX4l͝3gΪU)Ǐ^~ZjwV^]bA+'k@oXn6s1Y 6cƌM6{޼y-Z(eUk׮=#jԨ#n:e\x{g攔83X~ԨQg}vrG;v_y_ z衇ʻG曝@Ik֬ǹuy뭷z +bҤIqƽ&PM7ok4799K/1cFPǥM6o2e#<zٲe}׿t'x㍵k׎s#-Յxz]tq@ $HhZ`7|bŊk.Z( G1yv 2$op 6lׯߟۯ\[u94+Xm@Ϙ1[n8pgYb .rn.H]g}vƌEǎyE9r?O_|W_ݤIXazzzI"HvΝwqAGvM׫' ĉ 6]m^^^NNNa]Zj5wܠXb HK6ZC5064hw{۲eˣ>zkӦ{ݠA>c-:y睓'O_ꮻ1cƜs9;GѨKJJڽF>zM:5>oرceRRR_̙3o_=O:餇~x…AݥK9s~ꩧx:ts]`AX4m1*%zO?tX\xSN2}.w_mFK/5h Q:xm֬YO<vڵk\K?<(Ҷqp駟O.i8֤Im֬YGmݺu8~&''2㎻ ֭[z8viκ[¢gϞuqL $z-[ 3fdee̴i\[ڵÇ:6yʵSLy̠Ǎ7~j :lWa}i}3R ׻w/(Zj]{Ջ͝?SOo7nܘZsxׯ_Տ??8|#Go}k̡ټy'&edd ĩSN+@o?|=P4>|=Ӿ}ƍo_~ /[Hw@fɅtAz+@qƅF"=ؕ ʐlٲn֬YJJC P^{-;<4@<@hB @Bx!ρضm[}ٴi̙3Cxp4\l}ͮɜ :rEPŋu5 Nƀ !$04@֯_WNKK بСH$KE6u"EhT0 ! !$@ZP￿N8AgJ'([ LD"P:CpheF999a'@!''묬t=!8H4 aݻmVr6m_u^233  !$@hB @BHT-(BڥKX!qu,@?CphB @B ovXqC'1 @rssx㍰ԩ N !$@hB @BHT-`w_lT9ٛ7}򗰞L=3;4JhB @B iY.'4@  @BHCpeXݲf--vz$7e޼i? ιx.?g0=hego24 ! !$B VINܿP4PլYHs$ !$04@S"'4@Yʴs>/fB;4\ @BHCp!//oanߥx!@\e{~aݺE4@ @BXbU3j,.2!h@)G/sh4]0 ! !$@ZPط?c5\GIfD4\eHOKcIPF{!$@ƀ(G{=/8kFzC}Wr6mzYz;VӦM5J0 ! !$@ZTN˖-+vznn#T6h:_8@ecB @B M6'4@:,}(/*wy7*A TR͚5+vz Trh2X"4i9q@!;;{ĉa=pL=H4 ! !ӨQ#+*ڴiSڵ:8 'w@f4:T9y9dIX}픴 =㑚ۓ;H4 ! !$!9%j $ I)7@XR.^YHD yL|v)s[ .qV4=pB@B 9?WX7pTrjC Ph4%ZCdB @B wL(eg^`{@gRF"-$@hCwNVVիcSO CwӧO/>|qeddT-YsRڻ;RT9s*/XFZTU;u,z1p~iժUA=a„pb4Fi)WFK_Q:@(B@BHCp!!'TOKJJxK3{ϙ3端_׮]ޤIΝ;geetIU"ZjرcN_[aÆGqE]t189L"gLOū֕0cqID"%.ԩSO9唼*#o~чvXeދc2dÆ )˗/_ã>XT~-.\c KC>I&k֬YpLfϞݵk &r)v/暰hذA;gΜUVSƏw߽˵jڊ5+Yzur.I]X,6s)Y 6cƌM6{޼y-Z;{5NN3'srrƎ{o޼_נAz衭Xѣof Pd-(ɭڭ[֭[?cEܹ󫯾 63E:uz޽{s<+4iR8eܸqo T8w@y毽ZIs/3f\i;hݺOoرceRRR*~ ¢iӦ:P{ /|ᇫL>N+v?HKK2^2%G&ZC┬駟O.iʿ#rKXٳN::$5%>IR-[Ō3JZfڴiGLsm9}ܸqǏV0СCqx{Uֵ^[^~O=Tvƍ^>rСC5:{m]NSN1~ x衇7n~//_^x9sԨQ^zիW2d\P%=zk׮e˖+W vƍ޽{x7;f͚{o~~ƍ}" o>33 +Wň#*C\uU-[ --oh(Snn~;vPوQJx]w]%1bu]Ww駟WZ=kʺV^}5,^xҥs̴>طo8>3f̷~NW_ީS>xv;~NN.~<3k޼SO=j_~%_y 饗.[줓N??  r5jԥK#FȈM5k_^ׯ߽{믿>[n0%)))XM7ԣG`n%̮]kn?֭[wWD;.L{פI7nܴi.¿ziiiírJMr[nW]uU)>1c{y)QGu!TU.~|p("t[lYNBM6tP}r.mv+2媫z뭷~mV_ASRR/Җ/_ާOsM*6mtgZQ ʐ7?/bukN>/88 rN'aٳg-^~N8ᢋ.j޼y#`S>쳇~^[lYzzA4dȐc9&5k<Gjݺe]?w98x`_~y֬YM6 NLNO>_{cCvW7Ħ<^ ?A[ &L+駟 s%5\7 IF^~Ν;]6r 7x|k۷o#Oڵkϙ3gҥwuoׯ_|?wޙ4iRJJ_}E曱VX1z_ޫSN)ۼqs=瞋MYd?f̘™`Lw^Ш`3d̙wq=s}x_b yĉo {4vݺu=z5kVԩSG5lذ8[QGm}_|A% D8RJNԩSXϐ wǖ)W[2e_|\qHd>رcG=䓷l \~cAz뭿kw#G={|,X78{t5777LpJhƌ?~|8SO=uW^O? 7]gyW^ @U˖- v-8vΝ{,{GAѭ[.(̌-yO?=ذ'@Vv Ψse۱_vƏ)wP{x>^~;CA=+ ŮpժU?c6mć~xzjVVV֭ 뮻œ^I&z:ܬY5k o/ΒP߾}QF6Yƍ&LP#s9c[j֨Q#ة@{6,x[YY?X6vÆ gqƤI^Oqw}O+瞤pz.] %K5?~WjժέSȪsϠ[w<]ve{olbЁl^pP^z3cƌ$ѣG<} /\~E]/bw[n,k޸qcO>֭[p,\𪫮 :,s[u7N裃?]v,4q '}W^ye_~zb9fիנAIn-Z(i5\͛4iRG駟i&?ϝ;7~'v-Gz)ST+oEE˗/:th`|Mvn Tp_q 65hTPEr|G.k;uW/ӧOF+=pLII)sssO^8}?޻w^|s nѢE%}7|*:꧟~K.-eͱv… o,&N:),~³b^V^}8.]W^yejgw}/bQ*_?q!Jj׮}QGʕ+ß/دrD]aaÆ0ُSN,M6ϡ;>a?͉-jڴi.]ު]nԨQNX v3{!@m9wРA@{ѣG/ۮpnجY-(jj Tp’m۶ȸ#Gw UVM<9ׯ_T+|w-i.ظ 2o޼^x!w^bo4k֬P%K<[X6mZcg^cgNf[v.v S+l͚5:a}bWȚ{9mEN/t9}7nصkGygϞx']v 4@ ~AL+`Wv_~C GsF{ݮ]m/iӦ^z?<,7|sVVVkrm7k֬(sH;tr+6aʔ)M4)}-|ϚzpRabϜ"MvQ#8";;~)Sbذc ʧAk PiҥKƭ_>++?\%kꘖ-[֩S_ q _ذaCKᄆzժU><,E&Mo]v;v?~|޽_|iӦ 4k֬:c ]#?*C==lѢEguV8H-aVuօڵkK_(e啸>W viov7o>|;i$K Pօ29JN鬳κ[bog̘qu ;/86r쾜ŋMc="''+e5jꒆjp۰C9>bóggq% ?1$)e:th's=O=N999a "6Ȍ3Y+v7ogϞ]ʒ{%`r!@"\-7wo]@C oڠ'>v&˸q<^{m1,w$z…G_>+vtzR\bEXwܹb7#և &. 70vyƌ_}U w}7x9~ԫW﮻b 9rvK)B 4sss\2==kٲe<]_~ [hѢE,;ꨣb \y3ߊ`?K.ح:222駟.O=իWfv믿%'NӧkvmY袋]OSI#i[[k>L6-eݺu .t!8=}w2(7>}l֭ۋ/XDs4@Y[%@eƹ;iҤ~8k֬³/_޷ow=|oM6bP;r5ruIw;3(."z뭱zɇrȔ)SbrKǎ1cƔ7nC׿ua}uӘ'x"|Da1p@~f͚؆&M\z饱^{gGź_?++M6[>;tЙWhw(9Q,1V"5.1#Vl` `A"*Ƃp")t8;a{/w'Wߏw3fL^;rΝ;wС}ѺvK,Y`b46mZ;5/RZ^W^/[,Z eee]vesν袋v*"zy5DG>}gܨQׇjժ[[Kۧzŋ,Xp嗿I`ƌ7xc;v]w}Lg//N9jխ[wʕa7--7ߌ^[Ξ={Dp Ǐ7n\jԨѠA5kք:u3&Ck׮]zGHGNx]둓&X"끅 FEv%awygm̙ӷo{._~ɓ4-UY v X >}PL4)/"/// TnbNzUW͘1ʎϜ9C;UvCݺuɓk-[nܸ/ ۷7g:th>h߾}s26mD;`ӧOfϞݸq`o(?Cڿo;]vժUgĠرchfw}BpaÆ^:X-[~1c+Xd,KYgm۶eD/V ߰aC8$tVp ᬠw}ꫯ/)K  ^CVVVDˢD=ӟ Y3<wM|_}ɓ'Z*ܢc9&x'tN-xzv;`/ҧ~bŊ܆ y]tQnpA \}w]wׯ_!VpvԩT|-_ N5ܹoYe-" VCZ vDW_}u7N:5حs=ꨣ 9sn!HZiv/!5(hb˛ @ʦ__!Q&&Ą41 @L(@ Ą!..QFXBI\s<R & hbE[zu7h >?4@Ʒv[j8\@L(@ Ą41 @L$HÙgn%;Y3X>GcRr59;@qP & hbM(zj4bR(B|ժi*˗8}qIKK7] E"e hbY$ʒ8yR P쩓> C>@yoCnݒJ4@rϝ9JhbQLR97s>- @|gbjTr8,X0eʔ+V5nܸsέ[*h` 4+pzZZs(:tX9m=}_~7p5kժUW^]ƨ M9-if@Y%k^a3yZdCtP0mlsp)>>?,ƍàSN2Fe P4Eg~۷/^8: !T Et[oݺA/lPI(@@-_;lӦo *<7!ؚ;w駟e˖Ço^B<\ iӦ~۲eK~W8055Ur DgeqrRb\\P~z7nN}&L0~f͚4@ge?00wjIre Qsq$ m޼Cmݺe˾)S关͛wN4)>Tp UZv/jD$ C%K}C 2eʘ1cz!QTlbYf/#<2|piSRr7tIaW_I4>}իe OJGUV *<h(=F^,pp3^iEuUv=}4\ζ1f͚>xݺu }AFFF=233 vG: (v=<%!F-[nݺ 4hѣ׿{͚5[fͻۿ &tE$tرwC \lٍ;>ӵkGyG *rT"wJZk>H(M6_sΒk?_,_<''N:oѢENL %B E#S)|IJJ֭<y(}(Eə2eJwI(_⒓+ =@L(@~HiK,)pzNNήd Pʴc9# & hbBP &(&O\ƍK@ P>}Dc R֬Y7mTr8h"UVM~)c@ Ą41a h"dff a|W}vI)=5jڴ{aIUyq@*H$kk֬ٺukjj^xUWխ[Wr(} PAl߾gϞHg9蠃)~_?3Rf h  iֻwM6]x+W"J4TÆ O;7ؼy1cR ڵksrr^z饌0ؼyQʌ @ŴeZxfIL$ٴi޽!CzYd~.۰aI'4xM.\pРA}}-[yW\y֪UkС.fPXٺuA۷g0G_~K/[}GsN0k׮}PJ6lp '+Y`AzFq Hǎ*EE(''gڴiHdk׮9sG}4z+Ws5k\z饹uy'51C#--mҥ_~y8Ԯ]{ԩy7<.] M6)@uꐗa}7= .}r,!!@15lذ^zUQ2n׮_LYvmq{222.UNs4iҭ[ѣGO4iȑ\pA+ -^[o=4o>T)(\^nβ) PNz#s9g̙knԨQ|||0eԨQ)@[_ +6z=zt 48%\rmZ+0a}DiRbYf# 曙A>o߾6*N<~&M4o|ş~?Ю];R/}aСCݵSN8s$v;y`8}f5l0 fΜ)Ք&h,Y֭oi?e˖͛7oС;ںukԩSG)M -X GzsjjjآEK.$H$t ^zNdҤ {ُ?}0.|u%Kvza_|nݺ k]M)s2dҤI<ȿjժ'O޽5kS.W^yEbʸ˗G5k2:wժUUV:Gѽ{w}e˖?xx ' 0@)e e[o\rnG>^}!C$'' |vє;Y$ƷdWK*'C*6?~rd811IIIaaÆW4ō1b{gݹs7xcwuw]!+>}ddd^:&Ddۃt 33s 馛>9s^~寽ZZ$B_A8Zۺuk4/m/75jIJ׹ !ek.\vvv4޳Air4@Q' לzP4J\52+++XOq.Njjj1[n۶- j֬)uq Ѕ6m믿>gΜ 6lڴ)))i;cw~gWby8//֭ u>Z4 л5nܸnݺ柘5g_|cǎO?tΝV <;?ҥK_wK/d rjͅ^9iiieMf7nXHh7>z;lKNN_l~ڴi'x#uVfo v)'''==}_}nJJ Pf~n$=(kVڶmӧqFFF!--[ Д л-&&&N0!/߿333/3fhѢnHz t?>k?~|ev+x稳:w=\0` :+%%eӦMs]`A˖-wm3nܸ0hӦMN$O"Ȝ9s͛-[ . gmٲ̾uuԱիwy/R1;صͰa믗1"3fԨQǏݷ}ћn7{)p 4XdIRRR- iv!|~)6O>~ϟ߽{w@nW^y%''Gӧ>'|}![( o֬Yr=}=餓 o6xg}kҤI-ԩSsZjWwk`oYdf@Ymn,iӦi[322?Y?s߾} %%eĈ z_裏Flٲm۶u 4oN>*))7M5j<OKK+m@֠VӤEoagKc$++kƌk׮={Qg͚չsMa9([n%u]ʪUӰa|pرc>`G2tƍ>q{GSUv޼yawv:TӦMZ|aV-[dȐ_ʎD.{(q.E]OSO=rM7խ[7:w֬Ykm۶2{+FvZڵk #u]߀wyv@y2dy P v1\uU?|$0`c=ֱcƍoܸqK.8==f͚u֭QƝw٫W2-k>9䐖-[X"x۶m g]tE+W2х &h"Z*6lFţ]E#;; K/499YNC ޽uo?(SjNOKK4*02C=<_=zE#PP & hbBH()of;u$9TB Pb7n\421 h:ʄO>9K@1jժ~7  @L(@ Ą1)=:th׮}7޸~nݺM4w#R Pedd4h@V^]~ xg7vX{1";wYuֲA h_b*U/^iii@,ddd0hܸ_~y]w^:XO;[h!>h__-^1xD"@q|gݻw2dHϞ=lo_ve6l8餓ܴiӅ 4o߾=߲e9sf|oVJJʦM.C9$SN/(eXٺu:>'N<3͛׵kׅ ʕ+&J4}w؃| g}ڷo_`Ν;ɓ?f͚A͚;!V(M ĥ]&(YqtawmԩӴiӥKF">ꫯ?SNa}agqcnBPCzRJj$x`!Zn|Nt$֭[r(q Eͮ2P,Xxͻ;}f5lYf4kҥ㡇z6Iqa\(; .Yd׹zk4}„ c~R {ˣ/:wժU;=.{.z=uw?vF67!l811IIIaaÆ]xWޥK믿~o_y;gJ4e{Pdff X瞛3gɓ-[vGy睅+ MQvNfvzWKroP"n 57Dѻ:he34ek.\vvv4VQ)@^VFbʊƵjՒ:8hRSSr۶maWfMSy8//֭ u>Z4Q˚5k7n,e}e_QW9UB4WjնmN>=322 ilٲ0P\P(wt_oxݎC-^%Ϗ`;묳RRR`ܹ ,(͸q M6:u4>hW~y# l3lذ0erAٹ#rKB}G֯_O>$CݻtQ.(@!7ʗKsr_27w۶maiӦi?g} #Fuj(Pbf̘vٳg5jo}֬Y;wnڴiJJJبQ["]w+ZO> 6LOO,X~;g h_l v|;Y[lyfs=6[{4?~%\YNm^wuW_}u5$rDKwEiiR"_{VZ=ÒI` hbB0@EVnM*y$J4@E_~}yN$Ă+)=nu߬fr'++k1afIIIApy @\ @LIII\p<R & hbիŋiii; E3fLw޽o߾YHD~vĄ41 @L(@ @O7 JJ^^^! H"O=TxgJ]겳rrr mE~ iiiR{W\\\RRRVVV͛kԨ!-'8g\k(UJJJ4޸qQ8_/I~= W C˖-S~ z?NIMM_@yUZhhӦM ,HMMUVBBa[bټyƍW&99Y~~ h"笭[eG\\\&M~;HP"=%h:ʄN:M5jנ~oqk_OsꩧkN(Ya z84i\"rF-[ܾ}ƍ7mڔ%'/l䍒 TqqqvhРA$3(P$>>^97߄...jժ)!''>;*@S Ą41 @L(@ Ą41 @L(@ D.99nPL Eg}21 @Lھ}{4?P寃寏UB ֒%Kq%Jeɒ%zh|Cp[ׯ*J^S&6n( TZ>fb#Gl۶+o:tﮤGrvgdƌӪU+omʩz[Jc,%_=[o@w_p/v@mV>p9s\c4۶mۮ]=^U͚5ǽ)D{Eɾl`zyx\_կ_C ueiX"$}21 @L(@ ĄRn\q]vMKK+Б Jc,%_*)X_PI`T",;?Cxƌny#rt!8 hbBP &) ׯ߿h,!89VqH@31 @L(@ Ą41 @L(@ Ą41 @L(@ D#999_}Uzzu4hЦMc9&..Nf i;tp 7sɓ'Ϛ5kƍM49C=Ptq͛7nݺYf:u:@?1;;yGydZZZYΰ4%/778pʕ+woӦMgΜhѢVZ}ݗ^zQI>$.\|F+999/"۶m?O_?;zM6m;v q T.nݺu{"GQFo6## fuQv Pi%K~zՃŃ)|͖-[N?G},y F ڰa9gխ[wԨQ]v &ٰaî쫮j~ s=r@hѢEg/|FSI|s83g̘ѲeѣGwСʎ_=C7;H/P ?Gp-[>IIIO=T޽vq|?qCmРA01##k}7RSS_{> fJ %(33O/|pLJӃ?;vبQwygԩ[n=Sr׿ ۼ`Jd+V|ͯjWZ|Iv;nΜ95jԘ8qbv]t&~?#T]ܼyo7n8hٴipzRRg̝6m{wa٫]\ɓ'~6m:C>}'s=>;w1c6M4)k9w*%馛n\pAin޽;v=رc2toJ "++{gy ^~ ,?O~;$Ap 7| T..;;;"v ׯSV^=///^dT.nݺu^xmۂ Z柛c =͛7+@SbM}޵ANo*D6l'cرƍ ϋ.6QW_]eo9%y*OO_* %%W^.ޤI . 6nxwM@-]4Zn}xNʎAB~ᲖshJ<)޸qΝ;sIH/͛7l0I} P9 :j+y&L?~}JNN.|_~9Ri[nøgϞ{zח<;(6m3fL_\\\w7a+ 1gΜp@Vj߾}ݺu/㏗/_^/yyy9rdfffGgm޼9Z+#TU(_~C-etĉo? ?@:u C wq\<55e˖ajժR1eʔhܦMBZm6 &M$uݝ/mذaRT.n={5kd]\t}ݷA[6!!!zkĉ' Д/7禧K|PH$_SjժZPỸ5k,Z8O-[,XPvMXlY4nܸq!-lKs.nÆ 7o4jԨ'C+~, ДuEZj2ܵkJPmܸ1k֬YxcK@uR T.b Д?7֯_%K@euR T.b Д[F㤤bl"u%K@e.b Д5jDBZ柛)'KΗJ!]\!))))833_ ɒ%vqzH@W{HhJF&Mq_j۶mѸaÆRTt+I=$PٺC*@S2> ДG˗rٲe.|P9h~/0CKNNnժUq:իW?N)'xb47o^!-s/|iGqۃ>ƍKxҤIke˖;-2Wg]ҢE?tT.cǎ:t(r)S!Jŝy/r'Arr\PMk`ӦM+ͻvګW/I*>}g}Q`#GAV_u*|]|ر?~9P޽{Qv hJL6m0~' lOA~RRR$ ~h\eN=3<3/<̮ }00`@իWxڵk_]>|x??a[4iӧO/"ٳgGAү_gg9&__|_} P93&_ "H!{u/^܇zhѢEUvΓ[tqjժ]w4ouw}v Py}_oߞn^^ލ7ޘď=X-Zs=6JJRR?a„˗ᣎ:yᬑ#G+''O=ztեg846@e}?裏F,Ydƌw]yRSSu6nܸUV|x[O:ѴiG]ŋoVBBBxjjԩSWZզMt*m~M7?f͚GqDofݺuAwqe3 ֭['N8k֬͛7׮]}]tILL*?o۶m}ԩQGeg@XjUrѢEAV]\/,Y׸qΝ;tAe9] Ą+ hbBP & hbBP & hbBP & hbBP & hbBP & hbBP & hbBzf͚R@ R >|ҥR@ Jbjڵ͛7ߺuk_Ke ><%%϶(A [o/5jԫW;.--f͚5^VV 7;oMՃ8dff\r„ ܼys)-q .s饗() ȑ#=z 6VZ6شiSԨQ_~;@[n}qygqFg6m >J1k~gqƨQ ,_|a|GznOoݺu͚56m*%n;S>J+z+!!矏+ӷl{l!:jժk6Sv%O{S|$QZ#Q_RhBj˯͏P=m{öYPI6i0ʆB&m:0$a0c3| &=swuQvvvΝԩoVbP8deeeFF@C=Ji3o޼o&'7o޼ׯ?3UXX8yz>?4q?Qv-Z&mjjjJJJ)-WZC.ʕ+tgq ' .;v,"z6mW_y4iRX>  .7nڵk) pnnYg%b hLڵkׯ_߀fYYYIa8$ŋG??Kf͚5l4ewϏ% /e˖~? L&Mܹsg@|Dc=qpڲeˬY.ڵk>SpΟ??Z ˗A.ݽ{bڷoi222~ @K2oqe%6 :裯P $ 8$ f˖-y̙3'--)LeH @H.99.wbŊ:uq ~vvvAAAzzOg(+h@F}T:4iRʕO%#q(h 6|Ѻي+ϟW{챦 8T.C&M ˌ aPp%*'GuE}W+%%%?~^^֭[WTi]l 6ԭ[+~V#嗵jպKׯ=:vYfԫWM6'pƍÞ/]v;vx>;/t\r٥իA;<ӷk׮ŋo̊+FשS'\'xН)amwqQܙ3g\21---Pi-mZ4hPz?[nV*,, M6:th/]v~{6loZbEV8~hV^_gϞ o߾rJNNNX y?~_}{w.₂hf͚3gl֬At{1bO;oҥ˒%Kӧ?裫WZfggf^zi6m֯_Rj)S4j;G~Y>}qLڵk/۷Ga{I&]uUիW8+ גmۆmܹs[lVvyAزeK?~|ҏ_Z/a$zW}Ҿ}0MἏ?xN=z/"ڬRJÞf͚3lnnn" L}+Wܜ9s·LC%= >|mޮ]%Kxwyضm0ka(͛7qĦM6iҤUV3/r8f+[µ ܽ{wXo߾aLmv54nx?lذ^ze˖ax0ba ̙3o*Uܝ֬YM>=l~gWѡ\tMO=TÆ z0e,58-Zcǎh}+G#F8묳V-qK,I_p BĞ|ɓ'O.^8(QX~?\a{.Rzu)Su]JӦMؼy=V:vQݽ{#GKvmիWhYf :;1nݺ\r៾N:)<#Ç?ꨣ 6lРƍ~ԩ/!Lgy_n]zzO;3f̘?ҥKwGaY^?G}ԪUիWO>}dR+|:TT#-k׮-,,;wQ:vx'$.lq)&MTi iiiu S3I bz뭰>UV%.,,Yf%t TUHKK֭ۖ-[Su 0aBVVV6m:t{Dzp/\Z(|'{=`);M֤IO?H=_WXwZBrwT=w* H6m_~ў7x#,/DIHrs +{>ꨣ.]g8rAA_\^vYfEo2L8$HMMyדie /~[o5l4đǏ5۫jժ=:Xsp:T>DغpW^yu&O\1cW׫trMtF/,3 'e~w)j*??^ܹ?U#ƍy氞?Go~}QGEm|1DŽ=QwܹQEjn4oiĩgonݺvζmV\9cǎ"ځ:?FgIMM uiС2Apk&)SzΝ#G5k֞i$t_lѢETLlڴ)O?0QI=XqoߞTO81,:묰s_PPbŊ}3;szzz%|#63 \R˗ϝ;'ضm[^^ީz7_uU%2?={vAAA56lx]w~C4j[oСCˣFZlYʕ+UtwXbh…_}8uTB jڴif§B_ځ{E6m1GQt?cƌѣG^:cԩSfffr &Lƌӻw?kk֬r(GT^;#ZUVEPV=z 2$0,a/^.-LJ.]ۺuKX$u]ǯSN+bi׮N+.a_}pcǕ#bk„ }_ڽ{*U=7o^bżyx5k,X?_y啳g^n]T0e]w];%8lܸO>ݺuׯ_>˗^z&M?cƌwaÆ W\!}> ƍףGP̧~5ޫ uSNOOO7ԇ|pz;MֵkW#P9sI'=yyy{m0vذLKK;zW_}>f͚t{,zdXHII12 8|Gʲe~LUR^z)//I&SLI1o ,G}}w駕+Wv ;EɃ 3fL> stream xZ;$ +* F'z ?`ggof\=s ["E>\`,ۥ҃"?Y?nKXL.뷭`zI1m/M ~3mf?{V؂r1x^E}ޒ8=&'޺?&%Ȣ~sQIЙ҈U"kbiZ&Y~qj0³RлAKX6͈T^Kۡg2Cde0ُf2I+w "#ө.˭; B4.Fw~-0Wq_#DU=`C @gQ{E͒%(4J`6G?e1*M~y͡ uPFaZl+hVfZ{ZA?_1 ` YnFN- h z*C픽58>4.Ԓl`bJ9\wO %I]'(fh3^_0vU*-|GXAvRL@ح2WZtvh=.VSQv880UmYHaS7WqL L2]k*Og)5Q݊r}oat݈ΔsAۖN;<*m$]2=fϹDD<}TnQ7;ּq+E5ni]V,t <_W-.4}#uz`uo;`ÁLf?ޠ~ \x+_f%r>{ѳix|::.Y>R"Zx+W"fr<0%o[u\qпI[2%YǭfYjYZlͯ3rQM]o [@FChM3֝B!oD:w$K+:]TKvMb}ƾ\GO#qzcqÇee%lBeGp쮃¦vh.[X>.מ=BOB85Kƥ6j3 [ ֐5as<~URҀ/Ajr_Yۜarf۳a9w29(wj(Ktpe叿ma endstream endobj 83 0 obj <>/Filter/FlateDecode/Length 4539>> stream x1 ð'ݱQ$E____________________%___________ ____________W____________________%___________ ____________W____________________%___________ ____________W____________________%___________ ____________W____________________%___________ ____________W____________________%___________ ____________W____________________%___________ ____________W____________________%___________ ____________W______________F=̚ endstream endobj 84 0 obj <>/Filter/FlateDecode/Length 94316>> stream xXqP[`b,  h,1\h$v" Q  ܝeEd#9s̙nCBa@Q3"}                            Ћ2(ݎ=}'O>z())F,--4@1zkp‚t2y@mVZկ_8__Ȥ$--,,llllmmTҠA ~&Q<4hо}+W]g͚[zb,>! ńyʕn߾ܹsժU,H'+VPƒvk׮{;v/ѱ4gϊj׮]ogϞhBҡCYiӦ;SuVB!$$$ȴmۖHڵ'N( wz!Cԯ_aÆ;idL/~'O;?zh)jhhتUӧK~ѢERd ۶m[nn>/]յTN[Do+-1a„SJ?̀(ʕ+׭[gϞɕ999ӧEA:)[ ͝;w֭[Tf޼y5'NS144tqq޽{z~}8!(*UԷo_ʘ={5kK-Jq ޸[n3gΔ7?ޱcǧO/RVo##[^(a92nܸQYVR_;;;˛O<(233/ yu! Pܕ/_^-8{-[pi% vQF~._pƍ4.4^;]* ^å̱ί///ܽ{ԼsP(&Nح[7՚T??+͞62QbER꒒(pՑ.]+oݺ{b\;|D㔔Ǐ<jժ:toa+W={6>>^QFE>B)&&&zxIpppϞ=---ϟ9s&99ѱm۶pBCC߿V\9&MHPl(,--7nr߾}'O2eJ!:1c*]]]UkNoWSItll vؑ.矇 zMMZ٪U7:::a~Xj0r.]<|}{nԨQŊ߿//^\lË$5}1P. =j(QnР۶mkܸ֮+=}t!!!NVlYq:QhҤɈ#k$ B fVf^7@RR~aTTԕ+W,??{6kL\J*ݼysɒ% Rm۶mLLL6mD[޹s77PΞƍ }}}Ϟ=G@qCꫯl\.]={V'|{cǎ:{?yHrӦM8p?PJ~ڰaô4QNHH:uUv)'''ѣ}||?.mazoߖ7]]]G)o:88L8Q5˯Jzƍ+xYUu$eUJJJNErۏ;V<==6m*0 <<<((O4hPƍ>}*m.^XkZ>**f͚v l߾}?ӆ |(nx:@ɳ`iJڵkJ*Y ^ئlٲ[7υ .^X5,ԪUKq,5k}6P>pfDDĽ{^=z'OTM7߿)&*<<떖{%0PQSuU݌9}j@4[raI<֭[h"i}˛@A111ٱcjePPj%~#4*jˏ\nڴ6SrJF\`dggϟ?_Z^*RJlذZKsssjBڼy'ns/AT7-[Vvmf;vwUkpqBzڴiTQ-d(XDrppL8aÆ۷366fff5QZRfhhvei3::8BHLLsΩS6mCM.].\6;[0c i_u̙Z[J hQo8$u~^'O\]]UkKjzF7C$J \gϞrM^^^޽ϟ?_Zb>xyo\w﮵ݽM6_|񅗗ʕ+7hРsJ˗/4i?3**Yu =:rTnҤF(Rb4@ ֣GK.faaQG:Q-U{lkkcdddjjjmm]B''j9r E911qʕÆ Ski&߿h@mt^^cgggP(٦Nzʕ={5.]#yS^Z"碕$@F4x뭷l٢\rNNNݻGp^رcrhh_%wQL޽{%= '% hRg̙5]v$8ۻYf9s>C{{{Tʕ+'O:E@J4ǨQZn߾ȼ"I[n}(l޼ـ7RJr9889*555''7$Je˖5d^]Νk׮-tim-8--m9j֭#z h+##C*!cΨP(ە^CC1cț˖-ۼysJJJ̊t*Sݜ:u͛7u믿Pܐ(RSSϔ:J*AAAO|q~3m4sE{ '''7rÇZۤ TT_~^^^ɺVڮ];y3==}:iӦ#GO1@1A?޽6mtҥl:]:==}mgϞZ_Zd\oT\&8))ht]T^;vhm^2+99 q%GV݌nԨ͛c;v˖- P<(Ν;g̥^~eU:4iDuaäķ쯿jѢ\yIݿ_.wDMk'i=Jvm"_~WԲeK pB__^!**J5h v233ѣGiiij |||Z#^B^ǎ;f̘.]8::Μ9vժU|\OƓ'O& ڵk,dbbKЮ]ӧOKYOO.]hmfjj{i3))i7oܷo_`` *V(˗/8p ...22f͚#FV1P.U1x`KKK͓n۶M o*]j6 3g)0p2eʨ5;wx5bl-_ٳoV|r+W7/>z(44tɒ%C ?nݚ{ MDDDxxvϟ?6lp;;;t [&Mxyyiq޽AAAfÆ 7o֦M6jꔔ+W:uJ7n߾}{ƍ(H@/ !!!SN'\r +ZYY+###__;v>u$oo۷?{L8}Hbbb6mz9sGSSS1 /X`۶mÇW#*߿ƍu뚙;wnӦM+V7oZ w^JJJ͚5EͬYD^uՅO޽YNKٳgڴi7nT{E5k\~իo~w8;;/^X7oL+rqqUMoܸH4iRriӦ}O>m֬YddR֭[[hz'N,^x̙j+oҤI X2A.(5?c֭[qqq|GiٲFu1y葝>Ncǎ ".K.988/Zlos΅-[zZ*W1kժU[Poo6mHJ;w`cc緷)q7@%%! h͚5AP$ pwwo۶- 4b bر0rHCCC hf͚u{{ x!_yyy6l~\rb)Sʖ-K" ۗ.]* )))]M4ZժU#>K1Kp4@/H@4@/H@4@/H@4@/255ׯ\& ,hgddF(ׄ$zA vYYY7nʽ{9( E^^I(244422? E# vWhL@Pdgg')gVIHQ! f̌KMM%LPdee=VTayu ξ{.g@of!JW\ʹ"П̌Ry1fXCnnnlllNNtofquu566&4Ç򦑑(򲳳+oqvv&>4!77799Y455uuu-SKKK{{,ReIЯx#lhhBP#! Z8(LH b]^͍(AT?[XX@xk7wNII!,Fchʧ^P7%DNkkkJ5M@^Kp~ "77W,[,1caa!r~̀@;CCC rDSjjo-Sh$̧[Am 's, ̀~, ` OzH@]^^'R٦|E##6CvB  v9Yvl]}ejfNLS PH$TMzOAVC좹)17W_Ġ#.,g ` @i}?^hnnn }# ( R A@X+y!AV$ޥKH hܽ{TBH@( Bѯ_HBB3)cܥ\& fAAA:@;cccՈ%ŝKٞ@9rEt# ( ܜFp[C]pjͤI&O,/^ؠACVZկ_?՚ׯ+uyܸqR+W\VzTƍ/\9-Z\zݻdѳBjZh1i$63gΝe## vtÆ >ܹsY&M֮]+bl/԰aCͽfff(شkNmoNNN^&M$dgg'N̘1;77w*qqqCݷo|xJāM6cǮ] |XxYF(䌳ZOj>fP(͛`9F!J-kkkӳ}RyݺuO&M;wN.\8fSSSi׮]XnرRѣG3 %\\N:" hMWfMqTA2~]XXXA@Idii* {#""B0 2mڴϟ@9-ZG{sssS."" @H}kRf ~gΜ]vEEEU^]1j(++#cuϏNKK/L^ 2d̙ߦMݿ899I7o>}B xo޳gO.+ Xx {Yŋ1bDӦMO*5Xdɶm._,m<~X*'&&j|m‘ >L{ [*ԩSGm۶fo,f@yVVFJիW1bŊDE4^ξq=jSoP(@1aÆ tܹgϞݺu+Wĉ  hܯ_Q>tСC-[/_MSlYkkk3f|E4JǏ8p8^GKOO'8eʔٳgOf,,,ʕ+G;Ƞ4B'&&ǧG~!x[jնmۃZYYJ͛3QDFFhkʔ)Dܻw{:uꔙI@(I:uTj~hk֬iӦ×.]J@ q=%,4FgRΝ;bŊG޿_l:;;{yyuHW%Zj5bĈsnݺGD}r2ˊj5nnn&(1.]wyըQh􀀀Ç2dٲe&&&+W\vaҤIk֬6lX۶m+T@HQjq݀8P? J &"Ѷmۇ^vmĉ|wO8//^lܸ;ttbkk;~ǏRȘa񉉉s)-5j=z|RDjXfgJJJqE(:tпavvvbT?r䈎D'ʕ+SSSiQ:x$'E]zzիEo߾A͛7_gƼf͚K.؞:uJLIIߴiժ\>t萎}]{{;wrߢtU$(ۗРA2fyZx|mT"o޼7##ۯ_~˖-ܺxTEGn%O$QmݺUlݺ!(Zuֽ~TQ濟Kik{_ѣYYYD"Qmhz(|K hk⯩ÇB˖-F;wǏ/^ؽ{~im:KQw,7o#N$fn7֥KՄT޽7lѣ~׮]KJJ*W\ӦMGRǏsNZZZJ,?g{rxҥKg>|V9> Op쳁2x֭:uV}˗/͕+<$}v͹;vP> Ν{뭷?CuWtte˂lllTwW^ABrrСC׭[JKnto߾Ν;X!++ʕ+===-ZԬY3lR𯾦VkГ<1$lddԽ{w̜bbb[Qoɓ'ϑխ[733ZjՋ*?.欭6mڰa$Qs=zTPArѱe˖nnn)))ʴ'ufffV.o߾ݪU+y8Yf...𴴴ӧO9sF:gYEVlYaq">;u$qvv677X766|ɀ:w:m޽[J؈{zzZZZ>z@9zÆ t~[BTS1K8p谽}-yʢjWk׮A+ȍ1HʕkҤBFFFbbbNN|9DH4{3f̵kפrFTRE\ * /YTdCCCS%"h?~|dϞ=K,"b{~!۶m_|R:$}-[s>ǡ?9oڕEK_Ŋ2!!A+.Ǐ-Z$`/Ry˖-r'No999;v<|pvvg͚p''_K.:%K|wF qƬ]FFF(7k]+W/?--?իU?~hcii)"Ӻuk9 ƍ'/_ݱcZ1N1 .(SӧO߿ٲeo8pcĹ -!\j٪U+)(F?wmذӧO֭Ά4KK ={믿D\ /P(LII݉xu :ujҤIGիWxxxj佝4_~X_R~ԩ9٠P+53Ż%!6lٳ_@2C Sw):::>}E1116l{ΝVXﯶ('O|EQUVhh꿐۠AqHFFƚ5kzzhSfͫW^2Ҍ`+Ϟ=ɓPRǏ׮][u7o~…3f̜91yrIUVuyIܹ#~V\.Yȑ#Plك. bhh(qF__ߜ<?u-:$B-Βx₿{Pl2>-[_++}/8( ; >Qg%{*U֨QC*<|PS>8peDyE.ϟon:)9//iӦ% `\zhK@?|ƌRyƍjg̙3۴i#۷o/`2}޽vvvM6^G駟)S4ڷo߻wkUm}jU|WR[^^; &Nf͚?SuF9(7+*7;EAZa,WR K@2,,y_3 h*7n/-**J/-?Q_ѽ{wq:cc+WdgN,Ti e`Y0S{åUrss_+Wrss/\Ю]"p!&t$ 6mܹSP|駪{rxx4СCGʚ8qb>}D ǎСB/Lqvvza_;epq9hFF4EqԨQsU_Nu[ϟo۶n>e˖O*U:pg!zȐ*o7+c5a9E$\ңGkhTREsoO:%_޷oTieegϞ:Hi7TΞ='$$98::J(ͲuQ">kpK<2eʼ@@@=9ӧiiiZJ`3O u5)Wkbb:z)5k@ZC5]Z5i 17n,֡e˖Ra˖-ӫ MNJrϞ=ݬyRa޽:fˉx6m$ ѲoUsss z-υ >S+L8q„ /GZEF>^,/f̀^bE~ԛ2eTСkÐ!CСCܹy={,Y"'MrfYV|||5Semm=yd?M{ R]ɓ'~6'NǍӛ7oKw&H8}t)תUÇ~@,^O?f] 0sZ+!^Z*m۶e˖,innu7@`Y/ڵ1P[;v쨏ըQ#U"u1PfETϞ=۷%Mqq… 'L G\vM6ӥZ}RaܹR͙3gǎHt颏l۶M%))a333HDW!!!UVF{IIIyaZ*fĉ'NxP8w Ǐ񪕟}Q {GGiӦJ{.\\\&MdeemJJG}T^5j<|Ү^zN#{ӧO B qƲeر?gqwwʺp“'Ofmڴ?~~絳q֪U@ݻwoZZrZow+0LLLg(g]V/^_իWŒ3l`kk;n8k5y̿[sʰZжo޺u븸84իשSG #,,L֭[ҭR~}q/4h ͛)nϟ_~]mq---ŭhaa!zPKΧN*$ 4n!""R!C5_@_n#8Q~Z^K Ju]rܹs̙⛀^p۶m vvv:tPfddȋu֭[WsŋG$כ?^H䲻Ke wFvΝٳbm%yѰafϞ-5ܰ PqۻwT~wtnjjyf__oFڦzVZ(=zE$BBB4+.]c "5kּyݵkW=44O>"tb3JI[V#F(W[nGd++剫vi͐9s@¬Yn6f""\gϞw~aÆũu$'LuDō` 888 @qsgRM@,ZH뢽˗o׮o᡹Ʈui~Ι3g߾}1115j'2d-[5k֌3*Wf͚z;ȑ#uօ=zH^Z6m 4HzƠnF:{l||xySN9::>}CvVRq]4R=5YsγgϖfP͚5O>u_tI?5ѣx!yyy+VW^6PfWM4;|ݻwsssmllDztҺutq?޽[^nBI:uĉ~נL'''q5])|'#G\rԾCg-~{|P܈?{!}-IHHpvvVo¿xJtssRyRܹsǏ˕+G@ SN>>>Æ gN+"YF =%""B^|Æ /Udff[N*Wu!>wJjРz777J[o^AՉ2ϜJPڵk˖-U\+6iDI'P- vUs}#F7oT={1cO}]޽{Oovj_xqǎ{nBBK6m飹JV^u֑#GN..B -ZꫯGmIu&=aK"t&&& &M$E/p7 TsCFnڿ޽ HQtҪ&m۶_1]$ϸuV^Ο?c^jwB*Z䚹3i12)\f-s'0d̝̈́=%ʭ.t?wv:I|{v۠~[v H' uRɝ;wڷoMˋM֮]DޥK/KH һw dϟ? ׯ_wqq 7o,nӦ;w܏?(vYw^jժV*oܸ!6cW^\\ۗ,Y"U/^T*͛'\LaÆȕt1.]dLi&MjE})Sp*߀lllGC={Ԯ]:T߿_hܸqǎI>9CTTI>K ѣxҥ >q4gH|||jժto߾}"## ru֝;w_~b`yٲeŻѮ+V PQ\9+_z[n?QF+oڴi\>䓦M:99-X@l.O9vxݙV%V{"D&M9rܹs-[FB^J|Λ7Ot8qOŋi6hu xt)nѢ=OHHh888Ho޼).|)x&#F~z.FRN`\|yƍ7E@:y)SH3f 0H… ]9RO>O?yԩ&L*ӧO߻wT 9sA}u)0{=nW( ְaC??hӦM⃸`xlْ8122j/#5\RJqtt˳dΝ z葻_dJ(!eʔիu?~,}&A sIJJ|B`Jtn>r7zW΀o/\p1ӦM_5Pܸ&M*_|Q?kE|bϵ֭[{{{ܹdի֭Wf͚]]7bw@Ə_ZOg;Hrrk<..m7lmmɹcaa!?O˾}팔J۪UDuM\V8qU~\r|T߿HHֿ޸qAjW΀^tE6J\d>8_7͛7ݵkիF<<<ƌ> P(z9d׮]/uX9O>]O:U~ch?.ǚ3j֬KsUVr|wS8 z͎5zmEw-G| yyy 4hԨQr_$daÆI&̱&O|[ 3)))7nl׮oٚ#F?~'''ϛ7f͚k׮|Ez^$cM١fI5w.6l'BCC\-[=]zZl<ԩ'!Y4՚5k,Yrz؊$ LJJʊ+fϞmdd4k֬=zfw}%}~ѧ~|+W6h@O㌋߯M11YLK/\Q-[?F~ٲeSLoܸQ0~&(<Eڵk{=l0j988,V/ҥKvڶmG}dffƭUP^`9~ݻw8))Icǎݾ};]]]IZUf͚lTTT,3n8)~⅟_fkرC f_JJAkkkOO,޽\8pgQxP^%͉Ɂo=|KbASfaRRR||A8UXQO8!{)))R !3#Gl׮1d G?E0hРΝ;g+\2<<\+Vb0FFFcƌ∈5jnݚ @t!MLdɒQF;vԩSK.mDzw'N_w)-vhѢ$SSiӦi.*Q77opW_}5}ҥKK˯\"800QF"~iݵqܻwO*ƓZJkɓ'ǎ+3f4}*<ĭF?/; 'УTN׬M69}5Pxa_}O_ʒz=BŊoݺuEIͧNZv}||cǎĻ=ZzXs>:Tرfff ϟ9s9s,,,f@DGGXO?-_<..n)))JJJm{Rd|"  d8Ushi@fBBB… 9#""BG0aspp h͛׌3%7n\NlmmkժgդSN-1E\?qb)9LBPPT9V$ h]ȑ#۷o=xl# a͛7r劾o߾իWpht311ݻpΝ; ht322]6yKHHٳǏIrtKMMׯɓ'Ir4P($''߾};/{HIIs˖-\-8W6lxŒGY?U&M ԭ[7o޼900pʕNNNZ&%%رc׮]ǏW?ٳg?bcU #""ZjuMyI@:)nР3/EѶm&~R$'WHHȺunz ygφyfyɣG㏦˗/5^s¯#-III ߿ÇՂnjsIL2vA߿/773gJq---M3I| *Ԯ][.@GFF;III˖-kذajj}ϟsr>}n*kfffϞ=laaaAAAҒ+W 4Hq˖-wޝ2eʹs[bI``G}$ibb"=]KKKՎ  <*))I,ٳgԩS'MP(uZnGmV*4o۶mP޽kN|:t\ի״iF!lРA6m˕+Wh _BU*Qo!_\}8::._\~9}tw_:+|r<{l,)SL=ɓP/ +&T>KիWϜ9#Udl juzO>r Q ;BѻwoR#_.]Zf@~4j')1!?zҔG<g*$Z;w(@b4PԪUKI @`ee%ǚM3ŒP@"e\ZVseJEWʳ!!(-)VLVVd )&&F4iBBP$P@7BQ֍7T@aFL $ SNIyWU@NBظqAk.OKKc}tp233(r4PXܽ{W={,qqq۷oz(@ő#GIݟ  Cz{{kmrE9pB}>x@={q[n'OmӦDFF~'ݻB4?K%-x! lm@###[n\/htKNJ)vԌȶB U*d kҤɾ}f̘1o޼,YJ*Æ 4hPժU5\|_yttѣEh"//ݓW駟㏖-[n۶M9ٸwΝC3fnbgϞjժm-X @!RB^mRי.vؑ!5o\=z hEr=8I0H'ut7=of$'9Kԣl!T*]]]Ƀ hhS(@тW,))I ZpW#^^ CJE~z_p8 ^.C3{KVΞ=;o޼4iɵk>Ӟ={VXZjd Eh֭ۙ3gM';wv҅,H O& ((@H#Nm1  ǿ\t46zA-@>0-. PUk9b4%':uQ۷xĄ K[jZ ץة  (@(;ZZ"ПSRňI:bOPhTLZZZ> ş@LJJJ>Zp3aaa/*R(J2))IzWdI$AX|0:/(@3Z]:*]tTTPVt~oorx-hђݻߗZ7QcaaAZ333{M ssscccuY}2'/(@ E ޽+O|NNNJGr̾eTGdɒ+W6,<3(@F 2#A>Zp7yi#'(ZJ,YzĘؤ$rJtCBTK{{{r"GPUiiirch044^k Ȉ<z@4@/h*_{lhtRooo|fff ,ϊw Xb̘1UTh w[#رC> ž]t>}\ws[YXqBRRNLF!yŏ@@PFqbsMIIqwwݳg͚eggqλ\r͚5ӧO/U"T) g̅& ŋ.\ "۷oШQ#M6M(B#߿H )Zp(:;;+ So>T*I@Uk^j2r4?A۶mF.HƟ?~VZhۈȗv PTP7AAAnnn>}lذÇW޷oߕ+W˔)[o=);_tIl~۷ox\rmڴ2d;3OLLܵkWpp8BUT]v={kcǎ/>{LlӦM{-߬Yׯ5Jdٲe y79RlYhР(͓ݓnܹÇ 1HoY޺uVYx_g|X*:tgϞrɓǏ]֬Y3)K" =k׮|Z477?~|fg!ȭ4ob ...J/&3/_fVZ%4iRbESSӧO޸qCzؕvmu^-nT>@:~R,WCCI&3F8p 99yժUsz+.ZM Z|qwooo<|ԩRQo߾>>>UV|7<<[nObb ӦMbYf5lPz%?hM5׮]+trr&\˙QƼyt"2yƍ}֭[W5]=L0SN9Jŭ[ /]3--wRl̙}Y% + <8...y`bbrͿK8~)S:_tIYe7UfOOϞ={t5g@@Z6lлwexСғ?vvv'NTdժUڵkZZV;霒%T^~3:v(b퀯^:b)u@@tڵk?+W| q WMo_3 hA%ۧB%qŘt钒N߻w믿֚޺uxbٲegئ<ܡ_i-Q\\\uٽJ*Z5L~R̙38_} @.n$Rɇ@5x"! w(@א!C2.zqr̸fĉC8Yt=s4[V_vyllDA$h>P\WI9EZΖJU~3gμ>ʬYBBB~NMgynFm۶jժ¹sb,&fsoZZCIO,`;###)HLLE*D~r~C^ԨQCj,5 #Glݺu߿_z٪Uɓ'hm+֑5kf},lctYvlKmٲeʔ)W\yUh@_/[3 ktVkW=EDDogΜ|rHHHhhhZZAzӉN:yyy}󲍍yzRJ%&&5Zd)k׮N:uupp=}tw4#ޠ@ ((@j Y#¥>/GIKKqFժUMLLlmmJV\\ܩS6nܸa1ŋWVmo޼QrڐAD)MK'͵?yD.f.;gŻ{쑋Ofʹɯγ=c^} ]ݻw:ߺrJHHLLL2 sرcu9Vܼ}:oo-[̚5KݸqcN\\/]E={hTYεVZEEE[Ng *ܽ{ӧYGv{nZP>7J~zjE 'Qt III۶mb_S0-QsW---rcǎW֩[:""rRٵkW)o2zjf/xb:V ˍ3Ӻuki̒ 6d()puulPw2){UVŀZW>v=N/rẁ.:iӦr &Mʸw}7gkkku_oܹTZڵkR8'mݺu^^^Z&%%+JAHH[МxÇtljj:bĈZcǎXo8p`ddd㔛\5Nپ}w;T[.߿h/Gcta7x`N?3fX(4iRG0aBTT?c)q֭ZCBBsElR-_|!Ç1bfUlۮ]~2;Hoppnj#zk&MהNxWN:j'''BӧO?gqi`ɒ%_~<YryI/Ũw-kѥ3f@o6""Bsa~ +;;ӧ_tI -nTҔ)SfK:wXF\x1>>^z?&j9}߽{7***Ik׮ZjիW/UիrA˸87oxѢE~~~ziKKKq=… 哺|̚5_^Ml>a„4v)6m|F6mjkkr-iC6lPrܹs͒(Q⧟~rvv~HrNDԩ#N$((H>ɍ7 -"X^jZ9s ĨDbbbDεڀ..J,)0`u@rY)hٲ% o,Yh"[l++{O^yf)[nƢ޲e˼/L8QHZh!ժUf\rJꕷiPV+Wm6FFF6m_I矚+[gc:t(rWX133۹s;wRSSO:%P( p̶Yϝ;׽{$._7isȑݖ -vQ|rsss14'NhaÆtN #O,Y"%m۶;wѣG)))իWo޼8Bl'I&oջvuVbbbŊ۷o?pf͚=zTݻw?~xGi)]￟kQNEsy4ggAeXk׮s؇D5k8qB$m.\WҲI&zHKKbɝCCCqqU8pݻRΛ6m&/Ν;b:믿={kƫo7>>^ruQOq-_ռys##2ep7f_XXXʕEרQCOG qtt 6d)ȣuIq߾}MMM c{5 ƥKիڦ4k֬F7n_CBOj3ҡCUg۶mRШQ#_ Ie#RjZ=n8"EN4m666| 7n4nܸcǎd Ѐ%JL4IVϝ;ldȕȘBXx1@ihX_"&!= vZ???oooӧO4({As mےrta1m4Usa~ xI&/_(ׯ~Tnܸѻwo.{'O.SL!ъ+ZnsN,DFF9f͚?aq.\ʬq]v ོ 4jԨnQ&O|[֪Ul@ 3 -[j(|/[,99SN'NFմf͚%KKcS<zA iL*0 @>RIE*= 4joTD?IIIomuݔJev'}ѣIPQ@7Baee%$ n&&&;v$=P P 6jժ@G@h)44om 薒r)vssS*a2@/(@E̒%K}c4@ckkR4@/(@;֫W<@_.~9 G z@r45lؐ<kh^P=-99yRܹsgC@NQ@KqN(@Sh^Ph^P1)PD|r9y$%&YbŘ1cD~PlRoI}5!! g(@:ڵKgYs΁ ݽ{wݺu6665LMMsҥKgʕ ,(so+'!)u`)P4Bf_@P(@:LP+00P|uvvV(L~>|8))ITr(Tzm{?.@' Ѐ8 mx?ӭZZOGƍWEh} rss,gÆ >\z}\]LzkNNNҥKbG޾}ŋʕkӦ͐!Cy}ŵk֭[w;wZYY9::vuK~汱;vxg6mػsqȀLQ ނ"V+8(RiE* d2 ™b&2g B8>)EzԨQC_~+V{^$]謟y晤H9mر&t9YYYEF:묳z,^8l4o|1=; .Ze]vjje| F:ziӦkqȐ!aJ*-[VG}ۿm6iҤ'M6jժբEO?43eʔϚ58 )8^/<`Guڵk.]6v;w.wuɕx 6ܶmے%KoO>[:[ne׮][l L6ز{B?u~ꩧ֮];))\zG|mڴ β)a=Gο 6;ZA.ݭ&}} F:zjԨcH.57xAy]v]x~z衢w]?ޥKCѣo7|sAs1vfA>zXbW_}ջw_=++O>s1Sᄚ} 6Eˍ7|g -z 4xꩧbǶ {ɖ&$$͟<} pŒ/|'pBn \1y`Pο옏= ]vYp֪SNLi4K/g; !A=sݺuP SooZy睺uF$$$ׯA;w޽{رc{QtI >ފ[ hѢO<1Ν{giiiÆ _~M71bĈ0VĉƳ>8{ׯoKw[j5o޼v-\pǎO>d)@_uf͚V+.ݭ.޼ys)P#o-y⒟a1 H@ny llԨQذaCѻ?ᢋz. ~'e˖aÆ@9׿}О1cFL=ܓ4~>G9r֬Y6m^ׯ`_paЎTpŬ 9^ZJ yuk4R_ e@L]s5{n kqvY:u[~{%\pM,QLfڲeKhgo݂W_7o߾^ >`@ʕ{e&77w0/oݺukX#st,1wZR-7oyuqq5(Pjey{XP)))'|errr>?tx7b"O[n]$tps0>[n] `/3.\>R⤤|EV.ѭ6233}.S,P:5ZlYoK}Ⱦ+gIuօ&MWʕӧ2dʕB<И֊T;GnnP:u8O 8pĈ 6ƻǙg)?$%ET/F:uJ}Ⱦ >|xw/ s̉dr;t0tN;@IW\yӦMA;]vnom۶e=!++k֬Yacǎnl(8!8̝;woo\2g=ܰ1uԬ~)5VZ|"zΙ3gB@]^x!lOp ~:ワ?>{c?֯_W6ڧ!77w9 e Cرc=6:tPjR/w_n_Dυ ֫WYfk׮lLLLܹs}ׯ_?nܸ իxwƍaSN{득]A1Jcc.r gz 'pڐϚ()%8% .2eJn l;vl=&D/C=TZǟ}EJ* c=_~{v۶mW_,YRn[~{0ݻwO<č7X`߬ݻo߾&$$֟~f͚6lX6l0.]^+K Wc4hиqjժ5iҤ7ٳ_z饋/VaWO+K.doo(()+1}_~4hP1|9x=$,Zn۸qU.W߳gϘ27tSΝ?VXq秦ڵk_xm޼ݯ_=x}4~o_fEL3l9;,.\2?ܹsgǩWayk%ȘO#l޼?իWzUWOp*૯ںuk(98|X-{oZZZW^ye\\\jZj˗/޼ysnݺC )m۶.y5ڰaç~s/s%KD|ƍW^Ǎ=uԠ=3O5k_F;'%%=M6-h?cǎ=䓃YO> TZ5oc.s~+V G_t Zp hGuTL^O>^A|GmѢEa˖-yA +U\#8".a66|z^$>-3<{~TҥKr{c|Ǒ>eϊ98Q+հabZD⬳ 9r?^`QGաCI0W`O>wܹCÆ Yĉ?={^|SLyW.]oPaOsW^yqf͚W_eff֮]W_}iXAJ*-X /dɒ칹w\pK/48ϝ;7ҿzgyfe\\\ bcǎ?7|sG6l08B.]ڴis 23gΌԎ_tE?Y:"~Bܹ|}d?Wn@ilR@` Dۀ&L(3nݺu-Z(3&hz.7Z@II@Cիz ,/s9ˬ<żyV^})k\3h k!hXsss^ N8aO.66'p)g.?A[@Ԁp 7?~رlܸqT5eʔ믿l6mڴ%KݻGDɯù?O v}_o+=׬YP׫:y䜜V^ݭ[:kw}G]./h||1cZj5p3gFDy߾}'MTJOO曛4iR68H@F8}QZCݻoٸqԩSv W^駟>bĈ`ƍꫣt={4cǎe3>}dddUV-6oVL} >CԀ>H]pիW/jժ?A^j:e{ݿ+VD=f:w} ^|ŦMR>ْYfݻJ YgUF0"|e|ۿ;u9/\3<3r y>[9b>3 ?vmB%b4 *?[n7p!=ƍϔs[駟RSSJ h(k=С>s=ץ{ۧo(+ hB *$ Bט zrlL:Dqb[N %% so@tu}1C6 D4Q! @T ڕ;fIVؾ`ժĔaⓀ΍inh4+,P|JpD4Q! @TH@3%%% 4Qɱa~%Q! @T(5weݺ@dgg~;/lK brrO pED@IKK׬Y6RRRD 7ܰ\($p;vGqDll@H@@ᲳgϞt钘(&P"B@TH@D@9rd} @' 5jD)))P)@TH@D]v͚5'##cժUaK.INN(+ +`/GVо}H[@?\ wd gy8@)@TH@DP  b%" ywv֭% *$ hB *$ h>[Yfeq z23Vl)l451)YL(KO/a hBP kEH[v}[t+ hRӹ" WJpD4Q4.+3 O.W],&P"V@V@3>5#PaKj_w}RR pR *U$~,i8=1Y_o]>NNJJcGb.8Y @TH@D\|NfJJ0@ w毛De # # $p ڟs8@Y@TH@DP]rXӬQ b%" ە3aʼn'H@@45)E4Q! @T(ܼjզ@dddlI~/l;d1:uOV@Dڵkg׮]EwHIII@9 " (@TH@Jp@ᒒH[@$pUV(5%8 +ŋOZ ' h~nݺSNI 9iҤ}Wx!4.77w˖-@II@5kO*### ͖,&P"qB@4X aw7ѓGa{لԀGӮ>W(5+ hB  f ݾf͚=7! ̹Bp[!sssE P *$>wC}r_sN*66J០- PR~z9,^PСC׭[׶mm۶Ͽ#+$;០- P_|ѪU38{9RRٳg[SN..T(37oԩSzƍW>fTF'馛ƌaS >R2{=z]v֬Y w[n}71M6]v J*t{JJPjJp@3fٳxF 2cׯƍ]hʓ9[6@II@CY㎔}UZoOOO:tkMy;g@I)q5j#KNN~G{뭷>S՛0a©@aPj~O?# 7/^ܥKȖ={N4ipСcƌ4hPf"T~ّGh=Y9Ò۪d''Ƌ p8>V ҉b7V@FD?e˖˗_JRo&MTz={ wg'ORRJݳF@ Ȱajժ=8;.Kp C >|׮]=z8H=طoߍ7Sssszzꩧy￿RJ>qH@FZl9a„hѢ0ZL[ӟO֯_ѿJٹsgx+ WcHz~_~=z M c 0gΜ[֨Qe˖& &O3 кu|`E};v89kw‡~8a„ [.x٠A:x>h߾M l[qWL ߺu78iҤ4c=?z=3' 6azܹρ]v͞=;zpڱcǠA~ԑ}=σ>8cƌ-p^xqh޼yq5gΜΝ;?Q ++yy-Z5ꬳ:7ѣ`ss&%%ծ];55uٲe W}ݴiSZZZО:ujqȐ!aJ*-[VG}ۿm6iҤ'M6jժբEO?43eʔϚ5?\p—G}t̓~k֬ٹsgd{sUW}76llپ}{0b{g޽{zuYڵko rA<:$&&p j *1yp~`u?ꨣ֮]t`$Aܹ󞻯X"^1OszꩵkNJJW^&6۴iD;.]Jws `_Cޫ5j^[bŘm0>}l۶}#Fȿe͚5aw;Ox \SL'n(x?)Tӵk0\ z ꫯzgeeG;s9G{C1&$$GΟ} pŒ/|'p=ঽ˂V꛳N:1ܷoK/t݂#ߠ(j(() {uVpuׅ{޷VZNݺu#[נAΝ;޽{ر=z(PNDFp Qb^{-i`G}tǎw@]zoƚ5kO?JFFFL^:g? q>|[o(xѢE'xbw6wܳ>?LKK6l_G믃5k,wuWƭZ7o^vyر'tjժ׿ϱt7g{%@XڵsO/ݻAv.6mڞ=6mS8lٲ%Ri/}%&&F3fسCXƺeUׯ`+W: _}؎T9P:ǒޜ 񌌌,1r)F9Od=78;w,zN:o=l,\Їŋ۠QZniozQYf33yyK.$>>a֬Y[l m[Vc;ݾ}{L^ M$77?9s棏>z뭷^wucƌt|nHR,Qd7o?u r'/[,''?}Ȑ!+W<ߜTRGI˶mѓF]v bp% Hnn:BX|Vx7f̘ 駨8Pq(Q8PN38J*eff6KGyٰa;邏Nկ~ոq5j,Ym۶>[2LmwP|e*##r u)kժ6"eggٔ*k׮}w>`K/ J7T"WaРAÇ/H*WiӦHNhs̉d;t0tN;@(cXĻſ9m}.J/? /6'[oҙ?9'6aD lX]Ǝs=aCUV-[jUzu==zسϏ?sϕٔW1bDp DƁ_|M7m޼98?tн\pa^T2wܺuAnp {ƍaSN{ƍ 7g8N8>_L-\pʔ){n;vl=>>~{v_r6mtE?))馛n ۷r_g:==,g}饗=z^*Uw?^hm۶Zvg}dɒ6l0wzl9O^+  :ꨣN:=SٳgK9 пկ|.k}_~4hP1|9x=K,Zn۸qU.SN:餘g֭++$cTEpL>=QfQ=C{ns}Q+V8SSSvڵ/G8Sc+<6FywDDV\ _ܹsϪAy͛7z*iЮ*YW_}u֘|UP8)W{oZZZW^ye\\\jZj˗/߾}{͛ u2dHʕu<;RJxGcqC0>D+ Ī7gYg{{5r=.L>=luQ:t(31y%\O8pΝD'$$ :o/t$gqFݠA"ϑS={_扼۴iӾ}|1yE_۵kY ro>14o޼~!]veoFEr1SLرc%ɩyaÆǏ<0b5idժUK.ڵ>g=zʕ+o۽{wp;:h"99w YYY6l=Pxr);{L@Ǝ3&Fpeyys_[lYF Cޫvڍ52s__͚5[bŞԩcǎ<Ȝ9sRSS5j+byC 6f͚'N,4i;L6mɟ|I0Uz꩗]vYϞ=w=vI`1yؽ=V1+_^pa0]h2CD9ϔ)S^y啥K~I?7]T׏RΝoGp?7|sG եK6mܹ3fΜwwK 3SRRR _ӡCޜ1Wb=,6vܙ, >OOOO?켝:uj׮]~k׮W^/5jXya+~rJ!33sҤIa+سkٲe-)))DO ˛5kv؆ hN;QFW~Ww^?wծ],g1i۶mPbcc9H[@j*E'm N0θu˗hѢΘ;f̘q뭷- PRuz^ ?1̙s'eyy^SNi׮+@HbŊ>|x.8?y2aٜ1`llO=lTVmСtX9Gѯ_q]}Q:KϞ= Աc2W>}222x㍪Uʔ79bV, YYW''' \rT^зUv aoݻwWXSqx1eyy{ajK@@II@pQ45#E!D4QprUѓ%BSN:DX @TH@Dڵkk׮"MIIC8H@P\g}잛+pX%&&3() h(\\\\5Jõ D4Q4.33sڴiacRRŋPV-Q"$p鑶@nݺEwS(JpDf͚~8#vp%'' 4pU$DONveފOB8M\ARS *$ 5?!5k"DP|sn,nٻB' tlH[@$o-_>+VtM)*$ PXjj9眳xC}"C]n]۶mm' h(S_|EV8{PKJfϞnݺN:eff  eg͝:uW޸qnjjԨ1q7|nr}(@ h(#wѣڵkg͚PnպuoyĈmڴ\hʓݻwl>lqqq N(5 h(#cƌ={>ڨQr6!CL8_~m۶VkM;'{ۯ/$&p8tl/SRRJb.( w۷o7U~Cuy}=ߪ_믿~ '"[zzM0SO=܂pOH@?f*4HMM?5\S?9||믿?7L4z={<n ρɓ';6)Tҽ{zjĈ>hL/6;(z'#+gXҲ}[x1b/҉bw]bm۶{Y&&9x%:ĉ۸qzrʥU\'wE)^6ovѣAnܸ1---hOY멧zgJ*0 ]4mt؂t0ZR'Nܱc'|xvڕzT[ӟO֯_чVΝ;'L4⊃&^c=# wGNOO{4E,U13g֭[kԨѲe=Ӹ .`SN(5i~/ʉ]]ӦM i&66Oɓfee%&&wϋ|/ ( ~Z?~8so߾dɒ>Dx4bV^8T ŋw%6{=w}7nܸ^{mʕ7o><nݺ8/_<}ѢE_;;s9k~?|xYm{s /p _qsρ|cȢC{v\ v[A4ڷoiӦۃar-E(kW>PN3gh"h~wc+V'\SSLOwxpk׮ݪU ,;v2괴ѣGϙ3gժUZ ӬP®]>䓒&СC}S(Oy |6mdffF6+3g|'['OΟU\nݔ<ݺu?~|rrrF_;mSo߾Q|בnG駟^x"3$%%/EρcԩS+~4~뭷>Þ8qb 2v .ʞA; s>g͚U xѼy}wΜ9;wBcVVֿ7CpF:묳 n;33'Zve˖ A۷MbR!C*UlٲZj}Qjjjnnn۶mۤI"v4iRبUV-?Ӱϔ)S֯_?k֬{.p"bDQ{/Qwc j5vAԐ[a{h{ V {{,˲E𜝝v9;;sN"\rs̹x?,fffիW|rLL̀Xs>~(mԩqftȑbmb&10`l"J"ʚ;&u{5sGs=z2Em >v:S>|x\4iR`nݺ#""޽{'MQ4,Y :TkUoxj*%\jUEKKK?VBS~}֭['WR/drzE:`ɴKJP4mkkpBYV&ꬮ{Μ9˨͛_V5k ,hѢm~ R˗/5jTxx&V˚ǧ~(dɒ׏Vڵklڵ۰aClvݾ}{Jկ_?=ptt\rXfr^z'O1c4m|WӜbn( 2dŲ;;;e… /[,00pǎeϞ=̙sU>4Q ٮ޵8~f̙3Ǐ ڵ[-G?O<";wD$Q=58SN&,n s3R!ܸqc.]2 0@vٝ;wPB !C8::n:&&ӳ{ZI'ΤIׯoΘhB&˷nݺlٲreWݣ+%~-^X5k֌7Nݝ;w5ۤJuY53駟dʕ+^3fhfYaaa4]9f? Dii)-"BCC/ުU;v,x{{:/AT oy6Qwg9N)vju=ҜSNBΝ,b%VT~~~?Q b &/YDY&Mqi r%eqwN˗yftt+W5j .\8Aӑz]l;w1bǏ===5Wc9T7L?Sliiij#-L3gN Bxx8Mi]DT ܾF̙̉ c8NNN7ogϞ%qUq`.6++nݺ-_|Ϟ=_Γ'm6t*T˔)Zm߾}񾾾i7ʳ"tޫ;*G^ 鏳3AHL@/8v#""֯_?buٳ&lN†?6o=Aeq{mQKmd_ӧOwڕ+6t%Qعs[r%J_@w͛e{WܢEr999xƍ]/|RM޼yc6w܋/'Ov֭G͛7;o>9ϐ!CJ(!ѣGϏ=^믿ZlٲYf[z}JE4js%~~~gϖ).\ W>q[ [&?eʔW\Zjo~a*UҥKÕ)$VuJбK HիWPخغxi&yk׮U\*8ӧO-DVُ?hu!vJ|e&>JGG|:ܹ;Ŷ6mZ|bŊ",ԬY+Rizɒ%-Қ}vYȕ+Wfʹaaaa[n2eľsٲennnԔ'N;v=QRvttԙ}kswwW^.=z!. 8 WrHMkׯ_+s.\ tP\…\Tɓ7mW`/iM+>|dɲ~'ODGG+9\σ Z`EϞ=-ݖ[B]Aѯ_!-Y4%Jܽ{ڵk:t?˳g.B#*{9*U$E9""Bg͚*Uh/W hsss٣Ȋ+ &{S/g TnܸҥK=ZrݨQ *-[۱gƞޫW-[Ν;СCO<177wrr0`[Sԩ3k֬3fϟݺu:)^xeM255ҥK2eʕ+0-Pw}gaaq1eX?1sn4Wheek׮͛7_|իW**_|ժUԩSڴi*͛Y[hw^vxz)lTt7o.ZHσ ,X~}鐡]v.]2d<+$%{$q "ŷ~kggg[n={;9DEe===/9s+VLmۊ~ 1ۏ1az ؊_WWf͚%h[ 3LMMe!444K,r]>}N:&[憸tR̓VZ5nxȐ!)ŀ… ½{۷o+WN7nܘc`"#صk,o>s21!C3q֭efPd5ڶm~f2eرK}NNN?|֭Fŋ d|d* 64^Ha3g @: )U\$! cu&Aˎ1B]vmOnݺs|H#QT+VQFZ@`oF| AO>UڵK7`wyzACJ*S=zUTiܸ1 h@[hhhj6lXDW)!!!()[N fRfϞ/64g S`/@AЀ[nՇ޾}۽{w???1eЈ 2hР5kxzz/^ܨe˖~)"m۶˗/W@=ɮbŊ J%Nlmm?YL@N2E՜bff'Os9vq)\\LL̇Nzʕߋ鮮۷O:WXQvm77o~zKK˔9dAAAÆ +QD 1cdBLH^tE&n߾]lll&Nȱ%_|}YjUddبQ $eիW7oސ!CV^-oѣ{˖-S쐹)))!RYAEԩL֏PE5ʂJ"< p4(H@4(H@4(2Ho~MjK<)SӦM2 ׄқyHwdLD Q]pHn\69qāW`F"##eA! nϟzꑀ H@@]̿_?V>] *EJE|@ 0#c  0   0 D ]z4,^IL@ # W~Fht˒%˄ hiQɾ CA R;@z  0 @={r۶m3gΜU2Ǐ \ +TTD(H@4(H@4(2mɒ%ּysbHOH@`t@ ]p4(@ZոqeOdddܹeA! nM6%$]p4(H@>-**˲\ZL a@?S+UD Q hQ hQ hQ hQd"Ҩ#w N`<a_מdBL TAH$"en?*e@B@7SSSDhQp4(y@\o=έ9$ hVu|+*$ ht?c,7؃qH(RB>}TbBQ hQ4hSqբ=H$dD C,`$FAҪ& &00{/dO2WF˶# hQp4*j  E2[Xti q$uaXd.r6U*Ai}@4(ݢ=y!ŋؙ4EFEo9|ZmO Qp4GG~Dr ]p4(H@>U/.I`HSSS k5Q Ʌ4HӣbOtpp0z*$$C qT*UDDDU|,-- Kґ@̙3wI)Mj2|fcߌ3 "* t$̬PB  y8)Rܜh$ hx"<<\yiffSBCDFF~Te9Q[ޞ$ hVEGGIy9s"EdD@U޼y@H.ZM  MRT/?@r:hm t ʟ?J>܇ (,,LhB4,Y;F*WϿ%0pOd@:OhQ]pE/?~ y .Ym2Ye1zWR"h>}ٳ5j/"**ԩSYf%HH@Eb3*ŋ|||z[z#GO?իWH[:ݻG1 3f̙3&_|9_|z={Qzw֬Y#GL߼yiҥK۶mx⠠#F<~۶m7n֭[nn… ,x!GAWڕ+W6mk!SM64gϏ>}ڡCǏ[XXĵ`Ϟ=+Tpe˖=zHNž 搀RԽ{j׮]FI&XYYjժp«WN5]nСCWX!F:^Y.ռ%15UՉ 6lXrԩҥKe˖%iѾ}duݰaCwԩ;w)cǎXb&M \CϞ=f_͙37uٳO8{]v߿ȑ#?~SdQ˗ʜwsٝ;w߽{_~'OTfxҥK;vykkk=ڽ{ݻw+Sg6n8|p]&iӦ%ʢ=o۶ò= bDıSfy󦻻Y(.j$ 4x(mv,Y(swA8899peEm3ۗE:g7o2JJ:׭['V[xӧOgϞ=c6AV]J3FSzr}ԩVVV|$Q˗߲eK֭111]tx04dy6jԨJ*R̬Zɒ%uP\~L2{Q&^Z\ʵiӦm۶J;&4en޽bo ,ؾ}:3PB޽xfΜW&Qٳ>ϟ_W<==?~{7n4lҥK5{?^4i|W^?T22?رΝ;e [ܹsZ:trzz\]]!x… \r9rwմV'Oĵ 6 4hDqE~Px{{FشiS|:u(Q ׯ(˷ܹ+Wܹs+PW9Aܿذa"2b_v5zڵk}}}=<S]Yǃ-[mV~+w۷bZw }wbb,6ھ}J*6m*sw)w^ssChGDD鶿6m޼ySrsjUxGXbxx3gz!.T~R\~O>sz}ٟ?ݻÇ5o׍8={0a%fȐ!͚5eW\';mW^e͚U.[l Ȏ߽{WJJ}||"rjԨ!ht*׭[W0`ܹ)Sϗ8R͈ʟV\$ $vUJ4ig\| U  %D?}$NK1Mϛɘi@p C 4hΝ;=zHã 1__if]vOogI>kC<رcfWbӛ7o0`2ϯ2euݲeK:z՚o)^AAA+Vqh޼X  СCeߒ:#YdIo>^Q#""9s~ KD޾}۹s=bo.Y+l؁+W*g믿.Q,~ZG_W,:u*?~,}D5;v(ϟOh浚ﺹ4`\K~d'i;FJ]H+,XдiS} 84 =f2) =ܿ? -[VIk;wn…e900p׮]b ug:WXjU|ܹ*?&[nݻW<4Q+E{]̺l߾W2e5$&R]pFRG N7]˵jJв3ZC#LͷnZv[n)SLTRS̙S 3Tҙ4Q1 G<~رcrm}xE\oʕ+qWyk6[[zɫ7o>~!d׼_^Ob۷oCG:Lr\CBBO9=}thhhf͚iΰaNh_P*M@={m۶٨nݺmܸ˗W>r䈯+gg#Fh C,~ĉG}9_||M~p'8-'t[F9l\ܼysŷq㔾 \|C޽+6bgG>R׮]t$-[-,,Lsb |[l9uԃlvvv79rdv|||Ĝ7nD E3g\`AqNjٲ;w?￟>>o8Z~9ȑ#>~(Įs9Wȗ/XZ(ʚ;&u-^ѣGkM={ BCCݩS'%.Q/ϟ?o-' e=VRRUV\KKwݿ_\#Y~}֭[UׯásUJrsYiŚ A!ۍ=h@lRDKhSL@e9H7jԨnݺ.]~GI755-TL@gΜYNTMhG%88X4oIx e8|ڵkwݻ߽{D?'w…q8JжqȲ5Q?.oj޼_jժYf_`-j۶@,|QF$q'M$~ᇉ'j Sa|UV8::\D}˰L@(Oze~rqq%K9cwN4޽{^fѣGouJzSCSW< jH7-]-iݻwEeÆ }###eA B{D\Dp)Rŋ'+m~H=y## qV 6pJnjb7e˖xUNFYd 0}%KȞsVX/_>RоvQӚ$ )4p}e4o b[nRLMMSf3fhTk9::L@+yfTTTsݻw+V̐*>\!޿/{(Zo*GW޽{...z6y]vyf=+B6ma%>}zܾ}ѣGB,6p@=y/jժ{%KL`*K/;CCCwѳgحA3;y cKK h1H'''}tF̙;wߔ=zTUj=E$W .Go>~xPgL2_)SZ T94YK;*y:`C~Lh; IkxMrC,YR\7mTY,KΜ9ӧO{+W.~eeߵJԼZ瓲ksu\o̙3EwZNnNRWVmԩ7zjŊ͋YlY\zL6OG)E쇏ׯ_suB Zj~ݻDCEɗΝ#!*Νf͚+Z\5k֟YP\p$?~|:u3'OK.͛73:qY(VQ~]HJ_{.6-˗EdK1@a]v}|D;we˖tEd"? OD{q.|_ҥK]]]?nٲE^RhbԩjѣGŲǏn۶f͚+V,W2h- Z}||&fм)vz=O |PsӳgΝ;w5J.5k۷o={vڴi K.-TP*Uʕ+'{C +VիW /]x@ÇY;œ9sΙ3GyW,Yvrr?j֬)_Tk׮y{{ϟ?_YߴiSDt"!Bqyq8m6q,ĮVPAѣbeŠA4z8+WwQ\:88ʕ},B+VL>b*aƍW5 UV\#جfnn.kNs6OOO^q˖-Ǔ'Oh&MJH h~ R^=I{\S̘1# L_i}O>U֛7oΐ5trW^Qvm޴iAp y'UNj*&F@y_x$.-Zh„ Z9 ,x+ƻq]gܹs,YYHVZ 4H5dݺu֮]ۻw9 Xb%ĪLC"}7ݻw~ 2wzꙨ4W7n޽{+lmm˖-;x`=}3g+WzYXXXEIي|lW|zs2e4id큁_zV"^ sqe }G6ٓSz_qͣ=zVZշoʕ+֭[ z%YxyCG^$JbSQj=)f^ѤCԒyyWʕ+cO쥦gIPAmر@r.\ly1税3VK}hfȜ _s-5C:9rD4iRn]uҶe˖w܉=OLLi5e ˗/o=UsUtie ̔i* c? 0PuuuUo7Qw{'N3͛7iûxa oo֭ۍ7Df͚:.T,ZJsǏS u^4Z~b˨MEi@J }$B$(fpww]zl7s6665j8pMX#U.]XWe oo48tPH(_t,wAψ@eʔS#իϐ:/a۷Kq' pz` 80#߫gLjT:;\ҥKիW777睔̖jժqIp {999i+o.W,oܸc` j\K=ˊ ŋsTrz`ܺulٲ6믝߿nݺ ,54lxg8\x20#Fc0ۧOnݺЈƠRdޣF@" ]޽CJ*S=zUTiܸ1GJ *.8˚5ǰafϞl2coNR͙3W^)VAEQASSŋd`+V{PB  1c,3H(R A*Uفlٲ%((觟~Jm۶}Y&yt``)S_͉...fffyȟ??)|Ŋkvssۿ67p[ZZLՂ VDٳgpTM9`bCEE\!_n6ɔYU >K.]h۷˂ĉ9HaիW7oސ!CV^ݧO#mG-[Lz$!HZA%ŷ:[GUjRUZgiQEݡ-*q D͵__~9V&z><ޙgٙϠ8 Y0EPTy|[*& u3g C *"FI,,, y4@e~Я_?KKKc@dc~n={&<(xR$@Ο?o h1<{o|yL",,?Vnnn>>> 6$-0*H-͍aKBBB/\駟={6N:^iӦ|a$(@\AVׯ8qFiٲezT*UHHHTT0ɓ/cڵkG` (@:8a„C.X~DFcǎ?%%%55uĈ7o޴ ]LHqqqcǎ]`A@@T}(ÇoذA| ƀ4}ݨQf̘Q7|Fb|ucctrڵk>$oPա/Bbkkk2c@`՟ MF .cKKKHȑ#$%%71 @ y+B04;wLKKsqqٶmـ h_߬S<@F{=} Tz(;_&/::ҥKnnn67nȕ.0kR֭[fHVV[*&M$$$~7vvvy8PRmݺ]`$(@ Ȅ!^bE``'|oԩS'66s̱;qDHh@Zl9vX???idd\B\ZN:-YdffT`DPWqMMM *4 q̙ΝvtttvvW7#>8QpYYY{cOOO ~.(@Tf#BH*3BaiiI Ȃ4@ h,(@dA Y eaa1a)&!hՑQ72I#rFbH$@W Y0SJ᎚f)JRP"(\VfƉ~C-, TX䬢h4D(@P8F,>VI%YPȂ4@&Ϊg˝ hFǥVf)uBh4۷oߴiSlllZZaƍDrJ"##cРAfڵ79r7ow٫W/R=c hX`AVV고{cǎMNN6lؓ'OH+TT-n4L!J]b<򊕥9prn7=n{mڴIرcǕ+Wݻw„ d iPv 1 `ⲳ7o/M.䂾q407g@gN$'';;;_Y_̟?РAyo9#1hڴ)h,99yرO+3g/믿'zF*<26V*d P||իݻмyVVV%ZՓ'OGr = zU{ h]9qnOSN 0oРAm~;11K.֭swwWX1qe˖>|A|'O 6jժ[lg NV?|'/01νJ,--mŊs(`܇8p`NNѣ7o,[ݮ]!C_۩SSNկ__J_{5a%aaa5jxW((\fV,- *-[EE(;;ʕ+]Fٳ7n=z400ɓ'\666vԨQ999V{Dq R1zhqX888\|Y|3wիW6nΟ4133c `Ymݺu̘1UrK͚51b0%..8Ν;7>>^~U[V~={v׮]C}  ,<<>kӦ͟Yn]@W WWWq(={i&1~ m3f̘@!XbEq Т#GΘ1#::w9qpT^I&j*d~J%ժU*tCs›/|>yG'd]!Su'3`;&-Z({DDFƏwnppСCmllvޝo%5k 7n4k֌lCo(@^)k6UIfȅAF4kذaDD\x1_ヒ-[,]4הּ41ptt$'ʙZ c5binHHHYczFwFkdD(g<իki$=7o߾ևnݺuΝKHH>+g zR1y|T*U՟baʊd{aPѹtVKKintttNN4kȐ! 8x` ˗/_{ jhHbV^ѬP(,Xо}>}XZZ޽f/h]%暙͞={ڴi'O}vNNѣo^jUrA |+*8 ;;DRIJKKbmmh4R, M\$4355 zW2o @Q^׬]VVs>R/b̔bր 3{{bLOOBakkK`(@nݺRVLHH'''E1(\ffÇŸgϞ JN:IIIZZJF7> NVI1  SSӦMq||b@hPb'ڷo/oݺUT۷oI JN:'^{5kAhhhQmn޼-Ɲ;w&i0|S޽{ ;w OOOoooG(փ BlݺU &OLP!P g%K<{,?Cycǎ%]@EsJcfeeENTNNNzz'''ko9>,>>~ҤI۶mf=}tĉB`gg ֩'@E55Å2&feܺ@n9kjnINTHIcKey5Jw֭={ĈfΜyMwww;;;5ӧOh4fYfhhVv}5i҄O2* Fʕ+$AVQQQ[.tVjj\9s̝;`>lРABǏ9RR9::6m??~ IFBRhʸ~d @Q6UtpR^y2cCh gjn@4cXfҲWi P!$? B'ʅ )/66~['P4o<СCpppEߐyEDDt5%%݊JId%R ݻw۷o߶mۯoC"""훑Eμ{tOI%Ѐ$&&^ظqc"[veҤIׯg`Pz- (8q 4'jzĈ=:x𠹹yٮN:Mwvv9r!$ѣyς~Ҳ봳>| .]l2lȒ|T9 - ,+ Sr2d4… T*ULL̉'P5,^8;;{ĈrA__x q@@8QєqcƌٰaæMϟokkˇ a\l"'Or!==]l?j(I+X*@/,966v޽#F Ѐ]\\+t~  I)0)`3RI h@^v;wP(*?駟L >ƣއTp.F@BpuQ!ر1lxwjjŋ_y-ֈw+;Tѣem)&!Tb [>|۟rH* a۫WާO@ ꪇ.#3gxzzqϊz'&& %+[}c1fnfBNÞUOihf|=<<_lY_=v 6 X[cNJkbbK.四ߊW_}v,޽÷mV˾ӧO乴Y. >zϊ׉TL P5w(NM]MR$%ih3~ű8۷oQfΜ))?SGGG]gFswEFF&''C[RSS5ӤIZ#*} ƌ%>|NX{3qƌ*υR*͛7zjNNΥK^2UZw={?~\^q={V \-ʾX){NhϚAFFne˖۷OV g7^paF )pqU! .ݻwϙ3͛%]0**J Jݥ} ffK[t*uXrQC>bgʅ EvHJkѢž}֭+L9rH=y*@W(@C4N3uԥKJO4hꫯ6iҤiӦ 6tqqxb׮]2nE x&ejٳ*Jv"п^zhb/-[̘1A(@dT*޽+~ ۳gyKHgAnnnbU[jժO>jmueggk[=R%?Z=`Ji& ÇI4J~{7 uׯ yiرC }ݍ7tW^yE :h:((H5Hjժ% rou=z7(0Qr{0j:>><899Άoǎ!!!SL]vmׯٳg+ꫯĠgϞ~ Ko߾Erq흝ccc#""~#Fl}v _ ȽeqƢ %ڳ֫W?Ǐ:=<}䝨P(g4J;w,XW&b֬Y-ZyfB5h@zȑ#E-nii9iҤٳg '|"Vb{+m˓'Oje-VKMqF7776m:t_-X~G,@7mڔ?ptΝ7o AÆ  hظq{%>[`%Kħf*8Jɓ'zk VZ%W6m4WFϟ/]}mf>"?ɓ':uڰaCY &M_oc jݺuׯeٷZ{nqG$&&3ի%ڳ󎍍ٳU cJ!>>~AAAKJJrpph޼}]-w璮]ssspp.#,޵k/+\Pbcck;611qrr3?kRSSŹuԙ3gNժU 6%%w^^^? MOOg[\xQ>|(Z~+7h4 ,XlY˖-*tn7궶[4fa={:w-\=4hФI}KV$& qb m? ej+W6nqFD^KRa+-b SN 0oРAm~;11K.֭swwWX1qe˖>|X!u>7nK. vvv#G:zF2[zʕ+Mܽ{TVgϞ** @6m*U6%k׮:ujzz\tssy͜9wҶm[)_sG\ȪU._\V u| Zlieeu!r[h7i{ᅤa7n;y*(nح[7"߲A liFܹ?< [ݪUMMMsrr ^jb|/ۛP9XZZ;<(+V̝;7##Cݰv8pp>z͛7 v 2N::u ݞJeDDѣϜ9Spqw޽{o۶?߱cD2{W[{s1/U>UE^|+V5fN.-k055}+WsCCCʾC.^8888!!zn$횾} r~ʲg%=?Nl֭⏕͛5k6baJ\\\q;wn|||1T ZV~={v׮]C@*:w%߬ƍ {ٰa zCK/q޽Ç>\oڵksKqHv*l/_~)t7CqㆃIܲoq 6m{6cƌ +V,@;99Ah;:: wΝM ЀL2q-[|ڵ-[m5O?e0:]T:T=W)_T*UwyyyG >| /[N1ͷDGGO&ۘ1c;6 ,e߻wUVݺucձcĠEEqtttww{)oo0B{1(]q(5 Ѐ쬭g͚%t/ /?%ܯ(lBXjґ5ըQ#-6l(/^7C@yg1'NܼyԩS ܹ366>ڵK;v; hѢEftA>*ĉbܶmۼR/" 7f͚p"sZj!z>ĉҥK5g&3 (Қ5kV\oݻn޼yd^~K~G7n,Ve7riӦGo5n8Ju)2%o!47:::''4ݻ9rÆ ~a ĉjhI֬Y믿NJJCBB>v4v7n-/;o>}(OJJkiiaa!j:11x6mСɓ_xK.ϟ??'? (O7Pvk׮ѣŻ> ի;֮];,eQt[l ,aÆ۷oGFF _|qP'gfyӧO8q߾}z[6lx^RlqZgdd<{L|C+J @'ҤDKKF#Ұ5EbQ(@P(-ZT 6cǎMTHSӪm|r}Pyk.++KH )ʗM1[fffJqժUI hezz( [[[RG(gu֕bZeBB899i-0|FrVNsss1NJJR*@7jԈQʙiӦM8>>^KH1 ˨iu >o>$$DnݺUT۷oI 02iT*I}z֬Y#EyfvvwܙQpQŋŸM6ff|iQ޽쒓ܹ֠Amۛ14.1X[[vX҅ +䤧qrrƞI&Ӊ' ]@@ LffkT*U\\ܭ[#Κ9s͛7}||GWW|O>]̚5?::׷f͚}XXXڵפI򌊂4؅' y҈,--*RI>#պuBg%>3gܹs 6 $4;~ȑ#U*cӦM?ؐdT ƮYYUi4 9RYS~d` h,(@dt<#I4@efbbLOD r h?j5MrcW D(@z1~T YPȂ1TT{|}NdeebmsssrO& PQp&55UI%Yp4Ї/Q{RIP0qDY0)` YPB!Z۠D(@ 5 ۠d}5)B:?. 3R`%j߫W/.>>^(#F#'yrR(pRvm)&!eRJ[ʊ8xPjFIcooOZʂ4377o߾=y0dVVVaT*JNN Zö(&Z,0,(@JPԪUÇFIÏ*ʈ4[n͚5#y֠ з= (#~*6mnnN*ПP}p999w eccӠALr,,,---yCW(@P;w3(@8BaEѨjPĄ(@FPpƀȂ4@ h,(@dA YPŒP( wyGI%E™(J@OI@ c@P̟Y}() * )w ܟK_|EKKKr(U #D@E#)0` ެ]$JѣGZ2g hɳgH@A@ED@b PiFwմi-;\xQXɖ-[J,^.K߻wOe޽ Bnef{-*J 0 ^8;fgnܸ1tЂ #D@Eboo/M6m֬YK % IV˛t沯k(K rt2333yF!8 h,(@dMwySNJ+΁Mf{-*1ؗ 33Rh4~_vU  :CYPȂ4@ )P8;;ϙ3GI01d Fs h,(@dA YPȂ4@ h,(@dA 3RB믿BCC\\\<==_yBAf¾-Z|\DR߼y3))VZ͛7oժF֍>}Zxݴ:ux{{7i҄ЍyS} 0ۦN0X*k׮Ν:~W_ O[lz˗/u֍tA"|ɸqZtEf?~駟Ou֍Πf:vxm3g4kLnbbҡC,aɓ'_~e Ѝ{p>"]naaѳgOa+W:Ժuk@F{葜ܼyGVVM.7xԩSwٻwЦVZs`M СC^>ܱcǶlR/^o>]LUzuE *y9rD^vm=ztXX|Gʾ \N>K.gnDHVVpJ'_~s-V#Fx m70lذt!y皙-[LP x㍔C9hʕ+7I 6ɓ'~*j(c@qԭ[f͚b,8$~ |،?Jo` ЍYfpz"vvvcƌ)xZ*III_~% m7W_EDDAÆ {Qpqoo?Ur  - Իڦffs3ջwnݺE9)-,X m۶]vm,'N8~8FtՍ|bܻwoKKKo۶M8=at#F֭['ƃ *J/^ٳgg>j Prr޽{?B(Y_|E1'o(c|n߾-%nQGtލ;v,**깋K-jzU7k.JťY)))REPN 0@.\VZii)=sԡJ -It eF} 8 t#Fx&M  ?^===lڴgϒ:8*HLL !utҍb[nƒynD'݈x]\\ZL53gAM hHmKܑ;744QGtՍh4[RjU{{b.t#:Fbcc߿_ 555,,pH!b777--ν~:רQFSRR)_^y7R!B)Zyő:*Z$ƶsT@HOL9 Q` ;WJógϤ pT@n'&B݈H(@0DiiiRlaaQ555`PH(@0D666Re޹yFuHQ9z!ۍT^4Cdgg'*JK˼s.ꐀrt#BʷhVZR&Kq͚5II{~⟘ t#rt# 5jH;I&zTR9*nVh?1FF<<=*S iٲe-糳w#zrvv~gΜKKˡCT)@0P&LP(BvʕB~̘1$ JW N:_h]vt:F'Z^{9@7nbرbgϞޡå @yzz~bjժB۬\R fϞmggG@>}*oӭ[^z p¶vڂ 233ׯ_/ ,nnD'Ș1cīvYp;vT)~ˮFtۍL>VZBREзn:z͞=̑w߉7{ݲe4>wSN%](c@)ݻWS)F `ѢE.^Ur:x`r Ѝ177NIx֬Y(3gw۵kǮFtۍTVSN;WVO2%''G-[V^=C˳ܹs0LC 9qDTTжmۺu늳v5f̘=zZ[[.ڵkO>-Ȼ+ (5˗.]*=zڵk¹J,noo߯_hW_o>,q\rBy]6%%Em_x۷o߹sO>O9τnEnܸy!S'hРA#FP*uutt$WP qqqpѢEW|Q 'fYYYmڴqqqzp>gixDt#2u# ȯjff&,noohOO  0ЍڍN6㶶//&$$?бcG9hCZZڙ3gn޼աCsss2:gN>}޽ի{{{mۖqM߿%:iFXYF֍>>76tQȂ h,(@dA YPȂ4@ h,(@dA YPȂ4@ h,(@dA YPȂ4@ h(:u/8(@@qر#"""''T) A\\\ݺutm۶5܎;( A_򥫫1c:vT*mmmMLg3gX[[ FQTO<9qĢE{>+(-9$#B.hR[N3ٍ^m{]tmV%T&0Jl'mRkTΒ02}oܙx<}syP$dff_3ϔ+Wn[neʔ d~zӦM/袖-[gu)=222655 5۰a[oղe~eNNv&M~7r駗-[Ff8餓7on'{K(1f̘bŊâEe]VȡԩS\Cx?W^a,4y;v^:v fnnqHOO۴4ppEnB̝;~ONnҤI!=׭[wg+77wҤI5k;>|@/VViӦI"#lƍ;w.ÁW:%WNNΔ)S6h? I?`֨QO>1׬Y֭[קOp< E]jժժU˄ӓSN EZVV֢Ev^zquֵ|K E|%?_~y˖-qh(0qHNNر8h(˔)ӠAsڲe̙3jժY>CP@;o޼}(Q߳g|ڶmKSfgg @'|Qpx_WPt% 8$f͚egg?sG]v)))'p±|q@@ѕx]rr%\R\T:{cg;4]GhիWB_ĘJe˖)px Z~_|p-[l޼y^{m2e,!@@iii(a hJTN:s֭ .6޽{ žyegg~-Z(]>GׯQW__s GްaCժU/Zj+=^z̙k׮YfVN<č7=~m6mׯs7|?ONN>P/~CJOOTRgvڵhѢӧORJE7T^=\I'tȃ)a>O9֭[Q第3f,_<1%%%pi1H ETڪU֮]Ο}Y.]VX>6jhȐ!jպKvuwcСkٲe[[͚5znݺ硇*YgYt-_SO]vmh= '}^zݻUT1cFƍa< piνzԩŋiӦ=+WzfffnW\qEV֭[R|ɓ'׫WGg<={XիW_~;v>Ix222J*=akI,\֭v9s5k?Å^&a˖-Å~+o 3&0L믿mۆe _}ꩧvzwڨQ׿~r/ UVuY[n ŋw1HQk))):t(ߪU1cƄ ,7n\.]?K.$4233G]B1m~駃NKK;sˇv8ܴi;v|W]uU4io}9پ}u]נA9СC_}f͚}饗Œ/%g̘q-+Wƍ0 oڴi_F*3ԭ[w K>}Ԁh…;w^A8z8>|g_|q]w-^8ׯ{w)L4) 㤼D5--mݺu=X_xᅤr ֭['O|5͛h߾}bgFIw׮]GK,;*U?9ѳJ*_v!C~tkԨqw'?>lذbŊE֭[v7{W^=_@X:`f͚ԧ~:w8_.^wM7TNxw'K*T8qO/Mh(fΜh_zN; 6id׮]QHݏ?Ν;C(uN4i'tRqÛ7o>pdÆ W]uU>} H<avQbO<134?Cvq?B4)S>uK.رޣ/ ۴iSTxժUKt_5kVt*U6Q@0cǎyժU;(/p}wYYY999۷oߴio裏&~k׮N8t%JPB}qy޴iY2(*"0qĤwsqʕ-Zg|۶m i1ўpi3fF֭˗/ߺuk7MKK;w_*)]~?1|%=>hРYf-]4Q{رr(&#l׮]zj#ySSS`yase˖s97o>}y,Y| ? ۚ5k>͛\rڴi/@v999%J2k׮M{zRJўKF/ SO8H*U@޽;333Lԅ^xGTD;:>ﴪUn: ƍmڴw_333OG5X =ȼvچ >c;w^hmاի'՚8s*Ujʔ)]wݘ1c^Is5zIyo)\zunn9sFپ};3'v!e 0i )))5jK+I 86XyYnРA_|9SOE~gX")tax}v+kP\EJJJ.]lҳg}j [jծ]p>O>{B/ w9uԆ quGNK(^x׮]O>dϞ=Qe(&o۴iSv}F{~ꪫ%#I&͞=;4zQX%K|{K/ 6Zj3gΌdpPHʫQ~A͝;77 |srr (777.Q0͛7^øgϞ;7nܸtҢs׭[W^yeݻwOzh8 =s=۶mKII֭[rIy/|뭷imɒ%C㢋.ǁ8anTbQF%k.aÆ[.e˖k֬4iR=7^ܩS_B׫W/l7lؐgO<1ߩڼy󜜜_~cǎQO G7{͡}w?233+4hqD?cǎ#G &hҤIӡC#tׯ߶mے1cL:O:zv֭WNlݺuٲeèڷo(qh׮+W|VuaȐ!2Ap뮻.ɓ~1b̙3L#aJӦMjGĦMꫯ+޻ȟRJ7nǎIyĉy䑇zhΜ9xw޽lٲ39;sjjj%S^YWoҥsy駷oߞ]N^z]s52>G5kݻ+W\n{3^z~{v^9rG}TlҥK_K*~`o1:*PdZj7jԨqB.,`7mtӦM{_u8L>}ԨQ+W #)SLSG„ nѣG_UV]|y#eY]wV-LŠ+?Z7 24*T`f!5>裣J.ꫯfgg7lpɉ7ٳg7t}OmЎ߯_lٲn~5m499ya;}I& 0Kh`e˖$ f͚#F8L 6}^xaѢEyvYV-Zx㍕+Wv;m„ Æ ۵kW BO" Jp 4@ 4@ 4@ 4@ 4@ 4@ 4@ 4@ 4@ 4@ 4@ 4@ 4@ 4@ 4@ 4@ 4@ 4@ 4@ 4@ 4@ 4@ 4@ 4@ 4@ 4@ 4@ 4@ 4@ 4@ 4@ 4@ 4@ 4@ 4@ 4@ 4@ 4@ 4@ 4@ 4@ 4@ 4@ 4@ 4@ 4@ 4@ 4@ 4@ 4@J7 endstream endobj 86 0 obj <> stream x}V͎1 y&'U= Xm vI:]U8ũcq&I\ٰ8[<v(6Ad2Q>?;y` b b gSHfT,Bnf?{0ٖ:.e`;0:Gs̏5"'983ؒgS#[x+EL)C`x5iyWP "W@TJ6QӿԵ?hD{~-BPq^"l3KNUbϭ 䧖 CrλQײ49^gu %;&[󀔹6;/ݥV Z%sly=tHt: >,ꗫSz_kWծNfcѺꇑw;5['맻6A8bfF(CO3V/ߟoȂiTiKJ#C PI,UG(T5{2EA }}^רIiUS/Cԛ?n?T endstream endobj 89 0 obj <>/Filter/FlateDecode/Length 4539>> stream x1 ð'ݱQ$E____________________%___________ ____________W____________________%___________ ____________W____________________%___________ ____________W____________________%___________ ____________W____________________%___________ ____________W____________________%___________ ____________W____________________%___________ ____________W____________________%___________ ____________W______________F=̚ endstream endobj 90 0 obj <>/Filter/FlateDecode/Length 102429>> stream x\Gq:H,`CA5Xc^b55FEc4 *5.AAN}~8~5739/s ]3"}                            "11ٳo޼! c~ɓG>}~E>~k׮={ܿ˗bON|C$2R@@@ѢESY̙3_͵8pt[n) ×qDFF>^*| @&OŒܸq ~!Wŋ_ h/dm … M4w{ĉO<ɑ#Grׯob"""Ӯ];yꄄs=z۷ Yfun\\}}}߼y#6[b*U|%QQQ>>>O>]QFܹSd!!!III*U*Yd&'"bہ7n8w?yAkܸg[nݼy3448UV-]42Yս{m۶yO[J@{_~򊍍klܸvںjZP_~Ν666 [dIppreww+V)SF'M|r֬Y߿/6>~$2eСCS@[1cFte˖FEEɏsss9sRoΜ9&MRG\(o:;;?{LG2=ӦM>}zҥMZ@@E۵kw=^رE\ԅ - ntz}:u)}M+W.]d8c X6l7s޶rRZ[[h"11_~l޼y||'O qqqwӽ-[/W^}]mVRJ;vtwwϛ7hZl.Ǟ={5jTpagVۧOggg:ǎkӦMLL(:t}lnn^f͂ ߿_l޾}[Gؐ8C+W,PKD9wl!w.]` M4پ}${W\a4@f h,[nRѣRڵk֬!Cƌ3|i… wܩPB>(7n޼y;ԭ[Wŋ.]F͘~ݼy, M'%% :ThTӧϔ)Sٳ.)={ ZҲeuv}7"2>^VСC_mժT٢,hҥn*)bK,1bS15N߷o_߾}M`PyаGSN6O>}ɴ-2"YTٲe7 {n͜9V<~Nڵ򦓓?} *tIE?ݻ啶>|(CBBT*KH_V,[lAr\rjٲ\~8Ea)EFuP(f̘͗/_jɓGCK+RF:uJe"aÔq{ѫW/bŊ-9-u%2 hJZA/8`ii~NVY]w޼yVVV,,,6oެܫ+WhMAdDAgQ011Q?+-?u=-1֕Gi6mڤ>3]EGĉ?~( sssm"?iS3eX%Ȫ)U+V\Vϛ"eRY|N޼},g)ST.]WWX!e0k֬?*[۶m;p'N(+66 yYd-]UMǫ?/ێR33RGiPPжmۤpRk׮kn߾}r25rSP###|w.-|!r劋ٳ˖-QK]g˖-RZjڏmL}={B?*WkOX㓣tғ sέ}ϟժU  S*= kڴv{{ӧk|˗7nrzGDDtB>0rU#_tMd)Rs{V^ݸqcyiٝ;w7w\ Wy3""ѣGŋXY9-1E(y\GPe˖7nBFJɓ,..N*V|m*Ox|FFFy]`A:ӧO?4iRhQKKSNI ٳG܍9sΝ;wҦOV~cZYY Ł rJq4Zjm>|k… ׯ_ *$Wϟo޼ْ>}*o>x@{uݰaTײj*CCCiObb;wci+WDLHHrJ~:t ͼ\͛7O˗/;wS꽭Rͤ$ ))7n=zǏnj3iҤE,((ڵk%zJ\Ç=<<&O7o^Q^z<ѣ%KT޽{Eϟw-w)._rett|<(F-[ho۶MN:Ki=vXX2eȧZbT9ra0k,тb&,--+X[[ܹSeJ"JމxBX^# WZu //#F{ƍ;vܿݻ^IIIk֬Yl\-***22vfffθ8={{bbb"""TJhѢ˗/-X`B^~-׻woѫ-Z>͛75Ŋ<{ϯzβenذA, W^=p8VDfȐ!" 9s-0x`yoVM"9s)Uh׮]@@T5HNa>_|b,,,iӦ'uٵk׻w{wҥ#G={VFGupp?5o޼ʕTÇ/^x֬Yˤ[Ν;Xܴ8ORv),FcD>~(~RrwZjE$j*E6l#GyxuK,Qׯ.VZfR7jHTВ[fw@ORD3g.]:uT9/̬L2 YfWڷo}v]u֭[paLL8y9Ŭ˗/?vؔC(]'ׂPaTAAAYаnݺ3gάWg">RhWNwJN<9iҤWe-~ʕ+2wAZ]DE.]&O 0`wʕk"R1**SNݺuSr*9o޼o޼d+aaaÇ:TIUV7oR4iRB={vQyQfll\D {{{1lą/-Usssp/_N8'hK=J h|6ӧO_~}gϞ%%%*TbŊ*TmqqqN?:::֨QCRɓ7nܰkڴ=y g_RC\8CHHHLLLܹK.]zu|w:=z'44̬\r 4077_[n:$!!… n ϛ7oZ/޾}gΜÇy\r*U4TEMcPzA$zA$7 u$BBB7_xAL# ϶qF-[LRN:r˗{yy֫W@5+$z)MvL&L@PPα4@/H@4@/H@4@/H@4@/H@4@/H@4@/H@4@/H@4@/H@4@/H@4@/H@4@/H@4@/H@4@/H@4@/H@4@/H@4@/H@4@/H@4@/H@4@/H@4@/H@4@/H@4@/H@4@/H@4@/H@4@/H@4@/L+gJ… Iewww;;l ٳg۵kGe޽m۶Ͷ  | .,[xqbYllT߿?KЕG B*f |͔Sŋ/W1,&&A*-[‚oKp4@/H@4@/H@4@/H@4@/LȞ#  hM9;;o^$zA [JݻBm)$P 244422_ Αٔ-+ @.H& dCCCSSSd@@t4@I(B Kfee/_>sss’~ }o6oB~̀dߍ&ʻo<33 &Se'|Mx71HEHHHllidd3(d[IIIOoqtt$>A [HLL 7̜LL}GXYY?{,..N)2$O[قφ & o oo%*8H D Ȋ?[ZZ@xk7wș3'aI3aռ_NBAĀHy" Rbmm& _?B(oȑ)_^Ӄ 244c/ %%%)oP5@ 7xI3 253oڱ'qلNhatz@/H@%8KӸUyGҌ4Ȧ>}-skd'dk kDp@TB|[r>?[YRMd,qXF H@@՛XIjv{&+iD]43[<8@co";ߴ07#&ubkO _ : h^#+ {Tv8;;+@EQTS( Et !Ј4uV4" ;uI( hiP(O(B)Sv2>رco-H@lصtQ@Oyj[O˗/'Ў4ùD> l2=ӦM>}TyfJTYn]>}ܿ_6mL8Q#N{ϟʕYfEQ>jժ׮]SRڵϟ?/o>zD*޽{Z?{T}=7@Ƙ8qb=<<_W˔)sǏ oVyk֬ٷoÇ=M6MJJ5k̙3㥝!!!ׯ?rŋ]\\:)άP(=kמ6mr ,رcѲe:vhffeq`^^*UVmƍ!,p! d d \$˙3g&MT^MHH֭iӤ=bϹs̙SN͛7Kd6ZJӱcGVLMMK;vׯJ_---MQ(]47H@l*>>?3ت^SS~.@(S EjxxIrϞ=;&&F.\XN__ܹsKuj֬)^Zv(=z4((p'8q5kEF؁#G$/-@vҺm)A'T.߲1AVHȊ>E-LMMs,UT9xأ\aRZP\tI%]hΝ;o۶MO>}=lx\\ZbEn>!.9⢒}6Ғ Ǐ/ … +9r{&HWF.GFFWpuum֬T޴iׯU*l۶Ĥ{i@qAT8;;. KJJҸ„ ǎhOO)S/EFF8pyi@bjժʚ pOjРAjծ^*˖-?~Xi@v7aK附QΕ+׷~K6$}%K , ޽;vX׮] ҆47N*ߺuԩS{8@!(P@*ϟ? yRJ h HHHHN3775jT>|dzrTHLLA#B-VCuE)K ˱WB_7 s244믿VZn:???ұcۋ ŊkѢEWndU ?>uG>K.׬YsʋJ?yɩN:nzΝ-+++ݶ>|sQjѣG7mڔ  hM)X,~ d##4o>=Mi9i(;5K,y1nRB]}[Yb|[r=M͉ K@z8pť~GX݁8HV{+W̞=B'H@@qȆΝkiiٿB  dyCr@@Tpvv&2_~ȑy% @wxb\t -lٲtҜ9siӦk׮;v̕+ה)St4턇'!!A?>l0i;rM|+FnraccsΜ9;w&8!ȒŽ=Jeر۷&LLL|o)fdE6lhԨш#V\I@@WZ!uN@IFԣGݺzשS^x!6:uԺuk##>*]_ȑ#,Xаa.]d C#cR2Af\vvv& $H@#XrC.\XxqCC 9qWZejjv\]]7nXB6mچ ޸q< @<..wrrZ*ʕ+nj#Fחitr9>N/by:m z hd6mٳg߷o}6Hacݻw͛}/t7))AWlllw`׊4y%$$#m3gN޽tҠAi6lruúݼyE-+"^N*}}}qUnW=1׵kFFFRdĤ3~/L@@H@#^~(|i;CJo>u/ 6DEEݺuŋ9.\_#""D|֬YNǿUnWիۇݻ"C$$* :GÇ*UU,ϰkVzJ.߹sGc ~MC+ܦ8zFFF͚5;vR@F xP hdvڰaCCCC[e˖ˮ똘QKi:Q^=ԩSqqqD!h2B&B8q(ԭ[h܂ n޼٩S>}h}:wܢ|۷?~,(PiӦcǎ-Qʩ޽{' *THvɓ'?.6]\\DІ co޼)&#Z\lxx[Bj׮-o֭5N7KP]?T~2kkkѽ:i#:\*(5Szɛ۶m4hill~Զ[*U~S^իW?pҥK pcc͛7+'P?=Y׮]׭[gaawȑ#qb[͛7bĈٳgK9eYPPzYt钿rDW=lg|Q2ewFEE3fՉNqڵK}nݻUςhի%J4hɓ'_ Xj9s*$?߬| Æ ۴iro'[xqǎ~6m(_ ڵk]]]/_^fMwءKIDDssi$%%.Ie##N:i_" s b奒Bmp=чB ={,U@yC yTӦM 666*Uʓ'ן={P(Fոq%Kj9|ӦMR@oߖl߾=88inɓ'͛76˔)sΝׯ_'$$̟?_j߾}Q+W.^(N+mJskժ%e g:Ϟ=͛|=)w߉:*gϖ-[^vM̙3]PPЍ7DE;wiFy-Z(::ZyOm 0N:WWuɛ?{(ȝ)^ٺukq7+W͛e˪!MNM3))k׮RBtxРAR #O"1T\.\0m4oonݺ*?M2Kn޼yGQ;"nSNkϙ汝K&ζj* R UnlС7VզY\g=AVjטY;0!Clҵk]]pA tR…=ÇwqqiӦMRRW=TN8}tirR|||VTIaÆݻ7mTpQdɒw޽uIS5.7n N]k ޏ.Kjhv/^B6w /X׭F@aBd "3(d@©NNN9sÇ~-ZHO jɄ GGG77OV+V?cee%.󓋫A32JѣG/X@eʔ)[l%ƍoHdxVZo˗ѣz|#߹|eW:&&Fjr1[,TTsN.]\^C`VP… ÇK;#""<آE )7rƌ3o<]EC3Ӵ~JIǏT򪈒P_fW^+ =dTD`Ȑ!b:uJ}.7o4>T3t5nSBoJT.앀>zhJ+߻wO՚jyhF֭={)ŋW\5kT.Zh|._,z:խ[Kvo*+eE,X000ݻwګժUK*:tHPԐh۶mR~XfEo߼yi&5ILRrڵΝ;>}Z\)SDDDn ]mutƍ'탂RZ KFO&-@倀; @^ h//3f̐ -Z۵k7bĈػwڵcǎ5kVBB(T\Y۷o|xh# ۷8azƦEt֮]>,,Lܺuk={n˖-ZN oP}?n߿_Zw޽*9vsnZԩSEXOݣGR/MIÛ4i$wZ%') <@jǎRK.|m{//,!,,L׏Ywoo۷7ޔ)S+y-UŋݢaÞ>}~===i46-gEMiA 4mTJ5e666ӧOʿ%K46ѯ_weTܗ/_j񙛛1B*;VcDWŷO%ݵ;v,5ݖJPS:0f;;2eʤ{e˖gϞM) pƍrʥ9]mSSS{̙ѕۤL\uGu=.[,%#CBlBߡC/^ |F ׯnj#vJSLQ_̙3'N|Çuٳ\LhhhO8=bŊ;w4ׯM6-_TX~Thܸqݺu---EY:EJ$GճgO1Bru ~~~M4yAr*e˖_UTZ)S 9[-}gg7W*,[l|dɽ{D@Oi3::ZdQ ‚ x-?ݻ W#i׮>:sNʇ߿߷oOF866VDٳ)e?8x`DDzsؖگ^Zy$m۶=}t*GWnSJ3[ -\AEDX|Vs۝;w&N:UK.N+vuD 7P9OnSJ3[laoڻwA%eeW~عsTkѢJ6&&F^ɱlٲ1WX1zhd~SSSI&i쉛\vqqsZJ.}ծ]JfL~h͓aUZUg(:tH*7Z733۾}{֭G%}LNb֭[Wn]cǎոٳgw\RKDK,7ntA{Mٳ|L~TRC 9rAjP_6m*OB^hQRRk.]R˗ ?-=iܹ*M9pWbb?#dhh8tД@ʕ닦d'OqDSYjlO2E4$ Jj"EU!F r4ߦfIKT%^]|G<)_ofcffviܹsO4UTиȑ# ,(m۵kW~ d477/[l^֯_҇'''kkׯlݺU>lQbeeջwի'%%}1::ZĤdɒ]v]zxI=@G+WnҥK?>((H\?y9+V:t4U܎"E4idԩ˗/W,ZֲBf„ ?ګJKof2޽{#>Fmܸ3Γ'O?+6g''_vmNz˗/-Z\m?EK|}}|||ƌ#ؿK?naaѼyÇ˷ 8::JwJlٲꃗ/0uLEћ7o֬YSg@ +W 1cԪU۷N^pEyzzwpp–-[*O<'>~xРATquu_5k>ӱcISDϟ_cȤ4O?D\Dx 1b茋yfC/w@TJC7lq-[|G}t̙Eꩾ֥K֭[{4iR~WAAA|ǝ:u3{l%KRR% w>rHHH|xAo۷t8iY߽{C={ܱcAEb?ի[nT/v?yήbŊr:00P"Uf͘}-_\z/[x…%J1bDTT\Imܹs֯__|\tg}6uT\O |Zj96@cězoߞlqϞ=ArDP3FWX+;wGϐL6jժj@^֮]{ZjISÿۓ'Onڴի >|xɒ%.]-[;}ؔo߾ݣG={=zۧL"믿nРA֭/^,V{9sF}ևJoh#GH s.z@#(TФIF`OOO^w… *CDœZZlYNvں<RoO:%ō7 -R>^ ޼y$ ׯ_XݻYhIDp֭m۶իWO*bSN?|p33۱7oTZd? ϕn$믿if&L>BΚ5~J9s:P=zcǎ /s.z@#1bD:u֮]m߾=<<\ݡtҐ!C6mJ6 Ir4 |)RD###X?`oߦX1koq *$%J/\ŋҘoO-̟??>>^~rʥQJޏǎ;M1W΀Gկ_ ڐnytXX̙3OIJ.k5ּysww n߾i&iw(*U7I9k,q rI@.?~ݫW45MHHVZܺuM6nXYY&g|=*!wNظ{k֬qDDo֭3SS=zH+suEG}Rرc)m6=Uj\W`{@Xbҥ;Tҭbuj 6ciLi֬?|0\q6SwޱcCݺur]-@wA䟂g}V\]]G-Զl2iҤ;wSL9~ݻ3{C ۶mkժU.]J*ޒnnnǏ℄ VReÆ ݔ/HKCGdÏH*WܫW/)޲eϝ;L<Ѵi,\kSvQؐͯow֯_|Ǐ7i҄l Eˡxo*$%11qժU300;wf5M?|ʕ+W^]vm-3&&o4t.^k.Uw4?ܹSOuqOOϩSJӷm&ry<CR|vnذa#Fؿ? {{k Qa*Ν;7oիX 0]v'Nܸqcrr7joFSK(!>,[lzK|G34_2́ׯ߶m'NFǝ0aIRRO}R|GP̟?3lْ3vF7޾}{ƫkk 6S,Uf] 5MwzW+i]~40amٲENz9o\ܻwo:n8nڵkΜ9SjԨ{~MRRZn-gΜѰ'OM6Q``-[J*i.Ӯ]zIt+m۶ t֍K+ Ǐ߾}[R0Lozb6m$)'+ 9NJJr۴iGI9 6TRfwt)4,?H͛7}ri\')>zHg9s7إKt*zM6""" 7n}vڵ-)*0 ZXXhXW^ru{Ȑ!_f<4k;&z͔)Sh=Uӧ`||wTwQ~sÇztbbH[HϨQZj%Ţ ӧo8;;w)M8(ի?~,UVYXXhhرc8,,r͛7; IE<nsWQV>| Y@S//_\__W^Ɨ/_^d_|qܹ1wٲe5kzyy}TҥKۙ3gOW^BO?Ttjժ>}ԩS2Rޝ Ŋ;vzWD>lff&/r.]D{4ƍ6m:tPϟE0{lpРA'O S FUxx+}N99L?34XBΝK*EV@Ko'(=|ٲe߿ =U .lذaڵgϞ6mĉϜ9#s?n:={&Pw^vmΜ9777I(22Wb]v\2&&f߾}ǎ2eʉ'O>-PPC*)R)44TÇ;vW(!!!۷o8pׯE.G-M 4hqFCCC,_|bȑ#3C/^C X~}sӨ> &//z[ݻ TRݿ[jr_xBL:ujR^*T*ظB nnncǎIٳg+7FFFȑb˗/OםDfgg[333-3f}V}eʔy7o5k֔)SED>Aҥ y1ZA ۷Y&yPܸqcC Yh ABh suuqFTTɓ'I @>$J O cժU3775jT۶mOmdc@XFG[#S(@eddԷo_9&!@z?~ܿN: dh UFbcc{ R,` hiKJJ8pI4'$$$<}I&) i͛ӧe˖RyϞ=7o߿C=U^zVZÆ _~̙3ܤknѢ}DDxzj d nBB *UJ۴i3yd,qppXrp֬Ys+trjՒ۷o_|YUp n\/ju͚5͛7^t Q:Bѷo_HzHhUfÇZ Cp {V*Ǒ$h (^e Ȣ9.c[[[|"5kVժUgEGG5۷r]lY)tRBB۷ N駟;5FR}pcSreUV !ׯٳg/\044p*T1bsŊ՗\rɓS9f,]u[n}䉼]믦MٳG<֭۩S8paƎ+M>v+W/ެY3ƍsCZټyɓ'㍍I]jR$Q|8 {ᅨe˖dҷ ޼ysҥf͚e|Ű.H-oի!۲e˳g֭[wр%J4id̘1[7o>}o߾裏Zh~8ȃ}* +++[[ۮ]̛7o5ڿ7^~-ANNN}oذݻwvF-?ԩS%Ko~̙ u{{{{iakצ矧nha^lWDPVbǏxhgg'vꚑu]&NΝ;""e˖m޼:w^G:,7Ý8qBٳ ._hAVp۶m}h`ĉӧOF@t344LLL~z +V6m}(@gcǎ苷u֪UΞ=;..N믿8p`ٲe...W700ؼyzUaSSliÇ bC?Cz(o>gg爈=`#G;vP/@޽;EYqŋǏ ^z={ͥ fq111"{988ddkѮ6mRo _~iڵkRR;*ׯSΊ+6mzl9ixꕓӑ#G)9"&^~]\*nnnsI1COB3g jڵ)x@цeˊ???~PP6.../_ SiSNE֭[ʕ+>T*Gn׮]*U4i&)SNBB7eozAlu&וlmmmllJ(!]\vMØɓg͚%/PZJ*\t͛7W^:thY`%K޽{'=]C 0`ڴi+VT=z\tIDfΜ):t;wn:u7nLV&ܰauRk93+W^paΝE&=zT^=5jȣdtUI &t1S%+UMMM̙w*THOUgpС1117"ԩSK8{ԩSO8W_ݼy3yɖdM|||^⩝݊+#Z~x}...^^^}~V^#/OSKE2bĈvڥ.fY%4W Xe666zYc޽߻جx mٲo߾˸aä;ݝ={VΝ+WR/_><ZjUZ5BѳgsO. '>˖/_.Q0S:b4ŋ7iDkD'11qWe:tׯTPi.)29|p)|67 һdi˛6mJtظq3fHYx ёJhJω9,Tقߦ(|yt%Y?}'N|2 ]Wԩ#ܦM+VL^۷fff)}޽{xΜ9iVT╤'PRdd%9HtrLB Q.Y=!/$jKJJ|g}̝;?|t=5T֭۲eˊ+> W^?4kL^Znt͚5۴iK*jW믿[n-a}ڵĉ=rX5W 4eE(\p۶mEJѧO^I | ..$~/bL@{(@k{s+WozfSv=o|pZa,{qf"""6mڔeʔyѫW4oGСCJ2j>m۶I7|n:]Eu5@֤I 6`޽3gLo'OR,E곯KEpqew,--CBB%P& ký#ZO (@ik׮M-*POwXb3g*[n4hPԨQC;w {ERz׮]Ŋ"駟vڕꡡ9SS$uG_UEw߾}iΟ?as7onii.zW^mٲEF"""@ 4dDnݺ%.GG45kVbbK```\*#G~gR,YR=0 ^eғbŊ#)J^ldӧOSL N1? 8qbkk׮T#$L4)?-,,6l w}3ft2͹wܑEcw޽l2WW+͛794yݻ:y٥}2igϞ0ɓEߋ . 2$<<\C;AYNNѣGwՀcǮ[zӦM 4аհaÖ,Y"bqԯ_?u?_ :tH/^;v(=4iR7N>=a„w~Wym)ƍzo~#G^źZ?r,c~xxx#኏m-ڽ{jp?SxҀ;w]DFF޾}{=;s̑ϟ?yYb}>c2k׮in\rRfuԩS9p@6 (0mg̘>qg͚uM/pddԻ\rSNMѳXөS'ʕ+?}ƍ޽f}WR'.]$Ǐ=HSsj~~~ZTR"Eĺoߖ FFF?sāxҥk׮U8ׯK+VLou]dnݺ%sO0!#)9shh|ݺu4h`ee}iaÆթS'uիW5,TЮ]ڴis;U^]|8@D+AСC׬YT*̙xb*(À.N .,ԛΖ'/#mq4wby۽{HÇEڷo/\f˗/xBZm۶R?4G;e~جڕ+W;/qPUxlTrKLدdajOd![̟?޼y*[n/^4555110`xx]inbb}ݺuӦMOOϓ'On߾̙3/^0IIIҝT5\G*=>z۷bUVݹs'|z ΝKTMsϥL"Y^{/yw>WswiX&gcw?XYYl*(@gccSu%Kԩ#Hs\`QF)SFz(w5d=U͛7&&&5j8pWz_*T`ffv;;[ʛMOHHȽ{֬Ydɒ*U(wʼnYŋwppѣnj3<<<4GP|'mb bXwޞ#G KUu KEUsaii)&)QDZsEѣG"Ȁ|h؋8"i\-۸qv^xQFeBUtҥ}#G$ȸˋ޽{+W.x˖-/"tD\\ܦMx&&&9/# Q4z͚5kl*? 6\r``#GGB2ӧ"@رCl׮ٳ'Ŕg#ƌ3b/// }ͼv>qJrժU"7nSL0 [ZZ?~<((ldÇUȔcǎ֫W}d4RB&MT*,X@62Bj… "e#h bٲed6H_i{' kHÈ#6lذvZwww{{{ÇN*q>>>.]rvvݷKڵuHS˖-Ih",,l̙ā*UjҤIKΏuȑ͛7'%%ɇطo_=ՈSL)QDlU7o~O n߾i& 5jT*Ur{3Y\#22'B:XbҥKSLܹsXXXL6-?shh;vHȑ#f5j!nݺ!Cpcǎܹ3@ :466?(VcüCiβq'Q5[j.k)})9л^ ƀ؄$:+,4$mmmI@P:Ĵ`,.-Ә֒Jtd Cp4@+(@1R* qRldlP( d/ @G%^eΤd# tUr'V 3و4@++:ߪiC ՋgT{\351&'rj$ $,ESO%c 9ba_XT1O1@+ tmArLB ?yo Z7r =  `iL{F$$JLLJWА9eҙaKr@ABĤoJqU(@@c hV+9+V<@ezA755%':DhVPhhV04BBBROLLLL=֖t (@Yf\RT.dCp4;?>كX#XbDl+R(E^.J E+JkJ, jbi mcR=s=_ĜI|/xls9g  Ps@3egg#  h`,,,(4`"##.tqq!2xA$sWV k֬Idx!@f*---,,L*9H@Ej89tIe7H@g*~D Lz6V*_ZwƎlف E04@$  Ps@P=}x?˜ j5@!7yPޅu&糥F!\($(4PO`n>~  hbcc۷oI(`nv횒B40$~zvZj5k,sS\{'==YXXUr'eG0>} 'ggD%cufL@] 0oo;wٳƆlui/ܹAlɻpD/4`$Vڻw%K݉܌3֯_׵kWGGGA.yj"1}wUڵ=ZN"pʔ)^oDFFzyy%$$KE3&((hժU" (줸RbM7 KCFǞ={>  ۷o;99 :OM4^7,lذ!44Ύc*_!Cŋ/Y8~qJ^ʓ7% -Ī~o=0$|L>L2))) 駘qE:ZpppVVwixObbb||(og i4sׯWމ'%#F|Wk֬;wnr*4 3|p)]$֭aÆSsrr/6Cׯم "##===%-[sNooo ! (k߾}͚5#%1jb! (k֭kΝ-,,[oaÆ#Gddd0Ou'ya&4 kҡCDCDKdӧOڶm[}s=Q3_7n JM 2dƍGXX؁\Tr֭[O8SNbcǎݺuٳgJ?÷~[+Z[R%}9|=99yݺuvx)N7A1Ĺ9%[h^K;PYYY.\(jzŊAAA|4#iӦ ̛7/==]^w޽{ٲe>>>wڰan(..n׮]kooo?{,00pBѤZ߾}Q;w5jTbb 󋎎Vi''=5kI/G9G&曀WBFFoZM6]bE6mR`CBB>Sqэе?~K!^pAt ѻrM`!zZ?"nҙ66ԨQ#66ҥK|VϣGUčpƌR|͚5stt<{lllF0aB׮]ׯgpҴi̋/Jl޼޽{{)իי3g*TTҝ;wΝ;'Z+6ݶm[߾}=g}6w\9]հaúu릤DEE=}Td*ݔVgqq:oѢE5?~|ƍmΟ?߹sgQKA`R]rݸDz 8۷夰P&%E!gg/R*rڴi*T/zuСիW/\P5jXt4+$$dҤI?C@@8 \RY4r#F(S͛F:zhFFѣ۷o_J\Gm3gT޽ 6m*LLLs277J# "gKmllDku'O1cFDDDZZ:u( DT?fmsժUE $B]Kѵ䩸+VC^fq_<{>>>֭3l%ԩEa=z(vڵ?CʕgM,5k5S8{﹛#L+P"H@ƍ;v'OԩSjՒ7'''44;tE| v0c6lVԵm۞9s&>>~u7zTŋK ME믿V4γgϾwݻw?\Cw۵kwAOOψgϞ\2)Э[׼CK[+V?^k%̙3ϟ?/ 4UFh֬"--Mt!C-kfM,2IIIEYo+`n-{7,"2&`\ ݥd_?ΨQ_OH*3(ѣG*ͥ{ صD( DjjRyܹcIX .4rg(x1ڨ>ü 8TӧUSL>AѣbwQUfbSBB­[_^^=iӧO[<|*#FXxZ4o&߽{799YCjrJ/|SކZvr(:ϛoYf>>>f'?>xW^}(𓔔$X0&KKjP hxݥyV4irҥ3gt֭xUH <;X[w4Ym۶zvԩТ;wܾ}{DD6#2'- mUkEFFJ=Gʖ-ۥKF#zy0ZYYIt.4&Qr*qwwt(ܽ{*.IҸ8P~}{988(m۶͘1ʕ+%yY[gkDmVkݹsG*R@N0(WCth4?HS5kjd%i,)/_֭[cǎ5zWիյkWQ\NRڬPגQl&>62JBAɵN^P֭ PH$_&iiizf~TYf .-}LnϞ=ΙkãGZmsVVZ5jԐ .]4h-BK^ђRpSRRT쳞`dddٳG*ݛNGeer4c=WBCCEa˖-+͛7wppHIIv˗yo߾ <u#,,/Yo߾nYm>tP~Z[^v(|s)hwRM6׶mw}-Z4i$ 899ݹsM/{&G2#2 -(=k,гgϊ+~'%%͝;7((-#""FQ|תUKZhkk۷oM6̙3oߞ{7MLL zזiKvL[mZ w3͛7ni787NpyΝz~~~ݺu3HVRJ=ϫ4ʤN:(߿waLLL4 //͛7A* ӦM˻ :::]m۶z(_̙3'N(s̩Z_RRRF)嘢0eј;v,[lܸq2dӧOo?U*;;{z[1//|9p.Ԓ?Cm sqq 7 ν{~ysZyر_|(^ѢE ={H3fcg#GH77}ھkFM/2)ꫯ&*d$/ѣGN^@Pz9mڴ$;vlɉ׮]{;`s„ CUi?߷o߳gnݭ[X6 իW/ݵ?8BBB;v<~sTNj^ZwyLLGWTX|ԩSŒ+W\h255U /E*  2lm'%%]zuĈEju܅ZEnR:u:t萼Jʕ+(>rH 1tRaɢvqF;ve PRL27oLNN/)F@+%!!a ?KKKGGO?eܹ/_'%%Ikժ5c MIIyw<<<߿Tig%**J.߾};11I233l"ʻj׮ݸqc]Y믿;i8q֭[EyҥM4si+ӆJ1OzjQ{2eDϞ=;H8ZJTLѣ^b/YiӦb'O\z5ׄ"ʕlٲHTYppl4O?$رcW.)ծKΩ;wNhopyyq~*C\uk:qw!jSD\ٌ3g<|PڬK.8_w ]b9sDGwDfx gg7oJ;V\RbU@GZ)˗/_ti۶m *Uٳg^ZZUi?wHʕ+RSS/kmllLoKZj%ܞ}Viؼys޽'L ōݦnݺk׮СCݭM4;w8)`hooXrO?I1huuu}v5lp̘1R9$$/ 8uT7m* .>{li&|EiZ($•kv]vܹ YWw{@aq-,,VZ5~xQ%BvV']_~E\}iZZKKK??`y~aw ݻm(:v讯n 2Jo͚5svvj̙DA w~YUUT2eJӦMSj֬9~իK/'n߾}ȑOٽ|u PvmgϺmڴI>skҸ]q"iii֮ݺu>}+<4⭷mPBrrrjjXV{u+WݻwoɎ5*6I&dEVVV]tDEE>0iԩ򜶢ݻɓ'"⤼͛`!CZDڵ8p~gfԕܾ};22R;I YhԨʕ+chתU}zjm;g͚]Iʕ+'jٲeNN] qF2Ω:[FEE5HOzq9?#]Jϙ3G|]XZZVZuڴiժU{߰aCvv|j7n}ÇwѠAQ=I?Q#K6m4,2+r=z:uf͚?zQ$k֬Y|Ç[nM4P*dgr>*k[BE0-gmvO>w׮]f^zOV9@$?T?lewP B06 :.>|@ @@AE(4@$&L r]TV6y75ˋ8@|U]ٿֶy7Q )8`40W֪zm* ߵrٍWA 3SpAE040SY\9ݭm,mhj@.$Ѩ>*Us@.LP h"H@AEX`Ʒ [;k }MDH@sWޢv%K{ h@@Q1@$` 8>JZAZWZ>RH@3egg7aa " Y111j55__߂Vi4x$h4IIIRbŊ 40SK,ʓ'O'&`X<E(90k˗/˽z" " Ysvvj )8  P h"`.<==7nL0F@h`?Y[C.0o^A֭[&(4@$`h`233#""r.]lll  h`O>-;uD )8  P h"H@AE(šā_@ HOTq.Ξa׼6A1E00SvL$)hULMj5qL߸> \UrFC0LP h"H@Le\R*{ Ξa1F@o ZGM# (> ZV&>9(4@L0mmlB KSe\/۝%&``gb4@!$  Ps@PL1w_Q.&2ZM40SV_/ t[5 LXhߜ8rP h"H@LeeeGEߐo4v"&`X$Tnڰ h@1: *s+s@B?<*Vl@ KKK{bTޞ%f͚0E0sN˳]V ˋ4i۶mh4 E()89r\& `p$X"q0@`<.wqq!84UVk֬IpL h`åax! h`4͓'O2# 0ժUЕVB,=1ò$%0`.vxq,;3X|i +`H[Gv (4@L}ݗ111/j5!f4Ƀt_֙j4BSpA@ HHHؿ?q뗚J(PJXXXw-eZƶo>22Pu5%%hTdmڲO 7/B۵kתUYf rݻ7..O>gMIJJӧkXXрyrvv^~}.]WZE@ׂ-/jILH@F}Ν={AoN:?~ŝ;w4hrU]t_jbZj޽sww'0s3f̨R_bb"@ d?+eG0L:UV bŊSLIHH "(A9Yw~2c 3bŊ_K,3fLa4iW_}nݺ-ZH ՆCM2?K^^^ CsMjժڵkv4PCT\>֊0mfqR=mFv%g]oii#G?y}Q4ހm(vNNNC- =СCgaÆ 1|C Xx1T1ڌ,X%--->>ѣ*c eec2`b .%ILL/[Y׋PԫWĉE?#Fꫯ֬Y3wrRL h3ҬYu։±cǤk!uʕ+k׮R"7`IMM6lX) NNN_~TfhϞ=pBdd'o`˖-EoLHHعs7!F,Т Űo߾dggf͚ёJyC `A,--waÆ-[%"F_V h@Y[n_;wlaaA4\z 69r$##֖R3?_ tAP)СCСC%}2ӧQQQm۶-Ҿ7ngJ EFFzyyf7 q?#,,W\IJJ\r֭'NةS~ecnݺٳW^y}~omS8s̺u>'^sܸq_~ѣǣGr-σ1lذ 6Zr ({#Z[M: m0Tto׽RJ}9rd +::ǢФIBVt͛7_xQ(*JNNQFvDݻr#CBBwڵE;!ɓ'ݻWlYWWWq].Bޏof̘1"\r?i֬Ywl7oy/Q@qk?++… EM@XBo Ҍt|۷߿//yh3}|srrm&iӦvrjh!PBϞ=mmm}}} CAC^Μ9#׽RJw9wh@\wq}ͻ{tt*ͺi-ZԨQ7n܈>E4s"V"&N(^yqYYYJtN͛=z~RV7j(--ҥK⒉z?sQ]ʖ-~ ɓ'r?d3;5J*ϟ?ʚ7oT5kVw}Ȑ!FU[nͻVYjΝ;jɓ'Lӛ6mʕ}Y۷@ƺ3WԮ]W\ɷm"}Tg1q:/B:|SZ={T;w.9@Siii\e^ jga"]4~aޅ\BjjSЪ)SH|WwGGG66BU&e~ff<( 6HQUfbJ;YWSv❈Fzݻ,Y2iҤcǮZJ'o)ڵKzȡڛoYf>>>R|o;&@yZb SpNj./ZݤIK.egg9s[n84_J;mAYeX,Onݺ|׮]*0HmK8..^z4Y}=Ν;o!=5D SÀTlٲ]th4*(I^F@P0%$s)KDA[ ݓ / ߿NN֭[uҤNNN/8qBVavɛ,*|۶m1cƕ+WJU)ܹsG*R ..ّr)]ׅjkї ̔>#xzzZ?J/ХF1ɗK4 Ƿo߾h"iCB͚5[jkarӟ;7, `˺uvرQFjzv2vN݃|XC~AΝTЖ#F(_kժӅ711Q*yĜr J@s ^^^" w3mi8vs=Ա0nܸQط ;Dモ&\޹s_n *UydP" u{i?"mT7oΛ U Mw_ .ttt\v<7/;;;>L?ceޜȑ#y,w=yիWԘro*˗9sĉEyΜ9UV˻YJJ8kʕ[|(J/MwcǎM<911ڵkh5Ri:uW>~xذa~ORu۶mժU{뭷 q0aСCUI {Y u&z-ZPiW|+/_>uTy@ʕ+1-^杵YD?OJJz#Ç05Ο?_3>x@\Cɫ\rҐ#G{9˿zj"ߎ=Z}ŋIEv!%^o޼ҙ9F@(!!a ?KKKGGO?eܹ/_~6))I\V3f888=lJJ;~/J}in.QQQrۉNNN5L2۷oܹ=z[nFD-r]7nH nҤ8#yO?bŊtyIsh :4oU< 7|#͆,x6$*[lZknܸ._w9vܹ=zfK,iڴfOUC_ߵ 4?~J;`S*uQrA9UA?~\B|n_ /v͛7{„ ҐX-֭݊v:mX׿vڹsܳ qpp/rrrD?9uT 6mjooiQȐOpB5o\w~ h++l UVhJ/isƆ +2x`iZZKKK??`y~ἦM&*]z\DT:f̘\ܽ{AǔW  f͚9;;}c5sL/[[ݻw?{,*UL2iӦb|YW._}#G@<}W_֭[WЇڵk;88={mӦMaptt?ѰaTq o֧~ҢE]vvwwOʔ)#;vXafaa1b#U\pTTTLL~ܸq=Aqkfq4kkkWWnݺM>}Ŋzvm)^*Ĺt޽_~"PO<uYZZ6xyy͛7oC 399Y4#++Klخ];{F^W\Wյjj߾EΟ??|Yf/j,FBrD]-[W.8Iiԩ&;W[R_III",  2j(t;R=p BKT*߿r|L/^ʯg-?r+ m(/ jooO@/ҲeK+++cW>}<==}pԈݝ;w\]]E !7nX"|KaÆI (Ad͚5}V @i˗7nl`7|ƍ!C[k5{kNk׮e,,,T" Jݗd$&N([h5&''_|٘3~xi4UV¤IJ# 4#F899>|X)ڷo_Æ 9Հk7nh޼'TWLiӦi4`#T'*ZhÍvƯ/;gMaaal2 ch|}}׮]P^=EڼysBBG}d3~xmݺ5**jԨQmڴ1f9?#9s戯 ?KK˪UN6ZjD|XYYZ]vwVWԌ_#^v Ǐ_q>cy. ,6qrrr{4"+V,]4m۶IGGǠ dVZlxb??#G*TСC{m2~xٍ=:--+V$(Y!!!Ry RwbŊuFиq.\0a„֭[+4ƍ4h`̓2~xM>ûv`H@+M6+WfffԩS5k4#[f>ܺukbz#jsFemmv z>}3^HDҏЀYXX,\8 uЁ `V P h"H@AB^/fy x)/ NZ&R  ̔\& `p$jڴ)qB" 0ndElF1#`40S{!# 8GVM첳ϟ?/{A )8@|RN(E(4@$  P5!5A5q hP ueKZME0/{{{! 0g_U8ҀL'O̵0@$  P h[|||LLɟFIQBJ5LOO_`Tڇ%b( 6bbW4젉,$.Xb vNaBso}wa]`ww_\\g3gfw={L}@ 4 ؘ=kr+o40]1 .8A hЛy; a&4GՆ8;$,d H@ 4 `*ZܨbN6cD;^!& EbTUiG -A`tLE͊2Gr=T}L h`ܛ')r   H@ 4 wdZ40Qq rRH@ޑ[>|KH@` 49@CA`t0OsW@~kcggGLLɉ d- hA}@ȌՆǫtvv&J hQ OD29DC);DYXX8880! hA}@qFYܹ-12! k! LTRRϕ22ŋ qrr",Y ]p 4 `*-o*:::o޼,ꆝ1" 0^[.؟~EHu1P8aiCcm# H@ 4 DxÇ/ @6CEe36))d3$27UQ); . #G޻woڵ9r  ,~Aw^0O?4 PL0!$$YfDS@0;w4lذ~'N$05rڽ{wHHH֭cbb hx޼yӺu%J &+W;vlСDl>#ILL޽{pp] LV&M 6{lWW.]|@IrE-b0V6ߥ1H@Fxݻwϙ3lٲD&gʕC i֬Y >Ą'rV--l `h 9tvv&&-}cxرc-'nj & 1Z@g7֭ׯ_DD樒%K>|TRdFdɒ%JXN:1cƼ~z֬Y󍀀wwϟ+Czj*NӀ&Lxboooqb%ͮ]RL ߿СC3fk׮}wgϞ5 Zp=z DD@5,^֖ߞxdɒٳgϙ38+{K>ѱ lK,YlɓsET~H@g7*TUoHһwo{M+WFFF^|9 Eު&M .,YEV2(**jŊ_gJRp] W&z^zDm|mۺwNT~H@Cp[ OKf1ܳgϻwjժEʮ՛襏E˖-W^arvv&Jd$ڸqjnnN45}ի:kccC@0oUCL8+ЀF @e@DD4hyCCCwg>LpwwW,xxxYӧ~~~y7o2bĈ&M2kGyAddd"E>ژF;wNs̹zj;; >}ݻw5j_7o>{͛7߽{7G:u1cܭԶYynݺʗcǎ8q~#//М8w+VС( 77W^ =rH-}qإkΜ9jAprrڹsg͚5Ey޽b^~:*jׯBjtYK֯_ʕWpqmll+ְaCʠͨCCC-ZgϞ۷oj իU'!!!uuvvv;wٳghvY77 CtB㏢f!aaaݻw߻w2Db˗g̘1tЩSƠD_Kk3s6+V,00ի%dWBCC͒r,ɓVZ xb```RRÛ5kV|y-ZJk֬w9?~k.tlmժUgΜyٝ;wFFF쳅Ezϟ?8p@9qѢEkԨqԩoߊm]Ŋ?e?bE5*&&رc^^^b!`1Yr&(00PvꚘXnGN֮]իW̙j߾}RReK^d=Bd&M=zTFr?{l!:n񏝦DaȐ!͓}8::*(Qb…7o/{5su}伹.UһևV'Ntuu5KΘ2]H+W6Zݞ*Iϛ7O˗;Vm-[de:Fv>4E&MzG~' 6ܿ-###.\owg?^bEy.] *;vAecc#NZjUDGGC~iV^ݘkf޼yq +z0/g%,] ,[,9sf09/+SNv0@y~o&9dcZj&[.]?Nm #l{(Yo޼?窅w}'.\N6M5l h̝;W˪URLO4I'OL7ȑC(`"r/B@в 5/!**J[NmԘ1cd?GIڵnX՚$\tacZ{U?~ХK-GDD%wɒU'%%ݺukΝs5j/^XJ1 o߾]>UKSE={F h3 ;J]pTZ5#˾S\ZW&$$\pyYQ%4iҥˈ#޾}뫺͛7˞a3{U#m6|8㰱1tdA7 f4mT!))ѣ;w2,6>a Yn_%1J h1He^zU=zE5}`ac)W\-ڶmۋ/ *$oܸQT.鸺M6ܼy3[988Xe2AHH5<P8 E+^ELLUi\\\2 +B i ԯ_EƮZjĈfIXU{gZLL{fϞ,SLƍ+UTrr988?YfY7BU>jL!Q 1ttt6wܑ'''YYYecWv:u\pO&n*SM@ΝիWJ~M={(/b„ *=B||z rbdիΖ>(Wl4E/<<,9EPN-]xׯG{e.$$A/_\jUׯ߅ ĊΝ;'ֲe1VZ˗>G}U[N???kΠAvqqY|(_Kj=zٳ'|D=sQF ~…S4f?6KjdJ@Ĝ8q,||G޽k0jݱcv۶mN@cb gg'NY(]ӧu˗rqqq _j h}}С1117nؼysǎSlɲ9vڵ˕+g${Ȑ!͛7wB h) RT)ޒ ݞ|ضmÇ-b,̞=[UV̚5KvCannZ;߿+ťdɒN:f+5D$j,̟?رJh͛=z9s|b͛6m͛7nӧOZwު9n]5N:U={֤I(-\s&omӦAOSufݿݻw`eϟ?4iRhh={ZXX,Xttt@Ç8Ν;EYrAHqfKj)NAPfIΝ;RE)_۷:hxѢEs!MLL;{) j֬)ӧE966V§OڵkN/{ЖGŋ6Lj2OtQsuUvp?2VvȐ!3fP֔D/`3[kժ[2ُ3fܹ322RsTƌSfM1lDiذa}|9aYؼy~+ aaa+Wٳ+RPdܹs_xtk׮U~=Rӧ[nb#Uq˖9sr.=X:NAʕ+oBƍe&ZAAA2*D(Zlٮ];kkoߊoaa!*|ݧL2m4Q=޽{'jBboo %TTIxMq/駟w.]Իw'+'H:(ʕK^zE;U|].]T_vFNT.#lɡC6j@ ,:ϟ~(ĉpYQLO {S4vW$J$**ΎϱK'N٢E4ܹsճcfZnݢE!Cd5" .Q(ܹslٲZ׫V*k֬1sSnݺU۷ooccCLEiȈkתTb79ceձcG˖-{ݽ{zxxmk@kDVxbQ5j r_" 22?$$DuVпui})\ݻg߳gOŊՀ׈nwޭ]v- h@]TTTݺu5kV|/!?(!Zȗ9r7.))iƌF~3gh3Չ:#N ssy 1]v͛+W7o޼z{nCN<1M |r___//rtׯ_h3mܸ},Q)?u^wh=С:gϞ 7n\ѢE9Ԕ(QB.11͛7'Opׯ~o>}˴\xqÆ viu떧UlmmsȌFduϟ?6lX@9sѣ WL@/X?Pi&Y(X 8)Ro]ti\\ؼysfd՛={!C =zꫯvȌFdu>zW]]f/qt4;AdPL@lrϟ?U`͛s9vډm(ZoXXXŊ;p@KK .v/_>|XfM yȌFdi?oNd'|և8vi;1&&.\v)'''/I=҈ݲeϟA) 1YYY-_k׮ "05ʕ;}4@YͧON`5jDU#JD$A hA hA hAX&gAVVnnnJ"@]c G .8A`t0 LRؒ8j >o }H@/M6&&_$JHH8},7i҄4}@T= K|!q hRsK*EC`$A`$A`Vfï@roaLC40QvvvǏ'`8-bI7':ر#Ad-A`tԅnFGGO6MGmggǡ4P7h 8  0  0  ͟? 24P@G`$aE&EUT!PXZZ(ezG(kkk777C  0h`ϟ?/uֵs7ZD߲\fMwt0  0  0  0  0+B0n<8@}kYO1`:.Iޑ&Ko2# Ly< >A h0 }X )QD h0 [766QIIID h`bco^--:9w$JJJxV);B0  00oq-cDޑQq-cK"wt0 hism8q",Y`{ֆ0i/mQUN `&¼b)'L@@H@ecmݵէДGz, BZL,III B  0&*!!NY.WҒ~&*.>a(}! NmT|||j )&&ٳg@$%%>O+W"E# LMN2> F0Kn*HqUqvv)tƑ&¢x.!!!883Wqm)Y% {ILL"o2kkkQ&>t6(ˁ"0NNN'#H@*!!ݻwK%KZYf+W… ʁ #34 YS͋/N@FkI'm۶ɲ;! +Rm3gNd뉒wϛ7/aI7D%&&(e|Xrtt$>R21O<^Ν[5M@24x⩍?Ȓ#19sT:#6r MdIja)aEz/&34ȒT_>]OԮ6HE#nV%"ꆝ14 h 0^[.؟~EHu1P8aimKLb㷍h 0 HYċ'>|9ٙph" L}AL8h3RS2))pȊBBB=zT~}BA?~%GD h`,m4iKIJJ:{d.]ի'ܹo߾"E|wM41x@,X]\\ԩw\`vřx5ظq8p%K\f"8_{=ݷo.88y&g"L!,00O?of„ }fÉ>E6id2ɓ2c…[h1jԨg(P@ 177 ?~wյZj&^čɐ!C7o.Bҥl4h 4gΜb-=z4L'O,4`Twiذa'NH4`jrʵ{֭[|pIo^?Q& M6LuȒ%K.\ *ԦMQ֭v}PdQ$cxرc===`oo?fio„ Dʕ+o7;/^۷/ɮ BN:mڴPd]}~}ףGUV`̘1_5kVfb`&LXxwɒ%YT˯#r0,ggC7|tWo괒pJ9o^~ 9dڵ5ju_Ȍ3F:W^+Veqo?~Y|||Ə6066v7nys̒3qb ŐRJ%6?HJJڻwʕ+?.TP۶m'MT@nWK,… E맽#l1֭[7o|̙gϞ{bŊhѢo߾u_7nXx9}tƍ^Z3>?~|˖-G} ؆޽{)SFmb>} ,㥣1847nKnݺG!#u2..NTBK.̙DZ߿jwRO>GDM1ѣŐ˗/ϛ7履~矕e>}ӦMb-2"nnn'Oٳ1<'v>K+_ׯBϟY%@ȞRvڹȲ|0hР+hݻw0zC qvvwܛ7o^hљ3g[n(\޽5iҤ7oވ5k֥KΝ+ޯ_?e;w+֛(NjȈY_>g֬Y3|p{M2%,ƍ+ *88X8vdW^,&V}{$ M6}(O:USС0q|E([W(Q}X$qU}ehh(+JJJ2W\)[\'NΝ4 O*3I`*ydM^קO%K,[lɹr"$U~6m]v={ n޼yڵEAV¢Z *@qŋʕ+o۶M'>mݽbŊbĝm߾]lmbڷoߡCѣG/^<,,L"޺u_M-(Kf-*ٳg(__߷oj>Bʕ+͚5+[s.dG8qջwoq+}Ȏ;nٲEJĜ:uubCΙ3\oҤI~! /\w|XIL7nX_~e"}\\XeY!9&NpeqhѢi6A{_gbG%zu݉*2VZ͞={СׯO>1߫`\5JJm#Dڵk[l1Kn<}rdk׮yիןѣ#""6mԴiSeO?T|޽{?yd%Eĉ!!!ƍ땝uܻwO\~`ƍ Pf СëWĒZ|bb,Vھ}5k ČrJ1dɒ۷oܳgvllnm˗j՚5kڮ+Wn߾}5jԈ9yd=MGjouvv9s(oo3f>}ܿ_{޹sgkkk___z?~%fȐ!_|񅣣(J=݉ý{(Gr*U|G󰰰ڵkZz%1@ԱFGG'&&j_]c7n,8PI@w_~Q/qD+_?,YDI@Hl(lR-΢ʉ9<='Z x}7Ej՛;Jܔy 2dРA7n#M?՞p|'8}wMCBB~7쳤4zS[ѽ{6oެUXuL?(/嗻w5؈RTG)^ϟ?_xCVWKl*N1*T ;vPA jvQ7o>L}tZ4_$3KHs!~h |suubhNV^}! F_6Rbܹsݻ˗;vQ04ռa5'PM-[Vs՞ ޽D>J*JXլY#[چnݺU#""/^lE SR>uTj;7ߤk׮m߾]"sʔ)qqqfɭtt3ghRyDyEX5NNN).j۷E!_|)>0PkUu}h(O,,,L--xݴiѣUO fݩ=A0 q`d_ϟoРA"L,--7lаak׮!K͚5۷o6/o޼'P0$ŖR)H.\84i"ʯ^Cggg=|-wZiǎ=z؟g֚Vm jU$)-j8OO8lٲ/BuիWsA2i: ]5f͚O۷͛o޼ɟ?ˈ#ԾxImv!f?rȃ"##)駟?ĈܵkuM577>qkӦMxxo߫> 6mV^[nJW]pر|hѢ={ܾ}[Z^ι.[)w+VPo…^^^ѪW.ިoyN>~Ǐ߻wOLآE#GjlҥKb+W"bmll+&)"V_}mO:;v,00PVLjժjMAP\~=,,,lw9TU2_ɓ?Ι3g%D{]Bݯ i:Axd3'N ++˗/5` &F2wWB))o޼~7ggϞVǻ<}to^I@;\Y; xOJ'q$ ? hoZݻŝ5|˒.]*kB jc]Vg͚駟>}UVmڴCRk#ˤ k[vm LPhh;v9oY׮]/_سgՆ&;z|)֨Nq-[fkk+_?~\I M@/Zq7lؠErL[l|Wrʹs;<:ÇK,ٺu{TaT8בԬYs||U>}v|L#G>>>ǏcJUVեe'U%#ºww^e8˗/Ϙ1cСSNUA/'쇕fmZĵQ>;02K N>(I&n7""]*=&g L2o޼e8ߐ… rwO>{6 f(qpvvN)UƗ,n>H_[n2s(I5vܹss2iիWҠ|xf͔>S}ժUXf͸+WiD}]tmjvٶm[.NH''!b.]RQͪӌ92>>۷Nln_' EAKо"JbCɼUƍU{ ߫ 1cܹsԖߩS'%%ޥQ/O>m[+.r.]bԩ#aaawޕftuujӦMjeq8c|j׮-j8_>uꔼ*͛7O5 (b Ф]NVt۝;wsRJcիWE55kXx3gN zx܆Iڵ;qDe111SNgl 2iATYV%f?7nl*ߪU-]tg/V.1s-ZhԨQF;vK,$[>qDn_O0TRcCBBpywYQt%K%7"*TH,K.M2e&MdѣGS5k._\s )[̙3+=AAAk~eʕ~:>@Sƍ+VhnnngN6M5hQҥwޭE|\\\4o'y󦇇G モ .hNpر={%gmۖb 4őj۶vY3]UEDcܹr_'Sן4i,Oׯ_^ J*$>3eCʧ+Ν;={ݪv)[N˃yOHm꽍wމv-SnڴI#K&nɓ'ŪS̘vYIyzq -)_}d2*<<<}?B/R|./UvUQ/\мyteڴiׯ_?vӧO'&3KyrZj5jH\M. ׺RkZJLLܶm͛Z+$ĠO{_UdA˷ALдiS{Ν;sT:0½jw Z,Xp..._>Ky*zI6ׯ[m8q1'|RR7o[<<<6lؠUܽ{Yfk{ѱ_~۷q+we_$,_jժ^^^ܹ믿p-(P@;t̙:{=޽{ S 若KuTT͛{Y[=;RO^вRZ~ ҧlٲf7oÇoٲ~S6le ӀB_{tt9i&VgrJHf]U%88XJ(}JeqpT~^RthwKS OgN@9l+⣋ *777UVMfϝ;wI. g^z˗Of>}9VJJJ?N񗲪K:_>}5e4i Yrnݺ'OnѢٳg/={vbb… S])Si"(_?L:[n m (PJ.w!~NtJz .ܸqC3Z@@2T$򯴋/GGMQ8JP =e˖SL:v=w|h0lذׯ/Z( ;vWҵ޳gF%M~/_>t'NWd;wNNNzÇܴdĈ|Ijj׮-*K[ni믿vtt|WիWt˗/Ο?_fwwMzyyI!m[uرO6nx:tpuuMMM?WZ-6~ܸqN,\r/osTo|||j Ylʕ+wE%8s žڨQ##G ;Zx9^zk^RYB|n?5c 00PLD[ݻwo߾](v===ԩ#KKq"r54мys@^"H{/eb,4 SҥKŞ;Bȑ#Ǐ?حb]bj=rZBӍOݻw7n'"dF 耀ң-4=OŧHHDDDsS~Ν;ڵ+:::666۽/TGvnڴ)b~Z-هw߾}-{}uG5#m 0@ƍ kK.I7oޜnB B͚5Ϝ9ߣ7YOz*+=ʕ+◍<^X6͓c7m4R(ocǎ8::4fd$Uwbl+(B5\MUwڰaCΉ5t,' . 4LrZb UXp^gQ oj*/F~j ޶9NDO&%y@,ͪÇo >qDӧO9ɓ''O' nСÿl/_ܯ_p K.6nܨ=] jiLbҥjzɒ%{qWTҍ'իN*DDD 0`˔\/޾}W_}U~'OZsٲesA0Ͱ]vzիWsޱeCk\`g|q۶m=*-Zv-ڣG\RX:[ĩR*%H1_O 4iڴiͻt钜R|Ի}̙,NJJ֭ÇSRRY~qpgwթ9/5k֬UVٲe{^*&͵/s}c޽~3fظqc J.-~y &h&5F~BBB.]6l(6#ϝ;722Rlw]\\._,?tȑM4~˅ /_.ʕ+RA(00PV|ɓ'u}V..ڻ`e˖ffBHWeʔdžo?x@O>D̕ )6Xn} h7D*_{J*}CkG={Zܹ#N:#<--ܹsR:H| gXܢ'?:%_1M@^zʕ&ڵK*TP7̖ST;wsڵ&LHII!O={v^ig^f|f+UTlY)sK#[unذ!!OK]ihϭVݻw5sĈr++LB~cNJV_geJڿ;wVɳs&>䓐6Dl^^^nX~VV~ixmM4wQNKK³h"]߼y .)ҳSSS$Vb ]w_"2qԩSWϞ=uG[&7mT&̫XrΝElll`ii)~F|}}ϟp~X%&&5 'Nlݺrʖ-+uI IJJ^}U???l %ľ }ر_ڵk :Zx_#J:q̈BXj1۸qcsu 9T Jɓ'(t߿I&Ȏ;bbb>ckDIߟ={vС>>>Df=Ν+~jO4heŊO^re=j[0a Wi_#Jc֩S믿6p.dYmWBHpfz͚5+W6q׮]Ri{Y-.]:zM6 :@k0`ĉwnv(RN:U|yi3tΝ׬YsSǎ(F 7n\˖- 4m֭kF(f̘qر`́i&}||k׮MOOҥӧ }OFDm޼yǎkٲ%@1oߖ&(JšFY[oٲeҤIfvgΜ? M&j4`l bѢEf^#KB0  0lYYTGpx!0WN@׷Tz_B40o圥J%  0*+](t$ dt+Ryg*ZFlmm q4)KKK۷oqѣq1Ah`֞;wJ%&p>PTH@ H@P"##O>ì,WWWڵkfq666Z3g>_5mۮ^~0$̈q0&MԨQCR=x@:yd-;ֲeKs@(4{9rd޽,XPfMiZ޾}#?޿[[[gIB;lذ ܹS> _~6l^޺u 4P8ˁN<9׹}XT|29``ƾ?zΎh@YYY˫BҥK1LY[Ln󟤳]:b@]'!!A*x{{1L)R6{A@`ho s1ر#99o!0#_D^z}dee%}z޽.i4`p[lYdLPEgA`$A`$A`$A`ք'[[ۑ#Ge4\J=q@#{M`h 0`0Yy1tQ,RI@  TzZ}rg[9+wjB  h`jӸL@14 H@ 4 H@ Zfur#>H@r,:Vm۶y昘rgNNNG0={Tk׮[rwyg;vڵ+!14`",X~),tyذa}y!`.:^i`D SYݰz({](egKL Z(B[n:t͛副ʕ+;rHc40ׯA eU(،-[˕+'c40S66}`jubb"qpppP(Li͛׳gOG޻wO*ԯ_#FF%&&68MdlZj;vHy+{;9J@>ŭ^7oLHH(_|Æ {*U@zў={lllOb ## (_VjEp"߆SN7hРgϞuaݺunnnnZbe:TV|ѣG} 3 h`>\+ZZpf+V̚5+555?q:twl"[ݲe{s۶m{ԩ5kXȳgڵk'Ybŝ;w  Lg=.'{-1I&fȸp׫V>}{ʕGٳѣG|oLL333WZDi RH:|ϟ6f׮]]vmӦͦMH@kkk6[2B2nРAŔw֬Yqqq0h {{lsTңG={}޽_@B!ݾ}>k޼^zuz!@ay\R|ʕ+K޽;? ͛7KCZgȐ!{+V'-0`ɓ?~r;&7nWGGG77(Z?GsCCC{]L]ve[HJn߾- W\iРцѐK^NxxTSjk׎gfK@KjJNN @ʊʺsܰlʕ+'~֨Qcj.]7ц1؝;wRSS+Tw^YoVҥ:ԬY3[||(|􀆑1\<*_.qLR%]*GkVT)b7nnn`&qē'O^v-33>ضm=AF  ̔ZߣG|`89Ԗ,RI$99Y.[ZzlZٔ)SE# ̔qVV^~ͺedϒEL2&X h+W.5SRRB([,C1G(bիWYYY:jK'''ݣEc@3v!o=йH;'$$)'ԩCPf*+++22R.!++뇅r\\ߗ $Q"PdBCCuuuu%DѺuk)}ռꤥ]vM*{yy4$wwws|kn͚5W ܾ}{q׭[7Q~3f7|#z䉿(888ܹSSG*\Ls'HOzT1} (GRIKKtJzݻYSLqssspp?+W&MRӧO z_JÿȪU׫W#% h3r`P2ed $BT*j Xx1  ̖¦\&rz0۷o眨T* LJ 02LY؛8>9'֘KM{$! 88H@2BQ\9HXbbb~gs';)))3A0;wi&44P̞=;**7HJJ"(juZr[H@Fuƍ֭[s!07e˖駟zT"v.((t ϳgzjժmڴh}8+[5DRId!8#Y~O?4|37sLGGѣG L h?~}dɒ0FhhoLL>QL nj`lC1z(I/9y*d˟ܹ3w&%%EL@X6TZΝ;/^ !mXennn/5Rh{jb|o hsm۶} 9r~M{]]]O>.O=zt͚5{葕пlI_|zݶnȑ#:u INN^vW_}֭sEt҇~BmWw-~jV|򋧧Y.\ u׿"ϲXJںuk~D{ήQF\P&ٳES:rTjcmIL߫ejD0.N_l=<<Çu}ٲe}mmذaRyzndՏ;ѯ_\ZKˏ?X*;w.܌K9she;wuɅٓ'O,6v˥7|W f@JƎef\2`hJ\@)裏rNRRRtk֔)SCBB|K.|TPXH@0Յ3 .]*U믿^^׮]ٳomԳ/YٲeSSS_8.{V^ {LR ;w]6nx߾}]Ǐ {*"##u%==pK?yDί1W=[jUp>})uTRTyu1:Cp,4 kxJڼyIꫯDݻFF?7Y/_I{۷K?pӦM}{VO?V[J*oVѣl˖-l" ?ܹsv?C*oYC%w}7j/O ׿|޽F0ʤAoJ%'X@W}8899s}=_+ hL@@@^9sH7||/ }ܺukgg瘘9o۶pKլY̙3WVoӦMy% e3&55ʕ+wիW͛'nڴiڵ 7oι9۷Opy޽:Gرcdl%:Ɵ5ʤH58@=|0۷o_% q&Lqa(~v]{B@  ٱcGt@@n&b9_.Zi˖-r\ժUK^ou>ƌ矋'|"֕35tИ-![[=zzZ6qM65o<{~s_gF]gȑ˗/QF5k,gT8n:} .g{_|9[?.j:[[FgzI s_$x(_! /' 0KB>|'NӧO9Jɓ''O'΃NTXzԩS%$N Xx2%%%b{իgmV;o?pQw_YfsTvڵdɒgϞ]zuȐ!ݳni~lָ`)cգGʳDs֮]ۻwo"yС=zu!RaƍE~'N䳱ڵ K.Mkٳgy%doݺh5 Bo}Μ9-[Xs׬YASu3gxF)aÆDEЦ,&&/֞8h KKK''iӦѼy.]sigϤ3gδϹؤnݺyyyyxx<|0<<<%%EK9{\{8UVÇ߸qZ^`e˚4i"65!!իنk/[hN2eDhҥw-ˏ?ҥKZի'344TZ͛7&7lP,AHKwoP[-\pA#v+W ԫTKTB46{@kQݳgXŝ;wDT;u$bҠAsJ:t U޽^3foK;v/H1Gr -G̝;wndd87or[nIF)vADdzKX %ȩS|}}z4h4x֭sssW[+V_l١CRA\'7 eǎ02YfShѢ겆H?K*egg7p@܌ ~hhhڵgϖ k׮=~}K]qmllΝ+N4)=?BsW)Stoyn?PbQQQT UhҤRҲbIIID,Upvvѣ.=LK[[n3fREz_Pl۷ʼn`oЭ~//y̋k҃7lpرقbŊJ,^U3og^K9}ԣ\D͛bkq0U&~=kgΜQFαA/"bKEh5jʕy=^F6&L0Pbbb֬Y#W^y><9P"|`ڲ\R:u|D$<9!ML+++;w6l5jTZ?GKad$'qZfMLLLYNNN;v$D%Naaaƍkٲضm[ݺu(%ڌ3;aڒ䲍R!++ٳg9ؼysҥ۴i3vjժ;w.((h޼ySN%0>ȓO7kצwSk z;ի;ֲeK^"myI 6lvZhh%شit+  h9k-[L4?88Ԯ]̙3~'P0y;wJ%Q%''eKKK5jwjX9Ѐ)E^{ ?,<`4uKOO˥J"t(, Pʔ)ϚiiirޞС# rfJJTP(e˖%t(H@sի嬬,5㥂Ѣc++F*'$$)'ԩCPUr\\ߗ $Q"KK0PZn& W^ͫNZZڵkפACGh`9KEHPTڵk'!۷oOP*+M%(*ݺusppׯGFFZRۛ# UV0 hҥKK*ܹ3:[n cǎ%b(`..9~ي8@JW/,JP|L4)(((##cɒ%~~~*TО{qi 6pD4`()))R911QweOOyB\\ܘ1cg=y_vimMR @IKKtJzݻYSLqssspp?+W&MRӧO z_JÿȪU׫W8  SZ*ܹsNJ%a>ٳ2`JX~QF}Geʔ!(AH@<=Z&,ЇR[ŋ &Lc@ 4 99938H@2KKKggghNA`f"ldY0B &PH@3P(Uϋ5: hAc@/"݉C~vZqj  h`jӧO2B  'T`T  0  J$B,g hP"YZFzz:1P(222tmPs5!%B_&%%B|"3և5!V^ݵkWJ8Lj8hg>H@ETCK---===2H;~Uj8hS\9¢Lۗ8P*UFN%&&FFF+Wښ1ȧ8}%BRݻwj4%===N(3 7U4\tԩAĔ)SZj9hПBq!zVPI9hBP Bh`233oܸ!k׮meeEL(ʔ)SVԄĴ4blmmʕ+ggg40S;vʓ'O& @IP(Jiꬬ,%IgC  LBP04 H@ 4 H@ 4 H@ 4 H@ 4 0O  h`,--J%q^y_{_E8E ( (f ,ݻWŶ6PDTFQֆK@$$C9<9̙μ/;Q@ 404pڹsȑ#r^<@v+/YD_&M*UJD+W!4lʕ\AؗÇk +ӓN:ƀ mܸQ#" m޼Y#bY-b?3{̙/r.QJԟdɒ,cƌ_./FvqiOGzգf^pa2= Hd*T?FrƲeˆ%J .oFoܩ{~?]zG[[=hW!Cp 4@ 4B؋޽{m۶nݺJSQkƥBMӣh==Z֣F\@o7n,XI==УO h"! h"! )v80V ==УOrssp h"! h"! h"! h"! h"! h" HH_yӫVڠASO=XbZxt￿iӦ (,_|ŷ~y5jp 't[n2eJ۶m;#O<Ć }vÆ 'O^bEVVV:uN>u@hv5tAY&8U… -[V~/DMzWXYo 6,m޼y*U̙jժf͚;vU!a:lzzw/lٲL2fZ~}6mVZ(H}vʕG]t``̙3322:t0dȐc=hsH6m:'O\rzm۶`+7++kyg Bw}6e˖F UpiکS }ovӦMcKbŊ ̣y!:y=~e˖GW^01###wذa%K|ꩧj[~ݺu۰aCǎ_~Uׯ_׿*Tx׻tR[k-Fffg=mڴ~mf͎8Ϟ={۶m۷\_k&rrr)mhXzmkթS'--- uֵifѢEeʔ:ujF󟳲}~'kgxt7ի5kժN/YdǎO̙7o޼ l_|ѡC-[p NV /3^qK. 7x㞗?ޠ0`Yfig[.(u]~n Tt霜K/tʕ6uMOOѣۃ#<iJJ?^X…^u֢hĜ9sbg߳Bp<馛mV8(jԩSZ;v C^pmg2e7)? B a~7(/_O>{^Fݻ͛7u]6u{Ǡp1taO3τ/x_k}7n,ZlW lٲe̘1aN+V^UTI&aW_nN750-ZC=aO7\C!uOVZΊsrr^uqgSSS333qG[n]+/ /ܱcGX>餓 tԩwSL`W'xbXNcGO :l!gPGx qg 9{Æ 9䐢$ӧ 4(rrr>sMA{(ܴiܹs5]ݏٿkjy>RJUV׼)))G#N:5@b*) y4@u3fSʕ+WBB ڵk-[V_!##cҥE$~)V^z57h:PXD*U8BvM6mݺ5rGE;{-nQhAzzz\\jtݺuy\lق+;B\wŸ}61D{^4vqmƍ >:BwŸ}61Dm۶Xdɒ<$gddh:C!{곉$2eYYYi`8:BwX>]^ @"(_|Y@ C!{곉$5jҞo+WVMJ(QJ=tx;$@M./ {챱rZZZ5ڰaCM@ 3337mPa˖- >!l H7WZU@͟~isC!6Vظqcc}TR/W矽t+: ( y晱ŋ XbPXB-[j:7lذzZf_vm_/Y$,m۶xE$-ZvaayԜ1cFXh޼HguVRۧh:{9*0{ff rΝ5;i͞s_/Y$==hvy4 8j}>$*Ut˖-\B۬YMӧO!lN?_}ԩaTRݻw/R ( A\wuŊ K.3g^{[*TӧF p aaׯk԰P~ص4w66رccվf?34i!Nlɒ%[ok c+XhD OΰawY|y@ѷaÆX'o߾SNA!>|vφ|09 kӧOxCuFׯ5jTR|l>{רQ#(̝;w7AwqPsݢN8H?p_~96Vȑ#MN?[nEsQM?И1cb377=\ʕ A/_ۧ^lYRڵ%Jx9䐠|w~>??Znm@\C=Cbvǎ?ɹw:s@b(Ydn&MjժLUVu ?JMMӧOvvv~ҥKk.>eʔk^y(.o?9t!CĮW\`"633TR{*TpyM0!---x='Æ ۃW\\i=#|֭vڋ-׿UrvKts…?$m/K/[n:u;0m֭[74(8k W_jѢEժUϟ\֪Ukq$˗/o}ݔ` *̞=;--A>\`A"yz'N,[l˖-̜93===(?mڴ)m. 1m۶mԩ~֭[+Vظq?%J2N7&@ٸq)S,Y}J*x≭Z23$dMKK N}-[h"XVD+V6mʕ+srrW~)wqED¿~ 4@ 4@ 4@ 4@ 4@ 4@ 4@ 4@ 4@ 4@ 4@ 4@ 4@[Gy;C)VFwڥ)R4P[N:۶m; K1b_UQF/_K.6(}0<#ӦMu-[69CΝ; 0~Nҥrnnnff5k&M4hР[ ѤW_]~a|@(RSS׋.W^)Wܞlʔ)sws1mڴ9;vmР&=Fvc hZng}ֱcǷzkevv3r֭7ԡCc9lٲjҪܨQTҮ];؍;wMIIy+ sӟ XTzʕ+͛7\ݸ(RSS{Qf}U/b\8Daȑk^l>`Oh p+ 3mڴINnݺu5;NXG]$ "w߭QF6m q+V,?.h+V\r%{aIhJMM֭[ !==}Æ n=FJ Qׯ P3226l(<|͠a5kf{ iW K fZ.\I'}q4b֨Qnݺw6rȤ}a!4222Ν (dff3e˖׷}@q믿{キyKA(5*99G6P4b#)SYfy &~5j԰h N>=,j*%H_΋w233{u0l>p… 7nSL93bosssGYdɋ/`|oa pHm۶zb4~ZZڧ~ڡCJ* -@=.99SN)+\Tݻ'F㧦ڵgϞ?[h5jTbŸXxԨQe˖-#Y3k׮]xqX6lӧO?˔)cH #F #E%6pR!-[̚5kÆ y暊+SOy1Ǵo߾t{w_}ڵkkժu9~ל9s%[z꧝vZݺu/U+z &ծ]SNv)?S.]7ncǎO>dѢEM6 Vx\#GV\9hgeee͝;wׯ_R?֬Y3UT)`A{y䑝;wPBX9##c+VC= 3[hW~QG\ݻ?mѢ?\nSN9%++n۷A-\˗/ T]g}vUW*QĞ߲lٲzɒ%7pC:u҂B0_TҾ믿>SwV[l+p} :[n9裃_%\2o޼.hرO=ʕ+ÚAO?SN?s-˗75j- ϵ^ub^N۾}{6h~{ԨQ{nʕsp;w^m4iR۶mΝ;O:餠6o}饗^=|0hɠ裏 6S0{}{O>|Or?WZՠA-[ŋ#ؑt|T[+bΝ7xcvС{ׯY:uogΜ/ݻiӦrJPHMM}g+Vuy}^N8s=|ŋvW|7mڴپ}ٳ>ƍn?~뭷222bh{ ^cWa/^O? z7{Mjz;cȐ!7|s]t7oޫ /?0C=cǎAͧzꪫ 6I&O|#8??}s|v?ѹs`M1mڴ^{E͛7o׮]2~a0~_?0'''(4 7Я_Mmv4k֬04h|жm۠ynjX0c Ǐ˕+{Z˖-o`Ǝ l뮻W^__|l2e h ̚5kǎa#ڰaWP6o޼3g]6ҿ#(8(J(.]:,ddd{ÇuQ[ׯ߻wW^yey7nܘ ѩS'xo{uY+иq38cҤIfjժU8O> ^ m۶0hР=c>Ӵ믿n8kƎ/s=ŋOMM=w1]lF洴͛?]vܹs򗿄TU͚5ƚ(w*U7߼ Gy_Z:RzI& 6[nv[N-.QnR=-Cj 6'"v_|EڤIr\x3τCOΛ7ov5P\p:{^{nu~ F1rN:u5 K, w;4ocm =-x}Iʋ.\ @ D|Z]zzzto12eʔώ ʟ]=O? }-Vݖk׮SO= 2F&LdV )o|ƍ8pڴi|{}T`z> .6Ln5ܴiS|>1'''/[i]uU5$ 4pyvr]wmݺCW]uU8jժIy_mɒ%}Yek%'|M7oݖK/ ա:|˿j!C);v\fѣwg=3#G&''_r%q5j[.Ġ;찤SNv{=z$ 4pY~ 7ܰiӦyw>^xajjjb:aÆ-XlٲK袋nRJ͜9K/ :Du9rd-Zl|.!?)_|M6m2pܸqÇ_re&eʔ޽{~v#fĈW\qų>۷oZꬳZbE#v߂4?&ի_ylޠY;wnӂr%p ۷?~<6 5k;sf̘k`9]ty饗oO/=m; @✃ bu&1bĵ^#ӧ\rM6-_|ڴi^jՌ3N<[~ǎ'Nf͚p ӂMvw@0@Z~^ۻwo1L)))+Wn޼yǍk׮waڵ|9#}W^yv ҥK333qꨣ ^׬Y7{쩩oI&7tSvǎe]eH0hxu 'qjff^+ ko;57a„? B^*VeH0h -Z(,,X ~Wҥ?͛'̘1+{z;o߿K-[.t6m$''qqF}-h@l̙={\|yvvvRj׮Ow&[?mܸqoܹsرnݺ۷KVj7({ڈ# լYHHh"a"! h"! h"! h"! h"! h"! h"! h"! h"! h"! h"! h"! h"! h"! h"! h"! h"! h"! h"! h"! h"! h"! h"! h"! h"! h"! h"! h"! h"! h"! h"! h"! h"! h"! h"! h"! h"! h"! h"! h"! h"! h"! h"! h"! h"! h"! h"! h"! h"! h"! h"Eq endstream endobj 93 0 obj <> stream xڥZɲ[+R^h)[,D&h܁%?C\~[bvIX\/_:Z,t(kr n-/smݏj?gp0eټ藋q>Hi'XZb]>V;B*˧/q՘xL oJOļ"~l;ﴶTK}գm$73v=|ZQ­~\5E% eWZg:ղLOmz H+iN{ *xB,XMoeiҩJp6u|/2w棰*D5ڲ\׍H~^|/|#u<ϔں>^ RU48, d):f 3|6Xm1T_-7+5ZY*Wh3gv8p6ͥvo6Ycj$Jh͍Ċ> 3(2hہd~wfu e pVM rJ!]8l M BcKq$4Zh#{b`TڨnQoCIE#uPu3o}2tuFnـiKwl&Ͱ2\AnD%(u]'rd%@XZ香HUt:3|&K][#[Pz1."_477T]T$rvqP2+ntd+L,]GL+fZ983m~C4Кy+q+} SPݮoFud73e?#6=o{@eӠsʺ%h%)akXCôАo4' K&df`0g~I+߬2?V'kɬ?"5) ᇽЬ mәmq6y'0AY޽P_xB16_A7|DCM@\:i93Rg3R[#*^J]|@QF;2I 5Zl]_S*@ih?d3`1C;2G1O6ڦ=TgY^;#s%&ּhٸr#9Ԯ3p;}/Ɂ8@';\rF@̉66#B?*248$Dwn3m!:V-m)Zù-$mr{{L'4O UÌqrcR;}Ŵ)%iDʿ7=9 D)m@b V5>ׯЅ^|Ҥs'"چ̸m:0CT*m;.(f%<'IcopzVNɆ~55M- J* lClvj*Oy--۠I'vgΜFms#N6vNd FM`u!~XƼ&(/4pjчdt-(29>f{|}19>䊇 r)qhGȁM}4'% l5Gp:'w߽DKDaMVNR7.ҚRyIeͺ*UqxzبmrcKI%q LŗtrU0(I[eT~QMMZ5LfǎM6Q3֣~;bbVZv5W( nՆm+*fyz3^S,^>0D+KI-'&3ZKX9GpR'|ЯWWi[7zk{9 qS9;7h-2Ldp=mحY\Kάbo87/J#xl v y=15fA_q =3.u5%[ͽ AaJi\~E+:TzS)e'яnCsmr3U_cI0H7ֱY/&8H.dfc' $~ܚ YѪ#wyW /m{*.pPhs3µ}MA#! aP4QI+,mrлbay3C|P^ڬf{!k7-N_MּIZ>WF} Ic9Gid`d^*X ̏f0"] 'gT>Pֿ3Yk"g1).B6 ߖ|3$LuTCywdZy$cqZ&Y"FC_#4W\.khT|U&i:_4~|'K)qD7ړ7FPꬍżJCrt'񧬴G;efț~|cjRY u ^g쨻{mV)?x ?È b9*_uO,`ȨkƩ_*JZF #n[>V5̈́є3q߾Xۚ]贵m8׸S ߢ_sͱY endstream endobj 107 0 obj <> stream xڵ[Ɏ#+*'P }3|(u|_fldIT=3hR3dD0~vfKvKl_p{1Z|ʞlti{ n/os0tl/ [KqrۊsۋKIaqrV?>ņXz$:'W=F}:wJkWoyJf/^ś ol/4[T|*b eCb&/͞k&a))HmY,i]|2ʴK^$\CJfH.jơfa{C&^l2w%45^$zR;,n)i ;ܥ#9)|#ײ͉?8yރvěu}{QAҵy-sWZKZq gwEQI34(W4qYc]KB!j-*Nn,hfr[QovY^]ͬ A)]m$ ig)`*k.U)8ZD>]J#x]>3>a4[[?GQ# ѡokL@?Q5{W EB7!B*A`6oJ,-n`K^jsnOub]K}E#3#]x<ؿZNzmMLGJYMr0]bBH \߷[b8 AcI$&S cUn,)=(X}@ 8`eE9;t ){nCNJ J&6:Ag3H$Aa9~^$3L'۩E):wB&8 KP.4P=oj~{˨$vIbE]A;xO5Ph01шќ4v[.&𑕰0/P@DXÈ$^KYh_ϧ!e5N9.=~MǤOހD98uM"B8`r8"_tlWR Y2tWc'Kn)&'>tBx3d(N!NdJϚ{ >l K&LROuDAMIG2S ¥;8] EkaJv J܍c@؏i_{=GPUT}{TG2Q9ՇjZ?c^TNjd9 ɳ)'ϣ,082\@⡔yٶ-m%?# PfmŨ2Ǟ-t,6Ejis6itM՘/OI {bc<-n^E__uA$&p4u|!X]4M\/+v_TdB:N'Ju?pL9V3J$݄Cz؜Pq}uK"&A_];,e֔{Ke<.:eM{- #XbikT^x0 >"z՞W88k:=#5QڅJǩ2~SيF@^@_4xHL11Tu55N9Lf(CB&r~z' ,8 `eN[pAh88^TNA_p-)mgI(>X!Vx.حGXT.D8eTEhw3rTCnU#"2ch76j7sMnt*vLkNKTqmv{EfnQt(oc1trҥ{ZҼe)|7JzO֒e`Lz0<s!J6K8 (Ҋ8e< պ m%(&MȈ jF/V蘜1tw#uu,m2 * [/x$f(1Ug`clxߞ Up}T564nfrF1ZʍuP 5DȋxϤ2VYk2l/-BęWeofnNPu&J5(T0H--vSyfΑzиcxS05;]Pq3,  F/}l6uJJnj'S@t>OȜ\p\t^ c^R\($~# FS|kuǷ~.WL\k`7j ^6y# ~{_@y{{A<\^;P!z;;4;;Y*3ctjK_)ݯLh`bn7e Tұq6TD)V2txzJ$GʱHMX#\.;w•@ khSѐ|CʋJ/Z\ǜȵ|_+-;`Ct68C؁jA =_O|F)D%z`#C@h2NbR~]VHa,NA%ܹpǔm\!+zjsls\] Zոxj.N,^_҅"A׎N-K4U׶hg}whޑI:0Wjh,kqV:"]Jw QXӒh\kejL|w=n@Y|GA~S+.D[Sc:htQr _n6RjӏZ͍s8㨫0dcoM~o]\޺:i5J"{'*gcϏƧ#9Xsy>$5wE۔q"#Ewl3tOPxujuDwNUvTO<CQO-LyY=.?5( XIo]5~so]a tUv_E;=Ķ&BSQ@]|lr`@ߕ{J9iOMܜOaQ^Ywqt"ס@/ɇgG,] V<$fZQ71Xb͚.&)n,.>챤QG79h傝^.w,狫2wKtDA2j8zAO=_BXB-0G 9#tD2&9 !`rwg[j[qO䧭*mVMMj\ٸL+H j $H_6vڽXbʖXnmqBJ3r,Ov)A\KjvkpmS,NqvcxJxj&'}Kn+tb`)#ƁWZ@eTYcX+50;w1|*dmsyz(< _K(CXXM4i7yA endstream endobj 123 0 obj <> stream xڽZK# WDQ/C&s rh۹dl.IQbwfhخ҃ɏ/-,~Y۟_2,9UWuicteH!/\hO./OKu5AZ\$\!;u '}Px4 賽͑޶/\ۓ6>y3B?φL+<_>=V4K c\_܈_snGT[!q4_]l2۟C}ٟEQȁv)8.m?偧>"oɌrƴ$A0jI;SMko:?=2KS(jub55iOI+=8==w>g9QN99VestQfOhް2#Q(DEe~8*wɯ*ぷ$1˚ ǐ/6Ԋ.u'=2B:mY?7[QV&leu"B#= [ZQBpUH4 +IFؼ 8h_ _bmk$8M!12.`o |E¡0 59DVd vGtbDi_\ C?SxQ,dWk$@Kh: pH+m3[u풬pMOQ# 3Ѐg$BNi:XrDx, L.`ܛgg>tw=MkA܇~W^9Ļam_Yj4E"EAEQ  IV"_D"DeE=O.]`@# nbܟεi' ݸ06E]ddHdE(K ;, r3 ͦTt/MV ;!Uco{|.A0o^:ʚfOf-ʕ7+>ݺSZe'?aᘚЀlq 4\EF;eKfA]=w UNYI/RA:ʐ3BŬXrPٌh[{f=YX07EvG{n jq\Vi0Әa)uhf7,] AE^)|b 犰H .r;|,2Ψo.îuw`BjST@tqj:'Jm1}@"Y OosJwOxܳ}SoLe rO!et)p%5bU*rsu4,=@b {_+ki 䒏#."N=VZLђw`Eq6d&j_7φtֳ^1!aPIZ}(CEb2Mqǜ&oA%Eؠ(4I3pSa/C'NB aS=B@;~`ۃEM^ڽ[g ({%41l;S-2_RVf6M\etk!_8W>R)D2́ 74VFqUGaAi6r v(ڲnUkfP[P#TUT95IQ]D.h宬Q ZV"=0;д#ew<^0>, K9Nrmn&3-_ΙmOrIs+u qac3p0!a3⡨kRdvG>v4C}H~E÷h-{;(VQ%.!?)ŏyb76eu%Ft.QS4Uce@ ։G[@Qg{^7=VDUܮN #@i[Ե &l;+JU:EyK1 ngsYI[ 3PZ6+m2X*sƲ͍S9Frc]}%fS6zGUЖ3M~#W):aDv[Vjˮs͘gOӫJXvî'Ozğ.OIdk65>bt/D=~gəݚt@@[l?,Hyt %ty`b903*ƖB b 1TعN\s9sm[ֺ휼-YkT*pLH?VUaJђ3p@"7I0AkI݅m`w;b)Ny %[0m?ˤ:C7SO$w k3HSgaFs0Gjh˄/eao;OoiM?kfՎo)ށHi#`=\Ob+~}'/MhǘwVh r*EMq`7}>62eJl[.^?j|!u ,-1ڠxI/ be#o7a2<,`kp~œѴ{H+]Gge2phE QFܮy ^<٥kTF4QqG zrGMs_ŕgz7_$}ԷZX7(twoUأƎ\lٹԅ0t)[7\gwQuhw!,.=0~Y`skg mD\ym_]4Il^n ]Զ[V.y hgS7x;Y_(4ɛnQkڹ δ;>36iOjslwlhO}-R;w+bWH)@q1ͲՇa,ZDs1#޹Ƹ}\l۪oU=]DJ$S1E6Vj@k w-M$%hF<#MѮp {ӔuϿ?9# endstream endobj 172 0 obj <> stream x%OLqdOʰZ.|xV2D[s:f ҼTxu$*Ld\MPu˭Kթn91$ D= \jB$Na\ #q.+ UYgV"6.HefqxܬhiN]O642Y+yUOujTh-Wg1u=NֵkizjJUzh\o :ZKޢ,y' 4٭Sv#7GhL*ـoCH[DQ}9]],g{_We-)b1Rl6" up2P4$c{|v⮐3J!8LNq`6 bU~͂BwsD H xYY< vpb?'Nk\0fCDL0 x&d2iam|c wIbI0 !Glq;S wa616>> @la9S~QH : pzSW5IH..}3_#L T#H({$$sK[ou ܦ A`>վ̀mAE/؊V(f+QywX=SVP傖e9ay@X+T )s endstream endobj 174 0 obj <> stream xKqw;,F)ɼ2cۨl96sns?nPT%<?G=TO^02תP H%0'uhۡSS}D9:la63Nz<,U4P LAPSX9hֱU)UyM|3ӷ̝(peGZ{gf_ endstream endobj 176 0 obj <> stream xڭ{LSgϡsdynzNuVp"K7DTZJE@\R"JQ.j14n^dN'.q4neɒ%_K|އ$b$IUk"S-϶o³"~x*A8A8uUz 2jxBL6 J~\MJRV2+[2700G'JB MJL%iJVlT'(YsdggʔjMR$;E,Vd*4Y $\JdJP2]Uh$jBHGˈDBA$iL'tD#‰DE >$bu AbDBurJ$#^%r <ҷ!U l#/~Jowx5`q`z<, ^z0 s-Pu=.3d߯{cZݮ7c)=W.gK[s7PT%m6*^3}iQdtG[_NIx@oCnnSA__JS @uVv9 -ph.yZ_yju)(VCbDӐ"L[9!P Y1Iݱ$\fԢ75vb 3*!aY; y;m'O\BD1F[or0dk5[h0ݛкޚӺAdU= @c"(|[&u%jȄL^+`4v5b4}/G7FۂM挝&AC>s fAs`'7ÙVę ܀ŘxLӡ{dz`GO{PJ?!_ t}aԶJcZBXvwO9Ug2 ,,Ůx%\Ե%TY9ljZZ=TzMǮʻ>k {- E||gBAuq|H4}X(pD0r0?f}!&V>5HaL(<% jE_ecK F!, ,FgVv`!&N~Lݟ\ cȫ`v6.#3+Oty ܷY |kFaY-e3V>},;Fp}4?o=# endstream endobj 178 0 obj <> stream xڵ}PTUeVP80D" %!>JXeA]vW[`wQX]hKѡ(+3̈)sX)迦9gw<$!$I&'mM\=9[?޼U˅2ׇ %{ _+q#Ыzu%/~Bu QQ6O%6n^#YQR͓IUR]\)^3jY\gt׮5 RHVh!O+Ik ْxJ'I*/rWJ^'JZABJ9 %!]BDH$&4b'!!HBHԓR)߅y8ߏ8kc3s;@3 \d`o|s\š 6]GTūp0Ǎ(`A\GKPď i -2Avw.(MO\SG1] I RM|Nyi;YdO%VN󞬾]Gw&A߶bҪswL@ g'sٽTIkfu d^ЛUY]@9L^*e]990tX3̬d,p_wYJD Rba#ey?ox LU9o%P?>T}ʳbl}I*`oh%Ns0'4:qMzzW겡YQ0OOq IY2zϚ.e wqNtz +ߖ }ZCf''3-eڐ9Ę]p"X׳8H >Y"|TvE?R|pKN&]W}8'GOf49v7}mu>JX85[k5Pn"ΪVfUpڔbRXjʏ`6vgSd*=;~#WkBzw;D飢baCtw=H C endstream endobj 180 0 obj <> stream x]HSaN:5fPn@K榈>4V2&!e v܇ۙkd~1}p~>pLHü1"Z!%aXt{t]y<@\.""SrR*gs{6 ctnBh;9fRoiLA\ MIX"ɕ[.AgLM% \DF76FmjFOLd*-͸3z8Nd[lsYٔ)%mm*DS-&m¢mfȏLF)0r #ZXw"vwxh=+iֵ_UmFP% bciY8 }E/ W~ +iZpmayhq ,2TBC1\l-#f+M\ ;غ_M@/#Tz/k) O'c=0;A#OΈO<Üm\)WVxٟ0VCMNO*scHaрCvp^ o{#yڛEy  7Y P Fzp*jA%|Ӵx LL4n;T+`~V>l!/!?A-` endstream endobj 182 0 obj <> stream xcd`aa`dds sv 44 l2 ?d1g!"Ћ?,OAD~j@1VyRqfnfNbs~AeQfzFFcnjQfrbobIFjnb ZRaQRR`_^^[_nPYZZTWr-(-I-ROI-:QH30129|e֩rm-]ڞ!>ۢE 17Ai}3]~T= bǞx;3y?.b>sWfܛg1"Ԅ endstream endobj 189 0 obj <> stream x]n {-61&]^'kD .9ks8G ޝ'%?gp0 7hRY.znx;ĔC~;?rp TPnIA_WCRP qЮya䤆4>AR&evgi[<&% -} :bdLQ+!([HT6GnPkΙ(V Z* *P9C(WAu 캘rԜWr&,zN'qhߤpO;%> stream x]n0 ~ ;@UR Ҥr$J1y=L)?I1|<ɳ3Ey6Nzpj57+֩뤓`{/MhRZ˟;Oeôz>Wnnnz>/ὶz3ޙyx2~|̲r5"{ax^-g=%ջ;{S߶g?ݠܤpO.7kg ,K&QMoڶWW삆aSx5:[7Z_,&!lh[ BA(tFz_:T"A'HF tDQH2" Ђh{"^SAgj+D"eDYB`;SAg"U$ט@I 5)AsYsD$?T[RT[c~J|Dw% z/7o͹,qn3i`ݼ endstream endobj 191 0 obj <> stream x]Pj0+B. =Cdi \$*`V3씇@*#RJ|v PQ w:ԝ(9+lj^WfJ87.mU@ hBs-Ј$f>Ȧjڽ+uIKN5O )uI9 endstream endobj 192 0 obj <> stream x]n0y _vm7RFi" T_:Ϧ_XME/M> stream x]ێ0E ?Nf_@BH"ЋġE&Txo1mh>>{8qlA̿8ATu[zwp.u;(b ߢ|%(up/**U];ܢw]׺&oeyw_s 8LQ 86VXfBznMٝݧi/ۋxyߝW׌2D<[vt>/ۋF Z񳞹o~Dع*~~Z.8VrhȀs (i] >88J%4 H Pb@KӑA*FvhQ AmA`HPADN$:pG)ixP RTW,瀺hxHT^C-43%AՖ=hڒ=al TCgSC Œ:eN5Tޒ,=,=XyTײ$xШKB5‘% b<E'(>Ff"eH&edB}.2Kqaef@K2.>j8] >(ޏWmxkK|o=_,}OQy}- endstream endobj 194 0 obj <> stream x]n0y _Һ?",i\쏚,ؖqV_QBa̙cOڜ'3;^/;.:$Y?v(>uIhv,s)Wx?.ք[M;2;[c3ަ?0|Bc&Y_XN|:K v c*g /k?+{hܤUI]tlvڷ檓ݪAlw\?uMe,v?߶gBY$AArd:`g:bT ,3@ ;%k)U@*ARR*|AUBNTP] T+ CK%ARAKNB9L J8L3$ '*kpBTPT лBOċH7n}_w~qDl{4su[T endstream endobj 195 0 obj <> stream x]ݎ0y _nz1L*EHd#?jvEcS5o_cEQ lϜ3qz.zp,eJ,-j[b7:r Ԙ$=~ȍf"8!wx?kِvCܬqh8e-ױϳ !9S|#_Nh~f}XۻY}C7˶vd}gOkXƌ4yL$ue;ZLӒm j?NHwn}xvDžPYQ.A9$A8)S 茓E Jrh{@Ez^ @ ((8+b+ Y {g$RRWE* 8+(愗zJ UP]^|$3P9Q_VQO*(TYAA04q:.=iWk{A3٢ endstream endobj 196 0 obj <> stream x]n0E|TCxRFʢ58fH-S5_AYЙcxׇAIGij, zREIJ)L-zn~@bʍK}K98j (w3@x餠]aM|Ԯ}di?*|oZM6ӻŶxb+Օ,1D1aQUGXmV_UOr,I^XTH@)G*@*PEZcF!*H f,P2EBe^'eH,CsoSӘS~ YÙQ*T?> stream x]Kn >T*1v^dYJF0]$હ}1XYfX*)Jb58 X5N$v6Pw?(Tkt׋_-:*,Vizlzp?if\L9"ma(:7`dPQ$^ux> #dcNQ,C,)CeI5g?$H kZCZywo^$@HH@H@> stream x]n {.lkҘlxAo?^8C'3O:\EgD*gŷƾ76ҥ^ ߡRvF ,P{m{%h?{03ښ^.c|:HpS"PFC;Cv+fFҴ0}8 N Yul0Ee~0Fk]X+2-5n4pr^N^PUʣj:^6uD6 *f=Ee3P!QMTqnNU\ 1pAST|O endstream endobj 199 0 obj <> stream x]n } .dTUĘlvM؟D .9M/fPCs>!,c%%QKz!;Z  dŤ ,L@BhM7~GSO;1[+ig4H{@ҎqrcRRaQbß7u0UW$B/fJ;a:0BR~^ar& ‡<\u0놃iA<'ڞ7fn;ciT mNH;O )H{)yR)#H%(1R e̥R̗%2|n']K6s ooZU!~yҫʿ+4 endstream endobj 200 0 obj <> stream x]n \vJ4i\686"ŀ06o3F3gp;FVv8&mF{bgu&++6!0.+v{+޹)W?OOք%Npw*wUu_oޔ<סcO6LP 6 q} j>ɲ&c,vgmG嵹)n]]0u]+Q`G~P7mޱ!~LxK5N)DDTUU1-b5**O0ITPiA'$HI1.A g5^ DA MLQrF}i@j9LK^$Wj[H@SAEDЬZt,nOZGjkRA5C} $/DT-:&]^x:Яz7I/t)zO8U endstream endobj 202 0 obj <> stream x]U TSGG~j ?1)򪖥G"Rȯi! !$P, uYA.im,j=mOsvμ3޹͝ (쌘u2F2%p"c8W)/#DU{O}`h5ֺ]W(dAkЏe)O:ke244M}3̓-t:2/z\S>U>bAPUSC[o , ? |r /v\Ȕbo=D!{E *C)fʷur~"!ue],]ђ+_5,O8sOqk:/O5`uHA8eyquB<|9 Az< 9WdžH8 o& tN9^\9?:zq#Zy w|;cߍI)xK0uU436jzMNّnہKoj*m0($Qv}֡XSrD]qW^ɲC=6^6j!F/K -j|Ocj@6@^ߞ'3}g{$| [ͩe:f{IFR]ۧ EIZuI{ɾqJq84JJ"sW6^o Ra *x^b&wpbְn+b sp9"1S׶ #~\/nXyn\GE1Vaـ+#†/;a&!/jJ?2}pS rwt4;իToN. "zBoTA"dL%vѲeMKSH4p{LMMn endstream endobj 204 0 obj <> stream xk```U`Vec[ endstream endobj 206 0 obj <> stream xڍX tef0,:A*,*@oiBKM$_&iӝt.lZE@DDŢ *xw;iZ朦Y{y..H$l}U>3_޸- &q٫b^='+˳ωx1^!Ǻi;.%F$A"F9?S'w} E"7Q> AL&3Yl“C'%RbXA@$V /b-M'6/ b F~^b@'0" "("H TZL \5H)lv&ItM!.;F14푿>nu\]j)dzL;w̍fUY:Fn1e9&X‡;y|y& e<뭕_޲ũ'򨽤О> M,s&apԩI _U;{[8HfhBТHJ_r rAcG ,A( c Tvlp')697HRlu_n[ҝY,I_ӖgC>NV4c&n7Q9|nhv H8\aLZ\\{ysߦyȗ>LM/}k%Go0l*Fi"Œƍ=2{no{NcSܒi4SjU2ChflU*)c##V5<Uv 6i5nen~t5۹v#2s9*w4mQ#gӆlgPf&{Rf O,p`t;)C=0߲ )lͮhgxzxYߋ0mSud2Ex1thgWFc45djǽh#14%4Л óaы#q5E6sܦo2-FSȾ٬d$zM8,є"+[3my,އ? . <1¯!|Cv#.!vvKZkQ*~\< ScEiinc)?k 2BR3r* ):HiL8MPGǯVu6” {JW}{`C;Ha:gA< |ųsY ~.SmC=2`N /K Q]){$s 䨗 ? zUdt9\RSkNg}!pF#)o vӎ@T\?lHᄁْTO'81&~𰑤|C>I:.&a!tX*dY&=A*O;C fY,$-V;x̍Gjmեp7$%:ːX-\Hlz\a!s?"Wooő\Y"{;aЋc- :_Wgu(/ڙ Zv/ ,y4 AA:kDu#yK7OR(2PntZd$%dZmBگ߇cjG el." ߀GAӣYC<־݃mj YJOM_{s/҉v6.ժjW!]A{ R0+ۅ6}oҡV|k/Еbr@c9)|zYpGJ >Ȑж["_;ڳ y9L!+2ʳl6Y_?;#,Y^~ [h|wr@"d͗WQLQ#÷E 49>< C}N AITey.EMN;1SC#T)Up?5zv=iX/k5U22!Odژ6TQMm&nyiCi8NQ;ꏠo+| 3g\Þ,6=T,)̻L07[Y0%Y+U!ԇ ,o ؂lO e;ῷ۹(y*e lh^1ЁerAaX,䠕ڋv2?ȽS'y]XF #ݬI4^eh9h$PtRXKKP s@ɇ>iQ,I0Y:!.2U%%BU96rxA>|) BXX(,6`?--pYZvf$ɲZ: 6dts+\W)m[cc9*yM{sGWkt5j=Z,>bS; ertT=PbF\S730GpwPn:g Dݖ\Y dcG ]oOHg3ʷj٢HS<jMtC"yL{Q#kҽW;Hw[[YNo$#gHؚ)Oؔ 'P&N 7z7iP,>L JA4E%|q{2ja{m̡z}zhq:d +a)2| "\ iݕOpg/Y/ޖc{s{}w>RcF؜ O"8~sΪȖC,%on1Okn0[WhE jJgf1:1'%0q+}ҘYQNKRMVrZ _Gg ˥CrgLwE :^ޮ+jO^ovQ I9,6J 2lmy6 7oˆWЮ`.16mVrʓM:eus#WڮzsYFS##26}˄+o}"B|V7],-GB6_)KF ^D#Fm)+Qa_8wWn övYZd3&d[͹60Vl(ZOPۯٴ!{N}LdHgio%CLLGX!$db錘 ˢ.K)  8 HX ѬcRQ<ȍ [gJׅm]- ]rRO 7g]lcnWT׿Aȯ(ٝߑ-Ý&G[Ϥ*2J݋Wˡ}Bkh?&Pe`ĿfɮE\v7ϚL[_KvB,0VPzK-j׉h~5$`Rrz2Ijl'O%\6O*F91"#e+LԦ:Iֵ\.𼡜oȺ1K g>[pHn% gqO. f]F`'rR"_F9(5KHۀE G hq$Aܓj=h6HXJd2盬fw/bv}: endstream endobj 208 0 obj <> stream xk``0W\G?)T endstream endobj 210 0 obj <> stream x]R[HSa[(g_ٕaEIqmtIJG9gM Z`蘊ŇQ.VX AOA"B8_:(v?ua?Fdtԑzf" dQ- h%uzu@մ~zns @54_`7;lRTmdc81*D 6G:-,c8%Ƣヰ>qJF!!C0.¤LYDEB jR@qĉ(| P ~'=7:xAB!iKJY,]!-Ft.=޳m6V` 0$.0Łn3hk`2[SUdQ.7)JI2k1bvdn?sa|" ېB.o͚C K_eW9d|V~n(<_P2ȶ[51)6j>8?{moJj5cR&wcs 얡>9ͼ3M'ZrҮ֨Rt<.JF\] y,rce2]Q,_8#YlNrStÐc endstream endobj 212 0 obj <> stream xk` endstream endobj 214 0 obj <> stream xmX \֟f&""jFgPH"B . %ȎTe@Qu_PJ"H,VVB|2Y&7޳? ajJH$<ԋTsm<|-Ԅ&DrsNCLG]Ad Ӎ`5C?4VÍ? hĒ< q ksJ8r82NQ8'jcCV9qv'׍=8O}f'+Wjc!8ۿ흯 $%CaX†Gb*aO8N ™IWCsy'E'b!C,"|ń?@,#AD0!VaD8 "(BKD<@$D  |gIhrI &L*tL NV KLS(=Dd2 z9uی04[avqﹽK̓͝?3OQ "^_Ӿ{r=pz0ĹIktР"(t[E~O5C T GBr)H qC`hVh 7V ර%vdo/UA4ȥSNmrt%(Ć&rǮHbP~[x , aK 2P3)poUW#c4CB QX{{HheEOf|u)MZ!3h7~޽wןrQ#ϴm9g**W,ħ8! .$L]n ѷUL,Lj_/DE԰c]f'jz(ZQ ;\z,l zTkEh: oLXA Eod t;yًՑ Yx‡IyUž{@<^tL@ N~8F] #gb'+¬x! ͕:X:ѸrqoQn.f{d\!…/|@n̉WcGi]ɜ|LUi˵Ya1#Oyׄ%B ͵aW,Yz W.Gp/1fE:Mw>Q&' ЂNGzRCo66ݪx QѓH'5ӍI?> }crݣܢ9TJ[@H ΃Sy  *`)SÃYX:  w`vRj+&Aoq{)FdFC=ͭu"Cbm 2 PPgWN}}Ԍ(8uޙ؂ \Ԗ_|GLL;\}ҌO5#0fS(bs> =(J`cXd]FY@a{=0ňEF =<2?bq@8Ě촀%Q|ntOpr^?MK 2,ʡ+F{/6I;eI4[Xʡ`o;S 6 h1cNOYzm;rhĢ~ 7'O( Ϙš0->ujjpn`׵zYe}&eKkwo74bwh%0i j@$QfLZɆ;{ 0  Oziu__?~IDi~]c\ݹKO>mgB/J؊rɈX'gf'gTMti/WY 2',XxֽIf~\ ~0-Zj=Xҁ9Tnr~HeBHQZAN0\MEC`*_|P F(Mc=Q Uu"g$EAY$z 0DFj?;#"4p+A~8^G`zguM VoCfo-T+w9>8>]IOO ^y^0.,)d[p7K.>Hx,{*bSKiC(>uّX޼kvR B]Ƈh?鄊_u~`@[CO=:s=i qQm]588oSNi R R퀌RTj$=ƿ,E|` &ipHTCQ"y/ެw鞭9[srrv27޶kܜܜ[?_5 endstream endobj 216 0 obj <> stream xk``00-{޿{< endstream endobj 218 0 obj <> stream xڽz\W,`Ce\3fXb bEKXʲ.ׅ FbI, FbIL̙.ߜ9OU[:.:nj@נ)'{zK\LK!71 8ڂrC-½-о!0AR_܍M${7]{FX`{coXO-v\3uĄzDNK6. ]i;amODDȜI"$ü'yO )|{]aª qX<1":+837 |k` a`cqxl6MbӰ Cl&6 >>a[-Ɩ`Ke l% [a1{ۀ9bMfl+ sv`.+憹c'``X0baX8IH, bX,ۉc z5o,l, y7?[&ZbQB/,  Y.xo |P`bpߩ)T;FcdW/y blC -la?zĄ'FysQsbl86Xn1pۣFׁbm|VEF/HSfq Y|ܻ~Q58U n 髵\=!@p\#S M=4sH"&n)X,XgS_'N0b}L'?@' x73-[ѳ8-,--OkخfkGxLe͊ŊUV}6HGa=TƐv5 ߰cbR=~Vƣ:Lڸ7Al >{J0p쯐0yTvԽIߢLE@FvUJ'Lx7DDF;dQ?xGrL$d5h1Íl/4{8Y ,Ie5wBWPAJac \8~NR<<;u+hGm~{ ,K6ș sI/gp6hK)Fu^1sTv})ԳvڻQF/8 8 %4N\ $>Hy7<M/@Ad4E!] ,L"J@T.)̖xh@Ӑ r)p ЊWzT6;ইBn%Md\d&H7Ng߶ȹnd*4dgZ>A'! CVskhasBׅWUUU1埮B#s\jVwВ ndв=!46.pPB7g)Xw({^I,P5+(2Њ"Z$c¨⨚*fhx\ܢu,~\F}($v15)¶x3pAZURiTHS0Q[Ѥ=‰M=@J1 0KLgO~Cof4[(6ܕ Nٟ8˒竴ZUPngɊA3{կ?CҤ&I5!g mD9ȌKV1:&'-`"D㠍n;vXd#*Zbn-|)44@ CfZ@L^6Ėhq. '&GqUD&Se)|Os>U9Q:7}Hm| `q>ɣ/,lғW[i4UѳEqp4DhdF{}i*O"s02el&2q$ȫ]ً1/k[ ja7w4W T1, :3o64z&P!i=&kRP \’VX ZRm7/;FbHtZ&ȍ;&p \iЙ^(F!{X: "scWi>X$U81?{GY#h `\hZɪC{$.l XIg9pBr3)f;KTMr4O@qъ):{} TKgtxFadǣG3 l2K);|6?n)Эm4:n!^o*, $1>As]wHvp-KyHZ ".@g!L|PS0 ;Rw~ay;O *Ml CNhLA>h,V4e)"1E"Τ>Πȥ3ӵYo=OnZM)<`[;J-e ,a>!O '~ȆIHJ%Kg c" "A pEh'0.7sIi2fe!vؒ"zޢŜ鄖PqѴ&cLVr<9JIf8Qg8ⳓXwoѿ=imZ)jy?RPq؂a k[ R,vM3r] m|vUߢ6?ӑR?I ɪ$(s tv_gv <Q_g8 + 9uI?'_y|c﹉hйiuhu,Ga, 72{@,Ѿr 9`Q]ECI<>/|**FzFBxNk.}D#ac@Hhy:<- 3 !NݚNH^w|ymF~Hxa}: 7T|KW0&~+gNPzt]\LL |dʙS4ѵjL_S^F!ۮwy,\ %k8%HArY$ !MsykÆn^IDw(}V!^WkL8 ۀo#,VVOo~0>{K<-L}W30 G?i:KTFJ +vJ#8p N_2\uY,gYnths&#SY]2X݌SUT tkJhY'nyc֤JmY83h^Ƒ˪&\U P|ǰȉyYjM^ '_eNZa:M7̦0t;q]_GkC޲Tcu]h1%@èb8M <)Yt;id8Fևʾ"-6ϝ}h@JOz) juYMi ?}-tKW|mV ,u&;*]IM1UaimhejHz/z(bH.Bʂ29&Cn^-ιTou],T| ;J1TI&rRAG#nXeǃ.|g"+N zmT|k:V&1;݁s@N]񮜖;B(CE?)(G8o[b"Udl!pY~Az[Mrl`ڗ%^`Џ@#r ss {TZ 7Mz p&QXѥ@Vȁ4cMӯ }rr'&8Q KM^ ("?4ozQL1UQ6] CJO~3a^i<&R#;T'B}HlV{r7w+a_.pC x_̷5g?E};EuuGN戡n=%\ SYR•t*UDI@ _m1hȒ.Ki)<(?~C~W3mw _ 6G;x@Sw$JI]$4Z&z !sS F_0iQ-ioIQRy,`ߜ,y-oX͠ J2ih8+|fx Qz*P=%аwB=О=LC іN4fdb:8lVщB܇FK͔ۖ:ΐwڽz(h,1LOﯽXL!bHAqdH ȤqUEh)e Z8 .+逪; z!t2Y]/ E](`a8]asMg8M\7ܴ_D*nz@*/"JVRfFvnnHC՚@6. (`pLMZ]C͜ʒ}n/ bfh2ؔ'7u /)RL*rCӹ\̠vfT6M:SYbv-@38)ҪWi^,t2ZmR]*Ee9]r$L/"Ը@ht趔$H%'ݤQ2LK`&Ѽ*MUy9>PAvj.V&(mxXu6սQ<祵AޠL}_Q3.M7_Қm~SzPr2 ~ Ħ C$}]WDi!wfBK'T)Dh.CG\MNu%&Kؙ,98U̷UȀf4<*>pHyi>eؚR*Ga NE)F+DILo]Eo&S 'rc.E\kϞ#UՃ MEL24Ou/ؕ^k:э_6#j~y̜}ף'i?[5-jVx>X*]ɌiL{Jσˠ`rtC{26[ŏ29 j#Vƾ&vxӘ z<ڄ0P7D >f 0Њ<|;Qb8NLa5Ɍ"$'R@'}vHm4`^Q|ؠI&cV,L곾tIӤ桞k5q%=U;qw兹ZQS Wf_aO0wݵx7OpQ_Kgbi_K8xTPX!b p<Ctxߌߜ7 )w]Z ! z>QUrŅ- LȰ[hMU!1aCK隵~{0=FLi!wл^:5YBVm&Sfds? a endstream endobj 220 0 obj <> stream xk``0,TRA endstream endobj 222 0 obj <> stream xڅX X "*c,/c虜.VZ XQ(`UW%HaF6#*źUŭ5նvVkkߙ{?7ɝ{gι3"‚Dv,Ym2B0?in?[#Q<'Y3^nοbFkO:%Jm:7l' _3]i=Q"j,5Bԫԯ"9+(\`ML{+#TALYosr7J:4X/w{=Zj|2BE WȽkjJ-T)#{Z*T.a ?_P5A ի.=x"BO. yF9o MtR8#@X6H=tt^]/\^(Ur/8LmlÞJU_%,5 FMfP3Y[;\j>rQSZIj-@R)?j * ©*RQ*3L-ө&[P ԿDXlY/g,$a7}"F)e#>#nZMl2rlefMIGkLcŶ^`ƃ 6f_ъvܜleL)D8%Z$]iN\jfdYx3BF֮Ł\obM7*F\=qGZ)/gbL.l!6NV Lry7͓sx]BcE[vtۑ ϏǑ%9di2&Èq ?m_@zlQw0 FVsuHU33ve5ܺ1ۛuZezC"tۣΉI8&%Ɩdep PS23l+ܨ<Yĝּ P{2av-{5 Pd4!L g`n̝șNP VmOh-ڍk߾i,~e%nK>JΜ>qi緐I |-83a[6%gntbg/Ñ[Nt5Ilc]UWq\N&"'-pl0Fl|jC΢bb1!%r[N\VX_$A_4mld^/` '!ݒtkȖ,M{0OyZ[teZ3_pO{Vy$㳟q[&@\zNlZ:M::.ct] x2r2c/A v4`W|r=ާzM٤`6l 7x 8KqC_d$~g}ˎ1({, wo^J_gt:{|{7OmGRUsrkpwO{ 8t뒈m]C:)uRK;qGް )As݂I!]- }kyW|0Mo5S$Nh/o2Ú4B\GDn2 ]$ 8#6雟6mlA&!Y+k}|:l֢:+ȎX8-je!vCJa9@qĹ]JT4؆ĝSEgڶ542cl/ SoBUTU!#RҢeA*} b28३x8[8ɻ㎮݅dx{:S8fbjjy^=l//؉rhzػWsLǶTWXP,jV5rd1^wb:x :n2j}GG-ll?=зIUm}!e|ccxI,QVЏ9v\ vVMX5kl!1D5dsh>2,a|d=@&Җz2:Zni $@2VN` Tg &s[`{F&0=| KR%_v'*Ch5|ՖqluCIc{38X*azPH$,ybo na7=Uĝw$GKDW#Tã߇C+u4?U5m{8Ie|xab,?´?@dr)ÍH,T20?f`E.R{G''~8OacB=@Wѕ)<63%% )f{Z&*eꬊ}E{$ R0 w MH (tH #iRs^8X,޹'$5]W w5 Q`)0 }r#cyؚ[x1$[[Np'a7qB&Xd?ʒ َڨ=yͮuUlsa!|h4'pF #7 :qULmzbIFas&@8:mY"mS+l;УgXs'oUڢ:4#!c; ^#Hw}ǴIuQցu%@FSvJVOWT"퇩\zY],N\tR&E' ߋ+tZ6NñY,Lq(6.xR֋o<>)Ļpxj3]39x n=A8^Z#4d$&!cfu7q$tCIj&?^rܹNnܽd{#q2?5jMPzn@:qHk@)KɅ>wn+D<1 ~s!76?cp ׄ𪖄 {=Wzh1\?00F-'#)}? 07l"}GP>>\g(Ui] 0lw36gTg0'4pJڲ7xaj;REaٱ#bzX/ɬep]-v)Qזե>,1w\0{!ׯH"LEKڷdޛ?X\_k+ە?ڲ*/: 1ZIC>egTvsJ#ҺBTQ#YsG_GxAJe'e:__˒3MzҿL^# qظ UgeD J]w) 9̎ f}`s4w/ o#sRg3o->^)625ʌMUm='_'23m`1w@7`-ۮ+h[rY٣ub@;qQf ѱlո.($/&yܦaǛ8H9pέЉѢo WI=bk_G^(q(/o<چz?l9uRv3"!K64%5TgG헅@Ġh.4Jk2;GL(DhfcVxrwn(V[D'i{o `1a͒M*I1g=~`%s\OĆX-;;%ʤXiVYZ1:Tq֍M_ő()NKJ5 j1pf,;A dFH[4FH(tO]׸m!"Ui2}F#x&盰 Lp{!LϛRB YO==ͨoΖnl jv/!>%ILe:%HEL}eԋTVQ#o0DD\ft`5q a%<'KČXL͍x>&/ #r)njB70K'S2EQL/AmSK%FV^Jz8rOetY[wm/(vi=2 endstream endobj 224 0 obj <> stream xk`` j0n㓺U(M endstream endobj 226 0 obj <> stream xmW XW!$K DvEZCRPA> `I E* U[)]( b?FQ@,,RKm[{^ ufL;8fh8m K}7E*+m)*'gYF4Gcs'p ,#AT e.|jnxPha506Mby -ZK-Wh5im)Tqr{oU2w[wdTRNE*e޶M2?/V*dq12e,X]ԲX2%I,8.^-*U 2'#hY"Zi2-A2B#󍏒+rLeqMj;;MJRk˨gvu6~6^6mb*Y\}察R'2_؛ f-V`+՘3[c뱍 6cXmb۱l`n,ۃ%a*,K`q Ā3 |6wǏ6f & ~$VGRQ L<>+`Vs1w,[T,¿~FS'^t[e ݅ZjhD0JT\~pGԲqtZBt*8 ]RXsM!5taIw$%~ژZ~&$7~,y SdrdžF% !)>~\i5ud'fw`7^˛&lIDm8 g$|ɔK](#xO.3̎B(bT 9l1f,濚, lY%@󅍢?;WwiP3e:턚+> IOFef wIaԆuWA tbuzD:%tVuXӡ2XٍϿdr |3#(cG8NRÊfi2U`oI[W,Jp֕wI: 9h=-.-")sTR0\~s}oWc%m6kx6zFrzZd!&  ;Ig2W!^+g%^>5j_Jf?cxr;`~k7,| ̑3pj_d®kz*z6tEfU3YLsYcSC٭a=cm67=i;8vvT~WG%13%n S6+2y³qkJb+LF~\}Ju<N8La"mE"دIy?Y9&-dﵳGiͥߛK:`[o |bF;ljlA6^o1kw J4e[ x|>xc' ǟm+' 0i[h> l:Om'd#3?Ea?LYظO>Es.P>yRDA W|F+2\6%>a7 c6f_@a$0KIo;O.4J-hOSfHy<Up7_K0dL- @/@B; h}=n}-}z!>N.Saߨ 5D _SoUyb_x(ZGߙׅoß?w.e8ZL =.zx cosE%PKD ww :x܍髪`_yzkto&ÐC^?XM3WXzW'{*rOvfa wJKu?ϗaO"HO^z~L&Jcd1 .E#o0Cb:- 9<#("as x#3ӥyl:&*~eu)Ye-E?6n'JtKLw1i8| C<}S߮5S!lHrf߆Į]x8 E^c ;.8~Az=y!+.+ )Th)iriSwPkkaRߕlEe*L& p% z^Hя3ΔIP`"Bo@r_L` 14z6^%L&d:L#og,6f~]z꽒4_GKbW9?NhM{( #ӻqT&Yd bBr`c+>qL^+uunJnVG{>nzd^^w5.a.WWw0B(Xv{y7<6+%DVV~D"N] ۾'E^+`W2ZZ"cB%ע6q2vL nϝp~It19@:IՈEw!I>SoEƓg'CyiRt";fa(>cnfVptECznYpxxAQ'l=yjh‚c(G82QD;7ʦբ((~yi-iW]w1,E5 _!LHrc؏" ¼_|N]=9~ifupp‡d? ,z1=}0jKc朇p_{ҫmԏ_bjuHJI/UCSn߷ ;ۣbwJX`"/;X4QmZr=0Xi45=> stream xk``5ج1aU""U endstream endobj 230 0 obj <> stream x]UPW%ykhrݭSo!VOTTR@~DHB$+6"h4i{ ?{is]ѻ܏ng۷>?3_R*)_0  LV璤ŕ! %Hs9xrEYEV9%0lrMQ{zd>5sr$pGMXO=f[iEKէrOMIYJd^ʯSI&7l=csQ YbTYJyq '_؝6G/B`r fjX.k756gYJSx)|(_MNk6GYrLO39/icRV檌 X+6oDҩ/TIOHRU:*ʥ(#UD( UE9ɉb䕒t,]Fwף訢>E"SSЫ7QZZwY!PZN}z+쿴e6nw YRǿ䲇ʃK7: sMfܲKp5UC&҆ڶ;"?.'>:z4 ?w6:_([$1D|s}iiشm1T; NDDc <ƒ#n72pڦ[;9c7v-#P,֔K͓:h/\\mZ4dЫS3Pє?f `c#D30 :\AFLpRRׂJ3dTr[DxuEvnRT}udO@-#j& n: +8Mb~4  ʚYUMd: 8kLF4\}(>>ú!zuKKkOa $'KHɻ< Z B[pBKIaI &1 9-`$T&~N6}B/{-aGr‹A{xh1ā)hr??%sIrz 3u?s];>j9~e4܇<􅵲}MVH+X[o,l~D0Xa'^-MIަa 0j;{tɼ#];uS}~h{gQ?wp"Dvx;)7mw{xmmEݐs YJ@ (ȺиgB޸j"PۆMv@w꘳؉D!"uշᮎވڍC!Ҙr5ݑӛ?q7k9n#Ʒ0@I5ԡ_0\DQ\ja߲Cu@NRTߡ굿v㽞f?4nj:ib%TԢH89rÛN39Fg>!8e==y/ endstream endobj 232 0 obj <> stream xk```Pdh9pE: endstream endobj 234 0 obj <> stream x]T PUˢ%g4PsEJ&.,KY1X"bH!e`|Lc޿.S5ts={wΙ>`0:!$L<.>I3Hgrz]$.2,ⱋh&~OA'`XKv^ Ep;#Zj|,z @%;om}3hQc, er ,]eTQk|VC)uTR &j(/FGh2'b2d:2h3\R*Y1P^KqΡ:22h=eTTdJR2T&hۛ 4Mܵ>>FSgdr:J; dpERԸHe2z*6*59鿀~2pPDh Dž#'d'vQ']Ml[lȊuL<6 FۢN3g.K@e>DP}L* K:z+luL'l}Z pՎL5Y-BG Pdt*$Пip>$˶oeN6ֲ چjBK87V:y;cV&(J)u(l*|]8[\7 9<WrNή ª*w@^hڊI8p 'ؠ)hY/ !6:߾..@iʡ*q9 ݹi߬jO+0_9>EpdM9*^lo.,.ZDoLlB5\ށOfDs"'ۼ1O9TpR$;fh_>~S endstream endobj 236 0 obj <> stream xk` L < > endstream endobj 238 0 obj <> stream x]U}P^4615Q?h#q8c]F*#Dz #ڊĉf&DH픳6Xcnݣ}Uyf[Ӊ]fK5f҉tv˳" nvVt%QL'jYa C׺Dn&7C:I3KZZJ2g'vXHK#$ ;ǹ6edpteTk "6CٗⴂsJrfMrd lE =^ُ#AԈJYY izsU#aIBIuT[Mf\,|S.)!%vFͺJJK>@h7<:oL1s\ZP k$E.ly8w"K<~Oa0ŕ/\Ҋˁ5!V U?¾5&ϑf(8rmmܼɳbѻ>=<`uI9ER:+!_{, pBx,X c$7{󁵠|C5l.4aB5>[9ق!ὸ N@ M%8APr6FCm"xp̠im q )PJ[<څx43I@l7 1 g[Bu,f5M;^k<,v:\C˅uاQi.`@R%c轛/OR 9jt|!F;fQZbIZI"aR҃5P @ d6ꭻw>^Όg%@FP}A͖y@ yb{ s,pK^~yPr (8>38f/<%+* m 2T E)כ4NFFvZ5>4h &ɱ.0,KkS߼SB0Y 4\.2Bh$Sbp÷rvj7M^c+٤\ĆY ki<~0?`*+^ϷۧSgޤY endstream endobj 240 0 obj <> stream xk````dP`hX| endstream endobj 242 0 obj <> stream xuV{TW0EefY]9UGSV8>j L O%<ViK,Gmۣ۳ϞȺ}w}};L&$/7fnޒ.+13K6j&5i-2? *0dOK_\ 0_!QȞ)wW, اǞihb8ZMx]1K$Sf?(iEL6Hkf lقtRf5YzAr^Hg5Fa3hLl!W3]իM:ѹʹݦb+c%ҹzv0V#-ZVmvsJz-Fަ\z cful 4mji=Z%%DZelIq![RX/aMNvnºٛV',ctULXwMaIblr<,ی 1+b4lZ8a/$%Eyt[\f4D~;QZt-áx.s@ g l)XyV,\i>Qv!huV)ay0`Jztl ^bdpV"L!r}šgT%£Nr3LJy(ʽ^ 55>S]G:Wuz? l\(D wSn s(9B;x>pR 4B7uY{O>)HiQ#L| ^:wNcx(!-Vyۗ\_q#}f rHIPfHg+?Y +L[{$cf'P? tR(&= [J o_w , }}d5zORCu~薨 f;n sq_B( "XOc= ò a}>vE{`f<wZēNO.xaw{sr18v:]Z@t ]4( ';;4a[Q>D pC+I&Xz@<:Wd Z_(L"飇(J s׋]7o謒<_ٹ3"j;1. ?]v.ސ /)_jmGAݜS *A ]U}ڵ=$Sk?PVzB3SUjI};Фn-,0E8XZU\|yoMU4DmCkYLF8CCD2_G){ .ٸΖ 8B.!Ǻ'X8A?0k'=}]p=~!c[jDrvos)kS;Jݜ~^u:z/.[75RY,wBK; ]$Zv :'8bP\B#>)g GV4Q =Уg˗LsKFpb]E:ZbatBxȞ;g/6}IlN{a79\7o$Čj#xMʞQS:VWAYXBcխE~ ~t5VBd;1WYZzfLYEyFo&h}G Uta*O*bK[scvZG /r$̽NgU_?P745wZS?1G?C endstream endobj 244 0 obj <> stream xk```ddPhkXJ endstream endobj 246 0 obj <> stream xڭZ x׵!Fp$3 ǖeL ɲ%c[*Kͻly/š!!-i,kKQo٤M-{gY+=sA&OF8άܿo{$ىKmO&_E?M?B @hCh~v2qo7B'1sC?g, }>C S&!dB ! /ݐ"IJݚ*-[dɊM\azF`ْ%6.lKLI DBA8Em`b.IP0O"$f$f $ii^jn =W"ɛX!HrEonjVjb^j@*NIg 6#D/!LN -RS9k_|1_Xb&ޔbpݢ_DYc]{# $DaV?ox$7;1 apid6Bd"YBV#kWWd# y نlGv ;hd7A#H$!)H*LDd#9!$CdH\6 dE0*ۓN]q;&Dע-y_M̥p < lWAI,l1 3-%x^ԒݱO^Mg"Fc(Shn;V 5PH)\ W-qDa!ìE  J(TT 7h饺q{#נϤ. ы|ri|VhjBhrGy6*`a 6H#BQg4f!3wh# 7<}_MW/R\Tq&/44&Ơ%z~N 7Jnr0{nD_f8sWdw^+(A/'|l.PC<8NS%}W0<Ż-hp8wǽ9|I/ Xvd'n}iCd:*vspǨQBHH\]bv̥)HԨ¨4bJ7:f֨(Jyc$jTIتÛ?T9#JaaG"?p8X7YMv+n4\I؀^K]sg#e?>  90[tZ PS$n?3cj|pf8Ŀ]<Q lJGq0.oS{zQ&mX/厜;c=#gĐTMЂxJ[|3O0|f63w޻[7J Qȑ@N]{+i23t:__K%Dx0K_Sof îraQ~E!5k't.U{^åIIsBLB)p5yqlb*I+bqUZIvت1_$7 #nXLuT/F<[Lg$3Xmh{IA.$ C\ʉ,!%J$26r6rN^eXI6PNӳ%(! lIHJB"_=q .5B#߿},Ň}d˗fcp/h7;gi;CoCl~|XOspf23<'**H*L"8 J,! XePQ<%>~c/QcƸk] Xp<|dd?Z&)9:.aw~n P8]8~t^\Σ1k$*%9ɒk~*w1i均bяX!5;XY%Y Uj2w{_ JD:gn\fjv8Cp9[22#03o>j4SJ  1 7|/k2ut_1v3d*amǾmls rA;ށovϊU@4hu: @-^pwh.˯P1>6v5 *'KQLpgw; CO r\VJ|LXpOd <&f3p斂J ^|&/n~#T: ms8#xg",F!8F!/%=-kMv4{xיkƷ L5vRSxYZTBKc|Tkbۈ늁z8 q'彷ݼe]uD.+ JW/+m&ikJjdXy T2QPce_[]KQw Bb֒hs0]4@O }1ШMGs_u-UÜ繰^tZc{O.Y\(d3cBz]A)zoڱJ2pAen@㭕y'Xx̗cPp& ēc򽩊8CZi,*S㾩BcR{̆jFmwa 4x᱓}yvҏo?D=>8|ƒ(wgw5u7z#-!y\#H K]g#y VNz4jutW:bxȃǁ!Dk(_mL?oGoc_=H$d:(]Fy,aAM0ƶ3Qt-Z5d83Ϲsj(JE2Z1ieo=zt=@Km&C6 5ZM S-&=5oF99 L!MU#7?kiK`u3N |zL"̨Rf|iP LKmYz|O*(?>Q O\޼=imvD?A({OI8g }:mioXt'q{? /L'J.oO'R2FLUppw!M3‰tGڿ.q0ZP o,NRΦdk;X]vGsbGs)u<8`R< w΋@*}.9Ϥ @|V闕u [-³K4ڎ4`g3gޫ?nzA0+/?̠3s֠>UINʃzy!>ҝB:/޷gߌkNavt/~k:pgلgM~j'%ie7__i&jb{ %%ҍ7:&K8ͫ0=߮:Im,".>o>hۼQF9r &Fe9}?5S{\i|jqSN(#Hӱ%~lomarU ˤ0Mown'htW\Le]L6J;WO_||K?ﯫKr{skjIk*C7նT & le&-}劼MKQa,!Ny*e]?/ðQ"(G}Gs@UJOҒp)UMz}G:f7x5Xpz4bjbSdp/zqM#%55Z"2k̭!Km=뱕n'ᇡa9)8yZpXzo~ ˪WV?O`pgM2?}ҧ7uIDZ{Ԛ{l"W2Rfy$Ep-_ pf#ĄxbQX#| t)\Y[)DQTo Ljl~=cn^+vd4e[ub0M&wK|h[ݼFh mpӍf%bFmbY/cO(̈́ovg b%^csgF{Nx~VR&EK$~whSfsHq95XK kFB[qa}e_5Vo*|,.[3C٫ `spY{P')[tpeh Op_"cpʺ3ykm[ah]yOYp'M®)0qڳ*[ە0 YYj^4JoVͬYnsvmdQ_:)pYrs Bv͂y@ldFtEP^!)gqG.t}|t6ت؉L f/l*hICyy y(ІXl9MľaO9pɅpo(2Ʈ!YD2Lr2S)Otzث*VDĖ\ _BFY`%۔?QjaE5vk0\$XVhbvdqRdZ Ʈv>lՖI޲`j`ЪF>^Jˍ MUA)<6_dW[m4BN's=M5ǬntcBٻr;F4EX(-@as< _F>uĚ0Fb1e#GgZ>J8Gip(:-ys;{*OviՃ9}Av\Oc߻jn z0ntxQma. lJ,Ldl &B:LM)df^WzoyLd*E\V63VI~t2)*z^*>Y&^ Uzw ]wo` Q&/*YQ˩Rl/(?e/m&nΰSi5?QTIqTYI0kR7ߔۀ0~[Ek P)"m!~Išz^ȥsjЗpٯA|oz Ë"XlZe{+ĂcqQB> ouC7D2 ^1q>T Q.kקt? s݄2*B|@et8 /4:A^ꯟ)ћ<0\&HBig,coV{hh-a2ټtCcRW` endstream endobj 248 0 obj <> stream xk``0Mkύ7_>90[= endstream endobj 17 0 obj <> stream x\YwF}_Nڗ>#[v,e+;M2Ԑͯ{ nn1NjBe"!e2fZLX)2-2qd!Lp6>JLj33iGL2Aw ")94>ʔA;ũq=qoqG* ht~{Qe#Da Y(l?8r"A)0 BeVaRjqb -YQ1d`!Y&d6rPI.`XF+g-.0I!J[2ABہLc `zh%NfAc$ ҈,JL ,U/$A c;( g9'Ib-ZCBԃ!8 |UI(4'  .ő[̟2p_i5N(g]Hp+N3 D/Wa$ʁ'RDˊЧ7(2(Tǚ*b)|u&ElsNEL;61N, O&fg1oOQ0_!Ѡ*)g $Bi]F"@hCVi*e !460$i. G5IH,K_K@bOXN>l#%ތ` Hq<8@AEDUbb+՜4~ >* gX>;OV* 7ޱ1NiQSzkZ8Äa1d0~KaJX=E$U7zh4JsR*%ў̂ʰi`#RBchb3A L2Fa{B@86$yc*,t 3_w7eq8) -#pDxVRuCT;/ޜotz3sQ\h|YHc-~M&ewgeڱ2z{]b6GQ<L"Ș+x * t$ף$j/䢘a! Hk 1M2*=8Cq!E\'mkkyF^Dl ݫu:354t4䥧Szο:Gh|dҙ]Hg!1F{8S=-^M|\U^R;_L?$rX\dZH/s ND9kh^*״`sA#Ҝz#f.%ڷohHጹhs~ dWԧ)B/e?*nig8cf 9s|1qڨ< %krI'vafs}OO߇^_p"ȿEr/2flo 5Wm-' =[}9Noߗs~M/ʋ Nз!z1j;{h$.*҅ .j^?y8*)!E*\o,\p(rhHBx,0y?]ziXRx鐧g ^8*H3˹Ȭ3@"7IԹs`bGkhmDl&250=W[](w8sWGwUFV/ CΕYk\ow*;]5:y:0˘+E;3Rܻo%T3w0JZb,⪰+pxC";CU3{ )D,N^Ο&j@+b< P&F3IՐ9 2 CSS⮥z d)D-Eyks6gK CE// n]gӷu2ւUtHOcaV]7)>ON۫SֹZA*̣RhŜt.~La9d?,,9 qV&)wP& YhͽsZ3TNʠ9w(KCMdE%#.8im6siv7 v \~_GtZQi/ϾTx1L'='u~*En?Ryg>6ޗ̸M5״ٞ}kZzaX>sz>5Ť#+8hclB12 ok76G܎vc$7߇A+[D3Zg;|_B_ 6ߧ[V41Gd:IWuo|ڵPŗ޸N2LOіZ@:=CpdЎi9eX]~:.?-ur[77Wweʦ7-nZro)頟zS!52Vve& ݇Gl!xy{}z~3:7K͇ͯJ}:o%{̀`:?q ˫LϧWӫ%h#je9pފntpUMy9`tQ.>bԒA-I 1sB wO~Ij;evY ^p+xE&Wu ]*ɻ[>4V +\%Q$>Q,._bX] ֨ _VG%]:|_\,,j?5{@3-fude,hnha$[O9aY||*j ~Xe{ZS-Gߡց~XbldKo]{Z7n0侲ktUzm?}ӵWN!-|nuzUlU ;Ƌnwq%VEцǘCK:F+ rPcH7Gb>;oߞ>Ӌp"Yytwy<0J1`2^ރ9$d0;< лۛiDkUGo3D6ũ'; ~LrڑuZKը,hRE4Xw67'5h9Y<tYeTxAL Kקh_#JvmEOKz(~tuK7lƦٌ?B?e QE]m7L;A/R5BIN Jt.#ldۑj͊;xok87s\6a)]kS,Rܵ)= >|H H΍ܹxԷ6I"%Փ#^~G$KݳBo-:Va6vn<{pV;)Skt &2qANdA&ZJVYneL:` N:ԿRVS [ݴ͘HF]̧3[R8ʱ\(ytbTM}tAzًpSI/{YSU),3 !wcL_`jM赈'5O0kR$v1PUW~ 2K icQI]mJ5BJR|dH~ND2_wluHB1(8E*o%_>~IQwslI}UFibnHHTFIkvn@MLI%BVLGi<*v24`AftOfhHōgyj߷.ʿ`g6Afd7Vp [O{~6 ^L,2I5պ>-tRNtܼTzB<ߝ4DUϳiՂ~qN-\7מV@"Z+ RĐ&Ѩ6G%lsa1z6b= ٮ kfκ`iqOo~e S*NmhqZ0aYXO,jdƉJ #YdC*؁Y [}Uak`Jz''RVCY .T:\*4zߧur^M{CRVk!*_+X'W_W40Π=wM+u]hWEv j@z3pe9=NFE3ly15.<Ǐ 8xV<GuFihp%ul++2D C!cRU)6`lXV Vc f 1֧[KV[H>ZU]n+6[>Z5`lc>GJa`#ïXP% u endstream endobj 249 0 obj <<8196ad1a764799e2cae5cde4fccccc15>]/Size 250/W[1 3 2]/Filter/FlateDecode/Length 647>> stream x5GpMQD^"z]t!w!$Z"^D!!RHN΂ Y `Ɍ0d{;11لYG1ƈs -,jtvؚlN>&mi´ODد-ψQغQ֋eC%i_s 77}9i{iO' endstream endobj startxref 668143 %%EOF unyt-3.0.4/paper/paper.bib000066400000000000000000000253161476461141700154330ustar00rootroot00000000000000@article{SymPy, title = {SymPy: symbolic computing in Python}, author = {Meurer, Aaron and Smith, Christopher P. and Paprocki, Mateusz and \v{C}ert\'{i}k, Ond\v{r}ej and Kirpichev, Sergey B. and Rocklin, Matthew and Kumar, AMiT and Ivanov, Sergiu and Moore, Jason K. and Singh, Sartaj and Rathnayake, Thilina and Vig, Sean and Granger, Brian E. and Muller, Richard P. and Bonazzi, Francesco and Gupta, Harsh and Vats, Shivam and Johansson, Fredrik and Pedregosa, Fabian and Curry, Matthew J. and Terrel, Andy R. and Rou\v{c}ka, \v{S}t\v{e}p\'{a}n and Saboo, Ashutosh and Fernando, Isuru and Kulal, Sumith and Cimrman, Robert and Scopatz, Anthony}, year = 2017, month = jan, keywords = {Python, Computer algebra system, Symbolics}, abstract = { SymPy is an open source computer algebra system written in pure Python. It is built with a focus on extensibility and ease of use, through both interactive and programmatic applications. These characteristics have led SymPy to become a popular symbolic library for the scientific Python ecosystem. This paper presents the architecture of SymPy, a description of its features, and a discussion of select submodules. The supplementary material provide additional examples and further outline details of the architecture and features of SymPy. }, volume = 3, pages = {e103}, journal = {PeerJ Computer Science}, issn = {2376-5992}, url = {https://doi.org/10.7717/peerj-cs.103}, doi = {10.7717/peerj-cs.103} } @ARTICLE{vanderwalt2011, author={S. van der Walt and S. C. Colbert and G. Varoquaux}, journal={Computing in Science Engineering}, title={The NumPy Array: A Structure for Efficient Numerical Computation}, year={2011}, volume={13}, number={2}, pages={22-30}, keywords={data structures;high level languages;mathematics computing;numerical analysis;Python programming language;high level language;numerical computation;numerical data;numpy array;Arrays;Computational efficiency;Finite element methods;Numerical analysis;Performance evaluation;Resource management;Vector quantization;NumPy;Python;numerical computations;programming libraries;scientific programming}, doi={10.1109/MCSE.2011.37}, ISSN={1521-9615}, month={March}, } @book{NumPy, place={USA}, title={A guide to NumPy}, publisher={Trelgol Publishing}, author={Oliphant, Travis E}, year={2006} } @Book{h5py, keywords = {python, hdf5}, year = {2013}, publisher = {O'Reilly}, title = {Python and HDF5}, author = {Andrew Collette} } @unpublished{bekolay2013, title={A comprehensive look at representing physical quantities in Python}, author={Bekolay, Trevor}, year={2013}, note={SciPy Conference}, url={https://www.youtube.com/watch?v=N-edLdxiM40} } @misc{dimensionful, author = {Stark, Casey W.}, title = {dimensionful}, year = {2012}, publisher = {GitHub}, journal = {GitHub repository}, howpublished = {\url{https://github.com/caseywstark/dimensionful}} } @ARTICLE{yt, author = {{Turk}, M.~J. and {Smith}, B.~D. and {Oishi}, J.~S. and {Skory}, S. and {Skillman}, S.~W. and {Abel}, T. and {Norman}, M.~L.}, title = "{yt: A Multi-code Analysis Toolkit for Astrophysical Simulation Data}", journal = {The Astrophysical Journal Supplement Series}, archivePrefix = "arXiv", eprint = {1011.3514}, primaryClass = "astro-ph.IM", keywords = {cosmology: theory, methods: data analysis, methods: numerical}, year = 2011, month = jan, volume = 192, eid = {9}, pages = {9}, doi = {10.1088/0067-0049/192/1/9}, adsurl = {http://adsabs.harvard.edu/abs/2011ApJS..192....9T}, adsnote = {Provided by the SAO/NASA Astrophysics Data System} } @misc{pynbody, author = {{Pontzen}, A. and {Ro{\v s}kar}, R. and {Stinson}, G.~S. and {Woods}, R. and {Reed}, D.~M. and {Coles}, J. and {Quinn}, T.~R.}, title = "{pynbody: Astrophysics Simulation Analysis for Python}", note = {Astrophysics Source Code Library, ascl:1305.002}, year = 2013, howpublished = {\url{http://ascl.net/1305.002}} } @ARTICLE{astropy, author = {{The Astropy Collaboration} and {Price-Whelan}, A.~M. and {Sip{\H o}cz}, B.~M. and {G{\"u}nther}, H.~M. and {Lim}, P.~L. and {Crawford}, S.~M. and {Conseil}, S. and {Shupe}, D.~L. and {Craig}, M.~W. and {Dencheva}, N. and {Ginsburg}, A. and {VanderPlas}, J.~T. and {Bradley}, L.~D. and {P{\'e}rez-Su{\'a}rez}, D. and {de Val-Borro}, M. and {Aldcroft}, T.~L. and {Cruz}, K.~L. and {Robitaille}, T.~P. and {Tollerud}, E.~J. and {Ardelean}, C. and {Babej}, T. and {Bachetti}, M. and {Bakanov}, A.~V. and {Bamford}, S.~P. and {Barentsen}, G. and {Barmby}, P. and {Baumbach}, A. and {Berry}, K.~L. and {Biscani}, F. and {Boquien}, M. and {Bostroem}, K.~A. and {Bouma}, L.~G. and {Brammer}, G.~B. and {Bray}, E.~M. and {Breytenbach}, H. and {Buddelmeijer}, H. and {Burke}, D.~J. and {Calderone}, G. and {Cano Rodr{\'{\i}}guez}, J.~L. and {Cara}, M. and {Cardoso}, J.~V.~M. and {Cheedella}, S. and {Copin}, Y. and {Crichton}, D. and {D{\'A}vella}, D. and {Deil}, C. and {Depagne}, {\'E}. and {Dietrich}, J.~P. and {Donath}, A. and {Droettboom}, M. and {Earl}, N. and {Erben}, T. and {Fabbro}, S. and {Ferreira}, L.~A. and {Finethy}, T. and {Fox}, R.~T. and {Garrison}, L.~H. and {Gibbons}, S.~L.~J. and {Goldstein}, D.~A. and {Gommers}, R. and {Greco}, J.~P. and {Greenfield}, P. and {Groener}, A.~M. and {Grollier}, F. and {Hagen}, A. and {Hirst}, P. and {Homeier}, D. and {Horton}, A.~J. and {Hosseinzadeh}, G. and {Hu}, L. and {Hunkeler}, J.~S. and {Ivezi{\'c}}, {\v Z}. and {Jain}, A. and {Jenness}, T. and {Kanarek}, G. and {Kendrew}, S. and {Kern}, N.~S. and {Kerzendorf}, W.~E. and {Khvalko}, A. and {King}, J. and {Kirkby}, D. and {Kulkarni}, A.~M. and {Kumar}, A. and {Lee}, A. and {Lenz}, D. and {Littlefair}, S.~P. and {Ma}, Z. and {Macleod}, D.~M. and {Mastropietro}, M. and {McCully}, C. and {Montagnac}, S. and {Morris}, B.~M. and {Mueller}, M. and {Mumford}, S.~J. and {Muna}, D. and {Murphy}, N.~A. and {Nelson}, S. and {Nguyen}, G.~H. and {Ninan}, J.~P. and {N{\"o}the}, M. and {Ogaz}, S. and {Oh}, S. and {Parejko}, J.~K. and {Parley}, N. and {Pascual}, S. and {Patil}, R. and {Patil}, A.~A. and {Plunkett}, A.~L. and {Prochaska}, J.~X. and {Rastogi}, T. and {Reddy Janga}, V. and {Sabater}, J. and {Sakurikar}, P. and {Seifert}, M. and {Sherbert}, L.~E. and {Sherwood-Taylor}, H. and {Shih}, A.~Y. and {Sick}, J. and {Silbiger}, M.~T. and {Singanamalla}, S. and {Singer}, L.~P. and {Sladen}, P.~H. and {Sooley}, K.~A. and {Sornarajah}, S. and {Streicher}, O. and {Teuben}, P. and {Thomas}, S.~W. and {Tremblay}, G.~R. and {Turner}, J.~E.~H. and {Terr{\'o}n}, V. and {van Kerkwijk}, M.~H. and {de la Vega}, A. and {Watkins}, L.~L. and {Weaver}, B.~A. and {Whitmore}, J.~B. and {Woillez}, J. and {Zabalza}, V.}, title = "{The Astropy Project: Building an inclusive, open-science project and status of the v2.0 core package}", journal = {ArXiv e-prints}, archivePrefix = "arXiv", eprint = {1801.02634}, primaryClass = "astro-ph.IM", keywords = {Astrophysics - Instrumentation and Methods for Astrophysics}, year = 2018, month = jan, adsurl = {http://adsabs.harvard.edu/abs/2018arXiv180102634T}, adsnote = {Provided by the SAO/NASA Astrophysics Data System}, howpublished = {\url{https://arxiv.org/abs/1801.02634}} } @misc{Pint, author = {Grecco, Hernan E.}, title = {Pint}, year = {2018}, publisher = {GitHub}, journal = {GitHub repository}, howpublished = {\url{https://github.com/hgrecco/pint}} } @misc{cloc, author = {Danial, Al}, title = {cloc}, year = {2018}, publisher = {GitHub}, journal = {GitHub repository}, howpublished = {\url{https://github.com/AlDanial/cloc}} } @misc{perf, author = {Stinner, Victor}, title = {perf}, year = {2018}, publisher = {GitHub}, journal = {GitHub repository}, howpublished = {\url{https://github.com/vstinner/perf}} } @misc{fastcache, author = {Brady, Peter}, title = {fastcache}, year = {2017}, publisher = {GitHub}, journal = {GitHub repository}, howpublished = {\url{https://github.com/pbrady/fastcache}} } @inproceedings{Gopinath2014, author = {Gopinath, Rahul and Jensen, Carlos and Groce, Alex}, title = {Code Coverage for Suite Evaluation by Developers}, booktitle = {Proceedings of the 36th International Conference on Software Engineering}, series = {ICSE 2014}, year = {2014}, isbn = {978-1-4503-2756-5}, location = {Hyderabad, India}, pages = {72--82}, numpages = {11}, url = {http://doi.acm.org/10.1145/2568225.2568278}, doi = {10.1145/2568225.2568278}, acmid = {2568278}, publisher = {ACM}, address = {New York, NY, USA}, keywords = {evaluation of coverage criteria, statistical analysis, test frameworks}, } @INPROCEEDINGS{Koru2007, author={A. G. Koru and D. Zhang and H. Liu}, booktitle={Predictor Models in Software Engineering, 2007. PROMISE'07: ICSE Workshops 2007. International Workshop on}, title={Modeling the Effect of Size on Defect Proneness for Open-Source Software}, year={2007}, volume={}, number={}, pages={10-10}, keywords={public domain software;software quality;software reliability;Cox proportional hazards modeling;Mozilla product;defect proneness;open-source software;quality modeling;software module size;software quality;Application software;Hazards;Information systems;Object oriented modeling;Open source software;Quality assurance;Size measurement;Software quality;Testing;Time measurement}, doi={10.1109/PROMISE.2007.9}, ISSN={}, month={May},} @article{Lipow1982, author = {Lipow, M.}, title = {Number of Faults Per Line of Code}, journal = {IEEE Trans. Softw. Eng.}, issue_date = {July 1982}, volume = {8}, number = {4}, month = jul, year = {1982}, issn = {0098-5589}, pages = {437--439}, numpages = {3}, url = {http://dx.doi.org/10.1109/TSE.1982.235579}, doi = {10.1109/TSE.1982.235579}, acmid = {1313659}, publisher = {IEEE Press}, address = {Piscataway, NJ, USA}, keywords = {Fault prediction, fault rate, program complexity, software science, software science, Fault prediction, fault rate, program complexity}, } @ARTICLE{croton2013, author = {{Croton}, D.~J.}, title = "{Damn You, Little h! (Or, Real-World Applications of the Hubble Constant Using Observed and Simulated Data)}", journal = {Publications of the Astronomical Society of Australia}, archivePrefix = "arXiv", eprint = {1308.4150}, keywords = {cosmology, galaxies, methods: observational}, year = 2013, month = oct, volume = 30, eid = {e052}, pages = {e052}, doi = {10.1017/pasa.2013.31}, adsurl = {http://adsabs.harvard.edu/abs/2013PASA...30...52C}, adsnote = {Provided by the SAO/NASA Astrophysics Data System} } @book{nasa1999, title={Mars Climate Orbiter Mishap Investigation Board: Phase I Report}, author={Mars Climate Orbiter Mishap Investigation Board}, url={https://books.google.com/books?id=4OMIHQAACAAJ}, year={1999}, publisher={Jet Propulsion Laboratory} } unyt-3.0.4/paper/paper.md000066400000000000000000000557341476461141700153060ustar00rootroot00000000000000--- title: 'unyt: Handle, manipulate, and convert data with units in Python' tags: - units - quantities - Python - NumPy - sympy authors: - name: Nathan J. Goldbaum orcid: 0000-0001-5557-267X affiliation: 1 - name: John A. ZuHone orcid: 0000-0003-3175-2347 affiliation: 2 - name: Matthew J. Turk orcid: 0000-0002-5294-0198 affiliation: 1 - name: Kacper Kowalik orcid: 0000-0003-1709-3744 affiliation: 1 - name: Anna L. Rosen orcid: 0000-0003-4423-0660 affiliation: 2 affiliations: - name: National Center for Supercomputing Applications, University of Illinois at Urbana-Champaign. 1205 W Clark St, Urbana, IL USA 61801 index: 1 - name: Harvard-Smithsonian Center for Astrophysics. 60 Garden St, Cambridge, MA USA 02138 index: 2 date: 24 May 2018 bibliography: paper.bib --- # Summary Software that processes real-world data or that models a physical system must have some way of managing units. This might be as simple as the convention that all floating point numbers are understood to be in the same physical unit system (for example, the SI MKS units system). While simple approaches like this do work in practice, they also are fraught with possible error, both by programmers modifying the code who unintentionally misinterpret the units, and by users of the software who must take care to supply data in the correct units or who need to infer the units of data returned by the software. Famously, NASA lost contact with the Mars Climate Orbiter spacecraft after it crash-landed on the surface of Mars due to the use of English Imperial units rather than metric units in the spacecraft control software [@nasa1999]. The `unyt` library is designed both to aid quick calculations at an interactive Python prompt and to be tightly integrated into a larger Python application or library. The top-level `unyt` namespace ships with a large number of predefined units and physical constants to aid setting up quick calculations without needing to look up unit data or the value of a physical constant. Using the `unyt` library as an interactive calculation aid only requires knowledge of basic Python syntax and awareness of a few of the methods of the `unyt_array` class — for example, the `unyt_array.to()` method to convert data to a different unit. As the complexity of the usage increases, `unyt` provides a number of optional features to aid these cases, including custom unit registries containing both predefined physical units as well as user-defined units, built-in output to disk via the pickle protocol and to HDF5 files using the `h5py` library [@h5py], and round-trip converters for unit objects defined by other popular Python unit libraries. Physical units in the `unyt` class are defined in terms of the dimensions of the unit, a string representation, and a floating point scaling to the MKS unit system. Rather than implementing algebra for unit expressions, we rely on the `SymPy` symbolic algebra library [@SymPy] to handle symbolic algebraic manipulation. The `unyt.Unit` object can represent arbitrary units formed out of base dimensions in the SI unit system: time, length, mass, temperature, luminance, and electric current. We currently treat units such as mol with the seventh SI base dimension, amount of substance, as dimensionless, although we are open to changing this based on feedback from users. The `unyt` library supports forming quantities defined in other unit systems — in particular CGS Gaussian units common in astrophysics as well as geometrized "natural" units common in relativistic calculations. In addition, `unyt` ships with a number of other useful predefined unit systems including imperial units; Planck units; a unit system for calculations in the solar system; and a "galactic" unit system based on the solar mass, kiloparsecs, and Myr, a convention common in galactic astronomy. In addition to the `unyt.Unit` class, `unyt` also provides a two subclasses of the NumPy [@NumPy] `ndarray` [@vanderwalt2011], `unyt.unyt_array` and `unyt.unyt_quantity` to represent arrays and scalars with units attached, respectively. The `unyt` library also provides a `unyt.UnitRegistry` class to allow custom systems of units, for example to track the internal unit system used in a simulation. These subclasses are tightly integrated with the NumPy ufunc system, which ensures that algebraic calculations that include data with units automatically check to make sure the units are consistent, and allow automatic converting of the final answer of a calculation into a convenient unit. We direct readers interested in usage examples and a guide for integrating `unyt` into an existing Python application or workflow to the unyt documentation hosted at http://unyt.readthedocs.io/en/latest/. # Comparison with `Pint` and ``astropy.units`` The scientific Python ecosystem has a long history of efforts to develop a library to handle unit conversions and enforce unit consistency. For a relatively recent review of these efforts, see [@bekolay2013]. While we won't exhaustively cover extant Python libraries for handling units in this paper, we will focus on `Pint` [@Pint] and `astropy.units` [@astropy], which both provide a robust implementation of an array container with units attached and are commonly used in research software projects. At time of writing a GitHub search for `import astropy.units` returns approximately 10,500 results and a search for `import pint` returns approximately 1,500 results. While `unyt` provides functionality that overlaps with `astropy.units` and `Pint`, there are important differences which we elaborate on below. In addition, it is worth noting that all three codebases had origins at roughly the same time period. `Pint` initially began development in 2012 according to the git repository logs. Likewise `astropy.units` began development in 2012 and was released as part of `astropy 0.2` in 2013, although the initial implementation was adapted from the `pynbody` library [@pynbody], which started its units implementation in 2010 according to the git repository logs. In the case of `unyt`, it originated via the `dimensionful` library [@dimensionful] in 2012. Later, `dimensionful` was elaborated on and improved to become `yt.units`, the unit system for the `yt` library [@yt] at a `yt` developer workshop in 2013 and was subsequently released as part of `yt 3.0` in 2014. One of the design goals for the `yt` unit system was the ability to dynamically define "code" units (e.g. units internal to data loaded by yt) as well as units that depend on details of the dataset — in particular cosmological comoving units and the "little $h$" factor, used to parameterize the Hubble constant in cosmology calculations [@croton2013]. For cosmology simulations in particular, comparing data with different unit systems can be tricky because one might want to use data from multiple outputs in a time series, with each output having a different mapping from internal units to physical units. This despite the fact that each output in the time series represents the same physical system and common workflows involve combining data from multiple outputs. This requirement to manage complex custom units and interoperate between custom unit systems drove the `yt` community to independently develop a custom unit system solution. We have decided to repackage and improve `yt.units` in the form of `unyt` to both make it easier to work on and improve the unit system and encourage use of the unit system for scientific Python users who do not want to install a heavy-weight dependency like `yt`. Below we present a table comparing `unyt` with `astropy.units` and `Pint`. Estimates for lines of code in the library were generated using the `cloc` tool [@cloc]; blank and comment lines are excluded from the estimate. Test coverage was estimated using the `coveralls` output for `Pint` and `astropy.units` and using the `codecov.io` output for `unyt`. | Library | `unyt` | `astropy.units` | `Pint` | |--------------------------------|----------------|-----------------|------------| | Lines of code | 5128 | 10163 | 8908 | | Lines of code excluding tests | 3195 | 5504 | 4499 | | Test Coverage | 99.91% | 93.63% | 77.44% | We offer lines of code as a very rough estimate for the "hackability" of the codebase. In general, smaller codebases with higher test coverage have fewer defects [@Lipow1982; @Koru2007; @Gopinath2014]. This comparison is somewhat unfair in favor of `unyt` in that `astropy.units` only depends on NumPy and `Pint` has no dependencies, while `unyt` depends on both `SymPy` and NumPy. Much of the reduction in the size of the `unyt` library can be attributed to offloading the handling of algebra to `SymPy` rather than needing to implement the algebra of unit symbols directly in `unyt`. For potential users who are wary of adding `SymPy` as a dependency, that might argue in favor of using `Pint` in favor of `unyt`. ## `astropy.units` The `astropy.units` subpackage provides a `PrefixUnit` class, a `Quantity` class that represents both scalar and array data with attached units, and a large number of predefined unit symbols. The preferred way to create `Quantity` instances is via multiplication with a `PrefixUnit` instance. Similar to `unyt`, the `Quantity` class is implemented via a subclass of the NumPy `ndarray` class. Indeed, in many ways the everyday usage patterns of `astropy.units` and `unyt` are similar, although `unyt` is not quite a drop-in replacement for `astropy.units` as there are some API differences. The main functional difference between `astropy.units` and `unyt` is that `astropy.units` is a subpackage of the larger `astropy` package. This means that depending on `astropy.units` requires installing a large collection of astronomically focused software included in the `astropy` package, including a substantial amount of compiled C code. This presents a barrier to usage for potential users of `astropy.units` who are not astronomers or do not need the observational astronomy capabilities provided by `astropy`. ## `Pint` The `Pint` package provides a different API for accessing units compared with `unyt` and `astropy.units`. Rather than making units immediately importable from the `Pint` namespace, `Pint` instead requires users to instantiate a `UnitRegistry` instance (unrelated to the `unyt.UnitRegistry` class), which in turn has `Unit` instances as attributes. Just like with `unyt` and `astropy.units`, creating a `Quantity` instance requires multiplying an array or scalar by a `Unit` instance. Exposing the `UnitRegistry` directly to all users like this does force users of the library to think about which system of units they are working with, which may be beneficial in some cases, however it also means that users have a bit of extra cognitive overhead they need to deal with every time they use `Pint`. ![A benchmark comparing the ratio of the time to apply units to lists and NumPy `ndarray` instances to the time to interpret the same list or `ndarray` to an `ndarray`. This ratio, $T_{\rm package}/T_{\rm numpy}$, corresponds to the overhead of converting data to work with one of the three packages. Values close to unity correspond to zero or negligible overhead, while values larger than unity correspond to measurable overhead. Optimally all values would be near unity. In practice, applying units to small arrays incurs substantial overhead. Each test is shown for three different sizes of input data, including inputs with size 3, 1,000, and 1,000,000. The black lines at the top of the bars indicate the sample standard deviation. The $T_{\rm numpy}$ time is calculated by benchmarking the time to perform `np.asarray(data)` where `data` is either a `list` or an `ndarray`.](apply.png) In addition, the `Quantity` class provided by `Pint` is not a subclass of NumPy's `ndarray`. Instead, it is a wrapper around an internal `ndarray` buffer. This simplifies the implementation of `Pint` by avoiding the somewhat arcane process for creating an `ndarray` subclass, although the `Pint` `Quantity` class must also be careful to emulate the full NumPy `ndarray` API so that it can be a drop-in replacement for `ndarray`. Finally, in comparing the output of our benchmarks of `Pint`, `astropy.units`, and `unyt`, we found that in-place operations making use of a NumPy `ufunc` will unexpectedly strip units in `Pint`. For example, if `a` and `b` are `Pint` `Quantity` instances, `np.add(a, b, out=out))` will operate on `a` and `b` as if neither have units attached. Interestingly, without the `out` keyword, `Pint` does get the correct answer, so it is possible that this is a bug in `Pint`, and we have reported it as such upstream (see https://github.com/hgrecco/pint/issues/644). ## Performance Comparison Checking units will always add some overhead over using hard-coded unit conversion factors. Thus a library that is entrusted with checking units in an application should incur the minimum possible overhead to avoid triggering performance regressions after integrating unit checking into an application. Optimally, a unit library will add zero overhead regardless of the size of the array. In practice that is not the case for any of the three libraries under consideration, and there is a minimum array size above which the overhead of doing a mathematical operation exceeds the overhead of checking units. It is thus worth benchmarking unit libraries in a fair manner, comparing with the same operation implemented using plain NumPy. ![A benchmark comparing the time to square an array and to take the square root of an array. See Figure 1 for a detailed explanation of the plot style.](unary.png) Here we present such a set of benchmarks. We made use of the `perf` [@perf] Python benchmarking tool, which not only provides facilities for establishing the statistical significance of a benchmark run, but also can tune a linux system to turn off operating system and hardware features like CPU throttling that might introduce variance in a benchmark. We made use of a Dell Latitude E7270 laptop equipped with an Intel i5-6300U CPU clocked at 2.4 GHz. The testing environment was based on `Python 3.6.3` and had `NumPy 1.14.2`, `sympy 1.1.1`, `fastcache 1.0.2`, `Astropy 3.0.1`, and `Pint 0.8.1` installed. `fastcache` [@fastcache] is an optional dependency of `SymPy` that provides an optimized LRU cache implemented in C that can substantially speed up `SymPy`. The system was instrumented using `perf system tune` to turn off CPU features that might interfere with stable benchmarks. We did not make any boot-time Linux kernel parameter changes. ![A benchmark comparing the time to perform various binary arithmetic operations on input operans that have different but dimensionallty compatible units. See Figure 1 for a detailed explanation of the plot style.](binary_different_units.png) For each of the benchmarks we show the ratio of the time to perform an operation with one of `unyt`, `Pint`, and `astopy.units`, $T_{\rm package}$, to the time it takes for NumPy to perform the equivalent operation, $T_{\rm numpy}$. For example, for the comparison of the performance of `np.add(a, b)` where `a` and `b` have different units with the same dimension, the corresponding benchmark to generate $T_{\rm numpy}$ would use the code `np.add(a, c*b)` where `a` and `b` would be `ndarray` instances and `c` would be the floating point conversion factor between the units of `a` and `b`. Much of the time in $T_{\rm package}$ relative to $T_{\rm numpy}$ is spent in the respective packages calculating the appropriate conversion factor `c`. Thus the comparisons below depict very directly the overhead for using a unit library over an equivalent operation that uses hard-coded unit-conversion factors. In some cases when the operands of an operation have different dimensionally compatible units, using a unit library will produce a result *faster* than a pure-numpy implementation. In cases where this happens, the resulting $T_{\rm package}/T_{\rm numpy}$ measurement will come out less than unity. As an example, consider the operation of one milligram multiplied by 3 kilograms. In this case one could write down the answer as e.g. `3000 mg`, `0.003 kg`, or `3 kg*g`. In the former two cases, one of the operands needs to be scaled by a floating point conversion factor to ensure that both operands have the same unit before the pure-numpy implementation can actually evaluate the result, since each array can only have one unit. In the latter case the conversion factor is implicitly handled by the unit metadata. Note that this effect will only happen if the operands of an operation have different units. If the units are the same there is no need to calculate a conversion factor to convert the operands to the same unit system, so the pure-numpy operation avoids the extra cost that a unit library avoids in all cases. ![A benchmark comparing the time to perform various binary arithmetic operations on input operands that have the same units. See Figure 1 for a detailed explanation of the plot style.](binary_same_units.png) ### Applying units to data In Figure 1 we plot the overhead for applying units to data, showing both Python lists and NumPy `ndarray` instances as the input to apply data to. Since all three libraries eventually convert input data to a NumPy `ndarray`, the comparison with array inputs more explicitly shows the overhead for *just* applying units to data. When applying units to a list, all three libraries as well as NumPy need to first copy the contents of the list into a NumPy array or a subclass of `ndarray`. This explains why the overhead is systematically lower when starting with a list. ![A benchmark comparing the overhead for computing various NumPy `ufunc` operations. The operands of all binary `ufuncs` have the same units. See Figure 1 for a detailed explanation of the plot style.](ufunc.png) In all cases, `unyt` either is fastest by a statistically significant margin, or ties with `astropy`. Even for large input arrays, `Pint` still has statistically significant overhead, while both `unyt` and `astropy.units` have negligible overhead once the input array size reaches $10^6$ elements. ### Unary arithmetic operations Expressions involving powers of data with units, including integer and fractional powers, are very common in the physical sciences. It is therefore very important for a library that handles units to be able to track this case in a performant way. In Figure 2 we present a benchmark comparing `Pint`, `unyt`, and `astropy.units` for the squaring and square root operation. In all cases, `unyt` has the lowest overhead, with `Pint` coming in second, and `astropy.units` trailing. Note that the x-axis is plotted on a log scale, so `astropy` is as much as 4 times slower than `unyt` for these operations. ### Binary arithmetic operations Binary operations form the core of arithmetic. It is vital for a library that handles unit manipulation to both transparently convert units when necessary and to ensure that expressions involving quantities with units are dimensionally consistent. In Figure 3 and 4 we present benchmarks for binary arithmetic expressions, both with input data that has the same units and with input data with different units but the same dimensions. In most cases, `unyt` has less overhead than both `astropy` and `Pint`, although there are a few anomalies that are worth explaining in more detail. For comparison operations, `Pint` exhibits a slowdown even on large input arrays. This is not present for other binary operations, so it is possible that this overhead might be eliminated with a code change in `Pint`. For multiplication on large arrays, all three libraries have measured overhead of $\sim 0.5$ than that of the "equivalent" Numpy operation. See the discussion in the "Performance Comparison" section above for why this happens, but briefly, in all three libraries there is no need to multiply the result of a multiplication operation by an additional constant to ensure the result has a well-defined unit, while a pure NumPy implementation would need to multiply the result by a constant to ensure both operands of the operation are in the same units. For division, both `Pint` and `astropy.units` exhibit the same behavior as for multiplication, and for similar reasons: the result of the division operation is output with units given by the ratio of the input units. On the other hand, `unyt` will automatically cancel the dimensionally compatible units in the ratio and return a result with dimensionless units. To make that concrete, in `astropy` and `Pint`, the result of `(4*g) / (2*kg)` is `2 g/kg` while `unyt` would report `.002`. ![The same as Figure 5, but with in-place `ufunc` operations. See Figure 1 for a detailed explanation of the plot style.](ufuncout.png) ### NumPy `ufunc` performance Lastly, in Figures 5 and 6, we present benchmarks of NumPy `ufunc` operations. A NumPy `ufunc` is a fast C implementation of a basic mathematical operation. This includes arithmetic operators as well as trigonometric and special functions. By using a `ufunc` directly, one bypasses the Python object protocol and short-circuits directly to the low-level NumPy math kernels. We show both directly using the NumPy `ufunc` operators (Figure 5) as well as using the same operators with a pre-allocated output array to benchmark in-place operations. As for the other benchmarks, `unyt` tends to have the lowest amount of overhead, although there are some significant exceptions. For `np.power`, `Pint` has the lowest overhead, except for very large input arrays, where the overhead for all three libraries is negligible. On the other hand, for `np.sqrt`, `np.equal`, `np.add`, and `np.subtract`, `Pint` still has statistically significant overhead for large input arrays. Finally, for the in-place `ufunc` comparison, `Pint` has the lowest overhead for all operations. However, as discussed above, this is because of a bug in `Pint` which causes the library to ignore units when calling a `ufunc` with the `out` keyword set. # Conclusions In this paper we present the `unyt` library, giving background on the reasons for its existence and some historical context for its origin. We also present a set of benchmarks for common arithmetic operations, comparing the performance of `unyt` with `Pint` and `astropy.units`. In general, we find that `unyt` either outperforms or matches the performance `astropy.units` and `Pint`, depending on the operation and size of the input data. We also demonstrate that the `unyt` library constitutes a smaller codebase with higher test coverage than both `Pint` and `astropy.units`. # Acknowledgements NJG would like to thank Brandon Carswell and Alex Farthing of the NCSA IT staff for providing a laptop with Linux installed for the performance benchmark. This work was supported by NSF grant OAC-1663914 (NJG, MJT), by the Gordon and Betty Moore Foundation's Data-Driven Discovery Initiative through Grant GBMF4561 (MJT) and by NASA through Einstein Postdoctoral Fellowship grant number PF7-180166 awarded by the Chandra X-ray Center, which is operated by the Smithsonian Astrophysical Observatory for NASA under contract NAS8-03060 (ALR). # References unyt-3.0.4/paper/ufunc.png000066400000000000000000003243561476461141700155020ustar00rootroot00000000000000PNG  IHDR^sBIT|d pHYs.#.#x?v9tEXtSoftwarematplotlib version 2.2.2, http://matplotlib.org/ IDATxy|M9HB$B!T5kV*EMUү׽_WUm5yrqWC`*HPC1DdџqNFIvyC#J`p9uIǏw:^?x掉 /={j֬۷bŊ9-[޽{;ݻW͚5slڻwf͚+W:k߾KYfйsg_^M9_)S'NԤIB@^#`W^U-tYcUl믿F={O]{ZJ{VJJ[Ըq.vޭzJTT)ݸqiLZZƍ+VI&ߥ`˗W~\T^W*UH|9>{6mC߭[ԡC[)}Jr9jjڴiЊ+<0mۦqGyDV#}u[o%k׮9la?$&&jӦMb1R2e\`3fs4}n=777uŊiFN:;w] `Juɓ]{tl!W"6mw%?ހj ~0Ӛ0aw]ƍٚbJ嗲e˺zj>WRݼyDQ bXӧOСCշo__nZZn*UoΝ;[nrz2ȑ#eaÆyyN[m]X|KJJ2,vBCCիW/yzz:o>ƪbŊj׮s+**Jwo;wtҪ]7nl_fR2P+I7nܨI&ڵ>CY, {g?~*Wg}V6mp'___=6l{= ܹs]ιtRթSG=^u/[oFuֺ|r~>9q5*U҆ 2}eUP9>lrJܹ霑#GfxNHH9&Mrӿ㑑~zZJ]tQJ4h EGGۏw[7nÇUre 6L֭[ժU+UPA=zЄ 4ydjٲ*T#FŠL/00PVO?$M2E5RΝ:<󌒓n:Y:g|٣[fǏkʕZr;rƍշotsx (QB?VJO[lI;wTw|Y=O?9ժUK˗WfCf҅ gϞ {jl j9TR?_~EK.Ujjjt]~~~_b ݺu+s:u *{?O{L2z?դ$-[L׿yf;INNol6>3ݼyS .Rׯ_א!CrJ/T׮]UlYl6m߾]]v͛7駟jɒ%ZrZj L`ҤIDov#sΥRSSUzu={֡{goGEEAtøQF^G:w׫J*:|9駟jĈ};v0hܽ{z)oV Z!!!8pϛh͟?_>>>tNҨQwߩyҥʗ/_UsULL[]vm̙3jӦ֮]k>_{S?j׮P4o;ߺuK]tQlll\H"z}^4nÆ Zn9jժ9۴i AܹsڼyƏD1112eZn-???>}Z/֨Q+h_Bye뫯r ;8j„ NgӒqJ+I=z9-[Tɒ%sYriر2d+㯽{YFaaa3f_?z!_|E׋QvtU^]֭sJR~qqq2'( S~|V}QHݻW]vu:QF9mݺpIau::t({‚XbZzY))5r]rfʕ+n֬Uj Vjڴ{HNN'|v)!!_|ym޼Y?<<< ul{-[L/_)g)gUW͛5k{νcccuycmۦիWKԩmfZӽ'x"s0 UhhVwM0A7o6)RC=sy̶X,N3f]uqEDDܹs_f=Z9rt颟Y͚5p7|>RRR4{L+9dU`9UVuv˱}Ů 4HfRǎMfyP50B 4|ˡ?--M}я?~ؠֽ+ysz^Nٳq-TmW_UӦM4oҥ?}W8qb+{߯3gi;m՗\m۶ۍ75F#F@(|K.$7[6#oZ5Էc~< :`)Sȑ#ڰaCC4x`-ZȠ >Wa\V%";wΡoP%P0xy{{+44T>>>N}]޽ۀSkVǎ\n͛혘*$UVM˗/wzmJJz衋/Trbرݻwzj-ZT}򐇎=jt `(`yM6ͩ?**JݺuSbbU!'ռys3go޼Y111zgU\.tޠJ` ?5jԿ~ZʀS^b.]$IZl$6;CCCu֭l* C 7o4iO8TtY5kִ5{lܹ۟sg+Dn;whΜ9ٞgРAZpan!`* _Xbn%͖\$Ţ1c8͛7O˖-۷գGkMsPj-[:M2Ek={Էo, A T$Io߾y*U5k8 LWGDDZUz!|jjjзo_1119r͡} ߹sGپν?Ӵ,[?+WO?ŋtR;~_P0 .HΟ?s5kL_~eνwp||6mڔ7nhƌN9Y*r:#M97oZu-UREy~{?Wpɓ~Y2ýcbccXa|Vsg3y׻ jٲe.m;V}ՓO>_A T8 鏐ɓ=_ĬhܸÇ?OOٓu~7v|||ݸqé/++:/]<vYGZiȐ!vׯ_\'thYsfo޼C>SNe3g8oܸ,՘UW rSߝ;w\}g/;GEEwW߾}5vX3F]tQŊ5m4*UJ~m@aB 4:SLL33c -f3;EFF^z=zfϞ'UVjذl6VXbŊ9/kњ3gSvMGt aWAPзy WiXeܹs~yyyi}N}tYfiڴiׯ*WoFK,Qz/_\ݻw?NݺuK׿,՘Ճ:]ԩS;8q"sQ]r劖,YiӦi ۷榥K*(((Z0r_C 111ڿ-[E9mzKZR r|ׯI& Ws7n:^͚5ڵkU|y;-U~iuA{̙HZ) u]+Wրh׮]:~.]2 O? cǎӧkZ~իWWΝW^yE~۷{zTjUUX1ϟ{Qխ[WԢE b^=zfΜ///-YD:u҄ ۏYV5ixG{ٳ uZ ~W˖-ռysUPA[v ʋϪ?e޽0WJF9]ѣzVkjҤIѩS'exT\9-^8_… m?y_;vֿ,s?ﲞҥKۦNjKNN.]6x`ێ;l6[rrrܹs6f8qbرzY:vܹlvx?,sssI͝;7_g}ޭV[nߓf/YV[vl!!!k׮97zlOaYMLLլYfX2B ofl׮]UV-{TmϞ=.#556g[ʕ]n2dҥKY@ `Ȇ;wƍҥK ݻuUvjݺӳWmۦ'|RnnnYɓUu{ nҎ;$usw!bŊ^dtQ]zUŋWժUժU+,Y Lo3fz0,X Iz  `޽[O=TpY,K ϰ`Z6Mcǎ$;#ӵo>+WNVZZ*///UTI{֎;ٳgk񒤿*^x0Zbzƍk߾}Z4/кzj˔)˗ PhuQ^^^.hΝz*C (Tm۶駟Vҥkʔ):~֭kt+ & `0 `0 `0 `0 `0 `0 `0 `0 `0 `0 `0 `0 `0 `0 `0 `0 `0 `0 `0 `0 `0 `0 `0 `0 `0 `0 `0 `0 `0 `0 `0 `0 `0 `0 `0 `0 `0 `0 `0 `0 `0 `0 `0 `0 `0 `0 `0 `0 `0 `0 `0F(nܸ]vrww7"@A .O=||| |9k.u2ڵk /]40[@r, vZ=#U.A m^xxãwf?`@V=#]AKBB|}}G}TV?E>       (jt"777 0 VUAAAFdLY IDATLLg ))IK,#777+vXm޼Ysj„ c~z=ͳZh"ϯa@~hݺ7nH>33Fnnn1֭ӝ;w,@1vX+WhҥǗ.]ҥKҀF׮]Uzu{?~6oެ^z݈аZ{СCھ}$iJJJbg<PO+VgΜ)kԨMU`8`إX~rwwȑ#7jΝڹs^y,aXSSSsF(E.Ashs $>}:qǎshl<+#C￯[nfgϞX{z뭷.Pd<uj߾}u̙3ZØcǎiԨQ*Yn߾-IrRΝ;:tM&IR۶m~~~~w=CժUիW x jٲ,Xj֭1b5kS*00>vܹZr>lKIIQtt}ƍt{YvLLL~Uwϒl={ծ]tm#x 4Hݺu/]Azgd-]T/yZdve[d]vܹsN״lO6nܘa`|/???uUTdI{K/fo۶Mj۶5ka0a h@Pbъ+4o<}:v,TݻkС*W$jժzԭ[75iDV l٢oqj֬M꣏>R5w\͜9ygϞU``fgZ{NrJuMY%Jh˖->|٣ŋQF5jy,4lJLLeX W|@Fڵ.UѢEa5o\%JPҥձcGڵKcǎ5<@" H+,,R`ɓ'ŋj׮n߾mt9Zh0)&&F6lP˖-. (z'ԴiS7S}.^^F F͛7ϫro.,Xm֗_~itI upz6J@aB  iii۷.\ 6XbFt}ھ}~7I6m=zSNZCV4b}jӦz왗ck Y-*l  _~;}zG.!>>^T׮]yi˖-T|y-ZH/6l#Gdyމ'L2>|~<yZ|k6E^G W^{ァ Fv:q&L֭[+88X_}駒QFZzu.UƏhM<9/oB,5 ƍ9sflkԨQJLLyxxh֬Y2d}Ϗ1BZv<<<\>|"""4sL%%%_~ TF2Ȑ!C_={T֭Y ҂ ۆ֒-Z4=s8pRRR{>zmۖ 8P4|_ Ǯ_) +$$Dԯ_\A Ѳe-_ ,Н;wt!]N%IoVHH Ƿjʡe˖,]I&*Wbccv X){`%0 lƍ4h`t9y+^Gp|JڿkcZվ}{-ZH˗/W߾}W( tvƉp Z0`+VHڴi#bp5_>N5;ׯK֭a=駟4|pժUK^^^Rz4n8]x1~AUӱ;whꫯp,::Z;vЎ;4uTZ*ݕWVBBcցTZ5 2DDDDh޼yZfΜ9#ooo1aaauI~aÆi…NÇOս{,ϹqFug'IIII:uN:~ռyss-_<۷n:u-_j5BZZ6nho[V#绻O>k<L曊o|r'{yyAz衇?+22R6M#GTvTz,͹pB+~JNNÇ-[K.iÆ ʗ{ͺΞ=g}akPPjժ9rD׮]SJJfΜÇ+44T%Jp|0mذWZ5IR-$5mTSN%I'N/Zh2RO?+/\_~E6MQQQj׮V\Ν;;5c }'m߾o۶M=zpXI\\9կ__ڷo$ڵk:u7ntcǎI|||Tti_@].]ZuC=*""B7lP~~~rwwImӦ/_gpB-[Liii? z)UPe%KԘ1cV#|w_{lm-g k_TX,jՄ 4zhj̔uw}WӧO~~;8$"湈CQhSSլ5ϏZCbjM5EPPZJPyN$?r\n$r$yֺk}>|E޽wA͚5KƵj6l1{/콥GHըQ(+VLsUƍm|2d߿/Iܹ-ZٲeSTTTu\{ּy#Gݾ};>>T믿J)N4I]v޽?{.ճY8V%4m45kL:t +WNLpu͵a-[Vǎ볐bf6jH۷o$iҤIٳ^VVZw}Waaaƽ5j0 ԩQ0vvv֍7}I~Ә1c{xx(00PEM6{ԫjNOj[I8o7o^]z5Ut@N{] LU |7{E8`K%v xԕ,Y2٭i Y500f R/K_lڶmksIz2 ϟ_{UBl;;;_~*V^uEGGkj߾ի^ƎkJ._~%^qEzR*UT^=kj׮5joL*UJǎKў'wԩF7o޼ULx+͛7VZ:x𠂃5ydM2s$%: 617 ڲeK-ڶm\rYfk>H/_֥K&8C^֭["5o޼' TF/UV+{wΝTgy H3[$| Lm۶񊿱J(a_r1g̘WӦMս{w}I&K8-rݼyS3gΔ3szҥIjԨ>HÆ $M0!$ :vX, x844T'O6˗/OESLQ $I~+7*[l̙S5kL7oJRĉX7nvif)R$={(D]v;dq9sŋ_~bg|+222=lLoH5Zs2,SѣGbdkr7ol(n߾]7o޴kH\sܿq{u]޽[׮]S޼yXV\Wʓ'O?ޘ /$gÆ 5֯_?ټחBBBt?ƞ/,޽{ ٳG.]$Ν[O]NjGNҥKt]>}^g 7vtt_v=xq7s玼"&MijqSdnCOIْ=9D)]K<ɬh[N~o߮^2R֮]ruuՊ+ojwՂFO+G򽥻$̗e fZfGHPR^Kɬ¸}###SHeϞ]޾'O4t]ܹSM4_es_BRR@M?5jSIl88lk6/ulٲ*WJ,)8p@ 6L HY M8Qy͛Ur47<<8~ \S!<<ܮ[6IR*Xq|Q[v=;r H_Ŋo3u7md\TXX6lؠ&MEĖ~}Z򋤘fl1ڼyQMjoi/j„ 敤۷p*_;XR/9!//_d[niٲev} ywfq\qg&6~Ѕ t}[.>5Ξ=f͚[vJv +W.EψSh' ZdJɖ ТEd_EM@Z I@VD8o߮+W&z}uGGG5*Ѿ|ɣҥKkϞ=Q^v߾}u̙DܰaϟonjdƸ~ըQ# 8///;hǚ5kVԭ[7]pAǎӁ4^hquuUСC֭BBBRG{-[5n\q;vDZE=C QΜ9Ul'q-kЮ]xTTO/_>Eω-p i?QQQjٲ.]dv áJnu_!Cw5΍5*ig1B7nɓ'o] |&Md]z駟~yM6Rݺu믿de%iѢEqÆ UNyxxH<77$gKաCI1_:oPP^y;wNRLQ^Krjժb>ǔ,8d-[VRL^z nRǎ^[~mxΜ9lf:~:tiӦfhժ>Lk̶֬g]z>3ݹsG'NP׮]}_qEDD_QQQڵk .dhmذAaaa)*>}Zwޕd48dtFݻS]/"՞ D3}G l֭[Asȑ#?~M0Aw6l )oH *1c'}_+T%Jʕ+믿oK1{NaÆٜ 6%I'NЫjJ˗Wdd<ׯ4h^ٲeK6s…m].]N8!Ij֬6nh\322R:u"E|rvv֩Sh\suuզMT~}{>3g|f;v0f֭[W峹'G1bK Gߪe˖v',]~}]v8Wxq-[V<О={lʝ;6l()f)#G\ѣKʕ++ Չ']Es疇F`4<<\*TЩS$B?(((HO6{;wn:rHM8Ѧ3[Vk.YqwwWXXnη~֭[KFߥde˖%:k-]hw~0Ë/12ZFTTs%K٪-IKS 4-))֭S-dZu)KJϏD<{O3gΜD]zq3gN5iD~a~6r%:o޼yI1+WH>ijҤΝ;Gi6},飙3g&<~~~߿]>R̟_I%IUT1 im߾}j߾=_2R!S-ݨQd =^y ~Or.]ɓ4hJ.-777e˖M*UҰat uѧ~ܹs| HUXھ}6nܨm۪prqqʕ+~Ú1cF1x`hU|y@)̂], (޽{HСΝ; _WB"WWW-ZT[ʕ+uĉԲeK^^^)^J:v_~%EIRRw^-_\ ,(y{{W_… ^zbŊ/Q^RL1>ӟ~L2𐫫UfMW۶mӡC}v;VUVU\$'''u%e˖ѣG5aUZU9s攻J(=zh5kVЧE4hrȡ*Uf͚y⯔}|GI2f@F?QFQRN LtzR\ |Z\HիQ!!!ʕ+ّ͛QFׯQ .\0TD Sr4XϚؕ1RJ$$a [j~ЪUʕ+R}ZRjl3'OTƍuYhBVZvmCM4џ˗СCڶm̈́K.Xb*ZQbkVh"/_^aaaZn̙(IٳiӦʕK}Qhh>|ƍլYx]Ujj֬YzW4f̘D"sc0 ڵm6:u8cӦM*S3֭[O52;L`z$PBF7bѠAܹsnѣڻw$[9ָqTtTemٲ,X-[bŊC;vҥKSN{ァ{Nt5o\gϞUx5Λ7l"oooI҉'oڼ-ZhZhͽ+Wѣw^uYziƌڲe\\\ӧOݻ=o߾ʙ3޸qcKdǮ4ڷo$!sc0 pwwרQ4`M:U3;3vڴiҥQ VUFbٳ͎OJثSxxvikԨ!777,((8~|X%K4ݻSN>۷Iɓ'SYTlY=zTt1XB~~~ٳk63}qc{3񽽽5euUh̙kӯp6Ν;wIpk1bN0A7o[4iY[lѫh+V[M4I2?fψ>}rZ`ArJ3;! @Pn/@*8::rH [q[@}\llwܱirԩSuiժU.8Ε+z$_~bO],kΘ,I~i=mڅ Jr(O}6lؠ8YZ4d5k(w}W#GD,Zn=zě>%,)~9rЄ UbE :TO/| o6:NLjlڱ{FZlΝKСC^bWZ *J*Άp27nl3?!s+Nb:qy5'|%K({ƹk׮wQ͚5uȑ?Yaaa6m{g&̙Ӧmo8%c&5qo+V廑QANNNZpڶm>}&+Yۧ5k@ZÇua=zD UJWժUѣGզMձcG8qB]tW_etʕ˦زˏ{|Y^ļqFc= 777 6h߿_ ,H53&9zjIRܹջwokٲM[n}6?`,YܻwOG9xAX%<~DHԩzm{ioڴɦ(eǎnݺF{%.I޽6m3l/tEIܹs]2dV%K/u? fy b̙jڿΝ+???7{lݻWg-Ғ%K|֬Y)WWW}h;VVґ#G4rHM0A^^^Ǐ:{lIR$֭[jٲeKA_tɘ]ۼys͚5+%iɒ%/o> :T4qDlҮ1cuŦ`ܱcݏ̏0Y\=Njժ5sL?Q͚5hذa>}իg;rjժɓ'\r6cvE۷ƍ_i˖-֭9rhڵ\r,۷Ѿ}ڶmʕ+իu֩M6:tZ|:۷o'}||/sβX,ڽ{*UsڿN*U($$DGֺugO Pj_ׯW\]]_߷k;HݩS0עE积J'O/.C>>>k׮?;wnk̘1ʓ'Oc<ժ˗k$)RDא!C'eĉX,ZdܹŋCׯQ 3gN9rD۷$}޽{ԢE4h -^X۶mȑ#u}ɓG%KթS'(P EkoF&Lѣٳx=zu림yh̸5kO?T s 988h8p`+]v%xUVZO>D|IgϞٳg9*U>,ƋUT)-^8Ǖp@\, d ׯZnmv< H3}ގf2VM~(K^/ 礡NyB̎ mŲX,ƫr~,@&ڵkv-bb"iҥZf_koV^j*׮]ӦMi&hB˗/qرc ЪUtĉ$vZThhq>**J.\P.]c{=!Ch߾} +W.mذAԠA]|YI&ܒh &։'Wttّ@s(P@eʔ9_~YGռyw^-[6ڵkծ];hڴi ۷}~ܹ5x`UPUfMIիuaRg *Sʔ)#ggg|9eʔo1 jذ6lw}W9sTdd$iӦM?~F%b饗ԦMթS(]V|:tjժZjbfl2׭[WԫW/ٳ8[o?Vs*URڵUdIݸqC_M6iI 30wwwɓhׯ__~M7V 4|s&LYIɗ/M{ʔ)6XrRV}XU H`,.Ʌ Jo6mTbE} p>|h´n: d4`عsƎoo׮]%Iz㯿i9r$]rŦ}D>}ڦ}ծ]8 QΝkƍ]vH{j~Xn7 @Z~wךVx>B 5kV\}Oھ}>SEDDhҤI1b}ׯ_ٳgѣG\6mT^=*UJbf~76}-[Mk״rx}V=S3fН;w$,Yb 9::jƌz72 &x˿6]ޓ[w<9[$jժڲe&NiӦP"EԧOu]Ew&8;w4h IҬYԷo_ >\˗/ץKl~_/jڵ7xC;wӵ~zKC 1ϟ_[nՠAt!̙SjUFT}>0l(P@gٳgԨQ5jL)S￷{XիWo= `0 4v9af2pE+FadYANYR=1aE͛77;F- AEfK@@&A 4<dr*\0)bXTpayxx)@, lv3 %LH{w.&$/^\ ݻwiv,yyy){ruueՀ 0T)TPs? IbMnnnjUtt" `0"GGGc{@&A2 |`pM\sE-sɑ@z أb෌@xbe˖M+WVXXX;uTEEE}i2W&OΝ;뭷/lNSi}OK׮]%I_ݻgrO"U_2^QfG P2ŋ:rٓ&c>x@-$u1MƔ*UhѢEZrFf㦅9>MիW޽u֙<#(\ٳi5vӦM{|||TJ4Y4>ǧA7$Z4`ag0x$Iחb19 G|vءH LSZqV+m jO?$ISiӁTVT mtgbؼڷo/Iz&L?ͫ7xC?AJ*ʙ3\\\TP!ڹsgzuIRDD~C/'wwwyxxWg}SǏ{wj֬Yjذ|||ϮiӦZ|UPA...1cF1͛'wwxbQJtM}4p@UZU9s攛-={Grppo׮]&M$L"///Y&[nI*Vh_~*[J*iĈxb;|F^{MŋWuGrsq41@fi$k  *@/_DK@gP{͛7,IZjqm̘1ƱTܹsСC:wV TRv[rz/ʕ+uemذA^^^O}_^oW\dZue>|XF{C?Ԅ l)SFŋWXX8{?ԻロSN̙3܎;uo֭߮[ٻ*oInG,ԱtwӬɭ:dIEf&ek\eͥ\Q1DGz>g=p]ץKAAAԥKb诿Çm۶V{vIkO9KW^ﯿ=zh޽ƹ5j(""BTߩ,YĮڦMթSG޺p(){m.]zj/stv֢ԩSG'Oԏ?Xkapssk[,EGGkĉь ܹsN:?o\~|MM4ɘqFS)))yMf*UG~9G|AYFr~eQ=s/ `qp.fT.{vQ+Vyxx3 /$$D_}իgwSQQQ U~X 2D;w.sh(oԨQBlnuՋ/ݻK[СC;v;vbbbdoh,[{GNGʹ_t!~er.hlA|M>L;v榁F{3gٳgu=s0뮻e}U-ZH/R;kŋK=88Xpb'lNd8p`o0ܹsꫯ)֧O1hϞ=᱋#g6/ڷSNj֬toƌJKK$}s[pj֬YBCCq|Y^ڵkg̜-Oϟb-㝑_|hϘ1#O7=zhߠAm۶M,>ŢhٳpΚ;oZp3a ʽr -J߾}3yd[=S oӢEtEFFjԨQҥ"##`Kzʕ+裏ٳ -J{>\/\$^dJ5FYr$k]tI3sϨ.Ȑ!CJ5f:z;3gҥK:~'YVYsŋTʇEk@iQvAMw–PaZղeK߿_ڳgusQ|| Ϛ1cf̘!){-ZEjժ:v쨆 X}V:tPΝ;.]Sv+//"qύkݺuھ}=Fy)7MP .8K:[XXOoΌPgVvء륗^ݻkOӧe\4uT#N>m7iQؒfPff233ٳqq>XvMTYs3. 5;P!knT6bh0`kϞ=:p㕘,IKSO<^<3=< IY5jTnU\~~~vZs/j 풘0abbb5jH:uRբE 5nXڽ{v *93إQ 9'ѣGu:=GVV# IDAT԰aCyzz*88(|}ZbҴ`jvx1svR_YYYʲ+z_ϟ+,%$$8vs~C6mdWPիOmۿ }fd|oIS>"3f0{ꥀ/%eW\Yd-Z&%%_~1Ϟ=~VSNի /44{#宻RPPq_Q? .hŊb۷,3>ʚ[ϩaÆ @l|*m@;wJLL4^PV(!۷oתU k\wwwWttt}VZjڴvUL3gTrrr>lmƾׯׂ +==]֕+WJlԩS8Ι}?qN 9^EرcSO='NGqg;{X>̡qoϱSnѢE|jذB9rde{WM8Q#G4EGGΝ;3(%%EGѠA.2_/">>{%IӴifOmDDt9JMM5 4H˖-{Λ7O&M;d,\2QFSJJ:dWձc|f㜯Zl%&&~0ŋ;S_x^{5?SN]=zgiiiбc$eӟ`F\oF%''+88Xn:=g9pt_~8רQ#5o\ڵkg#00P]v$lRSL>j(-Yh*22R!!!uСR``T)ShsXYs[N $/ϭ<+""hXY]vM˖-3=MLB=?fu+)<|m6;v8e˖-JHHP֭ս{w 0|*::Z6M3;fSttܴ`(Ţ /,Pz @cƌҥK &qfGYFwֈ#Ծ}{q+<<@ѱcG#p1aHJJ /<~aY,ժUKѪ] ͵yf-_\vQBB(){oSFft*www-^Xwu&LO>H7d=jҤI){c.`TBax74|]8 +V1b={6^h***<;C111oGyH7#G*--M̎* 6G *O``uVN*EN:SOhĈ7n͎sS:umۦ׫iӦf3ah߾}?#[LLbbb̎aEٳ+խ[H7wyG .ԶmԮ];p,jKE0xxxhҥz5f̎thܸk5n()+C:%wOh[R annn;w1n:;v4;ifecViTR3.0 "( .4%h0*+ AZѴZePi4 "X4q5g*<@eOlF~6Y,nt>]tb2`X,np+#K@`0@%x!sVՔ,* T.g8g f@6hn)/P:Le˲k@iYp "(``Jē@A\VSs8X\`pE0iz̎T :pca*`9)psfv2Cnnn^]J0 <==ս{wcp1 ঑Dcfi3FdLNP6 .&ŋv@@,6@P0AFFnj////pL3A\`7 ĉf(34eZ͎Pf( .ѽ{wt}D\3EPA\{Sk@iQ0"##͎Ű4 "(``\~]7n4} &o{I@4 "( .0\g5cB4şhwR^>&&rкBa0 "(``xyy /B'ִxj:9 T|LG  ^50*A\`p `ͦ׌LLP0kEFcb"0|eD-$P9PV n6($P9Xp "X4=(1J!M:~h?|LL@%#pf .%M-LR dZJ05XlNL" `wwunaҢ `wu#P , .07Yx&='8`ddd{?n.wp .\r3P0AFfv~gބ0Rc`p8 hjv RHKK>D@OMnݺfG."( .=:uk^Ze" PN:tPl6W"( .=LѣG۵(M>>>fb(@9ٵkWBBB1 pUԫWku-$UYpf --Mo~G@Q8MڵY pPZZvݺu)(5 ~>ov Rȼ~M4= pZHG#*A\`p @qsy%&&9gZ> R @kL~6*#A@O?zꩧt})55( B:y?k׮]fG1uiuU/_6;(p=Kwyf̘avSiƍ:}k׮ zH\`ŋշo_կ__oq*`{/4vXph,fGFFVV SNiÆ 4;Rѹsg=䓊Q.] ey-|*+ *ŋkƍzWfv gڴiz]*00Hp2Z!yYX@/gjѣ͎S!hJNN͎* _nnnVN8Q?-[ :a֭JNNs~劍S߯jժiӦaÌpiwߕ2[6,_7ڹ e;PPPkȐ!er T6mҥKVZRX,ѣ/_իWS@|jy@ Tk֬$uEnnn&|Xӟd}rJeee)55UҫjwQcn|UfCSN-2gyF{VFTV-y{{jժj֬y}'uIR˖-}exޤ$͘1CڵS͚5iٲ<<~pJ7xCz) .d˖-[r6m_|Q׮]O`=1ݵ|<ŵӧOkժUZjK OXWjĉZd233%''kǎڱcfϞu9))I6mҦM4o>^zj7[;8_)Rgff*66V3f(0Mԯ_<.I:|>wyGz7Ծ}{>k.8""XV}75e]z3#ުSN<;36(ѣGJJJ]1m4jժjժw^l6%%%k׮ZvW=f͚eWn֬5j˗/kݺrۧ#G}n,9RFF~w]rE6l$eeew6mTNy{{… JHHPbb왳]tի RNzQF ;~x[nQVHNtjnPyQv!z׍v"$Y,EGGkĉV$)##C{֭[uu-YDsux:uh߿$Iׯ_כoI&ڵk/4a-YϜ~(%{}g}G֭+y=Z (= , @B&b 8{1mݺURvQ!!!ꫯT^=󞞞Rhh맬,jȐ!ܹsF_Iٳ,wܩf͚O?T:tО={9s襗^СC;v;vbbb^@{ߙUl3gٳgu=s&뮻eu]۷oիWhѢ|?p IRڵ}oexހ}y~ə5}E VppS@pbyOyʍ.pSf;w1_}<#Fٳg;<~w͙3h\2*t IDATo///ߺu7c I|-\Aڶm<b7ڹ.oϟ-^wΜ9>;3=--M% 4^=ֽ?g Z7QFx-g9hIJMMux̾}gr۷oZ(ݰa~wIR.]ԥK"ҥ'Nѣjܸqʕ+裏ٳ -J_XIota=zTǎә3gt%ce)y͒>>>N=oa^ŋ *YPXTo($""">fΞZjٲ߯LٳGݺusZ?8_ |,v}V:tИ;wବ,}Zno߮$#(]1*zyy٧4wwwڵk% \DIu0߿_t{x{ O64i8~og[vM!Tfffl[~Grsz.PPFle6vI 9֭;_3,+ &(&&\FԩS'5o\-ZPƍݻwk׮&%]VsT-HI#93եST/(_@%Ԯ];#AQbiiizQnݺN͐{'j޼yNرcg6m+KӧOW۶m_Q :]!y322W˗%eYn@Œ 6{2Rק~Zd2{zzX+;ǫWVzzzlݺ#G}6m*{(^Ņڴi?p뭷J.\P*nݺ>={VR>*ͦ\1̓0J,66>3f0{ꥀf1O>Yf9tU~}ԩSv׼ԯ_?9ٳgo;A KII1[dׯ83f/{eGTuw4ߑϭaÆ@!Ν;gJLL@D%}vZ뱱uwwwEGG_jժMj׮]gZ]^ zGt)8p@wgɲXׯׂ /==]֕+WioԜ9sQFۋg94nY?;<9}R<ĉUzu5o<GmܸQ}Q׋ONE%@~~WÆ P1QF92ǿ&N#G碣 \vΝzg#GhРA*7NCdرcկ_?ݻ7OxuM'O$թSG{/""B?vTTƎ>>{%e)Mf+InnnwDDt9JMM5 4H˖-{Λ7O&M; ===]?3KR .OOO;vn?_ooomڴI]twL sL\ٽ{wK%ׯ__ƍZPP4i#Gh߾}*JR֭78\vwwWffܴxb=JMM~~G{تU+ӊa hҽ{" 5kTntmumVaÆȑ#?~6m*oפIt! [;vܹsp5tP|XSNU֭Uzu*,,L>[-_ntQm6]Vׯ/oooyyy^zrJ=zT={sPPڵkgwbW^Ѿ}f͚J*6?ڶmݫ۷W6mTF yxxCÆ 3ӧ~ծ][_ ӯ_?I_|Qy=ƏjJڵ3f;bذarssӰa4rH|GyfI2fH,bʽmjj|||LLSܹ(v4w;UF #۷wﮨ(N:K=0Ӳ4Pr, %@Ya Lնm[)!!A7o͎dܹsS1,޵kWSڵ[jeR`?~ƌw}B/]8EFF`ٴxbIҤILNpU!!!vmjN{0m۶رcf1lڴI͚5PKoٲE jݺwnv`Xb, @/Lhl6͛78gھ6lQ 6Mrssӂ ̎<==էOiv$.%TcƌҥK &qƦYj㏛#5kh1bڷoov|~ 7;7- (TRR^x%%%bVZVڵMHh͛7k̴4p@I{L:U5j0+fŋ뮻҄ 'СC=z-[&ooor䖜'|RM40@Cz74|]8 +V1b={6^h***>ޔ +VPtt)Sj۶mZ~6mjvb˒~O~OVbbN0j߾}?#[LLbbb̎r-ZׯgϞꫯTnr}Td}wpBm۶Mڵ3;g̐}!Iz3P*;fp<<g!wfv0<==էOcp1, .@ $%%ٵcZaC(1cxfcL`tU]J+ `ׯkƍF21W`1;9( .=Xp]w&%@(`j5' K@ .08Mnv RHKKӑ#;>$dR@T J _BmZUDP L@KѪܫV Z*WE\g (6{s RO<1 Ld+2 ɉWԗ4$ '''zt h !20@p`lܸ1fϞ?@ /}QM h !20@P B C$w>wַH:4 ֯KI~sa$$?^#  C(d-_??cMD4嵞[TTT4@c ju_/B C(dH{nOv~$Vd+[?_u@IN+N:/*@IN h ! m{'J:4 bC|lIj|R[Q`" #KN@c0@P =`YYq^|A9 I ͲU:s^A @ӣ ܜ%0d` ! %%Rw99Ld`/ܿ0'_wL c)RԾ/(jd. B hݣUIǀ&aݺu]|{t!` B hEi~III1 $K&kc7ر!h !2 Ϗ3<ZJ:aVФ͙3N۵k@I y 4i;v:4PH0@֯_'ONO;O0 P^^+V4/`f=J jiݺu.:t ;%^*$n\ア05~Y{xM;#P Vd` 4\ ,q_QQQzu|yӜ4@P @txǓK/4N8XvmQ4ʎ:YYYԟ4… ?a̙3'(3fL,^8:XjUq4ɍqꑝt$ (·~~xr!qW''q͛73gŋc~#TN6|8p`^qw&(,,I&ŏ b„ IG ;u4P2dH,Z(f̘ڡnO>q7}' ҼM/**1A-Fc„ 1s̸K.IiF{1"-[tJcŒGyYiґ 4 ׿+>|xqVZW\K.1c$ҒXԣ$H@FnK.$֯_VPPƍs=ǻs^q=Aθiy}}+o&?4s̉?>.]ZiСCci?߹cƌ &ȑ#SNi?C.&uJcl۩-7FA^6<81lR & `o֭I?O?tx㍱bŊ7o^ vDt^tOcѶm:thYǓO>YqVвe8ScÆ q7@e0رc]vnݺ(..gy&""|쭎77n())!C4տÇe˖EqqqDDL:5Aygğ뮻k͛7yEFW^q=DDij>*Pn>}Ŀ:uǧ)eyy}eڵLvZaS۶m[nI7/7>8ڶmK.Gy$ ƽ$iر|;wn38ìYbʕQXXzJ:NsLD옂3c-(^U`A5mڴ۷odee%i9#{~:6lyyyIGҤj5mgKHLyyy<qG$X1zx񊋋cҥuzG}TstB̙UQq/"8#m۶{G'?g}v9o޼⋣W^뮻F^^^t1N9DLj#-[F˖-/</^\cѺu*⑓_~ywix|;>3ѦMju]y뻥?FS۷o7.V\Y̟??눈ѣvey뭷/>mڴhٲeqgꫣwޱףGwh,YJt5""XlYsKJJb̙qW6h5f͚9rdqQZZZiҥKggy&x衇wΜ9ݻwsYfŠAdذaC]w={n-=:>ƏW]uUYt|}pxh2|ꫯ8""N7z-[F^uo… <.8ꨣb1'O>7 IDATzޮ]ٳglܸ1Ν?eʔcƌѲey_~es1onѽ{hݺu[`Xvm2lذOcɒ%^:f̘u/_:ujŃ>eee^{š:uN:)5 /իWǗ_~O>d|W.ıyyy_Vh zj.ZQhѢx7<㨣|0 T1ϟknu|Q}AE#???裏bip߾}cԩ[͔n_|q{z}* sMkni*O/*)YYY1jԨ9rd.i5RLKn1۷oO۸qc?>.Ԋ:+㎴ eeeѯ_={vDD_{nN9u8cժUr!/ ,Ν;GĦ_|Qc'{(**O>y+WXnv_mq۷o {g:vDYgjk\XXnw1cƌ8wީ{p駧 ٹlٲ*_|1F.;s̛7/u–߽6m-MxkƍW;.:믯k$ꫯa98SӚa{tIUӦMq^E6mD߾}wH y}WXQ^ yyy=Ps+-;uO=T[}?qƛnhcǎwbubÆ ht [T}UylI:sjW:"bڵ>9+H=={v8s̉>,""Zn\p6_3dȐ>sT1b'O<15kE41cFX"""[v߾}m۶i~Xի7>-((U(//{/Oƍ.,;80aB)绷y t6-˖$NQQQ#~(--_=>贜}pDO~kj3gGhpx嗣wޕoK5 y}{_5NRYYYŋc}2_cn\߽ؠ3|{)7B7tovDDjEi:|_ڎtIqDYYYL6J5t۶m~h`1:lNNNFiiv{cn1m yiUWspw1Iظqc{DDD~"777D@mlTR^^ǭnSe7|h爆)FDt!9jU,|s\rI?Tg}Ep 1sozjαlwoݺu獡 ^YYY,Y$ؼs[bݺujСC߮]?VٸqcΟm-Z/Rva׿55 {}792nƺۊ-ZW_}UY[f͊o95>cc̘1I#"JJJj5o{{VM6HF[vyǷ9wMͭ=*3gάjYfpI'VV|{?я֐׷bsDԩScÆ uX|;_<@/~5kVZ?OnsN}{+P;ulX`AB[bĉۜsW{ѪUmFDŋ:ᄡ?kW_Z '9a ',,^8Znٱ^{EnbѢE{Sǭe˖8pZQ~wnsN}{׮suOѹsjٳcʔ)58qbjfbԨQ5hӦMt5̙S\_z'83όK;~_NMsD^ߖ-[~}kVZgyf,Z(yxת>DD_|Q{_V6b{[#G]w58̙3kݫNE!-rgWi_ȑ#Nm5jT-h}ٸcٲe)Rȑ#M>}T)}qim`@?{gyIFk{EСC#",.4hPUΟ??>XpaDDo> Pq:蠈tܺL;SR?ŕW^YiUpw}77tSjڵkk7>b{gQ\6lXE]{XreDTn|{$jҥq5Dqqq}~zdggG֭㪫vŵ^͋իW-_䈈رc=:ZhQ9WZw\t=tK,sڵkSsN9o~S1\ 駟ƲeR퇷fvz(׿O?>p@]6̙S)O>(^{W]uU?nW^C@6=\<{n^ې7bS+7ԩS#"b1}ԩSt-rss?y^_"??y7L;,>;<7n\3ڵk+V{s΍͛G֭cw*U}뭷*͝7o^;ZabŊ߿#gUg̑U^^^tow1f̘mλ+cM{vkJRoK-EDƘ1c+9J+]{㏷ms|A :4^}t5^xa_yJ+%?ZR֭/o=^igT!6ܽ]tQm7>w߽ҵk⪫[K뿶gϞQPP/r}p@;OJnmKNNNFVVVL0!.z_:nիWwGSN|6[~}L<95>j`ǫY ΖW7o^t-DG ofw}86?:t~56lX|qG׮] 7ox`\ve{ժuG 7[nݺZ ~/R1`h߾}E۶mqwǼy΋=zTy}.]*5"N:gʪv[N?ׯ_4k,⬳ΪS7a6СCc…1q4hPt1"???:w?bʔ){ 4("6*;;;?ěo#F?vyϏmF޽WU|cEݛ kFAAAizj|ѬYXtinIGdѯ_1bDQ*YhQ^F.]a0l?+q[dI{UL @~D.]⣏>{,N=Ԥ#Udɒh߾}1QGX}+ƣ]vnW;-]|q=$d +WyEϞ=RIyyyL0!"".첄+///N<#///H@PwgD۶m㩧?88)f͊߿ѵ4~'⣏>}{ѯ_0v)Fq7&'"6馛bذaIG"777D@&P -6,4,**aYغx뭷R+ :W4h !20@P "'@|ra.z; $B2Hn[4(*J$ CX #Fy5nǎI`s=tK.3fH$OD 4 -20@P ظqcjYYY:fqqq_`:/**9G 7nGy$5>#//^/YXT^^B C(d` !r@zOk I(d:(Ah !2M~[nIǀ&aݺu'xb$ ɉ>}Tԗ4$`0@:0@P =PRR/Bj0rr eH@III<쳩q޽z C(d` !20@P "'dyE1Iذ~]_Mj #/ D~NIGg0@ yq+K YYYZ>hN^^EEEuL@](ju_/Pw !2 ذ~]̺xpO`a#F h a0Pɭӟwh$ԕ0PIw:i~QQJ@]i !20@p`昼w[c@.6ć͖'y &zF ۫ I' C(d-,;;z߹""|V\jZQQveQH@nnNC@#~R !2{$4ԸǾ{EfLd`l,)Gf#5޿s{`k?yc.M04"E 0r4e'PZ@6_ZuM:4 ֭HrP$ :tH:l7+20@P @[hQ疔j^QQv @;~K yyy1tJcRH@vvv&0I =20@p`_>M?d9sznv0 4,`ҥK+h8;v:4`hXZ@d` 4i{jnݺe]R:DAAAL @\寒MB Sy!' sL;#@b B C0M/Tٶ`*ۊ> 4 4Y.\e[˫+//i y ʎmUԗ0h-]4^߿Qu饗NI$;'7?6@h.\?cΜ9IGIܘ1cbqQGŪU4b @rH\}II\c̙x8p`_>H@# 4*˗/^{wygq˜4iRO\pI)2dH,Z(f̘IGjT^xa|ѷo|xqVZW\K.1c$hdaxhѢEdee(**O>$teEVG''#W\|Mmr̙Ui ;sE1a„Xpahf1cF^zs.\O>ds9;(U=q7FDļyϏW^y%LM~1yh۶m :48uOҥKl{cĉޖ-[SO=5?7ƍK-nt hm(oƗyLAt ?t`;(v)VZUe?X`ADD<&MÇǾ/BhѢ^RvvVIQ׶xQRRC ir#"˖-∈:ujj_yyy3Έ?q]wŵ^͛7oM0;L׮]cذa"e}M4)֬Y?cΜ9ѯ_B>}oN:zmCX IDATvms= 2m6nxpC9m۶txGbȐ! ~NK~"\jرcc1wٳgb֬Yr(,,^z%Ύc9&"vL| WU~,XP4 V6mZDD7NӴyqO?6l#MX_w [7@z))//'|2""8∄4=+W^Zva:^qqq,]N裏uN 29s0*=*_ĵ^Gydm6b='n1͛_|q+vuˋ;)l֬Y{U K/)SĔ)SO; 7W_M׬Y~zZҼW^y%v[G|'^3~oHm뮻"???矏+WVWZZ'NﳶNŽUVţ>'tRs^xꫯj_ZZ&Mcn|5<@vK/7|S܍7ƌ3/oΜ9ݻwSYfŠAʾ 6~u]ѳgϸC9aqUWŚ5k*mO6???ڷo .~;&*c Ç*#bSaѣS[lz֭[o .袋⨣ۯVǜw7tS+1}XfM|pkeeeqy^{Ɓ/RX"N^{m|[/())+VիcƌQVVY['O)ST:O?*G?=ܳ׶h"~_oÆ O?4,YRiﹶyښI&ŤI4{XbEq1DSOM9rdlذ!5kV\2ZnGuT\P9ϟknuwwT*tAѾ}Ϗ:>X`ADlZ)ܷoߘ:uj 4W~6/|ѫW())I϶^v⥗^;Vڞ#F; eee1q2dHӧ&W_}u7"6;Tx=I&; ())?2Q{s9'n());j=éU[~ߪSњU[s5}Y7vOJX9|zg-,,zh>SvvO>bT.]RϷl5ƍRqguVj|u{nm/~Q8֢ט>OѣGr!1q껢{?y܊+oOKN:SO=nv[Ύ??5~|;vli""bݺuaÆ E-*?*=[QqDAGljs\[+"|٩bW}{߫>""7"]MX_qhLT|>xꩧ矏Mmkc5xb1nܸ΋ &T9玲tl+ ޼xlyE=ߎx裏N{ګ^I^c?f͚WKh6Cf[y}+ ^6]tovDD|g rmLJ#>O5i޼yz1~xG/6mڤO6-""8w[cDiiiuѣwݮ7&ݼsAӞXqc@~xjԸwNM_޽doE $tTI>:|+Z|g1gΜ߈+X֥z;ө?ndlE&"I%T*QD){vI00c~5s~͜~]y>缮s3z=rHu*[reK:u6oެӧO֭[6KV6l###ܐ/Fgt=s挱흝!!/WW~zGj…1b$iڵF3# … ƍVIزef̘a}YM4I 6?q~l$K4plΤ1>:~$-T^0k^$yxxhڵfQǏׁ$Ik֬$իWOժUK|AI͛73o,KڲeKO?'+˗%ݛGp&-X >'O6}YyxxdgH9{!XI<76lХKt][.BKQ^ze^xBBBoI_IT\AAAr۶m… 쓕6UX10͕+W.\E8|/XǧO>Q%TzucXu>qqqs*t)[02XNHBɞSv+R]|֬Y8999{:Vʕ%I_F''ݻviٶm[5j+zHоIm޼Y?C۳&$k֬ N:%{UX1 2p+ڲeպk׮iԨQF\I?~|%]wڥ~[ׯ_ӧճg\SR\rmpڷo;wdXizꩧ3fѣFbhFb'''u!cSN8p$ƍU???UP!CyG$ݛ'7#OX/;X N>}>3EDD:/U4}tɓzWJ, mѝ;w$I5r sMK_ɶWԸq>@ǎSxx/44X\9M0A NaaazJ*r劎9OϞ=x /^ׯdɒ)3}t߿_!!!_~EzR>}ԦM}ڰaСV&Lk̙)Xn݌ѽϟZj@x:$bOk &Xb'Ojڴi Kcy…ڵkW>>{=x)ѣLsW4hRJ|}}S<9s(**j}JMggg45Yr&xGUN9rDW\ WGc7jHϟ/Ţ>H3gΔtm\\\xafY۶mժU+ :P\tI˗$9sFUT1%FkN:Ĉ@Nb0W~300Z5YhذTgj֭6Gʕ+*[a$b IO?mZ@ָgZp,KP^="hHȰx^Zҽg7b 2D/U ;wرc5;+E͓$3hdit|||r>a`wޕ֯]VW\$Y>ϋ/%Kjǎ:w\g/[lQ5r]I۷ٳ_Zjev8 ! 5h@O?UCIJUve\ eX4mڴ,,>3PX,?^NNN={\l:v$I Rhhnܸ޽{ɓ>HNNNv9ߐ!Ch"-X@#GTժUrZ|BBBkGR+W5`=fLݺuUV-ŢPa_$S)S(888ٶ}Y%JUL"D^Y |?Snݒ$+ԩyq9r6nhcgɓ'5x`-]T3-BBB4|pUV-׌yQQQ9s~?~#HbΜ9/ln[jI&TXȣTti˚?bbbolkٲ>sGՌ34tP-\P/ϑ}ѨQϛr+/"ñ)$fq߳mv0p*y,Y2>jٲeE,/Osѻ+///ϟ_={nݪ d9_u 0@o?-H˲e4~xSΝ{O;vК5kTzu#S, dF^\]]5ydM<9GW_)&&F[޽{흣m oF_~v!??? `-Z7|SC TUVվ}TjUCAnu;Īy…d]|||r&Hȕ4uT4ibv햎jVLҖ$ŒSL H  08 \d 3'w0)dZnmF%>>>ā quuap0A%vsU00i-O5/@# `-ZĈ8&Ӿ}vfH28 7m`ɑX  yXWjZ,`g `p$AA8?po@! yFa`J@`0 ϯ'`p*+\9@bW.],`08 ( `h[hwAv=GppppB㓩 `5dȐ;bɱsH%AA `p$AǗ_~}ڴi )$TR2c( 08J@UVUayBLL/n[h!777#HM[6;  H  9L Օ?d 2 66V?% ( 08 H  08 H  cۉ:U0ilXRO@ZH &:JWk[u#7|&F0bQVm*g `p$A00޸;>=RKO H4nq?Z"3.Wv|?qVG#``8;;FEo6d `G'G.zBRcX2`& IDATdϼ-" `p0A\\^1U+xĈ8&t" ( ޡ39lE>ST|r$ً H  9v_Q0L6;ːŢxY,CNNNrvv١HULLBCC 999M𐛛!!H@.uҥTƦd\TT^pCb(::Z!!! QBTti˗АA$L]Z Fez_Gŋ&Wxx.^(F1$Lrʙd8]t/qt*T A:Օ+WeYEUѢE&ggg$ݾ}[oV||-**JW\"#H&..NwܱZ *Օ rwwWBTdI*::v1 8+݄[T\9G\rrrrZ;r/~ ::Z֭3:t}$[`Ay ,h SѢEM E j@R{۽r(K:BH"&EȊ… 'K#o  TrRCbQ\\պ  + ,hՎbIVsVu#"dTHb$[H1țls%vSL\Wz ȭ"##UhQG`p5c 򄸘(<h.M.nLpYrCdF  8kWpBu>>> [0{xZ.[l]ŷX,9 -3; 66Vw (`v80 \UY{G,߯M6iӦM:|G89sF .Զmt%tkf͚ŋcm߾]۷o͛7uy*9o-'''rի>ִiRrwwwϟ_͛7Wڵ!iСX,jٲ/SR%YF^^^֐!Cr4 M*UR>}rȘ+W `Ι3gԸqc=c}3;8rx[.]ҦM$f͚i1c7oݻB%@5kĉF;&&F]tU\t#a>YqqC@]ڵkfA hƼyyf}ᇪRJ&LP5tP]~pL1zhuj]pp:t蠈t] 1;Aqqq۷gv(p $ W^;# ZtIQ!;vnݺӧ繧.s4h&MyiԨQPϑY QM#2:N;j.AK̎(P@ׯc=/;խ[Wof=m43&~iF֭[zlc„ 8qmھ}V\8p@ҽdЂ ~z(,,L+VT~4|p˗8b֭[d8p@/_V%Ծ}{*^x޳bʕ+`矺{ʔ)MW^IEk׮իիrssSٲeժU+ 0 y#""?hɒ%?~4iM:U/Vpp|I})^ڽ{֬Y_~EUV߿*Wlsŋ_mҤIp$2#<<\/u C*UR۶m5h (Q"K%&&F}֮]Ç+88X TզM 80)?^Z+WT6moKo-\P;vХKziܸqzꩧqM-]T+Wӧ5jhȐ!z^z k._^6mҞ={-Ţ~A ,СCU6l\]SrjԨΝ;g~ɒ%Z6[>lbhŊZd:ׯ+..Ϊp`0O?dŷ~("BnsE-]T%KT>}'r/Rzh͘1+ʔ)M6H"Vǎm۶eղeT-ZTGѦMԬYTkҥ֭J,mjҥJ?WQF_UHH"""t [޽9}z)=sZrΟ?(ᅳ3g7nt߈uY=zЎ;tMEEE)00PK.5ӻᆱ֭[~z-i̙ɓM6_~*]z۷+..Naaajٲ&L+<<\[lѬY:ZhM꯿رc}vm߾]ӱc4eըQCÇWtttctQ+!CXm G}x%iͪ^6oެCj֭ںutkQ6nܘ$o>ծ][sU>}qFm߾]ԉ'VZ?~}Ϝ9?P5Rٲe5tP޽[՘1cT~}}C7nܐ~i˖-Sj4|pٳ7_izw]V=zPɒ%3hΜ9xbccu%=ܹnݪW*22RGըQԫWd{i&4wYn~QQQj۶z[n/k.=(o `dkС޽wnb|K,QB뫰0~]vڴiU޽_)qIƓ|óyAڵ|r_UxCgΜ1K,-[ᅲSUvm=s6_ڵkYfy.\^M4Ѳej֭gZ|v%???UZU[nձcǴ{n{I}'~? ^Rt,X@{NGOS>|X*TPllf͚͛[%ݛF+%JXmQ2toIչsgmٲEm۶Uڵռys͙3G{פsYz"1hBjݻյkWթSGM4G}sJ7X +VLŋW||>""BڵҥK5zhm۶MǎӺu댑4hn߾Ç֛o={ZzuOtl9r~a$ZJֆ T^=ϪUrJ܌{#I+&Nh$wܩ.]WO>Nٙɒ] AGDD(v=~͵sNIҶmԪU+~6""BK֝;w~Y>ٲSLoMtq?x}NQH8}.Q ) QBlllkժU>ͬ'jҤIZhUS 6j]͚5o߾d#+ߛZf^BիR/g>S;n.]fI*Wkתf͚ɎY^==z תUԢEd}yfI҃> f͚_~$U\YAAA3fFa>wƏ˗[;w\ 4j]XXի+ @UV4i~Mҽ)___>k׮UΝvUB͟?_...ڲe fSsŋjРBBBT^=8pN:uUڵku֥P@iWGUrtIn߾-///ݽ{WwIKW?>YQ_թSGqqq:rXQ`:{ldwhh<==Qnnn߿>s(P\J*7ӪoPPVj̿>^Kֵk4l0}6 0@r {c, 8O%^FeǵݲeܹRJY=9;;g$Xhu :t bĉӧO$S666V?So1{i駟LJ /`,_|YO;wNWN6opʕW_{%lʔ):{ZhbWN &CYCBB4o}ʗ/ovZyIR ?#V{&ڼKv6رcZ~NO[>K,gs}+00PWyީ3}_X1(Q"ž{[a1鿻j ͛7S=Zg|jժdsO'Y&#js`XO?IW0b:<<\TFt`J"rVXƍرc)SW:u21)Zh&-u%֤{I!C護2ܹ??~1jz\r%>Ŋo_,Y2}J*f͚c=zT.\Y29;$Njґ鹾)ٰaQO>V˳ 2{ۺͺIc}kk$szyxxq͛oѳ>~ s:$K^K˖- j۶m PhhxiĈ6Hǎ… k.?^wUҥOhzꩧ-[Ҷm۴i&=zTϟWhhTT)]v ӧ~7|S^ob d5齋yM<9CkܹڲeN>t_ǁj)pC/A…xb$|W9r1o@buΝ;r(˵{n;wNRV4zK>|X˗/ב#Gt)Ν;rwwWٲeոqcuA?|GݻW믿*00PQQQ*\)ʕ+vjӦM"컶?~xvxg5㩩]\]]; 9shҤIY:ssuRG[YWhQmڴI=1wbQ߾}UjL}e&gLѩS'Ǎ9}%iҤIСCkkϞ=ƲySҰaC#,5͛UltaΟ?jN޴*Qmf]C[1O(::ZڴiviVqsm\Tzu}%ֆ qF͞=[N1]\\&KiZ|zE{ڲe^}U]t@_~1-Z(Y87(_|ƺݻw[%%)..N ,Pxܹ7n1z^+Vdz0mذfr͚5)qt8p@UVՠAlN~}ZvΝ;i-[]vTEGGԩS:uꔾjΜ9znܸ_|Q6lH֭[u.]]v/Є 4qD~umߍGv_C{=|lٲ ѣG6,_iwdX ~z5k/<<\:tЁudY8ʕ+Cׯ%Kf{IKMխv)-OXre˖՟kZ̺٥aÆZ|zi6lؠm۶iȑzwTpa&|[S{I=Z}õi&IzeĈrҥU^=Ʀ:.]T˗/:?l$|d'(\|MۦM?\6ﯮ]Z%K,)___ݹsGtoBSNaÆVǟ?UGQٲe/_>ݼySgϞՅ $ڼysXBڵobڷooKXb_<==[ni޽/ٳg'Kg׵M,IbŊHby )xZGkyc=%KGum۶]r `wwwc[9M:iv 3pJM2IgY7/}iqcǎԻwo>}ZҽJ},YYf9@ aJҬYvt5j(c4fllڴi~I111?Nc-[V_|:t`ܹs5fEEE_ȑ#5|gIH]N#mbgFe5Y->sgϞPYԭ[7>}j^Z=Zu51'qs&.0 #J<%&&F/_փ>~ j5o=*::ZҽcM?״u [ >'|bnxxxˀ *X>cJY111V[NWQi1kܸq:Fe7o^c/=zP=lnKrJ9sL+W.= `?t;#7nիWSOFrrrRN%UFǔJ*i6K+VL~~~)Nv_BرCիWpvvkfڲe{%X֭[d5ҩStUo>~ƍq Cv Xdd!&6^_4^1F̧T/`5ijݎ;L&ov횱ܪU+c9q2ٳ駟}M6o߾ 05kf޽{wewww5n؞a*]bi;vΝԩSwVf5׮]3= dc^J5[$[3#0;d=!{s-:|񊋷z #bcc3~U\9L\M1f4֭[F+Z%6mjwذaJgΜQ߾}tf;*_|F7XLM۷W%>[_;wh̘1oժU7oZ, <8]jϞ=3fLyf5̈?gR{Vmf.2} "{IO eIly??M)iI͛UX xș3gRwqvzeyڵk3ydaq=CFɓիW g駟Vղe < ///+F;00PׯOu8m޼YҽjǏ/>>>vfz/ZHӧOOu7^[oeeztt?7x#zj۶zիgkI5B9""BWNq1-vB5.y_fK*UyzEjΝZf>S@aB4n85X7k,=zT&LP&M|oa``f͚9sdɒm7}>H>q0#_N}-[ָ'<MXXvء:?#3H "/Zj;^8br)/ggguUիWrJ[j۶*Ud][СCh-̔Ilȑ֭Urz饗4uTm޼YO֭[25IQYB23e5iqݻFm۶eze5gΜt饗eŢ{N_~//7nj׮^x*1rJ-^XW[—.]JI%.GTҁ)%K(a,9r$ ۷N>={LJRΝrϫH"QbTLM0AZbV{?l3) 6Gt9fرc1b$iРAzwR<_e޻wbU~}uWVÆ UhQUXQ TZpB/_^k׮M14cRoڴicnΝj֬)+pU`A[Jp5ۯ]ꠙW\j'M'?XS'0CBBԯ_?߿_7oO>^gϞUŊRJk׮ڵ',,L{-[L={*Ud2KzɎx'ҫTR>-[4c jҤIjذM>/dO!^… ƍɞJKv]xz 9pXرcɩSԽ{wM2wQU{ǟ@%!H׈"ATH `(zA`FG`h"*J% WTP%Dd?xsndLfs^Cw8p8ڶu>|yԅ 4tP㺗N觟~2OIIѲeڵkիڿ֯_o,)gш#tmf͚JIIѾ}tȑL'L u][J*={hzwΜ9SUT߮ {N:U.\І q Ӄ>%%%?ɓ'_qfΜejĉV+%%%ypp>SY,رC2eU-RJԮ];di5kj֭8pV^/BЋ/Fի5qDk2dH~Ο?#Gh߾}zw Sh tիW/}wV.]K.[ւ t뭷ZgXBK,6b]rEuQZ+ 4H~=W^5fKwܡEN:V=h„ Vʍ7>PVTZ5UXQΝѣG?_wzWUV-ժUKںu֭[իW[8pF5jeb ?3/^~ؘٳZtiM63<)S3w,Y%KESLѣ>鞈ٳGC QuEmڴIǏWjjz-=sCd)jS+m$r k;jժIn%Krgu߾}/777ũ\ry{?ujΝݫP]xQ:{lm:G1f67_f+߯M6I֭[vej-Z(>>^-Ӓy7/x SO=s~ƍz$Iwuf,Z~3f(_Nl=ںu7ook mڴi~AWtttCО={Y_xWg h /h~ 9nɔ.$$$s3줤d'f͚!I}Ǝ9ҩk֬QƍwZZuƍkر/4c ܹS׮]SժUձcG 2DUV$=rssS׮]u}Y ,Ps|5jСCQ*Ur?{Q&Mk׮ۦ(**Jk֬Qdd=X_WԧO|9shÆ :yUbE5mTݻwWϞ=3skߕ+WVLLL?ŋkӦM:y򤒓g˶p;}t 6,O3k֬L bݺuϴyfʕ+Eׯޯ :4Og>.:|mܸQOVjjn5kL{VN3 ;C_=ǶFvܙߩVjUNW1|Wg9RJ ՗_~)Iܹ~'r`=VkKP>|o߮˫EzWu]wmr"zլY&l;=ݍ+GGGT1"""r-[qm^d,yo>} ˱}zԶm[-_\:w\1ǡC |ܹss-w3nf7|c,!_dO߼ʸ{ǎsm׽HkE? 80?7͛+zlW[?5`xFcE+?8\aÆ s-g7U=ԣG̙3'kaaa~~.000_rZ:'5RF4jԨݟ hڴibŊ~iٲZlY{j5fh߾ڷoOӧu[h-Z7mxxԶI&~t ?C}yjjժ|߬YL3od/\@ga\wqq|+vƺo+...vq\Lle\ 5SSS=/֬YdgU\8q/t?q|g+f?8222ׯSm۶}'zG޽{m~_~/>װئř3gg0/˫nݺڶm[=gp! 4Hk׮zܹsz5h l&ݸqFx8p@{W!,#G,ٳG?$]_~7}3.1'd~1c T8fԯ$MUTriȑyZ駟**UJM4Q@@.^g,|}}Uti=:Sr۶mܹN:eˍ˫}Yn$hU8W^f͚^zI Vttu777曹qnz6ߌnyyy#GX2]̙ߌkFvN8!)֩SGSuEUV\]];SC w}}{͵o///mذA˗/W׮]umCR}Q}:xڵk~???5oHY֭[cg[:tS[gggM4I;vаaTN.]ZƸ :T6lcǪiӦP\]]ꪰ,ի믿gUzTT)O<>C=z4KvloԩS'Ivk vÇ\r Q͍f%iݺudPxxx_~ƃٿla2.뚐!5kL...S ̎dcǎjӦ fv+n6IlJ/}dz.((iPܱ48. Twyu![N?ّ>}Z+W6;F&K?C! )O4kᒤ .]Rtt4ibv+kW^19 (J(0] 6f1]Vu)rK:tnvi8 b())x+[` h+U /h„ 5kّdX4qDŊbQxx4}tdҸqcj(11Qq>rHyzz#`0"aȐ!jҤ"""2m*oK*..N=Q|ڶm P"9;CL'gggUXQT ٳgK/oִ,h"yxxFqqqzTV-M08Yjjw1!%Yeyfn[1#bpfΜ>(k˗/7}}}oެXj֬&OaÆiܹ0`)9_~Y<)A)11Q6mRr̎ A۶mc___n&%СC5p@ڳg)/^pS^;;o6lؠ+WvfE3sk֬Yvڵk-[(00~Q+Λ7O3fІ ԼysŸO&%]sqz1AAA6@D@W_Ր!Cj*#f͚ںuj֬iv9Y4pG:Vn`Xl0"IǏ7;FвeK#b=APA`pfpOLiwGb lM iJAK@!-U:jK.(l IDAT&mE7G!OOObQrr1̎Y INN7|cv թS'yxx  `Χ}ǏgᠠDp[l1; 5;0 Tz97Lc}:ujM-KA"ڲeN>4(44T5k4;0%@ƍfvbڵkڵk1ruV+_~kƌWMNf @ FA͞=[C bQ&MTZ5%&&j׮]:u$iƍj֬6lؠ͛=9rJ=֭<;v/ڵkŋL2+WO>JNN695{ P ;wN{ァe˖zVםkΜ9sGя?x(C|x 92vzRŊ={;0 }L'=Mf2Ο{9yxx:pPiii7|k['''ܹsRJ;Q [XR}CNNN֯_/^4CBB @ANNN4;(t1IRhhBCC -]TW^>S3fl෵JcÊf'MԽ{w#JSNiѪS/_`#3 ޽\իWAfGp4 W_d#q%}۵{n;3ŋf`g0ɵk̎\ڤcǎBplQQQ9s,YЛ9s觟~Rdd(2d "˺UfM||A71 { P <851 { P4o8vqq11 {r5;PT͘1&t&P~~~qJLL(I?AAA6¸zqq60%޽{=z=1VB^+WJ4hGy40 \]]լY3cbٳ?=zTgϖΝ˗F`3mڴQ͎,\vRQ8խ[WϟwyGޙǫk׮JLL… R:(Pڵ'O󒤔M6M+WԈ#裏J*ի5fy{{맟~=crr~bI&8ps'O$թSGW֤I{n@ `jyÆ bb" 5;O>ѨQ믿ԩSJIITZ5/PQ0k_uԡ z^1!p0ԩ1 [p0NNN0;,08 ( 08 (p5;@Ig} 0f=Ie̎O6;njv%K@ % )69&((idLp-9I?}8crs01([&X, 'L`Xo|9{ 08 \PM\< &$PQ($[&(MHPtY,-^XS\\^e˪gϞ4h|}}͎8 $uMEfRڵ%Iׯף>w}WK.ULN  zt5^(JR۶m5p@]tI={ԙ3gLL 8fl^lv XHT6λy씧nv-\P1110`͛gue˖裏te}zgMJ 8fIAّܹsJIIoulٲL0 &pwsSXVf8(ŢK.ay{{ɉ/sѣ5n8uMVo^z7;p(`.]aEDDXZEF#Gfy$vmjݺ͌8$ %ı_:AAA6x͘1Ck׮աCtE+WN 6TSOӳPq\Rnnn%`Zg |baʹe#8P#˦MԥKEDD[noQ~t=#Ghڴi2dLu֩FwSeʔт `smlb"(^iӦiرJJJ~֭S׮]'|R7)Wond_|:t{GsU```r~PZp/za~ӧ.\srwfcǎU||$_~*SLnVuI+W//B=zk999G:v;oU;7aAbsڻwNɓ'%I ,ɓ3zq㓯4@ Çy-flk׮,۔-[VTZ57ubK*<`JJJ2+T|}};6;vTRn:5m4_Uϟ$=s %6sL+W@1eDY7@#p N:}۝={VrqqjciٲeҥV^5jX]:u${W2 P]|-OiiipBNNNZl{=}zG*-_\ o=(lQzWWW:r䈺v/̙3z'ua0 ԩS'sQviJRJJJׂ#zչsZ,gtС:t萿0 \\\lv E\- tߍ͍3yڵkV瞞X+]tKNN:/S-( %\ٲe t_BBq$///[EP@JUZǾy;0 n:}kT"777cOߋ/龌Zj% `4>|,...Wv%I}'O4)E`(6oޜ@Ipw>99YqޠAewRJob%<3gJrmo>Z[6yN?,oooIҁڵk:u($$Į `TR޽qlٲ/\8~ @P$W_D'M7v?oÆ 5p@`3*Ub! UlY<00P&&"55U K|o:u4n8?Q||y}Vm 2De˖Ec_#f^Zm@z-I7w/E.n&&8/4;0YJJ>nɊVbbΝ;kŊ5ڌ5JShh@UT)>_}UY,kѢE:{[nEQQQuaU\YVRݺuo֏ (PܹSNiӦ9rf͚Yfύ3Fcǎ?֭f͚HWQz4tP=*]~6B bK8q]`?L$e(+Ng{رc^ }PdpQ0jav Y;gת>{|p0ފ0;6;d0Ie˖5;f3;FG}T fGE`Eu=hfG1ݛo=C|q؈bQKƃ`Euw뮻[oevyyyi͚5Qǎdv$6?,7i)fG@ruQvΝkv"_ .ԃ>^g6;/ڌnbY((2ԧOZz̎Tz4ydjJ={4;^ yAa hEٳf 6;N4fhذa7;(b((Ξ=^{MAAA LvZ]tI 1;N쬶m>Ӳe(H:{9 Dz'((.Y T_|$UVrrr29Mr>ӏ?d RmY_yo()(0b? IjٲiW\ѶmԢEB|sСB&P8]VPXf@ɵyfꑾ3g4n8{󓻻nuY7n,P>|BBBT|yJ*ݻ͋;wjȑСjԨ+CުS o|?5l0խ[WVƍ5rH8ݳgΟ?/Ijذa8Jllz-5o\>>>6{5lP׿dzk׮B9s4hG.] @I Y |Y6UvmJJJoF~OO}g111Zt.]^ziOkתSNJMMt-99Y/͛7OM4̙3/]CjѢE~Ψ(EEEiڴi޽{2۫͛7 4׽G[5zh]zy[<<-ZdI&v횢K.թSzjy{{OM6UʕСC:v오3\[je˖SN9{9mV۷o7P4h ___ܹSǎSBBX^JgKRUB|kqÇǷrBBBb]z8**ʦ@D״iӌZIrvvVxx^~e-[V:~еk'hyr裏ԥK99]kڵk+())IK/O>,ɪ榟~)S_՘1cD[ѪVZ}W^FSy*UJv-[Ayj~9rD|~cbŊ6mz)gK}(6{}/q~[Nɓ'g96m(22RW^լYfHIRR~zlҪz *GQJJJYoGJ*U{c\rUNmK9}…Bg7R}8A\XN' P)#^ze* 6O>eL2%S7~X4{<UVՆ o>=ٶsvvs=gYKIIѻk[m6׵w?Tm=g*fT]LD%'',$SY@HM~LAA> `IO?tҗ<ٱc\ی5Js̑$EFFO_,/|pr\vʹ~3*S"f^c%%%%O |. 7KZZtD͛7ss?$8q¬àS^rlo>ٳGmA,Yb?S;wnlѢqfY,\;^vift뭷ĉ:|8?G}46yߥ/TpQFz8n߾ʕ+g cǎˌڻ[~~~SLL>s'ϟŋM7իk֭}G{;wn¼ǮZj pxOcDz+>>^fpX3;F2n8I#<隓VZu#ĈҥKa\wqqQxxxm?UXQk͛FVyr~\xxx7GGf~3ߌ̙3JKK}1_~Y˗Wݺum۶|{5k諯z~wYI/׫W!k׮9>U[8p'IYiG%ʠA2-S|94h\xxxnܸQ#GT||8޽{xٻwoxƌzײ\e߾}۷&Nh<~/֭+z1T<x7tM6t}ܜm|#{$-_\&M҅ ~?Ϲiʑ#Gt%IKv-뭷RUbEOZ̙3~2~>[ꡇRF8 @4L~[Oѣq)::ZW\1]pXֹJ*3fʔ)k^|Y?4h`>}ZQQQJHH0[z}8#ĉr-ZРA'bӔ)SԤIŋڿK"GEEK*]Fm'K*+VUV:{N8vکF[yf3ݡC1lذFm\W#ݎ;Tz\О(I;wj{jŋծ];5lP5j(.+۷o7CCC cڴit颈u-_~7ׯ.\|PuM6MC є)Sn:Uw^?J޺t ꫯR֭ 'b1;Jc7̵k۫e˖7oU185k^z,7|SF5~g{yu\KKKӈ#4u\g7iDںukkuֵ)]_Fo߾?vZcO?q… Q6E~rttׯ_>a%a6mG[VFrFe{=,,LUvmyzzK7+ײeK?^_\Y&MҎ;4l0թSGKԼys :T6lcǪiӦP\]]Zji˖-աCU\X]v?ϪaÆZW:u$I[$Ʊk׮>|ʕ+5oʕ+#e˖Iz!ӊ/zHHMJΝ;k޽*W*U$g닿X(5o<|9߿Q6mZ`___8"}||$]/:jժU L7|p 2D ,(RK.)::ZM41;ŢٳgK^yݜ PVV/(I*_,ͬd\eb:qℱraFUT18>{lrL׿iÆ :|q k׮U:uܒ:o]mڴ1;JJJRDD(Ȟ<aqƹQ``W,e\m"@2n;PԶ  LWT)bh„ fǑt?6&N}1nŢp999if@Zj鞚5k۶m˲=c,MeWd% C{w^Uu.MH DAAEE-j""j+Պ*N:ZlZhk *kETZQ NED@d3<%Nr|'gֻYg{#G~8R' 0vXdI\zickƍz+.R˫|p#i@PXX'ON裸bԨQxbɒ%i޼ytI0+vİabqUWE߾}Ӳ_8v~+<^|?~| ֬YS[qO=.))+W|hذa|k_+2:vov5*nOZ(Q:2n1bDŀ_:׶/b7: ͍?k ,}~Js>`ɉ#G_?cN)x7bOw*@YVDyΝ6l+V7o^ϯjvnݺRJKT{u@&Qj;ҝFЯ_tTdԏvxȾe{HT䭬R TG:Nܟo5jTq7n,nҤIu`\fͪ4nYYYѸqJ "`\NJKJJ*5 y敾w0Ps vsOګVԸ- ]v W^xe*5n P;;:{9<9sLL>="">߸qc|ǩvϞ=k,7vF;<$??I_3fl~ũ 'Pc [o}f@zѴiXzuٳgGΝ?qnݺE޽wEv0@ϖ-  6ڏ?x򗿤_y5c\f@GzNj_\QDsOҗ믏QFEqqq}qeŞ{M^z)u߃:(.]*PWdM6SիWWzln[oe˖W\M˗Èhڴio_p QG:tMFM6eƼ#IcԨQxˢu1cƌo~gώ{ PT}Cq+ T{޼yCϯJj@YpavaYvm1"Fznq7;?qq1bĈ4iR{QXX{wx1lذKQFRj Pw[G$dK.q]wHl0@P Ӽy/ӝFj޼yS(0dhٲe , !\ P\٤Ii7ʪL PVݷfv+ B C0.'i@PTT/B̘McFJNw d(`4H$֮][ , !\ ;/ܩsݩ;5̥ ;?aZopc h !YYY<&uSII6ϕ5S(P-ʹ( yyǿ"+++ׯ_5k֤)vw]\G;k~VS&P4m4-[jZ*ZlhP$IV*\ӦMӔ ;J }ZکՓB[bѾ}{P$I ,f f͚)#v0@14] "77T`1{h֬Y4i$rrrG)))XfMZjonnn);v0&+++ڵk}Y$Iz(-[V`ju8 TG=ҝf5;nSnʊ;FFҝ ;TN*U\ 6mYfԫW/TFEΝcÆ jժXzulܸ1iPGӦMYfg:J bرO~` #eeeE AѪUH$JJJ, PdeeEvvoP`P C(d` !20@P B C;Q/Y iN0 B C0@lܸ1Fj2$ׯƌL @mذTO>IS&Plذ!|TW^ƌm=3j~i#F;ϟzh( @X")P)PeVJw ag~S=nܸ8w*oV'y䑝%.'Z6駟.]t\؝{.˿ږ{}Tgܝ53Զy!]6枎jܐuPrw޸;>3<3{fv0U֬YR<0zS17nsYrZ.]j$.d~_uPrOW>b5[9ljۼ.uPsOGN{Tt˿ږsƭg h !20@Iw???SZb;r{m+]ߚGuƭm8ج.jc^ Cmݹ{zֶme%I; ?zjϜ93zƌ 32Y C(d` !2DNjٲe >T^>[2 k^V$I`Y C(d` !20@P B C(d` !rҝuOqqq1cƌ(((VZEn裏t@={vr-qUW]UkŇ~VvAzh5g {s5kW_ٳgǺub}޽{Gݫ۝˗+͋ԩSq_嘵+M6wOs1ԩS[o}kѵk5jTӄ:e1toTVO .nݺ#<+Vׯn)ڴiG}4O8âw@:gİaâe˖o;ƍ/R\qqƱ׿v8Wlwϟ{nj*+yx㢋.Ν;ǩw(f@%X"9㒈H7oL<9$9rdDDr'6mJ_P̞=;:thDD2|tʧ~\pʯ~v3>KzDDҹswMm+**Jn$"oW{sӓ;&~ٳS֬Y\~ID$O?Pu6gO4){sHN9dũmK.M$4k,0aB%_}WoyU OSFVVV;.=+++9hӦML0!yXn]?YC4gΜk/wy'JJJ""㏏?>@-hѢN:ŋc;tׯ_|ѨQ2eJ#=;;;EQQQL2%^~hٲeq2Rmg͚w\,Z(ڶmSL:ׯ_?N9唘5kVsa]v_@-;ٯZ 0 V^tP=3QFo}+^yO?t 0 ڵkWCmf hkI-qgF~wE!wyg?~ݢE?A?+#-aƍ_"71bDcwy1{숈rg?Ym6""xOIm3όKFDhٲe}hذa9ϯv9 :X~}DD7 n/'''~FVVV_>oŚ5k*xu0zwK \qʊ+2վ뢸F󃺢:O u)Znj~U3~8qbDl>OۨQK""8*`wSx }݈hڴi :ܘڵ3<3""VZ?ϫ:jm/yDD1`rc;=؈|ܾu0H$""ڶmGuTO;ɉv/. N IDATldgWn-#WǓ'OI&Ui2goڴ)~ߤڧzjU:棏>fͪTuU&gqFywƊ+;6S\W~:>c#++1{W+5jTuYUOS08;gϞѼyTy,Qs/ .ܡ[~QRR_;`wSWqEaaܲϚ5kJ}/^raÆTCԸ-M28ԴW_}T籽{N=~K P3jbήJf͚EΝSRyNʜ]ݻw Q\zvn*5L=.))՚T:cW\ӧOּVM>%KTj[kUVۍ]vML5b] @\#"]vu3fT[N=cꞳ$iӦoҤI4k֬J1!@iua^dI̙3gsܺڵkcٕ[(P jm۶RW[N=[F-*5y,Wsʕ+c͚5۴iS\|T.UNu0*(((nҤImoҥՖT$VZj7nܸcZ51gWbPwl)P?,4hPq[jҊ+Jݫeu Pjbήw>6S\֭+ծ_~mAvj *RsZM>jF]}l @5jT]TTTq[:ԔÖy,@ͪ9@ͨ+sρ(WӦMK +5n~[Rsز:Y51gue90j׮]ggڭ["ѢET{sӊ8صjbήw>6S\]v-^xqmݯ{Ֆlϖ籅rJs UݸqR_WbY]tJ̺HrٳT{…` @Mr PwĜ+VT*5 sv^^^tes:fÆ cFrp ڳfͪԸ-eeemjRu6k,?j mĜ]1wm۶8E]dɒJ_'Q^J(W>}bNyJ6mZaV@o|ԗ5U9߿T{nVs'\oeb̙3SSO=RyNʜe$IO>rc5 ^z}/?1k׮-!xZh J+s;gΜRK95jb>Cޡz3g80ZlC1Lzgyvf T?AdeeEDٳw߭>OCfbС5#l.K=~Wbٲe7n\q.]J}@ͪ9{˘Ǐ/U(^̯ѫW ; sv㢋.JUoC )U@PnK/MUO=馛iӦ5o_?1bĈrnܸ1zTnl_4g:4uEҥKcر\lY3&"6_Rye>g_Ѯ]>}zWG#""6m7tS} 7v~ѥKxGJݳaKGSFDqW_}.ꚪd駟.?ITjh޼yDDq1w2y1gΜؼ|0n6ٹ#D ""o,uo-p rʈO}TuY{gLذa6JJJG?Qlڴ)""~ƾ[f:%JXpar衇&l2yWJmǓ $ 0 YbE2ᦛnJ""sG'%%%N Ү81cF'Æ KիW33"38#y'wy'YlY>d}M""ӧmڴ){$";/ٰaCMDQS&-ZH""۷o駟&?ORp XSO%5J""4hPxԶ+W&w^INNNnꊬ$d9ڵkE1"֬Y|pt1>͛7k6W^ӅZeӦMĂ 瞋#F+gqFs9:u;M@,]tuwĵ^ݘ7pC5*O>ѪUxb޼yѡC>|x}ueΞ;wn\wuSOENNN'5kN,^8uv[~镎 Ps3kI&Eƍx뭷 ?뮻_~Y)֭[SL?0֬Y{G3EnnnӃZ&NbŊxWO>^{{#<=j/^SL9sDQQQl2{v/ueΞ7o^L:5ϟ%%%Ѷm8ꨣrJ CV !20@P B C(d` !20@P B C(d` !20@P B C(d` !j/2gx'ӝ P)"cƌ?<6mڔT:HjҥKqƑU?FJK3&6m *sc HNLSOźu""M61tׯ_Gƍ#;{foܸ1ꪘ0aBj 7C FDD$QXX_|EL<9XfM*6uߧ~׿{^}k)" Pƍ/K4iҤܾWN=nԨQtMeׯ_qq)DDDnݪ9sa̘1qg1T$+I$I@&Ztim6N:x"++ܾű{ڵk#"NI&mw]v ի+OгgXxq,Z(rr}=SOENNNn!mB]D1S}~ѤI 0cƌxK/-/; @7n\uYѾ}}J+[KRb1dȐr8q0ԀK/\%w#"Nz}Ը/2>*H$I;6:vXn1T+՚^ywr^vL 5`ܸq|RQ?~̟??ծR|8'Ø1c"".c `˖-s=R}zֵkFx'{q!1T{@ xw*w{FTXסCv(/jI&ŗ_~Æ c W@myfv"???}ɰˍ=:"*^yG9` ivژ>}z]+5 O]t)ؽ)@Q\\j+^}XjU^ݛ0QUJf3fLdggYgUm1S{S4^5C9$ٰ+Z*&Nw\k׮:``w$IG99%%%lٲR?7ӝV=QXXC 1@|bŊT;S}W_z$IS6ѣ~qgT[g hH-b׿5ݩ/^z) {W])| &SNM=Ύ:*Ԭ83ӝF1nܸشiS}ww:)@lyf=b=Hc65Ͻhc̘1ѸqjvLR4XdI̚5+նTcܹi5) B½Zw_cƌ!CTk\a%H-cźիWo˗/(((/8cx_Ѱa ͜93Œ%KCq'G˖-+ϻov,]4ڶm{lWzؙ\-Z'NŋGǎc{Dzebĉ`4hPٳԸ 6ċ/qdgg{lԫWJaѼy߿Ɨgg>}z,Y$3lذK~>h߾} 80ZhQWf-}'N=h֬Yjڵkc„ 1o޼hѢE 0 ڷo>}+ Zm۶VzG}~ <8.袸SN~85kՋ뮻.G-3ܹs㏏[o5֮];vW_}5:t\sMUǜ9sFvitԩSe]J7|3rrr"+++ӦM6mZ_*:Xxqt9N:u/8 .)Rjܸqs1zhݺu$I~|Wmi̙1s;;;o61ǎ3f̈-ZldrDĽ'tRx{ҥq'ưabᑕUf:eʔ{wm_Dl^zե򗿌2dH4h`_'xb+oF̙3':r_gUU1ԃ mi&"6/w\슿n/^ĭ[ To /~xӦMvE= 9N2cڵQ\\֭xW.oܸqQTT{W4l0rrrb=f͚UK'tR1~Xn]4j("6j2|gժUl{""O>Q^m_pql7GK.K.¾Uu͚5e*&I7nL=nݺ0aBDDt޽}v%""͛WMVDѣ#"bȐ![]yLݻׯ.{۷77CÇǤIbΜ9E]S 5(:u*@V䮻sg}v`8ذaC̝;7`[nQXXMbܸqqġY]}LmYL}Ӷm8Sc?)n喈Xvm q}hnPK@@ ͍իWG$UoDDÆ S׮]zzя~[_ѣG9rd̘1#ooŊQ/:tJ1f̘*kҀC? ~x5^{իWn:""f͊<0|T?""㏏3fT8&#"4iZyqǪU~w&׈G}4FG^׿u<1nܸ.H֭W^'T7lO>dvaѵkt[Vk7[W^ :4"6>Բ. | &L_~yW_}5""Nx#?E͍#8"())o92 S=3FǻuAwgr\{,H /SN9%;vl}z8QF=ztdggwJ.ѣGDD,]tmsO{Iӎo֩S8餓8}8묳j*U4`˖-.,V\qM7~ַƍTÇG bܸqFD;qYgovDl^o߾~wqG\r%1f̘+b͚5?1|/Y*ǯ\zu,ZԶSO=57n˖-|;V\#GVZEnݢ[nѳg۷oyqZ2:""+++ƍ~z\ve㏧ 7n/&NXn+W{.۷omPPPϞ={텅"<_yyy?)֯_=X$Iݻw+;&O\}ӦM1w ҎorEE;7nx$I$85gΜկOiӦwnѢE 4(FY*yl(Zj?O>; uؖ&S =?~СCI&Q\\+WsԩS; ƴiӢwնSN9%&M_|Ejcү:7s΍38#u:[ PG-[,_~ym999Ѽyh޼yvaqF޽c„ V^dI1` ?).t) dٳgGaaaꞳw}#"/?Q\\g}v$}>իW\y啩6lO>d{i JS2AmڴQFEaaa}~ለ0`@8o8Ӫ-&3qxxbȐ!{9;`>3g1کaÆ= <֬Y^RRӦM;/~_7Ym+bѸqjΫ_~7xcdgg3<cǎ&*J$Iw@[qgܹs8""cǎq}Uk33<?ӧOΎ 6D~~~?9hժUSUG{'Cu2%20@P B C(d` !20@P B C(d` !20@P B C(d` !20@P B C(g2F`BL`  0!&0`BL`  0!&0`BL`  0!&0`BL`  0!&0`BL`  0!&0`BL`  0!&0`BL`  0!&0`BL`  0!&0`BL`  0!&GkIENDB`unyt-3.0.4/paper/ufuncout.png000066400000000000000000003452441476461141700162310ustar00rootroot00000000000000PNG  IHDR^sBIT|d pHYs.#.#x?v9tEXtSoftwarematplotlib version 2.2.2, http://matplotlib.org/ IDATxwtUפC -MB$;HQ@ j* .*$ ERAEЯ@H$6Y' If2y>9s~=c|ޏ0 C20`p"E A .\0`p"E A .\0`p"E A .\0`p+٬]xѥ@ ܅ '(""BjR=o>Gp}ᇺ{ՠAO5҃>GE&0 G9qBCCoݻk۶m[PfmݺU[n޽{Wí[G)%bzG'zO?ٌo*@ŋ+55U'N7|ӧO9><<\ݺuSPPׯAb^|r]v8kYz,Iڹsڶm+<>3TƍKN#P&edd}ڻwݱ^*>5ʦu~RK@ INN˭m\Dž*:: V(*WJ677~5tUk׮UVVm2X 4[>2+0ݻw;@)Ξ=ueG(e.0 vcs,99Y6mұcTB5kL=zG*J qvv;9rDIIIձcGկ_TԯX]xQTzulR[孹^۷JNNVJT~}uAUVycccxY,)<<\ 6zEffK(ݻwm^͛76l޽{禯eݫ={(!!AԦM5nV 8e֭[ I6?:K/d뱤$'0[ߗb۶m3ƍgǏaYYYƬYoݮFIFݍZ9ߺuk΋7"##]fM6,K_cƌކunOOOcƾ} =gvv1oyׯ 0F8Ͼ|*7Ǐ7^uy W^5Mf~ 4VZUWVjjok6$&0L&uf͚QQQ7ygʍ^PƍմiS+:~͘]vE?2228vnݻDj4ںu"##=z(**Jiii1NR׮] /(...ybbbtwh͚5%R$}jԨykJKKSrr[^^^JHHФIbjn٬W_}U-Zʕ+5uT>|X׮]ӕ+Wt!lRYYYꫯy8cԺuk=쳺K.)==]۶mSV$]u]:u-F7W/ڴiSsx ͜9nez~޳4hl{$IӦMɓ :,,LsO?Bs ꫺;)S(66f֭[W_UJJϟСCꚒ?yz͛7+##CJMMճ>+ƍS߾}\Y 0]܌m۶gϞ6}:qDWXt߿_sџИ1c4p@yzzj WffVZM6չsg}.]7xCϟWBBQcƌѣGΝ;B :t6lؠ,>>>VǎdioݺU=zs~[hB-ZGygy&Z5l0]VkVttt[2رC;w[xƍ뼇֝wީ3g9.))I-[ٳg}g֔)S򭹰f:tᡇŋm |=cmO>]3f({*,,̦s/_P4""BQQQ6֭[s)..N6ݻw[iii>|ڷo/mڴI\:t<==}>K,?,٬wyGO<0ԤI:t׳gOmٲN͡w}yᆯ1x`ŋvykolofڵf͚۷osv4iZ9o hlԫW:0͹6n6իymX!CX{ͷ?q͵k׌ƍ!C;ܹsm4LƮ]=(^{"f?BmS 9/ 6, ('NߠAf͚F.]SNٍ뭷^իW{UVYx~/_[@i` hRӦMmڻw?+W+"7h#G ֮]ԵkWck͛pA-\?tQkl6+>>>ױ6_pA{skʕ{N͚5˷۴Ϟ= .؍{饗K;ȑ#mV|_~9sf͚E>Zjv*:s~>^z)::ZuԱi<ٳg0 իWO/R 0{B \TM}׿V+V9}K[*U|My-[f<>bڵkv}y[_|Q69M8Q^^^vFBDے;wn-אSnnEU[F9jҥKd2_~6}}N+WHYη;f[qTlK9*ϩWM;)VZ׼ys6};V,u5o\M47n͛9>k{uQv[4mTWciӦMv[p222$)Yΐ8++Kw.Թ(WsaO֊+߉|zG5h }vgȹZ1--An|O?uܞ驿 oεm۶-|wu<WǷ~Μ9Sm|B(9~'2͒B/gnӂ nHp `(-*)X߾}fSCm5jhƌ9w>#mڴɦ0 :pT,u^v/MҥKRnMa7o>.*U`?~ڗ\*޲e{=^Z۷W```!~~~RemOۧFiܸq6ps:+wC6o\sO 畖Dk>OPʹ߿دa,Y3fɓ6l6lؠ:ui˗/۴SSSe^8<@Y@ .bŊ6U 6l;yyy)**J2wlllfι"éSVbƌ2 ?%ds m?Cm۶w}'IZh-UTiرCaR`>z- dX?:(].]dܹs̛Ӓ={j̘14g͚5mqqqڲeMm}wX,75/ʎ *ح裏nj#pv~wFVZ2[oX#F9۵kgoy>@O>ݬY3y{{[gϞ`yΜ9o\/Bǎ+qqq SJJJq%\PBB &܋-i;0[9nݺ6}7nҥK ]O?g}VSLyyyM66nl]UVE:0 SKVVV_J7tݦ &zxFF#Gr`e 02+;;ۮl6;j*c????Xv}t/\9 67Iz{B؍=ܣQFiӦ6Fmޱc.\X7$$$hȐ!0`6lXɒkrÉ'nZ9E:gsum޼Y'N,0Ԉ#t?(@{PuLP^s[a<0XۯԩSھ}M_RRRc{1ׯfΜ4s,ԹsgY,͞=nѣoM6Y ۷o80rn}5mذ!.]ک^+瘢leu)###֭ӏ?XK{EÇ޽{lgS_7P*U\#802+66֮/55UG)ܜAf~ Νi_zs[aV._\w$K'NwÇr>U&///_[}v[wa}|UkWzu͝;n,͘1C4h~i;vԩʕ+ZpUfw~ʕ',իWm۶G&Eնm[յ&Mӧܹj׮@c}ױX,:~MߡC ]g׮]mqqq[=k65| :.p駟sUդIkN&Lдi۫e˖ڹs gc2z#pIIIjݺu[>Z`׮]h5jP\\<<< Ξ=۷ɓg}V0a-XJ*?~BBBt(&&FM4gZWVM ՘1cVyWm|||zXcjjVlnZC=kɓzԷo_f|v4p@5nخcjٲeֶdRRRR+͛7%&&J%}}*\e˖ɷpVʄEEE?~z]x15R޽kРA裏ڝ3h̙TMff^}U+>87h@֐!CSOcǎrwwׅ xb͘1CW^U~l2UZ5׹W^Y_ۗN:W^ TddԩS[L&=7obcc븾}jڵY,M4In^{M=\cCiҥ~B/dm~9uAVR͚5dO>߿F%-ZHڳgV^[???s=jڴ#'ZJC wqɤӧkڼy]ܰaC_C Q.]k֬Ѱaّ#Gjv]٬ *QFut=>|xԫW/}'y (?nHݻaoQJ|zyyZ^W_5 mmw]BLL 2j* 8eA .- N:6UV~uZp#p_kP9bs rcWfT phm7mT>>>GAzc hp"E A .\0Gyyyiܸq6m}@9榐Gb"E@9?~8aX,2 ѥp$777L&Gb@ CE'Ni(?te]|YYYYd2)%&PNddd(!!Aiii.1 CJLLTbb|}}UfMy{{;4ҩSJZZN:,G"b0M@dTrtMHK'(>vt tfN>lG ӧ,wwwGB"+##æM+WVʕ)776 +Ţ,]rEW\bP||X!f6b`yxKQN #p\UU{~~~@YRR%e[@ ÐlPPTXѦm6c2@9d2PMX,v}@YۿC`Vߡc]j=@ao+A .-P|hTJ·(,$]+W.776 xHHp)U8`ƕ˭A*/oVY,.e8 V[nn&G\]LP +]K;?kj0M뀚y: থ+S!{(Fut(M;ME` hNy=kwHHH8л&0J(]PlPnݻW< 0ʥ 6Liii.(6(w иqtaG+GyzkP6mʓo/]Pr]aC]@6 L=zw] P"S OHH?EFFd2̘1fܞ={rg2ܿ^uuIg϶4i4h *ouԩ\iӦM6Lҥ9GɳLu.Pp"ԩSfi&qM4ў={rJl2q?FI&:u~GbW^Q˖-;ȑ#JOOW||Ԯ];;vnuiƌ2L6;wƍdssN1B{OC/_m۶sڶmZ~|_4@9 ծ][wqG㼽ժU+jJ+WV>}rQFRJ>}?++KFw}ٳgK.2Zlu%nBB"##~z9k֬ӧ+..N ,:4:<==վ}{ :T~~aM8zQF+Z*Vƍe 0Peeek_Zӓ_0 ㏹>|VX!Iںu<<ޱ?KR.]fF~Aݻw6ʹGyDC 4n8o߾㏵b >演}NJ}̴cbbtqkz˦o9ծ][$UT9d6a-[ԥKK:t0P4Mz ɤ>L ,ŋ~L&խ[WCĉUF IRzԯ_? 2Dڵ7n?ڵfK.qj߾^uuQ̙cwcǎ)88X]tї_~)ook{kȐ!-sX6nܨI&W ԦM=3۷o 0P k[&ɁM'Nĉ_߷oBNa:uJQs6loX0Mxf/Y{Ĝ/-wςᮎ.p5k֨nݺѣK0U5_oU{ǸO?Yf\Teyz7TbE=#.p!im'N> )bھ}4ydU^N2d6Щ{lF)W˗wQʕu}iȑ:|*Uhڴi.p*pJ)))7n%I7nTdd~U:<)9 7*T_fϞÇrE#XbbGok׮98]V;vTŊUJ 0@1112e0N<.]hǎ.J̙3u[.NSNڱcҔkת[n. pZ:|:w_vt9Pj|}}n:9sF{222]).˗/{_-Z@ Ԓ%KԫW/=]NOmGsP%gD 8ŢѣGZv<==]n? *::ZgϞ$}6lS=4yd͝;WwyFcrsWF6meoZ6퐐pJn:k_-vƏN:Z`6nܨf͚Zl֭[k߾}.pJӧOWժU5i$]x.Cí?nm(+ DBB+$$D?-HOOW޽jڴiٳt[o%Iڳgڴi+W:j^PbbfΜrL' /ҥK3gNlgQFFGӄ ny~m2Zj|||r3i$8qBsQffƎ`iӦۄ 4sL}zg蒬TZf] ]r3l+PNpNҥKUF 3Xm_%K%Zp|<^z%Kߖ4 ???=ܹs]8=` ѣe$͞=[1bziwtd*,,L$,[LE㕝؀ڎՖ-[J2?3$ߧK>C@ymhs֟l3 v횢$Icǎ-yO>D?m[d]zU{Վ;]N$IҢEߣGƍK2?3$ߧvکFJIIѪUun<69`%~z(00P.yW#۴ۗoݦ{Mef\EIOnnn뮻$I~i PNOՉ't G">$y2LiӦ6|{xxشs޷uݺu$EGG+33/4K*44ѥ(< M6IvjPΝDٳGÆ ӸqspZJ:캱CBZZO:uΟ?"sȑ[&Zcd2=Zt9TnTF yyyf͚8pmvSs駟Vxx:uhԨQںuÞkIԵe;Vu֕V0 :tP&&&^zvԩ͊4j*:tPVVV2y/"ϕ{֖-[Q\\֮]+??RyYױctw|CBBԤIk߾}pwQU{ǟ BB*.EADtD/JA)"EUQA$)*JQ@J)$ !7fH'L~֚>>3oٳgرcڰa-ԩSw^mڴ8WBIR ,>ӴitIɓ'u1IRԠA$jժ:d˟?1ŋ?e6f͚i͚5z5sL}'qn׮]ƒ;wTΝ-f{yyf͚sQdv/թS'=$I.^vM-ZÇs TUP!9rD!!!Dj-׮][%Jn޼ӧO+$$DRۦM?pZzbccs _ƍhѢIرc,-l޼(JR߾}3ksJѣpݽ{'H(f={F%օ ,){[ާJ(=$f?d2YQF1cbbԪU+cѣG?N%JмyԮ];h3f1_~ϭoˊ\ѪWQ-[.\-ZX/5j(ݻwOԻwo-]41ͫT_g(P@nJσԴiS믒 ӦMS>}f;{g,'O'H5G&M,9W|y͚5Kmڴ.\ ___]~]UVաCmݶm[mڴIUTщ'ZHqE͛3]]]5m4[ƿl6_o"""kSܫW/`ׯ'o'wwwW``ʔ)j?^z)Κf|}}ϗ)ˌs߿!!!^Hin͟_%[䴦 tiYUN}ڵҕ+9pey N q18粫+VPn,9::jF~KxŊӾ}TdINNN2dʖ-_~YZdz&MdߑU}QR%/S0`jժ&M(22R˖-SռyDc:;;bŊ:qDp>3g4EO?ʕ+'WlYmݺU 4СCӧkƌiΓPƍwY-UN>`ըQ#B˹s$)ٙrYxQtssӶm-em2ԭ[7,XPmڴQLLL?z!jF޽{Zhc+'e}g}f|i_)s>9~i[H*ygo߾ᱼ33۹ֳlruҭ[Dx˗7\1ΝȚP֭կ_?=mڴ4u }'fN/_gXUFFF*::i!: g^㑙sP '\rВ,{MM۶mS;>g7ҴpFX#׆ eE6m8p&O;whϞ=z)SO=իWյkTpǽp1+z6mRxxiӦmڴs6$N?\yM5 ݽ{W!{˒B iС^ӣG}שKl_[gΜ˗u={o522;:OjΝ MY%'eӧ:$ZJ۷{f{|kbŊΐ5khĉ:ydoM9)zjϟ_ׯe3*e W rhW)Y1d:c-f[GHRJ>YV &?cҥ?~{:u(Jҝ;w{nlRǎ.))&'TzuMr_$o޼J 9r̙cq\rjܸTUB լY3gH>ԩSUHmݺU5k̲{g{H8; ;iڻ5aa'HDյk4;rŋOSO=~͘e9EDDhӦMjٲQ$NnG5J3gLSތqEQ#5Ŋ3Ϝ9k>>VN]rbIr4l`ɒ%֭[}&OllR XVծ]; :TQQQ:qℾ[u)1LI*Tlb7tY/⊛ŋuYmݺ(4}:tn߾K.iʔ)4iRy%iΝӧ˧[Z}ejʖ-kg}V^^^ ӥKrJ#7oԊ+4׍mۦ?3J8/o9+fTHHڴi#///ڵKeʔIPPZliyNӧO?uyԐ!C/fj3O\={СC/É_%.Zjvڒ^,/8j(URER\I& 7oked%W_5,Xw}bm'OgϞ5kq,Ĕ<9s,.5͚={֮]+)nvey5khٺ}էOT] EEE]v?j/66V6mRDDDfs™yCBB+hZ?39EfW9}:{ܹ#r)|>~~~ڳgOF&s.Z,,,L|BCC%bIΝ;A UX1M2E{6m$)+HcĉaܾիWWu;vL7{W|d)9sƌcq.44T^^^igF.I K/dJY[ժUStt:k׮Ͻ ڰa͛jRJKJRJ,IjӦ6ol5۵k1q*Z5 иqҼԁ,L& 4H|IAfeMtTl}w+ŽWI3)^f}tcŋ->J* .HJrz뭷ߚ?39EfW9}%IjղIoѣ_2(W-ݼyT O<^|E=ө~'Lq_]N҈#TR%*o޼Qƌ4Y5j?X Rj5)Vx^^^ڹs6oެnݺTRrvvV!Cȑ#;wn1rHժU3 R d2x8p`_I={d*Y2eʨsZz,JRǎꚮ˗/KI//:IXۧ+WUV*Q套^zI_}5`=S/_^R\Ai?5dU\Yrqqׯkǎ:|vܩ_kV(GGGR?)Svʗ/7o^ڡC1B PZT~}ceyՎ;fuAJUdIo^+W^z)LOI^~$c0r~A͛7׽{l%Cݻʔ@VUK@gK-߿?̒]s!u֭Я5k֨rڰa:uꤎ;*:::1Ο?5jhر*_tmڴ)-{'Nhԩwء'Of͚IΝ;3fvڪ^f̘˗/K԰aC ŋշo_EEEY}5uIںu͛7kΝ6l.^YfW=zPxxxСCUdI5lPK,1]tI͚5Svn:]pAQQQf5mTC IT5D_Ү\6nܨ1QGGG|X;v찘se-[Veʔ1 R\1aÆ*V.]jժ)""Bׯׂ #I?5k,ڂ jРA ׻koѢڴi赋Wvmf͛7O/&Nl_~>}K;vЙ3gl lٲE+WV˼fL@fپ}N>-___5oqr9s$,Y(3L1b^p"##-?~\퓤D4Ǜ4i*U;vԒ%Km6=S0_vW^ʗ/~m=۷om۶ Q%lٲ$4.Rm&///IRppڷooСKZ\zjM0AS޽GM4ܹsm69;;}̙={$i7oޜ<8~ dnnnӰa4sL-Z֑f믿n( ,fY~~~2L?$RF &AݻwzՆd?k *wՙ3g,6rI֭[I訡CԩS,IR?.I:qVZ%___IRl2Rܾ^^^1c#I?'ERJY{7x#16mɓ'kܸq6emݺբ_|zڴifo۶M/RyWZ%///l2>Af͚ZdIEZzo:!;f2K@@<}gu ɓ'j֬im֢_"EԻwoh}v%*ݪU$> j޼ysk֬,XKf\ȘujΜ9ڴi [DZk={ԨQԦM1dL@fy7^ u\d2s߿}]Y’G[@M2h8p@O=F EѽSNv^ѠA7zh98,`U^]jp6|SѢE I)T^|,J'B m(QB+VLfk̙#)n W^p&Ԏ;lWd?ٜX[nM-ZhF;ٷ&ɢTM6O47) ֩SGʕpoVIp``6mڤu֩K.ruup&-aaaIpE;:::Q={ܹsޫm۶,ऊ1_XUcƌ1Ӓ%KR.V'NpYFTP! 80MuѢܷo_ݺu埑`;ۀD}޽ &X{ ,l ؛HgHOnaÆ)888oirG ,õg8p@ .|}}d?ۧkƍK믿N^Stt\\\&zWnnnFߗ=kʔ)ʗ:}Q IDAT/ɓ'PuttT@@jժ%Iy:vRЗ/_6f׶mVKu|I'5zhIԩSձc4_(k0`o{I K.<==Uti: թSǢO>D=ׯw}Wcƌќ9sԤIѣGՠAM>]UV_W=yf;vL۶mS߾}5vX(P@֭S͚5eqww[ԭ[7լYS?֬Yp_^]tÇ-o׮V\Ç֭[/w2LڳgjԨ :pfΜZj),,L&LӼϮ :u/_UդI/;3rsso!)nv^=r?G[,]T/t)\rj׮/oooIŋէO*Tڵk'p?fV\+WJUti :TFғO>lSd2믿۷U\9SC 1 ,HڣGG?\K>ZtFe˖iǎ?~ݻ… B 9rzŋlժM2E&LPhhϯri„ ۷)1jӦ>c=s|I `4|p ><~e˖?s:ulpGGG}G裏|~?~F={ƋWbE-[Jc8~yjQ, 7n:wl(Ȧ @ռ:"5=[O4 l i[ܟu 7o޽{qMQsiٶdO&)oa[ϟ( :QQ쐃IEE6 HթS4}tkNUVudc;r3B$I>>>6@NmѾ~5i$9;;Eٳ+(&&F}QGGSe>;$l60 9SPPEѣgϞ $޽[ƍ3裏TB ̙3Zzzmq~3fvܩǏ'!oo$8Pcƌy켰PT }ȈzΛf͜9S3gΔ$?~\իWO6l/IC]v \0`XvZ[Jm߾=SƆ}` h%(@.V)@۶  )OWxtҶ ` h%(@.a{tdI[KNHit[G1r K4`'BCC%I!!! .,Irp໡`N 4(9l$,|r KP\=;"I*U~(@N_x;tR-]TѶ+`0`',X Ijժh"'Q섷$Gq"ٝlfŠL,dr Iڸqcjm۶cEr K4ch޼Ufұck)$$D}ٳm U |h66J 9srʟ? ^xfY\b{uɓm۶) @_|񅭣Y0`ԭ[76ɥKԳgOnQ,ECyQʕmLΝ;ڵkd9@CKu&(@;wX}̤sZfMȎ(ع#GhĈ*Q&Md駟ԫW/-[V*ZZn.1?.???UXQk}v 4HK֥K$IfY֭SVThQ駟ٳ,(7Qe˖d25ker$Ptt֭[g;t ggg&d -_\k׮ӧ-u  W^Ֆ-[euA+W' xu) @?Í111x^uڵǏkԨQڿ,+X6m$Iz?HڵkiӦ\]]S{iUpp񈍍u$@r劊/ʕ+[ ?ǏkѢEڷo~W{ʛ7oݺu޽?4k,֭[޿PB9rWnqɓ_$i͚5:r䈾{ժUf͚D`'''U\Y+Wq@ʕ+' `5kf͚7ߔ%I[lɓ'dgեK5j([N|z:uN:fv17nX4`ݻ8ߵkW}:tqFjذ*TׯK>3u 0pFiӦz,Wŋ[2eJ~*NIѢE-3f̰(+X:udϊV@(عK&,Y2ž]tSO=eu!>uo)qjՌw͛i`/( L&ufq.Ι)_|wf}=öDc-:*RAWn `{}K:9F ,ϟIc0ҥRJ۷o( GQ@xzzZ]4R?-!O->>>YѲe$EvipFG7I h׮]FI<0`L&o N>mA6L ! rrvQ}mC8p8ѣ<<66֢[+ׯ_{F .-[~IҔ)STRD}ܹb;4gwE;r%F;""BׯO=0 w￟ho>}$I ȑ#رcG&{+WXoݺl߳gZoܸd aaaݻ~wm޼Y 6@Vb`f݋7-G߸Ǣݺ6J@Rzu͛7OW+ܹS4m47k׮)$$D0asfR|ԤIUXQf~7}WX֭[իWzD}k׮[oiܹ}%WZ,OOO5h@cǎUz2`X(^ϟ???34cƌ4ݰaCǍWn]or f<'T%1hrD.6L@6t iFj۶cj d2bt` h%(عh6L XB. 1$ su]zh=zԆid`c+b@sfIŋua͚5KKo:w%JlٲaRAo^J򹯾J_}$iƍjӦMVFm@& !Wqra"^6ux,, `%Xح׮$y>$$$k|||2' X``2㒿l6gR8\0KXXnjrѣG}@C@?^ 6޽{m@.6i$]tI͚5SDD@B@zgU^=MS?q [c`Ip႖/_.///*cnذAQQQ"##l2[V'_|޽>3͙3Gsεu$Il10Voi+$<|? k::/uX re˖)o޼Y"""6̙3=zXe$M>]{V׮]ccc2~Fd눌92ӧ$/ݻw:6`belH @l2ݻwOG޽{2tRIkf1%VZZtV^ &Xm\kȌ{sd{Un]yyyΝ;Z~U܄%.Ykͤݲeܹ#oooժU*cfw::xor|ԢE }7W=:>`/BBY'$$>>>0D$IM6dqsoѮ]-ggg[Gr2%D2fi(`6?J5jd4?+ܽ{WT {PӧO? pw^y{{d2Y<5e=s򒳳)W^yE?c#FVZJ,W_}UwΪ$)**JgyFE&Mhٚvޭ'[nWU&oooUXQ5䢾i VpM4IʳٽTE;k@{YiӦ/-ZHaaaիW^y\JiӦi鹞>}^z%ݼy3nnnzW5j(0`VZg*22Ro|||켊ٴiFSNd2_nÆ 2d.^hMf'|RԺukm:t蠸8c|gjР͛7[mWZ5;vL*U$ի6Ú'xB+VȳoÇK.]*U5]ѣGkʕV/zGrJIRVm6cnڴI={TzzעEtZmSѱ:˗տ}Vxxxh̘15kVS"%%EQx킃uNj=^Zrg2t 9R-*uoonO֪\1gJIM {ǫ#=:0c߾} ubD)KQFҥKJLL$Y8mlWXQ͚5S@@vܩ'Nl6gQtw5fVNL֭[ڳg>66VgϞƍUb"?X=?>L&j֬*Ul6ٳڽ{2226c^PLLEF^z~o߮dڵKCs9sh…qq쫯2S޽u5|``u5#z:tHskqmm׮WUPA=/I\r.\PΝsNX*U޽[ q{ʖKZ$T[h5j[/_ѣG )6""Bk֬QϞ=s(cuǏK.&I7VJJݫ .(--MӞ={|Ä W*99Y7n${]qdz̛7O&L(?fɓ ׼𬬫QN8={?:*]žLգiii֭ /=f5+W^ƹ[niɒ%zg!Chҥm߾]RfUUnN>~h[N#閛Kjذa~Ν5{l.^q,۷5K5w\=rwwɓ'ռys]xQM4ю;>~.]،%/=zƍոqc߿~ԩQYfipZfׯ}m'xH{zzŋ9=~ IDATd?(::xk߾}?rXDjjZjݻwKBBBh"u٢͛o?$oWܸq5!!;C];=͛:t#UREuԱ-*o}{wrS5OM0J;*(FpѣE;8#F ~񂃃?VZ===SZl:nwo)ի#]͚5K/SNr&;xƌc3Fϗtyv~kN|EmڵuQ{̙:{Μ9^x! O_|񅑄?xbuVIuڴiFBaÆdK#FPfԾ}{hŊׯYr%޺뮻Fwc.X G7ݻkȐ!Y;?/]ٶ]vjԨ#Vq#iƌJII$&{TjDo)$$D|I,W\Y[6*gӥK$)_x饗^2g̘#]Νկ_?ǯS6oެ]#G4wak8JIҥKZp+W3ժU+͜9؏)`ge[Ŕ:91Ud.(TƷJJX֖=zl3i$2׸tC=Zx._p 6L E[///?P r/99Y|?k֬>^}~7͘1C3f̐9s&MԤI5kLm۶Uݺu&%i׮]FdR6m슣}J*""RV">Cf{.##C6lкu|Q\JR[n52.< 6l6oc=VT!l+WI֞P0nnnd#3uկ_X7"ԑ*UZׯ׿om߾8wi>}Z_|qM6:u:wxO6N+lv6+==>gϞ5scڵցd|G*i:uخ]YAYٖ} gϢK>V+ĩ1J֭[;;@@e65ջwo[oc߿_JHHPFFj.]hϏ@V^b{͛75Exnk 7nϟoq^zj׮7n&MA աC`k瓟mYkxl˚Ar~J"KJJ]k9rخYѣGUn]yzz*((Hg~~'zwW_}U!!!;vxc|gW6m*dgMeG:}L&6+yteϾv'|bW%w5|ImڴH[uKRej>ϙ3g?{oȪ/ yg;wV?AAAv}qge[֔&eOjjbccWA(eR(V˖-Sdddmf̘alwUc˖-2ͫWlߤIuYk׮Ubb.\jժ罼ԳgO^Z4m4[.1Ϟ=˗B({ݏ>X[tÆ yAm۶kOJJJӧzjj˗/wޱk/=z9>WիW/3F7on:=yQZZ/J4hРq=z@;i3KժUmʶMu-Pd6}qܹ\'$$z/60XIe˖)66VR洶SL:ֿ/ [oeQ=3gTgCYվM4Iꫯjѹ~)99@1SǫW^9|WvHHHeOo)##î5}5f &M6V4VRRq#es>sƽ#cQ:~x-_\ZrZliW 1B .$=Z-Z|6nܨ%Kyi$wU^=G\x///u]lJ7iҤP<'p$݆c .h:tqlʔ)Vo4qD]xQV߾} \}wq=r=~iϞ=2Em㰰044f̘ xkN~m-*ߌ[j65o<_^RӶ-Z$YPոqcIHp^|Y {j[I۷k矷r=;wqƍFe-Eڵk5o<]rEԠA캮,f2fϟW_ZIKKŋcU'OK/d>>>>_UT.]t XHHBCC;v… ƹx@~wҤIjRhh|}}uIܹ3GUK?VʕsҀgu֩wޒ2YK||Œwy@U@IqM\0`]{W{Y,lShh#r=$KiӦi6=ڵgvoE){u7n\;)3:}tM4)زe~c?$$H#+;C~~~k6l7xusc6kWJRi[C _=8]Z$sssSO= 5sÆ uaM|XcǎUÆ #???}zguA_Ij۶ohhEŭ=|IUPA߯իԩ#yxxjժ{SO>Ӂl&̤Ĕ)St!M:U͛7Wʕk矵jժ\{MbŊ֭[diܸݫh"aÆ駟~+bw%kYkk;ԏ?իW[nQ.]護Ҿ}4b5m4G[+e&͛]v)**J5R@nZG͛sNiڴijѢT"yxxhyXT>#;vլY3nژ ?O>D]&M(**Jwւ J(J6oެkGQڵ-///ժUK?V^#GK.Ժu<_Ҁgӧ~*IF0 '*aU 7n;ʾc*K_r)))IUTqvHM=ԩS'EEE9;X:uJkז$9rD/(:TuT=J)sׯGO?U~ns9ըQa&k:8% @~o֬Y1G(HرcSO&\\vMSxxCl6Gg zd2o 2uKP R``6oެcǎ9;di&5jԈgQf|:z7oN:9;(HȕL"٬9s8;٬sj(fYSL^}UgwwwթSqw)XSO魷Ҳe4n85h!yJJJȑ# P,{=m߾]C ѽp,} uv@m!HÐ_|Q9=rwwWj4eU^ :ק~UV)===zT2:uT0\rzu}iܸq㏝RvA5J+W\RR~iy%r&7J4W;;0 -+빵k>}zqUb 2DgϞqŋZf H˗kz5~x=(CUJJ[;;(X Ν;+0006رc1ETD ,P'֊+:( ![=g2/@04ze24j(gפIӧ;;(HwU fd2_-kM4INZ~޺urܗ8;4% >\*T믿'N8;3#Ə9#!U7nTrrrmN8/RÆ + TRR6lؠ;llp5$8զMt5Yf%LVhrwwWΝj*Y0eXBbى>>Z5i$:t(ϾE^6l-]xݬ+j1%OM6G~|ݢE^zuv!_|ELW֌3d24emٲE.\Э[>HZhQ|'t]wiڽ{\[nӊUDD"9rBCC;vיZcǎUeݰa֭1c(..NIIIƽ۴iﯖ-[*66Vu֬Yc1F^͛Ƕlْ#Ie˖MԲeK_ҧ~ǏŋJMMu!ٳ7o{PTnjloi%KnݺO?˗/;3reuM:tЪU믿͛|~͞=[aaa?~RSSs(k֬)p裏 Է$Y5jH[)]ȨQt%%&&JE2::خX5kܹS'Nl3<:;k̕+W ׭[g}llΞ=7bŊEsʬ֭vaTQ|)ڵKfYС֮]={9 /DGFT^=]~]۷oWrrvڥCmnOuoo;aիJNNƍ%IyƺtR-ZP5˗/ѣJHHY95kؼE)>>^TreUR%_}K;ؾ;ԬY393rquEG1L&5nX)))ڻw.\4͛7O{ч~[S+W*66W_}e$1۵kիڷB z7o&LP,AAA:y򤼽jϳF:qSPK\p8777}wwwM2EǏWJ$Iiii֭ /=f5+W^ƹ[niɒ%zg*!Chҥnݺ}2fҠAk;~ bLjժtR 6ܹfϞpŋ5n8۪U+m۶j7kխ[WRf~:O}9BqFRSSժU+޽[Esm|M?^$~<.rv۷7OեKrM:t+J*S]myVyӧ{|eMA> =Zr)%5]7YQ?WSxsbD`c6(Y-fݷoBCC ywiqC#F0?TZ,{zz***J!!!ٳ222l2_۷/unΜ9F;7|Fh>Lmڴю;ٳgwԘ1c1chF/K@@= \fΜg̙3zr&_SNŋzï*IV+R_[l~,ȴiӌbÆ w)((ȢFf͚}JIIъ+ԯ_?u19.g*oY3\rc帷G>5ˈ,7ϝ;g ,ȑͮ{2d?k,ǶիW5{lcչ&xyyY$֭[k3f&{׬V:uh:pgFig6]tI 4qiٳg~,ȥKpBIbʕy&Zj3g111 Yٖ5CJJյ]#:54~<Tɔ5T٧2]tR洠ѣ6&Mo!IӥK(ݸq^*IPDD> TRR~W9rD 40'''>0g͚gW\rРA;d}l6СC:r䈎;3gڵk:~&99ieh]o^,Ei#""t=3j(͘1C׮]Ӗ-[tyq Yٖ}Z+W(00ЉW 2"\rZ1LjڴݫtرC;vtX YMJV9.OHڵ˨5LjӦ]co߾D$322a[NqqqJLLtvHVe%c 5gy19s:M6Y$Cvӧ[]C$+TK.Y$uQZ7--ͮvEQw^Ǯ~Yɒš1 eddX|YvuI_{XTmܸlwg>ƴ̞Vtնm[c{͚5~k޼1Ço>mڴЯ}kw5|Imڴ(d%._~zKm3>ϙ3g?{o\Od}̝;Wrss+򟠠 ³ٳ2PxfY.]2~rF [*[l"##l3c ck׮wh ?ƌ+WӊmӠATbE}gUqK={ի%IӦMӺuٳZ|y.&Gŋ=zl֭H!!!ڶm[---׻|r=y)H^4fݼyS׺u裏'&&ƨ^n޼4hP8}/.{J>X?wÆ zGl;i3KժUmʶu-PܹsV%$$ٗ/\ `X\\bcc&-[XISN2X׿/+ @oڴicW +VԴi4vXIҋ/jժ)**jׯkFb `I4ibccW_ѣs/55USrr]1KYI]իW&==]g϶kzqqqիW?sc-JYqoȐ}?^˗/WppV\-[O>D|$p~>#)((H#F… %IGV-&6nܨ%KyOꫯc]wU ʶp&M ! 2tS_pAǏСCcSL:7|'ŋ:|훯gyF?̩Knj={jΝ9ǫcǎ:qℤ̄Enr ?Oc?**JcƌQRRRڵkox%Y$.]|BBz!}v׷o_c^?oQ%z5w\؍7^Zh!)s\[]Vӕ+WtA 4r3p ~F ӬYվ}Siznj?|>>z衇$e& p?&,,L׹sgݸq8׷o_\Rʕ˵9sZKLLT``מ'x%N: ;f6mڤ\LOOW~{|}}մiS+!!Aq_W\$jJ۶mI,jժPɓڹsg&88XѪ\q~ذaI___+88XW^N奀/_^'O6Ԝ9st}~'#i۲eK,_4uT8KJJRPP$iݺukJɓK/YgiӦW^>#I|]t1 eV`*55U;vЅ s<>C9nQKIIQXX;&)Re˖ R||?n9r-Zd)xV[nz-)398+,,w T 7oʕ+5e=P{?w})44ԉ.dڴivϫk׮j۶ͶoEr,{rk7nE"+;OOOM>]&M5l٢ 1j*=3VfW^=[6lVLLbbbi)IWddprrzaҮv:yd,YÇ0a.\h:<<\>>>ظqc߿߸ jzgoذ>ɓ'_{ܢ^IZ~}uWoCrss믿~ПƔ%))I]QQQ3gŚ{vP׮]-ٹ驧… FYs_Vfr8! WEJ6E5RIRNlVVZU;v]weTf<8p>cǪaÆ񑟟n=:xĖ$mX744Ԣ6q8qB˖-SϞ=UV-yyy[uU޽ڕwssӔ)St!M:U͛7Wʕk矵j*Ji[yf]VIGIk.R2eJDD\~ 1СC/$${L3<3ڷof͊w}78i3gΌ޻AM3A]zgbѢEY%777:t~dZMoyĵh"ƏT*nᆤDĦQ7xc1"(i 1ԥT*Ǐ[Yy~7h޼yґS@ ¨Q{;3.ѣGyM%%%q$cK 1ԥ~8^~8CI:N_+(S˗ǵ^˗/3Έe]bn%0YO=TaÆrhѢE1lذشOhӦMR1f͚Ŕ)SK/4xIJ,\0FSNrl!fTRR_|q ff@jM<9nJM>=]vqUWWώ>O?4zt[n1z(tA1iҤ=ztu]qYg%c1v+3A]:s4̙[N:Nz_dDyIǀz#PZ Wۧ]vqQGS夓N?F:to ώK.$,XHx Əȵ3A]Of͊G}4k@JmߏƤFSC9dҤI1iҤcdo=֯_>:uTo wv[̚5+tn"F'P{?q5*#G /D=o'k,nּ/񩋋k 4H999q'g~xxۯ]STm YB K(d `,t gn>"Y*w MQ]" v(˯{qqqg` K MQn^DCʷh|P ^7K:l]U#|ʌG 3L %0@04AR""䋍1O IDAT{^7˩:ZS&hc*b'""9"bmUwMRۘ a h, %0@PyIuoGA^N䍺 @)@saNsn6aNP{ YLkΝѤ?H:Bvm|n_pADAA`h K.I:f h,a0˵/^\OqqqJFaԨQ[J! 4\ RXreݺuI0 MڵkoNǍ & r@f(d `,a `n\{Ȑ! %KF\8 Ѐ K(d `,a `Z8p`+ %&(///\ @[h_~I L %0@04AׯٳgGqD4oŻC S@d `,a hZf*Xt X4[nP0/ L ؂WN]ztf0@P Y@,pyv\\fh j۳\ :ǦR &h jJe( @c h, %L MPAAAuY4~ D֭@ K 4(sέv) @ 4(;wvN) @ Mڵkcԩ駟 &  JR矗k)nfIhժUݩS(,,L0t2`ZW J:چkcohgt0 K(d S@42_|¶ŋWV\\\aE9 ۺ/JC!14@P @URRO?t1zteqk֬I: dha#'? oyߏo~1wܤ誫KƑGWN:dܼ#ҏܼIG wމ;,>꫓ԣw1f̘K.CڵkШ%`K+WCF.]⮻J:#.(Lt$gć*lH44 @q8bɒ%OF榢qǤIbq'' Aq14`L2%f̘?ϣ{I6a„h۶m=:>Ӥ@Im/{?Hmܐt$2@h>O~1rȤ @֭+ꪫYgㆲXcㆲ#ASFcf l-[Ʃ֭I&%SA8qb?O>9路oܸqϕ[mt 7DYYYvinoDȑ#cq''\n_*J(Ubw>}իC5:3#"/"cF "Fӷo߸{cڴiӟtտhժU>qwd(eݨf͚{#"OO6L-o>+MӦMK:Nֹ/^{-ΝtQWA۷UVc=d#`ĉcʕG>}U-o&̜93VZEEEѷoߤm9B=T^4hPDD""? xCݮ-_}+(~lUVUA}FnnnsI?裫ܴl2}1 j}'6kMVfѻwmz{}]gyґG>`\}Q\\Ǐ{.>X~},_<~ƀbt38#w߸c޼yrX~},]4M SN9%JKKҪU?axq뭷… cձzx㗿eqw_W7lw_}˖-^z)~O?oYYY̘1n.f_~e\pѫWW^I?KJJc3&w=V7w޽{os3g7/O=TO?4֭[Wqw1<|uwD׮]c„ /Ɗ+EDX" Gyd{ڵkcŊkĉw1vXn]wڵkm{*A ;=PWx+((;FDoQ4Nȑ#_W,_<"6}ل [l}vū~RK#=ܳF:ujyO>~xM}Q<Ѳe:?4hPmmڴ޽{Gvb޼yxXfM[3bĈbٲeOFĶ??CƍcqҥKr! '{_||I_ѪU8#???FUy7i!CW^IokժUzD%Ko[RX|yy1}8c< ""bw6mlO_:6lؐnpѱc(((+VĢEbi⡇6S3&|]w}FYYY""wIo+..={Fiii'DYYYtM?;CT.,?/޸qcNӦM+Co;vmJi?qMoGtMqe𢢢FVرcoMEN*yQI)͍رcUVi4!Ck^veq;v[n%=w_~yz4g3mq8p`̞=;"" EwyѢETx衇sΉիW=^k.^8vo?9sDqqq{U[jUEiii~0`@M_qghug5.**z+ڶm[yO>d3|qBvO?P@_&LHߗv!ϟu᫟]v%+N>4B}~""֭[|p̛7/""u'OAswرc/qV{-5kDaaajxꩧb5:nk֮]߫,pfR6mbwQ_UN>xk|K]xixLv$mܘ>"ˎS?U@X`AYAϟzJ0Q1zbذa矟./:t?|tܹ͛ѣ[nq1ƍ;N;-?ǖtE.?r}rrrbذaѦMeeeP'xb̙3'/^/RtAoNψQnHwuxgcЯ[nOǡJ,_<&NYy7ԫ%5\sM|GѳvX<3_~~{2usU_ً~.(^?)Ϗ??ҸSOftE]eee<ݾ+'4hР8S3N8͇~~Ve]bmyL81~+-n_#Tw:ȵxwYf[oUϓ\pA4umĉޯElϿկ"bӽ:uj>8tx&*絪JKK\S\SGqqB:sܷy:MwСC+H?={v8 s΍?0""ڵk]tV92vnU3W\fr^m9|&Fd#S@7@[{)_]2ű>o6lW^y%:ꨌ\#??ԤO}< /Bׯ#T*sIxl @y Pm̈́ݻoQ }Q{֗N8!.ظqc< n߾}ߺ5uy|,X ,X稪 6l 6:c„ [o A]}"",Y~ޥKeߥKf4ZUo˙/vqln'Ǫݒ~ &͛'jДJmL: ?g}u{8ѩS8+ݷ-әoK/N:\w=Gqnj3ߎ>,f͚Ukԗ}2jr˾[^ꕖ704v7ne˖[@*qʖ_Wg˵Z;uꔱw!w߭1ׯ3OM#KgMjպ[ƎTjK,JϽN;EDߟ[9sfL4)>㥗^E]w]vY 2$z[N0QVVV~uًرcQ5e߯}kT6nXPyzTmH4$ MO?>oVzz͛ǁz3f̨hǙ3gfp 'Gnyk|N$[]##"z行.\Zb6?яb̙}f^DF|K/ۇrvˑeK.hݺu717xcFNNN?j4kUWG%Xl9~cPn"6lX}>:ZnvaѾ}())Kƃ>vZWX<@Ʈ 3gN<#1iҤXbEzjᤦ{qE]+WKu]W]uV={vyѲex駣st-^xZY駟:tQ~]wqWmED{qEڵk7ߌGy$~T{u]G=;ǢEju\^޿SUhs{8zѣGQGU/Sm6 kUͿ+vZ|PuL&b1mڴ*y͚5We.^{ܹskt袋.,{*uYQRRRsק@%~ߤ /IMQe˖,ݾk[nիWYgK,7|3^~Jspޚw5>رccw={Vf̘QHm6WQQQ^X';#ݞ0aBo޼yg}Vi 6ĉk-El7.>xSNqnرѳgψT߿܊+OԧmiOyvo}[IF{%#b԰]tQs1ꫯV`8ꨣߏM!CTz8 "6Sܝr)v['?)7*xz+7xczۚ5k\tqM7ʕ+c…qgnSʌ1k_"]4㏣.++o=N:h:+9jϽ_/^8mʼwI&[6JM7>hDDı[4^UVEDi(/'eND5\˗/Mknv'Fnnnk.СC\wu1/'ԟ0aBNTz޽{Gcٲeǚ5kN9唘:ujSp q嗗۶|h߾}~7|3 qz{={5kܹs٬]vqGFD>W^ye/bL<UݯrsscU. IDAT(+ӿ3gNo>VXeee1jԨ[k|[W7"bݺuqg{FDѫWh޼y19sf 0Ҽ%%%QTTXpaKGvbv+2]P+?y ,ئU}gc=jD M#0{֭W^y%>#8"q=W\QnhΝW^ѢEW_0}:Ą bwѻwxw#bHo|QTT ,\pALk4]=9u }{G^L}F'?>:?ˍ ܲuǥ^Zi/bӴW]uU\q^+7ҵ[n/kߎÇ_*^1rȸ+K/)yax:j׮]B~1rr?5ڬ.\rIm{=ܳ^{oW^ye\{7e]կ:O>QXX/B}={7|3""} k~5yyyaÆɉ)S_ݟLTt6lXix*777F7pC5]_СC:jK.T;;cz8DF_WQתqG߾}+}>(m!9 @S\[Fh[5۶m8ꨣb}SNVĈoǘ1cbqc/<.\TD_}k.zꕞy[ǃ>C ;F~~~o>sO̟???g}*߽{rFDwqֶ6l6sgYfg}6#6o]- ><;cΝ;G~~~D׮]OiӦ… kt_6OY?i}*777nG;va(((G~ /YfūgώgqD6m"///bĈs1f̘hݺu7_S#F1bDs9e"WDDc1cƌ6lXt%E=F͋oFňw1f͚ӧO?>t;w;.|xwbfׯ_ٳgqu;sh"w{nq-dM7kUO=TD{-v*gpr5k04PqAEf͢$ڴitrѣG' ’%KK.;Da0`Hβe*l۷om~\j6dxѽ{XhQ;9䐤T~E^PpY|y\{| 8͍]v%Ə[ j͚5)Sa^zi<eYpa92N BIII\|ű{6OFt X7n,EEEk N8L<9nJM>=]vqUWW,:(&MG+:Dr ><Ǝ~Uj. sNƜ9suIǁnݺ;qEaaa,2hР}@&0淽&zot 2ae Fd # Z~}̘1#w͛7O0 @VYbcQ\\tmÆ 1o޼t{ YdZ]vM:$B K(d `, %0Y彋w{/@&ɍ(.N:$`,a04QIG j[;'-?H:BRXXƍK:f h, %0cIhTRTT*Hd@^`{5*"Bs]6&Nn7. L@& %0@P YBFn{/8`(..N:$N K(d4F^z%`h4~&`50@P&,?7#/?;@TVV>lݯ_?`,` h, %0@P YB K(d4F{xsNIǀZ[4,,~oG~AaȤ=@BF@< qA604A999|?ZRmSq!Ph.zx+ٖJU, YB KukKcwCN504Q֭M:f h,a0@#uU;sPhֹŻs04@P Y0(U:Pk.i,>`^'Zj ztF`, %L MPwrm-py;v\\aSqqqfh j/|1^(,,L0@ԩSYB K(d kYaɒ%n/++$C zhR:L`hc4~ FQQQ1Ȱܤ YB KXk?ntIQPP`"7wJwС$GT*%%%]Ν+ީSzNS@d `,a hZmL'IVZZZJ;u & jG_޽\ovbfC9Ӝ+#"E*9%CDPSĜȪO"Q>QNC6g;vxr~^x;a#{O.I%\ >!` hp$AȈp.Ӷ'Ndo`E)3m9%s=T! CNNXŢ G@~Mݺu3:dҤI￵b ,Yp*6I'O<;w B4sLEGGSN3:;$9GU6mԺuk͚5p"OOOmڴIٳ `EիWճgOU^]K.5:Uhh~G;p0"#--MC ӧ!WWWC`kܸq?:t蠁pLiiq鼹\B%99|(u{Umle@$`H(2,YM6iS0XppBCCN:;"݇-d_E+_G6:ENX͜9p.F/Uti999eǏM`C2Wzʖ3: R2p+a`D!FGπP#@1Df*Y2_t IRZZZѣGnݺ駟Ttw;ggF(kk+sUJJ bw%iѺxbbb$IV23LF0luزz)}'4{lyzzZmp4$Qhׯ>|9IYPqܹS]tJڷo_~Y-R5xbumm͛Zl$>>> `?Ӱ[SVX}W2dGC'(W[x}oۤfͺ~|}}ռysATi-Y]v˵j*(rNd֎640CK:t '''PTkN˗/w}$`VuYb;KH0dҷ~+Ij۶(gׯ￿@(666O;v@ltֲsN/}WgVv#777UTI{?68ǫy*W\MgbbZjeSׯWddz)yxxuZ\\^u5mTXyf_lٲ\\\4}t5Jq?132ehڵ9gy{{gۆLX &d~~~ڷonddʗ/e]777͟??s=={7nГO>ݻ+""B.\Pbbvޭnݺiɒ%jҤOƍx/^#G&&&Zl۾}_n-55U!!!V|wjժ|'qOtRSSS)'_~e6::Z?7ʕ+YMNNVDDDvi~ݨQ<ŷyflR"##?ŋJJJR\\>S^ԢE x ŋUfM_~˗ ˗/{ԩ/_Ǐ+11Q/_~[5ĉegڞUjUIҟs@q0S@=Z.]RLL[?N 6.S7o.oooݻW'Odҋ/N:^zj3,,J*j֬~s+WٳP2e>5j{Ow_7n(..NVZ)%%Esϙ\6mY׮]ӪU4{lժU'M]vM_ZZUgniʕI|P+WҥK륗^ʱÇԩS:w9Ix?$44TJMMUdd]rʩk׮Zl|6oެׯ[:uƎ9<(I*Wʗ/>SsV*www]|YǎӉ'$)ܡCZJz(Ǜ_WTI͛7WJJ!}?[n:zy6l矺pRRR4o<߿_6lPR,ڱw?͛7O&M*⾾:usU}(QժUuI߿?_AmjZ`LFύj#NN?`9;;kڴi8qʖ-+IJIIQS%O4IsuUVՇ~>}%''kŚ˗TRZ~+IZdFu{MyxxdYĉY֏*Uҿ2|pHEY$}H###խ[|kºqmGENR*UK0a{ٳ"""԰aC:t(O1=Ĺ.^)yc+**JTT)8pwr{VXQ|(g[/w$%%%u橻kժ k׮u>3M8Q7nܐ$ 6L˖-˱m[W3JUF\յU_k?e4p@{7qR_庮k 00"0#/РA,3'wؑTY~~~]]]ZjW^JKKSHH }dg֬YС[=z*URhhydX⢔e0NQn7j(}GJIIW^y%zk׮5ʽǏKR#sٳ:s^{,GζiF[nU.]7nhѢEzw|ՠAݷ3f0'ׯQusϩyj߾K.VhjҤadB_e/}W-__LȯzGlaɠAMFթSi~s`Lɺza[o]>Ŷ#4~i䘻UDG)+7V֭%I!!!>>lZtޱ3q5m6>|b:9;;7< ~Lߌ o/]_ҭcuzٳg[=&dYɒ%%I ٮQ ߷t[+ 8dZ5%fnuN=h& IDATj~eNvY[-,އ$u MЋt?e'stqm۶-gjnMӚTye2t}Z`&O{NK,t’Sr:vÆ i;t萫ѣ͉۷V Yrx S,̊7n,IJMMիh?W 8AHHHk֬1韥%cWZZ֭[rjذzkܹZdyw#ݱvΝ׹y@Hǎ%J ۢrVD _RJV8nKJI5:$XAX8?ZK:uJΜ9csiFWxq?eSŋW_… Xyxx$aÆ^WEuj Çu|Qb˾=}u^X7::ڪ1!kU2i`$Qy2: -.\rz( qPFnT ?yI gΜ1\_ Ln„ z,kO?9shӦM믿tʕ,.jlٷ˨Ɍu.pW9KHH0. `(HSsrQjժ*CZZRRRCO-Z=#IZtyủ$K.-)?͛5|sݻu1-]T&MRUn]yyyYL{ٲoVj~>872ֽ뮻-t~*H?IPΧou>J\]]u:,CjO////[p~J|AٳGvZIRU^\|rb/ͯ~im޼~r6㔀ׯ1gΜݻq"?{OrrroF*jٳg%Z7b-qY̯~ayyy:,C;v,ndƍkm~W۷oĖ{'*>>^˖-~'Iy+IjҮ]fŋͯ{yyjߚ.]G}4:>}hرJLLԡCf?cfϞm٢E խ[qV頠 uܹPbPB, }U)5k+Fs2m;qDm<0 |ʕ+Mxhʕ%Jhڴiٶ;hΜ9&1畫9qvU\9STۅZ2NzA'SVZWNy?Z2e4h -]T+WTƍ*''' 80Omծ][*--Mι 8ֵ˪3ڲeK8q.]*U(,,L-[mڴIׯ6 o\zK^xs=&"""xbs9888m]aϴ4ilŖ}eQzG8>@D a xgyfm.\ĉJҴiӲ~Д)StE_zNj̔~~~ן~i'NP޽fXwC_ϟ?bmLɤy晧,vrrcX~*,F$]t^vԨQ#ߒnlٲ|}}uA??.\m[.\itY^ӌy?ֵͨiӦڿ`7.K>5k֨_~>vԨQI%KYfR]#Gd;UT)W_}UoEiav}۸qcծ];_ֈȑ#֭N|/?^ח<==մiSM|4|p=3VF\ҭeTT6mڤAzrssSɒ%uw+((Hӂ r]k}퉵)22ṚYs!͛7ofJyaO?dIQ9ٳgZj%J(66V˗fֳgOuEAAAFbӧUzuIѣGUNC`0MRR֭[g.WnnnF 7 1r7 n>Z%yqN::v"##nsΩjժFIQ UVI:udXpDnnny@ѐt͛77 @QE@i͚5n%d 5f-[H%_YfFb؂dҒ%K$I'O68+rXݍ7h֭ӹs$I=\SOG۶m]ejРA⵨֭[u1hB]t1:(H͛7ղeKuI޽{%Iz$IS^ |%Kjڴi2L;wn۳ɤ{OÇ7: E5.L&M&'''}GFv)d:|$SW^եK4d9rD[o*3f>sh„ [Uͯ+W*66V?qܮBxx~W9RwdҴiSo&IW^5upbbbo(&&&Ӿ'|RΪXMʕ+!쉽OիWtU͞=[\"IzgԷo_DZdڴi &믿Zyu=Zaaarww7,ո[ոqT^"33h`syʔ)00"X ,\P~aV^m~3gVXSv?UTI#FЧ~dEEEuY٪U+͟?_AAAZtFasСC5qD=#?;E5.y%$$?'Kވ20}4ñpv*x{{s΅=O .k*UC͚5G}H,Y&|4rH:xMq'_|MfȹsRTmڶm֮]Fr^ ?~rqqѬY4k֬B=EnݺVj E5TT>m۶)00p0@?\ Ҙ1c@![vEyw-V'N0: pP$999iΜ9Fm`ƫfxL&# `p$AA `p.Fu|V+...֭E_y`(슋V H` h|8Pu0: d4s^_5.@$b(99YQQQrǎj`D@1]v۷' X48q0CC~j֬it!H  08 H  r|\i?~0C`M% `p!͘10`e`|Zu3: dпC `p$A04^LL!؝)Sa`DF3"08 H  08{!E `=___C Hp1:{ԥKo%JP``E0P [nF+c hp$A@1_\nٲ\\{/<@1o\n֬ ` H  08 H  08 \m9tVKoI :xrs00"ro@FY$b]=?mQ# CNNN,et2` 1gOމ ;/`4}" "ᅞmsW/uL;'GD(ursKrus70"X `2L~͢ lt  08@sUGj6_$P$嗫z5l ` `pL ])=0|KP8g.s'w7# 99`08FŐԬfQ# Cn0'\J,5@Ssnd2Y!X `pL C:z\F(Q` $b(9%U+#2K048F9equ-@+# bǿ*J148 H` `|t#yy7: ts9=00"jժ0ИL&d2 srrV@`Sɺz^dP999U^^^򒫫!!Hr;IIIc+DIbbΟ?xC܁dRRRbcc+OOOUTIF<" Cnnn0`Eov}SN@SNߟv0P 9;;0RSSui`RRRtiըQC%J0: `U;wNۜUlY-[Vrvv6(:Ҕk׮ڵkJKK3KLLԹsTZ5#D^XMjj_nM5jԐ ?I@Q&OOOɓJJJ2~RSSl'x `5e''';"???999YlE$}Wr>}f`Dp-U;RJY$}TlYBn4EGG[^ܹuTRr2e J.) @??;֩VZ!DdRjjŶ%K J*eQNMMd454XEV3˔(Q€H730L1R SVY}ϣa h|\2SK®%$$lٲrja`DF`0@>LXv]2: RudIse?2 )F  ) \N8>a%'yxy[k9%L& G:sZnmt(0@JJo߮@,YpX `*{Bd2{nEDD(""BӰaHѣGtRmٲEOV\\*U@=j߾!q:uJ۶m֭[uV]|YǏg0H2esܹsm+ܹsm_r o}Zp4i'O꣏>RHHTSN)<<\=VXQ1}]jԨ#FhժU|rPdo1: hҤIzGuMC/{M4I..ZFGG_~JNNU[Æ Ӗ-[f͚H 6j%ɓ'%WWWqXzTre}jӦ飨(+W\/22P9r9[ 6,s(|$I'O<;w B4sLEGGSN3:*WkΜ9onǎzrՖtɓ'kݪP$I|f̘nݺ(pss:tƍCSPPL&:w,g`jժkתJ*VƌS*UJUZFޜ;wH(r=6mڨu֚5k(DڴiճgO%&&LiiJzgJK3:$ G;vԸq2mOhѢ,P4lٲEӦM3: 8;WsUSիWҥKy IDAT|}};jرZd!)-5Y\n}J8pں*}lU(YdǍFm۶n+}܀lܸDF[qCGF@@!Cӊ`-k߾ƍC8p!Xg*/@}:t萹__H_5㺬@A|XqqqY q܆dRddBCCg={V+VT޽믫B yz&I o7nrj׮y\l2n:YFΟ?/WWWUZU]tȑ#͛7~zjڴij۶n޼9shٲeу>˗g{skZv̙3>W.]4|pծ];c-[z*}3g@O~kٲeڰa:XyyyVZٳ}YUX@JrrVXui߾}QRTzuu]F6֬Ypu]SL$ZtmۦӧOM͛7׫zȢ˗/+,,L믿 h̘11bDKAaRR~mذAڱcU&ɤ+$$D{իWUn] >\cǎeJ.&&Fo ߿Ӛ6m}Y1LZjBCCw^]xQu:#as~mS_,_\DԩS jt8y=N2e4x`%%%iF(b*W)SbԩSe˖|;l0mݺU;wα^ٲe~EDD}9֍WXX{1gϞ 3ߪ~8q~G͛:t^~e 8М믿CG Ǖ3gh…jӦ^{MO Ҷmte%&&ɓ 3'SRRrl篿R``^{5uMׯWTT^~eEGGkj֬f͚ؤ$EDDhذaT [*55UqqqܹuqkrowquQڵSj֭ںu 7P 4n8%%%ejGÇ3bÇ[o;^IڴiׯM6)((HT{nM6M 4_]n׮]jܸ/^C믿֭[5j(:tHo駟f:ѣz7ujժ ەMN:_~畐?S'N3W|yEDDú뮻cq|W%&&gϞzu}_?hҤIQ͍=ZAAA8phd2Y|Tfg3[]۹s*%%EC ѿR i`~J>S||Uؿƍkʕ*Q3ڤiРA:zhQΝ_)zjܸze+]pAw}wu.]GyDm۶_|~Mwc=fa\R?Un]EFFھ}n{;~Æ %JWT)S 2$6߯x@%JО={4l05mT:u9`M>Çkƍotuqo/^{ֽޫNcǎ?{L͚5Sj߾}QRRRCqIjР/+ZkРTx[Mz~iٳ7n:h…z$ݺ&+EEEcǎ 5`5iDm۶[oŋKèQgY!+WN*TPZZy͛7իW/iҤIڲe8<8--M>]qiʔ)z饗c@Iիo>mܸQ͛77Yz-suu58nƌDYfz5gmذAΤ;؜>\R+WBCCu ڹsW޼y<OZj{⸁_VTTﯦMgϞ'|y"--M!! ڵgԭ[l&GA'SD_lqm7oެׯ( i`~rvvV׮]%IVjNUPPƌcСCNe`J֝d\S6%%E~,|hرcf`qY?w֚5k2\vm}嗙]ӧOϴocǎcǎ[nzG̯ҴtLu6lhQՒ%K׷{:vN:&Mnرi'Ncկ_߼N$mܸ1(*))I#F2 +ݚ]vsiӦwĉtZ޽| X"S=///dhv'dɒ{ェW|mܸtժU~}YikXreᆱcfW|y\޵kW7p.\ C.ٙ0aB`0uЁ V?,@j.뿚5{[n6nܨ`"I/??lޞSNuoرcw% "1{yYѺux;|=XLun6O?Ֆ;p6l`.wnj=g϶J,c/s}sɓZfUϛoUcrʩbŊxgu[~?( :>>^/_wtOzI@J B/B("AQ (]z |EPҥ" RD,T.kIGNM݄Lyܙ;3ޙl>sMi2G_tiUPy2bѯ*ISG}mۦڵk?.^h>>ʕ+Tn]M1<Ν;ӽbfRJjѢJ<]ԢfqHpZr *dX,ڵvmbdj>'k,oذX5jvؑk+<<.jƍr\j{1MjժTvΝV;~x>KyS7=2z'wϛՆɱi'OW&MԼys=z 4=d{|7*[Ǝd^/jŊ駟4c c髯JX:}/^ŋ}?sDdիջwo:u*'Oɓ'?~/S{3&ѿyU͝;7rJ> >\wܱZok;.Y$ɽ[niŊjݺum˖-K]vi֭*]zꥰ$uN8O?T=iիWYfT ݻwO5oc9RU=GmB6m2+Us8 Aq{z:u5kXիWsNM8Qƍg!#?+2.Z9uRѢEulu +rsuUHP+#<˗nݺ|oV-uVn:F,YRy%I/_6J\V^]E^#Gĉz֯__K,Qf͒7;wyVͫjժ)]͛7f̘$쨶M(IyꡇJ~BlC{z{ܞ;FYL RݻwҥKɓk.-_\r:#?+2㾋7yd <8SzJYSB ]veh@kj߾cǎm۶Zve/...*VN z9Mt~SGu떱a$:=eVfgz/Zl 6SN:t萤#ƍӂ 4}t(qp@@On~- 0b 4&M_g飏>E-Zn߿_| (z7g=Kѣ?^tQFDI>}Z-[ԶmR}ڵkg `k;6k,IBnݺFoCQF)$$DO RJiҤIzOժU˗UBd6No$qF9RኌTgϞdo˗/Yf$=0~xo>I5kj֭NQmбcnj8Qmr˽{Ժuk#Yf瞳3o< 4HwQXX/XȌ.^~԰aC z䯽\%g.޾}M6ȑ#  L۶mu!yi5x`URȲ&;7~&IF*a'ٳz/wVM'wwwݻwOR\֪U+/_״ՆVF ܹS&M҄ Ξ=_|Qz̙3fr0$_}VջwocH"ڼy+fCWpp5kX͝;W:uRݺu}$? fɒ%SW?00Pcǎճ>+IFlyU\9ݎO~áx:r8*U$;￯g̙3l'xB}YΝ;={>ڵ-[UhdN:U/]f$~2%)ŞqT{=رCRܽ* zU#-X@;v4>;*WlvIpRYHҮVox~R&1b:d5ԩSհaC~߿o,'8pI#6;v)7R[fɗ/8<<Ib,jnj֪U8o޼U|||GQXX߯;xWWW(?)?zjIqɡ1$ծ][ԅ Լy9ҕ+W$)C; !+W\Ѵi$ŵ… SM|=z1cǵJ]gjdd&xݏvs& JhܹSպ0ɞ.]d,'|=a2ȑ#F+[\R]vO6Hb6wcSO<=JU]di6l>>oxx˔)k[ne:5d?9"{e堠 I<?~uy=ZuU޼yUX1=s8pK?~\{x^dF;WŊU~>Nll{uQ VլY3 8P'NԜ9sl~b.jLl59lCG Me[^\rAŵJ]Jl$(= IDATxH^[ϟ_VR޼yӵ_N N޽{ʶ$˲x?G644T˗7PǎSM9rD 6Tj38.D)W_}(}ZO/b]}dݎe!.]#GjvHܿTB_@nC39?uꔱ\xqKXv Z.H57PjvnłȰ(s`Zj*3ٲel25jjN^zIyյk$uII,K'RS.qm镜V:22R&L$|*q54f=.\М9s4ejI8ic*,,L$ >>>| d>}vޭ#GN:rucxIM>]fR5~dϛLKx5cr}„0=.ZqOn۶M7~nnݺ0h¨?n8u!H˗O+V9^8L񔗫ڴi[… ooiӦ 6>e5RhY-֌ ЛomZ%.K,W^yE}VZCڵk$a/,>?݆͑xcdº̷9VK|VH2ם;wQ֮]׬Yl+E?fΜ۷?裏K.VoV_|oߞ$p‡K-pĉ%(R@]Rp=uURMJR֭ sN SPPͫ… kȑԒ%KTt4_93O+V0?z^zd{%Iz;3qonU޼yb?~Ufc=&(QBrRŊ矫xSLj'->؀ԤIu֭+???(QB ɓ+W.VWK.ŋF9{>K.iƖޞmx9rBǎ*4ߓO>i,GDD襗^UV'Lկ_dW޲e1|`(p>9#DFF4oi!AGll9%JCjӦڴicԹu/-ZH_"##5c b2hXZ= d w՚2eQnܸF\&s~aP=zСCGYg_^{؞;wnM6Mݻwof֒%KeΝ;:p֮]k )W^IC@ JOryxxzuUٲeUJ{}A;wKR0׃e`ܬhӧO+((Hym:% +Zz]S„7|˨m/6t ?شϙ3gg=INF~'M$WWW8`SެpuJ(~'t)Ν;ĉ)pϯի[Mu'+Wst{VCfZdz)GʕӐ!Cw^u]R]}UXX^5khȐ!ʕ+~ժUK#GLkVÆ ոqcIqGW\Y-ZHһڵk۷WGԩS'3׮]S>}[}PhQڹsƎj۶BCCնm[^Z=fWt„ ڱc_zմiS-X@{I˗Uzu[j„ WռPB駟W^T믿رcuرdR\2FҥS&{xWZ>$OOO-X@7nT=TT)Ν[zGԦM}ڲeK7lxFhX4tPլYS|$O>Q͚55hР$_^M65z$8p@Ӈ~?S͚5K{ARPPPEŋ:y*]tSLQҥ5i$E/ڵk[ VVqFNۼy:wU~={^xm޼YR/v2ܹsվ}T=Xnܸc8<ٿu-*Ts=KŋtR1Ǒ#G2+ *Kg+!$~Gk7m4Eҹsgɣ>L=zPժUSgϞ=@KvȕݶڲeK,uT:#jѢ^uEEEi߾}dIΘ1cժUKްEF?+3ݿ=3Mx˗/MjfkծSB%JPdWJVJ߿cnU۶mնmT|)n֭[ LԆNMʕUre 6,C'RJ>}z)PiԩSGuоZ9Ghܸ@z̘1C3fyk׮ڵk{#FЈ#l{&m?}5k&?+^A>3w\c[Z&LPTlYc\xfO"X/p)a@=WZYix PM4in߾mUʕ+Сo.)n^߶sDGGk:GҥK5yd]~]/ls\)=q7H]pAuM2|)1chϞ=}1A7o.)#GZMiJ*TR:wvڥw:t蠅 -}'N!CXx ,hN)`,YRs˗u OuhaÆY+V*V( 5 )#G&>}Q,Y2CPdd*UGJUYF h޽:vQO>5kVcرCӦMaݺuF>*\>yСCmgϞV H"q8Ȟʟ?rʥÇ'In۶M͛7+h˗/k֭Vmܹ<%G&Iq$n:9 cZ߻woz:G\PFtIc]pp*V{ѥKm 4˕;wTϊxG\~;iFR\b<#3ݻW*U2_u"STT.\htbp2_9l$d3:|պҥK[M{={bŊtLXs{ァQFYwQƍm޼yVIٳg7ߴJ$QF9wSO`s[TP!Ν -[V~ysm5m4`ŋO>DzZhΜ9z7ۿ7nl}zz>h&VlY:tHÇ|`sje˖%s5gAZq#.)h߾P8qMY*_zK}V7] `8(&۵kGȢHC?~< m⇬Mi8 [j*ONR%IVRL;w.mUVMq[PP uAߺLcTR:r֬Y;sΩhѢfDV p!j6lhZ3)R$m$y9XJ sI͛ڳgBBBJV pb̭>d0ӽ*XtQ1^ZʕrCfոG_tUVM>Nb(**x13 s`hш#ohĉ={!bhҤI֭١Xɪq`X4bhƌfD*UZddƏo*ooo#=@Я_?hܹI&7ŋ>}~mۦݻ+44p [p*.^>@/^Lk׮ruuU4b.\؄9s'Лo~ɴX8}j…2-IJj\#DDD7P2e4qDIVVJwqwQ&{ Yf?NvҥKkԨQj֬)Sս{wSܹ ^x$8«Hʓ'@A8=f͚ϟ_H9Ac1((ȼ8`  N08 r/)k^,'X0 o}QFy&F,ݻgvNS...f" wVXavNYf2; Hs0]HN<)///ۼy!8PC4riݏ\9IqGj:wbccUHtf  R<<<#۹viviڲe  6$nݺ9s*Tɑl$<<|X۷oիW+I}:u{5G" ]tI=zиqd[mwqqQǎ駟;uev2 `lh„ ҥj۫@Fy޽ x vhծ]bŊ4뺸TRt$0 ` r> _2ebbDpuuڵkm cjժ @Aȁ\\\a´X'NHBCCjn@9ŋu/4;F` _a8_|p gϞU\9-]TJ2;$F :tH/n߾+WRJf rHM:(0@&F7oj֬YzumIR5Խ{w}G79BR.^'NFi߮]ԻwojСFWbbb駟F:uꔉQp4z Kׯ$b ˭[TZ5.]ZgΜ͛7+66֨sakN6l+gD Ԯ][k׶Zw)/u7o֏?VZer2v8bŊi@~~~I^|YZRdd,X5k1B`Νf;:s挮^*Iӵl2 3 6L7nٳg|`*QV\@Dȁ߿(+W0dC 6; Y `5kfvN E$p2...2;  `p$I'A `p$I'nv2zmU@G ;}QfSfG@T{l!I'0E 8ϒf ;D8@t^~[(kF^&Fȉ^kZ':6bI;I $b2?'A `p$Ir?O3!y).fG2!,k͛7Osծ];ʟ?!N0***J[bٳUlYIڵkղeK;V/V&ML5n8ݿ_+W4sϩGyڵkϛ% ySE[fdX9k}Zݻw׼yթSGnݺQ{6)J9ȁ\]]T4x]tIњ?._lXuE` PX,ݼy0\\x6=1cƨuʟ?նSN*TC'sM0ܹsz"mCСCݶ~zIR3dfXS" ęiW}"*AAA Y˗5sL^ZGэ7'O=z+qy-[L;w<<<=s@Qv9bq 3l޼Fhh!-Zhܹjݺu]bvׯAOcǎiׯN5k֨dɒj׮|}}_@]2ŋΝ;>}{=EEE{>5k֨UVK/sתUKmڴQ͵fխ[W񇂃m:U^=ѣ*P,Y~:@H9нuFyhV41""ww;vF,]K.i߾}_l2?>CNjP.]|iƌF7^pAAA:}^z%cߴɓGҥKդI=?>ݙ_ -X@/lŊթS'-X@.]Jc{=]|YԵkW&[Vfʹl2mذA~ڶmsm۶:q~mըQCyt 1@6ռys۷OgΜQddo߮'*w>իW5o<ܽ{T'%i>_Ν;E.\Pn2|qHdS=ʗ/~X)Iʛ7*UjPcyӦM2tއ~k޽:8  OҮT9@f,WR%S``N>-ŢY={LRoӦMj۶rʥK&{B ĉ}bŊ#@G9FP*9<]veʔiҥKӒm۶%0aΜ9#I/4eʔ$uܹc,˗/]q9\ll=jmM&sdKJ(Ν;'nX$Vjs" Ý_N @j֬UK,::::AAA sUն>ŪpʕKM4Q&M r 777*U0fk׶۱I =ymu}= ˕+WwUx$r8 w]cEsWH20@#Xcccmիrm;00ݻwOk֬1ʍ7|OXb0qM%L)S!H@GZ9*T;wJ._l~gΜ1IY `mڴɦzEqp$'0H{tA\R%v$@WX1:8U^=͚5Kk׮4߿_F~ 혉z'I:tAYz\\9UZա `G/Q^dI,X`, @$魷ޒ{ 'Oֵkגn:cG}T=zȴ92p1l-22RF900P&Fݽ{(߼y}˕+1c˗/sO%KIc \lbv@܏ҁO_l{C8hCȖzݻwO{Qdd.]ŋ:Æ @)00P No,F … ۷ *]v?ѣGUhQ-_\˗Ϭ $vavpgϪzֹ}fϞٳgF{/}~mnZgVxx:wH˗O*Tk={*W\z+0@6$c*UJ&Mrȱ8 ` GrG._2 sݾt.m'NHq[PP8PCd"@2ϴ5; VmCSQ=|88???͝;0!@Hd\\\ov$""B?aDV˖-u]Cl0,ɓz'i&CFӧOaÆuNbޝƋ$ `YÇOѣ@&ʝ;VZӧOiӦ2;$iF_{f;``YմiS/^\0A@@,X _ל9s !_⶙jfb$ `YFll:uSNiʕ0;$&[x M2EWv r(ⶠ SahYƜ9sj*;VJ2;&9r˧f `Y… ;(((H}5;Y@WkÆ bSoVXXUvmS}]ܶmF8U^]jРz!]7os=o]M4QZEGGӲeJ~M#FPLLBBB6mѣGKݾ}$IZ*q{g?_|Q:tP͚5]v]q T͚5նm[7NGm?UjUzz Ukך{Q~//,xUVږ-[JJfRlllzU܉o%uhfV7ΌšO?d`>}4{l(JnʕSlݶmj֬Yܹs>|l>ʕ+WBB$ƍ]|'xBt)ըQ9y,KY?~.\`~yw}jժU6jȑ6mZK,##Cfyذa/P/""BǏ/p{jtY{Jy͝; U"##u! :Tf*KE??j٠z(,q.PZڳgmM65 ӷo߮Xm6lٲL}paÆԩSJJJ$-\7i$s;$$Dqqq զMt!~Zz5kfWR^Rm۶֭[ ,ǵ|r8qVIvک^z PZZݫJʛڥK-\Pw}w[nڴiy[5Pm޼Yŋ8_)oի(+9ytQF۵kV\\ۿwnfeddWJJ5sLmݺUK.UPPU?E 1c(;;[gϞ|rIRnnn1&$$hV[sN:N:R}Y{͜9Scƌ)/uakQxlW:[=T?kR|||4a=ZUVkoݼn1c4}tWyܗ{Ocǎ5g4H~[~<(Sjjj5i$%&&Jm6]uU]5m4=\… 5x`m~/뮻|rhǎEֽ3GGU~m=[^H}UΝ8K/:v&NXoQ ^{>{=3Yre}رUŢ>}F;]lH͙Eq ժUӺumu{yw'O6͛7p:2dԹsgeddhܹzյkWԺukWQU.p̙2^*\%n;ao/ot{m @>}H^en8q:y7 $;4h sM4I;w,s_Ǐ7~UNNrrrJޑϣ9;r䈹ݰaC{QxՇ`FgKuAcWZffVK0?_e(_:tpu@lڴIw}ڴiKQFZݻw׆ TNG x6 ù׿{goX֤IuI111jѢ6mp믺[/>>[Y-}GWsqߒ̘_k^edd@an۷Ϯ8\o~UTѩS:p,vsqW^=s׃>hW˳%nݺYrss+__}[*^zzoIkW^'|bmJJ^{5IÇ]"5Eٹso.IP,_n^b (z+WtYխ[WVZZZ9ytoFsOuyuAgϖ$-^X/rm;,_e#77}rQp JMMUBBbۼ;vgKRXX9R첪VŻ4}dd*55axPϟ-[hԨQV!?P: IDATGG}TNj/hnCժUsh Ob3oTXXuQ͛7O}Y?--M}ͯqڸqc[Y>b]={l٢4i*^zY믿.π9R'N$3F7pc=d?Tnnݿsѣ+""B j߾}b -^f$]i א!C[oIv|/_{,O4ӧUNurrrꫯkap텾~ݸqcW֭KZrESK]V%{nܹS2@X ,SѣGkm&L w}{Nڽ{z!g>C?=.òsNOyŋm^OvъLܹsgZZŋ3]v[ +9y?\3gԙ3gk. 8l0`@筤]Y6m0;y:woƪNvvջwo {Lw}w}7h ?xz5ko~_7xzah̙/%J;^+߯sI?@٤_TTV-+,,L]tѬYlwߏFDDhƍ[պukEFFjРA:pPX ~ INNK/$I… }<|||+""BSNնmty-_\={JKnL4IUT1uXEEEĉںu.^h{衇`uf̘cZV>qKʕնm[EDDٳڵk% ?* cuE'O4okҤbbbtEmذq^[oUԪU+?j| }܅q8~x+Vo^VZI&:׮]Խ{w:tȼqjٲ.]~M)))[nEK.Uppp7jh ԲeKU\Y֦M @6""B&MR ?##CڷoY۷Wxxo߮uYf|3^}z$%]k?RdEffrZ@KՎW֞={nkڴեP>%׮]^z飏>*Ry9sFr kzO?iӦZjժ:tƍwyG!!!:wD-^Xv[/Yc߳m۶M-[,SF.3ydM2z?z衎;[O>JOg)4Y)-:e7Xnnfܸqc3a\3FoV3۶m@mܸرݻw_~_lټys 6LO=TPvw/Jz]*rrrdX멧*q爸էOex#Fhƌv?^wuWF6lÇOz-wwV=쳚>}iӦuFH .ɓٳgkv_j.GٳoRϞ=j*կ__k׮-{{GK,7gΜQ&M[ڼ, `ߔ#uڵ7Ț5knS֭_~"9`޽[FR`iFcǎծ]MIRǎlR-*M~>>>9sjĈVPP:hZz6mڤDMbbbkԩj׮Wʕ++**J?~';^x j*I2g aJKKӞ={tR >\M4Ѹq㔙Yտf͚zw z秏>H~~~:z}"|i/)TZ5qJOO?_Xc0Z7/xS/kUR%%''F]wݥ]jĈŊ8Ñ#G԰aCIҞ={8 (ox"ff;;3gRZl[nEs5/%VO>y~ZoͺKIyyݻwNXBe˖B's+?~|}}u)/w].u5(**J{ժU:$+'NPz\F 뭷, +|W6ŕc$pg={Ԏ;TZ5թS\}/0JKK'|b{"8L6涭"k֬))//K.%rFғO>9sUܹsڶmڶmPk\3_4vXG@/22[QVVꫯ!I^,_fÆ :|[~ 40 >yd`{.c 8PaaaZzpL+WTtt-qG{W_]:dff꣏>2Js](wYzݦMb׬YSחC߅˿ϟ/en < `.WreM0AahƌGR믿nu1ʋ8a0a,}]Wnjn7k̮6M65BtMԩֹpB@X[x'5{l}GzgN\aJNNСC]Ǖ5.-Z_U ZN]iF-[tu@eddh͚5fS`` #Rnn [lRhڵkSNZfvܩusQIW_@WIJJ/yVZ0aԩaKJo3c=c._~=zNܿ- VFF֮]jժ:wqrpp];yrrrTRu~>դIs߲e/'P^J\F^[n5km mVQ$-[駟V\g}͛]^իtRyrnU󳫝3g\>O>QʕuM7驧RÆ o)!!ASN?_D^/tgxxeee{U~rwM8k\#}'?իWCpDX222l|?C6lбcԡC?k( `nWgֳ>'|RK.uuHIӦMqF5mաCsСC5###]U.\`U񱫝aVyLx0dX4}tWu!֯hswL^{eeeY2ZAAAjw%r*U2  PUZT.^hn[,;*$D,|OFʹvKKK3CCCv0!PUuu7ҠAg.Yf @ɑ*U-ZT;v& ~TT07˅7ި-[HvUlK.?0˱N  TTU̿HP1/1nZl;w*;;,w)q(@E#P1q $޽[+ʕ+h95>! TD9- `@ʕu YܹSO=崸 (m5_R˰J(,Ce: ygl͜9SÆ S ojJ *P `//s6::ZSN$jȑ:uJO>$)$$D ./swFu%m۶MJIIѮ]_())ɬ3n8ܹS_ׯկ__u)g}Vah„ JHHɓ'5l0ծ][[nk}^zZtbbb `:aۇR@@$)22EJj׮]uΟ?xǛM4I'Oo>+11QSFFj֬-Zhz䨇AH[?eo;:6 7qQQQzם7%H` hLhh\ uu` `\X3 (W2$,<, ##* Ţu*r]0p/A `\v6h0~Wq233off* Ha:}U%KGKJJuAWK@=6c0x %HbKg-p[a< `C9++*;;mp?JX,-==Eߟ0^(00P~~~Vsi߾}ZT"___~n$77WJOOٳg $PR$cXTn]>|j第,Z:8xеkWla[ RÆ $baÆ ru((8$C_ `ў={rӦMUR%Fo&M(33SgϞչst%W_!!!ZXC*,-X,?s$E Txx Pnn.KCX,!%HʍbLJ8/A `$K/A `$K/? 0 G|||0`, ^0x %0P]tI3?]0233{uQ$cdff6˭[V@@ #߫]ʎ09bUի"#>>! ȑ#]Wv8}C߻9 `;={!^%о}{EԢER5`j߾̙Sbr~k%KUwÝtu|uθw{,mǸW,/8]b\|X^q:>EU>vء޽{+wCّCժU-ZP˖-KWppoiό/**a}+w{=u\tu|qcyqח{7K@  ^0x %|]@E3`uY~1&w6?NWW^q@W(tu|կ%ʮ8]rk ,am߾]fy۶mjٲ #ʆc,5EƸ|, ^0x %H  ^x0M4ɪ x2iY 0\X `$K/A `$K/A `$K/Yvv~gmݺUiii WttnY,WPۧ_~YmڴO?]>222aܹSgϞUݺuժU+]}J]Ǻt}ڷo.\ (..N111@VƻSNiڵ:t萲ԨQ#]{,u̙3ՠAxz~zM:U7t5kW پ}4p@曮߯֜9stqEM8QuQ׮]S?ykNqqqZjXÇ+,,L{-ZoV#GT-t7맟~*q*49rDSxxy_ҪU4h 5iDwqv]>ݞ+O6:udH2BCC5k֘rssٳg~~~$cFNNJi߾} ___C1i$WpS7׿Jqa#66֐d4iؼy/++˘6m!ɰX,ɓ(hzWiɓ- 222t뭷jX,N:-ڶm:uhٲeڴi.\]0j~И1c4x`mڴIΝ;sή VN8cO?UFtI:u_uQ񇂂n:l㣛nIYYYZn;ku+>ٳG:u҉'u֩~~C{͛b kN͚5+UnÆ ޽Ν;VZoQAAA{vZ޽[K,QUn]w=\Fwرc mJf̘K[@i8qBC ѐ!CTF .])S믿֌3_}Q۷O4b_ԍ?^~Z[c]VVz{^vl IDATI /BN>]+WVnn#GqNmKKKӃ>/J^{5U\@=___曲X,x= `yfߑ#GڬkXSOc*;;۩ezQF]YnܸqYtV\))jĈ6$eggkرO;uf͛%I!!!8p>֭޽{KΞ=^xTh݋/GJ6mݻ3..N7|6z W_a]Eٳ|}}%-S3w\>*.}4m4sSzk֬QbbbJ]ƺkf;P@@}Ԟ={@]ZZ{=|}ߕ}Θ1CO.;# ܹsZdYeXlSF n,'$$8->JA?kHRNmP9w猱n:~x9677W}Ym$ bܻwliHHTobۜ?4{Hjҝwi9:p{cpwڶm6mڔQ6oWXXX\n޽{ƝC b$۷O7o.TZUtzfذaڵkZdEQQQV_2rXϥKZ%;/Zn]lPR0kРAf/(+|ᇭȞ0СC[dwyܞ8qBBB:uʪ|pڵnvIRvvmֽt}SSS5|Iy+\+v gUݺu%I[l)r]7HBBB4qDu=gWTT$iΜ9V(o޼yZ~$SNzg-FJAPq-YĪ?0 ~ $M>],ތ3tIyKJ:???͙3G &X]2q̙3^:t+fwիWׇ~h&yeffQF)''Goꪫ*ObǍڐdk׮ڿpB#00Аdt8}"Joĉ$n0rss]dgg5VZe >ܨT{$3+cӦMFjjjܹӸꪫ IFӦM-[rrrz˰X,$G5233#ƺj2$:t0o0{93qq^Pu[xdH2Nɓ3g>!5}bð3P?^SLQ||զM5lPvޭP3FcǎUJ\.PرCǎӊ+oO}Udd5j5k(Z;HII)ѵϦO1cq㔐,o^u!կ__&Mf8uرcxb}Z6mڤ'O*::ZӦMS^@Rǻ[jJLLTpp_UZZر};Ep֭[;w*==]ժUSlln&:}Zݫ/Fu]5x gu'OԺuteee),,L۷W\\yxw!_^GQnn"""t׫y]/O*K/A `$K/A `$K/A `$K/A `$K/A `$K/A `$K/A^RR4hաeBuQ:LH`Y,%$$Aϟy睅uuxb]pATN 8P;vTddK.駟ֲe̶ƍUreIaП5khJOO7ۿ~'|ݯ1OAxEI^͝;WUTYܹsvPP&NXhiӦرZC ϗ$=C6pLSX 0\(""BvVX!bnvvWKtbYf:~Ν;Wd :yN8!_߂%8I ŋ?Dږ-[D$xvGƍUJu^`֭ھ}ZhWgquh"=W^u7l`U7YgKzyI~au8I FJJ;ܕכ>>>С]풒t 7*F0`5lfc k,^XuUǎfllUfWÇ3[ lذAR>}l.1OCxE:Gё#G̲KԩSj޼y{?$顇Yc 0ׯ]uK{+&&dmf=)x ƦM쮛Zɺkǎ% '11QIII>|x8i *5֭Hr7o}t]'.]4gӻw 6,x`V\ٳg?O7q{ygR]]=9~5L6-]vw=U__<+V|.T~>/NϞ=3lذt֭Ukߙ.]m9SҹsJg֬Yyӭ[ 2$={l6Gk?776c5>va;駟Θ1c2bĈwy-Cɓӹs~ӧOL:q-ZN8!w~;=Pz/| n .>viys衇fչ袋2lذ^zGMvRUUU#smZo稣sgΜ99CsgذaYdI :I3gϴiR[[(2f̘=ɓO>S׷ d1bDڷogOKϰarg_b^yL6-cƌ+]?Aٳg7hѢ/:u|JK.nm[ޒ:{$=ܓO|YbEjkk3u7SLiv{tMիWo~sկVYdI:w\io׮]:묝=Nlܸ)I#F8>he~O=Tk (^{m *:{ir|ĉE3 ,(w^~ŪU}E/noKuWIO<;wnzWIo#GIEQO~K.p EUUU1~&-[VŊ+9oKI|pHkSEo/.¢I]6lhE$<Ь}ڵAT$)Lѣ;wnO~xWߩSXt6qO)ST/^YU=zG}tpkh+@? %)&Ncm{m+۷/&m:t(rJ6m*I9s4sƍl֬Y8"I1|mֻkGuTo|IK+Wl_HR|h'(ڷo_t֭xWo /XvaE^y5C9HR 8k~aI￿[~-E9V+q-G$mݶkܓ禱۷o1cF[Yvmo=Irqek~sa]/Bqeܸq=wwk}嗓 qK[E^} 6d֬YIw]-yG$I[.o~[L6-I2z-{ci${jG}s$sNƏ/ .lɓsye!J>zh%ܞk&\sMcF{#Llܸ1-ʻ&k֬wߝ~8Ϛ5kxm7$I޽wgy&'tR^xu]g?SojOX" iZ|yovڵr|…ٸqcd„ [6թaiӦ̜93Gyd:[򿾧<z׷)+zk$s=dҤIkmoJ}Yn]حOkIҡCׯ|nݺ|sKmmm/SLOzK_*=zȃ>믿>s797wfٝ1}$٣ 2$zŋ6Çɓwo?'I $~wnO?t'IN8$O<>-5$p@e!Cd̘1Yvm.ߓZdԩ6mZ #Fdꪫr7f̙ng 0 IssLqy9cwc꾬fk_9$}ꩧXYv0noի3k֬$%\R9C%IN<&nڐnƌ$ƍKUUUϟ_-λiӦ|Cf][8qb9̞=;~{=5ټr3~̙3'=z5mYHCCCEO<ɒ-={v֬Yf$V\ Kڦ]}n^kرo~\y啕eL _/!CdرݻwOXYy7kllL$se1W\qEN2%C ٞ-馛lޏԚlj'Nb -cǎСClٲ̘1sn|Mǎ;ִiR]]3I2o޼${8p`\6 YIDAT7n\OK/4/ryַդ-o~[.K.mv)SNYre>O7YZyOk1bDLݻ|gw8p`8L0ɒIRUU3gD.?ȯj&Mٳgo3@bݺuo~AgϞ=w_zk^WWWy[cK_JMMMnּ+I6~{Ȼ$5\~yvk7mڔEmsӒ;/;u9ڒ(]ɖpj…y(6lH]]] /0[ ~\{iӦt=GqDoxG&L &_~2bĈfc_ SNСC>O _Bjjj$=X:,\nӧOL6-w\[SSù'|2 իsu]l(rw禛n /ۧcǎ93r$֦N>;rKƍs%KcXJh[UU޽{_r./+a|9ssW6'̄ ㏧]vԩS<\tEO?}ٳg9|'s6=w-Çϔ)SԷ'kק{;n&E[Y}ٻ샦N . r9H444d͚5YhQ̙YdIΝc9:th,[1{_k>7-駟^y]r\p3fL.&mڵK׮]ӵk{9s1d֬YX"{o "-[o5{.`d`Zs=ʞsa%I-[jqihhȨQZmL0`@.ʱ7;g>XK @9ꨣңG?M]]vϝ}2xuY޽.Qk<7SNĉS__>:&M3vJ%!( 0@IJB P`$%!( 0@IJB P`$%!( 0@IJB P`$%!( 0@IJB P`$%!( 0@IJB P`$%!( 0@IJB P`$%!( 0@IJB P`$%!( 0@IJB P`$%!( 0@IJB P`$%!( 0@IJB P`$%!( 0@IJB P`$%!( 0@IJB P`$%!( 0@IJB P2}!|IIENDB`unyt-3.0.4/paper/unary.png000066400000000000000000002124351476461141700155120ustar00rootroot00000000000000PNG  IHDR^sBIT|d pHYs.#.#x?v9tEXtSoftwarematplotlib version 2.2.2, http://matplotlib.org/ IDATxyTչ?A@d5&NQsx$*z!ԈqGD jq`dG})Ƃy֪zk>yI$&/?P39B #9B #9B #9B #9B #9B #9B #9B #9B #9B #9B #9B #9B #9B lEEE.5km]`6!CW_jשS'MTN Ԉ$Ie˖1O?={ܹѦMH$믿mۮלoQFѤIJ._<;xꩧy1t0`OP%=:u޽zݪUڵkDDl_Ȓ0`@{U_PPw}w,^8>3/O@ Ԙd*رcDD!3obv)kz~a|1{(++vEϞ=k׮4_s΍w=շlٲ3fL̜93:vsLԭ[wL:5}ݘ;wnFfb}74hPi&M~ٳut>˜4iR|1o޼Ϗ֭[;={\,Y$z8蠃M6iǦNZ,X ZhtҥXlYL81O/ D.]-bPPCDD㮻O=$"SO=ұ˖-K}䨣JԩDD_?I=2jK?u͞=;8p`Ҵi9sfrW'͚5+wLݓ+"{=IIIr7&G}t:^QeeeɟTPگ_N_u?9EDҨQ^G|ArW&;Sʕ+O?J!_^g%KRVK/MfϞ$I&ǏOve\zZ.jjY?өpK/6G}t-$???+s&I$EEESO=m6]v%ʭJF92]tQت7| :4,(^]aaarYg%yyyID$g}v2mڴ3f$ޛ-Z$?|s> '4iҤꫯŚ>}T*K.Mk$"֭['GN,Y$ɏرc~ЧO*/5&yaÆ%~{AvaID${Nd_^nh:nܸ3vش_.wܨQ3aÆsTg׻wrW¤±ӧOO]+3M6-ѣGұcTP;HSNX=z ?{뭷Nr䮻/_VӪGO>sX"v}M7tSڼyyyɓƬ`.JvOSRZZ$]tQZjq$o۴?ڎ=شZ_KuB?ɻロ|G~YgI$/Rjժ:zus);^zȑ#3/X ܹsvۭ.]ԫW/[nk}ԩIZ@;f}1iҤ83u1 7nܸH?RҘ={vckժ{G=2eJژfGyd]vY<ѻwT'OQFȑ#{MFzr[ /?Ct):*=*9rd=lذJ;e]wyŌ3a[lWTT'ON;-jժvAqm9眓ֿrrqW#"/_k|IW_6fg߼y{ӎ929䐸k"???jժÇ &_'O֭[?si+W^i_k}-ձnŮ;C\wu{e?#"vq5T<0~ߦڥ&rǷj*5kK/{nf͚UW];Ă ʝ7ވˆkrM;cKJ FenݺU_Wi%KĬYbŊ 6o<邏NׯgqF{ѠAcmڴO>9G,_Cιzx>$+"֭[nUz%\-ZHWX+ߴiӴE]]tY9Ez꩘7o^Zw}rKDDtܹкuڵkZߋ//.w}Æ n~_C5˚&GAAAWe˖ŵ^jtAѪU*=k:u͋o_{bС>T8~C~L:5-ZTvZT9_sn;G;ݺu=zTKV8v}GqD+?1me… ck}Ί+駟SN9ȳ>ӦMK צ^zqǧ=K.I+..g}6oԨQUFDFH\\\ncӧOr)q 7D˖-R@nX.jժϱ.Çǧ~rJ|izt%JL2%vu״qO8Nv{1cĵ^g}v w0=/WeWuԩjJ$1zڵkq;ƌ3{VVzWUNi}7tSƸ syH=Oƍ+W\*[W~xK.פ^{-{8ヒ_=Ǝx`KЦtTU6mbĉѯ_c .;/~`} ӨQ5WU!Κ!]6͝;7oc1zxG}.-%\~衇bΜ9ܹs_͛ǯ~=wygϞ]yܷju"i l*Lqqq<&M~ѻwlVMZƍc=w_n:ĉc='|2 H ԘmfJŋGDDkd'Ğ{jDDyqꩧf6ۤEEE1bĈTG8k|uAAAFT{+V[h^ug-#"tiӦi>?ZbE7n83bĈQq>ӍZY'xb|q饗Fӎ-]4=?\"jL׮]3jj7|:tDze裏*""88餓\Uj׮ JSAC=5*KZ?k vw_ִhѢ>e˖i^}(++ǹǏÇgjjѢEi&߯wҥ駟FDd67>Tb5Usgmi`{b֬Y1q֭[_M8K.7xZsy9gM>}zӧOݽ{{\7K/y>ZI&=ܓjzN6-*?,˵Sx3ϬxT/OQ"@c=2[?~z駟h۶m5WM;;v\v6lL$IO`߿F_u˗_|jo6䞶N?>ôvM_kz=bͰymGݺuK|cǦ/*?͜93zJǝwyi5.0P  6L 2e$IbРA1uhݺueuʕ+4nFZCYfſ﴾^TTT*Ӽy8~cmsTdСi!~rK;C}U>oU;r <0c̉'M4I8p`7Ν}s_rǬWs_paZ߮plqqq 4Gy$mse 3oVZň#׸q`3Ixgb=?ENʝϾ:UGDL2%g[dIwq1uԴz+u&Iw%C/:(^}մq˖- ͜9s# . .[kp㏧nѢEk1`ʾ#JJJ2^Ky{3Vgi7k,5Ϗ/8O?^zy|A~1lذvm-"ƌ/|)S;ڼ}ӦM[nF-ѫW8p`5*w\㎱;5\6g+" =zH AR;ÊlqGޣwC袋bРA… c~s΍;.$ ƓO>=z9x+*\ `ׯ_\25kƍ˸:8+VZG .,N9ԩSuQW_ W/O>=^{~;&MT_2sΉ~ň#bȑ1ph߾}L6-28bĈѧOxgҎ">C9$İabwǾ6'gto1}OOpU'|zo[lcǎ6mT8g)5\ IDAT}K.M;bu0`@Ɩ۫W\j P`y6m$h޼y<9ꨣ#8"{s:m>쓼:Ygk۶m[o> vm3Ӯ]dy睤~微UdIIII2eʔ 72eJ2xGk߾}r'7xcoʘ1cRp 5׺>|x׹sGIVX$I,_KѣG'徦K.Ijժz{]rw& .Lyw_1W]uZ*0A-Y$1bDһw$??ҀEI^zZ),,Luu֭['wyg$?Aµ=ڶmki{嗓ۗ~Ʉ 80m\~~~r&˗/OZ01"!Cg7ߤ;vz͵䤓NJ5j5m4v->|x*쫊5+{֭['ԪU+i֬YmڴIT\cƌIzSOM>r?{j}:tXk=3gLg ;#-|ڵk/~Ag,Y$5{z]^rQG%K;o&MRv-뮻25'5jk&]v٥͛'?p9k?6mݺuKƌֺ#/I*w -[,1cF,Z(,YՋf͚Ŗ[nݺuv!eVYQQQ1uH$ڵk{Wtر׿wߍMF߾}C?mc/hӦM|WQVrx/yEFuѳgܹsرc|嗩]wvZX"ǴibŊѪUܹs>⋘,~xWc̘1[@G|򗿌;FN_~Y~`/UW]|AENs9'ԩjܨQ⬳Ϊ^zo+`!`!`!`!`!`!`!`!`!`!`!`!`!`!`!`!`!`!`!`!`!`!`!`!`!];-Z&LH۷obEƤ0TO>Ѵi,V{@0aB׿v&⩧:*e[@0@4Pcڷo~ꩧsY6_1jԨT3t?n`駟Nrl'j̚SΝ{Y6_+W-[;cԯ_?╚g ha0zlcl 4@0@p`1+Wo9վ `3!rl@ Gr Gr Gv@$Ieee$IK$///#///ۥP)..ŋŋX D^^^ԩS'4iM4:ud$֑61wXlYK62IDQQQ|wwEÆ UVQ^lF50lc֬Y_J-[f͊lB5Y 9VZ뮻[iii|WQRRRMHIII|WѡCq"SN2ٳ0/???7n7:uD~MCasUVV??eeec1{h׮]+:JKKcɒ%i}u֍:DbGu֍ F-/Ա%KDiiU?=}_\k׎G^^^Zo:\[PPuR5nݺQPPַt,UCuyѷQalҘT{",:v.Ȳ5WmYؔ4j(C@)))7ǿj]f)I(--MkРA6%k.--$I2fc hQeee}P+Na#$IF{@U]Qw 0@0@.8%`Yai[5y.!`\ Fv9m~b.!`a h1ԍNLky@Ϗ[f 9B fm.j{gyfˀc gM8بS^+Ӓ%Kcmf1`1IĢߥtIi3fSl fc<.jl6⢋.n-ۥa0fh+M2qyyy1zr>}z\wu{ǰa/ҥK4h ڶmoc֬Yγ{Txw}3駟VXkQQQ 6mڴzo&lDMԼotfZwر`S3x8C㪫~q;CL2%>쳸ꪫ/wwyg<'eee%(..N={v=:8qbliso=Hmg2dH:o7tS<Ñzk?֭$IrJ[{=ܓVZ{&j?|L%Whl^ڵkڵ:vm իW/vee]qq;$~DFbȐ!o~j 6,w(--/wܹ1pxlժU 2$߿uԩS'z~888sSǷ~(((HDn*|`Sb h;P屝:uXݣgϞqeES7|s,Z(>4hPg?޽{ǭ~zjرc㫯firnjSi?|D5lNTek:uDfRw=}ٴU:IĤIʝSN1`TW^iӦUXCQQQ<뮻;\i͐KԨ:uvmګ޽{Z\rI$Iop?-SO=%C[lEZ{ٲeu]CIޘ?~c|]vp 5S(lBcevG~`PVV^zi+Vߞ1fٲe1f̘8Ce˖5^#ljgfծS7?lFy1hT?8 sx뭷""[oK.$֭3mfK mW:c?A%ӹK1gΜx|0r8#U"d=!d}ѵkTnJpx㏏ze<:0S{/^~刈xǣlcb޷_eee. j'mڴI?nG^UdrLIqQXQR\؄ӱRz /L{?~|?>N9*͑kll6l{ƌ:ujZ;I VSe=hܸq;."~\\O`/D iR?3ge:uj 4(5j3gNUu 7n{n=o޼8}Uz[oO>I KJJ⡇r-1lN?+W/2eJ7.~^{C:~裏￟+)).^hQ}]v]#"wߥqq믿/rtA۰l<ׯ_i={}<>` 0 yK,x &L{(՞0aB|L$7[k`:>興hԨQk̀^z)w/~{W=aS"#<#GgѰahԨQqWG}~xDDlv?1&MϏ'|2.xcSO=5mEEnbI&Eď+~;~ѡC8âJ_~i2LAAAѷo(((*9xbذaU66+@Ϗs=7y۷o۷Jyj֬YsUt5^x6V3fLlg(X kbЪS۪alI>}z1tv9Qϋ[vPn! 3v)Ѱ41z8sy.6V@]N6l1bD4n8<8cƌѿr-OSKrL~Zu.ے%KNxc?p4k,[Fl4h[lEdž  +Rڵg޽{GAAAlqań K/vyQ4;&N2`!SR\_j:Ш]n+"SVV, =rsHݏ{.6;+(fԚj7%׳4l ~t+`f0@0@0@0@M7Lky@WN\x.,4@0@p`1I%S&[D^^^+"SXT7;&|1Q^,VlgV,"φ@[}_v폟E4l*'%9B ޽i94LDE*tذ[.!"*Y!WZEh eBVSJ+S:Lu7Ic|<}]s}ԣ^}>&iB &iB NgٲeaÆTT7͢%K7qEԸq2vFѣGxǣQFnv)`(g{я;_ZƸqbnvYV@ɪT)&]3wv 4U)?T^O?=V^Vvիwީn D?~|yѴim"'''4hݻwgyfs;?yI+2鞻+N8Xre}7qƕx޼yw];qn̙_2j֬5'xbΚ5+8â~k׎6mW_fڦ7ވ_Q~xƏ=z}'rssAѫWxWJN+}SZ\XXX؛ni~Ă Cݻ{z3&7oǏN;-z'! Mѣc֬YG=6޽{{zk^z)ϟt%"">`bg}]tݻرcO?ucƌΝ;_^"Pbذatys?}deeo_~yM7wN4_o??:*˨W^kQzhӦMt-:uSNoqK_*.\g}v1"qN8!X"䍈_~CM4ƍ'{g^ŋǻロt^ . 8㌘6mZ|Gnݺ~{l,Z(N=5jTde'6СC\pѵk?O>dK/E ,Y_ノ:0CQ~xGe˖jժ7n\{QXXÆ 2dH=#.Ϗ뮻.qcOL7˭hС_FDDÆ F&NÇo9rsscbѽ{R?!yG|pEDIJe ._~9ڶmqӧOWDD|ѭ[XhQ4h FnT^xe˖l IDATxSNS&~SO=5""uM6M;rȸsMSNqI'E׮]ݻw:*ie]+VDDĄ bС^HKe hH3b EEn ܼy׿JӬYw} .L>rSYYYe-8~omFDD5bĈÇO?/8Uԩފ?O%5j(>sJu9nD]\\ *1zq' ċ/^#"zꩨSNtuHo` cǎvmoMիW:i*U&RJx=O?}__ngyfԩS'Qy%ށ\r夺aÆ[+ڵk'I&_]~C윳gώǙgY*0@7`=zt/{nY~}Rݱcj{/:w|A֭۲RF-n/ڵkW5oNvvvҖ˗/iӦFSNIʼn^zq9$I&%z*""i<Ȉ={\Pb|]w]*CYfV3gΌ:(oFR黎7}Iuia:#O>qW_}udf'*QFEVM6;60I 㩧;Fnݢv1z׿0`@^~} 2$xK/L7M8zҥ;˦snFM6=z$'xO-ƽM4ƥ^~\uUQ^m;G}4jԨ8W_Ź~x̞=jVZTow=x{jwkƟO=TTT)z}|Ai&.h߾}E^VguVѷoߤ|8C㡇*ַ{TonMm-vڵwN:ۮ]8ccݺuaÆ=zt򗿌p_ܹsCsa#G,zn?yyyqꩧ&'OѶ>TϚ5ktpƍwMWkn㯽ҥK'W^y%+? gϞ@k):u[y{"=*^z)DݪURouvc=K[}Toذa;wNz>[buoi~kI&;̙3^x6wϝ;7;vl+rssw'҃K-[VO?4.(((1^?x֭[bpi!7;׏\sM^zu<[ooq(,,1cDDDZK.٦z>wyb ?D i&7';n'7';-tѣK}ҹMW |͛}VAAAY&"":4hTO>=8qbR(\XX88\qѱcD}7o6xW7ߌ??N8ᄭ>""/KԪUk[ZJ*oDthڴiyt?4{+^{-fΜÇmF۶mrʉqÆ ӧǰaKGG-YsODNNNr-%m6To5jT̞=; ի'ϟ??qtXhVެ=zti&""ѣG[A/Y$[nq=luG}4xR͘1#ꈈ[G4F}M :'ڷo"ş8뮋k&:uJ={vqqG-۷o;&Ls̉_|1;0`@ԬY3Ǝ[.KժU.K+V38#Zn/B3&cܸqѫWxw޽{<Ɗ+63׭[7NsNdddkrH ><~9sfu]ѦMXlYqm{v۶mW\qEo>zx7_wѩSɉz(g*UĹ߯>쳷{[F񶼱`̛7/Zjcl(.)H#+\f"++k&눻l}? #v.(/׿Ƈ~UV&MD .uFDG}z+jժݻwo1j׮O?tٳc}N8!~{^ c裏Ʒ~M4>}_ޭL<@[7{1bDKxXzuԮ];5kw\}ٱ^{mun}7ƙg ɓ'ҥKFѤI8΋zmu͙:ujt1:vꏞgKv׏(%̝-[ܡ9Ic ̌+2-obʔ)^;Ӣ,feewwqGׇ Æ lt!w]fmLj#|ވcxC`H3kMמjd#,=sQZͮb_oQOv1|M}bҥ+ĵ^vwDfvh~қ#,\0FsNI&5\'O+nݺKkR1X ?a˗//q8뮸뮻"""///Zj=:⊘1cFDDg?G˦qҚ~'I&$sl &if*UmڴIf*WG}t l &iB &Laaa̙3'Q||@),,W_}5QhB PA M҄ M҄ M҄ MDVvNQPP6Zvvvddd Rs=6Zn"'''miD i&''';Dx-^/,,ƍq7f222f͚n mq?2T7@z3gNr!Qf͸ꪫRT(`e]s̉:th˩n * 0ejÆ IWa@`H3֭{,QuYŽhsN|ѯ_8cR_|O  G~~~R C秺xc׿5խ@liӦm%;>,'pB[r% 6޻:`s֮]={:խ@LuPV6lg}v̘1#խ@J\Ϗ={Ƙ1cy3Tpf͊7ѠA8ꫯg_ƞ{'pBoyyyqfĤIK/}'>쳈(..c{gTR%>K .]m۶-ۈ#"###iݺ~-K` h޼yc3< ,HbŊ袋bԨQI꫘8qbL81N=x'#'''qދѣGǨQǎG^x! cѷox嗓ˋ1cƌ=ztҵ=#8c?ݻmݖviVT@_|E^Ѽy˖->:sL>=^bvK;vly晉ފ!CIJebŊ[}~Z⪫VZ%?~~1f̘5kV<ѦMĘ1cƔ+W͛7͛Gʕk֬8߼yhֿܸ؅Y Pu%tcw8qb|qGFFFbGz:*;6ӧOo>ڷo߯ѣ߱cLj袋bڴi~zr-ѿĹC9$:t͚5˗GDz*oҋX*Uvډsq 7$j*ᄂs *1nU[{&ՃN 7c=NK3f̈m~T`H3ѤI'3olLnذ+:D;34jժ?[[l8o@Ea hH3q' TFFFq87mڴh׮Oի'}]ԪU'.J, `qI'|>l % .p@Rߦ`S`To53: ifÆ '}7*UŽH7YYS:uR )0dz>/0e*???n׮]:6e h˂ 999qGl3g&{ժUKa7%edd$7lؐN .O=TD|w%%F˗!$|[>qĘ1cFDD 4(8cV\ իW'՛ 4h8~czժU1nܸm~&j$+qM7X]tQ\uU1gΜzٛ}_|TXbc?裤w:t8^lYs9oƄ C"ʓVZ=-ZaÆŴibƌ1hР~qmşw~o#<LsȐ!?1f͚0O8SN/v1`ڵkyyyѥK2dH}ѵk3fL\z{ŵ^[/ڷoŋ']o7Mi&z衈0`@4n8&M4駟Fũ8w'Gfbɒ%Ic͚5!C$_~L4)8͍=3N=x7.ۡvvYnH_{gީn*kF5{)]^{Æ aÆm=_}\<~1xm;~mwC=4xvuV +ReggGnRFZNu @@Ύ}&?FFFF䤺 Lfff4n8m`'s3DNu0R k֬Ia'Lu㫯JԳgNa7T@/)EEE|A{ /Ĝ9sbժU)^4ST>>RntX̪Žr);z~8""{8˳5` w_T~Tl &iB &iB &iB &iB i*##ĹtjJ+v>Yn(k[VR TL%ׁ_>*Wn]IaaasG iRhT222";;; VZUVMaW`ժUIuvv1=ի'8mk ;/0MCǒ%K@cɒ%~5jHQGl/[@@͍ʕ+'9+W F5ZjݞPEaaaZ*K+Wu@).*u+I9  +###kOV_>/_˗/Oawn!@)ڰ>:>Q7?wT"Zj4jԨD %ѨQZj[a;XrʩnT\Y*UF&Mbݺu+WT$zQFɱ.J @/J=hѢR7nk`'u֍(**54T`)M`kO/ז>_vYFFFTT)mPF M҄ M҄ Md(k>~h9wK i&RV4hsTR nzq3# i&#RިY  M҄ Mx0"oz#3o 4S\!V|DgCo-҄ M҄ M҄ M҄ MDV^٩n@T9''mH`4!H i8 ufVvddd#ʋ]hYcTڂ qD}m"7R ;ȘSsD;e h4!H`4!H`4!H`4!H`4!HYn([Y2S=j*0JI0-a[oŅ^͚5UFժUc:+}(**Ju[fM/N9hذa1iҤ{^z'ڵT @vRk׮.]DcQzĵ:D~{ǬYg?Y<ѣGv @*YÇH|T_R;+"6lƍK /*"" ⬳Ί~<vBy_&>lPQS`nݺsk׮#F#v6^wСqQGEsύ}nq+ׯCh?e#kCN<#K/g}_t5.k֬Y1rȘ3gN|ʕ+#;;;4hGydt=N<˶ IDAT\\pA >x衇u1|F`jժxgu۫N:Qn2sg}nQQQL81QgffFϞ=wxޥKƲe˶ s?:=xwchժUԪU+f͚-5kc=Ms>IajvA|M,X ]ҬYs1jԨ8餓J#GFQQQ/:v{gVV- Pnlyx O>Iw^l=|P~e(8Dv[\xQJ~KQFůXj6[zqʕW^I:oč7'OkƯ~;wn%;餓J:u)SDDĵ^wܶ?qYret!vmx衇yk b|8sv1yTvlzni}SNr-e27T7nリL授K] ,ݻYS93~PR @ .CӦM%KDDDZ[w8>׿ .%Kʕ+㣏>J)զ;Ez+;vltA1p2eJO^: vjNR@".CzO޳-c~(ƍO?tL<9.]}}1z裏k6"" ^xa\yѠAyn~wމxꩧSN)gj\f.4.>"?}bĈ裏'7pCmvzĉK. /U˗/իWGÆ l?N|/Q_y|  .CGqDx„ I+7gĉ[SO%=ܘ8qfԟg}75kرcKS^Ο??9x嗣J*1|xnݺw /]46ll+Uƍr]N+k2eQ?>lH|8e#:uDAO>|M<[wnݺmu*.W]1>l,^8V^ƍ+uLy۠Ahٲe~_W_%7333oFl2;차Z.Eʼn'uԉ)SD&M8~޼yѢEH2OW_}u|ǛyŲe˶:ɓ'ou/1iРAx޼yٸqD~WFӧO͛']J:Ccܹ1uty>o?>VSL}w glr@qFDė_~:u*p~7qYgmWU{뮻.iFϏ>}Đ!C֬Y>:qwݺuѽ{(,,)SDF8(XjTև=TO?tt9O㎋&Māk֬iӦŚ5kJܻ`83""⠃F<@muQ~Ϗ?>sΙ3'vmUVTZ5+38#nXpa,_<ڴi?Ϣnݺ1o޼裏c/hݺuW[qe.Ke۷oL:5""6lVReHo@-bԩѧOx뭷""b…p¤qp@\r%qWD5jTDD5*~_F#"jժş(**5kK}~֭#777f̘_DDĝw޹@5777{ڵk|'aÆ9sfҘKOwKt&L;n;{uȑ#Խp@dfZPI~"L>=|8Auԉ;.~;wn\tEqAiӦǏ;_͛7UFNNNԩS'?⥗^w}7&O7tSk.cʊ۷>#/// ڵw=TM6 . fΜsVws?Xrev塬c߈3@Q\\\&0o޼hժU;hpvڂ qEnvv9""%se˖)(X &i"+ eRfFn_I5L~hT@  M҄ Mx0 EED}PݣRP ͬ/,qo~U3*e!ʍ@iB &l dfyzl_ۀ gݺuY̽ϊvT4~x[H`4!H`4!H`4!HYn([+WSN9%b@̌Zj RiB &i; ?OzD ؗpҡQ_T64P+!6KjkJ$L a4;;ӿ:^cy<$Kgw_O $l8p`pްaCrzqygƃ+vXzu0v:N|{ohZ0* D`*J,Yd  "\0?Ux h$`$`$`Ȧߏ>1}&N:~"FDlf#عsgLOO^IIIIIIIIIIJlk#"nZ͏{q7D5b5~c%W@$!$!$!@6s݈G}>0"!;WGD3bŊh6n ` ͟?v~E+ 'c<""UqG,< Q XdI,_ .``H+|n `\ w@2n7}وjqwD^/ Q`Hő#Gȑ#s\I D`zDDZK%i4y戈زeK5+ ~333133N'~IH`fggc޽/TZ«HBHBH3qXzu0v:N⋥gPw$!$!$!$!$!$Q+=Z6lw`<0$O0IIIx0$v7o nׯ_/ W@$!$!$!$!$!$!$!$!$!$Q+=fSSST*jJW@$!$!$LӉ;w` ;q_@8_ Dax;㼿Oa @90•W/;n/r\ w@2Z=>0$3QV\Sz jDe IDAT  d3oE(Q ^ygƃ+ WRE]> x zw|3(IIX06~op~l՛K/."0$|kp^|oER)Q StwHBHBHBHBHBHBHBHBHBHVz0\j5nygƃ jXgP+ `HfvÃ'n~ zϿ88o^p h$`$`$<`y?n\`ώ~? h$`$`$`$`$jj6b ~O~uo <>=@~HBHBH3 L8{l6 .`T`HӧHBHBHBHBHBHVzn{_n5 G>25b h$+W;0`HhĦMJo HBH,C뮺^ \˃+Z\˖-;MNN.Mdfgg? c3 Wш[;0`Hfbb"-[Vz   dfffb޽]wF"FEdz^;vl h$`$`$<X0K.3`t:hZ0* D Jπ37;?yF,7X oHt*Zygƃ TXaSIIIx0$bO `Lon6U*͂oHBHBHBHBHBHBHBHBHBHVz0\Z;0FLTco_[z   dzn spj͇cG>ڙۑ#Gov{cL?^y ?8'9뿭:X/W@$!$!$!$!$Q+=rnC#\ 0窳[`(\ + jQzLތU3(IIIx0$ם'jT*0*0$Oxlp^uZo\          ZpU&0 x j-\uï!O~+V_ ީ?{Buw9 `$`$\ tzq'?o^ueԪ8 \?FZp#@IIIIIp{gnO?tz^Vz\ N8wcϞ=tW^E-.\0?xl۶-x'>_җbrr2~ӟC=cΝo߾Xrlx7{m_h4⩧Ɣ oc߾}qw\l߾=կFR֭[cӦMo߾ذaC<3.,vNj| `8_=ޘ+"~AzU2p }-osvoV[gנAr%6 ?3LJJJdt(F,'g%H)$%%̙3Fdɒ:/yÇx\Y1qzql'^ܹիW7*wwwIRҥsͼΝ;fNqhl'%%֭[>:#9rշo_͘1Ce˖UXX[7|e˖'G\]a#E{2%$$X:#HHHPHH*U$;;/2vƾ%JvrssӡCtyEGGkjsΜ9fԆ |rrrRDD7`:VZ) @:uJ _URRo˖- $lRe˖MܢEjyoV=Hu8'͉$d;~uAON5% /=zhٲezl2⋏71_W^M5m+bŊAV?A%III۷umUll^ *X0Bd%ԫW/#ɓ'뭷RB$K5 @ofdøںu>Iڹsƍ@Ĩw:z*WN:J@}\7-QQQjѢ)QƱ%bݫ_]JR```~ݻgV/)~ΩyԩS'-]4d̙͛7+22R}ɓ'ULGw̙O <2J*$ VQE.\8XTTOX+V%I ҦM4|p#+OK˖-/}Zn-OOO͟??U2U7oM6u֒'g͚WuyoHH{n5lPqA4i$5o\ǎΝ;Gnr$of9[cǎ&puuի*Iȑ#so RF+V$ *V(XU8!!A&M2&L3<ڵ++U͛7ĉckkwy(ߙj?EjҦMԲeKݻwO:vP!!!z4f%&&cǎڵk+k~ . #ݟ;')S&SP7h^ds +R-\p],1U$ꕫ]v˒$777 :4s^}Ud2ӧu]|YQQQ:{Qqz"7m۶ڶm}iԩZbԩ1cHmۦ>HkѣG'|k~6mllTHl]+V4|r 4( t#WtTk) sv5,|d2)11l_G2LǪW\1kԨ/]TƍӉ'r`xoT=VXQƚoy9pǎ3QiI>7&&&:WVHHݻ+WY'-_w1]~쿶3fv)///=ө}"I:uT#ʓHgRnݺ?߷oΝ;?r _*䤡CQFo ۭZa7fX'-˗7;fSKR*UҬW+VLGݻiv,H4iG_΋~ *nݺ倀tÆ K)STT)լYSvzhٳZlYFGG)Iv|||R%8#""Էo_ojww}?l'NP>}FkFGGM S~W:rQ6L>}/_.]thy}07dG6FeO>]i۲eo:u4pO:uz4VPAÆ Ӱatҥtc7رF^II:tI|pBl2)SFtEo^ժUSǎզM/_^ .Lunppz^z?77kLoɓ'bŊj֬|}}նm[UXQjJ5tPP)_V~駇ޫW/cnxxׯ/ooouIժUȑ#oիf;yoz뜔zzzAxxn޼!CH\\\`6wJ7+_x1'OOVV-S>}uQϜ9jך5kjƔTzոqcIEoQRR{4WgIR\\^*I:u yZf^x]pA믿hȐ!oҽyozڴiu֩e˖Y>QtGdرc`]~]V2e/RgΜQj*cmzΚc2w^]Vk׮աCԿ͟?`7m IDATҡYӧOk޼yڸqBBBt)SFzwc.^͛7kӦMڴi"""t9yxxX$ M:UFR/ÇƍfI缙3gjȑҥ}]:-+g1͘1C|Ξ= `< XÇk֭5jTåKLտmܸQ_~*WlvMƍӆ Կq .(00Pj-YDʖ-Eyҥe6lؐq 8P'Oԙ3g2\@GX*[|||4uTrrr2;sNj]?>ݻXFƏ=ǯq(gggjJuԱt8VΝ;zd2Զm[_ J*Z|ʕ+ *hȐ!y[…*UO>y7իV0׺uk 6,ٳgk֬YYjTRԩ$gUFr$!X Sl55yVt;3dvt位7jرV0 _(TP ۷g$rիm;;; F9ҥKӧ- `@RV-r||u릐L`=رcTLLwﮰ0K+A5Jݻw7w uEљjQ` tȇկ_?ٳҡ+666W&M8p@PT(bbb,۷o{ZtC! q4>`} *UV'0ۿxb}嗏idcckՋLƏvZO76߾}[ӧOW˖-Uti.\X^^^/kֆdճgOUREΪP 7ofM&Զm[,YRP~dջwoUREEgjh-^Xڵ3ք TJ-ZT/"""|)m߾]ÇWUti9::L2z'̙3=wf߹d2}>?q]}wzUB999tkS_;wVJTP!~3f.\a׮]ӷ~g}V_|:tjժ%M6ںuk6"""4c h7ҠA͛7/0..N6mҐ!CTR%]tIgŊСʖ-B 驧ӕ7nA=7Kudү^xAeʔ}-Ppaʺ2>6 \QlY]V...f?#mܸ1_6mR۶mӭWX1iڵIݻw`Cر,X`Lf͚9r)44T:~>CSSNӋ/%Kܹs˗5sL5o\nFGGWz͛X]pA ,г>7|3$VrN>o^ҥKU^=M0!չW2eꫯjӦMJLLԝ;wԶm[7NΝݻw~z͘1#טҹsԺuk=:x>#mڴI6mRuQM8Q6lR/ĉ'!C;q&Ox%iݺuY֭[{O6lІ ԭ[7ݻWcǎ֬YHgթSG5khӦM4h?I&KsIuӧ5i$5kL˗{ァ۷+&&F ԠAՉ'tݼySjӦEF6lvi9x?8W+ԫW/]v9s.^M6Ն tuȑ#9r^yT(QBk׮Չ'ߥKfʕfƪcǎݻ"##wi?5j(cz{lS~WuH&%%W^ڻwW6նm[5h@f#;U:uTNլY3ݾnܸ0ժU+UBg޼y>>z7նm[]pA3f 6lPxq/^<>J*%OOlŗ ^Ӑ!C7ߘkժ5aWk֬Qv@uI۷W@@/3ϨFzu] 4Hvvvz׍:ǏJ,$ctt:uhԨQjݺʗ/` 6L.\PRRz-kN|VXѣGyruuՉ'4vX F:諯2.]%MKTʕ+gxթS'c˗5uT5kLjՒ#9kɉ{زeK5iDoy={ԇ~@uMuUǎ(777ٳ7+4w\IRv̒o^ x侐{ b㏜K+ɭTZNHHZjf޽K_ui۷o_cʕ+:{l9sF˖-g}%K/JvNĉ֭[; ^2\'4+~0o/ꩧR:t1ȑ#j֬)???zjcDv^z뭷7x#$t:g}(_zU֭{~G7ogϞ*QDulmmաC|=-^8Uŋ%C}Y͞=[ JUqƪQQ իB fӯ?lꜼe˖5+:thz%JPn݌={FAgGXXFqddC1"Pp04\slOEUb09Kx{{[: oFgΜn[zƍ溳2UbŇMVC릜6688,ٜ///lJӧOuEI^b{1*8u6lhV޽{w:)믿n6=8zVZe >>FBݻҥg֭5˩YVŋ֭[p؅ mwwT'׮]3ݻj֬iVqFŔ#U,yo5#q_ջwoc=իWkƍ1b>c-ZQҘ`u~iwYØccccNΝ;vTTTK9 uVǏR7?}ky_֎;TF c_ll&O5kjԣGM0l_``Fek&_;74y6nv(Q؎ו+W2u^"E,uk5jÇkĉfS_rEݺuo. `;vk믿֢E,Qol'8tځYnݻXTTɬ|СL|䰤<[WzoURގΈa^pvv֘1ct99RƱ~! 0(8Hܹs3ϘۼyɟŒN 6׮]~LcV޾}{λ|͛dXJ~_G} ?s[}'77X憰0k.~777M6MV:uӧO;$ƒx Y:QV>!^Y)?4έ\Yg5;tPfӧկ_?9kz^~e999_~E&)ܹ .+%|.]~A}y~M&{sN}4hP YLF6o߾#kԨ7Ӗ߹sG9( |A#唼u5K%_w1rfeŊ+0aٚM6ՓO>iO=s֬Y}?4[8+tb6u-[4|t{cǎzWUfl,u"Pֲez\rjݺQf0 _x0 ܹs:f͚Z|2}NTۧ;ws%,|ڵcVN/2ޚ)̨1112e$ii+VPFn:S_ &qƊԬYR2 !+&M$OOO|X/\\\!WWW-[VƍSLLTz ᯿Jrqqի!I:sfѣz%Io>ㇶ{Aݻs,/B 40۷l25nXŊSʕUpayyyi޼yz'b Qv4 >\:t0۷uVE+WVѢEӧpZr@XXnܸa{S {3*e{xUrrgϞ5+߼y3z-Z0CCCտݻW֭S-R% 2$̈́={k.Iĉ`Ə?Af<٩F!P`UP!R%87ѣG%ݟ"[nɦ4h@Ӯ]#֭Zn߾={Hbcc~z?8JC ŋi%ɤ ޽{:|~G͙3Ǭ'O\rJLLt1ٳG&LPXXٽ(^*;vLVرc͒QGTD csg*&&Fqu]]pA+WԀtMѣGkiF.[6m+VH:w[%V~Tٽ{ׁf͚iz駍:[SҥÇI&iߘ,%=zs `90`0ɭd οX]`ek$L2.   ׺ur߾}SN +A `$J[:իWS;|} " @ڵkL\P14X `%H  VeooyEv IDAT6mژaPX"վ[ @AFP\Tx) X `L ןi}||`y0V&))I.\0+ +A `$J+A9d2)..aX-GGGX: H `L\\V^m0VNd0 ML VX-ZԬ ݻw[:ѴiSK" qttT^,,̙3ڽ{^$+WNM6U-l02g}ڱcG}||wߩVZyF4dL&իWO+WVLL>+WHO5iD7o#l-gzӧOZb֯_K.iѢE*Z$ݻzWg&Xɤ(c2,rAXXɓ'+ @UT1;nccW^yEg6={V[lP!X8NSLQ߾}n^zTRFرc b `|&))IAAAZzuumllTZ5I * rMݺue0'&&F[n5O?-r6nܘo6ׯ!xL04%IM6UӦM-\E믺wJ.e$ԕ+W4fyzz*00PժUtHrkXSN[n{֮]ڵk[:$yV$**JSLQ tEFFQFz뭷tmK 1@=qvayPPPfΜ_~%U711Qg֭[+Z(J0s4hիڻwvޭ$ӧճgOرCL X#VYfj֬پ}wʕ+յk<@^  W(*V~IO=Fa_HV`em0yn:Jk&w(ll߸qÂM$ Y00@nl-[ւM 7r֭[qp=c;h`օ0V&11Q{1>>>$Ǐ=z`$rS@˗/$ծ][/[HCׯ_u ;wN?4o<ڒ"S@P[:|qoz'!{{{ k `|J*R! qttTN,rtttP$2666rrrt `w8;;k`+`jϟ?o=<<,?rHς-}e o2,?r#2111/- 0 `$J0ʢݟŢ .Sݢ{xxX+A `$J+ao@βYP0888}`L V0rd… ժU+թSGUVU_(<<V)bcc+ɤYff͚7_֤I믿C IHHО={OBBC@7ydkڵFWڵk***J={Եk,%`H`e4mٲEoFc<$Ν;Zre^XUaaaJHHO?jbŊwXɤ(Ka\\\dccc01c?̎۵jC+Z: 5w\QȘ<}vIO`f]tI7Hu#KCjѢ͛ *d+nj5`I^^^zWﯰ,7~xK맢EY_:u˗kǎZdz葥lllԣG?^~5jK*Ur@tYǏ˗jڴi*RHۊЏ?hxt?HY޽{vɒ%37wUw Y (" " .- mk|[T[kGomiujtkǪS LۺPiJmu RY,K; Yp!&7y~w¹yN/bٲevEؽ5tuoYYYDDuQq>IXzuDDt%Ǝq:+WFUUUs{Z^jU}_ѥKx'Oo󣢢"""ZW!R֮]ڵkFnݺݯϧ>8 /L}w_DD}qw6&mNh^UUU1}Tk,f@[}vaaaF㊊Ruuue˖ˋGy$8 .Gqqq?sƭ73.0`1I͛p Q_AAAvmկ~5yXdIW\?O[nMK؏0@sδv~~fO⃽#\8qbL8q  `n+y3U]]ܹss`Ivޝv+g>`IvڕZˋ]6WJ@)tpNke4"ܳgό B#T{֭ۻ<|f h<`SN1jԨTsZ$/q мcĈim8I&Eiiil۶-zXlY :OZZ,/qn) @{͛7逸J=㎋rL Ʈ]Rm۶erK,^8Ə8p`ׯޘ7tS$Izk<ñnݺ} ]w˖-#8"̙#Gl2 96.]js1ѩS,fZS]6N>ٱcGL>=Ozmڴiq78k_Z\|1};wn\~QYY=zQFԩSk(av +V4{Ȫ1cF}Fqq 2 2$$iÆ {Eb-Gg;jMwA/ @?>)ЊcJKKv94)4HrL^^^e; -r0@p h:uv ׿!b^;`:!0 iqᇧڃΝ;7vh{'4`c+Lk1(@Ϗ!-la> 9B Gx0ݻw̙3S.,ErL]]]X" @9B G(` #r0@P 6mZxqѣ㢋.z+۩AN(v@ˋ~-xWxp 1gΜ?!`1ѧOl֯_{vw\p~bΜ9,`1uuu~TO>"y뭷bq1d9}> {1cFkN vh,\0^ذaCwqyS!;w|;},6o۶mk'|2.زeK\q>&Oƍ.,͛~,]4&LoK-s=1cƌ(--x衇sqE0@;$ITTTҥKcΜ91u:thr-QUUx6l}sQ[[=z֧0(,,իWW\q{Ǐ;wN[aŤIb9ک|0zÇO}S1g?zjRo=#"[n0`@|>fϞ`̽ sFD/~F_ Sh>Ook֬_w}wtڵѱ***G?Q?eʔۿ5دgϞCۧGQSS/rF+v4Mݣ{뗿eTVVFDcƌ9`ǧ_xxwcРA;#S R֭kT@:WR'pA#{E;vlj'IZn6tT … SÇh1Z\oO?=vmSbٲe=-X>}:+""/^\oիWGDI'G}tF \2̙ކgϞUV5k_ZDD׿wO?v[FVUXX^xaZdڵi]f4n~֭ԩ~>_?bԩ1tԺ<^~}40N:ʼn'4hGoߞr]]]lٲGG?.]駟]w] 4(^yx;M EkpDDeee} ?A,Y$^xXfM7.oda:;w3$IZ{7cc=6F @JLUWW;wc7M=iҤo@TRRҤqwNkw֭90xR;O*++kҸ]vk׮͕Dn <8]WWѸrϞ=3~v0r\ @~nŊؼysr(..!CZ/Gydu֌]>|x40—l5w_DD$ILh:uF DDDyyyF֬YZVuiZ~7ݱdɒT{̘1-8 gZ^pA/^8jjjRs9EG4iRFD[o˖-;`'x"=վkն@vƮ]Rm۶e=Ν_~yTVVF=bԨQ1uԸkvh& Ԑ!C"I=lذ{Z$6rC G4$I*...,f={볝FٳgSh0䘪wj|ѹs,fݻw  G(` #r0@P2 IDAT 9B G(䈂l'40aBZ'c-q4@PYZ$YCUWWkQYsuuu2PZ}?i{<rLMMM<駟|Z^^^^ݻSm߾=JJJM}vQQ+ -sύѣGg; pjjjgIǍ @)--T{֭ѻwoEv$IغukkYʆr6fAuuuYƳډ$Ib͚5¿,KX.t9 ۶me˖EYYYt- }D$QWW|`+///|s0&///:u4 gy0@Pn 98n60䘼ܹs  G(`c*++{^}{&0@ 92)n #r0@P 9B Gd; wTUU~,e[UUU_>~78o`Nh6VJk_xYӳ@VZ'tR)n 4͛7g;Q[h~ @ٺukS@9SڳgώQF5[+2?)<@^cǜ[2v{Nc=Æ k@fZG.ȥO[ߗlךomDiXnr{%".?my_ubq%8t @)++Kk5*Flv=mc-=漯aÆh|~or%[z[-r6l۲\޴}v~˥O[ޗlf~i8tn #r0@PN SW^yeL0! .1疌s˥O[ߗlךomDl>9nKߛ/ί~7rӖ%۹_Z?&-#/I$Ik3&^hQ=:A`~%_Z[@` #r0@(v@ݻwL6- >Ex -$I-r0@P 9B G(` #r0@P 9 _MMMO FEEE'FЇ"///@lٲ'pB\MQYY/B,^8n ;.N:flk瞋e˖Ν;#cȑ#CבM6ų>+WLj#͛77:Ʈ]n~Ź=P<=N>;vl<-=1uݻw\tE1{xꩧ+_J5*<\84iXjU\~ѧOկ~O>d\uU1tИ4iR[%My䬳J""ٳgO%?$":^,[,2eJRPPDD2mڴl4;3|O5vNw1c$ :4yR몫;3$///ۛy/h-myX`A2hР$"SO=5YlYjۓ/ID$EEEF @tycܹI=H?dݺuu7nL>O%%?x{uooj3*++#H̟??㬳JˋO<1?x꫱s8s5˗ 7W_}uQWW&L &d79{ク'?I <8֭[6mJo̜q83bɒ%QRR͋ѣG駟1o޼xgw|w 獥KYg^?͛L/**??.]Z7O>9o^x!;ضm[wq>?'?>l[cy Ж4h_WSXK3ΨUW]'xbDD}1gΜV:{//޽{N>:ݻw??o۸cMuWIJe""_r|߈GD_J @jFuuu\r%qƈ7{wҥKg?XjUuy".صkWDDu]ѥKˋ]v'?ؾ}}P^K47///To:~[,?`ߒ%KRk8묳:f̘1ѳgT9;@XvmbS]]]?=Z_{7fϞw۷B#{饗*>餓2wy0s=}ر{,FSbСCSGy$<h]ehJ̑#GFΝ({#FhܨQRuuu7k^qξe˖X`A@Fs|7cÆ ycݻw>}4^AAA ><՞7o^q0oaط… -'8g1{H$^~ݺu&HmO{76l˗/otݱcG,[,m05k֤Ѹ}m9{aaa+q:7l۷oOޯ_s1}ahj@1#` cinݺe4n~7nl=b֭v׮]3iy(&mC{7E{(D?}=IciϨ|r_KM@1h˼a.CΝ;EEE;-'`G8gZb02oP2VRR֮hܾ9;@ky\˼a.CXiiiZ2q7pz^__% s@j/h` c Hk{+ڵ+ݷof أ0zj{~ :7сb6y\0Ç׭[Ѹ}9r압e˖9g蘚{ڵk~vta6lX7د)1#` ccƌIk]6qk֬9`y8g1Zb{y_!f.hüQ\\Æ ktҥK}mks9iKf4n~yyyGsũڬy6ļ1Gh{ܰaCW*۩ &DN2);SGѸ_~9|'@[8C{QPPļ},o&1+++cѢEI2^捽c&I_ߎc7 @:uRcvؑ{@իW\pv&˗/Oՙsv%O<1N8F| eޘ8qbݻQ1͛Z...K.c2`Q/D^^^DD,[,^{gYYYL2s_bjg={vjyذaiZb;9s>?X̳>;? @vy(T.K+ G @1"TS˷v[XnK6mڔ~+sύ'FDDMMML>wLQ@{Ԗ)Sڸqcg?k0fyyy̚5+"}Q\7n0`@DD,XW>""JKKnko{:_% <@6s̘?~DDuY#tdM=yږ{,G$~={~bŊz}ݱ|s۴ɓ'7=a-DΝ#"[oM{nزeKDD|_qe3&?<~ UUUڈGuT1ە ֮]tIID${N}ٴ}z/wg?2dH <8z葥llܸQϙw7pAcr-Guuur)ѧOxcʕ1p6mZh˼bŊG8S,^}Xn]1"θ 3 @uyc…կ~5Ν]vSO=5 sTTTĩsOqlCsΘ7o^,^8ovX3&N?(,,vzZ -7o{.~صkWt=Ǝ=ļnݺ7o^,_~DD13'f͚g_^$I mܸ1G7M5ط&?رcGDDs91w܃ncvضm>3&֭[^)裏FAAA?֭B]Xpakoc !N7{K#8h^x!i.I3gΌ.>)qxg2nDS1nܸƭ_>>5)Gڎ$Ig?Y 4Bc aE=1`832՚cƌ;,q5s /+WO z10`ZٳSTFQ]jUZ*ViӦ8c'mìY""3L}S SE_Qߦ>uǎ1rHźv&~ȑ#Olc a @zW3Z#2/ 80xFE3wX~}L:S s0mWk0 dhu3gΌS@G @cǎX`A镚xSOaÆ5KL)&OT[c[nm֫S@G @gf͚q饗6[L)&֒8 i֭OYg h)#*v$I⋩?((Ƚ_Yꢼ<xʺ_QYY]vY(@ֽyT;WosgZ$Yʦ9sf_l1;1/ UoD&Lӟ4۩֭z*;޽{(Ⱥ秖cY̦e%\4ڌٳgGmmm|3iָ؛0Y՚G;,ٴ<Ϣf͊]6;1>`jÆ tTۭz;+Vċ/':L֎k֬Yqe5k\БYZ#W۶m[+iӦW^y%^|Ũc9&=ҥ-Z(?Ɔ b},zq>Z+q߿y1dȐ7ơ{O<֭AĉGQ^^O7nGEEE|_'FEEEF?) "///կ_x嗛-;.֭[Cꫯ'ڵkcΜ9q种={v :4fΜ}$I+N8!׿f{[hQ,Z(&O rL{qgĉ+n!v3gΌ+2v={; OlDDw /06l}z(>>f1pZIDAT4o]6R ⳟl!H K$"H&Oܤ8ӟR1FinݚID${~8㌤w~_5ӟt^hQҧOdСɦM]qID$SNovG>_n\$"3f~饗&C!:th|1nܸ;#mW_eeeq嗧ƍ^zi?b…ѫWnq}G?Ѹ[ƍ#HL:5MyyykjK>Gi>s m۶ַqeEΝ~??>綾^˗ǥ^~6UsSߚ ./">`3 }q߾}#"b͚5ND7yyyq7_ѣ}d;rߦMbǎQSS;wx{M7{쨮ݻG.] ;(++k<>яK/s̉;wFIIID)w}y֭[[$""N9ԩ~??jժ8skk}#"-$_M$vޝz}ΝGDȑ#a""bʕm۶(--=`3gΌ.}Қرc}("]>T o""LӦMsӞ|UW]uHOU]]N {{'|3:c#"*VXFJ[e˖կ~/be˖Xzu^4hPsZdI|UVŜ9s:f̘ذaCDA|$nݺsEiϞ=S/_<""__bƌ۷oGeeA 1{8c㤓N:~֧ pt~z?&Ms̉_|ߎ;v~ ؟[@>V˲`ٞ=?|}]P;wΝ;rL?k׮^Ν; ={̌32p̟??k׮_~>e]ٳgO>y3o޼#?ֹ4(\pAK/:&Izݕ򗿴ulٲ%rQ=\z뭌=copw$o߾$+|8< g{xjСCgܸq;wneܣݻw6nܘw}SSSKN.]2lذL2%ÇO>}?w>|x~2pbxό?OI3I2mڴ93C5nذ!ofFdڵGL[OM&In* 744d̘1 &9x$ ,… seeԨQό32w,Y$խgϞOGG{ɲe2xKwpX4k9>;6}׿8(޽{|y'$zk?I/վ|8-Z(=\d񩪪ʺu۷/{auYfK_RVX{3d˃ ʴiӲjժ[.ضm۲iӦOzJq7n\OgeO @Qf̘wygvڕ7~]]]>3oF^x$xܹs_z&OL:Ο?? ?}Ӗo9kkkzMĨjiiiI:6mڔ>/{466N-ܒomF 6?iV\}._W8̙33s 80'OΨQ9̙3'Jjkkӵk׌1"~{jjj$k֬ 7ܐM6Uܹs뗅 s׾YrޚwyYrqu:tho~ﱦ&?ه%˗/σ>ܹsN:\s5[`An̛7/Ǐ?؎e˖\tE^UUU۷oL[n%̛oYI/~17tSO|ׯ̙3/SNu]'K/ͺuңG##Fȑ#{رcs=fV^G>|xϟj~s|޽{SWWgy&s"PCV~:v2B ,Ȅ r}eر֭[);v͛jժ<ٲeKV^:ݮ?lذ\2[n,}Lkf9rd tcs{۶m˄ 2f̘z뭭u)ݻwO3x9묳O[~ϦA-#h8N444ri+hsqVXueܹinnN̝;7_}N> ^~W:p&N]v466fYxqvU9ܜիWo]wݕ_&MƍS[[n}3tTWWgԩ˳hѢvm'bTѓcf͚=:7oNSSS*}ٳ5b)˗/o~˩Nuuuٓ~K/ 7ܐ*'P{7 ,ȬYwޜy晙={SB(% !B( P0@!`B !B( P0@!`B !B( P0@!`B !B( P0@!`B !B( P0@!`B !B( P0@!`B !B( P0@!`B !B( P0@!`B !B( P0@!`B !B( P0@!`B !B( P0@!`B !B( P0@!`B !B( PM3XȃIENDB`unyt-3.0.4/pyproject.toml000066400000000000000000000056441476461141700154550ustar00rootroot00000000000000[build-system] requires = [ "setuptools>=61.2", "setuptools_scm[toml]>=7.0.1", ] build-backend = "setuptools.build_meta" [project] name = "unyt" description = "A package for handling numpy arrays with units" authors = [ { name = "The yt project", email = "yt-dev@python.org" }, ] classifiers = [ "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: BSD License", "Natural Language :: English", "Programming Language :: Python :: 3", "Programming Language :: Python :: Implementation :: CPython", ] keywords = [ "unyt", ] requires-python = ">=3.9" dependencies = [ "numpy>=1.19.3, <3.0", "sympy>=1.7", "packaging>=20.9", ] dynamic = [ "version", ] [project.readme] file = "README.rst" content-type = "text/x-rst" [project.license] text = "BSD-3-Clause" [project.urls] Homepage = "https://github.com/yt-project/unyt" Documentation = "https://unyt.readthedocs.io/en/stable/index.html" Changelog = "https://unyt.readthedocs.io/en/stable/history.html" [tool.setuptools] include-package-data = true zip-safe = false license-files = [ "LICENSE", ] [tool.setuptools.packages.find] include = [ "unyt", "unyt.tests", "unyt.tests.data", "unyt._mpl_array_converter", ] namespaces = false [tool.setuptools.package-data] unyt = [ "tests/data/old_json_registry.txt", ] [tool.black] exclude = ''' ( /( \.eggs | \.git | \.hg | \.mypy_cache | \.tox | \.venv | _build | buck-out | build | dist | _version.py ) ) ''' [tool.ruff] exclude = [ ".*/", "benchmarks/*.py", "paper/*.py", "*/_version.py", "*/__init__.py", ] [tool.ruff.lint] ignore = [ "E501", "B904", ] select = [ "E", "F", "W", "C4", # flake8-comprehensions "B", # flake8-bugbear "YTT", # flake8-2020 "I", # isort "UP", # pyupgrade "NPY", # numpy specific rules ] [tool.ruff.lint.isort] combine-as-imports = true [tool.setuptools_scm] write_to = "unyt/_version.py" version_scheme = "post-release" [tool.pytest.ini_options] addopts = "--ignore=benchmarks --ignore=paper --ignore=unyt/_mpl_array_converter --color=yes" filterwarnings = [ "error", "ignore:Matplotlib is currently using agg, which is a non-GUI backend, so cannot show the figure.:UserWarning", "ignore:FigureCanvasAgg is non-interactive, and thus cannot be shown:UserWarning", "ignore:distutils Version classes are deprecated. Use packaging.version instead.:DeprecationWarning", 'ignore:datetime\.datetime\.utcfromtimestamp\(\) is deprecated:DeprecationWarning', # https://github.com/dateutil/dateutil/pull/1285 'ignore:mpnumeric is deprecated:DeprecationWarning', # sympy 1.12 VS mpmath 1.4, solution: https://github.com/sympy/sympy/pull/25290 ] [tool.coverage.run] omit = [ "docs/*", "unyt/_version.py", "unyt/_on_demand_imports.py", "unyt/tests/test_linters.py", ] unyt-3.0.4/tox.ini000066400000000000000000000047141476461141700140510ustar00rootroot00000000000000[tox] envlist = py39-docs,begin,py39-dependencies,py39-versions,py{39,310,311,312,313},py39-unyt-module-test-function,end isolated_build = True [gh-actions] python = 3.9: py39, py39-docs, py39-dependencies, py39-versions, py39-unyt-module-test-function 3.10: py310 3.11: py311 3.12: py312 3.13: py313 [testenv] package = wheel wheel_build_env = .pkg setenv = PYTHONPATH = {toxinidir} MPLBACKEND = agg recreate = true depends = begin deps = pytest h5py !py39: pint !py39: astropy!=6.1.1 coverage[toml]>=5.0 pytest-cov pytest-doctestplus matplotlib!=3.5.0 docutils dask[array,diagnostics] commands = pytest --cov=unyt --cov-append --doctest-modules --doctest-plus --doctest-rst --basetemp={envtmpdir} coverage report --omit='.tox/*' [testenv:py39] # skip doctest on py39 because doctests require numpy>=2.0 and all optional deps, # but some of our optional deps (pint, astropy) don't have a version that support # both numpy>=2.0 and Python 3.9 commands= pytest --cov=unyt --cov-append --basetemp={envtmpdir} coverage report --omit='.tox/*' [testenv:py39-versions] deps = docutils pytest sympy==1.7 numpy==1.19.3 h5py==3.0.0 pint==0.9 astropy==4.0.4 matplotlib==3.3.3 coverage[toml] pytest-cov pytest-doctestplus dask[array,diagnostics]==2021.04.1 commands = # don't do doctests on old numpy versions pytest --cov=unyt --cov-append --basetemp={envtmpdir} coverage report --omit='.tox/*' [testenv:py39-dependencies] deps = docutils pytest coverage[toml] pytest-cov pytest-doctestplus depends = begin commands = # don't do doctests in rst files due to lack of way to specify optional # test dependencies there pytest --cov=unyt --cov-append --doctest-modules --doctest-plus --basetemp={envtmpdir} coverage report --omit='.tox/*' [testenv:py39-docs] allowlist_externals = make changedir = docs deps = pytest sphinx matplotlib!=3.5.0 dask[array,diagnostics] commands = make clean python -m sphinx -M html "." "_build" -W [testenv:py39-unyt-module-test-function] depends = py39 commands = python -c 'import unyt; unyt.test()' [testenv:begin] commands = coverage erase depends = skip_install = true deps = coverage[toml] [testenv:end] commands = coverage report --omit='.tox/*' coverage html --omit='.tox/*' skip_install = true depends = py{39,310,311,312} deps = coverage[toml] unyt-3.0.4/unyt/000077500000000000000000000000001476461141700135275ustar00rootroot00000000000000unyt-3.0.4/unyt/__init__.py000066400000000000000000000053011476461141700156370ustar00rootroot00000000000000""" The unyt package. Note that the symbols defined in :mod:`unyt.physical_constants` and :mod:`unyt.unit_symbols` are importable from this module. For example:: >>> from unyt import km, clight >>> print((km/clight).to('ns')) 3335.64095198152 ns In addition, the following functions and classes are importable from the top-level ``unyt`` namespace: * :func:`unyt.array.loadtxt` * :func:`unyt.array.savetxt` * :func:`unyt.test` * :func:`unyt.array.uconcatenate` * :func:`unyt.array.ucross` * :func:`unyt.array.udot` * :func:`unyt.array.uhstack` * :func:`unyt.array.uintersect1d` * :func:`unyt.array.unorm` * :func:`unyt.array.ustack` * :func:`unyt.array.uunion1d` * :func:`unyt.array.uvstack` * :class:`unyt.array.unyt_array` * :class:`unyt.array.unyt_quantity` * :func:`unyt.unit_object.define_unit` * :class:`unyt.unit_object.Unit` * :class:`unyt.unit_registry.UnitRegistry` * :class:`unyt.unit_systems.UnitSystem` * :func:`unyt.testing.assert_allclose_units` * :func:`unyt.array.allclose_units` * :func:`unyt.dimensions.accepts` * :func:`unyt.dimensions.returns` """ from unyt import physical_constants, unit_symbols from unyt.array import ( # NOQA: F401 allclose_units, loadtxt, savetxt, uconcatenate, ucross, udot, uhstack, uintersect1d, unorm, unyt_array, unyt_quantity, ustack, uunion1d, uvstack, ) from unyt.dimensions import accepts, returns # NOQA: F401 from unyt.testing import assert_allclose_units # NOQA: F401 from unyt.unit_object import Unit, define_unit # NOQA: F401 from unyt.unit_registry import UnitRegistry # NOQA: F401 from unyt.unit_systems import UnitSystem # NOQA: F401 from ._version import __version__ # function to only import quantities into this namespace # we go through the trouble of doing this instead of "import *" # to avoid including extraneous variables (e.g. floating point # constants used to *construct* a physical constant) in this namespace def import_units(module, namespace): """Import Unit objects from a module into a namespace""" for key, value in module.__dict__.items(): if key not in namespace and isinstance(value, (unyt_quantity, Unit)): namespace[key] = value import_units(physical_constants, globals()) import_units(unit_symbols, globals()) del import_units def test(): # pragma: no cover """Execute the unit tests on an installed copy of unyt. Note that this function requires pytest to run. If pytest is not installed this function will raise ImportError. """ import os import pytest pytest.main([os.path.dirname(os.path.abspath(__file__))]) # isort: off from unyt.mpl_interface import matplotlib_support matplotlib_support = matplotlib_support() unyt-3.0.4/unyt/_array_functions.py000066400000000000000000001067541476461141700174630ustar00rootroot00000000000000import warnings from importlib.metadata import version from numbers import Number import numpy as np from packaging.version import Version from unyt import delta_degC from unyt.array import NULL_UNIT, unyt_array, unyt_quantity from unyt.dimensions import temperature from unyt.exceptions import ( InvalidUnitOperation, UnitInconsistencyError, UnytError, ) NUMPY_VERSION = Version(version("numpy")) # Functions for which passing units doesn't make sense # bail out with NotImplemented (escalated to TypeError by numpy) _UNSUPPORTED_FUNCTIONS = { # Polynomials np.poly, np.polyadd, np.polyder, np.polydiv, np.polyfit, np.polyint, np.polymul, np.polysub, np.polyval, np.roots, np.vander, # datetime64 is not a sensible dtype for unyt_array np.datetime_as_string, np.busday_count, np.busday_offset, np.is_busday, # not clear how to approach np.piecewise, # astropy.units doesn't have a simple implementation either np.packbits, np.unpackbits, np.ix_, } _HANDLED_FUNCTIONS = {} def implements(numpy_function): """Register an __array_function__ implementation for unyt_array objects.""" # See NEP 18 https://numpy.org/neps/nep-0018-array-function-protocol.html def decorator(func): _HANDLED_FUNCTIONS[numpy_function] = func return func return decorator @implements(np.array2string) def array2string(a, *args, **kwargs): return ( np.array2string._implementation(a, *args, **kwargs) + f", units={str(a.units)!r}" ) def product_helper(a, b, out, func): prod_units = getattr(a, "units", NULL_UNIT) * getattr(b, "units", NULL_UNIT) if out is None: return func._implementation(np.asarray(a), np.asarray(b)) * prod_units res = func._implementation(np.asarray(a), np.asarray(b), out=np.asarray(out)) if getattr(out, "units", None) is not None: out.units = prod_units return unyt_array(res, prod_units, bypass_validation=True) @implements(np.dot) def dot(a, b, out=None): return product_helper(a, b, out, np.dot) @implements(np.vdot) def vdot(a, b): return np.vdot._implementation(np.asarray(a), np.asarray(b)) * ( getattr(a, "units", NULL_UNIT) * getattr(b, "units", NULL_UNIT) ) @implements(np.inner) def inner(a, b): return np.inner._implementation(np.asarray(a), np.asarray(b)) * ( getattr(a, "units", NULL_UNIT) * getattr(b, "units", NULL_UNIT) ) @implements(np.outer) def outer(a, b, out=None): return product_helper(a, b, out, np.outer) @implements(np.kron) def kron(a, b): return np.kron._implementation(np.asarray(a), np.asarray(b)) * ( getattr(a, "units", NULL_UNIT) * getattr(b, "units", NULL_UNIT) ) @implements(np.linalg.inv) def linalg_inv(a, *args, **kwargs): return np.linalg.inv._implementation(np.asarray(a), *args, **kwargs) / a.units @implements(np.linalg.tensorinv) def linalg_tensorinv(a, *args, **kwargs): return np.linalg.tensorinv._implementation(a, *args, **kwargs) / a.units @implements(np.linalg.pinv) def linalg_pinv(a, *args, **kwargs): return np.linalg.pinv._implementation(a, *args, **kwargs).view(np.ndarray) / a.units @implements(np.linalg.svd) def linalg_svd(a, full_matrices=True, compute_uv=True, *args, **kwargs): ret_units = a.units retv = np.linalg.svd._implementation( np.asarray(a), full_matrices, compute_uv, *args, **kwargs ) if compute_uv: u, s, vh = retv return (u, s * ret_units, vh) else: return retv * ret_units def _sanitize_range(_range, units): # helper function to histogram* functions ndim = len(units) if _range is None: return _range new_range = np.empty((ndim, 2)) for i in range(ndim): ilim = _range[2 * i : 2 * (i + 1)] imin, imax = ilim if not (hasattr(imin, "units") and hasattr(imax, "units")): if len(units) == 1: # allow range to be pure numerical scalars # for backward compatibility with unyt 2.9.5 # see https://github.com/yt-project/unyt/issues/465 imin *= units[0] imax *= units[0] else: raise TypeError( f"Elements of range must both have a 'units' attribute. Got {_range}" ) new_range[i] = imin.to_value(units[i]), imax.to_value(units[i]) return new_range.squeeze() def _histogram(a, *, bins=10, range=None, density=None, weights=None, normed=None): range = _sanitize_range(range, units=[getattr(a, "units", None)]) if NUMPY_VERSION >= Version("1.24"): counts, bins = np.histogram._implementation( np.asarray(a), bins=bins, range=range, density=density, weights=np.asarray(weights) if weights is not None else None, ) else: counts, bins = np.histogram._implementation( np.asarray(a), bins=bins, range=range, normed=normed, weights=np.asarray(weights) if weights is not None else None, density=density, ) # a and/or weights could have units, only apply if present # don't getattr(..., "units", NULL_UNIT) because e.g. we don't want # a unyt_array if weights are not a unyt_array and not density if density and hasattr(a, "units"): counts /= a.units if weights is not None and hasattr(weights, "units"): counts *= weights.units return counts, bins * getattr(a, "units", 1) if NUMPY_VERSION >= Version("1.24"): @implements(np.histogram) def histogram(a, bins=10, range=None, density=None, weights=None): return _histogram(a, bins=bins, range=range, density=density, weights=weights) else: @implements(np.histogram) def histogram(a, bins=10, range=None, normed=None, weights=None, density=None): return _histogram( a, bins=bins, range=range, normed=normed, weights=weights, density=density ) def _histogram2d(x, y, *, bins=10, range=None, density=None, weights=None, normed=None): range = _sanitize_range( range, units=[getattr(x, "units", None), getattr(y, "units", None)] ) if NUMPY_VERSION >= Version("1.24"): counts, xbins, ybins = np.histogram2d._implementation( np.asarray(x), np.asarray(y), bins=bins, range=range, density=density, weights=np.asarray(weights) if weights is not None else None, ) else: counts, xbins, ybins = np.histogram2d._implementation( np.asarray(x), np.asarray(y), bins=bins, range=range, normed=normed, weights=np.asarray(weights) if weights is not None else None, density=density, ) # x, y and/or weights could have units, only apply if present # don't getattr(..., "units", NULL_UNIT) because e.g. we don't want # a unyt_array if weights are not a unyt_array and not density if density: if hasattr(x, "units"): counts /= x.units if hasattr(y, "units"): counts /= y.units if weights is not None and hasattr(weights, "units"): counts *= weights.units return counts, xbins * getattr(x, "units", 1), ybins * getattr(y, "units", 1) if NUMPY_VERSION >= Version("1.24"): @implements(np.histogram2d) def histogram2d(x, y, bins=10, range=None, density=None, weights=None): return _histogram2d( x, y, bins=bins, range=range, density=density, weights=weights ) else: @implements(np.histogram2d) def histogram2d(x, y, bins=10, range=None, normed=None, weights=None, density=None): return _histogram2d( x, y, bins=bins, range=range, normed=normed, weights=weights, density=density, ) def _histogramdd( sample, *, bins=10, range=None, density=None, weights=None, normed=None ): range = _sanitize_range(range, units=[getattr(_, "units", None) for _ in sample]) if NUMPY_VERSION >= Version("1.24"): counts, bins = np.histogramdd._implementation( [np.asarray(_) for _ in sample], bins=bins, range=range, density=density, weights=np.asarray(weights) if weights is not None else None, ) else: counts, bins = np.histogramdd._implementation( [np.asarray(_) for _ in sample], bins=bins, range=range, normed=normed, weights=np.asarray(weights) if weights is not None else None, density=density, ) # sample(s) and/or weights could have units, only apply if present # don't getattr(..., "units", NULL_UNIT) because e.g. we don't want # a unyt_array if weights are not a unyt_array and not density if density: divider_units = 1 * NULL_UNIT for s in sample: if not hasattr(s, "units"): continue divider_units *= s.units if divider_units != NULL_UNIT: counts /= divider_units if weights is not None and hasattr(weights, "units"): counts *= weights.units return counts, tuple(_bin * getattr(s, "units", 1) for _bin, s in zip(bins, sample)) if NUMPY_VERSION >= Version("1.24"): @implements(np.histogramdd) def histogramdd(sample, bins=10, range=None, density=None, weights=None): return _histogramdd( sample, bins=bins, range=range, density=density, weights=weights ) else: @implements(np.histogramdd) def histogramdd( sample, bins=10, range=None, normed=None, weights=None, density=None ): return _histogramdd( sample, bins=bins, range=range, normed=normed, weights=weights, density=density, ) @implements(np.histogram_bin_edges) def histogram_bin_edges(a, *args, **kwargs): return ( np.histogram_bin_edges._implementation(np.asarray(a), *args, **kwargs) * a.units ) def get_units(objs): units = [] for sub in objs: if isinstance(sub, np.ndarray): units.append(getattr(sub, "units", NULL_UNIT)) elif isinstance(sub, Number): units.append(NULL_UNIT) else: units.extend(get_units(sub)) return units def _validate_units_consistency(objs): """ Return unique units or raise UnitInconsistencyError if units are mixed. """ # NOTE: we cannot validate that all arrays are unyt_arrays # by using this as a guard clause in unyt_array.__array_function__ # because it's already a necessary condition for numpy to use our # custom implementations units = get_units(objs) # filtering unique units using equality comparison implicitly allows for units # which have a dimensionless**(n) component to be treated as one and the same unique_units = [units[0], *(u for u in units if u != units[0])] if len(unique_units) == 1: return units[0] else: raise UnitInconsistencyError(*units) def _validate_units_consistency_v2(ref_units, *args) -> None: """ raise UnitInconsistencyError if units are mixed if all args are pure numbers, they are treated as having ref_units, otherwise they are treated as dimensionless """ if all(isinstance(_, Number) for _ in args): return else: _validate_units_consistency((1 * ref_units, *args)) @implements(np.concatenate) def concatenate(arrs, /, axis=0, out=None, *args, **kwargs): ret_units = _validate_units_consistency(arrs) if out is not None: out_view = np.asarray(out) else: out_view = out res = np.concatenate._implementation( [np.asarray(_) for _ in arrs], axis, out_view, *args, **kwargs ) if getattr(out, "units", None) is not None: out.units = ret_units return unyt_array(res, ret_units, bypass_validation=True) @implements(np.cross) def cross(a, b, *args, **kwargs): prod_units = getattr(a, "units", NULL_UNIT) * getattr(b, "units", NULL_UNIT) return ( np.cross._implementation(np.asarray(a), np.asarray(b), *args, **kwargs) * prod_units ) @implements(np.intersect1d) def intersect1d(ar1, ar2, assume_unique=False, return_indices=False): _validate_units_consistency((ar1, ar2)) retv = np.intersect1d._implementation( np.asarray(ar1), np.asarray(ar2), assume_unique=assume_unique, return_indices=return_indices, ) if return_indices: return retv else: return retv * ar1.units @implements(np.union1d) def union1d(ar1, ar2): _validate_units_consistency((ar1, ar2)) return np.union1d._implementation(np.asarray(ar1), np.asarray(ar2)) * ar1.units @implements(np.linalg.norm) def norm(x, *args, **kwargs): return np.linalg.norm._implementation(np.asarray(x), *args, **kwargs) * x.units @implements(np.vstack) def vstack(tup, **kwargs): ret_units = _validate_units_consistency(tup) return np.vstack._implementation([np.asarray(_) for _ in tup], **kwargs) * ret_units @implements(np.hstack) def hstack(tup, **kwargs): ret_units = _validate_units_consistency(tup) return np.vstack._implementation([np.asarray(_) for _ in tup], **kwargs) * ret_units @implements(np.dstack) def dstack(tup): ret_units = _validate_units_consistency(tup) return np.dstack._implementation([np.asarray(_) for _ in tup]) * ret_units @implements(np.column_stack) def column_stack(tup): ret_units = _validate_units_consistency(tup) return np.column_stack._implementation([np.asarray(_) for _ in tup]) * ret_units @implements(np.stack) def stack(arrays, axis=0, out=None, **kwargs): ret_units = _validate_units_consistency(arrays) if out is None: return ( np.stack._implementation([np.asarray(_) for _ in arrays], axis=axis) * ret_units ) res = np.stack._implementation( [np.asarray(_) for _ in arrays], axis=axis, out=np.asarray(out), **kwargs ) if getattr(out, "units", None) is not None: out.units = ret_units return unyt_array(res, ret_units, bypass_validation=True) @implements(np.around) def around(a, decimals=0, out=None): ret_units = a.units if out is None: return np.around._implementation(np.asarray(a), decimals=decimals) * ret_units res = np.around._implementation( np.asarray(a), decimals=decimals, out=np.asarray(out) ) if getattr(out, "units", None) is not None: out.units = ret_units return unyt_array(res, ret_units, bypass_validation=True) @implements(np.block) def block(arrays): ret_units = _validate_units_consistency(arrays) return np.block._implementation(arrays) * ret_units @implements(np.fft.fft) def fft_fft(a, *args, **kwargs): return np.fft.fft._implementation(np.asarray(a), *args, **kwargs) * a.units @implements(np.fft.fft2) def fft_fft2(a, *args, **kwargs): return np.fft.fft2._implementation(np.asarray(a), *args, **kwargs) * a.units @implements(np.fft.fftn) def fft_fftn(a, *args, **kwargs): return np.fft.fftn._implementation(np.asarray(a), *args, **kwargs) * a.units @implements(np.fft.hfft) def fft_hfft(a, *args, **kwargs): return np.fft.hfft._implementation(np.asarray(a), *args, **kwargs) * a.units @implements(np.fft.rfft) def fft_rfft(a, *args, **kwargs): return np.fft.rfft._implementation(np.asarray(a), *args, **kwargs) * a.units @implements(np.fft.rfft2) def fft_rfft2(a, *args, **kwargs): return np.fft.rfft2._implementation(np.asarray(a), *args, **kwargs) * a.units @implements(np.fft.rfftn) def fft_rfftn(a, *args, **kwargs): return np.fft.rfftn._implementation(np.asarray(a), *args, **kwargs) * a.units @implements(np.fft.ifft) def fft_ifft(a, *args, **kwargs): return np.fft.ifft._implementation(np.asarray(a), *args, **kwargs) * a.units @implements(np.fft.ifft2) def fft_ifft2(a, *args, **kwargs): return np.fft.ifft2._implementation(np.asarray(a), *args, **kwargs) * a.units @implements(np.fft.ifftn) def fft_ifftn(a, *args, **kwargs): return np.fft.ifftn._implementation(np.asarray(a), *args, **kwargs) * a.units @implements(np.fft.ihfft) def fft_ihfft(a, *args, **kwargs): return np.fft.ihfft._implementation(np.asarray(a), *args, **kwargs) * a.units @implements(np.fft.irfft) def fft_irfft(a, *args, **kwargs): return np.fft.irfft._implementation(np.asarray(a), *args, **kwargs) * a.units @implements(np.fft.irfft2) def fft_irfft2(a, *args, **kwargs): return np.fft.irfft2._implementation(np.asarray(a), *args, **kwargs) * a.units @implements(np.fft.irfftn) def fft_irfftn(a, *args, **kwargs): return np.fft.irfftn._implementation(np.asarray(a), *args, **kwargs) * a.units @implements(np.fft.fftshift) def fft_fftshift(x, *args, **kwargs): return np.fft.fftshift._implementation(np.asarray(x), *args, **kwargs) * x.units @implements(np.fft.ifftshift) def fft_ifftshift(x, *args, **kwargs): return np.fft.ifftshift._implementation(np.asarray(x), *args, **kwargs) * x.units @implements(np.sort_complex) def sort_complex(a): return np.sort_complex._implementation(np.asarray(a)) * a.units def _array_comp_helper(a, b): au = getattr(a, "units", NULL_UNIT) bu = getattr(b, "units", NULL_UNIT) if bu != au and au != NULL_UNIT and bu != NULL_UNIT: b = b.in_units(au) elif bu == NULL_UNIT: b = np.array(b) * au elif au == NULL_UNIT: a = np.array(a) * bu return a, b @implements(np.isclose) def isclose(a, b, *args, **kwargs): a, b = _array_comp_helper(a, b) return np.isclose._implementation(np.asarray(a), np.asarray(b), *args, **kwargs) @implements(np.allclose) def allclose(a, b, *args, **kwargs): a, b = _array_comp_helper(a, b) return np.allclose._implementation(np.asarray(a), np.asarray(b), *args, **kwargs) @implements(np.array_equal) def array_equal(a1, a2, *args, **kwargs) -> bool: u1 = getattr(a1, "units", NULL_UNIT) u2 = getattr(a2, "units", NULL_UNIT) if u2 != u1: return False return np.array_equal._implementation( np.asarray(a1), np.asarray(a2), *args, **kwargs ) @implements(np.array_equiv) def array_equiv(a1, a2, *args, **kwargs) -> bool: u1 = getattr(a1, "units", NULL_UNIT) u2 = getattr(a2, "units", NULL_UNIT) if u2 != u1: return False return np.array_equiv._implementation( np.asarray(a1), np.asarray(a2), *args, **kwargs ) def _linspace( start, stop, num=50, endpoint=True, retstep=False, dtype=None, axis=0, *, device=None, ): _validate_units_consistency((start, stop)) kwargs = { "num": num, "endpoint": endpoint, "retstep": retstep, "dtype": dtype, "axis": axis, } if NUMPY_VERSION >= Version("2.1.0.dev0"): kwargs["device"] = device result = np.linspace._implementation(np.asarray(start), np.asarray(stop), **kwargs) if retstep: return result[0] * start.units, result[1] * start.units else: return result * start.units if NUMPY_VERSION >= Version("2.0.0.dev0"): @implements(np.linspace) def linspace( start, stop, num=50, endpoint=True, retstep=False, dtype=None, axis=0, *, device=None, ): return _linspace( start, stop, num=num, endpoint=endpoint, retstep=retstep, dtype=dtype, axis=axis, device=device, ) else: @implements(np.linspace) def linspace(start, stop, num=50, endpoint=True, retstep=False, dtype=None, axis=0): return _linspace( start, stop, num=num, endpoint=endpoint, retstep=retstep, dtype=dtype, axis=axis, ) @implements(np.logspace) def logspace(start, stop, num=50, endpoint=True, base=10, dtype=None, axis=0): startu = getattr(start, "units", NULL_UNIT) stopu = getattr(stop, "units", NULL_UNIT) if startu != NULL_UNIT or stopu != NULL_UNIT: raise TypeError( "The first two arguments to numpy.logspace must be dimensionless, " f"got units={startu} (arg1) and units={stopu} (arg2). If output with" " units is desired, apply the units to the `base` kwarg." ) result = np.logspace._implementation( np.asarray(start), np.asarray(stop), num=num, endpoint=endpoint, base=np.asarray(base), dtype=dtype, axis=axis, ) return result * getattr(base, "units", NULL_UNIT) @implements(np.geomspace) def geomspace(start, stop, *args, **kwargs): _validate_units_consistency((start, stop)) return ( np.geomspace._implementation( np.asarray(start), np.asarray(stop), *args, **kwargs ) * start.units ) @implements(np.copyto) def copyto(dst, src, *args, **kwargs): # note that np.copyto is heavily used internally # in numpy, and it may be used with fundamental datatypes, # so we don't attempt to pass ndarray views to keep generality np.copyto._implementation(dst, src, *args, **kwargs) if getattr(dst, "units", None) is not None: dst.units = getattr(src, "units", dst.units) @implements(np.prod) def prod(a, *args, **kwargs): res = np.prod._implementation(np.asarray(a), *args, **kwargs) return res * a.units ** (a.size // res.size) @implements(np.var) def var(a, *args, **kwargs): return np.var._implementation(np.asarray(a), *args, **kwargs) * a.units**2 @implements(np.trace) def trace(a, *args, **kwargs): return np.trace._implementation(np.asarray(a), *args, **kwargs) * a.units @implements(np.percentile) def percentile(a, *args, **kwargs): return np.percentile._implementation(np.asarray(a), *args, **kwargs) * a.units @implements(np.quantile) def quantile(a, *args, **kwargs): return np.quantile._implementation(np.asarray(a), *args, **kwargs) * a.units @implements(np.nanpercentile) def nanpercentile(a, *args, **kwargs): return np.nanpercentile._implementation(np.asarray(a), *args, **kwargs) * a.units @implements(np.nanquantile) def nanquantile(a, *args, **kwargs): return np.nanquantile._implementation(np.asarray(a), *args, **kwargs) * a.units @implements(np.linalg.det) def linalg_det(a, *args, **kwargs): return ( np.linalg.det._implementation(np.asarray(a), *args, **kwargs) * a.units ** (a.shape[0]) ) @implements(np.linalg.lstsq) def linalg_lstsq(a, b, *args, **kwargs): x, residuals, rank, s = np.linalg.lstsq._implementation( np.asarray(a), np.asarray(b), *args, **kwargs ) au = getattr(a, "units", NULL_UNIT) bu = getattr(b, "units", NULL_UNIT) return (x * bu / au, residuals * bu / au, rank, s * au) @implements(np.linalg.solve) def linalg_solve(a, b, *args, **kwargs): au = getattr(a, "units", NULL_UNIT) bu = getattr(b, "units", NULL_UNIT) return ( np.linalg.solve._implementation(np.asarray(a), np.asarray(b), *args, **kwargs) * bu / au ) @implements(np.linalg.tensorsolve) def linalg_tensorsolve(a, b, *args, **kwargs): au = getattr(a, "units", NULL_UNIT) bu = getattr(b, "units", NULL_UNIT) return ( np.linalg.tensorsolve._implementation( np.asarray(a), np.asarray(b), *args, **kwargs ) * bu / au ) @implements(np.linalg.eig) def linalg_eig(a, *args, **kwargs): ret_units = a.units w, v = np.linalg.eig._implementation(np.asarray(a), *args, **kwargs) return w * ret_units, v @implements(np.linalg.eigh) def linalg_eigh(a, *args, **kwargs): ret_units = a.units w, v = np.linalg.eigh._implementation(np.asarray(a), *args, **kwargs) return w * ret_units, v @implements(np.linalg.eigvals) def linalg_eigvals(a, *args, **kwargs): return np.linalg.eigvals._implementation(np.asarray(a), *args, **kwargs) * a.units @implements(np.linalg.eigvalsh) def linalg_eigvalsh(a, *args, **kwargs): return np.linalg.eigvalsh._implementation(np.asarray(a), *args, **kwargs) * a.units @implements(np.savetxt) def savetxt(fname, X, *args, **kwargs): warnings.warn( "numpy.savetxt does not preserve units, " "and will only save the raw numerical data from the unyt_array object.\n" "If this is the intended behaviour, call `numpy.savetxt(file, arr.d)` " "to silence this warning.\n" "If you want to preserve units, use `unyt.savetxt` " "(and `unyt.loadtxt`) instead.", stacklevel=4, ) return np.savetxt._implementation(fname, np.asarray(X), *args, **kwargs) @implements(np.apply_over_axes) def apply_over_axes(func, a, axes): res = func(a, axes[0]) if len(axes) > 1: # this function is recursive by nature, # here we intentionally do not call the base _implementation return np.apply_over_axes(func, res, axes[1:]) else: return res def diff_helper(func, arr, *args, **kwargs): u = getattr(arr, "units", NULL_UNIT) if u.dimensions is temperature: if u.base_offset: raise InvalidUnitOperation( "Quantities with units of Fahrenheit or Celsius " "cannot be multiplied, divided, subtracted or added." ) ret_units = delta_degC else: ret_units = u return func._implementation(np.asarray(arr), *args, **kwargs) * ret_units @implements(np.diff) def diff(a, *args, **kwargs): return diff_helper(np.diff, a, *args, **kwargs) @implements(np.ediff1d) def ediff1d(ary, *args, **kwargs): return diff_helper(np.ediff1d, ary, *args, **kwargs) @implements(np.ptp) def ptp(a, *args, **kwargs): return diff_helper(np.ptp, a, *args, **kwargs) @implements(np.cumprod) def cumprod(a, *args, **kwargs): raise UnytError( "numpy.cumprod (and other cumulative product function) cannot be used " "with a unyt_array as all return elements should (but cannot) " "have different units." ) if NUMPY_VERSION >= Version("2.1.0.dev0"): @implements(np.cumulative_prod) def cumulative_prod(x, /, *args, **kwargs): return cumprod(x, *args, **kwargs) @implements(np.pad) def pad(array, *args, **kwargs): return np.pad._implementation(np.asarray(array), *args, **kwargs) * array.units @implements(np.choose) def choose(a, choices, out=None, *args, **kwargs): if (au := getattr(a, "units", NULL_UNIT)) != NULL_UNIT: raise TypeError( f"The first argument to numpy.choose must be dimensionless, got units={au}" ) retu = _validate_units_consistency(choices) if out is None: return ( np.choose._implementation( a, [np.asarray(c) for c in choices], *args, **kwargs ) * retu ) res = np.choose._implementation( a, [np.asarray(c) for c in choices], *args, out=np.asarray(out), **kwargs, ) if getattr(out, "units", None) is not None: out.units = retu return unyt_array(res, retu, bypass_validation=True) @implements(np.fill_diagonal) def fill_diagonal(a, val, *args, **kwargs) -> None: _validate_units_consistency_v2(a.units, val) np.fill_diagonal._implementation(np.asarray(a), val, *args, **kwargs) @implements(np.insert) def insert(arr, obj, values, *args, **kwargs): _validate_units_consistency_v2(arr.units, values) return ( np.insert._implementation( np.asarray(arr), obj, np.asarray(values), *args, **kwargs ) * arr.units ) @implements(np.isin) def isin(element, test_elements, *args, **kwargs): _validate_units_consistency((element, test_elements)) return np.isin._implementation( np.asarray(element), np.asarray(test_elements), *args, **kwargs ) @implements(np.place) def place(arr, mask, vals, *args, **kwargs) -> None: _validate_units_consistency_v2(arr.units, vals) np.place._implementation(np.asarray(arr), mask, np.asarray(vals), *args, **kwargs) @implements(np.put) def put(a, ind, v, *args, **kwargs) -> None: _validate_units_consistency_v2(a.units, v) np.put._implementation(np.asarray(a), ind, np.asarray(v)) @implements(np.put_along_axis) def put_along_axis(arr, indices, values, axis, *args, **kwargs) -> None: _validate_units_consistency_v2(arr.units, values) np.put_along_axis._implementation( np.asarray(arr), indices, np.asarray(values), axis, *args, **kwargs ) @implements(np.putmask) def putmask(a, mask, values, *args, **kwargs) -> None: _validate_units_consistency_v2(a.units, values) np.putmask._implementation(np.asarray(a), mask, np.asarray(values), *args, **kwargs) @implements(np.searchsorted) def searchsorted(a, v, *args, **kwargs): _validate_units_consistency_v2(a.units, v) return np.searchsorted._implementation( np.asarray(a), np.asarray(v), *args, **kwargs ) @implements(np.select) def select(condlist, choicelist, default=0, *args, **kwargs): ref_units = choicelist[0].units _validate_units_consistency_v2(ref_units, choicelist, default) return ( np.select._implementation( condlist, [np.asarray(c) for c in choicelist], default ) * ref_units ) @implements(np.setdiff1d) def setdiff1d(ar1, ar2, *args, **kwargs): retu = _validate_units_consistency((ar1, ar2)) return ( np.setdiff1d._implementation(np.asarray(ar1), np.asarray(ar2), *args, **kwargs) * retu ) @implements(np.sinc) def sinc(x, *args, **kwargs): # this implementation becomes necessary after implementing where # we *want* this one to ignore units return np.sinc._implementation(np.asarray(x), *args, **kwargs) def clip_impl(a, a_min, a_max, out=None, *args, **kwargs): _validate_units_consistency_v2(a.units, a_min, a_max) if out is None: return ( np.clip._implementation( np.asarray(a), np.asarray(a_min), np.asarray(a_max), *args, **kwargs ) * a.units ) res = ( np.clip._implementation( np.asarray(a), np.asarray(a_min), np.asarray(a_max), *args, out=np.asarray(out), **kwargs, ) * a.units ) if getattr(out, "units", None) is not None: out.units = a.units return unyt_array(res, a.units, bypass_validation=True) if NUMPY_VERSION >= Version("2.1.0.dev0"): from numpy._globals import _NoValue @implements(np.clip) def clip(a, a_min=_NoValue, a_max=_NoValue, out=None, *args, **kwargs): return clip_impl(a, a_min, a_max, out, *args, **kwargs) else: @implements(np.clip) def clip(a, a_min, a_max, out=None, *args, **kwargs): return clip_impl(a, a_min, a_max, out, *args, **kwargs) @implements(np.where) def where(condition, *args, **kwargs): if len(args) == 0: return np.where._implementation(np.asarray(condition), **kwargs) elif len(args) < 2: # error message borrowed from numpy 1.24.1 raise ValueError("either both or neither of x and y should be given") x, y, *args = args retu = _validate_units_consistency((x, y)) return ( np.where._implementation( condition, np.asarray(x), np.asarray(y), *args, **kwargs ) * retu ) @implements(np.triu) def triu(m, *args, **kwargs): return np.triu._implementation(np.asarray(m), *args, **kwargs) * m.units @implements(np.tril) def tril(m, *args, **kwargs): return np.tril._implementation(np.asarray(m), *args, **kwargs) * m.units @implements(np.einsum) def einsum(*operands, out=None, **kwargs): subscripts, *operands = operands ret_units = _validate_units_consistency(operands) if out is not None: out_view = np.asarray(out) else: out_view = out res = np.einsum._implementation(subscripts, *operands, out=out_view) if getattr(out, "units", None) is not None: out.units = ret_units if res.ndim == 0: cls = unyt_quantity else: cls = unyt_array return cls(res, ret_units, bypass_validation=True) @implements(np.convolve) def convolve(a, v, *args, **kwargs): ret_units = np.prod(get_units((a, v))) return ( np.convolve._implementation(np.asarray(a), np.asarray(v), *args, **kwargs) * ret_units ) @implements(np.correlate) def correlate(a, v, *args, **kwargs): ret_units = np.prod(get_units((a, v))) return ( np.correlate._implementation(np.asarray(a), np.asarray(v), *args, **kwargs) * ret_units ) @implements(np.tensordot) def tensordot(a, b, *args, **kwargs): ret_units = np.prod(get_units((a, b))) return ( np.tensordot._implementation(np.asarray(a), np.asarray(b), *args, **kwargs) * ret_units ) @implements(np.unwrap) def unwrap(p, *args, **kwargs): ret_units = p.units return np.unwrap._implementation(np.asarray(p), *args, **kwargs) * ret_units @implements(np.interp) def interp(x, xp, fp, *args, **kwargs): _validate_units_consistency((x, xp)) # return array type should match fp's # so, the fallback multiplier is 1 instead of NULL_UNITS # This avoid leaking a dimensionless unyt_array if reference data # is a pure np.ndarray ret_units = getattr(fp, "units", 1) return ( np.interp(np.asarray(x), np.asarray(xp), np.asarray(fp), *args, **kwargs) * ret_units ) @implements(np.array_repr) def array_repr(arr, *args, **kwargs): rep = np.array_repr._implementation(np.asarray(arr), *args, **kwargs) rep = rep.replace("array", arr.__class__.__name__) units_repr = arr.units.__repr__() if "=" in rep: return rep[:-1] + ", units='" + units_repr + "')" else: return rep[:-1] + ", '" + units_repr + "')" if NUMPY_VERSION < Version("2.0.0dev0"): # functions that are removed in numpy 2.0.0 @implements(np.asfarray) # noqa: NPY201 def asfarray(a, dtype=np.double): ret_units = a.units arr = np.asfarray._implementation(np.asarray(a), dtype=dtype) # noqa: NPY201 return arr * ret_units _trapezoid_func = np.trapz # noqa: NPY201 elif NUMPY_VERSION >= Version("2.0.0dev0"): # functions that were added in numpy 2.0.0 @implements(np.linalg.outer) def linalg_outer(x1, x2, /): return product_helper(x1, x2, out=None, func=np.linalg.outer) _trapezoid_func = np.trapezoid @implements(_trapezoid_func) def trapezoid(y, x=None, dx=1.0, *args, **kwargs): ret_units = y.units if x is None: ret_units = ret_units * getattr(dx, "units", NULL_UNIT) else: ret_units = ret_units * getattr(x, "units", NULL_UNIT) if isinstance(x, np.ndarray): x = np.asarray(x) if isinstance(dx, np.ndarray): dx = np.asarray(dx) return ( _trapezoid_func._implementation(np.asarray(y), x, dx, *args, **kwargs) * ret_units ) if hasattr(np, "in1d"): @implements(np.in1d) # noqa: NPY201 def in1d(ar1, ar2, *args, **kwargs): _validate_units_consistency((ar1, ar2)) return np.isin._implementation( np.asarray(ar1), np.asarray(ar2), *args, **kwargs ) @implements(np.take) def take(a, indices, axis=None, out=None, mode="raise"): ret_units = getattr(a, "units", NULL_UNIT) if out is not None: out_view = np.asarray(out) else: out_view = None res = np.take._implementation( np.asarray(a), indices, axis=axis, out=out_view, mode=mode ) if getattr(out, "units", None) is not None: out.units = ret_units ret_cls = unyt_quantity if res.ndim == 0 else unyt_array return ret_cls(res, ret_units, bypass_validation=True) unyt-3.0.4/unyt/_deprecation.py000066400000000000000000000007341476461141700165410ustar00rootroot00000000000000import warnings from typing import Optional def warn_deprecated( name, /, *, stacklevel: int = 3, replacement: Optional[str] = None, since_version: str, ) -> None: msg = ( f"{name} is deprecated and will be removed in a future version\n" f"Instead, {replacement}\n" f"(deprecated since unyt v{since_version})" ) warnings.warn( msg, category=DeprecationWarning, stacklevel=stacklevel, ) unyt-3.0.4/unyt/_mpl_array_converter/000077500000000000000000000000001476461141700177435ustar00rootroot00000000000000unyt-3.0.4/unyt/_mpl_array_converter/__init__.py000066400000000000000000000113111476461141700220510ustar00rootroot00000000000000from weakref import WeakKeyDictionary from matplotlib.units import AxisInfo, ConversionInterface from unyt.array import unyt_array, unyt_quantity from unyt.unit_object import Unit class unyt_arrayConverter(ConversionInterface): """Matplotlib interface for unyt_array""" _instance = None _labelstyle = "()" _axisnames = WeakKeyDictionary() # ensure that unyt_arrayConverter is a singleton def __new__(cls): if unyt_arrayConverter._instance is None: unyt_arrayConverter._instance = super().__new__(cls) return unyt_arrayConverter._instance # When matplotlib first encounters a type in its units.registry, it will # call default_units to obtain the units. Then it calls axisinfo to # customize the axis - in our case, just set the label. Then matplotlib calls # convert. @staticmethod def axisinfo(unit, axis): """Set the axis label based on unit Parameters ---------- unit : Unit object, string, or tuple This parameter comes from unyt_arrayConverter.default_units() or from user code such as Axes.plot(), Axis.set_units(), etc. In user code, it is possible to convert the plotted units by specifying the new unit as a string, such as "ms", or as a tuple, such as ("J", "thermal") following the call signature of unyt_array.convert_to_units(). axis : Axis object Returns ------- AxisInfo object with the label formatted as in-line math latex """ if isinstance(unit, tuple): unit = unit[0] unit_obj = unit if isinstance(unit, Unit) else Unit(unit) name = unyt_arrayConverter._axisnames.get(axis, "") if unit_obj.is_dimensionless: label = name else: name += " " unit_str = unit_obj.latex_representation() if unyt_arrayConverter._labelstyle == "[]": label = name + "$\\left[" + unit_str + "\\right]$" elif unyt_arrayConverter._labelstyle == "/": axsym = "$q_{\\rm" + axis.axis_name + "}$" name = axsym if name == " " else name if "/" in unit_str: label = name + "$\\;/\\;\\left(" + unit_str + "\\right)$" else: label = name + "$\\;/\\;" + unit_str + "$" else: label = name + "$\\left(" + unit_str + "\\right)$" return AxisInfo(label=label.strip()) @staticmethod def default_units(x, axis): """Return the Unit object of the unyt_array x Parameters ---------- x : unyt_array axis : Axis object Returns ------- Unit object """ # In the case where the first matplotlib command is setting limits, # x may be a tuple of length two (with the same units). if isinstance(x, tuple): name = getattr(x[0], "name", "") units = x[0].units else: name = getattr(x, "name", "") units = x.units # maintain a mapping between Axis and name since Axis does not point to # its underlying data and we want to propagate the name to the axis # label in the subsequent call to axisinfo unyt_arrayConverter._axisnames[axis] = name if name is not None else "" return units @staticmethod def convert(value, unit, axis): """Convert the units of value to unit Parameters ---------- value : unyt_array, unyt_quantity, or sequence there of unit : Unit, string or tuple This parameter comes from unyt_arrayConverter.default_units() or from user code such as Axes.plot(), Axis.set_units(), etc. In user code, it is possible to convert the plotted units by specifying the new unit as a string, such as "ms", or as a tuple, such as ("J", "thermal") following the call signature of unyt_array.convert_to_units(). axis : Axis object Returns ------- unyt_array Raises ------ UnitConversionError if unit does not have the same dimensions as value or if we don't know how to convert value. """ converted_value = value if isinstance(unit, str) or isinstance(unit, Unit): unit = (unit,) if isinstance(value, (unyt_array, unyt_quantity)): converted_value = value.to(*unit) else: value_type = type(value) converted_value = [] for obj in value: converted_value.append(obj.to(*unit)) converted_value = value_type(converted_value) return converted_value unyt-3.0.4/unyt/_on_demand_imports.py000066400000000000000000000077301476461141700177500ustar00rootroot00000000000000""" A set of convenient on-demand imports """ import sys from functools import wraps from importlib.util import find_spec class NotAModule: """ A class to implement an informative error message that will be outputted if someone tries to use an on-demand import without having the requisite package installed. """ def __init__(self, pkg_name, exc=None): self.pkg_name = pkg_name self._original_exception = exc error_note = ( f"Something went wrong while trying to lazy-import {pkg_name}. " f"Please make sure that {pkg_name} is properly installed.\n" "If the problem persists, please file an issue at " "https://github.com/yt-project/unyt/issues/new" ) if exc is None: self.error = ImportError(error_note) elif sys.version_info >= (3, 11): exc.add_note(error_note) self.error = exc else: # mimic Python 3.11 behaviour: # preserve error message and traceback self.error = type(exc)(f"{exc!s}\n{error_note}").with_traceback( exc.__traceback__ ) def __getattr__(self, item): raise self.error def __call__(self, *args, **kwargs): raise self.error def __repr__(self) -> str: if self._original_exception is None: return f"NotAModule({self.pkg_name!r})" else: return f"NotAModule({self.pkg_name!r}, {self._original_exception!r})" class OnDemand: _default_factory: type[NotAModule] = NotAModule def __init_subclass__(cls): if not cls.__name__.endswith("_imports"): raise TypeError(f"class {cls}'s name needs to be suffixed '_imports'") def __new__(cls): if cls is OnDemand: raise TypeError("The OnDemand base class cannot be instantiated.") else: return object.__new__(cls) @property def _name(self) -> str: _name, _, _suffix = self.__class__.__name__.rpartition("_") return _name @property def __is_available__(self) -> bool: return find_spec(self._name) is not None def safe_import(func): @property @wraps(func) def inner(self): try: return func(self) except ImportError as exc: return self._default_factory(self._name, exc) return inner class astropy_imports(OnDemand): @safe_import def log(self): from astropy import log if log.exception_logging_enabled(): log.disable_exception_logging() return log @safe_import def units(self): from astropy import units self.log # noqa: B018 return units @safe_import def __version__(self): from astropy import __version__ return __version__ _astropy = astropy_imports() class h5py_imports(OnDemand): @safe_import def File(self): from h5py import File return File @safe_import def __version__(self): from h5py import __version__ return __version__ _h5py = h5py_imports() class pint_imports(OnDemand): @safe_import def UnitRegistry(self): from pint import UnitRegistry return UnitRegistry _pint = pint_imports() class matplotlib_imports(OnDemand): @safe_import def __version__(self): from matplotlib import __version__ return __version__ @safe_import def pyplot(self): from matplotlib import pyplot return pyplot @safe_import def units(self): from matplotlib import units return units @safe_import def use(self): from matplotlib import use return use _matplotlib = matplotlib_imports() class dask_imports(OnDemand): @safe_import def array(self): from dask import array return array @safe_import def __version__(self): from dask import __version__ return __version__ _dask = dask_imports() unyt-3.0.4/unyt/_parsing.py000066400000000000000000000052641476461141700157120ustar00rootroot00000000000000""" parsing utilities """ import token from sympy import Basic, Float, Integer, Rational, Symbol, sqrt from sympy.parsing.sympy_parser import auto_number, parse_expr, rationalize from unyt._unit_lookup_table import inv_name_alternatives from unyt.exceptions import UnitParseError def _auto_positive_symbol(tokens, local_dict, global_dict): """ Inserts calls to ``Symbol`` for undefined variables. Passes in positive=True as a keyword argument. Adapted from sympy.sympy.parsing.sympy_parser.auto_symbol """ result = [] tokens.append((None, None)) # so zip traverses all tokens for tok, nextTok in zip(tokens, tokens[1:]): tokNum, tokVal = tok nextTokNum, nextTokVal = nextTok if tokNum == token.NAME: name = tokVal if name in global_dict: obj = global_dict[name] if isinstance(obj, (Basic, type)) or callable(obj): result.append((token.NAME, name)) continue # try to resolve known alternative unit name try: used_name = inv_name_alternatives[str(name)] except KeyError: # if we don't know this name it's a user-defined unit name # so we should create a new symbol for it used_name = str(name) result.extend( [ (token.NAME, "Symbol"), (token.OP, "("), (token.NAME, repr(used_name)), (token.OP, ","), (token.NAME, "positive"), (token.OP, "="), (token.NAME, "True"), (token.OP, ")"), ] ) else: result.append((tokNum, tokVal)) return result global_dict = { "Symbol": Symbol, "Integer": Integer, "Float": Float, "Rational": Rational, "sqrt": sqrt, } unit_text_transform = (_auto_positive_symbol, auto_number, rationalize) def parse_unyt_expr(unit_expr): if not unit_expr: # Bug catch... # if unit_expr is an empty string, parse_expr fails hard... unit_expr = "1" # Avoid a parse error if someone uses the percent unit and the # parser tries to interpret it as the modulo operator unit_expr = unit_expr.replace("%", "percent") unit_expr = unit_expr.replace("°", "deg") try: unit_expr = parse_expr( unit_expr, global_dict=global_dict, transformations=unit_text_transform ) except Exception as e: msg = f"Unit expression '{unit_expr}' raised an error during parsing:\n{e!r}" raise UnitParseError(msg) return unit_expr unyt-3.0.4/unyt/_physical_ratios.py000066400000000000000000000141531476461141700174410ustar00rootroot00000000000000import numpy as np # # Physical Constants and Units Conversion Factors # # Values for these constants, unless otherwise noted, are drawn from IAU, # IUPAC, NIST, and NASA data, whichever is newer. # http://maia.usno.navy.mil/NSFA/IAU2009_consts.html # http://goldbook.iupac.org/list_goldbook_phys_constants_defs.html # http://physics.nist.gov/cuu/Constants/index.html # http://nssdc.gsfc.nasa.gov/planetary/factsheet/jupiterfact.html # Elementary masses mass_electron_kg = 9.10938291e-31 amu_kg = 1.660538921e-27 amu_grams = amu_kg * 1.0e3 mass_hydrogen_kg = 1.007947 * amu_kg mass_proton_kg = 1.672623110e-27 # Solar values (see Mamajek 2012) # https://sites.google.com/site/mamajeksstarnotes/bc-scale mass_sun_kg = 1.98841586e30 temp_sun_kelvin = 5870.0 luminosity_sun_watts = 3.8270e26 # Solar abundances from various references metallicity_sun = 0.01295 # Cloudy 17.03 metallicity_sun_angr = 0.01937 # Anders & Grevesse (1989) metallicity_sun_aspl = 0.01337 # Asplund et al. (2009) metallicity_sun_feld = 0.01909 # Feldman (1992) metallicity_sun_lodd = 0.01321 # Lodders (2003) # Conversion Factors: X au * mpc_per_au = Y mpc # length mpc_per_mpc = 1e0 mpc_per_kpc = 1e-3 mpc_per_pc = 1e-6 mpc_per_au = 4.84813682e-12 mpc_per_rsun = 2.253962e-14 mpc_per_rearth = 2.06470307893e-16 mpc_per_rjup = 2.26566120943e-15 mpc_per_miles = 5.21552871e-20 mpc_per_km = 3.24077929e-20 mpc_per_m = 3.24077929e-23 kpc_per_m = mpc_per_m / mpc_per_kpc pc_per_m = mpc_per_m / mpc_per_pc km_per_pc = 3.08567758e13 cm_per_pc = 3.08567758e18 cm_per_mpc = 3.08567758e21 km_per_m = 1e-3 km_per_cm = 1e-5 m_per_cm = 1e-2 ly_per_m = 1.05702341e-16 rsun_per_m = 1.4378145e-9 rearth_per_m = 1.56961033e-7 # Mean (volumetric) radius rjup_per_m = 1.43039006737e-8 # Mean (volumetric) radius au_per_m = 6.68458712e-12 ang_per_m = 1.0e10 m_per_fpc = 0.0324077929 kpc_per_mpc = 1.0 / mpc_per_kpc pc_per_mpc = 1.0 / mpc_per_pc au_per_mpc = 1.0 / mpc_per_au rsun_per_mpc = 1.0 / mpc_per_rsun rearth_per_mpc = 1.0 / mpc_per_rearth rjup_per_mpc = 1.0 / mpc_per_rjup miles_per_mpc = 1.0 / mpc_per_miles km_per_mpc = 1.0 / mpc_per_km m_per_mpc = 1.0 / mpc_per_m m_per_kpc = 1.0 / kpc_per_m m_per_km = 1.0 / km_per_m cm_per_km = 1.0 / km_per_cm cm_per_m = 1.0 / m_per_cm pc_per_km = 1.0 / km_per_pc m_per_pc = 1.0 / pc_per_m m_per_ly = 1.0 / ly_per_m m_per_rsun = 1.0 / rsun_per_m m_per_rearth = 1.0 / rearth_per_m m_per_rjup = 1.0 / rjup_per_m m_per_au = 1.0 / au_per_m m_per_ang = 1.0 / ang_per_m # time # "IAU Style Manual" by G.A. Wilkins, Comm. 5, in IAU Transactions XXB (1989) sec_per_Gyr = 31.5576e15 sec_per_Myr = 31.5576e12 sec_per_kyr = 31.5576e9 sec_per_year = 31.5576e6 sec_per_day = 86400.0 sec_per_hr = 3600.0 sec_per_min = 60.0 day_per_year = 365.25 # velocities, accelerations speed_of_light_m_per_s = 2.99792458e8 speed_of_light_cm_per_s = speed_of_light_m_per_s * 100.0 standard_gravity_m_per_s2 = 9.80665 # some constants newton_mks = 6.67408e-11 planck_mks = 6.62606957e-34 elementary_charge_C = 1.6021766208e-19 # permeability of Free Space mu_0 = 4.0e-7 * np.pi # permittivity of Free Space eps_0 = 1.0 / (speed_of_light_m_per_s**2 * mu_0) avogadros_number = 6.022141410704091e23 rydberg_constant_mks = ( 0.125 * mass_electron_kg * elementary_charge_C**4 / (eps_0**2 * planck_mks**3 * speed_of_light_m_per_s) ) # temperature / energy boltzmann_constant_J_per_K = 1.3806488e-23 erg_per_eV = 1.602176562e-12 J_per_eV = erg_per_eV * 1.0e-7 erg_per_keV = erg_per_eV * 1.0e3 J_per_keV = J_per_eV * 1.0e3 K_per_keV = J_per_keV / boltzmann_constant_J_per_K keV_per_K = 1.0 / K_per_keV keV_per_erg = 1.0 / erg_per_keV eV_per_erg = 1.0 / erg_per_eV kelvin_per_rankine = 5.0 / 9.0 watt_per_horsepower = 745.69987158227022 erg_per_s_per_watt = 1e7 J_per_BTU = 1055.0559 J_per_watt_hour = 3600 btu_per_mmbtu = 1e6 * J_per_BTU btu_per_therm = 1e5 * J_per_BTU btu_per_quad = 1e15 * J_per_BTU rydberg_unit_mks = planck_mks * speed_of_light_m_per_s * rydberg_constant_mks stefan_boltzmann_W_per_sqm_per_K4 = ( 2.0 * np.pi**5 * boltzmann_constant_J_per_K**4 / (15.0 * speed_of_light_m_per_s**2 * planck_mks**3) ) radiation_constant_J_per_m3_per_K4 = ( 4.0 * stefan_boltzmann_W_per_sqm_per_K4 / speed_of_light_m_per_s ) J_per_foe = 1.0e44 # Solar System masses # Standish, E.M. (1995) "Report of the IAU WGAS Sub-Group on Numerical # Standards", in Highlights of Astronomy (I. Appenzeller, ed.), Table 1, # Kluwer Academic Publishers, Dordrecht. # REMARK: following masses include whole systems (planet + moons) mass_jupiter_kg = mass_sun_kg / 1047.3486 mass_mercury_kg = mass_sun_kg / 6023600.0 mass_venus_kg = mass_sun_kg / 408523.71 mass_earth_kg = mass_sun_kg / 328900.56 mass_mars_kg = mass_sun_kg / 3098708.0 mass_saturn_kg = mass_sun_kg / 3497.898 mass_uranus_kg = mass_sun_kg / 22902.98 mass_neptune_kg = mass_sun_kg / 19412.24 # flux jansky_mks = 1.0e-26 # Cosmological constants # Calculated with H = 100 km/s/Mpc, value given in units of h^2 g cm^-3 # Multiply by h^2 to get the critical density in units of g cm^-3 rho_crit_g_cm3_h2 = 1.8788e-29 primordial_H_mass_fraction = 0.76 # Misc. Approximations mass_mean_atomic_cosmology = 1.22 mass_mean_atomic_galactic = 2.3 # Miscellaneous HUGE = 1.0e90 TINY = 1.0e-40 # Planck units hbar_mks = 0.5 * planck_mks / np.pi planck_mass_kg = np.sqrt(hbar_mks * speed_of_light_m_per_s / newton_mks) planck_length_m = np.sqrt(hbar_mks * newton_mks / speed_of_light_m_per_s**3) planck_time_s = planck_length_m / speed_of_light_m_per_s planck_energy_J = planck_mass_kg * speed_of_light_m_per_s * speed_of_light_m_per_s planck_temperature_K = planck_energy_J / boltzmann_constant_J_per_K planck_charge_C = np.sqrt(4.0 * np.pi * eps_0 * hbar_mks * speed_of_light_m_per_s) # Imperial and other non-metric units kg_per_pound = 0.45359237 pascal_per_atm = 101325.0 m_per_inch = 0.0254 m_per_ft = 0.3048 m_per_mile = 1609.344 # https://en.wikipedia.org/wiki/Comparison_of_the_imperial_and_US_customary_measurement_systems uk_fl_oz_per_L = 0.0284130625e-3 us_fl_oz_per_L = 0.0295735295625e-3 # logarithmic units # IEC 60027-3: https://webstore.iec.ch/publication/94 # NIST Special Publication 811: https://www.nist.gov/pml/special-publication-811 neper_per_bel = np.log(10) / 2 unyt-3.0.4/unyt/_pint_conversions.py000066400000000000000000000030201476461141700176350ustar00rootroot00000000000000""" Stuff for pint conversions """ pint_aliases = { "meter": "m", "second": "s", "gram": "g", "joule": "J", "franklin": "esu", "dyne": "dyn", "parsec": "pc", "mole": "mol", "rankine": "R", "watt": "W", "pascal": "Pa", "tesla": "T", "kelvin": "K", "year": "yr", "minute": "min", "hour": "hr", "volt": "V", "ampere": "A", "foot": "ft", "coulomb": "C", "newton": "N", "hertz": "Hz", "arcsecond": "arcsec", "arcminute": "arcmin", "speed_of_light": "c", "esu_per_second": "statA", "atomic_mass_unit": "amu", "astronomical_unit": "au", "light_year": "ly", "electron_mass": "me", "proton_mass": "mp", } pint_prefixes = { "yotta": "Y", "zetta": "Z", "exa": "E", "peta": "P", "tera": "T", "giga": "G", "mega": "M", "kilo": "k", "deci": "d", "centi": "c", "milli": "m", "micro": "u", "nano": "n", "pico": "p", "femto": "f", "atto": "a", "zepto": "z", "yocto": "y", } def convert_pint_units(unit_expr): uexpr = unit_expr pfx = "" for prefix in pint_prefixes: if unit_expr.startswith(prefix): pfx = pint_prefixes[prefix] uexpr = uexpr[len(prefix) :] break if uexpr in pint_aliases: uexpr = pint_aliases[uexpr] if pfx == "": return uexpr else: return pfx + uexpr # If we can't figure it out just pass it and see # what happens return unit_expr unyt-3.0.4/unyt/_unit_lookup_table.py000066400000000000000000000716131476461141700177670ustar00rootroot00000000000000""" The default unit symbol lookup table. """ from collections import OrderedDict, defaultdict import numpy as np from unyt import dimensions from unyt._physical_ratios import ( J_per_BTU, J_per_eV, J_per_foe, J_per_watt_hour, amu_grams, amu_kg, avogadros_number, boltzmann_constant_J_per_K, btu_per_mmbtu, btu_per_quad, btu_per_therm, elementary_charge_C, eps_0, jansky_mks, kelvin_per_rankine, kg_per_pound, luminosity_sun_watts, m_per_ang, m_per_au, m_per_ft, m_per_inch, m_per_ly, m_per_mile, m_per_pc, m_per_rearth, m_per_rjup, m_per_rsun, mass_earth_kg, mass_electron_kg, mass_hydrogen_kg, mass_jupiter_kg, mass_mars_kg, mass_mercury_kg, mass_neptune_kg, mass_proton_kg, mass_saturn_kg, mass_sun_kg, mass_uranus_kg, mass_venus_kg, metallicity_sun, metallicity_sun_angr, metallicity_sun_aspl, metallicity_sun_feld, metallicity_sun_lodd, mu_0, neper_per_bel, newton_mks, pascal_per_atm, planck_charge_C, planck_energy_J, planck_length_m, planck_mass_kg, planck_mks, planck_temperature_K, planck_time_s, radiation_constant_J_per_m3_per_K4, rydberg_constant_mks, rydberg_unit_mks, sec_per_day, sec_per_hr, sec_per_min, sec_per_year, speed_of_light_m_per_s, standard_gravity_m_per_s2, stefan_boltzmann_W_per_sqm_per_K4, temp_sun_kelvin, uk_fl_oz_per_L, us_fl_oz_per_L, watt_per_horsepower, ) # Lookup a unit symbol with the symbol string, and provide a tuple with the # conversion factor to cgs and dimensionality. default_unit_symbol_lut = OrderedDict( [ # base ("m", (1.0, dimensions.length, 0.0, r"\rm{m}", True)), ("g", (1.0e-3, dimensions.mass, 0.0, r"\rm{g}", True)), ("s", (1.0, dimensions.time, 0.0, r"\rm{s}", True)), ("K", (1.0, dimensions.temperature, 0.0, r"\rm{K}", True)), ("rad", (1.0, dimensions.angle, 0.0, r"\rm{rad}", True)), ("A", (1.0, dimensions.current_mks, 0.0, r"\rm{A}", True)), ("cd", (1.0, dimensions.luminous_intensity, 0.0, r"\rm{cd}", True)), ("mol", (1.0 / amu_grams, dimensions.dimensionless, 0.0, r"\rm{mol}", True)), # some cgs ("dyn", (1.0e-5, dimensions.force, 0.0, r"\rm{dyn}", True)), ("erg", (1.0e-7, dimensions.energy, 0.0, r"\rm{erg}", True)), ("Ba", (0.1, dimensions.pressure, 0.0, r"\rm{Ba}", True)), ("G", (0.1**0.5, dimensions.magnetic_field_cgs, 0.0, r"\rm{G}", True)), ("statC", (1.0e-3**1.5, dimensions.charge_cgs, 0.0, r"\rm{statC}", True)), ("statA", (1.0e-3**1.5, dimensions.current_cgs, 0.0, r"\rm{statA}", True)), ( "statV", ( 0.1 * 1.0e-3**0.5, dimensions.electric_potential_cgs, 0.0, r"\rm{statV}", True, ), ), ("statohm", (100.0, dimensions.resistance_cgs, 0.0, r"\rm{statohm}", True)), ("Mx", (1.0e-3**1.5, dimensions.magnetic_flux_cgs, 0.0, r"\rm{Mx}", True)), # some SI ("J", (1.0, dimensions.energy, 0.0, r"\rm{J}", True)), ("W", (1.0, dimensions.power, 0.0, r"\rm{W}", True)), ("Hz", (1.0, dimensions.rate, 0.0, r"\rm{Hz}", True)), ("N", (1.0, dimensions.force, 0.0, r"\rm{N}", True)), ("C", (1.0, dimensions.charge_mks, 0.0, r"\rm{C}", True)), ("T", (1.0, dimensions.magnetic_field_mks, 0.0, r"\rm{T}", True)), ("Pa", (1.0, dimensions.pressure, 0.0, r"\rm{Pa}", True)), ("bar", (1.0e5, dimensions.pressure, 0.0, r"\rm{bar}", True)), ("V", (1.0, dimensions.electric_potential, 0.0, r"\rm{V}", True)), ("F", (1.0, dimensions.capacitance, 0.0, r"\rm{F}", True)), ("H", (1.0, dimensions.inductance, 0.0, r"\rm{H}", True)), ("Ω", (1.0, dimensions.resistance, 0.0, r"\Omega", True)), ("Wb", (1.0, dimensions.magnetic_flux, 0.0, r"\rm{Wb}", True)), ("lm", (1.0, dimensions.luminous_flux, 0.0, r"\rm{lm}", True)), ( "lx", (1.0, dimensions.luminous_flux / dimensions.area, 0.0, r"\rm{lx}", True), ), ("degC", (1.0, dimensions.temperature, -273.15, r"^\circ\rm{C}", True)), ("delta_degC", (1.0, dimensions.temperature, 0, r"\Delta^\circ\rm{C}", True)), ("L", (1e-3, dimensions.volume, 0, r"\rm{L}", True)), ("ha", (1.0e4, dimensions.area, 0.0, r"\rm{ha}", False)), ("t", (1.0e3, dimensions.mass, 0.0, r"\rm{t}", False)), # Imperial and other non-metric units ("mil", (1e-3 * m_per_inch, dimensions.length, 0.0, r"\rm{mil}", False)), ("inch", (m_per_inch, dimensions.length, 0.0, r"\rm{in}", False)), ("ft", (m_per_ft, dimensions.length, 0.0, r"\rm{ft}", False)), ("yd", (0.9144, dimensions.length, 0.0, r"\rm{yd}", False)), ("mile", (m_per_mile, dimensions.length, 0.0, r"\rm{mile}", False)), ("nmi", (m_per_mile * 1.1508, dimensions.length, 0.0, r"\rm{nmi}", False)), ( "mph", (m_per_mile / sec_per_hr, dimensions.velocity, 0.0, r"\rm{mph}", False), ), ( "kt", ( m_per_mile * 1.1508 / sec_per_hr, dimensions.velocity, 0.0, r"\rm{kt}", False, ), ), ("acre", (4046.8564224, dimensions.area, 0.0, r"\rm{acre}", False)), ("furlong", (m_per_ft * 660.0, dimensions.length, 0.0, r"\rm{fur}", False)), ( "degF", ( kelvin_per_rankine, dimensions.temperature, -459.67, r"^\circ\rm{F}", False, ), ), ( "delta_degF", ( kelvin_per_rankine, dimensions.temperature, 0.0, r"\Delta^\circ\rm{F}", False, ), ), ( "R", (kelvin_per_rankine, dimensions.temperature, 0.0, r"^\circ\rm{R}", False), ), ( "lbf", ( kg_per_pound * standard_gravity_m_per_s2, dimensions.force, 0.0, r"\rm{lbf}", False, ), ), ( "kip", ( 1000 * kg_per_pound * standard_gravity_m_per_s2, dimensions.force, 0.0, r"\rm{kip}", False, ), ), ("lb", (kg_per_pound, dimensions.mass, 0.0, r"\rm{lb}", False)), ("atm", (pascal_per_atm, dimensions.pressure, 0.0, r"\rm{atm}", False)), ("hp", (watt_per_horsepower, dimensions.power, 0.0, r"\rm{hp}", False)), ("oz", (kg_per_pound / 16.0, dimensions.mass, 0.0, r"\rm{oz}", False)), ("ton", (kg_per_pound * 2000.0, dimensions.mass, 0.0, r"\rm{US ton}", False)), ( "ton_UK", (kg_per_pound * 2240.0, dimensions.mass, 0.0, r"\rm{UK ton}", False), ), ( "slug", ( kg_per_pound * standard_gravity_m_per_s2 / m_per_ft, dimensions.mass, 0.0, r"\rm{slug}", False, ), ), ("fl_oz_US", (us_fl_oz_per_L, dimensions.volume, 0.0, r"\rm{US fl oz}", False)), ("fl_oz_UK", (uk_fl_oz_per_L, dimensions.volume, 0.0, r"\rm{UK fl oz}", False)), ( "pt_US", (us_fl_oz_per_L * 16.0, dimensions.volume, 0.0, r"\rm{US pt}", False), ), ( "pt_UK", (uk_fl_oz_per_L * 20.0, dimensions.volume, 0.0, r"\rm{UK pt}", False), ), ( "qt_US", (us_fl_oz_per_L * 32.0, dimensions.volume, 0.0, r"\rm{US qt}", False), ), ( "qt_UK", (uk_fl_oz_per_L * 40.0, dimensions.volume, 0.0, r"\rm{UK qt}", False), ), ( "gal_US", (us_fl_oz_per_L * 128.0, dimensions.volume, 0.0, r"\rm{US gal}", False), ), ( "gal_UK", (uk_fl_oz_per_L * 160.0, dimensions.volume, 0.0, r"\rm{UK gal}", False), ), ("cal", (4.184, dimensions.energy, 0.0, r"\rm{cal}", True)), ("BTU", (J_per_BTU, dimensions.energy, 0.0, r"\rm{BTU}", False)), ("MMBTU", (btu_per_mmbtu, dimensions.energy, 0.0, r"\rm{MMBTU}", False)), ("therm", (btu_per_therm, dimensions.energy, 0.0, r"\rm{therm}", False)), ("quad", (btu_per_quad, dimensions.energy, 0.0, r"\rm{quad}", False)), ("Wh", (J_per_watt_hour, dimensions.energy, 0.0, r"\rm{Wh}", True)), ( "pli", ( kg_per_pound * standard_gravity_m_per_s2 / m_per_inch, dimensions.tension, 0.0, r"\rm{pli}", False, ), ), ( "plf", ( kg_per_pound * standard_gravity_m_per_s2 / m_per_ft, dimensions.tension, 0.0, r"\rm{plf}", False, ), ), ( "psi", ( kg_per_pound * standard_gravity_m_per_s2 / m_per_inch**2, dimensions.pressure, 0.0, r"\rm{psi}", False, ), ), ( "psf", ( kg_per_pound * standard_gravity_m_per_s2 / m_per_ft**2, dimensions.pressure, 0.0, r"\rm{psf}", False, ), ), ( "kli", ( 1000 * kg_per_pound * standard_gravity_m_per_s2 / m_per_inch, dimensions.tension, 0.0, r"\rm{kli}", False, ), ), ( "klf", ( 1000 * kg_per_pound * standard_gravity_m_per_s2 / m_per_ft, dimensions.tension, 0.0, r"\rm{klf}", False, ), ), ( "ksi", ( 1000 * kg_per_pound * standard_gravity_m_per_s2 / m_per_inch**2, dimensions.pressure, 0.0, r"\rm{ksi}", False, ), ), ( "ksf", ( 1000 * kg_per_pound * standard_gravity_m_per_s2 / m_per_ft**2, dimensions.pressure, 0.0, r"\rm{ksf}", False, ), ), ("smoot", (1.7018, dimensions.length, 0.0, r"\rm{smoot}", False)), # dimensionless stuff ("dimensionless", (1.0, dimensions.dimensionless, 0.0, r"", False)), ("%", (0.01, dimensions.dimensionless, 0.0, r"\%", False)), # times ("min", (sec_per_min, dimensions.time, 0.0, r"\rm{min}", False)), ("hr", (sec_per_hr, dimensions.time, 0.0, r"\rm{hr}", False)), ("day", (sec_per_day, dimensions.time, 0.0, r"\rm{d}", False)), ("week", (7.0 * sec_per_day, dimensions.time, 0.0, r"\rm{week}", False)), ( "fortnight", (14.0 * sec_per_day, dimensions.time, 0.0, r"\rm{fortnight}", False), ), ("yr", (sec_per_year, dimensions.time, 0.0, r"\rm{yr}", True)), # Velocities ("c", (speed_of_light_m_per_s, dimensions.velocity, 0.0, r"\rm{c}", False)), # Solar units ("Msun", (mass_sun_kg, dimensions.mass, 0.0, r"\rm{M}_\odot", False)), ("Rsun", (m_per_rsun, dimensions.length, 0.0, r"\rm{R}_\odot", False)), ("Lsun", (luminosity_sun_watts, dimensions.power, 0.0, r"\rm{L}_\odot", False)), ( "Tsun", (temp_sun_kelvin, dimensions.temperature, 0.0, r"\rm{T}_\odot", False), ), ( "Zsun", (metallicity_sun, dimensions.dimensionless, 0.0, r"\rm{Z}_\odot", False), ), ( "Zsun_angr", ( metallicity_sun_angr, dimensions.dimensionless, 0.0, r"\rm{Z}_\odot", False, ), ), ( "Zsun_aspl", ( metallicity_sun_aspl, dimensions.dimensionless, 0.0, r"\rm{Z}_\odot", False, ), ), ( "Zsun_feld", ( metallicity_sun_feld, dimensions.dimensionless, 0.0, r"\rm{Z}_\odot", False, ), ), ( "Zsun_lodd", ( metallicity_sun_lodd, dimensions.dimensionless, 0.0, r"\rm{Z}_\odot", False, ), ), ("Mjup", (mass_jupiter_kg, dimensions.mass, 0.0, r"\rm{M}_{\rm{Jup}}", False)), ("Mearth", (mass_earth_kg, dimensions.mass, 0.0, r"\rm{M}_\oplus", False)), ("Rjup", (m_per_rjup, dimensions.length, 0.0, r"\rm{R}_\mathrm{Jup}", False)), ("Rearth", (m_per_rearth, dimensions.length, 0.0, r"\rm{R}_\oplus", False)), # astro distances ("AU", (m_per_au, dimensions.length, 0.0, r"\rm{AU}", False)), ("ly", (m_per_ly, dimensions.length, 0.0, r"\rm{ly}", False)), ("pc", (m_per_pc, dimensions.length, 0.0, r"\rm{pc}", True)), # angles ("degree", (np.pi / 180.0, dimensions.angle, 0.0, r"^\circ", False)), ("arcmin", (np.pi / 10800.0, dimensions.angle, 0.0, r"\rm{arcmin}", False)), ("arcsec", (np.pi / 648000.0, dimensions.angle, 0.0, r"\rm{arcsec}", False)), ("mas", (np.pi / 648000000.0, dimensions.angle, 0.0, r"\rm{mas}", False)), ("hourangle", (np.pi / 12.0, dimensions.angle, 0.0, r"\rm{HA}", False)), ("sr", (1.0, dimensions.solid_angle, 0.0, r"\rm{sr}", False)), ("lat", (-np.pi / 180.0, dimensions.angle, 90.0, r"\rm{Latitude}", False)), ("lon", (np.pi / 180.0, dimensions.angle, -180.0, r"\rm{Longitude}", False)), ( "rpm", (2.0 * np.pi / 60.0, dimensions.angular_frequency, 0.0, r"\rm{RPM}", False), ), ( "rev", (2.0 * np.pi, dimensions.angle, 0.0, r"\rm{rev}", False), ), # misc ("eV", (J_per_eV, dimensions.energy, 0.0, r"\rm{eV}", True)), # note: 1 foe = 1 Bethe ("foe", (J_per_foe, dimensions.energy, 0.0, r"\rm{foe}", False)), ("bethe", (J_per_foe, dimensions.energy, 0.0, r"\rm{B}", False)), ("amu", (amu_kg, dimensions.mass, 0.0, r"\rm{amu}", False)), ("Å", (m_per_ang, dimensions.length, 0.0, r"\AA", False)), ("Jy", (jansky_mks, dimensions.specific_flux, 0.0, r"\rm{Jy}", True)), ("counts", (1.0, dimensions.dimensionless, 0.0, r"\rm{counts}", False)), ("photons", (1.0, dimensions.dimensionless, 0.0, r"\rm{photons}", False)), ("me", (mass_electron_kg, dimensions.mass, 0.0, r"m_e", False)), ("mp", (mass_hydrogen_kg, dimensions.mass, 0.0, r"m_p", False)), ("Sv", (1.0, dimensions.specific_energy, 0.0, r"\rm{Sv}", True)), ("Ry", (rydberg_unit_mks, dimensions.energy, 0.0, r"\rm{Ry}", False)), ( "rayleigh", (2.5e9 / np.pi, dimensions.count_intensity, 0.0, r"\rm{R}", False), ), ("lambert", (1.0e4 / np.pi, dimensions.luminance, 0.0, r"\rm{L}", False)), ("nt", (1.0, dimensions.luminance, 0.0, r"\rm{nt}", False)), # Planck units ("m_pl", (planck_mass_kg, dimensions.mass, 0.0, r"m_{\rm{P}}", False)), ("l_pl", (planck_length_m, dimensions.length, 0.0, r"\ell_{\rm{P}}", False)), ("t_pl", (planck_time_s, dimensions.time, 0.0, r"t_{\rm{P}}", False)), ( "T_pl", (planck_temperature_K, dimensions.temperature, 0.0, r"T_{\rm{P}}", False), ), ("q_pl", (planck_charge_C, dimensions.charge_mks, 0.0, r"q_{\rm{P}}", False)), ("E_pl", (planck_energy_J, dimensions.energy, 0.0, r"E_{\rm{P}}", False)), # Geometrized units ("m_geom", (mass_sun_kg, dimensions.mass, 0.0, r"\rm{M}_\odot", False)), ( "l_geom", ( newton_mks * mass_sun_kg / speed_of_light_m_per_s**2, dimensions.length, 0.0, r"\rm{M}_\odot", False, ), ), ( "t_geom", ( newton_mks * mass_sun_kg / speed_of_light_m_per_s**3, dimensions.time, 0.0, r"\rm{M}_\odot", False, ), ), # logarithmic units ("B", (neper_per_bel, dimensions.logarithmic, 0.0, r"\rm{B}", True)), ("Np", (1.0, dimensions.logarithmic, 0.0, r"\rm{Np}", True)), ] ) # This dictionary formatting from magnitude package, credit to Juan Reyero. unit_prefixes = OrderedDict( [ ("Y", (1e24, "yotta")), ("Z", (1e21, "zetta")), ("E", (1e18, "exa")), ("P", (1e15, "peta")), ("T", (1e12, "tera")), ("G", (1e9, "giga")), ("M", (1e6, "mega")), ("k", (1e3, "kilo")), ("h", (1e2, "hecto")), ("da", (1e1, "deca")), ("d", (1e-1, "deci")), ("c", (1e-2, "centi")), ("m", (1e-3, "mili")), # typo, kept for backward compatibility ("m", (1e-3, "milli")), ("µ", (1e-6, "micro")), # ('MICRO SIGN' U+00B5) ("u", (1e-6, "micro")), ("μ", (1e-6, "micro")), # ('GREEK SMALL LETTER MU' U+03BC) ("n", (1e-9, "nano")), ("p", (1e-12, "pico")), ("f", (1e-15, "femto")), ("a", (1e-18, "atto")), ("z", (1e-21, "zepto")), ("y", (1e-24, "yocto")), ] ) default_base_units = { dimensions.mass: "kg", dimensions.length: "m", dimensions.time: "s", dimensions.temperature: "K", dimensions.angle: "radian", dimensions.current_mks: "A", dimensions.luminous_intensity: "cd", } physical_constants = OrderedDict( [ ("me", (mass_electron_kg, "kg", ["mass_electron", "electron_mass"])), ("Na", (avogadros_number, "mol**-1", ["Avogadros_number", "avogadros_number"])), ("mp", (mass_proton_kg, "kg", ["proton_mass", "mass_proton"])), ("mh", (mass_hydrogen_kg, "kg", ["hydrogen_mass", "mass_hydrogen"])), ("c", (speed_of_light_m_per_s, "m/s", ["clight", "speed_of_light"])), ( "σ_T", ( 6.65245854533e-29, "m**2", [ "sigma_thompson", "thompson_cross_section", "cross_section_thompson", ] # typos, kept for backwards compatibility + ["sigma_thomson", "thomson_cross_section", "cross_section_thomson"], ), ), ( "qp", ( elementary_charge_C, "C", ["proton_charge", "elementary_charge", "charge_proton"], ), ), ("qe", (-elementary_charge_C, "C", ["electron_charge", "charge_electron"])), ("kb", (boltzmann_constant_J_per_K, "J/K", ["kboltz", "boltzmann_constant"])), ( "G", ( newton_mks, "m**3/kg/s**2", ["newtons_constant", "gravitational_constant"], ), ), ("h", (planck_mks, "J*s", ["planck_constant"])), ("hbar", (0.5 * planck_mks / np.pi, "J*s", ["reduced_planck_constant"])), ( "σ", ( stefan_boltzmann_W_per_sqm_per_K4, "W/m**2/K**4", ["stefan_boltzmann_constant"], ), ), ( "a", ( radiation_constant_J_per_m3_per_K4, "J/m**3/K**4", ["radiation_density_constant"], ), ), ("Tcmb", (2.726, "K", ["CMB_temperature"])), ( "Msun", ( mass_sun_kg, "kg", ["msun", "m_sun", "m_Sun", "M_sun", "M_Sun", "solar_mass", "mass_sun"], ), ), ("Mjup", (mass_jupiter_kg, "kg", ["mjup", "jupiter_mass", "mass_jupiter"])), ("mercury_mass", (mass_mercury_kg, "kg", ["mass_mercury"])), ("venus_mass", (mass_venus_kg, "kg", ["mass_venus"])), ("Mearth", (mass_earth_kg, "kg", ["mearth", "earth_mass", "mass_earth"])), ("mars_mass", (mass_mars_kg, "kg", ["mass_mars"])), ("saturn_mass", (mass_saturn_kg, "kg", ["mass_saturn"])), ("uranus_mass", (mass_uranus_kg, "kg", ["mass_uranus"])), ("neptune_mass", (mass_neptune_kg, "kg", ["mass_neptune"])), ("m_pl", (planck_mass_kg, "kg", ["planck_mass"])), ("l_pl", (planck_length_m, "m", ["planck_length"])), ("t_pl", (planck_time_s, "s", ["planck_time"])), ("E_pl", (planck_energy_J, "J", ["planck_energy"])), ("q_pl", (planck_charge_C, "C", ["planck_charge"])), ("T_pl", (planck_temperature_K, "K", ["planck_temperature"])), ("mu_0", (mu_0, "N/A**2", ["vacuum_permeability", "magnetic_constant", "μ_0"])), ( "eps_0", ( eps_0, "C**2/N/m**2", ["vacuum_permittivity", "electric_constant", "ε_0", "epsilon_0"], ), ), ("R_inf", (rydberg_constant_mks, "m**-1", ["rydberg_constant", "R_∞"])), ("standard_gravity", (standard_gravity_m_per_s2, "m/s**2", [])), ] ) default_unit_name_alternatives = OrderedDict( [ # base ("m", ("meter", "metre")), ("g", ("gram", "gramme")), ("s", ("second",)), ("K", ("degree_kelvin", "kelvin")), ("rad", ("radian",)), ("A", ("ampere", "amp", "Amp")), ("cd", ("candela",)), ("mol", ("mole",)), # some cgs ("dyn", ("dyne",)), ("erg", ("ergs",)), ("Ba", ("barye",)), ("G", ("gauss",)), ("statC", ("statcoulomb", "esu", "ESU", "electrostatic_unit")), ("statA", ("statampere",)), ("statV", ("statvolt",)), ("Mx", ("maxwell",)), # some SI ("J", ("joule",)), ("W", ("watt",)), ("Hz", ("hertz",)), ("N", ("newton",)), ("C", ("coulomb",)), ("T", ("tesla",)), ("Pa", ("pascal",)), ("V", ("volt",)), ("F", ("farad",)), ("H", ("henry",)), ("Ω", ("ohm", "Ohm")), ("Wb", ("weber",)), ("lm", ("lumen",)), ("lx", ("lux",)), ("degC", ("degree_celsius", "degree_Celsius", "celcius", "celsius", "°C")), ("L", ("liter", "litre", "l")), ("ha", ("hectare",)), ( "t", ( "tonne", "metric_ton", ), ), # Imperial and other non-metric units ("mil", ("thou", "thousandth")), ("inch", ("in",)), ("ft", ("foot",)), ("yd", ("yard",)), ("furlong", ("fur",)), ("degF", ("degree_fahrenheit", "degree_Fahrenheit", "fahrenheit", "°F")), ("R", ("degree_rankine", "rankine")), ("lbf", ("pound_force",)), ("kip", ("kilopound", "kipf")), ("lb", ("pound", "pound_mass", "lbm")), ("ton", ("ton_US", "ton_US_short", "short_ton")), ("ton_UK", ("ton_US_long", "long_ton")), ("atm", ("atmosphere",)), ("hp", ("horsepower",)), ("oz", ("ounce",)), ("nmi", ("nautical_mile",)), ("fl_oz_US", ("fluid_ounce_US",)), ("fl_oz_UK", ("fluid_ounce_UK",)), ("pt_US", ("pint_US",)), ("pt_UK", ("pint_UK",)), ("qt_US", ("quart_US",)), ("qt_UK", ("quart_UK",)), ("gal_US", ("gallon_US",)), ("gal_UK", ("gallon_UK",)), ("kt", ("knot",)), ("cal", ("calorie",)), ( "BTU", ( "british_thermal_unit", "btu", ), ), ("MMBTU", ("mmbtu",)), ("therm", ("therms",)), ("quad", ("quads",)), ( "Wh", ( "watt_hour", "watt_hours", ), ), ("pli", ("pounds_per_inch",)), ("plf", ("pounds_per_ft",)), ("psi", ("pounds_per_square_inch",)), ("psf", ("pounds_per_square_ft",)), ("kli", ("kips_per_inch",)), ("klf", ("kips_per_ft",)), ("ksi", ("kips_per_square_inch",)), ("ksf", ("kips_per_square_ft",)), # dimensionless stuff ("dimensionless", ("_", "")), ("B", ("bel",)), ("Np", ("neper",)), # times ("min", ("minute",)), ("hr", ("hour",)), ("day", ("d",)), ("yr", ("year",)), # Solar units ( "Msun", ("msun", "m_sun", "M_sun", "m_Sun", "solar_mass", "solMass", "mass_sun"), ), ("Rsun", ("rsun", "r_sun", "R_sun", "r_Sun", "solar_radius", "solRadius")), ( "Lsun", ("lsun", "l_sun", "L_sun", "l_Sun", "solar_luminosity", "solLuminosity"), ), ( "Tsun", ("t_sun", "tsun", "T_sun", "t_Sun", "solar_temperature", "solTemperature"), ), ( "Zsun", ("z_sun", "zsun", "Z_sun", "z_Sun", "solar_metallicity", "solMetallicity"), ), ("Mjup", ("m_jup", "jupiter_mass")), ("Mearth", ("m_earth", "earth_mass")), ("Rjup", ("r_jup", "jupiter_radius")), ("Rearth", ("r_earth", "earth_radius")), # astro distances ("AU", ("au", "astronomical_unit")), ("pc", ("parsec",)), ("ly", ("light_year",)), # angles ("degree", ("deg",)), ("arcmin", ("arcminute",)), ("arcsec", ("arcsecond",)), ("mas", ("milliarcsecond",)), ("hourangle", ("HA",)), ("sr", ("steradian",)), ("lat", ("latitude", "degree_latitude")), ("lon", ("longitude", "degree_longitude")), ("rev", ("revolution",)), # misc ("eV", ("electronvolt",)), ("amu", ("atomic_mass_unit",)), ("Å", ("angstrom",)), ("Jy", ("jansky",)), ("counts", ("count",)), ("photons", ("photon",)), ("me", ("electron_mass",)), ("mp", ("proton_mass",)), ("Sv", ("sievert",)), ("nt", ("nit",)), ("%", ("percent",)), # Planck units ("m_pl", ("planck_mass",)), ("l_pl", ("planck_length",)), ("t_pl", ("planck_time",)), ("T_pl", ("planck_temperature",)), ("q_pl", ("planck_charge",)), ("E_pl", ("planck_energy",)), ] ) def generate_name_alternatives(): names = defaultdict(list) inv_names = {} seen = set() def append_name(n, okey, key): if key not in seen: n.append(key) inv_names[key] = okey seen.add(key) else: if okey[0] not in ["u", "μ"]: raise RuntimeError( # pragma: no cover f"Duplicate unit name found: {key}, {okey}" ) for key, entry in default_unit_symbol_lut.items(): append_name(names[key], key, key) # Are we SI prefixable or not? if entry[4]: for prefix in unit_prefixes: # This is specifically to work around # https://github.com/yt-project/unyt/issues/145 if prefix in ["u", "μ", "µ"]: used_prefix = "μ" else: used_prefix = prefix append_name(names[prefix + key], used_prefix + key, prefix + key) elif len(key) > 3 and key.title() != key: if all(len(k) > 3 for k in key.split("_")): append_name(names[key], key, key.title()) if key in default_unit_name_alternatives: alternatives = default_unit_name_alternatives[key] # Are we SI prefixable or not? if entry[4]: for a in alternatives: for up, up_data in unit_prefixes.items(): if len(a) < 4: append_name(names[up + key], up + key, up + a) alt = up_data[1] + a if alt not in seen: append_name(names[up + key], up + key, alt) if alt.title() not in names[up + key]: append_name(names[up + key], up + key, alt.title()) for alt in alternatives: append_name(names[key], key, alt) if not alt.islower() or len(alt) < 4: continue if alt.title() not in names[key]: append_name(names[key], key, alt.title()) return names, inv_names name_alternatives, inv_name_alternatives = generate_name_alternatives() unyt-3.0.4/unyt/array.py000066400000000000000000002547031476461141700152320ustar00rootroot00000000000000""" unyt_array class. """ import copy import re import sys import warnings from functools import lru_cache from importlib.metadata import version from numbers import Number as numeric_type import numpy as np from numpy import ( absolute, add, arccos, arccosh, arcsin, arcsinh, arctan, arctan2, arctanh, bitwise_and, bitwise_or, bitwise_xor, cbrt, ceil, clip, conj, copysign, cos, cosh, deg2rad, divide, divmod as divmod_, equal, exp, exp2, expm1, fabs, floor, floor_divide, fmax, fmin, fmod, frexp, greater, greater_equal, heaviside, hypot, invert, iscomplex, isfinite, isinf, isnan, isnat, isreal, ldexp, left_shift, less, less_equal, log, log1p, log2, log10, logaddexp, logaddexp2, logical_and, logical_not, logical_or, logical_xor, matmul, maximum, minimum, mod, modf, multiply, negative, nextafter, not_equal, ones_like, positive, power, rad2deg, reciprocal, remainder, right_shift, rint, sign, signbit, sin, sinh, spacing, sqrt, square, subtract, tan, tanh, true_divide, trunc, ) from packaging.version import Version from sympy import Rational from unyt._on_demand_imports import _astropy, _dask, _pint from unyt._pint_conversions import convert_pint_units from unyt._unit_lookup_table import default_unit_symbol_lut from unyt.dimensions import angle, temperature from unyt.equivalencies import equivalence_registry from unyt.exceptions import ( InvalidUnitEquivalence, InvalidUnitOperation, IterableUnitCoercionError, MKSCGSConversionError, SymbolNotFoundError, UnitConversionError, UnitOperationError, UnitsNotReducible, ) from unyt.unit_object import Unit, _check_em_conversion, _em_conversion from unyt.unit_registry import ( UnitRegistry, _correct_old_unit_registry, _sanitize_unit_system, default_unit_registry, ) from unyt.unit_symbols import delta_degC, delta_degF from ._deprecation import warn_deprecated NUMPY_VERSION = Version(version("numpy")) if NUMPY_VERSION >= Version("2.0.0.dev0"): from numpy import vecdot NULL_UNIT = Unit() POWER_MAPPING = {multiply: lambda x: x, divide: lambda x: 2 - x} DISALLOWED_DTYPES = ( "S", # bytestring "a", # bytestring "U", # (unicode) bytes "O", # Python object "M", # datetime "m", # timedelta ) __doctest_requires__ = { ("unyt_array.from_pint", "unyt_array.to_pint"): ["pint"], ("unyt_array.from_astropy", "unyt_array.to_astropy"): ["astropy"], } _COPY_IF_NEEDED = None if Version(version("numpy")) >= Version("2.0.0dev0") else False # This is partially adapted from the following SO thread # https://stackoverflow.com/questions/41668588/regex-to-match-scientific-notation _NUMB_PATTERN = ( r"[+/-]?(?:((?:\d\.?\d*[Ee][+\-]?\d+)|(?:\d+\.\d*|\d*\.\d+))|\d+|inf\s|nan\s)" ) # *all* greek letters are considered valid unit string elements. # This may be an overshoot. We rely on unyt.Unit to do the actual validation _UNIT_PATTERN = r"((\s*[*/]\s*)?[α-ωΑ-Ωa-zA-Z]+(\*\*([+-]?\d+|\([+-]?\d+\)))?)+" _QUAN_PATTERN = rf"{_NUMB_PATTERN}\s*{_UNIT_PATTERN}" _NUMB_REGEXP = re.compile(_NUMB_PATTERN) _UNIT_REGEXP = re.compile(_UNIT_PATTERN) _QUAN_REGEXP = re.compile(_QUAN_PATTERN) def _iterable(obj): try: len(obj) except Exception: return False return True @lru_cache(maxsize=128, typed=False) def _sqrt_unit(unit): return 1, unit**0.5 @lru_cache(maxsize=128, typed=False) def _cbrt_unit(unit): return 1, unit ** (1.0 / 3.0) @lru_cache(maxsize=128, typed=False) def _multiply_units(unit1, unit2): try: ret = (unit1 * unit2).simplify() except SymbolNotFoundError: # Some operators are not natively commutative when operands are # defined within different unit registries, and conversion # is defined one way but not the other. ret = (unit2 * unit1).simplify() return ret.as_coeff_unit() @lru_cache(maxsize=128, typed=False) def _preserve_units(unit1, unit2=None): if unit2 is None or unit1.dimensions is not temperature: return 1, unit1 if unit1.base_offset == 0.0 and unit2.base_offset != 0.0: return 1, unit2 return 1, unit1 @lru_cache(maxsize=128, typed=False) def _difference_units(unit1, unit2=None): if unit1.dimensions is not temperature: return _preserve_units(unit1, unit2) s1 = repr(unit1) if unit2 is not None and unit2 != unit1: s2 = repr(unit2) if s1 in s2 and s2.startswith("delta_"): return 1, unit1 elif s2 in s1 and s1.startswith("delta_"): return 1, unit2 else: raise InvalidUnitOperation( "Quantities with units of Fahrenheit or Celsius " "cannot be multiplied, divided, subtracted or " "added with data that has different units." ) if unit1.base_offset == 0.0: return 1, unit1 if s1 == "degF": return 1, delta_degF elif s1 == "degC": return 1, delta_degC else: # This is supposed to be unreachable raise RuntimeError( "Could not determine difference temperature units " f"in operation ({unit1} - {unit2}).\n" "If you see this error please file an issue at " "https://github.com/yt-project/unyt/issues/new" ) @lru_cache(maxsize=128, typed=False) def _power_unit(unit, power): return 1, unit**power @lru_cache(maxsize=128, typed=False) def _square_unit(unit): return 1, unit * unit @lru_cache(maxsize=128, typed=False) def _divide_units(unit1, unit2): try: ret = (unit1 / unit2).simplify() except SymbolNotFoundError: ret = (1 / (unit2 / unit1).simplify()).units return ret.as_coeff_unit() @lru_cache(maxsize=128, typed=False) def _reciprocal_unit(unit): return 1, unit**-1 def _passthrough_unit(unit, unit2=None): return 1, unit def _return_without_unit(unit, unit2=None): return 1, None def _arctan2_unit(unit1, unit2): return 1, NULL_UNIT def _comparison_unit(unit1, unit2=None): return 1, None def _invert_units(unit): raise TypeError("Bit-twiddling operators are not defined for unyt_array instances") def _bitop_units(unit1, unit2): raise TypeError("Bit-twiddling operators are not defined for unyt_array instances") def _coerce_iterable_units(input_object, registry=None): if isinstance(input_object, np.ndarray): ret = input_object elif _iterable(input_object): if any(isinstance(o, unyt_array) for o in input_object): ff = getattr(input_object[0], "units", NULL_UNIT) if any(ff != getattr(_, "units", NULL_UNIT) for _ in input_object): ret = [] for datum in input_object: try: ret.append(datum.in_units(ff.units)) except UnitConversionError: raise IterableUnitCoercionError(str(input_object)) ret = unyt_array(np.array(ret), ff, registry=registry) # This will create a copy of the data in the iterable. else: ret = unyt_array(np.array(input_object), ff, registry=registry) else: ret = np.asarray(input_object) else: ret = np.asarray(input_object) if ret.dtype.char in DISALLOWED_DTYPES: raise IterableUnitCoercionError(str(input_object)) return ret def _sanitize_units_convert(possible_units, registry): if isinstance(possible_units, Unit): return possible_units # let Unit() try to parse this if it's not already a Unit unit = Unit(possible_units, registry=registry) return unit def _apply_power_mapping(ufunc, in_unit, in_size, in_shape, input_kwarg_dict): # a reduction of a multiply or divide corresponds to # a repeated product which we implement as an exponent mul = 1 power_map = POWER_MAPPING[ufunc] if input_kwarg_dict.get("axis", None) is not None: unit = in_unit ** (power_map(in_shape[input_kwarg_dict["axis"]])) else: unit = in_unit ** (power_map(in_size)) return mul, unit unary_operators = ( negative, absolute, rint, sign, conj, exp, exp2, log, log2, log10, expm1, log1p, sqrt, cbrt, square, reciprocal, sin, cos, tan, arcsin, arccos, arctan, sinh, cosh, tanh, arcsinh, arccosh, arctanh, deg2rad, rad2deg, invert, logical_not, isreal, iscomplex, isfinite, isinf, isnan, signbit, floor, ceil, trunc, modf, frexp, fabs, spacing, positive, isnat, ones_like, ) binary_operators = ( add, subtract, multiply, divide, logaddexp, logaddexp2, true_divide, power, remainder, mod, arctan2, hypot, bitwise_and, bitwise_or, bitwise_xor, left_shift, right_shift, greater, greater_equal, less, less_equal, not_equal, equal, logical_and, logical_or, logical_xor, maximum, minimum, fmax, fmin, copysign, nextafter, ldexp, fmod, divmod_, heaviside, ) if NUMPY_VERSION >= Version("2.0.0.dev0"): binary_operators += (vecdot,) trigonometric_operators = (sin, cos, tan) multiple_output_operators = {modf: 2, frexp: 2, divmod_: 2} LARGE_INPUT = {4: 16777217, 8: 9007199254740993} class unyt_array(np.ndarray): """ An ndarray subclass that attaches a symbolic unit object to the array data. Parameters ---------- input_array : iterable A tuple, list, or array to attach units to units : String unit name, unit symbol object, or astropy unit The units of the array. Powers must be specified using python syntax (cm**3, not cm^3). registry : :class:`unyt.unit_registry.UnitRegistry` The registry to create units from. If units is already associated with a unit registry and this is specified, this will be used instead of the registry associated with the unit object. dtype : numpy dtype or dtype name The dtype of the array data. Defaults to the dtype of the input data, or, if none is found, uses np.float64 bypass_validation : boolean If True, all input validation is skipped. Using this option may produce corrupted, invalid units or array data, but can lead to significant speedups in the input validation logic adds significant overhead. If set, units *must* be a valid unit object. Defaults to False. name : string The name of the array. Defaults to None. This attribute does not propagate through mathematical operations, but is preserved under indexing and unit conversions. Examples -------- >>> from unyt import unyt_array >>> a = unyt_array([1, 2, 3], 'cm') >>> b = unyt_array([4, 5, 6], 'm') >>> a + b unyt_array([401., 502., 603.], 'cm') >>> b + a unyt_array([4.01, 5.02, 6.03], 'm') NumPy ufuncs will pass through units where appropriate. >>> from unyt import g, cm >>> import numpy as np >>> a = (np.arange(8) - 4)*g/cm**3 >>> np.abs(a) unyt_array([4, 3, 2, 1, 0, 1, 2, 3], 'g/cm**3') and strip them when it would be annoying to deal with them. >>> np.log10(np.arange(8)+1) array([0. , 0.30103 , 0.47712125, 0.60205999, 0.69897 , 0.77815125, 0.84509804, 0.90308999]) """ _ufunc_registry = { add: _preserve_units, subtract: _difference_units, multiply: _multiply_units, divide: _divide_units, logaddexp: _return_without_unit, logaddexp2: _return_without_unit, true_divide: _divide_units, floor_divide: _divide_units, negative: _passthrough_unit, power: _power_unit, remainder: _preserve_units, mod: _preserve_units, fmod: _preserve_units, absolute: _passthrough_unit, fabs: _passthrough_unit, rint: _return_without_unit, sign: _return_without_unit, conj: _passthrough_unit, exp: _return_without_unit, exp2: _return_without_unit, log: _return_without_unit, log2: _return_without_unit, log10: _return_without_unit, expm1: _return_without_unit, log1p: _return_without_unit, sqrt: _sqrt_unit, cbrt: _cbrt_unit, square: _square_unit, reciprocal: _reciprocal_unit, sin: _return_without_unit, cos: _return_without_unit, tan: _return_without_unit, sinh: _return_without_unit, cosh: _return_without_unit, tanh: _return_without_unit, arcsin: _return_without_unit, arccos: _return_without_unit, arctan: _return_without_unit, arctan2: _arctan2_unit, arcsinh: _return_without_unit, arccosh: _return_without_unit, arctanh: _return_without_unit, hypot: _preserve_units, deg2rad: _return_without_unit, rad2deg: _return_without_unit, bitwise_and: _bitop_units, bitwise_or: _bitop_units, bitwise_xor: _bitop_units, invert: _invert_units, left_shift: _bitop_units, right_shift: _bitop_units, greater: _comparison_unit, greater_equal: _comparison_unit, less: _comparison_unit, less_equal: _comparison_unit, not_equal: _comparison_unit, equal: _comparison_unit, logical_and: _comparison_unit, logical_or: _comparison_unit, logical_xor: _comparison_unit, logical_not: _return_without_unit, maximum: _preserve_units, minimum: _preserve_units, fmax: _preserve_units, fmin: _preserve_units, isreal: _return_without_unit, iscomplex: _return_without_unit, isfinite: _return_without_unit, isinf: _return_without_unit, isnan: _return_without_unit, signbit: _return_without_unit, copysign: _passthrough_unit, nextafter: _preserve_units, modf: _passthrough_unit, ldexp: _bitop_units, frexp: _return_without_unit, floor: _passthrough_unit, ceil: _passthrough_unit, trunc: _passthrough_unit, spacing: _passthrough_unit, positive: _passthrough_unit, divmod_: _passthrough_unit, isnat: _return_without_unit, heaviside: _preserve_units, matmul: _multiply_units, clip: _passthrough_unit, } if NUMPY_VERSION >= Version("2.0.0.dev0"): _ufunc_registry[vecdot] = _multiply_units __array_priority__ = 2.0 def __new__( cls, input_array, units=None, registry=None, dtype=None, *, bypass_validation=False, name=None, ): input_units = units if bypass_validation is True: if dtype is None: dtype = input_array.dtype obj = input_array.view(type=cls, dtype=dtype) obj.units = input_units if registry is not None: obj.units.registry = registry obj.name = name return obj if isinstance(input_array, unyt_array): ret = input_array.view(cls) if input_units is None: if registry is None: ret.units = input_array.units else: units = Unit(str(input_array.units), registry=registry) ret.units = units elif isinstance(input_units, Unit): ret.units = input_units else: ret.units = Unit(input_units, registry=registry) ret.name = name return ret elif isinstance(input_array, np.ndarray): pass elif _iterable(input_array) and input_array: if isinstance(input_array[0], unyt_array): return _coerce_iterable_units(input_array, registry) # Input array is an already formed ndarray instance # We first cast to be our class type obj = np.asarray(input_array, dtype=dtype).view(cls) # Check units type if input_units is None: # Nothing provided. Make dimensionless... units = Unit() elif isinstance(input_units, Unit): if registry and registry is not input_units.registry: units = Unit(str(input_units), registry=registry) else: units = input_units else: # units kwarg set, but it's not a Unit object. # don't handle all the cases here, let the Unit class handle if # it's a str. units = Unit(input_units, registry=registry) # Attach the units and name obj.units = units obj.name = name return obj def __repr__(self): return np.array_repr(self) def __str__(self): return str(self.view(np.ndarray)) + " " + str(self.units) def __format__(self, format_spec): return f"{self.d.__format__(format_spec)} {self.units}" # # Start unit conversion methods # def convert_to_units(self, units, equivalence=None, **kwargs): """ Convert the array to the given units in-place. Optionally, an equivalence can be specified to convert to an equivalent quantity which is not in the same dimensions. Parameters ---------- units : Unit object or string The units you want to convert to. equivalence : string, optional The equivalence you wish to use. To see which equivalencies are supported for this object, try the ``list_equivalencies`` method. Default: None kwargs: optional Any additional keyword arguments are supplied to the equivalence Raises ------ If the provided unit does not have the same dimensions as the array this will raise a UnitConversionError Examples -------- >>> from unyt import cm, km >>> length = [3000, 2000, 1000]*cm >>> length.convert_to_units('m') >>> print(length) [30. 20. 10.] m """ units = _sanitize_units_convert(units, self.units.registry) if equivalence is None: conv_data = _check_em_conversion( self.units, units, registry=self.units.registry ) if any(conv_data): new_units, (conv_factor, offset) = _em_conversion( self.units, conv_data, units ) else: new_units = units (conv_factor, offset) = self.units.get_conversion_factor( new_units, self.dtype ) self.units = new_units values = self.d # if our dtype is an integer do the following somewhat awkward # dance to change the dtype in-place. We can't use astype # directly because that will create a copy and not update self if self.dtype.kind in ("u", "i"): # create a copy of the original data in floating point # form, it's possible this may lose precision for very # large integers dsize = values.dtype.itemsize if dsize == 1: raise ValueError( "Can't convert memory buffer in place. " f"Input dtype ({self.dtype}) has a smaller itemsize than the " "smallest floating point representation possible." ) new_dtype = "f" + str(dsize) large = LARGE_INPUT.get(dsize, 0) if large and np.any(np.abs(values) > large): warnings.warn( f"Overflow encountered while converting to units '{new_units}'", RuntimeWarning, stacklevel=2, ) float_values = values.astype(new_dtype) # change the dtypes in-place, this does not change the # underlying memory buffer values.dtype = new_dtype self.dtype = new_dtype # actually fill in the new float values now that our # dtype is correct np.copyto(values, float_values) values *= conv_factor if offset: np.subtract(values, offset, values) else: self.convert_to_equivalent(units, equivalence, **kwargs) def convert_to_base(self, unit_system=None, equivalence=None, **kwargs): """ Convert the array in-place to the equivalent base units in the specified unit system. Optionally, an equivalence can be specified to convert to an equivalent quantity which is not in the same dimensions. Parameters ---------- unit_system : string, optional The unit system to be used in the conversion. If not specified, the configured base units are used (defaults to MKS). equivalence : string, optional The equivalence you wish to use. To see which equivalencies are supported for this object, try the ``list_equivalencies`` method. Default: None kwargs: optional Any additional keyword arguments are supplied to the equivalence Raises ------ If the provided unit does not have the same dimensions as the array this will raise a UnitConversionError Examples -------- >>> from unyt import erg, s >>> E = 2.5*erg/s >>> E.convert_to_base("mks") >>> E unyt_quantity(2.5e-07, 'W') """ self.convert_to_units( self.units.get_base_equivalent(unit_system), equivalence=equivalence, **kwargs, ) def convert_to_cgs(self, equivalence=None, **kwargs): """ Convert the array and in-place to the equivalent cgs units. Optionally, an equivalence can be specified to convert to an equivalent quantity which is not in the same dimensions. Parameters ---------- equivalence : string, optional The equivalence you wish to use. To see which equivalencies are supported for this object, try the ``list_equivalencies`` method. Default: None kwargs: optional Any additional keyword arguments are supplied to the equivalence Raises ------ If the provided unit does not have the same dimensions as the array this will raise a UnitConversionError Examples -------- >>> from unyt import Newton >>> data = [1., 2., 3.]*Newton >>> data.convert_to_cgs() >>> data unyt_array([100000., 200000., 300000.], 'dyn') """ self.convert_to_units( self.units.get_cgs_equivalent(), equivalence=equivalence, **kwargs ) def convert_to_mks(self, equivalence=None, **kwargs): """ Convert the array and units to the equivalent mks units. Optionally, an equivalence can be specified to convert to an equivalent quantity which is not in the same dimensions. Parameters ---------- equivalence : string, optional The equivalence you wish to use. To see which equivalencies are supported for this object, try the ``list_equivalencies`` method. Default: None kwargs: optional Any additional keyword arguments are supplied to the equivalence Raises ------ If the provided unit does not have the same dimensions as the array this will raise a UnitConversionError Examples -------- >>> from unyt import dyne, erg >>> data = [1., 2., 3.]*erg >>> data unyt_array([1., 2., 3.], 'erg') >>> data.convert_to_mks() >>> data unyt_array([1.e-07, 2.e-07, 3.e-07], 'J') """ self.convert_to_units(self.units.get_mks_equivalent(), equivalence, **kwargs) def in_units(self, units, equivalence=None, **kwargs): """ Creates a copy of this array with the data converted to the supplied units, and returns it. Optionally, an equivalence can be specified to convert to an equivalent quantity which is not in the same dimensions. Parameters ---------- units : Unit object or string The units you want to get a new quantity in. equivalence : string, optional The equivalence you wish to use. To see which equivalencies are supported for this object, try the ``list_equivalencies`` method. Default: None kwargs: optional Any additional keyword arguments are supplied to the equivalence Raises ------ If the provided unit does not have the same dimensions as the array this will raise a UnitConversionError Examples -------- >>> from unyt import c, gram >>> m = 10*gram >>> E = m*c**2 >>> print(E.in_units('erg')) 8.987551787368176e+21 erg >>> print(E.in_units('J')) 898755178736817.6 J """ units = _sanitize_units_convert(units, self.units.registry) if equivalence is None: conv_data = _check_em_conversion( self.units, units, registry=self.units.registry ) if any(conv_data): new_units, (conversion_factor, offset) = _em_conversion( self.units, conv_data, units ) offset = 0 else: new_units = units (conversion_factor, offset) = self.units.get_conversion_factor( new_units, self.dtype ) dsize = max(2, self.dtype.itemsize) if self.dtype.kind in ("u", "i"): large = LARGE_INPUT.get(dsize, 0) if large and np.any(np.abs(self.d) > large): warnings.warn( f"Overflow encountered while converting to units '{new_units}'", RuntimeWarning, stacklevel=2, ) new_dtypekind = "c" if self.dtype.kind == "c" else "f" new_dtype = np.dtype(new_dtypekind + str(dsize)) ret = np.asarray(self.ndview * conversion_factor, dtype=new_dtype) if offset: np.subtract(ret, offset, ret) try: new_array = type(self)( ret, new_units, bypass_validation=True, name=self.name ) except TypeError: # subclasses might not take name as a kwarg new_array = type(self)(ret, new_units, bypass_validation=True) return new_array else: return self.to_equivalent(units, equivalence, **kwargs) def to(self, units, equivalence=None, **kwargs): """ Creates a copy of this array with the data converted to the supplied units, and returns it. Optionally, an equivalence can be specified to convert to an equivalent quantity which is not in the same dimensions. .. note:: All additional keyword arguments are passed to the equivalency, which should be used if that particular equivalency requires them. Parameters ---------- units : Unit object or string The units you want to get a new quantity in. equivalence : string, optional The equivalence you wish to use. To see which equivalencies are supported for this unitful quantity, try the :meth:`list_equivalencies` method. Default: None kwargs: optional Any additional keyword arguments are supplied to the equivalence Raises ------ If the provided unit does not have the same dimensions as the array this will raise a UnitConversionError Examples -------- >>> from unyt import c, gram >>> m = 10*gram >>> E = m*c**2 >>> print(E.to('erg')) 8.987551787368176e+21 erg >>> print(E.to('J')) 898755178736817.6 J """ return self.in_units(units, equivalence=equivalence, **kwargs) def to_value(self, units=None, equivalence=None, **kwargs): """ Creates a copy of this array with the data in the supplied units, and returns it without units. Output is therefore a bare NumPy array. Optionally, an equivalence can be specified to convert to an equivalent quantity which is not in the same dimensions. .. note:: All additional keyword arguments are passed to the equivalency, which should be used if that particular equivalency requires them. Parameters ---------- units : Unit object or string, optional The units you want to get the bare quantity in. If not specified, the value will be returned in the current units. equivalence : string, optional The equivalence you wish to use. To see which equivalencies are supported for this unitful quantity, try the :meth:`list_equivalencies` method. Default: None Examples -------- >>> from unyt import km >>> a = [3, 4, 5]*km >>> print(a.to_value('cm')) [300000. 400000. 500000.] """ if units is None: v = self.value else: v = self.in_units(units, equivalence=equivalence, **kwargs).value if isinstance(self, unyt_quantity): return float(v) else: return v def in_base(self, unit_system=None): """ Creates a copy of this array with the data in the specified unit system, and returns it in that system's base units. Parameters ---------- unit_system : string, optional The unit system to be used in the conversion. If not specified, the configured default base units of are used (defaults to MKS). Examples -------- >>> from unyt import erg, s >>> E = 2.5*erg/s >>> print(E.in_base("mks")) 2.5e-07 W """ us = _sanitize_unit_system(unit_system, self) try: conv_data = _check_em_conversion( self.units, unit_system=us, registry=self.units.registry ) except MKSCGSConversionError: raise UnitsNotReducible(self.units, us) if any(conv_data): um = us.units_map u = self.units if u.dimensions in um and u.expr == um[self.units.dimensions]: return self.copy() to_units, (conv, offset) = _em_conversion(u, conv_data, unit_system=us) else: to_units = self.units.get_base_equivalent(unit_system) conv, offset = self.units.get_conversion_factor(to_units, self.dtype) ret = self.v * conv if offset: ret = ret - offset return type(self)(ret, to_units) def in_cgs(self): """ Creates a copy of this array with the data in the equivalent cgs units, and returns it. Returns ------- unyt_array object with data in this array converted to cgs units. Example ------- >>> from unyt import Newton, km >>> print((10*Newton/km).in_cgs()) 10.0 g/s**2 """ return self.in_base("cgs") def in_mks(self): """ Creates a copy of this array with the data in the equivalent mks units, and returns it. Returns ------- unyt_array object with data in this array converted to mks units. Example ------- >>> from unyt import mile >>> print((1.*mile).in_mks()) 1609.344 m """ return self.in_base("mks") def convert_to_equivalent(self, unit, equivalence, **kwargs): """ Convert the array in-place to the specified units, assuming the given equivalency. The dimensions of the specified units and the dimensions of the original array need not match so long as there is an appropriate conversion in the specified equivalency. Parameters ---------- unit : string The unit that you wish to convert to. equivalence : string The equivalence you wish to use. To see which equivalencies are supported for this unitful quantity, try the :meth:`list_equivalencies` method. Examples -------- >>> from unyt import K >>> a = [10, 20, 30]*(1e7*K) >>> a.convert_to_equivalent("keV", "thermal") >>> a unyt_array([ 8.6173324, 17.2346648, 25.8519972], 'keV') """ conv_unit = Unit(unit, registry=self.units.registry) if self.units.same_dimensions_as(conv_unit): self.convert_to_units(conv_unit) return this_equiv = equivalence_registry[equivalence](in_place=True) if self.has_equivalent(equivalence): this_equiv.convert(self, conv_unit.dimensions, **kwargs) self.convert_to_units(conv_unit) # set name to None since the semantic meaning has changed self.name = None else: raise InvalidUnitEquivalence(equivalence, self.units, conv_unit) def to_equivalent(self, unit, equivalence, **kwargs): """ Return a copy of the unyt_array in the units specified units, assuming the given equivalency. The dimensions of the specified units and the dimensions of the original array need not match so long as there is an appropriate conversion in the specified equivalency. Parameters ---------- unit : string The unit that you wish to convert to. equivalence : string The equivalence you wish to use. To see which equivalencies are supported for this unitful quantity, try the :meth:`list_equivalencies` method. Examples -------- >>> from unyt import K >>> a = 1.0e7*K >>> print(a.to_equivalent("keV", "thermal")) 0.8617332401096504 keV """ conv_unit = Unit(unit, registry=self.units.registry) if self.units.same_dimensions_as(conv_unit): return self.in_units(conv_unit) this_equiv = equivalence_registry[equivalence]() if self.has_equivalent(equivalence): new_arr = this_equiv.convert(self, conv_unit.dimensions, **kwargs) return new_arr.in_units(conv_unit) else: raise InvalidUnitEquivalence(equivalence, self.units, unit) def list_equivalencies(self): """ Lists the possible equivalencies associated with this unyt_array or unyt_quantity. Example ------- >>> from unyt import km >>> (1.0*km).list_equivalencies() spectral: length <-> spatial_frequency <-> frequency <-> energy schwarzschild: mass <-> length compton: mass <-> length """ self.units.list_equivalencies() def has_equivalent(self, equivalence): """ Check to see if this unyt_array or unyt_quantity has an equivalent unit in *equiv*. Example ------- >>> from unyt import km, keV >>> (1.0*km).has_equivalent('spectral') True >>> print((1*km).to_equivalent('MHz', equivalence='spectral')) 0.299792458 MHz >>> print((1*keV).to_equivalent('angstrom', equivalence='spectral')) 12.39841931521966 Å """ return self.units.has_equivalent(equivalence) def ndarray_view(self): """ Returns a view into the array as a numpy array Returns ------- View of this array's data. Example ------- >>> from unyt import km >>> a = [3, 4, 5]*km >>> a unyt_array([3, 4, 5], 'km') >>> a.ndarray_view() array([3, 4, 5]) This function returns a view that shares the same underlying memory as the original array. >>> b = a.ndarray_view() >>> b.base is a.base True >>> b[2] = 4 >>> b array([3, 4, 4]) >>> a unyt_array([3, 4, 4], 'km') """ return self.view(np.ndarray) def to_ndarray(self): """ Creates a copy of this array with the unit information stripped Example ------- >>> from unyt import km >>> a = [3, 4, 5]*km >>> a unyt_array([3, 4, 5], 'km') >>> b = a.to_ndarray() >>> b array([3, 4, 5]) The returned array will contain a copy of the data contained in the original array. >>> a.base is not b.base True """ return np.array(self) def argsort(self, axis=-1, kind="quicksort", order=None): """ Returns the indices that would sort the array. See the documentation of ndarray.argsort for details about the keyword arguments. Example ------- >>> from unyt import km >>> data = [3, 8, 7]*km >>> print(np.argsort(data)) [0 2 1] km >>> print(data.argsort()) [0 2 1] """ return self.view(np.ndarray).argsort(axis, kind, order) @classmethod def from_astropy(cls, arr, unit_registry=None): """ Convert an AstroPy "Quantity" to a unyt_array or unyt_quantity. Parameters ---------- arr : AstroPy Quantity The Quantity to convert from. unit_registry : unyt.UnitRegistry, optional A unyt unit registry to use in the conversion. If one is not supplied, the default one will be used. Example ------- >>> from astropy.units import km >>> unyt_quantity.from_astropy(km) unyt_quantity(1., 'km') >>> a = [1, 2, 3]*km >>> a >>> unyt_array.from_astropy(a) unyt_array([1., 2., 3.], 'km') """ # Converting from AstroPy Quantity try: u = arr.unit _arr = arr except AttributeError: u = arr _arr = 1.0 * u ap_units = [] for base, exponent in zip(u.bases, u.powers): unit_str = base.to_string() # we have to do this because AstroPy is silly and defines # hour as "h" if unit_str == "h": unit_str = "hr" ap_units.append(f"{unit_str}**({Rational(exponent)})") ap_units = "*".join(ap_units) if isinstance(_arr.value, np.ndarray) and _arr.shape != (): return unyt_array(_arr.value, ap_units, registry=unit_registry) else: return unyt_quantity(_arr.value, ap_units, registry=unit_registry) def to_astropy(self, **kwargs): """ Creates a new AstroPy quantity with the same unit information. Example ------- >>> from unyt import g, cm >>> data = [3, 4, 5]*g/cm**3 >>> data.to_astropy() """ if self.units.is_dimensionless: s_units = "" else: s_units = str(self.units) return self.value * _astropy.units.Unit(s_units, **kwargs) @classmethod def from_pint(cls, arr, unit_registry=None): """ Convert a Pint "Quantity" to a unyt_array or unyt_quantity. Parameters ---------- arr : Pint Quantity The Quantity to convert from. unit_registry : unyt.UnitRegistry, optional A unyt unit registry to use in the conversion. If one is not supplied, the default one will be used. Examples -------- >>> from pint import UnitRegistry >>> import numpy as np >>> ureg = UnitRegistry() >>> a = np.arange(4) >>> b = ureg.Quantity(a, "erg/cm**3") >>> b >>> c = unyt_array.from_pint(b) >>> c unyt_array([0, 1, 2, 3], 'erg/cm**3') """ p_units = [] for base, exponent in arr._units.items(): bs = convert_pint_units(base) p_units.append(f"{bs}**({Rational(exponent)})") p_units = "*".join(p_units) if isinstance(arr.magnitude, np.ndarray): return unyt_array(arr.magnitude, p_units, registry=unit_registry) else: return unyt_quantity(arr.magnitude, p_units, registry=unit_registry) def to_pint(self, unit_registry=None): """ Convert a unyt_array or unyt_quantity to a Pint Quantity. Parameters ---------- arr : unyt_array or unyt_quantity The unitful quantity to convert from. unit_registry : Pint UnitRegistry, optional The Pint UnitRegistry to use in the conversion. If one is not supplied, the default one will be used. NOTE: This is not the same as a unyt.UnitRegistry object. Examples -------- >>> from unyt import cm, s >>> a = 4*cm**2/s >>> print(a) 4 cm**2/s >>> a.to_pint() """ if unit_registry is None: unit_registry = _pint.UnitRegistry() powers_dict = self.units.expr.as_powers_dict() units = [] for unit, pow in powers_dict.items(): # we have to do this because Pint doesn't recognize # "yr" as "year" if str(unit).endswith("yr") and len(str(unit)) in [2, 3]: unit = str(unit).replace("yr", "year") units.append(f"{unit}**({Rational(pow)})") units = "*".join(units) return unit_registry.Quantity(self.value, units) @staticmethod def from_string(s, unit_registry=None): """ Parse a string to a unyt_quantity object. Parameters ---------- s: str A string representing a single quantity. unit_registry: unyt.UnitRegistry, optional A unyt unit registry to use in the conversion. If one is not supplied, the default one will be used. Examples -------- >>> from unyt import unyt_quantity >>> unyt_quantity.from_string("1cm") unyt_quantity(1, 'cm') >>> unyt_quantity.from_string("+1e3 Mearth") unyt_quantity(1000., 'Mearth') >>> unyt_quantity.from_string("-10. kg") unyt_quantity(-10., 'kg') >>> unyt_quantity.from_string(".66\tum") unyt_quantity(0.66, 'μm') >>> unyt_quantity.from_string("42") unyt_quantity(42, '(dimensionless)') >>> unyt_quantity.from_string("1.0 g/cm**3") unyt_quantity(1., 'g/cm**3') """ v = s.strip() if re.fullmatch(_NUMB_REGEXP, v): num = re.match(_NUMB_REGEXP, v).group() unit = Unit() elif re.fullmatch(_UNIT_REGEXP, v): num = 1 unit = Unit(re.match(_UNIT_REGEXP, v).group()) elif not re.fullmatch(_QUAN_REGEXP, v): raise ValueError(f"Received invalid quantity expression '{s}'.") else: res = re.search(_NUMB_REGEXP, v) num = res.group() res = re.search(_UNIT_REGEXP, v[res.span()[1] :]) unit = res.group().strip() if unit.startswith(("/", "*")): unit = f"1{unit}" try: num = int(num) except ValueError: num = float(num) return num * Unit(unit, registry=unit_registry) def to_string(self): # this is implemented purely for symmetry's sake return str(self) # # End unit conversion methods # def write_hdf5(self, filename, dataset_name=None, info=None, group_name=None): r"""Writes a unyt_array to hdf5 file. Parameters ---------- filename: string The filename to create and write a dataset to dataset_name: string The name of the dataset to create in the file. info: dictionary A dictionary of supplementary info to write to append as attributes to the dataset. group_name: string An optional group to write the arrays to. If not specified, the arrays are datasets at the top level by default. Examples -------- >>> from unyt import cm >>> a = [1,2,3]*cm >>> myinfo = {'field':'dinosaurs', 'type':'field_data'} >>> a.write_hdf5('test_array_data.h5', dataset_name='dinosaurs', ... info=myinfo) # doctest: +SKIP """ import pickle from unyt._on_demand_imports import _h5py as h5py if info is None: info = {} info["units"] = str(self.units) lut = {} for k, v in self.units.registry.lut.items(): if k not in default_unit_registry.lut: lut[k] = v info["unit_registry"] = np.void(pickle.dumps(lut)) if dataset_name is None: dataset_name = "array_data" f = h5py.File(filename, "a") if group_name is not None: if group_name in f: g = f[group_name] else: g = f.create_group(group_name) else: g = f if dataset_name in g.keys(): d = g[dataset_name] # Overwrite without deleting if we can get away with it. if d.shape == self.shape and d.dtype == self.dtype: d[...] = self for k in d.attrs.keys(): del d.attrs[k] else: del f[dataset_name] d = g.create_dataset(dataset_name, data=self) else: d = g.create_dataset(dataset_name, data=self) for k, v in info.items(): d.attrs[k] = v f.close() @classmethod def from_hdf5(cls, filename, dataset_name=None, group_name=None): r"""Attempts read in and convert a dataset in an hdf5 file into a unyt_array. Parameters ---------- filename: string The filename to of the hdf5 file. dataset_name: string The name of the dataset to read from. If the dataset has a units attribute, attempt to infer units as well. group_name: string An optional group to read the arrays from. If not specified, the arrays are datasets at the top level by default. """ import pickle from unyt._on_demand_imports import _h5py as h5py if dataset_name is None: dataset_name = "array_data" f = h5py.File(filename, "r") if group_name is not None: g = f[group_name] else: g = f dataset = g[dataset_name] data = dataset[...] units = dataset.attrs.get("units", "") unit_lut = default_unit_symbol_lut.copy() unit_lut_load = pickle.loads(dataset.attrs["unit_registry"].tobytes()) unit_lut.update(unit_lut_load) f.close() registry = UnitRegistry(lut=unit_lut, add_default_symbols=False) return cls(data, units, registry=registry) # # Start convenience methods # @property def value(self): """ Creates a copy of this array with the unit information stripped Example ------- >>> from unyt import km >>> a = [3, 4, 5]*km >>> a unyt_array([3, 4, 5], 'km') >>> b = a.value >>> b array([3, 4, 5]) The returned array will contain a copy of the data contained in the original array. >>> a.base is not b.base True """ return np.array(self) @property def v(self): """ Creates a copy of this array with the unit information stripped Example ------- >>> from unyt import km >>> a = [3, 4, 5]*km >>> a unyt_array([3, 4, 5], 'km') >>> b = a.v >>> b array([3, 4, 5]) The returned array will contain a copy of the data contained in the original array. >>> a.base is not b.base True """ return np.array(self) @property def ndview(self): """ Returns a view into the array as a numpy array Returns ------- View of this array's data. Example ------- >>> from unyt import km >>> a = [3, 4, 5]*km >>> a unyt_array([3, 4, 5], 'km') >>> a.ndview array([3, 4, 5]) This function returns a view that shares the same underlying memory as the original array. >>> b = a.ndview >>> b.base is a.base True >>> b[2] = 4 >>> b array([3, 4, 4]) >>> a unyt_array([3, 4, 4], 'km') """ return self.view(np.ndarray) @property def d(self): """ Returns a view into the array as a numpy array Returns ------- View of this array's data. Example ------- >>> from unyt import km >>> a = [3, 4, 5]*km >>> a unyt_array([3, 4, 5], 'km') >>> a.d array([3, 4, 5]) This function returns a view that shares the same underlying memory as the original array. >>> b = a.d >>> b.base is a.base True >>> b[2] = 4 >>> b array([3, 4, 4]) >>> a unyt_array([3, 4, 4], 'km') """ return self.view(np.ndarray) @property def unit_quantity(self): """ Return a quantity with a value of 1 and the same units as this array Example ------- >>> from unyt import km >>> a = [4, 5, 6]*km >>> a.unit_quantity unyt_quantity(1, 'km') >>> print(a + 7*a.unit_quantity) [11 12 13] km """ return unyt_quantity(1, self.units) @property def uq(self): """ Return a quantity with a value of 1 and the same units as this array Example ------- >>> from unyt import km >>> a = [4, 5, 6]*km >>> a.uq unyt_quantity(1, 'km') >>> print(a + 7*a.uq) [11 12 13] km """ return unyt_quantity(1, self.units) @property def unit_array(self): """ Return an array filled with ones with the same units as this array Example ------- >>> from unyt import km >>> a = [4, 5, 6]*km >>> a.unit_array unyt_array([1, 1, 1], 'km') >>> print(a + 7*a.unit_array) [11 12 13] km """ return np.ones_like(self) @property def ua(self): """ Return an array filled with ones with the same units as this array Example ------- >>> from unyt import km >>> a = [4, 5, 6]*km >>> a.unit_array unyt_array([1, 1, 1], 'km') >>> print(a + 7*a.unit_array) [11 12 13] km """ return np.ones_like(self) def __getitem__(self, item): ret = super().__getitem__(item) if getattr(ret, "shape", None) == (): ret = unyt_quantity(ret, bypass_validation=True, name=self.name) ret.units = self.units return ret def __setitem__(self, item, value): if hasattr(value, "units"): if value.units != self.units and value.units != NULL_UNIT: value = value.to(self.units) super().__setitem__(item, value) def __pow__(self, p, mod=None, /): """ Power function """ # see https://github.com/yt-project/unyt/issues/203 if np.isscalar(p) and p == 0.0: ret = self.unit_array ret.units = Unit("dimensionless") return ret else: return super().__pow__(p, mod) def __eq__(self, other): try: return super().__eq__(other) except (IterableUnitCoercionError, UnitOperationError): return np.zeros(self.shape, dtype="bool") def __ne__(self, other): try: return super().__ne__(other) except (IterableUnitCoercionError, UnitOperationError): return np.ones(self.shape, dtype="bool") # # Start operation methods # def __array_ufunc__(self, ufunc, method, *inputs, **kwargs): func = getattr(ufunc, method) if "out" not in kwargs: if ufunc in multiple_output_operators: out = (None,) * multiple_output_operators[ufunc] out_func = out else: out = None out_func = None else: # we need to get both the actual "out" object and a view onto it # in case we need to do in-place operations out = kwargs.pop("out") if ufunc in multiple_output_operators: out_func = [] for arr in out: out_func.append(arr.view(np.ndarray)) out_func = tuple(out_func) else: out = out[0] if out.dtype.kind in ("u", "i"): new_dtype = "f" + str(out.dtype.itemsize) float_values = out.astype(new_dtype) out.dtype = new_dtype np.copyto(out, float_values) out_func = out.view(np.ndarray) if len(inputs) == 1: # Unary ufuncs inp = inputs[0] u = getattr(inp, "units", None) if u.dimensions is angle and ufunc in trigonometric_operators: # ensure np.sin(90*degrees) works as expected inp = inp.in_units("radian").v # evaluate the ufunc out_arr = func(np.asarray(inp), out=out_func, **kwargs) if ufunc in (multiply, divide) and method == "reduce": mul, unit = _apply_power_mapping(ufunc, u, inp.size, inp.shape, kwargs) else: # get unit of result mul, unit = self._ufunc_registry[ufunc](u) # use type(self) here so we can support user-defined # subclasses of unyt_array ret_class = type(self) elif len(inputs) == 2: # binary ufuncs i0 = inputs[0] i1 = inputs[1] if "dask" in sys.modules and isinstance(i1, _dask.array.core.Array): # need to short circuit all this to handle binary operations # like unyt_quantity(2,'m') / unyt_dask_array_instance # only need to check the second argument as if the first arg # is a unyt_dask_array, it won't end up here. return i1.__array_ufunc__(ufunc, method, *inputs, **kwargs) # coerce inputs to be ndarrays if they aren't already inp0 = _coerce_iterable_units(i0) inp1 = _coerce_iterable_units(i1) u0 = getattr(i0, "units", None) or getattr(inp0, "units", None) u1 = getattr(i1, "units", None) or getattr(inp1, "units", None) ret_class = _get_binary_op_return_class(type(i0), type(i1)) if u0 is None: u0 = Unit(registry=getattr(u1, "registry", None)) if u1 is None and ufunc is not power: u1 = Unit(registry=getattr(u0, "registry", None)) elif ufunc is power: u1 = inp1 if inp0.shape == () or inp1.shape == (): if isinstance(u1, unyt_array) and not u1.units.is_dimensionless: raise UnitOperationError(ufunc, u0, u1.units) if u1.shape == (): u1 = float(u1) else: u1 = 1.0 elif inp0.shape == inp1.shape: if isinstance(u1, unyt_array) and not u1.units.is_dimensionless: raise UnitOperationError(ufunc, u0, getattr(u1, "units", None)) if ( (isinstance(u0, Unit) and not u0.is_dimensionless) or isinstance(u0, unyt_array) and not u0.units.is_dimensionless ): # u0 has units if np.ptp(u1) != 0: raise UnitOperationError( ufunc, u0, getattr(u1, "units", None) ) first_element_slice = (0,) * u1.ndim u1 = float(u1[first_element_slice]) else: raise UnitOperationError(ufunc, u0, u1) unit_operator = self._ufunc_registry[ufunc] if ( unit_operator is _preserve_units and u0.dimensions is temperature and u1 is not None and u1.base_offset != 0.0 and u0.base_offset == 0.0 and str(u0.expr) in ["K", "R"] ): raise UnitOperationError(ufunc, u0, u1) if unit_operator in ( _preserve_units, _comparison_unit, _arctan2_unit, _difference_units, ): # check "is" equality first for speed if u0 is not u1 and u0 != u1: # we allow adding, multiplying, comparisons with # zero-filled arrays, lists, etc or scalar zero. We # do not allow zero-filled unyt_array instances for # performance reasons. If we did allow it, every # binary operation would need to scan over all the # elements of both arrays to check for arrays filled # with zeros if not isinstance(i0, unyt_array) or not isinstance(i1, unyt_array): any_nonzero = [np.count_nonzero(i0), np.count_nonzero(i1)] if any_nonzero[0] == 0: u0 = u1 elif any_nonzero[1] == 0: u1 = u0 if not u0.same_dimensions_as(u1): if unit_operator is _comparison_unit: # we allow comparisons between data with # units and dimensionless data if u0.is_dimensionless: u0 = u1 elif u1.is_dimensionless: u1 = u0 else: # comparison with different units, so need to check if # this is == and != which we allow and handle in a # special way using an early return from __array_ufunc__ if ufunc in (equal, not_equal): if ufunc is equal: func = np.zeros_like else: func = np.ones_like ret = func(np.asarray(inp1), dtype=bool) if out is not None: out[:] = ret[:] if isinstance(out, unyt_array): out.units = Unit( "", registry=self.units.registry ) if ret.shape == (): ret = bool(ret) return ret else: raise UnitOperationError(ufunc, u0, u1) else: raise UnitOperationError(ufunc, u0, u1) conv, offset = u1.get_conversion_factor(u0, inp1.dtype) new_dtype = np.dtype("f" + str(inp1.dtype.itemsize)) conv = new_dtype.type(conv) if ( offset is not None and u1.base_offset != 0.0 and not repr(u0).startswith("delta_") ): raise InvalidUnitOperation( "Quantities with units of Fahrenheit or Celsius " "cannot be multiplied, divided, subtracted or " "added with data that has different units." ) inp1 = np.asarray(inp1, dtype=new_dtype) * conv # get the unit of the result mul, unit = unit_operator(u0, u1) # actually evaluate the ufunc out_arr = func( inp0.view(np.ndarray), inp1.view(np.ndarray), out=out_func, **kwargs ) if unit_operator in (_multiply_units, _divide_units): if unit.is_dimensionless and unit.base_value != 1.0: if not u0.is_dimensionless: if u0.dimensions == u1.dimensions: out_arr = np.multiply( out_arr.view(np.ndarray), unit.base_value, out=out_func ) unit = Unit(registry=unit.registry) if ( u0.base_offset and u0.dimensions is temperature or u1.base_offset and u1.dimensions is temperature ): raise InvalidUnitOperation( "Quantities with units of Fahrenheit or Celsius " "cannot be multiplied, divided, subtracted or added." ) else: if ufunc is clip: inp = [] for i in inputs: if isinstance(i, unyt_array): inp.append(i.to(inputs[0].units).view(np.ndarray)) else: inp.append(i) if out is not None: _out = out.view(np.ndarray) else: _out = None out_arr = ufunc(*inp, out=_out) unit = inputs[0].units ret_class = type(inputs[0]) mul = 1 else: raise RuntimeError( f"Support for the {ufunc} ufunc with {len(inputs)} " "inputs has not been added to unyt_array." ) if unit is None: out_arr = np.array(out_arr, copy=_COPY_IF_NEEDED) elif ufunc in (modf, divmod_): out_arr = tuple(ret_class(o, unit) for o in out_arr) elif out_arr.shape == (): out_arr = unyt_quantity(np.asarray(out_arr), unit) elif out_arr.size == 1: out_arr = unyt_array(np.asarray(out_arr), unit) else: if issubclass(ret_class, unyt_quantity): # This happens if you do ndarray * unyt_quantity. # Explicitly casting to unyt_array avoids creating a # unyt_quantity with size > 1 out_arr = unyt_array(out_arr, unit) else: out_arr = ret_class(out_arr, unit, bypass_validation=True) if out is not None: if mul != 1: multiply(out, mul, out=out) if np.shares_memory(out_arr, out): mul = 1 if isinstance(out, unyt_array): try: out.units = out_arr.units except AttributeError: # out_arr is an ndarray out.units = Unit("", registry=self.units.registry) elif isinstance(out, tuple): for o, oa in zip(out, out_arr): if o is None: continue o.units = oa.units if mul == 1: return out_arr return mul * out_arr def __array_function__(self, func, types, args, kwargs): # Follow NEP 18 guidelines # https://numpy.org/neps/nep-0018-array-function-protocol.html from unyt._array_functions import _HANDLED_FUNCTIONS, _UNSUPPORTED_FUNCTIONS if func in _UNSUPPORTED_FUNCTIONS: # following NEP 18, return NotImplemented as a sentinel value # which will lead to raising a TypeError, while # leaving other arguments a chance to take the lead return NotImplemented if func not in _HANDLED_FUNCTIONS: # default to numpy's private implementation return func._implementation(*args, **kwargs) # Note: this allows subclasses that don't override # __array_function__ to handle unyt_array objects if not all(issubclass(t, unyt_array) or t is np.ndarray for t in types): return NotImplemented return _HANDLED_FUNCTIONS[func](*args, **kwargs) def copy(self, order="C"): """ Return a copy of the array. Parameters ---------- order : {'C', 'F', 'A', 'K'}, optional Controls the memory layout of the copy. 'C' means C-order, 'F' means F-order, 'A' means 'F' if `a` is Fortran contiguous, 'C' otherwise. 'K' means match the layout of `a` as closely as possible. (Note that this function and :func:`numpy.copy` are very similar, but have different default values for their order= arguments.) See also -------- numpy.copy numpy.copyto Examples -------- >>> from unyt import km >>> x = [[1,2,3],[4,5,6]] * km >>> y = x.copy() >>> x.fill(0) >>> print(x) [[0 0 0] [0 0 0]] km >>> print(y) [[1 2 3] [4 5 6]] km """ name = getattr(self, "name", None) try: return type(self)(np.copy(np.asarray(self)), self.units, name=name) except TypeError: # subclasses might not take name as a kwarg return type(self)(np.copy(np.asarray(self)), self.units) def __array_finalize__(self, obj): self.units = getattr(obj, "units", NULL_UNIT) self.name = getattr(obj, "name", None) def __pos__(self): """Posify the data.""" # this needs to be defined for all numpy versions, see # numpy issue #9081 return type(self)(super().__pos__(), self.units) def dot(self, b, out=None): """dot product of two arrays. Refer to `numpy.dot` for full documentation. See Also -------- numpy.dot : equivalent function Examples -------- >>> from unyt import km, s >>> a = np.eye(2)*km >>> b = (np.ones((2, 2)) * 2)*s >>> print(a.dot(b)) [[2. 2.] [2. 2.]] km*s This array method can be conveniently chained: >>> print(a.dot(b).dot(b)) [[8. 8.] [8. 8.]] km*s**2 """ res_units = self.units * getattr(b, "units", NULL_UNIT) ret = self.view(np.ndarray).dot(np.asarray(b), out=out) * res_units if out is not None: out.units = res_units return ret def take(self, indices, axis=None, out=None, mode="raise"): """method Return an array formed from the elements of `a` at the given indices. Refer to :func:`numpy.take` for full documentation. See also -------- numpy.take : equivalent function """ from ._array_functions import take return take(self, indices, axis=axis, out=out, mode=mode) def __reduce__(self): """Pickle reduction method See the documentation for the standard library pickle module: http://docs.python.org/2/library/pickle.html Unit metadata is encoded in the zeroth element of third element of the returned tuple, itself a tuple used to restore the state of the ndarray. This is always defined for numpy arrays. """ np_ret = super().__reduce__() obj_state = np_ret[2] unit_state = (((str(self.units), self.units.registry.lut),) + obj_state[:],) new_ret = np_ret[:2] + unit_state + np_ret[3:] return new_ret def __setstate__(self, state): """Pickle setstate method This is called inside pickle.read() and restores the unit data from the metadata extracted in __reduce__ and then serialized by pickle. """ super().__setstate__(state[1:]) unit, lut = state[0] lut = _correct_old_unit_registry(lut) registry = UnitRegistry(lut=lut, add_default_symbols=False) self.units = Unit(unit, registry=registry) def __deepcopy__(self, memodict=None): """copy.deepcopy implementation This is necessary for stdlib deepcopy of arrays and quantities. """ ret = super().__deepcopy__(memodict) try: return type(self)(ret, copy.deepcopy(self.units), name=self.name) except TypeError: # subclasses might not take name as a kwarg return type(self)(ret, copy.deepcopy(self.units)) class unyt_quantity(unyt_array): """ A scalar associated with a unit. Parameters ---------- input_scalar : an integer or floating point scalar The scalar to attach units to units : String unit specification, unit symbol object, or astropy units The units of the quantity. Powers must be specified using python syntax (cm**3, not cm^3). registry : A UnitRegistry object The registry to create units from. If units is already associated with a unit registry and this is specified, this will be used instead of the registry associated with the unit object. dtype : data-type The dtype of the array data. bypass_validation : boolean If True, all input validation is skipped. Using this option may produce corrupted, invalid units or array data, but can lead to significant speedups in the input validation logic adds significant overhead. If set, units *must* be a valid unit object. Defaults to False. name : string The name of the scalar. Defaults to None. This attribute does not propagate through mathematical operations, but is preserved under indexing and unit conversions. Examples -------- >>> a = unyt_quantity(3., 'cm') >>> b = unyt_quantity(2., 'm') >>> print(a + b) 203.0 cm >>> print(b + a) 2.03 m NumPy ufuncs will pass through units where appropriate. >>> import numpy as np >>> from unyt import g, cm >>> a = 12*g/cm**3 >>> print(np.abs(a)) 12 g/cm**3 and strip them when it would be annoying to deal with them. >>> print(np.log10(a)) 1.0791812460476249 """ def __new__( cls, input_scalar, units=None, registry=None, dtype=None, *, bypass_validation=False, name=None, ): input_units = units if not ( bypass_validation or isinstance(input_scalar, (numeric_type, np.number, np.ndarray)) ): raise RuntimeError("unyt_quantity values must be numeric") if input_units is None: units = getattr(input_scalar, "units", None) else: units = input_units ret = unyt_array.__new__( cls, np.asarray(input_scalar), units, registry, dtype=dtype, bypass_validation=bypass_validation, name=name, ) if ret.size > 1: raise RuntimeError("unyt_quantity instances must be scalars") return ret def __round__(self): return type(self)(round(float(self)), self.units) def reshape(self, *shape, order="C"): # this is necessary to support some numpy operations # natively, like numpy.meshgrid, which internally performs # reshaping, e.g., arr.reshape(1, -1), which doesn't affect the size, # but does change the object's internal representation to a >0D array # see https://github.com/yt-project/unyt/issues/224 if len(shape) == 1: shape = shape[0] if shape == () or shape is None: return super().reshape(shape, order=order) else: return unyt_array(self).reshape(shape, order=order) def _validate_numpy_wrapper_units(v, arrs): if not any(isinstance(a, unyt_array) for a in arrs): return v if not all(isinstance(a, unyt_array) for a in arrs): raise RuntimeError("Not all of your arrays are unyt_arrays.") a1 = arrs[0] if not all(a.units == a1.units for a in arrs[1:]): raise RuntimeError("Your arrays must have identical units.") v.units = a1.units return v def uconcatenate(arrs, axis=0): """Concatenate a sequence of arrays. This wrapper around numpy.concatenate preserves units. All input arrays must have the same units. See the documentation of numpy.concatenate for full details. Examples -------- >>> from unyt import cm >>> A = [1, 2, 3]*cm >>> B = [2, 3, 4]*cm >>> uconcatenate((A, B)) # doctest: +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): DeprecationWarning: ... unyt_array([1, 2, 3, 2, 3, 4], 'cm') """ warn_deprecated( "unyt.uconcatenate", replacement="use numpy.concatenate", since_version="3.0" ) v = np.concatenate._implementation(arrs, axis=axis) v = _validate_numpy_wrapper_units(v, arrs) return v def ucross(arr1, arr2, registry=None, axisa=-1, axisb=-1, axisc=-1, axis=None): """Applies the cross product to two YT arrays. This wrapper around numpy.cross preserves units. See the documentation of numpy.cross for full details. """ warn_deprecated( "unyt.ucross", replacement=( "use numpy.cross (note that the *registry* argument will not be ported)" ), since_version="3.0", ) v = np.cross._implementation( arr1, arr2, axisa=axisa, axisb=axisb, axisc=axisc, axis=axis ) units = arr1.units * arr2.units arr = unyt_array(v, units, registry=registry) return arr def uintersect1d(arr1, arr2, assume_unique=False): """Find the sorted unique elements of the two input arrays. A wrapper around numpy.intersect1d that preserves units. All input arrays must have the same units. See the documentation of numpy.intersect1d for full details. Examples -------- >>> from unyt import cm >>> A = [1, 2, 3]*cm >>> B = [2, 3, 4]*cm >>> uintersect1d(A, B) # doctest: +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): DeprecationWarning: ... unyt_array([2, 3], 'cm') """ warn_deprecated( "unyt.uintersect1d", replacement="use numpy.intersect1d", since_version="3.0" ) return np.intersect1d(arr1, arr2, assume_unique=assume_unique) def uunion1d(arr1, arr2): """Find the union of two arrays. A wrapper around numpy.intersect1d that preserves units. All input arrays must have the same units. See the documentation of numpy.intersect1d for full details. Examples -------- >>> from unyt import cm >>> A = [1, 2, 3]*cm >>> B = [2, 3, 4]*cm >>> uunion1d(A, B) # doctest: +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): DeprecationWarning: ... unyt_array([1, 2, 3, 4], 'cm') """ warn_deprecated( "unyt.uunion1d", replacement="use numpy.union1d", since_version="3.0" ) return np.union1d(arr1, arr2) def unorm(data, ord=None, axis=None, keepdims=False): """Matrix or vector norm that preserves units This is a wrapper around np.linalg.norm that preserves units. See the documentation for that function for descriptions of the keyword arguments. Examples -------- >>> from unyt import km >>> data = [1, 2, 3]*km >>> print(unorm(data)) # doctest: +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): DeprecationWarning: ... 3.7416573867739413 km """ warn_deprecated( "unyt.unorm", replacement="use numpy.linalg.norm", since_version="3.0" ) norm = np.linalg.norm._implementation(data, ord=ord, axis=axis, keepdims=keepdims) if norm.shape == (): return unyt_quantity(norm, data.units) return unyt_array(norm, data.units) def udot(op1, op2): """Matrix or vector dot product that preserves units This is a wrapper around np.dot that preserves units. Examples -------- >>> from unyt import km, s >>> a = np.eye(2)*km >>> b = (np.ones((2, 2)) * 2)*s >>> print(udot(a, b)) # doctest: +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): DeprecationWarning: ... [[2. 2.] [2. 2.]] km*s """ warn_deprecated("unyt.udot", replacement="use numpy.dot", since_version="3.0") dot = np.dot._implementation(op1.d, op2.d) units = op1.units * op2.units if dot.shape == (): return unyt_quantity(dot, units) return unyt_array(dot, units) def uvstack(arrs): """Stack arrays in sequence vertically (row wise) while preserving units This is a wrapper around np.vstack that preserves units. Examples -------- >>> from unyt import km >>> a = [1, 2, 3]*km >>> b = [2, 3, 4]*km >>> print(uvstack([a, b])) # doctest: +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): DeprecationWarning: ... [[1 2 3] [2 3 4]] km """ warn_deprecated("unyt.uvstack", replacement="use numpy.vstack", since_version="3.0") v = np.vstack._implementation(arrs) v = _validate_numpy_wrapper_units(v, arrs) return v def uhstack(arrs): """Stack arrays in sequence horizontally while preserving units This is a wrapper around np.hstack that preserves units. Examples -------- >>> from unyt import km >>> a = [1, 2, 3]*km >>> b = [2, 3, 4]*km >>> print(uhstack([a, b])) # doctest: +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): DeprecationWarning: ... [1 2 3 2 3 4] km >>> a = [[1],[2],[3]]*km >>> b = [[2],[3],[4]]*km >>> print(uhstack([a, b])) # doctest: +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): DeprecationWarning: ... [[1 2] [2 3] [3 4]] km """ warn_deprecated("unyt.uhstack", replacement="use numpy.hstack", since_version="3.0") v = np.hstack._implementation(arrs) v = _validate_numpy_wrapper_units(v, arrs) return v def ustack(arrs, axis=0): """Join a sequence of arrays along a new axis while preserving units The axis parameter specifies the index of the new axis in the dimensions of the result. For example, if ``axis=0`` it will be the first dimension and if ``axis=-1`` it will be the last dimension. This is a wrapper around np.stack that preserves units. See the documentation for np.stack for full details. Examples -------- >>> from unyt import km >>> a = [1, 2, 3]*km >>> b = [2, 3, 4]*km >>> print(ustack([a, b])) # doctest: +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): DeprecationWarning: ... [[1 2 3] [2 3 4]] km """ warn_deprecated("unyt.ustack", replacement="use numpy.stack", since_version="3.0") v = np.stack._implementation(arrs, axis=axis) v = _validate_numpy_wrapper_units(v, arrs) return v def _get_binary_op_return_class(cls1, cls2): if cls1 is cls2: return cls1 if cls1 in (Unit, np.ndarray, np.matrix, np.ma.masked_array) or issubclass( cls1, (numeric_type, np.number, list, tuple) ): return cls2 if cls2 in (Unit, np.ndarray, np.matrix, np.ma.masked_array) or issubclass( cls2, (numeric_type, np.number, list, tuple) ): return cls1 if issubclass(cls1, unyt_quantity): return cls2 if issubclass(cls2, unyt_quantity): return cls1 if issubclass(cls1, cls2): return cls1 if issubclass(cls2, cls1): return cls2 else: raise RuntimeError( "Undefined operation for a unyt_array subclass. " f"Received operand types ({cls1}) and ({cls2})" ) def loadtxt(fname, dtype="float", delimiter="\t", usecols=None, comments="#"): r""" Load unyt_arrays with unit information from a text file. Each row in the text file must have the same number of values. Parameters ---------- fname : str Filename to read. dtype : data-type, optional Data-type of the resulting array; default: float. delimiter : str, optional The string used to separate values. By default, this is any whitespace. usecols : sequence, optional Which columns to read, with 0 being the first. For example, ``usecols = (1,4,5)`` will extract the 2nd, 5th and 6th columns. The default, None, results in all columns being read. comments : str, optional The character used to indicate the start of a comment; default: '#'. Examples -------- >>> temp, velx = loadtxt( ... "sphere.dat", usecols=(1,2), delimiter="\t") # doctest: +SKIP """ f = open(fname) next_one = False units = [] num_cols = -1 for line in f.readlines(): words = line.strip().split() if len(words) == 0: continue if line[0] == comments: if next_one: units = words[1:] if len(words) == 2 and words[1] == "Units": next_one = True else: # Here we catch the first line of numbers col_words = line.strip().split(delimiter) for word in col_words: # test that word can be converted to a number complex(word) num_cols = len(col_words) break f.close() if len(units) != num_cols: units = ["dimensionless"] * num_cols arrays = np.loadtxt( fname, dtype=dtype, comments=comments, delimiter=delimiter, converters=None, unpack=True, usecols=usecols, ndmin=0, ) if len(arrays.shape) < 2: arrays = [arrays] if usecols is not None: units = [units[col] for col in usecols] ret = tuple(unyt_array(arr, unit) for arr, unit in zip(arrays, units)) if len(ret) == 1: return ret[0] return ret def savetxt( fname, arrays, fmt="%.18e", delimiter="\t", header="", footer="", comments="#" ): r""" Write unyt_arrays with unit information to a text file. Parameters ---------- fname : str The file to write the unyt_arrays to. arrays : list of unyt_arrays or single unyt_array The array(s) to write to the file. fmt : str or sequence of strs, optional A single format (%10.5f), or a sequence of formats. delimiter : str, optional String or character separating columns. header : str, optional String that will be written at the beginning of the file, before the unit header. footer : str, optional String that will be written at the end of the file. comments : str, optional String that will be prepended to the ``header`` and ``footer`` strings, to mark them as comments. Default: '# ', as expected by e.g. ``unyt.loadtxt``. Examples -------- >>> import unyt as u >>> a = [1, 2, 3]*u.cm >>> b = [8, 10, 12]*u.cm/u.s >>> c = [2, 85, 9]*u.g >>> savetxt("sphere.dat", [a,b,c], header='My sphere stuff', ... delimiter="\t") # doctest: +SKIP """ if not isinstance(arrays, list): arrays = [arrays] units = [] for array in arrays: if hasattr(array, "units"): units.append(str(array.units)) else: units.append("dimensionless") if header != "" and not header.endswith("\n"): header += "\n" header += " Units\n " + "\t".join(units) np.savetxt( fname, np.transpose(arrays), header=header, fmt=fmt, delimiter=delimiter, footer=footer, newline="\n", comments=comments, ) def allclose_units(actual, desired, rtol=1e-7, atol=0, **kwargs): """Returns False if two objects are not equal up to desired tolerance This is a wrapper for :func:`numpy.allclose` that also verifies unit consistency Parameters ---------- actual : array-like Array obtained (possibly with attached units) desired : array-like Array to compare with (possibly with attached units) rtol : float, optional Relative tolerance, defaults to 1e-7 atol : float or quantity, optional Absolute tolerance. If units are attached, they must be consistent with the units of ``actual`` and ``desired``. If no units are attached, assumes the same units as ``desired``. Defaults to zero. Raises ------ RuntimeError If units of ``rtol`` are not dimensionless See Also -------- :func:`unyt.testing.assert_allclose_units` Notes ----- Also accepts additional keyword arguments accepted by :func:`numpy.allclose`, see the documentation of that function for details. Examples -------- >>> import unyt as u >>> actual = [1e-5, 1e-3, 1e-1]*u.m >>> desired = actual.to("cm") >>> allclose_units(actual, desired) True """ # Create a copy to ensure this function does not alter input arrays act = unyt_array(actual) des = unyt_array(desired) try: des = des.in_units(act.units) except (UnitOperationError, UnitConversionError): return False rt = unyt_array(rtol) if not rt.units.is_dimensionless: raise RuntimeError(f"Units of rtol ({rt.units}) are not dimensionless") if not isinstance(atol, unyt_array): at = unyt_quantity(atol, des.units) else: at = atol try: at = at.in_units(act.units) except (UnitOperationError, UnitConversionError): return False # units have been validated, so we strip units before calling numpy # to avoid spurious errors act = act.value des = des.value rt = rt.value at = at.value return np.allclose(act, des, rt, at, **kwargs) unyt-3.0.4/unyt/dask_array.py000066400000000000000000000533271476461141700162330ustar00rootroot00000000000000""" a dask array class (unyt_dask_array) and helper functions for unyt. """ import sys from functools import wraps import numpy as np import unyt.array as ua if "pytest" in sys.modules: # should only happen if pytest is installed *and* already imported, # so we can skip collecting doctests from this module when dask isn't installed # while avoiding making pytest itself a hard dependency to this module. # This check is constructed to work with direct invocation (pytest unyt) # as well as through python -m pytest import pytest pytest.importorskip("dask") del pytest from dask.array.core import Array as DaskArray, finalize as dask_finalize # noqa: E402 # the following attributes hang off of dask.array.core.Array and do not modify units _use_unary_decorator = { "min", "max", "sum", "mean", "std", "cumsum", "squeeze", "rechunk", "clip", "view", "swapaxes", "round", "copy", "repeat", "astype", "reshape", "topk", } def _unary_dask_decorator(dask_func, current_unyt_dask): """ a decorator for the simpler dask functions that can just copy over the current unit info after applying the dask function. this includes functions that return single values (e.g., min()) or functions that change the array in ways that do not affect units like reshaping or rounding. Parameters ---------- dask_func: func handle the dask function handle to call current_unyt_dask: unyt_dask_array the current instance of a unyt_dask_array Returns a new unyt_dask_array instance with appropriate units """ @wraps(dask_func) def wrapper(*args, **kwargs): da = dask_func(*args, **kwargs) # will return standard dask array return _create_with_quantity(da, current_unyt_dask._unyt_array) return wrapper # the following list are the unyt_quantity/array attributes that will also # be attributes of the unyt_dask_array. These methods are wrapped with a unit # conversion decorator. _unyt_funcs_to_track = {"to", "in_units", "in_cgs", "in_base", "in_mks"} # helper sanitizing functions for handling ufunc, array operations def _extract_unyt(obj): # returns the hidden _unyt_quantity if it exists in obj, otherwise return obj if _is_iterable(obj): return [_extract_unyt(ob) for ob in obj] return obj._unyt_array if isinstance(obj, unyt_dask_array) else obj def _extract_dask(obj): # returns a plain dask array if the obj is a unyt_dask_array, otherwise return obj if _is_iterable(obj): return [_extract_dask(ob) for ob in obj] return obj.to_dask() if isinstance(obj, unyt_dask_array) else obj def _extract_unyt_val(obj): # returns the value of a unyt_quantity if obj is a unyt_quantity if _is_iterable(obj): return [_extract_unyt_val(ob) for ob in obj] return obj.to_value() if isinstance(obj, ua.unyt_quantity) else obj def _check_unyt_inputs(ui_0, ui_1): # returns True if the args need to be converted to the same unit, based on # if the units have the same dimensions but different units. if hasattr(ui_0, "units") and hasattr(ui_1, "units"): un_0, un_1 = ui_0.units, ui_1.units return un_0 != un_1 and un_0.dimensions == un_1.dimensions return False def _is_iterable(obj): return type(obj) is tuple or type(obj) is list # noqa E721 def _sanitize_unit_args(*input): # returns sanitized inputs and unyt_inputs for calling the ufunc unyt_inputs = _extract_unyt(input) if len(unyt_inputs) > 1: # note 1: even though we rely on the ufunc applied to the unyt quantities # to get the final units of our unyt_dask_array after an operation, # we need to first ensure that if our arguments have the same dimensions # they are in the same units. This happens internally for unyt_quantities # but we also need to apply those internal unit conversions to our # dask_unyt_array objects, so we do those checks manually here. # note 2: we do NOT check for validity of the operation here. The subsequent # call to the ufunc with the unyt_inputs will enforce the unyt rules # (e.g., addition must have same dimensions). ui_0, ui_1 = unyt_inputs[0], unyt_inputs[1] if _check_unyt_inputs(ui_0, ui_1): # convert to the unit with the larger base input = list(input) if ui_0.units.base_value < ui_1.units.base_value: input[0] = input[0].to(ui_1.units) else: input[1] = input[1].to(ui_0.units) unyt_inputs = _extract_unyt(input) return input, unyt_inputs def _prep_ufunc(ufunc, *input, extract_dask=False, **kwargs): # this function: # (1) sanitizes inputs for calls to __array_func__, __array__ and _elementwise # (2) applies the function to the hidden unyt quantities # (3) (optional) makes inputs extra clean: converts unyt_dask_array args to # plain dask array objects # apply the operation to the hidden unyt_quantities input, unyt_inputs = _sanitize_unit_args(*input) unyt_result = ufunc(*unyt_inputs, **kwargs) if extract_dask: input = _extract_dask(input) input = _extract_unyt_val(input) return input, unyt_result def _post_ufunc(dask_superfunk, unyt_result): # a decorator to attach hidden unyt quantity to result of a ufunc, array or # elemwise calculation def wrapper(*args, **kwargs): dask_result = dask_superfunk(*args, **kwargs) if hasattr(unyt_result, "units"): return _create_with_quantity(dask_result, unyt_result) return dask_result return wrapper def _special_dec(the_func): # decorator for special operations like __mul__ , __truediv__, which will # bypass __getattribute__ def wrapper(self, *args, **kwargs): funcname = the_func.__name__ ufunc = getattr(ua.unyt_quantity, funcname) ufunc_args = (self,) + args args, unyt_result = _prep_ufunc(ufunc, *ufunc_args, extract_dask=True, **kwargs) # remove the first arg, cause we need to supply self as first arg _ = args.pop(0) daskresult = the_func(self, *args, **kwargs) if hasattr(unyt_result, "units"): return _create_with_quantity(daskresult, unyt_result) return daskresult # __lt__, __le__, etc. will hit this return wrapper class unyt_dask_array(DaskArray): """ a dask.array.core.Array subclass that tracks units. This class is only recommended for advanced usage, most cases should use the unyt_from_dask helper function to instantiate a new unyt_dask_array. Parameters ---------- The following parameters are those for a standard dask.array.core.Array: dask : dict Task dependency graph name : string Name of array in dask shape : tuple of ints Shape of the entire array chunks: iterable of tuples block sizes along each dimension dtype : str or dtype Typecode or data-type for the new Dask Array meta : empty ndarray empty ndarray created with same NumPy backend, ndim and dtype as the Dask Array being created (overrides dtype) The following keyword-only parameters are the same as for a standard unyt.unyt_array (check there for definitions): units registry bypass_validation unyt_name """ def __new__( cls, dask_graph, name, chunks, dtype=None, meta=None, shape=None, *, units=None, registry=None, bypass_validation=False, unyt_name=None, ): # get the base dask array obj = super().__new__( cls, dask_graph, name, chunks, dtype, meta, shape, ) # attach our unyt sidecar quantity dtype = obj.dtype obj._unyt_array = ua.unyt_array( [1.0], units, registry, dtype, bypass_validation=bypass_validation, name=unyt_name, ) obj.units = obj._unyt_array.units obj.unyt_name = obj._unyt_array.name # set the unit conversion attributes so they are discoverable. no name # conflicts for now, but this could be an issue if _unyt_funcs_to_track # is expanded. for attr in _unyt_funcs_to_track: setattr(obj, attr, getattr(obj._unyt_array, attr)) return obj def _elemwise(self, ufunc, *args, **kwargs): args, unyt_result = _prep_ufunc(ufunc, *args, **kwargs) return _post_ufunc(super()._elemwise, unyt_result)(ufunc, *args, **kwargs) def __array_ufunc__(self, numpy_ufunc, method, *inputs, **kwargs): inputs, unyt_result = _prep_ufunc( numpy_ufunc, *inputs, extract_dask=True, **kwargs ) wrapped_func = _post_ufunc(super().__array_ufunc__, unyt_result) return wrapped_func(numpy_ufunc, method, *inputs, **kwargs) def __array_function__(self, func, types, args, kwargs): args, unyt_result = _prep_ufunc(func, *args, extract_dask=True, **kwargs) types = [type(i) for i in args] wrapped_func = _post_ufunc(super().__array_function__, unyt_result) return wrapped_func(func, types, args, kwargs) def __getitem__(self, index): return _unary_dask_decorator(super().__getitem__, self)(index) def __setitem__(self, key, value): # requires dask >= 2021.4.1 super().__setitem__(key, value) def __repr__(self): disp_str = super().__repr__().replace("dask.array", "unyt_dask_array") units_str = f", units={self.units}>" return disp_str.replace(">", units_str) def _repr_html_(self): # controls jupyter notebook display of an array. called from _repr_html_ base_table = super()._repr_html_() table = base_table.split("\n") new_table = [] for row in table: if "" in row: u = self.units newrow = f" Units {u} {u} " new_table.append(newrow) new_table.append(row) return "\n".join(new_table) def to_dask(self): """ convert to a plain dask array Returns ------- dask.array object Examples -------- >>> from unyt import dask_array >>> import dask.array as da >>> x = da.random.random((10000, 10000), chunks=(1000, 1000)) >>> x_da = dask_array.unyt_from_dask(x, 'm') >>> x_da.to_dask() ... # doctest: +NORMALIZE_WHITESPACE dask.array """ (_, args) = super().__reduce__() return DaskArray(*args) def __reduce__(self): (_, args) = super().__reduce__() unyt_state = { "_unyt_array": self._unyt_array, "units": self.units, "unyt_name": self.unyt_name, } # reminder: the 3rd object returned by a __reduce__ tuple is the object # state. when there is no __setstate__, it must be a dict and it will be # added to the __dict__ attribute. return unyt_dask_array, args, unyt_state def __getattribute__(self, name): if name in _unyt_funcs_to_track: return self._track_conversion(name) result = super().__getattribute__(name) if name in _use_unary_decorator: return _unary_dask_decorator(result, self) return result def _track_conversion(self, unyt_func_name): """ a decorator to use with unyt functions that convert units Parameters ---------- unyt_func_name: str the name of the function to call from the sidecar _unyt_quantity. Must be an attribute of unyt.unyt_quantity. current_unyt_dask: unyt_dask_array the current instance of a unyt_dask_array Returns a new unyt_dask_array instance with appropriate units """ def wrapper(*args, **kwargs): # current value of sidecar quantity init_val = self._unyt_array.value # get the unyt function handle and call it the_func = getattr(self._unyt_array, unyt_func_name) new_unyt_quantity = the_func(*args, **kwargs) # calculate the conversion factor and pull out the new name and units # might be able to use _get_conversion_factor here too... factor = new_unyt_quantity.value / init_val # apply the factor, return new new_unyt_quantity[:] = 1 # reset sidecar value new_obj = _create_with_quantity(self * factor, new_unyt_quantity) return new_obj # functools wrap fails here because unyt_func_name is a string, copy manually: wrapper.__doc__ = getattr(self._unyt_array, unyt_func_name).__doc__ return wrapper def __dask_postcompute__(self): # a dask hook to catch after .compute(), see # https://docs.dask.org/en/latest/custom-collections.html#example-dask-collection return _finalize_unyt, ((self.units,)) def _set_unit_state(self, units, new_unyt_array, unyt_name): # sets just the unit state of the object self.units = units self._unyt_array = new_unyt_array self.unyt_name = unyt_name # These methods bypass __getattribute__ and numpy hooks, so they are defined # explicitly here (but are handled generically by the _special_dec decorator). @_special_dec def __abs__(self): return super().__abs__() @_special_dec def __pow__(self, other): return super().__pow__(other) @_special_dec def __mul__(self, other): return super().__mul__(other) @_special_dec def __rmul__(self, other): return super().__rmul__(other) @_special_dec def __truediv__(self, other): return super().__truediv__(other) @_special_dec def __rtruediv__(self, other): return super().__rtruediv__(other) @_special_dec def __add__(self, other): return super().__add__(other) @_special_dec def __sub__(self, other): return super().__sub__(other) @_special_dec def __lt__(self, other): return super().__lt__(other) @_special_dec def __le__(self, other): return super().__le__(other) @_special_dec def __gt__(self, other): return super().__gt__(other) @_special_dec def __ge__(self, other): return super().__ge__(other) @_special_dec def __eq__(self, other): return super().__eq__(other) @_special_dec def __ne__(self, other): return super().__ne__(other) def prod(self, *args, **kwargs): _, unit = ua._apply_power_mapping( np.multiply, self._unyt_array.units, self.size, self.shape, kwargs ) dask_result = super().prod(*args, **kwargs) return _create_with_quantity(dask_result, ua.unyt_array([1.0], unit)) def _finalize_unyt(results, unit_name): """ the function to call from the __dask_postcompute__ hook. Parameters ---------- results : the dask results object unit_name : the units of the result Returns ------- unyt_array or unyt_quantity """ # here, we first call the standard finalize function for a dask array # and then return a standard unyt_array from the now in-memory result if # the result is an array, otherwise return a unyt_quantity. result = dask_finalize(results) if type(result) is np.ndarray: return ua.unyt_array(result, unit_name) else: return ua.unyt_quantity(result, unit_name) def _create_with_quantity(dask_array, new_unyt_array): """ this function instantiates a new unyt_dask_array instance and then sets the unit state, including units. Used to wrap dask operations Parameters ---------- dask_array : a standard dask array or a unyt_dask_array new_unyt_array : a standard unyt array remaining arguments get passed to unyt.unyt_array, check there for a description. """ if isinstance(dask_array, unyt_dask_array): dask_array = dask_array.to_dask() out = unyt_from_dask(dask_array) # attach the unyt_array units = new_unyt_array.units unyt_name = new_unyt_array.name out._set_unit_state(units, new_unyt_array, unyt_name) return out def unyt_from_dask( dask_array, units=None, *, registry=None, bypass_validation=False, unyt_name=None, ): """ creates a unyt_dask_array from a standard dask array. This function will create a new copy of the dask graph associated with an array. Parameters ---------- dask_array : a standard dask array units : String unit name, unit symbol object, or astropy unit The units of the array. Powers must be specified using python syntax (cm**3, not cm^3). registry : :class:`unyt.unit_registry.UnitRegistry` The registry to create units from. If units is already associated with a unit registry and this is specified, this will be used instead of the registry associated with the unit object. bypass_validation : boolean If True, all input validation is skipped. Using this option may produce corrupted, invalid units or array data, but can lead to significant speedups in the input validation logic adds significant overhead. If set, units *must* be a valid unit object. Defaults to False. unyt_name : string The name of the array. Defaults to None. This attribute does not propagate through mathematical operations, but is preserved under indexing and unit conversions. Notes ----- All of the above parameters with the exception of the dask_array are the same as for a standard unyt.unyt_array. Examples -------- >>> from unyt import dask_array, unyt_quantity >>> import dask.array as da >>> x = da.ones((1000, 1000), chunks=(100, 100)) >>> x_da = dask_array.unyt_from_dask(x, 'm') >>> type(x_da) >>> x_da.units m >>> x_da.mean().units m >>> x_da.mean().compute() unyt_quantity(1., 'm') >>> x_da.to('cm').mean().compute() unyt_quantity(100., 'cm') >>> (x_da.to('cm')/unyt_quantity(10, 's')).mean().compute() unyt_quantity(10., 'cm/s') """ # reduce the dask array to pull out the arguments required for instantiating # a new dask.array.core.Array object and then initialize our new unyt_dask # array (cls, args) = dask_array.__reduce__() da = unyt_dask_array( *args, units=units, registry=registry, bypass_validation=bypass_validation, unyt_name=unyt_name, ) return da # note: the unyt_dask_array class has no way of catching daskified reductions. # operations like dask.array.min() get routed through dask.array.reductions.min() # and will return plain arrays or float/int values. # # When these operations exist as attributes, they can be called and will return # unyt objects. i.e., : # # import dask; import unyt # x_da = unyt_from_dask(dask.array.ones((10, 10), chunks=(2, 2)), unyt.m) # dask.array.min(x_da).compute() # returns a plain float # x_da.min().compute() # returns a unyt quantity # # but when the functions do not exist as attributes, like dask.array.nanmin(), # it is difficult to handle without manually wrapping all of those reductions # functions and exposing them here. The following function, reduce_with_units, # is a compromise: it is a simple helper function for calling # a general dask.array.reductions method on a unyt_dask array to correctly # handle units. # # check https://github.com/yt-project/unyt/issues/269 for further discussion _nan_ops = {"nansum", "nanmean", "nanmedian", "nanstd", "nanmax", "nanmin", "nancumsum"} _passthrough_reductions = {"diagonal", "median"} _allowed_funcs = _nan_ops.union(_passthrough_reductions, _use_unary_decorator) def reduce_with_units(dask_func, unyt_dask_in, *args, **kwargs): """ Call a dask.array.reduction function and preserve units. Parameters ---------- dask_func : function a function handle from dask.array.reduction (e.g., dask.array.min, dask.array.var, dask.array.nanstd) to call. unyt_dask_in : unyt_dask_array the unyt dask array, first argument to dask_func args: any arguments to the dask_func kwargs: any keyword arguments to the dask_func Returns ------- unyt_dask_array: the result of dask_func with units preserved Examples -------- >>> import dask.array >>> from unyt.dask_array import unyt_from_dask, reduce_with_units >>> a = dask.array.ones((10000,), chunks=(100,)) >>> a = unyt_from_dask(a, 'm') >>> b = reduce_with_units(dask.array.median, a, axis=0) >>> b.compute() unyt_quantity(1., 'm') """ if dask_func.__name__ in _allowed_funcs: # e.g., min, max, nanstd. functions that return the same units can # be called directly as the dask function will treat unyt_dask_in as # a standard dask array and then we copy over the initial units. return _unary_dask_decorator(dask_func, unyt_dask_in)( unyt_dask_in, *args, **kwargs ) else: # the operation may change the units npfunc = getattr(np, dask_func.__name__, None) if npfunc: newargs, unyt_result = _prep_ufunc( npfunc, unyt_dask_in, *args, extract_dask=True, **kwargs ) return _post_ufunc(dask_func, unyt_result)(*newargs) else: raise ValueError("could not deduce np equivalent of dask reduction") unyt-3.0.4/unyt/dimensions.py000066400000000000000000000244231476461141700162560ustar00rootroot00000000000000""" Dimensions of physical quantities """ from functools import wraps from itertools import chain from sympy import Rational, Symbol, sympify from unyt._deprecation import warn_deprecated #: mass mass = Symbol("(mass)", positive=True) #: length length = Symbol("(length)", positive=True) #: time time = Symbol("(time)", positive=True) #: temperature temperature = Symbol("(temperature)", positive=True) #: angle angle = Symbol("(angle)", positive=True) #: current_mks current_mks = Symbol("(current_mks)", positive=True) #: luminous_intensity luminous_intensity = Symbol("(luminous_intensity)", positive=True) #: dimensionless dimensionless = sympify(1) #: logarithmic logarithmic = Symbol("(logarithmic)", positive=True) #: A list of all of the base dimensions base_dimensions = [ mass, length, time, temperature, angle, current_mks, dimensionless, luminous_intensity, logarithmic, ] # # Derived dimensions # # rate rate = 1 / time # frequency (alias for rate) frequency = rate # angular frequency angular_frequency = angle * rate # spatial frequency spatial_frequency = 1 / length #: solid_angle solid_angle = angle * angle #: velocity velocity = length / time #: acceleration acceleration = length / time**2 #: jerk jerk = length / time**3 #: snap snap = length / time**4 #: crackle crackle = length / time**5 #: pop pop = length / time**6 #: area area = length * length #: volume volume = area * length #: momentum momentum = mass * velocity #: force force = mass * acceleration #: surface tension tension = force / length #: pressure pressure = force / area #: energy energy = force * length #: power power = energy / time #: flux flux = power / area #: specific_flux specific_flux = flux / rate #: number_density number_density = 1 / (length * length * length) #: density density = mass * number_density #: angular_momentum angular_momentum = mass * length * velocity #: specific_angular_momentum specific_angular_momentum = angular_momentum / mass #: specific_energy specific_energy = energy / mass #: count_flux count_flux = 1 / (area * time) #: count_intensity count_intensity = count_flux / solid_angle #: luminous_flux luminous_flux = luminous_intensity * solid_angle #: luminance luminance = luminous_intensity / area # Gaussian electromagnetic units #: charge_cgs charge_cgs = (energy * length) ** Rational(1, 2) # proper 1/2 power #: current_cgs current_cgs = charge_cgs / time #: electric_field_cgs electric_field_cgs = charge_cgs / length**2 #: magnetic_field_cgs magnetic_field_cgs = electric_field_cgs #: electric_potential_cgs electric_potential_cgs = energy / charge_cgs #: resistance_cgs resistance_cgs = electric_potential_cgs / current_cgs #: magnetic_flux_cgs magnetic_flux_cgs = magnetic_field_cgs * area # SI electromagnetic units #: charge charge = charge_mks = current_mks * time #: electric_field electric_field = electric_field_mks = force / charge_mks #: magnetic_field magnetic_field = magnetic_field_mks = electric_field_mks / velocity #: electric_potential electric_potential = electric_potential_mks = energy / charge_mks #: resistance resistance = resistance_mks = electric_potential_mks / current_mks #: capacitance capacitance = capacitance_mks = charge / electric_potential #: magnetic_flux magnetic_flux = magnetic_flux_mks = magnetic_field_mks * area #: inductance inductance = inductance_mks = magnetic_flux_mks / current_mks #: a list containing all derived_dimensions derived_dimensions = [ rate, velocity, acceleration, jerk, snap, crackle, pop, momentum, force, energy, power, charge_cgs, electric_field_cgs, magnetic_field_cgs, solid_angle, flux, specific_flux, volume, luminous_flux, area, current_cgs, charge_mks, electric_field_mks, magnetic_field_mks, electric_potential_cgs, electric_potential_mks, resistance_cgs, resistance_mks, magnetic_flux_mks, magnetic_flux_cgs, luminance, spatial_frequency, angular_frequency, ] #: a list containing all dimensions dimensions = base_dimensions + derived_dimensions #: a dict containing a bidirectional mapping from #: mks dimension to cgs dimension em_dimensions = { magnetic_field_mks: magnetic_field_cgs, magnetic_flux_mks: magnetic_flux_cgs, charge_mks: charge_cgs, current_mks: current_cgs, electric_potential_mks: electric_potential_cgs, resistance_mks: resistance_cgs, } for k, v in list(em_dimensions.items()): em_dimensions[v] = k def accepts(**arg_units): """Decorator for checking units of function arguments. Parameters ---------- arg_units: dict Mapping of function arguments to dimensions, of the form 'arg1'=dimension1 etc where ``'arg1'`` etc are the function arguments and ``dimension1`` etc are SI base units (or combination of units), eg. length/time. Notes ----- Keyword args are not dimensionally check, being directly passed to the decorated function. Function arguments that don't have attached units can be skipped can bypass dimensionality checking by not being passed to the decorator. See ``baz`` in the examples, where ``a`` has no units. Examples -------- >>> import unyt as u >>> from unyt.dimensions import length, time >>> @accepts(a=time, v=length/time) ... def foo(a, v): ... return a * v ... >>> res = foo(a= 2 * u.s, v = 3 * u.m/u.s) >>> print(res) 6 m >>> @accepts(a=length, v=length/time) ... def bar(a, v): ... return a * v ... >>> bar(a= 2 * u.s, v = 3 * u.m/u.s) Traceback (most recent call last): ... TypeError: arg 'a=2 s' does not match (length) >>> @accepts(v=length/time) ... def baz(a, v): ... return a * v ... >>> res = baz(a= 2, v = 3 * u.m/u.s) >>> print(res) 6 m/s """ def check_accepts(f): """Decorates original function. Parameters ---------- f : function Function being decorated. Returns ------- new_f: function Decorated function. """ names_of_args = f.__code__.co_varnames @wraps(f) def new_f(*args, **kwargs): """The new function being returned from the decorator. Check units of `args` and `kwargs`, then run original function. Raises ------ TypeError If the units do not match. """ for arg_name, arg_value in chain(zip(names_of_args, args), kwargs.items()): if arg_name in arg_units: # function argument needs to be checked dimension = arg_units[arg_name] if not _has_dimensions(arg_value, dimension): raise TypeError( f"arg '{arg_name}={arg_value}' does not match {dimension}" ) return f(*args, **kwargs) return new_f return check_accepts def returns(*r_units, r_unit=None): """Decorator for checking function return units. Parameters ---------- *r_units: :py:class:`sympy.core.symbol.Symbol` SI base unit (or combination of units), eg. length/time of the value(s) returned by the original function r_unit: :py:class:`sympy.core.symbol.Symbol` Deprecated version of `r_units` which supports only one named return value. Examples -------- >>> import unyt as u >>> from unyt.dimensions import length, time >>> @returns(length) ... def f(a, v): ... return a * v ... >>> res = f(a= 2 * u.s, v = 3 * u.m/u.s) >>> print(res) 6 m >>> @returns(length/time) ... def f(a, v): ... return a * v ... >>> f(a= 2 * u.s, v = 3 * u.m/u.s) Traceback (most recent call last): ... TypeError: result '6 m' does not match (length)/(time) >>> @returns(length, length/time**2) ... def f(a, v): ... return a * v, v / a ... >>> res = f(a= 2 * u.s, v = 3 * u.m/u.s) >>> print(*res) 6 m 1.5 m/s**2 """ # Convert deprecated arguments into current ones where possible. if r_unit is not None: if len(r_units) > 0: raise ValueError( "Cannot specify `r_unit` and other return values simultaneously" ) else: warn_deprecated( "@unyt.returns(r_unit=...)", replacement="use @unyt.returns(...)", since_version="3.0", ) r_units = (r_unit,) def check_returns(f): """Decorates original function. Parameters ---------- f : function Function being decorated. Returns ------- new_f: function Decorated function. """ @wraps(f) def new_f(*args, **kwargs): """The decorated function, which checks the return units. Raises ------ TypeError If the units do not match. """ results = f(*args, **kwargs) # Make results a tuple so we can treat single and multiple return values the # same way. if isinstance(results, tuple): result_tuple = results else: result_tuple = (results,) for result, dimension in zip(result_tuple, r_units): if not _has_dimensions(result, dimension): raise TypeError(f"result '{result}' does not match {dimension}") return results return new_f return check_returns def _has_dimensions(quant, dim): """Checks the argument has the right dimensionality. Parameters ---------- quant : :py:class:`unyt.array.unyt_quantity` Quantity whose dimensionality we want to check. dim : :py:class:`sympy.core.symbol.Symbol` SI base unit (or combination of units), eg. length/time Returns ------- bool True if check successful. Examples -------- >>> import unyt as u >>> from unyt.dimensions import length, time >>> _has_dimensions(3 * u.m/u.s, length/time) True >>> _has_dimensions(3, length) False """ try: arg_dim = quant.units.dimensions except AttributeError: arg_dim = dimensionless return arg_dim == dim unyt-3.0.4/unyt/equivalencies.py000066400000000000000000000355301476461141700167440ustar00rootroot00000000000000""" Equivalencies between different kinds of units """ from collections import OrderedDict import numpy as np from unyt.dimensions import ( density, dimensionless, energy, flux, length, mass, number_density, rate, spatial_frequency, temperature, velocity, ) from unyt.exceptions import InvalidUnitEquivalence equivalence_registry = OrderedDict() class _RegisteredEquivalence(type): def __init__(cls, name, b, d): type.__init__(cls, name, b, d) if hasattr(cls, "type_name"): equivalence_registry[cls.type_name] = cls class Equivalence(metaclass=_RegisteredEquivalence): def __init__(self, in_place=False): self.in_place = in_place def convert(self, x, new_dims, **kwargs): if x.units.dimensions in self._dims and new_dims in self._dims: return self._convert(x, new_dims, **kwargs) else: raise InvalidUnitEquivalence(self, x.units, new_dims) def _get_out(self, x): if self.in_place: return x return None class NumberDensityEquivalence(Equivalence): """Equivalence between mass and number density, given a mean molecular weight. Given a number density :math:`n`, the mass density :math:`\\rho` is: .. math:: \\rho = \\mu m_{\\rm H} n And similarly .. math:: n = \\rho (\\mu m_{\\rm H})^{-1} Parameters ---------- mu : float The mean molecular weight. Defaults to 0.6 which is valid for fully ionized gas with primordial composition. Example ------- >>> print(NumberDensityEquivalence()) number density: density <-> number density >>> from unyt import Msun, pc >>> rho = 3*Msun/pc**3 >>> rho.to_equivalent('cm**-3', 'number_density', mu=1.4) unyt_quantity(86.64869896, 'cm**(-3)') """ type_name = "number_density" _dims = (density, number_density) def _convert(self, x, new_dims, mu=0.6): from unyt import physical_constants as pc if new_dims == number_density: return np.true_divide(x, mu * pc.mh, out=self._get_out(x)) elif new_dims == density: return np.multiply(x, mu * pc.mh, out=self._get_out(x)) def __str__(self): return "number density: density <-> number density" class ThermalEquivalence(Equivalence): """Equivalence between temperature and energy via the Boltzmann constant Given a temperature :math:`T` in an absolute scale (e.g. Kelvin or Rankine), the equivalent thermal energy :math:`E` for that temperature is given by: .. math:: E = k_B T And .. math:: T = E/k_B Where :math:`k_B` is Boltzmann's constant. Example ------- >>> print(ThermalEquivalence()) thermal: temperature <-> energy >>> from unyt import Kelvin >>> temp = 1e6*Kelvin >>> temp.to_equivalent('keV', 'thermal') unyt_quantity(0.08617332, 'keV') """ type_name = "thermal" _dims = (temperature, energy) def _convert(self, x, new_dims): from unyt import physical_constants as pc if new_dims == energy: return np.multiply(x, pc.kboltz, out=self._get_out(x)) elif new_dims == temperature: return np.true_divide(x, pc.kboltz, out=self._get_out(x)) def __str__(self): return "thermal: temperature <-> energy" class MassEnergyEquivalence(Equivalence): """Equivalence between mass and energy in special relativity Given a body with mass :math:`m`, the self-energy :math:`E` of that mass is given by .. math:: E = m c^2 where :math:`c` is the speed of light. Example ------- >>> print(MassEnergyEquivalence()) mass_energy: mass <-> energy >>> from unyt import g >>> (3.5*g).to_equivalent('J', 'mass_energy') unyt_quantity(3.14564313e+14, 'J') """ type_name = "mass_energy" _dims = (mass, energy) def _convert(self, x, new_dims): from unyt import physical_constants as pc if new_dims == energy: return np.multiply(x, pc.clight * pc.clight, out=self._get_out(x)) elif new_dims == mass: return np.true_divide(x, pc.clight * pc.clight, out=self._get_out(x)) def __str__(self): return "mass_energy: mass <-> energy" class SpectralEquivalence(Equivalence): """Equivalence between wavelength, frequency, and energy of a photon. Given a photon with wavelength :math:`\\lambda`, spatial frequency :math:`\\bar\\nu`, frequency :math:`\\nu` and Energy :math:`E`, these quantities are related by the following forumlae: .. math:: E = h \\nu = h c / \\lambda = h c \\bar\\nu where :math:`h` is Planck's constant and :math:`c` is the speed of light. Example ------ >>> print(SpectralEquivalence()) spectral: length <-> spatial_frequency <-> frequency <-> energy >>> from unyt import angstrom, km >>> (3*angstrom).to_equivalent('keV', 'spectral') unyt_quantity(4.13280644, 'keV') >>> (1*km).to_equivalent('MHz', 'spectral') unyt_quantity(0.29979246, 'MHz') """ type_name = "spectral" _dims = (length, rate, energy, spatial_frequency) def _convert(self, x, new_dims): from unyt import physical_constants as pc if new_dims == energy: if x.units.dimensions == length: return np.true_divide(pc.clight * pc.h_mks, x, out=self._get_out(x)) elif x.units.dimensions == rate: return np.multiply(x, pc.h_mks, out=self._get_out(x)) elif x.units.dimensions == spatial_frequency: return np.multiply(x, pc.h_mks * pc.clight, out=self._get_out(x)) elif new_dims == length: if x.units.dimensions == rate: return np.true_divide(pc.clight, x, out=self._get_out(x)) elif x.units.dimensions == energy: return np.true_divide(pc.h_mks * pc.clight, x, out=self._get_out(x)) elif x.units.dimensions == spatial_frequency: return np.true_divide(1, x, out=self._get_out(x)) elif new_dims == rate: if x.units.dimensions == length: return np.true_divide(pc.clight, x, out=self._get_out(x)) elif x.units.dimensions == energy: return np.true_divide(x, pc.h_mks, out=self._get_out(x)) elif x.units.dimensions == spatial_frequency: return np.multiply(x, pc.clight, out=self._get_out(x)) elif new_dims == spatial_frequency: if x.units.dimensions == length: return np.true_divide(1, x, out=self._get_out(x)) elif x.units.dimensions == energy: return np.true_divide(x, pc.clight * pc.h_mks, out=self._get_out(x)) elif x.units.dimensions == rate: return np.true_divide(x, pc.clight, out=self._get_out(x)) def __str__(self): return "spectral: length <-> spatial_frequency <-> frequency " + "<-> energy" class SoundSpeedEquivalence(Equivalence): """Equivalence between the sound speed, temperature, and thermal energy of an ideal gas For an ideal gas with sound speed :math:`c_s`, temperature :math:`T`, and thermal energy :math:`E`, the following equalities will hold: .. math:: c_s = \\sqrt{\\frac{\\gamma k_B T}{\\mu m_{\\rm H}}} and .. math:: E = c_s^2 \\mu m_{\\rm H} / \\gamma = k_B T where :math:`k_B` is Boltzmann's constant, :math:`\\mu` is the mean molecular weight of the gas, and :math:`\\gamma` is the ratio of specific heats. Parameters ---------- gamma : float The ratio of specific heats. Defaults to 5/3, which is correct for monatomic species. mu : float The mean molecular weight. Defaults to 0.6, which is valid for fully ionized gas with primordial composition. Example ------- >>> print(SoundSpeedEquivalence()) sound_speed (ideal gas): velocity <-> temperature <-> energy >>> from unyt import Kelvin, km, s >>> hot = 1e6*Kelvin >>> hot.to_equivalent('km/s', 'sound_speed') unyt_quantity(151.37249927, 'km/s') >>> hot.to_equivalent('keV', 'sound_speed') unyt_quantity(0.08617332, 'keV') >>> cs = 100*km/s >>> cs.to_equivalent('K', 'sound_speed') unyt_quantity(436421.39881617, 'K') >>> cs.to_equivalent('keV', 'sound_speed') unyt_quantity(0.03760788, 'keV') """ type_name = "sound_speed" _dims = (velocity, temperature, energy) def _convert(self, x, new_dims, mu=0.6, gamma=5.0 / 3.0): from unyt import physical_constants as pc if new_dims == velocity: if x.units.dimensions == temperature: v2 = np.multiply( pc.kboltz * gamma / (mu * pc.mh), x, out=self._get_out(x) ) elif x.units.dimensions == energy: v2 = np.multiply(gamma / (mu * pc.mh), x, out=self._get_out(x)) return np.sqrt(v2, out=self._get_out(x)) elif new_dims == temperature: if x.units.dimensions == velocity: v2 = np.multiply(x, x, out=self._get_out(x)) kT = np.multiply(v2, mu * pc.mh / gamma, out=self._get_out(x)) return np.true_divide(kT, pc.kboltz, out=self._get_out(x)) else: return np.true_divide(x, pc.kboltz, out=self._get_out(x)) else: if x.units.dimensions == velocity: v2 = np.multiply(x, x, out=self._get_out(x)) return np.multiply(mu * pc.mh / gamma, v2, out=self._get_out(x)) else: return np.multiply(x, pc.kboltz, out=self._get_out(x)) def __str__(self): return "sound_speed (ideal gas): velocity <-> temperature <-> energy" class LorentzEquivalence(Equivalence): """Equivalence between velocity and the Lorentz gamma factor. For a body with velocity :math:`v`, the Lorentz gamma factor, :math:`\\gamma` is .. math:: \\gamma = \\frac{1}{\\sqrt{1 - v^2/c^2}} and similarly .. math:: v = \\frac{c}{\\sqrt{1 - \\gamma^2}} where :math:`c` is the speed of light. Example ------- >>> print(LorentzEquivalence()) lorentz: velocity <-> dimensionless >>> from unyt import c, dimensionless >>> v = 0.99*c >>> print(v.to_equivalent('', 'lorentz')) 7.088812050083393 dimensionless >>> fast = 99.9*dimensionless >>> fast.to_equivalent('c', 'lorentz') unyt_quantity(0.9999499, 'c') >>> fast.to_equivalent('km/s', 'lorentz') unyt_quantity(299777.43797656, 'km/s') """ type_name = "lorentz" _dims = (dimensionless, velocity) def _convert(self, x, new_dims): from unyt import physical_constants as pc if new_dims == dimensionless: beta = np.true_divide(x, pc.clight, out=self._get_out(x)) beta2 = np.multiply(beta, beta, out=self._get_out(x)) inv_gamma_2 = np.subtract(1, beta2, out=self._get_out(x)) inv_gamma = np.sqrt(inv_gamma_2, out=self._get_out(x)) gamma = np.true_divide(1.0, inv_gamma, out=self._get_out(x)) return gamma elif new_dims == velocity: gamma2 = np.multiply(x, x, out=self._get_out(x)) inv_gamma_2 = np.true_divide(1, gamma2, out=self._get_out(x)) beta2 = np.subtract(1, inv_gamma_2, out=self._get_out(x)) beta = np.sqrt(beta2, out=self._get_out(x)) return np.multiply(pc.clight, beta, out=self._get_out(x)) def __str__(self): return "lorentz: velocity <-> dimensionless" class SchwarzschildEquivalence(Equivalence): """Equivalence between the mass and radius of a Schwarzschild black hole A Schwarzschild black hole of mass :math:`M` has radius :math:`R` .. math:: R = \\frac{2 G M}{c^2} and similarly .. math:: M = \\frac{R c^2}{2 G} where :math:`G` is Newton's gravitational constant and :math:`c` is the speed of light. Example ------- >>> print(SchwarzschildEquivalence()) schwarzschild: mass <-> length >>> from unyt import Msun, AU >>> (10*Msun).to_equivalent('km', 'schwarzschild') unyt_quantity(29.53161626, 'km') >>> (1*AU).to_equivalent('Msun', 'schwarzschild') unyt_quantity(50656851.7815179, 'Msun') """ type_name = "schwarzschild" _dims = (mass, length) def _convert(self, x, new_dims): from unyt import physical_constants as pc if new_dims == length: return np.multiply( 2.0 * pc.G / (pc.clight * pc.clight), x, out=self._get_out(x) ) elif new_dims == mass: return np.multiply( 0.5 * pc.clight * pc.clight / pc.G, x, out=self._get_out(x) ) def __str__(self): return "schwarzschild: mass <-> length" class ComptonEquivalence(Equivalence): """Equivalence between the Compton wavelength of a particle and its mass. .. math:: \\lambda_c = h/mc Example ------- >>> print(ComptonEquivalence()) compton: mass <-> length >>> from unyt import me, fm >>> me.to_equivalent('angstrom', 'compton') unyt_quantity(0.0242631, 'Å') >>> (10*fm).to_equivalent('me', 'compton') unyt_quantity(242.63102371, 'me') """ type_name = "compton" _dims = (mass, length) def _convert(self, x, new_dims): from unyt import physical_constants as pc return np.true_divide(pc.h_mks / pc.clight, x, out=self._get_out(x)) def __str__(self): return "compton: mass <-> length" class EffectiveTemperatureEquivalence(Equivalence): """Equivalence between the emitted flux across all wavelengths and temperature of a blackbody For a blackbody emitter with Temperature :math:`T` emitting radiation with a flux :math:`F`, the following equality holds: .. math:: F = \\sigma T^4 where :math:`\\sigma` is the Stefan-Boltzmann constant. Example ------- >>> print(EffectiveTemperatureEquivalence()) effective_temperature: flux <-> temperature >>> from unyt import K, W, m >>> (5000.*K).to_equivalent('W/m**2', 'effective_temperature') unyt_quantity(35439828.89119583, 'W/m**2') >>> (100.*W/m**2).to_equivalent('K', 'effective_temperature') unyt_quantity(204.92601755, 'K') """ type_name = "effective_temperature" _dims = (flux, temperature) def _convert(self, x, new_dims): from unyt import physical_constants as pc if new_dims == flux: x4 = np.power(x, 4, out=self._get_out(x)) return np.multiply( pc.stefan_boltzmann_constant_mks, x4, out=self._get_out(x) ) elif new_dims == temperature: T4 = np.true_divide( x, pc.stefan_boltzmann_constant_mks, out=self._get_out(x) ) ret = np.power(T4, 0.25, out=self._get_out(x)) return ret def __str__(self): return "effective_temperature: flux <-> temperature" unyt-3.0.4/unyt/exceptions.py000066400000000000000000000231361476461141700162670ustar00rootroot00000000000000""" Exception classes defined by unyt """ class UnytError(Exception): """ A generic exception type that all unyt exceptions are derived from. It may be raised directly in rare situations that do not fit any existing exceptions and don't justify the creation of a new one. Since it serves as a base class for all unyt exceptions, it may be used as a catch-all exception type in dependent code. For instance >>> from unyt import cm, s >>> from unyt.exceptions import UnytError >>> a = 1 * cm >>> b = 1 / s >>> try: ... a + b ... except UnytError: ... pass However, it is generally recommended to only capture exceptions as specific as possible. """ pass class UnitOperationError(UnytError, ValueError): """An exception that is raised when unit operations are not allowed Example ------- >>> import unyt as u >>> 3*u.g + 4*u.m\ # doctest: +IGNORE_EXCEPTION_DETAIL +NORMALIZE_WHITESPACE Traceback (most recent call last): ... unyt.exceptions.UnitOperationError: The operator for unyt_arrays with units "g" (dimensions "(mass)") and "m" (dimensions "(length)") is not well defined. """ def __init__(self, operation, unit1, unit2=None): self.operation = operation self.unit1 = unit1 self.unit2 = unit2 super().__init__() def __str__(self): err = ( f"The {self.operation} operator for unyt_arrays with units " f"'{self.unit1}' (dimensions '{self.unit1.dimensions}') " ) if self.unit2 is not None: err += f"and '{self.unit2}' (dimensions '{self.unit2.dimensions}') " err += "is not well defined." return err class UnitInconsistencyError(UnitOperationError): """An error raised on forbidden numpy API expecting arrays with identical units. Example ------- >>> import unyt as u >>> import numpy as np >>> np.concatenate([[1, 2, 3] * u.cm, [4, 5, 6] * u.s])\ # doctest: +IGNORE_EXCEPTION_DETAIL +NORMALIZE_WHITESPACE Traceback (most recent call last): ... unyt.exceptions.UnitInconsistencyError: Expected all unyt_array arguments to have identical units. Received mixed units (cm, s) """ def __init__(self, *units): self.units = units def __str__(self): return ( "Expected all unyt_array arguments to have identical units. " f"Received mixed units ({', '.join(str(u) for u in self.units)})" ) class UnitConversionError(UnytError): """An error raised when converting to a unit with different dimensions. Example ------- >>> import unyt as u >>> data = 3*u.g >>> data.to('m') # doctest: +IGNORE_EXCEPTION_DETAIL +NORMALIZE_WHITESPACE Traceback (most recent call last): ... unyt.exceptions.UnitConversionError: Cannot convert between 'g' (dim '(mass)') and 'm' (dim '(length)'). """ def __init__(self, unit1, dimension1, unit2, dimension2): self.unit1 = unit1 self.unit2 = unit2 self.dimension1 = dimension1 self.dimension2 = dimension2 super().__init__() def __str__(self): err = ( f"Cannot convert between '{self.unit1}' (dim '{self.dimension1}') " f"and '{self.unit2}' (dim '{self.dimension2}')." ) return err class MissingMKSCurrent(UnytError): """Raised when querying a unit system for MKS current dimensions Since current is a base dimension for SI or SI-like unit systems but not in CGS or CGS-like unit systems, dimensions that include the MKS current dimension (the dimension of ampere) are not representable in CGS-like unit systems. When a CGS-like unit system is queried for such a dimension, this error is raised. Example ------- >>> from unyt.unit_systems import cgs_unit_system as us >>> from unyt import ampere >>> us[ampere.dimensions]\ # doctest: +IGNORE_EXCEPTION_DETAIL +NORMALIZE_WHITESPACE Traceback (most recent call last): ... unyt.exceptions.MissingMKSCurrent: The cgs unit system does not have a MKS current base unit """ def __init__(self, unit_system_name): self.unit_system_name = unit_system_name super().__init__() def __str__(self): return ( f"The {self.unit_system_name} unit system does not have " "a MKS current base unit" ) class MKSCGSConversionError(UnytError): """Raised when conversion between MKS and CGS units cannot be performed This error is raised and caught internally and will expose itself to the user as part of a chained exception leading to a UnitConversionError. """ pass class UnitsNotReducible(UnytError): """Raised when a unit cannot be safely represented in a unit system Example ------- >>> from unyt import A, cm >>> data = 12*A/cm >>> data.in_cgs()\ # doctest: +IGNORE_EXCEPTION_DETAIL +NORMALIZE_WHITESPACE Traceback (most recent call last): ... unyt.exceptions.UnitsNotReducible: The unit "A/cm" (dimensions "(current_mks)/(length)") cannot be reduced to an expression within the cgs system of units. """ def __init__(self, unit, units_base): self.unit = unit self.units_base = units_base super().__init__() def __str__(self): return ( f"The unit '{self.unit}' (dimensions '{self.unit.dimensions}') cannot be " f"reduced to an expression within the {self.units_base} system of units." ) class IterableUnitCoercionError(UnytError): """Raised when an iterable cannot be converted to a unyt_array Example ------- >>> from unyt import g, cm, unyt_array >>> data = [2*cm, 3*g] >>> unyt_array(data)\ # doctest: +IGNORE_EXCEPTION_DETAIL +NORMALIZE_WHITESPACE Traceback (most recent call last): ... unyt.exceptions.IterableUnitCoercionError: Received an input or operand that cannot be converted to a unyt_array with uniform units: [unyt_quantity(2., 'cm'), unyt_quantity(3., 'g')] """ def __init__(self, op): self.op = op super().__init__() def __str__(self): err = ( "Received an input or operand that cannot be converted " f"to a unyt_array with uniform units: {self.op}" ) return err class InvalidUnitEquivalence(UnytError): """Raised an equivalence does not apply to a unit conversion Example ------- >>> import unyt as u >>> data = 12*u.g >>> data.to('erg', equivalence='thermal')\ # doctest: +IGNORE_EXCEPTION_DETAIL +NORMALIZE_WHITESPACE Traceback (most recent call last): ... unyt.exceptions.InvalidUnitEquivalence: The unit equivalence 'thermal' does not exist for the units 'g' and 'erg'. """ def __init__(self, equiv, unit1, unit2): self.equiv = equiv self.unit1 = unit1 self.unit2 = unit2 super().__init__() def __str__(self): from unyt.unit_object import Unit if isinstance(self.unit2, Unit): msg = ( "The unit equivalence '%s' does not exist for the units '%s' and '%s'." ) else: msg = ( "The unit equivalence '%s' does not exist for units '%s' " "to convert to a new unit with dimensions '%s'." ) return msg % (self.equiv, self.unit1, self.unit2) class InvalidUnitOperation(UnytError): """Raised when an operation on a unit object is not allowed Example ------- >>> from unyt import cm, g >>> cm + g # doctest: +IGNORE_EXCEPTION_DETAIL +NORMALIZE_WHITESPACE Traceback (most recent call last): ... unyt.exceptions.InvalidUnitOperation: addition with unit objects is not allowed """ pass class SymbolNotFoundError(UnytError): """Raised when a unit name is not available in a unit registry Example ------- >>> from unyt.unit_registry import default_unit_registry >>> default_unit_registry['made_up_unit']\ # doctest: +IGNORE_EXCEPTION_DETAIL +NORMALIZE_WHITESPACE Traceback (most recent call last): ... unyt.exceptions.SymbolNotFoundError: The symbol 'made_up_unit' does not exist in this registry. """ pass class UnitParseError(UnytError): """Raised when a string unit name is not parseable as a valid unit Example ------- >>> from unyt import Unit >>> Unit('hello')\ # doctest: +IGNORE_EXCEPTION_DETAIL +NORMALIZE_WHITESPACE Traceback (most recent call last): ... unyt.exceptions.UnitParseError: Could not find unit symbol 'hello' in the provided symbols. """ pass class IllDefinedUnitSystem(UnytError): """Raised when the dimensions of the base units of a unit system are inconsistent. Example ------- >>> from unyt.unit_systems import UnitSystem >>> UnitSystem('atomic', 'nm', 'fs', 'nK', 'rad')\ # doctest: +IGNORE_EXCEPTION_DETAIL +NORMALIZE_WHITESPACE Traceback (most recent call last): ... unyt.exceptions.IllDefinedUnitSystem: Cannot create unit system with inconsistent mapping from dimensions to units. Received: OrderedDict([((length), nm), ((mass), fs), ((time), nK), ((temperature), rad), ((angle), rad), ((current_mks), A), ((luminous_intensity), cd)]) """ def __init__(self, units_map): self.units_map = units_map super().__init__() def __str__(self): return ( "Cannot create unit system with inconsistent mapping from " f"dimensions to units. Received:\n{self.units_map}" ) unyt-3.0.4/unyt/mpl_interface.py000066400000000000000000000045321476461141700167150ustar00rootroot00000000000000""" Matplotlib offers support for custom classes, such as unyt_array, allowing customization of axis information and unit conversion. In the case of unyt, the axis label is set based on the unyt_array.name and unyt_array.units attributes. It is also possible to convert the plotted units. This feature is optional and has to be enabled using the matplotlib_support context manager. """ from unyt.array import unyt_array, unyt_quantity from ._on_demand_imports import _matplotlib __all__ = ["matplotlib_support"] class matplotlib_support: """Context manager for enabling the feature When used in a with statement, the feature is enabled during the context and then disabled after it exits. Parameters ---------- label_style : str One of the following set, ``{'()', '[]', '/'}``. These choices correspond to the following unit labels: * ``'()'`` -> ``'(unit)'`` * ``'[]'`` -> ``'[unit]'`` * ``'/'`` -> ``'q_x / unit'`` """ @property def array_converter(self): from ._mpl_array_converter import unyt_arrayConverter unyt_arrayConverter._labelstyle = self.label_style return unyt_arrayConverter def __init__(self, label_style="()"): self._labelstyle = label_style self._enabled = False def __call__(self): self.__enter__() @property def label_style(self): """str: One of the following set, ``{'()', '[]', '/'}``. These choices correspond to the following unit labels: * ``'()'`` -> ``'(unit)'`` * ``'[]'`` -> ``'[unit]'`` * ``'/'`` -> ``'q_x / unit'`` """ return self._labelstyle @label_style.setter def label_style(self, label_style="()"): self._labelstyle = label_style self.array_converter._labelstyle = label_style def __enter__(self): _matplotlib.units.registry[unyt_array] = self.array_converter() _matplotlib.units.registry[unyt_quantity] = self.array_converter() self._enabled = True def __exit__(self, exc_type, exc_val, exc_tb): _matplotlib.units.registry.pop(unyt_array) _matplotlib.units.registry.pop(unyt_quantity) self._enabled = False def enable(self): self.__enter__() def disable(self): if self._enabled: self.__exit__(None, None, None) unyt-3.0.4/unyt/physical_constants.py000066400000000000000000000012331476461141700200100ustar00rootroot00000000000000""" Predefined useful physical constants Note that all of these names can be imported from the top-level unyt namespace. For example:: >>> from unyt.physical_constants import gravitational_constant, solar_mass >>> from unyt import AU >>> from math import pi >>> >>> period = 2 * pi * ((1 * AU)**3 / (gravitational_constant * solar_mass))**0.5 >>> period.in_units('day') unyt_quantity(365.26236846, 'day') .. show_all_constants:: """ from unyt.unit_registry import default_unit_registry as _default_unit_registry from unyt.unit_systems import add_constants as _add_constants _add_constants(globals(), registry=_default_unit_registry) unyt-3.0.4/unyt/testing.py000066400000000000000000000052421476461141700155610ustar00rootroot00000000000000""" Utilities for writing tests """ import warnings from unyt.array import NULL_UNIT, allclose_units def assert_allclose_units(actual, desired, rtol=1e-7, atol=0, **kwargs): """Raise an error if two objects are not equal up to desired tolerance This is a wrapper for :func:`numpy.testing.assert_allclose` that also verifies unit consistency Parameters ---------- actual : array-like Array obtained (possibly with attached units) desired : array-like Array to compare with (possibly with attached units) rtol : float, optional Relative tolerance, defaults to 1e-7 atol : float or quantity, optional Absolute tolerance. If units are attached, they must be consistent with the units of ``actual`` and ``desired``. If no units are attached, assumes the same units as ``desired``. Defaults to zero. See Also -------- :func:`unyt.array.allclose_units` Notes ----- Also accepts additional keyword arguments accepted by :func:`numpy.testing.assert_allclose`, see the documentation of that function for details. Examples -------- >>> import unyt as u >>> actual = [1e-5, 1e-3, 1e-1]*u.m >>> desired = actual.to("cm") >>> assert_allclose_units(actual, desired) """ if not allclose_units(actual, desired, rtol, atol, **kwargs): raise AssertionError def assert_array_equal_units(x, y, **kwargs): """A thin wrapper around :func:`numpy.testing.assert_array_equal` that also verifies unit consistency Arrays without units are considered dimensionless. Parameters ---------- x : array_like The actual object to check. y : array_like The desired, expected object. See Also -------- :func:`numpy.testing.assert_array_equal` Notes ----- Also accepts additional keyword arguments accepted by :func:`numpy.testing.assert_array_equel`, see the documentation of that function for details. """ # see https://github.com/yt-project/unyt/issues/281 from numpy.testing import assert_array_equal assert_array_equal(x, y, **kwargs) if not (xu := getattr(x, "units", NULL_UNIT)) == ( yu := getattr(y, "units", NULL_UNIT) ): raise AssertionError(f"Arguments' units do not match (got {xu} and {yu})") def _process_warning(op, message, warning_class, args=(), kwargs=None): if kwargs is None: kwargs = {} with warnings.catch_warnings(record=True) as w: warnings.simplefilter("always") op(*args, **kwargs) assert len(w) == 1 assert issubclass(w[0].category, warning_class) assert str(w[0].message) == message unyt-3.0.4/unyt/tests/000077500000000000000000000000001476461141700146715ustar00rootroot00000000000000unyt-3.0.4/unyt/tests/__init__.py000066400000000000000000000000001476461141700167700ustar00rootroot00000000000000unyt-3.0.4/unyt/tests/data/000077500000000000000000000000001476461141700156025ustar00rootroot00000000000000unyt-3.0.4/unyt/tests/data/__init__.py000066400000000000000000000000001476461141700177010ustar00rootroot00000000000000unyt-3.0.4/unyt/tests/data/old_json_registry.txt000066400000000000000000000312301476461141700221010ustar00rootroot00000000000000{"g": [1.0, "Symbol('(mass)', positive=True)", 0.0, "\\rm{g}"], "s": [1.0, "Symbol('(time)', positive=True)", 0.0, "\\rm{s}"], "K": [1.0, "Symbol('(temperature)', positive=True)", 0.0, "\\rm{K}"], "radian": [1.0, "Symbol('(angle)', positive=True)", 0.0, "\\rm{radian}"], "dyne": [1.0, "Mul(Symbol('(length)', positive=True), Symbol('(mass)', positive=True), Pow(Symbol('(time)', positive=True), Integer(-2)))", 0.0, "\\rm{dyn}"], "erg": [1.0, "Mul(Pow(Symbol('(length)', positive=True), Integer(2)), Symbol('(mass)', positive=True), Pow(Symbol('(time)', positive=True), Integer(-2)))", 0.0, "\\rm{erg}"], "esu": [1.0, "Mul(Pow(Symbol('(length)', positive=True), Rational(3, 2)), Pow(Symbol('(mass)', positive=True), Rational(1, 2)), Pow(Symbol('(time)', positive=True), Integer(-1)))", 0.0, "\\rm{esu}"], "gauss": [1.0, "Mul(Pow(Symbol('(length)', positive=True), Rational(-1, 2)), Pow(Symbol('(mass)', positive=True), Rational(1, 2)), Pow(Symbol('(time)', positive=True), Integer(-1)))", 0.0, "\\rm{G}"], "degC": [1.0, "Symbol('(temperature)', positive=True)", -273.15, "^\\circ\\rm{C}"], "statA": [1.0, "Mul(Pow(Symbol('(length)', positive=True), Rational(3, 2)), Pow(Symbol('(mass)', positive=True), Rational(1, 2)), Pow(Symbol('(time)', positive=True), Integer(-2)))", 0.0, "\\rm{statA}"], "statV": [1.0, "Mul(Pow(Symbol('(length)', positive=True), Rational(1, 2)), Pow(Symbol('(mass)', positive=True), Rational(1, 2)), Pow(Symbol('(time)', positive=True), Integer(-1)))", 0.0, "\\rm{statV}"], "statohm": [1.0, "Mul(Pow(Symbol('(length)', positive=True), Integer(-1)), Symbol('(time)', positive=True))", 0.0, "\\rm{statohm}"], "m": [100.0, "Symbol('(length)', positive=True)", 0.0, "\\rm{m}"], "J": [10000000.0, "Mul(Pow(Symbol('(length)', positive=True), Integer(2)), Symbol('(mass)', positive=True), Pow(Symbol('(time)', positive=True), Integer(-2)))", 0.0, "\\rm{J}"], "W": [10000000.0, "Mul(Pow(Symbol('(length)', positive=True), Integer(2)), Symbol('(mass)', positive=True), Pow(Symbol('(time)', positive=True), Integer(-3)))", 0.0, "\\rm{W}"], "Hz": [1.0, "Pow(Symbol('(time)', positive=True), Integer(-1))", 0.0, "\\rm{Hz}"], "N": [100000.0, "Mul(Symbol('(length)', positive=True), Symbol('(mass)', positive=True), Pow(Symbol('(time)', positive=True), Integer(-2)))", 0.0, "\\rm{N}"], "C": [1.0, "Mul(Symbol('(current_mks)', positive=True), Symbol('(time)', positive=True))", 0.0, "\\rm{C}"], "A": [1.0, "Symbol('(current_mks)', positive=True)", 0.0, "\\rm{A}"], "T": [1000.0, "Mul(Pow(Symbol('(current_mks)', positive=True), Integer(-1)), Symbol('(mass)', positive=True), Pow(Symbol('(time)', positive=True), Integer(-2)))", 0.0, "\\rm{T}"], "Pa": [10.0, "Mul(Pow(Symbol('(length)', positive=True), Integer(-1)), Symbol('(mass)', positive=True), Pow(Symbol('(time)', positive=True), Integer(-2)))", 0.0, "\\rm{Pa}"], "V": [10000000.0, "Mul(Pow(Symbol('(current_mks)', positive=True), Integer(-1)), Pow(Symbol('(length)', positive=True), Integer(2)), Symbol('(mass)', positive=True), Pow(Symbol('(time)', positive=True), Integer(-3)))", 0.0, "\\rm{V}"], "ohm": [10000000.0, "Mul(Pow(Symbol('(current_mks)', positive=True), Integer(-2)), Pow(Symbol('(length)', positive=True), Integer(2)), Symbol('(mass)', positive=True), Pow(Symbol('(time)', positive=True), Integer(-3)))", 0.0, "\\Omega"], "ft": [30.48, "Symbol('(length)', positive=True)", 0.0, "\\rm{ft}"], "mile": [160934.4, "Symbol('(length)', positive=True)", 0.0, "\\rm{mile}"], "degF": [0.5555555555555556, "Symbol('(temperature)', positive=True)", -459.67, "^\\circ\\rm{F}"], "R": [0.5555555555555556, "Symbol('(temperature)', positive=True)", 0.0, "^\\circ\\rm{R}"], "lbf": [444822.16152605, "Mul(Symbol('(length)', positive=True), Symbol('(mass)', positive=True), Pow(Symbol('(time)', positive=True), Integer(-2)))", 0.0, "\\rm{lbf}"], "lbm": [453.59237, "Symbol('(mass)', positive=True)", 0.0, "\\rm{lbm}"], "atm": [1013250.0, "Mul(Pow(Symbol('(length)', positive=True), Integer(-1)), Symbol('(mass)', positive=True), Pow(Symbol('(time)', positive=True), Integer(-2)))", 0.0, "\\rm{atm}"], "h": [0.702, "Integer(1)", 0.0, "h"], "dimensionless": [1.0, "Integer(1)", 0.0, ""], "min": [60.0, "Symbol('(time)', positive=True)", 0.0, "\\rm{min}"], "hr": [3600.0, "Symbol('(time)', positive=True)", 0.0, "\\rm{hr}"], "day": [86400.0, "Symbol('(time)', positive=True)", 0.0, "\\rm{d}"], "d": [86400.0, "Symbol('(time)', positive=True)", 0.0, "\\rm{d}"], "yr": [31557600.0, "Symbol('(time)', positive=True)", 0.0, "\\rm{yr}"], "c": [29979245800.0, "Mul(Symbol('(length)', positive=True), Pow(Symbol('(time)', positive=True), Integer(-1)))", 0.0, "\\rm{c}"], "Msun": [1.98841586e+33, "Symbol('(mass)', positive=True)", 0.0, "\\rm{M}_\\odot"], "msun": [1.98841586e+33, "Symbol('(mass)', positive=True)", 0.0, "\\rm{M}_\\odot"], "Rsun": [69550001060.63751, "Symbol('(length)', positive=True)", 0.0, "\\rm{R}_\\odot"], "rsun": [69550001060.63751, "Symbol('(length)', positive=True)", 0.0, "\\rm{R}_\\odot"], "R_sun": [69550001060.63751, "Symbol('(length)', positive=True)", 0.0, "\\rm{R}_\\odot"], "r_sun": [69550001060.63751, "Symbol('(length)', positive=True)", 0.0, "\\rm{R}_\\odot"], "Lsun": [3.827e+33, "Mul(Pow(Symbol('(length)', positive=True), Integer(2)), Symbol('(mass)', positive=True), Pow(Symbol('(time)', positive=True), Integer(-3)))", 0.0, "\\rm{L}_\\odot"], "Tsun": [5870.0, "Symbol('(temperature)', positive=True)", 0.0, "\\rm{T}_\\odot"], "Zsun": [0.01295, "Integer(1)", 0.0, "\\rm{Z}_\\odot"], "Mjup": [1.8985234333630655e+30, "Symbol('(mass)', positive=True)", 0.0, "\\rm{M}_{\\rm{Jup}}"], "Mearth": [6.045644495102106e+27, "Symbol('(mass)', positive=True)", 0.0, "\\rm{M}_\\oplus"], "AU": [14959787075076.674, "Symbol('(length)', positive=True)", 0.0, "\\rm{AU}"], "au": [14959787075076.674, "Symbol('(length)', positive=True)", 0.0, "\\rm{AU}"], "ly": [9.460528409678268e+17, "Symbol('(length)', positive=True)", 0.0, "\\rm{ly}"], "pc": [3.0856775809623245e+18, "Symbol('(length)', positive=True)", 0.0, "\\rm{pc}"], "degree": [0.017453292519943295, "Symbol('(angle)', positive=True)", 0.0, "^\\circ"], "arcmin": [0.0002908882086657216, "Symbol('(angle)', positive=True)", 0.0, "\\rm{arcmin}"], "arcsec": [4.84813681109536e-06, "Symbol('(angle)', positive=True)", 0.0, "\\rm{arcsec}"], "mas": [4.8481368110953594e-09, "Symbol('(angle)', positive=True)", 0.0, "\\rm{mas}"], "hourangle": [0.2617993877991494, "Symbol('(angle)', positive=True)", 0.0, "\\rm{HA}"], "steradian": [1.0, "Pow(Symbol('(angle)', positive=True), Integer(2))", 0.0, "\\rm{sr}"], "lat": [-0.017453292519943295, "Symbol('(angle)', positive=True)", 90.0, "\\rm{Latitude}"], "lon": [0.017453292519943295, "Symbol('(angle)', positive=True)", -180.0, "\\rm{Longitude}"], "eV": [1.602176562e-12, "Mul(Pow(Symbol('(length)', positive=True), Integer(2)), Symbol('(mass)', positive=True), Pow(Symbol('(time)', positive=True), Integer(-2)))", 0.0, "\\rm{eV}"], "amu": [1.660538921e-24, "Symbol('(mass)', positive=True)", 0.0, "\\rm{amu}"], "angstrom": [1e-08, "Symbol('(length)', positive=True)", 0.0, "\\AA"], "Jy": [1e-23, "Mul(Symbol('(mass)', positive=True), Pow(Symbol('(time)', positive=True), Integer(-2)))", 0.0, "\\rm{Jy}"], "counts": [1.0, "Integer(1)", 0.0, "\\rm{counts}"], "photons": [1.0, "Integer(1)", 0.0, "\\rm{photons}"], "me": [9.10938291e-28, "Symbol('(mass)', positive=True)", 0.0, "m_e"], "mp": [1.6737352238051868e-24, "Symbol('(mass)', positive=True)", 0.0, "m_p"], "mol": [6.02214129011674e+23, "Integer(1)", 0.0, "\\rm{mol}"], "Sv": [10000.0, "Mul(Pow(Symbol('(length)', positive=True), Integer(2)), Pow(Symbol('(time)', positive=True), Integer(-2)))", 0.0, "\\rm{Sv}"], "rayleigh": [79577.47154594767, "Mul(Pow(Symbol('(angle)', positive=True), Integer(-2)), Pow(Symbol('(length)', positive=True), Integer(-2)), Pow(Symbol('(time)', positive=True), Integer(-1)))", 0.0, "\\rm{R}"], "solMass": [1.98841586e+33, "Symbol('(mass)', positive=True)", 0.0, "\\rm{M}_\\odot"], "solRad": [69550001060.63751, "Symbol('(length)', positive=True)", 0.0, "\\rm{R}_\\odot"], "solLum": [3.827e+33, "Mul(Pow(Symbol('(length)', positive=True), Integer(2)), Symbol('(mass)', positive=True), Pow(Symbol('(time)', positive=True), Integer(-3)))", 0.0, "\\rm{L}_\\odot"], "dyn": [1.0, "Mul(Symbol('(length)', positive=True), Symbol('(mass)', positive=True), Pow(Symbol('(time)', positive=True), Integer(-2)))", 0.0, "\\rm{dyn}"], "sr": [1.0, "Pow(Symbol('(angle)', positive=True), Integer(2))", 0.0, "\\rm{sr}"], "rad": [1.0, "Symbol('(angle)', positive=True)", 0.0, "\\rm{rad}"], "deg": [0.017453292519943295, "Symbol('(angle)', positive=True)", 0.0, "^\\circ"], "Fr": [1.0, "Mul(Pow(Symbol('(length)', positive=True), Rational(3, 2)), Pow(Symbol('(mass)', positive=True), Rational(1, 2)), Pow(Symbol('(time)', positive=True), Integer(-1)))", 0.0, "\\rm{Fr}"], "G": [1.0, "Mul(Pow(Symbol('(length)', positive=True), Rational(-1, 2)), Pow(Symbol('(mass)', positive=True), Rational(1, 2)), Pow(Symbol('(time)', positive=True), Integer(-1)))", 0.0, "\\rm{G}"], "Angstrom": [1e-08, "Symbol('(length)', positive=True)", 0.0, "\\AA"], "statC": [1.0, "Mul(Pow(Symbol('(length)', positive=True), Rational(3, 2)), Pow(Symbol('(mass)', positive=True), Rational(1, 2)), Pow(Symbol('(time)', positive=True), Integer(-1)))", 0.0, "\\rm{statC}"], "m_pl": [2.1764701184435688e-05, "Symbol('(mass)', positive=True)", 0.0, "m_{\\rm{P}}"], "l_pl": [1.6162283157597786e-33, "Symbol('(length)', positive=True)", 0.0, "\\ell_{\\rm{P}}"], "t_pl": [5.391157357800438e-44, "Symbol('(time)', positive=True)", 0.0, "t_{\\rm{P}}"], "T_pl": [1.4168076561665009e+32, "Symbol('(temperature)', positive=True)", 0.0, "T_{\\rm{P}}"], "q_pl": [1.8755459562040954e-18, "Mul(Symbol('(current_mks)', positive=True), Symbol('(time)', positive=True))", 0.0, "q_{\\rm{P}}"], "E_pl": [1.9561137903170922e+16, "Mul(Pow(Symbol('(length)', positive=True), Integer(2)), Symbol('(mass)', positive=True), Pow(Symbol('(time)', positive=True), Integer(-2)))", 0.0, "E_{\\rm{P}}"], "m_geom": [1.98841586e+33, "Symbol('(mass)', positive=True)", 0.0, "\\rm{M}_\\odot"], "l_geom": [147658.08127593446, "Symbol('(length)', positive=True)", 0.0, "\\rm{M}_\\odot"], "t_geom": [4.925343427950227e-06, "Symbol('(time)', positive=True)", 0.0, "\\rm{M}_\\odot"], "R_earth": [637100801.9550942, "Symbol('(length)', positive=True)", 0.0, "\\rm{R}_\\oplus"], "r_earth": [637100801.9550942, "Symbol('(length)', positive=True)", 0.0, "\\rm{R}_\\oplus"], "R_jup": [6991100000.006706, "Symbol('(length)', positive=True)", 0.0, "\\rm{R}_\\mathrm{Jup}"], "r_jup": [6991100000.006706, "Symbol('(length)', positive=True)", 0.0, "\\rm{R}_\\mathrm{Jup}"], "code_length": [1.4065766789943524e+26, "Symbol('(length)', positive=True)", 0.0, "\\rm{code\\ length}"], "code_mass": [7.032854288130045e+48, "Symbol('(mass)', positive=True)", 0.0, "\\rm{code\\ mass}"], "code_density": [2.527210293640965e-30, "Mul(Pow(Symbol('(length)', positive=True), Integer(-3)), Symbol('(mass)', positive=True))", 0.0, "\\rm{code\\ density}"], "code_specific_energy": [2.1385728000000008e+18, "Mul(Pow(Symbol('(length)', positive=True), Integer(2)), Pow(Symbol('(time)', positive=True), Integer(-2)))", 0.0, "\\rm{code\\ specific\\ energy}"], "code_time": [1885954521538155.5, "Symbol('(time)', positive=True)", 0.0, "\\rm{code\\ time}"], "code_magnetic": [8.24114664867782e-06, "Mul(Pow(Symbol('(length)', positive=True), Rational(-1, 2)), Pow(Symbol('(mass)', positive=True), Rational(1, 2)), Pow(Symbol('(time)', positive=True), Integer(-1)))", 0.0, "\\rm{code\\ magnetic}"], "code_temperature": [1.0, "Symbol('(temperature)', positive=True)", 0.0, "\\rm{code\\ temperature}"], "code_pressure": [1.4057425005421192e-08, "Mul(Pow(Symbol('(length)', positive=True), Integer(-1)), Symbol('(mass)', positive=True), Pow(Symbol('(time)', positive=True), Integer(-2)))", 0.0, "\\rm{code\\ pressure}"], "code_velocity": [1462385995.556577, "Mul(Symbol('(length)', positive=True), Pow(Symbol('(time)', positive=True), Integer(-1)))", 0.0, "\\rm{code\\ velocity}"], "code_metallicity": [1.0, "Integer(1)", 0.0, "\\rm{code\\ metallicity}"], "a": [1.0000000027810865, "Integer(1)", 0.0, "\\rm{a}"], "mcm": [100.00000027810864, "Symbol('(length)', positive=True)", 0.0, "\\rm{m}/(1+z)"], "pccm": [3.08567758954386e+18, "Symbol('(length)', positive=True)", 0.0, "\\rm{pc}/(1+z)"], "AUcm": [14959787116681.133, "Symbol('(length)', positive=True)", 0.0, "\\rm{AU}/(1+z)"], "aucm": [14959787116681.133, "Symbol('(length)', positive=True)", 0.0, "\\rm{au}/(1+z)"], "Mpccm": [3.08567758954386e+24, "Symbol('(length)', positive=True)", 0.0, "\\rm{Mpc}/(1+z)"], "cm": [1.0, "Symbol('(length)', positive=True)", 0.0, "\\rm{cm}"], "unitary": [1.4065766789943524e+26, "Symbol('(length)', positive=True)", 0.0, "\\rm{unitary}"], "keV": [1.602176562e-09, "Mul(Pow(Symbol('(length)', positive=True), Integer(2)), Symbol('(mass)', positive=True), Pow(Symbol('(time)', positive=True), Integer(-2)))", 0.0, "\\rm{keV}"]} unyt-3.0.4/unyt/tests/test_array_functions.py000066400000000000000000002052321476461141700215140ustar00rootroot00000000000000# tests for NumPy __array_function__ support import inspect import re import warnings from importlib.metadata import version import numpy as np import pytest from numpy.testing import assert_allclose from packaging.version import Version from unyt import A, K, Msun, cm, degC, delta_degC, dimensionless, g, km, rad, s from unyt._array_functions import ( _HANDLED_FUNCTIONS as HANDLED_FUNCTIONS, _UNSUPPORTED_FUNCTIONS as UNSUPPORTED_FUNCTIONS, ) from unyt.array import unyt_array, unyt_quantity from unyt.exceptions import ( UnitConversionError, UnitInconsistencyError, UnytError, ) from unyt.testing import assert_array_equal_units from unyt.unit_object import Unit from unyt.unit_registry import UnitRegistry NUMPY_VERSION = Version(version("numpy")) VAR_POSITIONAL = inspect.Parameter.VAR_POSITIONAL VAR_KEYWORD = inspect.Parameter.VAR_KEYWORD POSITIONAL_ONLY = inspect.Parameter.POSITIONAL_ONLY KEYWORD_ONLY = inspect.Parameter.KEYWORD_ONLY POSITIONAL_OR_KEYWORD = inspect.Parameter.POSITIONAL_OR_KEYWORD # this is a subset of NOT_HANDLED_FUNCTIONS for which there's nothing to do # because they don't apply to (real) numeric types # or they work as expected out of the box # This is not necessarily complete ! NOOP_FUNCTIONS = { np.all, # expects booleans np.amax, # works out of the box (tested) np.amin, # works out of the box (tested) np.angle, # expects complex numbers np.any, # works out of the box (tested) np.append, # we get it for free with np.concatenate (tested) np.apply_along_axis, # works out of the box (tested) np.argmax, # returns pure numbers np.argmin, # returns pure numbers np.argpartition, # returns pure numbers np.argsort, # returns pure numbers np.argwhere, # returns pure numbers np.array_str, # hooks into __str__ np.atleast_1d, # works out of the box (tested) np.atleast_2d, # works out of the box (tested) np.atleast_3d, # works out of the box (tested) np.average, # works out of the box (tested) np.can_cast, # works out of the box (tested) np.common_type, # works out of the box (tested) np.result_type, # works out of the box (tested) np.iscomplex, # works out of the box (tested) np.iscomplexobj, # works out of the box (tested) np.isreal, # works out of the box (tested) np.isrealobj, # works out of the box (tested) np.nan_to_num, # works out of the box (tested) np.nanargmax, # return pure numbers np.nanargmin, # return pure numbers np.nanmax, # works out of the box (tested) np.nanmean, # works out of the box (tested) np.nanmedian, # works out of the box (tested) np.nanmin, # works out of the box (tested) np.trim_zeros, # works out of the box (tested) np.max, # works out of the box (tested) np.mean, # works out of the box (tested) np.median, # works out of the box (tested) np.min, # works out of the box (tested) np.ndim, # return pure numbers np.shape, # returns pure numbers np.size, # returns pure numbers np.sort, # works out of the box (tested) np.sum, # works out of the box (tested) np.repeat, # works out of the box (tested) np.tile, # works out of the box (tested) np.shares_memory, # works out of the box (tested) np.nonzero, # works out of the box (tested) np.count_nonzero, # returns pure numbers np.flatnonzero, # works out of the box (tested) np.isneginf, # works out of the box (tested) np.isposinf, # works out of the box (tested) np.empty_like, # works out of the box (tested) np.full_like, # works out of the box (tested) np.ones_like, # works out of the box (tested) np.zeros_like, # works out of the box (tested) np.copy, # works out of the box (tested) np.meshgrid, # works out of the box (tested) np.transpose, # works out of the box (tested) np.reshape, # works out of the box (tested) np.resize, # works out of the box (tested) np.roll, # works out of the box (tested) np.rollaxis, # works out of the box (tested) np.rot90, # works out of the box (tested) np.expand_dims, # works out of the box (tested) np.squeeze, # works out of the box (tested) np.flip, # works out of the box (tested) np.fliplr, # works out of the box (tested) np.flipud, # works out of the box (tested) np.delete, # works out of the box (tested) np.partition, # works out of the box (tested) np.broadcast_to, # works out of the box (tested) np.broadcast_arrays, # works out of the box (tested) np.split, # works out of the box (tested) np.array_split, # works out of the box (tested) np.dsplit, # works out of the box (tested) np.hsplit, # works out of the box (tested) np.vsplit, # works out of the box (tested) np.swapaxes, # works out of the box (tested) np.moveaxis, # works out of the box (tested) np.nansum, # works out of the box (tested) np.std, # works out of the box (tested) np.nanstd, # works out of the box (tested) np.nanvar, # works out of the box (tested) np.nanprod, # works out of the box (tested) np.diag, # works out of the box (tested) np.diag_indices_from, # returns pure numbers np.diagflat, # works out of the box (tested) np.diagonal, # works out of the box (tested) np.ravel, # returns pure numbers np.ravel_multi_index, # returns pure numbers np.unravel_index, # returns pure numbers np.fix, # works out of the box (tested) np.round, # is implemented via np.around np.may_share_memory, # returns pure numbers (booleans) np.linalg.matrix_power, # works out of the box (tested) np.linalg.cholesky, # works out of the box (tested) np.linalg.multi_dot, # works out of the box (tested) np.linalg.matrix_rank, # returns pure numbers np.linalg.qr, # works out of the box (tested) np.linalg.slogdet, # undefined units np.linalg.cond, # works out of the box (tested) np.gradient, # works out of the box (tested) np.cumsum, # works out of the box (tested) np.nancumsum, # works out of the box (tested) np.nancumprod, # we get it for free with np.cumprod (tested) np.bincount, # works out of the box (tested) np.unique, # works out of the box (tested) np.min_scalar_type, # returns dtypes np.extract, # works out of the box (tested) np.setxor1d, # we get it for free with previously implemented functions (tested) np.lexsort, # returns pure numbers np.digitize, # returns pure numbers np.tril_indices_from, # returns pure numbers np.triu_indices_from, # returns pure numbers np.imag, # works out of the box (tested) np.real, # works out of the box (tested) np.real_if_close, # works out of the box (tested) np.einsum_path, # returns pure numbers np.cov, # returns pure numbers np.corrcoef, # returns pure numbers np.compress, # works out of the box (tested) np.take_along_axis, # works out of the box (tested) } if NUMPY_VERSION >= Version("2.0.0dev0"): # the following all work out of the box (tested) NOOP_FUNCTIONS |= { np.linalg.cross, np.linalg.diagonal, np.linalg.matmul, np.linalg.matrix_norm, np.linalg.matrix_transpose, np.linalg.svdvals, np.linalg.tensordot, np.linalg.trace, np.linalg.vecdot, np.linalg.vector_norm, np.astype, np.matrix_transpose, np.unique_all, np.unique_counts, np.unique_inverse, np.unique_values, } if NUMPY_VERSION >= Version("2.1.0dev0"): NOOP_FUNCTIONS |= { np.unstack, np.cumulative_sum, } # Functions for which behaviour is intentionally left to default IGNORED_FUNCTIONS = { np.i0, # IO functions (no way to add units) np.save, np.savez, np.savez_compressed, } # map subsets of deprecated functions to the version they were removed # so that we can drop the ones that are not present in any version we support DEPRECATED_FUNCTIONS = { Version("1.20"): { "fv", "ipmt", "irr", "mirr", "nper", "npv", "pmt", "ppmt", "pv", "rate", }, Version("1.22"): { "alen", "asscalar", }, Version("2.0.0b1"): { "msort", "product", "cumproduct", "round_", "sometrue", "alltrue", }, # functions that are deprecated but not yet removed in any known version # should be added here Version("999.999.999"): set(), } NOT_HANDLED_FUNCTIONS = NOOP_FUNCTIONS | UNSUPPORTED_FUNCTIONS | IGNORED_FUNCTIONS with warnings.catch_warnings(): warnings.simplefilter("ignore", DeprecationWarning) for removal_version, functions in DEPRECATED_FUNCTIONS.items(): if NUMPY_VERSION >= removal_version: continue for func in functions: NOT_HANDLED_FUNCTIONS.add(getattr(np, func)) def get_decorators(func): # adapted from # https://stackoverflow.com/questions/3232024/introspection-to-get-decorator-names-on-a-method import ast import inspect target = func decorators = {} def visit_FunctionDef(node): decorators[node.name] = [] for n in node.decorator_list: name = "" if isinstance(n, ast.Call): name = n.func.attr if isinstance(n.func, ast.Attribute) else n.func.id else: name = n.attr if isinstance(n, ast.Attribute) else n.id decorators[node.name].append(name) node_iter = ast.NodeVisitor() node_iter.visit_FunctionDef = visit_FunctionDef try: node_iter.visit(ast.parse(inspect.getsource(target))) return decorators[func.__name__] except TypeError: # may be raised if inspecting a C compiled function # in which case, we return with empty hands return [] def get_wrapped_functions(*modules): """get functions that support __array_function__ in modules This was adapted from astropy's tests """ wrapped_functions = {} for mod in modules: for name, f in mod.__dict__.items(): if callable(f) and hasattr(f, "__wrapped__"): if ( f is np.printoptions or f.__name__.startswith("_") or "deprecate" in get_decorators(f) ): continue wrapped_functions[mod.__name__ + "." + name] = f return dict(sorted(wrapped_functions.items())) def test_wrapping_completeness(): """Ensure we wrap all numpy functions that support __array_function__""" handled_numpy_functions = set(HANDLED_FUNCTIONS.keys()) # ensure no functions appear in both NOT_HANDLED_FUNCTIONS and HANDLED_FUNCTIONS assert NOT_HANDLED_FUNCTIONS.isdisjoint(handled_numpy_functions), ( NOT_HANDLED_FUNCTIONS.intersection(handled_numpy_functions) ) # get list of functions that support wrapping by introspection on numpy module wrappable_functions = get_wrapped_functions(np, np.fft, np.linalg) for function in HANDLED_FUNCTIONS: # ensure we only have wrappers for functions that support wrapping assert function in wrappable_functions.values() all_funcs = NOT_HANDLED_FUNCTIONS.union(handled_numpy_functions) # ensure all functions in numpy that support wrapping either have wrappers # or are explicitly whitelisted for function in wrappable_functions.values(): assert function in all_funcs @pytest.mark.parametrize( "arrays", [ [np.array([1]), [2] * Unit()], [np.array([1]), [2] * Unit(registry=UnitRegistry())], [[1], [2] * Unit()], ], ) def test_unit_validation(arrays): # see https://github.com/yt-project/unyt/issues/462 # numpy.concatenate isn't essential to this test # what we're really testing is the unit consistency validation # underneath, but we do so using public API res = np.concatenate(arrays) assert res.units.is_dimensionless def test_unit_validation_dimensionless_factors(): # see https://github.com/yt-project/unyt/issues/477 # numpy.concatenate isn't essential to this test # what we're really testing is the unit consistency validation # underneath, but we do so using public API res = np.concatenate([[1] * cm, unyt_array([1], "cm*dimensionless")]) assert res.units is cm def test_array_repr(): arr = [1, 2, 3] * cm assert re.fullmatch(r"unyt_array\(\[1, 2, 3\], (units=)?'cm'\)", np.array_repr(arr)) def test_dot_vectors(): a = [1, 2, 3] * cm b = [1, 2, 3] * s res = np.dot(a, b) assert res.units == cm * s assert res.d == 14 # NOTE: explicitly setting the dtype of out arrays as `dtype=np.int_` is the # cross-platform way to guarantee that their dtype matches that of np.arange(x) # (on POSIX system it's int64, while on windows it's int32) @pytest.mark.parametrize( "out", [ None, np.empty((3, 3), dtype=np.int_), np.empty((3, 3), dtype=np.int_, order="C") * cm * s, np.empty((3, 3), dtype=np.int_, order="C") * km * s, ], ids=[ "None", "pure ndarray", "same units", "convertible units", ], ) def test_dot_matrices(out): a = np.arange(9) * cm a.shape = (3, 3) b = np.arange(9) * s b.shape = (3, 3) res = np.dot(a, b, out=out) if out is not None: np.testing.assert_array_equal(res, out) assert np.shares_memory(res, out) assert isinstance(res, unyt_array) assert isinstance(out, np.ndarray) if isinstance(out, unyt_array): # check that the result can be converted to predictable units res.in_units("cm * s") assert out.units == res.units def test_dot_mixed_ndarray_unyt_array(): a = np.ones((3, 3)) b = np.ones((3, 3)) * cm res = np.dot(a, b) assert isinstance(res, unyt_array) assert res.units == cm out = np.zeros((3, 3)) res = np.dot(a, b, out=out) assert isinstance(res, unyt_array) assert type(out) is np.ndarray assert np.shares_memory(out, res) np.testing.assert_array_equal(out, res) out = np.zeros((3, 3)) * km res = np.dot(a, b, out=out) assert isinstance(res, unyt_array) assert isinstance(out, unyt_array) assert res.units == out.units == cm assert np.shares_memory(res, out) # check this works with an ndarray as the first operand out = np.zeros((3, 3)) * km res = np.dot(b, a, out=out) assert isinstance(res, unyt_array) assert isinstance(out, unyt_array) assert res.units == out.units == cm assert np.shares_memory(res, out) def test_invalid_dot_matrices(): a = np.arange(9) * cm a.shape = (3, 3) b = np.arange(9) * s b.shape = (3, 3) out = np.empty((3, 3), dtype=np.int_, order="C") * s**2 res = np.dot(a, b, out=out) np.testing.assert_array_equal(res, out) assert out.units == res.units == cm * s def test_vdot(): a = np.arange(9) * cm b = np.arange(9) * s res = np.vdot(a, b) assert res.units == cm * s def test_inner(): a = np.array([1, 2, 3]) * cm b = np.array([0, 1, 0]) * s res = np.inner(a, b) assert res.d == 2 assert res.units == cm * s def test_outer(): a = np.array([1, 2, 3]) * cm b = np.array([0, 1, 0]) * s res = np.outer(a, b) expected = np.array([[0, 1, 0], [0, 2, 0], [0, 3, 0]]) np.testing.assert_array_equal(res.ndview, expected) assert res.units == cm * s def test_kron(): a = np.eye(2) * cm b = np.ones((2, 2)) * s res = np.kron(a, b) assert res.units == cm * s def test_linalg_inv(): rng = np.random.default_rng() arr = rng.random((3, 3)) * cm iarr = np.linalg.inv(arr) assert 1 * iarr.units == 1 / cm def test_linalg_tensorinv(): a = np.eye(4 * 6) * cm a.shape = (4, 6, 8, 3) ia = np.linalg.tensorinv(a) assert 1 * ia.units == 1 / cm def test_linalg_pinv(): rng = np.random.default_rng() a = rng.standard_normal(size=(9, 6)) * cm B = np.linalg.pinv(a) assert 1 * B.units == 1 / cm np.testing.assert_allclose(a, np.dot(a, np.dot(B, a))) np.testing.assert_allclose(B, np.dot(B, np.dot(a, B))) # see https://github.com/numpy/numpy/issues/22444 @pytest.mark.xfail( reason=( "as of numpy 1.21.2, the __array_function__ protocol doesn't let " "us overload np.linalg.pinv for stacks of matrices" ) ) def test_matrix_stack_linalg_pinv(): stack = [np.eye(4) * g for _ in range(3)] B = np.linalg.pinv(stack) assert 1 * B.units == 1 / g # see https://github.com/numpy/numpy/issues/22444 @pytest.mark.xfail( reason=( "as of numpy 1.21.2, the __array_function__ protocol doesn't let " "us overload np.linalg.pinv for stacks of matrices" ) ) def test_invalid_matrix_stack_linalg_pinv(): stack = [np.eye(4) * g, np.eye(4) * s] with pytest.raises( TypeError, match=re.escape( "numpy.linalg.pinv cannot operate on a stack " "of matrices with different units." ), ): np.linalg.pinv(stack) @pytest.mark.skipif( NUMPY_VERSION < Version("2.0.0dev0"), reason="linalg.diagonal is new in numpy 2.0" ) def test_linalg_diagonal(): a = np.eye(3) * cm b = np.linalg.diagonal(a) assert type(b) is unyt_array assert b.units == a.units @pytest.mark.skipif( NUMPY_VERSION < Version("2.0.0dev0"), reason="linalg.trace is new in numpy 2.0" ) def test_linalg_trace(): a = np.eye(3) * cm b = np.linalg.trace(a) assert type(b) is unyt_quantity assert b.units == a.units @pytest.mark.skipif( NUMPY_VERSION < Version("2.0.0dev0"), reason="linalg.outer is new in numpy 2.0" ) def test_linalg_outer(): a = np.arange(10) * cm assert_array_equal_units(np.linalg.outer(a, a), np.outer(a, a)) @pytest.mark.skipif( NUMPY_VERSION < Version("2.0.0dev0"), reason="linalg.cross is new in numpy 2.0" ) def test_linalg_cross(): a = np.arange(3) * cm assert_array_equal_units(np.linalg.cross(a, a), np.cross(a, a)) @pytest.mark.skipif( NUMPY_VERSION < Version("2.0.0dev0"), reason="linalg.matmul is new in numpy 2.0" ) def test_linalg_matmul(): a = np.eye(3) * cm assert_array_equal_units(np.linalg.matmul(a, a), np.matmul(a, a)) @pytest.mark.skipif( NUMPY_VERSION < Version("2.0.0dev0"), reason="linalg.matrix_norm is new in numpy 2.0", ) def test_linalg_matrix_norm(): a = np.eye(3) * cm assert_array_equal_units(np.linalg.matrix_norm(a), np.linalg.norm(a)) @pytest.mark.skipif( NUMPY_VERSION < Version("2.0.0dev0"), reason="matrix_transpose is new in numpy 2.0" ) @pytest.mark.parametrize("namespace", [None, "linalg"]) def test_matrix_transpose(namespace): if namespace is None: func = np.matrix_transpose else: func = getattr(np, namespace).matrix_transpose a = np.arange(0, 9).reshape(3, 3) assert_array_equal_units(func(a), np.transpose(a)) @pytest.mark.skipif( NUMPY_VERSION < Version("2.0.0dev0"), reason="linalg.vecdot is new in numpy 2.0" ) def test_linalg_vecdot(): a = np.arange(0, 9) assert_array_equal_units(np.linalg.vecdot(a, a), np.vdot(a, a)) @pytest.mark.skipif( NUMPY_VERSION < Version("2.0.0dev0"), reason="linalg.vector_norm is new in numpy 2.0", ) def test_linalg_vector_norm(): a = np.arange(0, 9) assert_array_equal_units(np.linalg.vector_norm(a), np.linalg.norm(a)) def test_linalg_svd(): rng = np.random.default_rng() a = (rng.standard_normal(size=(9, 6)) + 1j * rng.standard_normal(size=(9, 6))) * cm u, s, vh = np.linalg.svd(a) assert type(u) is np.ndarray assert type(vh) is np.ndarray assert type(s) is unyt_array assert s.units == cm s = np.linalg.svd(a, compute_uv=False) assert type(s) is unyt_array assert s.units == cm @pytest.mark.skipif( NUMPY_VERSION < Version("2.0.0dev0"), reason="linalg.svdvals is new in numpy 2.0" ) def test_linalg_svdvals(): q = np.arange(9).reshape(3, 3) * cm _, ref, _ = np.linalg.svd(q) res = np.linalg.svdvals(q) assert type(res) is unyt_array assert_allclose(res, ref, rtol=5e-16) @pytest.mark.skipif( NUMPY_VERSION < Version("2.0.0dev0"), reason="linalg.tensordot is new in numpy 2.0" ) def test_linalg_tensordot(): q = np.arange(9).reshape(3, 3) * cm ref = np.tensordot(q, q) res = np.linalg.tensordot(q, q) assert_array_equal_units(res, ref) class TestHistograms: def test_histogram(self): rng = np.random.default_rng() arr = rng.normal(size=1000) * cm counts, bins = np.histogram(arr, bins=10, range=(arr.min(), arr.max())) assert type(counts) is np.ndarray assert bins.units == arr.units def test_histogram_implicit_units(self): # see https://github.com/yt-project/unyt/issues/465 rng = np.random.default_rng() arr = rng.normal(size=1000) * cm counts, bins = np.histogram( arr, bins=10, range=(arr.min().value, arr.max().value) ) assert type(counts) is np.ndarray assert bins.units == arr.units def test_histogram_with_density(self): rng = np.random.default_rng() arr = rng.normal(size=1000) * cm density, bins = np.histogram( arr, bins=10, range=(arr.min(), arr.max()), density=True ) assert type(density) is unyt_array assert density.units == arr.units**-1 assert bins.units == arr.units def test_histogram_with_weights(self): rng = np.random.default_rng() arr = rng.normal(size=1000) * cm w = rng.uniform(size=1000) * g wcounts, wbins = np.histogram( arr, bins=10, range=(arr.min(), arr.max()), weights=w ) assert type(wcounts) is unyt_array assert wcounts.units == w.units assert wbins.units == arr.units def test_histogram_with_weights_and_density(self): rng = np.random.default_rng() arr = rng.normal(size=1000) * cm w = rng.uniform(size=1000) * g wdensity, wdbins = np.histogram( arr, bins=10, range=(arr.min(), arr.max()), density=True, weights=w ) assert type(wdensity) is unyt_array assert wdensity.units == w.units / arr.units assert wdbins.units == arr.units def test_histogram_with_weights_and_dimless_arr(self): rng = np.random.default_rng() arr = rng.normal(size=1000) * cm w = rng.uniform(size=1000) * g wcounts2, wbins2 = np.histogram(arr.to_value(arr.units), weights=w) assert type(wcounts2) is unyt_array assert wcounts2.units == w.units assert not hasattr(wbins2, "units") def test_histogram2d(self): rng = np.random.default_rng() x = rng.normal(size=100) * cm y = rng.normal(loc=10, size=100) * s counts, xbins, ybins = np.histogram2d(x, y) assert counts.ndim == 2 assert xbins.units == x.units assert ybins.units == y.units def test_histogram2d_with_density(self): rng = np.random.default_rng() x = rng.normal(size=100) * cm y = rng.normal(loc=10, size=100) * s density, xbins, ybins = np.histogram2d(x, y, density=True) assert density.ndim == 2 assert type(density) is unyt_array assert density.units == (x.units * y.units) ** -1 assert xbins.units == x.units assert ybins.units == y.units def test_histogram2d_with_weights(self): rng = np.random.default_rng() x = rng.normal(size=100) * cm y = rng.normal(loc=10, size=100) * s w = rng.uniform(size=100) * g wcounts, xwbins, ywbins = np.histogram2d(x, y, weights=w) assert wcounts.ndim == 2 assert type(wcounts) is unyt_array assert wcounts.units == w.units assert xwbins.units == x.units assert ywbins.units == y.units def test_histogram2d_with_weights_and_density(self): rng = np.random.default_rng() x = rng.normal(size=100) * cm y = rng.normal(loc=10, size=100) * s w = rng.uniform(size=100) * g wdensity, xwdbins, ywdbins = np.histogram2d(x, y, weights=w, density=True) assert wdensity.ndim == 2 assert type(wdensity) is unyt_array assert wdensity.units == w.units / (x.units * y.units) assert xwdbins.units == x.units assert ywdbins.units == y.units def test_histogram2d_with_weights_and_dimless_arr(self): rng = np.random.default_rng() x = rng.normal(size=100) * cm y = rng.normal(loc=10, size=100) * s w = rng.uniform(size=100) * g wcounts2, xwbins2, ywbins2 = np.histogram2d( x.to_value(x.units), y.to_value(y.units), weights=w ) assert type(wcounts2) is unyt_array assert wcounts2.units == w.units assert not hasattr(xwbins2, "units") assert not hasattr(ywbins2, "units") def test_histogramdd(self): rng = np.random.default_rng() x = rng.normal(size=100) * cm y = rng.normal(size=100) * s z = rng.normal(size=100) * g counts, (xbins, ybins, zbins) = np.histogramdd((x, y, z)) assert counts.ndim == 3 assert xbins.units == x.units assert ybins.units == y.units assert zbins.units == z.units def test_histogramdd_with_density(self): rng = np.random.default_rng() x = rng.normal(size=100) * cm y = rng.normal(loc=10, size=100) * s z = rng.normal(size=100) * g density, (xbins, ybins, zbins) = np.histogramdd((x, y, z), density=True) assert density.ndim == 3 assert type(density) is unyt_array assert density.units == (x.units * y.units * z.units) ** -1 assert xbins.units == x.units assert ybins.units == y.units assert zbins.units == z.units def test_histogramdd_with_weights(self): rng = np.random.default_rng() x = rng.normal(size=100) * cm y = rng.normal(loc=10, size=100) * s z = rng.normal(size=100) * g w = rng.uniform(size=100) * K wcounts, (xwbins, ywbins, zwbins) = np.histogramdd((x, y, z), weights=w) assert wcounts.ndim == 3 assert type(wcounts) is unyt_array assert wcounts.units == w.units assert xwbins.units == x.units assert ywbins.units == y.units assert zwbins.units == z.units def test_histogramdd_with_weights_and_density(self): rng = np.random.default_rng() x = rng.normal(size=100) * cm y = rng.normal(loc=10, size=100) * s z = rng.normal(size=100) * g w = rng.uniform(size=100) * K wdensity, (xwdbins, ywdbins, zwdbins) = np.histogramdd( (x, y, z), weights=w, density=True ) assert wdensity.ndim == 3 assert type(wdensity) is unyt_array assert wdensity.units == w.units / (x.units * y.units * z.units) assert xwdbins.units == x.units assert ywdbins.units == y.units assert zwdbins.units == z.units def test_histogramdd_with_weights_and_dimless_arr(self): rng = np.random.default_rng() x = rng.normal(size=100) * cm y = rng.normal(loc=10, size=100) * s z = rng.normal(size=100) * g w = rng.uniform(size=100) * K wcounts2, (xwbins2, ywbins2, zwbins2) = np.histogramdd( (x.to_value(x.units), y.to_value(y.units), z.to_value(z.units)), weights=w ) assert type(wcounts2) is unyt_array assert wcounts2.units == w.units assert not hasattr(xwbins2, "units") assert not hasattr(ywbins2, "units") assert not hasattr(zwbins2, "units") @pytest.mark.parametrize("weights", [None, [0, 1, 2], [0, 1, 2] * cm]) def test_histogramdd_recursion(self, weights): # regression test for https://github.com/yt-project/unyt/issues/540 sample = [unyt_array(np.arange(3), Msun)] np.histogramdd(sample, density=True, weights=weights) def test_histogram_bin_edges(self): rng = np.random.default_rng() arr = rng.normal(size=1000) * cm bins = np.histogram_bin_edges(arr) assert type(bins) is unyt_array assert bins.units == arr.units def test_concatenate(): rng = np.random.default_rng() x1 = rng.normal(size=100) * cm x2 = rng.normal(size=100) * cm res = np.concatenate((x1, x2)) assert res.units == cm assert res.shape == (200,) def test_concatenate_different_units(): rng = np.random.default_rng() x1 = rng.normal(size=100) * cm x2 = rng.normal(size=100) * s with pytest.raises( UnitInconsistencyError, match=( r"Expected all unyt_array arguments to have identical units\. " r"Received mixed units \(cm, s\)" ), ): np.concatenate((x1, x2)) def test_cross(): x1 = [1, 2, 3] * cm x2 = [4, 5, 6] * s res = np.cross(x1, x2) assert res.units == cm * s def test_intersect1d(): x1 = [1, 2, 3, 4, 5, 6, 7, 8] * cm x2 = [0, 2, 4, 6, 8] * cm res = np.intersect1d(x1, x2) assert res.units == cm np.testing.assert_array_equal(res, [2, 4, 6, 8]) def test_intersect1d_return_indices(): x1 = [1, 2, 3, 4, 5, 6, 7, 8] * cm x2 = [0, 2, 4, 6, 8] * cm ures = np.intersect1d(x1, x2, return_indices=True) rres = np.intersect1d(x1.d, x2.d, return_indices=True) np.testing.assert_array_equal(ures, rres) def test_union1d(): x1 = [-1, 0, 1] * cm x2 = [-2, -1, -3] * cm res = np.union1d(x1, x2) assert res.units == cm np.testing.assert_array_equal(res, [-3, -2, -1, 0, 1]) def test_linalg_norm(): x = [1, 1, 1] * s res = np.linalg.norm(x) assert res.units == s assert res == pytest.approx(np.sqrt(3)) @pytest.mark.parametrize("func", [np.vstack, np.hstack, np.dstack, np.column_stack]) def test_xstack(func): x1 = [0, 1, 2] * cm x2 = [3, 4, 5] * cm res = func((x1, x2)) assert type(res) is unyt_array assert res.units == cm @pytest.mark.parametrize( "axis, expected", [(0, [[0, 1, 2], [3, 4, 5]]), (1, [[0, 3], [1, 4], [2, 5]])] ) def test_stack(axis, expected): x1 = [0, 1, 2] * cm x2 = [3, 4, 5] * cm res = np.stack((x1, x2), axis=axis) assert res.units == cm np.testing.assert_array_equal(res, expected) def test_amax(): x1 = [[0.0, 1.0, 2.0], [3.0, 4.0, 5.0]] * cm res = np.amax(x1) assert type(res) is unyt_quantity res = np.amax(x1, axis=1) assert type(res) is unyt_array def test_amin(): x1 = [[0.0, 1.0, 2.0], [3.0, 4.0, 5.0]] * cm res = np.amin(x1) assert type(res) is unyt_quantity res = np.amin(x1, axis=1) assert type(res) is unyt_array def test_around(): x1 = [[1, 2, 3], [1, 2, 3], [1, 2, 3.0]] * g res = np.around(x1, 2) assert type(res) is unyt_array assert res.units == g def test_atleast_nd(): x0 = 1.0 * cm x1 = np.atleast_1d(x0) assert type(x1) is unyt_array assert x1.ndim == 1 assert x1.units == cm x2 = np.atleast_2d(x0) assert type(x2) is unyt_array assert x2.ndim == 2 assert x2.units == cm x3 = np.atleast_3d(x0) assert type(x3) is unyt_array assert x3.ndim == 3 assert x3.units == cm def test_average(): x1 = [0.0, 1.0, 2.0] * cm res = np.average(x1) assert type(res) is unyt_quantity assert res == 1 * cm def test_trim_zeros(): x1 = [0, 1, 2, 3, 0] * cm res = np.trim_zeros(x1) assert type(res) is unyt_array def test_any(): assert not np.any([0, 0, 0] * cm) assert np.any([1, 0, 0] * cm) x = [1, 2, 3] * cm assert np.any(x >= 3) assert np.any(x >= 3 * cm) assert not np.any(x >= 3 * km) def test_append(): a = [0, 1, 2, 3] * cm b = np.append(a, [4, 5, 6] * cm) assert type(b) is unyt_array assert b.units == cm def test_append_inconsistent_units(): a = [0, 1, 2, 3] * cm with pytest.raises( UnitInconsistencyError, match=re.escape( r"Expected all unyt_array arguments to have identical units. " r"Received mixed units (cm, dimensionless)" ), ): np.append(a, [4, 5, 6]) @pytest.mark.skipif( NUMPY_VERSION >= Version("2.0.0dev0"), reason="np.asfarray is removed in numpy 2.0" ) def test_asfarray(): x1 = np.eye(3, dtype="int64") * cm x2 = np.asfarray(x1) # noqa: NPY201 assert type(x2) is unyt_array assert x2.units == cm assert x2.dtype == "float64" def test_block(): x1 = 1 * np.ones((3, 3)) * cm x2 = 2 * np.ones((3, 1)) * cm res = np.block([[x1, x2]]) assert type(res) is unyt_array assert res.units == cm def test_block_units_inconsistency(): # check that unit inconsistency is correctly detected # for nested lists x1 = 1 * np.ones((3, 3)) * cm x2 = [3 * cm, 3 * cm, 3 * km] with pytest.raises(UnitInconsistencyError): np.block([[x1, x2]]) def test_can_cast(): a = [0, 1, 2] * cm assert np.can_cast(a, "float64") assert np.can_cast(a, "int64") assert not np.can_cast(a, "float16") def test_isreal_like(): a = [1, 2, 3] * cm assert np.all(np.isreal(a)) assert np.isrealobj(a) assert not np.any(np.iscomplex(a)) assert not np.iscomplexobj(a) b = [1j, 2j, 3j] * cm assert not np.any(np.isreal(b)) assert not np.isrealobj(b) assert np.all(np.iscomplex(b)) assert np.iscomplexobj(b) @pytest.mark.parametrize( "func", [ np.fft.fft, np.fft.hfft, np.fft.rfft, np.fft.ifft, np.fft.ihfft, np.fft.irfft, ], ) def test_fft_1D(func): x1 = [0, 1, 2] * cm res = func(x1) assert type(res) is unyt_array assert res.units == (1 * cm).units @pytest.mark.parametrize( "func", [ np.fft.fft2, np.fft.fftn, np.fft.rfft2, np.fft.rfftn, np.fft.ifft2, np.fft.ifftn, np.fft.irfft2, np.fft.irfftn, ], ) def test_fft_ND(func): x1 = [[0, 1, 2], [0, 1, 2], [0, 1, 2]] * cm res = func(x1) assert type(res) is unyt_array assert res.units == (1 * cm).units @pytest.mark.parametrize("func", [np.fft.fftshift, np.fft.ifftshift]) def test_fft_shift(func): x1 = [[0, 1, 2], [0, 1, 2], [0, 1, 2]] * cm res = func(x1) assert type(res) is unyt_array assert res.units == cm if NUMPY_VERSION >= Version("2.0.0dev0"): _trapezoid_func = np.trapezoid else: _trapezoid_func = np.trapz # noqa: NPY201 def test_trapezoid_no_x(): y = [0, 1, 2, 3] * cm res = _trapezoid_func(y) assert type(res) is unyt_quantity assert res.units == cm def test_trapezoid_with_raw_x(): y = [0, 1, 2, 3] * cm x = [0, 1, 2, 3] res = _trapezoid_func(y, x) assert type(res) is unyt_quantity assert res.units == cm def test_trapezoid_with_unit_x(): y = [0, 1, 2, 3] * cm x = [0, 1, 2, 3] * s res = _trapezoid_func(y, x) assert type(res) is unyt_quantity assert res.units == cm * s def test_trapezoid_with_raw_dx(): y = [0, 1, 2, 3] * cm dx = 2.0 res = _trapezoid_func(y, dx=dx) assert type(res) is unyt_quantity assert res.units == cm def test_trapezoid_with_unit_dx(): y = [0, 1, 2, 3] * cm dx = 2.0 * s res = _trapezoid_func(y, dx=dx) assert type(res) is unyt_quantity assert res.units == cm * s @pytest.mark.parametrize( "op", ["min", "max", "mean", "median", "sum", "nanmin", "nanmax", "nanmean", "nanmedian"], ) def test_scalar_reduction(op): x = [0, 1, 2] * cm res = getattr(np, op)(x) assert type(res) is unyt_quantity assert res.units == cm @pytest.mark.parametrize("op", ["sort", "sort_complex"]) def test_sort(op): x = [2, 0, 1] * cm res = getattr(np, op)(x) assert type(res) is unyt_array assert res.units == cm def test_repeat(): x = [2, 0, 1] * cm res = np.repeat(x, 2) assert type(res) is unyt_array assert res.units == cm def test_tile(): x = [2, 0, 1] * cm res = np.tile(x, (2, 3)) assert type(res) is unyt_array assert res.units == cm def test_shares_memory(): x = [1, 2, 3] * cm assert np.shares_memory(x, x.view(np.ndarray)) def test_nonzero(): x = [1, 2, 0] * cm res = np.nonzero(x) assert len(res) == 1 np.testing.assert_array_equal(res[0], [0, 1]) res2 = np.flatnonzero(x) np.testing.assert_array_equal(res[0], res2) def test_isinf(): x = [1, float("inf"), float("-inf")] * cm res = np.isneginf(x) np.testing.assert_array_equal(res, [False, False, True]) res = np.isposinf(x) np.testing.assert_array_equal(res, [False, True, False]) def test_allclose(): x = [1, 2, 3] * cm y = [1, 2, 3] * km assert not np.allclose(x, y) @pytest.mark.parametrize( "a, b, expected", [ ([1, 2, 3] * cm, [1, 2, 3] * km, [False] * 3), ([1, 2, 3] * cm, [1, 2, 3], [True] * 3), ([1, 2, 3] * K, [-272.15, -271.15, -270.15] * degC, [True] * 3), ], ) def test_isclose(a, b, expected): res = np.isclose(a, b) np.testing.assert_array_equal(res, expected) def test_isclose_error(): x = [1, 2, 3] * cm y = [1, 2, 3] * g with pytest.raises(UnitConversionError): np.isclose(x, y) def test_linspace(): res = np.linspace(1 * cm, 11 * cm, 10) assert type(res) is unyt_array assert res.units == cm def test_linspace_with_retstep(): res, step = np.linspace(1 * cm, 11 * cm, 10, retstep=True) assert type(res) is unyt_array assert res.units == cm assert type(step) is unyt_quantity assert step.units == cm def test_logspace_with_units_raises(): with pytest.raises( TypeError, match="The first two arguments to numpy.logspace must be dimensionless", ): np.logspace(1 * cm, 2 * cm) def test_logspace_with_base(): res = np.logspace(1, 2, base=10 * cm) assert type(res) is unyt_array assert res.units == cm def test_logspace_with_dimless_input(): res = np.logspace(1 * dimensionless, 2 * dimensionless) assert type(res) is unyt_array assert res.units == dimensionless def test_geomspace(): res = np.geomspace(1 * cm, 11 * cm, 10) assert type(res) is unyt_array assert res.units == cm def test_full_like(): x = [1, 2, 3] * cm res = np.full_like(x, 6 * cm) assert type(res) is unyt_array assert res.units == cm @pytest.mark.parametrize( "func", [ np.empty_like, np.zeros_like, np.ones_like, ], ) def test_x_like(func): x = unyt_array([1, 2, 3], cm, dtype="float32") res = func(x) assert type(res) is unyt_array assert res.units == x.units assert res.shape == x.shape assert res.dtype == x.dtype def test_copy(): x = [1, 2, 3] * cm y = np.copy(x) # by default, subok=False, so we shouldn't # expect a unyt_array without switching this arg assert type(y) is np.ndarray def test_copy_subok(): x = [1, 2, 3] * cm y = np.copy(x, subok=True) assert type(y) is unyt_array assert y.units == cm def test_copyto(): x = [1, 2, 3] * cm y = np.empty_like(x) np.copyto(y, x) assert type(y) is unyt_array assert y.units == cm np.testing.assert_array_equal(x, y) def test_copyto_edge_cases(): x = [1, 2, 3] * cm y = [1, 2, 3] * g # copying to an array with a different unit is supported # to be in line with how we treat the 'out' param in most # numpy operations np.copyto(y, x) assert type(y) is unyt_array assert y.units == cm y = np.empty_like(x.view(np.ndarray)) np.copyto(y, x) assert type(y) is np.ndarray @pytest.mark.skipif( NUMPY_VERSION < Version("2.0.0dev0"), reason="astype is new in numpy 2.0" ) def test_astype(): x = np.array([1, 2, 3], dtype="int64") * cm res = np.astype(x, "int32") assert type(res) is unyt_array assert res.units == cm def test_meshgrid(): x = [1, 2, 3] * cm y = [1, 2, 3] * s x2d, y2d = np.meshgrid(x, y) assert type(x2d) is unyt_array assert type(y2d) is unyt_array assert x2d.units == cm assert y2d.units == s @pytest.mark.parametrize( "func, args, kwargs", [ (np.transpose, (), {}), (np.reshape, ((9, 2),), {}), (np.resize, ((3, 6),), {}), (np.expand_dims, (0,), {}), (np.squeeze, (), {}), (np.swapaxes, (0, 1), {}), (np.moveaxis, (0, 2), {}), (np.rot90, (), {}), (np.roll, (3,), {}), (np.rollaxis, (2,), {}), (np.flip, (), {}), (np.fliplr, (), {}), (np.flipud, (), {}), (np.broadcast_to, ((1, 1, 2, 3, 3),), {"subok": True}), (np.delete, (0, 1), {}), (np.partition, (2,), {}), ], ) def test_reshaper(func, args, kwargs): x = [ [ [ [1, 2, 3], [4, 5, 6], [7, 8, 9], ], [ [10, 11, 12], [13, 14, 15], [16, 17, 18], ], ] ] * cm y = func(x, *args, **kwargs) assert type(y) is unyt_array assert y.units == cm def test_broadcast_arrays(): x = [1, 2, 3] * cm y = [ 4, ] * g res = np.broadcast_arrays(x, y, subok=True) assert all(type(_) is unyt_array for _ in res) @pytest.mark.parametrize( "func_name, args", [ ("split", (3, 2)), ("dsplit", (3,)), ("hsplit", (2,)), ("vsplit", (1,)), ("array_split", (3,)), pytest.param( "unstack", (), marks=pytest.mark.skipif( NUMPY_VERSION < Version("2.1.0dev0"), reason="np.unstack is new in NumPy 2.1", ), ), ], ) def test_xsplit(func_name, args): func = getattr(np, func_name) x = [ [ [ [1, 2, 3], [4, 5, 6], [7, 8, 9], ], [ [10, 11, 12], [13, 14, 15], [16, 17, 18], ], ] ] * cm y = func(x, *args) assert all(type(_) is unyt_array for _ in y) assert all(_.units == cm for _ in y) @pytest.mark.parametrize( "func, expected_units", [ (np.prod, cm**9), (np.var, cm**2), (np.std, cm), (np.nanprod, cm**9), (np.nansum, cm), (np.nanvar, cm**2), (np.nanstd, cm), (np.trace, cm), ], ) def test_scalar_reducer(func, expected_units): x = [ [1, 2, 3], [4, 5, 6], [7, 8, 9], ] * cm y = func(x) assert type(y) is unyt_quantity assert y.units == expected_units @pytest.mark.parametrize("func", [np.prod, np.nanprod]) def test_prod_with_axis(func): x = [ [1, 2, 3], [4, 5, 6], [7, 8, 9], ] * cm y = func(x, axis=0) assert type(y) is unyt_array assert y.units == cm**3 @pytest.mark.parametrize( "func", [ np.percentile, np.quantile, np.nanpercentile, np.nanquantile, ], ) def test_percentile(func): x = [ [1, 2, 3], [4, 5, 6], [7, 8, 9], ] * cm y = func(x, 1) assert type(y) is unyt_quantity assert y.units == cm @pytest.mark.parametrize( "func", [ np.diag, np.diagflat, np.diagonal, ], ) def test_diagx(func): x = [ [1, 2, 3], [4, 5, 6], [7, 8, 9], ] * cm y = func(x) assert type(y) is unyt_array assert y.units == cm def test_fix(): y = np.fix(1.2 * cm) assert y == 1.0 * cm def test_linalg_matrix_power(): x = [ [1, 2, 3], [4, 5, 6], [7, 8, 9], ] * cm y = np.linalg.matrix_power(x, 2) assert type(y) is unyt_array assert y.units == cm**2 def test_linalg_det(): x = [ [1, 2, 3], [4, 5, 6], [7, 8, 9], ] * cm y = np.linalg.det(x) assert type(y) is unyt_quantity assert y.units == cm ** (len(x)) def test_linalg_cholesky(): x = np.eye(3) * cm y = np.linalg.cholesky(x) assert type(y) is unyt_array assert y.units == cm def test_linalg_lstsq(): a = np.eye(3) * cm b = np.ones(3).T * g # setting rcond explicitly to avoid a FutureWarning # see https://numpy.org/doc/stable/reference/generated/numpy.linalg.lstsq.html x, residuals, rank, s = np.linalg.lstsq(a, b, rcond=-1) assert type(x) is unyt_array assert x.units == g / cm assert type(residuals) is unyt_array assert residuals.units == g / cm assert type(s) is unyt_array assert s.units == cm def test_linalg_multi_dot(): a = np.eye(3) * cm b = np.eye(3) * g c = np.eye(3) * s res = np.linalg.multi_dot([a, b, c]) assert type(res) is unyt_array assert res.units == cm * g * s def test_linalg_qr(): x = [ [1, 2, 3], [4, 5, 6], [7, 8, 9], ] * cm q, r = np.linalg.qr(x) assert type(q) is unyt_array assert q.units == cm assert type(r) is unyt_array assert r.units == cm @pytest.mark.parametrize("func", [np.linalg.solve, np.linalg.tensorsolve]) def test_linalg_solve(func): a = np.eye(3) * cm b = np.ones(3).T * g x = func(a, b) assert type(x) is unyt_array assert x.units == g / cm def is_any_dimless(x) -> bool: return (not hasattr(x, "units")) or x.units.is_dimensionles def test_linalg_cond(): a = np.eye(3) * cm res = np.linalg.cond(a) assert is_any_dimless(res) @pytest.mark.parametrize("func", [np.linalg.eig, np.linalg.eigh]) def test_eig(func): a = np.eye(3) * cm w, v = func(a) assert type(w) is unyt_array assert w.units == cm assert is_any_dimless(v) @pytest.mark.parametrize("func", [np.linalg.eigvals, np.linalg.eigvalsh]) def test_eigvals(func): a = np.eye(3) * cm w = func(a) assert type(w) is unyt_array assert w.units == cm def test_savetxt(tmp_path): a = [1, 2, 3] * cm with pytest.warns( UserWarning, match=re.escape( "numpy.savetxt does not preserve units, " "and will only save the raw numerical data from the unyt_array object.\n" "If this is the intended behaviour, call `numpy.savetxt(file, arr.d)` " "to silence this warning.\n" "If you want to preserve units, use `unyt.savetxt` " "(and `unyt.loadtxt`) instead." ), ): np.savetxt(tmp_path / "savefile.npy", a) # check that doing what the warning says doesn't trigger any other warning np.savetxt(tmp_path / "savefile.npy", a.d) def test_apply_along_axis(): a = np.eye(3) * cm ret = np.apply_along_axis(lambda x: x * cm, 0, a) assert type(ret) is unyt_array assert ret.units == cm**2 @pytest.mark.parametrize("axes, expected_units", [((0, 1), cm**4), ((0,), cm**2)]) def test_apply_over_axes(axes, expected_units): # the user-supplied function must be trusted to treat units # sensibly (mainly that it doesn't give a mix of units across # the resulting array), but we can check that units are # propagated correctly for well-behaved functions. a = np.eye(3) * cm ret = np.apply_over_axes(lambda x, axis: x[axis] ** 2, a, axes) assert isinstance(ret, unyt_array) # could be unyt_quantity assert ret.units == expected_units def test_array_equal(): a = [1, 2, 3] * cm b = [1, 2, 3] * cm c = [1, 2, 3] * km assert np.array_equal(a, b) assert not np.array_equal(a, c) def test_array_equiv(): a = [1, 2, 3] * cm b = [1, 2, 3] * cm c = [1, 2, 3] * km d = [[1, 2, 3]] * cm assert np.array_equiv(a, b) assert np.array_equiv(a, d) assert not np.array_equiv(a, c) def test_common_type(): a = np.array([1, 2, 3], dtype="float32") * cm b = np.array([1, 2, 3], dtype="float64") * cm dtype = np.common_type(a, b) assert dtype == np.dtype("float64") def test_result_type(): scalar = 3 * cm array = np.arange(7, dtype="i1") if NUMPY_VERSION >= Version("2.0.0dev0"): # promotion rules vary under NEP 50. The default behaviour is different # in numpy 2.0 VS numpy 1.x # see https://github.com/numpy/numpy/pull/23912 # see https://numpy.org/neps/nep-0050-scalar-promotion.html expected_dtype = scalar.dtype else: expected_dtype = array.dtype assert np.result_type(scalar, array) == expected_dtype @pytest.mark.parametrize( "func", [ np.diff, np.ediff1d, np.gradient, np.ptp, ], ) @pytest.mark.parametrize("input_units, output_units", [(cm, cm), (K, delta_degC)]) def test_deltas(func, input_units, output_units): x = np.arange(0, 4) * input_units res = func(x) assert isinstance(res, unyt_array) assert res.units == output_units @pytest.mark.parametrize( "func_name", [ "cumsum", "nancumsum", pytest.param( "cumulative_sum", marks=pytest.mark.skipif( NUMPY_VERSION < Version("2.1.0dev0"), reason="np.cumulative_sum is new in NumPy 2.1", ), ), ], ) def test_cumsum(func_name): a = [1, 2, 3] * cm func = getattr(np, func_name) res = func(a) assert type(res) is unyt_array assert res.units == cm @pytest.mark.parametrize( "func_name", [ "cumprod", "nancumprod", pytest.param( "cumulative_prod", marks=pytest.mark.skipif( NUMPY_VERSION < Version("2.1.0dev0"), reason="np.cumulative_prod is new in NumPy 2.1", ), ), ], ) def test_cumprod(func_name): a = [1, 2, 3] * cm func = getattr(np, func_name) with pytest.raises( UnytError, match=re.escape( r"numpy.cumprod (and other cumulative product function) cannot be used " r"with a unyt_array as all return elements should (but cannot) " r"have different units." ), ): func(a) def test_bincount(): a = [1, 2, 3] * cm res = np.bincount(a) assert type(res) is np.ndarray def test_unique(): a = [1, 2, 3] * cm res = np.unique(a) assert type(res) is unyt_array assert res.units == cm @pytest.mark.skipif( NUMPY_VERSION < Version("2.0.0dev0"), reason="unique_all is new in numpy 2.0" ) def test_unique_all(): q = np.arange(9).reshape(3, 3) * cm values, indices, inverse_indices, counts = np.unique( q, return_index=True, return_inverse=True, return_counts=True, equal_nan=False, ) res = np.unique_all(q) assert len(res) == 4 assert_array_equal_units(res.values, values) assert_array_equal_units(res.indices, indices) assert_array_equal_units(res.inverse_indices, inverse_indices) assert_array_equal_units(res.counts, counts) @pytest.mark.skipif( NUMPY_VERSION < Version("2.0.0dev0"), reason="unique_counts is new in numpy 2.0" ) def test_unique_counts(): q = np.arange(9).reshape(3, 3) * cm values, counts = np.unique( q, return_counts=True, equal_nan=False, ) res = np.unique_counts(q) assert len(res) == 2 assert_array_equal_units(res.values, values) assert_array_equal_units(res.counts, counts) @pytest.mark.skipif( NUMPY_VERSION < Version("2.0.0dev0"), reason="unique_inverse is new in numpy 2.0" ) def test_unique_inverse(): q = np.arange(9).reshape(3, 3) * cm values, inverse_indices = np.unique( q, return_inverse=True, equal_nan=False, ) res = np.unique_inverse(q) assert len(res) == 2 assert_array_equal_units(res.values, values) assert_array_equal_units(res.inverse_indices, inverse_indices) @pytest.mark.skipif( NUMPY_VERSION < Version("2.0.0dev0"), reason="unique_values is new in numpy 2.0" ) def test_unique_values(): q = np.arange(9).reshape(3, 3) * cm values = np.unique(q, equal_nan=False) res = np.unique_values(q) # np.unique_values' output is not guaranteed to be sorted, # so exact results may differ. # see https://github.com/numpy/numpy/issues/28493#issuecomment-2721303048 res.sort() assert_array_equal_units(res, values) @pytest.mark.parametrize("indices", [[0, 1], 0]) def test_take(indices): a = [1, 2, 3] * cm res = np.take(a, indices) if res.ndim == 0: assert type(res) is unyt_quantity else: assert type(res) is unyt_array assert res.units == cm @pytest.mark.parametrize("indices", [[0, 1], 0]) def test_ndarray_take(indices): a = [1, 2, 3] * cm res = a.take(indices) if res.ndim == 0: assert type(res) is unyt_quantity else: assert type(res) is unyt_array assert res.units == cm def test_pad(): a = [1, 2, 3] * cm res = np.pad(a, [0, 1]) assert type(res) is unyt_array assert res.units == cm def test_sinc(): a = [1, 2, 3] * cm res = np.sinc(a) # we *want* this one to ignore units assert type(res) is np.ndarray def test_choose_mixed_units(): choices = [[1, 2, 3] * cm, [4, 5, 6] * km] with pytest.raises(UnitInconsistencyError): np.choose([1, 0, 1], choices=choices) def test_choose(): choices = [[1, 2, 3] * cm, [4, 5, 6] * cm] res = np.choose([1, 0, 1], choices=choices) assert type(res) is unyt_array assert res.units == cm def test_extract(): a = [1, 2, 3] * cm res = np.extract(a > 1 * cm, a) assert type(res) is unyt_array assert res.units == cm def test_fill_diagonal_mixed_units(): a = np.zeros(9).reshape((3, 3)) * cm with pytest.raises(UnitInconsistencyError): np.fill_diagonal(a, 1 * km) @pytest.mark.parametrize("val", [1 * cm, 1]) def test_fill_diagonal(val): a = np.zeros(9).reshape((3, 3)) * cm np.fill_diagonal(a, val) assert type(a) is unyt_array assert a.units == cm def test_insert_mixed_units(): a = [1, 2, 3] * cm with pytest.raises(UnitInconsistencyError): np.insert(a, 1, 42 * km) @pytest.mark.parametrize("val", [42, 42 * cm]) def test_insert(val): a = [1, 2, 3] * cm res = np.insert(a, 1, val) assert type(res) is unyt_array assert res.units == cm def test_isin_mixed_units(): a = [1, 2, 3] * cm with pytest.raises(UnitInconsistencyError): np.isin(1, a) def test_isin(): a = [1, 2, 3] * cm assert np.isin(1 * cm, a) @pytest.mark.filterwarnings("ignore:`in1d` is deprecated. Use `np.isin` instead.") def test_in1d_mixed_units(): a = [1, 2, 3] * cm with pytest.raises(UnitInconsistencyError): np.in1d([1, 2], a) # noqa: NPY201 @pytest.mark.filterwarnings("ignore:`in1d` is deprecated. Use `np.isin` instead.") def test_in1d(): a = [1, 2, 3] * cm b = [1, 2] * cm assert np.all(np.in1d(b, a)) # noqa: NPY201 def test_place_mixed_units(): arr = np.arange(6).reshape(2, 3) * cm with pytest.raises(UnitInconsistencyError): np.place(arr, arr > 2, [44, 55]) def test_place(): arr = np.arange(6).reshape(2, 3) * cm np.place(arr, arr > 2, [44, 55] * cm) assert type(arr) is unyt_array assert arr.units == cm def test_put_mixed_units(): arr = np.arange(6).reshape(2, 3) * cm with pytest.raises(UnitInconsistencyError): np.put(arr, [1, 2], [44, 55]) def test_put(): arr = np.arange(6).reshape(2, 3) * cm np.put(arr, [1, 2], [44, 55] * cm) assert type(arr) is unyt_array assert arr.units == cm def test_put_along_axis_mixed_units(): arr = np.arange(6).reshape(2, 3) * cm with pytest.raises(UnitInconsistencyError): np.put_along_axis(arr, np.array([[1, 2], [0, 1]]), [44, 55], 1) def test_put_along_axis(): arr = np.arange(6).reshape(2, 3) * cm np.put_along_axis(arr, np.array([[1, 2], [0, 1]]), [44, 55] * cm, 1) assert type(arr) is unyt_array assert arr.units == cm def test_putmask_mixed_units(): arr = np.arange(6, dtype=np.int_).reshape(2, 3) * cm with pytest.raises(UnitInconsistencyError): np.putmask(arr, arr > 2 * cm, np.zeros_like(arr.d)) def test_putmask(): arr = np.arange(6, dtype=np.int_).reshape(2, 3) * cm np.putmask(arr, arr > 2 * cm, np.zeros_like(arr)) assert type(arr) is unyt_array assert arr.units == cm def test_searchsorted_mixed_units(): with pytest.raises(UnitInconsistencyError): np.searchsorted([1, 2, 3, 4, 5] * cm, 3 * km) @pytest.mark.parametrize("val", [3 * cm, 3]) def test_searchsorted(val): res = np.searchsorted([1, 2, 3, 4, 5] * cm, val) assert res == 2 @pytest.mark.parametrize( "choicelist_gen, default", [ ([lambda x: x**3, lambda x: x**2], 34 * cm), # invalid choicelist ([lambda x: x, lambda x: x + 3 * x.units], 34), # invalid default ], ) def test_select_mixed_units(choicelist_gen, default): a = [1, 2, 3, 4, 5, 6] * cm with pytest.raises(UnitInconsistencyError): np.select( [a > 3 * cm, a < 3 * cm], [f(a) for f in choicelist_gen], default=default ) def test_select(): a = [1, 2, 3, 4, 5, 6] * cm res = np.select([a > 3 * cm, a < 3 * cm], [a, a + 3 * cm], default=34 * cm) assert_array_equal_units(res, [4, 5, 34, 4, 5, 6] * cm) def test_setdiff1d_mixed_units(): a = [1, 2, 3] * cm b = [0, 1, 2] with pytest.raises(UnitInconsistencyError): np.setdiff1d(a, b) def test_setdiff1d(): a = [1, 2, 3] * cm b = [0, 1, 2] * cm res = np.setdiff1d(a, b) assert_array_equal_units(res, [3] * cm) def test_setxor1d_mixed_units(): a = [1, 2, 3] * cm b = [0, 1, 2] with pytest.raises(UnitInconsistencyError): np.setxor1d(a, b) def test_setxor1d(): a = [1, 2, 3] * cm b = [0, 1, 2] * cm res = np.setxor1d(a, b) assert_array_equal_units(res, [0, 3] * cm) def test_clip_mixed_units(): a = [1, 2, 3, 4, 5, 6] * cm with pytest.raises(UnitInconsistencyError): np.clip(a, 3 * cm, 4) @pytest.mark.parametrize("vmin,vmax", [(3 * cm, 4 * cm), (3, 4)]) def test_clip(vmin, vmax): a = [1, 2, 3, 4, 5, 6] * cm res = np.clip(a, vmin, vmax) assert_array_equal_units(res, [3, 3, 3, 4, 4, 4] * cm) def test_where_mixed_units(): x = [-1, 2, -3] * cm y = [0, 0, 0] with pytest.raises(UnitInconsistencyError): np.where(x > y, x, y) def test_where_single_arg(): x = [0, 2, -1, 0, 1] * cm res = np.where(x) assert isinstance(res, tuple) assert len(res) == 1 assert type(res[0]) is np.ndarray np.testing.assert_array_equal(res[0], [1, 2, 4]) def test_where_xy(): x = [-1, 2, -3] * cm y = [0, 0, 0] * cm res = np.where(x > y, x, y) assert type(res) is unyt_array assert res.units == cm @pytest.mark.parametrize( "func", [ np.imag, np.real, np.real_if_close, ], ) def test_complex_reductions(func): a = [1 + 1j for _ in range(3)] * A res = func(a) assert type(res) is unyt_array assert res.units == A @pytest.mark.parametrize( "func", [np.tril, np.triu], ) def test_triangles(func): a = np.eye(4) * cm res = func(a) assert type(res) is unyt_array assert res.units == cm def test_einsum(): a = np.eye(4) * cm # extract diagonal res = np.einsum("ii->i", a) assert type(res) is unyt_array assert res.units == cm # sum diagonal elements, the result should be a scalar res = np.einsum("ii", a) assert type(res) is unyt_quantity assert res.units == cm def test_convolve(): a = [1, 2, 3] * cm v = [4, 5, 6] * s res = np.convolve(a, v) assert type(res) is unyt_array assert res.units == cm * s def test_correlate(): a = [1, 2, 3] * cm v = [4, 5, 6] * s res = np.correlate(a, v) assert type(res) is unyt_array assert res.units == cm * s def test_tensordot(): a = np.arange(60.0).reshape(3, 4, 5) * cm b = np.arange(24.0).reshape(4, 3, 2) * s res = np.tensordot(a, b, axes=([1, 0], [0, 1])) assert type(res) is unyt_array assert res.units == cm * s def test_compress(): a = [1, 2, 3] * cm res = np.compress(a > 1, a) assert type(res) is unyt_array assert res.units == cm np.compress(a > 1, a, out=res) assert type(res) is unyt_array assert res.units == cm np.compress(a > 1, a, out=res.view(np.ndarray)) assert type(res) is unyt_array assert res.units == cm def test_take_along_axis(): a = np.array([[10, 30, 20], [60, 40, 50]]) * cm ai = np.argsort(a, axis=1) res = np.take_along_axis(a, ai, axis=1) assert type(res) is unyt_array assert res.units == cm def test_unwrap(): phase = np.linspace(0, np.pi, num=5) * rad phase[3:] += np.pi * rad res = np.unwrap(phase) assert type(res) is unyt_array assert res.units == rad def test_interp(): _x = np.array([1.1, 2.2, 3.3]) _xp = np.array([1, 2, 3]) _fp = np.array([4, 8, 12]) # any of the three input array-like might be unitful # let's test all relevant combinations # return type should match fp's with pytest.raises(UnitInconsistencyError): np.interp(_x * cm, _xp, _fp) with pytest.raises(UnitInconsistencyError): res = np.interp(_x, _xp * cm, _fp) res = np.interp(_x * cm, _xp * cm, _fp) assert type(res) is np.ndarray res = np.interp(_x, _xp, _fp * K) assert type(res) is unyt_array assert res.units == K res = np.interp(_x * cm, _xp * cm, _fp * K) assert type(res) is unyt_array assert res.units == K @pytest.mark.parametrize( "target, helper", sorted( HANDLED_FUNCTIONS.items(), key=lambda items: items[0].__name__, ), ids=lambda func: func.__name__, ) class TestFunctionHelpersSignatureCompatibility: """ Check that a helper function's signature is *at least* as flexible as the helped (target) function's. E.g., any argument that is allowed positionally, or as keyword, by the target must be re-exposed *somehow* by the helper. We explicitly allow helper's signature to be *more* flexible than the target signature by allowing *args and **kwargs catch-all arguments, which we use to limit code duplication, and also help with forward and backward compatibility. See https://github.com/astropy/astropy/issues/15703 """ # this test class is adapted from astropy.units @staticmethod def have_catchall_argument(parameters, kind) -> bool: return any(p.kind is kind for p in parameters.values()) @staticmethod def get_param_group(parameters, kinds: list) -> list[str]: return [name for name, p in parameters.items() if p.kind in kinds] def test_all_arguments_reexposed(self, target, helper): try: sig_target = inspect.signature(target) except ValueError: pytest.skip("Non Python function cannot be inspected at runtime") params_target = sig_target.parameters sig_helper = inspect.signature(helper) params_helper = sig_helper.parameters have_args_helper = self.have_catchall_argument(params_helper, VAR_POSITIONAL) have_kwargs_helper = self.have_catchall_argument(params_helper, VAR_KEYWORD) args_helper = list(params_helper.items()) pos_helper = 0 for nt, pt in params_target.items(): kt = pt.kind if kt in (POSITIONAL_ONLY, POSITIONAL_OR_KEYWORD): assert pos_helper < len(args_helper), ( "helper's signature is too short; " "some arguments are not properly re-exposed" ) nh, ph = args_helper[pos_helper] if (kh := ph.kind) is not VAR_POSITIONAL: assert nh == nt, f"argument {nt!r} isn't re-exposed as positional" assert kh is kt, ( f"helper is not re-exposing argument {nt!r} properly:" f"expected {kt}, got {kh}" ) pos_helper += 1 continue if kt in (KEYWORD_ONLY, POSITIONAL_OR_KEYWORD): if nt in params_helper: kh = params_helper[nt].kind assert kh is kt, ( f"helper is not re-exposing argument {nt!r} properly: " f"expected {kt}, got {kh}" ) elif kt is KEYWORD_ONLY: assert have_kwargs_helper, ( f"argument {nt!r} is not re-exposed as keyword" ) elif kt is POSITIONAL_OR_KEYWORD: assert have_args_helper and have_kwargs_helper, ( f"argument {nt!r} is not re-exposed as positional-or-keyword" ) elif kt is VAR_POSITIONAL: assert have_args_helper, "helper is missing a catch-all *args argument" elif kt is VAR_KEYWORD: assert have_kwargs_helper, ( "helper is missing a catch-all **kwargs argument" ) def test_known_arguments(self, target, helper): # validate that all exposed arguments map to something in the target try: sig_target = inspect.signature(target) except ValueError: pytest.skip("Non Python function cannot be inspected at runtime") params_target = sig_target.parameters sig_helper = inspect.signature(helper) params_helper = sig_helper.parameters for kind in (POSITIONAL_ONLY, POSITIONAL_OR_KEYWORD): args_target = self.get_param_group(params_helper, [kind]) args_helper = self.get_param_group(params_helper, [kind]) if (nhelper := len(args_helper)) > (ntarget := len(args_target)): unknown: list[str] = args_helper[ntarget:] raise AssertionError( f"Found unknown {kind} parameter(s) " "in helper's signature: " f"{unknown}, at position(s) {list(range(ntarget, nhelper))}" ) # keyword-allowed keyword_allowed_target = set( self.get_param_group(params_target, [KEYWORD_ONLY, POSITIONAL_OR_KEYWORD]) ) keyword_allowed_helper = set( self.get_param_group(params_helper, [KEYWORD_ONLY, POSITIONAL_OR_KEYWORD]) ) # additional private keyword-only argument are allowed because # they are only intended for testing purposes. # For instance, quantile has such a parameter '_q_unit' keyword_allowed_helper = { name for name in keyword_allowed_helper if not name.startswith("_") } diff = keyword_allowed_helper - keyword_allowed_target assert not diff, ( "Found some keyword-allowed parameters in helper " f"that are unknown to target: {diff}" ) # finally, check that default values are correctly replicated for name, ph in params_helper.items(): if name not in params_target: # In a few cases, the helper defines names that are not in # the target (e.g., a private name like _q_unit in quantile, # or a *args, **kwargs that captures further arguments # that do not matter. We let such cases slip by. continue pt = params_target[name] assert ph.default == pt.default, ( f"Default value mismatch for argument {name!r}. " f"Helper has {ph.default!r}, target has {pt.default!r}" ) unyt-3.0.4/unyt/tests/test_dask_arrays.py000066400000000000000000000272471476461141700206210ustar00rootroot00000000000000import pickle from collections import defaultdict import numpy as np import pytest from numpy.testing import assert_array_equal from unyt import unyt_array, unyt_quantity from unyt._on_demand_imports import _dask as dask if not dask.__is_available__: pytest.skip("dask isn't installed", allow_module_level=True) from unyt.dask_array import ( _create_with_quantity, _use_unary_decorator, reduce_with_units, unyt_dask_array, unyt_from_dask, ) from unyt.exceptions import UnitOperationError from unyt.unit_symbols import cm, g, m def test_unyt_dask_creation(): x = dask.array.ones((10, 10), chunks=(2, 2)) x_da = unyt_from_dask(x, m) assert type(x_da) is unyt_dask_array assert x_da.units == m assert type(x_da.compute()) is unyt_array x_da = _create_with_quantity(x, unyt_quantity(1, m)) assert type(x_da) is unyt_dask_array assert x_da.units == m assert type(x_da.compute()) is unyt_array x_dask = x_da.to_dask() assert_array_equal(x.compute(), x_dask.compute()) def test_unyt_dask_slice(): # tests __getitem__ x = dask.array.ones((10, 10), chunks=(2, 2)) x_da = unyt_from_dask(x, m) slc = x_da[:, 0] assert slc.units == m assert slc.compute().units == m assert type(slc.compute()) is unyt_array def test_unyt_set(): # tests __setitem__ x = dask.array.ones((10, 10), chunks=(2, 2)) x_da = unyt_from_dask(x, m) x_da[:, 0] = 3.0 assert (x_da[:, 0].compute() == 3.0).sum() == 10 def test_unit_conversions(): x = dask.array.ones((10, 10), chunks=(2, 2)) x_da = unyt_from_dask(x, m) x_da = x_da.to(cm) assert type(x_da) is unyt_dask_array assert x_da.units == cm assert x_da.compute().units == cm assert x_da[0, 0].compute().value == 100 x_da_2 = unyt_from_dask(x, m) result = x_da + x_da_2 assert type(result) is unyt_dask_array assert result.units == m assert result.compute().units == m x_da_2 = unyt_from_dask(x, "g") with pytest.raises(UnitOperationError): x_da + x_da_2 def test_conversion_to_dask(): x = dask.array.ones((10, 10), chunks=(2, 2)) x_da = unyt_from_dask(x, m) x_again = x_da.to_dask() assert_array_equal(x.compute(), x_again.compute()) assert type(x_again) is type(x) result = np.isfinite(x_da) # should return plain dask array assert type(result) is dask.array.core.Array def unary_test(the_func, unyt_dask_obj, unyt_array_in, *args, **kwargs): # the_func can be a numpy ufunc or dask.array numpy ufunc implementation result_delay = the_func(unyt_dask_obj, *args, **kwargs) correct_unyt = the_func(unyt_array_in, *args, **kwargs) assert result_delay.units == correct_unyt.units # units should already match unary_result_test(result_delay, correct_unyt) def unary_result_test(dask_unyt_delayed, correct_unyt): # computes a delayed dask_unyt_array and compares resulting units and values result = dask_unyt_delayed.compute() assert result.units == correct_unyt.units assert type(result) is type(correct_unyt) # value comparison: if type(result) is unyt_array: assert_array_equal(result, correct_unyt) else: assert result == correct_unyt def test_unary(): x = dask.array.ones((10, 10), chunks=(2, 2)) x_da = unyt_from_dask(x, m) x_unyt = unyt_array(np.ones((10, 10)), m) for unary_op in [np.sqrt, dask.array.sqrt]: unary_test(unary_op, x_da, x_unyt) # some of the daskified ufunc attributes need to be called directly unary_result_test(x_da.sum(), x_unyt.sum()) unary_result_test(x_da.min(), x_unyt.min()) unary_result_test(x_da.max(), x_unyt.max()) unary_result_test(x_da.mean(), x_unyt.mean()) unary_result_test(x_da.std(), x_unyt.std()) unary_result_test(x_da.cumsum(0), x_unyt.cumsum(0)) unary_result_test(abs(x_da), abs(x_unyt)) # __abs__ @pytest.mark.parametrize( "logical_op", ["__lt__", "__le__", "__gt__", "__ge__", "__eq__", "__ne__"] ) @pytest.mark.parametrize("other", [None, unyt_quantity(2, m)]) def test_logical(logical_op, other): def check_operator(arg1, arg2): # comparisons should return plain dask arrays func = getattr(arg1, logical_op) result = func(arg2) assert type(result) is dask.array.Array x = dask.array.ones((10, 10), chunks=(2, 2)) x_da = unyt_from_dask(x, m) if other is None: other = 2 * x_da # test another unyt-dask array check_operator(x_da, other) check_operator(other, x_da) def test_binary(): x = dask.array.ones((10, 10), chunks=(2, 2)) x2 = dask.array.full((10, 10), 2, chunks=(2, 2)) x_da = unyt_from_dask(x, m) x_da_2 = unyt_from_dask(x2, g) # multiplications result = x_da * x_da_2 assert result.units == m * g result = x_da_2 * x_da assert result.units == m * g result = x_da_2 * 2 assert result.units == g result = 2 * x_da_2 assert result.units == g result = x_da_2 * unyt_quantity(2, "m") assert result.units == m * g result = unyt_quantity(2, "m") * x_da_2 assert result.units == m * g # divisions result = x_da_2 / x_da assert result.units == g / m result = x_da / x_da_2 assert result.units == m / g result = x_da_2 / 2 assert result.units == g result = 2 / x_da_2 # __rtruediv__ assert result.units == g**-1 result = x_da_2 / unyt_quantity(2, "m") assert result.units == g / m result = unyt_quantity(2, "m") / x_da_2 assert result.units == m / g result = x_da**2 assert result.units == m * m def test_addition(): x = dask.array.ones((10, 10), chunks=(2, 2)) x2 = dask.array.full((10, 10), 2, chunks=(2, 2)) x_da = unyt_from_dask(x, m) # two unyt_dask_array objects, any order, any units with same dimension x_da_2 = unyt_from_dask(x2, m) result = x_da + x_da_2 assert result.units == m result = x_da_2 + x_da assert result.units == m x_da_3 = unyt_from_dask(x2, "cm") result = x_da + x_da_3 assert result.units == m result = x_da_3 + x_da assert result.units == m # one unyt_dask_array, one unyt_quantity, any order, any units with same dim result = x_da + unyt_quantity(1, "m") assert result.units == m result = unyt_quantity(1, "m") + x_da assert result.units == m result = unyt_quantity(100, "cm") + x_da # test same dimensions assert result.units == m assert result.max().compute() == unyt_quantity(2, "m") result = x_da + unyt_quantity(100, "cm") # test same dimensions assert result.units == m assert result.max().compute() == unyt_quantity(200, "cm") def test_subtraction(): x = dask.array.ones((10, 10), chunks=(2, 2)) x2 = dask.array.full((10, 10), 2, chunks=(2, 2)) x_da = unyt_from_dask(x, m) # two unyt_dask_array objects, any order, any units with same dimension x_da_2 = unyt_from_dask(x2, m) result = x_da - x_da_2 assert result.units == m result = x_da_2 - x_da assert result.units == m x_da_3 = unyt_from_dask(x2, "cm") result = x_da - x_da_3 assert result.units == m result = x_da_3 - x_da assert result.units == m # one unyt_dask_array, one unyt_quantity, any order, any units with same dim result = x_da - unyt_quantity(1, "m") assert result.units == m result = unyt_quantity(1, "m") - x_da assert result.units == m result = unyt_quantity(100, "cm") - x_da # test same dimensions assert result.units == m assert result.max().compute() == unyt_quantity(0, "m") result = x_da - unyt_quantity(100, "cm") # test same dimensions assert result.units == m assert result.max().compute() == unyt_quantity(0, "cm") def test_unyt_type_result(): # test that the return type of a compute is unyt_array or unyt_quantity when # appropriate x = dask.array.ones((10, 10), chunks=(2, 2)) x_da = unyt_from_dask(x, m) x_unyt = unyt_array(np.ones((10, 10)), m) result = x_da.compute() assert type(result) is unyt_array assert_array_equal(result, x_unyt) result = x_da.min().compute() assert type(result) is unyt_quantity assert result == unyt_quantity(1, m) _func_args = defaultdict(lambda: ()) _func_args["reshape"] = ((100, 1),) _func_args["rechunk"] = ((5, 5),) _func_args["cumsum"] = (0,) _func_args["clip"] = (0, 2) _func_args["swapaxes"] = (0, 1) _func_args["repeat"] = (1, 0) _func_args["astype"] = (int,) _func_args["topk"] = (1,) @pytest.mark.parametrize( ("da_func", "args"), [(f, _func_args[f]) for f in _use_unary_decorator] ) def test_dask_passthroughs(da_func, args): # tests the array class functions that do not modify units x = dask.array.ones((10, 10), chunks=(2, 2)) x_da = unyt_from_dask(x, m) assert getattr(x_da, da_func)(*args).units == m def test_repr(): x = dask.array.ones((10, 10), chunks=(2, 2)) x_da = unyt_from_dask(x, m) assert "unyt_dask_array" in x_da.__repr__() table_str = x_da._repr_html_() assert "Units" in table_str @pytest.mark.parametrize( ("dask_func_str", "actual", "axis", "check_nan"), [ ("mean", unyt_quantity(1.0, "m"), None, True), ("min", unyt_quantity(1.0, "m"), None, True), ("max", unyt_quantity(1.0, "m"), None, True), ("std", unyt_quantity(0.0, "m"), None, True), ( "median", unyt_quantity(1.0, "m"), 1, True, ), # median requires an axis for dask ("var", unyt_quantity(0.0, "m**2"), None, True), ("sum", unyt_quantity(100.0, "m"), None, True), ( "cumsum", unyt_array(np.ones((10, 10)), "m").cumsum(axis=1), 1, True, ), ("average", unyt_quantity(1.0, "m"), None, False), ("diagonal", unyt_array(np.ones((10,)), "m"), None, False), ("unique", unyt_array([1.0], "m"), None, False), ], ) def test_dask_array_reductions(dask_func_str, actual, axis, check_nan): extra_kwargs = {} if axis is not None: extra_kwargs["axis"] = axis func_strs = [ dask_func_str, ] if check_nan: func_strs.append("nan" + dask_func_str) for func_str in func_strs: dask_func = getattr(dask.array, func_str) x_da = unyt_from_dask(dask.array.ones((10, 10), chunks=(2, 2)), m) result = reduce_with_units(dask_func, x_da, **extra_kwargs).compute() assert_array_equal(result, actual) def test_bad_dask_array_reductions(): x_da = unyt_from_dask(dask.array.ones((10, 10), chunks=(2, 2)), m) with pytest.raises( ValueError, match="could not deduce np equivalent of dask reduction" ): reduce_with_units(lambda: None, x_da).compute() def test_prod(): x_da = unyt_from_dask(dask.array.ones((10, 10), chunks=(2, 2)), m) assert x_da.prod().units == m**100 def test_pickle(): x_da = unyt_from_dask(dask.array.ones((10, 10), chunks=(2, 2)), m) x_da2 = pickle.loads(pickle.dumps(x_da)) assert_array_equal(x_da, x_da2) assert x_da.units == x_da2.units def test_np_array_funcs(): # test some numpy array functions to catch __array_function__ x_da = unyt_from_dask(dask.array.ones((10, 10), chunks=(2, 2)), m) actual = np.sum(x_da).compute() assert actual == unyt_quantity(100, m) plain_dask = dask.array.arange(100, chunks=(20,)) x_da = unyt_from_dask(plain_dask, m) actual = np.mean(x_da).compute() expected = unyt_quantity(np.mean(plain_dask).compute(), m) assert actual == expected actual = np.std(x_da[x_da > 10.0]).compute() expected = unyt_quantity(np.std(plain_dask[plain_dask > 10]).compute(), m) assert actual == expected actual = np.max(np.power(x_da, 2)).compute() expected = unyt_quantity(np.max(np.power(plain_dask, 2)).compute(), m * m) assert actual == expected unyt-3.0.4/unyt/tests/test_define_unit.py000066400000000000000000000023331476461141700205740ustar00rootroot00000000000000import pytest from unyt.array import unyt_quantity from unyt.unit_object import define_unit from unyt.unit_registry import UnitRegistry def test_define_unit(): define_unit("kph", (1.0, "km/hr")) a = unyt_quantity(2.0, "kph") b = unyt_quantity(1.0, "km") c = unyt_quantity(1.0, "hr") assert a == 2.0 * b / c d = unyt_quantity(1000.0, "cm**3") define_unit("Baz", d, prefixable=True) e = unyt_quantity(1.0, "mBaz") f = unyt_quantity(1.0, "cm**3") assert e == f define_unit("Foo", (1.0, "V/sqrt(s)")) g = unyt_quantity(1.0, "Foo") volt = unyt_quantity(1.0, "V") second = unyt_quantity(1.0, "s") assert g == volt / second ** (0.5) # Test custom registry reg = UnitRegistry() define_unit("Foo", (1, "m"), registry=reg) define_unit("Baz", (1, "Foo**2"), registry=reg) h = unyt_quantity(1, "Baz", registry=reg) i = unyt_quantity(1, "m**2", registry=reg) assert h == i def test_define_unit_error(): from unyt import define_unit with pytest.raises(RuntimeError): define_unit("foobar", "baz") with pytest.raises(RuntimeError): define_unit("foobar", 12) with pytest.raises(RuntimeError): define_unit("C", (1.0, "A*s")) unyt-3.0.4/unyt/tests/test_mpl_interface.py000066400000000000000000000162361476461141700211220ustar00rootroot00000000000000"""Test Matplotlib ConversionInterface""" import numpy as np import pytest from unyt import K, m, matplotlib_support, s, unyt_array, unyt_quantity from unyt._on_demand_imports import NotAModule, _matplotlib try: from unyt._mpl_array_converter import unyt_arrayConverter except ImportError: pass check_matplotlib = pytest.mark.skipif( isinstance(_matplotlib.pyplot, NotAModule), reason="matplotlib not installed" ) @pytest.fixture def ax(): _matplotlib.use("agg") matplotlib_support.enable() matplotlib_support.label_style = "()" fig, ax = _matplotlib.pyplot.subplots() yield ax _matplotlib.pyplot.close() matplotlib_support.disable() @check_matplotlib def test_label(ax): x = [0, 1, 2] * s y = [3, 4, 5] * K matplotlib_support.label_style = "()" ax.plot(x, y) expected_xlabel = "$\\left(\\rm{s}\\right)$" assert ax.xaxis.get_label().get_text() == expected_xlabel expected_ylabel = "$\\left(\\rm{K}\\right)$" assert ax.yaxis.get_label().get_text() == expected_ylabel _matplotlib.pyplot.close() @check_matplotlib def test_convert_unit(ax): x = [0, 1, 2] * s y = [1000, 2000, 3000] * K ax.plot(x, y, yunits="Celsius") expected = y.to("Celsius") line = ax.lines[0] original_y_array = line.get_data()[1] converted_y_array = line.convert_yunits(original_y_array) results = converted_y_array == expected assert results.all() @check_matplotlib def test_convert_equivalency(ax): x = [0, 1, 2] * s y = [1000, 2000, 3000] * K ax.clear() ax.plot(x, y, yunits=("J", "thermal")) expected = y.to("J", "thermal") line = ax.lines[0] original_y_array = line.get_data()[1] converted_y_array = line.convert_yunits(original_y_array) results = converted_y_array == expected assert results.all() @check_matplotlib def test_dimensionless(ax): x = [0, 1, 2] * s y = [3, 4, 5] * K / K ax.plot(x, y) expected_ylabel = "" assert ax.yaxis.get_label().get_text() == expected_ylabel @check_matplotlib def test_conversionerror(ax): x = [0, 1, 2] * s y = [3, 4, 5] * K ax.plot(x, y) ax.xaxis.callbacks.exception_handler = None with pytest.raises(_matplotlib.units.ConversionError): ax.xaxis.set_units("V") @check_matplotlib def test_ndarray_label(ax): x = [0, 1, 2] * s y = np.arange(3, 6) matplotlib_support.label_style = "()" ax.plot(x, y) expected_xlabel = "$\\left(\\rm{s}\\right)$" assert ax.xaxis.get_label().get_text() == expected_xlabel expected_ylabel = "" assert ax.yaxis.get_label().get_text() == expected_ylabel @check_matplotlib def test_list_label(ax): x = [0, 1, 2] * s y = [3, 4, 5] matplotlib_support.label_style = "()" ax.plot(x, y) expected_xlabel = "$\\left(\\rm{s}\\right)$" assert ax.xaxis.get_label().get_text() == expected_xlabel expected_ylabel = "" assert ax.yaxis.get_label().get_text() == expected_ylabel @check_matplotlib def test_errorbar(ax): x = unyt_array([8, 9, 10], "cm") y = unyt_array([8, 9, 10], "kg") y_scatter = [unyt_array([0.1, 0.2, 0.3], "kg"), unyt_array([0.1, 0.2, 0.3], "kg")] x_lims = (unyt_quantity(5, "cm"), unyt_quantity(12, "cm")) y_lims = (unyt_quantity(5, "kg"), unyt_quantity(12, "kg")) ax.errorbar(x, y, yerr=y_scatter) x_lims = (unyt_quantity(5, "cm"), unyt_quantity(12, "cm")) y_lims = (unyt_quantity(5, "kg"), unyt_quantity(12, "kg")) ax.set_xlim(*x_lims) ax.set_ylim(*y_lims) @check_matplotlib def test_hist2d(ax): rng = np.random.default_rng() x = rng.normal(size=50000) * s y = 3 * x + rng.normal(size=50000) * s ax.hist2d(x, y, bins=(50, 50)) @check_matplotlib def test_imshow(ax): rng = np.random.default_rng() data = np.reshape(rng.normal(size=10000), (100, 100)) ax.imshow(data, vmin=data.min(), vmax=data.max()) @check_matplotlib def test_hist(ax): rng = np.random.default_rng() data = rng.normal(size=10000) * s bin_edges = np.linspace(data.min(), data.max(), 50) ax.hist(data, bins=bin_edges) @check_matplotlib def test_matplotlib_support(): with pytest.raises(KeyError): _matplotlib.units.registry[unyt_array] matplotlib_support.enable() assert isinstance(_matplotlib.units.registry[unyt_array], unyt_arrayConverter) matplotlib_support.disable() assert unyt_array not in _matplotlib.units.registry.keys() assert unyt_quantity not in _matplotlib.units.registry.keys() # test as a callable matplotlib_support() assert isinstance(_matplotlib.units.registry[unyt_array], unyt_arrayConverter) @check_matplotlib def test_labelstyle(): x = [0, 1, 2] * s y = [3, 4, 5] * K matplotlib_support.label_style = "[]" assert matplotlib_support.label_style == "[]" matplotlib_support.enable() assert unyt_arrayConverter._labelstyle == "[]" fig, ax = _matplotlib.pyplot.subplots() ax.plot(x, y) expected_xlabel = "$\\left[\\rm{s}\\right]$" assert ax.xaxis.get_label().get_text() == expected_xlabel expected_ylabel = "$\\left[\\rm{K}\\right]$" assert ax.yaxis.get_label().get_text() == expected_ylabel matplotlib_support.label_style = "/" ax.clear() x.name = "$t$" ax.plot(x, y) expected_xlabel = "$t$ $\\;/\\;\\rm{s}$" assert ax.xaxis.get_label().get_text() == expected_xlabel expected_ylabel = "$q_{\\rmy}$$\\;/\\;\\rm{K}$" assert ax.yaxis.get_label().get_text() == expected_ylabel x = [0, 1, 2] * m / s ax.clear() ax.plot(x, y) expected_xlabel = "$q_{\\rmx}$$\\;/\\;\\left(\\rm{m} / \\rm{s}\\right)$" assert ax.xaxis.get_label().get_text() == expected_xlabel _matplotlib.pyplot.close() matplotlib_support.disable() @check_matplotlib def test_name(ax): x = unyt_array([0, 1, 2], "s", name="time") assert x.name == "time" y = unyt_array([3, 4, 5], "m", name="distance") ax.plot(x, y) expected_xlabel = "time $\\left(\\rm{s}\\right)$" assert ax.xaxis.get_label().get_text() == expected_xlabel expected_ylabel = "distance $\\left(\\rm{m}\\right)$" assert ax.yaxis.get_label().get_text() == expected_ylabel ax.clear() ax.plot(x, y, xunits="ms") expected_xlabel = "time $\\left(\\rm{ms}\\right)$" @check_matplotlib def test_multiple_subplots(): x1 = unyt_array([0, 1, 2], "s", name="time") y1 = unyt_array([6, 7, 8], "m", name="distance") x2 = unyt_array([3, 4, 5], "V", name="voltage") y2 = unyt_array([9, 10, 11], "A", name="current") matplotlib_support.enable() fig, ax = _matplotlib.pyplot.subplots(nrows=1, ncols=2) ax[0].plot(x1, y1) ax[1].plot(x2, y2) expected_labels = [ "time $\\left(\\rm{s}\\right)$", "distance $\\left(\\rm{m}\\right)$", "voltage $\\left(\\rm{V}\\right)$", "current $\\left(\\rm{A}\\right)$", ] generated_labels = [] for subplot in ax: xlabel = subplot.xaxis.get_label().get_text() ylabel = subplot.yaxis.get_label().get_text() generated_labels.extend((xlabel, ylabel)) assert generated_labels == expected_labels _matplotlib.pyplot.close() matplotlib_support.disable() @check_matplotlib def test_empty_plot(ax): ax.set_xlim(*unyt_array([0, 1], "s")) _matplotlib.pyplot.close() unyt-3.0.4/unyt/tests/test_no_duplicates.py000066400000000000000000000007221476461141700211340ustar00rootroot00000000000000def test_no_duplicates(): import unyt from unyt import physical_constants, unit_symbols from unyt.array import unyt_quantity symbol_names = {key for key in unit_symbols.__dict__ if not key.startswith("_")} constant_names = { key for key in physical_constants.__dict__ if not key.startswith("_") } dups = symbol_names.intersection(constant_names) for dup in dups: assert isinstance(getattr(unyt, dup), unyt_quantity) unyt-3.0.4/unyt/tests/test_unit_registry.py000066400000000000000000000105641476461141700212170ustar00rootroot00000000000000""" Test unit lookup tables and registry """ import os import pytest from numpy.testing import assert_allclose from unyt.dimensions import energy, length, mass, temperature, time from unyt.exceptions import SymbolNotFoundError, UnitParseError from unyt.unit_object import Unit from unyt.unit_registry import UnitRegistry from unyt.unit_systems import UnitSystem def test_add_modify_error(): from unyt import m ureg = UnitRegistry() with pytest.raises(UnitParseError): ureg.add("tayne", 1, length) with pytest.raises(UnitParseError): ureg.add("tayne", "blah", length) with pytest.raises(UnitParseError): ureg.add("tayne", 1.0, length, offset=1) with pytest.raises(UnitParseError): ureg.add("tayne", 1.0, length, offset="blah") ureg.add("tayne", 1.1, length) with pytest.raises(SymbolNotFoundError): ureg.remove("tayn") with pytest.raises(SymbolNotFoundError): ureg.modify("tayn", 1.2) ureg.modify("tayne", 1.0 * m) assert ureg["tayne"][:3] == ureg["m"][:3] def test_modify_symbol_from_default_unit_registry(): # see https://github.com/yt-project/unyt/issues/473 from unyt import km from unyt.unit_object import default_unit_registry with pytest.raises(TypeError): default_unit_registry.modify("cm", 10 * km) with pytest.raises(TypeError): default_unit_registry.remove("cm") def test_modify_cache_clear(): # see https://github.com/yt-project/unyt/issues/473 ureg = UnitRegistry() ureg.add("celery", 1.0, length) u0 = Unit("m", registry=ureg) u1 = Unit("celery", registry=ureg) assert u1 == u0 ureg.modify("celery", 0.5) # check that this equality still holds post registry modification assert u1 == u0 u2 = Unit("celery", registry=ureg) assert u2 != u1 assert 1.0 * u2 == 0.5 * u0 def test_remove_unit(): ureg = UnitRegistry() ureg.add("celery", 1.0, length) ureg.remove("celery") with pytest.raises(UnitParseError): Unit("celery", registry=ureg) def test_keys(): ureg = UnitRegistry() assert sorted(ureg.keys()) == sorted(ureg.lut.keys()) def test_prefixable_units(): ureg = UnitRegistry() pu = ureg.prefixable_units assert "m" in pu assert "pc" in pu assert "mol" in pu ureg.add("foobar", 1.0, length, prefixable=True) assert "foobar" in ureg.prefixable_units mfoobar = Unit("mfoobar", registry=ureg) foobar = Unit("foobar", registry=ureg) assert (1 * foobar) / (1 * mfoobar) == 1000 def test_registry_contains(): ureg = UnitRegistry() assert "m" in ureg assert "cm" in ureg assert "erg" in ureg assert "Merg" in ureg assert "foobar" not in ureg assert Unit("m", registry=ureg) in ureg def test_registry_json(): reg = UnitRegistry() reg.add("tayne", 1.0, length) json_reg = reg.to_json() unserialized_reg = UnitRegistry.from_json(json_reg) assert reg.lut == unserialized_reg.lut assert reg.lut["m"][1] is length assert reg.lut["erg"][1] is energy assert reg.lut["tayne"][1] is length OLD_JSON_PATH = os.sep.join([os.path.dirname(__file__), "data/old_json_registry.txt"]) def test_old_registry_multiple_load(): # See Issue #157 for details reg1 = UnitRegistry() reg1.add("code_length", 1.0, length) reg1.add("code_mass", 1.0, mass) reg1.add("code_time", 1.0, time) reg1.add("code_temperature", 1.0, temperature) UnitSystem( reg1.unit_system_id, "code_length", "code_mass", "code_time", "code_temperature", registry=reg1, ) cm = Unit("code_mass", registry=reg1) cl = Unit("code_length", registry=reg1) (cm / cl).latex_representation() with open(OLD_JSON_PATH) as f: json_data = f.read() reg2 = UnitRegistry.from_json(json_data) UnitSystem( reg2.unit_system_id, "code_length", "code_mass", "code_time", "code_temperature", registry=reg2, ) def test_old_registry_json(): with open(OLD_JSON_PATH) as f: json_text = f.read() reg = UnitRegistry.from_json(json_text) default_reg = UnitRegistry() loaded_keys = reg.keys() for k in default_reg.keys(): assert k in loaded_keys loaded_val = reg[k] val = default_reg[k] assert_allclose(loaded_val[0], val[0]) assert loaded_val[1:] == val[1:] unyt-3.0.4/unyt/tests/test_unit_systems.py000066400000000000000000000076131476461141700210570ustar00rootroot00000000000000""" Test unit systems. """ import pytest import unyt.unit_symbols as us from unyt import dimensions from unyt.exceptions import IllDefinedUnitSystem, MissingMKSCurrent from unyt.unit_object import Unit from unyt.unit_registry import UnitRegistry from unyt.unit_systems import ( UnitSystem, cgs_unit_system, mks_unit_system, unit_system_registry, ) def test_unit_systems(): goofy_unit_system = UnitSystem( "goofy", "ly", "lbm", "hr", temperature_unit="R", angle_unit="arcsec", current_mks_unit="mA", luminous_intensity_unit="cd", ) assert goofy_unit_system["temperature"] == Unit("R") assert goofy_unit_system[dimensions.solid_angle] == Unit("arcsec**2") assert goofy_unit_system["energy"] == Unit("lbm*ly**2/hr**2") goofy_unit_system["energy"] = "eV" assert goofy_unit_system["energy"] == Unit("eV") assert goofy_unit_system["magnetic_field_mks"] == Unit("lbm/(hr**2*mA)") assert "goofy" in unit_system_registry def test_unit_system_id(): reg1 = UnitRegistry() reg2 = UnitRegistry() assert reg1.unit_system_id == reg2.unit_system_id reg1.modify("g", 2.0) assert reg1.unit_system_id != reg2.unit_system_id reg1 = UnitRegistry() reg1.add("dinosaurs", 12.0, dimensions.length) assert reg1.unit_system_id != reg2.unit_system_id reg1 = UnitRegistry() reg1.remove("g") assert reg1.unit_system_id != reg2.unit_system_id reg1.add("g", 1.0e-3, dimensions.mass, prefixable=True) assert reg1.unit_system_id == reg2.unit_system_id def test_bad_unit_system(): with pytest.raises(IllDefinedUnitSystem): UnitSystem("atomic", "nm", "fs", "nK", "rad") with pytest.raises(IllDefinedUnitSystem): UnitSystem("atomic", "nm", "fs", "nK", "rad", registry=UnitRegistry()) with pytest.raises(IllDefinedUnitSystem): UnitSystem("atomic", us.nm, us.fs, us.nK, us.rad) with pytest.raises(IllDefinedUnitSystem): UnitSystem("atomic", us.nm, us.fs, us.nK, us.rad, registry=UnitRegistry()) def test_code_unit_system(): ureg = UnitRegistry() ureg.add("code_length", 2.0, dimensions.length) ureg.add("code_mass", 3.0, dimensions.mass) ureg.add("code_time", 4.0, dimensions.time) ureg.add("code_temperature", 5.0, dimensions.temperature) code_unit_system = UnitSystem( "my_unit_system", "code_length", "code_mass", "code_time", "code_temperature", registry=ureg, ) assert code_unit_system["length"] == Unit("code_length", registry=ureg) assert code_unit_system["length"].base_value == 2 assert code_unit_system["mass"] == Unit("code_mass", registry=ureg) assert code_unit_system["mass"].base_value == 3 assert code_unit_system["time"] == Unit("code_time", registry=ureg) assert code_unit_system["time"].base_value == 4 assert code_unit_system["temperature"] == Unit("code_temperature", registry=ureg) assert code_unit_system["temperature"].base_value == 5 def test_mks_current(): with pytest.raises(MissingMKSCurrent): cgs_unit_system[dimensions.current_mks] with pytest.raises(MissingMKSCurrent): cgs_unit_system[dimensions.magnetic_field] with pytest.raises(MissingMKSCurrent): cgs_unit_system[dimensions.current_mks] = "foo" with pytest.raises(MissingMKSCurrent): cgs_unit_system[dimensions.magnetic_field] = "bar" assert cgs_unit_system.has_current_mks is False assert mks_unit_system.has_current_mks is True def test_create_unit_system_from_unit_objects(): s = UnitSystem("test_units", us.Mpc, us.Msun, us.s) assert s["length"] == us.Mpc assert s["mass"] == us.Msun assert s["time"] == us.s def test_create_unit_system_from_quantity(): s = UnitSystem("test_units", us.Mpc, 3 * us.Msun, us.s) assert s["length"] == us.Mpc assert s["mass"] == Unit("3*Msun") assert s["time"] == us.s unyt-3.0.4/unyt/tests/test_units.py000066400000000000000000000555741476461141700174640ustar00rootroot00000000000000""" Test symbolic unit handling. """ import operator import pickle import numpy as np import pytest from numpy.testing import ( assert_allclose, assert_almost_equal, assert_array_almost_equal_nulp, assert_equal, ) from sympy import Symbol import unyt.unit_symbols as unit_symbols from unyt._physical_ratios import ( m_per_km, m_per_mpc, m_per_pc, mass_sun_kg, sec_per_year, ) from unyt._unit_lookup_table import ( default_unit_symbol_lut, name_alternatives, unit_prefixes, ) from unyt.array import unyt_quantity from unyt.dimensions import ( energy, length, magnetic_field_cgs, magnetic_field_mks, mass, power, rate, temperature, time, ) from unyt.exceptions import InvalidUnitOperation, UnitConversionError, UnitsNotReducible from unyt.testing import assert_allclose_units from unyt.unit_object import Unit, UnitParseError, default_unit_registry from unyt.unit_registry import UnitRegistry from unyt.unit_systems import UnitSystem, cgs_unit_system def test_no_conflicting_symbols(): """ Check unit symbol definitions for conflicts. """ full_set = set(default_unit_symbol_lut.keys()) # go through all possible prefix combos for symbol in default_unit_symbol_lut.keys(): if default_unit_symbol_lut[symbol][4]: keys = unit_prefixes.keys() else: keys = [symbol] for prefix in keys: new_symbol = f"{prefix}{symbol}" # test if we have seen this symbol assert new_symbol not in full_set, f"Duplicate symbol: {new_symbol}" full_set.add(new_symbol) def test_dimensionless(): """ Create dimensionless unit and check attributes. """ u1 = Unit() assert u1.is_dimensionless assert u1.expr == 1 assert u1.base_value == 1 assert u1.dimensions == 1 assert u1 != "hello!" assert (u1 == "hello") is False u2 = Unit("") assert u2.is_dimensionless assert u2.expr == 1 assert u2.base_value == 1 assert u2.dimensions == 1 assert_equal(u1.latex_repr, "") assert_equal(u2.latex_repr, "") def test_create_from_string(): """ Create units with strings and check attributes. """ u1 = Unit("kg * m**2 * s**-2") assert u1.dimensions == energy assert u1.base_value == 1.0 # make sure order doesn't matter u2 = Unit("m**2 * s**-2 * kg") assert u2.dimensions == energy assert u2.base_value == 1.0 # Test rationals u3 = Unit("kg**0.5 * m**-0.5 * s**-1") assert u3.dimensions == magnetic_field_cgs assert u3.base_value == 1.0 # sqrt functions u4 = Unit("sqrt(kg)/sqrt(m)/s") assert u4.dimensions == magnetic_field_cgs assert u4.base_value == 1.0 # commutative sqrt function u5 = Unit("sqrt(kg/m)/s") assert u5.dimensions == magnetic_field_cgs assert u5.base_value == 1.0 # nonzero CGS conversion factor u6 = Unit("Msun/pc**3") assert u6.dimensions == mass / length**3 assert_array_almost_equal_nulp( np.array([u6.base_value]), np.array([mass_sun_kg / m_per_pc**3]) ) with pytest.raises(UnitParseError): Unit("m**m") with pytest.raises(UnitParseError): Unit("m**g") with pytest.raises(UnitParseError): Unit("m+g") with pytest.raises(UnitParseError): Unit("m-g") with pytest.raises(UnitParseError): Unit("hello!") with pytest.raises(UnitParseError): Unit("True") with pytest.raises(UnitParseError): Unit("else") with pytest.raises(UnitParseError): Unit("hello(37)") with pytest.raises(UnitParseError): Unit("hello(foo=37)") cm = Unit("cm") data = 1 * cm assert Unit(data) == cm assert Unit(b"cm") == cm def test_create_from_expr(): """ Create units from sympy Exprs and check attributes. """ pc_mks = m_per_pc yr_mks = sec_per_year # Symbol expr s1 = Symbol("pc", positive=True) s2 = Symbol("yr", positive=True) # Mul expr s3 = s1 * s2 # Pow expr s4 = s1**2 * s2 ** (-1) u1 = Unit(s1) u2 = Unit(s2) u3 = Unit(s3) u4 = Unit(s4) assert u1.expr == s1 assert u2.expr == s2 assert u3.expr == s3 assert u4.expr == s4 assert_allclose_units(u1.base_value, pc_mks, 1e-12) assert_allclose_units(u2.base_value, yr_mks, 1e-12) assert_allclose_units(u3.base_value, pc_mks * yr_mks, 1e-12) assert_allclose_units(u4.base_value, pc_mks**2 / yr_mks, 1e-12) assert u1.dimensions == length assert u2.dimensions == time assert u3.dimensions == length * time assert u4.dimensions == length**2 / time def test_create_with_duplicate_dimensions(): """ Create units with overlapping dimensions. Ex: km/Mpc. """ u1 = Unit("J * s**-1") u2 = Unit("km/s/Mpc") km_mks = m_per_km Mpc_mks = m_per_mpc assert u1.base_value == 1 assert u1.dimensions == power assert_allclose_units(u2.base_value, km_mks / Mpc_mks, 1e-12) assert u2.dimensions == rate def test_create_new_symbol(): """ Create unit with unknown symbol. """ u1 = Unit("abc", base_value=42, dimensions=(mass / time)) assert u1.expr == Symbol("abc", positive=True) assert u1.base_value == 42 assert u1.dimensions == mass / time u1 = Unit("abc", base_value=42, dimensions=length**3) assert u1.expr == Symbol("abc", positive=True) assert u1.base_value == 42 assert u1.dimensions == length**3 u1 = Unit("abc", base_value=42, dimensions=length * (mass * length)) assert u1.expr == Symbol("abc", positive=True) assert u1.base_value == 42 assert u1.dimensions == length**2 * mass with pytest.raises(UnitParseError): Unit("abc", base_value=42, dimensions=length**length) with pytest.raises(UnitParseError): Unit("abc", base_value=42, dimensions=length ** (length * length)) with pytest.raises(UnitParseError): Unit("abc", base_value=42, dimensions=length - mass) with pytest.raises(UnitParseError): Unit("abc", base_value=42, dimensions=length + mass) def test_create_fail_on_unknown_symbol(): """ Fail to create unit with unknown symbol, without base_value and dimensions. """ with pytest.raises(UnitParseError): Unit(Symbol("jigawatts")) def test_create_fail_on_bad_symbol_type(): """ Fail to create unit with bad symbol type. """ with pytest.raises(UnitParseError): Unit([1]) # something other than Expr and str def test_create_fail_on_bad_dimensions_type(): """ Fail to create unit with bad dimensions type. """ with pytest.raises(UnitParseError): Unit("a", base_value=1, dimensions="(mass)") def test_create_fail_on_dimensions_content(): """ Fail to create unit with bad dimensions expr. """ a = Symbol("a") with pytest.raises(UnitParseError): Unit("a", base_value=1, dimensions=a) def test_create_fail_on_base_value_type(): """ Fail to create unit with bad base_value type. """ with pytest.raises(UnitParseError): Unit("a", base_value="a", dimensions=(mass / time)) def test_string_representation(): """ Check unit string representation. """ pc = Unit("pc") Myr = Unit("Myr") speed = pc / Myr dimensionless = Unit() assert str(pc) == "pc" assert str(Myr) == "Myr" assert str(speed) == "pc/Myr" assert repr(speed) == "pc/Myr" assert str(dimensionless) == "dimensionless" assert repr(dimensionless) == "(dimensionless)" def test_multiplication(): """ Multiply two units. """ msun_mks = mass_sun_kg pc_mks = m_per_pc # Create symbols msun_sym = Symbol("Msun", positive=True) pc_sym = Symbol("pc", positive=True) s_sym = Symbol("s", positive=True) # Create units u1 = Unit("Msun") u2 = Unit("pc") # Mul operation u3 = u1 * u2 assert u3.expr == msun_sym * pc_sym assert_allclose_units(u3.base_value, msun_mks * pc_mks, 1e-12) assert u3.dimensions == mass * length # Pow and Mul operations u4 = Unit("pc**2") u5 = Unit("Msun * s") u6 = u4 * u5 assert u6.expr == pc_sym**2 * msun_sym * s_sym assert_allclose_units(u6.base_value, pc_mks**2 * msun_mks, 1e-12) assert u6.dimensions == length**2 * mass * time def test_division(): """ Divide two units. """ pc_mks = m_per_pc km_mks = m_per_km # Create symbols pc_sym = Symbol("pc", positive=True) km_sym = Symbol("km", positive=True) s_sym = Symbol("s", positive=True) # Create units u1 = Unit("pc") u2 = Unit("km * s") u3 = u1 / u2 assert u3.expr == pc_sym / (km_sym * s_sym) assert_allclose_units(u3.base_value, pc_mks / km_mks, 1e-12) assert u3.dimensions == 1 / time def test_power(): """ Take units to some power. """ from sympy import nsimplify from unyt import dimensionless pc_mks = m_per_pc mK_mks = 1e-3 u1_dims = mass * length**2 * time**-3 * temperature**4 u1 = Unit("kg * pc**2 * s**-3 * mK**4") u2 = u1**2 assert u2.dimensions == u1_dims**2 assert_allclose_units(u2.base_value, (pc_mks**2 * mK_mks**4) ** 2, 1e-12) u3 = u1 ** (-1.0 / 3) assert u3.dimensions == nsimplify(u1_dims ** (-1.0 / 3)) assert_allclose_units(u3.base_value, (pc_mks**2 * mK_mks**4) ** (-1.0 / 3), 1e-12) assert u1**0.0 == dimensionless def test_equality(): """ Check unit equality with different symbols, but same dimensions and base_value. """ u1 = Unit("km * s**-1") u2 = Unit("m * ms**-1") assert u1 == u2 assert u1.copy() == u2 def test_invalid_operations(): u1 = Unit("cm") u2 = Unit("m") with pytest.raises(InvalidUnitOperation): u1 + u2 with pytest.raises(InvalidUnitOperation): u1 += u2 with pytest.raises(InvalidUnitOperation): 1 + u1 with pytest.raises(InvalidUnitOperation): u1 + 1 with pytest.raises(InvalidUnitOperation): u1 - u2 with pytest.raises(InvalidUnitOperation): u1 -= u2 with pytest.raises(InvalidUnitOperation): 1 - u1 with pytest.raises(InvalidUnitOperation): u1 - 1 with pytest.raises(InvalidUnitOperation): u1 *= u2 with pytest.raises(InvalidUnitOperation): u1 * "hello!" with pytest.raises(InvalidUnitOperation): u1 /= u2 with pytest.raises(InvalidUnitOperation): u1 / "hello!" with pytest.raises(InvalidUnitOperation): Unit("B") * Unit("V") with pytest.raises(InvalidUnitOperation): Unit("V") * Unit("B") with pytest.raises(InvalidUnitOperation): Unit("V") / Unit("Np") with pytest.raises(InvalidUnitOperation): Unit("dB") / Unit("dB") with pytest.raises(InvalidUnitOperation): Unit("B") ** 2 def test_base_equivalent(): """ Check base equivalent of a unit. """ Msun_mks = mass_sun_kg Mpc_mks = m_per_mpc u1 = Unit("Msun * Mpc**-3") u2 = Unit("kg * m**-3") u3 = u1.get_base_equivalent() assert u2.expr == u3.expr assert u2 == u3 assert_allclose_units(u1.base_value, Msun_mks / Mpc_mks**3, 1e-12) assert u2.base_value == 1 assert u3.base_value == 1 mass_density = mass / length**3 assert u1.dimensions == mass_density assert u2.dimensions == mass_density assert u3.dimensions == mass_density assert_allclose_units(u1.get_conversion_factor(u3)[0], Msun_mks / Mpc_mks**3, 1e-12) with pytest.raises(UnitConversionError): u1.get_conversion_factor(Unit("m")) with pytest.raises(UnitConversionError): u1.get_conversion_factor(Unit("degF")) reg = UnitRegistry(unit_system=cgs_unit_system) u = Unit("kg", registry=reg) assert u.get_base_equivalent() == Unit("g") u = Unit("kg") assert u.get_base_equivalent() == Unit("kg") u = Unit("A") assert u.get_base_equivalent(unit_system="mks") == Unit("A") @pytest.mark.parametrize( ("u1", "u2"), [ (Unit("degC"), Unit("degF")), (Unit("degC"), Unit("K")), (Unit("degF"), Unit("K")), ], ) def test_temperature_offsets(u1, u2): with pytest.raises(InvalidUnitOperation): operator.mul(u1, u2) with pytest.raises(InvalidUnitOperation): operator.truediv(u1, u2) assert u1 != u2 assert u2 != u1 assert not u1 == u2 assert not u2 == u1 def test_latex_repr(): registry = UnitRegistry() # create a fake comoving unit registry.add( "pccm", registry.lut["pc"][0] / (1 + 2), length, "\\rm{pc}/(1+z)", prefixable=True, ) test_unit = Unit("Mpccm", registry=registry) assert_almost_equal(test_unit.base_value, m_per_mpc / 3) assert_equal(test_unit.latex_repr, r"\rm{Mpc}/(1+z)") test_unit = Unit("cm**-3", base_value=1.0, registry=registry) assert_equal(test_unit.latex_repr, "\\frac{1}{\\rm{cm}^{3}}") test_unit = Unit("m_geom/l_geom**3") assert_equal(test_unit.latex_repr, "\\frac{1}{\\rm{M}_\\odot^{2}}") test_unit = Unit("1e9*cm") assert_equal(test_unit.latex_repr, "1.0 \\times 10^{9}\\ \\rm{cm}") test_unit = Unit("1.0*cm") assert_equal(test_unit.latex_repr, "\\rm{cm}") def test_latitude_longitude(): lat = unit_symbols.lat lon = unit_symbols.lon deg = unit_symbols.deg assert_equal(lat.units.base_offset, 90.0) assert_equal((deg * 90.0).in_units("lat").value, 0.0) assert_equal((deg * 180).in_units("lat").value, -90.0) assert_equal((lat * 0.0).in_units("deg"), deg * 90.0) assert_equal((lat * -90).in_units("deg"), deg * 180) assert_equal(lon.units.base_offset, -180.0) assert_equal((deg * 0.0).in_units("lon").value, -180.0) assert_equal((deg * 90.0).in_units("lon").value, -90.0) assert_equal((deg * 180).in_units("lon").value, 0.0) assert_equal((deg * 360).in_units("lon").value, 180.0) assert_equal((lon * -180.0).in_units("deg"), deg * 0.0) assert_equal((lon * -90.0).in_units("deg"), deg * 90.0) assert_equal((lon * 0.0).in_units("deg"), deg * 180.0) assert_equal((lon * 180.0).in_units("deg"), deg * 360) def test_creation_from_unyt_array(): from unyt import electrostatic_unit, elementary_charge_cgs u1 = Unit(electrostatic_unit) assert_equal(str(u1), "statC") assert_equal(u1, Unit("esu")) assert_equal(u1, electrostatic_unit.units) u2 = Unit(elementary_charge_cgs) assert_equal(str(u2), "4.80320467299766e-10*statC") assert_equal(u2, Unit("4.80320467299766e-10*statC")) assert_equal(u1, elementary_charge_cgs.units) assert_allclose((u1 / u2).base_value, electrostatic_unit / elementary_charge_cgs) with pytest.raises(UnitParseError): Unit([1, 2, 3] * elementary_charge_cgs) def test_list_same_dimensions(): from unyt import m reg = default_unit_registry for equiv in reg.list_same_dimensions(m): assert Unit(equiv).dimensions is length def test_decagram(): dag = Unit("dag") g = Unit("g") assert dag.get_conversion_factor(g) == (10.0, None) def test_pickle(): cm = Unit("cm") assert cm == pickle.loads(pickle.dumps(cm)) def test_preserve_offset(): from unyt import degF, dimensionless new_unit = degF * dimensionless assert new_unit is not degF assert new_unit == degF assert new_unit.base_offset == degF.base_offset new_unit = degF / dimensionless assert new_unit is not degF assert new_unit == degF assert new_unit.base_offset == degF.base_offset with pytest.raises(InvalidUnitOperation): dimensionless / degF def test_code_unit(): from unyt import UnitRegistry ureg = UnitRegistry() ureg.add("code_length", 10.0, length) ureg.add("code_magnetic_field", 2.0, magnetic_field_mks) u = Unit("code_length", registry=ureg) assert u.is_code_unit is True assert u.get_base_equivalent() == Unit("m") u = Unit("cm") assert u.is_code_unit is False u = Unit("code_magnetic_field", registry=ureg) assert u.get_base_equivalent("mks") == Unit("T") with pytest.raises(UnitsNotReducible): assert u.get_base_equivalent("cgs") # see issue #60 u = Unit("s/m") assert u.get_mks_equivalent() == Unit("s/m") assert u.get_mks_equivalent() != Unit("ohm") assert u.get_cgs_equivalent() == Unit("s/cm") u = Unit("kC") assert u.get_cgs_equivalent() == Unit("kesu") assert u.get_cgs_equivalent().get_mks_equivalent() == u UnitSystem(ureg.unit_system_id, "code_length", "kg", "s", registry=ureg) u = Unit("cm", registry=ureg) ue = u.get_base_equivalent("code") assert str(ue) == "code_length" assert ue.base_value == 10 assert ue.dimensions is length class FakeDataset: unit_registry = ureg ds = FakeDataset() UnitSystem(ds, "code_length", "kg", "s", registry=ureg) u = Unit("cm", registry=ureg) ue = u.get_base_equivalent(ds) assert str(ue) == "code_length" assert ue.base_value == 10 assert ue.dimensions is length with pytest.raises(UnitParseError): Unit("code_length") def test_bad_equivalence(): from unyt import cm with pytest.raises(KeyError): cm.has_equivalent("dne") def test_em_unit_base_equivalent(): from unyt import A, cm with pytest.raises(UnitsNotReducible): (A / cm).get_base_equivalent("cgs") def test_symbol_lut_length(): for v in default_unit_symbol_lut.values(): assert len(v) == 5 def test_simplify(): import unyt as u answers = { u.Hz * u.s: "dimensionless", u.kg / u.g: "1000", u.Hz * u.s * u.km: "km", u.kHz * u.s: "1000", u.kHz * u.s * u.km: "1000*km", u.kHz * u.s**2: "1000*s", u.kHz * u.s**2 * u.km: "1000*km*s", u.Hz**-1 * u.s: "s/Hz", u.Hz**-1 * u.s * u.km: "km*s/Hz", u.Hz**1.5 * u.s**1.7: "sqrt(Hz)*s**(7/10)", u.Hz**1.5 * u.s**1.7 * u.km: "sqrt(Hz)*km*s**(7/10)", u.m**2 / u.cm**2: "10000", } for unit, answer in answers.items(): assert str(unit.simplify()) == answer def test_micro_prefix(): import unyt as u # both versions of unicode mu work correctly assert u.um == u.µm assert u.um == u.μm # parsing both versions works as well assert u.ug == u.Unit("µg") assert u.ug == u.Unit("μg") def test_name_alternatives(): import unyt from unyt._unit_lookup_table import ( default_unit_name_alternatives, inv_name_alternatives, name_alternatives, ) # concatenated list of all alternative unit names allowed_names = sum(name_alternatives.values(), []) # ensure the values are all tuples and not e.g. strings for val in default_unit_name_alternatives.values(): assert isinstance(val, tuple) # all names are unique assert len(set(allowed_names)) == len(allowed_names) # each allowed name has a key in the inverse dict assert len(inv_name_alternatives.keys()) == len(allowed_names) assert set(inv_name_alternatives.keys()) == set(allowed_names) for name in allowed_names: assert hasattr(unyt, name) assert hasattr(unyt.unit_symbols, name) def test_solar_unit_name_alternatives(): import unyt from unyt import Unit # check that m_sun, m_Sun, M_sun, M_Sun, msun, and Msun all work for lower_name_prefix in "mrltz": base_name = lower_name_prefix + "sun" for name_prefix in [lower_name_prefix, lower_name_prefix.upper()]: alternative_names = [name_prefix + suf for suf in ["sun", "_sun", "_Sun"]] for name in alternative_names: assert Unit(name) == Unit(base_name) assert hasattr(unyt, name) # only solar mass units are in physical constants if lower_name_prefix == "m": assert hasattr(unyt.physical_constants, name) def test_attosecond(): from unyt import Unit, attosecond, second assert Unit("as") == attosecond assert str(Unit("as")) == "as" assert Unit("as/s") == attosecond / second def test_micro(): from unyt import Unit assert str(Unit("um")) == "μm" assert str(Unit("us")) == "μs" def test_show_all_units_doc_table_ops(): for name in set(name_alternatives.keys()): u = Unit(name) (1 * u).in_mks() try: (1 * u).in_cgs() except UnitsNotReducible: pass def test_hPa_mbar(): assert Unit("hPa").dimensions == Unit("bar").dimensions assert (5 * Unit("hPa") == 5 * Unit("mbar")).all() assert (5 * Unit("hPa") != 1 * Unit("bar")).all() def test_percent(): a = 300 * Unit("percent") b = 3.0 * Unit("dimensionless") c = 300.0 * Unit("%") d = 300.0 * Unit("V*%/V") assert a == b assert str(a) == "300 %" assert repr(a) == "unyt_quantity(300, '%')" assert a == c assert c == d def test_equal_has_same_hash(): a = Unit("m") b = Unit("m") c = Unit("m*s/s") assert a == b assert b == c assert hash(a) == hash(b) assert hash(b) == hash(c) def test_bel_neper(): assert Unit("B").dimensions == Unit("Np").dimensions a = 1 * Unit("B") / (np.log(10) / 2) assert_allclose_units(a.to("Np"), 1 * Unit("Np")) a = 2 * Unit("B") b = 20 * Unit("decibel") assert (a == b).all() c = 2 * Unit("Np") d = 20 * Unit("decineper") assert (c == d).all() assert Unit("dB") ** 1 == Unit("dB") def test_henry(): assert (Unit("H") / Unit("Ω")).dimensions == time def test_degC(): assert Unit("degree_celsius") == Unit("degC") assert Unit("degree_Celsius") == Unit("degC") assert Unit("Celsius") == Unit("degC") assert Unit("°C") == Unit("degC") a = 1 * Unit("degC") assert str(a) == "1 °C" def test_degC_with_SIprefixes(): assert_allclose_units(1 * Unit("mdegC"), 0.001 * Unit("degC")) assert_allclose_units(1 * Unit("degC"), 1000 * Unit("mdegC")) assert_allclose_units(73 * Unit("degF"), 22777.779 * Unit("mdegC")) assert_allclose_units(22777.779 * Unit("mdegC"), 73 * Unit("degF")) assert_allclose_units(22777.779 * Unit("mdegC"), 532.67 * Unit("R")) assert_allclose_units(1 * Unit("mK"), -273149.0 * Unit("mdegC")) assert_allclose_units(1 * Unit("mdegC"), 273151.0 * Unit("mK")) def test_delta_degC(): a = 1 * Unit("delta_degC") assert str(a) == "1 Δ°C" def test_degF(): assert Unit("degree_fahrenheit") == Unit("degF") assert Unit("degree_Fahrenheit") == Unit("degF") assert Unit("Fahrenheit") == Unit("degF") assert Unit("°F") == Unit("degF") a = 1 * Unit("degF") assert str(a) == "1 °F" def test_delta_degF(): a = 1 * Unit("delta_degF") assert str(a) == "1 Δ°F" def test_mixed_registry_operations(): reg = UnitRegistry(unit_system="cgs") reg.add("fake_length", 0.001, length) a = unyt_quantity(1, units="fake_length", registry=reg) b = unyt_quantity(1, "cm") assert_almost_equal(a + b, b + a) assert_almost_equal(a - b, -(b - a)) assert_almost_equal(a * b, b * a) assert_almost_equal(b / a, b / a.in_units("km")) assert_almost_equal(a / b, a / b.in_units("km")) unyt-3.0.4/unyt/tests/test_unyt_array.py000066400000000000000000002624751476461141700205170ustar00rootroot00000000000000""" Test ndarray subclass that handles symbolic units. """ import copy import itertools import math import operator import os import pickle import re import shutil import tempfile from importlib.metadata import version import numpy as np import pytest from numpy import array from numpy.testing import ( assert_almost_equal, assert_array_almost_equal, assert_array_equal, assert_equal, ) from packaging.version import Version from unyt import K, R, Unit, degC, degF, delta_degC, delta_degF, dimensions from unyt._on_demand_imports import _astropy, _h5py, _pint from unyt._physical_ratios import metallicity_sun, speed_of_light_cm_per_s from unyt.array import ( binary_operators, loadtxt, savetxt, uconcatenate, ucross, udot, uintersect1d, unary_operators, unorm, unyt_array, unyt_quantity, ustack, uunion1d, uvstack, ) from unyt.exceptions import ( InvalidUnitEquivalence, InvalidUnitOperation, IterableUnitCoercionError, UnitConversionError, UnitOperationError, UnitParseError, UnitsNotReducible, ) from unyt.testing import ( _process_warning, assert_allclose_units, assert_array_equal_units, ) from unyt.unit_registry import UnitRegistry from unyt.unit_symbols import cm, degree, g, m NUMPY_VERSION = Version(version("numpy")) def operate_and_compare(a, b, op, answer): # Test generator for unyt_arrays tests assert_array_almost_equal(op(a, b), answer) def assert_isinstance(a, type): assert isinstance(a, type) def test_addition(): """ Test addition of two unyt_arrays """ # Same units a1 = unyt_array([1, 2, 3], "cm") a2 = unyt_array([4, 5, 6], "cm") a3 = [4 * cm, 5 * cm, 6 * cm] answer = unyt_array([5, 7, 9], "cm") operate_and_compare(a1, a2, operator.add, answer) operate_and_compare(a2, a1, operator.add, answer) operate_and_compare(a1, a3, operator.add, answer) operate_and_compare(a3, a1, operator.add, answer) operate_and_compare(a2, a1, np.add, answer) operate_and_compare(a1, a2, np.add, answer) operate_and_compare(a1, a3, np.add, answer) operate_and_compare(a3, a1, np.add, answer) # different units a1 = unyt_array([1, 2, 3], "cm") a2 = unyt_array([4, 5, 6], "m") a3 = [4 * m, 5 * m, 6 * m] answer1 = unyt_array([401, 502, 603], "cm") answer2 = unyt_array([4.01, 5.02, 6.03], "m") operate_and_compare(a1, a2, operator.add, answer1) operate_and_compare(a2, a1, operator.add, answer2) operate_and_compare(a1, a3, operator.add, answer1) operate_and_compare(a3, a1, operator.add, answer2) operate_and_compare(a1, a2, np.add, answer1) operate_and_compare(a2, a1, np.add, answer2) operate_and_compare(a1, a3, np.add, answer1) operate_and_compare(a3, a1, np.add, answer2) # Test dimensionless quantities a1 = unyt_array([1, 2, 3]) a2 = array([4, 5, 6]) a3 = [4, 5, 6] answer = unyt_array([5, 7, 9]) operate_and_compare(a1, a2, operator.add, answer) operate_and_compare(a2, a1, operator.add, answer) operate_and_compare(a1, a3, operator.add, answer) operate_and_compare(a3, a1, operator.add, answer) operate_and_compare(a1, a2, np.add, answer) operate_and_compare(a2, a1, np.add, answer) operate_and_compare(a1, a3, np.add, answer) operate_and_compare(a3, a1, np.add, answer) # Catch the different dimensions error a1 = unyt_array([1, 2, 3], "m") a2 = unyt_array([4, 5, 6], "kg") a3 = [7, 8, 9] a4 = unyt_array([10, 11, 12], "") with pytest.raises(UnitOperationError): operator.add(a1, a2) with pytest.raises(UnitOperationError): operator.iadd(a1, a2) with pytest.raises(UnitOperationError): operator.add(a1, a3) with pytest.raises(UnitOperationError): operator.iadd(a1, a3) with pytest.raises(UnitOperationError): operator.add(a3, a1) with pytest.raises(UnitOperationError): operator.iadd(a3, a1) with pytest.raises(UnitOperationError): operator.add(a1, a4) with pytest.raises(UnitOperationError): operator.iadd(a1, a4) with pytest.raises(UnitOperationError): operator.add(a4, a1) with pytest.raises(UnitOperationError): operator.iadd(a4, a1) # adding with zero is allowed irrespective of the units zeros = np.zeros(3) zeros_yta_dimless = unyt_array(zeros, "dimensionless") zeros_yta_length = unyt_array(zeros, "m") zeros_yta_mass = unyt_array(zeros, "kg") operands = [0, zeros, zeros_yta_length] for op in [operator.add, np.add]: for operand in operands: operate_and_compare(a1, operand, op, a1) operate_and_compare(operand, a1, op, a1) operate_and_compare(4 * m, operand, op, 4 * m) operate_and_compare(operand, 4 * m, op, 4 * m) operands = [ unyt_quantity(0), unyt_quantity(0, "kg"), zeros_yta_dimless, zeros_yta_mass, ] for op in [operator.add, np.add]: for operand in operands: with pytest.raises(UnitOperationError): operate_and_compare(a1, operand, op, a1) with pytest.raises(UnitOperationError): operate_and_compare(operand, a1, op, a1) with pytest.raises(UnitOperationError): operate_and_compare(4 * m, operand, op, 4 * m) with pytest.raises(UnitOperationError): operate_and_compare(operand, 4 * m, op, 4 * m) def test_subtraction(): """ Test subtraction of two unyt_arrays """ # Same units a1 = unyt_array([1, 2, 3], "cm") a2 = unyt_array([4, 5, 6], "cm") a3 = [4 * cm, 5 * cm, 6 * cm] answer1 = unyt_array([-3, -3, -3], "cm") answer2 = unyt_array([3, 3, 3], "cm") operate_and_compare(a1, a2, operator.sub, answer1) operate_and_compare(a2, a1, operator.sub, answer2) operate_and_compare(a1, a3, operator.sub, answer1) operate_and_compare(a3, a1, operator.sub, answer2) operate_and_compare(a1, a2, np.subtract, answer1) operate_and_compare(a2, a1, np.subtract, answer2) operate_and_compare(a1, a3, np.subtract, answer1) operate_and_compare(a3, a1, np.subtract, answer2) # different units a1 = unyt_array([1, 2, 3], "cm") a2 = unyt_array([4, 5, 6], "m") a3 = [4 * m, 5 * m, 6 * m] answer1 = unyt_array([-399, -498, -597], "cm") answer2 = unyt_array([3.99, 4.98, 5.97], "m") answer3 = unyt_array([399, 498, 597], "cm") operate_and_compare(a1, a2, operator.sub, answer1) operate_and_compare(a2, a1, operator.sub, answer2) operate_and_compare(a1, a3, operator.sub, answer1) operate_and_compare(a3, a1, operator.sub, answer3) operate_and_compare(a1, a2, np.subtract, answer1) operate_and_compare(a2, a1, np.subtract, answer2) operate_and_compare(a1, a3, np.subtract, answer1) operate_and_compare(a3, a1, np.subtract, answer3) # Test dimensionless quantities a1 = unyt_array([1, 2, 3]) a2 = array([4, 5, 6]) a3 = [4, 5, 6] answer1 = unyt_array([-3, -3, -3]) answer2 = unyt_array([3, 3, 3]) operate_and_compare(a1, a2, operator.sub, answer1) operate_and_compare(a2, a1, operator.sub, answer2) operate_and_compare(a1, a3, operator.sub, answer1) operate_and_compare(a3, a1, operator.sub, answer2) operate_and_compare(a1, a2, np.subtract, answer1) operate_and_compare(a2, a1, np.subtract, answer2) operate_and_compare(a1, a3, np.subtract, answer1) operate_and_compare(a3, a1, np.subtract, answer2) # Catch the different dimensions error a1 = unyt_array([1, 2, 3], "m") a2 = unyt_array([4, 5, 6], "kg") a3 = [7, 8, 9] a4 = unyt_array([10, 11, 12], "") with pytest.raises(UnitOperationError): operator.sub(a1, a2) with pytest.raises(UnitOperationError): operator.isub(a1, a2) with pytest.raises(UnitOperationError): operator.sub(a1, a3) with pytest.raises(UnitOperationError): operator.isub(a1, a3) with pytest.raises(UnitOperationError): operator.sub(a3, a1) with pytest.raises(UnitOperationError): operator.isub(a3, a1) with pytest.raises(UnitOperationError): operator.sub(a1, a4) with pytest.raises(UnitOperationError): operator.isub(a1, a4) with pytest.raises(UnitOperationError): operator.sub(a4, a1) with pytest.raises(UnitOperationError): operator.isub(a4, a1) # subtracting with zero is allowed irrespective of the units zeros = np.zeros(3) zeros_yta_dimless = unyt_array(zeros, "dimensionless") zeros_yta_length = unyt_array(zeros, "m") zeros_yta_mass = unyt_array(zeros, "kg") operands = [0, zeros, zeros_yta_length] for op in [operator.sub, np.subtract]: for operand in operands: operate_and_compare(a1, operand, op, a1) operate_and_compare(operand, a1, op, -a1) operate_and_compare(4 * m, operand, op, 4 * m) operate_and_compare(operand, 4 * m, op, -4 * m) operands = [ unyt_quantity(0), unyt_quantity(0, "kg"), zeros_yta_dimless, zeros_yta_mass, ] for op in [operator.sub, np.subtract]: for operand in operands: with pytest.raises(UnitOperationError): operate_and_compare(a1, operand, op, a1) with pytest.raises(UnitOperationError): operate_and_compare(operand, a1, op, -a1) with pytest.raises(UnitOperationError): operate_and_compare(4 * m, operand, op, 4 * m) with pytest.raises(UnitOperationError): operate_and_compare(operand, 4 * m, op, -4 * m) def test_multiplication(): """ Test multiplication of two unyt_arrays """ # Same units a1 = unyt_array([1, 2, 3], "cm") a2 = unyt_array([4, 5, 6], "cm") a3 = [4 * cm, 5 * cm, 6 * cm] answer = unyt_array([4, 10, 18], "cm**2") operate_and_compare(a1, a2, operator.mul, answer) operate_and_compare(a2, a1, operator.mul, answer) operate_and_compare(a1, a3, operator.mul, answer) operate_and_compare(a3, a1, operator.mul, answer) operate_and_compare(a1, a2, np.multiply, answer) operate_and_compare(a2, a1, np.multiply, answer) operate_and_compare(a1, a3, np.multiply, answer) operate_and_compare(a3, a1, np.multiply, answer) # different units, same dimension a1 = unyt_array([1, 2, 3], "cm") a2 = unyt_array([4, 5, 6], "m") a3 = [4 * m, 5 * m, 6 * m] answer1 = unyt_array([400, 1000, 1800], "cm**2") answer2 = unyt_array([0.04, 0.10, 0.18], "m**2") answer3 = unyt_array([4, 10, 18], "cm*m") operate_and_compare(a1, a2, operator.mul, answer1) operate_and_compare(a2, a1, operator.mul, answer2) operate_and_compare(a1, a3, operator.mul, answer1) operate_and_compare(a3, a1, operator.mul, answer2) operate_and_compare(a1, a2, np.multiply, answer3) operate_and_compare(a2, a1, np.multiply, answer3) operate_and_compare(a1, a3, np.multiply, answer3) operate_and_compare(a3, a1, np.multiply, answer3) # different dimensions a1 = unyt_array([1, 2, 3], "cm") a2 = unyt_array([4, 5, 6], "g") a3 = [4 * g, 5 * g, 6 * g] answer = unyt_array([4, 10, 18], "cm*g") operate_and_compare(a1, a2, operator.mul, answer) operate_and_compare(a2, a1, operator.mul, answer) operate_and_compare(a1, a3, operator.mul, answer) operate_and_compare(a3, a1, operator.mul, answer) operate_and_compare(a1, a2, np.multiply, answer) operate_and_compare(a2, a1, np.multiply, answer) operate_and_compare(a1, a3, np.multiply, answer) operate_and_compare(a3, a1, np.multiply, answer) # One dimensionless, one unitful a1 = unyt_array([1, 2, 3], "cm") a2 = array([4, 5, 6]) a3 = [4, 5, 6] answer = unyt_array([4, 10, 18], "cm") operate_and_compare(a1, a2, operator.mul, answer) operate_and_compare(a2, a1, operator.mul, answer) operate_and_compare(a1, a3, operator.mul, answer) operate_and_compare(a3, a1, operator.mul, answer) operate_and_compare(a1, a2, np.multiply, answer) operate_and_compare(a2, a1, np.multiply, answer) operate_and_compare(a1, a3, np.multiply, answer) operate_and_compare(a3, a1, np.multiply, answer) # Both dimensionless quantities a1 = unyt_array([1, 2, 3]) a2 = array([4, 5, 6]) a3 = [4, 5, 6] answer = unyt_array([4, 10, 18]) operate_and_compare(a1, a2, operator.mul, answer) operate_and_compare(a2, a1, operator.mul, answer) operate_and_compare(a1, a3, operator.mul, answer) operate_and_compare(a3, a1, operator.mul, answer) operate_and_compare(a1, a2, np.multiply, answer) operate_and_compare(a2, a1, np.multiply, answer) operate_and_compare(a1, a3, np.multiply, answer) operate_and_compare(a3, a1, np.multiply, answer) # With np.multiply.reduce a = unyt_array([1.0, 2.0, 3.0], "cm") answer = unyt_quantity(6.0, "cm**3") assert_equal(np.multiply.reduce(a), answer) a = unyt_array([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]], "cm") answer = unyt_array([6.0, 120.0], "cm**3") assert_equal(np.multiply.reduce(a, axis=1), answer) def test_division(): """ Test division of two unyt_arrays """ # Same units a1 = unyt_array([1.0, 2.0, 3.0], "cm") a2 = unyt_array([4.0, 5.0, 6.0], "cm") a3 = [4 * cm, 5 * cm, 6 * cm] answer1 = unyt_array([0.25, 0.4, 0.5]) answer2 = unyt_array([4, 2.5, 2]) op = operator.truediv operate_and_compare(a1, a2, op, answer1) operate_and_compare(a2, a1, op, answer2) operate_and_compare(a1, a3, op, answer1) operate_and_compare(a3, a1, op, answer2) operate_and_compare(a1, a2, np.divide, answer1) operate_and_compare(a2, a1, np.divide, answer2) operate_and_compare(a1, a3, np.divide, answer1) operate_and_compare(a3, a1, np.divide, answer2) # different units, same dimension a1 = unyt_array([1.0, 2.0, 3.0], "cm") a2 = unyt_array([4.0, 5.0, 6.0], "m") a3 = [4 * m, 5 * m, 6 * m] answer1 = unyt_array([0.0025, 0.004, 0.005]) answer2 = unyt_array([400, 250, 200]) operate_and_compare(a1, a2, op, answer1) operate_and_compare(a2, a1, op, answer2) operate_and_compare(a1, a3, op, answer1) operate_and_compare(a3, a1, op, answer2) operate_and_compare(a1, a2, np.divide, answer1) operate_and_compare(a2, a1, np.divide, answer2) operate_and_compare(a1, a3, np.divide, answer1) operate_and_compare(a3, a1, np.divide, answer2) # different dimensions a1 = unyt_array([1.0, 2.0, 3.0], "cm") a2 = unyt_array([4.0, 5.0, 6.0], "g") a3 = [4 * g, 5 * g, 6 * g] answer1 = unyt_array([0.25, 0.4, 0.5], "cm/g") answer2 = unyt_array([4, 2.5, 2], "g/cm") operate_and_compare(a1, a2, op, answer1) operate_and_compare(a2, a1, op, answer2) operate_and_compare(a1, a3, op, answer1) operate_and_compare(a3, a1, op, answer2) operate_and_compare(a1, a2, np.divide, answer1) operate_and_compare(a2, a1, np.divide, answer2) operate_and_compare(a1, a3, np.divide, answer1) operate_and_compare(a3, a1, np.divide, answer2) # One dimensionless, one unitful a1 = unyt_array([1.0, 2.0, 3.0], "cm") a2 = array([4.0, 5.0, 6.0]) a3 = [4, 5, 6] answer1 = unyt_array([0.25, 0.4, 0.5], "cm") answer2 = unyt_array([4, 2.5, 2], "1/cm") operate_and_compare(a1, a2, op, answer1) operate_and_compare(a2, a1, op, answer2) operate_and_compare(a1, a3, op, answer1) operate_and_compare(a3, a1, op, answer2) operate_and_compare(a1, a2, np.divide, answer1) operate_and_compare(a2, a1, np.divide, answer2) operate_and_compare(a1, a3, np.divide, answer1) operate_and_compare(a3, a1, np.divide, answer2) # Both dimensionless quantities a1 = unyt_array([1.0, 2.0, 3.0]) a2 = array([4.0, 5.0, 6.0]) a3 = [4, 5, 6] answer1 = unyt_array([0.25, 0.4, 0.5]) answer2 = unyt_array([4, 2.5, 2]) operate_and_compare(a1, a2, op, answer1) operate_and_compare(a2, a1, op, answer2) operate_and_compare(a1, a3, op, answer1) operate_and_compare(a3, a1, op, answer2) operate_and_compare(a1, a3, np.divide, answer1) operate_and_compare(a3, a1, np.divide, answer2) operate_and_compare(a1, a3, np.divide, answer1) operate_and_compare(a3, a1, np.divide, answer2) # With np.multiply.reduce a = unyt_array([3.0, 2.0, 1.0], "cm") answer = unyt_quantity(1.5, "cm**-1") assert_equal(np.divide.reduce(a), answer) a = unyt_array([[3.0, 2.0, 1.0], [6.0, 5.0, 4.0]], "cm") answer = unyt_array([1.5, 0.3], "cm**-1") assert_equal(np.divide.reduce(a, axis=1), answer) def test_power(): """ Test power operator ensure units are correct. """ from unyt import cm cm_arr = np.array([1.0, 1.0]) * cm cm_quant = 1.0 * cm assert_equal((1 * cm) ** 3, unyt_quantity(1, "cm**3")) assert_equal(np.power((1 * cm), 3), unyt_quantity(1, "cm**3")) assert_equal((1 * cm) ** unyt_quantity(3), unyt_quantity(1, "cm**3")) with pytest.raises(UnitOperationError): np.power((1 * cm), unyt_quantity(3, "g")) with pytest.raises(InvalidUnitOperation): np.power(cm, cm) assert_equal(cm_arr**3, unyt_array([1, 1], "cm**3")) assert_equal(np.power(cm_arr, 3), unyt_array([1, 1], "cm**3")) assert_equal(cm_arr ** unyt_quantity(3), unyt_array([1, 1], "cm**3")) with pytest.raises(UnitOperationError): np.power(cm_arr, unyt_quantity(3, "g")) try: np.power(cm_arr, unyt_quantity(3, "g")) except UnitOperationError as err: assert isinstance(err.unit1, Unit) assert isinstance(err.unit2, Unit) # when the power is 0.0 numpy short-circuits via ones_like so we # need to test the special handling for that case assert_array_equal_units(cm_quant**0, unyt_quantity(1.0, "dimensionless")) assert_array_equal_units(cm_arr**0, unyt_quantity(1.0, "dimensionless")) @pytest.mark.parametrize( "p", [ pytest.param(unyt_array(0), id="scalar power 0"), pytest.param(unyt_array(1), id="scalar power 1"), pytest.param(unyt_array([0, 0, 0, 0]), id="uniform power 0"), pytest.param(unyt_array([1, 1, 1, 1]), id="uniform power 1"), pytest.param(unyt_array([0, 1, 2, 3]), id="non-uniform power"), ], ) def test_dimensionless_array_power(p): # see https://github.com/yt-project/unyt/issues/522 a = unyt_array([1, 2, 3, 4]) expected = a.value**p.value assert_array_equal(expected, (a**p).value) @pytest.mark.parametrize( "a, p", [ pytest.param( unyt_array([0, 1, 2, 3], "cm"), unyt_array([0, 1, 2, 3]), id="non-uniform power with arr non dimensionless", ), pytest.param( unyt_array([0, 1, 2, 3]), unyt_array([0, 0, 0, 0], "cm"), id="non dimensionless power", ), ], ) def test_invalid_array_power(a, p): # see https://github.com/yt-project/unyt/issues/522 with pytest.raises(UnitOperationError): a**p def test_comparisons(): """ Test numpy ufunc comparison operators for unit consistency. """ from unyt.array import unyt_array a1 = unyt_array([1, 2, 3], "cm") a2 = unyt_array([2, 1, 3], "cm") a3 = unyt_array([0.02, 0.01, 0.03], "m") a4 = unyt_array([1, 2, 3], "g") dimless = np.array([2, 1, 3]) ops = (np.less, np.less_equal, np.greater, np.greater_equal, np.equal, np.not_equal) answers = ( [True, False, False], [True, False, True], [False, True, False], [False, True, True], [False, False, True], [True, True, False], ) for op, answer in zip(ops, answers): operate_and_compare(a1, a2, op, answer) for op, answer in zip(ops, answers): operate_and_compare(a1, dimless, op, answer) for op, answer in zip(ops, answers): operate_and_compare(a1, a3, op, answer) for op, answer in zip(ops, answers): operate_and_compare(a1, a3.in_units("cm"), op, answer) # Check that comparisons with dimensionless quantities work in both # directions. operate_and_compare(a3, dimless, np.less, [True, True, True]) operate_and_compare(dimless, a3, np.less, [False, False, False]) assert_equal(a1 < 2, [True, False, False]) assert_equal(a1 < 2, np.less(a1, 2)) assert_equal(2 < a1, [False, False, True]) assert_equal(2 < a1, np.less(2, a1)) # Check that comparisons with arrays that have different units with # different dimensions work properly operate_and_compare(a1, a4, np.equal, [False, False, False]) operate_and_compare(a1, a4, np.not_equal, [True, True, True]) # check that comparing quantities returns bools and not 0-D arrays el1, el4 = a1[0], a4[0] assert (el1 == el4) is False assert (el1 != el4) is True # comparisons that aren't == and != with pytest.raises(UnitOperationError): np.greater(a1, a4) with pytest.raises(UnitOperationError): a1 > a4 # noqa: B015 with pytest.raises(UnitOperationError): np.greater(el1, el4) with pytest.raises(UnitOperationError): el1 > el4 # noqa: B015 def test_unit_conversions(): """ Test operations that convert to different units or cast to ndarray """ from unyt.array import unyt_quantity from unyt.unit_object import Unit km = unyt_quantity(1.0, "km", dtype="float64") km_in_cm = km.in_units("cm") cm_unit = Unit("cm") kpc_unit = Unit("kpc") assert_equal(km_in_cm, km) assert_equal(km_in_cm.in_cgs(), 1e5) assert_equal(km_in_cm.in_mks(), 1e3) assert_equal(km_in_cm.units, cm_unit) km_view = km.ndarray_view() km.convert_to_units("cm") assert km_view.base is km.base assert_equal(km, unyt_quantity(1, "km")) assert_equal(km.in_cgs(), 1e5) assert_equal(km.in_mks(), 1e3) assert_equal(km.units, cm_unit) km.convert_to_units("kpc") assert km_view.base is km.base assert_array_almost_equal(km, unyt_quantity(1, "km")) assert_array_almost_equal(km.in_cgs(), unyt_quantity(1e5, "cm")) assert_array_almost_equal(km.in_mks(), unyt_quantity(1e3, "m")) assert_equal(km.units, kpc_unit) assert_isinstance(km.to_ndarray(), np.ndarray) assert_isinstance(km.ndarray_view(), np.ndarray) dyne = unyt_quantity(1.0, "dyne") assert_equal(dyne.in_cgs(), dyne) assert_equal(dyne.in_cgs(), 1.0) assert_equal(dyne.in_mks(), dyne) assert_equal(dyne.in_mks(), 1e-5) assert_equal(str(dyne.in_mks().units), "N") assert_equal(str(dyne.in_cgs().units), "dyn") em3 = unyt_quantity(1.0, "erg/m**3") assert_equal(em3.in_cgs(), em3) assert_equal(em3.in_cgs(), 1e-6) assert_equal(em3.in_mks(), em3) assert_equal(em3.in_mks(), 1e-7) assert_equal(str(em3.in_mks().units), "Pa") assert_equal(str(em3.in_cgs().units), "dyn/cm**2") em3_converted = unyt_quantity(1545436840.386756, "Msun/(Myr**2*kpc)") assert_equal(em3.in_base(unit_system="galactic"), em3) assert_array_almost_equal(em3.in_base(unit_system="galactic"), em3_converted) assert_equal(str(em3.in_base(unit_system="galactic").units), "Msun/(Myr**2*kpc)") dimless = unyt_quantity(1.0, "") assert_equal(dimless.in_cgs(), dimless) assert_equal(dimless.in_cgs(), 1.0) assert_equal(dimless.in_mks(), dimless) assert_equal(dimless.in_mks(), 1.0) assert_equal(str(dimless.in_cgs().units), "dimensionless") kg = unyt_quantity(1.0, "kg") assert kg.to(g).v == 1000 assert kg.in_units(g).v == 1000 kg.convert_to_units(g) assert kg.v == 1000 ten_grams = 10 * g assert kg.to(ten_grams).v == 100 assert kg.in_units(ten_grams).v == 100 kg.convert_to_units(ten_grams) assert kg.v == 100 with pytest.raises(UnitParseError): kg.to([1, 2] * g) with pytest.raises(UnitParseError): kg.in_units([1, 2] * g) with pytest.raises(UnitParseError): kg.convert_to_units([1, 2] * g) def test_temperature_conversions(): """ Test conversions between various supported temperatue scales. Also ensure we only allow compound units with temperature scales that have a proper zero point. """ from unyt.unit_object import InvalidUnitOperation km = unyt_quantity(1, "km", dtype="float64") balmy = unyt_quantity(300, "K", dtype="float64") balmy_F = unyt_quantity(80.33, "degF") balmy_C = unyt_quantity(26.85, "degC") balmy_R = unyt_quantity(540, "R") assert_array_almost_equal(balmy.in_units("degF").d, balmy_F.d) assert balmy.in_units("degF").units, balmy_F.units assert_array_almost_equal(balmy.in_units("degC").d, balmy_C.d) assert balmy.in_units("degC").units, balmy_C.units assert_array_almost_equal(balmy.in_units("R").d, balmy_R.d) assert balmy.in_units("R").units == balmy_R.units balmy_view = balmy.ndarray_view() balmy.convert_to_units("degF") assert balmy_view.base is balmy.base assert_array_almost_equal(np.array(balmy), np.array(balmy_F)) balmy.convert_to_units("degC") assert balmy_view.base is balmy.base assert_array_almost_equal(np.array(balmy), np.array(balmy_C)) balmy.convert_to_units("R") assert balmy_view.base is balmy.base assert_array_almost_equal(np.array(balmy), np.array(balmy_R)) balmy.convert_to_units("degF") assert balmy_view.base is balmy.base assert_array_almost_equal(np.array(balmy), np.array(balmy_F)) with pytest.raises(InvalidUnitOperation): np.multiply(balmy, km) with pytest.raises(InvalidUnitOperation): np.multiply(balmy, balmy) with pytest.raises(InvalidUnitOperation): np.multiply(balmy_F, balmy_F) with pytest.raises(InvalidUnitOperation): np.multiply(balmy_F, balmy_C) with pytest.raises(InvalidUnitOperation): np.divide(balmy, balmy) with pytest.raises(InvalidUnitOperation): np.divide(balmy_F, balmy_F) with pytest.raises(InvalidUnitOperation): np.divide(balmy_F, balmy_C) with pytest.raises(InvalidUnitOperation): balmy * km with pytest.raises(InvalidUnitOperation): balmy * balmy with pytest.raises(InvalidUnitOperation): balmy_F * balmy_F with pytest.raises(InvalidUnitOperation): balmy_F * balmy_C with pytest.raises(InvalidUnitOperation): 2 * balmy_F with pytest.raises(InvalidUnitOperation): balmy / balmy with pytest.raises(InvalidUnitOperation): balmy_F / balmy_F with pytest.raises(InvalidUnitOperation): balmy_F / balmy_C assert np.add(balmy_F, balmy_F) == unyt_quantity(80.33 * 2, "degF") with pytest.raises(InvalidUnitOperation): np.add(balmy_F, balmy_C) with pytest.raises(InvalidUnitOperation): balmy_F + balmy_C assert_equal(np.subtract(balmy_C, balmy_C), unyt_quantity(0, "degC")) with pytest.raises(InvalidUnitOperation): np.subtract(balmy_F, balmy_C) with pytest.raises(InvalidUnitOperation): balmy_F - balmy_C # Does CGS conversion from F to K work? assert_array_almost_equal(balmy.in_cgs(), unyt_quantity(300, "K")) def test_unyt_array_unyt_quantity_ops(): """ Test operations that combine unyt_array and unyt_quantity """ a = unyt_array(range(10, 1), "cm") b = unyt_quantity(5, "g") assert_isinstance(a * b, unyt_array) assert_isinstance(b * a, unyt_array) assert_isinstance(a / b, unyt_array) assert_isinstance(b / a, unyt_array) assert_isinstance(a * a, unyt_array) assert_isinstance(a / a, unyt_array) assert_isinstance(b * b, unyt_quantity) assert_isinstance(b / b, unyt_quantity) def test_selecting(): """ Test slicing of two unyt_arrays """ a = unyt_array(range(10), "cm") a_slice = a[:3] a_fancy_index = a[[1, 1, 3, 5]] a_array_fancy_index = a[array([[1, 1], [3, 5]])] a_boolean_index = a[a > 5] a_selection = a[0] assert_array_equal(a_slice, unyt_array([0, 1, 2], "cm")) assert_equal(a_slice.units, a.units) assert_array_equal(a_fancy_index, unyt_array([1, 1, 3, 5], "cm")) assert_equal(a_fancy_index.units, a.units) assert_array_equal(a_array_fancy_index, unyt_array([[1, 1], [3, 5]], "cm")) assert_equal(a_array_fancy_index.units, a.units) assert_array_equal(a_boolean_index, unyt_array([6, 7, 8, 9], "cm")) assert_equal(a_boolean_index.units, a.units) assert_isinstance(a_selection, unyt_quantity) assert_equal(a_selection.units, a.units) # .base points to the original array for a numpy view. If it is not a # view, .base is None. assert a_slice.base is a def test_iteration(): """ Test that iterating over a unyt_array returns a sequence of unyt_quantity instances """ a = np.arange(3) b = unyt_array(np.arange(3), "cm") for ia, ib in zip(a, b): assert_equal(ia, ib.value) assert_equal(ib.units, b.units) def test_unyt_array_pickle(): test_data = [unyt_quantity(12.0, "cm"), unyt_array([1, 2, 3], "km")] for data in test_data: tempf = tempfile.NamedTemporaryFile(delete=False) pickle.dump(data, tempf) tempf.close() with open(tempf.name, "rb") as fname: loaded_data = pickle.load(fname) os.unlink(tempf.name) assert_array_equal(data, loaded_data) assert_equal(data.units, loaded_data.units) assert_array_equal(array(data.in_cgs()), array(loaded_data.in_cgs())) assert_equal(float(data.units.base_value), float(loaded_data.units.base_value)) SYMPY_VERSION = Version(version("sympy")) def test_copy(): quan = unyt_quantity(1, "g") arr = unyt_array([1, 2, 3], "cm") assert_equal(copy.copy(quan), quan) assert_array_equal(copy.copy(arr), arr) assert_equal(copy.deepcopy(quan), quan) assert_array_equal(copy.deepcopy(arr), arr) memo = {} assert_equal(copy.deepcopy(quan, memo), quan) assert_array_equal(copy.deepcopy(arr), arr) assert_equal(quan.copy(), quan) assert_array_equal(arr.copy(), arr) assert_equal(np.copy(quan), quan) assert_array_equal(np.copy(arr), arr) # needed so the tests function on older numpy versions that have # different sets of ufuncs def yield_np_ufuncs(ufunc_list): for u in ufunc_list: ufunc = getattr(np, u, None) if ufunc is not None: yield ufunc def unary_ufunc_comparison(ufunc, a): out = a.copy() a_array = a.to_ndarray() if ufunc in (np.isreal, np.iscomplex): # According to the numpy docs, these two explicitly do not do # in-place copies. ret = ufunc(a) assert not hasattr(ret, "units") assert_array_equal(ret, ufunc(a)) elif ufunc in yield_np_ufuncs( [ "exp", "exp2", "log", "log2", "log10", "expm1", "log1p", "sin", "cos", "tan", "arcsin", "arccos", "arctan", "sinh", "cosh", "tanh", "arccosh", "arcsinh", "arctanh", "deg2rad", "rad2deg", "isfinite", "isinf", "isnan", "signbit", "sign", "rint", "logical_not", ] ): # These operations should return identical results compared to numpy. with np.errstate(invalid="ignore"): ret = ufunc(a, out=out) assert_array_equal(ret, out) assert_array_equal(ret, ufunc(a_array)) # In-place copies do not drop units. assert hasattr(out, "units") assert not hasattr(ret, "units") elif ufunc in yield_np_ufuncs( [ "absolute", "fabs", "conjugate", "floor", "ceil", "trunc", "negative", "spacing", "positive", ] ): ret = ufunc(a, out=out) assert_array_equal(ret, out) assert_array_equal(ret.to_ndarray(), ufunc(a_array)) assert ret.units == out.units elif ufunc in yield_np_ufuncs( ["ones_like", "square", "sqrt", "cbrt", "reciprocal"] ): if ufunc is np.ones_like: ret = ufunc(a) else: with np.errstate(invalid="ignore"): ret = ufunc(a, out=out) assert_array_equal(ret, out) with np.errstate(invalid="ignore"): assert_array_equal(ret.to_ndarray(), ufunc(a_array)) if ufunc is np.square: assert out.units == a.units**2 assert ret.units == a.units**2 elif ufunc is np.sqrt: assert out.units == a.units**0.5 assert ret.units == a.units**0.5 elif ufunc is np.cbrt: assert out.units == a.units ** (1.0 / 3.0) assert ret.units == a.units ** (1.0 / 3.0) elif ufunc is np.reciprocal: assert out.units == a.units**-1 assert ret.units == a.units**-1 elif ufunc is np.modf: ret1, ret2 = ufunc(a) npret1, npret2 = ufunc(a_array) assert_array_equal(ret1.to_ndarray(), npret1) assert_array_equal(ret2.to_ndarray(), npret2) elif ufunc is np.frexp: ret1, ret2 = ufunc(a) npret1, npret2 = ufunc(a_array) assert_array_equal(ret1, npret1) assert_array_equal(ret2, npret2) elif ufunc is np.invert: with pytest.raises(TypeError): ufunc(a.astype("int64")) elif hasattr(np, "isnat") and ufunc is np.isnat: # numpy 1.13 raises ValueError, numpy 1.14 and newer raise TypeError with pytest.raises((TypeError, ValueError)): ufunc(a) # no untested ufuncs assert ufunc in yield_np_ufuncs( [ "isreal", "iscomplex", "exp", "exp2", "log", "log2", "log10", "expm1", "log1p", "sin", "cos", "tan", "arcsin", "arccos", "arctan", "sinh", "cosh", "tanh", "arccosh", "arcsinh", "arctanh", "deg2rad", "rad2deg", "isfinite", "isinf", "isnan", "signbit", "sign", "rint", "logical_not", "absolute", "fabs", "conjugate", "floor", "ceil", "trunc", "negative", "spacing", "positive", "ones_like", "square", "sqrt", "cbrt", "reciprocal", "invert", "isnat", "modf", "frexp", ] ) def binary_ufunc_comparison(ufunc, a, b): if ufunc in [np.divmod]: out = (b.copy(), b.copy()) else: out = b.copy() if ufunc in yield_np_ufuncs( [ "add", "subtract", "remainder", "fmod", "mod", "arctan2", "hypot", "greater", "greater_equal", "less", "less_equal", "logical_and", "logical_or", "logical_xor", "maximum", "minimum", "fmax", "fmin", "nextafter", "heaviside", ] ): if a.units != b.units and a.units.dimensions != b.units.dimensions: with pytest.raises(UnitOperationError): ufunc(a, b) return if ufunc in yield_np_ufuncs( [ "bitwise_and", "bitwise_or", "bitwise_xor", "left_shift", "right_shift", "ldexp", ] ): with pytest.raises(TypeError): ufunc(a, b) return ret = ufunc(a, b, out=out) ret = ufunc(a, b) if ufunc is np.multiply: assert ret.units == (a.units * b.units).simplify().as_coeff_unit()[1] elif ufunc in (np.divide, np.true_divide, np.arctan2): assert ret.units.dimensions == (a.units / b.units).dimensions elif ufunc in ( np.greater, np.greater_equal, np.less, np.less_equal, np.not_equal, np.equal, np.logical_and, np.logical_or, np.logical_xor, ): assert not isinstance(ret, unyt_array) and isinstance(ret, np.ndarray) if isinstance(ret, tuple): assert isinstance(out, tuple) assert len(out) == len(ret) for o, r in zip(out, ret): assert_array_equal(r, o) else: assert_array_equal(ret, out) if ufunc in (np.divide, np.true_divide, np.arctan2) and ( a.units.dimensions == b.units.dimensions ): assert_array_almost_equal( np.array(ret), ufunc(np.array(a.in_cgs()), np.array(b.in_cgs())) ) def test_ufuncs(): for ufunc in unary_operators: unary_ufunc_comparison( ufunc, unyt_array([0.3, 0.4, 0.5], "cm", dtype="float64") ) unary_ufunc_comparison(ufunc, unyt_array([12, 23, 47], "g", dtype="float64")) unary_ufunc_comparison( ufunc, unyt_array([2, 4, -6], "erg/m**3", dtype="float64") ) for ufunc in binary_operators: # arr**arr is undefined for arrays with units because # each element of the result would have different units. if ufunc is np.power: a = unyt_array([0.3, 0.4, 0.5], "cm") b = unyt_array([0.1, 0.2, 0.3], "dimensionless") c = np.array(b) d = unyt_array([1.0, 2.0, 3.0], "g") with pytest.raises(UnitOperationError): ufunc(a, b) with pytest.raises(UnitOperationError): ufunc(a, c) with pytest.raises(UnitOperationError): ufunc(a, d) binary_ufunc_comparison(ufunc, np.array(2.0), b) continue a = unyt_array([0.3, 0.4, 0.5], "cm") b = unyt_array([0.1, 0.2, 0.3], "cm") c = unyt_array([0.1, 0.2, 0.3], "m") d = unyt_array([0.1, 0.2, 0.3], "g") e = unyt_array([0.1, 0.2, 0.3], "erg/m**3") for pair in itertools.product([a, b, c, d, e], repeat=2): binary_ufunc_comparison(ufunc, pair[0], pair[1]) def test_dot_matmul(): arr = unyt_array([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]], "cm") ev_result = arr.dot(unyt_array([1.0, 2.0, 3.0], "kg")) matmul_result = arr @ unyt_array([1.0, 2.0, 3.0], "kg") res = unyt_array([14.0, 32.0], "cm*kg") assert_equal(ev_result, res) assert_equal(ev_result.units, res.units) assert_isinstance(ev_result, unyt_array) assert_equal(matmul_result, res) assert_equal(matmul_result.units, res.units) assert_isinstance(matmul_result, unyt_array) ev_result = arr.dot(np.array([1.0, 2.0, 3.0])) matmul_result = arr @ np.array([1.0, 2.0, 3.0]) res = unyt_array([14.0, 32.0], "cm") assert_equal(ev_result, res) assert_equal(ev_result.units, res.units) assert_isinstance(ev_result, unyt_array) assert_equal(matmul_result, res) assert_equal(matmul_result.units, res.units) assert_isinstance(matmul_result, unyt_array) ev_result = arr.dot(arr.T) matmul_result = arr @ arr.T res = unyt_array([[14.0, 32.0], [32.0, 77.0]], "cm**2") assert_equal(ev_result, res) assert_equal(ev_result.units, res.units) assert_isinstance(ev_result, unyt_array) assert_equal(matmul_result, res) assert_equal(matmul_result.units, res.units) assert_isinstance(matmul_result, unyt_array) ev_result = arr.v.dot(arr.T) matmul_result = arr.v @ arr.T res = unyt_array([[14.0, 32.0], [32.0, 77.0]], "cm") assert_equal(ev_result, res) assert_equal(ev_result.units, res.units) assert_isinstance(ev_result, unyt_array) assert_equal(matmul_result, res) assert_equal(matmul_result.units, res.units) assert_isinstance(matmul_result, unyt_array) ev_result = arr.dot(arr.T.v) matmul_result = arr @ arr.T.v res = unyt_array([[14.0, 32.0], [32.0, 77.0]], "cm") assert_equal(ev_result, res) assert_equal(ev_result.units, res.units) assert_isinstance(ev_result, unyt_array) assert_equal(matmul_result, res) assert_equal(matmul_result.units, res.units) assert_isinstance(matmul_result, unyt_array) arr = unyt_array([[1.0, 2.0], [3.0, 4.0]], "kg") arr.dot(arr.T, out=arr) res = unyt_array([[5.0, 11.0], [11.0, 25.0]], "kg**2") assert_equal(arr, res) assert_equal(arr.units, res.units) assert_isinstance(arr, unyt_array) qv = unyt_array([1, 2, 3], "cm").dot(unyt_array([1, 2, 3], "cm")) mv = unyt_array([1, 2, 3], "cm") @ unyt_array([1, 2, 3], "cm") qa = unyt_quantity(14, "cm**2") assert qv == qa assert qv.units == qa.units assert_isinstance(qv, unyt_quantity) assert mv == qa assert mv.units == qa.units assert_isinstance(mv, unyt_quantity) qv = unyt_array([1, 2, 3], "cm").dot(np.array([1, 2, 3])) mv = unyt_array([1, 2, 3], "cm") @ np.array([1, 2, 3]) qa = unyt_quantity(14, "cm") assert qv == qa assert qv.units == qa.units assert_isinstance(qv, unyt_quantity) assert mv == qa assert mv.units == qa.units assert_isinstance(mv, unyt_quantity) def test_reductions(): arr = unyt_array([[1, 2, 3], [4, 5, 6]], "cm") answers = { "prod": ( unyt_quantity(720, "cm**6"), unyt_array([4, 10, 18], "cm**2"), unyt_array([6, 120], "cm**3"), ), "sum": ( unyt_quantity(21, "cm"), unyt_array([5.0, 7.0, 9.0], "cm"), unyt_array([6, 15], "cm"), ), "mean": ( unyt_quantity(3.5, "cm"), unyt_array([2.5, 3.5, 4.5], "cm"), unyt_array([2, 5], "cm"), ), "std": ( unyt_quantity(1.707825127659933, "cm"), unyt_array([1.5, 1.5, 1.5], "cm"), unyt_array([0.81649658, 0.81649658], "cm"), ), } for op, (result1, result2, result3) in answers.items(): ev_result = getattr(arr, op)() assert_almost_equal(ev_result, result1) assert_equal(ev_result.units, result1.units) assert_isinstance(ev_result, unyt_quantity) for axis, result in [(0, result2), (1, result3), (-1, result3)]: ev_result = getattr(arr, op)(axis=axis) assert_almost_equal(ev_result, result) assert_equal(ev_result.units, result.units) assert_isinstance(ev_result, unyt_array) def test_convenience(): for orig in [ [1.0, 2.0, 3.0], (1.0, 2.0, 3.0), np.array([1.0, 2.0, 3.0]), [[1.0], [2.0], [3.0]], np.array([[1.0], [2.0], [3.0]]), [[1.0, 2.0, 3.0]], np.array([[1.0, 2.0, 3.0]]), ]: arr = unyt_array(orig, "cm") arrou = unyt_array(orig, "1/cm") uoarr = unyt_array(1.0 / np.array(orig), "cm") assert_equal(arr.unit_quantity, unyt_quantity(1, "cm")) assert_equal(arr.uq, unyt_quantity(1, "cm")) assert_isinstance(arr.unit_quantity, unyt_quantity) assert_isinstance(arr.uq, unyt_quantity) assert_array_equal(arr.unit_array, unyt_array(np.ones_like(arr), "cm")) assert_array_equal(arr.ua, unyt_array(np.ones_like(arr), "cm")) assert_isinstance(arr.unit_array, unyt_array) assert_isinstance(arr.ua, unyt_array) for u in [arr.units, arr.unit_quantity, arr.unit_array, arr.uq, arr.ua]: assert_array_equal(u * orig, arr) assert_array_equal(orig * u, arr) assert_array_equal(orig / u, arrou) assert_array_equal(u / orig, uoarr) assert_array_equal(arr.ndview, arr.view(np.ndarray)) assert_array_equal(arr.d, arr.view(np.ndarray)) assert arr.ndview.base is arr.base assert arr.d.base is arr.base assert_array_equal(arr.value, np.array(arr)) assert_array_equal(arr.v, np.array(arr)) def test_registry_association(): reg = UnitRegistry() a = unyt_quantity(3, "cm", registry=reg) b = unyt_quantity(4, "m") c = unyt_quantity(6, "", registry=reg) d = 5 assert_equal(id(a.units.registry), id(reg)) def binary_op_registry_comparison(op): e = op(a, b) f = op(b, a) g = op(c, d) h = op(d, c) assert_equal(id(e.units.registry), id(reg)) assert_equal(id(f.units.registry), id(b.units.registry)) assert_equal(id(g.units.registry), id(h.units.registry)) assert_equal(id(g.units.registry), id(reg)) def unary_op_registry_comparison(op): c = op(a) d = op(b) assert_equal(id(c.units.registry), id(reg)) assert_equal(id(d.units.registry), id(b.units.registry)) binary_ops = [operator.add, operator.sub, operator.mul, operator.truediv] for op in binary_ops: binary_op_registry_comparison(op) for op in [operator.abs, operator.neg, operator.pos]: unary_op_registry_comparison(op) def test_to_value(): a = unyt_array([1.0, 2.0, 3.0], "kpc") assert_equal(a.to_value(), np.array([1.0, 2.0, 3.0])) assert_equal(a.to_value(), a.value) assert_equal(a.to_value("km"), a.in_units("km").value) b = unyt_quantity(5.5, "Msun") assert_equal(b.to_value(), 5.5) assert_equal(b.to_value("g"), b.in_units("g").value) def test_astropy(): pytest.importorskip("astropy") ap_arr = np.arange(10) * _astropy.units.km / _astropy.units.hr un_arr = unyt_array(np.arange(10), "km/hr") un_arr2 = unyt_array.from_astropy(ap_arr) ap_quan = 10.0 * _astropy.units.Msun**0.5 / (_astropy.units.kpc**3) un_quan = unyt_quantity(10.0, "sqrt(Msun)/kpc**3") un_quan2 = unyt_quantity.from_astropy(ap_quan) assert_array_equal(ap_arr, un_arr.to_astropy()) assert_array_equal(un_arr, unyt_array.from_astropy(ap_arr)) assert_array_equal(un_arr, un_arr2) assert_equal(ap_quan, un_quan.to_astropy()) assert_equal(un_quan, unyt_quantity.from_astropy(ap_quan)) assert_equal(un_quan, un_quan2) assert_array_equal(un_arr, unyt_array.from_astropy(un_arr.to_astropy())) assert_equal(un_quan, unyt_quantity.from_astropy(un_quan.to_astropy())) def test_astropy_dimensionless(): # see https://github.com/yt-project/unyt/issues/436 pytest.importorskip("astropy") arr = unyt_array([1, 2, 3], "") ap_arr = np.array([1, 2, 3]) * _astropy.units.Unit("") assert_array_equal(ap_arr, arr.to_astropy()) assert_array_equal(arr, unyt_array.from_astropy(ap_arr)) def test_pint(): pytest.importorskip("pint") def assert_pint_array_equal(arr1, arr2): assert_array_equal(arr1.magnitude, arr2.magnitude) assert str(arr1.units) == str(arr2.units) ureg = _pint.UnitRegistry() p_arr = np.arange(10) * ureg.km / ureg.year un_arr = unyt_array(np.arange(10), "km/yr") un_arr2 = unyt_array.from_pint(p_arr) p_quan = 10.0 * ureg.g**0.5 / (ureg.mm**3) un_quan = unyt_quantity(10.0, "sqrt(g)/mm**3") un_quan2 = unyt_quantity.from_pint(p_quan) assert_pint_array_equal(p_arr, un_arr.to_pint()) assert_array_equal(un_arr, unyt_array.from_pint(p_arr)) assert_array_equal(un_arr, un_arr2) assert_pint_array_equal(p_quan, un_quan.to_pint()) assert_equal(un_quan, unyt_quantity.from_pint(p_quan)) assert_equal(un_quan, un_quan2) assert_array_equal(un_arr, unyt_array.from_pint(un_arr.to_pint())) assert_equal(un_quan, unyt_quantity.from_pint(un_quan.to_pint())) def test_subclass(): class unyt_a_subclass(unyt_array): def __new__( cls, input_array, units=None, registry=None, bypass_validation=None ): return super().__new__( cls, input_array, units, registry=registry, bypass_validation=bypass_validation, ) a = unyt_a_subclass([4, 5, 6], "g") b = unyt_a_subclass([7, 8, 9], "kg") nu = unyt_a_subclass([10, 11, 12], "") nda = np.array([3, 4, 5]) yta = unyt_array([6, 7, 8], "mg") loq = [unyt_quantity(6, "mg"), unyt_quantity(7, "mg"), unyt_quantity(8, "mg")] ytq = unyt_quantity(4, "cm") ndf = np.float64(3) def op_comparison(op, inst1, inst2, compare_class): assert_isinstance(op(inst1, inst2), compare_class) assert_isinstance(op(inst2, inst1), compare_class) ops = [operator.mul, operator.truediv] for op in ops: for inst in (b, ytq, ndf, yta, nda, loq): op_comparison(op, a, inst, unyt_a_subclass) op_comparison(op, ytq, nda, unyt_array) op_comparison(op, ytq, yta, unyt_array) for op in (operator.add, operator.sub): op_comparison(op, nu, nda, unyt_a_subclass) op_comparison(op, a, b, unyt_a_subclass) op_comparison(op, a, yta, unyt_a_subclass) op_comparison(op, a, loq, unyt_a_subclass) assert_isinstance(a[0], unyt_quantity) assert_isinstance(a[:], unyt_a_subclass) assert_isinstance(a[:2], unyt_a_subclass) assert_isinstance(unyt_a_subclass(yta), unyt_a_subclass) assert_isinstance(a.to("kg"), unyt_a_subclass) assert_isinstance(a.copy(), unyt_a_subclass) assert_isinstance(copy.deepcopy(a), unyt_a_subclass) def test_string_operations_raise_errors(): a = unyt_array([1, 2, 3], "g") with pytest.raises(IterableUnitCoercionError): a + "hello" with pytest.raises(IterableUnitCoercionError): a * "hello" with pytest.raises(IterableUnitCoercionError): a ** "hello" def test_string_ne(): a = unyt_array([1, 2, 3], "g") if NUMPY_VERSION >= Version("1.25.0.dev0"): ctx = pytest.raises(ValueError) else: ctx = pytest.warns(FutureWarning) with ctx: assert a != "hello" def test_string_operations_raise_errors_quantity(): q = 2 * g with pytest.raises(IterableUnitCoercionError): q + "hello" with pytest.raises(IterableUnitCoercionError): q * "hello" with pytest.raises(IterableUnitCoercionError): q ** "hello" assert q != "hello" def test_h5_io(): pytest.importorskip("h5py") tmpdir = tempfile.mkdtemp() curdir = os.getcwd() os.chdir(tmpdir) reg = UnitRegistry() reg.add("code_length", 10.0, dimensions.length) rng = np.random.default_rng() warr = unyt_array(rng.random((256, 256)), "code_length", registry=reg) warr.write_hdf5("test.h5") iarr = unyt_array.from_hdf5("test.h5") assert_equal(warr, iarr) assert_equal(warr.units.registry["code_length"], iarr.units.registry["code_length"]) # test code to overwrite existing dataset warr.write_hdf5("test.h5") giarr = unyt_array.from_hdf5("test.h5") assert_equal(warr, giarr) # test code to overwrite existing dataset with data that has a different # shape rng = np.random.default_rng() warr = unyt_array(rng.random((255, 255)), "code_length", registry=reg) warr.write_hdf5("test.h5") giarr = unyt_array.from_hdf5("test.h5") assert_equal(warr, giarr) os.remove("test.h5") # write to a group that doesn't exist warr.write_hdf5( "test.h5", dataset_name="test_dset", group_name="/arrays/test_group" ) giarr = unyt_array.from_hdf5( "test.h5", dataset_name="test_dset", group_name="/arrays/test_group" ) assert_equal(warr, giarr) os.remove("test.h5") # write to a group that does exist with _h5py.File("test.h5", "a") as f: f.create_group("/arrays/test_group") warr.write_hdf5( "test.h5", dataset_name="test_dset", group_name="/arrays/test_group" ) giarr = unyt_array.from_hdf5( "test.h5", dataset_name="test_dset", group_name="/arrays/test_group" ) assert_equal(warr, giarr) os.remove("test.h5") os.chdir(curdir) shutil.rmtree(tmpdir) def test_h5_quantity_roundtrip(tmp_path): # regression test for https://github.com/yt-project/unyt/issues/559 pytest.importorskip("h5py") q1 = 5 * cm savefile = tmp_path / "savefile.hdf5" q1.write_hdf5( savefile, dataset_name="test_ds", group_name="test_group", ) q2 = unyt_quantity.from_hdf5( savefile, dataset_name="test_ds", group_name="test_group", ) assert_array_equal_units(q2, q1) def test_equivalencies(): import unyt as u # equivalence is ignored if the conversion doesn't need one data = 12.0 * u.g data.convert_to_equivalent("kg", None) assert data.value == 0.012 assert data.units == u.kg data = 12.0 * u.g data = data.to_equivalent("kg", None) assert data.value == 0.012 assert data.units == u.kg # incorrect usage of an equivalence raises errors with pytest.raises(InvalidUnitEquivalence): data.convert_to_equivalent("erg", "thermal") with pytest.raises(InvalidUnitEquivalence) as excinfo: data.convert_to_equivalent("m", "mass_energy") assert ( str(excinfo.value) == "The unit equivalence 'mass_energy: mass <-> energy' does not " "exist for units 'kg' to convert to a new unit with dimensions " "'(length)'." ) with pytest.raises(InvalidUnitEquivalence): data.to_equivalent("erg", "thermal") with pytest.raises(InvalidUnitEquivalence): data.to_equivalent("m", "mass_energy") # Mass-energy mp = u.mp.copy() mp.convert_to_units("keV", "mass_energy") assert_allclose_units(u.mp.in_units("keV", "mass_energy"), mp) assert_allclose_units(mp, u.mp * u.clight * u.clight) assert_allclose_units(u.mp, mp.in_units("g", "mass_energy")) mp.convert_to_units("g", "mass_energy") assert_allclose_units(u.mp, mp) # Thermal T = 1e8 * u.K E = T.in_units("W*hr", "thermal") assert_allclose_units(E, (u.kboltz * T).in_units("W*hr")) assert_allclose_units(T, E.in_units("K", "thermal")) T.convert_to_units("W*hr", "thermal") assert_allclose_units(E, T) T.convert_to_units("K", "thermal") assert_allclose_units(T, 1e8 * u.K) # Spectral # wavelength to frequency lam = 4000 * u.angstrom nu = lam.in_units("Hz", "spectral") assert_allclose_units(nu, u.clight / lam) lam.convert_to_units("MHz", "spectral") assert_allclose_units(lam, nu) assert lam.units == u.MHz.units assert nu.units == u.Hz.units # wavelength to photon energy lam = 4000 * u.angstrom hnu = lam.in_units("erg", "spectral") assert_allclose_units(hnu, u.h_mks * u.clight / lam) lam.convert_to_units("eV", "spectral") assert_allclose_units(lam, hnu) assert lam.units == u.eV.units assert hnu.units == u.erg.units # wavelength to spatial frequency lam = 4000 * u.angstrom nubar = lam.in_units("1/angstrom", "spectral") assert_allclose_units(nubar, 1 / lam) lam.convert_to_units("1/cm", "spectral") assert_allclose_units(lam, nubar) assert lam.units == (1 / u.cm).units assert nubar.units == (1 / u.angstrom).units # frequency to wavelength nu = 1.0 * u.MHz lam = nu.to("km", "spectral") assert_allclose_units(lam, u.clight / nu) nu.convert_to_units("m", "spectral") assert_allclose_units(lam, nu) assert lam.units == u.km.units assert nu.units == u.m.units # frequency to spatial frequency nu = 1.0 * u.MHz nubar = nu.to("1/km", "spectral") assert_allclose_units(nubar, nu / u.clight) nu.convert_to_units("1/m", "spectral") assert_allclose_units(nubar, nu) assert nubar.units == (1 / u.km).units assert nu.units == (1 / u.m).units # frequency to photon energy nu = 1.0 * u.MHz E = nu.to("erg", "spectral") assert_allclose_units(E, u.h_mks * nu) nu.convert_to_units("J", "spectral") assert_allclose_units(nu, E) assert nu.units == u.J.units assert E.units == u.erg.units # photon energy to frequency E = 13.6 * u.eV nu = E.to("Hz", "spectral") assert_allclose_units(nu, E / u.h_mks) E.convert_to_units("MHz", "spectral") assert_allclose_units(nu, E) assert E.units == u.MHz.units assert nu.units == u.Hz.units # photon energy to wavelength E = 13.6 * u.eV lam = E.to("nm", "spectral") assert_allclose_units(lam, u.h_mks * u.clight / E) E.convert_to_units("angstrom", "spectral") assert_allclose_units(E, lam) assert E.units == u.angstrom.units assert lam.units == u.nm.units # photon energy to spatial frequency E = 13.6 * u.eV nubar = E.to("1/nm", "spectral") assert_allclose_units(nubar, E / (u.h_mks * u.clight)) E.convert_to_units("1/angstrom", "spectral") assert_allclose_units(E, nubar) assert E.units == (1 / u.angstrom).units assert nubar.units == (1 / u.nm).units # spatial frequency to frequency nubar = 1500.0 / u.cm nu = nubar.to("Hz", "spectral") assert_allclose_units(nu, nubar * u.clight) nubar.convert_to_units("MHz", "spectral") assert_allclose_units(nu, nubar) assert nubar.units == u.MHz.units assert nu.units == u.Hz.units # spatial frequency to wavelength nubar = 1500.0 / u.cm lam = nubar.to("nm", "spectral") assert_allclose_units(lam, 1 / nubar) nubar.convert_to_units("angstrom", "spectral") assert_allclose_units(nubar, lam) assert nubar.units == u.angstrom.units assert lam.units == u.nm.units # spatial frequency to photon energy nubar = 1500.0 / u.cm E = nubar.to("erg", "spectral") assert_allclose_units(E, u.h_mks * u.clight * nubar) nubar.convert_to_units("J", "spectral") assert_allclose_units(nubar, E) assert nubar.units == u.J.units assert E.units == u.erg.units # Sound-speed # tempearature <-> velocity mu = 0.6 gg = 5.0 / 3.0 T = 1e8 * u.K c_s = T.in_units("km/s", equivalence="sound_speed") assert_allclose_units(c_s, np.sqrt(gg * u.kboltz * T / (mu * u.mh))) assert_allclose_units(T, c_s.in_units("K", "sound_speed")) T.convert_to_units("m/s", "sound_speed") assert_allclose_units(c_s, T) assert T.units == u.m.units / u.s.units assert c_s.units == u.km.units / u.s.units mu = 0.5 gg = 4.0 / 3.0 T = 1e8 * u.K c_s = T.in_units("km/s", "sound_speed", mu=mu, gamma=gg) assert_allclose_units(c_s, np.sqrt(gg * u.kboltz * T / (mu * u.mh))) assert_allclose_units(T, c_s.in_units("K", "sound_speed", mu=mu, gamma=gg)) T.convert_to_units("m/s", "sound_speed", mu=mu, gamma=gg) assert_allclose_units(c_s, T) assert T.units == u.m.units / u.s.units assert c_s.units == u.km.units / u.s.units # temperature <-> energy mu = 0.5 gg = 4.0 / 3.0 T = 1e8 * u.K kT = T.in_units("eV", "sound_speed", mu=mu, gamma=gg) assert_allclose_units(kT, u.kboltz * T) T.convert_to_units("erg", "sound_speed", mu=mu, gamma=gg) assert_allclose_units(T, kT) assert T.units == u.erg.units assert kT.units == u.eV.units assert_allclose_units(T.in_units("K", "sound_speed", mu=mu, gamma=gg), 1e8 * u.K) kT.convert_to_units("K", "sound_speed", mu=mu, gamma=gg) assert_allclose_units(kT, 1e8 * u.K) # velocity <-> energy c_s = 300 * u.m / u.s kT = c_s.in_units("erg", "sound_speed", mu=mu, gamma=gg) assert_allclose_units(kT, c_s**2 * mu * u.mh / gg) c_s.convert_to_units("J", "sound_speed", mu=mu, gamma=gg) assert_allclose_units(c_s, kT) assert c_s.units == u.J.units assert kT.units == u.erg.units assert_allclose_units( kT.in_units("m/s", "sound_speed", mu=mu, gamma=gg), 300 * u.m / u.s ) c_s.convert_to_units("m/s", "sound_speed", mu=mu, gamma=gg) assert_allclose_units(c_s, 300 * u.m / u.s) # Lorentz v = 0.8 * u.clight g = v.in_units("dimensionless", "lorentz") g2 = unyt_quantity(1.0 / np.sqrt(1.0 - 0.8 * 0.8), "dimensionless") assert_allclose_units(g, g2) v.convert_to_units("", "lorentz") assert_allclose_units(v, g2) v.convert_to_units("c", "lorentz") v2 = g2.in_units("mile/hr", "lorentz") assert_allclose_units(v2, v.in_units("mile/hr")) # Schwarzschild msun = 1.0 * u.unit_symbols.Msun msun.convert_to_equivalent("km", "schwarzschild") R = u.mass_sun_mks.in_units("kpc", "schwarzschild") assert_allclose_units(msun, R) assert_allclose_units(R.in_mks(), 2 * u.G * u.mass_sun_mks / (u.clight**2)) assert_allclose_units(u.mass_sun_mks, R.in_units("kg", "schwarzschild")) R.convert_to_units("Msun", "schwarzschild") assert_allclose_units(u.mass_sun_mks, R) assert R.units == u.unit_symbols.Msun.units assert msun.units == u.km.units # Compton me = 1.0 * u.me me.convert_to_units("nm", "compton") length = u.me.in_units("angstrom", "compton") assert_allclose_units(length, me) assert_allclose_units(length, u.h_mks / (u.me * u.clight)) assert_allclose_units(u.me, length.in_units("g", "compton")) assert me.units == u.nm.units assert length.units == u.angstrom.units me.convert_to_units("me", "compton") assert_almost_equal(me.value, 1.0) # Number density rho = u.mp / u.m**3 n = rho.in_units("m**-3", "number_density") assert_allclose_units(n, rho / (u.mh * 0.6)) assert_allclose_units(rho, n.in_units("kg/m**3", "number_density")) rho.convert_to_units("cm**-3", "number_density") assert rho.units == (1 / u.cm**3).units assert n.units == (1 / u.m**3).units assert_allclose_units(n, rho) rho.convert_to_units("kg/m**3", "number_density") assert_allclose_units(u.mp / u.m**3, rho) assert rho.units == (u.kg / u.m**3).units rho = u.mp / u.m**3 n = rho.in_units("m**-3", equivalence="number_density", mu=0.75) assert_allclose_units(n, rho / (u.mh * 0.75)) assert_allclose_units( rho, n.in_units("kg/m**3", equivalence="number_density", mu=0.75) ) rho.convert_to_units("cm**-3", "number_density", mu=0.75) assert rho.units == (1 / u.cm**3).units assert n.units == (1 / u.m**3).units assert_allclose_units(n, rho) rho.convert_to_units("kg/m**3", "number_density", mu=0.75) assert_allclose_units(u.mp / u.m**3, rho) assert rho.units == (u.kg / u.m**3).units # Effective temperature T = 1e4 * u.K F = T.in_units("W/m**2", equivalence="effective_temperature") assert_allclose_units(F, u.stefan_boltzmann_constant * T**4) assert_allclose_units(T, F.in_units("K", equivalence="effective_temperature")) T.convert_to_units("erg/s/cm**2", "effective_temperature") assert_allclose_units(T, F) assert T.units == u.Unit("erg/cm**2/s") assert F.units == u.W / u.m**2 assert_almost_equal(T.in_units("K", "effective_temperature").value, 1e4) T.convert_to_units("K", "effective_temperature") assert_almost_equal(T.value, 1e4) assert T.units == u.K # to_value test assert_allclose_units( F.value, T.to_value("W/m**2", equivalence="effective_temperature") ) assert_allclose_units( n.value, rho.to_value("m**-3", equivalence="number_density", mu=0.75) ) def test_electromagnetic(): import unyt as u # Various tests of SI and CGS electromagnetic units t = 1.0 * u.Tesla g = 1.0 * u.gauss assert t.to("gauss") == 1e4 * u.gauss assert g.to("T") == 1e-4 * u.Tesla assert t.in_mks() == t assert g.in_cgs() == g t.convert_to_mks() assert t == 1.0 * u.Tesla g.convert_to_cgs() assert g == 1.0 * u.gauss qp_mks = u.qp_cgs.in_units("C") assert_equal(qp_mks.units.dimensions, dimensions.charge_mks) assert_almost_equal(qp_mks.v, 10.0 * u.qp.v / speed_of_light_cm_per_s) qp = 1.0 * u.qp_cgs assert_equal(qp, u.qp_cgs.in_units("esu")) qp.convert_to_units("C") assert_equal(qp.units.dimensions, dimensions.charge_mks) assert_almost_equal(qp.v, 10 * u.qp.v / u.clight.v) qp_cgs = u.qp.in_units("esu") assert_array_almost_equal(qp_cgs, u.qp_cgs) assert_equal(qp_cgs.units.dimensions, u.qp_cgs.units.dimensions) qp = u.qp.copy() qp.convert_to_units("esu") assert_almost_equal(qp_cgs, qp_cgs) assert qp.units == u.esu.units qp.convert_to_units("C") assert_almost_equal(u.qp, qp) assert qp.units == u.C.units qp_mks_k = u.qp_cgs.in_units("kC") assert_array_almost_equal(qp_mks_k.v, 1.0e-2 * u.qp_cgs.v / speed_of_light_cm_per_s) qp = 1.0 * u.qp_cgs qp.convert_to_units("kC") assert_almost_equal(qp, qp_mks_k) B = 1.0 * u.T B_cgs = B.in_units("gauss") assert_equal(B.units.dimensions, dimensions.magnetic_field_mks) assert_equal(B_cgs.units.dimensions, dimensions.magnetic_field_cgs) assert_array_almost_equal(B_cgs, unyt_quantity(1.0e4, "gauss")) B_cgs = B.in_cgs() assert_equal(B.units.dimensions, dimensions.magnetic_field_mks) assert_equal(B_cgs.units.dimensions, dimensions.magnetic_field_cgs) assert_array_almost_equal(B_cgs, unyt_quantity(1.0e4, "gauss")) B_cgs = B.in_base("cgs") assert_equal(B.units.dimensions, dimensions.magnetic_field_mks) assert_equal(B_cgs.units.dimensions, dimensions.magnetic_field_cgs) assert_array_almost_equal(B_cgs, unyt_quantity(1.0e4, "gauss")) B.convert_to_cgs() assert_almost_equal(B, B_cgs) B.convert_to_mks() B_cgs2 = B.to("gauss") assert_almost_equal(B_cgs, B_cgs2) B_mks2 = B_cgs2.to("T") assert_almost_equal(B, B_mks2) B = 1.0 * u.T u_mks = B * B / (2 * u.mu_0) assert_equal(u_mks.units.dimensions, dimensions.pressure) u_cgs = B_cgs * B_cgs / (8 * np.pi) assert_equal(u_mks, u_cgs.to(u_mks.units)) assert_equal(u_mks.to(u_cgs.units), u_cgs) assert_equal(u_mks.in_cgs(), u_cgs) assert_equal(u_cgs.in_mks(), u_mks) current = 1.0 * u.A I_cgs = current.in_units("statA") assert_array_almost_equal( I_cgs, unyt_quantity(0.1 * speed_of_light_cm_per_s, "statA") ) assert_array_almost_equal(I_cgs.in_units("mA"), current.in_units("mA")) assert_equal(I_cgs.units.dimensions, dimensions.current_cgs) current.convert_to_units("statA") assert current.units == u.statA.units current.convert_to_units("A") assert current.units == u.A.units I_cgs2 = current.to("statA") assert I_cgs2.units == u.statA.units assert_array_almost_equal( I_cgs2, unyt_quantity(0.1 * speed_of_light_cm_per_s, "statA") ) current = 1.0 * u.A R = unyt_quantity(1.0, "ohm") R_cgs = R.in_units("statohm") P_mks = current * current * R P_cgs = I_cgs * I_cgs * R_cgs assert_equal(P_mks.units.dimensions, dimensions.power) assert_equal(P_cgs.units.dimensions, dimensions.power) assert_almost_equal(P_cgs.in_cgs(), P_cgs) assert_almost_equal(P_mks.in_cgs(), P_cgs) assert_almost_equal(P_cgs.in_mks(), P_mks) assert_almost_equal(P_mks.in_mks(), P_mks) V = unyt_quantity(1.0, "statV") V_mks = V.in_units("V") assert_array_almost_equal(V_mks.v, 1.0e8 * V.v / speed_of_light_cm_per_s) data = 1.0 * u.C * u.T * u.V with pytest.raises(UnitConversionError): data.to("statC*G*statV") with pytest.raises(UnitConversionError): data.convert_to_units("statC*G*statV") with pytest.raises(UnitsNotReducible): data.in_cgs() data = 1.0 * u.statC * u.G * u.statV with pytest.raises(UnitConversionError): data.to("C*T*V") with pytest.raises(UnitConversionError): data.convert_to_units("C*T*V") assert_almost_equal(data.in_mks(), 6.67408e-18 * u.m**5 / u.s**4) mu_0 = 4.0e-7 * math.pi * u.N / u.A**2 eps_0 = 8.85418781782e-12 * u.m**-3 / u.kg * u.s**4 * u.A**2 assert_almost_equal((1.0 / (u.clight**2 * mu_0)).in_units(eps_0.units), eps_0) def test_unyt_array_coercion(): a = unyt_array([1, 2, 3], "cm") q = unyt_quantity(3, "cm") na = np.array([1, 2, 3]) assert_isinstance(a * q, unyt_array) assert_isinstance(q * na, unyt_array) assert_isinstance(q * 3, unyt_quantity) assert_isinstance(q * np.float64(3), unyt_quantity) assert_isinstance(q * np.array(3), unyt_quantity) def test_numpy_wrappers(): a1 = unyt_array([1, 2, 3], "cm") a2 = unyt_array([2, 3, 4, 5, 6], "cm") a3 = unyt_array([[1, 2, 3], [4, 5, 6]], "cm") a4 = unyt_array([7, 8, 9, 10, 11], "cm") catenate_answer = [1, 2, 3, 2, 3, 4, 5, 6] intersect_answer = [2, 3] union_answer = [1, 2, 3, 4, 5, 6] vstack_answer = [[2, 3, 4, 5, 6], [7, 8, 9, 10, 11]] vstack_answer_last_axis = [[2, 7], [3, 8], [4, 9], [5, 10], [6, 11]] cross_answer = [-2, 4, -2] norm_answer = np.sqrt(1**2 + 2**2 + 3**2) arr_norm_answer = [norm_answer, np.sqrt(4**2 + 5**2 + 6**2)] dot_answer = 14 with pytest.warns(DeprecationWarning): assert_array_equal(unyt_array(catenate_answer, "cm"), uconcatenate((a1, a2))) assert_array_equal(catenate_answer, np.concatenate((a1, a2))) with pytest.warns(DeprecationWarning): assert_array_equal(unyt_array(intersect_answer, "cm"), uintersect1d(a1, a2)) assert_array_equal(intersect_answer, np.intersect1d(a1, a2)) with pytest.warns(DeprecationWarning): assert_array_equal(unyt_array(union_answer, "cm"), uunion1d(a1, a2)) assert_array_equal(union_answer, np.union1d(a1, a2)) with pytest.warns(DeprecationWarning): assert_array_equal( unyt_array(cross_answer, "cm**2"), ucross(a1, a1 + (2 * a1.units)) ) assert_array_equal(cross_answer, np.cross(a1.v, a1.v + 2)) with pytest.warns(DeprecationWarning): assert_array_equal(unorm(a1), unyt_quantity(norm_answer, "cm")) assert_array_equal(np.linalg.norm(a1), norm_answer) with pytest.warns(DeprecationWarning): assert_array_equal(unorm(a3, axis=1), unyt_array(arr_norm_answer, "cm")) assert_array_equal(np.linalg.norm(a3, axis=1), arr_norm_answer) with pytest.warns(DeprecationWarning): assert_array_equal(udot(a1, a1), unyt_quantity(dot_answer, "cm**2")) with pytest.warns(DeprecationWarning): assert_array_equal(np.array(catenate_answer), uconcatenate((a1.v, a2.v))) with pytest.raises(RuntimeError): with pytest.warns(DeprecationWarning): uconcatenate((a1, a2.v)) with pytest.raises(RuntimeError): with pytest.warns(DeprecationWarning): uconcatenate((a1.to("m"), a2)) with pytest.warns(DeprecationWarning): assert_array_equal(unyt_array(vstack_answer, "cm"), uvstack([a2, a4])) assert_array_equal(vstack_answer, np.vstack([a2, a4])) with pytest.warns(DeprecationWarning): assert_array_equal(unyt_array(vstack_answer, "cm"), ustack([a2, a4])) assert_array_equal(vstack_answer, np.stack([a2, a4])) with pytest.warns(DeprecationWarning): assert_array_equal( unyt_array(vstack_answer_last_axis, "cm"), ustack([a2, a4], axis=-1) ) assert_array_equal(vstack_answer_last_axis, np.stack([a2, a4], axis=-1)) def test_dimensionless_conversion(): a = unyt_quantity(1, "Zsun") b = a.in_units("Zsun") a.convert_to_units("Zsun") assert a.units.base_value == metallicity_sun assert b.units.base_value == metallicity_sun def test_modified_unit_division(): reg1 = UnitRegistry() reg2 = UnitRegistry() reg1.modify("g", 50) a = unyt_quantity(3, "g", registry=reg1) b = unyt_quantity(3, "g", registry=reg2) ret = a / b assert ret == 50000.0 assert ret.units.is_dimensionless assert ret.units.base_value == 1.0 def test_loadtxt_and_savetxt(): tmpdir = tempfile.mkdtemp() curdir = os.getcwd() os.chdir(tmpdir) rng = np.random.default_rng() a = unyt_array(rng.random(10), "kpc") b = unyt_array(rng.random(10), "Msun") c = unyt_array(rng.random(10), "km/s") savetxt("arrays.dat", [a, b, c], delimiter=",") d, e = loadtxt("arrays.dat", usecols=(1, 2), delimiter=",") assert_array_equal(b, d) assert_array_equal(c, e) # adding newlines to the file doesn't matter savetxt("arrays.dat", [a, b, c], delimiter=",") with open("arrays.dat", "r+") as f: content = f.read() f.seek(0, 0) f.write("\n" + content) d, e = loadtxt("arrays.dat", usecols=(1, 2), delimiter=",") assert_array_equal(b, d) assert_array_equal(c, e) # data saved by numpy savetxt are loaded without units np.savetxt("arrays.dat", np.squeeze(np.transpose([a.v, b.v, c.v])), delimiter=",") d, e = loadtxt("arrays.dat", usecols=(1, 2), delimiter=",") assert_array_equal(b.v, d) assert_array_equal(c.v, e) # save a single array savetxt("arrays.dat", a) d = loadtxt("arrays.dat") assert_array_equal(a, d) # save an array with no units and an array with units with a header savetxt("arrays.dat", [a.v, b], header="this is a header!") d, e = loadtxt("arrays.dat") assert_array_equal(a.v, d) assert_array_equal(b, e) os.chdir(curdir) shutil.rmtree(tmpdir) def test_trig_ufunc_degrees(): rng = np.random.default_rng() for ufunc in (np.sin, np.cos, np.tan): degree_values = rng.random(10) * degree radian_values = degree_values.in_units("radian") assert_array_equal(ufunc(degree_values), ufunc(radian_values)) def test_builtin_sum(): from unyt import km arr = [1, 2, 3] * km assert_equal(sum(arr), 6 * km) def test_initialization_different_registries(): reg1 = UnitRegistry() reg2 = UnitRegistry() reg1.add("code_length", 1.0, dimensions.length) reg2.add("code_length", 3.0, dimensions.length) l1 = unyt_quantity(1.0, "code_length", registry=reg1) l2 = unyt_quantity(1.0, "code_length", registry=reg2) assert_almost_equal(float(l1.in_mks()), 1.0) assert_almost_equal(float(l2.in_mks()), 3.0) def test_ones_and_zeros_like(): data = unyt_array([1, 2, 3], "cm") zd = np.zeros_like(data) od = np.ones_like(data) assert_equal(zd, unyt_array([0, 0, 0], "cm")) assert_equal(zd.units, data.units) assert_equal(od, unyt_array([1, 1, 1], "cm")) assert_equal(od.units, data.units) def test_coerce_iterable(): from unyt import cm, g, m a = unyt_array([1, 2, 3], "cm") b = [1 * cm, 2 * m, 3 * cm] c = [1 * g, 2 * m, 3 * cm] assert_equal(a + b, unyt_array([2, 202, 6], "cm")) assert_equal(b + a, unyt_array([2, 202, 6], "cm")) with pytest.raises(IterableUnitCoercionError): a + c with pytest.raises(IterableUnitCoercionError): c + a assert_equal(unyt_array(b), unyt_array([1, 200, 3], "cm")) with pytest.raises(IterableUnitCoercionError): unyt_array(c) def test_bypass_validation(): from unyt import UnitRegistry, cm, unyt_array obj = unyt_array(np.array([1.0, 2.0, 3.0]), cm, bypass_validation=True) assert obj.units is cm reg = UnitRegistry() obj = unyt_array( np.array([1.0, 2.0, 3.0]), cm, registry=reg, bypass_validation=True ) assert obj.units == cm assert obj.units.registry is reg def test_creation(): from unyt import UnitRegistry, cm data = [1, 2, 3] * cm new_data = unyt_array(data) assert new_data.units is cm assert_array_equal(new_data.v, np.array([1, 2, 3], dtype="float64")) reg = UnitRegistry() new_data = unyt_array(data, registry=reg) assert_array_equal(new_data.v, np.array([1, 2, 3], dtype="float64")) assert new_data.units is not cm assert new_data.units == cm assert new_data.units.registry is reg new_data = unyt_array([1, 2, 3], cm) assert_array_equal(new_data.v, np.array([1, 2, 3], dtype="float64")) assert new_data.units is cm new_data = unyt_array([1, 2, 3], cm, registry=reg) assert_array_equal(new_data.v, np.array([1, 2, 3], dtype="float64")) assert new_data.units is not cm assert new_data.units == cm assert new_data.units.registry is reg with pytest.raises(RuntimeError): unyt_quantity("hello", "cm") with pytest.raises(RuntimeError): unyt_quantity(np.array([1, 2, 3]), "cm") def test_round(): from unyt import km assert_equal(round(3.3 * km), 3.0) assert_equal(round(3.5 * km), 4.0) assert_equal(round(3 * km), 3) assert_equal(round(3.7 * km), 4) with pytest.raises(TypeError): round([1, 2, 3] * km) @pytest.mark.parametrize("itemsize", (8, 16, 32, 64)) def test_conversion_from_int_types(itemsize): a = unyt_array([1], "cm", dtype=f"int{itemsize}") # check copy conversion a.in_units("m") # check in place conversion if itemsize == 8: with pytest.raises( ValueError, match=re.escape( "Can't convert memory buffer in place. " "Input dtype (int8) has a smaller itemsize than the " "smallest floating point representation possible." ), ): a.convert_to_units("m") else: a.convert_to_units("m") assert a.dtype == f"float{itemsize}" def test_integer_arrays(): from unyt import km, m, mile, ms, s def integer_semantics(inp): arr = inp * km assert arr.dtype == np.int_ arr = np.array(inp, dtype="int32") * km assert arr.dtype.name == "int32" ret = arr.in_units("mile") assert arr.dtype.name == "int32" answer = (inp * km).astype("int32").to("mile") assert_array_equal(ret, answer) assert ret.dtype.name == "float32" ret = arr.in_units("m") assert arr.dtype != ret.dtype assert ret.dtype.name == "float32" arr.convert_to_units("m") assert arr.dtype.name == "float32" arr = inp * km arr.convert_to_units("mile") assert arr.dtype.name == "float" + str(np.int_().dtype.itemsize * 8) for foo in [[1, 2, 3], 12, -8, 0, [1, -2, 3]]: integer_semantics(foo) arr1 = [1, 2, 3] * km arr2 = [4, 5, 6] * mile assert (arr1 + arr2).dtype.name == "float64" assert (arr1 * arr2).dtype == np.int_ assert (arr1 / arr2).dtype.name == "float64" arr1 = [1, 2, 3] * km arr2 = [4, 5, 6] * m assert (arr1 + arr2).dtype.name == "float64" assert (arr1 * arr2).dtype == np.int_ assert (arr1 / arr2).dtype.name == "float64" arr1 = [1, 2, 3] * km arr2 = [4, 5, 6] * km assert (arr1 + arr2).dtype == np.int_ assert (arr1 * arr2).dtype == np.int_ assert (arr1 / arr2).dtype.name == "float64" # see issue #118 for details assert 1000 * ms == 1 * s assert 1 * s == 1000 * ms def test_overflow_warnings(): from unyt import km data = [2**53, 2**54] * km message = "Overflow encountered while converting to units 'mile'" _process_warning(data.to, message, RuntimeWarning, ("mile",)) _process_warning(data.in_units, message, RuntimeWarning, ("mile",)) _process_warning(data.convert_to_units, message, RuntimeWarning, ("mile",)) def test_clip(): from unyt import km data = [1, 2, 3, 4, 5, 6] * km answer = [2, 2, 3, 4, 4, 4] * km ret = np.clip(data, 2, 4) assert_array_equal(ret, answer) assert ret.units == answer.units np.clip(data, 2, 4, out=data) assert_array_equal(data, answer) assert data.units == answer.units left_edge = [0.0, 0.0, 0.0] * km right_edge = [1.0, 1.0, 1.0] * km positions = [[0.0, 0.0, 0.0], [1.0, 1.0, -0.1], [1.5, 1.0, 0.9]] * km np.clip(positions, left_edge, right_edge, positions) assert positions.units == left_edge.units assert positions.max() == 1.0 * km assert positions.min() == 0.0 * km def test_name_attribute(): a = unyt_array([0, 1, 2], "s") assert a.name is None a.name = "time" assert a.name == "time" assert a[0].name == "time" a.convert_to_units("ms") assert a.name == "time" b = unyt_quantity(1, "m", name="distance") assert b.name == "distance" c = b.copy() assert c.name == "distance" c_1 = copy.deepcopy(b) assert c_1.name == "distance" d = b.in_units("mm") assert d.name == "distance" e = b.to("mm") assert e.name == "distance" f = unyt_array([3, 4, 5], "K", name="temperature") g = f.in_units("J", equivalence="thermal") assert g.name is None g_1 = f.to_equivalent("J", equivalence="thermal") assert g_1.name is None f.convert_to_equivalent("J", equivalence="thermal") assert f.name is None h = f.to("J", equivalence="thermal") assert h.name is None def test_neper_bel(): assert 0 * Unit("dB") + 20 * Unit("dB") == unyt_quantity(20, "dB") with pytest.raises(InvalidUnitOperation): unyt_array([1, 10], "V") * Unit("dB") with pytest.raises(InvalidUnitOperation): Unit("Np") * unyt_array([1, 10], "s") with pytest.raises(InvalidUnitOperation): unyt_array([0, 20], "dB") ** 2 with pytest.raises(InvalidUnitOperation): np.power(unyt_array([0, 20], "dB"), -2) def test_delta_degC(): t1 = 10 * degC t2 = 1 * K assert t1 + t2 == 11 * degC with pytest.raises(UnitOperationError): t2 + t1 t3 = 1 * delta_degC assert t1 + t3 == 11 * degC assert t3 + t1 == 11 * degC assert 1 * delta_degC + 2 * delta_degC == 3 * delta_degC assert 2 * delta_degC == unyt_quantity(2, "delta_degC") def test_delta_degF(): t1 = 10 * degF t2 = 1 * R assert t1 + t2 == 11 * degF with pytest.raises(UnitOperationError): t2 + t1 t3 = 1 * delta_degF assert t1 + t3 == 11 * degF assert t3 + t1 == 11 * degF assert 1 * delta_degF + 2 * delta_degF == 3 * delta_degF assert 2 * delta_degF == unyt_quantity(2, "delta_degF") @pytest.mark.parametrize( ("u0", "u1", "uout"), [ (K, K, K), (R, R, R), (degC, degC, delta_degC), (degF, degF, delta_degF), (degC, delta_degC, degC), (delta_degC, degC, degC), (degF, delta_degF, degF), (delta_degF, degF, degF), ], ) def test_delta_temperature_diff(u0, u1, uout): # using repr comparison because # 1) we don't care that Unit instances might not be the same # 2) some temperature units will compare as equal even when they are not identical (e.g. K and delta_degC) assert repr((2 * u0 - 1 * u1).units) == repr(uout) def test_mil(): assert_allclose_units(unyt_quantity(1, "mil"), unyt_quantity(0.001, "inch")) def test_kip(): assert_allclose_units(unyt_quantity(1, "lbf"), unyt_quantity(0.001, "kip")) def test_ksi(): assert_allclose_units(unyt_quantity(1, "lbf/inch**2"), unyt_quantity(0.001, "ksi")) def test_masked_array(): data = unyt_array([1, 2, 3], "s") mask = [False, False, True] marr = np.ma.MaskedArray(data, mask) assert_array_equal(marr.data, data) assert all(marr.mask == mask) assert marr.sum() == unyt_quantity(3, "s") assert np.ma.notmasked_contiguous(marr) == [slice(0, 2, None)] assert marr.argmax() == 1 assert marr.max() == unyt_quantity(2, "s") data = unyt_array([1, 2, np.inf], "s") marr = np.ma.MaskedArray(data) marr_masked = np.ma.masked_invalid(marr) assert all(marr_masked.mask == [False, False, True]) marr_masked.set_fill_value(unyt_quantity(3, "s")) assert_array_equal(marr_masked.filled(), unyt_array([1, 2, 3], "s")) marr_fixed = np.ma.fix_invalid(marr) assert_array_equal(marr_fixed.data, unyt_array([1, 2, 1e20], "s")) assert_array_equal(np.ma.filled(marr, unyt_quantity(3, "s")), data) assert_array_equal(np.ma.compressed(marr_masked), unyt_array([1, 2], "s")) # executing the repr should not raise an exception marr.__repr__() def test_complexvalued(tmp_path): freq = unyt_array([1j, 1j * 10], "Hz") arr = 1 / (Unit("F") * Unit("Ω") * freq) arr = arr.to("dimensionless") assert arr.units.is_dimensionless assert np.all(arr.v == np.asarray([-1j, -1j * 0.1])) arr = unyt_array([1j, 1j * 10], "mJ") arr.convert_to_base() assert_allclose_units(arr, unyt_array([1j * 0.001, 1j * 0.01], "J")) arr.convert_to_units("mJ") assert_allclose_units(arr, unyt_array([1j, 1j * 10], "mJ")) arr.convert_to_mks() assert_allclose_units(arr, unyt_array([1j * 0.001, 1j * 0.01], "J")) arr.convert_to_cgs() assert_allclose_units(arr, unyt_array([1j * 10000, 1j * 100000], "erg")) arr.convert_to_equivalent("K", "thermal") assert_allclose_units( arr, unyt_array([1j * 7.24297157e19, 1j * 7.24297157e20], "K") ) arr = arr.to_equivalent("J", "thermal") assert_allclose_units(arr, unyt_array([1j * 0.001, 1j * 0.01], "J")) assert_allclose_units(arr.to_ndarray(), np.asarray([1j * 0.001, 1j * 0.01])) assert_allclose_units(arr.to_value(), np.asarray([1j * 0.001, 1j * 0.01])) assert arr.tolist() == [1j * 0.001, 1j * 0.01] assert_allclose_units(arr.in_units("mJ"), unyt_array([1j, 1j * 10], "mJ")) assert_allclose_units(arr.in_base(), unyt_array([1j * 0.001, 1j * 0.01], "J")) assert_allclose_units(arr.in_cgs(), unyt_array([1j * 10000, 1j * 100000], "erg")) assert_allclose_units(arr.in_mks(), unyt_array([1j * 0.001, 1j * 0.01], "J")) fname = tmp_path / "testcomplexvalued.txt" savetxt(fname, arr) farr = loadtxt(fname, dtype=np.complex128) assert_allclose_units(farr, unyt_array([1j * 0.001, 1j * 0.01], "J")) def test_string_formatting(): d = unyt_array((1, 2, 3), "Msun") expected = "[1 2 3] Msun" assert f"{d}" == expected assert f"{d}" == expected @pytest.mark.parametrize( "s, expected, normalized", [ ("+1cm", 1.0 * Unit("cm"), "1 cm"), ("1cm", 1.0 * Unit("cm"), "1 cm"), ("1.cm", 1.0 * Unit("cm"), "1.0 cm"), ("1.0 cm", 1.0 * Unit("cm"), "1.0 cm"), ("1.0\tcm", 1.0 * Unit("cm"), "1.0 cm"), ("1.0\t cm", 1.0 * Unit("cm"), "1.0 cm"), ("1.0 cm", 1.0 * Unit("cm"), "1.0 cm"), ("1.0\t\tcm", 1.0 * Unit("cm"), "1.0 cm"), ("10e-1cm", 1.0 * Unit("cm"), "1.0 cm"), ("10E-1cm", 1.0 * Unit("cm"), "1.0 cm"), ("+1cm", 1.0 * Unit("cm"), "1 cm"), ("1um", 1.0 * Unit("μm"), "1 μm"), ("1μm", 1.0 * Unit("μm"), "1 μm"), ("-5 Msun", -5.0 * Unit("Msun"), "-5 Msun"), ("1e3km", 1e3 * Unit("km"), "1000.0 km"), ("-1e3 km", -1e3 * Unit("km"), "-1000.0 km"), ("1.0 g/cm**3", 1.0 * Unit("g/cm**3"), "1.0 g/cm**3"), ("1 g*cm**-3", 1.0 * Unit("g/cm**3"), "1 g/cm**3"), ("1.0 g*cm", 1.0 * Unit("g*cm"), "1.0 cm*g"), ("nan g", float("nan") * Unit("g"), "nan g"), ("-nan g", float("nan") * Unit("g"), "nan g"), ("inf g", float("inf") * Unit("g"), "inf g"), ("+inf g", float("inf") * Unit("g"), "inf g"), ("-inf g", -float("inf") * Unit("g"), "-inf g"), ("1", 1.0 * Unit(), "1 dimensionless"), ("g", 1.0 * Unit("g"), "1 g"), # from https://github.com/yt-project/unyt/issues/361 ("1 g**2/cm**2", 1.0 * Unit("g") ** 2 / Unit("cm") ** 2, "1 g**2/cm**2"), ("g**2/cm**2", 1.0 * Unit("g") ** 2 / Unit("cm") ** 2, "1 g**2/cm**2"), ("1*cm**2", 1.0 * Unit("cm") ** 2, "1 cm**2"), ("1/cm**2", 1.0 / Unit("cm") ** 2, "1 cm**(-2)"), ("1 / cm**2", 1.0 / Unit("cm") ** 2, "1 cm**(-2)"), ( "1e-3 g**2 / cm**2", 1e-3 * Unit("g") ** 2 / Unit("cm") ** 2, "0.001 g**2/cm**2", ), ], ) def test_valid_quantity_from_string(s, expected, normalized): actual = unyt_quantity.from_string(s) assert actual.to_string() == normalized roundtrip = unyt_quantity.from_string(actual.to_string()) if "nan" not in s: assert actual == expected assert roundtrip == expected assert actual.to_string() == normalized assert roundtrip.to_string() == normalized @pytest.mark.parametrize( "s", [ "++1cm", "--1cm", "cm10", "cm 10.", ".cm", "1cm**(-1", "1cm**/2", "1cm**3 hello", ], ) def test_invalid_expression_quantity_from_string(s): with pytest.raises(ValueError, match=r"^(Received invalid quantity expression )"): unyt_quantity.from_string(s) @pytest.mark.parametrize( "s", [ "10 cmmmm", "50. Km", ".6 MSUN", "infcm", # space sep is required here ], ) def test_invalid_unit_quantity_from_string(s): # using a lazy solution here # this test would need to be refactored if we want to add other cases # without a space separator between number and unit. un_str = s.split()[-1] with pytest.raises( UnitParseError, match=f"Could not find unit symbol '{un_str}' in the provided symbols.", ): unyt_quantity.from_string(s) def test_constant_type(): # see https://github.com/yt-project/unyt/issues/224 a = [1] * cm assert type(a) is unyt_array b = 2 * a assert type(b) is unyt_array def test_composite_meshgrid(): # see https://github.com/yt-project/unyt/issues/224 a = np.array(1) # pure numpy call to illustrate that the problem # is only with units np.meshgrid(np.array([1, 2]), a) np.meshgrid(np.array([1, 2]), a * m) @pytest.mark.parametrize( "shape, expected_output_shape", [ (1, (1,)), ((1,), (1,)), ((1, 1), (1, 1)), ((1, -1), (1, 1)), ], ) def test_reshape_quantity_to_array(shape, expected_output_shape): a = unyt_quantity(1, "m") b = a.reshape(shape) assert b.shape == expected_output_shape assert type(b) is unyt_array @pytest.mark.parametrize("shape", ((), None)) def test_reshape_quantity_noop(shape): a = unyt_quantity(1, "m") b = a.reshape(shape) assert b.shape == a.shape == () assert type(b) is unyt_quantity def test_reshape_quantity_via_shape_tuple(): # this is necessary to support np.tile a = unyt_quantity(1, "m") b = a.reshape(-1, 1) assert b.shape == (1, 1) assert type(b) is unyt_array def test_string_comparison(): # exercise comparison between a unyt_quantity object and a string # see regression https://github.com/numpy/numpy/issues/22744 a = 1 * cm assert not (a == "hello") assert a != "hello" def test_int8_comparison(): # see regression https://github.com/yt-project/unyt/issues/369 a = unyt_array(np.zeros(5, dtype=np.int8)) assert all(e == 0 for e in a) def test_setitem(): # see https://github.com/yt-project/unyt/issues/373 a = [1, 2, 3] * cm a[1] = 2 * m assert a[1].value == 200 assert a[1].units == cm with pytest.raises(UnitConversionError): a[1] = 2 * g a[1] = 2 assert a[1].value == 2 assert a[1].units == cm a[1] = unyt_quantity(2) assert a[1].value == 2 assert a[1].units == cm unyt-3.0.4/unyt/tests/test_unyt_testing.py000066400000000000000000000057721476461141700210510ustar00rootroot00000000000000""" Test unyt.testing module that contains utilities for writing tests. """ import pytest from unyt import accepts, meter, returns, second from unyt.array import unyt_array, unyt_quantity from unyt.dimensions import length, time from unyt.testing import assert_allclose_units def test_equality(): a1 = unyt_array([1.0, 2.0, 3.0], "cm") a2 = unyt_array([1.0, 2.0, 3.0], "cm") assert_allclose_units(a1, a2) def test_unequal_error(): a1 = unyt_array([1.0, 2.0, 3.0], "cm") a2 = unyt_array([4.0, 5.0, 6.0], "cm") with pytest.raises(AssertionError): assert_allclose_units(a1, a2) def test_conversion_error(): a1 = unyt_array([1.0, 2.0, 3.0], "cm") a2 = unyt_array([1.0, 2.0, 3.0], "kg") with pytest.raises(AssertionError): assert_allclose_units(a1, a2) def test_runtime_error(): a1 = unyt_array([1.0, 2.0, 3.0], "cm") a2 = unyt_array([1.0, 2.0, 3.0], "cm") with pytest.raises(RuntimeError): assert_allclose_units(a1, a2, rtol=unyt_quantity(1e-7, "cm")) def test_atol_conversion_error(): a1 = unyt_array([1.0, 2.0, 3.0], "cm") a2 = unyt_array([1.0, 2.0, 3.0], "cm") with pytest.raises(AssertionError): assert_allclose_units(a1, a2, atol=unyt_quantity(0.0, "kg")) def test_accepts(): @accepts(a=time, v=length / time) def foo(a, v): return a * v foo(a=2 * second, v=3 * meter / second) with pytest.raises(TypeError): foo(a=2 * meter, v=3 * meter / second) with pytest.raises(TypeError): foo(a=2 * second, v=3 * meter) def test_accepts_partial(): @accepts(a=time) def bar(a, v): return a * v bar(a=2 * second, v=3 * meter / second) bar(a=2 * second, v=3 * meter) with pytest.raises(TypeError): bar(a=2 * meter, v=3 * meter / second) @accepts(v=length / time) def baz(a, v): return a * v baz(a=2 * second, v=3 * meter / second) baz(a=2 * meter, v=3 * meter / second) with pytest.raises(TypeError): baz(a=2 * second, v=3 * meter) def test_returns(): @returns(length) def foo(a, v): return a * v # This usage is deprecated, but we still want to support it for now. with pytest.deprecated_call(): @returns(r_unit=length) def bar(a, v): return a * v for func in [foo, bar]: func(a=2 * second, v=3 * meter / second) with pytest.raises(TypeError): func(a=2 * meter, v=3 * meter / second) with pytest.raises(TypeError): func(a=2 * second, v=3 * meter) # We don't support a mixture of the two usage styles. with pytest.raises(ValueError): @returns(length, r_unit=time) def _(a, v): return a, v def test_returns_multiple(): @returns(time, length / time) def baz(a, v): return a, v baz(a=2 * second, v=3 * meter / second) with pytest.raises(TypeError): baz(a=2 * meter, v=3 * meter / second) with pytest.raises(TypeError): baz(a=2 * second, v=3 * meter) unyt-3.0.4/unyt/unit_object.py000066400000000000000000001054331476461141700164140ustar00rootroot00000000000000""" A class that represents a unit symbol. """ import copy import itertools import math from functools import lru_cache from numbers import Number as numeric_type import numpy as np from sympy import ( Add, Basic, Expr, Float, Mod, Mul, Number, Pow, Rational, Symbol, floor, latex, sympify, ) from sympy.core.numbers import One import unyt.dimensions as dims from unyt._parsing import parse_unyt_expr from unyt._physical_ratios import speed_of_light_cm_per_s from unyt.dimensions import ( angle, base_dimensions, current_mks, dimensionless, logarithmic, temperature, ) from unyt.equivalencies import equivalence_registry from unyt.exceptions import ( InvalidUnitOperation, MissingMKSCurrent, MKSCGSConversionError, UnitConversionError, UnitParseError, UnitsNotReducible, ) from unyt.unit_registry import _lookup_unit_symbol, default_unit_registry from unyt.unit_systems import _split_prefix sympy_one = sympify(1) def _get_latex_representation(expr, registry): symbol_table = {} for ex in expr.free_symbols: try: symbol_table[ex] = registry.lut[str(ex)][3] except KeyError: symbol_table[ex] = r"\rm{" + str(ex).replace("_", r"\ ") + "}" # invert the symbol table dict to look for keys with identical values invert_symbols = {} for key, value in symbol_table.items(): if value not in invert_symbols: invert_symbols[value] = [key] else: invert_symbols[value].append(key) # if there are any units with identical latex representations, substitute # units to avoid uncanceled terms in the final latex expression. for val in invert_symbols: symbols = invert_symbols[val] for i in range(1, len(symbols)): expr = expr.subs(symbols[i], symbols[0]) prefix = None l_expr = expr if isinstance(expr, Mul): coeffs = expr.as_coeff_Mul() if coeffs[0] == 1 or not isinstance(coeffs[0], Number): l_expr = coeffs[1] else: l_expr = coeffs[1] prefix = Float(coeffs[0], 2) latex_repr = latex( l_expr, symbol_names=symbol_table, mul_symbol="dot", fold_frac_powers=True, fold_short_frac=True, ) if prefix is not None: latex_repr = latex(prefix, mul_symbol="times") + "\\ " + latex_repr if latex_repr == "1": return "" else: return latex_repr class _ImportCache: __slots__ = ["_ua", "_uq"] def __init__(self): self._ua = None self._uq = None @property def ua(self): if self._ua is None: from unyt.array import unyt_array self._ua = unyt_array return self._ua @property def uq(self): if self._uq is None: from unyt.array import unyt_quantity self._uq = unyt_quantity return self._uq _import_cache_singleton = _ImportCache() class Unit: """ A symbolic unit, using sympy functionality. We only add "dimensions" so that sympy understands relations between different units. """ __slots__ = [ "expr", "is_atomic", "base_value", "base_offset", "dimensions", "_latex_repr", "registry", "is_Unit", ] # Set some assumptions for sympy. is_positive = True # make sqrt(m**2) --> m is_commutative = True is_number = False __array_priority__ = 3.0 def __new__( cls, unit_expr=sympy_one, base_value=None, base_offset=0.0, dimensions=None, registry=None, latex_repr=None, ): """ Create a new unit. May be an atomic unit (like a gram) or combinations of atomic units (like g / cm**3). Parameters ---------- unit_expr : Unit object, sympy.core.expr.Expr object, or str The symbolic unit expression. base_value : float The unit's value in yt's base units. base_offset : float The offset necessary to normalize temperature units to a common zero point. dimensions : sympy.core.expr.Expr A sympy expression representing the dimensionality of this unit. It must contain only mass, length, time, temperature and angle symbols. registry : UnitRegistry object The unit registry we use to interpret unit symbols. latex_repr : string A string to render the unit as LaTeX """ unit_cache_key = None # Simplest case. If user passes a Unit object, just use the expr. if hasattr(unit_expr, "is_Unit"): # grab the unit object's sympy expression. unit_expr = unit_expr.expr elif hasattr(unit_expr, "units") and hasattr(unit_expr, "value"): # something that looks like a unyt_array, grab the unit and value if unit_expr.shape != (): raise UnitParseError( "Cannot create a unit from a non-scalar unyt_array, " f"received: {unit_expr}" ) value = unit_expr.value if value == 1: unit_expr = unit_expr.units.expr else: unit_expr = unit_expr.value * unit_expr.units.expr # Parse a text unit representation using sympy's parser elif isinstance(unit_expr, (str, bytes)): if isinstance(unit_expr, bytes): unit_expr = unit_expr.decode("utf-8") # this cache substantially speeds up unit conversions if registry and unit_expr in registry._unit_object_cache: return registry._unit_object_cache[unit_expr] unit_cache_key = unit_expr unit_expr = parse_unyt_expr(unit_expr) # Make sure we have an Expr at this point. if not isinstance(unit_expr, Expr): raise UnitParseError( "Unit representation must be a string or " f"sympy Expr. '{unit_expr}' has type '{type(unit_expr)}'." ) if dimensions is None and unit_expr is sympy_one: dimensions = dimensionless if registry is None: # Caller did not set the registry, so use the default. registry = default_unit_registry # done with argument checking... # see if the unit is atomic. is_atomic = False if isinstance(unit_expr, Symbol): is_atomic = True # # check base_value and dimensions # if base_value is not None: # check that base_value is a float or can be converted to one try: base_value = float(base_value) except ValueError: raise UnitParseError( "Could not use base_value as a float. " f"base_value is '{base_value}' (type {type(base_value)})." ) # check that dimensions is valid if dimensions is not None: _validate_dimensions(dimensions) else: # lookup the unit symbols unit_data = _get_unit_data_from_expr(unit_expr, registry.lut) base_value = unit_data[0] dimensions = unit_data[1] if len(unit_data) > 2: base_offset = unit_data[2] latex_repr = unit_data[3] else: base_offset = 0.0 # Create obj with superclass construct. obj = super().__new__(cls) # Attach attributes to obj. obj.expr = unit_expr obj.is_atomic = is_atomic obj.base_value = base_value obj.base_offset = base_offset obj.dimensions = dimensions obj._latex_repr = latex_repr obj.registry = registry # lets us avoid isinstance calls obj.is_Unit = True # if we parsed a string unit expression, cache the result # for faster lookup later if unit_cache_key is not None: registry._unit_object_cache[unit_cache_key] = obj # Return `obj` so __init__ can handle it. return obj @property def latex_repr(self): """A LaTeX representation for the unit Examples -------- >>> from unyt import g, cm >>> (g/cm**3).units.latex_repr '\\\\frac{\\\\rm{g}}{\\\\rm{cm}^{3}}' """ if self._latex_repr is not None: return self._latex_repr if self.expr.is_Atom: expr = self.expr else: expr = self.expr.copy() self._latex_repr = _get_latex_representation(expr, self.registry) return self._latex_repr @property def units(self): return self def __hash__(self): return int(self.registry.unit_system_id, 16) ^ hash(self.expr) # end sympy conventions def __repr__(self): if self.expr == sympy_one: return "(dimensionless)" # @todo: don't use dunder method? return self.expr.__repr__() def __str__(self): if self.expr == sympy_one: return "dimensionless" unit_str = self.expr.__str__() if unit_str == "degC": return "°C" if unit_str == "delta_degC": return "Δ°C" if unit_str == "degF": return "°F" if unit_str == "delta_degF": return "Δ°F" # @todo: don't use dunder method? return unit_str # # Start unit operations # def __add__(self, u): raise InvalidUnitOperation("addition with unit objects is not allowed") def __radd__(self, u): raise InvalidUnitOperation("addition with unit objects is not allowed") def __sub__(self, u): raise InvalidUnitOperation("subtraction with unit objects is not allowed") def __rsub__(self, u): raise InvalidUnitOperation("subtraction with unit objects is not allowed") def __iadd__(self, u): raise InvalidUnitOperation( "in-place operations with unit objects are not allowed" ) def __isub__(self, u): raise InvalidUnitOperation( "in-place operations with unit objects are not allowed" ) def __imul__(self, u): raise InvalidUnitOperation( "in-place operations with unit objects are not allowed" ) def __itruediv__(self, u): raise InvalidUnitOperation( "in-place operations with unit objects are not allowed" ) def __rmul__(self, u): return self.__mul__(u) def __mul__(self, u): """Multiply Unit with u (Unit object).""" if not getattr(u, "is_Unit", False): data = np.array(u, subok=True) unit = getattr(u, "units", None) if unit is not None: if self.dimensions is logarithmic: raise InvalidUnitOperation( f"Tried to multiply '{self}' and '{unit}'." ) units = unit * self else: units = self if data.dtype.kind not in ("f", "u", "i", "c"): raise InvalidUnitOperation( f"Tried to multiply a Unit object with '{u}' (type {type(u)}). " "This behavior is undefined." ) if data.shape == (): return _import_cache_singleton.uq(data, units, bypass_validation=True) return _import_cache_singleton.ua(data, units, bypass_validation=True) elif self.dimensions is logarithmic and not u.is_dimensionless: raise InvalidUnitOperation(f"Tried to multiply '{self}' and '{u}'.") elif u.dimensions is logarithmic and not self.is_dimensionless: raise InvalidUnitOperation(f"Tried to multiply '{self}' and '{u}'.") base_offset = 0.0 if self.base_offset or u.base_offset: if u.dimensions in (temperature, angle) and self.is_dimensionless: base_offset = u.base_offset elif self.dimensions in (temperature, angle) and u.is_dimensionless: base_offset = self.base_offset else: raise InvalidUnitOperation( "Quantities with dimensions of angle or units of " "Fahrenheit or Celsius cannot be multiplied." ) return Unit( self.expr * u.expr, base_value=(self.base_value * u.base_value), base_offset=base_offset, dimensions=(self.dimensions * u.dimensions), registry=self.registry, ) def __truediv__(self, u): """Divide Unit by u (Unit object).""" if not isinstance(u, Unit): if isinstance(u, (numeric_type, list, tuple, np.ndarray)): from unyt.array import unyt_quantity return unyt_quantity(1.0, self) / u else: raise InvalidUnitOperation( f"Tried to divide a Unit object by '{u}' (type {type(u)}). " "This behavior is undefined." ) elif self.dimensions is logarithmic and not u.is_dimensionless: raise InvalidUnitOperation(f"Tried to divide '{self}' and '{u}'.") elif u.dimensions is logarithmic and not self.is_dimensionless: raise InvalidUnitOperation(f"Tried to divide '{self}' and '{u}'.") base_offset = 0.0 if self.base_offset or u.base_offset: if self.dimensions in (temperature, angle) and u.is_dimensionless: base_offset = self.base_offset else: raise InvalidUnitOperation( "Quantities with units of Farhenheit and Celsius cannot be divided." ) return Unit( self.expr / u.expr, base_value=(self.base_value / u.base_value), base_offset=base_offset, dimensions=(self.dimensions / u.dimensions), registry=self.registry, ) def __rtruediv__(self, u): return u * self**-1 def __pow__(self, p): """Take Unit to power p (float).""" try: p = Rational(str(p)).limit_denominator() except (ValueError, TypeError): raise InvalidUnitOperation( f"Tried to take a Unit object to the power '{p}' (type {type(p)}). " "Failed to cast it to a float." ) if self.dimensions is logarithmic and p != 1: raise InvalidUnitOperation(f"Tried to raise '{self}' to power '{p}'") return Unit( self.expr**p, base_value=(self.base_value**p), dimensions=(self.dimensions**p), registry=self.registry, ) def __eq__(self, u): """Test unit equality.""" return ( isinstance(u, Unit) and math.isclose(self.base_value, u.base_value) and math.isclose(self.base_offset, u.base_offset) and ( # use 'is' comparison dimensions to avoid expensive sympy operation self.dimensions is u.dimensions # fall back to expensive sympy comparison or self.dimensions == u.dimensions ) ) def copy(self, *, deep=False): expr = str(self.expr) base_value = copy.deepcopy(self.base_value) base_offset = copy.deepcopy(self.base_offset) dimensions = copy.deepcopy(self.dimensions) if deep: registry = copy.deepcopy(self.registry) else: registry = copy.copy(self.registry) return Unit(expr, base_value, base_offset, dimensions, registry) def __deepcopy__(self, memodict=None): return self.copy(deep=True) # # End unit operations # def same_dimensions_as(self, other_unit): """Test if the dimensions of *other_unit* are the same as this unit Examples -------- >>> from unyt import Msun, kg, mile >>> Msun.units.same_dimensions_as(kg.units) True >>> Msun.units.same_dimensions_as(mile.units) False """ # test first for 'is' equality to avoid expensive sympy operation if self.dimensions is other_unit.dimensions: return True return (self.dimensions / other_unit.dimensions) == sympy_one @property def is_dimensionless(self): """Is this a dimensionless unit? Returns ------- True for a dimensionless unit, False otherwise Examples -------- >>> from unyt import count, kg >>> count.units.is_dimensionless True >>> kg.units.is_dimensionless False """ return self.dimensions is sympy_one @property def is_code_unit(self): """Is this a "code" unit? Returns ------- True if the unit consists of atom units that being with "code". False otherwise """ for atom in self.expr.atoms(): if not (str(atom).startswith("code") or atom.is_Number): return False return True def list_equivalencies(self): """Lists the possible equivalencies associated with this unit object Examples -------- >>> from unyt import km >>> km.units.list_equivalencies() spectral: length <-> spatial_frequency <-> frequency <-> energy schwarzschild: mass <-> length compton: mass <-> length """ from unyt.equivalencies import equivalence_registry for k, v in equivalence_registry.items(): if self.has_equivalent(k): print(v()) def has_equivalent(self, equiv): """ Check to see if this unit object as an equivalent unit in *equiv*. Example ------- >>> from unyt import km >>> km.has_equivalent('spectral') True >>> km.has_equivalent('mass_energy') False """ try: this_equiv = equivalence_registry[equiv]() except KeyError: raise KeyError(f'No such equivalence "{equiv}".') old_dims = self.dimensions return old_dims in this_equiv._dims def get_base_equivalent(self, unit_system=None): """Create and return dimensionally-equivalent units in a specified base. >>> from unyt import g, cm >>> (g/cm**3).get_base_equivalent('mks') kg/m**3 >>> (g/cm**3).get_base_equivalent('solar') Mearth/AU**3 """ from unyt.unit_registry import _sanitize_unit_system unit_system = _sanitize_unit_system(unit_system, self) try: conv_data = _check_em_conversion( self.units, registry=self.registry, unit_system=unit_system ) um = unit_system.units_map if self.dimensions in um and self.expr == um[self.dimensions]: return self.copy() except MKSCGSConversionError: raise UnitsNotReducible(self.units, unit_system) if any(conv_data): new_units, _ = _em_conversion(self, conv_data, unit_system=unit_system) else: try: new_units = unit_system[self.dimensions] except MissingMKSCurrent: raise UnitsNotReducible(self.units, unit_system) return Unit(new_units, registry=self.registry) def get_cgs_equivalent(self): """Create and return dimensionally-equivalent cgs units. Example ------- >>> from unyt import kg, m >>> (kg/m**3).get_cgs_equivalent() g/cm**3 """ return self.get_base_equivalent(unit_system="cgs") def get_mks_equivalent(self): """Create and return dimensionally-equivalent mks units. Example ------- >>> from unyt import g, cm >>> (g/cm**3).get_mks_equivalent() kg/m**3 """ return self.get_base_equivalent(unit_system="mks") def get_conversion_factor(self, other_units, dtype=None): """Get the conversion factor and offset (if any) from one unit to another Parameters ---------- other_units: unit object The units we want the conversion factor for dtype: numpy dtype The dtype to return the conversion factor as Returns ------- conversion_factor : float old_units / new_units offset : float or None Offset between this unit and the other unit. None if there is no offset. Examples -------- >>> from unyt import km, cm, degree_fahrenheit, degree_celsius >>> km.get_conversion_factor(cm) (100000.0, None) >>> degree_celsius.get_conversion_factor(degree_fahrenheit) (1.7999999999999998, -31.999999999999886) """ return _get_conversion_factor(self, other_units, dtype) def latex_representation(self): """A LaTeX representation for the unit Examples -------- >>> from unyt import g, cm >>> (g/cm**3).latex_representation() '\\\\frac{\\\\rm{g}}{\\\\rm{cm}^{3}}' """ return self.latex_repr def as_coeff_unit(self): """Factor the coefficient multiplying a unit For units that are multiplied by a constant dimensionless coefficient, returns a tuple containing the coefficient and a new unit object for the unmultiplied unit. Example ------- >>> import unyt as u >>> unit = (u.m**2/u.cm).simplify() >>> unit 100*m >>> unit.as_coeff_unit() (100.0, m) """ coeff, mul = self.expr.as_coeff_Mul() coeff = float(coeff) ret = Unit( mul, self.base_value / coeff, self.base_offset, self.dimensions, self.registry, ) return coeff, ret def simplify(self): """Return a new equivalent unit object with a simplified unit expression >>> import unyt as u >>> unit = (u.m**2/u.cm).simplify() >>> unit 100*m """ expr = self.expr self.expr = _cancel_mul(expr, self.registry) return self def _factor_pairs(expr): factors = expr.as_ordered_factors() expanded_factors = [] for f in factors: if f.is_Number: continue base, exp = f.as_base_exp() if exp.q != 1: expanded_factors.append(base ** Mod(exp, 1)) exp = floor(exp) if exp >= 0: f = (base,) * exp else: f = (1 / base,) * abs(exp) expanded_factors.extend(f) return list(itertools.combinations(expanded_factors, 2)) def _create_unit_from_factor(factor, registry): base, exp = factor.as_base_exp() f = registry[str(base)] return Unit(base, f[0], f[2], f[1], registry, f[3]) ** exp def _cancel_mul(expr, registry): pairs_to_consider = _factor_pairs(expr) uncancelable_pairs = set() while len(pairs_to_consider): pair = pairs_to_consider.pop() if pair in uncancelable_pairs: continue u1 = _create_unit_from_factor(pair[0], registry) u2 = _create_unit_from_factor(pair[1], registry) prod = u1 * u2 if prod.dimensions == 1: expr = expr / pair[0] expr = expr / pair[1] value = prod.base_value if value != 1: if value.is_integer(): value = int(value) expr *= value else: uncancelable_pairs.add(pair) pairs_to_consider = _factor_pairs(expr) return expr # # Unit manipulation functions # # map from dimensions in one unit system to dimensions in other system, # canonical unit to convert to in that system, and floating point # conversion factor em_conversions = { ("C", dims.charge_mks): (dims.charge_cgs, "statC", 0.1 * speed_of_light_cm_per_s), ("statC", dims.charge_cgs): (dims.charge_mks, "C", 10.0 / speed_of_light_cm_per_s), ("T", dims.magnetic_field_mks): (dims.magnetic_field_cgs, "G", 1.0e4), ("G", dims.magnetic_field_cgs): (dims.magnetic_field_mks, "T", 1.0e-4), ("A", dims.current_mks): (dims.current_cgs, "statA", 0.1 * speed_of_light_cm_per_s), ("statA", dims.current_cgs): ( dims.current_mks, "A", 10.0 / speed_of_light_cm_per_s, ), ("V", dims.electric_potential_mks): ( dims.electric_potential_cgs, "statV", 1.0e-8 * speed_of_light_cm_per_s, ), ("statV", dims.electric_potential_cgs): ( dims.electric_potential_mks, "V", 1.0e8 / speed_of_light_cm_per_s, ), ("Ω", dims.resistance_mks): ( dims.resistance_cgs, "statohm", 1.0e9 / (speed_of_light_cm_per_s**2), ), ("statohm", dims.resistance_cgs): ( dims.resistance_mks, "Ω", 1.0e-9 * speed_of_light_cm_per_s**2, ), } em_conversion_dims = [k[1] for k in em_conversions.keys()] def _em_conversion(orig_units, conv_data, to_units=None, unit_system=None): """Convert between E&M & MKS base units. If orig_units is a CGS (or MKS) E&M unit, conv_data contains the corresponding MKS (or CGS) unit and scale factor converting between them. This must be done by replacing the expression of the original unit with the new one in the unit expression and multiplying by the scale factor. """ conv_unit, canonical_unit, scale = conv_data if conv_unit is None: conv_unit = canonical_unit new_expr = scale * canonical_unit.expr if unit_system is not None: # we don't know the to_units, so we get it directly from the # conv_data to_units = Unit(conv_unit.expr, registry=orig_units.registry) new_units = Unit(new_expr, registry=orig_units.registry) conv = new_units.get_conversion_factor(to_units) return to_units, conv @lru_cache(maxsize=128, typed=False) def _check_em_conversion(unit, to_unit=None, unit_system=None, registry=None): """Check to see if the units contain E&M units This function supports unyt's ability to convert data to and from E&M electromagnetic units. However, this support is limited and only very simple unit expressions can be readily converted. This function tries to see if the unit is an atomic base unit that is present in the em_conversions dict. If it does not contain E&M units, the function returns an empty tuple. If it does contain an atomic E&M unit in the em_conversions dict, it returns a tuple containing the unit to convert to and scale factor. If it contains a more complicated E&M unit and we are trying to convert between CGS & MKS E&M units, it raises an error. """ em_map = () if unit == to_unit or unit.dimensions not in em_conversion_dims: return em_map if unit.is_atomic: prefix, unit_wo_prefix = _split_prefix(str(unit), unit.registry.lut) else: prefix, unit_wo_prefix = "", str(unit) if (unit_wo_prefix, unit.dimensions) in em_conversions: em_info = em_conversions[unit_wo_prefix, unit.dimensions] em_unit = Unit(prefix + em_info[1], registry=registry) if to_unit is None: cmks_in_unit = current_mks in unit.dimensions.atoms() cmks_in_unit_system = unit_system.units_map[current_mks] cmks_in_unit_system = cmks_in_unit_system is not None if cmks_in_unit and cmks_in_unit_system: em_map = (unit_system[unit.dimensions], unit, 1.0) else: em_map = (None, em_unit, em_info[2]) elif to_unit.dimensions == em_unit.dimensions: em_map = (to_unit, em_unit, em_info[2]) if em_map: return em_map if unit_system is None: from unyt.unit_systems import unit_system_registry unit_system = unit_system_registry["mks"] for unit_atom in unit.expr.atoms(): if unit_atom.is_Number: continue bu = str(unit_atom) budims = Unit(bu, registry=registry).dimensions try: if str(unit_system[budims]) == bu: continue except MissingMKSCurrent: raise MKSCGSConversionError(unit) return em_map def _get_conversion_factor(old_units, new_units, dtype): """ Get the conversion factor between two units of equivalent dimensions. This is the number you multiply data by to convert from values in `old_units` to values in `new_units`. Parameters ---------- old_units: str or Unit object The current units. new_units : str or Unit object The units we want. dtype: NumPy dtype The dtype of the conversion factor Returns ------- conversion_factor : float `old_units / new_units` offset : float or None Offset between the old unit and new unit. """ if old_units.dimensions != new_units.dimensions: raise UnitConversionError( old_units, old_units.dimensions, new_units, new_units.dimensions ) old_basevalue = old_units.base_value old_baseoffset = old_units.base_offset new_basevalue = new_units.base_value new_baseoffset = new_units.base_offset ratio = old_basevalue / new_basevalue if old_baseoffset == 0 and new_baseoffset == 0: return (ratio, None) else: # the dimensions are either temperature or angle (lat, lon) if old_units.dimensions == temperature: # for degree Celsius, back out the SI prefix scaling from # offset scaling for degree Fahrenheit old_prefix, _ = _split_prefix(str(old_units), old_units.registry.lut) if old_prefix != "": old_baseoffset /= old_basevalue new_prefix, _ = _split_prefix(str(new_units), new_units.registry.lut) if new_prefix != "": new_baseoffset /= new_basevalue return ratio, ratio * old_baseoffset - new_baseoffset # # Helper functions # def _get_unit_data_from_expr(unit_expr, unit_symbol_lut): """ Grabs the total base_value and dimensions from a valid unit expression. Parameters ---------- unit_expr: Unit object, or sympy Expr object The expression containing unit symbols. unit_symbol_lut: dict Provides the unit data for each valid unit symbol. """ # Now for the sympy possibilities if isinstance(unit_expr, Number): if unit_expr is sympy_one: return (1.0, sympy_one) return (float(unit_expr), sympy_one) if isinstance(unit_expr, Symbol): return _lookup_unit_symbol(unit_expr.name, unit_symbol_lut) if isinstance(unit_expr, Pow): unit_data = _get_unit_data_from_expr(unit_expr.args[0], unit_symbol_lut) power = unit_expr.args[1] if isinstance(power, Symbol): raise UnitParseError(f"Invalid unit expression '{unit_expr}'.") conv = float(unit_data[0] ** power) unit = unit_data[1] ** power return (conv, unit) if isinstance(unit_expr, Mul): base_value = 1.0 dimensions = 1 for expr in unit_expr.args: unit_data = _get_unit_data_from_expr(expr, unit_symbol_lut) base_value *= unit_data[0] dimensions *= unit_data[1] return (float(base_value), dimensions) raise UnitParseError( f"Cannot parse for unit data from {str(unit_expr)!r}. Please supply " "an expression of only Unit, Symbol, Pow, and Mul objects." ) def _validate_dimensions(dimensions): if isinstance(dimensions, Mul): for dim in dimensions.args: _validate_dimensions(dim) elif isinstance(dimensions, Symbol): if dimensions not in base_dimensions: raise UnitParseError( f"Dimensionality expression contains an unknown symbol '{dimensions}'." ) elif isinstance(dimensions, Pow): if not isinstance(dimensions.args[1], Number): raise UnitParseError( f"Dimensionality expression '{dimensions}' contains a " "unit symbol as a power." ) elif isinstance(dimensions, (Add, Number)): if not isinstance(dimensions, One): raise UnitParseError( "Only dimensions that are instances of Pow, " "Mul, or symbols in the base dimensions are " f"allowed. Got dimensions '{dimensions}'" ) elif not isinstance(dimensions, Basic): raise UnitParseError(f"Bad dimensionality expression '{dimensions}'.") def define_unit( symbol, value, tex_repr=None, offset=None, prefixable=False, registry=None ): """ Define a new unit and add it to the specified unit registry. Parameters ---------- symbol : string The symbol for the new unit. value : tuple or :class:`unyt.array.unyt_quantity` The definition of the new unit in terms of some other units. For example, one would define a new "mph" unit with ``(1.0, "mile/hr")`` or with ``1.0*unyt.mile/unyt.hr`` tex_repr : string, optional The LaTeX representation of the new unit. If one is not supplied, it will be generated automatically based on the symbol string. offset : float, optional The default offset for the unit. If not set, an offset of 0 is assumed. prefixable : boolean, optional Whether or not the new unit can use SI prefixes. Default: False registry : :class:`unyt.unit_registry.UnitRegistry` or None The unit registry to add the unit to. If None, then defaults to the global default unit registry. If registry is set to None then the unit object will be added as an attribute to the top-level :mod:`unyt` namespace to ease working with the newly defined unit. See the example below. Examples -------- >>> from unyt import day >>> two_weeks = 14.0*day >>> one_day = 1.0*day >>> define_unit("two_weeks", two_weeks) >>> from unyt import two_weeks >>> print((3*two_weeks)/one_day) 42.0 dimensionless """ import unyt from unyt.array import _iterable, unyt_quantity if registry is None: registry = default_unit_registry if symbol in registry: raise RuntimeError( f"Unit symbol '{symbol}' already exists in the provided registry" ) if not isinstance(value, unyt_quantity): if _iterable(value) and len(value) == 2: value = unyt_quantity(value[0], value[1], registry=registry) else: raise RuntimeError('"value" needs to be a quantity or (value, unit) tuple!') base_value = float(value.in_base(unit_system="mks")) dimensions = value.units.dimensions registry.add( symbol, base_value, dimensions, prefixable=prefixable, tex_repr=tex_repr, offset=offset, ) if registry is default_unit_registry: u = Unit(symbol, registry=registry) setattr(unyt, symbol, u) NULL_UNIT = Unit() unyt-3.0.4/unyt/unit_registry.py000066400000000000000000000300601476461141700170070ustar00rootroot00000000000000""" A registry for units that can be added to and modified. """ import copy import json from functools import lru_cache from hashlib import md5 from sympy import sympify from unyt import dimensions as unyt_dims from unyt._unit_lookup_table import default_unit_symbol_lut, unit_prefixes from unyt.exceptions import SymbolNotFoundError, UnitParseError from unyt.unit_systems import _split_prefix, mks_unit_system, unit_system_registry def _sanitize_unit_system(unit_system, obj): if unit_system is None: try: unit_system = obj.units.registry.unit_system except AttributeError: unit_system = mks_unit_system if hasattr(unit_system, "name"): return unit_system_registry[unit_system.name] elif hasattr(unit_system, "unit_registry"): unit_system = unit_system.unit_registry.unit_system_id elif unit_system == "code": unit_system = obj.units.registry.unit_system_id return unit_system_registry[str(unit_system)] @lru_cache(maxsize=128, typed=False) def cached_sympify(u): """ Successive loads of unit systems produce the same calls to sympify in UnitRegistry.from_json. Even within a single load, this is a net improvement because there will often be a few cache hits """ return sympify(u, locals=vars(unyt_dims)) class UnitRegistry: """A registry for unit symbols""" _unit_system_id = None def __init__(self, add_default_symbols=True, lut=None, unit_system=None): self._unit_object_cache = {} if lut: self.lut = lut else: self.lut = {} self.unit_system = _sanitize_unit_system(unit_system, None) if add_default_symbols: self.lut.update(default_unit_symbol_lut) def __getitem__(self, key): try: ret = self.lut[str(key)] except KeyError: try: _lookup_unit_symbol(str(key), self.lut) ret = self.lut[str(key)] except UnitParseError: raise SymbolNotFoundError( f"The symbol '{key}' does not exist in this registry." ) return ret def __contains__(self, item): if str(item) in self.lut: return True try: _lookup_unit_symbol(str(item), self.lut) return True except UnitParseError: return False @property def unit_system_id(self): """ This is a unique identifier for the unit registry created from a FNV hash. It is needed to register a dataset's code unit system in the unit system registry. """ if self._unit_system_id is None: hash_data = bytearray() for k, v in sorted(self.lut.items()): hash_data.extend(k.encode("utf8")) hash_data.extend(repr(v).encode("utf8")) m = md5() m.update(hash_data) self._unit_system_id = str(m.hexdigest()) return self._unit_system_id @property def prefixable_units(self): return [u for u in self.lut if self.lut[u][4]] def add( self, symbol, base_value, dimensions, tex_repr=None, offset=None, prefixable=False, ): """ Add a symbol to this registry. Parameters ---------- symbol : str The name of the unit base_value : float The scaling from the units value to the equivalent SI unit with the same dimensions dimensions : expr The dimensions of the unit tex_repr : str, optional The LaTeX representation of the unit. If not provided a LaTeX representation is automatically generated from the name of the unit. offset : float, optional If set, the zero-point offset to apply to the unit to convert to SI. This is mostly used for units like Farhenheit and Celsius that are not defined on an absolute scale. prefixable : bool If True, then SI-prefix versions of the unit will be created along with the unit itself. """ from unyt.unit_object import _validate_dimensions self._unit_system_id = None # Validate if not isinstance(base_value, float): raise UnitParseError( f"base_value ({base_value}) must be a float, got a {type(base_value)}." ) if offset is not None: if not isinstance(offset, float): raise UnitParseError( f"offset value ({offset}) must be a float, got a {type(offset)}." ) else: offset = 0.0 _validate_dimensions(dimensions) if tex_repr is None: # make educated guess that will look nice in most cases tex_repr = r"\rm{" + symbol.replace("_", r"\ ") + "}" # Add to lut self.lut[symbol] = (base_value, dimensions, offset, tex_repr, prefixable) def remove(self, symbol): """ Remove the entry for the unit matching `symbol`. Parameters ---------- symbol : str The name of the unit symbol to remove from the registry. """ self._unit_system_id = None if symbol not in self.lut: raise SymbolNotFoundError( r"Tried to remove the symbol {symbol!r}, but it does not exist " "in this registry." ) del self.lut[symbol] if symbol in self._unit_object_cache: del self._unit_object_cache[symbol] def modify(self, symbol, base_value): """ Change the base value of a unit symbol. Useful for adjusting code units after parsing parameters. Parameters ---------- symbol : str The name of the symbol to modify base_value : float The new base_value for the symbol. """ self._unit_system_id = None if symbol not in self.lut: raise SymbolNotFoundError( f"Tried to modify the symbol {symbol!r}, but it does not exist " "in this registry." ) if hasattr(base_value, "in_base"): new_dimensions = base_value.units.dimensions base_value = base_value.in_base("mks") base_value = base_value.value else: new_dimensions = self.lut[symbol][1] self.lut[symbol] = (float(base_value), new_dimensions) + self.lut[symbol][2:] if symbol in self._unit_object_cache: del self._unit_object_cache[symbol] def keys(self): """ Print out the units contained in the lookup table. """ return self.lut.keys() def to_json(self): """ Returns a json-serialized version of the unit registry """ sanitized_lut = {} for k, v in self.lut.items(): san_v = list(v) repr_dims = str(v[1]) san_v[1] = repr_dims sanitized_lut[k] = tuple(san_v) return json.dumps(sanitized_lut) @classmethod def from_json(cls, json_text): """ Returns a UnitRegistry object from a json-serialized unit registry Parameters ---------- json_text : str A string containing a json representation of a UnitRegistry """ data = json.loads(json_text) lut = _correct_old_unit_registry(data, sympify=True) return cls(lut=lut, add_default_symbols=False) def list_same_dimensions(self, unit_object): """ Return a list of base unit names that this registry knows about that are of equivalent dimensions to *unit_object*. """ equiv = [k for k, v in self.lut.items() if v[1] is unit_object.dimensions] equiv = sorted(set(equiv)) return equiv def __deepcopy__(self, memodict=None): lut = copy.deepcopy(self.lut) return type(self)(lut=lut) class _NonModifiableUnitRegistry(UnitRegistry): """The class of the default unit registry""" def modify(self, symbol, base_value): raise TypeError("Units from unyt's default registry cannot be modified.") def remove(self, symbol): raise TypeError("Units from unyt's default registry cannot be removed.") #: The default unit registry default_unit_registry = _NonModifiableUnitRegistry() def _lookup_unit_symbol(symbol_str, unit_symbol_lut): """ Searches for the unit data tuple corresponding to the given symbol. Parameters ---------- symbol_str : str The unit symbol to look up. unit_symbol_lut : dict Dictionary with symbols as keys and unit data tuples as values. """ if symbol_str in unit_symbol_lut: # lookup successful, return the tuple directly return unit_symbol_lut[symbol_str] # could still be a known symbol with a prefix prefix, symbol_wo_prefix = _split_prefix(symbol_str, unit_symbol_lut) if prefix: # lookup successful, it's a symbol with a prefix unit_data = unit_symbol_lut[symbol_wo_prefix] prefix_value = unit_prefixes[prefix][0] # Need to add some special handling for comoving units # this is fine for now, but it wouldn't work for a general # unit that has an arbitrary LaTeX representation if symbol_wo_prefix != "cm" and symbol_wo_prefix.endswith("cm"): sub_symbol_wo_prefix = symbol_wo_prefix[:-2] sub_symbol_str = symbol_str[:-2] else: sub_symbol_wo_prefix = symbol_wo_prefix sub_symbol_str = symbol_str latex_repr = unit_data[3].replace( "{" + sub_symbol_wo_prefix + "}", "{" + sub_symbol_str + "}" ) # Leave offset and dimensions the same, but adjust scale factor and # LaTeX representation ret = ( unit_data[0] * prefix_value, unit_data[1], unit_data[2], latex_repr, False, ) unit_symbol_lut[symbol_str] = ret return ret # no dice raise UnitParseError( f"Could not find unit symbol '{symbol_str}' in the provided symbols." ) def _correct_old_unit_registry(data, sympify=False): lut = {} for k, v in data.items(): unsan_v = list(v) if sympify: unsan_v[1] = cached_sympify(v[1]) if len(unsan_v) == 4: # old unit registry so we need to add SI-prefixability to the registry # entry, correct the base_value to be in MKS units, and swap dimensions to # use unyt's dimension singletons # add SI-prefixability to LUT entry if k in default_unit_symbol_lut: unsan_v.append(default_unit_symbol_lut[k][4]) else: unsan_v.append(False) dims = unsan_v[1] for dim_factor in dims.as_ordered_factors(): dim, power = dim_factor.as_base_exp() # Swap dimensions in the LUT entry to use unyt's dimension singletons for base_dim in unyt_dims.base_dimensions: # If they're *equal* but not *identical*, swap them if base_dim == dim and base_dim is not dim: if power != 1: unsan_v[1] /= dim**power unsan_v[1] *= base_dim**power else: # need a special case for power == 1 because id(symbol ** 1) # is not necessarily the same as id(symbol) unsan_v[1] /= dim unsan_v[1] *= base_dim break # correct base value to be in MKS units if dim == unyt_dims.mass: unsan_v[0] /= 1000 ** float(power) if dim == unyt_dims.length: unsan_v[0] /= 100 ** float(power) lut[k] = tuple(unsan_v) for k in default_unit_symbol_lut: if k not in lut: lut[k] = default_unit_symbol_lut[k] return lut unyt-3.0.4/unyt/unit_symbols.py000066400000000000000000000012211476461141700166240ustar00rootroot00000000000000""" Predefined useful aliases to physical units Note that all of these names can be imported from the top-level unyt namespace. For example:: >>> from unyt import cm, g, s >>> data = [3, 4, 5]*g*cm/s >>> print(data) [3 4 5] cm*g/s .. show_all_units:: """ from unyt._unit_lookup_table import name_alternatives as _name_alternatives from unyt.unit_object import Unit as _Unit from unyt.unit_registry import default_unit_registry as _registry _namespace = globals() for _canonical_name, _alt_names in _name_alternatives.items(): for _alt_name in _alt_names: _namespace[_alt_name] = _Unit(_canonical_name, registry=_registry) unyt-3.0.4/unyt/unit_systems.py000066400000000000000000000276351476461141700166640ustar00rootroot00000000000000""" Unit system class. """ from collections import OrderedDict from unyt import dimensions from unyt._parsing import parse_unyt_expr from unyt._unit_lookup_table import ( default_unit_symbol_lut as default_lut, inv_name_alternatives, physical_constants, unit_prefixes, ) from unyt.exceptions import IllDefinedUnitSystem, MissingMKSCurrent, UnitsNotReducible def add_symbols(namespace, registry): """Adds the unit symbols from :mod:`unyt.unit_symbols` to a namespace Parameters ---------- namespace : dict The dict to insert unit symbols into. The keys will be string unit names and values will be the corresponding unit objects. registry : :class:`unyt.unit_registry.UnitRegistry` The registry to create units from. Note that if you would like to use a custom unit system, ensure your registry was created using that unit system. Example ------- >>> from unyt.unit_registry import UnitRegistry >>> class MyClass(): ... def __init__(self): ... self.reg = UnitRegistry() ... add_symbols(vars(self), self.reg) >>> foo = MyClass() >>> foo.kilometer km >>> foo.joule J """ import unyt.unit_symbols as us from unyt.unit_object import Unit for name, unit in vars(us).items(): if name.startswith("_"): continue namespace[name] = Unit(unit.expr, registry=registry) for name in [k for k in registry.keys() if k not in namespace]: namespace[name] = Unit(name, registry=registry) def add_constants(namespace, registry): """Adds the quantities from :mod:`unyt.physical_constants` to a namespace Parameters ---------- namespace : dict The dict to insert quantities into. The keys will be string names and values will be the corresponding quantities. registry : :class:`unyt.unit_registry.UnitRegistry` The registry to create units from. Note that if you would like to use a custom unit system, ensure your registry was created using that unit system. Example ------- >>> from unyt.unit_registry import UnitRegistry >>> class MyClass(): ... def __init__(self): ... self.reg = UnitRegistry(unit_system='cgs') ... add_constants(vars(self), self.reg) >>> foo = MyClass() >>> foo.gravitational_constant unyt_quantity(6.67408e-08, 'cm**3/(g*s**2)') >>> foo.speed_of_light unyt_quantity(2.99792458e+10, 'cm/s') """ from unyt.array import unyt_quantity for constant_name in physical_constants: value, unit_name, alternate_names = physical_constants[constant_name] for name in alternate_names + [constant_name]: quan = unyt_quantity(value, unit_name, registry=registry) try: namespace[name] = quan.in_base(unit_system=registry.unit_system) except UnitsNotReducible: namespace[name] = quan namespace[name + "_mks"] = unyt_quantity( value, unit_name, registry=registry ) try: namespace[name + "_cgs"] = quan.in_cgs() except UnitsNotReducible: pass if name == "h": # backward compatibility for unyt 1.0, which defined hmks namespace["hmks"] = namespace["h_mks"].copy() namespace["hcgs"] = namespace["h_cgs"].copy() def _split_prefix(symbol_str, unit_symbol_lut): possible_prefix = symbol_str[0] if symbol_str[:2] == "da": possible_prefix = "da" if possible_prefix in unit_prefixes: # the first character could be a prefix, check the rest of the symbol symbol_wo_pref = symbol_str[1:] # deca is the only prefix with length 2 if symbol_str[:2] == "da": symbol_wo_pref = symbol_str[2:] possible_prefix = "da" entry = unit_symbol_lut.get(symbol_wo_pref, None) if entry and entry[4]: return possible_prefix, symbol_wo_pref return "", symbol_str def _get_system_unit_string(dims, base_units): # The dimensions of a unit object is the product of the base dimensions. # Use sympy to factor the dimensions into base CGS unit symbols. units = [] my_dims = dims.expand() if my_dims is dimensions.dimensionless: return "" for factor in my_dims.as_ordered_factors(): dim = list(factor.free_symbols)[0] unit_string = str(base_units[dim]) if factor.is_Pow: power_string = f"**({factor.as_base_exp()[1]})" else: power_string = "" units.append(f"({unit_string}){power_string}") return " * ".join(units) unit_system_registry = {} cmks = dimensions.current_mks class UnitSystem: """ Create a UnitSystem for facilitating conversions to a default set of units. Parameters ---------- name : string The name of the unit system. Will be used as the key in the *unit_system_registry* dict to reference the unit system by. length_unit : string or :class:`unyt.unit_object.Unit` The base length unit of this unit system. mass_unit : string or :class:`unyt.unit_object.Unit` The base mass unit of this unit system. time_unit : string or :class:`unyt.unit_object.Unit` The base time unit of this unit system. temperature_unit : string or :class:`unyt.unit_object.Unit`, optional The base temperature unit of this unit system. Defaults to "K". angle_unit : string or :class:`unyt.unit_object.Unit`, optional The base angle unit of this unit system. Defaults to "rad". mks_system: boolean, optional Whether or not this unit system has SI-specific units. Default: False current_mks_unit : string or :class:`unyt.unit_object.Unit`, optional The base current unit of this unit system. Defaults to "A". luminous_intensity_unit : string or :class:`unyt.unit_object.Unit`, optional The base luminous intensity unit of this unit system. Defaults to "cd". registry : :class:`unyt.unit_registry.UnitRegistry` object The unit registry associated with this unit system. Only useful for defining unit systems based on code units. """ def __init__( self, name, length_unit, mass_unit, time_unit, temperature_unit="K", angle_unit="rad", current_mks_unit="A", luminous_intensity_unit="cd", logarithmic_unit="Np", registry=None, ): self.registry = registry self.units_map = OrderedDict( [ (dimensions.length, length_unit), (dimensions.mass, mass_unit), (dimensions.time, time_unit), (dimensions.temperature, temperature_unit), (dimensions.angle, angle_unit), (dimensions.current_mks, current_mks_unit), (dimensions.luminous_intensity, luminous_intensity_unit), (dimensions.logarithmic, logarithmic_unit), ] ) for k, v in self.units_map.items(): if v is not None: if hasattr(v, "value") and hasattr(v, "units"): self.units_map[k] = v.value * v.units.expr else: self.units_map[k] = parse_unyt_expr(str(v)) for dimension, unit in self.units_map.items(): # CGS sets the current_mks unit to none, so catch it here if unit is None and dimension is dimensions.current_mks: continue if unit.is_Mul: unit = unit.as_coeff_Mul()[1] if ( self.registry is not None and self.registry[str(unit)][1] is not dimension ): raise IllDefinedUnitSystem(self.units_map) elif self.registry is None: bu = _split_prefix(str(unit), default_lut)[1] inferred_dimension = default_lut[inv_name_alternatives[bu]][1] if inferred_dimension is not dimension: raise IllDefinedUnitSystem(self.units_map) self._dims = [ "length", "mass", "time", "temperature", "angle", "current_mks", "luminous_intensity", "logarithmic", ] self.registry = registry self.base_units = self.units_map.copy() unit_system_registry[name] = self self.name = name def __getitem__(self, key): from unyt.unit_object import Unit if isinstance(key, str): key = getattr(dimensions, key) um = self.units_map if key not in um or um[key] is None: if cmks in key.free_symbols and self.units_map[cmks] is None: raise MissingMKSCurrent(self.name) units = _get_system_unit_string(key, self.units_map) self.units_map[key] = parse_unyt_expr(units) return Unit(units, registry=self.registry) return Unit(self.units_map[key], registry=self.registry) def __setitem__(self, key, value): if isinstance(key, str): if key not in self._dims: self._dims.append(key) key = getattr(dimensions, key) if self.units_map[cmks] is None and cmks in key.free_symbols: raise MissingMKSCurrent(self.name) self.units_map[key] = parse_unyt_expr(str(value)) def __str__(self): return self.name def __repr__(self): repr = f"{self.name} Unit System\n" repr += " Base Units:\n" for dim in self.base_units: if self.base_units[dim] is not None: repr += f" {str(dim).strip('()')}: {self.base_units[dim]}\n" repr += " Other Units:\n" for key in self._dims: dim = getattr(dimensions, key) if dim not in self.base_units: repr += f" {key}: {self.units_map[dim]}\n" return repr[:-1] @property def has_current_mks(self): """Does this unit system have an MKS current dimension?""" return self.units_map[cmks] is not None #: The CGS unit system cgs_unit_system = UnitSystem("cgs", "cm", "g", "s", current_mks_unit=None) cgs_unit_system["energy"] = "erg" cgs_unit_system["specific_energy"] = "erg/g" cgs_unit_system["pressure"] = "dyne/cm**2" cgs_unit_system["force"] = "dyne" cgs_unit_system["magnetic_field_cgs"] = "gauss" cgs_unit_system["charge_cgs"] = "esu" cgs_unit_system["current_cgs"] = "statA" cgs_unit_system["power"] = "erg/s" #: The MKS unit system mks_unit_system = UnitSystem("mks", "m", "kg", "s") mks_unit_system["energy"] = "J" mks_unit_system["specific_energy"] = "J/kg" mks_unit_system["pressure"] = "Pa" mks_unit_system["force"] = "N" mks_unit_system["magnetic_field"] = "T" mks_unit_system["charge"] = "C" mks_unit_system["frequency"] = "Hz" mks_unit_system["power"] = "W" mks_unit_system["electric_potential"] = "V" mks_unit_system["capacitance"] = "F" mks_unit_system["inductance"] = "H" mks_unit_system["resistance"] = "ohm" mks_unit_system["magnetic_flux"] = "Wb" mks_unit_system["luminous_flux"] = "lm" #: The imperial unit system imperial_unit_system = UnitSystem("imperial", "ft", "lb", "s", temperature_unit="R") imperial_unit_system["force"] = "lbf" imperial_unit_system["energy"] = "ft*lbf" imperial_unit_system["pressure"] = "lbf/ft**2" imperial_unit_system["power"] = "hp" #: The galactic unit system galactic_unit_system = UnitSystem("galactic", "kpc", "Msun", "Myr") galactic_unit_system["energy"] = "keV" galactic_unit_system["magnetic_field_cgs"] = "uG" #: The solar unit system solar_unit_system = UnitSystem("solar", "AU", "Mearth", "yr") #: Geometrized unit system geometrized_unit_system = UnitSystem("geometrized", "l_geom", "m_geom", "t_geom") #: Planck unit system planck_unit_system = UnitSystem( "planck", "l_pl", "m_pl", "t_pl", temperature_unit="T_pl" ) planck_unit_system["energy"] = "E_pl" planck_unit_system["charge_mks"] = "q_pl"