diff --git a/.buildinfo b/.buildinfo new file mode 100644 index 0000000..a8fd44b --- /dev/null +++ b/.buildinfo @@ -0,0 +1,4 @@ +# Sphinx build info version 1 +# This file hashes the configuration used when building these files. When it is not found, a full rebuild will be done. +config: b6a381630f4596ad311b6f044ab9c529 +tags: 645f666f9bcd5a90fca523b33c5a78b7 diff --git a/.cruft.json b/.cruft.json deleted file mode 100644 index 3b9c548..0000000 --- a/.cruft.json +++ /dev/null @@ -1,43 +0,0 @@ -{ - "template": "https://codebase.helmholtz.cloud/hcdc/software-templates/python-package-template.git", - "commit": "2a291586c1d99092860b9cf26a034a285479131e", - "checkout": null, - "context": { - "cookiecutter": { - "project_authors": "Philipp S. Sommer", - "project_author_emails": "philipp.sommer@hereon.de", - "project_maintainers": "Philipp S. Sommer", - "project_maintainer_emails": "philipp.sommer@hereon.de", - "gitlab_host": "codebase.helmholtz.cloud", - "gitlab_username": "psyplot", - "git_remote_protocoll": "ssh", - "institution": "Helmholtz-Zentrum Hereon", - "institution_url": "https://www.hereon.de", - "copyright_holder": "Helmholtz-Zentrum hereon GmbH", - "copyright_year": "2021-2024", - "use_reuse": "yes", - "code_license": "LGPL-3.0-only", - "documentation_license": "CC-BY-4.0", - "supplementary_files_license": "CC0-1.0", - "project_title": "psyplot", - "project_slug": "psyplot", - "package_folder": "psyplot", - "project_short_description": "Python package for interactive data visualization", - "keywords": "visualization,netcdf,raster,cartopy,earth-sciences", - "documentation_url": "https://psyplot.github.io", - "use_markdown_for_documentation": "no", - "ci_matrix": "pipenv", - "deploy_package_in_ci": "yes", - "deploy_pages_in_ci": "git-push", - "_extensions": [ - "local_extensions.UnderlinedExtension" - ], - "_template": "https://codebase.helmholtz.cloud/hcdc/software-templates/python-package-template.git" - } - }, - "directory": null, - "skip": [ - ".git", - ".mypy_cache" - ] -} diff --git a/.cruft.json.license b/.cruft.json.license deleted file mode 100644 index 919c9c1..0000000 --- a/.cruft.json.license +++ /dev/null @@ -1,3 +0,0 @@ -SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH - -SPDX-License-Identifier: CC0-1.0 diff --git a/.flake8 b/.flake8 deleted file mode 100644 index 18607ff..0000000 --- a/.flake8 +++ /dev/null @@ -1,10 +0,0 @@ -# SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH -# -# SPDX-License-Identifier: CC0-1.0 - -[flake8] -extend-ignore = - E203 - E402 - E501 - W503 diff --git a/.gitattributes b/.gitattributes deleted file mode 100644 index cd7ac8f..0000000 --- a/.gitattributes +++ /dev/null @@ -1,5 +0,0 @@ -# SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH -# -# SPDX-License-Identifier: CC0-1.0 - -psyplot/_version.py export-subst diff --git a/.gitignore b/.gitignore deleted file mode 100644 index 18726a3..0000000 --- a/.gitignore +++ /dev/null @@ -1,155 +0,0 @@ -# SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH -# -# SPDX-License-Identifier: CC0-1.0 - -# Byte-compiled / optimized / DLL files -__pycache__/ -*.py[cod] -*$py.class - -# C extensions -*.so - -# Distribution / packaging -.Python -build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -.eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -wheels/ -share/python-wheels/ -*.egg-info/ -.installed.cfg -*.egg -MANIFEST - -# 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/ -.nox/ -.coverage -.coverage.* -.cache -nosetests.xml -coverage.xml -*.cover -*.py,cover -.hypothesis/ -.pytest_cache/ -cover/ - -# Translations -*.mo -*.pot - -# Django stuff: -*.log -local_settings.py -db.sqlite3 -db.sqlite3-journal - -# Flask stuff: -instance/ -.webassets-cache - -# Scrapy stuff: -.scrapy - -# Sphinx documentation -docs/_build/ - -# PyBuilder -.pybuilder/ -target/ - -# Jupyter Notebook -.ipynb_checkpoints - -# IPython -profile_default/ -ipython_config.py - -# pyenv -# For a library or package, you might want to ignore these files since the code is -# intended to run in multiple environments; otherwise, check them in: -# .python-version - -# pipenv -# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. -# However, in case of collaboration, if having platform-specific dependencies or dependencies -# having no cross-platform support, pipenv may install dependencies that don't work, or not -# install all needed dependencies. -#Pipfile.lock - -# PEP 582; used by e.g. github.com/David-OConnor/pyflow -__pypackages__/ - -# Celery stuff -celerybeat-schedule -celerybeat.pid - -# SageMath parsed files -*.sage.py - -# Environments -.env -.venv -env/ -venv/ -ENV/ -env.bak/ -venv.bak/ - -# Spyder project settings -.spyderproject -.spyproject - -# Rope project settings -.ropeproject - -# mkdocs documentation -/site - -# mypy -.mypy_cache/ -.dmypy.json -dmypy.json - -# Pyre type checker -.pyre/ - -# pytype static type analyzer -.pytype/ - -# Cython debug symbols -cython_debug/ - -static/ - -docs/api -psyplot/migrations/00*.py -docs/_static/orcid.* - -# ignore Pipfile.lock files in ci -# if a lock-file needs to be added, add it with `git add -f` -ci/matrix/*/Pipfile.lock - - -*psyplot_testresults diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml deleted file mode 100644 index dd7e7cc..0000000 --- a/.gitlab-ci.yml +++ /dev/null @@ -1,101 +0,0 @@ -# SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH -# -# SPDX-License-Identifier: CC0-1.0 - -image: python:3.9 - -variables: - PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip" - -cache: - paths: - - .cache/pip - -before_script: - # replace git internal paths in order to use the CI_JOB_TOKEN - - apt-get update -y && apt-get install -y pandoc graphviz - - python -m pip install -U pip - -test-package: - stage: test - script: - - pip install build twine - - make dist - - twine check dist/* - artifacts: - name: python-artifacts - paths: - - "dist/*" - expire_in: 7 days - -test: - stage: test - variables: - PIPENV_PIPFILE: "ci/matrix/${SCENARIO}/Pipfile" - script: - - pip install pipenv - - pipenv install - - make pipenv-test - parallel: - matrix: - - SCENARIO: - - default - coverage: '/(?i)total.*? (100(?:\.0+)?\%|[1-9]?\d(?:\.\d+)?\%)$/' - artifacts: - name: pipfile - paths: - - "ci/matrix/${SCENARIO}/*" - expire_in: 30 days - -test-docs: - stage: test - script: - - make dev-install - - make -C docs html - - make -C docs linkcheck - artifacts: - paths: - - docs/_build - - -deploy-package: - stage: deploy - needs: - - test-package - - test-docs - - test - only: - - master - script: - - pip install twine - - TWINE_PASSWORD=${CI_JOB_TOKEN} TWINE_USERNAME=gitlab-ci-token python -m twine upload --repository-url ${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/pypi dist/* - - - -deploy-docs: - stage: deploy - only: - - master - needs: - - test-docs - image: node:21 - before_script: - - npm install -g gh-pages@6.1.1 - - mkdir .gh-pages-cache - script: - # make sure, the DEPLOY_TOKEN is defined - - >- - [ ${CI_DEPLOY_TOKEN} ] || - echo "The CI_DEPLOY_TOKEN variable is not set. Please create an access - token with scope 'read_repository' and 'write_repository'" && - [ ${CI_DEPLOY_TOKEN} ] - - >- - CACHE_DIR=$(realpath .gh-pages-cache) - gh-pages - --dotfiles - --nojekyll - --branch gh-pages - --repo https://ci-user:${CI_DEPLOY_TOKEN}@${CI_SERVER_HOST}/${CI_PROJECT_PATH}.git - --user "${CI_COMMIT_AUTHOR}" - --message "CI Pipeline ${CI_PIPELINE_ID}, commit ${CI_COMMIT_SHORT_SHA}" - --dist docs/_build/html diff --git a/docs/_templates/.gitignore b/.nojekyll similarity index 100% rename from docs/_templates/.gitignore rename to .nojekyll diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml deleted file mode 100644 index 757e75a..0000000 --- a/.pre-commit-config.yaml +++ /dev/null @@ -1,58 +0,0 @@ -# SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH -# -# SPDX-License-Identifier: CC0-1.0 - -# https://pre-commit.com/ -repos: - - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.4.0 - hooks: - - id: trailing-whitespace - - id: end-of-file-fixer - - id: check-yaml - # isort should run before black as black sometimes tweaks the isort output - - repo: https://github.com/PyCQA/isort - rev: 5.12.0 - hooks: - - id: isort - args: - - --profile - - black - - --line-length - - "79" - - --filter-files - - -skip-gitignore - - --float-to-top - # https://github.com/python/black#version-control-integration - - repo: https://github.com/psf/black - rev: 23.1.0 - hooks: - - id: black - args: - - --line-length - - "79" - - --exclude - - venv - - repo: https://github.com/pycqa/flake8 - rev: 6.0.0 - hooks: - - id: flake8 - # - repo: https://github.com/pre-commit/mirrors-mypy # disabled for now - # rev: v1.0.1 - # hooks: - # - id: mypy - # additional_dependencies: - # - types-PyYAML - # args: - # - --ignore-missing-imports - - - repo: https://github.com/fsfe/reuse-tool - rev: v1.1.2 - hooks: - - id: reuse - - - repo: https://github.com/citation-file-format/cff-converter-python - # there is no release with this hook yet - rev: "44e8fc9" - hooks: - - id: validate-cff diff --git a/.reuse/add_license.py b/.reuse/add_license.py deleted file mode 100644 index 3342870..0000000 --- a/.reuse/add_license.py +++ /dev/null @@ -1,118 +0,0 @@ -# SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH -# -# SPDX-License-Identifier: LGPL-3.0-only - -"""Helper script to add licenses to files. - -This script can be used to apply the licenses and default copyright holders -to files in the repository. - -It uses the short cuts from the ``.reuse/shortcuts.yaml`` file and -adds them to the call of ``reuse annotate``. Any command line option however -overwrites the config in ``shortcuts.yaml`` - -Usage:: - - python .reuse/add_license.py [OPTIONS] -""" - -import os.path as osp -from argparse import ArgumentParser -from textwrap import dedent -from typing import Dict, Optional, TypedDict - -import yaml -from reuse.project import Project -from reuse.vcs import find_root - -try: - from reuse._annotate import add_arguments as _orig_add_arguments - from reuse._annotate import run -except ImportError: - # reuse < 3.0 - from reuse.header import add_arguments as _orig_add_arguments - from reuse.header import run - - -class LicenseShortCut(TypedDict): - """Shortcut to add a copyright statement""" - - #: The copyright statement - copyright: str - - #: year of copyright statement - year: str - - #: SPDX Identifier of the license - license: Optional[str] - - -def load_shortcuts() -> Dict[str, LicenseShortCut]: - """Load the ``shortcuts.yaml`` file.""" - - with open(osp.join(osp.dirname(__file__), "shortcuts.yaml")) as f: - return yaml.safe_load(f) - - -def add_arguments( - parser: ArgumentParser, shortcuts: Dict[str, LicenseShortCut] -): - parser.add_argument( - "shortcut", - choices=[key for key in shortcuts if not key.startswith(".")], - help=( - "What license should be applied? Shortcuts are loaded from " - ".reuse/shortcuts.yaml. Possible shortcuts are %(choices)s" - ), - ) - - _orig_add_arguments(parser) - - parser.set_defaults(func=run) - parser.set_defaults(parser=parser) - - -def main(argv=None): - shortcuts = load_shortcuts() - - parser = ArgumentParser( - prog=".reuse/add_license.py", - description=dedent( - """ - Add copyright and licensing into the header of files with shortcuts - - This script uses the ``reuse annotate`` command to add copyright - and licensing information into the header the specified files. - - It accepts the same arguments as ``reuse annotate``, plus an - additional required `shortcuts` argument. The given `shortcut` - comes from the file at ``.reuse/shortcuts.yaml`` to fill in - copyright, year and license identifier. - - For further information, please type ``reuse annotate --help``""" - ), - ) - add_arguments(parser, shortcuts) - - args = parser.parse_args(argv) - - shortcut = shortcuts[args.shortcut] - - if args.year is None: - args.year = [] - if args.copyright is None: - args.copyright = [] - - if args.license is None and shortcut.get("license"): - args.license = [shortcut["license"]] - elif args.license and shortcut.get("license"): - args.license.append(shortcut["license"]) - args.year.append(shortcut["year"]) - args.copyright.append(shortcut["copyright"]) - - project = Project(find_root()) - args.func(args, project) - - -if __name__ == "__main__": - main() diff --git a/.reuse/shortcuts.yaml b/.reuse/shortcuts.yaml deleted file mode 100644 index 43a4548..0000000 --- a/.reuse/shortcuts.yaml +++ /dev/null @@ -1,23 +0,0 @@ -# SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH -# -# SPDX-License-Identifier: CC0-1.0 - -.defaults: &defaults - year: "2021-2024" - copyright: "Helmholtz-Zentrum hereon GmbH" - -# The following dictionaries items map to dictionaries with three possible -# keys: -# -# copyright: The copyright statement -# year: year of copyright statement -# license: SPDX Identifier -docs: - <<: *defaults - license: "CC-BY-4.0" -code: - <<: *defaults - license: "LGPL-3.0-only" -supp: - <<: *defaults - license: "CC0-1.0" diff --git a/CHANGELOG.rst b/CHANGELOG.rst deleted file mode 100644 index 439e215..0000000 --- a/CHANGELOG.rst +++ /dev/null @@ -1,262 +0,0 @@ -.. SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH -.. -.. SPDX-License-Identifier: CC-BY-4.0 - -v1.4.3 -====== -Minor fix for grid files (`#53 __`) - -v1.4.2 -====== -Fix for compatibility with python 3.7 - -Changed -------- -- plugin entrypoint compatibility fix for python 3.7 (`#47 `__) -- ignore SNF links in linkcheck (`#49 `__) -- Replace gitter with mattermost (`#45 `__) - - -v1.4.1 -====== -Compatibility fixes and minor improvements - -Added ------ -- An abstract ``convert_coordinate`` method has been implemented for the - ``Plotter`` and ``Formatoption`` class that can be used in subclasses to - convert coordinates for the required visualization. The default - implementation does nothing (see - `#39 `__) - -Fixed ------ -- the update method now only takes the coordinates that are dimensions in the - dataset see `#39 `__ -- psyplot is now compatible with matplotlib 3.5 and python 3.10 - -Changed -------- -- loading more than one variables into a ``DataArray`` now first selects the - corresponding dimensions, then puts it into a single ``DataArray``. This - avoids loading the entire data (see - `#39 `__) - - -v1.4.0 -====== -Compatibility fixes and LGPL license - -Fixed ------ -- psyplot is now compatible with 0.18 - -Added ------ -- psyplot does now have a CITATION.cff file, see https://citation-file-format.github.io - -Changed -------- -- psyplot is now officially licensed under LGPL-3.0-only, - see `#33 `__ -- the lower bound for supported xarray versions is now 0.17. -- project files do not store the Store anymore as this information cannot be - gathered from xarray 0.18. We now rely on xarray to automatically find the - engine to open the files. -- Documentation is now hosted with Github Pages at https://psyplot.github.io/psyplot. - Redirects from the old documentation at `https://psyplot.readthedocs.io` have - been configured. -- Examples have been removed from the psyplot repository as they now live in a - central place at https://github.com/psyplot/examples -- We use CicleCI now for a standardized CI/CD pipeline to build and test - the code and docs all at one place, see `#32 `__ - -v1.3.2 -====== -Fixed ------ -- The ``get_xname``-like methods of the decoder have been fixed if they get a - variable without any dimensions. See `#30 `__ - -v1.3.1 -====== - -Fixed ------ -- 3D bounds of coordinate are not interpreted as unstructured anymore (see - `660c703 `__ - -v1.3.0 -====== -New repository, presets and compatibility fixes - -Added ------ -* You can now save and load presets for the formatoptions of a project which - applies the formatoptions that you stored in a file to a specific plot method, - see `#24 `__ -* the ``rcParams`` do now have a ``catch`` method that allows a temporary change - of formatoptions. - - Usage:: - - rcParams['some_key'] = 0 - with rcParams.catch(): - rcParams['some_key'] = 1 - assert rcParams['some_key'] == 1 - assert rcParams['some_key'] == 0 - -* ``ArrayList.from_dataset`` (and consecutively all plotmethods) now support - different input types for the decoder. You can pass an instance of the - ``CFDecoder`` class, a sub class of ``CFDecoder``, or keyword arguments - that are used to initialize the decoder, - see `#20 `__. Furthermore, the - `check_data` method of the various plotmethods now also accept a `decoder` - parameter, see `#22 `__ -* ``psyplot.data.open_dataset`` now decodes grid_mappings attributes, - see `#17 `__ -* psyplot projects now support the with syntax, e.g. something like:: - - with psy.plot.mapplot('file.nc') as sp: - sp.export('output.png') - - sp will be closed automatically (see `#18 `__) -* the update to variables with other dimensions works now as well - (see `#22 `__) -* a ``psyplot.project.Project`` now has a new ``format_string`` method to - format a string with the meta attributes of the data in the projects -* The ``ArrayList`` class now supports filtering by formatoption keys. You can - filter for plotters that have a ``cmap`` formatoption via:: - - sp1 = psy.plot.mapplot(ds) - sp2 = psy.plot.lineplot(ds) - full_sp = sp1 + sp2 - full_sp(fmts='cmap') # gives equivalent results as addressing sp1 directly - -Changed -------- -* psyplot has been moved from https://github.com/Chilipp/psyplot to https://github.com/psyplot/psyplot, - see `#16 `__ -* Specifying names in `x`, `y`, `t` and `z` attributes of the `CFDecoder` class - now means that any other attribute (such as the `coordinates` or `axis` attribute) - are ignored -* If a given variable cannot be found in the provided coords to ``CFDecoder.get_variable_by_axis``, - we fall back to the ``CFDecoder.ds.coords`` attribute, see `#19 `__ -* A bug has been fixed for initializing a ``CFDecoder`` with ``x, y, z`` and - ``t`` parameters (see `#20 `__) - - -v1.2.1 -====== -This patch fixes compatibility issues with xarray 0.12 and cdo 1.5. Additionally we now officially drop support for python 2.7. - -v1.2.0 -====== - -Added ------ -* The ``psyplot.plotter.Plotter.initialize_plot`` method now takes a - *priority* keyword to only initialize only formatoptions of a certain - priority - -Removed -------- -* The installers from the `psyplot-conda `__ - repositories have been depreceated. Instead, now download the latest - `miniconda `__ and install psyplot and the - plugins via ``conda install -c conda-forge psy-maps psyplot-gui psy-reg`` - -Changed -------- -* We generalized the handling of unstructured data as lined out in - `issue#6 `__. The new method - ``psyplot.data.CFDecoder.get_cell_node_coord`` returns the coordinates of the - nodes for a given grid cell. These informations are used by the - psy-simple and psy-maps plugins for displaying any unstructured data. See - also the example on the - `visualization of unstructured grids `__ -* We removed the inplace parameter for the CFDecoder methods since it is - deprecated with xarray 0.12 (see - `issue #8 `__). The - ``CFDecoder.decode_ds`` method now always decodes inplace - -v1.1.0 -====== -This new release mainly adds new xarray accossors (``psy``) for DataArrays -and Datasets. Additionally we provide methods to calculate the spatially -weighted mean, such as fldmean, fldstd and fldpctl. - -Added ------ -* The yaxis_inverted and xaxis_inverted is now considered when loading and - saving a matplotlib axes -* Added the ``seaborn-style`` command line argument -* Added the ``concat_dim`` command line argument -* Added the plot attribute to the DataArray and Dataset accessors. It is now - possible to plot directly from the dataset and the data array -* Added ``requires_replot`` attribute for the ``Formatoption`` class. If this - attribute is True and the formatoption is contained in an update, it is the - same as calling ``Plotter.update(replot=True))``. -* We added support for multifile datasets when saving a project. - Multifile datasets are datasets that have been opened with, e.g. - ``psyplot.data.open_mfdataset`` or - ``psyplot.project.plot.(..., mfmode=True)``. This however does - not always work with datasets opened with ``xarray.open_mfdataset``. In these - cases, you have to set the ``Dataset.psy._concat_dim`` attribute manually -* Added the ``chname`` parameter when loading a project. This parameter can - be used to display another variable from the dataset than the one stored - in the psyplot project file -* Added the ``gridweights``, ``fldmean``, ``fldstd`` and ``fldpctl`` methods - to the ``psy`` DataArray accessor to calculate weighted means, standard - deviations and percentiles over the spatial dimensions (x- and y). -* Added the ``additional_children`` and ``additional_dependencies`` parameters - to the Formatoption intialization. These parameters can be used to provide - additional children for a formatoption for one plotter class -* We added the ``psyplot.plotter.Formatoption.get_fmt_widget`` method which can - be implemented to insert widgets in the formatoptions widget of the - graphical user interface - - -v1.0.0 -====== -.. image:: https://zenodo.org/badge/87944102.svg - :target: https://zenodo.org/badge/latestdoi/87944102 - -Added ------ -* Changelog - -Changed -------- -* When creating new plots using the ``psyplot.project.Project.plot`` attribute, - ``scp`` for the newly created subproject is only called when the - corresponding ``Project`` is the current main project (``gcp(True)``) -* The ``alternate_paths`` keyword in the ``psyplot.project.Project.save_project`` - and ``psyplot.data.ArrayList.array_info`` methods has been changed to - ``alternative_paths`` -* The ``psyplot.project.Cdo`` class does not accept any of the keywords - ``returnDA, returnMaps`` or ``returnLine`` anymore. Instead it takes - the ``plot_method`` keyword and several others. -* The ``psyplot.project.close`` method by default now removes the data from - the current project and closes attached datasets -* The modules in the psyplot.plotter modules have been moved to separate - packages to make the debugging and testing easier - - - The psyplot.plotter.simple, baseplotter and colors modules have been moved - to the psy-simple_ package - - The psyplot.plotter.maps and boxes modules have been moved to the psy-maps_ - package - - The psyplot.plotter.linreg module has been moved to the psy-reg_ package -* The endings of the yaml configuration files are now all *.yml*. Hence, - - - the configuration file name is now *psyplotrc.yml* instead of - *psyplotrc.yaml* - - the default logging configuration file name is now *logging.yml* instead - of *logging.yaml* -* Under osx, the configuration directory is now also expected to be in - ``$HOME/.config/psyplot`` (as it is for linux) - - -.. _psy-simple: https://github.com/psyplot/psy-simple -.. _psy-maps: https://github.com/psyplot/psy-maps -.. _psy-reg: https://github.com/psyplot/psy-reg diff --git a/CITATION.cff b/CITATION.cff deleted file mode 100644 index 25bbf38..0000000 --- a/CITATION.cff +++ /dev/null @@ -1,55 +0,0 @@ -# SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH -# -# SPDX-License-Identifier: CC0-1.0 - -# YAML 1.2 ---- -cff-version: "1.2.0" -message: "If you use this software, please cite both the article from preferred-citation and the software itself." -title: "psyplot: Interactive data visualization with Python" -authors: - - family-names: Sommer - given-names: "Philipp S." - affiliation: "Helmholtz-Zentrum Hereon" - orcid: "https://orcid.org/0000-0001-6171-7716" - website: "https://www.philipp-s-sommer.de" - post-code: 21502 - city: Geesthacht - country: DE - email: philipp.sommer@hereon.de -doi: "10.5281/zenodo.593798" -contact: - - email: psyplot@hereon.de - name: "Psyplot developers at hereon" -license: "LGPL-3.0-only" -repository-code: https://github.com/psyplot/psyplot -type: software -keywords: - - psyplot - - python - - visualization - - xarray - - matplotlib - - netcdf4 - - interactive - - climate models - - unstructured -preferred-citation: - title: "The psyplot interactive visualization framework" - authors: - - family-names: Sommer - given-names: "Philipp S." - affiliation: "Helmholtz-Zentrum Hereon" - orcid: "https://orcid.org/0000-0001-6171-7716" - year: 2017 - type: article - doi: "10.21105/joss.00363" - date-published: 2017-08-22 - journal: Journal of Open Source Software - volume: 2 - number: 16 - pages: 363 - publisher: - name: The Open Journal - license: CC-BY-4.0 -... diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md deleted file mode 100644 index 1fb3d50..0000000 --- a/CODE_OF_CONDUCT.md +++ /dev/null @@ -1,79 +0,0 @@ - - -# Contributor Covenant Code of Conduct - -## Our Pledge - -In the interest of fostering an open and welcoming environment, we as -contributors and maintainers pledge to making participation in our project and -our community a harassment-free experience for everyone, regardless of age, body -size, disability, ethnicity, gender identity and expression, level of experience, -education, socio-economic status, nationality, personal appearance, race, -religion, or sexual identity and orientation. - -## Our Standards - -Examples of behavior that contributes to creating a positive environment -include: - -* Using welcoming and inclusive language -* Being respectful of differing viewpoints and experiences -* Gracefully accepting constructive criticism -* Focusing on what is best for the community -* Showing empathy towards other community members - -Examples of unacceptable behavior by participants include: - -* The use of sexualized language or imagery and unwelcome sexual attention or - advances -* Trolling, insulting/derogatory comments, and personal or political attacks -* Public or private harassment -* Publishing others' private information, such as a physical or electronic - address, without explicit permission -* Other conduct which could reasonably be considered inappropriate in a - professional setting - -## Our Responsibilities - -Project maintainers are responsible for clarifying the standards of acceptable -behavior and are expected to take appropriate and fair corrective action in -response to any instances of unacceptable behavior. - -Project maintainers have the right and responsibility to remove, edit, or -reject comments, commits, code, wiki edits, issues, and other contributions -that are not aligned to this Code of Conduct, or to ban temporarily or -permanently any contributor for other behaviors that they deem inappropriate, -threatening, offensive, or harmful. - -## Scope - -This Code of Conduct applies both within project spaces and in public spaces -when an individual is representing the project or its community. Examples of -representing a project or community include using an official project e-mail -address, posting via an official social media account, or acting as an appointed -representative at an online or offline event. Representation of a project may be -further defined and clarified by project maintainers. - -## Enforcement - -Instances of abusive, harassing, or otherwise unacceptable behavior may be -reported by contacting the main developer at . All -complaints will be reviewed and investigated and will result in a response that -is deemed necessary and appropriate to the circumstances. The project team is -obligated to maintain confidentiality with regard to the reporter of an incident. -Further details of specific enforcement policies may be posted separately. - -Project maintainers who do not follow or enforce the Code of Conduct in good -faith may face temporary or permanent repercussions as determined by other -members of the project's leadership. - -## Attribution - -This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, -available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html - -[homepage]: https://www.contributor-covenant.org diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index 246a7c8..0000000 --- a/CONTRIBUTING.md +++ /dev/null @@ -1,170 +0,0 @@ - - -# Contributing to psyplot - -:+1::tada: First off, thanks for taking the time to contribute! :tada::+1: - -The following is a set of guidelines for contributing to psyplot and its packages, which are hosted on GitHub. These are mostly guidelines, not rules. Use your best judgment, and feel free to propose changes to this document in a pull request. - -#### Table Of Contents - -[Code of Conduct](#code-of-conduct) - -[What should I know before I get started?](#what-should-i-know-before-i-get-started) - * [The psyplot framework](#the-psyplot-framework) - -[How Can I Contribute?](#how-can-i-contribute) - * [Reporting Bugs](#reporting-bugs) - * [Suggesting Enhancements](#suggesting-enhancements) - * [Pull Requests](#pull-requests) - * [Adding new examples](#adding-new-examples) - -[Styleguides](#styleguides) - * [Git Commit Messages](#git-commit-messages) - * [Documentation Styleguide](#documentation-styleguide) - -## Code of Conduct - -This project and everyone participating in it is governed by the [psyplot Code of Conduct](CODE_OF_CONDUCT.md). By participating, you are expected to uphold this code. - -## What should I know before I get started? - -### The psyplot framework - -`psyplot` is just the framework that allows interactive data analysis and visualization. Much of the functionality however is implemented by other packages. What package is the correct one for your bug report/feature request, can be determined by the following list - -* [psyplot-gui](https://github.com/psyplot/psyplot-gui/issues): Everything specific to the graphical user interface -* [psy-view](https://github.com/psyplot/psy-view/issues): Everything specific to the psy-view graphical user interface -* [psy-simple](https://github.com/psyplot/psy-simple/issues): Everything concerning, e.g. the `lineplot`, `plot2d`, `density` or `vector` plot methods -* [psy-maps](https://github.com/psyplot/psy-maps/issues): Everything concerning, e.g. the `mapplot`, `mapvector` `mapcombined` plot methods -* [psy-reg](https://github.com/psyplot/psy-reg/issues): Everything concerning, e.g. the `linreg` or `densityreg` plot methods -* [psyplot](https://github.com/psyplot/psyplot/issues): Everything concerning the general framework, e.g. data handling, parallel update, etc. - -Concerning plot methods, you can simply find out which module implemented it via - -```python -import psyplot.project as psy -print(psy.plot.name-of-your-plot-method._plugin) -``` - -If you still don't know, where to open the issue, just go for [psyplot](https://github.com/psyplot/psyplot/issues). - -## How Can I Contribute? - -### Reporting Bugs - -This section guides you through submitting a bug report for psyplot. Following these guidelines helps maintainers and the community understand your report, reproduce the behavior, and find related reports. - -Before creating bug reports, please check existing issues and pull requests as you might find out that you don't need to create one. When you are creating a bug report, please [include as many details as possible](#how-do-i-submit-a-good-bug-report). Fill out [the required template](.github/ISSUE_TEMPLATE.md), the information it asks for helps us resolve issues faster. - -> **Note:** If you find a **Closed** issue that seems like it is the same thing that you're experiencing, open a new issue and include a link to the original issue in the body of your new one. - -#### How Do I Submit A (Good) Bug Report? - -Bugs are tracked as [GitHub issues](https://guides.github.com/features/issues/). After you've determined [which repository](#the-psyplot-framework) your bug is related to, create an issue on that repository and provide the following information by filling in [the template](ISSUE_TEMPLATE.md). - -Explain the problem and include additional details to help maintainers reproduce the problem: - -* **Use a clear and descriptive title** for the issue to identify the problem. -* **Describe the exact steps which reproduce the problem** in as many details as possible. For example, start by explaining how you started psyplot, e.g. which command exactly you used in the terminal, or how you started psyplot otherwise. When listing steps, **don't just say what you did, but explain how you did it**. For example, did you update via GUI or console and what? -* **Provide specific examples to demonstrate the steps**. Include links to files or GitHub projects, or copy/pasteable snippets, which you use in those examples. If you're providing snippets in the issue, use [Markdown code blocks](https://docs.github.com/en/github/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax#quoting-code). -* **Describe the behavior you observed after following the steps** and point out what exactly is the problem with that behavior. -* **Explain which behavior you expected to see instead and why.** -* **Include screenshots and animated GIFs** which show you following the described steps and clearly demonstrate the problem. -* **If the problem is related to your data structure**, include a small example how a similar data structure can be generated - -Include details about your configuration and environment: - -* **Which version of psyplot are you using?** You can get the exact version by running `psyplot -aV` in your terminal, or by starting the psyplot-gui and open Help->Dependencies. -* **What's the name and version of the OS you're using**? - -### Suggesting Enhancements - -This section guides you through submitting an enhancement suggestion for psyplot, including completely new features and minor improvements to existing functionality. - -If you want to change an existing feature, use the [change feature template](https://github.com/psyplot/psyplot/issues/new?template=change_feature.md&title=CHANGE+FEATURE:), otherwise fill in the [new feature template](https://github.com/psyplot/psyplot/issues/new?template=new_feature.md&title=NEW+FEATURE:). - -#### How Do I Submit A (Good) Enhancement Suggestion? - -Enhancement suggestions are tracked as [GitHub issues](https://guides.github.com/features/issues/). After you've determined [which repository](#the-psyplot-framework) your enhancement suggestion is related to, create an issue on that repository and provide the following information: - -* **Use a clear and descriptive title** for the issue to identify the suggestion. -* **Provide a step-by-step description of the suggested enhancement** in as many details as possible. -* **Provide specific examples to demonstrate the steps**. Include copy/pasteable snippets which you use in those examples, as [Markdown code blocks](https://docs.github.com/en/github/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax#quoting-code). -* **Describe the current behavior** and **explain which behavior you expected to see instead** and why. -* **Include screenshots and animated GIFs** which help you demonstrate the steps or point out the part of psyplot which the suggestion is related to. -* **Explain why this enhancement would be useful** to most psyplot users. -* **List some other analysis software or applications where this enhancement exists.** -* **Specify which version of psyplot you're using.** You can get the exact version by running `psyplot -aV` in your terminal, or by starting the psyplot-gui and open Help->Dependencies. -* **Specify the name and version of the OS you're using.** - -### Pull Requests - -* Fill in [the required template](.github/PULL_REQUEST_TEMPLATE.md) -* Do not include issue numbers in the PR title -* Include screenshots and animated GIFs in your pull request whenever possible. -* Document new code based on the [Documentation Styleguide](#documentation-styleguide) -* End all files with a newline and follow the [PEP8](https://www.python.org/dev/peps/pep-0008/), e.g. by using [flake8](https://pypi.org/project/flake8/) - -### Adding new examples -You have new examples? Great! If you want to add them to the documentation, please just fork the correct github repository and add a jupyter notebook in the [examples](examples) directory, together with all the necessary data files. - -And we are always happy to help you finalizing incomplete pull requests. - -## Styleguides - -### Git Commit Messages - -* Use the present tense ("Add feature" not "Added feature") -* Use the imperative mood ("Move cursor to..." not "Moves cursor to...") -* Limit the first line (summary) to 72 characters or less -* Reference issues and pull requests liberally after the first line -* When only changing documentation, include `[ci skip]` in the commit title - -### Documentation Styleguide - -* Follow the [numpy documentation guidelines](https://numpydoc.readthedocs.io/en/latest/format.html#docstring-standard). -* Use [reStructuredText](http://www.sphinx-doc.org/en/master/usage/restructuredtext/basics.html). -* Try to not repeat yourself and make use of the `psyplot.docstring.docstrings` - -#### Example - -```python -@docstrings.get_sections(base='new_function') -def new_function(a=1): - """Make some cool new feature - - This function implements a cool new feature - - Parameters - ---------- - a: int - First parameter - - Returns - ------- - something awesome - The result""" - ... - -@docstrings.dedent -def another_new_function(a=1, b=2): - """Make another cool new feature - - Parameters - ---------- - %(new_function.parameters)s - b: int - Another parameter - - Returns - ------- - Something even more awesome""" - ... -``` - -> **Note:** This document has been inspired by [the contribution guidelines of Atom](https://github.com/atom/atom/blob/master/CONTRIBUTING.md#git-commit-messages) diff --git a/LICENSES/CC-BY-4.0.txt b/LICENSES/CC-BY-4.0.txt deleted file mode 100644 index 13ca539..0000000 --- a/LICENSES/CC-BY-4.0.txt +++ /dev/null @@ -1,156 +0,0 @@ -Creative Commons Attribution 4.0 International - - Creative Commons Corporation (“Creative Commons”) is not a law firm and does not provide legal services or legal advice. Distribution of Creative Commons public licenses does not create a lawyer-client or other relationship. Creative Commons makes its licenses and related information available on an “as-is” basis. Creative Commons gives no warranties regarding its licenses, any material licensed under their terms and conditions, or any related information. Creative Commons disclaims all liability for damages resulting from their use to the fullest extent possible. - -Using Creative Commons Public Licenses - -Creative Commons public licenses provide a standard set of terms and conditions that creators and other rights holders may use to share original works of authorship and other material subject to copyright and certain other rights specified in the public license below. The following considerations are for informational purposes only, are not exhaustive, and do not form part of our licenses. - -Considerations for licensors: Our public licenses are intended for use by those authorized to give the public permission to use material in ways otherwise restricted by copyright and certain other rights. Our licenses are irrevocable. Licensors should read and understand the terms and conditions of the license they choose before applying it. Licensors should also secure all rights necessary before applying our licenses so that the public can reuse the material as expected. Licensors should clearly mark any material not subject to the license. This includes other CC-licensed material, or material used under an exception or limitation to copyright. More considerations for licensors. - -Considerations for the public: By using one of our public licenses, a licensor grants the public permission to use the licensed material under specified terms and conditions. If the licensor’s permission is not necessary for any reason–for example, because of any applicable exception or limitation to copyright–then that use is not regulated by the license. Our licenses grant only permissions under copyright and certain other rights that a licensor has authority to grant. Use of the licensed material may still be restricted for other reasons, including because others have copyright or other rights in the material. A licensor may make special requests, such as asking that all changes be marked or described. Although not required by our licenses, you are encouraged to respect those requests where reasonable. More considerations for the public. - -Creative Commons Attribution 4.0 International Public License - -By exercising the Licensed Rights (defined below), You accept and agree to be bound by the terms and conditions of this Creative Commons Attribution 4.0 International Public License ("Public License"). To the extent this Public License may be interpreted as a contract, You are granted the Licensed Rights in consideration of Your acceptance of these terms and conditions, and the Licensor grants You such rights in consideration of benefits the Licensor receives from making the Licensed Material available under these terms and conditions. - -Section 1 – Definitions. - - a. Adapted Material means material subject to Copyright and Similar Rights that is derived from or based upon the Licensed Material and in which the Licensed Material is translated, altered, arranged, transformed, or otherwise modified in a manner requiring permission under the Copyright and Similar Rights held by the Licensor. For purposes of this Public License, where the Licensed Material is a musical work, performance, or sound recording, Adapted Material is always produced where the Licensed Material is synched in timed relation with a moving image. - - b. Adapter's License means the license You apply to Your Copyright and Similar Rights in Your contributions to Adapted Material in accordance with the terms and conditions of this Public License. - - c. Copyright and Similar Rights means copyright and/or similar rights closely related to copyright including, without limitation, performance, broadcast, sound recording, and Sui Generis Database Rights, without regard to how the rights are labeled or categorized. For purposes of this Public License, the rights specified in Section 2(b)(1)-(2) are not Copyright and Similar Rights. - - d. Effective Technological Measures means those measures that, in the absence of proper authority, may not be circumvented under laws fulfilling obligations under Article 11 of the WIPO Copyright Treaty adopted on December 20, 1996, and/or similar international agreements. - - e. Exceptions and Limitations means fair use, fair dealing, and/or any other exception or limitation to Copyright and Similar Rights that applies to Your use of the Licensed Material. - - f. Licensed Material means the artistic or literary work, database, or other material to which the Licensor applied this Public License. - - g. Licensed Rights means the rights granted to You subject to the terms and conditions of this Public License, which are limited to all Copyright and Similar Rights that apply to Your use of the Licensed Material and that the Licensor has authority to license. - - h. Licensor means the individual(s) or entity(ies) granting rights under this Public License. - - i. Share means to provide material to the public by any means or process that requires permission under the Licensed Rights, such as reproduction, public display, public performance, distribution, dissemination, communication, or importation, and to make material available to the public including in ways that members of the public may access the material from a place and at a time individually chosen by them. - - j. Sui Generis Database Rights means rights other than copyright resulting from Directive 96/9/EC of the European Parliament and of the Council of 11 March 1996 on the legal protection of databases, as amended and/or succeeded, as well as other essentially equivalent rights anywhere in the world. - - k. You means the individual or entity exercising the Licensed Rights under this Public License. Your has a corresponding meaning. - -Section 2 – Scope. - - a. License grant. - - 1. Subject to the terms and conditions of this Public License, the Licensor hereby grants You a worldwide, royalty-free, non-sublicensable, non-exclusive, irrevocable license to exercise the Licensed Rights in the Licensed Material to: - - A. reproduce and Share the Licensed Material, in whole or in part; and - - B. produce, reproduce, and Share Adapted Material. - - 2. Exceptions and Limitations. For the avoidance of doubt, where Exceptions and Limitations apply to Your use, this Public License does not apply, and You do not need to comply with its terms and conditions. - - 3. Term. The term of this Public License is specified in Section 6(a). - - 4. Media and formats; technical modifications allowed. The Licensor authorizes You to exercise the Licensed Rights in all media and formats whether now known or hereafter created, and to make technical modifications necessary to do so. The Licensor waives and/or agrees not to assert any right or authority to forbid You from making technical modifications necessary to exercise the Licensed Rights, including technical modifications necessary to circumvent Effective Technological Measures. For purposes of this Public License, simply making modifications authorized by this Section 2(a)(4) never produces Adapted Material. - - 5. Downstream recipients. - - A. Offer from the Licensor – Licensed Material. Every recipient of the Licensed Material automatically receives an offer from the Licensor to exercise the Licensed Rights under the terms and conditions of this Public License. - - B. No downstream restrictions. You may not offer or impose any additional or different terms or conditions on, or apply any Effective Technological Measures to, the Licensed Material if doing so restricts exercise of the Licensed Rights by any recipient of the Licensed Material. - - 6. No endorsement. Nothing in this Public License constitutes or may be construed as permission to assert or imply that You are, or that Your use of the Licensed Material is, connected with, or sponsored, endorsed, or granted official status by, the Licensor or others designated to receive attribution as provided in Section 3(a)(1)(A)(i). - -b. Other rights. - - 1. Moral rights, such as the right of integrity, are not licensed under this Public License, nor are publicity, privacy, and/or other similar personality rights; however, to the extent possible, the Licensor waives and/or agrees not to assert any such rights held by the Licensor to the limited extent necessary to allow You to exercise the Licensed Rights, but not otherwise. - - 2. Patent and trademark rights are not licensed under this Public License. - - 3. To the extent possible, the Licensor waives any right to collect royalties from You for the exercise of the Licensed Rights, whether directly or through a collecting society under any voluntary or waivable statutory or compulsory licensing scheme. In all other cases the Licensor expressly reserves any right to collect such royalties. - -Section 3 – License Conditions. - -Your exercise of the Licensed Rights is expressly made subject to the following conditions. - - a. Attribution. - - 1. If You Share the Licensed Material (including in modified form), You must: - - A. retain the following if it is supplied by the Licensor with the Licensed Material: - - i. identification of the creator(s) of the Licensed Material and any others designated to receive attribution, in any reasonable manner requested by the Licensor (including by pseudonym if designated); - - ii. a copyright notice; - - iii. a notice that refers to this Public License; - - iv. a notice that refers to the disclaimer of warranties; - - v. a URI or hyperlink to the Licensed Material to the extent reasonably practicable; - - B. indicate if You modified the Licensed Material and retain an indication of any previous modifications; and - - C. indicate the Licensed Material is licensed under this Public License, and include the text of, or the URI or hyperlink to, this Public License. - - 2. You may satisfy the conditions in Section 3(a)(1) in any reasonable manner based on the medium, means, and context in which You Share the Licensed Material. For example, it may be reasonable to satisfy the conditions by providing a URI or hyperlink to a resource that includes the required information. - - 3. If requested by the Licensor, You must remove any of the information required by Section 3(a)(1)(A) to the extent reasonably practicable. - - 4. If You Share Adapted Material You produce, the Adapter's License You apply must not prevent recipients of the Adapted Material from complying with this Public License. - -Section 4 – Sui Generis Database Rights. - -Where the Licensed Rights include Sui Generis Database Rights that apply to Your use of the Licensed Material: - - a. for the avoidance of doubt, Section 2(a)(1) grants You the right to extract, reuse, reproduce, and Share all or a substantial portion of the contents of the database; - - b. if You include all or a substantial portion of the database contents in a database in which You have Sui Generis Database Rights, then the database in which You have Sui Generis Database Rights (but not its individual contents) is Adapted Material; and - - c. You must comply with the conditions in Section 3(a) if You Share all or a substantial portion of the contents of the database. -For the avoidance of doubt, this Section 4 supplements and does not replace Your obligations under this Public License where the Licensed Rights include other Copyright and Similar Rights. - -Section 5 – Disclaimer of Warranties and Limitation of Liability. - - a. Unless otherwise separately undertaken by the Licensor, to the extent possible, the Licensor offers the Licensed Material as-is and as-available, and makes no representations or warranties of any kind concerning the Licensed Material, whether express, implied, statutory, or other. This includes, without limitation, warranties of title, merchantability, fitness for a particular purpose, non-infringement, absence of latent or other defects, accuracy, or the presence or absence of errors, whether or not known or discoverable. Where disclaimers of warranties are not allowed in full or in part, this disclaimer may not apply to You. - - b. To the extent possible, in no event will the Licensor be liable to You on any legal theory (including, without limitation, negligence) or otherwise for any direct, special, indirect, incidental, consequential, punitive, exemplary, or other losses, costs, expenses, or damages arising out of this Public License or use of the Licensed Material, even if the Licensor has been advised of the possibility of such losses, costs, expenses, or damages. Where a limitation of liability is not allowed in full or in part, this limitation may not apply to You. - - c. The disclaimer of warranties and limitation of liability provided above shall be interpreted in a manner that, to the extent possible, most closely approximates an absolute disclaimer and waiver of all liability. - -Section 6 – Term and Termination. - - a. This Public License applies for the term of the Copyright and Similar Rights licensed here. However, if You fail to comply with this Public License, then Your rights under this Public License terminate automatically. - - b. Where Your right to use the Licensed Material has terminated under Section 6(a), it reinstates: - - 1. automatically as of the date the violation is cured, provided it is cured within 30 days of Your discovery of the violation; or - - 2. upon express reinstatement by the Licensor. - - c. For the avoidance of doubt, this Section 6(b) does not affect any right the Licensor may have to seek remedies for Your violations of this Public License. - - d. For the avoidance of doubt, the Licensor may also offer the Licensed Material under separate terms or conditions or stop distributing the Licensed Material at any time; however, doing so will not terminate this Public License. - - e. Sections 1, 5, 6, 7, and 8 survive termination of this Public License. - -Section 7 – Other Terms and Conditions. - - a. The Licensor shall not be bound by any additional or different terms or conditions communicated by You unless expressly agreed. - - b. Any arrangements, understandings, or agreements regarding the Licensed Material not stated herein are separate from and independent of the terms and conditions of this Public License. - -Section 8 – Interpretation. - - a. For the avoidance of doubt, this Public License does not, and shall not be interpreted to, reduce, limit, restrict, or impose conditions on any use of the Licensed Material that could lawfully be made without permission under this Public License. - - b. To the extent possible, if any provision of this Public License is deemed unenforceable, it shall be automatically reformed to the minimum extent necessary to make it enforceable. If the provision cannot be reformed, it shall be severed from this Public License without affecting the enforceability of the remaining terms and conditions. - - c. No term or condition of this Public License will be waived and no failure to comply consented to unless expressly agreed to by the Licensor. - - d. Nothing in this Public License constitutes or may be interpreted as a limitation upon, or waiver of, any privileges and immunities that apply to the Licensor or You, including from the legal processes of any jurisdiction or authority. - -Creative Commons is not a party to its public licenses. Notwithstanding, Creative Commons may elect to apply one of its public licenses to material it publishes and in those instances will be considered the “Licensor.” Except for the limited purpose of indicating that material is shared under a Creative Commons public license or as otherwise permitted by the Creative Commons policies published at creativecommons.org/policies, Creative Commons does not authorize the use of the trademark “Creative Commons” or any other trademark or logo of Creative Commons without its prior written consent including, without limitation, in connection with any unauthorized modifications to any of its public licenses or any other arrangements, understandings, or agreements concerning use of licensed material. For the avoidance of doubt, this paragraph does not form part of the public licenses. - -Creative Commons may be contacted at creativecommons.org. diff --git a/LICENSES/CC0-1.0.txt b/LICENSES/CC0-1.0.txt deleted file mode 100644 index 0e259d4..0000000 --- a/LICENSES/CC0-1.0.txt +++ /dev/null @@ -1,121 +0,0 @@ -Creative Commons Legal Code - -CC0 1.0 Universal - - CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE - LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN - ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS - INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES - REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS - PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM - THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED - HEREUNDER. - -Statement of Purpose - -The laws of most jurisdictions throughout the world automatically confer -exclusive Copyright and Related Rights (defined below) upon the creator -and subsequent owner(s) (each and all, an "owner") of an original work of -authorship and/or a database (each, a "Work"). - -Certain owners wish to permanently relinquish those rights to a Work for -the purpose of contributing to a commons of creative, cultural and -scientific works ("Commons") that the public can reliably and without fear -of later claims of infringement build upon, modify, incorporate in other -works, reuse and redistribute as freely as possible in any form whatsoever -and for any purposes, including without limitation commercial purposes. -These owners may contribute to the Commons to promote the ideal of a free -culture and the further production of creative, cultural and scientific -works, or to gain reputation or greater distribution for their Work in -part through the use and efforts of others. - -For these and/or other purposes and motivations, and without any -expectation of additional consideration or compensation, the person -associating CC0 with a Work (the "Affirmer"), to the extent that he or she -is an owner of Copyright and Related Rights in the Work, voluntarily -elects to apply CC0 to the Work and publicly distribute the Work under its -terms, with knowledge of his or her Copyright and Related Rights in the -Work and the meaning and intended legal effect of CC0 on those rights. - -1. Copyright and Related Rights. A Work made available under CC0 may be -protected by copyright and related or neighboring rights ("Copyright and -Related Rights"). Copyright and Related Rights include, but are not -limited to, the following: - - i. the right to reproduce, adapt, distribute, perform, display, - communicate, and translate a Work; - ii. moral rights retained by the original author(s) and/or performer(s); -iii. publicity and privacy rights pertaining to a person's image or - likeness depicted in a Work; - iv. rights protecting against unfair competition in regards to a Work, - subject to the limitations in paragraph 4(a), below; - v. rights protecting the extraction, dissemination, use and reuse of data - in a Work; - vi. database rights (such as those arising under Directive 96/9/EC of the - European Parliament and of the Council of 11 March 1996 on the legal - protection of databases, and under any national implementation - thereof, including any amended or successor version of such - directive); and -vii. other similar, equivalent or corresponding rights throughout the - world based on applicable law or treaty, and any national - implementations thereof. - -2. Waiver. To the greatest extent permitted by, but not in contravention -of, applicable law, Affirmer hereby overtly, fully, permanently, -irrevocably and unconditionally waives, abandons, and surrenders all of -Affirmer's Copyright and Related Rights and associated claims and causes -of action, whether now known or unknown (including existing as well as -future claims and causes of action), in the Work (i) in all territories -worldwide, (ii) for the maximum duration provided by applicable law or -treaty (including future time extensions), (iii) in any current or future -medium and for any number of copies, and (iv) for any purpose whatsoever, -including without limitation commercial, advertising or promotional -purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each -member of the public at large and to the detriment of Affirmer's heirs and -successors, fully intending that such Waiver shall not be subject to -revocation, rescission, cancellation, termination, or any other legal or -equitable action to disrupt the quiet enjoyment of the Work by the public -as contemplated by Affirmer's express Statement of Purpose. - -3. Public License Fallback. Should any part of the Waiver for any reason -be judged legally invalid or ineffective under applicable law, then the -Waiver shall be preserved to the maximum extent permitted taking into -account Affirmer's express Statement of Purpose. In addition, to the -extent the Waiver is so judged Affirmer hereby grants to each affected -person a royalty-free, non transferable, non sublicensable, non exclusive, -irrevocable and unconditional license to exercise Affirmer's Copyright and -Related Rights in the Work (i) in all territories worldwide, (ii) for the -maximum duration provided by applicable law or treaty (including future -time extensions), (iii) in any current or future medium and for any number -of copies, and (iv) for any purpose whatsoever, including without -limitation commercial, advertising or promotional purposes (the -"License"). The License shall be deemed effective as of the date CC0 was -applied by Affirmer to the Work. Should any part of the License for any -reason be judged legally invalid or ineffective under applicable law, such -partial invalidity or ineffectiveness shall not invalidate the remainder -of the License, and in such case Affirmer hereby affirms that he or she -will not (i) exercise any of his or her remaining Copyright and Related -Rights in the Work or (ii) assert any associated claims and causes of -action with respect to the Work, in either case contrary to Affirmer's -express Statement of Purpose. - -4. Limitations and Disclaimers. - - a. No trademark or patent rights held by Affirmer are waived, abandoned, - surrendered, licensed or otherwise affected by this document. - b. Affirmer offers the Work as-is and makes no representations or - warranties of any kind concerning the Work, express, implied, - statutory or otherwise, including without limitation warranties of - title, merchantability, fitness for a particular purpose, non - infringement, or the absence of latent or other defects, accuracy, or - the present or absence of errors, whether or not discoverable, all to - the greatest extent permissible under applicable law. - c. Affirmer disclaims responsibility for clearing rights of other persons - that may apply to the Work or any use thereof, including without - limitation any person's Copyright and Related Rights in the Work. - Further, Affirmer disclaims responsibility for obtaining any necessary - consents, permissions or other rights required for any use of the - Work. - d. Affirmer understands and acknowledges that Creative Commons is not a - party to this document and has no duty or obligation with respect to - this CC0 or use of the Work. diff --git a/LICENSES/LGPL-3.0-only.txt b/LICENSES/LGPL-3.0-only.txt deleted file mode 100644 index 513d1c0..0000000 --- a/LICENSES/LGPL-3.0-only.txt +++ /dev/null @@ -1,304 +0,0 @@ -GNU LESSER GENERAL PUBLIC LICENSE -Version 3, 29 June 2007 - -Copyright (C) 2007 Free Software Foundation, Inc. - -Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. - -This version of the GNU Lesser General Public License incorporates the terms and conditions of version 3 of the GNU General Public License, supplemented by the additional permissions listed below. - -0. Additional Definitions. - -As used herein, "this License" refers to version 3 of the GNU Lesser General Public License, and the "GNU GPL" refers to version 3 of the GNU General Public License. - -"The Library" refers to a covered work governed by this License, other than an Application or a Combined Work as defined below. - -An "Application" is any work that makes use of an interface provided by the Library, but which is not otherwise based on the Library. Defining a subclass of a class defined by the Library is deemed a mode of using an interface provided by the Library. - -A "Combined Work" is a work produced by combining or linking an Application with the Library. The particular version of the Library with which the Combined Work was made is also called the "Linked Version". - -The "Minimal Corresponding Source" for a Combined Work means the Corresponding Source for the Combined Work, excluding any source code for portions of the Combined Work that, considered in isolation, are based on the Application, and not on the Linked Version. - -The "Corresponding Application Code" for a Combined Work means the object code and/or source code for the Application, including any data and utility programs needed for reproducing the Combined Work from the Application, but excluding the System Libraries of the Combined Work. - -1. Exception to Section 3 of the GNU GPL. -You may convey a covered work under sections 3 and 4 of this License without being bound by section 3 of the GNU GPL. - -2. Conveying Modified Versions. -If you modify a copy of the Library, and, in your modifications, a facility refers to a function or data to be supplied by an Application that uses the facility (other than as an argument passed when the facility is invoked), then you may convey a copy of the modified version: - - a) under this License, provided that you make a good faith effort to ensure that, in the event an Application does not supply the function or data, the facility still operates, and performs whatever part of its purpose remains meaningful, or - - b) under the GNU GPL, with none of the additional permissions of this License applicable to that copy. - -3. Object Code Incorporating Material from Library Header Files. -The object code form of an Application may incorporate material from a header file that is part of the Library. You may convey such object code under terms of your choice, provided that, if the incorporated material is not limited to numerical parameters, data structure layouts and accessors, or small macros, inline functions and templates (ten or fewer lines in length), you do both of the following: - - a) Give prominent notice with each copy of the object code that the Library is used in it and that the Library and its use are covered by this License. - - b) Accompany the object code with a copy of the GNU GPL and this license document. - -4. Combined Works. -You may convey a Combined Work under terms of your choice that, taken together, effectively do not restrict modification of the portions of the Library contained in the Combined Work and reverse engineering for debugging such modifications, if you also do each of the following: - - a) Give prominent notice with each copy of the Combined Work that the Library is used in it and that the Library and its use are covered by this License. - - b) Accompany the Combined Work with a copy of the GNU GPL and this license document. - - c) For a Combined Work that displays copyright notices during execution, include the copyright notice for the Library among these notices, as well as a reference directing the user to the copies of the GNU GPL and this license document. - - d) Do one of the following: - - 0) Convey the Minimal Corresponding Source under the terms of this License, and the Corresponding Application Code in a form suitable for, and under terms that permit, the user to recombine or relink the Application with a modified version of the Linked Version to produce a modified Combined Work, in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source. - - 1) Use a suitable shared library mechanism for linking with the Library. A suitable mechanism is one that (a) uses at run time a copy of the Library already present on the user's computer system, and (b) will operate properly with a modified version of the Library that is interface-compatible with the Linked Version. - - e) Provide Installation Information, but only if you would otherwise be required to provide such information under section 6 of the GNU GPL, and only to the extent that such information is necessary to install and execute a modified version of the Combined Work produced by recombining or relinking the Application with a modified version of the Linked Version. (If you use option 4d0, the Installation Information must accompany the Minimal Corresponding Source and Corresponding Application Code. If you use option 4d1, you must provide the Installation Information in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source.) - -5. Combined Libraries. -You may place library facilities that are a work based on the Library side by side in a single library together with other library facilities that are not Applications and are not covered by this License, and convey such a combined library under terms of your choice, if you do both of the following: - - a) Accompany the combined library with a copy of the same work based on the Library, uncombined with any other library facilities, conveyed under the terms of this License. - - b) Give prominent notice with the combined library that part of it is a work based on the Library, and explaining where to find the accompanying uncombined form of the same work. - -6. Revised Versions of the GNU Lesser General Public License. -The Free Software Foundation may publish revised and/or new versions of the GNU Lesser General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. - -Each version is given a distinguishing version number. If the Library as you received it specifies that a certain numbered version of the GNU Lesser General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that published version or of any later version published by the Free Software Foundation. If the Library as you received it does not specify a version number of the GNU Lesser General Public License, you may choose any version of the GNU Lesser General Public License ever published by the Free Software Foundation. - -If the Library as you received it specifies that a proxy can decide whether future versions of the GNU Lesser General Public License shall -apply, that proxy's public statement of acceptance of any version is permanent authorization for you to choose that version for the Library. - -GNU GENERAL PUBLIC LICENSE -Version 3, 29 June 2007 - -Copyright © 2007 Free Software Foundation, Inc. - -Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. - -Preamble - -The GNU General Public License is a free, copyleft license for software and other kinds of works. - -The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. - -When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. - -To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. - -For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. - -Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. - -For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. - -Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. - -Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. - -The precise terms and conditions for copying, distribution and modification follow. - -TERMS AND CONDITIONS - -0. Definitions. - -“This License” refers to version 3 of the GNU General Public License. - -“Copyright” also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. - -“The Program” refers to any copyrightable work licensed under this License. Each licensee is addressed as “you”. “Licensees” and “recipients” may be individuals or organizations. - -To “modify” a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a “modified version” of the earlier work or a work “based on” the earlier work. - -A “covered work” means either the unmodified Program or a work based on the Program. - -To “propagate” a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. - -To “convey” a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. - -An interactive user interface displays “Appropriate Legal Notices” to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. - -1. Source Code. -The “source code” for a work means the preferred form of the work for making modifications to it. “Object code” means any non-source form of a work. - -A “Standard Interface” means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. - -The “System Libraries” of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A “Major Component”, in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. - -The “Corresponding Source” for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. - -The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. - -The Corresponding Source for a work in source code form is that same work. - -2. Basic Permissions. -All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. - -You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. - -Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. - -3. Protecting Users' Legal Rights From Anti-Circumvention Law. -No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. - -When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. - -4. Conveying Verbatim Copies. -You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. - -You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. - -5. Conveying Modified Source Versions. -You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: - - a) The work must carry prominent notices stating that you modified it, and giving a relevant date. - - b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to “keep intact all notices”. - - c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. - - d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. - -A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an “aggregate” if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. - -6. Conveying Non-Source Forms. -You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: - - a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. - - b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. - - c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. - - d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. - - e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. - -A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. - -A “User Product” is either (1) a “consumer product”, which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, “normally used” refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. - -“Installation Information” for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. - -If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). - -The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. - -Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. - -7. Additional Terms. -“Additional permissions” are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. - -When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. - -Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: - - a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or - - b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or - - c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or - - d) Limiting the use for publicity purposes of names of licensors or authors of the material; or - - e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or - - f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. - -All other non-permissive additional terms are considered “further restrictions” within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. - -If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. - -Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. - -8. Termination. -You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). - -However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. - -Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. - -Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. - -9. Acceptance Not Required for Having Copies. -You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. - -10. Automatic Licensing of Downstream Recipients. -Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. - -An “entity transaction” is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. - -You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. - -11. Patents. -A “contributor” is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's “contributor version”. - -A contributor's “essential patent claims” are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, “control” includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. - -Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. - -In the following three paragraphs, a “patent license” is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To “grant” such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. - -If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. “Knowingly relying” means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. - -If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. - -A patent license is “discriminatory” if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. - -Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. - -12. No Surrender of Others' Freedom. -If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. - -13. Use with the GNU Affero General Public License. -Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. - -14. Revised Versions of this License. -The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. - -Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License “or any later version” applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. - -If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. - -Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. - -15. Disclaimer of Warranty. -THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM “AS IS” WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. - -16. Limitation of Liability. -IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. - -17. Interpretation of Sections 15 and 16. -If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. - -END OF TERMS AND CONDITIONS - -How to Apply These Terms to Your New Programs - -If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. - -To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the “copyright” line and a pointer to where the full notice is found. - - - Copyright (C) - - This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. - - This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with this program. If not, see . - -Also add information on how to contact you by electronic and paper mail. - -If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: - - Copyright (C) - This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. - This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. - -The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an “about box”. - -You should also get your employer (if you work as a programmer) or school, if any, to sign a “copyright disclaimer” for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see . - -The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read . diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index 2b48ff0..0000000 --- a/MANIFEST.in +++ /dev/null @@ -1,11 +0,0 @@ -# SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH -# -# SPDX-License-Identifier: CC0-1.0 - -include README.rst -include COPYING -include COPYING.LESSER -include psyplot/plugin-template-files/* -include psyplot/plugin-template-files/plugin_template/* -include versioneer.py -include psyplot/_version.py diff --git a/Makefile b/Makefile deleted file mode 100644 index b23d43a..0000000 --- a/Makefile +++ /dev/null @@ -1,125 +0,0 @@ -# SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH -# -# SPDX-License-Identifier: CC0-1.0 - -.PHONY: clean clean-build clean-pyc clean-test coverage dist docs help install lint lint/flake8 lint/black -.DEFAULT_GOAL := help - -define BROWSER_PYSCRIPT -import os, webbrowser, sys - -from urllib.request import pathname2url - -webbrowser.open("file://" + pathname2url(os.path.abspath(sys.argv[1]))) -endef -export BROWSER_PYSCRIPT - -define PRINT_HELP_PYSCRIPT -import re, sys - -for line in sys.stdin: - match = re.match(r'^([a-zA-Z_-]+):.*?## (.*)$$', line) - if match: - target, help = match.groups() - print("%-20s %s" % (target, help)) -endef -export PRINT_HELP_PYSCRIPT - -BROWSER := python -c "$$BROWSER_PYSCRIPT" - -help: - @python -c "$$PRINT_HELP_PYSCRIPT" < $(MAKEFILE_LIST) - -clean: clean-build clean-pyc clean-test clean-venv ## remove all build, virtual environments, test, coverage and Python artifacts - -clean-build: ## remove build artifacts - rm -fr build/ - rm -fr dist/ - rm -fr .eggs/ - find . -name '*.egg-info' -exec rm -fr {} + - find . -name '*.egg' -exec rm -f {} + - -clean-pyc: ## remove Python file artifacts - find . -name '*.pyc' -exec rm -f {} + - find . -name '*.pyo' -exec rm -f {} + - find . -name '*~' -exec rm -f {} + - find . -name '__pycache__' -exec rm -fr {} + - -clean-test: ## remove test and coverage artifacts - rm -fr .tox/ - rm -f .coverage - rm -fr htmlcov/ - rm -fr .pytest_cache - -clean-venv: # remove the virtual environment - rm -rf venv - -lint/isort: ## check style with flake8 - isort --check psyplot tests -lint/flake8: ## check style with flake8 - flake8 psyplot tests -lint/black: ## check style with black - black --check psyplot tests - blackdoc --check psyplot tests -lint/reuse: ## check licenses - reuse lint - -lint: lint/isort lint/black lint/flake8 lint/reuse ## check style - -formatting: - isort psyplot tests - black psyplot tests - blackdoc psyplot tests - -quick-test: ## run tests quickly with the default Python - python -m pytest - -pipenv-test: ## run tox - pipenv run isort --check psyplot - pipenv run black --line-length 79 --check psyplot - pipenv run flake8 psyplot - pipenv run pytest -v --cov=psyplot -x - pipenv run reuse lint - pipenv run cffconvert --validate - -test: ## run tox - tox - -test-all: test test-docs ## run tests and test the docs - -coverage: ## check code coverage quickly with the default Python - python -m pytest --cov psyplot --cov-report=html - $(BROWSER) htmlcov/index.html - -docs: ## generate Sphinx HTML documentation, including API docs - $(MAKE) -C docs clean - $(MAKE) -C docs html - $(BROWSER) docs/_build/html/index.html - -test-docs: ## generate Sphinx HTML documentation, including API docs - $(MAKE) -C docs clean - $(MAKE) -C docs linkcheck - -servedocs: docs ## compile the docs watching for changes - watchmedo shell-command -p '*.rst' -c '$(MAKE) -C docs html' -R -D . - -release: dist ## package and upload a release - twine upload dist/* - -dist: clean ## builds source and wheel package - python -m build - ls -l dist - -install: clean ## install the package to the active Python's site-packages - python -m pip install . - -dev-install: clean - python -m pip install -r docs/requirements.txt - python -m pip install -e .[dev] - pre-commit install - -venv-install: clean - python -m venv venv - venv/bin/python -m pip install -r docs/requirements.txt - venv/bin/python -m pip install -e .[dev] - venv/bin/pre-commit install diff --git a/README.rst b/README.rst deleted file mode 100644 index 7a02c79..0000000 --- a/README.rst +++ /dev/null @@ -1,167 +0,0 @@ -.. SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH -.. -.. SPDX-License-Identifier: CC-BY-4.0 - -=============================================== -The psyplot interactive visualization framework -=============================================== - -.. start-badges - -|CI| -|Code coverage| -|Latest Release| -|PyPI version| -|Code style: black| -|Imports: isort| -|PEP8| -|REUSE status| - -.. end-badges - -Welcome! **psyplot** is an open source python project that mainly combines the -plotting utilities of matplotlib_ and the data management of the xarray_ -package. The main purpose is to have a framework that allows a fast, -attractive, flexible, easily applicable, easily reproducible and especially -an interactive visualization of your data. - -The ultimate goal is to help scientists and especially climate model -developers in their daily work by providing a flexible visualization tool that -can be enhanced by their own visualization scripts. ``psyplot`` can be used -through the python command line and through the psyplot-gui_ module which -provides a graphical user interface for an easier interactive usage. - -The package is very new and there are many features that will be included in -the future. So we are very pleased for feedback! Please simply raise an issue -on `GitHub `__ (see also -`How to contribute`_ in the docs). - -.. _psyplot-gui: http://psyplot.github.io/psyplot-gui/ -.. _How to contribute: http://psyplot.github.io/psyplot/contribute.html - -You can see the full documentation on -`psyplot.github.io/psyplot `__. - - -Get in touch ------------- -Any quesions? Do not hessitate to get in touch with the psyplot developers. - -- Create an issue at the `bug tracker`_ -- Chat with the developers in out `team on mattermost`_ -- Subscribe to the `mailing list`_ and ask for support -- Sent a mail to psyplot@hzg.de - -See also the `code of conduct`_, and our `contribution guide`_ for more -information and a guide about good bug reports. - -.. _bug tracker: https://github.com/psyplot/psyplot -.. _team on mattermost: https://mattermost.hzdr.de/psyplot/ -.. _mailing list: https://www.listserv.dfn.de/sympa/subscribe/psyplot -.. _code of conduct: https://github.com/psyplot/psyplot/blob/master/CODE_OF_CONDUCT.md -.. _contribution guide: https://github.com/psyplot/psyplot/blob/master/CONTRIBUTING.md - - -How to cite psyplot -------------------- - -When using psyplot, you should at least cite the publication in -`the Journal of Open Source Software`_: - -.. image:: http://joss.theoj.org/papers/3535c28017003f0b5fb63b1b64118b60/status.svg - :alt: Journal of Open Source Software - :target: http://joss.theoj.org/papers/3535c28017003f0b5fb63b1b64118b60 - -Sommer, P. S.: The psyplot interactive visualization framework, -*The Journal of Open Source Software*, 2, doi:10.21105/joss.00363, -https://doi.org/10.21105/joss.00363, 2017. - -Furthermore, each release of psyplot and it's subprojects_ is -associated with a DOI using zenodo.org_. If you want to cite a specific -version or plugin, please refer to the `releases page of psyplot` or the -releases page of the corresponding subproject. - - -.. _the Journal of Open Source Software: http://joss.theoj.org/ -.. _subprojects: https://psyplot.github.io/ -.. _zenodo.org: https://zenodo.org/ -.. _releases page of psyplot: https://github.com/psyplot/psyplot/releases/ - - -Acknowledgment --------------- -This package is being developed by Philipp S. Sommer at the -`Helmholtz Coastal Data Center (HCDC)`_ of the `Helmholtz-Zentrum Hereon`_. - -I want to thank the developers of the matplotlib_, xarray_ and cartopy_ -packages for their great packages and of course the python developers for their -fascinating work on this beautiful language. - -A special thanks to Stefan Hagemann and Tobias Stacke from the -Max-Planck-Institute of Meteorology in Hamburg, Germany for the motivation on -this project and to the people of the `Not yet visible`_ agency for their -advice in designing the logo and webpage. - -Finally the author thanks the Swiss National Science Foundation (SNF) for their -support. Funding for the author came from the `ACACIA grant (CR10I2_146314)`_ -and the `HORNET grant (200021_169598)`_. - -.. _Helmholtz Coastal Data Center (HCDC): https://hcdc.hereon.de -.. _Helmholtz-Zentrum Hereon: https://www.hereon.de -.. _matplotlib: http://matplotlib.org -.. _xarray: http://xarray.pydata.org/ -.. _cartopy: http://scitools.org.uk/cartopy -.. _Not yet visible: https://notyetvisible.de/ -.. _ACACIA grant (CR10I2_146314): http://p3.snf.ch/project-146314 -.. _HORNET grant (200021_169598): http://p3.snf.ch/project-169598 - - - -Note ----- -Commits on github prior to version 1.0 were moved into another repository, the -`psyplot_old`_ repository. This has been done because prior to version 1.0, -the github repository contained all the reference figures used for testing -which made the size of the repository too large. - -.. _psyplot_old: https://github.com/Chilipp/psyplot_old - - -Copyright ---------- -Copyright © 2021 Helmholtz-Zentrum Hereon, 2020-2021 Helmholtz-Zentrum -Geesthacht, 2016-2021 University of Lausanne - -psyplot is released under the GNU LGPL-3.O license. -See COPYING and COPYING.LESSER in the root of the repository for full -licensing details. - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU Lesser General Public License version 3.0 as -published by the Free Software Foundation. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU LGPL-3.0 license for more details. - -You should have received a copy of the GNU LGPL-3.0 license -along with this program. If not, see https://www.gnu.org/licenses/. - - -.. |CI| image:: https://codebase.helmholtz.cloud/psyplot/psyplot/badges/master/pipeline.svg - :target: https://codebase.helmholtz.cloud/psyplot/psyplot/-/pipelines?page=1&scope=all&ref=master -.. |Code coverage| image:: https://codebase.helmholtz.cloud/psyplot/psyplot/badges/master/coverage.svg - :target: https://codebase.helmholtz.cloud/psyplot/psyplot/-/graphs/develop/charts -.. |Latest Release| image:: https://codebase.helmholtz.cloud/psyplot/psyplot/-/badges/release.svg - :target: https://codebase.helmholtz.cloud/psyplot/psyplot -.. |PyPI version| image:: https://img.shields.io/pypi/v/psyplot.svg - :target: https://pypi.python.org/pypi/psyplot/ -.. |Code style: black| image:: https://img.shields.io/badge/code%20style-black-000000.svg - :target: https://github.com/psf/black -.. |Imports: isort| image:: https://img.shields.io/badge/%20imports-isort-%231674b1?style=flat&labelColor=ef8336 - :target: https://pycqa.github.io/isort/ -.. |PEP8| image:: https://img.shields.io/badge/code%20style-pep8-orange.svg - :target: https://www.python.org/dev/peps/pep-0008/ -.. |REUSE status| image:: https://api.reuse.software/badge/codebase.helmholtz.cloud/psyplot/psyplot - :target: https://api.reuse.software/info/codebase.helmholtz.cloud/psyplot/psyplot diff --git a/docs/demo.nc b/_downloads/6d351965500a2dd3ef53e3daa3ac50d1/demo.nc similarity index 100% rename from docs/demo.nc rename to _downloads/6d351965500a2dd3ef53e3daa3ac50d1/demo.nc diff --git a/_images/docs_dataarray_accessor_1.png b/_images/docs_dataarray_accessor_1.png new file mode 100644 index 0000000..a792697 Binary files /dev/null and b/_images/docs_dataarray_accessor_1.png differ diff --git a/_images/docs_dataarray_accessor_2.png b/_images/docs_dataarray_accessor_2.png new file mode 100644 index 0000000..41e51f1 Binary files /dev/null and b/_images/docs_dataarray_accessor_2.png differ diff --git a/_images/docs_dataarray_accessor_4.png b/_images/docs_dataarray_accessor_4.png new file mode 100644 index 0000000..96c429e Binary files /dev/null and b/_images/docs_dataarray_accessor_4.png differ diff --git a/_images/docs_dataarray_accessor_5.png b/_images/docs_dataarray_accessor_5.png new file mode 100644 index 0000000..e40b402 Binary files /dev/null and b/_images/docs_dataarray_accessor_5.png differ diff --git a/_images/docs_dataset_accessor.png b/_images/docs_dataset_accessor.png new file mode 100644 index 0000000..3584d59 Binary files /dev/null and b/_images/docs_dataset_accessor.png differ diff --git a/_images/docs_demo_MyPlotter_simple.png b/_images/docs_demo_MyPlotter_simple.png new file mode 100644 index 0000000..60681dd Binary files /dev/null and b/_images/docs_demo_MyPlotter_simple.png differ diff --git a/_images/docs_framework_plotter_demo.png b/_images/docs_framework_plotter_demo.png new file mode 100644 index 0000000..a792697 Binary files /dev/null and b/_images/docs_framework_plotter_demo.png differ diff --git a/_images/docs_framework_project_demo1.png b/_images/docs_framework_project_demo1.png new file mode 100644 index 0000000..5456991 Binary files /dev/null and b/_images/docs_framework_project_demo1.png differ diff --git a/_images/docs_framework_project_demo2.png b/_images/docs_framework_project_demo2.png new file mode 100644 index 0000000..d37a8f7 Binary files /dev/null and b/_images/docs_framework_project_demo2.png differ diff --git a/_images/docs_getting_started.png b/_images/docs_getting_started.png new file mode 100644 index 0000000..a792697 Binary files /dev/null and b/_images/docs_getting_started.png differ diff --git a/_images/docs_getting_started_1.png b/_images/docs_getting_started_1.png new file mode 100644 index 0000000..543e8ba Binary files /dev/null and b/_images/docs_getting_started_1.png differ diff --git a/_images/docs_multiple_plots.png b/_images/docs_multiple_plots.png new file mode 100644 index 0000000..6016d07 Binary files /dev/null and b/_images/docs_multiple_plots.png differ diff --git a/_images/docs_presets_1.png b/_images/docs_presets_1.png new file mode 100644 index 0000000..fd1bde3 Binary files /dev/null and b/_images/docs_presets_1.png differ diff --git a/_images/docs_presets_2.png b/_images/docs_presets_2.png new file mode 100644 index 0000000..fd1bde3 Binary files /dev/null and b/_images/docs_presets_2.png differ diff --git a/docs/_static/psyplot.png b/_images/psyplot.png similarity index 100% rename from docs/_static/psyplot.png rename to _images/psyplot.png diff --git a/docs/develop/psyplot_framework.gif b/_images/psyplot_framework.gif similarity index 100% rename from docs/develop/psyplot_framework.gif rename to _images/psyplot_framework.gif diff --git a/_modules/index.html b/_modules/index.html new file mode 100644 index 0000000..9c30427 --- /dev/null +++ b/_modules/index.html @@ -0,0 +1,419 @@ + + + + + + Overview: module code — psyplot documentation + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+
    +
  • + +
  • +
  • +
+
+
+ + + + +
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/psyplot.html b/_modules/psyplot.html new file mode 100644 index 0000000..bcc9834 --- /dev/null +++ b/_modules/psyplot.html @@ -0,0 +1,586 @@ + + + + + + psyplot — psyplot documentation + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for psyplot

+"""psyplot visualization framework."""
+
+# SPDX-FileCopyrightText: 2016-2024 University of Lausanne
+# SPDX-FileCopyrightText: 2020-2021 Helmholtz-Zentrum Geesthacht
+# SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH
+#
+# SPDX-License-Identifier: LGPL-3.0-only
+
+import datetime as dt
+import logging as _logging
+import sys
+
+import psyplot.config as config
+from psyplot.config.rcsetup import rcParams
+from psyplot.data import (  # noqa: F401
+    ArrayList,
+    InteractiveArray,
+    InteractiveList,
+    open_dataset,
+    open_mfdataset,
+)
+from psyplot.warning import critical, disable_warnings, warn  # noqa: F401
+
+from ._version import get_versions
+
+__version__ = get_versions()["version"]
+del get_versions
+
+
+__author__ = "Philipp S. Sommer"
+__copyright__ = """
+2016-2024 University of Lausanne
+2020-2021 Helmholtz-Zentrum Geesthacht
+2021-2024 Helmholtz-Zentrum hereon GmbH
+"""
+__credits__ = ["Philipp S. Sommer"]
+__license__ = "LGPL-3.0-only"
+
+__maintainer__ = "Philipp S. Sommer"
+__email__ = "psyplot@hereon.de"
+
+__status__ = "Production"
+
+
+logger = _logging.getLogger(__name__)
+logger.debug(
+    "%s: Initializing psyplot, version %s",
+    dt.datetime.now().isoformat(),
+    __version__,
+)
+logger.debug("Logging configuration file: %s", config.logcfg_path)
+logger.debug("Configuration file: %s", config.config_path)
+
+
+rcParams.HEADER += "\n\npsyplot version: " + __version__
+rcParams.load_plugins()
+rcParams.load_from_file()
+
+
+_project_imported = False
+
+#: Boolean that is True, if psyplot runs inside the graphical user interface
+#: by the ``psyplot_gui`` module
+with_gui = False
+
+
+
+[docs] +def get_versions(requirements=True, key=None): + """ + Get the version information for psyplot, the plugins and its requirements + + Parameters + ---------- + requirements: bool + If True, the requirements of the plugins and psyplot are investigated + key: func + A function that determines whether a plugin shall be considererd or + not. The function must take a single argument, that is the name of the + plugin as string, and must return True (import the plugin) or False + (skip the plugin). If None, all plugins are imported + + Returns + ------- + dict + A mapping from ``'psyplot'``/the plugin names to a dictionary with the + ``'version'`` key and the corresponding version is returned. If + `requirements` is True, it also contains a mapping from + ``'requirements'`` a dictionary with the versions + + Examples + -------- + Using the built-in JSON module, we get something like + + .. code-block:: python + + import json + + print(json.dumps(psyplot.get_versions(), indent=4)) + { + "psy_simple.plugin": {"version": "1.0.0.dev0"}, + "psyplot": { + "version": "1.0.0.dev0", + "requirements": { + "matplotlib": "1.5.3", + "numpy": "1.11.3", + "pandas": "0.19.2", + "xarray": "0.9.1", + }, + }, + "psy_maps.plugin": { + "version": "1.0.0.dev0", + "requirements": {"cartopy": "0.15.0"}, + }, + } + """ + from psyplot.utils import plugin_entrypoints + + eps = plugin_entrypoints("psyplot", "plugin") + + ret = {"psyplot": _get_versions(requirements)} + for ep in eps: + if str(ep) in rcParams._plugins: + logger.debug("Loading entrypoint %s", ep) + + try: + ep.module + except AttributeError: # python<3.10 + ep.module = ep.pattern.match(ep.value).group("module") + + if key is not None and not key(ep.module): + continue + try: + mod = ep.load() + except (ImportError, ModuleNotFoundError): + logger.debug("Could not import %s" % (ep,), exc_info=True) + logger.warning("Could not import %s" % (ep,), exc_info=True) + else: + try: + ret[str(ep.module)] = mod.get_versions(requirements) + except AttributeError: + ret[str(ep.module)] = { + "version": getattr( + mod, + "plugin_version", + getattr(mod, "__version__", ""), + ) + } + if key is None: + try: + import psyplot_gui + except ImportError: + pass + else: + ret["psyplot_gui"] = psyplot_gui.get_versions(requirements) + return ret
+ + + +def _get_versions(requirements=True): + if requirements: + import matplotlib as mpl + import numpy as np + import pandas as pd + import xarray as xr + + return { + "version": __version__, + "requirements": { + "matplotlib": mpl.__version__, + "xarray": xr.__version__, + "pandas": pd.__version__, + "numpy": np.__version__, + "python": " ".join(sys.version.splitlines()), + }, + } + else: + return {"version": __version__} +
+ +
+
+ + + +
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/psyplot/config/logsetup.html b/_modules/psyplot/config/logsetup.html new file mode 100644 index 0000000..ef76f5c --- /dev/null +++ b/_modules/psyplot/config/logsetup.html @@ -0,0 +1,507 @@ + + + + + + psyplot.config.logsetup — psyplot documentation + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for psyplot.config.logsetup

+"""Logging configuration module of the psyplot package
+
+This module defines the essential functions for setting up the
+:class:`logging.Logger` instances that are used by the psyplot package."""
+
+# SPDX-FileCopyrightText: 2016-2024 University of Lausanne
+# SPDX-FileCopyrightText: 2020-2021 Helmholtz-Zentrum Geesthacht
+
+# SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH
+#
+# SPDX-License-Identifier: LGPL-3.0-only
+
+import logging
+import logging.config
+import os
+import sys
+
+import six
+import yaml
+
+from psyplot.docstring import dedent
+
+
+def _get_home():
+    """Find user's home directory if possible.
+    Otherwise, returns None.
+
+    :see:  http://mail.python.org/pipermail/python-list/2005-February/325395.html
+
+    This function is copied from matplotlib version 1.4.3, Jan 2016
+    """
+    try:
+        if six.PY2 and sys.platform == "win32":
+            path = os.path.expanduser(b"~").decode(sys.getfilesystemencoding())
+        else:
+            path = os.path.expanduser("~")
+    except ImportError:
+        # This happens on Google App Engine (pwd module is not present).
+        pass
+    else:
+        if os.path.isdir(path):
+            return path
+    for evar in ("HOME", "USERPROFILE", "TMP"):
+        path = os.environ.get(evar)
+        if path is not None and os.path.isdir(path):
+            return path
+    return None
+
+
+
+[docs] +@dedent +def setup_logging( + default_path=None, default_level=logging.INFO, env_key="LOG_PSYPLOT" +): + """ + Setup logging configuration + + Parameters + ---------- + default_path: str + Default path of the yaml logging configuration file. If None, it + defaults to the 'logging.yaml' file in the config directory + default_level: int + Default: :data:`logging.INFO`. Default level if default_path does not + exist + env_key: str + environment variable specifying a different logging file than + `default_path` (Default: 'LOG_CFG') + + Returns + ------- + path: str + Path to the logging configuration file + + Notes + ----- + Function taken from + http://victorlin.me/posts/2012/08/26/good-logging-practice-in-python""" + path = default_path or os.path.join( + os.path.dirname(__file__), "logging.yml" + ) + value = os.getenv(env_key, None) + home = _get_home() + if value: + path = value + if os.path.exists(path): + with open(path, "rt") as f: + config = yaml.load(f.read(), Loader=yaml.SafeLoader) + for handler in config.get("handlers", {}).values(): + if "~" in handler.get("filename", ""): + handler["filename"] = handler["filename"].replace("~", home) + logging.config.dictConfig(config) + else: + path = None + logging.basicConfig(level=default_level) + return path
+ +
+ +
+
+ + + +
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/psyplot/config/rcsetup.html b/_modules/psyplot/config/rcsetup.html new file mode 100644 index 0000000..3b84d01 --- /dev/null +++ b/_modules/psyplot/config/rcsetup.html @@ -0,0 +1,1863 @@ + + + + + + psyplot.config.rcsetup — psyplot documentation + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for psyplot.config.rcsetup

+"""Default management of the psyplot package
+
+This module defines the necessary classes, data and functions for the default
+configuration of the module.
+The structure is motivated and to larger parts taken from the matplotlib_
+package.
+
+.. _matplotlib: http://matplotlib.org/api/"""
+
+# SPDX-FileCopyrightText: 2016-2024 University of Lausanne
+# SPDX-FileCopyrightText: 2020-2021 Helmholtz-Zentrum Geesthacht
+
+# SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH
+#
+# SPDX-License-Identifier: LGPL-3.0-only
+
+import contextlib
+import inspect
+import logging
+import os
+import re
+import sys
+from collections import UserDict, defaultdict
+from itertools import chain
+
+import six
+import yaml
+
+from psyplot.config.logsetup import _get_home
+from psyplot.docstring import dedent, docstrings, safe_modulo
+from psyplot.utils import isstring
+from psyplot.warning import warn
+
+
+
+[docs] +@docstrings.get_sections(base="safe_list") +@dedent +def safe_list(iterable): + """Function to create a list + + Parameters + ---------- + iterable: iterable or anything else + Parameter that shall be converted to a list. + + - If string or any non-iterable, it will be put into a list + - if iterable, it will be converted to a list + + Returns + ------- + list + `l` put (or converted) into a list""" + if isstring(iterable): + return [iterable] + try: + return list(iterable) + except TypeError: + return [iterable]
+ + + +
+[docs] +class SubDict(UserDict, dict): # type: ignore + """Class that keeps week reference to the base dictionary + + This class is used by the :meth:`RcParams.find_and_replace` method + to provide an easy handable instance that keeps reference to the + base rcParams dictionary.""" + + @property + def data(self): + """Dictionary representing this :class:`SubDict` instance + + See Also + -------- + iteritems + """ + return dict(list(self.iteritems())) + + @property + def replace(self): + """:class:`bool`. If True, matching strings in the :attr:`base_str` + attribute are replaced with an empty string.""" + return self._replace + + @replace.setter + def replace(self, value): + def replace_base(key): + for pattern in self.patterns: + try: + return pattern.match(key).group("key") + except AttributeError: # if match is None + pass + raise KeyError( + "Could not find any matching key for %s in the base " + "dictionary!" % key + ) + + value = bool(value) + if hasattr(self, "_replace") and value == self._replace: + return + if not hasattr(self, "_replace"): + self._replace = value + return + # if the value has changed, we change the key in the SubDict instance + # to match the ones in the base dictionary (if they exist) + for key, val in iter(dict.items(self)): + try: + if value: + new_key = replace_base(key) + else: + new_key = self._get_val_and_base(key)[0] + except KeyError: + continue + else: + dict.__setitem__(self, new_key, dict.pop(self, key)) + self._replace = value + + #: :class:`dict`. Reference dictionary + base = {} + + #: list of strings. The strings that are used to set and get a specific key + #: from the :attr:`base` dictionary + base_str = [] + + #: list of compiled patterns from the :attr:`base_str` attribute, that + #: are used to look for the matching keys in :attr:`base` + patterns = [] + + #: :class:`bool`. If True, changes are traced back to the :attr:`base` dict + trace = False + +
+[docs] + @docstrings.get_sections(base="SubDict.add_base_str") + @dedent + def add_base_str( + self, base_str, pattern=".+", pattern_base=None, append=True + ): + r""" + Add further base string to this instance + + Parameters + ---------- + base_str: str or list of str + Strings that are used as to look for keys to get and set keys in + the :attr:`base` dictionary. If a string does not contain + ``'%(key)s'``, it will be appended at the end. ``'%(key)s'`` will + be replaced by the specific key for getting and setting an item. + pattern: str + Default: ``'.+'``. This is the pattern that is inserted for + ``%(key)s`` in a base string to look for matches (using the + :mod:`re` module) in the `base` dictionary. The default `pattern` + matches everything without white spaces. + pattern_base: str or list or str + If None, the whatever is given in the `base_str` is used. + Those strings will be used for generating the final search + patterns. You can specify this parameter by yourself to avoid the + misinterpretation of patterns. For example for a `base_str` like + ``'my.str'`` it is recommended to additionally provide the + `pattern_base` keyword with ``'my\.str'``. + Like for `base_str`, the ``%(key)s`` is appended if not already in + the string. + append: bool + If True, the given `base_str` are appended (i.e. it is first + looked for them in the :attr:`base` dictionary), otherwise they are + put at the beginning""" + base_str = safe_list(base_str) + pattern_base = safe_list(pattern_base or []) + for i, s in enumerate(base_str): + if "%(key)s" not in s: + base_str[i] += "%(key)s" + if pattern_base: + for i, s in enumerate(pattern_base): + if "%(key)s" not in s: + pattern_base[i] += "%(key)s" + else: + pattern_base = base_str + self.base_str = base_str + self.base_str + self.patterns = ( + list( + map( + lambda s: re.compile( + s.replace("%(key)s", "(?P<key>%s)" % pattern) + ), + pattern_base, + ) + ) + + self.patterns + )
+ + + docstrings.delete_params("SubDict.add_base_str.parameters", "append") + + @docstrings.get_sections(base="SubDict") + @docstrings.dedent + def __init__( + self, + base, + base_str, + pattern=".+", + pattern_base=None, + trace=False, + replace=True, + ): + """ + Parameters + ---------- + base: dict + base dictionary + %(SubDict.add_base_str.parameters.no_append)s + trace: bool + Default: False. If True, changes in the SubDict are traced back to + the `base` dictionary. You can change this behaviour also + afterwards by changing the :attr:`trace` attribute + replace: bool + Default: True. If True, everything but the '%%(key)s' part in a + base string is replaced (see examples below) + + + Notes + ----- + - If a key of matches multiple strings in `base_str`, the first + matching one is used. + - the SubDict class is (of course) not that efficient as the + :attr:`base` dictionary, since we loop multiple times through it's + keys + + Examples + -------- + Initialization example:: + + >>> from psyplot import rcParams + >>> d = rcParams.find_and_replace(['plotter.baseplotter.', + ... 'plotter.vector.']) + >>> print d['title'] + + >>> print d['arrowsize'] + 1.0 + + To convert it to a usual dictionary, simply use the :attr:`data` + attribute:: + + >>> d.data + {'title': None, 'arrowsize': 1.0, ...} + + Note that changing one keyword of your :class:`SubDict` will not change + the :attr:`base` dictionary, unless you set the :attr:`trace` attribute + to ``True``:: + + >>> d['title'] = 'my title' + >>> print(d['title']) + my title + + >>> print(rcParams['plotter.baseplotter.title']) + + >>> d.trace = True + >>> d['title'] = 'my second title' + >>> print(d['title']) + my second title + >>> print(rcParams['plotter.baseplotter.title']) + my second title + + Furthermore, changing the :attr:`replace` attribute will change how you + can access the keys:: + + >>> d.replace = False + + # now setting d['title'] = 'anything' would raise an error (since + # d.trace is set to True and 'title' is not a key in the rcParams + # dictionary. Instead we need + >>> d['plotter.baseplotter.title'] = 'anything' + + See Also + -------- + RcParams.find_and_replace""" + self.base = base + self.base_str = [] + self.patterns = [] + self.replace = bool(replace) + self.trace = bool(trace) + self.add_base_str( + base_str, pattern=pattern, pattern_base=pattern_base, append=False + ) + + def __getitem__(self, key): + if key in iter(dict.keys(self)): + return dict.__getitem__(self, key) + if not self.replace: + return self.base[key] + return self._get_val_and_base(key)[1] + + def __setitem__(self, key, val): + # set it in the SubDict instance if trace is False + if not self.trace: + dict.__setitem__(self, key, val) + return + base = self.base + # set it with the given key, if trace is True + if not self.replace: + base[key] = val + dict.pop(self, key, None) + return + # first look if the key already exists in the base dictionary + for s, patt in self._iter_base_and_pattern(key): + m = patt.match(s) + if m and s in base: + base[m.group()] = val + return + # if the key does not exist, we set it + self.base[key] = val + + def _get_val_and_base(self, key): + found = False + e = None + for s, patt in self._iter_base_and_pattern(key): + found = True + try: + m = patt.match(s) + if m: + return m.group(), self.base[m.group()] + else: + raise KeyError( + "{0} does not match the specified pattern!".format(s) + ) + except KeyError: + pass + if not found: + if e is not None: + raise + raise KeyError("{0} does not match the specified pattern!".format(key)) + + def _iter_base_and_pattern(self, key): + return zip( + map(lambda s: safe_modulo(s, {"key": key}), self.base_str), + self.patterns, + ) + +
+[docs] + def iterkeys(self): + """Unsorted iterator over keys""" + patterns = self.patterns + replace = self.replace + seen = set() + for key in six.iterkeys(self.base): + for pattern in patterns: + m = pattern.match(key) + if m: + ret = m.group("key") if replace else m.group() + if ret not in seen: + seen.add(ret) + yield ret + break + for key in iter(dict.keys(self)): + if key not in seen: + yield key
+ + +
+[docs] + def iteritems(self): + """Unsorted iterator over items""" + return ((key, self[key]) for key in self.iterkeys())
+ + +
+[docs] + def itervalues(self): + """Unsorted iterator over values""" + return (val for key, val in self.iteritems())
+ + +
+[docs] + def update(self, *args, **kwargs): + """Update the dictionary""" + for k, v in six.iteritems(dict(*args, **kwargs)): + self[k] = v
+
+ + + +docstrings.delete_params("SubDict.parameters", "base") + + +
+[docs] +class RcParams(dict): + """A dictionary object including validation + + validating functions are defined and associated with rc parameters in + :data:`defaultParams` + + This class is essentially the same as in maplotlibs + :class:`~matplotlib.RcParams` but has the additional + :meth:`find_and_replace` method.""" + + @property + def validate(self): + """Dictionary with validation methods as values""" + depr = self._all_deprecated + return dict( + (key, val[1]) + for key, val in six.iteritems(self.defaultParams) + if key not in depr + ) + + @property + def descriptions(self): + """The description of each keyword in the rcParams dictionary""" + return { + key: val[2] + for key, val in six.iteritems(self.defaultParams) + if len(val) >= 3 + } + + HEADER = """Configuration parameters of the psyplot module + +You can copy this file (or parts of it) to another path and save it as +psyplotrc.yml. The directory should then be stored in the PSYPLOTCONFIGDIR +environment variable.""" + + msg_depr = "%s is deprecated and replaced with %s; please use the latter." + msg_depr_ignore = "%s is deprecated and ignored. Use %s" + + #: possible connections that shall be called if the rcParams value change + _connections = defaultdict(list) + + #: the names of the entry points that are loaded during the + #: :meth:`load_plugins` method + _plugins = [] + + @property + def _all_deprecated(self): + return set(chain(self._deprecated_ignore_map, self._deprecated_map)) + + @property + def defaultParams(self): + return getattr(self, "_defaultParams", defaultParams) + + @defaultParams.setter + def defaultParams(self, value): + self._defaultParams = value + + @defaultParams.deleter + def defaultParams(self): + del self._defaultParams + + # validate values on the way in + def __init__(self, *args, **kwargs): + """ + Parameters + ---------- + defaultParams: dict + The defaultParams to use (see the :attr:`defaultParams` attribute). + By default, the :attr:`psyplot.config.rcsetup.defaultParams` + dictionary is used + + Other Parameters + ---------------- + *args, **kwargs + Any key-value pair for the initialization of the dictionary + """ + defaultParams = kwargs.pop("defaultParams", None) + if defaultParams is not None: + self.defaultParams = defaultParams + self._deprecated_map = {} + self._deprecated_ignore_map = {} + for k, v in six.iteritems(dict(*args, **kwargs)): + try: + self[k] = v + except (ValueError, RuntimeError): + # force the issue + warn( + _rcparam_warn_str.format( + key=repr(k), value=repr(v), func="__init__" + ) + ) + dict.__setitem__(self, k, v) + + def __setitem__(self, key, val): + key, val = self._get_depreceated(key, val) + if key is None: + return + try: + cval = self.validate[key](val) + except ValueError as ve: + raise ValueError("Key %s: %s" % (key, str(ve))) + dict.__setitem__(self, key, cval) + for func in self._connections.get(key, []): + func(cval) + + def _get_depreceated(self, key, *args): + if key in self._deprecated_map: + alt_key, alt_val = self._deprecated_map[key] + warn(self.msg_depr % (key, alt_key)) + key = alt_key + return key, alt_val(args[0]) if args else None + elif key in self._deprecated_ignore_map: + alt = self._deprecated_ignore_map[key] + warn(self.msg_depr_ignore % (key, alt)) + return None, None + elif key not in self.defaultParams: + raise KeyError( + "%s is not a valid rc parameter. See rcParams.keys() for a " + "list of valid parameters." % (key,) + ) + return key, args[0] if args else None + + def __getitem__(self, key): + key = self._get_depreceated(key)[0] + if key is not None: + return dict.__getitem__(self, key) + +
+[docs] + def connect(self, key, func): + """Connect a function to the given formatoption + + Parameters + ---------- + key: str + The rcParams key + func: function + The function that shall be called when the rcParams key changes. + It must accept a single value that is the new value of the + key.""" + key = self._get_depreceated(key)[0] + if key is not None: + self._connections[key].append(func)
+ + +
+[docs] + def disconnect(self, key=None, func=None): + """Disconnect the connections to the an rcParams key + + Parameters + ---------- + key: str + The rcParams key. If None, all keys are used + func: function + The function that is connected. If None, all functions are + connected + """ + if key is None: + for key, connections in self._connections.items(): + for conn in connections[:]: + if func is None or conn is func: + connections.remove(conn) + else: + connections = self._connections[key] + for conn in connections[:]: + if func is None or conn is func: + connections.remove(conn)
+ + +
+[docs] + def remove(self, key, func): + key = self._get_depreceated(key)[0] + if key is not None: + self._connections[key].remove(func)
+ + + # the default dict `update` does not use __setitem__ + # so rcParams.update(...) (such as in seaborn) side-steps + # all of the validation over-ride update to force + # through __setitem__ +
+[docs] + def update(self, *args, **kwargs): + for k, v in six.iteritems(dict(*args, **kwargs)): + try: + self[k] = v + except (ValueError, RuntimeError): + # force the issue + warn( + _rcparam_warn_str.format( + key=repr(k), value=repr(v), func="update" + ) + ) + dict.__setitem__(self, k, v)
+ + +
+[docs] + def update_from_defaultParams(self, defaultParams=None, plotters=True): + """Update from the a dictionary like the :attr:`defaultParams` + + Parameters + ---------- + defaultParams: dict + The :attr:`defaultParams` like dictionary. If None, the + :attr:`defaultParams` attribute will be updated + plotters: bool + If True, ``'project.plotters'`` will be updated too""" + if defaultParams is None: + defaultParams = self.defaultParams + self.update( + { + key: val[0] + for key, val in defaultParams.items() + if plotters or key != "project.plotters" + } + )
+ + + def __repr__(self): + import pprint + + class_name = self.__class__.__name__ + indent = len(class_name) + 1 + repr_split = pprint.pformat( + dict(self), indent=1, width=80 - indent + ).split("\n") + repr_indented = ("\n" + " " * indent).join(repr_split) + return "{0}({1})".format(class_name, repr_indented) + + def __str__(self): + return "\n".join( + "{0}: {1}".format(k, v) for k, v in sorted(self.items()) + ) + +
+[docs] + def keys(self): + """ + Return sorted list of keys. + """ + k = list(dict.keys(self)) + k.sort() + return k
+ + +
+[docs] + def values(self): + """ + Return values in order of sorted keys. + """ + return [self[k] for k in self.keys()]
+ + +
+[docs] + def find_all(self, pattern): + """ + Return the subset of this RcParams dictionary whose keys match, + using :func:`re.search`, the given ``pattern``. + + Parameters + ---------- + pattern: str + pattern as suitable for re.compile + + Returns + ------- + RcParams + RcParams instance with entries that match the given `pattern` + + Notes + ----- + Changes to the returned dictionary are (different from + :meth:`find_and_replace` are *not* propagated to the parent RcParams + dictionary. + + See Also + -------- + find_and_replace""" + pattern_re = re.compile(pattern) + ret = RcParams() + ret.defaultParams = self.defaultParams + ret.update( + (key, value) + for key, value in self.items() + if pattern_re.search(key) + ) + return ret
+ + +
+[docs] + @docstrings.dedent + def find_and_replace(self, *args, **kwargs): + """ + Like :meth:`find_all` but the given strings are replaced + + This method returns a dictionary-like object that keeps weak reference + to this rcParams instance. The resulting `SubDict` instance takes the + keys from this rcParams instance but leaves away what is found in + `base_str`. + + ``*args`` and ``**kwargs`` are determined by the :class:`SubDict` + class, where the `base` dictionary is this one. + + Parameters + ---------- + %(SubDict.parameters.no_base)s + + Returns + ------- + SubDict + SubDict with this rcParams instance as reference. + + Examples + -------- + The syntax is the same as for the initialization of the + :class:`SubDict` class:: + + >>> from psyplot import rcParams + >>> d = rcParams.find_and_replace(['plotter.baseplotter.', + ... 'plotter.vector.']) + >>> print(d['title']) + None + + >>> print(d['arrowsize']) + 1.0 + + See Also + -------- + find_all + SubDict""" + return SubDict(self, *args, **kwargs)
+ + +
+[docs] + def load_from_file(self, fname=None): + """Update rcParams from user-defined settings + + This function updates the instance with what is found in `fname` + + Parameters + ---------- + fname: str + Path to the yaml configuration file. Possible keys of the + dictionary are defined by :data:`config.rcsetup.defaultParams`. + If None, the :func:`config.rcsetup.psyplot_fname` function is used. + + See Also + -------- + dump_to_file, psyplot_fname""" + fname = fname or psyplot_fname() + if fname and os.path.exists(fname): + with open(fname) as f: + d = yaml.load(f, Loader=yaml.SafeLoader) + self.update(d) + if ( + d.get("project.plotters.user") + and "project.plotters" in self + ): + self["project.plotters"].update(d["project.plotters.user"])
+ + +
+[docs] + def dump( + self, + fname=None, + overwrite=True, + include_keys=None, + exclude_keys=["project.plotters"], + include_descriptions=True, + **kwargs, + ): + """Dump this instance to a yaml file + + Parameters + ---------- + fname: str or None + file name to write to. If None, the string that would be written + to a file is returned + overwrite: bool + If True and `fname` already exists, it will be overwritten + include_keys: None or list of str + Keys in the dictionary to be included. If None, all keys are + included + exclude_keys: list of str + Keys from the :class:`RcParams` instance to be excluded + + Other Parameters + ---------------- + ``**kwargs`` + Any other parameter for the :func:`yaml.dump` function + + Returns + ------- + str or None + if fname is ``None``, the string is returned. Otherwise, ``None`` + is returned + + Raises + ------ + IOError + If `fname` already exists and `overwrite` is False + + See Also + -------- + load_from_file""" + if fname is not None and not overwrite and os.path.exists(fname): + raise IOError( + "%s already exists! Set overwrite=True to overwrite it!" + % (fname) + ) + if six.PY2: + kwargs.setdefault("encoding", "utf-8") + d = { + key: val + for key, val in six.iteritems(self) + if (include_keys is None or key in include_keys) + and key not in exclude_keys + } + kwargs["default_flow_style"] = False + if include_descriptions: + s = yaml.dump(d, **kwargs) + desc = self.descriptions + i = 2 + header = ( + self.HEADER.splitlines() + + ["", "Created with python", ""] + + sys.version.splitlines() + + ["", ""] + ) + lines = ["# " + line for line in header] + s.splitlines() + for line in lines[2:]: + key = line.split(":")[0] + if key in desc: + lines.insert(i, "# " + "\n# ".join(desc[key].splitlines())) + i += 1 + i += 1 + s = "\n".join(lines) + if fname is None: + return s + else: + with open(fname, "w") as f: + f.write(s) + else: + if fname is None: + return yaml.dump(d, **kwargs) + with open(fname, "w") as f: + yaml.dump(d, f, **kwargs) + return None
+ + + def _load_plugin_entrypoints(self): + """Load the modules for the psyplot plugins + + Yields + ------ + importlib.metadata.EntryPoint + The entry point for the psyplot plugin module""" + from psyplot.utils import plugin_entrypoints + + def load_plugin(ep): + try: + ep.module + except AttributeError: # python<3.10 + try: + ep.module = ep.pattern.match(ep.value).group("module") + except AttributeError: # python<3.8 + ep.module = ep.module_name + + if plugins_env == ["no"]: + return False + elif ep.module in exclude_plugins: + return False + elif include_plugins and ep.module not in include_plugins: + return False + return True + + self._plugins = self._plugins or [] + + plugins_env = os.getenv("PSYPLOT_PLUGINS", "").split("::") + include_plugins = [s[4:] for s in plugins_env if s.startswith("yes:")] + exclude_plugins = [s[3:] for s in plugins_env if s.startswith("no:")] + + logger = logging.getLogger(__name__) + + eps = plugin_entrypoints("psyplot", "plugin") + for ep in eps: + if not load_plugin(ep): + logger.debug("Skipping entrypoint %s", ep) + continue + self._plugins.append(str(ep)) + logger.debug("Loading entrypoint %s", ep) + yield ep + +
+[docs] + def load_plugins(self, raise_error=False): + """ + Load the plotters and defaultParams from the plugins + + This method loads the `plotters` attribute and `defaultParams` + attribute from the plugins that use the entry point specified by + `group`. Entry points must be objects (or modules) that have a + `defaultParams` and a `plotters` attribute. + + Parameters + ---------- + raise_error: bool + If True, an error is raised when multiple plugins define the same + plotter or rcParams key. Otherwise only a warning is raised""" + + pm_env = os.getenv("PSYPLOT_PLOTMETHODS", "").split("::") + include_pms = [s[4:] for s in pm_env if s.startswith("yes:")] + exclude_pms = [s[3:] for s in pm_env if s.startswith("no:")] + + logger = logging.getLogger(__name__) + + plotters = self["project.plotters"] + def_plots = {"default": list(plotters)} + defaultParams = self.defaultParams + def_keys = {"default": defaultParams} + + def register_pm(ep, name): + full_name = "%s:%s" % (ep.module, name) + ret = True + if pm_env == ["no"]: + ret = False + elif name in exclude_pms or full_name in exclude_pms: + ret = False + elif include_pms and ( + name not in include_pms and full_name not in include_pms + ): + ret = False + if not ret: + logger.debug("Skipping plot method %s", full_name) + return ret + + for ep in self._load_plugin_entrypoints(): + try: + plugin_mod = ep.load() + except (ModuleNotFoundError, ImportError): + logger.debug("Failed to import %s!" % (ep,), exc_info=True) + logger.warning("Failed to import %s!" % (ep,)) + continue + rc = plugin_mod.rcParams + + # load the plotters + plugin_plotters = { + key: val + for key, val in rc.get("project.plotters", {}).items() + if register_pm(ep, key) + } + already_defined = set(plotters).intersection(plugin_plotters) + if already_defined: + msg = ( + "Error while loading psyplot plugin %s! The " + "following plotters have already been " + "defined" + ) % (ep,) + msg += "and will be overwritten:" if not raise_error else ":" + msg += "\n" + "\n".join( + chain.from_iterable( + ( + ( + "%s by %s" % (key, plugin) + for plugin, keys in def_plots.items() + if key in keys + ) + for key in already_defined + ) + ) + ) + if raise_error: + raise ImportError(msg) + else: + warn(msg) + for d in plugin_plotters.values(): + d["plugin"] = ep.module + plotters.update(plugin_plotters) + def_plots[ep] = list(plugin_plotters) + + # load the defaultParams keys + plugin_defaultParams = rc.defaultParams + already_defined = set(defaultParams).intersection( + plugin_defaultParams + ) - {"project.plotters"} + if already_defined: + msg = ( + "Error while loading psyplot plugin %s! The " + "following default keys have already been " + "defined:" + ) % (ep,) + msg += "\n" + "\n".join( + chain.from_iterable( + ( + ( + "%s by %s" % (key, plugin) + for plugin, keys in def_keys.items() + if key in keys + ) + for key in already_defined + ) + ) + ) + if raise_error: + raise ImportError(msg) + else: + warn(msg) + update_keys = set(plugin_defaultParams) - {"project.plotters"} + def_keys[ep] = update_keys + self.defaultParams.update( + {key: plugin_defaultParams[key] for key in update_keys} + ) + + # load the rcParams (without validation) + super(RcParams, self).update({key: rc[key] for key in update_keys}) + + # add the deprecated keys + self._deprecated_ignore_map.update(rc._deprecated_ignore_map) + self._deprecated_map.update(rc._deprecated_map)
+ + +
+[docs] + def copy(self): + """Make sure, the right class is retained""" + return RcParams(self)
+ + +
+[docs] + @contextlib.contextmanager + def catch(self): + """Context manager to reset the rcParams afterwards + + Usage:: + + rcParams['some_key'] = 0 + with rcParams.catch(): + rcParams['some_key'] = 1 + assert rcParams['some_key'] == 1 + assert rcParams['some_key'] == 0 + """ + save = dict(self) + yield + super().update(save) # reset settings
+
+ + + +
+[docs] +def psyplot_fname(env_key="PSYPLOTRC", fname="psyplotrc.yml", if_exists=True): + """ + Get the location of the config file. + + The file location is determined in the following order + + - `$PWD/psyplotrc.yml` + + - environment variable `PSYPLOTRC` (pointing to the file location or a + directory containing the file `psyplotrc.yml`) + + - `$PSYPLOTCONFIGDIR/psyplot` + + - On Linux and osx, + + - `$HOME/.config/psyplot/psyplotrc.yml` + + - On other platforms, + + - `$HOME/.psyplot/psyplotrc.yml` if `$HOME` is defined. + + - Lastly, it looks in `$PSYPLOTDATA/psyplotrc.yml` for a + system-defined copy. + + Parameters + ---------- + env_key: str + The environment variable that can be used for the configuration + directory + fname: str + The name of the configuration file + if_exists: bool + If True, the path is only returned if the file exists + + Returns + ------- + None or str + None, if no file could be found and `if_exists` is True, else the path + to the psyplot configuration file + + Notes + ----- + This function is motivated by the :func:`matplotlib.matplotlib_fname` + function""" + cwd = os.getcwd() + full_fname = os.path.join(cwd, fname) + if os.path.exists(full_fname): + return full_fname + + if env_key in os.environ: + path = os.environ[env_key] + if os.path.exists(path): + if os.path.isdir(path): + full_fname = os.path.join(path, fname) + if os.path.exists(full_fname): + return full_fname + else: + return path + + configdir = get_configdir() + if configdir is not None: + full_fname = os.path.join(configdir, fname) + if os.path.exists(full_fname) or not if_exists: + return full_fname + + return None
+ + + +
+[docs] +def get_configdir(name="psyplot", env_key="PSYPLOTCONFIGDIR"): + """ + Return the string representing the configuration directory. + + The directory is chosen as follows: + + 1. If the `env_key` environment variable is supplied, choose that. + + 2a. On Linux and osx, choose ``'$HOME/.config/' + name``. + + 2b. On other platforms, choose ``'$HOME/.' + name``. + + 3. If the chosen directory exists, use that as the + configuration directory. + 4. A directory: return None. + + Parameters + ---------- + name: str + The name of the program + env_key: str + The environment variable that can be used for the configuration + directory + + Notes + ----- + This function is motivated by the :func:`matplotlib.matplotlib_fname` + function""" + configdir = os.environ.get(env_key) + if configdir is not None: + return os.path.abspath(configdir) + + p = None + h = _get_home() + if ( + sys.platform.startswith("linux") or sys.platform == "darwin" + ) and h is not None: + p = os.path.join(h, ".config/" + name) + elif h is not None: + p = os.path.join(h, "." + name) + + if not os.path.exists(p): + os.makedirs(p, exist_ok=True) + return p
+ + + +
+[docs] +def validate_path_exists(s): + """If s is a path, return s, else False""" + if s is None: + return None + if os.path.exists(s): + return s + else: + raise ValueError('"%s" should be a path but it does not exist' % s)
+ + + +
+[docs] +def validate_files_exist(files): + """Validate if all pathnames in a given list exists""" + return [validate_str(fn) and validate_path_exists(fn) for fn in files]
+ + + +
+[docs] +def validate_dict(d): + """Validate a dictionary + + Parameters + ---------- + d: dict or str + If str, it must be a path to a yaml file + + Returns + ------- + dict + + Raises + ------ + ValueError""" + try: + return dict(d) + except TypeError: + d = validate_path_exists(d) + try: + with open(d) as f: + return dict(yaml.load(f, Loader=yaml.SafeLoader)) + except Exception: + raise ValueError("Could not convert {} to dictionary!".format(d))
+ + + +
+[docs] +def validate_bool_maybe_none(b): + "Convert b to a boolean or raise" + if isinstance(b, six.string_types): + b = b.lower() + if b is None or b == "none": + return None + return validate_bool(b)
+ + + +
+[docs] +def validate_bool(b): + """Convert b to a boolean or raise""" + if isinstance(b, six.string_types): + b = b.lower() + if b in ("t", "y", "yes", "on", "true", "1", 1, True): + return True + elif b in ("f", "n", "no", "off", "false", "0", 0, False): + return False + else: + raise ValueError('Could not convert "%s" to boolean' % b)
+ + + +
+[docs] +def validate_str(s): + """Validate a string + + Parameters + ---------- + s: str + + Returns + ------- + str + + Raises + ------ + ValueError""" + if not isinstance(s, six.string_types): + raise ValueError("Did not found string!") + return six.text_type(s)
+ + + +
+[docs] +def validate_stringlist(s): + """Validate a list of strings + + Parameters + ---------- + val: iterable of strings + + Returns + ------- + list + list of str + + Raises + ------ + ValueError""" + if isinstance(s, six.string_types): + return [six.text_type(v.strip()) for v in s.split(",") if v.strip()] + else: + try: + return list(map(validate_str, s)) + except TypeError as e: + raise ValueError(e.message)
+ + + +
+[docs] +def validate_stringset(*args, **kwargs): + """Validate a set of strings + + Parameters + ---------- + val: iterable of strings + + Returns + ------- + set + set of str + + Raises + ------ + ValueError""" + return set(validate_stringlist(*args, **kwargs))
+ + + +#: :class:`dict` with default values and validation functions +defaultParams = { + # user defined plotter keys + "plotter.user": [ + {}, + validate_dict, + inspect.cleandoc( + """ + formatoption keys and values that are defined by the user to be used by + the specified plotters. For example to modify the title of all + :class:`psyplot.plotter.maps.FieldPlotter` instances, set + ``{'plotter.fieldplotter.title': 'my title'}``""" + ), + ], + "gridweights.use_cdo": [ + None, + validate_bool_maybe_none, + "Boolean flag to control whether CDOs (Climate Data Operators) should " + "be used to calculate grid weights. If None, they are tried to be " + "used.", + ], + # decoder + "decoder.x": [ + set(), + validate_stringset, + "names that shall be interpreted as the longitudinal x dim", + ], + "decoder.y": [ + set(), + validate_stringset, + "names that shall be interpreted as the latitudinal y dim", + ], + "decoder.z": [ + set(), + validate_stringset, + "names that shall be interpreted as the vertical z dim", + ], + "decoder.t": [ + {"time"}, + validate_stringset, + "names that shall be interpreted as the time dimension", + ], + "decoder.interp_kind": [ + "linear", + validate_str, + "interpolation method to calculate 2D-bounds (see the `kind` parameter" + "in the :meth:`psyplot.data.CFDecoder.get_plotbounds` method)", + ], + # specify automatic drawing and showing of figures + "auto_draw": [ + True, + validate_bool, + ( + "Automatically draw the figures if the draw keyword in the " + "update and start_update methods is None" + ), + ], + "auto_show": [ + False, + validate_bool, + ( + "Automatically show the figures after the update and" + "start_update methods" + ), + ], + # data + "datapath": [None, validate_path_exists, "path for supplementary data"], + # list settings + "lists.auto_update": [ + True, + validate_bool, + "default value (boolean) for the auto_update " + "parameter in the initialization of Plotter, " + "Project, etc. instances", + ], + # project settings + # auto_import: If True the plotters in project,plotters are automatically + # imported + "project.auto_import": [ + False, + validate_bool, + "boolean controlling whether all plotters " + "specified in the project.plotters item will be " + "automatically imported when importing the " + "psyplot.project module", + ], + "project.import_seaborn": [ + None, + validate_bool_maybe_none, + "boolean controlling whether the seaborn module shall be imported " + "when importing the project module. If None, it is only tried to " + "import the module.", + ], + "project.plotters": [ + {}, + validate_dict, + "mapping from identifier to plotter definitions for the Project class." + " See the :func:`psyplot.project.register_plotter` function for " + "possible keywords and values. See " + ":attr:`psyplot.project.registered_plotters` for examples.", + ], + "project.plotters.user": [ + {}, + validate_dict, + "Plot methods that are defined by the user and overwrite those in the" + "``'project.plotters'`` key. Use this if you want to define your own " + "plotters without writing a plugin", + ], + # presets + "presets.trusted": [ + [], + validate_files_exist, + "A list of filenames with trusted presets", + ], +} + + +_rcparam_warn_str = ( + "Trying to set {key} to {value} via the {func} " + "method of RcParams which does not validate cleanly. " +) + + +_seq_err_msg = ( + "You must supply exactly {n:d} values, you provided " "{num:d} values: {s}" +) + + +_str_err_msg = ( + "You must supply exactly {n:d} comma-separated values, " + "you provided " + "{num:d} comma-separated values: {s}" +) + +#: :class:`~psyplot.config.rcsetup.RcParams` instance that stores default +#: formatoptions and configuration settings. +rcParams = RcParams() +rcParams.update_from_defaultParams() + +defaultParams_orig = defaultParams.copy() +
+ +
+
+ + + +
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/psyplot/data.html b/_modules/psyplot/data.html new file mode 100644 index 0000000..d35705c --- /dev/null +++ b/_modules/psyplot/data.html @@ -0,0 +1,6829 @@ + + + + + + psyplot.data — psyplot documentation + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for psyplot.data

+"""Data management core routines of psyplot."""
+
+# SPDX-FileCopyrightText: 2016-2024 University of Lausanne
+# SPDX-FileCopyrightText: 2020-2021 Helmholtz-Zentrum Geesthacht
+
+# SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH
+#
+# SPDX-License-Identifier: LGPL-3.0-only
+
+from __future__ import division
+
+import datetime as dt
+import inspect
+import io
+import logging
+import os
+import os.path as osp
+import re
+import traceback as tb
+from collections import defaultdict
+from functools import partial
+from glob import glob
+from importlib import import_module
+from itertools import chain, count, cycle, islice, product, repeat, starmap
+from queue import Queue
+from threading import Thread
+from typing import Dict, List, Optional
+
+import numpy as np
+import six
+import xarray as xr
+import xarray.backends.api as xarray_api
+from pandas import to_datetime
+from xarray.core.formatting import first_n_items, format_item
+from xarray.core.utils import NDArrayMixin
+
+import psyplot.utils as utils
+from psyplot.config.rcsetup import rcParams, safe_list
+from psyplot.docstring import dedent, docstrings
+from psyplot.utils import isstring
+from psyplot.warning import PsyPlotRuntimeWarning, warn
+
+try:
+    import dask  # noqa: F401
+
+    with_dask = True
+except ImportError:
+    with_dask = False
+
+try:
+    import xarray.backends.plugins as xr_plugins
+except ImportError:
+    xr_plugins = None  # type: ignore
+
+
+# No data variable. This is used for filtering if an attribute could not have
+# been accessed
+_NODATA = object
+
+
+VARIABLELABEL = "variable"
+
+
+logger = logging.getLogger(__name__)
+
+
+_ds_counter = count(1)
+
+xr_version = tuple(map(int, xr.__version__.split(".")[:2]))
+
+
+def _no_auto_update_getter(self):
+    """:class:`bool`. Boolean controlling whether the :meth:`start_update`
+    method is automatically called by the :meth:`update` method
+
+
+    Examples
+    --------
+    You can disable the automatic update via
+
+        >>> with data.no_auto_update:
+        ...     data.update(time=1)
+        ...     data.start_update()
+
+    To permanently disable the automatic update, simply set
+
+        >>> data.no_auto_update = True
+        >>> data.update(time=1)
+        >>> data.no_auto_update = False  # reenable automatical update"""
+    if getattr(self, "_no_auto_update", None) is not None:
+        return self._no_auto_update
+    else:
+        self._no_auto_update = utils._TempBool()
+    return self._no_auto_update
+
+
+def _infer_interval_breaks(coord):
+    """
+    >>> _infer_interval_breaks(np.arange(5))
+    array([-0.5,  0.5,  1.5,  2.5,  3.5,  4.5])
+
+    Taken from xarray.plotting.plot module
+    """
+    coord = np.asarray(coord)
+    deltas = 0.5 * (coord[1:] - coord[:-1])
+    first = coord[0] - deltas[0]
+    last = coord[-1] + deltas[-1]
+    return np.r_[[first], coord[:-1] + deltas, [last]]
+
+
+def _get_variable_names(arr):
+    """Return the variable names of an array"""
+    if VARIABLELABEL in arr.dims:
+        return arr.coords[VARIABLELABEL].tolist()
+    else:
+        return arr.name
+
+
+def _get_dims(arr):
+    """Return all dimensions but the :attr:`VARIABLELABEL`"""
+    return tuple(filter(lambda d: d != VARIABLELABEL, arr.dims))
+
+
+def _open_store(store_mod, store_cls, fname):
+    try:
+        return getattr(import_module(store_mod), store_cls).open(fname)
+    except AttributeError:
+        return getattr(import_module(store_mod), store_cls)(fname)
+
+
+def _fix_times(dims):
+    # xarray 0.16 fails with pandas 1.1.0 for datetime, see
+    # https://github.com/pydata/xarray/issues/4283
+    for key, val in dims.items():
+        if np.issubdtype(np.asarray(val).dtype, np.datetime64):
+            dims[key] = to_datetime([val])[0]
+
+
+
+[docs] +@docstrings.get_sections(base="setup_coords") +@dedent +def setup_coords(arr_names=None, sort=[], dims={}, **kwargs): + """ + Sets up the arr_names dictionary for the plot + + Parameters + ---------- + arr_names: string, list of strings or dictionary + Set the unique array names of the resulting arrays and (optionally) + dimensions. + + - if string: same as list of strings (see below). Strings may + include {0} which will be replaced by a counter. + - list of strings: those will be used for the array names. The final + number of dictionaries in the return depend in this case on the + `dims` and ``**furtherdims`` + - dictionary: + Then nothing happens and an :class:`dict` version of + `arr_names` is returned. + sort: list of strings + This parameter defines how the dictionaries are ordered. It has no + effect if `arr_names` is a dictionary (use a + :class:`dict` for that). It can be a list of + dimension strings matching to the dimensions in `dims` for the + variable. + dims: dict + Keys must be variable names of dimensions (e.g. time, level, lat or + lon) or 'name' for the variable name you want to choose. + Values must be values of that dimension or iterables of the values + (e.g. lists). Note that strings will be put into a list. + For example dims = {'name': 't2m', 'time': 0} will result in one plot + for the first time step, whereas dims = {'name': 't2m', 'time': [0, 1]} + will result in two plots, one for the first (time == 0) and one for the + second (time == 1) time step. + ``**kwargs`` + The same as `dims` (those will update what is specified in `dims`) + + Returns + ------- + dict + A mapping from the keys in `arr_names` and to dictionaries. Each + dictionary corresponds defines the coordinates of one data array to + load""" + try: + return dict(arr_names) + except (ValueError, TypeError): + # ValueError for cydict, TypeError for dic + pass + if arr_names is None: + arr_names = repeat("arr{0}") + elif isstring(arr_names): + arr_names = repeat(arr_names) + dims = dict(dims) + for key, val in six.iteritems(kwargs): + dims.setdefault(key, val) + sorted_dims = dict() + if sort: + for key in sort: + sorted_dims[key] = dims.pop(key) + for key, val in six.iteritems(dims): + sorted_dims[key] = val + else: + # make sure, it is first sorted for the variable names + if "name" in dims: + sorted_dims["name"] = None + for key, val in sorted(dims.items()): + sorted_dims[key] = val + for key, val in six.iteritems(kwargs): + sorted_dims.setdefault(key, val) + for key, val in six.iteritems(sorted_dims): + sorted_dims[key] = iter(safe_list(val)) + return dict( + [ + (arr_name.format(i), dict(zip(sorted_dims.keys(), dim_tuple))) + for i, (arr_name, dim_tuple) in enumerate( + zip(arr_names, product(*map(list, sorted_dims.values()))) + ) + ] + )
+ + + +
+[docs] +def to_slice(arr): + """Test whether `arr` is an integer array that can be replaced by a slice + + Parameters + ---------- + arr: numpy.array + Numpy integer array + + Returns + ------- + slice or None + If `arr` could be converted to an array, this is returned, otherwise + `None` is returned + + See Also + -------- + get_index_from_coord""" + if isinstance(arr, slice): + return arr + if len(arr) == 1: + return slice(arr[0], arr[0] + 1) + step = np.unique(arr[1:] - arr[:-1]) + if len(step) == 1: + return slice(arr[0], arr[-1] + step[0], step[0])
+ + + +
+[docs] +def get_index_from_coord(coord, base_index): + """Function to return the coordinate as integer, integer array or slice + + If `coord` is zero-dimensional, the corresponding integer in `base_index` + will be supplied. Otherwise it is first tried to return a slice, if that + does not work an integer array with the corresponding indices is returned. + + Parameters + ---------- + coord: xarray.Coordinate or xarray.Variable + Coordinate to convert + base_index: pandas.Index + The base index from which the `coord` was extracted + + Returns + ------- + int, array of ints or slice + The indexer that can be used to access the `coord` in the + `base_index` + """ + try: + values = coord.values + except AttributeError: + values = coord + if values.ndim == 0: + try: + return base_index.get_loc(values[()]) + except KeyError: + # the location does not exactly match, so we try a nearest match + return base_index.get_indexer([values[()]], "nearest")[0] + if len(values) == len(base_index) and (values == base_index).all(): + return slice(None) + values = np.array(list(map(lambda i: base_index.get_loc(i), values))) + return to_slice(values) or values
+ + + +#: mapping that translates datetime format strings to regex patterns +t_patterns = { + "%Y": "[0-9]{4}", + "%m": "[0-9]{1,2}", + "%d": "[0-9]{1,2}", + "%H": "[0-9]{1,2}", + "%M": "[0-9]{1,2}", + "%S": "[0-9]{1,2}", +} + + +
+[docs] +@docstrings.get_sections(base="get_tdata") +@dedent +def get_tdata(t_format, files): + """ + Get the time information from file names + + Parameters + ---------- + t_format: str + The string that can be used to get the time information in the files. + Any numeric datetime format string (e.g. %Y, %m, %H) can be used, but + not non-numeric strings like %b, etc. See [1]_ for the datetime format + strings + files: list of str + The that contain the time informations + + Returns + ------- + pandas.Index + The time coordinate + list of str + The file names as they are sorten in the returned index + + References + ---------- + .. [1] https://docs.python.org/2/library/datetime.html""" + + def median(arr): + return arr.min() + (arr.max() - arr.min()) / 2 + + import re + + from pandas import Index + + t_pattern = t_format + for fmt, patt in t_patterns.items(): + t_pattern = t_pattern.replace(fmt, patt) + t_pattern = re.compile(t_pattern) + time = list(range(len(files))) + for i, f in enumerate(files): + time[i] = median( + np.array( + list( + map( + lambda s: np.datetime64( + dt.datetime.strptime(s, t_format) + ), + t_pattern.findall(f), + ) + ) + ) + ) + ind = np.argsort(time) # sort according to time + files = np.array(files)[ind] + time = np.array(time)[ind] + return to_datetime(Index(time, name="time")), files
+ + + +docstrings.get_sections( + xr.Dataset.to_netcdf.__doc__, "xarray.Dataset.to_netcdf" +) + + +
+[docs] +@docstrings.dedent +def to_netcdf(ds, *args, **kwargs): + """ + Store the given dataset as a netCDF file + + This functions works essentially the same as the usual + :meth:`xarray.Dataset.to_netcdf` method but can also encode absolute time + units + + Parameters + ---------- + ds: xarray.Dataset + The dataset to store + %(xarray.Dataset.to_netcdf.parameters)s + """ + to_update = {} + for v, obj in six.iteritems(ds.variables): + units = obj.attrs.get("units", obj.encoding.get("units", None)) + if units == "day as %Y%m%d.%f" and np.issubdtype( + obj.dtype, np.datetime64 + ): + to_update[v] = xr.Variable( + obj.dims, + AbsoluteTimeEncoder(obj), + attrs=obj.attrs.copy(), + encoding=obj.encoding, + ) + to_update[v].attrs["units"] = units + if to_update: + ds = ds.copy() + ds.update(to_update) + return xarray_api.to_netcdf(ds, *args, **kwargs)
+ + + +def _get_fname_netCDF4(store): + """Try to get the file name from the NetCDF4DataStore store""" + return getattr(store, "_filename", None) + + +def _get_fname_scipy(store): + """Try to get the file name from the ScipyDataStore store""" + try: + return store.ds.filename + except AttributeError: + return None + + +def _get_fname_nio(store): + """Try to get the file name from the NioDataStore store""" + try: + f = store.ds.file + except AttributeError: + return None + try: + return f.path + except AttributeError: + return None + + +
+[docs] +class Signal(object): + """Signal to connect functions to a specific event + + This class behaves almost similar to PyQt's + :class:`PyQt4.QtCore.pyqtBoundSignal` + """ + + instance = None + owner = None + + def __init__(self, name=None, cls_signal=False): + self.name = name + self.cls_signal = cls_signal + self._connections = [] + +
+[docs] + def connect(self, func): + if func not in self._connections: + self._connections.append(func)
+ + +
+[docs] + def emit(self, *args, **kwargs): + if not getattr(self.owner, "block_signals", False) and not getattr( + self.instance, "block_signals", False + ): + logger.debug("Emitting signal %s", self.name) + for func in self._connections[:]: + logger.debug("Calling %s", func) + func(*args, **kwargs)
+ + +
+[docs] + def disconnect(self, func=None): + """Disconnect a function call to the signal. If None, all connections + are disconnected""" + if func is None: + self._connections = [] + else: + self._connections.remove(func)
+ + + def __get__(self, instance, owner): + self.owner = owner + if instance is None or self.cls_signal: + return self + ret = getattr(instance, self.name, None) + if ret is None: + setattr(instance, self.name, Signal(self.name)) + ret = getattr(instance, self.name, None) + ret.instance = instance + return ret
+ + + +#: functions to use to extract the file name from a data store +get_fname_funcs = [_get_fname_netCDF4, _get_fname_scipy, _get_fname_nio] + + +
+[docs] +@docstrings.get_sections(base="get_filename_ds") +@docstrings.dedent +def get_filename_ds(ds, dump=True, paths=None, **kwargs): + """ + Return the filename of the corresponding to a dataset + + This method returns the path to the `ds` or saves the dataset + if there exists no filename + + Parameters + ---------- + ds: xarray.Dataset + The dataset you want the path information for + dump: bool + If True and the dataset has not been dumped so far, it is dumped to a + temporary file or the one generated by `paths` is used + paths: iterable or True + An iterator over filenames to use if a dataset has no filename. + If paths is ``True``, an iterator over temporary files will be + created without raising a warning + + Other Parameters + ---------------- + ``**kwargs`` + Any other keyword for the :func:`to_netcdf` function + %(xarray.Dataset.to_netcdf.parameters)s + + Returns + ------- + str or None + None, if the dataset has not yet been dumped to the harddisk and + `dump` is False, otherwise the complete the path to the input + file + str + The module of the :class:`xarray.backends.common.AbstractDataStore` + instance that is used to hold the data + str + The class name of the + :class:`xarray.backends.common.AbstractDataStore` instance that is + used to open the data + """ + from tempfile import NamedTemporaryFile + + # if already specified, return that filename + if ds.psy._filename is not None: + return tuple([ds.psy._filename] + list(ds.psy.data_store)) + + def dump_nc(): + # make sure that the data store is not closed by providing a + # write argument + if xr_version < (0, 11): + kwargs.setdefault("writer", xarray_api.ArrayWriter()) + store = to_netcdf(ds, fname, **kwargs) + else: + # `writer` parameter was removed by + # https://github.com/pydata/xarray/pull/2261 + kwargs.setdefault("multifile", True) + store = to_netcdf(ds, fname, **kwargs)[1] + store_mod = store.__module__ + store_cls = store.__class__.__name__ + ds._file_obj = store + return store_mod, store_cls + + def tmp_it(): + while True: + yield NamedTemporaryFile(suffix=".nc").name + + fname = None + if paths is True or (dump and paths is None): + paths = tmp_it() + elif paths is not None: + if isstring(paths): + paths = iter([paths]) + else: + paths = iter(paths) + store_mod, store_cls = ds.psy.data_store + if "source" in ds.encoding: + fname = ds.encoding["source"] + store_mod = None + store_cls = None + + # check if paths is provided and if yes, save the file + if fname is None and paths is not None: + fname = next(paths, None) + if dump and fname is not None: + store_mod, store_cls = dump_nc() + + ds.psy.filename = fname + ds.psy.data_store = (store_mod, store_cls) + + return fname, store_mod, store_cls
+ + + +
+[docs] +class CFDecoder(object): + """ + Class that interpretes the coordinates and attributes accordings to + cf-conventions""" + + _registry = [] + + #: True if the data of the CFDecoder supports the extraction of a subset of + #: the data based on the indices. + supports_spatial_slicing = True + + @property + def logger(self): + """:class:`logging.Logger` of this instance""" + try: + return self._logger + except AttributeError: + name = "%s.%s" % (self.__module__, self.__class__.__name__) + self._logger = logging.getLogger(name) + self.logger.debug("Initializing...") + return self._logger + + @logger.setter + def logger(self, value): + self._logger = value + + def __init__(self, ds=None, x=None, y=None, z=None, t=None): + self.ds = ds + self.x = rcParams["decoder.x"].copy() if x is None else set(x) + self.y = rcParams["decoder.y"].copy() if y is None else set(y) + self.z = rcParams["decoder.z"].copy() if z is None else set(z) + self.t = rcParams["decoder.t"].copy() if t is None else set(t) + +
+[docs] + @staticmethod + def register_decoder(decoder_class, pos=0): + """Register a new decoder + + This function registeres a decoder class to use + + Parameters + ---------- + decoder_class: type + The class inherited from the :class:`CFDecoder` + pos: int + The position where to register the decoder (by default: the first + position""" + CFDecoder._registry.insert(pos, decoder_class)
+ + +
+[docs] + @classmethod + @docstrings.get_sections( + base="CFDecoder.can_decode", sections=["Parameters", "Returns"] + ) + def can_decode(cls, ds, var): + """ + Class method to determine whether the object can be decoded by this + decoder class. + + Parameters + ---------- + ds: xarray.Dataset + The dataset that contains the given `var` + var: xarray.Variable or xarray.DataArray + The array to decode + + Returns + ------- + bool + True if the decoder can decode the given array `var`. Otherwise + False + + Notes + ----- + The default implementation returns True for any argument. Subclass this + method to be specific on what type of data your decoder can decode + """ + return True
+ + +
+[docs] + @classmethod + @docstrings.get_sections(base="CFDecoder.get_decoder") + @docstrings.dedent + def get_decoder(cls, ds, var, *args, **kwargs): + """ + Class method to get the right decoder class that can decode the + given dataset and variable + + Parameters + ---------- + %(CFDecoder.can_decode.parameters)s + + Returns + ------- + CFDecoder + The decoder for the given dataset that can decode the variable + `var`""" + for decoder_cls in cls._registry: + if decoder_cls.can_decode(ds, var): + return decoder_cls(ds, *args, **kwargs) + return CFDecoder(ds, *args, **kwargs)
+ + +
+[docs] + @staticmethod + @docstrings.get_sections( + base="CFDecoder.decode_coords", sections=["Parameters", "Returns"] + ) + def decode_coords(ds, gridfile=None): + """ + Sets the coordinates and bounds in a dataset + + This static method sets those coordinates and bounds that are marked + marked in the netCDF attributes as coordinates in :attr:`ds` (without + deleting them from the variable attributes because this information is + necessary for visualizing the data correctly) + + Parameters + ---------- + ds: xarray.Dataset + The dataset to decode + gridfile: str + The path to a separate grid file or a xarray.Dataset instance which + may store the coordinates used in `ds` + + Returns + ------- + xarray.Dataset + `ds` with additional coordinates""" + + def add_attrs(obj): + if "coordinates" in obj.attrs: + extra_coords.update(obj.attrs["coordinates"].split()) + obj.encoding["coordinates"] = obj.attrs.pop("coordinates") + if "grid_mapping" in obj.attrs: + extra_coords.add(obj.attrs["grid_mapping"]) + if "bounds" in obj.attrs: + extra_coords.add(obj.attrs["bounds"]) + + if gridfile is not None and not isinstance(gridfile, xr.Dataset): + gridfile = open_dataset(gridfile) + extra_coords = set(ds.coords) + for k, v in six.iteritems(ds.variables): + add_attrs(v) + add_attrs(ds) + if gridfile is not None: + ds.update( + { + k: v + for k, v in six.iteritems(gridfile.variables) + if k in extra_coords + } + ) + if xr_version < (0, 11): + ds.set_coords( + extra_coords.intersection(ds.variables), inplace=True + ) + else: + ds._coord_names.update(extra_coords.intersection(ds.variables)) + return ds
+ + +
+[docs] + @docstrings.get_sections( + base="CFDecoder.is_unstructured", sections=["Parameters", "Returns"] + ) + @docstrings.get_sections( + base="CFDecoder.get_cell_node_coord", + sections=["Parameters", "Returns"], + ) + @dedent + def get_cell_node_coord(self, var, coords=None, axis="x", nans=None): + """ + Checks whether the bounds in the variable attribute are triangular + + Parameters + ---------- + var: xarray.Variable or xarray.DataArray + The variable to check + coords: dict + Coordinates to use. If None, the coordinates of the dataset in the + :attr:`ds` attribute are used. + axis: {'x', 'y'} + The spatial axis to check + nans: {None, 'skip', 'only'} + Determines whether values with nan shall be left (None), skipped + (``'skip'``) or shall be the only one returned (``'only'``) + + Returns + ------- + xarray.DataArray or None + the bounds corrdinate (if existent)""" + if coords is None: + coords = self.ds.coords + axis = axis.lower() + get_coord = self.get_x if axis == "x" else self.get_y + coord = get_coord(var, coords=coords) + if coord is not None: + bounds = self._get_coord_cell_node_coord( + coord, coords, nans, var=var + ) + if bounds is None: + bounds = self.get_plotbounds(coord) + if bounds.ndim == 1: + dim0 = coord.dims[-1] + bounds = xr.DataArray( + np.dstack([bounds[:-1], bounds[1:]])[0], + dims=(dim0, "_bnds"), + attrs=coord.attrs.copy(), + name=coord.name + "_bnds", + ) + elif bounds.ndim == 2: + warn("2D bounds are not yet sufficiently tested!") + bounds = xr.DataArray( + np.dstack( + [ + bounds[1:, 1:].ravel(), + bounds[1:, :-1].ravel(), + bounds[:-1, :-1].ravel(), + bounds[:-1, 1:].ravel(), + ] + )[0], + dims=("".join(var.dims[-2:]), "_bnds"), + attrs=coord.attrs.copy(), + name=coord.name + "_bnds", + ) + else: + raise NotImplementedError( + "More than 2D-bounds are not supported" + ) + if bounds is not None and bounds.shape[-1] == 2: + # normal CF-Conventions for rectangular grids + arr = bounds.values + if axis == "y": + stacked = np.c_[ + arr[..., :1], arr[..., :1], arr[..., 1:], arr[..., 1:] + ] + if bounds.ndim == 2: + stacked = np.repeat( + stacked.reshape((-1, 4)), + len(self.get_x(var, coords)), + axis=0, + ) + else: + stacked = stacked.reshape((-1, 4)) + else: + stacked = np.c_[arr, arr[..., ::-1]] + if bounds.ndim == 2: + stacked = np.tile( + stacked, (len(self.get_y(var, coords)), 1) + ) + else: + stacked = stacked.reshape((-1, 4)) + bounds = xr.DataArray( + stacked, + dims=("cell", bounds.dims[1]), + name=bounds.name, + attrs=bounds.attrs, + ) + + return bounds + return None
+ + + docstrings.delete_params( + "CFDecoder.get_cell_node_coord.parameters", "var", "axis" + ) + + @docstrings.dedent + def _get_coord_cell_node_coord( + self, coord, coords=None, nans=None, var=None + ): + """ + Get the boundaries of an unstructed coordinate + + Parameters + ---------- + coord: xr.Variable + The coordinate whose bounds should be returned + %(CFDecoder.get_cell_node_coord.parameters.no_var|axis)s + + Returns + ------- + %(CFDecoder.get_cell_node_coord.returns)s + """ + bounds = coord.attrs.get("bounds") + if bounds is not None: + bounds = self.ds.coords.get(bounds) + if bounds is not None: + if coords is not None: + bounds = bounds.sel( + **{ + key: coords[key] + for key in set(coords).intersection(bounds.dims) + } + ) + if nans is not None and var is None: + raise ValueError("Need the variable to deal with NaN!") + elif nans is None: + pass + elif nans == "skip": + dims = [dim for dim in set(var.dims) - set(bounds.dims)] + mask = var.notnull().all(list(dims)) if dims else var.notnull() + try: + bounds = bounds[mask.values] + except IndexError: # 3D bounds + bounds = bounds.where(mask) + elif nans == "only": + dims = [dim for dim in set(var.dims) - set(bounds.dims)] + mask = var.isnull().all(list(dims)) if dims else var.isnull() + bounds = bounds[mask.values] + else: + raise ValueError( + "`nans` must be either None, 'skip', or 'only'! " + "Not {0}!".format(str(nans)) + ) + return bounds + + @docstrings.get_sections( + base="CFDecoder._check_unstructured_bounds", + sections=["Parameters", "Returns"], + ) + @docstrings.dedent + def _check_unstructured_bounds( + self, var, coords=None, axis="x", nans=None + ): + """ + Checks whether the bounds in the variable attribute are triangular + + Parameters + ---------- + %(CFDecoder.get_cell_node_coord.parameters)s + + Returns + ------- + bool or None + True, if unstructered, None if it could not be determined + xarray.Coordinate or None + the bounds corrdinate (if existent)""" + # !!! WILL BE REMOVED IN THE NEAR FUTURE! !!! + bounds = self.get_cell_node_coord(var, coords, axis=axis, nans=nans) + if bounds is not None: + return bounds.shape[-1] == 3, bounds + else: + return None, None + +
+[docs] + @docstrings.dedent + def is_unstructured(self, var): + """ + Test if a variable is on an unstructered grid + + Parameters + ---------- + %(CFDecoder.is_unstructured.parameters)s + + Returns + ------- + %(CFDecoder.is_unstructured.returns)s + + Notes + ----- + Currently this is the same as :meth:`is_unstructured` method, but may + change in the future to support hexagonal grids""" + if str(var.attrs.get("grid_type")) == "unstructured": + return True + xcoord = self.get_x(var) + if xcoord is not None: + bounds = self._get_coord_cell_node_coord(xcoord) + if ( + bounds is not None + and bounds.ndim == 2 + and bounds.shape[-1] > 2 + ): + return True
+ + +
+[docs] + @docstrings.dedent + def is_circumpolar(self, var): + """ + Test if a variable is on a circumpolar grid + + Parameters + ---------- + %(CFDecoder.is_unstructured.parameters)s + + Returns + ------- + %(CFDecoder.is_unstructured.returns)s""" + xcoord = self.get_x(var) + return xcoord is not None and xcoord.ndim == 2
+ + +
+[docs] + def get_variable_by_axis(self, var, axis, coords=None): + """Return the coordinate matching the specified axis + + This method uses to ``'axis'`` attribute in coordinates to return the + corresponding coordinate of the given variable + + Possible types + -------------- + var: xarray.Variable + The variable to get the dimension for + axis: {'x', 'y', 'z', 't'} + The axis string that identifies the dimension + coords: dict + Coordinates to use. If None, the coordinates of the dataset in the + :attr:`ds` attribute are used. + + Returns + ------- + xarray.Coordinate or None + The coordinate for `var` that matches the given `axis` or None if + no coordinate with the right `axis` could be found. + + Notes + ----- + This is a rather low-level function that only interpretes the + CFConvention. It is used by the :meth:`get_x`, + :meth:`get_y`, :meth:`get_z` and :meth:`get_t` methods + + Warning + ------- + If None of the coordinates have an ``'axis'`` attribute, we use the + ``'coordinate'`` attribute of `var` (if existent). + Since however the CF Conventions do not determine the order on how + the coordinates shall be saved, we try to use a pattern matching + for latitude (``'lat'``) and longitude (``lon'``). If this patterns + do not match, we interpret the coordinates such that x: -1, y: -2, + z: -3. This is all not very safe for awkward dimension names, + but works for most cases. If you want to be a hundred percent sure, + use the :attr:`x`, :attr:`y`, :attr:`z` and :attr:`t` attribute. + + See Also + -------- + get_x, get_y, get_z, get_t""" + + def get_coord(cname, raise_error=True): + try: + return coords[cname] + except KeyError: + if cname not in self.ds.coords: + if raise_error: + raise + return None + ret = self.ds.coords[cname] + try: + idims = var.psy.idims + except AttributeError: # got xarray.Variable + idims = {} + return ret.isel( + **{d: sl for d, sl in idims.items() if d in ret.dims} + ) + + def guess_x(cname): + cname = cname.lower() + return ( + "lon" in cname or cname.endswith("x") or cname.startswith("x") + ) + + def guess_y(cname): + cname = cname.lower() + return ( + "lat" in cname or cname.endswith("y") or cname.startswith("y") + ) + + axis = axis.lower() + if axis not in list("xyzt"): + raise ValueError( + "Axis must be one of X, Y, Z, T, not {0}".format(axis) + ) + # we first check for the dimensions and then for the coordinates + # attribute + coords = coords or self.ds.coords + coord_names = var.attrs.get( + "coordinates", var.encoding.get("coordinates", "") + ).split() + if not coord_names: + return + ret = [] + matched = [] + for coord in map( + lambda dim: coords[dim], + filter(lambda dim: dim in coords, chain(coord_names, var.dims)), + ): + # check for the axis attribute or whether the coordinate is in the + # list of possible coordinate names + if coord.name not in (c.name for c in ret): + if coord.name in getattr(self, axis): + matched.append(coord) + elif coord.attrs.get("axis", "").lower() == axis: + ret.append(coord) + if matched: + if len(set([c.name for c in matched])) > 1: + warn( + "Found multiple matches for %s coordinate in the " + "coordinates: %s. I use %s" + % ( + axis, + ", ".join([c.name for c in matched]), + matched[0].name, + ), + PsyPlotRuntimeWarning, + ) + return matched[0] + elif ret: + return None if len(ret) > 1 else ret[0] + # If the coordinates attribute is specified but the coordinate + # variables themselves have no 'axis' attribute, we interpret the + # coordinates such that x: -1, y: -2, z: -3 + # Since however the CF Conventions do not determine the order on how + # the coordinates shall be saved, we try to use a pattern matching + # for latitude and longitude. This is not very nice, hence it is + # better to specify the :attr:`x` and :attr:`y` attribute + tnames = self.t.intersection(coord_names) + if axis == "x": + for cname in filter(guess_x, coord_names): + return get_coord(cname) + return get_coord(coord_names[-1], raise_error=False) + elif axis == "y" and len(coord_names) >= 2: + for cname in filter(guess_y, coord_names): + return get_coord(cname) + return get_coord(coord_names[-2], raise_error=False) + elif ( + axis == "z" + and len(coord_names) >= 3 + and coord_names[-3] not in tnames + ): + return get_coord(coord_names[-3], raise_error=False) + elif axis == "t" and tnames: + tname = next(iter(tnames)) + if len(tnames) > 1: + warn( + "Found multiple matches for time coordinate in the " + "coordinates: %s. I use %s" % (", ".join(tnames), tname), + PsyPlotRuntimeWarning, + ) + return get_coord(tname, raise_error=False)
+ + +
+[docs] + @docstrings.get_sections( + base="CFDecoder.get_x", sections=["Parameters", "Returns"] + ) + @dedent + def get_x(self, var, coords=None): + """ + Get the x-coordinate of a variable + + This method searches for the x-coordinate in the :attr:`ds`. It first + checks whether there is one dimension that holds an ``'axis'`` + attribute with 'X', otherwise it looks whether there is an intersection + between the :attr:`x` attribute and the variables dimensions, otherwise + it returns the coordinate corresponding to the last dimension of `var` + + Possible types + -------------- + var: xarray.Variable + The variable to get the x-coordinate for + coords: dict + Coordinates to use. If None, the coordinates of the dataset in the + :attr:`ds` attribute are used. + + Returns + ------- + xarray.Coordinate or None + The y-coordinate or None if it could be found""" + coords = coords or self.ds.coords + coord = self.get_variable_by_axis(var, "x", coords) + if coord is not None: + return coord + return coords.get(self.get_xname(var))
+ + +
+[docs] + def get_xname(self, var, coords=None): + """Get the name of the x-dimension + + This method gives the name of the x-dimension (which is not necessarily + the name of the coordinate if the variable has a coordinate attribute) + + Parameters + ---------- + var: xarray.Variables + The variable to get the dimension for + coords: dict + The coordinates to use for checking the axis attribute. If None, + they are not used + + Returns + ------- + str + The coordinate name + + See Also + -------- + get_x""" + if coords is not None: + coord = self.get_variable_by_axis(var, "x", coords) + if coord is not None and coord.name in var.dims: + return coord.name + dimlist = list(self.x.intersection(var.dims)) + if dimlist: + if len(dimlist) > 1: + warn( + "Found multiple matches for x coordinate in the variable:" + "%s. I use %s" % (", ".join(dimlist), dimlist[0]), + PsyPlotRuntimeWarning, + ) + return dimlist[0] + # otherwise we return the coordinate in the last position + if var.dims: + return var.dims[-1]
+ + +
+[docs] + @docstrings.get_sections( + base="CFDecoder.get_y", sections=["Parameters", "Returns"] + ) + @dedent + def get_y(self, var, coords=None): + """ + Get the y-coordinate of a variable + + This method searches for the y-coordinate in the :attr:`ds`. It first + checks whether there is one dimension that holds an ``'axis'`` + attribute with 'Y', otherwise it looks whether there is an intersection + between the :attr:`y` attribute and the variables dimensions, otherwise + it returns the coordinate corresponding to the second last dimension of + `var` (or the last if the dimension of var is one-dimensional) + + Possible types + -------------- + var: xarray.Variable + The variable to get the y-coordinate for + coords: dict + Coordinates to use. If None, the coordinates of the dataset in the + :attr:`ds` attribute are used. + + Returns + ------- + xarray.Coordinate or None + The y-coordinate or None if it could be found""" + coords = coords or self.ds.coords + coord = self.get_variable_by_axis(var, "y", coords) + if coord is not None: + return coord + return coords.get(self.get_yname(var))
+ + +
+[docs] + def get_yname(self, var, coords=None): + """Get the name of the y-dimension + + This method gives the name of the y-dimension (which is not necessarily + the name of the coordinate if the variable has a coordinate attribute) + + Parameters + ---------- + var: xarray.Variables + The variable to get the dimension for + coords: dict + The coordinates to use for checking the axis attribute. If None, + they are not used + + Returns + ------- + str + The coordinate name + + See Also + -------- + get_y""" + if coords is not None: + coord = self.get_variable_by_axis(var, "y", coords) + if coord is not None and coord.name in var.dims: + return coord.name + dimlist = list(self.y.intersection(var.dims)) + if dimlist: + if len(dimlist) > 1: + warn( + "Found multiple matches for y coordinate in the variable:" + "%s. I use %s" % (", ".join(dimlist), dimlist[0]), + PsyPlotRuntimeWarning, + ) + return dimlist[0] + # otherwise we return the coordinate in the last or second last + # position + if var.dims: + if self.is_unstructured(var): + return var.dims[-1] + return var.dims[-2 if var.ndim > 1 else -1]
+ + +
+[docs] + @docstrings.get_sections( + base="CFDecoder.get_z", sections=["Parameters", "Returns"] + ) + @dedent + def get_z(self, var, coords=None): + """ + Get the vertical (z-) coordinate of a variable + + This method searches for the z-coordinate in the :attr:`ds`. It first + checks whether there is one dimension that holds an ``'axis'`` + attribute with 'Z', otherwise it looks whether there is an intersection + between the :attr:`z` attribute and the variables dimensions, otherwise + it returns the coordinate corresponding to the third last dimension of + `var` (or the second last or last if var is two or one-dimensional) + + Possible types + -------------- + var: xarray.Variable + The variable to get the z-coordinate for + coords: dict + Coordinates to use. If None, the coordinates of the dataset in the + :attr:`ds` attribute are used. + + Returns + ------- + xarray.Coordinate or None + The z-coordinate or None if no z coordinate could be found""" + coords = coords or self.ds.coords + coord = self.get_variable_by_axis(var, "z", coords) + if coord is not None: + return coord + zname = self.get_zname(var) + if zname is not None: + return coords.get(zname) + return None
+ + +
+[docs] + def get_zname(self, var, coords=None): + """Get the name of the z-dimension + + This method gives the name of the z-dimension (which is not necessarily + the name of the coordinate if the variable has a coordinate attribute) + + Parameters + ---------- + var: xarray.Variables + The variable to get the dimension for + coords: dict + The coordinates to use for checking the axis attribute. If None, + they are not used + + Returns + ------- + str or None + The coordinate name or None if no vertical coordinate could be + found + + See Also + -------- + get_z""" + if coords is not None: + coord = self.get_variable_by_axis(var, "z", coords) + if coord is not None and coord.name in var.dims: + return coord.name + dimlist = list(self.z.intersection(var.dims)) + if dimlist: + if len(dimlist) > 1: + warn( + "Found multiple matches for z coordinate in the variable:" + "%s. I use %s" % (", ".join(dimlist), dimlist[0]), + PsyPlotRuntimeWarning, + ) + return dimlist[0] + # otherwise we return the coordinate in the third last position + if var.dims: + is_unstructured = self.is_unstructured(var) + icheck = -2 if is_unstructured else -3 + min_dim = ( + abs(icheck) if "variable" not in var.dims else abs(icheck - 1) + ) + if var.ndim >= min_dim and var.dims[icheck] != self.get_tname( + var, coords + ): + return var.dims[icheck] + return None
+ + +
+[docs] + @docstrings.get_sections( + base="CFDecoder.get_t", sections=["Parameters", "Returns"] + ) + @dedent + def get_t(self, var, coords=None): + """ + Get the time coordinate of a variable + + This method searches for the time coordinate in the :attr:`ds`. It + first checks whether there is one dimension that holds an ``'axis'`` + attribute with 'T', otherwise it looks whether there is an intersection + between the :attr:`t` attribute and the variables dimensions, otherwise + it returns the coordinate corresponding to the first dimension of `var` + + Possible types + -------------- + var: xarray.Variable + The variable to get the time coordinate for + coords: dict + Coordinates to use. If None, the coordinates of the dataset in the + :attr:`ds` attribute are used. + + Returns + ------- + xarray.Coordinate or None + The time coordinate or None if no time coordinate could be found""" + coords = coords or self.ds.coords + coord = self.get_variable_by_axis(var, "t", coords) + if coord is not None: + return coord + dimlist = list(self.t.intersection(var.dims).intersection(coords)) + if dimlist: + if len(dimlist) > 1: + warn( + "Found multiple matches for time coordinate in the " + "variable: %s. I use %s" + % (", ".join(dimlist), dimlist[0]), + PsyPlotRuntimeWarning, + ) + return coords[dimlist[0]] + tname = self.get_tname(var) + if tname is not None: + return coords.get(tname) + return None
+ + +
+[docs] + def get_tname(self, var, coords=None): + """Get the name of the t-dimension + + This method gives the name of the time dimension + + Parameters + ---------- + var: xarray.Variables + The variable to get the dimension for + coords: dict + The coordinates to use for checking the axis attribute. If None, + they are not used + + Returns + ------- + str or None + The coordinate name or None if no time coordinate could be found + + See Also + -------- + get_t""" + if coords is not None: + coord = self.get_variable_by_axis(var, "t", coords) + if coord is not None and coord.name in var.dims: + return coord.name + dimlist = list(self.t.intersection(var.dims)) + if dimlist: + if len(dimlist) > 1: + warn( + "Found multiple matches for t coordinate in the variable:" + "%s. I use %s" % (", ".join(dimlist), dimlist[0]), + PsyPlotRuntimeWarning, + ) + return dimlist[0] + # otherwise we return None + return None
+ + +
+[docs] + def get_idims(self, arr, coords=None): + """Get the coordinates in the :attr:`ds` dataset as int or slice + + This method returns a mapping from the coordinate names of the given + `arr` to an integer, slice or an array of integer that represent the + coordinates in the :attr:`ds` dataset and can be used to extract the + given `arr` via the :meth:`xarray.Dataset.isel` method. + + Parameters + ---------- + arr: xarray.DataArray + The data array for which to get the dimensions as integers, slices + or list of integers from the dataset in the :attr:`base` attribute + coords: iterable + The coordinates to use. If not given all coordinates in the + ``arr.coords`` attribute are used + + Returns + ------- + dict + Mapping from coordinate name to integer, list of integer or slice + + See Also + -------- + xarray.Dataset.isel, InteractiveArray.idims""" + if coords is None: + coords = arr.coords + else: + coords = { + label: coord + for label, coord in six.iteritems(arr.coords) + if label in coords + } + ret = self.get_coord_idims(coords) + # handle the coordinates that are not in the dataset + missing = set(arr.dims).difference(ret) + if missing: + warn( + "Could not get slices for the following dimensions: %r" + % (missing,), + PsyPlotRuntimeWarning, + ) + return ret
+ + +
+[docs] + def get_coord_idims(self, coords): + """Get the slicers for the given coordinates from the base dataset + + This method converts `coords` to slicers (list of + integers or ``slice`` objects) + + Parameters + ---------- + coords: dict + A subset of the ``ds.coords`` attribute of the base dataset + :attr:`ds` + + Returns + ------- + dict + Mapping from coordinate name to integer, list of integer or slice + """ + ret = dict( + (label, get_index_from_coord(coord, self.ds.indexes[label])) + for label, coord in six.iteritems(coords) + if label in self.ds.indexes + ) + return ret
+ + +
+[docs] + @docstrings.get_sections( + base="CFDecoder.get_plotbounds", sections=["Parameters", "Returns"] + ) + @dedent + def get_plotbounds(self, coord, kind=None, ignore_shape=False): + """ + Get the bounds of a coordinate + + This method first checks the ``'bounds'`` attribute of the given + `coord` and if it fails, it calculates them. + + Parameters + ---------- + coord: xarray.Coordinate + The coordinate to get the bounds for + kind: str + The interpolation method (see :func:`scipy.interpolate.interp1d`) + that is used in case of a 2-dimensional coordinate + ignore_shape: bool + If True and the `coord` has a ``'bounds'`` attribute, this + attribute is returned without further check. Otherwise it is tried + to bring the ``'bounds'`` into a format suitable for (e.g.) the + :func:`matplotlib.pyplot.pcolormesh` function. + + Returns + ------- + bounds: np.ndarray + The bounds with the same number of dimensions as `coord` but one + additional array (i.e. if `coord` has shape (4, ), `bounds` will + have shape (5, ) and if `coord` has shape (4, 5), `bounds` will + have shape (5, 6)""" + if "bounds" in coord.attrs: + bounds = self.ds.coords[coord.attrs["bounds"]] + if ignore_shape: + return bounds.values.ravel() + if not bounds.shape[:-1] == coord.shape: + bounds = self.ds.isel(**self.get_idims(coord)) + try: + return self._get_plotbounds_from_cf(coord, bounds) + except ValueError as e: + warn( + (e.message if six.PY2 else str(e)) + + " Bounds are calculated automatically!" + ) + return self._infer_interval_breaks(coord, kind=kind)
+ + + @staticmethod + @docstrings.dedent + def _get_plotbounds_from_cf(coord, bounds): + """ + Get plot bounds from the bounds stored as defined by CFConventions + + Parameters + ---------- + coord: xarray.Coordinate + The coordinate to get the bounds for + bounds: xarray.DataArray + The bounds as inferred from the attributes of the given `coord` + + Returns + ------- + %(CFDecoder.get_plotbounds.returns)s + + Notes + ----- + this currently only works for rectilinear grids""" + + if bounds.shape[:-1] != coord.shape or bounds.shape[-1] != 2: + raise ValueError( + "Cannot interprete bounds with shape {0} for {1} " + "coordinate with shape {2}.".format( + bounds.shape, coord.name, coord.shape + ) + ) + ret = np.zeros(tuple(map(lambda i: i + 1, coord.shape))) + ret[tuple(map(slice, coord.shape))] = bounds[..., 0] + last_slices = tuple(slice(-1, None) for _ in coord.shape) + ret[last_slices] = bounds[tuple(chain(last_slices, [1]))] + return ret + + docstrings.keep_params( + "CFDecoder._check_unstructured_bounds.parameters", "nans" + ) + +
+[docs] + @docstrings.get_sections( + base="CFDecoder.get_triangles", sections=["Parameters", "Returns"] + ) + @docstrings.dedent + def get_triangles( + self, + var, + coords=None, + convert_radian=True, + copy=False, + src_crs=None, + target_crs=None, + nans=None, + stacklevel=1, + ): + """ + Get the triangles for the variable + + Parameters + ---------- + var: xarray.Variable or xarray.DataArray + The variable to use + coords: dict + Alternative coordinates to use. If None, the coordinates of the + :attr:`ds` dataset are used + convert_radian: bool + If True and the coordinate has units in 'radian', those are + converted to degrees + copy: bool + If True, vertice arrays are copied + src_crs: cartopy.crs.Crs + The source projection of the data. If not None, a transformation + to the given `target_crs` will be done + target_crs: cartopy.crs.Crs + The target projection for which the triangles shall be transformed. + Must only be provided if the `src_crs` is not None. + %(CFDecoder._check_unstructured_bounds.parameters.nans)s + + Returns + ------- + matplotlib.tri.Triangulation + The spatial triangles of the variable + + Raises + ------ + ValueError + If `src_crs` is not None and `target_crs` is None""" + warn( + "The 'get_triangles' method is depreceated and will be removed " + "soon! Use the 'get_cell_node_coord' method!", + DeprecationWarning, + stacklevel=stacklevel, + ) + from matplotlib.tri import Triangulation + + def get_vertices(axis): + bounds = self._check_unstructured_bounds( + var, coords=coords, axis=axis, nans=nans + )[1] + if coords is not None: + bounds = coords.get(bounds.name, bounds) + vertices = bounds.values.ravel() + if convert_radian: + coord = getattr(self, "get_" + axis)(var) + if coord.attrs.get("units") == "radian": + vertices = vertices * 180.0 / np.pi + return vertices if not copy else vertices.copy() + + if coords is None: + coords = self.ds.coords + + xvert = get_vertices("x") + yvert = get_vertices("y") + if src_crs is not None and src_crs != target_crs: + if target_crs is None: + raise ValueError( + "Found %s for the source crs but got None for the " + "target_crs!" % (src_crs,) + ) + arr = target_crs.transform_points(src_crs, xvert, yvert) + xvert = arr[:, 0] + yvert = arr[:, 1] + triangles = np.reshape(range(len(xvert)), (len(xvert) // 3, 3)) + return Triangulation(xvert, yvert, triangles)
+ + + docstrings.delete_params( + "CFDecoder.get_plotbounds.parameters", "ignore_shape" + ) + + @staticmethod + def _infer_interval_breaks(coord, kind=None): + """ + Interpolate the bounds from the data in coord + + Parameters + ---------- + %(CFDecoder.get_plotbounds.parameters.no_ignore_shape)s + + Returns + ------- + %(CFDecoder.get_plotbounds.returns)s + + Notes + ----- + this currently only works for rectilinear grids""" + if coord.ndim == 1: + return _infer_interval_breaks(coord) + elif coord.ndim == 2: + from scipy.interpolate import RectBivariateSpline + + kind = kind or rcParams["decoder.interp_kind"] + y, x = map(np.arange, coord.shape) + new_x, new_y = map(_infer_interval_breaks, [x, y]) + coord = np.asarray(coord) + interpolation_types = {"linear": 1, "cubic": 3, "quintic": 5} + try: + kx = ky = interpolation_types[kind] + except KeyError as e: + raise ValueError( + f"Unsupported interpolation type {repr(kind)}, must be " + f"either of {', '.join(map(repr, interpolation_types))}." + ) from e + interpolate = RectBivariateSpline(x, y, coord.T, kx=kx, ky=ky) + return interpolate(new_x, new_y).T + + @classmethod + @docstrings.get_sections(base="CFDecoder._decode_ds") + @docstrings.dedent + def _decode_ds( + cls, ds, gridfile=None, decode_coords=True, decode_times=True + ): + """ + Static method to decode coordinates and time informations + + This method interpretes absolute time informations (stored with units + ``'day as %Y%m%d.%f'``) and coordinates + + Parameters + ---------- + %(CFDecoder.decode_coords.parameters)s + decode_times : bool, optional + If True, decode times encoded in the standard NetCDF datetime + format into datetime objects. Otherwise, leave them encoded as + numbers. + decode_coords : bool, optional + If True, decode the 'coordinates' attribute to identify coordinates + in the resulting dataset.""" + if decode_coords: + ds = cls.decode_coords(ds, gridfile=gridfile) + if decode_times: + for k, v in six.iteritems(ds.variables): + # check for absolute time units and make sure the data is not + # already decoded via dtype check + if v.attrs.get("units", "") == "day as %Y%m%d.%f" and ( + np.issubdtype(v.dtype, np.float64) + ): + decoded = xr.Variable( + v.dims, + AbsoluteTimeDecoder(v), + attrs=v.attrs, + encoding=v.encoding, + ) + ds.update({k: decoded}) + return ds + +
+[docs] + @classmethod + @docstrings.dedent + def decode_ds(cls, ds, *args, **kwargs): + """ + Static method to decode coordinates and time informations + + This method interpretes absolute time informations (stored with units + ``'day as %Y%m%d.%f'``) and coordinates + + Parameters + ---------- + %(CFDecoder._decode_ds.parameters)s + + Returns + ------- + xarray.Dataset + The decoded dataset""" + for decoder_cls in cls._registry + [CFDecoder]: + ds = decoder_cls._decode_ds(ds, *args, **kwargs) + return ds
+ + +
+[docs] + def correct_dims(self, var, dims={}, remove=True): + """Expands the dimensions to match the dims in the variable + + Parameters + ---------- + var: xarray.Variable + The variable to get the data for + dims: dict + a mapping from dimension to the slices + remove: bool + If True, dimensions in `dims` that are not in the dimensions of + `var` are removed""" + method_mapping = { + "x": self.get_xname, + "z": self.get_zname, + "t": self.get_tname, + } + dims = dict(dims) + if self.is_unstructured(var): # we assume a one-dimensional grid + method_mapping["y"] = self.get_xname + else: + method_mapping["y"] = self.get_yname + for key in six.iterkeys(dims.copy()): + if key in method_mapping and key not in var.dims: + dim_name = method_mapping[key](var, self.ds.coords) + if dim_name in dims: + dims.pop(key) + else: + new_name = method_mapping[key](var) + if new_name is not None: + dims[new_name] = dims.pop(key) + # now remove the unnecessary dimensions + if remove: + for key in set(dims).difference(var.dims): + dims.pop(key) + self.logger.debug( + "Could not find a dimensions matching %s in variable %s!", + key, + var, + ) + return dims
+ + +
+[docs] + def standardize_dims(self, var, dims={}): + """Replace the coordinate names through x, y, z and t + + Parameters + ---------- + var: xarray.Variable + The variable to use the dimensions of + dims: dict + The dictionary to use for replacing the original dimensions + + Returns + ------- + dict + The dictionary with replaced dimensions""" + dims = dict(dims) + name_map = { + self.get_xname(var, self.ds.coords): "x", + self.get_yname(var, self.ds.coords): "y", + self.get_zname(var, self.ds.coords): "z", + self.get_tname(var, self.ds.coords): "t", + } + dims = dict(dims) + for dim in set(dims).intersection(name_map): + dims[name_map[dim]] = dims.pop(dim) + return dims
+ + +
+[docs] + def clear_cache(self): + """Clear any cached data. + The default method does nothing but can be reimplemented by subclasses + to clear data has been computed.""" + pass
+ + + # ------------------------------------------------------------------------- + # --------------- Grid informations on a variable ------------------------- + # ------------------------------------------------------------------------- + +
+[docs] + @docstrings.get_sections(base="CFDecoder.get_metadata_for_variable") + @docstrings.dedent + def get_metadata_for_variable( + self, + var: xr.DataArray, + coords: Optional[Dict] = None, + fail_on_error: bool = False, + include_tracebacks: bool = False, + ) -> Dict[str, Dict[str, str]]: + """Get the metadata information on a variable. + + Parameters + ---------- + var : xarray.DataArray + The data array to get the metadata for + coords: Dict, optional + The coordinates to use. If none, we'll fallback to the coordinates + of the base dataset. + fail_on_error: bool, default False + If True, an error is raised when an error occurs. Otherwise it is + captured and entered as an attribute to the metadata. + include_tracebacks: bool, default False + If True, the full traceback of the error is included + + Returns + ------- + Dict[str, Dict[str, str]] + A mapping from meta data sections for meta data attributes on the + specific section. + """ + sections = self.get_metadata_sections(var) + ret = {} + if coords is None: + coords = self.ds.coords + for section in sections: + try: + attrs = self.get_metadata_for_section(var, section, coords) + except Exception as e: + if fail_on_error: + raise + else: + attrs = {"error": str(e)} + if include_tracebacks: + s = io.StringIO() + tb.print_exc(file=s) + attrs["traceback"] = s.getvalue() + if attrs: + ret[section] = attrs + return ret
+ + + docstrings.keep_params( + "CFDecoder.get_metadata_for_variable.parameters", "var" + ) + +
+[docs] + @docstrings.dedent + def get_metadata_sections(self, var: xr.DataArray) -> List[str]: + """Get the metadata sections for a variable. + + Parameters + ---------- + %(CFDecoder.get_metadata_for_variable.parameters.var)s + + Returns + ------- + List[str] + The sections for the metadata information + """ + return [ + "Attributes", + "Time information", + "Vertical information", + "X-Coordinate information", + "Y-Coordinate information", + "Other dimensions", + "Projection info", + "Grid type info", + ]
+ + +
+[docs] + @docstrings.dedent + @docstrings.get_sections( + base="CFDecoder.get_metadata_for_section", + sections=["Parameters", "Returns"], + ) + def get_metadata_for_section( + self, var: xr.DataArray, section: str, coords: Dict + ) -> Dict[str, str]: + """Get the metadata for a specific section + + Parameters + ---------- + %(CFDecoder.get_metadata_for_variable.parameters.var)s + section : str + The section name + coords : Dict + Other coordinates in the dataset + + Returns + ------- + Dict[str, str] + A mapping from metadata name to section. + """ + standard_dimensions = { + "Time information": self.get_t_metadata, + "Vertical information": self.get_z_metadata, + "X-Coordinate information": self.get_x_metadata, + "Y-Coordinate information": self.get_y_metadata, + } + if section in standard_dimensions: + return standard_dimensions[section](var, coords) + elif section == "Other dimensions": + xyzt = { + self.get_xname(var, self.ds.coords), + self.get_yname(var, self.ds.coords), + self.get_zname(var, self.ds.coords), + self.get_tname(var, self.ds.coords), + } + other_dims = list(map(str, set(var.dims) - xyzt)) + if other_dims: + return {"Dimension names": ", ".join(other_dims)} + elif section == "Attributes": + return {key: str(val) for key, val in var.attrs.items()} + elif section == "Projection info": + if "grid_mapping" in var.attrs: + return self.get_projection_info(var, coords) + elif section == "Grid type info": + return self.get_grid_type_info(var, coords) + + return {}
+ + +
+[docs] + def get_grid_type_info( + self, var: xr.DataArray, coords: Dict + ) -> Dict[str, str]: + """Get info on the grid type + + Parameters + ---------- + %(CFDecoder.get_metadata_for_variable.parameters.var)s + coords : Dict + Other coordinates in the dataset + + Returns + ------- + Dict[str, str] + The info on the grid type + """ + return { + "unstructured": self.is_unstructured(var), + "curvilinear": self.is_circumpolar(var), + }
+ + +
+[docs] + def get_projection_info( + self, var: xr.DataArray, coords: Dict + ) -> Dict[str, str]: + """Get info on the projection + + Parameters + ---------- + %(CFDecoder.get_metadata_for_variable.parameters.var)s + coords : Dict + Other coordinates in the dataset + + Returns + ------- + Dict[str, str] + The grid mapping attributes + + Raises + ------ + KeyError + when the variable specified by the `grid_mapping` is not part of + the given `coords` + """ + try: + grid_mapping = coords[var.attrs["grid_mapping"]] + except KeyError: + raise KeyError( + f"Grid mapping variable {repr(var.attrs['grid_mapping'])}" + f"could not be found in list of coordinates {tuple(coords)}" + ) + else: + return {key: str(val) for key, val in grid_mapping.attrs.items()}
+ + +
+[docs] + @docstrings.dedent + def get_coord_info( + self, + var: xr.DataArray, + dimname: str, + coord: xr.DataArray, + coords: Dict, + what: str, + ) -> Dict[str, str]: + """_summary_ + + Parameters + ---------- + %(CFDecoder.get_metadata_for_variable.parameters.var)s + dimname : str + The dimension in the dimension of `var` + coord : Union[xr.Variable, xr.DataArray] + The coordinate to get the info from + coords : Dict + Other coordinates in the dataset + what : str + The name on what this is all bout + + Returns + ------- + Dict[str, str] + The coordinate infos + + Raises + ------ + ValueError + When the coordinates specifies boundaries but they could not be + found in the given `coords` + """ + ret: Dict[str, str] = { + "Dimension name": dimname, + "Coordinate": str(coord.name), + "Shape": f"{coord.dims} -> {coord.shape}", + } + if "bounds" in coord.attrs: + bounds = coord.attrs["bounds"] + ret["Boundary variable"] = bounds + try: + bc = coords[bounds] + except KeyError: + raise KeyError( + f"{what} {repr(coord.name)} specifies a bounds " + f"attribute, but {repr(bounds)} is not part of the given " + f"coordinates {tuple(coords)}." + ) + else: + ret["Boundary variable shape"] = f"{bc.dims} -> {bc.shape}" + return ret
+ + +
+[docs] + @docstrings.dedent + def get_t_metadata( + self, var: xr.DataArray, coords: Dict + ) -> Dict[str, str]: + """Get the temporal metadata for a variable. + + Parameters + ---------- + %(CFDecoder.get_metadata_for_variable.parameters.var)s + coords: Dict + The coordinates to use + + Returns + ------- + %(CFDecoder.get_metadata_for_section.returns)s + """ + dimname = self.get_tname(var) + coord = self.get_t(var) + if not dimname or coord is None: + return {} + return self.get_coord_info( + var, dimname, coord, coords, "Time coordinate" + )
+ + +
+[docs] + @docstrings.dedent + def get_z_metadata( + self, var: xr.DataArray, coords: Dict + ) -> Dict[str, str]: + """Get the vertical level metadata for a variable. + + Parameters + ---------- + %(CFDecoder.get_metadata_for_variable.parameters.var)s + coords: Dict + The coordinates to use + + Returns + ------- + %(CFDecoder.get_metadata_for_section.returns)s + """ + dimname = self.get_zname(var) + coord = self.get_z(var) + if not dimname or coord is None: + return {} + return self.get_coord_info( + var, dimname, coord, coords, "Vertical coordinate" + )
+ + +
+[docs] + @docstrings.dedent + def get_x_metadata( + self, var: xr.DataArray, coords: Dict + ) -> Dict[str, str]: + """Get the metadata for spatial x-dimension. + + Parameters + ---------- + %(CFDecoder.get_metadata_for_variable.parameters.var)s + coords: Dict + The coordinates to use + + Returns + ------- + %(CFDecoder.get_metadata_for_section.returns)s + """ + dimname = self.get_xname(var) + coord = self.get_x(var) + if not dimname or coord is None: + return {} + return self.get_coord_info(var, dimname, coord, coords, "X-coordinate")
+ + +
+[docs] + @docstrings.dedent + def get_y_metadata( + self, var: xr.DataArray, coords: Dict + ) -> Dict[str, str]: + """Get the metadata for spatial y-dimension. + + Parameters + ---------- + %(CFDecoder.get_metadata_for_variable.parameters.var)s + coords: Dict + The coordinates to use + + Returns + ------- + %(CFDecoder.get_metadata_for_section.returns)s + """ + dimname = self.get_yname(var) + coord = self.get_y(var) + if not dimname or coord is None: + return {} + return self.get_coord_info(var, dimname, coord, coords, "X-coordinate")
+
+ + + +
+[docs] +class UGridDecoder(CFDecoder): + """ + Decoder for UGrid data sets + + Warnings + -------- + Currently only triangles are supported.""" + + #: True if the data of the CFDecoder supports the extraction of a subset of + #: the data based on the indices. + #: + #: For UGRID conventions, this is not easily possible because the + #: extraction of a subset breaks the connectivity information of the mesh + supports_spatial_slicing: bool = False + +
+[docs] + def is_unstructured(self, *args, **kwargs): + """Reimpletemented to return always True. Any ``*args`` and ``**kwargs`` + are ignored""" + return True
+ + +
+[docs] + def get_mesh(self, var, coords=None): + """Get the mesh variable for the given `var` + + Parameters + ---------- + var: xarray.Variable + The data source whith the ``'mesh'`` attribute + coords: dict + The coordinates to use. If None, the coordinates of the dataset of + this decoder is used + + Returns + ------- + xarray.Coordinate + The mesh coordinate""" + mesh = var.attrs.get("mesh") + if mesh is None: + return None + if coords is None: + coords = self.ds.coords + return coords.get(mesh, self.ds.coords.get(mesh))
+ + +
+[docs] + @classmethod + @docstrings.dedent + def can_decode(cls, ds, var): + """ + Check whether the given variable can be decoded. + + Returns True if a mesh coordinate could be found via the + :meth:`get_mesh` method + + Parameters + ---------- + %(CFDecoder.can_decode.parameters)s + + Returns + ------- + %(CFDecoder.can_decode.returns)s""" + return cls(ds).get_mesh(var) is not None
+ + +
+[docs] + @docstrings.dedent + def get_triangles( + self, + var, + coords=None, + convert_radian=True, + copy=False, + src_crs=None, + target_crs=None, + nans=None, + stacklevel=1, + ): + """ + Get the of the given coordinate. + + Parameters + ---------- + %(CFDecoder.get_triangles.parameters)s + + Returns + ------- + %(CFDecoder.get_triangles.returns)s + + Notes + ----- + If the ``'location'`` attribute is set to ``'node'``, a delaunay + triangulation is performed using the + :class:`matplotlib.tri.Triangulation` class. + + .. todo:: + Implement the visualization for UGrid data shown on the edge of the + triangles""" + warn( + "The 'get_triangles' method is depreceated and will be removed " + "soon! Use the 'get_cell_node_coord' method!", + DeprecationWarning, + stacklevel=stacklevel, + ) + from matplotlib.tri import Triangulation + + if coords is None: + coords = self.ds.coords + + def get_coord(coord): + return coords.get(coord, self.ds.coords.get(coord)) + + mesh = self.get_mesh(var, coords) + nodes = self.get_nodes(mesh, coords) + if any(n is None for n in nodes): + raise ValueError("Could not find the nodes variables!") + xvert, yvert = nodes + xvert = xvert.values + yvert = yvert.values + loc = var.attrs.get("location", "face") + if loc == "face": + triangles = get_coord( + mesh.attrs.get("face_node_connectivity", "") + ).values + if triangles is None: + raise ValueError( + "Could not find the connectivity information!" + ) + elif loc == "node": + triangles = None + else: + raise ValueError( + "Could not interprete location attribute (%s) of mesh " + "variable %s!" % (loc, mesh.name) + ) + + if convert_radian: + for coord in nodes: + if coord.attrs.get("units") == "radian": + coord = coord * 180.0 / np.pi + if src_crs is not None and src_crs != target_crs: + if target_crs is None: + raise ValueError( + "Found %s for the source crs but got None for the " + "target_crs!" % (src_crs,) + ) + xvert = xvert[triangles].ravel() + yvert = yvert[triangles].ravel() + arr = target_crs.transform_points(src_crs, xvert, yvert) + xvert = arr[:, 0] + yvert = arr[:, 1] + if loc == "face": + triangles = np.reshape(range(len(xvert)), (len(xvert) // 3, 3)) + + return Triangulation(xvert, yvert, triangles)
+ + +
+[docs] + @docstrings.dedent + def get_cell_node_coord(self, var, coords=None, axis="x", nans=None): + """ + Checks whether the bounds in the variable attribute are triangular + + Parameters + ---------- + %(CFDecoder.get_cell_node_coord.parameters)s + + Returns + ------- + %(CFDecoder.get_cell_node_coord.returns)s""" + if coords is None: + coords = self.ds.coords + + idims = self.get_coord_idims(coords) + + def get_coord(coord): + coord = coords.get(coord, self.ds.coords.get(coord)) + return coord.isel( + **{d: sl for d, sl in idims.items() if d in coord.dims} + ) + + mesh = self.get_mesh(var, coords) + if mesh is None: + return + nodes = self.get_nodes(mesh, coords) + if not len(nodes): + raise ValueError( + "Could not find the nodes variables for the %s " + "coordinate!" % axis + ) + vert = nodes[0 if axis == "x" else 1] + if vert is None: + raise ValueError( + "Could not find the nodes variables for the %s " + "coordinate!" % axis + ) + loc = var.attrs.get("location", "face") + if loc == "node": + # we assume a triangular grid and use matplotlibs triangulation + from matplotlib.tri import Triangulation + + xvert, yvert = nodes + triangles = Triangulation(xvert, yvert) + if axis == "x": + bounds = triangles.x[triangles.triangles] + else: + bounds = triangles.y[triangles.triangles] + elif loc in ["edge", "face"]: + connectivity = get_coord( + mesh.attrs.get("%s_node_connectivity" % loc, "") + ) + if connectivity is None: + raise ValueError( + "Could not find the connectivity information!" + ) + connectivity = connectivity.values + bounds = vert.values[ + np.where( + np.isnan(connectivity), connectivity[:, :1], connectivity + ).astype(int) + ] + else: + raise ValueError( + "Could not interprete location attribute (%s) of mesh " + "variable %s!" % (loc, mesh.name) + ) + dim0 = "__face" if loc == "node" else var.dims[-1] + return xr.DataArray( + bounds, + coords={ + key: val for key, val in coords.items() if (dim0,) == val.dims + }, + dims=( + dim0, + "__bnds", + ), + name=vert.name + "_bnds", + attrs=vert.attrs.copy(), + )
+ + +
+[docs] + @staticmethod + @docstrings.dedent + def decode_coords(ds, gridfile=None): + """ + Reimplemented to set the mesh variables as coordinates + + Parameters + ---------- + %(CFDecoder.decode_coords.parameters)s + + Returns + ------- + %(CFDecoder.decode_coords.returns)s""" + extra_coords = set(ds.coords) + for var in six.itervalues(ds.variables): + if "mesh" in var.attrs: + mesh = var.attrs["mesh"] + if mesh not in extra_coords: + extra_coords.add(mesh) + try: + mesh_var = ds.variables[mesh] + except KeyError: + warn("Could not find mesh variable %s" % mesh) + continue + if "node_coordinates" in mesh_var.attrs: + extra_coords.update( + mesh_var.attrs["node_coordinates"].split() + ) + if "face_node_connectivity" in mesh_var.attrs: + extra_coords.add( + mesh_var.attrs["face_node_connectivity"] + ) + if gridfile is not None and not isinstance(gridfile, xr.Dataset): + gridfile = open_dataset(gridfile) + ds.update( + { + k: v + for k, v in six.iteritems(gridfile.variables) + if k in extra_coords + } + ) + if xr_version < (0, 11): + ds.set_coords( + extra_coords.intersection(ds.variables), inplace=True + ) + else: + ds._coord_names.update(extra_coords.intersection(ds.variables)) + return ds
+ + +
+[docs] + def get_nodes(self, coord, coords): + """Get the variables containing the definition of the nodes + + Parameters + ---------- + coord: xarray.Coordinate + The mesh variable + coords: dict + The coordinates to use to get node coordinates""" + + def get_coord(coord): + return coords.get(coord, self.ds.coords.get(coord)) + + return list( + map(get_coord, coord.attrs.get("node_coordinates", "").split()[:2]) + )
+ + +
+[docs] + @docstrings.dedent + def get_x(self, var, coords=None): + """ + Get the centers of the triangles in the x-dimension + + Parameters + ---------- + %(CFDecoder.get_y.parameters)s + + Returns + ------- + %(CFDecoder.get_y.returns)s""" + if coords is None: + coords = self.ds.coords + # first we try the super class + ret = super(UGridDecoder, self).get_x(var, coords) + # but if that doesn't work because we get the variable name in the + # dimension of `var`, we use the means of the triangles + if ( + ret is None + or ret.name in var.dims + or (hasattr(var, "mesh") and ret.name == var.mesh) + ): + bounds = self.get_cell_node_coord(var, axis="x", coords=coords) + if bounds is not None: + centers = bounds.mean(axis=-1) + x = self.get_nodes(self.get_mesh(var, coords), coords)[0] + try: + cls = xr.IndexVariable + except AttributeError: # xarray < 0.9 + cls = xr.Coordinate + return cls(x.name, centers, attrs=x.attrs.copy()) + else: + return ret
+ + +
+[docs] + @docstrings.dedent + def get_y(self, var, coords=None): + """ + Get the centers of the triangles in the y-dimension + + Parameters + ---------- + %(CFDecoder.get_y.parameters)s + + Returns + ------- + %(CFDecoder.get_y.returns)s""" + if coords is None: + coords = self.ds.coords + # first we try the super class + ret = super(UGridDecoder, self).get_y(var, coords) + # but if that doesn't work because we get the variable name in the + # dimension of `var`, we use the means of the triangles + if ( + ret is None + or ret.name in var.dims + or (hasattr(var, "mesh") and ret.name == var.mesh) + ): + bounds = self.get_cell_node_coord(var, axis="y", coords=coords) + if bounds is not None: + centers = bounds.mean(axis=-1) + y = self.get_nodes(self.get_mesh(var, coords), coords)[1] + try: + cls = xr.IndexVariable + except AttributeError: # xarray < 0.9 + cls = xr.Coordinate + return cls(y.name, centers, attrs=y.attrs.copy()) + else: + return ret
+
+ + + +# register the UGridDecoder +CFDecoder.register_decoder(UGridDecoder) + +docstrings.keep_params("CFDecoder.decode_coords.parameters", "gridfile") +docstrings.get_sections( + inspect.cleandoc(xr.open_dataset.__doc__.split("\n", 1)[1]), + "xarray.open_dataset", +) +docstrings.delete_params("xarray.open_dataset.parameters", "engine") + + +
+[docs] +@docstrings.get_sections(base="open_dataset") +@docstrings.dedent +def open_dataset( + filename_or_obj, + decode_cf=True, + decode_times=True, + decode_coords=True, + engine=None, + gridfile=None, + **kwargs, +): + """ + Open an instance of :class:`xarray.Dataset`. + + This method has the same functionality as the :func:`xarray.open_dataset` + method except that is supports an additional 'gdal' engine to open + gdal Rasters (e.g. GeoTiffs) and that is supports absolute time units like + ``'day as %Y%m%d.%f'`` (if `decode_cf` and `decode_times` are True). + + Parameters + ---------- + %(xarray.open_dataset.parameters.no_engine)s + engine: {'netcdf4', 'scipy', 'pydap', 'h5netcdf', 'gdal'}, optional + Engine to use when reading netCDF files. If not provided, the default + engine is chosen based on available dependencies, with a preference for + 'netcdf4'. + %(CFDecoder.decode_coords.parameters.gridfile)s + + Returns + ------- + xarray.Dataset + The dataset that contains the variables from `filename_or_obj`""" + # use the absolute path name (is saver when saving the project) + if isstring(filename_or_obj) and osp.exists(filename_or_obj): + filename_or_obj = osp.abspath(filename_or_obj) + if engine == "gdal": + from psyplot.gdal_store import GdalStore + + filename_or_obj = GdalStore(filename_or_obj) + engine = None + ds = xr.open_dataset( + filename_or_obj, + decode_cf=decode_cf, + decode_coords=False, + engine=engine, + decode_times=decode_times, + **kwargs, + ) + if isstring(filename_or_obj): + ds.psy.filename = filename_or_obj + if decode_cf: + ds = CFDecoder.decode_ds( + ds, + decode_coords=decode_coords, + decode_times=decode_times, + gridfile=gridfile, + ) + return ds
+ + + +docstrings.get_sections( + inspect.cleandoc(xr.open_mfdataset.__doc__.split("\n", 1)[1]), + "xarray.open_mfdataset", +) +docstrings.delete_params("xarray.open_mfdataset.parameters", "engine") +docstrings.keep_params("get_tdata.parameters", "t_format") + +docstrings.params["xarray.open_mfdataset.parameters.no_engine"] = ( + docstrings.params["xarray.open_mfdataset.parameters.no_engine"] + .replace("**kwargs", "``**kwargs``") + .replace('"path/to/my/files/*.nc"', '``"path/to/my/files/*.nc"``') +) + + +docstrings.keep_params("open_dataset.parameters", "engine") + + +
+[docs] +@docstrings.dedent +def open_mfdataset( + paths, + decode_cf=True, + decode_times=True, + decode_coords=True, + engine=None, + gridfile=None, + t_format=None, + **kwargs, +): + """ + Open multiple files as a single dataset. + + This function is essentially the same as the :func:`xarray.open_mfdataset` + function but (as the :func:`open_dataset`) supports additional decoding + and the ``'gdal'`` engine. + You can further specify the `t_format` parameter to get the time + information from the files and use the results to concatenate the files + + Parameters + ---------- + %(xarray.open_mfdataset.parameters.no_engine)s + %(open_dataset.parameters.engine)s + %(get_tdata.parameters.t_format)s + %(CFDecoder.decode_coords.parameters.gridfile)s + + Returns + ------- + xarray.Dataset + The dataset that contains the variables from `filename_or_obj`""" + if t_format is not None or engine == "gdal": + if isinstance(paths, six.string_types): + paths = sorted(glob(paths)) + if not paths: + raise IOError("no files to open") + if t_format is not None: + time, paths = get_tdata(t_format, paths) + kwargs["concat_dim"] = "time" + if xr_version > (0, 11): + kwargs["combine"] = "nested" + if all(map(isstring, paths)): + filenames = list(paths) + else: + filenames = None + if engine == "gdal": + from psyplot.gdal_store import GdalStore + + paths = list(map(GdalStore, paths)) + engine = None + if xr_version < (0, 18): + kwargs["lock"] = False + + ds = xr.open_mfdataset( + paths, + decode_cf=decode_cf, + decode_times=decode_times, + engine=engine, + decode_coords=False, + **kwargs, + ) + ds.psy.filename = filenames + if decode_cf: + ds = CFDecoder.decode_ds( + ds, + gridfile=gridfile, + decode_coords=decode_coords, + decode_times=decode_times, + ) + ds.psy._concat_dim = kwargs.get("concat_dim") + ds.psy._combine = kwargs.get("combine") + if t_format is not None: + ds["time"] = time + return ds
+ + + +
+[docs] +class InteractiveBase(object): + """Class for the communication of a data object with a suitable plotter + + This class serves as an interface for data objects (in particular as a + base for :class:`InteractiveArray` and :class:`InteractiveList`) to + communicate with the corresponding :class:`~psyplot.plotter.Plotter` in the + :attr:`plotter` attribute""" + + #: The :class:`psyplot.project.DataArrayPlotter` + _plot = None + + @property + def plotter(self): + """:class:`psyplot.plotter.Plotter` instance that makes the interactive + plotting of the data""" + return self._plotter + + @plotter.setter + def plotter(self, value): + self._plotter = value + + @plotter.deleter + def plotter(self): + self._plotter = None + + no_auto_update = property( + _no_auto_update_getter, doc=_no_auto_update_getter.__doc__ + ) + + @property + def plot(self): + """An object to visualize this data object + + To make a 2D-plot with the :mod:`psy-simple <psy_simple.plugin>` + plugin, you can just type + + .. code-block:: python + + plotter = da.psy.plot.plot2d() + + It will create a new :class:`psyplot.plotter.Plotter` instance with the + extracted and visualized data. + + See Also + -------- + psyplot.project.DataArrayPlotter: for the different plot methods""" + if self._plot is None: + import psyplot.project as psy + + self._plot = psy.DataArrayPlotter(self) + return self._plot + + @no_auto_update.setter + def no_auto_update(self, value): + if self.plotter is not None: + self.plotter.no_auto_update = value + self.no_auto_update.value = bool(value) + + @property + def logger(self): + """:class:`logging.Logger` of this instance""" + try: + return self._logger + except AttributeError: + name = "%s.%s.%s" % ( + self.__module__, + self.__class__.__name__, + self.arr_name, + ) + self._logger = logging.getLogger(name) + self.logger.debug("Initializing...") + return self._logger + + @logger.setter + def logger(self, value): + self._logger = value + + @property + def ax(self): + """The matplotlib axes the plotter of this data object plots on""" + return None if self.plotter is None else self.plotter.ax + + @ax.setter + def ax(self, value): + if self.plotter is None: + raise ValueError( + "Cannot set the axes because the plotter attribute is None!" + ) + self.plotter.ax = value + + block_signals = utils._temp_bool_prop( + "block_signals", "Block the emitting of signals of this instance" + ) + + # ------------------------------------------------------------------------- + # -------------------------------- SIGNALS -------------------------------- + # ------------------------------------------------------------------------- + + #: :class:`Signal` to be emitted when the object has been updated + onupdate = Signal("_onupdate") + _onupdate = None + + _plotter = None + + @property + @docstrings.get_docstring(base="InteractiveBase._njobs") + @dedent + def _njobs(self): + """ + The number of jobs taken from the queue during an update process + + Returns + ------- + list of int + The length of the list determines the number of neccessary queues, + the numbers in the list determines the number of tasks per queue + this instance fullfills during the update process""" + return self.plotter._njobs if self.plotter is not None else [] + + @property + def arr_name(self): + """:class:`str`. The internal name of the :class:`InteractiveBase`""" + return self._arr_name + + @arr_name.setter + def arr_name(self, value): + self._arr_name = value + try: + del self._logger + except AttributeError: + pass + self.onupdate.emit() + + _arr_name = None + + _no_auto_update = None + + @docstrings.get_sections(base="InteractiveBase") + @dedent + def __init__(self, plotter=None, arr_name="arr0", auto_update=None): + """ + Parameters + ---------- + plotter: Plotter + Default: None. Interactive plotter that makes the plot via + formatoption keywords. + arr_name: str + Default: ``'data'``. unique string of the array + auto_update: bool + Default: None. A boolean indicating whether this list shall + automatically update the contained arrays when calling the + :meth:`update` method or not. See also the :attr:`no_auto_update` + attribute. If None, the value from the ``'lists.auto_update'`` + key in the :attr:`psyplot.rcParams` dictionary is used.""" + self.plotter = plotter + self.arr_name = arr_name + if auto_update is None: + auto_update = rcParams["lists.auto_update"] + self.no_auto_update = not bool(auto_update) + self.replot = False + + def _finish_all(self, queues): + for n, queue in zip(safe_list(self._njobs), safe_list(queues)): + if queue is not None: + for i in range(n): + queue.task_done() + + @docstrings.get_sections(base="InteractiveBase._register_update") + @dedent + def _register_update( + self, replot=False, fmt={}, force=False, todefault=False + ): + """ + Register new formatoptions for updating + + Parameters + ---------- + replot: bool + Boolean that determines whether the data specific formatoptions + shall be updated in any case or not. Note, if `dims` is not empty + or any coordinate keyword is in ``**kwargs``, this will be set to + True automatically + fmt: dict + Keys may be any valid formatoption of the formatoptions in the + :attr:`plotter` + force: str, list of str or bool + If formatoption key (i.e. string) or list of formatoption keys, + thery are definitely updated whether they changed or not. + If True, all the given formatoptions in this call of the are + :meth:`update` method are updated + todefault: bool + If True, all changed formatoptions (except the registered ones) + are updated to their default value as stored in the + :attr:`~psyplot.plotter.Plotter.rc` attribute + + See Also + -------- + start_update""" + self.replot = self.replot or replot + if self.plotter is not None: + self.plotter._register_update( + replot=self.replot, fmt=fmt, force=force, todefault=todefault + ) + +
+[docs] + @docstrings.get_sections( + base="InteractiveBase.start_update", sections=["Parameters", "Returns"] + ) + @dedent + def start_update(self, draw=None, queues=None): + """ + Conduct the formerly registered updates + + This method conducts the updates that have been registered via the + :meth:`update` method. You can call this method if the + :attr:`no_auto_update` attribute of this instance and the `auto_update` + parameter in the :meth:`update` method has been set to False + + Parameters + ---------- + draw: bool or None + Boolean to control whether the figure of this array shall be drawn + at the end. If None, it defaults to the `'auto_draw'`` parameter + in the :attr:`psyplot.rcParams` dictionary + queues: list of :class:`Queue.Queue` instances + The queues that are passed to the + :meth:`psyplot.plotter.Plotter.start_update` method to ensure a + thread-safe update. It can be None if only one single plotter is + updated at the same time. The number of jobs that are taken from + the queue is determined by the :meth:`_njobs` attribute. Note that + there this parameter is automatically configured when updating + from a :class:`~psyplot.project.Project`. + + Returns + ------- + bool + A boolean indicating whether a redrawing is necessary or not + + See Also + -------- + :attr:`no_auto_update`, update + """ + if self.plotter is not None: + return self.plotter.start_update(draw=draw, queues=queues)
+ + + docstrings.keep_params("InteractiveBase.start_update.parameters", "draw") + +
+[docs] + @docstrings.get_sections( + base="InteractiveBase.update", sections=["Parameters", "Notes"] + ) + @docstrings.dedent + def update( + self, + fmt={}, + replot=False, + draw=None, + auto_update=False, + force=False, + todefault=False, + **kwargs, + ): + """ + Update the coordinates and the plot + + This method updates all arrays in this list with the given coordinate + values and formatoptions. + + Parameters + ---------- + %(InteractiveBase._register_update.parameters)s + auto_update: bool + Boolean determining whether or not the :meth:`start_update` method + is called at the end. This parameter has no effect if the + :attr:`no_auto_update` attribute is set to ``True``. + %(InteractiveBase.start_update.parameters.draw)s + ``**kwargs`` + Any other formatoption that shall be updated (additionally to those + in `fmt`) + + Notes + ----- + If the :attr:`no_auto_update` attribute is True and the given + `auto_update` parameter are is False, the update of the plots are + registered and conducted at the next call of the :meth:`start_update` + method or the next call of this method (if the `auto_update` parameter + is then True). + """ + fmt = dict(fmt) + fmt.update(kwargs) + + self._register_update( + replot=replot, fmt=fmt, force=force, todefault=todefault + ) + + if not self.no_auto_update or auto_update: + self.start_update(draw=draw)
+ + +
+[docs] + def to_interactive_list(self): + """Return a :class:`InteractiveList` that contains this object""" + raise NotImplementedError( + "Not implemented for the %s class" % (self.__class__.__name__,) + )
+
+ + + +
+[docs] +@xr.register_dataarray_accessor("psy") +class InteractiveArray(InteractiveBase): + """Interactive psyplot accessor for the data array + + This class keeps reference to the base :class:`xarray.Dataset` where the + :class:`array.DataArray` originates from and enables to switch between the + coordinates in the array. Furthermore it has a :attr:`plotter` attribute to + enable interactive plotting via an :class:`psyplot.plotter.Plotter` + instance.""" + + @property + def base(self): + """Base dataset this instance gets its data from""" + if self._base is None: + if "variable" in self.arr.dims: + + def to_dataset(i): + ret = self.isel(variable=i).to_dataset( + name=self.arr.coords["variable"].values[i] + ) + try: + return ret.drop_vars("variable") + except ValueError: # 'variable' Variable not defined + pass + return ret + + ds = to_dataset(0) + if len(self.arr.coords["variable"]) > 1: + for i in range(1, len(self.arr.coords["variable"])): + ds.update(ds.merge(to_dataset(i))) + self._base = ds + else: + self._base = self.arr.to_dataset( + name=self.arr.name or self.arr_name + ) + self.onbasechange.emit() + return self._base + + @base.setter + def base(self, value): + self._base = value + self.onbasechange.emit() + + @property + def decoder(self): + """The decoder of this array""" + try: + return self._decoder + except AttributeError: + self._decoder = CFDecoder.get_decoder(self.base, self.arr) + return self._decoder + + @decoder.setter + def decoder(self, value): + self._decoder = value + + @property + def idims(self): + """Coordinates in the :attr:`base` dataset as int or slice + + This attribute holds a mapping from the coordinate names of this + array to an integer, slice or an array of integer that represent the + coordinates in the :attr:`base` dataset""" + if self._idims is None: + self._idims = self.decoder.get_idims(self.arr) + return self._idims + + @idims.setter + def idims(self, value): + self._idims = value + + @property + @docstrings + def _njobs(self): + """%(InteractiveBase._njobs)s""" + ret = super(self.__class__, self)._njobs or [0] + ret[0] += 1 + return ret + + logger = InteractiveBase.logger + _idims = None + _base = None + + # -------------- SIGNALS -------------------------------------------------- + #: :class:`Signal` to be emiited when the base of the object changes + onbasechange = Signal("_onbasechange") + _onbasechange = None + + @docstrings.dedent + def __init__(self, xarray_obj, *args, **kwargs): + """ + The ``*args`` and ``**kwargs`` are essentially the same as for the + :class:`xarray.DataArray` method, additional ``**kwargs`` are + described below. + + Other Parameters + ---------------- + base: xarray.Dataset + Default: None. Dataset that serves as the origin of the data + contained in this DataArray instance. This will be used if you want + to update the coordinates via the :meth:`update` method. If None, + this instance will serve as a base as soon as it is needed. + decoder: psyplot.CFDecoder + The decoder that decodes the `base` dataset and is used to get + bounds. If not given, a new :class:`CFDecoder` is created + idims: dict + Default: None. dictionary with integer values and/or slices in the + `base` dictionary. If not given, they are determined automatically + %(InteractiveBase.parameters)s + """ + self.arr = xarray_obj + super(InteractiveArray, self).__init__(*args, **kwargs) + self._registered_updates = {} + self._new_dims = {} + self.method = None + +
+[docs] + def init_accessor( + self, base=None, idims=None, decoder=None, *args, **kwargs + ): + """ + Initialize the accessor instance + + This method initializes the accessor + + Parameters + ---------- + base: xr.Dataset + The base dataset for the data + idims: dict + A mapping from dimension name to indices. If not provided, it is + calculated when the :attr:`idims` attribute is accessed + decoder: CFDecoder + The decoder of this object + %(InteractiveBase.parameters)s + """ + if base is not None: + self.base = base + self.idims = idims + if decoder is not None: + self.decoder = decoder + super(InteractiveArray, self).__init__(*args, **kwargs)
+ + + @property + def iter_base_variables(self): + """An iterator over the base variables in the :attr:`base` dataset""" + if VARIABLELABEL in self.arr.coords: + return ( + self._get_base_var(name) + for name in safe_list( + self.arr.coords[VARIABLELABEL].values.tolist() + ) + ) + name = self.arr.name + if name is None: + return iter([self.arr._variable]) + return iter([self.base.variables[name]]) + + def _get_base_var(self, name): + try: + return self.base.variables[name] + except KeyError: + return self.arr.sel(**{VARIABLELABEL: name}).rename(name) + + @property + def base_variables(self): + """A mapping from the variable name to the variablein the :attr:`base` + dataset.""" + if VARIABLELABEL in self.arr.coords: + return dict( + [ + (name, self._get_base_var(name)) + for name in safe_list( + self.arr.coords[VARIABLELABEL].values.tolist() + ) + ] + ) + name = self.arr.name + if name is None: + return {name: self.arr._variable} + else: + return {self.arr.name: self.base.variables[self.arr.name]} + + docstrings.keep_params("setup_coords.parameters", "dims") + + @docstrings.get_sections(base="InteractiveArray._register_update") + @docstrings.dedent + def _register_update( + self, + method="isel", + replot=False, + dims={}, + fmt={}, + force=False, + todefault=False, + ): + """ + Register new dimensions and formatoptions for updating + + Parameters + ---------- + method: {'isel', None, 'nearest', ...} + Selection method of the xarray.Dataset to be used for setting the + variables from the informations in `dims`. + If `method` is 'isel', the :meth:`xarray.Dataset.isel` method is + used. Otherwise it sets the `method` parameter for the + :meth:`xarray.Dataset.sel` method. + %(setup_coords.parameters.dims)s + %(InteractiveBase._register_update.parameters)s + + See Also + -------- + start_update""" + if self._new_dims and self.method != method: + raise ValueError( + "New dimensions were already specified for with the %s method!" + " I can not choose a new method %s" % (self.method, method) + ) + else: + self.method = method + if "name" in dims: + self._new_dims["name"] = dims.pop("name") + if "name" in self._new_dims: + name = self._new_dims["name"] + if not isstring(name): + name = name[0] # concatenated array + arr = self.base[name] + else: + arr = next(six.itervalues(self.base_variables)) + self._new_dims.update(self.decoder.correct_dims(arr, dims)) + InteractiveBase._register_update( + self, + fmt=fmt, + replot=replot or bool(self._new_dims), + force=force, + todefault=todefault, + ) + + def _update_concatenated(self, dims, method): + """Updates a concatenated array to new dimensions""" + + def is_unequal(v1, v2): + try: + return bool(v1 != v2) + except ValueError: # arrays + try: + (v1 == v2).all() + except AttributeError: + return False + + def filter_attrs(item): + """Checks whether the attribute is from the base variable""" + return item[0] not in self.base.attrs or is_unequal( + item[1], self.base.attrs[item[0]] + ) + + saved_attrs = list(filter(filter_attrs, six.iteritems(self.arr.attrs))) + saved_name = self.arr.name + self.arr.name = "None" + if "name" in dims: + name = dims.pop("name") + else: + name = list(self.arr.coords["variable"].values) + base_dims = self.base[name].dims + if method == "isel": + self.idims.update(dims) + dims = self.idims + for dim in set(base_dims) - set(dims): + dims[dim] = slice(None) + for dim in set(dims) - set(self.base[name].dims): + del dims[dim] + res = self.base[name].isel(**dims).to_array() + else: + self._idims = None + for key, val in six.iteritems(self.arr.coords): + if key in base_dims and key != "variable": + dims.setdefault(key, val) + kws = dims.copy() + # the sel method does not work with slice objects + if not any(isinstance(idx, slice) for idx in dims.values()): + kws["method"] = method + try: + res = self.base[name].sel(**kws) + except KeyError: + _fix_times(kws) + res = self.base[name].sel(**kws) + res = res.to_array() + if "coordinates" in self.base[name[0]].encoding: + res.encoding["coordinates"] = self.base[name[0]].encoding[ + "coordinates" + ] + self.arr._variable = res._variable + self.arr._coords = res._coords + try: + self.arr._indexes = ( + res._indexes.copy() if res._indexes is not None else None + ) + except AttributeError: # res.indexes not existent for xr<0.12 + pass + self.arr.name = saved_name + for key, val in saved_attrs: + self.arr.attrs[key] = val + + def _update_array(self, dims, method): + """Updates the array to the new dims from then :attr:`base` dataset""" + + def is_unequal(v1, v2): + try: + return bool(v1 != v2) + except ValueError: # arrays + try: + (v1 == v2).all() + except AttributeError: + return False + + def filter_attrs(item): + """Checks whether the attribute is from the base variable""" + return item[0] not in base_var.attrs or is_unequal( + item[1], base_var.attrs[item[0]] + ) + + base_var = self.base.variables[self.arr.name] + if "name" in dims: + name = dims.pop("name") + self.arr.name = name + else: + name = self.arr.name + # save attributes that have been changed by the user + saved_attrs = list(filter(filter_attrs, six.iteritems(self.arr.attrs))) + if method == "isel": + self.idims.update(dims) + dims = self.idims + for dim in set(self.base[name].dims) - set(dims): + dims[dim] = slice(None) + for dim in set(dims) - set(self.base[name].dims): + del dims[dim] + res = self.base[name].isel(**dims) + else: + self._idims = None + old_dims = self.arr.dims[:] + for key, val in six.iteritems(self.arr.coords): + if key in base_var.dims: + dims.setdefault(key, val) + kws = dims.copy() + # the sel method does not work with slice objects + if not any(isinstance(idx, slice) for idx in dims.values()): + kws["method"] = method + try: + res = self.base[name].sel(**kws) + except KeyError: + _fix_times(kws) + res = self.base[name].sel(**kws) + # squeeze the 0-dimensional dimensions + res = res.isel( + **{ + dim: 0 + for i, dim in enumerate(res.dims) + if (res.shape[i] == 1 and dim not in old_dims) + } + ) + self.arr._variable = res._variable + self.arr._coords = res._coords + try: + self.arr._indexes = ( + res._indexes.copy() if res._indexes is not None else None + ) + except AttributeError: # res.indexes not existent for xr<0.12 + pass + # update to old attributes + for key, val in saved_attrs: + self.arr.attrs[key] = val + +
+[docs] + def shiftlon(self, central_longitude): + """ + Shift longitudes and the data so that they match map projection region. + + Only valid for cylindrical/pseudo-cylindrical global projections and + data on regular lat/lon grids. longitudes need to be 1D. + + Parameters + ---------- + central_longitude + center of map projection region + + References + ---------- + This function is copied and taken from the + :class:`mpl_toolkits.basemap.Basemap` class. The only difference is + that we do not mask values outside the map projection region + """ + if xr_version < (0, 10): + raise NotImplementedError( + "xarray>=0.10 is required for the shiftlon method!" + ) + arr = self.arr + ret = arr.copy(True, arr.values.copy()) + if arr.ndim > 2: + xname = self.get_dim("x") + yname = self.get_dim("y") + shapes = dict( + [ + (dim, range(i)) + for dim, i in zip(arr.dims, arr.shape) + if dim not in [xname, yname] + ] + ) + dims = list(shapes) + for indexes in product(*shapes.values()): + d = dict(zip(dims, indexes)) + shifted = ret[d].psy.shiftlon(central_longitude) + ret[d] = shifted.values + + x = shifted.psy.get_coord("x") + ret[x.name] = shifted[x.name].variable + + return ret + + lon = self.get_coord("x").variable + xname = self.get_dim("x") + ix = arr.dims.index(xname) + lon = lon.copy(True, lon.values.copy()) + lonsin = lon.values + datain = arr.values.copy() + + clon = np.asarray(central_longitude) + + if lonsin.ndim not in [1]: + raise ValueError("1D longitudes required") + elif clon.ndim: + raise ValueError( + "Central longitude must be a scalar, not " + "%i-dimensional!" % clon.ndim + ) + + lonsin = np.where(lonsin > clon + 180, lonsin - 360, lonsin) + lonsin = np.where(lonsin < clon - 180, lonsin + 360, lonsin) + londiff = np.abs(lonsin[0:-1] - lonsin[1:]) + londiff_sort = np.sort(londiff) + thresh = 360.0 - londiff_sort[-2] + itemindex = len(lonsin) - np.where(londiff >= thresh)[0] + if itemindex.size: + # check to see if cyclic (wraparound) point included + # if so, remove it. + if np.abs(lonsin[0] - lonsin[-1]) < 1.0e-4: + hascyclic = True + lonsin_save = lonsin.copy() + lonsin = lonsin[1:] + if datain is not None: + datain_save = datain.copy() + datain = datain[1:] + else: + hascyclic = False + lonsin = np.roll(lonsin, itemindex - 1) + if datain is not None: + datain = np.roll(datain, itemindex - 1, [ix]) + # add cyclic point back at beginning. + if hascyclic: + lonsin_save[1:] = lonsin + lonsin_save[0] = lonsin[-1] - 360.0 + lonsin = lonsin_save + if datain is not None: + datain_save[1:] = datain + datain_save[0] = datain[-1] + datain = datain_save + ret = ret.copy(True, datain) + lon.values[:] = lonsin + ret[lon.name] = lon + return ret
+ + +
+[docs] + @docstrings.dedent + def start_update(self, draw=None, queues=None): + """ + Conduct the formerly registered updates + + This method conducts the updates that have been registered via the + :meth:`update` method. You can call this method if the + :attr:`no_auto_update` attribute of this instance is True and the + `auto_update` parameter in the :meth:`update` method has been set to + False + + Parameters + ---------- + %(InteractiveBase.start_update.parameters)s + + Returns + ------- + %(InteractiveBase.start_update.returns)s + + See Also + -------- + :attr:`no_auto_update`, update + """ + + def filter_attrs(item): + return ( + item[0] not in self.base.attrs + or item[1] != self.base.attrs[item[0]] + ) + + if queues is not None: + # make sure that no plot is updated during gathering the data + queues[0].get() + try: + dims = self._new_dims + method = self.method + if dims: + self.decoder.clear_cache() + if VARIABLELABEL in self.arr.coords: + self._update_concatenated(dims, method) + else: + self._update_array(dims, method) + if queues is not None: + queues[0].task_done() + self._new_dims = {} + self.onupdate.emit() + except Exception: + self._finish_all(queues) + raise + return InteractiveBase.start_update(self, draw=draw, queues=queues)
+ + +
+[docs] + @docstrings.get_sections( + base="InteractiveArray.update", sections=["Parameters", "Notes"] + ) + @docstrings.dedent + def update( + self, + method="isel", + dims={}, + fmt={}, + replot=False, + auto_update=False, + draw=None, + force=False, + todefault=False, + **kwargs, + ): + """ + Update the coordinates and the plot + + This method updates all arrays in this list with the given coordinate + values and formatoptions. + + Parameters + ---------- + %(InteractiveArray._register_update.parameters)s + auto_update: bool + Boolean determining whether or not the :meth:`start_update` method + is called after the end. + %(InteractiveBase.start_update.parameters)s + ``**kwargs`` + Any other formatoption or dimension that shall be updated + (additionally to those in `fmt` and `dims`) + + Notes + ----- + When updating to a new array while trying to set the dimensions at the + same time, you have to specify the new dimensions via the `dims` + parameter, e.g.:: + + da.psy.update(name='new_name', dims={'new_dim': 3}) + + if ``'new_dim'`` is not yet a dimension of this array + + %(InteractiveBase.update.notes)s""" + dims = dict(dims) + fmt = dict(fmt) + vars_and_coords = set( + chain(self.arr.dims, self.arr.coords, ["name", "x", "y", "z", "t"]) + ) + furtherdims, furtherfmt = utils.sort_kwargs(kwargs, vars_and_coords) + dims.update(furtherdims) + fmt.update(furtherfmt) + + self._register_update( + method=method, + replot=replot, + dims=dims, + fmt=fmt, + force=force, + todefault=todefault, + ) + + if not self.no_auto_update or auto_update: + self.start_update(draw=draw)
+ + + def _short_info(self, intend=0, maybe=False): + str_intend = " " * intend + if "variable" in self.arr.coords: + name = ", ".join(self.arr.coords["variable"].values) + else: + name = self.arr.name + if self.arr.ndim > 0: + dims = ", with (%s)=%s" % ( + ", ".join(self.arr.dims), + self.arr.shape, + ) + else: + dims = "" + return str_intend + "%s: %i-dim %s of %s%s, %s" % ( + self.arr_name, + self.arr.ndim, + self.arr.__class__.__name__, + name, + dims, + ", ".join( + "%s=%s" % (coord, format_item(val.values)) + for coord, val in six.iteritems(self.arr.coords) + if val.ndim == 0 + ), + ) + + def __getitem__(self, key): + ret = self.arr.__getitem__(key) + ret.psy._base = self.base + return ret + +
+[docs] + def isel(self, *args, **kwargs): + """Select a subset of the array based on position. + + Same method as :meth:`xarray.DataArray.isel` but keeps information on + the base dataset. + """ + ret = self.arr.isel(*args, **kwargs) + ret.psy._base = self._base + return ret
+ + +
+[docs] + def sel(self, *args, **kwargs): + """Select a subset of the array based on indexes. + + Same method as :meth:`xarray.DataArray.sel` but keeps information on + the base dataset. + """ + ret = self.arr.sel(*args, **kwargs) + ret.psy._base = self._base + return ret
+ + +
+[docs] + def copy(self, deep=False): + """Copy the array + + This method returns a copy of the underlying array in the :attr:`arr` + attribute. It is more stable because it creates a new `psy` accessor""" + arr = self.arr.copy(deep) + try: + arr.psy = InteractiveArray(arr) + except AttributeError: # attribute is read-only for xarray >=0.13 + pass + return arr
+ + +
+[docs] + def to_interactive_list(self): + return InteractiveList([self], arr_name=self.arr_name)
+ + +
+[docs] + @docstrings.get_sections(base="InteractiveArray.get_coord") + @docstrings.dedent + def get_coord(self, what, base=False): + """ + The x-coordinate of this data array + + Parameters + ---------- + what: {'t', 'x', 'y', 'z'} + The letter of the axis + base: bool + If True, use the base variable in the :attr:`base` dataset.""" + what = what.lower() + return getattr(self.decoder, "get_" + what)( + next(six.itervalues(self.base_variables)) if base else self.arr, + self.arr.coords, + )
+ + +
+[docs] + @docstrings.dedent + def get_dim(self, what, base=False): + """ + The name of the x-dimension of this data array + + Parameters + ---------- + %(InteractiveArray.get_coord.parameters)s""" + what = what.lower() + return getattr(self.decoder, "get_%sname" % what)( + next(six.itervalues(self.base_variables)) if base else self.arr + )
+ + + # ------------------ Calculations ----------------------------------------- + + def _gridweights(self): + """Calculate the gridweights with a simple rectangular approximation""" + arr = self.arr + xcoord = self.get_coord("x") + ycoord = self.get_coord("y") + # convert the units + xcoord_orig = xcoord + ycoord_orig = ycoord + units = xcoord.attrs.get("units", "") + in_metres = False + in_degree = False + if "deg" in units or ( + "rad" not in units + and "lon" in xcoord.name + and "lat" in ycoord.name + ): + xcoord = xcoord * np.pi / 180 + ycoord = ycoord * np.pi / 180 + in_degree = True + elif "rad" in units: + pass + else: + in_metres = True + + # calculate the gridcell boundaries + xbounds = self.decoder.get_plotbounds(xcoord, arr.coords) + ybounds = self.decoder.get_plotbounds(ycoord, arr.coords) + if xbounds.ndim == 1: + xbounds, ybounds = np.meshgrid(xbounds, ybounds) + + # calculate the weights based on the units + if xcoord.ndim == 2 or self.decoder.is_unstructured(self.arr): + warn( + "[%s] - Curvilinear grids are not supported! " + "Using constant grid cell area weights!" % self.logger.name, + PsyPlotRuntimeWarning, + ) + weights = np.ones_like(xcoord.values) + elif in_metres: + weights = np.abs(xbounds[:-1, :-1] - xbounds[1:, 1:]) * ( + np.abs(ybounds[:-1, :-1] - ybounds[1:, 1:]) + ) + else: + weights = np.abs(xbounds[:-1, :-1] - xbounds[1:, 1:]) * ( + np.sin(ybounds[:-1, :-1]) - np.sin(ybounds[1:, 1:]) + ) + + # normalize the weights by dividing through the sum + if in_degree: + xmask = (xcoord_orig.values < -400) | (xcoord_orig.values > 400) + ymask = (ycoord_orig.values < -200) | (ycoord_orig.values > 200) + if xmask.any() or ymask.any(): + if xmask.ndim == 1 and weights.ndim != 1: + xmask, ymask = np.meshgrid(xmask, ymask) + weights[xmask | ymask] = np.nan + if np.any(~np.isnan(weights)): + weights /= np.nansum(weights) + return weights + + def _gridweights_cdo(self): + """Estimate the gridweights using CDOs""" + from tempfile import NamedTemporaryFile + + from cdo import Cdo + + sdims = {self.get_dim("y"), self.get_dim("x")} + cdo = Cdo() + fname = NamedTemporaryFile(prefix="psy", suffix=".nc").name + arr = self.arr + base = arr.psy.base + dims = arr.dims + ds = arr.isel(**{d: 0 for d in set(dims) - sdims}).to_dataset() + for coord in six.itervalues(ds.coords): + bounds = coord.attrs.get("bounds", coord.encoding.get("bounds")) + if ( + bounds + and bounds in set(base.coords) - set(ds.coords) + and sdims.intersection(base.coords[bounds].dims) + ): + ds[bounds] = base.sel( + **{d: arr.coords[d].values for d in sdims} + ).coords[bounds] + ds = ds.drop_vars( + [c.name for c in six.itervalues(ds.coords) if not c.ndim] + ) + to_netcdf(ds, fname) + ret = cdo.gridweights(input=fname, returnArray="cell_weights") + try: + os.remove(fname) + except Exception: + pass + return ret + + def _weights_to_da(self, weights, keepdims=False, keepshape=False): + """Convert the 2D weights into a DataArray and potentially enlarge it""" + arr = self.arr + xcoord = self.get_coord("x") + ycoord = self.get_coord("y") + sdims = (self.get_dim("y"), self.get_dim("x")) + if sdims[0] == sdims[1]: # unstructured grids + sdims = sdims[:1] + if (ycoord.name, xcoord.name) != sdims: + attrs = dict(coordinates=ycoord.name + " " + xcoord.name) + else: + attrs = {} + + # reshape and expand if necessary + if not keepdims and not keepshape: + coords = {ycoord.name: ycoord, xcoord.name: xcoord} + dims = sdims + elif keepshape: + if with_dask: + from dask.array import broadcast_to, notnull + else: + from numpy import broadcast_to, isnan + + def notnull(a): + return ~isnan(a) + + dims = arr.dims + coords = arr.coords + weights = broadcast_to(weights / weights.sum(), arr.shape) + # set nans to zero weigths. This step takes quite a lot of time for + # large arrays since it involves a copy of the entire `arr` + weights = weights * notnull(arr) + # normalize the weights + if with_dask: + summed_weights = weights.sum( + axis=tuple(map(dims.index, sdims)), keepdims=True + ) + else: + summed_weights = weights.sum( + axis=tuple(map(dims.index, sdims)) + ) + weights = weights / summed_weights + else: + dims = arr.dims + coords = arr.isel( + **{d: 0 if d not in sdims else slice(None) for d in dims} + ).coords + weights = weights.reshape( + tuple( + 1 if dim not in sdims else s + for s, dim in zip(arr.shape, arr.dims) + ) + ) + return xr.DataArray( + weights, dims=dims, coords=coords, name="cell_weights", attrs=attrs + ) + +
+[docs] + def gridweights(self, keepdims=False, keepshape=False, use_cdo=None): + """Calculate the cell weights for each grid cell + + Parameters + ---------- + keepdims: bool + If True, keep the number of dimensions + keepshape: bool + If True, keep the exact shape as the source array and the missing + values in the array are masked + use_cdo: bool or None + If True, use Climate Data Operators (CDOs) to calculate the + weights. Note that this is used automatically for unstructured + grids. If None, it depends on the ``'gridweights.use_cdo'`` + item in the :attr:`psyplot.rcParams`. + + Returns + ------- + xarray.DataArray + The 2D-DataArray with the grid weights""" + if use_cdo is None: + use_cdo = rcParams["gridweights.use_cdo"] + if not use_cdo and self.decoder.is_unstructured(self.arr): + use_cdo = True + if use_cdo is None or use_cdo: + try: + weights = self._gridweights_cdo() + except Exception: + if use_cdo: + raise + else: + weights = self._gridweights() + else: + weights = self._gridweights() + + return self._weights_to_da( + weights, keepdims=keepdims, keepshape=keepshape + )
+ + + def _fldaverage_args(self): + """Masked array, xname, yname and axis for calculating the average""" + arr = self.arr + sdims = (self.get_dim("y"), self.get_dim("x")) + if sdims[0] == sdims[1]: + sdims = sdims[:1] + axis = tuple(map(arr.dims.index, sdims)) + return arr, sdims, axis + + def _insert_fldmean_bounds(self, da, keepdims=False): + xcoord = self.get_coord("x") + ycoord = self.get_coord("y") + sdims = (self.get_dim("y"), self.get_dim("x")) + xbounds = np.array([[xcoord.min(), xcoord.max()]]) + ybounds = np.array([[ycoord.min(), ycoord.max()]]) + xdims = (sdims[-1], "bnds") if keepdims else ("bnds",) + ydims = (sdims[0], "bnds") if keepdims else ("bnds",) + xattrs = xcoord.attrs.copy() + xattrs.pop("bounds", None) + yattrs = ycoord.attrs.copy() + yattrs.pop("bounds", None) + da.psy.base.coords[xcoord.name + "_bnds"] = xr.Variable( + xdims, xbounds if keepdims else xbounds[0], attrs=xattrs + ) + da.psy.base.coords[ycoord.name + "_bnds"] = xr.Variable( + ydims, ybounds if keepdims else ybounds[0], attrs=yattrs + ) + +
+[docs] + def fldmean(self, keepdims=False): + """Calculate the weighted mean over the x- and y-dimension + + This method calculates the weighted mean of the spatial dimensions. + Weights are calculated using the :meth:`gridweights` method, missing + values are ignored. x- and y-dimensions are identified using the + :attr:`decoder`s :meth:`~CFDecoder.get_xname` and + :meth:`~CFDecoder.get_yname` methods. + + Parameters + ---------- + keepdims: bool + If True, the dimensionality of this array is maintained + + Returns + ------- + xr.DataArray + The computed fldmeans. The dimensions are the same as in this + array, only the spatial dimensions are omitted if `keepdims` is + False. + + See Also + -------- + fldstd: For calculating the weighted standard deviation + fldpctl: For calculating weighted percentiles + """ + gridweights = self.gridweights() + arr, sdims, axis = self._fldaverage_args() + + xcoord = self.decoder.get_x( + next(six.itervalues(self.base_variables)), arr.coords + ) + ycoord = self.decoder.get_y( + next(six.itervalues(self.base_variables)), arr.coords + ) + means = ((arr * gridweights)).sum(axis=axis) * ( + gridweights.size / arr.notnull().sum(axis=axis) + ) + if keepdims: + means = means.expand_dims(sdims, axis=axis) + + if keepdims: + means[xcoord.name] = xcoord.mean().expand_dims(xcoord.dims[0]) + means[ycoord.name] = ycoord.mean().expand_dims(ycoord.dims[0]) + else: + means[xcoord.name] = xcoord.mean() + means[ycoord.name] = ycoord.mean() + means.coords[xcoord.name].attrs["bounds"] = xcoord.name + "_bnds" + means.coords[ycoord.name].attrs["bounds"] = ycoord.name + "_bnds" + self._insert_fldmean_bounds(means, keepdims) + means.name = arr.name + return means
+ + +
+[docs] + def fldstd(self, keepdims=False): + """Calculate the weighted standard deviation over x- and y-dimension + + This method calculates the weighted standard deviation of the spatial + dimensions. Weights are calculated using the :meth:`gridweights` + method, missing values are ignored. x- and y-dimensions are identified + using the :attr:`decoder`s :meth:`~CFDecoder.get_xname` and + :meth:`~CFDecoder.get_yname` methods. + + Parameters + ---------- + keepdims: bool + If True, the dimensionality of this array is maintained + + Returns + ------- + xr.DataArray + The computed standard deviations. The dimensions are the same as + in this array, only the spatial dimensions are omitted if + `keepdims` is False. + + See Also + -------- + fldmean: For calculating the weighted mean + fldpctl: For calculating weighted percentiles + """ + arr, sdims, axis = self._fldaverage_args() + means = self.fldmean(keepdims=True) + weights = self.gridweights(keepshape=True) + variance = ((arr - means.values) ** 2 * weights).sum(axis=axis) + if keepdims: + variance = variance.expand_dims(sdims, axis=axis) + for key, coord in six.iteritems(means.coords): + if key not in variance.coords: + dims = set(sdims).intersection(coord.dims) + variance[key] = ( + coord + if keepdims + else coord.isel(**dict(zip(dims, repeat(0)))) + ) + for key, coord in six.iteritems(means.psy.base.coords): + if key not in variance.psy.base.coords: + dims = set(sdims).intersection(coord.dims) + variance.psy.base[key] = ( + coord + if keepdims + else coord.isel(**dict(zip(dims, repeat(0)))) + ) + std = variance**0.5 + std.name = arr.name + return std
+ + +
+[docs] + def fldpctl(self, q, keepdims=False): + """Calculate the percentiles along the x- and y-dimensions + + This method calculates the specified percentiles along the given + dimension. Percentiles are weighted by the :meth:`gridweights` method + and missing values are ignored. x- and y-dimensions are estimated + through the :attr:`decoder`s :meth:`~CFDecoder.get_xname` and + :meth:`~CFDecoder.get_yname` methods + + Parameters + ---------- + q: float or list of floats between 0 and 100 + The quantiles to estimate + keepdims: bool + If True, the number of dimensions of the array are maintained + + Returns + ------- + xr.DataArray + The data array with the dimensions. If `q` is a list or `keepdims` + is True, the first dimension will be the percentile ``'pctl'``. + The other dimensions are the same as in this array, only the + spatial dimensions are omitted if `keepdims` is False. + + See Also + -------- + fldstd: For calculating the weighted standard deviation + fldmean: For calculating the weighted mean + + Warnings + -------- + This method does load the entire array into memory! So take care if you + handle big data.""" + gridweights = self.gridweights(keepshape=True) + arr = self.arr + + q = np.asarray(q) / 100.0 + if not (np.all(q >= 0) and np.all(q <= 100)): + raise ValueError("q should be in [0, 100]") + reduce_shape = False if keepdims else (not bool(q.ndim)) + if not q.ndim: + q = q[np.newaxis] + data = arr.values.copy() + sdims, axis = self._fldaverage_args()[1:] + weights = gridweights.values + # flatten along the spatial axis + for ax in axis: + data = np.rollaxis(data, ax, 0) + weights = np.rollaxis(weights, ax, 0) + data = data.reshape( + (np.product(data.shape[: len(axis)]),) + data.shape[len(axis) :] + ) + weights = weights.reshape( + (np.product(weights.shape[: len(axis)]),) + + weights.shape[len(axis) :] + ) + + # sort the data + sorter = np.argsort(data, axis=0) + all_indices = map(tuple, product(*map(range, data.shape[1:]))) + for indices in all_indices: + indices = (slice(None),) + indices + data.__setitem__( + indices, data.__getitem__(indices)[sorter.__getitem__(indices)] + ) + weights.__setitem__( + indices, + weights.__getitem__(indices)[sorter.__getitem__(indices)], + ) + + # compute the percentiles + try: + weights = np.nancumsum(weights, axis=0) - 0.5 * weights + except AttributeError: + notnull = ~np.isnan(weights) + weights[notnull] = np.cumsum(weights[notnull]) + all_indices = map(tuple, product(*map(range, data.shape[1:]))) + pctl = np.zeros((len(q),) + data.shape[1:]) + + for indices in all_indices: + indices = (slice(None),) + indices + mask = ~np.isnan(data.__getitem__(indices)) + pctl.__setitem__( + indices, + np.interp( + q, + weights.__getitem__(indices)[mask], + data.__getitem__(indices)[mask], + ), + ) + + # setup the data array and it's coordinates + xcoord = self.decoder.get_x( + next(six.itervalues(self.base_variables)), arr.coords + ) + ycoord = self.decoder.get_y( + next(six.itervalues(self.base_variables)), arr.coords + ) + coords = dict(arr.coords) + if keepdims: + pctl = pctl.reshape( + (len(q),) + + tuple(1 if i in axis else s for i, s in enumerate(arr.shape)) + ) + coords[xcoord.name] = xcoord.mean().expand_dims(xcoord.dims[0]) + coords[ycoord.name] = ycoord.mean().expand_dims(ycoord.dims[0]) + dims = arr.dims + else: + coords[xcoord.name] = xcoord.mean() + coords[ycoord.name] = ycoord.mean() + dims = tuple(d for d in arr.dims if d not in sdims) + if reduce_shape: + pctl = pctl[0] + coords["pctl"] = xr.Variable( + (), q[0] * 100.0, attrs={"long_name": "Percentile"} + ) + else: + coords["pctl"] = xr.Variable( + ("pctl",), q * 100.0, attrs={"long_name": "Percentile"} + ) + dims = ("pctl",) + dims + coords[xcoord.name].attrs["bounds"] = xcoord.name + "_bnds" + coords[ycoord.name].attrs["bounds"] = ycoord.name + "_bnds" + coords = { + name: c for name, c in coords.items() if set(c.dims) <= set(dims) + } + ret = xr.DataArray( + pctl, + name=arr.name, + dims=dims, + coords=coords, + attrs=arr.attrs.copy(), + ) + self._insert_fldmean_bounds(ret, keepdims) + return ret
+
+ + + +
+[docs] +class ArrayList(list): + """Base class for creating a list of interactive arrays from a dataset + + This list contains and manages :class:`InteractiveArray` instances""" + + docstrings.keep_params("InteractiveBase.parameters", "auto_update") + + @property + def dims(self): + """Dimensions of the arrays in this list""" + return set(chain(*(arr.dims for arr in self))) + + @property + def dims_intersect(self): + """Dimensions of the arrays in this list that are used in all arrays""" + return set.intersection( + *map( + set, (getattr(arr, "dims_intersect", arr.dims) for arr in self) + ) + ) + + @property + def arr_names(self): + """Names of the arrays (!not of the variables!) in this list + + This attribute can be set with an iterable of unique names to change + the array names of the data objects in this list.""" + return list(arr.psy.arr_name for arr in self) + + @arr_names.setter + def arr_names(self, value): + value = list(islice(value, 0, len(self))) + if not len(set(value)) == len(self): + raise ValueError( + "Got %i unique array names for %i data objects!" + % (len(set(value)), len(self)) + ) + for arr, n in zip(self, value): + arr.psy.arr_name = n + + @property + def names(self): + """Set of the variable in this list""" + ret = set() + for arr in self: + if isinstance(arr, InteractiveList): + ret.update(arr.names) + else: + ret.add(arr.name) + return ret + + @property + def all_names(self): + """The variable names for each of the arrays in this list""" + return [ + _get_variable_names(arr) + if not isinstance(arr, ArrayList) + else arr.all_names + for arr in self + ] + + @property + def all_dims(self): + """The dimensions for each of the arrays in this list""" + return [ + _get_dims(arr) if not isinstance(arr, ArrayList) else arr.all_dims + for arr in self + ] + + @property + def is_unstructured(self): + """A boolean for each array whether it is unstructured or not""" + return [ + arr.psy.decoder.is_unstructured(arr) + if not isinstance(arr, ArrayList) + else arr.is_unstructured + for arr in self + ] + + @property + def coords(self): + """Names of the coordinates of the arrays in this list""" + return set(chain(*(arr.coords for arr in self))) + + @property + def coords_intersect(self): + """Coordinates of the arrays in this list that are used in all arrays""" + return set.intersection( + *map( + set, + (getattr(arr, "coords_intersect", arr.coords) for arr in self), + ) + ) + + @property + def with_plotter(self): + """The arrays in this instance that are visualized with a plotter""" + return self.__class__( + (arr for arr in self if arr.psy.plotter is not None), + auto_update=bool(self.auto_update), + ) + + no_auto_update = property( + _no_auto_update_getter, doc=_no_auto_update_getter.__doc__ + ) + + @no_auto_update.setter + def no_auto_update(self, value): + for arr in self: + arr.psy.no_auto_update = value + self.no_auto_update.value = bool(value) + + @property + def logger(self): + """:class:`logging.Logger` of this instance""" + try: + return self._logger + except AttributeError: + name = "%s.%s" % (self.__module__, self.__class__.__name__) + self._logger = logging.getLogger(name) + self.logger.debug("Initializing...") + return self._logger + + @logger.setter + def logger(self, value): + self._logger = value + + @property + def arrays(self): + """A list of all the :class:`xarray.DataArray` instances in this list""" + return list( + chain.from_iterable( + ( + [arr] + if not isinstance(arr, InteractiveList) + else arr.arrays + for arr in self + ) + ) + ) + +
+[docs] + @docstrings.get_sections( + base="ArrayList.rename", sections=["Parameters", "Raises"] + ) + @dedent + def rename(self, arr, new_name=True): + """ + Rename an array to find a name that isn't already in the list + + Parameters + ---------- + arr: InteractiveBase + A :class:`InteractiveArray` or :class:`InteractiveList` instance + whose name shall be checked + new_name: bool or str + If False, and the ``arr_name`` attribute of the new array is + already in the list, a ValueError is raised. + If True and the ``arr_name`` attribute of the new array is not + already in the list, the name is not changed. Otherwise, if the + array name is already in use, `new_name` is set to 'arr{0}'. + If not True, this will be used for renaming (if the array name of + `arr` is in use or not). ``'{0}'`` is replaced by a counter + + Returns + ------- + InteractiveBase + `arr` with changed ``arr_name`` attribute + bool or None + True, if the array has been renamed, False if not and None if the + array is already in the list + + Raises + ------ + ValueError + If it was impossible to find a name that isn't already in the list + ValueError + If `new_name` is False and the array is already in the list""" + name_in_me = arr.psy.arr_name in self.arr_names + if not name_in_me: + return arr, False + elif name_in_me and not self._contains_array(arr): + if new_name is False: + raise ValueError( + "Array name %s is already in use! Set the `new_name` " + "parameter to None for renaming!" % arr.psy.arr_name + ) + elif new_name is True: + new_name = new_name if isstring(new_name) else "arr{0}" + arr.psy.arr_name = self.next_available_name(new_name) + return arr, True + return arr, None
+ + + docstrings.keep_params("ArrayList.rename.parameters", "new_name") + docstrings.keep_params("InteractiveBase.parameters", "auto_update") + + @docstrings.get_sections(base="ArrayList") + @docstrings.dedent + def __init__(self, iterable=[], attrs={}, auto_update=None, new_name=True): + """ + Parameters + ---------- + iterable: iterable + The iterable (e.g. another list) defining this list + attrs: dict-like or iterable, optional + Global attributes of this list + %(InteractiveBase.parameters.auto_update)s + %(ArrayList.rename.parameters.new_name)s""" + super(ArrayList, self).__init__() + self.attrs = dict(attrs) + if auto_update is None: + auto_update = rcParams["lists.auto_update"] + self.auto_update = not bool(auto_update) + # append the data in order to set the correct names + self.extend( + filter( + lambda arr: isinstance( + getattr(arr, "psy", None), InteractiveBase + ), + iterable, + ), + new_name=new_name, + ) + +
+[docs] + def copy(self, deep=False): + """Returns a copy of the list + + Parameters + ---------- + deep: bool + If False (default), only the list is copied and not the contained + arrays, otherwise the contained arrays are deep copied""" + if not deep: + return self.__class__( + self[:], + attrs=self.attrs.copy(), + auto_update=not bool(self.no_auto_update), + ) + else: + return self.__class__( + [arr.psy.copy(deep) for arr in self], + attrs=self.attrs.copy(), + auto_update=not bool(self.auto_update), + )
+ + + docstrings.keep_params("InteractiveArray.update.parameters", "method") + +
+[docs] + @classmethod + @docstrings.get_sections( + base="ArrayList.from_dataset", + sections=["Parameters", "Other Parameters", "Returns"], + ) + @docstrings.dedent + def from_dataset( + cls, + base, + method="isel", + default_slice=None, + decoder=None, + auto_update=None, + prefer_list=False, + squeeze=True, + attrs=None, + load=False, + **kwargs, + ): + """ + Construct an ArrayList instance from an existing base dataset + + Parameters + ---------- + base: xarray.Dataset + Dataset instance that is used as reference + %(InteractiveArray.update.parameters.method)s + %(InteractiveBase.parameters.auto_update)s + prefer_list: bool + If True and multiple variable names pher array are found, the + :class:`InteractiveList` class is used. Otherwise the arrays are + put together into one :class:`InteractiveArray`. + default_slice: indexer + Index (e.g. 0 if `method` is 'isel') that shall be used for + dimensions not covered by `dims` and `furtherdims`. If None, the + whole slice will be used. Note that the `default_slice` is always + based on the `isel` method. + decoder: CFDecoder or dict + Arguments for the decoder. This can be one of + + - an instance of :class:`CFDecoder` + - a subclass of :class:`CFDecoder` + - a dictionary with keyword-arguments to the automatically + determined decoder class + - None to automatically set the decoder + squeeze: bool, optional + Default True. If True, and the created arrays have a an axes with + length 1, it is removed from the dimension list (e.g. an array + with shape (3, 4, 1, 5) will be squeezed to shape (3, 4, 5)) + attrs: dict, optional + Meta attributes that shall be assigned to the selected data arrays + (additional to those stored in the `base` dataset) + load: bool or dict + If True, load the data from the dataset using the + :meth:`xarray.DataArray.load` method. If :class:`dict`, those will + be given to the above mentioned ``load`` method + + Other Parameters + ---------------- + %(setup_coords.parameters)s + + Returns + ------- + ArrayList + The list with the specified :class:`InteractiveArray` instances + that hold a reference to the given `base`""" + try: + load = dict(load) + except (TypeError, ValueError): + + def maybe_load(arr): + return arr.load() if load else arr + + else: + + def maybe_load(arr): + return arr.load(**load) + + def iter_dims(dims): + """Split the given dictionary into multiples and iterate over it""" + if not dims: + while 1: + yield {} + else: + dims = dict(dims) + keys = dims.keys() + for vals in zip(*map(cycle, map(safe_list, dims.values()))): + yield dict(zip(keys, vals)) + + def recursive_selection(key, dims, names): + names = safe_list(names) + if len(names) > 1 and prefer_list: + keys = ("arr%i" % i for i in range(len(names))) + return InteractiveList( + starmap(sel_method, zip(keys, iter_dims(dims), names)), + auto_update=auto_update, + arr_name=key, + ) + elif len(names) > 1: + return sel_method(key, dims, tuple(names)) + else: + return sel_method(key, dims, names[0]) + + def ds2arr(arr): + base_var = next( + var + for key, var in arr.variables.items() + if key not in arr.coords + ) + attrs = base_var.attrs + arr = arr.to_array() + if "coordinates" in base_var.encoding: + arr.encoding["coordinates"] = base_var.encoding["coordinates"] + arr.attrs.update(attrs) + return arr + + decoder_input = decoder + + def get_decoder(arr): + if decoder_input is None: + return CFDecoder.get_decoder(base, arr) + elif isinstance(decoder_input, CFDecoder): + return decoder_input + elif isinstance(decoder_input, dict): + return CFDecoder.get_decoder(base, arr, **decoder_input) + else: + return decoder_input(base) + + def add_missing_dimensions(arr): + # add the missing dimensions to the dataset. This is not anymore + # done by default from xarray >= 0.9 but we need it to ensure the + # interactive treatment of DataArrays + missing = set(arr.dims).difference(base.coords) - {"variable"} + for dim in missing: + try: + size = base.sizes[dim] + except AttributeError: + # old xarray version + size = base.dims[dim] + base[dim] = arr.coords[dim] = np.arange(size) + + if squeeze: + + def squeeze_array(arr): + return arr.isel( + **{ + dim: 0 + for i, dim in enumerate(arr.dims) + if arr.shape[i] == 1 + } + ) + + else: + + def squeeze_array(arr): + return arr + + if method == "isel": + + def sel_method(key, dims, name=None): + if name is None: + return recursive_selection(key, dims, dims.pop("name")) + elif isinstance( + name, six.string_types + ) or not utils.is_iterable(name): + arr = base[name] + decoder = get_decoder(arr) + dims = decoder.correct_dims(arr, dims) + else: + arr = base[list(name)] + decoder = get_decoder(base[name[0]]) + dims = decoder.correct_dims(base[name[0]], dims) + def_slice = ( + slice(None) if default_slice is None else default_slice + ) + dims.update( + { + dim: def_slice + for dim in set(arr.dims).difference(dims) + if dim != "variable" + } + ) + add_missing_dimensions(arr) + ret = arr.isel(**dims) + if not isinstance(ret, xr.DataArray): + ret = ds2arr(ret) + ret = squeeze_array(ret) + # delete the variable dimension for the idims + dims.pop("variable", None) + ret.psy.init_accessor( + arr_name=key, base=base, idims=dims, decoder=decoder + ) + return maybe_load(ret) + + else: + + def sel_method(key, dims, name=None): + if name is None: + return recursive_selection(key, dims, dims.pop("name")) + elif isinstance( + name, six.string_types + ) or not utils.is_iterable(name): + arr = base[name] + decoder = get_decoder(arr) + dims = decoder.correct_dims(arr, dims) + else: + arr = base[list(name)] + decoder = get_decoder(base[name[0]]) + dims = decoder.correct_dims(base[name[0]], dims) + if default_slice is not None: + if isinstance(default_slice, slice): + dims.update( + { + dim: default_slice + for dim in set(arr.dims).difference(dims) + if dim != "variable" + } + ) + else: + dims.update( + { + dim: arr.coords[dim][default_slice] + for dim in set(arr.dims).difference(dims) + if dim != "variable" + } + ) + kws = dims.copy() + kws["method"] = method + # the sel method does not work with slice objects + for dim, val in dims.items(): + if isinstance(val, slice): + if val == slice(None): + kws.pop(dim) # the full slice is the default + else: + kws.pop("method", None) + add_missing_dimensions(arr) + try: + ret = arr.sel(**kws) + except KeyError: + _fix_times(kws) + ret = arr.sel(**kws) + if not isinstance(ret, xr.DataArray): + ret = ds2arr(ret) + ret = squeeze_array(ret) + ret.psy.init_accessor(arr_name=key, base=base, decoder=decoder) + return maybe_load(ret) + + if "name" not in kwargs: + default_names = list( + key for key in base.variables if key not in base.coords + ) + try: + default_names.sort() + except TypeError: + pass + kwargs["name"] = default_names + names = setup_coords(**kwargs) + # check coordinates + possible_keys = ["t", "x", "y", "z", "name"] + list(base.dims) + for key in set(chain(*six.itervalues(names))): + utils.check_key(key, possible_keys, name="dimension") + instance = cls( + starmap(sel_method, six.iteritems(names)), + attrs=base.attrs, + auto_update=auto_update, + ) + # convert to interactive lists if an instance is not + if prefer_list and any( + not isinstance(arr, InteractiveList) for arr in instance + ): + # if any instance is an interactive list, than convert the others + if any(isinstance(arr, InteractiveList) for arr in instance): + for i, arr in enumerate(instance): + if not isinstance(arr, InteractiveList): + instance[i] = InteractiveList([arr]) + else: # put everything into one single interactive list + instance = cls( + [ + InteractiveList( + instance, attrs=base.attrs, auto_update=auto_update + ) + ] + ) + instance[0].psy.arr_name = instance[0][0].psy.arr_name + if attrs is not None: + for arr in instance: + arr.attrs.update(attrs) + return instance
+ + + @classmethod + def _get_dsnames( + cls, + data, + ignore_keys=["attrs", "plotter", "ds"], + concat_dim=False, + combine=False, + ): + """Recursive method to get all the file names out of a dictionary + `data` created with the :meth`array_info` method""" + + def filter_ignores(item): + return item[0] not in ignore_keys and isinstance(item[1], dict) + + if "fname" in data: + return { + tuple( + [data["fname"], data["store"]] + + ([data.get("concat_dim")] if concat_dim else []) + + ([data.get("combine")] if combine else []) + ) + } + return set( + chain( + *map( + partial( + cls._get_dsnames, + concat_dim=concat_dim, + combine=combine, + ignore_keys=ignore_keys, + ), + dict(filter(filter_ignores, six.iteritems(data))).values(), + ) + ) + ) + + @classmethod + def _get_ds_descriptions( + cls, data, ds_description={"ds", "fname", "arr"}, **kwargs + ): + def new_dict(): + return defaultdict(list) + + ret = defaultdict(new_dict) + ds_description = set(ds_description) + for d in cls._get_ds_descriptions_unsorted(data, **kwargs): + try: + num = d.get("num") or d["ds"].psy.num + except KeyError: + raise ValueError( + "Could not find either the dataset number nor the dataset " + "in the data! However one must be provided." + ) + d_ret = ret[num] + for key, val in six.iteritems(d): + if key == "arr": + d_ret["arr"].append(d["arr"]) + else: + d_ret[key] = val + return ret + + @classmethod + def _get_ds_descriptions_unsorted( + cls, data, ignore_keys=["attrs", "plotter"], nums=None + ): + """Recursive method to get all the file names or datasets out of a + dictionary `data` created with the :meth`array_info` method""" + ds_description = {"ds", "fname", "num", "arr", "store"} + if "ds" in data: + # make sure that the data set has a number assigned to it + data["ds"].psy.num + keys_in_data = ds_description.intersection(data) + if keys_in_data: + return {key: data[key] for key in keys_in_data} + for key in ignore_keys: + data.pop(key, None) + func = partial( + cls._get_ds_descriptions_unsorted, + ignore_keys=ignore_keys, + nums=nums, + ) + return chain( + *map( + lambda d: [d] if isinstance(d, dict) else d, + map(func, six.itervalues(data)), + ) + ) + +
+[docs] + @classmethod + @docstrings.get_sections(base="ArrayList.from_dict") + @docstrings.dedent + def from_dict( + cls, + d, + alternative_paths={}, + datasets=None, + pwd=None, + ignore_keys=["attrs", "plotter", "ds"], + only=None, + chname={}, + **kwargs, + ): + """ + Create a list from the dictionary returned by :meth:`array_info` + + This classmethod creates an :class:`~psyplot.data.ArrayList` instance + from a dictionary containing filename, dimension infos and array names + + Parameters + ---------- + d: dict + The dictionary holding the data + alternative_paths: dict or list or str + A mapping from original filenames as used in `d` to filenames that + shall be used instead. If `alternative_paths` is not None, + datasets must be None. Paths must be accessible from the current + working directory. + If `alternative_paths` is a list (or any other iterable) is + provided, the file names will be replaced as they appear in `d` + (note that this is very unsafe if `d` is not and dict) + datasets: dict or list or None + A mapping from original filenames in `d` to the instances of + :class:`xarray.Dataset` to use. If it is an iterable, the same + holds as for the `alternative_paths` parameter + pwd: str + Path to the working directory from where the data can be imported. + If None, use the current working directory. + ignore_keys: list of str + Keys specified in this list are ignored and not seen as array + information (note that ``attrs`` are used anyway) + only: string, list or callable + Can be one of the following three things: + + - a string that represents a pattern to match the array names + that shall be included + - a list of array names to include + - a callable with two arguments, a string and a dict such as + + .. code-block:: python + + def filter_func(arr_name: str, info: dict): -> bool + ''' + Filter the array names + + This function should return True if the array shall be + included, else False + + Parameters + ---------- + arr_name: str + The array name (i.e. the ``arr_name`` attribute) + info: dict + The dictionary with the array informations. Common + keys are ``'name'`` that points to the variable name + and ``'dims'`` that points to the dimensions and + ``'fname'`` that points to the file name + ''' + return True or False + + The function should return ``True`` if the array shall be + included, else ``False``. This function will also be given to + subsequents instances of :class:`InteractiveList` objects that + are contained in the returned value + chname: dict + A mapping from variable names in the project to variable names + that should be used instead + + Other Parameters + ---------------- + ``**kwargs`` + Any other parameter from the `psyplot.data.open_dataset` function + %(open_dataset.parameters)s + + Returns + ------- + psyplot.data.ArrayList + The list with the interactive objects + + See Also + -------- + from_dataset, array_info""" + pwd = pwd or os.getcwd() + if only is None: + + def only_filter(arr_name, info): + return True + + elif callable(only): + only_filter = only + elif isstring(only): + + def only_filter(arr_name, info): + return patt.search(arr_name) is not None + + patt = re.compile(only) + only = None + else: + + def only_filter(arr_name, info): + return arr_name in save_only + + save_only = only + only = None + + def get_fname_use(fname): + squeeze = isstring(fname) + fname = safe_list(fname) + ret = tuple( + f + if utils.is_remote_url(f) or osp.isabs(f) + else osp.join(pwd, f) + for f in fname + ) + return ret[0] if squeeze else ret + + def get_name(name): + if not isstring(name): + return list(map(get_name, name)) + else: + return chname.get(name, name) + + if not isinstance(alternative_paths, dict): + it = iter(alternative_paths) + alternative_paths = defaultdict(partial(next, it, None)) + # first open all datasets if not already done + if datasets is None: + replace_concat_dim = "concat_dim" not in kwargs + replace_combine = "combine" not in kwargs + + names_and_stores = cls._get_dsnames( + d, concat_dim=True, combine=True + ) + datasets = {} + for ( + fname, + (store_mod, store_cls), + concat_dim, + combine, + ) in names_and_stores: + fname_use = fname + got = True + if replace_concat_dim and concat_dim is not None: + kwargs["concat_dim"] = concat_dim + elif replace_concat_dim and concat_dim is None: + kwargs.pop("concat_dim", None) + if replace_combine and combine is not None: + kwargs["combine"] = combine + elif replace_combine and combine is None: + kwargs.pop("combine", None) + try: + fname_use = alternative_paths[fname] + except KeyError: + got = False + if not got or not fname_use: + if fname is not None: + fname_use = get_fname_use(fname) + if fname_use is not None: + datasets[fname] = _open_ds_from_store( + fname_use, store_mod, store_cls, **kwargs + ) + if alternative_paths is not None: + for fname in set(alternative_paths).difference(datasets): + datasets[fname] = _open_ds_from_store(fname, **kwargs) + elif not isinstance(datasets, dict): + it_datasets = iter(datasets) + datasets = defaultdict(partial(next, it_datasets, None)) + arrays = [0] * len(d) + i = 0 + for arr_name, info in six.iteritems(d): + if arr_name in ignore_keys or not only_filter(arr_name, info): + arrays.pop(i) + continue + if not {"fname", "ds", "arr"}.intersection(info): + # the described object is an InteractiveList + arr = InteractiveList.from_dict( + info, + alternative_paths=alternative_paths, + datasets=datasets, + chname=chname, + ) + if not arr: + warn("Skipping empty list %s!" % arr_name) + arrays.pop(i) + continue + else: + if "arr" in info: + arr = info.pop("arr") + elif "ds" in info: + arr = cls.from_dataset( + info["ds"], + dims=info["dims"], + name=get_name(info["name"]), + )[0] + else: + fname = info["fname"] + if fname is None: + warn( + "Could not open array %s because no filename was " + "specified!" % arr_name + ) + arrays.pop(i) + continue + try: # in case, datasets is a defaultdict + datasets[fname] + except KeyError: + pass + if fname not in datasets: + warn( + "Could not open array %s because %s was not in " + "the list of datasets!" % (arr_name, fname) + ) + arrays.pop(i) + continue + arr = cls.from_dataset( + datasets[fname], + dims=info["dims"], + name=get_name(info["name"]), + )[0] + for key, val in six.iteritems(info.get("attrs", {})): + arr.attrs.setdefault(key, val) + arr.psy.arr_name = arr_name + arrays[i] = arr + i += 1 + return cls(arrays, attrs=d.get("attrs", {}))
+ + + docstrings.delete_params("get_filename_ds.parameters", "ds", "dump") + +
+[docs] + @docstrings.get_sections(base="ArrayList.array_info") + @docstrings.dedent + def array_info( + self, + dump=None, + paths=None, + attrs=True, + standardize_dims=True, + pwd=None, + use_rel_paths=True, + alternative_paths={}, + ds_description={"fname", "store"}, + full_ds=True, + copy=False, + **kwargs, + ): + """ + Get dimension informations on you arrays + + This method returns a dictionary containing informations on the + array in this instance + + Parameters + ---------- + dump: bool + If True and the dataset has not been dumped so far, it is dumped to + a temporary file or the one generated by `paths` is used. If it is + False or both, `dump` and `paths` are None, no data will be stored. + If it is None and `paths` is not None, `dump` is set to True. + %(get_filename_ds.parameters.no_ds|dump)s + attrs: bool, optional + If True (default), the :attr:`ArrayList.attrs` and + :attr:`xarray.DataArray.attrs` attributes are included in the + returning dictionary + standardize_dims: bool, optional + If True (default), the real dimension names in the dataset are + replaced by x, y, z and t to be more general. + pwd: str + Path to the working directory from where the data can be imported. + If None, use the current working directory. + use_rel_paths: bool, optional + If True (default), paths relative to the current working directory + are used. Otherwise absolute paths to `pwd` are used + ds_description: 'all' or set of {'fname', 'ds', 'num', 'arr', 'store'} + Keys to describe the datasets of the arrays. If all, all keys + are used. The key descriptions are + + fname + the file name is inserted in the ``'fname'`` key + store + the data store class and module is inserted in the ``'store'`` + key + ds + the dataset is inserted in the ``'ds'`` key + num + The unique number assigned to the dataset is inserted in the + ``'num'`` key + arr + The array itself is inserted in the ``'arr'`` key + full_ds: bool + If True and ``'ds'`` is in `ds_description`, the entire dataset is + included. Otherwise, only the DataArray converted to a dataset is + included + copy: bool + If True, the arrays and datasets are deep copied + + + Other Parameters + ---------------- + %(get_filename_ds.other_parameters)s + + Returns + ------- + dict + An ordered mapping from array names to dimensions and filename + corresponding to the array + + See Also + -------- + from_dict""" + saved_ds = kwargs.pop("_saved_ds", {}) + + def get_alternative(f): + return next( + filter( + lambda t: osp.samefile(f, t[0]), + six.iteritems(alternative_paths), + ), + [False, f], + ) + + if copy: + + def copy_obj(obj): + # try to get the number of the dataset and create only one copy + # copy for each dataset + try: + num = obj.psy.num + except AttributeError: + pass + else: + try: + return saved_ds[num] + except KeyError: + saved_ds[num] = obj.psy.copy(True) + return saved_ds[num] + return obj.psy.copy(True) + + else: + + def copy_obj(obj): + return obj + + ret = dict() + if ds_description == "all": + ds_description = {"fname", "ds", "num", "arr", "store"} + if paths is not None: + if dump is None: + dump = True + paths = iter(paths) + elif dump is None: + dump = False + if pwd is None: + pwd = os.getcwd() + for arr in self: + if isinstance(arr, InteractiveList): + ret[arr.arr_name] = arr.array_info( + dump, + paths, + pwd=pwd, + attrs=attrs, + standardize_dims=standardize_dims, + use_rel_paths=use_rel_paths, + ds_description=ds_description, + alternative_paths=alternative_paths, + copy=copy, + _saved_ds=saved_ds, + **kwargs, + ) + else: + if standardize_dims: + idims = arr.psy.decoder.standardize_dims( + next(arr.psy.iter_base_variables), arr.psy.idims + ) + else: + idims = arr.psy.idims + ret[arr.psy.arr_name] = d = {"dims": idims} + if "variable" in arr.coords: + d["name"] = [list(arr.coords["variable"].values)] + else: + d["name"] = arr.name + if "fname" in ds_description or "store" in ds_description: + fname, store_mod, store_cls = get_filename_ds( + arr.psy.base, dump=dump, paths=paths, **kwargs + ) + if "store" in ds_description: + d["store"] = (store_mod, store_cls) + if "fname" in ds_description: + d["fname"] = [] + for i, f in enumerate(safe_list(fname)): + if f is None or utils.is_remote_url(f): + d["fname"].append(f) + else: + found, f = get_alternative(f) + if use_rel_paths: + f = osp.relpath(f, pwd) + else: + f = osp.abspath(f) + d["fname"].append(f) + if fname is None or isinstance( + fname, six.string_types + ): + d["fname"] = d["fname"][0] + else: + d["fname"] = tuple(safe_list(fname)) + if arr.psy.base.psy._concat_dim is not None: + d["concat_dim"] = arr.psy.base.psy._concat_dim + if arr.psy.base.psy._combine is not None: + d["combine"] = arr.psy.base.psy._combine + if "ds" in ds_description: + if full_ds: + d["ds"] = copy_obj(arr.psy.base) + else: + d["ds"] = copy_obj(arr.to_dataset()) + if "num" in ds_description: + d["num"] = arr.psy.base.psy.num + if "arr" in ds_description: + d["arr"] = copy_obj(arr) + if attrs: + d["attrs"] = arr.attrs + ret["attrs"] = self.attrs + return ret
+ + + def _get_tnames(self): + """Get the name of the time coordinate of the objects in this list""" + tnames = set() + for arr in self: + if isinstance(arr, InteractiveList): + tnames.update(arr.get_tnames()) + else: + tnames.add( + arr.psy.decoder.get_tname( + next(arr.psy.iter_base_variables), arr.coords + ) + ) + return tnames - {None} + + @docstrings.dedent + def _register_update( + self, + method="isel", + replot=False, + dims={}, + fmt={}, + force=False, + todefault=False, + ): + """ + Register new dimensions and formatoptions for updating. The keywords + are the same as for each single array + + Parameters + ---------- + %(InteractiveArray._register_update.parameters)s""" + + for arr in self: + arr.psy._register_update( + method=method, + replot=replot, + dims=dims, + fmt=fmt, + force=force, + todefault=todefault, + ) + +
+[docs] + @docstrings.get_sections(base="ArrayList.start_update") + @dedent + def start_update(self, draw=None): + """ + Conduct the registered plot updates + + This method starts the updates from what has been registered by the + :meth:`update` method. You can call this method if you did not set the + `auto_update` parameter when calling the :meth:`update` method to True + and when the :attr:`no_auto_update` attribute is True. + + Parameters + ---------- + draw: bool or None + If True, all the figures of the arrays contained in this list will + be drawn at the end. If None, it defaults to the `'auto_draw'`` + parameter in the :attr:`psyplot.rcParams` dictionary + + See Also + -------- + :attr:`no_auto_update`, update""" + + def worker(arr): + results[arr.psy.arr_name] = arr.psy.start_update( + draw=False, queues=queues + ) + + if len(self) == 0: + return + + results = {} + threads = [ + Thread( + target=worker, args=(arr,), name="update_%s" % arr.psy.arr_name + ) + for arr in self + ] + jobs = [arr.psy._njobs for arr in self] + queues = [Queue() for _ in range(max(map(len, jobs)))] + # populate the queues + for i, arr in enumerate(self): + for j, n in enumerate(jobs[i]): + for k in range(n): + queues[j].put(arr.psy.arr_name) + for thread in threads: + thread.setDaemon(True) + for thread in threads: + thread.start() + for thread in threads: + thread.join() + if draw is None: + draw = rcParams["auto_draw"] + if draw: + self( + arr_name=[ + name for name, adraw in six.iteritems(results) if adraw + ] + ).draw() + if rcParams["auto_show"]: + self.show()
+ + + docstrings.keep_params("InteractiveArray.update.parameters", "auto_update") + +
+[docs] + @docstrings.get_sections(base="ArrayList.update") + @docstrings.dedent + def update( + self, + method="isel", + dims={}, + fmt={}, + replot=False, + auto_update=False, + draw=None, + force=False, + todefault=False, + enable_post=None, + **kwargs, + ): + """ + Update the coordinates and the plot + + This method updates all arrays in this list with the given coordinate + values and formatoptions. + + Parameters + ---------- + %(InteractiveArray._register_update.parameters)s + %(InteractiveArray.update.parameters.auto_update)s + %(ArrayList.start_update.parameters)s + enable_post: bool + If not None, enable (``True``) or disable (``False``) the + :attr:`~psyplot.plotter.Plotter.post` formatoption in the plotters + ``**kwargs`` + Any other formatoption or dimension that shall be updated + (additionally to those in `fmt` and `dims`) + + Notes + ----- + %(InteractiveArray.update.notes)s + + See Also + -------- + no_auto_update, start_update""" + dims = dict(dims) + fmt = dict(fmt) + vars_and_coords = set( + chain(self.dims, self.coords, ["name", "x", "y", "z", "t"]) + ) + furtherdims, furtherfmt = utils.sort_kwargs(kwargs, vars_and_coords) + dims.update(furtherdims) + fmt.update(furtherfmt) + + self._register_update( + method=method, + replot=replot, + dims=dims, + fmt=fmt, + force=force, + todefault=todefault, + ) + if enable_post is not None: + for arr in self.with_plotter: + arr.psy.plotter.enable_post = enable_post + if not self.no_auto_update or auto_update: + self.start_update(draw)
+ + +
+[docs] + def draw(self): + """Draws all the figures in this instance""" + for fig in set( + chain( + *map(lambda arr: arr.psy.plotter.figs2draw, self.with_plotter) + ) + ): + self.logger.debug("Drawing figure %s", fig.number) + fig.canvas.draw() + for arr in self: + if arr.psy.plotter is not None: + arr.psy.plotter._figs2draw.clear() + self.logger.debug("Done drawing.")
+ + + def __call__(self, types=None, method="isel", fmts=[], **attrs): + """Get the arrays specified by their attributes + + Parameters + ---------- + types: type or tuple of types + Any class that shall be used for an instance check via + :func:`isinstance`. If not None, the :attr:`plotter` attribute + of the array is checked against this `types` + method: {'isel', 'sel'} + Selection method for the dimensions in the arrays to be used. + If `method` is 'isel', dimension values in `attrs` must correspond + to integer values as they are found in the + :attr:`InteractiveArray.idims` attribute. + Otherwise the :meth:`xarray.DataArray.coords` attribute is used. + fmts: list + List of formatoption strings. Only arrays with plotters who have + this formatoption are returned + ``**attrs`` + Parameters may be any attribute of the arrays in this instance, + including the matplotlib axes (``ax``), matplotlib figure + (``fig``) and the array name (``arr_name``). + Values may be iterables (e.g. lists) of the attributes to consider + or callable functions that accept the attribute as a value. If the + value is a string, it will be put into a list.""" + + def safe_item_list(key, val): + return key, val if callable(val) else safe_list(val) + + def filter_list(arr): + other_attrs = attrs.copy() + arr_names = other_attrs.pop("arr_name", None) + return ( + arr_names is None + or ( + arr_names(arr.psy.arr_name) + if callable(arr_names) + else arr.psy.arr_name in arr_names + ) + ) and len(arr) == len( + arr(types=types, method=method, **other_attrs) + ) + + if not attrs: + + def filter_by_attrs(arr): + return True + + elif method == "sel": + + def filter_by_attrs(arr): + if isinstance(arr, InteractiveList): + return filter_list(arr) + tname = arr.psy.decoder.get_tname( + next(six.itervalues(arr.psy.base_variables)) + ) + + def check_values(arr, key, vals): + if key == "arr_name": + attr = arr.psy.arr_name + elif key == "ax": + attr = arr.psy.ax + elif key == "fig": + attr = getattr(arr.psy.ax, "figure", None) + else: + try: + attr = getattr(arr, key) + except AttributeError: + return False + if np.ndim(attr): # do not filter for multiple items + return False + if hasattr(arr.psy, "decoder") and (arr.name == tname): + try: + vals = np.asarray(vals, dtype=np.datetime64) + except ValueError: + pass + else: + return attr.values.astype(vals.dtype) in vals + if callable(vals): + return vals(attr) + return getattr(attr, "values", attr) in vals + + return all( + check_values(arr, key, val) + for key, val in six.iteritems( + arr.psy.decoder.correct_dims( + next(six.itervalues(arr.psy.base_variables)), + attrs, + remove=False, + ) + ) + ) + + else: + + def check_values(arr, key, vals): + if key == "arr_name": + attr = arr.psy.arr_name + elif key == "ax": + attr = arr.psy.ax + elif key == "fig": + attr = getattr(arr.psy.ax, "figure", None) + elif key in arr.coords: + attr = arr.psy.idims[key] + else: + try: + attr = getattr(arr, key) + except AttributeError: + return False + if np.ndim(attr): # do not filter for multiple items + return False + if callable(vals): + return vals(attr) + return attr in vals + + def filter_by_attrs(arr): + if isinstance(arr, InteractiveList): + return filter_list(arr) + return all( + check_values(arr, key, val) + for key, val in six.iteritems( + arr.psy.decoder.correct_dims( + next(six.itervalues(arr.psy.base_variables)), + attrs, + remove=False, + ) + ) + ) + + attrs = dict(starmap(safe_item_list, six.iteritems(attrs))) + ret = self.__class__( + # iterable + ( + arr + for arr in self + if (types is None or isinstance(arr.psy.plotter, types)) + and filter_by_attrs(arr) + ), + # give itself as base and the auto_update parameter + auto_update=bool(self.auto_update), + ) + # now filter for the formatoptions + if fmts: + fmts = set(safe_list(fmts)) + ret = self.__class__( + filter( + lambda arr: ( + arr.psy.plotter and fmts <= set(arr.psy.plotter) + ), + ret, + ) + ) + return ret + + def __contains__(self, val): + try: + name = val if isstring(val) else val.psy.arr_name + except AttributeError: + return False + else: + return name in self.arr_names and ( + isstring(val) or self._contains_array(val) + ) + + def _contains_array(self, val): + """Checks whether exactly this array is in the list""" + arr = self(arr_name=val.psy.arr_name)[0] + is_not_list = any( + map(lambda a: not isinstance(a, InteractiveList), [arr, val]) + ) + is_list = any( + map(lambda a: isinstance(a, InteractiveList), [arr, val]) + ) + # if one is an InteractiveList and the other not, they differ + if is_list and is_not_list: + return False + # if both are interactive lists, check the lists + if is_list: + return all(a in arr for a in val) and all(a in val for a in arr) + # else we check the shapes and values + return arr is val + + def _short_info(self, intend=0, maybe=False): + if maybe: + intend = 0 + str_intend = " " * intend + if len(self) == 1: + return str_intend + "%s%s.%s([%s])" % ( + "" if not hasattr(self, "arr_name") else self.arr_name + ": ", + self.__class__.__module__, + self.__class__.__name__, + self[0].psy._short_info(intend + 4, maybe=True), + ) + return str_intend + "%s%s.%s([\n%s])" % ( + "" if not hasattr(self, "arr_name") else self.arr_name + ": ", + self.__class__.__module__, + self.__class__.__name__, + ",\n".join( + "%s" % (arr.psy._short_info(intend + 4)) for arr in self + ), + ) + + def __str__(self): + return self._short_info() + + def __repr__(self): + return self.__str__() + + def __getitem__(self, key): + """Overwrites lists __getitem__ by returning an ArrayList if `key` is a + slice""" + if isinstance(key, slice): # return a new ArrayList + return self.__class__(super(ArrayList, self).__getitem__(key)) + else: # return the item + return super(ArrayList, self).__getitem__(key) + + if six.PY2: # for compatibility to python 2.7 + + def __getslice__(self, *args): + return self[slice(*args)] + +
+[docs] + def next_available_name(self, fmt_str="arr{0}", counter=None): + """Create a new array out of the given format string + + Parameters + ---------- + format_str: str + The base string to use. ``'{0}'`` will be replaced by a counter + counter: iterable + An iterable where the numbers should be drawn from. If None, + ``range(100)`` is used + + Returns + ------- + str + A possible name that is not in the current project""" + names = self.arr_names + counter = counter or iter(range(1000)) + try: + new_name = next( + filter(lambda n: n not in names, map(fmt_str.format, counter)) + ) + except StopIteration: + raise ValueError("{0} already in the list".format(fmt_str)) + return new_name
+ + +
+[docs] + @docstrings.dedent + def append(self, value, new_name=False): + """ + Append a new array to the list + + Parameters + ---------- + value: InteractiveBase + The data object to append to this list + %(ArrayList.rename.parameters.new_name)s + + Raises + ------ + %(ArrayList.rename.raises)s + + See Also + -------- + list.append, extend, rename""" + arr, renamed = self.rename(value, new_name) + if renamed is not None: + super(ArrayList, self).append(value)
+ + +
+[docs] + @docstrings.dedent + def extend(self, iterable, new_name=False): + """ + Add further arrays from an iterable to this list + + Parameters + ---------- + iterable + Any iterable that contains :class:`InteractiveBase` instances + %(ArrayList.rename.parameters.new_name)s + + Raises + ------ + %(ArrayList.rename.raises)s + + See Also + -------- + list.extend, append, rename""" + # extend those arrays that aren't alredy in the list + super(ArrayList, self).extend( + t[0] + for t in filter( + lambda t: t[1] is not None, + (self.rename(arr, new_name) for arr in iterable), + ) + )
+ + +
+[docs] + def remove(self, arr): + """Removes an array from the list + + Parameters + ---------- + arr: str or :class:`InteractiveBase` + The array name or the data object in this list to remove + + Raises + ------ + ValueError + If no array with the specified array name is in the list""" + name = arr if isinstance(arr, six.string_types) else arr.psy.arr_name + if arr not in self: + raise ValueError("Array {0} not in the list".format(name)) + for i, arr in enumerate(self): + if arr.psy.arr_name == name: + del self[i] + return + raise ValueError("No array found with name {0}".format(name))
+
+ + + +
+[docs] +@xr.register_dataset_accessor("psy") +class DatasetAccessor(object): + """A dataset accessor to interface with the psyplot package""" + + _filename = None + _data_store = None + _num = None + _plot = None + + #: The concatenation dimension for datasets opened with open_mfdataset + _concat_dim = None + + #: The combine method to open multiple datasets with open_mfdataset + _combine = None + + @property + def num(self): + """A unique number for the dataset""" + if self._num is None: + self._num = next(_ds_counter) + return self._num + + @num.setter + def num(self, value): + self._num = value + + def __init__(self, ds): + self.ds = ds + + @property + def plot(self): + """An object to generate new plots from this dataset + + To make a 2D-plot with the :mod:`psy-simple <psy_simple.plugin>` + plugin, you can just type + + .. code-block:: python + + project = ds.psy.plot.plot2d(name='variable-name') + + It will create a new subproject with the extracted and visualized data. + + See Also + -------- + psyplot.project.DatasetPlotter: for the different plot methods + """ + if self._plot is None: + import psyplot.project as psy + + self._plot = psy.DatasetPlotter(self.ds) + return self._plot + + @property + def filename(self): + """The name of the file that stores this dataset""" + fname = self._filename + if fname is None: + fname = get_filename_ds(self.ds, dump=False)[0] + return fname + + @filename.setter + def filename(self, value): + self._filename = value + + @property + def data_store(self): + """The :class:`xarray.backends.common.AbstractStore` used to save the + dataset""" + store_info = self._data_store + if store_info is None or any(s is None for s in store_info): + store = getattr(self.ds, "_file_obj", None) + store_mod = store.__module__ if store is not None else None + store_cls = store.__class__.__name__ if store is not None else None + return store_mod, store_cls + return store_info + + @data_store.setter + def data_store(self, value): + self._data_store = value + +
+[docs] + @docstrings.dedent + def create_list(self, *args, **kwargs): + """ + Create a :class:`psyplot.data.ArrayList` with arrays from this dataset + + Parameters + ---------- + %(ArrayList.from_dataset.parameters)s + + Other Parameters + ---------------- + %(ArrayList.from_dataset.other_parameters)s + + Returns + ------- + %(ArrayList.from_dataset.returns)s + + See Also + -------- + psyplot.data.ArrayList.from_dataset""" + return ArrayList.from_dataset(self.ds, *args, **kwargs)
+ + +
+[docs] + def to_array(self, *args, **kwargs): + """Same as :meth:`xarray.Dataset.to_array` but sets the base""" + # the docstring is set below + ret = self.ds.to_array(*args, **kwargs) + ret.psy.base = self.ds + return ret
+ + + to_array.__doc__ = xr.Dataset.to_array.__doc__ + + def __getitem__(self, key): + ret = self.ds[key] + if isinstance(ret, xr.DataArray): + ret.psy.base = self.ds + return ret + + def __getattr__(self, attr): + if attr != "ds" and attr in self.ds: + ret = getattr(self.ds, attr) + ret.psy.base = self.ds + return ret + else: + raise AttributeError( + "%s has not Attribute %s" % (self.__class__.__name__, attr) + ) + +
+[docs] + def copy(self, deep=False): + """Copy the array + + This method returns a copy of the underlying array in the :attr:`arr` + attribute. It is more stable because it creates a new `psy` accessor""" + ds = self.ds.copy(deep) + ds.psy = DatasetAccessor(ds) + return ds
+
+ + + +
+[docs] +class InteractiveList(ArrayList, InteractiveBase): + """List of :class:`InteractiveArray` instances that can be plotted itself + + This class combines the :class:`ArrayList` and the interactive plotting + through :class:`psyplot.plotter.Plotter` classes. It is mainly used by the + :mod:`psyplot.plotter.simple` module""" + + no_auto_update = property( + _no_auto_update_getter, doc=_no_auto_update_getter.__doc__ + ) + + @no_auto_update.setter + def no_auto_update(self, value): + ArrayList.no_auto_update.fset(self, value) + InteractiveBase.no_auto_update.fset(self, value) + + @property + @docstrings + def _njobs(self): + """%(InteractiveBase._njobs)s""" + ret = super(self.__class__, self)._njobs or [0] + ret[0] += 1 + return ret + + @property + def psy(self): + """Return the list itself""" + return self + + logger = InteractiveBase.logger + + docstrings.delete_params("InteractiveBase.parameters", "auto_update") + + @docstrings.dedent + def __init__(self, *args, **kwargs): + """ + Parameters + ---------- + %(ArrayList.parameters)s + %(InteractiveBase.parameters.no_auto_update)s""" + ibase_kwargs, array_kwargs = utils.sort_kwargs( + kwargs, ["plotter", "arr_name"] + ) + self._registered_updates = {} + InteractiveBase.__init__(self, **ibase_kwargs) + with self.block_signals: + ArrayList.__init__(self, *args, **kwargs) + + @docstrings.dedent + def _register_update( + self, + method="isel", + replot=False, + dims={}, + fmt={}, + force=False, + todefault=False, + ): + """ + Register new dimensions and formatoptions for updating + + Parameters + ---------- + %(InteractiveArray._register_update.parameters)s""" + ArrayList._register_update(self, method=method, dims=dims) + InteractiveBase._register_update( + self, + fmt=fmt, + todefault=todefault, + replot=bool(dims) or replot, + force=force, + ) + +
+[docs] + @docstrings.dedent + def start_update(self, draw=None, queues=None): + """ + Conduct the formerly registered updates + + This method conducts the updates that have been registered via the + :meth:`update` method. You can call this method if the + :attr:`auto_update` attribute of this instance is True and the + `auto_update` parameter in the :meth:`update` method has been set to + False + + Parameters + ---------- + %(InteractiveBase.start_update.parameters)s + + Returns + ------- + %(InteractiveBase.start_update.returns)s + + See Also + -------- + :attr:`no_auto_update`, update + """ + if queues is not None: + queues[0].get() + try: + for arr in self: + arr.psy.start_update(draw=False) + self.onupdate.emit() + except Exception: + self._finish_all(queues) + raise + if queues is not None: + queues[0].task_done() + return InteractiveBase.start_update(self, draw=draw, queues=queues)
+ + +
+[docs] + def to_dataframe(self): + def to_df(arr): + df = arr.to_pandas() + if hasattr(df, "to_frame"): + df = df.to_frame() + if not keep_names: + return df.rename(columns={df.keys()[0]: arr.psy.arr_name}) + return df + + if len(self) == 1: + return self[0].to_series().to_frame() + else: + keep_names = len(set(arr.name for arr in self)) == self + df = to_df(self[0]) + for arr in self[1:]: + df = df.merge( + to_df(arr), left_index=True, right_index=True, how="outer" + ) + return df
+ + + docstrings.delete_params("ArrayList.from_dataset.parameters", "plotter") + docstrings.delete_kwargs( + "ArrayList.from_dataset.other_parameters", "args", "kwargs" + ) + +
+[docs] + @classmethod + @docstrings.dedent + def from_dataset(cls, *args, **kwargs): + """ + Create an InteractiveList instance from the given base dataset + + Parameters + ---------- + %(ArrayList.from_dataset.parameters.no_plotter)s + plotter: psyplot.plotter.Plotter + The plotter instance that is used to visualize the data in this + list + make_plot: bool + If True, the plot is made + + Other Parameters + ---------------- + %(ArrayList.from_dataset.other_parameters.no_args_kwargs)s + ``**kwargs`` + Further keyword arguments may point to any of the dimensions of the + data (see `dims`) + + Returns + ------- + %(ArrayList.from_dataset.returns)s""" + plotter = kwargs.pop("plotter", None) + make_plot = kwargs.pop("make_plot", True) + instance = super(InteractiveList, cls).from_dataset(*args, **kwargs) + if plotter is not None: + plotter.initialize_plot(instance, make_plot=make_plot) + return instance
+ + +
+[docs] + def extend(self, *args, **kwargs): + # reimplemented to emit onupdate + super(InteractiveList, self).extend(*args, **kwargs) + self.onupdate.emit()
+ + +
+[docs] + def append(self, *args, **kwargs): + # reimplemented to emit onupdate + super(InteractiveList, self).append(*args, **kwargs) + self.onupdate.emit()
+ + +
+[docs] + def to_interactive_list(self): + return self
+
+ + + +class _MissingModule(object): + """Class that can be used if an optional module is not avaible. + + This class raises an error if any attribute is accessed or it is called""" + + def __init__(self, error): + """ + Parameters + ---------- + error: ImportError + The error that has been raised when tried to import the module""" + self.error = error + + def __getattr__(self, attr): + raise self.error + + def __call__(self, *args, **kwargs): + raise self.error + + +def _open_ds_from_store(fname, store_mod=None, store_cls=None, **kwargs): + """Open a dataset and return it""" + if isinstance(fname, xr.Dataset): + return fname + if not isstring(fname): + try: # test iterable + fname[0] + except TypeError: + pass + else: + if store_mod is not None and store_cls is not None: + if isstring(store_mod): + store_mod = repeat(store_mod) + if isstring(store_cls): + store_cls = repeat(store_cls) + fname = [ + _open_store(sm, sc, f) + for sm, sc, f in zip(store_mod, store_cls, fname) + ] + kwargs["engine"] = None + kwargs["lock"] = False + return open_mfdataset(fname, **kwargs) + else: + # try guessing with open_dataset + return open_mfdataset(fname, **kwargs) + if store_mod is not None and store_cls is not None: + fname = _open_store(store_mod, store_cls, fname) + return open_dataset(fname, **kwargs) + + +
+[docs] +def decode_absolute_time(times): + def decode(t): + day = np.floor(t).astype(int) + sub = t - day + rest = dt.timedelta(days=sub) + # round microseconds + if rest.microseconds: + rest += dt.timedelta(microseconds=1e6 - rest.microseconds) + return np.datetime64(dt.datetime.strptime("%i" % day, "%Y%m%d") + rest) + + return np.vectorize(decode, [np.datetime64])(times)
+ + + +
+[docs] +def encode_absolute_time(times): + def encode(t): + t = to_datetime(t) + return ( + float(t.strftime("%Y%m%d")) + + (t - dt.datetime(t.year, t.month, t.day)).total_seconds() + / 86400.0 + ) + + return np.vectorize(encode, [float])(times)
+ + + +
+[docs] +class AbsoluteTimeDecoder(NDArrayMixin): + def __init__(self, array): + self.array = array + example_value = first_n_items(array, 1) or 0 + try: + result = decode_absolute_time(example_value) + except Exception: + logger.error("Could not interprete absolute time values!") + raise + else: + self._dtype = getattr(result, "dtype", np.dtype("object")) + + @property + def dtype(self): + return self._dtype + + def __getitem__(self, key): + return decode_absolute_time(self.array[key])
+ + + +
+[docs] +class AbsoluteTimeEncoder(NDArrayMixin): + def __init__(self, array): + self.array = array + example_value = first_n_items(array, 1) or 0 + try: + result = encode_absolute_time(example_value) + except Exception: + logger.error("Could not interprete absolute time values!") + raise + else: + self._dtype = getattr(result, "dtype", np.dtype("object")) + + @property + def dtype(self): + return self._dtype + + def __getitem__(self, key): + return encode_absolute_time(self.array[key])
+ +
+ +
+
+ + + +
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/psyplot/docstring.html b/_modules/psyplot/docstring.html new file mode 100644 index 0000000..8a6b563 --- /dev/null +++ b/_modules/psyplot/docstring.html @@ -0,0 +1,526 @@ + + + + + + psyplot.docstring — psyplot documentation + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for psyplot.docstring

+"""Docstring module of the psyplot package
+
+We use the docrep_ package for managing our docstrings
+
+.. _docrep: http://docrep.readthedocs.io/en/latest/
+"""
+
+# SPDX-FileCopyrightText: 2016-2024 University of Lausanne
+# SPDX-FileCopyrightText: 2020-2021 Helmholtz-Zentrum Geesthacht
+
+# SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH
+#
+# SPDX-License-Identifier: LGPL-3.0-only
+
+import inspect
+import types
+
+import six
+from docrep import DocstringProcessor, safe_modulo  # noqa: F401
+
+
+
+[docs] +def dedent(func): + """ + Dedent the docstring of a function and substitute with :attr:`params` + + Parameters + ---------- + func: function + function with the documentation to dedent""" + if isinstance(func, types.MethodType) and not six.PY3: + func = func.im_func + func.__doc__ = func.__doc__ and inspect.cleandoc(func.__doc__) + return func
+ + + +
+[docs] +def indent(text, num=4): + """Indet the given string""" + str_indent = " " * num + return str_indent + ("\n" + str_indent).join(text.splitlines())
+ + + +
+[docs] +def append_original_doc(parent, num=0): + """Return an iterator that append the docstring of the given `parent` + function to the applied function""" + + def func(func): + func.__doc__ = func.__doc__ and func.__doc__ + indent( + parent.__doc__, num + ) + return func + + return func
+ + + +_docstrings = DocstringProcessor() + +_docstrings.get_sections(base="DocstringProcessor.get_sections")( + dedent(DocstringProcessor.get_sections) +) + + +
+[docs] +class PsyplotDocstringProcessor(DocstringProcessor): + """ + A :class:`docrep.DocstringProcessor` subclass with possible types section + """ + + param_like_sections = DocstringProcessor.param_like_sections + [ + "Possible types" + ] + +
+[docs] + @_docstrings.dedent + def get_sections( + self, + s=None, + base=None, + sections=["Parameters", "Other Parameters", "Possible types"], + ): + """ + Extract the specified sections out of the given string + + The same as the :meth:`docrep.DocstringProcessor.get_sections` method + but uses the ``'Possible types'`` section by default, too + + Parameters + ---------- + %(DocstringProcessor.get_sections.parameters)s + + Returns + ------- + str + The replaced string + """ + return super(PsyplotDocstringProcessor, self).get_sections( + s, base, sections + )
+
+ + + +del _docstrings + +#: :class:`docrep.PsyplotDocstringProcessor` instance that simplifies the reuse +#: of docstrings from between different python objects. +docstrings = PsyplotDocstringProcessor() +
+ +
+
+ + + +
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/psyplot/gdal_store.html b/_modules/psyplot/gdal_store.html new file mode 100644 index 0000000..d210463 --- /dev/null +++ b/_modules/psyplot/gdal_store.html @@ -0,0 +1,585 @@ + + + + + + psyplot.gdal_store — psyplot documentation + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for psyplot.gdal_store

+# -*- coding: utf-8 -*-
+"""Gdal Store for reading GeoTIFF files into an :class:`xarray.Dataset`
+
+This module contains the definition of the :class:`GdalStore` class that can
+be used to read in a GeoTIFF file into an :class:`xarray.Dataset`.
+It requires that you have the python gdal module installed.
+
+Examples
+--------
+to open a GeoTIFF file named ``'my_tiff.tiff'`` you can do::
+
+    >>> from psyplot.gdal_store import GdalStore
+    >>> from xarray import open_dataset
+    >>> ds = open_dataset(GdalStore("my_tiff"))
+
+Or you use the `engine` of the :func:`psyplot.open_dataset` function:
+
+    >>> ds = open_dataset("my_tiff.tiff", engine="gdal")
+"""
+
+# SPDX-FileCopyrightText: 2016-2024 University of Lausanne
+# SPDX-FileCopyrightText: 2020-2021 Helmholtz-Zentrum Geesthacht
+
+# SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH
+#
+# SPDX-License-Identifier: LGPL-3.0-only
+
+
+import six
+from numpy import arange, dtype, nan
+from xarray import Variable
+from xarray.backends.common import AbstractDataStore
+
+import psyplot.data as psyd
+from psyplot.warning import warn
+
+try:
+    from xarray.core.utils import FrozenOrderedDict
+except ImportError:
+    FrozenOrderedDict = dict
+try:
+    import gdal
+    from osgeo import gdal_array
+except ImportError as e:
+    gdal = psyd._MissingModule(e)
+try:
+    from dask.array import Array
+
+    with_dask = True
+except ImportError:
+    with_dask = False
+
+
+
+[docs] +class GdalStore(AbstractDataStore): + """Datastore to read raster files suitable for the gdal package + + We recommend to use the :func:`psyplot.open_dataset` function to open + a geotiff file:: + + >>> ds = psyplot.open_dataset("my_geotiff.tiff", engine="gdal") + + Notes + ----- + The :class:`GdalStore` object is not as elaborate as, for example, the + `gdal_translate` command. Many attributes, e.g. variable names or netCDF + dimensions will not be interpreted. We only support two + dimensional arrays and each band is saved into one variable named like + ``'Band1', 'Band2', ...``. If you want a more elaborate translation of your + GDAL Raster, convert the file to a netCDF file using ``gdal_translate`` or + the ``gdal.GetDriverByName('netCDF').CreateCopy`` method. However this + class does not create an extra file on your hard disk as it is done by + GDAL.""" + + def __init__(self, filename_or_obj): + """ + Parameters + ---------- + filename_or_obj: str + The path to the GeoTIFF file or a gdal dataset""" + if isinstance(psyd.safe_list(filename_or_obj)[0], six.string_types): + self.ds = gdal.Open(filename_or_obj) + self._filename = filename_or_obj + else: + self.ds = filename_or_obj + fnames = self.ds.GetFileList() + self._filename = fnames[0] if len(fnames) == 1 else fnames + +
+[docs] + def get_variables(self): + def load(band): + band = ds.GetRasterBand(band) + a = band.ReadAsArray() + no_data = band.GetNoDataValue() + if no_data is not None: + try: + a[a == no_data] = a.dtype.type(nan) + except ValueError: + pass + return a + + ds = self.ds + dims = ["lat", "lon"] + chunks = ((ds.RasterYSize,), (ds.RasterXSize,)) + shape = (ds.RasterYSize, ds.RasterXSize) + variables = dict() + for iband in range(1, ds.RasterCount + 1): + band = ds.GetRasterBand(iband) + dt = dtype(gdal_array.codes[band.DataType]) + if with_dask: + dsk = {("x", 0, 0): (load, iband)} + arr = Array(dsk, "x", chunks, shape=shape, dtype=dt) + else: + arr = load(iband) + attrs = band.GetMetadata_Dict() + try: + dt.type(nan) + attrs["_FillValue"] = nan + except ValueError: + no_data = band.GetNoDataValue() + attrs.update({"_FillValue": no_data} if no_data else {}) + variables["Band%i" % iband] = Variable(dims, arr, attrs) + variables["lat"], variables["lon"] = self._load_GeoTransform() + return FrozenOrderedDict(variables)
+ + + def _load_GeoTransform(self): + """Calculate latitude and longitude variable calculated from the + gdal.Open.GetGeoTransform method""" + + def load_lon(): + return arange(ds.RasterXSize) * b[1] + b[0] + + def load_lat(): + return arange(ds.RasterYSize) * b[5] + b[3] + + ds = self.ds + b = self.ds.GetGeoTransform() # bbox, interval + if with_dask: + lat = Array( + {("lat", 0): (load_lat,)}, + "lat", + (self.ds.RasterYSize,), + shape=(self.ds.RasterYSize,), + dtype=float, + ) + lon = Array( + {("lon", 0): (load_lon,)}, + "lon", + (self.ds.RasterXSize,), + shape=(self.ds.RasterXSize,), + dtype=float, + ) + else: + lat = load_lat() + lon = load_lon() + return Variable(("lat",), lat), Variable(("lon",), lon) + +
+[docs] + def get_attrs(self): + from osr import SpatialReference + + attrs = self.ds.GetMetadata() + try: + sp = SpatialReference(wkt=self.ds.GetProjection()) + proj4 = sp.ExportToProj4() + except Exception: + warn("Could not identify projection") + else: + attrs["proj4"] = proj4 + return FrozenOrderedDict(attrs)
+
+ +
+ +
+
+ + + +
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/psyplot/plotter.html b/_modules/psyplot/plotter.html new file mode 100644 index 0000000..517c33b --- /dev/null +++ b/_modules/psyplot/plotter.html @@ -0,0 +1,3298 @@ + + + + + + psyplot.plotter — psyplot documentation + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for psyplot.plotter

+"""Core package for interactive visualization in the psyplot package
+
+This package defines the :class:`Plotter` and :class:`Formatoption` classes,
+the core of the visualization in the :mod:`psyplot` package. Each
+:class:`Plotter` combines a set of formatoption keys where each formatoption
+key is represented by a :class:`Formatoption` subclass."""
+
+# SPDX-FileCopyrightText: 2016-2024 University of Lausanne
+# SPDX-FileCopyrightText: 2020-2021 Helmholtz-Zentrum Geesthacht
+
+# SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH
+#
+# SPDX-License-Identifier: LGPL-3.0-only
+
+import logging
+import weakref
+from abc import ABCMeta, abstractmethod
+from collections import defaultdict
+from datetime import datetime, timedelta
+from itertools import chain, groupby, repeat, starmap, tee
+from textwrap import TextWrapper
+from threading import RLock
+
+import six
+from numpy import datetime64, inf, ndarray, timedelta64
+from xarray.core.formatting import format_timedelta, format_timestamp
+
+from psyplot import rcParams
+from psyplot.config.rcsetup import SubDict
+from psyplot.data import CFDecoder, InteractiveList, _no_auto_update_getter
+from psyplot.docstring import dedent, docstrings
+from psyplot.utils import (
+    Defaultdict,
+    _temp_bool_prop,
+    _TempBool,
+    check_key,
+    unique_everseen,
+)
+from psyplot.warning import PsyPlotRuntimeWarning, warn
+
+#: the default function to use when printing formatoption infos (the default is
+#: use print or in the gui, use the help explorer)
+default_print_func = six.print_
+
+
+#: :class:`dict`. Mapping from group to group names
+groups = {
+    "data": "Data manipulation formatoptions",
+    "axes": "Axes formatoptions",
+    "labels": "Label formatoptions",
+    "plotting": "Plot formatoptions",
+    "post_processing": "Post processing formatoptions",
+    "colors": "Color coding formatoptions",
+    "misc": "Miscallaneous formatoptions",
+    "ticks": "Axis tick formatoptions",
+    "vector": "Vector plot formatoptions",
+    "masking": "Masking formatoptions",
+    "regression": "Fitting formatoptions",
+}
+
+
+def _identity(*args):
+    """identity function to make no validation
+
+    Returns
+    -------
+    object
+        just return the last argument in ``*args``"""
+    return args[-1]
+
+
+
+[docs] +def format_time(x): + """Formats date values + + This function formats :class:`datetime.datetime` and + :class:`datetime.timedelta` objects (and the corresponding numpy objects) + using the :func:`xarray.core.formatting.format_timestamp` and the + :func:`xarray.core.formatting.format_timedelta` functions. + + Parameters + ---------- + x: object + The value to format. If not a time object, the value is returned + + Returns + ------- + str or `x` + Either the formatted time object or the initial `x`""" + if isinstance(x, (datetime64, datetime)): + return format_timestamp(x) + elif isinstance(x, (timedelta64, timedelta)): + return format_timedelta(x) + elif isinstance(x, ndarray): + return list(x) if x.ndim else x[()] + return x
+ + + +
+[docs] +def is_data_dependent(fmto, data): + """Check whether a formatoption is data dependent + + Parameters + ---------- + fmto: Formatoption + The :class:`Formatoption` instance to check + data: xarray.DataArray + The data array to use if the :attr:`~Formatoption.data_dependent` + attribute is a callable + + Returns + ------- + bool + True, if the formatoption depends on the data""" + if callable(fmto.data_dependent): + return fmto.data_dependent(data) + return fmto.data_dependent
+ + + +def _child_property(childname): + def get_x(self): + return getattr(self.plotter, self._child_mapping[childname]) + + return property( + get_x, doc=childname + " Formatoption instance in the plotter" + ) + + +
+[docs] +class FormatoptionMeta(ABCMeta): + """Meta class for formatoptions + + This class serves as a meta class for formatoptions and allows a more + efficient docstring generation by using the + :attr:`psyplot.docstring.docstrings` when creating a new formatoption + class""" + + def __new__(cls, clsname, bases, dct): + """Assign an automatic documentation to the formatoption""" + doc = dct.get("__doc__") + if doc is not None: + dct["__doc__"] = docstrings.dedent(doc) + new_cls = super(FormatoptionMeta, cls).__new__( + cls, clsname, bases, dct + ) + for childname in chain( + new_cls.children, + new_cls.dependencies, + new_cls.connections, + new_cls.parents, + ): + setattr(new_cls, childname, _child_property(childname)) + if new_cls.plot_fmt: + new_cls.data_dependent = True + return new_cls
+ + + +# priority values + +#: Priority value of formatoptions that are updated before the data is loaded. +START = 30 +#: Priority value of formatoptions that are updated before the plot it made. +BEFOREPLOTTING = 20 +#: Priority value of formatoptions that are updated at the end. +END = 10 + + +
+[docs] +@six.add_metaclass(FormatoptionMeta) +class Formatoption(object): + """Abstract formatoption + + This class serves as an abstract version of an formatoption descriptor + that can be used by :class:`~psyplot.plotter.Plotter` instances.""" + + priority = END + """:class:`int`. Priority value of the the formatoption determining when + the formatoption is updated. + + - 10: at the end (for labels, etc.) + - 20: before the plotting (e.g. for colormaps, etc.) + - 30: before loading the data (e.g. for lonlatbox)""" + + #: :class:`str`. Formatoption key of this class in the + #: :class:`~psyplot.plotter.Plotter` class + key = None + + _plotter = None + + @property + def plotter(self): + """:class:`~psyplot.plotter.Plotter`. Plotter instance this + formatoption belongs to""" + if self._plotter is None: + return + return self._plotter() + + @plotter.setter + def plotter(self, value): + if value is not None: + self._plotter = weakref.ref(value) + else: + self._plotter = value + + #: `list of str`. List of formatoptions that have to be updated before this + #: one is updated. Those formatoptions are only updated if they exist in + #: the update parameters. + children = [] + + #: `list of str`. List of formatoptions that force an update of this + #: formatoption if they are updated. + dependencies = [] + + #: `list of str`. Connections to other formatoptions that are (different + #: from :attr:`dependencies` and :attr:`children`) not important for the + #: update process + connections = [] + + #: `list of str`. List of formatoptions that, if included in the update, + #: prevent the update of this formatoption. + parents = [] + + #: :class:`bool`. Has to be True if the formatoption has a ``make_plot`` + #: method to make the plot. + plot_fmt = False + + #: :class:`bool`. True if an update of this formatoption requires a + #: clearing of the axes and reinitialization of the plot + requires_clearing = False + + #: :class:`str`. Key of the group name in :data:`groups` of this + #: formatoption keyword + group = "misc" + + #: :class:`bool` or a callable. This attribute indicates whether this + #: :class:`Formatoption` depends on the data and should be updated if the + #: data changes. If it is a callable, it must accept one argument: the + #: new data. (Note: This is automatically set to True for plot + #: formatoptions) + data_dependent = False + + #: :class:`bool`. True if this formatoption needs an update after the plot + #: has changed + update_after_plot = False + + #: :class:`set` of the :class:`Formatoption` instance that are shared + #: with this instance. + shared = set() + + #: int or None. Index that is used in case the plotting data is a + #: :class:`psyplot.InteractiveList` + index_in_list = 0 + + #: :class:`str`. A bit more verbose name than the formatoption key to be + #: included in the gui. If None, the key is used in the gui + name = None + + #: Boolean that is True if an update of the formatoption requires a replot + requires_replot = False + + @property + def init_kwargs(self): + """:class:`dict` key word arguments that are passed to the + initialization of a new instance when accessed from the descriptor""" + return self._child_mapping + + @property + def project(self): + """Project of the plotter of this instance""" + return self.plotter.project + + @property + def ax(self): + """The axes this Formatoption plots on""" + return self.plotter.ax + + @property + def lock(self): + """A :class:`threading.Rlock` instance to lock while updating + + This lock is used when multiple :class:`plotter` instances are + updated at the same time while sharing formatoptions.""" + try: + return self._lock + except AttributeError: + self._lock = RLock() + return self._lock + + @property + def logger(self): + """Logger of the plotter""" + return self.plotter.logger.getChild(self.key) + + @property + def groupname(self): + """Long name of the group this formatoption belongs too.""" + try: + return groups[self.group] + except KeyError: + warn( + "Unknown formatoption group " + str(self.group), + PsyPlotRuntimeWarning, + ) + return self.group + + @property + def raw_data(self): + """The original data of the plotter of this formatoption""" + if self.index_in_list is not None and isinstance( + self.plotter.data, InteractiveList + ): + return self.plotter.data[self.index_in_list] + else: + return self.plotter.data + + @property + def decoder(self): + """The :class:`~psyplot.data.CFDecoder` instance that decodes the + :attr:`raw_data`""" + # If the decoder is modified by one of the formatoptions, use this one + if self.plotter.plot_data_decoder is not None: + if self.index_in_list is not None and isinstance( + self.plotter.plot_data, InteractiveList + ): + ret = self.plotter.plot_data_decoder[self.index_in_list] + if ret is not None: + return ret + else: + return self.plotter.plot_data_decoder + data = self.raw_data + check = isinstance(data, InteractiveList) + while check: + data = data[0] + check = isinstance(data, InteractiveList) + return data.psy.decoder + + @decoder.setter + def decoder(self, value): + self.set_decoder(value, self.index_in_list) + + @property + def any_decoder(self): + """Return the first possible decoder""" + ret = self.decoder + while not isinstance(ret, CFDecoder): + ret = ret[0] + return ret + + @property + def data(self): + """The data that is plotted""" + if self.index_in_list is not None and isinstance( + self.plotter.plot_data, InteractiveList + ): + return self.plotter.plot_data[self.index_in_list] + else: + return self.plotter.plot_data + + @data.setter + def data(self, value): + self.set_data(value, self.index_in_list) + + @property + def iter_data(self): + """Returns an iterator over the plot data arrays""" + data = self.data + if isinstance(data, InteractiveList): + return iter(data) + return iter([data]) + + @property + def iter_raw_data(self): + """Returns an iterator over the original data arrays""" + data = self.raw_data + if isinstance(data, InteractiveList): + return iter(data) + return iter([data]) + + @property + def validate(self): + """Validation method of the formatoption""" + try: + return self._validate + except AttributeError: + try: + self._validate = self.plotter.get_vfunc(self.key) + except KeyError: + warn( + "Could not find a validation function for %s " + "formatoption keyword! No validation will be made!" + % (self.key), + PsyPlotRuntimeWarning, + logger=self.logger, + ) + self._validate = _identity + return self._validate + + @validate.setter + def validate(self, value): + self._validate = value + + @property + def default(self): + """Default value of this formatoption""" + return self.plotter.rc[self.key] + + @property + def default_key(self): + """The key of this formatoption in the :attr:`psyplot.rcParams`""" + return self.plotter.rc._get_val_and_base(self.key)[0] + + @property + def shared_by(self): + """None if the formatoption is not controlled by another formatoption + of another plotter, otherwise the corresponding :class:`Formatoption` + instance""" + return self.plotter._shared.get(self.key) + + @property + def value(self): + """Value of the formatoption in the corresponding :attr:`plotter` or + the shared value""" + shared_by = self.shared_by + if shared_by: + return shared_by.value2share + return self.plotter[self.key] + + @property + @dedent + def changed(self): + """ + :class:`bool` indicating whether the value changed compared to the + default or not.""" + return self.diff(self.default) + + @property + @dedent + def value2share(self): + """ + The value that is passed to shared formatoptions (by default, the + :attr:`value` attribute)""" + return self.value + + @property + @dedent + def value2pickle(self): + """ + The value that can be used when pickling the information of the project + """ + return self.value + + @docstrings.get_sections(base="Formatoption") + @dedent + def __init__( + self, + key, + plotter=None, + index_in_list=None, + additional_children=[], + additional_dependencies=[], + **kwargs, + ): + """ + Parameters + ---------- + key: str + formatoption key in the `plotter` + plotter: psyplot.plotter.Plotter + Plotter instance that holds this formatoption. If None, it is + assumed that this instance serves as a descriptor. + index_in_list: int or None + The index that shall be used if the data is a + :class:`psyplot.InteractiveList` + additional_children: list or str + Additional children to use (see the :attr:`children` attribute) + additional_dependencies: list or str + Additional dependencies to use (see the :attr:`dependencies` + attribute) + ``**kwargs`` + Further keywords may be used to specify different names for + children, dependencies and connection formatoptions that match the + setup of the plotter. Hence, keywords may be anything of the + :attr:`children`, :attr:`dependencies` and :attr:`connections` + attributes, with values being the name of the new formatoption in + this plotter.""" + self.key = key + self.plotter = plotter + self.index_in_list = index_in_list + self.shared = set() + self.additional_children = additional_children + self.additional_dependencies = additional_dependencies + self.children = self.children + additional_children + self.dependencies = self.dependencies + additional_dependencies + self._child_mapping = dict( + zip( + *tee( + chain( + self.children, + self.dependencies, + self.connections, + self.parents, + ), + 2, + ) + ) + ) + # check kwargs + for key in (key for key in kwargs if key not in self._child_mapping): + raise TypeError( + "%s.__init__() got an unexpected keyword argument %r" + % (self.__class__.__name__, key) + ) + # set up child mapping + self._child_mapping.update(kwargs) + # reset the dependency lists to match the current plotter setup + for attr in ["children", "dependencies", "connections", "parents"]: + setattr( + self, + attr, + [self._child_mapping[key] for key in getattr(self, attr)], + ) + + def __set__(self, instance, value): + if isinstance(value, Formatoption): + setattr(instance, "_" + self.key, value) + else: + fmto = getattr(instance, self.key) + fmto.set_value(value) + + def __get__(self, instance, owner): + if instance is None: + return self + try: + return getattr(instance, "_" + self.key) + except AttributeError: + fmto = self.__class__( + self.key, + instance, + self.index_in_list, + additional_children=self.additional_children, + additional_dependencies=self.additional_dependencies, + **self.init_kwargs, + ) + setattr(instance, "_" + self.key, fmto) + return fmto + + def __delete__(self, instance, owner): + fmto = getattr(instance, "_" + self.key) + with instance.no_validation: + instance[self.key] = fmto.default + +
+[docs] + @docstrings.get_sections(base="Formatoption.set_value") + @dedent + def set_value(self, value, validate=True, todefault=False): + """ + Set (and validate) the value in the plotter. This method is called by + the plotter when it attempts to change the value of the formatoption. + + Parameters + ---------- + value + Value to set + validate: bool + if True, validate the `value` before it is set + todefault: bool + True if the value is updated to the default value""" + # do nothing if the key is shared + if self.key in self.plotter._shared: + return + with self.plotter.no_validation: + self.plotter[self.key] = ( + value if not validate else self.validate(value) + )
+ + +
+[docs] + def set_data(self, data, i=None): + """ + Replace the data to plot + + This method may be used to replace the data that is visualized by the + plotter. It changes it's behaviour depending on whether an + :class:`psyplot.data.InteractiveList` is visualized or a single + :class:`pysplot.data.InteractiveArray` + + Parameters + ---------- + data: psyplot.data.InteractiveBase + The data to insert + i: int + The position in the InteractiveList where to insert the data (if + the plotter visualizes a list anyway) + + Notes + ----- + This method uses the :attr:`Formatoption.data` attribute + """ + if self.index_in_list is not None: + i = self.index_in_list + if i is not None and isinstance( + self.plotter.plot_data, InteractiveList + ): + self.plotter.plot_data[i] = data + else: + self.plotter.plot_data = data
+ + +
+[docs] + def set_decoder(self, decoder, i=None): + """ + Replace the data to plot + + This method may be used to replace the data that is visualized by the + plotter. It changes it's behaviour depending on whether an + :class:`psyplot.data.InteractiveList` is visualized or a single + :class:`pysplot.data.InteractiveArray` + + Parameters + ---------- + decoder: psyplot.data.CFDecoder + The decoder to insert + i: int + The position in the InteractiveList where to insert the data (if + the plotter visualizes a list anyway) + """ + # we do not modify the raw data but instead set it on the plotter + # TODO: This is not safe for encapsulated InteractiveList instances! + if i is not None and isinstance( + self.plotter.plot_data, InteractiveList + ): + n = len(self.plotter.plot_data) + decoders = self.plotter.plot_data_decoder or [None] * n + decoders[i] = decoder + self.plotter.plot_data_decoder = decoders + else: + if isinstance( + self.plotter.plot_data, InteractiveList + ) and isinstance(decoder, CFDecoder): + decoder = [decoder] * len(self.plotter.plot_data) + self.plotter.plot_data_decoder = decoder
+ + +
+[docs] + def get_decoder(self, i=None): + # we do not modify the raw data but instead set it on the plotter + # TODO: This is not safe for encapsulated InteractiveList instances! + if i is not None and isinstance( + self.plotter.plot_data, InteractiveList + ): + n = len(self.plotter.plot_data) + decoders = self.plotter.plot_data_decoder or [None] * n + return decoders[i] or self.plotter.plot_data[i].psy.decoder + else: + return self.decoder
+ + +
+[docs] + def check_and_set(self, value, todefault=False, validate=True): + """Checks the value and sets the value if it changed + + This method checks the value and sets it only if the :meth:`diff` + method result of the given `value` is True + + Parameters + ---------- + value + A possible value to set + todefault: bool + True if the value is updated to the default value + + Returns + ------- + bool + A boolean to indicate whether it has been set or not""" + if validate: + value = self.validate(value) + if self.diff(value): + self.set_value(value, validate=False, todefault=todefault) + return True + return False
+ + +
+[docs] + def diff(self, value): + """Checks whether the given value differs from what is currently set + + Parameters + ---------- + value + A possible value to set (make sure that it has been validate via + the :attr:`validate` attribute before) + + Returns + ------- + bool + True if the value differs from what is currently set""" + return value != self.value
+ + +
+[docs] + def initialize_plot(self, value, *args, **kwargs): + """Method that is called when the plot is made the first time + + Parameters + ---------- + value + The value to use for the initialization""" + self.update(value, *args, **kwargs)
+ + +
+[docs] + @abstractmethod + def update(self, value): + """Method that is call to update the formatoption on the axes + + Parameters + ---------- + value + Value to update""" + pass
+ + +
+[docs] + def get_fmt_widget(self, parent, project): + """Get a widget to update the formatoption in the GUI + + This method should return a QWidget that is loaded by the psyplot-gui + when the formatoption is selected in the + :attr:`psyplot_gui.main.Mainwindow.fmt_widget`. It should call the + :meth:`~psyplot_gui.fmt_widget.FormatoptionWidget.insert_text` method + when the update text for the formatoption should be changed. + + Parameters + ---------- + parent: psyplot_gui.fmt_widget.FormatoptionWidget + The parent widget that contains the returned QWidget + project: psyplot.project.Project + The current subproject (see :func:`psyplot.project.gcp`) + + Returns + ------- + PyQt5.QtWidgets.QWidget + The widget to control the formatoption""" + return None
+ + +
+[docs] + def share(self, fmto, initializing=False, **kwargs): + """Share the settings of this formatoption with other data objects + + Parameters + ---------- + fmto: Formatoption + The :class:`Formatoption` instance to share the attributes with + ``**kwargs`` + Any other keyword argument that shall be passed to the update + method of `fmto`""" + # lock all the childrens and the formatoption itself + self.lock.acquire() + fmto._lock_children() + fmto.lock.acquire() + # update the other plotter + if initializing: + fmto.initialize_plot(self.value2share, **kwargs) + else: + fmto.update(self.value2share, **kwargs) + self.shared.add(fmto) + # release the locks + fmto.lock.release() + fmto._release_children() + self.lock.release()
+ + + def _lock_children(self): + """acquire the locks of the children""" + plotter = self.plotter + for key in self.children + self.dependencies: + try: + getattr(plotter, key).lock.acquire() + except AttributeError: + pass + + def _release_children(self): + """release the locks of the children""" + plotter = self.plotter + for key in self.children + self.dependencies: + try: + getattr(plotter, key).lock.release() + except AttributeError: + pass + +
+[docs] + def finish_update(self): + """Finish the update, initialization and sharing process + + This function is called at the end of the :meth:`Plotter.start_update`, + :meth:`Plotter.initialize_plot` or the :meth:`Plotter.share` methods. + """ + pass
+ + +
+[docs] + @dedent + def remove(self): + """ + Method to remove the effects of this formatoption + + This method is called when the axes is cleared due to a + formatoption with :attr:`requires_clearing` set to True. You don't + necessarily have to implement this formatoption if your plot results + are removed by the usual :meth:`matplotlib.axes.Axes.clear` method.""" + pass
+ + +
+[docs] + @docstrings.get_extended_summary(base="Formatoption.convert_coordinate") + @docstrings.get_sections( + base="Formatoption.convert_coordinate", + sections=["Parameters", "Returns"], + ) + def convert_coordinate(self, coord, *variables): + """Convert a coordinate to units necessary for the plot. + + This method takes a single coordinate variable (e.g. the `bounds` of a + coordinate, or the coordinate itself) and transforms the units that the + plotter requires. + + One might also provide additional `variables` that are supposed to be + on the same unit, in case the given `coord` does not specify a `units` + attribute. `coord` might be a CF-conform `bounds` variable, and one of + the variables might be the corresponding `coordinate`. + + Parameters + ---------- + coord: xr.Variable + The variable to transform + ``*variables`` + The variables that are on the same unit as `coord` + + Returns + ------- + xr.Variable + The transformed `coord` + + Notes + ----- + By default, this method uses the :meth:`~Plotter.convert_coordinate` + method of the :attr:`plotter`. + """ + return self.plotter.convert_coordinate(coord, *variables)
+
+ + + +
+[docs] +class DictFormatoption(Formatoption): + """ + Base formatoption class defining an alternative set_value that works for + dictionaries.""" + +
+[docs] + @docstrings.dedent + def set_value(self, value, validate=True, todefault=False): + """ + Set (and validate) the value in the plotter + + Parameters + ---------- + %(Formatoption.set_value.parameters)s + + Notes + ----- + - If the current value in the plotter is None, then it will be set with + the given `value`, otherwise the current value in the plotter is + updated + - If the value is an empty dictionary, the value in the plotter is + cleared""" + value = value if not validate else self.validate(value) + # if the key in the plotter is not already set (i.e. it is initialized + # with None, we set it) + if self.plotter[self.key] is None: + with self.plotter.no_validation: + self.plotter[self.key] = value.copy() + # in case of an empty dict, clear the value + elif not value: + self.plotter[self.key].clear() + # otherwhise we update the dictionary + else: + if todefault: + self.plotter[self.key].clear() + self.plotter[self.key].update(value)
+
+ + + +
+[docs] +class PostTiming(Formatoption): + """ + Determine when to run the :attr:`post` formatoption + + This formatoption determines, whether the :attr:`post` formatoption + should be run never, after replot or after every update. + + Possible types + -------------- + 'never' + Never run post processing scripts + 'always' + Always run post processing scripts + 'replot' + Only run post processing scripts when the data changes or a replot + is necessary + + See Also + -------- + post: The post processing formatoption""" + + default = "never" + + priority = -inf + + group = "post_processing" + + name = "Timing of the post processing" + +
+[docs] + @staticmethod + def validate(value): + value = six.text_type(value) + possible_values = ["never", "always", "replot"] + if value not in possible_values: + raise ValueError( + "String must be one of %s, not %r" % (possible_values, value) + ) + return value
+ + +
+[docs] + def update(self, value): + pass
+ + +
+[docs] + def get_fmt_widget(self, parent, project): + from psyplot_gui.compat.qtcompat import QComboBox + + combo = QComboBox(parent) + combo.addItems(["never", "always", "replot"]) + combo.setCurrentText( + next((plotter[self.key] for plotter in project.plotters), "never") + ) + combo.currentTextChanged.connect(parent.set_obj) + return combo
+
+ + + +
+[docs] +class PostProcDependencies(object): + """The dependencies of this formatoption""" + + def __get__(self, instance, owner): + if ( + instance is None + or instance.plotter is None + or not instance.plotter._initialized + ): + return [] + elif instance.post_timing.value == "always": + return list(set(instance.plotter) - {instance.key}) + else: + return [] + + def __set__(self, instance, value): + pass
+ + + +
+[docs] +class PostProcessing(Formatoption): + """ + Apply your own postprocessing script + + This formatoption let's you apply your own post processing script. Just + enter the script as a string and it will be executed. The formatoption + will be made available via the ``self`` variable + + Possible types + -------------- + None + Don't do anything + str + The post processing script as string + + Note + ---- + This formatoption uses the built-in :func:`exec` function to compile the + script. Since this poses a security risk when loading psyplot projects, + it is by default disabled through the :attr:`Plotter.enable_post` + attribute. If you are sure that you can trust the script in this + formatoption, set this attribute of the corresponding :class:`Plotter` to + ``True`` + + Examples + -------- + Assume, you want to manually add the mean of the data to the title of the + matplotlib axes. You can simply do this via + + .. code-block:: python + + from psyplot.plotter import Plotter + from xarray import DataArray + + plotter = Plotter(DataArray([1, 2, 3])) + # enable the post formatoption + plotter.enable_post = True + plotter.update(post="self.ax.set_title(str(self.data.mean()))") + plotter.ax.get_title() + "2.0" + + By default, the ``post`` formatoption is only ran, when it is explicitly + updated. However, you can use the :attr:`post_timing` formatoption, to + run it automatically. E.g. for running it after every update of the + plotter, you can set + + .. code-block:: python + + plotter.update(post_timing="always") + + See Also + -------- + post_timing: Determine the timing of this formatoption""" + + children = ["post_timing"] + + default = None + + priority = -inf + + group = "post_processing" + + name = "Custom post processing script" + +
+[docs] + @staticmethod + def validate(value): + if value is None: + return value + elif not isinstance(value, six.string_types): + raise ValueError("Expected a string, not %s" % (type(value),)) + else: + return six.text_type(value)
+ + + @property + def data_dependent(self): + """True if the corresponding :class:`post_timing <PostTiming>` + formatoption is set to ``'replot'`` to run the post processing script + after every change of the data""" + return self.post_timing.value == "replot" + + dependencies = PostProcDependencies() + +
+[docs] + def update(self, value): + if value is None: + return + if not self.plotter.enable_post: + warn( + "Post processing is disabled. Set the ``enable_post`` " + "attribute to True to run the script" + ) + else: + exec(value, {"self": self})
+
+ + + +
+[docs] +class Plotter(dict): + """Interactive plotting object for one or more data arrays + + This class is the base for the interactive plotting with the psyplot + module. It capabilities are determined by it's descriptor classes that are + derived from the :class:`Formatoption` class""" + + #: List of base strings in the :attr:`psyplot.rcParams` dictionary + _rcparams_string = [] + + post_timing = PostTiming("post_timing") + post = PostProcessing("post") + + no_validation = _temp_bool_prop( + "no_validation", + """ + Temporarily disable the validation + + Examples + -------- + Although it is not recommended to set a value with disabled validation, + you can disable it via:: + + >>> with plotter.no_validation: + ... plotter["ticksize"] = "x" + ... + + To permanently disable the validation, simply set + + >>> plotter.no_validation = True + >>> plotter["ticksize"] = "x" + >>> plotter.no_validation = False # reenable validation""", + ) + + #: Temporarily include links in the key descriptions from + #: :meth:`show_keys`, :meth:`show_docs` and :meth:`show_summaries`. + #: Note that this is a class attribute, so each change to the value of this + #: attribute will affect all instances and subclasses + include_links = _TempBool() + + @property + def ax(self): + """Axes instance of the plot""" + if self._ax is None: + import matplotlib.pyplot as plt + + plt.figure() + self._ax = plt.axes(projection=self._get_sample_projection()) + return self._ax + + @ax.setter + def ax(self, value): + self._ax = value + + #: The :class:`psyplot.project.Project` instance this plotter belongs to + _project = None + + @property + def project(self): + """:class:`psyplot.project.Project` instance this plotter belongs to""" + if self._project is None: + return + return self._project() + + @project.setter + def project(self, value): + if value is not None: + self._project = weakref.ref(value) + else: + self._project = value + + @property + @dedent + def rc(self): + """ + Default values for this plotter + + This :class:`~psyplot.config.rcsetup.SubDict` stores the default values + for this plotter. A modification of the dictionary does not affect + other plotter instances unless you set the + :attr:`~psyplot.config.rcsetup.SubDict.trace` attribute to True""" + try: + return self._rc + except AttributeError: + self._set_rc() + return self._rc + + @property + def base_variables(self): + """A mapping from the base_variable names to the variables""" + if isinstance(self.data, InteractiveList): + return dict( + chain( + *map( + lambda arr: six.iteritems(arr.psy.base_variables), + self.data, + ) + ) + ) + else: + return self.data.psy.base_variables + + @property + def iter_base_variables(self): + """A mapping from the base_variable names to the variables""" + if isinstance(self.data, InteractiveList): + return chain(*(arr.psy.iter_base_variables for arr in self.data)) + else: + return self.data.psy.iter_base_variables + + no_auto_update = property( + _no_auto_update_getter, doc=_no_auto_update_getter.__doc__ + ) + + @no_auto_update.setter + def no_auto_update(self, value): + self.no_auto_update.value = bool(value) + + @property + def changed(self): + """:class:`dict` containing the key value pairs that are not the + default""" + return { + key: value + for key, value in six.iteritems(self) + if getattr(self, key).changed + } + + @property + def figs2draw(self): + """All figures that have been manipulated through sharing and the own + figure. + + Notes + ----- + Using this property set will reset the figures too draw""" + return self._figs2draw.union([self.ax.get_figure()]) + + @property + @docstrings + def _njobs(self): + """%(InteractiveBase._njobs)s""" + if self.disabled: + return [0] + return [1, 1] + + @property + def _fmtos(self): + """Iterator over the formatoptions""" + return (getattr(self, key) for key in self) + + @property + def _fmto_groups(self): + """Mapping from group to a set of formatoptions""" + ret = defaultdict(set) + for key in self: + ret[getattr(self, key).group].add(getattr(self, key)) + return dict(ret) + + @property + def fmt_groups(self): + """A mapping from the formatoption group to the formatoptions""" + ret = defaultdict(set) + for key in self: + ret[getattr(self, key).group].add(key) + return dict(ret) + + @property + def groups(self): + """A mapping from the group short name to the group description""" + return {group: groups[group] for group in self.fmt_groups} + + @property + def data(self): + """The :class:`psyplot.InteractiveBase` instance of this plotter""" + return self._data + + @data.setter + def data(self, value): + self._data = value + + @property + def plot_data(self): + """The data that is used for plotting""" + return getattr(self, "_plot_data", self.data) + + @plot_data.setter + def plot_data(self, value): + self._set_data(value) + + #: The decoder to use for the formatoptions. If None, the decoder of the + #: raw data is used + plot_data_decoder = None + + #: :class:`bool` that has to be ``True`` if the post processing script in + #: the :attr:`post` formatoption should be enabled + enable_post = False + + def _set_data(self, value): + if isinstance(value, InteractiveList): + self._plot_data = value.copy() + else: + self._plot_data = value + + @property + def logger(self): + """:class:`logging.Logger` of this plotter""" + try: + return self.data.psy.logger.getChild(self.__class__.__name__) + except AttributeError: + name = "%s.%s" % (self.__module__, self.__class__.__name__) + return logging.getLogger(name) + + docstrings.keep_params("InteractiveBase.parameters", "auto_update") + + @docstrings.get_sections(base="Plotter") + @docstrings.dedent + def __init__( + self, + data=None, + ax=None, + auto_update=None, + project=None, + draw=False, + make_plot=True, + clear=False, + enable_post=False, + **kwargs, + ): + """ + Parameters + ---------- + data: InteractiveArray or ArrayList, optional + Data object that shall be visualized. If given and `plot` is True, + the :meth:`initialize_plot` method is called at the end. Otherwise + you can call this method later by yourself + ax: matplotlib.axes.Axes + Matplotlib Axes to plot on. If None, a new one will be created as + soon as the :meth:`initialize_plot` method is called + %(InteractiveBase.parameters.auto_update)s + %(InteractiveBase.start_update.parameters.draw)s + make_plot: bool + If True, and `data` is not None, the plot is initialized. Otherwise + only the framework between plotter and data is set up + clear: bool + If True, the axes is cleared first + enable_post: bool + If True, the :attr:`post` formatoption is enabled and post + processing scripts are allowed + ``**kwargs`` + Any formatoption key from the :attr:`formatoptions` attribute that + shall be used""" + self.project = project + self.ax = ax + self.data = data + self.enable_post = enable_post + if auto_update is None: + auto_update = rcParams["lists.auto_update"] + self.no_auto_update = not bool(auto_update) + self._registered_updates = {} + self._todefault = False + self._old_fmt = [] + self._figs2draw = set() + #: formatoptions that have to be updated by other plotters that share + #: the given formatoption with this Plotter. :attr:`_to_update` is a + #: mapping from the formatoptions in this plotter to the corresponding + #: other plotter + self._to_update = {} + self.disabled = False + #: Dictionary holding the Formatoption instances of other plotters + #: if their value shall be used instead of the one in this instance + self._shared = {} + #: list of str. Formatoption keys that were changed during the last + #: update + self._last_update = [] + #: The set of formatoptions that shall be updated even if they did not + #: change + self._force = set() + self.replot = True + self.cleared = clear + self._updating = False + # will be set to True when the plot is first initialized + self._initialized = False + + # first we initialize all keys with None. This is necessary in order + # to make the validation functioning + with self.no_validation: + for key in self._get_formatoptions(): + self[key] = None + for key in self: # then we set the default values + fmto = getattr(self, key) + self._try2set(fmto, fmto.default, validate=False) + self._set_rc() + for key, value in six.iteritems(kwargs): # then the user values + self[key] = value + self.initialize_plot( + data, ax=ax, draw=draw, clear=clear, make_plot=make_plot + ) + + def _try2set(self, fmto, *args, **kwargs): + """Sets the value in `fmto` and gives additional informations when fail + + Parameters + ---------- + fmto: Formatoption + ``*args`` and ``**kwargs`` + Anything that is passed to `fmto`s :meth:`~Formatoption.set_value` + method""" + fmto.set_value(*args, **kwargs) + + def __getitem__(self, key): + try: + return dict.__getitem__(self, key) + except KeyError: + self.check_key(key) + + def __setitem__(self, key, value): + if not self.no_validation: + self.check_key(key) + self._try2set(getattr(self, key), value) + return + # prevent from setting during an update process + getattr(self, key).lock.acquire() + dict.__setitem__(self, key, value) + getattr(self, key).lock.release() + + def __delitem__(self, key): + self[key] = getattr(self, key).default + + docstrings.delete_params("check_key.parameters", "possible_keys", "name") + +
+[docs] + @docstrings.dedent + def check_key(self, key, raise_error=True, *args, **kwargs): + """ + Checks whether the key is a valid formatoption + + Parameters + ---------- + %(check_key.parameters.no_possible_keys|name)s + + Returns + ------- + %(check_key.returns)s + + Raises + ------ + %(check_key.raises)s""" + return check_key( + key, + possible_keys=list(self), + raise_error=raise_error, + name="formatoption keyword", + *args, + **kwargs, + )
+ + +
+[docs] + @classmethod + @docstrings.get_sections( + base="Plotter.check_data", sections=["Parameters", "Returns"] + ) + @dedent + def check_data(cls, name, dims, is_unstructured): + """ + A validation method for the data shape + + The default method does nothing and should be subclassed to validate + the results. If the plotter accepts a :class:`InteractiveList`, it + should accept a list for name and dims + + Parameters + ---------- + name: str or list of str + The variable name(s) of the data + dims: list of str or list of lists of str + The dimension name(s) of the data + is_unstructured: bool or list of bool + True if the corresponding array is unstructured + + Returns + ------- + list of bool or None + True, if everything is okay, False in case of a serious error, + None if it is intermediate. Each object in this list corresponds to + one in the given `name` + list of str + The message giving more information on the reason. Each object in + this list corresponds to one in the given `name`""" + if isinstance(name, six.string_types): + name = [name] + dims = [dims] + is_unstructured = [is_unstructured] + N = len(name) + if len(dims) != N or len(is_unstructured) != N: + return [False] * N, [ + "Number of provided names (%i) and dimensions " + "(%i) or unstructured information (%i) are not the same" + % (N, len(dims), len(is_unstructured)) + ] * N + return [True] * N, [""] * N
+ + + docstrings.keep_params("Plotter.parameters", "ax", "make_plot", "clear") + +
+[docs] + @docstrings.dedent + def initialize_plot( + self, + data=None, + ax=None, + make_plot=True, + clear=False, + draw=False, + remove=False, + priority=None, + ): + """ + Initialize the plot for a data array + + Parameters + ---------- + data: InteractiveArray or ArrayList, optional + Data object that shall be visualized. + + - If not None and `plot` is True, the given data is visualized. + - If None and the :attr:`data` attribute is not None, the data in + the :attr:`data` attribute is visualized + - If both are None, nothing is done. + %(Plotter.parameters.ax|make_plot|clear)s + %(InteractiveBase.start_update.parameters.draw)s + remove: bool + If True, old effects by the formatoptions in this plotter are + undone first + priority: int + If given, initialize only the formatoption with the given priority. + This value must be out of :data:`START`, :data:`BEFOREPLOTTING` or + :data:`END` + """ + if data is None and self.data is not None: + data = self.data + else: + self.data = data + self.ax = ax + if data is None: # nothing to do if no data is given + return + self.no_auto_update = not ( + not self.no_auto_update or not data.psy.no_auto_update + ) + data.psy.plotter = self + if not make_plot: # stop here if we shall not plot + return + self.logger.debug("Initializing plot...") + if remove: + self.logger.debug(" Removing old formatoptions...") + for fmto in self._fmtos: + try: + fmto.remove() + except Exception: + self.logger.debug( + "Could not remove %s while initializing", + fmto.key, + exc_info=True, + ) + if clear: + self.logger.debug(" Clearing axes...") + self.ax.clear() + self.cleared = True + # get the formatoptions. We sort them here by key to make sure that the + # order always stays the same (easier for debugging) + fmto_groups = self._grouped_fmtos( + self._sorted_by_priority( + sorted(self._fmtos, key=lambda fmto: fmto.key) + ) + ) + self.plot_data = self.data + self._updating = True + for fmto_priority, grouper in fmto_groups: + if priority is None or fmto_priority == priority: + self._plot_by_priority( + fmto_priority, grouper, initializing=True + ) + self._release_all(True) # finish the update + self.cleared = False + self.replot = False + self._initialized = True + self._updating = False + + if draw is None: + draw = rcParams["auto_draw"] + if draw: + self.draw() + if rcParams["auto_show"]: + self.show()
+ + + docstrings.keep_params( + "InteractiveBase._register_update.parameters", "force", "todefault" + ) + + @docstrings.get_sections(base="Plotter._register_update") + @docstrings.dedent + def _register_update( + self, fmt={}, replot=False, force=False, todefault=False + ): + """ + Register formatoptions for the update + + Parameters + ---------- + fmt: dict + Keys can be any valid formatoptions with the corresponding values + (see the :attr:`formatoptions` attribute) + replot: bool + Boolean that determines whether the data specific formatoptions + shall be updated in any case or not. + %(InteractiveBase._register_update.parameters.force|todefault)s""" + if self.disabled: + return + self.replot = self.replot or replot + self._todefault = self._todefault or todefault + if force is True: + force = list(fmt) + self._force.update( + [ret[0] for ret in map(self.check_key, force or [])] + ) + # check the keys + list(map(self.check_key, fmt)) + self._registered_updates.update(fmt) + +
+[docs] + def make_plot(self): + """Method for making the plot + + This method is called at the end of the :attr:`BEFOREPLOTTING` stage if + and only if the :attr:`plot_fmt` attribute is set to ``True``""" + pass
+ + +
+[docs] + @docstrings.dedent + def start_update(self, draw=None, queues=None, update_shared=True): + """ + Conduct the registered plot updates + + This method starts the updates from what has been registered by the + :meth:`update` method. You can call this method if you did not set the + `auto_update` parameter to True when calling the :meth:`update` method + and when the :attr:`no_auto_update` attribute is True. + + Parameters + ---------- + %(InteractiveBase.start_update.parameters)s + + Returns + ------- + %(InteractiveBase.start_update.returns)s + + See Also + -------- + :attr:`no_auto_update`, update""" + + def update_the_others(): + for fmto in fmtos: + for other_fmto in fmto.shared: + if not other_fmto.plotter._updating: + other_fmto.plotter._register_update( + force=[other_fmto.key] + ) + for fmto in fmtos: + for other_fmto in fmto.shared: + if not other_fmto.plotter._updating: + other_draw = other_fmto.plotter.start_update( + draw=False, update_shared=False + ) + if other_draw: + self._figs2draw.add( + other_fmto.plotter.ax.get_figure() + ) + + if self.disabled: + return False + + if queues is not None: + queues[0].get() + self.logger.debug( + "Starting update of %r", self._registered_updates.keys() + ) + # update the formatoptions + self._save_state() + try: + # get the formatoptions. We sort them here by key to make sure that + # the order always stays the same (easier for debugging) + fmtos = sorted(self._set_and_filter(), key=lambda fmto: fmto.key) + except Exception: + # restore last (working) state + last_state = self._old_fmt.pop(-1) + with self.no_validation: + for key in self: + self[key] = last_state.get(key, getattr(self, key).default) + if queues is not None: + queues[0].task_done() + self._release_all(queue=None if queues is None else queues[1]) + # raise the error + raise + for fmto in fmtos: + for fmto2 in fmto.shared: + fmto2.plotter._to_update[fmto2] = self + if queues is not None: + self._updating = True + queues[0].task_done() + # wait for the other tasks to finish + queues[0].join() + queues[1].get() + fmtos.extend( + [ + fmto + for fmto in self._insert_additionals(list(self._to_update)) + if fmto not in fmtos + ] + ) + self._to_update.clear() + + fmto_groups = self._grouped_fmtos(self._sorted_by_priority(fmtos[:])) + # if any formatoption requires a clearing of the axes is updated, + # we reinitialize the plot + try: + if self.cleared: + self.reinit(draw=draw) + update_the_others() + arr_draw = True + else: + # otherwise we update it + arr_draw = False + for priority, grouper in fmto_groups: + arr_draw = True + self._plot_by_priority(priority, grouper) + update_the_others() + except Exception: + raise + finally: + # make sure that all locks are released + self._release_all( + finish=True, queue=None if queues is None else queues[1] + ) + if draw is None: + draw = rcParams["auto_draw"] + if draw and arr_draw: + self.draw() + if rcParams["auto_show"]: + self.show() + self.replot = False + return arr_draw
+ + + def _release_all(self, finish=False, queue=None): + # make sure that all locks are released + try: + for fmto in self._fmtos: + if finish: + fmto.finish_update() + try: + fmto.lock.release() + except RuntimeError: + pass + except Exception: + raise + finally: + if queue is not None: + queue.task_done() + queue.join() + self._updating = False + + def _plot_by_priority(self, priority, fmtos, initializing=False): + def update(fmto): + other_fmto = self._shared.get(fmto.key) + if other_fmto: + self.logger.debug( + "%s is shared with %s", + fmto.key, + other_fmto.plotter.logger.name, + ) + other_fmto.share(fmto, initializing=initializing) + # but if not, share them + else: + if initializing: + self.logger.debug("Initializing %s", fmto.key) + fmto.initialize_plot(fmto.value) + else: + self.logger.debug("Updating %s", fmto.key) + fmto.update(fmto.value) + try: + fmto.lock.release() + except RuntimeError: + pass + + self._initializing = initializing + + self.logger.debug( + "%s formatoptions with priority %i", + "Initializing" if initializing else "Updating", + priority, + ) + + if priority >= START or priority == END: + for fmto in fmtos: + update(fmto) + elif priority == BEFOREPLOTTING: + for fmto in fmtos: + update(fmto) + self._make_plot() + + self._initializing = False + +
+[docs] + @docstrings.dedent + def reinit(self, draw=None, clear=False): + """ + Reinitializes the plot with the same data and on the same axes. + + Parameters + ---------- + %(InteractiveBase.start_update.parameters.draw)s + clear: bool + Whether to clear the axes or not + + Warnings + -------- + The axes may be cleared when calling this method (even if `clear` is + set to False)!""" + # call the initialize_plot method. Note that clear can be set to + # False if any fmto has requires_clearing attribute set to True, + # because this then has been cleared before + self.initialize_plot( + self.data, + self._ax, + draw=draw, + clear=clear or any(fmto.requires_clearing for fmto in self._fmtos), + remove=True, + )
+ + +
+[docs] + def draw(self): + """Draw the figures and those that are shared and have been changed""" + for fig in self.figs2draw: + fig.canvas.draw() + self._figs2draw.clear()
+ + + def _grouped_fmtos(self, fmtos): + def key_func(fmto): + if fmto.priority >= START: + return START + elif fmto.priority >= BEFOREPLOTTING: + return BEFOREPLOTTING + else: + return END + + return groupby(fmtos, key_func) + + def _set_and_filter(self): + """Filters the registered updates and sort out what is not needed + + This method filters out the formatoptions that have not changed, sets + the new value and returns an iterable that is sorted by the priority + (highest priority comes first) and dependencies + + Returns + ------- + list + list of :class:`Formatoption` objects that have to be updated""" + fmtos = [] + seen = set() + for key in self._force: + self._registered_updates.setdefault(key, getattr(self, key).value) + for key, value in chain( + six.iteritems(self._registered_updates), + six.iteritems({key: getattr(self, key).default for key in self}) + if self._todefault + else (), + ): + if key in seen: + continue + seen.add(key) + fmto = getattr(self, key) + # if the key is shared, a warning will be printed as long as + # this plotter is not also updating (for example due to a whole + # project update) + if key in self._shared and key not in self._force: + if not self._shared[key].plotter._updating: + warn( + ( + "%s formatoption is shared with another plotter." + " Use the unshare method to enable the updating" + ) + % (fmto.key), + logger=self.logger, + ) + changed = False + else: + try: + changed = fmto.check_and_set( + value, + todefault=self._todefault, + validate=not self.no_validation, + ) + except Exception as e: + self._registered_updates.pop(key, None) + self.logger.debug("Failed to set %s", key) + raise e + changed = changed or key in self._force + if changed: + fmtos.append(fmto) + fmtos = self._insert_additionals(fmtos, seen) + for fmto in fmtos: + fmto.lock.acquire() + self._todefault = False + self._registered_updates.clear() + self._force.clear() + return fmtos + + def _insert_additionals(self, fmtos, seen=None): + """ + Insert additional formatoptions into `fmtos`. + + This method inserts those formatoptions into `fmtos` that are required + because one of the following criteria is fullfilled: + + 1. The :attr:`replot` attribute is True + 2. Any formatoption with START priority is in `fmtos` + 3. A dependency of one formatoption is in `fmtos` + + Parameters + ---------- + fmtos: list + The list of formatoptions that shall be updated + seen: set + The formatoption keys that shall not be included. If None, all + formatoptions in `fmtos` are used + + Returns + ------- + fmtos + The initial `fmtos` plus further formatoptions + + Notes + ----- + `fmtos` and `seen` are modified in place (except that any formatoption + in the initial `fmtos` has :attr:`~Formatoption.requires_clearing` + attribute set to True)""" + + def get_dependencies(fmto): + if fmto is None: + return [] + return fmto.dependencies + list( + chain( + *map( + lambda key: get_dependencies(getattr(self, key, None)), + fmto.dependencies, + ) + ) + ) + + seen = seen or {fmto.key for fmto in fmtos} + keys = {fmto.key for fmto in fmtos} + self.replot = self.replot or any( + fmto.requires_replot for fmto in fmtos + ) + if self.replot or any(fmto.priority >= START for fmto in fmtos): + self.replot = True + self.plot_data = self.data + new_fmtos = dict( + (f.key, f) + for f in self._fmtos + if ((f not in fmtos and is_data_dependent(f, self.data))) + ) + seen.update(new_fmtos) + keys.update(new_fmtos) + fmtos += list(new_fmtos.values()) + + # insert the formatoptions that have to be updated if the plot is + # changed + if any(fmto.priority >= BEFOREPLOTTING for fmto in fmtos): + new_fmtos = dict( + (f.key, f) + for f in self._fmtos + if ((f not in fmtos and f.update_after_plot)) + ) + fmtos += list(new_fmtos.values()) + for fmto in set(self._fmtos).difference(fmtos): + all_dependencies = get_dependencies(fmto) + if keys.intersection(all_dependencies): + fmtos.append(fmto) + if any(fmto.requires_clearing for fmto in fmtos): + self.cleared = True + return list(self._fmtos) + return fmtos + + def _sorted_by_priority(self, fmtos, changed=None): + """Sort the formatoption objects by their priority and dependency + + Parameters + ---------- + fmtos: list + list of :class:`Formatoption` instances + changed: list + the list of formatoption keys that have changed + + Yields + ------ + Formatoption + The next formatoption as it comes by the sorting + + Warnings + -------- + The list `fmtos` is cleared by this method!""" + + def pop_fmto(key): + idx = fmtos_keys.index(key) + del fmtos_keys[idx] + return fmtos.pop(idx) + + def get_children(fmto, parents_keys): + all_fmtos = fmtos_keys + parents_keys + for key in fmto.children + fmto.dependencies: + if key not in fmtos_keys: + continue + child_fmto = pop_fmto(key) + for childs_child in get_children( + child_fmto, parents_keys + [child_fmto.key] + ): + yield childs_child + # filter out if parent is in update list + if ( + any(key in all_fmtos for key in child_fmto.parents) + or fmto.key in child_fmto.parents + ): + continue + yield child_fmto + + fmtos.sort(key=lambda fmto: fmto.priority, reverse=True) + fmtos_keys = [fmto.key for fmto in fmtos] + self._last_update = changed or fmtos_keys[:] + self.logger.debug("Update the formatoptions %s", fmtos_keys) + while fmtos: + del fmtos_keys[0] + fmto = fmtos.pop(0) + # first update children + for child_fmto in get_children(fmto, [fmto.key]): + yield child_fmto + # filter out if parent is in update list + if any(key in fmtos_keys for key in fmto.parents): + continue + yield fmto + + @classmethod + def _get_formatoptions(cls, include_bases=True): + """ + Iterator over formatoptions + + This class method returns an iterator that contains all the + formatoption keys that are in this class and that are defined + in the base classes + + Notes + ----- + There is absolutely no need to call this method besides the plotter + initialization, since all formatoptions are in the plotter itself. + Just type:: + + >>> list(plotter) + + to get the formatoptions. + + See Also + -------- + _format_keys""" + + def base_fmtos(base): + return filter( + lambda key: isinstance(getattr(cls, key), Formatoption), + getattr(base, "_get_formatoptions", empty)(False), + ) + + def empty(*args, **kwargs): + return list() + + fmtos = ( + attr + for attr, obj in six.iteritems(cls.__dict__) + if isinstance(obj, Formatoption) + ) + if not include_bases: + return fmtos + return unique_everseen(chain(fmtos, *map(base_fmtos, cls.__mro__))) + + docstrings.keep_types( + "check_key.parameters", "kwargs", r"``\*args,\*\*kwargs``" + ) + + @classmethod + @docstrings.get_sections(base="Plotter._enhance_keys") + @docstrings.dedent + def _enhance_keys(cls, keys=None, *args, **kwargs): + """ + Enhance the given keys by groups + + Parameters + ---------- + keys: list of str or None + If None, the all formatoptions of the given class are used. Group + names from the :attr:`psyplot.plotter.groups` mapping are replaced + by the formatoptions + + Other Parameters + ---------------- + %(check_key.parameters.kwargs)s + + Returns + ------- + list of str + The enhanced list of the formatoptions""" + all_keys = list(cls._get_formatoptions()) + if isinstance(keys, six.string_types): + keys = [keys] + else: + keys = list(keys or sorted(all_keys)) + fmto_groups = defaultdict(list) + for key in all_keys: + fmto_groups[getattr(cls, key).group].append(key) + new_i = 0 + for i, key in enumerate(keys[:]): + if key in fmto_groups: + del keys[new_i] + for key2 in fmto_groups[key]: + if key2 not in keys: + keys.insert(new_i, key2) + new_i += 1 + else: + valid, similar, message = check_key( + key, + all_keys, + False, + "formatoption keyword", + *args, + **kwargs, + ) + if not valid: + keys.remove(key) + new_i -= 1 + warn(message) + new_i += 1 + return keys + +
+[docs] + @classmethod + @docstrings.get_sections( + base="Plotter.show_keys", + sections=["Parameters", "Returns", "Other Parameters"], + ) + @docstrings.dedent + def show_keys( + cls, + keys=None, + indent=0, + grouped=False, + func=None, + include_links=False, + *args, + **kwargs, + ): + """ + Classmethod to return a nice looking table with the given formatoptions + + Parameters + ---------- + %(Plotter._enhance_keys.parameters)s + indent: int + The indentation of the table + grouped: bool, optional + If True, the formatoptions are grouped corresponding to the + :attr:`Formatoption.groupname` attribute + + Other Parameters + ---------------- + func: function or None + The function the is used for returning (by default it is printed + via the :func:`print` function or (when using the gui) in the + help explorer). The given function must take a string as argument + include_links: bool or None, optional + Default False. If True, links (in restructured formats) are + included in the description. If None, the behaviour is determined + by the :attr:`psyplot.plotter.Plotter.include_links` attribute. + %(Plotter._enhance_keys.other_parameters)s + + Returns + ------- + results of `func` + None if `func` is the print function, otherwise anything else + + See Also + -------- + show_summaries, show_docs""" + + def titled_group(groupname): + bars = str_indent + "*" * len(groupname) + "\n" + return bars + str_indent + groupname + "\n" + bars + + keys = cls._enhance_keys(keys, *args, **kwargs) + str_indent = " " * indent + func = func or default_print_func + # call this function recursively when grouped is True + if grouped: + grouped_keys = Defaultdict(list) + for fmto in map(lambda key: getattr(cls, key), keys): + grouped_keys[fmto.groupname].append(fmto.key) + text = "" + for group, keys in six.iteritems(grouped_keys): + text += ( + titled_group(group) + + cls.show_keys( + keys, + indent=indent, + grouped=False, + func=six.text_type, + include_links=include_links, + ) + + "\n\n" + ) + return func(text.rstrip()) + + if not keys: + return + n = len(keys) + ncols = min([4, n]) # number of columns + # The number of cells in the table is one of the following cases: + # 1. The number of columns and equal to the number of keys + # 2. The number of keys + # 3. The number of keys plus the empty cells in the last column + ncells = n + ((ncols - (n % ncols)) if n != ncols else 0) + if include_links or (include_links is None and cls.include_links): + long_keys = list( + map( + lambda key: ":attr:`~%s.%s.%s`" + % (cls.__module__, cls.__name__, key), + keys, + ) + ) + else: + long_keys = keys + maxn = max(map(len, long_keys)) # maximal lenght of the keys + # extend with empty cells + long_keys.extend([" " * maxn] * (ncells - n)) + bars = (str_indent + "+-" + ("-" * (maxn) + "-+-") * ncols)[:-1] + lines = ( + "| %s |\n%s" + % ( + " | ".join( + key.ljust(maxn) for key in long_keys[i : i + ncols] + ), + bars, + ) + for i in range(0, n, ncols) + ) + text = bars + "\n" + str_indent + ("\n" + str_indent).join(lines) + if six.PY2: + text = text.encode("utf-8") + + return func(text)
+ + + @classmethod + @docstrings.dedent + def _show_doc( + cls, + fmt_func, + keys=None, + indent=0, + grouped=False, + func=None, + include_links=False, + *args, + **kwargs, + ): + """ + Classmethod to print the formatoptions and their documentation + + This function is the basis for the :meth:`show_summaries` and + :meth:`show_docs` methods + + Parameters + ---------- + fmt_func: function + A function that takes the key, the key as it is printed, and the + documentation of a formatoption as argument and returns what shall + be printed + %(Plotter.show_keys.parameters)s + + Other Parameters + ---------------- + %(Plotter.show_keys.other_parameters)s + + Returns + ------- + %(Plotter.show_keys.returns)s + + See Also + -------- + show_summaries, show_docs""" + + def titled_group(groupname): + bars = str_indent + "*" * len(groupname) + "\n" + return bars + str_indent + groupname + "\n" + bars + + func = func or default_print_func + + keys = cls._enhance_keys(keys, *args, **kwargs) + str_indent = " " * indent + if grouped: + grouped_keys = Defaultdict(list) + for fmto in map(lambda key: getattr(cls, key), keys): + grouped_keys[fmto.groupname].append(fmto.key) + text = "\n\n".join( + titled_group(group) + + cls._show_doc( + fmt_func, + keys, + indent=indent, + grouped=False, + func=str, + include_links=include_links, + ) + for group, keys in six.iteritems(grouped_keys) + ) + return func(text.rstrip()) + + if include_links or (include_links is None and cls.include_links): + long_keys = list( + map( + lambda key: ":attr:`~%s.%s.%s`" + % (cls.__module__, cls.__name__, key), + keys, + ) + ) + else: + long_keys = keys + + text = "\n".join( + str_indent + + long_key + + "\n" + + fmt_func(key, long_key, getattr(cls, key).__doc__) + for long_key, key in zip(long_keys, keys) + ) + return func(text) + +
+[docs] + @classmethod + @docstrings.dedent + def show_summaries(cls, keys=None, indent=0, *args, **kwargs): + """ + Classmethod to print the summaries of the formatoptions + + Parameters + ---------- + %(Plotter.show_keys.parameters)s + + Other Parameters + ---------------- + %(Plotter.show_keys.other_parameters)s + + Returns + ------- + %(Plotter.show_keys.returns)s + + See Also + -------- + show_keys, show_docs""" + + def find_summary(key, key_txt, doc): + return "\n".join(wrapper.wrap(doc[: doc.find("\n\n")])) + + str_indent = " " * indent + wrapper = TextWrapper( + width=80, + initial_indent=str_indent + " " * 4, + subsequent_indent=str_indent + " " * 4, + ) + return cls._show_doc( + find_summary, keys=keys, indent=indent, *args, **kwargs + )
+ + +
+[docs] + @classmethod + @docstrings.dedent + def show_docs(cls, keys=None, indent=0, *args, **kwargs): + """ + Classmethod to print the full documentations of the formatoptions + + Parameters + ---------- + %(Plotter.show_keys.parameters)s + + Other Parameters + ---------------- + %(Plotter.show_keys.other_parameters)s + + Returns + ------- + %(Plotter.show_keys.returns)s + + See Also + -------- + show_keys, show_docs""" + + def full_doc(key, key_txt, doc): + return ("=" * len(key_txt)) + "\n" + doc + "\n" + + return cls._show_doc( + full_doc, keys=keys, indent=indent, *args, **kwargs + )
+ + + @classmethod + def _get_rc_strings(cls): + """ + Recursive method to get the base strings in the rcParams dictionary. + + This method takes the :attr:`_rcparams_string` attribute from the given + `class` and combines it with the :attr:`_rcparams_string` attributes + from the base classes. + The returned frozenset can be used as base strings for the + :meth:`psyplot.config.rcsetup.RcParams.find_and_replace` method. + + Returns + ------- + list + The first entry is the :attr:`_rcparams_string` of this class, + the following the :attr:`_rcparams_string` attributes of the + base classes according to the method resolution order of this + class""" + return list( + unique_everseen( + chain( + *map( + lambda base: getattr(base, "_rcparams_string", []), + cls.__mro__, + ) + ) + ) + ) + + def _set_rc(self): + """Method to set the rcparams and defaultParams for this plotter""" + base_str = self._get_rc_strings() + # to make sure that the '.' is not interpreted as a regex pattern, + # we specify the pattern_base by ourselves + pattern_base = map(lambda s: s.replace(".", r"\."), base_str) + # pattern for valid keys being all formatoptions in this plotter + pattern = "(%s)(?=$)" % "|".join(self._get_formatoptions()) + self._rc = rcParams.find_and_replace( + base_str, pattern=pattern, pattern_base=pattern_base + ) + user_rc = SubDict( + rcParams["plotter.user"], + base_str, + pattern=pattern, + pattern_base=pattern_base, + ) + self._rc.update(user_rc.data) + + self._defaultParams = SubDict( + rcParams.defaultParams, + base_str, + pattern=pattern, + pattern_base=pattern_base, + ) + + docstrings.keep_params("InteractiveBase.update.parameters", "auto_update") + +
+[docs] + @docstrings.get_sections(base="Plotter.update") + @docstrings.dedent + def update( + self, + fmt={}, + replot=False, + auto_update=False, + draw=None, + force=False, + todefault=False, + **kwargs, + ): + """ + Update the formatoptions and the plot + + If the :attr:`data` attribute of this plotter is None, the plotter is + updated like a usual dictionary (see :meth:`dict.update`). Otherwise + the update is registered and the plot is updated if `auto_update` is + True or if the :meth:`start_update` method is called (see below). + + Parameters + ---------- + %(Plotter._register_update.parameters)s + %(InteractiveBase.start_update.parameters)s + %(InteractiveBase.update.parameters.auto_update)s + ``**kwargs`` + Any other formatoption that shall be updated (additionally to those + in `fmt`) + + Notes + ----- + %(InteractiveBase.update.notes)s""" + if self.disabled: + return + fmt = dict(fmt) + if kwargs: + fmt.update(kwargs) + # if the data is None, update like a usual dictionary (but with + # validation) + if not self._initialized: + for key, val in six.iteritems(fmt): + self[key] = val + return + + self._register_update( + fmt=fmt, replot=replot, force=force, todefault=todefault + ) + if not self.no_auto_update or auto_update: + self.start_update(draw=draw)
+ + + def _set_sharing_keys(self, keys): + """ + Set the keys to share or unshare + + Parameters + ---------- + keys: string or iterable of strings + The iterable may contain formatoptions that shall be shared (or + unshared), or group names of formatoptions to share all + formatoptions of that group (see the :attr:`fmt_groups` property). + If None, all formatoptions of this plotter are inserted. + + Returns + ------- + set + The set of formatoptions to share (or unshare)""" + if isinstance(keys, str): + keys = {keys} + keys = set(self) if keys is None else set(keys) + fmto_groups = self._fmto_groups + keys.update( + chain( + *( + map(lambda fmto: fmto.key, fmto_groups[key]) + for key in keys.intersection(fmto_groups) + ) + ) + ) + keys.difference_update(fmto_groups) + return keys + +
+[docs] + @docstrings.get_sections(base="Plotter.share") + @docstrings.dedent + def share(self, plotters, keys=None, draw=None, auto_update=False): + """ + Share the formatoptions of this plotter with others + + This method shares the formatoptions of this :class:`Plotter` instance + with others to make sure that, if the formatoption of this changes, + those of the others change as well + + Parameters + ---------- + plotters: list of :class:`Plotter` instances or a :class:`Plotter` + The plotters to share the formatoptions with + keys: string or iterable of strings + The formatoptions to share, or group names of formatoptions to + share all formatoptions of that group (see the + :attr:`fmt_groups` property). If None, all formatoptions of this + plotter are unshared. + %(InteractiveBase.start_update.parameters.draw)s + %(InteractiveBase.update.parameters.auto_update)s + + See Also + -------- + unshare, unshare_me""" + auto_update = auto_update or not self.no_auto_update + if isinstance(plotters, Plotter): + plotters = [plotters] + keys = self._set_sharing_keys(keys) + for plotter in plotters: + for key in keys: + fmto = self._shared.get(key, getattr(self, key)) + if not getattr(plotter, key) == fmto: + plotter._shared[key] = getattr(self, key) + fmto.shared.add(getattr(plotter, key)) + # now exit if we are not initialized + if self._initialized: + self.update(force=keys, auto_update=auto_update, draw=draw) + for plotter in plotters: + if not plotter._initialized: + continue + old_registered = plotter._registered_updates.copy() + plotter._registered_updates.clear() + try: + plotter.update(force=keys, auto_update=auto_update, draw=draw) + except Exception: + raise + finally: + plotter._registered_updates.clear() + plotter._registered_updates.update(old_registered) + if draw is None: + draw = rcParams["auto_draw"] + if draw: + self.draw() + if rcParams["auto_show"]: + self.show()
+ + +
+[docs] + @docstrings.dedent + def unshare(self, plotters, keys=None, auto_update=False, draw=None): + """ + Close the sharing connection of this plotter with others + + This method undoes the sharing connections made by the :meth:`share` + method and releases the given `plotters` again, such that the + formatoptions in this plotter may be updated again to values different + from this one. + + Parameters + ---------- + plotters: list of :class:`Plotter` instances or a :class:`Plotter` + The plotters to release + keys: string or iterable of strings + The formatoptions to unshare, or group names of formatoptions to + unshare all formatoptions of that group (see the + :attr:`fmt_groups` property). If None, all formatoptions of this + plotter are unshared. + %(InteractiveBase.start_update.parameters.draw)s + %(InteractiveBase.update.parameters.auto_update)s + + See Also + -------- + share, unshare_me""" + auto_update = auto_update or not self.no_auto_update + if isinstance(plotters, Plotter): + plotters = [plotters] + keys = self._set_sharing_keys(keys) + for plotter in plotters: + plotter.unshare_me( + keys, auto_update=auto_update, draw=draw, update_other=False + ) + self.update(force=keys, auto_update=auto_update, draw=draw)
+ + +
+[docs] + @docstrings.get_sections(base="Plotter.unshare_me") + @docstrings.dedent + def unshare_me( + self, keys=None, auto_update=False, draw=None, update_other=True + ): + """ + Close the sharing connection of this plotter with others + + This method undoes the sharing connections made by the :meth:`share` + method and release this plotter again. + + Parameters + ---------- + keys: string or iterable of strings + The formatoptions to unshare, or group names of formatoptions to + unshare all formatoptions of that group (see the + :attr:`fmt_groups` property). If None, all formatoptions of this + plotter are unshared. + %(InteractiveBase.start_update.parameters.draw)s + %(InteractiveBase.update.parameters.auto_update)s + + See Also + -------- + share, unshare""" + auto_update = auto_update or not self.no_auto_update + keys = self._set_sharing_keys(keys) + to_update = [] + for key in keys: + fmto = getattr(self, key) + try: + other_fmto = self._shared.pop(key) + except KeyError: + pass + else: + other_fmto.shared.remove(fmto) + if update_other: + other_fmto.plotter._register_update(force=[other_fmto.key]) + to_update.append(other_fmto.plotter) + self.update(force=keys, draw=draw, auto_update=auto_update) + if update_other and auto_update: + for plotter in to_update: + plotter.start_update(draw=draw)
+ + +
+[docs] + def get_vfunc(self, key): + """Return the validation function for a specified formatoption + + Parameters + ---------- + key: str + Formatoption key in the :attr:`rc` dictionary + + Returns + ------- + function + Validation function for this formatoption""" + return self._defaultParams[key][1]
+ + + def _save_state(self): + """Saves the current formatoptions""" + self._old_fmt.append(self.changed) + +
+[docs] + def show(self): + """Shows all open figures""" + import matplotlib.pyplot as plt + + plt.show(block=False)
+ + +
+[docs] + @dedent + def has_changed(self, key, include_last=True): + """ + Determine whether a formatoption changed in the last update + + Parameters + ---------- + key: str + A formatoption key contained in this plotter + include_last: bool + if True and the formatoption has been included in the last update, + the return value will not be None. Otherwise the return value will + only be not None if it changed during the last update + + Returns + ------- + None or list + - None, if the value has not been changed during the last update or + `key` is not a valid formatoption key + - a list of length two with the old value in the first place and + the given `value` at the second""" + if self._initializing or key not in self: + return + fmto = getattr(self, key) + if self._old_fmt and key in self._old_fmt[-1]: + old_val = self._old_fmt[-1][key] + else: + old_val = fmto.default + if fmto.diff(old_val) or ( + include_last and fmto.key in self._last_update + ): + return [old_val, fmto.value]
+ + +
+[docs] + def get_enhanced_attrs(self, arr, axes=["x", "y", "t", "z"]): + if isinstance(arr, InteractiveList): + all_attrs = list( + starmap(self.get_enhanced_attrs, zip(arr, repeat(axes))) + ) + attrs = { + key: val + for key, val in six.iteritems(all_attrs[0]) + if all( + key in attrs and attrs[key] == val + for attrs in all_attrs[1:] + ) + } + attrs.update(arr.attrs) + else: + attrs = arr.attrs.copy() + base_variables = self.base_variables + if len(base_variables) > 1: # multiple variables + for name, base_var in six.iteritems(base_variables): + attrs.update( + { + six.text_type(name) + key: value + for key, value in six.iteritems(base_var.attrs) + } + ) + else: + base_var = next(six.itervalues(base_variables)) + attrs["name"] = arr.name + for dim, coord in six.iteritems(getattr(arr, "coords", {})): + if coord.size == 1: + attrs[dim] = format_time(coord.values) + if isinstance(self.data, InteractiveList): + base = self.data[0].psy.base + else: + base = self.data.psy.base + for dim in axes: + for obj in [base_var, arr]: + decoder = CFDecoder.get_decoder(base, obj) + coord = getattr(decoder, "get_" + dim)( + obj, coords=getattr(arr, "coords", None) + ) + if coord is None: + continue + if coord.size == 1: + attrs[dim] = format_time(coord.values) + attrs[dim + "name"] = coord.name + for key, val in six.iteritems(coord.attrs): + attrs[dim + key] = val + self._enhanced_attrs = attrs + return attrs
+ + + def _make_plot(self): + plot_fmtos = [fmto for fmto in self._fmtos if fmto.plot_fmt] + plot_fmtos.sort(key=lambda fmto: fmto.priority, reverse=True) + for fmto in plot_fmtos: + self.logger.debug("Making plot with %s formatoption", fmto.key) + fmto.make_plot() + + @classmethod + def _get_sample_projection(cls): + """Returns None. May be subclassed to return a projection that + can be used when creating a subplot""" + pass + +
+[docs] + @docstrings.dedent + def convert_coordinate(self, coord, *variables): + """Convert a coordinate to units necessary for the plot. + + %(Formatoption.convert_coordinate.summary_ext)s + + Parameters + ---------- + %(Formatoption.convert_coordinate.parameters)s + + Returns + ------- + %(Formatoption.convert_coordinate.returns)s + + Notes + ----- + This method is supposed to be implemented by subclasses. The default + implementation by the :class:`Plotter` class does nothing. + """ + return coord
+
+ +
+ +
+
+ + + +
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/psyplot/project.html b/_modules/psyplot/project.html new file mode 100644 index 0000000..711067a --- /dev/null +++ b/_modules/psyplot/project.html @@ -0,0 +1,3640 @@ + + + + + + psyplot.project — psyplot documentation + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for psyplot.project

+"""Project module of the psyplot Package.
+
+This module contains the :class:`Project` class that serves as the main
+part of the psyplot API. One instance of the :class:`Project` class serves as
+coordinator of multiple plots and can be distributed into subprojects that
+keep reference to the main project without holding all array instances
+
+Furthermore this module contains an easy pyplot-like API to the current
+subproject."""
+
+# SPDX-FileCopyrightText: 2016-2024 University of Lausanne
+# SPDX-FileCopyrightText: 2020-2021 Helmholtz-Zentrum Geesthacht
+
+# SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH
+#
+# SPDX-License-Identifier: LGPL-3.0-only
+
+import logging
+import os
+import os.path as osp
+import pickle
+import sys
+from collections import defaultdict
+from copy import deepcopy as _deepcopy
+from functools import partial, wraps
+from importlib import import_module
+from itertools import chain, count, cycle, islice, repeat
+
+import matplotlib as mpl
+import matplotlib.figure as mfig
+import numpy as np
+import pandas as pd
+import six
+import xarray
+import yaml
+from matplotlib.axes import SubplotBase
+
+import psyplot
+import psyplot.utils as utils
+from psyplot import get_versions, rcParams
+from psyplot.config.rcsetup import get_configdir, psyplot_fname
+from psyplot.data import (
+    ArrayList,
+    CFDecoder,
+    InteractiveList,
+    Signal,
+    _MissingModule,
+    open_dataset,
+    open_mfdataset,
+    safe_list,
+)
+from psyplot.docstring import dedent, docstrings, safe_modulo
+from psyplot.plotter import Plotter, unique_everseen
+from psyplot.utils import get_default_value as _get_default_value
+from psyplot.warning import critical, warn
+
+try:
+    from cdo import CDO_PY_VERSION as cdo_version
+    from cdo import Cdo as _CdoBase
+
+    with_cdo = True
+    cdo_version = tuple(map(int, cdo_version.split(".")[:2]))
+except ImportError as e:
+    Cdo = _MissingModule(e)
+    with_cdo = False
+    cdo_version = None
+
+try:  # try import show_colormaps for convenience
+    from psy_simple.colors import get_cmap, show_colormaps  # noqa: F401
+except ImportError:
+    pass
+
+if rcParams["project.import_seaborn"] is not False:
+    try:
+        import seaborn as _sns
+    except ImportError as e:
+        if rcParams["project.import_seaborn"]:
+            raise
+        _sns = _MissingModule(e)
+
+_open_projects = []  # list of open projects
+_current_project = None  # current main project
+_current_subproject = None  # current subproject
+
+# the informations on the psyplot and plugin versions
+_versions = get_versions(requirements=False)
+
+
+_concat_dim_default = _get_default_value(xarray.open_mfdataset, "concat_dim")
+
+
+def _update_versions():
+    """Update :attr:`_versions` with the registered plotter methods"""
+    for pm_name in plot._plot_methods:
+        pm = getattr(plot, pm_name)
+        plugin = pm._plugin
+        if (
+            plugin is not None
+            and plugin not in _versions
+            and pm.module in sys.modules
+        ):
+            _versions.update(get_versions(key=lambda s: s == plugin))
+
+
+
+[docs] +@docstrings.get_sections(base="multiple_subplots") +@docstrings.dedent +def multiple_subplots( + rows=1, + cols=1, + maxplots=None, + n=1, + delete=True, + for_maps=False, + *args, + **kwargs, +): + """ + Function to create subplots. + + This function creates so many subplots on so many figures until the + specified number `n` is reached. + + Parameters + ---------- + rows: int + The number of subplots per rows + cols: int + The number of subplots per column + maxplots: int + The number of subplots per figure (if None, it will be row*cols) + n: int + number of subplots to create + delete: bool + If True, the additional subplots per figure are deleted + for_maps: bool + If True this is a simple shortcut for setting + ``subplot_kw=dict(projection=cartopy.crs.PlateCarree())`` and is + useful if you want to use the :attr:`~ProjectPlotter.mapplot`, + :attr:`~ProjectPlotter.mapvector` or + :attr:`~ProjectPlotter.mapcombined` plotting methods + ``*args`` and ``**kwargs`` + anything that is passed to the :func:`matplotlib.pyplot.subplots` + function + + Returns + ------- + list + list of maplotlib.axes.SubplotBase instances""" + import matplotlib.pyplot as plt + + axes = np.array([]) + maxplots = maxplots or rows * cols + kwargs.setdefault("figsize", [min(8.0 * cols, 16), min(6.5 * rows, 12)]) + if for_maps: + import cartopy.crs as ccrs + + subplot_kw = kwargs.setdefault("subplot_kw", {}) + subplot_kw["projection"] = ccrs.PlateCarree() + for i in range(0, n, maxplots): + fig, ax = plt.subplots(rows, cols, *args, **kwargs) + try: + axes = np.append(axes, ax.ravel()[:maxplots]) + if delete: + for iax in range(maxplots, rows * cols): + fig.delaxes(ax.ravel()[iax]) + except AttributeError: # got a single subplot + axes = np.append(axes, [ax]) + if i + maxplots > n and delete: + for ax2 in axes[n:]: + fig.delaxes(ax2) + axes = axes[:n] + return axes
+ + + +def _is_slice(val): + return isinstance(val, slice) + + +def _only_main(func): + """Call the given `func` only from the main project""" + + @wraps(func) + def wrapper(self, *args, **kwargs): + if not self.is_main: + return getattr(self.main, func.__name__)(*args, **kwargs) + return func(self, *args, **kwargs) + + return wrapper + + +def _first_main(func): + """Call the given `func` with the same arguments but after the function + of the main project""" + + @wraps(func) + def wrapper(self, *args, **kwargs): + if not self.is_main: + getattr(self.main, func.__name__)(*args, **kwargs) + return func(self, *args, **kwargs) + + return wrapper + + +
+[docs] +class Project(ArrayList): + """A manager of multiple interactive data projects""" + + _main = None + + _registered_plotters = {} #: registered plotter identifiers + + #: signal to be emiitted when the current main and/or subproject changes + oncpchange = Signal(name="oncpchange", cls_signal=True) + + # block the signals of this class + block_signals = utils._TempBool() + + @property + def main(self): + """:class:`Project`. The main project of this subproject""" + return self._main if self._main is not None else self + + @main.setter + def main(self, value): + self._main = value + + @property + @dedent + def plot(self): + """ + Plotting instance of this :class:`Project`. See the + :class:`ProjectPlotter` class for method documentations""" + return self._plot + + @property + def _fmtos(self): + """An iterator over formatoption objects + + Contains only the formatoption whose keys are in all plotters in this + list""" + plotters = self.plotters + if len(plotters) == 0: + return {} + p0 = plotters[0] + if len(plotters) == 1: + return p0._fmtos + return ( + getattr(p0, key) + for key in set(p0).intersection(*map(set, plotters[1:])) + ) + + @property + def is_csp(self): + """Boolean that is True if the project is the current subproject""" + return self is _current_subproject + + @property + def is_cmp(self): + """Boolean that is True if the project is the current main project""" + return self is _current_project + + @property + def figs(self): + """A mapping from figures to data objects with the plotter in this + figure""" + ret = utils.Defaultdict(lambda: self[1:0]) + for arr in self: + if arr.psy.plotter is not None: + ret[arr.psy.plotter.ax.get_figure()].append(arr) + return dict(ret) + + @property + def axes(self): + """A mapping from axes to data objects with the plotter in this axes""" + ret = utils.Defaultdict(lambda: self[1:0]) + for arr in self: + if arr.psy.plotter is not None: + ret[arr.psy.plotter.ax].append(arr) + return dict(ret) + + @property + def is_main(self): + """:class:`bool`. True if this :class:`Project` is a main project""" + return self._main is None + + @property + def logger(self): + """:class:`logging.Logger` of this instance""" + if not self.is_main: + return self.main.logger + try: + return self._logger + except AttributeError: + name = "%s.%s.%s" % ( + self.__module__, + self.__class__.__name__, + self.num, + ) + self._logger = logging.getLogger(name) + self.logger.debug("Initializing...") + return self._logger + + @logger.setter + def logger(self, value): + self._logger = value + + def with_plotter(self): + ret = super(Project, self).with_plotter + ret.main = self.main + return ret + + with_plotter = property(with_plotter, doc=ArrayList.with_plotter.__doc__) + + @property + def arr_names(self): + """Names of the arrays (!not of the variables!) in this list + + This attribute can be set with an iterable of unique names to change + the array names of the data objects in this list.""" + return list(arr.psy.arr_name for arr in self) + + @arr_names.setter + def arr_names(self, value): + value = list(islice(value, len(self))) + if not len(set(value)) == len(self): + raise ValueError( + "Got %i unique array names for %i data objects!" + % (len(set(value)), len(self)) + ) + elif not self.is_main and set(value) & ( + set(self.main.arr_names) - set(self.arr_names) + ): + raise ValueError( + "Cannot rename arrays because there are duplicates with the " + "main project: %s" + % ( + set(value) + & (set(self.main.arr_names) - set(self.arr_names)), + ) + ) + for arr, n in zip(self, value): + arr.psy.arr_name = n + if self.main is gcp(True): + for arr in self: + arr.psy.onupdate.emit() + + @property + def plotters(self): + """A list of all the plotters in this instance""" + return [arr.psy.plotter for arr in self.with_plotter] + + @property + def datasets(self): + """A mapping from dataset numbers to datasets in this list""" + return { + key: val["ds"] + for key, val in six.iteritems( + self._get_ds_descriptions( + self.array_info(ds_description=["ds"]) + ) + ) + } + + @property + def dsnames_map(self): + """A dictionary from the dataset numbers in this list to their + filenames""" + return { + key: val["fname"] + for key, val in six.iteritems( + self._get_ds_descriptions( + self.array_info(ds_description=["num", "fname"]), + ds_description={"fname"}, + ) + ) + } + + @property + def dsnames(self): + """The set of dataset names in this instance""" + return {t[0] for t in self._get_dsnames(self.array_info()) if t[0]} + + @docstrings.get_sections(base="Project") + @docstrings.dedent + def __init__(self, *args, **kwargs): + """ + Parameters + ---------- + %(ArrayList.parameters)s + main: Project + The main project this subproject belongs to (or None if this + project is the main project) + num: int + The number of the project + """ + self.main = kwargs.pop("main", None) + self._plot = ProjectPlotter(self) + self.num = kwargs.pop("num", 1) + self._ds_counter = count() + with self.block_signals: + super(Project, self).__init__(*args, **kwargs) + + @classmethod + @docstrings.get_sections(base="Project._register_plotter") + @dedent + def _register_plotter( + cls, identifier, module, plotter_name, plotter_cls=None + ): + """ + Register a plotter in the :class:`Project` class to easy access it + + Parameters + ---------- + identifier: str + Name of the attribute that is used to filter for the instances + belonging to this plotter + module: str + The module from where to import the `plotter_name` + plotter_name: str + The name of the plotter class in `module` + plotter_cls: type + The imported class of `plotter_name`. If None, it will be imported + when it is needed + """ + if plotter_cls is not None: # plotter has already been imported + + def get_x(self): + return self(plotter_cls) + + else: + + def get_x(self): + return self(getattr(import_module(module), plotter_name)) + + setattr( + cls, + identifier, + property( + get_x, + doc=( + "List of data arrays that are plotted by :class:`%s.%s`" + " plotters" + ) + % (module, plotter_name), + ), + ) + cls._registered_plotters[identifier] = (module, plotter_name) + +
+[docs] + def disable(self): + """Disables the plotters in this list""" + for arr in self: + if arr.psy.plotter: + arr.psy.plotter.disabled = True
+ + +
+[docs] + def enable(self): + for arr in self: + if arr.psy.plotter: + arr.psy.plotter.disabled = False
+ + + def __call__(self, *args, **kwargs): + ret = super(Project, self).__call__(*args, **kwargs) + ret.main = self.main + return ret + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + self.close(True, True, True) + + @staticmethod + @docstrings.get_sections( + base="Project._load_preset", sections=["Parameters", "Notes"] + ) + def _load_preset(preset: str): + """Load a preset from disk + + Parameters + ---------- + preset: str or dict + The filename or identifier of a preset. If the given `preset` is + the path to an existing yaml file, it will be loaded. Otherwise we + look up the `preset` in the psyplot configuration directory (see + :func:`~psyplot.config.rcsetup.get_configdir`). + If a dictionary is provided, we assume that this is the preset + + Returns + ------- + dict + The loaded preset + + Notes + ----- + An identifier is the filename without extension. If you want to list + the available presets, run ``psyplot -lp`` from the command-line""" + if isinstance(preset, dict): + config = preset + else: + path = Project._resolve_preset_path(preset) + if path in rcParams["presets.trusted"]: + loader = yaml.Loader + else: + loader = yaml.SafeLoader + with open(path) as f: + try: + config = yaml.load(f, loader) + except yaml.constructor.ConstructorError as e: + e.note = (e.note or "") + ( + " You might want to add it to the trusted presets " + 'via\n\npsy.rcParams["presets.trusted"].append("{}")\n\n' + "and run this method again. To permanently store " + "this preset, edit the file at\n\n{} " + ).format(path, psyplot_fname()) + raise + + return config + + @staticmethod + def _resolve_preset_path(preset, if_exists=True): + if osp.exists(preset): + return preset + else: + confdir = get_configdir() + presets_dir = osp.join(confdir, "presets") + if osp.exists(osp.join(presets_dir, preset)): + return osp.join(presets_dir, preset) + elif osp.exists(osp.join(presets_dir, preset + ".yml")): + return osp.join(presets_dir, preset + ".yml") + else: + if if_exists: + raise ValueError( + f"Could not find a preset with name {preset}" + ) + else: + if not preset.endswith(".yml"): + return osp.join(presets_dir, preset + ".yml") + return preset + +
+[docs] + @docstrings.dedent + def load_preset(self, preset: str, **kwargs): + """Load a preset from disk and apply it to the open project. + + This method loads a preset and updates the corresponding plots + + Parameters + ---------- + %(Project._load_preset.parameters)s + ``**kwargs`` + Any other parameter that shall be passed to the + :meth:`~psyplot.data.ArrayList.update` method + + Notes + ----- + %(Project._load_preset.notes)s + """ + config = self._load_preset(preset) + plotmethods = self.plot._plot_methods + pm_config, defaults = utils.sort_kwargs(config, plotmethods) + with self.no_auto_update: + for pm in plotmethods: + method = getattr(self.plot, pm) + if method.is_imported: + sp = getattr(self, pm) + if sp: + valid = list(method.plotter_cls._get_formatoptions()) + fmts = { + key: val + for key, val in defaults.items() + if key in valid + } + fmts.update(pm_config.get(pm, {})) + sp.update(fmt=fmts, **kwargs) + self.start_update()
+ + +
+[docs] + @staticmethod + def extract_fmts_from_preset(preset: str, plotmethod: str): + """Extract the formatoptions for a plotmethod from a given preset + + This method takes the preset and extracts the formatoptions valid for + the given plotmethod + + Parameters + ---------- + %(Project._load_preset.parameters)s + plotmethod: str + The plotmethod to use""" + preset = Project._load_preset(preset) + try: + plotmethod._method + except AttributeError: + method = getattr(plot, plotmethod) + else: + method = plotmethod + plotmethod = method._method + + plotmethods = plot._plot_methods + pm_config, defaults = utils.sort_kwargs(preset, plotmethods) + valid = list(method.plotter_cls._get_formatoptions()) + fmts = {key: val for key, val in defaults.items() if key in valid} + fmts.update(pm_config.get(plotmethod, {})) + return fmts
+ + +
+[docs] + def save_preset(self, fname=None, include_defaults=False, update=False): + """Save the formatoptions of this project as a preset + + This method takes the formatoptions in the plotters of this project and + saves it as a preset file""" + + def include(fmto, plotters): + key = fmto.key + for plotter in plotters: + if fmto.diff(plotter[key]): + return False + return True if include_defaults else fmto.changed + + if update: + with open(fname) as f: + preset = yaml.load(f, yaml.Loader) + else: + preset = {} + plotters = self.plotters + + for fmto in self._fmtos: + if include(fmto, plotters): + preset[fmto.key] = fmto.value + + for pm in self.plot._plot_methods: + method = getattr(self.plot, pm) + if method.is_imported: + sp = getattr(self, pm) + plotters = sp.plotters + for fmto in sp._fmtos: + if fmto.key not in preset and include(fmto, plotters): + preset.setdefault(pm, {}) + preset[pm][fmto.key] = fmto.value + if fname is not None: + fname = self._resolve_preset_path(fname, False) + os.makedirs(osp.dirname(fname), exist_ok=True) + with open(fname, "w") as f: + yaml.dump(preset, f) + else: + return preset
+ + +
+[docs] + @_first_main + def extend(self, *args, **kwargs): + len0 = len(self) + ret = super(Project, self).extend(*args, **kwargs) + if self._main is None: + for arr in self: + if arr.psy.plotter is not None: + arr.psy.plotter._project = self + if len(self) > len0 and (self.is_csp or self.is_cmp): + self.oncpchange.emit(self) + return ret
+ + + extend.__doc__ = ArrayList.extend.__doc__ + +
+[docs] + @_first_main + def append(self, *args, **kwargs): + len0 = len(self) + ret = super(Project, self).append(*args, **kwargs) + if self._main is None: + for arr in self: + if arr.psy.plotter is not None: + arr.psy.plotter._project = self + if len(self) > len0 and (self.is_csp or self.is_cmp): + self.oncpchange.emit(self) + return ret
+ + + append.__doc__ = ArrayList.append.__doc__ + + __call__.__doc__ = ArrayList.__call__.__doc__ + +
+[docs] + @docstrings.get_sections(base="Project.close") + @dedent + def close(self, figs=True, data=False, ds=False, remove_only=False): + """ + Close this project instance + + Parameters + ---------- + figs: bool + Close the figures + data: bool + delete the arrays from the (main) project + ds: bool + If True, close the dataset as well + remove_only: bool + If True and `figs` is True, the figures are not closed but the + plotters are removed""" + import matplotlib.pyplot as plt + + close_ds = ds + for arr in self[:]: + if figs and arr.psy.plotter is not None: + if remove_only: + for fmto in arr.psy.plotter._fmtos: + try: + fmto.remove() + except Exception: + pass + else: + plt.close(arr.psy.plotter.ax.get_figure().number) + arr.psy.plotter = None + if data: + self.remove(arr) + if not self.is_main: + try: + self.main.remove(arr) + except ValueError: # arr not in list + pass + if close_ds: + if isinstance(arr, InteractiveList): + for ds in [ + val["ds"] + for val in six.itervalues( + arr._get_ds_descriptions( + arr.array_info( + ds_description=["ds"], + standardize_dims=False, + ) + ) + ) + ]: + ds.close() + else: + arr.psy.base.close() + if self.is_main and self is gcp(True) and data: + scp(None) + elif self.is_main and self.is_cmp: + self.oncpchange.emit(self) + elif self.main.is_cmp: + self.oncpchange.emit(self.main)
+ + + docstrings.keep_params("multiple_subplots.parameters", "delete") + docstrings.delete_params("ArrayList.from_dataset.parameters", "base") + docstrings.delete_kwargs( + "ArrayList.from_dataset.other_parameters", kwargs="kwargs" + ) + docstrings.keep_params("xarray.open_mfdataset.parameters", "concat_dim") + docstrings.keep_params("Project._load_preset.parameters", "preset") + + @_only_main + @docstrings.get_sections( + base="Project._add_data", + sections=["Parameters", "Other Parameters", "Returns"], + ) + @docstrings.dedent + def _add_data( + self, + plotter_cls, + filename_or_obj, + fmt={}, + make_plot=True, + draw=False, + mf_mode=False, + ax=None, + engine=None, + delete=True, + share=False, + clear=False, + enable_post=None, + concat_dim=_concat_dim_default, + load=False, + *args, + **kwargs, + ): + """ + Extract data from a dataset and visualize it with the given plotter + + Parameters + ---------- + plotter_cls: type + The subclass of :class:`psyplot.plotter.Plotter` to use for + visualization + filename_or_obj: filename, :class:`xarray.Dataset` or data store + The object (or file name) to open. If not a dataset, the + :func:`psyplot.data.open_dataset` will be used to open a dataset + fmt: dict + Formatoptions that shall be when initializing the plot (you can + however also specify them as extra keyword arguments) + make_plot: bool + If True, the data is plotted at the end. Otherwise you have to + call the :meth:`psyplot.plotter.Plotter.initialize_plot` method or + the :meth:`psyplot.plotter.Plotter.reinit` method by yourself + %(InteractiveBase.start_update.parameters.draw)s + mf_mode: bool + If True, the :func:`psyplot.open_mfdataset` method is used. + Otherwise we use the :func:`psyplot.open_dataset` method which can + open only one single dataset + ax: None, tuple (x, y[, z]) or (list of) matplotlib.axes.Axes + Specifies the subplots on which to plot the new data objects. + + - If None, a new figure will be created for each created plotter + - If tuple (x, y[, z]), `x` specifies the number of rows, `y` the + number of columns and the optional third parameter `z` the + maximal number of subplots per figure. + - If :class:`matplotlib.axes.Axes` (or list of those, e.g. created + by the :func:`matplotlib.pyplot.subplots` function), the data + will be plotted on these subplots + %(open_dataset.parameters.engine)s + %(multiple_subplots.parameters.delete)s + share: bool, fmt key or list of fmt keys + Determines whether the first created plotter shares it's + formatoptions with the others. If True, all formatoptions are + shared. Strings or list of strings specify the keys to share. + clear: bool + If True, axes are cleared before making the plot. This is only + necessary if the `ax` keyword consists of subplots with projection + that differs from the one that is needed + enable_post: bool + If True, the :attr:`~psyplot.plotter.Plotter.post` formatoption is + enabled and post processing scripts are allowed. If ``None``, this + parameter is set to True if there is a value given for the `post` + formatoption in `fmt` or `kwargs` + %(xarray.open_mfdataset.parameters.concat_dim)s + This parameter only does have an effect if `mf_mode` is True. + load: bool + If True, load the complete dataset into memory before plotting. + This might be useful if the data of other variables in the dataset + has to be accessed multiple times, e.g. for unstructured grids. + %(ArrayList.from_dataset.parameters.no_base)s + + Other Parameters + ---------------- + %(ArrayList.from_dataset.other_parameters.no_args_kwargs)s + ``**kwargs`` + Any other dimension or formatoption that shall be passed to `dims` + or `fmt` respectively. + + Returns + ------- + Project + The subproject that contains the new (visualized) data array""" + if not isinstance(filename_or_obj, xarray.Dataset): + if mf_mode: + filename_or_obj = open_mfdataset( + filename_or_obj, engine=engine, concat_dim=concat_dim + ) + else: + filename_or_obj = open_dataset(filename_or_obj, engine=engine) + if load: + old = filename_or_obj + filename_or_obj = filename_or_obj.load() + old.close() + + fmt = dict(fmt) + possible_fmts = list(plotter_cls._get_formatoptions()) + additional_fmt, kwargs = utils.sort_kwargs(kwargs, possible_fmts) + fmt.update(additional_fmt) + if enable_post is None: + enable_post = bool(fmt.get("post")) + # create the subproject + sub_project = self.from_dataset(filename_or_obj, **kwargs) + sub_project.main = self + sub_project.no_auto_update = not ( + not sub_project.no_auto_update or not self.no_auto_update + ) + # create the subplots + proj = plotter_cls._get_sample_projection() + if isinstance(ax, tuple): + axes = iter( + multiple_subplots( + *ax, n=len(sub_project), subplot_kw={"projection": proj} + ) + ) + elif ax is None or isinstance( + ax, (mpl.axes.SubplotBase, mpl.axes.Axes) + ): + axes = repeat(ax) + else: + axes = iter(ax) + clear = clear or (isinstance(ax, tuple) and proj is not None) + + for arr in sub_project: + plotter_cls( + arr, + make_plot=(not bool(share) and make_plot), + draw=False, + ax=next(axes), + clear=clear, + project=self, + enable_post=enable_post, + **fmt, + ) + if share: + if share is True: + share = possible_fmts + elif isinstance(share, six.string_types): + share = [share] + else: + share = list(share) + sub_project[0].psy.plotter.share( + [arr.psy.plotter for arr in sub_project[1:]], + keys=share, + draw=False, + ) + if make_plot: + for arr in sub_project: + arr.psy.plotter.reinit(draw=False, clear=clear) + if draw is None: + draw = rcParams["auto_draw"] + if draw: + sub_project.draw() + if rcParams["auto_show"]: + self.show() + self.extend(sub_project, new_name=True) + if self is gcp(True): + scp(sub_project) + return sub_project + + def __getitem__(self, key): + """Overwrites lists __getitem__ by returning subproject if `key` is a + slice""" + if isinstance(key, slice): # return a new project + ret = self.__class__(super(Project, self).__getitem__(key)) + ret.main = self.main + else: # return the item + ret = super(Project, self).__getitem__(key) + return ret + + if six.PY2: # for compatibility to python 2.7 + + def __getslice__(self, *args): + return self[slice(*args)] + + def __add__(self, other): + # overwritte to return a subproject + ret = self.__class__(super(Project, self).__add__(other)) + ret.main = self.main + return ret + +
+[docs] + @staticmethod + def show(): + """Shows all open figures""" + import matplotlib.pyplot as plt + + plt.show(block=False)
+ + + docstrings.keep_params("join_dicts.parameters", "delimiter") + docstrings.keep_params("join_dicts.parameters", "keep_all") + +
+[docs] + @docstrings.get_sections(base="Project.joined_attrs") + @docstrings.with_indent(8) + def joined_attrs( + self, delimiter=", ", enhanced=True, plot_data=False, keep_all=True + ): + """Join the attributes of the arrays in this project + + Parameters + ---------- + %(join_dicts.parameters.delimiter)s + enhanced: bool + If True, the :meth:`psyplot.plotter.Plotter.get_enhanced_attrs` + method is used, otherwise the :attr:`xarray.DataArray.attrs` + attribute is used. + plot_data: bool + It True, use the :attr:`psyplot.plotter.Plotter.plot_data` + attribute of the plotters rather than the raw data in this project + %(join_dicts.parameters.keep_all)s + + Returns + ------- + dict + A mapping from the attribute to the joined attributes which are + either strings or (if there is only one attribute value), the + data type of the corresponding value""" + if enhanced: + all_attrs = [ + plotter.get_enhanced_attrs( + getattr(plotter, "plot_data" if plot_data else "data") + ) + for plotter in self.plotters + ] + else: + if plot_data: + all_attrs = [ + plotter.plot_data.attrs for plotter in self.plotters + ] + else: + all_attrs = [arr.attrs for arr in self] + return utils.join_dicts( + all_attrs, delimiter=delimiter, keep_all=keep_all + )
+ + +
+[docs] + @docstrings.get_sections(base="Project.format_string") + @docstrings.with_indent(8) + def format_string( + self, s, use_time=False, format_args=None, *args, **kwargs + ): + """Format a string with the attributes in this project + + Parameters + ---------- + s: str + The string that is subject to be formatted + use_time: bool + If True, formatting strings for the + :meth:`datetime.datetime.strftime` are expected to be found in + `output` (e.g. ``'%%m'``, ``'%%Y'``, etc.). If so, other formatting + strings must be escaped by double ``'%%'`` (e.g. ``'%%%i'`` + instead of (``'%%i'``)) + format_args: tuple + A tuple of arguments that shall be inserted in `s` via + ``s %% format_args``. (There will be no error, when this fails!) + %(Project.joined_attrs.parameters)s + + Returns + ------- + str + The formatted string `s` + """ + attrs = self.joined_attrs(*args, **kwargs) + if use_time: + tnames = self._get_tnames() + tname = next(iter(tnames)) if len(tnames) == 1 else None + + time = attrs[tname] + try: # assume a valid datetime.datetime instance + s = pd.to_datetime(time).strftime(s) + except ValueError: + pass + if format_args is not None: + try: + s = safe_modulo(s, format_args, print_warning=False) + except TypeError: + pass + return safe_modulo(s, attrs)
+ + + docstrings.keep_params("Project.format_string.parameters", "use_time") + +
+[docs] + @docstrings.with_indent(8) + def export( + self, + output, + tight=False, + concat=True, + close_pdf=None, + use_time=False, + **kwargs, + ): + """Exports the figures of the project to one or more image files + + Parameters + ---------- + output: str, iterable or matplotlib.backends.backend_pdf.PdfPages + if string or list of strings, those define the names of the output + files. Otherwise you may provide an instance of + :class:`matplotlib.backends.backend_pdf.PdfPages` to save the + figures in it. + If string (or iterable of strings), attribute names in the + xarray.DataArray.attrs attribute as well as index dimensions + are replaced by the respective value (see examples below). + Furthermore a single format string without key (e.g. %%i, %%s, %%d, + etc.) is replaced by a counter. + tight: bool + If True, it is tried to figure out the tight bbox of the figure + (same as bbox_inches='tight') + concat: bool + if True and the output format is `pdf`, all figures are + concatenated into one single pdf + close_pdf: bool or None + If True and the figures are concatenated into one single pdf, + the resulting pdf instance is closed. If False it remains open. + If None and `output` is a string, it is the same as + ``close_pdf=True``, if None and `output` is neither a string nor an + iterable, it is the same as ``close_pdf=False`` + %(Project.format_string.parameters.use_time)s + ``**kwargs`` + Any valid keyword for the :func:`matplotlib.pyplot.savefig` + function + + Returns + ------- + matplotlib.backends.backend_pdf.PdfPages or None + a PdfPages instance if output is a string and close_pdf is False, + otherwise None + + Examples + -------- + Simply save all figures into one single pdf:: + + >>> p = psy.gcp() + >>> p.export('my_plots.pdf') + + Save all figures into separate pngs with increasing numbers (e.g. + ``'my_plots_1.png'``):: + + >>> p.export('my_plots_%%i.png') + + Save all figures into separate pngs with the name of the variables + shown in each figure (e.g. ``'my_plots_t2m.png'``):: + + >>> p.export('my_plots_%%(name)s.png') + + Save all figures into separate pngs with the name of the variables + shown in each figure and with increasing numbers (e.g. + ``'my_plots_1_t2m.png'``):: + + >>> p.export('my_plots_%%i_%%(name)s.png') + + Specify the names for each figure directly via a list:: + + >>> p.export(['my_plots1.pdf', 'my_plots2.pdf']) + """ + from matplotlib.backends.backend_pdf import PdfPages + + if tight: + kwargs["bbox_inches"] = "tight" + + not_enough_files_warnings = ( + "Not enough output files specified! %i figures are open " + "but only %i filenames have been given! This will cause " + "that some figures may be overwritten after being " + "exported! Use a pdf instead if you want to save all " + "figures or include a '%%i' string in the filename to " + "avoid duplicates." + ) + + if isinstance(output, six.string_types): # a single string + out_fmt = kwargs.pop("format", os.path.splitext(output))[1][1:] + if out_fmt.lower() == "pdf" and concat: + output = self.format_string(output, use_time, delimiter="-") + pdf = PdfPages(output) + + for fig in self.figs: + pdf.savefig(fig, **kwargs) + if close_pdf is None or close_pdf: + pdf.close() + return + else: + return pdf + else: + output = [output] * len(self.figs) + + if utils.is_iterable(output): # a list of strings + output = [ + sp.format_string(out, use_time, i, delimiter="-") + for i, (out, sp) in enumerate( + zip(output, self.figs.values()), 1 + ) + ] + if len(set(output)) != len(output): + warn(not_enough_files_warnings % (len(output), len(self.figs))) + output = iter(output) + + for fig, out in zip(self.figs, output): + fig.savefig(out, **kwargs) + else: # an instances of matplotlib.backends.backend_pdf.PdfPages + for fig in self.figs: + output.savefig(fig, **kwargs) + if close_pdf: + output.close()
+ + + docstrings.keep_params("Plotter.share.parameters", "keys") + docstrings.delete_params("Plotter.share.parameters", "keys", "plotters") + +
+[docs] + @docstrings.dedent + def share(self, base=None, keys=None, by=None, **kwargs): + """ + Share the formatoptions of one plotter with all the others + + This method shares specified formatoptions from `base` with all the + plotters in this instance. + + Parameters + ---------- + base: None, Plotter, xarray.DataArray, InteractiveList, or list of them + The source of the plotter that shares its formatoptions with the + others. It can be None (then the first instance in this project + is used), a :class:`~psyplot.plotter.Plotter` or any data object + with a *psy* attribute. If `by` is not None, then it is expected + that `base` is a list of data objects for each figure/axes + %(Plotter.share.parameters.keys)s + by: {'fig', 'figure', 'ax', 'axes'} + Share the formatoptions only with the others on the same + ``'figure'`` or the same ``'axes'``. In this case, base must either + be ``None`` or a list of the types specified for `base` + %(Plotter.share.parameters.no_keys|plotters)s + + See Also + -------- + psyplot.plotter.share""" + if by is not None: + if base is not None: + if hasattr(base, "psy") or isinstance(base, Plotter): + base = [base] + if by.lower() in ["ax", "axes"]: + bases = { + ax: p[0] for ax, p in six.iteritems(Project(base).axes) + } + elif by.lower() in ["fig", "figure"]: + bases = { + fig: p[0] + for fig, p in six.iteritems(Project(base).figs) + } + else: + raise ValueError( + "*by* must be out of {'fig', 'figure', 'ax', 'axes'}. " + "Not %s" % (by,) + ) + else: + bases = {} + projects = self.axes if by == "axes" else self.figs + for obj, p in projects.items(): + p.share(bases.get(obj), keys, **kwargs) + else: + plotters = self.plotters + if not plotters: + return + if base is None: + if len(plotters) == 1: + return + base = plotters[0] + plotters = plotters[1:] + elif not isinstance(base, Plotter): + base = getattr(getattr(base, "psy", base), "plotter", base) + base.share(plotters, keys=keys, **kwargs)
+ + +
+[docs] + @docstrings.dedent + def unshare(self, **kwargs): + """ + Unshare the formatoptions of all the plotters in this instance + + This method uses the :meth:`psyplot.plotter.Plotter.unshare_me` + method to release the specified formatoptions in `keys`. + + Parameters + ---------- + %(Plotter.unshare_me.parameters)s + + See Also + -------- + psyplot.plotter.Plotter.unshare, psyplot.plotter.Plotter.unshare_me""" + for plotter in self.plotters: + plotter.unshare_me(**kwargs)
+ + + docstrings.delete_params("ArrayList.array_info.parameters", "pwd", "copy") + +
+[docs] + @docstrings.get_sections(base="Project.save_project") + @docstrings.dedent + def save_project(self, fname=None, pwd=None, pack=False, **kwargs): + """ + Save this project to a file + + Parameters + ---------- + fname: str or None + If None, the dictionary will be returned. Otherwise the necessary + information to load this project via the :meth:`load` method is + saved to `fname` using the :mod:`pickle` module + pwd: str or None, optional + Path to the working directory from where the data can be imported. + If None and `fname` is the path to a file, `pwd` is set to the + directory of this file. Otherwise the current working directory is + used. + pack: bool + If True, all datasets are packed into the folder of `fname` + and will be used if the data is loaded + %(ArrayList.array_info.parameters.no_pwd|copy)s + + Notes + ----- + You can also store the entire data in the pickled file by setting + ``ds_description={'ds'}``""" + # store the figure informatoptions and array informations + if fname is not None and pwd is None and not pack: + pwd = os.path.dirname(fname) + if pack and fname is not None: + target_dir = os.path.dirname(fname) + if not os.path.exists(target_dir): + os.makedirs(target_dir, exist_ok=True) + + def tmp_it(): + from tempfile import NamedTemporaryFile + + while True: + yield NamedTemporaryFile(dir=target_dir, suffix=".nc").name + + kwargs.setdefault("paths", tmp_it()) + if fname is not None: + kwargs["copy"] = True + + _update_versions() + ret = { + "figs": dict(map(_ProjectLoader.inspect_figure, self.figs)), + "arrays": self.array_info(pwd=pwd, **kwargs), + "versions": _deepcopy(_versions), + } + if pack and fname is not None: + # we get the filenames out of the results and copy the datasets + # there. After that we check the filenames again and force them + # to the desired directory + from shutil import copyfile + + fnames = (f[0] for f in self._get_dsnames(ret["arrays"])) + alternative_paths = kwargs.pop("alternative_paths", {}) + counters = defaultdict(int) + if kwargs.get("use_rel_paths", True): + get_path = partial(os.path.relpath, start=target_dir) + else: + get_path = os.path.abspath + for ds_fname in unique_everseen(chain(alternative_paths, fnames)): + if ds_fname is None or utils.is_remote_url(ds_fname): + continue + dst_file = alternative_paths.get( + ds_fname, + os.path.join(target_dir, os.path.basename(ds_fname)), + ) + orig_dst_file = dst_file + if counters[dst_file] and ( + not os.path.exists(dst_file) + or not os.path.samefile(ds_fname, dst_file) + ): + dst_file, ext = os.path.splitext(dst_file) + dst_file += "-" + str(counters[orig_dst_file]) + ext + if not os.path.exists(dst_file) or not os.path.samefile( + ds_fname, dst_file + ): + copyfile(ds_fname, dst_file) + counters[orig_dst_file] += 1 + alternative_paths.setdefault(ds_fname, get_path(dst_file)) + ret["arrays"] = self.array_info( + pwd=pwd, alternative_paths=alternative_paths, **kwargs + ) + # store the plotter settings + for arr, d in zip(self, six.itervalues(ret["arrays"])): + if arr.psy.plotter is None: + continue + plotter = arr.psy.plotter + d["plotter"] = { + "ax": _ProjectLoader.inspect_axes(plotter.ax), + "fmt": { + key: getattr(plotter, key).value2pickle for key in plotter + }, + "cls": ( + plotter.__class__.__module__, + plotter.__class__.__name__, + ), + "shared": {}, + } + d["plotter"]["ax"]["shared"] = set( + other.psy.arr_name + for other in self + if other.psy.ax == plotter.ax + ) + if plotter.ax._sharex: + d["plotter"]["ax"]["sharex"] = next( + ( + other.psy.arr_name + for other in self + if other.psy.ax == plotter.ax._sharex + ), + None, + ) + if plotter.ax._sharey: + d["plotter"]["ax"]["sharey"] = next( + ( + other.psy.arr_name + for other in self + if other.psy.ax == plotter.ax._sharey + ), + None, + ) + shared = d["plotter"]["shared"] + for fmto in plotter._fmtos: + if fmto.shared: + shared[fmto.key] = [ + other_fmto.plotter.data.psy.arr_name + for other_fmto in fmto.shared + ] + if fname is not None: + with open(fname, "wb") as f: + pickle.dump(ret, f) + return None + + return ret
+ + +
+[docs] + @docstrings.dedent + def keys(self, *args, **kwargs): + """ + Show the available formatoptions in this project + + Parameters + ---------- + %(Plotter.show_keys.parameters)s + + Other Parameters + ---------------- + %(Plotter.show_keys.other_parameters)s + + Returns + ------- + %(Plotter.show_keys.returns)s""" + + class TmpClass(Plotter): + pass + + for fmto in self._fmtos: + setattr(TmpClass, fmto.key, type(fmto)(fmto.key)) + return TmpClass.show_keys(*args, **kwargs)
+ + +
+[docs] + @docstrings.dedent + def summaries(self, *args, **kwargs): + """ + Show the available formatoptions and their summaries in this project + + Parameters + ---------- + %(Plotter.show_keys.parameters)s + + Other Parameters + ---------------- + %(Plotter.show_keys.other_parameters)s + + Returns + ------- + %(Plotter.show_keys.returns)s""" + + class TmpClass(Plotter): + pass + + for fmto in self._fmtos: + setattr(TmpClass, fmto.key, type(fmto)(fmto.key)) + return TmpClass.show_summaries(*args, **kwargs)
+ + +
+[docs] + @docstrings.dedent + def docs(self, *args, **kwargs): + """ + Show the available formatoptions in this project and their full docu + + Parameters + ---------- + %(Plotter.show_keys.parameters)s + + Other Parameters + ---------------- + %(Plotter.show_keys.other_parameters)s + + Returns + ------- + %(Plotter.show_keys.returns)s""" + + class TmpClass(Plotter): + pass + + for fmto in self._fmtos: + setattr(TmpClass, fmto.key, type(fmto)(fmto.key)) + return TmpClass.show_docs(*args, **kwargs)
+ + +
+[docs] + @classmethod + @docstrings.with_indent(8) + def from_dataset(cls, *args, **kwargs): + """Construct an ArrayList instance from an existing base dataset + + Parameters + ---------- + %(ArrayList.from_dataset.parameters)s + main: Project + The main project that this project corresponds to + + Other Parameters + ---------------- + %(ArrayList.from_dataset.other_parameters)s + + Returns + ------- + Project + The newly created project instance + """ + main = kwargs.pop("main", None) + ret = super(Project, cls).from_dataset(*args, **kwargs) + if main is not None: + ret.main = main + main.extend(ret, new_name=False) + return ret
+ + + docstrings.delete_params("ArrayList.from_dict.parameters", "d", "pwd") + docstrings.keep_params("Project._add_data.parameters", "make_plot") + docstrings.keep_params("Project._add_data.parameters", "clear") + +
+[docs] + @classmethod + @docstrings.get_sections(base="Project.load_project") + @docstrings.dedent + def load_project( + cls, + fname, + auto_update=None, + make_plot=True, + draw=False, + alternative_axes=None, + main=False, + encoding=None, + enable_post=False, + new_fig=True, + clear=None, + **kwargs, + ): + """ + Load a project from a file or dict + + This classmethod allows to load a project that has been stored using + the :meth:`save_project` method and reads all the data and creates the + figures. + + Since the data is stored in external files when saving a project, + make sure that the data is accessible under the relative paths + as stored in the file `fname` or from the current working directory + if `fname` is a dictionary. Alternatively use the `alternative_paths` + parameter or the `pwd` parameter + + Parameters + ---------- + fname: str or dict + The string might be the path to a file created with the + :meth:`save_project` method, or it might be a dictionary from this + method + %(InteractiveBase.parameters.auto_update)s + %(Project._add_data.parameters.make_plot)s + %(InteractiveBase.start_update.parameters.draw)s + alternative_axes: dict, None or list + alternative axes instances to use + + - If it is None, the axes and figures from the saving point will be + reproduced. + - a dictionary should map from array names in the created + project to matplotlib axes instances + - a list should contain axes instances that will be used for + iteration + main: bool, optional + If True, a new main project is created and returned. + Otherwise (by default default) the data is added to the current + main project. + encoding: str + The encoding to use for loading the project. If None, it is + automatically determined by pickle. Note: Set this to ``'latin1'`` + if using a project created with python2 on python3. + enable_post: bool + If True, the :attr:`~psyplot.plotter.Plotter.post` formatoption is + enabled and post processing scripts are allowed. Do only set this + parameter to ``True`` if you know you can trust the information in + `fname` + new_fig: bool + If True (default) and `alternative_axes` is None, new figures are + created if the figure already exists + %(Project._add_data.parameters.clear)s + pwd: str or None, optional + Path to the working directory from where the data can be imported. + If None and `fname` is the path to a file, `pwd` is set to the + directory of this file. Otherwise the current working directory is + used. + %(ArrayList.from_dict.parameters.no_d|pwd)s + + Other Parameters + ---------------- + %(ArrayList.from_dict.parameters)s + + Returns + ------- + Project + The project in state of the saving point""" + from pkg_resources import iter_entry_points + + def get_ax_base(name, alternatives): + ax_base = next(iter(obj(arr_name=name).axes), None) + if ax_base is None: + ax_base = next(iter(obj(arr_name=alternatives).axes), None) + if ax_base is not None: + alternatives.difference_update(obj(ax=ax_base).arr_names) + return ax_base + + def join_axes(grouper_view, *axes): + """Join multiple axes together""" + try: + grouper = grouper_view._grouper + except AttributeError: # matplotlib 3.5 + grouper = grouper_view + grouper.join(*axes) + + pwd = kwargs.pop("pwd", None) + if isinstance(fname, six.string_types): + with open(fname, "rb") as f: + pickle_kws = {} if not encoding else {"encoding": encoding} + d = pickle.load(f, **pickle_kws) + pwd = pwd or os.path.dirname(fname) + else: + d = dict(fname) + pwd = pwd or os.getcwd() + # check for patches of plugins + for ep in iter_entry_points("psyplot", name="patches"): + patches = ep.load() + for arr_d in d.get("arrays").values(): + plotter_cls = arr_d.get("plotter", {}).get("cls") + if plotter_cls is not None and plotter_cls in patches: + # apply the patch + patches[plotter_cls]( + arr_d["plotter"], d.get("versions", {}) + ) + fig_map = {} + if alternative_axes is None: + for fig_dict in six.itervalues(d.get("figs", {})): + orig_num = fig_dict.get("num") or 1 + fig_map[orig_num] = _ProjectLoader.load_figure( + fig_dict, new_fig=new_fig + ).number + elif not isinstance(alternative_axes, dict): + alternative_axes = cycle(iter(alternative_axes)) + obj = cls.from_dict(d["arrays"], pwd=pwd, **kwargs) + if main: + # we create a new project with the project factory to make sure + # that everything is handled correctly + obj = project(None, obj) + axes = {} + arr_names = obj.arr_names + sharex = defaultdict(set) + sharey = defaultdict(set) + for arr, (arr_name, arr_dict) in zip( + obj, + filter(lambda t: t[0] in arr_names, six.iteritems(d["arrays"])), + ): + if not arr_dict.get("plotter"): + continue + plot_dict = arr_dict["plotter"] + plotter_cls = getattr( + import_module(plot_dict["cls"][0]), plot_dict["cls"][1] + ) + ax = None + if alternative_axes is not None: + if isinstance(alternative_axes, dict): + ax = alternative_axes.get(arr.arr_name) + else: + ax = next(alternative_axes, None) + if ax is None and "ax" in plot_dict: + already_opened = ( + plot_dict["ax"].get("shared", set()).intersection(axes) + ) + if already_opened: + ax = axes[next(iter(already_opened))] + else: + plot_dict["ax"].pop("shared", None) + plot_dict["ax"]["fig"] = fig_map[ + plot_dict["ax"].get("fig") or 1 + ] + if plot_dict["ax"].get("sharex"): + sharex[plot_dict["ax"].pop("sharex")].add( + arr.psy.arr_name + ) + if plot_dict["ax"].get("sharey"): + sharey[plot_dict["ax"].pop("sharey")].add( + arr.psy.arr_name + ) + axes[arr.psy.arr_name] = ax = _ProjectLoader.load_axes( + plot_dict["ax"] + ) + plotter_cls( + arr, + make_plot=False, + draw=False, + clear=False, + ax=ax, + project=obj.main, + enable_post=enable_post, + **plot_dict["fmt"], + ) + # handle shared x and y-axes + for key, names in sharex.items(): + ax_base = get_ax_base(key, names) + if ax_base is not None: + join_axes( + ax_base.get_shared_x_axes(), + ax_base, + *obj(arr_name=names).axes, + ) + for ax in obj(arr_name=names).axes: + ax._sharex = ax_base + for key, names in sharey.items(): + ax_base = get_ax_base(key, names) + if ax_base is not None: + join_axes( + ax_base.get_shared_y_axes(), + ax_base, + *obj(arr_name=names).axes, + ) + for ax in obj(arr_name=names).axes: + ax._sharey = ax_base + for arr in obj.with_plotter: + shared = d["arrays"][arr.psy.arr_name]["plotter"].get("shared", {}) + for key, arr_names in six.iteritems(shared): + arr.psy.plotter.share( + obj(arr_name=arr_names).plotters, keys=[key] + ) + if make_plot: + for plotter in obj.plotters: + plotter.reinit( + draw=False, + clear=clear + or ( + clear is None + and plotter_cls._get_sample_projection() is not None + ), + ) + if draw is None: + draw = rcParams["auto_draw"] + if draw: + obj.draw() + if rcParams["auto_show"]: + obj.show() + if auto_update is None: + auto_update = rcParams["lists.auto_update"] + if not main: + obj._main = gcp(True) + obj.main.extend(obj, new_name=True) + obj.no_auto_update = not auto_update + scp(obj) + return obj
+ + +
+[docs] + @classmethod + @docstrings.get_sections(base="Project.scp") + @dedent + def scp(cls, project): + """ + Set the current project + + Parameters + ---------- + project: Project or None + The project to set. If it is None, the current subproject is set + to empty. If it is a sub project (see:attr:`Project.is_main`), + the current subproject is set to this project. Otherwise it + replaces the current main project + + See Also + -------- + scp: The global version for setting the current project + gcp: Returns the current project + project: Creates a new project""" + if project is None: + _scp(None) + cls.oncpchange.emit(gcp()) + elif not project.is_main: + if project.main is not _current_project: + _scp(project.main, True) + cls.oncpchange.emit(project.main) + _scp(project) + cls.oncpchange.emit(project) + else: + _scp(project, True) + cls.oncpchange.emit(project) + sp = project[:] + _scp(sp) + cls.oncpchange.emit(sp)
+ + + docstrings.delete_params("Project.parameters", "num") + +
+[docs] + @classmethod + @docstrings.dedent + def new(cls, num=None, *args, **kwargs): + """ + Create a new main project + + Parameters + ---------- + num: int + The number of the project + %(Project.parameters.no_num)s + + Returns + ------- + Project + The with the given `num` (if it does not already exist, it is + created) + + See Also + -------- + scp: Sets the current project + gcp: Returns the current project + """ + project = cls(*args, num=num, **kwargs) + scp(project) + return project
+ + + def __str__(self): + return (("%i Main " % self.num) if self.is_main else "") + super( + Project, self + ).__str__()
+ + + +class _ProjectLoader(object): + """Class to inspect a project and reproduce it""" + + @staticmethod + def inspect_figure(fig): + """Get the parameters (heigth, width, etc.) to create a figure + + This method returns the number of the figure and a dictionary + containing the necessary information for the + :func:`matplotlib.pyplot.figure` function""" + return fig.number, { + "num": fig.number, + "figsize": (fig.get_figwidth(), fig.get_figheight()), + "dpi": fig.get_dpi() / getattr(fig.canvas, "_dpi_ratio", 1), + "facecolor": fig.get_facecolor(), + "edgecolor": fig.get_edgecolor(), + "frameon": fig.get_frameon(), + "tight_layout": fig.get_tight_layout(), + "subplotpars": vars(fig.subplotpars), + } + + @staticmethod + def load_figure(d, new_fig=True): + """Create a figure from what is returned by :meth:`inspect_figure`""" + import matplotlib.pyplot as plt + + subplotpars = d.pop("subplotpars", None) + if subplotpars is not None: + subplotpars.pop("validate", None) + subplotpars.pop("_validate", None) + subplotpars = mfig.SubplotParams(**subplotpars) + if new_fig: + nums = plt.get_fignums() + if d.get("num") in nums: + d["num"] = next( + i + for i in range(max(plt.get_fignums()) + 1, 0, -1) + if i not in nums + ) + return plt.figure(subplotpars=subplotpars, **d) + + @staticmethod + def inspect_axes(ax): + """Inspect an axes or subplot to get the initialization parameters""" + ret = {"fig": ax.get_figure().number} + if mpl.__version__ < "2.0": + ret["axisbg"] = ax.get_axis_bgcolor() + else: # axisbg is depreceated + ret["facecolor"] = ax.get_facecolor() + proj = getattr(ax, "projection", None) + if proj is not None and not isinstance(proj, six.string_types): + proj = (proj.__class__.__module__, proj.__class__.__name__) + ret["projection"] = proj + ret["visible"] = ax.get_visible() + ret["spines"] = {} + ret["zorder"] = ax.get_zorder() + ret["yaxis_inverted"] = ax.yaxis_inverted() + ret["xaxis_inverted"] = ax.xaxis_inverted() + for key, val in ax.spines.items(): + ret["spines"][key] = {} + for prop in [ + "linestyle", + "edgecolor", + "linewidth", + "facecolor", + "visible", + ]: + ret["spines"][key][prop] = getattr(val, "get_" + prop)() + if isinstance(ax, SubplotBase): + sp = ax.get_subplotspec().get_topmost_subplotspec() + ret["grid_spec"] = sp.get_geometry()[:2] + ret["subplotspec"] = [sp.num1, sp.num2] + ret["is_subplot"] = True + else: + ret["args"] = [ax.get_position(True).bounds] + ret["is_subplot"] = False + return ret + + @staticmethod + def load_axes(d): + """Create an axes or subplot from what is returned by + :meth:`inspect_axes`""" + import matplotlib.pyplot as plt + + fig = plt.figure(d.pop("fig", None)) + proj = d.pop("projection", None) + spines = d.pop("spines", None) + invert_yaxis = d.pop("yaxis_inverted", None) + invert_xaxis = d.pop("xaxis_inverted", None) + if mpl.__version__ >= "2.0" and "axisbg" in d: # axisbg is depreceated + d["facecolor"] = d.pop("axisbg") + elif mpl.__version__ < "2.0" and "facecolor" in d: + d["axisbg"] = d.pop("facecolor") + if proj is not None and not isinstance(proj, six.string_types): + proj = getattr(import_module(proj[0]), proj[1])() + if d.pop("is_subplot", None): + grid_spec = mpl.gridspec.GridSpec(*d.pop("grid_spec", (1, 1))) + subplotspec = mpl.gridspec.SubplotSpec( + grid_spec, *d.pop("subplotspec", (1, None)) + ) + return fig.add_subplot(subplotspec, projection=proj, **d) + ret = fig.add_axes(*d.pop("args", []), projection=proj, **d) + if spines is not None: + for key, val in spines.items(): + ret.spines[key].update(val) + if invert_xaxis: + if ret.get_xlim()[0] < ret.get_xlim()[1]: + ret.invert_xaxis() + if invert_yaxis: + if ret.get_ylim()[0] < ret.get_ylim()[1]: + ret.invert_yaxis() + return ret + + +
+[docs] +class ProjectPlotter(object): + """Plotting methods of the :class:`psyplot.project.Project` class""" + + #: the base class for new plot methods. Is set below with the + #: :class:`PlotterInterface` class + _plot_method_base_cls = None + + @property + def project(self): + return self._project if self._project is not None else gcp(True) + + def __init__(self, project=None): + self._project = project + + docstrings.keep_params( + "ArrayList.from_dataset.parameters", "default_slice" + ) + + @property + def _plot_methods(self): + """A dictionary with mappings from plot method to their summary""" + ret = {} + for attr in filter(lambda s: not s.startswith("_"), dir(self)): + obj = getattr(self, attr) + if isinstance(obj, PlotterInterface): + ret[attr] = obj._summary + return ret + +
+[docs] + def show_plot_methods(self): + """Print the plotmethods of this instance""" + print_func = PlotterInterface._print_func + if print_func is None: + print_func = six.print_ + s = "\n".join( + "%s\n %s" % t for t in six.iteritems(self._plot_methods) + ) + return print_func(s)
+ + + @docstrings.get_sections( + base="ProjectPlotter._add_data", + sections=["Parameters", "Other Parameters", "Returns"], + ) + @docstrings.dedent + def _add_data(self, *args, **kwargs): + """ + Add new plots to the project + + Parameters + ---------- + %(Project._add_data.parameters)s + + Other Parameters + ---------------- + %(Project._add_data.other_parameters)s + + Returns + ------- + %(Project._add_data.returns)s + """ + # this method is just a shortcut to the :meth:`Project._add_data` + # method but is reimplemented by subclasses as the + # :class:`DatasetPlotter` or the :class:`DataArrayPlotter` + return self.project._add_data(*args, **kwargs) + + @classmethod + @docstrings.get_sections(base="ProjectPlotter._register_plotter") + @docstrings.dedent + def _register_plotter( + cls, + identifier, + module, + plotter_name, + plotter_cls=None, + summary="", + prefer_list=False, + default_slice=None, + default_dims={}, + show_examples=True, + example_call="filename, name=['my_variable'], ...", + plugin=None, + ): + """ + Register a plotter for making plots + + This class method registeres a plot function for the :class:`Project` + class under the name of the given `identifier` + + Parameters + ---------- + %(Project._register_plotter.parameters)s + + Other Parameters + ---------------- + prefer_list: bool + Determines the `prefer_list` parameter in the `from_dataset` + method. If True, the plotter is expected to work with instances of + :class:`psyplot.InteractiveList` instead of + :class:`psyplot.InteractiveArray`. + %(ArrayList.from_dataset.parameters.default_slice)s + default_dims: dict + Default dimensions that shall be used for plotting (e.g. + {'x': slice(None), 'y': slice(None)} for longitude-latitude plots) + show_examples: bool, optional + If True, examples how to access the plotter documentation are + included in class documentation + example_call: str, optional + The arguments and keyword arguments that shall be included in the + example of the generated plot method. This call will then appear as + ``>>> psy.plot.%%(identifier)s(%%(example_call)s)`` in the + documentation + plugin: str + The name of the plugin + """ + full_name = "%s.%s" % (module, plotter_name) + if plotter_cls is not None: # plotter has already been imported + docstrings.params[ + "%s.formatoptions" % (full_name) + ] = plotter_cls.show_keys( + indent=4, + func=str, + # include links in sphinx doc + include_links=None, + ) + doc_str = ( + "Possible formatoptions are\n\n" "%%(%s.formatoptions)s" + ) % full_name + else: + doc_str = "" + + summary = summary or ( + "Open and plot data via :class:`%s.%s` plotters" + % (module, plotter_name) + ) + + if plotter_cls is not None: + _versions.update(get_versions(key=lambda s: s == plugin)) + + class PlotMethod(cls._plot_method_base_cls): + __doc__ = cls._gen_doc( + summary, + full_name, + identifier, + example_call, + doc_str, + show_examples, + ) + + _default_slice = default_slice + _default_dims = default_dims + _plotter_cls = plotter_cls + _prefer_list = prefer_list + _plugin = plugin + + _summary = summary + + setattr(cls, identifier, PlotMethod(identifier, module, plotter_name)) + + @classmethod + def _gen_doc( + cls, + summary, + full_name, + identifier, + example_call, + doc_str, + show_examples, + ): + """Generate the documentation docstring for a PlotMethod""" + ret = docstrings.dedent( + """ + %s + + This plotting method adds data arrays and plots them via + :class:`%s` plotters + + To plot data from a netCDF file type:: + + >>> psy.plot.%s(%s) + + %s""" + % (summary, full_name, identifier, example_call, doc_str) + ) + + if show_examples: + ret += "\n\n" + cls._gen_examples(identifier) + return ret + + @classmethod + def _gen_examples(cls, identifier): + """Generate examples how to axes the formatoption docs""" + return docstrings.dedent( + """ + Examples + -------- + To explore the formatoptions and their documentations, use the + ``keys``, ``summaries`` and ``docs`` methods. For example:: + + >>> import psyplot.project as psy + + # show the keys corresponding to a group or multiple + # formatopions + >>> psy.plot.%(id)s.keys('labels') + + # show the summaries of a group of formatoptions or of a + # formatoption + >>> psy.plot.%(id)s.summaries('title') + + # show the full documentation + >>> psy.plot.%(id)s.docs('plot') + + # or access the documentation via the attribute + >>> psy.plot.%(id)s.plot""" + % {"id": identifier} + )
+ + + +
+[docs] +class PlotterInterface(object): + """Base class for visualizing a data array from an predefined plotter + + See the :meth:`__call__` method for details on plotting.""" + + @property + def _logger(self): + name = "%s.%s.%s" % ( + self.__module__, + self.__class__.__name__, + self._method, + ) + return logging.getLogger(name) + + @property + def is_imported(self): + """True if the module for this plot method has been imported already""" + return self.module in sys.modules + + @property + def plotter_cls(self): + """The plotter class""" + ret = self._plotter_cls + if ret is None: + self._logger.debug("importing %s", self.module) + mod = import_module(self.module) + plotter = self.plotter_name + if plotter not in vars(mod): + raise ImportError( + "Module %r does not have a %r plotter!" % (mod, plotter) + ) + ret = self._plotter_cls = getattr(mod, plotter) + _versions.update(get_versions(key=lambda s: s == self._plugin)) + return ret + + _prefer_list = False + _default_slice = None + _default_dims = {} + + _print_func = None + + @property + def print_func(self): + """The function that is used to return a formatoption + + By default the :func:`print` function is used (i.e. it is printed to + the terminal)""" + return self._print_func or six.print_ + + @print_func.setter + def print_func(self, value): + self._print_func = value + + def __init__(self, methodname, module, plotter_name, project_plotter=None): + self._method = methodname + self._project_plotter = project_plotter + self.module = module + self.plotter_name = plotter_name + + docstrings.delete_params( + "ProjectPlotter._add_data.parameters", "plotter_cls" + ) + + @docstrings.dedent + def __call__(self, *args, **kwargs): + """ + Parameters + ---------- + %(ProjectPlotter._add_data.parameters.no_plotter_cls)s + %(Project._load_preset.parameters.preset)s + + Other Parameters + ---------------- + %(ProjectPlotter._add_data.other_parameters)s + + + Returns + ------- + %(ProjectPlotter._add_data.returns)s + """ + preset = kwargs.pop("preset", None) + if preset: + preset = self._project_plotter.project._load_preset(preset) + if len(args) >= 2: + fmt = args[1] + else: + fmt = kwargs.setdefault("fmt", {}) + for key, val in preset.get(self._method, {}).items(): + fmt.setdefault(key, val) + valid = list(self.plotter_cls._get_formatoptions()) + for key, val in preset.items(): + if key in valid: + fmt.setdefault(key, val) + + return self._project_plotter._add_data( + self.plotter_cls, + *args, + **dict( + chain( + [ + ("prefer_list", self._prefer_list), + ("default_slice", self._default_slice), + ], + six.iteritems(self._default_dims), + six.iteritems(kwargs), + ) + ), + ) + + def __getattr__(self, attr): + if attr in self.plotter_cls._get_formatoptions(): + return partial( + self.print_func, getattr(self.plotter_cls, attr).__doc__ + ) + else: + raise AttributeError( + "%s instance does not have a %s attribute" + % (self.__class__.__name__, attr) + ) + + def __get__(self, instance, owner): + if instance is None: + return self + else: + try: + return getattr(instance, "_" + self._method) + except AttributeError: + setattr( + instance, + "_" + self._method, + self.__class__( + self._method, self.module, self.plotter_name, instance + ), + ) + return getattr(instance, "_" + self._method) + + def __set__(self, instance, value): + """Actually not required. We just implement it to ensure the python + "help" function works well""" + setattr(instance, "_" + self._method, value) + + def __dir__(self): + try: + return sorted( + chain( + dir(self.__class__), + self.__dict__, + self.plotter_cls._get_formatoptions(), + ) + ) + except Exception: + return sorted(chain(dir(self.__class__), self.__dict__)) + +
+[docs] + @docstrings.dedent + def keys(self, *args, **kwargs): + """ + Classmethod to return a nice looking table with the given formatoptions + + Parameters + ---------- + %(Plotter.show_keys.parameters)s + + Other Parameters + ---------------- + %(Plotter.show_keys.other_parameters)s + + Returns + ------- + %(Plotter.show_keys.returns)s + + See Also + -------- + summaries, docs""" + return self.plotter_cls.show_keys(*args, **kwargs)
+ + +
+[docs] + @docstrings.dedent + def summaries(self, *args, **kwargs): + """ + Method to print the summaries of the formatoptions + + Parameters + ---------- + %(Plotter.show_keys.parameters)s + + Other Parameters + ---------------- + %(Plotter.show_keys.other_parameters)s + + Returns + ------- + %(Plotter.show_keys.returns)s + + See Also + -------- + keys, docs""" + return self.plotter_cls.show_summaries(*args, **kwargs)
+ + +
+[docs] + @docstrings.dedent + def docs(self, *args, **kwargs): + """ + Method to print the full documentations of the formatoptions + + Parameters + ---------- + %(Plotter.show_keys.parameters)s + + Other Parameters + ---------------- + %(Plotter.show_keys.other_parameters)s + + Returns + ------- + %(Plotter.show_keys.returns)s + + See Also + -------- + keys, docs""" + return self.plotter_cls.show_docs(*args, **kwargs)
+ + +
+[docs] + @docstrings.dedent + def check_data(self, ds, name, dims, decoder=None, *args, **kwargs): + """ + A validation method for the data shape + + Parameters + ---------- + name: list of lists of strings + The variable names (see the + :meth:`~psyplot.plotter.Plotter.check_data` method of the + :attr:`plotter_cls` attribute for details) + dims: list of dictionaries + The dimensions of the arrays. It will be enhanced by the default + dimensions of this plot method + is_unstructured: bool or list of bool + True if the corresponding array is unstructured. + decoder: :class:`psyplot.data.CFDecoder`, dict or a list of them + The decoders to use per array. Dictionaries are parsed as keyword + arguments to the :meth:`psyplot.data.CFDecoder.get_decoder` + method + + Returns + ------- + %(Plotter.check_data.returns)s + """ + if isinstance(name, six.string_types): + name = [name] + dims = [dims] + decoders = [decoder] + else: + dims = list(dims) + decoders = list(decoder if decoder is not None else [None]) + variables = [ds[safe_list(n)[0]] for n in name] + if decoders is None: + decoders = [CFDecoder.get_decoder(ds, var) for var in variables] + else: + for i, (decoder, var) in enumerate(zip(decoders, variables)): + if decoder is None: + decoder = {} + if isinstance(decoder, dict): + decoders[i] = CFDecoder.get_decoder(ds, var, **decoder) + default_slice = ( + slice(None) if self._default_slice is None else self._default_slice + ) + for i, (dim_dict, var, decoder) in enumerate( + zip(dims, variables, decoders) + ): + corrected = decoder.correct_dims( + var, + dict( + chain(six.iteritems(self._default_dims), dim_dict.items()) + ), + ) + # now use the default slice (we don't do this before because the + # `correct_dims` method doesn't use 'x', 'y', 'z' and 't' (as used + # for the _default_dims) if the real dimension name is already in + # the dictionary) + for dim in var.dims: + corrected.setdefault(dim, default_slice) + dims[i] = [ + dim + for dim, val in map( + lambda t: (t[0], safe_list(t[1])), six.iteritems(corrected) + ) + if val and (len(val) > 1 or _is_slice(val[0])) + ] + return self.plotter_cls.check_data( + name, + dims, + [ + decoder.is_unstructured(var) + for decoder, var in zip(decoders, variables) + ], + )
+
+ + + +# set the base class for the :class:`ProjectPlotter` plot methods +ProjectPlotter._plot_method_base_cls = PlotterInterface + + +
+[docs] +class DatasetPlotterInterface(PlotterInterface): + """Interface for the :class:`DatasetPlotter` to a plotter""" + + # there are not changes here compared to :class:`PlotterInterface`, except + # for a different docstring for the __call__ method + + docstrings.delete_params( + "ProjectPlotter._add_data.parameters", "plotter_cls", "filename_or_obj" + ) + + @docstrings.dedent + def __call__(self, *args, **kwargs): + """ + Parameters + ---------- + %(ProjectPlotter._add_data.parameters.no_plotter_cls|filename_or_obj)s + + Other Parameters + ---------------- + %(ProjectPlotter._add_data.other_parameters)s + + + Returns + ------- + %(ProjectPlotter._add_data.returns)s + """ + return super(DatasetPlotterInterface, self).__call__(*args, **kwargs)
+ + + +
+[docs] +class DatasetPlotter(ProjectPlotter): + """Interface between the :class:`xarray.Dataset` and the psyplot project + + This class can be used to make new plots from a given dataset and add them + to the current :func:`psyplot.project` + """ + + _plot_method_base_cls = DatasetPlotterInterface + + def __init__(self, ds, *args, **kwargs): + super(DatasetPlotter, self).__init__(*args, **kwargs) + self._ds = ds + + docstrings.delete_params( + "ProjectPlotter._add_data.parameters", "filename_or_obj" + ) + + @docstrings.get_sections( + base="ProjectPlotter._add_data", + sections=["Parameters", "Other Parameters", "Returns"], + ) + @docstrings.dedent + def _add_data(self, plotter_cls, *args, **kwargs): + """ + Add new plots to the project + + Parameters + ---------- + %(ProjectPlotter._add_data.parameters.no_filename_or_obj)s + + Other Parameters + ---------------- + %(ProjectPlotter._add_data.other_parameters)s + + Returns + ------- + %(ProjectPlotter._add_data.returns)s + """ + # this method is just a shortcut to the :meth:`Project._add_data` + # method but is reimplemented by subclasses as the + # :class:`DatasetPlotter` or the :class:`DataArrayPlotter` + return super(DatasetPlotter, self)._add_data( + plotter_cls, self._ds, *args, **kwargs + ) + + @classmethod + def _gen_doc( + cls, + summary, + full_name, + identifier, + example_call, + doc_str, + show_examples, + ): + """Generate the documentation docstring for a PlotMethod""" + # leave out the first argument + example_call = ", ".join(map(str.strip, example_call.split(",")[1:])) + ret = docstrings.dedent( + """ + %s + + This plotting method adds data arrays and plots them via + :class:`%s` plotters + + To plot a variable in this dataset, type:: + + >>> ds.psy.plot.%s(%s) + + %s""" + % (summary, full_name, identifier, example_call, doc_str) + ) + + if show_examples: + ret += "\n\n" + cls._gen_examples(identifier) + return ret + + @classmethod + def _gen_examples(cls, identifier): + """Generate examples how to axes the formatoption docs""" + return docstrings.dedent( + """ + Examples + -------- + To explore the formatoptions and their documentations, use the + ``keys``, ``summaries`` and ``docs`` methods. For example:: + + # show the keys corresponding to a group or multiple + # formatopions + >>> ds.psy.plot.%(id)s.keys('labels') + + # show the summaries of a group of formatoptions or of a + # formatoption + >>> ds.psy.plot.%(id)s.summaries('title') + + # show the full documentation + >>> ds.psy.plot.%(id)s.docs('plot') + + # or access the documentation via the attribute + >>> ds.psy.plot.%(id)s.plot""" + % {"id": identifier} + )
+ + + +
+[docs] +class DataArrayPlotterInterface(PlotterInterface): + """Interface for the :class:`DataArrayPlotter` to a plotter""" + + # we reimplement the call method because we do not use the + # prefer_list, etc. keywords. And we reimplment the check_data method + # because we use the data array directly + + docstrings.delete_params("Plotter.parameters", "data") + + @docstrings.dedent + def __call__(self, *args, **kwargs): + """ + Parameters + ---------- + %(Plotter.parameters.no_data)s + + + Returns + ------- + psyplot.plotter.Plotter + The plotter that visualizes the data + """ + checks, messages = self.check_data() + if not all(checks): + raise ValueError( + "Cannot visualize the data using %s! Reasons:\n %s" + % (self.plotter_name, "\n ".join(filter(None, messages))) + ) + return self._project_plotter._add_data( + self.plotter_cls, *args, **kwargs + ) + +
+[docs] + def check_data(self, *args, **kwargs): + """Check whether the plotter of this plot method can visualize the data""" + plotter_cls = self.plotter_cls + da_list = self._project_plotter._da.psy.to_interactive_list() + return plotter_cls.check_data( + da_list.all_names, da_list.all_dims, da_list.is_unstructured + )
+
+ + + +
+[docs] +class DataArrayPlotter(ProjectPlotter): + """Interface between the :class:`xarray.Dataset` and the psyplot project + + This class can be used to make new plots from a given dataset and add them + to the current :func:`psyplot.project` + """ + + _plot_method_base_cls = DataArrayPlotterInterface + + def __init__(self, da, *args, **kwargs): + super(DataArrayPlotter, self).__init__(*args, **kwargs) + self._da = getattr(da, "arr", da) + + @docstrings.dedent + def _add_data(self, plotter_cls, *args, **kwargs): + """ + Visualize this data array + + Parameters + ---------- + %(Plotter.parameters.no_data)s + + Returns + ------- + psyplot.plotter.Plotter + The plotter that visualizes the data + """ + # this method is just a shortcut to the :meth:`Project._add_data` + # method but is reimplemented by subclasses as the + # :class:`DatasetPlotter` or the :class:`DataArrayPlotter` + return plotter_cls(self._da, *args, **kwargs) + + @classmethod + def _gen_doc( + cls, + summary, + full_name, + identifier, + example_call, + doc_str, + show_examples, + ): + """Generate the documentation docstring for a PlotMethod""" + # leave out the first argument + example_call = ", ".join(map(str.strip, example_call.split(",")[1:])) + ret = docstrings.dedent( + """ + %s + + This plotting method visualizes the data via a + :class:`%s` plotters + + To plot a variable in this dataset, type:: + + >>> da.psy.plot.%s() + + %s""" + % (summary, full_name, identifier, doc_str) + ) + + if show_examples: + ret += "\n\n" + cls._gen_examples(identifier) + return ret + + @classmethod + def _gen_examples(cls, identifier): + """Generate examples how to axes the formatoption docs""" + return docstrings.dedent( + """ + Examples + -------- + To explore the formatoptions and their documentations, use the + ``keys``, ``summaries`` and ``docs`` methods. For example:: + + # show the keys corresponding to a group or multiple + # formatopions + >>> da.psy.plot.%(id)s.keys('labels') + + # show the summaries of a group of formatoptions or of a + # formatoption + >>> da.psy.plot.%(id)s.summaries('title') + + # show the full documentation + >>> da.psy.plot.%(id)s.docs('plot') + + # or access the documentation via the attribute + >>> da.psy.plot.%(id)s.plot""" + % {"id": identifier} + )
+ + + +if with_cdo: + CDF_MOD_NCREADER = "xarray" + + docstrings.keep_params( + "Project._add_data.parameters", + "dims", + "fmt", + "ax", + "make_plot", + "method", + ) + + class Cdo(_CdoBase): + __doc__ = docstrings.dedent( + """ + Subclass of the original cdo.Cdo class in the cdo.py module + + Requirements are a working cdo binary and the installed cdo.py + python module. + + For a documentation of an operator, use the python help function, + for a list of operators, use the builtin dir function. + Further documentation on the operators can be found here: + https://code.zmaw.de/projects/cdo/wiki/Cdo%7Brbpy%7D + and on the usage of the cdo.py module here: + https://code.zmaw.de/projects/cdo + + For a demonstration script on how cdos are implemented, see the + examples of the psyplot package + + Compared to the original cdo.Cdo class, the following things + changed, the default cdf handler is the + :func:`psyplot.data.open_dataset` function and the following + keywords are implemented for each cdo operator. If any of those is + specified, the return will be a subproject (i.e. an instance of + :class:`psyplot.project.Project`) + + Other Parameters + ---------------- + plot_method: str or psyplot.project.PlotterInterface + An registered plotting function to plot the data (e.g. + `psyplot.project.plot.mapplot` to plot on a map). If ``None``, + no plot will be created. In any case, the returned value is a + subproject. If string, it must correspond to the attribute of + the :class:`psyplot.project.ProjectPlotter` class + name: str or list of str + The variable names to plot/extract + %(Project._add_data.parameters.dims|fmt|ax|make_plot|method)s + + Examples + -------- + Calculate the timmean of a 3-dimensional array and plot it on a map + using the psy-maps package + + .. code-block:: python + + cdo = psy.Cdo() + sp = cdo.timmean(input='ifile.nc', name='temperature', + plot_method='mapplot') + + which is essentially the same as + + .. code-block:: python + + sp = cdo.timmean(input='ifile.nc', name='temperature', + plot_method=psy.plot.mapplot) + # and + sp = psy.plot.mapplot( + cdo.timmean(input='ifile.nc', returnCdf=True), + name='temperature', plot_method=psy.plot.mapplot) + """ + ) + + def __init__(self, *args, **kwargs): + if cdo_version < (1, 5): + kwargs.setdefault("cdfMod", CDF_MOD_NCREADER) + super(Cdo, self).__init__(*args, **kwargs) + if cdo_version < (1, 5): + self.loadCdf() + + def loadCdf(self, *args, **kwargs): + """Load data handler as specified by self.cdfMod""" + if cdo_version < (1, 5): + + def open_nc(*args, **kwargs): + kwargs.pop("mode", None) + return open_dataset(*args, **kwargs) + + if self.cdfMod == CDF_MOD_NCREADER: + self.cdf = open_nc + else: + super(Cdo, self).loadCdf(*args, **kwargs) + else: + super(Cdo, self).readCdf(*args, **kwargs) + + def __getattr__(self, method_name): + def my_get(get): + """Wrapper for get method of Cdo class to include several plotters""" + + @wraps(get) + def wrapper(self, *args, **kwargs): + added_kwargs = {"plot_method", "name", "dims", "fmt"} + if added_kwargs.intersection(kwargs): + plot_method = kwargs.pop("plot_method", None) + ax = kwargs.pop("ax", None) + make_plot = kwargs.pop("make_plot", True) + fmt = kwargs.pop("fmt", {}) + dims = kwargs.pop("dims", {}) + name = kwargs.pop("name", None) + method = kwargs.pop("method", "isel") + if cdo_version < (1, 5): + kwargs["returnCdf"] = True + else: + kwargs["returnXDataset"] = True + ds = get(*args, **kwargs) + if isinstance(plot_method, six.string_types): + plot_method = getattr(plot, plot_method) + if plot_method is None: + ret = Project.from_dataset( + ds, name=name, dims=dims, method=method + ) + ret.main = gcp(True) + return ret + else: + return plot_method( + ds, + name=name, + fmt=fmt, + dims=dims, + ax=ax, + make_plot=make_plot, + method=method, + ) + else: + return get(*args, **kwargs) + + return wrapper + + get = my_get(super(Cdo, self).__getattr__(method_name)) + setattr(self.__class__, method_name, get) + return get.__get__(self) + + +
+[docs] +@dedent +def gcp(main=False): + """ + Get the current project + + Parameters + ---------- + main: bool + If True, the current main project is returned, otherwise the current + subproject is returned. + See Also + -------- + scp: Sets the current project + project: Creates a new project""" + if main: + return project() if _current_project is None else _current_project + else: + return ( + gcp(True) if _current_subproject is None else _current_subproject + )
+ + + +
+[docs] +@dedent +def scp(project): + """ + Set the current project + + Parameters + ---------- + %(Project.scp.parameters)s + + See Also + -------- + gcp: Returns the current project + project: Creates a new project""" + return PROJECT_CLS.scp(project)
+ + + +def _scp(p, main=False): + """scp version that allows a bit more control over whether the project is a + main project or not""" + global _current_subproject + global _current_project + if p is None: + mp = ( + project() if main or _current_project is None else _current_project + ) + _current_subproject = Project(main=mp) + elif not main: + _current_subproject = p + else: + _current_project = p + + +
+[docs] +@docstrings.dedent +def project(num=None, *args, **kwargs): + """ + Create a new main project + + Parameters + ---------- + num: int + The number of the project + %(Project.parameters.no_num)s + + Returns + ------- + Project + The with the given `num` (if it does not already exist, it is created) + + See Also + -------- + scp: Sets the current project + gcp: Returns the current project + """ + numbers = [project.num for project in _open_projects] + if num in numbers: + return _open_projects[numbers.index(num)] + if num is None: + num = max(numbers) + 1 if numbers else 1 + project = PROJECT_CLS.new(num, *args, **kwargs) + _open_projects.append(project) + return project
+ + + +
+[docs] +@docstrings.dedent +def close(num=None, figs=True, data=True, ds=True, remove_only=False): + """ + Close the project + + This method closes the current project (figures, data and datasets) or the + project specified by `num` + + Parameters + ---------- + num: int, None or 'all' + if :class:`int`, it specifies the number of the project, if None, the + current subproject is closed, if ``'all'``, all open projects are + closed + %(Project.close.parameters)s + + See Also + -------- + Project.close""" + kws = dict(figs=figs, data=data, ds=ds, remove_only=remove_only) + cp_num = gcp(True).num + got_cp = False + if num is None: + project = gcp() + scp(None) + project.close(**kws) + elif num == "all": + for project in _open_projects[:]: + project.close(**kws) + got_cp = got_cp or project.main.num == cp_num + del _open_projects[0] + else: + if isinstance(num, Project): + project = num + else: + project = [ + project for project in _open_projects if project.num == num + ][0] + project.close(**kws) + try: + _open_projects.remove(project) + except ValueError: + pass + got_cp = got_cp or project.main.num == cp_num + if got_cp: + if _open_projects: + # set last opened project to the current + scp(_open_projects[-1]) + else: + _scp(None, True) # set the current project to None
+ + + +docstrings.delete_params("Project._register_plotter.parameters", "plotter_cls") + + +
+[docs] +@docstrings.dedent +def register_plotter( + identifier, + module, + plotter_name, + plotter_cls=None, + sorter=True, + plot_func=True, + import_plotter=None, + **kwargs, +): + """ + Register a :class:`psyplot.plotter.Plotter` for the projects + + This function registers plotters for the :class:`Project` class to allow + a dynamical handling of different plotter classes. + + Parameters + ---------- + %(Project._register_plotter.parameters.no_plotter_cls)s + sorter: bool, optional + If True, the :class:`Project` class gets a new property with the name + of the specified `identifier` which allows you to access the instances + that are plotted by the specified `plotter_name` + plot_func: bool, optional + If True, the :class:`ProjectPlotter` (the class that holds the + plotting method for the :class:`Project` class and can be accessed via + the :attr:`Project.plot` attribute) gets an additional method to plot + via the specified `plotter_name` (see `Other Parameters` below.) + import_plotter: bool, optional + If True, the plotter is automatically imported, otherwise it is only + imported when it is needed. If `import_plotter` is None, then it is + determined by the :attr:`psyplot.rcParams` ``'project.auto_import'`` + item. + + Other Parameters + ---------------- + %(ProjectPlotter._register_plotter.other_parameters)s + """ + if plotter_cls is None: + if ( + import_plotter is None and rcParams["project.auto_import"] + ) or import_plotter: + try: + plotter_cls = getattr(import_module(module), plotter_name) + except Exception as e: + critical( + ("Could not import %s!\n" % module) + e.message + if six.PY2 + else str(e) + ) + return + if sorter: + if hasattr(Project, identifier): + raise ValueError( + "Project class already has a %s attribute" % identifier + ) + Project._register_plotter( + identifier, module, plotter_name, plotter_cls + ) + if plot_func: + if hasattr(ProjectPlotter, identifier): + raise ValueError( + "Project class already has a %s attribute" % identifier + ) + ProjectPlotter._register_plotter( + identifier, module, plotter_name, plotter_cls, **kwargs + ) + DatasetPlotter._register_plotter( + identifier, module, plotter_name, plotter_cls, **kwargs + ) + DataArrayPlotter._register_plotter( + identifier, module, plotter_name, plotter_cls, **kwargs + ) + if identifier not in registered_plotters: + kwargs.update( + dict( + module=module, + plotter_name=plotter_name, + sorter=sorter, + plot_func=plot_func, + import_plotter=import_plotter, + ) + ) + registered_plotters[identifier] = kwargs + return
+ + + +
+[docs] +def unregister_plotter(identifier, sorter=True, plot_func=True): + """ + Unregister a :class:`psyplot.plotter.Plotter` for the projects + + Parameters + ---------- + identifier: str + Name of the attribute that is used to filter for the instances + belonging to this plotter or to create plots with this plotter + sorter: bool + If True, the identifier will be unregistered from the :class:`Project` + class + plot_func: bool + If True, the identifier will be unregistered from the + :class:`ProjectPlotter` class + """ + d = registered_plotters.get(identifier, {}) + if sorter and hasattr(Project, identifier): + delattr(Project, identifier) + d["sorter"] = False + if plot_func and hasattr(ProjectPlotter, identifier): + for cls in [ProjectPlotter, DatasetPlotter, DataArrayPlotter]: + delattr(cls, identifier) + try: + delattr(plot, "_" + identifier) + except AttributeError: + pass + d["plot_func"] = False + if sorter and plot_func: + registered_plotters.pop(identifier, None)
+ + + +registered_plotters = {} + +for _identifier, _plotter_settings in rcParams["project.plotters"].items(): + register_plotter(_identifier, **_plotter_settings) + + +
+[docs] +def get_project_nums(): + """Returns the project numbers of the open projects""" + return [p.num for p in _open_projects]
+ + + +#: :class:`ProjectPlotter` of the current project. See the class documentation +#: for available plotting methods +plot = ProjectPlotter() + +#: The project class that is used for creating new projects +PROJECT_CLS = Project + +psyplot._project_imported = True +
+ +
+
+ + + +
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/psyplot/sphinxext/extended_napoleon.html b/_modules/psyplot/sphinxext/extended_napoleon.html new file mode 100644 index 0000000..a7111dd --- /dev/null +++ b/_modules/psyplot/sphinxext/extended_napoleon.html @@ -0,0 +1,595 @@ + + + + + + psyplot.sphinxext.extended_napoleon — psyplot documentation + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for psyplot.sphinxext.extended_napoleon

+"""Sphinx extension module to provide additional sections for numpy docstrings
+
+This extension extends the :mod:`sphinx.ext.napoleon` package with an
+additional *Possible types* section in order to document possible types for
+descriptors.
+
+Notes
+-----
+If you use this module as a sphinx extension, you should not list the
+:mod:`sphinx.ext.napoleon` module in the extensions variable of your conf.py.
+This module has been tested for sphinx 1.3.1."""
+
+# SPDX-FileCopyrightText: 2016-2024 University of Lausanne
+# SPDX-FileCopyrightText: 2020-2021 Helmholtz-Zentrum Geesthacht
+
+# SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH
+#
+# SPDX-License-Identifier: LGPL-3.0-only
+
+from abc import ABCMeta, abstractmethod
+
+from sphinx.ext.napoleon import GoogleDocstring, NumpyDocstring
+from sphinx.ext.napoleon import setup as napoleon_setup
+
+
+
+[docs] +class DocstringExtension(object): + """Class that introduces a "Possible Types" section + + This class serves as a base class for + :class:`sphinx.ext.napoleon.NumpyDocstring` and + :class:`sphinx.ext.napoleon.GoogleDocstring` to introduce + another section names *Possible types* + + Examples + -------- + The usage is the same as for the NumpyDocstring class, but it supports + the `Possible types` section:: + + >>> from sphinx.ext.napoleon import Config + + >>> from psyplot.sphinxext.extended_napoleon import ( + ... ExtendedNumpyDocstring, + ... ) + >>> config = Config(napoleon_use_param=True, napoleon_use_rtype=True) + >>> docstring = ''' + ... Possible types + ... -------------- + ... type1 + ... Description of `type1` + ... type2 + ... Description of `type2`''' + >>> print(ExtendedNumpyDocstring(docstring, config)) + .. rubric:: Possible types + + * *type1* -- + Description of `type1` + * *type2* -- + Description of `type2`""" + + __metaclass__ = ABCMeta + + def _parse_possible_types_section(self, section): + fields = self._consume_fields(prefer_type=True) + lines = [".. rubric:: %s" % section, ""] + multi = len(fields) > 1 + for _name, _type, _desc in fields: + field = self._format_field(_name, _type, _desc) + if multi: + lines.extend(self._format_block("* ", field)) + else: + lines.extend(field) + return lines + [""] + + @abstractmethod + def _parse(self): + pass
+ + + +
+[docs] +class ExtendedNumpyDocstring(NumpyDocstring, DocstringExtension): + """:class:`sphinx.ext.napoleon.NumpyDocstring` with more sections""" + + def _parse(self, *args, **kwargs): + self._sections["possible types"] = self._parse_possible_types_section + return super(ExtendedNumpyDocstring, self)._parse(*args, **kwargs)
+ + + +
+[docs] +class ExtendedGoogleDocstring(GoogleDocstring, DocstringExtension): + """:class:`sphinx.ext.napoleon.GoogleDocstring` with more sections""" + + def _parse(self, *args, **kwargs): + self._sections["possible types"] = self._parse_possible_types_section + return super(ExtendedGoogleDocstring, self)._parse(*args, **kwargs)
+ + + +
+[docs] +def process_docstring(app, what, name, obj, options, lines): + """Process the docstring for a given python object. + + Called when autodoc has read and processed a docstring. `lines` is a list + of docstring lines that `_process_docstring` modifies in place to change + what Sphinx outputs. + + The following settings in conf.py control what styles of docstrings will + be parsed: + + * ``napoleon_google_docstring`` -- parse Google style docstrings + * ``napoleon_numpy_docstring`` -- parse NumPy style docstrings + + Parameters + ---------- + app : sphinx.application.Sphinx + Application object representing the Sphinx process. + what : str + A string specifying the type of the object to which the docstring + belongs. Valid values: "module", "class", "exception", "function", + "method", "attribute". + name : str + The fully qualified name of the object. + obj : module, class, exception, function, method, or attribute + The object to which the docstring belongs. + options : sphinx.ext.autodoc.Options + The options given to the directive: an object with attributes + inherited_members, undoc_members, show_inheritance and noindex that + are True if the flag option of same name was given to the auto + directive. + lines : list of str + The lines of the docstring, see above. + + .. note:: `lines` is modified *in place* + + Notes + ----- + This function is (to most parts) taken from the :mod:`sphinx.ext.napoleon` + module, sphinx version 1.3.1, and adapted to the classes defined here""" + result_lines = lines + if app.config.napoleon_numpy_docstring: + docstring = ExtendedNumpyDocstring( + result_lines, app.config, app, what, name, obj, options + ) + result_lines = docstring.lines() + if app.config.napoleon_google_docstring: + docstring = ExtendedGoogleDocstring( + result_lines, app.config, app, what, name, obj, options + ) + result_lines = docstring.lines() + + lines[:] = result_lines[:]
+ + + +
+[docs] +def setup(app): + """Sphinx extension setup function + + When the extension is loaded, Sphinx imports this module and executes + the ``setup()`` function, which in turn notifies Sphinx of everything + the extension offers. + + Parameters + ---------- + app : sphinx.application.Sphinx + Application object representing the Sphinx process + + Notes + ----- + This function uses the setup function of the :mod:`sphinx.ext.napoleon` + module""" + from sphinx.application import Sphinx + + if not isinstance(app, Sphinx): + return # probably called by tests + + app.connect("autodoc-process-docstring", process_docstring) + return napoleon_setup(app)
+ +
+ +
+
+ + + +
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/psyplot/utils.html b/_modules/psyplot/utils.html new file mode 100644 index 0000000..f6404b3 --- /dev/null +++ b/_modules/psyplot/utils.html @@ -0,0 +1,844 @@ + + + + + + psyplot.utils — psyplot documentation + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for psyplot.utils

+"""Miscallaneous utility functions for the psyplot package."""
+
+# SPDX-FileCopyrightText: 2016-2024 University of Lausanne
+# SPDX-FileCopyrightText: 2020-2021 Helmholtz-Zentrum Geesthacht
+
+# SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH
+#
+# SPDX-License-Identifier: LGPL-3.0-only
+
+import inspect
+import re
+import sys
+from difflib import get_close_matches
+from itertools import chain, filterfalse
+
+import six
+
+from psyplot.docstring import dedent, docstrings
+
+
+
+[docs] +def get_default_value(func, arg): + argspec = inspect.getfullargspec(func) + return next( + default + for a, default in zip(reversed(argspec[0]), reversed(argspec.defaults)) + if a == arg + )
+ + + +
+[docs] +def isstring(s): + return isinstance(s, str)
+ + + +
+[docs] +def plugin_entrypoints(group="psyplot", name="name"): + """This utility function gets the entry points of the psyplot plugins""" + if sys.version_info[:2] > (3, 7): + from importlib.metadata import entry_points + + try: + eps = entry_points(group=group, name=name) + except TypeError: # python<3.10 + eps = [ + ep for ep in entry_points().get(group, []) if ep.name == name + ] + else: + from pkg_resources import iter_entry_points + + eps = iter_entry_points(group=group, name=name) + return eps
+ + + +
+[docs] +class Defaultdict(dict): + """An ordered :class:`collections.defaultdict` + + Taken from http://stackoverflow.com/a/6190500/562769""" + + def __init__(self, default_factory=None, *a, **kw): + if default_factory is not None and not callable(default_factory): + raise TypeError("first argument must be callable") + dict.__init__(self, *a, **kw) + self.default_factory = default_factory + + def __getitem__(self, key): + try: + return dict.__getitem__(self, key) + except KeyError: + return self.__missing__(key) + + def __missing__(self, key): + if self.default_factory is None: + raise KeyError(key) + self[key] = value = self.default_factory() + return value + + def __reduce__(self): + if self.default_factory is None: + args = tuple() + else: + args = (self.default_factory,) + return type(self), args, None, None, self.items() + +
+[docs] + def copy(self): + """Return a shallow copy of the dictionary""" + return self.__copy__()
+ + + def __copy__(self): + return type(self)(self.default_factory, self) + + def __deepcopy__(self, memo): + import copy + + return type(self)(self.default_factory, copy.deepcopy(self.items())) + + def __repr__(self): + return "Defaultdict(%s, %s)" % ( + self.default_factory, + dict.__repr__(self), + )
+ + + +class _TempBool(object): + """Wrapper around a boolean defining an __enter__ and __exit__ method + + Notes + ----- + If you want to use this class as an instance property, rather use the + :func:`_temp_bool_prop` because this class as a descriptor is ment to be a + class descriptor""" + + #: default boolean value for the :attr:`value` attribute + default = False + + #: boolean value indicating whether there shall be a validation or not + value = False + + def __init__(self, default=False): + """ + Parameters + ---------- + default: bool + value of the object""" + self.default = default + self.value = default + self._entered = [] + + def __enter__(self): + self.value = not self.default + self._entered.append(1) + + def __exit__(self, type, value, tb): + self._entered.pop(-1) + if not self._entered: + self.value = self.default + + if six.PY2: + + def __nonzero__(self): + return self.value + + else: + + def __bool__(self): + return self.value + + def __repr__(self): + return repr(bool(self)) + + def __str__(self): + return str(bool(self)) + + def __call__(self, value=None): + """ + Parameters + ---------- + value: bool or None + If None, the current value will be negated. Otherwise the current + value of this instance is set to the given `value`""" + if value is None: + self.value = not self.value + else: + self.value = value + + def __get__(self, instance, owner): + return self + + def __set__(self, instance, value): + self.value = value + + +def _temp_bool_prop(propname, doc="", default=False): + """Creates a property that uses the :class:`_TempBool` class + + Parameters + ---------- + propname: str + The attribute name to use. The _TempBool instance will be stored in the + ``'_' + propname`` attribute of the corresponding instance + doc: str + The documentation of the property + default: bool + The default value of the _TempBool class""" + + def getx(self): + if getattr(self, "_" + propname, None) is not None: + return getattr(self, "_" + propname) + else: + setattr(self, "_" + propname, _TempBool(default)) + return getattr(self, "_" + propname) + + def setx(self, value): + getattr(self, propname).value = bool(value) + + def delx(self): + getattr(self, propname).value = default + + return property(getx, setx, delx, doc) + + +
+[docs] +def unique_everseen(iterable, key=None): + """List unique elements, preserving order. Remember all elements ever seen. + + Function taken from https://docs.python.org/2/library/itertools.html""" + # unique_everseen('AAAABBBCCDAABBB') --> A B C D + # unique_everseen('ABBCcAD', str.lower) --> A B C D + seen = set() + seen_add = seen.add + if key is None: + for element in filterfalse(seen.__contains__, iterable): + seen_add(element) + yield element + else: + for element in iterable: + k = key(element) + if k not in seen: + seen_add(k) + yield element
+ + + +
+[docs] +def is_remote_url(path): + patt = re.compile(r"^https?\://") + if not isinstance(path, six.string_types): + return all(map(patt.search, (s or "" for s in path))) + return bool(re.search(r"^https?\://", path))
+ + + +
+[docs] +@docstrings.get_sections( + base="check_key", sections=["Parameters", "Returns", "Raises"] +) +@dedent +def check_key( + key, + possible_keys, + raise_error=True, + name="formatoption keyword", + msg=("See show_fmtkeys function for possible formatopion " "keywords"), + *args, + **kwargs, +): + """ + Checks whether the key is in a list of possible keys + + This function checks whether the given `key` is in `possible_keys` and if + not looks for similar sounding keys + + Parameters + ---------- + key: str + Key to check + possible_keys: list of strings + a list of possible keys to use + raise_error: bool + If not True, a list of similar keys is returned + name: str + The name of the key that shall be used in the error message + msg: str + The additional message that shall be used if no close match to + key is found + *args, **kwargs + They are passed to the :func:`difflib.get_close_matches` function + (i.e. `n` to increase the number of returned similar keys and + `cutoff` to change the sensibility) + + Returns + ------- + str + The `key` if it is a valid string, else an empty string + list + A list of similar formatoption strings (if found) + str + An error message which includes + + Raises + ------ + KeyError + If the key is not a valid formatoption and `raise_error` is True""" + if key not in possible_keys: + similarkeys = get_close_matches(key, possible_keys, *args, **kwargs) + if similarkeys: + msg = ("Unknown %s %s! Possible similiar " "frasings are %s.") % ( + name, + key, + ", ".join(similarkeys), + ) + else: + msg = ("Unknown %s %s! ") % (name, key) + msg + if not raise_error: + return "", similarkeys, msg + raise KeyError(msg) + else: + return key, [key], ""
+ + + +
+[docs] +def sort_kwargs(kwargs, *param_lists): + """Function to sort keyword arguments and sort them into dictionaries + + This function returns dictionaries that contain the keyword arguments + from `kwargs` corresponding given iterables in ``*params`` + + Parameters + ---------- + kwargs: dict + Original dictionary + ``*param_lists`` + iterables of strings, each standing for a possible key in kwargs + + Returns + ------- + list + len(params) + 1 dictionaries. Each dictionary contains the items of + `kwargs` corresponding to the specified list in ``*param_lists``. The + last dictionary contains the remaining items""" + return chain( + ( + {key: kwargs.pop(key) for key in params.intersection(kwargs)} + for params in map(set, param_lists) + ), + [kwargs], + )
+ + + +
+[docs] +def hashable(val): + """Test if `val` is hashable and if not, get it's string representation + + Parameters + ---------- + val: object + Any (possibly not hashable) python object + + Returns + ------- + val or string + The given `val` if it is hashable or it's string representation""" + if val is None: + return val + try: + hash(val) + except TypeError: + return repr(val) + else: + return val
+ + + +
+[docs] +@docstrings.get_sections(base="join_dicts") +def join_dicts(dicts, delimiter=None, keep_all=False): + """Join multiple dictionaries into one + + Parameters + ---------- + dicts: list of dict + A list of dictionaries + delimiter: str + The string that shall be used as the delimiter in case that there + are multiple values for one attribute in the arrays. If None, they + will be returned as sets + keep_all: bool + If True, all formatoptions are kept. Otherwise only the intersection + + Returns + ------- + dict + The combined dictionary""" + if not dicts: + return {} + if keep_all: + all_keys = set(chain(*(d.keys() for d in dicts))) + else: + all_keys = set(dicts[0]) + for d in dicts[1:]: + all_keys.intersection_update(d) + ret = {} + for key in all_keys: + vals = {hashable(d.get(key, None)) for d in dicts} - {None} + if len(vals) == 1: + ret[key] = next(iter(vals)) + elif delimiter is None: + ret[key] = vals + else: + ret[key] = delimiter.join(map(str, vals)) + return ret
+ + + +
+[docs] +def is_iterable(iterable): + """Test if an object is iterable + + Parameters + ---------- + iterable: object + The object to test + + Returns + ------- + bool + True, if the object is an iterable object""" + try: + iter(iterable) + except TypeError: + return False + else: + return True
+ +
+ +
+
+ + + +
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/psyplot/warning.html b/_modules/psyplot/warning.html new file mode 100644 index 0000000..7c83b8c --- /dev/null +++ b/_modules/psyplot/warning.html @@ -0,0 +1,567 @@ + + + + + + psyplot.warning — psyplot documentation + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for psyplot.warning

+# coding: utf-8
+"""Warning module of the psyplot python module.
+
+This module controls the warning behaviour of the module via the python
+builtin warnings module and introduces three new warning classes:
+
+..autosummay::
+
+    PsPylotRuntimeWarning
+    PsyPlotWarning
+    PsyPlotCritical"""
+
+# SPDX-FileCopyrightText: 2016-2024 University of Lausanne
+# SPDX-FileCopyrightText: 2020-2021 Helmholtz-Zentrum Geesthacht
+
+# SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH
+#
+# SPDX-License-Identifier: LGPL-3.0-only
+
+import logging
+import warnings
+
+# disable a warning about "comparison to 'None' in backend_pdf which occurs
+# in the matplotlib.backends.backend_pdf.PdfPages class
+warnings.filterwarnings(
+    "ignore",
+    "comparison",
+    FutureWarning,
+    "matplotlib.backends.backend_pdf",
+    2264,
+)
+# disable a warning about "np.array_split" that occurs for certain numpy
+# versions
+warnings.filterwarnings(
+    "ignore",
+    "in the future np.array_split will retain",
+    FutureWarning,
+    "numpy.lib.shape_base",
+    431,
+)
+# disable a warning about "elementwise comparison of a string" in the
+# matplotlib.collection.Collection.get_edgecolor method that occurs for certain
+# matplotlib and numpy versions
+warnings.filterwarnings(
+    "ignore",
+    "elementwise comparison failed",
+    FutureWarning,
+    "matplotlib.collections",
+    590,
+)
+
+
+logger = logging.getLogger(__name__)
+
+
+
+[docs] +class PsyPlotRuntimeWarning(RuntimeWarning): + """Runtime warning that appears only ones""" + + pass
+ + + +
+[docs] +class PsyPlotWarning(UserWarning): + """Normal UserWarning for psyplot module""" + + pass
+ + + +
+[docs] +class PsyPlotCritical(UserWarning): + """Critical UserWarning for psyplot module""" + + pass
+ + + +_issued_psyplot_warnings = [] + + +warnings.simplefilter("default", PsyPlotWarning) +warnings.simplefilter("always", PsyPlotCritical) + + +
+[docs] +def disable_warnings(critical=False): + """Function that disables all warnings and all critical warnings (if + critical evaluates to True) related to the psyplot Module. + Please note that you can also configure the warnings via the + psyplot.warning logger (logging.getLogger(psyplot.warning)).""" + warnings.filterwarnings("ignore", r"\w", PsyPlotWarning, "psyplot", 0) + if critical: + warnings.filterwarnings("ignore", r"\w", PsyPlotCritical, "psyplot", 0)
+ + + +
+[docs] +def warn(message, category=PsyPlotWarning, logger=None): + """wrapper around the warnings.warn function for non-critical warnings. + logger may be a logging.Logger instance""" + if logger is not None: + message = "[Warning by %s]\n%s" % (logger.name, message) + warnings.warn(message, category, stacklevel=3)
+ + + +
+[docs] +def critical(message, category=PsyPlotCritical, logger=None): + """wrapper around the warnings.warn function for critical warnings. + logger may be a logging.Logger instance""" + if logger is not None: + message = "[Critical warning by %s]\n%s" % (logger.name, message) + warnings.warn(message, category, stacklevel=2)
+ + + +old_showwarning = warnings.showwarning + + +
+[docs] +def customwarn(message, category, filename, lineno, *args, **kwargs): + """Use the psyplot.warning logger for categories being out of + PsyPlotWarning and PsyPlotCritical and the default warnings.showwarning + function for all the others.""" + if category is PsyPlotWarning: + # for whatever reason, the `default` warnings filter does not work + # when hovering over plots. This is why we implement a custom warning + # filter here + key = (str(message), filename, lineno) + if key not in _issued_psyplot_warnings: + logger.warning( + warnings.formatwarning( + "\n%s" % message, category, filename, lineno + ) + ) + _issued_psyplot_warnings.append(key) + elif category is PsyPlotCritical: + logger.critical( + warnings.formatwarning( + "\n%s" % message, category, filename, lineno + ), + exc_info=True, + ) + else: + old_showwarning(message, category, filename, lineno, *args, **kwargs)
+ + + +warnings.showwarning = customwarn +
+ +
+
+ + + +
+
+
+
+ + + + \ No newline at end of file diff --git a/docs/about.rst b/_sources/about.rst.txt similarity index 97% rename from docs/about.rst rename to _sources/about.rst.txt index aab1dcb..03ea653 100644 --- a/docs/about.rst +++ b/_sources/about.rst.txt @@ -55,7 +55,7 @@ What it is, and what it is not points below, `edit this document`_ and click on *Propose File Change* and *Create pull request*. We can then discuss your changes. -.. _edit this document: https://github.com/psyplot/psyplot/edit/master/docs/about.rst +.. _edit this document: https://codebase.helmholtz.cloud/psyplot/psyplot/edit/master/docs/about.rst There are tons of software tools around for visualization, so what is special about psyplot? The following list should hopefully provide you some guidance. @@ -137,8 +137,8 @@ with a few downsides. .. _flask: https://flask.palletsprojects.com .. _options provided by matplotlib: https://matplotlib.org/3.1.1/faq/howto_faq.html#how-to-use-matplotlib-in-a-web-application-server .. _other visualization backends: https://github.com/psyplot/psy-vtk -.. _psy-view: https://github.com/psyplot/psy-view -.. _ncview: http://meteora.ucsd.edu/~pierce/ncview_home_page.html +.. _psy-view: https://codebase.helmholtz.cloud/psyplot/psy-view +.. _ncview: https://cirrus.ucsd.edu/ncview/ .. _psy-reg: https://psyplot.github.io/psy-reg .. _seaborn: https://seaborn.pydata.org .. _R: https://www.r-project.org/ diff --git a/docs/accessors.rst b/_sources/accessors.rst.txt similarity index 100% rename from docs/accessors.rst rename to _sources/accessors.rst.txt diff --git a/docs/api.rst b/_sources/api.rst.txt similarity index 100% rename from docs/api.rst rename to _sources/api.rst.txt diff --git a/_sources/api/psyplot.config.logsetup.rst.txt b/_sources/api/psyplot.config.logsetup.rst.txt new file mode 100644 index 0000000..ec93926 --- /dev/null +++ b/_sources/api/psyplot.config.logsetup.rst.txt @@ -0,0 +1,4 @@ +.. automodule:: psyplot.config.logsetup + :members: + :undoc-members: + :show-inheritance: diff --git a/_sources/api/psyplot.config.rcsetup.rst.txt b/_sources/api/psyplot.config.rcsetup.rst.txt new file mode 100644 index 0000000..2da5b27 --- /dev/null +++ b/_sources/api/psyplot.config.rcsetup.rst.txt @@ -0,0 +1,4 @@ +.. automodule:: psyplot.config.rcsetup + :members: + :undoc-members: + :show-inheritance: diff --git a/_sources/api/psyplot.config.rst.txt b/_sources/api/psyplot.config.rst.txt new file mode 100644 index 0000000..6b138f0 --- /dev/null +++ b/_sources/api/psyplot.config.rst.txt @@ -0,0 +1,16 @@ +psyplot.config package +====================== + +.. automodule:: psyplot.config + :members: + :undoc-members: + :show-inheritance: + +Submodules +---------- + +.. toctree:: + :maxdepth: 4 + + psyplot.config.logsetup + psyplot.config.rcsetup diff --git a/_sources/api/psyplot.data.rst.txt b/_sources/api/psyplot.data.rst.txt new file mode 100644 index 0000000..3d2c9a8 --- /dev/null +++ b/_sources/api/psyplot.data.rst.txt @@ -0,0 +1,4 @@ +.. automodule:: psyplot.data + :members: + :undoc-members: + :show-inheritance: diff --git a/_sources/api/psyplot.docstring.rst.txt b/_sources/api/psyplot.docstring.rst.txt new file mode 100644 index 0000000..4ca4a96 --- /dev/null +++ b/_sources/api/psyplot.docstring.rst.txt @@ -0,0 +1,4 @@ +.. automodule:: psyplot.docstring + :members: + :undoc-members: + :show-inheritance: diff --git a/_sources/api/psyplot.gdal_store.rst.txt b/_sources/api/psyplot.gdal_store.rst.txt new file mode 100644 index 0000000..d1be6cd --- /dev/null +++ b/_sources/api/psyplot.gdal_store.rst.txt @@ -0,0 +1,4 @@ +.. automodule:: psyplot.gdal_store + :members: + :undoc-members: + :show-inheritance: diff --git a/_sources/api/psyplot.plotter.rst.txt b/_sources/api/psyplot.plotter.rst.txt new file mode 100644 index 0000000..15907de --- /dev/null +++ b/_sources/api/psyplot.plotter.rst.txt @@ -0,0 +1,4 @@ +.. automodule:: psyplot.plotter + :members: + :undoc-members: + :show-inheritance: diff --git a/_sources/api/psyplot.project.rst.txt b/_sources/api/psyplot.project.rst.txt new file mode 100644 index 0000000..375bc24 --- /dev/null +++ b/_sources/api/psyplot.project.rst.txt @@ -0,0 +1,4 @@ +.. automodule:: psyplot.project + :members: + :undoc-members: + :show-inheritance: diff --git a/_sources/api/psyplot.rst.txt b/_sources/api/psyplot.rst.txt new file mode 100644 index 0000000..5bf8b2f --- /dev/null +++ b/_sources/api/psyplot.rst.txt @@ -0,0 +1,30 @@ +psyplot package +=============== + +.. automodule:: psyplot + :members: + :undoc-members: + :show-inheritance: + +Subpackages +----------- + +.. toctree:: + :maxdepth: 4 + + psyplot.config + psyplot.sphinxext + +Submodules +---------- + +.. toctree:: + :maxdepth: 4 + + psyplot.data + psyplot.docstring + psyplot.gdal_store + psyplot.plotter + psyplot.project + psyplot.utils + psyplot.warning diff --git a/_sources/api/psyplot.sphinxext.extended_napoleon.rst.txt b/_sources/api/psyplot.sphinxext.extended_napoleon.rst.txt new file mode 100644 index 0000000..5ec6042 --- /dev/null +++ b/_sources/api/psyplot.sphinxext.extended_napoleon.rst.txt @@ -0,0 +1,4 @@ +.. automodule:: psyplot.sphinxext.extended_napoleon + :members: + :undoc-members: + :show-inheritance: diff --git a/_sources/api/psyplot.sphinxext.rst.txt b/_sources/api/psyplot.sphinxext.rst.txt new file mode 100644 index 0000000..40dd08e --- /dev/null +++ b/_sources/api/psyplot.sphinxext.rst.txt @@ -0,0 +1,15 @@ +psyplot.sphinxext package +========================= + +.. automodule:: psyplot.sphinxext + :members: + :undoc-members: + :show-inheritance: + +Submodules +---------- + +.. toctree:: + :maxdepth: 4 + + psyplot.sphinxext.extended_napoleon diff --git a/_sources/api/psyplot.utils.rst.txt b/_sources/api/psyplot.utils.rst.txt new file mode 100644 index 0000000..6e7cd57 --- /dev/null +++ b/_sources/api/psyplot.utils.rst.txt @@ -0,0 +1,4 @@ +.. automodule:: psyplot.utils + :members: + :undoc-members: + :show-inheritance: diff --git a/_sources/api/psyplot.warning.rst.txt b/_sources/api/psyplot.warning.rst.txt new file mode 100644 index 0000000..74f0151 --- /dev/null +++ b/_sources/api/psyplot.warning.rst.txt @@ -0,0 +1,4 @@ +.. automodule:: psyplot.warning + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/changelog.rst b/_sources/changelog.rst.txt similarity index 100% rename from docs/changelog.rst rename to _sources/changelog.rst.txt diff --git a/docs/command_line.rst b/_sources/command_line.rst.txt similarity index 100% rename from docs/command_line.rst rename to _sources/command_line.rst.txt diff --git a/docs/configuration.rst b/_sources/configuration.rst.txt similarity index 100% rename from docs/configuration.rst rename to _sources/configuration.rst.txt diff --git a/docs/contributing.rst b/_sources/contributing.rst.txt similarity index 91% rename from docs/contributing.rst rename to _sources/contributing.rst.txt index 5448814..7fdd3a9 100644 --- a/docs/contributing.rst +++ b/_sources/contributing.rst.txt @@ -32,7 +32,7 @@ Code of Conduct --------------- This project and everyone participating in it is governed by the -`psyplot Code of Conduct `__. +`psyplot Code of Conduct `__. By participating, you are expected to uphold this code. What should I know before I get started? @@ -46,19 +46,19 @@ and visualization. Much of the functionality however is implemented by other packages. What package is the correct one for your bug report/feature request, can be determined by the following list -- `psyplot-gui `__: +- `psyplot-gui `__: Everything specific to the graphical user interface -- `psy-view `__: +- `psy-view `__: Everything specific to the psy-view graphical user interface -- `psy-simple `__: +- `psy-simple `__: Everything concerning, e.g. the ``lineplot``, ``plot2d``, ``density`` or ``vector`` plot methods -- `psy-maps `__: Everything +- `psy-maps `__: Everything concerning, e.g. the ``mapplot``, ``mapvector`` ``mapcombined`` plot methods -- `psy-reg `__: Everything +- `psy-reg `__: Everything concerning, e.g. the ``linreg`` or ``densityreg`` plot methods -- `psyplot `__: Everything +- `psyplot `__: Everything concerning the general framework, e.g. data handling, parallel update, etc. @@ -72,7 +72,7 @@ implemented it via print(psy.plot.name - of - your - plot - method._plugin) If you still don’t know, where to open the issue, just go for -`psyplot `__. +`psyplot `__. .. _Helmholtz-Zentrum Hereon: https://www.hereon.de .. _open an issue at the source code repository: https://codebase.helmholtz.cloud/psyplot/psyplot diff --git a/docs/develop/framework.rst b/_sources/develop/framework.rst.txt similarity index 100% rename from docs/develop/framework.rst rename to _sources/develop/framework.rst.txt diff --git a/docs/develop/index.rst b/_sources/develop/index.rst.txt similarity index 100% rename from docs/develop/index.rst rename to _sources/develop/index.rst.txt diff --git a/docs/develop/plugins_guide.rst b/_sources/develop/plugins_guide.rst.txt similarity index 91% rename from docs/develop/plugins_guide.rst rename to _sources/develop/plugins_guide.rst.txt index c03938b..c935399 100644 --- a/docs/develop/plugins_guide.rst +++ b/_sources/develop/plugins_guide.rst.txt @@ -332,10 +332,6 @@ The advantages of this methodology are basically: Creating new plugins -------------------- -.. todo:: - - The plugin generation needs to be revised - Now that you have created your plotter, you may want to include it in the plot methods of the :class:`~psyplot.project.Project` class such that you can do something like @@ -354,32 +350,57 @@ There are three possibilities how you can do this: :attr:`~psyplot.config.rcsetup.rcParams` 3. The steady and shareable solution: Create a new plugin -The third solution has been used for the psy-maps_ and psy-simple_ plugins. To -create a skeleton for your plugin, you can use the ``psyplot-plugin`` command -that is installed when you install psyplot. +The third solution has been used for the psy-maps_ and psy-simple_ plugins and +will be described in the following section. + +Creating a package with the psyplot-plugin-template +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The psyplot-plugin-template_ provides a template to create a python package that +integrates with the psyplot environment. We recommend using this template as it +already contains a setup for automated formatters and linters, and a setup for +continuous integration. + +.. note:: + + When creating a real package, we strongly recommend to use cruft_ instead + of cookiecutter_! -For our demonstration, let's create a plugin named my-plugin. This is simply -done via +.. _psyplot-plugin-template: https://codebase.helmholtz.cloud/psyplot/psyplot-plugin-template +.. _cruft: https://cruft.github.io/cruft +.. _cookiecutter: https://cookiecutter.readthedocs.io + +For our demonstration, let's create a plugin named *my-plugin*. We will save +this name in to a YAML-file and use this to create our new plugin. .. ipython:: - :verbatim: - In [1]: !psyplot-plugin my-plugin + In [1]: !echo "default_context: {project_slug: my-plugin}" > "config.yaml" + + @suppress + In [1]: !rm -rf my-plugin + ...: !PARENT_GIT_REPO="../../" SILENT_HOOKS=1 cookiecutter --no-input --config-file config.yaml https://codebase.helmholtz.cloud/psyplot/psyplot-plugin-template.git + + @verbatim + In [1]: cookiecutter --no-input --config-file config.yaml https://codebase.helmholtz.cloud/psyplot/psyplot-plugin-template.git In [2]: import glob In [3]: glob.glob('my-plugin/**', recursive=True) + @suppress + In [1]: !rm -rf my-plugin config.yaml LICENSES/EUPL-1.2.txt + The following files are created in a directory named ``'my-plugin'``: -``'setup.py'`` - The installation script -``'my_plugin/plugin.py'`` +``pyproject.toml`` + The python package configuration +``'my_python_package/plugin.py'`` The file that sets up the configuration of our plugin. This file should define the ``rcParams`` for the plugin (see also :ref:`plugins-rcParams`) -``'my_plugin/plotters.py'`` - The file in which we define the plotters. This file should define the - plotters and formatoptions. +``'my_python_package/plotters.py'`` + The file in which we define the plotters. This file should contain the + plotters and formatoptions from our previous section. If you want to see more, look into the comments in the created files. diff --git a/docs/getting_started.rst b/_sources/getting_started.rst.txt similarity index 100% rename from docs/getting_started.rst rename to _sources/getting_started.rst.txt diff --git a/docs/index.rst b/_sources/index.rst.txt similarity index 95% rename from docs/index.rst rename to _sources/index.rst.txt index 08d318f..4e1ed8d 100644 --- a/docs/index.rst +++ b/_sources/index.rst.txt @@ -79,11 +79,11 @@ See also the `code of conduct`, and our :ref:`contribution guide ` for more information and a guide about good bug reports. -.. _bug tracker: https://github.com/psyplot/psyplot +.. _bug tracker: https://codebase.helmholtz.cloud/psyplot/psyplot .. _team on mattermost: https://mattermost.hzdr.de/psyplot/ .. _mailing list: https://www.listserv.dfn.de/sympa/subscribe/psyplot -.. _code of conduct: https://github.com/psyplot/psyplot/blob/master/CODE_OF_CONDUCT.md -.. _contribution guide: https://github.com/psyplot/psyplot/blob/master/CONTRIBUTING.md +.. _code of conduct: https://codebase.helmholtz.cloud/psyplot/psyplot/blob/master/CODE_OF_CONDUCT.md +.. _contribution guide: https://codebase.helmholtz.cloud/psyplot/psyplot/blob/master/CONTRIBUTING.md .. _citation: diff --git a/docs/installing.rst b/_sources/installing.rst.txt similarity index 97% rename from docs/installing.rst rename to _sources/installing.rst.txt index 9c80a55..fd45c77 100644 --- a/docs/installing.rst +++ b/_sources/installing.rst.txt @@ -65,7 +65,7 @@ Installation from source To install it from source, make sure you have the :ref:`dependencies` installed, clone the github_ repository via:: - git clone https://github.com/psyplot/psyplot.git + git clone https://codebase.helmholtz.cloud/psyplot/psyplot.git and install it via:: @@ -164,7 +164,7 @@ Then build the docs via:: You then have to install the necessary modules for each of the examples in the new ``'py37'`` environment. -.. _github: https://github.com/psyplot/psyplot +.. _github: https://codebase.helmholtz.cloud/psyplot/psyplot .. _ipykernel: https://ipykernel.readthedocs.io .. _pytest: https://pytest.org/latest/contents.html diff --git a/docs/plugins.rst b/_sources/plugins.rst.txt similarity index 96% rename from docs/plugins.rst rename to _sources/plugins.rst.txt index 8b49db3..c64eb30 100644 --- a/docs/plugins.rst +++ b/_sources/plugins.rst.txt @@ -64,7 +64,7 @@ If you have new plugins that you think should be included in this list, please do not hesitate to open an issue on the `github project page of psyplot`_ or implement it by yourself in `this file`_ and make a pull request. -.. _this file: https://github.com/psyplot/psyplot/blob/master/docs/plugins.rst +.. _this file: https://codebase.helmholtz.cloud/psyplot/psyplot/blob/master/docs/plugins.rst .. note:: @@ -88,7 +88,7 @@ implement it by yourself in `this file`_ and make a pull request. where ``PLUGIN_NAME`` is any of ``psy_simple, psy_maps``, etc. -.. _github project page of psyplot: https://github.com/psyplot/psyplot +.. _github project page of psyplot: https://codebase.helmholtz.cloud/psyplot/psyplot .. _excluding_plugins: diff --git a/docs/projects.rst b/_sources/projects.rst.txt similarity index 100% rename from docs/projects.rst rename to _sources/projects.rst.txt diff --git a/docs/todos.rst b/_sources/todos.rst.txt similarity index 100% rename from docs/todos.rst rename to _sources/todos.rst.txt diff --git a/_sphinx_design_static/design-tabs.js b/_sphinx_design_static/design-tabs.js new file mode 100644 index 0000000..b25bd6a --- /dev/null +++ b/_sphinx_design_static/design-tabs.js @@ -0,0 +1,101 @@ +// @ts-check + +// Extra JS capability for selected tabs to be synced +// The selection is stored in local storage so that it persists across page loads. + +/** + * @type {Record} + */ +let sd_id_to_elements = {}; +const storageKeyPrefix = "sphinx-design-tab-id-"; + +/** + * Create a key for a tab element. + * @param {HTMLElement} el - The tab element. + * @returns {[string, string, string] | null} - The key. + * + */ +function create_key(el) { + let syncId = el.getAttribute("data-sync-id"); + let syncGroup = el.getAttribute("data-sync-group"); + if (!syncId || !syncGroup) return null; + return [syncGroup, syncId, syncGroup + "--" + syncId]; +} + +/** + * Initialize the tab selection. + * + */ +function ready() { + // Find all tabs with sync data + + /** @type {string[]} */ + let groups = []; + + document.querySelectorAll(".sd-tab-label").forEach((label) => { + if (label instanceof HTMLElement) { + let data = create_key(label); + if (data) { + let [group, id, key] = data; + + // add click event listener + // @ts-ignore + label.onclick = onSDLabelClick; + + // store map of key to elements + if (!sd_id_to_elements[key]) { + sd_id_to_elements[key] = []; + } + sd_id_to_elements[key].push(label); + + if (groups.indexOf(group) === -1) { + groups.push(group); + // Check if a specific tab has been selected via URL parameter + const tabParam = new URLSearchParams(window.location.search).get( + group + ); + if (tabParam) { + console.log( + "sphinx-design: Selecting tab id for group '" + + group + + "' from URL parameter: " + + tabParam + ); + window.sessionStorage.setItem(storageKeyPrefix + group, tabParam); + } + } + + // Check is a specific tab has been selected previously + let previousId = window.sessionStorage.getItem( + storageKeyPrefix + group + ); + if (previousId === id) { + // console.log( + // "sphinx-design: Selecting tab from session storage: " + id + // ); + // @ts-ignore + label.previousElementSibling.checked = true; + } + } + } + }); +} + +/** + * Activate other tabs with the same sync id. + * + * @this {HTMLElement} - The element that was clicked. + */ +function onSDLabelClick() { + let data = create_key(this); + if (!data) return; + let [group, id, key] = data; + for (const label of sd_id_to_elements[key]) { + if (label === this) continue; + // @ts-ignore + label.previousElementSibling.checked = true; + } + window.sessionStorage.setItem(storageKeyPrefix + group, id); +} + +document.addEventListener("DOMContentLoaded", ready, false); diff --git a/_sphinx_design_static/sphinx-design.min.css b/_sphinx_design_static/sphinx-design.min.css new file mode 100644 index 0000000..a325746 --- /dev/null +++ b/_sphinx_design_static/sphinx-design.min.css @@ -0,0 +1 @@ +.sd-bg-primary{background-color:var(--sd-color-primary) !important}.sd-bg-text-primary{color:var(--sd-color-primary-text) !important}button.sd-bg-primary:focus,button.sd-bg-primary:hover{background-color:var(--sd-color-primary-highlight) !important}a.sd-bg-primary:focus,a.sd-bg-primary:hover{background-color:var(--sd-color-primary-highlight) !important}.sd-bg-secondary{background-color:var(--sd-color-secondary) !important}.sd-bg-text-secondary{color:var(--sd-color-secondary-text) !important}button.sd-bg-secondary:focus,button.sd-bg-secondary:hover{background-color:var(--sd-color-secondary-highlight) !important}a.sd-bg-secondary:focus,a.sd-bg-secondary:hover{background-color:var(--sd-color-secondary-highlight) !important}.sd-bg-success{background-color:var(--sd-color-success) !important}.sd-bg-text-success{color:var(--sd-color-success-text) !important}button.sd-bg-success:focus,button.sd-bg-success:hover{background-color:var(--sd-color-success-highlight) !important}a.sd-bg-success:focus,a.sd-bg-success:hover{background-color:var(--sd-color-success-highlight) !important}.sd-bg-info{background-color:var(--sd-color-info) !important}.sd-bg-text-info{color:var(--sd-color-info-text) !important}button.sd-bg-info:focus,button.sd-bg-info:hover{background-color:var(--sd-color-info-highlight) !important}a.sd-bg-info:focus,a.sd-bg-info:hover{background-color:var(--sd-color-info-highlight) !important}.sd-bg-warning{background-color:var(--sd-color-warning) !important}.sd-bg-text-warning{color:var(--sd-color-warning-text) !important}button.sd-bg-warning:focus,button.sd-bg-warning:hover{background-color:var(--sd-color-warning-highlight) !important}a.sd-bg-warning:focus,a.sd-bg-warning:hover{background-color:var(--sd-color-warning-highlight) !important}.sd-bg-danger{background-color:var(--sd-color-danger) !important}.sd-bg-text-danger{color:var(--sd-color-danger-text) !important}button.sd-bg-danger:focus,button.sd-bg-danger:hover{background-color:var(--sd-color-danger-highlight) !important}a.sd-bg-danger:focus,a.sd-bg-danger:hover{background-color:var(--sd-color-danger-highlight) !important}.sd-bg-light{background-color:var(--sd-color-light) !important}.sd-bg-text-light{color:var(--sd-color-light-text) !important}button.sd-bg-light:focus,button.sd-bg-light:hover{background-color:var(--sd-color-light-highlight) !important}a.sd-bg-light:focus,a.sd-bg-light:hover{background-color:var(--sd-color-light-highlight) !important}.sd-bg-muted{background-color:var(--sd-color-muted) !important}.sd-bg-text-muted{color:var(--sd-color-muted-text) !important}button.sd-bg-muted:focus,button.sd-bg-muted:hover{background-color:var(--sd-color-muted-highlight) !important}a.sd-bg-muted:focus,a.sd-bg-muted:hover{background-color:var(--sd-color-muted-highlight) !important}.sd-bg-dark{background-color:var(--sd-color-dark) !important}.sd-bg-text-dark{color:var(--sd-color-dark-text) !important}button.sd-bg-dark:focus,button.sd-bg-dark:hover{background-color:var(--sd-color-dark-highlight) !important}a.sd-bg-dark:focus,a.sd-bg-dark:hover{background-color:var(--sd-color-dark-highlight) !important}.sd-bg-black{background-color:var(--sd-color-black) !important}.sd-bg-text-black{color:var(--sd-color-black-text) !important}button.sd-bg-black:focus,button.sd-bg-black:hover{background-color:var(--sd-color-black-highlight) !important}a.sd-bg-black:focus,a.sd-bg-black:hover{background-color:var(--sd-color-black-highlight) !important}.sd-bg-white{background-color:var(--sd-color-white) !important}.sd-bg-text-white{color:var(--sd-color-white-text) !important}button.sd-bg-white:focus,button.sd-bg-white:hover{background-color:var(--sd-color-white-highlight) !important}a.sd-bg-white:focus,a.sd-bg-white:hover{background-color:var(--sd-color-white-highlight) !important}.sd-text-primary,.sd-text-primary>p{color:var(--sd-color-primary) !important}a.sd-text-primary:focus,a.sd-text-primary:hover{color:var(--sd-color-primary-highlight) !important}.sd-text-secondary,.sd-text-secondary>p{color:var(--sd-color-secondary) !important}a.sd-text-secondary:focus,a.sd-text-secondary:hover{color:var(--sd-color-secondary-highlight) !important}.sd-text-success,.sd-text-success>p{color:var(--sd-color-success) !important}a.sd-text-success:focus,a.sd-text-success:hover{color:var(--sd-color-success-highlight) !important}.sd-text-info,.sd-text-info>p{color:var(--sd-color-info) !important}a.sd-text-info:focus,a.sd-text-info:hover{color:var(--sd-color-info-highlight) !important}.sd-text-warning,.sd-text-warning>p{color:var(--sd-color-warning) !important}a.sd-text-warning:focus,a.sd-text-warning:hover{color:var(--sd-color-warning-highlight) !important}.sd-text-danger,.sd-text-danger>p{color:var(--sd-color-danger) !important}a.sd-text-danger:focus,a.sd-text-danger:hover{color:var(--sd-color-danger-highlight) !important}.sd-text-light,.sd-text-light>p{color:var(--sd-color-light) !important}a.sd-text-light:focus,a.sd-text-light:hover{color:var(--sd-color-light-highlight) !important}.sd-text-muted,.sd-text-muted>p{color:var(--sd-color-muted) !important}a.sd-text-muted:focus,a.sd-text-muted:hover{color:var(--sd-color-muted-highlight) !important}.sd-text-dark,.sd-text-dark>p{color:var(--sd-color-dark) !important}a.sd-text-dark:focus,a.sd-text-dark:hover{color:var(--sd-color-dark-highlight) !important}.sd-text-black,.sd-text-black>p{color:var(--sd-color-black) !important}a.sd-text-black:focus,a.sd-text-black:hover{color:var(--sd-color-black-highlight) !important}.sd-text-white,.sd-text-white>p{color:var(--sd-color-white) !important}a.sd-text-white:focus,a.sd-text-white:hover{color:var(--sd-color-white-highlight) !important}.sd-outline-primary{border-color:var(--sd-color-primary) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-primary:focus,a.sd-outline-primary:hover{border-color:var(--sd-color-primary-highlight) !important}.sd-outline-secondary{border-color:var(--sd-color-secondary) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-secondary:focus,a.sd-outline-secondary:hover{border-color:var(--sd-color-secondary-highlight) !important}.sd-outline-success{border-color:var(--sd-color-success) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-success:focus,a.sd-outline-success:hover{border-color:var(--sd-color-success-highlight) !important}.sd-outline-info{border-color:var(--sd-color-info) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-info:focus,a.sd-outline-info:hover{border-color:var(--sd-color-info-highlight) !important}.sd-outline-warning{border-color:var(--sd-color-warning) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-warning:focus,a.sd-outline-warning:hover{border-color:var(--sd-color-warning-highlight) !important}.sd-outline-danger{border-color:var(--sd-color-danger) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-danger:focus,a.sd-outline-danger:hover{border-color:var(--sd-color-danger-highlight) !important}.sd-outline-light{border-color:var(--sd-color-light) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-light:focus,a.sd-outline-light:hover{border-color:var(--sd-color-light-highlight) !important}.sd-outline-muted{border-color:var(--sd-color-muted) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-muted:focus,a.sd-outline-muted:hover{border-color:var(--sd-color-muted-highlight) !important}.sd-outline-dark{border-color:var(--sd-color-dark) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-dark:focus,a.sd-outline-dark:hover{border-color:var(--sd-color-dark-highlight) !important}.sd-outline-black{border-color:var(--sd-color-black) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-black:focus,a.sd-outline-black:hover{border-color:var(--sd-color-black-highlight) !important}.sd-outline-white{border-color:var(--sd-color-white) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-white:focus,a.sd-outline-white:hover{border-color:var(--sd-color-white-highlight) !important}.sd-bg-transparent{background-color:transparent !important}.sd-outline-transparent{border-color:transparent !important}.sd-text-transparent{color:transparent !important}.sd-p-0{padding:0 !important}.sd-pt-0,.sd-py-0{padding-top:0 !important}.sd-pr-0,.sd-px-0{padding-right:0 !important}.sd-pb-0,.sd-py-0{padding-bottom:0 !important}.sd-pl-0,.sd-px-0{padding-left:0 !important}.sd-p-1{padding:.25rem !important}.sd-pt-1,.sd-py-1{padding-top:.25rem !important}.sd-pr-1,.sd-px-1{padding-right:.25rem !important}.sd-pb-1,.sd-py-1{padding-bottom:.25rem !important}.sd-pl-1,.sd-px-1{padding-left:.25rem !important}.sd-p-2{padding:.5rem !important}.sd-pt-2,.sd-py-2{padding-top:.5rem !important}.sd-pr-2,.sd-px-2{padding-right:.5rem !important}.sd-pb-2,.sd-py-2{padding-bottom:.5rem !important}.sd-pl-2,.sd-px-2{padding-left:.5rem !important}.sd-p-3{padding:1rem !important}.sd-pt-3,.sd-py-3{padding-top:1rem !important}.sd-pr-3,.sd-px-3{padding-right:1rem !important}.sd-pb-3,.sd-py-3{padding-bottom:1rem !important}.sd-pl-3,.sd-px-3{padding-left:1rem !important}.sd-p-4{padding:1.5rem !important}.sd-pt-4,.sd-py-4{padding-top:1.5rem !important}.sd-pr-4,.sd-px-4{padding-right:1.5rem !important}.sd-pb-4,.sd-py-4{padding-bottom:1.5rem !important}.sd-pl-4,.sd-px-4{padding-left:1.5rem !important}.sd-p-5{padding:3rem !important}.sd-pt-5,.sd-py-5{padding-top:3rem !important}.sd-pr-5,.sd-px-5{padding-right:3rem !important}.sd-pb-5,.sd-py-5{padding-bottom:3rem !important}.sd-pl-5,.sd-px-5{padding-left:3rem !important}.sd-m-auto{margin:auto !important}.sd-mt-auto,.sd-my-auto{margin-top:auto !important}.sd-mr-auto,.sd-mx-auto{margin-right:auto !important}.sd-mb-auto,.sd-my-auto{margin-bottom:auto !important}.sd-ml-auto,.sd-mx-auto{margin-left:auto !important}.sd-m-0{margin:0 !important}.sd-mt-0,.sd-my-0{margin-top:0 !important}.sd-mr-0,.sd-mx-0{margin-right:0 !important}.sd-mb-0,.sd-my-0{margin-bottom:0 !important}.sd-ml-0,.sd-mx-0{margin-left:0 !important}.sd-m-1{margin:.25rem !important}.sd-mt-1,.sd-my-1{margin-top:.25rem !important}.sd-mr-1,.sd-mx-1{margin-right:.25rem !important}.sd-mb-1,.sd-my-1{margin-bottom:.25rem !important}.sd-ml-1,.sd-mx-1{margin-left:.25rem !important}.sd-m-2{margin:.5rem !important}.sd-mt-2,.sd-my-2{margin-top:.5rem !important}.sd-mr-2,.sd-mx-2{margin-right:.5rem !important}.sd-mb-2,.sd-my-2{margin-bottom:.5rem !important}.sd-ml-2,.sd-mx-2{margin-left:.5rem !important}.sd-m-3{margin:1rem !important}.sd-mt-3,.sd-my-3{margin-top:1rem !important}.sd-mr-3,.sd-mx-3{margin-right:1rem !important}.sd-mb-3,.sd-my-3{margin-bottom:1rem !important}.sd-ml-3,.sd-mx-3{margin-left:1rem !important}.sd-m-4{margin:1.5rem !important}.sd-mt-4,.sd-my-4{margin-top:1.5rem !important}.sd-mr-4,.sd-mx-4{margin-right:1.5rem !important}.sd-mb-4,.sd-my-4{margin-bottom:1.5rem !important}.sd-ml-4,.sd-mx-4{margin-left:1.5rem !important}.sd-m-5{margin:3rem !important}.sd-mt-5,.sd-my-5{margin-top:3rem !important}.sd-mr-5,.sd-mx-5{margin-right:3rem !important}.sd-mb-5,.sd-my-5{margin-bottom:3rem !important}.sd-ml-5,.sd-mx-5{margin-left:3rem !important}.sd-w-25{width:25% !important}.sd-w-50{width:50% !important}.sd-w-75{width:75% !important}.sd-w-100{width:100% !important}.sd-w-auto{width:auto !important}.sd-h-25{height:25% !important}.sd-h-50{height:50% !important}.sd-h-75{height:75% !important}.sd-h-100{height:100% !important}.sd-h-auto{height:auto !important}.sd-d-none{display:none !important}.sd-d-inline{display:inline !important}.sd-d-inline-block{display:inline-block !important}.sd-d-block{display:block !important}.sd-d-grid{display:grid !important}.sd-d-flex-row{display:-ms-flexbox !important;display:flex !important;flex-direction:row !important}.sd-d-flex-column{display:-ms-flexbox !important;display:flex !important;flex-direction:column !important}.sd-d-inline-flex{display:-ms-inline-flexbox !important;display:inline-flex !important}@media(min-width: 576px){.sd-d-sm-none{display:none !important}.sd-d-sm-inline{display:inline !important}.sd-d-sm-inline-block{display:inline-block !important}.sd-d-sm-block{display:block !important}.sd-d-sm-grid{display:grid !important}.sd-d-sm-flex{display:-ms-flexbox !important;display:flex !important}.sd-d-sm-inline-flex{display:-ms-inline-flexbox !important;display:inline-flex !important}}@media(min-width: 768px){.sd-d-md-none{display:none !important}.sd-d-md-inline{display:inline !important}.sd-d-md-inline-block{display:inline-block !important}.sd-d-md-block{display:block !important}.sd-d-md-grid{display:grid !important}.sd-d-md-flex{display:-ms-flexbox !important;display:flex !important}.sd-d-md-inline-flex{display:-ms-inline-flexbox !important;display:inline-flex !important}}@media(min-width: 992px){.sd-d-lg-none{display:none !important}.sd-d-lg-inline{display:inline !important}.sd-d-lg-inline-block{display:inline-block !important}.sd-d-lg-block{display:block !important}.sd-d-lg-grid{display:grid !important}.sd-d-lg-flex{display:-ms-flexbox !important;display:flex !important}.sd-d-lg-inline-flex{display:-ms-inline-flexbox !important;display:inline-flex !important}}@media(min-width: 1200px){.sd-d-xl-none{display:none !important}.sd-d-xl-inline{display:inline !important}.sd-d-xl-inline-block{display:inline-block !important}.sd-d-xl-block{display:block !important}.sd-d-xl-grid{display:grid !important}.sd-d-xl-flex{display:-ms-flexbox !important;display:flex !important}.sd-d-xl-inline-flex{display:-ms-inline-flexbox !important;display:inline-flex !important}}.sd-align-major-start{justify-content:flex-start !important}.sd-align-major-end{justify-content:flex-end !important}.sd-align-major-center{justify-content:center !important}.sd-align-major-justify{justify-content:space-between !important}.sd-align-major-spaced{justify-content:space-evenly !important}.sd-align-minor-start{align-items:flex-start !important}.sd-align-minor-end{align-items:flex-end !important}.sd-align-minor-center{align-items:center !important}.sd-align-minor-stretch{align-items:stretch !important}.sd-text-justify{text-align:justify !important}.sd-text-left{text-align:left !important}.sd-text-right{text-align:right !important}.sd-text-center{text-align:center !important}.sd-font-weight-light{font-weight:300 !important}.sd-font-weight-lighter{font-weight:lighter !important}.sd-font-weight-normal{font-weight:400 !important}.sd-font-weight-bold{font-weight:700 !important}.sd-font-weight-bolder{font-weight:bolder !important}.sd-font-italic{font-style:italic !important}.sd-text-decoration-none{text-decoration:none !important}.sd-text-lowercase{text-transform:lowercase !important}.sd-text-uppercase{text-transform:uppercase !important}.sd-text-capitalize{text-transform:capitalize !important}.sd-text-wrap{white-space:normal !important}.sd-text-nowrap{white-space:nowrap !important}.sd-text-truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.sd-fs-1,.sd-fs-1>p{font-size:calc(1.375rem + 1.5vw) !important;line-height:unset !important}.sd-fs-2,.sd-fs-2>p{font-size:calc(1.325rem + 0.9vw) !important;line-height:unset !important}.sd-fs-3,.sd-fs-3>p{font-size:calc(1.3rem + 0.6vw) !important;line-height:unset !important}.sd-fs-4,.sd-fs-4>p{font-size:calc(1.275rem + 0.3vw) !important;line-height:unset !important}.sd-fs-5,.sd-fs-5>p{font-size:1.25rem !important;line-height:unset !important}.sd-fs-6,.sd-fs-6>p{font-size:1rem !important;line-height:unset !important}.sd-border-0{border:0 solid !important}.sd-border-top-0{border-top:0 solid !important}.sd-border-bottom-0{border-bottom:0 solid !important}.sd-border-right-0{border-right:0 solid !important}.sd-border-left-0{border-left:0 solid !important}.sd-border-1{border:1px solid !important}.sd-border-top-1{border-top:1px solid !important}.sd-border-bottom-1{border-bottom:1px solid !important}.sd-border-right-1{border-right:1px solid !important}.sd-border-left-1{border-left:1px solid !important}.sd-border-2{border:2px solid !important}.sd-border-top-2{border-top:2px solid !important}.sd-border-bottom-2{border-bottom:2px solid !important}.sd-border-right-2{border-right:2px solid !important}.sd-border-left-2{border-left:2px solid !important}.sd-border-3{border:3px solid !important}.sd-border-top-3{border-top:3px solid !important}.sd-border-bottom-3{border-bottom:3px solid !important}.sd-border-right-3{border-right:3px solid !important}.sd-border-left-3{border-left:3px solid !important}.sd-border-4{border:4px solid !important}.sd-border-top-4{border-top:4px solid !important}.sd-border-bottom-4{border-bottom:4px solid !important}.sd-border-right-4{border-right:4px solid !important}.sd-border-left-4{border-left:4px solid !important}.sd-border-5{border:5px solid !important}.sd-border-top-5{border-top:5px solid !important}.sd-border-bottom-5{border-bottom:5px solid !important}.sd-border-right-5{border-right:5px solid !important}.sd-border-left-5{border-left:5px solid !important}.sd-rounded-0{border-radius:0 !important}.sd-rounded-1{border-radius:.2rem !important}.sd-rounded-2{border-radius:.3rem !important}.sd-rounded-3{border-radius:.5rem !important}.sd-rounded-pill{border-radius:50rem !important}.sd-rounded-circle{border-radius:50% !important}.shadow-none{box-shadow:none !important}.sd-shadow-sm{box-shadow:0 .125rem .25rem var(--sd-color-shadow) !important}.sd-shadow-md{box-shadow:0 .5rem 1rem var(--sd-color-shadow) !important}.sd-shadow-lg{box-shadow:0 1rem 3rem var(--sd-color-shadow) !important}@keyframes sd-slide-from-left{0%{transform:translateX(-100%)}100%{transform:translateX(0)}}@keyframes sd-slide-from-right{0%{transform:translateX(200%)}100%{transform:translateX(0)}}@keyframes sd-grow100{0%{transform:scale(0);opacity:.5}100%{transform:scale(1);opacity:1}}@keyframes sd-grow50{0%{transform:scale(0.5);opacity:.5}100%{transform:scale(1);opacity:1}}@keyframes sd-grow50-rot20{0%{transform:scale(0.5) rotateZ(-20deg);opacity:.5}75%{transform:scale(1) rotateZ(5deg);opacity:1}95%{transform:scale(1) rotateZ(-1deg);opacity:1}100%{transform:scale(1) rotateZ(0);opacity:1}}.sd-animate-slide-from-left{animation:1s ease-out 0s 1 normal none running sd-slide-from-left}.sd-animate-slide-from-right{animation:1s ease-out 0s 1 normal none running sd-slide-from-right}.sd-animate-grow100{animation:1s ease-out 0s 1 normal none running sd-grow100}.sd-animate-grow50{animation:1s ease-out 0s 1 normal none running sd-grow50}.sd-animate-grow50-rot20{animation:1s ease-out 0s 1 normal none running sd-grow50-rot20}.sd-badge{display:inline-block;padding:.35em .65em;font-size:.75em;font-weight:700;line-height:1;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25rem}.sd-badge:empty{display:none}a.sd-badge{text-decoration:none}.sd-btn .sd-badge{position:relative;top:-1px}.sd-btn{background-color:transparent;border:1px solid transparent;border-radius:.25rem;cursor:pointer;display:inline-block;font-weight:400;font-size:1rem;line-height:1.5;padding:.375rem .75rem;text-align:center;text-decoration:none;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;vertical-align:middle;user-select:none;-moz-user-select:none;-ms-user-select:none;-webkit-user-select:none}.sd-btn:hover{text-decoration:none}@media(prefers-reduced-motion: reduce){.sd-btn{transition:none}}.sd-btn-primary,.sd-btn-outline-primary:hover,.sd-btn-outline-primary:focus{color:var(--sd-color-primary-text) !important;background-color:var(--sd-color-primary) !important;border-color:var(--sd-color-primary) !important;border-width:1px !important;border-style:solid !important}.sd-btn-primary:hover,.sd-btn-primary:focus{color:var(--sd-color-primary-text) !important;background-color:var(--sd-color-primary-highlight) !important;border-color:var(--sd-color-primary-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-primary{color:var(--sd-color-primary) !important;border-color:var(--sd-color-primary) !important;border-width:1px !important;border-style:solid !important}.sd-btn-secondary,.sd-btn-outline-secondary:hover,.sd-btn-outline-secondary:focus{color:var(--sd-color-secondary-text) !important;background-color:var(--sd-color-secondary) !important;border-color:var(--sd-color-secondary) !important;border-width:1px !important;border-style:solid !important}.sd-btn-secondary:hover,.sd-btn-secondary:focus{color:var(--sd-color-secondary-text) !important;background-color:var(--sd-color-secondary-highlight) !important;border-color:var(--sd-color-secondary-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-secondary{color:var(--sd-color-secondary) !important;border-color:var(--sd-color-secondary) !important;border-width:1px !important;border-style:solid !important}.sd-btn-success,.sd-btn-outline-success:hover,.sd-btn-outline-success:focus{color:var(--sd-color-success-text) !important;background-color:var(--sd-color-success) !important;border-color:var(--sd-color-success) !important;border-width:1px !important;border-style:solid !important}.sd-btn-success:hover,.sd-btn-success:focus{color:var(--sd-color-success-text) !important;background-color:var(--sd-color-success-highlight) !important;border-color:var(--sd-color-success-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-success{color:var(--sd-color-success) !important;border-color:var(--sd-color-success) !important;border-width:1px !important;border-style:solid !important}.sd-btn-info,.sd-btn-outline-info:hover,.sd-btn-outline-info:focus{color:var(--sd-color-info-text) !important;background-color:var(--sd-color-info) !important;border-color:var(--sd-color-info) !important;border-width:1px !important;border-style:solid !important}.sd-btn-info:hover,.sd-btn-info:focus{color:var(--sd-color-info-text) !important;background-color:var(--sd-color-info-highlight) !important;border-color:var(--sd-color-info-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-info{color:var(--sd-color-info) !important;border-color:var(--sd-color-info) !important;border-width:1px !important;border-style:solid !important}.sd-btn-warning,.sd-btn-outline-warning:hover,.sd-btn-outline-warning:focus{color:var(--sd-color-warning-text) !important;background-color:var(--sd-color-warning) !important;border-color:var(--sd-color-warning) !important;border-width:1px !important;border-style:solid !important}.sd-btn-warning:hover,.sd-btn-warning:focus{color:var(--sd-color-warning-text) !important;background-color:var(--sd-color-warning-highlight) !important;border-color:var(--sd-color-warning-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-warning{color:var(--sd-color-warning) !important;border-color:var(--sd-color-warning) !important;border-width:1px !important;border-style:solid !important}.sd-btn-danger,.sd-btn-outline-danger:hover,.sd-btn-outline-danger:focus{color:var(--sd-color-danger-text) !important;background-color:var(--sd-color-danger) !important;border-color:var(--sd-color-danger) !important;border-width:1px !important;border-style:solid !important}.sd-btn-danger:hover,.sd-btn-danger:focus{color:var(--sd-color-danger-text) !important;background-color:var(--sd-color-danger-highlight) !important;border-color:var(--sd-color-danger-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-danger{color:var(--sd-color-danger) !important;border-color:var(--sd-color-danger) !important;border-width:1px !important;border-style:solid !important}.sd-btn-light,.sd-btn-outline-light:hover,.sd-btn-outline-light:focus{color:var(--sd-color-light-text) !important;background-color:var(--sd-color-light) !important;border-color:var(--sd-color-light) !important;border-width:1px !important;border-style:solid !important}.sd-btn-light:hover,.sd-btn-light:focus{color:var(--sd-color-light-text) !important;background-color:var(--sd-color-light-highlight) !important;border-color:var(--sd-color-light-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-light{color:var(--sd-color-light) !important;border-color:var(--sd-color-light) !important;border-width:1px !important;border-style:solid !important}.sd-btn-muted,.sd-btn-outline-muted:hover,.sd-btn-outline-muted:focus{color:var(--sd-color-muted-text) !important;background-color:var(--sd-color-muted) !important;border-color:var(--sd-color-muted) !important;border-width:1px !important;border-style:solid !important}.sd-btn-muted:hover,.sd-btn-muted:focus{color:var(--sd-color-muted-text) !important;background-color:var(--sd-color-muted-highlight) !important;border-color:var(--sd-color-muted-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-muted{color:var(--sd-color-muted) !important;border-color:var(--sd-color-muted) !important;border-width:1px !important;border-style:solid !important}.sd-btn-dark,.sd-btn-outline-dark:hover,.sd-btn-outline-dark:focus{color:var(--sd-color-dark-text) !important;background-color:var(--sd-color-dark) !important;border-color:var(--sd-color-dark) !important;border-width:1px !important;border-style:solid !important}.sd-btn-dark:hover,.sd-btn-dark:focus{color:var(--sd-color-dark-text) !important;background-color:var(--sd-color-dark-highlight) !important;border-color:var(--sd-color-dark-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-dark{color:var(--sd-color-dark) !important;border-color:var(--sd-color-dark) !important;border-width:1px !important;border-style:solid !important}.sd-btn-black,.sd-btn-outline-black:hover,.sd-btn-outline-black:focus{color:var(--sd-color-black-text) !important;background-color:var(--sd-color-black) !important;border-color:var(--sd-color-black) !important;border-width:1px !important;border-style:solid !important}.sd-btn-black:hover,.sd-btn-black:focus{color:var(--sd-color-black-text) !important;background-color:var(--sd-color-black-highlight) !important;border-color:var(--sd-color-black-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-black{color:var(--sd-color-black) !important;border-color:var(--sd-color-black) !important;border-width:1px !important;border-style:solid !important}.sd-btn-white,.sd-btn-outline-white:hover,.sd-btn-outline-white:focus{color:var(--sd-color-white-text) !important;background-color:var(--sd-color-white) !important;border-color:var(--sd-color-white) !important;border-width:1px !important;border-style:solid !important}.sd-btn-white:hover,.sd-btn-white:focus{color:var(--sd-color-white-text) !important;background-color:var(--sd-color-white-highlight) !important;border-color:var(--sd-color-white-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-white{color:var(--sd-color-white) !important;border-color:var(--sd-color-white) !important;border-width:1px !important;border-style:solid !important}.sd-stretched-link::after{position:absolute;top:0;right:0;bottom:0;left:0;z-index:1;content:""}.sd-hide-link-text{font-size:0}.sd-octicon,.sd-material-icon{display:inline-block;fill:currentColor;vertical-align:middle}.sd-avatar-xs{border-radius:50%;object-fit:cover;object-position:center;width:1rem;height:1rem}.sd-avatar-sm{border-radius:50%;object-fit:cover;object-position:center;width:3rem;height:3rem}.sd-avatar-md{border-radius:50%;object-fit:cover;object-position:center;width:5rem;height:5rem}.sd-avatar-lg{border-radius:50%;object-fit:cover;object-position:center;width:7rem;height:7rem}.sd-avatar-xl{border-radius:50%;object-fit:cover;object-position:center;width:10rem;height:10rem}.sd-avatar-inherit{border-radius:50%;object-fit:cover;object-position:center;width:inherit;height:inherit}.sd-avatar-initial{border-radius:50%;object-fit:cover;object-position:center;width:initial;height:initial}.sd-card{background-clip:border-box;background-color:var(--sd-color-card-background);border:1px solid var(--sd-color-card-border);border-radius:.25rem;color:var(--sd-color-card-text);display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;min-width:0;position:relative;word-wrap:break-word}.sd-card>hr{margin-left:0;margin-right:0}.sd-card-hover:hover{border-color:var(--sd-color-card-border-hover);transform:scale(1.01)}.sd-card-body{-ms-flex:1 1 auto;flex:1 1 auto;padding:1rem 1rem}.sd-card-title{margin-bottom:.5rem}.sd-card-subtitle{margin-top:-0.25rem;margin-bottom:0}.sd-card-text:last-child{margin-bottom:0}.sd-card-link:hover{text-decoration:none}.sd-card-link+.card-link{margin-left:1rem}.sd-card-header{padding:.5rem 1rem;margin-bottom:0;background-color:var(--sd-color-card-header);border-bottom:1px solid var(--sd-color-card-border)}.sd-card-header:first-child{border-radius:calc(0.25rem - 1px) calc(0.25rem - 1px) 0 0}.sd-card-footer{padding:.5rem 1rem;background-color:var(--sd-color-card-footer);border-top:1px solid var(--sd-color-card-border)}.sd-card-footer:last-child{border-radius:0 0 calc(0.25rem - 1px) calc(0.25rem - 1px)}.sd-card-header-tabs{margin-right:-0.5rem;margin-bottom:-0.5rem;margin-left:-0.5rem;border-bottom:0}.sd-card-header-pills{margin-right:-0.5rem;margin-left:-0.5rem}.sd-card-img-overlay{position:absolute;top:0;right:0;bottom:0;left:0;padding:1rem;border-radius:calc(0.25rem - 1px)}.sd-card-img,.sd-card-img-bottom,.sd-card-img-top{width:100%}.sd-card-img,.sd-card-img-top{border-top-left-radius:calc(0.25rem - 1px);border-top-right-radius:calc(0.25rem - 1px)}.sd-card-img,.sd-card-img-bottom{border-bottom-left-radius:calc(0.25rem - 1px);border-bottom-right-radius:calc(0.25rem - 1px)}.sd-cards-carousel{width:100%;display:flex;flex-wrap:nowrap;-ms-flex-direction:row;flex-direction:row;overflow-x:hidden;scroll-snap-type:x mandatory}.sd-cards-carousel.sd-show-scrollbar{overflow-x:auto}.sd-cards-carousel:hover,.sd-cards-carousel:focus{overflow-x:auto}.sd-cards-carousel>.sd-card{flex-shrink:0;scroll-snap-align:start}.sd-cards-carousel>.sd-card:not(:last-child){margin-right:3px}.sd-card-cols-1>.sd-card{width:90%}.sd-card-cols-2>.sd-card{width:45%}.sd-card-cols-3>.sd-card{width:30%}.sd-card-cols-4>.sd-card{width:22.5%}.sd-card-cols-5>.sd-card{width:18%}.sd-card-cols-6>.sd-card{width:15%}.sd-card-cols-7>.sd-card{width:12.8571428571%}.sd-card-cols-8>.sd-card{width:11.25%}.sd-card-cols-9>.sd-card{width:10%}.sd-card-cols-10>.sd-card{width:9%}.sd-card-cols-11>.sd-card{width:8.1818181818%}.sd-card-cols-12>.sd-card{width:7.5%}.sd-container,.sd-container-fluid,.sd-container-lg,.sd-container-md,.sd-container-sm,.sd-container-xl{margin-left:auto;margin-right:auto;padding-left:var(--sd-gutter-x, 0.75rem);padding-right:var(--sd-gutter-x, 0.75rem);width:100%}@media(min-width: 576px){.sd-container-sm,.sd-container{max-width:540px}}@media(min-width: 768px){.sd-container-md,.sd-container-sm,.sd-container{max-width:720px}}@media(min-width: 992px){.sd-container-lg,.sd-container-md,.sd-container-sm,.sd-container{max-width:960px}}@media(min-width: 1200px){.sd-container-xl,.sd-container-lg,.sd-container-md,.sd-container-sm,.sd-container{max-width:1140px}}.sd-row{--sd-gutter-x: 1.5rem;--sd-gutter-y: 0;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;margin-top:calc(var(--sd-gutter-y) * -1);margin-right:calc(var(--sd-gutter-x) * -0.5);margin-left:calc(var(--sd-gutter-x) * -0.5)}.sd-row>*{box-sizing:border-box;flex-shrink:0;width:100%;max-width:100%;padding-right:calc(var(--sd-gutter-x) * 0.5);padding-left:calc(var(--sd-gutter-x) * 0.5);margin-top:var(--sd-gutter-y)}.sd-col{flex:1 0 0%;-ms-flex:1 0 0%}.sd-row-cols-auto>*{flex:0 0 auto;width:auto}.sd-row-cols-1>*{flex:0 0 auto;-ms-flex:0 0 auto;width:100%}.sd-row-cols-2>*{flex:0 0 auto;-ms-flex:0 0 auto;width:50%}.sd-row-cols-3>*{flex:0 0 auto;-ms-flex:0 0 auto;width:33.3333333333%}.sd-row-cols-4>*{flex:0 0 auto;-ms-flex:0 0 auto;width:25%}.sd-row-cols-5>*{flex:0 0 auto;-ms-flex:0 0 auto;width:20%}.sd-row-cols-6>*{flex:0 0 auto;-ms-flex:0 0 auto;width:16.6666666667%}.sd-row-cols-7>*{flex:0 0 auto;-ms-flex:0 0 auto;width:14.2857142857%}.sd-row-cols-8>*{flex:0 0 auto;-ms-flex:0 0 auto;width:12.5%}.sd-row-cols-9>*{flex:0 0 auto;-ms-flex:0 0 auto;width:11.1111111111%}.sd-row-cols-10>*{flex:0 0 auto;-ms-flex:0 0 auto;width:10%}.sd-row-cols-11>*{flex:0 0 auto;-ms-flex:0 0 auto;width:9.0909090909%}.sd-row-cols-12>*{flex:0 0 auto;-ms-flex:0 0 auto;width:8.3333333333%}@media(min-width: 576px){.sd-col-sm{flex:1 0 0%;-ms-flex:1 0 0%}.sd-row-cols-sm-auto{flex:1 0 auto;-ms-flex:1 0 auto;width:100%}.sd-row-cols-sm-1>*{flex:0 0 auto;-ms-flex:0 0 auto;width:100%}.sd-row-cols-sm-2>*{flex:0 0 auto;-ms-flex:0 0 auto;width:50%}.sd-row-cols-sm-3>*{flex:0 0 auto;-ms-flex:0 0 auto;width:33.3333333333%}.sd-row-cols-sm-4>*{flex:0 0 auto;-ms-flex:0 0 auto;width:25%}.sd-row-cols-sm-5>*{flex:0 0 auto;-ms-flex:0 0 auto;width:20%}.sd-row-cols-sm-6>*{flex:0 0 auto;-ms-flex:0 0 auto;width:16.6666666667%}.sd-row-cols-sm-7>*{flex:0 0 auto;-ms-flex:0 0 auto;width:14.2857142857%}.sd-row-cols-sm-8>*{flex:0 0 auto;-ms-flex:0 0 auto;width:12.5%}.sd-row-cols-sm-9>*{flex:0 0 auto;-ms-flex:0 0 auto;width:11.1111111111%}.sd-row-cols-sm-10>*{flex:0 0 auto;-ms-flex:0 0 auto;width:10%}.sd-row-cols-sm-11>*{flex:0 0 auto;-ms-flex:0 0 auto;width:9.0909090909%}.sd-row-cols-sm-12>*{flex:0 0 auto;-ms-flex:0 0 auto;width:8.3333333333%}}@media(min-width: 768px){.sd-col-md{flex:1 0 0%;-ms-flex:1 0 0%}.sd-row-cols-md-auto{flex:1 0 auto;-ms-flex:1 0 auto;width:100%}.sd-row-cols-md-1>*{flex:0 0 auto;-ms-flex:0 0 auto;width:100%}.sd-row-cols-md-2>*{flex:0 0 auto;-ms-flex:0 0 auto;width:50%}.sd-row-cols-md-3>*{flex:0 0 auto;-ms-flex:0 0 auto;width:33.3333333333%}.sd-row-cols-md-4>*{flex:0 0 auto;-ms-flex:0 0 auto;width:25%}.sd-row-cols-md-5>*{flex:0 0 auto;-ms-flex:0 0 auto;width:20%}.sd-row-cols-md-6>*{flex:0 0 auto;-ms-flex:0 0 auto;width:16.6666666667%}.sd-row-cols-md-7>*{flex:0 0 auto;-ms-flex:0 0 auto;width:14.2857142857%}.sd-row-cols-md-8>*{flex:0 0 auto;-ms-flex:0 0 auto;width:12.5%}.sd-row-cols-md-9>*{flex:0 0 auto;-ms-flex:0 0 auto;width:11.1111111111%}.sd-row-cols-md-10>*{flex:0 0 auto;-ms-flex:0 0 auto;width:10%}.sd-row-cols-md-11>*{flex:0 0 auto;-ms-flex:0 0 auto;width:9.0909090909%}.sd-row-cols-md-12>*{flex:0 0 auto;-ms-flex:0 0 auto;width:8.3333333333%}}@media(min-width: 992px){.sd-col-lg{flex:1 0 0%;-ms-flex:1 0 0%}.sd-row-cols-lg-auto{flex:1 0 auto;-ms-flex:1 0 auto;width:100%}.sd-row-cols-lg-1>*{flex:0 0 auto;-ms-flex:0 0 auto;width:100%}.sd-row-cols-lg-2>*{flex:0 0 auto;-ms-flex:0 0 auto;width:50%}.sd-row-cols-lg-3>*{flex:0 0 auto;-ms-flex:0 0 auto;width:33.3333333333%}.sd-row-cols-lg-4>*{flex:0 0 auto;-ms-flex:0 0 auto;width:25%}.sd-row-cols-lg-5>*{flex:0 0 auto;-ms-flex:0 0 auto;width:20%}.sd-row-cols-lg-6>*{flex:0 0 auto;-ms-flex:0 0 auto;width:16.6666666667%}.sd-row-cols-lg-7>*{flex:0 0 auto;-ms-flex:0 0 auto;width:14.2857142857%}.sd-row-cols-lg-8>*{flex:0 0 auto;-ms-flex:0 0 auto;width:12.5%}.sd-row-cols-lg-9>*{flex:0 0 auto;-ms-flex:0 0 auto;width:11.1111111111%}.sd-row-cols-lg-10>*{flex:0 0 auto;-ms-flex:0 0 auto;width:10%}.sd-row-cols-lg-11>*{flex:0 0 auto;-ms-flex:0 0 auto;width:9.0909090909%}.sd-row-cols-lg-12>*{flex:0 0 auto;-ms-flex:0 0 auto;width:8.3333333333%}}@media(min-width: 1200px){.sd-col-xl{flex:1 0 0%;-ms-flex:1 0 0%}.sd-row-cols-xl-auto{flex:1 0 auto;-ms-flex:1 0 auto;width:100%}.sd-row-cols-xl-1>*{flex:0 0 auto;-ms-flex:0 0 auto;width:100%}.sd-row-cols-xl-2>*{flex:0 0 auto;-ms-flex:0 0 auto;width:50%}.sd-row-cols-xl-3>*{flex:0 0 auto;-ms-flex:0 0 auto;width:33.3333333333%}.sd-row-cols-xl-4>*{flex:0 0 auto;-ms-flex:0 0 auto;width:25%}.sd-row-cols-xl-5>*{flex:0 0 auto;-ms-flex:0 0 auto;width:20%}.sd-row-cols-xl-6>*{flex:0 0 auto;-ms-flex:0 0 auto;width:16.6666666667%}.sd-row-cols-xl-7>*{flex:0 0 auto;-ms-flex:0 0 auto;width:14.2857142857%}.sd-row-cols-xl-8>*{flex:0 0 auto;-ms-flex:0 0 auto;width:12.5%}.sd-row-cols-xl-9>*{flex:0 0 auto;-ms-flex:0 0 auto;width:11.1111111111%}.sd-row-cols-xl-10>*{flex:0 0 auto;-ms-flex:0 0 auto;width:10%}.sd-row-cols-xl-11>*{flex:0 0 auto;-ms-flex:0 0 auto;width:9.0909090909%}.sd-row-cols-xl-12>*{flex:0 0 auto;-ms-flex:0 0 auto;width:8.3333333333%}}.sd-col-auto{flex:0 0 auto;-ms-flex:0 0 auto;width:auto}.sd-col-1{flex:0 0 auto;-ms-flex:0 0 auto;width:8.3333333333%}.sd-col-2{flex:0 0 auto;-ms-flex:0 0 auto;width:16.6666666667%}.sd-col-3{flex:0 0 auto;-ms-flex:0 0 auto;width:25%}.sd-col-4{flex:0 0 auto;-ms-flex:0 0 auto;width:33.3333333333%}.sd-col-5{flex:0 0 auto;-ms-flex:0 0 auto;width:41.6666666667%}.sd-col-6{flex:0 0 auto;-ms-flex:0 0 auto;width:50%}.sd-col-7{flex:0 0 auto;-ms-flex:0 0 auto;width:58.3333333333%}.sd-col-8{flex:0 0 auto;-ms-flex:0 0 auto;width:66.6666666667%}.sd-col-9{flex:0 0 auto;-ms-flex:0 0 auto;width:75%}.sd-col-10{flex:0 0 auto;-ms-flex:0 0 auto;width:83.3333333333%}.sd-col-11{flex:0 0 auto;-ms-flex:0 0 auto;width:91.6666666667%}.sd-col-12{flex:0 0 auto;-ms-flex:0 0 auto;width:100%}.sd-g-0,.sd-gy-0{--sd-gutter-y: 0}.sd-g-0,.sd-gx-0{--sd-gutter-x: 0}.sd-g-1,.sd-gy-1{--sd-gutter-y: 0.25rem}.sd-g-1,.sd-gx-1{--sd-gutter-x: 0.25rem}.sd-g-2,.sd-gy-2{--sd-gutter-y: 0.5rem}.sd-g-2,.sd-gx-2{--sd-gutter-x: 0.5rem}.sd-g-3,.sd-gy-3{--sd-gutter-y: 1rem}.sd-g-3,.sd-gx-3{--sd-gutter-x: 1rem}.sd-g-4,.sd-gy-4{--sd-gutter-y: 1.5rem}.sd-g-4,.sd-gx-4{--sd-gutter-x: 1.5rem}.sd-g-5,.sd-gy-5{--sd-gutter-y: 3rem}.sd-g-5,.sd-gx-5{--sd-gutter-x: 3rem}@media(min-width: 576px){.sd-col-sm-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto}.sd-col-sm-1{-ms-flex:0 0 auto;flex:0 0 auto;width:8.3333333333%}.sd-col-sm-2{-ms-flex:0 0 auto;flex:0 0 auto;width:16.6666666667%}.sd-col-sm-3{-ms-flex:0 0 auto;flex:0 0 auto;width:25%}.sd-col-sm-4{-ms-flex:0 0 auto;flex:0 0 auto;width:33.3333333333%}.sd-col-sm-5{-ms-flex:0 0 auto;flex:0 0 auto;width:41.6666666667%}.sd-col-sm-6{-ms-flex:0 0 auto;flex:0 0 auto;width:50%}.sd-col-sm-7{-ms-flex:0 0 auto;flex:0 0 auto;width:58.3333333333%}.sd-col-sm-8{-ms-flex:0 0 auto;flex:0 0 auto;width:66.6666666667%}.sd-col-sm-9{-ms-flex:0 0 auto;flex:0 0 auto;width:75%}.sd-col-sm-10{-ms-flex:0 0 auto;flex:0 0 auto;width:83.3333333333%}.sd-col-sm-11{-ms-flex:0 0 auto;flex:0 0 auto;width:91.6666666667%}.sd-col-sm-12{-ms-flex:0 0 auto;flex:0 0 auto;width:100%}.sd-g-sm-0,.sd-gy-sm-0{--sd-gutter-y: 0}.sd-g-sm-0,.sd-gx-sm-0{--sd-gutter-x: 0}.sd-g-sm-1,.sd-gy-sm-1{--sd-gutter-y: 0.25rem}.sd-g-sm-1,.sd-gx-sm-1{--sd-gutter-x: 0.25rem}.sd-g-sm-2,.sd-gy-sm-2{--sd-gutter-y: 0.5rem}.sd-g-sm-2,.sd-gx-sm-2{--sd-gutter-x: 0.5rem}.sd-g-sm-3,.sd-gy-sm-3{--sd-gutter-y: 1rem}.sd-g-sm-3,.sd-gx-sm-3{--sd-gutter-x: 1rem}.sd-g-sm-4,.sd-gy-sm-4{--sd-gutter-y: 1.5rem}.sd-g-sm-4,.sd-gx-sm-4{--sd-gutter-x: 1.5rem}.sd-g-sm-5,.sd-gy-sm-5{--sd-gutter-y: 3rem}.sd-g-sm-5,.sd-gx-sm-5{--sd-gutter-x: 3rem}}@media(min-width: 768px){.sd-col-md-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto}.sd-col-md-1{-ms-flex:0 0 auto;flex:0 0 auto;width:8.3333333333%}.sd-col-md-2{-ms-flex:0 0 auto;flex:0 0 auto;width:16.6666666667%}.sd-col-md-3{-ms-flex:0 0 auto;flex:0 0 auto;width:25%}.sd-col-md-4{-ms-flex:0 0 auto;flex:0 0 auto;width:33.3333333333%}.sd-col-md-5{-ms-flex:0 0 auto;flex:0 0 auto;width:41.6666666667%}.sd-col-md-6{-ms-flex:0 0 auto;flex:0 0 auto;width:50%}.sd-col-md-7{-ms-flex:0 0 auto;flex:0 0 auto;width:58.3333333333%}.sd-col-md-8{-ms-flex:0 0 auto;flex:0 0 auto;width:66.6666666667%}.sd-col-md-9{-ms-flex:0 0 auto;flex:0 0 auto;width:75%}.sd-col-md-10{-ms-flex:0 0 auto;flex:0 0 auto;width:83.3333333333%}.sd-col-md-11{-ms-flex:0 0 auto;flex:0 0 auto;width:91.6666666667%}.sd-col-md-12{-ms-flex:0 0 auto;flex:0 0 auto;width:100%}.sd-g-md-0,.sd-gy-md-0{--sd-gutter-y: 0}.sd-g-md-0,.sd-gx-md-0{--sd-gutter-x: 0}.sd-g-md-1,.sd-gy-md-1{--sd-gutter-y: 0.25rem}.sd-g-md-1,.sd-gx-md-1{--sd-gutter-x: 0.25rem}.sd-g-md-2,.sd-gy-md-2{--sd-gutter-y: 0.5rem}.sd-g-md-2,.sd-gx-md-2{--sd-gutter-x: 0.5rem}.sd-g-md-3,.sd-gy-md-3{--sd-gutter-y: 1rem}.sd-g-md-3,.sd-gx-md-3{--sd-gutter-x: 1rem}.sd-g-md-4,.sd-gy-md-4{--sd-gutter-y: 1.5rem}.sd-g-md-4,.sd-gx-md-4{--sd-gutter-x: 1.5rem}.sd-g-md-5,.sd-gy-md-5{--sd-gutter-y: 3rem}.sd-g-md-5,.sd-gx-md-5{--sd-gutter-x: 3rem}}@media(min-width: 992px){.sd-col-lg-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto}.sd-col-lg-1{-ms-flex:0 0 auto;flex:0 0 auto;width:8.3333333333%}.sd-col-lg-2{-ms-flex:0 0 auto;flex:0 0 auto;width:16.6666666667%}.sd-col-lg-3{-ms-flex:0 0 auto;flex:0 0 auto;width:25%}.sd-col-lg-4{-ms-flex:0 0 auto;flex:0 0 auto;width:33.3333333333%}.sd-col-lg-5{-ms-flex:0 0 auto;flex:0 0 auto;width:41.6666666667%}.sd-col-lg-6{-ms-flex:0 0 auto;flex:0 0 auto;width:50%}.sd-col-lg-7{-ms-flex:0 0 auto;flex:0 0 auto;width:58.3333333333%}.sd-col-lg-8{-ms-flex:0 0 auto;flex:0 0 auto;width:66.6666666667%}.sd-col-lg-9{-ms-flex:0 0 auto;flex:0 0 auto;width:75%}.sd-col-lg-10{-ms-flex:0 0 auto;flex:0 0 auto;width:83.3333333333%}.sd-col-lg-11{-ms-flex:0 0 auto;flex:0 0 auto;width:91.6666666667%}.sd-col-lg-12{-ms-flex:0 0 auto;flex:0 0 auto;width:100%}.sd-g-lg-0,.sd-gy-lg-0{--sd-gutter-y: 0}.sd-g-lg-0,.sd-gx-lg-0{--sd-gutter-x: 0}.sd-g-lg-1,.sd-gy-lg-1{--sd-gutter-y: 0.25rem}.sd-g-lg-1,.sd-gx-lg-1{--sd-gutter-x: 0.25rem}.sd-g-lg-2,.sd-gy-lg-2{--sd-gutter-y: 0.5rem}.sd-g-lg-2,.sd-gx-lg-2{--sd-gutter-x: 0.5rem}.sd-g-lg-3,.sd-gy-lg-3{--sd-gutter-y: 1rem}.sd-g-lg-3,.sd-gx-lg-3{--sd-gutter-x: 1rem}.sd-g-lg-4,.sd-gy-lg-4{--sd-gutter-y: 1.5rem}.sd-g-lg-4,.sd-gx-lg-4{--sd-gutter-x: 1.5rem}.sd-g-lg-5,.sd-gy-lg-5{--sd-gutter-y: 3rem}.sd-g-lg-5,.sd-gx-lg-5{--sd-gutter-x: 3rem}}@media(min-width: 1200px){.sd-col-xl-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto}.sd-col-xl-1{-ms-flex:0 0 auto;flex:0 0 auto;width:8.3333333333%}.sd-col-xl-2{-ms-flex:0 0 auto;flex:0 0 auto;width:16.6666666667%}.sd-col-xl-3{-ms-flex:0 0 auto;flex:0 0 auto;width:25%}.sd-col-xl-4{-ms-flex:0 0 auto;flex:0 0 auto;width:33.3333333333%}.sd-col-xl-5{-ms-flex:0 0 auto;flex:0 0 auto;width:41.6666666667%}.sd-col-xl-6{-ms-flex:0 0 auto;flex:0 0 auto;width:50%}.sd-col-xl-7{-ms-flex:0 0 auto;flex:0 0 auto;width:58.3333333333%}.sd-col-xl-8{-ms-flex:0 0 auto;flex:0 0 auto;width:66.6666666667%}.sd-col-xl-9{-ms-flex:0 0 auto;flex:0 0 auto;width:75%}.sd-col-xl-10{-ms-flex:0 0 auto;flex:0 0 auto;width:83.3333333333%}.sd-col-xl-11{-ms-flex:0 0 auto;flex:0 0 auto;width:91.6666666667%}.sd-col-xl-12{-ms-flex:0 0 auto;flex:0 0 auto;width:100%}.sd-g-xl-0,.sd-gy-xl-0{--sd-gutter-y: 0}.sd-g-xl-0,.sd-gx-xl-0{--sd-gutter-x: 0}.sd-g-xl-1,.sd-gy-xl-1{--sd-gutter-y: 0.25rem}.sd-g-xl-1,.sd-gx-xl-1{--sd-gutter-x: 0.25rem}.sd-g-xl-2,.sd-gy-xl-2{--sd-gutter-y: 0.5rem}.sd-g-xl-2,.sd-gx-xl-2{--sd-gutter-x: 0.5rem}.sd-g-xl-3,.sd-gy-xl-3{--sd-gutter-y: 1rem}.sd-g-xl-3,.sd-gx-xl-3{--sd-gutter-x: 1rem}.sd-g-xl-4,.sd-gy-xl-4{--sd-gutter-y: 1.5rem}.sd-g-xl-4,.sd-gx-xl-4{--sd-gutter-x: 1.5rem}.sd-g-xl-5,.sd-gy-xl-5{--sd-gutter-y: 3rem}.sd-g-xl-5,.sd-gx-xl-5{--sd-gutter-x: 3rem}}.sd-flex-row-reverse{flex-direction:row-reverse !important}details.sd-dropdown{position:relative;font-size:var(--sd-fontsize-dropdown)}details.sd-dropdown:hover{cursor:pointer}details.sd-dropdown .sd-summary-content{cursor:default}details.sd-dropdown summary.sd-summary-title{padding:.5em 1em;font-size:var(--sd-fontsize-dropdown-title);font-weight:var(--sd-fontweight-dropdown-title);user-select:none;-moz-user-select:none;-ms-user-select:none;-webkit-user-select:none;list-style:none;display:inline-flex;justify-content:space-between}details.sd-dropdown summary.sd-summary-title::-webkit-details-marker{display:none}details.sd-dropdown summary.sd-summary-title:focus{outline:none}details.sd-dropdown summary.sd-summary-title .sd-summary-icon{margin-right:.6em;display:inline-flex;align-items:center}details.sd-dropdown summary.sd-summary-title .sd-summary-icon svg{opacity:.8}details.sd-dropdown summary.sd-summary-title .sd-summary-text{flex-grow:1;line-height:1.5;padding-right:.5rem}details.sd-dropdown summary.sd-summary-title .sd-summary-state-marker{pointer-events:none;display:inline-flex;align-items:center}details.sd-dropdown summary.sd-summary-title .sd-summary-state-marker svg{opacity:.6}details.sd-dropdown summary.sd-summary-title:hover .sd-summary-state-marker svg{opacity:1;transform:scale(1.1)}details.sd-dropdown[open] summary .sd-octicon.no-title{visibility:hidden}details.sd-dropdown .sd-summary-chevron-right{transition:.25s}details.sd-dropdown[open]>.sd-summary-title .sd-summary-chevron-right{transform:rotate(90deg)}details.sd-dropdown[open]>.sd-summary-title .sd-summary-chevron-down{transform:rotate(180deg)}details.sd-dropdown:not([open]).sd-card{border:none}details.sd-dropdown:not([open])>.sd-card-header{border:1px solid var(--sd-color-card-border);border-radius:.25rem}details.sd-dropdown.sd-fade-in[open] summary~*{-moz-animation:sd-fade-in .5s ease-in-out;-webkit-animation:sd-fade-in .5s ease-in-out;animation:sd-fade-in .5s ease-in-out}details.sd-dropdown.sd-fade-in-slide-down[open] summary~*{-moz-animation:sd-fade-in .5s ease-in-out,sd-slide-down .5s ease-in-out;-webkit-animation:sd-fade-in .5s ease-in-out,sd-slide-down .5s ease-in-out;animation:sd-fade-in .5s ease-in-out,sd-slide-down .5s ease-in-out}.sd-col>.sd-dropdown{width:100%}.sd-summary-content>.sd-tab-set:first-child{margin-top:0}@keyframes sd-fade-in{0%{opacity:0}100%{opacity:1}}@keyframes sd-slide-down{0%{transform:translate(0, -10px)}100%{transform:translate(0, 0)}}.sd-tab-set{border-radius:.125rem;display:flex;flex-wrap:wrap;margin:1em 0;position:relative}.sd-tab-set>input{opacity:0;position:absolute}.sd-tab-set>input:checked+label{border-color:var(--sd-color-tabs-underline-active);color:var(--sd-color-tabs-label-active)}.sd-tab-set>input:checked+label+.sd-tab-content{display:block}.sd-tab-set>input:not(:checked)+label:hover{color:var(--sd-color-tabs-label-hover);border-color:var(--sd-color-tabs-underline-hover)}.sd-tab-set>input:focus+label{outline-style:auto}.sd-tab-set>input:not(.focus-visible)+label{outline:none;-webkit-tap-highlight-color:transparent}.sd-tab-set>label{border-bottom:.125rem solid transparent;margin-bottom:0;color:var(--sd-color-tabs-label-inactive);border-color:var(--sd-color-tabs-underline-inactive);cursor:pointer;font-size:var(--sd-fontsize-tabs-label);font-weight:700;padding:1em 1.25em .5em;transition:color 250ms;width:auto;z-index:1}html .sd-tab-set>label:hover{color:var(--sd-color-tabs-label-active)}.sd-col>.sd-tab-set{width:100%}.sd-tab-content{box-shadow:0 -0.0625rem var(--sd-color-tabs-overline),0 .0625rem var(--sd-color-tabs-underline);display:none;order:99;padding-bottom:.75rem;padding-top:.75rem;width:100%}.sd-tab-content>:first-child{margin-top:0 !important}.sd-tab-content>:last-child{margin-bottom:0 !important}.sd-tab-content>.sd-tab-set{margin:0}.sd-sphinx-override,.sd-sphinx-override *{-moz-box-sizing:border-box;-webkit-box-sizing:border-box;box-sizing:border-box}.sd-sphinx-override p{margin-top:0}:root{--sd-color-primary: #0071bc;--sd-color-secondary: #6c757d;--sd-color-success: #28a745;--sd-color-info: #17a2b8;--sd-color-warning: #f0b37e;--sd-color-danger: #dc3545;--sd-color-light: #f8f9fa;--sd-color-muted: #6c757d;--sd-color-dark: #212529;--sd-color-black: black;--sd-color-white: white;--sd-color-primary-highlight: #0060a0;--sd-color-secondary-highlight: #5c636a;--sd-color-success-highlight: #228e3b;--sd-color-info-highlight: #148a9c;--sd-color-warning-highlight: #cc986b;--sd-color-danger-highlight: #bb2d3b;--sd-color-light-highlight: #d3d4d5;--sd-color-muted-highlight: #5c636a;--sd-color-dark-highlight: #1c1f23;--sd-color-black-highlight: black;--sd-color-white-highlight: #d9d9d9;--sd-color-primary-bg: rgba(0, 113, 188, 0.2);--sd-color-secondary-bg: rgba(108, 117, 125, 0.2);--sd-color-success-bg: rgba(40, 167, 69, 0.2);--sd-color-info-bg: rgba(23, 162, 184, 0.2);--sd-color-warning-bg: rgba(240, 179, 126, 0.2);--sd-color-danger-bg: rgba(220, 53, 69, 0.2);--sd-color-light-bg: rgba(248, 249, 250, 0.2);--sd-color-muted-bg: rgba(108, 117, 125, 0.2);--sd-color-dark-bg: rgba(33, 37, 41, 0.2);--sd-color-black-bg: rgba(0, 0, 0, 0.2);--sd-color-white-bg: rgba(255, 255, 255, 0.2);--sd-color-primary-text: #fff;--sd-color-secondary-text: #fff;--sd-color-success-text: #fff;--sd-color-info-text: #fff;--sd-color-warning-text: #212529;--sd-color-danger-text: #fff;--sd-color-light-text: #212529;--sd-color-muted-text: #fff;--sd-color-dark-text: #fff;--sd-color-black-text: #fff;--sd-color-white-text: #212529;--sd-color-shadow: rgba(0, 0, 0, 0.15);--sd-color-card-border: rgba(0, 0, 0, 0.125);--sd-color-card-border-hover: hsla(231, 99%, 66%, 1);--sd-color-card-background: transparent;--sd-color-card-text: inherit;--sd-color-card-header: transparent;--sd-color-card-footer: transparent;--sd-color-tabs-label-active: hsla(231, 99%, 66%, 1);--sd-color-tabs-label-hover: hsla(231, 99%, 66%, 1);--sd-color-tabs-label-inactive: hsl(0, 0%, 66%);--sd-color-tabs-underline-active: hsla(231, 99%, 66%, 1);--sd-color-tabs-underline-hover: rgba(178, 206, 245, 0.62);--sd-color-tabs-underline-inactive: transparent;--sd-color-tabs-overline: rgb(222, 222, 222);--sd-color-tabs-underline: rgb(222, 222, 222);--sd-fontsize-tabs-label: 1rem;--sd-fontsize-dropdown: inherit;--sd-fontsize-dropdown-title: 1rem;--sd-fontweight-dropdown-title: 700} diff --git a/_static/_sphinx_javascript_frameworks_compat.js b/_static/_sphinx_javascript_frameworks_compat.js new file mode 100644 index 0000000..8141580 --- /dev/null +++ b/_static/_sphinx_javascript_frameworks_compat.js @@ -0,0 +1,123 @@ +/* Compatability shim for jQuery and underscores.js. + * + * Copyright Sphinx contributors + * Released under the two clause BSD licence + */ + +/** + * small helper function to urldecode strings + * + * See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/decodeURIComponent#Decoding_query_parameters_from_a_URL + */ +jQuery.urldecode = function(x) { + if (!x) { + return x + } + return decodeURIComponent(x.replace(/\+/g, ' ')); +}; + +/** + * small helper function to urlencode strings + */ +jQuery.urlencode = encodeURIComponent; + +/** + * This function returns the parsed url parameters of the + * current request. Multiple values per key are supported, + * it will always return arrays of strings for the value parts. + */ +jQuery.getQueryParameters = function(s) { + if (typeof s === 'undefined') + s = document.location.search; + var parts = s.substr(s.indexOf('?') + 1).split('&'); + var result = {}; + for (var i = 0; i < parts.length; i++) { + var tmp = parts[i].split('=', 2); + var key = jQuery.urldecode(tmp[0]); + var value = jQuery.urldecode(tmp[1]); + if (key in result) + result[key].push(value); + else + result[key] = [value]; + } + return result; +}; + +/** + * highlight a given string on a jquery object by wrapping it in + * span elements with the given class name. + */ +jQuery.fn.highlightText = function(text, className) { + function highlight(node, addItems) { + if (node.nodeType === 3) { + var val = node.nodeValue; + var pos = val.toLowerCase().indexOf(text); + if (pos >= 0 && + !jQuery(node.parentNode).hasClass(className) && + !jQuery(node.parentNode).hasClass("nohighlight")) { + var span; + var isInSVG = jQuery(node).closest("body, svg, foreignObject").is("svg"); + if (isInSVG) { + span = document.createElementNS("http://www.w3.org/2000/svg", "tspan"); + } else { + span = document.createElement("span"); + span.className = className; + } + span.appendChild(document.createTextNode(val.substr(pos, text.length))); + node.parentNode.insertBefore(span, node.parentNode.insertBefore( + document.createTextNode(val.substr(pos + text.length)), + node.nextSibling)); + node.nodeValue = val.substr(0, pos); + if (isInSVG) { + var rect = document.createElementNS("http://www.w3.org/2000/svg", "rect"); + var bbox = node.parentElement.getBBox(); + rect.x.baseVal.value = bbox.x; + rect.y.baseVal.value = bbox.y; + rect.width.baseVal.value = bbox.width; + rect.height.baseVal.value = bbox.height; + rect.setAttribute('class', className); + addItems.push({ + "parent": node.parentNode, + "target": rect}); + } + } + } + else if (!jQuery(node).is("button, select, textarea")) { + jQuery.each(node.childNodes, function() { + highlight(this, addItems); + }); + } + } + var addItems = []; + var result = this.each(function() { + highlight(this, addItems); + }); + for (var i = 0; i < addItems.length; ++i) { + jQuery(addItems[i].parent).before(addItems[i].target); + } + return result; +}; + +/* + * backward compatibility for jQuery.browser + * This will be supported until firefox bug is fixed. + */ +if (!jQuery.browser) { + jQuery.uaMatch = function(ua) { + ua = ua.toLowerCase(); + + var match = /(chrome)[ \/]([\w.]+)/.exec(ua) || + /(webkit)[ \/]([\w.]+)/.exec(ua) || + /(opera)(?:.*version|)[ \/]([\w.]+)/.exec(ua) || + /(msie) ([\w.]+)/.exec(ua) || + ua.indexOf("compatible") < 0 && /(mozilla)(?:.*? rv:([\w.]+)|)/.exec(ua) || + []; + + return { + browser: match[ 1 ] || "", + version: match[ 2 ] || "0" + }; + }; + jQuery.browser = {}; + jQuery.browser[jQuery.uaMatch(navigator.userAgent).browser] = true; +} diff --git a/_static/basic.css b/_static/basic.css new file mode 100644 index 0000000..f316efc --- /dev/null +++ b/_static/basic.css @@ -0,0 +1,925 @@ +/* + * basic.css + * ~~~~~~~~~ + * + * Sphinx stylesheet -- basic theme. + * + * :copyright: Copyright 2007-2024 by the Sphinx team, see AUTHORS. + * :license: BSD, see LICENSE for details. + * + */ + +/* -- main layout ----------------------------------------------------------- */ + +div.clearer { + clear: both; +} + +div.section::after { + display: block; + content: ''; + clear: left; +} + +/* -- relbar ---------------------------------------------------------------- */ + +div.related { + width: 100%; + font-size: 90%; +} + +div.related h3 { + display: none; +} + +div.related ul { + margin: 0; + padding: 0 0 0 10px; + list-style: none; +} + +div.related li { + display: inline; +} + +div.related li.right { + float: right; + margin-right: 5px; +} + +/* -- sidebar --------------------------------------------------------------- */ + +div.sphinxsidebarwrapper { + padding: 10px 5px 0 10px; +} + +div.sphinxsidebar { + float: left; + width: 230px; + margin-left: -100%; + font-size: 90%; + word-wrap: break-word; + overflow-wrap : break-word; +} + +div.sphinxsidebar ul { + list-style: none; +} + +div.sphinxsidebar ul ul, +div.sphinxsidebar ul.want-points { + margin-left: 20px; + list-style: square; +} + +div.sphinxsidebar ul ul { + margin-top: 0; + margin-bottom: 0; +} + +div.sphinxsidebar form { + margin-top: 10px; +} + +div.sphinxsidebar input { + border: 1px solid #98dbcc; + font-family: sans-serif; + font-size: 1em; +} + +div.sphinxsidebar #searchbox form.search { + overflow: hidden; +} + +div.sphinxsidebar #searchbox input[type="text"] { + float: left; + width: 80%; + padding: 0.25em; + box-sizing: border-box; +} + +div.sphinxsidebar #searchbox input[type="submit"] { + float: left; + width: 20%; + border-left: none; + padding: 0.25em; + box-sizing: border-box; +} + + +img { + border: 0; + max-width: 100%; +} + +/* -- search page ----------------------------------------------------------- */ + +ul.search { + margin: 10px 0 0 20px; + padding: 0; +} + +ul.search li { + padding: 5px 0 5px 20px; + background-image: url(file.png); + background-repeat: no-repeat; + background-position: 0 7px; +} + +ul.search li a { + font-weight: bold; +} + +ul.search li p.context { + color: #888; + margin: 2px 0 0 30px; + text-align: left; +} + +ul.keywordmatches li.goodmatch a { + font-weight: bold; +} + +/* -- index page ------------------------------------------------------------ */ + +table.contentstable { + width: 90%; + margin-left: auto; + margin-right: auto; +} + +table.contentstable p.biglink { + line-height: 150%; +} + +a.biglink { + font-size: 1.3em; +} + +span.linkdescr { + font-style: italic; + padding-top: 5px; + font-size: 90%; +} + +/* -- general index --------------------------------------------------------- */ + +table.indextable { + width: 100%; +} + +table.indextable td { + text-align: left; + vertical-align: top; +} + +table.indextable ul { + margin-top: 0; + margin-bottom: 0; + list-style-type: none; +} + +table.indextable > tbody > tr > td > ul { + padding-left: 0em; +} + +table.indextable tr.pcap { + height: 10px; +} + +table.indextable tr.cap { + margin-top: 10px; + background-color: #f2f2f2; +} + +img.toggler { + margin-right: 3px; + margin-top: 3px; + cursor: pointer; +} + +div.modindex-jumpbox { + border-top: 1px solid #ddd; + border-bottom: 1px solid #ddd; + margin: 1em 0 1em 0; + padding: 0.4em; +} + +div.genindex-jumpbox { + border-top: 1px solid #ddd; + border-bottom: 1px solid #ddd; + margin: 1em 0 1em 0; + padding: 0.4em; +} + +/* -- domain module index --------------------------------------------------- */ + +table.modindextable td { + padding: 2px; + border-collapse: collapse; +} + +/* -- general body styles --------------------------------------------------- */ + +div.body { + min-width: 360px; + max-width: 800px; +} + +div.body p, div.body dd, div.body li, div.body blockquote { + -moz-hyphens: auto; + -ms-hyphens: auto; + -webkit-hyphens: auto; + hyphens: auto; +} + +a.headerlink { + visibility: hidden; +} + +a:visited { + color: #551A8B; +} + +h1:hover > a.headerlink, +h2:hover > a.headerlink, +h3:hover > a.headerlink, +h4:hover > a.headerlink, +h5:hover > a.headerlink, +h6:hover > a.headerlink, +dt:hover > a.headerlink, +caption:hover > a.headerlink, +p.caption:hover > a.headerlink, +div.code-block-caption:hover > a.headerlink { + visibility: visible; +} + +div.body p.caption { + text-align: inherit; +} + +div.body td { + text-align: left; +} + +.first { + margin-top: 0 !important; +} + +p.rubric { + margin-top: 30px; + font-weight: bold; +} + +img.align-left, figure.align-left, .figure.align-left, object.align-left { + clear: left; + float: left; + margin-right: 1em; +} + +img.align-right, figure.align-right, .figure.align-right, object.align-right { + clear: right; + float: right; + margin-left: 1em; +} + +img.align-center, figure.align-center, .figure.align-center, object.align-center { + display: block; + margin-left: auto; + margin-right: auto; +} + +img.align-default, figure.align-default, .figure.align-default { + display: block; + margin-left: auto; + margin-right: auto; +} + +.align-left { + text-align: left; +} + +.align-center { + text-align: center; +} + +.align-default { + text-align: center; +} + +.align-right { + text-align: right; +} + +/* -- sidebars -------------------------------------------------------------- */ + +div.sidebar, +aside.sidebar { + margin: 0 0 0.5em 1em; + border: 1px solid #ddb; + padding: 7px; + background-color: #ffe; + width: 40%; + float: right; + clear: right; + overflow-x: auto; +} + +p.sidebar-title { + font-weight: bold; +} + +nav.contents, +aside.topic, +div.admonition, div.topic, blockquote { + clear: left; +} + +/* -- topics ---------------------------------------------------------------- */ + +nav.contents, +aside.topic, +div.topic { + border: 1px solid #ccc; + padding: 7px; + margin: 10px 0 10px 0; +} + +p.topic-title { + font-size: 1.1em; + font-weight: bold; + margin-top: 10px; +} + +/* -- admonitions ----------------------------------------------------------- */ + +div.admonition { + margin-top: 10px; + margin-bottom: 10px; + padding: 7px; +} + +div.admonition dt { + font-weight: bold; +} + +p.admonition-title { + margin: 0px 10px 5px 0px; + font-weight: bold; +} + +div.body p.centered { + text-align: center; + margin-top: 25px; +} + +/* -- content of sidebars/topics/admonitions -------------------------------- */ + +div.sidebar > :last-child, +aside.sidebar > :last-child, +nav.contents > :last-child, +aside.topic > :last-child, +div.topic > :last-child, +div.admonition > :last-child { + margin-bottom: 0; +} + +div.sidebar::after, +aside.sidebar::after, +nav.contents::after, +aside.topic::after, +div.topic::after, +div.admonition::after, +blockquote::after { + display: block; + content: ''; + clear: both; +} + +/* -- tables ---------------------------------------------------------------- */ + +table.docutils { + margin-top: 10px; + margin-bottom: 10px; + border: 0; + border-collapse: collapse; +} + +table.align-center { + margin-left: auto; + margin-right: auto; +} + +table.align-default { + margin-left: auto; + margin-right: auto; +} + +table caption span.caption-number { + font-style: italic; +} + +table caption span.caption-text { +} + +table.docutils td, table.docutils th { + padding: 1px 8px 1px 5px; + border-top: 0; + border-left: 0; + border-right: 0; + border-bottom: 1px solid #aaa; +} + +th { + text-align: left; + padding-right: 5px; +} + +table.citation { + border-left: solid 1px gray; + margin-left: 1px; +} + +table.citation td { + border-bottom: none; +} + +th > :first-child, +td > :first-child { + margin-top: 0px; +} + +th > :last-child, +td > :last-child { + margin-bottom: 0px; +} + +/* -- figures --------------------------------------------------------------- */ + +div.figure, figure { + margin: 0.5em; + padding: 0.5em; +} + +div.figure p.caption, figcaption { + padding: 0.3em; +} + +div.figure p.caption span.caption-number, +figcaption span.caption-number { + font-style: italic; +} + +div.figure p.caption span.caption-text, +figcaption span.caption-text { +} + +/* -- field list styles ----------------------------------------------------- */ + +table.field-list td, table.field-list th { + border: 0 !important; +} + +.field-list ul { + margin: 0; + padding-left: 1em; +} + +.field-list p { + margin: 0; +} + +.field-name { + -moz-hyphens: manual; + -ms-hyphens: manual; + -webkit-hyphens: manual; + hyphens: manual; +} + +/* -- hlist styles ---------------------------------------------------------- */ + +table.hlist { + margin: 1em 0; +} + +table.hlist td { + vertical-align: top; +} + +/* -- object description styles --------------------------------------------- */ + +.sig { + font-family: 'Consolas', 'Menlo', 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', monospace; +} + +.sig-name, code.descname { + background-color: transparent; + font-weight: bold; +} + +.sig-name { + font-size: 1.1em; +} + +code.descname { + font-size: 1.2em; +} + +.sig-prename, code.descclassname { + background-color: transparent; +} + +.optional { + font-size: 1.3em; +} + +.sig-paren { + font-size: larger; +} + +.sig-param.n { + font-style: italic; +} + +/* C++ specific styling */ + +.sig-inline.c-texpr, +.sig-inline.cpp-texpr { + font-family: unset; +} + +.sig.c .k, .sig.c .kt, +.sig.cpp .k, .sig.cpp .kt { + color: #0033B3; +} + +.sig.c .m, +.sig.cpp .m { + color: #1750EB; +} + +.sig.c .s, .sig.c .sc, +.sig.cpp .s, .sig.cpp .sc { + color: #067D17; +} + + +/* -- other body styles ----------------------------------------------------- */ + +ol.arabic { + list-style: decimal; +} + +ol.loweralpha { + list-style: lower-alpha; +} + +ol.upperalpha { + list-style: upper-alpha; +} + +ol.lowerroman { + list-style: lower-roman; +} + +ol.upperroman { + list-style: upper-roman; +} + +:not(li) > ol > li:first-child > :first-child, +:not(li) > ul > li:first-child > :first-child { + margin-top: 0px; +} + +:not(li) > ol > li:last-child > :last-child, +:not(li) > ul > li:last-child > :last-child { + margin-bottom: 0px; +} + +ol.simple ol p, +ol.simple ul p, +ul.simple ol p, +ul.simple ul p { + margin-top: 0; +} + +ol.simple > li:not(:first-child) > p, +ul.simple > li:not(:first-child) > p { + margin-top: 0; +} + +ol.simple p, +ul.simple p { + margin-bottom: 0; +} + +aside.footnote > span, +div.citation > span { + float: left; +} +aside.footnote > span:last-of-type, +div.citation > span:last-of-type { + padding-right: 0.5em; +} +aside.footnote > p { + margin-left: 2em; +} +div.citation > p { + margin-left: 4em; +} +aside.footnote > p:last-of-type, +div.citation > p:last-of-type { + margin-bottom: 0em; +} +aside.footnote > p:last-of-type:after, +div.citation > p:last-of-type:after { + content: ""; + clear: both; +} + +dl.field-list { + display: grid; + grid-template-columns: fit-content(30%) auto; +} + +dl.field-list > dt { + font-weight: bold; + word-break: break-word; + padding-left: 0.5em; + padding-right: 5px; +} + +dl.field-list > dd { + padding-left: 0.5em; + margin-top: 0em; + margin-left: 0em; + margin-bottom: 0em; +} + +dl { + margin-bottom: 15px; +} + +dd > :first-child { + margin-top: 0px; +} + +dd ul, dd table { + margin-bottom: 10px; +} + +dd { + margin-top: 3px; + margin-bottom: 10px; + margin-left: 30px; +} + +.sig dd { + margin-top: 0px; + margin-bottom: 0px; +} + +.sig dl { + margin-top: 0px; + margin-bottom: 0px; +} + +dl > dd:last-child, +dl > dd:last-child > :last-child { + margin-bottom: 0; +} + +dt:target, span.highlighted { + background-color: #fbe54e; +} + +rect.highlighted { + fill: #fbe54e; +} + +dl.glossary dt { + font-weight: bold; + font-size: 1.1em; +} + +.versionmodified { + font-style: italic; +} + +.system-message { + background-color: #fda; + padding: 5px; + border: 3px solid red; +} + +.footnote:target { + background-color: #ffa; +} + +.line-block { + display: block; + margin-top: 1em; + margin-bottom: 1em; +} + +.line-block .line-block { + margin-top: 0; + margin-bottom: 0; + margin-left: 1.5em; +} + +.guilabel, .menuselection { + font-family: sans-serif; +} + +.accelerator { + text-decoration: underline; +} + +.classifier { + font-style: oblique; +} + +.classifier:before { + font-style: normal; + margin: 0 0.5em; + content: ":"; + display: inline-block; +} + +abbr, acronym { + border-bottom: dotted 1px; + cursor: help; +} + +.translated { + background-color: rgba(207, 255, 207, 0.2) +} + +.untranslated { + background-color: rgba(255, 207, 207, 0.2) +} + +/* -- code displays --------------------------------------------------------- */ + +pre { + overflow: auto; + overflow-y: hidden; /* fixes display issues on Chrome browsers */ +} + +pre, div[class*="highlight-"] { + clear: both; +} + +span.pre { + -moz-hyphens: none; + -ms-hyphens: none; + -webkit-hyphens: none; + hyphens: none; + white-space: nowrap; +} + +div[class*="highlight-"] { + margin: 1em 0; +} + +td.linenos pre { + border: 0; + background-color: transparent; + color: #aaa; +} + +table.highlighttable { + display: block; +} + +table.highlighttable tbody { + display: block; +} + +table.highlighttable tr { + display: flex; +} + +table.highlighttable td { + margin: 0; + padding: 0; +} + +table.highlighttable td.linenos { + padding-right: 0.5em; +} + +table.highlighttable td.code { + flex: 1; + overflow: hidden; +} + +.highlight .hll { + display: block; +} + +div.highlight pre, +table.highlighttable pre { + margin: 0; +} + +div.code-block-caption + div { + margin-top: 0; +} + +div.code-block-caption { + margin-top: 1em; + padding: 2px 5px; + font-size: small; +} + +div.code-block-caption code { + background-color: transparent; +} + +table.highlighttable td.linenos, +span.linenos, +div.highlight span.gp { /* gp: Generic.Prompt */ + user-select: none; + -webkit-user-select: text; /* Safari fallback only */ + -webkit-user-select: none; /* Chrome/Safari */ + -moz-user-select: none; /* Firefox */ + -ms-user-select: none; /* IE10+ */ +} + +div.code-block-caption span.caption-number { + padding: 0.1em 0.3em; + font-style: italic; +} + +div.code-block-caption span.caption-text { +} + +div.literal-block-wrapper { + margin: 1em 0; +} + +code.xref, a code { + background-color: transparent; + font-weight: bold; +} + +h1 code, h2 code, h3 code, h4 code, h5 code, h6 code { + background-color: transparent; +} + +.viewcode-link { + float: right; +} + +.viewcode-back { + float: right; + font-family: sans-serif; +} + +div.viewcode-block:target { + margin: -1px -10px; + padding: 0 10px; +} + +/* -- math display ---------------------------------------------------------- */ + +img.math { + vertical-align: middle; +} + +div.body div.math p { + text-align: center; +} + +span.eqno { + float: right; +} + +span.eqno a.headerlink { + position: absolute; + z-index: 1; +} + +div.math:hover a.headerlink { + visibility: visible; +} + +/* -- printout stylesheet --------------------------------------------------- */ + +@media print { + div.document, + div.documentwrapper, + div.bodywrapper { + margin: 0 !important; + width: 100%; + } + + div.sphinxsidebar, + div.related, + div.footer, + #top-link { + display: none; + } +} \ No newline at end of file diff --git a/_static/css/badge_only.css b/_static/css/badge_only.css new file mode 100644 index 0000000..c718cee --- /dev/null +++ b/_static/css/badge_only.css @@ -0,0 +1 @@ +.clearfix{*zoom:1}.clearfix:after,.clearfix:before{display:table;content:""}.clearfix:after{clear:both}@font-face{font-family:FontAwesome;font-style:normal;font-weight:400;src:url(fonts/fontawesome-webfont.eot?674f50d287a8c48dc19ba404d20fe713?#iefix) format("embedded-opentype"),url(fonts/fontawesome-webfont.woff2?af7ae505a9eed503f8b8e6982036873e) format("woff2"),url(fonts/fontawesome-webfont.woff?fee66e712a8a08eef5805a46892932ad) format("woff"),url(fonts/fontawesome-webfont.ttf?b06871f281fee6b241d60582ae9369b9) format("truetype"),url(fonts/fontawesome-webfont.svg?912ec66d7572ff821749319396470bde#FontAwesome) format("svg")}.fa:before{font-family:FontAwesome;font-style:normal;font-weight:400;line-height:1}.fa:before,a .fa{text-decoration:inherit}.fa:before,a .fa,li .fa{display:inline-block}li .fa-large:before{width:1.875em}ul.fas{list-style-type:none;margin-left:2em;text-indent:-.8em}ul.fas li .fa{width:.8em}ul.fas li .fa-large:before{vertical-align:baseline}.fa-book:before,.icon-book:before{content:"\f02d"}.fa-caret-down:before,.icon-caret-down:before{content:"\f0d7"}.fa-caret-up:before,.icon-caret-up:before{content:"\f0d8"}.fa-caret-left:before,.icon-caret-left:before{content:"\f0d9"}.fa-caret-right:before,.icon-caret-right:before{content:"\f0da"}.rst-versions{position:fixed;bottom:0;left:0;width:300px;color:#fcfcfc;background:#1f1d1d;font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif;z-index:400}.rst-versions a{color:#2980b9;text-decoration:none}.rst-versions .rst-badge-small{display:none}.rst-versions .rst-current-version{padding:12px;background-color:#272525;display:block;text-align:right;font-size:90%;cursor:pointer;color:#27ae60}.rst-versions .rst-current-version:after{clear:both;content:"";display:block}.rst-versions .rst-current-version .fa{color:#fcfcfc}.rst-versions .rst-current-version .fa-book,.rst-versions .rst-current-version .icon-book{float:left}.rst-versions .rst-current-version.rst-out-of-date{background-color:#e74c3c;color:#fff}.rst-versions .rst-current-version.rst-active-old-version{background-color:#f1c40f;color:#000}.rst-versions.shift-up{height:auto;max-height:100%;overflow-y:scroll}.rst-versions.shift-up .rst-other-versions{display:block}.rst-versions .rst-other-versions{font-size:90%;padding:12px;color:grey;display:none}.rst-versions .rst-other-versions hr{display:block;height:1px;border:0;margin:20px 0;padding:0;border-top:1px solid #413d3d}.rst-versions .rst-other-versions dd{display:inline-block;margin:0}.rst-versions .rst-other-versions dd a{display:inline-block;padding:6px;color:#fcfcfc}.rst-versions.rst-badge{width:auto;bottom:20px;right:20px;left:auto;border:none;max-width:300px;max-height:90%}.rst-versions.rst-badge .fa-book,.rst-versions.rst-badge .icon-book{float:none;line-height:30px}.rst-versions.rst-badge.shift-up .rst-current-version{text-align:right}.rst-versions.rst-badge.shift-up .rst-current-version .fa-book,.rst-versions.rst-badge.shift-up .rst-current-version .icon-book{float:left}.rst-versions.rst-badge>.rst-current-version{width:auto;height:30px;line-height:30px;padding:0 6px;display:block;text-align:center}@media screen and (max-width:768px){.rst-versions{width:85%;display:none}.rst-versions.shift{display:block}} \ No newline at end of file diff --git a/_static/css/fonts/Roboto-Slab-Bold.woff b/_static/css/fonts/Roboto-Slab-Bold.woff new file mode 100644 index 0000000..6cb6000 Binary files /dev/null and b/_static/css/fonts/Roboto-Slab-Bold.woff differ diff --git a/_static/css/fonts/Roboto-Slab-Bold.woff2 b/_static/css/fonts/Roboto-Slab-Bold.woff2 new file mode 100644 index 0000000..7059e23 Binary files /dev/null and b/_static/css/fonts/Roboto-Slab-Bold.woff2 differ diff --git a/_static/css/fonts/Roboto-Slab-Regular.woff b/_static/css/fonts/Roboto-Slab-Regular.woff new file mode 100644 index 0000000..f815f63 Binary files /dev/null and b/_static/css/fonts/Roboto-Slab-Regular.woff differ diff --git a/_static/css/fonts/Roboto-Slab-Regular.woff2 b/_static/css/fonts/Roboto-Slab-Regular.woff2 new file mode 100644 index 0000000..f2c76e5 Binary files /dev/null and b/_static/css/fonts/Roboto-Slab-Regular.woff2 differ diff --git a/_static/css/fonts/fontawesome-webfont.eot b/_static/css/fonts/fontawesome-webfont.eot new file mode 100644 index 0000000..e9f60ca Binary files /dev/null and b/_static/css/fonts/fontawesome-webfont.eot differ diff --git a/_static/css/fonts/fontawesome-webfont.svg b/_static/css/fonts/fontawesome-webfont.svg new file mode 100644 index 0000000..855c845 --- /dev/null +++ b/_static/css/fonts/fontawesome-webfont.svg @@ -0,0 +1,2671 @@ + + + + +Created by FontForge 20120731 at Mon Oct 24 17:37:40 2016 + By ,,, +Copyright Dave Gandy 2016. All rights reserved. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/_static/css/fonts/fontawesome-webfont.ttf b/_static/css/fonts/fontawesome-webfont.ttf new file mode 100644 index 0000000..35acda2 Binary files /dev/null and b/_static/css/fonts/fontawesome-webfont.ttf differ diff --git a/_static/css/fonts/fontawesome-webfont.woff b/_static/css/fonts/fontawesome-webfont.woff new file mode 100644 index 0000000..400014a Binary files /dev/null and b/_static/css/fonts/fontawesome-webfont.woff differ diff --git a/_static/css/fonts/fontawesome-webfont.woff2 b/_static/css/fonts/fontawesome-webfont.woff2 new file mode 100644 index 0000000..4d13fc6 Binary files /dev/null and b/_static/css/fonts/fontawesome-webfont.woff2 differ diff --git a/_static/css/fonts/lato-bold-italic.woff b/_static/css/fonts/lato-bold-italic.woff new file mode 100644 index 0000000..88ad05b Binary files /dev/null and b/_static/css/fonts/lato-bold-italic.woff differ diff --git a/_static/css/fonts/lato-bold-italic.woff2 b/_static/css/fonts/lato-bold-italic.woff2 new file mode 100644 index 0000000..c4e3d80 Binary files /dev/null and b/_static/css/fonts/lato-bold-italic.woff2 differ diff --git a/_static/css/fonts/lato-bold.woff b/_static/css/fonts/lato-bold.woff new file mode 100644 index 0000000..c6dff51 Binary files /dev/null and b/_static/css/fonts/lato-bold.woff differ diff --git a/_static/css/fonts/lato-bold.woff2 b/_static/css/fonts/lato-bold.woff2 new file mode 100644 index 0000000..bb19504 Binary files /dev/null and b/_static/css/fonts/lato-bold.woff2 differ diff --git a/_static/css/fonts/lato-normal-italic.woff b/_static/css/fonts/lato-normal-italic.woff new file mode 100644 index 0000000..76114bc Binary files /dev/null and b/_static/css/fonts/lato-normal-italic.woff differ diff --git a/_static/css/fonts/lato-normal-italic.woff2 b/_static/css/fonts/lato-normal-italic.woff2 new file mode 100644 index 0000000..3404f37 Binary files /dev/null and b/_static/css/fonts/lato-normal-italic.woff2 differ diff --git a/_static/css/fonts/lato-normal.woff b/_static/css/fonts/lato-normal.woff new file mode 100644 index 0000000..ae1307f Binary files /dev/null and b/_static/css/fonts/lato-normal.woff differ diff --git a/_static/css/fonts/lato-normal.woff2 b/_static/css/fonts/lato-normal.woff2 new file mode 100644 index 0000000..3bf9843 Binary files /dev/null and b/_static/css/fonts/lato-normal.woff2 differ diff --git a/_static/css/theme.css b/_static/css/theme.css new file mode 100644 index 0000000..19a446a --- /dev/null +++ b/_static/css/theme.css @@ -0,0 +1,4 @@ +html{box-sizing:border-box}*,:after,:before{box-sizing:inherit}article,aside,details,figcaption,figure,footer,header,hgroup,nav,section{display:block}audio,canvas,video{display:inline-block;*display:inline;*zoom:1}[hidden],audio:not([controls]){display:none}*{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}html{font-size:100%;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%}body{margin:0}a:active,a:hover{outline:0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:700}blockquote{margin:0}dfn{font-style:italic}ins{background:#ff9;text-decoration:none}ins,mark{color:#000}mark{background:#ff0;font-style:italic;font-weight:700}.rst-content code,.rst-content tt,code,kbd,pre,samp{font-family:monospace,serif;_font-family:courier new,monospace;font-size:1em}pre{white-space:pre}q{quotes:none}q:after,q:before{content:"";content:none}small{font-size:85%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-.5em}sub{bottom:-.25em}dl,ol,ul{margin:0;padding:0;list-style:none;list-style-image:none}li{list-style:none}dd{margin:0}img{border:0;-ms-interpolation-mode:bicubic;vertical-align:middle;max-width:100%}svg:not(:root){overflow:hidden}figure,form{margin:0}label{cursor:pointer}button,input,select,textarea{font-size:100%;margin:0;vertical-align:baseline;*vertical-align:middle}button,input{line-height:normal}button,input[type=button],input[type=reset],input[type=submit]{cursor:pointer;-webkit-appearance:button;*overflow:visible}button[disabled],input[disabled]{cursor:default}input[type=search]{-webkit-appearance:textfield;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;box-sizing:content-box}textarea{resize:vertical}table{border-collapse:collapse;border-spacing:0}td{vertical-align:top}.chromeframe{margin:.2em 0;background:#ccc;color:#000;padding:.2em 0}.ir{display:block;border:0;text-indent:-999em;overflow:hidden;background-color:transparent;background-repeat:no-repeat;text-align:left;direction:ltr;*line-height:0}.ir br{display:none}.hidden{display:none!important;visibility:hidden}.visuallyhidden{border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.visuallyhidden.focusable:active,.visuallyhidden.focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto}.invisible{visibility:hidden}.relative{position:relative}big,small{font-size:100%}@media print{body,html,section{background:none!important}*{box-shadow:none!important;text-shadow:none!important;filter:none!important;-ms-filter:none!important}a,a:visited{text-decoration:underline}.ir a:after,a[href^="#"]:after,a[href^="javascript:"]:after{content:""}blockquote,pre{page-break-inside:avoid}thead{display:table-header-group}img,tr{page-break-inside:avoid}img{max-width:100%!important}@page{margin:.5cm}.rst-content .toctree-wrapper>p.caption,h2,h3,p{orphans:3;widows:3}.rst-content .toctree-wrapper>p.caption,h2,h3{page-break-after:avoid}}.btn,.fa:before,.icon:before,.rst-content .admonition,.rst-content .admonition-title:before,.rst-content .admonition-todo,.rst-content .attention,.rst-content .caution,.rst-content .code-block-caption .headerlink:before,.rst-content .danger,.rst-content .eqno .headerlink:before,.rst-content .error,.rst-content .hint,.rst-content .important,.rst-content .note,.rst-content .seealso,.rst-content .tip,.rst-content .warning,.rst-content code.download span:first-child:before,.rst-content dl dt .headerlink:before,.rst-content h1 .headerlink:before,.rst-content h2 .headerlink:before,.rst-content h3 .headerlink:before,.rst-content h4 .headerlink:before,.rst-content h5 .headerlink:before,.rst-content h6 .headerlink:before,.rst-content p.caption .headerlink:before,.rst-content p .headerlink:before,.rst-content table>caption .headerlink:before,.rst-content tt.download span:first-child:before,.wy-alert,.wy-dropdown .caret:before,.wy-inline-validate.wy-inline-validate-danger .wy-input-context:before,.wy-inline-validate.wy-inline-validate-info .wy-input-context:before,.wy-inline-validate.wy-inline-validate-success .wy-input-context:before,.wy-inline-validate.wy-inline-validate-warning .wy-input-context:before,.wy-menu-vertical li.current>a button.toctree-expand:before,.wy-menu-vertical li.on a button.toctree-expand:before,.wy-menu-vertical li button.toctree-expand:before,input[type=color],input[type=date],input[type=datetime-local],input[type=datetime],input[type=email],input[type=month],input[type=number],input[type=password],input[type=search],input[type=tel],input[type=text],input[type=time],input[type=url],input[type=week],select,textarea{-webkit-font-smoothing:antialiased}.clearfix{*zoom:1}.clearfix:after,.clearfix:before{display:table;content:""}.clearfix:after{clear:both}/*! + * Font Awesome 4.7.0 by @davegandy - http://fontawesome.io - @fontawesome + * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) + */@font-face{font-family:FontAwesome;src:url(fonts/fontawesome-webfont.eot?674f50d287a8c48dc19ba404d20fe713);src:url(fonts/fontawesome-webfont.eot?674f50d287a8c48dc19ba404d20fe713?#iefix&v=4.7.0) format("embedded-opentype"),url(fonts/fontawesome-webfont.woff2?af7ae505a9eed503f8b8e6982036873e) format("woff2"),url(fonts/fontawesome-webfont.woff?fee66e712a8a08eef5805a46892932ad) format("woff"),url(fonts/fontawesome-webfont.ttf?b06871f281fee6b241d60582ae9369b9) format("truetype"),url(fonts/fontawesome-webfont.svg?912ec66d7572ff821749319396470bde#fontawesomeregular) format("svg");font-weight:400;font-style:normal}.fa,.icon,.rst-content .admonition-title,.rst-content .code-block-caption .headerlink,.rst-content .eqno .headerlink,.rst-content code.download span:first-child,.rst-content dl dt .headerlink,.rst-content h1 .headerlink,.rst-content h2 .headerlink,.rst-content h3 .headerlink,.rst-content h4 .headerlink,.rst-content h5 .headerlink,.rst-content h6 .headerlink,.rst-content p.caption .headerlink,.rst-content p .headerlink,.rst-content table>caption .headerlink,.rst-content tt.download span:first-child,.wy-menu-vertical li.current>a button.toctree-expand,.wy-menu-vertical li.on a button.toctree-expand,.wy-menu-vertical li button.toctree-expand{display:inline-block;font:normal normal normal 14px/1 FontAwesome;font-size:inherit;text-rendering:auto;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.fa-lg{font-size:1.33333em;line-height:.75em;vertical-align:-15%}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-fw{width:1.28571em;text-align:center}.fa-ul{padding-left:0;margin-left:2.14286em;list-style-type:none}.fa-ul>li{position:relative}.fa-li{position:absolute;left:-2.14286em;width:2.14286em;top:.14286em;text-align:center}.fa-li.fa-lg{left:-1.85714em}.fa-border{padding:.2em .25em .15em;border:.08em solid #eee;border-radius:.1em}.fa-pull-left{float:left}.fa-pull-right{float:right}.fa-pull-left.icon,.fa.fa-pull-left,.rst-content .code-block-caption .fa-pull-left.headerlink,.rst-content .eqno .fa-pull-left.headerlink,.rst-content .fa-pull-left.admonition-title,.rst-content code.download span.fa-pull-left:first-child,.rst-content dl dt .fa-pull-left.headerlink,.rst-content h1 .fa-pull-left.headerlink,.rst-content h2 .fa-pull-left.headerlink,.rst-content h3 .fa-pull-left.headerlink,.rst-content h4 .fa-pull-left.headerlink,.rst-content h5 .fa-pull-left.headerlink,.rst-content h6 .fa-pull-left.headerlink,.rst-content p .fa-pull-left.headerlink,.rst-content table>caption .fa-pull-left.headerlink,.rst-content tt.download span.fa-pull-left:first-child,.wy-menu-vertical li.current>a button.fa-pull-left.toctree-expand,.wy-menu-vertical li.on a button.fa-pull-left.toctree-expand,.wy-menu-vertical li button.fa-pull-left.toctree-expand{margin-right:.3em}.fa-pull-right.icon,.fa.fa-pull-right,.rst-content .code-block-caption .fa-pull-right.headerlink,.rst-content .eqno .fa-pull-right.headerlink,.rst-content .fa-pull-right.admonition-title,.rst-content code.download span.fa-pull-right:first-child,.rst-content dl dt .fa-pull-right.headerlink,.rst-content h1 .fa-pull-right.headerlink,.rst-content h2 .fa-pull-right.headerlink,.rst-content h3 .fa-pull-right.headerlink,.rst-content h4 .fa-pull-right.headerlink,.rst-content h5 .fa-pull-right.headerlink,.rst-content h6 .fa-pull-right.headerlink,.rst-content p .fa-pull-right.headerlink,.rst-content table>caption .fa-pull-right.headerlink,.rst-content tt.download span.fa-pull-right:first-child,.wy-menu-vertical li.current>a button.fa-pull-right.toctree-expand,.wy-menu-vertical li.on a button.fa-pull-right.toctree-expand,.wy-menu-vertical li button.fa-pull-right.toctree-expand{margin-left:.3em}.pull-right{float:right}.pull-left{float:left}.fa.pull-left,.pull-left.icon,.rst-content .code-block-caption .pull-left.headerlink,.rst-content .eqno .pull-left.headerlink,.rst-content .pull-left.admonition-title,.rst-content code.download span.pull-left:first-child,.rst-content dl dt .pull-left.headerlink,.rst-content h1 .pull-left.headerlink,.rst-content h2 .pull-left.headerlink,.rst-content h3 .pull-left.headerlink,.rst-content h4 .pull-left.headerlink,.rst-content h5 .pull-left.headerlink,.rst-content h6 .pull-left.headerlink,.rst-content p .pull-left.headerlink,.rst-content table>caption .pull-left.headerlink,.rst-content tt.download span.pull-left:first-child,.wy-menu-vertical li.current>a button.pull-left.toctree-expand,.wy-menu-vertical li.on a button.pull-left.toctree-expand,.wy-menu-vertical li button.pull-left.toctree-expand{margin-right:.3em}.fa.pull-right,.pull-right.icon,.rst-content .code-block-caption .pull-right.headerlink,.rst-content .eqno .pull-right.headerlink,.rst-content .pull-right.admonition-title,.rst-content code.download span.pull-right:first-child,.rst-content dl dt .pull-right.headerlink,.rst-content h1 .pull-right.headerlink,.rst-content h2 .pull-right.headerlink,.rst-content h3 .pull-right.headerlink,.rst-content h4 .pull-right.headerlink,.rst-content h5 .pull-right.headerlink,.rst-content h6 .pull-right.headerlink,.rst-content p .pull-right.headerlink,.rst-content table>caption .pull-right.headerlink,.rst-content tt.download span.pull-right:first-child,.wy-menu-vertical li.current>a button.pull-right.toctree-expand,.wy-menu-vertical li.on a button.pull-right.toctree-expand,.wy-menu-vertical li button.pull-right.toctree-expand{margin-left:.3em}.fa-spin{-webkit-animation:fa-spin 2s linear infinite;animation:fa-spin 2s linear infinite}.fa-pulse{-webkit-animation:fa-spin 1s steps(8) infinite;animation:fa-spin 1s steps(8) infinite}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}.fa-rotate-90{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=1)";-webkit-transform:rotate(90deg);-ms-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2)";-webkit-transform:rotate(180deg);-ms-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=3)";-webkit-transform:rotate(270deg);-ms-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)";-webkit-transform:scaleX(-1);-ms-transform:scaleX(-1);transform:scaleX(-1)}.fa-flip-vertical{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)";-webkit-transform:scaleY(-1);-ms-transform:scaleY(-1);transform:scaleY(-1)}:root .fa-flip-horizontal,:root .fa-flip-vertical,:root .fa-rotate-90,:root .fa-rotate-180,:root .fa-rotate-270{filter:none}.fa-stack{position:relative;display:inline-block;width:2em;height:2em;line-height:2em;vertical-align:middle}.fa-stack-1x,.fa-stack-2x{position:absolute;left:0;width:100%;text-align:center}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-glass:before{content:""}.fa-music:before{content:""}.fa-search:before,.icon-search:before{content:""}.fa-envelope-o:before{content:""}.fa-heart:before{content:""}.fa-star:before{content:""}.fa-star-o:before{content:""}.fa-user:before{content:""}.fa-film:before{content:""}.fa-th-large:before{content:""}.fa-th:before{content:""}.fa-th-list:before{content:""}.fa-check:before{content:""}.fa-close:before,.fa-remove:before,.fa-times:before{content:""}.fa-search-plus:before{content:""}.fa-search-minus:before{content:""}.fa-power-off:before{content:""}.fa-signal:before{content:""}.fa-cog:before,.fa-gear:before{content:""}.fa-trash-o:before{content:""}.fa-home:before,.icon-home:before{content:""}.fa-file-o:before{content:""}.fa-clock-o:before{content:""}.fa-road:before{content:""}.fa-download:before,.rst-content code.download span:first-child:before,.rst-content tt.download span:first-child:before{content:""}.fa-arrow-circle-o-down:before{content:""}.fa-arrow-circle-o-up:before{content:""}.fa-inbox:before{content:""}.fa-play-circle-o:before{content:""}.fa-repeat:before,.fa-rotate-right:before{content:""}.fa-refresh:before{content:""}.fa-list-alt:before{content:""}.fa-lock:before{content:""}.fa-flag:before{content:""}.fa-headphones:before{content:""}.fa-volume-off:before{content:""}.fa-volume-down:before{content:""}.fa-volume-up:before{content:""}.fa-qrcode:before{content:""}.fa-barcode:before{content:""}.fa-tag:before{content:""}.fa-tags:before{content:""}.fa-book:before,.icon-book:before{content:""}.fa-bookmark:before{content:""}.fa-print:before{content:""}.fa-camera:before{content:""}.fa-font:before{content:""}.fa-bold:before{content:""}.fa-italic:before{content:""}.fa-text-height:before{content:""}.fa-text-width:before{content:""}.fa-align-left:before{content:""}.fa-align-center:before{content:""}.fa-align-right:before{content:""}.fa-align-justify:before{content:""}.fa-list:before{content:""}.fa-dedent:before,.fa-outdent:before{content:""}.fa-indent:before{content:""}.fa-video-camera:before{content:""}.fa-image:before,.fa-photo:before,.fa-picture-o:before{content:""}.fa-pencil:before{content:""}.fa-map-marker:before{content:""}.fa-adjust:before{content:""}.fa-tint:before{content:""}.fa-edit:before,.fa-pencil-square-o:before{content:""}.fa-share-square-o:before{content:""}.fa-check-square-o:before{content:""}.fa-arrows:before{content:""}.fa-step-backward:before{content:""}.fa-fast-backward:before{content:""}.fa-backward:before{content:""}.fa-play:before{content:""}.fa-pause:before{content:""}.fa-stop:before{content:""}.fa-forward:before{content:""}.fa-fast-forward:before{content:""}.fa-step-forward:before{content:""}.fa-eject:before{content:""}.fa-chevron-left:before{content:""}.fa-chevron-right:before{content:""}.fa-plus-circle:before{content:""}.fa-minus-circle:before{content:""}.fa-times-circle:before,.wy-inline-validate.wy-inline-validate-danger .wy-input-context:before{content:""}.fa-check-circle:before,.wy-inline-validate.wy-inline-validate-success .wy-input-context:before{content:""}.fa-question-circle:before{content:""}.fa-info-circle:before{content:""}.fa-crosshairs:before{content:""}.fa-times-circle-o:before{content:""}.fa-check-circle-o:before{content:""}.fa-ban:before{content:""}.fa-arrow-left:before{content:""}.fa-arrow-right:before{content:""}.fa-arrow-up:before{content:""}.fa-arrow-down:before{content:""}.fa-mail-forward:before,.fa-share:before{content:""}.fa-expand:before{content:""}.fa-compress:before{content:""}.fa-plus:before{content:""}.fa-minus:before{content:""}.fa-asterisk:before{content:""}.fa-exclamation-circle:before,.rst-content .admonition-title:before,.wy-inline-validate.wy-inline-validate-info .wy-input-context:before,.wy-inline-validate.wy-inline-validate-warning .wy-input-context:before{content:""}.fa-gift:before{content:""}.fa-leaf:before{content:""}.fa-fire:before,.icon-fire:before{content:""}.fa-eye:before{content:""}.fa-eye-slash:before{content:""}.fa-exclamation-triangle:before,.fa-warning:before{content:""}.fa-plane:before{content:""}.fa-calendar:before{content:""}.fa-random:before{content:""}.fa-comment:before{content:""}.fa-magnet:before{content:""}.fa-chevron-up:before{content:""}.fa-chevron-down:before{content:""}.fa-retweet:before{content:""}.fa-shopping-cart:before{content:""}.fa-folder:before{content:""}.fa-folder-open:before{content:""}.fa-arrows-v:before{content:""}.fa-arrows-h:before{content:""}.fa-bar-chart-o:before,.fa-bar-chart:before{content:""}.fa-twitter-square:before{content:""}.fa-facebook-square:before{content:""}.fa-camera-retro:before{content:""}.fa-key:before{content:""}.fa-cogs:before,.fa-gears:before{content:""}.fa-comments:before{content:""}.fa-thumbs-o-up:before{content:""}.fa-thumbs-o-down:before{content:""}.fa-star-half:before{content:""}.fa-heart-o:before{content:""}.fa-sign-out:before{content:""}.fa-linkedin-square:before{content:""}.fa-thumb-tack:before{content:""}.fa-external-link:before{content:""}.fa-sign-in:before{content:""}.fa-trophy:before{content:""}.fa-github-square:before{content:""}.fa-upload:before{content:""}.fa-lemon-o:before{content:""}.fa-phone:before{content:""}.fa-square-o:before{content:""}.fa-bookmark-o:before{content:""}.fa-phone-square:before{content:""}.fa-twitter:before{content:""}.fa-facebook-f:before,.fa-facebook:before{content:""}.fa-github:before,.icon-github:before{content:""}.fa-unlock:before{content:""}.fa-credit-card:before{content:""}.fa-feed:before,.fa-rss:before{content:""}.fa-hdd-o:before{content:""}.fa-bullhorn:before{content:""}.fa-bell:before{content:""}.fa-certificate:before{content:""}.fa-hand-o-right:before{content:""}.fa-hand-o-left:before{content:""}.fa-hand-o-up:before{content:""}.fa-hand-o-down:before{content:""}.fa-arrow-circle-left:before,.icon-circle-arrow-left:before{content:""}.fa-arrow-circle-right:before,.icon-circle-arrow-right:before{content:""}.fa-arrow-circle-up:before{content:""}.fa-arrow-circle-down:before{content:""}.fa-globe:before{content:""}.fa-wrench:before{content:""}.fa-tasks:before{content:""}.fa-filter:before{content:""}.fa-briefcase:before{content:""}.fa-arrows-alt:before{content:""}.fa-group:before,.fa-users:before{content:""}.fa-chain:before,.fa-link:before,.icon-link:before{content:""}.fa-cloud:before{content:""}.fa-flask:before{content:""}.fa-cut:before,.fa-scissors:before{content:""}.fa-copy:before,.fa-files-o:before{content:""}.fa-paperclip:before{content:""}.fa-floppy-o:before,.fa-save:before{content:""}.fa-square:before{content:""}.fa-bars:before,.fa-navicon:before,.fa-reorder:before{content:""}.fa-list-ul:before{content:""}.fa-list-ol:before{content:""}.fa-strikethrough:before{content:""}.fa-underline:before{content:""}.fa-table:before{content:""}.fa-magic:before{content:""}.fa-truck:before{content:""}.fa-pinterest:before{content:""}.fa-pinterest-square:before{content:""}.fa-google-plus-square:before{content:""}.fa-google-plus:before{content:""}.fa-money:before{content:""}.fa-caret-down:before,.icon-caret-down:before,.wy-dropdown .caret:before{content:""}.fa-caret-up:before{content:""}.fa-caret-left:before{content:""}.fa-caret-right:before{content:""}.fa-columns:before{content:""}.fa-sort:before,.fa-unsorted:before{content:""}.fa-sort-desc:before,.fa-sort-down:before{content:""}.fa-sort-asc:before,.fa-sort-up:before{content:""}.fa-envelope:before{content:""}.fa-linkedin:before{content:""}.fa-rotate-left:before,.fa-undo:before{content:""}.fa-gavel:before,.fa-legal:before{content:""}.fa-dashboard:before,.fa-tachometer:before{content:""}.fa-comment-o:before{content:""}.fa-comments-o:before{content:""}.fa-bolt:before,.fa-flash:before{content:""}.fa-sitemap:before{content:""}.fa-umbrella:before{content:""}.fa-clipboard:before,.fa-paste:before{content:""}.fa-lightbulb-o:before{content:""}.fa-exchange:before{content:""}.fa-cloud-download:before{content:""}.fa-cloud-upload:before{content:""}.fa-user-md:before{content:""}.fa-stethoscope:before{content:""}.fa-suitcase:before{content:""}.fa-bell-o:before{content:""}.fa-coffee:before{content:""}.fa-cutlery:before{content:""}.fa-file-text-o:before{content:""}.fa-building-o:before{content:""}.fa-hospital-o:before{content:""}.fa-ambulance:before{content:""}.fa-medkit:before{content:""}.fa-fighter-jet:before{content:""}.fa-beer:before{content:""}.fa-h-square:before{content:""}.fa-plus-square:before{content:""}.fa-angle-double-left:before{content:""}.fa-angle-double-right:before{content:""}.fa-angle-double-up:before{content:""}.fa-angle-double-down:before{content:""}.fa-angle-left:before{content:""}.fa-angle-right:before{content:""}.fa-angle-up:before{content:""}.fa-angle-down:before{content:""}.fa-desktop:before{content:""}.fa-laptop:before{content:""}.fa-tablet:before{content:""}.fa-mobile-phone:before,.fa-mobile:before{content:""}.fa-circle-o:before{content:""}.fa-quote-left:before{content:""}.fa-quote-right:before{content:""}.fa-spinner:before{content:""}.fa-circle:before{content:""}.fa-mail-reply:before,.fa-reply:before{content:""}.fa-github-alt:before{content:""}.fa-folder-o:before{content:""}.fa-folder-open-o:before{content:""}.fa-smile-o:before{content:""}.fa-frown-o:before{content:""}.fa-meh-o:before{content:""}.fa-gamepad:before{content:""}.fa-keyboard-o:before{content:""}.fa-flag-o:before{content:""}.fa-flag-checkered:before{content:""}.fa-terminal:before{content:""}.fa-code:before{content:""}.fa-mail-reply-all:before,.fa-reply-all:before{content:""}.fa-star-half-empty:before,.fa-star-half-full:before,.fa-star-half-o:before{content:""}.fa-location-arrow:before{content:""}.fa-crop:before{content:""}.fa-code-fork:before{content:""}.fa-chain-broken:before,.fa-unlink:before{content:""}.fa-question:before{content:""}.fa-info:before{content:""}.fa-exclamation:before{content:""}.fa-superscript:before{content:""}.fa-subscript:before{content:""}.fa-eraser:before{content:""}.fa-puzzle-piece:before{content:""}.fa-microphone:before{content:""}.fa-microphone-slash:before{content:""}.fa-shield:before{content:""}.fa-calendar-o:before{content:""}.fa-fire-extinguisher:before{content:""}.fa-rocket:before{content:""}.fa-maxcdn:before{content:""}.fa-chevron-circle-left:before{content:""}.fa-chevron-circle-right:before{content:""}.fa-chevron-circle-up:before{content:""}.fa-chevron-circle-down:before{content:""}.fa-html5:before{content:""}.fa-css3:before{content:""}.fa-anchor:before{content:""}.fa-unlock-alt:before{content:""}.fa-bullseye:before{content:""}.fa-ellipsis-h:before{content:""}.fa-ellipsis-v:before{content:""}.fa-rss-square:before{content:""}.fa-play-circle:before{content:""}.fa-ticket:before{content:""}.fa-minus-square:before{content:""}.fa-minus-square-o:before,.wy-menu-vertical li.current>a button.toctree-expand:before,.wy-menu-vertical li.on a button.toctree-expand:before{content:""}.fa-level-up:before{content:""}.fa-level-down:before{content:""}.fa-check-square:before{content:""}.fa-pencil-square:before{content:""}.fa-external-link-square:before{content:""}.fa-share-square:before{content:""}.fa-compass:before{content:""}.fa-caret-square-o-down:before,.fa-toggle-down:before{content:""}.fa-caret-square-o-up:before,.fa-toggle-up:before{content:""}.fa-caret-square-o-right:before,.fa-toggle-right:before{content:""}.fa-eur:before,.fa-euro:before{content:""}.fa-gbp:before{content:""}.fa-dollar:before,.fa-usd:before{content:""}.fa-inr:before,.fa-rupee:before{content:""}.fa-cny:before,.fa-jpy:before,.fa-rmb:before,.fa-yen:before{content:""}.fa-rouble:before,.fa-rub:before,.fa-ruble:before{content:""}.fa-krw:before,.fa-won:before{content:""}.fa-bitcoin:before,.fa-btc:before{content:""}.fa-file:before{content:""}.fa-file-text:before{content:""}.fa-sort-alpha-asc:before{content:""}.fa-sort-alpha-desc:before{content:""}.fa-sort-amount-asc:before{content:""}.fa-sort-amount-desc:before{content:""}.fa-sort-numeric-asc:before{content:""}.fa-sort-numeric-desc:before{content:""}.fa-thumbs-up:before{content:""}.fa-thumbs-down:before{content:""}.fa-youtube-square:before{content:""}.fa-youtube:before{content:""}.fa-xing:before{content:""}.fa-xing-square:before{content:""}.fa-youtube-play:before{content:""}.fa-dropbox:before{content:""}.fa-stack-overflow:before{content:""}.fa-instagram:before{content:""}.fa-flickr:before{content:""}.fa-adn:before{content:""}.fa-bitbucket:before,.icon-bitbucket:before{content:""}.fa-bitbucket-square:before{content:""}.fa-tumblr:before{content:""}.fa-tumblr-square:before{content:""}.fa-long-arrow-down:before{content:""}.fa-long-arrow-up:before{content:""}.fa-long-arrow-left:before{content:""}.fa-long-arrow-right:before{content:""}.fa-apple:before{content:""}.fa-windows:before{content:""}.fa-android:before{content:""}.fa-linux:before{content:""}.fa-dribbble:before{content:""}.fa-skype:before{content:""}.fa-foursquare:before{content:""}.fa-trello:before{content:""}.fa-female:before{content:""}.fa-male:before{content:""}.fa-gittip:before,.fa-gratipay:before{content:""}.fa-sun-o:before{content:""}.fa-moon-o:before{content:""}.fa-archive:before{content:""}.fa-bug:before{content:""}.fa-vk:before{content:""}.fa-weibo:before{content:""}.fa-renren:before{content:""}.fa-pagelines:before{content:""}.fa-stack-exchange:before{content:""}.fa-arrow-circle-o-right:before{content:""}.fa-arrow-circle-o-left:before{content:""}.fa-caret-square-o-left:before,.fa-toggle-left:before{content:""}.fa-dot-circle-o:before{content:""}.fa-wheelchair:before{content:""}.fa-vimeo-square:before{content:""}.fa-try:before,.fa-turkish-lira:before{content:""}.fa-plus-square-o:before,.wy-menu-vertical li button.toctree-expand:before{content:""}.fa-space-shuttle:before{content:""}.fa-slack:before{content:""}.fa-envelope-square:before{content:""}.fa-wordpress:before{content:""}.fa-openid:before{content:""}.fa-bank:before,.fa-institution:before,.fa-university:before{content:""}.fa-graduation-cap:before,.fa-mortar-board:before{content:""}.fa-yahoo:before{content:""}.fa-google:before{content:""}.fa-reddit:before{content:""}.fa-reddit-square:before{content:""}.fa-stumbleupon-circle:before{content:""}.fa-stumbleupon:before{content:""}.fa-delicious:before{content:""}.fa-digg:before{content:""}.fa-pied-piper-pp:before{content:""}.fa-pied-piper-alt:before{content:""}.fa-drupal:before{content:""}.fa-joomla:before{content:""}.fa-language:before{content:""}.fa-fax:before{content:""}.fa-building:before{content:""}.fa-child:before{content:""}.fa-paw:before{content:""}.fa-spoon:before{content:""}.fa-cube:before{content:""}.fa-cubes:before{content:""}.fa-behance:before{content:""}.fa-behance-square:before{content:""}.fa-steam:before{content:""}.fa-steam-square:before{content:""}.fa-recycle:before{content:""}.fa-automobile:before,.fa-car:before{content:""}.fa-cab:before,.fa-taxi:before{content:""}.fa-tree:before{content:""}.fa-spotify:before{content:""}.fa-deviantart:before{content:""}.fa-soundcloud:before{content:""}.fa-database:before{content:""}.fa-file-pdf-o:before{content:""}.fa-file-word-o:before{content:""}.fa-file-excel-o:before{content:""}.fa-file-powerpoint-o:before{content:""}.fa-file-image-o:before,.fa-file-photo-o:before,.fa-file-picture-o:before{content:""}.fa-file-archive-o:before,.fa-file-zip-o:before{content:""}.fa-file-audio-o:before,.fa-file-sound-o:before{content:""}.fa-file-movie-o:before,.fa-file-video-o:before{content:""}.fa-file-code-o:before{content:""}.fa-vine:before{content:""}.fa-codepen:before{content:""}.fa-jsfiddle:before{content:""}.fa-life-bouy:before,.fa-life-buoy:before,.fa-life-ring:before,.fa-life-saver:before,.fa-support:before{content:""}.fa-circle-o-notch:before{content:""}.fa-ra:before,.fa-rebel:before,.fa-resistance:before{content:""}.fa-empire:before,.fa-ge:before{content:""}.fa-git-square:before{content:""}.fa-git:before{content:""}.fa-hacker-news:before,.fa-y-combinator-square:before,.fa-yc-square:before{content:""}.fa-tencent-weibo:before{content:""}.fa-qq:before{content:""}.fa-wechat:before,.fa-weixin:before{content:""}.fa-paper-plane:before,.fa-send:before{content:""}.fa-paper-plane-o:before,.fa-send-o:before{content:""}.fa-history:before{content:""}.fa-circle-thin:before{content:""}.fa-header:before{content:""}.fa-paragraph:before{content:""}.fa-sliders:before{content:""}.fa-share-alt:before{content:""}.fa-share-alt-square:before{content:""}.fa-bomb:before{content:""}.fa-futbol-o:before,.fa-soccer-ball-o:before{content:""}.fa-tty:before{content:""}.fa-binoculars:before{content:""}.fa-plug:before{content:""}.fa-slideshare:before{content:""}.fa-twitch:before{content:""}.fa-yelp:before{content:""}.fa-newspaper-o:before{content:""}.fa-wifi:before{content:""}.fa-calculator:before{content:""}.fa-paypal:before{content:""}.fa-google-wallet:before{content:""}.fa-cc-visa:before{content:""}.fa-cc-mastercard:before{content:""}.fa-cc-discover:before{content:""}.fa-cc-amex:before{content:""}.fa-cc-paypal:before{content:""}.fa-cc-stripe:before{content:""}.fa-bell-slash:before{content:""}.fa-bell-slash-o:before{content:""}.fa-trash:before{content:""}.fa-copyright:before{content:""}.fa-at:before{content:""}.fa-eyedropper:before{content:""}.fa-paint-brush:before{content:""}.fa-birthday-cake:before{content:""}.fa-area-chart:before{content:""}.fa-pie-chart:before{content:""}.fa-line-chart:before{content:""}.fa-lastfm:before{content:""}.fa-lastfm-square:before{content:""}.fa-toggle-off:before{content:""}.fa-toggle-on:before{content:""}.fa-bicycle:before{content:""}.fa-bus:before{content:""}.fa-ioxhost:before{content:""}.fa-angellist:before{content:""}.fa-cc:before{content:""}.fa-ils:before,.fa-shekel:before,.fa-sheqel:before{content:""}.fa-meanpath:before{content:""}.fa-buysellads:before{content:""}.fa-connectdevelop:before{content:""}.fa-dashcube:before{content:""}.fa-forumbee:before{content:""}.fa-leanpub:before{content:""}.fa-sellsy:before{content:""}.fa-shirtsinbulk:before{content:""}.fa-simplybuilt:before{content:""}.fa-skyatlas:before{content:""}.fa-cart-plus:before{content:""}.fa-cart-arrow-down:before{content:""}.fa-diamond:before{content:""}.fa-ship:before{content:""}.fa-user-secret:before{content:""}.fa-motorcycle:before{content:""}.fa-street-view:before{content:""}.fa-heartbeat:before{content:""}.fa-venus:before{content:""}.fa-mars:before{content:""}.fa-mercury:before{content:""}.fa-intersex:before,.fa-transgender:before{content:""}.fa-transgender-alt:before{content:""}.fa-venus-double:before{content:""}.fa-mars-double:before{content:""}.fa-venus-mars:before{content:""}.fa-mars-stroke:before{content:""}.fa-mars-stroke-v:before{content:""}.fa-mars-stroke-h:before{content:""}.fa-neuter:before{content:""}.fa-genderless:before{content:""}.fa-facebook-official:before{content:""}.fa-pinterest-p:before{content:""}.fa-whatsapp:before{content:""}.fa-server:before{content:""}.fa-user-plus:before{content:""}.fa-user-times:before{content:""}.fa-bed:before,.fa-hotel:before{content:""}.fa-viacoin:before{content:""}.fa-train:before{content:""}.fa-subway:before{content:""}.fa-medium:before{content:""}.fa-y-combinator:before,.fa-yc:before{content:""}.fa-optin-monster:before{content:""}.fa-opencart:before{content:""}.fa-expeditedssl:before{content:""}.fa-battery-4:before,.fa-battery-full:before,.fa-battery:before{content:""}.fa-battery-3:before,.fa-battery-three-quarters:before{content:""}.fa-battery-2:before,.fa-battery-half:before{content:""}.fa-battery-1:before,.fa-battery-quarter:before{content:""}.fa-battery-0:before,.fa-battery-empty:before{content:""}.fa-mouse-pointer:before{content:""}.fa-i-cursor:before{content:""}.fa-object-group:before{content:""}.fa-object-ungroup:before{content:""}.fa-sticky-note:before{content:""}.fa-sticky-note-o:before{content:""}.fa-cc-jcb:before{content:""}.fa-cc-diners-club:before{content:""}.fa-clone:before{content:""}.fa-balance-scale:before{content:""}.fa-hourglass-o:before{content:""}.fa-hourglass-1:before,.fa-hourglass-start:before{content:""}.fa-hourglass-2:before,.fa-hourglass-half:before{content:""}.fa-hourglass-3:before,.fa-hourglass-end:before{content:""}.fa-hourglass:before{content:""}.fa-hand-grab-o:before,.fa-hand-rock-o:before{content:""}.fa-hand-paper-o:before,.fa-hand-stop-o:before{content:""}.fa-hand-scissors-o:before{content:""}.fa-hand-lizard-o:before{content:""}.fa-hand-spock-o:before{content:""}.fa-hand-pointer-o:before{content:""}.fa-hand-peace-o:before{content:""}.fa-trademark:before{content:""}.fa-registered:before{content:""}.fa-creative-commons:before{content:""}.fa-gg:before{content:""}.fa-gg-circle:before{content:""}.fa-tripadvisor:before{content:""}.fa-odnoklassniki:before{content:""}.fa-odnoklassniki-square:before{content:""}.fa-get-pocket:before{content:""}.fa-wikipedia-w:before{content:""}.fa-safari:before{content:""}.fa-chrome:before{content:""}.fa-firefox:before{content:""}.fa-opera:before{content:""}.fa-internet-explorer:before{content:""}.fa-television:before,.fa-tv:before{content:""}.fa-contao:before{content:""}.fa-500px:before{content:""}.fa-amazon:before{content:""}.fa-calendar-plus-o:before{content:""}.fa-calendar-minus-o:before{content:""}.fa-calendar-times-o:before{content:""}.fa-calendar-check-o:before{content:""}.fa-industry:before{content:""}.fa-map-pin:before{content:""}.fa-map-signs:before{content:""}.fa-map-o:before{content:""}.fa-map:before{content:""}.fa-commenting:before{content:""}.fa-commenting-o:before{content:""}.fa-houzz:before{content:""}.fa-vimeo:before{content:""}.fa-black-tie:before{content:""}.fa-fonticons:before{content:""}.fa-reddit-alien:before{content:""}.fa-edge:before{content:""}.fa-credit-card-alt:before{content:""}.fa-codiepie:before{content:""}.fa-modx:before{content:""}.fa-fort-awesome:before{content:""}.fa-usb:before{content:""}.fa-product-hunt:before{content:""}.fa-mixcloud:before{content:""}.fa-scribd:before{content:""}.fa-pause-circle:before{content:""}.fa-pause-circle-o:before{content:""}.fa-stop-circle:before{content:""}.fa-stop-circle-o:before{content:""}.fa-shopping-bag:before{content:""}.fa-shopping-basket:before{content:""}.fa-hashtag:before{content:""}.fa-bluetooth:before{content:""}.fa-bluetooth-b:before{content:""}.fa-percent:before{content:""}.fa-gitlab:before,.icon-gitlab:before{content:""}.fa-wpbeginner:before{content:""}.fa-wpforms:before{content:""}.fa-envira:before{content:""}.fa-universal-access:before{content:""}.fa-wheelchair-alt:before{content:""}.fa-question-circle-o:before{content:""}.fa-blind:before{content:""}.fa-audio-description:before{content:""}.fa-volume-control-phone:before{content:""}.fa-braille:before{content:""}.fa-assistive-listening-systems:before{content:""}.fa-american-sign-language-interpreting:before,.fa-asl-interpreting:before{content:""}.fa-deaf:before,.fa-deafness:before,.fa-hard-of-hearing:before{content:""}.fa-glide:before{content:""}.fa-glide-g:before{content:""}.fa-sign-language:before,.fa-signing:before{content:""}.fa-low-vision:before{content:""}.fa-viadeo:before{content:""}.fa-viadeo-square:before{content:""}.fa-snapchat:before{content:""}.fa-snapchat-ghost:before{content:""}.fa-snapchat-square:before{content:""}.fa-pied-piper:before{content:""}.fa-first-order:before{content:""}.fa-yoast:before{content:""}.fa-themeisle:before{content:""}.fa-google-plus-circle:before,.fa-google-plus-official:before{content:""}.fa-fa:before,.fa-font-awesome:before{content:""}.fa-handshake-o:before{content:""}.fa-envelope-open:before{content:""}.fa-envelope-open-o:before{content:""}.fa-linode:before{content:""}.fa-address-book:before{content:""}.fa-address-book-o:before{content:""}.fa-address-card:before,.fa-vcard:before{content:""}.fa-address-card-o:before,.fa-vcard-o:before{content:""}.fa-user-circle:before{content:""}.fa-user-circle-o:before{content:""}.fa-user-o:before{content:""}.fa-id-badge:before{content:""}.fa-drivers-license:before,.fa-id-card:before{content:""}.fa-drivers-license-o:before,.fa-id-card-o:before{content:""}.fa-quora:before{content:""}.fa-free-code-camp:before{content:""}.fa-telegram:before{content:""}.fa-thermometer-4:before,.fa-thermometer-full:before,.fa-thermometer:before{content:""}.fa-thermometer-3:before,.fa-thermometer-three-quarters:before{content:""}.fa-thermometer-2:before,.fa-thermometer-half:before{content:""}.fa-thermometer-1:before,.fa-thermometer-quarter:before{content:""}.fa-thermometer-0:before,.fa-thermometer-empty:before{content:""}.fa-shower:before{content:""}.fa-bath:before,.fa-bathtub:before,.fa-s15:before{content:""}.fa-podcast:before{content:""}.fa-window-maximize:before{content:""}.fa-window-minimize:before{content:""}.fa-window-restore:before{content:""}.fa-times-rectangle:before,.fa-window-close:before{content:""}.fa-times-rectangle-o:before,.fa-window-close-o:before{content:""}.fa-bandcamp:before{content:""}.fa-grav:before{content:""}.fa-etsy:before{content:""}.fa-imdb:before{content:""}.fa-ravelry:before{content:""}.fa-eercast:before{content:""}.fa-microchip:before{content:""}.fa-snowflake-o:before{content:""}.fa-superpowers:before{content:""}.fa-wpexplorer:before{content:""}.fa-meetup:before{content:""}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto}.fa,.icon,.rst-content .admonition-title,.rst-content .code-block-caption .headerlink,.rst-content .eqno .headerlink,.rst-content code.download span:first-child,.rst-content dl dt .headerlink,.rst-content h1 .headerlink,.rst-content h2 .headerlink,.rst-content h3 .headerlink,.rst-content h4 .headerlink,.rst-content h5 .headerlink,.rst-content h6 .headerlink,.rst-content p.caption .headerlink,.rst-content p .headerlink,.rst-content table>caption .headerlink,.rst-content tt.download span:first-child,.wy-dropdown .caret,.wy-inline-validate.wy-inline-validate-danger .wy-input-context,.wy-inline-validate.wy-inline-validate-info .wy-input-context,.wy-inline-validate.wy-inline-validate-success .wy-input-context,.wy-inline-validate.wy-inline-validate-warning .wy-input-context,.wy-menu-vertical li.current>a button.toctree-expand,.wy-menu-vertical li.on a button.toctree-expand,.wy-menu-vertical li button.toctree-expand{font-family:inherit}.fa:before,.icon:before,.rst-content .admonition-title:before,.rst-content .code-block-caption .headerlink:before,.rst-content .eqno .headerlink:before,.rst-content code.download span:first-child:before,.rst-content dl dt .headerlink:before,.rst-content h1 .headerlink:before,.rst-content h2 .headerlink:before,.rst-content h3 .headerlink:before,.rst-content h4 .headerlink:before,.rst-content h5 .headerlink:before,.rst-content h6 .headerlink:before,.rst-content p.caption .headerlink:before,.rst-content p .headerlink:before,.rst-content table>caption .headerlink:before,.rst-content tt.download span:first-child:before,.wy-dropdown .caret:before,.wy-inline-validate.wy-inline-validate-danger .wy-input-context:before,.wy-inline-validate.wy-inline-validate-info .wy-input-context:before,.wy-inline-validate.wy-inline-validate-success .wy-input-context:before,.wy-inline-validate.wy-inline-validate-warning .wy-input-context:before,.wy-menu-vertical li.current>a button.toctree-expand:before,.wy-menu-vertical li.on a button.toctree-expand:before,.wy-menu-vertical li button.toctree-expand:before{font-family:FontAwesome;display:inline-block;font-style:normal;font-weight:400;line-height:1;text-decoration:inherit}.rst-content .code-block-caption a .headerlink,.rst-content .eqno a .headerlink,.rst-content a .admonition-title,.rst-content code.download a span:first-child,.rst-content dl dt a .headerlink,.rst-content h1 a .headerlink,.rst-content h2 a .headerlink,.rst-content h3 a .headerlink,.rst-content h4 a .headerlink,.rst-content h5 a .headerlink,.rst-content h6 a .headerlink,.rst-content p.caption a .headerlink,.rst-content p a .headerlink,.rst-content table>caption a .headerlink,.rst-content tt.download a span:first-child,.wy-menu-vertical li.current>a button.toctree-expand,.wy-menu-vertical li.on a button.toctree-expand,.wy-menu-vertical li a button.toctree-expand,a .fa,a .icon,a .rst-content .admonition-title,a .rst-content .code-block-caption .headerlink,a .rst-content .eqno .headerlink,a .rst-content code.download span:first-child,a .rst-content dl dt .headerlink,a .rst-content h1 .headerlink,a .rst-content h2 .headerlink,a .rst-content h3 .headerlink,a .rst-content h4 .headerlink,a .rst-content h5 .headerlink,a .rst-content h6 .headerlink,a .rst-content p.caption .headerlink,a .rst-content p .headerlink,a .rst-content table>caption .headerlink,a .rst-content tt.download span:first-child,a .wy-menu-vertical li button.toctree-expand{display:inline-block;text-decoration:inherit}.btn .fa,.btn .icon,.btn .rst-content .admonition-title,.btn .rst-content .code-block-caption .headerlink,.btn .rst-content .eqno .headerlink,.btn .rst-content code.download span:first-child,.btn .rst-content dl dt .headerlink,.btn .rst-content h1 .headerlink,.btn .rst-content h2 .headerlink,.btn .rst-content h3 .headerlink,.btn .rst-content h4 .headerlink,.btn .rst-content h5 .headerlink,.btn .rst-content h6 .headerlink,.btn .rst-content p .headerlink,.btn .rst-content table>caption .headerlink,.btn .rst-content tt.download span:first-child,.btn .wy-menu-vertical li.current>a button.toctree-expand,.btn .wy-menu-vertical li.on a button.toctree-expand,.btn .wy-menu-vertical li button.toctree-expand,.nav .fa,.nav .icon,.nav .rst-content .admonition-title,.nav .rst-content .code-block-caption .headerlink,.nav .rst-content .eqno .headerlink,.nav .rst-content code.download span:first-child,.nav .rst-content dl dt .headerlink,.nav .rst-content h1 .headerlink,.nav .rst-content h2 .headerlink,.nav .rst-content h3 .headerlink,.nav .rst-content h4 .headerlink,.nav .rst-content h5 .headerlink,.nav .rst-content h6 .headerlink,.nav .rst-content p .headerlink,.nav .rst-content table>caption .headerlink,.nav .rst-content tt.download span:first-child,.nav .wy-menu-vertical li.current>a button.toctree-expand,.nav .wy-menu-vertical li.on a button.toctree-expand,.nav .wy-menu-vertical li button.toctree-expand,.rst-content .btn .admonition-title,.rst-content .code-block-caption .btn .headerlink,.rst-content .code-block-caption .nav .headerlink,.rst-content .eqno .btn .headerlink,.rst-content .eqno .nav .headerlink,.rst-content .nav .admonition-title,.rst-content code.download .btn span:first-child,.rst-content code.download .nav span:first-child,.rst-content dl dt .btn .headerlink,.rst-content dl dt .nav .headerlink,.rst-content h1 .btn .headerlink,.rst-content h1 .nav .headerlink,.rst-content h2 .btn .headerlink,.rst-content h2 .nav .headerlink,.rst-content h3 .btn .headerlink,.rst-content h3 .nav .headerlink,.rst-content h4 .btn .headerlink,.rst-content h4 .nav .headerlink,.rst-content h5 .btn .headerlink,.rst-content h5 .nav .headerlink,.rst-content h6 .btn .headerlink,.rst-content h6 .nav .headerlink,.rst-content p .btn .headerlink,.rst-content p .nav .headerlink,.rst-content table>caption .btn .headerlink,.rst-content table>caption .nav .headerlink,.rst-content tt.download .btn span:first-child,.rst-content tt.download .nav span:first-child,.wy-menu-vertical li .btn button.toctree-expand,.wy-menu-vertical li.current>a .btn button.toctree-expand,.wy-menu-vertical li.current>a .nav button.toctree-expand,.wy-menu-vertical li .nav button.toctree-expand,.wy-menu-vertical li.on a .btn button.toctree-expand,.wy-menu-vertical li.on a .nav button.toctree-expand{display:inline}.btn .fa-large.icon,.btn .fa.fa-large,.btn .rst-content .code-block-caption .fa-large.headerlink,.btn .rst-content .eqno .fa-large.headerlink,.btn .rst-content .fa-large.admonition-title,.btn .rst-content code.download span.fa-large:first-child,.btn .rst-content dl dt .fa-large.headerlink,.btn .rst-content h1 .fa-large.headerlink,.btn .rst-content h2 .fa-large.headerlink,.btn .rst-content h3 .fa-large.headerlink,.btn .rst-content h4 .fa-large.headerlink,.btn .rst-content h5 .fa-large.headerlink,.btn .rst-content h6 .fa-large.headerlink,.btn .rst-content p .fa-large.headerlink,.btn .rst-content table>caption .fa-large.headerlink,.btn .rst-content tt.download span.fa-large:first-child,.btn .wy-menu-vertical li button.fa-large.toctree-expand,.nav .fa-large.icon,.nav .fa.fa-large,.nav .rst-content .code-block-caption .fa-large.headerlink,.nav .rst-content .eqno .fa-large.headerlink,.nav .rst-content .fa-large.admonition-title,.nav .rst-content code.download span.fa-large:first-child,.nav .rst-content dl dt .fa-large.headerlink,.nav .rst-content h1 .fa-large.headerlink,.nav .rst-content h2 .fa-large.headerlink,.nav .rst-content h3 .fa-large.headerlink,.nav .rst-content h4 .fa-large.headerlink,.nav .rst-content h5 .fa-large.headerlink,.nav .rst-content h6 .fa-large.headerlink,.nav .rst-content p .fa-large.headerlink,.nav .rst-content table>caption .fa-large.headerlink,.nav .rst-content tt.download span.fa-large:first-child,.nav .wy-menu-vertical li button.fa-large.toctree-expand,.rst-content .btn .fa-large.admonition-title,.rst-content .code-block-caption .btn .fa-large.headerlink,.rst-content .code-block-caption .nav .fa-large.headerlink,.rst-content .eqno .btn .fa-large.headerlink,.rst-content .eqno .nav .fa-large.headerlink,.rst-content .nav .fa-large.admonition-title,.rst-content code.download .btn span.fa-large:first-child,.rst-content code.download .nav span.fa-large:first-child,.rst-content dl dt .btn .fa-large.headerlink,.rst-content dl dt .nav .fa-large.headerlink,.rst-content h1 .btn .fa-large.headerlink,.rst-content h1 .nav .fa-large.headerlink,.rst-content h2 .btn .fa-large.headerlink,.rst-content h2 .nav .fa-large.headerlink,.rst-content h3 .btn .fa-large.headerlink,.rst-content h3 .nav .fa-large.headerlink,.rst-content h4 .btn .fa-large.headerlink,.rst-content h4 .nav .fa-large.headerlink,.rst-content h5 .btn .fa-large.headerlink,.rst-content h5 .nav .fa-large.headerlink,.rst-content h6 .btn .fa-large.headerlink,.rst-content h6 .nav .fa-large.headerlink,.rst-content p .btn .fa-large.headerlink,.rst-content p .nav .fa-large.headerlink,.rst-content table>caption .btn .fa-large.headerlink,.rst-content table>caption .nav .fa-large.headerlink,.rst-content tt.download .btn span.fa-large:first-child,.rst-content tt.download .nav span.fa-large:first-child,.wy-menu-vertical li .btn button.fa-large.toctree-expand,.wy-menu-vertical li .nav button.fa-large.toctree-expand{line-height:.9em}.btn .fa-spin.icon,.btn .fa.fa-spin,.btn .rst-content .code-block-caption .fa-spin.headerlink,.btn .rst-content .eqno .fa-spin.headerlink,.btn .rst-content .fa-spin.admonition-title,.btn .rst-content code.download span.fa-spin:first-child,.btn .rst-content dl dt .fa-spin.headerlink,.btn .rst-content h1 .fa-spin.headerlink,.btn .rst-content h2 .fa-spin.headerlink,.btn .rst-content h3 .fa-spin.headerlink,.btn .rst-content h4 .fa-spin.headerlink,.btn .rst-content h5 .fa-spin.headerlink,.btn .rst-content h6 .fa-spin.headerlink,.btn .rst-content p .fa-spin.headerlink,.btn .rst-content table>caption .fa-spin.headerlink,.btn .rst-content tt.download span.fa-spin:first-child,.btn .wy-menu-vertical li button.fa-spin.toctree-expand,.nav .fa-spin.icon,.nav .fa.fa-spin,.nav .rst-content .code-block-caption .fa-spin.headerlink,.nav .rst-content .eqno .fa-spin.headerlink,.nav .rst-content .fa-spin.admonition-title,.nav .rst-content code.download span.fa-spin:first-child,.nav .rst-content dl dt .fa-spin.headerlink,.nav .rst-content h1 .fa-spin.headerlink,.nav .rst-content h2 .fa-spin.headerlink,.nav .rst-content h3 .fa-spin.headerlink,.nav .rst-content h4 .fa-spin.headerlink,.nav .rst-content h5 .fa-spin.headerlink,.nav .rst-content h6 .fa-spin.headerlink,.nav .rst-content p .fa-spin.headerlink,.nav .rst-content table>caption .fa-spin.headerlink,.nav .rst-content tt.download span.fa-spin:first-child,.nav .wy-menu-vertical li button.fa-spin.toctree-expand,.rst-content .btn .fa-spin.admonition-title,.rst-content .code-block-caption .btn .fa-spin.headerlink,.rst-content .code-block-caption .nav .fa-spin.headerlink,.rst-content .eqno .btn .fa-spin.headerlink,.rst-content .eqno .nav .fa-spin.headerlink,.rst-content .nav .fa-spin.admonition-title,.rst-content code.download .btn span.fa-spin:first-child,.rst-content code.download .nav span.fa-spin:first-child,.rst-content dl dt .btn .fa-spin.headerlink,.rst-content dl dt .nav .fa-spin.headerlink,.rst-content h1 .btn .fa-spin.headerlink,.rst-content h1 .nav .fa-spin.headerlink,.rst-content h2 .btn .fa-spin.headerlink,.rst-content h2 .nav .fa-spin.headerlink,.rst-content h3 .btn .fa-spin.headerlink,.rst-content h3 .nav .fa-spin.headerlink,.rst-content h4 .btn .fa-spin.headerlink,.rst-content h4 .nav .fa-spin.headerlink,.rst-content h5 .btn .fa-spin.headerlink,.rst-content h5 .nav .fa-spin.headerlink,.rst-content h6 .btn .fa-spin.headerlink,.rst-content h6 .nav .fa-spin.headerlink,.rst-content p .btn .fa-spin.headerlink,.rst-content p .nav .fa-spin.headerlink,.rst-content table>caption .btn .fa-spin.headerlink,.rst-content table>caption .nav .fa-spin.headerlink,.rst-content tt.download .btn span.fa-spin:first-child,.rst-content tt.download .nav span.fa-spin:first-child,.wy-menu-vertical li .btn button.fa-spin.toctree-expand,.wy-menu-vertical li .nav button.fa-spin.toctree-expand{display:inline-block}.btn.fa:before,.btn.icon:before,.rst-content .btn.admonition-title:before,.rst-content .code-block-caption .btn.headerlink:before,.rst-content .eqno .btn.headerlink:before,.rst-content code.download span.btn:first-child:before,.rst-content dl dt .btn.headerlink:before,.rst-content h1 .btn.headerlink:before,.rst-content h2 .btn.headerlink:before,.rst-content h3 .btn.headerlink:before,.rst-content h4 .btn.headerlink:before,.rst-content h5 .btn.headerlink:before,.rst-content h6 .btn.headerlink:before,.rst-content p .btn.headerlink:before,.rst-content table>caption .btn.headerlink:before,.rst-content tt.download span.btn:first-child:before,.wy-menu-vertical li button.btn.toctree-expand:before{opacity:.5;-webkit-transition:opacity .05s ease-in;-moz-transition:opacity .05s ease-in;transition:opacity .05s ease-in}.btn.fa:hover:before,.btn.icon:hover:before,.rst-content .btn.admonition-title:hover:before,.rst-content .code-block-caption .btn.headerlink:hover:before,.rst-content .eqno .btn.headerlink:hover:before,.rst-content code.download span.btn:first-child:hover:before,.rst-content dl dt .btn.headerlink:hover:before,.rst-content h1 .btn.headerlink:hover:before,.rst-content h2 .btn.headerlink:hover:before,.rst-content h3 .btn.headerlink:hover:before,.rst-content h4 .btn.headerlink:hover:before,.rst-content h5 .btn.headerlink:hover:before,.rst-content h6 .btn.headerlink:hover:before,.rst-content p .btn.headerlink:hover:before,.rst-content table>caption .btn.headerlink:hover:before,.rst-content tt.download span.btn:first-child:hover:before,.wy-menu-vertical li button.btn.toctree-expand:hover:before{opacity:1}.btn-mini .fa:before,.btn-mini .icon:before,.btn-mini .rst-content .admonition-title:before,.btn-mini .rst-content .code-block-caption .headerlink:before,.btn-mini .rst-content .eqno .headerlink:before,.btn-mini .rst-content code.download span:first-child:before,.btn-mini .rst-content dl dt .headerlink:before,.btn-mini .rst-content h1 .headerlink:before,.btn-mini .rst-content h2 .headerlink:before,.btn-mini .rst-content h3 .headerlink:before,.btn-mini .rst-content h4 .headerlink:before,.btn-mini .rst-content h5 .headerlink:before,.btn-mini .rst-content h6 .headerlink:before,.btn-mini .rst-content p .headerlink:before,.btn-mini .rst-content table>caption .headerlink:before,.btn-mini .rst-content tt.download span:first-child:before,.btn-mini .wy-menu-vertical li button.toctree-expand:before,.rst-content .btn-mini .admonition-title:before,.rst-content .code-block-caption .btn-mini .headerlink:before,.rst-content .eqno .btn-mini .headerlink:before,.rst-content code.download .btn-mini span:first-child:before,.rst-content dl dt .btn-mini .headerlink:before,.rst-content h1 .btn-mini .headerlink:before,.rst-content h2 .btn-mini .headerlink:before,.rst-content h3 .btn-mini .headerlink:before,.rst-content h4 .btn-mini .headerlink:before,.rst-content h5 .btn-mini .headerlink:before,.rst-content h6 .btn-mini .headerlink:before,.rst-content p .btn-mini .headerlink:before,.rst-content table>caption .btn-mini .headerlink:before,.rst-content tt.download .btn-mini span:first-child:before,.wy-menu-vertical li .btn-mini button.toctree-expand:before{font-size:14px;vertical-align:-15%}.rst-content .admonition,.rst-content .admonition-todo,.rst-content .attention,.rst-content .caution,.rst-content .danger,.rst-content .error,.rst-content .hint,.rst-content .important,.rst-content .note,.rst-content .seealso,.rst-content .tip,.rst-content .warning,.wy-alert{padding:12px;line-height:24px;margin-bottom:24px;background:#e7f2fa}.rst-content .admonition-title,.wy-alert-title{font-weight:700;display:block;color:#fff;background:#6ab0de;padding:6px 12px;margin:-12px -12px 12px}.rst-content .danger,.rst-content .error,.rst-content .wy-alert-danger.admonition,.rst-content .wy-alert-danger.admonition-todo,.rst-content .wy-alert-danger.attention,.rst-content .wy-alert-danger.caution,.rst-content .wy-alert-danger.hint,.rst-content .wy-alert-danger.important,.rst-content .wy-alert-danger.note,.rst-content .wy-alert-danger.seealso,.rst-content .wy-alert-danger.tip,.rst-content .wy-alert-danger.warning,.wy-alert.wy-alert-danger{background:#fdf3f2}.rst-content .danger .admonition-title,.rst-content .danger .wy-alert-title,.rst-content .error .admonition-title,.rst-content .error .wy-alert-title,.rst-content .wy-alert-danger.admonition-todo .admonition-title,.rst-content .wy-alert-danger.admonition-todo .wy-alert-title,.rst-content .wy-alert-danger.admonition .admonition-title,.rst-content .wy-alert-danger.admonition .wy-alert-title,.rst-content .wy-alert-danger.attention .admonition-title,.rst-content .wy-alert-danger.attention .wy-alert-title,.rst-content .wy-alert-danger.caution .admonition-title,.rst-content .wy-alert-danger.caution .wy-alert-title,.rst-content .wy-alert-danger.hint .admonition-title,.rst-content .wy-alert-danger.hint .wy-alert-title,.rst-content .wy-alert-danger.important .admonition-title,.rst-content .wy-alert-danger.important .wy-alert-title,.rst-content .wy-alert-danger.note .admonition-title,.rst-content .wy-alert-danger.note .wy-alert-title,.rst-content .wy-alert-danger.seealso .admonition-title,.rst-content .wy-alert-danger.seealso .wy-alert-title,.rst-content .wy-alert-danger.tip .admonition-title,.rst-content .wy-alert-danger.tip .wy-alert-title,.rst-content .wy-alert-danger.warning .admonition-title,.rst-content .wy-alert-danger.warning .wy-alert-title,.rst-content .wy-alert.wy-alert-danger .admonition-title,.wy-alert.wy-alert-danger .rst-content .admonition-title,.wy-alert.wy-alert-danger .wy-alert-title{background:#f29f97}.rst-content .admonition-todo,.rst-content .attention,.rst-content .caution,.rst-content .warning,.rst-content .wy-alert-warning.admonition,.rst-content .wy-alert-warning.danger,.rst-content .wy-alert-warning.error,.rst-content .wy-alert-warning.hint,.rst-content .wy-alert-warning.important,.rst-content .wy-alert-warning.note,.rst-content .wy-alert-warning.seealso,.rst-content .wy-alert-warning.tip,.wy-alert.wy-alert-warning{background:#ffedcc}.rst-content .admonition-todo .admonition-title,.rst-content .admonition-todo .wy-alert-title,.rst-content .attention .admonition-title,.rst-content .attention .wy-alert-title,.rst-content .caution .admonition-title,.rst-content .caution .wy-alert-title,.rst-content .warning .admonition-title,.rst-content .warning .wy-alert-title,.rst-content .wy-alert-warning.admonition .admonition-title,.rst-content .wy-alert-warning.admonition .wy-alert-title,.rst-content .wy-alert-warning.danger .admonition-title,.rst-content .wy-alert-warning.danger .wy-alert-title,.rst-content .wy-alert-warning.error .admonition-title,.rst-content .wy-alert-warning.error .wy-alert-title,.rst-content .wy-alert-warning.hint .admonition-title,.rst-content .wy-alert-warning.hint .wy-alert-title,.rst-content .wy-alert-warning.important .admonition-title,.rst-content .wy-alert-warning.important .wy-alert-title,.rst-content .wy-alert-warning.note .admonition-title,.rst-content .wy-alert-warning.note .wy-alert-title,.rst-content .wy-alert-warning.seealso .admonition-title,.rst-content .wy-alert-warning.seealso .wy-alert-title,.rst-content .wy-alert-warning.tip .admonition-title,.rst-content .wy-alert-warning.tip .wy-alert-title,.rst-content .wy-alert.wy-alert-warning .admonition-title,.wy-alert.wy-alert-warning .rst-content .admonition-title,.wy-alert.wy-alert-warning .wy-alert-title{background:#f0b37e}.rst-content .note,.rst-content .seealso,.rst-content .wy-alert-info.admonition,.rst-content .wy-alert-info.admonition-todo,.rst-content .wy-alert-info.attention,.rst-content .wy-alert-info.caution,.rst-content .wy-alert-info.danger,.rst-content .wy-alert-info.error,.rst-content .wy-alert-info.hint,.rst-content .wy-alert-info.important,.rst-content .wy-alert-info.tip,.rst-content .wy-alert-info.warning,.wy-alert.wy-alert-info{background:#e7f2fa}.rst-content .note .admonition-title,.rst-content .note .wy-alert-title,.rst-content .seealso .admonition-title,.rst-content .seealso .wy-alert-title,.rst-content .wy-alert-info.admonition-todo .admonition-title,.rst-content .wy-alert-info.admonition-todo .wy-alert-title,.rst-content .wy-alert-info.admonition .admonition-title,.rst-content .wy-alert-info.admonition .wy-alert-title,.rst-content .wy-alert-info.attention .admonition-title,.rst-content .wy-alert-info.attention .wy-alert-title,.rst-content .wy-alert-info.caution .admonition-title,.rst-content .wy-alert-info.caution .wy-alert-title,.rst-content .wy-alert-info.danger .admonition-title,.rst-content .wy-alert-info.danger .wy-alert-title,.rst-content .wy-alert-info.error .admonition-title,.rst-content .wy-alert-info.error .wy-alert-title,.rst-content .wy-alert-info.hint .admonition-title,.rst-content .wy-alert-info.hint .wy-alert-title,.rst-content .wy-alert-info.important .admonition-title,.rst-content .wy-alert-info.important .wy-alert-title,.rst-content .wy-alert-info.tip .admonition-title,.rst-content .wy-alert-info.tip .wy-alert-title,.rst-content .wy-alert-info.warning .admonition-title,.rst-content .wy-alert-info.warning .wy-alert-title,.rst-content .wy-alert.wy-alert-info .admonition-title,.wy-alert.wy-alert-info .rst-content .admonition-title,.wy-alert.wy-alert-info .wy-alert-title{background:#6ab0de}.rst-content .hint,.rst-content .important,.rst-content .tip,.rst-content .wy-alert-success.admonition,.rst-content .wy-alert-success.admonition-todo,.rst-content .wy-alert-success.attention,.rst-content .wy-alert-success.caution,.rst-content .wy-alert-success.danger,.rst-content .wy-alert-success.error,.rst-content .wy-alert-success.note,.rst-content .wy-alert-success.seealso,.rst-content .wy-alert-success.warning,.wy-alert.wy-alert-success{background:#dbfaf4}.rst-content .hint .admonition-title,.rst-content .hint .wy-alert-title,.rst-content .important .admonition-title,.rst-content .important .wy-alert-title,.rst-content .tip .admonition-title,.rst-content .tip .wy-alert-title,.rst-content .wy-alert-success.admonition-todo .admonition-title,.rst-content .wy-alert-success.admonition-todo .wy-alert-title,.rst-content .wy-alert-success.admonition .admonition-title,.rst-content .wy-alert-success.admonition .wy-alert-title,.rst-content .wy-alert-success.attention .admonition-title,.rst-content .wy-alert-success.attention .wy-alert-title,.rst-content .wy-alert-success.caution .admonition-title,.rst-content .wy-alert-success.caution .wy-alert-title,.rst-content .wy-alert-success.danger .admonition-title,.rst-content .wy-alert-success.danger .wy-alert-title,.rst-content .wy-alert-success.error .admonition-title,.rst-content .wy-alert-success.error .wy-alert-title,.rst-content .wy-alert-success.note .admonition-title,.rst-content .wy-alert-success.note .wy-alert-title,.rst-content .wy-alert-success.seealso .admonition-title,.rst-content .wy-alert-success.seealso .wy-alert-title,.rst-content .wy-alert-success.warning .admonition-title,.rst-content .wy-alert-success.warning .wy-alert-title,.rst-content .wy-alert.wy-alert-success .admonition-title,.wy-alert.wy-alert-success .rst-content .admonition-title,.wy-alert.wy-alert-success .wy-alert-title{background:#1abc9c}.rst-content .wy-alert-neutral.admonition,.rst-content .wy-alert-neutral.admonition-todo,.rst-content .wy-alert-neutral.attention,.rst-content .wy-alert-neutral.caution,.rst-content .wy-alert-neutral.danger,.rst-content .wy-alert-neutral.error,.rst-content .wy-alert-neutral.hint,.rst-content .wy-alert-neutral.important,.rst-content .wy-alert-neutral.note,.rst-content .wy-alert-neutral.seealso,.rst-content .wy-alert-neutral.tip,.rst-content .wy-alert-neutral.warning,.wy-alert.wy-alert-neutral{background:#f3f6f6}.rst-content .wy-alert-neutral.admonition-todo .admonition-title,.rst-content .wy-alert-neutral.admonition-todo .wy-alert-title,.rst-content .wy-alert-neutral.admonition .admonition-title,.rst-content .wy-alert-neutral.admonition .wy-alert-title,.rst-content .wy-alert-neutral.attention .admonition-title,.rst-content .wy-alert-neutral.attention .wy-alert-title,.rst-content .wy-alert-neutral.caution .admonition-title,.rst-content .wy-alert-neutral.caution .wy-alert-title,.rst-content .wy-alert-neutral.danger .admonition-title,.rst-content .wy-alert-neutral.danger .wy-alert-title,.rst-content .wy-alert-neutral.error .admonition-title,.rst-content .wy-alert-neutral.error .wy-alert-title,.rst-content .wy-alert-neutral.hint .admonition-title,.rst-content .wy-alert-neutral.hint .wy-alert-title,.rst-content .wy-alert-neutral.important .admonition-title,.rst-content .wy-alert-neutral.important .wy-alert-title,.rst-content .wy-alert-neutral.note .admonition-title,.rst-content .wy-alert-neutral.note .wy-alert-title,.rst-content .wy-alert-neutral.seealso .admonition-title,.rst-content .wy-alert-neutral.seealso .wy-alert-title,.rst-content .wy-alert-neutral.tip .admonition-title,.rst-content .wy-alert-neutral.tip .wy-alert-title,.rst-content .wy-alert-neutral.warning .admonition-title,.rst-content .wy-alert-neutral.warning .wy-alert-title,.rst-content .wy-alert.wy-alert-neutral .admonition-title,.wy-alert.wy-alert-neutral .rst-content .admonition-title,.wy-alert.wy-alert-neutral .wy-alert-title{color:#404040;background:#e1e4e5}.rst-content .wy-alert-neutral.admonition-todo a,.rst-content .wy-alert-neutral.admonition a,.rst-content .wy-alert-neutral.attention a,.rst-content .wy-alert-neutral.caution a,.rst-content .wy-alert-neutral.danger a,.rst-content .wy-alert-neutral.error a,.rst-content .wy-alert-neutral.hint a,.rst-content .wy-alert-neutral.important a,.rst-content .wy-alert-neutral.note a,.rst-content .wy-alert-neutral.seealso a,.rst-content .wy-alert-neutral.tip a,.rst-content .wy-alert-neutral.warning a,.wy-alert.wy-alert-neutral a{color:#2980b9}.rst-content .admonition-todo p:last-child,.rst-content .admonition p:last-child,.rst-content .attention p:last-child,.rst-content .caution p:last-child,.rst-content .danger p:last-child,.rst-content .error p:last-child,.rst-content .hint p:last-child,.rst-content .important p:last-child,.rst-content .note p:last-child,.rst-content .seealso p:last-child,.rst-content .tip p:last-child,.rst-content .warning p:last-child,.wy-alert p:last-child{margin-bottom:0}.wy-tray-container{position:fixed;bottom:0;left:0;z-index:600}.wy-tray-container li{display:block;width:300px;background:transparent;color:#fff;text-align:center;box-shadow:0 5px 5px 0 rgba(0,0,0,.1);padding:0 24px;min-width:20%;opacity:0;height:0;line-height:56px;overflow:hidden;-webkit-transition:all .3s ease-in;-moz-transition:all .3s ease-in;transition:all .3s ease-in}.wy-tray-container li.wy-tray-item-success{background:#27ae60}.wy-tray-container li.wy-tray-item-info{background:#2980b9}.wy-tray-container li.wy-tray-item-warning{background:#e67e22}.wy-tray-container li.wy-tray-item-danger{background:#e74c3c}.wy-tray-container li.on{opacity:1;height:56px}@media screen and (max-width:768px){.wy-tray-container{bottom:auto;top:0;width:100%}.wy-tray-container li{width:100%}}button{font-size:100%;margin:0;vertical-align:baseline;*vertical-align:middle;cursor:pointer;line-height:normal;-webkit-appearance:button;*overflow:visible}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}button[disabled]{cursor:default}.btn{display:inline-block;border-radius:2px;line-height:normal;white-space:nowrap;text-align:center;cursor:pointer;font-size:100%;padding:6px 12px 8px;color:#fff;border:1px solid rgba(0,0,0,.1);background-color:#27ae60;text-decoration:none;font-weight:400;font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif;box-shadow:inset 0 1px 2px -1px hsla(0,0%,100%,.5),inset 0 -2px 0 0 rgba(0,0,0,.1);outline-none:false;vertical-align:middle;*display:inline;zoom:1;-webkit-user-drag:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;-webkit-transition:all .1s linear;-moz-transition:all .1s linear;transition:all .1s linear}.btn-hover{background:#2e8ece;color:#fff}.btn:hover{background:#2cc36b;color:#fff}.btn:focus{background:#2cc36b;outline:0}.btn:active{box-shadow:inset 0 -1px 0 0 rgba(0,0,0,.05),inset 0 2px 0 0 rgba(0,0,0,.1);padding:8px 12px 6px}.btn:visited{color:#fff}.btn-disabled,.btn-disabled:active,.btn-disabled:focus,.btn-disabled:hover,.btn:disabled{background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);filter:alpha(opacity=40);opacity:.4;cursor:not-allowed;box-shadow:none}.btn::-moz-focus-inner{padding:0;border:0}.btn-small{font-size:80%}.btn-info{background-color:#2980b9!important}.btn-info:hover{background-color:#2e8ece!important}.btn-neutral{background-color:#f3f6f6!important;color:#404040!important}.btn-neutral:hover{background-color:#e5ebeb!important;color:#404040}.btn-neutral:visited{color:#404040!important}.btn-success{background-color:#27ae60!important}.btn-success:hover{background-color:#295!important}.btn-danger{background-color:#e74c3c!important}.btn-danger:hover{background-color:#ea6153!important}.btn-warning{background-color:#e67e22!important}.btn-warning:hover{background-color:#e98b39!important}.btn-invert{background-color:#222}.btn-invert:hover{background-color:#2f2f2f!important}.btn-link{background-color:transparent!important;color:#2980b9;box-shadow:none;border-color:transparent!important}.btn-link:active,.btn-link:hover{background-color:transparent!important;color:#409ad5!important;box-shadow:none}.btn-link:visited{color:#9b59b6}.wy-btn-group .btn,.wy-control .btn{vertical-align:middle}.wy-btn-group{margin-bottom:24px;*zoom:1}.wy-btn-group:after,.wy-btn-group:before{display:table;content:""}.wy-btn-group:after{clear:both}.wy-dropdown{position:relative;display:inline-block}.wy-dropdown-active .wy-dropdown-menu{display:block}.wy-dropdown-menu{position:absolute;left:0;display:none;float:left;top:100%;min-width:100%;background:#fcfcfc;z-index:100;border:1px solid #cfd7dd;box-shadow:0 2px 2px 0 rgba(0,0,0,.1);padding:12px}.wy-dropdown-menu>dd>a{display:block;clear:both;color:#404040;white-space:nowrap;font-size:90%;padding:0 12px;cursor:pointer}.wy-dropdown-menu>dd>a:hover{background:#2980b9;color:#fff}.wy-dropdown-menu>dd.divider{border-top:1px solid #cfd7dd;margin:6px 0}.wy-dropdown-menu>dd.search{padding-bottom:12px}.wy-dropdown-menu>dd.search input[type=search]{width:100%}.wy-dropdown-menu>dd.call-to-action{background:#e3e3e3;text-transform:uppercase;font-weight:500;font-size:80%}.wy-dropdown-menu>dd.call-to-action:hover{background:#e3e3e3}.wy-dropdown-menu>dd.call-to-action .btn{color:#fff}.wy-dropdown.wy-dropdown-up .wy-dropdown-menu{bottom:100%;top:auto;left:auto;right:0}.wy-dropdown.wy-dropdown-bubble .wy-dropdown-menu{background:#fcfcfc;margin-top:2px}.wy-dropdown.wy-dropdown-bubble .wy-dropdown-menu a{padding:6px 12px}.wy-dropdown.wy-dropdown-bubble .wy-dropdown-menu a:hover{background:#2980b9;color:#fff}.wy-dropdown.wy-dropdown-left .wy-dropdown-menu{right:0;left:auto;text-align:right}.wy-dropdown-arrow:before{content:" ";border-bottom:5px solid #f5f5f5;border-left:5px solid transparent;border-right:5px solid transparent;position:absolute;display:block;top:-4px;left:50%;margin-left:-3px}.wy-dropdown-arrow.wy-dropdown-arrow-left:before{left:11px}.wy-form-stacked select{display:block}.wy-form-aligned .wy-help-inline,.wy-form-aligned input,.wy-form-aligned label,.wy-form-aligned select,.wy-form-aligned textarea{display:inline-block;*display:inline;*zoom:1;vertical-align:middle}.wy-form-aligned .wy-control-group>label{display:inline-block;vertical-align:middle;width:10em;margin:6px 12px 0 0;float:left}.wy-form-aligned .wy-control{float:left}.wy-form-aligned .wy-control label{display:block}.wy-form-aligned .wy-control select{margin-top:6px}fieldset{margin:0}fieldset,legend{border:0;padding:0}legend{width:100%;white-space:normal;margin-bottom:24px;font-size:150%;*margin-left:-7px}label,legend{display:block}label{margin:0 0 .3125em;color:#333;font-size:90%}input,select,textarea{font-size:100%;margin:0;vertical-align:baseline;*vertical-align:middle}.wy-control-group{margin-bottom:24px;max-width:1200px;margin-left:auto;margin-right:auto;*zoom:1}.wy-control-group:after,.wy-control-group:before{display:table;content:""}.wy-control-group:after{clear:both}.wy-control-group.wy-control-group-required>label:after{content:" *";color:#e74c3c}.wy-control-group .wy-form-full,.wy-control-group .wy-form-halves,.wy-control-group .wy-form-thirds{padding-bottom:12px}.wy-control-group .wy-form-full input[type=color],.wy-control-group .wy-form-full input[type=date],.wy-control-group .wy-form-full input[type=datetime-local],.wy-control-group .wy-form-full input[type=datetime],.wy-control-group .wy-form-full input[type=email],.wy-control-group .wy-form-full input[type=month],.wy-control-group .wy-form-full input[type=number],.wy-control-group .wy-form-full input[type=password],.wy-control-group .wy-form-full input[type=search],.wy-control-group .wy-form-full input[type=tel],.wy-control-group .wy-form-full input[type=text],.wy-control-group .wy-form-full input[type=time],.wy-control-group .wy-form-full input[type=url],.wy-control-group .wy-form-full input[type=week],.wy-control-group .wy-form-full select,.wy-control-group .wy-form-halves input[type=color],.wy-control-group .wy-form-halves input[type=date],.wy-control-group .wy-form-halves input[type=datetime-local],.wy-control-group .wy-form-halves input[type=datetime],.wy-control-group .wy-form-halves input[type=email],.wy-control-group .wy-form-halves input[type=month],.wy-control-group .wy-form-halves input[type=number],.wy-control-group .wy-form-halves input[type=password],.wy-control-group .wy-form-halves input[type=search],.wy-control-group .wy-form-halves input[type=tel],.wy-control-group .wy-form-halves input[type=text],.wy-control-group .wy-form-halves input[type=time],.wy-control-group .wy-form-halves input[type=url],.wy-control-group .wy-form-halves input[type=week],.wy-control-group .wy-form-halves select,.wy-control-group .wy-form-thirds input[type=color],.wy-control-group .wy-form-thirds input[type=date],.wy-control-group .wy-form-thirds input[type=datetime-local],.wy-control-group .wy-form-thirds input[type=datetime],.wy-control-group .wy-form-thirds input[type=email],.wy-control-group .wy-form-thirds input[type=month],.wy-control-group .wy-form-thirds input[type=number],.wy-control-group .wy-form-thirds input[type=password],.wy-control-group .wy-form-thirds input[type=search],.wy-control-group .wy-form-thirds input[type=tel],.wy-control-group .wy-form-thirds input[type=text],.wy-control-group .wy-form-thirds input[type=time],.wy-control-group .wy-form-thirds input[type=url],.wy-control-group .wy-form-thirds input[type=week],.wy-control-group .wy-form-thirds select{width:100%}.wy-control-group .wy-form-full{float:left;display:block;width:100%;margin-right:0}.wy-control-group .wy-form-full:last-child{margin-right:0}.wy-control-group .wy-form-halves{float:left;display:block;margin-right:2.35765%;width:48.82117%}.wy-control-group .wy-form-halves:last-child,.wy-control-group .wy-form-halves:nth-of-type(2n){margin-right:0}.wy-control-group .wy-form-halves:nth-of-type(odd){clear:left}.wy-control-group .wy-form-thirds{float:left;display:block;margin-right:2.35765%;width:31.76157%}.wy-control-group .wy-form-thirds:last-child,.wy-control-group .wy-form-thirds:nth-of-type(3n){margin-right:0}.wy-control-group .wy-form-thirds:nth-of-type(3n+1){clear:left}.wy-control-group.wy-control-group-no-input .wy-control,.wy-control-no-input{margin:6px 0 0;font-size:90%}.wy-control-no-input{display:inline-block}.wy-control-group.fluid-input input[type=color],.wy-control-group.fluid-input input[type=date],.wy-control-group.fluid-input input[type=datetime-local],.wy-control-group.fluid-input input[type=datetime],.wy-control-group.fluid-input input[type=email],.wy-control-group.fluid-input input[type=month],.wy-control-group.fluid-input input[type=number],.wy-control-group.fluid-input input[type=password],.wy-control-group.fluid-input input[type=search],.wy-control-group.fluid-input input[type=tel],.wy-control-group.fluid-input input[type=text],.wy-control-group.fluid-input input[type=time],.wy-control-group.fluid-input input[type=url],.wy-control-group.fluid-input input[type=week]{width:100%}.wy-form-message-inline{padding-left:.3em;color:#666;font-size:90%}.wy-form-message{display:block;color:#999;font-size:70%;margin-top:.3125em;font-style:italic}.wy-form-message p{font-size:inherit;font-style:italic;margin-bottom:6px}.wy-form-message p:last-child{margin-bottom:0}input{line-height:normal}input[type=button],input[type=reset],input[type=submit]{-webkit-appearance:button;cursor:pointer;font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif;*overflow:visible}input[type=color],input[type=date],input[type=datetime-local],input[type=datetime],input[type=email],input[type=month],input[type=number],input[type=password],input[type=search],input[type=tel],input[type=text],input[type=time],input[type=url],input[type=week]{-webkit-appearance:none;padding:6px;display:inline-block;border:1px solid #ccc;font-size:80%;font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif;box-shadow:inset 0 1px 3px #ddd;border-radius:0;-webkit-transition:border .3s linear;-moz-transition:border .3s linear;transition:border .3s linear}input[type=datetime-local]{padding:.34375em .625em}input[disabled]{cursor:default}input[type=checkbox],input[type=radio]{padding:0;margin-right:.3125em;*height:13px;*width:13px}input[type=checkbox],input[type=radio],input[type=search]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration{-webkit-appearance:none}input[type=color]:focus,input[type=date]:focus,input[type=datetime-local]:focus,input[type=datetime]:focus,input[type=email]:focus,input[type=month]:focus,input[type=number]:focus,input[type=password]:focus,input[type=search]:focus,input[type=tel]:focus,input[type=text]:focus,input[type=time]:focus,input[type=url]:focus,input[type=week]:focus{outline:0;outline:thin dotted\9;border-color:#333}input.no-focus:focus{border-color:#ccc!important}input[type=checkbox]:focus,input[type=file]:focus,input[type=radio]:focus{outline:thin dotted #333;outline:1px auto #129fea}input[type=color][disabled],input[type=date][disabled],input[type=datetime-local][disabled],input[type=datetime][disabled],input[type=email][disabled],input[type=month][disabled],input[type=number][disabled],input[type=password][disabled],input[type=search][disabled],input[type=tel][disabled],input[type=text][disabled],input[type=time][disabled],input[type=url][disabled],input[type=week][disabled]{cursor:not-allowed;background-color:#fafafa}input:focus:invalid,select:focus:invalid,textarea:focus:invalid{color:#e74c3c;border:1px solid #e74c3c}input:focus:invalid:focus,select:focus:invalid:focus,textarea:focus:invalid:focus{border-color:#e74c3c}input[type=checkbox]:focus:invalid:focus,input[type=file]:focus:invalid:focus,input[type=radio]:focus:invalid:focus{outline-color:#e74c3c}input.wy-input-large{padding:12px;font-size:100%}textarea{overflow:auto;vertical-align:top;width:100%;font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif}select,textarea{padding:.5em .625em;display:inline-block;border:1px solid #ccc;font-size:80%;box-shadow:inset 0 1px 3px #ddd;-webkit-transition:border .3s linear;-moz-transition:border .3s linear;transition:border .3s linear}select{border:1px solid #ccc;background-color:#fff}select[multiple]{height:auto}select:focus,textarea:focus{outline:0}input[readonly],select[disabled],select[readonly],textarea[disabled],textarea[readonly]{cursor:not-allowed;background-color:#fafafa}input[type=checkbox][disabled],input[type=radio][disabled]{cursor:not-allowed}.wy-checkbox,.wy-radio{margin:6px 0;color:#404040;display:block}.wy-checkbox input,.wy-radio input{vertical-align:baseline}.wy-form-message-inline{display:inline-block;*display:inline;*zoom:1;vertical-align:middle}.wy-input-prefix,.wy-input-suffix{white-space:nowrap;padding:6px}.wy-input-prefix .wy-input-context,.wy-input-suffix .wy-input-context{line-height:27px;padding:0 8px;display:inline-block;font-size:80%;background-color:#f3f6f6;border:1px solid #ccc;color:#999}.wy-input-suffix .wy-input-context{border-left:0}.wy-input-prefix .wy-input-context{border-right:0}.wy-switch{position:relative;display:block;height:24px;margin-top:12px;cursor:pointer}.wy-switch:before{left:0;top:0;width:36px;height:12px;background:#ccc}.wy-switch:after,.wy-switch:before{position:absolute;content:"";display:block;border-radius:4px;-webkit-transition:all .2s ease-in-out;-moz-transition:all .2s ease-in-out;transition:all .2s ease-in-out}.wy-switch:after{width:18px;height:18px;background:#999;left:-3px;top:-3px}.wy-switch span{position:absolute;left:48px;display:block;font-size:12px;color:#ccc;line-height:1}.wy-switch.active:before{background:#1e8449}.wy-switch.active:after{left:24px;background:#27ae60}.wy-switch.disabled{cursor:not-allowed;opacity:.8}.wy-control-group.wy-control-group-error .wy-form-message,.wy-control-group.wy-control-group-error>label{color:#e74c3c}.wy-control-group.wy-control-group-error input[type=color],.wy-control-group.wy-control-group-error input[type=date],.wy-control-group.wy-control-group-error input[type=datetime-local],.wy-control-group.wy-control-group-error input[type=datetime],.wy-control-group.wy-control-group-error input[type=email],.wy-control-group.wy-control-group-error input[type=month],.wy-control-group.wy-control-group-error input[type=number],.wy-control-group.wy-control-group-error input[type=password],.wy-control-group.wy-control-group-error input[type=search],.wy-control-group.wy-control-group-error input[type=tel],.wy-control-group.wy-control-group-error input[type=text],.wy-control-group.wy-control-group-error input[type=time],.wy-control-group.wy-control-group-error input[type=url],.wy-control-group.wy-control-group-error input[type=week],.wy-control-group.wy-control-group-error textarea{border:1px solid #e74c3c}.wy-inline-validate{white-space:nowrap}.wy-inline-validate .wy-input-context{padding:.5em .625em;display:inline-block;font-size:80%}.wy-inline-validate.wy-inline-validate-success .wy-input-context{color:#27ae60}.wy-inline-validate.wy-inline-validate-danger .wy-input-context{color:#e74c3c}.wy-inline-validate.wy-inline-validate-warning .wy-input-context{color:#e67e22}.wy-inline-validate.wy-inline-validate-info .wy-input-context{color:#2980b9}.rotate-90{-webkit-transform:rotate(90deg);-moz-transform:rotate(90deg);-ms-transform:rotate(90deg);-o-transform:rotate(90deg);transform:rotate(90deg)}.rotate-180{-webkit-transform:rotate(180deg);-moz-transform:rotate(180deg);-ms-transform:rotate(180deg);-o-transform:rotate(180deg);transform:rotate(180deg)}.rotate-270{-webkit-transform:rotate(270deg);-moz-transform:rotate(270deg);-ms-transform:rotate(270deg);-o-transform:rotate(270deg);transform:rotate(270deg)}.mirror{-webkit-transform:scaleX(-1);-moz-transform:scaleX(-1);-ms-transform:scaleX(-1);-o-transform:scaleX(-1);transform:scaleX(-1)}.mirror.rotate-90{-webkit-transform:scaleX(-1) rotate(90deg);-moz-transform:scaleX(-1) rotate(90deg);-ms-transform:scaleX(-1) rotate(90deg);-o-transform:scaleX(-1) rotate(90deg);transform:scaleX(-1) rotate(90deg)}.mirror.rotate-180{-webkit-transform:scaleX(-1) rotate(180deg);-moz-transform:scaleX(-1) rotate(180deg);-ms-transform:scaleX(-1) rotate(180deg);-o-transform:scaleX(-1) rotate(180deg);transform:scaleX(-1) rotate(180deg)}.mirror.rotate-270{-webkit-transform:scaleX(-1) rotate(270deg);-moz-transform:scaleX(-1) rotate(270deg);-ms-transform:scaleX(-1) rotate(270deg);-o-transform:scaleX(-1) rotate(270deg);transform:scaleX(-1) rotate(270deg)}@media only screen and (max-width:480px){.wy-form button[type=submit]{margin:.7em 0 0}.wy-form input[type=color],.wy-form input[type=date],.wy-form input[type=datetime-local],.wy-form input[type=datetime],.wy-form input[type=email],.wy-form input[type=month],.wy-form input[type=number],.wy-form input[type=password],.wy-form input[type=search],.wy-form input[type=tel],.wy-form input[type=text],.wy-form input[type=time],.wy-form input[type=url],.wy-form input[type=week],.wy-form label{margin-bottom:.3em;display:block}.wy-form input[type=color],.wy-form input[type=date],.wy-form input[type=datetime-local],.wy-form input[type=datetime],.wy-form input[type=email],.wy-form input[type=month],.wy-form input[type=number],.wy-form input[type=password],.wy-form input[type=search],.wy-form input[type=tel],.wy-form input[type=time],.wy-form input[type=url],.wy-form input[type=week]{margin-bottom:0}.wy-form-aligned .wy-control-group label{margin-bottom:.3em;text-align:left;display:block;width:100%}.wy-form-aligned .wy-control{margin:1.5em 0 0}.wy-form-message,.wy-form-message-inline,.wy-form .wy-help-inline{display:block;font-size:80%;padding:6px 0}}@media screen and (max-width:768px){.tablet-hide{display:none}}@media screen and (max-width:480px){.mobile-hide{display:none}}.float-left{float:left}.float-right{float:right}.full-width{width:100%}.rst-content table.docutils,.rst-content table.field-list,.wy-table{border-collapse:collapse;border-spacing:0;empty-cells:show;margin-bottom:24px}.rst-content table.docutils caption,.rst-content table.field-list caption,.wy-table caption{color:#000;font:italic 85%/1 arial,sans-serif;padding:1em 0;text-align:center}.rst-content table.docutils td,.rst-content table.docutils th,.rst-content table.field-list td,.rst-content table.field-list th,.wy-table td,.wy-table th{font-size:90%;margin:0;overflow:visible;padding:8px 16px}.rst-content table.docutils td:first-child,.rst-content table.docutils th:first-child,.rst-content table.field-list td:first-child,.rst-content table.field-list th:first-child,.wy-table td:first-child,.wy-table th:first-child{border-left-width:0}.rst-content table.docutils thead,.rst-content table.field-list thead,.wy-table thead{color:#000;text-align:left;vertical-align:bottom;white-space:nowrap}.rst-content table.docutils thead th,.rst-content table.field-list thead th,.wy-table thead th{font-weight:700;border-bottom:2px solid #e1e4e5}.rst-content table.docutils td,.rst-content table.field-list td,.wy-table td{background-color:transparent;vertical-align:middle}.rst-content table.docutils td p,.rst-content table.field-list td p,.wy-table td p{line-height:18px}.rst-content table.docutils td p:last-child,.rst-content table.field-list td p:last-child,.wy-table td p:last-child{margin-bottom:0}.rst-content table.docutils .wy-table-cell-min,.rst-content table.field-list .wy-table-cell-min,.wy-table .wy-table-cell-min{width:1%;padding-right:0}.rst-content table.docutils .wy-table-cell-min input[type=checkbox],.rst-content table.field-list .wy-table-cell-min input[type=checkbox],.wy-table .wy-table-cell-min input[type=checkbox]{margin:0}.wy-table-secondary{color:grey;font-size:90%}.wy-table-tertiary{color:grey;font-size:80%}.rst-content table.docutils:not(.field-list) tr:nth-child(2n-1) td,.wy-table-backed,.wy-table-odd td,.wy-table-striped tr:nth-child(2n-1) td{background-color:#f3f6f6}.rst-content table.docutils,.wy-table-bordered-all{border:1px solid #e1e4e5}.rst-content table.docutils td,.wy-table-bordered-all td{border-bottom:1px solid #e1e4e5;border-left:1px solid #e1e4e5}.rst-content table.docutils tbody>tr:last-child td,.wy-table-bordered-all tbody>tr:last-child td{border-bottom-width:0}.wy-table-bordered{border:1px solid #e1e4e5}.wy-table-bordered-rows td{border-bottom:1px solid #e1e4e5}.wy-table-bordered-rows tbody>tr:last-child td{border-bottom-width:0}.wy-table-horizontal td,.wy-table-horizontal th{border-width:0 0 1px;border-bottom:1px solid #e1e4e5}.wy-table-horizontal tbody>tr:last-child td{border-bottom-width:0}.wy-table-responsive{margin-bottom:24px;max-width:100%;overflow:auto}.wy-table-responsive table{margin-bottom:0!important}.wy-table-responsive table td,.wy-table-responsive table th{white-space:nowrap}a{color:#2980b9;text-decoration:none;cursor:pointer}a:hover{color:#3091d1}a:visited{color:#9b59b6}html{height:100%}body,html{overflow-x:hidden}body{font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif;font-weight:400;color:#404040;min-height:100%;background:#edf0f2}.wy-text-left{text-align:left}.wy-text-center{text-align:center}.wy-text-right{text-align:right}.wy-text-large{font-size:120%}.wy-text-normal{font-size:100%}.wy-text-small,small{font-size:80%}.wy-text-strike{text-decoration:line-through}.wy-text-warning{color:#e67e22!important}a.wy-text-warning:hover{color:#eb9950!important}.wy-text-info{color:#2980b9!important}a.wy-text-info:hover{color:#409ad5!important}.wy-text-success{color:#27ae60!important}a.wy-text-success:hover{color:#36d278!important}.wy-text-danger{color:#e74c3c!important}a.wy-text-danger:hover{color:#ed7669!important}.wy-text-neutral{color:#404040!important}a.wy-text-neutral:hover{color:#595959!important}.rst-content .toctree-wrapper>p.caption,h1,h2,h3,h4,h5,h6,legend{margin-top:0;font-weight:700;font-family:Roboto Slab,ff-tisa-web-pro,Georgia,Arial,sans-serif}p{line-height:24px;font-size:16px;margin:0 0 24px}h1{font-size:175%}.rst-content .toctree-wrapper>p.caption,h2{font-size:150%}h3{font-size:125%}h4{font-size:115%}h5{font-size:110%}h6{font-size:100%}hr{display:block;height:1px;border:0;border-top:1px solid #e1e4e5;margin:24px 0;padding:0}.rst-content code,.rst-content tt,code{white-space:nowrap;max-width:100%;background:#fff;border:1px solid #e1e4e5;font-size:75%;padding:0 5px;font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;color:#e74c3c;overflow-x:auto}.rst-content tt.code-large,code.code-large{font-size:90%}.rst-content .section ul,.rst-content .toctree-wrapper ul,.rst-content section ul,.wy-plain-list-disc,article ul{list-style:disc;line-height:24px;margin-bottom:24px}.rst-content .section ul li,.rst-content .toctree-wrapper ul li,.rst-content section ul li,.wy-plain-list-disc li,article ul li{list-style:disc;margin-left:24px}.rst-content .section ul li p:last-child,.rst-content .section ul li ul,.rst-content .toctree-wrapper ul li p:last-child,.rst-content .toctree-wrapper ul li ul,.rst-content section ul li p:last-child,.rst-content section ul li ul,.wy-plain-list-disc li p:last-child,.wy-plain-list-disc li ul,article ul li p:last-child,article ul li ul{margin-bottom:0}.rst-content .section ul li li,.rst-content .toctree-wrapper ul li li,.rst-content section ul li li,.wy-plain-list-disc li li,article ul li li{list-style:circle}.rst-content .section ul li li li,.rst-content .toctree-wrapper ul li li li,.rst-content section ul li li li,.wy-plain-list-disc li li li,article ul li li li{list-style:square}.rst-content .section ul li ol li,.rst-content .toctree-wrapper ul li ol li,.rst-content section ul li ol li,.wy-plain-list-disc li ol li,article ul li ol li{list-style:decimal}.rst-content .section ol,.rst-content .section ol.arabic,.rst-content .toctree-wrapper ol,.rst-content .toctree-wrapper ol.arabic,.rst-content section ol,.rst-content section ol.arabic,.wy-plain-list-decimal,article ol{list-style:decimal;line-height:24px;margin-bottom:24px}.rst-content .section ol.arabic li,.rst-content .section ol li,.rst-content .toctree-wrapper ol.arabic li,.rst-content .toctree-wrapper ol li,.rst-content section ol.arabic li,.rst-content section ol li,.wy-plain-list-decimal li,article ol li{list-style:decimal;margin-left:24px}.rst-content .section ol.arabic li ul,.rst-content .section ol li p:last-child,.rst-content .section ol li ul,.rst-content .toctree-wrapper ol.arabic li ul,.rst-content .toctree-wrapper ol li p:last-child,.rst-content .toctree-wrapper ol li ul,.rst-content section ol.arabic li ul,.rst-content section ol li p:last-child,.rst-content section ol li ul,.wy-plain-list-decimal li p:last-child,.wy-plain-list-decimal li ul,article ol li p:last-child,article ol li ul{margin-bottom:0}.rst-content .section ol.arabic li ul li,.rst-content .section ol li ul li,.rst-content .toctree-wrapper ol.arabic li ul li,.rst-content .toctree-wrapper ol li ul li,.rst-content section ol.arabic li ul li,.rst-content section ol li ul li,.wy-plain-list-decimal li ul li,article ol li ul li{list-style:disc}.wy-breadcrumbs{*zoom:1}.wy-breadcrumbs:after,.wy-breadcrumbs:before{display:table;content:""}.wy-breadcrumbs:after{clear:both}.wy-breadcrumbs>li{display:inline-block;padding-top:5px}.wy-breadcrumbs>li.wy-breadcrumbs-aside{float:right}.rst-content .wy-breadcrumbs>li code,.rst-content .wy-breadcrumbs>li tt,.wy-breadcrumbs>li .rst-content tt,.wy-breadcrumbs>li code{all:inherit;color:inherit}.breadcrumb-item:before{content:"/";color:#bbb;font-size:13px;padding:0 6px 0 3px}.wy-breadcrumbs-extra{margin-bottom:0;color:#b3b3b3;font-size:80%;display:inline-block}@media screen and (max-width:480px){.wy-breadcrumbs-extra,.wy-breadcrumbs li.wy-breadcrumbs-aside{display:none}}@media print{.wy-breadcrumbs li.wy-breadcrumbs-aside{display:none}}html{font-size:16px}.wy-affix{position:fixed;top:1.618em}.wy-menu a:hover{text-decoration:none}.wy-menu-horiz{*zoom:1}.wy-menu-horiz:after,.wy-menu-horiz:before{display:table;content:""}.wy-menu-horiz:after{clear:both}.wy-menu-horiz li,.wy-menu-horiz ul{display:inline-block}.wy-menu-horiz li:hover{background:hsla(0,0%,100%,.1)}.wy-menu-horiz li.divide-left{border-left:1px solid #404040}.wy-menu-horiz li.divide-right{border-right:1px solid #404040}.wy-menu-horiz a{height:32px;display:inline-block;line-height:32px;padding:0 16px}.wy-menu-vertical{width:300px}.wy-menu-vertical header,.wy-menu-vertical p.caption{color:#55a5d9;height:32px;line-height:32px;padding:0 1.618em;margin:12px 0 0;display:block;font-weight:700;text-transform:uppercase;font-size:85%;white-space:nowrap}.wy-menu-vertical ul{margin-bottom:0}.wy-menu-vertical li.divide-top{border-top:1px solid #404040}.wy-menu-vertical li.divide-bottom{border-bottom:1px solid #404040}.wy-menu-vertical li.current{background:#e3e3e3}.wy-menu-vertical li.current a{color:grey;border-right:1px solid #c9c9c9;padding:.4045em 2.427em}.wy-menu-vertical li.current a:hover{background:#d6d6d6}.rst-content .wy-menu-vertical li tt,.wy-menu-vertical li .rst-content tt,.wy-menu-vertical li code{border:none;background:inherit;color:inherit;padding-left:0;padding-right:0}.wy-menu-vertical li button.toctree-expand{display:block;float:left;margin-left:-1.2em;line-height:18px;color:#4d4d4d;border:none;background:none;padding:0}.wy-menu-vertical li.current>a,.wy-menu-vertical li.on a{color:#404040;font-weight:700;position:relative;background:#fcfcfc;border:none;padding:.4045em 1.618em}.wy-menu-vertical li.current>a:hover,.wy-menu-vertical li.on a:hover{background:#fcfcfc}.wy-menu-vertical li.current>a:hover button.toctree-expand,.wy-menu-vertical li.on a:hover button.toctree-expand{color:grey}.wy-menu-vertical li.current>a button.toctree-expand,.wy-menu-vertical li.on a button.toctree-expand{display:block;line-height:18px;color:#333}.wy-menu-vertical li.toctree-l1.current>a{border-bottom:1px solid #c9c9c9;border-top:1px solid #c9c9c9}.wy-menu-vertical .toctree-l1.current .toctree-l2>ul,.wy-menu-vertical .toctree-l2.current .toctree-l3>ul,.wy-menu-vertical .toctree-l3.current .toctree-l4>ul,.wy-menu-vertical .toctree-l4.current .toctree-l5>ul,.wy-menu-vertical .toctree-l5.current .toctree-l6>ul,.wy-menu-vertical .toctree-l6.current .toctree-l7>ul,.wy-menu-vertical .toctree-l7.current .toctree-l8>ul,.wy-menu-vertical .toctree-l8.current .toctree-l9>ul,.wy-menu-vertical .toctree-l9.current .toctree-l10>ul,.wy-menu-vertical .toctree-l10.current .toctree-l11>ul{display:none}.wy-menu-vertical .toctree-l1.current .current.toctree-l2>ul,.wy-menu-vertical .toctree-l2.current .current.toctree-l3>ul,.wy-menu-vertical .toctree-l3.current .current.toctree-l4>ul,.wy-menu-vertical .toctree-l4.current .current.toctree-l5>ul,.wy-menu-vertical .toctree-l5.current .current.toctree-l6>ul,.wy-menu-vertical .toctree-l6.current .current.toctree-l7>ul,.wy-menu-vertical .toctree-l7.current .current.toctree-l8>ul,.wy-menu-vertical .toctree-l8.current .current.toctree-l9>ul,.wy-menu-vertical .toctree-l9.current .current.toctree-l10>ul,.wy-menu-vertical .toctree-l10.current .current.toctree-l11>ul{display:block}.wy-menu-vertical li.toctree-l3,.wy-menu-vertical li.toctree-l4{font-size:.9em}.wy-menu-vertical li.toctree-l2 a,.wy-menu-vertical li.toctree-l3 a,.wy-menu-vertical li.toctree-l4 a,.wy-menu-vertical li.toctree-l5 a,.wy-menu-vertical li.toctree-l6 a,.wy-menu-vertical li.toctree-l7 a,.wy-menu-vertical li.toctree-l8 a,.wy-menu-vertical li.toctree-l9 a,.wy-menu-vertical li.toctree-l10 a{color:#404040}.wy-menu-vertical li.toctree-l2 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l3 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l4 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l5 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l6 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l7 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l8 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l9 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l10 a:hover button.toctree-expand{color:grey}.wy-menu-vertical li.toctree-l2.current li.toctree-l3>a,.wy-menu-vertical li.toctree-l3.current li.toctree-l4>a,.wy-menu-vertical li.toctree-l4.current li.toctree-l5>a,.wy-menu-vertical li.toctree-l5.current li.toctree-l6>a,.wy-menu-vertical li.toctree-l6.current li.toctree-l7>a,.wy-menu-vertical li.toctree-l7.current li.toctree-l8>a,.wy-menu-vertical li.toctree-l8.current li.toctree-l9>a,.wy-menu-vertical li.toctree-l9.current li.toctree-l10>a,.wy-menu-vertical li.toctree-l10.current li.toctree-l11>a{display:block}.wy-menu-vertical li.toctree-l2.current>a{padding:.4045em 2.427em}.wy-menu-vertical li.toctree-l2.current li.toctree-l3>a{padding:.4045em 1.618em .4045em 4.045em}.wy-menu-vertical li.toctree-l3.current>a{padding:.4045em 4.045em}.wy-menu-vertical li.toctree-l3.current li.toctree-l4>a{padding:.4045em 1.618em .4045em 5.663em}.wy-menu-vertical li.toctree-l4.current>a{padding:.4045em 5.663em}.wy-menu-vertical li.toctree-l4.current li.toctree-l5>a{padding:.4045em 1.618em .4045em 7.281em}.wy-menu-vertical li.toctree-l5.current>a{padding:.4045em 7.281em}.wy-menu-vertical li.toctree-l5.current li.toctree-l6>a{padding:.4045em 1.618em .4045em 8.899em}.wy-menu-vertical li.toctree-l6.current>a{padding:.4045em 8.899em}.wy-menu-vertical li.toctree-l6.current li.toctree-l7>a{padding:.4045em 1.618em .4045em 10.517em}.wy-menu-vertical li.toctree-l7.current>a{padding:.4045em 10.517em}.wy-menu-vertical li.toctree-l7.current li.toctree-l8>a{padding:.4045em 1.618em .4045em 12.135em}.wy-menu-vertical li.toctree-l8.current>a{padding:.4045em 12.135em}.wy-menu-vertical li.toctree-l8.current li.toctree-l9>a{padding:.4045em 1.618em .4045em 13.753em}.wy-menu-vertical li.toctree-l9.current>a{padding:.4045em 13.753em}.wy-menu-vertical li.toctree-l9.current li.toctree-l10>a{padding:.4045em 1.618em .4045em 15.371em}.wy-menu-vertical li.toctree-l10.current>a{padding:.4045em 15.371em}.wy-menu-vertical li.toctree-l10.current li.toctree-l11>a{padding:.4045em 1.618em .4045em 16.989em}.wy-menu-vertical li.toctree-l2.current>a,.wy-menu-vertical li.toctree-l2.current li.toctree-l3>a{background:#c9c9c9}.wy-menu-vertical li.toctree-l2 button.toctree-expand{color:#a3a3a3}.wy-menu-vertical li.toctree-l3.current>a,.wy-menu-vertical li.toctree-l3.current li.toctree-l4>a{background:#bdbdbd}.wy-menu-vertical li.toctree-l3 button.toctree-expand{color:#969696}.wy-menu-vertical li.current ul{display:block}.wy-menu-vertical li ul{margin-bottom:0;display:none}.wy-menu-vertical li ul li a{margin-bottom:0;color:#d9d9d9;font-weight:400}.wy-menu-vertical a{line-height:18px;padding:.4045em 1.618em;display:block;position:relative;font-size:90%;color:#d9d9d9}.wy-menu-vertical a:hover{background-color:#4e4a4a;cursor:pointer}.wy-menu-vertical a:hover button.toctree-expand{color:#d9d9d9}.wy-menu-vertical a:active{background-color:#2980b9;cursor:pointer;color:#fff}.wy-menu-vertical a:active button.toctree-expand{color:#fff}.wy-side-nav-search{display:block;width:300px;padding:.809em;margin-bottom:.809em;z-index:200;background-color:#2980b9;text-align:center;color:#fcfcfc}.wy-side-nav-search input[type=text]{width:100%;border-radius:50px;padding:6px 12px;border-color:#2472a4}.wy-side-nav-search img{display:block;margin:auto auto .809em;height:45px;width:45px;background-color:#2980b9;padding:5px;border-radius:100%}.wy-side-nav-search .wy-dropdown>a,.wy-side-nav-search>a{color:#fcfcfc;font-size:100%;font-weight:700;display:inline-block;padding:4px 6px;margin-bottom:.809em;max-width:100%}.wy-side-nav-search .wy-dropdown>a:hover,.wy-side-nav-search>a:hover{background:hsla(0,0%,100%,.1)}.wy-side-nav-search .wy-dropdown>a img.logo,.wy-side-nav-search>a img.logo{display:block;margin:0 auto;height:auto;width:auto;border-radius:0;max-width:100%;background:transparent}.wy-side-nav-search .wy-dropdown>a.icon img.logo,.wy-side-nav-search>a.icon img.logo{margin-top:.85em}.wy-side-nav-search>div.version{margin-top:-.4045em;margin-bottom:.809em;font-weight:400;color:hsla(0,0%,100%,.3)}.wy-nav .wy-menu-vertical header{color:#2980b9}.wy-nav .wy-menu-vertical a{color:#b3b3b3}.wy-nav .wy-menu-vertical a:hover{background-color:#2980b9;color:#fff}[data-menu-wrap]{-webkit-transition:all .2s ease-in;-moz-transition:all .2s ease-in;transition:all .2s ease-in;position:absolute;opacity:1;width:100%;opacity:0}[data-menu-wrap].move-center{left:0;right:auto;opacity:1}[data-menu-wrap].move-left{right:auto;left:-100%;opacity:0}[data-menu-wrap].move-right{right:-100%;left:auto;opacity:0}.wy-body-for-nav{background:#fcfcfc}.wy-grid-for-nav{position:absolute;width:100%;height:100%}.wy-nav-side{position:fixed;top:0;bottom:0;left:0;padding-bottom:2em;width:300px;overflow-x:hidden;overflow-y:hidden;min-height:100%;color:#9b9b9b;background:#343131;z-index:200}.wy-side-scroll{width:320px;position:relative;overflow-x:hidden;overflow-y:scroll;height:100%}.wy-nav-top{display:none;background:#2980b9;color:#fff;padding:.4045em .809em;position:relative;line-height:50px;text-align:center;font-size:100%;*zoom:1}.wy-nav-top:after,.wy-nav-top:before{display:table;content:""}.wy-nav-top:after{clear:both}.wy-nav-top a{color:#fff;font-weight:700}.wy-nav-top img{margin-right:12px;height:45px;width:45px;background-color:#2980b9;padding:5px;border-radius:100%}.wy-nav-top i{font-size:30px;float:left;cursor:pointer;padding-top:inherit}.wy-nav-content-wrap{margin-left:300px;background:#fcfcfc;min-height:100%}.wy-nav-content{padding:1.618em 3.236em;height:100%;max-width:800px;margin:auto}.wy-body-mask{position:fixed;width:100%;height:100%;background:rgba(0,0,0,.2);display:none;z-index:499}.wy-body-mask.on{display:block}footer{color:grey}footer p{margin-bottom:12px}.rst-content footer span.commit tt,footer span.commit .rst-content tt,footer span.commit code{padding:0;font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;font-size:1em;background:none;border:none;color:grey}.rst-footer-buttons{*zoom:1}.rst-footer-buttons:after,.rst-footer-buttons:before{width:100%;display:table;content:""}.rst-footer-buttons:after{clear:both}.rst-breadcrumbs-buttons{margin-top:12px;*zoom:1}.rst-breadcrumbs-buttons:after,.rst-breadcrumbs-buttons:before{display:table;content:""}.rst-breadcrumbs-buttons:after{clear:both}#search-results .search li{margin-bottom:24px;border-bottom:1px solid #e1e4e5;padding-bottom:24px}#search-results .search li:first-child{border-top:1px solid #e1e4e5;padding-top:24px}#search-results .search li a{font-size:120%;margin-bottom:12px;display:inline-block}#search-results .context{color:grey;font-size:90%}.genindextable li>ul{margin-left:24px}@media screen and (max-width:768px){.wy-body-for-nav{background:#fcfcfc}.wy-nav-top{display:block}.wy-nav-side{left:-300px}.wy-nav-side.shift{width:85%;left:0}.wy-menu.wy-menu-vertical,.wy-side-nav-search,.wy-side-scroll{width:auto}.wy-nav-content-wrap{margin-left:0}.wy-nav-content-wrap .wy-nav-content{padding:1.618em}.wy-nav-content-wrap.shift{position:fixed;min-width:100%;left:85%;top:0;height:100%;overflow:hidden}}@media screen and (min-width:1100px){.wy-nav-content-wrap{background:rgba(0,0,0,.05)}.wy-nav-content{margin:0;background:#fcfcfc}}@media print{.rst-versions,.wy-nav-side,footer{display:none}.wy-nav-content-wrap{margin-left:0}}.rst-versions{position:fixed;bottom:0;left:0;width:300px;color:#fcfcfc;background:#1f1d1d;font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif;z-index:400}.rst-versions a{color:#2980b9;text-decoration:none}.rst-versions .rst-badge-small{display:none}.rst-versions .rst-current-version{padding:12px;background-color:#272525;display:block;text-align:right;font-size:90%;cursor:pointer;color:#27ae60;*zoom:1}.rst-versions .rst-current-version:after,.rst-versions .rst-current-version:before{display:table;content:""}.rst-versions .rst-current-version:after{clear:both}.rst-content .code-block-caption .rst-versions .rst-current-version .headerlink,.rst-content .eqno .rst-versions .rst-current-version .headerlink,.rst-content .rst-versions .rst-current-version .admonition-title,.rst-content code.download .rst-versions .rst-current-version span:first-child,.rst-content dl dt .rst-versions .rst-current-version .headerlink,.rst-content h1 .rst-versions .rst-current-version .headerlink,.rst-content h2 .rst-versions .rst-current-version .headerlink,.rst-content h3 .rst-versions .rst-current-version .headerlink,.rst-content h4 .rst-versions .rst-current-version .headerlink,.rst-content h5 .rst-versions .rst-current-version .headerlink,.rst-content h6 .rst-versions .rst-current-version .headerlink,.rst-content p .rst-versions .rst-current-version .headerlink,.rst-content table>caption .rst-versions .rst-current-version .headerlink,.rst-content tt.download .rst-versions .rst-current-version span:first-child,.rst-versions .rst-current-version .fa,.rst-versions .rst-current-version .icon,.rst-versions .rst-current-version .rst-content .admonition-title,.rst-versions .rst-current-version .rst-content .code-block-caption .headerlink,.rst-versions .rst-current-version .rst-content .eqno .headerlink,.rst-versions .rst-current-version .rst-content code.download span:first-child,.rst-versions .rst-current-version .rst-content dl dt .headerlink,.rst-versions .rst-current-version .rst-content h1 .headerlink,.rst-versions .rst-current-version .rst-content h2 .headerlink,.rst-versions .rst-current-version .rst-content h3 .headerlink,.rst-versions .rst-current-version .rst-content h4 .headerlink,.rst-versions .rst-current-version .rst-content h5 .headerlink,.rst-versions .rst-current-version .rst-content h6 .headerlink,.rst-versions .rst-current-version .rst-content p .headerlink,.rst-versions .rst-current-version .rst-content table>caption .headerlink,.rst-versions .rst-current-version .rst-content tt.download span:first-child,.rst-versions .rst-current-version .wy-menu-vertical li button.toctree-expand,.wy-menu-vertical li .rst-versions .rst-current-version button.toctree-expand{color:#fcfcfc}.rst-versions .rst-current-version .fa-book,.rst-versions .rst-current-version .icon-book{float:left}.rst-versions .rst-current-version.rst-out-of-date{background-color:#e74c3c;color:#fff}.rst-versions .rst-current-version.rst-active-old-version{background-color:#f1c40f;color:#000}.rst-versions.shift-up{height:auto;max-height:100%;overflow-y:scroll}.rst-versions.shift-up .rst-other-versions{display:block}.rst-versions .rst-other-versions{font-size:90%;padding:12px;color:grey;display:none}.rst-versions .rst-other-versions hr{display:block;height:1px;border:0;margin:20px 0;padding:0;border-top:1px solid #413d3d}.rst-versions .rst-other-versions dd{display:inline-block;margin:0}.rst-versions .rst-other-versions dd a{display:inline-block;padding:6px;color:#fcfcfc}.rst-versions.rst-badge{width:auto;bottom:20px;right:20px;left:auto;border:none;max-width:300px;max-height:90%}.rst-versions.rst-badge .fa-book,.rst-versions.rst-badge .icon-book{float:none;line-height:30px}.rst-versions.rst-badge.shift-up .rst-current-version{text-align:right}.rst-versions.rst-badge.shift-up .rst-current-version .fa-book,.rst-versions.rst-badge.shift-up .rst-current-version .icon-book{float:left}.rst-versions.rst-badge>.rst-current-version{width:auto;height:30px;line-height:30px;padding:0 6px;display:block;text-align:center}@media screen and (max-width:768px){.rst-versions{width:85%;display:none}.rst-versions.shift{display:block}}.rst-content .toctree-wrapper>p.caption,.rst-content h1,.rst-content h2,.rst-content h3,.rst-content h4,.rst-content h5,.rst-content h6{margin-bottom:24px}.rst-content img{max-width:100%;height:auto}.rst-content div.figure,.rst-content figure{margin-bottom:24px}.rst-content div.figure .caption-text,.rst-content figure .caption-text{font-style:italic}.rst-content div.figure p:last-child.caption,.rst-content figure p:last-child.caption{margin-bottom:0}.rst-content div.figure.align-center,.rst-content figure.align-center{text-align:center}.rst-content .section>a>img,.rst-content .section>img,.rst-content section>a>img,.rst-content section>img{margin-bottom:24px}.rst-content abbr[title]{text-decoration:none}.rst-content.style-external-links a.reference.external:after{font-family:FontAwesome;content:"\f08e";color:#b3b3b3;vertical-align:super;font-size:60%;margin:0 .2em}.rst-content blockquote{margin-left:24px;line-height:24px;margin-bottom:24px}.rst-content pre.literal-block{white-space:pre;margin:0;padding:12px;font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;display:block;overflow:auto}.rst-content div[class^=highlight],.rst-content pre.literal-block{border:1px solid #e1e4e5;overflow-x:auto;margin:1px 0 24px}.rst-content div[class^=highlight] div[class^=highlight],.rst-content pre.literal-block div[class^=highlight]{padding:0;border:none;margin:0}.rst-content div[class^=highlight] td.code{width:100%}.rst-content .linenodiv pre{border-right:1px solid #e6e9ea;margin:0;padding:12px;font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;user-select:none;pointer-events:none}.rst-content div[class^=highlight] pre{white-space:pre;margin:0;padding:12px;display:block;overflow:auto}.rst-content div[class^=highlight] pre .hll{display:block;margin:0 -12px;padding:0 12px}.rst-content .linenodiv pre,.rst-content div[class^=highlight] pre,.rst-content pre.literal-block{font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;font-size:12px;line-height:1.4}.rst-content div.highlight .gp,.rst-content div.highlight span.linenos{user-select:none;pointer-events:none}.rst-content div.highlight span.linenos{display:inline-block;padding-left:0;padding-right:12px;margin-right:12px;border-right:1px solid #e6e9ea}.rst-content .code-block-caption{font-style:italic;font-size:85%;line-height:1;padding:1em 0;text-align:center}@media print{.rst-content .codeblock,.rst-content div[class^=highlight],.rst-content div[class^=highlight] pre{white-space:pre-wrap}}.rst-content .admonition,.rst-content .admonition-todo,.rst-content .attention,.rst-content .caution,.rst-content .danger,.rst-content .error,.rst-content .hint,.rst-content .important,.rst-content .note,.rst-content .seealso,.rst-content .tip,.rst-content .warning{clear:both}.rst-content .admonition-todo .last,.rst-content .admonition-todo>:last-child,.rst-content .admonition .last,.rst-content .admonition>:last-child,.rst-content .attention .last,.rst-content .attention>:last-child,.rst-content .caution .last,.rst-content .caution>:last-child,.rst-content .danger .last,.rst-content .danger>:last-child,.rst-content .error .last,.rst-content .error>:last-child,.rst-content .hint .last,.rst-content .hint>:last-child,.rst-content .important .last,.rst-content .important>:last-child,.rst-content .note .last,.rst-content .note>:last-child,.rst-content .seealso .last,.rst-content .seealso>:last-child,.rst-content .tip .last,.rst-content .tip>:last-child,.rst-content .warning .last,.rst-content .warning>:last-child{margin-bottom:0}.rst-content .admonition-title:before{margin-right:4px}.rst-content .admonition table{border-color:rgba(0,0,0,.1)}.rst-content .admonition table td,.rst-content .admonition table th{background:transparent!important;border-color:rgba(0,0,0,.1)!important}.rst-content .section ol.loweralpha,.rst-content .section ol.loweralpha>li,.rst-content .toctree-wrapper ol.loweralpha,.rst-content .toctree-wrapper ol.loweralpha>li,.rst-content section ol.loweralpha,.rst-content section ol.loweralpha>li{list-style:lower-alpha}.rst-content .section ol.upperalpha,.rst-content .section ol.upperalpha>li,.rst-content .toctree-wrapper ol.upperalpha,.rst-content .toctree-wrapper ol.upperalpha>li,.rst-content section ol.upperalpha,.rst-content section ol.upperalpha>li{list-style:upper-alpha}.rst-content .section ol li>*,.rst-content .section ul li>*,.rst-content .toctree-wrapper ol li>*,.rst-content .toctree-wrapper ul li>*,.rst-content section ol li>*,.rst-content section ul li>*{margin-top:12px;margin-bottom:12px}.rst-content .section ol li>:first-child,.rst-content .section ul li>:first-child,.rst-content .toctree-wrapper ol li>:first-child,.rst-content .toctree-wrapper ul li>:first-child,.rst-content section ol li>:first-child,.rst-content section ul li>:first-child{margin-top:0}.rst-content .section ol li>p,.rst-content .section ol li>p:last-child,.rst-content .section ul li>p,.rst-content .section ul li>p:last-child,.rst-content .toctree-wrapper ol li>p,.rst-content .toctree-wrapper ol li>p:last-child,.rst-content .toctree-wrapper ul li>p,.rst-content .toctree-wrapper ul li>p:last-child,.rst-content section ol li>p,.rst-content section ol li>p:last-child,.rst-content section ul li>p,.rst-content section ul li>p:last-child{margin-bottom:12px}.rst-content .section ol li>p:only-child,.rst-content .section ol li>p:only-child:last-child,.rst-content .section ul li>p:only-child,.rst-content .section ul li>p:only-child:last-child,.rst-content .toctree-wrapper ol li>p:only-child,.rst-content .toctree-wrapper ol li>p:only-child:last-child,.rst-content .toctree-wrapper ul li>p:only-child,.rst-content .toctree-wrapper ul li>p:only-child:last-child,.rst-content section ol li>p:only-child,.rst-content section ol li>p:only-child:last-child,.rst-content section ul li>p:only-child,.rst-content section ul li>p:only-child:last-child{margin-bottom:0}.rst-content .section ol li>ol,.rst-content .section ol li>ul,.rst-content .section ul li>ol,.rst-content .section ul li>ul,.rst-content .toctree-wrapper ol li>ol,.rst-content .toctree-wrapper ol li>ul,.rst-content .toctree-wrapper ul li>ol,.rst-content .toctree-wrapper ul li>ul,.rst-content section ol li>ol,.rst-content section ol li>ul,.rst-content section ul li>ol,.rst-content section ul li>ul{margin-bottom:12px}.rst-content .section ol.simple li>*,.rst-content .section ol.simple li ol,.rst-content .section ol.simple li ul,.rst-content .section ul.simple li>*,.rst-content .section ul.simple li ol,.rst-content .section ul.simple li ul,.rst-content .toctree-wrapper ol.simple li>*,.rst-content .toctree-wrapper ol.simple li ol,.rst-content .toctree-wrapper ol.simple li ul,.rst-content .toctree-wrapper ul.simple li>*,.rst-content .toctree-wrapper ul.simple li ol,.rst-content .toctree-wrapper ul.simple li ul,.rst-content section ol.simple li>*,.rst-content section ol.simple li ol,.rst-content section ol.simple li ul,.rst-content section ul.simple li>*,.rst-content section ul.simple li ol,.rst-content section ul.simple li ul{margin-top:0;margin-bottom:0}.rst-content .line-block{margin-left:0;margin-bottom:24px;line-height:24px}.rst-content .line-block .line-block{margin-left:24px;margin-bottom:0}.rst-content .topic-title{font-weight:700;margin-bottom:12px}.rst-content .toc-backref{color:#404040}.rst-content .align-right{float:right;margin:0 0 24px 24px}.rst-content .align-left{float:left;margin:0 24px 24px 0}.rst-content .align-center{margin:auto}.rst-content .align-center:not(table){display:block}.rst-content .code-block-caption .headerlink,.rst-content .eqno .headerlink,.rst-content .toctree-wrapper>p.caption .headerlink,.rst-content dl dt .headerlink,.rst-content h1 .headerlink,.rst-content h2 .headerlink,.rst-content h3 .headerlink,.rst-content h4 .headerlink,.rst-content h5 .headerlink,.rst-content h6 .headerlink,.rst-content p.caption .headerlink,.rst-content p .headerlink,.rst-content table>caption .headerlink{opacity:0;font-size:14px;font-family:FontAwesome;margin-left:.5em}.rst-content .code-block-caption .headerlink:focus,.rst-content .code-block-caption:hover .headerlink,.rst-content .eqno .headerlink:focus,.rst-content .eqno:hover .headerlink,.rst-content .toctree-wrapper>p.caption .headerlink:focus,.rst-content .toctree-wrapper>p.caption:hover .headerlink,.rst-content dl dt .headerlink:focus,.rst-content dl dt:hover .headerlink,.rst-content h1 .headerlink:focus,.rst-content h1:hover .headerlink,.rst-content h2 .headerlink:focus,.rst-content h2:hover .headerlink,.rst-content h3 .headerlink:focus,.rst-content h3:hover .headerlink,.rst-content h4 .headerlink:focus,.rst-content h4:hover .headerlink,.rst-content h5 .headerlink:focus,.rst-content h5:hover .headerlink,.rst-content h6 .headerlink:focus,.rst-content h6:hover .headerlink,.rst-content p.caption .headerlink:focus,.rst-content p.caption:hover .headerlink,.rst-content p .headerlink:focus,.rst-content p:hover .headerlink,.rst-content table>caption .headerlink:focus,.rst-content table>caption:hover .headerlink{opacity:1}.rst-content p a{overflow-wrap:anywhere}.rst-content .wy-table td p,.rst-content .wy-table td ul,.rst-content .wy-table th p,.rst-content .wy-table th ul,.rst-content table.docutils td p,.rst-content table.docutils td ul,.rst-content table.docutils th p,.rst-content table.docutils th ul,.rst-content table.field-list td p,.rst-content table.field-list td ul,.rst-content table.field-list th p,.rst-content table.field-list th ul{font-size:inherit}.rst-content .btn:focus{outline:2px solid}.rst-content table>caption .headerlink:after{font-size:12px}.rst-content .centered{text-align:center}.rst-content .sidebar{float:right;width:40%;display:block;margin:0 0 24px 24px;padding:24px;background:#f3f6f6;border:1px solid #e1e4e5}.rst-content .sidebar dl,.rst-content .sidebar p,.rst-content .sidebar ul{font-size:90%}.rst-content .sidebar .last,.rst-content .sidebar>:last-child{margin-bottom:0}.rst-content .sidebar .sidebar-title{display:block;font-family:Roboto Slab,ff-tisa-web-pro,Georgia,Arial,sans-serif;font-weight:700;background:#e1e4e5;padding:6px 12px;margin:-24px -24px 24px;font-size:100%}.rst-content .highlighted{background:#f1c40f;box-shadow:0 0 0 2px #f1c40f;display:inline;font-weight:700}.rst-content .citation-reference,.rst-content .footnote-reference{vertical-align:baseline;position:relative;top:-.4em;line-height:0;font-size:90%}.rst-content .citation-reference>span.fn-bracket,.rst-content .footnote-reference>span.fn-bracket{display:none}.rst-content .hlist{width:100%}.rst-content dl dt span.classifier:before{content:" : "}.rst-content dl dt span.classifier-delimiter{display:none!important}html.writer-html4 .rst-content table.docutils.citation,html.writer-html4 .rst-content table.docutils.footnote{background:none;border:none}html.writer-html4 .rst-content table.docutils.citation td,html.writer-html4 .rst-content table.docutils.citation tr,html.writer-html4 .rst-content table.docutils.footnote td,html.writer-html4 .rst-content table.docutils.footnote tr{border:none;background-color:transparent!important;white-space:normal}html.writer-html4 .rst-content table.docutils.citation td.label,html.writer-html4 .rst-content table.docutils.footnote td.label{padding-left:0;padding-right:0;vertical-align:top}html.writer-html5 .rst-content dl.citation,html.writer-html5 .rst-content dl.field-list,html.writer-html5 .rst-content dl.footnote{display:grid;grid-template-columns:auto minmax(80%,95%)}html.writer-html5 .rst-content dl.citation>dt,html.writer-html5 .rst-content dl.field-list>dt,html.writer-html5 .rst-content dl.footnote>dt{display:inline-grid;grid-template-columns:max-content auto}html.writer-html5 .rst-content aside.citation,html.writer-html5 .rst-content aside.footnote,html.writer-html5 .rst-content div.citation{display:grid;grid-template-columns:auto auto minmax(.65rem,auto) minmax(40%,95%)}html.writer-html5 .rst-content aside.citation>span.label,html.writer-html5 .rst-content aside.footnote>span.label,html.writer-html5 .rst-content div.citation>span.label{grid-column-start:1;grid-column-end:2}html.writer-html5 .rst-content aside.citation>span.backrefs,html.writer-html5 .rst-content aside.footnote>span.backrefs,html.writer-html5 .rst-content div.citation>span.backrefs{grid-column-start:2;grid-column-end:3;grid-row-start:1;grid-row-end:3}html.writer-html5 .rst-content aside.citation>p,html.writer-html5 .rst-content aside.footnote>p,html.writer-html5 .rst-content div.citation>p{grid-column-start:4;grid-column-end:5}html.writer-html5 .rst-content dl.citation,html.writer-html5 .rst-content dl.field-list,html.writer-html5 .rst-content dl.footnote{margin-bottom:24px}html.writer-html5 .rst-content dl.citation>dt,html.writer-html5 .rst-content dl.field-list>dt,html.writer-html5 .rst-content dl.footnote>dt{padding-left:1rem}html.writer-html5 .rst-content dl.citation>dd,html.writer-html5 .rst-content dl.citation>dt,html.writer-html5 .rst-content dl.field-list>dd,html.writer-html5 .rst-content dl.field-list>dt,html.writer-html5 .rst-content dl.footnote>dd,html.writer-html5 .rst-content dl.footnote>dt{margin-bottom:0}html.writer-html5 .rst-content dl.citation,html.writer-html5 .rst-content dl.footnote{font-size:.9rem}html.writer-html5 .rst-content dl.citation>dt,html.writer-html5 .rst-content dl.footnote>dt{margin:0 .5rem .5rem 0;line-height:1.2rem;word-break:break-all;font-weight:400}html.writer-html5 .rst-content dl.citation>dt>span.brackets:before,html.writer-html5 .rst-content dl.footnote>dt>span.brackets:before{content:"["}html.writer-html5 .rst-content dl.citation>dt>span.brackets:after,html.writer-html5 .rst-content dl.footnote>dt>span.brackets:after{content:"]"}html.writer-html5 .rst-content dl.citation>dt>span.fn-backref,html.writer-html5 .rst-content dl.footnote>dt>span.fn-backref{text-align:left;font-style:italic;margin-left:.65rem;word-break:break-word;word-spacing:-.1rem;max-width:5rem}html.writer-html5 .rst-content dl.citation>dt>span.fn-backref>a,html.writer-html5 .rst-content dl.footnote>dt>span.fn-backref>a{word-break:keep-all}html.writer-html5 .rst-content dl.citation>dt>span.fn-backref>a:not(:first-child):before,html.writer-html5 .rst-content dl.footnote>dt>span.fn-backref>a:not(:first-child):before{content:" "}html.writer-html5 .rst-content dl.citation>dd,html.writer-html5 .rst-content dl.footnote>dd{margin:0 0 .5rem;line-height:1.2rem}html.writer-html5 .rst-content dl.citation>dd p,html.writer-html5 .rst-content dl.footnote>dd p{font-size:.9rem}html.writer-html5 .rst-content aside.citation,html.writer-html5 .rst-content aside.footnote,html.writer-html5 .rst-content div.citation{padding-left:1rem;padding-right:1rem;font-size:.9rem;line-height:1.2rem}html.writer-html5 .rst-content aside.citation p,html.writer-html5 .rst-content aside.footnote p,html.writer-html5 .rst-content div.citation p{font-size:.9rem;line-height:1.2rem;margin-bottom:12px}html.writer-html5 .rst-content aside.citation span.backrefs,html.writer-html5 .rst-content aside.footnote span.backrefs,html.writer-html5 .rst-content div.citation span.backrefs{text-align:left;font-style:italic;margin-left:.65rem;word-break:break-word;word-spacing:-.1rem;max-width:5rem}html.writer-html5 .rst-content aside.citation span.backrefs>a,html.writer-html5 .rst-content aside.footnote span.backrefs>a,html.writer-html5 .rst-content div.citation span.backrefs>a{word-break:keep-all}html.writer-html5 .rst-content aside.citation span.backrefs>a:not(:first-child):before,html.writer-html5 .rst-content aside.footnote span.backrefs>a:not(:first-child):before,html.writer-html5 .rst-content div.citation span.backrefs>a:not(:first-child):before{content:" "}html.writer-html5 .rst-content aside.citation span.label,html.writer-html5 .rst-content aside.footnote span.label,html.writer-html5 .rst-content div.citation span.label{line-height:1.2rem}html.writer-html5 .rst-content aside.citation-list,html.writer-html5 .rst-content aside.footnote-list,html.writer-html5 .rst-content div.citation-list{margin-bottom:24px}html.writer-html5 .rst-content dl.option-list kbd{font-size:.9rem}.rst-content table.docutils.footnote,html.writer-html4 .rst-content table.docutils.citation,html.writer-html5 .rst-content aside.footnote,html.writer-html5 .rst-content aside.footnote-list aside.footnote,html.writer-html5 .rst-content div.citation-list>div.citation,html.writer-html5 .rst-content dl.citation,html.writer-html5 .rst-content dl.footnote{color:grey}.rst-content table.docutils.footnote code,.rst-content table.docutils.footnote tt,html.writer-html4 .rst-content table.docutils.citation code,html.writer-html4 .rst-content table.docutils.citation tt,html.writer-html5 .rst-content aside.footnote-list aside.footnote code,html.writer-html5 .rst-content aside.footnote-list aside.footnote tt,html.writer-html5 .rst-content aside.footnote code,html.writer-html5 .rst-content aside.footnote tt,html.writer-html5 .rst-content div.citation-list>div.citation code,html.writer-html5 .rst-content div.citation-list>div.citation tt,html.writer-html5 .rst-content dl.citation code,html.writer-html5 .rst-content dl.citation tt,html.writer-html5 .rst-content dl.footnote code,html.writer-html5 .rst-content dl.footnote tt{color:#555}.rst-content .wy-table-responsive.citation,.rst-content .wy-table-responsive.footnote{margin-bottom:0}.rst-content .wy-table-responsive.citation+:not(.citation),.rst-content .wy-table-responsive.footnote+:not(.footnote){margin-top:24px}.rst-content .wy-table-responsive.citation:last-child,.rst-content .wy-table-responsive.footnote:last-child{margin-bottom:24px}.rst-content table.docutils th{border-color:#e1e4e5}html.writer-html5 .rst-content table.docutils th{border:1px solid #e1e4e5}html.writer-html5 .rst-content table.docutils td>p,html.writer-html5 .rst-content table.docutils th>p{line-height:1rem;margin-bottom:0;font-size:.9rem}.rst-content table.docutils td .last,.rst-content table.docutils td .last>:last-child{margin-bottom:0}.rst-content table.field-list,.rst-content table.field-list td{border:none}.rst-content table.field-list td p{line-height:inherit}.rst-content table.field-list td>strong{display:inline-block}.rst-content table.field-list .field-name{padding-right:10px;text-align:left;white-space:nowrap}.rst-content table.field-list .field-body{text-align:left}.rst-content code,.rst-content tt{color:#000;font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;padding:2px 5px}.rst-content code big,.rst-content code em,.rst-content tt big,.rst-content tt em{font-size:100%!important;line-height:normal}.rst-content code.literal,.rst-content tt.literal{color:#e74c3c;white-space:normal}.rst-content code.xref,.rst-content tt.xref,a .rst-content code,a .rst-content tt{font-weight:700;color:#404040;overflow-wrap:normal}.rst-content kbd,.rst-content pre,.rst-content samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace}.rst-content a code,.rst-content a tt{color:#2980b9}.rst-content dl{margin-bottom:24px}.rst-content dl dt{font-weight:700;margin-bottom:12px}.rst-content dl ol,.rst-content dl p,.rst-content dl table,.rst-content dl ul{margin-bottom:12px}.rst-content dl dd{margin:0 0 12px 24px;line-height:24px}.rst-content dl dd>ol:last-child,.rst-content dl dd>p:last-child,.rst-content dl dd>table:last-child,.rst-content dl dd>ul:last-child{margin-bottom:0}html.writer-html4 .rst-content dl:not(.docutils),html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple){margin-bottom:24px}html.writer-html4 .rst-content dl:not(.docutils)>dt,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple)>dt{display:table;margin:6px 0;font-size:90%;line-height:normal;background:#e7f2fa;color:#2980b9;border-top:3px solid #6ab0de;padding:6px;position:relative}html.writer-html4 .rst-content dl:not(.docutils)>dt:before,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple)>dt:before{color:#6ab0de}html.writer-html4 .rst-content dl:not(.docutils)>dt .headerlink,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple)>dt .headerlink{color:#404040;font-size:100%!important}html.writer-html4 .rst-content dl:not(.docutils) dl:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple)>dt,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) dl:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple)>dt{margin-bottom:6px;border:none;border-left:3px solid #ccc;background:#f0f0f0;color:#555}html.writer-html4 .rst-content dl:not(.docutils) dl:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple)>dt .headerlink,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) dl:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple)>dt .headerlink{color:#404040;font-size:100%!important}html.writer-html4 .rst-content dl:not(.docutils)>dt:first-child,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple)>dt:first-child{margin-top:0}html.writer-html4 .rst-content dl:not(.docutils) code.descclassname,html.writer-html4 .rst-content dl:not(.docutils) code.descname,html.writer-html4 .rst-content dl:not(.docutils) tt.descclassname,html.writer-html4 .rst-content dl:not(.docutils) tt.descname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) code.descclassname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) code.descname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) tt.descclassname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) tt.descname{background-color:transparent;border:none;padding:0;font-size:100%!important}html.writer-html4 .rst-content dl:not(.docutils) code.descname,html.writer-html4 .rst-content dl:not(.docutils) tt.descname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) code.descname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) tt.descname{font-weight:700}html.writer-html4 .rst-content dl:not(.docutils) .optional,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) .optional{display:inline-block;padding:0 4px;color:#000;font-weight:700}html.writer-html4 .rst-content dl:not(.docutils) .property,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) .property{display:inline-block;padding-right:8px;max-width:100%}html.writer-html4 .rst-content dl:not(.docutils) .k,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) .k{font-style:italic}html.writer-html4 .rst-content dl:not(.docutils) .descclassname,html.writer-html4 .rst-content dl:not(.docutils) .descname,html.writer-html4 .rst-content dl:not(.docutils) .sig-name,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) .descclassname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) .descname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) .sig-name{font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;color:#000}.rst-content .viewcode-back,.rst-content .viewcode-link{display:inline-block;color:#27ae60;font-size:80%;padding-left:24px}.rst-content .viewcode-back{display:block;float:right}.rst-content p.rubric{margin-bottom:12px;font-weight:700}.rst-content code.download,.rst-content tt.download{background:inherit;padding:inherit;font-weight:400;font-family:inherit;font-size:inherit;color:inherit;border:inherit;white-space:inherit}.rst-content code.download span:first-child,.rst-content tt.download span:first-child{-webkit-font-smoothing:subpixel-antialiased}.rst-content code.download span:first-child:before,.rst-content tt.download span:first-child:before{margin-right:4px}.rst-content .guilabel,.rst-content .menuselection{font-size:80%;font-weight:700;border-radius:4px;padding:2.4px 6px;margin:auto 2px}.rst-content .guilabel,.rst-content .menuselection{border:1px solid #7fbbe3;background:#e7f2fa}.rst-content :not(dl.option-list)>:not(dt):not(kbd):not(.kbd)>.kbd,.rst-content :not(dl.option-list)>:not(dt):not(kbd):not(.kbd)>kbd{color:inherit;font-size:80%;background-color:#fff;border:1px solid #a6a6a6;border-radius:4px;box-shadow:0 2px grey;padding:2.4px 6px;margin:auto 0}.rst-content .versionmodified{font-style:italic}@media screen and (max-width:480px){.rst-content .sidebar{width:100%}}span[id*=MathJax-Span]{color:#404040}.math{text-align:center}@font-face{font-family:Lato;src:url(fonts/lato-normal.woff2?bd03a2cc277bbbc338d464e679fe9942) format("woff2"),url(fonts/lato-normal.woff?27bd77b9162d388cb8d4c4217c7c5e2a) format("woff");font-weight:400;font-style:normal;font-display:block}@font-face{font-family:Lato;src:url(fonts/lato-bold.woff2?cccb897485813c7c256901dbca54ecf2) format("woff2"),url(fonts/lato-bold.woff?d878b6c29b10beca227e9eef4246111b) format("woff");font-weight:700;font-style:normal;font-display:block}@font-face{font-family:Lato;src:url(fonts/lato-bold-italic.woff2?0b6bb6725576b072c5d0b02ecdd1900d) format("woff2"),url(fonts/lato-bold-italic.woff?9c7e4e9eb485b4a121c760e61bc3707c) format("woff");font-weight:700;font-style:italic;font-display:block}@font-face{font-family:Lato;src:url(fonts/lato-normal-italic.woff2?4eb103b4d12be57cb1d040ed5e162e9d) format("woff2"),url(fonts/lato-normal-italic.woff?f28f2d6482446544ef1ea1ccc6dd5892) format("woff");font-weight:400;font-style:italic;font-display:block}@font-face{font-family:Roboto Slab;font-style:normal;font-weight:400;src:url(fonts/Roboto-Slab-Regular.woff2?7abf5b8d04d26a2cafea937019bca958) format("woff2"),url(fonts/Roboto-Slab-Regular.woff?c1be9284088d487c5e3ff0a10a92e58c) format("woff");font-display:block}@font-face{font-family:Roboto Slab;font-style:normal;font-weight:700;src:url(fonts/Roboto-Slab-Bold.woff2?9984f4a9bda09be08e83f2506954adbe) format("woff2"),url(fonts/Roboto-Slab-Bold.woff?bed5564a116b05148e3b3bea6fb1162a) format("woff");font-display:block} \ No newline at end of file diff --git a/_static/design-tabs.js b/_static/design-tabs.js new file mode 100644 index 0000000..b25bd6a --- /dev/null +++ b/_static/design-tabs.js @@ -0,0 +1,101 @@ +// @ts-check + +// Extra JS capability for selected tabs to be synced +// The selection is stored in local storage so that it persists across page loads. + +/** + * @type {Record} + */ +let sd_id_to_elements = {}; +const storageKeyPrefix = "sphinx-design-tab-id-"; + +/** + * Create a key for a tab element. + * @param {HTMLElement} el - The tab element. + * @returns {[string, string, string] | null} - The key. + * + */ +function create_key(el) { + let syncId = el.getAttribute("data-sync-id"); + let syncGroup = el.getAttribute("data-sync-group"); + if (!syncId || !syncGroup) return null; + return [syncGroup, syncId, syncGroup + "--" + syncId]; +} + +/** + * Initialize the tab selection. + * + */ +function ready() { + // Find all tabs with sync data + + /** @type {string[]} */ + let groups = []; + + document.querySelectorAll(".sd-tab-label").forEach((label) => { + if (label instanceof HTMLElement) { + let data = create_key(label); + if (data) { + let [group, id, key] = data; + + // add click event listener + // @ts-ignore + label.onclick = onSDLabelClick; + + // store map of key to elements + if (!sd_id_to_elements[key]) { + sd_id_to_elements[key] = []; + } + sd_id_to_elements[key].push(label); + + if (groups.indexOf(group) === -1) { + groups.push(group); + // Check if a specific tab has been selected via URL parameter + const tabParam = new URLSearchParams(window.location.search).get( + group + ); + if (tabParam) { + console.log( + "sphinx-design: Selecting tab id for group '" + + group + + "' from URL parameter: " + + tabParam + ); + window.sessionStorage.setItem(storageKeyPrefix + group, tabParam); + } + } + + // Check is a specific tab has been selected previously + let previousId = window.sessionStorage.getItem( + storageKeyPrefix + group + ); + if (previousId === id) { + // console.log( + // "sphinx-design: Selecting tab from session storage: " + id + // ); + // @ts-ignore + label.previousElementSibling.checked = true; + } + } + } + }); +} + +/** + * Activate other tabs with the same sync id. + * + * @this {HTMLElement} - The element that was clicked. + */ +function onSDLabelClick() { + let data = create_key(this); + if (!data) return; + let [group, id, key] = data; + for (const label of sd_id_to_elements[key]) { + if (label === this) continue; + // @ts-ignore + label.previousElementSibling.checked = true; + } + window.sessionStorage.setItem(storageKeyPrefix + group, id); +} + +document.addEventListener("DOMContentLoaded", ready, false); diff --git a/_static/docs_dataarray_accessor_1.png b/_static/docs_dataarray_accessor_1.png new file mode 100644 index 0000000..a792697 Binary files /dev/null and b/_static/docs_dataarray_accessor_1.png differ diff --git a/_static/docs_dataarray_accessor_2.png b/_static/docs_dataarray_accessor_2.png new file mode 100644 index 0000000..41e51f1 Binary files /dev/null and b/_static/docs_dataarray_accessor_2.png differ diff --git a/_static/docs_dataarray_accessor_4.png b/_static/docs_dataarray_accessor_4.png new file mode 100644 index 0000000..96c429e Binary files /dev/null and b/_static/docs_dataarray_accessor_4.png differ diff --git a/_static/docs_dataarray_accessor_5.png b/_static/docs_dataarray_accessor_5.png new file mode 100644 index 0000000..e40b402 Binary files /dev/null and b/_static/docs_dataarray_accessor_5.png differ diff --git a/_static/docs_dataset_accessor.png b/_static/docs_dataset_accessor.png new file mode 100644 index 0000000..3584d59 Binary files /dev/null and b/_static/docs_dataset_accessor.png differ diff --git a/_static/docs_demo_MyPlotter_simple.png b/_static/docs_demo_MyPlotter_simple.png new file mode 100644 index 0000000..60681dd Binary files /dev/null and b/_static/docs_demo_MyPlotter_simple.png differ diff --git a/_static/docs_framework_plotter_demo.png b/_static/docs_framework_plotter_demo.png new file mode 100644 index 0000000..a792697 Binary files /dev/null and b/_static/docs_framework_plotter_demo.png differ diff --git a/_static/docs_framework_project_demo1.png b/_static/docs_framework_project_demo1.png new file mode 100644 index 0000000..5456991 Binary files /dev/null and b/_static/docs_framework_project_demo1.png differ diff --git a/_static/docs_framework_project_demo2.png b/_static/docs_framework_project_demo2.png new file mode 100644 index 0000000..d37a8f7 Binary files /dev/null and b/_static/docs_framework_project_demo2.png differ diff --git a/_static/docs_getting_started.png b/_static/docs_getting_started.png new file mode 100644 index 0000000..a792697 Binary files /dev/null and b/_static/docs_getting_started.png differ diff --git a/_static/docs_getting_started_1.png b/_static/docs_getting_started_1.png new file mode 100644 index 0000000..543e8ba Binary files /dev/null and b/_static/docs_getting_started_1.png differ diff --git a/_static/docs_multiple_plots.png b/_static/docs_multiple_plots.png new file mode 100644 index 0000000..6016d07 Binary files /dev/null and b/_static/docs_multiple_plots.png differ diff --git a/_static/docs_presets_1.png b/_static/docs_presets_1.png new file mode 100644 index 0000000..fd1bde3 Binary files /dev/null and b/_static/docs_presets_1.png differ diff --git a/_static/docs_presets_2.png b/_static/docs_presets_2.png new file mode 100644 index 0000000..fd1bde3 Binary files /dev/null and b/_static/docs_presets_2.png differ diff --git a/_static/doctools.js b/_static/doctools.js new file mode 100644 index 0000000..4d67807 --- /dev/null +++ b/_static/doctools.js @@ -0,0 +1,156 @@ +/* + * doctools.js + * ~~~~~~~~~~~ + * + * Base JavaScript utilities for all Sphinx HTML documentation. + * + * :copyright: Copyright 2007-2024 by the Sphinx team, see AUTHORS. + * :license: BSD, see LICENSE for details. + * + */ +"use strict"; + +const BLACKLISTED_KEY_CONTROL_ELEMENTS = new Set([ + "TEXTAREA", + "INPUT", + "SELECT", + "BUTTON", +]); + +const _ready = (callback) => { + if (document.readyState !== "loading") { + callback(); + } else { + document.addEventListener("DOMContentLoaded", callback); + } +}; + +/** + * Small JavaScript module for the documentation. + */ +const Documentation = { + init: () => { + Documentation.initDomainIndexTable(); + Documentation.initOnKeyListeners(); + }, + + /** + * i18n support + */ + TRANSLATIONS: {}, + PLURAL_EXPR: (n) => (n === 1 ? 0 : 1), + LOCALE: "unknown", + + // gettext and ngettext don't access this so that the functions + // can safely bound to a different name (_ = Documentation.gettext) + gettext: (string) => { + const translated = Documentation.TRANSLATIONS[string]; + switch (typeof translated) { + case "undefined": + return string; // no translation + case "string": + return translated; // translation exists + default: + return translated[0]; // (singular, plural) translation tuple exists + } + }, + + ngettext: (singular, plural, n) => { + const translated = Documentation.TRANSLATIONS[singular]; + if (typeof translated !== "undefined") + return translated[Documentation.PLURAL_EXPR(n)]; + return n === 1 ? singular : plural; + }, + + addTranslations: (catalog) => { + Object.assign(Documentation.TRANSLATIONS, catalog.messages); + Documentation.PLURAL_EXPR = new Function( + "n", + `return (${catalog.plural_expr})` + ); + Documentation.LOCALE = catalog.locale; + }, + + /** + * helper function to focus on search bar + */ + focusSearchBar: () => { + document.querySelectorAll("input[name=q]")[0]?.focus(); + }, + + /** + * Initialise the domain index toggle buttons + */ + initDomainIndexTable: () => { + const toggler = (el) => { + const idNumber = el.id.substr(7); + const toggledRows = document.querySelectorAll(`tr.cg-${idNumber}`); + if (el.src.substr(-9) === "minus.png") { + el.src = `${el.src.substr(0, el.src.length - 9)}plus.png`; + toggledRows.forEach((el) => (el.style.display = "none")); + } else { + el.src = `${el.src.substr(0, el.src.length - 8)}minus.png`; + toggledRows.forEach((el) => (el.style.display = "")); + } + }; + + const togglerElements = document.querySelectorAll("img.toggler"); + togglerElements.forEach((el) => + el.addEventListener("click", (event) => toggler(event.currentTarget)) + ); + togglerElements.forEach((el) => (el.style.display = "")); + if (DOCUMENTATION_OPTIONS.COLLAPSE_INDEX) togglerElements.forEach(toggler); + }, + + initOnKeyListeners: () => { + // only install a listener if it is really needed + if ( + !DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS && + !DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS + ) + return; + + document.addEventListener("keydown", (event) => { + // bail for input elements + if (BLACKLISTED_KEY_CONTROL_ELEMENTS.has(document.activeElement.tagName)) return; + // bail with special keys + if (event.altKey || event.ctrlKey || event.metaKey) return; + + if (!event.shiftKey) { + switch (event.key) { + case "ArrowLeft": + if (!DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS) break; + + const prevLink = document.querySelector('link[rel="prev"]'); + if (prevLink && prevLink.href) { + window.location.href = prevLink.href; + event.preventDefault(); + } + break; + case "ArrowRight": + if (!DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS) break; + + const nextLink = document.querySelector('link[rel="next"]'); + if (nextLink && nextLink.href) { + window.location.href = nextLink.href; + event.preventDefault(); + } + break; + } + } + + // some keyboard layouts may need Shift to get / + switch (event.key) { + case "/": + if (!DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS) break; + Documentation.focusSearchBar(); + event.preventDefault(); + } + }); + }, +}; + +// quick alias for translations +const _ = Documentation.gettext; + +_ready(Documentation.init); diff --git a/_static/documentation_options.js b/_static/documentation_options.js new file mode 100644 index 0000000..7e4c114 --- /dev/null +++ b/_static/documentation_options.js @@ -0,0 +1,13 @@ +const DOCUMENTATION_OPTIONS = { + VERSION: '', + LANGUAGE: 'en', + COLLAPSE_INDEX: false, + BUILDER: 'html', + FILE_SUFFIX: '.html', + LINK_SUFFIX: '.html', + HAS_SOURCE: true, + SOURCELINK_SUFFIX: '.txt', + NAVIGATION_WITH_KEYS: false, + SHOW_SEARCH_SUMMARY: true, + ENABLE_SEARCH_SHORTCUTS: true, +}; \ No newline at end of file diff --git a/_static/file.png b/_static/file.png new file mode 100644 index 0000000..a858a41 Binary files /dev/null and b/_static/file.png differ diff --git a/_static/jquery.js b/_static/jquery.js new file mode 100644 index 0000000..c4c6022 --- /dev/null +++ b/_static/jquery.js @@ -0,0 +1,2 @@ +/*! jQuery v3.6.0 | (c) OpenJS Foundation and other contributors | jquery.org/license */ +!function(e,t){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error("jQuery requires a window with a document");return t(e)}:t(e)}("undefined"!=typeof window?window:this,function(C,e){"use strict";var t=[],r=Object.getPrototypeOf,s=t.slice,g=t.flat?function(e){return t.flat.call(e)}:function(e){return t.concat.apply([],e)},u=t.push,i=t.indexOf,n={},o=n.toString,v=n.hasOwnProperty,a=v.toString,l=a.call(Object),y={},m=function(e){return"function"==typeof e&&"number"!=typeof e.nodeType&&"function"!=typeof e.item},x=function(e){return null!=e&&e===e.window},E=C.document,c={type:!0,src:!0,nonce:!0,noModule:!0};function b(e,t,n){var r,i,o=(n=n||E).createElement("script");if(o.text=e,t)for(r in c)(i=t[r]||t.getAttribute&&t.getAttribute(r))&&o.setAttribute(r,i);n.head.appendChild(o).parentNode.removeChild(o)}function w(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?n[o.call(e)]||"object":typeof e}var f="3.6.0",S=function(e,t){return new S.fn.init(e,t)};function p(e){var t=!!e&&"length"in e&&e.length,n=w(e);return!m(e)&&!x(e)&&("array"===n||0===t||"number"==typeof t&&0+~]|"+M+")"+M+"*"),U=new RegExp(M+"|>"),X=new RegExp(F),V=new RegExp("^"+I+"$"),G={ID:new RegExp("^#("+I+")"),CLASS:new RegExp("^\\.("+I+")"),TAG:new RegExp("^("+I+"|[*])"),ATTR:new RegExp("^"+W),PSEUDO:new RegExp("^"+F),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+M+"*(even|odd|(([+-]|)(\\d*)n|)"+M+"*(?:([+-]|)"+M+"*(\\d+)|))"+M+"*\\)|)","i"),bool:new RegExp("^(?:"+R+")$","i"),needsContext:new RegExp("^"+M+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+M+"*((?:-\\d)?\\d*)"+M+"*\\)|)(?=[^-]|$)","i")},Y=/HTML$/i,Q=/^(?:input|select|textarea|button)$/i,J=/^h\d$/i,K=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,ee=/[+~]/,te=new RegExp("\\\\[\\da-fA-F]{1,6}"+M+"?|\\\\([^\\r\\n\\f])","g"),ne=function(e,t){var n="0x"+e.slice(1)-65536;return t||(n<0?String.fromCharCode(n+65536):String.fromCharCode(n>>10|55296,1023&n|56320))},re=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ie=function(e,t){return t?"\0"===e?"\ufffd":e.slice(0,-1)+"\\"+e.charCodeAt(e.length-1).toString(16)+" ":"\\"+e},oe=function(){T()},ae=be(function(e){return!0===e.disabled&&"fieldset"===e.nodeName.toLowerCase()},{dir:"parentNode",next:"legend"});try{H.apply(t=O.call(p.childNodes),p.childNodes),t[p.childNodes.length].nodeType}catch(e){H={apply:t.length?function(e,t){L.apply(e,O.call(t))}:function(e,t){var n=e.length,r=0;while(e[n++]=t[r++]);e.length=n-1}}}function se(t,e,n,r){var i,o,a,s,u,l,c,f=e&&e.ownerDocument,p=e?e.nodeType:9;if(n=n||[],"string"!=typeof t||!t||1!==p&&9!==p&&11!==p)return n;if(!r&&(T(e),e=e||C,E)){if(11!==p&&(u=Z.exec(t)))if(i=u[1]){if(9===p){if(!(a=e.getElementById(i)))return n;if(a.id===i)return n.push(a),n}else if(f&&(a=f.getElementById(i))&&y(e,a)&&a.id===i)return n.push(a),n}else{if(u[2])return H.apply(n,e.getElementsByTagName(t)),n;if((i=u[3])&&d.getElementsByClassName&&e.getElementsByClassName)return H.apply(n,e.getElementsByClassName(i)),n}if(d.qsa&&!N[t+" "]&&(!v||!v.test(t))&&(1!==p||"object"!==e.nodeName.toLowerCase())){if(c=t,f=e,1===p&&(U.test(t)||z.test(t))){(f=ee.test(t)&&ye(e.parentNode)||e)===e&&d.scope||((s=e.getAttribute("id"))?s=s.replace(re,ie):e.setAttribute("id",s=S)),o=(l=h(t)).length;while(o--)l[o]=(s?"#"+s:":scope")+" "+xe(l[o]);c=l.join(",")}try{return H.apply(n,f.querySelectorAll(c)),n}catch(e){N(t,!0)}finally{s===S&&e.removeAttribute("id")}}}return g(t.replace($,"$1"),e,n,r)}function ue(){var r=[];return function e(t,n){return r.push(t+" ")>b.cacheLength&&delete e[r.shift()],e[t+" "]=n}}function le(e){return e[S]=!0,e}function ce(e){var t=C.createElement("fieldset");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function fe(e,t){var n=e.split("|"),r=n.length;while(r--)b.attrHandle[n[r]]=t}function pe(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&e.sourceIndex-t.sourceIndex;if(r)return r;if(n)while(n=n.nextSibling)if(n===t)return-1;return e?1:-1}function de(t){return function(e){return"input"===e.nodeName.toLowerCase()&&e.type===t}}function he(n){return function(e){var t=e.nodeName.toLowerCase();return("input"===t||"button"===t)&&e.type===n}}function ge(t){return function(e){return"form"in e?e.parentNode&&!1===e.disabled?"label"in e?"label"in e.parentNode?e.parentNode.disabled===t:e.disabled===t:e.isDisabled===t||e.isDisabled!==!t&&ae(e)===t:e.disabled===t:"label"in e&&e.disabled===t}}function ve(a){return le(function(o){return o=+o,le(function(e,t){var n,r=a([],e.length,o),i=r.length;while(i--)e[n=r[i]]&&(e[n]=!(t[n]=e[n]))})})}function ye(e){return e&&"undefined"!=typeof e.getElementsByTagName&&e}for(e in d=se.support={},i=se.isXML=function(e){var t=e&&e.namespaceURI,n=e&&(e.ownerDocument||e).documentElement;return!Y.test(t||n&&n.nodeName||"HTML")},T=se.setDocument=function(e){var t,n,r=e?e.ownerDocument||e:p;return r!=C&&9===r.nodeType&&r.documentElement&&(a=(C=r).documentElement,E=!i(C),p!=C&&(n=C.defaultView)&&n.top!==n&&(n.addEventListener?n.addEventListener("unload",oe,!1):n.attachEvent&&n.attachEvent("onunload",oe)),d.scope=ce(function(e){return a.appendChild(e).appendChild(C.createElement("div")),"undefined"!=typeof e.querySelectorAll&&!e.querySelectorAll(":scope fieldset div").length}),d.attributes=ce(function(e){return e.className="i",!e.getAttribute("className")}),d.getElementsByTagName=ce(function(e){return e.appendChild(C.createComment("")),!e.getElementsByTagName("*").length}),d.getElementsByClassName=K.test(C.getElementsByClassName),d.getById=ce(function(e){return a.appendChild(e).id=S,!C.getElementsByName||!C.getElementsByName(S).length}),d.getById?(b.filter.ID=function(e){var t=e.replace(te,ne);return function(e){return e.getAttribute("id")===t}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n=t.getElementById(e);return n?[n]:[]}}):(b.filter.ID=function(e){var n=e.replace(te,ne);return function(e){var t="undefined"!=typeof e.getAttributeNode&&e.getAttributeNode("id");return t&&t.value===n}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n,r,i,o=t.getElementById(e);if(o){if((n=o.getAttributeNode("id"))&&n.value===e)return[o];i=t.getElementsByName(e),r=0;while(o=i[r++])if((n=o.getAttributeNode("id"))&&n.value===e)return[o]}return[]}}),b.find.TAG=d.getElementsByTagName?function(e,t){return"undefined"!=typeof t.getElementsByTagName?t.getElementsByTagName(e):d.qsa?t.querySelectorAll(e):void 0}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){while(n=o[i++])1===n.nodeType&&r.push(n);return r}return o},b.find.CLASS=d.getElementsByClassName&&function(e,t){if("undefined"!=typeof t.getElementsByClassName&&E)return t.getElementsByClassName(e)},s=[],v=[],(d.qsa=K.test(C.querySelectorAll))&&(ce(function(e){var t;a.appendChild(e).innerHTML="",e.querySelectorAll("[msallowcapture^='']").length&&v.push("[*^$]="+M+"*(?:''|\"\")"),e.querySelectorAll("[selected]").length||v.push("\\["+M+"*(?:value|"+R+")"),e.querySelectorAll("[id~="+S+"-]").length||v.push("~="),(t=C.createElement("input")).setAttribute("name",""),e.appendChild(t),e.querySelectorAll("[name='']").length||v.push("\\["+M+"*name"+M+"*="+M+"*(?:''|\"\")"),e.querySelectorAll(":checked").length||v.push(":checked"),e.querySelectorAll("a#"+S+"+*").length||v.push(".#.+[+~]"),e.querySelectorAll("\\\f"),v.push("[\\r\\n\\f]")}),ce(function(e){e.innerHTML="";var t=C.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),e.querySelectorAll("[name=d]").length&&v.push("name"+M+"*[*^$|!~]?="),2!==e.querySelectorAll(":enabled").length&&v.push(":enabled",":disabled"),a.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&v.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),v.push(",.*:")})),(d.matchesSelector=K.test(c=a.matches||a.webkitMatchesSelector||a.mozMatchesSelector||a.oMatchesSelector||a.msMatchesSelector))&&ce(function(e){d.disconnectedMatch=c.call(e,"*"),c.call(e,"[s!='']:x"),s.push("!=",F)}),v=v.length&&new RegExp(v.join("|")),s=s.length&&new RegExp(s.join("|")),t=K.test(a.compareDocumentPosition),y=t||K.test(a.contains)?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)while(t=t.parentNode)if(t===e)return!0;return!1},j=t?function(e,t){if(e===t)return l=!0,0;var n=!e.compareDocumentPosition-!t.compareDocumentPosition;return n||(1&(n=(e.ownerDocument||e)==(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!d.sortDetached&&t.compareDocumentPosition(e)===n?e==C||e.ownerDocument==p&&y(p,e)?-1:t==C||t.ownerDocument==p&&y(p,t)?1:u?P(u,e)-P(u,t):0:4&n?-1:1)}:function(e,t){if(e===t)return l=!0,0;var n,r=0,i=e.parentNode,o=t.parentNode,a=[e],s=[t];if(!i||!o)return e==C?-1:t==C?1:i?-1:o?1:u?P(u,e)-P(u,t):0;if(i===o)return pe(e,t);n=e;while(n=n.parentNode)a.unshift(n);n=t;while(n=n.parentNode)s.unshift(n);while(a[r]===s[r])r++;return r?pe(a[r],s[r]):a[r]==p?-1:s[r]==p?1:0}),C},se.matches=function(e,t){return se(e,null,null,t)},se.matchesSelector=function(e,t){if(T(e),d.matchesSelector&&E&&!N[t+" "]&&(!s||!s.test(t))&&(!v||!v.test(t)))try{var n=c.call(e,t);if(n||d.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(e){N(t,!0)}return 0":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(te,ne),e[3]=(e[3]||e[4]||e[5]||"").replace(te,ne),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||se.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&se.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return G.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&X.test(n)&&(t=h(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(te,ne).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=m[e+" "];return t||(t=new RegExp("(^|"+M+")"+e+"("+M+"|$)"))&&m(e,function(e){return t.test("string"==typeof e.className&&e.className||"undefined"!=typeof e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(n,r,i){return function(e){var t=se.attr(e,n);return null==t?"!="===r:!r||(t+="","="===r?t===i:"!="===r?t!==i:"^="===r?i&&0===t.indexOf(i):"*="===r?i&&-1:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function j(e,n,r){return m(n)?S.grep(e,function(e,t){return!!n.call(e,t,e)!==r}):n.nodeType?S.grep(e,function(e){return e===n!==r}):"string"!=typeof n?S.grep(e,function(e){return-1)[^>]*|#([\w-]+))$/;(S.fn.init=function(e,t,n){var r,i;if(!e)return this;if(n=n||D,"string"==typeof e){if(!(r="<"===e[0]&&">"===e[e.length-1]&&3<=e.length?[null,e,null]:q.exec(e))||!r[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(r[1]){if(t=t instanceof S?t[0]:t,S.merge(this,S.parseHTML(r[1],t&&t.nodeType?t.ownerDocument||t:E,!0)),N.test(r[1])&&S.isPlainObject(t))for(r in t)m(this[r])?this[r](t[r]):this.attr(r,t[r]);return this}return(i=E.getElementById(r[2]))&&(this[0]=i,this.length=1),this}return e.nodeType?(this[0]=e,this.length=1,this):m(e)?void 0!==n.ready?n.ready(e):e(S):S.makeArray(e,this)}).prototype=S.fn,D=S(E);var L=/^(?:parents|prev(?:Until|All))/,H={children:!0,contents:!0,next:!0,prev:!0};function O(e,t){while((e=e[t])&&1!==e.nodeType);return e}S.fn.extend({has:function(e){var t=S(e,this),n=t.length;return this.filter(function(){for(var e=0;e\x20\t\r\n\f]*)/i,he=/^$|^module$|\/(?:java|ecma)script/i;ce=E.createDocumentFragment().appendChild(E.createElement("div")),(fe=E.createElement("input")).setAttribute("type","radio"),fe.setAttribute("checked","checked"),fe.setAttribute("name","t"),ce.appendChild(fe),y.checkClone=ce.cloneNode(!0).cloneNode(!0).lastChild.checked,ce.innerHTML="",y.noCloneChecked=!!ce.cloneNode(!0).lastChild.defaultValue,ce.innerHTML="",y.option=!!ce.lastChild;var ge={thead:[1,"","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};function ve(e,t){var n;return n="undefined"!=typeof e.getElementsByTagName?e.getElementsByTagName(t||"*"):"undefined"!=typeof e.querySelectorAll?e.querySelectorAll(t||"*"):[],void 0===t||t&&A(e,t)?S.merge([e],n):n}function ye(e,t){for(var n=0,r=e.length;n",""]);var me=/<|&#?\w+;/;function xe(e,t,n,r,i){for(var o,a,s,u,l,c,f=t.createDocumentFragment(),p=[],d=0,h=e.length;d\s*$/g;function je(e,t){return A(e,"table")&&A(11!==t.nodeType?t:t.firstChild,"tr")&&S(e).children("tbody")[0]||e}function De(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function qe(e){return"true/"===(e.type||"").slice(0,5)?e.type=e.type.slice(5):e.removeAttribute("type"),e}function Le(e,t){var n,r,i,o,a,s;if(1===t.nodeType){if(Y.hasData(e)&&(s=Y.get(e).events))for(i in Y.remove(t,"handle events"),s)for(n=0,r=s[i].length;n").attr(n.scriptAttrs||{}).prop({charset:n.scriptCharset,src:n.url}).on("load error",i=function(e){r.remove(),i=null,e&&t("error"===e.type?404:200,e.type)}),E.head.appendChild(r[0])},abort:function(){i&&i()}}});var _t,zt=[],Ut=/(=)\?(?=&|$)|\?\?/;S.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var e=zt.pop()||S.expando+"_"+wt.guid++;return this[e]=!0,e}}),S.ajaxPrefilter("json jsonp",function(e,t,n){var r,i,o,a=!1!==e.jsonp&&(Ut.test(e.url)?"url":"string"==typeof e.data&&0===(e.contentType||"").indexOf("application/x-www-form-urlencoded")&&Ut.test(e.data)&&"data");if(a||"jsonp"===e.dataTypes[0])return r=e.jsonpCallback=m(e.jsonpCallback)?e.jsonpCallback():e.jsonpCallback,a?e[a]=e[a].replace(Ut,"$1"+r):!1!==e.jsonp&&(e.url+=(Tt.test(e.url)?"&":"?")+e.jsonp+"="+r),e.converters["script json"]=function(){return o||S.error(r+" was not called"),o[0]},e.dataTypes[0]="json",i=C[r],C[r]=function(){o=arguments},n.always(function(){void 0===i?S(C).removeProp(r):C[r]=i,e[r]&&(e.jsonpCallback=t.jsonpCallback,zt.push(r)),o&&m(i)&&i(o[0]),o=i=void 0}),"script"}),y.createHTMLDocument=((_t=E.implementation.createHTMLDocument("").body).innerHTML="
",2===_t.childNodes.length),S.parseHTML=function(e,t,n){return"string"!=typeof e?[]:("boolean"==typeof t&&(n=t,t=!1),t||(y.createHTMLDocument?((r=(t=E.implementation.createHTMLDocument("")).createElement("base")).href=E.location.href,t.head.appendChild(r)):t=E),o=!n&&[],(i=N.exec(e))?[t.createElement(i[1])]:(i=xe([e],t,o),o&&o.length&&S(o).remove(),S.merge([],i.childNodes)));var r,i,o},S.fn.load=function(e,t,n){var r,i,o,a=this,s=e.indexOf(" ");return-1").append(S.parseHTML(e)).find(r):e)}).always(n&&function(e,t){a.each(function(){n.apply(this,o||[e.responseText,t,e])})}),this},S.expr.pseudos.animated=function(t){return S.grep(S.timers,function(e){return t===e.elem}).length},S.offset={setOffset:function(e,t,n){var r,i,o,a,s,u,l=S.css(e,"position"),c=S(e),f={};"static"===l&&(e.style.position="relative"),s=c.offset(),o=S.css(e,"top"),u=S.css(e,"left"),("absolute"===l||"fixed"===l)&&-1<(o+u).indexOf("auto")?(a=(r=c.position()).top,i=r.left):(a=parseFloat(o)||0,i=parseFloat(u)||0),m(t)&&(t=t.call(e,n,S.extend({},s))),null!=t.top&&(f.top=t.top-s.top+a),null!=t.left&&(f.left=t.left-s.left+i),"using"in t?t.using.call(e,f):c.css(f)}},S.fn.extend({offset:function(t){if(arguments.length)return void 0===t?this:this.each(function(e){S.offset.setOffset(this,t,e)});var e,n,r=this[0];return r?r.getClientRects().length?(e=r.getBoundingClientRect(),n=r.ownerDocument.defaultView,{top:e.top+n.pageYOffset,left:e.left+n.pageXOffset}):{top:0,left:0}:void 0},position:function(){if(this[0]){var e,t,n,r=this[0],i={top:0,left:0};if("fixed"===S.css(r,"position"))t=r.getBoundingClientRect();else{t=this.offset(),n=r.ownerDocument,e=r.offsetParent||n.documentElement;while(e&&(e===n.body||e===n.documentElement)&&"static"===S.css(e,"position"))e=e.parentNode;e&&e!==r&&1===e.nodeType&&((i=S(e).offset()).top+=S.css(e,"borderTopWidth",!0),i.left+=S.css(e,"borderLeftWidth",!0))}return{top:t.top-i.top-S.css(r,"marginTop",!0),left:t.left-i.left-S.css(r,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var e=this.offsetParent;while(e&&"static"===S.css(e,"position"))e=e.offsetParent;return e||re})}}),S.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(t,i){var o="pageYOffset"===i;S.fn[t]=function(e){return $(this,function(e,t,n){var r;if(x(e)?r=e:9===e.nodeType&&(r=e.defaultView),void 0===n)return r?r[i]:e[t];r?r.scrollTo(o?r.pageXOffset:n,o?n:r.pageYOffset):e[t]=n},t,e,arguments.length)}}),S.each(["top","left"],function(e,n){S.cssHooks[n]=Fe(y.pixelPosition,function(e,t){if(t)return t=We(e,n),Pe.test(t)?S(e).position()[n]+"px":t})}),S.each({Height:"height",Width:"width"},function(a,s){S.each({padding:"inner"+a,content:s,"":"outer"+a},function(r,o){S.fn[o]=function(e,t){var n=arguments.length&&(r||"boolean"!=typeof e),i=r||(!0===e||!0===t?"margin":"border");return $(this,function(e,t,n){var r;return x(e)?0===o.indexOf("outer")?e["inner"+a]:e.document.documentElement["client"+a]:9===e.nodeType?(r=e.documentElement,Math.max(e.body["scroll"+a],r["scroll"+a],e.body["offset"+a],r["offset"+a],r["client"+a])):void 0===n?S.css(e,t,i):S.style(e,t,n,i)},s,n?e:void 0,n)}})}),S.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(e,t){S.fn[t]=function(e){return this.on(t,e)}}),S.fn.extend({bind:function(e,t,n){return this.on(e,null,t,n)},unbind:function(e,t){return this.off(e,null,t)},delegate:function(e,t,n,r){return this.on(t,e,n,r)},undelegate:function(e,t,n){return 1===arguments.length?this.off(e,"**"):this.off(t,e||"**",n)},hover:function(e,t){return this.mouseenter(e).mouseleave(t||e)}}),S.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(e,n){S.fn[n]=function(e,t){return 0",d.insertBefore(c.lastChild,d.firstChild)}function d(){var a=y.elements;return"string"==typeof a?a.split(" "):a}function e(a,b){var c=y.elements;"string"!=typeof c&&(c=c.join(" ")),"string"!=typeof a&&(a=a.join(" ")),y.elements=c+" "+a,j(b)}function f(a){var b=x[a[v]];return b||(b={},w++,a[v]=w,x[w]=b),b}function g(a,c,d){if(c||(c=b),q)return c.createElement(a);d||(d=f(c));var e;return e=d.cache[a]?d.cache[a].cloneNode():u.test(a)?(d.cache[a]=d.createElem(a)).cloneNode():d.createElem(a),!e.canHaveChildren||t.test(a)||e.tagUrn?e:d.frag.appendChild(e)}function h(a,c){if(a||(a=b),q)return a.createDocumentFragment();c=c||f(a);for(var e=c.frag.cloneNode(),g=0,h=d(),i=h.length;i>g;g++)e.createElement(h[g]);return e}function i(a,b){b.cache||(b.cache={},b.createElem=a.createElement,b.createFrag=a.createDocumentFragment,b.frag=b.createFrag()),a.createElement=function(c){return y.shivMethods?g(c,a,b):b.createElem(c)},a.createDocumentFragment=Function("h,f","return function(){var n=f.cloneNode(),c=n.createElement;h.shivMethods&&("+d().join().replace(/[\w\-:]+/g,function(a){return b.createElem(a),b.frag.createElement(a),'c("'+a+'")'})+");return n}")(y,b.frag)}function j(a){a||(a=b);var d=f(a);return!y.shivCSS||p||d.hasCSS||(d.hasCSS=!!c(a,"article,aside,dialog,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}mark{background:#FF0;color:#000}template{display:none}")),q||i(a,d),a}function k(a){for(var b,c=a.getElementsByTagName("*"),e=c.length,f=RegExp("^(?:"+d().join("|")+")$","i"),g=[];e--;)b=c[e],f.test(b.nodeName)&&g.push(b.applyElement(l(b)));return g}function l(a){for(var b,c=a.attributes,d=c.length,e=a.ownerDocument.createElement(A+":"+a.nodeName);d--;)b=c[d],b.specified&&e.setAttribute(b.nodeName,b.nodeValue);return e.style.cssText=a.style.cssText,e}function m(a){for(var b,c=a.split("{"),e=c.length,f=RegExp("(^|[\\s,>+~])("+d().join("|")+")(?=[[\\s,>+~#.:]|$)","gi"),g="$1"+A+"\\:$2";e--;)b=c[e]=c[e].split("}"),b[b.length-1]=b[b.length-1].replace(f,g),c[e]=b.join("}");return c.join("{")}function n(a){for(var b=a.length;b--;)a[b].removeNode()}function o(a){function b(){clearTimeout(g._removeSheetTimer),d&&d.removeNode(!0),d=null}var d,e,g=f(a),h=a.namespaces,i=a.parentWindow;return!B||a.printShived?a:("undefined"==typeof h[A]&&h.add(A),i.attachEvent("onbeforeprint",function(){b();for(var f,g,h,i=a.styleSheets,j=[],l=i.length,n=Array(l);l--;)n[l]=i[l];for(;h=n.pop();)if(!h.disabled&&z.test(h.media)){try{f=h.imports,g=f.length}catch(o){g=0}for(l=0;g>l;l++)n.push(f[l]);try{j.push(h.cssText)}catch(o){}}j=m(j.reverse().join("")),e=k(a),d=c(a,j)}),i.attachEvent("onafterprint",function(){n(e),clearTimeout(g._removeSheetTimer),g._removeSheetTimer=setTimeout(b,500)}),a.printShived=!0,a)}var p,q,r="3.7.3",s=a.html5||{},t=/^<|^(?:button|map|select|textarea|object|iframe|option|optgroup)$/i,u=/^(?:a|b|code|div|fieldset|h1|h2|h3|h4|h5|h6|i|label|li|ol|p|q|span|strong|style|table|tbody|td|th|tr|ul)$/i,v="_html5shiv",w=0,x={};!function(){try{var a=b.createElement("a");a.innerHTML="",p="hidden"in a,q=1==a.childNodes.length||function(){b.createElement("a");var a=b.createDocumentFragment();return"undefined"==typeof a.cloneNode||"undefined"==typeof a.createDocumentFragment||"undefined"==typeof a.createElement}()}catch(c){p=!0,q=!0}}();var y={elements:s.elements||"abbr article aside audio bdi canvas data datalist details dialog figcaption figure footer header hgroup main mark meter nav output picture progress section summary template time video",version:r,shivCSS:s.shivCSS!==!1,supportsUnknownElements:q,shivMethods:s.shivMethods!==!1,type:"default",shivDocument:j,createElement:g,createDocumentFragment:h,addElements:e};a.html5=y,j(b);var z=/^$|\b(?:all|print)\b/,A="html5shiv",B=!q&&function(){var c=b.documentElement;return!("undefined"==typeof b.namespaces||"undefined"==typeof b.parentWindow||"undefined"==typeof c.applyElement||"undefined"==typeof c.removeNode||"undefined"==typeof a.attachEvent)}();y.type+=" print",y.shivPrint=o,o(b),"object"==typeof module&&module.exports&&(module.exports=y)}("undefined"!=typeof window?window:this,document); \ No newline at end of file diff --git a/_static/js/html5shiv.min.js b/_static/js/html5shiv.min.js new file mode 100644 index 0000000..cd1c674 --- /dev/null +++ b/_static/js/html5shiv.min.js @@ -0,0 +1,4 @@ +/** +* @preserve HTML5 Shiv 3.7.3 | @afarkas @jdalton @jon_neal @rem | MIT/GPL2 Licensed +*/ +!function(a,b){function c(a,b){var c=a.createElement("p"),d=a.getElementsByTagName("head")[0]||a.documentElement;return c.innerHTML="x",d.insertBefore(c.lastChild,d.firstChild)}function d(){var a=t.elements;return"string"==typeof a?a.split(" "):a}function e(a,b){var c=t.elements;"string"!=typeof c&&(c=c.join(" ")),"string"!=typeof a&&(a=a.join(" ")),t.elements=c+" "+a,j(b)}function f(a){var b=s[a[q]];return b||(b={},r++,a[q]=r,s[r]=b),b}function g(a,c,d){if(c||(c=b),l)return c.createElement(a);d||(d=f(c));var e;return e=d.cache[a]?d.cache[a].cloneNode():p.test(a)?(d.cache[a]=d.createElem(a)).cloneNode():d.createElem(a),!e.canHaveChildren||o.test(a)||e.tagUrn?e:d.frag.appendChild(e)}function h(a,c){if(a||(a=b),l)return a.createDocumentFragment();c=c||f(a);for(var e=c.frag.cloneNode(),g=0,h=d(),i=h.length;i>g;g++)e.createElement(h[g]);return e}function i(a,b){b.cache||(b.cache={},b.createElem=a.createElement,b.createFrag=a.createDocumentFragment,b.frag=b.createFrag()),a.createElement=function(c){return t.shivMethods?g(c,a,b):b.createElem(c)},a.createDocumentFragment=Function("h,f","return function(){var n=f.cloneNode(),c=n.createElement;h.shivMethods&&("+d().join().replace(/[\w\-:]+/g,function(a){return b.createElem(a),b.frag.createElement(a),'c("'+a+'")'})+");return n}")(t,b.frag)}function j(a){a||(a=b);var d=f(a);return!t.shivCSS||k||d.hasCSS||(d.hasCSS=!!c(a,"article,aside,dialog,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}mark{background:#FF0;color:#000}template{display:none}")),l||i(a,d),a}var k,l,m="3.7.3-pre",n=a.html5||{},o=/^<|^(?:button|map|select|textarea|object|iframe|option|optgroup)$/i,p=/^(?:a|b|code|div|fieldset|h1|h2|h3|h4|h5|h6|i|label|li|ol|p|q|span|strong|style|table|tbody|td|th|tr|ul)$/i,q="_html5shiv",r=0,s={};!function(){try{var a=b.createElement("a");a.innerHTML="",k="hidden"in a,l=1==a.childNodes.length||function(){b.createElement("a");var a=b.createDocumentFragment();return"undefined"==typeof a.cloneNode||"undefined"==typeof a.createDocumentFragment||"undefined"==typeof a.createElement}()}catch(c){k=!0,l=!0}}();var t={elements:n.elements||"abbr article aside audio bdi canvas data datalist details dialog figcaption figure footer header hgroup main mark meter nav output picture progress section summary template time video",version:m,shivCSS:n.shivCSS!==!1,supportsUnknownElements:l,shivMethods:n.shivMethods!==!1,type:"default",shivDocument:j,createElement:g,createDocumentFragment:h,addElements:e};a.html5=t,j(b),"object"==typeof module&&module.exports&&(module.exports=t)}("undefined"!=typeof window?window:this,document); \ No newline at end of file diff --git a/_static/js/theme.js b/_static/js/theme.js new file mode 100644 index 0000000..1fddb6e --- /dev/null +++ b/_static/js/theme.js @@ -0,0 +1 @@ +!function(n){var e={};function t(i){if(e[i])return e[i].exports;var o=e[i]={i:i,l:!1,exports:{}};return n[i].call(o.exports,o,o.exports,t),o.l=!0,o.exports}t.m=n,t.c=e,t.d=function(n,e,i){t.o(n,e)||Object.defineProperty(n,e,{enumerable:!0,get:i})},t.r=function(n){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(n,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(n,"__esModule",{value:!0})},t.t=function(n,e){if(1&e&&(n=t(n)),8&e)return n;if(4&e&&"object"==typeof n&&n&&n.__esModule)return n;var i=Object.create(null);if(t.r(i),Object.defineProperty(i,"default",{enumerable:!0,value:n}),2&e&&"string"!=typeof n)for(var o in n)t.d(i,o,function(e){return n[e]}.bind(null,o));return i},t.n=function(n){var e=n&&n.__esModule?function(){return n.default}:function(){return n};return t.d(e,"a",e),e},t.o=function(n,e){return Object.prototype.hasOwnProperty.call(n,e)},t.p="",t(t.s=0)}([function(n,e,t){t(1),n.exports=t(3)},function(n,e,t){(function(){var e="undefined"!=typeof window?window.jQuery:t(2);n.exports.ThemeNav={navBar:null,win:null,winScroll:!1,winResize:!1,linkScroll:!1,winPosition:0,winHeight:null,docHeight:null,isRunning:!1,enable:function(n){var t=this;void 0===n&&(n=!0),t.isRunning||(t.isRunning=!0,e((function(e){t.init(e),t.reset(),t.win.on("hashchange",t.reset),n&&t.win.on("scroll",(function(){t.linkScroll||t.winScroll||(t.winScroll=!0,requestAnimationFrame((function(){t.onScroll()})))})),t.win.on("resize",(function(){t.winResize||(t.winResize=!0,requestAnimationFrame((function(){t.onResize()})))})),t.onResize()})))},enableSticky:function(){this.enable(!0)},init:function(n){n(document);var e=this;this.navBar=n("div.wy-side-scroll:first"),this.win=n(window),n(document).on("click","[data-toggle='wy-nav-top']",(function(){n("[data-toggle='wy-nav-shift']").toggleClass("shift"),n("[data-toggle='rst-versions']").toggleClass("shift")})).on("click",".wy-menu-vertical .current ul li a",(function(){var t=n(this);n("[data-toggle='wy-nav-shift']").removeClass("shift"),n("[data-toggle='rst-versions']").toggleClass("shift"),e.toggleCurrent(t),e.hashChange()})).on("click","[data-toggle='rst-current-version']",(function(){n("[data-toggle='rst-versions']").toggleClass("shift-up")})),n("table.docutils:not(.field-list,.footnote,.citation)").wrap("
"),n("table.docutils.footnote").wrap("
"),n("table.docutils.citation").wrap("
"),n(".wy-menu-vertical ul").not(".simple").siblings("a").each((function(){var t=n(this);expand=n(''),expand.on("click",(function(n){return e.toggleCurrent(t),n.stopPropagation(),!1})),t.prepend(expand)}))},reset:function(){var n=encodeURI(window.location.hash)||"#";try{var e=$(".wy-menu-vertical"),t=e.find('[href="'+n+'"]');if(0===t.length){var i=$('.document [id="'+n.substring(1)+'"]').closest("div.section");0===(t=e.find('[href="#'+i.attr("id")+'"]')).length&&(t=e.find('[href="#"]'))}if(t.length>0){$(".wy-menu-vertical .current").removeClass("current").attr("aria-expanded","false"),t.addClass("current").attr("aria-expanded","true"),t.closest("li.toctree-l1").parent().addClass("current").attr("aria-expanded","true");for(let n=1;n<=10;n++)t.closest("li.toctree-l"+n).addClass("current").attr("aria-expanded","true");t[0].scrollIntoView()}}catch(n){console.log("Error expanding nav for anchor",n)}},onScroll:function(){this.winScroll=!1;var n=this.win.scrollTop(),e=n+this.winHeight,t=this.navBar.scrollTop()+(n-this.winPosition);n<0||e>this.docHeight||(this.navBar.scrollTop(t),this.winPosition=n)},onResize:function(){this.winResize=!1,this.winHeight=this.win.height(),this.docHeight=$(document).height()},hashChange:function(){this.linkScroll=!0,this.win.one("hashchange",(function(){this.linkScroll=!1}))},toggleCurrent:function(n){var e=n.closest("li");e.siblings("li.current").removeClass("current").attr("aria-expanded","false"),e.siblings().find("li.current").removeClass("current").attr("aria-expanded","false");var t=e.find("> ul li");t.length&&(t.removeClass("current").attr("aria-expanded","false"),e.toggleClass("current").attr("aria-expanded",(function(n,e){return"true"==e?"false":"true"})))}},"undefined"!=typeof window&&(window.SphinxRtdTheme={Navigation:n.exports.ThemeNav,StickyNav:n.exports.ThemeNav}),function(){for(var n=0,e=["ms","moz","webkit","o"],t=0;t0 + var meq1 = "^(" + C + ")?" + V + C + "(" + V + ")?$"; // [C]VC[V] is m=1 + var mgr1 = "^(" + C + ")?" + V + C + V + C; // [C]VCVC... is m>1 + var s_v = "^(" + C + ")?" + v; // vowel in stem + + this.stemWord = function (w) { + var stem; + var suffix; + var firstch; + var origword = w; + + if (w.length < 3) + return w; + + var re; + var re2; + var re3; + var re4; + + firstch = w.substr(0,1); + if (firstch == "y") + w = firstch.toUpperCase() + w.substr(1); + + // Step 1a + re = /^(.+?)(ss|i)es$/; + re2 = /^(.+?)([^s])s$/; + + if (re.test(w)) + w = w.replace(re,"$1$2"); + else if (re2.test(w)) + w = w.replace(re2,"$1$2"); + + // Step 1b + re = /^(.+?)eed$/; + re2 = /^(.+?)(ed|ing)$/; + if (re.test(w)) { + var fp = re.exec(w); + re = new RegExp(mgr0); + if (re.test(fp[1])) { + re = /.$/; + w = w.replace(re,""); + } + } + else if (re2.test(w)) { + var fp = re2.exec(w); + stem = fp[1]; + re2 = new RegExp(s_v); + if (re2.test(stem)) { + w = stem; + re2 = /(at|bl|iz)$/; + re3 = new RegExp("([^aeiouylsz])\\1$"); + re4 = new RegExp("^" + C + v + "[^aeiouwxy]$"); + if (re2.test(w)) + w = w + "e"; + else if (re3.test(w)) { + re = /.$/; + w = w.replace(re,""); + } + else if (re4.test(w)) + w = w + "e"; + } + } + + // Step 1c + re = /^(.+?)y$/; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + re = new RegExp(s_v); + if (re.test(stem)) + w = stem + "i"; + } + + // Step 2 + re = /^(.+?)(ational|tional|enci|anci|izer|bli|alli|entli|eli|ousli|ization|ation|ator|alism|iveness|fulness|ousness|aliti|iviti|biliti|logi)$/; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + suffix = fp[2]; + re = new RegExp(mgr0); + if (re.test(stem)) + w = stem + step2list[suffix]; + } + + // Step 3 + re = /^(.+?)(icate|ative|alize|iciti|ical|ful|ness)$/; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + suffix = fp[2]; + re = new RegExp(mgr0); + if (re.test(stem)) + w = stem + step3list[suffix]; + } + + // Step 4 + re = /^(.+?)(al|ance|ence|er|ic|able|ible|ant|ement|ment|ent|ou|ism|ate|iti|ous|ive|ize)$/; + re2 = /^(.+?)(s|t)(ion)$/; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + re = new RegExp(mgr1); + if (re.test(stem)) + w = stem; + } + else if (re2.test(w)) { + var fp = re2.exec(w); + stem = fp[1] + fp[2]; + re2 = new RegExp(mgr1); + if (re2.test(stem)) + w = stem; + } + + // Step 5 + re = /^(.+?)e$/; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + re = new RegExp(mgr1); + re2 = new RegExp(meq1); + re3 = new RegExp("^" + C + v + "[^aeiouwxy]$"); + if (re.test(stem) || (re2.test(stem) && !(re3.test(stem)))) + w = stem; + } + re = /ll$/; + re2 = new RegExp(mgr1); + if (re.test(w) && re2.test(w)) { + re = /.$/; + w = w.replace(re,""); + } + + // and turn initial Y back to y + if (firstch == "y") + w = firstch.toLowerCase() + w.substr(1); + return w; + } +} + diff --git a/docs/_static/license_logo.png b/_static/license_logo.png similarity index 100% rename from docs/_static/license_logo.png rename to _static/license_logo.png diff --git a/docs/_static/license_logo.png.license b/_static/license_logo.png.license similarity index 100% rename from docs/_static/license_logo.png.license rename to _static/license_logo.png.license diff --git a/_static/minus.png b/_static/minus.png new file mode 100644 index 0000000..d96755f Binary files /dev/null and b/_static/minus.png differ diff --git a/_static/nc-table-styles.css b/_static/nc-table-styles.css new file mode 100644 index 0000000..99ae771 --- /dev/null +++ b/_static/nc-table-styles.css @@ -0,0 +1,16 @@ +.nc-attr-table { + table-layout: fixed; + width: 100%; +} + +.nc-attr-description { + white-space: break-spaces !important; +} + +.nc-attr-example { + white-space: break-spaces !important; +} + +.nc-attr-example ul.simple { + overflow: auto; +} \ No newline at end of file diff --git a/_static/orcid.png b/_static/orcid.png new file mode 100644 index 0000000..ef10914 Binary files /dev/null and b/_static/orcid.png differ diff --git a/_static/orcid.svg b/_static/orcid.svg new file mode 100644 index 0000000..6bd8373 --- /dev/null +++ b/_static/orcid.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/_static/plot_directive.css b/_static/plot_directive.css new file mode 100644 index 0000000..d45593c --- /dev/null +++ b/_static/plot_directive.css @@ -0,0 +1,16 @@ +/* + * plot_directive.css + * ~~~~~~~~~~~~ + * + * Stylesheet controlling images created using the `plot` directive within + * Sphinx. + * + * :copyright: Copyright 2020-* by the Matplotlib development team. + * :license: Matplotlib, see LICENSE for details. + * + */ + +img.plot-directive { + border: 0; + max-width: 100%; +} diff --git a/_static/plus.png b/_static/plus.png new file mode 100644 index 0000000..7107cec Binary files /dev/null and b/_static/plus.png differ diff --git a/docs/_static/psyplot.ico b/_static/psyplot.ico similarity index 100% rename from docs/_static/psyplot.ico rename to _static/psyplot.ico diff --git a/docs/_static/psyplot.ico.license b/_static/psyplot.ico.license similarity index 100% rename from docs/_static/psyplot.ico.license rename to _static/psyplot.ico.license diff --git a/icon/icon1024.png b/_static/psyplot.png similarity index 100% rename from icon/icon1024.png rename to _static/psyplot.png diff --git a/docs/_static/psyplot.png.license b/_static/psyplot.png.license similarity index 100% rename from docs/_static/psyplot.png.license rename to _static/psyplot.png.license diff --git a/_static/pygments.css b/_static/pygments.css new file mode 100644 index 0000000..84ab303 --- /dev/null +++ b/_static/pygments.css @@ -0,0 +1,75 @@ +pre { line-height: 125%; } +td.linenos .normal { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; } +span.linenos { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; } +td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } +span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } +.highlight .hll { background-color: #ffffcc } +.highlight { background: #f8f8f8; } +.highlight .c { color: #3D7B7B; font-style: italic } /* Comment */ +.highlight .err { border: 1px solid #FF0000 } /* Error */ +.highlight .k { color: #008000; font-weight: bold } /* Keyword */ +.highlight .o { color: #666666 } /* Operator */ +.highlight .ch { color: #3D7B7B; font-style: italic } /* Comment.Hashbang */ +.highlight .cm { color: #3D7B7B; font-style: italic } /* Comment.Multiline */ +.highlight .cp { color: #9C6500 } /* Comment.Preproc */ +.highlight .cpf { color: #3D7B7B; font-style: italic } /* Comment.PreprocFile */ +.highlight .c1 { color: #3D7B7B; font-style: italic } /* Comment.Single */ +.highlight .cs { color: #3D7B7B; font-style: italic } /* Comment.Special */ +.highlight .gd { color: #A00000 } /* Generic.Deleted */ +.highlight .ge { font-style: italic } /* Generic.Emph */ +.highlight .ges { font-weight: bold; font-style: italic } /* Generic.EmphStrong */ +.highlight .gr { color: #E40000 } /* Generic.Error */ +.highlight .gh { color: #000080; font-weight: bold } /* Generic.Heading */ +.highlight .gi { color: #008400 } /* Generic.Inserted */ +.highlight .go { color: #717171 } /* Generic.Output */ +.highlight .gp { color: #000080; font-weight: bold } /* Generic.Prompt */ +.highlight .gs { font-weight: bold } /* Generic.Strong */ +.highlight .gu { color: #800080; font-weight: bold } /* Generic.Subheading */ +.highlight .gt { color: #0044DD } /* Generic.Traceback */ +.highlight .kc { color: #008000; font-weight: bold } /* Keyword.Constant */ +.highlight .kd { color: #008000; font-weight: bold } /* Keyword.Declaration */ +.highlight .kn { color: #008000; font-weight: bold } /* Keyword.Namespace */ +.highlight .kp { color: #008000 } /* Keyword.Pseudo */ +.highlight .kr { color: #008000; font-weight: bold } /* Keyword.Reserved */ +.highlight .kt { color: #B00040 } /* Keyword.Type */ +.highlight .m { color: #666666 } /* Literal.Number */ +.highlight .s { color: #BA2121 } /* Literal.String */ +.highlight .na { color: #687822 } /* Name.Attribute */ +.highlight .nb { color: #008000 } /* Name.Builtin */ +.highlight .nc { color: #0000FF; font-weight: bold } /* Name.Class */ +.highlight .no { color: #880000 } /* Name.Constant */ +.highlight .nd { color: #AA22FF } /* Name.Decorator */ +.highlight .ni { color: #717171; font-weight: bold } /* Name.Entity */ +.highlight .ne { color: #CB3F38; font-weight: bold } /* Name.Exception */ +.highlight .nf { color: #0000FF } /* Name.Function */ +.highlight .nl { color: #767600 } /* Name.Label */ +.highlight .nn { color: #0000FF; font-weight: bold } /* Name.Namespace */ +.highlight .nt { color: #008000; font-weight: bold } /* Name.Tag */ +.highlight .nv { color: #19177C } /* Name.Variable */ +.highlight .ow { color: #AA22FF; font-weight: bold } /* Operator.Word */ +.highlight .w { color: #bbbbbb } /* Text.Whitespace */ +.highlight .mb { color: #666666 } /* Literal.Number.Bin */ +.highlight .mf { color: #666666 } /* Literal.Number.Float */ +.highlight .mh { color: #666666 } /* Literal.Number.Hex */ +.highlight .mi { color: #666666 } /* Literal.Number.Integer */ +.highlight .mo { color: #666666 } /* Literal.Number.Oct */ +.highlight .sa { color: #BA2121 } /* Literal.String.Affix */ +.highlight .sb { color: #BA2121 } /* Literal.String.Backtick */ +.highlight .sc { color: #BA2121 } /* Literal.String.Char */ +.highlight .dl { color: #BA2121 } /* Literal.String.Delimiter */ +.highlight .sd { color: #BA2121; font-style: italic } /* Literal.String.Doc */ +.highlight .s2 { color: #BA2121 } /* Literal.String.Double */ +.highlight .se { color: #AA5D1F; font-weight: bold } /* Literal.String.Escape */ +.highlight .sh { color: #BA2121 } /* Literal.String.Heredoc */ +.highlight .si { color: #A45A77; font-weight: bold } /* Literal.String.Interpol */ +.highlight .sx { color: #008000 } /* Literal.String.Other */ +.highlight .sr { color: #A45A77 } /* Literal.String.Regex */ +.highlight .s1 { color: #BA2121 } /* Literal.String.Single */ +.highlight .ss { color: #19177C } /* Literal.String.Symbol */ +.highlight .bp { color: #008000 } /* Name.Builtin.Pseudo */ +.highlight .fm { color: #0000FF } /* Name.Function.Magic */ +.highlight .vc { color: #19177C } /* Name.Variable.Class */ +.highlight .vg { color: #19177C } /* Name.Variable.Global */ +.highlight .vi { color: #19177C } /* Name.Variable.Instance */ +.highlight .vm { color: #19177C } /* Name.Variable.Magic */ +.highlight .il { color: #666666 } /* Literal.Number.Integer.Long */ \ No newline at end of file diff --git a/_static/searchtools.js b/_static/searchtools.js new file mode 100644 index 0000000..b08d58c --- /dev/null +++ b/_static/searchtools.js @@ -0,0 +1,620 @@ +/* + * searchtools.js + * ~~~~~~~~~~~~~~~~ + * + * Sphinx JavaScript utilities for the full-text search. + * + * :copyright: Copyright 2007-2024 by the Sphinx team, see AUTHORS. + * :license: BSD, see LICENSE for details. + * + */ +"use strict"; + +/** + * Simple result scoring code. + */ +if (typeof Scorer === "undefined") { + var Scorer = { + // Implement the following function to further tweak the score for each result + // The function takes a result array [docname, title, anchor, descr, score, filename] + // and returns the new score. + /* + score: result => { + const [docname, title, anchor, descr, score, filename] = result + return score + }, + */ + + // query matches the full name of an object + objNameMatch: 11, + // or matches in the last dotted part of the object name + objPartialMatch: 6, + // Additive scores depending on the priority of the object + objPrio: { + 0: 15, // used to be importantResults + 1: 5, // used to be objectResults + 2: -5, // used to be unimportantResults + }, + // Used when the priority is not in the mapping. + objPrioDefault: 0, + + // query found in title + title: 15, + partialTitle: 7, + // query found in terms + term: 5, + partialTerm: 2, + }; +} + +const _removeChildren = (element) => { + while (element && element.lastChild) element.removeChild(element.lastChild); +}; + +/** + * See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#escaping + */ +const _escapeRegExp = (string) => + string.replace(/[.*+\-?^${}()|[\]\\]/g, "\\$&"); // $& means the whole matched string + +const _displayItem = (item, searchTerms, highlightTerms) => { + const docBuilder = DOCUMENTATION_OPTIONS.BUILDER; + const docFileSuffix = DOCUMENTATION_OPTIONS.FILE_SUFFIX; + const docLinkSuffix = DOCUMENTATION_OPTIONS.LINK_SUFFIX; + const showSearchSummary = DOCUMENTATION_OPTIONS.SHOW_SEARCH_SUMMARY; + const contentRoot = document.documentElement.dataset.content_root; + + const [docName, title, anchor, descr, score, _filename] = item; + + let listItem = document.createElement("li"); + let requestUrl; + let linkUrl; + if (docBuilder === "dirhtml") { + // dirhtml builder + let dirname = docName + "/"; + if (dirname.match(/\/index\/$/)) + dirname = dirname.substring(0, dirname.length - 6); + else if (dirname === "index/") dirname = ""; + requestUrl = contentRoot + dirname; + linkUrl = requestUrl; + } else { + // normal html builders + requestUrl = contentRoot + docName + docFileSuffix; + linkUrl = docName + docLinkSuffix; + } + let linkEl = listItem.appendChild(document.createElement("a")); + linkEl.href = linkUrl + anchor; + linkEl.dataset.score = score; + linkEl.innerHTML = title; + if (descr) { + listItem.appendChild(document.createElement("span")).innerHTML = + " (" + descr + ")"; + // highlight search terms in the description + if (SPHINX_HIGHLIGHT_ENABLED) // set in sphinx_highlight.js + highlightTerms.forEach((term) => _highlightText(listItem, term, "highlighted")); + } + else if (showSearchSummary) + fetch(requestUrl) + .then((responseData) => responseData.text()) + .then((data) => { + if (data) + listItem.appendChild( + Search.makeSearchSummary(data, searchTerms, anchor) + ); + // highlight search terms in the summary + if (SPHINX_HIGHLIGHT_ENABLED) // set in sphinx_highlight.js + highlightTerms.forEach((term) => _highlightText(listItem, term, "highlighted")); + }); + Search.output.appendChild(listItem); +}; +const _finishSearch = (resultCount) => { + Search.stopPulse(); + Search.title.innerText = _("Search Results"); + if (!resultCount) + Search.status.innerText = Documentation.gettext( + "Your search did not match any documents. Please make sure that all words are spelled correctly and that you've selected enough categories." + ); + else + Search.status.innerText = _( + "Search finished, found ${resultCount} page(s) matching the search query." + ).replace('${resultCount}', resultCount); +}; +const _displayNextItem = ( + results, + resultCount, + searchTerms, + highlightTerms, +) => { + // results left, load the summary and display it + // this is intended to be dynamic (don't sub resultsCount) + if (results.length) { + _displayItem(results.pop(), searchTerms, highlightTerms); + setTimeout( + () => _displayNextItem(results, resultCount, searchTerms, highlightTerms), + 5 + ); + } + // search finished, update title and status message + else _finishSearch(resultCount); +}; +// Helper function used by query() to order search results. +// Each input is an array of [docname, title, anchor, descr, score, filename]. +// Order the results by score (in opposite order of appearance, since the +// `_displayNextItem` function uses pop() to retrieve items) and then alphabetically. +const _orderResultsByScoreThenName = (a, b) => { + const leftScore = a[4]; + const rightScore = b[4]; + if (leftScore === rightScore) { + // same score: sort alphabetically + const leftTitle = a[1].toLowerCase(); + const rightTitle = b[1].toLowerCase(); + if (leftTitle === rightTitle) return 0; + return leftTitle > rightTitle ? -1 : 1; // inverted is intentional + } + return leftScore > rightScore ? 1 : -1; +}; + +/** + * Default splitQuery function. Can be overridden in ``sphinx.search`` with a + * custom function per language. + * + * The regular expression works by splitting the string on consecutive characters + * that are not Unicode letters, numbers, underscores, or emoji characters. + * This is the same as ``\W+`` in Python, preserving the surrogate pair area. + */ +if (typeof splitQuery === "undefined") { + var splitQuery = (query) => query + .split(/[^\p{Letter}\p{Number}_\p{Emoji_Presentation}]+/gu) + .filter(term => term) // remove remaining empty strings +} + +/** + * Search Module + */ +const Search = { + _index: null, + _queued_query: null, + _pulse_status: -1, + + htmlToText: (htmlString, anchor) => { + const htmlElement = new DOMParser().parseFromString(htmlString, 'text/html'); + for (const removalQuery of [".headerlink", "script", "style"]) { + htmlElement.querySelectorAll(removalQuery).forEach((el) => { el.remove() }); + } + if (anchor) { + const anchorContent = htmlElement.querySelector(`[role="main"] ${anchor}`); + if (anchorContent) return anchorContent.textContent; + + console.warn( + `Anchored content block not found. Sphinx search tries to obtain it via DOM query '[role=main] ${anchor}'. Check your theme or template.` + ); + } + + // if anchor not specified or not found, fall back to main content + const docContent = htmlElement.querySelector('[role="main"]'); + if (docContent) return docContent.textContent; + + console.warn( + "Content block not found. Sphinx search tries to obtain it via DOM query '[role=main]'. Check your theme or template." + ); + return ""; + }, + + init: () => { + const query = new URLSearchParams(window.location.search).get("q"); + document + .querySelectorAll('input[name="q"]') + .forEach((el) => (el.value = query)); + if (query) Search.performSearch(query); + }, + + loadIndex: (url) => + (document.body.appendChild(document.createElement("script")).src = url), + + setIndex: (index) => { + Search._index = index; + if (Search._queued_query !== null) { + const query = Search._queued_query; + Search._queued_query = null; + Search.query(query); + } + }, + + hasIndex: () => Search._index !== null, + + deferQuery: (query) => (Search._queued_query = query), + + stopPulse: () => (Search._pulse_status = -1), + + startPulse: () => { + if (Search._pulse_status >= 0) return; + + const pulse = () => { + Search._pulse_status = (Search._pulse_status + 1) % 4; + Search.dots.innerText = ".".repeat(Search._pulse_status); + if (Search._pulse_status >= 0) window.setTimeout(pulse, 500); + }; + pulse(); + }, + + /** + * perform a search for something (or wait until index is loaded) + */ + performSearch: (query) => { + // create the required interface elements + const searchText = document.createElement("h2"); + searchText.textContent = _("Searching"); + const searchSummary = document.createElement("p"); + searchSummary.classList.add("search-summary"); + searchSummary.innerText = ""; + const searchList = document.createElement("ul"); + searchList.classList.add("search"); + + const out = document.getElementById("search-results"); + Search.title = out.appendChild(searchText); + Search.dots = Search.title.appendChild(document.createElement("span")); + Search.status = out.appendChild(searchSummary); + Search.output = out.appendChild(searchList); + + const searchProgress = document.getElementById("search-progress"); + // Some themes don't use the search progress node + if (searchProgress) { + searchProgress.innerText = _("Preparing search..."); + } + Search.startPulse(); + + // index already loaded, the browser was quick! + if (Search.hasIndex()) Search.query(query); + else Search.deferQuery(query); + }, + + _parseQuery: (query) => { + // stem the search terms and add them to the correct list + const stemmer = new Stemmer(); + const searchTerms = new Set(); + const excludedTerms = new Set(); + const highlightTerms = new Set(); + const objectTerms = new Set(splitQuery(query.toLowerCase().trim())); + splitQuery(query.trim()).forEach((queryTerm) => { + const queryTermLower = queryTerm.toLowerCase(); + + // maybe skip this "word" + // stopwords array is from language_data.js + if ( + stopwords.indexOf(queryTermLower) !== -1 || + queryTerm.match(/^\d+$/) + ) + return; + + // stem the word + let word = stemmer.stemWord(queryTermLower); + // select the correct list + if (word[0] === "-") excludedTerms.add(word.substr(1)); + else { + searchTerms.add(word); + highlightTerms.add(queryTermLower); + } + }); + + if (SPHINX_HIGHLIGHT_ENABLED) { // set in sphinx_highlight.js + localStorage.setItem("sphinx_highlight_terms", [...highlightTerms].join(" ")) + } + + // console.debug("SEARCH: searching for:"); + // console.info("required: ", [...searchTerms]); + // console.info("excluded: ", [...excludedTerms]); + + return [query, searchTerms, excludedTerms, highlightTerms, objectTerms]; + }, + + /** + * execute search (requires search index to be loaded) + */ + _performSearch: (query, searchTerms, excludedTerms, highlightTerms, objectTerms) => { + const filenames = Search._index.filenames; + const docNames = Search._index.docnames; + const titles = Search._index.titles; + const allTitles = Search._index.alltitles; + const indexEntries = Search._index.indexentries; + + // Collect multiple result groups to be sorted separately and then ordered. + // Each is an array of [docname, title, anchor, descr, score, filename]. + const normalResults = []; + const nonMainIndexResults = []; + + _removeChildren(document.getElementById("search-progress")); + + const queryLower = query.toLowerCase().trim(); + for (const [title, foundTitles] of Object.entries(allTitles)) { + if (title.toLowerCase().trim().includes(queryLower) && (queryLower.length >= title.length/2)) { + for (const [file, id] of foundTitles) { + const score = Math.round(Scorer.title * queryLower.length / title.length); + const boost = titles[file] === title ? 1 : 0; // add a boost for document titles + normalResults.push([ + docNames[file], + titles[file] !== title ? `${titles[file]} > ${title}` : title, + id !== null ? "#" + id : "", + null, + score + boost, + filenames[file], + ]); + } + } + } + + // search for explicit entries in index directives + for (const [entry, foundEntries] of Object.entries(indexEntries)) { + if (entry.includes(queryLower) && (queryLower.length >= entry.length/2)) { + for (const [file, id, isMain] of foundEntries) { + const score = Math.round(100 * queryLower.length / entry.length); + const result = [ + docNames[file], + titles[file], + id ? "#" + id : "", + null, + score, + filenames[file], + ]; + if (isMain) { + normalResults.push(result); + } else { + nonMainIndexResults.push(result); + } + } + } + } + + // lookup as object + objectTerms.forEach((term) => + normalResults.push(...Search.performObjectSearch(term, objectTerms)) + ); + + // lookup as search terms in fulltext + normalResults.push(...Search.performTermsSearch(searchTerms, excludedTerms)); + + // let the scorer override scores with a custom scoring function + if (Scorer.score) { + normalResults.forEach((item) => (item[4] = Scorer.score(item))); + nonMainIndexResults.forEach((item) => (item[4] = Scorer.score(item))); + } + + // Sort each group of results by score and then alphabetically by name. + normalResults.sort(_orderResultsByScoreThenName); + nonMainIndexResults.sort(_orderResultsByScoreThenName); + + // Combine the result groups in (reverse) order. + // Non-main index entries are typically arbitrary cross-references, + // so display them after other results. + let results = [...nonMainIndexResults, ...normalResults]; + + // remove duplicate search results + // note the reversing of results, so that in the case of duplicates, the highest-scoring entry is kept + let seen = new Set(); + results = results.reverse().reduce((acc, result) => { + let resultStr = result.slice(0, 4).concat([result[5]]).map(v => String(v)).join(','); + if (!seen.has(resultStr)) { + acc.push(result); + seen.add(resultStr); + } + return acc; + }, []); + + return results.reverse(); + }, + + query: (query) => { + const [searchQuery, searchTerms, excludedTerms, highlightTerms, objectTerms] = Search._parseQuery(query); + const results = Search._performSearch(searchQuery, searchTerms, excludedTerms, highlightTerms, objectTerms); + + // for debugging + //Search.lastresults = results.slice(); // a copy + // console.info("search results:", Search.lastresults); + + // print the results + _displayNextItem(results, results.length, searchTerms, highlightTerms); + }, + + /** + * search for object names + */ + performObjectSearch: (object, objectTerms) => { + const filenames = Search._index.filenames; + const docNames = Search._index.docnames; + const objects = Search._index.objects; + const objNames = Search._index.objnames; + const titles = Search._index.titles; + + const results = []; + + const objectSearchCallback = (prefix, match) => { + const name = match[4] + const fullname = (prefix ? prefix + "." : "") + name; + const fullnameLower = fullname.toLowerCase(); + if (fullnameLower.indexOf(object) < 0) return; + + let score = 0; + const parts = fullnameLower.split("."); + + // check for different match types: exact matches of full name or + // "last name" (i.e. last dotted part) + if (fullnameLower === object || parts.slice(-1)[0] === object) + score += Scorer.objNameMatch; + else if (parts.slice(-1)[0].indexOf(object) > -1) + score += Scorer.objPartialMatch; // matches in last name + + const objName = objNames[match[1]][2]; + const title = titles[match[0]]; + + // If more than one term searched for, we require other words to be + // found in the name/title/description + const otherTerms = new Set(objectTerms); + otherTerms.delete(object); + if (otherTerms.size > 0) { + const haystack = `${prefix} ${name} ${objName} ${title}`.toLowerCase(); + if ( + [...otherTerms].some((otherTerm) => haystack.indexOf(otherTerm) < 0) + ) + return; + } + + let anchor = match[3]; + if (anchor === "") anchor = fullname; + else if (anchor === "-") anchor = objNames[match[1]][1] + "-" + fullname; + + const descr = objName + _(", in ") + title; + + // add custom score for some objects according to scorer + if (Scorer.objPrio.hasOwnProperty(match[2])) + score += Scorer.objPrio[match[2]]; + else score += Scorer.objPrioDefault; + + results.push([ + docNames[match[0]], + fullname, + "#" + anchor, + descr, + score, + filenames[match[0]], + ]); + }; + Object.keys(objects).forEach((prefix) => + objects[prefix].forEach((array) => + objectSearchCallback(prefix, array) + ) + ); + return results; + }, + + /** + * search for full-text terms in the index + */ + performTermsSearch: (searchTerms, excludedTerms) => { + // prepare search + const terms = Search._index.terms; + const titleTerms = Search._index.titleterms; + const filenames = Search._index.filenames; + const docNames = Search._index.docnames; + const titles = Search._index.titles; + + const scoreMap = new Map(); + const fileMap = new Map(); + + // perform the search on the required terms + searchTerms.forEach((word) => { + const files = []; + const arr = [ + { files: terms[word], score: Scorer.term }, + { files: titleTerms[word], score: Scorer.title }, + ]; + // add support for partial matches + if (word.length > 2) { + const escapedWord = _escapeRegExp(word); + if (!terms.hasOwnProperty(word)) { + Object.keys(terms).forEach((term) => { + if (term.match(escapedWord)) + arr.push({ files: terms[term], score: Scorer.partialTerm }); + }); + } + if (!titleTerms.hasOwnProperty(word)) { + Object.keys(titleTerms).forEach((term) => { + if (term.match(escapedWord)) + arr.push({ files: titleTerms[term], score: Scorer.partialTitle }); + }); + } + } + + // no match but word was a required one + if (arr.every((record) => record.files === undefined)) return; + + // found search word in contents + arr.forEach((record) => { + if (record.files === undefined) return; + + let recordFiles = record.files; + if (recordFiles.length === undefined) recordFiles = [recordFiles]; + files.push(...recordFiles); + + // set score for the word in each file + recordFiles.forEach((file) => { + if (!scoreMap.has(file)) scoreMap.set(file, {}); + scoreMap.get(file)[word] = record.score; + }); + }); + + // create the mapping + files.forEach((file) => { + if (!fileMap.has(file)) fileMap.set(file, [word]); + else if (fileMap.get(file).indexOf(word) === -1) fileMap.get(file).push(word); + }); + }); + + // now check if the files don't contain excluded terms + const results = []; + for (const [file, wordList] of fileMap) { + // check if all requirements are matched + + // as search terms with length < 3 are discarded + const filteredTermCount = [...searchTerms].filter( + (term) => term.length > 2 + ).length; + if ( + wordList.length !== searchTerms.size && + wordList.length !== filteredTermCount + ) + continue; + + // ensure that none of the excluded terms is in the search result + if ( + [...excludedTerms].some( + (term) => + terms[term] === file || + titleTerms[term] === file || + (terms[term] || []).includes(file) || + (titleTerms[term] || []).includes(file) + ) + ) + break; + + // select one (max) score for the file. + const score = Math.max(...wordList.map((w) => scoreMap.get(file)[w])); + // add result to the result list + results.push([ + docNames[file], + titles[file], + "", + null, + score, + filenames[file], + ]); + } + return results; + }, + + /** + * helper function to return a node containing the + * search summary for a given text. keywords is a list + * of stemmed words. + */ + makeSearchSummary: (htmlText, keywords, anchor) => { + const text = Search.htmlToText(htmlText, anchor); + if (text === "") return null; + + const textLower = text.toLowerCase(); + const actualStartPosition = [...keywords] + .map((k) => textLower.indexOf(k.toLowerCase())) + .filter((i) => i > -1) + .slice(-1)[0]; + const startWithContext = Math.max(actualStartPosition - 120, 0); + + const top = startWithContext === 0 ? "" : "..."; + const tail = startWithContext + 240 < text.length ? "..." : ""; + + let summary = document.createElement("p"); + summary.classList.add("context"); + summary.textContent = top + text.substr(startWithContext, 240).trim() + tail; + + return summary; + }, +}; + +_ready(Search.init); diff --git a/_static/sphinx-design.min.css b/_static/sphinx-design.min.css new file mode 100644 index 0000000..a325746 --- /dev/null +++ b/_static/sphinx-design.min.css @@ -0,0 +1 @@ +.sd-bg-primary{background-color:var(--sd-color-primary) !important}.sd-bg-text-primary{color:var(--sd-color-primary-text) !important}button.sd-bg-primary:focus,button.sd-bg-primary:hover{background-color:var(--sd-color-primary-highlight) !important}a.sd-bg-primary:focus,a.sd-bg-primary:hover{background-color:var(--sd-color-primary-highlight) !important}.sd-bg-secondary{background-color:var(--sd-color-secondary) !important}.sd-bg-text-secondary{color:var(--sd-color-secondary-text) !important}button.sd-bg-secondary:focus,button.sd-bg-secondary:hover{background-color:var(--sd-color-secondary-highlight) !important}a.sd-bg-secondary:focus,a.sd-bg-secondary:hover{background-color:var(--sd-color-secondary-highlight) !important}.sd-bg-success{background-color:var(--sd-color-success) !important}.sd-bg-text-success{color:var(--sd-color-success-text) !important}button.sd-bg-success:focus,button.sd-bg-success:hover{background-color:var(--sd-color-success-highlight) !important}a.sd-bg-success:focus,a.sd-bg-success:hover{background-color:var(--sd-color-success-highlight) !important}.sd-bg-info{background-color:var(--sd-color-info) !important}.sd-bg-text-info{color:var(--sd-color-info-text) !important}button.sd-bg-info:focus,button.sd-bg-info:hover{background-color:var(--sd-color-info-highlight) !important}a.sd-bg-info:focus,a.sd-bg-info:hover{background-color:var(--sd-color-info-highlight) !important}.sd-bg-warning{background-color:var(--sd-color-warning) !important}.sd-bg-text-warning{color:var(--sd-color-warning-text) !important}button.sd-bg-warning:focus,button.sd-bg-warning:hover{background-color:var(--sd-color-warning-highlight) !important}a.sd-bg-warning:focus,a.sd-bg-warning:hover{background-color:var(--sd-color-warning-highlight) !important}.sd-bg-danger{background-color:var(--sd-color-danger) !important}.sd-bg-text-danger{color:var(--sd-color-danger-text) !important}button.sd-bg-danger:focus,button.sd-bg-danger:hover{background-color:var(--sd-color-danger-highlight) !important}a.sd-bg-danger:focus,a.sd-bg-danger:hover{background-color:var(--sd-color-danger-highlight) !important}.sd-bg-light{background-color:var(--sd-color-light) !important}.sd-bg-text-light{color:var(--sd-color-light-text) !important}button.sd-bg-light:focus,button.sd-bg-light:hover{background-color:var(--sd-color-light-highlight) !important}a.sd-bg-light:focus,a.sd-bg-light:hover{background-color:var(--sd-color-light-highlight) !important}.sd-bg-muted{background-color:var(--sd-color-muted) !important}.sd-bg-text-muted{color:var(--sd-color-muted-text) !important}button.sd-bg-muted:focus,button.sd-bg-muted:hover{background-color:var(--sd-color-muted-highlight) !important}a.sd-bg-muted:focus,a.sd-bg-muted:hover{background-color:var(--sd-color-muted-highlight) !important}.sd-bg-dark{background-color:var(--sd-color-dark) !important}.sd-bg-text-dark{color:var(--sd-color-dark-text) !important}button.sd-bg-dark:focus,button.sd-bg-dark:hover{background-color:var(--sd-color-dark-highlight) !important}a.sd-bg-dark:focus,a.sd-bg-dark:hover{background-color:var(--sd-color-dark-highlight) !important}.sd-bg-black{background-color:var(--sd-color-black) !important}.sd-bg-text-black{color:var(--sd-color-black-text) !important}button.sd-bg-black:focus,button.sd-bg-black:hover{background-color:var(--sd-color-black-highlight) !important}a.sd-bg-black:focus,a.sd-bg-black:hover{background-color:var(--sd-color-black-highlight) !important}.sd-bg-white{background-color:var(--sd-color-white) !important}.sd-bg-text-white{color:var(--sd-color-white-text) !important}button.sd-bg-white:focus,button.sd-bg-white:hover{background-color:var(--sd-color-white-highlight) !important}a.sd-bg-white:focus,a.sd-bg-white:hover{background-color:var(--sd-color-white-highlight) !important}.sd-text-primary,.sd-text-primary>p{color:var(--sd-color-primary) !important}a.sd-text-primary:focus,a.sd-text-primary:hover{color:var(--sd-color-primary-highlight) !important}.sd-text-secondary,.sd-text-secondary>p{color:var(--sd-color-secondary) !important}a.sd-text-secondary:focus,a.sd-text-secondary:hover{color:var(--sd-color-secondary-highlight) !important}.sd-text-success,.sd-text-success>p{color:var(--sd-color-success) !important}a.sd-text-success:focus,a.sd-text-success:hover{color:var(--sd-color-success-highlight) !important}.sd-text-info,.sd-text-info>p{color:var(--sd-color-info) !important}a.sd-text-info:focus,a.sd-text-info:hover{color:var(--sd-color-info-highlight) !important}.sd-text-warning,.sd-text-warning>p{color:var(--sd-color-warning) !important}a.sd-text-warning:focus,a.sd-text-warning:hover{color:var(--sd-color-warning-highlight) !important}.sd-text-danger,.sd-text-danger>p{color:var(--sd-color-danger) !important}a.sd-text-danger:focus,a.sd-text-danger:hover{color:var(--sd-color-danger-highlight) !important}.sd-text-light,.sd-text-light>p{color:var(--sd-color-light) !important}a.sd-text-light:focus,a.sd-text-light:hover{color:var(--sd-color-light-highlight) !important}.sd-text-muted,.sd-text-muted>p{color:var(--sd-color-muted) !important}a.sd-text-muted:focus,a.sd-text-muted:hover{color:var(--sd-color-muted-highlight) !important}.sd-text-dark,.sd-text-dark>p{color:var(--sd-color-dark) !important}a.sd-text-dark:focus,a.sd-text-dark:hover{color:var(--sd-color-dark-highlight) !important}.sd-text-black,.sd-text-black>p{color:var(--sd-color-black) !important}a.sd-text-black:focus,a.sd-text-black:hover{color:var(--sd-color-black-highlight) !important}.sd-text-white,.sd-text-white>p{color:var(--sd-color-white) !important}a.sd-text-white:focus,a.sd-text-white:hover{color:var(--sd-color-white-highlight) !important}.sd-outline-primary{border-color:var(--sd-color-primary) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-primary:focus,a.sd-outline-primary:hover{border-color:var(--sd-color-primary-highlight) !important}.sd-outline-secondary{border-color:var(--sd-color-secondary) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-secondary:focus,a.sd-outline-secondary:hover{border-color:var(--sd-color-secondary-highlight) !important}.sd-outline-success{border-color:var(--sd-color-success) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-success:focus,a.sd-outline-success:hover{border-color:var(--sd-color-success-highlight) !important}.sd-outline-info{border-color:var(--sd-color-info) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-info:focus,a.sd-outline-info:hover{border-color:var(--sd-color-info-highlight) !important}.sd-outline-warning{border-color:var(--sd-color-warning) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-warning:focus,a.sd-outline-warning:hover{border-color:var(--sd-color-warning-highlight) !important}.sd-outline-danger{border-color:var(--sd-color-danger) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-danger:focus,a.sd-outline-danger:hover{border-color:var(--sd-color-danger-highlight) !important}.sd-outline-light{border-color:var(--sd-color-light) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-light:focus,a.sd-outline-light:hover{border-color:var(--sd-color-light-highlight) !important}.sd-outline-muted{border-color:var(--sd-color-muted) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-muted:focus,a.sd-outline-muted:hover{border-color:var(--sd-color-muted-highlight) !important}.sd-outline-dark{border-color:var(--sd-color-dark) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-dark:focus,a.sd-outline-dark:hover{border-color:var(--sd-color-dark-highlight) !important}.sd-outline-black{border-color:var(--sd-color-black) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-black:focus,a.sd-outline-black:hover{border-color:var(--sd-color-black-highlight) !important}.sd-outline-white{border-color:var(--sd-color-white) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-white:focus,a.sd-outline-white:hover{border-color:var(--sd-color-white-highlight) !important}.sd-bg-transparent{background-color:transparent !important}.sd-outline-transparent{border-color:transparent !important}.sd-text-transparent{color:transparent !important}.sd-p-0{padding:0 !important}.sd-pt-0,.sd-py-0{padding-top:0 !important}.sd-pr-0,.sd-px-0{padding-right:0 !important}.sd-pb-0,.sd-py-0{padding-bottom:0 !important}.sd-pl-0,.sd-px-0{padding-left:0 !important}.sd-p-1{padding:.25rem !important}.sd-pt-1,.sd-py-1{padding-top:.25rem !important}.sd-pr-1,.sd-px-1{padding-right:.25rem !important}.sd-pb-1,.sd-py-1{padding-bottom:.25rem !important}.sd-pl-1,.sd-px-1{padding-left:.25rem !important}.sd-p-2{padding:.5rem !important}.sd-pt-2,.sd-py-2{padding-top:.5rem !important}.sd-pr-2,.sd-px-2{padding-right:.5rem !important}.sd-pb-2,.sd-py-2{padding-bottom:.5rem !important}.sd-pl-2,.sd-px-2{padding-left:.5rem !important}.sd-p-3{padding:1rem !important}.sd-pt-3,.sd-py-3{padding-top:1rem !important}.sd-pr-3,.sd-px-3{padding-right:1rem !important}.sd-pb-3,.sd-py-3{padding-bottom:1rem !important}.sd-pl-3,.sd-px-3{padding-left:1rem !important}.sd-p-4{padding:1.5rem !important}.sd-pt-4,.sd-py-4{padding-top:1.5rem !important}.sd-pr-4,.sd-px-4{padding-right:1.5rem !important}.sd-pb-4,.sd-py-4{padding-bottom:1.5rem !important}.sd-pl-4,.sd-px-4{padding-left:1.5rem !important}.sd-p-5{padding:3rem !important}.sd-pt-5,.sd-py-5{padding-top:3rem !important}.sd-pr-5,.sd-px-5{padding-right:3rem !important}.sd-pb-5,.sd-py-5{padding-bottom:3rem !important}.sd-pl-5,.sd-px-5{padding-left:3rem !important}.sd-m-auto{margin:auto !important}.sd-mt-auto,.sd-my-auto{margin-top:auto !important}.sd-mr-auto,.sd-mx-auto{margin-right:auto !important}.sd-mb-auto,.sd-my-auto{margin-bottom:auto !important}.sd-ml-auto,.sd-mx-auto{margin-left:auto !important}.sd-m-0{margin:0 !important}.sd-mt-0,.sd-my-0{margin-top:0 !important}.sd-mr-0,.sd-mx-0{margin-right:0 !important}.sd-mb-0,.sd-my-0{margin-bottom:0 !important}.sd-ml-0,.sd-mx-0{margin-left:0 !important}.sd-m-1{margin:.25rem !important}.sd-mt-1,.sd-my-1{margin-top:.25rem !important}.sd-mr-1,.sd-mx-1{margin-right:.25rem !important}.sd-mb-1,.sd-my-1{margin-bottom:.25rem !important}.sd-ml-1,.sd-mx-1{margin-left:.25rem !important}.sd-m-2{margin:.5rem !important}.sd-mt-2,.sd-my-2{margin-top:.5rem !important}.sd-mr-2,.sd-mx-2{margin-right:.5rem !important}.sd-mb-2,.sd-my-2{margin-bottom:.5rem !important}.sd-ml-2,.sd-mx-2{margin-left:.5rem !important}.sd-m-3{margin:1rem !important}.sd-mt-3,.sd-my-3{margin-top:1rem !important}.sd-mr-3,.sd-mx-3{margin-right:1rem !important}.sd-mb-3,.sd-my-3{margin-bottom:1rem !important}.sd-ml-3,.sd-mx-3{margin-left:1rem !important}.sd-m-4{margin:1.5rem !important}.sd-mt-4,.sd-my-4{margin-top:1.5rem !important}.sd-mr-4,.sd-mx-4{margin-right:1.5rem !important}.sd-mb-4,.sd-my-4{margin-bottom:1.5rem !important}.sd-ml-4,.sd-mx-4{margin-left:1.5rem !important}.sd-m-5{margin:3rem !important}.sd-mt-5,.sd-my-5{margin-top:3rem !important}.sd-mr-5,.sd-mx-5{margin-right:3rem !important}.sd-mb-5,.sd-my-5{margin-bottom:3rem !important}.sd-ml-5,.sd-mx-5{margin-left:3rem !important}.sd-w-25{width:25% !important}.sd-w-50{width:50% !important}.sd-w-75{width:75% !important}.sd-w-100{width:100% !important}.sd-w-auto{width:auto !important}.sd-h-25{height:25% !important}.sd-h-50{height:50% !important}.sd-h-75{height:75% !important}.sd-h-100{height:100% !important}.sd-h-auto{height:auto !important}.sd-d-none{display:none !important}.sd-d-inline{display:inline !important}.sd-d-inline-block{display:inline-block !important}.sd-d-block{display:block !important}.sd-d-grid{display:grid !important}.sd-d-flex-row{display:-ms-flexbox !important;display:flex !important;flex-direction:row !important}.sd-d-flex-column{display:-ms-flexbox !important;display:flex !important;flex-direction:column !important}.sd-d-inline-flex{display:-ms-inline-flexbox !important;display:inline-flex !important}@media(min-width: 576px){.sd-d-sm-none{display:none !important}.sd-d-sm-inline{display:inline !important}.sd-d-sm-inline-block{display:inline-block !important}.sd-d-sm-block{display:block !important}.sd-d-sm-grid{display:grid !important}.sd-d-sm-flex{display:-ms-flexbox !important;display:flex !important}.sd-d-sm-inline-flex{display:-ms-inline-flexbox !important;display:inline-flex !important}}@media(min-width: 768px){.sd-d-md-none{display:none !important}.sd-d-md-inline{display:inline !important}.sd-d-md-inline-block{display:inline-block !important}.sd-d-md-block{display:block !important}.sd-d-md-grid{display:grid !important}.sd-d-md-flex{display:-ms-flexbox !important;display:flex !important}.sd-d-md-inline-flex{display:-ms-inline-flexbox !important;display:inline-flex !important}}@media(min-width: 992px){.sd-d-lg-none{display:none !important}.sd-d-lg-inline{display:inline !important}.sd-d-lg-inline-block{display:inline-block !important}.sd-d-lg-block{display:block !important}.sd-d-lg-grid{display:grid !important}.sd-d-lg-flex{display:-ms-flexbox !important;display:flex !important}.sd-d-lg-inline-flex{display:-ms-inline-flexbox !important;display:inline-flex !important}}@media(min-width: 1200px){.sd-d-xl-none{display:none !important}.sd-d-xl-inline{display:inline !important}.sd-d-xl-inline-block{display:inline-block !important}.sd-d-xl-block{display:block !important}.sd-d-xl-grid{display:grid !important}.sd-d-xl-flex{display:-ms-flexbox !important;display:flex !important}.sd-d-xl-inline-flex{display:-ms-inline-flexbox !important;display:inline-flex !important}}.sd-align-major-start{justify-content:flex-start !important}.sd-align-major-end{justify-content:flex-end !important}.sd-align-major-center{justify-content:center !important}.sd-align-major-justify{justify-content:space-between !important}.sd-align-major-spaced{justify-content:space-evenly !important}.sd-align-minor-start{align-items:flex-start !important}.sd-align-minor-end{align-items:flex-end !important}.sd-align-minor-center{align-items:center !important}.sd-align-minor-stretch{align-items:stretch !important}.sd-text-justify{text-align:justify !important}.sd-text-left{text-align:left !important}.sd-text-right{text-align:right !important}.sd-text-center{text-align:center !important}.sd-font-weight-light{font-weight:300 !important}.sd-font-weight-lighter{font-weight:lighter !important}.sd-font-weight-normal{font-weight:400 !important}.sd-font-weight-bold{font-weight:700 !important}.sd-font-weight-bolder{font-weight:bolder !important}.sd-font-italic{font-style:italic !important}.sd-text-decoration-none{text-decoration:none !important}.sd-text-lowercase{text-transform:lowercase !important}.sd-text-uppercase{text-transform:uppercase !important}.sd-text-capitalize{text-transform:capitalize !important}.sd-text-wrap{white-space:normal !important}.sd-text-nowrap{white-space:nowrap !important}.sd-text-truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.sd-fs-1,.sd-fs-1>p{font-size:calc(1.375rem + 1.5vw) !important;line-height:unset !important}.sd-fs-2,.sd-fs-2>p{font-size:calc(1.325rem + 0.9vw) !important;line-height:unset !important}.sd-fs-3,.sd-fs-3>p{font-size:calc(1.3rem + 0.6vw) !important;line-height:unset !important}.sd-fs-4,.sd-fs-4>p{font-size:calc(1.275rem + 0.3vw) !important;line-height:unset !important}.sd-fs-5,.sd-fs-5>p{font-size:1.25rem !important;line-height:unset !important}.sd-fs-6,.sd-fs-6>p{font-size:1rem !important;line-height:unset !important}.sd-border-0{border:0 solid !important}.sd-border-top-0{border-top:0 solid !important}.sd-border-bottom-0{border-bottom:0 solid !important}.sd-border-right-0{border-right:0 solid !important}.sd-border-left-0{border-left:0 solid !important}.sd-border-1{border:1px solid !important}.sd-border-top-1{border-top:1px solid !important}.sd-border-bottom-1{border-bottom:1px solid !important}.sd-border-right-1{border-right:1px solid !important}.sd-border-left-1{border-left:1px solid !important}.sd-border-2{border:2px solid !important}.sd-border-top-2{border-top:2px solid !important}.sd-border-bottom-2{border-bottom:2px solid !important}.sd-border-right-2{border-right:2px solid !important}.sd-border-left-2{border-left:2px solid !important}.sd-border-3{border:3px solid !important}.sd-border-top-3{border-top:3px solid !important}.sd-border-bottom-3{border-bottom:3px solid !important}.sd-border-right-3{border-right:3px solid !important}.sd-border-left-3{border-left:3px solid !important}.sd-border-4{border:4px solid !important}.sd-border-top-4{border-top:4px solid !important}.sd-border-bottom-4{border-bottom:4px solid !important}.sd-border-right-4{border-right:4px solid !important}.sd-border-left-4{border-left:4px solid !important}.sd-border-5{border:5px solid !important}.sd-border-top-5{border-top:5px solid !important}.sd-border-bottom-5{border-bottom:5px solid !important}.sd-border-right-5{border-right:5px solid !important}.sd-border-left-5{border-left:5px solid !important}.sd-rounded-0{border-radius:0 !important}.sd-rounded-1{border-radius:.2rem !important}.sd-rounded-2{border-radius:.3rem !important}.sd-rounded-3{border-radius:.5rem !important}.sd-rounded-pill{border-radius:50rem !important}.sd-rounded-circle{border-radius:50% !important}.shadow-none{box-shadow:none !important}.sd-shadow-sm{box-shadow:0 .125rem .25rem var(--sd-color-shadow) !important}.sd-shadow-md{box-shadow:0 .5rem 1rem var(--sd-color-shadow) !important}.sd-shadow-lg{box-shadow:0 1rem 3rem var(--sd-color-shadow) !important}@keyframes sd-slide-from-left{0%{transform:translateX(-100%)}100%{transform:translateX(0)}}@keyframes sd-slide-from-right{0%{transform:translateX(200%)}100%{transform:translateX(0)}}@keyframes sd-grow100{0%{transform:scale(0);opacity:.5}100%{transform:scale(1);opacity:1}}@keyframes sd-grow50{0%{transform:scale(0.5);opacity:.5}100%{transform:scale(1);opacity:1}}@keyframes sd-grow50-rot20{0%{transform:scale(0.5) rotateZ(-20deg);opacity:.5}75%{transform:scale(1) rotateZ(5deg);opacity:1}95%{transform:scale(1) rotateZ(-1deg);opacity:1}100%{transform:scale(1) rotateZ(0);opacity:1}}.sd-animate-slide-from-left{animation:1s ease-out 0s 1 normal none running sd-slide-from-left}.sd-animate-slide-from-right{animation:1s ease-out 0s 1 normal none running sd-slide-from-right}.sd-animate-grow100{animation:1s ease-out 0s 1 normal none running sd-grow100}.sd-animate-grow50{animation:1s ease-out 0s 1 normal none running sd-grow50}.sd-animate-grow50-rot20{animation:1s ease-out 0s 1 normal none running sd-grow50-rot20}.sd-badge{display:inline-block;padding:.35em .65em;font-size:.75em;font-weight:700;line-height:1;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25rem}.sd-badge:empty{display:none}a.sd-badge{text-decoration:none}.sd-btn .sd-badge{position:relative;top:-1px}.sd-btn{background-color:transparent;border:1px solid transparent;border-radius:.25rem;cursor:pointer;display:inline-block;font-weight:400;font-size:1rem;line-height:1.5;padding:.375rem .75rem;text-align:center;text-decoration:none;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;vertical-align:middle;user-select:none;-moz-user-select:none;-ms-user-select:none;-webkit-user-select:none}.sd-btn:hover{text-decoration:none}@media(prefers-reduced-motion: reduce){.sd-btn{transition:none}}.sd-btn-primary,.sd-btn-outline-primary:hover,.sd-btn-outline-primary:focus{color:var(--sd-color-primary-text) !important;background-color:var(--sd-color-primary) !important;border-color:var(--sd-color-primary) !important;border-width:1px !important;border-style:solid !important}.sd-btn-primary:hover,.sd-btn-primary:focus{color:var(--sd-color-primary-text) !important;background-color:var(--sd-color-primary-highlight) !important;border-color:var(--sd-color-primary-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-primary{color:var(--sd-color-primary) !important;border-color:var(--sd-color-primary) !important;border-width:1px !important;border-style:solid !important}.sd-btn-secondary,.sd-btn-outline-secondary:hover,.sd-btn-outline-secondary:focus{color:var(--sd-color-secondary-text) !important;background-color:var(--sd-color-secondary) !important;border-color:var(--sd-color-secondary) !important;border-width:1px !important;border-style:solid !important}.sd-btn-secondary:hover,.sd-btn-secondary:focus{color:var(--sd-color-secondary-text) !important;background-color:var(--sd-color-secondary-highlight) !important;border-color:var(--sd-color-secondary-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-secondary{color:var(--sd-color-secondary) !important;border-color:var(--sd-color-secondary) !important;border-width:1px !important;border-style:solid !important}.sd-btn-success,.sd-btn-outline-success:hover,.sd-btn-outline-success:focus{color:var(--sd-color-success-text) !important;background-color:var(--sd-color-success) !important;border-color:var(--sd-color-success) !important;border-width:1px !important;border-style:solid !important}.sd-btn-success:hover,.sd-btn-success:focus{color:var(--sd-color-success-text) !important;background-color:var(--sd-color-success-highlight) !important;border-color:var(--sd-color-success-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-success{color:var(--sd-color-success) !important;border-color:var(--sd-color-success) !important;border-width:1px !important;border-style:solid !important}.sd-btn-info,.sd-btn-outline-info:hover,.sd-btn-outline-info:focus{color:var(--sd-color-info-text) !important;background-color:var(--sd-color-info) !important;border-color:var(--sd-color-info) !important;border-width:1px !important;border-style:solid !important}.sd-btn-info:hover,.sd-btn-info:focus{color:var(--sd-color-info-text) !important;background-color:var(--sd-color-info-highlight) !important;border-color:var(--sd-color-info-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-info{color:var(--sd-color-info) !important;border-color:var(--sd-color-info) !important;border-width:1px !important;border-style:solid !important}.sd-btn-warning,.sd-btn-outline-warning:hover,.sd-btn-outline-warning:focus{color:var(--sd-color-warning-text) !important;background-color:var(--sd-color-warning) !important;border-color:var(--sd-color-warning) !important;border-width:1px !important;border-style:solid !important}.sd-btn-warning:hover,.sd-btn-warning:focus{color:var(--sd-color-warning-text) !important;background-color:var(--sd-color-warning-highlight) !important;border-color:var(--sd-color-warning-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-warning{color:var(--sd-color-warning) !important;border-color:var(--sd-color-warning) !important;border-width:1px !important;border-style:solid !important}.sd-btn-danger,.sd-btn-outline-danger:hover,.sd-btn-outline-danger:focus{color:var(--sd-color-danger-text) !important;background-color:var(--sd-color-danger) !important;border-color:var(--sd-color-danger) !important;border-width:1px !important;border-style:solid !important}.sd-btn-danger:hover,.sd-btn-danger:focus{color:var(--sd-color-danger-text) !important;background-color:var(--sd-color-danger-highlight) !important;border-color:var(--sd-color-danger-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-danger{color:var(--sd-color-danger) !important;border-color:var(--sd-color-danger) !important;border-width:1px !important;border-style:solid !important}.sd-btn-light,.sd-btn-outline-light:hover,.sd-btn-outline-light:focus{color:var(--sd-color-light-text) !important;background-color:var(--sd-color-light) !important;border-color:var(--sd-color-light) !important;border-width:1px !important;border-style:solid !important}.sd-btn-light:hover,.sd-btn-light:focus{color:var(--sd-color-light-text) !important;background-color:var(--sd-color-light-highlight) !important;border-color:var(--sd-color-light-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-light{color:var(--sd-color-light) !important;border-color:var(--sd-color-light) !important;border-width:1px !important;border-style:solid !important}.sd-btn-muted,.sd-btn-outline-muted:hover,.sd-btn-outline-muted:focus{color:var(--sd-color-muted-text) !important;background-color:var(--sd-color-muted) !important;border-color:var(--sd-color-muted) !important;border-width:1px !important;border-style:solid !important}.sd-btn-muted:hover,.sd-btn-muted:focus{color:var(--sd-color-muted-text) !important;background-color:var(--sd-color-muted-highlight) !important;border-color:var(--sd-color-muted-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-muted{color:var(--sd-color-muted) !important;border-color:var(--sd-color-muted) !important;border-width:1px !important;border-style:solid !important}.sd-btn-dark,.sd-btn-outline-dark:hover,.sd-btn-outline-dark:focus{color:var(--sd-color-dark-text) !important;background-color:var(--sd-color-dark) !important;border-color:var(--sd-color-dark) !important;border-width:1px !important;border-style:solid !important}.sd-btn-dark:hover,.sd-btn-dark:focus{color:var(--sd-color-dark-text) !important;background-color:var(--sd-color-dark-highlight) !important;border-color:var(--sd-color-dark-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-dark{color:var(--sd-color-dark) !important;border-color:var(--sd-color-dark) !important;border-width:1px !important;border-style:solid !important}.sd-btn-black,.sd-btn-outline-black:hover,.sd-btn-outline-black:focus{color:var(--sd-color-black-text) !important;background-color:var(--sd-color-black) !important;border-color:var(--sd-color-black) !important;border-width:1px !important;border-style:solid !important}.sd-btn-black:hover,.sd-btn-black:focus{color:var(--sd-color-black-text) !important;background-color:var(--sd-color-black-highlight) !important;border-color:var(--sd-color-black-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-black{color:var(--sd-color-black) !important;border-color:var(--sd-color-black) !important;border-width:1px !important;border-style:solid !important}.sd-btn-white,.sd-btn-outline-white:hover,.sd-btn-outline-white:focus{color:var(--sd-color-white-text) !important;background-color:var(--sd-color-white) !important;border-color:var(--sd-color-white) !important;border-width:1px !important;border-style:solid !important}.sd-btn-white:hover,.sd-btn-white:focus{color:var(--sd-color-white-text) !important;background-color:var(--sd-color-white-highlight) !important;border-color:var(--sd-color-white-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-white{color:var(--sd-color-white) !important;border-color:var(--sd-color-white) !important;border-width:1px !important;border-style:solid !important}.sd-stretched-link::after{position:absolute;top:0;right:0;bottom:0;left:0;z-index:1;content:""}.sd-hide-link-text{font-size:0}.sd-octicon,.sd-material-icon{display:inline-block;fill:currentColor;vertical-align:middle}.sd-avatar-xs{border-radius:50%;object-fit:cover;object-position:center;width:1rem;height:1rem}.sd-avatar-sm{border-radius:50%;object-fit:cover;object-position:center;width:3rem;height:3rem}.sd-avatar-md{border-radius:50%;object-fit:cover;object-position:center;width:5rem;height:5rem}.sd-avatar-lg{border-radius:50%;object-fit:cover;object-position:center;width:7rem;height:7rem}.sd-avatar-xl{border-radius:50%;object-fit:cover;object-position:center;width:10rem;height:10rem}.sd-avatar-inherit{border-radius:50%;object-fit:cover;object-position:center;width:inherit;height:inherit}.sd-avatar-initial{border-radius:50%;object-fit:cover;object-position:center;width:initial;height:initial}.sd-card{background-clip:border-box;background-color:var(--sd-color-card-background);border:1px solid var(--sd-color-card-border);border-radius:.25rem;color:var(--sd-color-card-text);display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;min-width:0;position:relative;word-wrap:break-word}.sd-card>hr{margin-left:0;margin-right:0}.sd-card-hover:hover{border-color:var(--sd-color-card-border-hover);transform:scale(1.01)}.sd-card-body{-ms-flex:1 1 auto;flex:1 1 auto;padding:1rem 1rem}.sd-card-title{margin-bottom:.5rem}.sd-card-subtitle{margin-top:-0.25rem;margin-bottom:0}.sd-card-text:last-child{margin-bottom:0}.sd-card-link:hover{text-decoration:none}.sd-card-link+.card-link{margin-left:1rem}.sd-card-header{padding:.5rem 1rem;margin-bottom:0;background-color:var(--sd-color-card-header);border-bottom:1px solid var(--sd-color-card-border)}.sd-card-header:first-child{border-radius:calc(0.25rem - 1px) calc(0.25rem - 1px) 0 0}.sd-card-footer{padding:.5rem 1rem;background-color:var(--sd-color-card-footer);border-top:1px solid var(--sd-color-card-border)}.sd-card-footer:last-child{border-radius:0 0 calc(0.25rem - 1px) calc(0.25rem - 1px)}.sd-card-header-tabs{margin-right:-0.5rem;margin-bottom:-0.5rem;margin-left:-0.5rem;border-bottom:0}.sd-card-header-pills{margin-right:-0.5rem;margin-left:-0.5rem}.sd-card-img-overlay{position:absolute;top:0;right:0;bottom:0;left:0;padding:1rem;border-radius:calc(0.25rem - 1px)}.sd-card-img,.sd-card-img-bottom,.sd-card-img-top{width:100%}.sd-card-img,.sd-card-img-top{border-top-left-radius:calc(0.25rem - 1px);border-top-right-radius:calc(0.25rem - 1px)}.sd-card-img,.sd-card-img-bottom{border-bottom-left-radius:calc(0.25rem - 1px);border-bottom-right-radius:calc(0.25rem - 1px)}.sd-cards-carousel{width:100%;display:flex;flex-wrap:nowrap;-ms-flex-direction:row;flex-direction:row;overflow-x:hidden;scroll-snap-type:x mandatory}.sd-cards-carousel.sd-show-scrollbar{overflow-x:auto}.sd-cards-carousel:hover,.sd-cards-carousel:focus{overflow-x:auto}.sd-cards-carousel>.sd-card{flex-shrink:0;scroll-snap-align:start}.sd-cards-carousel>.sd-card:not(:last-child){margin-right:3px}.sd-card-cols-1>.sd-card{width:90%}.sd-card-cols-2>.sd-card{width:45%}.sd-card-cols-3>.sd-card{width:30%}.sd-card-cols-4>.sd-card{width:22.5%}.sd-card-cols-5>.sd-card{width:18%}.sd-card-cols-6>.sd-card{width:15%}.sd-card-cols-7>.sd-card{width:12.8571428571%}.sd-card-cols-8>.sd-card{width:11.25%}.sd-card-cols-9>.sd-card{width:10%}.sd-card-cols-10>.sd-card{width:9%}.sd-card-cols-11>.sd-card{width:8.1818181818%}.sd-card-cols-12>.sd-card{width:7.5%}.sd-container,.sd-container-fluid,.sd-container-lg,.sd-container-md,.sd-container-sm,.sd-container-xl{margin-left:auto;margin-right:auto;padding-left:var(--sd-gutter-x, 0.75rem);padding-right:var(--sd-gutter-x, 0.75rem);width:100%}@media(min-width: 576px){.sd-container-sm,.sd-container{max-width:540px}}@media(min-width: 768px){.sd-container-md,.sd-container-sm,.sd-container{max-width:720px}}@media(min-width: 992px){.sd-container-lg,.sd-container-md,.sd-container-sm,.sd-container{max-width:960px}}@media(min-width: 1200px){.sd-container-xl,.sd-container-lg,.sd-container-md,.sd-container-sm,.sd-container{max-width:1140px}}.sd-row{--sd-gutter-x: 1.5rem;--sd-gutter-y: 0;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;margin-top:calc(var(--sd-gutter-y) * -1);margin-right:calc(var(--sd-gutter-x) * -0.5);margin-left:calc(var(--sd-gutter-x) * -0.5)}.sd-row>*{box-sizing:border-box;flex-shrink:0;width:100%;max-width:100%;padding-right:calc(var(--sd-gutter-x) * 0.5);padding-left:calc(var(--sd-gutter-x) * 0.5);margin-top:var(--sd-gutter-y)}.sd-col{flex:1 0 0%;-ms-flex:1 0 0%}.sd-row-cols-auto>*{flex:0 0 auto;width:auto}.sd-row-cols-1>*{flex:0 0 auto;-ms-flex:0 0 auto;width:100%}.sd-row-cols-2>*{flex:0 0 auto;-ms-flex:0 0 auto;width:50%}.sd-row-cols-3>*{flex:0 0 auto;-ms-flex:0 0 auto;width:33.3333333333%}.sd-row-cols-4>*{flex:0 0 auto;-ms-flex:0 0 auto;width:25%}.sd-row-cols-5>*{flex:0 0 auto;-ms-flex:0 0 auto;width:20%}.sd-row-cols-6>*{flex:0 0 auto;-ms-flex:0 0 auto;width:16.6666666667%}.sd-row-cols-7>*{flex:0 0 auto;-ms-flex:0 0 auto;width:14.2857142857%}.sd-row-cols-8>*{flex:0 0 auto;-ms-flex:0 0 auto;width:12.5%}.sd-row-cols-9>*{flex:0 0 auto;-ms-flex:0 0 auto;width:11.1111111111%}.sd-row-cols-10>*{flex:0 0 auto;-ms-flex:0 0 auto;width:10%}.sd-row-cols-11>*{flex:0 0 auto;-ms-flex:0 0 auto;width:9.0909090909%}.sd-row-cols-12>*{flex:0 0 auto;-ms-flex:0 0 auto;width:8.3333333333%}@media(min-width: 576px){.sd-col-sm{flex:1 0 0%;-ms-flex:1 0 0%}.sd-row-cols-sm-auto{flex:1 0 auto;-ms-flex:1 0 auto;width:100%}.sd-row-cols-sm-1>*{flex:0 0 auto;-ms-flex:0 0 auto;width:100%}.sd-row-cols-sm-2>*{flex:0 0 auto;-ms-flex:0 0 auto;width:50%}.sd-row-cols-sm-3>*{flex:0 0 auto;-ms-flex:0 0 auto;width:33.3333333333%}.sd-row-cols-sm-4>*{flex:0 0 auto;-ms-flex:0 0 auto;width:25%}.sd-row-cols-sm-5>*{flex:0 0 auto;-ms-flex:0 0 auto;width:20%}.sd-row-cols-sm-6>*{flex:0 0 auto;-ms-flex:0 0 auto;width:16.6666666667%}.sd-row-cols-sm-7>*{flex:0 0 auto;-ms-flex:0 0 auto;width:14.2857142857%}.sd-row-cols-sm-8>*{flex:0 0 auto;-ms-flex:0 0 auto;width:12.5%}.sd-row-cols-sm-9>*{flex:0 0 auto;-ms-flex:0 0 auto;width:11.1111111111%}.sd-row-cols-sm-10>*{flex:0 0 auto;-ms-flex:0 0 auto;width:10%}.sd-row-cols-sm-11>*{flex:0 0 auto;-ms-flex:0 0 auto;width:9.0909090909%}.sd-row-cols-sm-12>*{flex:0 0 auto;-ms-flex:0 0 auto;width:8.3333333333%}}@media(min-width: 768px){.sd-col-md{flex:1 0 0%;-ms-flex:1 0 0%}.sd-row-cols-md-auto{flex:1 0 auto;-ms-flex:1 0 auto;width:100%}.sd-row-cols-md-1>*{flex:0 0 auto;-ms-flex:0 0 auto;width:100%}.sd-row-cols-md-2>*{flex:0 0 auto;-ms-flex:0 0 auto;width:50%}.sd-row-cols-md-3>*{flex:0 0 auto;-ms-flex:0 0 auto;width:33.3333333333%}.sd-row-cols-md-4>*{flex:0 0 auto;-ms-flex:0 0 auto;width:25%}.sd-row-cols-md-5>*{flex:0 0 auto;-ms-flex:0 0 auto;width:20%}.sd-row-cols-md-6>*{flex:0 0 auto;-ms-flex:0 0 auto;width:16.6666666667%}.sd-row-cols-md-7>*{flex:0 0 auto;-ms-flex:0 0 auto;width:14.2857142857%}.sd-row-cols-md-8>*{flex:0 0 auto;-ms-flex:0 0 auto;width:12.5%}.sd-row-cols-md-9>*{flex:0 0 auto;-ms-flex:0 0 auto;width:11.1111111111%}.sd-row-cols-md-10>*{flex:0 0 auto;-ms-flex:0 0 auto;width:10%}.sd-row-cols-md-11>*{flex:0 0 auto;-ms-flex:0 0 auto;width:9.0909090909%}.sd-row-cols-md-12>*{flex:0 0 auto;-ms-flex:0 0 auto;width:8.3333333333%}}@media(min-width: 992px){.sd-col-lg{flex:1 0 0%;-ms-flex:1 0 0%}.sd-row-cols-lg-auto{flex:1 0 auto;-ms-flex:1 0 auto;width:100%}.sd-row-cols-lg-1>*{flex:0 0 auto;-ms-flex:0 0 auto;width:100%}.sd-row-cols-lg-2>*{flex:0 0 auto;-ms-flex:0 0 auto;width:50%}.sd-row-cols-lg-3>*{flex:0 0 auto;-ms-flex:0 0 auto;width:33.3333333333%}.sd-row-cols-lg-4>*{flex:0 0 auto;-ms-flex:0 0 auto;width:25%}.sd-row-cols-lg-5>*{flex:0 0 auto;-ms-flex:0 0 auto;width:20%}.sd-row-cols-lg-6>*{flex:0 0 auto;-ms-flex:0 0 auto;width:16.6666666667%}.sd-row-cols-lg-7>*{flex:0 0 auto;-ms-flex:0 0 auto;width:14.2857142857%}.sd-row-cols-lg-8>*{flex:0 0 auto;-ms-flex:0 0 auto;width:12.5%}.sd-row-cols-lg-9>*{flex:0 0 auto;-ms-flex:0 0 auto;width:11.1111111111%}.sd-row-cols-lg-10>*{flex:0 0 auto;-ms-flex:0 0 auto;width:10%}.sd-row-cols-lg-11>*{flex:0 0 auto;-ms-flex:0 0 auto;width:9.0909090909%}.sd-row-cols-lg-12>*{flex:0 0 auto;-ms-flex:0 0 auto;width:8.3333333333%}}@media(min-width: 1200px){.sd-col-xl{flex:1 0 0%;-ms-flex:1 0 0%}.sd-row-cols-xl-auto{flex:1 0 auto;-ms-flex:1 0 auto;width:100%}.sd-row-cols-xl-1>*{flex:0 0 auto;-ms-flex:0 0 auto;width:100%}.sd-row-cols-xl-2>*{flex:0 0 auto;-ms-flex:0 0 auto;width:50%}.sd-row-cols-xl-3>*{flex:0 0 auto;-ms-flex:0 0 auto;width:33.3333333333%}.sd-row-cols-xl-4>*{flex:0 0 auto;-ms-flex:0 0 auto;width:25%}.sd-row-cols-xl-5>*{flex:0 0 auto;-ms-flex:0 0 auto;width:20%}.sd-row-cols-xl-6>*{flex:0 0 auto;-ms-flex:0 0 auto;width:16.6666666667%}.sd-row-cols-xl-7>*{flex:0 0 auto;-ms-flex:0 0 auto;width:14.2857142857%}.sd-row-cols-xl-8>*{flex:0 0 auto;-ms-flex:0 0 auto;width:12.5%}.sd-row-cols-xl-9>*{flex:0 0 auto;-ms-flex:0 0 auto;width:11.1111111111%}.sd-row-cols-xl-10>*{flex:0 0 auto;-ms-flex:0 0 auto;width:10%}.sd-row-cols-xl-11>*{flex:0 0 auto;-ms-flex:0 0 auto;width:9.0909090909%}.sd-row-cols-xl-12>*{flex:0 0 auto;-ms-flex:0 0 auto;width:8.3333333333%}}.sd-col-auto{flex:0 0 auto;-ms-flex:0 0 auto;width:auto}.sd-col-1{flex:0 0 auto;-ms-flex:0 0 auto;width:8.3333333333%}.sd-col-2{flex:0 0 auto;-ms-flex:0 0 auto;width:16.6666666667%}.sd-col-3{flex:0 0 auto;-ms-flex:0 0 auto;width:25%}.sd-col-4{flex:0 0 auto;-ms-flex:0 0 auto;width:33.3333333333%}.sd-col-5{flex:0 0 auto;-ms-flex:0 0 auto;width:41.6666666667%}.sd-col-6{flex:0 0 auto;-ms-flex:0 0 auto;width:50%}.sd-col-7{flex:0 0 auto;-ms-flex:0 0 auto;width:58.3333333333%}.sd-col-8{flex:0 0 auto;-ms-flex:0 0 auto;width:66.6666666667%}.sd-col-9{flex:0 0 auto;-ms-flex:0 0 auto;width:75%}.sd-col-10{flex:0 0 auto;-ms-flex:0 0 auto;width:83.3333333333%}.sd-col-11{flex:0 0 auto;-ms-flex:0 0 auto;width:91.6666666667%}.sd-col-12{flex:0 0 auto;-ms-flex:0 0 auto;width:100%}.sd-g-0,.sd-gy-0{--sd-gutter-y: 0}.sd-g-0,.sd-gx-0{--sd-gutter-x: 0}.sd-g-1,.sd-gy-1{--sd-gutter-y: 0.25rem}.sd-g-1,.sd-gx-1{--sd-gutter-x: 0.25rem}.sd-g-2,.sd-gy-2{--sd-gutter-y: 0.5rem}.sd-g-2,.sd-gx-2{--sd-gutter-x: 0.5rem}.sd-g-3,.sd-gy-3{--sd-gutter-y: 1rem}.sd-g-3,.sd-gx-3{--sd-gutter-x: 1rem}.sd-g-4,.sd-gy-4{--sd-gutter-y: 1.5rem}.sd-g-4,.sd-gx-4{--sd-gutter-x: 1.5rem}.sd-g-5,.sd-gy-5{--sd-gutter-y: 3rem}.sd-g-5,.sd-gx-5{--sd-gutter-x: 3rem}@media(min-width: 576px){.sd-col-sm-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto}.sd-col-sm-1{-ms-flex:0 0 auto;flex:0 0 auto;width:8.3333333333%}.sd-col-sm-2{-ms-flex:0 0 auto;flex:0 0 auto;width:16.6666666667%}.sd-col-sm-3{-ms-flex:0 0 auto;flex:0 0 auto;width:25%}.sd-col-sm-4{-ms-flex:0 0 auto;flex:0 0 auto;width:33.3333333333%}.sd-col-sm-5{-ms-flex:0 0 auto;flex:0 0 auto;width:41.6666666667%}.sd-col-sm-6{-ms-flex:0 0 auto;flex:0 0 auto;width:50%}.sd-col-sm-7{-ms-flex:0 0 auto;flex:0 0 auto;width:58.3333333333%}.sd-col-sm-8{-ms-flex:0 0 auto;flex:0 0 auto;width:66.6666666667%}.sd-col-sm-9{-ms-flex:0 0 auto;flex:0 0 auto;width:75%}.sd-col-sm-10{-ms-flex:0 0 auto;flex:0 0 auto;width:83.3333333333%}.sd-col-sm-11{-ms-flex:0 0 auto;flex:0 0 auto;width:91.6666666667%}.sd-col-sm-12{-ms-flex:0 0 auto;flex:0 0 auto;width:100%}.sd-g-sm-0,.sd-gy-sm-0{--sd-gutter-y: 0}.sd-g-sm-0,.sd-gx-sm-0{--sd-gutter-x: 0}.sd-g-sm-1,.sd-gy-sm-1{--sd-gutter-y: 0.25rem}.sd-g-sm-1,.sd-gx-sm-1{--sd-gutter-x: 0.25rem}.sd-g-sm-2,.sd-gy-sm-2{--sd-gutter-y: 0.5rem}.sd-g-sm-2,.sd-gx-sm-2{--sd-gutter-x: 0.5rem}.sd-g-sm-3,.sd-gy-sm-3{--sd-gutter-y: 1rem}.sd-g-sm-3,.sd-gx-sm-3{--sd-gutter-x: 1rem}.sd-g-sm-4,.sd-gy-sm-4{--sd-gutter-y: 1.5rem}.sd-g-sm-4,.sd-gx-sm-4{--sd-gutter-x: 1.5rem}.sd-g-sm-5,.sd-gy-sm-5{--sd-gutter-y: 3rem}.sd-g-sm-5,.sd-gx-sm-5{--sd-gutter-x: 3rem}}@media(min-width: 768px){.sd-col-md-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto}.sd-col-md-1{-ms-flex:0 0 auto;flex:0 0 auto;width:8.3333333333%}.sd-col-md-2{-ms-flex:0 0 auto;flex:0 0 auto;width:16.6666666667%}.sd-col-md-3{-ms-flex:0 0 auto;flex:0 0 auto;width:25%}.sd-col-md-4{-ms-flex:0 0 auto;flex:0 0 auto;width:33.3333333333%}.sd-col-md-5{-ms-flex:0 0 auto;flex:0 0 auto;width:41.6666666667%}.sd-col-md-6{-ms-flex:0 0 auto;flex:0 0 auto;width:50%}.sd-col-md-7{-ms-flex:0 0 auto;flex:0 0 auto;width:58.3333333333%}.sd-col-md-8{-ms-flex:0 0 auto;flex:0 0 auto;width:66.6666666667%}.sd-col-md-9{-ms-flex:0 0 auto;flex:0 0 auto;width:75%}.sd-col-md-10{-ms-flex:0 0 auto;flex:0 0 auto;width:83.3333333333%}.sd-col-md-11{-ms-flex:0 0 auto;flex:0 0 auto;width:91.6666666667%}.sd-col-md-12{-ms-flex:0 0 auto;flex:0 0 auto;width:100%}.sd-g-md-0,.sd-gy-md-0{--sd-gutter-y: 0}.sd-g-md-0,.sd-gx-md-0{--sd-gutter-x: 0}.sd-g-md-1,.sd-gy-md-1{--sd-gutter-y: 0.25rem}.sd-g-md-1,.sd-gx-md-1{--sd-gutter-x: 0.25rem}.sd-g-md-2,.sd-gy-md-2{--sd-gutter-y: 0.5rem}.sd-g-md-2,.sd-gx-md-2{--sd-gutter-x: 0.5rem}.sd-g-md-3,.sd-gy-md-3{--sd-gutter-y: 1rem}.sd-g-md-3,.sd-gx-md-3{--sd-gutter-x: 1rem}.sd-g-md-4,.sd-gy-md-4{--sd-gutter-y: 1.5rem}.sd-g-md-4,.sd-gx-md-4{--sd-gutter-x: 1.5rem}.sd-g-md-5,.sd-gy-md-5{--sd-gutter-y: 3rem}.sd-g-md-5,.sd-gx-md-5{--sd-gutter-x: 3rem}}@media(min-width: 992px){.sd-col-lg-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto}.sd-col-lg-1{-ms-flex:0 0 auto;flex:0 0 auto;width:8.3333333333%}.sd-col-lg-2{-ms-flex:0 0 auto;flex:0 0 auto;width:16.6666666667%}.sd-col-lg-3{-ms-flex:0 0 auto;flex:0 0 auto;width:25%}.sd-col-lg-4{-ms-flex:0 0 auto;flex:0 0 auto;width:33.3333333333%}.sd-col-lg-5{-ms-flex:0 0 auto;flex:0 0 auto;width:41.6666666667%}.sd-col-lg-6{-ms-flex:0 0 auto;flex:0 0 auto;width:50%}.sd-col-lg-7{-ms-flex:0 0 auto;flex:0 0 auto;width:58.3333333333%}.sd-col-lg-8{-ms-flex:0 0 auto;flex:0 0 auto;width:66.6666666667%}.sd-col-lg-9{-ms-flex:0 0 auto;flex:0 0 auto;width:75%}.sd-col-lg-10{-ms-flex:0 0 auto;flex:0 0 auto;width:83.3333333333%}.sd-col-lg-11{-ms-flex:0 0 auto;flex:0 0 auto;width:91.6666666667%}.sd-col-lg-12{-ms-flex:0 0 auto;flex:0 0 auto;width:100%}.sd-g-lg-0,.sd-gy-lg-0{--sd-gutter-y: 0}.sd-g-lg-0,.sd-gx-lg-0{--sd-gutter-x: 0}.sd-g-lg-1,.sd-gy-lg-1{--sd-gutter-y: 0.25rem}.sd-g-lg-1,.sd-gx-lg-1{--sd-gutter-x: 0.25rem}.sd-g-lg-2,.sd-gy-lg-2{--sd-gutter-y: 0.5rem}.sd-g-lg-2,.sd-gx-lg-2{--sd-gutter-x: 0.5rem}.sd-g-lg-3,.sd-gy-lg-3{--sd-gutter-y: 1rem}.sd-g-lg-3,.sd-gx-lg-3{--sd-gutter-x: 1rem}.sd-g-lg-4,.sd-gy-lg-4{--sd-gutter-y: 1.5rem}.sd-g-lg-4,.sd-gx-lg-4{--sd-gutter-x: 1.5rem}.sd-g-lg-5,.sd-gy-lg-5{--sd-gutter-y: 3rem}.sd-g-lg-5,.sd-gx-lg-5{--sd-gutter-x: 3rem}}@media(min-width: 1200px){.sd-col-xl-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto}.sd-col-xl-1{-ms-flex:0 0 auto;flex:0 0 auto;width:8.3333333333%}.sd-col-xl-2{-ms-flex:0 0 auto;flex:0 0 auto;width:16.6666666667%}.sd-col-xl-3{-ms-flex:0 0 auto;flex:0 0 auto;width:25%}.sd-col-xl-4{-ms-flex:0 0 auto;flex:0 0 auto;width:33.3333333333%}.sd-col-xl-5{-ms-flex:0 0 auto;flex:0 0 auto;width:41.6666666667%}.sd-col-xl-6{-ms-flex:0 0 auto;flex:0 0 auto;width:50%}.sd-col-xl-7{-ms-flex:0 0 auto;flex:0 0 auto;width:58.3333333333%}.sd-col-xl-8{-ms-flex:0 0 auto;flex:0 0 auto;width:66.6666666667%}.sd-col-xl-9{-ms-flex:0 0 auto;flex:0 0 auto;width:75%}.sd-col-xl-10{-ms-flex:0 0 auto;flex:0 0 auto;width:83.3333333333%}.sd-col-xl-11{-ms-flex:0 0 auto;flex:0 0 auto;width:91.6666666667%}.sd-col-xl-12{-ms-flex:0 0 auto;flex:0 0 auto;width:100%}.sd-g-xl-0,.sd-gy-xl-0{--sd-gutter-y: 0}.sd-g-xl-0,.sd-gx-xl-0{--sd-gutter-x: 0}.sd-g-xl-1,.sd-gy-xl-1{--sd-gutter-y: 0.25rem}.sd-g-xl-1,.sd-gx-xl-1{--sd-gutter-x: 0.25rem}.sd-g-xl-2,.sd-gy-xl-2{--sd-gutter-y: 0.5rem}.sd-g-xl-2,.sd-gx-xl-2{--sd-gutter-x: 0.5rem}.sd-g-xl-3,.sd-gy-xl-3{--sd-gutter-y: 1rem}.sd-g-xl-3,.sd-gx-xl-3{--sd-gutter-x: 1rem}.sd-g-xl-4,.sd-gy-xl-4{--sd-gutter-y: 1.5rem}.sd-g-xl-4,.sd-gx-xl-4{--sd-gutter-x: 1.5rem}.sd-g-xl-5,.sd-gy-xl-5{--sd-gutter-y: 3rem}.sd-g-xl-5,.sd-gx-xl-5{--sd-gutter-x: 3rem}}.sd-flex-row-reverse{flex-direction:row-reverse !important}details.sd-dropdown{position:relative;font-size:var(--sd-fontsize-dropdown)}details.sd-dropdown:hover{cursor:pointer}details.sd-dropdown .sd-summary-content{cursor:default}details.sd-dropdown summary.sd-summary-title{padding:.5em 1em;font-size:var(--sd-fontsize-dropdown-title);font-weight:var(--sd-fontweight-dropdown-title);user-select:none;-moz-user-select:none;-ms-user-select:none;-webkit-user-select:none;list-style:none;display:inline-flex;justify-content:space-between}details.sd-dropdown summary.sd-summary-title::-webkit-details-marker{display:none}details.sd-dropdown summary.sd-summary-title:focus{outline:none}details.sd-dropdown summary.sd-summary-title .sd-summary-icon{margin-right:.6em;display:inline-flex;align-items:center}details.sd-dropdown summary.sd-summary-title .sd-summary-icon svg{opacity:.8}details.sd-dropdown summary.sd-summary-title .sd-summary-text{flex-grow:1;line-height:1.5;padding-right:.5rem}details.sd-dropdown summary.sd-summary-title .sd-summary-state-marker{pointer-events:none;display:inline-flex;align-items:center}details.sd-dropdown summary.sd-summary-title .sd-summary-state-marker svg{opacity:.6}details.sd-dropdown summary.sd-summary-title:hover .sd-summary-state-marker svg{opacity:1;transform:scale(1.1)}details.sd-dropdown[open] summary .sd-octicon.no-title{visibility:hidden}details.sd-dropdown .sd-summary-chevron-right{transition:.25s}details.sd-dropdown[open]>.sd-summary-title .sd-summary-chevron-right{transform:rotate(90deg)}details.sd-dropdown[open]>.sd-summary-title .sd-summary-chevron-down{transform:rotate(180deg)}details.sd-dropdown:not([open]).sd-card{border:none}details.sd-dropdown:not([open])>.sd-card-header{border:1px solid var(--sd-color-card-border);border-radius:.25rem}details.sd-dropdown.sd-fade-in[open] summary~*{-moz-animation:sd-fade-in .5s ease-in-out;-webkit-animation:sd-fade-in .5s ease-in-out;animation:sd-fade-in .5s ease-in-out}details.sd-dropdown.sd-fade-in-slide-down[open] summary~*{-moz-animation:sd-fade-in .5s ease-in-out,sd-slide-down .5s ease-in-out;-webkit-animation:sd-fade-in .5s ease-in-out,sd-slide-down .5s ease-in-out;animation:sd-fade-in .5s ease-in-out,sd-slide-down .5s ease-in-out}.sd-col>.sd-dropdown{width:100%}.sd-summary-content>.sd-tab-set:first-child{margin-top:0}@keyframes sd-fade-in{0%{opacity:0}100%{opacity:1}}@keyframes sd-slide-down{0%{transform:translate(0, -10px)}100%{transform:translate(0, 0)}}.sd-tab-set{border-radius:.125rem;display:flex;flex-wrap:wrap;margin:1em 0;position:relative}.sd-tab-set>input{opacity:0;position:absolute}.sd-tab-set>input:checked+label{border-color:var(--sd-color-tabs-underline-active);color:var(--sd-color-tabs-label-active)}.sd-tab-set>input:checked+label+.sd-tab-content{display:block}.sd-tab-set>input:not(:checked)+label:hover{color:var(--sd-color-tabs-label-hover);border-color:var(--sd-color-tabs-underline-hover)}.sd-tab-set>input:focus+label{outline-style:auto}.sd-tab-set>input:not(.focus-visible)+label{outline:none;-webkit-tap-highlight-color:transparent}.sd-tab-set>label{border-bottom:.125rem solid transparent;margin-bottom:0;color:var(--sd-color-tabs-label-inactive);border-color:var(--sd-color-tabs-underline-inactive);cursor:pointer;font-size:var(--sd-fontsize-tabs-label);font-weight:700;padding:1em 1.25em .5em;transition:color 250ms;width:auto;z-index:1}html .sd-tab-set>label:hover{color:var(--sd-color-tabs-label-active)}.sd-col>.sd-tab-set{width:100%}.sd-tab-content{box-shadow:0 -0.0625rem var(--sd-color-tabs-overline),0 .0625rem var(--sd-color-tabs-underline);display:none;order:99;padding-bottom:.75rem;padding-top:.75rem;width:100%}.sd-tab-content>:first-child{margin-top:0 !important}.sd-tab-content>:last-child{margin-bottom:0 !important}.sd-tab-content>.sd-tab-set{margin:0}.sd-sphinx-override,.sd-sphinx-override *{-moz-box-sizing:border-box;-webkit-box-sizing:border-box;box-sizing:border-box}.sd-sphinx-override p{margin-top:0}:root{--sd-color-primary: #0071bc;--sd-color-secondary: #6c757d;--sd-color-success: #28a745;--sd-color-info: #17a2b8;--sd-color-warning: #f0b37e;--sd-color-danger: #dc3545;--sd-color-light: #f8f9fa;--sd-color-muted: #6c757d;--sd-color-dark: #212529;--sd-color-black: black;--sd-color-white: white;--sd-color-primary-highlight: #0060a0;--sd-color-secondary-highlight: #5c636a;--sd-color-success-highlight: #228e3b;--sd-color-info-highlight: #148a9c;--sd-color-warning-highlight: #cc986b;--sd-color-danger-highlight: #bb2d3b;--sd-color-light-highlight: #d3d4d5;--sd-color-muted-highlight: #5c636a;--sd-color-dark-highlight: #1c1f23;--sd-color-black-highlight: black;--sd-color-white-highlight: #d9d9d9;--sd-color-primary-bg: rgba(0, 113, 188, 0.2);--sd-color-secondary-bg: rgba(108, 117, 125, 0.2);--sd-color-success-bg: rgba(40, 167, 69, 0.2);--sd-color-info-bg: rgba(23, 162, 184, 0.2);--sd-color-warning-bg: rgba(240, 179, 126, 0.2);--sd-color-danger-bg: rgba(220, 53, 69, 0.2);--sd-color-light-bg: rgba(248, 249, 250, 0.2);--sd-color-muted-bg: rgba(108, 117, 125, 0.2);--sd-color-dark-bg: rgba(33, 37, 41, 0.2);--sd-color-black-bg: rgba(0, 0, 0, 0.2);--sd-color-white-bg: rgba(255, 255, 255, 0.2);--sd-color-primary-text: #fff;--sd-color-secondary-text: #fff;--sd-color-success-text: #fff;--sd-color-info-text: #fff;--sd-color-warning-text: #212529;--sd-color-danger-text: #fff;--sd-color-light-text: #212529;--sd-color-muted-text: #fff;--sd-color-dark-text: #fff;--sd-color-black-text: #fff;--sd-color-white-text: #212529;--sd-color-shadow: rgba(0, 0, 0, 0.15);--sd-color-card-border: rgba(0, 0, 0, 0.125);--sd-color-card-border-hover: hsla(231, 99%, 66%, 1);--sd-color-card-background: transparent;--sd-color-card-text: inherit;--sd-color-card-header: transparent;--sd-color-card-footer: transparent;--sd-color-tabs-label-active: hsla(231, 99%, 66%, 1);--sd-color-tabs-label-hover: hsla(231, 99%, 66%, 1);--sd-color-tabs-label-inactive: hsl(0, 0%, 66%);--sd-color-tabs-underline-active: hsla(231, 99%, 66%, 1);--sd-color-tabs-underline-hover: rgba(178, 206, 245, 0.62);--sd-color-tabs-underline-inactive: transparent;--sd-color-tabs-overline: rgb(222, 222, 222);--sd-color-tabs-underline: rgb(222, 222, 222);--sd-fontsize-tabs-label: 1rem;--sd-fontsize-dropdown: inherit;--sd-fontsize-dropdown-title: 1rem;--sd-fontweight-dropdown-title: 700} diff --git a/_static/sphinx_highlight.js b/_static/sphinx_highlight.js new file mode 100644 index 0000000..8a96c69 --- /dev/null +++ b/_static/sphinx_highlight.js @@ -0,0 +1,154 @@ +/* Highlighting utilities for Sphinx HTML documentation. */ +"use strict"; + +const SPHINX_HIGHLIGHT_ENABLED = true + +/** + * highlight a given string on a node by wrapping it in + * span elements with the given class name. + */ +const _highlight = (node, addItems, text, className) => { + if (node.nodeType === Node.TEXT_NODE) { + const val = node.nodeValue; + const parent = node.parentNode; + const pos = val.toLowerCase().indexOf(text); + if ( + pos >= 0 && + !parent.classList.contains(className) && + !parent.classList.contains("nohighlight") + ) { + let span; + + const closestNode = parent.closest("body, svg, foreignObject"); + const isInSVG = closestNode && closestNode.matches("svg"); + if (isInSVG) { + span = document.createElementNS("http://www.w3.org/2000/svg", "tspan"); + } else { + span = document.createElement("span"); + span.classList.add(className); + } + + span.appendChild(document.createTextNode(val.substr(pos, text.length))); + const rest = document.createTextNode(val.substr(pos + text.length)); + parent.insertBefore( + span, + parent.insertBefore( + rest, + node.nextSibling + ) + ); + node.nodeValue = val.substr(0, pos); + /* There may be more occurrences of search term in this node. So call this + * function recursively on the remaining fragment. + */ + _highlight(rest, addItems, text, className); + + if (isInSVG) { + const rect = document.createElementNS( + "http://www.w3.org/2000/svg", + "rect" + ); + const bbox = parent.getBBox(); + rect.x.baseVal.value = bbox.x; + rect.y.baseVal.value = bbox.y; + rect.width.baseVal.value = bbox.width; + rect.height.baseVal.value = bbox.height; + rect.setAttribute("class", className); + addItems.push({ parent: parent, target: rect }); + } + } + } else if (node.matches && !node.matches("button, select, textarea")) { + node.childNodes.forEach((el) => _highlight(el, addItems, text, className)); + } +}; +const _highlightText = (thisNode, text, className) => { + let addItems = []; + _highlight(thisNode, addItems, text, className); + addItems.forEach((obj) => + obj.parent.insertAdjacentElement("beforebegin", obj.target) + ); +}; + +/** + * Small JavaScript module for the documentation. + */ +const SphinxHighlight = { + + /** + * highlight the search words provided in localstorage in the text + */ + highlightSearchWords: () => { + if (!SPHINX_HIGHLIGHT_ENABLED) return; // bail if no highlight + + // get and clear terms from localstorage + const url = new URL(window.location); + const highlight = + localStorage.getItem("sphinx_highlight_terms") + || url.searchParams.get("highlight") + || ""; + localStorage.removeItem("sphinx_highlight_terms") + url.searchParams.delete("highlight"); + window.history.replaceState({}, "", url); + + // get individual terms from highlight string + const terms = highlight.toLowerCase().split(/\s+/).filter(x => x); + if (terms.length === 0) return; // nothing to do + + // There should never be more than one element matching "div.body" + const divBody = document.querySelectorAll("div.body"); + const body = divBody.length ? divBody[0] : document.querySelector("body"); + window.setTimeout(() => { + terms.forEach((term) => _highlightText(body, term, "highlighted")); + }, 10); + + const searchBox = document.getElementById("searchbox"); + if (searchBox === null) return; + searchBox.appendChild( + document + .createRange() + .createContextualFragment( + '" + ) + ); + }, + + /** + * helper function to hide the search marks again + */ + hideSearchWords: () => { + document + .querySelectorAll("#searchbox .highlight-link") + .forEach((el) => el.remove()); + document + .querySelectorAll("span.highlighted") + .forEach((el) => el.classList.remove("highlighted")); + localStorage.removeItem("sphinx_highlight_terms") + }, + + initEscapeListener: () => { + // only install a listener if it is really needed + if (!DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS) return; + + document.addEventListener("keydown", (event) => { + // bail for input elements + if (BLACKLISTED_KEY_CONTROL_ELEMENTS.has(document.activeElement.tagName)) return; + // bail with special keys + if (event.shiftKey || event.altKey || event.ctrlKey || event.metaKey) return; + if (DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS && (event.key === "Escape")) { + SphinxHighlight.hideSearchWords(); + event.preventDefault(); + } + }); + }, +}; + +_ready(() => { + /* Do not call highlightSearchWords() when we are on the search page. + * It will highlight words from the *previous* search query. + */ + if (typeof Search === "undefined") SphinxHighlight.highlightSearchWords(); + SphinxHighlight.initEscapeListener(); +}); diff --git a/about.html b/about.html new file mode 100644 index 0000000..e35363e --- /dev/null +++ b/about.html @@ -0,0 +1,553 @@ + + + + + + + About psyplot — psyplot documentation + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +
+

About psyplot

+
+

Why psyplot?

+

When visualizing data, one always has to choose:

+
    +
  • Either create the plot with an intuitive graphical user interface (GUI) +(e.g. panoply) but less options for customization and difficult to script

  • +
  • or create the plot from the command line, e.g. via NCL, R or python with more +possibilities for customization and scripting but also less intuitive

  • +
+

psyplot wants to combine these two worlds: create a well-documented and +easy accessible framework to visualize data from a GUI and the command line +(and of course through a script).

+

There exists nothing like that. Of course you can also work with software like +Paraview via the built-in python shell. But, if you really want to explore your +data it is totally not straightforward to access and explore it from within +such a software using numeric functions from numpy, scipy, etc.

+

Therefore I developed this modular framework that can create and customize plots +efficiently with short and comprehensive commands, that can be accessed +through a GUI (see Subprojects) and where you have always a comprehensive +API to access your data.

+

Different from the usual use with matplotlib, which in the end results most of +the time in copy-pasting parts of your code, this software is build on the +don’t repeat yourself principle. Each of the small parts that make up a +visualization, whether it is part of the data evaluation or of the appearance +of the plot, psyplot puts it into a formatoption can be reused when it is +needed.

+

Nevertheless, it’s again a new piece of software. Therefore, if you want to use +it, for sure you need a bit of time to get comfortable with the framework. I +promise to you, it’s worth it. So get started and +please let me know if you have a different opinion.

+
+
+

What it is, and what it is not

+
+

Note

+

First of all, it’s open source! So please, if you don’t agree with the +points below, edit this document and click on Propose File Change and +Create pull request. We can then discuss your changes.

+
+

There are tons of software tools around for visualization, so what is special +about psyplot? The following list should hopefully provide you some guidance.

+
+

What it is

+
    +
  • It is fast. Not necessarily when it comes down to being the fastest +interactive visualization software, but for sure when it comes down to +development time, as it is very user-friendly from the command line. There are +no other software packages that provide a simple and intuitive visualization +such as

    +
    psy.plot.mapplot("my-netcdf-file.nc", lonlatbox="Germany")
    +
    +
    +

    while still providing a very high range of flexible options to adjust the +visualization. No GUI, independent of it’s intuitiveness, can ever beat the +speed of a scientist that knows a bit of coding and how to use the different +formatoptions in psyplot.

    +
  • +
  • it visualizes unstructured grids, +such as ICON or UGRID model data

  • +
  • it automatically decodes CF-conventions

  • +
  • it intuitively integrates the structure of netCDF files. So if you often +work with netCDF files, psyplot might be a good option

  • +
  • it is pythonic. If you are using python anyway, psyplot is worth a try and we +are always keen to help new users getting started.

  • +
  • it is very flexible (I think we made this point already), from command-line +and GUI.

    +
      +
    • We can implement tons of new visualization and data analysis techniques and +you can implement your own.

    • +
    • they are automatically implemented in the GUI

    • +
    • the user can do his statistical and numerical computations with software +like xarray, numpy, scipy, etc. and then use the psyplot visualization +methods in the same script

    • +
    • its modular framework allows to tackle new scientific questions and handle +them in separate psyplot plugins with it’s own formatoptions and +plotting methods

    • +
    +
  • +
  • it will always be free and open-source under the LGPL License.

  • +
+
+
+

What it is not

+

No software can do everything, neither can psyplot. Our main focus on +flexibility, easy command-line usage and the GUI integration inevitably comes +with a few downsides.

+
    +
  • it is not the fastest, because we use matplotlib to be flexible in our +visualization, and this runs on the CPU, rather than the GPU. But if +matplotlib or the standard visualization utilities from R, NCL, etc. are +sufficient for you, you can go with psyplot.

  • +
  • it is not the best for interactive web-applications. Although it would be +pretty simple to set up a backend server with psyplot and tornado or Flask, +for instance, it’s limited to sending rastered image data around, due to the +options provided by matplotlib.

  • +
  • it is not as fast as ncview. psyplot (and psy-view in particular) are +written in the dynamically interpreted python language (which allows the +combination of GUI and command-line, and the high flexibility). But we will +never beat the speed of the (compiled but less flexible) ncview software.

  • +
  • our GUI is not the most interactive one. psyplot is a command-line-first +software, i.e. we put the most effort in making the usage from command-line +and scripts as easy as possible. The GUI is something on top and is limited by +the speed and functionalities of matplotlib (which is, nevertheless, pretty +rich). But we are constantly improving the GUI, see psy-view for instance.

  • +
  • it is not made for statistical visualizations. We will never beat the +possibilities by packages like seaborn or R. The only advantage of psy-reg +over these other software tools, is the possibility to adapt everything using +the full power of matplotlib artists within and outside of the psyplot +framework

  • +
  • it is not the best software for manipulating shapefiles, although some support +of this might come in the future.

  • +
+
+
+
+

About the author

+

I, (Philipp Sommer), work as a Data Scientist at the +Helmholtz-Zentrum Hereon (Germany) in the +Helmholtz Coastal Data Center (HCDC).

+
+
+

License

+

Copyright © 2021 Helmholtz-Zentrum Hereon, 2020-2021 Helmholtz-Zentrum +Geesthacht, 2016-2021 University of Lausanne

+

psyplot is released under the GNU LGPL-3.O license. +See COPYING and COPYING.LESSER in the root of the repository for full +licensing details.

+

This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License version 3.0 as +published by the Free Software Foundation.

+

This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU LGPL-3.0 license for more details.

+

You should have received a copy of the GNU LGPL-3.0 license +along with this program. If not, see https://www.gnu.org/licenses/.

+
+
+ + +
+
+ + + +
+
+
+
+ + + + \ No newline at end of file diff --git a/accessors.html b/accessors.html new file mode 100644 index 0000000..b659993 --- /dev/null +++ b/accessors.html @@ -0,0 +1,610 @@ + + + + + + + xarray Accessors — psyplot documentation + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +
+

xarray Accessors

+

psyplot defines a DataArray and a Dataset +accessor. You can use these accessors (see Xarray Internals) to +visualize your data and to update your plots. The following sections will show +you how to make and update plots with these accessors. The plotmethods of the +accessors are the same as for the psyplot.project.plot object.

+
+

The DatasetAccessor dataset accessor

+

Importing the psyplot package registers a new dataset accessor (see +xarray.register_dataset_accessor()), the +DatasetAccessor. You can access it via the psy +attribute of the Dataset class, i.e.

+ + + + + + +

xarray.Dataset.psy

alias of DatasetAccessor

+

It can be used to visualize the variables in the dataset directly from the +dataset itself, e.g.

+
In [1]: import psyplot
+
+In [2]: ds = psyplot.open_dataset("demo.nc")
+
+In [3]: sp = ds.psy.plot.mapplot(name="t2m", cmap="Reds")
+
+
+_images/docs_dataset_accessor.png +

The variable sp is a psyplot subproject of the current main project.

+
In [4]: print(sp)
+psyplot.project.Project([    arr0: 2-dim DataArray of t2m, with (lat, lon)=(96, 192), lev=1e+05, time=1979-01-31T18:00:00])
+
+
+

Hence, it would be completely equivalent if you type

+
In [5]: import psyplot.project as psyplot
+
+In [6]: sp = psy.plot.mapplot(ds, name="t2m", cmap="Reds")
+
+
+

Note that the DatasetAccessor.plot attribute has the +same plotmethods as the psyplot.project.plot instance.

+
+
+

The InteractiveArray dataarray accessor

+

More advanced then the dataset accessor is the +registered DataArray accessor, the InteractiveArray.

+

As well as the DatasetAccessor, it is registered as the 'psy' +attribute of any DataArray, i.e.

+ + + + + + +

xarray.DataArray.psy

alias of InteractiveArray

+

You can use it for two things:

+
    +
  1. create plots of the array

  2. +
  3. update the plots and the array

  4. +
+
+

Creating plots with the dataarray accessor

+

Just use the plot attribute the accessor.

+
In [7]: import psyplot
+
+In [8]: ds = psyplot.open_dataset("demo.nc")
+
+In [9]: da = ds.t2m[0, 0]
+
+# this is a two dimensional array
+In [10]: print(da)
+<xarray.DataArray 't2m' (lat: 96, lon: 192)> Size: 74kB
+[18432 values with dtype=float32]
+Coordinates:
+  * lon      (lon) float64 2kB 0.0 1.875 3.75 5.625 ... 352.5 354.4 356.2 358.1
+  * lat      (lat) float64 768B 88.57 86.72 84.86 83.0 ... -84.86 -86.72 -88.57
+    lev      float64 8B 1e+05
+    time     datetime64[ns] 8B 1979-01-31T18:00:00
+Attributes:
+    long_name:  Temperature
+    units:      K
+    code:       130
+    table:      128
+    grid_type:  gaussian
+
+# and we can plot it using the mapplot plot method
+In [11]: plotter = da.psy.plot.mapplot()
+
+
+_images/docs_dataarray_accessor_1.png +

The resulting plotter, an instance of the psyplot.plotter.Plotter +class, is the object that visualizes the data array. It can also +be accessed via the da.psy.plotter attribute. Note that the creation of +such a plotter overwrites any previous plotter in the da.psy.plotter +attribute.

+

This methodology does not only work for DataArrays, +but also for multiple DataArrays in a InteractiveList. This data +structure is, for example, used by the +psyplot.project.plot.lineplot plot method to visualize +multiple lines. Consider the following example:

+
In [12]: ds0 = ds.isel(lev=0)  # select a subset of the dataset
+
+# create a list of arrays at different longitudes
+In [13]: l = psyplot.InteractiveList(
+   ....:     [
+   ....:         ds0.t2m.sel(lon=2.35, lat=48.86, method="nearest"),  # Paris
+   ....:         ds0.t2m.sel(lon=13.39, lat=52.52, method="nearest"),  # Berlin
+   ....:         ds0.t2m.sel(lon=-74.01, lat=40.71, method="nearest"),  # NYC
+   ....:     ]
+   ....: )
+   ....: 
+
+In [14]: l.arr_names = ["Paris", "Berlin", "NYC"]
+
+# plot the list
+In [15]: plotter = l.psy.plot.lineplot(xticks="data", xticklabels="%B")
+
+
+_images/docs_dataarray_accessor_2.png +

Note that for the InteractiveList, the psy +attribute is just the list it self. So it would have been equivalent to call

+
In [16]: l.plot.lineplot()
+
+
+
+
+

Updating plots and arrays with the dataarray accessor

+

The InteractiveArray accessor is designed for interactive usage of, +not only the matplotlib figures, but also of the data. If you selected a +subset of a dataset, e.g. via

+
In [17]: da = ds.t2m[0, 0]
+   ....: print(da.time)  # January 1979
+   ....: 
+<xarray.DataArray 'time' ()> Size: 8B
+array('1979-01-31T18:00:00.000000000', dtype='datetime64[ns]')
+Coordinates:
+    lev      float64 8B 1e+05
+    time     datetime64[ns] 8B 1979-01-31T18:00:00
+Attributes:
+    standard_name:  time
+
+
+

You can change to a different slice using the InteractiveArray.update() +method.

+
In [18]: da.psy.base = ds  # tell psyplot the source of the dataarray
+
+In [19]: da.psy.update(time=2)
+   ....: print(da.time)  # changed to March 1979
+   ....: 
+<xarray.DataArray 'time' ()> Size: 8B
+array('1979-03-31T18:00:00.000000000', dtype='datetime64[ns]')
+Coordinates:
+    lev      float64 8B 1e+05
+    time     datetime64[ns] 8B 1979-03-31T18:00:00
+Attributes:
+    standard_name:  time
+
+
+

The da.psy.base = ds command hereby tells the dataarray, where it is +coming from, since this information is not known in the standard +xarray framework.

+
+

Hint

+

You can avoid this, using the DatasetAccessor.create_list() method +of the dataset accessor

+
In [20]: da = ds.psy.create_list(time=0, lev=0, name="t2m")[0]
+   ....: print(da.psy.base is ds)
+   ....: 
+True
+
+
+
+

If you plotted the data, you can also change the formatoptions using the +update() method, e.g.

+
# create plot
+
+
+
In [21]: da.psy.update(cmap="Reds")
+
+
+_images/docs_dataarray_accessor_4.png +

The same holds for the Interactive list

+
In [22]: l.update(
+   ....:     time=slice(
+   ....:         1, 4
+   ....:     ),  # change the data by selecting a subset of the timeslice
+   ....:     title="Subset",  # change a formatoption, the title of the plot
+   ....: )
+   ....: 
+
+
+_images/docs_dataarray_accessor_5.png +
+
+
+ + +
+
+ + + +
+
+
+
+ + + + \ No newline at end of file diff --git a/api.html b/api.html new file mode 100644 index 0000000..24c308c --- /dev/null +++ b/api.html @@ -0,0 +1,956 @@ + + + + + + + API Reference — psyplot documentation + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +
+

API Reference

+
+ +
+
+ + +
+
+ + + +
+
+
+
+ + + + \ No newline at end of file diff --git a/api/psyplot.config.html b/api/psyplot.config.html new file mode 100644 index 0000000..266f27c --- /dev/null +++ b/api/psyplot.config.html @@ -0,0 +1,510 @@ + + + + + + + psyplot.config package — psyplot documentation + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +
+

psyplot.config package

+

Configuration module of the psyplot package

+

This module contains the module for managing rc parameters and the logging. +Default parameters are defined in the rcsetup.defaultParams +dictionary, however you can set up your own configuration in a yaml file (see +psyplot.load_rc_from_file())

+

Data:

+ + + + + + + + + +

config_path

str or None.

logcfg_path

str.

+
+
+psyplot.config.config_path = None
+

str or None. Path to the yaml configuration file (if found). +See psyplot_fname() for further information

+
+
Type:
+

class

+
+
+
+ +
+
+psyplot.config.logcfg_path = '/builds/psyplot/psyplot/psyplot/config/logging.yml'
+

str. Path to the yaml logging configuration file

+
+ +
+

Submodules

+ +
+
+ + +
+
+ + + +
+
+
+
+ + + + \ No newline at end of file diff --git a/api/psyplot.config.logsetup.html b/api/psyplot.config.logsetup.html new file mode 100644 index 0000000..d006ff5 --- /dev/null +++ b/api/psyplot.config.logsetup.html @@ -0,0 +1,454 @@ + + + + + + + <no title> — psyplot documentation + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+ +
+
+ +

Logging configuration module of the psyplot package

+

This module defines the essential functions for setting up the +logging.Logger instances that are used by the psyplot package.

+

Functions:

+ + + + + + +

setup_logging([default_path, default_level, ...])

Setup logging configuration

+
+
+psyplot.config.logsetup.setup_logging(default_path=None, default_level=20, env_key='LOG_PSYPLOT')[source]
+

Setup logging configuration

+
+
Parameters:
+
    +
  • default_path (str) – Default path of the yaml logging configuration file. If None, it +defaults to the ‘logging.yaml’ file in the config directory

  • +
  • default_level (int) – Default: logging.INFO. Default level if default_path does not +exist

  • +
  • env_key (str) – environment variable specifying a different logging file than +default_path (Default: ‘LOG_CFG’)

  • +
+
+
Returns:
+

path – Path to the logging configuration file

+
+
Return type:
+

str

+
+
+

Notes

+

Function taken from +http://victorlin.me/posts/2012/08/26/good-logging-practice-in-python

+
+ + + +
+
+ + + +
+
+
+
+ + + + \ No newline at end of file diff --git a/api/psyplot.config.rcsetup.html b/api/psyplot.config.rcsetup.html new file mode 100644 index 0000000..763f538 --- /dev/null +++ b/api/psyplot.config.rcsetup.html @@ -0,0 +1,1302 @@ + + + + + + + <no title> — psyplot documentation + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+ +
+
+ +

Default management of the psyplot package

+

This module defines the necessary classes, data and functions for the default +configuration of the module. +The structure is motivated and to larger parts taken from the matplotlib +package.

+

Classes:

+ + + + + + + + + +

RcParams(*args, **kwargs)

A dictionary object including validation

SubDict(base, base_str[, pattern, ...])

Class that keeps week reference to the base dictionary

+

Data:

+ + + + + + + + + +

defaultParams

dict with default values and validation functions

rcParams

RcParams instance that stores default formatoptions and configuration settings.

+

Functions:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

get_configdir([name, env_key])

Return the string representing the configuration directory.

psyplot_fname([env_key, fname, if_exists])

Get the location of the config file.

safe_list(iterable)

Function to create a list

validate_bool(b)

Convert b to a boolean or raise

validate_bool_maybe_none(b)

Convert b to a boolean or raise

validate_dict(d)

Validate a dictionary

validate_files_exist(files)

Validate if all pathnames in a given list exists

validate_path_exists(s)

If s is a path, return s, else False

validate_str(s)

Validate a string

validate_stringlist(s)

Validate a list of strings

validate_stringset(*args, **kwargs)

Validate a set of strings

+
+
+class psyplot.config.rcsetup.RcParams(*args, **kwargs)[source]
+

Bases: dict

+

A dictionary object including validation

+

validating functions are defined and associated with rc parameters in +defaultParams

+

This class is essentially the same as in maplotlibs +RcParams but has the additional +find_and_replace() method.

+
+
Parameters:
+
    +
  • defaultParams (dict) – The defaultParams to use (see the defaultParams attribute). +By default, the psyplot.config.rcsetup.defaultParams +dictionary is used

  • +
  • *args – Any key-value pair for the initialization of the dictionary

  • +
  • **kwargs – Any key-value pair for the initialization of the dictionary

  • +
+
+
+

Attributes:

+ + + + + + + + + + + + + + + + + + + + + +

HEADER

defaultParams

descriptions

The description of each keyword in the rcParams dictionary

msg_depr

msg_depr_ignore

validate

Dictionary with validation methods as values

+

Methods:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

catch()

Context manager to reset the rcParams afterwards

connect(key, func)

Connect a function to the given formatoption

copy()

Make sure, the right class is retained

disconnect([key, func])

Disconnect the connections to the an rcParams key

dump([fname, overwrite, include_keys, ...])

Dump this instance to a yaml file

find_all(pattern)

Return the subset of this RcParams dictionary whose keys match, using re.search(), the given pattern.

find_and_replace(*args, **kwargs)

Like find_all() but the given strings are replaced

keys()

Return sorted list of keys.

load_from_file([fname])

Update rcParams from user-defined settings

load_plugins([raise_error])

Load the plotters and defaultParams from the plugins

remove(key, func)

update([E, ]**F)

If E is present and has a .keys() method, then does: for k in E: D[k] = E[k] If E is present and lacks a .keys() method, then does: for k, v in E: D[k] = v In either case, this is followed by: for k in F: D[k] = F[k]

update_from_defaultParams([defaultParams, ...])

Update from the a dictionary like the defaultParams

values()

Return values in order of sorted keys.

+
+
+HEADER = 'Configuration parameters of the psyplot module\n\nYou can copy this file (or parts of it) to another path and save it as\npsyplotrc.yml. The directory should then be stored in the PSYPLOTCONFIGDIR\nenvironment variable.'
+
+ +
+
+catch()[source]
+

Context manager to reset the rcParams afterwards

+

Usage:

+
rcParams['some_key'] = 0
+with rcParams.catch():
+    rcParams['some_key'] = 1
+    assert rcParams['some_key'] == 1
+assert rcParams['some_key'] == 0
+
+
+
+ +
+
+connect(key, func)[source]
+

Connect a function to the given formatoption

+
+
Parameters:
+
    +
  • key (str) – The rcParams key

  • +
  • func (function) – The function that shall be called when the rcParams key changes. +It must accept a single value that is the new value of the +key.

  • +
+
+
+
+ +
+
+copy()[source]
+

Make sure, the right class is retained

+
+ +
+
+property defaultParams
+
+ +
+
+property descriptions
+

The description of each keyword in the rcParams dictionary

+
+ +
+
+disconnect(key=None, func=None)[source]
+

Disconnect the connections to the an rcParams key

+
+
Parameters:
+
    +
  • key (str) – The rcParams key. If None, all keys are used

  • +
  • func (function) – The function that is connected. If None, all functions are +connected

  • +
+
+
+
+ +
+
+dump(fname=None, overwrite=True, include_keys=None, exclude_keys=['project.plotters'], include_descriptions=True, **kwargs)[source]
+

Dump this instance to a yaml file

+
+
Parameters:
+
    +
  • fname (str or None) – file name to write to. If None, the string that would be written +to a file is returned

  • +
  • overwrite (bool) – If True and fname already exists, it will be overwritten

  • +
  • include_keys (None or list of str) – Keys in the dictionary to be included. If None, all keys are +included

  • +
  • exclude_keys (list of str) – Keys from the RcParams instance to be excluded

  • +
  • **kwargs – Any other parameter for the yaml.dump() function

  • +
+
+
Returns:
+

if fname is None, the string is returned. Otherwise, None +is returned

+
+
Return type:
+

str or None

+
+
Raises:
+

IOError – If fname already exists and overwrite is False

+
+
+
+

See also

+

load_from_file

+
+
+ +
+
+find_all(pattern)[source]
+

Return the subset of this RcParams dictionary whose keys match, +using re.search(), the given pattern.

+
+
Parameters:
+

pattern (str) – pattern as suitable for re.compile

+
+
Returns:
+

RcParams instance with entries that match the given pattern

+
+
Return type:
+

RcParams

+
+
+

Notes

+

Changes to the returned dictionary are (different from +find_and_replace() are not propagated to the parent RcParams +dictionary.

+
+

See also

+

find_and_replace

+
+
+ +
+
+find_and_replace(*args, **kwargs)[source]
+

Like find_all() but the given strings are replaced

+

This method returns a dictionary-like object that keeps weak reference +to this rcParams instance. The resulting SubDict instance takes the +keys from this rcParams instance but leaves away what is found in +base_str.

+

*args and **kwargs are determined by the SubDict +class, where the base dictionary is this one.

+
+
Parameters:
+
    +
  • base_str (str or list of str) – Strings that are used as to look for keys to get and set keys in +the base dictionary. If a string does not contain +'%(key)s', it will be appended at the end. '%(key)s' will +be replaced by the specific key for getting and setting an item.

  • +
  • pattern (str) – Default: '.+'. This is the pattern that is inserted for +%(key)s in a base string to look for matches (using the +re module) in the base dictionary. The default pattern +matches everything without white spaces.

  • +
  • pattern_base (str or list or str) – If None, the whatever is given in the base_str is used. +Those strings will be used for generating the final search +patterns. You can specify this parameter by yourself to avoid the +misinterpretation of patterns. For example for a base_str like +'my.str' it is recommended to additionally provide the +pattern_base keyword with 'my\.str'. +Like for base_str, the %(key)s is appended if not already in +the string.

  • +
  • trace (bool) – Default: False. If True, changes in the SubDict are traced back to +the base dictionary. You can change this behaviour also +afterwards by changing the trace attribute

  • +
  • replace (bool) – Default: True. If True, everything but the ‘%(key)s’ part in a +base string is replaced (see examples below)

  • +
+
+
Returns:
+

SubDict with this rcParams instance as reference.

+
+
Return type:
+

SubDict

+
+
+
+

Examples

+

The syntax is the same as for the initialization of the +SubDict class:

+
>>> from psyplot import rcParams
+>>> d = rcParams.find_and_replace(['plotter.baseplotter.',
+...                                'plotter.vector.'])
+>>> print(d['title'])
+None
+
+>>> print(d['arrowsize'])
+1.0
+
+
+
+
+

See also

+

find_all, SubDict

+
+
+ +
+
+keys()[source]
+

Return sorted list of keys.

+
+ +
+
+load_from_file(fname=None)[source]
+

Update rcParams from user-defined settings

+

This function updates the instance with what is found in fname

+
+
Parameters:
+

fname (str) – Path to the yaml configuration file. Possible keys of the +dictionary are defined by config.rcsetup.defaultParams. +If None, the config.rcsetup.psyplot_fname() function is used.

+
+
+
+

See also

+

dump_to_file, psyplot_fname

+
+
+ +
+
+load_plugins(raise_error=False)[source]
+

Load the plotters and defaultParams from the plugins

+

This method loads the plotters attribute and defaultParams +attribute from the plugins that use the entry point specified by +group. Entry points must be objects (or modules) that have a +defaultParams and a plotters attribute.

+
+
Parameters:
+

raise_error (bool) – If True, an error is raised when multiple plugins define the same +plotter or rcParams key. Otherwise only a warning is raised

+
+
+
+ +
+
+msg_depr = '%s is deprecated and replaced with %s; please use the latter.'
+
+ +
+
+msg_depr_ignore = '%s is deprecated and ignored. Use %s'
+
+ +
+
+remove(key, func)[source]
+
+ +
+
+update([E, ]**F) None.  Update D from dict/iterable E and F.[source]
+

If E is present and has a .keys() method, then does: for k in E: D[k] = E[k] +If E is present and lacks a .keys() method, then does: for k, v in E: D[k] = v +In either case, this is followed by: for k in F: D[k] = F[k]

+
+ +
+
+update_from_defaultParams(defaultParams=None, plotters=True)[source]
+

Update from the a dictionary like the defaultParams

+
+
Parameters:
+
    +
  • defaultParams (dict) – The defaultParams like dictionary. If None, the +defaultParams attribute will be updated

  • +
  • plotters (bool) – If True, 'project.plotters' will be updated too

  • +
+
+
+
+ +
+
+property validate
+

Dictionary with validation methods as values

+
+ +
+
+values()[source]
+

Return values in order of sorted keys.

+
+ +
+ +
+
+class psyplot.config.rcsetup.SubDict(base, base_str, pattern='.+', pattern_base=None, trace=False, replace=True)[source]
+

Bases: UserDict, dict

+

Class that keeps week reference to the base dictionary

+

This class is used by the RcParams.find_and_replace() method +to provide an easy handable instance that keeps reference to the +base rcParams dictionary.

+
+
Parameters:
+
    +
  • base (dict) – base dictionary

  • +
  • base_str (str or list of str) – Strings that are used as to look for keys to get and set keys in +the base dictionary. If a string does not contain +'%(key)s', it will be appended at the end. '%(key)s' will +be replaced by the specific key for getting and setting an item.

  • +
  • pattern (str) – Default: '.+'. This is the pattern that is inserted for +%(key)s in a base string to look for matches (using the +re module) in the base dictionary. The default pattern +matches everything without white spaces.

  • +
  • pattern_base (str or list or str) – If None, the whatever is given in the base_str is used. +Those strings will be used for generating the final search +patterns. You can specify this parameter by yourself to avoid the +misinterpretation of patterns. For example for a base_str like +'my.str' it is recommended to additionally provide the +pattern_base keyword with 'my\.str'. +Like for base_str, the %(key)s is appended if not already in +the string.

  • +
  • trace (bool) – Default: False. If True, changes in the SubDict are traced back to +the base dictionary. You can change this behaviour also +afterwards by changing the trace attribute

  • +
  • replace (bool) – Default: True. If True, everything but the ‘%(key)s’ part in a +base string is replaced (see examples below)

  • +
+
+
+

Notes

+
    +
  • If a key of matches multiple strings in base_str, the first +matching one is used.

  • +
  • the SubDict class is (of course) not that efficient as the +base dictionary, since we loop multiple times through it’s +keys

  • +
+
+

Examples

+

Initialization example:

+
>>> from psyplot import rcParams
+>>> d = rcParams.find_and_replace(['plotter.baseplotter.',
+...                                'plotter.vector.'])
+>>> print d['title']
+
+>>> print d['arrowsize']
+1.0
+
+
+

To convert it to a usual dictionary, simply use the data +attribute:

+
>>> d.data
+{'title': None, 'arrowsize': 1.0, ...}
+
+
+

Note that changing one keyword of your SubDict will not change +the base dictionary, unless you set the trace attribute +to True:

+
>>> d['title'] = 'my title'
+>>> print(d['title'])
+my title
+
+>>> print(rcParams['plotter.baseplotter.title'])
+
+>>> d.trace = True
+>>> d['title'] = 'my second title'
+>>> print(d['title'])
+my second title
+>>> print(rcParams['plotter.baseplotter.title'])
+my second title
+
+
+

Furthermore, changing the replace attribute will change how you +can access the keys:

+
>>> d.replace = False
+
+# now setting d['title'] = 'anything' would raise an error (since
+# d.trace is set to True and 'title' is not a key in the rcParams
+# dictionary. Instead we need
+>>> d['plotter.baseplotter.title'] = 'anything'
+
+
+
+ +

Methods:

+ + + + + + + + + + + + + + + + + + +

add_base_str(base_str[, pattern, ...])

Add further base string to this instance

iteritems()

Unsorted iterator over items

iterkeys()

Unsorted iterator over keys

itervalues()

Unsorted iterator over values

update(*args, **kwargs)

Update the dictionary

+

Attributes:

+ + + + + + + + + + + + + + + + + + + + + +

base

dict.

base_str

list of strings.

data

Dictionary representing this SubDict instance

patterns

list of compiled patterns from the base_str attribute, that are used to look for the matching keys in base

replace

bool.

trace

bool.

+
+
+add_base_str(base_str, pattern='.+', pattern_base=None, append=True)[source]
+

Add further base string to this instance

+
+
Parameters:
+
    +
  • base_str (str or list of str) – Strings that are used as to look for keys to get and set keys in +the base dictionary. If a string does not contain +'%(key)s', it will be appended at the end. '%(key)s' will +be replaced by the specific key for getting and setting an item.

  • +
  • pattern (str) – Default: '.+'. This is the pattern that is inserted for +%(key)s in a base string to look for matches (using the +re module) in the base dictionary. The default pattern +matches everything without white spaces.

  • +
  • pattern_base (str or list or str) – If None, the whatever is given in the base_str is used. +Those strings will be used for generating the final search +patterns. You can specify this parameter by yourself to avoid the +misinterpretation of patterns. For example for a base_str like +'my.str' it is recommended to additionally provide the +pattern_base keyword with 'my\.str'. +Like for base_str, the %(key)s is appended if not already in +the string.

  • +
  • append (bool) – If True, the given base_str are appended (i.e. it is first +looked for them in the base dictionary), otherwise they are +put at the beginning

  • +
+
+
+
+ +
+
+base = {}
+

dict. Reference dictionary

+
+ +
+
+base_str = []
+

list of strings. The strings that are used to set and get a specific key +from the base dictionary

+
+ +
+
+property data
+

Dictionary representing this SubDict instance

+
+

See also

+

iteritems

+
+
+ +
+
+iteritems()[source]
+

Unsorted iterator over items

+
+ +
+
+iterkeys()[source]
+

Unsorted iterator over keys

+
+ +
+
+itervalues()[source]
+

Unsorted iterator over values

+
+ +
+
+patterns = []
+

list of compiled patterns from the base_str attribute, that +are used to look for the matching keys in base

+
+ +
+
+property replace
+

bool. If True, matching strings in the base_str +attribute are replaced with an empty string.

+
+ +
+
+trace = False
+

bool. If True, changes are traced back to the base dict

+
+ +
+
+update(*args, **kwargs)[source]
+

Update the dictionary

+
+ +
+ +
+
+psyplot.config.rcsetup.defaultParams
+

dict with default values and validation functions

+
+ +
+
+psyplot.config.rcsetup.get_configdir(name='psyplot', env_key='PSYPLOTCONFIGDIR')[source]
+

Return the string representing the configuration directory.

+

The directory is chosen as follows:

+
    +
  1. If the env_key environment variable is supplied, choose that.

  2. +
+

2a. On Linux and osx, choose '$HOME/.config/' + name.

+

2b. On other platforms, choose '$HOME/.' + name.

+
    +
  1. If the chosen directory exists, use that as the +configuration directory.

  2. +
  3. A directory: return None.

  4. +
+
+
Parameters:
+
    +
  • name (str) – The name of the program

  • +
  • env_key (str) – The environment variable that can be used for the configuration +directory

  • +
+
+
+

Notes

+

This function is motivated by the matplotlib.matplotlib_fname() +function

+
+ +
+
+psyplot.config.rcsetup.psyplot_fname(env_key='PSYPLOTRC', fname='psyplotrc.yml', if_exists=True)[source]
+

Get the location of the config file.

+

The file location is determined in the following order

+
    +
  • $PWD/psyplotrc.yml

  • +
  • environment variable PSYPLOTRC (pointing to the file location or a +directory containing the file psyplotrc.yml)

  • +
  • $PSYPLOTCONFIGDIR/psyplot

  • +
  • On Linux and osx,

    +
    +
      +
    • $HOME/.config/psyplot/psyplotrc.yml

    • +
    +
    +
  • +
  • On other platforms,

    +
    +
      +
    • $HOME/.psyplot/psyplotrc.yml if $HOME is defined.

    • +
    +
    +
  • +
  • Lastly, it looks in $PSYPLOTDATA/psyplotrc.yml for a +system-defined copy.

  • +
+
+
Parameters:
+
    +
  • env_key (str) – The environment variable that can be used for the configuration +directory

  • +
  • fname (str) – The name of the configuration file

  • +
  • if_exists (bool) – If True, the path is only returned if the file exists

  • +
+
+
Returns:
+

None, if no file could be found and if_exists is True, else the path +to the psyplot configuration file

+
+
Return type:
+

None or str

+
+
+

Notes

+

This function is motivated by the matplotlib.matplotlib_fname() +function

+
+ +
+
+psyplot.config.rcsetup.rcParams
+

RcParams instance that stores default +formatoptions and configuration settings.

+
+ +
+
+psyplot.config.rcsetup.safe_list(iterable)[source]
+

Function to create a list

+
+
Parameters:
+

iterable (iterable or anything else) –

Parameter that shall be converted to a list.

+
    +
  • If string or any non-iterable, it will be put into a list

  • +
  • if iterable, it will be converted to a list

  • +
+

+
+
Returns:
+

l put (or converted) into a list

+
+
Return type:
+

list

+
+
+
+ +
+
+psyplot.config.rcsetup.validate_bool(b)[source]
+

Convert b to a boolean or raise

+
+ +
+
+psyplot.config.rcsetup.validate_bool_maybe_none(b)[source]
+

Convert b to a boolean or raise

+
+ +
+
+psyplot.config.rcsetup.validate_dict(d)[source]
+

Validate a dictionary

+
+
Parameters:
+

d (dict or str) – If str, it must be a path to a yaml file

+
+
Return type:
+

dict

+
+
Raises:
+

ValueError

+
+
+
+ +
+
+psyplot.config.rcsetup.validate_files_exist(files)[source]
+

Validate if all pathnames in a given list exists

+
+ +
+
+psyplot.config.rcsetup.validate_path_exists(s)[source]
+

If s is a path, return s, else False

+
+ +
+
+psyplot.config.rcsetup.validate_str(s)[source]
+

Validate a string

+
+
Parameters:
+

s (str)

+
+
Return type:
+

str

+
+
Raises:
+

ValueError

+
+
+
+ +
+
+psyplot.config.rcsetup.validate_stringlist(s)[source]
+

Validate a list of strings

+
+
Parameters:
+

val (iterable of strings)

+
+
Returns:
+

list of str

+
+
Return type:
+

list

+
+
Raises:
+

ValueError

+
+
+
+ +
+
+psyplot.config.rcsetup.validate_stringset(*args, **kwargs)[source]
+

Validate a set of strings

+
+
Parameters:
+

val (iterable of strings)

+
+
Returns:
+

set of str

+
+
Return type:
+

set

+
+
Raises:
+

ValueError

+
+
+
+ + + +
+
+ + + +
+
+
+
+ + + + \ No newline at end of file diff --git a/api/psyplot.data.html b/api/psyplot.data.html new file mode 100644 index 0000000..3cf1271 --- /dev/null +++ b/api/psyplot.data.html @@ -0,0 +1,4245 @@ + + + + + + + <no title> — psyplot documentation + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Data management core routines of psyplot.

+

Classes:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

AbsoluteTimeDecoder(array)

AbsoluteTimeEncoder(array)

ArrayList([iterable, attrs, auto_update, ...])

Base class for creating a list of interactive arrays from a dataset

CFDecoder([ds, x, y, z, t])

Class that interpretes the coordinates and attributes accordings to cf-conventions

DatasetAccessor(ds)

A dataset accessor to interface with the psyplot package

InteractiveArray(xarray_obj, *args, **kwargs)

Interactive psyplot accessor for the data array

InteractiveBase([plotter, arr_name, auto_update])

Class for the communication of a data object with a suitable plotter

InteractiveList(*args, **kwargs)

List of InteractiveArray instances that can be plotted itself

Signal([name, cls_signal])

Signal to connect functions to a specific event

UGridDecoder([ds, x, y, z, t])

Decoder for UGrid data sets

+

Functions:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

decode_absolute_time(times)

encode_absolute_time(times)

get_filename_ds(ds[, dump, paths])

Return the filename of the corresponding to a dataset

get_index_from_coord(coord, base_index)

Function to return the coordinate as integer, integer array or slice

get_tdata(t_format, files)

Get the time information from file names

open_dataset(filename_or_obj[, decode_cf, ...])

Open an instance of xarray.Dataset.

open_mfdataset(paths[, decode_cf, ...])

Open multiple files as a single dataset.

setup_coords([arr_names, sort, dims])

Sets up the arr_names dictionary for the plot

to_netcdf(ds, *args, **kwargs)

Store the given dataset as a netCDF file

to_slice(arr)

Test whether arr is an integer array that can be replaced by a slice

+

Data:

+ + + + + + + + + +

get_fname_funcs

functions to use to extract the file name from a data store

t_patterns

mapping that translates datetime format strings to regex patterns

+
+
+class psyplot.data.AbsoluteTimeDecoder(array)[source]
+

Bases: NDArrayMixin

+

Attributes:

+ + + + + + +

dtype

+
+
+property dtype
+
+ +
+ +
+
+class psyplot.data.AbsoluteTimeEncoder(array)[source]
+

Bases: NDArrayMixin

+

Attributes:

+ + + + + + +

dtype

+
+
+property dtype
+
+ +
+ +
+
+class psyplot.data.ArrayList(iterable=[], attrs={}, auto_update=None, new_name=True)[source]
+

Bases: list

+

Base class for creating a list of interactive arrays from a dataset

+

This list contains and manages InteractiveArray instances

+
+
Parameters:
+
    +
  • iterable (iterable) – The iterable (e.g. another list) defining this list

  • +
  • attrs (dict-like or iterable, optional) – Global attributes of this list

  • +
  • auto_update (bool) – Default: None. A boolean indicating whether this list shall +automatically update the contained arrays when calling the +update() method or not. See also the no_auto_update +attribute. If None, the value from the 'lists.auto_update' +key in the psyplot.rcParams dictionary is used.

  • +
  • new_name (bool or str) – If False, and the arr_name attribute of the new array is +already in the list, a ValueError is raised. +If True and the arr_name attribute of the new array is not +already in the list, the name is not changed. Otherwise, if the +array name is already in use, new_name is set to ‘arr{0}’. +If not True, this will be used for renaming (if the array name of +arr is in use or not). '{0}' is replaced by a counter

  • +
+
+
+

Attributes:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

all_dims

The dimensions for each of the arrays in this list

all_names

The variable names for each of the arrays in this list

arr_names

Names of the arrays (!not of the variables!) in this list

arrays

A list of all the xarray.DataArray instances in this list

coords

Names of the coordinates of the arrays in this list

coords_intersect

Coordinates of the arrays in this list that are used in all arrays

dims

Dimensions of the arrays in this list

dims_intersect

Dimensions of the arrays in this list that are used in all arrays

is_unstructured

A boolean for each array whether it is unstructured or not

logger

logging.Logger of this instance

names

Set of the variable in this list

no_auto_update

bool.

with_plotter

The arrays in this instance that are visualized with a plotter

+

Methods:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

append(value[, new_name])

Append a new array to the list

array_info([dump, paths, attrs, ...])

Get dimension informations on you arrays

copy([deep])

Returns a copy of the list

draw()

Draws all the figures in this instance

extend(iterable[, new_name])

Add further arrays from an iterable to this list

from_dataset(base[, method, default_slice, ...])

Construct an ArrayList instance from an existing base dataset

from_dict(d[, alternative_paths, datasets, ...])

Create a list from the dictionary returned by array_info()

next_available_name([fmt_str, counter])

Create a new array out of the given format string

remove(arr)

Removes an array from the list

rename(arr[, new_name])

Rename an array to find a name that isn't already in the list

start_update([draw])

Conduct the registered plot updates

update([method, dims, fmt, replot, ...])

Update the coordinates and the plot

+
+
+property all_dims
+

The dimensions for each of the arrays in this list

+
+ +
+
+property all_names
+

The variable names for each of the arrays in this list

+
+ +
+
+append(value, new_name=False)[source]
+

Append a new array to the list

+
+
Parameters:
+
    +
  • value (InteractiveBase) – The data object to append to this list

  • +
  • new_name (bool or str) – If False, and the arr_name attribute of the new array is +already in the list, a ValueError is raised. +If True and the arr_name attribute of the new array is not +already in the list, the name is not changed. Otherwise, if the +array name is already in use, new_name is set to ‘arr{0}’. +If not True, this will be used for renaming (if the array name of +arr is in use or not). '{0}' is replaced by a counter

  • +
+
+
Raises:
+
    +
  • ValueError – If it was impossible to find a name that isn’t already in the list

  • +
  • ValueError – If new_name is False and the array is already in the list

  • +
+
+
+
+

See also

+

list.append, extend, rename

+
+
+ +
+
+property arr_names
+

Names of the arrays (!not of the variables!) in this list

+

This attribute can be set with an iterable of unique names to change +the array names of the data objects in this list.

+
+ +
+
+array_info(dump=None, paths=None, attrs=True, standardize_dims=True, pwd=None, use_rel_paths=True, alternative_paths={}, ds_description={'fname', 'store'}, full_ds=True, copy=False, **kwargs)[source]
+

Get dimension informations on you arrays

+

This method returns a dictionary containing informations on the +array in this instance

+
+
Parameters:
+
    +
  • dump (bool) – If True and the dataset has not been dumped so far, it is dumped to +a temporary file or the one generated by paths is used. If it is +False or both, dump and paths are None, no data will be stored. +If it is None and paths is not None, dump is set to True.

  • +
  • paths (iterable or True) – An iterator over filenames to use if a dataset has no filename. +If paths is True, an iterator over temporary files will be +created without raising a warning

  • +
  • attrs (bool, optional) – If True (default), the ArrayList.attrs and +xarray.DataArray.attrs attributes are included in the +returning dictionary

  • +
  • standardize_dims (bool, optional) – If True (default), the real dimension names in the dataset are +replaced by x, y, z and t to be more general.

  • +
  • pwd (str) – Path to the working directory from where the data can be imported. +If None, use the current working directory.

  • +
  • use_rel_paths (bool, optional) – If True (default), paths relative to the current working directory +are used. Otherwise absolute paths to pwd are used

  • +
  • ds_description ('all' or set of {'fname', 'ds', 'num', 'arr', 'store'}) –

    Keys to describe the datasets of the arrays. If all, all keys +are used. The key descriptions are

    +
    +
    fname

    the file name is inserted in the 'fname' key

    +
    +
    store

    the data store class and module is inserted in the 'store' +key

    +
    +
    ds

    the dataset is inserted in the 'ds' key

    +
    +
    num

    The unique number assigned to the dataset is inserted in the +'num' key

    +
    +
    arr

    The array itself is inserted in the 'arr' key

    +
    +
    +

  • +
  • full_ds (bool) – If True and 'ds' is in ds_description, the entire dataset is +included. Otherwise, only the DataArray converted to a dataset is +included

  • +
  • copy (bool) – If True, the arrays and datasets are deep copied

  • +
  • **kwargs – Any other keyword for the to_netcdf() function

  • +
  • path (str, path-like or file-like, optional) – Path to which to save this dataset. File-like objects are only +supported by the scipy engine. If no path is provided, this +function returns the resulting netCDF file as bytes; in this case, +we need to use scipy, which does not support netCDF version 4 (the +default format becomes NETCDF3_64BIT).

  • +
  • mode ({"w", "a"}, default: "w") – Write (‘w’) or append (‘a’) mode. If mode=’w’, any existing file at +this location will be overwritten. If mode=’a’, existing variables +will be overwritten.

  • +
  • format ({"NETCDF4", "NETCDF4_CLASSIC", "NETCDF3_64BIT", "NETCDF3_CLASSIC"}, optional) –

    File format for the resulting netCDF file:

    +
      +
    • NETCDF4: Data is stored in an HDF5 file, using netCDF4 API +features.

    • +
    • NETCDF4_CLASSIC: Data is stored in an HDF5 file, using only +netCDF 3 compatible API features.

    • +
    • NETCDF3_64BIT: 64-bit offset version of the netCDF 3 file format, +which fully supports 2+ GB files, but is only compatible with +clients linked against netCDF version 3.6.0 or later.

    • +
    • NETCDF3_CLASSIC: The classic netCDF 3 file format. It does not +handle 2+ GB files very well.

    • +
    +

    All formats are supported by the netCDF4-python library. +scipy.io.netcdf only supports the last two formats.

    +

    The default format is NETCDF4 if you are saving a file to disk and +have the netCDF4-python library available. Otherwise, xarray falls +back to using scipy to write netCDF files and defaults to the +NETCDF3_64BIT format (scipy does not support netCDF4).

    +

  • +
  • group (str, optional) – Path to the netCDF4 group in the given file to open (only works for +format=’NETCDF4’). The group(s) will be created if necessary.

  • +
  • engine ({"netcdf4", "scipy", "h5netcdf"}, optional) – Engine to use when writing netCDF files. If not provided, the +default engine is chosen based on available dependencies, with a +preference for ‘netcdf4’ if writing to a file on disk.

  • +
  • encoding (dict, optional) –

    Nested dictionary with variable names as keys and dictionaries of +variable specific encodings as values, e.g., +{"my_variable": {"dtype": "int16", "scale_factor": 0.1, +"zlib": True}, ...}. +If encoding is specified the original encoding of the variables of +the dataset is ignored.

    +

    The h5netcdf engine supports both the NetCDF4-style compression +encoding parameters {"zlib": True, "complevel": 9} and the h5py +ones {"compression": "gzip", "compression_opts": 9}. +This allows using any compression plugin installed in the HDF5 +library, e.g. LZF.

    +

  • +
+
+
Returns:
+

An ordered mapping from array names to dimensions and filename +corresponding to the array

+
+
Return type:
+

dict

+
+
+
+

See also

+

from_dict

+
+
+ +
+
+property arrays
+

A list of all the xarray.DataArray instances in this list

+
+ +
+
+property coords
+

Names of the coordinates of the arrays in this list

+
+ +
+
+property coords_intersect
+

Coordinates of the arrays in this list that are used in all arrays

+
+ +
+
+copy(deep=False)[source]
+

Returns a copy of the list

+
+
Parameters:
+

deep (bool) – If False (default), only the list is copied and not the contained +arrays, otherwise the contained arrays are deep copied

+
+
+
+ +
+
+property dims
+

Dimensions of the arrays in this list

+
+ +
+
+property dims_intersect
+

Dimensions of the arrays in this list that are used in all arrays

+
+ +
+
+draw()[source]
+

Draws all the figures in this instance

+
+ +
+
+extend(iterable, new_name=False)[source]
+

Add further arrays from an iterable to this list

+
+
Parameters:
+
    +
  • iterable – Any iterable that contains InteractiveBase instances

  • +
  • new_name (bool or str) – If False, and the arr_name attribute of the new array is +already in the list, a ValueError is raised. +If True and the arr_name attribute of the new array is not +already in the list, the name is not changed. Otherwise, if the +array name is already in use, new_name is set to ‘arr{0}’. +If not True, this will be used for renaming (if the array name of +arr is in use or not). '{0}' is replaced by a counter

  • +
+
+
Raises:
+
    +
  • ValueError – If it was impossible to find a name that isn’t already in the list

  • +
  • ValueError – If new_name is False and the array is already in the list

  • +
+
+
+
+

See also

+

list.extend, append, rename

+
+
+ +
+
+classmethod from_dataset(base, method='isel', default_slice=None, decoder=None, auto_update=None, prefer_list=False, squeeze=True, attrs=None, load=False, **kwargs)[source]
+

Construct an ArrayList instance from an existing base dataset

+
+
Parameters:
+
    +
  • base (xarray.Dataset) – Dataset instance that is used as reference

  • +
  • method ({'isel', None, 'nearest', ...}) – Selection method of the xarray.Dataset to be used for setting the +variables from the informations in dims. +If method is ‘isel’, the xarray.Dataset.isel() method is +used. Otherwise it sets the method parameter for the +xarray.Dataset.sel() method.

  • +
  • auto_update (bool) – Default: None. A boolean indicating whether this list shall +automatically update the contained arrays when calling the +update() method or not. See also the no_auto_update +attribute. If None, the value from the 'lists.auto_update' +key in the psyplot.rcParams dictionary is used.

  • +
  • prefer_list (bool) – If True and multiple variable names pher array are found, the +InteractiveList class is used. Otherwise the arrays are +put together into one InteractiveArray.

  • +
  • default_slice (indexer) – Index (e.g. 0 if method is ‘isel’) that shall be used for +dimensions not covered by dims and furtherdims. If None, the +whole slice will be used. Note that the default_slice is always +based on the isel method.

  • +
  • decoder (CFDecoder or dict) –

    Arguments for the decoder. This can be one of

    +
      +
    • an instance of CFDecoder

    • +
    • a subclass of CFDecoder

    • +
    • a dictionary with keyword-arguments to the automatically +determined decoder class

    • +
    • None to automatically set the decoder

    • +
    +

  • +
  • squeeze (bool, optional) – Default True. If True, and the created arrays have a an axes with +length 1, it is removed from the dimension list (e.g. an array +with shape (3, 4, 1, 5) will be squeezed to shape (3, 4, 5))

  • +
  • attrs (dict, optional) – Meta attributes that shall be assigned to the selected data arrays +(additional to those stored in the base dataset)

  • +
  • load (bool or dict) – If True, load the data from the dataset using the +xarray.DataArray.load() method. If dict, those will +be given to the above mentioned load method

  • +
  • arr_names (string, list of strings or dictionary) –

    Set the unique array names of the resulting arrays and (optionally) +dimensions.

    +
      +
    • if string: same as list of strings (see below). Strings may +include {0} which will be replaced by a counter.

    • +
    • list of strings: those will be used for the array names. The final +number of dictionaries in the return depend in this case on the +dims and **furtherdims

    • +
    • dictionary: +Then nothing happens and an dict version of +arr_names is returned.

    • +
    +

  • +
  • sort (list of strings) – This parameter defines how the dictionaries are ordered. It has no +effect if arr_names is a dictionary (use a +dict for that). It can be a list of +dimension strings matching to the dimensions in dims for the +variable.

  • +
  • dims (dict) – Keys must be variable names of dimensions (e.g. time, level, lat or +lon) or ‘name’ for the variable name you want to choose. +Values must be values of that dimension or iterables of the values +(e.g. lists). Note that strings will be put into a list. +For example dims = {‘name’: ‘t2m’, ‘time’: 0} will result in one plot +for the first time step, whereas dims = {‘name’: ‘t2m’, ‘time’: [0, 1]} +will result in two plots, one for the first (time == 0) and one for the +second (time == 1) time step.

  • +
  • **kwargs – The same as dims (those will update what is specified in dims)

  • +
+
+
Returns:
+

The list with the specified InteractiveArray instances +that hold a reference to the given base

+
+
Return type:
+

ArrayList

+
+
+
+ +
+
+classmethod from_dict(d, alternative_paths={}, datasets=None, pwd=None, ignore_keys=['attrs', 'plotter', 'ds'], only=None, chname={}, **kwargs)[source]
+

Create a list from the dictionary returned by array_info()

+

This classmethod creates an ArrayList instance +from a dictionary containing filename, dimension infos and array names

+
+
Parameters:
+
    +
  • d (dict) – The dictionary holding the data

  • +
  • alternative_paths (dict or list or str) – A mapping from original filenames as used in d to filenames that +shall be used instead. If alternative_paths is not None, +datasets must be None. Paths must be accessible from the current +working directory. +If alternative_paths is a list (or any other iterable) is +provided, the file names will be replaced as they appear in d +(note that this is very unsafe if d is not and dict)

  • +
  • datasets (dict or list or None) – A mapping from original filenames in d to the instances of +xarray.Dataset to use. If it is an iterable, the same +holds as for the alternative_paths parameter

  • +
  • pwd (str) – Path to the working directory from where the data can be imported. +If None, use the current working directory.

  • +
  • ignore_keys (list of str) – Keys specified in this list are ignored and not seen as array +information (note that attrs are used anyway)

  • +
  • only (string, list or callable) –

    Can be one of the following three things:

    +
      +
    • a string that represents a pattern to match the array names +that shall be included

    • +
    • a list of array names to include

    • +
    • a callable with two arguments, a string and a dict such as

      +
      def filter_func(arr_name: str, info: dict): -> bool
      +    '''
      +    Filter the array names
      +
      +    This function should return True if the array shall be
      +    included, else False
      +
      +    Parameters
      +    ----------
      +    arr_name: str
      +        The array name (i.e. the ``arr_name`` attribute)
      +    info: dict
      +        The dictionary with the array informations. Common
      +        keys are ``'name'`` that points to the variable name
      +        and ``'dims'`` that points to the dimensions and
      +        ``'fname'`` that points to the file name
      +    '''
      +    return True or False
      +
      +
      +

      The function should return True if the array shall be +included, else False. This function will also be given to +subsequents instances of InteractiveList objects that +are contained in the returned value

      +
    • +
    +

  • +
  • chname (dict) – A mapping from variable names in the project to variable names +that should be used instead

  • +
  • **kwargs (dict) – Any other parameter from the psyplot.data.open_dataset function

  • +
  • filename_or_obj (str, Path, file-like or DataStore) – Strings and Path objects are interpreted as a path to a netCDF file +or an OpenDAP URL and opened with python-netCDF4, unless the filename +ends with .gz, in which case the file is gunzipped and opened with +scipy.io.netcdf (only netCDF3 supported). Byte-strings or file-like +objects are opened by scipy.io.netcdf (netCDF3) or h5py (netCDF4/HDF).

  • +
  • chunks (int, dict, 'auto' or None, optional) – If chunks is provided, it is used to load the new dataset into dask +arrays. chunks=-1 loads the dataset with dask using a single +chunk for all arrays. chunks={} loads the dataset with dask using +engine preferred chunks if exposed by the backend, otherwise with +a single chunk for all arrays. In order to reproduce the default behavior +of xr.open_zarr(...) use xr.open_dataset(..., engine='zarr', chunks={}). +chunks='auto' will use dask auto chunking taking into account the +engine preferred chunks. See dask chunking for more details.

  • +
  • cache (bool, optional) – If True, cache data loaded from the underlying datastore in memory as +NumPy arrays when accessed to avoid reading from the underlying data- +store multiple times. Defaults to True unless you specify the chunks +argument to use dask, in which case it defaults to False. Does not +change the behavior of coordinates corresponding to dimensions, which +always load their data from disk into a pandas.Index.

  • +
  • decode_cf (bool, optional) – Whether to decode these variables, assuming they were saved according +to CF conventions.

  • +
  • mask_and_scale (bool, optional) – If True, replace array values equal to _FillValue with NA and scale +values according to the formula original_values * scale_factor + +add_offset, where _FillValue, scale_factor and add_offset are +taken from variable attributes (if they exist). If the _FillValue or +missing_value attribute contains multiple values a warning will be +issued and all array values matching one of the multiple values will +be replaced by NA. This keyword may not be supported by all the backends.

  • +
  • decode_times (bool, optional) – If True, decode times encoded in the standard NetCDF datetime format +into datetime objects. Otherwise, leave them encoded as numbers. +This keyword may not be supported by all the backends.

  • +
  • decode_timedelta (bool, optional) – If True, decode variables and coordinates with time units in +{“days”, “hours”, “minutes”, “seconds”, “milliseconds”, “microseconds”} +into timedelta objects. If False, leave them encoded as numbers. +If None (default), assume the same value of decode_time. +This keyword may not be supported by all the backends.

  • +
  • use_cftime (bool, optional) – Only relevant if encoded dates come from a standard calendar +(e.g. “gregorian”, “proleptic_gregorian”, “standard”, or not +specified). If None (default), attempt to decode times to +np.datetime64[ns] objects; if this is not possible, decode times to +cftime.datetime objects. If True, always decode times to +cftime.datetime objects, regardless of whether or not they can be +represented using np.datetime64[ns] objects. If False, always +decode times to np.datetime64[ns] objects; if this is not possible +raise an error. This keyword may not be supported by all the backends.

  • +
  • concat_characters (bool, optional) – If True, concatenate along the last dimension of character arrays to +form string arrays. Dimensions will only be concatenated over (and +removed) if they have no corresponding variable and if they are only +used as the last dimension of character arrays. +This keyword may not be supported by all the backends.

  • +
  • decode_coords (bool or {"coordinates", "all"}, optional) –

    Controls which variables are set as coordinate variables:

    +
      +
    • ”coordinates” or True: Set variables referred to in the +'coordinates' attribute of the datasets or individual variables +as coordinate variables.

    • +
    • ”all”: Set variables referred to in 'grid_mapping', 'bounds' and +other attributes as coordinate variables.

    • +
    +

    Only existing variables can be set as coordinates. Missing variables +will be silently ignored.

    +

  • +
  • drop_variables (str or iterable of str, optional) – A variable or list of variables to exclude from being parsed from the +dataset. This may be useful to drop variables with problems or +inconsistent values.

  • +
  • inline_array (bool, default: False) – How to include the array in the dask task graph. +By default(inline_array=False) the array is included in a task by +itself, and each chunk refers to that task by its key. With +inline_array=True, Dask will instead inline the array directly +in the values of the task graph. See dask.array.from_array().

  • +
  • chunked_array_type (str, optional) – Which chunked array type to coerce this datasets’ arrays to. +Defaults to ‘dask’ if installed, else whatever is registered via the ChunkManagerEnetryPoint system. +Experimental API that should not be relied upon.

  • +
  • from_array_kwargs (dict) – Additional keyword arguments passed on to the ChunkManagerEntrypoint.from_array method used to create +chunked arrays, via whichever chunk manager is specified through the chunked_array_type kwarg. +For example if dask.array.Array() objects are used for chunking, additional kwargs will be passed +to dask.array.from_array(). Experimental API that should not be relied upon.

  • +
  • backend_kwargs (dict) – Additional keyword arguments passed on to the engine open function, +equivalent to **kwargs.

  • +
  • **kwargs

    Additional keyword arguments passed on to the engine open function. +For example:

    +
      +
    • ’group’: path to the netCDF4 group in the given file to open given as +a str,supported by “netcdf4”, “h5netcdf”, “zarr”.

    • +
    • ’lock’: resource lock to use when reading data from disk. Only +relevant when using dask or another form of parallelism. By default, +appropriate locks are chosen to safely read and write files with the +currently active dask scheduler. Supported by “netcdf4”, “h5netcdf”, +“scipy”.

    • +
    +

    See engine open function for kwargs accepted by each specific engine.

    +

  • +
  • engine ({'netcdf4', 'scipy', 'pydap', 'h5netcdf', 'gdal'}, optional) – Engine to use when reading netCDF files. If not provided, the default +engine is chosen based on available dependencies, with a preference for +‘netcdf4’.

  • +
  • gridfile (str) – The path to a separate grid file or a xarray.Dataset instance which +may store the coordinates used in ds

  • +
+
+
Returns:
+

The list with the interactive objects

+
+
Return type:
+

psyplot.data.ArrayList

+
+
+
+

See also

+

from_dataset, array_info

+
+
+ +
+
+property is_unstructured
+

A boolean for each array whether it is unstructured or not

+
+ +
+
+property logger
+

logging.Logger of this instance

+
+ +
+
+property names
+

Set of the variable in this list

+
+ +
+
+next_available_name(fmt_str='arr{0}', counter=None)[source]
+

Create a new array out of the given format string

+
+
Parameters:
+
    +
  • format_str (str) – The base string to use. '{0}' will be replaced by a counter

  • +
  • counter (iterable) – An iterable where the numbers should be drawn from. If None, +range(100) is used

  • +
+
+
Returns:
+

A possible name that is not in the current project

+
+
Return type:
+

str

+
+
+
+ +
+
+property no_auto_update
+

bool. Boolean controlling whether the start_update() +method is automatically called by the update() method

+
+

Examples

+

You can disable the automatic update via

+
>>> with data.no_auto_update:
+...     data.update(time=1)
+...     data.start_update()
+
+
+

To permanently disable the automatic update, simply set

+
>>> data.no_auto_update = True
+>>> data.update(time=1)
+>>> data.no_auto_update = False  # reenable automatical update
+
+
+
+
+ +
+
+remove(arr)[source]
+

Removes an array from the list

+
+
Parameters:
+

arr (str or InteractiveBase) – The array name or the data object in this list to remove

+
+
Raises:
+

ValueError – If no array with the specified array name is in the list

+
+
+
+ +
+
+rename(arr, new_name=True)[source]
+

Rename an array to find a name that isn’t already in the list

+
+
Parameters:
+
    +
  • arr (InteractiveBase) – A InteractiveArray or InteractiveList instance +whose name shall be checked

  • +
  • new_name (bool or str) – If False, and the arr_name attribute of the new array is +already in the list, a ValueError is raised. +If True and the arr_name attribute of the new array is not +already in the list, the name is not changed. Otherwise, if the +array name is already in use, new_name is set to ‘arr{0}’. +If not True, this will be used for renaming (if the array name of +arr is in use or not). '{0}' is replaced by a counter

  • +
+
+
Returns:
+

    +
  • InteractiveBasearr with changed arr_name attribute

  • +
  • bool or None – True, if the array has been renamed, False if not and None if the +array is already in the list

  • +
+

+
+
Raises:
+
    +
  • ValueError – If it was impossible to find a name that isn’t already in the list

  • +
  • ValueError – If new_name is False and the array is already in the list

  • +
+
+
+
+ +
+
+start_update(draw=None)[source]
+

Conduct the registered plot updates

+

This method starts the updates from what has been registered by the +update() method. You can call this method if you did not set the +auto_update parameter when calling the update() method to True +and when the no_auto_update attribute is True.

+
+
Parameters:
+

draw (bool or None) – If True, all the figures of the arrays contained in this list will +be drawn at the end. If None, it defaults to the ‘auto_draw’` +parameter in the psyplot.rcParams dictionary

+
+
+
+

See also

+

no_auto_update, update

+
+
+ +
+
+update(method='isel', dims={}, fmt={}, replot=False, auto_update=False, draw=None, force=False, todefault=False, enable_post=None, **kwargs)[source]
+

Update the coordinates and the plot

+

This method updates all arrays in this list with the given coordinate +values and formatoptions.

+
+
Parameters:
+
    +
  • method ({'isel', None, 'nearest', ...}) – Selection method of the xarray.Dataset to be used for setting the +variables from the informations in dims. +If method is ‘isel’, the xarray.Dataset.isel() method is +used. Otherwise it sets the method parameter for the +xarray.Dataset.sel() method.

  • +
  • dims (dict) – Keys must be variable names of dimensions (e.g. time, level, lat or +lon) or ‘name’ for the variable name you want to choose. +Values must be values of that dimension or iterables of the values +(e.g. lists). Note that strings will be put into a list. +For example dims = {‘name’: ‘t2m’, ‘time’: 0} will result in one plot +for the first time step, whereas dims = {‘name’: ‘t2m’, ‘time’: [0, 1]} +will result in two plots, one for the first (time == 0) and one for the +second (time == 1) time step.

  • +
  • replot (bool) – Boolean that determines whether the data specific formatoptions +shall be updated in any case or not. Note, if dims is not empty +or any coordinate keyword is in **kwargs, this will be set to +True automatically

  • +
  • fmt (dict) – Keys may be any valid formatoption of the formatoptions in the +plotter

  • +
  • force (str, list of str or bool) – If formatoption key (i.e. string) or list of formatoption keys, +thery are definitely updated whether they changed or not. +If True, all the given formatoptions in this call of the are +update() method are updated

  • +
  • todefault (bool) – If True, all changed formatoptions (except the registered ones) +are updated to their default value as stored in the +rc attribute

  • +
  • auto_update (bool) – Boolean determining whether or not the start_update() method +is called after the end.

  • +
  • draw (bool or None) – If True, all the figures of the arrays contained in this list will +be drawn at the end. If None, it defaults to the ‘auto_draw’` +parameter in the psyplot.rcParams dictionary

  • +
  • enable_post (bool) – If not None, enable (True) or disable (False) the +post formatoption in the plotters

  • +
  • **kwargs – Any other formatoption or dimension that shall be updated +(additionally to those in fmt and dims)

  • +
+
+
+

Notes

+

When updating to a new array while trying to set the dimensions at the +same time, you have to specify the new dimensions via the dims +parameter, e.g.:

+
da.psy.update(name='new_name', dims={'new_dim': 3})
+
+
+

if 'new_dim' is not yet a dimension of this array

+

If the no_auto_update attribute is True and the given +auto_update parameter are is False, the update of the plots are +registered and conducted at the next call of the start_update() +method or the next call of this method (if the auto_update parameter +is then True).

+ +
+ +
+
+property with_plotter
+

The arrays in this instance that are visualized with a plotter

+
+ +
+ +
+
+class psyplot.data.CFDecoder(ds=None, x=None, y=None, z=None, t=None)[source]
+

Bases: object

+

Class that interpretes the coordinates and attributes accordings to +cf-conventions

+

Methods:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

can_decode(ds, var)

Class method to determine whether the object can be decoded by this decoder class.

clear_cache()

Clear any cached data.

correct_dims(var[, dims, remove])

Expands the dimensions to match the dims in the variable

decode_coords(ds[, gridfile])

Sets the coordinates and bounds in a dataset

decode_ds(ds, *args, **kwargs)

Static method to decode coordinates and time informations

get_cell_node_coord(var[, coords, axis, nans])

Checks whether the bounds in the variable attribute are triangular

get_coord_idims(coords)

Get the slicers for the given coordinates from the base dataset

get_coord_info(var, dimname, coord, coords, what)

_summary_

get_decoder(ds, var, *args, **kwargs)

Class method to get the right decoder class that can decode the given dataset and variable

get_grid_type_info(var, coords)

Get info on the grid type

get_idims(arr[, coords])

Get the coordinates in the ds dataset as int or slice

get_metadata_for_section(var, section, coords)

Get the metadata for a specific section

get_metadata_for_variable(var[, coords, ...])

Get the metadata information on a variable.

get_metadata_sections(var)

Get the metadata sections for a variable.

get_plotbounds(coord[, kind, ignore_shape])

Get the bounds of a coordinate

get_projection_info(var, coords)

Get info on the projection

get_t(var[, coords])

Get the time coordinate of a variable

get_t_metadata(var, coords)

Get the temporal metadata for a variable.

get_tname(var[, coords])

Get the name of the t-dimension

get_triangles(var[, coords, convert_radian, ...])

Get the triangles for the variable

get_variable_by_axis(var, axis[, coords])

Return the coordinate matching the specified axis

get_x(var[, coords])

Get the x-coordinate of a variable

get_x_metadata(var, coords)

Get the metadata for spatial x-dimension.

get_xname(var[, coords])

Get the name of the x-dimension

get_y(var[, coords])

Get the y-coordinate of a variable

get_y_metadata(var, coords)

Get the metadata for spatial y-dimension.

get_yname(var[, coords])

Get the name of the y-dimension

get_z(var[, coords])

Get the vertical (z-) coordinate of a variable

get_z_metadata(var, coords)

Get the vertical level metadata for a variable.

get_zname(var[, coords])

Get the name of the z-dimension

is_circumpolar(var)

Test if a variable is on a circumpolar grid

is_unstructured(var)

Test if a variable is on an unstructered grid

register_decoder(decoder_class[, pos])

Register a new decoder

standardize_dims(var[, dims])

Replace the coordinate names through x, y, z and t

+

Attributes:

+ + + + + + + + + +

logger

logging.Logger of this instance

supports_spatial_slicing

True if the data of the CFDecoder supports the extraction of a subset of the data based on the indices.

+
+
+classmethod can_decode(ds, var)[source]
+

Class method to determine whether the object can be decoded by this +decoder class.

+
+
Parameters:
+
+
+
Returns:
+

True if the decoder can decode the given array var. Otherwise +False

+
+
Return type:
+

bool

+
+
+

Notes

+

The default implementation returns True for any argument. Subclass this +method to be specific on what type of data your decoder can decode

+
+ +
+
+clear_cache()[source]
+

Clear any cached data. +The default method does nothing but can be reimplemented by subclasses +to clear data has been computed.

+
+ +
+
+correct_dims(var, dims={}, remove=True)[source]
+

Expands the dimensions to match the dims in the variable

+
+
Parameters:
+
    +
  • var (xarray.Variable) – The variable to get the data for

  • +
  • dims (dict) – a mapping from dimension to the slices

  • +
  • remove (bool) – If True, dimensions in dims that are not in the dimensions of +var are removed

  • +
+
+
+
+ +
+
+static decode_coords(ds, gridfile=None)[source]
+

Sets the coordinates and bounds in a dataset

+

This static method sets those coordinates and bounds that are marked +marked in the netCDF attributes as coordinates in ds (without +deleting them from the variable attributes because this information is +necessary for visualizing the data correctly)

+
+
Parameters:
+
    +
  • ds (xarray.Dataset) – The dataset to decode

  • +
  • gridfile (str) – The path to a separate grid file or a xarray.Dataset instance which +may store the coordinates used in ds

  • +
+
+
Returns:
+

ds with additional coordinates

+
+
Return type:
+

xarray.Dataset

+
+
+
+ +
+
+classmethod decode_ds(ds, *args, **kwargs)[source]
+

Static method to decode coordinates and time informations

+

This method interpretes absolute time informations (stored with units +'day as %Y%m%d.%f') and coordinates

+
+
Parameters:
+
    +
  • ds (xarray.Dataset) – The dataset to decode

  • +
  • gridfile (str) – The path to a separate grid file or a xarray.Dataset instance which +may store the coordinates used in ds

  • +
  • decode_times (bool, optional) – If True, decode times encoded in the standard NetCDF datetime +format into datetime objects. Otherwise, leave them encoded as +numbers.

  • +
  • decode_coords (bool, optional) – If True, decode the ‘coordinates’ attribute to identify coordinates +in the resulting dataset.

  • +
+
+
Returns:
+

The decoded dataset

+
+
Return type:
+

xarray.Dataset

+
+
+
+ +
+
+get_cell_node_coord(var, coords=None, axis='x', nans=None)[source]
+

Checks whether the bounds in the variable attribute are triangular

+
+
Parameters:
+
    +
  • var (xarray.Variable or xarray.DataArray) – The variable to check

  • +
  • coords (dict) – Coordinates to use. If None, the coordinates of the dataset in the +ds attribute are used.

  • +
  • axis ({'x', 'y'}) – The spatial axis to check

  • +
  • nans ({None, 'skip', 'only'}) – Determines whether values with nan shall be left (None), skipped +('skip') or shall be the only one returned ('only')

  • +
+
+
Returns:
+

the bounds corrdinate (if existent)

+
+
Return type:
+

xarray.DataArray or None

+
+
+
+ +
+
+get_coord_idims(coords)[source]
+

Get the slicers for the given coordinates from the base dataset

+

This method converts coords to slicers (list of +integers or slice objects)

+
+
Parameters:
+

coords (dict) – A subset of the ds.coords attribute of the base dataset +ds

+
+
Returns:
+

Mapping from coordinate name to integer, list of integer or slice

+
+
Return type:
+

dict

+
+
+
+ +
+
+get_coord_info(var: DataArray, dimname: str, coord: DataArray, coords: Dict, what: str) Dict[str, str][source]
+

_summary_

+
+
Parameters:
+
    +
  • var (xarray.DataArray) – The data array to get the metadata for

  • +
  • dimname (str) – The dimension in the dimension of var

  • +
  • coord (Union[xr.Variable, xr.DataArray]) – The coordinate to get the info from

  • +
  • coords (Dict) – Other coordinates in the dataset

  • +
  • what (str) – The name on what this is all bout

  • +
+
+
Returns:
+

The coordinate infos

+
+
Return type:
+

Dict[str, str]

+
+
Raises:
+

ValueError – When the coordinates specifies boundaries but they could not be + found in the given coords

+
+
+
+ +
+
+classmethod get_decoder(ds, var, *args, **kwargs)[source]
+

Class method to get the right decoder class that can decode the +given dataset and variable

+
+
Parameters:
+
+
+
Returns:
+

The decoder for the given dataset that can decode the variable +var

+
+
Return type:
+

CFDecoder

+
+
+
+ +
+
+get_grid_type_info(var: DataArray, coords: Dict) Dict[str, str][source]
+

Get info on the grid type

+
+
Parameters:
+
    +
  • %(CFDecoder.get_metadata_for_variable.parameters.var)s

  • +
  • coords (Dict) – Other coordinates in the dataset

  • +
+
+
Returns:
+

The info on the grid type

+
+
Return type:
+

Dict[str, str]

+
+
+
+ +
+
+get_idims(arr, coords=None)[source]
+

Get the coordinates in the ds dataset as int or slice

+

This method returns a mapping from the coordinate names of the given +arr to an integer, slice or an array of integer that represent the +coordinates in the ds dataset and can be used to extract the +given arr via the xarray.Dataset.isel() method.

+
+
Parameters:
+
    +
  • arr (xarray.DataArray) – The data array for which to get the dimensions as integers, slices +or list of integers from the dataset in the base attribute

  • +
  • coords (iterable) – The coordinates to use. If not given all coordinates in the +arr.coords attribute are used

  • +
+
+
Returns:
+

Mapping from coordinate name to integer, list of integer or slice

+
+
Return type:
+

dict

+
+
+ +
+ +
+
+get_metadata_for_section(var: DataArray, section: str, coords: Dict) Dict[str, str][source]
+

Get the metadata for a specific section

+
+
Parameters:
+
    +
  • var (xarray.DataArray) – The data array to get the metadata for

  • +
  • section (str) – The section name

  • +
  • coords (Dict) – Other coordinates in the dataset

  • +
+
+
Returns:
+

A mapping from metadata name to section.

+
+
Return type:
+

Dict[str, str]

+
+
+
+ +
+
+get_metadata_for_variable(var: DataArray, coords: Dict | None = None, fail_on_error: bool = False, include_tracebacks: bool = False) Dict[str, Dict[str, str]][source]
+

Get the metadata information on a variable.

+
+
Parameters:
+
    +
  • var (xarray.DataArray) – The data array to get the metadata for

  • +
  • coords (Dict, optional) – The coordinates to use. If none, we’ll fallback to the coordinates +of the base dataset.

  • +
  • fail_on_error (bool, default False) – If True, an error is raised when an error occurs. Otherwise it is +captured and entered as an attribute to the metadata.

  • +
  • include_tracebacks (bool, default False) – If True, the full traceback of the error is included

  • +
+
+
Returns:
+

A mapping from meta data sections for meta data attributes on the +specific section.

+
+
Return type:
+

Dict[str, Dict[str, str]]

+
+
+
+ +
+
+get_metadata_sections(var: DataArray) List[str][source]
+

Get the metadata sections for a variable.

+
+
Parameters:
+

var (xarray.DataArray) – The data array to get the metadata for

+
+
Returns:
+

The sections for the metadata information

+
+
Return type:
+

List[str]

+
+
+
+ +
+
+get_plotbounds(coord, kind=None, ignore_shape=False)[source]
+

Get the bounds of a coordinate

+

This method first checks the 'bounds' attribute of the given +coord and if it fails, it calculates them.

+
+
Parameters:
+
    +
  • coord (xarray.Coordinate) – The coordinate to get the bounds for

  • +
  • kind (str) – The interpolation method (see scipy.interpolate.interp1d()) +that is used in case of a 2-dimensional coordinate

  • +
  • ignore_shape (bool) – If True and the coord has a 'bounds' attribute, this +attribute is returned without further check. Otherwise it is tried +to bring the 'bounds' into a format suitable for (e.g.) the +matplotlib.pyplot.pcolormesh() function.

  • +
+
+
Returns:
+

bounds – The bounds with the same number of dimensions as coord but one +additional array (i.e. if coord has shape (4, ), bounds will +have shape (5, ) and if coord has shape (4, 5), bounds will +have shape (5, 6)

+
+
Return type:
+

np.ndarray

+
+
+
+ +
+
+get_projection_info(var: DataArray, coords: Dict) Dict[str, str][source]
+

Get info on the projection

+
+
Parameters:
+
    +
  • %(CFDecoder.get_metadata_for_variable.parameters.var)s

  • +
  • coords (Dict) – Other coordinates in the dataset

  • +
+
+
Returns:
+

The grid mapping attributes

+
+
Return type:
+

Dict[str, str]

+
+
Raises:
+

KeyError – when the variable specified by the grid_mapping is not part of + the given coords

+
+
+
+ +
+
+get_t(var, coords=None)[source]
+

Get the time coordinate of a variable

+

This method searches for the time coordinate in the ds. It +first checks whether there is one dimension that holds an 'axis' +attribute with ‘T’, otherwise it looks whether there is an intersection +between the t attribute and the variables dimensions, otherwise +it returns the coordinate corresponding to the first dimension of var

+

Possible types

+
    +
  • var (xarray.Variable) – The variable to get the time coordinate for

  • +
  • coords (dict) – Coordinates to use. If None, the coordinates of the dataset in the +ds attribute are used.

  • +
+
+
Returns:
+

The time coordinate or None if no time coordinate could be found

+
+
Return type:
+

xarray.Coordinate or None

+
+
+
+ +
+
+get_t_metadata(var: DataArray, coords: Dict) Dict[str, str][source]
+

Get the temporal metadata for a variable.

+
+
Parameters:
+
    +
  • var (xarray.DataArray) – The data array to get the metadata for

  • +
  • coords (Dict) – The coordinates to use

  • +
+
+
Returns:
+

A mapping from metadata name to section.

+
+
Return type:
+

Dict[str, str]

+
+
+
+ +
+
+get_tname(var, coords=None)[source]
+

Get the name of the t-dimension

+

This method gives the name of the time dimension

+
+
Parameters:
+
    +
  • var (xarray.Variables) – The variable to get the dimension for

  • +
  • coords (dict) – The coordinates to use for checking the axis attribute. If None, +they are not used

  • +
+
+
Returns:
+

The coordinate name or None if no time coordinate could be found

+
+
Return type:
+

str or None

+
+
+
+

See also

+

get_t

+
+
+ +
+
+get_triangles(var, coords=None, convert_radian=True, copy=False, src_crs=None, target_crs=None, nans=None, stacklevel=1)[source]
+

Get the triangles for the variable

+
+
Parameters:
+
    +
  • var (xarray.Variable or xarray.DataArray) – The variable to use

  • +
  • coords (dict) – Alternative coordinates to use. If None, the coordinates of the +ds dataset are used

  • +
  • convert_radian (bool) – If True and the coordinate has units in ‘radian’, those are +converted to degrees

  • +
  • copy (bool) – If True, vertice arrays are copied

  • +
  • src_crs (cartopy.crs.Crs) – The source projection of the data. If not None, a transformation +to the given target_crs will be done

  • +
  • target_crs (cartopy.crs.Crs) – The target projection for which the triangles shall be transformed. +Must only be provided if the src_crs is not None.

  • +
  • nans ({None, 'skip', 'only'}) – Determines whether values with nan shall be left (None), skipped +('skip') or shall be the only one returned ('only')

  • +
+
+
Returns:
+

The spatial triangles of the variable

+
+
Return type:
+

matplotlib.tri.Triangulation

+
+
Raises:
+

ValueError – If src_crs is not None and target_crs is None

+
+
+
+ +
+
+get_variable_by_axis(var, axis, coords=None)[source]
+

Return the coordinate matching the specified axis

+

This method uses to 'axis' attribute in coordinates to return the +corresponding coordinate of the given variable

+

Possible types

+
    +
  • var (xarray.Variable) – The variable to get the dimension for

  • +
  • axis ({‘x’, ‘y’, ‘z’, ‘t’}) – The axis string that identifies the dimension

  • +
  • coords (dict) – Coordinates to use. If None, the coordinates of the dataset in the +ds attribute are used.

  • +
+
+
Returns:
+

The coordinate for var that matches the given axis or None if +no coordinate with the right axis could be found.

+
+
Return type:
+

xarray.Coordinate or None

+
+
+

Notes

+

This is a rather low-level function that only interpretes the +CFConvention. It is used by the get_x(), +get_y(), get_z() and get_t() methods

+
+

Warning

+

If None of the coordinates have an 'axis' attribute, we use the +'coordinate' attribute of var (if existent). +Since however the CF Conventions do not determine the order on how +the coordinates shall be saved, we try to use a pattern matching +for latitude ('lat') and longitude (lon'). If this patterns +do not match, we interpret the coordinates such that x: -1, y: -2, +z: -3. This is all not very safe for awkward dimension names, +but works for most cases. If you want to be a hundred percent sure, +use the x, y, z and t attribute.

+
+
+

See also

+

get_x, get_y, get_z, get_t

+
+
+ +
+
+get_x(var, coords=None)[source]
+

Get the x-coordinate of a variable

+

This method searches for the x-coordinate in the ds. It first +checks whether there is one dimension that holds an 'axis' +attribute with ‘X’, otherwise it looks whether there is an intersection +between the x attribute and the variables dimensions, otherwise +it returns the coordinate corresponding to the last dimension of var

+

Possible types

+
    +
  • var (xarray.Variable) – The variable to get the x-coordinate for

  • +
  • coords (dict) – Coordinates to use. If None, the coordinates of the dataset in the +ds attribute are used.

  • +
+
+
Returns:
+

The y-coordinate or None if it could be found

+
+
Return type:
+

xarray.Coordinate or None

+
+
+
+ +
+
+get_x_metadata(var: DataArray, coords: Dict) Dict[str, str][source]
+

Get the metadata for spatial x-dimension.

+
+
Parameters:
+
    +
  • var (xarray.DataArray) – The data array to get the metadata for

  • +
  • coords (Dict) – The coordinates to use

  • +
+
+
Returns:
+

A mapping from metadata name to section.

+
+
Return type:
+

Dict[str, str]

+
+
+
+ +
+
+get_xname(var, coords=None)[source]
+

Get the name of the x-dimension

+

This method gives the name of the x-dimension (which is not necessarily +the name of the coordinate if the variable has a coordinate attribute)

+
+
Parameters:
+
    +
  • var (xarray.Variables) – The variable to get the dimension for

  • +
  • coords (dict) – The coordinates to use for checking the axis attribute. If None, +they are not used

  • +
+
+
Returns:
+

The coordinate name

+
+
Return type:
+

str

+
+
+
+

See also

+

get_x

+
+
+ +
+
+get_y(var, coords=None)[source]
+

Get the y-coordinate of a variable

+

This method searches for the y-coordinate in the ds. It first +checks whether there is one dimension that holds an 'axis' +attribute with ‘Y’, otherwise it looks whether there is an intersection +between the y attribute and the variables dimensions, otherwise +it returns the coordinate corresponding to the second last dimension of +var (or the last if the dimension of var is one-dimensional)

+

Possible types

+
    +
  • var (xarray.Variable) – The variable to get the y-coordinate for

  • +
  • coords (dict) – Coordinates to use. If None, the coordinates of the dataset in the +ds attribute are used.

  • +
+
+
Returns:
+

The y-coordinate or None if it could be found

+
+
Return type:
+

xarray.Coordinate or None

+
+
+
+ +
+
+get_y_metadata(var: DataArray, coords: Dict) Dict[str, str][source]
+

Get the metadata for spatial y-dimension.

+
+
Parameters:
+
    +
  • var (xarray.DataArray) – The data array to get the metadata for

  • +
  • coords (Dict) – The coordinates to use

  • +
+
+
Returns:
+

A mapping from metadata name to section.

+
+
Return type:
+

Dict[str, str]

+
+
+
+ +
+
+get_yname(var, coords=None)[source]
+

Get the name of the y-dimension

+

This method gives the name of the y-dimension (which is not necessarily +the name of the coordinate if the variable has a coordinate attribute)

+
+
Parameters:
+
    +
  • var (xarray.Variables) – The variable to get the dimension for

  • +
  • coords (dict) – The coordinates to use for checking the axis attribute. If None, +they are not used

  • +
+
+
Returns:
+

The coordinate name

+
+
Return type:
+

str

+
+
+
+

See also

+

get_y

+
+
+ +
+
+get_z(var, coords=None)[source]
+

Get the vertical (z-) coordinate of a variable

+

This method searches for the z-coordinate in the ds. It first +checks whether there is one dimension that holds an 'axis' +attribute with ‘Z’, otherwise it looks whether there is an intersection +between the z attribute and the variables dimensions, otherwise +it returns the coordinate corresponding to the third last dimension of +var (or the second last or last if var is two or one-dimensional)

+

Possible types

+
    +
  • var (xarray.Variable) – The variable to get the z-coordinate for

  • +
  • coords (dict) – Coordinates to use. If None, the coordinates of the dataset in the +ds attribute are used.

  • +
+
+
Returns:
+

The z-coordinate or None if no z coordinate could be found

+
+
Return type:
+

xarray.Coordinate or None

+
+
+
+ +
+
+get_z_metadata(var: DataArray, coords: Dict) Dict[str, str][source]
+

Get the vertical level metadata for a variable.

+
+
Parameters:
+
    +
  • var (xarray.DataArray) – The data array to get the metadata for

  • +
  • coords (Dict) – The coordinates to use

  • +
+
+
Returns:
+

A mapping from metadata name to section.

+
+
Return type:
+

Dict[str, str]

+
+
+
+ +
+
+get_zname(var, coords=None)[source]
+

Get the name of the z-dimension

+

This method gives the name of the z-dimension (which is not necessarily +the name of the coordinate if the variable has a coordinate attribute)

+
+
Parameters:
+
    +
  • var (xarray.Variables) – The variable to get the dimension for

  • +
  • coords (dict) – The coordinates to use for checking the axis attribute. If None, +they are not used

  • +
+
+
Returns:
+

The coordinate name or None if no vertical coordinate could be +found

+
+
Return type:
+

str or None

+
+
+
+

See also

+

get_z

+
+
+ +
+
+is_circumpolar(var)[source]
+

Test if a variable is on a circumpolar grid

+
+
Parameters:
+
    +
  • var (xarray.Variable or xarray.DataArray) – The variable to check

  • +
  • coords (dict) – Coordinates to use. If None, the coordinates of the dataset in the +ds attribute are used.

  • +
  • axis ({'x', 'y'}) – The spatial axis to check

  • +
  • nans ({None, 'skip', 'only'}) – Determines whether values with nan shall be left (None), skipped +('skip') or shall be the only one returned ('only')

  • +
+
+
Returns:
+

the bounds corrdinate (if existent)

+
+
Return type:
+

xarray.DataArray or None

+
+
+
+ +
+
+is_unstructured(var)[source]
+

Test if a variable is on an unstructered grid

+
+
Parameters:
+
    +
  • var (xarray.Variable or xarray.DataArray) – The variable to check

  • +
  • coords (dict) – Coordinates to use. If None, the coordinates of the dataset in the +ds attribute are used.

  • +
  • axis ({'x', 'y'}) – The spatial axis to check

  • +
  • nans ({None, 'skip', 'only'}) – Determines whether values with nan shall be left (None), skipped +('skip') or shall be the only one returned ('only')

  • +
+
+
Returns:
+

the bounds corrdinate (if existent)

+
+
Return type:
+

xarray.DataArray or None

+
+
+

Notes

+

Currently this is the same as is_unstructured() method, but may +change in the future to support hexagonal grids

+
+ +
+
+property logger
+

logging.Logger of this instance

+
+ +
+
+static register_decoder(decoder_class, pos=0)[source]
+

Register a new decoder

+

This function registeres a decoder class to use

+
+
Parameters:
+
    +
  • decoder_class (type) – The class inherited from the CFDecoder

  • +
  • pos (int) – The position where to register the decoder (by default: the first +position

  • +
+
+
+
+ +
+
+standardize_dims(var, dims={})[source]
+

Replace the coordinate names through x, y, z and t

+
+
Parameters:
+
    +
  • var (xarray.Variable) – The variable to use the dimensions of

  • +
  • dims (dict) – The dictionary to use for replacing the original dimensions

  • +
+
+
Returns:
+

The dictionary with replaced dimensions

+
+
Return type:
+

dict

+
+
+
+ +
+
+supports_spatial_slicing = True
+

True if the data of the CFDecoder supports the extraction of a subset of +the data based on the indices.

+
+ +
+ +
+
+class psyplot.data.DatasetAccessor(ds)[source]
+

Bases: object

+

A dataset accessor to interface with the psyplot package

+

Methods:

+ + + + + + + + + + + + +

copy([deep])

Copy the array

create_list(*args, **kwargs)

Create a psyplot.data.ArrayList with arrays from this dataset

to_array(*args, **kwargs)

Deprecated version of to_dataarray

+

Attributes:

+ + + + + + + + + + + + + + + +

data_store

The xarray.backends.common.AbstractStore used to save the dataset

filename

The name of the file that stores this dataset

num

A unique number for the dataset

plot

An object to generate new plots from this dataset

+
+
+copy(deep=False)[source]
+

Copy the array

+

This method returns a copy of the underlying array in the arr +attribute. It is more stable because it creates a new psy accessor

+
+ +
+
+create_list(*args, **kwargs)[source]
+

Create a psyplot.data.ArrayList with arrays from this dataset

+
+
Parameters:
+
    +
  • base (xarray.Dataset) – Dataset instance that is used as reference

  • +
  • method ({'isel', None, 'nearest', ...}) – Selection method of the xarray.Dataset to be used for setting the +variables from the informations in dims. +If method is ‘isel’, the xarray.Dataset.isel() method is +used. Otherwise it sets the method parameter for the +xarray.Dataset.sel() method.

  • +
  • auto_update (bool) – Default: None. A boolean indicating whether this list shall +automatically update the contained arrays when calling the +update() method or not. See also the no_auto_update +attribute. If None, the value from the 'lists.auto_update' +key in the psyplot.rcParams dictionary is used.

  • +
  • prefer_list (bool) – If True and multiple variable names pher array are found, the +InteractiveList class is used. Otherwise the arrays are +put together into one InteractiveArray.

  • +
  • default_slice (indexer) – Index (e.g. 0 if method is ‘isel’) that shall be used for +dimensions not covered by dims and furtherdims. If None, the +whole slice will be used. Note that the default_slice is always +based on the isel method.

  • +
  • decoder (CFDecoder or dict) –

    Arguments for the decoder. This can be one of

    +
      +
    • an instance of CFDecoder

    • +
    • a subclass of CFDecoder

    • +
    • a dictionary with keyword-arguments to the automatically +determined decoder class

    • +
    • None to automatically set the decoder

    • +
    +

  • +
  • squeeze (bool, optional) – Default True. If True, and the created arrays have a an axes with +length 1, it is removed from the dimension list (e.g. an array +with shape (3, 4, 1, 5) will be squeezed to shape (3, 4, 5))

  • +
  • attrs (dict, optional) – Meta attributes that shall be assigned to the selected data arrays +(additional to those stored in the base dataset)

  • +
  • load (bool or dict) – If True, load the data from the dataset using the +xarray.DataArray.load() method. If dict, those will +be given to the above mentioned load method

  • +
  • arr_names (string, list of strings or dictionary) –

    Set the unique array names of the resulting arrays and (optionally) +dimensions.

    +
      +
    • if string: same as list of strings (see below). Strings may +include {0} which will be replaced by a counter.

    • +
    • list of strings: those will be used for the array names. The final +number of dictionaries in the return depend in this case on the +dims and **furtherdims

    • +
    • dictionary: +Then nothing happens and an dict version of +arr_names is returned.

    • +
    +

  • +
  • sort (list of strings) – This parameter defines how the dictionaries are ordered. It has no +effect if arr_names is a dictionary (use a +dict for that). It can be a list of +dimension strings matching to the dimensions in dims for the +variable.

  • +
  • dims (dict) – Keys must be variable names of dimensions (e.g. time, level, lat or +lon) or ‘name’ for the variable name you want to choose. +Values must be values of that dimension or iterables of the values +(e.g. lists). Note that strings will be put into a list. +For example dims = {‘name’: ‘t2m’, ‘time’: 0} will result in one plot +for the first time step, whereas dims = {‘name’: ‘t2m’, ‘time’: [0, 1]} +will result in two plots, one for the first (time == 0) and one for the +second (time == 1) time step.

  • +
  • **kwargs – The same as dims (those will update what is specified in dims)

  • +
+
+
Returns:
+

The list with the specified InteractiveArray instances +that hold a reference to the given base

+
+
Return type:
+

ArrayList

+
+
+ +
+ +
+
+property data_store
+

The xarray.backends.common.AbstractStore used to save the +dataset

+
+ +
+
+property filename
+

The name of the file that stores this dataset

+
+ +
+
+property num
+

A unique number for the dataset

+
+ +
+
+property plot
+

An object to generate new plots from this dataset

+

To make a 2D-plot with the psy-simple +plugin, you can just type

+
project = ds.psy.plot.plot2d(name='variable-name')
+
+
+

It will create a new subproject with the extracted and visualized data.

+
+

See also

+
+
psyplot.project.DatasetPlotter

for the different plot methods

+
+
+
+
+ +
+
+to_array(*args, **kwargs)[source]
+

Deprecated version of to_dataarray

+
+ +
+ +
+
+class psyplot.data.InteractiveArray(xarray_obj, *args, **kwargs)[source]
+

Bases: InteractiveBase

+

Interactive psyplot accessor for the data array

+

This class keeps reference to the base xarray.Dataset where the +array.DataArray originates from and enables to switch between the +coordinates in the array. Furthermore it has a plotter attribute to +enable interactive plotting via an psyplot.plotter.Plotter +instance.

+

The *args and **kwargs are essentially the same as for the +xarray.DataArray method, additional **kwargs are +described below.

+
+
Parameters:
+
    +
  • base (xarray.Dataset) – Default: None. Dataset that serves as the origin of the data +contained in this DataArray instance. This will be used if you want +to update the coordinates via the update() method. If None, +this instance will serve as a base as soon as it is needed.

  • +
  • decoder (psyplot.CFDecoder) – The decoder that decodes the base dataset and is used to get +bounds. If not given, a new CFDecoder is created

  • +
  • idims (dict) – Default: None. dictionary with integer values and/or slices in the +base dictionary. If not given, they are determined automatically

  • +
  • plotter (Plotter) – Default: None. Interactive plotter that makes the plot via +formatoption keywords.

  • +
  • arr_name (str) – Default: 'data'. unique string of the array

  • +
  • auto_update (bool) – Default: None. A boolean indicating whether this list shall +automatically update the contained arrays when calling the +update() method or not. See also the no_auto_update +attribute. If None, the value from the 'lists.auto_update' +key in the psyplot.rcParams dictionary is used.

  • +
+
+
+

Attributes:

+ + + + + + + + + + + + + + + + + + + + + + + + +

base

Base dataset this instance gets its data from

base_variables

A mapping from the variable name to the variablein the base dataset.

decoder

The decoder of this array

idims

Coordinates in the base dataset as int or slice

iter_base_variables

An iterator over the base variables in the base dataset

logger

logging.Logger of this instance

onbasechange

Signal to be emiited when the base of the object changes

+

Methods:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

copy([deep])

Copy the array

fldmean([keepdims])

Calculate the weighted mean over the x- and y-dimension

fldpctl(q[, keepdims])

Calculate the percentiles along the x- and y-dimensions

fldstd([keepdims])

Calculate the weighted standard deviation over x- and y-dimension

get_coord(what[, base])

The x-coordinate of this data array

get_dim(what[, base])

The name of the x-dimension of this data array

gridweights([keepdims, keepshape, use_cdo])

Calculate the cell weights for each grid cell

init_accessor([base, idims, decoder])

Initialize the accessor instance

isel(*args, **kwargs)

Select a subset of the array based on position.

sel(*args, **kwargs)

Select a subset of the array based on indexes.

shiftlon(central_longitude)

Shift longitudes and the data so that they match map projection region.

start_update([draw, queues])

Conduct the formerly registered updates

to_interactive_list()

Return a InteractiveList that contains this object

update([method, dims, fmt, replot, ...])

Update the coordinates and the plot

+
+
+property base
+

Base dataset this instance gets its data from

+
+ +
+
+property base_variables
+

A mapping from the variable name to the variablein the base +dataset.

+
+ +
+
+copy(deep=False)[source]
+

Copy the array

+

This method returns a copy of the underlying array in the arr +attribute. It is more stable because it creates a new psy accessor

+
+ +
+
+property decoder
+

The decoder of this array

+
+ +
+
+fldmean(keepdims=False)[source]
+

Calculate the weighted mean over the x- and y-dimension

+

This method calculates the weighted mean of the spatial dimensions. +Weights are calculated using the gridweights() method, missing +values are ignored. x- and y-dimensions are identified using the +decoder`s :meth:`~CFDecoder.get_xname and +get_yname() methods.

+
+
Parameters:
+

keepdims (bool) – If True, the dimensionality of this array is maintained

+
+
Returns:
+

The computed fldmeans. The dimensions are the same as in this +array, only the spatial dimensions are omitted if keepdims is +False.

+
+
Return type:
+

xr.DataArray

+
+
+
+

See also

+
+
fldstd

For calculating the weighted standard deviation

+
+
fldpctl

For calculating weighted percentiles

+
+
+
+
+ +
+
+fldpctl(q, keepdims=False)[source]
+

Calculate the percentiles along the x- and y-dimensions

+

This method calculates the specified percentiles along the given +dimension. Percentiles are weighted by the gridweights() method +and missing values are ignored. x- and y-dimensions are estimated +through the decoder`s :meth:`~CFDecoder.get_xname and +get_yname() methods

+
+
Parameters:
+
    +
  • q (float or list of floats between 0 and 100) – The quantiles to estimate

  • +
  • keepdims (bool) – If True, the number of dimensions of the array are maintained

  • +
+
+
Returns:
+

The data array with the dimensions. If q is a list or keepdims +is True, the first dimension will be the percentile 'pctl'. +The other dimensions are the same as in this array, only the +spatial dimensions are omitted if keepdims is False.

+
+
Return type:
+

xr.DataArray

+
+
+
+

See also

+
+
fldstd

For calculating the weighted standard deviation

+
+
fldmean

For calculating the weighted mean

+
+
+
+
+

Warning

+

This method does load the entire array into memory! So take care if you +handle big data.

+
+
+ +
+
+fldstd(keepdims=False)[source]
+

Calculate the weighted standard deviation over x- and y-dimension

+

This method calculates the weighted standard deviation of the spatial +dimensions. Weights are calculated using the gridweights() +method, missing values are ignored. x- and y-dimensions are identified +using the decoder`s :meth:`~CFDecoder.get_xname and +get_yname() methods.

+
+
Parameters:
+

keepdims (bool) – If True, the dimensionality of this array is maintained

+
+
Returns:
+

The computed standard deviations. The dimensions are the same as +in this array, only the spatial dimensions are omitted if +keepdims is False.

+
+
Return type:
+

xr.DataArray

+
+
+
+

See also

+
+
fldmean

For calculating the weighted mean

+
+
fldpctl

For calculating weighted percentiles

+
+
+
+
+ +
+
+get_coord(what, base=False)[source]
+

The x-coordinate of this data array

+
+
Parameters:
+
    +
  • what ({'t', 'x', 'y', 'z'}) – The letter of the axis

  • +
  • base (bool) – If True, use the base variable in the base dataset.

  • +
+
+
+
+ +
+
+get_dim(what, base=False)[source]
+

The name of the x-dimension of this data array

+
+
Parameters:
+
    +
  • what ({'t', 'x', 'y', 'z'}) – The letter of the axis

  • +
  • base (bool) – If True, use the base variable in the base dataset.

  • +
+
+
+
+ +
+
+gridweights(keepdims=False, keepshape=False, use_cdo=None)[source]
+

Calculate the cell weights for each grid cell

+
+
Parameters:
+
    +
  • keepdims (bool) – If True, keep the number of dimensions

  • +
  • keepshape (bool) – If True, keep the exact shape as the source array and the missing +values in the array are masked

  • +
  • use_cdo (bool or None) – If True, use Climate Data Operators (CDOs) to calculate the +weights. Note that this is used automatically for unstructured +grids. If None, it depends on the 'gridweights.use_cdo' +item in the psyplot.rcParams.

  • +
+
+
Returns:
+

The 2D-DataArray with the grid weights

+
+
Return type:
+

xarray.DataArray

+
+
+
+ +
+
+property idims
+

Coordinates in the base dataset as int or slice

+

This attribute holds a mapping from the coordinate names of this +array to an integer, slice or an array of integer that represent the +coordinates in the base dataset

+
+ +
+
+init_accessor(base=None, idims=None, decoder=None, *args, **kwargs)[source]
+

Initialize the accessor instance

+

This method initializes the accessor

+
+
Parameters:
+
    +
  • base (xr.Dataset) – The base dataset for the data

  • +
  • idims (dict) – A mapping from dimension name to indices. If not provided, it is +calculated when the idims attribute is accessed

  • +
  • decoder (CFDecoder) – The decoder of this object

  • +
  • %(InteractiveBase.parameters)s

  • +
+
+
+
+ +
+
+isel(*args, **kwargs)[source]
+

Select a subset of the array based on position.

+

Same method as xarray.DataArray.isel() but keeps information on +the base dataset.

+
+ +
+
+property iter_base_variables
+

An iterator over the base variables in the base dataset

+
+ +
+
+property logger
+

logging.Logger of this instance

+
+ +
+
+onbasechange
+

Signal to be emiited when the base of the object changes

+
+ +
+
+sel(*args, **kwargs)[source]
+

Select a subset of the array based on indexes.

+

Same method as xarray.DataArray.sel() but keeps information on +the base dataset.

+
+ +
+
+shiftlon(central_longitude)[source]
+

Shift longitudes and the data so that they match map projection region.

+

Only valid for cylindrical/pseudo-cylindrical global projections and +data on regular lat/lon grids. longitudes need to be 1D.

+
+
Parameters:
+

central_longitude – center of map projection region

+
+
+

References

+

This function is copied and taken from the +mpl_toolkits.basemap.Basemap class. The only difference is +that we do not mask values outside the map projection region

+
+ +
+
+start_update(draw=None, queues=None)[source]
+

Conduct the formerly registered updates

+

This method conducts the updates that have been registered via the +update() method. You can call this method if the +no_auto_update attribute of this instance is True and the +auto_update parameter in the update() method has been set to +False

+
+
Parameters:
+
    +
  • draw (bool or None) – Boolean to control whether the figure of this array shall be drawn +at the end. If None, it defaults to the ‘auto_draw’` parameter +in the psyplot.rcParams dictionary

  • +
  • queues (list of Queue.Queue instances) – The queues that are passed to the +psyplot.plotter.Plotter.start_update() method to ensure a +thread-safe update. It can be None if only one single plotter is +updated at the same time. The number of jobs that are taken from +the queue is determined by the _njobs() attribute. Note that +there this parameter is automatically configured when updating +from a Project.

  • +
+
+
Returns:
+

A boolean indicating whether a redrawing is necessary or not

+
+
Return type:
+

bool

+
+
+
+

See also

+

no_auto_update, update

+
+
+ +
+
+to_interactive_list()[source]
+

Return a InteractiveList that contains this object

+
+ +
+
+update(method='isel', dims={}, fmt={}, replot=False, auto_update=False, draw=None, force=False, todefault=False, **kwargs)[source]
+

Update the coordinates and the plot

+

This method updates all arrays in this list with the given coordinate +values and formatoptions.

+
+
Parameters:
+
    +
  • method ({'isel', None, 'nearest', ...}) – Selection method of the xarray.Dataset to be used for setting the +variables from the informations in dims. +If method is ‘isel’, the xarray.Dataset.isel() method is +used. Otherwise it sets the method parameter for the +xarray.Dataset.sel() method.

  • +
  • dims (dict) – Keys must be variable names of dimensions (e.g. time, level, lat or +lon) or ‘name’ for the variable name you want to choose. +Values must be values of that dimension or iterables of the values +(e.g. lists). Note that strings will be put into a list. +For example dims = {‘name’: ‘t2m’, ‘time’: 0} will result in one plot +for the first time step, whereas dims = {‘name’: ‘t2m’, ‘time’: [0, 1]} +will result in two plots, one for the first (time == 0) and one for the +second (time == 1) time step.

  • +
  • replot (bool) – Boolean that determines whether the data specific formatoptions +shall be updated in any case or not. Note, if dims is not empty +or any coordinate keyword is in **kwargs, this will be set to +True automatically

  • +
  • fmt (dict) – Keys may be any valid formatoption of the formatoptions in the +plotter

  • +
  • force (str, list of str or bool) – If formatoption key (i.e. string) or list of formatoption keys, +thery are definitely updated whether they changed or not. +If True, all the given formatoptions in this call of the are +update() method are updated

  • +
  • todefault (bool) – If True, all changed formatoptions (except the registered ones) +are updated to their default value as stored in the +rc attribute

  • +
  • auto_update (bool) – Boolean determining whether or not the start_update() method +is called after the end.

  • +
  • draw (bool or None) – Boolean to control whether the figure of this array shall be drawn +at the end. If None, it defaults to the ‘auto_draw’` parameter +in the psyplot.rcParams dictionary

  • +
  • queues (list of Queue.Queue instances) – The queues that are passed to the +psyplot.plotter.Plotter.start_update() method to ensure a +thread-safe update. It can be None if only one single plotter is +updated at the same time. The number of jobs that are taken from +the queue is determined by the _njobs() attribute. Note that +there this parameter is automatically configured when updating +from a Project.

  • +
  • **kwargs – Any other formatoption or dimension that shall be updated +(additionally to those in fmt and dims)

  • +
+
+
+

Notes

+

When updating to a new array while trying to set the dimensions at the +same time, you have to specify the new dimensions via the dims +parameter, e.g.:

+
da.psy.update(name='new_name', dims={'new_dim': 3})
+
+
+

if 'new_dim' is not yet a dimension of this array

+

If the no_auto_update attribute is True and the given +auto_update parameter are is False, the update of the plots are +registered and conducted at the next call of the start_update() +method or the next call of this method (if the auto_update parameter +is then True).

+
+ +
+ +
+
+class psyplot.data.InteractiveBase(plotter=None, arr_name='arr0', auto_update=None)[source]
+

Bases: object

+

Class for the communication of a data object with a suitable plotter

+

This class serves as an interface for data objects (in particular as a +base for InteractiveArray and InteractiveList) to +communicate with the corresponding Plotter in the +plotter attribute

+
+
Parameters:
+
    +
  • plotter (Plotter) – Default: None. Interactive plotter that makes the plot via +formatoption keywords.

  • +
  • arr_name (str) – Default: 'data'. unique string of the array

  • +
  • auto_update (bool) – Default: None. A boolean indicating whether this list shall +automatically update the contained arrays when calling the +update() method or not. See also the no_auto_update +attribute. If None, the value from the 'lists.auto_update' +key in the psyplot.rcParams dictionary is used.

  • +
+
+
+

Attributes:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + +

arr_name

str.

ax

The matplotlib axes the plotter of this data object plots on

block_signals

Block the emitting of signals of this instance

logger

logging.Logger of this instance

no_auto_update

bool.

onupdate

Signal to be emitted when the object has been updated

plot

An object to visualize this data object

plotter

psyplot.plotter.Plotter instance that makes the interactive plotting of the data

+

Methods:

+ + + + + + + + + + + + +

start_update([draw, queues])

Conduct the formerly registered updates

to_interactive_list()

Return a InteractiveList that contains this object

update([fmt, replot, draw, auto_update, ...])

Update the coordinates and the plot

+
+
+property arr_name
+

str. The internal name of the InteractiveBase

+
+ +
+
+property ax
+

The matplotlib axes the plotter of this data object plots on

+
+ +
+
+property block_signals
+

Block the emitting of signals of this instance

+
+ +
+
+property logger
+

logging.Logger of this instance

+
+ +
+
+property no_auto_update
+

bool. Boolean controlling whether the start_update() +method is automatically called by the update() method

+
+

Examples

+

You can disable the automatic update via

+
>>> with data.no_auto_update:
+...     data.update(time=1)
+...     data.start_update()
+
+
+

To permanently disable the automatic update, simply set

+
>>> data.no_auto_update = True
+>>> data.update(time=1)
+>>> data.no_auto_update = False  # reenable automatical update
+
+
+
+
+ +
+
+onupdate
+

Signal to be emitted when the object has been updated

+
+ +
+
+property plot
+

An object to visualize this data object

+

To make a 2D-plot with the psy-simple +plugin, you can just type

+
plotter = da.psy.plot.plot2d()
+
+
+

It will create a new psyplot.plotter.Plotter instance with the +extracted and visualized data.

+
+

See also

+
+
psyplot.project.DataArrayPlotter

for the different plot methods

+
+
+
+
+ +
+
+property plotter
+

psyplot.plotter.Plotter instance that makes the interactive +plotting of the data

+
+ +
+
+start_update(draw=None, queues=None)[source]
+

Conduct the formerly registered updates

+

This method conducts the updates that have been registered via the +update() method. You can call this method if the +no_auto_update attribute of this instance and the auto_update +parameter in the update() method has been set to False

+
+
Parameters:
+
    +
  • draw (bool or None) – Boolean to control whether the figure of this array shall be drawn +at the end. If None, it defaults to the ‘auto_draw’` parameter +in the psyplot.rcParams dictionary

  • +
  • queues (list of Queue.Queue instances) – The queues that are passed to the +psyplot.plotter.Plotter.start_update() method to ensure a +thread-safe update. It can be None if only one single plotter is +updated at the same time. The number of jobs that are taken from +the queue is determined by the _njobs() attribute. Note that +there this parameter is automatically configured when updating +from a Project.

  • +
+
+
Returns:
+

A boolean indicating whether a redrawing is necessary or not

+
+
Return type:
+

bool

+
+
+
+

See also

+

no_auto_update, update

+
+
+ +
+
+to_interactive_list()[source]
+

Return a InteractiveList that contains this object

+
+ +
+
+update(fmt={}, replot=False, draw=None, auto_update=False, force=False, todefault=False, **kwargs)[source]
+

Update the coordinates and the plot

+

This method updates all arrays in this list with the given coordinate +values and formatoptions.

+
+
Parameters:
+
    +
  • replot (bool) – Boolean that determines whether the data specific formatoptions +shall be updated in any case or not. Note, if dims is not empty +or any coordinate keyword is in **kwargs, this will be set to +True automatically

  • +
  • fmt (dict) – Keys may be any valid formatoption of the formatoptions in the +plotter

  • +
  • force (str, list of str or bool) – If formatoption key (i.e. string) or list of formatoption keys, +thery are definitely updated whether they changed or not. +If True, all the given formatoptions in this call of the are +update() method are updated

  • +
  • todefault (bool) – If True, all changed formatoptions (except the registered ones) +are updated to their default value as stored in the +rc attribute

  • +
  • auto_update (bool) – Boolean determining whether or not the start_update() method +is called at the end. This parameter has no effect if the +no_auto_update attribute is set to True.

  • +
  • draw (bool or None) – Boolean to control whether the figure of this array shall be drawn +at the end. If None, it defaults to the ‘auto_draw’` parameter +in the psyplot.rcParams dictionary

  • +
  • **kwargs – Any other formatoption that shall be updated (additionally to those +in fmt)

  • +
+
+
+

Notes

+

If the no_auto_update attribute is True and the given +auto_update parameter are is False, the update of the plots are +registered and conducted at the next call of the start_update() +method or the next call of this method (if the auto_update parameter +is then True).

+
+ +
+ +
+
+class psyplot.data.InteractiveList(*args, **kwargs)[source]
+

Bases: ArrayList, InteractiveBase

+

List of InteractiveArray instances that can be plotted itself

+

This class combines the ArrayList and the interactive plotting +through psyplot.plotter.Plotter classes. It is mainly used by the +psyplot.plotter.simple module

+
+
Parameters:
+
    +
  • iterable (iterable) – The iterable (e.g. another list) defining this list

  • +
  • attrs (dict-like or iterable, optional) – Global attributes of this list

  • +
  • auto_update (bool) – Default: None. A boolean indicating whether this list shall +automatically update the contained arrays when calling the +update() method or not. See also the no_auto_update +attribute. If None, the value from the 'lists.auto_update' +key in the psyplot.rcParams dictionary is used.

  • +
  • new_name (bool or str) – If False, and the arr_name attribute of the new array is +already in the list, a ValueError is raised. +If True and the arr_name attribute of the new array is not +already in the list, the name is not changed. Otherwise, if the +array name is already in use, new_name is set to ‘arr{0}’. +If not True, this will be used for renaming (if the array name of +arr is in use or not). '{0}' is replaced by a counter

  • +
  • plotter (Plotter) – Default: None. Interactive plotter that makes the plot via +formatoption keywords.

  • +
  • arr_name (str) – Default: 'data'. unique string of the array

  • +
+
+
+

Methods:

+ + + + + + + + + + + + + + + + + + + + + +

append(*args, **kwargs)

Append a new array to the list

extend(*args, **kwargs)

Add further arrays from an iterable to this list

from_dataset(*args, **kwargs)

Create an InteractiveList instance from the given base dataset

start_update([draw, queues])

Conduct the formerly registered updates

to_dataframe()

to_interactive_list()

Return a InteractiveList that contains this object

+

Attributes:

+ + + + + + + + + + + + +

logger

logging.Logger of this instance

no_auto_update

bool.

psy

Return the list itself

+
+
+append(*args, **kwargs)[source]
+

Append a new array to the list

+
+
Parameters:
+
    +
  • value (InteractiveBase) – The data object to append to this list

  • +
  • new_name (bool or str) – If False, and the arr_name attribute of the new array is +already in the list, a ValueError is raised. +If True and the arr_name attribute of the new array is not +already in the list, the name is not changed. Otherwise, if the +array name is already in use, new_name is set to ‘arr{0}’. +If not True, this will be used for renaming (if the array name of +arr is in use or not). '{0}' is replaced by a counter

  • +
+
+
Raises:
+
    +
  • ValueError – If it was impossible to find a name that isn’t already in the list

  • +
  • ValueError – If new_name is False and the array is already in the list

  • +
+
+
+
+

See also

+

list.append, extend, rename

+
+
+ +
+
+extend(*args, **kwargs)[source]
+

Add further arrays from an iterable to this list

+
+
Parameters:
+
    +
  • iterable – Any iterable that contains InteractiveBase instances

  • +
  • new_name (bool or str) – If False, and the arr_name attribute of the new array is +already in the list, a ValueError is raised. +If True and the arr_name attribute of the new array is not +already in the list, the name is not changed. Otherwise, if the +array name is already in use, new_name is set to ‘arr{0}’. +If not True, this will be used for renaming (if the array name of +arr is in use or not). '{0}' is replaced by a counter

  • +
+
+
Raises:
+
    +
  • ValueError – If it was impossible to find a name that isn’t already in the list

  • +
  • ValueError – If new_name is False and the array is already in the list

  • +
+
+
+
+

See also

+

list.extend, append, rename

+
+
+ +
+
+classmethod from_dataset(*args, **kwargs)[source]
+

Create an InteractiveList instance from the given base dataset

+
+
Parameters:
+
    +
  • base (xarray.Dataset) – Dataset instance that is used as reference

  • +
  • method ({'isel', None, 'nearest', ...}) – Selection method of the xarray.Dataset to be used for setting the +variables from the informations in dims. +If method is ‘isel’, the xarray.Dataset.isel() method is +used. Otherwise it sets the method parameter for the +xarray.Dataset.sel() method.

  • +
  • auto_update (bool) – Default: None. A boolean indicating whether this list shall +automatically update the contained arrays when calling the +update() method or not. See also the no_auto_update +attribute. If None, the value from the 'lists.auto_update' +key in the psyplot.rcParams dictionary is used.

  • +
  • prefer_list (bool) – If True and multiple variable names pher array are found, the +InteractiveList class is used. Otherwise the arrays are +put together into one InteractiveArray.

  • +
  • default_slice (indexer) – Index (e.g. 0 if method is ‘isel’) that shall be used for +dimensions not covered by dims and furtherdims. If None, the +whole slice will be used. Note that the default_slice is always +based on the isel method.

  • +
  • decoder (CFDecoder or dict) –

    Arguments for the decoder. This can be one of

    +
      +
    • an instance of CFDecoder

    • +
    • a subclass of CFDecoder

    • +
    • a dictionary with keyword-arguments to the automatically +determined decoder class

    • +
    • None to automatically set the decoder

    • +
    +

  • +
  • squeeze (bool, optional) – Default True. If True, and the created arrays have a an axes with +length 1, it is removed from the dimension list (e.g. an array +with shape (3, 4, 1, 5) will be squeezed to shape (3, 4, 5))

  • +
  • attrs (dict, optional) – Meta attributes that shall be assigned to the selected data arrays +(additional to those stored in the base dataset)

  • +
  • load (bool or dict) – If True, load the data from the dataset using the +xarray.DataArray.load() method. If dict, those will +be given to the above mentioned load method

  • +
  • plotter (psyplot.plotter.Plotter) – The plotter instance that is used to visualize the data in this +list

  • +
  • make_plot (bool) – If True, the plot is made

  • +
  • arr_names (string, list of strings or dictionary) –

    Set the unique array names of the resulting arrays and (optionally) +dimensions.

    +
      +
    • if string: same as list of strings (see below). Strings may +include {0} which will be replaced by a counter.

    • +
    • list of strings: those will be used for the array names. The final +number of dictionaries in the return depend in this case on the +dims and **furtherdims

    • +
    • dictionary: +Then nothing happens and an dict version of +arr_names is returned.

    • +
    +

  • +
  • sort (list of strings) – This parameter defines how the dictionaries are ordered. It has no +effect if arr_names is a dictionary (use a +dict for that). It can be a list of +dimension strings matching to the dimensions in dims for the +variable.

  • +
  • dims (dict) – Keys must be variable names of dimensions (e.g. time, level, lat or +lon) or ‘name’ for the variable name you want to choose. +Values must be values of that dimension or iterables of the values +(e.g. lists). Note that strings will be put into a list. +For example dims = {‘name’: ‘t2m’, ‘time’: 0} will result in one plot +for the first time step, whereas dims = {‘name’: ‘t2m’, ‘time’: [0, 1]} +will result in two plots, one for the first (time == 0) and one for the +second (time == 1) time step.

  • +
  • **kwargs – Further keyword arguments may point to any of the dimensions of the +data (see dims)

  • +
+
+
Returns:
+

The list with the specified InteractiveArray instances +that hold a reference to the given base

+
+
Return type:
+

ArrayList

+
+
+
+ +
+
+property logger
+

logging.Logger of this instance

+
+ +
+
+property no_auto_update
+

bool. Boolean controlling whether the start_update() +method is automatically called by the update() method

+
+

Examples

+

You can disable the automatic update via

+
>>> with data.no_auto_update:
+...     data.update(time=1)
+...     data.start_update()
+
+
+

To permanently disable the automatic update, simply set

+
>>> data.no_auto_update = True
+>>> data.update(time=1)
+>>> data.no_auto_update = False  # reenable automatical update
+
+
+
+
+ +
+
+property psy
+

Return the list itself

+
+ +
+
+start_update(draw=None, queues=None)[source]
+

Conduct the formerly registered updates

+

This method conducts the updates that have been registered via the +update() method. You can call this method if the +auto_update attribute of this instance is True and the +auto_update parameter in the update() method has been set to +False

+
+
Parameters:
+
    +
  • draw (bool or None) – Boolean to control whether the figure of this array shall be drawn +at the end. If None, it defaults to the ‘auto_draw’` parameter +in the psyplot.rcParams dictionary

  • +
  • queues (list of Queue.Queue instances) – The queues that are passed to the +psyplot.plotter.Plotter.start_update() method to ensure a +thread-safe update. It can be None if only one single plotter is +updated at the same time. The number of jobs that are taken from +the queue is determined by the _njobs() attribute. Note that +there this parameter is automatically configured when updating +from a Project.

  • +
+
+
Returns:
+

A boolean indicating whether a redrawing is necessary or not

+
+
Return type:
+

bool

+
+
+
+

See also

+

no_auto_update, update

+
+
+ +
+
+to_dataframe()[source]
+
+ +
+
+to_interactive_list()[source]
+

Return a InteractiveList that contains this object

+
+ +
+ +
+
+class psyplot.data.Signal(name=None, cls_signal=False)[source]
+

Bases: object

+

Signal to connect functions to a specific event

+

This class behaves almost similar to PyQt’s +PyQt4.QtCore.pyqtBoundSignal

+

Methods:

+ + + + + + + + + + + + +

connect(func)

disconnect([func])

Disconnect a function call to the signal.

emit(*args, **kwargs)

+

Attributes:

+ + + + + + + + + +

instance

owner

+
+
+connect(func)[source]
+
+ +
+
+disconnect(func=None)[source]
+

Disconnect a function call to the signal. If None, all connections +are disconnected

+
+ +
+
+emit(*args, **kwargs)[source]
+
+ +
+
+instance = None
+
+ +
+
+owner = None
+
+ +
+ +
+
+class psyplot.data.UGridDecoder(ds=None, x=None, y=None, z=None, t=None)[source]
+

Bases: CFDecoder

+

Decoder for UGrid data sets

+
+

Warning

+

Currently only triangles are supported.

+
+

Methods:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

can_decode(ds, var)

Check whether the given variable can be decoded.

decode_coords(ds[, gridfile])

Reimplemented to set the mesh variables as coordinates

get_cell_node_coord(var[, coords, axis, nans])

Checks whether the bounds in the variable attribute are triangular

get_mesh(var[, coords])

Get the mesh variable for the given var

get_nodes(coord, coords)

Get the variables containing the definition of the nodes

get_triangles(var[, coords, convert_radian, ...])

Get the of the given coordinate.

get_x(var[, coords])

Get the centers of the triangles in the x-dimension

get_y(var[, coords])

Get the centers of the triangles in the y-dimension

is_unstructured(*args, **kwargs)

Reimpletemented to return always True.

+

Attributes:

+ + + + + + +

supports_spatial_slicing

True if the data of the CFDecoder supports the extraction of a subset of the data based on the indices.

+
+
+classmethod can_decode(ds, var)[source]
+

Check whether the given variable can be decoded.

+

Returns True if a mesh coordinate could be found via the +get_mesh() method

+
+
Parameters:
+
+
+
Returns:
+

True if the decoder can decode the given array var. Otherwise +False

+
+
Return type:
+

bool

+
+
+
+ +
+
+static decode_coords(ds, gridfile=None)[source]
+

Reimplemented to set the mesh variables as coordinates

+
+
Parameters:
+
    +
  • ds (xarray.Dataset) – The dataset to decode

  • +
  • gridfile (str) – The path to a separate grid file or a xarray.Dataset instance which +may store the coordinates used in ds

  • +
+
+
Returns:
+

ds with additional coordinates

+
+
Return type:
+

xarray.Dataset

+
+
+
+ +
+
+get_cell_node_coord(var, coords=None, axis='x', nans=None)[source]
+

Checks whether the bounds in the variable attribute are triangular

+
+
Parameters:
+
    +
  • var (xarray.Variable or xarray.DataArray) – The variable to check

  • +
  • coords (dict) – Coordinates to use. If None, the coordinates of the dataset in the +ds attribute are used.

  • +
  • axis ({'x', 'y'}) – The spatial axis to check

  • +
  • nans ({None, 'skip', 'only'}) – Determines whether values with nan shall be left (None), skipped +('skip') or shall be the only one returned ('only')

  • +
+
+
Returns:
+

the bounds corrdinate (if existent)

+
+
Return type:
+

xarray.DataArray or None

+
+
+
+ +
+
+get_mesh(var, coords=None)[source]
+

Get the mesh variable for the given var

+
+
Parameters:
+
    +
  • var (xarray.Variable) – The data source whith the 'mesh' attribute

  • +
  • coords (dict) – The coordinates to use. If None, the coordinates of the dataset of +this decoder is used

  • +
+
+
Returns:
+

The mesh coordinate

+
+
Return type:
+

xarray.Coordinate

+
+
+
+ +
+
+get_nodes(coord, coords)[source]
+

Get the variables containing the definition of the nodes

+
+
Parameters:
+
    +
  • coord (xarray.Coordinate) – The mesh variable

  • +
  • coords (dict) – The coordinates to use to get node coordinates

  • +
+
+
+
+ +
+
+get_triangles(var, coords=None, convert_radian=True, copy=False, src_crs=None, target_crs=None, nans=None, stacklevel=1)[source]
+

Get the of the given coordinate.

+
+
Parameters:
+
    +
  • var (xarray.Variable or xarray.DataArray) – The variable to use

  • +
  • coords (dict) – Alternative coordinates to use. If None, the coordinates of the +ds dataset are used

  • +
  • convert_radian (bool) – If True and the coordinate has units in ‘radian’, those are +converted to degrees

  • +
  • copy (bool) – If True, vertice arrays are copied

  • +
  • src_crs (cartopy.crs.Crs) – The source projection of the data. If not None, a transformation +to the given target_crs will be done

  • +
  • target_crs (cartopy.crs.Crs) – The target projection for which the triangles shall be transformed. +Must only be provided if the src_crs is not None.

  • +
  • nans ({None, 'skip', 'only'}) – Determines whether values with nan shall be left (None), skipped +('skip') or shall be the only one returned ('only')

  • +
+
+
Returns:
+

The spatial triangles of the variable

+
+
Return type:
+

matplotlib.tri.Triangulation

+
+
+

Notes

+

If the 'location' attribute is set to 'node', a delaunay +triangulation is performed using the +matplotlib.tri.Triangulation class.

+
+ +
+
+get_x(var, coords=None)[source]
+

Get the centers of the triangles in the x-dimension

+
+
Returns:
+

The y-coordinate or None if it could be found

+
+
Return type:
+

xarray.Coordinate or None

+
+
+
+ +
+
+get_y(var, coords=None)[source]
+

Get the centers of the triangles in the y-dimension

+
+
Returns:
+

The y-coordinate or None if it could be found

+
+
Return type:
+

xarray.Coordinate or None

+
+
+
+ +
+
+is_unstructured(*args, **kwargs)[source]
+

Reimpletemented to return always True. Any *args and **kwargs +are ignored

+
+ +
+
+supports_spatial_slicing: bool = False
+

True if the data of the CFDecoder supports the extraction of a subset of +the data based on the indices.

+

For UGRID conventions, this is not easily possible because the +extraction of a subset breaks the connectivity information of the mesh

+
+ +
+ +
+
+psyplot.data.decode_absolute_time(times)[source]
+
+ +
+
+psyplot.data.encode_absolute_time(times)[source]
+
+ +
+
+psyplot.data.get_filename_ds(ds, dump=True, paths=None, **kwargs)[source]
+

Return the filename of the corresponding to a dataset

+

This method returns the path to the ds or saves the dataset +if there exists no filename

+
+
Parameters:
+
    +
  • ds (xarray.Dataset) – The dataset you want the path information for

  • +
  • dump (bool) – If True and the dataset has not been dumped so far, it is dumped to a +temporary file or the one generated by paths is used

  • +
  • paths (iterable or True) – An iterator over filenames to use if a dataset has no filename. +If paths is True, an iterator over temporary files will be +created without raising a warning

  • +
  • **kwargs – Any other keyword for the to_netcdf() function

  • +
  • path (str, path-like or file-like, optional) – Path to which to save this dataset. File-like objects are only +supported by the scipy engine. If no path is provided, this +function returns the resulting netCDF file as bytes; in this case, +we need to use scipy, which does not support netCDF version 4 (the +default format becomes NETCDF3_64BIT).

  • +
  • mode ({"w", "a"}, default: "w") – Write (‘w’) or append (‘a’) mode. If mode=’w’, any existing file at +this location will be overwritten. If mode=’a’, existing variables +will be overwritten.

  • +
  • format ({"NETCDF4", "NETCDF4_CLASSIC", "NETCDF3_64BIT", "NETCDF3_CLASSIC"}, optional) –

    File format for the resulting netCDF file:

    +
      +
    • NETCDF4: Data is stored in an HDF5 file, using netCDF4 API +features.

    • +
    • NETCDF4_CLASSIC: Data is stored in an HDF5 file, using only +netCDF 3 compatible API features.

    • +
    • NETCDF3_64BIT: 64-bit offset version of the netCDF 3 file format, +which fully supports 2+ GB files, but is only compatible with +clients linked against netCDF version 3.6.0 or later.

    • +
    • NETCDF3_CLASSIC: The classic netCDF 3 file format. It does not +handle 2+ GB files very well.

    • +
    +

    All formats are supported by the netCDF4-python library. +scipy.io.netcdf only supports the last two formats.

    +

    The default format is NETCDF4 if you are saving a file to disk and +have the netCDF4-python library available. Otherwise, xarray falls +back to using scipy to write netCDF files and defaults to the +NETCDF3_64BIT format (scipy does not support netCDF4).

    +

  • +
  • group (str, optional) – Path to the netCDF4 group in the given file to open (only works for +format=’NETCDF4’). The group(s) will be created if necessary.

  • +
  • engine ({"netcdf4", "scipy", "h5netcdf"}, optional) – Engine to use when writing netCDF files. If not provided, the +default engine is chosen based on available dependencies, with a +preference for ‘netcdf4’ if writing to a file on disk.

  • +
  • encoding (dict, optional) –

    Nested dictionary with variable names as keys and dictionaries of +variable specific encodings as values, e.g., +{"my_variable": {"dtype": "int16", "scale_factor": 0.1, +"zlib": True}, ...}. +If encoding is specified the original encoding of the variables of +the dataset is ignored.

    +

    The h5netcdf engine supports both the NetCDF4-style compression +encoding parameters {"zlib": True, "complevel": 9} and the h5py +ones {"compression": "gzip", "compression_opts": 9}. +This allows using any compression plugin installed in the HDF5 +library, e.g. LZF.

    +

  • +
+
+
Returns:
+

    +
  • str or None – None, if the dataset has not yet been dumped to the harddisk and +dump is False, otherwise the complete the path to the input +file

  • +
  • str – The module of the xarray.backends.common.AbstractDataStore +instance that is used to hold the data

  • +
  • str – The class name of the +xarray.backends.common.AbstractDataStore instance that is +used to open the data

  • +
+

+
+
+
+ +
+
+psyplot.data.get_fname_funcs = [<function _get_fname_netCDF4>, <function _get_fname_scipy>, <function _get_fname_nio>]
+

functions to use to extract the file name from a data store

+
+ +
+
+psyplot.data.get_index_from_coord(coord, base_index)[source]
+

Function to return the coordinate as integer, integer array or slice

+

If coord is zero-dimensional, the corresponding integer in base_index +will be supplied. Otherwise it is first tried to return a slice, if that +does not work an integer array with the corresponding indices is returned.

+
+
Parameters:
+
    +
  • coord (xarray.Coordinate or xarray.Variable) – Coordinate to convert

  • +
  • base_index (pandas.Index) – The base index from which the coord was extracted

  • +
+
+
Returns:
+

The indexer that can be used to access the coord in the +base_index

+
+
Return type:
+

int, array of ints or slice

+
+
+
+ +
+
+psyplot.data.get_tdata(t_format, files)[source]
+

Get the time information from file names

+
+
Parameters:
+
    +
  • t_format (str) – The string that can be used to get the time information in the files. +Any numeric datetime format string (e.g. %Y, %m, %H) can be used, but +not non-numeric strings like %b, etc. See [1] for the datetime format +strings

  • +
  • files (list of str) – The that contain the time informations

  • +
+
+
Returns:
+

    +
  • pandas.Index – The time coordinate

  • +
  • list of str – The file names as they are sorten in the returned index

  • +
+

+
+
+

References

+ +
+ +
+
+psyplot.data.open_dataset(filename_or_obj, decode_cf=True, decode_times=True, decode_coords=True, engine=None, gridfile=None, **kwargs)[source]
+

Open an instance of xarray.Dataset.

+

This method has the same functionality as the xarray.open_dataset() +method except that is supports an additional ‘gdal’ engine to open +gdal Rasters (e.g. GeoTiffs) and that is supports absolute time units like +'day as %Y%m%d.%f' (if decode_cf and decode_times are True).

+
+
Parameters:
+
    +
  • filename_or_obj (str, Path, file-like or DataStore) – Strings and Path objects are interpreted as a path to a netCDF file +or an OpenDAP URL and opened with python-netCDF4, unless the filename +ends with .gz, in which case the file is gunzipped and opened with +scipy.io.netcdf (only netCDF3 supported). Byte-strings or file-like +objects are opened by scipy.io.netcdf (netCDF3) or h5py (netCDF4/HDF).

  • +
  • chunks (int, dict, 'auto' or None, optional) – If chunks is provided, it is used to load the new dataset into dask +arrays. chunks=-1 loads the dataset with dask using a single +chunk for all arrays. chunks={} loads the dataset with dask using +engine preferred chunks if exposed by the backend, otherwise with +a single chunk for all arrays. In order to reproduce the default behavior +of xr.open_zarr(...) use xr.open_dataset(..., engine='zarr', chunks={}). +chunks='auto' will use dask auto chunking taking into account the +engine preferred chunks. See dask chunking for more details.

  • +
  • cache (bool, optional) – If True, cache data loaded from the underlying datastore in memory as +NumPy arrays when accessed to avoid reading from the underlying data- +store multiple times. Defaults to True unless you specify the chunks +argument to use dask, in which case it defaults to False. Does not +change the behavior of coordinates corresponding to dimensions, which +always load their data from disk into a pandas.Index.

  • +
  • decode_cf (bool, optional) – Whether to decode these variables, assuming they were saved according +to CF conventions.

  • +
  • mask_and_scale (bool, optional) – If True, replace array values equal to _FillValue with NA and scale +values according to the formula original_values * scale_factor + +add_offset, where _FillValue, scale_factor and add_offset are +taken from variable attributes (if they exist). If the _FillValue or +missing_value attribute contains multiple values a warning will be +issued and all array values matching one of the multiple values will +be replaced by NA. This keyword may not be supported by all the backends.

  • +
  • decode_times (bool, optional) – If True, decode times encoded in the standard NetCDF datetime format +into datetime objects. Otherwise, leave them encoded as numbers. +This keyword may not be supported by all the backends.

  • +
  • decode_timedelta (bool, optional) – If True, decode variables and coordinates with time units in +{“days”, “hours”, “minutes”, “seconds”, “milliseconds”, “microseconds”} +into timedelta objects. If False, leave them encoded as numbers. +If None (default), assume the same value of decode_time. +This keyword may not be supported by all the backends.

  • +
  • use_cftime (bool, optional) – Only relevant if encoded dates come from a standard calendar +(e.g. “gregorian”, “proleptic_gregorian”, “standard”, or not +specified). If None (default), attempt to decode times to +np.datetime64[ns] objects; if this is not possible, decode times to +cftime.datetime objects. If True, always decode times to +cftime.datetime objects, regardless of whether or not they can be +represented using np.datetime64[ns] objects. If False, always +decode times to np.datetime64[ns] objects; if this is not possible +raise an error. This keyword may not be supported by all the backends.

  • +
  • concat_characters (bool, optional) – If True, concatenate along the last dimension of character arrays to +form string arrays. Dimensions will only be concatenated over (and +removed) if they have no corresponding variable and if they are only +used as the last dimension of character arrays. +This keyword may not be supported by all the backends.

  • +
  • decode_coords (bool or {"coordinates", "all"}, optional) –

    Controls which variables are set as coordinate variables:

    +
      +
    • ”coordinates” or True: Set variables referred to in the +'coordinates' attribute of the datasets or individual variables +as coordinate variables.

    • +
    • ”all”: Set variables referred to in 'grid_mapping', 'bounds' and +other attributes as coordinate variables.

    • +
    +

    Only existing variables can be set as coordinates. Missing variables +will be silently ignored.

    +

  • +
  • drop_variables (str or iterable of str, optional) – A variable or list of variables to exclude from being parsed from the +dataset. This may be useful to drop variables with problems or +inconsistent values.

  • +
  • inline_array (bool, default: False) – How to include the array in the dask task graph. +By default(inline_array=False) the array is included in a task by +itself, and each chunk refers to that task by its key. With +inline_array=True, Dask will instead inline the array directly +in the values of the task graph. See dask.array.from_array().

  • +
  • chunked_array_type (str, optional) – Which chunked array type to coerce this datasets’ arrays to. +Defaults to ‘dask’ if installed, else whatever is registered via the ChunkManagerEnetryPoint system. +Experimental API that should not be relied upon.

  • +
  • from_array_kwargs (dict) – Additional keyword arguments passed on to the ChunkManagerEntrypoint.from_array method used to create +chunked arrays, via whichever chunk manager is specified through the chunked_array_type kwarg. +For example if dask.array.Array() objects are used for chunking, additional kwargs will be passed +to dask.array.from_array(). Experimental API that should not be relied upon.

  • +
  • backend_kwargs (dict) – Additional keyword arguments passed on to the engine open function, +equivalent to **kwargs.

  • +
  • **kwargs (dict) –

    Additional keyword arguments passed on to the engine open function. +For example:

    +
      +
    • ’group’: path to the netCDF4 group in the given file to open given as +a str,supported by “netcdf4”, “h5netcdf”, “zarr”.

    • +
    • ’lock’: resource lock to use when reading data from disk. Only +relevant when using dask or another form of parallelism. By default, +appropriate locks are chosen to safely read and write files with the +currently active dask scheduler. Supported by “netcdf4”, “h5netcdf”, +“scipy”.

    • +
    +

    See engine open function for kwargs accepted by each specific engine.

    +

  • +
  • engine ({'netcdf4', 'scipy', 'pydap', 'h5netcdf', 'gdal'}, optional) – Engine to use when reading netCDF files. If not provided, the default +engine is chosen based on available dependencies, with a preference for +‘netcdf4’.

  • +
  • gridfile (str) – The path to a separate grid file or a xarray.Dataset instance which +may store the coordinates used in ds

  • +
+
+
Returns:
+

The dataset that contains the variables from filename_or_obj

+
+
Return type:
+

xarray.Dataset

+
+
+
+ +
+
+psyplot.data.open_mfdataset(paths, decode_cf=True, decode_times=True, decode_coords=True, engine=None, gridfile=None, t_format=None, **kwargs)[source]
+

Open multiple files as a single dataset.

+

This function is essentially the same as the xarray.open_mfdataset() +function but (as the open_dataset()) supports additional decoding +and the 'gdal' engine. +You can further specify the t_format parameter to get the time +information from the files and use the results to concatenate the files

+
+
Parameters:
+
    +
  • paths (str or nested sequence of paths) – Either a string glob in the form ``"path/to/my/files/*.nc"`` or an explicit list of +files to open. Paths can be given as strings or as pathlib Paths. If +concatenation along more than one dimension is desired, then paths must be a +nested list-of-lists (see combine_nested for details). (A string glob will +be expanded to a 1-dimensional list.)

  • +
  • chunks (int, dict, 'auto' or None, optional) – Dictionary with keys given by dimension names and values given by chunk sizes. +In general, these should divide the dimensions of each dataset. If int, chunk +each dimension by chunks. By default, chunks will be chosen to load entire +input files into memory at once. This has a major impact on performance: please +see the full documentation for more details [2]_.

  • +
  • concat_dim (str, DataArray, Index or a Sequence of these or None, optional) – Dimensions to concatenate files along. You only need to provide this argument +if combine='nested', and if any of the dimensions along which you want to +concatenate is not a dimension in the original datasets, e.g., if you want to +stack a collection of 2D arrays along a third dimension. Set +concat_dim=[..., None, ...] explicitly to disable concatenation along a +particular dimension. Default is None, which for a 1D list of filepaths is +equivalent to opening the files separately and then merging them with +xarray.merge.

  • +
  • combine ({"by_coords", "nested"}, optional) – Whether xarray.combine_by_coords or xarray.combine_nested is used to +combine all the data. Default is to use xarray.combine_by_coords.

  • +
  • compat ({"identical", "equals", "broadcast_equals", "no_conflicts", "override"}, default: "no_conflicts") –

    String indicating how to compare variables of the same name for +potential conflicts when merging:

    +
    +
      +
    • ”broadcast_equals”: all values must be equal when variables are +broadcast against each other to ensure common dimensions.

    • +
    • ”equals”: all values and dimensions must be the same.

    • +
    • ”identical”: all values, dimensions and attributes must be the +same.

    • +
    • ”no_conflicts”: only values which are not null in both datasets +must be equal. The returned dataset then contains the combination +of all non-null values.

    • +
    • ”override”: skip comparing and pick variable from first dataset

    • +
    +
    +

  • +
  • engine ({'netcdf4', 'scipy', 'pydap', 'h5netcdf', 'gdal'}, optional) – Engine to use when reading netCDF files. If not provided, the default +engine is chosen based on available dependencies, with a preference for +‘netcdf4’.

  • +
  • t_format (str) – The string that can be used to get the time information in the files. +Any numeric datetime format string (e.g. %Y, %m, %H) can be used, but +not non-numeric strings like %b, etc. See [1] for the datetime format +strings

  • +
  • gridfile (str) – The path to a separate grid file or a xarray.Dataset instance which +may store the coordinates used in ds

  • +
+
+
Returns:
+

The dataset that contains the variables from filename_or_obj

+
+
Return type:
+

xarray.Dataset

+
+
+
+ +
+
+psyplot.data.setup_coords(arr_names=None, sort=[], dims={}, **kwargs)[source]
+

Sets up the arr_names dictionary for the plot

+
+
Parameters:
+
    +
  • arr_names (string, list of strings or dictionary) –

    Set the unique array names of the resulting arrays and (optionally) +dimensions.

    +
      +
    • if string: same as list of strings (see below). Strings may +include {0} which will be replaced by a counter.

    • +
    • list of strings: those will be used for the array names. The final +number of dictionaries in the return depend in this case on the +dims and **furtherdims

    • +
    • dictionary: +Then nothing happens and an dict version of +arr_names is returned.

    • +
    +

  • +
  • sort (list of strings) – This parameter defines how the dictionaries are ordered. It has no +effect if arr_names is a dictionary (use a +dict for that). It can be a list of +dimension strings matching to the dimensions in dims for the +variable.

  • +
  • dims (dict) – Keys must be variable names of dimensions (e.g. time, level, lat or +lon) or ‘name’ for the variable name you want to choose. +Values must be values of that dimension or iterables of the values +(e.g. lists). Note that strings will be put into a list. +For example dims = {‘name’: ‘t2m’, ‘time’: 0} will result in one plot +for the first time step, whereas dims = {‘name’: ‘t2m’, ‘time’: [0, 1]} +will result in two plots, one for the first (time == 0) and one for the +second (time == 1) time step.

  • +
  • **kwargs – The same as dims (those will update what is specified in dims)

  • +
+
+
Returns:
+

A mapping from the keys in arr_names and to dictionaries. Each +dictionary corresponds defines the coordinates of one data array to +load

+
+
Return type:
+

dict

+
+
+
+ +
+
+psyplot.data.t_patterns = {'%H': '[0-9]{1,2}', '%M': '[0-9]{1,2}', '%S': '[0-9]{1,2}', '%Y': '[0-9]{4}', '%d': '[0-9]{1,2}', '%m': '[0-9]{1,2}'}
+

mapping that translates datetime format strings to regex patterns

+
+ +
+
+psyplot.data.to_netcdf(ds, *args, **kwargs)[source]
+

Store the given dataset as a netCDF file

+

This functions works essentially the same as the usual +xarray.Dataset.to_netcdf() method but can also encode absolute time +units

+
+
Parameters:
+
    +
  • ds (xarray.Dataset) – The dataset to store

  • +
  • path (str, path-like or file-like, optional) – Path to which to save this dataset. File-like objects are only +supported by the scipy engine. If no path is provided, this +function returns the resulting netCDF file as bytes; in this case, +we need to use scipy, which does not support netCDF version 4 (the +default format becomes NETCDF3_64BIT).

  • +
  • mode ({"w", "a"}, default: "w") – Write (‘w’) or append (‘a’) mode. If mode=’w’, any existing file at +this location will be overwritten. If mode=’a’, existing variables +will be overwritten.

  • +
  • format ({"NETCDF4", "NETCDF4_CLASSIC", "NETCDF3_64BIT", "NETCDF3_CLASSIC"}, optional) –

    File format for the resulting netCDF file:

    +
      +
    • NETCDF4: Data is stored in an HDF5 file, using netCDF4 API +features.

    • +
    • NETCDF4_CLASSIC: Data is stored in an HDF5 file, using only +netCDF 3 compatible API features.

    • +
    • NETCDF3_64BIT: 64-bit offset version of the netCDF 3 file format, +which fully supports 2+ GB files, but is only compatible with +clients linked against netCDF version 3.6.0 or later.

    • +
    • NETCDF3_CLASSIC: The classic netCDF 3 file format. It does not +handle 2+ GB files very well.

    • +
    +

    All formats are supported by the netCDF4-python library. +scipy.io.netcdf only supports the last two formats.

    +

    The default format is NETCDF4 if you are saving a file to disk and +have the netCDF4-python library available. Otherwise, xarray falls +back to using scipy to write netCDF files and defaults to the +NETCDF3_64BIT format (scipy does not support netCDF4).

    +

  • +
  • group (str, optional) – Path to the netCDF4 group in the given file to open (only works for +format=’NETCDF4’). The group(s) will be created if necessary.

  • +
  • engine ({"netcdf4", "scipy", "h5netcdf"}, optional) – Engine to use when writing netCDF files. If not provided, the +default engine is chosen based on available dependencies, with a +preference for ‘netcdf4’ if writing to a file on disk.

  • +
  • encoding (dict, optional) –

    Nested dictionary with variable names as keys and dictionaries of +variable specific encodings as values, e.g., +{"my_variable": {"dtype": "int16", "scale_factor": 0.1, +"zlib": True}, ...}. +If encoding is specified the original encoding of the variables of +the dataset is ignored.

    +

    The h5netcdf engine supports both the NetCDF4-style compression +encoding parameters {"zlib": True, "complevel": 9} and the h5py +ones {"compression": "gzip", "compression_opts": 9}. +This allows using any compression plugin installed in the HDF5 +library, e.g. LZF.

    +

  • +
+
+
+
+ +
+
+psyplot.data.to_slice(arr)[source]
+

Test whether arr is an integer array that can be replaced by a slice

+
+
Parameters:
+

arr (numpy.array) – Numpy integer array

+
+
Returns:
+

If arr could be converted to an array, this is returned, otherwise +None is returned

+
+
Return type:
+

slice or None

+
+
+
+

See also

+

get_index_from_coord

+
+
+ + + +
+
+ + + +
+
+
+
+ + + + \ No newline at end of file diff --git a/api/psyplot.docstring.html b/api/psyplot.docstring.html new file mode 100644 index 0000000..fd5ce31 --- /dev/null +++ b/api/psyplot.docstring.html @@ -0,0 +1,544 @@ + + + + + + + <no title> — psyplot documentation + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Docstring module of the psyplot package

+

We use the docrep package for managing our docstrings

+

Classes:

+ + + + + + +

PsyplotDocstringProcessor(*args, **kwargs)

A docrep.DocstringProcessor subclass with possible types section

+

Functions:

+ + + + + + + + + + + + +

append_original_doc(parent[, num])

Return an iterator that append the docstring of the given parent function to the applied function

dedent(func)

Dedent the docstring of a function and substitute with params

indent(text[, num])

Indet the given string

+

Data:

+ + + + + + +

docstrings

docrep.PsyplotDocstringProcessor instance that simplifies the reuse of docstrings from between different python objects.

+
+
+class psyplot.docstring.PsyplotDocstringProcessor(*args, **kwargs)[source]
+

Bases: DocstringProcessor

+

A docrep.DocstringProcessor subclass with possible types section

+
+
Parameters:
+
    +
  • *args – Positional parameters that shall be used for the substitution. Note +that you can only provide either *args or **kwargs, +furthermore most of the methods like get_sections require +**kwargs to be provided (if any).

  • +
  • **kwargs – Initial parameters to use

  • +
+
+
+

Methods:

+ + + + + + +

get_sections([s, base, sections])

Extract the specified sections out of the given string

+

Attributes:

+ + + + + + +

param_like_sections

sections that behave the same as the Parameter section by defining a list

+
+
+get_sections(s=None, base=None, sections=['Parameters', 'Other Parameters', 'Possible types'])[source]
+

Extract the specified sections out of the given string

+

The same as the docrep.DocstringProcessor.get_sections() method +but uses the 'Possible types' section by default, too

+
+
Parameters:
+
    +
  • s (str) – Docstring to split

  • +
  • base (str) – base to use in the sections attribute

  • +
  • sections (list of str) – sections to look for. Each section must be followed by a newline +character (’n’) and a bar of ‘-’ (following the numpy (napoleon) +docstring conventions).

  • +
+
+
Returns:
+

The replaced string

+
+
Return type:
+

str

+
+
+
+ +
+
+param_like_sections = ['Parameters', 'Other Parameters', 'Returns', 'Raises', 'Possible types']
+

sections that behave the same as the Parameter section by defining a +list

+
+ +
+ +
+
+psyplot.docstring.append_original_doc(parent, num=0)[source]
+

Return an iterator that append the docstring of the given parent +function to the applied function

+
+ +
+
+psyplot.docstring.dedent(func)[source]
+

Dedent the docstring of a function and substitute with params

+
+
Parameters:
+

func (function) – function with the documentation to dedent

+
+
+
+ +
+
+psyplot.docstring.docstrings(s) = <psyplot.docstring.PsyplotDocstringProcessor object>
+

docrep.PsyplotDocstringProcessor instance that simplifies the reuse +of docstrings from between different python objects.

+
+ +
+
+psyplot.docstring.indent(text, num=4)[source]
+

Indet the given string

+
+ + + +
+
+ + + +
+
+
+
+ + + + \ No newline at end of file diff --git a/api/psyplot.gdal_store.html b/api/psyplot.gdal_store.html new file mode 100644 index 0000000..041cc8b --- /dev/null +++ b/api/psyplot.gdal_store.html @@ -0,0 +1,488 @@ + + + + + + + <no title> — psyplot documentation + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Gdal Store for reading GeoTIFF files into an xarray.Dataset

+

This module contains the definition of the GdalStore class that can +be used to read in a GeoTIFF file into an xarray.Dataset. +It requires that you have the python gdal module installed.

+
+

Examples

+

to open a GeoTIFF file named 'my_tiff.tiff' you can do:

+
>>> from psyplot.gdal_store import GdalStore
+>>> from xarray import open_dataset
+>>> ds = open_dataset(GdalStore("my_tiff"))
+
+
+

Or you use the engine of the psyplot.open_dataset() function:

+
>>> ds = open_dataset("my_tiff.tiff", engine="gdal")
+
+
+
+

Classes:

+ + + + + + +

GdalStore(filename_or_obj)

Datastore to read raster files suitable for the gdal package

+
+
+class psyplot.gdal_store.GdalStore(filename_or_obj)[source]
+

Bases: AbstractDataStore

+

Datastore to read raster files suitable for the gdal package

+

We recommend to use the psyplot.open_dataset() function to open +a geotiff file:

+
>>> ds = psyplot.open_dataset("my_geotiff.tiff", engine="gdal")
+
+
+

Notes

+

The GdalStore object is not as elaborate as, for example, the +gdal_translate command. Many attributes, e.g. variable names or netCDF +dimensions will not be interpreted. We only support two +dimensional arrays and each band is saved into one variable named like +'Band1', 'Band2', .... If you want a more elaborate translation of your +GDAL Raster, convert the file to a netCDF file using gdal_translate or +the gdal.GetDriverByName('netCDF').CreateCopy method. However this +class does not create an extra file on your hard disk as it is done by +GDAL.

+
+
Parameters:
+

filename_or_obj (str) – The path to the GeoTIFF file or a gdal dataset

+
+
+

Methods:

+ + + + + + + + + +

get_attrs()

get_variables()

+
+
+get_attrs()[source]
+
+ +
+
+get_variables()[source]
+
+ +
+ + + +
+
+ + + +
+
+
+
+ + + + \ No newline at end of file diff --git a/api/psyplot.html b/api/psyplot.html new file mode 100644 index 0000000..362fa7f --- /dev/null +++ b/api/psyplot.html @@ -0,0 +1,1031 @@ + + + + + + + psyplot package — psyplot documentation + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +
+

psyplot package

+

psyplot visualization framework.

+

Functions:

+ + + + + + +

get_versions([requirements, key])

Get the version information for psyplot, the plugins and its requirements

+

Data:

+ + + + + + +

with_gui

Boolean that is True, if psyplot runs inside the graphical user interface by the psyplot_gui module

+
+
+psyplot.get_versions(requirements=True, key=None)[source]
+

Get the version information for psyplot, the plugins and its requirements

+
+
Parameters:
+
    +
  • requirements (bool) – If True, the requirements of the plugins and psyplot are investigated

  • +
  • key (func) – A function that determines whether a plugin shall be considererd or +not. The function must take a single argument, that is the name of the +plugin as string, and must return True (import the plugin) or False +(skip the plugin). If None, all plugins are imported

  • +
+
+
Returns:
+

A mapping from 'psyplot'/the plugin names to a dictionary with the +'version' key and the corresponding version is returned. If +requirements is True, it also contains a mapping from +'requirements' a dictionary with the versions

+
+
Return type:
+

dict

+
+
+
+

Examples

+

Using the built-in JSON module, we get something like

+
import json
+
+print(json.dumps(psyplot.get_versions(), indent=4))
+{
+    "psy_simple.plugin": {"version": "1.0.0.dev0"},
+    "psyplot": {
+        "version": "1.0.0.dev0",
+        "requirements": {
+            "matplotlib": "1.5.3",
+            "numpy": "1.11.3",
+            "pandas": "0.19.2",
+            "xarray": "0.9.1",
+        },
+    },
+    "psy_maps.plugin": {
+        "version": "1.0.0.dev0",
+        "requirements": {"cartopy": "0.15.0"},
+    },
+}
+
+
+
+
+ +
+
+psyplot.with_gui = False
+

Boolean that is True, if psyplot runs inside the graphical user interface +by the psyplot_gui module

+
+ +
+

Subpackages

+ +
+
+

Submodules

+
+ +
+
+
+ + +
+
+ + + +
+
+
+
+ + + + \ No newline at end of file diff --git a/api/psyplot.plotter.html b/api/psyplot.plotter.html new file mode 100644 index 0000000..3a93691 --- /dev/null +++ b/api/psyplot.plotter.html @@ -0,0 +1,2513 @@ + + + + + + + <no title> — psyplot documentation + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Core package for interactive visualization in the psyplot package

+

This package defines the Plotter and Formatoption classes, +the core of the visualization in the psyplot package. Each +Plotter combines a set of formatoption keys where each formatoption +key is represented by a Formatoption subclass.

+

Data:

+ + + + + + + + + + + + + + + +

BEFOREPLOTTING

Priority value of formatoptions that are updated before the plot it made.

END

Priority value of formatoptions that are updated at the end.

START

Priority value of formatoptions that are updated before the data is loaded.

groups

dict.

+

Classes:

+ + + + + + + + + + + + + + + + + + + + + + + + +

DictFormatoption(key[, plotter, ...])

Base formatoption class defining an alternative set_value that works for dictionaries.

Formatoption(key[, plotter, index_in_list, ...])

Abstract formatoption

FormatoptionMeta(clsname, bases, dct)

Meta class for formatoptions

Plotter([data, ax, auto_update, project, ...])

Interactive plotting object for one or more data arrays

PostProcDependencies()

The dependencies of this formatoption

PostProcessing(key[, plotter, ...])

Apply your own postprocessing script

PostTiming(key[, plotter, index_in_list, ...])

Determine when to run the post formatoption

+

Functions:

+ + + + + + + + + +

format_time(x)

Formats date values

is_data_dependent(fmto, data)

Check whether a formatoption is data dependent

+
+
+psyplot.plotter.BEFOREPLOTTING = 20
+

Priority value of formatoptions that are updated before the plot it made.

+
+ +
+
+class psyplot.plotter.DictFormatoption(key, plotter=None, index_in_list=None, additional_children=[], additional_dependencies=[], **kwargs)[source]
+

Bases: Formatoption

+

Base formatoption class defining an alternative set_value that works for +dictionaries.

+
+
Parameters:
+
    +
  • key (str) – formatoption key in the plotter

  • +
  • plotter (psyplot.plotter.Plotter) – Plotter instance that holds this formatoption. If None, it is +assumed that this instance serves as a descriptor.

  • +
  • index_in_list (int or None) – The index that shall be used if the data is a +psyplot.InteractiveList

  • +
  • additional_children (list or str) – Additional children to use (see the children attribute)

  • +
  • additional_dependencies (list or str) – Additional dependencies to use (see the dependencies +attribute)

  • +
  • **kwargs – Further keywords may be used to specify different names for +children, dependencies and connection formatoptions that match the +setup of the plotter. Hence, keywords may be anything of the +children, dependencies and connections +attributes, with values being the name of the new formatoption in +this plotter.

  • +
+
+
+

Methods:

+ + + + + + +

set_value(value[, validate, todefault])

Set (and validate) the value in the plotter

+
+
+set_value(value, validate=True, todefault=False)[source]
+

Set (and validate) the value in the plotter

+
+
Parameters:
+
    +
  • value – Value to set

  • +
  • validate (bool) – if True, validate the value before it is set

  • +
  • todefault (bool) – True if the value is updated to the default value

  • +
+
+
+

Notes

+
    +
  • If the current value in the plotter is None, then it will be set with +the given value, otherwise the current value in the plotter is +updated

  • +
  • If the value is an empty dictionary, the value in the plotter is +cleared

  • +
+
+ +
+ +
+
+psyplot.plotter.END = 10
+

Priority value of formatoptions that are updated at the end.

+
+ +
+
+class psyplot.plotter.Formatoption(key, plotter=None, index_in_list=None, additional_children=[], additional_dependencies=[], **kwargs)[source]
+

Bases: object

+

Abstract formatoption

+

This class serves as an abstract version of an formatoption descriptor +that can be used by Plotter instances.

+
+
Parameters:
+
    +
  • key (str) – formatoption key in the plotter

  • +
  • plotter (psyplot.plotter.Plotter) – Plotter instance that holds this formatoption. If None, it is +assumed that this instance serves as a descriptor.

  • +
  • index_in_list (int or None) – The index that shall be used if the data is a +psyplot.InteractiveList

  • +
  • additional_children (list or str) – Additional children to use (see the children attribute)

  • +
  • additional_dependencies (list or str) – Additional dependencies to use (see the dependencies +attribute)

  • +
  • **kwargs – Further keywords may be used to specify different names for +children, dependencies and connection formatoptions that match the +setup of the plotter. Hence, keywords may be anything of the +children, dependencies and connections +attributes, with values being the name of the new formatoption in +this plotter.

  • +
+
+
+

Interface to the data:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

any_decoder

Return the first possible decoder

ax

The axes this Formatoption plots on

data

The data that is plotted

data_dependent

bool or a callable.

decoder

The CFDecoder instance that decodes the raw_data

index_in_list

int or None.

iter_data

Returns an iterator over the plot data arrays

iter_raw_data

Returns an iterator over the original data arrays

project

Project of the plotter of this instance

raw_data

The original data of the plotter of this formatoption

set_data(data[, i])

Replace the data to plot

set_decoder(decoder[, i])

Replace the data to plot

+

Interface for the plotter:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

changed

bool indicating whether the value changed compared to the default or not.

check_and_set(value[, todefault, validate])

Checks the value and sets the value if it changed

diff(value)

Checks whether the given value differs from what is currently set

finish_update()

Finish the update, initialization and sharing process

initialize_plot(value, *args, **kwargs)

Method that is called when the plot is made the first time

key

str.

lock

A threading.Rlock instance to lock while updating

plot_fmt

bool.

plotter

Plotter.

priority

int.

remove()

Method to remove the effects of this formatoption

requires_clearing

bool.

requires_replot

Boolean that is True if an update of the formatoption requires a replot

set_value(value[, validate, todefault])

Set (and validate) the value in the plotter.

share(fmto[, initializing])

Share the settings of this formatoption with other data objects

update(value)

Method that is call to update the formatoption on the axes

update_after_plot

bool.

+

Interface to other formatoptions:

+ + + + + + + + + + + + + + + + + + + + + +

children

list of str.

connections

list of str.

dependencies

list of str.

parents

list of str.

shared

set of the Formatoption instance that are shared with this instance.

shared_by

None if the formatoption is not controlled by another formatoption of another plotter, otherwise the corresponding Formatoption instance

+

Methods:

+ + + + + + + + + + + + +

convert_coordinate(coord, *variables)

Convert a coordinate to units necessary for the plot.

get_decoder([i])

get_fmt_widget(parent, project)

Get a widget to update the formatoption in the GUI

+

Formatoption intrinsic:

+ + + + + + + + + + + + + + + + + + +

default

Default value of this formatoption

validate

Validation method of the formatoption

value

Value of the formatoption in the corresponding plotter or the shared value

value2pickle

The value that can be used when pickling the information of the project

value2share

The value that is passed to shared formatoptions (by default, the value attribute)

+

Information attributes:

+ + + + + + + + + + + + + + + +

default_key

The key of this formatoption in the psyplot.rcParams

group

str.

groupname

Long name of the group this formatoption belongs too.

name

str.

+

Miscellaneous:

+ + + + + + + + + +

init_kwargs

dict key word arguments that are passed to the initialization of a new instance when accessed from the descriptor

logger

Logger of the plotter

+
+
+property any_decoder
+

Return the first possible decoder

+
+ +
+
+property ax
+

The axes this Formatoption plots on

+
+ +
+
+property changed
+

bool indicating whether the value changed compared to the +default or not.

+
+ +
+
+check_and_set(value, todefault=False, validate=True)[source]
+

Checks the value and sets the value if it changed

+

This method checks the value and sets it only if the diff() +method result of the given value is True

+
+
Parameters:
+
    +
  • value – A possible value to set

  • +
  • todefault (bool) – True if the value is updated to the default value

  • +
+
+
Returns:
+

A boolean to indicate whether it has been set or not

+
+
Return type:
+

bool

+
+
+
+ +
+
+children = []
+

list of str. List of formatoptions that have to be updated before this +one is updated. Those formatoptions are only updated if they exist in +the update parameters.

+
+ +
+
+connections = []
+

list of str. Connections to other formatoptions that are (different +from dependencies and children) not important for the +update process

+
+ +
+
+convert_coordinate(coord, *variables)[source]
+

Convert a coordinate to units necessary for the plot.

+

This method takes a single coordinate variable (e.g. the bounds of a +coordinate, or the coordinate itself) and transforms the units that the +plotter requires.

+

One might also provide additional variables that are supposed to be +on the same unit, in case the given coord does not specify a units +attribute. coord might be a CF-conform bounds variable, and one of +the variables might be the corresponding coordinate.

+
+
Parameters:
+
    +
  • coord (xr.Variable) – The variable to transform

  • +
  • *variables – The variables that are on the same unit as coord

  • +
+
+
Returns:
+

The transformed coord

+
+
Return type:
+

xr.Variable

+
+
+

Notes

+

By default, this method uses the convert_coordinate() +method of the plotter.

+
+ +
+
+property data
+

The data that is plotted

+
+ +
+
+data_dependent = False
+

bool or a callable. This attribute indicates whether this +Formatoption depends on the data and should be updated if the +data changes. If it is a callable, it must accept one argument: the +new data. (Note: This is automatically set to True for plot +formatoptions)

+
+ +
+
+property decoder
+

The CFDecoder instance that decodes the +raw_data

+
+ +
+
+property default
+

Default value of this formatoption

+
+ +
+
+property default_key
+

The key of this formatoption in the psyplot.rcParams

+
+ +
+
+dependencies = []
+

list of str. List of formatoptions that force an update of this +formatoption if they are updated.

+
+ +
+
+diff(value)[source]
+

Checks whether the given value differs from what is currently set

+
+
Parameters:
+

value – A possible value to set (make sure that it has been validate via +the validate attribute before)

+
+
Returns:
+

True if the value differs from what is currently set

+
+
Return type:
+

bool

+
+
+
+ +
+
+finish_update()[source]
+

Finish the update, initialization and sharing process

+

This function is called at the end of the Plotter.start_update(), +Plotter.initialize_plot() or the Plotter.share() methods.

+
+ +
+
+get_decoder(i=None)[source]
+
+ +
+
+get_fmt_widget(parent, project)[source]
+

Get a widget to update the formatoption in the GUI

+

This method should return a QWidget that is loaded by the psyplot-gui +when the formatoption is selected in the +psyplot_gui.main.Mainwindow.fmt_widget. It should call the +insert_text() method +when the update text for the formatoption should be changed.

+
+
Parameters:
+
+
+
Returns:
+

The widget to control the formatoption

+
+
Return type:
+

PyQt5.QtWidgets.QWidget

+
+
+
+ +
+
+group = 'misc'
+

str. Key of the group name in groups of this +formatoption keyword

+
+ +
+
+property groupname
+

Long name of the group this formatoption belongs too.

+
+ +
+
+index_in_list = 0
+

int or None. Index that is used in case the plotting data is a +psyplot.InteractiveList

+
+ +
+
+property init_kwargs
+

dict key word arguments that are passed to the +initialization of a new instance when accessed from the descriptor

+
+ +
+
+initialize_plot(value, *args, **kwargs)[source]
+

Method that is called when the plot is made the first time

+
+
Parameters:
+

value – The value to use for the initialization

+
+
+
+ +
+
+property iter_data
+

Returns an iterator over the plot data arrays

+
+ +
+
+property iter_raw_data
+

Returns an iterator over the original data arrays

+
+ +
+
+key = None
+

str. Formatoption key of this class in the +Plotter class

+
+ +
+
+property lock
+

A threading.Rlock instance to lock while updating

+

This lock is used when multiple plotter instances are +updated at the same time while sharing formatoptions.

+
+ +
+
+property logger
+

Logger of the plotter

+
+ +
+
+name = None
+

str. A bit more verbose name than the formatoption key to be +included in the gui. If None, the key is used in the gui

+
+ +
+
+parents = []
+

list of str. List of formatoptions that, if included in the update, +prevent the update of this formatoption.

+
+ +
+
+plot_fmt = False
+

bool. Has to be True if the formatoption has a make_plot +method to make the plot.

+
+ +
+
+property plotter
+

Plotter. Plotter instance this +formatoption belongs to

+
+ +
+
+priority = 10
+

int. Priority value of the the formatoption determining when +the formatoption is updated.

+
    +
  • 10: at the end (for labels, etc.)

  • +
  • 20: before the plotting (e.g. for colormaps, etc.)

  • +
  • 30: before loading the data (e.g. for lonlatbox)

  • +
+
+ +
+
+property project
+

Project of the plotter of this instance

+
+ +
+
+property raw_data
+

The original data of the plotter of this formatoption

+
+ +
+
+remove()[source]
+

Method to remove the effects of this formatoption

+

This method is called when the axes is cleared due to a +formatoption with requires_clearing set to True. You don’t +necessarily have to implement this formatoption if your plot results +are removed by the usual matplotlib.axes.Axes.clear() method.

+
+ +
+
+requires_clearing = False
+

bool. True if an update of this formatoption requires a +clearing of the axes and reinitialization of the plot

+
+ +
+
+requires_replot = False
+

Boolean that is True if an update of the formatoption requires a replot

+
+ +
+
+set_data(data, i=None)[source]
+

Replace the data to plot

+

This method may be used to replace the data that is visualized by the +plotter. It changes it’s behaviour depending on whether an +psyplot.data.InteractiveList is visualized or a single +pysplot.data.InteractiveArray

+
+
Parameters:
+
    +
  • data (psyplot.data.InteractiveBase) – The data to insert

  • +
  • i (int) – The position in the InteractiveList where to insert the data (if +the plotter visualizes a list anyway)

  • +
+
+
+

Notes

+

This method uses the Formatoption.data attribute

+
+ +
+
+set_decoder(decoder, i=None)[source]
+

Replace the data to plot

+

This method may be used to replace the data that is visualized by the +plotter. It changes it’s behaviour depending on whether an +psyplot.data.InteractiveList is visualized or a single +pysplot.data.InteractiveArray

+
+
Parameters:
+
    +
  • decoder (psyplot.data.CFDecoder) – The decoder to insert

  • +
  • i (int) – The position in the InteractiveList where to insert the data (if +the plotter visualizes a list anyway)

  • +
+
+
+
+ +
+
+set_value(value, validate=True, todefault=False)[source]
+

Set (and validate) the value in the plotter. This method is called by +the plotter when it attempts to change the value of the formatoption.

+
+
Parameters:
+
    +
  • value – Value to set

  • +
  • validate (bool) – if True, validate the value before it is set

  • +
  • todefault (bool) – True if the value is updated to the default value

  • +
+
+
+
+ +
+
+share(fmto, initializing=False, **kwargs)[source]
+

Share the settings of this formatoption with other data objects

+
+
Parameters:
+
    +
  • fmto (Formatoption) – The Formatoption instance to share the attributes with

  • +
  • **kwargs – Any other keyword argument that shall be passed to the update +method of fmto

  • +
+
+
+
+ +
+
+shared = {}
+

set of the Formatoption instance that are shared +with this instance.

+
+ +
+
+property shared_by
+

None if the formatoption is not controlled by another formatoption +of another plotter, otherwise the corresponding Formatoption +instance

+
+ +
+
+abstract update(value)[source]
+

Method that is call to update the formatoption on the axes

+
+
Parameters:
+

value – Value to update

+
+
+
+ +
+
+update_after_plot = False
+

bool. True if this formatoption needs an update after the plot +has changed

+
+ +
+
+property validate
+

Validation method of the formatoption

+
+ +
+
+property value
+

Value of the formatoption in the corresponding plotter or +the shared value

+
+ +
+
+property value2pickle
+

The value that can be used when pickling the information of the project

+
+ +
+
+property value2share
+

The value that is passed to shared formatoptions (by default, the +value attribute)

+
+ +
+ +
+
+class psyplot.plotter.FormatoptionMeta(clsname, bases, dct)[source]
+

Bases: ABCMeta

+

Meta class for formatoptions

+

This class serves as a meta class for formatoptions and allows a more +efficient docstring generation by using the +psyplot.docstring.docstrings when creating a new formatoption +class

+

Assign an automatic documentation to the formatoption

+
+ +
+
+class psyplot.plotter.Plotter(data=None, ax=None, auto_update=None, project=None, draw=False, make_plot=True, clear=False, enable_post=False, **kwargs)[source]
+

Bases: dict

+

Interactive plotting object for one or more data arrays

+

This class is the base for the interactive plotting with the psyplot +module. It capabilities are determined by it’s descriptor classes that are +derived from the Formatoption class

+
+
Parameters:
+
    +
  • data (InteractiveArray or ArrayList, optional) – Data object that shall be visualized. If given and plot is True, +the initialize_plot() method is called at the end. Otherwise +you can call this method later by yourself

  • +
  • ax (matplotlib.axes.Axes) – Matplotlib Axes to plot on. If None, a new one will be created as +soon as the initialize_plot() method is called

  • +
  • auto_update (bool) – Default: None. A boolean indicating whether this list shall +automatically update the contained arrays when calling the +update() method or not. See also the no_auto_update +attribute. If None, the value from the 'lists.auto_update' +key in the psyplot.rcParams dictionary is used.

  • +
  • draw (bool or None) – Boolean to control whether the figure of this array shall be drawn +at the end. If None, it defaults to the ‘auto_draw’` parameter +in the psyplot.rcParams dictionary

  • +
  • make_plot (bool) – If True, and data is not None, the plot is initialized. Otherwise +only the framework between plotter and data is set up

  • +
  • clear (bool) – If True, the axes is cleared first

  • +
  • enable_post (bool) – If True, the post formatoption is enabled and post +processing scripts are allowed

  • +
  • **kwargs – Any formatoption key from the formatoptions attribute that +shall be used

  • +
+
+
+

Attributes:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

ax

Axes instance of the plot

base_variables

A mapping from the base_variable names to the variables

changed

dict containing the key value pairs that are not the default

data

The psyplot.InteractiveBase instance of this plotter

enable_post

bool that has to be True if the post processing script in the post formatoption should be enabled

figs2draw

All figures that have been manipulated through sharing and the own figure.

fmt_groups

A mapping from the formatoption group to the formatoptions

groups

A mapping from the group short name to the group description

include_links([value])

Temporarily include links in the key descriptions from show_keys(), show_docs() and show_summaries().

iter_base_variables

A mapping from the base_variable names to the variables

logger

logging.Logger of this plotter

no_auto_update

bool.

no_validation

Temporarily disable the validation

plot_data

The data that is used for plotting

plot_data_decoder

The decoder to use for the formatoptions.

post

Apply your own postprocessing script

post_timing

Determine when to run the post formatoption

project

psyplot.project.Project instance this plotter belongs to

rc

Default values for this plotter

+

Methods:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

check_data(name, dims, is_unstructured)

A validation method for the data shape

check_key(key[, raise_error])

Checks whether the key is a valid formatoption

convert_coordinate(coord, *variables)

Convert a coordinate to units necessary for the plot.

draw()

Draw the figures and those that are shared and have been changed

get_enhanced_attrs(arr[, axes])

get_vfunc(key)

Return the validation function for a specified formatoption

has_changed(key[, include_last])

Determine whether a formatoption changed in the last update

initialize_plot([data, ax, make_plot, ...])

Initialize the plot for a data array

make_plot()

Method for making the plot

reinit([draw, clear])

Reinitializes the plot with the same data and on the same axes.

share(plotters[, keys, draw, auto_update])

Share the formatoptions of this plotter with others

show()

Shows all open figures

show_docs([keys, indent])

Classmethod to print the full documentations of the formatoptions

show_keys([keys, indent, grouped, func, ...])

Classmethod to return a nice looking table with the given formatoptions

show_summaries([keys, indent])

Classmethod to print the summaries of the formatoptions

start_update([draw, queues, update_shared])

Conduct the registered plot updates

unshare(plotters[, keys, auto_update, draw])

Close the sharing connection of this plotter with others

unshare_me([keys, auto_update, draw, ...])

Close the sharing connection of this plotter with others

update([fmt, replot, auto_update, draw, ...])

Update the formatoptions and the plot

+
+
+property ax
+

Axes instance of the plot

+
+ +
+
+property base_variables
+

A mapping from the base_variable names to the variables

+
+ +
+
+property changed
+

dict containing the key value pairs that are not the +default

+
+ +
+
+classmethod check_data(name, dims, is_unstructured)[source]
+

A validation method for the data shape

+

The default method does nothing and should be subclassed to validate +the results. If the plotter accepts a InteractiveList, it +should accept a list for name and dims

+
+
Parameters:
+
    +
  • name (str or list of str) – The variable name(s) of the data

  • +
  • dims (list of str or list of lists of str) – The dimension name(s) of the data

  • +
  • is_unstructured (bool or list of bool) – True if the corresponding array is unstructured

  • +
+
+
Returns:
+

    +
  • list of bool or None – True, if everything is okay, False in case of a serious error, +None if it is intermediate. Each object in this list corresponds to +one in the given name

  • +
  • list of str – The message giving more information on the reason. Each object in +this list corresponds to one in the given name

  • +
+

+
+
+
+ +
+
+check_key(key, raise_error=True, *args, **kwargs)[source]
+

Checks whether the key is a valid formatoption

+
+
Parameters:
+
    +
  • key (str) – Key to check

  • +
  • raise_error (bool) – If not True, a list of similar keys is returned

  • +
  • msg (str) – The additional message that shall be used if no close match to +key is found

  • +
  • *args – They are passed to the difflib.get_close_matches() function +(i.e. n to increase the number of returned similar keys and +cutoff to change the sensibility)

  • +
  • **kwargs – They are passed to the difflib.get_close_matches() function +(i.e. n to increase the number of returned similar keys and +cutoff to change the sensibility)

  • +
+
+
Returns:
+

    +
  • str – The key if it is a valid string, else an empty string

  • +
  • list – A list of similar formatoption strings (if found)

  • +
  • str – An error message which includes

  • +
+

+
+
Raises:
+

KeyError – If the key is not a valid formatoption and raise_error is True

+
+
+
+ +
+
+convert_coordinate(coord, *variables)[source]
+

Convert a coordinate to units necessary for the plot.

+

This method takes a single coordinate variable (e.g. the bounds of a +coordinate, or the coordinate itself) and transforms the units that the +plotter requires.

+

One might also provide additional variables that are supposed to be +on the same unit, in case the given coord does not specify a units +attribute. coord might be a CF-conform bounds variable, and one of +the variables might be the corresponding coordinate.

+
+
Parameters:
+
    +
  • coord (xr.Variable) – The variable to transform

  • +
  • *variables – The variables that are on the same unit as coord

  • +
+
+
Returns:
+

The transformed coord

+
+
Return type:
+

xr.Variable

+
+
+

Notes

+

This method is supposed to be implemented by subclasses. The default +implementation by the Plotter class does nothing.

+
+ +
+
+property data
+

The psyplot.InteractiveBase instance of this plotter

+
+ +
+
+draw()[source]
+

Draw the figures and those that are shared and have been changed

+
+ +
+
+enable_post = False
+

bool that has to be True if the post processing script in +the post formatoption should be enabled

+
+ +
+
+property figs2draw
+

All figures that have been manipulated through sharing and the own +figure.

+

Notes

+

Using this property set will reset the figures too draw

+
+ +
+
+property fmt_groups
+

A mapping from the formatoption group to the formatoptions

+
+ +
+
+get_enhanced_attrs(arr, axes=['x', 'y', 't', 'z'])[source]
+
+ +
+
+get_vfunc(key)[source]
+

Return the validation function for a specified formatoption

+
+
Parameters:
+

key (str) – Formatoption key in the rc dictionary

+
+
Returns:
+

Validation function for this formatoption

+
+
Return type:
+

function

+
+
+
+ +
+
+property groups
+

A mapping from the group short name to the group description

+
+ +
+
+has_changed(key, include_last=True)[source]
+

Determine whether a formatoption changed in the last update

+
+
Parameters:
+
    +
  • key (str) – A formatoption key contained in this plotter

  • +
  • include_last (bool) – if True and the formatoption has been included in the last update, +the return value will not be None. Otherwise the return value will +only be not None if it changed during the last update

  • +
+
+
Returns:
+

    +
  • None, if the value has not been changed during the last update or +key is not a valid formatoption key

  • +
  • a list of length two with the old value in the first place and +the given value at the second

  • +
+

+
+
Return type:
+

None or list

+
+
+
+ +
+ +

Temporarily include links in the key descriptions from +show_keys(), show_docs() and show_summaries(). +Note that this is a class attribute, so each change to the value of this +attribute will affect all instances and subclasses

+
+ +
+
+initialize_plot(data=None, ax=None, make_plot=True, clear=False, draw=False, remove=False, priority=None)[source]
+

Initialize the plot for a data array

+
+
Parameters:
+
    +
  • data (InteractiveArray or ArrayList, optional) –

    Data object that shall be visualized.

    +
      +
    • If not None and plot is True, the given data is visualized.

    • +
    • If None and the data attribute is not None, the data in +the data attribute is visualized

    • +
    • If both are None, nothing is done.

    • +
    +

  • +
  • ax (matplotlib.axes.Axes) – Matplotlib Axes to plot on. If None, a new one will be created as +soon as the initialize_plot() method is called

  • +
  • make_plot (bool) – If True, and data is not None, the plot is initialized. Otherwise +only the framework between plotter and data is set up

  • +
  • clear (bool) – If True, the axes is cleared first

  • +
  • draw (bool or None) – Boolean to control whether the figure of this array shall be drawn +at the end. If None, it defaults to the ‘auto_draw’` parameter +in the psyplot.rcParams dictionary

  • +
  • remove (bool) – If True, old effects by the formatoptions in this plotter are +undone first

  • +
  • priority (int) – If given, initialize only the formatoption with the given priority. +This value must be out of START, BEFOREPLOTTING or +END

  • +
+
+
+
+ +
+
+property iter_base_variables
+

A mapping from the base_variable names to the variables

+
+ +
+
+property logger
+

logging.Logger of this plotter

+
+ +
+
+make_plot()[source]
+

Method for making the plot

+

This method is called at the end of the BEFOREPLOTTING stage if +and only if the plot_fmt attribute is set to True

+
+ +
+
+property no_auto_update
+

bool. Boolean controlling whether the start_update() +method is automatically called by the update() method

+
+

Examples

+

You can disable the automatic update via

+
>>> with data.no_auto_update:
+...     data.update(time=1)
+...     data.start_update()
+
+
+

To permanently disable the automatic update, simply set

+
>>> data.no_auto_update = True
+>>> data.update(time=1)
+>>> data.no_auto_update = False  # reenable automatical update
+
+
+
+
+ +
+
+property no_validation
+

Temporarily disable the validation

+
+

Examples

+

Although it is not recommended to set a value with disabled validation, +you can disable it via:

+
>>> with plotter.no_validation:
+...     plotter["ticksize"] = "x"
+...
+
+
+

To permanently disable the validation, simply set

+
>>> plotter.no_validation = True
+>>> plotter["ticksize"] = "x"
+>>> plotter.no_validation = False  # reenable validation
+
+
+
+
+ +
+
+property plot_data
+

The data that is used for plotting

+
+ +
+
+plot_data_decoder = None
+

The decoder to use for the formatoptions. If None, the decoder of the +raw data is used

+
+ +
+
+post
+

Apply your own postprocessing script

+

This formatoption let’s you apply your own post processing script. Just +enter the script as a string and it will be executed. The formatoption +will be made available via the self variable

+

Possible types

+
    +
  • None – Don’t do anything

  • +
  • str – The post processing script as string

  • +
+
+

Note

+

This formatoption uses the built-in exec() function to compile the +script. Since this poses a security risk when loading psyplot projects, +it is by default disabled through the Plotter.enable_post +attribute. If you are sure that you can trust the script in this +formatoption, set this attribute of the corresponding Plotter to +True

+
+
+

Examples

+

Assume, you want to manually add the mean of the data to the title of the +matplotlib axes. You can simply do this via

+
from psyplot.plotter import Plotter
+from xarray import DataArray
+
+plotter = Plotter(DataArray([1, 2, 3]))
+# enable the post formatoption
+plotter.enable_post = True
+plotter.update(post="self.ax.set_title(str(self.data.mean()))")
+plotter.ax.get_title()
+"2.0"
+
+
+

By default, the post formatoption is only ran, when it is explicitly +updated. However, you can use the post_timing formatoption, to +run it automatically. E.g. for running it after every update of the +plotter, you can set

+
plotter.update(post_timing="always")
+
+
+
+
+

See also

+
+
post_timing

Determine the timing of this formatoption

+
+
+
+
+ +
+
+post_timing
+

Determine when to run the post formatoption

+

This formatoption determines, whether the post formatoption +should be run never, after replot or after every update.

+

Possible types

+
    +
  • ‘never’ – Never run post processing scripts

  • +
  • ‘always’ – Always run post processing scripts

  • +
  • ‘replot’ – Only run post processing scripts when the data changes or a replot +is necessary

  • +
+
+

See also

+
+
post

The post processing formatoption

+
+
+
+
+ +
+
+property project
+

psyplot.project.Project instance this plotter belongs to

+
+ +
+
+property rc
+

Default values for this plotter

+

This SubDict stores the default values +for this plotter. A modification of the dictionary does not affect +other plotter instances unless you set the +trace attribute to True

+
+ +
+
+reinit(draw=None, clear=False)[source]
+

Reinitializes the plot with the same data and on the same axes.

+
+
Parameters:
+
    +
  • draw (bool or None) – Boolean to control whether the figure of this array shall be drawn +at the end. If None, it defaults to the ‘auto_draw’` parameter +in the psyplot.rcParams dictionary

  • +
  • clear (bool) – Whether to clear the axes or not

  • +
+
+
+
+

Warning

+

The axes may be cleared when calling this method (even if clear is +set to False)!

+
+
+ +
+
+share(plotters, keys=None, draw=None, auto_update=False)[source]
+

Share the formatoptions of this plotter with others

+

This method shares the formatoptions of this Plotter instance +with others to make sure that, if the formatoption of this changes, +those of the others change as well

+
+
Parameters:
+
    +
  • plotters (list of Plotter instances or a Plotter) – The plotters to share the formatoptions with

  • +
  • keys (string or iterable of strings) – The formatoptions to share, or group names of formatoptions to +share all formatoptions of that group (see the +fmt_groups property). If None, all formatoptions of this +plotter are unshared.

  • +
  • draw (bool or None) – Boolean to control whether the figure of this array shall be drawn +at the end. If None, it defaults to the ‘auto_draw’` parameter +in the psyplot.rcParams dictionary

  • +
  • auto_update (bool) – Boolean determining whether or not the start_update() method +is called at the end. This parameter has no effect if the +no_auto_update attribute is set to True.

  • +
+
+
+
+

See also

+

unshare, unshare_me

+
+
+ +
+
+show()[source]
+

Shows all open figures

+
+ +
+
+classmethod show_docs(keys=None, indent=0, *args, **kwargs)[source]
+

Classmethod to print the full documentations of the formatoptions

+
+
Parameters:
+
    +
  • keys (list of str or None) – If None, the all formatoptions of the given class are used. Group +names from the psyplot.plotter.groups mapping are replaced +by the formatoptions

  • +
  • indent (int) – The indentation of the table

  • +
  • grouped (bool, optional) – If True, the formatoptions are grouped corresponding to the +Formatoption.groupname attribute

  • +
  • func (function or None) – The function the is used for returning (by default it is printed +via the print() function or (when using the gui) in the +help explorer). The given function must take a string as argument

  • +
  • include_links (bool or None, optional) – Default False. If True, links (in restructured formats) are +included in the description. If None, the behaviour is determined +by the psyplot.plotter.Plotter.include_links attribute.

  • +
+
+
Returns:
+

The enhanced list of the formatoptions

+
+
Return type:
+

list of str

+
+
+
+

See also

+

show_keys, show_docs

+
+
+ +
+
+classmethod show_keys(keys=None, indent=0, grouped=False, func=None, include_links=False, *args, **kwargs)[source]
+

Classmethod to return a nice looking table with the given formatoptions

+
+
Parameters:
+
    +
  • keys (list of str or None) – If None, the all formatoptions of the given class are used. Group +names from the psyplot.plotter.groups mapping are replaced +by the formatoptions

  • +
  • indent (int) – The indentation of the table

  • +
  • grouped (bool, optional) – If True, the formatoptions are grouped corresponding to the +Formatoption.groupname attribute

  • +
  • func (function or None) – The function the is used for returning (by default it is printed +via the print() function or (when using the gui) in the +help explorer). The given function must take a string as argument

  • +
  • include_links (bool or None, optional) – Default False. If True, links (in restructured formats) are +included in the description. If None, the behaviour is determined +by the psyplot.plotter.Plotter.include_links attribute.

  • +
+
+
Returns:
+

The enhanced list of the formatoptions

+
+
Return type:
+

list of str

+
+
Returns:
+

None if func is the print function, otherwise anything else

+
+
Return type:
+

results of func

+
+
+
+

See also

+

show_summaries, show_docs

+
+
+ +
+
+classmethod show_summaries(keys=None, indent=0, *args, **kwargs)[source]
+

Classmethod to print the summaries of the formatoptions

+
+
Parameters:
+
    +
  • keys (list of str or None) – If None, the all formatoptions of the given class are used. Group +names from the psyplot.plotter.groups mapping are replaced +by the formatoptions

  • +
  • indent (int) – The indentation of the table

  • +
  • grouped (bool, optional) – If True, the formatoptions are grouped corresponding to the +Formatoption.groupname attribute

  • +
  • func (function or None) – The function the is used for returning (by default it is printed +via the print() function or (when using the gui) in the +help explorer). The given function must take a string as argument

  • +
  • include_links (bool or None, optional) – Default False. If True, links (in restructured formats) are +included in the description. If None, the behaviour is determined +by the psyplot.plotter.Plotter.include_links attribute.

  • +
+
+
Returns:
+

The enhanced list of the formatoptions

+
+
Return type:
+

list of str

+
+
+
+

See also

+

show_keys, show_docs

+
+
+ +
+
+start_update(draw=None, queues=None, update_shared=True)[source]
+

Conduct the registered plot updates

+

This method starts the updates from what has been registered by the +update() method. You can call this method if you did not set the +auto_update parameter to True when calling the update() method +and when the no_auto_update attribute is True.

+
+
Parameters:
+
    +
  • draw (bool or None) – Boolean to control whether the figure of this array shall be drawn +at the end. If None, it defaults to the ‘auto_draw’` parameter +in the psyplot.rcParams dictionary

  • +
  • queues (list of Queue.Queue instances) – The queues that are passed to the +psyplot.plotter.Plotter.start_update() method to ensure a +thread-safe update. It can be None if only one single plotter is +updated at the same time. The number of jobs that are taken from +the queue is determined by the _njobs() attribute. Note that +there this parameter is automatically configured when updating +from a Project.

  • +
+
+
Returns:
+

A boolean indicating whether a redrawing is necessary or not

+
+
Return type:
+

bool

+
+
+
+

See also

+

no_auto_update, update

+
+
+ +
+
+unshare(plotters, keys=None, auto_update=False, draw=None)[source]
+

Close the sharing connection of this plotter with others

+

This method undoes the sharing connections made by the share() +method and releases the given plotters again, such that the +formatoptions in this plotter may be updated again to values different +from this one.

+
+
Parameters:
+
    +
  • plotters (list of Plotter instances or a Plotter) – The plotters to release

  • +
  • keys (string or iterable of strings) – The formatoptions to unshare, or group names of formatoptions to +unshare all formatoptions of that group (see the +fmt_groups property). If None, all formatoptions of this +plotter are unshared.

  • +
  • draw (bool or None) – Boolean to control whether the figure of this array shall be drawn +at the end. If None, it defaults to the ‘auto_draw’` parameter +in the psyplot.rcParams dictionary

  • +
  • auto_update (bool) – Boolean determining whether or not the start_update() method +is called at the end. This parameter has no effect if the +no_auto_update attribute is set to True.

  • +
+
+
+
+

See also

+

share, unshare_me

+
+
+ +
+
+unshare_me(keys=None, auto_update=False, draw=None, update_other=True)[source]
+

Close the sharing connection of this plotter with others

+

This method undoes the sharing connections made by the share() +method and release this plotter again.

+
+
Parameters:
+
    +
  • keys (string or iterable of strings) – The formatoptions to unshare, or group names of formatoptions to +unshare all formatoptions of that group (see the +fmt_groups property). If None, all formatoptions of this +plotter are unshared.

  • +
  • draw (bool or None) – Boolean to control whether the figure of this array shall be drawn +at the end. If None, it defaults to the ‘auto_draw’` parameter +in the psyplot.rcParams dictionary

  • +
  • auto_update (bool) – Boolean determining whether or not the start_update() method +is called at the end. This parameter has no effect if the +no_auto_update attribute is set to True.

  • +
+
+
+
+

See also

+

share, unshare

+
+
+ +
+
+update(fmt={}, replot=False, auto_update=False, draw=None, force=False, todefault=False, **kwargs)[source]
+

Update the formatoptions and the plot

+

If the data attribute of this plotter is None, the plotter is +updated like a usual dictionary (see dict.update()). Otherwise +the update is registered and the plot is updated if auto_update is +True or if the start_update() method is called (see below).

+
+
Parameters:
+
    +
  • fmt (dict) – Keys can be any valid formatoptions with the corresponding values +(see the formatoptions attribute)

  • +
  • replot (bool) – Boolean that determines whether the data specific formatoptions +shall be updated in any case or not.

  • +
  • force (str, list of str or bool) – If formatoption key (i.e. string) or list of formatoption keys, +thery are definitely updated whether they changed or not. +If True, all the given formatoptions in this call of the are +update() method are updated

  • +
  • todefault (bool) – If True, all changed formatoptions (except the registered ones) +are updated to their default value as stored in the +rc attribute

  • +
  • draw (bool or None) – Boolean to control whether the figure of this array shall be drawn +at the end. If None, it defaults to the ‘auto_draw’` parameter +in the psyplot.rcParams dictionary

  • +
  • queues (list of Queue.Queue instances) – The queues that are passed to the +psyplot.plotter.Plotter.start_update() method to ensure a +thread-safe update. It can be None if only one single plotter is +updated at the same time. The number of jobs that are taken from +the queue is determined by the _njobs() attribute. Note that +there this parameter is automatically configured when updating +from a Project.

  • +
  • auto_update (bool) – Boolean determining whether or not the start_update() method +is called at the end. This parameter has no effect if the +no_auto_update attribute is set to True.

  • +
  • **kwargs – Any other formatoption that shall be updated (additionally to those +in fmt)

  • +
+
+
+

Notes

+

If the no_auto_update attribute is True and the given +auto_update parameter are is False, the update of the plots are +registered and conducted at the next call of the start_update() +method or the next call of this method (if the auto_update parameter +is then True).

+
+ +
+ +
+
+class psyplot.plotter.PostProcDependencies[source]
+

Bases: object

+

The dependencies of this formatoption

+
+ +
+
+class psyplot.plotter.PostProcessing(key, plotter=None, index_in_list=None, additional_children=[], additional_dependencies=[], **kwargs)[source]
+

Bases: Formatoption

+

Apply your own postprocessing script

+

This formatoption let’s you apply your own post processing script. Just +enter the script as a string and it will be executed. The formatoption +will be made available via the self variable

+

Possible types

+
    +
  • None – Don’t do anything

  • +
  • str – The post processing script as string

  • +
+
+

Note

+

This formatoption uses the built-in exec() function to compile the +script. Since this poses a security risk when loading psyplot projects, +it is by default disabled through the Plotter.enable_post +attribute. If you are sure that you can trust the script in this +formatoption, set this attribute of the corresponding Plotter to +True

+
+
+

Examples

+

Assume, you want to manually add the mean of the data to the title of the +matplotlib axes. You can simply do this via

+
from psyplot.plotter import Plotter
+from xarray import DataArray
+
+plotter = Plotter(DataArray([1, 2, 3]))
+# enable the post formatoption
+plotter.enable_post = True
+plotter.update(post="self.ax.set_title(str(self.data.mean()))")
+plotter.ax.get_title()
+"2.0"
+
+
+

By default, the post formatoption is only ran, when it is explicitly +updated. However, you can use the post_timing formatoption, to +run it automatically. E.g. for running it after every update of the +plotter, you can set

+
plotter.update(post_timing="always")
+
+
+
+
+

See also

+
+
post_timing

Determine the timing of this formatoption

+
+
+
+
+
Parameters:
+
    +
  • key (str) – formatoption key in the plotter

  • +
  • plotter (psyplot.plotter.Plotter) – Plotter instance that holds this formatoption. If None, it is +assumed that this instance serves as a descriptor.

  • +
  • index_in_list (int or None) – The index that shall be used if the data is a +psyplot.InteractiveList

  • +
  • additional_children (list or str) – Additional children to use (see the children attribute)

  • +
  • additional_dependencies (list or str) – Additional dependencies to use (see the dependencies +attribute)

  • +
  • **kwargs – Further keywords may be used to specify different names for +children, dependencies and connection formatoptions that match the +setup of the plotter. Hence, keywords may be anything of the +children, dependencies and connections +attributes, with values being the name of the new formatoption in +this plotter.

  • +
+
+
+

Attributes:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + +

children

list of str.

data_dependent

True if the corresponding post_timing formatoption is set to 'replot' to run the post processing script after every change of the data

default

dependencies

list of str.

group

str.

name

str.

post_timing

post_timing Formatoption instance in the plotter

priority

int.

+

Methods:

+ + + + + + + + + +

update(value)

Method that is call to update the formatoption on the axes

validate(value)

Validation method of the formatoption

+
+
+children = ['post_timing']
+

list of str. List of formatoptions that have to be updated before this +one is updated. Those formatoptions are only updated if they exist in +the update parameters.

+
+ +
+
+property data_dependent
+

True if the corresponding post_timing +formatoption is set to 'replot' to run the post processing script +after every change of the data

+
+ +
+
+default = None
+
+ +
+
+dependencies = []
+

list of str. List of formatoptions that force an update of this +formatoption if they are updated.

+
+ +
+
+group = 'post_processing'
+

str. Key of the group name in groups of this +formatoption keyword

+
+ +
+
+name = 'Custom post processing script'
+

str. A bit more verbose name than the formatoption key to be +included in the gui. If None, the key is used in the gui

+
+ +
+
+property post_timing
+

post_timing Formatoption instance in the plotter

+
+ +
+
+priority = -inf
+

int. Priority value of the the formatoption determining when +the formatoption is updated.

+
    +
  • 10: at the end (for labels, etc.)

  • +
  • 20: before the plotting (e.g. for colormaps, etc.)

  • +
  • 30: before loading the data (e.g. for lonlatbox)

  • +
+
+ +
+
+update(value)[source]
+

Method that is call to update the formatoption on the axes

+
+
Parameters:
+

value – Value to update

+
+
+
+ +
+
+static validate(value)[source]
+

Validation method of the formatoption

+
+ +
+ +
+
+class psyplot.plotter.PostTiming(key, plotter=None, index_in_list=None, additional_children=[], additional_dependencies=[], **kwargs)[source]
+

Bases: Formatoption

+

Determine when to run the post formatoption

+

This formatoption determines, whether the post formatoption +should be run never, after replot or after every update.

+

Possible types

+
    +
  • ‘never’ – Never run post processing scripts

  • +
  • ‘always’ – Always run post processing scripts

  • +
  • ‘replot’ – Only run post processing scripts when the data changes or a replot +is necessary

  • +
+
+

See also

+
+
post

The post processing formatoption

+
+
+
+
+
Parameters:
+
    +
  • key (str) – formatoption key in the plotter

  • +
  • plotter (psyplot.plotter.Plotter) – Plotter instance that holds this formatoption. If None, it is +assumed that this instance serves as a descriptor.

  • +
  • index_in_list (int or None) – The index that shall be used if the data is a +psyplot.InteractiveList

  • +
  • additional_children (list or str) – Additional children to use (see the children attribute)

  • +
  • additional_dependencies (list or str) – Additional dependencies to use (see the dependencies +attribute)

  • +
  • **kwargs – Further keywords may be used to specify different names for +children, dependencies and connection formatoptions that match the +setup of the plotter. Hence, keywords may be anything of the +children, dependencies and connections +attributes, with values being the name of the new formatoption in +this plotter.

  • +
+
+
+

Attributes:

+ + + + + + + + + + + + + + + +

default

group

str.

name

str.

priority

int.

+

Methods:

+ + + + + + + + + + + + +

get_fmt_widget(parent, project)

Get a widget to update the formatoption in the GUI

update(value)

Method that is call to update the formatoption on the axes

validate(value)

Validation method of the formatoption

+
+
+default = 'never'
+
+ +
+
+get_fmt_widget(parent, project)[source]
+

Get a widget to update the formatoption in the GUI

+

This method should return a QWidget that is loaded by the psyplot-gui +when the formatoption is selected in the +psyplot_gui.main.Mainwindow.fmt_widget. It should call the +insert_text() method +when the update text for the formatoption should be changed.

+
+
Parameters:
+
+
+
Returns:
+

The widget to control the formatoption

+
+
Return type:
+

PyQt5.QtWidgets.QWidget

+
+
+
+ +
+
+group = 'post_processing'
+

str. Key of the group name in groups of this +formatoption keyword

+
+ +
+
+name = 'Timing of the post processing'
+

str. A bit more verbose name than the formatoption key to be +included in the gui. If None, the key is used in the gui

+
+ +
+
+priority = -inf
+

int. Priority value of the the formatoption determining when +the formatoption is updated.

+
    +
  • 10: at the end (for labels, etc.)

  • +
  • 20: before the plotting (e.g. for colormaps, etc.)

  • +
  • 30: before loading the data (e.g. for lonlatbox)

  • +
+
+ +
+
+update(value)[source]
+

Method that is call to update the formatoption on the axes

+
+
Parameters:
+

value – Value to update

+
+
+
+ +
+
+static validate(value)[source]
+

Validation method of the formatoption

+
+ +
+ +
+
+psyplot.plotter.START = 30
+

Priority value of formatoptions that are updated before the data is loaded.

+
+ +
+
+psyplot.plotter.default_print_func()
+

the default function to use when printing formatoption infos (the default is +use print or in the gui, use the help explorer)

+
+ +
+
+psyplot.plotter.format_time(x)[source]
+

Formats date values

+

This function formats datetime.datetime and +datetime.timedelta objects (and the corresponding numpy objects) +using the xarray.core.formatting.format_timestamp() and the +xarray.core.formatting.format_timedelta() functions.

+
+
Parameters:
+

x (object) – The value to format. If not a time object, the value is returned

+
+
Returns:
+

Either the formatted time object or the initial x

+
+
Return type:
+

str or x

+
+
+
+ +
+
+psyplot.plotter.groups = {'axes': 'Axes formatoptions', 'colors': 'Color coding formatoptions', 'data': 'Data manipulation formatoptions', 'labels': 'Label formatoptions', 'masking': 'Masking formatoptions', 'misc': 'Miscallaneous formatoptions', 'plotting': 'Plot formatoptions', 'post_processing': 'Post processing formatoptions', 'regression': 'Fitting formatoptions', 'ticks': 'Axis tick formatoptions', 'vector': 'Vector plot formatoptions'}
+

dict. Mapping from group to group names

+
+ +
+
+psyplot.plotter.is_data_dependent(fmto, data)[source]
+

Check whether a formatoption is data dependent

+
+
Parameters:
+
+
+
Returns:
+

True, if the formatoption depends on the data

+
+
Return type:
+

bool

+
+
+
+ + + +
+
+ + + +
+
+
+
+ + + + \ No newline at end of file diff --git a/api/psyplot.project.html b/api/psyplot.project.html new file mode 100644 index 0000000..cf9bd0b --- /dev/null +++ b/api/psyplot.project.html @@ -0,0 +1,6290 @@ + + + + + + + <no title> — psyplot documentation + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Project module of the psyplot Package.

+

This module contains the Project class that serves as the main +part of the psyplot API. One instance of the Project class serves as +coordinator of multiple plots and can be distributed into subprojects that +keep reference to the main project without holding all array instances

+

Furthermore this module contains an easy pyplot-like API to the current +subproject.

+

Classes:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + +

DataArrayPlotter(da, *args, **kwargs)

Interface between the xarray.Dataset and the psyplot project

DataArrayPlotterInterface(methodname, ...[, ...])

Interface for the DataArrayPlotter to a plotter

DatasetPlotter(ds, *args, **kwargs)

Interface between the xarray.Dataset and the psyplot project

DatasetPlotterInterface(methodname, module, ...)

Interface for the DatasetPlotter to a plotter

PROJECT_CLS

The project class that is used for creating new projects

PlotterInterface(methodname, module, ...[, ...])

Base class for visualizing a data array from an predefined plotter

Project(*args, **kwargs)

A manager of multiple interactive data projects

ProjectPlotter([project])

Plotting methods of the psyplot.project.Project class

+

Functions:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + +

close([num, figs, data, ds, remove_only])

Close the project

gcp([main])

Get the current project

get_project_nums()

Returns the project numbers of the open projects

multiple_subplots([rows, cols, maxplots, n, ...])

Function to create subplots.

project([num])

Create a new main project

register_plotter(identifier, module, ...[, ...])

Register a psyplot.plotter.Plotter for the projects

scp(project)

Set the current project

unregister_plotter(identifier[, sorter, ...])

Unregister a psyplot.plotter.Plotter for the projects

+

Data:

+ + + + + + +

plot

ProjectPlotter of the current project.

+
+
+class psyplot.project.DataArrayPlotter(da, *args, **kwargs)[source]
+

Bases: ProjectPlotter

+

Interface between the xarray.Dataset and the psyplot project

+

This class can be used to make new plots from a given dataset and add them +to the current psyplot.project()

+

Attributes:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

barplot(*args, **kwargs)

Make a bar plot of one-dimensional data

combined(*args, **kwargs)

Plot a 2D scalar field with an overlying vector field

density(*args, **kwargs)

Make a density plot of point data

fldmean(*args, **kwargs)

Calculate and plot the mean over x- and y-dimensions

lineplot(*args, **kwargs)

Make a line plot of one-dimensional data

mapcombined(*args, **kwargs)

Plot a 2D scalar field with an overlying vector field on a map

mapplot(*args, **kwargs)

Plot a 2D scalar field on a map

mapvector(*args, **kwargs)

Plot a 2D vector field on a map

plot2d(*args, **kwargs)

Make a simple plot of a 2D scalar field

vector(*args, **kwargs)

Make a simple plot of a 2D vector field

violinplot(*args, **kwargs)

Make a violin plot of your data

+
+
+barplot(*args, **kwargs)
+

Make a bar plot of one-dimensional data

+
+
+

This plotting method visualizes the data via a +psy_simple.plotters.BarPlotter plotters

+

To plot a variable in this dataset, type:

+
>>> da.psy.plot.barplot()
+
+
+

Possible formatoptions are

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

alpha

axiscolor

background

categorical

color

coord

figtitle

figtitleprops

figtitlesize

figtitleweight

grid

labelprops

labelsize

labelweight

legend

legendlabels

mask

maskbetween

maskgeq

maskgreater

maskleq

maskless

plot

post

post_timing

sym_lims

text

ticksize

tickweight

tight

title

titleprops

titlesize

titleweight

transpose

widths

xlabel

xlim

xrotation

xticklabels

xtickprops

xticks

ylabel

ylim

yrotation

yticklabels

ytickprops

yticks

+
+
+

Examples

+

To explore the formatoptions and their documentations, use the +keys, summaries and docs methods. For example:

+
# show the keys corresponding to a group or multiple
+# formatopions
+>>> da.psy.plot.barplot.keys('labels')
+
+# show the summaries of a group of formatoptions or of a
+# formatoption
+>>> da.psy.plot.barplot.summaries('title')
+
+# show the full documentation
+>>> da.psy.plot.barplot.docs('plot')
+
+# or access the documentation via the attribute
+>>> da.psy.plot.barplot.plot
+
+
+
+
+ +
+
+combined(*args, **kwargs)
+

Plot a 2D scalar field with an overlying vector field

+
+
+

This plotting method visualizes the data via a +psy_simple.plotters.CombinedSimplePlotter plotters

+

To plot a variable in this dataset, type:

+
>>> da.psy.plot.combined()
+
+
+

Possible formatoptions are

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

arrowsize

arrowstyle

axiscolor

background

bounds

cbar

cbarspacing

clabel

clabelprops

clabelsize

clabelweight

cmap

color

cticklabels

ctickprops

cticks

cticksize

ctickweight

datagrid

density

extend

figtitle

figtitleprops

figtitlesize

figtitleweight

grid

interp_bounds

labelprops

labelsize

labelweight

levels

linewidth

mask

mask_datagrid

maskbetween

maskgeq

maskgreater

maskleq

maskless

miss_color

plot

post

post_timing

sym_lims

text

ticksize

tickweight

tight

title

titleprops

titlesize

titleweight

transpose

vbounds

vcbar

vcbarspacing

vclabel

vclabelprops

vclabelsize

vclabelweight

vcmap

vcticklabels

vctickprops

vcticks

vcticksize

vctickweight

vplot

xlabel

xlim

xrotation

xticklabels

xtickprops

xticks

ylabel

ylim

yrotation

yticklabels

ytickprops

yticks

+
+
+

Examples

+

To explore the formatoptions and their documentations, use the +keys, summaries and docs methods. For example:

+
# show the keys corresponding to a group or multiple
+# formatopions
+>>> da.psy.plot.combined.keys('labels')
+
+# show the summaries of a group of formatoptions or of a
+# formatoption
+>>> da.psy.plot.combined.summaries('title')
+
+# show the full documentation
+>>> da.psy.plot.combined.docs('plot')
+
+# or access the documentation via the attribute
+>>> da.psy.plot.combined.plot
+
+
+
+
+ +
+
+density(*args, **kwargs)
+

Make a density plot of point data

+
+
+

This plotting method visualizes the data via a +psy_simple.plotters.DensityPlotter plotters

+

To plot a variable in this dataset, type:

+
>>> da.psy.plot.density()
+
+
+

Possible formatoptions are

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

axiscolor

background

bins

bounds

cbar

cbarspacing

clabel

clabelprops

clabelsize

clabelweight

cmap

coord

cticklabels

ctickprops

cticks

cticksize

ctickweight

datagrid

density

extend

figtitle

figtitleprops

figtitlesize

figtitleweight

grid

interp_bounds

labelprops

labelsize

labelweight

levels

mask

mask_datagrid

maskbetween

maskgeq

maskgreater

maskleq

maskless

miss_color

normed

plot

post

post_timing

precision

sym_lims

text

ticksize

tickweight

tight

title

titleprops

titlesize

titleweight

transpose

xlabel

xlim

xrange

xrotation

xticklabels

xtickprops

xticks

ylabel

ylim

yrange

yrotation

yticklabels

ytickprops

yticks

+
+
+

Examples

+

To explore the formatoptions and their documentations, use the +keys, summaries and docs methods. For example:

+
# show the keys corresponding to a group or multiple
+# formatopions
+>>> da.psy.plot.density.keys('labels')
+
+# show the summaries of a group of formatoptions or of a
+# formatoption
+>>> da.psy.plot.density.summaries('title')
+
+# show the full documentation
+>>> da.psy.plot.density.docs('plot')
+
+# or access the documentation via the attribute
+>>> da.psy.plot.density.plot
+
+
+
+
+ +
+
+fldmean(*args, **kwargs)
+

Calculate and plot the mean over x- and y-dimensions

+
+
+

This plotting method visualizes the data via a +psy_simple.plotters.FldmeanPlotter plotters

+

To plot a variable in this dataset, type:

+
>>> da.psy.plot.fldmean()
+
+
+

Possible formatoptions are

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

axiscolor

background

color

coord

err_calc

error

erroralpha

figtitle

figtitleprops

figtitlesize

figtitleweight

grid

labelprops

labelsize

labelweight

legend

legendlabels

linewidth

marker

markersize

mask

maskbetween

maskgeq

maskgreater

maskleq

maskless

mean

plot

post

post_timing

sym_lims

text

ticksize

tickweight

tight

title

titleprops

titlesize

titleweight

transpose

xlabel

xlim

xrotation

xticklabels

xtickprops

xticks

ylabel

ylim

yrotation

yticklabels

ytickprops

yticks

+
+
+

Examples

+

To explore the formatoptions and their documentations, use the +keys, summaries and docs methods. For example:

+
# show the keys corresponding to a group or multiple
+# formatopions
+>>> da.psy.plot.fldmean.keys('labels')
+
+# show the summaries of a group of formatoptions or of a
+# formatoption
+>>> da.psy.plot.fldmean.summaries('title')
+
+# show the full documentation
+>>> da.psy.plot.fldmean.docs('plot')
+
+# or access the documentation via the attribute
+>>> da.psy.plot.fldmean.plot
+
+
+
+
+ +
+
+lineplot(*args, **kwargs)
+

Make a line plot of one-dimensional data

+
+
+

This plotting method visualizes the data via a +psy_simple.plotters.LinePlotter plotters

+

To plot a variable in this dataset, type:

+
>>> da.psy.plot.lineplot()
+
+
+

Possible formatoptions are

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

axiscolor

background

color

coord

error

erroralpha

figtitle

figtitleprops

figtitlesize

figtitleweight

grid

labelprops

labelsize

labelweight

legend

legendlabels

linewidth

marker

markersize

mask

maskbetween

maskgeq

maskgreater

maskleq

maskless

plot

post

post_timing

sym_lims

text

ticksize

tickweight

tight

title

titleprops

titlesize

titleweight

transpose

xlabel

xlim

xrotation

xticklabels

xtickprops

xticks

ylabel

ylim

yrotation

yticklabels

ytickprops

yticks

+
+
+

Examples

+

To explore the formatoptions and their documentations, use the +keys, summaries and docs methods. For example:

+
# show the keys corresponding to a group or multiple
+# formatopions
+>>> da.psy.plot.lineplot.keys('labels')
+
+# show the summaries of a group of formatoptions or of a
+# formatoption
+>>> da.psy.plot.lineplot.summaries('title')
+
+# show the full documentation
+>>> da.psy.plot.lineplot.docs('plot')
+
+# or access the documentation via the attribute
+>>> da.psy.plot.lineplot.plot
+
+
+
+
+ +
+
+mapcombined(*args, **kwargs)
+

Plot a 2D scalar field with an overlying vector field on a map

+
+
+

This plotting method visualizes the data via a +psy_maps.plotters.CombinedPlotter plotters

+

To plot a variable in this dataset, type:

+
>>> da.psy.plot.mapcombined()
+
+
+

Possible formatoptions are

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

arrowsize

arrowstyle

background

bounds

cbar

cbarspacing

clabel

clabelprops

clabelsize

clabelweight

clat

clip

clon

cmap

color

cticklabels

ctickprops

cticks

cticksize

ctickweight

datagrid

density

extend

figtitle

figtitleprops

figtitlesize

figtitleweight

google_map_detail

grid_color

grid_labels

grid_labelsize

grid_settings

interp_bounds

levels

linewidth

lonlatbox

lsm

map_extent

mask

mask_datagrid

maskbetween

maskgeq

maskgreater

maskleq

maskless

miss_color

plot

post

post_timing

projection

stock_img

text

tight

title

titleprops

titlesize

titleweight

transform

transpose

vbounds

vcbar

vcbarspacing

vclabel

vclabelprops

vclabelsize

vclabelweight

vcmap

vcticklabels

vctickprops

vcticks

vcticksize

vctickweight

vplot

xgrid

ygrid

+
+
+

Examples

+

To explore the formatoptions and their documentations, use the +keys, summaries and docs methods. For example:

+
# show the keys corresponding to a group or multiple
+# formatopions
+>>> da.psy.plot.mapcombined.keys('labels')
+
+# show the summaries of a group of formatoptions or of a
+# formatoption
+>>> da.psy.plot.mapcombined.summaries('title')
+
+# show the full documentation
+>>> da.psy.plot.mapcombined.docs('plot')
+
+# or access the documentation via the attribute
+>>> da.psy.plot.mapcombined.plot
+
+
+
+
+ +
+
+mapplot(*args, **kwargs)
+

Plot a 2D scalar field on a map

+
+
+

This plotting method visualizes the data via a +psy_maps.plotters.FieldPlotter plotters

+

To plot a variable in this dataset, type:

+
>>> da.psy.plot.mapplot()
+
+
+

Possible formatoptions are

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

background

bounds

cbar

cbarspacing

clabel

clabelprops

clabelsize

clabelweight

clat

clip

clon

cmap

cticklabels

ctickprops

cticks

cticksize

ctickweight

datagrid

extend

figtitle

figtitleprops

figtitlesize

figtitleweight

google_map_detail

grid_color

grid_labels

grid_labelsize

grid_settings

interp_bounds

levels

lonlatbox

lsm

map_extent

mask

mask_datagrid

maskbetween

maskgeq

maskgreater

maskleq

maskless

miss_color

plot

post

post_timing

projection

stock_img

text

tight

title

titleprops

titlesize

titleweight

transform

transpose

xgrid

ygrid

+
+
+

Examples

+

To explore the formatoptions and their documentations, use the +keys, summaries and docs methods. For example:

+
# show the keys corresponding to a group or multiple
+# formatopions
+>>> da.psy.plot.mapplot.keys('labels')
+
+# show the summaries of a group of formatoptions or of a
+# formatoption
+>>> da.psy.plot.mapplot.summaries('title')
+
+# show the full documentation
+>>> da.psy.plot.mapplot.docs('plot')
+
+# or access the documentation via the attribute
+>>> da.psy.plot.mapplot.plot
+
+
+
+
+ +
+
+mapvector(*args, **kwargs)
+

Plot a 2D vector field on a map

+
+
+

This plotting method visualizes the data via a +psy_maps.plotters.VectorPlotter plotters

+

To plot a variable in this dataset, type:

+
>>> da.psy.plot.mapvector()
+
+
+

Possible formatoptions are

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

arrowsize

arrowstyle

background

bounds

cbar

cbarspacing

clabel

clabelprops

clabelsize

clabelweight

clat

clip

clon

cmap

color

cticklabels

ctickprops

cticks

cticksize

ctickweight

datagrid

density

extend

figtitle

figtitleprops

figtitlesize

figtitleweight

google_map_detail

grid_color

grid_labels

grid_labelsize

grid_settings

linewidth

lonlatbox

lsm

map_extent

mask

mask_datagrid

maskbetween

maskgeq

maskgreater

maskleq

maskless

plot

post

post_timing

projection

stock_img

text

tight

title

titleprops

titlesize

titleweight

transform

transpose

xgrid

ygrid

+
+
+

Examples

+

To explore the formatoptions and their documentations, use the +keys, summaries and docs methods. For example:

+
# show the keys corresponding to a group or multiple
+# formatopions
+>>> da.psy.plot.mapvector.keys('labels')
+
+# show the summaries of a group of formatoptions or of a
+# formatoption
+>>> da.psy.plot.mapvector.summaries('title')
+
+# show the full documentation
+>>> da.psy.plot.mapvector.docs('plot')
+
+# or access the documentation via the attribute
+>>> da.psy.plot.mapvector.plot
+
+
+
+
+ +
+
+plot2d(*args, **kwargs)
+

Make a simple plot of a 2D scalar field

+
+
+

This plotting method visualizes the data via a +psy_simple.plotters.Simple2DPlotter plotters

+

To plot a variable in this dataset, type:

+
>>> da.psy.plot.plot2d()
+
+
+

Possible formatoptions are

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

axiscolor

background

bounds

cbar

cbarspacing

clabel

clabelprops

clabelsize

clabelweight

cmap

cticklabels

ctickprops

cticks

cticksize

ctickweight

datagrid

extend

figtitle

figtitleprops

figtitlesize

figtitleweight

grid

interp_bounds

labelprops

labelsize

labelweight

levels

mask

mask_datagrid

maskbetween

maskgeq

maskgreater

maskleq

maskless

miss_color

plot

post

post_timing

sym_lims

text

ticksize

tickweight

tight

title

titleprops

titlesize

titleweight

transpose

xlabel

xlim

xrotation

xticklabels

xtickprops

xticks

ylabel

ylim

yrotation

yticklabels

ytickprops

yticks

+
+
+

Examples

+

To explore the formatoptions and their documentations, use the +keys, summaries and docs methods. For example:

+
# show the keys corresponding to a group or multiple
+# formatopions
+>>> da.psy.plot.plot2d.keys('labels')
+
+# show the summaries of a group of formatoptions or of a
+# formatoption
+>>> da.psy.plot.plot2d.summaries('title')
+
+# show the full documentation
+>>> da.psy.plot.plot2d.docs('plot')
+
+# or access the documentation via the attribute
+>>> da.psy.plot.plot2d.plot
+
+
+
+
+ +
+
+vector(*args, **kwargs)
+

Make a simple plot of a 2D vector field

+
+
+

This plotting method visualizes the data via a +psy_simple.plotters.SimpleVectorPlotter plotters

+

To plot a variable in this dataset, type:

+
>>> da.psy.plot.vector()
+
+
+

Possible formatoptions are

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

arrowsize

arrowstyle

axiscolor

background

bounds

cbar

cbarspacing

clabel

clabelprops

clabelsize

clabelweight

cmap

color

cticklabels

ctickprops

cticks

cticksize

ctickweight

datagrid

density

extend

figtitle

figtitleprops

figtitlesize

figtitleweight

grid

labelprops

labelsize

labelweight

linewidth

mask

mask_datagrid

maskbetween

maskgeq

maskgreater

maskleq

maskless

plot

post

post_timing

sym_lims

text

ticksize

tickweight

tight

title

titleprops

titlesize

titleweight

transpose

xlabel

xlim

xrotation

xticklabels

xtickprops

xticks

ylabel

ylim

yrotation

yticklabels

ytickprops

yticks

+
+
+

Examples

+

To explore the formatoptions and their documentations, use the +keys, summaries and docs methods. For example:

+
# show the keys corresponding to a group or multiple
+# formatopions
+>>> da.psy.plot.vector.keys('labels')
+
+# show the summaries of a group of formatoptions or of a
+# formatoption
+>>> da.psy.plot.vector.summaries('title')
+
+# show the full documentation
+>>> da.psy.plot.vector.docs('plot')
+
+# or access the documentation via the attribute
+>>> da.psy.plot.vector.plot
+
+
+
+
+ +
+
+violinplot(*args, **kwargs)
+

Make a violin plot of your data

+
+
+

This plotting method visualizes the data via a +psy_simple.plotters.ViolinPlotter plotters

+

To plot a variable in this dataset, type:

+
>>> da.psy.plot.violinplot()
+
+
+

Possible formatoptions are

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

axiscolor

background

color

figtitle

figtitleprops

figtitlesize

figtitleweight

grid

labelprops

labelsize

labelweight

legend

legendlabels

mask

maskbetween

maskgeq

maskgreater

maskleq

maskless

plot

post

post_timing

sym_lims

text

ticksize

tickweight

tight

title

titleprops

titlesize

titleweight

transpose

xlabel

xlim

xrotation

xticklabels

xtickprops

xticks

ylabel

ylim

yrotation

yticklabels

ytickprops

yticks

+
+
+

Examples

+

To explore the formatoptions and their documentations, use the +keys, summaries and docs methods. For example:

+
# show the keys corresponding to a group or multiple
+# formatopions
+>>> da.psy.plot.violinplot.keys('labels')
+
+# show the summaries of a group of formatoptions or of a
+# formatoption
+>>> da.psy.plot.violinplot.summaries('title')
+
+# show the full documentation
+>>> da.psy.plot.violinplot.docs('plot')
+
+# or access the documentation via the attribute
+>>> da.psy.plot.violinplot.plot
+
+
+
+
+ +
+ +
+
+class psyplot.project.DataArrayPlotterInterface(methodname, module, plotter_name, project_plotter=None)[source]
+

Bases: PlotterInterface

+

Interface for the DataArrayPlotter to a plotter

+

Methods:

+ + + + + + +

check_data(*args, **kwargs)

Check whether the plotter of this plot method can visualize the data

+
+
+check_data(*args, **kwargs)[source]
+

Check whether the plotter of this plot method can visualize the data

+
+ +
+ +
+
+class psyplot.project.DatasetPlotter(ds, *args, **kwargs)[source]
+

Bases: ProjectPlotter

+

Interface between the xarray.Dataset and the psyplot project

+

This class can be used to make new plots from a given dataset and add them +to the current psyplot.project()

+

Attributes:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

barplot(*args, **kwargs)

Make a bar plot of one-dimensional data

combined(*args, **kwargs)

Plot a 2D scalar field with an overlying vector field

density(*args, **kwargs)

Make a density plot of point data

fldmean(*args, **kwargs)

Calculate and plot the mean over x- and y-dimensions

lineplot(*args, **kwargs)

Make a line plot of one-dimensional data

mapcombined(*args, **kwargs)

Plot a 2D scalar field with an overlying vector field on a map

mapplot(*args, **kwargs)

Plot a 2D scalar field on a map

mapvector(*args, **kwargs)

Plot a 2D vector field on a map

plot2d(*args, **kwargs)

Make a simple plot of a 2D scalar field

vector(*args, **kwargs)

Make a simple plot of a 2D vector field

violinplot(*args, **kwargs)

Make a violin plot of your data

+
+
+barplot(*args, **kwargs)
+

Make a bar plot of one-dimensional data

+
+
+

This plotting method adds data arrays and plots them via +psy_simple.plotters.BarPlotter plotters

+

To plot a variable in this dataset, type:

+
>>> ds.psy.plot.barplot(name=['my_variable'], ...)
+
+
+

Possible formatoptions are

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

alpha

axiscolor

background

categorical

color

coord

figtitle

figtitleprops

figtitlesize

figtitleweight

grid

labelprops

labelsize

labelweight

legend

legendlabels

mask

maskbetween

maskgeq

maskgreater

maskleq

maskless

plot

post

post_timing

sym_lims

text

ticksize

tickweight

tight

title

titleprops

titlesize

titleweight

transpose

widths

xlabel

xlim

xrotation

xticklabels

xtickprops

xticks

ylabel

ylim

yrotation

yticklabels

ytickprops

yticks

+
+
+

Examples

+

To explore the formatoptions and their documentations, use the +keys, summaries and docs methods. For example:

+
# show the keys corresponding to a group or multiple
+# formatopions
+>>> ds.psy.plot.barplot.keys('labels')
+
+# show the summaries of a group of formatoptions or of a
+# formatoption
+>>> ds.psy.plot.barplot.summaries('title')
+
+# show the full documentation
+>>> ds.psy.plot.barplot.docs('plot')
+
+# or access the documentation via the attribute
+>>> ds.psy.plot.barplot.plot
+
+
+
+
+ +
+
+combined(*args, **kwargs)
+

Plot a 2D scalar field with an overlying vector field

+
+
+

This plotting method adds data arrays and plots them via +psy_simple.plotters.CombinedSimplePlotter plotters

+

To plot a variable in this dataset, type:

+
>>> ds.psy.plot.combined(name=[['my_variable', ['u_var', 'v_var']]], ...)
+
+
+

Possible formatoptions are

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

arrowsize

arrowstyle

axiscolor

background

bounds

cbar

cbarspacing

clabel

clabelprops

clabelsize

clabelweight

cmap

color

cticklabels

ctickprops

cticks

cticksize

ctickweight

datagrid

density

extend

figtitle

figtitleprops

figtitlesize

figtitleweight

grid

interp_bounds

labelprops

labelsize

labelweight

levels

linewidth

mask

mask_datagrid

maskbetween

maskgeq

maskgreater

maskleq

maskless

miss_color

plot

post

post_timing

sym_lims

text

ticksize

tickweight

tight

title

titleprops

titlesize

titleweight

transpose

vbounds

vcbar

vcbarspacing

vclabel

vclabelprops

vclabelsize

vclabelweight

vcmap

vcticklabels

vctickprops

vcticks

vcticksize

vctickweight

vplot

xlabel

xlim

xrotation

xticklabels

xtickprops

xticks

ylabel

ylim

yrotation

yticklabels

ytickprops

yticks

+
+
+

Examples

+

To explore the formatoptions and their documentations, use the +keys, summaries and docs methods. For example:

+
# show the keys corresponding to a group or multiple
+# formatopions
+>>> ds.psy.plot.combined.keys('labels')
+
+# show the summaries of a group of formatoptions or of a
+# formatoption
+>>> ds.psy.plot.combined.summaries('title')
+
+# show the full documentation
+>>> ds.psy.plot.combined.docs('plot')
+
+# or access the documentation via the attribute
+>>> ds.psy.plot.combined.plot
+
+
+
+
+ +
+
+density(*args, **kwargs)
+

Make a density plot of point data

+
+
+

This plotting method adds data arrays and plots them via +psy_simple.plotters.DensityPlotter plotters

+

To plot a variable in this dataset, type:

+
>>> ds.psy.plot.density(name=['my_variable'], ...)
+
+
+

Possible formatoptions are

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

axiscolor

background

bins

bounds

cbar

cbarspacing

clabel

clabelprops

clabelsize

clabelweight

cmap

coord

cticklabels

ctickprops

cticks

cticksize

ctickweight

datagrid

density

extend

figtitle

figtitleprops

figtitlesize

figtitleweight

grid

interp_bounds

labelprops

labelsize

labelweight

levels

mask

mask_datagrid

maskbetween

maskgeq

maskgreater

maskleq

maskless

miss_color

normed

plot

post

post_timing

precision

sym_lims

text

ticksize

tickweight

tight

title

titleprops

titlesize

titleweight

transpose

xlabel

xlim

xrange

xrotation

xticklabels

xtickprops

xticks

ylabel

ylim

yrange

yrotation

yticklabels

ytickprops

yticks

+
+
+

Examples

+

To explore the formatoptions and their documentations, use the +keys, summaries and docs methods. For example:

+
# show the keys corresponding to a group or multiple
+# formatopions
+>>> ds.psy.plot.density.keys('labels')
+
+# show the summaries of a group of formatoptions or of a
+# formatoption
+>>> ds.psy.plot.density.summaries('title')
+
+# show the full documentation
+>>> ds.psy.plot.density.docs('plot')
+
+# or access the documentation via the attribute
+>>> ds.psy.plot.density.plot
+
+
+
+
+ +
+
+fldmean(*args, **kwargs)
+

Calculate and plot the mean over x- and y-dimensions

+
+
+

This plotting method adds data arrays and plots them via +psy_simple.plotters.FldmeanPlotter plotters

+

To plot a variable in this dataset, type:

+
>>> ds.psy.plot.fldmean(name=['my_variable'], ...)
+
+
+

Possible formatoptions are

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

axiscolor

background

color

coord

err_calc

error

erroralpha

figtitle

figtitleprops

figtitlesize

figtitleweight

grid

labelprops

labelsize

labelweight

legend

legendlabels

linewidth

marker

markersize

mask

maskbetween

maskgeq

maskgreater

maskleq

maskless

mean

plot

post

post_timing

sym_lims

text

ticksize

tickweight

tight

title

titleprops

titlesize

titleweight

transpose

xlabel

xlim

xrotation

xticklabels

xtickprops

xticks

ylabel

ylim

yrotation

yticklabels

ytickprops

yticks

+
+
+

Examples

+

To explore the formatoptions and their documentations, use the +keys, summaries and docs methods. For example:

+
# show the keys corresponding to a group or multiple
+# formatopions
+>>> ds.psy.plot.fldmean.keys('labels')
+
+# show the summaries of a group of formatoptions or of a
+# formatoption
+>>> ds.psy.plot.fldmean.summaries('title')
+
+# show the full documentation
+>>> ds.psy.plot.fldmean.docs('plot')
+
+# or access the documentation via the attribute
+>>> ds.psy.plot.fldmean.plot
+
+
+
+
+ +
+
+lineplot(*args, **kwargs)
+

Make a line plot of one-dimensional data

+
+
+

This plotting method adds data arrays and plots them via +psy_simple.plotters.LinePlotter plotters

+

To plot a variable in this dataset, type:

+
>>> ds.psy.plot.lineplot(name=['my_variable'], ...)
+
+
+

Possible formatoptions are

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

axiscolor

background

color

coord

error

erroralpha

figtitle

figtitleprops

figtitlesize

figtitleweight

grid

labelprops

labelsize

labelweight

legend

legendlabels

linewidth

marker

markersize

mask

maskbetween

maskgeq

maskgreater

maskleq

maskless

plot

post

post_timing

sym_lims

text

ticksize

tickweight

tight

title

titleprops

titlesize

titleweight

transpose

xlabel

xlim

xrotation

xticklabels

xtickprops

xticks

ylabel

ylim

yrotation

yticklabels

ytickprops

yticks

+
+
+

Examples

+

To explore the formatoptions and their documentations, use the +keys, summaries and docs methods. For example:

+
# show the keys corresponding to a group or multiple
+# formatopions
+>>> ds.psy.plot.lineplot.keys('labels')
+
+# show the summaries of a group of formatoptions or of a
+# formatoption
+>>> ds.psy.plot.lineplot.summaries('title')
+
+# show the full documentation
+>>> ds.psy.plot.lineplot.docs('plot')
+
+# or access the documentation via the attribute
+>>> ds.psy.plot.lineplot.plot
+
+
+
+
+ +
+
+mapcombined(*args, **kwargs)
+

Plot a 2D scalar field with an overlying vector field on a map

+
+
+

This plotting method adds data arrays and plots them via +psy_maps.plotters.CombinedPlotter plotters

+

To plot a variable in this dataset, type:

+
>>> ds.psy.plot.mapcombined(name=[['my_variable', ['u_var', 'v_var']]], ...)
+
+
+

Possible formatoptions are

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

arrowsize

arrowstyle

background

bounds

cbar

cbarspacing

clabel

clabelprops

clabelsize

clabelweight

clat

clip

clon

cmap

color

cticklabels

ctickprops

cticks

cticksize

ctickweight

datagrid

density

extend

figtitle

figtitleprops

figtitlesize

figtitleweight

google_map_detail

grid_color

grid_labels

grid_labelsize

grid_settings

interp_bounds

levels

linewidth

lonlatbox

lsm

map_extent

mask

mask_datagrid

maskbetween

maskgeq

maskgreater

maskleq

maskless

miss_color

plot

post

post_timing

projection

stock_img

text

tight

title

titleprops

titlesize

titleweight

transform

transpose

vbounds

vcbar

vcbarspacing

vclabel

vclabelprops

vclabelsize

vclabelweight

vcmap

vcticklabels

vctickprops

vcticks

vcticksize

vctickweight

vplot

xgrid

ygrid

+
+
+

Examples

+

To explore the formatoptions and their documentations, use the +keys, summaries and docs methods. For example:

+
# show the keys corresponding to a group or multiple
+# formatopions
+>>> ds.psy.plot.mapcombined.keys('labels')
+
+# show the summaries of a group of formatoptions or of a
+# formatoption
+>>> ds.psy.plot.mapcombined.summaries('title')
+
+# show the full documentation
+>>> ds.psy.plot.mapcombined.docs('plot')
+
+# or access the documentation via the attribute
+>>> ds.psy.plot.mapcombined.plot
+
+
+
+
+ +
+
+mapplot(*args, **kwargs)
+

Plot a 2D scalar field on a map

+
+
+

This plotting method adds data arrays and plots them via +psy_maps.plotters.FieldPlotter plotters

+

To plot a variable in this dataset, type:

+
>>> ds.psy.plot.mapplot(name=['my_variable'], ...)
+
+
+

Possible formatoptions are

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

background

bounds

cbar

cbarspacing

clabel

clabelprops

clabelsize

clabelweight

clat

clip

clon

cmap

cticklabels

ctickprops

cticks

cticksize

ctickweight

datagrid

extend

figtitle

figtitleprops

figtitlesize

figtitleweight

google_map_detail

grid_color

grid_labels

grid_labelsize

grid_settings

interp_bounds

levels

lonlatbox

lsm

map_extent

mask

mask_datagrid

maskbetween

maskgeq

maskgreater

maskleq

maskless

miss_color

plot

post

post_timing

projection

stock_img

text

tight

title

titleprops

titlesize

titleweight

transform

transpose

xgrid

ygrid

+
+
+

Examples

+

To explore the formatoptions and their documentations, use the +keys, summaries and docs methods. For example:

+
# show the keys corresponding to a group or multiple
+# formatopions
+>>> ds.psy.plot.mapplot.keys('labels')
+
+# show the summaries of a group of formatoptions or of a
+# formatoption
+>>> ds.psy.plot.mapplot.summaries('title')
+
+# show the full documentation
+>>> ds.psy.plot.mapplot.docs('plot')
+
+# or access the documentation via the attribute
+>>> ds.psy.plot.mapplot.plot
+
+
+
+
+ +
+
+mapvector(*args, **kwargs)
+

Plot a 2D vector field on a map

+
+
+

This plotting method adds data arrays and plots them via +psy_maps.plotters.VectorPlotter plotters

+

To plot a variable in this dataset, type:

+
>>> ds.psy.plot.mapvector(name=[['u_var', 'v_var']], ...)
+
+
+

Possible formatoptions are

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

arrowsize

arrowstyle

background

bounds

cbar

cbarspacing

clabel

clabelprops

clabelsize

clabelweight

clat

clip

clon

cmap

color

cticklabels

ctickprops

cticks

cticksize

ctickweight

datagrid

density

extend

figtitle

figtitleprops

figtitlesize

figtitleweight

google_map_detail

grid_color

grid_labels

grid_labelsize

grid_settings

linewidth

lonlatbox

lsm

map_extent

mask

mask_datagrid

maskbetween

maskgeq

maskgreater

maskleq

maskless

plot

post

post_timing

projection

stock_img

text

tight

title

titleprops

titlesize

titleweight

transform

transpose

xgrid

ygrid

+
+
+

Examples

+

To explore the formatoptions and their documentations, use the +keys, summaries and docs methods. For example:

+
# show the keys corresponding to a group or multiple
+# formatopions
+>>> ds.psy.plot.mapvector.keys('labels')
+
+# show the summaries of a group of formatoptions or of a
+# formatoption
+>>> ds.psy.plot.mapvector.summaries('title')
+
+# show the full documentation
+>>> ds.psy.plot.mapvector.docs('plot')
+
+# or access the documentation via the attribute
+>>> ds.psy.plot.mapvector.plot
+
+
+
+
+ +
+
+plot2d(*args, **kwargs)
+

Make a simple plot of a 2D scalar field

+
+
+

This plotting method adds data arrays and plots them via +psy_simple.plotters.Simple2DPlotter plotters

+

To plot a variable in this dataset, type:

+
>>> ds.psy.plot.plot2d(name=['my_variable'], ...)
+
+
+

Possible formatoptions are

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

axiscolor

background

bounds

cbar

cbarspacing

clabel

clabelprops

clabelsize

clabelweight

cmap

cticklabels

ctickprops

cticks

cticksize

ctickweight

datagrid

extend

figtitle

figtitleprops

figtitlesize

figtitleweight

grid

interp_bounds

labelprops

labelsize

labelweight

levels

mask

mask_datagrid

maskbetween

maskgeq

maskgreater

maskleq

maskless

miss_color

plot

post

post_timing

sym_lims

text

ticksize

tickweight

tight

title

titleprops

titlesize

titleweight

transpose

xlabel

xlim

xrotation

xticklabels

xtickprops

xticks

ylabel

ylim

yrotation

yticklabels

ytickprops

yticks

+
+
+

Examples

+

To explore the formatoptions and their documentations, use the +keys, summaries and docs methods. For example:

+
# show the keys corresponding to a group or multiple
+# formatopions
+>>> ds.psy.plot.plot2d.keys('labels')
+
+# show the summaries of a group of formatoptions or of a
+# formatoption
+>>> ds.psy.plot.plot2d.summaries('title')
+
+# show the full documentation
+>>> ds.psy.plot.plot2d.docs('plot')
+
+# or access the documentation via the attribute
+>>> ds.psy.plot.plot2d.plot
+
+
+
+
+ +
+
+vector(*args, **kwargs)
+

Make a simple plot of a 2D vector field

+
+
+

This plotting method adds data arrays and plots them via +psy_simple.plotters.SimpleVectorPlotter plotters

+

To plot a variable in this dataset, type:

+
>>> ds.psy.plot.vector(name=[['u_var', 'v_var']], ...)
+
+
+

Possible formatoptions are

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

arrowsize

arrowstyle

axiscolor

background

bounds

cbar

cbarspacing

clabel

clabelprops

clabelsize

clabelweight

cmap

color

cticklabels

ctickprops

cticks

cticksize

ctickweight

datagrid

density

extend

figtitle

figtitleprops

figtitlesize

figtitleweight

grid

labelprops

labelsize

labelweight

linewidth

mask

mask_datagrid

maskbetween

maskgeq

maskgreater

maskleq

maskless

plot

post

post_timing

sym_lims

text

ticksize

tickweight

tight

title

titleprops

titlesize

titleweight

transpose

xlabel

xlim

xrotation

xticklabels

xtickprops

xticks

ylabel

ylim

yrotation

yticklabels

ytickprops

yticks

+
+
+

Examples

+

To explore the formatoptions and their documentations, use the +keys, summaries and docs methods. For example:

+
# show the keys corresponding to a group or multiple
+# formatopions
+>>> ds.psy.plot.vector.keys('labels')
+
+# show the summaries of a group of formatoptions or of a
+# formatoption
+>>> ds.psy.plot.vector.summaries('title')
+
+# show the full documentation
+>>> ds.psy.plot.vector.docs('plot')
+
+# or access the documentation via the attribute
+>>> ds.psy.plot.vector.plot
+
+
+
+
+ +
+
+violinplot(*args, **kwargs)
+

Make a violin plot of your data

+
+
+

This plotting method adds data arrays and plots them via +psy_simple.plotters.ViolinPlotter plotters

+

To plot a variable in this dataset, type:

+
>>> ds.psy.plot.violinplot(name=['my_variable'], ...)
+
+
+

Possible formatoptions are

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

axiscolor

background

color

figtitle

figtitleprops

figtitlesize

figtitleweight

grid

labelprops

labelsize

labelweight

legend

legendlabels

mask

maskbetween

maskgeq

maskgreater

maskleq

maskless

plot

post

post_timing

sym_lims

text

ticksize

tickweight

tight

title

titleprops

titlesize

titleweight

transpose

xlabel

xlim

xrotation

xticklabels

xtickprops

xticks

ylabel

ylim

yrotation

yticklabels

ytickprops

yticks

+
+
+

Examples

+

To explore the formatoptions and their documentations, use the +keys, summaries and docs methods. For example:

+
# show the keys corresponding to a group or multiple
+# formatopions
+>>> ds.psy.plot.violinplot.keys('labels')
+
+# show the summaries of a group of formatoptions or of a
+# formatoption
+>>> ds.psy.plot.violinplot.summaries('title')
+
+# show the full documentation
+>>> ds.psy.plot.violinplot.docs('plot')
+
+# or access the documentation via the attribute
+>>> ds.psy.plot.violinplot.plot
+
+
+
+
+ +
+ +
+
+class psyplot.project.DatasetPlotterInterface(methodname, module, plotter_name, project_plotter=None)[source]
+

Bases: PlotterInterface

+

Interface for the DatasetPlotter to a plotter

+
+ +
+
+psyplot.project.PROJECT_CLS
+

The project class that is used for creating new projects

+

Methods:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

append(*args, **kwargs)

Append a new array to the list

close([figs, data, ds, remove_only])

Close this project instance

disable()

Disables the plotters in this list

docs(*args, **kwargs)

Show the available formatoptions in this project and their full docu

enable()

export(output[, tight, concat, close_pdf, ...])

Exports the figures of the project to one or more image files

extend(*args, **kwargs)

Add further arrays from an iterable to this list

extract_fmts_from_preset(preset, plotmethod)

Extract the formatoptions for a plotmethod from a given preset

format_string(s[, use_time, format_args])

Format a string with the attributes in this project

from_dataset(*args, **kwargs)

Construct an ArrayList instance from an existing base dataset

joined_attrs([delimiter, enhanced, ...])

Join the attributes of the arrays in this project

keys(*args, **kwargs)

Show the available formatoptions in this project

load_preset(preset, **kwargs)

Load a preset from disk and apply it to the open project.

load_project(fname[, auto_update, ...])

Load a project from a file or dict

new([num])

Create a new main project

save_preset([fname, include_defaults, update])

Save the formatoptions of this project as a preset

save_project([fname, pwd, pack])

Save this project to a file

scp(project)

Set the current project

share([base, keys, by])

Share the formatoptions of one plotter with all the others

show()

Shows all open figures

summaries(*args, **kwargs)

Show the available formatoptions and their summaries in this project

unshare(**kwargs)

Unshare the formatoptions of all the plotters in this instance

+

Attributes:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

arr_names

Names of the arrays (!not of the variables!) in this list

axes

A mapping from axes to data objects with the plotter in this axes

barplot

List of data arrays that are plotted by psy_simple.plotters.BarPlotter plotters

block_signals([value])

Wrapper around a boolean defining an __enter__ and __exit__ method

combined

List of data arrays that are plotted by psy_simple.plotters.CombinedSimplePlotter plotters

datasets

A mapping from dataset numbers to datasets in this list

density

List of data arrays that are plotted by psy_simple.plotters.DensityPlotter plotters

dsnames

The set of dataset names in this instance

dsnames_map

A dictionary from the dataset numbers in this list to their filenames

figs

A mapping from figures to data objects with the plotter in this figure

fldmean

List of data arrays that are plotted by psy_simple.plotters.FldmeanPlotter plotters

is_cmp

Boolean that is True if the project is the current main project

is_csp

Boolean that is True if the project is the current subproject

is_main

bool.

lineplot

List of data arrays that are plotted by psy_simple.plotters.LinePlotter plotters

logger

logging.Logger of this instance

main

Project.

mapcombined

List of data arrays that are plotted by psy_maps.plotters.CombinedPlotter plotters

mapplot

List of data arrays that are plotted by psy_maps.plotters.FieldPlotter plotters

maps

List of data arrays that are plotted by psy_maps.plotters.MapPlotter plotters

mapvector

List of data arrays that are plotted by psy_maps.plotters.VectorPlotter plotters

oncpchange

signal to be emiitted when the current main and/or subproject changes

plot

Plotting instance of this Project.

plot2d

List of data arrays that are plotted by psy_simple.plotters.Simple2DPlotter plotters

plotters

A list of all the plotters in this instance

simple

List of data arrays that are plotted by psy_simple.plotters.SimplePlotterBase plotters

vector

List of data arrays that are plotted by psy_simple.plotters.SimpleVectorPlotter plotters

violinplot

List of data arrays that are plotted by psy_simple.plotters.ViolinPlotter plotters

with_plotter

The arrays in this instance that are visualized with a plotter

+
+ +
+
+class psyplot.project.PlotterInterface(methodname, module, plotter_name, project_plotter=None)[source]
+

Bases: object

+

Base class for visualizing a data array from an predefined plotter

+

See the __call__() method for details on plotting.

+

Methods:

+ + + + + + + + + + + + + + + +

check_data(ds, name, dims[, decoder])

A validation method for the data shape

docs(*args, **kwargs)

Method to print the full documentations of the formatoptions

keys(*args, **kwargs)

Classmethod to return a nice looking table with the given formatoptions

summaries(*args, **kwargs)

Method to print the summaries of the formatoptions

+

Attributes:

+ + + + + + + + + + + + +

is_imported

True if the module for this plot method has been imported already

plotter_cls

The plotter class

print_func

The function that is used to return a formatoption

+
+
+check_data(ds, name, dims, decoder=None, *args, **kwargs)[source]
+

A validation method for the data shape

+
+
Parameters:
+
    +
  • name (list of lists of strings) – The variable names (see the +check_data() method of the +plotter_cls attribute for details)

  • +
  • dims (list of dictionaries) – The dimensions of the arrays. It will be enhanced by the default +dimensions of this plot method

  • +
  • is_unstructured (bool or list of bool) – True if the corresponding array is unstructured.

  • +
  • decoder (psyplot.data.CFDecoder, dict or a list of them) – The decoders to use per array. Dictionaries are parsed as keyword +arguments to the psyplot.data.CFDecoder.get_decoder() +method

  • +
+
+
Returns:
+

    +
  • list of bool or None – True, if everything is okay, False in case of a serious error, +None if it is intermediate. Each object in this list corresponds to +one in the given name

  • +
  • list of str – The message giving more information on the reason. Each object in +this list corresponds to one in the given name

  • +
+

+
+
+
+ +
+
+docs(*args, **kwargs)[source]
+

Method to print the full documentations of the formatoptions

+
+
Parameters:
+
    +
  • keys (list of str or None) – If None, the all formatoptions of the given class are used. Group +names from the psyplot.plotter.groups mapping are replaced +by the formatoptions

  • +
  • indent (int) – The indentation of the table

  • +
  • grouped (bool, optional) – If True, the formatoptions are grouped corresponding to the +Formatoption.groupname attribute

  • +
  • func (function or None) – The function the is used for returning (by default it is printed +via the print() function or (when using the gui) in the +help explorer). The given function must take a string as argument

  • +
  • include_links (bool or None, optional) – Default False. If True, links (in restructured formats) are +included in the description. If None, the behaviour is determined +by the psyplot.plotter.Plotter.include_links attribute.

  • +
+
+
Returns:
+

The enhanced list of the formatoptions

+
+
Return type:
+

list of str

+
+
+
+

See also

+

keys, docs

+
+
+ +
+
+property is_imported
+

True if the module for this plot method has been imported already

+
+ +
+
+keys(*args, **kwargs)[source]
+

Classmethod to return a nice looking table with the given formatoptions

+
+
Parameters:
+
    +
  • keys (list of str or None) – If None, the all formatoptions of the given class are used. Group +names from the psyplot.plotter.groups mapping are replaced +by the formatoptions

  • +
  • indent (int) – The indentation of the table

  • +
  • grouped (bool, optional) – If True, the formatoptions are grouped corresponding to the +Formatoption.groupname attribute

  • +
  • func (function or None) – The function the is used for returning (by default it is printed +via the print() function or (when using the gui) in the +help explorer). The given function must take a string as argument

  • +
  • include_links (bool or None, optional) – Default False. If True, links (in restructured formats) are +included in the description. If None, the behaviour is determined +by the psyplot.plotter.Plotter.include_links attribute.

  • +
+
+
Returns:
+

The enhanced list of the formatoptions

+
+
Return type:
+

list of str

+
+
+
+

See also

+

summaries, docs

+
+
+ +
+
+property plotter_cls
+

The plotter class

+
+ +
+
+property print_func
+

The function that is used to return a formatoption

+

By default the print() function is used (i.e. it is printed to +the terminal)

+
+ +
+
+summaries(*args, **kwargs)[source]
+

Method to print the summaries of the formatoptions

+
+
Parameters:
+
    +
  • keys (list of str or None) – If None, the all formatoptions of the given class are used. Group +names from the psyplot.plotter.groups mapping are replaced +by the formatoptions

  • +
  • indent (int) – The indentation of the table

  • +
  • grouped (bool, optional) – If True, the formatoptions are grouped corresponding to the +Formatoption.groupname attribute

  • +
  • func (function or None) – The function the is used for returning (by default it is printed +via the print() function or (when using the gui) in the +help explorer). The given function must take a string as argument

  • +
  • include_links (bool or None, optional) – Default False. If True, links (in restructured formats) are +included in the description. If None, the behaviour is determined +by the psyplot.plotter.Plotter.include_links attribute.

  • +
+
+
Returns:
+

The enhanced list of the formatoptions

+
+
Return type:
+

list of str

+
+
+
+

See also

+

keys, docs

+
+
+ +
+ +
+
+class psyplot.project.Project(*args, **kwargs)[source]
+

Bases: ArrayList

+

A manager of multiple interactive data projects

+
+
Parameters:
+
    +
  • iterable (iterable) – The iterable (e.g. another list) defining this list

  • +
  • attrs (dict-like or iterable, optional) – Global attributes of this list

  • +
  • auto_update (bool) – Default: None. A boolean indicating whether this list shall +automatically update the contained arrays when calling the +update() method or not. See also the no_auto_update +attribute. If None, the value from the 'lists.auto_update' +key in the psyplot.rcParams dictionary is used.

  • +
  • new_name (bool or str) – If False, and the arr_name attribute of the new array is +already in the list, a ValueError is raised. +If True and the arr_name attribute of the new array is not +already in the list, the name is not changed. Otherwise, if the +array name is already in use, new_name is set to ‘arr{0}’. +If not True, this will be used for renaming (if the array name of +arr is in use or not). '{0}' is replaced by a counter

  • +
  • main (Project) – The main project this subproject belongs to (or None if this +project is the main project)

  • +
  • num (int) – The number of the project

  • +
+
+
+

Methods:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

append(*args, **kwargs)

Append a new array to the list

close([figs, data, ds, remove_only])

Close this project instance

disable()

Disables the plotters in this list

docs(*args, **kwargs)

Show the available formatoptions in this project and their full docu

enable()

export(output[, tight, concat, close_pdf, ...])

Exports the figures of the project to one or more image files

extend(*args, **kwargs)

Add further arrays from an iterable to this list

extract_fmts_from_preset(preset, plotmethod)

Extract the formatoptions for a plotmethod from a given preset

format_string(s[, use_time, format_args])

Format a string with the attributes in this project

from_dataset(*args, **kwargs)

Construct an ArrayList instance from an existing base dataset

joined_attrs([delimiter, enhanced, ...])

Join the attributes of the arrays in this project

keys(*args, **kwargs)

Show the available formatoptions in this project

load_preset(preset, **kwargs)

Load a preset from disk and apply it to the open project.

load_project(fname[, auto_update, ...])

Load a project from a file or dict

new([num])

Create a new main project

save_preset([fname, include_defaults, update])

Save the formatoptions of this project as a preset

save_project([fname, pwd, pack])

Save this project to a file

scp(project)

Set the current project

share([base, keys, by])

Share the formatoptions of one plotter with all the others

show()

Shows all open figures

summaries(*args, **kwargs)

Show the available formatoptions and their summaries in this project

unshare(**kwargs)

Unshare the formatoptions of all the plotters in this instance

+

Attributes:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

arr_names

Names of the arrays (!not of the variables!) in this list

axes

A mapping from axes to data objects with the plotter in this axes

barplot

List of data arrays that are plotted by psy_simple.plotters.BarPlotter plotters

block_signals([value])

Wrapper around a boolean defining an __enter__ and __exit__ method

combined

List of data arrays that are plotted by psy_simple.plotters.CombinedSimplePlotter plotters

datasets

A mapping from dataset numbers to datasets in this list

density

List of data arrays that are plotted by psy_simple.plotters.DensityPlotter plotters

dsnames

The set of dataset names in this instance

dsnames_map

A dictionary from the dataset numbers in this list to their filenames

figs

A mapping from figures to data objects with the plotter in this figure

fldmean

List of data arrays that are plotted by psy_simple.plotters.FldmeanPlotter plotters

is_cmp

Boolean that is True if the project is the current main project

is_csp

Boolean that is True if the project is the current subproject

is_main

bool.

lineplot

List of data arrays that are plotted by psy_simple.plotters.LinePlotter plotters

logger

logging.Logger of this instance

main

Project.

mapcombined

List of data arrays that are plotted by psy_maps.plotters.CombinedPlotter plotters

mapplot

List of data arrays that are plotted by psy_maps.plotters.FieldPlotter plotters

maps

List of data arrays that are plotted by psy_maps.plotters.MapPlotter plotters

mapvector

List of data arrays that are plotted by psy_maps.plotters.VectorPlotter plotters

oncpchange

signal to be emiitted when the current main and/or subproject changes

plot

Plotting instance of this Project.

plot2d

List of data arrays that are plotted by psy_simple.plotters.Simple2DPlotter plotters

plotters

A list of all the plotters in this instance

simple

List of data arrays that are plotted by psy_simple.plotters.SimplePlotterBase plotters

vector

List of data arrays that are plotted by psy_simple.plotters.SimpleVectorPlotter plotters

violinplot

List of data arrays that are plotted by psy_simple.plotters.ViolinPlotter plotters

with_plotter

The arrays in this instance that are visualized with a plotter

+
+
+append(*args, **kwargs)[source]
+

Append a new array to the list

+
+
Parameters:
+
    +
  • value (InteractiveBase) – The data object to append to this list

  • +
  • new_name (bool or str) – If False, and the arr_name attribute of the new array is +already in the list, a ValueError is raised. +If True and the arr_name attribute of the new array is not +already in the list, the name is not changed. Otherwise, if the +array name is already in use, new_name is set to ‘arr{0}’. +If not True, this will be used for renaming (if the array name of +arr is in use or not). '{0}' is replaced by a counter

  • +
+
+
Raises:
+
    +
  • ValueError – If it was impossible to find a name that isn’t already in the list

  • +
  • ValueError – If new_name is False and the array is already in the list

  • +
+
+
+
+

See also

+

list.append, extend, rename

+
+
+ +
+
+property arr_names
+

Names of the arrays (!not of the variables!) in this list

+

This attribute can be set with an iterable of unique names to change +the array names of the data objects in this list.

+
+ +
+
+property axes
+

A mapping from axes to data objects with the plotter in this axes

+
+ +
+
+property barplot
+

List of data arrays that are plotted by psy_simple.plotters.BarPlotter plotters

+
+ +
+
+block_signals(value=None)
+

Wrapper around a boolean defining an __enter__ and __exit__ method

+

Notes

+

If you want to use this class as an instance property, rather use the +_temp_bool_prop() because this class as a descriptor is ment to be a +class descriptor

+
+ +
+
+close(figs=True, data=False, ds=False, remove_only=False)[source]
+

Close this project instance

+
+
Parameters:
+
    +
  • figs (bool) – Close the figures

  • +
  • data (bool) – delete the arrays from the (main) project

  • +
  • ds (bool) – If True, close the dataset as well

  • +
  • remove_only (bool) – If True and figs is True, the figures are not closed but the +plotters are removed

  • +
+
+
+
+ +
+
+property combined
+

List of data arrays that are plotted by psy_simple.plotters.CombinedSimplePlotter plotters

+
+ +
+
+property datasets
+

A mapping from dataset numbers to datasets in this list

+
+ +
+
+property density
+

List of data arrays that are plotted by psy_simple.plotters.DensityPlotter plotters

+
+ +
+
+disable()[source]
+

Disables the plotters in this list

+
+ +
+
+docs(*args, **kwargs)[source]
+

Show the available formatoptions in this project and their full docu

+
+
Parameters:
+
    +
  • keys (list of str or None) – If None, the all formatoptions of the given class are used. Group +names from the psyplot.plotter.groups mapping are replaced +by the formatoptions

  • +
  • indent (int) – The indentation of the table

  • +
  • grouped (bool, optional) – If True, the formatoptions are grouped corresponding to the +Formatoption.groupname attribute

  • +
  • func (function or None) – The function the is used for returning (by default it is printed +via the print() function or (when using the gui) in the +help explorer). The given function must take a string as argument

  • +
  • include_links (bool or None, optional) – Default False. If True, links (in restructured formats) are +included in the description. If None, the behaviour is determined +by the psyplot.plotter.Plotter.include_links attribute.

  • +
+
+
Returns:
+

The enhanced list of the formatoptions

+
+
Return type:
+

list of str

+
+
+
+ +
+
+property dsnames
+

The set of dataset names in this instance

+
+ +
+
+property dsnames_map
+

A dictionary from the dataset numbers in this list to their +filenames

+
+ +
+
+enable()[source]
+
+ +
+
+export(output, tight=False, concat=True, close_pdf=None, use_time=False, **kwargs)[source]
+

Exports the figures of the project to one or more image files

+
+
Parameters:
+
    +
  • output (str, iterable or matplotlib.backends.backend_pdf.PdfPages) – if string or list of strings, those define the names of the output +files. Otherwise you may provide an instance of +matplotlib.backends.backend_pdf.PdfPages to save the +figures in it. +If string (or iterable of strings), attribute names in the +xarray.DataArray.attrs attribute as well as index dimensions +are replaced by the respective value (see examples below). +Furthermore a single format string without key (e.g. %i, %s, %d, +etc.) is replaced by a counter.

  • +
  • tight (bool) – If True, it is tried to figure out the tight bbox of the figure +(same as bbox_inches=’tight’)

  • +
  • concat (bool) – if True and the output format is pdf, all figures are +concatenated into one single pdf

  • +
  • close_pdf (bool or None) – If True and the figures are concatenated into one single pdf, +the resulting pdf instance is closed. If False it remains open. +If None and output is a string, it is the same as +close_pdf=True, if None and output is neither a string nor an +iterable, it is the same as close_pdf=False

  • +
  • use_time (bool) – If True, formatting strings for the +datetime.datetime.strftime() are expected to be found in +output (e.g. '%m', '%Y', etc.). If so, other formatting +strings must be escaped by double '%' (e.g. '%%i' +instead of ('%i'))

  • +
  • **kwargs – Any valid keyword for the matplotlib.pyplot.savefig() +function

  • +
+
+
Returns:
+

a PdfPages instance if output is a string and close_pdf is False, +otherwise None

+
+
Return type:
+

matplotlib.backends.backend_pdf.PdfPages or None

+
+
+
+

Examples

+

Simply save all figures into one single pdf:

+
>>> p = psy.gcp()
+>>> p.export('my_plots.pdf')
+
+
+

Save all figures into separate pngs with increasing numbers (e.g. +'my_plots_1.png'):

+
>>> p.export('my_plots_%i.png')
+
+
+

Save all figures into separate pngs with the name of the variables +shown in each figure (e.g. 'my_plots_t2m.png'):

+
>>> p.export('my_plots_%(name)s.png')
+
+
+

Save all figures into separate pngs with the name of the variables +shown in each figure and with increasing numbers (e.g. +'my_plots_1_t2m.png'):

+
>>> p.export('my_plots_%i_%(name)s.png')
+
+
+

Specify the names for each figure directly via a list:

+
>>> p.export(['my_plots1.pdf', 'my_plots2.pdf'])
+
+
+
+
+ +
+
+extend(*args, **kwargs)[source]
+

Add further arrays from an iterable to this list

+
+
Parameters:
+
    +
  • iterable – Any iterable that contains InteractiveBase instances

  • +
  • new_name (bool or str) – If False, and the arr_name attribute of the new array is +already in the list, a ValueError is raised. +If True and the arr_name attribute of the new array is not +already in the list, the name is not changed. Otherwise, if the +array name is already in use, new_name is set to ‘arr{0}’. +If not True, this will be used for renaming (if the array name of +arr is in use or not). '{0}' is replaced by a counter

  • +
+
+
Raises:
+
    +
  • ValueError – If it was impossible to find a name that isn’t already in the list

  • +
  • ValueError – If new_name is False and the array is already in the list

  • +
+
+
+
+

See also

+

list.extend, append, rename

+
+
+ +
+
+static extract_fmts_from_preset(preset: str, plotmethod: str)[source]
+

Extract the formatoptions for a plotmethod from a given preset

+

This method takes the preset and extracts the formatoptions valid for +the given plotmethod

+
+
Parameters:
+
    +
  • %(Project._load_preset.parameters)s

  • +
  • plotmethod (str) – The plotmethod to use

  • +
+
+
+
+ +
+
+property figs
+

A mapping from figures to data objects with the plotter in this +figure

+
+ +
+
+property fldmean
+

List of data arrays that are plotted by psy_simple.plotters.FldmeanPlotter plotters

+
+ +
+
+format_string(s, use_time=False, format_args=None, *args, **kwargs)[source]
+

Format a string with the attributes in this project

+
+
Parameters:
+
    +
  • s (str) – The string that is subject to be formatted

  • +
  • use_time (bool) – If True, formatting strings for the +datetime.datetime.strftime() are expected to be found in +output (e.g. '%m', '%Y', etc.). If so, other formatting +strings must be escaped by double '%' (e.g. '%%i' +instead of ('%i'))

  • +
  • format_args (tuple) – A tuple of arguments that shall be inserted in s via +s % format_args. (There will be no error, when this fails!)

  • +
  • delimiter (str) – The string that shall be used as the delimiter in case that there +are multiple values for one attribute in the arrays. If None, they +will be returned as sets

  • +
  • enhanced (bool) – If True, the psyplot.plotter.Plotter.get_enhanced_attrs() +method is used, otherwise the xarray.DataArray.attrs +attribute is used.

  • +
  • plot_data (bool) – It True, use the psyplot.plotter.Plotter.plot_data +attribute of the plotters rather than the raw data in this project

  • +
  • keep_all (bool) – If True, all formatoptions are kept. Otherwise only the intersection

  • +
+
+
Returns:
+

The formatted string s

+
+
Return type:
+

str

+
+
+
+ +
+
+classmethod from_dataset(*args, **kwargs)[source]
+

Construct an ArrayList instance from an existing base dataset

+
+
Parameters:
+
    +
  • base (xarray.Dataset) – Dataset instance that is used as reference

  • +
  • method ({'isel', None, 'nearest', ...}) – Selection method of the xarray.Dataset to be used for setting the +variables from the informations in dims. +If method is ‘isel’, the xarray.Dataset.isel() method is +used. Otherwise it sets the method parameter for the +xarray.Dataset.sel() method.

  • +
  • auto_update (bool) – Default: None. A boolean indicating whether this list shall +automatically update the contained arrays when calling the +update() method or not. See also the no_auto_update +attribute. If None, the value from the 'lists.auto_update' +key in the psyplot.rcParams dictionary is used.

  • +
  • prefer_list (bool) – If True and multiple variable names pher array are found, the +InteractiveList class is used. Otherwise the arrays are +put together into one InteractiveArray.

  • +
  • default_slice (indexer) – Index (e.g. 0 if method is ‘isel’) that shall be used for +dimensions not covered by dims and furtherdims. If None, the +whole slice will be used. Note that the default_slice is always +based on the isel method.

  • +
  • decoder (CFDecoder or dict) –

    Arguments for the decoder. This can be one of

    +
      +
    • an instance of CFDecoder

    • +
    • a subclass of CFDecoder

    • +
    • a dictionary with keyword-arguments to the automatically +determined decoder class

    • +
    • None to automatically set the decoder

    • +
    +

  • +
  • squeeze (bool, optional) – Default True. If True, and the created arrays have a an axes with +length 1, it is removed from the dimension list (e.g. an array +with shape (3, 4, 1, 5) will be squeezed to shape (3, 4, 5))

  • +
  • attrs (dict, optional) – Meta attributes that shall be assigned to the selected data arrays +(additional to those stored in the base dataset)

  • +
  • load (bool or dict) – If True, load the data from the dataset using the +xarray.DataArray.load() method. If dict, those will +be given to the above mentioned load method

  • +
  • main (Project) – The main project that this project corresponds to

  • +
  • arr_names (string, list of strings or dictionary) –

    Set the unique array names of the resulting arrays and (optionally) +dimensions.

    +
      +
    • if string: same as list of strings (see below). Strings may +include {0} which will be replaced by a counter.

    • +
    • list of strings: those will be used for the array names. The final +number of dictionaries in the return depend in this case on the +dims and **furtherdims

    • +
    • dictionary: +Then nothing happens and an dict version of +arr_names is returned.

    • +
    +

  • +
  • sort (list of strings) – This parameter defines how the dictionaries are ordered. It has no +effect if arr_names is a dictionary (use a +dict for that). It can be a list of +dimension strings matching to the dimensions in dims for the +variable.

  • +
  • dims (dict) – Keys must be variable names of dimensions (e.g. time, level, lat or +lon) or ‘name’ for the variable name you want to choose. +Values must be values of that dimension or iterables of the values +(e.g. lists). Note that strings will be put into a list. +For example dims = {‘name’: ‘t2m’, ‘time’: 0} will result in one plot +for the first time step, whereas dims = {‘name’: ‘t2m’, ‘time’: [0, 1]} +will result in two plots, one for the first (time == 0) and one for the +second (time == 1) time step.

  • +
  • **kwargs – The same as dims (those will update what is specified in dims)

  • +
+
+
Returns:
+

The newly created project instance

+
+
Return type:
+

Project

+
+
+
+ +
+
+property is_cmp
+

Boolean that is True if the project is the current main project

+
+ +
+
+property is_csp
+

Boolean that is True if the project is the current subproject

+
+ +
+
+property is_main
+

bool. True if this Project is a main project

+
+ +
+
+joined_attrs(delimiter=', ', enhanced=True, plot_data=False, keep_all=True)[source]
+

Join the attributes of the arrays in this project

+
+
Parameters:
+
+
+
Returns:
+

A mapping from the attribute to the joined attributes which are +either strings or (if there is only one attribute value), the +data type of the corresponding value

+
+
Return type:
+

dict

+
+
+
+ +
+
+keys(*args, **kwargs)[source]
+

Show the available formatoptions in this project

+
+
Parameters:
+
    +
  • keys (list of str or None) – If None, the all formatoptions of the given class are used. Group +names from the psyplot.plotter.groups mapping are replaced +by the formatoptions

  • +
  • indent (int) – The indentation of the table

  • +
  • grouped (bool, optional) – If True, the formatoptions are grouped corresponding to the +Formatoption.groupname attribute

  • +
  • func (function or None) – The function the is used for returning (by default it is printed +via the print() function or (when using the gui) in the +help explorer). The given function must take a string as argument

  • +
  • include_links (bool or None, optional) – Default False. If True, links (in restructured formats) are +included in the description. If None, the behaviour is determined +by the psyplot.plotter.Plotter.include_links attribute.

  • +
+
+
Returns:
+

The enhanced list of the formatoptions

+
+
Return type:
+

list of str

+
+
+
+ +
+
+property lineplot
+

List of data arrays that are plotted by psy_simple.plotters.LinePlotter plotters

+
+ +
+
+load_preset(preset: str, **kwargs)[source]
+

Load a preset from disk and apply it to the open project.

+

This method loads a preset and updates the corresponding plots

+
+
Parameters:
+
    +
  • preset (str or dict) – The filename or identifier of a preset. If the given preset is +the path to an existing yaml file, it will be loaded. Otherwise we +look up the preset in the psyplot configuration directory (see +get_configdir()). +If a dictionary is provided, we assume that this is the preset

  • +
  • **kwargs – Any other parameter that shall be passed to the +update() method

  • +
+
+
+

Notes

+

An identifier is the filename without extension. If you want to list +the available presets, run psyplot -lp from the command-line

+
+ +
+
+classmethod load_project(fname, auto_update=None, make_plot=True, draw=False, alternative_axes=None, main=False, encoding=None, enable_post=False, new_fig=True, clear=None, **kwargs)[source]
+

Load a project from a file or dict

+

This classmethod allows to load a project that has been stored using +the save_project() method and reads all the data and creates the +figures.

+

Since the data is stored in external files when saving a project, +make sure that the data is accessible under the relative paths +as stored in the file fname or from the current working directory +if fname is a dictionary. Alternatively use the alternative_paths +parameter or the pwd parameter

+
+
Parameters:
+
    +
  • fname (str or dict) – The string might be the path to a file created with the +save_project() method, or it might be a dictionary from this +method

  • +
  • auto_update (bool) – Default: None. A boolean indicating whether this list shall +automatically update the contained arrays when calling the +update() method or not. See also the no_auto_update +attribute. If None, the value from the 'lists.auto_update' +key in the psyplot.rcParams dictionary is used.

  • +
  • make_plot (bool) – If True, the data is plotted at the end. Otherwise you have to +call the psyplot.plotter.Plotter.initialize_plot() method or +the psyplot.plotter.Plotter.reinit() method by yourself

  • +
  • draw (bool or None) – Boolean to control whether the figure of this array shall be drawn +at the end. If None, it defaults to the ‘auto_draw’` parameter +in the psyplot.rcParams dictionary

  • +
  • alternative_axes (dict, None or list) –

    alternative axes instances to use

    +
      +
    • If it is None, the axes and figures from the saving point will be +reproduced.

    • +
    • a dictionary should map from array names in the created +project to matplotlib axes instances

    • +
    • a list should contain axes instances that will be used for +iteration

    • +
    +

  • +
  • main (bool, optional) – If True, a new main project is created and returned. +Otherwise (by default default) the data is added to the current +main project.

  • +
  • encoding (str) – The encoding to use for loading the project. If None, it is +automatically determined by pickle. Note: Set this to 'latin1' +if using a project created with python2 on python3.

  • +
  • enable_post (bool) – If True, the post formatoption is +enabled and post processing scripts are allowed. Do only set this +parameter to True if you know you can trust the information in +fname

  • +
  • new_fig (bool) – If True (default) and alternative_axes is None, new figures are +created if the figure already exists

  • +
  • clear (bool) – If True, axes are cleared before making the plot. This is only +necessary if the ax keyword consists of subplots with projection +that differs from the one that is needed

  • +
  • pwd (str) – Path to the working directory from where the data can be imported. +If None and fname is the path to a file, pwd is set to the +directory of this file. Otherwise the current working directory is +used.

  • +
  • alternative_paths (dict or list or str) – A mapping from original filenames as used in d to filenames that +shall be used instead. If alternative_paths is not None, +datasets must be None. Paths must be accessible from the current +working directory. +If alternative_paths is a list (or any other iterable) is +provided, the file names will be replaced as they appear in d +(note that this is very unsafe if d is not and dict)

  • +
  • datasets (dict or list or None) – A mapping from original filenames in d to the instances of +xarray.Dataset to use. If it is an iterable, the same +holds as for the alternative_paths parameter

  • +
  • ignore_keys (list of str) – Keys specified in this list are ignored and not seen as array +information (note that attrs are used anyway)

  • +
  • only (string, list or callable) –

    Can be one of the following three things:

    +
      +
    • a string that represents a pattern to match the array names +that shall be included

    • +
    • a list of array names to include

    • +
    • a callable with two arguments, a string and a dict such as

      +
      def filter_func(arr_name: str, info: dict): -> bool
      +    '''
      +    Filter the array names
      +
      +    This function should return True if the array shall be
      +    included, else False
      +
      +    Parameters
      +    ----------
      +    arr_name: str
      +        The array name (i.e. the ``arr_name`` attribute)
      +    info: dict
      +        The dictionary with the array informations. Common
      +        keys are ``'name'`` that points to the variable name
      +        and ``'dims'`` that points to the dimensions and
      +        ``'fname'`` that points to the file name
      +    '''
      +    return True or False
      +
      +
      +

      The function should return True if the array shall be +included, else False. This function will also be given to +subsequents instances of InteractiveList objects that +are contained in the returned value

      +
    • +
    +

  • +
  • chname (dict) – A mapping from variable names in the project to variable names +that should be used instead

  • +
  • d (dict) – The dictionary holding the data

  • +
  • alternative_paths – A mapping from original filenames as used in d to filenames that +shall be used instead. If alternative_paths is not None, +datasets must be None. Paths must be accessible from the current +working directory. +If alternative_paths is a list (or any other iterable) is +provided, the file names will be replaced as they appear in d +(note that this is very unsafe if d is not and dict)

  • +
  • datasets – A mapping from original filenames in d to the instances of +xarray.Dataset to use. If it is an iterable, the same +holds as for the alternative_paths parameter

  • +
  • pwd – Path to the working directory from where the data can be imported. +If None, use the current working directory.

  • +
  • ignore_keys – Keys specified in this list are ignored and not seen as array +information (note that attrs are used anyway)

  • +
  • only

    Can be one of the following three things:

    +
      +
    • a string that represents a pattern to match the array names +that shall be included

    • +
    • a list of array names to include

    • +
    • a callable with two arguments, a string and a dict such as

      +
      def filter_func(arr_name: str, info: dict): -> bool
      +    '''
      +    Filter the array names
      +
      +    This function should return True if the array shall be
      +    included, else False
      +
      +    Parameters
      +    ----------
      +    arr_name: str
      +        The array name (i.e. the ``arr_name`` attribute)
      +    info: dict
      +        The dictionary with the array informations. Common
      +        keys are ``'name'`` that points to the variable name
      +        and ``'dims'`` that points to the dimensions and
      +        ``'fname'`` that points to the file name
      +    '''
      +    return True or False
      +
      +
      +

      The function should return True if the array shall be +included, else False. This function will also be given to +subsequents instances of InteractiveList objects that +are contained in the returned value

      +
    • +
    +

  • +
  • chname – A mapping from variable names in the project to variable names +that should be used instead

  • +
+
+
Returns:
+

The project in state of the saving point

+
+
Return type:
+

Project

+
+
+
+ +
+
+property logger
+

logging.Logger of this instance

+
+ +
+
+property main
+

Project. The main project of this subproject

+
+ +
+
+property mapcombined
+

List of data arrays that are plotted by psy_maps.plotters.CombinedPlotter plotters

+
+ +
+
+property mapplot
+

List of data arrays that are plotted by psy_maps.plotters.FieldPlotter plotters

+
+ +
+
+property maps
+

List of data arrays that are plotted by psy_maps.plotters.MapPlotter plotters

+
+ +
+
+property mapvector
+

List of data arrays that are plotted by psy_maps.plotters.VectorPlotter plotters

+
+ +
+
+classmethod new(num=None, *args, **kwargs)[source]
+

Create a new main project

+
+
Parameters:
+
    +
  • num (int) – The number of the project

  • +
  • iterable (iterable) – The iterable (e.g. another list) defining this list

  • +
  • attrs (dict-like or iterable, optional) – Global attributes of this list

  • +
  • auto_update (bool) – Default: None. A boolean indicating whether this list shall +automatically update the contained arrays when calling the +update() method or not. See also the no_auto_update +attribute. If None, the value from the 'lists.auto_update' +key in the psyplot.rcParams dictionary is used.

  • +
  • new_name (bool or str) – If False, and the arr_name attribute of the new array is +already in the list, a ValueError is raised. +If True and the arr_name attribute of the new array is not +already in the list, the name is not changed. Otherwise, if the +array name is already in use, new_name is set to ‘arr{0}’. +If not True, this will be used for renaming (if the array name of +arr is in use or not). '{0}' is replaced by a counter

  • +
  • main (Project) – The main project this subproject belongs to (or None if this +project is the main project)

  • +
+
+
Returns:
+

The with the given num (if it does not already exist, it is +created)

+
+
Return type:
+

Project

+
+
+
+

See also

+
+
scp

Sets the current project

+
+
gcp

Returns the current project

+
+
+
+
+ +
+
+oncpchange
+

signal to be emiitted when the current main and/or subproject changes

+
+ +
+
+property plot
+

Plotting instance of this Project. See the +ProjectPlotter class for method documentations

+
+ +
+
+property plot2d
+

List of data arrays that are plotted by psy_simple.plotters.Simple2DPlotter plotters

+
+ +
+
+property plotters
+

A list of all the plotters in this instance

+
+ +
+
+save_preset(fname=None, include_defaults=False, update=False)[source]
+

Save the formatoptions of this project as a preset

+

This method takes the formatoptions in the plotters of this project and +saves it as a preset file

+
+ +
+
+save_project(fname=None, pwd=None, pack=False, **kwargs)[source]
+

Save this project to a file

+
+
Parameters:
+
    +
  • fname (str or None) – If None, the dictionary will be returned. Otherwise the necessary +information to load this project via the load() method is +saved to fname using the pickle module

  • +
  • pwd (str or None, optional) – Path to the working directory from where the data can be imported. +If None and fname is the path to a file, pwd is set to the +directory of this file. Otherwise the current working directory is +used.

  • +
  • pack (bool) – If True, all datasets are packed into the folder of fname +and will be used if the data is loaded

  • +
  • dump (bool) – If True and the dataset has not been dumped so far, it is dumped to +a temporary file or the one generated by paths is used. If it is +False or both, dump and paths are None, no data will be stored. +If it is None and paths is not None, dump is set to True.

  • +
  • paths (iterable or True) – An iterator over filenames to use if a dataset has no filename. +If paths is True, an iterator over temporary files will be +created without raising a warning

  • +
  • attrs (bool, optional) – If True (default), the ArrayList.attrs and +xarray.DataArray.attrs attributes are included in the +returning dictionary

  • +
  • standardize_dims (bool, optional) – If True (default), the real dimension names in the dataset are +replaced by x, y, z and t to be more general.

  • +
  • use_rel_paths (bool, optional) – If True (default), paths relative to the current working directory +are used. Otherwise absolute paths to pwd are used

  • +
  • ds_description ('all' or set of {'fname', 'ds', 'num', 'arr', 'store'}) –

    Keys to describe the datasets of the arrays. If all, all keys +are used. The key descriptions are

    +
    +
    fname

    the file name is inserted in the 'fname' key

    +
    +
    store

    the data store class and module is inserted in the 'store' +key

    +
    +
    ds

    the dataset is inserted in the 'ds' key

    +
    +
    num

    The unique number assigned to the dataset is inserted in the +'num' key

    +
    +
    arr

    The array itself is inserted in the 'arr' key

    +
    +
    +

  • +
  • full_ds (bool) – If True and 'ds' is in ds_description, the entire dataset is +included. Otherwise, only the DataArray converted to a dataset is +included

  • +
+
+
+

Notes

+

You can also store the entire data in the pickled file by setting +ds_description={'ds'}

+
+ +
+
+classmethod scp(project)[source]
+

Set the current project

+
+
Parameters:
+

project (Project or None) – The project to set. If it is None, the current subproject is set +to empty. If it is a sub project (see:attr:Project.is_main), +the current subproject is set to this project. Otherwise it +replaces the current main project

+
+
+
+

See also

+
+
scp

The global version for setting the current project

+
+
gcp

Returns the current project

+
+
project

Creates a new project

+
+
+
+
+ +
+
+share(base=None, keys=None, by=None, **kwargs)[source]
+

Share the formatoptions of one plotter with all the others

+

This method shares specified formatoptions from base with all the +plotters in this instance.

+
+
Parameters:
+
    +
  • base (None, Plotter, xarray.DataArray, InteractiveList, or list of them) – The source of the plotter that shares its formatoptions with the +others. It can be None (then the first instance in this project +is used), a Plotter or any data object +with a psy attribute. If by is not None, then it is expected +that base is a list of data objects for each figure/axes

  • +
  • keys (string or iterable of strings) – The formatoptions to share, or group names of formatoptions to +share all formatoptions of that group (see the +fmt_groups property). If None, all formatoptions of this +plotter are unshared.

  • +
  • by ({'fig', 'figure', 'ax', 'axes'}) – Share the formatoptions only with the others on the same +'figure' or the same 'axes'. In this case, base must either +be None or a list of the types specified for base

  • +
  • draw (bool or None) – Boolean to control whether the figure of this array shall be drawn +at the end. If None, it defaults to the ‘auto_draw’` parameter +in the psyplot.rcParams dictionary

  • +
  • auto_update (bool) – Boolean determining whether or not the start_update() method +is called at the end. This parameter has no effect if the +no_auto_update attribute is set to True.

  • +
+
+
+
+

See also

+

psyplot.plotter.share

+
+
+ +
+
+static show()[source]
+

Shows all open figures

+
+ +
+
+property simple
+

List of data arrays that are plotted by psy_simple.plotters.SimplePlotterBase plotters

+
+ +
+
+summaries(*args, **kwargs)[source]
+

Show the available formatoptions and their summaries in this project

+
+
Parameters:
+
    +
  • keys (list of str or None) – If None, the all formatoptions of the given class are used. Group +names from the psyplot.plotter.groups mapping are replaced +by the formatoptions

  • +
  • indent (int) – The indentation of the table

  • +
  • grouped (bool, optional) – If True, the formatoptions are grouped corresponding to the +Formatoption.groupname attribute

  • +
  • func (function or None) – The function the is used for returning (by default it is printed +via the print() function or (when using the gui) in the +help explorer). The given function must take a string as argument

  • +
  • include_links (bool or None, optional) – Default False. If True, links (in restructured formats) are +included in the description. If None, the behaviour is determined +by the psyplot.plotter.Plotter.include_links attribute.

  • +
+
+
Returns:
+

The enhanced list of the formatoptions

+
+
Return type:
+

list of str

+
+
+
+ +
+
+unshare(**kwargs)[source]
+

Unshare the formatoptions of all the plotters in this instance

+

This method uses the psyplot.plotter.Plotter.unshare_me() +method to release the specified formatoptions in keys.

+
+
Parameters:
+
    +
  • keys (string or iterable of strings) – The formatoptions to unshare, or group names of formatoptions to +unshare all formatoptions of that group (see the +fmt_groups property). If None, all formatoptions of this +plotter are unshared.

  • +
  • draw (bool or None) – Boolean to control whether the figure of this array shall be drawn +at the end. If None, it defaults to the ‘auto_draw’` parameter +in the psyplot.rcParams dictionary

  • +
  • auto_update (bool) – Boolean determining whether or not the start_update() method +is called at the end. This parameter has no effect if the +no_auto_update attribute is set to True.

  • +
+
+
+ +
+ +
+
+property vector
+

List of data arrays that are plotted by psy_simple.plotters.SimpleVectorPlotter plotters

+
+ +
+
+property violinplot
+

List of data arrays that are plotted by psy_simple.plotters.ViolinPlotter plotters

+
+ +
+
+property with_plotter
+

The arrays in this instance that are visualized with a plotter

+
+ +
+ +
+
+class psyplot.project.ProjectPlotter(project=None)[source]
+

Bases: object

+

Plotting methods of the psyplot.project.Project class

+

Attributes:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

barplot(*args, **kwargs)

Make a bar plot of one-dimensional data

combined(*args, **kwargs)

Plot a 2D scalar field with an overlying vector field

density(*args, **kwargs)

Make a density plot of point data

fldmean(*args, **kwargs)

Calculate and plot the mean over x- and y-dimensions

lineplot(*args, **kwargs)

Make a line plot of one-dimensional data

mapcombined(*args, **kwargs)

Plot a 2D scalar field with an overlying vector field on a map

mapplot(*args, **kwargs)

Plot a 2D scalar field on a map

mapvector(*args, **kwargs)

Plot a 2D vector field on a map

plot2d(*args, **kwargs)

Make a simple plot of a 2D scalar field

project

vector(*args, **kwargs)

Make a simple plot of a 2D vector field

violinplot(*args, **kwargs)

Make a violin plot of your data

+

Methods:

+ + + + + + +

show_plot_methods()

Print the plotmethods of this instance

+
+
+barplot(*args, **kwargs)
+

Make a bar plot of one-dimensional data

+
+
+

This plotting method adds data arrays and plots them via +psy_simple.plotters.BarPlotter plotters

+

To plot data from a netCDF file type:

+
>>> psy.plot.barplot(filename, name=['my_variable'], ...)
+
+
+

Possible formatoptions are

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

alpha

axiscolor

background

categorical

color

coord

figtitle

figtitleprops

figtitlesize

figtitleweight

grid

labelprops

labelsize

labelweight

legend

legendlabels

mask

maskbetween

maskgeq

maskgreater

maskleq

maskless

plot

post

post_timing

sym_lims

text

ticksize

tickweight

tight

title

titleprops

titlesize

titleweight

transpose

widths

xlabel

xlim

xrotation

xticklabels

xtickprops

xticks

ylabel

ylim

yrotation

yticklabels

ytickprops

yticks

+
+
+

Examples

+

To explore the formatoptions and their documentations, use the +keys, summaries and docs methods. For example:

+
>>> import psyplot.project as psy
+
+# show the keys corresponding to a group or multiple
+# formatopions
+>>> psy.plot.barplot.keys('labels')
+
+# show the summaries of a group of formatoptions or of a
+# formatoption
+>>> psy.plot.barplot.summaries('title')
+
+# show the full documentation
+>>> psy.plot.barplot.docs('plot')
+
+# or access the documentation via the attribute
+>>> psy.plot.barplot.plot
+
+
+
+
+ +
+
+combined(*args, **kwargs)
+

Plot a 2D scalar field with an overlying vector field

+
+
+

This plotting method adds data arrays and plots them via +psy_simple.plotters.CombinedSimplePlotter plotters

+

To plot data from a netCDF file type:

+
>>> psy.plot.combined(filename, name=[['my_variable', ['u_var', 'v_var']]], ...)
+
+
+

Possible formatoptions are

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

arrowsize

arrowstyle

axiscolor

background

bounds

cbar

cbarspacing

clabel

clabelprops

clabelsize

clabelweight

cmap

color

cticklabels

ctickprops

cticks

cticksize

ctickweight

datagrid

density

extend

figtitle

figtitleprops

figtitlesize

figtitleweight

grid

interp_bounds

labelprops

labelsize

labelweight

levels

linewidth

mask

mask_datagrid

maskbetween

maskgeq

maskgreater

maskleq

maskless

miss_color

plot

post

post_timing

sym_lims

text

ticksize

tickweight

tight

title

titleprops

titlesize

titleweight

transpose

vbounds

vcbar

vcbarspacing

vclabel

vclabelprops

vclabelsize

vclabelweight

vcmap

vcticklabels

vctickprops

vcticks

vcticksize

vctickweight

vplot

xlabel

xlim

xrotation

xticklabels

xtickprops

xticks

ylabel

ylim

yrotation

yticklabels

ytickprops

yticks

+
+
+

Examples

+

To explore the formatoptions and their documentations, use the +keys, summaries and docs methods. For example:

+
>>> import psyplot.project as psy
+
+# show the keys corresponding to a group or multiple
+# formatopions
+>>> psy.plot.combined.keys('labels')
+
+# show the summaries of a group of formatoptions or of a
+# formatoption
+>>> psy.plot.combined.summaries('title')
+
+# show the full documentation
+>>> psy.plot.combined.docs('plot')
+
+# or access the documentation via the attribute
+>>> psy.plot.combined.plot
+
+
+
+
+ +
+
+density(*args, **kwargs)
+

Make a density plot of point data

+
+
+

This plotting method adds data arrays and plots them via +psy_simple.plotters.DensityPlotter plotters

+

To plot data from a netCDF file type:

+
>>> psy.plot.density(filename, name=['my_variable'], ...)
+
+
+

Possible formatoptions are

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

axiscolor

background

bins

bounds

cbar

cbarspacing

clabel

clabelprops

clabelsize

clabelweight

cmap

coord

cticklabels

ctickprops

cticks

cticksize

ctickweight

datagrid

density

extend

figtitle

figtitleprops

figtitlesize

figtitleweight

grid

interp_bounds

labelprops

labelsize

labelweight

levels

mask

mask_datagrid

maskbetween

maskgeq

maskgreater

maskleq

maskless

miss_color

normed

plot

post

post_timing

precision

sym_lims

text

ticksize

tickweight

tight

title

titleprops

titlesize

titleweight

transpose

xlabel

xlim

xrange

xrotation

xticklabels

xtickprops

xticks

ylabel

ylim

yrange

yrotation

yticklabels

ytickprops

yticks

+
+
+

Examples

+

To explore the formatoptions and their documentations, use the +keys, summaries and docs methods. For example:

+
>>> import psyplot.project as psy
+
+# show the keys corresponding to a group or multiple
+# formatopions
+>>> psy.plot.density.keys('labels')
+
+# show the summaries of a group of formatoptions or of a
+# formatoption
+>>> psy.plot.density.summaries('title')
+
+# show the full documentation
+>>> psy.plot.density.docs('plot')
+
+# or access the documentation via the attribute
+>>> psy.plot.density.plot
+
+
+
+
+ +
+
+fldmean(*args, **kwargs)
+

Calculate and plot the mean over x- and y-dimensions

+
+
+

This plotting method adds data arrays and plots them via +psy_simple.plotters.FldmeanPlotter plotters

+

To plot data from a netCDF file type:

+
>>> psy.plot.fldmean(filename, name=['my_variable'], ...)
+
+
+

Possible formatoptions are

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

axiscolor

background

color

coord

err_calc

error

erroralpha

figtitle

figtitleprops

figtitlesize

figtitleweight

grid

labelprops

labelsize

labelweight

legend

legendlabels

linewidth

marker

markersize

mask

maskbetween

maskgeq

maskgreater

maskleq

maskless

mean

plot

post

post_timing

sym_lims

text

ticksize

tickweight

tight

title

titleprops

titlesize

titleweight

transpose

xlabel

xlim

xrotation

xticklabels

xtickprops

xticks

ylabel

ylim

yrotation

yticklabels

ytickprops

yticks

+
+
+

Examples

+

To explore the formatoptions and their documentations, use the +keys, summaries and docs methods. For example:

+
>>> import psyplot.project as psy
+
+# show the keys corresponding to a group or multiple
+# formatopions
+>>> psy.plot.fldmean.keys('labels')
+
+# show the summaries of a group of formatoptions or of a
+# formatoption
+>>> psy.plot.fldmean.summaries('title')
+
+# show the full documentation
+>>> psy.plot.fldmean.docs('plot')
+
+# or access the documentation via the attribute
+>>> psy.plot.fldmean.plot
+
+
+
+
+ +
+
+lineplot(*args, **kwargs)
+

Make a line plot of one-dimensional data

+
+
+

This plotting method adds data arrays and plots them via +psy_simple.plotters.LinePlotter plotters

+

To plot data from a netCDF file type:

+
>>> psy.plot.lineplot(filename, name=['my_variable'], ...)
+
+
+

Possible formatoptions are

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

axiscolor

background

color

coord

error

erroralpha

figtitle

figtitleprops

figtitlesize

figtitleweight

grid

labelprops

labelsize

labelweight

legend

legendlabels

linewidth

marker

markersize

mask

maskbetween

maskgeq

maskgreater

maskleq

maskless

plot

post

post_timing

sym_lims

text

ticksize

tickweight

tight

title

titleprops

titlesize

titleweight

transpose

xlabel

xlim

xrotation

xticklabels

xtickprops

xticks

ylabel

ylim

yrotation

yticklabels

ytickprops

yticks

+
+
+

Examples

+

To explore the formatoptions and their documentations, use the +keys, summaries and docs methods. For example:

+
>>> import psyplot.project as psy
+
+# show the keys corresponding to a group or multiple
+# formatopions
+>>> psy.plot.lineplot.keys('labels')
+
+# show the summaries of a group of formatoptions or of a
+# formatoption
+>>> psy.plot.lineplot.summaries('title')
+
+# show the full documentation
+>>> psy.plot.lineplot.docs('plot')
+
+# or access the documentation via the attribute
+>>> psy.plot.lineplot.plot
+
+
+
+
+ +
+
+mapcombined(*args, **kwargs)
+

Plot a 2D scalar field with an overlying vector field on a map

+
+
+

This plotting method adds data arrays and plots them via +psy_maps.plotters.CombinedPlotter plotters

+

To plot data from a netCDF file type:

+
>>> psy.plot.mapcombined(filename, name=[['my_variable', ['u_var', 'v_var']]], ...)
+
+
+

Possible formatoptions are

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

arrowsize

arrowstyle

background

bounds

cbar

cbarspacing

clabel

clabelprops

clabelsize

clabelweight

clat

clip

clon

cmap

color

cticklabels

ctickprops

cticks

cticksize

ctickweight

datagrid

density

extend

figtitle

figtitleprops

figtitlesize

figtitleweight

google_map_detail

grid_color

grid_labels

grid_labelsize

grid_settings

interp_bounds

levels

linewidth

lonlatbox

lsm

map_extent

mask

mask_datagrid

maskbetween

maskgeq

maskgreater

maskleq

maskless

miss_color

plot

post

post_timing

projection

stock_img

text

tight

title

titleprops

titlesize

titleweight

transform

transpose

vbounds

vcbar

vcbarspacing

vclabel

vclabelprops

vclabelsize

vclabelweight

vcmap

vcticklabels

vctickprops

vcticks

vcticksize

vctickweight

vplot

xgrid

ygrid

+
+
+

Examples

+

To explore the formatoptions and their documentations, use the +keys, summaries and docs methods. For example:

+
>>> import psyplot.project as psy
+
+# show the keys corresponding to a group or multiple
+# formatopions
+>>> psy.plot.mapcombined.keys('labels')
+
+# show the summaries of a group of formatoptions or of a
+# formatoption
+>>> psy.plot.mapcombined.summaries('title')
+
+# show the full documentation
+>>> psy.plot.mapcombined.docs('plot')
+
+# or access the documentation via the attribute
+>>> psy.plot.mapcombined.plot
+
+
+
+
+ +
+
+mapplot(*args, **kwargs)
+

Plot a 2D scalar field on a map

+
+
+

This plotting method adds data arrays and plots them via +psy_maps.plotters.FieldPlotter plotters

+

To plot data from a netCDF file type:

+
>>> psy.plot.mapplot(filename, name=['my_variable'], ...)
+
+
+

Possible formatoptions are

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

background

bounds

cbar

cbarspacing

clabel

clabelprops

clabelsize

clabelweight

clat

clip

clon

cmap

cticklabels

ctickprops

cticks

cticksize

ctickweight

datagrid

extend

figtitle

figtitleprops

figtitlesize

figtitleweight

google_map_detail

grid_color

grid_labels

grid_labelsize

grid_settings

interp_bounds

levels

lonlatbox

lsm

map_extent

mask

mask_datagrid

maskbetween

maskgeq

maskgreater

maskleq

maskless

miss_color

plot

post

post_timing

projection

stock_img

text

tight

title

titleprops

titlesize

titleweight

transform

transpose

xgrid

ygrid

+
+
+

Examples

+

To explore the formatoptions and their documentations, use the +keys, summaries and docs methods. For example:

+
>>> import psyplot.project as psy
+
+# show the keys corresponding to a group or multiple
+# formatopions
+>>> psy.plot.mapplot.keys('labels')
+
+# show the summaries of a group of formatoptions or of a
+# formatoption
+>>> psy.plot.mapplot.summaries('title')
+
+# show the full documentation
+>>> psy.plot.mapplot.docs('plot')
+
+# or access the documentation via the attribute
+>>> psy.plot.mapplot.plot
+
+
+
+
+ +
+
+mapvector(*args, **kwargs)
+

Plot a 2D vector field on a map

+
+
+

This plotting method adds data arrays and plots them via +psy_maps.plotters.VectorPlotter plotters

+

To plot data from a netCDF file type:

+
>>> psy.plot.mapvector(filename, name=[['u_var', 'v_var']], ...)
+
+
+

Possible formatoptions are

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

arrowsize

arrowstyle

background

bounds

cbar

cbarspacing

clabel

clabelprops

clabelsize

clabelweight

clat

clip

clon

cmap

color

cticklabels

ctickprops

cticks

cticksize

ctickweight

datagrid

density

extend

figtitle

figtitleprops

figtitlesize

figtitleweight

google_map_detail

grid_color

grid_labels

grid_labelsize

grid_settings

linewidth

lonlatbox

lsm

map_extent

mask

mask_datagrid

maskbetween

maskgeq

maskgreater

maskleq

maskless

plot

post

post_timing

projection

stock_img

text

tight

title

titleprops

titlesize

titleweight

transform

transpose

xgrid

ygrid

+
+
+

Examples

+

To explore the formatoptions and their documentations, use the +keys, summaries and docs methods. For example:

+
>>> import psyplot.project as psy
+
+# show the keys corresponding to a group or multiple
+# formatopions
+>>> psy.plot.mapvector.keys('labels')
+
+# show the summaries of a group of formatoptions or of a
+# formatoption
+>>> psy.plot.mapvector.summaries('title')
+
+# show the full documentation
+>>> psy.plot.mapvector.docs('plot')
+
+# or access the documentation via the attribute
+>>> psy.plot.mapvector.plot
+
+
+
+
+ +
+
+plot2d(*args, **kwargs)
+

Make a simple plot of a 2D scalar field

+
+
+

This plotting method adds data arrays and plots them via +psy_simple.plotters.Simple2DPlotter plotters

+

To plot data from a netCDF file type:

+
>>> psy.plot.plot2d(filename, name=['my_variable'], ...)
+
+
+

Possible formatoptions are

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

axiscolor

background

bounds

cbar

cbarspacing

clabel

clabelprops

clabelsize

clabelweight

cmap

cticklabels

ctickprops

cticks

cticksize

ctickweight

datagrid

extend

figtitle

figtitleprops

figtitlesize

figtitleweight

grid

interp_bounds

labelprops

labelsize

labelweight

levels

mask

mask_datagrid

maskbetween

maskgeq

maskgreater

maskleq

maskless

miss_color

plot

post

post_timing

sym_lims

text

ticksize

tickweight

tight

title

titleprops

titlesize

titleweight

transpose

xlabel

xlim

xrotation

xticklabels

xtickprops

xticks

ylabel

ylim

yrotation

yticklabels

ytickprops

yticks

+
+
+

Examples

+

To explore the formatoptions and their documentations, use the +keys, summaries and docs methods. For example:

+
>>> import psyplot.project as psy
+
+# show the keys corresponding to a group or multiple
+# formatopions
+>>> psy.plot.plot2d.keys('labels')
+
+# show the summaries of a group of formatoptions or of a
+# formatoption
+>>> psy.plot.plot2d.summaries('title')
+
+# show the full documentation
+>>> psy.plot.plot2d.docs('plot')
+
+# or access the documentation via the attribute
+>>> psy.plot.plot2d.plot
+
+
+
+
+ +
+
+property project
+
+ +
+
+show_plot_methods()[source]
+

Print the plotmethods of this instance

+
+ +
+
+vector(*args, **kwargs)
+

Make a simple plot of a 2D vector field

+
+
+

This plotting method adds data arrays and plots them via +psy_simple.plotters.SimpleVectorPlotter plotters

+

To plot data from a netCDF file type:

+
>>> psy.plot.vector(filename, name=[['u_var', 'v_var']], ...)
+
+
+

Possible formatoptions are

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

arrowsize

arrowstyle

axiscolor

background

bounds

cbar

cbarspacing

clabel

clabelprops

clabelsize

clabelweight

cmap

color

cticklabels

ctickprops

cticks

cticksize

ctickweight

datagrid

density

extend

figtitle

figtitleprops

figtitlesize

figtitleweight

grid

labelprops

labelsize

labelweight

linewidth

mask

mask_datagrid

maskbetween

maskgeq

maskgreater

maskleq

maskless

plot

post

post_timing

sym_lims

text

ticksize

tickweight

tight

title

titleprops

titlesize

titleweight

transpose

xlabel

xlim

xrotation

xticklabels

xtickprops

xticks

ylabel

ylim

yrotation

yticklabels

ytickprops

yticks

+
+
+

Examples

+

To explore the formatoptions and their documentations, use the +keys, summaries and docs methods. For example:

+
>>> import psyplot.project as psy
+
+# show the keys corresponding to a group or multiple
+# formatopions
+>>> psy.plot.vector.keys('labels')
+
+# show the summaries of a group of formatoptions or of a
+# formatoption
+>>> psy.plot.vector.summaries('title')
+
+# show the full documentation
+>>> psy.plot.vector.docs('plot')
+
+# or access the documentation via the attribute
+>>> psy.plot.vector.plot
+
+
+
+
+ +
+
+violinplot(*args, **kwargs)
+

Make a violin plot of your data

+
+
+

This plotting method adds data arrays and plots them via +psy_simple.plotters.ViolinPlotter plotters

+

To plot data from a netCDF file type:

+
>>> psy.plot.violinplot(filename, name=['my_variable'], ...)
+
+
+

Possible formatoptions are

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

axiscolor

background

color

figtitle

figtitleprops

figtitlesize

figtitleweight

grid

labelprops

labelsize

labelweight

legend

legendlabels

mask

maskbetween

maskgeq

maskgreater

maskleq

maskless

plot

post

post_timing

sym_lims

text

ticksize

tickweight

tight

title

titleprops

titlesize

titleweight

transpose

xlabel

xlim

xrotation

xticklabels

xtickprops

xticks

ylabel

ylim

yrotation

yticklabels

ytickprops

yticks

+
+
+

Examples

+

To explore the formatoptions and their documentations, use the +keys, summaries and docs methods. For example:

+
>>> import psyplot.project as psy
+
+# show the keys corresponding to a group or multiple
+# formatopions
+>>> psy.plot.violinplot.keys('labels')
+
+# show the summaries of a group of formatoptions or of a
+# formatoption
+>>> psy.plot.violinplot.summaries('title')
+
+# show the full documentation
+>>> psy.plot.violinplot.docs('plot')
+
+# or access the documentation via the attribute
+>>> psy.plot.violinplot.plot
+
+
+
+
+ +
+ +
+
+psyplot.project.close(num=None, figs=True, data=True, ds=True, remove_only=False)[source]
+

Close the project

+

This method closes the current project (figures, data and datasets) or the +project specified by num

+
+
Parameters:
+
    +
  • num (int, None or 'all') – if int, it specifies the number of the project, if None, the +current subproject is closed, if 'all', all open projects are +closed

  • +
  • figs (bool) – Close the figures

  • +
  • data (bool) – delete the arrays from the (main) project

  • +
  • ds (bool) – If True, close the dataset as well

  • +
  • remove_only (bool) – If True and figs is True, the figures are not closed but the +plotters are removed

  • +
+
+
+
+

See also

+

Project.close

+
+
+ +
+
+psyplot.project.gcp(main=False)[source]
+

Get the current project

+
+
Parameters:
+

main (bool) – If True, the current main project is returned, otherwise the current +subproject is returned.

+
+
+
+

See also

+
+
scp

Sets the current project

+
+
project

Creates a new project

+
+
+
+
+ +
+
+psyplot.project.get_project_nums()[source]
+

Returns the project numbers of the open projects

+
+ +
+
+psyplot.project.multiple_subplots(rows=1, cols=1, maxplots=None, n=1, delete=True, for_maps=False, *args, **kwargs)[source]
+

Function to create subplots.

+

This function creates so many subplots on so many figures until the +specified number n is reached.

+
+
Parameters:
+
    +
  • rows (int) – The number of subplots per rows

  • +
  • cols (int) – The number of subplots per column

  • +
  • maxplots (int) – The number of subplots per figure (if None, it will be row*cols)

  • +
  • n (int) – number of subplots to create

  • +
  • delete (bool) – If True, the additional subplots per figure are deleted

  • +
  • for_maps (bool) – If True this is a simple shortcut for setting +subplot_kw=dict(projection=cartopy.crs.PlateCarree()) and is +useful if you want to use the mapplot, +mapvector or +mapcombined plotting methods

  • +
  • **kwargs (*args and) – anything that is passed to the matplotlib.pyplot.subplots() +function

  • +
+
+
Returns:
+

list of maplotlib.axes.SubplotBase instances

+
+
Return type:
+

list

+
+
+
+ +
+
+psyplot.project.plot = <psyplot.project.ProjectPlotter object>
+

ProjectPlotter of the current project. See the class documentation +for available plotting methods

+
+ +
+
+psyplot.project.project(num=None, *args, **kwargs)[source]
+

Create a new main project

+
+
Parameters:
+
    +
  • num (int) – The number of the project

  • +
  • iterable (iterable) – The iterable (e.g. another list) defining this list

  • +
  • attrs (dict-like or iterable, optional) – Global attributes of this list

  • +
  • auto_update (bool) – Default: None. A boolean indicating whether this list shall +automatically update the contained arrays when calling the +update() method or not. See also the no_auto_update +attribute. If None, the value from the 'lists.auto_update' +key in the psyplot.rcParams dictionary is used.

  • +
  • new_name (bool or str) – If False, and the arr_name attribute of the new array is +already in the list, a ValueError is raised. +If True and the arr_name attribute of the new array is not +already in the list, the name is not changed. Otherwise, if the +array name is already in use, new_name is set to ‘arr{0}’. +If not True, this will be used for renaming (if the array name of +arr is in use or not). '{0}' is replaced by a counter

  • +
  • main (Project) – The main project this subproject belongs to (or None if this +project is the main project)

  • +
+
+
Returns:
+

The with the given num (if it does not already exist, it is created)

+
+
Return type:
+

Project

+
+
+
+

See also

+
+
scp

Sets the current project

+
+
gcp

Returns the current project

+
+
+
+
+ +
+
+psyplot.project.register_plotter(identifier, module, plotter_name, plotter_cls=None, sorter=True, plot_func=True, import_plotter=None, **kwargs)[source]
+

Register a psyplot.plotter.Plotter for the projects

+

This function registers plotters for the Project class to allow +a dynamical handling of different plotter classes.

+
+
Parameters:
+
    +
  • identifier (str) – Name of the attribute that is used to filter for the instances +belonging to this plotter

  • +
  • module (str) – The module from where to import the plotter_name

  • +
  • plotter_name (str) – The name of the plotter class in module

  • +
  • sorter (bool, optional) – If True, the Project class gets a new property with the name +of the specified identifier which allows you to access the instances +that are plotted by the specified plotter_name

  • +
  • plot_func (bool, optional) – If True, the ProjectPlotter (the class that holds the +plotting method for the Project class and can be accessed via +the Project.plot attribute) gets an additional method to plot +via the specified plotter_name (see Other Parameters below.)

  • +
  • import_plotter (bool, optional) – If True, the plotter is automatically imported, otherwise it is only +imported when it is needed. If import_plotter is None, then it is +determined by the psyplot.rcParams 'project.auto_import' +item.

  • +
  • prefer_list (bool) – Determines the prefer_list parameter in the from_dataset +method. If True, the plotter is expected to work with instances of +psyplot.InteractiveList instead of +psyplot.InteractiveArray.

  • +
  • default_slice (indexer) – Index (e.g. 0 if method is ‘isel’) that shall be used for +dimensions not covered by dims and furtherdims. If None, the +whole slice will be used. Note that the default_slice is always +based on the isel method.

  • +
  • default_dims (dict) – Default dimensions that shall be used for plotting (e.g. +{‘x’: slice(None), ‘y’: slice(None)} for longitude-latitude plots)

  • +
  • show_examples (bool, optional) – If True, examples how to access the plotter documentation are +included in class documentation

  • +
  • example_call (str, optional) – The arguments and keyword arguments that shall be included in the +example of the generated plot method. This call will then appear as +>>> psy.plot.%(identifier)s(%(example_call)s) in the +documentation

  • +
  • plugin (str) – The name of the plugin

  • +
+
+
+
+ +
+
+psyplot.project.scp(project)[source]
+

Set the current project

+
+
Parameters:
+

%(Project.scp.parameters)s

+
+
+
+

See also

+
+
gcp

Returns the current project

+
+
project

Creates a new project

+
+
+
+
+ +
+
+psyplot.project.unregister_plotter(identifier, sorter=True, plot_func=True)[source]
+

Unregister a psyplot.plotter.Plotter for the projects

+
+
Parameters:
+
    +
  • identifier (str) – Name of the attribute that is used to filter for the instances +belonging to this plotter or to create plots with this plotter

  • +
  • sorter (bool) – If True, the identifier will be unregistered from the Project +class

  • +
  • plot_func (bool) – If True, the identifier will be unregistered from the +ProjectPlotter class

  • +
+
+
+
+ + + +
+
+ + + +
+
+
+
+ + + + \ No newline at end of file diff --git a/api/psyplot.sphinxext.extended_napoleon.html b/api/psyplot.sphinxext.extended_napoleon.html new file mode 100644 index 0000000..b5665cb --- /dev/null +++ b/api/psyplot.sphinxext.extended_napoleon.html @@ -0,0 +1,558 @@ + + + + + + + <no title> — psyplot documentation + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+ +
+
+ +

Sphinx extension module to provide additional sections for numpy docstrings

+

This extension extends the sphinx.ext.napoleon package with an +additional Possible types section in order to document possible types for +descriptors.

+

Notes

+

If you use this module as a sphinx extension, you should not list the +sphinx.ext.napoleon module in the extensions variable of your conf.py. +This module has been tested for sphinx 1.3.1.

+

Classes:

+ + + + + + + + + + + + +

DocstringExtension()

Class that introduces a "Possible Types" section

ExtendedGoogleDocstring(docstring[, config, ...])

sphinx.ext.napoleon.GoogleDocstring with more sections

ExtendedNumpyDocstring(docstring[, config, ...])

sphinx.ext.napoleon.NumpyDocstring with more sections

+

Functions:

+ + + + + + + + + +

process_docstring(app, what, name, obj, ...)

Process the docstring for a given python object.

setup(app)

Sphinx extension setup function

+
+
+class psyplot.sphinxext.extended_napoleon.DocstringExtension[source]
+

Bases: object

+

Class that introduces a “Possible Types” section

+

This class serves as a base class for +sphinx.ext.napoleon.NumpyDocstring and +sphinx.ext.napoleon.GoogleDocstring to introduce +another section names Possible types

+
+

Examples

+

The usage is the same as for the NumpyDocstring class, but it supports +the Possible types section:

+
>>> from sphinx.ext.napoleon import Config
+
+>>> from psyplot.sphinxext.extended_napoleon import (
+...     ExtendedNumpyDocstring,
+... )
+>>> config = Config(napoleon_use_param=True, napoleon_use_rtype=True)
+>>> docstring = '''
+... Possible types
+... --------------
+... type1
+...     Description of `type1`
+... type2
+...     Description of `type2`'''
+>>> print(ExtendedNumpyDocstring(docstring, config))
+.. rubric:: Possible types
+
+* *type1* --
+  Description of `type1`
+* *type2* --
+  Description of `type2`
+
+
+
+
+ +
+
+class psyplot.sphinxext.extended_napoleon.ExtendedGoogleDocstring(docstring: str | list[str], config: SphinxConfig | None = None, app: Sphinx | None = None, what: str = '', name: str = '', obj: Any = None, options: Any = None)[source]
+

Bases: GoogleDocstring, DocstringExtension

+

sphinx.ext.napoleon.GoogleDocstring with more sections

+
+ +
+
+class psyplot.sphinxext.extended_napoleon.ExtendedNumpyDocstring(docstring: str | list[str], config: SphinxConfig | None = None, app: Sphinx | None = None, what: str = '', name: str = '', obj: Any = None, options: Any = None)[source]
+

Bases: NumpyDocstring, DocstringExtension

+

sphinx.ext.napoleon.NumpyDocstring with more sections

+
+ +
+
+psyplot.sphinxext.extended_napoleon.process_docstring(app, what, name, obj, options, lines)[source]
+

Process the docstring for a given python object.

+

Called when autodoc has read and processed a docstring. lines is a list +of docstring lines that _process_docstring modifies in place to change +what Sphinx outputs.

+

The following settings in conf.py control what styles of docstrings will +be parsed:

+
    +
  • napoleon_google_docstring – parse Google style docstrings

  • +
  • napoleon_numpy_docstring – parse NumPy style docstrings

  • +
+
+
Parameters:
+
    +
  • app (sphinx.application.Sphinx) – Application object representing the Sphinx process.

  • +
  • what (str) – A string specifying the type of the object to which the docstring +belongs. Valid values: “module”, “class”, “exception”, “function”, +“method”, “attribute”.

  • +
  • name (str) – The fully qualified name of the object.

  • +
  • obj (module, class, exception, function, method, or attribute) – The object to which the docstring belongs.

  • +
  • options (sphinx.ext.autodoc.Options) – The options given to the directive: an object with attributes +inherited_members, undoc_members, show_inheritance and noindex that +are True if the flag option of same name was given to the auto +directive.

  • +
  • lines (list of str) –

    The lines of the docstring, see above.

    +
    +

    Note

    +

    lines is modified in place

    +
    +

  • +
+
+
+

Notes

+

This function is (to most parts) taken from the sphinx.ext.napoleon +module, sphinx version 1.3.1, and adapted to the classes defined here

+
+ +
+
+psyplot.sphinxext.extended_napoleon.setup(app)[source]
+

Sphinx extension setup function

+

When the extension is loaded, Sphinx imports this module and executes +the setup() function, which in turn notifies Sphinx of everything +the extension offers.

+
+
Parameters:
+

app (sphinx.application.Sphinx) – Application object representing the Sphinx process

+
+
+

Notes

+

This function uses the setup function of the sphinx.ext.napoleon +module

+
+ + + +
+
+ + + +
+
+
+
+ + + + \ No newline at end of file diff --git a/api/psyplot.sphinxext.html b/api/psyplot.sphinxext.html new file mode 100644 index 0000000..1db4075 --- /dev/null +++ b/api/psyplot.sphinxext.html @@ -0,0 +1,431 @@ + + + + + + + psyplot.sphinxext package — psyplot documentation + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +
+

psyplot.sphinxext package

+

Sphinx extension package of the psyplot module

+
+

Submodules

+ +
+
+ + +
+
+ + + +
+
+
+
+ + + + \ No newline at end of file diff --git a/api/psyplot.utils.html b/api/psyplot.utils.html new file mode 100644 index 0000000..6850207 --- /dev/null +++ b/api/psyplot.utils.html @@ -0,0 +1,627 @@ + + + + + + + <no title> — psyplot documentation + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Miscallaneous utility functions for the psyplot package.

+

Classes:

+ + + + + + +

Defaultdict([default_factory])

An ordered collections.defaultdict

+

Functions:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

check_key(key, possible_keys[, raise_error, ...])

Checks whether the key is in a list of possible keys

get_default_value(func, arg)

hashable(val)

Test if val is hashable and if not, get it's string representation

is_iterable(iterable)

Test if an object is iterable

is_remote_url(path)

isstring(s)

join_dicts(dicts[, delimiter, keep_all])

Join multiple dictionaries into one

plugin_entrypoints([group, name])

This utility function gets the entry points of the psyplot plugins

sort_kwargs(kwargs, *param_lists)

Function to sort keyword arguments and sort them into dictionaries

unique_everseen(iterable[, key])

List unique elements, preserving order.

+
+
+class psyplot.utils.Defaultdict(default_factory=None, *a, **kw)[source]
+

Bases: dict

+

An ordered collections.defaultdict

+

Taken from http://stackoverflow.com/a/6190500/562769

+

Methods:

+ + + + + + +

copy()

Return a shallow copy of the dictionary

+
+
+copy()[source]
+

Return a shallow copy of the dictionary

+
+ +
+ +
+
+psyplot.utils.check_key(key, possible_keys, raise_error=True, name='formatoption keyword', msg='See show_fmtkeys function for possible formatopion keywords', *args, **kwargs)[source]
+

Checks whether the key is in a list of possible keys

+

This function checks whether the given key is in possible_keys and if +not looks for similar sounding keys

+
+
Parameters:
+
    +
  • key (str) – Key to check

  • +
  • possible_keys (list of strings) – a list of possible keys to use

  • +
  • raise_error (bool) – If not True, a list of similar keys is returned

  • +
  • name (str) – The name of the key that shall be used in the error message

  • +
  • msg (str) – The additional message that shall be used if no close match to +key is found

  • +
  • *args – They are passed to the difflib.get_close_matches() function +(i.e. n to increase the number of returned similar keys and +cutoff to change the sensibility)

  • +
  • **kwargs – They are passed to the difflib.get_close_matches() function +(i.e. n to increase the number of returned similar keys and +cutoff to change the sensibility)

  • +
+
+
Returns:
+

    +
  • str – The key if it is a valid string, else an empty string

  • +
  • list – A list of similar formatoption strings (if found)

  • +
  • str – An error message which includes

  • +
+

+
+
Raises:
+

KeyError – If the key is not a valid formatoption and raise_error is True

+
+
+
+ +
+
+psyplot.utils.get_default_value(func, arg)[source]
+
+ +
+
+psyplot.utils.hashable(val)[source]
+

Test if val is hashable and if not, get it’s string representation

+
+
Parameters:
+

val (object) – Any (possibly not hashable) python object

+
+
Returns:
+

The given val if it is hashable or it’s string representation

+
+
Return type:
+

val or string

+
+
+
+ +
+
+psyplot.utils.is_iterable(iterable)[source]
+

Test if an object is iterable

+
+
Parameters:
+

iterable (object) – The object to test

+
+
Returns:
+

True, if the object is an iterable object

+
+
Return type:
+

bool

+
+
+
+ +
+
+psyplot.utils.is_remote_url(path)[source]
+
+ +
+
+psyplot.utils.isstring(s)[source]
+
+ +
+
+psyplot.utils.join_dicts(dicts, delimiter=None, keep_all=False)[source]
+

Join multiple dictionaries into one

+
+
Parameters:
+
    +
  • dicts (list of dict) – A list of dictionaries

  • +
  • delimiter (str) – The string that shall be used as the delimiter in case that there +are multiple values for one attribute in the arrays. If None, they +will be returned as sets

  • +
  • keep_all (bool) – If True, all formatoptions are kept. Otherwise only the intersection

  • +
+
+
Returns:
+

The combined dictionary

+
+
Return type:
+

dict

+
+
+
+ +
+
+psyplot.utils.plugin_entrypoints(group='psyplot', name='name')[source]
+

This utility function gets the entry points of the psyplot plugins

+
+ +
+
+psyplot.utils.sort_kwargs(kwargs, *param_lists)[source]
+

Function to sort keyword arguments and sort them into dictionaries

+

This function returns dictionaries that contain the keyword arguments +from kwargs corresponding given iterables in *params

+
+
Parameters:
+
    +
  • kwargs (dict) – Original dictionary

  • +
  • *param_lists – iterables of strings, each standing for a possible key in kwargs

  • +
+
+
Returns:
+

len(params) + 1 dictionaries. Each dictionary contains the items of +kwargs corresponding to the specified list in *param_lists. The +last dictionary contains the remaining items

+
+
Return type:
+

list

+
+
+
+ +
+
+psyplot.utils.unique_everseen(iterable, key=None)[source]
+

List unique elements, preserving order. Remember all elements ever seen.

+

Function taken from https://docs.python.org/2/library/itertools.html

+
+ + + +
+
+ + + +
+
+
+
+ + + + \ No newline at end of file diff --git a/api/psyplot.warning.html b/api/psyplot.warning.html new file mode 100644 index 0000000..c879bb7 --- /dev/null +++ b/api/psyplot.warning.html @@ -0,0 +1,507 @@ + + + + + + + <no title> — psyplot documentation + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Warning module of the psyplot python module.

+

This module controls the warning behaviour of the module via the python +builtin warnings module and introduces three new warning classes:

+

..autosummay:

+
PsPylotRuntimeWarning
+PsyPlotWarning
+PsyPlotCritical
+
+
+

Exceptions:

+ + + + + + + + + + + + +

PsyPlotCritical

Critical UserWarning for psyplot module

PsyPlotRuntimeWarning

Runtime warning that appears only ones

PsyPlotWarning

Normal UserWarning for psyplot module

+

Functions:

+ + + + + + + + + + + + + + + +

critical(message[, category, logger])

wrapper around the warnings.warn function for critical warnings.

customwarn(message, category, filename, ...)

Use the psyplot.warning logger for categories being out of PsyPlotWarning and PsyPlotCritical and the default warnings.showwarning function for all the others.

disable_warnings([critical])

Function that disables all warnings and all critical warnings (if critical evaluates to True) related to the psyplot Module.

warn(message[, category, logger])

wrapper around the warnings.warn function for non-critical warnings.

+
+
+exception psyplot.warning.PsyPlotCritical[source]
+

Bases: UserWarning

+

Critical UserWarning for psyplot module

+
+ +
+
+exception psyplot.warning.PsyPlotRuntimeWarning[source]
+

Bases: RuntimeWarning

+

Runtime warning that appears only ones

+
+ +
+
+exception psyplot.warning.PsyPlotWarning[source]
+

Bases: UserWarning

+

Normal UserWarning for psyplot module

+
+ +
+
+psyplot.warning.critical(message, category=<class 'psyplot.warning.PsyPlotCritical'>, logger=None)[source]
+

wrapper around the warnings.warn function for critical warnings. +logger may be a logging.Logger instance

+
+ +
+
+psyplot.warning.customwarn(message, category, filename, lineno, *args, **kwargs)[source]
+

Use the psyplot.warning logger for categories being out of +PsyPlotWarning and PsyPlotCritical and the default warnings.showwarning +function for all the others.

+
+ +
+
+psyplot.warning.disable_warnings(critical=False)[source]
+

Function that disables all warnings and all critical warnings (if +critical evaluates to True) related to the psyplot Module. +Please note that you can also configure the warnings via the +psyplot.warning logger (logging.getLogger(psyplot.warning)).

+
+ +
+
+psyplot.warning.warn(message, category=<class 'psyplot.warning.PsyPlotWarning'>, logger=None)[source]
+

wrapper around the warnings.warn function for non-critical warnings. +logger may be a logging.Logger instance

+
+ + + +
+
+ + + +
+
+
+
+ + + + \ No newline at end of file diff --git a/changelog.html b/changelog.html new file mode 100644 index 0000000..f003c4a --- /dev/null +++ b/changelog.html @@ -0,0 +1,740 @@ + + + + + + + Changelog — psyplot documentation + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +
+

Changelog

+
+

v1.5.1

+

Compatibility fixes for scipy and matplotlib

+
+

Added

+
    +
  • Add compatibility fixes for scipy 1.14, see !74 <https://codebase.helmholtz.cloud/psyplot/psyplot/-/merge_requests/74>

  • +
  • Add compatibility fixes for matplotlib 3.9, see !76 <https://codebase.helmholtz.cloud/psyplot/psyplot/-/merge_requests/76>

  • +
+
+
+
+

v1.5.0

+

Compatibility fixes, cli improvements and code formatting

+
+

Changed

+
    +
  • migrate to python-package-template, see merge request !60

  • +
  • implement custom warning handling, see merge request !61

  • +
  • fallback to nearest index when not found, see merge request !62

  • +
  • improve guessing of coordinates based on their names, see merge request !63

  • +
  • Fix enhanced attrs, see merge request !66

  • +
  • minor fix for matplotlib 3.8 compatibility, see merge request !68

  • +
  • implement ci-matrix for different python and mpl versions, see merge request !69

  • +
  • Update plugin guide, see merge request !71

  • +
+
+
+

Added

+
    +
  • Add metadata info, see merge request !64, !65

  • +
  • improve cli yaml options, see merge request !67

  • +
+
+
+
+

v1.4.3

+

Minor fix for grid files (#53)

+
+
+

v1.4.2

+

Fix for compatibility with python 3.7

+
+

Changed

+
    +
  • plugin entrypoint compatibility fix for python 3.7 (#47)

  • +
  • ignore SNF links in linkcheck (#49)

  • +
  • Replace gitter with mattermost (#45)

  • +
+
+
+
+

v1.4.1

+

Compatibility fixes and minor improvements

+
+

Added

+
    +
  • An abstract convert_coordinate method has been implemented for the +Plotter and Formatoption class that can be used in subclasses to +convert coordinates for the required visualization. The default +implementation does nothing (see +#39)

  • +
+
+
+

Fixed

+
    +
  • the update method now only takes the coordinates that are dimensions in the +dataset see #39

  • +
  • psyplot is now compatible with matplotlib 3.5 and python 3.10

  • +
+
+
+

Changed

+
    +
  • loading more than one variables into a DataArray now first selects the +corresponding dimensions, then puts it into a single DataArray. This +avoids loading the entire data (see +#39)

  • +
+
+
+
+

v1.4.0

+

Compatibility fixes and LGPL license

+
+

Fixed

+
    +
  • psyplot is now compatible with 0.18

  • +
+
+
+

Added

+ +
+
+

Changed

+
    +
  • psyplot is now officially licensed under LGPL-3.0-only, +see #33

  • +
  • the lower bound for supported xarray versions is now 0.17.

  • +
  • project files do not store the Store anymore as this information cannot be +gathered from xarray 0.18. We now rely on xarray to automatically find the +engine to open the files.

  • +
  • Documentation is now hosted with Github Pages at https://psyplot.github.io/psyplot. +Redirects from the old documentation at https://psyplot.readthedocs.io have +been configured.

  • +
  • Examples have been removed from the psyplot repository as they now live in a +central place at https://github.com/psyplot/examples

  • +
  • We use CicleCI now for a standardized CI/CD pipeline to build and test +the code and docs all at one place, see #32

  • +
+
+
+
+

v1.3.2

+
+

Fixed

+
    +
  • The get_xname-like methods of the decoder have been fixed if they get a +variable without any dimensions. See #30

  • +
+
+
+
+

v1.3.1

+
+

Fixed

+
    +
  • 3D bounds of coordinate are not interpreted as unstructured anymore (see +660c703

  • +
+
+
+
+

v1.3.0

+

New repository, presets and compatibility fixes

+
+

Added

+
    +
  • You can now save and load presets for the formatoptions of a project which +applies the formatoptions that you stored in a file to a specific plot method, +see #24

  • +
  • the rcParams do now have a catch method that allows a temporary change +of formatoptions.

    +

    Usage:

    +
    rcParams['some_key'] = 0
    +with rcParams.catch():
    +    rcParams['some_key'] = 1
    +    assert rcParams['some_key'] == 1
    +assert rcParams['some_key'] == 0
    +
    +
    +
  • +
  • ArrayList.from_dataset (and consecutively all plotmethods) now support +different input types for the decoder. You can pass an instance of the +CFDecoder class, a sub class of CFDecoder, or keyword arguments +that are used to initialize the decoder, +see #20. Furthermore, the +check_data method of the various plotmethods now also accept a decoder +parameter, see #22

  • +
  • psyplot.data.open_dataset now decodes grid_mappings attributes, +see #17

  • +
  • psyplot projects now support the with syntax, e.g. something like:

    +
    with psy.plot.mapplot('file.nc') as sp:
    +    sp.export('output.png')
    +
    +
    +

    sp will be closed automatically (see #18)

    +
  • +
  • the update to variables with other dimensions works now as well +(see #22)

  • +
  • a psyplot.project.Project now has a new format_string method to +format a string with the meta attributes of the data in the projects

  • +
  • The ArrayList class now supports filtering by formatoption keys. You can +filter for plotters that have a cmap formatoption via:

    +
    sp1 = psy.plot.mapplot(ds)
    +sp2 = psy.plot.lineplot(ds)
    +full_sp = sp1 + sp2
    +full_sp(fmts='cmap')  # gives equivalent results as addressing sp1 directly
    +
    +
    +
  • +
+
+
+

Changed

+
    +
  • psyplot has been moved from https://github.com/Chilipp/psyplot to https://github.com/psyplot/psyplot, +see #16

  • +
  • Specifying names in x, y, t and z attributes of the CFDecoder class +now means that any other attribute (such as the coordinates or axis attribute) +are ignored

  • +
  • If a given variable cannot be found in the provided coords to CFDecoder.get_variable_by_axis, +we fall back to the CFDecoder.ds.coords attribute, see #19

  • +
  • A bug has been fixed for initializing a CFDecoder with x, y, z and +t parameters (see #20)

  • +
+
+
+
+

v1.2.1

+

This patch fixes compatibility issues with xarray 0.12 and cdo 1.5. Additionally we now officially drop support for python 2.7.

+
+
+

v1.2.0

+
+

Added

+
    +
  • The psyplot.plotter.Plotter.initialize_plot method now takes a +priority keyword to only initialize only formatoptions of a certain +priority

  • +
+
+
+

Removed

+
    +
  • The installers from the psyplot-conda +repositories have been depreceated. Instead, now download the latest +miniconda and install psyplot and the +plugins via conda install -c conda-forge psy-maps psyplot-gui psy-reg

  • +
+
+
+

Changed

+
    +
  • We generalized the handling of unstructured data as lined out in +issue#6. The new method +psyplot.data.CFDecoder.get_cell_node_coord returns the coordinates of the +nodes for a given grid cell. These informations are used by the +psy-simple and psy-maps plugins for displaying any unstructured data. See +also the example on the +visualization of unstructured grids

  • +
  • We removed the inplace parameter for the CFDecoder methods since it is +deprecated with xarray 0.12 (see +issue #8). The +CFDecoder.decode_ds method now always decodes inplace

  • +
+
+
+
+

v1.1.0

+

This new release mainly adds new xarray accossors (psy) for DataArrays +and Datasets. Additionally we provide methods to calculate the spatially +weighted mean, such as fldmean, fldstd and fldpctl.

+
+

Added

+
    +
  • The yaxis_inverted and xaxis_inverted is now considered when loading and +saving a matplotlib axes

  • +
  • Added the seaborn-style command line argument

  • +
  • Added the concat_dim command line argument

  • +
  • Added the plot attribute to the DataArray and Dataset accessors. It is now +possible to plot directly from the dataset and the data array

  • +
  • Added requires_replot attribute for the Formatoption class. If this +attribute is True and the formatoption is contained in an update, it is the +same as calling Plotter.update(replot=True)).

  • +
  • We added support for multifile datasets when saving a project. +Multifile datasets are datasets that have been opened with, e.g. +psyplot.data.open_mfdataset or +psyplot.project.plot.<plotmethod>(..., mfmode=True). This however does +not always work with datasets opened with xarray.open_mfdataset. In these +cases, you have to set the Dataset.psy._concat_dim attribute manually

  • +
  • Added the chname parameter when loading a project. This parameter can +be used to display another variable from the dataset than the one stored +in the psyplot project file

  • +
  • Added the gridweights, fldmean, fldstd and fldpctl methods +to the psy DataArray accessor to calculate weighted means, standard +deviations and percentiles over the spatial dimensions (x- and y).

  • +
  • Added the additional_children and additional_dependencies parameters +to the Formatoption intialization. These parameters can be used to provide +additional children for a formatoption for one plotter class

  • +
  • We added the psyplot.plotter.Formatoption.get_fmt_widget method which can +be implemented to insert widgets in the formatoptions widget of the +graphical user interface

  • +
+
+
+
+

v1.0.0

+https://zenodo.org/badge/87944102.svg +
+

Added

+
    +
  • Changelog

  • +
+
+
+

Changed

+
    +
  • When creating new plots using the psyplot.project.Project.plot attribute, +scp for the newly created subproject is only called when the +corresponding Project is the current main project (gcp(True))

  • +
  • The alternate_paths keyword in the psyplot.project.Project.save_project +and psyplot.data.ArrayList.array_info methods has been changed to +alternative_paths

  • +
  • The psyplot.project.Cdo class does not accept any of the keywords +returnDA, returnMaps or returnLine anymore. Instead it takes +the plot_method keyword and several others.

  • +
  • The psyplot.project.close method by default now removes the data from +the current project and closes attached datasets

  • +
  • The modules in the psyplot.plotter modules have been moved to separate +packages to make the debugging and testing easier

    +
      +
    • The psyplot.plotter.simple, baseplotter and colors modules have been moved +to the psy-simple package

    • +
    • The psyplot.plotter.maps and boxes modules have been moved to the psy-maps +package

    • +
    • The psyplot.plotter.linreg module has been moved to the psy-reg package

    • +
    +
  • +
  • The endings of the yaml configuration files are now all .yml. Hence,

    +
      +
    • the configuration file name is now psyplotrc.yml instead of +psyplotrc.yaml

    • +
    • the default logging configuration file name is now logging.yml instead +of logging.yaml

    • +
    +
  • +
  • Under osx, the configuration directory is now also expected to be in +$HOME/.config/psyplot (as it is for linux)

  • +
+
+
+
+ + +
+
+ + + +
+
+
+
+ + + + \ No newline at end of file diff --git a/ci/matrix/default/Pipfile b/ci/matrix/default/Pipfile deleted file mode 100644 index edaee53..0000000 --- a/ci/matrix/default/Pipfile +++ /dev/null @@ -1,20 +0,0 @@ -# SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH -# -# SPDX-License-Identifier: CC0-1.0 - -[[source]] -url = "https://pypi.org/simple" -verify_ssl = true -name = "pypi" - -[packages] -psyplot = {extras = ["testsite"], file = "../../..", editable=true} -matplotlib = "3.7.*" - -[dev-packages] - -[pipenv] -allow_prereleases = true - -[requires] -python_version = "3.9" diff --git a/command_line.html b/command_line.html new file mode 100644 index 0000000..bb7b193 --- /dev/null +++ b/command_line.html @@ -0,0 +1,611 @@ + + + + + + + Command line usage — psyplot documentation + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +
+

Command line usage

+

The psyplot.__main__ module defines a simple parser to parse commands +from the command line to make a plot of data in a netCDF file. Note that the +arguments change slightly if you have the psyplot-gui module installed +(see psyplot-gui documentation).

+

It can be run from the command line via:

+
python -m psyplot [options] [arguments]
+
+
+

or simply:

+
psyplot [options] [arguments]
+
+
+

+

Load a dataset, make the plot and save the result to a file

+
+

+
usage: psyplot [-h] [-V] [-aV] [-i] [-lp] [-lpm] [-lds] [-lps]
+               [-n [variable_name ...]] [-d dim,val1[,val2[,...]]
+               [dim,val1[,val2[,...]] ...]]
+               [-pm {'combined', 'fldmean', 'plot2d', 'vector', 'lineplot', 'mapvector', 'violinplot', 'barplot', 'mapcombined', 'mapplot', 'density'}]
+               [-o str or list of str] [-p str] [-engine str]
+               [-fmt FILENAME_OR_YAML] [-t] [-rc RC_FILE] [-e str]
+               [--enable-post] [-sns str] [-op str] [-cd str]
+               [-chname [project-variable,variable-to-use ...]] [-preset str]
+               [--decoder FILENAME_OR_YAML]
+               [str ...]
+
+
+
+

Positional Arguments

+
+
str
+

Either the filenames to show, or, if the project parameter is set, +the a list of ,-separated filenames to make a mapping from the +original filename to a new one

+

Default: []

+
+
+
+
+

Named Arguments

+
+
-n, --name
+

The variable names to plot if the output parameter is set

+

Default: []

+
+
-d, --dims
+

A mapping from coordinate names to integers if the project is not +given

+
+
-pm, --plot-method
+

Possible choices: combined, fldmean, plot2d, vector, lineplot, mapvector, violinplot, barplot, mapcombined, mapplot, density

+

The name of the plot_method to use

+
+
-p, --project
+

If set, the project located at the given file name is loaded

+
+
-engine
+

The engine to use for opening the dataset (see +psyplot.data.open_dataset())

+
+
-fmt, --formatoptions
+
+

YAML-formatted formatoption (e.g. ‘cmap: Reds’), or the path to a yaml +('.yml' or '.yaml'), JSON ('.json') or pickle file +defining a dictionary of formatoption that is applied to the +data visualized by the chosen plot_method

+
+
+
-rc, --rc-file
+

The path to a yaml configuration file that can be used to update the +rcParams

+
+
-e, --encoding
+

The encoding to use for loading the project. If None, it is +automatically determined by pickle. Note: Set this to 'latin1' +if using a project created with python2 on python3.

+
+
--enable-post
+

Enable the post processing +formatoption. If True/set, post processing scripts are enabled in the +given project. Only set this if you are sure that you can trust the +given project file because it may be a security vulnerability.

+

Default: False

+
+
-sns, --seaborn-style
+

The name of the style of the seaborn package that can be used for +the seaborn.set_style() function

+
+
-cd, --concat-dim
+

The concatenation dimension if multiple files in fnames are +provided

+
+
-chname
+
+

A mapping from variable names in the project to variable names in the +datasets that should be used instead. Variable names should be +separated by a comma.

+
+

Default: {}

+
+
-preset
+

The filename or identifier of a preset. If the given preset is +the path to an existing yaml file, it will be loaded. Otherwise we +look up the preset in the psyplot configuration directory (see +get_configdir()).

+
+
--decoder
+
+

YAML-formatted decoder options (e.g. ‘x: x-coordinate’), or the path to +a yaml ('.yml' or '.yaml'), JSON ('.json') or pickle file +defining a dictionary of formatoption that is applied to the +data visualized by the chosen plot_method

+
+
+
+
+
+

Info options

+

Options that print informations and quit afterwards

+
+
-V, --version
+

show program’s version number and exit

+
+
-aV, --all-versions
+

Print the versions of all plugins and requirements and exit

+
+
-i, --info
+

Show grid information on the specified variables.

+
+
-lp, --list-plugins
+

Print the names of the plugins and exit

+
+
-lpm, --list-plot-methods
+

List the available plot methods and what they do

+
+
-lds, --list-datasets
+

List the used dataset names in the given project.

+
+
-lps, --list-presets
+

Print available presets and exit

+
+
+
+
+

Output options

+

Options that only have an effect if the -o option is set.

+
+
-o, --output
+

If set, the data is loaded and the figures are saved to the specified +filename and now graphical user interface is shown

+
+
-t, --tight
+

If True/set, it is tried to figure out the tight bbox of the figure and +adjust the paper size of the output to it

+

Default: False

+
+
-op, --output-project
+

The name of a project file to save the project to

+
+
+
+

Examples

+

Here are some examples on how to use psyplot from the command line.

+

Plot the variable 't2m' in a netCDF file 'myfile.nc' and save +the plot to 'plot.pdf':

+
$ psyplot myfile.nc -n t2m -pm mapplot -o test.pdf
+
+
+

Create two plots for 't2m' with the first and second timestep on +the second vertical level:

+
$ psyplot myfile.nc -n t2m  -pm mapplot -o test.pdf -d t,0,1 z,1
+
+
+

If you have save a project using the +psyplot.project.Project.save_project() method into a file named +'project.pkl', you can replot this via:

+
$ psyplot -p project.pkl -o test.pdf
+
+
+

If you use a different dataset than the one you used in the project +(e.g. 'other_ds.nc'), you can replace it via:

+
$ psyplot other_dataset.nc -p project.pkl -o test.pdf
+
+
+

or explicitly via:

+
$ psyplot old_ds.nc,other_ds.nc -p project.pkl -o test.pdf
+
+
+

You can also load formatoptions from a configuration file, e.g.:

+
$ echo 'title: my title' > fmt.yaml
+$ psyplot myfile.nc -n t2m  -pm mapplot -fmt fmt.yaml -o test.pdf
+
+
+

+
+ + +
+
+ + + +
+
+
+
+ + + + \ No newline at end of file diff --git a/configuration.html b/configuration.html new file mode 100644 index 0000000..07a8c84 --- /dev/null +++ b/configuration.html @@ -0,0 +1,548 @@ + + + + + + + Configuration — psyplot documentation + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +
+

Configuration

+
+

The rcParams

+
+

Hint

+

If you are using the psyplot-gui module, +you can also use the preferences widget to modify the configuration. See +Configuration of the GUI.

+
+

Psyplot, and especially it’s plugins have a lot of configuration values. +Our rcParams handling is motivated by +matplotlib although we extended the +possibilities of it’s matplotlib.RcParams class. Our rcParams +are stored in the psyplot.rcParams +object. Without any plugins, this looks like

+
In [1]: from psyplot import rcParams
+
+In [2]: print(rcParams.dump(exclude_keys=[]))
+# Configuration parameters of the psyplot module
+# 
+# You can copy this file (or parts of it) to another path and save it as
+# psyplotrc.yml. The directory should then be stored in the PSYPLOTCONFIGDIR
+# environment variable.
+# 
+# Created with python
+# 
+# 3.9.19 (main, Jul 10 2024, 19:03:52) 
+# [GCC 12.2.0]
+# 
+# 
+# Automatically draw the figures if the draw keyword in the update and start_update methods is None
+auto_draw: true
+# Automatically show the figures after the update andstart_update methods
+auto_show: false
+# path for supplementary data
+datapath: null
+# interpolation method to calculate 2D-bounds (see the `kind` parameterin the :meth:`psyplot.data.CFDecoder.get_plotbounds` method)
+decoder.interp_kind: linear
+# names that shall be interpreted as the time dimension
+decoder.t: !!set
+  time: null
+# names that shall be interpreted as the longitudinal x dim
+decoder.x: !!set {}
+# names that shall be interpreted as the latitudinal y dim
+decoder.y: !!set {}
+# names that shall be interpreted as the vertical z dim
+decoder.z: !!set {}
+# Boolean flag to control whether CDOs (Climate Data Operators) should be used to calculate grid weights. If None, they are tried to be used.
+gridweights.use_cdo: null
+# default value (boolean) for the auto_update parameter in the initialization of Plotter, Project, etc. instances
+lists.auto_update: true
+# formatoption keys and values that are defined by the user to be used by
+# the specified plotters. For example to modify the title of all
+# :class:`psyplot.plotter.maps.FieldPlotter` instances, set
+# ``{'plotter.fieldplotter.title': 'my title'}``
+plotter.user: {}
+# A list of filenames with trusted presets
+presets.trusted: []
+# boolean controlling whether all plotters specified in the project.plotters item will be automatically imported when importing the psyplot.project module
+project.auto_import: false
+# boolean controlling whether the seaborn module shall be imported when importing the project module. If None, it is only tried to import the module.
+project.import_seaborn: null
+# mapping from identifier to plotter definitions for the Project class. See the :func:`psyplot.project.register_plotter` function for possible keywords and values. See :attr:`psyplot.project.registered_plotters` for examples.
+project.plotters: {}
+# Plot methods that are defined by the user and overwrite those in the``'project.plotters'`` key. Use this if you want to define your own plotters without writing a plugin
+project.plotters.user: {}
+
+
+

You can use this object like a dictionary and modify the default values. For +example, if you do not want, that the seaborn package is imported when the +psyplot.project module is imported, you can simply do this via:

+
In [3]: rcParams["project.import_seaborn"] = False
+
+
+

Additionally, you can make these changes permanent. At every first import of +the psyplot module, the rcParams are updated from a yaml configuration +file. On Linux and OS X, this is stored under +$HOME/.config/psyplot/psyplotrc.yml, under Windows it is stored at +$HOME/.psyplot/psyplotrc.yml. But use the +psyplot.config.rcsetup.psyplot_fname() function, to get the correct +location.

+

To make our changes from above permanent, we could just do:

+
In [4]: import yaml
+   ...: from psyplot.config.rcsetup import psyplot_fname
+   ...: 
+
+In [5]: with open(psyplot_fname(if_exists=False), "w") as f:
+   ...:     yaml.dump({"project.import_seaborn": False}, f)
+   ...: 
+
+# or we use the dump method
+In [6]: rcParams.dump(
+   ...:     psyplot_fname(if_exists=False),
+   ...:     overwrite=True,  # update the existing file
+   ...:     include_keys=["project.import_seaborn"],
+   ...: )
+   ...: 
+
+
+
+
+

Default formatoptions

+

The psyplot plugins, (psy_simple.plugin, psy_maps.plugin, etc.) +define their own rcParams instance. When the plugins +are loaded at the first import of psyplot, these instances update +psyplot.rcParams.

+

The update mainly defines the default values for the plotters defined by that +plugin. However, it is not always obvious, which key in the +psyplot.rcParams belongs to which +formatoption. For this purpose, however, you can use the +default_key attribute. For example, +the title formatoption has the +default_key

+
In [7]: import psyplot.project as psy
+
+In [8]: plotter = psy.plot.lineplot.plotter_cls()
+   ...: plotter.title.default_key
+   ...: 
+Out[8]: 'plotter.baseplotter.title'
+
+
+

As our plotters are based on inheritance, the default values use it, too. +Therefore, the FieldPlotter, the underlying plotter +for the mapplot plot method, uses the +same configuration value in the +psyplot.rcParams:

+
In [9]: plotter = psy.plot.mapplot.plotter_cls()
+   ...: plotter.title.default_key
+   ...: 
+Out[9]: 'plotter.baseplotter.title'
+
+
+
+
+ + +
+
+ + + +
+
+
+
+ + + + \ No newline at end of file diff --git a/contributing.html b/contributing.html new file mode 100644 index 0000000..cc2d193 --- /dev/null +++ b/contributing.html @@ -0,0 +1,625 @@ + + + + + + + Contribution and development hints — psyplot documentation + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +
+

Contribution and development hints

+
+

Warning

+

This page has been automatically generated as has not yet been reviewed by the +authors of psyplot!

+
+

The psyplot project is developed by the +Helmholtz-Zentrum Hereon. It is open-source +as we believe that this analysis can be helpful for reproducibility and +collaboration, and we are looking forward for your feedback, +questions and especially for your contributions.

+ + +
+

Code of Conduct

+

This project and everyone participating in it is governed by the +psyplot Code of Conduct. +By participating, you are expected to uphold this code.

+
+
+

What should I know before I get started?

+
+

The psyplot framework

+

psyplot is just the framework that allows interactive data analysis +and visualization. Much of the functionality however is implemented by +other packages. What package is the correct one for your bug +report/feature request, can be determined by the following list

+
    +
  • psyplot-gui: +Everything specific to the graphical user interface

  • +
  • psy-view: +Everything specific to the psy-view graphical user interface

  • +
  • psy-simple: +Everything concerning, e.g. the lineplot, plot2d, density +or vector plot methods

  • +
  • psy-maps: Everything +concerning, e.g. the mapplot, mapvector mapcombined plot +methods

  • +
  • psy-reg: Everything +concerning, e.g. the linreg or densityreg plot methods

  • +
  • psyplot: Everything +concerning the general framework, e.g. data handling, parallel +update, etc.

  • +
+

Concerning plot methods, you can simply find out which module +implemented it via

+
import psyplot.project as psy
+
+print(psy.plot.name - of - your - plot - method._plugin)
+
+
+

If you still don’t know, where to open the issue, just go for +psyplot.

+
+
+
+

Contributing in the development

+
+

Note

+

We use automated formatters to ensure a high quality and maintanability of +our source code. Getting familiar with these techniques can take quite some +time and you might get error messages that are hard to understand.

+

We not slow down your development and we do our best to support you with +these techniques. If you have any troubles, just commit with +git commit --no-verify (see below) and the maintainers will take care +of the tests and continuous integration.

+
+

Thanks for your wish to contribute to this project!! The source code of +the psyplot package is hosted at +https://codebase.helmholtz.cloud/psyplot/psyplot.

+

This is an open gitlab where you can register via the Helmholtz AAI. If your +home institution is not listed in the Helmholtz AAI, please use one of the +social login providers, such as Google, GitHub or OrcID.

+

Once you created an account in this gitlab, you can fork this +repository to your own user account and implement the changes.

+

Afterwards, please make a merge request into the main repository. If you +have any questions, please do not hesitate to create an issue on gitlab +and contact the maintainers of this package.

+

Once you created you fork, you can clone it via

+
git clone https://codebase.helmholtz.cloud/<your-user>/psyplot.git
+
+
+

we recommend that you change into the directory and create a virtual +environment via:

+
cd psyplot
+python -m venv venv
+source venv/bin/activate # (or venv/Scripts/Activate.bat on windows)
+
+
+

and install it in development mode with the [dev] option via:

+
pip install -e ./psyplot/[dev]
+
+
+
+
+

Helpers

+
+

Shortcuts with make

+

There are several shortcuts available with the Makefile in the root of +the repository. On Linux, you can execute make help to get an overview.

+
+
+

Annotating licenses

+

If you want to create new files, you need to set license and copyright +statements correctly. We use reuse to check that the licenses are +correctly encoded. As a helper script, you can use the script at +.reuse/add_license.py that provides several shortcuts from +.reuse/shortcuts.yaml. Please select the correct shortcut, namely

+
    +
  • If you create a new python file, you should run:

    +
    python .reuse/add_license.py code <file-you-created>.py
    +
    +
    +
  • +
  • If you created a new file for the docs, you should run:

    +
    python .reuse/add_license.py docs <file-you-created>.py
    +
    +
    +
  • +
  • If you created any other non-code file, you should run:

    +
    python .reuse/add_license.py supp <file-you-created>.py
    +
    +
    +
  • +
+

If you have any questions on how licenses are handled, please do not hesitate +to contact the maintainers of psyplot.

+
+
+
+

Fixing the docs

+

The documentation for this package is written in restructured Text and built +with sphinx and deployed on readthedocs.

+

If you found something in the docs that you want to fix, head over to the +docs folder, install the necessary requirements via +pip install -r requirements.txt ../[docs] and build the docs with +make html (or make.bat on windows).

+

The docs are then available in docs/_build/html/index.html that you can +open with your local browser.

+

Implement your fixes in the corresponding .rst-file and push them to your +fork on gitlab.

+
+
+

Contributing to the code

+

We use automated formatters (see their config in pyproject.toml), namely

+
    +
  • Black for standardized +code formatting

  • +
  • blackdoc for +standardized code formatting in documentation

  • +
  • Flake8 for general code +quality

  • +
  • isort for standardized order in +imports.

  • +
  • mypy for static type checking on +type hints

  • +
  • reuse for handling of licenses

  • +
  • cffconvert +for validating the CITATION.cff file.

  • +
+

We highly recommend that you setup +pre-commit hooks to automatically run all the +above tools every time you make a git commit. This can be done by running:

+
pre-commit install
+
+
+

from the root of the repository. You can skip the pre-commit checks with +git commit --no-verify but note that the CI will fail if it +encounters any formatting errors.

+

You can also run the pre-commit step manually by invoking:

+
pre-commit run --all-files
+
+
+
+
+

Updating the skeleton for this package

+

This package has been generated from the template +`https://codebase.helmholtz.cloud/hcdc/software-templates/python-package-template.git`__.

+

See the template repository for instructions on how to update the skeleton for +this package.

+
+
+ + +
+
+ + + +
+
+
+
+ + + + \ No newline at end of file diff --git a/develop/framework.html b/develop/framework.html new file mode 100644 index 0000000..2b8d3d0 --- /dev/null +++ b/develop/framework.html @@ -0,0 +1,744 @@ + + + + + + + The psyplot framework — psyplot documentation + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +
+

The psyplot framework

+../_images/psyplot_framework.gif +

The main module we used so far, was the psyplot.project module. It is +the end of a whole framework that is setup by the psyplot package.

+

This framework is designed in analogy to matplotlibs +figure - axes - artist setup, +where one figure controls multiple axes, an axes is the manager of multiple +artists (e.g. a simple line) and each artist is responsible for visualizing one +or more objects on the plot. The psyplot framework instead is defined through +the Project - +(InteractiveBase - Plotter) - +Formatoption relationship.

+

The last to parts in this framework, the Plotter and +Formatoption, are only defined through abstract base +classes in this package. They are filled with contents in plugins such as the +psy-simple or the psy-maps plugin (see Psyplot plugins).

+
+

The project() function

+

The psyplot.project.Project class (in analogy to matplotlibs +Figure class) is basically a list that controls +multiple plot objects. It comprises the full functionality of the package and +packs it into one class, the Project class.

+

In analogy to pyplots figure() function, a new project +can simply be created via

+
In [1]: import psyplot.project as psy
+
+In [2]: p = psy.project()
+
+
+

This automatically sets p to be the current project which can be accessed +through the gcp() method. You can also set the current +project by using the scp() function.

+
+

Note

+

We highly recommend to use the project() function to create new +projects instead of creating projects from the Project. This +ensures the right numbering of the projects of old projects.

+
+

The project uses the plotters from the psyplot.plotter module to +visualize your data. Hence you can add new plots and new data to the project by +using the Project.plot attribute or the psyplot.project.plot +attribute which targets the current project. The return types of the plotting +methods are again instances of the Project class, however we consider +them as subprojects in contrast main projects that are created through the +project() function. There is basically no difference but the result of the +Project.is_main attribute which is False for subprojects. Hence, +each new plot creates a subproject but also stores the data array in the +corresponding main project of the Project instance from which the plot +method has been called. The newly created subproject can be accessed via

+
In [3]: sp = psy.gcp()
+
+
+

whereas the current main project can be accessed via

+
In [4]: p = psy.gcp(main=True)
+
+
+

Plots created by a specific method of the Project.plot attribute may +however be accessed via the corresponding attribute of the Project +class. The following example creates three subprojects, two with the +mapplot and mapvector methods +from the psy-maps plugin and one with the simple +lineplot method from the psy-simple plugin to visualize +simple lines.

+
In [5]: import matplotlib.pyplot as plt
+
+In [6]: import cartopy.crs as ccrs
+
+# the subplots for the maps (need cartopy projections)
+In [7]: ax = list(psy.multiple_subplots(2, 2, n=3, for_maps=True))
+
+# the subplot for the line plot
+In [8]: ax.append(plt.gcf().add_subplot(2, 2, 4))
+
+# scalar field of the zonal wind velocity in the file demo.nc
+In [9]: psy.plot.mapplot('demo.nc', name='u', ax=ax[0], clabel='{desc}')
+Out[9]: psyplot.project.Project([    arr0: 2-dim DataArray of u, with (lat, lon)=(96, 192), lev=1e+05, time=1979-01-31T18:00:00])
+
+# a second scalar field of temperature
+In [10]: psy.plot.mapplot('demo.nc', name='t2m', ax=ax[1], clabel='{desc}')
+Out[10]: psyplot.project.Project([    arr1: 2-dim DataArray of t2m, with (lat, lon)=(96, 192), lev=1e+05, time=1979-01-31T18:00:00])
+
+# a vector plot projected on the earth
+In [11]: psy.plot.mapvector('demo.nc', name=[['u', 'v']], ax=ax[2],
+   ....:                    attrs={'long_name': 'Wind speed'})
+   ....: 
+Out[11]: psyplot.project.Project([    arr2: 3-dim DataArray of u, v, with (variable, lat, lon)=(2, 96, 192), lev=1e+05, time=1979-01-31T18:00:00])
+
+In [12]: psy.plot.lineplot('demo.nc', name='t2m', x=0, y=0, z=range(4),
+   ....:                   ax=ax[3], xticklabels='%b %d', ylabel='{desc}',
+   ....:                   legendlabels='%(zname)s = %(z)s %(zunits)s')
+   ....: 
+Out[12]: 
+psyplot.project.Project([arr3: psyplot.data.InteractiveList([
+    arr0: 1-dim DataArray of t2m, with (time)=(5,), lon=0.0, lat=88.57, lev=1e+05,
+    arr1: 1-dim DataArray of t2m, with (time)=(5,), lon=0.0, lat=88.57, lev=8.5e+04,
+    arr2: 1-dim DataArray of t2m, with (time)=(5,), lon=0.0, lat=88.57, lev=5e+04,
+    arr3: 1-dim DataArray of t2m, with (time)=(5,), lon=0.0, lat=88.57, lev=2e+04])])
+
+
+../_images/docs_framework_project_demo1.png +

The latter is now the current subproject we could access via +psy.gcp(). However we can access all of them through the main +project

+
In [13]: mp = psy.gcp(True)
+
+In [14]: mp  # all arrays
+Out[14]: 
+2 Main psyplot.project.Project([
+    arr0: 2-dim DataArray of u, with (lat, lon)=(96, 192), lev=1e+05, time=1979-01-31T18:00:00,
+    arr1: 2-dim DataArray of t2m, with (lat, lon)=(96, 192), lev=1e+05, time=1979-01-31T18:00:00,
+    arr2: 3-dim DataArray of u, v, with (variable, lat, lon)=(2, 96, 192), lev=1e+05, time=1979-01-31T18:00:00,
+    arr3: psyplot.data.InteractiveList([
+        arr0: 1-dim DataArray of t2m, with (time)=(5,), lon=0.0, lat=88.57, lev=1e+05,
+        arr1: 1-dim DataArray of t2m, with (time)=(5,), lon=0.0, lat=88.57, lev=8.5e+04,
+        arr2: 1-dim DataArray of t2m, with (time)=(5,), lon=0.0, lat=88.57, lev=5e+04,
+        arr3: 1-dim DataArray of t2m, with (time)=(5,), lon=0.0, lat=88.57, lev=2e+04])])
+
+In [15]: mp.mapplot  # all scalar fields
+Out[15]: 
+psyplot.project.Project([
+    arr0: 2-dim DataArray of u, with (lat, lon)=(96, 192), lev=1e+05, time=1979-01-31T18:00:00,
+    arr1: 2-dim DataArray of t2m, with (lat, lon)=(96, 192), lev=1e+05, time=1979-01-31T18:00:00])
+
+In [16]: mp.mapvector  # all vector plots
+Out[16]: psyplot.project.Project([    arr2: 3-dim DataArray of u, v, with (variable, lat, lon)=(2, 96, 192), lev=1e+05, time=1979-01-31T18:00:00])
+
+In [17]: mp.maps  # all data arrays that are plotted on a map
+Out[17]: 
+psyplot.project.Project([
+    arr0: 2-dim DataArray of u, with (lat, lon)=(96, 192), lev=1e+05, time=1979-01-31T18:00:00,
+    arr1: 2-dim DataArray of t2m, with (lat, lon)=(96, 192), lev=1e+05, time=1979-01-31T18:00:00,
+    arr2: 3-dim DataArray of u, v, with (variable, lat, lon)=(2, 96, 192), lev=1e+05, time=1979-01-31T18:00:00])
+
+In [18]: mp.lineplot # the simple plot we created
+Out[18]: 
+psyplot.project.Project([arr3: psyplot.data.InteractiveList([
+    arr0: 1-dim DataArray of t2m, with (time)=(5,), lon=0.0, lat=88.57, lev=1e+05,
+    arr1: 1-dim DataArray of t2m, with (time)=(5,), lon=0.0, lat=88.57, lev=8.5e+04,
+    arr2: 1-dim DataArray of t2m, with (time)=(5,), lon=0.0, lat=88.57, lev=5e+04,
+    arr3: 1-dim DataArray of t2m, with (time)=(5,), lon=0.0, lat=88.57, lev=2e+04])])
+
+
+

The advantage is, since every plotter has different formatoptions, we can +now update them very easily. For example lets update the arrowsize to +1 (which only works for the mapvector plots), the projection +to an orthogonal (which only works for maps), the simple +plots to use the 'viridis' colormap for color coding the lines and for all +we choose their title corresponding to the variable names

+
In [19]: p.maps.update(projection='ortho')
+
+In [20]: p.mapvector.update(color='r', plot='stream', lonlatbox='Europe')
+
+In [21]: p.lineplot.update(color='viridis')
+
+In [22]: p.update(title='%(long_name)s')
+
+
+../_images/docs_framework_project_demo2.png +
+
+

The InteractiveBase and the Plotter classes

+
+

Interactive data objects

+

The next level are instances of the +InteractiveBase class. This abstract base +class provides an interface between the data and the visualization. Hence a +plotter (that’s how we call instances of the Plotter class) will deal +with the subclasses of the InteractiveBase:

+ + + + + + + + + +

InteractiveArray(xarray_obj, *args, **kwargs)

Interactive psyplot accessor for the data array

InteractiveList(*args, **kwargs)

List of InteractiveArray instances that can be plotted itself

+

Those classes (in particular the InteractiveArray) keep +the reference to the base dataset to allow the update of the dataslice you are +plotting. The InteractiveList class can be used in a +plotter for the visualization of multiple +InteractiveArray instances (see for example the +psyplot.plotter.simple.LinePlotter and +psyplot.plotter.maps.CombinedPlotter classes). +Furthermore those data instances have a +plotter attribute that is usually +occupied by an instance of a Plotter subclass.

+
+

Note

+

The InteractiveArray serves as a +DataArray accessor. After you imported psyplot, you can +access it via the psy attribute of a DataArray, i.e. +via

+
In [23]: import xarray as xr
+
+In [24]: xr.DataArray([]).psy
+Out[24]: <psyplot.data.InteractiveArray at 0x7f39a07a62b0>
+
+
+
+
+
+

Visualization objects

+

Each plotter class is the coordinator of several visualization options. +Thereby the Plotter class itself contains only +the structural functionality for managing the formatoptions that do the +real work. The plotters for the real usage are defined in plugins like the +psy-simple or the psy-maps package.

+

Hence each InteractiveBase instance is visualized by +exactly one Plotter class. If you don’t want to use the +project framework, the initialization of such an +instance nevertheless straight forward. Just open a dataset, extract the right +data array and plot it

+
In [25]: from psyplot import open_dataset
+
+In [26]: from psy_maps.plotters import FieldPlotter
+
+In [27]: ds = open_dataset('demo.nc')
+
+In [28]: arr = ds.t2m[0, 0]
+
+In [29]: plotter = FieldPlotter(arr)
+
+
+../_images/docs_framework_plotter_demo.png +

Now we created a plotter with all it’s formatoptions:

+
In [30]: type(plotter), plotter
+Out[30]: 
+(psy_maps.plotters.FieldPlotter,
+ {'levels': None,
+  'interp_bounds': None,
+  'plot': 'mesh',
+  'miss_color': None,
+  'background': 'rc',
+  'transpose': False,
+  'projection': 'cf',
+  'transform': 'cf',
+  'clon': None,
+  'clat': None,
+  'lonlatbox': None,
+  'lsm': {'res': '110m', 'linewidth': 1.0, 'coast': 'k'},
+  'stock_img': False,
+  'grid_color': 'k',
+  'grid_labels': None,
+  'grid_labelsize': 12.0,
+  'grid_settings': {},
+  'xgrid': True,
+  'ygrid': True,
+  'map_extent': None,
+  'google_map_detail': None,
+  'datagrid': None,
+  'clip': None,
+  'cmap': 'white_blue_red',
+  'bounds': [<BoundsMethod.rounded: 'rounded'>, None, 0.0, 100.0, None, None],
+  'extend': 'neither',
+  'cbar': {'b'},
+  'clabel': '',
+  'clabelsize': 'medium',
+  'clabelweight': None,
+  'cbarspacing': 'uniform',
+  'clabelprops': {},
+  'cticks': None,
+  'cticklabels': None,
+  'cticksize': 'medium',
+  'ctickweight': None,
+  'ctickprops': {},
+  'mask_datagrid': True,
+  'tight': False,
+  'maskless': None,
+  'maskleq': None,
+  'maskgreater': None,
+  'maskgeq': None,
+  'maskbetween': None,
+  'mask': None,
+  'title': '',
+  'titlesize': 'large',
+  'titleweight': None,
+  'titleprops': {},
+  'figtitle': '',
+  'figtitlesize': 12.0,
+  'figtitleweight': None,
+  'figtitleprops': {},
+  'text': [],
+  'post_timing': 'never',
+  'post': None})
+
+
+

You can use the show_keys(), show_summaries() and +show_docs() methods to have a look into the documentation into +the formatoptions or you simply use the builtin help() function for it:

+
>>> help(plotter.clabel)
+
+
+

The update methods are the same as for the Project +class. You can use the psyplot.data.InteractiveArray.update() via +arr.psy.update() which updates the data and forwards the formatoptions to +the Plotter.update() method.

+
+

Note

+

Plotters are subclasses of dictionaries where each item represents the +key-value pair of one formatoption. Anyway, although you could now simply +set a formatoption like you set an item for a dictionary via

+
In [31]: plotter['clabel'] = 'my label'
+
+
+

or equivalently

+
In [32]: plotter.clabel = 'my label'
+
+
+

this would not change the plot! Instead you have to use the +psyplot.plotter.Plotter.update() method, i.e.

+
In [33]: plotter.update(clabel='my label')
+
+
+
+
+
+
+

Formatoptions

+

Formatoptions are the core of the visualization in the psyplot framework. They +conceptually correspond to the basic matplotlib.artist.Artist and +inherit from the abstract Formatoption class. Each +plotter is set up through it’s formatoptions where each formatoption has a +unique formatoption key inside the plotter. This formatoption key (e.g. ‘title’ +or ‘clabel’) is what is used for updating the plot etc. You can find more +information in How to implement your own plotters and plugins .

+
+
+ + +
+
+ + + +
+
+
+
+ + + + \ No newline at end of file diff --git a/develop/index.html b/develop/index.html new file mode 100644 index 0000000..d7ad3ba --- /dev/null +++ b/develop/index.html @@ -0,0 +1,424 @@ + + + + + + + Developers guide — psyplot documentation + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +
+

Developers guide

+

In this section we provide a deeper overview and introduction in the psyplot +frameworks that is necessary for creating new plugins.

+ +
+ + +
+
+ + + +
+
+
+
+ + + + \ No newline at end of file diff --git a/develop/plugins_guide.html b/develop/plugins_guide.html new file mode 100644 index 0000000..83a8ffd --- /dev/null +++ b/develop/plugins_guide.html @@ -0,0 +1,901 @@ + + + + + + + How to implement your own plotters and plugins — psyplot documentation + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +
+

How to implement your own plotters and plugins

+

New plotters and plugins to the psyplot framework are highly welcomed. In this +guide, we present how to create new plotters and explain +to you how you can include them as a plugin in psyplot.

+
+

Creating plotters

+

Implementing new plotters can be very easy or quite an effort depending on how +sophisticated you want to do it. In principle, you only have to implement the +Formatoption.update() method and a default value. I.e., one simple +formatoption would be

+
In [1]: from psyplot.plotter import Formatoption, Plotter
+
+In [2]: class MyFormatoption(Formatoption):
+   ...:     default = 'my text'
+   ...:     def update(self, value):
+   ...:         self.ax.text(0.5, 0.5, value, fontsize='xx-large')
+   ...: 
+
+
+

together with a plotter

+
In [3]: class MyPlotter(Plotter):
+   ...:     my_fmt = MyFormatoption('my_fmt')
+   ...: 
+
+
+

and your done. Now you can make a simple plot

+
In [4]: from psyplot import open_dataset
+
+In [5]: ds = open_dataset('demo.nc')
+
+In [6]: plotter = MyPlotter(ds.t2m)
+
+
+../_images/docs_demo_MyPlotter_simple.png +

However, if you’re using the psyplot framework, you probably will be a bit more +advanced so let’s talk about attributes and methods of the Formatoption +class.

+

If you look into the documentation of the Formatoption class, you find +quite a lot of attributes and methods which probably is a bit depressing and +confusing. But in principle, we can group them into 4 categories, the interface +to the data, to the plotter and to other formatoptions. Plus an additional +category for some Formatoption internals you definitely have to care about.

+
+

Interface for the plotter

+

The first interface is the one, that interfaces to the plotter. The most +important attributes in this group are the key, +priority, plot_fmt, +initialize_plot() and most important the +update() method.

+

The key is the unique key for the formatoption inside the +plotter. In our example above, we assign the 'my_fmt' key to the +MyFormatoption class in MyPlotter. Hence, this key is defined when the +plotter class is defined and will be automatically assigned to the formatoption.

+

The next important attribute is the priority attribute. There are three +stages in the update of a plotter:

+
    +
  1. The stage with data manipulation. If formatoptions manipulate the data that +shall be visualized (the data attribute), those +formatoptions are updated first. They have the psyplot.plotter.START +priority

  2. +
  3. The stage of the plot. Formatoptions that influence how the data is +visualized are updated here (e.g. the colormap or formatoptions that do the +plotting). They have the psyplot.plotter.BEFOREPLOTTING priority.

  4. +
  5. The stage of the plot where additional informations are inserted. Here all +the labels are updated, e.g. the title, xlabel, etc.. This is the default +priority of the Formatoption.priority attribute, the +psyplot.plotter.END priority.

  6. +
+

If there is any formatoption updated within the first two groups, the plot of +the plotter is updated. This brings us to the third important attribute, the +plot_fmt. This boolean tells the plotter, whether the +corresponding formatoption is assumed to make a plot at the end of the second +stage (the BEFOREPLOTTING stage). If this attribute is +True, then the plotter will call the Formatoption.make_plot() method +of the formatoption instance.

+

Finally, the initialize_plot() and +update() methods, this is were your contribution really is +required. The initialize_plot() method is called when the +plot is created for the first time, the update() method +when it is updated (the default implementation of the +initialize_plot() simply calls the +update() method). Implement these methods in your +formatoption and thereby make use of the interface to the +data and other +formatoptions.

+
+
+

Interface to the data

+

The next set of attributes help you to interface to the data. There are two +important parts in this section the interface to the data and the +interpretation of the data.

+

The first part is mainly represented to the Formatoption.data and +Formatoption.raw_data attributes. The plotter that contains the +formatoption often creates a copy of the data because the data for +the visualization might be modified (see for example the +psy_reg.plotter.LinRegPlotter). This modified data can be accessed +through the Formatoption.data and should be the standard approach to +access the data within a formatoption. Nevertheless, the original data can be +accessed through the Formatoption.raw_data attribute. However, it only +makes sense to access this data for formatoption with START +priority.

+

The result of these two attributes depend on the +Formatoption.index_in_list attribute. The data objects in the psyplot +framework are either a xarray.DataArray or a list of those in a +psyplot.data.InteractiveList. If the +index_in_list attribute is not None, and the data object +is an InteractiveList, then only the array at the +specified position is returned. To completely avoid this issue, you might also +use the iter_data or iter_raw_data +attributes.

+

The second part in this section is the interpretation of the data and here, +the formatoption can use the Formatoption.decoder attribute. This +subclass of the psyplot.data.CFDecoder helps you to identify the +x- and y-variables in the data.

+
+
+

Interfacing to other formatoptions

+

A formatoption is the lowest level in the psyplot framework. It is represented +at multiple levels:

+
    +
  1. at the lowest level through the subclass of the Formatoption class

  2. +
  3. at the Plotter class level which includes the formatoption class +as a descriptor (in our example above it’s MyPlotter.my_fmt)

  4. +
  5. at the Plotter instance level through

    +
      +
    1. a personalized instance of the corresponding Formatoption class +(i.e. plotter = MyPlotter(); plotter.my_fmt is not MyPlotter.my_fmt)

    2. +
    3. an item in the plotter (i.e. plotter = MyPlotter(); plotter['my_fmt'])

    4. +
    +
  6. +
  7. In the update methods of the Plotter, +psyplot.data.InteractiveBase and psyplot.data.ArrayList +as a keyword (i.e. +plotter = MyPlotter(); plotter.update(my_fmt='new value'))

  8. +
+

Hence, there is one big to the entire framework, that is: the functionality +of a new formatoption has to be completely defined through exactly one argument, +i.e. it must be possible to assign a value to the formatoption in the plotter.

+

For complex formatoption, this might indeed be quite a challenge for the +developer and there are two solutions to it:

+
    +
  1. The simple solution for the developer: Allow a dictionary as a formatoption, +here we also have the psyplot.plotter.DictFormatoption to help you.

  2. +
  3. Interface to other formatoptions

  4. +
+
+

First solution: Use a dict

+

That said, to implement a formatoption that inserts a custom text and let the +user define the size of the text, you either create a formatoption that accepts +a text via

+
class CustomText(DictFormatoption):
+
+    default = {'text': ''}
+
+    text = None
+
+    def validate(self, value):
+        if not isinstance(value, dict):
+            return {'text': value}
+        return value
+
+    def initialize_plot(self, value):
+        self.text = self.ax.text(0.2, 0.2, value['text'],
+                                 fontsize=value.get('size', 'large'))
+
+    def update(self, value):
+        self.text.set_text(value['text'])
+        self.text.set_fontsize(value.get('size', 'large'))
+
+
+class MyPlotter(Plotter):
+
+    my_fmt = CustomText('my_fmt')
+
+
+

and then you could create and update a plotter via

+
p = MyPlotter(xarray.DataArray([]))
+p.update(my_fmt='my text')  # updates the text
+p.update(my_fmt={'size': 14})  # updates the size
+p.update(my_fmt={'size': 14, 'text': 'Something'})  # updates text and size
+
+
+

This solution has the several advantages:

+
    +
  • The user does not get confused through too many formatoptions

  • +
  • It is easy to allow more keywords for this formatoption

  • +
+

Indeed, the psy_simple.plotter.Legend formatoption uses this framework +since the matplotlib.pyplot.legend() function accepts that many keywords +that it would be not informative to create a formatoption for each of them.

+

Otherwise you could of course avoid the DictFormatoption and just +force the user to always provide a new dictionary.

+
+
+

Second solution: Interact with other formatoptions

+

Another possibility is to implement a second formatoption for the size of the +text. And here, the psyplot framework helps you with several attributes of the +Formatoption class:

+
+
the children attribute

Forces the listed formatoptions in this list to be updated before the +current formatoption is updated

+
+
the dependencies attributes

Same as children but also forces an update if one +of the named formatoptions are updated

+
+
the parents attribute

Skip the update if one of the parents is updated

+
+
the connections attribute

just provides connections to the listed formatoptions

+
+
+

Each of those attributes accept a list of strings that represent the +formatoption keys of other formatoptions. Those formatoptions are then +accessible within the formatoption via the usual getattr(). I.e. if you +list a formatoption in the children attribute, you can +access it inside the formatoption (self) via self.other_formatoption.

+

In our example of the CustomText, this could be implemented via

+
class CustomTextSize(Formatoption):
+    """
+    Set the fontsize of the custom text
+
+    Possible types
+    --------------
+    int
+        The fontsize of the text
+    """
+
+    default = int
+
+    def validate(self, value):
+        return int(value)
+
+    # this text has not to be updated if the custom text is updated
+    children = ['text']
+
+    def update(self, value):
+        self.text.text.set_fontsize(value)
+
+
+class CustomText(Formatoption):
+    """
+    Place a text
+
+    Possible types
+    --------------
+    str
+        The text to display""""
+
+    def initialize_plot(self, value):
+        self.text = self.ax.text(0.2, 0.2, value['text'])
+
+    def update(self, value):
+        self.text.set_text(value)
+
+
+class MyPlotter(Plotter):
+
+    my_fmt = CustomText('my_fmt')
+    my_fmtsize = CustomTextSize('my_fmtsize', text='my_fmt')
+
+
+

the update in that sense would be like

+

and then you could create and update a plotter via

+
p = MyPlotter(xarray.DataArray([]))
+p.update(my_fmt='my text')  # updates the text
+p.update(my_fmtsize=14)  # updates the size
+p.update(my_fmt='Something', my_fmtsize=14)  # updates text and size
+
+
+

The advantages of this methodology are basically:

+
    +
  • The user straight away sees two formatoptions that can be interpreted +easiliy

  • +
  • The formatoption that controls the font size could easily be subclassed and +replaced in a subclass of MyPlotter. In the first framework using the +DictFormatoption, this would mean that the entire process has to be +rewritten.

    +

    As you see in the above definition +my_fmtsize = CustomTextSize('my_fmtsize', text='my_fmt'), we provide an +additional text keyword. That is because we explicitly named the +text key in the children attribute of the CustomTextSize +formatoption. In that way we can tell the my_fmtsize formatoption how to +find the necessary formatoption. That works for all keys listed in the +children, dependencies, +parents and connections +attributes.

    +
  • +
+
+
+
+
+

Creating new plugins

+

Now that you have created your plotter, you may want to include it in the +plot methods of the Project class such that you can +do something like

+
import psyplot.project as psy
+psy.plot.my_plotter('netcdf-file.nc', name='varname')
+
+
+

There are three possibilities how you can do this:

+
    +
  1. The easy and fast solution for one session: register the plotter using the +psyplot.project.register_plotter() function

  2. +
  3. The easy and steady solution: Save the calls you used in step 1 in the +'project.plotter.user' key of the +rcParams

  4. +
  5. The steady and shareable solution: Create a new plugin

  6. +
+

The third solution has been used for the psy-maps and psy-simple plugins and +will be described in the following section.

+
+

Creating a package with the psyplot-plugin-template

+

The psyplot-plugin-template provides a template to create a python package that +integrates with the psyplot environment. We recommend using this template as it +already contains a setup for automated formatters and linters, and a setup for +continuous integration.

+
+

Note

+

When creating a real package, we strongly recommend to use cruft instead +of cookiecutter!

+
+

For our demonstration, let’s create a plugin named my-plugin. We will save +this name in to a YAML-file and use this to create our new plugin.

+
In [7]: !echo "default_context: {project_slug: my-plugin}" > "config.yaml"
+
+In [8]: cookiecutter --no-input --config-file config.yaml https://codebase.helmholtz.cloud/psyplot/psyplot-plugin-template.git
+
+In [9]: import glob
+
+In [10]: glob.glob('my-plugin/**', recursive=True)
+Out[10]: 
+['my-plugin/',
+ 'my-plugin/my_plugin',
+ 'my-plugin/my_plugin/py.typed',
+ 'my-plugin/my_plugin/_version.py',
+ 'my-plugin/my_plugin/plugin.py',
+ 'my-plugin/my_plugin/__init__.py',
+ 'my-plugin/my_plugin/plotters.py',
+ 'my-plugin/tests',
+ 'my-plugin/tests/test_imports.py',
+ 'my-plugin/setup.py',
+ 'my-plugin/pyproject.toml',
+ 'my-plugin/conftest.py',
+ 'my-plugin/docs',
+ 'my-plugin/docs/installation.md',
+ 'my-plugin/docs/index.md',
+ 'my-plugin/docs/requirements.txt',
+ 'my-plugin/docs/_templates',
+ 'my-plugin/docs/_templates/footer.html',
+ 'my-plugin/docs/make.bat',
+ 'my-plugin/docs/contributing.md',
+ 'my-plugin/docs/_static',
+ 'my-plugin/docs/_static/license_logo.png.license',
+ 'my-plugin/docs/_static/license_logo.png',
+ 'my-plugin/docs/api.md',
+ 'my-plugin/docs/conf.py',
+ 'my-plugin/docs/Makefile',
+ 'my-plugin/README.md',
+ 'my-plugin/CITATION.cff',
+ 'my-plugin/CHANGELOG.md',
+ 'my-plugin/MANIFEST.in',
+ 'my-plugin/tox.ini',
+ 'my-plugin/Makefile']
+
+
+

The following files are created in a directory named 'my-plugin':

+
+
pyproject.toml

The python package configuration

+
+
'my_python_package/plugin.py'

The file that sets up the configuration of our plugin. This file should +define the rcParams for the plugin (see also rcParams handling in plugins)

+
+
'my_python_package/plotters.py'

The file in which we define the plotters. This file should contain the +plotters and formatoptions from our previous section.

+
+
+

If you want to see more, look into the comments in the created files.

+
+
+
+

rcParams handling in plugins

+

Every formatoption does have default values. In +our example above, we simply set it via the +default attribute. This is a hard-coded, +but easy, stable and quick solution.

+

However, your formatoption could also be used in different plotters, each +requiring a different default value. Or you want to give the user the +possibility to set his own default value. For this, we implemented the

+ + + + + + +

psyplot.plotter.Plotter._rcparams_string

List of base strings in the psyplot.rcParams dictionary

+

attribute. Here you can specify a string for this plotter which is used to +get the default value of the formatoptions in this plotter from the +rcParams. The expected +default_key for one formatoption would +then be the_chosen_string + fmt_key.

+

The following example illustrates this:

+
In [11]: from psyplot.config.rcsetup import rcParams
+   ....: from psyplot.plotter import Plotter, Formatoption
+   ....: 
+
+
+

First we define our defaultParams, a mapping from default key to the +default value, a validation function, and a description (see the +psyplot.config.rcsetup.defaultParams dictionary).

+
In [12]: defaultParams = {
+   ....:     'plotter.example_plotter.fmt1': [
+   ....:         1, lambda val: int(val), 'Example formatoption']
+   ....:     }
+   ....: 
+
+
+

Then we update the defaultParams of +the psyplot.rcParams and set the +value

+
In [13]: rcParams.defaultParams.update(defaultParams)
+
+In [14]: rcParams.update_from_defaultParams(defaultParams)
+   ....: print(rcParams['plotter.example_plotter.fmt1'])
+   ....: 
+1
+
+
+

Now we define a formatoption for our new plotter class and implement it in a +new plotter object.

+
In [15]: class ExampleFmt(Formatoption):
+   ....:     def update(self, value):
+   ....:         pass
+   ....: 
+
+In [16]: class ExamplePlotter(Plotter):
+   ....:     # we use our base string, 'plotter.example_plotter.'
+   ....:     _rcparams_string = ['plotter.example_plotter.']
+   ....:     # and register a formatoption for the plotter
+   ....:     fmt1 = ExampleFmt('fmt1')
+   ....: 
+
+
+

If we now create a new instance of this ExamplePlotter, the fmt1 +formatoption will have a value of 1, as we defined it in the above +defaultParams:

+
In [17]: plotter = ExamplePlotter()
+
+In [18]: print(plotter['fmt1'])
+1
+
+# and the default_key is our string in the defaultParams, a combination
+# of the _rcparams_string and the formatoption key
+In [19]: print(plotter.fmt1.default_key)
+plotter.example_plotter.fmt1
+
+In [20]: print(plotter.fmt1.default)
+1
+
+
+

Changing the value in the rcParams, also +changes the default value for the plotter

+
In [21]: rcParams['plotter.example_plotter.fmt1'] = 2
+
+In [22]: print(plotter.fmt1.default)
+2
+
+
+

Also, if we subclass this plotter, the default_key will not change

+
In [23]: class SecondPlotter(ExamplePlotter):
+   ....:     # we set a new _rcparams_string
+   ....:     _rcparams_string = ['plotter.another_plotter.']
+   ....: 
+
+In [24]: plotter = SecondPlotter()
+
+# still the same key, although we defined a different _rcparams_string
+In [25]: print(plotter.fmt1.default_key)
+plotter.example_plotter.fmt1
+
+
+

If you’re developing a new plugin you would then have to define the +rcParams and defaultParams in the plugin.py script (see +Creating new plugins) and they will then be automatically implemented in +psyplot.rcParams.

+
+
+ + +
+
+ + + +
+
+
+
+ + + + \ No newline at end of file diff --git a/docs/Makefile b/docs/Makefile deleted file mode 100644 index b1567a1..0000000 --- a/docs/Makefile +++ /dev/null @@ -1,24 +0,0 @@ -# SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH -# -# SPDX-License-Identifier: CC0-1.0 - -# Minimal makefile for Sphinx documentation -# - -# You can set these variables from the command line, and also -# from the environment for the first two. -SPHINXOPTS ?= -SPHINXBUILD ?= sphinx-build -SOURCEDIR = . -BUILDDIR = _build - -# Put it first so that "make" without argument is like "make help". -help: - @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) - -.PHONY: help Makefile - -# Catch-all target: route all unknown targets to Sphinx using the new -# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). -%: Makefile - @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/docs/_static/.gitignore b/docs/_static/.gitignore deleted file mode 100644 index 0afbd0b..0000000 --- a/docs/_static/.gitignore +++ /dev/null @@ -1,5 +0,0 @@ -# SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH -# -# SPDX-License-Identifier: CC0-1.0 - -docs_*.png diff --git a/docs/_templates/footer.html b/docs/_templates/footer.html deleted file mode 100644 index 3e57079..0000000 --- a/docs/_templates/footer.html +++ /dev/null @@ -1,22 +0,0 @@ - - -{% extends "!footer.html" %} -{% block extrafooter %} - - - -{% endblock %} diff --git a/docs/conf.py b/docs/conf.py deleted file mode 100644 index c055b05..0000000 --- a/docs/conf.py +++ /dev/null @@ -1,280 +0,0 @@ -# SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH -# -# SPDX-License-Identifier: LGPL-3.0-only - -# Configuration file for the Sphinx documentation builder. -# -# This file only contains a selection of the most common options. For a full -# list see the documentation: -# https://www.sphinx-doc.org/en/master/usage/configuration.html - -# -- Path setup -------------------------------------------------------------- - -# If extensions (or modules to document with autodoc) are in another directory, -# add these directories to sys.path here. If the directory is relative to the -# documentation root, use os.path.abspath to make it absolute, like shown here. -# -import os -import sys -import warnings -from itertools import product -from pathlib import Path - -from sphinx.ext import apidoc - -# make sure, psyplot from parent directory is used -sys.path.insert(0, os.path.abspath("..")) - -# isort: off - -import psyplot - -# isort: on - -from psyplot.plotter import Formatoption, Plotter - -# automatically import all plotter classes -psyplot.rcParams["project.auto_import"] = True -# include links to the formatoptions in the documentation of the -# :attr:`psyplot.project.ProjectPlotter` methods -Plotter.include_links(True) - -warnings.filterwarnings("ignore", message="axes.color_cycle is deprecated") -warnings.filterwarnings( - "ignore", message=("This has been deprecated in mpl 1.5,") -) -warnings.filterwarnings("ignore", message="invalid value encountered in ") -warnings.filterwarnings("ignore", message=r"\s*examples.directory") -warnings.filterwarnings("ignore", message="numpy.dtype size changed") -warnings.filterwarnings( - "ignore", message="Using an implicitly registered datetime converter" -) -warnings.filterwarnings( - "ignore", message=r"\s*The on_mappable_changed function" -) -warnings.filterwarnings( - "ignore", message=r".+multi-part geometries is deprecated" -) -warnings.filterwarnings( - "ignore", message=r"\s*The array interface is deprecated" -) - - -def generate_apidoc(app): - appdir = Path(app.__file__).parent - apidoc.main( - ["-fMEeTo", str(api), str(appdir), str(appdir / "migrations" / "*")] - ) - - -api = Path("api") - -if not api.exists(): - generate_apidoc(psyplot) - -# -- Project information ----------------------------------------------------- - -project = "psyplot" -copyright = "2021-2024 Helmholtz-Zentrum hereon GmbH" -author = "Philipp S. Sommer" - - -linkcheck_ignore = [ - # we do not check link of the psyplot as the - # badges might not yet work everywhere. Once psyplot - # is settled, the following link should be removed - r"https://.*psyplot" - # HACK: SNF seems to have a temporary problem - r"https://p3.snf.ch/project-\d+", -] - -linkcheck_anchors_ignore = ["^install$"] - - -# -- General configuration --------------------------------------------------- - -# Add any Sphinx extension module names here, as strings. They can be -# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom -# ones. -extensions = [ - "hereon_nc_sphinxext", - "sphinx.ext.doctest", - "sphinx.ext.intersphinx", - "sphinx_design", - "sphinx.ext.autosummary", - "sphinx.ext.todo", - "sphinx.ext.viewcode", - "sphinx.ext.extlinks", - "matplotlib.sphinxext.plot_directive", - "IPython.sphinxext.ipython_console_highlighting", - "IPython.sphinxext.ipython_directive", - "sphinxarg.ext", - "psyplot.sphinxext.extended_napoleon", - "autodocsumm", - "sphinx.ext.imgconverter", -] - - -# Add any paths that contain templates here, relative to this directory. -templates_path = ["_templates"] - -# List of patterns, relative to source directory, that match files and -# directories to ignore when looking for source files. -# This pattern also affects html_static_path and html_extra_path. -exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] - -napoleon_use_admonition_for_examples = True - - -autodoc_default_options = { - "show_inheritance": True, - "members": True, - "autosummary": True, -} - -autoclass_content = "both" - -not_document_data = [ - "psyplot.config.rcsetup.defaultParams", - "psyplot.config.rcsetup.rcParams", -] - -ipython_savefig_dir = "_static" - -# fontawesome icons -html_css_files = [ - "https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.1.1/css/all.min.css" -] - -sd_fontawesome_latex = True - -# -- Options for HTML output ------------------------------------------------- - -# The theme to use for HTML and HTML Help pages. See the documentation for -# a list of builtin themes. -html_theme = "sphinx_rtd_theme" - -html_theme_options = { - "collapse_navigation": False, - "includehidden": False, -} - -# 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/psyplot.png" - -# The name of an image file (within the static path) to use as favicon of the -# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 -# pixels large. -html_favicon = "_static/psyplot.ico" - -# -- Options for LaTeX output --------------------------------------------- - -latex_elements = { - # Additional stuff for the LaTeX preamble. - "preamble": r"\setcounter{tocdepth}{10}" -} - -intersphinx_mapping = { - "pandas": ("https://pandas.pydata.org/pandas-docs/stable/", None), - "numpy": ("https://numpy.org/doc/stable/", None), - "matplotlib": ("https://matplotlib.org/stable/", None), - "seaborn": ("https://seaborn.pydata.org/", None), - "sphinx": ("https://www.sphinx-doc.org/en/master/", None), - "xarray": ("https://xarray.pydata.org/en/stable/", None), - "cartopy": ("https://scitools.org.uk/cartopy/docs/latest/", None), - "psy_maps": ("https://psyplot.github.io/psy-maps/", None), - "psy_simple": ("https://psyplot.github.io/psy-simple/", None), - "psy_reg": ("https://psyplot.github.io/psy-reg/", None), - "psyplot_gui": ("https://psyplot.github.io/psyplot-gui/", None), - "psy_view": ("https://psyplot.github.io/psy-view/", None), - "psyplot_examples": ("https://psyplot.github.io/examples/", None), - "python": ("https://docs.python.org/3/", None), -} - -replacements = { - "`psyplot.rcParams`": "`~psyplot.config.rcsetup.rcParams`", - "`psyplot.InteractiveList`": "`~psyplot.data.InteractiveList`", - "`psyplot.InteractiveArray`": "`~psyplot.data.InteractiveArray`", - "`psyplot.open_dataset`": "`~psyplot.data.open_dataset`", - "`psyplot.open_mfdataset`": "`~psyplot.data.open_mfdataset`", -} - - -def link_aliases(app, what, name, obj, options, lines): - for (key, val), (i, line) in product( - replacements.items(), enumerate(lines) - ): - lines[i] = line.replace(key, val) - - -fmt_attrs_map = { - "Interface to other formatoptions": [ - "children", - "dependencies", - "connections", - "parents", - "shared", - "shared_by", - ], - "Formatoption intrinsic": [ - "value", - "value2share", - "value2pickle", - "default", - "validate", - ], - "Interface for the plotter": [ - "lock", - "diff", - "set_value", - "check_and_set", - "initialize_plot", - "update", - "share", - "finish_update", - "remove", - "changed", - "plotter", - "priority", - "key", - "plot_fmt", - "update_after_plot", - "requires_clearing", - "requires_replot", - ], - "Interface to the data": [ - "data_dependent", - "index_in_list", - "project", - "ax", - "raw_data", - "decoder", - "any_decoder", - "data", - "iter_data", - "iter_raw_data", - "set_data", - "set_decoder", - ], - "Information attributes": ["group", "name", "groupname", "default_key"], - "Miscellaneous": ["init_kwargs", "logger"], -} - - -def group_fmt_attributes(app, what, name, obj, section, parent): - if parent is Formatoption: - return next( - (group for group, val in fmt_attrs_map.items() if name in val), - None, - ) - - -def setup(app): - app.connect("autodoc-process-docstring", link_aliases) - app.connect("autodocsumm-grouper", group_fmt_attributes) diff --git a/docs/demo.nc.license b/docs/demo.nc.license deleted file mode 100644 index 919c9c1..0000000 --- a/docs/demo.nc.license +++ /dev/null @@ -1,3 +0,0 @@ -SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH - -SPDX-License-Identifier: CC0-1.0 diff --git a/docs/develop/psyplot_framework.ai b/docs/develop/psyplot_framework.ai deleted file mode 100644 index cacaa20..0000000 --- a/docs/develop/psyplot_framework.ai +++ /dev/null @@ -1,7432 +0,0 @@ -%PDF-1.5 % -1 0 obj <>/OCGs[6 0 R 138 0 R 269 0 R]>>/Pages 3 0 R/Type/Catalog>> endobj 2 0 obj <>stream - - - - - application/pdf - - - Print - - - 2017-03-05T18:36:43+01:00 - 2017-03-05T18:36:43+01:00 - 2016-04-13T10:29:44+02:00 - Adobe Illustrator CC 2015 (Macintosh) - - - - 256 - 256 - JPEG - /9j/4AAQSkZJRgABAgEASABIAAD/7QAsUGhvdG9zaG9wIDMuMAA4QklNA+0AAAAAABAASAAAAAEA AQBIAAAAAQAB/+4ADkFkb2JlAGTAAAAAAf/bAIQABgQEBAUEBgUFBgkGBQYJCwgGBggLDAoKCwoK DBAMDAwMDAwQDA4PEA8ODBMTFBQTExwbGxscHx8fHx8fHx8fHwEHBwcNDA0YEBAYGhURFRofHx8f Hx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8f/8AAEQgBAAEAAwER AAIRAQMRAf/EAaIAAAAHAQEBAQEAAAAAAAAAAAQFAwIGAQAHCAkKCwEAAgIDAQEBAQEAAAAAAAAA AQACAwQFBgcICQoLEAACAQMDAgQCBgcDBAIGAnMBAgMRBAAFIRIxQVEGE2EicYEUMpGhBxWxQiPB UtHhMxZi8CRygvElQzRTkqKyY3PCNUQnk6OzNhdUZHTD0uIIJoMJChgZhJRFRqS0VtNVKBry4/PE 1OT0ZXWFlaW1xdXl9WZ2hpamtsbW5vY3R1dnd4eXp7fH1+f3OEhYaHiImKi4yNjo+Ck5SVlpeYmZ qbnJ2en5KjpKWmp6ipqqusra6voRAAICAQIDBQUEBQYECAMDbQEAAhEDBCESMUEFURNhIgZxgZEy obHwFMHR4SNCFVJicvEzJDRDghaSUyWiY7LCB3PSNeJEgxdUkwgJChgZJjZFGidkdFU38qOzwygp 0+PzhJSktMTU5PRldYWVpbXF1eX1RlZmdoaWprbG1ub2R1dnd4eXp7fH1+f3OEhYaHiImKi4yNjo +DlJWWl5iZmpucnZ6fkqOkpaanqKmqq6ytrq+v/aAAwDAQACEQMRAD8A9U4q0zKqlmIVVFWY7AAY qw3XfzHtLZmg0uMXUq7Gd6iIEeAFC/4fTmPk1AjsNy5+n0E57nYMMv8AzV5gvifXvZAhFDHGfTSn gQlK/TmJLUSLtMegxR6Ww7zB5lutP1HTtNtrM317qfrGBWlESD0FDtyYq/Y+GGAMgSTVIzGMJCMY Ak+4LtI8xC81GfSry0ew1O3QTG3dkkV4WPESRuh+Ja7HYYJxIFg2E4pxlIxlHhkE5plfEe9yfCj3 B1MeI96+FHuCDe8u11WK0WydrV4mke+5KERwaCPj9ok9f89pA7XbUYjjEeHbvRlMjxHvbfCj3B1M eI96+FHuCnc3EFrby3Nw4jghRpJZD0VVFWJ+QxBkTTGUIRFkCvc1a3MF1bQ3VuwkgnRZYnFQGRxy U7+IOJMgaWMIEWAKPkq0x4j3svCj3B1MeI96+FHuDqY8R718KPcHUx4j3r4Ue4OpjxHvXwo9wdTH iPevhR7g6mPEe9fCj3B1MeI96+FHuDqY8R718KPcHUx4j3r4Ue4OpjxHvXwo9wdTHiPevhR7g6mP Ee9fCj3B1MeI96+FHuDasy/ZJHyx4z3oOGHcPkmth5q8wWJHoXshQCgjkPqJTwAetPoyyOokHHya DFLpTM9C/Me0uWWDVIxaytsJ0qYiT4g1Kfj9GZePUCWx2Lq9RoJw3G4ZkrKyhlIZWFVYbgg5kOA3 irTMqqWYhVUVZjsABiry7zj5xl1SVrOzYppyGhI2MxH7Tf5PgPpPtg58/QO50WiAHHP4BiuYjtnY qwvzhPcwedPKstrbG7nUX/C3V1jLfuFBozkKKDffMjEAYSvbk4GpJGWBAs7uv7DWgNc80XoWwu4t JntbC2hk9Ro1UNL6jyAL8fMbcdhhEo7RG+6JQn6skvSeEgBQi1TV9H8iN5mu72W+vprO3MUD09BD JxVG4jct8YLktv7YSBKfCBswjKcMXiEkkj4K/mCLVfLOjDXV1W6vZ7V4jfQTsrQzJJIqOFjCqI6c /hK/TXBAxmeGmWWE8UePiJI53yRpvrs/mSln60n1P9Dmf6vyPD1Dc8eZXpy47VyFDw/i3GZ8cDpw /pY3D5k1LVNKn1iBtXXUXMr6bb21tK9oqoxEUbcUZJOXH42J6k0pl/AImvTX2uEMs5gy9fF0oelP LjVdW1fW9N0RJZdJEmnrqeolABP8TCMQKWB4cWPxGlcqEYxBlz3pyZTnklGF8Pps96t5l0Rbfybq 8P168lVLeaZWlnZ3+CI/uy32mQ9wa1wY8lzGwTnwVil6pH4onyLp6WnlrTpFnnm+s2lvIVmlaRUJ iU8Yw32F36DI5pXItukx8MAbJsD8BkGVOS7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYqyr yd5xl0uVbO8YvpzmgJ3MJP7S/wCT4j6R75eDP0LqdbogRxw+IeoqysoZSGVhVWG4IOZzpmG/mPrr W1pHpcDcZboc5yNiIgaAf7Mj8PfMfUZOEUOZc/QafjnZ5B5tmuegdirsVSrUNBF5r+k6uZ+H6LFw BDxr6huECfartxp4ZOM6iR3tM8NzjK/pv7UXqtiNQ0u8sGf0xdwSQGQCpUSoUrTvSuRiaILPJDii R3hCQ+XrQ+WodBvD9ZtktktZGpw5BFC8gKtxO1RvtkvEPFxBgMA8PgO4pL18oXUy21tqerTX+nWj q8Vo0aIXMf8Ad+vIu8nE/KveuWeMBuBRaPyhNCUriOlIfTG/Snn2fWLRXOnW2nCxNw6MivMZ/UIj 5Acgo6nDMcMKPO0YpDJm4o/SI0i4/K+o2UkqaPq72NhNI0ptDDHN6bOSW9FmpwBY1oQwyJyg/ULL IaaUb4JcMe6rRWseXRfXVtqFtdyWOqWqmOO7jCtyjbdkkRhxZa70wQyUKIsM8un4iJA1IdVWPSbq bS7qw1a8N8LtGikdY1gCo6cGChS3udycBmLBApkMUjExnLivyp3l7SbzSdPSxnvTexQKsdqTGsZS JBxVTxJ5UA64MkxI2BScGKUBRN92yZ5BudirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdir 0n8uNda5tJNLnblLajnATuTETQj/AGBP4+2bHT5OIUeYef1+n4J2ORYZ5qvzfeYL2eoKCQxxkdOE fwAj50rmJqJXJ2mgx8OIeaU5S5jsVdiqhJeRrsg5nxHT7/6ZGUxHmmMZS+kKRvZT0VR95/plRzju bhppd4/Hya+uTeC/cf64PzA7k/lT3/Z+131ybwX7j/XH8wO5fyp7/s/a765N4L9x/rj+YHcv5U9/ 2ftd9cm8F+4/1x/MDuX8qe/7P2u+uTeC/cf64/mB3L+VPf8AZ+131ybwX7j/AFx/MDuX8qe/7P2u +uTeC/cf64/mB3L+VPf9n7XfXJvBfuP9cfzA7l/Knv8As/a765N4L9x/rj+YHcv5U9/2ftd9cm8F +4/1x/MDuX8qe/7P2u+uTeC/cf64/mB3L+VPf9n7XfXJvBfuP9cfzA7l/Knv+z9rvrk3gv3H+uP5 gdy/lT3/AGftd9cm8F+4/wBcfzA7l/Knv+z9rvrk3gv3H+uP5gdy/lT3/Z+131ybwX7j/XH8wO5f yp7/ALP2u+uTeC/cf64/mB3L+VPf9n7XfXJvBfuP9cfzA7l/Knv+z9rvrk3gv3H+uP5gdy/lT3/Z +131ybwX7j/XH8wO5fyp7/s/a765N4L9x/rj+YHcv5U9/wBn7XfXJvBfuP8AXH8wO5fyp7/s/auF 6/7SAjuQd/uP9ckM8WJ08x3FXiuIpdlPxDqp2OWg3yaeRo7FUwqm3lW/Nj5gspqhUMgjkJ6cJPgN flWuXaeVScPX4+LEfJKmbkxbxNcrnzLkYR6B7g1kWx2KoK5nMhMa7RjZj/Me/wBGU5cnDsObdhxc e5+n7/2JRPqk41T9H2sCSSRxpNM0svpD05GZR6YCSFyPTNeg6b5XHEOHiJ+y2c9QRk4IgE11NfLY 2rLq2nt6fGWvrXElpH8LbzRc+adO3pNv02yHhS7ul/Bt/MQ7/wCLh+PchNE8x2Wq29s61iuLiET+ iQ1Og5BJCqq/AmjcenfLM2nlAnuDTptdDKB0ken7eqvJrulx3DW7TESJIIZDwcojsAVV3C8E5chT kd8iNPMi6ZHW4hLhJ3Brkfv5LV8waSyzss5b6tIYZQschPqqzKY1AWrvVT8K1NN+hGP5ee23NTrc W+/I1yPPu8/g2mt2Uk9ukcilJ45pC7ckKi3KhwysuxUv8QYgjw8D4EgDfMV9qPzkCRXI8W/dw/jy XrrOmmGSb1eKRlQwZHVquaJRGAZuZ+zQfF2wHBO6pkNZiIJvYe/ry281BfMNm2p21gsc/O6heeN2 hlUAIwWjBkBXr+1SnfqKyOnIiZbbeYYDWxOQQAO4vke+u77U0zHcx2KuxV2KuxV2KuxV2KuxV2Ku xV2KuxV2KuxV2KuxV1OhBoR0I6jJRmYmwwyYxMUUbbT+opVv7xevuPHM2MhIWHXyiYmirq3Fg3ga /dlkOYas30H3FoYy5lOL6R7nZFmp3LlIWI2Y7A+52rgJoWtXt3oAAAADoOmYBNuzAAFBKdb0261A pEkUCoN475mPrwP/ADRLwIr/ALMfTmRgyxhvv7uhcHV6eeXYCP8AW34h7v7UNHoV+l5AA0X1O3v5 b9X5N6reusvJCvHiOLTbHluPDvPx411vh4WsaTJxD6eHxOPrbWi6HqdvFpUF8YPT0mOkbwM5aSQx mL4gyqFVVY9zyO/w9McueJ4qv1LptJkiYcXDUL5XvaGjsr6/m1+xQRLZ3N7wuJWZhKqm2g5cFC0a q9CWFOu+WHJGHBI3Yj+tpGHJk8SArhM9+/ojE0K9ihjkjaNrq2v7q+hRmYRutw0oCOwUlTwm6hTQ +OVePE7HlwgfJyPykxuK4hOUvKj+lLrjQ5bvU3tZZAJby1vmvWiqyQtci3jjUVp+zF7cqE0GXDMI x4gNhwgedW4ktKZT4CfUeMmunFVfci4fLlyhFyERL+GSN4ne6ublZFiEihXM1WQUlagWtCa75X+Y jy34TfQD7m/8lPaW3HEj+KRur7+XNH/UtRfVLLUZFhV44Jre6hV2YKJXRw0bFBzp6VKEL1yrjjwm O/Ow5IxZPEjM8PIg8++9tv1JpmO5jsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVXRuUl R/A0b5HY/wBcuwSo13uPqY3G+5MTmbHmHAy/Sfc4Yy5lcX0j3OyLND3392n+t/A5DL9JZ4vrH46F CZguxaZlVSzEKqirMdgAO5wgEmgpNblimp+fYIpDHYQ+vx2Mzkqp+QG5/DOl0ns7KQvKeHyHN0Gp 7djE1jHF59Es/wAf6z/vm3/4F/8AmvNh/obwfzp/Mf8AEuF/L2buj9v63f4/1n/fNv8A8C//ADXj /obwfzp/Mf8AEr/L2buj9v63f4/1n/fNv/wL/wDNeP8Aobwfzp/Mf8Sv8vZu6P2/rbX8wNXr8UFu R3AVx/xucB9m8H86f2fqUdvZu6P2/rT3RfOVjfyLBOv1W4bZQTVGPgG239jmm1/YeTCOKJ44/aHa 6PtfHlPDL0y+xkOaN262SRI0LyMFRRVmPQZOEDIgRFksMmSMImUjQCSXXmYBittEGA6O9d/oGdBp +wbF5JfAfreY1PtJRrFG/M/qQ3+Jr/8A33F9zf8ANWZX8g4e+X2fqcP/AESaj+bD5H/inf4mv/8A fcX3N/zVj/IOHvl9n6l/0Saj+bD5H/inf4mv/wDfcX3N/wA1Y/yDh75fZ+pf9Emo/mw+R/4p3+Jr /wD33F9zf81Y/wAg4e+X2fqX/RJqP5sPkf8Ainf4mv8A/fcX3N/zVj/IOHvl9n6l/wBEmo/mw+R/ 4p3+Jr//AH3F9zf81Y/yDh75fZ+pf9Emo/mw+R/4p3+Jr/8A33F9zf8ANWP8g4e+X2fqX/RJqP5s Pkf+Kd/ia/8A99xfc3/NWP8AIOHvl9n6l/0Saj+bD5H/AIp3+Jr/AP33F9zf81Y/yDh75fZ+pf8A RJqP5sPkf+Kd/ia//wB9xfc3/NWP8g4e+X2fqX/RJqP5sPkf+Kd/ia//AN9xfc3/ADVj/IOHvl9n 6l/0Saj+bD5H/inf4mv/APfcX3N/zVj/ACDh75fZ+pf9Emo/mw+R/wCKd/ia/wD99xfc3/NWP8g4 e+X2fqX/AESaj+bD5H/inf4mv/8AfcX3N/zVj/IOHvl9n6l/0Saj+bD5H/ilSHzPMG/fwqy9ylQf xrlWXsCFeiRvz/AbsPtLO/XEEeW332nlrdwXUQkhbkvcdwfAjOf1GmnhlwzD1Gl1ePPDigbCq/2G +RyrH9Q97Zm+g+4poc2EeYdZl+k+5wxlzK4vpHudkWaHvf7tP9b+ByvL9JZ4vrH46FCZhOxYt5y1 ZoZrOwVuCyMstwenwBqAH2qDX5Z03s9oxIyynpsPf1dB25qjEDGOu59ye+Yrqw1TXr+11rVLLUEu NZluNEeW7S4hi09FuGMRmhnQRJMWhWOJpo6EblBvnQYwYxBiCPTvt128vf0LzKnqmneRXWz03Tf0 ULdNSMs9zPcEP6U2n28wiJW7BKGf1Yv73ihUBpFJZ2Yyybk8XL9J8u7fl8FVNNk/L7QNY1cLfKum 6wLbTY0gWK74W9zbJLe863DGJEmkUK/OQqUI+MgnGXiTA23G/d126Lsld9pnkOC1ks4k09r1P0Nb i+N5NJH6l1bub+ciKVgyRSqvLiPh/DJxlkJvevV0+S7I2Tyz+Xs2rrbq8KwixGo3j2spkjiNlcuL mBeF1ehfXtlqnKVjz4048uIiMuSvjXzG3QcitB5hNIjzPJGghRmLJGpJCAmoUFiTt75nVsh6V5U1 R9Q0hHlPKeEmKVj1JUAg/SCM8/7Y0gwZyI/TLcPa9l6k5sIJ+obFC+ZbxmmW1U0RAGf3Y9PuGbTs PTAQOQ8zsPc6D2j1ZMxiHIbn3ph5CbSGl1C11WWKO0uxZRSrLIIg0f6StmlAaqnaMMxodgCe2bnN exHn9xedx1vf43R0Q8qjSr1re3sRdXmll1t5ZnpFPBflD6TPKrLI9uglCFqsdgOLcTA8Vjnz/Qy9 NK+s2H5fWcN7JYpHdmK0JsvUnCrKzXEMccnGK5ll9QRvIzqyx7D+7WhwRMzV/j7EyEA3eaJ5MiS2 MEljcaiIp1W2+tenZzSxtB6bSSG6kdA8csrDk8XIrTipBBROfnX48v1qYxV4Ljyl+jIBewWUEP1B YL2OzuXEjSrqiiUFFnb1PThb1ULVDeJVfhBEr2vn+hIMa/HegtP0nyGt3f2t1Mk5sIoIllilRVuG 4ubqeF5bi3j5K/FY/iIK7+m1TSUpToEdfx3MRGKVpAt35Q0sRXFur2N7ey3UUlzBDMqSJbcWWOR0 kevptTgD0yd1I+YH6WPMBk5j8oR61LdBbC4M93qsQa6u/W9YOtx9WlLi44xoTwTlKqmrcw56pT6q rfp+jybPTd+95m/222A3OymoHyNTmY463FXYq7FXYq7FXYq7FUdo941teoa/u5CEkHse/wBGa/tL TDLiP84bh2fZGrOHOP5stj+PJlz/AGG+RziofUPe9/m+g+4poc2EeYdZl+k+5wxlzK4vpHudkWaH vf7tP9b+ByvL9JZ4vrH46FCZhOxee+fP+O2v/GFP1tnc+z3+L/5x/Q8j25/f/wCaP0sczeOmdirs VdiqrFd3UUE0EUzxwXAUXESsQsgQ8lDgfaodxXAQFUsKs5/L3/eO7/4yL/xHOQ9pfrh7i9R2B9Ev et13/jqz/wCx/wCIDNh2R/i0fj/ui8725/jc/h/uQgM2TqmiQOpp88VdzT+Yffhoq7mn8w+/Giru afzD78aKtgg7g1GBXYq7FXYq7FXYq7FXYq7FXYq7FV0X96n+sP15Gf0lni+oe9nT/Yb5HPPIfUPe +n5voPuKaHNhHmHWZfpPucMZcyuL6R7nZFmh73+7T/W/gcry/SWeL6x+OhQmYTsXnnnz/jtr/wAY U/W2dz7Pf4v/AJx/Q8j25/f/AOaP0sdzeOmdirsVdirsVdirOfy9/wB47v8A4yL/AMRzkPaX64e4 vUdgfRL3rdd/46s/+x/4gM2HZH+LR+P+6Lzvbn+Nz+H+5CVw2urarLc2+krF/ocYlvLieeC2jjQu sYrJcPGgq7qvXqc3EYxiAZdeX4DrIxJY7qNpfWd7La3yNHdRGkiPuRtUb71BBqCOozNgQRY5KRSy 8s7qyupbS7iaC6gYxzQyAq6OpoVYHoRhjIEWFIpFXug6vZaZYapdWzRWGqCQ2E5KkSCFuElACSKN 4gZGOSJJAO45qYkC0vyaF8cssTco2KN4g0wGIPNU80vUzcfupf74CoP8w/rmFmw8O45ITEkAVOwH U5QqEurTzNPpx1Ky0y7OjjkTqS28phIUlWPq8eAAYU65k44QBqRHF3WyETzSD1rqRqc3dtzSpJ23 OZXCB0Q6H65PMkMPqSzSsEjjTkzMzGgVQNyScSAFpucXtvPJb3AkhnhZo5YpOSujqaMrKaEEEUIO IAO4WlgnmHSRvvOPCO5URbardwsKuZE7q+/49chPDEqn9rcx3MQkjOx2I7g+GYM4GJooVciq6L+9 T/WH68jP6SzxfUPezp/sN8jnnkPqHvfT830H3FNDmwjzDrMv0n3OGMuZXF9I9zsizQ97/dp/rfwO V5fpLPF9Y/HQoTMJ2LDfOsskT3MsX97HbF0NK/EqsRt883ehJ8MPlntYOLtKETyMYf7opC893bSo sv8AdGOZgZiikemIqeu8YKL8TOKqKdO+ZYke91c9JiyQJxkgxO9cR79gCdzyaOo3ImZTAvBZ1i6k MY2iLBgCKfFIOMZrRztt1w2e9B7KA245Xwk30ux+vcfFXtb1p/RDw+k0omJ+JHAELKuzIWBqX+ih wGR73C1Wj8KJlxk0a+kjffvPLbnuiqDwwcR73WeJLvLqDwx4j3r4ku8uoPDHiPeviS7yyPyr/c3H +sv6jms7RJPD8X032EkThyX/ADh9yV+Y39O/un/kUN90YOdL2ML08B7/APdFp7c/xufw/wByEj8s 6tpFtZ65Y6tJcRRataxwJcW0STujx3UNzVkklgBDCEj7XfN/lhImJjXpP6CPN10SKILKbH8ytFht NWDRXsU91a/UrKNHZoglvYLa2UzhZoV9aOWMSM7RyU/Y4ld8eWlkSOXO/ts9OXy82zxBur3n5meX rqXzRcTHUbiTXluRFBPVoUE1sUt1KC5Ef+jTH4XZH+GhVY2G4jpZDhG3pr79+nUe74qcg3as/wA0 9Bg5QLp0lvBp0DxaHdQgm4LHT5rEG4WSdok5mVZG9IdRvyO+MtJI73z5/MHbb71GUKPl78zNMtr7 QX1Y381lpVm8dzBEQVlupb2SaZmT1YvVSSCTgebD4vtKy1DHJpSRLhqyfsr9ajL3pX5j8w2r+TdN sIwv6TuggvpEkEh+pWjSCxjfiW4vSYhlO/FI8sxYz4hPQfeef48yxnLZh1vI0c8br1VgfxzJkLFN SfazM0dkQuxkIT6Duf1ZhYI3JCf6Prvl+0h8oapLqiLN5fhlF7pSRzm4mP16e4EQYx/V+EscqqxM mwJ27ZKeORMxX1ddu4D3t8ZDbfkm3l7zP5F0vTdPaO5tRqoR4nuJLJT6SXWn3McqSqtoS6pdNECT JMzJ0oC6ZVkxZJE869/cR591936WQlEKela/+Xem6Rp95JOlzrWlvHdQC3gFrO8ht5g0XqJaKo9O 5Mbq8skuwqOO6YZ48spEfwn49R593uUSiPeti82eRJdafVrmKCQ6nfaNcXsN7b/XJIYozMupgyNC FYvRHbgvxcthUUBOHII8I6CXLbu4eqOON37kHZ6z5Pk8uXTOlrPr0kizafp62Ma8biO7jEcQCWp9 SOS3BqHuOO5Ux1oxlKGTjHPh6m/L39/l8V4o15pL+Yq6Hba0umaNAIbeyVjOSqiX155GmeKRl6/V w6wfND45bpuIxuXX8fbzYZKugk+hSsty0X7LrWnuMOpjtbUU9zCVdF/ep/rD9eRn9JZ4vqHvZ0/2 G+RzzyH1D3vp+b6D7imhzYR5h1mX6T7nDGXMri+ke52RZoe9/u0/1v4HK8v0lni+sfjoUJmE7Fiv mQf7kSewjUknbbfN3oIGUNnyr2v088uv4YCz4Y+8pP68AZV9WPkwLKOa1KgVJG/TfM3wZfgh5v8A kjU/zftH622lhUgNIgLbKCyipDBNt/5mA+ePgy/FL/JGp/mfaP1tRzQShTFKkgcFk4up5BSASKHe hIxGGRX+SNT/ADPtH62muLdS4aaMGP8AvAXUcfhL/Fvt8IJ+WPgy/BC/yRqf5n2j9a/knDnzXht8 XIU3pTeveow+BOrr7l/kjU/zPtH61guLY0AmjJZuC0dd23HEb9fhO3tg8KX4pf5I1P8AN+0frZP5 WBENwD/Mv6s1XaUTEgH8cn0H2Iwyx48sZCpCQ+5J/NX+9N9/xj/5lDOm7E/uIe8/7ouN25/jc/8A N/3IYNnSOrdirsVdirsVdiq5Ptr8xgPJU71//eaP/X/gcw9NzKAkWZqXYq7FXYq2CQajYjocVaxV H6L/AL3D/VOUaj6VLIcwULov71P9YfryM/pLPF9Q97On+w3yOeeQ+oe99PzfQfcU0ObCPMOsy/Sf c4Yy5lcX0j3OyLND3v8Adp/rfwOV5fpLPF9Y/HQoTMJ2KQ+ZbUkx3NKoV9KT23qM3HZOcRlwnq8R 7TYjh1OPU/wEcEvLqCxp5LOOSO3ZOLTVjReB4niteJNOP2a9c6a4XX6GnZ11cWdrFynHCMVf7JYD j8ZOwPz+eM+COxSQFlpJp7O0Vqiq1sOBVU48BIA9BsNm2O2MDAnb7lFIUxWRvVQyRtG7uktuYUPO ZwxqXA6hFZfl1yoCPFzHyY0O9MRBAF4CJQppVQop8NOO3tTbMjgiB0ZUGjaWrFawIeDB0qq7MCSG HgRU4mER0CJcMRZOzL9CtWgsuTCjSnnT2oAP1Vzje0c/iZSRyd97O4DHDLLIUcsuL/NoCPzAv4sc 81f7033/ABj/AOZQzquxP7iHvP8Aui8725/jc/8AN/3IYNnSOrdirsVdirsVdiq5Ptr8xgPJU71/ /eaP/X/gcw9NzKAkWZqXYq7FXYq7FXYqj9F/3uH+qco1H0qWQ5goXRf3qf6w/XkZ/SWeL6h72dP9 hvkc88h9Q976fm+g+4poc2EeYdZl+k+5wxlzK4vpHudkWaHvf7tP9b+ByvL9JZ4vrH46FCZhOxWy RxyxtHIoZGFGU98kCQbDTqNPDNA45jijLmEiu/LkgYtbOGXsjmhHtXeubrS9sGIqQeQyez+pwGsE o5MfSMjUh5cW9j3oM6FqgP8AdA/7Nf65n/y1j/F/qaDou0P9RH/KyP6mv0Fqv++R/wAGv9cf5ax/ i/1L+S7Q/wBRH/KyP6m/0Fqv++R/wa/1x/lnH+L/AFL+S7Q/1Ef8rI/qcug6oTvGo9y4/hgPbWP8 f2JGh7QP+RiP+Sg/Umdh5fSJxJcsHI6Rr9n6SeuavV9qzyjhj6Q7HS9gTnIS1JHCP4I8v84nn7qA TjNS9UBTC/NX+9N9/wAY/wDmUM7nsT+4h7z/ALovn3bn+Nz/AM3/AHIYNnSOrdirsVdirsVdiq5P tr8xgPJU71//AHmj/wBf+BzD03MoCRZmpdirsVdirsVdiqP0X/e4f6pyjUfSpZDmChdF/ep/rD9e Rn9JZ4vqHvZ0/wBhvkc88h9Q976fm+g+4poc2EeYdZl+k+5wxlzK4vpHudkWaHvf7tP9b+ByvL9J Z4vrH46FCZhOxWSyxQxtLKwSNBVnY0AA8cnCBkRGIsljKQiLOwDFdQ8/wRuUsYDMB/u2Q8VPyXr9 9M6TTezkiLyS4fIfrdFqO3Yg1jF+ZQH/ACsHUv8Almh/4f8ArmZ/obw/zpfZ+pxP5eyfzY/a7/lY Opf8s0P/AA/9cf8AQ3h/nS+z9S/y9k/mx+13/KwdS/5Zof8Ah/64/wChvD/Ol9n6l/l7J/Nj9q6P 8wr4MPUtYmXuFLKfvPLIy9msdbSl9n7GUe353vEMj0XzNp+q/u0rFcgVML9SB3U980Wv7Ky6bc+q HeP09zuNH2ljz7DaXcm+at2DDfM6l7y8UdWQAfTGM7jsU1gh8f8AdF8+7c/xufw/3IYJnSurdirs VdirsVdiq+IFpEA6kgD78B5KnevKTaofBxX6Qcw9N9SAkOZqXYq7FXYq7FXYqmGiA/Xh7Kco1H0q WQZgoXRf3qf6w/XkZ/SWeL6h72dP9hvkc88h9Q976fm+g+4poc2EeYdZl+k+5wxlzK4vpHudkWaH vf7tP9b+ByvL9JZ4vrH46FCZhOxSaO7hvvzA8v6HPGs1k17b/XIJAGSTnIPgdTsV49vfOq7F0nDh lm/iN15PM9t6omQxDkNytt/Kq6zJp8euaf8AUtZQXUmp2drGunyizjMCwTPbQW05VuckmyW9So5N QDlm+OXhvhNx2rrvv1v9LoKRd9+XOhfpHTdNt7O5kitk1FNTv1nKeq1lfyW1QqW12wdVCOyIjEIR WgBkMY6iVEk91fEX3j8fJaWW/kvRLDTNe0K5mtpNZvLq9g0V545PrTLpTssbQFEeNPrEiSRsGda0 2qcJzSJjIcgBfx/UtKOo/l75Zj1IafYJfXFwdYvdLVJLmFA0djFFM7ApbyNzYSkKFRiTSik/CTHU SqzX0g/P4rShqP5d+X7O51VWvJhFpEMV9cozhXa1urQSQonqQxNzW6ZYmLItQ6ngtDhjqJEDbnt8 j+rdaeewzSwypLExSRCGRx1BGZU4CQMZCwVhMxII2Ieq6NqA1DTILvYNIvxgdmU0b8Rnm+u03gZp Q7vu6PeaTP4uKM+9j2u/8dWf/Y/8QGdX2R/i0fj/ALovD9uf43P4f7kMO1Owe3lLoKwsag+BPY50 OHLxCurq0DlyuxV2KuxV2Kppo9g7SLcSCka7oD3Pj9GY2fLQoITa7txcW7xHbkNj4EdMxYS4TasY likikMcg4svUZsgQRYSswq7FXYq7FXYqn2jWTQxmaQUeQUUdwv8AbmFnyWaCEyzHVdF/ep/rD9eR n9JZ4vqHvZ0/2G+RzzyH1D3vp+b6D7imhzYR5h1mX6T7nDGXMri+ke52RZoe9/u0/wBb+ByvL9JZ 4vrH46FCZhOxeYebCT5hvK7/ABKPuRc9D7HH+Cw936S8R2r/AIxL8dAlObJ17sVdirsVRUepXken y6fG4S1ndZJ1CqGcp9kM9OZUVrxrSu/XBwi7VC4Vei+R/wDjgr/xkf8AXnC+0H+M/wCaHsexf8XH vKB13/jqz/7H/iAzd9kf4tH4/wC6Lynbn+Nz+H+5CXkBgQRUHqDmydUhJNIsHNfT4n/JJH4ZaM8g qz9C2Pg3/BYfzElt36FsfBv+Cx/MSW3foWx8G/4LH8xJbVItKsYzUR8iO7En8DtgOaR6qi8qV2Kq NzZ29wKSpUjo3Qj6clCZjyVBHQbWu0jgeG39Mu/Mlbd+gLb/AH4/4f0x/MnuW3foC2/34/4f0x/M nuW3foC2/wB+P+H9MfzJ7ltXt9Ks4GDBS7joz70+jpkJ5pFUZlSuxVdF/ep/rD9eRn9JZ4vqHvZ0 /wBhvkc88h9Q976fm+g+4poc2EeYdZl+k+5wxlzK4vpHudkWaHvf7tP9b+ByvL9JZ4vrH46FCZhO xeYea/8AlILz/WX/AIgM9D7H/wAVh7v0l4jtT/GJ/jolObJ17sVdirsVdirsVei+R/8Ajgr/AMZH /XnDe0H+M/5oex7F/wAXHvKB13/jqz/7H/iAzddkf4tH4/7ovKduf43P4f7kIDNk6p2KuxV2KuxV 2KuxV2KuxV2KuxV2KuxV2KuxV2Krov71P9YfryM/pLPF9Q97On+w3yOeeQ+oe99PzfQfcU0ObCPM Osy/Sfc4Yy5lcX0j3OyLND3v92n+t/A5Xl+ks8X1j8dChMwnYsC81tIl1cNCD6rTQoOAQvR3RTx5 /DWh25bZ0OlnIYxv0fIu1/V2rkjInhvvP8y+iWLPPFcNFMI6enEyM5CL8c0iAM4BHqFFX4QKE1pl /iS7y409LHLASxykN+nEe4cieQ3U11G4qtbRdzcL6fIJIDGy+mpDAKrMh5FSfs/FXtj4ku8qezdy BkndR36b3f3fo3RdrcLPwBh4FoUmO6sP3jMAFK1Vh8BNa+GA5Jd5ddq8JxRB45GyehHKu82OfciO Cfyj7sfFl3lwPGn/ADj83cE/lH3Y+LLvK+NP+cfm7gn8o+7HxZd5Xxp/zj82WeWgBpoAFPjb+Gab tAk5N+59c9jZE6EE/wA6SR+a5JIm1GWI0ljgZ4zStGWGqmh98zNHM8AF/i3kO3RxdrSifpJj1/oR SZL28huGhuOH93EyNIQi/HNIoDOAR6hRV+EbEg0zL4ieRLhS0kMsBLHIjvqz3dCeQ3WJrF4SpNqD ya4X0uXGQGJl9NSGAVXZG5FSfs/FXtjcu8qezRZAnK6jv03u/u/Rui7G/N1xBi9OsKTH4lcfGzKA rIWVh+7JrXwyMpSHUuBqsBxRB45Gz3Ecq7zfXuRmR8SXeXA8SXeXY+JLvK+JLvLsfEl3lfEl3l2P iS7yviS7y7HxJd5XxJd5dj4ku8r4ku8ux8SXeV8SXeXY+JLvK+JLvLsfEl3lfEl3l2PiS7yviS7y 7HxJd5XxJd5dj4ku8r4ku8ux8SXeV8SXeW4/7xfmP14+JLvLZhyS4xueYZo/2G+RzQw+oe998zfQ fcU0ObCPMOsy/Sfc4Yy5lcX0j3OyLND3v92p7BhX7iMhkHpLPGamPx0QmYLsWG61QajcsxCqrDkz EADYeOdBpYE4xXc+Ne0GlyZu0M3ALoj/AHIQHqQ+oY/VT1AASnNeQBNBtXx2y/wpOp/krU/zPu/W tkuLaPiJJ4kLqXTlIgqqipYVPQDfE4pJ/knU/wAz7v1r0aNzRJEY8Q1FZT8LVodj0NDhGKR6L/JO p/mfaP1rDc2wDEzxUUlWPqJswIUg79QWA+nB4Ul/krU/zPu/WqMVUVZlAJoCWUbiu3X2w+DLuX+S dT/M+79axJreQoEmjcyAmMK6nkAATSh32IwDFIr/ACTqf5n2j9bLvLgpp1P8tv4Zpu0BWSvJ9R9j 4GOi4TzE5feletD/AHJTHYD4epp+wMzNHilLGCA8Z7S6PLl7QymEbHp/3EUsF3aN9meI1pSjr+03 Be/dhQe+ZPgy7nR/yXqP5v3LfrtlRD9ZipKQsR9RPiJoQF33Jrj4Ml/kvUfzfuVlIZQysCpFQQQQ QckNNk7l/kvUfzD9jdPcfeMfyuTuT/JWp/mH7FJrq0UuGnjUxf3oLqONRy+Lfb4RXI+DLuR/Jeo/ m/d+tURkcckdWFSKhgRVTQjbwIphGmmeQX+S9R/MP2N09x94w/lcncn+StR/MP2Op7j7xj+Vydy/ yVqP5h+xTa5tll9Jpo1lpy9MuoanjStaZE4Jjoj+S9R/M+5tZoHbisqMwIBAZSaleQHX+Xf5YjBP uX+S9R/M+5fT3H3jJflcncn+StR/MP2Op7j7xj+Vydy/yVqP5h+xZLNDEKyyJGKE1ZlUUHU7ntgO nmOYR/Jeo/mfc0lxbvK0KTRtKgBeMOpYA7gkA1FcAwTPRP8AJeo/mfd+tUp7j7xkvyuTuX+StR/M P2Op7j7xj+Vydy/yVqP5h+xdGP3i9Oo7jBLBOIshR2fngRKUaAI7u9mbfZI8RnPY/qD7nmPoPuTQ 5sI8w6zL9J9zhjLmVxfSPc7Is1k0fqRMnQkbH3G4/HFT5JcDX2PQjwIzAlGjTsscxIWGM+Y7Ol2Z GUNFcAVBFRyUUp92b/sfNH6C8F2vi/L9omZ+jPEb/wBKO1fjvST1rX6x9WpSVlMvEoQCFYAnkRxq DTvXN6DAmmWyy7n0+FViuVUIwCqhQstGPpgbAjflTBMwGxU0utJLOUO1sq/AfQYheNPTJHDcDZST hiYHkopA2y2ctwLdorSS3KEwKkZBLKVMpoQVA5FT1yvGYk16fkgUmfowmoMakV5UoPtGtT88yDGN dGVCmha23qI4hTnHX034iq1HE8T22FMjIRiLNbNeScYRMjyDNNKtjbWMcbCjmrOPdjWn0dM4jV5v EyGT1PYemlh0sRIVKVyP+cbr4ckn8yWQM4lZA8cnE7io5pQjr32BGbjsfPGuAvP9s4vA1nGfozAb /wBOO1fEVXuY+E08TCARIJXBkC8KVEb8q1pT4XevzNc3g4CaaNlw0+xAQC2jpEQ0dUU8SoCqRXuA oFcn4ce5NIj6MlS044UhDx22nrUxwwjkADxVBVVXivTqApoPbKqh5Mdl0M1mSYoHjryeqIVryDfv Nh35N8XuclEx5Ckq2TV2KoOS601b5IpAv1siiEoSfs86c6U6JXr4e2UylC9+amrRAt7dWDrEgYEM GCgGoXgDXx4/D8ssEAOi0qZJXYqseKKT+8jD0qByAOx69cjKIPNSFq21ss7XCwos7ji8oUByNti3 XsMRAA3S0q5JXYqi9MtWuLuNKfCDyb/VB3/pms7T1AhjI6lpOH8xmhgG9kSl5RibJ+PIe9mMCc5l HZfib6On45y+CO9vc6mfKKYHMuPMOFl+k+5wxlzK4vpHudkWbsVQ1zbFiZIx8X7S+P8AbkMmPiZ4 8hgfJL7m3huYmhlWq+HRlPj7HMaJljlfIp1ujw63EYT3H2g9/kUhufLt2jEwkSr23Ct9IO345vdP 20KqYeWn2RrsJqIjmj33wy+N7faofoPU/wDfB/4JP+asy/5Zwo/Kaz/UJf6bH/xbv0Hqf++D/wAE n/NWP8s4U/lNZ/qEv9Nj/wCLd+g9T/3wf+CT/mrH+WcKPyms/wBQl/psf/FuXQtSJ/uqe5ZKfgxw HtrFWyfyetOwwn4zh+iR+5NdN0JLdhLOQ8g3VR9kHx365p9b2nLN6RtF2Wh7CmZDJqCDW4gPpvvJ /i+VJtmrenU54Ip4jFKvJG6jJwmYmw42s0ePU4zjyC4n8WPNIrny9cKxMBEi9qni309s3un7ZoVJ 5DL2JrcBrGY5odLPDL49D70P+g9R/wB9H70/5qzL/lrE1jSa7rgP+nh/xTv0HqP++j96f81Y/wAs 4k/lNb/qB/0+P/inHQ9R/wB9H70/5qxHbWJI0mt/1CX+nx/8Upx+W7mIUjtVQEkkL6Y3O5Ox75H+ VsHd9i/k9Z/qEv8ATY/+KWw+V5oDWGzSI77oI1+1Tl0PfiK/LCO18A5D7F/J6z/UZf6bH/xSt+g9 R/30f+CT/mrJfyzh81/J6z/UZf6bH/xbv0HqP++j/wAEn/NWP8s4fNfyes/1GX+mx/8AFu/Qeo/7 6P8AwSf81Y/yzh80nSaz/UZf6bH/AMW79B6j/vo/8En/ADVj/LOHzR+T1n+oy/02P/i3foPUf99H /gk/5qx/lnD5r+T1n+oy/wBNj/4t36D1H/fR/wCCT/mrH+WcPmv5PWf6jL/TY/8Ai3foPUf99H/g k/5qx/lnD5p/J6z/AFGX+mx/8W79B6j/AL6P/BJ/zVj/ACzh80fk9Z/qMv8ATY/+Ld+g9R/30fvT /mrH+WcPmv5PWf6jL/TY/wDilSHy/es3xgRjuWIP4LX9eUZe2o16UR7M7QymhCOId8pCR+UbT3T9 Ojt1McA5yH+8kP8AHw+Waacp5jcuTv8AQaDFookRPHll9UjzPv7h3BN4YViTiNyd2bxOWAVsHI62 eaocnHmGGX6T7nDGXMri+ke52RZuxV2KrJIYpPtqCR0PQ/eN8VrqpGyirszAeFR/EZDw49zMZJjr 9zvqUf8AO34f0weFHuXxZ9/3fqd9Sj/nb8P6Y+FHuXxZ9/3fqd9Sj/nb8P6Y+FHuXxZ9/wB36nfU o/52/D+mPhR7l8Wff936nfUo/wCdvw/pj4Ue5fFn3/d+p31KP+dvw/pj4Ue5fFn3/d+p31KP+dvw /pj4Ue5fFn3/AHfqd9Sj/nb8P6Y+FHuXxZ9/3fqd9Sj/AJ2/D+mPhR7l8Wff936nfUo/52/D+mPh R7l8Wff936nfUo/52/D+mPhR7l8Wff8Ad+p31KP+dvw/pj4Ue5fFn3/d+p31KP8Anb8P6Y+FHuXx Z9/3fqd9Sj/nb8P6Y+FHuXxZ9/3fqd9Sj/nb8P6Y+FHuXxZ9/wB36nfUo/52/D+mPhR7l8Wff936 nfUo/wCdvw/pj4Ue5fFn3/d+p31KP+dvw/pj4Ue5fFn3/d+p31KP+dvw/pj4Ue5fFn3/AHfqd9Sj /nb8P6Y+FHuXxZ9/3fqd9Sj/AJ2/D+mPhR7l8Wff936nfUo/52/D+mPhR7l8Wff936mxZQj7VW+Z /pTJCERyDGUpHmT+PcrKqqAqgKo6AbDJIApvFXHJR5hhl+k+5wxlzK4vpHudkWbsVdiqlc3KW8YZ gzsxCRRRqXkkdtlSNBuzMegGW4cMskuGLXlyxhHilyYL5k84+abK8a0exk0dwOSxXULLOVPRisoA 3/1c6TTdjYauR4/dy+x0ebtXIT6RwhJj5381gAm8IDbqTFFv22+DMv8AknTfzftP63H/AJRz/wA7 7B+pVj82+dZbaa6imke2tyguJ1gjMcZkJCB2CUXkQaV64D2Vprrh+0/rX+Uc/wDO+wfqUf8AHPmn /lt/5JQ/80Yf5J03837ZfrX+Uc/877B+pcnnvzQrAm7DgfstFFQ/coOA9kac/wAP2n9aR2lmHX7A yvy358g1CVLS+QW905Cxuv8Adu3hv9kntmm13ZBxAygeKP2h2mk7SGQ8MtpMszSuzdirsVdirsVd irsVdirsVdirsVdirsVdirsVdirsVdirsVdirjko8wwy/Sfc4Yy5lcX0j3OyLN2KuxViF95lt7L8 yNEa9lMOm6TdW81y1GalWDs/FQWNEp0GdP2Xpf8AB5SH1Tt5/tTMTk4ekUPo2reVbKLT9MvtQhvU 003dyLs25lt5DcGBVtUF1a3DKAsTvya2PxNQcd3zZzhM2QKuv077H9LrAQml75l8kXGpaeI7q1j0 nSl1CK0tWsVYsrX8klurNNaXQCPbSDiWR2Dg8gpYvlccWQA7GzXXy36jr+OibCHj8zeRLK11fQrR pf0brdzfs1xEzpDBDyK6cJIXhaSX0yiyDiwK8t6mqiXhZCRI84gftRYdqPmTyNdakI7dbGzszrF7 ItxHp0dVsFiiNmCGgk+B5VcEemxUVJQ/ZLHHkA3snhHXr16rYX3tz5JeHW9VtLKJ7OzSCTTpfQUR S39zZ+hPbnlDb8gkx+sKojVRwaiqGwRGTYE7nn7r/ATs8xBIIINCOhzPYPYfK2pvqOh21zIeU1Ck p7lkPGp+dK5w3aGAYs0ojl0+L1mjzHJiEjzTXMJyXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FX Yq7FXYq7FXHJR5hhl+k+5wxlzK4vpHudkWbsVdiryLzl/wApNf8A+uv/ABBc7fsz/F4/jq8tr/76 X46JLme4bsVdirsVbqaU7eGKtYq9S/Lz/lHE/wCMsn6xnH9tf3/wD0vZf9z8SyXNS7B2KuxV2Kux V2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxVxyUeYYZfpPucMZcyuL6R7nZFm7FXYq8l84QTN5lv mWNmUutCASPsLnZ9m5YjBEEj8F5HtHNAZ5AyHz8km+rXH++n/wCBOZ3jQ/nD5uF+Yx/zo/MO+rXH ++n/AOBOPjQ/nD5r+Yx/zo/MO+rXH++n/wCBOPjQ/nD5r+Yx/wA6PzDvq1x/vp/+BOPjQ/nD5r+Y x/zo/MO+rXH++n/4E4+ND+cPmv5jH/Oj8w76tcf76f8A4E4+ND+cPmv5jH/Oj8w9P/L9WXy6qsCp 9WTYih7ZyfbMgc9juD1PZMhLCCDe5ZJmqdk7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7 FXYq45KPMMMv0n3NsvFivgaYz5lGE+ge4NZFsdirsVeaeclne/ulgBMjTxCgZo6qXTn8a1ZRxrUj oM2GP6Q+XdpmH8pz464fP+p5pQv12G4ZWctEY4+EhVpApM0lUopDMQhUFz4AnrljTKGlzQBFRN8r jE9Nz99fJYs+sAqWReVbgNGytQHkphBdOVaJU8gKH7P2sdlOk0ZJHFtUd+Ied7fL79girWa5k4+t GErCjkryoXdmBA5BSKBQaEV3wF12rw4YRHASSSesTXL+bfP3onA4DsVdirN/J/8AxyP+ejfwzE1H 1D3Pqfsn/iQ/rSTvKHpHYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FW1XkwXxNPvyU OYa830H3FNfNVgbHzBewUAQyGSMDpwk+MAfKtMs1Eak4+gycWIeSU5S5jsVdirz3zKVi1W9kkPBE YFiQehA8M2eCHFGx0fMO1tBPUa7Lw16THn5gJWLiAuU5Gq0qeL8ak8acqUrXLvAPePt/U4X8g5++ PzP6lk19aQorySEI6GQOEcrxVeZqQpA+EVocTgI6j7U/yBn74/M/qXw3FvMaRyBjwSWlGHwSV4nc d+JwjTk9Qv8AIGfvj8z+pDnV9MEZkNyvp1ZQ1GoSrKjU23HJxuMj4PmEfyDn74/M/qRLTwKvIyCn IpUVPxCtRsP8nJHTmrsJ/kDP3x+Z/UpR39lJJDEsw9ScExKVdSeKhz1A/ZYHAMBJqwj+QM/fH5n9 TP8AygKaR/z1f+Ga/VCp13Pd+zGMw0vCecZyHyKd5jPQuxV2KuxV2KuxV2KuxV2KuxV2KuxV2Kux V2KuxV2KuxV2Kpt5VsDfeYLKGgZBIJJAenCP42r86Uy7TxuTh6/Jw4j57Mz/ADH0Jrm0j1SBeUtq OE4G5MRNQf8AYE/j7Zl6jHxCxzDq9BqOCdHkXm2a56B2KuxViXm6wdLoXqisUoCyHwddhX5jNl2d qBGVF5DteB0+rGc/3WQcMj3SHIn4MZ9dvrAgMLgFC3qkD0/hIHGoNa716ZvROzVFvBBW3F20BUeh JLypvGOQFWC7/fU+2M5gdCVJC63uGnVz6TxcXZKSDiTxNOQ61B7HGMr6EJFIa12u+S/WeMicQstf SX0+O/xCoZuXfrQ5DHz/AIvixFeaO3rl7Pal0aSSOEVeTMQABuST4ZXlyRhEyLTnyjHG+fcO89A9 B0qzNnYQwGnNRV6fzMan9ecnknxSJ73fdmaY4MEYy+rmfeTZ+9F5BznYq7FXYq7FXYq7FXYq7FXY q7FXYq7FXYq7FXYq7FXYq7FXpP5caE1taSapOvGW6HCAHYiIGpP+zI/D3zY6fHwizzLz+v1HHOhy DMmVWUqwDKwoyncEHMhwHl3nHydLpcrXlmpfTnNSBuYSf2W/yfA/QffBz4OodzotaCOCfwLFcxHb OxVZNDFNG0Uqh43FGU7g4tWbDDLAwmLiejHrvychYtaT8FPSOQVp8mG+Z+HtCcBR3Dz57Dy4tsOT 0fzZi69xFFB/4O1LtND97f0zJ/lU934+bD+Tdd/OxfKbv8Hal/v6H72/5px/lU934+a/ybrv52H5 Td/g7Uv9/Q/e/wDzTj/Kp7vx81/k3XfzsPymuj8m33L454gO9Ax/5pwHtU9B+Pmyj2ZrTzliHuEj +lO9L0CzsD6grLP/AL8ban+qO2a/PqJ5D6i7DR9kxxS8SZOTJ3nkPcOn3pnlDtnYq7FXYq7FXYq7 FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FWVeTvJ0uqSreXilNOQ1AOxmI/ZX/J8T9A9svBg6l1Wt1o A4IfEvUVVVUKoCqooqjYADM50reKtMqspVgGVhRlO4IOKsN138uLS5Zp9LkFrK25gepiJPgRUp+P 0Zj5NOJbjYufp9fOGx3DDL/yr5gsSfWspCiipkjHqJTxqlafTmJLTyDtMevxS60lTKy/aBHz2yvg Pc5HjQ7x82q48J7k+LHvDq48J7l8WPeHVx4T3L4se8OrjwnuXxY94dXHhPcvix7w6uPCe5fFj3h1 ceE9y+LHvDq48J7l8WPeHVx4T3L4se8OrjwnuXxY94dXHhPcvix7w6uPCe5fFj3h1ceE9y+LHvDq 48J7l8WPeHVx4T3L4se8OrjwnuXxY94dXHhPcvix7w6uPCe5fFj3h1ceE9y+LHvDq48J7l8WPeHV x4T3L4se8OrjwnuXxY94bVWb7IJ+W+PAe5HjQ7x801sPKvmC+I9GykCMKiSQemlPHk9K/RlkdPMu Pk1+KPW/czPQvy4tLZln1SQXUq7iBKiIEeJNC/4fTmXj04judy6vUa+c9hsGZKqqoVQFVRRVGwAG ZDgN4q//2Q== - - - - uuid:9ff39a0b-946a-d54d-a1ee-61af368ae3be - xmp.did:7860e3cf-d23b-4f98-983a-08d0219afee4 - uuid:5D20892493BFDB11914A8590D31508C8 - proof:pdf - - xmp.iid:67228eb4-f215-42ab-b511-cbcff6680847 - xmp.did:67228eb4-f215-42ab-b511-cbcff6680847 - uuid:5D20892493BFDB11914A8590D31508C8 - proof:pdf - - - - - saved - xmp.iid:0ef985b9-68c0-4dc0-9907-ac69d2efe765 - 2016-04-13T09:17:47+02:00 - Adobe Illustrator CC 2015 (Macintosh) - / - - - saved - xmp.iid:7860e3cf-d23b-4f98-983a-08d0219afee4 - 2016-04-13T10:29:44+02:00 - Adobe Illustrator CC 2015 (Macintosh) - / - - - - Print - Document - False - True - 1 - - 207.080556 - 210.001556 - Millimeters - - - - - MyriadPro-Regular - Myriad Pro - Regular - Open Type - Version 2.106;PS 2.000;hotconv 1.0.70;makeotf.lib2.5.58329 - False - MyriadPro-Regular.otf - - - - - - Cyan - Magenta - Yellow - Black - - - - - - Default Swatch Group - 0 - - - - White - CMYK - PROCESS - 0.000000 - 0.000000 - 0.000000 - 0.000000 - - - Black - CMYK - PROCESS - 0.000000 - 0.000000 - 0.000000 - 100.000000 - - - CMYK Red - CMYK - PROCESS - 0.000000 - 100.000000 - 100.000000 - 0.000000 - - - CMYK Yellow - CMYK - PROCESS - 0.000000 - 0.000000 - 100.000000 - 0.000000 - - - CMYK Green - CMYK - PROCESS - 100.000000 - 0.000000 - 100.000000 - 0.000000 - - - CMYK Cyan - CMYK - PROCESS - 100.000000 - 0.000000 - 0.000000 - 0.000000 - - - CMYK Blue - CMYK - PROCESS - 100.000000 - 100.000000 - 0.000000 - 0.000000 - - - CMYK Magenta - CMYK - PROCESS - 0.000000 - 100.000000 - 0.000000 - 0.000000 - - - C=15 M=100 Y=90 K=10 - CMYK - PROCESS - 15.000000 - 100.000000 - 90.000000 - 10.000000 - - - C=0 M=90 Y=85 K=0 - CMYK - PROCESS - 0.000000 - 90.000000 - 85.000000 - 0.000000 - - - C=0 M=80 Y=95 K=0 - CMYK - PROCESS - 0.000000 - 80.000000 - 95.000000 - 0.000000 - - - C=0 M=50 Y=100 K=0 - CMYK - PROCESS - 0.000000 - 50.000000 - 100.000000 - 0.000000 - - - C=0 M=35 Y=85 K=0 - CMYK - PROCESS - 0.000000 - 35.000000 - 85.000000 - 0.000000 - - - C=5 M=0 Y=90 K=0 - CMYK - PROCESS - 5.000000 - 0.000000 - 90.000000 - 0.000000 - - - C=20 M=0 Y=100 K=0 - CMYK - PROCESS - 20.000000 - 0.000000 - 100.000000 - 0.000000 - - - C=50 M=0 Y=100 K=0 - CMYK - PROCESS - 50.000000 - 0.000000 - 100.000000 - 0.000000 - - - C=75 M=0 Y=100 K=0 - CMYK - PROCESS - 75.000000 - 0.000000 - 100.000000 - 0.000000 - - - C=85 M=10 Y=100 K=10 - CMYK - PROCESS - 85.000000 - 10.000000 - 100.000000 - 10.000000 - - - C=90 M=30 Y=95 K=30 - CMYK - PROCESS - 90.000000 - 30.000000 - 95.000000 - 30.000000 - - - C=75 M=0 Y=75 K=0 - CMYK - PROCESS - 75.000000 - 0.000000 - 75.000000 - 0.000000 - - - C=80 M=10 Y=45 K=0 - CMYK - PROCESS - 80.000000 - 10.000000 - 45.000000 - 0.000000 - - - C=70 M=15 Y=0 K=0 - CMYK - PROCESS - 70.000000 - 15.000000 - 0.000000 - 0.000000 - - - C=85 M=50 Y=0 K=0 - CMYK - PROCESS - 85.000000 - 50.000000 - 0.000000 - 0.000000 - - - C=100 M=95 Y=5 K=0 - CMYK - PROCESS - 100.000000 - 95.000000 - 5.000000 - 0.000000 - - - C=100 M=100 Y=25 K=25 - CMYK - PROCESS - 100.000000 - 100.000000 - 25.000000 - 25.000000 - - - C=75 M=100 Y=0 K=0 - CMYK - PROCESS - 75.000000 - 100.000000 - 0.000000 - 0.000000 - - - C=50 M=100 Y=0 K=0 - CMYK - PROCESS - 50.000000 - 100.000000 - 0.000000 - 0.000000 - - - C=35 M=100 Y=35 K=10 - CMYK - PROCESS - 35.000000 - 100.000000 - 35.000000 - 10.000000 - - - C=10 M=100 Y=50 K=0 - CMYK - PROCESS - 10.000000 - 100.000000 - 50.000000 - 0.000000 - - - C=0 M=95 Y=20 K=0 - CMYK - PROCESS - 0.000000 - 95.000000 - 20.000000 - 0.000000 - - - C=25 M=25 Y=40 K=0 - CMYK - PROCESS - 25.000000 - 25.000000 - 40.000000 - 0.000000 - - - C=40 M=45 Y=50 K=5 - CMYK - PROCESS - 40.000000 - 45.000000 - 50.000000 - 5.000000 - - - C=50 M=50 Y=60 K=25 - CMYK - PROCESS - 50.000000 - 50.000000 - 60.000000 - 25.000000 - - - C=55 M=60 Y=65 K=40 - CMYK - PROCESS - 55.000000 - 60.000000 - 65.000000 - 40.000000 - - - C=25 M=40 Y=65 K=0 - CMYK - PROCESS - 25.000000 - 40.000000 - 65.000000 - 0.000000 - - - C=30 M=50 Y=75 K=10 - CMYK - PROCESS - 30.000000 - 50.000000 - 75.000000 - 10.000000 - - - C=35 M=60 Y=80 K=25 - CMYK - PROCESS - 35.000000 - 60.000000 - 80.000000 - 25.000000 - - - C=40 M=65 Y=90 K=35 - CMYK - PROCESS - 40.000000 - 65.000000 - 90.000000 - 35.000000 - - - C=40 M=70 Y=100 K=50 - CMYK - PROCESS - 40.000000 - 70.000000 - 100.000000 - 50.000000 - - - C=50 M=70 Y=80 K=70 - CMYK - PROCESS - 50.000000 - 70.000000 - 80.000000 - 70.000000 - - - - - - Grays - 1 - - - - C=0 M=0 Y=0 K=100 - CMYK - PROCESS - 0.000000 - 0.000000 - 0.000000 - 100.000000 - - - C=0 M=0 Y=0 K=90 - CMYK - PROCESS - 0.000000 - 0.000000 - 0.000000 - 89.999400 - - - C=0 M=0 Y=0 K=80 - CMYK - PROCESS - 0.000000 - 0.000000 - 0.000000 - 79.998800 - - - C=0 M=0 Y=0 K=70 - CMYK - PROCESS - 0.000000 - 0.000000 - 0.000000 - 69.999700 - - - C=0 M=0 Y=0 K=60 - CMYK - PROCESS - 0.000000 - 0.000000 - 0.000000 - 59.999100 - - - C=0 M=0 Y=0 K=50 - CMYK - PROCESS - 0.000000 - 0.000000 - 0.000000 - 50.000000 - - - C=0 M=0 Y=0 K=40 - CMYK - PROCESS - 0.000000 - 0.000000 - 0.000000 - 39.999400 - - - C=0 M=0 Y=0 K=30 - CMYK - PROCESS - 0.000000 - 0.000000 - 0.000000 - 29.998800 - - - C=0 M=0 Y=0 K=20 - CMYK - PROCESS - 0.000000 - 0.000000 - 0.000000 - 19.999700 - - - C=0 M=0 Y=0 K=10 - CMYK - PROCESS - 0.000000 - 0.000000 - 0.000000 - 9.999100 - - - C=0 M=0 Y=0 K=5 - CMYK - PROCESS - 0.000000 - 0.000000 - 0.000000 - 4.998800 - - - - - - Brights - 1 - - - - C=0 M=100 Y=100 K=0 - CMYK - PROCESS - 0.000000 - 100.000000 - 100.000000 - 0.000000 - - - C=0 M=75 Y=100 K=0 - CMYK - PROCESS - 0.000000 - 75.000000 - 100.000000 - 0.000000 - - - C=0 M=10 Y=95 K=0 - CMYK - PROCESS - 0.000000 - 10.000000 - 95.000000 - 0.000000 - - - C=85 M=10 Y=100 K=0 - CMYK - PROCESS - 85.000000 - 10.000000 - 100.000000 - 0.000000 - - - C=100 M=90 Y=0 K=0 - CMYK - PROCESS - 100.000000 - 90.000000 - 0.000000 - 0.000000 - - - C=60 M=90 Y=0 K=0 - CMYK - PROCESS - 60.000000 - 90.000000 - 0.003100 - 0.003100 - - - - - - - Adobe PDF library 10.01 - - - - - - - - - - - - - - - - - - - - - - - - - endstream endobj 3 0 obj <> endobj 8 0 obj <>/Resources<>/ExtGState<>/Font<>/ProcSet[/PDF/Text]/Properties<>/XObject<>>>/Thumb 324 0 R/TrimBox[0.0 0.0 587.0 595.28]/Type/Page>> endobj 271 0 obj <>stream -HQO0)19MEii -2hYMc~g'03z7Xo8] Yh0hR<9\w*)t҂=|GV_Kҗ lYBu|L. ] 7'"R|&ՃI^ T>މA2Ldby&H݉1&E+ *ZG]vjai- 9p^^v.%vB|fG(bMA%J*9`KWJƂy7e},?HOʧYPQ'nzv,H[f}IH@UTI)ؕz\/zQͼ{(IjEK)OC@s hQ9 4ǁ@ Cp( PNy+ iyI _`0 p0`8 `@| ښgGP/(=q( r( r( r(X;DaVC',#n^Aټ'YS/ QRC3#]y,[rF(v)2Ү{gfU2 =6! -" Ě~*Lؠv紅I}\Dt:,o[⁂ <\GZfrWУTm=Q4e#.ncڇ4\\7E_C`;d.Ǧs8]V/`ų֣p}z}- -?J endstream endobj 272 0 obj <> endobj 324 0 obj <>stream -8;XELZ'D@D&6.l3&X;3iXlLHAAY9/PhoY?'#pcE8N$B5a_A1GChd$PM"Qr/OkC%t. -Se7`+(sCt7q]A`SPD]#Yp]9TM>Z880kW43blM9XIGX+ph"Ru8k@&enkcOS87+G^VL -!nbb6,%\@+Zg/YT3q6a2^ptN5g%.:%H$m3$@!-u;;.&\AMCDQ+4$STjetXo0l7HZ&cVF9=epD?5&9oRjmoj -Y7]BA50Iq.NsAYN+OA9J[PP!WOcT&7+>:C=ir-Oq8IX[6/0_(>AQ=V8PTT9sQ65^W -JLg=+)MsS=GVeB*p\Z7*3O8,d.tZCKj)J*Knki`YZLO$['TjKbp`R%nPW,L0_$LY^ -@%SjpLlC&0kO.];4rOQB+3K5W0%Q)Qd67?K<'9;Jj.>Zjq/mb-#KSY4I5m8W85PjMkCMXq+X]KBqe$in&/s+iXq6uE[dEU#>A0(;Ps]gA5Z79FC=TD>W6 -+)(ae6Q@p0M2$&l[*Y885]Br=?E*VB[><&u&ae4-ArpIe6iFI/Y;M9Y[3!iGs5S9= -]N;942D7AOLf"IN@;>O\9O?+C?h*Rm[L.@<.0$+p5<=W!c.q"CUARM:W846\u!Y>LhCWJi_M)H0WC(e"b!b%P*qSCpL6 -C4DGp]M_?m9ZFX"p(:lE!8cCLC;9/2pfA`g8,3!07SsdG1;\n'8hm[FP`Q0AiZju6 -A)reTBu+MR_MBP-1U endstream endobj 326 0 obj [/Indexed/DeviceRGB 255 327 0 R] endobj 327 0 obj <>stream -8;X]O>EqN@%''O_@%e@?J;%+8(9e>X=MR6S?i^YgA3=].HDXF.R$lIL@"pJ+EP(%0 -b]6ajmNZn*!='OQZeQ^Y*,=]?C.B+\Ulg9dhD*"iC[;*=3`oP1[!S^)?1)IZ4dup` -E1r!/,*0[*9.aFIR2&b-C#soRZ7Dl%MLY\.?d>Mn -6%Q2oYfNRF$$+ON<+]RUJmC0InDZ4OTs0S!saG>GGKUlQ*Q?45:CI&4J'_2j$XKrcYp0n+Xl_nU*O( -l[$6Nn+Z_Nq0]s7hs]`XX1nZ8&94a\~> endstream endobj 280 0 obj <>/ExtGState<>>>/Subtype/Form>>stream -/CS0 cs 0.15 1 0.9 0.1 scn -/GS0 gs -q 1 0 0 1 494.6779 15.5627 cm -0 0 m --394.076 0 l --441.042 0 -479.115 38.073 -479.115 85.039 c --479.115 479.115 l --479.115 526.081 -441.042 564.155 -394.076 564.155 c -0 564.155 l -46.966 564.155 85.039 526.081 85.039 479.115 c -85.039 85.039 l -85.039 38.073 46.966 0 0 0 c -f -Q -/CS0 CS 0 0 0 1 SCN -1 w 10 M 0 j 0 J []0 d -q 1 0 0 1 494.6779 15.5627 cm -0 0 m --394.076 0 l --441.042 0 -479.115 38.073 -479.115 85.039 c --479.115 479.115 l --479.115 526.081 -441.042 564.155 -394.076 564.155 c -0 564.155 l -46.966 564.155 85.039 526.081 85.039 479.115 c -85.039 85.039 l -85.039 38.073 46.966 0 0 0 c -h -S -Q - endstream endobj 281 0 obj <>/ExtGState<>>>/Subtype/Form>>stream -/CS0 cs 0 0.8 0.95 0 scn -/GS0 gs -q 1 0 0 1 507.8084 186.3477 cm -0 0 m --391.31 0 l --421.055 0 -445.168 24.113 -445.168 53.858 c --445.168 278.517 l --445.168 308.262 -421.055 332.375 -391.31 332.375 c -0 332.375 l -29.745 332.375 53.858 308.262 53.858 278.517 c -53.858 53.858 l -53.858 24.113 29.745 0 0 0 c -f -Q -/CS0 CS 0 0 0 1 SCN -1 w 10 M 0 j 0 J []0 d -q 1 0 0 1 507.8084 186.3477 cm -0 0 m --391.31 0 l --421.055 0 -445.168 24.113 -445.168 53.858 c --445.168 278.517 l --445.168 308.262 -421.055 332.375 -391.31 332.375 c -0 332.375 l -29.745 332.375 53.858 308.262 53.858 278.517 c -53.858 53.858 l -53.858 24.113 29.745 0 0 0 c -h -S -Q - endstream endobj 282 0 obj <>/ExtGState<>>>/Subtype/Form>>stream -/CS0 cs 0.6 0.9 0 0 scn -/CS0 CS 0.6 0.9 0 0 SCN -0.25 w 10 M 0 j 0 J []0 d -/GS0 gs -184.462 341.951 -29.167 15.081 re -B - endstream endobj 283 0 obj <>/ExtGState<>>>/Subtype/Form>>stream -/CS0 cs 0.6 0.9 0 0 scn -/GS0 gs -q 1 0 0 1 184.4618 341.9514 cm -0 0 m -0 0 l -0 15.081 l -25.407 15.081 l -19.618 3.036 7.18 0 0 0 c -f -Q -/CS0 CS 0.6 0.9 0 0 SCN -0.25 w 10 M 0 j 0 J []0 d -q 1 0 0 1 184.4618 341.9514 cm -0 0 m -0 0 l -0 15.081 l -25.407 15.081 l -19.618 3.036 7.18 0 0 0 c -h -S -Q - endstream endobj 284 0 obj <>/ExtGState<>>>/Subtype/Form>>stream -/CS0 cs 1 0.95 0.05 0 scn -/GS0 gs -q 1 0 0 1 180.683 196.4455 cm -0 0 m --56.693 0 l --75.479 0 -90.709 15.229 -90.709 34.016 c --90.709 94.965 l --90.709 113.752 -75.479 128.981 -56.693 128.981 c -0 128.981 l -18.786 128.981 34.016 113.752 34.016 94.965 c -34.016 34.016 l -34.016 15.229 18.786 0 0 0 c -f -Q - endstream endobj 285 0 obj <>/ExtGState<>>>/Subtype/Form>>stream -/CS0 cs 0.85 0.1 1 0 scn -/GS0 gs -q 1 0 0 1 184.0939 196.4455 cm -0 0 m --63.515 0 l --80.417 0 -94.12 14.17 -94.12 31.649 c --94.12 32.841 l --94.12 64.49 l -30.605 64.49 l -30.605 32.841 l -30.605 31.649 l -30.605 14.17 16.903 0 0 0 c -f -Q - endstream endobj 286 0 obj <>/ExtGState<>>>/Subtype/Form>>stream -/CS0 cs 0.6 0.9 0 0 scn -/GS0 gs -q 1 0 0 1 99.0266 215.0324 cm -0 0 m -24.974 0 l -24.974 15.081 l --3.695 15.081 l --3.695 5.025 0 0 y -f -Q -/CS0 CS 0.6 0.9 0 0 SCN -0.25 w 10 M 0 j 0 J []0 d -q 1 0 0 1 99.0266 215.0324 cm -0 0 m -24.974 0 l -24.974 15.081 l --3.695 15.081 l --3.695 5.025 0 0 y -h -S -Q - endstream endobj 287 0 obj <>/ExtGState<>>>/Subtype/Form>>stream -/CS0 cs 0.6 0.9 0 0 scn -/CS0 CS 0.6 0.9 0 0 SCN -0.25 w 10 M 0 j 0 J []0 d -/GS0 gs -153.167 215.032 -29.167 15.081 re -B - endstream endobj 288 0 obj <>/ExtGState<>>>/Subtype/Form>>stream -/CS0 cs 0.6 0.9 0 0 scn -/CS0 CS 0.6 0.9 0 0 SCN -0.25 w 10 M 0 j 0 J []0 d -/GS0 gs -182.334 215.032 -29.167 15.081 re -B - endstream endobj 289 0 obj <>/ExtGState<>>>/Subtype/Form>>stream -/CS0 cs 0.6 0.9 0 0 scn -/GS0 gs -q 1 0 0 1 207.7409 215.0324 cm -0 0 m --25.407 0 l --25.407 15.081 l -3.759 15.081 l -3.759 5.025 0 0 y -f -Q -/CS0 CS 0.6 0.9 0 0 SCN -0.25 w 10 M 0 j 0 J []0 d -q 1 0 0 1 207.7409 215.0324 cm -0 0 m --25.407 0 l --25.407 15.081 l -3.759 15.081 l -3.759 5.025 0 0 y -h -S -Q - endstream endobj 290 0 obj <>/ExtGState<>>>/Subtype/Form>>stream -/CS0 cs 0.6 0.9 0 0 scn -/GS0 gs -q 1 0 0 1 124.0005 199.9514 cm -0 0 m -0 0 l -0 15.081 l --24.91 15.081 l --19.234 3.036 -7.04 0 0 0 c -f -Q -/CS0 CS 0.6 0.9 0 0 SCN -0.25 w 10 M 0 j 0 J []0 d -q 1 0 0 1 124.0005 199.9514 cm -0 0 m -0 0 l -0 15.081 l --24.91 15.081 l --19.234 3.036 -7.04 0 0 0 c -h -S -Q - endstream endobj 291 0 obj <>/ExtGState<>>>/Subtype/Form>>stream -/CS0 cs 0.6 0.9 0 0 scn -/CS0 CS 0.6 0.9 0 0 SCN -0.25 w 10 M 0 j 0 J []0 d -/GS0 gs -153.167 199.951 -29.167 15.081 re -B - endstream endobj 292 0 obj <>/ExtGState<>>>/Subtype/Form>>stream -/CS0 cs 1 0.95 0.05 0 scn -/GS0 gs -q 1 0 0 1 182.8113 338.4455 cm -0 0 m --56.693 0 l --75.479 0 -90.709 15.229 -90.709 34.016 c --90.709 94.965 l --90.709 113.752 -75.479 128.981 -56.693 128.981 c -0 128.981 l -18.786 128.981 34.016 113.752 34.016 94.965 c -34.016 34.016 l -34.016 15.229 18.786 0 0 0 c -f -Q - endstream endobj 293 0 obj <>/ExtGState<>>>/Subtype/Form>>stream -/CS0 cs 0.6 0.9 0 0 scn -/CS0 CS 0.6 0.9 0 0 SCN -0.25 w 10 M 0 j 0 J []0 d -/GS0 gs -182.334 199.951 -29.167 15.081 re -B - endstream endobj 294 0 obj <>/ExtGState<>>>/Subtype/Form>>stream -/CS0 cs 0.6 0.9 0 0 scn -/GS0 gs -q 1 0 0 1 182.3336 199.9514 cm -0 0 m -0 0 l -0 15.081 l -25.407 15.081 l -19.618 3.036 7.18 0 0 0 c -f -Q -/CS0 CS 0.6 0.9 0 0 SCN -0.25 w 10 M 0 j 0 J []0 d -q 1 0 0 1 182.3336 199.9514 cm -0 0 m -0 0 l -0 15.081 l -25.407 15.081 l -19.618 3.036 7.18 0 0 0 c -h -S -Q - endstream endobj 295 0 obj <>/ExtGState<>>>/Subtype/Form>>stream -/CS0 cs 1 0.95 0.05 0 scn -/GS0 gs -q 1 0 0 1 180.683 40.4455 cm -0 0 m --56.693 0 l --75.479 0 -90.709 15.229 -90.709 34.016 c --90.709 94.965 l --90.709 113.752 -75.479 128.981 -56.693 128.981 c -0 128.981 l -18.786 128.981 34.016 113.752 34.016 94.965 c -34.016 34.016 l -34.016 15.229 18.786 0 0 0 c -f -Q - endstream endobj 296 0 obj <>/ExtGState<>>>/Subtype/Form>>stream -/CS0 cs 0.85 0.1 1 0 scn -/GS0 gs -q 1 0 0 1 184.0939 40.4455 cm -0 0 m --63.515 0 l --80.417 0 -94.12 14.17 -94.12 31.649 c --94.12 32.841 l --94.12 64.49 l -30.605 64.49 l -30.605 32.841 l -30.605 31.649 l -30.605 14.17 16.903 0 0 0 c -f -Q - endstream endobj 297 0 obj <>/ExtGState<>>>/Subtype/Form>>stream -/CS0 cs 0.6 0.9 0 0 scn -/GS0 gs -q 1 0 0 1 99.0266 59.0324 cm -0 0 m -24.974 0 l -24.974 15.081 l --3.695 15.081 l --3.695 5.025 0 0 y -f -Q -/CS0 CS 0.6 0.9 0 0 SCN -0.25 w 10 M 0 j 0 J []0 d -q 1 0 0 1 99.0266 59.0324 cm -0 0 m -24.974 0 l -24.974 15.081 l --3.695 15.081 l --3.695 5.025 0 0 y -h -S -Q - endstream endobj 298 0 obj <>/ExtGState<>>>/Subtype/Form>>stream -/CS0 cs 0.6 0.9 0 0 scn -/CS0 CS 0.6 0.9 0 0 SCN -0.25 w 10 M 0 j 0 J []0 d -/GS0 gs -153.167 59.032 -29.167 15.081 re -B - endstream endobj 299 0 obj <>/ExtGState<>>>/Subtype/Form>>stream -/CS0 cs 0.6 0.9 0 0 scn -/CS0 CS 0.6 0.9 0 0 SCN -0.25 w 10 M 0 j 0 J []0 d -/GS0 gs -182.334 59.032 -29.167 15.081 re -B - endstream endobj 300 0 obj <>/ExtGState<>>>/Subtype/Form>>stream -/CS0 cs 0.6 0.9 0 0 scn -/GS0 gs -q 1 0 0 1 207.7409 59.0324 cm -0 0 m --25.407 0 l --25.407 15.081 l -3.759 15.081 l -3.759 5.025 0 0 y -f -Q -/CS0 CS 0.6 0.9 0 0 SCN -0.25 w 10 M 0 j 0 J []0 d -q 1 0 0 1 207.7409 59.0324 cm -0 0 m --25.407 0 l --25.407 15.081 l -3.759 15.081 l -3.759 5.025 0 0 y -h -S -Q - endstream endobj 301 0 obj <>/ExtGState<>>>/Subtype/Form>>stream -/CS0 cs 0.6 0.9 0 0 scn -/GS0 gs -q 1 0 0 1 124.0005 43.9514 cm -0 0 m -0 0 l -0 15.081 l --24.91 15.081 l --19.234 3.036 -7.04 0 0 0 c -f -Q -/CS0 CS 0.6 0.9 0 0 SCN -0.25 w 10 M 0 j 0 J []0 d -q 1 0 0 1 124.0005 43.9514 cm -0 0 m -0 0 l -0 15.081 l --24.91 15.081 l --19.234 3.036 -7.04 0 0 0 c -h -S -Q - endstream endobj 302 0 obj <>/ExtGState<>>>/Subtype/Form>>stream -/CS0 cs 0.6 0.9 0 0 scn -/CS0 CS 0.6 0.9 0 0 SCN -0.25 w 10 M 0 j 0 J []0 d -/GS0 gs -153.167 43.951 -29.167 15.081 re -B - endstream endobj 303 0 obj <>/ExtGState<>>>/Subtype/Form>>stream -/CS0 cs 0.85 0.1 1 0 scn -/GS0 gs -q 1 0 0 1 186.2222 338.4455 cm -0 0 m --63.515 0 l --80.417 0 -94.12 14.17 -94.12 31.649 c --94.12 32.841 l --94.12 64.49 l -30.605 64.49 l -30.605 32.841 l -30.605 31.649 l -30.605 14.17 16.903 0 0 0 c -f -Q - endstream endobj 304 0 obj <>/ExtGState<>>>/Subtype/Form>>stream -/CS0 cs 0.6 0.9 0 0 scn -/CS0 CS 0.6 0.9 0 0 SCN -0.25 w 10 M 0 j 0 J []0 d -/GS0 gs -182.334 43.951 -29.167 15.081 re -B - endstream endobj 305 0 obj <>/ExtGState<>>>/Subtype/Form>>stream -/CS0 cs 0.6 0.9 0 0 scn -/GS0 gs -q 1 0 0 1 182.3336 43.9514 cm -0 0 m -0 0 l -0 15.081 l -25.407 15.081 l -19.618 3.036 7.18 0 0 0 c -f -Q -/CS0 CS 0.6 0.9 0 0 SCN -0.25 w 10 M 0 j 0 J []0 d -q 1 0 0 1 182.3336 43.9514 cm -0 0 m -0 0 l -0 15.081 l -25.407 15.081 l -19.618 3.036 7.18 0 0 0 c -h -S -Q - endstream endobj 306 0 obj <>/ExtGState<>>>/Subtype/Form>>stream -/CS0 cs 0.859 0.598 0.175 0.021 scn -/GS0 gs -q 1 0 0 1 510.2792 196.4455 cm -0 0 m --244.803 0 l --263.59 0 -278.819 15.229 -278.819 34.016 c --278.819 236.965 l --278.819 255.752 -263.59 270.981 -244.803 270.981 c -0 270.981 l -18.786 270.981 34.016 255.752 34.016 236.965 c -34.016 34.016 l -34.016 15.229 18.786 0 0 0 c -f -Q - endstream endobj 307 0 obj <>/ExtGState<>>>/Subtype/Form>>stream -/CS0 cs 0.85 0.1 1 0 scn -/GS0 gs -q 1 0 0 1 510.2792 196.4455 cm -0 0 m --244.803 0 l --270.838 0.459 -278.819 23.459 -278.819 31.649 c --278.819 32.841 l --278.819 64.49 l -34.016 64.49 l -34.016 32.841 l -34.016 31.649 l -34.016 24.834 26.846 0 0 0 c -f -Q - endstream endobj 308 0 obj <>/ExtGState<>>>/Subtype/Form>>stream -/CS0 cs 0.6 0.9 0 0 scn -/GS0 gs -q 1 0 0 1 239.8333 215.0324 cm -0 0 m -71.25 0 l -71.25 15.081 l --4.667 15.081 l --4.667 5.025 0 0 y -f -Q -/CS0 CS 0.6 0.9 0 0 SCN -0.25 w 10 M 0 j 0 J []0 d -q 1 0 0 1 239.8333 215.0324 cm -0 0 m -71.25 0 l -71.25 15.081 l --4.667 15.081 l --4.667 5.025 0 0 y -h -S -Q - endstream endobj 309 0 obj <>/ExtGState<>>>/Subtype/Form>>stream -/CS0 cs 0.6 0.9 0 0 scn -/CS0 CS 0.6 0.9 0 0 SCN -0.25 w 10 M 0 j 0 J []0 d -/GS0 gs -386.639 215.032 -75.556 15.081 re -B - endstream endobj 310 0 obj <>/ExtGState<>>>/Subtype/Form>>stream -/CS0 cs 0.6 0.9 0 0 scn -/CS0 CS 0.6 0.9 0 0 SCN -0.25 w 10 M 0 j 0 J []0 d -/GS0 gs -462.194 215.032 -75.556 15.081 re -B - endstream endobj 311 0 obj <>/ExtGState<>>>/Subtype/Form>>stream -/CS0 cs 0.6 0.9 0 0 scn -/GS0 gs -q 1 0 0 1 536.4408 215.0323 cm -0 0 m --74.246 0 l --74.246 15.081 l -4.059 15.081 l -4.059 5.026 0 0 y -f -Q -/CS0 CS 0.6 0.9 0 0 SCN -0.25 w 10 M 0 j 0 J []0 d -q 1 0 0 1 536.4408 215.0323 cm -0 0 m --74.246 0 l --74.246 15.081 l -4.059 15.081 l -4.059 5.026 0 0 y -h -S -Q - endstream endobj 312 0 obj <>/ExtGState<>>>/Subtype/Form>>stream -/CS0 cs 0.6 0.9 0 0 scn -/GS0 gs -q 1 0 0 1 269.4117 200.3633 cm -0 0 m -41.672 -0.412 l -41.672 14.669 l --29.722 14.669 l --26.7 11 -20.176 0 0 0 c -f -Q -/CS0 CS 0.6 0.9 0 0 SCN -0.25 w 10 M 0 j 0 J []0 d -q 1 0 0 1 269.4117 200.3633 cm -0 0 m -41.672 -0.412 l -41.672 14.669 l --29.722 14.669 l --26.7 11 -20.176 0 0 0 c -h -S -Q - endstream endobj 313 0 obj <>/ExtGState<>>>/Subtype/Form>>stream -/CS0 cs 0.6 0.9 0 0 scn -/CS0 CS 0.6 0.9 0 0 SCN -0.25 w 10 M 0 j 0 J []0 d -/GS0 gs -386.639 199.951 -75.556 15.081 re -B - endstream endobj 314 0 obj <>/ExtGState<>>>/Subtype/Form>>stream -/CS0 cs 0.6 0.9 0 0 scn -/GS0 gs -q 1 0 0 1 101.1549 357.0324 cm -0 0 m -24.974 0 l -24.974 15.081 l --3.695 15.081 l --3.695 5.026 0 0 y -f -Q -/CS0 CS 0.6 0.9 0 0 SCN -0.25 w 10 M 0 j 0 J []0 d -q 1 0 0 1 101.1549 357.0324 cm -0 0 m -24.974 0 l -24.974 15.081 l --3.695 15.081 l --3.695 5.026 0 0 y -h -S -Q - endstream endobj 315 0 obj <>/ExtGState<>>>/Subtype/Form>>stream -/CS0 cs 0.6 0.9 0 0 scn -/CS0 CS 0.6 0.9 0 0 SCN -0.25 w 10 M 0 j 0 J []0 d -/GS0 gs -462.194 199.951 -75.556 15.081 re -B - endstream endobj 316 0 obj <>/ExtGState<>>>/Subtype/Form>>stream -/CS0 cs 0.6 0.9 0 0 scn -/GS0 gs -q 1 0 0 1 506.5 199.9514 cm -0 0 m --44.305 0 l --44.305 15.081 l -30 15.081 l -27.846 12.528 18.843 0 0 0 c -f -Q -/CS0 CS 0.6 0.9 0 0 SCN -0.25 w 10 M 0 j 0 J []0 d -q 1 0 0 1 506.5 199.9514 cm -0 0 m --44.305 0 l --44.305 15.081 l -30 15.081 l -27.846 12.528 18.843 0 0 0 c -h -S -Q - endstream endobj 317 0 obj <>/ExtGState<>>>/Subtype/Form>>stream -/CS0 cs 1 0.95 0.05 0 scn -/GS0 gs -q 1 0 0 1 333.683 292.5419 cm -0 0 m --56.693 0 l --75.479 0 -90.709 15.229 -90.709 34.016 c --90.709 94.965 l --90.709 113.752 -75.479 128.981 -56.693 128.981 c -0 128.981 l -18.786 128.981 34.016 113.752 34.016 94.965 c -34.016 34.016 l -34.016 15.229 18.786 0 0 0 c -f -Q - endstream endobj 318 0 obj <>/ExtGState<>>>/Subtype/Form>>stream -/CS0 cs 1 0.95 0.05 0 scn -/GS0 gs -q 1 0 0 1 470.0164 290.64 cm -0 0 m --56.693 0 l --75.479 0 -90.709 15.229 -90.709 34.016 c --90.709 94.965 l --90.709 113.752 -75.479 128.981 -56.693 128.981 c -0 128.981 l -18.786 128.981 34.016 113.752 34.016 94.965 c -34.016 34.016 l -34.016 15.229 18.786 0 0 0 c -f -Q - endstream endobj 319 0 obj <>/ExtGState<>>>/Subtype/Form>>stream -/CS0 cs 0.6 0.9 0 0 scn -/CS0 CS 0.6 0.9 0 0 SCN -0.25 w 10 M 0 j 0 J []0 d -/GS0 gs -155.295 357.032 -29.167 15.081 re -B - endstream endobj 320 0 obj <>/ExtGState<>>>/Subtype/Form>>stream -/CS0 cs 0.6 0.9 0 0 scn -/CS0 CS 0.6 0.9 0 0 SCN -0.25 w 10 M 0 j 0 J []0 d -/GS0 gs -184.462 357.032 -29.167 15.081 re -B - endstream endobj 321 0 obj <>/ExtGState<>>>/Subtype/Form>>stream -/CS0 cs 0.6 0.9 0 0 scn -/GS0 gs -q 1 0 0 1 209.8692 357.0324 cm -0 0 m --25.407 0 l --25.407 15.081 l -3.759 15.081 l -3.759 5.026 0 0 y -f -Q -/CS0 CS 0.6 0.9 0 0 SCN -0.25 w 10 M 0 j 0 J []0 d -q 1 0 0 1 209.8692 357.0324 cm -0 0 m --25.407 0 l --25.407 15.081 l -3.759 15.081 l -3.759 5.026 0 0 y -h -S -Q - endstream endobj 322 0 obj <>/ExtGState<>>>/Subtype/Form>>stream -/CS0 cs 0.6 0.9 0 0 scn -/GS0 gs -q 1 0 0 1 126.1288 341.9514 cm -0 0 m -0 0 l -0 15.081 l --24.91 15.081 l --19.234 3.036 -7.04 0 0 0 c -f -Q -/CS0 CS 0.6 0.9 0 0 SCN -0.25 w 10 M 0 j 0 J []0 d -q 1 0 0 1 126.1288 341.9514 cm -0 0 m -0 0 l -0 15.081 l --24.91 15.081 l --19.234 3.036 -7.04 0 0 0 c -h -S -Q - endstream endobj 323 0 obj <>/ExtGState<>>>/Subtype/Form>>stream -/CS0 cs 0.6 0.9 0 0 scn -/CS0 CS 0.6 0.9 0 0 SCN -0.25 w 10 M 0 j 0 J []0 d -/GS0 gs -155.295 341.951 -29.167 15.081 re -B - endstream endobj 371 0 obj <> endobj 276 0 obj <> endobj 274 0 obj [/ICCBased 372 0 R] endobj 372 0 obj <>stream -HuTKtKKJI,t(݋4K%ҹH4J#Ғ(H -wqyy~3̙g<3Y9El -@ ]!O-@\+BVKK :OX~WCaiHKL0qY `5ck -X]x= 8 XĿ׽>.f#aPn D^{y8  dp H st:Y׬cxc IV?S!:_9[YbQP~+rA -ShHht^ '0߅™kYXY9Yqqpl'WzEE$%D>,^|t*K)%/`\ҫ:&D [7dplDa5|mb4,yy{e5 3⚅,t+whlA   m k -xYUH&%Ȥ -qO'Mz3KT@v[NUnn^\o]abTrtlmE]e~U+jאZ:zaqi5};CS[\_ۆwCaQ1;>L$Lz}4:%8M7l̎Χ/}XT^]X>\Ym[n!ycskkƶʷ;v{pIs0Xݯ3s󝋒&$WWW*)!$$%!e$cHNOAKIMEq ƕ;KLw@YX;ؚ8^+DspfKOTCPpJ%D=++O%$*8IZ\Z^UK_wL"dx]}>9=;s_G8/̹N!Gz[<=2|B}PQzlH0Wc(Een|Pds::5&89yFT"od䳔i/ZK^&gd:fgQl kJХeJ*+篍kj5U[ZUh0|em6]B@`PpH?QM1Msψ*iϛ.Z [JYZ)X-]R޸Ѻپw?@?5 ǖ'vNg -W3gLC#u!MMMEvAms˔FVNA̝GLwA̬,llؿsݛnͽ+!B²" 'R&k?3?4+:6oT\ұڿ6VʝoF?LT;:>::>:;eqvx^sawݥʕ'_EFO\DKLtAnFF)F|ԭ6\`@z?m+F;LwiAhy͖)Mgw~_ @ZH_XA,"F)%/*9aZ:Q,\B^_AU񡒀2 -*'[j o5[uR1uh`fm$1xJgBdrltlyyEe$feg-g#`dGbwj0TOC9; ܨݿxz6zx8IP=A!.aAxۑϊ}bG-ޒēx`G/Ԝq_O?0"۬խЮ˯ǰı²µŶȷ͸ӹۺ 0@RfzƏǦȾ *GcЀџҿ'LsٛFsM6+1MZ:{T?~ò~i~L}~cbA~Dad~ty~W~O>~\/~|~`Cx}%H}1X}%z}K} {N}׋<_~7A~-ψ||Dz|+E|[s|z} ^}wO@}-~ċ {Gu{Dz{]Ĭ{f{Zx|[]|ϕM?}R<}Ǝz]YzHħz|z={LNw{\|=>|v|ېI8z/r z;bz'sMzd6zɬqv{D[{0> |;|yyaIy?yazYvzݮ[{^=c{ФI{R*y߄yfUy`VyyuKzZi{ <{z%zȎ~+~}͇}W0}3}HtЄ}Zk}=~zɇ}!~Єd*s}Y<9wpSwuuVrUW؈|;,뇔{RsѲ;:8q)PCV:4.8Ȅ2񡂡?Up Vu9S c bփR.ՁNn U388A/ͬδz6߆өn1T\e7݀tXT)$̯̕6;eCʷˆ imw3SƀV7M -\lGNػځNāa5tNzlߴS<H6*-N}o2ن N%է>w֣A}⇤\fXMݘ2, KԐ3g°[} -0e6M _1 ? 1ӣǾI^I|B̯dܪwLe1$: rW] 1S{z|diL g0\ U{[G{!{ ޔ`{&yE{xbie{Jr|/c5}~ -~:f#MKx+Ca|uI~.yW ώәߎ%¡唘[w!^T`^H*- 5GȨ瘎=Π4rv_ҍRGf,ދ̋|,ƕ{ Ҙtٕ^1Fő,;',#h%T,Qۥ{[s:9󅼓&^!Փa@!" y -.Jl6mHju,bU6+s hܸd-ʥ}wi-sun=0Ľi-_*)U_ˈb$na+;ϧT;ppA7C4.*Iߥa8Mm.ACi7\j|fiԫ)]ޭjʄU]3(í whJch-4x7h׿*P0H됎L랇ڡuÂ,{Bz}8vggҲd[!XTZZ.vlAg -{;Sm`vؿ`~?ga. -3Ì{L^WYe4]L7ok!wI~Ira^=C#Zh`Wu}p)"z7ff&3$FJ8Ҷ5m -uR_,^VS&aR~PfLL_Dw*`\-9]q  TI6)>u6 D`e͢/xqY%9ʜ;åOd\˾P&eRz;].R<oΡ]P{?: r̨\ʻb Ҥ3|m s؟W9oZt]RnÅ\cW#+nI&gyAjsN06HiD'@J+a5V~cRI̫vwtUc[3+?F|l(iU^+O?Rs1Hqil$Wþh=(RE -1BvџnF/ BsGMY9>ܖ3ȗqI ڣ5V_1ȣβiJiX0WVH[8g_/ -n3 ` 38A.|f|ј0I6bv%& ;Y㿜҄#dD.).p'3J12K[Duɥ$s8IƊ.z^48e!R6}vcMiozo0'=~i,3:?-?oS,9w#ROa; ?pB -֞IO ݟe#}ԯN$\l?], y,>&Рq]yh0AqK)ĝBFҍcH:-h-ǟcf)K9T127]qEjL<>h;|U -dpG -ƫ`&!8al`83>.qɂnA9 -; `HByg KB*k㰗2fF=#OM eT? -mTm_OBۊV<ɆF('n3uG~Ȯ#7Њ9[١`Ns.P..콤 'KnpF\? B>-`NWOOWBlfxW^b-_x&*/(j_=߆󑊢zF`LdE:SNʔ@S 03|TOKokto}bFz$4-,.m'j*J|)J6BP ^3ewܫpX.*,07xPڳ:2XOT21|"7=0ߴy}ĸB)H[Fs V+̯+Y(I(x&9JAI'tXmyG=X[8TK)2<TSRvxlȓGO|g/{>4/gRFȶ&A52 uЯ*B幃AuFǞѧuD)B,*?n` 'qQIzK֗4{B_g68#ʉ2.A$69!̒ub1&D3Qx" >ɏnνxVG&TۨÓ)sxd-5KxߣD&1±jdGjJ|J{Z ޲f6/vTp̄ub PmBU#gBg˷)-*E -ar>>Ƶrn[ɭF-IByѸP=ĶKUC wG D}"vN.p]]Q8uY{#qCv}sax_oyiNr( d8aw2CQ}V8UWO\g \yk@dcZt9$u -p-1z(=f) -vě92 w u煼ת#{P6+Dq3HIi%BCb!kc5&U ):X$܎[b2*@PkcӘdoTB_L1Uwi")=2#pI9,RO>T@>;bnDPuCfk^^\G~ oLRcHqܮ=-8^5Ońy*9:-\g8:T<?*C;[yX+I;lRL߭$DvYTQ6DyVmfy%/sIsmXP1Lռȭvow)QBb_LVwupeėO*|+](uHװ4WU.{ 4\m.QwR~MAiRz+%BKz?'{ k҉aa{H]sX}da~3_auQz VM\ĵv5I0LM)DŽp1:5,&4 %!$}ocޤA]R^xT◬M&/B:DwA24?cd&g]5b4a?iǐ Ĉ.OA 6vfvsd(5yTH/P=(a;zUs bWxDa)Eʼ $sgPJreY3w`cFo0|U[j5k.5J&eTor È´}I lpjC8c5J=g%Uo|L58E" -ِ[Ak]J͆VBM"{NrQihЦ@Y?6^߫ZWٯ]ذc؋hKSLj:>O ɲ.ݰQ{5mm<ٷ?^v"}ъw9O&vX7km[ ,70nΒ7|eP\I;-wgFN cIP#qWI ;NٶA)H~7i thl~~dzY Cx2>*c&mb{9f1X*L #> - V@g蒼]7n249=MK% ;,F\j 1klZi؊ΐ.|Q9а$_.!;̿lE,ɥDi}D3^a`Y5g{J=mɳy3CM'jM-iЦm n5? SJE+U~ ;q.tXd~~p*QeS%.Ћ"ưBsZ6-6[\d;^z4`;64藸ͱw;|+&AfLU3XTm)lF'l VɺgcGObbɜ9;v \CL, >B?KGCe"z -@EHILp<5'҉$>8#gL2m c1 c Fw)P+rkC qp/u8#!*g°Pa`vu@oH`"Ž:z_Q<,D>'ӅWP .`xW3|!6 -5 El[",0 e[Oz0~lUO+&xkPc|u$k.?{Qp""kr6isVa=~@W_ -.<7 -2#h?c~m'rE_xs6aG+K 14L^kUp^^_mS^dШ'>}5$:τ!E[bJx&n t(m;ZsF5uqX.ՂBqKP *l%{ٓ{'f';,TT,bhUq2Z3;}T9vwRR;GD - K*/@hUv$j!@ vyבm,W|-͢ ^ ~D_􆭍"ĉ#c禘*X/Ϝe>|XH;:)d9gƖ4aBQ4Ew,C -ۯBU#>SV$L-5gV ϯ*B#} npþtdU$Db&$^\^&Z"/˺+-}%Z:}9AYu rTlP0"~! ͚*@5K?߫Z-P=j>܈[O?)a5 -?WUsy5^(ge${Cm> "Gգ+$踿ϫ& Xw8?g,'ō="/xNM)'EFqrf CįQ9ZY$r!6m)4 V9kJ$# FьX٥Cp[ģ)CS;rFP#ImKGɺzj>>X9,ZL-jIbkȉ8˚?vtxPIO}_ay@:|Ve6ubd/e3<֭ztea'cLaM -lz&,f^_!?l2x2Xyń3D)\?ye ~4O+9$  -EVDTSؓ7X?MM!ԼuOtP Cbt;iްa@gW#@4c9.Do z2>M5i~u0 qswQ9ǸLt삟Mz)>kɝI;io"U)]$YL >$$T:gUo$UK,C`sCMAJMÄKC(g]ٮ9sUG0?L5QM%0Ol5&`Ƒ1,x'{k+mY}-Js#\d:i/NK\8HstQ#-ND).s*Zymnf\1l{(E=VGW9s:?wǟQZsC6A1ƃ6K@8OUY^`7j6@9?,yt4&}"T- -\Y&kVx녣391ٵqQ=beMq\`/nņ|2͌JkzDmͫIR4\~5NlօKɁZ]TC3l̅D3jSS)tWw$IX[wV -WTUw^PeUhWE^ؓ~Wchs sIg`wgs (5mr] B`7JfAaA3ƓG?{O[ ?xj/Z*7exXz Ά})C?`KcMՌ&)Y5J]q':]$؞]Yv x(ıH1eU>_0b?*񸨎b¤،D;Wxm]|N7U13*;.=>SÜj)CM>.eI1/QvН6Tkk+Ɯn\\FFV#Xde&~WE7"bju^I@j@bQ Wk8w_D ^z xZKA _`T}] -x}ЁM0S,rV+ KO&ƈ`;E{irf0F] w86f fm_8c3V<)r1p +hs|p!QP'Ղʛ2rӤej4Y r, r?4! Uq]f(*&umM+;1 --c8CjL=L1TDJ7>)BH*cHY}~xI,{7WjWާʇhg_YovMKiN> QRǧ}AQj^G syJG"?txt,L>֍p_>Po$^<%}KDS4 -*S<ܖyd;éIJ~JMn>ȸcI6uɖژ䩊i77_5W2' 9t^}/8%wd0k)ͦF9kih3ShPBULzs'0$Y/L3ol|f ɪ\AW#siS-O^I+36xas @M -A hm45V-' ѵ1S+ ~*%~k˝ʉl * -lك=3_2~OgPs -Ccd[aے{<ХjA {! ߲ۓ;O'9+wEHE&JV?fiӺ j05瀶bhWZxo=ƺ 0zhK5mov (YOut;e=R*yMVn,$v:QڳE.yVl;svn,Wi.[@34SD_!MF>J柣ND @$Y~-CMu (+lBpБ^#$~2è /@̣6 3nh -;۪.3Fq3\َvZnZ"/vNFNJ2V{#ΚVse_쑮Ta8C¢!Η>FL\M{5eH~7;F AB?VY=۩Q i9J.sӿc%FVbdեiL`a)kD=W \ne>NX7Ƒ†2IYf-to7/~Uas[`W*v3_`~:kjR("E -* -e)DDIss,f_n6":hmh+]AqñQqSa9{~8|~bh6GZĠםN\h+(E30~kTMGβ1:zka'LG2>,gt X&@?e% -=@Ihs)HUOeX^m7R7~,, \jJԌfͬ8!*]JR:WR]Mɚ PZ;JN.8ɦ,[r*Α]MM"waX)Lbjd`>:?|:?u>^G$fa. -ʥ_S%ED8 J=ĕK{6r zGG Ui<Kg"^ q -I6vPWy^,uc/5@:ǹ+[N+li{P#^yv,ñ-NѳH⺣<֡gxV</nb6󴳜Ρ +nhB˾PoT(W##ĉTwZU} w-vT-9O᭺HIz) z9R'dI5aZGS˟agW=.P1ٜ y?2X)r4VaGXBe`9Q1͚@85$W?D}z2* -pt +;Br\ܕ'> -vCNeʔL-ʌqKHr 7I d<BgNelB^փRγF2AqCR&t7߄{" D9u)Cw1t}?"'[7o̩~1{>Ru* ʖdClutqf2[l~{S4>J$.nQnlP#x])By`r+wLH?VD:|iUG~ժ+&+Rb gP>}WԹkQǖ]WSkqwZ -DQdVd24KGMvU35KJ~4&jwJ*y;X߉˔O@5hw)񘴕o-9E:_̂o&6#V(ѽS-te$ פp}4%4mrnzhe4KX*KÃ29ʩ~'Ǥl|O5ÍB -;^j㛑Q`exH;J\*`l˴Khk -&tF|(8VǡܷR:ϳoG*UjSKknRgl ޅ-6&Nŗ7O4rGmO[du_TvY{ ̏Iy\aRKy&P7ݪJ)l"W5{K S_j0WSW;wixF1^lО伴^'1b%OAXhq)L7j}=9PX=n`ɗKX#CùA *7{ jWܴTByufכd=Af]F=_u*`q+_i݋\^`BaE|S&%Z a8+QgQ[IK-jIKr2Tcju=A ʧQ"7{ٮם*X|,Yzѽ}ƈf:jCo[>]x^hlhNrϳEDkcCǪ ת9c Ht<)}z!hE~DBӳ2S͆i{;ouIp??砃46ٺ^"1R<-65sjpCSjqi6dzھİ紈 41.$5EG9:=ob쾄 v#[xﯦAF+T(C@RQF772I$^a$Eq>.AEbiO0]ТK5ΫPÛG ZdJ*$d ^}E*֤>?Ƅ$dO _tl%$^7[KSECqz"$]*B]}W zT[Rk"n]EUYvFUW\B6-RB^Me2B4/wͺh4Ek5˖<1U[tD>Q!.kR涧7uJc>c -l/i^3;iڐ0sĀZnS -qW7Np:([568ViAFޜ~h9Pldüj2dO -+61--1Ewv =JCHW34܏&x8,&#Rc3Dvz6RSyu_N/nmكvT֥Y˼?RFװKzn9Q4gC^5l`P\ܲG&ޫ` 9PҞٲXr6 -V4,{a؄\tcY`]lǿԾar鴯؏=b!&Yb ^[\aYt$w -[R)i[{$7f"o Xp -zBz'hO|Ō4ǐ|-j -:}̴a%Tv5Y9QK d0 ?$ćH|#uD3 phrd@,@XmVKY@ou([8#!OM~.7SoJn%OG" -Ü3N|/'O-R_1Vh&׺ NPz8de 勊ZTH;XQ6}+'h_|ȋCcuHjBA,NOS{3 L`]1> A rxӴ*E^.ؐ`Q5 v{`=W6뼟\9avGOXc& v1w~0W:ʎ~f: 0/˵%m KRKAcR% P#CSߥfmD5oEx17B0<&Yd8"1wܡ5 TaaJ3p57A>+yIMcu Zd?Bk1x-rsV9sH6p]DGgO| y5S$aE`$Ls -[Ym ~u8p`6*I ߕ`S88sn9O3nXOE /7f^lbN[PBFO.9Z_.5>F S̉R'}ΪѬ`_dX|{dHXԾ3QlZe7PRqشO5OkZrx5u`aǂ:*`T), -DPQʮdߓJRk=H+ -*#u)h) )B6s9߹瞏HZGzGT"93hDͺ sr|b4y $TK "$I~$v(B#].qi?CN ~ޱ|ܷLcOnT~vxj̦5<.f\K<2p:CpSy,66>|zC -E -T)f/:X1}J+>_~Q;^ㆪvs&۸>.k7yZS:˩㜍rݖۜaKa!l.g57Kv0!;ڗfe %]"XT J3aժlwVj=v姠αe=bI/gH& :g,(y 27>aba88fVVqɌT0NɉB`( _"fo! t}Wg_0}HX 9,Qx=~Jٹx>ӱe9M2mFS)Vk-eZFF٥btg0O?Dǐ%7eyښ6WSCyeUS}l`a8i g"1лJ"|PKڝc,$+&PvꖴGBoj_t4I vqf熚(eC!b׼^SbYi1¨;2W`/7uh?4 -!z@#(T 6 ^!R S#>E/Sq9z_ /G%ӈ0C9[ۼ@(٩P ,}XTOkpQȫUG6 x2e,> -?ϭQެYz/T5FL^`tީ3\#̬D:,vw[mDW)TBZ`0Ֆ`3tBQ˟kks41y `\޸cV#z`XHhwA0چFTyqӵܫ*F˪%*/>9 -gS'"b'zL=N)cs*bR)W<#S 癛)K -&L\9WtW!Y17i*%wJ_ 閥nWJ!p-0T`:K6B+SzlL,~J#ZLHBEe߈Eq1 -ڸTD}bB;*OTCnՍl$OYQ0mz7o9NŻ|hDV[Ve֩b7YZÖHl~I)ܻJ5oOݑ%(,hZGҼmRd!/NEWutV57z;jjs^^lDǾ0-a_aL؁w44簍b^ppi&nX uƻ-݂ -cY4_g ?jGIfH %J҂[%ϩC6OzvWzoZtA$?z;ؼFT2/+0@@S<@>0bSuqw;j4S'/4sEթ(P[V^5ƊHkg/ۄw 0*֭ ajyB5TC J(_F4!m, RN ?S9 :״OfOV"յڇ1,V)S@._ -#Q`K|ͨ%cj/&\: [Ft^Z"q٤Jm뙊jMarח`VCg -w"~>< 8i}XT8dzQVY<p%HG/Û`rq;Nm~Ms\/Zh:(MXа^F.꜋.Ys}5`a((X0T+JS 4&~|iB!! !)$)ʰ WFY]E븎3x,˽}|dc -|i-0Ws -Q_GpRjy0׿tjT̎ԍD1څڍ›N:ka? 7ek_%]a;זF=9-b= &Mm0-vD'^j+/5(er^+EL F1$1KWE|fOFMKm::1`ڥfXЩM*i9 -l?+Lw?-Nx͈wɳ\C0瑃f sM;iđ`$O0z*RٹB9@"k5v~.lB?ug]ed8JAj͹um.DO^^v:y;ske+,L¶vŝҼخd_5Z;q#k> MU\J{l*͟ґ3Doy"UDcu#H)BPit/ v`_Sʝ{e5mpPpy=-2[m+v6*.WۿSǔ] -^DMk,2.#ɲ\!{^I4Ԉ.~çlDcBU\b"c jvJG|H`_2rHѥ tHHBaG :Bf{'9 -[jaЧe -&hz6Fdy?>gۑx&l$^:^nx-'-]O 5@S Uڏy]Tu _,zWPT|BJ,ɕ}`8ߴy?p7gˢu\JO(_vOUue4+Qbi?A.jCxyRJ駥Pt㸲rTfdd$ֺFR>PaL'v2M*׵T]`W*cD*hAe#"ɆKO9JKL2J( KgK3jԉfZnL5oM(_>FOӹGi}<@w#Ndhoo4Y ̾Fٸ2YAz$W֜5Copli\ 32l;a<;S?B>zprjsm1tZc̥{s/J{c*#3ހfϡneh->Bc9SJ"չO8'8ހ `yHϤu-*` x[c')Oy\x!QS9q*;$;d'=NY ,|ܶ34qT=ka%hs䬺UX7Fl[ o1apuxf9QGk4;e -˸7荇5xB:yZdͫ,`2?_a[0~9iY Fs3g Ë9u<,yx87 1Ja,O@/gO㔛94 |.]16'^@1'p:XtwL,jVQv@wl{έ̱\?R^UV\GI+9D03oyd[R<""" -.2}"!<4tH~(-r25DH@l"K濣,/S}"+~wF}V dRz,:w&?C~FqJ}JݢJirjzEgU#p]ZF%+[PjewVjlW7wR/*C%%jGx @EFH)&0_Օ|Xu -DRNXA\0JSH307͛73 CWc+U#r# aQOL4Eљ?s~{sIy?y>ҒLָKd-ޣJ1v*fH 6hz+~BO:IQqZUՍP[UD#BM >$ z|?^!J0W8N WzXfщ@'h< -%sdR۔e[$z,Z2H5[&Ht L UO 췯+52j&P6uRɮ! -a+rk!o4 `ܗP)f%VQTF(Z]s,TR|O)O?ho# ]6yл)OU,F٠E})gsٴGyҘp/kw~˖I'Y;TdgYU'I8@F* 8 $I+A2((+y8OϋWȗE {բbW"@}@C׌teYgvֈHofE`eagbN_4!/e%O;mhtWv6[iyFy4ʔat V] au #QYm3rM/q{~tjD 7fiɷ  . =[n`4qShBrx_5wԐ %nQ~x'G[ `+qb]Q2Ըi=UGn~ڋJ(Aݪd E7Kz +M]!} jnh-Cզ_魺a٭Dfrj6$-4nUZF)Zpux'@]U/ٳۿ3Ug`iU}ڰULWu+SU[;uXJPvOŀ{$KF,qQruH.}imfZh~atMBb0*iWC䶧jZmn[nKfi c+.&oV.&ʭ{5_s9dmIA. *s5: 1Ů m!|fl'6#N -Z>\oMkCZ8)*bEE@(27{I" $!0a=+vUZŁ`-xEJUǺ -~~7TSsV6i1=2J眆Jh@ Uu;7!0 -߽\醮%-;=.e/T7D$v{.ʫ|ZѮmcDֲ+-Cu_{>1H1]"D^nR ٺ:E3[h9 7TJOW+3 vœLimc @6'[c`Ǧ8v!bR{1_ӵuoPE2\@;4"mO m{ ߺE1dA}C=WB}[3']\PJG5VmnYG Xyahd'J[U~ vWۅWo]WnGnR9H7ѨAu 1vZm]lUrTVA -sj6lhm,My4A*0vJR? Ĵ>2C!*#q0MJ!:ŏCR|dFa?2݂ch3dBzSIt?%LmF[AxYGҏ0m;GY1űh%[sጒ@9 q_8G>r Wn)jodEzC.qJviN&If8bg - v|sd%:uTf&L0~p.(RU -; _)w%$/ t# -~#u`u[w.qsY_-*'̳ɩk/)2* i9$7fUzflc9}],툏WYCIkS-ty7>T! 26Kݲ m&cӣh' ..+upC6&@j5tdP0=I˂Ė -C{޶$tR:(ϭuOR4$=jluq1?פ9Si|cqF!_z^SK}`d%DT wV>;<'V=(5H%jWMV#9YD2֓p~~J }D]gNSsjJmn->,vg&SLl#>^i8ʞ%4'RJDhRN0hBA0(r0K+aMY|"EGE_R^v4/?m[˨yN`K/5[71[Gؒ' '铯RGhqꭁ]>iIX -5'\GB ćd^ux+[^%e ֪pxE - 6%!Itި@Ҿ#% :*h$r7שׁ55׈Ց'I+6*ЮwȰ%U#zD+Jt BaUؕ 6}uOr7dP Cu}FEua7RV"KST20 EN{^lkƕ$vW(,F7b ˢÞOy<"_).kh[n 9W?gڈ7yș*ӼuA@ OpIRrP($e[iVYR -n#(aFq&mq3%\g?%ӆM5XD3b$ʁW ƿ5&͔D4®KcᏊ . -1Zo -^`~¿`6z q aXǰ)Ӽ܄'84 n"Db.yC<K d},{*h -ڸh>wMv^ c8Iƻ(~j? -eoyl/Dl5Żרpy1ܣܵ^004{ .%CA22dWuQ>okL<5.ſȠiffh7S-|^TjX[wCY*sG^1Ve֗+˃L3 /2y{+.;CtJ } ->٫y6q< WxA_PZ? Q y1>yK\.!OqM -0Cl];Sk)=RZ@[ɷ5JBeǐ$Ni"0 -úR4H~9.☫|Dϸah-)r~"eoMK%4 _7"‘e -QD~0T.>"x*O>酧.Ey+HVy55RWsEk*PxEGB;(J X(8hiqmh^ 0`}_APWDLZ‹]<4zG֦`oyZR|u^gCF#nr)Va5ƪw9njyIt -xI1bIy>}-AگOShKFx6xqqQ -3SU\ka椚̩Di~ ?{>J3mtߐZt]YNju]ɒQYlZZsNѴѷW>Sݥ0Bj+7q҄fU7m :8^;#eտ+*,_CY3MSU*LX.jQȖg_IWJ5a"9R'C\y׳qH)VU-Z.\+Ѥ/aen/|F[?SPkr" -^Y>VH9 &yaIxQfd}+] -U.o.=q-y][viRgk*`/pLBu+A@[)&PYQ?im/K,Y*gu(i2`؀V"fJSs=RU@7+>dْsmY)w=U?ο3D qjv83׽} 1r@vy:{Eͩԡ.޸,珈~CH{ksv_l毁@"lOR."0Fl]]C˧Mfi nq˶Q{56ef e l[IuY_(i&;to 5kZ/ jjp~Ch⨿䦿iRs!G-֠5 -&wa7WAƫXUr8+}E)oVӃIÌ}qZlh<gw -A?=$6-ޡ|,)!<*ǘ*z!8߀ϸuPpD|Ŝe=sm4'ҢؽYaPOZ(vj?VGgxI=V-̹uMCJH_-C]B~2A\8*E8PTΔTo 9/whaߣby\'F,Ռo%wU/ժnM*T Ƌ{5NJԢT9L;y _fXD\uַA:x")V%V/*]1# )ԋ@X"SVӅ4u.f?Uչk%Nj;c~?]Pۺ˄WҌ=V듍1 -E ֻqd{q׉; -NYHdfttc #&vPtQjd1o ­R)ʽ@}<7 &8wyybH04͂@>o` ~M`Oi#T2"-!NSn\ z$SC%Q%;OzcT)!M.wf.Po1U=Bl1F#F0HD\u̞rڜ*ujQO5u8E$7:"І(UuANgulWYE*Z"cT\kTxlx)$8(YBIY`[}.Bb T$=U8Oŧ yP-x$]0_ -j(sOH|/=wKR` ptl>f*ӡuU<=Ts(&zpKA?sLo`N0Mq+~*m-~F7^5惬H]${|-Ҷ9Y&=X'Vu+^ϖEm -Y/0X cAdPc_X VRx6b|C6^FeC]o-F?f7Q3V>͝yFsy]ݯMF͊k^NնI#FZ.7ƆQfeϫCJn;AjB JFw -mԗ6t(I5beElXQ͌ i,)6QS 1zJezVBf ۹ʹ/ HQ89SnE%o-4NJ``,)~utyQN]vحp+e"xN6y*,7$'x\CQL[8.d@}CɏE)1D?@晹b$?7 - YM N| _Td'wa}0Z<9|3閗3~o=Y>l0Wb=P1jmE XR[louv:.C=;.a.BřS[nWJ3ǟN1='\Xr8۲:KXj6e g΀ap%z"K1.c1ɇzɭGTRiVBe-)K@iͬ!u@_`&2q up%P -SЧ|NWP !o-t_ nyV|ؤ賐e`HʏE=>\Tǀ|cҎkIST!%Gu,%[IR'+#T}m3\/df)`n2#\M(CQd6flqGv첵).Z&wITe{JQܕQE\m`p`Ҵ\z[v7OVo9ݜQ}$SSFMWdnyuя: *o[3 O FRJ0ոl+L+&oE+d- -@?^fEkoo\fyJ8zΰXmi  -Nw}OYpz&@>gݪHc. ]7Mz#fe"g\a@\qyºJc\3ܔ r'WQVE D|PLs\h_h#9Z-TdL>˼!WS/bniA3.1Fx@Ǡ3UNN^nPOZdtvWO&-8ךshveSȉ`wPU_cař=շ}m`<<$+UV66do88{ηzkG}ڻ<<7\jvg!5M!w&GmpfSgO3x? -wZsLRq/~lK]QV:om<Q' R]AMXyu ^ȩ $}! 9LHaH8hʡrTtD-*fY]]wuu[bgg޼ߛ"ȹ I7HR7HBHudt *Ჲ=eJtj| #TI/W?{ΝO^'`v'$^E=7ITF2˵7-^'Z"[x ;[U7,QyWrr9E6cy'I gIRm2ZQ -{0K,^H/>>G@l`T=FZnZH ѳ$m¯鵩KA3D;w7ŏw^J<`i$M_x8wU-,/h!pbP1|*k _U;N45jX_:]$ %ͫX+é Miwzz{7`fOE5FohX}fL}k%Jq_b_A54WK'h?:lTHmm. m&"X7rV7l̨b]r+ OpK[{0EuwrfӵFajCCPktMݻVw[FR(Y-VE8 P?)p>͛5 #TtF%3 qhk ;`LVOpZۓ. j&\Cʡ <*g!r)J;ȁ&xK0N\B&Գ$bԍ7fpt(0H23ӲG1d?ź -bVֆ|\[w+tjj?b7hwJCmm#b.^VBDRb8E]4J 7LGc.Xd/a&ڎ @顢zQuֈ4Tqi˽èb˕ 43~,ymoθ[0 -l} TCuLBt 2ZW>Eh@+[Řy0= -sU"r];û](̏{e E=ma^2'FKv~.Оm0Oj(esߺ Pk*!3IBЦs4{^|{6k\* }XYǠD=A %$hǹWǂORV UBꯪr+Ca6 Kԣe :Zڿu6&?W&k).]%],lb7MX][H"}WL)RIrfr?AƁY&I~_IB${XlZXE&|w#؆`_vߢfu3fm89?9 -̟NՎ`jz1*.@爎܋`oْJ_+-4α6@/DWEjE}HRDl;Y+ z/1Dѓ(z)oι&;.4aZ#gsbZ+XWi;<~n"( M'b6!G lP<^\nM8--aG+dyXP^s:0q \p3bWu.,R&rm#қs)lej(^ ,=/FV6fj;ex%Dk%!FW@ao2QTvs 5h0B{UHiGCOzL'pbIq+'_1Lv -QA%$[H~}{1fKٲ:HmWS -ëd}2w7 j< O7i2G;SWݒ!@YsZ~*PƐ6xQܡ/9i7cGHVf3R>K2jZxH"Z")vHD} @} YJ64T(P_(*C]miSJqOZgA(ny8}wν37;?߇*x"D6HaeZ -5K e -tE=H\ƒW8 72ym]Ly 1N<8͍@:> >6pӹ$.7$C$pA)hJewT*FmKg-lm*{{v\ܲsJa>3_*ݑہ>V5|WG_>RR_YL!RFjz S5fځO2< `}I\:XiZkRH*4[(xX$u|I9̺TkVzl_׼gC%*wXR nY)N.9+wZ[E9ľWJ%wp`Nj[.b|JOsdW,R~#* ĽyFdwCp*L(8OelL˞)A vfFʹ.Knd~A򥾺]Di(i]YʯJߟ?>w[侾7KK6w"!eDp5V* 3VEa{:KoEDcɾJ#oOU44lTjFk,>{S?ýSk>Su=|j}T -SU.nk.mcŮ)RxbT<TV*yÙ<+`RC;S^0-itp<ȗ2IZ_0ȡVVKHWol9=fd jb%}DCy{sI*{ZL1r`n}+D_*Uz3}i779_kjxL+u ;FxL.mmQ`sKzK#>&ޗxiBV^\s3_XX_رC+ҭj|S kϽ|j|[X -ΆBL.?\DCqߢ7nO(M&JOiݖw0IJLM,NCOYPoQRSTUVX Y#Z:[Q\f]x^_`abcdfgh#i3jBkRl^mgnqozpqrstuvwxyz{|}~ˀɁǂф{pdXL@3& ֜ȝ|jWE3 תū}kYG6$ڷȸ~kYG5"ŵƣǑ~lYD.оѧҐyaI1ڲۘ}bG,{W3qHvU3sIa)\ Z, -     !"#$%&'()*+,-./0123456789:;~<|=|>|?}@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`acdeefghijklmnopqrstuvwxyz{|z}o~dXMA5)ۈʉq`N=, -ٖɗmZH6%ؤʥwog`ZTOLIFEDEFHJNRW]cjr{ĄŊƐǖȝɥʭ˶̿*7DQ^kyކߔ ,8CNYcjnoldVD/h 2 -R e r xzzzyuph^RE7)4=@?:4 ,!#"#$$%&'()*+,-./|0p1d2Y3M4A566+7!89 ::;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{||}v~oiaZQH>5+! ؎͏Ðxpjc^YURPOOPRUY_fnx̰߱ 8Ql»!Ceª9^ɂʦ2TtҔӲ6Lat݇ޘߧoX\[VL=*b/fMq T - p_L7! }tfUA, !"#$%z&d'N(9)%**+,-./01y2g3U4D526"7889:;<=>?@}AoBbCUDIE~% ہ‚rW; ϊ}bG-ޒēx`G/Ԝq_O?0"۬խЮ˯ǰı²µŶȷ͸ӹۺ 0@RfzƏǦȾ *GcЀџҿ'LsٛFsM6+1MZ:{OX͙~ʹ~y~eL~j~Qc=9~|4~cl@~]̳~nf~C~لOiZ/gP8v}6q}0}>ϲ:}i^},~ ׉_LpK-~~,*~&E()D9vyowy=TS3wI!D)J%OBvwN64;>FVWm -S^Di*bPkpة?%"1#!ϼK`L<n-e2*+) X䥂C@v2l Q?(=0q MzǃIz7MEY; Y@K (-\U&>rI^2IMe;Ya"VN,S;o_%sD;fƎ.R?l ;0Dq>8zDKG)3o+&<4@n͗0EO94#ҐnW9 b_7}B2yːv/ąJH삻Ȧp$ȫވy;Æǘfo虔F¨LsI,KhW2!AjHE^τ _wdlXggΩr!jU)[%B\DCfp <_\?k,.wȲirJRݐ=>0+cvZ{HllLVAc۠ ^{6oCҏSمbȏ:sz 7jP@Q;[wg|z30Uq`!P-~|X3+z2lIђ:_p-FOJ*Yr(".O'qäfrCRJ'dc~h!€?`}WzBd;hѲGϲmT SAij9< -ߨ%@`8xLTqė=,Mk $hJdx_r̰gʱhtG,KytomVK0X?R=Џ ]ٛa`sʠ7g&Grŀ?>r&z`b>&z%sxbw&{~څ]"WR%c"zD zA rs!֝=jcf]rmANJl$ے#ؑ >wTfGFF699<׵.'SZ*˺#-Jl.ZZx%m*| o 2ӝ_TWK4eRsu33'jRFBWl| -Fgml0L1, y+Hu2f;[T0BE{:qntoT]okI, - LgV_R:Kϋ0dP?= vE̷փ(M4m\Tk׉o,H=Zw/EI-LQ[ 8F/g֖'$?[u~fghXjݚ- VImKՀ,%ibQ*e97WKMYiHtXTBUDw-49#iԗ/r]hGވ/ - -lD2 h‘%TTT*Fdw">GY?"[f r5ʊ4`TAo4H5rWS8Xy;$Yr'q vUPV&4m/5LJE:S7Hvy.. kPXAl` -,e: E$@BKr.!{A$A,CY[EA;| TJkU>41aƜdcT.Us R&BchR) - Pd;ʟHbl?1;_:i^mMh9Ӝ+,x+(‡j3=P6u>a}&b (0=.À<2&m%u9_~zL!S`(6͟>թVlW䨸m5ypg!2< PR%wC>ubvbF.0UK$K;؂P,!rA5%\v" -[2gwdxJ:_'Eښ_+^Cژ I! v,V72UJLNITUKɎIy/R+=+(֨v6!M @PB%R--3|4-)#ͯ w.ܘ<;b#;*>$eG ->3"و~AZ$xOUx f𜓜x;٥Q h X(Zx=`dš 8b†id, ϐ!enZ -b /޲І2P0~ +1baktT ?g)˧9 С`.ޓ`>'4\DRdPaxԗ?i|9,t Ĵq]"m-9OD'Ex>#Bz6Nk%tm6BDzVQGq,2O: y{iHcy[]vaZT5 ȨR 345N@qG!fYXr{3^M7HX1ey87ҙ;NP9tn/D=}*I:2s̋%G{7abTBm6ۺ4JZmI׶Fהz\FD*rEyք ̣V-8ˉi#7XmZLW:2 -$Iⷱd`U+z3 8"}Y\E^\Qܵ)<&uZ!FM)V"ڟ}&à/ ď 5 O546PW눤0 fGlEbdc 'ƪrӬ[{K("M/y%0=zFBx}{w6{Y50%,40R}ԓvTp>K@fR$7HU( /10f<,1BS>٨RI3#&&pa5j19#yTH9cI[էjU̟~? +7NzM`k|-kqJ}(Ҙ2SaӼGi ; b:`uǤayU}T 2Ftm̔%OpuDU0m~L-_:qWg0~huw-] NVrP =<]x;Y1iw@8,n\(zqb !$zB&5dn61Q& & CuЎy#c%$7]w'z\0Lk{8 ;fGS Fx¬P~Km%t3MccM(bCB$ _ J,@՜ %ӸZ;.6B)PT~~:_tHNITScΤ5_3bO6-[o 7$cn:zNqnE2~7\NT' "[fTT^2F&+c5r~ԕ(jl 48mWDC]X#<n_ T 45 C0 V~ m&AGA7w@w;Q8Q ?d9#1yʕq_eS]y|d*&6Q30J(WG>HN vAg+[o:y1ډGmUV'pJ{"M@3X|*oƙޞ%sfJ<ߔ[-0R'G i++qNPF\&XT~ykPx>–~u2LX'P MOW rة Z?qU\+w>-q}y/sRQQJ@737Ka[t̷E8X,Tp!PVK$`Κ׵bu~*LlBz-f{i8DbMp/ŲF_<`w[Uq. Y!'i7L' Rz$v]c-ީ%HY~ٕ 鞀ws{)Wa˹ԑ`{[z ϡZ& z -- U@uBP.8jz B{GtϤ1ޕq# ^o2N*`DZm錞c@QY@Oy`ŕ^ )H??s %J@f-H%{#}řPKn@u5w:=YX9(5#p 9#Av(~-"]Qb'䠡ya -'£ +vO@%7_*Z-r*~z Ց4!wBpG-q.a+c"wmqk=WfB +k^0>npu5㞃= m]0o-1:ǒ~%ui;pVO/a3;0oKܼL6Ed@ZU%{ ^ ͰyOVNHLmu?uMBEQ1\IُOui@L7Nk\dd[i|lRܰ3"rW^  -19~(VZQjsfb5~Nl, $LAE \Yv3k"*Ie.gj4uDk"*T~~g^ ~<|1cPx7kF84K(/AI\%HG;'6`kK -ZJAFqKq$5GT#.a;1 p't.t-SSUn;QY(sў*M8= -BHZ# GcDS{d',Utl=,}*vcr+](_1rØ@?A[KDlv'”o>=ԏ[?Q ôn!ܘeoiB]u3PzP'ߧ%44Qw L7@?;gSVjgohop7syR\7V%xL| 3n|2Q|-GotuV֘Gk}fd'̐yQ/;^+b#&~ي2(ɚpTֆ)$Dru:5zj,|~0T\~>*,6Y -]7E9!7;au*8Y?Ң#WfiA~\mB\$OwDhE16:_JqBR%*X3 !O:`Iok2+}Y'1%Y GPMJ{rK w_ L&N NyA'ճmﺾo4gz"v;L je %Ɯ{NS6U'*@djNcvo^=Bi 795l€Aⶫ627ICkyV_}B.I=YR2U^c~o\Ƙa3Ƹ2@eU*Tlmcӱ~ xnNU)o`Iχa]PFŚVTC&ϣ࿋Y=d]/..FBXs+$=}buM>RWm6Ŗ6ᢐFX 5x{v*j;zv<_~AVUJϐ^IjQxシuQo=lK_ՑEkZ\4sqU7vOa J?Q)4C^\k[{3y~M|J'g4Ay,$0( jHl:Q"V҉1X&e s)MZ(W |Ϲ\88&tcpҔa͔ CC GU$^fb|8u̸&A֍9ke7;㥦koAvՏ0o5y'M3q"y$[Y@SgÓ=ݎP1)L \!B;U!)/C$N$A³ueuU},3Y'/Jc .8_[ON-<"NawGm_+yj~P]ſ^\y X,r-|㒒ܳ<L^T},^eDR,nkqց%|r,!gJx=~p{"\eeEN;Þ=${q@Q_\?/иLe>u#Mp'Yn_e<q㼅Ra8pLB=(YK[l`BKB#4;c;HS^OA>Ʉx\+0lkOԼ`Fcfup.wlCnKJIi]&fXPAn1کFTKBoI!ӮZ f)~Xhy9 ݨOC5&|T2ӲnSLB5eD0:yP;(w9mΪnWhKu{`wk -kH>*ڲ1 wp5Q݌$;LvvJ1f3n*Tg@oO#9|}?V0M5.ۀz{" NK?C_$ P&B̆e>(qIu`|ob|_0l2WꂝsCܴLTIa?f(/+PIwB WhgšH EiŮ(G6 -"  -"(H2̙dfr $xZEP>ţC~EF:}< \{ -% rH6N$(߫Nᷘ_%1]2:$o-8ȥ I-qt;'kTjJW^}kfQUr\ulNkHn᫂H*Wd6M2 *{`V%VRoJJ`+"yO|s86Vy8 :+;9ɨ=.qqѝ=ɥ^ӏwldG;fH^2`zBȳ ŞO*{M2MoR0i:T~%$9ED~cj<}${.-+P]c=Vzpwz\S;!?C:GFIױqYŞ ݇>;]mS)yrEz_n˕aI"l|sGvmߵ_7e]֭>ГU)i:D΂G}V W5*{f? -($p\)9D$ZYr|(4D܁OHʳ ;ܫv۱jxLr_r ;Wi nV|Rudܦ;@YNl-QnJȲc/14C:'K&̕BOJ{ߴzfsW|F-q2 ?}Y[pXdY<\v+M{ir8~LJޯ vlL: ?@o[g`}>?UrǛI2Lk.}GpI8QRV%܂L0/PUE ?ɹTcۼfHs^QMC!)$ ; ej uIy W6#LMi9ĦͱP*HʘFg]mߝn+|X$Z6K'OQJq m(B~ljSuZ ťbhWP"z@UVJ΂\,<\HA 5Oaf΍C75O Uݮx7F>QL~:ʥ#][eTS2%c Æ~EWg9i%3W4ފ:}޼0_X|-ƣµVu8H{YF"qĔ-F95E!L/3zLw@"FRmOQ&[#ZO/xˤr~9T00bܬ 4Pߋb>_nMFY%MOaN$ʡ˖~ &($~>tBM%^i3ϐEf8UB '`-icIaͨ+ دR=ZȾŁ=5U#5HR>njky/s6H؃E oLyCG/?QE%FvMMz)=ZB.ϡƋ/•3O85&YKլ(ST eҝZVx'xaV4Ë*H]z~h~ i0d,K8CZy{jCF')b|xNJ>V{0e#|SE1b狛*_R"37Boξ(p3_<ݥ%-tɫBetƓpx HuRuɵ)H?mf@Iz͂qrgM_D|Ce -ӯ_wCՄYK/Ԩ 佨/Y0y̸7.]*ѳa !d[m9#{-;W[ U$mb?ci3ؘsq6ĂT t֠} dlv{Fyt/ټt̰KQ8 N"4ʻc'׸Ns6I ][#?wsb,4U_ f)Eď* uä6Go76ɵ{'CGa+RUA=@5_rgs1OUG*ʚO&Q͡4%nlc=%Z vY Zeਝ4? eC` _wvĦ10KB/*Brv4όwM 0r `$CܝGa6;g-N_&ɰ.` `0M/s\PMf`p3 $A7 i c(y jӍ 5!UiMSD-rBFL&^:OF-T4w T3c q]2Rd/3U\;?Up=@b -TYRJ3O)*+sWu.[L6ǼA. 귒hoN_=C|HW Gz}w\2h{?Ur_ס,[<4DmD〷C/Fl Mr_򑹾g"P\TMIiDw$=` IӐ }6.jYx^h}]"]l -8"ӽ΃ǐL"Hڝk:^֖Tm.^@1~qxTlU#U75:LE|4&W25exz*̖̆;M0do^lpmaIS7kD#'͊$"lL?bADINmEh 8Ԍ*"vұE݌5Z5 `z~x[MN&a|b(ǁ$ch |cq)M_Ɔw>bSО$  Dpz!G@o3a]PnN2);K4 U"p+q 7bLay$04iCc9(6>E3a{ R䏡0`?s07y9'`Lq`ScLr&MP.ڽ,_ru/F=܏=1ltŜ 9>1lם -KX_t+ =#ثL -uuWK̹ u)F@jR_$YuBśGbQl+$,o8qlg!) n2QήU>Ytw(^'Y! %GU9, &>YcwU Mj"Zo6VWF9=al mynqA/2AI̐i -qAN?!9NxlbO{eiYQ̶>SZ .&sbj?1_ǡPkٟx`дY!n6fVJ?ffon06l)7BuyMAѢ&m>>Nj#4J%&|E]ۊ:i2g0io*6zXh +҂3;1"2ҍ+O?KjaY|nMHpA/LsI5cu*ΐDx!W {|mpq%qehrYbBt M7uA- -w%5,x+ z!Ί}|%wpȩxeXx|Yy$M}yAz5{+=}5"6~{άq~p^Q~Md~*XŸ~,LU~S@~5 ~+f2T"P{pUIpf P[AE;Z1ٓ0U)Fj"0΂op~7f ![BPY_EE;T\1撠C)k"djpmfr=[M,1P\ǑES;`Ћ1')}"Ρmfni=pkqr^mtolVurX wtDyw'0|Yz>̾jqźjlr`ntpu0rnvgkbtgwWIv~yCtxz0b{x|bh|~j|l|^n|~pp|j\s}AVtu[}Bw}0z~l;fׇ i -9kDmh5})oviNqꂿUtXBEv=/yVǧeP{qgi卞|l{nohLp(TsuSAv@Z/ryX_dִ2f}}hƖMk/zmtLgdojT3rxAKuI/8xσ[c&5e[}gܞrQj.xylfoDSr d@u/x\ębp vdܫg%iwy3kyenbSq@to.wUad`RfWh-xkkemn)Rq\@?t@.wZtf4uhvjxxm0xyosekz.qR|{itP?|w-~zK'rp{sqԜu#svFtgwwtudxw*Qz%x?E{zb-}|Xpzr'zssj{@(t{vxv|cwy|Qy -}>z}-R|~H(oYpq݃^s=uPt;bvSPPx <>Ay-|0m{opzrt?s^auQOw+T=y>,{¹luSmoou{psGrlatqOvk?=txj,{ @k mܖnlprxqؔM`WsNuȌ=&x,zj׫4lgmomqq0_s*9N uI_|2so|u]}@vLO}xT;"~z-*|Ly(x*yyr z$y gWTaˢĮkTd@D\dPPp-HG&]30;sCg( 1DE*n6ܵaz*&>P3ĸg| ,X񦁓`S$>BG DǕu#i#܌-`xJ!wم:(`[HWeQ2UFD`|:Cd2~TvkdEeUb2̽p ʠ~[@QdF!7H$ #dLt!BOK*G-iCrB.UlmO> ,B2W<+367ߛ@ )۠&KO 0ޏO igm82=D 4FB[!AIb4~Z *fz\OtF&ӝN&3xF[Hjz&3n14bM zB! |+ -/hw{V\lsTjg?қ۟u 깮D}û.5ʺ(wM ұ=Ljeo(u\ yPXƢ8p2232"uh0 ;(3-ybݷ3WdsF@w ,8#!H*9)iF^ -P7Dg3I33D_)JQNdOm2ta':=J.۱ -s`d+uu- ǵiȵ\L -kw/i&G1|91:H^gW@-Eif?QF?/KvřMkz݈uN0:ӎ3BJ]PU@׊VVzDPC9>RTl{=EY^ScyjN96b~mwj[ Zl'd}[YގM:tU9WI-#d=sѣS IKuƷ6i/JO{s{c@6oPU,'9cV~M6IQ1WwoT+mlF0\Od?oi4M4MC%HfM[r0p[p|R’/Ld/_c8]׍ YpFKM(Ewo@jjI0/kad[H>|/ѓL |00SVRׂV2Cæav4x,'L82'7&n&CĿf]9-f]i{Ta4EeNٟή"V_ǔ3tf65ҷ, jP6Ex)ͻUSu@6M6dFVSˬGŦwƠuy@>.TȆVOdj?#驺sycA)w,zl<ـB*7ij,\P#;}}~r4fxO"ZhNMBe@(78,iA#FaN}qǖ*lf Zۋ M2HB-7߅,yY#p9|qeےNYƐ*M}"A튘6؈U,ۅ#||(qW,esY!MANJje6Ç,}#5tPcjOf=_`rhTkHm=op2s(Hv "zbtu5k#jl_-$nnSjpDHrB=tytn2ݑOv)yL |triIs ^ٟtSuSHt#v=_.x02y)B{! -}~st(o.w]^`cCcHlVf+;t)i0aldOȯ>tsw[-wnw\-_AMb0ke#SsShA!a7kO|o>#r -v0[Dn^aaShdL%rg{`j哟On-=rfv-vm3Zp]­ `܄cr f؝C` jRNnb=q-vBw~o`^q&ccrfBti quk_wInN1yq<{u8,-}pymjynlpptnRrp/qsr_;utMwv2>@?nC)HKс#Eu$%`^>[ -(?`~^x0_+OËv&"YD>s5x']~-if~>NF" P^OG# ǖ0<7ӆ7 :sXL!kݱrx{6Rt"+@q*7k1U誘Y}(~\H`J䞂\ -52[{F;Onݦ *C{2Hpuw0D(MHOB$vKѻX{'V' 5c - -sh]T4I DGãTD(2BNlz9eB_ ݫ.#JUbGɰ Pc36߅!3?o/˼ 4Ta1l-vKWZApɾ<>\Щހka8Z5$GdW#{{ߢ! e8l&Vlu4ʚ@ԸQWJ"쎛)9(6gf y'1?JL)b쭢l]4LkۘPpuﲹ)nCA Ŷ+2dEH'Hm&Y3uѷkѽӭ1n]_Z<ڮRvӛpjm9G݂#j}dA-uڠ -0\C"dhK>مٸ:IFq\BVhF'$[I&3BtK\ D'`;I ["%#N\I -|?a8+ş3"-Aש_ZZKO%u6`X{cͯw1 $+OM{'E],jz6+~ Qk a=_/E qbVk&S7fg\"&]KOÑ: %ijeB>%j:l=T1e~/ߪg I0^YV)<^ϑ% -զՏQS-WGpaθD8ߠ9D֑ՃXM' -UJ]I"mteuuE)-3`Ҍ SoO6Ju@$ZZǚ;oam>݄92)@m{>-V|WU>r$Ӳ]qّ¸zEYuɔ>GT@蚩\'}њG9mp.d.@L4c&,r;b ӂdlt3ݦ]Q<b-w Nk k bK%H@ j"W4sf|Aa{8c%J@bW\E':Ehsř=}9fǹTW !3ߔ% פԘ]YzĀ&XIkWdPيb]9gbIi $ O1wu_)xS$P)m/UI .mpsf5Uwl}oyh 4;=DUIKSDSjj:?2*w0P4o+G4O6jeu HW)ϛ=ݮȆs51 okaIӽ֒Wo0%>#}?V5N_r}%7 -Լ{!`D}K_4 -!Q\HҽzȔHN>uA-^Ჰbg%+k58W #wi+q0khcuTT[`5Z[`J &-v**cs0:-7o3G(Z!d  z Q}vx'E}aQ#*'viƷ|'in˵Y;eR{E1vikYT24o/;K |O c -Rr_T'UtKyγzaL= zs#k)|OĀ܇:axim&&^cŽoIѓ` -W82K/ױϬ˽^ipuO:JD:WtG<8YJ] - ՄyiZP-|xm4rQe`dZH ;4SX1̚`wpu>7 H2%Cd>zES?+&e{\Q>+) ^T9ZPFV+@l@ A B -r3L2$$x *,^-ڷ[]<**RInpdk ŻΫ :C>KXi<_TTՖqcs.JmZEŒ:^΄hsVIbm8tSX&^ a*Ɋn^m=A2s^mICca|k`K{"Y١:nf,ڱW x_n~ -!f睥# Aɧo(u -gįVg攷E)?n/ؠbdSu3QQIB`\C!d -P,2QC[Pһn`RXYU^',|Y5G4-},V{:T5zGFdx|4Zٲ u'ʦ"Ww[f^'0Xcx2rKJJDJmB|CÁ=55oc/hNL9'0jI. =$!_3s^>pX0]ScԹ`gi9Q?+,O|ekkC)6bf!),MjQZF_Y[-ۈfiv&mH!`5oIxudP#F -P&h_2nnmMsC?wOt[Pk+jnA ǐHځY*zל`L﵋TL01|w:44o(%j̨5YJ_|fyl00DO+/.5T"$8[g)T`MH?Ɠ\fިÕyL/\Zj@Ν(Wڢud>P"Yd'$$ʗVJ+W>pG[^Gڻ2|M 5kci{ZJbILFPCR7<]'wKÍQXb* -$f»~ ^̈́:)]}pA(+RXzE;b1t!9ݠBj` d> !L7gh%7nׅ _Qg1R2Ǽĸ:@n\KX)'WIC0hݤ!XL}4l5 Vh2,?bLb#(sÀytk]:ibP_"2S&F ߆*:/~5l6fݻ Ӡv(l1u;8qi7mL[@Wxlg Y<#nMDyYZOEX;/C<_IfGuROM++c7S -4ƊaZԃu Mߊ]>]o/m^&=Nh̕.g*>d_$ -]koj-]wz`g`@XRSZ^6uV^og~XQ 濮a%{s Tp4{HLydW)YU&R?FD/'gH7yOG -S0᪄g :po)-.XF:e*diG{.㯙nwn.tY<"`7dsSC!x$g:SX9Y%r_']4K . q cYv.㏢Mrm*ADbW냊M1Dqby9mT'buq7Or }yXK8`微.;~1K}wҭrB;ҏޒ &6 Rr*?j䆑lugICkM|vhZYHn8VzQ3N??֫zGP5|No(RGJ[5&Hs)qq}^&2n:zǰkFmP03;7Nsi+ZiӍ ^zs7Tm , zb@p22{96ʄ/= 4)c x -t&83B-(;^SedSy7yG^H@Es7<AQ|h[\jeZҎy1|i-M']|k!3h{&m5&[KiK%}UEk̀u hT[*FkkOZ e ev]G ؼ;GLW[d;oo3xY{OEk[@|l2섐^򒼗F6a 9uUQ[Em'*uWAw:^WfAw:Rc$DZ9-N7~c - -?;A34VfO 5*DvUe_Rqr_pMv]{қ[;f4( c5ڑGdxEjO-n -| g8 KٶŲ]{r3J(?ұqlu;S7qWA}ǰ=o -nxg|GCTpTaH͗O0U`llڤClt0jh~pڱY_,x',IUjn\[M zDBb<Ô]T7S0Co}2%sF͘MQ ś!7fSѕ&.!mFk(+O Oȏ@ W1fG 0JZ-#=qb>@@gIxFz|޴\E=Yg6atҺ*SY5T9vh  %2{}n}I90v zRf8kOʼjVo:*xH3_ 6WWx4\;5juK::i7rʶYAd~X:J1<;e -(;MsrlڪU[y5vw(k --OlHWeG㐣݆L9sŠFp6i&xИp0C2}TxmCH#ѽZyڇm{+EAaWdVSy%ې8bש"SLL14$Bs&Bj&d@Y?O+82}-D^ݒD(PR{Ѭ.s!$4Pڣo\i(#u"D8 -:]C>6ڒ׶*m@1GQm lìOrusg# tk-ۤ^G) yۂ2b+PgDWB;T+4Qv{9輵;!f6~/ė|@r~EM$,<`2+oMҿ$ȵk뤆)<$\nnu|LX+z-]:r"Xꗺ.KW;–YFC :Aǔ+IU u+U>.+͋;SN@] LUXKx6 ͑8=*U4^qݗۥ>S韒+Ż eLsf v?m!'粈Yv0zْ2GwT1e{BHM, &fr(y)% P Ehl% -$EVDĶt o \~6-s//E 2<뤪t :mbpVn(Q7:ziZNl*3miИ` snX -U\Пbi0^Kc=!!{pwpyKH&Ș/UDg#M@1&yf_sIrŔ\ Bc7HexXltbu!hI -&) ֩ršbps;Cu GFq~~c6RbO'l"<͖z [T0}5y V|EWrф\2aAA0 /ɷW&aA -AK]מ q\kPU"Jѻ?W{j#'rG^$U)~VHDTup7eÊ⚊R"I^w0^+mOXiMi-T5ȝ'N]~{e r5Ճ-wA-VYF~UgBOJt8y0.{KO(vlJ uS0փyk^?6Wc+ -Cl]Eko%ݼ脦g}h0[[tVۃw,U^|}X?4:a<X s%هU)<@ZQ/[6 . 0A=fxIҗQl3\PBoJ]Դ\>[3?,ЛMOyIOi> '|2kxo6oy*Zo9XYifNP?1k𾠣 *_BupֲB[ 4Xφ}P73d"dٮ&<ăT>x4Y"GXF%Ngt2S 8.hpq܏#~2HleҢ(j =~n$ Y9PKC‰/q䢘&lrS1|8+ۺp5q Z(QӸAX!\$$$CsrL2$L%,*OQuOłBuUX뵊]xV~n,[|nC --bY@X?(e92"կ)fm6@>_|Xȼ L N+VJ2v&ǂga:y*=>C,꽅zqwΣaVbP$Ԇ3H* -|tc^7CvfCUʆN\A X)MȊQrK{Fۏe"j%hCi24.$ҲɹDӮ?2]HMtaPZ+C9J*_r%QNH4r{W) |em}^e ٻ -.v_.e'T)V4(FoUgzf0=rƣ[(hGjKҢy}%]ʟ%(y쭬0L1sR1w^NJO7 نyoxõO`i0)¿6T@JJL#״C[!)9!w+@,&TQ0GU5a -5\1(-9]s41y3yʍ/ G䇫~IĴ41_35g%@.1N§ N̡Pi'74@rz8Z? i;f -cENOri@Du{A6.ѱ>1_:, Jf?/LCNN*E]٭!mq=p)ݍ -cFMH?b;t% 7r~L&3>ﰞ~6slD'9?6T­ϙ^ 5; -k[}gX0^hq$WKJm3qV/f̔&|}31sO[9"6ε6 9K+|dj8a&kɐ=9wUͩ?|0,lugzeU,}* e-^uGSoy77bC#Qşn[,( l^ 6!ʌ>":jbiq2$V1\$ǕwkGԣQ%[`ѐJ Ή `]+Y)u!*5(HIdaoElw17hYxЈrMyA39ScLYgBل*dlQ P/Džml)IR`i?ĞAY訌:et/ ysn琸M>dSG&HPe*p:vFӫ}9|%*CdڌTm ؍θSVkq~VQ< f -CB'LH? 6ǍZWzjxA|+cshi#a43 KZr?'H:m2AĽ eЭdcM^k^Cj#,@DL2I~tHGǫJ̀e W`_qZb -"pp߄CH I&d2L)xʪ*jXEtJJ]EZ_=@XY#>(UT#tgE UO4E]cDix`Ffw0b(U -Y]sAvjfhw@A,bx#iu+E_Xx˼U-EW'_@ce2b1( h^EN -`V[@-kbn_Pe:60lu-'\j|Dme;tHGD˪&աD!ߪ@M?B=rΕtSwo2Y!;DLž]򮆁˶Rf;˷-r0ۏ첸R}"?5#mk+3((.RxP{K$ ~?uX m(U$C[KIl9vL"F]C2q.OI61Qx 1iQZxle_)O&uZCj7$6} A~8zXmb|n^i>]fQBchJDj^ k]rou#Ih -8ЂTc1)üW+-*kxueI~PE:LR] &t-¬^*$M4-bB c鎳A9ZuKDۄT}pp;dzx0w - 7 ? rlJU/3BK3hf@jm1RזD*p֓2O(Vv -ndmMAO;1S`M-a6)N˛,_ -l[c.Hі%Ŗش+#]lcٶ$ s~&b~In^Y6-쪸ʟ/FRa` Ei|o$Գh:)=kZv6g|V'E;R^t\"ZW -YnN'⢒LiK[!6bjnf$=+ *.ӃKvIchP*%zډ,1-pGsD8DC7x&X8e!j5kL4Y &XqYLA)$]s_g^.[fx́{sHq  o݌ KFaa)1$PoגיDO̐Ńwq?0$װޮxYZN8$8 _ُ$`lcZ6ݐ?ȇY+0H5zቔkQ}Ö!~QQ2&P{BcH|7gz9^sylu^A ;RckU>)vQ 8:oVcsK68#7>^nNk_<w*>mڹ3"ΨŢl` D#ޣ7W-#hD:G"DxA4 >X( 6b-X>*'qkxOOX+{5| fP|~NEzEy?|S-2<3}=`[~#ltGPj_ _߷,cn$kaM=UlMQ"gɆ 5iЉ5M%7R%qvLSG[]]M vKsw>q| -7pL=#.[CjϨ^wUOlTvCe]j20uuFfձʪ:AƆ"E*S'_ -!Z:Qpt47rv윽Ys9{<Fr׃d+G1 F~ /bm1&&x, ^ LtZnDz4g?x7o߽06m3fB|=ksΛ 4|K5~Xp%&(*,.0<664^?|X@`PsB#b$ PX<1A͹O3l.O IrOS#?UBP' -BPT;} *~>22 -EOL_~[ g ,v,cy]zFl(}FVύPq㫪J6A$*H$Ρ`v0;f×9zL2ٞQC|QM5xzAR+Ԕ k*xGjsH%Ť^Vaݼr~Lȡ3h5$؋#2'$ -,FP].V!foDc&2`* _'ǹ{# ݰw%{2>aQ*X SV*5r1V/\2dL9x~dE ]0 -^z[AKmILŤSK``;m\ojc{.]w{]}A][UT5䄚T9"#֑$-QJ֙ -(R;7n^윆a:VVTST@e& -PkLlvw6ԷU8{`>5#8-Eʦhc5Ij ɱUx(EUu=XU=ux}{tjG -4a(=Gr(nËqZTivU肝 F7 :&|ؾĮȬ8CLNlG\nt{Bvx~T2?]ъ?:B': nAS+w."nG%PBRBz^MLpz&*T@ mHh؇Dc΢&ZT_Wj 5yI5LOї5m - һE/`v0;fˡp;ϙ־A}UlK8SQC#kדtYFUVErAF̾!b7E|{e wY쓌E8T@V4U4<7IIiA(R@: j:8vug*tE@EQ*r 럄B; !rIC@V@]_ӇQ5UW/)aY/-Ry%F2"  InK/i"tY{p8d|Q\Đxi'6ĩ/UUi5gԧyebLY(ke&\1q(h-Ev;wΛ6 !5kC(xH@m՝N&וy UFeaf5n\+#$,۾.wAڐ&T%_}ؗY6"s 9G&j ơR9aWLt~-m ANv$&! 2p0t{z$?5Z uTj]Ġ`9t& f,h؈!%gS$&T<6ncK /'z&bp`F*8b(@H3x!}': yo8IP&\P{C@Rt(ɓʌ*rH1𵐗&dx'McČ`$f>m|S~䃱ؕ$x0mq]Pe& i#eF6AWB~8QChiTɞ <|]z[u*nz!bg9Ԓr3lq Xr3" >4SPh=m@A8 {Ͼ+\Ǖ--F3a@4M6;ҩ'Z8JԐpjj6 DzQ0'չ=;Qv(X N#0-z#}2Ң>ƾ#Ahw8Vw5C/[r:mU5fYH7H)N6S PX'>}<5ӽe~y'NNdtOݗdjM Z̓x3YAdECM&-ڀjG ož>ْm\-u ZTS#%xG;Ѣ8]0^`#Hƺb~ںnA-9*ViTR8 -`'yM>aATm#GђZVZ˪ݐETD_l }mϒdo8zPc)VdjGT *:YϪ z*MSqKP}W7K۫Ov*om;Czzqt}JeVl|eryItV2j)kb腳h ?|lIlN^mzQr}\E+ݫl([Xp1ٔZ[m@_Xi䮠pvfy?q)?GZ3=@W =T2lvsdrڰP챢ށzE     q5YTp -yOCŻReb &l[Ghmb9M%>]8!p~{gkl’B42?ȩVnI6 -e%2G-8o QP6ncN/J/FQ&= }-9>#, +>nƙ,Π z,>3'ЏԍI6Mo$GWdosfܐT:jGyhKڻ)k[Leٓ#ceA>Vl oiEǪ2p˪lMe.{J~IT"Cvnc53}-"ÐhI'ِ,kHM"D[YjsUZCM:fD˂+)U -Naa␽Zfk@ 0,"IBLtrAlĐ  N9Vr:#Q1ha x!coDjԀE_dLqi&]8NLSNIS/)WKlƜ5==\[jTv]٨@(WKsm!fwO)iiLڤ?鑓#tɕOL=?ٯ9,o9̳t2UAP@C6-!d!@ BB6BĂQDkop94Mre9*ӍRMd0W:rB5*G1GRBd; ib"P'dh8^`B5yϕJ\ L΄*nW2b߭L)3t*E&' sdr* i@s?/=:Vh,~ߗ;{u15k}6EnA;xobhS$u,N%ɕ8j 'q/qO=`S)г ,Tרs=@o5-z$^˚Fk3(lUA?5(!4v(_uw1ff:w-}hXKvzqAOQ NϜ@:&z$B/ $Gc*8?z0;ߗ]/ZZV#sY]X&qzlKNCd P¶GFޜ=;èj!,z5ϥ+D`C^n"NJf90 2?}ɉ=yΝi*mJnL6M$_e A ($eEU Ȁӏ^9,>IoGs}YEHBWh֯յYTwL3rS1MOeS-)*d`[hh%؝jӣ͓\$|[XRK@-_JoЌ+כŋ8V"]?/&{d_$]B?,kʯ2xF5xun#s -[oyDs?{how1,8 fL?CVAyE% -K.?)-amU [5[ڜȺMtM0o?s}*Ϝ|-.̩ {JZVu (lIneC6%FQnj̍;\M{w 564q@p${{bKXQVx &\^fA{O򒻭m.B0b @ħ/d?4m/o -y0wA6kloz=vVtbd.RC{,DŽ4]@Г zӁ4#L#y,xK|}]XÿC>A𵲇i6pD1|܎,HψP(@c ii@Rq2[eaU^FR6Jz!` {v' fQm)0}^(6Rc$5 (r~P,y9wM:(^։gDHDϡyl"0A4t!5F5bl ”#@ -)ۚ+Ou`;\ mqׂZ4++'8bqu2ǬN Gt$ F7 G,)O '6bgSo/+WuQ.mlc`rj($oQM -0rIF?i#@I_S>8Z7gW-[ܫ J?&[1Ck\B"mф;[ - 7qD -$fØt;Sj͖%qzfg,;-^Q`-}"ҘGHv- 35Sl.J7oÉ@ 5pNgmwٱٙmu*ꊸ/#7H NH  @HB\$77!PxE.ov[O8bD>Π)Q6AY-aWjLGU-oF7k1Fj@3\=ۉ <'#Gޙ?uߎo qxeP IÉh1nzY=Wu Mզgԥ'(e]-gCGi.];^ɹ>~o[?) oOP^M!=aǠtRl69m^rU4\ O%%-,O]TB*s;?Mw+Pmv{ւC)#HܥO)ih\LC.!K'b1 HQs.w{ϟ/2Tp c6#s6"bI)i+˰exVz:;9 sYAnSKG?vOW{$a R*ը1o7l ˯WC^kh+qf7 :B|J+*u}B2#PCѦˋS%e*:g cCh܁li) -`Fm5{kï 5!>s^sUXt9UJ厓7YΆ-P7 $*gz0W]yl`\:XA>s97<5'&cE=ffӕDdyix M8ZH6."4Fm Iz9)d1 ź F+)mju@a7gDfFiUcԝRڊXxi>6|XG/@@+$kaQbќ0/nMҋ]%:c!רZTxY jq4Fּ]Xyw?=5a'v:u]㌵u=,"@n9 $$!+E@AHGBBpEA."(hA P뷙ӗ}Їw -oPEiԑ9qͩ[ q)Q<\Uh.gY}WS(35QEJYj)zS h/Pk<^~'?aS| A :8}F/R+|cha - 4Y^HjZU7 -[C1 ?w<}Aw{_Kyē]Pmp\+ؐ- TźˠRVYĐ[tX;-i(i7[9GPq4zg6@0=4kֈ\c-MANTij *A+7V |ZQ4fmld/ 5@ -ݽ#]w̋Usri07mN wˌ|!WQRQIc fWlerU:Gg&{ q? -n. |f0rg$u͚B869A$Vˊ:bVoi L,EUJ@!Og)Л@v4>4=A[+g $fy4"nv,9r1gJc:5J-AYL -:J匞Y*ϗȭy5Zg!W6@@6,GDOMBӆF`+٘^-+*uj/iuUcnC9K)7hsz 5]Nٰ;Td~>TJ4& *ow} u?zXcΑggS+~P2u.3MV&*1Z,_e%I#\iPpYRg/PphmsY}~'kGs4Tj`ޅX~>3en؈24"y 'ʸq~tZh/5kofصOa8s߸F_$@3q˰>'n9;7^^^=1.5?jD'_X,D,Qn?t/J\p &w!ב0؋gTStZ*j| D„=bCB3WYx{ot}5[,w$ 4LBA#oaQQ\xąʈ}IHNK ȇߠ Ke's}*_};v$p;$p\,1~ ?$ - - ! -9~|?}SRwp^@YH{VDrqQ"Ş'VpoTU$VdDױJtzt -*BM"{i1a=~oضR[ Q!q/eUV.yVH[(`IʪYL 1KWiE2c9rg0]DgQ])ܚd]ѯWiMU}:o@:vN?ćѱ@Fq?.[cT(y1oM70œh~8Jh.#lQDҭWF[3j;E#@O<~.;YKhk&qtd=rT}J+zPUX}Ψ9gTz<#8:<1)y/%O$yevUm:>Cn^!R$,@P18Qr .eFҺs&o|<#AD1@q47剜_NJ5yvAT8a@Â*2 -hc^3~13JEi颸r!:Aj$U^NMrs!&xt~8ۀ>4@sWѴm)9PV-kQŸiP8SYFR4c4Kl] IC4<Q zás!{2 ЅfNxfKH~JμΟuF^4܊prfJ@г:6BRBd -Am-[[ꍏm@Ch[kd+>~r`vS!CkBD+Y]d=a&JD;Dlw؛7c_so` - y툈z6tk4 6֗7Z *-Kآ&%ת#qfB׆cʡ2 GMTC?.X [ZH5:Wt6譥dUEFIҬŋ(ZǗkxZ,z0= >=P~?Y9=1y~4tV$aix%A!jLsLdEԶrV!tZQ<s`i ,{߸?xQ#/Ne`%zyx+UnGz)xVY'iNCV`k"|FyT&`y'_z>#n/F\Lz2Cs/)Tb%Ӌ\8yU B+|Ȫ/: {7Ӟ޸ho;A[,8N(V'O7* xUzjޝ;Wd(aCV%l`PPyp<}捑^gՕBkQG5wa…g7pkŭYlhd˿L^b/IİK(9w} ۿy7S[Zh=(L0~l.}-ZYn@."@P -gSDFd{W5d˸:n8 \o3K>^=ݻ_%%4$&8 j%| A -oմĶ^Ƿî:fԌ& 6-LzH| b?ӑu[}U -^^_b6QYU82Tݘi-434o'iͩZRn -ZoH͟sӹ?}W>ߪm7 -b#1en ?#s"*aQ{u5k ixtJK} -LjH -0}0:[gAM vtv3tљvZuծ]uC;rCDD @ !`BBHHBr;\BZPXnŋu ؇}f~/76ذQ @Bbh\Yuun^R! lQwLs6H-M{#RpRʒKʓ7k׌MrM'?gİkS!" q8@& xw3KsޖG!禼:􊑟 %X~H<齾vmWkaİu~AD -(Dh>F,AC~I)o|J"&xŭԤǮ03bgF}PM}3-z[6|ǓoK@C' 룐A PtD`#c{xʢHjl80bÀ!s'<jc/q/Ӄ@ | -8- QMxFeU>iHR|/1{.K<['-<+AIgPW7 K g - N H]iD/X"IYEMo( -g]Ytd_6]8|pR~ =)L}Uz{@ yf4HsRA:VPRX[CYqDu*ܹr. Y%3XlsZ~=*UN^i\U^,t{gP5y - AEr(ӣAeQq>IY`<<)`?5Y^2]b+0gnϪn]T_\Vc/=˚%>x[@A#I=,-B- g Vm<Ǿ_%߭PfZewJ-۸?{5# %SryUC ݠ>Ф'XʂRlFyCrsTI0%ŭҐǞ݌!Wi KFMvWZfC?]>jqF-VTyl?d^6b#Sl0bYKO̹4KftDuE5spx!DGSvWLv|j'mmcUZգ_E&Ѕmc~0 ֑ܙyWk:nv}þv sv$4y4A֏K磻2nuJUaDG222qwQ؃RpaWPgM/ uLnmXivu:3_0%yN䍡I/ɴQ:8nj %bP,|Tv@^@q;$8ΐBOGhOtP___r:!͆i`=li_(x1ra q#Ь$ $v@mdx8$ F{8 -;("a)^STS 7 -Ә>ɟAdL bc!3쨠bUom`kRS2i@1ȏlr>>^@=͚#K+ڴW+lc4`}_81CQ~u6hxF 0l? y;H !?)|$Y"3?iV徊H!fLSI̝Itx#{vMH!!M@0cr?H+e.%fNMcH͐/dLk V-I9wȫ_G 7^P6P%Ȩea-\`XL)jYFX| ך3"紒jro/&ꀣmjv;!NzA1 -1+d)VasYV.o*X0N?'Tg<'TZs{ZI=yw)=?S4О\ p|*N{?(ы -Q#eMeXqiJѳRSFz9XFRwOMnUzwOqKqOVgKx}E5qcu(:ʢ2 R^P)R @JHC"BE0 A\ gnև}99? ^!HyYz@-F*#1KcH9}b_Rh2/s/gf 97y7 HPa 0WRX3aA *v=A)%(j*5ybf?7 +@\MH@2 P7]APeB<*#q -r|h%x\N/bz|VViè- -5(n@ -^$k -$ub -wkd߁zf0]1>F)\d7KheRUr:[Dx%2Q5I%euaYI+tJ^%(G-il \~NSyU0.FyaM𔋵dCPq d&؜L,QdJ)BJ)dB֋$SC wNyߧ6Ʈ6/> -qJhMIlm"Y+q &WQ%+ŕm -Tbs@@ӞEoܭ-~b0䤶2'rą >UepKyBBc^3XVVIqUz1 >7O;AtzB;~ICțF-LZ,8GK(^4#J]cz9@YA}O_\;nzGPLh%%lƲ.I*\Y(ؼX%mK$ik ^-!Bs@i -?lu?ov9цwD%HS2{31| -n)c!5*!/Q)Hj&I A |sPsp3F>M/Gl|tĺκ>mw3ȭUNӑ98żbt,Bw2IjVs:L&9Z&9&^ MaݕɤvOeq'Ey+_hbh'GDzCȺB(kAzE*f5Ό0"4ӌ)ftPnjXo]+o?سB쨅手e36M$Po(u -v02`Ry=0^G/z*TN k㷩a#3 -sr%ۿ -Ve ˴?si1ߓAԇaqIw3SY*v5(Y51讆to40xQ9rl|Wӆus^Y~mKw|NQ^#Bqsғi1s̈9Zn0/GϷ`{|{cn[:6-2vk-oVZm-FC q4Fcqƴ(c j&Rߕ}L{#}9,Wϼ3 , S!VCfi}ؼþMGNK?z8O.{—`bc?[BD/b>bSPo93){J<#}Yw:W@F4 WAZY۾[hΪ8,v -]#xA7̀}@a zZ`C? O-"ܖ#>65ڷ;2"{+vM%\ -ypI^vq2_gQMg9=ǥ=Gg>(*(Ȏ;Hd%| ,심@EERVOU0l*wo{_;Ci zCg н|_H)Om;ݠ0ʃ]ʬ_Y4("65p`63q' ܭc~3!>G P~؎wr+ ..:rN@uᎅEc *lظ -zHMQ xzAԾDkW pN8t8@`s$@fka;PYln "b HQƺoc.᮳cً9 ܹ11?` v뀍5}wG!Bj/YD}鈿S -+5wqY.棇xcy/q14o(v7kHx AAn8x|A -e=1ı.${5pנq -&+0ȋ9 55l eԄJtJ{UK?Mj>"k>G>EOsE7ڙ+2k1`0)쉑KxP{ -]D#؄t -J2:xՙ&V"_8Cj71RuӲ -6YPsMҹ>jY,BOz;[Rd:MRhg75V]={__Зsbc kAENBv?k|?0j78H89PE --aoPoꤜYB#k 5*a\pP&k, -E|>O<3KbXC㟡m+y~oߛ`b<&Uȥ\59颦lY€VɋTg*uũ 6cdJ3Ft@6cv`^GKq;}^] -h;c;H N]/eS  VUfRe $7eMZYWF0W-3|@oΗ l1a ؜um%]V;B=vB\pW-%\gKERSy*ʐU(E_0}&79 @͟ -S߮\tncuO:>hp{+!Z#9RM2Ǫ* KH)T*mN6M2յ4\DgB9_2?B p%MumwuL@#pBA^ ST::8iQimlY"YY9}^Pd9(R6 D)LI3 %8)|'r2$E9)yW ro?(}Sӑ) ֩ COǥ]%c7M5Y,iY!iFy-_RM-ϻR?{9,Rl|RRF$5tYqE7 )ɏ<ޑ)  Y4PSF5;/xWg-^f72.ԊU!AyW2*R/}8Bfzc%9gʥAgjĥ:NwJCrgECzu6Wzsmsw~a5eJmN qȈԪkRbWH:&*_V/+w_rDgfIkU[4Pe1vGO}MO@ٛK_omϕY' YwFHNM?x=G_sb:Uݔɬyɮ|ɭRAb/+զtU|J -WmR}mNW)6'|cDŽ6%ňw3\Heܩ%w_J{1 GV(d2*uTnVyxիE5.vmyN5ҏ.b< >oDrZc}[-U$rD$j {.TB2/^#.SjПS3gi{ݒ>'Oqb_B]\~gݑ&ft{w t\ ꨎltz9)z68D WoZ?u#ꇗT ,iCzҏNF<,iQL?ЛO`S,W}ueyUL+vS;3$~S' j#*eߩ]o^T,7Y+O;'=#e4@ӑ/rdbO,B&xȏYhuX#wvݗ -C3깢L!rL:{NFN&&%ST˴}P<4Mt -/fVwWkS%*4ҩǡ; Ra:6p`F~ 0cFnuF##G! -E$Ks@9]0D Te8v,`X` N70I>~ r>ę["fȱ2E>ރwf6uw r3W)˕ 0b -WS $x9[LkpXBA{c7$;C#@!MO/ X/AbAh)c52 -E0"Z+l xj=ir$5w« /Urc3\嬃hD1w!av%8?)b|Jؠs~S6$ o=OQ3MAdpm:f2ɷ@Hq$KˡS YeLT~Sz7I}t _(Âh#t! NuM5exuH،x1bCp = Ȣ{v)Ki5)Zޤw=@0A}N7PF,`Ȅݾr<`&OlX+m$9CiFg#Zd= ̠W5o*oQ+~(F{.0F0Lw$sD% lggEw:v/@2ڿ.bϰ=l.R-:{RUp#V$BB Y$9Y$0Baod(PW+^!,E^y>9/yw}qzP!qO( CT=gd W o#oŸ_F M"#Q/IѯȷP(7b5. 0w~B~`9PXT?9; @X\V?, !tǻ4̡Y%ԴjH#uz:~CCoX}:No\{5MU?ͯO+r3nwfB` 9HY}LpuD(09ZMF5M.t+y&A ?,'L2򤨈2% `uM%;Ěsy~QC| %'bzjb72zjRXMI\I-)'Kb mB\@ḨOH8Ww~rCsk 3s63Q64r6[!¶K&~˙F"D]?L 49.5%Y =7pH`1],Y1W|rTMOweC/0m|L"H -Qo\JhKٍU}_6HϵIӹ{n OO?|{e/ʏU{Pu''L٠KT2^fq OhgK ^\RQ?& -lLjwxѬw݂{"YMв֞\;Tw}˄ nʦD֤ctB5YN7)S92 C'NEEC,PGI1YR PJ[rY¹}'}K5Uv Y/Ηg1c|I'SCR(NYd*R!Z2_ɞ*!hTAc2px3H]}=@]_Y0^}gwt# cOU EttAVJNSrY&U+UJJE1HaU@5ikwxN|ҹk5zC'KԘ<^-j3$/K5u&-Qp5 J暒Qr4rn,Am@7dK[>Tluٰ}së otxՕ`ߦ*P'B2p5 (\R' G&w5\gZ׻^<|}WwVPr9꘩{.+a%R!(Pq9g83mRa. $rt >SWV:rk>WX}rKEGK 2؀9ZG@$Ub\TDc+شB-h.YK}6(E[%XӸ$.wBly; -OU+ڼGr꽳ݳҚ7y(n)(A=Ǯ52:ZVf$+̂J]#EOP)=@/q֯/qxpoӡrΟ}=K+3FNȺ :VMi ӒLC5vDS7<]~QmP.rF/Pm`C߽yݏ:6Žў%GVg  uDЏ fB)7^^Lu)6Z2>u䝆c Ъh](VED$ -*d/FI - Œb#ngT-.uGܷ0n -B39+r?%RC]9˻RzU.y;w;l`Wqy-g?cS_iy=*| BKZJO6>b)MSXT*4VUj^cu:ZvctWn`>ӳ~˴[9N;W/9'%j:f8#mϲLviTv:^֚ۖǔ.[Wd1uV#eߴj%?Pbv$k4mv!&2yҶ]7tG۝8 /t)]8IWN0׵^bvWrRsLyc?=*˷ /m $KQ TL eP`F80+c_ĴŦXJU$& U% J>=r25j"#C##KnD]=q=ɑgDGw>ѝW!p|!ݲ7=^Jp|Rq^>(9!Q( HaY1!;BG.;QȞX?2n )~c3:Q/H&à r"d(|!/1B?T`GMG b ֶj+}<Aw#` 'p3nI`ǃѴ(ȦG@=# :d Ry=[9}Ʀ߷ V|aStD}Hp GP''C>i>ԓ}<9S|P6%_z=P5uv1 ġP/r. ܙIH@Z^(%Q| DJ/&8X`a:$I!a xa;{K!Ȉra93aӡ@ -eqqu1Syn-\Hnlf裆XT?go"aHi9C -crY3aaH @FVҖECm<$ 1n&x k&i}V3 #~{Pi کaa5, >.A C+Ĺ!<20DC:oe@Xu QS|pS\(nD{;rPo,'!6@f A c8Lש( _6 hLj] -䛙āh'#NwY3a)X<,a&Fc42Q)mkD,Bg_ ܒZTO.P&6+%_e- / _'E}4pR4Bo`,L\jV[x~IvX%=!+9x-7+__)[T-=YsSn\V/*G5f. 9sPl8PY^X#*EP.r`i^|onI)k-筮EҗvElSecM셦Y֓~G>A^W֯;8"߇UcPwGs-}5bc)pڳS2$kw[4UՇ5wtO7T]Kzuᔦp?VM63rz\?Y Brs9z!p2;ik#|r[a[!g=,Ʈlׂw1XWef ƫVD)tL^Nn?Γ8rFJF7qxg3Pr|UO3& S5`їƽ}/0~_5t<᳷9h[C䙆xO$_TN r0󖻍\g'9YߎAZ-՘MOd%LM59U}v!5J@XĖ1fGyPdвp.O80v9f< smOvcb8fZp(%-$T,,5K34HDuQP"KٗdZN<9\vupdi}{>Q `>7ZNHM$RCÆGda+2ZB'pĂp2SHr] -j yhC_K^hyb5b=lО# pQ,[8XG*cE_ODNCVNš)i8GU;ۈ&_HfPHZ!I!q"EmB"~>"pg#!(Ohg3aNQ4NB8kC{-!v,t5J d @T~|p7c1?#HKAo*V"t@' @ P{}dY7` -b u: Z34b(@,i!֡%`D(0~N} G69?CL  [(B[= q.Ш=4Bqq%xg`]y=;{5x5,k 2)Blp'0\Wx@c2;U ._ QM;#tp[\6scc~pG{ÜʘT e0} 5alZ(~'gYb.cny8=לOO11v -{*D̿D[!އ-L쑾h hśH 1%:K谺8|H!rP6 ca=,(^%~wBx/[bE܋=!9a grܑN6C=ڵQTUnE/?%'bW/wsᓸےRȬCAIɃL;8bXɜ!|n>sZzs~Ē7 ѯ4[؝>sQSYr_?ߓߑǷKWҋayu!CNF -;ڢ0xǡؐ|Ѹ#i{KcmJqkjobMZ:Oo tgw%;y}w,p>zݭB/M6小\!8D߲^7ZՐUPq̸%5:=iszGRUgcefobEf -b,g":z_Jמ 獡#NvF:unrsԱLvSQpxWZy}&6K&w*簩2yCgu9Irr{A"rYLtފ#oserɁ`{&^ɛu6LfJSdSy:qMP\Tee`KBE~Cb2isjrqؤϷ&,%!T ; (]@{:!PRB( R'DD H*" qwPagȇ99ߒs9I$(BVK S%> ~"^=7y^as`&ETSYAʨcGq'y3좂s‚nÔ/.w-XOlDde1%PD _*s:bhqИvN~Vqt`~xv>ǵ6Ç_TSq4Һ"މDnW49z)p}8EGדrlD@`VFExߡ³JdT=bH2`#7>"ak{?~л>;0y&6)!3)l09l:`9e̒ (FPyyX햅#`\/X˜pQ<cr9Ut(PZ=/2*PmC|zu;+lrJ'&I̩ZgTn$VlDt_$ X' ڤEmۓJper7ujRzdYgg穾P3Qֵ]SNA&&t.C#I.^hz-;XO#v>c>N6nkRlrk}xg.+98=7Q; pa``4ݣARP.F}CycJO$ ]ㅾjQPpav:MaC/ao,lfʹ%?wHo, ןDY\$o4(^U5"kUfJglYsVXV^ R x_md-;]:fֳ{l`^`h>jd~rgc" t^hXx@@!`CӘJ*䣃t'w9O~[=>*~fnsK;jZ|[=8t#42B/kd@su:pPQD-JSь6t7t䌞[_Ce!S -"gf(`*`Tݍ=.ne4.OH"Q(D'P\ЈhCFG t}JaFK!k.:7ict5A=Ș0EƬ_lWXi?M12qJ$ވ:&$*eQyPEY+:긺 (# ~| G E 3N:8ͺ;8Oz@5!8&cǴ -|5;Gk :{nq#x9g 8fӸ/<.ou[@1?s!p@3 if o^9-j y;Rf5@nrv' tR/2}e_^S\?zqfLxÞ7$>hp ANAF\2r6hjіI,[t;RZq3~.Ӿg\^3E&$ߑN_%| -, @`iRkCٽV@8y5l 9H:ff -(wĬMқ\?'?z u:Lw~v{ S?xJ;oe;5CB"/oSlKlYk3)Nd;9ut3{ܟ1N|ʸI/WIs >@e@>AngkJXO]%i2Bӟ֯eǤ鎣2Մ!n 1!ktkk:K7J?(}\[0G}Eb=l AdHQ@[!Mڮ{W{zn4yX)(6~;aj<ⵠ*+6EI>9?nj3qf K10$H 0<_^ ꝉh4 ]\ܒ\w,_!5{omwrqqQ{/3=.iH}!徽jϾ&)id`Oˬc6'vMUE]sz=H٤[ ע/Kj{FܕXRgkܴ?ZWLdUE7pQ=’_DőEQoQ3C:~AW= 1%ޙhFIiV V\-[SOxgWVS{zTg*|$1ZpqXqU_-khbOc/scs^r⦅sx!!n꽫QZM}y6Tvnj -Ҁ' ;#=T>)2U>(I*ي.Q$]qWVS4)u߀`_vP@cMjM給`:IkOk[ -lZ -ϗΉ#j3I%iCibVvr/]$8)NIC5Cǝ/: ;/1n&K `ŏX4jFtM@- - aPBzVYaLYㅘk|kObX3ٱ~&6r6ȻOOG6ɠDW9i"ӽQEhƜ ,0b*e9,'aՖS3c3{DQ4H0)ځPqE! -<Q=0i` 4LOt=.a.ʰ"aDCE4TQDU8 cPf([ .Rn(ASxX9xG r09ACڗZ1Jj ֨IGբ8hJ*\'8(>M\'ot b`8dLT;YR6*q~uF.J=QrNި?(KGyR$%zQQţGC1 0Vg်Qf@e;b/CxbQި$D*,,  ]彂w9zЧ[0OE-z c LZ` -c16\0j -#ڭaMzo0|?@uDЧj*[>*/x}P~|ݣ|ݥBY0< }c% \*fS1wM\H tdrtqƽ7jCd n]7{G}^kNtiD/5D/4Dj=|f~Rc5uԙqIDQ⊈ȾCHrsH }; - -#xZʴiZԱuZ>sx9||񐊵n.5YMAJ"KA 5 *#pL6#-pͶz7ӦJWn]Rc&S٥";H+,%p jHVJbe)Qa^b(,D y)|Z)qn3כ X)a zmVoRG,K)kȫvٕɎ|3LV&V%XU?@Uw(1ſ!1Ő(ZeW0Wi x6}=A{a.'M6eKȞ&!>6!$.ݙ[+tOfUUFW#ȑWy{R"wypьÝs8>Zﵡ7"fi-hgMoKKiIuHl7Iz7QCi -n -\+k{'B>p6?7{qevCd]@?ߓv> eЛbw8Gv廝xw{S;|)W[E?r/~V迒g9jfjk`s@=aSN3w1_3"ܑН]QM^i@AH ,!!   aȢ ѶNjkkGfܵ"hE .qj3/s{{sfW/=4rl4:&eUԉU'br(PV_}P#>NW8,9u >K~i]ԅ܋/a坟ÝyDUD^Rj NOD{Z\oO#"V7ЊwXN)iQOͿjr˹jʺZ\25/$7'6}&o 7}״Gm:i=ic l:;wP^Ս Ϳ㌊|QMD[}fpNۊ<zǷ1tmk|cm_blԶݜǸv ?6OvwP;;ye*pALdRԩ3vΰOJuvuO*vt/v^^ٳK޳[s.=͐^cHzak=U>GhùwK[w@9(+JcԾ"_L+)qZ;@U=h̦E;ȇ#J$ëpKi -נZV7n7ˁp;8]~QBi8 c>H7'""zBJ*'T"}kC]dR!EBXd/48pܑ~p֑ ͎,xx5quoC('u"4c )d $L.9t?$\0Q ‚̷C|n Pݠ}f>g#Ѕf!8w - -W(|!g5q ̤+$a.9N )Br=H$$(H-@TPiwgpZwl!_t1 b v{ cbh01dU!$Ą Va8*Ĥ@= >re(>/}K _2AR]`O!tZ -WR`HR~E$bP -ev0CKq'@7' - r\>&@~ aأ+{X>߀8rɀ7(qVH pIj*$&9f̙!vh7z+bMDbGd*FU'9oTת+-Πӧ<S@?IH䓐0)IO0M_=_3[|5略3h5gx/4x57xk}10=c ֟.)~ HEnZ{4:ML5y$҇V'c0l{nj]^An}SwQDMЮ$M|[:A8n@,ҘHB>#/~|qĒ2U<}̷;u 3+ޣ&Op/Bh3Pxtp_t=ᙨ*рK_걺I& (NBQ(e(:\ Ź77ǽ#g={ U[Zm7SH!zʿE-!ƚ+ƛ9ji&"N}} {o7sY Rʳj)s\ΞMoBVkNŲZД!cR֐ȧ̻$VqSmDcYi@~<4VJ' s<0,bK%!dW"fŹbR~]ʀs> *SINf패';Q̨<Ѡs,AeԽ"xBBZuh)MְBXRȶ[ȯ)\.<9q]QMi$((H*"@V,f5@ !LK@(h5x92NGǶsȇ߹~z}c)̓*u96Ϝ e^*3WuZM?YP2r}mob ZfkVPa~RM|%Qz|Ǹ$~(ŵO%n -%ZnUSOPj8=G`ߡ_ҥhܟ)<fA%z)U#%ܫefeE䶉ò3. ҼMBZ P+ڰ¦9$P%+2-%&DqlZ`ߗ+ ks9l3k2"Z*?﯊"you@+a{6 }jKKKbA*huE!j iTo5&#YP>e~L`C&ZSXQr5\k޸qM>ʮSkMmeCJ)׻_V& *W"5QXN< @>Bsoh\!B-"y3$0T`½z5:<̶ɖøPFm[ÉZUGJ>EMʪ|oHY8T*Wy-$W6Ec-sFF*"odRJ,48X`f:` -ؼbt̡Bpק{+y~š@~,6<_ɮdUL2d tt[Z?tBɐ!䭐\oJީr1p {@0uָ]r]Ky뀨1dzX]ksTu -BV&*)LU*CqP|Ce¬Aȿ!mpLp~Wy z{ô1){˻O9w&)HWԖu㕧4K3!i03"Y3JJTϑ+ r|ȭ^:OuW) {hshF}p|f+iZ@՜#H1\%wc홗.:3.f$p㦥NNzj5y˟>?}?Oq$6nfDgpG p =I=@+B;D7xxK>ؼ04+6g|`rŁfڵCk3eO=IW_zFοC#fwv~Qir -os+ k cLV-&۞˲?f`;Dx; ejgA'зhv 7|fkg/] z ٿկ{x`),@ [ߙ@C 8`64f1ƳGm4c5ȵ4W+jv8N Z] _;{ z LpA8"4```"pP, RFC` -l¥zb'&jA'^R -4TxPQ HG</chm6F&Vjr -l&e -#n#D -eSNCCC@:*"=S,kP%;LQRBlt$js_%nsFΐـ޻9sG^xWSo-Tj}'润 MuyVMg/hF5DӠDdEa0$L!g*Si=j0DG3t9G.ߌzFZd-tm%mӅZ!?9rNGؠq;EQ=QGNZ (M4LfΙIJz{zX[3ح ټkqyVcW\YgCSǟ"8(s9~P~Tx>좸6xx!IM8JEo`iǒ7g`Yûl;x -ʩg[at5#}!UgєPp6i 6-)>$VG7yTE_UF?UcP=LxI ds0<Z@{-ΑR.¸j8]ECF.-D -ǣ_:N N&!Ƚ2~"RVws܏^ZqO%(ߓok"!dc@13E4wкXD]c[lظ ]lq|,úՙ3 -\+ֹM.}7מEIRN+g^3?*I1ބS8Ä́!9&1<&_b7r2Wi1_ì͍dIUTfgT6k^QIɷ<^3{{j϶:-畅w_u+7nJG騘=C<R}ZVry^).jpdI*/Wy`vs-q-[ 5gdBV.YMY2O(g6yK.omZ>a"^.#NzK\ g8@U+beV%y:Ewn_Bu.Ϩ<PD H)#LQA,"tІFpF RD *1XQp]f%'nf=G}s=WR*x-^nAIܐ84wQSQQ;aQP_B61xCTT0^,p̕_-]Qךnܔm^`UfWH+v)OmRIޒ)ܤ޹oEDBLH$ oA26.98]pfnt.*[;hQ]&8+e6lDzBY[Q+HouSEg|2R>H{-H#BK&E20\ߖpQ )qXt)*+4W֕V"ҭ &ۖg:J $\IN^vNWFv -h[i Q^R"K0T꺭bQ#U+,-}).$)" -&{d1pq5k7٨&+46r5 j:^q:(X̝),dEK9wkE5/snAph}OQQQF_,Õ2ڃJwfm4Յlț5{V5d7DbRd+>6)uSu墈&ކ.uCq~hН) -Sxgz7.^܃ZZi>5Pt:2e^iRuI*Knm7rKs=M2 JnHC{p OpCpC:=zW? --4 ]@e*{磤ϖ)sg.VY97[pp֮(f):v!;ikw۪n{B.^R=lRMPzA]H-u̕IrbVύ>u4BcuGLBd.XPWvﰢqy7N}7{;s& 9:t}C@HĶQc$:2%@`u#BF6_s*ppqp5~'[-LjL.7h2h1=D[!b܍Y?.b/Qߪr#icؤ#7&s,17]Կ+_6dǁ\DU#c$&3+Y+&lU}'|2爦4SJM&-m):S]{ýqwx+}P2 -d.W)6ncmm,m -ib191 qBG|KV@E1aɂ:3jQ!9N,vP>'Sߨ־XԤN]O}&gI}D]\wa% R)i~=>BO͂82ٙ!.g.CX~خe6JlQ*\iTS@H.!y`b FdA@A 4 aJ"cD'( -(
Pϱ+߱}`:{uI ,c`#ց]{I|OdE?Xc{< -8b¯37*535.ClU4-B 8۰::paQpڱX'v1e| 2F9#a[ lA{̷Sɥ,s0\; -"_ h@ |9 fрW2:pb5 a|'&Gq b{̽D|^'Fa7BMhXt'=o) <_YؗڝW(5ܞdZnQcU!-[j!.z5{%-dp_jI:Pw1 d_hwWеL*D:臕fJ>Y)hץ(Sc +e&Ir2j}S_l_W- TC|)3I]':&ͺ(f^zLd/.XBVJ/)y+nd)˼hYh=w2٬ )vQ,yvi%)YaYYWCoadbτd`Π6AfҊ`u ؋M,hSbE nU/*H,X%%.ls>abAǐSN7=p w -%!9kųx)-vbE8{`u,= -*̃/ŖŔ fL7=[+"|WhW+BwK' ,:}mDss^R(shRX\)wPCTffU*'EL;mV1$bل-mWC_^!S~\[~ uI}q-v P߻`G)@N9@ΡKհSk -km N3<:fjm0ormڹ55K֤Uk|YxWWENѨQ?Jw0%wrf@Rɼe%ǘג3Ly)P@w_wOqץu*N:ڕyw[6~_U%1/;{xb ؅>K= c.Ul&׮VsǭX[-uMu^uY~%U4uyqx"*ʡvlC5ިxxŃJs`Vm\clTf3iwNl׶ݴMnc3w<>]لmqN -`TRiyFxs.q|r۵yyo띭}w8>9|nrolʖҵ-ˤe=UΧ䋜o)`"<#“QZ2\b$D+ mk ݾKvxr~Kqf(/]p6Q43` (; e /r*x> ].K< ^9e>gx:,fٌ M`tWDL+p`_+ǐ5|U"wxP w`EĄ+͸EQ"\!dAל8#P ܆Vk=!㼽ay4gTh֩ȑtG] ;z6& -,}sQD%IV%~pYJFii~Nu?V,'ZBsS` 9}yt{\T_b޼1zDw5Q]_Z|#x~sKn)$U9 48U*婄\C"⁒RX?"ZB =zOᨮFgyfG*˒V{3f{OBlMz 4eεFO >pZ`JUD/y:Ľr y̿_ # C{-4k-CF(^ԽfjppQ0f|7\^a3d{wUҕiM դ 0ь]}QNbWT.ŪUV^+1\"h:еg=Փp>j --b oЫ*CH׵Gh(MAcj1:QLtwxBOg tZf݈kVߙ^b]jP!SXIsGN/l7O3y|-0?a LYB6b>@p-3(.7RFvsնD7ó2?YWyĪw6vXhŽ]ٖ eɳјJgg]ȳfPQ%L^`}a`aQ PD0÷Q )Fal7Ls:q&3sޜ0e \[%%I8ù s>pٴi -]ʞQK @ ?IoUWp㠻6DC{=7ff:47BsP u~ڪ`v? lo>mnVGA '&:n1ߒBӡC U(| YO"$=3!Q2 @ׄBz=HfX0IF)_u@wPlP ( PC2hM? tB"A -kSRsӚEs@a=2`8Ȩl3q}JCHb >$L$)^>8qZt^wK-uD'3Ÿ2q'vABpaRNH^ɛB~ CXHPCnnDOZu T 52^HF"$W셺=W3uЯGnj6{ΆF.f#W'~#{;֫e=֥?:CןdNvå_…VW.D.rah+i 8Jc=a} Xa2bak7lcwݿfwܳmЗH=_2p5YIr4'jqbjQ3o7>xDxG#G퍼y13K~{ԷofÆ_$TT.nTwR7\v43g7p$I42w7y Y<=Aߎ~m1[b~(h-hHh\`%li<?"ﻝAtWΰ)83Aq^aQYpZӀ4ۥWsN)LjK$4%Ygu 煵Dğ |1SPk_yd`ZT[0VFr2zeN -K<׭EhJ3y5YxS}k]|tDP%VHEfuIcQؘo1}c%殺9Us0clƐfӧveٷ͙.J[}FG]z%WPt!A|BT*˗$S^X`EаtP7)r>0Oc m@o6Sm9`ߨIvV8ת\+Urg̬-l/VʣBYNxBKdń3_ ܣگ逋"`~ǸCBGGAӢñG%8XrӜ+wcNg3de7 }|aQ$G}%@~HJQbB'Ry"es8Ba+z|tٰ֠iEU9H.%:!_YW˫mks{H=%Qi/dj )Բb-in({HWFG'5ԗ25e;8a\sI}iqp)2t|b -~VA `T;!V.J亻r'?$ -K߱!u="!{KsH_[p"$bP[*( b ݜB~xmuSv%2MYY^aS̃$0(8qKQ[Q&']%3ZZ:WtCY?֠ȺYwrpnvC}V}^8vw֕z&Vk}j15,(-aW¨/U V]uTz>+C4-(lA~*h7#};jdEqmim2Gi9%5\y볿_x,?:_/aa ճ`>GSʹ -]=m]]@^7^/dٿA0Xnb>/!W[cv 幷%ޮB:B:㦉fz~t.tV.=Q7![@$oGx(3͉OF"Ʋ9u5ctmim##?r>o<Y, Q}hPv ec@¤b=%F:ފ] gBgb=3) ΙmU?nqxkzq 7/ޜS'Xc@ -v>ʵ sH:D&u9_[sc>oχ`|mq2oTh3q6٬܍~Ivl?ᮝn9~Wc2 Ng9ឋ@e.,x p iq6.a]xj_Ǻ%,e%V/YUKʥ#LyFr6#Y,/<爯1E#T{'trcIICm׀759`w -ﮃ^ł_9}PV )x=χ_u1>FH}oM+@ lzx> չle D((`W% 1`A,H@Dņ(< C-O1D!ODQDĂg0;=;{9F 9PY0s C Yҿ#DHNb:D X -ACFr<(g3J,Z=X=OZ8 `] h%+"6!j&;@:5ͣ1n@hm -}k7jGK(]48Zw }0`W.e@~5Gn+jM :kRsG?:=@ON}DoG=b{}`]$7bu)bѽ5t?+f 5(F?C?b>`hUŗ`RwŮ`0o4H%$"H>)k xCxjth(m0k0QLɼLDcOWI%KrW߀|ExNyn@߅BV5ջ ]x)[,<:t6ᑬt7J+&BZ7pC]h2ehTsE9塡|(T7Tj -:U3PŸ@7QMVen)wr{q]yMWՌCkp^øZsƝ{=fKm`f9/c)QDy P+Kz?'\z?#qnU듸c>;sC V}ҮBܾUXfLXD%L3lw`φ1H6G[g\qǜqy,wy"y_sW8-q;-v\#,s:Jvv:;9:wlqfLm|N:h{u A!8bnqm* -[u_epKؐ**2,m7֛l1l5)0.7TJ6 W\:dk\^V2Yg`(vF#9. % }#cwJFscS[ŋ6-X f%YZ=_ڽXU9 ֥t'+mZ#PM88>(cEV~O8qT oDѺk6+Y"ʐ-ʑg{fzmS,maeye//L:}?>4sЬD}>͟Po ;`k@xry`A1Zѓ٣L2eit,ET-RBg}[~=h(1:ӑ (X2 d 3lj2|/N&I I?Wbиlqr1_5׸S3Xejf<.iSGCp" -80(ٯ[u^ȉ̘AȈҢ}QXqR9Ӥ'S E ]|j)ǻMk"-&1sT?pjPEq췍Ҽ3NZ,ҿqBj;(v<.@0wlpvL8!f)xy\ԨLȵ" uyGEuqwgfd`.誈i*e60 URUZb2XYK(nQ@M\)GO-hknQ999s{}b<31=uO\u]D1D[~:s[<ס='ˍykP0e P0I(HҜy2s&3.N#56CiuXShvNޠGGp>36o_kE QY|7jdYc?4bIQ4I\tl-4 6)1D")!ΐc/T+b۵ \z/NFŋ~>\3T`'ٔuy%&G,5E^rR!+ea򗤚a6IѶE -$}LR¤r'Vaܦ 7w 3wY`%Rf5Q|'&`_ԥ;I 2ۭ^8cGbt8Nşi kܭz a5_b[7 W`=.Z -׆]4T[]Mo:`+@. -L p? f' iA̓0 8 ׃S -{t{Ȁ>-fn)Eϖ:4@ro9tXr0y TO&`R3`Q19*hZ]nusp2Nm U{0C{2OAy -vP7A%PJ^uqW}@w&cN7sG80u -p>-*ka{l(H/xArA$upup}DwPA;6yDt3=S-iw8O.ձ]#Zr_`HD)PY^K:_KFn )kp9}5O= G; pKŦ@ ؋+p By:xyDM?} :[KWO21 F.~EG+#ɗK q_po-~#nT]:˪^nb -8K!N>C<O}'iǠd[[k;ϯEf\ wNrgũ!p/394L`""}*/@%Spk6\KÍ8†NQp: -jp2`9Nű yy9t>`:G}vm(/cH?5'Ip?P;2z4.c: 'i8ڍVW0.bfzWt[=h/ - n{h˸_E zyɓTb5 O7?OEOHhq`t Dg)`Cʘ!]Zv{*vkphsѦŭ!CΉׇ7OZ4gI{Y*w}? A/zPg&2S:Qh MP3}:5<@SnT6hZ4uuqUҠ%YVkNq+5WSHOQZ*HyYITꩤމ&8biޡ'H}1 -"'b{d86Gji`6D3-vv]m / %^^%^DR[- & +[v\^'_H {BWG7&3ҿ| )-F{lM16ͱhIbƤ2l}C<@,L\5G$jW3NpZzfyմyQG}*.*D=P̎NQ-xyO |JOi:D'mxZѬSQ7uOg x $>[TujZ*W+F^kSRΌUighUjP yj9n/L]ns!I!X2)!K CmJ,S,HNe'e%9ĕىeܬJiybLR^[(TĻ|~$A& \9 4{IF ǪXnZ㻨1b12h![\npI%\BZ_ +/7+)ەN}?&zAI(^s?dN~7_mSJ<ñ TMAeZ$Sa2s -[jfKS\q7K]*Ӵ'Ԥ1n},)F??2 J/W袙h!kS.s(N9]Q;yIq#IlĦ3Ein8U(1} -$pGn?cUk(b,% J,v-I.. Eu݊#ʘOʘA'GHotE,9g0@X3}9ݓT84ɬOOZqIP/y_,*ʷ8o{PzN-gߑn1>c ӧ#% iJ-,KRĦwIp^4;D!:gk{Re܋$$ӻ0 -Lg6)C8cl7FgaTV?x B\,Il|ଥҨղYeY&rM"<'"*WB[+XIYIoR٢M^s=\wD\C5`0D"83ƹBqfL7JHCvKCviH#iȆe!Ԧ.e.I -^ ̦{~F`8[֘B99c@"u(AxI$ %_2JF_Tf!شzS۴Ne*Kv -PrQ? -_H -\ la d5i!݉tOR r+ZJWޕeE9X 0e,sòb 3КB[m(xuQ!b#IY}XLa[8 l5N /xF6#n7LŖ-lڶ* joAe}u͏Dt##s*g16Jҿ< pnPBUNP6t>2 kgBCfQttR@Z| 01O'06 z? 'Q@86!=Õx-~ 0h兠V>xڸ\[ 9/G0+"<5`#Ha 8iAu#y㼖➼ ŜG;/"WX_B_-'{9ȍN2I{F(;޾^S@y\|N u'^5Mw6'݁t$jV; . ={\\ ry =f -0^-z~I8m|E&w͜>ɤDtح;DM"P2$ydIOK exJVғ;؀DW!-tUU񭸆2Gq?"G@ο\!/"o™89iͦ=zГQ3pkMrpqUFjFgъSm$3‘O"%Cpb.đh8!x ܒNuY"o$[ TY:Sf*/G|6Eр&E :=؝~@JDd j|<\5x]7\uK18 Ψ)ؐ<޷=||E>86pcQgvǡJ? -`>e TNXI(ћ`Pl67HCNI6ܒCHrCEIίx̲\fimc?p}a2lEa$&4lLY(6COBao/}\)A55J .{]8..]n r[ۊ(%*XԱmSӦi3δv!mӴt2M3Mil/2f9ߞ>f&mJ`OfO-'_Ʌɍˍ "ܱj}6p/{Vp\qz܊5)hF+" ӚjLjIMs"fÙ!v43gNsCܠ"\4wYSe}~@DPCܦ+;t/m"hUc*7'sMέdFNfXa젶 i{~0ק=hgnK.UEg_  yyBrpeCmnNSgcDǐނA}ۘ~#ۥ悺\noӝQ ~+BY_٤+Ш>BIN1@QD,60aL@ش æ4g"dGٌt*tUVG5U~B$Zʜ5!M^Z{&Mpݵ6W&dw*&]g] ] -+"\F5uWep2CiJi -zE{RpqbS#uEuSnCw}jwςb_c٘B5Y3xwZ. -וywy_sjJ`&FOy]7Gif-PO՟ -f"1j=d\?_T䴼n"[n~i~-J#0GLQ;;ZPŽ0wn)j2@eE~W9tYV2s܁wyu65WGyu7HJxZ)st~P@1лoA^nhRqp@P>CfdJ U!#:¤zq65qMMKr)=kJu.ӞF D5-ʞ[ -d!st#2ƶc8ia=R|+,a_؉pH0ç] M&)|II74eָLZqhcq=dLO ej=N'$$O`fbI"qH+FB3sH\3oFH28O1p#Mύ!Z-v87 dRLL=e\,'`f< )H\8شhlX|s/#~qxqo n=<9) Ch_$uh -ПfIH^6]p) D"\ށX"vŌu+XEJʕA|-p~I|gėPG@pS%gi9i%ҿL/וP1M}SQQB_CRJSNhHER !|dB29>eȲZfǚ550żmw\]繟њ@ҚCeSeSE؈xxu`E D994|Cݬ`@c\ 0^_o !(`$' NRp>ٜ69mV<Z[9ɭ u;yr)ɘ+ƫf0jRӢ P676 -}@R;Nl_lL:X;:8 u'F7yۀ܋ouQ`= PSy -*_6XAEl<sDw' L7;0x0ZSלV/R"ȭN'w97?=G3sǼBOٌ<݋*%_꺑aуBFqd$$o+%9V)6 *5 Gp-'.o Y]> C+"/NyYG(2ꬢ:lΧq&9[<`_Gz)s 1'#`uQ/Z)ʤN`uSJY1ks4r.f~/Ȣ!ӝ7:WGP3ٌV"uC.b,lN%3_gpqoZ躙Y<8I + ᧾dJQϟ0ԊhI>K\P\͜E140M\ts :k42JC斒e.F` J.$A"gѦV84%^:e^.R/NZ*4؉zzu uawc3vE= 2,wwȍ>6^X㴱;MI(M"pX2 ʏqPze6>WNbOr۱t?63/QmvU揱-xN,+? bIaJ.l?=*q|]?o쵘ݖѨ)nyfQ%*W`U56YQ1 ^GXmnzxj3%Gyg{GFhh:!m3;m}PcjQevlMv`7v9Xgka}9VoE{X^nA+'C %bY(zԿ=}bi}z5 56t38zc?*ð1ӱ#9JYXR<,u*BSPۓ C%(Cg( -<'bg$LCRimE/R. aǜ|+W㬰@=ծL?2ԡHWO,TcNiB:[H+~vHZENһ\b͡\l{Jza|7[ +Θ_!90IB|B\`/.> \/E(TL : <&JAigV29Rz)d>rIv!cv RC⎙!c1# !a ؐHĄ -ѡIBTh0=4W -mSL -'-kzaRYA#[]dK3f H0$h\FFhM04ᘮiBdx0%|09_#M273(Bj+7& -"0#2L*8y2T2E$\Ct2_ۮfFmo^R=|yߔ-}ԋRR-)>Ϝ"3*{$efEim4%wW^zQM ʶ&fPndXVL#1Z[W,2Z2cI>&6j!<@ۖh!Y>q4M`,e,x 9*#fT{RclO8MIE,%eF+amڭku!u -j[5Vm8պʭVU_Z-mߺC[[7䷦A-Vsy\۾HJ1eRl4^kЯBnfs nԢ:D~aj^)K`eổf+]M"ˁ샓4(-wZ^;ir) 㞓nwF[Zi&sMk.:⽏B8jijpkxju-HN~spRb]05g9#э܆AV -xE{M\0pvƎ4Gh 1.::6zIù:bQG, r/ֱ>[#>AVG%h8ٜh[mӝihml҉GccPϡ_ONIt=.9_9%tzuR glf13] &;Jw>%}iBPWf2PWIU̫8rf`Db405nt;xZj~yl ҧp>HKo[ȝkrf>7vߐ@a5쇃L  B,$B&'fѿHi5\Buz}M=żtC:~5V)@C(M44sћ 4棱XׯuSDg-:XsE]>c}X+]`F>/jh   M@'_\h(Ac{)ezO=lK59cnE鄩zj>^TPnBHȅ@B.@!B- !"BAQDTRuκεgzvvnݥ]9o|=y2|k1;nmmW׆p%f.Ōb^pe^wqbpGX}qQ,MB!R}[;q+67Dĕ8.n0np̆vr|'p"~b!MX‘8p1Slӛ+,ejwѽW6\ڔsxiXJ$+܂d'wc.#2-޺[b_77 I}O0vG1QݟP{WH{1jm }=]8c,Pxub'k&j~GIF(}Ls1.è( Qv 0 .E!qbz]BgNtW2)ZXKM於C~ʚ%X$|@5敏)pS=e勔Ǡd#$Jr#K*C@ZԂvenx:) -f$sSk48?&"fE9OO5_{Hcq2Kc^2F9)_<Ay)(ѡ]QVE9*(Mp+Фtl ;|Us^lcQQfU=|ƌsdL3NY)GQF@:,xphRRW€ - WzPW [jƙEsjN1Ǩ}|H@1jO'Pݛz49D&N7@9z_ЦG t|4j JQU*;,:7:?L>fԏ /1*Go6Gg!=GrO4\Q|̒մoѽ =?eiѬ_> Tx -`5\@mj#5uiBuc:NVefZq1^Tr*L#NKT֬`o+&&uh<͔zSy(CC xIe_3LVe(%dtSV@uʹ[hUIѶ -X6# D(ЯvB / ?02xmY/sd?q5iݧg&#?E^`!! rB*d lnFf2SH/!-|H }z+NRi2Bz'6@m W7Dd;灼i06@0{]1K%5edX+aXo/m` ƣP;612@:Xvp {KO"ޣbrϯ.˥*4q~d%dԳճ|$$;G؍=g~Iރs{Ecpnk*>'͓|]%5!qw4V BB%}lN:PBp$aIvg9s~DD$<Ɂ' =Q%_BfjDd{=YpN')|FLN3,19%]`aB.(}INPWʤ8xd8Du:1>J;돟Ч[o pp p&t;1({@}>np/p_b?'v5Q5M+[4[Rjcr}Ǯ{GﱷEsS{^ =9כ `.AFhC+qrCH_i!eWk2[EB ;ɳtUvZ+~~vuGMD]쟀F3A#bÎZ̙m e|??[#(FXI 5hHKS?(4HИ9hb4qR<_Zق5b.@dP+^?jFؤsbguC |h4)ڏ$/{;vk.rrkmOqR-Yۤk#ވ ?;@_e.hza}D Bc>رlÎر;ꔏ<-zUv5ZVJ*T)W x+>hv@Iy _bh4ICgV)B^fUllF-n~TTj{OĎ\봷hh{NC-U'5vk}?UV0īܐe-5LbCfaJh*w\*v:"*p~9.ϔbUnܜoGEچ{hsVDh_wTb-pMD(9IQ&1S9DqpŚjdt/ a44ztc-Mh`yн\g̣:0+*"EPEaeXT7( -.ǚb&Zq_c5֥1xXҨZMD? \{0t^>|߂3s1TG9y%41W1~PŌV1V$ٍ6es[2͔-WJ3-WAMJ`?fr1 6 k`T78bEqgO9+h`U9Kq&(%a,pFIJHc0'+ ?:cx#%S3=|K!1'tTN쉽/[P%5)J)Iq$[d-`.s\ŧ<+SJM2ZbSI Qg[)#Si)ZdQ5DJH5ʜ4LiK+Rm9-QtzƦoԘТKj0;1Ue -v~ۘ7m]Č~2g V|F2-cY1YVEg56@cehKoPxve -G -r+^eti)̇ ߶LUvyWMP*սj4ʳrUS99~nբf@ pWKXN/`^ @8)a3/ffl^˹-~**uv4{Wnuٕ-thFt2K` Py;Nn{7M.v77\ĵ7TN(WRjgpG˽_&h'?mM^!A918P~!0qGBAlp.|7ݾ\Gx`K:9:A$'1 -G f:GMhI ކ* ]{.CvyH8ZZg8 U|J}'|/Fk~Eo#v{n;tk`3?M—Nñ=]|m--M< W8/t?úB9sIm|y=C魇 -ЏF{ok:KkOB<u:=K[Dp\џDlAOЕp@F=+1ɤI *!q|@#q8մNjB)odJOXWGta(V2:h䳣:FGqK]k!*WmWxvjgvBm1<{/H.ΐ}"1++YO䱜LYvNATúuLM&آMG2ӤO<JpW0`6``6` -$&!IsM4I&kf]zd=Uuӎv6դQҺN:mkUv޷dz{>I}R/xW%^սŋ7Zʥc:\G&dQqXtS gb"㙏5;e2|+ -O =.V%?{ewV,Y ,de#l33a*pN79nek4y g((FsP;."7)R.JŎ].%Yˏg m K(dXѢV 2X4Lq턶GIPݦ2=Ke6ҿ7Q׾H_Ny5K/Ib$SCrM6MNJ)&X:@w8]eos[<7C_kҝ6GYyҾLh_Fͱ 3k6Tmqeioi⧣"D{(Uh:D,xlO}fۯ_\DVyFWf/k\2,'XL5v IM[aS4,d +48/QxKEDd'{VwQi> fѩ6n5zqmIޚNuk>VֶJTzx#f(-Q[仗G~C(7_eJ"(YRZ X;TvPљN3eՔ1[(80EQ`#.x O~S -U..HgI*1'k*j;ʃ(`KO>=&z(쭥z MIv Y =DFۤ~&~OF'dDwK렴ĔDPKINA? L!w("d U9@pA҆GI#ydGΈ$ ?KŻ$ }*wJkYEHM%ZcUVQ[cȘ06HD:)y$OyZ'$bcxMćOb_O7xG?#~<Ši1"ѡ5UIJQ٘U!}z I$m8Ms`/68e|/Hu^dD~@cL<0""2 -* 5"(Ȧ(( (8* -+˩₩1n&DQc%i[5ǦMjԨI44>99=Ǚg}T:++Hϖs''- ŏ;q?>Əq)S&ժt"_u~uyzYWz+TXGO~>/~طb-v7R(=zB>C,N)V|^)P+[]G9DFx!Ngu%yab Qh@#`52yi>ZUƏq@Vf*%cDuX;;M,$ǩW5Ġ1 -㱟LVUG$oV*V[rcգ_Ks4g [{/^g A' -hEc)hdc)E -ZV,""[.v._iswr# kG>>wpelwUSVw JhYG%Vu.ZꚢZL-q"|Y܊TVjZ֤y-*s?RwTcxJ1lD%G(1,V aAي /иrF,؈lCuR#~=;iAo m -1 -ǽl09C"J (EӸъQ)5UkTtFF[4b0dǼa1|`!vS\7ya&po -K#.M ̣>0dQvMvD}}GEepcĠ`Ԉ\Ƹ5qiFkUظ/MjzbNs5MSTk7IOsf`f{{K9YeIPfRIIHJWzrҒR%SJMJM]j%7)MG`A,W}́z@y>9JTqd2јTI)& -`ҧ)1ݮEXgޭX`>x7e8نV7m\30*ǔ.SR3(;[9%ʩVdLE(}t jK4l)w)We 7v2l {Emg6k|m~sn0(z8E BװH~Rr_&,K8p.+*.]tqíAaa= Gw1]_5 ͩPFs([\!k\>ZiJɩm*si-䱎jb`;6{[ Vf6SDEVFr{ 6xh$2.c}cc}ǹ}7TGKH1Ia1y5빑oz v^x2 - 3#jrK y36 Y+0;g6~K8N[ u?E\vih2@o!ނ18I59͌# </W/RK ե e_&*F;Djǒ7pjY`\ U -\eN>aFї2gl MVżuؠu <=w'-]U'mu}r uvxa}k}Ӹ_C<ω <74}tWE/JD3|t*Ш-6KANw}eE|y\Y"qyW(29?9<{=;BDzQDJ^Gt<ΐ))y|X5<\i0w|G'X4HG# |4J=ͫ O[;i$Nb''sqbױsqiRM6Z:umU]K+T(L\Mh B6&B Ć m0ډ3??~:w}~{cc/V0]b -|Q_75O op}$1s4WG - :kѡ\i5ϫ~j%?L FX0i*\ъvif/hGɋ*ɒ5Q&>d -eEi׸?-Ye,-5jԪJ-ЬyC =ij׌!ƔiM5a<NjĮ1]ר鞒ה0F,Yڬ^FzЧ}c~,lZsLf1;5mnДEami21˘F-Jn\U c>nzRqU Zju~?>./8l>Xz{f,3qږ)Q)&iU֦-,xwnm~LъksxUa -WާyEit-<3M2s36{f 5dw*nנݧG=9bVr\Ym(TO5wU:koÇgZI"r=I8ce^FːH_mUPY^g8[R&Au׎*X;EuTo͉[=?kh=Rioޡyd,}TckDָTߖۿG.Ljj7T2|[/iW?ճ@su~NB/ ]m|5j RC%w{jc@霑sE՝GT*^eٻހ_p7ά濏YjN~#?yJ -ZUVnTTeOLCS-isCO,| $[[&[=>Vy54ИVA:R#Շ}…rn1*bQe\HnUCe٢CFS]C;'Ḵ{Mb?9WY73hzϣ3N Aۄ%n໣RU_*KT>`RـUA6 j`#e )>s2/]_SIǴ0:tf|0^ B-'F)ՃRuPAED6$dLeHKɘJ 'w([*H^T^r*7C%~(cFgJ D H~hObl3ɘ*QIʤ&*Lժ ըT@aتuL.EL%*Pl܎3% Ce{ˇvLJmϳ?ݿ}" JH%:bv̠RѰ$H@phĈ`ּ>5&ym xyX{g(b5 s/w)1WΣ0JWAJc6ԔG1 #uơK?C"<˚eße.o-q3<{>Mzmx_ShB?ʹ |5[By=g®r'oϳ.0gK2{9 2{2r{ 8|oaׄnZr1xvfK04&{CYi>>椏 ~q>J%?A۹B>zƸ%9j]cF2ur9ACa?/~곟B;i8'U9@mcAg|FW(ćW$ ^~Ea{3ظ!'}q=/XRl $Ip.G&& ҝjKt>oKOlH1ӝS{7$ۘ~S M̫2ґZv>Ϫ@VOS;tF=ğI |ݞpOѩye \0]׹ i"'kL>RXf)'Z:%t,ev+-H|';!.'v5LqTa'&3iB/mt9.hXIdn9L?Ev( ,r5^qOCr1/$v9u&q'-[|c!.yds.3: -On1.̓ي -U2E|$E/"|,||\q7˺LOgTT2CeO8[S6[.R^/i8:4D# <4(GJ31yJ}P\M曓Tp$:`v [6 jV^?!=8-:qHCh(fSwԫMԡAS4>. Y2a ݩЃj -=!vA@{ql5[=0fO53\6;ܠICtgUaɚR{Xi Tkh79|uq 5D,P}JEnGBTaT,5VŶDٜ*e/Hy&)7U9]N%}Ik2*#\gsó֣T= W|$^h)Ub{ -Fʳ'+מle'I.;FY)LTc|Pr:#x>3zhL9eHc_#yVR!: qq)ˑLS,yJO-QZZRL#}R\ z@IGeǕ6|W<h5 ћȅL|}^d+ -W\QhŔX]tȑ_$4(c,J*t=TO\K%7MEF4 gR]AQg]wEЪ(* --, -BmăD3iFUi;1&ͤNkNc̴L56i֣c,d?Y罾}FL+`WJQdv|dȕQ Jv\*C ~;+ιOcqX^8V±`>( *id_+;IFYIJdT'[y*u)ڋ'/ыp| <<_h&q;(@1τ;~$J ~dʼnJ**@0 :3"$ * !yURxP JlL_qÿ~Llu1JXbPt|R.Fz#ìCH Njų#aKgpK-/p -PH9ĜE̓}O?/Q_µEgKO F+k+:w%KF.(\/Qu`;ϰ-DMT\~vPBsy&1O _?f4`9VAZM.?Ppxs{Ez3r [d!m\@̳p}jΫ)$C7XlaX?X6N`LM6s6U|RMySpw+TQ"͡|ի^3uK a·A? XWY -q/O=r, w}qKCM~'q~g<>,O ڙzb/ku?#|agD:a/Caq0&Xku7F4(8!8G䠿&M sA ";`4"hu&x`x?NsfO8)w /:r΄;M6HhD9pɈH#88rpu\,b%% -~O -y.!MwAQj@|ν:+OQ8|H❧I~E?"sphBp;C->Un3o>$}|QX5=:7j ~{=Hj=k? -Ux3z]W]Rt+pk>\P\fFi3[GP'^uz|:z:~CE0-{/J'i : A ƸE+Zd$,%ض㷋\DKè!A6]Tyxscu9/pޏ#N[f|a -Gb]m;V]a;l/nvS<7v#dr EA+|2;17bۊtf.v#ʎ^DZ=B]F yBz}d%,ã%2vb\lQ*'a{:sυ.#U{~=7QBy5df'ީ~.=$8#`; ۓ=beد~ ?:CZEKo -rzSL9q,Ǭ`#vpFHo~:b&'2B". -8p@wtұkuԣj .3HxU32_ Vq G-*3VÑG&ȃceTY 1GT5Ii -De=G(\jycm+U5qr ?'L84^zJKXk'/SIF-6X3k,!K.l-HWMbHQuOzU&.UUfRqJL/tBEp |'6\p-^~w[62UcJӔjTM3Te|S**7WUV㖫hjͳUk}Eso*!=pm`cmzk.|q⛃SbUeMRŢ -MI*NS5[ֹ*ZS;IyW)7urR٩O+fL9p{HC -U |w*_ԖTRST:A575Kslʳ*VDEʞT5#}2.5-cD55,! ¿4`$|e}oJx  b I5AI*;œVYKfnVbUQyUۺuն]ﶹ]n9 d'y^z|*|̍W%Yety-Y*R OGrjU(Ek -&-V_vl4~PVg~”߬8Ki̥*PfYI(/TzT) jhQjE'Uo@ɾA%;Ċs2T\*>W?a;Rԃ|ǤJ pϊ|THپx&')ʨLiԪP*JnRbuDŚXFwlU|^U կ+|DUݬmo W -TP *1Q -<|.HF3ńٴF4P(NiO;JN3X3.kᡖ&lAĵ)0(41{$f[3K7E,^mfv)##ψvl/ dx:4z0^oQ&R1&J ȵ Ny=/亭Mԃ>!g}6blS|s>imd7yp.]6E,`c 5YQ>9fq/r9br9c/[yfg0% .mm,o:HCYk7f-Pl,`'&'ߡOQ!zt~"'(~sbϫ5*]Msv,!{_3hl<&Bh-TlDŽ0 ň2=r?F(8 -a:tPuVr4%-|4.F&1BJg蓳q\E?OAr3!pFpvR#<+;<au:Qx\(.A]6}fJ#+{^8i=syS~}=*:+G /P]Wiԟ%.~J~B.i\:ops0^/c_>Q\f -4G5t̻jL?~ʹy -JCxЙOEh47jvP}hũ߄3,ji0)(' -L5{ #u̼M`pEWhT՟W<~`;۹v0Ŵi%mx} %rǘ as9jj=7{L`e R5:%.Z;}Q`O#6Zm/u؞{݌VlEݥ Te е/iVқbX1\G.t욱k.l{]Z쇰V+#]Lb -Y:1~6ktv 5bׄE g?RX a2)snM?ӳٮ:e05&9(Fd}{\,XH.&=Fڍc~t!셱ۦv,n/f:z43UaKH}$A+oX&fp:9/:jQ6LC8JdRruaĉc;ǗN8NvlDZs:M$m״ K֭bBJAVSV1Dm0؀A h*h6&.ZQPG'e=:3Hì1V*f젗 c%Xz>A4lsGX 㰔gKH ;;Ѩ$:u42to>.& zg=;6%ʯc³x/U|8fwcniL".|5ը\nsL]:Yuv0-WxZ(m٣fA,ǔVr vM{RaG^{jSWKVZliDҸZJJ[;lWʺPɲ1%(n۬mjS" -ۧ:\G8N -6 -CC7]'caVDY]-vJ~%uJأjw)UԱ@ 1E(llVG!~*h<G -W^k[KBzNUy9-粼u7 ;\MZL3v@gi%r1O5m - ջ+rW]'OWT]HU+ީJ.Uq}\Kryr{oj'荓@.pm4$x#FE[תסץjWU DJ[զ~UT㟐ۿA |EFpJ偋rPOtk#Z!kR]'D~vy*婩&.W0#gGڅ2j<4)Gh/òRYcm݆]h44O#"YePP\u9rWɨUy}4t'[d"kdlUidJ%#DN *\d ԿEسA,$!=P ˀ91B4B6Lֺ"og4t@ mM@mݍ>T𚱮ib8d6cLll&qc|-0'3/<~w4\|tzFaɪ{Yנ6t-#Hb3ı8VjXCc1dOT -3oce}~z.hE75L\=5-Ch,I5$so%{sIFMı817v0&;XTVfH3׆A!s++z ))"ö[/:@ndwt/ ıv?~ޗ}S) -kyR{꣯s"!Rt{^sk^nh -Ƃz8K!Lt?I!q8feep#TxplCN.a0UXR|e>oH])a0K$SgX'0ٟq%=y2ղ1@ۏk#VR+{ @^y3xޔT'Y{.o?$ %KE&<{ŋsgW ml}y`}ò{ސ͚:Lm`VKs%O,~ccl:W {ś4썓dŧpO/yC/s /d"oGG,~~ͤyIKLWW^/}_%Կ,jg'ހ Ufyw?6sZ) -:2qӺ{Esxq~&̳gcۼ8m~v|;׉8iM鑶뵵)F=Cݠ$@cL ILHCC􏩈C$PP}~{<$% ݜ73 0(_fѯ=MgP^ O߰y ކ!$=~7V!Rd cse e:#h$>+xyK+Dgt*sB?Lm* у_u]S25t,v#Wȑq?>2S{R#aCdC/6k*< 3ϋJ\;-[Cw6Н@wY4:0 Gt7)T 2d V9-hm[=c0g!X=GG xl'[p3=ѲЄqQϰǦ![[-&v؉c'vة`;fL$GS\VY<:ށ(Na |ayjiȓ*ʝʕ3ݔ=$[愬epf(Hicc{SP2(:x$!(*n?/UK/w6$gGL*)r F%O9s* rg}-ckl@%!4 -AhߌM-]N9K-uma*$MG+],ljj@iCePPo)CН$PnNS!6J@e4U6]?MS'hu>[w4qu:@zJʱ{-hAz<2Lrr®Y~ΚE~A!ah66@A<0ǀfq&m&А ឦ ]` ta/)q ĮQaE{HYaNaV6 3]Qg6{9d7l[ Pb F -*e(P*SS -J/Pʥj-2 ʴ:ڱj 3Hm-ɞt;oel?V~YpYKbr5 c̉ջc,NY{&Μ&38]p~ᣴX,k:gHL6}?ѯ' v ?mI[-~x;gr!q68wsΕΒmQQ]·˨#rs[ 7c?}&{vdVĻH"8sIKi&xA;.Gd##h^e~WN0?HH3(qe3~VpNEj'[ٜ;nG<$H9X< WU~H<^W^ef\. euqDINۿ^p᳹ϏU6K<`,D$+5>>ɿJKb&>f- | -Ol.>IQAaM2z 2zQ{u΢k~8 p ޿z]uq-l$.%~u9Gem~~|?D~bz":'~BiUh -^VXe]SNڟ&hq48Zj%v؝lj~>^n.NC)u}v!~D_v<mv\pǝ;vd`IЈ"v;;eZu&v;#bl/"Vc(p< 4z"%kЙcp_/;muiG:ў؊ @ENA{;ӱ;arXeQÛ rW+b f8S a@䩾";=}ll>B~ *YoaT1v|*8=ط{Lcz\cQlz+۱ݍ>l`o6 ;s:>GNU QuCt~1lEоkپ Tc ~o~;@VdjYdg:YG-e:5c_ ;~σaWuMC,lr2ژT2c^y;u£)TE G7Y.wmkUh9WJ4fy$;B5ur%X| EΊ}ṗs&o/E̻,HK}ܥx#+iժDb񠂉jO˓˝lSMG;lqf܆i I|HbxSGdQh- ϻ|Iy"QX+3SD~ -& y24Xr5 9gϢ)K{caq+X³Yφ$/"\Cedj(fsI>'ݲ=&=#U0?;ӼMvū_nF5#\O&~mXflؒ! ||e6;A+h9/)>O&d\25 -r73D V:HJW xmǶAlcoC%K"K+>|pN+=`hiy׀)ޅ~F5}faX5 ZZ" -nUƱ3h:Z+neJ;=HYB6BIH@P !Ѻ/NT;նK2x:ɇ0p=!?}f^LRpφ`@Vr@G Aw"0<A!\ŜԪX<71 1 '#hGw_C0" 5m ṫ` ",B",BPGbP !BpS/ t3Ϟߧ"$/0` %:BrXa`F6;XApٕVb\r>i:_PK -:G/Ґ9c+.q|h"|X ~5.5uбFl 0a|x=u04.zE4)x C$Hl- yױ;'jn i\ W8tl-бk؎nA pNlEMlaY6{ר` -Y;y80_w97=Ecg@Ҁ= бQR$Ή {P1j` B΃Vݕ Yk`Õ(,7U -U+'F|` - ^EMB@n/+iQ'B/ paT/D;C!XB"0cr>Q88/l0݊M?xy~n07|cǎ0q)SMs^(d^^2l/WYn_zWl۾ܵ{"ވ־o|#G?>3L6ğ=w>1)BY"D-U5ڂ¢CiTSźƦffpvv]|nܼu}ŗ_o~OD%}y1<\'_ gK"0X8d$ D0QPp)#`@L6-F8n#mO@zH(=&c̾dݽz~x FEyy = % G X'$`(,K?W-=C o"[ ;=Qo;p0ȱ4Ï?!Idr -bXwAWM1 0 -z޻}_>xo=z;xɓOkMuDT__ba~CٖsJ:CR Z G#e&\WfHKi h0a@À 4 w|kfdKeUh_ݯAųs94HASe *g)AxӀ n_ToO*HSoTb.W]ޠZA Р%4(ײ3n膆>nE$YL!`*_mԝ/QsР 4y"ySIfuaƹgc,i0,5pCu~S9Ѡriȇ۝+]xWY"Z:ӸdM3^Dv 97V0N6CC4N۝#>1tdBG*@C'ie$5hͥotРUrS!\ʖrz$N:Ҡ#{脆ƒn#Hi КʷkJ -A˱)sNy6K"cwgI=q:E+6 Zg -Uo-/4CTРa;rV(ՕБu9'_4qbf՚ *ʶ̅ڸ|5ǢT۳,8Ȅ#Eƾt^鎗{<6XjwУ-VZzQQYkF}QLVנϋIh4X$&}49߻w?cW{YE˫}?Q -˱lpWDL|rV\`ƉмVmӰi4l6 m{Pdžg0|ǐ0aV]ց灡F!ʺ[Kn۹l{`?)`oh@lǧ"sf\޼-RtɌ)Nm-në= -5e'#1=0htHh#EAg"F Vh•Ibm0;;6 7`2>A :SvIQĢU]1W B% OXoL[n` `Q/c×hޫF'Jcs_+!DtU3(˗vjYy`xN+1™-x[VJf AƻC),ȗfjkۭTkëK/ck$fLGz(6lj;^i<)7m}Uɰw>&t%4aS&Hsĉe!e;l[԰0ݸ/WioƮOW}/>{cI_ᜲks,p!m,g9@Ov.Rgu6A$Ⱥ[5X=ښWǖͯslwrl$&";$&,aqJ'=ʲ[_vwMæaӰi7X?ښc˖9_ 0tJddD'%x:,&rA>'>\0EEh`NӽGWpkz^`x +Wc"R,Bq&<$Lci7_uA[=kV};Ǘ/ b$fǢ* ˱\PW@i.wEfx΁HmjiHW#-]`0(̩ IHL` HwEĴϙqrxsvB@E͌:yn8~ ^I3mfځ6MmҔK IJ qCwI֣yGﻭѾobKl˖%[^ p(t4uU}\?ɩk3Xb?<1{B1 )ʠ)u -e;5+jK״4Œ^S5x{z~q_=a8 ie/ŴxXj(Q@ӨʨVf =[rSPԤtuEhx{~ {/ͩ0/!=k[8P&ڪY V $7yMRULMogn`##4n%ubD@tPf*haTIȚ^ʸ,oe>OUq x -"8g3h.PԗMЬ] U,*WPW2M~K(d+\+x{ڍ^o_=NioYz!pg'ئb -Z(e^ik{dEDUۆa}B{_k_ӜB3sޔJ(6y -%<$iCPMAcqd"mnf:p~0HA\^0K Ì*QJ 82Eg`*)=P3؏6r[h/w`}o羣=\[u᣻nj:|ͶoZp7ȗ|ImKu:mlB%a50as5ޱwDGI^{Ivx\/$ٝh cD,IFIdB#mZ47"TՁ>m3V?1Yiޯ-:B}Ky/eN(^, -юd,A#$9Z6mtoJZmio=aqS5ݾ|OӂSacO0.v8hx'#TQ*LIHLʆt ޜYޖ~0˪a -аm=ć“!A)# xB1B 3QFg2R!@ R`, }owYr6[iì+auc71'R 9#lD}qNܱqZӝNUzuuk@zWEAAP I\Bx $F$@BȅpAEVԺ9;m-ʶ?*9M8bɢv:jh"(VV@ߠTei4EJtLpavwk}n䅜4~1+=n*(NU -<L;sYINiBx6 -_sZfFGܰZ)HB':!TUr_JDot$ H\$\VQ"Fa]|VaG ^j2#(Q6"*r*&!i"$]0 k A]0ݺ4!>DZр/rz[IV-9~`qL45z]ECmdULDD](ՀOICVt^DA$"C V[+{$SL:Q 1hG 5M|CF^kʇZx3UAPi/  n҄di=ۊ~i+zd%C6@>k\OX["d>Еq]iB6gx;iذ% -gd9 $*MM//uxUakfR2ȕ\o`*X( 0,OƤAq.<1*; O[T{j8lQƒ .3&Ba:A8/ W=hS g4IC΢/}ڐ:=kJ]* *8l]Kh-nH6j &_ciS 3Ҁir`xaؚDy]Mݧ 1M&o -Zr-s.j)kjTAAdGO۸7`pHGܤM$Հ!o?f*wm2~\?h2b۩z2lnʯK @1'TYY0FG)2UhӲ4`^2nK֬f{}Vm&pҁ-ZwZܥ5UUz(ԦMjJ m3GrA A%h4 `Z ЭlVy1>g~ |y؟~uG? ӷmHozޯ'|%:WS 8#^87Ѐ`SӏT]=r{L&u~C*gN{i%8 dp?3 x \aheeh jOy`~RMOU!KrUh>Du38lj,J0pzT~ޡ{&`jmյk˦t˸("R(HɼX&QoAqq˓2,ah6EeX=7eNަ ;63e0uOɧ4]jnH"QRĀvߌ帶la,/1 G#Œaܔ>ehi3~1k<ʞ1tȧiPQ'5D^LRMl)l8q(˛G| 0#xeX+)z 9Ys{xJ1?o'ud^H2kq2,9ʄq00|hahe>o옵fϘ+&;jpQNj$ -%h>鵐ifqfb5\Éߊys&``1k{ڦ4vTᢁ/AɩYL"2B5=+ v:̂*;\q`r!=\= ycʚqOZO:ᢉ/),duPbM97Fz\Wjz{Be7&H΋ ( un̬uyP>8Z?]'[E(fjY1)QUoh"^jN^l^$oGs4o-Ҁ28>u9Ƚyhlu^sKO3;(jzIyD. As\5KT1E7w>u>3mu *].NQ!iWcZDX ޲=7B^UtpQ+.hD2-hM;[l'Apd:d;,{OHgpj]<5jT:hCjJ -]QI%d@ [[ߎA;.}߆w[|pRB\G;A-٤}SKUT*K0)!D=eRoh`2xo.cxk{wt#;]ds=c?bv> k6`B:EM{MDZ"VE -Ӽgwo2oM{ ireӇŢ#3PS - }fj;8wym>3tE`uÅzAQlnwG6{xϫkE7]HH ~5_8ɯs뜀gq+>~?>].Lm`=acܜ>"ˑ~RJaiVUXaS/%(\bxa@ @Յ e nH\tzK?Y)ƶX f#fHvuqҨFą^DJ +a]XH:$?y.d_Y«ѶOo~~ZJ^]rrj[Eۛb.A\Ԓwͽ xYbN8ww`{-CplInF'LǬ/F>-/,zTB^O>{.V~1vtnYHI׽{Bc{C: >gώP:}$%_z^US~nˢeϪq%kҔIe?R˒6^L|,Oxri' ޥ^y/ >9}Ǿ+22AnB:@$dPɈğ?Ǐ۠d~u9;3'ܝd}/Ds;d~>O`?T.@WY4v,dG$xPt2\11 ЧO| @<(1>0nN\x??G )eUMuƥ6-k8b#S͢v횮馐J~Ү*`wo2`i(`!8):W@KD|Ъj){g3Wzǫqdq 1>, Ay-"8YhfNS%o_%B)X7oǶ;LyeT;- DA  p[ZT ͷ4zS>KkL7tDa 3fY`l^{j{~8 &x@ ?= -R7 -EUne2^dQDLr9I[M#D%@P؆~?VN8 o @A$o @ (pM@/6,qkًxդfu㍼*d %vk\Cn\ӂ9Xgh ?)lń(9 -R7DkPPqKf9T$Y?. c(w 5A3xی{6gsv` ;llHklԪa *,ђY.I38aOr791fkpoui6ٶ0 ( KJlK-Xo;_*%/K8 P*cK3\iaY< r|^|ǐk2L=>_USI;İ 6mNH OHT$+U=Td웒rl+Z3! 6?9(zI!73`zѯP^e-'ڜ2a@d#LҖ*1:HFמӼ(/J pEHy,pWt:;7 ^)m.3ȷ '=Zs&6qg -6q[ͷOG$$_py"!hgT6! !E f_+Rl.[buũ@36.}"~'>]W6SL - 1f񌒢Su<*qOhfuqi6gAm8%h?w=Oe4Ĕ=1a$P[k匭sH_g7)hv!oFVϷ0&96gtdul`5( _YT8PG]s߉5{4;~elH&{aL0Ejm<,P2|sszl e1- -?N٭s׏oPʝ~w8 JW14Gu'C0VЮ#ԫ%JFWV]R-fE`%la*2 -& 7Ym((C U5XB~dgr[7h~ }hč87w*A?:Lڞ64^or]҆Xѝ&jL/RiYvCA)Tu6Ae} -{48=?pkbPVg(3]BGiK{hnzicgXeTCP T!١} փNt[>59w#;vމ)/)+F $ev+Ӥ(󻒔.RPtSj]Τ -eGrJc(D 5f&P}j-~&swl&n.Yh)YQtвE~Nkbr[iWra;=VCjRic.TڄjP E &P)46_.K{OkVW<>D:Ewa>r:lHd(qm6r[uKT[|ks+AutpP.0Vhaf' ,լR:!]: sep1"@L)FK%tەYݑ@ 29!kZb.zۖ7.nޭY["B>ߝ1cEGC z)?"WWc{5: DUՄ/ -jDA?iW7lZ7ʷ;[%NJd&Dr'IY\hR60r-ʺ6WC`}UI$P,1oDAÖ/V:eņ-`,oY/ݱ)|! 1iTܽشDιt^73h0!-/]6(֣5~c#턉ӗR05nl:CLy! a1Q_sOq!)%5#03g!0̃T2^6:ע4C_XW L: ip='>sCa@Ci4kP z#T=saTؽ;`fVg  ;`xN@vvG! R\!pJCPy8Otغ.̾߄?m?.N8BpDt=~8+[Z!H[Ck#`X 0- - "dl2.b" >c @gaЫ\BXK&=ה%?}*_Ŗ͐iŢIbhX<" JFA0(&~> C e Cfpc/شLVbJ-?k.A7_"NDˊǣ%cƒ1;;AͲ^bYgT2Cb!,OK= yЫ7DvZC&3O&L%Hq1|4JYqZy->i':OJ|C> 1d#LĐ3ѫorٔTÛcM'M$cؚr]0IU=uf# ȮZT!΢<0ZOsjӞqkuQj-"eA` @XB$d%!@VI %$lj@AA VG;ߙuzݼ٦$DbRfw9WiQ^cUT-U3f5URmJ*0P 5ṗƝK@ޱ C? - ;61|3$-!xUF1x&(bJfX,tf(FނOg5p}o1(f|Sv/%V})$;͚F.MeםuEmvC'hQCݢYаsh],^trx77n97Lw@,Ddu,B %k{=eե:uS.uܥʐt*ڿB۷/7&V,tOmx} o*<^DAxbyލ0>P,8OkĸDT6.HO:{9F#OV{xAW~%=3ϭ/?ulmWۂ%/=J=:U|?HdeP2дpy7g3w{jd8⇃ȀX <&(Gdl1?Ƞʨ13?3vjjqBn8J:j`G'`21| ;7`&oPh1G a}C )ȁedD#O/6 P{]䈪F (䠀Kc.#KqgKhpu?ŀ׊@ؿtAC}"c_zAW;(v@ہ;\BPn  :w#-ya~ C'z6 UC_ - B 9t ;{p?*NN& n -nlw p?8_QC< -Lq;FVk)+>eRƜ%Y8ωgz4Q0kMa?M47q1콌!} Xu;1pC:b`!7Ey!%x„LiRK33oT-"֋2$+Ill2_;$'I$ʻ厐7Fz, \ GN-M"EǚT`R%~BL&6.dN(&pG~H988l' +]mE P7ȌE2&GrpI/9iγ"Szx2*}L|DjP'^81Nh~ʾ}8K ii1U vp9l Z$N0gy4x2L6AT'f=$7< Kl#&s)' /S՗@ A -N*1hb d| Q&O%xΗL(Ɠ+jU) QS4w75}M{Ҁ6D6%h'h ĈADA pm|("F-lTže 'Z88kaVmFwII7 -i~~~}FY;A2 Πq@PB ^WfΔT! sF.JsѯzJrИk8W\+e^_4 1b ,oB! APw}A"NUqSJxBrR9aC۴s%Ime]+nnYfSV)) !cHɽ_oCP% I/ ֔J zP*5aniԚ>Z*|a98fkz.7q{ʹ=O@dA (F0aDY0H R'uJP - ;-ִWSXmzNf+2~D]nt1k%~fo2 0~Py]܊?K -ՉLMeQkj\rU[kתKmVHaыLzqWb1CO@s0 -&߷uasQOԑLe-ZyUqR+ -Ygԕ[j2ZkkU6NQt.bA&b#VgL{BPz7CF7}V3GvHwVeU+mŲ.5[4my6kR-4UN#rH|jx>A2 91PRo<݂x.NW@Ʋ5΅ʃvz!0$lŜ KHH"N_Ԥy=Hzg04Ay,Ey٬,G} "}bg}OXeeK'!vD _0Yǩo"ȋąs^kJ86׍z99`t2~@2ȓCByvK߿靐E?)ԯ&X׺5\L^sv:F"ed? ƿK \⇻)t{]ue5yn4nq2ueI 1@&d tGeɍRR؞Z`nvb, S!O" -Hu rK}*e:.װ~vxcOѥ$Z"oieLMoʲ@[ F{^ ؙΜ.zD{@,D۵rZ ?8rD݁A bfL6lL0V;f`Kdp3% d 7 l+Gq@#[8ko G-x -,=j] bOrT!H4dT2-pSbj'tC>ZMISs?Ç k -LDFr$j@#H$C!ױAU&46Aw'(vGUNkp+o5SB!JbD}ӃP*CD}qIE3 aQ*qGt7Z#`&gV[VpV0wEJz@٦ }}/DІ.ݐr%`U 0j(6 -pUa/S 1f-u%o/&|E@j R|iA -~9_y" -c>CzϐBT0Bh2@EjpB e(;`uzP/R e@SWI-A+vw>o/e<{g@|˚]b={ǖ lMi24kp/70D'^' RʚBka~mg}#|%#3a&ϰ&5==-:+ZQԣuTD+ʅuBf! H,'$!Ҡ("e(U(Lx@e(λO}s7i /l>BG/`X/Ш[ DՄ.3#6'=0] 3ĉjқ:kci!i{JFӚ0#NI@Z -݀xr 9{"=qH{\v[laSBzYF -Hz1|`D>e1̦X 5Q5P7y7@?H @O< qzܻ,\>5F})b_d < y`ۣpnapE?tݦ,p89 ٹi$,~'<=E3ch/qǘcӬ*h䥄gx=?1x~M\!_;_[ 8> yȷ/5 Yt Ac|bIo#e\=;0 cÑ͢GV\_͘>؇:Cɹ>q%y?h] zjPo4L A f~ 'J8=leC5Q QI^M|or=񁬊@vܛ|ܛ,`:jp!ul,Ap#@䐏bv/f<#|`l \QރR܎V^N9OJtQ'i= -G,`Ow& iנ8 `ڹ} 3 ѻkJ&DD0 GMIT: wc;rjޑnct3:S ])lG en G `2w, oo~g1Ag[$KiPyRT'5kkCWlǷiYjl|(9Uѱrfr% 503o':M,s&[W8nR)UK]^6a֖ 6X~%dgEl|AWIg)E - b K1F|q B̳(V=1mxCY0;̂c&εk\,č `rlLjxcWʴ|Yu6NQaK:|a6.ݮX:ҝbMf*7CIC<\:W{}w/<صSS~ՍuquDPQT(bIl$$,D*0:ŒZ;NZ:nǵZP*2)UdK9}m^|^K.7VzaZjBK5}F_\c<\mzGiafӛ0ܻ=|j|4쳨Ǟ$MW?l{I]voqf"k[եm+UnzZh|:^Eh[m[?QIT"bŋxFR.p\T*m?;1te!WrΉDyjx,k#]!ԳQ>ňX&gk *Y>cȎcd%rQ)#5Ңq+QhG3bwF-!?&H#!EjZQq_qY_iRH #ܰ8΋ŊhM\ sp1nq9fG!~%d͠3Y /RLtFkӡ\Ob ICo2 : Ʃ:KayU4c&ϜBp,4? #G2_%dBR+>a.| sxF=qs@ ݄Y0)։AXISQ-~bOqp?;"s;TR4HH6•%t0 `Hp\"b4GvnM-13Vw_,Q1_@? `g]!gCzztPh -á.r=3'CM*${yCBEXtY m Rw26MV/z/钼vH?i3 lhS`¨DFf(Ь\_ܜvCrH1D%3O ;r,jߥh@aEvy7;S0 A1lz, -8HA6 MPnK|bH- z9DWUB𘂠z'~٨]BfoU A %@ǰlr2p`^cI<BW(w8 V)%$uWT5!zJ _6+_(ltrH e&f|U7h2}`t06 -cP2A J$7?OCj!L0lSAG~DuAYgV\7?QtR6?I:?K 94d0 \`Qr$TOCl6Vh%o eLpq__ӫڣI7?k~"-ցjWuDd !I 2 hQP(ThI ǭ{{̋yy~y$A'b*37EmJO%\OŚx4C  b'iݑ/f F}KF-%:v22vfAi:Oǡs=_H`0Z:*J?,m: 20% qqChmݨ6foT?'j݆49u NU<*А^ _b`406YAP24]f2e\w|D x~j&TxXp%=6s@4j rѐǓ -) [`bc1` i,p<f;/_ -|A;sT!5஘I 7X- eI$->CX?\Ij(cO3 4#76N0 Zd{߽\ml׷m#šC.9 !ƶ˜LV]Q[j6,KeDŽ =<Àd0 x9h@ZjKf{p?pjw˓S?+<ڕߡcSX8Z-PKj~!Bl0{R2Y:=,VGr=/mDP\s`z[k sBfjv,t^<{ j]7wZu@E מVET$xb%Rν)S $"B˸D5ŕhڷxHGz,߾ோ;^5YovYcS%]7+Îj~jrXUPPl,S.)Du2qrgH\&餢aH8, DO7"@@*,XSiy}-z.h umǟѨ1yHJ%e+f% b~jږʑ!K餈tXHFy1_d 9i9%FWa`FN֏oU6>\w1ҧ"6TU"Oe!<32%Q*f<%Ii#b|TȖ 8)GjD́dtm-,_tmkŃ]_t_w]|`eDAmLpfV"tnKR%q)yI㲇%dՈznLHK -B@ -6X֬6c7WG0}wv]:֋5-a9AZRNV -T#$Jđ%"\hrLǟ7J#rn<[%/sڥY-xg ~5=?Xt,S~gZxB/sI$4IŎ gj/C5z*4 F.!gCȚ0 -Em-xlۀl@З}pƁ}U7ܭ>"Ϳ{IŒ81k5Rji`MK vXQdbF0 v<[_o7l@陣UeEmz]~?hn/$%8vC2]$ow/4WԀWKկh!Ab;,å` -tYk24cGfMcݬ?Q }#ف!'Gz6⼆pq^o 7}:Y0y!`XNKg j,eUL9or^!p]/?4$BQ.X=㴞0&+Am;2]>0GzbL;Z hk ,A}kPdk-[me{Vg]1f=Ϝt{jx&{9:jo|}{׉ϾGt~;߁pF:0Yc>:̓|ޖy9ӡ7Fy:-p.]gQMy? q -.,* l!!!{ I 7kKGwKU#-X+:uA=zL[8 -B|潚|w]=hil*5{.]0wp3GN RqU"֘[>asbOn"){>G6bڸ-Gx}HY|HC4ЄaX(AQ> a@TNq Gq2͓$ߡ(2)*%`8z dE!; qL.}6D3e|4|Es262'aqh/Ȣhf3 2* (\GAi,; <As -Ru t:3ALd> 1y -J ' JCʀÄF KTaP-!DXK/ldAV'ɺ.g Ivg|[xbd=xM4d'ѡ`1IgB'^9pGCI<ے!ٟ -tNf@x&v.Ywg!>Y/yB t&xCȀ. &E [D(@/8nBܖ>BE<C!ρ ُQx /(#hPy#o1&BPPCUꓠ4 ʝ =GBH#3 KGR9 &'}HNJ1&QOn=[}KAݝ <Ϡ#4>(:qLT}å -A1(Iy -|v{8TgP^RWhʟk4Owyw:?.)4½a#*}P23L}*QhAd$?ҵj}jzoW ˦QӅQ9g0"7x&XśU@|e渱jGʰs)wtuV+neEc88ᑾx_~aKyrpf.l=tГ|{]Ċ:&N'ؐ=ա#1+mWU]GF&K_ -n[nZd(0[mmECSC-_zl/yAo"ؔ-Y#zY[|%+p2\+9TcqK?gK:-;,J/Y_8Z4h 8NJ),9yL~#d+ȷ.ͱLlK2ȟ9( vmpo]_JSMk{As_%Q{k7%γfGpYeM>'( dȾWOz4̣a[4;Yp؛=n[m .ѕ++ۗn)ztAGd9׉+eU|Yy+׾ʾݮ~.'0FfQC5&2%?1Ad袻[~mC?h9|{ɉǪ]]mK:j\]Etm_Wly8yƟ8H%CESf_˖889v!5dl!ҴeFiK4L^XYA@3AZ6]MDj+.;fw9&G7%ƞgTF.8M$, -%tIIlb樒I^֥N{:+vxof:4 kRe i"anH^lYXVt/#\Ԉ 5=/%z*"9z&,9j649j>$)j%=֓0{"_B4{YS.uEp@ -k%Y5_qOfKf|Pw .F -&BWLxYN\;.v% -#<{+UͤHߴzrLNM~jK -ODdg%222YI„)x䇑 ~d7*a:<:~7ǎ.DDaDrxY~nSћjᮽ&ʷmZ_s2P"wZ~ܙ *d 8ᇧOq#Rgy)~[& `A O_B'=q/n&yd,@؆%`mY`Yn`ug=w4{@7|I:H5 ?BHI t`{R"n>|bf/s/m!?삐OV"xF`'!,ɹ 0z}OX ҂Ag,7{Ɇ_g"D.ǃ -QvGlYMtBt"s+]*W5Fh+ !:i__#;?=G+b `>7ҁO=3@$fAb"h%[WWGmtp:f}6aי D @+5zq$X?r'j"Du"֕ -S g8@> JdHJ[Q+<: D3q,]bk,d;2{!8?Ds3듀UHXAPAK -},N&-*unH2 _x+lƴEwÆ؃Q7Q9/9}pŀw3Wq>&!?{ԯZ{d>@V#֊ArArUU=,7J$6^Z^%s^[%*7!q+C;Q 8/DN&A-d_Ɠ|Ň-֑{@w. …lٲt[R["WQT;KRgIO{[7c! qe#C1$WLhb- -#G4g _4egy?YH_κs[+▲%kҞ+o.J{IEeW@ܩj$>đ|)֑6UTN-g7G8/yZ\ИNn}%7,ܫQ=V!Jy27ңv[V-@g_Bidg'=6M%sz_e_- ~6K]nt^7 -r 9戞;O?O9$w&8|[ٮ]ٖ2h[ͩ㲦ԷƴwI dgQ@zlZhRjwZOkCf>VEuv$ٳ!}*$\KlWv#Ir8}`ZjMk귚}#ꆵVE}Ƹ|{[)!yDmH@6o<l&} ԭmݣFyN$,P}U.+*wWdS6g4e6d\Kٙ٫NQdsqYUDH$[G dΥ‘2VrG6O]m5n6;^.{vW6g?h䷙6 -[ -+eyU; jks?լ}0RiN0-1VU0.{$mJ l޲T͡ p<߽Vԫ{58xthWflYWf6nIY\#-lTWO0vZn|Z^03 iMqTU?(˷y{)L|28k݃(7x_h {YGՌF6Z -Ě*yeNfSkʦԒ4Sb:ST41L a&.&{S͠|>rǔmݭ%"J};uʍbBf\.1M),,ոLZ^ُĀ>ӐX:)(UƔLV&Bٜ3(CU沧iFuh:'ʿ۝j[W[Ģx=rzSS -nW&./fkIiViqUX٬5X9SY׺-CuyTe4\ѪuMBXEAaIXE@0qWzZD -REAPAܵEܗ#n=3v -cNUԞ,gg|~zy}?ѐf͂1=ŧoA4ӵV+ok2?mW{$QRYk+;.b}˶S"{qIyy%w,>{I@m˶\6E~у*!ݮ3FtmuM原Tյh'ly}OqOj# Ǭ;&a)*>K_X?+w᜜}md}=@V^`O2w  Y٧DN6 u1ֳ.3&sՒ"/jT6慮;TnuÛf=,=sӪo2/ UYeCswFRևD"_IUǧ M%S,\RU\,=㰽CQ>wݩy'G,iY5-yc\vSѬc{SkRNo / Æ/?R>*FGRGCo#zTFtb=tG_]ҡkT%^ 1MmDd+/d/>08g6;>'^:1U>>f6#9(TѰ臝Dw]۽j/qTyÈM{\]ۑފ_q3m,k |VS\1s6zڌ1יӣ vyŴ#>3D]!h`?Utr뮈ӖO}[8:>˼&<ت};hVFByCx]DFvAu:yDgD7#jnfʯӖ"kNkzżr =ZkCO]JOxVcz>Fȵ=U͊t2T8w(C@u752ω.4>/N͈V/y/eTFWfɯfOxdחa3/N׷!oc.܂M |{FD7$/!5Z!Dul+Xvv'_=7-)_3{p~jZxY4C -UClw~d5IJAlbY?hGXaD|K#Q;#JÎ7n:Z(3 -BHc?d`l.ATVK\_0l_Lj*P5˿C)EpVCԿ.4YEjE( "A% #@#r A("HM׫XVG+VWZ]gߝ/g|g]ך$i VcjD0!D -hzG[Cq n@=_\r}As}F} -ns[x -ϫAy9*Φ9|f9DY@DB(KD*׌F!.mz?2a4;Na1vk -ZC狰oR # ~H{/px*ٽ_ -LJjٰb׻ͷ=o:~y_#!|\qw| $|ÃQ>P@)wusW`Qn2#5hyR/ף5n3Q-߇/5uM  -N :!x\$hB6&P(APo8.S3)mOEHd`\iXf6iK'Ed Rtv阽';' :>|$l*@zg!U 4S V, =vS^jR -\g [ͨ.Ǭ="w99)xOHKU|%i t D0^y(ewE&:bh F 0$@@)=Į%Ωs?A şS~+[ovlLqɥgr"2.GRIZYEࡄc|;+#vl6Knsc$SA -j)0@7b-ǮȳCcSSfz3%쥓a㹱.#->J ;,3*o&e=d}06ߐp]PW%n 8r r`d0q-=-@Ѝ}M>*g./.qL'꒮O+IX") ]E7!=*nFgfONTF*=ERώ\>fP陕}z;D/*'Dˡ9a~5i(akRe --D}/ -=˷Duz|o.5-Bg7߿f6x@ wqo]GSI:mu~nG߶a6޲z1hQoge!̩R^[.*KחkUM/+(L U~P^^Z6j`0pXWwT hu:yMt52-&bEKh}]m[UM6]e_Q*P+K+njQ@ɵgCe"y;B;9S w!!tC}fh@nj ՔUOguUMbW]CVm7ϐNԱu/ D{X[~|pL[V)DBwNc=fh rͲ]5gm[Gn˞YʆܠRzBNQH~T -Ș -Ht@ĖZYpـp{C |i/CC._-+aNn݉[S;mŴݭՌV"0G)js23^;B|3$toL>,u'{RFj+E^O?dr7 N07]X!@*Bw]Ad Bc _ݤt{+k/7ZT_ks76mDna-r[;~cx|D_|J>KˎEћԂEG->v8T)Nв@]n|;)T{s%35q0Ͷm@yW5;dd&GyS-<D6zvc_֍Yco,dYbjmt"\8\ۅHMkD Ds;^ ,4㹼~ocd 8= TxV{ .\;vhH5mL¯.CwC׏ma3>^gsX~G[BQ(e>*  MCraxayFc xGaw$xKp' l`3vog&_$*BM# |Ʉ@CBZ(( *.\,\ xH` X&c ࠇW!fpU3+l?D"\" Hų: Ix -C =q?/8T 籎簝'c??g5|M˾Erb(xS(b -DZDhĒT /j!8K"f5SdZm$=m2] -{ --HEbfy"z} ];ҏ|!iү 9ꏨbD2wa1xd] ԠkyXzLVG'zB9 q h( F|?b2 ?ɜgfn3~_r -B,#dX,TzGPA}1a4{W#"f2ς友#; @vȨAKH?0q}5HpvE,UO ɯ)cI -n e@t 1W͈1Ҵʀ﫧4OmbEۄ?+[+M:VHiPv}>dj3q]3r57`g0o/iK9XߎM9#sdkQ5nBN y\8 <; ?QB+ y#p!uNxʶ [Ÿ] X&wg<%ݫ:0/<8S6|n:9@틼H뉸Axh|KD~F!ZS4.y} - -|&t3I l}#fr+Ȧ0k4f,9nD$s& J{jUwQ1k n$o<.x:rVȖQF"vIv$5 -Jst0k울 NeNEOU{JX( Z0D] -(ަi0E&pJהFߍyǷ ʣl2v2&%ݵI ť3ɵD K%)^U - -/ -Es -!Bh`/ {o. -c2{WTKEV}9{[I rU:]M/6 %}_7[͖7[|ĒC_dD[ :U7JHu!ܪ5*5LNe莖˜=jС&K<\YH)ʨ+d nQnz 1!Y*bRSv10x{J.7[$5; לvU< uSTbt<%7GEϒ׳dYa$8̯~Lđd"412D -Xp;O눠kXMaщԭq-5ǷUWFRW%TVzeRkYE;')O'̝{/!s[Y)(J"j& pk0hkZ1i8f .ZU*+{H˔Ԥj<|/_|b +.1]$[=gp{W#vVvYB{>bc'ٸQ9jU#'!@jYR.:S%񫚙'+|*'88|"*;R%S"h5[KLqf`34&w3T1Lz-#6-.Y(l5+ȼ&WdC#- n -Va#FpV#ZX+*_ͿE{Wp ``#6ფ!ly +@N{Ss\»JC:՞A=q;mAԣ͈zL(Auy{oq`w0@-vвuq1Q -q/xl#GN *v:s9>Վiq\r@ o/"s;ٿ}52GpsgN kdӻ iWRX0o39jUmW;'2w(tێLݒc} 9. ra ut 4|$@MH3v;b=IQ>as7[MΦ[sf -fjvg:`Kږ:duȎ1{\E+WwA'@?@ίXΟH m!f[Bਞ_l쫏^'1)i}g6Ky+wVn|8x8]Mh_ο-3'pC"HvY(9yѡY&/J9hZru3W/~,=A}ny;P gD.~gЗL{(m# a!: 5px7?ՙSa20 f`FP"JQ,X"q%Uc jtE=.Y{uƵG"%( -!;O}}'~$~0Ofh#v^R+uBW e{; F;m_ x(6Q}اD֍"j)]5GPps`|(|H?-"")bϏ߈5X/v~nH>6J-߳* .C4'DD8?( - - А:H>0ZArCOY -yJLX R`Ev%,M4/q-T{cDAD 38Ӆ㡽.Cw&]mqm{w'♯E^d֬QSzɫly]jyh'P=9]}GK4wV{Ju#qg|&xBSFӉПHD1v( Cjxm#TFtfNLPɮ+( }߆}fDTDDYaVePYM*X&FM0.59Ѵ1ihKs޼[ą3r { ʏ2hnڒ۪1Sb_ǯ*Ҫ=RDna_Y9sMF"",MB0R߯iPQt &VX) wj+\ټwIl徼Tʜl~Yv)(NBQj& -S(Xlaᮔ^;4>#80Pk=uL{Ӽ/xE}ZhBg./c$18%#p0U$MK]O=O>d(NGQb. -w`1JD}P:}'ih`A=hcZU4u kbMeՉV9iҊ~-FX_r'N>++D8E; -QB`4ԃs5ԃz{vH[Cje-ZEM+c-$u))Y$TzU7 Uxm];xs6pk -bJsS 5PH3@/*Ʌ.3rev.+k_ٶ0Ӥ{,wdh9(w辩KpBr_:lEX z.,^.Vô6T~GK5=Z)GvMw[n̳>\Q缮kD{xv;a="zNϤB 4MC rfh a]';m$gxF[bFl6_7 o7䴺)AU輺ɡQA5h8AzvV,Ns!eL83 Gx*NgLбB㐱Um -kpooȱ>^AwP~1?OH1Łi=3LL{յ3OǨޥzZtnT!ACӷyFsh"D3\p-Ds8I?DMy`%6U" lBgE b eJ2L^U++fMOe?Y-k7g]ew+bG)F)O+a5Xs\3 )ς@x+܊f֟btRk(j/˔? 'ODT up~ `$lF򙔱xV2eы,?xO{*PuAo_t?_#?%7j`X~|0^@0WANx絔Ahieޞ`og?hΓ|9g|Ht7B|{`'  zh%hp440ppX%B0H1Bo FʗRQ>= X=Q[LɅCy+)hEˉH #[!`|E~\BAYpS8RB7(ˉ -ro }bL x`B/Hb͇C<hƠ3̕A#z jAM,H`Z&)&5t>2L$U)}~D^ KK0hȠ ]̝ACo l`rI$! 2A%r|INeJvv :2hOZ1[•XB\RJj٨B: Bw,\'u}GEugqSFA"3u -DPAd230 ",BK5ZWcM=hbY-b'su߻}9(zy'V&q_ Nq%]ev^Hihde-r8hQA:'hE"[|}mqBLb?ǖ( zŨ-,rw( e}ow?$kxo%7WCgҋ_w?=߷{'+E;oKQܒ(['e8s21E3fNPxpz]8oW.Z ?Y̬ Y 0/2]7\ -g'\e -/p@w$@/#@oZP/^z~>+]}A&ݙ;U'Eb;w>3_q)0JƧ(:@38]z~@Iw}҆<4{~ެ>;ܛs\Z&Uٳg7'dY>=x5qχ&G<ޚ~f

#z}b!\ C a ZdC_E yN68=qh~y&sL?ݢ?`xOn>A]gwd-MwN6]V@A`Wal-pM9G2p:ҋ},b>H.p ,ݨ?$Ev/6߹r{Z6A[K:K7]`'QkԱO/&f~e%<疈JGT؃q=ѱ{#4=]7nmtۯ6lM%YK#٪w͡hOPc8O7cq>_'d$8,d_۝P=>Ұ;.AԵ$lSlEGtmMֈ6eY˩1sC9z:N(#5hWұ0e7gRYp" S'g67c{g7'upKJFu=1Ŭ-![ܪYĕ6/Yn"UVѩ6̥2+yy]7Li :Ƣ8н}I ڍ0۔)oS1ņ,؛m ;s䬞l/^g\Pu1$U)&uMCR.־:acE|sejkQ)Wjvţ3q$2 -ÍxAe Z!3|gVglnG^[΢ DY f itMuZ<ʾ$ɱHѩII'ܴI7r/Z52ĉȴI0.x82LcTe} -AO)tX6eiʟPj=VٵuZaIBC]U(ReS*,˶I+-K5;w01E]#.BdSc -PFF 9Pg?\Nay4;ʛfq+ Fuj,ĚqҘFYdNʊmLXŠKhLX9:RXU[<^H}ݍkW J8 -(8g6NZ`jNmzN?f`afnMPEESkٺn]6eyZ(*X -)JYYػہΞ;0}'MZB׋ǽ2-c$)nJjG%W?ō'=vpUB`J56<ցYki3d^S`gꪉ~E+߷bz + |NXc.tsȥձK,i)X,1$f=baoy-~KU^)5cFi(ޔmJצJGxiqoMnx$p̆; .X$lhIix^IUDnIcDܵZ"sVIdގ5^u+7r~v'l3`Jy*qEX[Qsl$S}Fna)kֹ9[V̭3ʮ؇-%$}0=5P-gťʁi\&TwWQXJ(W wݣwy2df3]/ӪKR\;-] lI6h )wHp8_\ɞ:P;`yVCNdQ7F׍j)3u{&կro7$1T(c1f`6ɝ.`2Wûùf6hXt$ G<gSFcwAUQ˴2-Z-~ˣQ;"ijro`R?PTY@Ƈ& cO!g|&_$#%;`?;}MCO"h-ݰ} `;+BgDi#3~n`k/b݅ F I'3@9=.ak[,m03Lv^NOй^6Am?tuނvU*3N5?evSO Hflo|oa1:w4;pPA7 -`s̟ɹ2;ك?e[V`'` x@7BdNqL9ćᇡ7\.,P.W/{rg̎ X>̽o,v$'ehB| CG{"$(C iJ0~OzJclr}jO][B 9 <9Sb(T/yf(ў:-TDA@'/R'yN[ߛ3?;nD$_}š-&¸P9U^x<~4^.0#;ߟi%G\ )PaI6Re١Ԫ֏ k괶MM6ѮAbN} :F9UrꧠǕiE`_PKufT :kA+i_ !7!q6Tt-? A$b@k"q$>ǫPZ%vٱDX}ب]ti;֨ڹS+D7Lj:##ݢ{-T3$88t%|t$ˉWӵ ki-Η=>wqڹ\wYsssy6%6{6&]jH`T$>5@| q4Ay@+#Wӝt[ZF⋴dډS5?gcb)+ )yLeKgMi4Hm5M'UvSUX*iIXgk{YjveVc5 Sհ|w cemyUWo5+ o" JbZE( K!@k@E(޸junkn۱vvt;ad?=s9s߰NJbMH k) ^ ك{x s%' 0!n%&,%^JR/5|ϹR3qS։ةPG2{4!xW!s΀e$ kg|¾Ct+J\V卵WI*9}V8=0MTL$[ƒۘI=!CCY=2/.H]r³ זDظTuYc繥ΕAt_fMMtfv<gTF0즎Їyj^]w!S[lϩ mn6gu4Caͤ&s>*Ie#YBCDHYCB>9Ήװ{^.p!g 0e b GP5&0z -ޝ,}`k~ I_Zȭusf털\;')Yh?P[xJ$  |s×jߢ7 A R7 -`LRʢܺKeM - "]`Ȭ3VVs͆v~YQaIH?+)/n(|+)1"4#Ucpу. {F[UQyнŜX[W]_]j6BJj9%m|cqP4*Ht+rޠ5~#0t`aB 8Y0O0{Ͳny\VQS -(2UXEj/-唞ה^dޗd3MD1AJ^W%fA=X4By#45Zѫ ޥ~E@C]S_kͭif!azSz;\Yu:\YHUITf"P _]AxkC?4 -`Cz'f,@w -;kW j0\Ž-nؾ$mˉuY [uMeW/ة)ZxM* u]xpNA{&q38;p;@57h~D@t[ۛ NDn^>pW BCȃz`uP y2cc}8ܻy3itu` cOx>>ޏ;x}~lFຕ@Cq \֥)bJr:ɣP-g< <ܗ\;JܖᦼUp8^E' 霽:'8^vMm -,U)Q٬jifM~/-߿-4˩ŸS۟*p-lQ犓|P:Ma(UOUϰfRn1MPm6MWf7 -l0Ԭ7m\keYb׭Vh %? Z+jslgXgzj~:J[EJ,6PnLW . )lڜk\]n^bԼfy\d\h,7W9aSs\ Nq+H -eu-??;w -WtX1QcJejtȴ* -OY4KTh;7h.?~vP}^P}n#~zБ]N-:3.mKvʺ{:+=TFiXCEqYZX, -SvfU6zY_L.4W:~Frǜ !{vziBЏdO%⹷7ubM7gjHwP,,ΏL떢u͌lsdvq);|a\NwYo _G=97Y#Y.{{3~,K`E=^&W{^VocvJ4yRp }بR=9$A_ٍCf =s c;eH~kZLtNr"}zpppc-4CJbe6%%ppj\&#}YI %)֘ꌉ!;_3T#R4b JIOde7 1P,,.V:,UHA@*`-k1Xb]QQD#UѱrԊ:k+ڙs@wŤ,F/(GFWύ8;jSxTQWc(a>_# }xk+$|dm8IZ%BN(If4-yYrR"!1ba\eLBUt|M,9"V6:p kv - A>0^舶Kgųf] ޹>-)9;r=$eѹ~Ȝ9aّʰ4$Khz: w=}lIV|(fYb.sFx <%!e3˦˂KQ~-'-Vy[M(Yc^IWؒSڎ]*lH!)6=g;ؖm^!I.I}*$BP# `hKWjlҪP3yU UeXxUYRzVnQyTWW+>j -a^c{s2|s@鎭WU[` |7q8P3kH̐ Y I{6+1n2w55w1lmxk:VXX\s;}FZ:K+* <moԪYG]׏[\?Mx,i+q1K6HVȆjdCLN2T+䃶^7τ={tW -MDofm]2 kPO  3CwǀPosc6.C}$NKE%q\[Hv l#z,za ˞u?0 &5M:0h`<c=F`ӒrXBz\U3X>"$d382;s `. 00(лв]:!e -mv0o E2 -N?!kvN}'5) i{M'܋HDrA..iT5/Z\/_\JyC2h/`pB/뭐yO33OW:赦;X_*8kx!v7\[cی@77,]N)KOgͣp4x0mځ=jz/ȏI~"r~T<</qC.נ(++7&F,(,ȲܖEvvrY˂+  ".!xCEh&Fmc6If:i:MSM[vڴ}z<_9y>|e >X6e7pmŕOK\@$ dXqu,xFVe -*U-])[kkݵMp={aj1drrr_w~ko7CfC $r"CkKGmoWkqKp/4 nRZ.GRZpP9E;}VC)g~֬(b}Bq}Lq==WΑHH둄CHW ׇG17r}G͛`!:)3aNi(-)>)wfi^Qg2z{88w}Hca kl!Mw07ߟWЧ>(U Qϊ귙.=CӞOQ[2 $<%b޿{?@ωlsc9ʅ49Lføv33 @fkזs5ތF~OF-L/jOJ[>})iNؕND"BWO_zp}b0L -&tRݱp@Gt>ի/`wg[]6^g@ێ֬@wV?Ӓt3Fݼ^wKZVw#ơ#"$9p7\G߷`=` -ci`@J0C1)Q0󊸞<+ߝ[ВrE 9{NÈaޒWޕ m'2H1D>O1wW9K(D}7 -A) iN3X&{m.,5V4 -ZE5=!8)Ae_HSGD瘃[xqz~\__z_ΒhlViI]lvcI>Yb9Jl5N-,+̃RaYLPZIXn6iH; \>b';(}-ügyQۼxQ}z ?jXxc^.=.vv)jdҐ0@+w(RV, Ư\2ZBm6^V{Nr1糨{{i'҈ߕ>j@k<ɃȣP]S!> kjX?7vy@E}eaOp}P, -(q]ՠƂADET,NPXh{,G$1qu]{Xۏ{@xgygΑڛ%_>`Q2l]f(2C/)멷4y赌A.| b38~Z9P rxë;<+"Q1ír\\p4éUp,2!9V3yLYǻH?RO VF*gS݀cju#`WDak261ZCcIڲ*K%\@]+!=bԝC݉Eݸr6ԯ_ȠAVh6#GdeYPV: S^ jO-Pwm.߃k=?CIl3Yw8ߕF6eل\dikbR5љ&+"CV!V`zmDQ7+|; R@.Wtll]> 7 Lb|II}g'&w!h!y6N(F{;Q׋]# DuOrLhv/C?[7lO 1yI#_ҐWhv<xռmּExD3=桍i<,`!Pqk6@kA? $#dYM6RDJvRK!u/+~xI!쨭PpW;H32$t䐍dur -.2i.'WG ƙ5H?2|B>N"u9RkC:k%2SVo>~CG7A8RWm! -GzjXjMf|tX@Tjds"@# ~I p'4q7F \hK_hZG9&ۇGx}Lԙ0&He%rM8O_ŠS 8tZ%#R9SThgG8A'5qU˲h%|:bN+qJ'98̃UYê -4jpHՀ&|W2cjAxQeNW^/'7~}6}pV7lGX3`?`8nsWu2:AC=84aT9F@YBz7ˈn.yJ\C;N;tQwðԹGT{$aL敃*|Tx{JHfKi -IA3!!Z=k, `;孁Pg} lʎcPR(bdl HVJ TX)Iy'e~LY֐՝FRK03Ov@ol=P4[Gas8OgHy!s!) !!9!5!U!쐣eOKC"#TțY?]8iG,=c~3XP7la(<`G`q8AZc"[eLeʳƕLFEq2ݸS^~EX(\(I< ԝn_>|r8nU =+LXcFie%-7e&2Lt\E)EjZL1S-0FlSϏ8gj1=6 ,Pe s :W|j -{Kھ>XX? -#e&5E\F3+Pydvf>6#hE ()Tm(O|ǧJx bca@OdFwƒ0XiQGdcNR̎LNNγ]mMV71.!Fh*a`+"}ccbuX2qH &̏(͍͎)K#"q4!SR4VhuGEyafav .D&葨((("0 ̌ (qh]\Q0.cMh4rZ=&Ic\kmm&A;8}}yIirjIjI4j{'JxU?3~F[6a>(ѠԦ*CPnH t$=WV^PVOW5MߡYgMYHc֋*^TZRES.qaQؑlZVudD9TfCi*LiPlJN]Y(_Yeƕjq&˸KΘqZqC&#CT ҏ}mf`69x%RuԘTfbK0ʬcQbBA>dbJxyhI%){rs~0AZy(R+‘R9HLT I\E4L6-U]ު^WY>J"r,JB2`y)PK8]LWA߳H^FB@̯}a瞂hĺ0ǝ"ĸ3ndX宖Ns/nO}M><<P{ - ~u@7hYGo ڥŠ$;Fc@G8;#\<өG] - <M hw=n];G;65+P`0^ہN``~ jCp(C!EAeaqC1}C"? 6je6րv1.Ao8]@8B{|a#hB>n~psynu󘈿+27ԝXg&Qs459=@{?0# pZM3lF{p3,?gyٟ!a{(pm>/д.d/`=fC70ԧ'J"H5K\~¿ƍ b^?EnD|B]k4RCIX= -(z%-BR&kOm?rw޸p0>&?62j4hGLAIþxq1GxPR*Ǎ+GsMԝPS20l<@?F-5Aո޸5ZxWXwq+0"<⢤MT8UKƱs qW\ %uW7hZpYӉQ\ňv ¸C8? ΍3n&<ĉIdGoN~:G ӊx0n11W&%atrF&0- _NI~GH) -^?`ST!|:lG0V#ӝ84ߛш3۱j웹 Cv`p>ݳ10v%%U'8V? _LHjȹ{<3&̬)8>'$r&cp{T`: 5cgP'vö~ak?|^ .l ->/څCք|/@܎FcyG92]ұcq6-.Rlw/# û º]bO~qui;X\/=R}F4XLk6c9 b r&G/Ė$W|ٱVW.jձ*]إ[/vI!-;*ޕb$7SjU=c;3Ҙ?ov$/޸ذ, -Xn}+Ra%=W.H Ƅ'NQ?RjjWr^\ Ekp4riӊ 2)I~<'yNGWJVRn0͐/BBl4ԉC6 rUe8T.j* w4eߓ(N*;STu˯lU' j^,^h71nFȘyP"\ֹ-B-΂d,NJ`/( -bAAdHyղ֯dۆ4fi,5L}2dZU%3_S11׫=W̽H;Xx:O#c㳪EpFIB81(rБ-6!Q"ګ{dwY>ey&& MCMZMZr4;ej\A+XEq 挄2r˲S/dSYlN)ݹINuVR55I4)I%)RCm|GiJ%i"ߋk$UNjr!ۥɕ,]]EBLHuW ]򲪣r|ը纯UUT -U+Wg`/*!mMVXE] k#ݳFz}IEDŽdAyZ8Z1~SIOrYSZU!ϸ\R㻤Ž@H55 IFJd$LEb<[ðu ⽉f`ׂX.omBw{P ޻bh'bh*6FU {'Za'|/^@,%#k& jU8"W-EĪdĢn+ºKuG|qXYĖ,$&niTUk_p -"$DԒPJ2UcLUjj:Jϕ:Gr#y<꒕Ȭ g*]FhI#tM#44B3`i M7-a *tfpY Sa*gC~mw@^dQbOE*<7Ps#)7Fay -믐 - -̟j_v;\y)`jcmAv3yf.fN5`={e!/b򥈥Rpq/R?- T@iڔʿ4A~kS>jmVҾU^#_WOjYQx?Vv&gR\)"K/ʥk%O<Xp1Pom$5qQ cXFTޕe) -SM4PIYhx>]B IϕR)51JjIb۶21 ocR P RCk(b Wovm7) - ĚzrjE oTK;$]++>v۽ c~ǏZZ}-ͥbbjW#0Gi%oFɺUh$/5?(G ~ŏc0$~b9EQ:_|F^}I;l 5wKa MchJV0E:\:Ǣt%B{ KuL/gds2y4]!T=AOI.?H+XMXPܥq>gA*KczM#c/v?>>~_zNo:ptp0JSjc &C0&51II1/gה_q0ބ10fXP+` -6ПPOls&\wV6= 0a&~j [Z=W^u_:Rtzme.4+k4xƠF -)O ίu/`@hȉ+f7r}!>w7%,gҹYn!Kktv> KP_ ٤4*3ZzCǪljjm3S/`R _Z- -N!Mhon6\[b6R\wϑc*=Vc=?jCZyF+n{>@NZ5/bF*#r#7i{YQǍԨ+ƌG(HyNd7xg{=Ê6Wvg"7*l.an ZЭ跔=Js'jvLbR53fg̈YaT2c?5b/ScSbM#I̯\%gˌ\}2|))enE1>=*U)@=Da)fn$[IcuhuX&L;by7q3qFzWhD{o͌ qڽ]iVK4+>B =5#>4`%ۆ)6ZSmD{d[1ѶИ`+UXj,m\Fλݳ 1g$b>ã0{ KHe K"I&L2IfLB&$$C!"ITBR, @!(}cVVc] -B_Hg3s9|/>X$E ҐcU8E5IsT@U-wdґ -G@2#Xa:Ŏ;BGYn;[ycq9.YK$_mqg.j]L,kc acSUU -TE<',ݩӣgFsQ3Lw[,q+'+yN+fNj?g8IK+MuJ,Ty]̈Qif3ȝ;KnEFk\mǽvo[Vr_\GF9ƱlMԀxꉧ,'r`fE8;J9SU3GK= qɔϛ\o@^o1Mfg<`9={4cVy+󱕒c}fCV?8+/n-xBRgT7\c_-or}'w*Q?_n0#b&w[I^+Z\xm&}$=o%PF0 7f|>xhA,BeVVQ2#*RzTVj|&)ԥ`- -V(!x lc 4o2cؒbی-͸"ۈ+c/bO~o&j`C5o(]k(FӜUʥWj֪"ͬFnԴnM=ɵ(zۚ\16&gJm<h|Pu<شZՃ4>\3 i7ѴxMmthJcuC2Mjӄ6ii|qkzCz[Qk`mD#hl#Yy&-)tS4s!&E:TKXܗ.S p8.jkaR3нAWe4ހRbc‡/L>e~>g|A`fNmh5@8 -q P`%:X>qBx_]}%~1%ޅ&V#7B%B70vޯեh>g^}~$%zEs`@}xËWbCaADA z,EL -Fe;{v0-[nrt#Lqjh8Чm>GulꖀiEP0'oeX׈?L0?gpjJU^lbPx;w@x#F7b;&"awQ\r㑗#G~-QXM7gQ;O-SQp2"G#q$*q`i9-2 v/kΘV#cak6X.#/a86`Cj~c>11_Cqy,$Ȱ31;VcJlYi$+6%90HAOPKL=PISA&ze?Z#tI %UoW9R2yWP~XaJy;RU496*pz9֧1ڌv d?}ѓݙCM7!Y'KG=2%|'>KL!rl/碗͙s1  ec [Do=9 V8PxWtS9ڕ{QNUy^ g#?¡3m>K,;&Ygɸl`\*cїyŸ_΂j -([ -OaZ p6¥jJ4 ꚰ>ªzB$a-@Sf4(cCO# 1.aV-EWQ|řh/΃D*.m^4aS$E0 u3J$a"?JE>Nśi^t!:Q%,r\pVhЬEv6VZk`n&AaԾ& EQH5咸Oͫz4KI='=駛qfܚ%piQ)CSU6UhԕêӣAgF&}F F@NZOIB[%*%qX'{j}񻋿UZLXj`-P:FC#j -1tBo Ag}Bcr:#w#K V5HA 5Їf$&"p|wHdꓰb | .&7P[M`븪oR#$32R$uHYjA\, iD"*cDAtH8MENLm]'{LwXǿ *pʐ%DqEׁ /BEyjMl`֓&8֪16Mn?H}~~=~_Y⦎%( )Dn(/WS:`ʖ@Φ%r2mKʡ|2LhLcx, W<$Rk3`\r2#s͡jgQ[ ٙek,3ƛvsZ+*Pb[嵅OΗg -S3`VjeSˌ[ۑCh(u:.:.Xn0g<̙+[F_sa -SyH1g`^.@Us$z 4fp'Eg  -m=E'{xK4bX94s퉮j#MCd;srÎ]ر;ر;h(|Ful]pwr߇ {)5՜ ݌>4Ap&B4hΏ{Hc_N`G#I#ůKuX4`;1'-cٮqux-tɞ%CR[1Y~ւ}8694.HbU(Mm™&>v ~fتnc8!;ݪu.4@W 9| -Mywt{>Sӆ#I? {YrU -nGL_M%݁{ց 0=&&OVۃAcYp drXw@0C̄9P eP`~aY̍;ټ' K==⭇܁uޔ_8 l4r9 scxƎi )>s]u ~˯| | \K68ش +/cHgi? ؂.c*Zkl7ң49Y}]ZZ9flAMOŢ:#WϚdDeo{g)Q~hAN^Z0UiEUUnE&herU|w+Wrm]w?<5nk0I!vßWÕ_n/*}cJ;U *4X;<1*J,{T\,POfӌov?)E]C)!*mU2a.mTAE5k)7Vy~7L9ɚ?^3kz7P2VkJ6Ҥ3J &=UjE7%gaz<+Ŗ7fn^ jzM9X=G(eJSF,BW(5x&k\A yOcB+%FXZ(.EQ6XU<+Ė|l%69i!]552{+w >4)l&MPjX&и /SJ -ج%E҈ CE#\ 06C9,{rhaVᮚɑJ쭉}5 5D㢒46jR'ktQ@#bjxFOCcNjH, Q-נ\ZZ#mPGjRRc<5>_b5&.R)qo%jd%ŧix|4,ޤ5^JأNh`'P-?<*?we1 $n "" 2,0QNHAA(MqZ5q4զM6mzĸ&ƚXa9}"ΡlX?m _fK1SU@Y(/ir'+; ,E&C2 2UZJRSdHPR>%RBNJO %$HrT#,g= 3 -boeȜ6RpeE+#=ItsjLJ26(\MG'qA;Nw(4bS:F @QU5IVcwU^56=VS5Y!r>><ekzP -)iluOy-P0yڇ>+7{4>]5o_pS$l7SO7=ϡ~z&j"9Ff3A(h ܚ4K}i~i<[dZ8ZL-P kygຈmlyh*^/|3Xs"kĚC 7IÚ%%m,1ϵxXE# C N01ҾCP"p8iFjz͚ 5Jc{Jj}?@?6f p뤰;YT':ppzybFcI7xZZ+ow^BmxBP\wFzw>{pbGs֏ ŜC9VM(gU8@xuq?Nx;AEquO/Nj[9WuqN ?%wŗ̺75f/NLN>V 1,vb{%Ө;[|;xR>prWJU}s_DщTMNΨ@Ə7𣋼:~ŏZu[8}D|E Bm'|.85go-/(t"шZ:s_lخ|l6bsll29=قldsۮ"G'#$:D b]Pr\l.`w,$ -ϓTVWUD̮r]9 ];A1B9 (hr4*Ѩf,Ry ZDd+G#r"pvrԾѿ+`ܵ^ Gk4:ıT-TiL\Yn0˰/2,3,31 $vcxKʭk7V괪*RRU~V.Q*Jc;R{;G: rq+YȦcn:JFd)ﱰG}VuWj폴~UeZr6f_T=/F|Tg"S8%S[K8]ͱlsrx[}嘽,Lr fÄce.vLg2=&> 1,wvi9Tk%?k5t2Y$Dq"nG9orj8`!E8\IL&1B(iK{SH9#3jJCq'_vDSyʝi%SK,'r %pj6iLU1݌2ьQfL4-0`tݙ2y_ d2LG_d}>'.zi{XdU˜jɌf*3Y&w/4i'p y-0ws_c=om)]Ɲo6t=&ezX✖.kߜ,ȔjٯZ͛ٗs J*fwa=V|En+x O=ūt?*%o^ΒVGaֺ"tQޓDY3%R=V -=xz1{GN]a92k=c`~53tRLrH[(m $`H#Ϸ\_!9 -}ue1ӿH4)$(~I$ =5XE_Z#_t ^}Wt,RT$k$S @:;I Th$9")Obp/ yvOL\Mb&&+#rrC|ǥĠp!ҮZjBVCq$Y!6BLaCSl aTdo1'"lgqHLݢէ9(Ji+"J_1uBp:ع DSbsMa}aܰnBywx fkf?T#VJ٭aH=Aa+\89JI_4)ҟMDZYXI׃(ORS_US[Ƕ\[U\=%=@vP5,O8"Y=%]6mzI0H_)K0l>.wR )ZL-vj5!/Cp'V54Xք,(z۩g C|D' z "9&5xZpT% -vz'57` BcㆎS}&Tyi0(:5 : HtRwKc)j<)^xrS긭Mz[95YiGcݲ9S OkI7e.5ӍB 2{2ceey(Kk]XXXv]`9DPEEE-}3Ѫ68ƨǚ&5UcըʹMG϶&iLL9l?qg~e}yG㣍 Y&FaV[O?r&4ݑ Cƹߢԩ~?pҪ 'Ki.g]l穋 LhݷS c)+C7`?vj $ur.{gxhV.37kznP7I7M;*D2f;y6U+6S|}.UXzM|;]jsrE5zH]< t9}" v@ Z3a,tS|=t}M>|\sOzO BVȆB}24FQG@Ǘ9 3ЕbA\Ru!u>}p?^0zݣ׽C.RC('~n>_~fb/%||% -x6Otk?Sn)qG>H^WBԟqsϨسu8Mhl6uF*ާVc%>V2e e摍ϯilJfSQM49αͷhf x%{Z1p"ơ4-6o,P -4jИTmnq_x y-5+8{wn}W0zh%KdhVWfvWc!޽LӺR8MI~Tc&X[Us<1=/gjjb(Gˋla5øph?YCif5iJ_&U}M (Q]HѸ8C5 TFU[UWo<=ߏ2pFqZ#-93#gjҔ>(BlQmp֘~+~e_UC,dlְڪ!T~q >O ИUEktI"2¡9*PJ4@);҃i Vgd(͑TG9du(9YgdrF wAF2:)SB}ځγ$:P^ ,g3_Yٲ5(ۢt%ggWk̮*\Jp5fuoQeEຬ(]EgS WgђL1P%PR]e`Yr#+sY<9Sg<);S2Ez7+;y*sIaSDn[X,4&rP ^pV,o4 P!2WdU薡PE#Q4NE --ZE'̃>8dY]p9dj FEvǮ"!fRzD1j56$HӚ1ZsL5Gڴ4uSv~LLJ=}}}J,}Zҗe-=/kMي\!iZP[OhgJ(~ԏ0h.CM& lXoӈ`]~񣇋5顸{ ٽ ]k4N>brK $B])f:[`ki8`ogg~rr9H};¥{Ev$9P,z)YJ\BcU?t-=7L0cQq-)8ť|?ct$`]9sMxB@w~DŽ q` -a2B5XXQfezE|^&WT_?xNDH x&@QGՠoLNי1]e -?>Ǐ?Y>c2D|oI9d 88>//w@<)3̤NL ?>ď0՝?{wgROyN9%x%cd5^ \{%e3)/&.lخlln应wEVوvϓ:^<@G!.b(?hDcy2ĶuDڄ]?Sm+_qх*?J&v%} (nC -D:\fkmt*t3zs7]/Rk3ɰZ嶥jm\Lʤ"iʒUTZM8K[T`T}wj9ME$QnrvJˤ3i ƗR-gE)v8T:Lũ#5-u&0PiєA^MԬI_ ەO_ kx -G҂Ҋ. `+ܔ9"T0k#Qi*7eyG(AuY`w]`e]]6xM0xD⠉hhԦ:M4=$ΤvI۴;^37{y{wiJ>SMM,)O"t]-)n~]6pDo}=׿%؃ -M|!.oNP9M1#U3&_,UVSSE嶶i] u.XwzHb=xpgيlAS!|(^UEUY\QYM29m^a,-<ٗоY.e|9)-0pvӍ*-M0 &]*pĪ̑,Wi*-5,TRŮ:&5UW27j{/h*u]9rFc3e.KFB|P e;GYli*.RQyVe(S^&OF{f)ӡQ]FV>L+y>FG*^3T͑[|oF׸[SQ5SӨ|3kWo2|ەۯ4 \UZ FoTQ=Fh.& B -}/P06Fk[yoHCY2uLRz` XmJ-ocB2)f(= &rkxȧPPɡf%*!BCC݊S\bo+6znڌ5]0Pp]W>mĤX6&*%p¹J[4,p(.ܨ0/6&|f,4b96Dx5ƌ@=|mA{D'Rb45AC[R@pDT#EMaP<0iBSBca<}P{{$7eh6ugrј?v6ʜMncښ 0mx9c8GXfH⽓1[s)V)m)nql( ".ɣ=åM$wc:<_O&(ӧ &⩒iX tSK(kRˆpp [eg%yt2'9drcN/8&s-[ֳji'7UjCm^0}ƛnr ]"W4y&걙ztG7B=V6,Ԣ\1ovaM]QD:Ro ig3tt:~͍[`+<(f"$#I̯e'{5N1bhof=Cc@~ Wad 0*r޸ΞqM&:$fϼɀ$`8dA>ؠAd,-=qB~#M][}wuO|ʯ~g ryAXzEa N -n2.SY4yy]C4b9eh'{̻Ja,#tZ\S Z!}5}L>U3 xG;h^ms{V3]8 -Je|INS4hTO[}?#ĞIybZg)W*7eƻjArz}}Fwㄧ ShGV4\ԭ~b&Tb n_}ث ѫmM-v,ϵ'`| />g,ƒ8B-^T*G_L|7{٢mӉ9:w [iɨ Fܤ`< W;k,ExNT2yg?fۈ_FtA7 -Z9#9NZ֓I:Y' -\9yv2È?#KBh&t0UjgBF5׏p6XfƱZtR'e]o;v.p8qNlp9&MNv-mvJWrT+[v h5[@QZXA\1& -  -!:'}<_+Qˌ7ъFQ4$M,c]OxaYEVW^eN{{J;Q>!ctM:^FݏNtf6R;Iha:fmMEQڣ^C")RQXs< -uM!}*FjT tj"W5=dƲ7k 7,jpV7PE,+s_ܴb%4J{JCvJ.Ym\)Weʘ&+W*ߩd~ -Z4ZHAD˜*@#S4hтk"6P:MAZ]ƴwRIJfa X<1ٔ0974dnҀ9~sb}kIieF˜, WrFAeY*]YzJ9ZM `<ĉeȖ" X-V(b ֣.:c -7(hߡ} -Skj.7-}G\ܓ<:B %CYXc)O/;Qb-SOYrʫYVI6+^UrD~ǤZ[ث&xFNjr;^Ɋphƒ8$:j0.kQmRD5jq*P mZjj9Ffy|Bu|A5WU| qOPٍQ4` z=^0>Eü^P.Z\jvW]/ۧv5GT_?(g\.&9=A6Á23|43 #7964U_.4&ab1۸zsVW6t-ribAL"c+ǶDObIcN9TYPs46LJs;0c`w^@nwUf 5V1!a5&9f6Ԍn&لnތc+Z$_-xIL|1yyuz8c}:`?GgisyǼO& ٷ=0ۃ>9g4OdNi8)<|.[O+q8O^aX\ūÚRZ#u}g\"ӿ30+FHaW MqM7-:uqSu_qp?a`ә>^h^; s͌I.1_^ }C~w=ue|>r,!݆!ytVLX1K/W[e2c̦Z^ً@'~yy_=ͫ O8v?;NDZ%n;7zImvݺ6[E֪] Bҁ( ʠ*kT.T`@`m2sQS>w{y+8!hn X_8wp@7)82}]c^e?̫ -<߅o79|NFb-COoc\#Wp9/|^u¹|<~KhM^#80lO1|e.c_ރw&2؉_3/^P'=C%i/pZO~|1!} -"zARBLy,|>Osyx4c$csyy0;G{ W=hϳ<*wnY6e6NY]adZyN8K΋eT/·df }?edNaA{)tg"֣=Sqy9 ܯ3 -OTnawj~ m+'`6@;vahO0y$>f{;{n2ŵSϰހa<_֜B~c7EE]TK2/{]B?J5hgI8Zrs̜y)u&x"<参 ǃ-hQt1"Eo2KnB>@6X=GRff}6CKc@ ZbKI~hyYE6VhF~ , )Y}E SQiT]ILkiS;,>Dj+>V%%9ib"6N&!ˀXf2ez˪cU99Ty:;QޣeJZV2V˼jQAE-\RrU!u9Eqs mohn0*]7t ԵTiU)CjꔴXSҤZnicƄYEyj0*h|JTgX>E:'?'n݅=XK۳|ý49u_;-s)(WaQV+TѦhEJgάkpnr*[yZsrW^Ru pSngN5pwoc-u@[>C )VB.1WljtU(r+RW:긂 -ܫuonkUsJ5_#9ϫ}*s:Ļv|OA?tbqo&FxL -y,j+P[ZޠMyu7)6U>(g9|'e="|Ruޜp-ĺV|e:^$Z"s~|KwF@܁rҪ d%"#O Y_%xQgTxSJ}NBw3h&hڡ{Pa\a.UF|rFBt-:'kt,=2GWY!D.5Wi8{ 5سly Cc~;7zUSr cN1d5OQy|Lj78SEc*hct U -9?ހڣس^V>7CPǵIrag+Z -eʖʚpȒp1xiE%ɔL%I$1ILY( IpB'Po+h{$qh~\r߅uc H^"sʢ!Sʩ%) -S jV `V4NaRRRl֝Iّ]hϠņd6h3//LH/S/9t5Hu i࿏{vw #A<0sqsc,1|rl7mF1f#+>i<>]s 4y%lH*P_rsN0+8t&ܧH()^b)e`.ɐц?:񣛜M'х.;q+\̎ ')'/aS} 0Pc7!g y?QK9d% .5u!}0klVO)^ތ6)`{`5=0XI}ƏuaK}u\kWr@nzcyGY<D^%-ći 5M7a&X90Os6&j>괟d+V곟Xl!y}K؟6Sϟ!s=U|R</hr뭁H(!0 XBqaC*5?+8plI>| L}_cuM;9`ͅ9x7Z}k -Ǹ(dD\,FN?#'')3q˥|xCulb3׻Z>W]yD&Qp$ U\0!Vs%q@ϓs ,QTg~)!ĻeW9IнFsƺFA}pC&k@&0@>r0u-ys% kTmr YWIe{1܏-S׀CX5ȵG6(пјwUH}r:~\eM! s?ΣijOZ{$U1#1J ]v[>_mpmu })'hЗQ* 8fy ->=w$) t9Ïa|$љY;6f_)b 5خǶ&b{gbx O)5dt;mG/Q'*;)p XUf{&wѺИPn.NVjd ~E=Kx:\?דDtO`X >-`U2K@ +\gQQH/5?z ORz^Nl߰Ǩc~g;h$i= lYnl WP:l7`{'!n-n_%sŃtAzDwm_P|ت]>`tdJa7#u:IDQR-4,MGZYmbÛV͵ǐy,G1C,E'9 'F;5:KQL䥅L>f2H6lq~+93=\ssAOǾٚ5E^8BpDJ8jnV_D*P#ĸc7|8V3pw1wG6Q)pd‘GnRx੅M؟Ld4HR1è:g(UW^1DwAýxK#>6s13:mԐ`ˀ)𔰓U W\ᚈxC v~nw]Al}`hi`, Y̹ßI)&;U^U'4$yRTRLPij݊.V8uS_T(u)G9%RQ}_ι_m>@xV˻JP=Eՙa2åL2TYrEm5WEA{kߢ^y䶝UrF8Xʸ HjFJޗO4:Vđ,Y**U" 3X|WF\~96>>`b:ɧ20ہZyVJ)7WJI*vf*RRY N@9U*!k]S˵HNZe^+쮳9ALk=YHI;cQ5WR> BK?/M< -*ϗ'';A׃AA?L pew^g;n ,$$\ T -D8V^Bq2ZN6#2Ң >q;9oyn'[鐫%g+{ ]Il% -e VP!d"Z "8<<]LpUq-@ԉhܛ_! -c> ) $Uc֥ &Sa -(Hw#)<as29C>q-JFP .~1͵~L{ MHi 4 -JG ll+@5c`*o >40E70cۭ +W/pdK,1!a7fF3bll m B&xjG؄;@ |Ro.<)> ?:cm9&'4^6O/3JκsܺbCg\o3@jiy\g6^g8 9@/}o1DNQCbwD&AzkN# -:Ncnx_`='d62!'#FW?r&eRR;㚅ͬU[uv ~&v6IZFPI`%XEnE㮥>yl>7 ,f=F5\3KTh\5!%>擤!s`1kD- ^[csX|0>CMlFY} s$A n–Z5d||X`5?b ߝ%b&&ϰo_`aM<~H{xy71RX:8{WYIň%@#_&A%WL$u8xWXQ>8B>0uUJ &^pKd|G|FwUV3]6KD_PLpoFp*%U~/N )ڇGNvSO8jQv85mjd+~Ɵ9D!Mǧ);Lr+jOU,U*.V_ZUbRmP*Q!y7^'q~&>@^gUcS;Y))*wzT0TjĞb{*tT(Ѭj-JsHOU/Zl( "Tr O%*HV^SSSI++5W0*7BMJ3:eK1VmlxRS4Na\ɑJb1ǐ' A9J@!%7=A9$e(ϗP&#}cr?8ʫ -o6$,fwI6l~vIHBH&@J$ -$AkJJJRZjŢXZdZQt:0VvږaV;0/۽=s}o -|ӷCyd}Ke+{y3݌slP⼫`M2|ey*(!Uh+ت`>9ò7RN-F 6`|KZi -|A|lr :)wr(4KP -Q~RpBpNˆ/B׏܆hDѲ!|PNc%|a#hpJF0ߕKżbxLq㤸Sn~ǐ Ѐ (%]%\a8g|JHlHOq4di X65|ՆoexhE0W!Д./n{y೎Y|< scZlŏv0 :rEE0&u٦0k@3nIk%e o!ya|HsvB!'KA#KYd>`]*Y Ճr\tuL-1GlkKx_ o8I/9kA!h.\c 2ꄯMS~w9Xeqrđ&fNjn q/X6,ao=puV?&kyGC&g3dL(9!Qjgky?ۇG>-})wžk) -!#6ko,c\ɊA(fC~yCv&ړ{OK߹F*JyW=烀% qe#3pH\΀j<9y{@&/|N:gT "bc|'ku4Jg-_-__߃?[mrrz{ҿrGQ --@t%"}v̨N|StLF8$P3\PEM/3y^b}"3ɹ#LV92l+C -3l3ԑ)eX%x?<>j* - --tL4&qXsh^x_xONjqJtR{L(P&˽v+p^Z!3Ne8qY Squ sa(C2M~] -{mf{Lm5:'`6?)=|w|_APŎL籟.Li$_y=Bz?"kzDZ}p_!B%}'] J)(<5kn tb#BŅ9!:NwpCȎn$|_)nSV"xfO*xlyxH%k7xxSspd;I쯇o9r8+[@ -ԁ8cҕhC|E\ -;{Tn6ٻ4wX܍*ɝyyrgEo/(3?do%3$$`BH@ Ud(Q"EED -TPM(Ȗ-Z""Kw;=4s3_sg}ߖ6(M@rAk&Ь%vJ^ ; KgM@ úZ|u9I<9v,}l+팸 O8:C_ mk<b$J.!)A-A&.~OHtOHZsѴѪŭ$NVi%M["VxsRܢx:{u>4 gqYBzt}N}.ѽzDEģgq%+ġ~ŢNy}d8/aKbrg*bNE} -A3A "M~K[4[<~M [[%VA[rNj?]༆ey|1G@(h`*]S@K!M>b Gs)4 N_(| SC,u%7$ђ4ų `6уv-dwCaآļb1a11NSaȤϋ8!5F -ESw':{U!<\>y0?*>⎤3&C̙",\&57K?GӚ -4JC96g! -dSCI23!rm3A{Z"%I cfqdZ$-*<|6xcw!^"0Å("qkNƤ5!96"M%3 Cso#:24s4/%RLZJkM[f -ESjhrٓ\gRGB %xPEWjI],-VI^T0 GFZќ^IRkA#VN,c.'ZϕW:g/\hL1$iRUkkuRG[bNڱEAΚh˺نTIn}Rɻ@S3$( 4[f\" h /\4DSGwΆ݊+yލCF3gDv6gTT a[>u 4UFhF4B@?hI| baX_MbHДt%0>BS .z$f*|ըj@8:FF0'YPyH`pΪ4M LH9:KFf T{|jHh#OS4puddq'qm*i~RRB7 ೛%ljۛ ZoIq@>CRM}jP~D&%x8(D笚|9*Ŭvh֘Ws/wQqN2,AI5F(SB)RV$M׍luxUyd./(ǘn+ДЬB݆j>׍l[ {T38؂r99<cBvOUdN[`(Y9y -6izEqe-Z^Yqխ暶U_WݱSv7٫w8vC 1r17~¤Snco5{gκos~?.x?.|EO<䩥˖xz3ϮZ k^Z~Ɨ7ymn߱s[{o{>Og𑯎~}Ϝ=ϟ.\JJ]IYWQdg+/RKrR+kAݔ7) nƃe-ʅpa2b"NLUV܉w+3~ܘP~< y G+K<Ô+/b:&e+8fMy[sP>| T}<:I?().^J>\ͻ%~__b2lvǣaO G8#1<\|LҚ7?3' t뮶;f[mq&O6}ƽfϝ7 [xe+.j[ȤƟILiLT*Nhj~o'G~83gϞ;w?]pƶehbm֮-;zW_5a3Жj꩛sӲZdOG. 54PCC 54PCC w +u%^]/9 _ԏ-~kwe ܝVOѢ'~HPG VFծ=Ͽ1}C&|ww>/n{y /Mٵzo.Ѳ{_d'װ6Rk]/޳̾{ttp|gvzffgi;fiNд2/AoF)TLP. E["-)'-߶g/yޘᎀN"_ -)Uka"a>W~ՓĢ ?J!&vezzc-̣>$CkSFkCe\ )j3J-)|ʉ鱶d(;nZs>w'C71}fDmX4 >J6 3O;F.^hW}F]υ -{E"f(/c`4Ѭ7h^!}w>85KhʙHAΡ 5.-F\ |{Gq{im=/ZtUppdK*8+#KD0 -3{717@AoPa.57d0~DK2qH9yBCOmHLU1tՔl2"8dX2, K%e( %6@du= -םj^A\ˎ*$,!2.KGv[=ZP -$A=]+#zw^ XaF*bZ*SRD &^\4h`C͏@v~'x<\վAGi]8^@(WI<}IXA-edYÍDVa( e Zg:8QCHz,I^PQ%xI-02,탡2Ad jk.s[g-HQ$Xi}yJ&E|Ne2@ZFO AF# zw/؄9Ќވ0RYeJ($9J;C`M􂜇}0 P/69&Il됼:y*LYT~?W@lͥt%SYBfaC*Pn;ӑW_ހ}]M*'(bN)%O2qbzȅ)}^KgX $'mo N_u_q y[r=P"fUdVp=!:qܴ.K``ZJ s^9z5-&i *ઑ9u(I#ObDQHnƖP VKE2jVwn '\G&ǼԲ֠m0NJ\ɰuUH Z %4Y@" 5Pog1ip`䉳ߦq/g(MEɱZ,T"V $KM2TjA})Ï9;g/(Ė>Ȝf+Z5b m @92v~ jl̆q'G%7B#~̇ސt& n磛#XB. xю?S]1a^‹w>LpP8:H"Ol@H4@D E> Eߧh x 3{ =p~@bIb)y`o%65~) -}OztGr( yĆ_ x {L|@mT+5s7*Nȁ3GR]xڅ)|9x^d\ \dyb.pPdmkkm_"8'q*)c{&B?P|5 -b} \hXL35j1|%/h`?b!4У( ^@9 Sa6r%'Pb 5A(=)|FIS|F!'AcIn#V4jBkn    {C.bE aԞ=ag*"tVP*GhHLf)sUQ͚H%PY~5[y6V!zgkhpwK -Ly}\DM3 3]WdkijJ$#d=U$yA -7B-P%P_6`ׅ3$}N+5AIL62U`#%yWFSE E[V\Ks2[nh`ԱCbx. 蕽|Đ -Cű-NV>ߊ=jVy& ޠe=ή0ۯk@G -*h\̰O^LA m(U42fwO'pxa0|YPa47(t%1 Ο;\0^vwvgԟg oB l;e^cdUd ~Pb07(PfG?^;87rzĸ~_|)8Wх5SSrITdC!b@ As/=s߼tD[gnyQAA$LN'3 c8FBm}.)u#=g4b4~noUc[Q8ܾMYBy͇fBHOI8t8:K - |H{ȠLdT}ūoݸ\7gMӫ[e`Gp=ϡnO"@EdYs@\4yjlflV/o.=ٴBj:*n%}lNmg[SeJKn/T^P@QQQ@@PA/],YinmyRؽ -e[ -C>yE@)H;a#ssEfKϖ?,?_V3|~lEfAx8䌃XH?Br_({Jh3[^^0_S 3`p^AkOot7$磐dmÐА -P+6/ -f^ Gp1~)$T1|RB.玦TMI千.YoA777SCkCɦC ?#4wPO?Nfo0qZbNYLn~(VbOKmkZeu8MB ̵Օ??#Ȏ,kNDs:#:'>V\9Oe t̚R2'L掞8Nhmנ镡1!?mb#~GPqEfV 7UO$K.R+ \f9\cLu%uMXyߪoNhkGWF 5Ak(9ԁZ$)93Ε4RfiNb]6*MZInAӉW2v -&Ġڷ6[ɐiuL Ye>3YK@e6rU/"4 SjbMCb"5b@ ЇIUBEI*w:SW.+Է[i>9Eߔ+UKr7Se@p^*6A3x!Y!,!*y-vANEM&ACӫ cuԆnvC$e^X_/.bm[rĠyb g \)ƴ&} IQRVmz;hgyn46,bhغ ;WB;b>tv/KWt:A`jNmg\}CEŴ6NΠ7Ҥ}4E111t"/,1AaO\4FٴC AJd2ȫ(8Um3R HaD ذ$[n+A? - cSΙ# ͺvUD֜Zא )F/ -(F;Lj! -14! -rfס^w Y]1|6QHUU5gvŹ=utsԚSͳe׈2xRKFEMP81H|ZOgk15zq4`.dzXFPV(3Kd&Z/asbP+KVPBCǎOf{~4]=|RPIZ-` I-|M)z圾ЂxME os?߻IocA?c)ґ]쉗RtIZbAe򺣕EHE)B)2*AW8/1/o\#=s#ǒF#l{t%/NՓ % ʸtM~aB4|}MG 5 qtl^44ra8((>' ) 1tIDIz*5+ @};XpgӻG\^km'#!_ٴ_DTXrR,-s4A8K`qqY9UX[~M6®QUA۠]tw \ԮUGg<6`**9{".#u 7 '8́j');m{k&h ho''DX_A!NW1K)LY[FfZy on: L㆚ P 5ۗ@+@t =  򆶤YIVS~AƷST*e\W;^^'cF:/n΢wt@s[\d FPEt$H3>eOsJ0)/(kh@>Ӭn fvhN1{vgo'h9_ >u6EgZcin=aqWu7h4(N~ ="l'h0SFI721MGMl/ 4d`B mȠ84'<@sgV;5Q9Ts ny*rQ(FRiھ,&#{ZsJgK|ݶ ۷6@|ftgB&'=TeH[H}U{˚6˪Zs}I/S9K 'h߷ -T7P1!O I=;scPcƪQm%WY.(IM7ـ Ad|LgGX"nJVx+9J%rS}Zd5LV%THdHm/6%^AfdP] ={}-*[[|SJј*9(#6a"[R)|Υt9Xe*\of{{8 o@eЅ փos{Ȑ選 WtEi2ȕMe*icAfa<&ZOki*QCP:gZd`"/zn߽ʯ'mytSqф̚t^s4{g%wQ5g,lmEڬJgd୛ m?gz݋NP*|3I;! RʼnC䖸 5mkey_goMi#֊NǺ_ъbZԊ"( D K! ـ!!@BB "ua(nXQHU^h?ܿʪ@-g{~o٦w7iaݯ'^ EQG TŶȟEa|1Hx]&]-E"͚`^?eܡaWtLmqu~)~?;I _)OԄi_hY=A.3+ݑq+rnW-n/^2禰gl}'=$qϛ2 &[ftC2.L?+N -z*)ShSes s -)[+,vU%8Zyps T z;v6ُ.3 -#0l&. . x D ꞏ|ces`"5^Y{JOD'"E5 2p!8 ;Bq(7n\v{>F+,Uyv.5ก -.I- -vA@Dl 8p}!:.uXb,15(`ռbYȅK2Uw ׇ _ yC7^R@qX;o;w]8|6s\FV[a4,/Aqp[k=;\ȯ f+7@!H26:C6 pm mlO +8{q\ׁD!P@rԣag -BdIR&ԉqMYϢhc.ƓޜG{a87Nso5dX݋`J#o;A ccx⇿LNHVvL7(I sU g F3LcL.SITS󞞹's1{.bXܛ|mz^y3-9D^鮒d L4T ~m8d{嘳@_h[;V'(PJIki)%F'\WTonuwWIeU'z+fjol/c}I6K m+ΨU\f\VnFeoÅwE*I> fh:֥ݰ˧֚.n&(袔[1Z(>D$h $k+{*%KGuS[o{d<5mq|셽qj??gpzB9(RF/oeV[׃ZE-M]5rI@.i' 7crk]:_Szp؜q봋sd 2hD Ӌ>n_ҧʫW\[1(\Ax*P& e~Qaўw5rr,` )||dh2e"1!>LpW1CBzmM><\( WyPzѿdf--hxomP1^R#g¦iIIVD6)/D~R:.rЬ,N/TV ֭],[y -)u߃'/RBHoƒHgyQ0 -L=ED] -DI(>݂]+Q[W_M[f1-cC\ÿDa)5:6`5\,+E$Eq$rV7z߂jZ'W ۄpN%4X`E3@D߅P Y &( Nv /Wm FI,ˣF9"/*J~ ?*\8 'C| X d}51RL܁i k|s'Z8B$1"۳ uod8:;r8 8DC+D8CN@;)/S~l ZCDݑA5"*{&#Ud8p@>3Nl ]80 {`ma CVsX@s`5Qd3c&?.n)TC*בֿ$\)/]N`,w[V6/SW1F@g JUTC4$"1 Bg $*AHP ' `jAj9H7Z $d;A, Q^jC>Ak6*8$l(|g<59jCo_j@nEDioK 0W, `{kX3_*yBJZJEZ n $/F.D>y;5?5HoCIH>,1Sd{jC<, 5ujS h5<:y)!j@Ky4@ݿ4$ig>3MM$" q:y;*CaӇ,.1ҽ?'qL.\|Q&$NzEseP ONM%O_P@<Ґֻ<eh@!Z jYu&o5C\D.S^GU$ͅ`̐n1p=pS4\:9x Va dYˤ~Hg/$'gK'd= iN%  t=n?e5&S74!i HCJ9@ї^\Z;=z4C1O# - FI%>aOЀ~9x,&i<4А͛lG68`H7K -ANmtaLs)JM - - -.QY2%Y")u"bb] -q@$8@50Eڿp0̿bv9YCu7DrV,8dI6Rr<ĵH i(ߴ׵G>trÛV/,أO[C0L]5SI =V E fKt XBkUSjOL~Wi_  鶅 ^9k>=퓝#ۃ͑QqMzZA(VIXb>Yʙ,uIr- RzV]@j -齒B  dW'`#g! ƽßo=UǹC;|{Z#"[o]MIjRE6') oTWV(VX5֝U[++h(\Aj|l>m/^7K>fs,ؐ'NYJdXʮK2_f dzh F<ߪ!zA=P4;A%r@Esw]kЙM? _p8#,-:Ym+jQYņ<YK G*hez}I$jp^ixo\GJ9t[ivO=W{y{7QUmqYd -݄/4s -b)R$Ī8V}pKsةUu_8t#Tov4t_6=sꋈw]`X ҊsdU4a|S$$E#LMaT! LdN)σM~kcK_o}vvo8~,x8<⻘4j̒byƉ_Qy"Z4`k0mz{i4hހ&ǁqܵy 9O"n|@F 'gR> ݈6fYM\d2m&Vp73pp,gnWA8vx2 5c/OOp>D962{"1>m"S^gEYQϸĐ҂z6 -v|yl@jGr j`bCo+ o=]ޜ3a93WgC3"㧉1ԸkxI!/)uJTm sE7Cj?EXzf?l^s_);xًٳa'fKf`cfKʂCgy!$a~W}g{e(]P;# Q7la -8 -` xd( yBr&gp>;~`}juBR*G!lu))6<קix A;/CԞPH i =H1= bPz\Qoh<-^(eCUM-tZ':-"%,X8.λی-~^m灴\qJw֮ udO~q|2PG( ac>lpNj2[Ke8vGEłH "BH)$${$!H*(( X.l"(3{9W?p.Y{ofF0  dI_}Rb/Wx"xW]^ <|s8ųq0}<Nb8!cVx} sw[1,b]YHLelQuҬ`ZdY-IVQ¾guDY@< ␋ -_x:[9pˣ!5Q-e'& -㤞-Pq$FDk -(ܚ|9ǻenkxn1P` #|"Ȝ̍j]w{MEP[[L('1F:רgKjJPER+.4ŧ3M7XN-Kt_>kDP"ga^c'qqK>˞^=SgsוGŜ*OJ)*JS9ZSXW -tr]:ǐ,ȣ+*,4EC>UuӜХKe"[q`/j, -IgMk@76޺|tŦڳ1ѥ|~y -M_La˭LQM-àͥXL$}9UBȹn&;t$=)Y-0epD`anj{9vZkgoaGQudC g Kv1[F?!RS -zdH4%Y0aN8u)H d9s8g[lIJŴ\u]I㚚G1hhjsP+]ռ\}xn~sDr^G{TAPI =uz:ڬ5>tk7ͤ﾿Mw/uk zDs7u#)y)6YAO9e;푷T֦P{@!AsSwZCRs?U O>Rj&;ߨ}M[Z -W}^w76MUK,Ց=8RAbyڰsԁ ֫ uTqZV"D '(`A^Iyݿ20duQ琯UPi5w["I^OjfƵ<ɕawY9*y;zZ]k^wiT4لݔxwg 10dBVI۰ՙ!NG|=xa~iw\p7?W^)m,m*m+w4XƋk/ TA!GRpn .q.qst [q3c>VFn=RRL{ e"Ibw:C/8hΎCBh$3b\PpJx֮CyK2c~q_vdrT]ptInpF"Sp},t66!l"FMl I3Ff9X!AN{H+RƍQgSwy*wlHڨTG= cpsdX"[,G\Z $7@t6KH  -/im>MKH{ -D6XkBA2]d (["O@~bN{@i 9Z F?x9 ϙaciǩF9ݬۜ{1HH@?!i&Ri#g.Qi@sTnBz!<y1v> eJ ?Vެ#Cbhw/e=(}z,~L:%|HŞcdA@6}GnRŨj=˱j*n_J2}rcf32gҎ$MIٔqQ~7לG᜻ vfOJsE]Y*HPkPNaZnZweCI~T~%c"7*kHNT$Kc͢ܧEw/ sGxvt'g@B6$7ϰ&5oT#G:i)E edY$oH I؄ld)C@+R>}/Ç}07H^0iƟRO~O$,i,K0)QN|BȢ`z@`/A\$%%!5v_k]7t 7S.lt9nF f=ް|܁Uǵ6[B!ڐgiznT[$߮sڀxoʐ8dgWos0f3iApZ@bڢ2Mq? n.a~DX+"UP"  "d&R0 w~T]sJ/)h -k0x QbF-&*E jQQDkbaĠ8?R9Q_3kỵ@` ܦO_,Z_t86]aɿe?'#a dëcV[L:@?}b#o$`W{tC -5@; A_m@%kPkj~CfeK0Jjn@A((g4:I?|kXyI*x $%Rtfffjj3Q*jS$bZ\uab'q(\} Tp0zSs( 2A hBmT‰c4탱!]~^qD:M"!#M)Xag -'d&n,D#fa$ȋO0K!G>(B'j m6s+.\I9MD*9N` (7Fx!$fツ^kx]2_K,GT-t@B{,Z_]$$)\Na8XE= Ìzs\2Nl숄^ RMVBd2ePڠfV*`u}P\\cB瀿<JmQۂG0ց~Ӯ^ 7Pz<^O$B°7yi}>Ǎhc'|>B':8<WC𼍑2M0,؀oNXׂ䣮wvkʯHH|;čWH^: 4.x#=h1, -GrR:" `q'yq~=@$= &j ڐw c!o& 0 *r4bY0CFsȒ,!M[t=NU")3`^D`0~폰I𱤡-YI|89H  #D:#eDN3afȎ\E%+!_q U7xw܎ pN}!kH) -'c-uh&W؁/1("B`p7Yke9⬐˶ETxATGLroc?KO~K RSS?{i_c#:u6ᐼJUOjoSp -'r"Rqΐ1QcM#)'XkF/%mG{~B?d%KD.vX5u3Qi`slBE|q볢0H+z/}+K~?Hcd`v1n O ?dh=L-6kq=NƙLR -=d,GJfb *`[%ƶh>U^ذl`=>ԧٝG0odg>$nޤ__I\2s)pFn[lu4v?5c5vwֿ) eG+ ˙%5oDH:2p~LξɹΧ}⏰O2t.KC 7:y, TzdVG[ʾ=VF9Pis.)/w"`8P#~}bC:1J"n!ȼ=sSbMsF])c~.SPnrn(W%3ʓNcžQ[=T ,BBxyɮht9e5hp[Ԋ˓vja̭J9jsT}vi.|oC#$ -$Z-2dAC.q {'i:&C D,{ږ^;2(+r9gJ' -*GUYs[eu-FvAdOse}N6V i(&A;}_;c䰛p­s}Vt*YIVe˯W6嵨~[TsdJiujejekq@H[2YD,%!iAGF>s:jh[i~W#J:Q#gW *J-eٍŻuMj*UgirYB?uz@Q5B];sk:~#-4)A)ԡZ I - -*HDP]@?8*~Yѳ}ssv: {:,{~'ŠBzjZƲԨ3Y i%9ՙp0W(D/D)췉y@4!M?a{g6-—KR.~4qj4h^\:Y]_^+(.*N/gd)ŒԌ 񟱒@:mCySn~c+ǯx`q_8ΩZAd -ёTaiEfFajŜ¾#E%/KiG $I;8-88 w>\/~e*=3rpAO^?۶[cK8!)LjFU^Y\Q/?//[>_LRZbٛ$8I rԫ⾶oý؁{q{nvh䘽a^k]gOhV0qјĴdL^M0TWõO5Ki1oxU+q@6\ET)I}QFˣ{*޴!ݖ17ݶa LjiO$3#>$Mr,ȅ#q)MU2Cu6d7m\N ;~8I jTDfl\o,jo: {FeDzS{“zbb#; WקjHjNgv̦_ -8$t%diF4;$ݑFSh䑎T^Ŗ86_p& ׄ!q쁊D$߁!c`V761=/{5JqP)^^ >;JBf6gdtmB᱃F՜ACƾ,ǴPOhޝ6wBš(whñԉ9mb%~cPI _}8-ۤН /οQKrk{5.T@%uR=w1щXE_R^K>KC  />/iE%FoZgaAլ HkU-ɫV"WCW9FHՔ}B[Z~Z/9})gOrveNfB82GuRbE| |#5lYwT [`wi} ӣ}x={ɏPcMeC0cy^a[I2ކP_B-7:=P&\hΆ80dWPh' bpN't/}hc{6m@]Ĭ Pet7||ϔA$י T%OX黀 >]4.#`\5ƨ LϬ_9,P-R ,?S@5"Ib-adVp'EBP>0O]QM^[TzYuQ Ɛ9! I@ @ A@(rUZPE -*Ȱw;9]笇ظm@X  o#D/a$vla,|Fk~`We0;xۜ{!p .B i+n hkrx[6?nǹQ'q-[FÖ8āh=н0 D Aq1O#pR=%~h@m 3@ ҵ@ez$|e[IZ؀ ws*CfV=zG%v?&W0e 'waO"w6x   Æ9~/ϸnl$;C8 T7[ځ|فa䮛 N!F#{4i!U!_ٔ>oA%W;9-<þ$`ODfhl<%n|TQG wN::>r~u.;Zm`.W&<AȍԄȾD WA3JJb;D|IoKIyO%^H>J3*dS -4p1:?,y7s_pyP]yW~1GyFԦЎ'XGE6&Z,L( %;Hi{I 2+F,,jB&)&:Wn*J.eW{;i4IsȒVIT|(0g-$Z.UíN2 +TU% iC -nE>rSiT՟՟y z ߾C7u27ۉZ5/[|ٲ[WjʙN[QM*jmU-M-4cR<7U>42_%di> -35LA,ty ]lrxOgwۂ_;CO=aCAֶ2fS<֨ThX%B}0/D=$קUR:U)?RHӎ1thkuNf Gkf,ںkyWS:s-=hUR*S Y#0deIU+*"0$N%ǘq5@:g6t1f_Km^t?p#`:Ἇ7R4ԲwVW e2raS(hr+F$!ˬeHP<3Sd[X15@ cbfq݉Ř]K0W4t QrFVXY_aS֗pRK qb\&-O+=JA]4_8̗-,qi O -1P= f<^g̍Kv;φڻp;iSe #ݓ &ZJMr`:/2O_^*yNi7.*a^\W<ƴH=;af~.al̽s0W0}Wu8/l_t.ҳWl:j~IH (" ;BBB@aG(Vԩ#.uSw -,0EtVEq=c= bʎϼO;~|}&GPnj'Stbo~~́ՍҴ껥? eUe>6 S9Gg?2ɃVEnvK7rk. ^H -/usYq[[ 7sbMFuaӶ9Y k -U E┆u9&NMit tIn3 .3^9w^SǰVՑ|פЭ37\X%XQ"L{~:ܮԶʬymdmC9TeZl7$chI-if+ qA3$MScCV{n Э _}#/zq|εIJŕJ*-A#HOr{kJxBrA7좑ܭt~NZn2I##rߒwYQ۞7<{7ú{cDfknNS2KR2LH.$ &zrX艨Y? -^3C \?bY>.Ԉd#hݐky˰qooLlqd)jMlp-:2{-O)zIH8!J"HX>--t?x9g#0 ee:@i5 !e96̰p\C8hՇ" `Bʂ,R)!*Bk ;[s@\#/x7){4<ǃ.fqq!cBE1"ST,B%W"=U^jo3v+w)n࿡) ->HmW] f{!և‡ T\cMPCj1de6C!>Q DHhu«)5QʤL @JM 0&jTًH CָCR6 Qrq Zلq鄏 gLQغ|AACil2} fl)2HϠY_8!e+<8!vCKľāIp1\h"G$wH -.HpAFڕLw8(XahS-^dltf',rw6&`FQ4%oL::!Az;׈Qɛo%Cҹ7I{-GK+ D4ȝP*yLa5%B )mP1oL6Ɍ=~iޫԆ&7rGQo1IyCWW/dclLg)BCBt!ő,:飔c}!"LPƷ:̚ bϱMr6s_`kcN8MEqkKXܪ {H @ b B-@"D@D -A^VPֶ^u9ߞ3)1 ŌyD>$ < {>p}n0 1[ n32+$lw} b:XۯQ)GrU`0kq&LxO1 Q f#@O[ a#O}kTm=0}ډ}Bs"oףX}i$6hWѰV0+ -s~1e9XET2K^Q9A~E6fLH@S@V@((C^ -d!o5n3aNKǪ]/wt9Q~uqXN-Q1J8yj^NB03S?*?g$z$ {s羚P?llew]O~{GI=VkȉŔe&tir:NfQJ8Y9$Q)JRR^y:m<$HX|\^ԯsv-K.O}ݪ7rV>VANfgf1ӋjMOu^>/NPJԷ$?IIeH\@\k X -:G.5yionm>KWg(Kɔ4m#%']ƍϬfeiƴ$:7<-3.#%. lb$ zob0[=\&:7;/j;E W_v_U*є(ɧB-K^P•Djω^H;)ʋ*|˔|J d!"5.7 Eg0ri={vUM-ݥ :gv_cĄr5-4uFxb$[ܒ\A?LQGl2d=iv'poٻǩѕ7w6qiD>YP|]>&Q*4qw mWB-PA;?k#t{h5h0phUw-uHy^/;,1 -0idҌ,M6ɍOhoht#$1a-0 pF;0r]m3`fouw)^lw+{/J#E] J ˒B'Q:*(v#-3>xJ÷!a m̝`,߷A ,hrO-i~%s0ɇv9-t9(ax@!p`9 l ,n0aߌ@v;(ݎ[G%];1MοÞOʾ\O%(wƁfdlfGmrޟ~n^BL Ѿ"4 I\dLCpfbc!Ń5RlOh0P¡Ej9Nh8b#MN dBBgCbrDd9CVI;hdFo۸O@p꣞Sc>k ᳐ENCP@^ᆌE gސlB@|<:S!RԨ/Do/G [1|l hf;U:A=*$(j='os背f2N/d~~C]'^OaM)^Rq|m$ y$ rȋACrr]3CS2.TkyP~@ȏA~6dJ -|:a9z[ gBƄ>c8i80 :W=79>Эvc4ۂnAPAu,lȗQ!ѿ)^E*T'* d#d@G4LX( aaV4D{%1K튢|O"Ə y%~Gnwv?DsdڐT ߳`F5}E=z&L`dcn= -'\y0.+\2lZb憘gMOsN=ɪHӞK{*+y&O\0TAtLi/vNuĂ 7ucżsJ>?.\0s}Իym9,?-wnog\?]}oD|$u0R.`ḁS.͇=/4[/[$ɿ(*n)1wƎKNw=Rv<[7E)F$z".5,uL,Xo?T48˽oҐ17*fhiHRRF酒EMs-3ԧL4'L]c;ڣϳۋd)!J~r}EoY{|_y"űo2ksk._YB|Ʋ.͊c/5K9P|>wϹ%VKedZ n)J{4/#/x -b}_`߻FܽR|~vLU k,WTy|zߜ_);qc2i@Vs%dM}Q ỐOsڷYVp3? c> tF}i1\Ci`mrkU{*7iw<狹_(o3~n9h2m:oHXնf>L0?I8XUA)ғ`B(@( =jjA@P((2눸zQ 3{f{vV|>_NnMF*1&8xxot~ |NΌъXQuTY٭9.}|gWF>UVJoO&51/'&Tſ -NZ 4D#/C -+2TVkrRtLgve뢰%1Gz ;ryr)R~1)ܿ>YƪLa&KEļus->TC"{٘p#W7 ưnA:hO6zIw VzE':J 9U*%IeQ'Nt=h/L@ -TP![ ד`]tk 5]Rwҗ]&ok7BۣKq-IM79'LWBȎr0yL, -M1e?0Y~rD#CdUV&z 0_@]=hxVr⸁pD0`ƿޟ:esZdjJh*dAC1b)VO(P T{kn~x{oeFgC5='ݼios~)wC,D쏹k5t$9ǐ27zO17ml36E|blՋ6<ȕ~WNP0- 7HP5 #(^C}lgIqLO΅sd?8{ &`V`ǘ9f32g̠촋#:JR%n+Wq gC5(~/r!Z Ɯ% Ygf~,"/|&x6dtmGUnߣCnO6p`sY9P@ -HUY.B )RyLR7*71[hP),SOeNwen6sew,~^p\O;Cde.|-{2!aYb3V]5+ꊟJJMWӌV}(>o;6kb6ە/a+~*p<@k:> }Kې_|4kC:(r:k!T -5C^pZ>}w <H[_Hh \~:L:IvMQ" -ְD{P9Jڍrw2Iu|u &9+m8)@ g)kHE vȜ` dYTg;Av5&@ $$$6!)67,E*n8RA[EQ}k=ťӊ֭Uq3_ۙx;}srpig0 bɴA$ -ZH2E1ʴ JYec'6PT9I~(“Fp [83ؘǰ' hiCzu%icj&v&ON߃Ѓ`O B Jπ_.xzǂ6p0~b8A\4uxg3O>C|x\-,@0Yï {M;H3)W;=%wÔ/x0ȃ&|<BDd,H:τ(} Xict `)$- ?&^[?i >Th\H>D,Ku YB !2m@½V"i EknٓWB_ a5W\R'H#ݘZXc!F](#,2S֛CMʆd͔dpG#vg&W߉ڏux!px.S lH`8G!ք`gr{Qv4bgمD)t01&*4 _c3fE;v7{^u~%;4 sI\I>{7s~c -RV$4ePͦ24/-!E: >&ϵ|3So"j9O=w60G=/xߑI;vM +3 " D[1`@*&D3>+ߤL_$~YT|?V.z}nFs#b{=bwUywTyߥ7 Q> v&c a$7d;3,!7#ўL {g c NrPչ_Khȹ&v߅s1D <}Ip"Xˀ|ăs8Ép'4!ιtl2K %dsobɬ&?$3K/w)|*Ι=G :Pui㟈&LUw( dx CIb̀B R2 -{ƛ*WI5GneЌeJѢDNgN˽^w05@rp_Rhhɠj_Ȁ꘴?lww}Kop[b RH~6[EFVQlƯuNV+.Y*Ns:v(@Vo?,r=K%};;RC ˷W$VCؓ/M7&wУ(#Qo̩G06NX'3ZȖgaks&%C>_GT䷫I+&gRVRZfQ6Qmv>-"{9צU[^RZ*^Q$ސA.o9wGxS#VD5\j}\?!m,DPSO;!oQzR[3~:7SohUڻR]\Qo+/]%n.Y'k*ڤXUMYW0Y?U)aDm{gՕAP_&(TLf ֦CnAlMU``|wBmMIs2<;?n-_[])XSY/^],+oWԖ~*鏬(9]VrUQ.rՕT iiQohjl5M]_M:4:-p|澮PѶ[ZT.kzlXпBаliTW}" aQ(B @ؑm( ""PYdY(.Pjg: eLm 8ȢTEgǙ3=a>~s>9 -f?pfތ=!)B3&w7- :íT~!3߷1 ײ~\btqjZVWdx,ҬdX]~at,^}w:}.();x{Br%FeTA:']Hx uE:L8˄M|j2$Z%&EŹ;itK+L)(I:Z㟘75Qp|o~ ^BD&}8jh]π4\nAG byэWYoP|&lmM95qU;U.puVnTUy?? Ȫ=us5:r%j8Of.-lHa^ttz/Z}=b]Zfx;:ƞ͒G7(=:}B|BF:V7^:{Nx -۵W:?yO^T]ׇ^+F5);ǃѽAOﺴ4BU\ۘ{>ȔF|rT^^v=#}#mJ6'jEVH]r.;hHO2pl`)0?cWh-KjэcǚD&Zn*Q<ZG+2[2Q)'9O7^F;uߑ.> g_E̟d+?Āp{߆L3Npp~; ơwRL2,X -kmb>E !T*ք!>8^LI dlT,q*+N׶>~LW4Ӡg3s'|:Dx ꩻDa5`ɿ | 9_!(_SW"y֑vuc8aڽ԰ye=c\\tm2YYpO'-Ba -|P&(Pq%HђBW=iS `0F3 1 -Ә+{XY2kY|uS}.?@O߁2``Aa1 -5ZJTP+Л^4#h=up,08XKt0Ļ75LU$;x #0%uj}4d/K P@領F)AWJ) (t" EƖF;R!"Jh`8 AhyǵHsq'*.Zoω7r,ɖXD8(g?E,A660G \ QJ\ Y ?ψrȝ/j:YB(>\E t1QB0MƲ!Y 2Pͺ(EQK  gijoĨ;Kx,(RfBҜ0SD% ݂5%;_t߸Jkx(i ZG>b9 z }z3u< -qz:O|I~`V=.h` t "7N qQTM ֆAH$2Ī3l5c ~[>`-c5Sfe~ #Ɯ}Ƃٽ0L ƋBF cJjH}1@LIJQz#r߇zVM#ٳn'AF}xiۻnzЂL0X^W6!``U8ߐ4|3-5.!q1GQkG,7[޸VNa{rq&ՐΗB;_ y4F=֧#TX`̿6M{W"H7Bd]沼5_g.}ʎ{fk"f|*k1vWZ~޴b.#" t?>1>Kg O.Qx,ŀ^ `38xػ,|+>Iq峜DKm%8opks=}2f43J>}pIʡ^~3G 4@x{;W`2@_VBkcڝ"_!y٦9zw,;XýȒp>IdY>--\!BEݔjoBWhN(Ԏ}ɂWD+,z*2m;xBa"4hR~/C);ªțfc?]Y뫊]H[@Y1BVaߩSGB:j&Z4y/#g$H;$l61ױTW*H}$B%M\(/, - nOY xb - ;jp:DUʎ0\2ckY,ͦ&k\wUEbʊ:NFQ _r*(VP~[|(B0l5g>BU t]*щvutc%߱0Nkc,l 3R5n^Ԑb]P+uHd|&3e#30x=KvHU=ki;m_0ptn.>'n_Zm}W ->H[4@oƤ`GLspf=1Ze_LRs`6`a(nQ5 S≧3JӾU_zk;qolcoLc9a91 5Rk)C:{PWX\(nvW(hdDzs۵ DQ:8[3.O08ĵ}ݶYٶG1ֻ5]ƶD\"ΡKԼ -*/= 0"&\Bֿl!:!#ԑufq:': ;#;?:kXݵ&$F$W1h|~z=}?o ? n3b{˜XFӛ̊VsBs>)Kk ~ׅBy yV?Kv@x D? ŸBG0qLd0%c Mc{ xgu7v+{u+}u+5=Jp_A_F9\w`l7@0JttTa4F1ned8 Y8ebIeI 52@cs'?25P@` EԔP\ 1l1 l01LF>ba c/jwJRC,Hn!? ۞ Fz0EYf&\TOal>/r* QD('UDE -尢O\\DEM‘0Qn ƹ28_BX=&P3] ^"°O,ߨ[VeJڤRUUG/Tgs⧪+⇪n}Sq[{jVwF@/eP`}\zi=u\Ԏc|p<˘VtS~U*~QV@do}v5uv^ͥQw5y2FkHWɕ(az{tGh.R0#3{g$inuD;nݽov%n=N7coK;bOIǶˮޖ]}$7,5ƣSk<:OQ0-mH8&dհd'dXdYIgs3e]~õ5NW7HL4\rA׭][ަm}^9U3~fݐ{S"6&d2:HF'6Q7t̓МЏyTbq~t[]͙i{iBέ\ΥM8t2&}w }q_GRJEϺ4tKGtfmL,+zwNq›NxLdnG*/ZN~I[rMuG5}qIK@H &8L 5! !!!!B-D~AumγMzvqwHEӒԫ|90;Fzx5OռQQBgC$kP|sKMgZ;> .=w$e@q<;eb6tU!ŕFQ=.Q#U"tGr VMN|D#2fo+) ;i Dokvpa2v\wz`Ys:P{qG2ˬ6d:jH343!ZHXi\:!$;D0ut [8?^|WA ,9ai;d8У;^rji&F7Q*Vnbh -CJdCJ,K!).R.>H_RT -DQ!5({AW ie$J/wwT_ZIqae-Q(w=|kXhރk}twKfWvzoUk%ƜyTe7J| EueTam5-hftd֌W RUXz͏eDL5;hϴ`!@[+{} =L۪_9w^ܧHr+pڸVwU9)ECi6P3&4l6m&lbB&fBdk*=[\?ޅ;`@Iv0ʾd Qџ\ЛvPԓ}-bnV>h0-<m[8u{\r#?Z_#3M/eȐoewbp8S-]YrNaUy,"_M|Tn'UԐZ\&MaSހׯT%=Oz{U z9gIO1!E41C׹z. 銌hԯ-iw {=k aC,CRDNئC -GgsYg./1n_nߘ7wϰ_d8"i46[4X\3 嵄 37%_A_Kn^ ,Pisc ?\tN B`hDSEoOu̐&ʡ'Q <8H1&FTȅ?G|&^(=7ت.Dv6ltvS!{lOVԆ!aP~`*ZpӄqtL:R͠LCsR|I_EuqӱKO,^&?u-Ñ#w6N/►*♺2jw*vEME4ʈhn -ҵПƃL##Z3p'S2'2h,ď,Q ;mu\ݾU{w+igaˣlnsۧ.eqhWD>F` }lAmstl6+Bx4sPzd\yʔŊBb5e Λ]7w+w_cmBKI"KPUvv4 t6L睦y 4,B8,pc7p5aFrD{̔=jG|Ŗ6\!uSiz)kN>*ve]]q+aMLtX*b72):F[\B:qP?Y@PU/bT5?F37Y-gUYv|cVtbYk25X2}ۗiӾ-K;4NҌA^6 ru КHW?w|98/#j8g> ̛fM -KeKr?R|Z9E5.fo/u$F!@n ulJA@5@XYE#.`ǕxZjUlF2Z;sng.8g>>&u)1$~*2~DV]Y$ILLl'bopk9@[D;W"!+H^૫QMmG3ӭ>ȵNSWV:u.NLVH:.K$Mr"91{b7P.8EC`{Jm]inTśXVʳmʲ Y咢LSAFK^z:kQ;kiZ2E]\PUnZV=CU`QJV$YNM.)g')v'(ds8]ٚ&3&I*`Fga4g˨}=@W:Eh[+WV(DVerی"ԒJiRqì5Mh'*o{DZŜb'z|K}jpvAH{WALl:O^UniCan4:vfGɖiY6I55تzv++ETuQOL9DU0DR/i?WS (l BԶ۠rW~>oJn -㬖u1D mRUjNaug}Pw5n|5s U3b7%zCt}v=7[*J5PjfqoEz S:BL6ψiO3ڐk6V*Y%]ֺ)s`7΁-Zòf&^l'~L !߽PvK'^e.=ӑ#Af'OЏl=R-4+Y֕mYdbkufM_Osak%[F~mf6zP(hU((H"y5E/_fm7A|oi=3zj,}{=e{g7>=f>yz LwoTy#@D rH!O.tȇe|5r B<x GhfL{'0yZLA8 c< -0~2$o򏁜a?O@ ([@z0F=7 30z ׋z9cQ- 7ö́1j5򄌑+)?W $ïK(ۍ(0` -2=1 |f Lmo08qLJ8 ƍcGg 0H YG6.Yc \#'+q/?dyTSWK¾!/@ Z*,ʾHHXHԸТH݊8mک^ENw~NrOqw¢)^/  (sHlCbh&M+_<8 xKKO>Nctf' HbGZdAC6=|Gwѽ8}|K ߑgcr?F>c!SMA\=iOcOg#SbH5|'.5ԘhZz%BSE]3QDtNSEWG-M1fO u?Ax}aM>nۓO_)S 'zRC*}I d+d[}̛~?$'7$XW<.z60s;pv*f} τp-#k -,IK`btV`Y.u˻w52U&߽6]n|ѳMF<My~ozIfC޿Ʀ76# uAmaG-Ƽ\%9Zy%FjM0=favo}OOՀ)c>׬>~?l\Ȃ aha߿~}'l_o0'([wb+`50@hlFcYqeF9 3Йۨ(֑p< u}ֽK{BVq -nn -+ogY^G-=w[k [?0^c_t-ě:aqFbe=-V؝՜vkNo o ;(61'ǖg;͑ض9 -|/=7SЅt>LCO!l/5ѓc;)մ31bGں=[n[cm\L_&X'\=* 1X쁭! Bwa0$Ѱѡ 1&T,ҨCh-Mfٚޔ$7'V94%6ҫVovZC,nKCQQ}܈.K]¸c]"0Iiq' AR :Jm(sEM^ğPи(Ƭ Ų\+gjm܂RZW#P BUQVW-Su=#*x!Tc' jLޤQ8' •\CT" RFM.P1ha, 1(6-+Id,[MQPSV -;ej׻ȵe=nRq4ew8],X\iP%28Z2 XNvQSK髂X& Z:\Q-Qg9(J+FuiWIcn(YS üIwypB{a4ܷ 7XFU7(oE42 ÍbrkS,klEV^UM*U.$W4 *\ݢXq04k 3!A4(ADqpU֩Z -X+ -8"8KlUk+.D:Zu}_Z?y}r?hL1+'Ǫ$;LZ_3~Pec^:A?iÖ8g~&h;+Wƒ&^I>7AR9{u{d*`}¬4=f15x/j\jѫ;|v G X{EwϔLmn5l%$ ݓWoy?8lJeҁ529ega:__qڋQq=C89NwnXۚ]2xuj8QgCls4و<+al܃?/ b, q=0DGcr504/"~}Ts{re.r,EvܜGD7H}zQI;q-ri9Ѩ }>mmV ⭱5^tWtSYNq̟Xbߢm_6*m管;k莿+gs' v}8 .B- 8Dz 6PF mmmmdۤ}hͮnyc!xP:շn+9 -d ;H΢l@@ѺEA0[TV%=вdKƠE4++JDuIU>%Kſ+ -T Oﳿ&3{_3[_ wE .R.uCKɗ"ۡrAWY E ] ombj/e?fSXhf? rh^U?mwfpػ>pbP٭P؋!vI/3xG@S` -j'hjXU5@¨#񥤆5kxk ^]zB/Hf,d~Љٖ@ˀO&|P:t^;5o -@] a\:$dwXNR]% -RJ:RpUu~ߜ%Hx]/dϦ{̽j  ~O9^D.Ue纍.O<Otc BTPwk`w%襸MtlVGKf#d<3#Kwa5,Ն!]jr}va2v7Ἰ}[S-.R\ -@!nӀ\!Cu~a/ZlEY`<7"{n\$n q͸Ah?J ŀ2EyuŹf)4S6b*B:Ul| 2ۚ0#Zŭ i4UT$wT9Si̴MuڴWUb*PԅJc~ 2W :b\Qq}nE%󖱢2YQjPYl:e^Ɯ>iNs8ar8.hvO820|aj|tmGF8BF"xY;ը&(n1PO|3Bq"zQt8/ǃKGhE2 jĪ}Eb'{c\jl!B!$K@$6Ibر@ 8X$vl'Y&vL=i&I:Mm433{y9^+b?uUL$L(8/~?b.JϤTJ> FI l -|,Ki #ޖ_LAYB e"dDG_ŞvQe sIZOKB/yȝK3kFJ0}n3уL̽{T -rr\9fW 9eAEEr5 z!֜l=+;ŝ\2S cr0GY)kXP!JEBlkdJ'+RRD.0ԓy 5LG aŸ!5Gh@h@DЯ$a!0\̜/񨻍(#AḦ́˚Ͱy 4iӥr)uQL6WlzUDQs=\,+ {xw/\: >ulG<>G~=<𞍄r98 z1iVIEזƌ3SZPJ\ܞx"Y|RԡZoS'&>Ij){|K !hQ< 0*A-3^d hE0cJ!MaCqG_NU{ʭ.m#۩mv4BfFԤYJh,]KNi~ɦ[ڴ߮%'7؇1wޭF0w]żcX[kN&U&VfG TF4nQZjFn5r]FЮk/'Yt6~5F_EO,g50_.|\}DW1y+Zu/iВFEtUG9*=QYhc5T4sm&Z@bbR&# -ӏJ!@xU&K>zpu6͘79o=Flc 61]ASF4[˩ -ʪnX;恸Jqʟp|&]ՇȗIz"CG/_p3u8mx 8لǚyRHG"^mQlFY]gTֵL}qq:46ZGHX#*qe_k%xa>}g6ּ::1wvw ҠMζm֖Ljn=LnhG; --Zeijg[nAYè>_b9Qe5^Rs|^b;Gxa}x&ּ+?1s [ܮhhw~{I6W*֕IvFVvǘ:˩ -zy{-ns[ -[gDdZ$E,,siJg|XHCBr<(ds r뀯hf'07!_R:WƊ2B_}(VM* 6U M Me0?;ϋgnse@@Ӏs%`-TMP^q7W;AT(ĉff>XxkU@c^_ c?\p/0Qz:Ue@+ n:ԤnZpC͐7݀3!/o)ca؉?DZڏCKswُM>0U﵀&Ѓ)yocܤQ}E.>o9G윸x~Q`:ϞWXx}ͼ{~⦆5i`M󞬉"CFQl`.~ <_ @]Q }Fi -ͦIٴ66*TL';1E;w<;A&W E8>UQ1=H?y,NxdJ<2uQ-R.iOeEBvWjz/+/ x=K{+~rK NX2Z*L-!Kel%]ϒ%#/X -|* -})v\UlSl}Mbc#?4esZ 4tU\q/Q]}IEcdOΔﰦ)[+ZW(7[sUͪ #s5oPtU]*60>kt&T Q?wQ=F*Nm %4N)h"/_WfWdkr6hvج o"nYo̠6ABmАc̿B$Q~<)p0EaWHiCxڰܰъ_({NV ^ -]dLk$d>=H(aAha^S}ZO#=vn4ݛjfWpj/s'Ϡ?FJ׀7GbCdr#H91Pf蛤^'Ygi3lz2 h8;8R}J_#6{܎~f췏l:lvژȉ醕1aRVtYFtbaʅ&-jiّ" )+G7Niq4%CrcG ;ғ=FYcP'pFnXoEF|O v"-6Q͠hfLΈIM=ߐe41zWCR[c@a [5{砚}>)8 -|`BV `)-,5!Z>ʔULM7]?1nݗbWq\>r{c ;ғm|/#Y.h=?goÌX<5/e GAkТ!#@ Az@TBt]OZa]-3umn~L _|?~i扫t$))2k89ǹ0ՒJT2k7gk[=LڃYSL^&3iH$%QS{ Krٻ>5`:d1UKkR$iAzc~97⚣[XVu'4i^ԛ4#uNpK J?sYIjeC?14LӱظP\!?kԜsr2\ VAZwmꔌ5I^Z Iz-Y/(bkی8(bq1;¬Ay¤c> xc&;b|G:1SYQ1#:As9|ҩw X=|}鄓2v q~ x́GO4=ˠ5½ -PBEE(z<(O=޷z]ɸ-w "N! t;< Ji7N}7PHI2$9CԿp;7qBƝIS0"@!tIeKo4pe" WX0/#tpL.#?o05w1cbzx;~~ 3 'MJpT,=/^`Q|9Y0y\t$o>r|O~|F!Dϵg/PdcE]cAnArKĂܑlX Y,?`/G|b‡hEE>{F)[6SDϣ̘.c x6o>&w -C}1<%ă=&YEyCp m49q42,&$ Ud=LZțNr qO?/ -z%qx:)$D-"d% d+APg?u1q -xk%w~AE?4tN"|G҉Xy8&>y;uvQ ?uR8ۃo>?pnA+r7Fx@qnT\9C41$[1jlf4h:Ӆ/u<;HT}Pem:X5$p 1$$"B"QmCToB -~ZC j]FҊ6\lU~\_qQBYOU"1J F* !zR}/&4w|kuWa\QƗ#.hVs|يs=1|Nw'#k"uqb  $?-2zp۸%Wb7;>ŹU8ӽz4Þ8ٳ'z^m8k:CO`kO]"_ǘ٧1-O$.E&꟏Q8ÑEOg`f_BfOr2lav lpxixm71Fd7w_AB> ' ]8\Q|L|4h9'6{6`ٮf:S-ڙLv.Yݤ]nnbkwrܺ!g5CV_>T-ÈG<&w銿6ZY=[|0,Ga_pҍ6 [tyجs0t%zmu:vMl*[/m[bJ,ѝ.ݒ6x,m!J? -O?$[FM|@380J]b^q!ވ,fM\2]WʶUqKiB}YHҤ_%o5OW̸񢔐O%RkL!jy{Io$('ClH&$%IfQmNpM$2BZ -P )Ҟs=n#ڌ2tŪh1hNMaf3sRNaC1,36 -K5e0j*` -G11E٫`Sg~+Ofz^b)K29sG1sӐc 7_k*ԘҐi:A/OL_LoWUPQ*L,Ch>rp:>iBzeE6l r1M4侍>'d[PJYҪ2 ܤW(6uy8ƓuE^W(6ҜN`g!XK- 5?OY=1#?ov` UyvΟ-R%(ZBe“LUQݭqZ>8,;9,?y™'ʝQxɷTd8GڳX@~*P`ڢQ3a6=$fb+ -rٲWZPėX}5 + .ka][׫m]NVM_jUTXE gܤ:![G-^]4:u&rDiי; ^Q%k}j_ooUwj,\ub3^wY Gr`C3}Qye1LȸfnowKlE~F/zGn)\)\*ܮ6,x2Js KtNRS*4~$'j+텒x|Q䋃7q2 t7畖Kr!Yw]Q{;TiޣTďQV"_ <3:S P4vNO~%npUFQ9FXҘlRir* J$?IRF*ErVe*IXPT!*E9!{:;)`Tҝui />aB0H1șldBLf(5\ZO N$I2Cp0]<^PU T$ -QSo&7h"i4L#UOs: {\?a0G!=p:c 066)a ~nL>\yTƟ3, -ʦ0 '-Dk$F5O465"eE 8Hpj%&*.TӨ(1> -&y{E -9^٬IθI&9]hBm^]u KY+ǢVwdX'!-'Y00g#YT:Gaf)r -/lV&TƜҘXe\*T%R=PC_7f1&yeVr dia=H>}BR8Ο,$}oɽX{c?&ؾc~RĬvywR@Դ`5GQk׋WI%0PCi4K+MA/@t Cc4b嘆HG;rX/usRغv)XHk}/q ;z8x@Mi3_pz"©G3*ViDhe -B*"r8*Ǣk$T͆U[U}VRS0\$1θلyY&7Vlc<.=c6$z =08WO] Թԩy$&ߓBwp_F;~v[.vB-ӎxJd"%"SB ԩN 5j{q|˿C?N?D_/b"Od -fRg>u p6Q)\s;SU[whWp}+\D ZBӅ9 H^!M?Ө3m&SǎXC56sjnݸX|8%:Uj- @oX ^zXHo2L77Z3X Ȧ -󸮥F5*phf,Nc'Y@*o1zuAS;hvcGbl ^;CQ T6`sQl -n?Jp!! "',Y<8}hHBmcj"G:rĦ lZDB4zT픊51n(T{GUHOic{WT^o}kd4hg7Pih2X8 PbxņiuBT#'Ib9/a2a"axFq-ENcEv:Y=k=ן@|U߶^pĦXcBIXcզQ74QZek!0}$-3-rPe*S,1mU,65*N*$Pf)盅"JCơqs5>}{`%v,iȵ2j/e[&IK-Ŗ兖 y%[a)-%yjťeyޙ{D K] qDpFf`fD -5.Kq-5zXTkĜ4mz5m<96ij4Iۓd1w= |zemA6G#ulI1kLslJFɄU&3-X,VUZMI[a(wcm<+1Vl y+6"SH"?7wg:xuH?6#<MXmE%4X2EZ -S,7{2 -ۼZ[b~^*6o]BeKa?LK^Ze}%s4kahEI٦*t۲mPj+KlbͫqֵRul:lsm/ԬkCzu]˸9Dq-빮l-#QW -eʔ$#JLHY"8Xr]+~)W$/U~Q)ʅpEy'<[!܃Yż1t7|ۊQBRu&T@j:\L5IRԀXݭRYVxO^YՐLܢߗuJ@o/K} J#Pdc:9pHG#KPX&.q5َ,Gjo2;uq.,q3l>P/^0GO4l^\NGV G3 -w><\$丌X]9bCJLWcqҦ6H&gltҥ^եOuo4gH꣰+y|'{X[rzTB^i$1qO➉ŞdyRaX,|!S$.TFO&ͽNkpoRrFIII>KB^ޠgS@-H zdW BVHX+' ; <)XTeʆUVU(ebNL,n{OKqޫ)ڸʐ6'.S8\>84ʕ] \n ~OFo }HYSs >Ź͚پSX[hbBڟ8tf`5 |?` 4HnX< S1?$0o.f0fi8Ycc 1Qu@fύC PD3I&s[1efƌhLoiqڪ3fL ((@Lс<؈ =x*)`|W ~KwQ{s+=o^[6 Q1LLjۢ0m,&MSX<`*30`1FkZن;aX"FԎG=a֐Qe۩BFT'%`^ v>ۣ0} ڣ1}t'i;w,ǠF ؂ h?[?CAzdݛX'$b_f1G Dqrº+RW,] Lz?]  |d8paD8vs 0CK77[7E. ̹3_oI}^3vi=EWGA a:-Dr:0 3G_l]BG>Z{#=`7ԧ__ DQԾI@!j{r aCя =aOpaȞߓ{G{]E Ybj٬5{#|Dc1=GO>g`|C x/y=dO4 rjbE 20*;o!"\>'ug_KH2kDT} ** EZnnhYDQA@B"2bM01rRV&NRV8ff\*5qܢo~T{=缤O~ld!Hu'3enDٍ^ӉYDdd"d3AvtS"oq?xW?" ~ 1 1tKlF3`'5ڨqssg#>mj O9z<&ȿ?eg7N&qdOT@EꬤF5j8s#5P{8g;V!}i_2:2G;C5ķQĝL%_AԌ3sӨCBjF%5jH-_'QB//} Moq~$7 /DÁc 9}r]*|=c\| urQDUԨF-5>V9wd4o鋫˴wi0Z"6;ٙ0eG'\;kBq5JN&gͣp y]U -Dh9YВSG|kwqlCyeÆo$O^17x Ұ,\p9bu,ǙU85|z6S 9G#qGF^Qߠ1] sh!ȓx吻|!+ȍpy~.)DpM1lt-C[ :jtmD6toO$xm}qoc<6WL7OfRߛ70L.Ot%wW􎝆|=^ }`M.Ůk:-ScJ O9Sylv M=D+4xB y4O3 : -]&s6L*gsf2ϴaR4{bW*UY[Q?kP7S+}]s_\uS^})ZO.;v{{bs%}4h1' >VlDoj|P[*BoηUX3P>G\=X6rޏb|Y,yP<\{-]~tS\ `*aRJ=ʔXLAҎ"eBY$,W ˔br+-]--YtI#e?!,CG߈.10vƲ1-Zָ. Uc6C}PUT:(PLy!E_H^X -1cx@k[Hb[fB+:q#1&.헥{Rh2q<3I+s#kvxa>Y=DlvBP&-~,"d%ĞXVjI 5bԴc1ZiCvZ3\o1\r{y{lb>Kz 4&Vq.]#4"!RhX0&>'dӀ~M}̽5G%]3G%>4G%VhdeT>` - 38E<gTJ&;iHbR48%LSh@jT6Q}Ҧ+:mҲ+3m),)tUShShdArCc#˰ Jsz2gکOzguStV_ٱ -ώS -˙МSPE9kS+c͹,2L/RXݲ|އB}0 f8*]A -, V@a Zƨe|,3mIU7"ue<-\GacFgWA+%r:!-;klql}Q3dcMW2UP -#[yL@^RE7_W?7Hq؃R)`+5okTg/S |!adg,@PՑXuw\ xº2s/)kS -܍>iޖloaHa1~R=Ci}_CP o,^Ç<OXI-A GhFoz<^ÒsdwT2GvNI8Eag0?:Ǚg -hrM@-H| -/:'֣?<ŕldllj֟%hMFg&9GEq\#dG(+t|+e`؛=vEHrsh@:st4CjQNFi-9c֋]DNg:ЙCGaoA:N:K(gJm5b>i-mP՝ U|ǴUl';cWC(NzM=~WO2|u{7W ?w1ԄZY?T}40VEq*  zM f*7h+;8WYEYy!GsC+-)%)a_ڸŵ7+x(0fl#Yik͊P- %,@=# ^+eOiJWZxR#2Q>_ -h- ZE%Hy!@$ $BТmN!Zҭ͵{3nu;֞vNZ!~>Ͻ`͊O= S&',V iw$uLs0^5K>[R)G{Z -6g-=Xaڌ -pŸQ ?|mX -o^:"YDX\f!U<ຒX`d?|lΞH)EkӰ:;9:rLh)GCN9u]Q-\ʰQjIY̡TP/*IT80Tf?گ>8b팣E5yhV-O j Q`e<y [v&*w _4#2]Y&H4cO79rZM;렂Ʊ _39j&c6.N:tpQæ_/'EYQZ' ̆wd%["G+ ?Xu ;i& }60(Ӱ9lT4 -a+,ƕ(5`1h(.z^ɊLC2iTVh#HEc[LyB~'Z$[s8ܦIpf -bLa5eXX0QRBq*[`4(0zOd:yc/"ɴfIJG=L+s3Y&Pa0JR si:K0PTf̊ʽЗ?]yX}"z\loTBe(me-rA/{"z`-]c,Mb{,KQhO|+UA[YJ*WC][:l1pYUD+~g9 ۀ}M.G}\fN*KqC0TGB_]"hj!&y5*P9P:+ZlW3 tu WH=*gDjUO!wIBFs/QwZǀ'Ɇ5y0(Ȑ_{8CAGL V;V߈Vx2oR/#{Z$y HDGqKU(=C$ s[*e^ Oo*2}QHGZ"țRڔM*,o6`YUHj"ѿK['?m$4CB$!s;ڹ'Zg[#cR3 Hi@R$bi,i]G[Xܪ ;u Fl 11man% ¼"EB̺1q}~ux@s `3]9 ;v#%L-[Jpmcr60%&^JI$"|HL8x˥^ȩI @@ \*""^b2T@W=j>gmt]36v[NvݦsT|?D~;K NH#H3i#ϑm%1|I1G,Cy|G3y~g_2)ѐ,O"ƯgFCldbOajWL#>[_0o69aOƒ #5 &$dP/:jTι_72~w1N.~vp:kߤ0ڍ>$%qαRgrragaoj^ԓ24jZ}\ q>)tvpgp//^_ğYopjG708=]O͙xԓ3I<87+]Jjbf@FRcn)C\vV{k4Wy? C~9wyD)B8%3/ DQU^jM]c:ut='ye&I-`SGch"x^Qy1H}^Y:9?"56qj66LubTZGKB<kW)hVub]X1eG;Kf ?6I:E1g ~s7ڧmFeV -5f4`Up>V.X6!QҌ%!X50<_EugxG|Lw -d*g> Iǚl)X>#"BTGP\,SEsajTϭCFTmCyTG@Y̋ Qo -O}؂ձ!|u iKd煕Q~X=u1cQl2jPkDe qN̏,Q⟠Hn (D -•>.SL >{Hh%kS'F$ $Ơ\*C4 z$d(OB܉(L\|2dp:F87`O9Ia0x'29gIigk譞>'B>e`H![BIFlp&9H*F^r%K))w"KyUȢ(S`n:ظv``볎>VG-}+Ǣ$t(#R#O -J٪\Rݰ#+F¤QsDyUd _Ads6x:ْ>(}T"O -:X5)hĄLm6KaLAn6tۑ? HcNAF?V'.w/Zd=F.V}0,9ԋa+`ԧ!Ð -}Bk\q=LH5|Q4@A4 " F'Aj?xK1#MQ2gLdL3a Yt -ZL ԙyH!RU2d"9k^>d{|~y0Bc{?wp$(^ J5ři h!Pۢf"Ֆ UlRHqCSyRHoԾsGBl$va3#{/u+9Tq/𹼜<\z1:EPyCYH΋D# -rGd $:m:!qAEk-b]g|A#% 9i?wQ{">9*VK!G%=B$A ;y @TQ `NQ#"7#xŠO!m9B!H@+9Κ~/;9_ -4s]QQXWeueߑE=-fQ(̸ `T 0q8QU bզAlVMM`L6{bCRc4how{ xmcb-fJM`PW`ŘՔhSE(4\ldZR[_yE`oجloCkiYΧ6B}3UXO|)uF(6VvЫ -dά<Ȇ3D$ͭlJabWS2mzAWڄf_0'xni]' )vba'luPC!d|R[Yp156v)40wIwvjQ:jXG .@Z.Z}-Kbna14,ttN_tb\KK34@ o/uahj!j6pENc$\bq'-%r?= kgY,zA&Z@q.IX4iÁ=9]lix3o'3#MF{- ~FK.wuNĐv>Q@$M1p2 u1(}\4׸7qț|m‘3}ldIƿ>_{[4Ү)yWP(]%| [6]?>FC#c61qF./l~ `0)`(bJzآ~d|isY;}/\pedZ AwH0Ŵ}k1˰_}- :55u]|gu N|OCx̹7T} c ضa{.0.S0I v͏C8 Zנ"ZIJa/`߈"ih~1/Ƕ \M?Ч<~b*-a8k7刦NƠABc")}gcfcߪ^N*ȎSD2P-T+nKK_ϡ1L4ʓIg#?EhXrc;YvO^Ö}51%;JUhi#:cFg1v՜\; keҧ.]:6k 8qW:Dy{+ePvw9] ƧimZqGiV9hsV8s\ -eJ]*TNuA2_T=z6k\FXᷔaw͆SUnZ=̣U9R%3EiI *4,\JhX|~C9>5fSvVfspN_FъpuQ7N :sh@ h;3bʹvK| ]9ʎ UVd21fF ֌=Vi)=MSf)kRb5)\c7+9Zh|qݚy};`sآ#;EXojz\kj|MV*%~&unS5Qr5.at_7W=hTbF&6jdѳm/uT@T@S2 0 ]`H䲨1 `y ^K$Zf*hY)=Zֶɶv:k%ִܓ?>y}}˚ƎL}%q4bb\9\0 -_EUG$+7ª\eGڔYQ5ʌ5EJTjl,5?NܛEra #NJ + q-z -)?zrX͎1*#&U,*-ήԸjYR)JNإ2%Șx~)S'FNm[q88GE9^2LTaJ3D)Ր KB JI,Pr\%ͪiV'4˸Q2ː.;?^b911AaaV٦0QG%#dɔ$cRf%*1D ɏȐRfřE[*| w)<@75؇~gא2jSHdd8̓d4*yS~NC@SKoޖt/*zXlȤ,bI&XP,cR4QE -*(M+NTV) -M-Ճ%4CJɧ䔼K>yC6&35‹JzYQ΅Zz-X҉oya+>J+)5I0=hD{&3SV$_VqOjlyʳ<˳:8e ,\~Zʹ4\SHl2y1!P&JOոJ? HyU%ʳ*UcQe{"n[FVKիUF.wZVmhȠF΅ǩzr@LI1Z(7T:B(GFe遺 : ;лX_mQg?ߎI~%g#=Rb|J cʥUÛйM\\k1>$mIgiໝSQ;vMG'$]0P`C@uQN w+ -}|7[ًO -FwJ]#y‘PRa#> eԥ8 t4v71qzjiW|?-/҃ -܏WO1xNA^SIAN$'gR,Yhmy׵u/`ͅ35b%Ұ>Z ҅\Opn!p8>c"5ec,ýKKf+ų`ߐoO!|z-Kp\uCѫ 7RnWosܦHv;; PeP hfh(MEyEևb7:󺮢gKp>5HCax$q`\,?Yu !yaMZ`{!`{9)E h̏Qh;:.iofp^'Ѻ7/}J3G~1`9U~ YKm@k6Ӣ?ڵzGOѺѩlS$8AQn<r_ w_pYX;|r"𓈏4-"el -ֱc X:V;؎t^*ׅ5h 9$ V,a߆};plՐZ| -]ɳ|kyF;lݘ؀m@an_L b -M&kk^5SWUv6ҤjTiViӤݴnUNC}>}}.Wy%z"Y/_{Ob> -ۻ3>wiJ>EOOUE79𓣛}!\+q~F6e;K 0"WҽMyޑ`HRsxx/Yڱ8]c~9Xze TtOҢQB|c29wxz8-RLSγhqyi'Ooi=lff1s c`4!F?јop4Vc:Wy,=|`oŔ>1 D`1*u`6ƎjLza"ΏnhF0pC LÒ{_CI"%M{MlbdK II%LTc,QcI%:0,APڀ!i H;0 EldcdsI^EwUtmDG{+3wLYfV 37C;1"##) /ՆCГք6tw`z?:GϘ2іyYwz ^ω?9B x6`uKlfiq'L) (R‚@v9NgѦUGޜ!4LE4?-xo s@Ïy uQD\, Nݿ{xmtMc[:oCxhdj2q@FV Z' -ܨ-ZKg1TΡRav+(7~@N<_&-7p%~X Rud h,LGz}jTpp2Ԣ؄ -c;E즣(3@y6uX-/>K%"Y=r`wps:T:&9&*Mp a7Qn.CŅRKlm$iXga]GQ"}opKcm*q-$ RG7u2VP֊&E&.wm  li&IX9㡭BS5uv۠T!ӆ^(Fp -Ho"!R䳈=%.p$[;xuwIE덂99r(ݬ«CׂL:|Hu!7 yI$lBr ҖHlI-_Y̷۴?77s, ijڥZ Qn Y8H!ůܟd2pAф;! !3 OqF_|g|AfY㼓#VA}FK=J} io{eD$ - B - -D !L`0!J{e7#bɽ6ɼ0Xa,L|qzJ] PSSm$;8D'!b8 --,FI> d0 1y7ȹf{5"Iq[\9 N98|_%~ / .) ._\Z!,8 ]u'0B(5wN FO3朜>dPg\Ҥ}jCtrt\\ȯkK8D??8{=<<wrx\O &5y vh}q- t=! P Fj0ؔf/TdV [=v]Ku_}K7펝ץc+ XASZQvg+tB-l7?ckncgX>Ntho+|+{n* ^k踂?t\B{lum29wtt"w71pyG\Vx塿ۏa -]x: k %^i optDoq>!;p(cv;i w|-88,~>^rlWaد@Z=ZAXf8Z_m&:-D`kwp~ >szK?"'f)X~vcN^F[4Eт&tl2!'*,42"^q6Q{rר_\1a#bP Gcᨆc2pӜ.E(>;Fvf|¤n 3a#<H#8 --F"t)rkKwմZ)eZTmjKU}*{lVEσF]W=x2undy>уnOun5W&h{j3T힫qCU1Bc=U9^= y.RJxh~Km**qtS>TgYB*ݨ|V {Sy+0w^s6|;:rT -?Oj_J#5ߤb*ꛪa}sTw+?\Co50hv++2*3䞲B#u jv}L$7TA* 2 (F588CB)7P9!_) t21RJq%ڕP -xwۜC^_IqD7g C"De(#<[JԈ -DNRRl٣Zw}L֘Kp(щCp`<9k6d>FBr<̨~J2(-*F)QJ+):C ׀r%j"ǭٴ]&gzWq2}61dZʧyo":*1CБTCd3F*'k\T%b.TLje΁kJJ%QC|`jK0~a||}XzfXd/,`0oJfJj_5 TE -WQ%+@3T.¤Vބ;@_Kڕ|VKH F7D`aLr_hvȣ<ȣ<iFF6wh es-**12eXI3b3Ism2Q"@zcn^NVN$)I&O/\T ypײ ~ h -x Nw 8 ^o7h:9ϚQL3xm\|pZ+>V4X9np 9 -%pb]79E|Fk.=tqߣp_ ~ @z! 8d -%8b&qO, 7G;[s}F7}#8>oDX׏xobE.!}F'W\G8?#} -y 7{//x8xB/?xxd!]ʥ?8 -Jqq`2ϓ9cʚv鷈uXi<^^G~_['228}@-1/i z]@"b#v91::f)d̲%8 )=`A}`7x#vL*%x[fEA>Nlb=Ӊe2į~xVav];aA-63ڧamFnf:iyZG1cW6!~>gbE,C %F3QVXn8ױwl=>t 3mIB6wh=X)p1b8{V e5YЕ(އq#%Y/>`ݍ.F($ p< -G - 68jȣZӴ<G UJ\ #J7á{6h^b{?v[!{8v -!J$D @2-Dʂ xPW`k,@9GY?[ԟ0G^m8rК.5~a_\0A O:YT W*N.gd m$VM{Mn+rޓ+}GXo|/DA]U9fy;kfTW5-hr,lSNCݚ;d\%X mh#aǕ~Iww[~8:ZڲE7*HFyb=41\ T3f(8NYɲGNWfd2"+Y&YMj:.,/>R+цhkknтҖQ|k9T(ƛm,S/My2̣d3[n\F)%fĬШW#^wh 8ӂ%mhY y>̠NSikTF.+_l# 0 3ΰl -(0.D4Dwq;hc9&٬i&VLlkXSi&=iZcܲUt=}kPqQE! 0ԩ,+7lFSNx1WUJ3nUK)rF7r+%.^nrn-d߂Y?=N#_<&0ҧzs+&OQA1#RVg&),3]Y -*հI2dPp<(0C9?(8/<39AM֪lTi&Sy?;pMgq$*rkPHnȐP`~˿̖qHA>+#C *B ܼg9G0s%\*(EYX'btTP%b_qq-OI,WWҍ1 %gحNՠ:iV4x i|U<}/!㤀A+ Р2|ˌ)h`yr\rYX -0嘵rCƠSW.jB豉4/Ɩd%ӛ BM -0wE=\.BD."c'1!Mdb61;-s8KpG`O+yɇK*a@ȡ3$x \ \5\t5\@5д!`u+-M_M;"88Lum6{&P\ U jbibh[6ҋP@/DG=lyC2D-\X:` -XX G} 35ã9p5XᲈXĢ.$ml||<[\ -nm 5ʡ]ֱ@!H]/Y@ & -VxҏVx£%RZq.|j&UL+q4+ZOX9HfF$|6K[w+(݋ < d۹xvzю(8r965]@:r;zgAK2>Ab{婋t} A===uh $߽V3u"o%9KɓFbvI9V#= u̐ǜc@E?eb(Ea.^zCU>_Z>QA\%!Կ_p55AGy1~ [/ g?>q&8Ǣ%Yzq]9@"g 57<Ǽ=f/΀w9Q|P5Xr*.S 8yP  q-M\׸p@E~_).: x~B>G"QUmpA0ҷx̯5c=U K+<.$;?1?R>@k?eέx -^?ni 53|5ezqA#_L -^.{8 3w𗿂8#=C=:n$2y?t,Y?8VrEr?أ8G:rXD^]M2m~A馲.= ݠ&救GZq+YȑLLV8DDRKX%_"6cvv'iP6Դl_+u:~G-rE.9ϢB1į DWc Ğ2 YNy: ߰Z_j%yWx=19v-{E'{Cf$Ilq1 BjrԑdkL76`0`n&&`CbH'@B(HB[Fi.K@%Ye (mfi6AZN]5mӺ}m6MӦM۪}ؤjڥ4G.S =z?y99『w f 8$7el{W('ߡܿ$xqÖ8 1Ua#f<ߦg3q;cX5#Df= MSw)h5졅p$v1iL.x -8K)gYBDim` $]v>NK<n'2LY%u )tY='e*\v/q~J M5+ɢmIښQ{rڒ˵9%M)aES՜USj61m"Z~D XR(j ?R/1~ -b:m:r"8+GS IږVQQU6`(VBZ7֫޸Qu6Mݪ5Ri3)yNU -VUX>T3SKH'bCطEȓ&K"L3TgU\Z_5ZjUm(hک_Qgͪ,mUb{Sg;>S#{Lo&Yg{(C$;I!Qk,ekͭ*[*m媰W^2GJ[SI39OQsWޜw6 %33IcuKZ~vlF9{IW3SNʜ.*8'"WH>涩 Gyv?ʬQ5` Q)-[J&RVj0vƟ^Dw;X҃][K> 'dMSfI,kr@ ʨVz(PRB=J -)1N0uR;HfbM ~f/w_ዾKax e"'q!a$|:xĞqbMG#a{i{sp mx AY2`͐ѐb: }0q8k]A(nbL4n"LvLavL"&i0bK4A<&?åC){1ǎJw ցJ9>c;cܘɋ9.?7FҳB_Hx| -:;_ U:G;0\|Hv,bb,R(2 $y{8G^~;?oسEi㗩WH*_%p p Ǎ' 67%X,e -2X&8ҫ>_{Ŵna"r܄*_a |n]M>gVcB~PW -Iʊt9c/ggTW6\ۏ_ݛ¸oo=^I/G!R6\{tƟ6%inmzK4IIKKEZ.E\1AAȠ ás)`e2q2&sӝYiOs~/<Yz,GG>ۇ;h -{mf5*c?,ks51#ꋚ b԰>_8?@}^Gnx7u6v̀/b@2(CAw6ڦq-gҿu7g8?R<7{{BGeER?.jK?wvT=:uч踂89,C%tz gz@{཮kz _>/߈M_p귪һܷtɜGG8qyqa6WqR6K'Hz0v]_p|ܟ>ݛ,::)tGsc88#8Zܬ}d/ _R@m!B#_y \b3e'"֯MzGek=:Bt5JR=pt±nѽ(\Sݰt*O.r?b̘C"f'Q~mmIG<4vPAo ɠy#ynsmEo - 8,OUB$P]*,Od_ 2\G{?vX-s^tSsd+\x -)c:h_P -~/k$?fOyF>OqmrѺ!.sSc>;\䱧"p᪇pMdptvZf^w@dG\ȝ --a4uAL&cjHָA9ʂ͞*P}LHuª4Z59_'`K0\RE-U$Fp+mw_ղqlI&&cqjHNQ8:CjU`b+4$JUI -$5ȗ4VH%oUqOxBwU`BSDEOƮGm%#P1i(bPɨ!Ft94y4T*Rjܤbs -*HW5r[)\ʱ\zD $#F#ϯw泥8!7#kȍߚ -YJ-*RBK -UX):Jn[rm3/T}RcxGi3-»[1nŌsJnFS'R*U`cO۞<{r%r9|I );AY㕙>MNgҝ+*{^2^5MlkH=Sl-~@ Fy24+iSө̌\P -93*=+4L={l]< K7#L_O̔zvx75RxeXՅ|vꤖz P#6(e3Ǣ49\#L*Yek.{LddHO*sLIs#>|o #c`;3 mcHrSCn|Ĥ* Y|vY -ke,K)EmJ.+x U\Qy|;rȻ chiCG3#t27^RL%VJe,u) % VRYH 2*ۡXxK^n"/˴2K-pg]9]m jF_-CF2֓b$&@>*-JLx_b}^ SH~gCcc ~cUEp>4q*=NsaXFh11+)`bA MhPi`0MA C?Vj)x6{LzӐأVBV7q7 $K%l\xa0t\x ǸcBHuhcC걓zCON0yy@0"dF\1RkRivHMdM4pġӄ&45GoLLk.Khhh k\ni)![ 9<h#;?;: 6+Xy#tp 30hs1 ; 9tG7&4nrхU]Gy,AUEpܳ:^J<a<2h6ƺ gGI'M/uE賏FG.Y'ṿ; 1pa0p{Lߐ {%W@Ca!WқO c *r1@_RqpfLtLRl`ut^o$6hVӐq -8.sfp>rFqخR+_W.0Y āt0Rοgjs;pH}A#GGs"^@ aG>|Tp!X4T |pƲ~kg88K8G<N]zS'u/ >z:=E;N*ңn<7U#` :._ORܠԍp/h=k!G!^7YJgz\hDt*bn 6^ 489x,؋h2GM>:p6Nv4#ԥY EfUR0we mXu8# teDt2!Ue/Z"\B.j(fmV]O{ jȭ7\~t \χc9)2xYŮC-Z@泳R\ ,F}9(48ĵ5xW:EiU5YJϨ.&j$ -n1 BxS(fYjC(i>'{ogG;k}+l$n9C5rxxK;\p%'/\p k4\5hr#{#PN. -idgqedY1@3zMaL$?r2C&X5>ȡ1A.%jTPFgiD a!w+'tCV:7)5C)O( 1|!OwDt.Xm)1PANO!ǁ紆*dT^Ur .eGxGRZySbdSd{< =ZQ1]!2YQ,jLN\r,rNi 9~LSeLȄ*W*,qB=9  -NUHO]pwL,Xߕ|VLl)f9#'CNPEyLxl2{2yeLJUD0(-U3Ui0v|:ɮL1v -ͥB+tr)D]᥊y -ݸ~0)\*ָɽû{Xfmհ2V|ߵ=růĔTMT۩jEZWj^vqq*B˄ΆKZ[µo5c[_U`8,G bK^2ٓ:hh5i|1/jZVXA>ך_,N7Ѧ _\[=_iu`xD@yy_2%ʹx>r؏{Թr`jf>+Te$9 `cU: I ~%ٱ/袁/h _s)qqlK3[j ML_>7\;ֲc4QkTT((kx[w -ሕKk4U@{.J1P╢4 ŗqE`ƎUn\ɼEi]l'${.yǵ1Ja} !Ϛ:mfG3m4I3]4E35q^'$;i츎[u r@ -1T<ȸѹm a-߉MKvǀz(j-|BL9~3p.Q3 xԭGn߶dN;|ܛ}6'Ѷ$3'qR<%&4S|qJ~DzR>ދx/9f |ʸ'yj= kâٱ ]0!,ڣp~ӳq0rN<Qٗc;ޥ`|<\^\e>PF<?WOcq|xiorM_a{ u| =&RK忚6W$dv}*1?X߶i{#_\Y3Nmc} 6>|d)];__/9Գ -3%OlOI' 3d,mB=E;bW8{; -,g_^U*IltBtl x( $/g :{'iv6l`gv;8hûCQO)͠s'I=. \x)9)#+yJ9ۉxs'5ۆ Tx>)3tSI/ WB)t~-vk~ƻFvNZMsEp]z>Dk;ddI8,ybi|ENbWVf{crVրco5(Xe1/sSG j+GYvꎣ7b%8pTȊ*J3LJY–ٲ_h9 -ukTz.?.7i<%oD,!`R8\)`. .jȥHB@H1%폎@TXb/&f:.cK4#1wsb=8|LfҖxxCCxt $2N(mt 5&j0T?CpmG2aEh9K(U/0q&{@AkX = =Y&zfͺ uЭ>HV^iPfPwމЋlxH9,4ٲ5f` ,x808!qM٠)]I l"10BTI##P$Ccba܍2Sc5#&F&G;Τ-gҖH#D >[3F5b( Ab${izз9&l^}p"F;b2!{asE -D&x#8j$,byb!p,dLY]ّ!1CHlp\q .U%NLH-Rdch -^@D3Hvgxq|Dp*жhcHu}67jʌ -?R#3I3< PS,> ؞Eq\=-R'6;9IAzɆٜI6|XdA,@W־+Y?[ړC iBF-(ӊ -A[(oq@j ȡ^s8j$,AE$h~?Xhڊ>ǁ-•a0|!St+R)5D@*zmahCFnlV7qm͐pnyQњ+{O#Ok R>5y]Nbs0 ;P^84~EJcil)%dtUY#Wq€rFtGz](9dj_8`]భKJ7HKwsؗ1TT..(rۮѵ}4f>z{ϟࣵLAϻsƌzfzkfL(քC ~h?j}CJ3E%/c_TVJ*pT_xEy\_^Hڨ;Wi YA"ҭ[l!Iv^يR9$Vd2nqy>=/<y;s+Nw $ ӟmWy0\*c<0gלuN@B! +G[Yu?R|^rrH/坑,~$K]Kn`l=Z5[7q|gUnr"~F8ߛ-cY đ\ೖ-K1Es)`[>zyH]PF(볫ܤ;dqFV Lk-zPߔJK{wWy~P'C8d,ߴ. :J@7 dzqF@` V" 6X ##  ZeWŔԃN~a~qfu#E".lйy.?Xϊ ;m HK=`(tu4G!gn_:^!B@zhCLZ8l$@ -+ @ e!OAx C8~ⷎNs]=/I֣3ѡM*{q6ljK~!}9Ym!!_7Hlް(Qppj`0GXs,D`+/xGF@ҚSШ -s=t##URuMT?|zq+[:sMnִ䂹33o\P7.B *OEtO1o,N4GO\ٞ~pc݌)GR0XQAl(f4 M)h@<׹L"]NJYsr,'%hݹv - ݆/U)|JnPW -x kFEQ`0|=t[ 1x}fpc3A&ŽpJ ~ 7%1,۰PRND,^HU0uf>7웻ñ]zQZVq6 S d`0XA#GVJ[(9 -RWvHo^0x3 bx -p`+gQ(^1ױ>9ږ騬*^x#qb ,Y2aHwcVMOb/f=-ȁ/} - `=瀾}k) -4`" C!)p3:mu@XoQv ngn3w:s+*qBV- M$NreO{}v R` 83JyMO4)XZGyQj{DM {_πY ̸Ӻ|)weUefᨈ.A]]dciI~\w<8/t Pg+e >*7E`S# 3\GHpχHn aKS[K 5uk;mɶcVރ iEHD_+߾U\'9GVXJ¬9M<~̨փI+qijL9%A0pcF"((`77Q#'q h[:-H,n#*Z_YXO -=Vy!pLYzY*K;x2}{"w7er"Iw:GSy\V[<6'Rչn%:溬'5mDtbZL\&$ -ܾ~vן{}߻<%E&gINDHJ"NƄdD] Q!c@ -d *>7 8PW% \ h`3^l:93cM|;egA :܂8XJ[7XI|0|N7w[{EkvcJȬi%J-Q#u|FBѵ<~ԠVTw|_JvV{J,͓ɯ)l/` R|Vxfm 96pL1c3Y0ߜ,/NP[@Qt+eKTe9ۏ-p -Ȯ|BpW$ %IHO޿y:~0?_(gD,rE}KcШ+)J_*=I,?!4l=Å[Pծ=Ğ [ }g OZO$o!xL=5dbBC) Oմ>RIr\r"#;@V2[kclzi5a#*Xm?;62.#:ĉ֙Li_8L+ endstream endobj 370 0 obj <> endobj 369 0 obj <> endobj 368 0 obj <> endobj 367 0 obj <> endobj 366 0 obj <> endobj 365 0 obj <> endobj 364 0 obj <> endobj 363 0 obj <> endobj 362 0 obj <> endobj 361 0 obj <> endobj 360 0 obj <> endobj 359 0 obj <> endobj 358 0 obj <> endobj 357 0 obj <> endobj 356 0 obj <> endobj 355 0 obj <> endobj 354 0 obj <> endobj 353 0 obj <> endobj 352 0 obj <> endobj 351 0 obj <> endobj 350 0 obj <> endobj 349 0 obj <> endobj 348 0 obj <> endobj 347 0 obj <> endobj 346 0 obj <> endobj 345 0 obj <> endobj 344 0 obj <> endobj 343 0 obj <> endobj 342 0 obj <> endobj 341 0 obj <> endobj 340 0 obj <> endobj 339 0 obj <> endobj 338 0 obj <> endobj 337 0 obj <> endobj 336 0 obj <> endobj 335 0 obj <> endobj 334 0 obj <> endobj 333 0 obj <> endobj 332 0 obj <> endobj 331 0 obj <> endobj 330 0 obj <> endobj 329 0 obj <> endobj 328 0 obj <> endobj 269 0 obj <> endobj 373 0 obj [/View/Design] endobj 374 0 obj <>>> endobj 268 0 obj <> endobj 375 0 obj <> endobj 376 0 obj <>stream -H|TyPgfPwCWNMO0r$ -jD - 7 ".pGQ[.BQD1!jTy=~Xn䯭ޫ{`8u_v/ڧ+6x+Cb#$dz8?ǔĂAQ۷o'O,D >_#Ԇ˖99L[ sZhьu+2}2R# -VDZQ!JR~H)C*Z&!BZir2REOgV(% -|BP#G) -(3]cPq:b22p`Ycg8fac[a.8 Vb1l1LWfL,Əe7&&)&&EޢvSW8 /D'%s1+7Y/+ud|S[\MTTP;V1^He`ڙ:*"a|oeohڣP#v..@һĔ,V->[ZU9RTrE:PuC@z9WC*Bh`t%I+Q܃T݉2Ay͡+qUocR2XpjtI7)<ᄠ0YRz(?{B(9ؚz:T< 7ݐT>bys7䜭nb rNyhchxFv/mm,zd5 -p-$N錮q3Y+u*"WV~Tǿr!) :~xԲw:ho !, AWGj4K%z@b#d]?P 2fg\r0G{$̆/3|vpn5"rQeӽP6 |p<\Xv}ݹ+\EeKҪ %ڭ)s^Lv^YV Sg(MfNvYi & -nBU%r.Mޅ% -)R'0&1~Nq9Iu_ P09 BHJP7: -D/!uL\8{ϡt0weY9t"*PW/ddqK ijnĚ .Y6eDym˺W=5,N<8n |ȐzHx (ȋݣ,?8ꞾWWBxe(Tщ0ߊn~LdUm1,`;?# v73M'B!L:Lnmuٗ-K?f$;;'$^kE0WcXon?ѵr}RWM>ENݏv`Mɳաz'153 tg,[;$C|VRi0i1H57H'bl@ZRjn:z9e3ʐ +:a^'^3 '@1-ˁCN%Hl͸=|{QISX6lt}2ξ.qI6`p87Jx<0u"9H22,kj*2}IDyZɯ0\[{@;ѝxZ_ " T[E"hH8( ѓ`z|ivRD <>-n?F%V|T~htcµb iE]IB;uF:mR2Wr y/%󔽘CfhNƄ QEʢse3R*tfHGE";Q@6rHNPMDh]R\ -?_([%7,-TF9%m̽x_᳊C #*>)mȫnaT8rtjİ¡swxRl4.1^cf o.{RI*2+(#4VOgNu(k]D#_fB1*v |]RP<-W!Z -FV@*v"c|Nh 8)J6 &Q3xya6}Zy`XllnD3=<s}]XPιH*R|sC+8,po5\uY>C)`5GO!6F<ڭ[H[6E7$]VO8t{Y X0cU7CdϷkhh0u#RE\h'ViF0Qx7aJ7S 5w{|I/+30O;ρU]Uv߲aA}\aD2N'3n&t]K^/{@4ZYSw,,fXkڿZzFft_:LBTu9k)W]kf#UP^~,B== ǡK,;EZ-'-*II*IRD#Iõ+'WV?*٭{+-i endstream endobj 275 0 obj <> endobj 277 0 obj <> endobj 278 0 obj <> endobj 279 0 obj <> endobj 273 0 obj <> endobj 377 0 obj <> endobj 378 0 obj <>stream -%!PS-Adobe-3.0 %%Creator: Adobe Illustrator(R) 17.0 %%AI8_CreatorVersion: 19.2.1 %%For: (Philipp Sommer) () %%Title: (psyplot_framework.ai) %%CreationDate: 05.03.17 18:36 %%Canvassize: 16383 %%BoundingBox: 15 -581 581 -15 %%HiResBoundingBox: 15.0627209584636 -580.21727907953 580.217279041539 -15.0627209204704 %%DocumentProcessColors: Cyan Magenta Yellow Black %AI5_FileFormat 13.0 %AI12_BuildNumber: 147 %AI3_ColorUsage: Color %AI7_ImageSettings: 0 %%CMYKProcessColor: 1 1 1 1 ([Registration]) %AI3_Cropmarks: 0 -595.280000000001 587 0 %AI3_TemplateBox: 298.5 -298.5 298.5 -298.5 %AI3_TileBox: 10 -653 569 130 %AI3_DocumentPreview: None %AI5_ArtSize: 14400 14400 %AI5_RulerUnits: 1 %AI9_ColorModel: 2 %AI5_ArtFlags: 0 0 0 1 0 0 1 0 0 %AI5_TargetResolution: 800 %AI5_NumLayers: 1 %AI17_Begin_Content_if_version_gt:17 1 %AI9_OpenToView: -231 47 1.5 1928 1078 18 0 0 -4 38 0 0 0 1 1 0 1 1 0 0 %AI17_Alternate_Content %AI9_OpenToView: -231 47 1.5 1928 1078 18 0 0 -4 38 0 0 0 1 1 0 1 1 0 0 %AI17_End_Versioned_Content %AI5_OpenViewLayers: 7 %%PageOrigin:-8 -694 %AI7_GridSettings: 72 8 72 8 1 0 0.800000011920929 0.800000011920929 0.800000011920929 0.899999976158142 0.899999976158142 0.899999976158142 %AI9_Flatten: 1 %AI12_CMSettings: 00.MS %%EndComments endstream endobj 379 0 obj <>stream -%%BoundingBox: 15 -581 581 -15 %%HiResBoundingBox: 15.0627209584636 -580.21727907953 580.217279041539 -15.0627209204704 %AI7_Thumbnail: 128 128 8 %%BeginData: 34800 Hex Bytes %0000330000660000990000CC0033000033330033660033990033CC0033FF %0066000066330066660066990066CC0066FF009900009933009966009999 %0099CC0099FF00CC0000CC3300CC6600CC9900CCCC00CCFF00FF3300FF66 %00FF9900FFCC3300003300333300663300993300CC3300FF333300333333 %3333663333993333CC3333FF3366003366333366663366993366CC3366FF %3399003399333399663399993399CC3399FF33CC0033CC3333CC6633CC99 %33CCCC33CCFF33FF0033FF3333FF6633FF9933FFCC33FFFF660000660033 %6600666600996600CC6600FF6633006633336633666633996633CC6633FF %6666006666336666666666996666CC6666FF669900669933669966669999 %6699CC6699FF66CC0066CC3366CC6666CC9966CCCC66CCFF66FF0066FF33 %66FF6666FF9966FFCC66FFFF9900009900339900669900999900CC9900FF %9933009933339933669933999933CC9933FF996600996633996666996699 %9966CC9966FF9999009999339999669999999999CC9999FF99CC0099CC33 %99CC6699CC9999CCCC99CCFF99FF0099FF3399FF6699FF9999FFCC99FFFF %CC0000CC0033CC0066CC0099CC00CCCC00FFCC3300CC3333CC3366CC3399 %CC33CCCC33FFCC6600CC6633CC6666CC6699CC66CCCC66FFCC9900CC9933 %CC9966CC9999CC99CCCC99FFCCCC00CCCC33CCCC66CCCC99CCCCCCCCCCFF %CCFF00CCFF33CCFF66CCFF99CCFFCCCCFFFFFF0033FF0066FF0099FF00CC %FF3300FF3333FF3366FF3399FF33CCFF33FFFF6600FF6633FF6666FF6699 %FF66CCFF66FFFF9900FF9933FF9966FF9999FF99CCFF99FFFFCC00FFCC33 %FFCC66FFCC99FFCCCCFFCCFFFFFF33FFFF66FFFF99FFFFCC110000001100 %000011111111220000002200000022222222440000004400000044444444 %550000005500000055555555770000007700000077777777880000008800 %000088888888AA000000AA000000AAAAAAAABB000000BB000000BBBBBBBB %DD000000DD000000DDDDDDDDEE000000EE000000EEEEEEEE0000000000FF %00FF0000FFFFFF0000FF00FFFFFF00FFFFFF %524C45FD0EFFCAA1A7A1A1A0A1A1A1A0A1A1A1A0A1A1A1A0A1A1A1A0A1A1 %A1A0A1A1A1A0A1A1A1A0A1A1A1A0A1A1A1A0A1A1A1A0A1A1A1A0A1A1A1A0 %A1A1A1A0A1A1A1A0A1A1A1A0A1A1A1A0A1A1A1A0A1A1A1A0A1A1A1A0A1A1 %A1A0A1A1A1A0A1A1A1A0FD04A1CAA8FD19FFA8A1A19AA09AC39AA09AC39A %A09AC39AA09AC39AA09AC39AA09AC39AA09AC39AA09AC39AC29AC39AA09A %C39AA09AC39AA09AC39AA09AC39AA09AC39AA09AC39AA09AC39AA09AC39A %A09AC39AA09AC39AA09AC39AA09AC39AA09AC39AA09AC39AA09AC39AA09A %A1A0A1A8FD14FFA8A7A0C39AC3A0C39A52515252C3A0C3A0C3A0C3A0C3A0 %C37652A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0 %C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0 %C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0A1A1CFFD11FF %A1A09AA19AA09AA19AA0F8A19A2752C29AC29AA19AA09AA19AA076A19AC2 %9AC39AA09AC39AC29A7652A09AA19AA09AA19AA09AA19AA09AA19AA09AA1 %9AA09AA19AA09AA19AA09AA19AA09AA19AA09AA19AA09AA19AA09AA19AA0 %9AA19AA09AA19AA09AA19AA09AA19AA09AA19AA09AA7A8FD0EFFA1C3A0C3 %A0C3A0C3A0C3A052A0C35276A0A176A1A0A152A1A0C376A1A0C3767CA0C3 %A0C3767676A1275276C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3 %A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3 %A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0A1A8FD0BFFCA76C39AA09A %C39AA09AC39AA027A19A277676F8529A7C277627769A524BC27652765276 %A02752527C9A52277C9AC39AA09AC39AA09AC39AA09AC39AA09AC39AA09A %C39AA09AC39AA09AC39AA09AC39AA09AC39AA09AC39AA09AC39AA09AC39A %A09AC39AA09AC39AA09AC39AA09AC39AA09AC39AA1A8FD0AFFA1C3A0C3A0 %C3A0C3A0C3A0C3A0274B5276C35252A0C327A19AA127C35252A02776A127 %7C76529AC3A0C352769AC3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0 %C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0 %C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0A1A8FD08FF7CA09A %A09AA19AA09AA19AA09AA127A09AC39A524BC276279AC39A279A7652A0F8 %765276765252A09AC39A5251C29AA09AA19AA09AA19AA09AA19AA09AA19A %A09AA19AA09AA19AA09AA19AA09AA19AA09AA19AA09AA19AA09AA19AA09A %A19AA09AA19AA09AA19AA09AA19AA09AA19AA09AA19AA09AA19AA1A8FD06 %FFA1C3A0C3A0C3A0C3A0C3A0C3A0C3A027A0C3A0C35276A0C3277DA07C27 %C35276A05276C3A0C37627A0C3A0C35276A0C3A0C3A0C3A0C3A0C3A0C3A0 %C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0 %C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0 %C39AA8FD05FFA8A09AC39AA09AC39AA09AC39AA09AA127A09AC39A7651C3 %9AA027524BA09A7651C2765227529AC3524B277C9AA027769AA19AC39AA0 %9AC39AA09AC39AA09AC39AA09AC39AA09AC39AA09AC39AA09AC39AA09AC3 %9AA09AC39AA09AC39AA09AC39AA09AC39AA09AC39AA09AC39AA09AC39AA0 %9AC39AA09AC39AA09ACAFD04FFA7A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3 %A0C3A0C3A0A1A0C3A0C39AC3A0A1277CA0C3A0A1A0C3A0C3A0A1A0C3A0A1 %9AC3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3 %A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3 %A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A1FFFFFFA89AA19AA09AA19AA09A %A19AA09AA19AA09AA19AA09AC39AA09AA19AA09AA14B529AC39AA09AC39A %A09AC39AA09AA19AA09AA19AA09AA19AA09AA19AA09AA19AA09AA19AA09A %A19AA09AA19AA09AA19AA09AA19AA09AA19AA09AA19AA09AA19AA09AA19A %A09AA19AA09AA19AA09AA19AA09AA19AA09AA19AA09AA19AA0A8FFFFA1C3 %A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3 %A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3 %A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3 %A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3 %A0C39ACAFFA8A19AC39AA09AC39AA09AC39AA09AC39AA19AA19AA09AA19A %A09AA19AA09AA19AA09AA19AA09AA19AA09AA19AA09AA19AA09AA19AA09A %A19AA09AA19AA09AA19AA09AA19AA09AA19AA09AA19AA09AA19AA09AA19A %A09AA19AA09AA19AA09AA19AA09AA19AA09AA19AA09AA19AA09AA19AA09A %C39AA09AC39AA09AC39AA09AFFCAA0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3 %A0A19AA09AC299C29AC299C29AC299C29AC299C29AC299C29AC299C29AC2 %99C29AC299C29AC299C29AC299C29AC299C29AC299C29AC299C29AC299C2 %9AC299C29AC299C29AC299C29AC299C29AC299C29AC299C29AC299C29AC2 %99C29AC299C29AA09AA1A0C3A0C3A0C3A0C3A0A1A8A1A09AA09AA19AA09A %A19AA09AA19AA176A099BC99BC99BC99BC99BC99BC99BC99BC99BC99BC99 %BC99BC99BC99BC99BC99BC99BC99BC99BC99BC99BC99BC99BC99BC99BC99 %BC99BC99BC99BC99BC99BC99BC99BC99BC99BC99BC99BC99BC99BC99BC99 %BC99BC99BC99BC99BC99BC99BC99BC99BC99A09AA09AA19AA09AA19AA7A7 %A0C3A0C3A0C3A0C3A0C3A0C3A0A19AC2BBC299C29AC299C29AC299C29AC2 %99C29AC299C29AC299C29AC299C29AC299C29AC299C29AC299C29AC299C2 %9AC299C29AC299C29AC299C29AC299C29AC299C29AC299C29AC299C29AC2 %99C29AC299C29AC299C29AC299C29AC299C29AC299C29AC299C2BBA09AC3 %A0C3A0C3A0C3A1A0C39AA09AC39AA09AC39AA09AA19ABC99C2999A99C299 %BC99C299BC99C299BC99C299BC99C299BC99C299BC99C299BC99C299BC99 %C299BC99C299BC99C299BC99C299BC99C299BC99C299BC99C299BC99C299 %BC99C299BC99C299BC99C299BC99C299BC99C299BC99C299BC99C299BC99 %C299BC99C2999A76C39AA09AC39AA1A19AC3A0C3A0C3A0C3A0C3A0A176C2 %BBC2767676C299C29A7699C29AC299A0BCC299C29AC2997CBBA099C29AA0 %76769AC299C29AC299C29AC299C29AC299C29AC299C29AC299C29AC299C2 %9AC299C29AC299C29AC299C29AC299C29AC299C29AC299C29AC299C29AC2 %99C29AC299C29AC299C29AC299C2BBA09AC3A0C3A0C3A1A0A09AA19AA09A %A19AA09AC3769A99BC995251BB759A75764B5251BC755251767552757651 %9A519A517675767576279A99BC99BC99BC99BC99BC99BC99BC99BC99BC99 %BC99BC99BC99BC99BC99BC99BC99BC99BC99BC99BC99BC99BC99BC99BC99 %BC99BC99BC99BC99BC99BC99BC99BC99BC99BC99BC99BC999A9AC39AA09A %A1A1A0C3A0C3A0C3A0C3A0C3A0A099C29AC2997C76769AA0767699769AA0 %76BC527676769AA0517C5276525276BB767CBBC299C29AC299C29AC299C2 %9AC299C29AC299C29AC299C29AC299C29AC299C29AC299C29AC299C29AC2 %99C29AC299C29AC299C29AC299C29AC299C29AC299C29AC299C29AC299C2 %9AC299A1A0C3A0C3A1A0A09AC39AA09AC39AA09AA099BC99C2999A757651 %764B764B9A4BC276527576519A757675765176519A757699764BC299BC99 %C299BC99C299BC99C299BC99C299BC99C299BC99C299BC99C299BC99C299 %BC99C299BC99C299BC99C299BC99C299BC99C299BC99C299BC99C299BC99 %C299BC99C299BC99C299BC99A19AA09AA1A1A0C3A0C3A0C3A0C3A0A19AC2 %9AC299C276769AC2767699A076A099A0517699A09AC276A09A76997C76C2 %767C9A769AC29AC299C29AC299C29AC299C29AC299C29AC299C29AC299C2 %9AC299C29AC299C29AC299C29AC299C29AC299C29AC299C29AC299C29AC2 %99C29AC299C29AC299C29AC299C29AC299C29AC29AC3A0C3A076C39AA09A %A19AA09AC39A9A99BC99BC99BC99BC99BC99BC99BC99BC757699BC99BC99 %BB997675BB99BC99BB99BC99BC99BC99BC99BC99BC99BC99BC99BC99BC99 %BC99BC99BC99BC99BC99BC99BC99BC99BC99BC99BC99BC99BC99BC99BC99 %BC99BC99BC99BC99BC99BC99BC99BC99BC99BC99BC99BC99BC99BC99A09A %A19AA1A1A0C3A0C3A0C3A0C3A0A199C29AC299C29AC299C29AC2BBC2BBC2 %BBC2BBC299C2BBC2BBC299C299C2BCC299C29AC299C29AC299C2BCC299C2 %BBC299C2BBC299C2BBC299C2BBC299C2BBC299C2BBC299C2BBC299C2BBC2 %99C2BBC299C2BBC299C2BBC299C2BBC299C2BBC299C2BBC299C2BBC299C2 %9AC299C29AC29AC3A0C3A09AC39AA09AC39AA09AC39A9A99C299BC99C299 %BC99C29A767077707670777076707770767077707699C299BC99C299BC99 %C299BC99C29A767677767676777676767776767677767676777676767776 %767677767676777676767776767677767676777676767776767677767676 %777676769A9ABC99C299BC99C2999A9AC39AA1A19AC3A0C3A0C3A0C3A0A1 %9AC299C29AC299C2BB9A4C53FD11294D4CA0BBC299C29AC299C2BBA05253 %FD3B2F5353A099C29AC299C29AA1A0C3A1A0A09AA19AA09AA19AA076BC99 %BC99BC99BC999A4C29282928292829282928292829282928292829282928 %9A99BC99BC99BC99762F2F2E532F5328532F2F2E532F2F2E532F2F2F532F %532E532F532E532F2F2E532F532E532F532E532F532E532F532E532F532E %532F532E532F532E532F532E2F2F7699BC99BC99A09AA09AA1A1A0C3A0C3 %A0C3A0C3A0A19AC299C29AC2BBBC53847E5429535354295328A27E292953 %4D53295329532953299ABBC29AC299A12F532FA853592F7E53532F595359 %5353537E597E53532F5953537E5A537E2F7E7E532F592F532F592F532F59 %2F532F592F532F592F532F592F532F592F532F5953532FA1BBC299C29AC3 %A0C3A1A0A09AC39AA09AC39AA19AC299BC99C299BC4C54534D7D787EA853 %787E4D7E7E537E7E537E7E537E532928294CBC99C2999A2F532F2F7DA8FD %057E847E7E59A8537E7EA87DA8597E7DA82F7E2F597E7E7DA92F532F532F %532F532F532F532F532F532F532F532F532F532F532F532F532F532F2F2E %C299BC99A09AA19AA1A1A0C3A0C3A0C3A0C3A0A199C29AC299C29A2953A2 %537E77A97E787DA977A27EA9537E53A97E7EA853294D282F9AC299C2762F %2F532F84FD047E59AF7EA853A8A8A853FD047EAF53A97E537E7E59A87E7E %7E532F532F532F532F532F532F532F532F532F532F532F532F532F532F53 %2F532F532F2F76C29AC29AC3A0C3A076C39AA09AA19AA09AC39A9A99BC99 %BC9976064D537728FD0753284D53534DFD04537E28292829067799BC9953 %2F532E53537D53595359537D5353537E537E537E5353532F5359287D7D7D %537E537D2F532F532E532F532E532F532E532F532E532F532E532F532E53 %2F532E532F532E532F7799BC999A9AA19AA1A1A0C3A0C3A0C3A0C3A0A199 %C29AC299C276FD04294D284D2929284D294D2929294D2929297E29532953 %292F4CC299C253532F592F532F532F532F532F532F532F532F532F532F53 %2F532F592F532F532F532F592F532F592F532F592F532F592F532F592F53 %2F592F532F592F532F592F532F592F5353C2BCC29AC3A0C3A09AC39AA09A %C39AA09AC39A9A99C299BC99772929284D2829284D2829284D2829284D28 %29284D2829284D2829285399BC99532F532F532F532F532F532F532F532F %532F532F532F532F532F532F532F532F532F532F532F532F532F532F532F %532F532F532F532F532F532F532F532F532F532F532F532F5399C2999A9A %C39AA1A19AC3A0C3A0C3A0C3A0A19AC299C29AC24C29294D2853294D2853 %294D2853294D2853294D2853294D285329294CC2BBC253532F532F532F53 %2F532F532F532F532F532F532F532F532F532F532F532F532F532F532F53 %2F532F532F532F532F532F532F532F532F532F532F532F532F532F532F53 %2F5353C299C29AA1A0C3A1A0A09AA19AA09AA19AA076BC99BC99BC997606 %29282928292829282928292829282928292829282928292829284D99BC99 %532E532F532E532F5328532F2F28532F2F28532F2F28532F2F2E532F532E %532F532E532F532E532F532E532F532E532F532E532F532E532F532E532F %532E532F532E532F532E5399BC99A09AA09AA1A1A0C3A0C3A0C3A0C3A0A1 %9AC299C29AC24C2F29532953295329532953295329532953295329532953 %295329294CC2BBC253592F532F592F2F292F292F292F292F292F292F292F %292F292F2F532F592F532F592F532F592F53292F292F292F292F292F292F %292F292F2F5953532F592F532F592F532F5953C299C29AC3A0C3A1A0A09A %C39AA09AC39AA19AC299BC99C29976284D2829284D2829284D2829284D28 %29284D2829284D2829284D295399C299532F532F53282929290629292906 %2929290629292906292929062929532F532F532F532F2F28292929062929 %2906292929062929290629292928532F532F532F532F532F532F5399BC99 %A09AA19AA1A1A0C3A0C3A0C3A0C3A0A199C29AC299C270292953294D2953 %294D2953294D2953294D2953294D2953294D29294CC299C253532F592F29 %292FFD07292F2929292F2929292FFD0529532F532F532F532929292F2929 %292FFD07292F2929292F2929295353532F532F532F532F5352C299C29AC3 %A0C3A076C39AA09AA19AA09AC39A9A99BC99BC9977282928292829282928 %2928292829282928292829282928292829285399BC99532F532829537E53 %29292F53FD0429A92929282F292906292929062929532E532F2F287E5353 %06290629062928532F2928290629282906290629062F2F532E532F532E53 %2F5399BC999A9AA19AA1A1A0C3A0C3A0C3A0C3A0A199C29AC299C252122E %5934340C3412340C3412340C3434340C3434340C3434340C3452C299C253 %532F2F297E297E537E7EA92FA8535AA9FD047E547E7E537E292F29292F59 %2F5329537E537E54537E7E54532F7EA9297E537E53545354532F292F2953 %2F592F532F592F5353C2BCC29AC3A0C3A09AC39AA09AC39AA09AC39A9A99 %C299BC997C11522D522E122D582E34113433341134111211341112113411 %12115899BC99532F2F06297E53537EA87E5354A87E537EA87E297E53FF53 %A953290629292F2F532F29297E287E53A87E7E29A87E7E7E547E7E5354FD %047E2929062929532F532F532F532F5399C2999A9AC39AA1A19AC3A0C3A0 %C3A0C3A0A19AC299C29ABC521252522E52525227522E522E521212123412 %34123412341234121252C2BBC253592F2929547E53297E5354537E535328 %7E532F295354537E532929292F29532F53292F7E7E537EA87E547E7EA953 %547E7E53547EA92FA953FD04292F2F532F532F532F5353C299C29AA1A0C3 %A1A0A09AA19AA09AA19AA076BC99BC99BC99580B522E3427522D522D522D %522D1211120B1211120B1211120B12115899BC99532E2F06290629062906 %2906290629062906290629062929530629282906292F5328290629292928 %29062929292829062906292829285353290629282928532F532E532F532E %5399BC99A09AA09AA1A1A0C3A0C3A0C3A0C3A0A19AC299C2BCC252FD0412 %343434123412341234123412341234123412341234121258C2BCC253592F %29292F292F292F292F292F292F292F292F292F2929292F292F292F29532F %53292F292F292F292F2929292F292F292F292F292F292F292F292F292F2F %532F592F532F5953C299C29AC3A0C3A1A0A09AC39AA09AC39AA19AC299BC %99C29958111211120B1211120B1211120B12111211121112111211121112 %115899C299532F2F29290629292906292929062929290629292906292929 %06292929062F2F5328292929062929290629292906292929062929290629 %29290629292928532F532F532F532F5399BC99A09AA19AA1A1A0C3A0C3A0 %C3A0C3A0A199C29AC299C258FD05120B3412120B341212123412120B3412 %120B1212120B1258C299C25353FD05292F2929292F2929292F2929292F29 %29292F2929292F29292953532F292F2929292F2929292F2929292F292929 %2F2929292F2929292F292F2F532F532F532F5352C299C29AC3A0C3A076C3 %9AA09AA19AA09AC39A9A99BC99BC99580C532EFD045253525228525253FD %04522E53525228522E522E5899BC99532F2F062928290629282906292829 %062928290629282906292829062906292E53292906292829062928290629 %2829062928290629282906292829062928532E532F532E532F5399BC999A %9AA19AA1A1A0C3A0C3A0C3A0C3A0A199C29AC299C27C3552775277525353 %5352535253537752535277527752775253525358C299C25353292F292F29 %2F292F292F292F292F292F292F292F292F292F292F29292959532F292F29 %2F292F292F292F292F292F292F292F292F292F292F292F292F2F592F532F %592F5353C2BCC29AC3A0C3A09AC39AA09AC39AA09AC39A9A99C299BC99A0 %0C534C534C534C534C534C534C534C5228534C534D534C534C532E7C99BC %99532F2F0629292906292929062929290629292906292929062929290629 %29292F532929062929290629292906292929062929290629292906292929 %062929532F532F532F532F5399C2999A9AC39AA1A19AC3A0C3A0C3A0C3A0 %A19AC299C29AC299345377527753774D77537753775377527753774D7753 %774D77533499C2BBC253592F29292F2929292F2929292F2929292F292929 %2F2929292F2929292F29532F2F2929292F2929292F2929292F2929292F29 %29292F2929292F2929292F2F532F532F532F5353C299C29AA1A0C3A1A0A0 %9AA19AA09AA19AA076BC99BC99BC99BC7512525227534C53275228284C53 %4C5227534C534C534C534C2F51BC99BC99532E2F06290629282906292829 %0629282906292829062928290629282906292F5328292829062928290629 %28290629282906292829062928290629282928532F532E532F532E5399BC %99A09AA09AA1A1A0C3A0C3A0C3A0C3A0A19AC299C29AC299C25712537753 %77537753775377537753775377537753774D5958C299C2BCC253592F2929 %2F292F292F292F292F292F292F292F292F292F292F292F292F29532F5329 %2F292F292F292F292F292F292F292F292F292F292F292F292F292F2F532F %592F532F5953C299C29AC3A0C3A1A0A09AC39AA09AC39AA19AC299BC99C2 %99BC99C257122E5353534C535253525352534C5352534C532E3451C299BC %99C299532F2F292906292929062929290629292906292929062929290629 %2929062F2F53282929290629292906292929062929290629292906292929 %0629292928532F532F532F532F5399BC99A09AA19AA1A1A0C3A0C3A0C3A0 %C3A0A199C29AC299C29AC299C2997C585834582E5834582E5834582E5834 %58527C9FC299C29AC299C25353FD05292F2929292F2929292F2929292F29 %29292F2929292F29292953532F292F2929292F2929292F2929292F292929 %2F2929292F2929292F292F2F532F532F532F5352C299C29AC3A0C3A076C3 %9AA09AA19AA09AC39A9A99BC99BC99BC99BC99BC99BC99BC999A99BC999A %99BC999A99BC99BC99BC99BC99BC99BC99532F2F06292829062928290629 %2829062928290629282906292829062906292E5329290629282906292829 %06292829062928290629282906292829062928532E532F532E532F5399BC %999A9AA19AA1A1A0C3A0C3A0C3A0C3A0A199C29AC299C29AC299C29AC299 %C2BBC299C2BBC299C2BBC299C2BBC299C29AC299C29AC299C25353292F29 %2F292F292F292F292F292F292F292F292F292F292F292F29292959532F29 %2F292F292F292F292F292F292F292F292F292F292F292F292F292F2F592F %532F592F5353C2BCC29AC3A0C3A09AC39AA09AC39AA09AC39A9A99C299BC %99C299BC99C29A9A7076767670767676707676767076769A99C299BC99C2 %99BC99532F2F062929290629292906292929062929290629292906292929 %062929292F53292906292929062929290629292906292929062929290629 %2929062929532F532F532F532F5399C2999A9AC39AA1A19AC3A0C3A0C3A0 %C3A0A19AC299C29AC299C2BB9A4C532929292F2929292F2929292F292929 %2F295376C2BBC299C2BBC253592F29292F2929292F2929292F2929292F29 %29292F2929292F2929292F29532F532929292F2929292F2929292F292929 %2F2929292F2929292F2929292F2F532F532F532F5353C299C29AA1A0C3A1 %A0A09AA19AA09AA19AA076BC99BC99BC99BC997628292829282928292829 %2229282928292829282928294CC299BC99BC99532E530629062928290629 %282906292829062928290629282906292829062F2F532829282906292829 %062928290629282906292829062928290629282928532F532E532F532E53 %99BC99A09AA09AA1A1A0C3A0C3A0C3A0C3A0A19AC299C29AC2999B537E53 %4D294D2953294D297E2929284D292929532953292F76C299C2BCC253592F %2F292F292F292F292F292F292F292F292F292F292F292F292F292F2F532F %53292F292F292F292F292F292F292F292F292F292F292F292F292F295353 %532F592F532F5953C299C29AC3A0C3A1A0A09AC39AA09AC39AA19AC299BC %99C2999A287E537E4D7853A8297E2953A853537EFD045329532829282976 %BC99C299532F532929062929290629292906292929062929290629292906 %29292928532F532F2F292906292929062929290629292906292929062929 %29062929292F532F532F532F532F5399BC99A09AA19AA1A1A0C3A0C3A0C3 %A0C3A0A199C29AC299C270297E537D7E7EA97E53A27E77A9FD047E53A97D %A8774D294D2977BBC299C253532F592929292F2929292F2929292F292929 %2F2929292F2929292F29532F532F532F2F2929292F2929292F2929292F29 %29292F2929292F2929292F2F532F532F532F532F5352C299C29AC3A0C3A0 %76C39AA09AA19AA09AC39A9A99BC99BC994D287E7D7753A8537E53A8537E %29A25377537E7E537E5328292829289A99BC99532F532E53292906290729 %0629072906290729062907290629062906532F532E532F53282906290629 %28290629282906292829062928290629072928532F532E532F532E532F53 %99BC999A9AA19AA1A1A0C3A0C3A0C3A0C3A0A199C29AC299C2294D295329 %532953295329532929295329532953537E2953295329539AC299C253532F %592F532F2F292F292F292F292F292F292F292F292F292F295353532F592F %532F592F2F292F2929292F2929292F2929292F2929292F292F2F592F532F %592F532F592F5353C2BCC29AC3A0C3A09AC39AA09AC39AA09AC39A9A99C2 %99BC994D2829284D282928292829282928292829282928532829284D2829 %289A99BC99532F532F532F532F532F2F28532F2F28532F2F28532F2F2953 %2F532F532F532F532F532F532F2F282F292F282F292F282F292F282F292F %28532F532F532F532F532F532F532F5399C2999A9AC39AA1A19AC3A0C3A0 %C3A0C3A0A19AC299C2BB9A2853294D2853294D2853294D2853294D285329 %4D2853294D2853292976C2BBC253532F532F532F532F532F532F592F532F %592F532F592F532F532F532F532F532F532F532F532F532F592F532F592F %532F592F532F592F532F532F532F532F532F532F532F5353C299C29AA1A0 %C3A1A0A09AA19AA09AA19AA076BC99BC99C29A2928292829282928292829 %28292829282928292829282928292829289A99BC99532E532F532E532F53 %2E532F532E532F532E532F532E532F532E532F532E532F532E532F532E53 %2F532E532F532E532F532E532F532E532F532E532F532E532F532E532F53 %2E5399BC99A09AA09AA1A1A0C3A0C3A0C3A0C3A0A19AC299C2BB9A295329 %5329532953295329532953295329532953295329532953295376C2BCC253 %592F532F592F532F592F532F592F532F592F532F592F532F592F532F592F %532F592F532F592F532F592F532F592F532F592F532F592F532F592F532F %592F532F592F532F5953C299C29AC3A0C3A1A0A09AC39AA09AC39AA19AC2 %99BC99C29A29284D2829284D2829284D2829284D2829284D2829284D2829 %2829289A99C299532F532F532F532F532F532F532F532F532F532F532F53 %2F532F532F532F532F532F532F532F532F532F532F532F532F532F532F53 %2F532F532F532F532F532F532F532F532F5399BC99A09AA19AA1A1A0C3A0 %C3A0C3A0C3A0A199C29AC2999A294D2853294D2853294D2853294D285329 %4D2853294D2853294D29539AC299C253532F532F532F532F532F532F532F %532F532F532F532F532F532F532F532F532F532F532F532F532F532F532F %532F532F532F532F532F532F532F532F532F532F532F532F5352C299C29A %C3A0C3A076C39AA09AA19AA09AC39A9A99BC99BC754D2829282928292829 %282928292829282928292829282928292829289A99BC99532F532E532F53 %2E532F532E532F532E532F532E532F532E532F532E532F532E532F532E53 %2F532E532F532E532F532E532F532E532F532E532F532E532F532E532F53 %2E532F5399BC999A9AA19AA1A1A0C3A0C3A0C3A0C3A0A199C29AC299A02E %352E352E350C350C340C352E352E352E352E352E352E352E350C3576C299 %C234353459FD043412343435343434353435343534353435343534353435 %343534353435343534353435343534353435343534353435343534353435 %3435343534353435343534C2BCC29AC3A0C3A09AC39AA09AC39AA09AC39A %9A99C299BC751233522D5233342D582E1211343312113411121134111211 %34111211A099BC993411522D5233342D582E121134331211341112113411 %121134111211341112113411121134111211341112113411121134111211 %3411121134111211341112113411121134113499C2999A9AC39AA1A19AC3 %A0C3A0C3A0C3A0A19AC299C2BBA011342E58275252272E522E522E581234 %12341234123412341234121276C2BCC233125252275252272E522E522E58 %123411341234113412341134123411341234113412341134123411341234 %113412341134123411341234113412341134123411341234113434C299C2 %9AA1A0C3A1A0A09AA19AA09AA19AA076BC99BC99BC75122D520B52275227 %522D522E520B1211120B1211120B1211120B12117699BC99341152335227 %5827582D522E522D12111211341112113411121134111211341112113411 %121134111211341112113411121134111211341112113411121134111211 %341112115899BC99A09AA09AA1A1A0C3A0C3A0C3A0C3A0A19AC299C2BCA0 %1234121212343412FD04341234123412341234123412341234121276C2BC %C2FD043412FD063458123412341234123412341234123412341234123412 %341234123412341234123412341234123412341234123412341234123412 %3412341234123434C299C29AC3A0C3A1A0A09AC39AA09AC39AA19AC299BC %99C29912111211120B1211120B1211121112111211121112111211121112 %127699C29934113411121134111211341112113411121134111211341112 %113411121134111211341112113411121134111211341112113411121134 %1134113411121134111211341112115899BC99A09AA19AA1A1A0C3A0C3A0 %C3A0C3A0A199C29AC299A012120B121212111212120B121212111212120B %1212120B121212111276C299C23412111212121112121211341234113412 %341134121211121234113412341134123411341212111212121134123411 %341234113412121134121211341234113412341134121234C2BCC29AC3A0 %C3A076C39AA09AA19AA09AC39A9A99BC99BC75122E522EFD0452532E522E %5252532E5252522E53525228522E530C7C99BC995852522E5252522E522E %52525352532E5352532E5252522E5352532E5352532E53525352522EFD04 %52532E5352532E5352532E5352522E5252532E5352532E5352532E532E58 %99BC99A09AA19AA1A1A0C3A0C3A0C3A0C3A0C39AC29AC299C21253527752 %77527753535277527753535253527753775277525353597CC299C2347752 %775277525252535277537753775377537752535253537753775377537753 %775377525352775253537753775377537753775353527752535377537753 %7753775377535334C2BCC29AC3A0C3A09AC39AA09AC39AA09AC3A0A099C2 %99BC99342E534C534C534C534C534C534C53525352534C534C534C534C53 %2EA099BC99582E534C534C534C534C534C5353534C5353534C534C534C53 %53534C5353534C5353534C534C534C534C534C5353534C5353534C534C53 %4C534C534C5353534C5353534C532E7C99C29AA09AC39AA1A19AC3A0C3A0 %C3A0C3A0C3A0A099C29ABC573553775277537753775377537753534C7753 %534D7753535377535899C29ABC7535537752775353537753775377537753 %7753534C7753534C53537753775377537753775377535353775377537753 %77537753774D7753775377537753775377537753774D357CBC99A0A0C3A0 %C3A1A0A09AA19AA09AA19AA09AA1769A99BC99A00B534C5228534C522752 %27524C53285228534C534C534C534C3475BC99BC99BC2D594C5228534C53 %4C534C534C534C534C5352524C534C524C534C534C534C534C534C522752 %4C534C534C534C534C534C534C534C534C534C534C534C534C534C534C2F %51BC999A76C39AA09AA1A1A0C3A0C3A0C3A0C3A0C3A0A19AC2BCC299A012 %534D7753775377537753775377537753775377537753357BC299C29AC299 %C23459537753775377537753775377537753535277525252775377537753 %775377537753775377537753775377537753775377537753775377537753 %7753775377533534C2BBC29AC3A0C3A0C3A1A0A09AC39AA09AC39AA09AC3 %9AA09AC299BC99A011342E534D534C534D534C534D534C534D534C532E34 %75C299BC99C299BC99A033122E5353534C5353534C5353534C534C534C53 %4C534C5353534C5353534C534D534C5353534C5353534C5353534C534D53 %4C5353534C5353534C5353532E12579A99C29AA19AC39AA09AA1A1A0C3A0 %C3A0C3A0C3A0C3A0C3A0A19AC2BBC299C27B582E5952582E5952592E5952 %582E5952582E7C9FBC99C29AC299C29AC299C27C58345834582E5834582E %5834582E5958582E5958582E5958582E5958582E5958582E5958582E5958 %582E5958582E5958582E5958582E5958582E58345875C2BBA09AC3A0C3A0 %C3A0C3A076C39AA09AA19AA09AA19AA09AA19AA176A099BC99BC999A99A0 %99A099A099A099A099A099A0999A99BC99BC99BC99BC99BC99BC99BC999A %99A099A099C299A099C299A099A099A099A099A099A099A099A099A099A0 %99A099A099A099A099A099A099A099C299A099A099A099A099A099C299BC %99A09AA19AA19AA09AA19AA1A1A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0 %A19AC29AC299C29AC299C29AC299C29AC299C29AC299C29AC299C29AC299 %C29AC299C29AC299C29AC299C29AC299C29AC299C29AC299C29AC299C29A %C299C29AC299C29AC299C29AC299C29AC299C29AC299C29AC299C29AC299 %C29AC299C29AA09AA1A0C3A0C3A0C3A0C3A0C3A09AC39AA09AC39AA09AC3 %9AA09AC39AA09AC3A0A19AA19AA09AA09AA09AA09AA09AA09AA09AA09AA0 %9AA09AA09AA09AA09AA09AA09AA09AA09AA09AA09AA09AA09AA09AA09AA0 %9AA09AA09AA09AA09AA09AA09AA09AA09AA09AA09AA09AA09AA09AA09AA0 %9AA09AA09AA09AA09AA09AA09AA19AA19AC39AA09AC39AA09AC39AA1A19A %C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0 %C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0 %C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0 %C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0 %C3A0C3A0C3A1A0A09AA19AA09AA19AA09AA19AA09AA19AA09AA19AA09AC3 %9AA09AC39AA09AC39AA09AC39AA09AC39AA09AA19AA09AA19AA09AA19AA0 %9AA19AA09AA19AA09AA19AA09AA19AA09AA19AA09AA19AA09AA19AA09AA1 %9AA09AA19AA09AA19AA09AA19AA09AA19AA09AA19AA09AA19AA09AA19AA0 %9AA19AA09AA19AA09AA19AA09AA1A1A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0 %C3A0C3A0C3A0C3A0A1A0C3A0A1A0C3A0A1A0C3A0A19AC3A0C3A0C3A0C3A0 %C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0 %C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0 %C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A1A0A09AC39AA09AC3 %9AA09AC39AA09AC39AA09AC3A0774C53284D2853284D2853284D28532829 %285376A1A0C39AA09AC39AA09AC39AA09AC39AA09AC39AA09AC39AA09AC3 %9AA09AC39AA09AC39AA09AC39AA09AC39AA09AC39AA09AC39AA09AC39AA0 %9AC39AA09AC39AA09AC39AA09AC39AA09AC39AA09AC39AA09AC39AA09AA1 %A1A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C37653292929532929295329 %29295329292953FD04294DA1A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3 %A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3 %A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3 %A0C3A0C3A0C3A076C39AA09AA19AA09AA19AA09AA19AA09AC37653535328 %2928532829284D53292829282928292829282928779AA19AA09AA19AA09A %A19AA09AA19AA09AA19AA09AA19AA09AA19AA09AA19AA09AA19AA09AA19A %A09AA19AA09AA19AA09AA19AA09AA19AA09AA19AA09AA19AA09AA19AA09A %A19AA09AA19AA09AA19AA09AA19AA1A1A0C3A0C3A0C3A0C3A0C3A0C3A0C3 %A0C3A0537E7E7E54537EA8FD0453FF537E777E537EFD0453292929A1A0C3 %A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3 %A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3 %A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A09AC39AA09AC39AA09A %C39AA09AC39AA0A077297E287E53A87E7E29A8537EA87E7DA25377FD047E %282929294CC39AA09AC39AA09AC39AA09AC39AA09AC39AA09AC39AA09AC3 %9AA09AC39AA09AC39AA09AC39AA09AC39AA09AC39AA09AC39AA09AC39AA0 %9AC39AA09AC39AA09AC39AA09AC39AA09AC39AA09AC39AA09AC39AA1A19A %C3A0C3A0C3A0C3A0C3A0C3A0C3A0C34C297E7E537EA87E537EA87E53537E %7E53787EA94DA95329295329539AC3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0 %C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0 %C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0 %C3A0C3A0C3A1A0A09AA19AA09AA19AA09AA19AA09AC39A29284D29292853 %282928532829284D282928532853532928292829289B9AA19AA09AA19AA0 %9AA19AA09AA19AA09AA19AA09AA19AA09AA19AA09AA19AA09AA19AA09AA1 %9AA09AA19AA09AA19AA09AA19AA09AA19AA09AA19AA09AA19AA09AA19AA0 %9AA19AA09AA19AA09AA19AA09AA1A1A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0 %A12953294D29532953295329532953295329532929295329532953292F76 %C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0 %C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0 %C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A1A0A09AC39AA09AC3 %9AA09AC39AA09AC376292829292928292929282929292829292928292929 %282929292829299B9AC39AA09AC39AA09AC39AA09AC39AA09AC39AA09AC3 %9AA09AC39AA09AC39AA09AC39AA09AC39AA09AC39AA09AC39AA09AC39AA0 %9AC39AA09AC39AA09AC39AA09AC39AA09AC39AA09AC39AA09AC39AA09AA1 %A1A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0A129292953294D2953294D295329 %4D2953294D2953294D2953294D295376C3A0C3A0C3A0C3A0C3A0C3A0C3A0 %C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0 %C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0 %C3A0C3A0C3A0C3A076C39AA09AA19AA09AA19AA09AA19AA0762928292829 %28292829282928292829282928292829282928292829289BA0A09AA19AA0 %9AA19AA09AA19AA09AA19AA09AA19AA09AA19AA09AA19AA09AA19AA09AA1 %9AA09AA19AA09AA19AA09AA19AA09AA19AA09AA19AA09AA19AA09AA19AA0 %9AA19AA09AA19AA09AA19AA09AA19AA1A1A0C3A0C3A0C3A0C3A0C3A0C3A0 %C3A0A1295329532953295329532953295329532953295329532953295329 %5377C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0 %C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0 %C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A09AC39AA09AC3 %9AA09AC39AA09AC39AC2764D292928292929282929292829292928292929 %2829292928292929289BA0A09AC39AA09AC39AA09AC39AA09AC39AA09AC3 %9AA09AC39AA09AC39AA09AC39AA09AC39AA09AC39AA09AC39AA09AC39AA0 %9AC39AA09AC39AA09AC39AA09AC39AA09AC39AA09AC39AA09AC39AA09AC3 %9AA1A19AC3A0C3A0C3A0C3A0C3A0C3A0C3A0A1294D294D294D294D294D29 %4D294D294D294D294D294D294D294D292976C3A0C3A0C3A0C3A0C3A0C3A0 %C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0 %C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0 %C3A0C3A0C3A0C3A0C3A1A0A09AA19AA09AA19AA09AA19AA09AC3762F062F %0C2E062F2E2F062F2E2F282F2E2F282F2E2F282F2E2F282F0C779AA19AA0 %9AA19AA09AA19AA09AA19AA09AA19AA09AA19AA09AA19AA09AA19AA09AA1 %9AA09AA19AA09AA19AA09AA19AA09AA19AA09AA19AA09AA19AA09AA19AA0 %9AA19AA09AA19AA09AA19AA09AA19AA09AA1A1A0C3A0C3A0C3A0C3A0C3A0 %C3A0C3A0A111345258343411341134111211341234113412341134123411 %3412127CC3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0 %C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0 %C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A1A0A09AC3 %9AA09AC39AA09AC39AA09AC376122D522D2E2D582D522D582E582E121112 %0B1211120B1211120B12117C9AC39AA09AC39AA09AC39AA09AC39AA09AC3 %9AA09AC39AA09AC39AA09AC39AA09AC39AA09AC39AA09AC39AA09AC39AA0 %9AC39AA09AC39AA09AC39AA09AC39AA09AC39AA09AC39AA09AC39AA09AC3 %9AA09AA1A1A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0A1123427582E522E2752 %5227522E5811341234123412341234123411127CC3A0C3A0C3A0C3A0C3A0 %C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0 %C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0 %C3A0C3A0C3A0C3A0C3A0C3A1A0C29AA09AA19AA09AA19AA09AA19AC27612 %0B340B582E522D582E582D5811120B1211120B1211120B1211120B7D9AA0 %9AA19AA09AA19AA09AA19AA09AA19AA09AA19AA09AA19AA09AA19AA09AA1 %9AA09AA19AA09AA19AA09AA19AA09AA19AA09AA19AA09AA19AA09AA19AA0 %9AA19AA09AA19AA09AA19AA09AA19AA09AA19AA1CAA0C3A0C3A0C3A0C3A0 %C3A0C3A0C3A0A11212113412121134121211341212123412341234123412 %34123412347CC3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0 %C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0 %C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A1A7A1 %9AA09AC39AA09AC39AA09AC39AC376121112111211121112111211121112 %1112111211121112111211120B7D9AA09AC39AA09AC39AA09AC39AA09AC3 %9AA09AC39AA09AC39AA09AC39AA09AC39AA09AC39AA09AC39AA09AC39AA0 %9AC39AA09AC39AA09AC39AA09AC39AA09AC39AA09AC39AA09AC39AA09AC3 %9AA09AC39AA8FFA0C3A0C3A0C3A0C3A0C3A0C3A0C3A0A112592E592E592E %592E5952582E592E592E592E592E592E582E592E3476C3A0C3A0C3A0C3A0 %C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0 %C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0 %C3A0C3A0C3A0C3A0C3A0C39AA1A8FFA19AA19AA09AA19AA09AA19AA09AC3 %76124C524C5228524C52275228524C53275228524C534C52275227532E7C %9AA19AA09AA19AA09AA19AA09AA19AA09AA19AA09AA19AA09AA19AA09AA1 %9AA09AA19AA09AA19AA09AA19AA09AA19AA09AA19AA09AA19AA09AA19AA0 %9AA19AA09AA19AA09AA19AA09AA19AA09AA19AA0A1FFFFA8C3A0C3A0C3A0 %C3A0C3A0C3A0C3A0C3345953775377537753775377537753535277537753 %77537753775359A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0 %C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0 %C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0CFFF %FFFFA1C39AA09AC39AA09AC39AA09AC39A580C774C534C534D534C534C53 %4C5352524C534C534C534D534C532EA09AC39AA09AC39AA09AC39AA09AC3 %9AA09AC39AA09AC39AA09AC39AA09AC39AA09AC39AA09AC39AA09AC39AA0 %9AC39AA09AC39AA09AC39AA09AC39AA09AC39AA09AC39AA09AC39AA09AC3 %9AA09AC39AA1A8FFFFFFCA9AC3A0C3A0C3A0C3A0C3A0C3A0C37C12527752 %534D775252525352774D7752534D775377537753772E7CA0C3A0C3A0C3A0 %C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0 %C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0 %C3A0C3A0C3A0C3A0C3A0C39AA1A8FD05FFA19AA19AA09AA19AA09AA19AA0 %9AC358124C5352534C534C524C524C53285252534C534C534C532E589AC3 %9AA09AA19AA09AA19AA09AA19AA09AA19AA09AA19AA09AA19AA09AA19AA0 %9AA19AA09AA19AA09AA19AA09AA19AA09AA19AA09AA19AA09AA19AA09AA1 %9AA09AA19AA09AA19AA09AA19AA09AA19AA07DFD07FFA1A0C3A0C3A0C3A0 %C3A0C3A0C3A0C35812525353774D534D774D774D774D534D7753774D772E %58A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0 %C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0 %C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A1FD08FFA8A19AA19A %C39AA09AC39AA09AC39AC37C582E2E2E352E2E2E352E2E2E352E2E2E352E %342E7CA0C39AC39AA09AC39AA09AC39AA09AC39AA09AC39AA09AC39AA09A %C39AA09AC39AA09AC39AA09AC39AA09AC39AA09AC39AA09AC39AA09AC39A %A09AC39AA09AC39AA09AC39AA09AC39AA09AC39AA09AC3A0FD0AFFA8A1A0 %C3A0C3A0C3A0C3A0C3A0C3A0C3A0A17C7C7C7D7C7C7C7D7C7C7C7D7C7C7C %7DA0A19AC3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0 %C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0 %C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0FD0CFFA8A19A %A09AA19AA09AA19AA09AA19AA09AC39AC39AC39AC39AC39AC39AC39AC39A %C39AA09AA19AA09AA19AA09AA19AA09AA19AA09AA19AA09AA19AA09AA19A %A09AA19AA09AA19AA09AA19AA09AA19AA09AA19AA09AA19AA09AA19AA09A %A19AA09AA19AA09AA19AA09AA19AA09AA19AA09AA0A0A9FD0EFFA8A0C3A0 %C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0 %C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0 %C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0C3A0 %C3A0C3A0C3A0C3A0C3A0C3A0C3A0C39AA1A1FD11FFA8A1A19AA09AC39AA0 %9AC39AA09AC39AA09AC39AA09AC39AA09AC39AA09AC39AA09AC39AA09AC3 %9AA09AC39AA09AC39AA09AC39AA09AC39AA09AC39AA09AC39AA09AC39AA0 %9AC39AA09AC39AA09AC39AA09AC39AA09AC39AA09AC39AA09AC39AA09AC3 %9AA09AC39AA09AC39AA1A8FD14FFA8A7A0A19AC3A0C39AC3A0C39AC3A0C3 %9AC3A0C39AC3A0C39AC3A0C39AC3A0C39AC3A0C39AC3A0C39AC3A0C39AC3 %A0C39AC3A0C39AC3A0C39AC3A0C39AC3A0C39AC3A0C39AC3A0C39AC3A0C3 %9AC3A0C39AC3A0C39AC3A0C39AC3A0C39AC3A0C39AC3A0C39AC3A0A1A1CA %FD17FFA8FFA8A7A0A1A0A176A1A0A176A1A0A176A1A0A176A1A0A176A1A0 %A176A1A0A176A1A0A176A1A0A176A1A0A176A1A0A176A1A0A176A1A0A176 %A1A0A176A1A0A176A1A0A176A1A0A176A1A0A176A1A0A176A1A0A176A1A0 %A176A1A0A176A1A0A176A1A0A1A1A8A8FD0CFFFF %%EndData endstream endobj 380 0 obj <>stream -c6 3:}Io2ApEG+ rj|;jdAX/FbE'X[- }u>6$hoz3>cgY쩋C}9e\<͊ 6DxU{>&&h$MaFX,q{tim+5BAN Ƞx_Rm7`s&]{腤Ze+Ee_'x}m= w\,V16eEY3SCJ-FAc ]m tBͺh/c$\54$򋇈1Št1곟t2Q! ¯~6I`Be.i Q!uk %#O_1mؑFFY+_R1)u#K>h*Ƚ2+M6ݨ5Kcb#n. -Geq?Or/%˚7ӊG)-VM> d͡y-fecw+P3 9Wc'!%ݺ -nȚ6Ă#:uوY4*V\d#Lw<ΖW\5RMC?ZJb"Oj<ٮh@?e]?PNJO) YY7PN-tWWtV<{ 礍O}t-⯰҈?`Fa߽d VTTݶnc nH;g%^ԡ2/  t2J#'fVcZćUFn&5]xE KZզ4x/$VqlDžY14}C"׎iEՇ n:E|EP^5l -wTsmjAhuGڪ/JU2>NߧlӚ6?c%< cr<ĪMA#4hĂ%&~a  -6vȨ :KJD/쭎s]I>?+ŬVZ&`[z{D(pQ kv\]Ȅʙ"Ě&`~`si.zAiprJ 3Yf5ͨ>&aRLGn؉eL Ϫ>=7 nH|`1$?7 w|d6RӲѸ#官H%[[>u1զ1)1a|cv<悆:whI8Uz'dWyĐ.~⨰l!\X?2rL/dZQ;KI\T] -.7=0;b͂3%Q}D E;,OhjZ $OtVq^]ɬ! OOQxq_{CC9+xpByz]ה8EemQn"tEAHŻV\@#35ωUL-}3hʟNk";E,쌫uo^QF1پP $Hn"G9ח(8mI ZbcRlњS08 u _cA\1"eکu-"C=o&eY#JH!UUi$", *Rh.u{GcsIW _9-jH16yg46l~wN|`h6wiHKʭPU:}@`L,D|<"voM 1+zH `:sKjg'9 ͕o|$>c8~&nO}?u8`RvU.} ^dGPskwWb[FK?tvc\xn{)>IM.-t61LSgEIב9g1gUk鰱 d_ؔ.^#mC<^hz7Ց}NODM -^v%7s{YI݋ 3sjUµh YO 蜐km>(ѷՏO٩/]ό%a 6PfppCM6hO|.G&\0PSX(ϖI6lIHHVpy9{!$Eؿe5jDoX7r$!t5bҝcJdPt_-Q秣 aTWtwYL"*-5q˪Nv╥V=%5CKw~"hՄ*聼h|&nZp{N&[w͂HٴJ>1&g9 64}^ϖ_?3Nb|?lwݓp풪)ѯ,Bƪ]T&{:Mv__#cg o'L/㦍 -r.[.ٗWm'ieATOtuio}p}40Dz c^ҊT{BO%.eGweQE-]ncE&/*k÷ahfn~.ʐ 5+\Dq>!H)[qN}D[21GE>J1dL#HCh#_8wS]i1*/?1Q;JJmfjxG6$-eA36gSlh6fyQϵYu뀼⨧1æT5f9mBPɕf3/+mɻ(ƞi;B$>U>>H-nʍ?ڪ?]’[ɗLקi9u>JIu J#фgD[e;+取=pW(,-'K|vBO9ON -s<ײ_7|}JO~C_}{q5y '3soS4ҮQjtU>"ՠ3/gTcj=)+.=nxvJPxok_ ?J}yRe =/BmX#wJ6C*^_ ߦ:_g:lgUdV8X`Ém솧=$=[RCSQ9U+Lƚ43OVn ЈF"A%wMkrx!}:S~}R_4e`獴 59g*.9:['4"P)q fB }!Tm9;6L8xxZp/36b{..5h`f'v˅/O;~JsЄmx@] -պݴq.byovt:Qgm?:4hKV\(׏ F$j"i=jFOȞP˖=Ql2+Z-5e=pVK wGLO\POJbgM̼>^="gxho, 5A.+fzYq4Yl6FLV=謼=+oz!\Ut+njAJh~\?j|>5'çNHq)6J͖8F0ߦ3;ND4 E׃GkfLŌV9;󢷥"Б{[ɚg7GH-?xd>>О~Պ9Gi|2b\}đ2Po8CZbP|«~Îz-렱FL撞XdV!D:e{p`H%rQ~>Kj_Is:f^{sʹv䧩3ҢG{-73gbOQ -<($UpE6Tƴ1bKsW*qڀM_* zBҠSG -Ry0Ƣ|ቾp_f9)+Vb/Y^Y9%;&navAWZ[¯~3@21:x߃.wZy>5hi>KvIWF- (&}{< P&I?fÁ%LSއ *kF& Fͤ]@XȣܫG=W֌۸db̆zFyoG_0sAm[>>~rçY! .7'n1[. -~9*DY%bVFLYԐsi2r x-bNi(ԻC;u(aMhU3*lҼq&>uk6@zHMGg}=?rqP!N٬^7\ ]~}K!#dՄtBVBP,Ъ[(  Z_z zF[ZƆ:q~-G KϨG5p,ZmiN4 -,V 1ĄtA l9nNQ<ğbR.o NТQhMBd%+.dIǬ$ezё)"jQG.S1FiZh{_@/h y&Fc6GZ 6.|."M:)nyWoPF+;sZn,fmZȵvf󨊚#Ɩ\֦&pKʥVrŢMHXvrFDU 1A+lԊ ZiUk6LvHZ+^Z! ϸ͊8id6l5 aCk^uQ@vF Zw c–M? QqC폰n&lDșCF.1 \ Rx/fA,}E7'z { 캝T{5.ͺ8ZxCꮟ{m6U=9H-2ݮ:Ӣ8 0[nX)iۅ*kݤ )yP<{lɖPVœuigRԳ1v.f'${ˎ=USkMugTќ쩊^&W4ĺYk*h. :Dͯbd⦳U``Vͪizz٬R6gdCmyGhZϭQ&amU4K4Ȯ8h D[Ggf4|ɫ ^0~6mB䨼l&oܰb#U/f:xַ>1-:6ބsOH'8u4sՃgU}mUyJ(f|rW6]oSbMЈʘ c9"n+9s4!kVfZéy:o&帄մlngz$[^e M_sP*9$hwPi[uKT!W2` 4>VZPigJE[aZK+Ҋ:&TԲEDfX_y `!lbVVوM'7dzI# -|nB"4Bg <́y)~ۆ i6|;:O<.[55 WӀZq^9`RvaN 3͑KXx?蕫1l;)@|^*g)UA#|tu8,;s1{-[tHzf,B]H -ٹ?APE&mFmU+pa)~ yw7mGZk׫^!{}KQW貼I91e^K. Z;t!oKWt~@mVG(7i,;8ex`*|x~g S+&oЀJ&4 je |A^6`= OYp #H'Zz6|\jhZA pOX=9 bX;:8@ -flxI>kΛ-LVeI1)Vm1wݼ;:-3*7FI -b{_=s&,}] wnevW)[SBIpogGx1,YwI;m;Πg'){wӵ5ɠ-FtA!M~zPy%' 6eࣷD.Tq& zY3xD|[_ol&[#qvwB HSZBμҸ=ޮ;wٴ3zl2$oOHU6"`LvDŽ;ĦWg6!- r\S#Rn)H4LݘF saJѯU% C\[=v=R42"HO*C<,gVcM"uzA-@|p抎fldy{C^>ܩuw͙:LTpVkps+ベI|e)% FN:h:dE2"SU7cd{C6'cz$9%$M{>.%1ٳĢM-<ȴ >NYUK>rF~֬QQ7jMV`,uz:L4Yn I =w-Y(.9 >j!X)CVWg{dB"A>5I.U+QTt^jvR3%:}?%mX̕.qp.}7(몴 TenEB] -C8d]8FIALʰ9ڣݝnO&]_݇ tW9ʯs)g}˘wwO{[Ic+)`ny™a{gYƖM šaqǖDz>*vQ](7HGrkދ1ҧDUaahH]p9d[:ڲG VԚ=f'1~-i"sM]wCPMiK, +W弿e5Whsm^P.:1mH4oZ.Dmˤ_W/W/}JBzXYok~"U\gp2 -wGQ%[>\帑Y=ޗwQ#YQVٝwAgMrɘQeoPp?ȉmEͨG_(XŒlJZ7F;SmƠQ5uQkxXl֭bk%]J0Vr+1\8eTZ]9ѕgf5`, ъǤi%R auRۚF}.=+1IPGˢ - 2ӓ/gA1(CgO(O;-9I53:LAseT!hm(W@1E' zYe,ٹݏ {pɒ9j3~Cgw&2c, fZ$jzz_TҮʒt*Dm< w$!ԏ͏K)*rp!˲KUYJ~kP54ؽ6ޥZdwu^8?t6'+n医Ks(AK^8ԣ`e-<ВigOZ(mB,_t -m:}\fwb5yФȯBzcO%A~+,ڝV.Dᠹ.̤E6tX6`i2w#E=F7nD6iJ}fYIǥsyŒn KL ZotՏWdR/hӶ:9_3{f*QHeEMJva믅%U~/=v9a1^~ xRao#O };,ryCe3D `9^L -&8T&AE6D``y_ވyɼo9et5%ʍwcWՅžGH`gEuRޠ7G g2@(ۭ/jNa~)*SIϗ5aoc"Ǯޯ  / -j2^by-mT))i)8sdxB3zrxj|1!'EǮ\{x?;e4%a'O {7XuʧTO~KK-k -;'[ֽ\ڄNMZtH=' dk<`*7GȮWHYuuwUg22$wW®ZEy' aI}x:Ǜi ?ȮUI>Am\sEa&ჼxj؃7+wR'DsEP{˶Ft*IP:|DPt]LoGhX\xZ԰5au^aس7c"^m782؜3ov=|W6PLg&A5~ϗxC ;<ΣƱgIa?m -LH~m<, UP6l nSK@o`hҹPҁ洟x2kO)cooW0׍co?`ut#fc"i35PșƐҝ&rxzF#rfM|^Dɔ.^K{^vyUiFAӰwCG "bƀh{D9]Cv>F竔r'SOޣŧ 'ezc/>ER®=({?\e9UN Duwˆ=Qjuk3$jJ!3%xՁ:Ͼ|>q^ٱ[7{Q#ċ扨l鉬Z(y9-Pؐj̨ r!xngRċPFenTIGs$vq a°{3y>&ϖan{t90$뭕 q$s,K3+aCWrxFNF$ ?^yQxv1w<rjdy>>HU3\R0Z]XV ߌt2ٯ1/bckw8SU;ݘ2#m 9i6e=Sf: -4k J~S:w?L"HH-dY ^ W\cwhׇNp^"ꂵ97Զzۥ#H,u60EOIdz=4Cͱ ^`Nd]}v Q/aS }2ˑcT[mcjTKyղKT*=Z`Oנ{7i+x0Ꝓ/!a7T];;,=XZDA 4ndNϨXQkط&yDg39Bbj>=S?)\iBkoTt_y = ;?P~!~i5ChOpGyac؞j1weʒʨl]"{Ja)_ % 2+Z3fn(04EDZZBZ |ΙQ挄>2(T S~C%[4g*9}73Ec':/;_,>*@ >jf{Gn2%fMֽ Y#^Aj m]*U %F`Z -RvΜO ~5GBc'fA-XZpNˤ5UZlT_(m 9Kz >o%r~ϿU}ahm;ů1ʗ~@GdRQ`$]YTʼM7S\eig֫(k2.:vN~؛hɨ3EaPE"/$gN* fR邎Qh}:ARB{?uTwJ,]ZTS"'h,0AƿH_%`ZWaB8 }Nwu#Rl~ިmմbUWg=']:*tށjD_ԣt fZ[@Ӌm]YqN؛e ng -9mtQKN-vZ F9ޞ!ZsSץk,Niݳ1BiZC]ƽk3]׆yY#N2 -NW( $! ,H $!̬7;^ycmԻޣ5|PʆCՃ#c^Qa"Sn$c\ͯ츑XX0k~-(f{ PYF8Ռ_wU'۠ѮJ5>5!.kWFN 4}eVv-u85h83{GȮZҚT3T2Owc®T7^=.hռZY6;wQW>0[$̜u!+mcbyOM[+<|soskhv-~c,N2g:YP[]Gݓ7D8ӌfHwi+I?YWˇJ? чY(=n[S!xVG@P{vd֦hiؕ+mO;'ylBSEO^b|t2?Gj}@y?'pB0%{EjmkGeogJPR1+]JX_[9ylLMӑvU;y njgA^r ,Hj9;K]+M(/c+aE t]z6:n -'F:Ĺ -4#o]թu׻Z6M Ϯmi|!uk8-9($oK1 -v\; tX'?R87UeDDME's]ـݜG:-ǛcN-Ҧj';݆g{[QW4*Fj ->4 槻nX84)0EQ\ReeYW[ BXBkҸM!LPts=%=-gYjѧK2t':i%cAR8-AWei["JC ̐J)\\0)j]N*(E5zgB\ -j ״̧![HHZgJ6' K~Bq"m'-;1ƼInmo2jWP7;C2b_Tswfl9 ̙`3(75m9F\(P|wöupl=&cwe< -jh>M xc#<9ЕhvLy<:^ -9\7z)7䴭TWE;|:&dm!-(&שĦ;slfL!x/{". ylR|foՓv}ucS/wZx!L4@Ʌm_ >\Xp_t#Fm󗵺v:Z϶T+M{S7k; lU:Za`Ow\K|:y|@+/0^.+, Ak~{SfU0 k޷2\+59qE,7t]][y7+%X"*)'zZ^``~)43[.OV5ig> -nx /eo`"JZrߟ+[2VץJmy(tEKNLaaCOȣdtEhY>G;hzl5LGԈ+>Y‚rLKE\9_8eHcQGI. gK4C?CƱq6teuhٵ\snUu"0n5`ckM-w]{,lk8h3KjĈi\rE+*NKꖰB y2 ۛg,u!_~IF9Yx]e}}KoLr>&ƾYF˞fQG@$"4XWՑ ;rP)h`ݑbH[^Sъ=sT^k˹U|krGlOX$!n9Zwo)/66e5UW<[(ObbFiꓮ~\(2Kv3t{jGP!!:f&z6!xB[µ1| -p艖}y ב>/F6ƫg3wv.mAX[ʮQS>/СmpGZ4T>9[nlve"|u br1':.Pŀ V*2|V_+DUP+$DCX+{ppZ.POh6)8b{#fVn>Y c^=6fwvCC ,ƫ=^9_cN)vj|Cۂs"UKK< )>ŧT*i V:Аk߷!:AJC9D$ȕ'[coə>euށu]h'jL1{~bCi@W<6&4A~7?fj:x@-X$ըgk9;Z't#Iq#{BT$&zU.dd}W}]pS,>&#$4='ty]]lKIdϾ? ~}[9?F^,֊j\hެɒSH - ݯ بcSlϺ7 M? -BZ>-!6!$LJ"%F|'~3^ _=\_2_jcUuҥ[Oשߝ"&W~XSu. A{CmWi/6}8ct%rWK𩉅F -TG** Z)s3ӏtBΓ3L*rKAqH1,:QkMN:DåS^lMyg#')U3zҝRB!*{ꕁcvGAAu?撳@˝a+8<)EE폁BlbFW0n"#\RVCLAr.˪OaD&=e̲dzrܞyEUwiITz@-?34pjSJHx =T*"gyqOoξ{E4gt0y uMXLאzu'"Wvo"Teu/S!pg4>zI jf19ǽkyUr*=5:60v):z=%7bbiB)0YZ\ξZWUE͛GH{ULkZ!A&Y÷{SN^}#(]cr\-Vul,l<%[ȸ&b%]79YWp8*=Iݓ` i?Y.94X4zSv͢gꆌ++r_{:_~~S1 -t Cy /EFMKϽmzf~%"-]JBXD}T\hmeM]UOS5e<>^fA-k8 7~WS~/w nZE??CGqqԤQBz?왲.RJkݽIJ⑶1KF%[G o|X6#쓰w,^=>geBE]64VMR]e9qKÈAb7 @͖o; -~'|mdV/E[[#舥{_k[ -oNГ<-(|4=P| $;i. &g8H}@Eobh)|3P6^B^*k/M" 腩(6axإ_=bXąVzbVo ݓ,S1)B;Bn \,79fswp%>wO> =+].f!3r4Й.+ 7喊{ m%w=rU>Ѓe6?B"L=O6oZGӊBZnz/_=bǼAE:oa=l$dإqZw)5{-k}H4:=39jcE,7񺂟sYOW -!?V'=>P0*F&늘h|vڦo좲?6YDS 禞[=e,G.ϻ1ڿLT[v1L8]lm?]xIP{d "mM|.|m̯lbLKΡ]rti`'¾5t;( -^''po+}{(f1-c_vM=Wm o Qc!ECUqzt7򑶖j< _hϸr[ec{ᯫ{;Bl*j{|zk 4>Н{pK)6\^(#h8 ٚ$ƍrOr۝ +ãփqR?)q_5r3I~tJ#@c8DX{ Ö;ou^wбuӿsy?}tu)|^;꧵!.lS_mCS{}pݛ_Ӑ5i5L :TP9rw &owVdNt<q{g%^X?-C/42=4\.~t !Rt X~d'įZnͼllMQ]Rk/fM+ *HK-p>i}e}O郇<45{Puu{pf/:|oh.ю|}>C=s2. 1LE4*BD4â/I>qGHUvs<1UyX`"ĺY`.ſJSo w[r8ťܦc&ӂk{btM@d/*ZLM yu CF.V>oշy SSJs΍٪(bg4@==]v)GY,o4GZP*u"x0**$%:izڮ|toOr*Ǭ޶ Bϖ[< S7enJuoSNU|ܮ-oλf%M3?IhII?#MMiqKg9Ws2v \?Z쬺c''X['EiSۡϷ/lv|X$lh4w[6~|;:@EfCGw b#+n)Мr[%SOucs/'N,D#kQamKsҐ/ݞy|Fwol.B^Z+V<" TS]},F\?<3[t<_`OZ+)yy.˻Q!}v !.)1=G$vi.nTՅNQVk0l$)I% waFsl#ow'f겯I?~,z2CNw.}Dåڳ0nBװƞ9rI ']_S'Nju]|rz|mBb%s?6RM퉎uI -IC}U7~-E/}>wL,ҟΟ[lu -ڣn.[1s 3Ы/k3c5x_Pm7:ԌC-v x a爩Jzc]dReQy{cս]k/+Ql<}DCx䤴)[>yjs*%9䔒Ih82+omrWTuק1?qHV -02Y<^h{4 QK *~}"X˹2'k!jrNņi+vA/3az򏲺P ?V{$C{$»VKys㊄Mp+-r},{RTyzCJjhDE/_Vno#_7]_Âw )>p|0 . *#JT_KCL8VcҎj - cS5*9?H44.zc$#Qz&tV}̪ߘ@$sZȂ?jlshV)4Nٔte3m͑aF -txO 6\toiiБ3|._Et<ܥ‷GBc -~$&8Տٷ7mV<3Kj2{_i9RcwC+&SNLO8D6Гir{V33t;oR2N 4hBsQTdpЋ^t"1*< W`oh":ryt"Jr"|cl"L(m)&Y}3BZuj?;Vq6tݟ$'P(;&#KЗk`2p]aĝKZ Or/-vAӾnpg3̰Bn}o>1srZϼJ>ߔIDK8Yj6AOd&) -2n)+Cq Z - U!wJhyD<-g߶OҜtsRS3%kgP4?XI@(?6>=4AS,OAxT\ǫ!/?`co٤ҝ1L|]=YxfVgș[`1ߚ+Zs~3b~]Ⱦwn `R~JSKcC_x>PDјyc40>Fݱ—;bVG]s˙%[P1-5GIe@KsLOOC.m"b7F1ڏ=Yu:;^ћuo"55%c9gW3\E`쵶)+#WGZƣa3 rgu?(B$-6l19='cGp15H͆٥䴕KKnDԜQbWSO3^otBBA^#C2zѾg=?Y`f11 <: }摊}b12<j呖QVN[yB&]"xeܫ×Xe(2/͹S?@wrxȺ3/ 1Mr*6GYqt-2Vuuc #ǖ:dWO&6@M^Ph}ý6$^ hx{ ytӭcD7[Sب]a-ٿ ޵N2ZyU/fdfڞĽu`U !/S6)>#An._t6;k{HrN{x֜ƌc 5xx]!=Sn*#{eJQZA/.2~?kx~賍1rg B5 -~>`[xw<%'c XE-tH1Uό]yh#fTlC<^G9f)Y^EuαU柁B: -T_66:(b­L# z,t4F}c}|)h~4W]xpe ^FFmv<*|n)WЧtG:ZPڠtQQFC -z6įƦm=?.3@ӯ`#Y_M2s;K2k;o,(uSdWTW(Zࢎ֡E91Y\u";H}o!0ܫ Z.PC/?]bWx}))emR>Zj -\qUgdlhi?VhV}2~ױPRN^]Zi+o raӏē]VCձ~/pn%!.}Y2MHx#bLʯju F)3 2Υ!lԽ?ZTAu=Zd.6@5y͑gg=1rwqYrSO4^Uקt݉^@QW>BOM,0 Y,\N2C)˧bVZe]1#6׀w8UV <|s잼6ƔۣqRuȱNr>h)Dj_C-tMX0udF*2/~֧^ʸudkj kHUse͓2&N 2f -z✭Nwݘ9e/@rnܴMC? ^@_!2x}`;eeYeW巭S9^5UGG-HYRn`"~0ǡLo)@q~%`GEtf=_yT\Kg.^TݐKkOWG_5uuEKd_x$rPzٯm9Sc}ݞ{ًUꋉMYqf894+7FŻ̳%.X{‰vV:gWSv% |߭lh|BG+-bX$,ɿ@]l4`Wt;O;.5I|W(Ia%2D䉖^zb`A3xgwbDZ{@جwh`BT2+PC/ZLe-pu}%&0OP<"\hUk -`Dj訸m< :VXx6ܟ*{t1f!6NYh jZك+*_VEzLź@s[skzlđXp&m^8DaAŝ/3৻ӘU>ͯTz0iw~?R Yzu -SRK|JLkh`]= 7C/9ZkQpQRj_eSJ;5XƦu-wc7hb[ Jr\OK.+?g ۀ&9cRlPIc@أoj8P(+Ui {LC0g<]=,XϾRm]Sb4\ķ̧׿dj=2r)G +gsϛQ3JLWqL=gUrˈg:ꁔ9}zqM{yz&hC=V51m32]Gѝqbߵ 1?&nM{l+,mDK|(1 i2wEumQ,i%y r자k)ܣ}q0eGGKxU.e !%$ /)ƪ# >>]U=dg{|[|dUNW0>+N蕁"J"^uG[QKO쉑ZVvAHxokC>82lCMK mYVoڄ^5)dfi[˞o#ӎ582MNא)uRr 3ᚅF.we\&qYBO[,ߎ13uT>~2S}[ZՌr8oʾT{$u?yUlTK/|&RӕXۚuiygK|ي h,;.9dg-uܱOV+YG:lZcY-Af by%'/}0KH9^dn6futPf/HyB !NŤ$A^Lt]%. ;L :2ߛLʻk|*@fXƴWzʞy^yfGE8,[]Kӵ-_Ɛ!c]`.219uJ6<z`Cj&#C. /< -\_9YfA9ZOq3V`dLL* 'g'Qo%T!4Nv1K|Z.%%i8X#{>CR¯"giMK8;+#\e:r12V{Yiyzr Ճ(,*C-bs~ntßpGz|s~eSyq}l)Vr)V87[|c>] -}i d&>!^UA=ѪNWjFQ+= ,Z}s*Lumlj<M23$@\ԅmPE83JOug'+\zS.e x8Wsccչ:fgS][BnCV{ott5_FP/` 6.se VWWx  -AUK[[>mOaY.h8s/4ҧB2VU1~<жTP7+TIX%} ={ McҏFWvGK`FM <24"t_Xx7[ K徔˪LU>E#>Sޟ$:e<䖓'DOs?@J;]l{tivxCW=?ç2$E'Tt퉚0~LHyn56qcUcW}ߧ+ nU.%H*NجEHπjhkOe_ RVj<,<)X˨<]\ -b}h1u-{Sݱgs Fp7?7jxg\`m7QnQv :jg8OS1xM#C'@CnV=<3PyL"|n2"lZm'S]:l\s4GNMuGs+َzGŮ:6 -TCȗ3hٝ%殍T=«ڥD,*x8qfZi -]Im_%~65u k>N- . hG5\9HBGLCK feR h7> #攕9ԓ=^U77:T o,RJ2yDgPR5 < EŸgoOs;W[ ͍[bvZƾOl86~֓",kJ߃ɯeN镁z.˭hrN[=+ -lKh K?7 _C*pȐoH4@Bn4ꓞ?-6岝f_YEW@Dϑ c-pe}:XL_m 6 < -.GEn0 -7F͍gˍJOI+{hN)=3R!LE>hG3 <*ګ@'OUڛxE5ZJRWKJ*"K9XmhXrSN.ٝƙǫ^Ye/68t0 ޙa!܋>cR-d|_\"fv2NYV*jET{ |٬;萺5;BDf]SM)(E>9ߥ?"p c]lije|zhO-q0,kms<}gm xf^1yG [\'PwSAzJگ9:W0+@ tb0N5T >;bV޶:"&1ƉoF"hDPcgvE˝wWO*tT1O=+*B_M)=1Աn@en -_ǩ 7JTW2/ ʯ+|QWoi@ĺT {.h<Ǟ:@SK{),tGp:l?Qm@]qg:F7l8Z\O 9rstncT{>-=~@oGkU͂D$#5-1aG\}A>|;PawxX{}X GE,8ꪋ ڿ3ME=կig ELO+_+ _NTl"^O\ -اl)r gЀ,',U\JRGIdS05jjh>]fz P>U#!o$G*S -J6%\YRt.u-ɫnqrJց=|e޺&$\ |^[ٹ6!euR}u8㕋RJY75.mVb=eU(۷0Od?R{__b/֡ނfQ`0锋P\"۲;Tm. :+I!M~mFҌMê_|2Q'fGɬ[l?=sP:Roy"$2vڕaJ>~O_@K:S@+MXAykwuu3$mQvJάv(1Q94._fDiA ~s"!O6G;^c.Y[3&{曑-=%lYAi^:AʤzFçJhXWCӬaj:MM7a℠|yB οu@Brfx ApW5o(''X7wy)g&f螦 3*g8KH{aSqec]r2Fqx\|cpHLup:Dj!%.PSM'fNCd_r:c0Gl"6&)QtSUOSͦVF=m~eVޚצQ{qHSQޙTTym4j0v(~h{NؘV=c#]VyazIYT -6:ڸ.Q#8<pEHy ٲ1ڕ$t#J.glM5ۙp1^F L -;Ou)qe|6*!4+k<1ujn["ײJVI^+(^zB\hUiJ(hK+nZ˛ō1es|雹mwf9ce}eeJP ¾?|]뉚Uю<튏̫EHy5|>:hEm*1⩅'gՊQ㼲NAs:Nexriy{#˯HǣQ#bP/mfi10']h; 7'ϭQ/ =⨥W6N:TYh^Un Sνw;6){e}%jS#kQ]%^:}Ts6a__])rb"?\kw~^aN}:Rm[}՞ _ea -(d 3;zeTϮ@""6v<2i-.,!tY7!#!pW'E ]W"&N xgZ®Fϵ1+udS_ń =j/nq0`V$$dz~9Ĵ F˶+>ݜRA$Xwf|"Fn>`P|t+|0H8,Y1 %1 ڗGHIgSܲK|v||bή ki~E,",2jLïM-q=115jf~cPˆ=T%Rb4TH/j}K:iSyhYHAIKZ=W㖮Qc9&,[Ia !sݨ NC C ܘAIt۞KڎoE킮kV:t{cF 'i^:G߃}!o˦Q܉;x-JpWcƈ 3."]/sc],H6'$ͨYBƍS݂&]*ekiŤ["'Ffez0fPӳݒ/bg%iX?q}Ss!<%Z_{Z꛺_v\yŹFT44aVa硘AƇ n9r&cW"N^{zV*I:oŬ Гx[Եq- 0`$MʭE-Jnwg/nuOֆr:iٍ)'OļRHr Y]HCc#Vʕ)ڹļ1f](b)m/L;1Jg|Og{YqXmxsqd[9wtzyPicԀ_k80rq+Pq3(j]B\J[!N9$qJ~#}͝I^ )[*)Q`$<] ѱmh8Po/ -*V_~DmsZK`OӭwȹkӢ˫ɧ6 >Ua@)̊y_5)&֤Kʉ%! .8ɸ9N鉅KHK]]p*CzV%bw%|pWnaVFH%aP@+v>$Y{I٬|]5bГ_R.@DO-ޜl;>zdsp:j xyeKv|m(hy^v>3)~'2_xܼTY+SZ3[k=Ѵ̿T~1C+h\zC+4UiI˩ ՜SK;Yu hѶCYМ*8iZ6Ϻ~rޯy>-weB -b7gQF_{F8 zŬۜv(IsHhz90~tzIZ?ntmM}~`E7ϛ,RX] qs"9kc9W7'uqm7̛NZUGG~`hΧYzFUºIA@iXnieD34 -V)csJJ!IW\Ϯk=5I8Ғ/&nmTC>!!Aj:Ox&p洠r}\\2"X|J:ŸDž߯.ee~em|)n ]N{꫎ciNE -j ŷԳsO;/=k=jEI#< eeחx*W[? Lב3>)Qzr[b6 )n5dxN,$Yn>Xii+P6v -`($ F~HͱΓZ)b_wa g} }:'ǝs$rkb~7˹Y]zIP)尞W+I3e]{CpVd&cXo5|j~<<J/_}Shc}R\bܫX] xOODLe{A84ϿkLx:fe) rJôӡ)E&BNܫF:On:B "fP K.D͑)biV^V|1v(jd* 4k@ -w.<89,ɤSːÖQԾ6N9UY\K3nN[o;oY#qĮuq[eXӼ Ƕɔos_;v<%!OON:|,gTELF^ҠUQ.ƭjk_ڐOYNoML:aUPøuK;~11P+<7k]uOBw ;;Qe}q2m6fnspHѮ!="Qޛ =n!,wAǛ`VF-v]""p0Ә*2B1hEEnEy -x tpFДYW*oL.fXWQyr$mst!cCLX;ϭ5}?4)NqH*ݙmE(Ybw\cMq3Jxx}S‡KܴwI{KYUq}L j #njDW8+Í{ah \P!ɸ8_3n9?픲z套%_]h脊H&95s$ ՉIysh`oU_OA椓ׂ^u'fMҋUӐ -Y3qZaCZO+@.}V1p|m÷ؖ^,_iTԅQW F(p-mN*44S Rsq/#’AKNYEJ:IJq~HۑM-j~m=/eW!4bGR&Akt+߿!j|U ha=Iٹ9UnN}  yjcvhMD>i>aQ$y?d7۬LQq߹:y|yzY4 -v\t" -+>=85ȴGk͏nA~(U6^ ]zBݧ~A"^/QXv~qL3HV~U%,aTdp\+ hm <7'_dJIv®i-o\8c1_/bk$茶mJZ1IJI٥[30K_5}nM@k!k1 5[SVn+jϫι[!MO #Q;jPr4H[ZF1~KtF >]5rpIKMĝeOfVՓtK~Gv [9P4TWd Ĭ&QsI'nenAֵ{."Q3_V>Y{%7iM'އ C!{I<7")({ovI^_ xQu*U2b?S >KJ5m[a=J;~j85YE_.iU_$ADv[%b/{Kd^"-mnDv[%b/{Kd^"-mnDv[%b/{Kd^"-mnDv[%b/{Kd^"-mnDv[%b/{Kd^"-mnDv[%b/{Kd^"-mnDv[%b/($~!X!kI>S>_X@8l_S}}|Ɔ`<whk}pc?x8z?q?& ~hLR,xL?Ĵ.ū%>hro9y俧r}wR%n\e/<'%Dj!j]i;N%RZ|frGWƈyq|\b]j)7Y[0 ZW6ӐK0'6!_D]Hr.buҨY86츻S6)a"f; 6a`mۡ(|dS4YqVP7JPm$<:᫕[%uZ #,2puWX}rdE቎<ȡ2e3ZNELǯGrڮ'ꞰԨSt"VvC!AMYE] :cs5n&VGʬMBvBžQ.lӋRUQP F) eT_6$YHiLSLyL-$'BJ%芛zꥤV;^x=9]ۖp"&՘UDƮM9KHD/SLM̀V%_JX혋߹ͪof}*5VRz vJZC]wҞigOfW0 -$kiJ:Lsb:NjВ[IɥYUoJب娃ZRJ":jL{g}RAЧJˤµפӁq楤ҫR1`U' ⤉YO qIZA!&-φyI#ԈUCŽRђ zQ*bAm!=tq=f&&MĂUҳa[® -n%Lw%ҴA7񛇻/y{." ʹ筼ʯP3J&$`V9˙MN9a6'i|ʤ^vZR~[ƭfC)F &iPIW]V|(FAOh՜ƫyyw|ѮD ̫8P :#H;죢?,(,i _H(` PpQB6q[Wbҡ8_OݟC?1- -pV~Si89 i:4, -^¨B+ԂMLJ̔CJ9w)xa3*M%ļ -i' .)+uv$WNN̬>'QHSv~[Ƭ@, 74VGmmgC&y|jP]t22FMͪ(6' 'F)("Q&z閎t.` # y{,oOyB݃# =n,ҨM@S<"&!9VbNsʹY/%?1rP->\wv9U!N^TӏmL51Ү^5 ݈|.0IǧW/A5LrvNcD) -L/iak~9jUW'dUDO\\x+ogcFIWo=5x4#ݜ6̩Awp@)GO7jU%ieVg=v*qPfaGҪDҮऴ5Iisޅ_{絺cJJhJܴ1zC?zC;jhE#?x|<{|Zcf);ؿ9Naem'J3ba_(i}ԭ絇Ք3j"(ƠVڎ';Oox7PZI@-&c"MX(+eԷY%O }1t qԂ_ߞWe̬Tٞx^sY~1/qs"v#D R4U̮'MmRv!ea6N=j!^L;YS1U%S6 ;y s{Q}'3'Af1u0~9laU/R,1 2ۿMC3< 83*Qsӱiy{SIϵiUw򹴅Ysy4U5P "B^ʬ.G怖V1-Ta5 =Bp()/0ͯ)jaş1 j&Du8D-eI+vkxvy硄VܱMSҰz)bdlno Gń׷?%&DD ǧYu7M=kbqx4h` bn?5յtIXm:^9t$hG궗O}ki#Sq-pKݩ>eR&Nxۡg5->xqӋļڡE/ܶe)^*\Q. ,<1-QǖW@kgj- %)IGo :nfͻy 7XMZ҅qN;Ow Ryfji>6 -k@SN})$f<2^ƣξoa PQ+s+@P\}2)x:椅[ӓ/D¸~5`7!gAwUܡMbǽ=)Q c2x e꨾pW+5Wg3 ;ƴr5A7>*}³/1tnx̆OɄY(fbArle'/D)嬏Pڨ0wsޢ;ĴdD&H#]3KZap$bFKq"0"zFdt;RXG<9>|3OK_,/:Q x!(0C -#3#'߭R.%AIVeDC)Xi~Y1ٕ7Ўݒ0`% LSw6A`v+iCJ Q-@'i\Y1gB}bn~'bD@,zkU3VЊ.Fu%'c&OPbI=r, CL"8G΂B}3B^ h쬲U)%/\ -a -tx^ޯ(vi-f!SRYRڸ(h4x8qM|91jaUfRAگPJef^>@ЁGY:=/SuO0Oj(;=c0 *'rvЂNV11TW!f66έ.cvE؆ߋy-q>0 k$'q1򛫟=.dǩ_4|aM jX ls} {0O.]b^ޜb_b,ޚj!0S]Dt(8t(kHG-J%̪U|dV?A*w?_{zd'-Fsc׵W^| <bfQg%hoK{/> ~$ zѳ"ZfT`|1Rv^s54, -.b7t-¡aq֟c# f2R%?kS+;bbF-fR?`4,szЁ4Eܣm=3K\ﻛtv Pf赎$a,eiwfKj|r9QVnn?V)3kiO-򀽮Y1n tX_@C̫Tŝx#Y3K@L{@{$Li̜|I۞sehŘQR捘 %?jb^M쐱b |JޔO.<|\뽃KfN}- /;'c> 9#u*Y -lW9I7=jT(tLְǙE/K‰AwΞZ|wtx 8M욐y% *8I+k-):Rףg1"ϰcS2B`BڴJR6j`v=v tM7Q-j4ϧ#zzDzQԫj$m-rsZdE]koZo՗cM~ Lu%@mM.l2F*F4tDz,c& )ۜ<*hY 0yEU5U%L?c:^;aN%026O-m3IEEMRJ,%&LN1s -Y9hc ;`b-f3 a +Z*RlVd| -Y;XZEx8niˋ)ňÝ%ca{ynVӪs~v !;U,kO&~uI~:P;%jDlbx -sm;P05㶧 6 D_ ܚj Lq32aani@O:ρcwN1&>,ϤhS|6,O+T8!vO՗$\t$mNj֟/kwg{Hw;lL/.3=DtVҡ$^F,quXWJ{&9O -%sYYK -NQ.Ƨqh 2ffdmS-׭H)BXTIi(XmG̒U%IYm:I;fTP -,HIJ>twh*#)9|a##ռMFGL5Zq[j5iVNdˡ--$ D -=h#:r c͘\0z?MLfA@-< -pi"͢]pУrnO|| q=@OA`5B -d7=Z9uy\T =2䗴Z3WB(s~epZ:0joecf^KL*5ܭZxToߖ+9ww7"5Z‰u_̴,sH43j:Rv;h :, ѯ@-%ja\EiQ[L'!U}/ s|4(/ig#&i -tj[~<hS!E%[^0Kw;+2#em{ 6!)Qsp bQM 5_5bUܞ}Q=1J:QpZ^ / :üFj$\pJTs"6N#Ѷ}nU<܆Wi֭iKc a4;B;vK0uvuo%eBH.I<5!i1^K8c>E7SJe'N05KcWCJ>Mϩ1W.I9;n22[״KI#swƼYr -I$§R!ޞkI㔯hG~ WCc6x?8y(\:v!BAg/,*p>Sﵢ>uĨE-2ʦU1?Shz>4BnQL85(a-bkY,<ùwN<'j 1>UFXUw˄b\GAjxN#:nUhz-f9g]f6_yTy-hR Z!&vfYG*uugz9b"9S 3H9n%-= -,g&`Ey.e/`ת0k*i A7L"X?28anu&AEHƜ +:7Q -FFq)&ܦ t af3 x_q%Y%Ӹ3g gww`sucz0j-'w^$n! upA|NAV426!̍1k{VHDL,Z*cxǤtO9b?tHY{>aJфJFV;-;#4/8@B8#%@K8H@vG󝮤OGǟ*c9%ps8D gd2&s!f~8 b칊;7bݟAn՜͈O5Iρ w,AϔBٴ8,z 7u#9 -b̩ ɆI1׈] >0%`|R9+LabN1G -lO-k;R`v&pB:`qϸwȥ˩C)9d~!~s$@%L?.SH:6= -JnP>NsB詜GP2Jn՟lw -|<;S,̡x$)i)fy_=Yt22k?ue@b=rًi?\?s,?sL~"}k8bk?j{ +`O& UgS½1lgoy"\Ҡ*l8tQoPq4!MD91T Qcl(?t6Iq'3t, -r0DS6v-?,Xkg9bA_X+bV呒pic.Qba<90=5ϤgkC q+IȋʓS5'3|P—c;98|N0e:>L-gB>V2y>>X% bV Qrv>#Se_~u8Ğ2.~Tedߜ܎ Q돧s r!hS;lJ0>}XqHkoEIQ / \Ε_  R  Y'SYy@@mHZRRO鸐{l쁂?J?ب3k6Vė '[`N] 21P 9U0{2Rb=y#aK5/ϼ09k[Am˨Й>%a@OYg↋qbF_׵D `ӑB@$m N5US@(+GSTqWuZjԨ-UBࣉؠc3?Pr0|.b/ORL|>͠6hJlb qڟ^}o<3),qRǚVFiQF}*>RgLDMI -.b>pI{2\Ёy4m+Xm>,:ys)l/>c7o\uf`HS% v#.4l8q9/%#M2R{(oB:t6t6qHã\t2OL,87>Nɹ?_S>B{̿Цgg6|0hlv{>S:PNJt&`2tJ9K)qP Tp3H3裔4WℏǓ<҈lA!d&xXݒ&YL5'OGOؘ%~1åc6H~$|_ˆ=S|SO& b~|VB揓i9q ІUVឲ|hӠ*Yd<̉9MNm|~O_PWrjFANދxhi>CCMFR.0^fDbz)в2!]?ƁdA!x$' 8$t^Im{)5<. y'3BxI6x -UޑR5{ \woМj֙3X.bspѼ̽Lց%#uxdQjD:״8s%fz FpY3t$[amp'rfs|hk %ht81}S%߄|6Sx);(9aIM{2Le o'Pc%> !EGSU~--hs#%ls NfH5fDIq{ -r|ĉ"{.64#=s>ƙPeqM{pohv H2E6LĞ9ԟEŌҳ#Y?2~-e ć<(%qα:t_)d)O hétΩ:# ef^[fP%dEI^w*B/\+ϯ3rv'CA?#AJrZ?[(G+юT9W>*;Ws)z걊thZl&xwg 'MȨ jaJ7B1k?#>; Nىw&x@rN@߆яx3_|qh_sNSz*l{:p= 5 cmҢ{hA+;JDo uVRq`sJ9)L1QeOֺ {"s*C߹渠Vhc` ؇q4Aa/3a0P/g*Fڑw8AFXIʢUWS%1x -q g+Ӆ {xUu3UMГgN>|Rͩ~*AL 1Ra~$'YiC̸Q 1&Y ?L7 Pv~V?}kM_8%8= 89|s% -{\H0Lp Ju:bvF)}s&w;۱K˾총AJ-''2ZfOʗ&{}?=dBD_~V~8ND8FqM΀=x|`q:I~z"e$kgwOZGOC"~سIfʱ<,q]|N(09Sws"OgmX'}h])s uc] >wc4}1q{a1|ruϜC>RPOY# /n,]gpv u$z4fҚpx=Ou8Ah)m8AZV7TwV Pe޸dI2 M̀a!~K 8p 醐n7{Igʬ\|NZ#B5FIp_kA-#7H^͌NNJK])\;( mOOu҂_k:owR|:^$UU,b`=XgAZ-֦TW >p_ -`RuDߥfrzXH8rƳ}~j(gϝnF+jS2]° P ?LZ1~5f|j07hN 6YU$ښk](33pP{ÔI&DKXSokJ*!XJAJѺaR{fjZ;9`mn}{HhǵL |}:? ޺!Jv/cxq "tH݊wYK\KP ъvك:I^*p6b۹Z-Оpxv>E6VE*h^h]/m(a*AV84q{6m4$^.L#"aBD$4ũUEqi'%tI=] J$.hhNWyySme›v/xD%ɸH]?3Jcn|\=tE|fw0.K ;h>DA?0ܶk_^A‚Gw|lG}YnDXK^s_/3|8/ U/ո/s(l%zNB? Eh'/5}۟p_vG]qjR3y 8[j!ꥺrcnXdAfr ͨ W?ي-o˭$NJ֜luz_5u顷WҜc.-4&>nu\z۠u>RZ~HՏ6Ei TL%擦fPjAE\'94¸>r \O$qs%Io)wMN~E Zo9`NW$|NU}Lp;n3eCX|]LUdQn5eaF3}&|)Tq܋qaqa=uQ/=ݛȆ>=8nx{t3S c[}\|`3IS}5" C}$L^\K1]lNq9Nёl֑r+xvཥJ{RfKIYnTY.եYW' 5Q<]mF}QW;=УI]-))jwڒh>Wd8d=~,j}2% R#V-j -MZ@[^mfm V .Wd*'eqʛ~v߳ep;]7#JÉfG,&cLg>6ݯ"^0v $j8jM1f뵩h{ėZ?+ZbE?()CrMTya&s5з}0BB-K[v!j)j!BY -{5&*Nx֎q[G )lV7-p -a.hɂSPakFVwnUL* -{(3mnZb}GEqaaAIVܣJ(:n'a8_ 8:xLjOSYMtYv0"q/{(޿wnHs2[yX,~7'nv4F8w1!ywL@ Ֆ>j$T<[IP% bm.q>uHt˭qoZbhKCLFh7W;nuEwƳcoڈT'b]flźx>,EXoVUmO'M$z׆D1Ok!ϪR^iq]iZb5׎w"Kt>^o+p˹2li[yNcif ρVBv8WV ?kvB%/7 + @j wڊqXjJ^lHVľ 2{)9+ȎKM$׹jf.'<?\k^jzQ| -F3MzG2#GXHsҽLwHPS^^p|naFlv8D]O^טl=SpRlK(_O 4U4_ڪDi=xKGILR!,&rOJR6ֺYQF5|ge,+MݎwW|Zr_H7rFUݐ04/b4l6[M:Yƚ( -^Lc>f$Z^, Ast[Tfٍp4#F -CT^%[vdSkh>C)I!&d[,7:s@Xfr}X,ⱶ8tR[M,vӉc< wenPjHᮮFXE>͗>[J|ȍ}`PD7ez BR unj2/4;d2'0-n I],8g#ͧ4%i5?-gqO.mwi*>}o=A_Xvy E:. Es5_l#q}wUw.7_jaqءc=G=lG&:mT~9|";,;iO+Ui.D5 -}I[l@mX n9\[] >"8[2[\n|Yl@Mp4 A[ xmDXk#Ԏ$qB:/Zʍ}*My?]bTe L |I {ޔf( -_tQo=Y-D:lC۽Ԩm-TON}mh"1 ݎ=|M yum!/ GJR܈vP%R@_V}Rmi{naR#QUB[go{W opF<\." M+`@=/4w.8敺2v[gS?զr~EW3C_WS^,Q2uBoqoC_q5hTO7^ރ#=sP:;AVŽԔEz#˝DOB2ʲ@}rkjO hf>ˎp"\f'w2/TYo/Oy}2:`7mu&lw}6:P/`]oV=MY0omABfo|C{ij/)SXeuV;nXlH6Z/Xh(q{Qu8?'tQOe,z& fGQ.fZm v:rP_i#ߔ<&g@ RvɎהٱQzّ/fJ+2,/,+XF]yv~FDU^mo^lŻj둶 aBی6*?hf,+,7J 3te=W_k!{]V뭌d=IHj|*xgnwIWVQ3wxֶf7ݟp0]WUP}';NM&1,7JM mMtނdFRMe~Z3g܂uo-@A<ިLך(+oGđGQ]t CVޖN࿖!V"l zԌ3NG>r,R#L]q.TbmHVNJ33OtWYfm9po`,'l r*v(v|hQa<+]SpΊ9~<$HtlC=|#ҸOz #xП~Kf-6gJ@9QAu~a_tPD}҈$f{qf^NGL6ǘ1!p0jp9ݟ֑Tmړ>e9tF-x#8߹J`" aۚCvzYq #inЌ_. Rm֚vc,G!~z~[ηpZ R'cga@?UW!ʪ0%$;0ӥaZ q4ͲGrwWVfj܈rcF6eowȵTiȓVӭj·?j N7g=(#P[ {$A$^t-\S8#9C !u9VVxvNnn*aSAxԀXoX #| +e f6ߑd0wXKxm hJA?!`7__jB|zNz d)fA 3xC<|'{K_=Qڲ=C.!>ɿ>ˋ~:Uqx2~87y0d_d.#őTe [~m͞,j+FUDuO51@-Gs^]{,m{HLbV:j Ik8__"oc} e1}PB?,s26[y\nZ|58xa짩#z3ֻI᳕O2R+ksO [%mIՂ([QݝVC//mZgpNöԊB,Ez@0ty"ISf1敲2칼$RcțhkaJ2AJOYme.62|uTǣ\bJ8a0[NTCYkmPc@:\V;,mp7˭$R?!/IVʻ -z=p&L27dߕ,0r(QSIc2yx$uyKuUG]a4YbX S`g% P`T%sENqh=f9zM?gS"9}"M=7L ϽNf^^ĻREu/?D@OX}\oI-LJ<"jKqXo}V -mwsy/Ѐs !!kjO -X$mOpP[S+ⲕtr'#bԌ`߁܎soB:,4,ya0L[bTOK<;bYk݄䗩 -H~EE(~HBHs NWa%OW;x:}{ =iXb3"+"+ً0͘K패.VaI>,3ShN3ouSWX!Kf.XYE\o4 c%5c grX]}OE6(aG!,jOc|=Lr7OM: X(YUbJ;9xg }7&JP ~2& ]ty 2AUJN<6\ Vc̴` + 0ylBGF:\/ms<)#r6Dj&=z:Yp`4d+]G>u##my}kh]zg(,5f[H΍JȢXM\ץۍ9ȥl^"8hZ,L\j~lgF\V?:Ul1$G4q%lׅlt8lX% V1/ЩbfKf\=a>f{7f - - iiG !CB m*JzXZkDnw <8Sk-Zs ˵ܐg@"TAhmǎ^팦S b_Xh&WY:!!fKZ -^ȡ{f_U0N0_/ۗegnpS7 ;c|A'-4'UW@gsBI92ɘl\K+l͖Q\Wj;yj~xNJ3dvG;,G]|Zݑlc,-Btkzh+i#”#wEy F^=2VL7-wcQ.k,Ꭲ[ΫҫHQ+yEܫ~Tza*xWXhJkǿI J7zTo׆)e覩 12L [Km ^B^ rȓ~|yv5>AFߩR9V 9+̸*ڧ|1E v7 }w$ 07jo^ ~/ ccB f8!]<ꥭ,4 բ,U-Kvq98JO<߾84y :~T?] YDq{20ZeEy^h^[HL{F#}h$~OawnA<^? Nz))º`NI:9 2Ȅ*Q6 :c9nT񨏏~1 1֥isݯ}Y'B|5%lKӹnB&.l4ϴ`vQg ^n m맗YA&8'm6ngvA=/u#;h#TDaMUZ g$=#؎BnB?_;_\y9\ }k8-4҃FĴ,B0q2UmxK˱6:7 asLr;#>+Loy. fnAY)Nv=~6%Fbl- JsG2>@ܑS6nL3֟J ~΋+'>ۭG뛙IBVTzfKvm9~7 pw% a[wC^>~wӿAl.!#nKb[R6\Bt5-tbBMqJ5n\%/A>Z}B< |}rU;_M!+}!Rf)+>J/ ~v)j _!/A>?1ܽ7r珫`7!OL!`ž*_M@m{.~BЙò.#b L㐷aW^ - IZZ@B^xܿu oA^ (B|kU R7|b'/ I \ʇb|Ƞ+hoKh7G9cȝk!yu&)xO"=M&J/J'Uɡ3 󺆁\Ip[Xރ{b~՝ېo@]/}|o_%ba2+[7.K,?P}Fp1S v;r-}{w!NϟC<߼qy~6Cɍ+< -‰+)$mɄ-ungdfڹwtmJ'%&O /_B}*9@n^>{?l l۾M8eՙn^]+ ߶ Qo8xoA?^z~ WLB ; -9/E5}#X{bluf"*12 -/7Gܺb}!ޖW1qYT'U%nkݔt-'u 1%:26R#ǐOrK0o܇}me-n%!I 齺(|')`w!}`) :[[A- B^34 ݻ~mȷo!(%wBm3;5lD998_訫!MA@Yއ|3{ys ѭg`0xcg`^ V8PMYYlψv Gx<<"497/f V! !M@ue?OWAT$>V|{즬 :ߚ5\,$RH 5c_WB]r^܂xjwMk5IۓySbLHْsB"ʧ]s~xML}"~TV]7Y+=Bր/r -t㹙#"IKAjSL*YrtuW[<>@s6Ul,Ҕ -_:\gu}gpS3^+tl{r4\ؖǴ)b㟓RL|B9\v -j9{ y -! nZHдVy~V4C*ԫs9%=T0^R/a)&;[vؿx?k[`&M q33l,e,Yf1)mO{}Ȼu>!ל֜}2o9SWTF>ygv ;)nQ;)f4,S5{IRz'NNGy VYQ9&14˃E,hHEt M60pj7@O r])Yxg:ht-}+76jk OܞcZ7-&ŏЃex!9%h ˽y|]e?S?_~ki7&#LŴ~k=rv.g~sP$fbrNv A=##Vžն)loL27rEC7ҙVCڏ\!qi9Lyܯ"62yj_&?Z Fa}ϸO܀6bMz^1A%`_8t*W -䪄XTҡa-g@iQڑg4)r(0PM_Aﮡpo熔]ZcTh Mv :-6.N@:dluJ>ͻ/v2f0og kYv3QQX{~Ht -(h -HuN!ɶĬ ))!ᕳQUgKȺyv1ywNsl\bfZ5^FhkΧ%ÁX-u>1,p_ -;5>%rA}3WMG'ݴyBy[#ۤ8s'52:^ڧf.бzDGjBÉ (( !F yl̼9DžT ),X.^w!j{T蚈}8fE͝c.9\]`n6>\%X'{Զ{x{0Ts}{CH(0CiN5" -pr(ʡYaQKW`ˣ -&4,c8-5?;g-AStM|{~geX6,)ž$n b: w@k/%=bp.kItmZGF./ci[iK U9U{Ͽbl8|Z&6l::,'6Rg[=۟ozpfұـlxʯ*~1бhcJMO5ͷK,::+$ -VQZB -be qHshp* 5v1&.Dg% -@ N͎шo: P=BlcsE|_W΀l.YUm Ҩ{4(2&2Zv}m`[#;YnDGNrzd`=bZtk!qs4B@:J.I3ya!U;+(کA`iČ\nX9-ůpVK5zԝv)%zh-գN͈Wb*b]lE(x]n_V ˭৆n!- :Fl -Rmt&Z쐞غRu7d4D Q1 3@l1#|k WrPs`FDC-$*8"BV+'<\jR"55qAnSOCf3r__E&ҠQݑuZP,I`z*f6.\Rdk'ƕx$}!"osQ[cfv^СtN/9lW- ~)2۶4.lH=f#yְc(dth[YoM𭃞ٗ:y>)bUQT݃'9зO^lO5ݕJ56q, T~068O;xཁwhh3`~;X+qeA%`7Y0w$Xfn}XAf(Yh bRFхg:.+$g<"zu.!'#UME . )*/ޟvxO[{ 풘92Y#Ā?A;btgΫ` lZ(,Ȧy#E{v{eآSUOG` -D\}m2:XOG{1-)~)ĕY!jKwJE>9D~%ŶZȹ&b5Ym&!Jb endstream endobj 381 0 obj <>stream -~~ >H&D Q 8VΓ |uBLK qQn9DFf{wN9Ts'F&HiPE?]=[J6p{ݐQ#ȱzgjs -K9\uKpUQ#RCΠ%1[M^OXg9:վɌiK ?zӹz jHM)f36)얒j!!bڡw; -B=?Iyz+|ՍK# ȴ9^5NٳKcY;Lœh -↗Υkmq۬ qQyN1:ׯBDTUa[D/rm -2!7$$yPTo xp6/ɹ'&Ub+k&hw ;I2k栂uI;ϝ&c^_X!S$ 4٧#ڎ Dȍț¶Wk$-!FQI^)vv4p@o'v^ؙ;#_CsO!oy^7@)oc٠h'_\44ɮ5R{zQR0~`e@Bwr簹_퍵v6"Ӑ~@\W3Nox1RQ-ࡔr-cG0o?Y<ж<\FD ة^Ur)'v]XƦ~u0*E.ů7߶ W3Ԇs(=UZFm>D[smyߋYMdI XnK0/R*1lbp#F.ޟOw@EΩ ih-l`N,晙Is p_|fc7r {BrXvhHˤ>6APU֘YjK`˓AxRJhMa!8>mbѿF(ޝ~ʻ[p;W{:Ws{s}f(M6 # OhoR ^j.f`YE|GCGz~F~׮'5BbR'^!#a -:$ yS7k͉- !vт*b_ +a1543(oKݛy;,$c`) -Tޱ[2tI@-rPedEdzch|k+7Y7c9!5^!^ĽOxBkkTSLA65^@ƌLB@A1eٷA,s J(ЈύG"&~Kw -ySg*zeD᩶# `R@En񊩠#˄LDLEňBY⑑ 5sgf=j 6Um.QKmsk 2*:'!V+,lېIj?Lά=kGPN:@ GaWp5R摡c |wsWlO_zS+dN0u"%W8DM#x*Dx+ h`l?5AA8coqfH`Ft5|b1ά$ K7[cbܑ2s?3v?XiZYWu!^g@ۅrQG)h ~ V=8.g&h|o˨xr1wL24JfWrzz~9a?֐KV՝,x~!(fabr -$ӱz5Yq(g%ygF!(%(A7螰[{+3qA-xHͰ`mMOLԱK %DmJ 聨6'ؔ,9ջ6[t# 5ޤadDŖehoZs֠ ]d'SLWc{KL=U1P݀Whc#=y_T|_ջ/֨{A %Ld#J[';\먂Q508ƶ`%_u#uwk")h=zbmȑ<ϳpO !vTdy5d<,4'#֘&ڞFyz@P!:a -|OOߙe};:9NO=0Ps1+Mu܁<"ʉk/`RF[%h,BӘhx JcZ*v,6zo:U[ 9s ,a垡겴 --a]+ 8[pJ@k*ǛZzjnd$Tl %739#skM*8cξx믻j`sK>k`_7|n?Ro;V`1G!!# '+*g{1TKF̲ -nan}&bWl1X_6oHݜ̻iJd-Ė.swٴs( -˔:Mɷ]E YE\NPwE)V)JHz,t])?q Pk1|OLC7!1,㽁>SQ-z06UA1HGh*pQgy_^I9^!3L/ R,*6~w0}(ai~q6Ұ'@^Yc9-od̔C": -4a]J.ǯ:>^s1%( 8N\q onq)B5c3S!.).-aG:;/*gB*6:xKū^"9i ;ALѶ;OȌ# ~xo%7Շ ˈ^}9p(O6p 87Kz6kkh= -8Y]rY:T\3 Uݰ?44<lzVL)a틤LXC!4RHe`O^]'ٺƫ! پKڛ860h\#L_LkK[&Z~FHbwޱP?Ԓ_&<ۤB*Tl㓈U3}倾7'CPYL}AoLvh%`p%PU[aXk_k ѝųKnpT*f")?c- sa"zk>Us50&8Qom*&/(FoWhXjo\n[VULO- `yMsr۲*H"Q[z[ƫM: -Y -8IQ|AVíf~<K9ʂḳ_}OL#NH{:)É37̶sK Keai[jS}o9D{SJ-6½ 1E>!SŨ\-b3{'[Wp; B VZ|N=nT[X {%\gxcZ:4\Wty}ot\ݷ-4>q,:|xfa:-ƕdбY!e~|9X{:Ihɹz}_t q^ ޹H%I}OKH㓰rcfߛĐ_闡sJB_ۖ1ڮ^!`աiWp_7PdFH+* &”Ր\-&k"$'O[ҏu8]u6:)u\z ކG{c57/k-<ˠRFAQkyL݃d;C1-}sz2rgFaː: -׀s]b[v2SR@rNn᱙>t\QP+?/:oڤ* sx3˨S8Xr-"BZb8uD6p -r>2b`{ZO (Fn|x`m}Q]țPYAMv'IKypJ| _u6G,2b}O"<_ˡ$Zŝo\%|`Jc6oF,(X#5صpwڱyή`t(@ս7gԿ{o})yG`ٷ~? h Q ؿڒh*|sqyXݖ^pEX,8j8(TZ-&#RTYa5 [mO[MxE> `n³Ύ鉭ˊw_nLc8{Lcb\[־s>c$jZ28%6гxT*<:^w{fEtU;p' ~K_u p3"4<`spVм?o>X`/=;ז`S=bZCHg }!<uXC{,L@T@~x)FLփuhsj飧ڑ_7}UwưYuO]ˤrh#CWOƗ[wLwMSO9\d9dRLtB."x~ |wlU}_iI[sax;C ,BD"4 ojzl9ރsHnc=qQ+^q|v aG֨Z\/,>b@@΢% d.I=2'uUR_euQ5+eN5r̴ -v1+[Fe,]d}$4TlCXcHOz ->gA{3Jnl\/6&nŶ12#;[=KǦZ$Xz.fk)">(ٯ<^I5>ޝEfUhhEHTc\%n,^(IJzQ@ȅ' i~YpRP:5OxPBD\vTnj#һwfpc& -ܧAJ_A\t/*s< \_ÿT}W*@?C$tfa){K /"D8hIO-9w:O"5zG4ձtI5)D2:+f䰽RLy)3ȋύEw|rhQ~gc@5T(0y!g!VI%1E.ܝPP<41^TIIl Kp^d׾8RKhKfև{= -|MK{ e8ig\;`<#Բ !- Ӱ'J&O qbAvf@/fqY҃v~P&q5F"UR\l}\t]WKw\m-`-tۓId:/w&ZN t왙3˱!g3Sĉ:5rG2s*M1eJ 04iU,$ )⹂Y hgA16i|b\^I|%ksHup; \em#bS@z7&y79x:!#z;a⎍<ËD(:¦̠߈kW~0̽7k5Xk[|s[(!qgD<`|zC2϶?=֎ɘE0Q'[ ?~d鞋!W -b-0WY?Na)hw_Pj3ʟL?NrSWU -@Yx*l_+sp%~QW7z -BS+Hm'[L6j_nMZ:-I n pgc}3ޒ|К\mM=̐s|薓jr\б.9j۠Ը nj{Cϔ x핐Kԃɶ#s,E {GH=Yҳ4~ L}2Q_Ʒ}վ̑ 4uhs􉙄>2y%e{\}{+}oc¤ly@s -7+>fl7>+BVSPoX{n -:W<8:XvewZqY`NB&٥7YO?TQr-O>ͣ m+\j*bӱ?2wNE6!-}bbˆ$yX2יu?wԽpUz1mOWtX+~ޛ'Z۞ےtwr:'AdxEwε$ygO2~޷gf -ɥ$<XG/gŴ8̤l{2\%Vwv{\i8j[wmwݷ^Ttk8"-GʽAnNrpH;_y$\OImQ6Cqi->-1u 6G^>2D]CcG!0I}pPt^ŷLtN!^iy_zKnnO$N㳾Yދϵ*41O9P;FO%A1nq7'nzJ 0VPovx W4=#-Bη$jz+lMÒ\bRÑiͳ𬀎ˏZ~-1.N ;=NMB (Nsjher"|{o[G_}՜}P"-e{qgxOEXiM$;7^׼+Ā>\#Kp7k,^xڿ\0,t4dL"_}\\g"սᆱyuS~՛/L ?6q,?|3Iw/[w&1 -.:oX`/=S -XMȄdž5hh@m)0~%h{!븴hBSM@ r(ɶ9f"2ᦗ_ qo/ii3ϣ2R2TU UuuL| _bvhdd"3'(?A^_:7@,yM=p,|Rl1wlM4'ڦp[/uSNkLq! VM۳pAup iĽEb_ͦĔZYGqXid_$_ )s pq@AޤQf29B58<+a`!1j!%Uۤ4! }!voX5BM2om;C_ݠlt4 v&A)ZƦ;f&nmV]ӿrK~cַk^\FfC_{oXGnOߵWb^ՌSFfx)wz:L,*m{7+W;WvOD+.Ns}gsg5Zu3iOp_$=ސhy՚V[ƇTB2;WGcbJb&2?#xo䲏4dudݽeT6.u{G%/%B=^UyU{P͡) 9 rK(p)ARmŠŒ5-tB@OluH`hoYp1/fD|HՎetmrxUg[0)ϑs~yZ6Kۙg Qɍ :6QJLyF)Ctn~)[Gxٖ:|Q|yu{gR)c{ tX;cwJpM9ϼV_d[Np~GSv*zRSO} U_ lL)}<-ɢD \}Udpα^ұRRQ=c iG ^5VN/*1`U&C]gEbkTǎEDlːk}6Hk H *TKAX=(A1w6T_?¹ _A R..RˮcCMoP2K~/`?m4=\.}x郝ᆤ8a`KДQЃ_]Տ+u|"JT[ػ#qlLЩ9>,-jK93QM{s$/ Qb+\-CץS7L45W -pT<*2R&ӈT걙F86&$f5M&qׅohg;Lu: - l s{-OBEXA rW*7<23}5%%zT_å﯑*ï2R GT4p^S RY_vܟƧoB.3iME~݃@ɟJ_pc\2:ثb:b7^WJi u_5Gە8]mLbSj3>-o{_A[[?!}EB ,A29/֘u s_˭Bíi%kgR;[|$l*37>\q)3*!bMv&ұI>%Trw}օ~LCTDmerKA-+?Eݙ0|ךz) Jx%w; ԬsScumivn߽!*Ly᡼+l탠6ORYc1%*,79͡_] s؂|̶q XQ{ctƤO枇ghn:J aA>(wnjzn0:B\PzU|d4Gk%,DL18t0A]xuatM|υ̢Q3c:l[+rp.wA (؈M8ݤ~f T3U  !\*=/?Kjk!1"%bf9t:#Fy$G[TRL7T - ̷ܵ=D^5 ~#t !o#j܂xiXn~7S7FUeW~Bn)YYueᶧ&}/B?l6?U%U<ԛF%h@`/? ;vwIXFG~h* gsNGm*CrXq.8s|ޓ-$ֱeӭb^ҿB]FCg 2/$6й~1/&BRbdsʉMD_*0eY0VѡQ=xB/rrCTGh?I89 |;zLkk^}ʞq鑀SbpkuP- x*:)"U4;\e24YӞwi -3UW.dՂ>E^Cd}2Jxah-v7 U=H)7>9KPbHGhŖ֕! oٛ fD=#>;nPF9Tշwᩮ%Tm ->ݔ|8Y7Z8`!q6_]G㯺""~"GB̛l y/>o{s^W7܋w1x>2G:=~_=# -JcpX]zo:n0 }:WL2RsKx I lm wH ]ײs6ONoa5_GfzG=2 M>հQX%UlRXɧۗɅ{4ܓn{Aw&/UJA"f5jѭ͡PWmMS3oν^%G ݠ'{# 3mUONL4ulB9GzX.kTe L`^?Rc'-K̾o2dJ'd2ME{TTzט6rNmIO(o}dj眎_fHJ1s͔ #]Wtꉞ;x/ܚ''%M/OO -nDo%V;3ه[ղWTM4m݆._e'2P BxpLS}Ca~y-,OV.:2hjdbQvf p+nq]vwWZ'w;%';ɔ u8嬸 @dU*b0X:tRRy>e^[0c+@$]{~lk 7gD%|Rp1S)/ikʮl:SLr.2@-fxq+:D>23;0J_ypjLR~;O5[Y?J~c"[3o89KW{>jLo d{]1S~bu2m'M5v[t8M~0>Oqxȧ0MLqt㍇U|{ |?D9Yeſ02|cVޞm«{~AS;/ũo*Z_{@jz[MԈ_dv ⪇'W4Ȓm e4/`)#:TFdob[)SgUu5ĩW=YlN_Suf/K;_x"'*`[x'[dZPp%ȪGg=qFe$c]Lܵ_3u$mZ;6=co]V e5! %RC7Rz%Wb}@ y3Вqzyx:ȿ6ߔ.~~x%= e[QE ,;c4䒙P=%$!]e T7"2ӟփ0VmuGgMr07ϿG>e~aLt/<^Hv&(9&lDUw݂g;^:tpA֬DXaWy m {#=C58I i =*lގ9=%"p~fOxa 6Rx01Rs [Ё^-k/V4b(m|x^i%z +5SOgAIsU m]X >O>x5yYqશ}sGYLqEOxԣߕӊ;)=QChQ`}]Gs⦶ɜ 򚮸8Uu{o\mY?7bk nNJZ-+/Mpʌ#}ӎ+]1#y vBs4jiOdenpifbŴ=^!л-?g^zz6"nMs!aQ^%w'UYNE_`3~V 4>քH*菸 X F - _F= lIQ|JZ}ɯ*}[_Kv\Y<\zÌ-=VN%~O{~b_{J/K.ytv{Aݫ?!+Ɯe]ۏ#8nZbj;+%8l ޼lH𦥗.iZM-7lML 4¿"Cw.hlTP7*zn0w/8q2訁X&oA"k; QndіW{SdT.qQ%2%%Gmb6,D5cf=1ya ^T!3~ I+jͶܴ[[~:b{kNtӲ^8+oyp ;4䇘CB^E.bYyMN);3#mmhI ,c|-aÉ^4!Ȓu8'ilEu+hăYd}Y*?j$/: |ъ_y˸C,)$kI8NٴP|D>I=)V2B}/隃VuvW~.2hN%uߞTNKEĴy:=,:.(0?OPҜi!쎱)#4ؚ[3F콠``gM^jVͤ1!jxV^Hu+2BZ)W$-O('A);YuC} ?﮻.^Nyx8:/}g#VL:xR˓O.:Wqvӻ?>GQ;Aa%64 E izx{5(yuÁDغ _kG,ܱ~>Y5eSYS!a^ -yUK{Aۨ?ڃ,jPqBHmvX[ Ve RT Yciu[1C b sy^u@zhw-*mKXRUbԮX>v^CkEdzLf.iYn6nN*kK?obv-j@)׍3;c%s{҆бdfwi14dE*Zn|4ڕyeYzՂȉp%1&)]gз$HlݛQ2PҊn17og-&cbkfӍNt8)̤i dV!&$ iDžX5sb45KsѕN 6/)Es Gy`_ֻ9:$TNҖ깣}ۃKkfh9nx6?g7c17=tȎ;u~[4-Y:3]DSCfY ߉Z3Of)%c=*d~{ _arW,Ě9-m';bl型 ˱Ykzh֦XcA\ qGIZN4?W/H=}$9 ~(a)&]C:**1dS?r. -NjN*d@%l&P2/xwV *4η[ݟLƊ@)QNJƟQ%|R/mJ|zDTr8Fh=)UteiIӣoMgBR}_n2! -m fл-c{1pz0?0y Gf&p^x馵-LA5h?{(1و%:we 4m6\ۋV&. ̫p]Ԑ7,uhJ|EKvL K?9 ,l~9ڿ )2(kN.e:OWûEUk#pm{ }RtM/?qU rr\xQN۴k<40 DWS CT?4"Li&qN2wݿ3w}憀/"}Mf۞De/T>Z -RႆTQW % &{p3[פQsGzQ/go7l0L[pnؙ!X ?dM'6.=!0ȈuOgF9Bꪞ\1Ow3.G;/Й~nyj.djڑ+2!.8l|լa˕4 h1[#}mwGg ~Y0,ܶKY,Q51H|O+ɥ)aۯ<\)12mZuojgF הĪi:~;I2=j"U/YِY@%99h5O<"ga9fӆ3=RA3Xy~łnش&ɪotONyB$&U%oqzHFZ;rc[cjzS;N_3T꒲ᄠKW~~o?0-ir_$KhZ6%,wWtؔ^SFb˽ -xNAo)Ewg#C~1^k;eyCWQq--6tDCT=3r`Ŏ풼ֲpeO)7F1'?³j|% qx,兕o؛wD烙B^NAvo]0"ɨϓtڂ?e-1e+VJ󪓉\r0/ >_Q<M ߚ譻 /(:s~2j,I9kV]s#+BͷCևӃfe ):sAlZ4U 4st|-UߟTfބ/ (&._}g͊V=#ge}-P3>V5rFU~9u[UbGGY1 sX#Jp5Iw/l9qY`3CgV)aQqQݑ!TQ{'=89: 0+pO;2>ơL(5e;(7 e?S&ReW9%U b+.؀G{^ -nN^5#;^IS~_.i kUˆq5'qJb`Ytԙ>k Sjotd:|k?#-nѐzXc7Ƙ5vՋ._tsH^A̘i&T[X9$7mXȀ  =BǸ>ʥN)gWs8q8}aGܫG>.}j]0VZKD8m{#$ &c'D,g+a\sȳvRݲ ]lLr"Klf Lڈ 9#Mkc2BƄRӦt22j!Ec!r ->G~Iڍ5GLGg}]x^AZ2A-:bT{)m^*˨8KfTU/۱Q(gGtએ w- ͟}gkc]]^tPkInIh\:˷mVxٚ Qwdb%3ߌոYEC=w=2yn+6 RĜ[Ւ `ϩDBX -}5o cC҈ -6ɣzl1-]9'-D\P(AmVj -tw&-Q6B㺓1" -QYY?KF|IX -szP;a=̵Q>{O-;h7at.kdd殌G|ZgT~M3eAh-~rǪwR[vpn6tt| p7$cGec %5:m{cTԺ6= Io~}j C"jR㧙.Ň0lyӷqۇ=\2">m{~T[gbPVL$aEnߙ/z%'  ?b{ˉ:*)\r-a#_&umَ[s+N.x6"mzyQ>!)#z9W/jv&͊iX0oczɌZwAD^::v'95?m+ Q3.jۂ2+^:D8.xE+b# +<`ъYR6qz-̻AtoۻāW0}PK:4|gS<~#;2I56jA6B)n ϝSÃqIG -ngZsc}ۣRO.Zp@kv.rZǬuu>BOѩKVzl#5_KҖ蠔_6=>jۧ D؀4:Hϙp=̈Rbr=㎣rBE~dBhFhbs7,šc -_usV݌e7f@7=T:oU`2L]'(aJ9 Fj^3 nf O86FhnhyQD-m^גQ>iE="'gf1qpG‚>n4my;r݌XdF= (3}=M&h΢Ph589IqO zapڅ%YG#baJ)3jJˬQr _P7LڲaҦ kqpo -?{Cj E:rզҺ'6oyMK*0%Zm6yR -mknbD{i%'uR/??1)Atэ\`^Awԅs>A4 ޜeȣU9b7&XߍXw# [USFj7In099aGC߀a<(H}YJ$=[ggcM *׽`z'$ڙ.P !z{RٙvޭI*9j̝3#JF +;D R*Lz,G2e iskǼȖF܈[1=}[?6`W}QDnڝj6[8g&nz4a^NAZɀq-:sChޞ&?E*nmؙw&pa -z'Pm]\8$̪dQ)+E~OKZZ_;L\K3øȰVB@G)v)XN aЀd3}f$cɅP1W, ^ 8Uj*aam5+Fs1A'Dani _ Cq ߬v(&L>A晌Kv#+J`]zfmojJQr5?:?)=kAXp* lM;`I=ȡ?0e]Tg?g8R9@)S{r)xS+Vlr+ im`͐"&CtR]/#dܬ[Z7r!~-=:#S^ 1Ȥy]ӫ5[gx,OZ^".Ux*vOa) ?mfw%~Ͻ"qLHh(Kf1}JA/jRHҴhn Œݹ^VI:X6kTHK21?R -5Z.W 0Eof%3U|Au+:D˙tP>9 ^1yҮ8/[~/AJ{(uJ헐]EG&uC/ IB69 *D(R2ȄcwX:x>|QzRgMn=z0.--l$7FيW,4r]AQjZL1%j8}yǜ鷯ߓݼ" {,!qƸOq%5dB(/׋cJjA@Iv)0I왭"BhZ6텦67;.1͂.UƝ6qyo@:XlQ*3AzC0Vd?IRўO-d?̭c4ڣz/`L@N:CjbQӋ4Je{`Df(;`:\N|XWP=W-!@xQ Mk8r\IɌp9K2 j&ͬV)m2 I?w -Ug~۷YeJ[$+:!D^(у\|RޑPw${@Wb -ߓU_z_4PǃbۥqϒPǞ1.r,s2LV}v F9ecEݞpj'RItI0"etNVekUJ$$FZG9Ws@,5c:LΨZeJ}C:_)Bgs|EǞCL)R={9rr5qN)\}f-WQ|6qJ˟V7\GUDMOJG wܫFZς:w7̸obxB60^%} \xHօtb$TAGT5ޅ +e̟ߦp4 <`Ru9ԙ PL#۟;2`]g)P> K1%Do|urvnNp8Fy S\FbtX.64ZkvRi]D FLOJ˸FѪ`RyOW)V0Ya.! CAa/;Jfz1!LD#̏'llWCʱHqF1MFM1a.e5Lΰ\Y0F~tmĀ)zFR޴3lt|<Һ _[y2bCgRiE^ 0>*~B([q0qVx:+4o#4a5}ԒO8XM*{ȯ<.ECG/r+0o=zv;ff0qcȂ)|؇Ywu.9%e4@FU/@gԳVRa -]ϫV'9U7 -/ĕfՍOԲ݈-d *!oz*zo/Q2T*IG[V039Hŭ[ jU3$eÆʛ#$glj,v"Fq5aAkZ >4l,+6Ьl_?JlyZq*k"x>#Vg= a۰B5V=fJΝ*f5qiq)uXI0 uOU4%9d!ݍ.^߱v)&OG>S>Pe3 ֚w`B|.y\bHo^1:֬hǃ}PzOaV,I:Xʘ9sQ7Jl:mCB?LY{!2|7X֜A6@o-!kDrF{ -yZ`! w( 34&~iw'57ҫ{d(s9eenbӬPP_Ո1mo]p0NR_D%]㛩+Qeݓ >ᦔ<85^ӖYοa}m[}L̪߰d>]7z +X5>WT=\ѼvA8Fh6uVP漢TE͖]69bʂ1-fakj`޲"}fcY"5ͩN2hvg>_u$ء{6hFT*CK?eƔg?pƁScGla{n\m@֖e<l6%}pk/p]q#~;n6}EDuUCՋmws85FGE wZPƞ]m}V^_߱4ݵ|p#KckY5@6lȼmy;|A[vdHo ,M'w| YjnK^BW-;nXjW7?"F5>PѮ9Q/hkGQe_-;>l㎗Yu;Sl4Wd?n~ <\Q4<^;qtdw6$l:C͏ק-j WgÖF[ 5YP4_V^,i9Gf/ 5loG2R۱kFPⲲ޾ |:;ْOS˓CO.C6 /w}|\_H1#!QY/^*:.*<-%'oi!B?-Ӛη??9ٹZb!PvRSmO^ ig8YN+nꎂ=ڒwlJeu[{ap::ܜɃL tK:C==;aMܴӡ{u²9yÙjBÎ_wlWO=ŗbRM yˏ:n۱߱V/ Σ=tش -D'/|k3m׾y-sOig2uB[(]Iqw!xRZ5@Cff{f>C!rDZ1s> _~2=7:$I|i2{`cj^Wάy_31+Mp[cUq{lՑ%?\-W y JTy= 3=Z[E? ,SkNVN%7^yS|6ui uǫBpZ\ktX/2N"v+[fq؞%>mO4cQ2:#fo@3G//lY'91RVoMc[r0NM2:{0}qKRqv_*. 9-NO%<&Mb!w^r0tz]՜?o|㕬 -y? ;'<QG8Y${ wV_ڞzoH^~]sp7M:uC./wU>2` s&7ËƁ9|YLHm-֏O1>JH\|v#{E]Ɯ_` 5#&IѴ\ .1kV*nPd~ ?brx.8J̳L10R$;;ƈ6s~u)HY6 lWJ;hrtҁPݭw*.'k oK. nR7?ˊdUmY? " њG;7-eO4DAē ^ZO:̊!DE($6Vy=93ob>yD jģe \>o/ jŪ.HDceZe*c_d_.hۃGSbJ{+}ANyTW*VEȧά3 ߫Z.N*M<4-6CrN!1FzP.5>;N5 X$:Fٚ&uyꎺ)zw݃Q++ʗ]ɗKM+:r "*('xP }Cw^|oq?}7 -9VsgsOΎ~~9z^ȿ_-yZ&2`$}KM-F16ÀP 71_WrFnv%=vi?De{uXP"X,b'WA3S{X(WӫZOG+g/3MϺg+s3V?8BGGj# ilJ!Ƣz9.l/rcMTc/̢bMg=k>^#?SQ92~zShc`^dF.O!$&-.CIBaohK(CEduquUFޒyӯga,uCJ."dWga= -j B!eg-ӏy.%5^սZ!rqG\rIPpV&,i85Qj?g !3&fe"9lDmJw)mK -^rN_xOJl{] coW?rbn+}fёK/˯H2N^]Pc|Ou36|JXtcj]e$&6ALO3 ͓L ,vw*r1 zޥ^Tv彝we#a'H߄m뒋C؄O_jJ)BDe>9rLdotSl ֩GKk{}Su7&ZƁk%W_Wܱ(ˬVƻPΤt

U=j{=.BיyI'}G)Cg+ WvFQgKC޳N .MѣZ{ {l斨Z O-,}k[us+aƉC_&y퍑ԝлȨt<R쿦R752k6À@ưNb{_\\}q]ӾAn(94 sUw ]T7~{^;~.lԷčΊ恒kIHvOCgUeSϦLR$QzE -wΓ\x4*p Uח#K.+@|*2(fi=V귇)&DY}WZvr]ٔ~u ?CN 1"8֗=R&4BHX,o:ehOAR{]6Sg=io6ɧO^|QtK֘vy ^_{0EceOIEكB/K[+uUs#"nQB7-CGa#dMd+"DR@%'(K. ʢ7W['*">.+ 84L,3-kιo&*{65L3̒&rh&^Ra͢bTwR1Q51nM@:8C"won+QA]!,f)s4>蕲AN _멼5Pf%%ȝ)F$.v-R'(64].d|?ҷuzDђ%d]Dr5.e#/q4#ar7Ԉx?ab^ٜ Lh"Sm5⫪ߍ#D0:})JЌzC379W5wWzyhاx0ۘWqe['3<!9X>㯶T\Y}]~Cr{=&fsZv!&ek[~w$}ѝ5?mPɪ'Ȅe>[_; [yfryRO}q;rU$_=uBW?0{k!q84]Ϋ1Mc5ygGБ[5;}HɤنWtέ̒5]KokZf9뫝ٿ,]z4Qȣy_F+ɹМtM6O^}yU5 1*a-W]%ۇMTWPsV:Yז|MౡHMGlO֣:l\[և5*کkOWya5ڭeP:<*rd&_r õbuUqTy84n;OLYN(-+bc嗕V]\S'4& ac j[䃀_㔐'іIBEcI\Uʇ))~5tBo# q f}g*,䚧%|8&Z SvTY{S0E+o ͈1A01j{`-l3Uy0t0uI=N5yC/SyI?$2HKԕMP3| zUùEso ue IybqV~xc I?Ll Lȱ8PثxC`#5ߒr@Uo*{>oik6a. rm_A.})WC켇sbC{0R~7<9ȀR9G(Ms]VKZnB~3MT`6^^^nӵ[}[~. \ɽwd:@xUi ћsyEKg]/ZETU#NcJX&+#=8yKUD+0>:x8^fc4sIF3YJ.f83#\SzvXM7qU~]m.4$Y|u<}`3 HD!7N6[Ӵ}С = -d@wE6@eog88v9=RhbV\ZWpez͹*3Mr6zQCg= bv7o`ǚMBLLY'Iȭ)rc^jC{_V!:WeZ3~^}WoY]YF(-cXj&)$22A0}/5zwTP9d $&forqkr¡}` .ge;ØmGѥko*XҶ]XUԝqbBh[I/5;z%P˔2à `VҶ%i|ުQ>M pm|/,3-8 Ju&&0 H,>ޫ r_!`u]w AemΧ RU@Oh^\X{[S4%|]A+=Zaؤ)Ʊp*( 4MB[= -F(ٹ_McEa~0v16>(`{O9j;ȍ^X;nq^yֵvKYqT۠vT*Lּu]t/E'QtK/5kǡy[dſ(suKqB_;xfCG\3[@V}UE\p}6|X, -|ƁIa<Ʀ?T<¥Ԝ&q)+=ۭ}W55Ylk7ZuwuOƁ.ƙVq}@̱ρE;f~9.DMyP6Wz{Ko_\+bi*4tE-s!sO3D{uRFYEKN1%Wf1};sğa=J~ZzWO} \⑎\ҷ`w.bBF@A*VdFT1tV5$1OT,먹҇Q]RJ_ɮ΄?<u2K)8Yin̓8>DFЏRfRHtJM7LG5t5-jRoᛧy7X0*j}qkX&ʻR |O'w_FC -QNGM,?Zq5>XG ӯbZ(ٟ7#= Wb33ЧKo^fSMaEJJʯE5BWͦLsj>v͢bRSğj*,i-pɸ(*%x:!C}K]>yJ]L[P!_59l~oGͮ]x[⫻ÕaylC ,75Z1SB!2}Y~8EH5vE3sgS:8"FnO5ǘAm0,*ޮ$^V5ݕVp\}l:OXKC,=0Nm=ogo#g4*?AM&[+@*(ȣeBB&cH;^ 2=OHs(1ʼ.֦$$8XMChCSSvif )@~%ﬢ1'u?IY~ mS?>g hmh,(aM:\Ͽr0|7olSWF,4r+Rs:|uŎs DCIn9$f퉘Y&Uέ2M##6ʯH~ZhN=u3jNyV 6j:MNU6S6&}?Tq`Jo-\sH.9| %딦)gUcک̋)&ӯ'[ Nw߽@:GFئQG9Ju_xzQ Oh#I\@B }Qu`E&ĵ'z"#:+.&(.xکI9R^hEpK\6A=2>aw6Cn`򚸏k<:𥞣iJzkiQv[єy?(1oޕ5pmVߡz\Wåz4\GEdawxd^iC{v8ps>$,gf-pƏm#E5>ߢM2 -x>Z#WEĽqDGGl5\7#/^_ ̀I&KگYn?+< e-:OZk"W -o-we_z?fsUыCZ ßx7Tf@sv^MqZZb[YFR^ʙw,b9+Cafqd![2~|%'0&+Nb9vi -xi=+AwG+\U- aGk#J>𰧇3\#߶Jn@ṰqR8)8A(ş7h3z !ȆvW{e,h(y伽W7zZ GiI-2~qM򝓸á xy?_%0=:N\t7I  9I7W)ʿp8CksrYsY>-e/Tx踀< ?(ޚ,"7HI+l3V[&y8VH5ۣ'T¡V1 %F aJS6$LꧭP_P2rs萄f/JCEwy6@N6<-G+|ΑK DHs.'eq1fٕziu y <+W,}OUS汚8cs;ӌ:s -vp;&.ko=ف۝n'e1r(1]G뎱~^R| 4CFyx}jSJԵfWũT#rGyheKCnUw7P~E< W3NOQ7j4O˸ [GZV}TߕynÛSӌSko7*f=Q[. tʑOv%ۣ&®h3cZr~f`z !p'@fpkY(eWMU{y- GB%Ly -2wxc7N]Ss1GMb^ŖHٝ@% -7`9BKףf @ -J4"!æ8R&y䲓5ãbTl" -zjC'i6`:Z>=6iAH kSY|5p3"]f\J@E:ĊxS0In ;՟uL[{Ve2-"dj_7ZDgw",ZŌܽ rl!8'W[=p;J$lVyaGOkEܜ~"ʿl|`4L*_3M$y.I!8~?J'bd*9Q ꎗ]CZ=F`+lu;^}0MJ&DmblR̐R|J..&[EH{7_$]NWY>eǩlk|l+?L*rɥ'byPM.ܔSu8E/s,0J;6o\ejDjn Axu|2lUNCǵ\CƣAVh4&~>-3gPKh?겯]됢?U!lr7<5ǀrz+a1 "ެvqV洠44k.DKG62__z'̣zZ^Ru%: -0nsW~-JWͪ䐳QG+%_矷iNK*N9":}(e WxUr8xPPN-zs%7)qŶ[S_Lo>b ,)G:.ʭ-bvikx[}MU\qAPtq #t?<4'kfdL3r^|lN|eo`Y!<.(tS5F.J@w1P-mfXf`$[j_Br)^̷_N֦Nu#cgkMUw}ԕڶsDW^z(p=ג -lۜMy"ƥNW> *'\OÄ{՞bniXbsZ eU`-s$YU5O Wޙoʺ65OcXǶb}NkFG d_qq*BkoY'j_!i_\?QCUwϻ.k\O[;3Ilj}㇍Vɲ{t~(bkM@ۙd;d|#)Tlԡ6*EYəN:LEO0 sKIZR=w fBVz s[yа *$t$b6'9yb=I)wF./-i!|rl_u-nFi=$U ͭn5TR -ӨI#Zqc˻YZ y8í/?jܳЈޤ3ڦK5)F{B aZ>o,>Yu}D -ޘM7gOF 7Vsn%ƾJEcaZVhoaEPR6JnZ(1pS;JJX=1UaGxTpyEQX(G=MWqP^Mh99vq3Xz:Nr+[5ӄ 7Gڄ:.eӊ7)RpMiLkQNu Xc,9=G,[%Ϭ O51ǩ -1_cUlm.4{=:_FfaDy 8dIy>5 -c\[Rk>H 鰀tvm`#ӌ␗%&pxFX2l Ћ.)pcE˕^)}rEF,g%yV 5>ǧa6a)i#Tt»XC`}Yg:%g"l26dGD/vjڞ\BP3,3IֹDY M&Q -bTT\lxYvQ4Yk*m,cS1:(e`蓧yrѱW p+-scC=3{_picΌ0`N5,Z4GƠZcJ[Xg+h6=oO3BmKN:>~I͝E!>ρ -&+#帤444r{,,tJSJ}"\7ziC>Y2 -rn3ˮq,67 |R=7r%- -نIV=l"ҥm6BRde2ix]QG3ZqiSPKjj[M.)>''k["i[%5ֹuoEp/q1i%n]yy3>{\._x Ke}.gcqrJ>Kq*@oXP:4À ̀f` -z boRR"rZWͲ&Q@! 3GԽ{=C{O5@[9s} gD̸aS Gփib -`0wʘ&EÙv8'D[igJB_Ck"ܽʇLZSspGi%>%#'xԲy=W|y*,&_ ~%fT"ueT~^Ђ:.ΩCo)G T{&l˾ݴqjYY -6`vfwfP6ӏׇmz9_oUp<96K>60v/!W*jO; oU.ͤ`WoLRm2r[b!|<>ˁ㎀JG1I{3Q{rs# SN,<=ZJ:Ya}ؘٖ%>QTE#x]sW|rSӧզ^Hܓ5Ts`hanY3:k8'W\2\]Fph ڗ77ZzNG(5P7Sf]&qJeybKJzaS3|Fp,,+I.E[mA?P%a ŒcN))yi;R9Xu-!g,75u e!8S]%cupspz~Xdݕ*Uj]f[L5.rzd|w4Qz: ;xVlvX~|x[F6B\?[YpmA- =xkoV7hTh`ZyC6VOmbF獖8Tye7Z><^Ա:hQ@K^oa:|g -~i{m?5Kʨ  6 86~{Էƙ$ܪw>qNE*&Ƥ[gJ -hlߓSJ/*hd.5TN5yK晝1Lws͡Md-qu)uc7p)ȝ g}6WUN$V9,UU$ޢ'-DwLbp4cmd9|Ҿp@00Yx3dv@L0I:cO/T+RBYK -gMӐ>!׿F8d*#4^}Wxao*:zǕš|Xc=rnSJ-iv)¾*_u9l]= *ɯc@}z6ʫkmI:̒fD I$#c滲kF!%XNb%x~%Ϲ@)S- XeDU{s\|s\ޙԀJ'6)Pa+MyV$qi-rOI j TBXZH -1еyBO͂Rsg>Tsk{; n)4㑿vKQpG榓Tٖy%Лn1{ "e'@ǃKu̍pysFwT6ū~3jbj6XXl9_C)d{ca]*ZG|Z󁇵9(V[i}sDBՄlh˂Mk-y:6x`uW -bK\<"(&BoAzXcc>n>p16s(l]A/ܛy/sK*~wm'<-a:cb =7 ݥń|[ꔣe:uU/%TA2qٹ1AXO-ɲTO / -`L>1fͷr5V_SVܳC|~ ħ 4̥`AlH`zPTX~Xý: -<ءwVy=2G˛ϙK 1&VK[J Y0,6~]CZHt0O:ge`O/=y&Z}ՑNx|yfC(,5pC1egy(90E(DWЊ\ Dq#@*!2IL*i>%&-GY%U Y|CZbLfe0l"\_;\K|5˳T^j{1 >zi_%Zf5{ f}^Xkf4<:yze%H4%m-4^5nGD;5>ߥ!ؕB345Ĕl >dCp&x4E%0Ao:Yoyyi)-V`swj*hG7A#<,L뼆ڋQ ->5=wKi%WX6,Q!{s]Wvd@+p^.5!K -hY4&"fyzM &HO-sٮfᑖ< ^H˂􊀖8^bc<*FO˄:TFb Tr[Sg+yr=|i`-{|D6!Os:&̮dfg{ţYOG9yS+`٠Ui˷P>W {s6awƻߝ`47[Z\Tk -,%9NI~U=(qtRgWE@O ~~U*,4"jM/+izTjۛzqʹr/HG)vK|jrWjz¹[[6%%SѡiZ-5_Wk1󶇊/&oj͓2UqF%u=*ܫ0AM`&`I\SA- -,3]F!ȓ{Sf11&æ}0P`l8Aߜ6u_i8'\ʿ:;TjC'\Ƈ0pp'lJ@&ǺqY ՏN@X )t,ܯܨo}&H3ϒr7IqxǮe#@6v)׸Mmzu&(B{5P~B v1آfW_ԺO @|^~YrO  -5͍+GjBG=ƯTk61 ddWm%/d'mda>ytƱ;bأ%_lirH24tJ䓣+ h~Cc0.ZT~euՀ@,g{4RVS:^q]סo/? 7/Nro'a"t>]l:X$v`2`PD5˱8qlKbQثHNb]qV)Cb>S{ڻ2w?7˳/}s{V.bާ+/߻Ы]M7 ݽf+>9=_: zځ\zޟAyϞ1slgv>sXOVAғ[];bٽm'w[f\ڗo^}݋lf:g͜ _nv=mO>sh|@p˹?svt?w]=O׶XG7<=זNi{̺s;2S΂>{/]}/mz_^{w Ձ+\ž_>o޹M]/<qd}Ǥzg~p+/8׎y /YYge=@ۂ\<}eټԦG7w|WZ'_|'c?|g}z'r7/N{m7bcV~}{ yoùS[Zmst]oM: { +69;;Kg3皿7? Oef{Y~Sϟ1sddgj:v3Zv~3zqgNlx/2[z-Kﺼܫis&}W>s1sq;شlZ~0OO{fM3Oh;N[m_vn_޻ٙyt=+.sb -m_piܥw֬k&y_qG>cp֧kf|CS¤o}Y߽3;7-{Лmw=vzK恣Z퟽μqs;|ؖ֏~r>6t~k`|k}['ԿWlѢyuum\qrs˽6tL=w]Cӻٗo|K_]`ݙy\\Ǚ]=>kKs{n/tmxdé+z?~??L3?w񫋴׵=pr{|둷{G6.߾7,6X+VoEgwۮ?ҾB3lo~rW-G7/gMze1{.nw:\՗˻ -K/_rdsWM+۾\x;Sw߻K;_ݟ[ry+|Y7v5ݘ{~pԺeim`Mه.^y5w/ڻϏ1緵{ew{Om-vn{S|vk黎mqUksg9 vlSǴ{`cqag{ --7kzj{ߢ3.( Ww;롏^Yvѷ;cOGA.;SA/9w,4lsm[[]cw~{[ٝ-?Ǿwӑ }3\{ϧ>ikKO~ޙZr:yϧͦU/cøw 1>ehɩ-}s.~ {%ޱsSOnYzx+:{rބc㾸+.-u}+ />o6䛫[On|fPvn`Χ_%7'{{`Ok"} [x}!/m/=vg/_nC?b]y:~j[ .zsv?Vmeӗ:~7vٻ?y% ;>|-?KΑ7;{W?9[=ͧ7鿸ٸfZ[x[`K;os w~}һ/`|O.6>z~zuɝwyܾm8Ί± ]izM.𛧟5 ~G6t>t|ss{\Ճk@x3tv]_X3O@eςs`ku-'u,yv>r-QS匿~~rۇ>MR'}V~b?^unWB)py`7OSMphW ^;m?󧷷2/mvfG_]{6 ;_Z;+^ѿK;z^]{mk;=s>]20gW_~wEW/qt"#/g\޾_g~}8Jnu\wyov]z V)gqϟ`?3a *?[}{9?S}?rtӣd}rN {}ңm͸ce彫nun׊{y~rjgڹ}G돭Uk]v~/O\Ȅkw~|8 -6̵}-bGd:=sXX}ygo??yq}F;cwVڻ"d?y9mew9Lc}9{Ɓm\tq'SOooս+[9|{bsگj"O|!{9 =}Bf=K[p~޳o_8?_>`3}>}u-z~G'w "IOH}D Rmj$T'A> "IOH}D Rmj$T'A> "IOH}D Rmj$T'A> "IOH}D Rmj$T'A> "IOH}D Rmj$T'A> "IOH}D Rmj$TgqV?N>۞t+s{҉Vk߼u kZ0uC;7ZfXfi¨Ƌ,p-\\;~Q^0\1m۬WwyxEוuQuuszo~ܺفo0o.@njTǟ%r=3ٞB30U?Mſivci ~ -u0S +m(dqm`]lWk>Ӄw`r200>`8Ն`kfG5742d b8H`2n]$6 w/FA٦uuJf0U6ouZF^k^ߟϦz&c5e]k¤u|:^vkkFa}T:# 4U7Ft+scD-^W(_?Lz<5HQ Mr0]ýLOo},+E)_HkSw >NҊgeXz%TmUGSdF.Sh$dvkbc=BT%I!?ITL(,s:ǂqV{#Tl{v#bTr}8KWl TL峅L%% ڕ-HeGr0Z -T<\Xɷgp&k$ܺq$GIp[7kIS.59ɬ(;;UVWGFdWeΧZ󜺼6b혶fR'kɇ+{j%\>͵Osci"\{¼zܖq(V2/V6/Tp|ejk|wª̤pneZZLecT+6Pn *$G)U&U_R6Ռ+(chTOOkf+$qxencd{RLw05[{:;*;\ V}C_Śoê˫YPVB.;lW3J^5`7&ej{,GJ;MkAJٱect @p_[E]mٮJ׺`Y=T5ధʯ PѾB|07v\ 熕pGaƧL|{tz{`rR[F|<IJkm 0[6)O#E2|WZYm\wʊ.JT"ӊ#$Փ@8ܢ=*MfK1HR6s=lBPXsrRdӝBmfڲ=#ͤ -S*hcObםrq;U^>BIW-ALՌZo!D;QmMRLGke@%8}C7 TT8lToiڿdho7u >0&cɅNOXpOWAḎFd" "FU(kDՆKv g ݙBVAj[ߎrI&obI9?'Rz.&$<{KNIE-?&s3BX䜫7T$䜓d2ffV>>tO&iZmm\Ξ(vߨ<Օ*Ϯl{Gސem-@ ʈ4|Su*ǒ!ƛ֪q|,qKBpI+qInCpXI:pLlg2=LmM2L\>3R= ЍpVd -#5uS]6N.]1Lɒߌskg,ex1HГXF2{3i0%zPN½ý`W8% bYq8~cI9I0' $$sF̑t`NuI0 挅ouIU) MD4rw11V5ܞ -I{jCsgsI* z9{ste 3Rّ -] -r:n:\U*]{ޔKhjРIgq٪eMZ8v 㦫7̎Tkn}8l\PvqAuٿ@[G1 )X]yڅZ?S@gZ+׵&(w; ղ8]uI*wtCP.vd*`/Y>NC - -գп-h<ʸ4VلXy<q% '1$QOm?($QrI$?I'$QSoc]HUP53mTꚖ˖_-7MWU vhc=qZR򙾁LO|:rר#ؓY#7^=+U^=-ҷg{:`o}6+;&Y+(i_J/+,B|$7$F*tš囈xù-._ţgHI4nJ$4k 2Q - *x/7nB7;- 0b0*)B`yQ9.ac} TO]V> -PV[ QKڎ] UkadZBT>`%x5N&^`~c <ƭ8^ ٴƲ;J$n`ue/`&n`&n`&n`eơXNX}n\=*_Uؠ/Lƭ8.(ۃHRT.ȩ}a#i0z#D76&Ӿ~!OϾ+XϠ\o*-,qayWapyˬZa X( F߂.MQ}7dNU`,sٞڔsw:Ct4g7XFou$ˮ3ȷҙh4|STxp(JS}atl^LvTΔ_* -uHne7[bjMɒm$}La,c~Xc=ĩ˵bAPM½ýLHX+v7&Sb=:)1n4SrZO7]'5/Mk)W:e+s,|DF7"bV5"#47 -"bT9•AD1S6RXqkT>[TpLF-iѮlaF*;jNL'*VUh+R*RU\D&64h턪C%2Rv֍bv@'}#X^S{_jצN)xTٽEjA7R.|VSUVnzv]ZVfԸn2[LjjHø)P;RMEF%d58Mc$jGŖ%njz@Sz=kfڅ^˵hmRm;47-/(Js!RBSԗ> T 3vzҏ֞Pwk r?!'7"&q -C{V>ViC7cTwMxN"'jI]]U42%7GIMclp3GXM-? -ABL̀GP?Oδg{K0O3wn?o!}{V? 7_2uubyᗇ2 ՛S,[Gh\ݬFn}w~smχ4 R=L5ݳFDžlBA݀ۍ9Ah‚F[X|z05WU? ۩oM5ڎ _FGs5 nei.B4q&xݨ hl˅gFBl荦 aYt 0pHh0|qLڊ78Zi0Z5uѱWdz9u0@`Fe:>!;@F6a۳ xЖ @KZz=~ -\ -& 400m|f[Q>Ki[ g~0NfJYY&<i!1 sL[78@`:<4Ĥq]8Ws-|%,6'&Ї6\ilāmC3,|\Sfp uK`B]x.Л#EE!M(`< -mkMd0@ZЈ%<iBmL;ir6iDk&"dc -k @9p-bua(0@Ŧs/3n0j*@k@O`p#*3r`r`r 8 -ǵ5OÁ -:rpkZ̢<ed0Y6 ȊRjU]V[H qr0@뢬dƃ1-zikS -"(T_Y i8ReL7 n,kć) 62>#K} _քc&DLYb ֈ"fZ7Ȗ%.$K Kbp .h${x&  kDWp i'Ȗ -uf9oh -^ᘈMS'e 1FG$QIhP^DuR eShHu '.nخ` J9>uz0OוFf¶VLA^q5l2p=d&:KMc7f@ <+0Ȕ߈]L( 5(A<vɨ t^: ~Mc#6v -@Q8'/9W[Le2x$H>G wjqDPxKu Gk02?O'" (:,d z|\L0Y{uY06(\帮){3F_Th8h,XK1*ftE-4 rF$`N oUE}9 FI˴h)ta\1 J="CۆX+$B 8!` 44ṭ NV,A,4ЊHLF+<|Ѽ$1YrJhl#+i(.oȾhhxtNd7a͊N'$: -+(FX!jhILP\s %g@[`AIvBYAx+`Kuh{<{6[!D#|A@m B؀8x*0L-. -#m牔;"8ASD^6:Rڤo`7ن1BjشIjD -=RdFp@Jy9u2M] JTDق H^Gz3l97D CNOm$ULAGq푾J L/`ź3 40(Dh֠7fYjnH $,EAgGvAfXoP3iW'Oxl:XFĊ;SGх -Cu# D{¢GE3ahN ۦ]FAz Ԛ]rx84CN> jJClAQ]x  PP1d@ɀBIr):ZĞ()L4 T-$(nhu"XxDJ¡4 .h,t A7:*}#G´n.br%IS\ ; x`{҅(r^3G9wvQ 29tXl\i@#{4`qΑUQR6!X$m%zhHb!X5d F#'q8h0%mu]7Ltr!כ_rYe\p[yǚt`9 vI>=P/SmD~?;Fm¥Hj$5]،f5]W":EDHŒL*(~?LPt.FEYX{?0 |Z{i6QU. Kk'D"g‘ -slà)1A Q45-\RQOS]ƚKax<:Fi2P& 콱ybh !a=6HjdV+$ҼΜq 'xJ*I0,t O:-AaOTQyxd\B |I?%,dJ ҢP4OE9M3> Q4\)SerD2ЁDΞEU()-[0[9-(:/vYOWuE˖ -*Z`HD]؂Pdj%@12 -Y& uGB!N>f#Ôd)(pTsy4v93PYdX:L)g/6i\@IsDd2CXB#r\ξ z\c6At*ԑX&(wDn(: XQ`]ʻXm^4% M) N!JK䣙hvd"7fyV"xºTZە 0 0,tIҭp0RjRɢ01< -`JVTqsaPoPcxy&#,Z Nk">CISp~ `Di)52i1[Mq3P S c{LYZV4]W!Z˩r$H1l6?g| qfrN`i"&()9-0##%E5ȷ'8m:2b\Nxz̰kA "PH7p9(;sŌdR9Hi+>ĽCXuወDUT2N\EynLJI%HHgEeKxc]@@a) e n6W$۪ XFƃǯR k>+-VՃȂt3DG:U#`Qu0͸^ hގ(Ĉ*LxM%3h.\l:Շp8Z0b!XQ+8]HfĊ7"i*d~RxAUsde"TlDʒFe.y1H}[ %[Pɕ9n0C2T0H /,ƍ}R&:H`]"Vk2 N t[:`ޖdVcl5 E M*^jǖv-7S@AqplJ'm2#-b/yŊfd\ Pδ'opyI'V Y 0)HMżd;Eoߥ|R^BE("̕?%h!izDJgx]%$M+x5^ -drDNՇ@mjzI6N43EIs,;#H9!ǀ!JIVUd -DׄD!%&4T)gT)a (sX%m0)+D#i c 'D݊P3R+ *Y;$>9BL@(G9`8X>!:4%'s7-SxD'kyj6[,v"ӟRLY%RD$u,@uM&ؓ7x4($8IJŅƒ %SĄܡBnGFIƣ2klC,%KZ2g!N4FmS([h8T{tr ,AKDJ!3% ;ZҼFH)DQ#phˣ#wPXCV'C!9XG\owĶŤ뢎oW]I8@Ǽl'P*]!ѹ Q74⊡؎v >Qy:h]g1`d"cիb;S&[fi.nF ۰UGBh+EtETLS% kU ~R/|C[l|-Q2RcGvHʠ1HIA2KXOͳ(ׄ Z -!#>6pD76l\4faXN,A6X|7"_I)loG^QPCpВ<ȕpJ9I2=z蝃F <#LgRp>_io̞ gbІuga3[E6DV޾&g`uFxT`U:&Hkua2׳w7*lfܣIy:(#{XBKtPӞUrHsVqAGd`4o]Gûл61N]_i& -U_* Q7kR?,Ω>gR&].b7.C!zwCW4kd /cRK4xs@Nak[S]Hbxs7Lh5s"vpYCb -Q\ 5li uy8L=Y)4:mB KnyR', 5guNO\0KRyaF>?IR=]׏2hG-06B~^w#ozAz!p=@KCH6M4u'̝g$ 'qlfP刄cV zl@6(]sng* [MyiT{MO7."5-h"Evz: u$H?biXdBīHStM4Af/ˁIwLC(P jʆE!f ֟~S/3,vL[ᨁ:" ,K̚^qu@U7.ebY8zN5녓%v0&fQ(o6ʓ';ݿwa\A3M)dô=ӳ#q -+V6nWA)GĭzX Tuh8b -,vMX*iijtR:Ԕl:eƮ4mӆ8u4R!&*| ? WBL 0srXނүHG7%)̢kQw\ mz.J?tm(yp D¥~=dAήK#XfpI1LN; ? RG=*qcE\ %#7z*.!QQk9;Mu< MWhÑfc{(`"`GpNE-[b?yAt,앣S m4kɻx# 뭱{=mƥ JBa2q5:+jpڄͧ@FvHcNh{tT=kp-ZܥQBCM6oKϵL{d :ՓPԄj7XxlӁsd.zYWwS8icЁ.ɣ7V[X;#2'aZpG9Wk>HncBpeJ<6$)@`T\<_륩/q0 l{a~:PI2 L{uP-Ḭm(N -³ vP&lCu_0tA2KVMx>6-ObbFzpX5VO6,xcI6K endstream endobj 382 0 obj <>stream - BuQ'jՅ-0퉅='&wa+*`TIrձ:1S qk.Kx'Vy?G҉T&."iq^9IW 7Bwk)qEma_bХNNr,tK9[47:! -t$l^[FlK >ۆ㸖Fެ Wԣ YSJn<]gj0PQI7."0҈X0 wB,e<,O@[{xJn)q5P [m>ŚbfҦ}. ,nMiܭ@jz shCb6hzi 2Npt$ާmL:U/_\nqon-6w66=xT&4&P:}C n!&ng-\ nS ѥE֠=)m;$Fܯ<22p{15.{ܶZc`jW}xhv:ǔeX n:DB[h3(@{4}`DL DR7ɠ MlGMRIgQ[&D"@ӶKFifL;p -nDGL' KS."Et<Х([ - -F2{. ۰Bm bI#:j:PO䮖;21<OLǒ -۔BYf8QT%tgr_DZeQKAx8:Q"u -n  JߎM:v=PrRqyzVy>-}(A)dnJ(^M9 jۏ>Wx?lHGq؊Sw5-\8$3`ɃzҹPY\ :dISO RYd?,LwD(Z,vY'6NO E{ؐ 2RJFc  jՒ?kϔ;Jg# .A]%wÞeAsPH=029Tk˭Rϑ}c$)V< -ti plKƳF -z,2-O{7<{ yբj9#U/GLOrtvB')'2]< 6-,~.s,<q@#X),eWXy ,Lyqvw1š"DrH22b%]ǁy[8cP g1{vf^03k(,'UY]<.DzԠ"0%D!zciR^Y3b.+ǃni(J:㱊S(4qkR{B)h7J$5]&  sJ6Y<61jfq,0Osd<֔>r5hW9P5zWay%@Kn憭̞`g#^E(ytG ΏyVե#ˎͫ.7_YlwT~mCBG0+,=5ִzps+\ZJz yZEX1 ?ϒtg4ʮU+u8jcuz0DVF`Ap~/4 u`2בl҃ /4C=X/.Q\t/uLs愾TZ\_F~a?FwBF~OO^|&eSVDq -t< -%/|d׉GoȬ3pهnT#I^!iQ+z0d[&\AtD)lCm^Hq?N!*!AFT=?˖2(ԝh|'_dyݔӒ7hڹV#b"Pj;+NK!Nd{V,I;&A$L]g9|7)Sh$HuŒ^}2VP"&"+A((q;4-FVORd'[גQ)'W@FB7b;+z$=Z<'26fLih_drѧږz획¤Fq(rlo)*|c:u( -:!gD u%/4C-ETi6SHHB8"놈l bE)ft5Ƌ۱ bS)ѩU4>8KNjMUM J!ȈA4ǟ$(𢇩鵄iʕnQ 鶜L̨&ߪ2aQ<|tp{"TrW  Iͥɒ.)DJ,2VZ>P`$>nG'ei bIV (&f&ARS1OF1ϩ!W@afg 3tRۗ۔#ceJ(9訧qnLM@'8J ^2 '! -!ėzк)OW p7af29Xf'Hś}6'TڔBJBKY -އr¹B; ]&I(哊+L&2]{:|.,͉xTǒC ~6RKQS^T7A@aR-) /Rn-1MU(AQ'&ڄof8h<01W"L"I@c臭fe,5 )i2Q[OsZ//`c%pƆ24_2 -h֢v# d j 0J_JCC%HJWSWRDbSD'W2ʃ#1 M -S=LͥIܔ51(7&O01 7|@bvdf'AJBcEYcb(@+ [Wr&IIc,q -L %: -4XX)!ɇ4!w;J*\&' -Std)\ZT:׎.i-*/TaZN*kf B " MI4}/Byt9 -` `ߐ!R.nv01 zBCBegLЖs>r -sRsV`Ǧ""05pRm#񄡖(("Mp=mzW_-&tx/hL/쌩{~v ggs>ZTar.Bߴ)UD456ess\T Cd4# M'i@b( ]GdĒ0#Nfj,YY L%sRgj,i4zx9_ڶ!H`!S9יt|M}bxBɤ5zB B!4`)R+,'8xc0 (`m^ Qϳ&d[q]?"dr>tҋptZfdmJ(1C) 8Rꍎ![D@b\nf )MT_JtpjliVde(l7UJE 9!J*A'JŢ {BeTHBi{7|TqЦX\y Rt{RILr*DDg7!#:17MAJy ZUS9E" ѧmK$tL-ts'CeJo R& -lXOtF0o\(p#TCA; -&|䱠9J{ enaRNTc ->BDYkAVHcuB:g؎ -T?^ Q#>ЏJ?:0TڻP2P\nY VͲKX䈙%2';fLs&%0 ?!Ǐ\MDI*~MDITC l4ɆFH m쁠b6ɡ^ -0gy5AAB}^PO'L+1rBjP&&0b܋[>pQ!8MNck'ל#V ?EžAXp,h!(: 2CxE#>$(bPl&wBS=yAKV:_GE"//("};]{ Ah( sR>(K*_?_yibʥHRhi$׏v Z=3 'T} ;TPm`kIȒ0p)f%{ Hn[6{Es% qzQty'pt!l2*EIЬ/-:k^HPnHDlYESf[$4Tgɭlŷnqt"y,$wc`-#\A +jr|-\w28.] -lfĀW$@ zJJ4_:‚* ȟ aj,C`lj0BY!Ar -`$ b (0eYHM5(nՋa_{[ aŋ;+VCX%;-VCX%;ijla(VCXa(VCXbw6\U!XqJw+V!hq|f*VCXa(]gɍwPu)VCXa(VCXa(VCXa(]ŠwDqbŠwDq0ŒwDq0cŒw(xTq0c"Ww3V!Xq0Kw'&$-3V!xq0cŒw(O;`1'xq0cŒwDq0cŒwT.f Gǯ a(fCa(fCa(]ǩ@aƊ;BaƊ;+faE;+VaŊ;+VaŊ;+VaŊ;+}QY%|gl(a9$+̘L|gfYwF`w& -0;nf9 -#Caw70d sqΡ9Ws`mZA(;+w(v# /w mv 4S>tldMfh4MyvtBY3'~Нuљ 3p .ug(Н<םssY/|YHAwÞxz3^ƹ{;\x;@g[Φ-ug꺳^dҝp@>wY(T$?jN|S\w;q;ə|.u "ELeK4]+;&|2}НM;uv;iniW;_QuFug%=E0E0븿]s~fDžTq}D 4f'fLj!O^ʑ"r/DAǝ+"Х(QY.US7 -hI"z3U7*z=NE DB7 -zH#oIo/LXЛIV-Rd܄t'/X,o"Ez3l6xCmI\d"u_}!Wc,oͨHHo'YPA:,Nyl\두xB%X`,ĉ7NgQ(LZPך%6K%D5f `8Z:P%rg7LoҮUrZMj?!PdhpGΓ64TI@TfG,RϤ(PE$ YR.]B+ʩ@|)aCoIn `YuY PbͦB]m(t #y5I3fG*'(dRCei{;J?2Ics@!όDl`֔Jl֤1K6ÙpoTcQl@⇮1KHRuTtҘB^ט%jG5fIxBiKA[ט%};xC6ƌ@핦z'$Dl1XИE=0oh̗,PO ;3:@7RIzs'I~8JO1c^"9Np쬭SPS!yA%'Zbu-{umҟJJ^xD9)0dO4gܤ_b1dg8&SNvrc)vXFf:f5!O'Xs^w*r&: rguNnOủ -=vc3ӇJlUzu$D{Ig(OJ_4L``D󑚃5{9"ÀLa`'m)e ,/ ^H&Ip8?&xHLtoE9C ]7$<)#NHbXi.Uc 110E>hN1VDkn$fo<(`?'&jLEpypQMbuXw}m&dM < ;r6o))7 F'R@5 -.kgUu`ĥ;MrSO5;;A%`NQ#7<Si񧲓Mdf'D@(#ycD -53hE2pȟ2<-uG)8QBQ}nc&j3׫fso9mIdf3xuO,1"pL\[Hr9U%9N >!jԻXqXOMbf<?g<|By}pޜ-0W )"G{$z&(hk2&dRx8ɣW+Dv$2Q"͉FIm8˰kX LN!u^Fω+D J͐e-YK&X="MV…dsF񔍱BaE *dB[ Y#_A $` 'p9=WbV,4m%Vx`h qEh%Kt?4V'?9"Fu_'o/3g-Aգk%∱SGjͻ}6uw=Rzo,'])TKV׳8tz^W6V'~ȱgۭ2- 88Sدv=BEfHd+КbW8 -fd37ht|2F6ˋw5I|3s%:zhބUހE~c1pg;PlhKAEYEE+,5Ϛ ~ dw*s~ /7r_BFd$Q G\c1|}q$?.C Oai'3|z>H36AN'gSL$2؇dQ ?;7?ND|a7j>_BȵdZ+b >b "l6K+bgmYv/Pe5,xi-w6`TaXUGbpQTcdt-=:em̍Ϩ%/mV+DLFz[L3şZ*aVeљ$}>.ިu]fw8utwZ\nKq@A|=%ǿA`MK\u΂f 90H?^QtzeYy>R$ EPpR>9EQ# \©:qe'Ӄf'D81&\˚ƃ@WQ/8-Z#vG%"(xQ *c9ӭyeaׇ@Gh.dxYxQ3=ym9W2C?2 /pna1fn&DAs8q+ -qR8W 9 -P?8[sAw>-le*ό$@;ĉ@ԓ -RJDd!6ըLDQOFi#QW,2m{8@, 3o YH DpH<. JQі %AxO# (" -0 |"BI"AH18;a \ -ĵ A*HLM8NLD91E<=D"b(ÐQ(J+P|Pc! p &3a"asx]81 ]^t -2ydySR# bJ!$+4QI180)$rE1@sbA=.Ґ7~Gb'*: 8E8tX 8ƑB W@ē'3l1DT"AON!8"C$$1!I3<.A h#c6L(@& 8> D@V2NQA$qe􈏩L%d0ho\QEQp/x9RYơ@  ؝xPR`5HD—bgZ5+A"i\gef+ ?q<>|> -%Γ@I6lny|,ᠪ -nm&ҕ,wp6o}w6 $Ф %CVm q%~)= S /aIW~h0}@ˡ ONfpx: -< ^/|vPCi9F/xTi5:Š) +0j*@簘IXq"(C  -R"q>2>^ɹ1yxb5G3Ku,Qp * /`gH qX&$.A:nb <:`7GǂM$<#d}N6/$7(H ac{ǓN=uep1u-Eñ^ }}HCCCSCtV8D%S,>o2q$éɏ.A"A@j7 )rAeᙋrCqc62dc@$a0&ƀgˊAMx P@*0Qv9"f)d*9fPeË1 c -(Tne*b ^!NlAm?`ѠbH&רBdEx-9 $"K! QEFOjȓ`&#Ib̥Sz4KΫ_4' -08ЧDzdWN&cW : T Rk UX= K@s,})C(4 OhH$1Pp'{A PbyT;DXUƨ - eƪ& Lz<}F`Ae#f(SQs2ɦ0*ǫ$5[XD,ED=*Cܳ[ QTKI I].MxA!NÉzJlrL -XbXxw܉:h"O5V%V@Tb P[F<'A0IXb䔉;'җk{0 -<Ȯ,QX 2hFn¸@.B&@L両S]$Pf:*`% -߱ |8lN¦kXT774l^I:(z$%>x)hDCitwX#! L@VX'pH*d2gb/SI$1aȍxp LkBP -ÞK( du1TqJCiHAdTq -& _Ah/`đ <<& 6v)ƃ=Ή 9H3JeB:g۔pd]i+!H:$q8^n@ fyA'SWD bWb#Bů/yz[-17 AM0&Ha^Y*gHquyyy+z8IG[!X .B * l@՘,~bʏmY @uşf%fa}lؐYddP֭J5  ,V !eEP#Aك@bݙIvVh B̳47^Lbj0+&8!H $2&}~9K8=tc-$~>_YDrC$oOr 4 6\3nWE'k`$ 4Io -d*.}\x\)nV r,m`n)+PpkwE(1" -$\ձ((`'8`G*A[бD-F< OLۄD#`q U:(N5Of" /eRi7wH$#`D4 Ӥ6 5h2 hl8Xtj\8#8\ oʅl(` Fύ]`y,p%&oa\'~@ݘbDĨcșz*A26OrDWȆ H( -LXld qGI`)p5V$2$( H78 -09&ԂuLVl0Z% -sq"M#xjba) o9ceV/ %2Z`7rpa(XdRW-|Qȗ]"^XDvܒG3mg),ct\.fvКUzEl>Kr7`ye޲]Fo'}_V]JrtF׸ ?8RGoFjx8S.wGW8x\X2_ (~P9Ek/{˕e${ ż|/GОBn򑥆t߸_Qi͌O -ą4/S_OEC@?Zkua:xC T%"BaK.4*d^$[Hdc9s[E@Et;N?693Q -l^Z.VT!Srk 6l-hO{HaC1-HbG1/\զݛ$)I dy6~7v<d;e^0^FdUǽVd#ؘ|7'鎣i!^6;Rv8 =PsQ=++/ى44>6A"f꾜AR@v.nF'dV;eqjoblE\,!1h$M_*.gi^_i}?B1ne'H>?7#ƹY {A =͸#~FkSFaeU_qJ0Jc慸2GIU~O-,~h}*yC|AYtǪ - (hÉ/ (™CfR (K/B%X2O}"ҾBhsF+2S-RpgDcU`Ts^0 *:`AFj8@CCugdJu*FM<WV: gc;^6G}>`(0W~X^ -ޭS-V_c Z!T%aLqPOeM;a̧uxx/д -xxQrgH' ԃ9)k -t| ->xBS .f@Q4Ęa(%2 R ʰvll`T5 jɯԘ|Q^b/oLA9>:9f8H~yseש|:edclW &3IU; ~|^Ga7ZP9%^3KoaJ<ܧW[$_@02[K}1[f_~2BE7↿I7&yqԩB3Tc=j)jy5 z?VϫPI>k_^8}1{jg/ *-fiNbQDg/eBPO* مaʟd+;.YDIIbIuD",eTpv͉>>)0~$~O}!H"vߣ N! U -~m`XH!R Hݽ-]׿EL]7b1o@zvcm7r_H" - -fr\!196: ':w3oڧ-W5x=;D+]jMtI=@u0Vnoj4R_g0Rd -b \ VC2׾k׸ެy} ߱6_]w2}s'I=wuc1,or6yx؏d<E?\T Bf* t17.?DQaE<@1kH,V6D/)y Q1؍I?)ĘOd_xaxVt56IWb~,.4MOwJ8F|$ߎQ)}5(>nvv& _bOaqZǭ%;Zώf)nZ#L/V%>2!֢;~ji1x'1O#M~Qןq6So T}O,uA#]E"N'߬9;I"'pu9so;6KT_Ta|iPWx$Lg /DK|8BӦR>% 8XL剑Šr~n)c7F8ht֢pZۍx9|:zWW Hɞ?׬xZ> -Oc1fҶ+cExroD_])Ţ!}2D0Z1D] +-!HZAϤ򎧷԰T^rhܛ8M;&è1_gMX R >#h7϶Sf'Rke>;ncik8 9D{z]e]@bDa,|+E7> 7 ۙrFnz=Yޙw1t*K A%g9su{ϱW+>|x~r_uނ;{ wu+>ߒO1߇Kx26#w!.{Rtݿ2L)T-)g-}L&fFq: 2|}ȴb)ųcUK#lZ^k…6*^[7bӕYwS홷 -y+^ Q2!Sw{oKPkaX_'iѺYevWn+k';)WuN\Zz/-7熖8Byn˫{8UĽۏڸ6&R_9&Y> ^}.h-|㽏/쒟մ}ZdeaEywB^bnMkˇ]w|NWSs[su.2H^967|+[߃նh흏j~.qjk*acO9Xm};+?\_fLi 뱔\9\2[}~~x -ڝW|C]v>>lHT9,5Gܑq]^:odmsz<`똶uy$ێ,W֝Ug -[pϬofPu#o~M}qJl2;kMҚ}zV|| 3Tf11O싅k𚙮jo>u8JI2|Iv5=:_7З,6Ǥg2] -EMN^t亥5Gp;&:k6F7Vٕ161umPx7OSa2w[:o1u_?rM}EԲ,yiࢳ>ۅloDYۂɊI:,9QF/rS^Il܅+ T)n^k~4څc#t[Y'\[VjAɕyd ƙs7.BjI{U2//bsU9̗XhQc[x}2yoe,Vv{Kw|eøw RtH_]pP&h~NAql^kn7:ֽq8sޢ{8Eu&͡%]`f[,n,)^.7eb<>m{j ՙU1Gƹm9>P5豕bBCz -5bqLrU A׽x*^ZI,hMyPj1Z>5:QXe>D!ouٴB]tc%?W[uaCbb[ߖj KQr7nS&UJ{*sg;Y_\F\/ʑoB`ގC1yƔ5g6\i]z#Jo+Z򶅚Wr,Om.CO5!nZ}ǿ >^*ݛg;HkUo?'SzGyfGij,xgS͡QKen/%qscy|> jdm~\o}tt3=U^u#nrw6(<9)S)[x*2n̪Йʥ]vSCR_No%TsT{zM⎢U;ufj2c)'T5Zz8֭M]uy(Rs##U\:==R'ǥNqg POr`>T58**O9.Tۭ5:>DZUVrv-j) S+SE֟娤aZ.=Oɍ75Ȅ njVXԧArS[r=:Uxe)-NݷK{2)^s~{df"4o_A#$Q}N+\5TQ'#ltU[; 5h qӌ<8lf56~HnV;z|8=fOj}|𵒞 ߈:p=&= %0p.&̝r$u`=nc%mZmpƯ8I*wC{mV9.VV҃c\}du^;fv"<4nqrdօD Cc"S¤ŽvuמIb;]|VaiX̠&䥕8ˤꎷLU+׾a涐fB<.\Fۗg3j>v/ tjtpw).Ňr7([ɚ͖&SV2SPr7Nt`i3wH\=xz(svo܁ oPzb' ? w{4Mwޛl9GĀWcr\(KN:}qM;(u.l#s娺Re h4;ukn[1clrRqëc"Pa:\Rt)FmB~ pH{wκ_Flj#s=9GtZaTiC \:y>툽VZٮ!1t޾Sݢ?­c{0 )SϨ{ؓeb>әS"ݬ%gE}Omrac[d֥v':|ɨl>WefwVE -j}9ҫ*WP/c[>oy^JZH䒘 6;Z27[C`k/} Lܾ>'UvTENNoz[طIrH$;zė?!{s7fp-U6}6-&9\z6?T\9COκ'}+;goo_͔pS٨\/N܇^Z1^5j ܻ9-r 6^ZUysm8`P FQY^<.祈ÁO9t-r}~|6XPhL޽Kt*GK< Wz:޺jΕu(;i,f+B/9;冚hg-GF1[O`vP ,%rJ:吭Tzv$3_WqFm}X*Jθ wt3aOlZ'˧|9m;s[e?kWZ8LEVw.OQ}r>CRux\vcCLm {뭴pr4yW|iRz_xZ W2D.&c|>/v1}s;&쥲 ]}m?iƖݔ똬~qdi˕˷x *w?dϮj*)V"Kmiӛ vb:2K -:lt~/lAw}M!1"#ZSN9Wc6~q&$:z Z^MgXn{xxe%b[m"rZ -vW%^6m;Vekw5\ʽ=np: -on~|]|Y*SbHu[E8V;̋Q)>B٣re1ϯ'ݡEGɭʋ[)GO㴶,;y?-jIj|ortՙ Ua0K74pȶh}Rhzex |MziD]!Wj kGʥήjlsqHwZueKXAkana|ٹEG6d[&h/~) "*’u(UmiG²Y ߏ}sl -IVk#НLJfZ 5z7-\m3 9{czs_a{>O3kk%蔐]wg>"=Y-o輗[X>M#qp1Te4{@^KM- c[>E /C-+OR#eJ4շ̳)'L ɦ5odx%wwh"UO<=ZSDhRY+y4ZܭnڰUl|9k/;Uo-W4p^\xZ[ѓV k8;MF6xz~oXC:jUm50hln_*lM>E:4ۣVNMU̻"}w?EZښmk< >'/]M:D^9M -)k=*3k:5kj3T_3þkz)B黷0`|\{~9xme,>bU$Z8!ےbҿׄ}H"/| -mV[rċ/J:NxK--5D ͛ý/\Ro¬QIqltk˼.LlBvJysi[l8'0<G:fc^ XMb(ۛ:|t{Q \",ek&!&Or?[}+o:^* -SY:B\3j';n|" YmxʍآZ%m'Jb}W;7}.yʭVX~TI_m_p:Ͷ8hNU'okzUW[H<ʣǛ+-{_/o>C-]\9?nCRVޝ|c ORln6TP-j&ͩVK_{vqcw<7+n1m1!@kKkAl||_rc,?/יf&}wGsdžPАy^u:LT/l5Ȳ&"}x{HLYG= ƙ 1&UN]ɰ nkZ.}\8ڏdr7XVF ~%晁6\F\X1=]̴l wm}8~/LFVb7pj/ݎZF}:`M4xkv5m&҂Dٸ.:K典%O7Ri):սrǷIx}ѮMsfKrޑ#> ĻE*2T7^-Wldں=,ה3ZJsc]FDV4qnL3Ӎdnܿ,%J}9piw[5 [zά#lrpyK?ubS1(oZ^|еy~F4nXstƾRm63Sw6Ynt+zr iU'ŝsrGN'e'y1{'}h#9y-Ee{a~5Ӂ9-dn,wf -AgבsKUW.*"b&眃ŜYZd>z5KxfTz`Yݖ4דzGe2KM_plFxI쟮SU3O/Sc5'GkWNSi br'$et +#,yWـp_LY b:E6N^caQWLH])BA\l ;ta}u??BQm*ނ:O!+ؚfJ[Vj2uZdRED9APh%15RkV{ܞJvyh0*]ߛ6X@̅ew[&g*Wó6aj|tJ}G^X D~*ebI{H)U-%>B[Z >T$E/pbRl摿,KLPݖfd^dt,uoSGTГTLE͐Y?1Ҝj'V@"AÐIpBuwu6$89ﺝ(9 epZ&>z;I(~iC"Wj# \}MgK@DeEjd"؃5]_ҬPN-=tsiU;e{s!=r4TYJz%='Z7-WQS&~YuZX+fS"gF4YtX(%goeɓycEGhVCZc;ѯf0{ۙ -.6TtpR;$Q]d>1%rfUid x?gj _PSn̿T ys=*$0ߣJpUrBr͵Ʈ~,7Nf~H}M/+_9<#,z7۾%UnzI+ke_$aO37oPyve/dNN!Z^ͩ{?ˤ1u5-,h orp7}<_d֗ ˏuhH=`rKZGӕ}+ǵ^/a  zQSiu>E+ U[uo9O]jIQi EIsop3F~x~ol9pw]kF[}NH[Pׂ"-Q:>ΠҺus|:aA_ iFYߔXi]Lk_9|/B-555vh@饨9i-{猀ѠFjetл'mzy$F ԯawe|O(=Zd+<Bnns'A$3aWBngJlbJ30Y` Fے6ڷ[@cmG1bk) /]rX!Lgt^It%Y O -R:1`HY~dۤT޷=Qn*Jȕ+c"SWld!ډ"Q;UԲ,Y4Vkײ{P>M~mT7T{p4 >'7)ޠ ь-RWw9wv- fϧu lDKo% W:52B|kC]]i8SdLM,FJ+HYF *~*j]Ѕ,'W7tfMo򿣎Ja{r~8^2of|o)vSq`V.x+jL?~j_pr(#7b;PUaZ4tw-tX7:oHGN#ҠO nع@½J/;0y0yaz J5bDCqp&>;l[$`07;Ԛ'G[?!!q +wvC#ɱ׉?Ztmj^ߗ6m1 -!z>ª-upK@^6Y F -~|+lw$EMw[D'ҔX٣~kIGJu5pOf -rŃOy{9ܤ&xZ(>=HP 8g/.egY*1q=}ֆ}L?}˼0"<7r:36x٥8B\{X_ʹƈn$J:OԸ-օźvJ5emb,'_r٭f=ӝј<-zQ9'{ -Qi?:;9ob[@;^fû<H'6fֻp5A@C-7d^OR^u*Y̾}JiE*!+=ai-泞)9V -n7?nP7,(1{ʏ[8QkՐd^J9˞slAQ=QJJqLfGq!_Cq(}{7֓E `~'tUֈwʝQ3䗗ɯK7d.Xap+An9 |\~H__ąmF"nw#g '~X\/kr/PsPSAK78ANSK}peGN2~Xeu"3r4X;׈4ܐ/ Y^/Eԙ - +5V:ڠ+[dQZYw~>U-+\SvGT,4MBO6O3״^w#(ojHzѫ}-w_zA(o-!־|iǕR2Ǫ)WeXiwmD+ۼ`nT;DL6xS%D% pÙ^rx᳙xkUXOt-@y2cZu=ؚ=u/JA7]ۜq_ n+<&W54cw*IopaH!0hj]~MXXAF"r+I*7/b3*U?#ms1 sk+ V@mY7n>xpƧV,`?2.) 7..Q:rs9!w*t -<߲E7BzdLg }GWMΙ)Ք9XR yqgJ$RbEz0T'U!9ǭ)(u4|R<~K!̓:aJ,bFcf=:OAiHڅsRF('AMLOQ*Kc)ϗIY]UWjA nX>ʼybwÃEL HQGp=Sh7BBֻDժb}~lKQ\;LES[@YXr%8e -ir4Ꝫ7 ^PGJu yu%۳~I_6'{̠JQ"%;_<%e Fd6{#I@"8$:ؽ@S{z>Wenņ,wCcteW$槧zuPg͞mk)70N?rx -Bj/<ͨ$,'64jnJ⪃t■^:qX5@/w~Q/l$Q8tHQc}y(}RO$qL-N.2{Lɋ+MWN077miȵzr+Wź9*6Bz#Ds_:'RQ}]XX2!sʵ»͗w;izM՚ R`'MWT᧚ͿS;lQvz (Md?o^kgWDxxT4jShE|uJ;Y1 EOv+_ul•x/\lj5؈%TYvw^{BePpINcNuyUzZkuu=5Jr^[H' CCܞ29PI^ǓN6p@ܔ ZWn%N{sWOsm^>zn5;fj=>;+i%!6Gdݑ-iqj$Nb[w?]Fz[}&i+&˪}4ݒrbZ3^"`nʂܪ\\=Ǯ3l{7ӟhy~;Wlpi ̧p-(<$704C述>[v>Y6ʓ()|cĈsk?q$=_F؏IR61Cf˺vUpr~p8e7h"0#6'צZ`_X=UX'YNת{6"o2ӿI S-]r6jz<9INi|S)lm:kCv`;.XͲ_<% Yevwlgr[YY+zLXM5hn-6l*K+y>^̽ [i3}Xf-/js׭ᗵxCYH4,Vyf*M{f{Wê2Gr{3!na~R kUrbW,0dًK}{5L@ӭl<5i<7eʈ V]fjJ"'ߘl3G{ϖRɚDV/>HmئO^I޷[(lG'A?*A*|ʹ;L`댵$!`^Ch,[HOo;}Xhio +- ƂQ+%`ʎY++u?V{0X၁ 7 '8޷@YL~I&{ֽ,CI}`j'-Njۖ_E@*/<'Q1h!wWv.ue?"qyS)W[G-/Ȃ>]7G`J˂Eϥ{z.j85ۀ{{(5aQuu\GWmݽ>G?'i{~XY^O|di]EG۰jN5U -ڡOؔ#.67ѝ qwHץ&b=r*n5Ų䫆+ gJn;SW5"/F\ydgsr!r]Apkool4bzC?g!bG5o{$iu2!,9 ^Pv;7!Rd2A}֬jjp(VbGoP0X9|zCW-bLS:y3- l'`3ng3Jq. \/ D̫ݰC dr,10f2ڽW:r6CiW%Zl-1a"MQ>ƥt P&tE|h$^ -z*Xr#BFMȥ=:sQa'}8{kw΀gOLX]qkY[db;wOEv.7HʛZxuˇ0̇ ]͊PK3,ςTohfzcV -wsT^nqc6@DXlG"^mByt_t-װwK34=F95t=E ǝ(SV:/0eH6nGږ=NΧiV,/Sg)&t^,ڭRȒ-y<)$lP-i$J]4Is=FѴ|K(㱯`xl9JCRO=1xOwjKߠK>oSsPv0ZXF5pR}K&1BZ:;܈!kb\ A.xt՛xѨ3K4i=5fE.)2CLq|Da1q8V܊mozՒw0ঝi>ob86C-rLаW(e">ᏺ3hGU5I'|ˎV}C DgiWa|f)?P{9M@?OޗA@UW;Yn<@)x^XL5pӊءKhRKG(s0S­`a+ҥy e'cBMSydSSKV>kIkYk`[~4N {ʻjT~̐ia[BGe]eլo}>ZURAcȩz;=Ya=0JZV V&3^<{,,0\`Ɇ|a9/'Pm_AQy?ޚ.y\f-^SZ Ws=z򀡵8QFsf> -6W[=X&7D- r1uo -_G74INђo\1:xa^,O5Kljڑ^z&/_X}'3O?Xh<k ='>Zd"rDvYun]Ҧw1^GS2MVdd~z z2(tVI}\wXI7_#= 1;*,[>_q?TkIi4 5ߵK>v)TA$HrrW'SQI4 6E=h>DA\fW&VOcX0Ô6hZj -yFW#S@UxΣ3FUQ3~Өn00 abwr(ȏg=]#p5ܗoO[ϒnEa(ۭdi4_+_=LئJ@o6)mm P;,bWpb7F֕֌?oJքx*!k]{L_jߒٖ{tyn041F;k3Z͒-չ@t4o3>sNaӛ?6CY{Si]TlI -+[q> umuŘtAs &gsQÑ4;_Ľl*͌(.˔MzԈ"Lο[Zz0-,:}hѕD|+ Z(l:0˞\mZ1@lĮGD:ؗqzDʜW~AJ1e5<v[wjX{;wA|V+X!nv4 on)KD췠7Ѷ2#ZKrDa4@3TL=[V@b^IP/KM$ gwwS3jeuWi4ҙs!ү6VL=̊g{P(5OESûز]9Gu eoǩ⥜ȁfo'=_EFNkmݓנ۲`_C8_aOiOI= ?]wd(k 8nyʛ9l"o(\LC-; |s3Z)WYv_l-YH뭇O/ߘBqKSF:U -8"䴼DWc_ Q]13݊f#;g_cېv<] #8BBKCQ[׶[s1^ 3ӝDSL2/kK+`V^S_]~n3 7N p Aƍݮ?pGM}s6N{+ ֫E+;S[o'pR;9O?.澉Vt"aieO>W;#enM' ǑlJT`hCyozhXuž'ӌY+H4\`Va#okItz\M)dP|G@waYAcҲb/*mi -e'ʑVI]d^,BNd57x4[JD3RhuPYXb 1o9\?N^276gvlm.:>!q?=u@9o2P5=&Mw˭T.@wr_5 RwAt *VTǦ5?&F*_SHw#X#Id(#MjJfLi&b o^qrn/oa J1;pTg˵:jydu7m Kf2ab̥f! bFG ԣz^w/aI> =1>Jw^|*v뀢[ypJteɹ1k:t {aCb*PˆP;,t폞vr],:.f B8#ick'n"n[ŬM%n"߁ [ͯ|em.usz tƇT~pCXkL$v E3X\8FΑˏ(gc'zh10-Cyx.n(>BlK8͗zE3"cohZ;TGf`?$;^xWLO9k5*?<“dޑ}\E> -:>` !ll{!F -%.Ƒw=bpJ}i֮ -}5kƳkuWSF0~'aYO'7;Ӥ!UtfDǡ/qQ~O`{&9j4EL|Ev)JKҮ'B,՚ZNoQy43+"J_X ~X> V{$P~}MŎR^enUQ}|be!{& )xXKrJ&IVP2϶.wT^L;'Dp - ~" +< =bG_Bm#?^p2!d'ξ,\ȨLG -iY3k\þ>WFy-o Zf6?`x:Z|tI3φ'CRPS+"J>eԂ?)op0x)z3MµGxKM, "G -2fmB]4N+ r΢SBԎb[oδSs? ،<]kT>Q5cMVø ]{GVs2 a!X {ƥ =ewVF35Ë!Д6 f3Jp*,YYnyC|wsָ7-+ !4,*<IhQѠf]֜rIEdSޚIsFHoF;յf1[QVzY^ QӒ;JQ7eQzwE1\?T:t.1`bxn%fa "pHvhnoIo/N/.#[EZ]yĥljW+vF{OޜvmPYKUrn}:v?w#Ϡڸ,kTEXklVjU?-^Sӻ m|Nqݽ DkD*xi:1"u5b\Km=7XˡOZۇOIG=)Ew -*H˷1sZ|3nXKu~cu -h$wz.6*RzuZrug H YVVVh VI՛MZӕzL@QbacӘ -[!?_|7t23]>I`^\Q7wkGJʢ2ҋuWZ䐙=,"Y&Esz-5tX5eɝpiQW/nfdV,]WlH# -A`ۨU 1ɴX _2DURn즧$v׳_S?r,'/G0|(ݺھu= GlcqWuҸ'>2Ge!+Շ]b\=^?vcq}~e+T޼ +2@8/wŁT`9XytG)vTU# WBIMxr[]w;e1dT;v)p] ItV7(H? s}j.t9/>V]~훎DB8nX9ԵӮE/L:ΰ%OO#֔'ike~yiYk\7YEKK+ߦտkRگ_ +fᥛoyMAYv[]Ve,I#^mF>$35~u;:6!PEؤ'Vƛ^|T_'N9Q= - y/QƝ㰢ƥ%q#N9yKVawm~phdjkumwf!!R+W!/T?2uA}:YēG#ԗRO[KQS $>gh8e6x[t,{cd -ER-c!Y J[OIl,a2=3Xոnbli0 \_lc湙{/FN02&{b_ ]>o0Wo<o}$qu I<%a!Pfm:-{>#wj˥a &N )àm>IU7_t6 8@.]1Q/eBИ5Im`;.fK_ @gIK ,fax]DhAdXso?NDQl|T~,rqa. ^-=˕s;/1GolviBĢ[lmM{{9OGoPt+Od N - xR&^M -\eVS {zd&fH۝~HrWNr{WnSB+jANt.7(3_*$}mf^~dKijĬ}^gӣQ)gc{80 غo0fĹU`ϲW@^H$Trz'b.U5^OʤLSΦ6®O0.N:cdXǏnЯ fP7ZXWER "rXQEmMֲ0 -"~aYշ>]^ /q*+ﺡdf?Gd){i yv:Bɦ*iȄQ̷ǦJ55-~=쁪jٗFI*k[,]*:Z_@w%g3$`/xK7=Z,KQtcst>g_BV=)a>:)M"MN6YF/ lӣ-25t3TS%/.ᤷZ#Y!޴}p :AxrzBc{a$\޷lF vhqQ0f3n3eg$Jx_MY{fSFJ i#bwdƧxAtIdE*$,Ѿ cs~@7u8-y \ڍ/DX8x?'r9Sef=/Zu}~o -uQ~ʕk[/?H˺XW -J]IFnl̐jL޽&Y.ְ(I7gOR4 킷)I20 glvO2rNxU^dbDHq*vK©AڳJIX617@ZlQn᳢xL -\ΤG݇o͗biR'ªqy|j(-I‘0yeGet)EdYQc/`O;\ +%%F[ jz7ʲ?Qt%U۽y,!kQ ;OFŋcܡ -i܈ៜqJ)3?b2)fUњ<);m>WC+ D۸x0-iG {gOp!reGf?$FLwk?8J=S+M!Cō-Or0AL I ƈ`OASZ$ZU);(!YFlLGn%BDP(HX*Aد5&<\U @׀#a|Rts̉3NϦZxDo;50։6{J0t XVrr3sa)ȔOɬZ{/\ 1TK|ҁ-ȡo~>.:h2ƞcc~r9Ya3ղ}mg@F::ҕaͯ3L)oH,e6HŔ*z]\-*3;NQ N)V3 c꛲K|yV_Ik5SL0oj]n+&j5YUQ@1zO(j_VJa0p~Ե;[VP9R,_~WT.+F 三l#1G;Z_δ6s*U#Xj򻫄-xW -*IyvtTOR5"*[RR'7 e>F/RW3ߑP:99\:%7 辦<V{akc-h՗{QpJ۷iu% -`>QP,CHkd,e]o}l}t bgǧܮ<΁,Y7 BV?*OIAl7Yk{ Y1m9Qm恆µ-6eM[cfl4Xe`{ 12*FȪjUd稻}%3-cU4QֹpCnTEֲVfadGNwq!WYd=sUg|F73/`Z.vG#DmQY}CkGi]<knu<'h@x?QQ/U$.͗vi Yh H,SxTKC_ǝ(1v8"ɤvj`S\H['9ve0+p5v೤$s\p$zsBئ-V^E+9_ +ֱhE1e*cڏcՊARa)B3'з /K }"Urt1N 7Sc+ayޑ+Y+߁\A(GF&ړ_Ñ+-»\fHNfE^u'E3 G=eksvEKE~d>sOtsGܹs3;|tAb4j^e=^ ÿ=IW7 4*v@Hr<࢘wmp65 JP0}%ov{ 9]';^Wm\2UoU%L@Jv$05y&#=\yOO]3]"]^މQӎ}!z/d?;^\ljy|y|Y}=|͜XOݵ;t2m4jZyE;pu -B1ioOc&_VUUݧs+:zn=9uOn>Iҥّ7 -1lVzs>A qP.҅ t5t9u EVP/C - Mmm𙏜׫lwy39jck<~"H/z{YrIDRLTnĽ69^:Ȼ:[)fEg DP%"Y |yvSbtz.w֝eCO -`Pvx\A-YW=XZm‘4y횎%7J=]J" "oB*,Ђæx( - -p vU d-~\;p2w\B;HJ { LZm5yŎpHђk(]B{B -v?Y[G}wǣv waS=]{pZO}AR5thaΦBak[1 ȆX̼+ R\er >~B!uz/[M8KV_փcf78nJ*w\Rw3[`fԶ PS 8m@R8@8k~.|K&J_[J H׺i΢nR#xLZWzUꏳרhDeFB*MTSH<]r1%Q]E:>sB%~w$@岙p4NgDѣ jѤI H`tZ}R -vٴwPqqawm܅[:%ǿڕy!Q},FJ0B@7AQZġ͕s><@9FT#-@@~v84'VQ~W+3:JaN_+ 3N%TC;WFAYnj & -%AJyDX9&Ӥ5>p}tpMU;,bFM#.yg^ rϙf| N9ڎu^Z _=:۶ -t\ktq4[4˫TZ$1%2cԟ5y:CFMk削k(ڵ D6?以2 W _Kު 9qV#揶xOnR-ёƣFC^MJܙ"QLѳ;r4 D_"f25kwluk -.xc'i -!! -\Zlj~ -?$ti b.dVNirUX$Fܻq,Jpcr|JOil<8۱)"VT%[r,du;ЁQY,F≽OxlFYɜ,;,ɁW8zFPb2y儌8Qbxw=u^m ғY=~{gנּ`*;/mv }Y)t[c R - E֪yPiydnc;]~jri;b]c2IdL`-a}cYrw1GJ]hL S\\c@ܐẴr c舫U\QJgŅZc&$E5|ղjqV)Ҕ.v0^*tɦ;=oNivN_IK9J;(jOhimtZ[l*ÜSM>`߳HLE)W4x%\*V`OcNK$0ۢ|k'gA l򤝷.ƊޥZ=QoSK们 -C԰)g9-tA -,ZQ83LRό*1oB[Э#kr%a蕔/JC~a:r,eu%ko=Nso͸Jǂh-k$F/QV=s.x꽆sSw~]}wRoI_ mwks#v&<_ a΃g<*NWP,Yr0)vъ{᪽4޷JCYЊmMgGX(|IW!XIďyVBլ#0j # gSM2׶S  ʋrwїױ ճRm޽,hZp9ɼ2s)x*fѠ锬dVcZH͞Ͻl1W▻=:W 7VBg ,iWgX,&qN~:p/DUJ /M;aZs*;sxu9krP(>a-wƱ)ZSDJP w#|2Aa2'^棫GOPuMnA|{/ yy2oWq; ΐ yksWu6}+f_nDjMԛۅh.ݛo:@|#+(ơ~WMFm돱R02SG˩O|'R VTx<_W,g^pwNܯtDYI٥Uw8W[L?WkOĖ2$&*:Ƕ͘Fwնm{93ٜ@Ep4fo|l(tܭWz/*VX,y ƨVSEmBuߵ-aW OAb%ѣTRʤ=ir.Wf/'ȏ4E ǤOYg~/?^&{c@;r g2r]= *8]KNgfol -貏sI(PcLYY4d -ߍ#gVbŚ1TFh m]%v26 q9-w+1K/% # *ӧz|# 1W%Pg]KE0 -~2k(Kw -UгU;xvwV=˰_aWNý~~8ՙhh4sC|ї?,qˁhO1J?U8^YĐ́Q1[y$súZҽvYeQ};O'KA훽]spc:SpGCÃgQeM!?0E~71_pV";<~_?fEآtkyf=+LRNy3Y+8+Kˠ9q'jV" Zn*r\)T!]JtM# /B{՚*5j-'d)_?o=tȆ1Z ݸCQ><͞ʋ,kPn[e;Nը(MfB6VS.q0- *9ݦ0qx7N~ay#['G _ -w`aTeکS%$U^Co -y- []t*3㹰46o{ֺKeA ٯFFJvYd&.7=ej%In/6[%ӣCd(RX&ױ$Q:((t -5J{gr56} TXoR.Ip^䧉N&SV6 tkoΠzSrP!NUvʸYoD9ҙwZ.c-Bm]{.O6wI?~0 Z K3|twH3^Te6q6Ad("X&mx'{o"Gg=r@* K*=+jfMweVᴙbVYX;cgw/4,77̋?<zT7-_Wiyyt:%!4A-BR~(9+niVswZd F>ŕe?~l?vMV/y*^r"A{ oE'iSݬ$<4r|P%߆jXpgΡUieu4_fFA)NUM η2$:HHVđj#O!+B"VX -@X?Qv Vg3ONWu1t >\O. ?㌀|f-mT$O4W9Ls" VFcz80#+<(=+Ktz Ll:)3YifEiXr'Hx^B)cZ~Ե}xs%@" s(A>~nF>_8us8r߂+2I 8d%Vfsw d:#V,eֹhϾ}RÒ-tnK&佰z=-7`)Wpxq5#*fZy.hI9W"ڣJ \?f oqwbRw} AGi$TCqSl/NJp",lG7p錬TS˙Bܟi:C!! Ez?҂$"ѫDI(([S0N -te)V Ph2=c\ -vzǟ0@[eI4Gx)f^w -Rki1Ufh˭dj(ʏ|o8w~:uĭ\t|ȖQ~2-O,&[+ ѵ0֝Hq۸*`ms|.v)ݸU)Z*#~Zo#9:,M2J.pVCz+==!Ҧ$'.A'^kad眳M9ɾ߽{f7t?}PJ*Jxՙ\m?%tWSak߇YQm Ib\W_d7^xg-^8tAGNw'RjZ^Y!kqͶ7OWT^%WoO]ЇUZrO-QAyqsq!G%M(Ci2̔XI1F#-~ -Il::ibGJ2SɭE6(CT%]aG*O"{PvT cp5E;< -{s V~ 1F4s%D'@:ZAI%jLDokOJbU>=0%'t~:9 ol-vJ!)92jJ՗UH1PdKK@?8ӡPmakaqtf0j7^+)$Xj~I=6q(52aoZ- a `J,sfHzk -51DgHՔe7Թ-r7#5܏wֽݫ"1! mū_e衼 -7*R$y^Z"RJ:ٱsP (˽'SUif@SAU\[1ѯJ-S=$[S˙hm+%Rh2bVsCOסAќc]us:~M?x?>OoI˾2>|#~1i.*@g=;1Lj5B_M˟ Gg qs~octے4R$J}-}R{>AdQa-TL*̰Ҝv+&T$MrMh$C09dؘ *TQ+] zf=uy~6|izI~z UUq J}Dlvh4y,b0ԏhCYiې)cb#hhg dǹUiXnxrg_o#WJiX_Gy؆wIA;|XZ-vN|Fl4rp̓lvxk8MUjw%5aTP2:#bƵwk`^M.\Y+28/m˔'Gjf}REfPemuŚ!C]> 6 z*- t^Ves/ 4ΞX=)O˟Vſ i׽y>!u!Sgզ`#[\4 Z>'~&ႊ&) mH=Dht.7;'ro}WAx?Vr(&dݽF N$w -kWj\~~򷋕Zɿs/C# ABp]Q¢8כkeT2cH}6M'X6NꎢhM>:Iqz4i2b;KqEis k] -P㉤C0\3~S#*Chi58EѦhF_cz$zMd -'#+o.8zdWxu+@zl<-EZ+X@*iiʳCV -UZ -!8agp'rvaneX+ҕ1N -*-%JֆݱOe*V^Ƶ=_$~`jr})ns}8-P.d*|:C?>veK9q~5ZnН7t ׺ 0:zD Lg|kyRvr9ֺ zsFZFzAy)g;zR -Ę?'E4.Ԭc1f *1biV%o^)>ƀzt1q6ZRBܕ<|ZoڹXwE&'E)Ow_b̤9pWwizJwh^z5dQ,\3~tΕm^@ro!QQ+KKC5Q#gM{NevJs>WCM?Etp#B;ry'o`(Hs4JmߘHsX}~[WrYeԼV'P$oM֞#lI'wz8IpuvF+1oVksԫ^zC31\aq -NB?;?3V<*ܵٱu̅8ծ|+6Ao68)SiJt k,W ߠ481GrUOQ>$+q"//QXzf)hm]>JbQv|kO -9>{ghf߅FD842^Ȭqt%ճ&N6vJE;+ЕFxK_Jl(c¾$Y&@㶺< u\IߴJC7 Y4fPao =Bjir+{êHTϯżP1Fu^OÖ?1Q݀^P6֐$^n#e!65WVsNFY[K7P5kgnvii&nECU; <-hsʮUͷ|t=m+s0.)( pwݦC;-ȼ5>NMq.BuK&<:lJL|ώ <!Rp({c -U.sp Ǘ^+}==Zg7-piZի2ς~x38HrS0\"l ?)˧"y -c<1ZK~A.޹KБd9'$5'Fύ5|2JUy25 8Z= -vW"ZX|wqdevtSR7-iX_g*ӝğ:%;C׭8ldmJv as9n)(,=X5p)mi[( -br Ԥ<|irgV4=􎆽Yh. i^4Q6$~ [c@w^i1$2؆AKc>cp~Iۗ^6-?vbV8-$Dq:K -J݌ϧ"gWvP鱊&UZ2zUr+FY—cH)u $ׄx?oc! #ND|)n"`UqP¢\~z7-ҿgarlb Y㮋/%#Zbi%- >-\اM(A$ߤ*%5M>+܃:24 4WbT(1bzyatisȚC$)%lzpF>'nM5Ǵb+ZoWUV^T'7Uq0ۖc;Gt1!ҖHOB!Pʠ]\6xa̮ȕ9ߩxzZ|(vOaJWmV+_x_>ц"{)|F|/YbW'MJSW`vG -gɎWcEj.7'/ھĺ4 cmk솇Ĭ?eZ3P`Lwڥ O+ddͯլ/j8Mpم :Uk8UvS1 U4߼4d~V(1 iDIƠV:vA6ܐ?ࠠ(C ڝq5|' -OpEeT-`fQ`5ߣ0l~M)'?Tג<} 9؞ -|P݄DUKDEhmW͇Į0h63% *?w.WHfYħv^/,H\|8 =TOsrG2us5E5neiVr%^Pǹf?O~r4 FgH`k$>Zïi_w*W1UA/ e5{s&W3B"Dц6W͹ k,lOt%Ǩ¿kb[۳?J{fm+ *q 33oVWq~ -`5feT6lF.g;pY1J2?N?"ӿM`m,=xqs_PT$Fd9pAU# LҊjXNT)lc?-QdHwSV]9FO®z,*+K?%8FeN,+ф0S |P% UM!twIhɇLe^(?f@9b*=}69iaqwa{U{o$*pz"S j\lla@+Zn0f:O$uon~ttoPQ8`$农懅moVCk+*[,ĵ~n>: 8%{Yǟ6ksj1̳iamڋ .f_fuz9m-~1}?9q`Uo('ƇG!p(vg?Y[WXwf+Kf$,LN9#kUW6çP)Bg=5;ש{WQ.0 b/"GLksD!'u wQ3 ̾IǓMaߟoܮCl/x<^W -?pX/PQ -Cv\(r.ᯁˇ`D d&9K|`S#ѨsJDUtbT5 ߱lމ@KO&R7d |Ȩwd /N7>{SGY~|g&5j?HdLzYyqu8' e,ph}g˺MËVU%ԑV+m +rshs=$ȗd޿ar={I9⃺%ιqvײ=TL3$"&Ts<ލ Ʌ,#J8٤Weg~Ovk?x3%sRgQ3ߠݩ8Dɾ*~o*dݽf/Ε{8[gK&1yAP/O;Ȫ kmXq|F8ew9Aq׊ēi2ӜA`n8E~r7Ew_G>13J2<ᐟgCL`\\JE _n- ײ}{+JK<}% Lώޫ؆aCmw3Fq -c}V+~6gM1l=5F/3/R 8"mX=vX'csέpzjѭe@owEKgÂufJ4q%s?X-r[Kwץt؂m/@4O6 *Mj&Ohڹ1arR^V݁ln5jjueҁUlo3) Ib =agNqFY)-\/ >ۃAmZip -ջMXC}4ARk5RhɆ2-sT,,\atyr*s=zKI1lђHW W~N H{.eN6iif!nJz"&Xl|w4+2*; endstream endobj 383 0 obj <>stream -LnPwq Uk-?oAIB’Eh'{I d"扷e^;O2{2~|xz{?GKn&WgN? 96 lYQmʯ5ʼn&;-tw@w7KvOR^NMgn.~{zוd67w6{WjS8T龨C~^Sx>$ť9IyJɂ\0r=v{T|T>i߯C/u\ۚc]?/~"m.N/Dm ըɄlO&N_]{1Эa~w,{VQS^]zՓS69?=mXז iF4 JJW+Z@,UTV`B wKn0>a^ ND+xRK.9ǻ狲_*_zK}Ls`@'"igfɔ,>(X~ײ[ÞݡC\39qU+g+qQًrwEo pB΅CШrhLJX$|X}/ܻFΚnXIT\ES4fZg>[Tm. Ģ~Njn343 nk2z |OZ+P!{WXVH#̿.G5ݭf!}kgoɭ_Ѷ:++X[61_ΫnzhZu=B m>?uL\! zH+wNfEQ6!3EEkg Jg;^41\L2D7 *-c4oSDSM*npCp}:{[۔Ѫ}twYٝ{3s"8mpFZNbj} v*-m:}1[Bm%r\LT {czZeo;'?O~ߦ_> 307z>m*O!/"'eofR'RyY.Im7>";N*x|9oHTmQ Ѿ,9(eMW;#-7o<ekuÈoPfq{4p7gۤ^Yz|.6o~ -U4NsGq>1:9*#F,{TT SƼ"%Q׹-1܊QeH9n냒.Ppk'e[]pnmE,9#^l"!$c): ->,bFar1u]ALQ[O? UzyRKڸiЛt3ëiwj,dzT+2-P%ѵvm ko<~Ba Knn R'qv'0_٬|K4W!1g#/Kؽ  "w<[梄dhM7lefWu}ŜYNN#+>WS:!$4YaZ;LJ, É -3i=aׄH3AP_\|hPu&m4KuGwΔל'vFnrSx\HF&0yXdf'jڝs-f -]KKiǚ];}Z0y Z-ϝ1řw,fc"뼋 YgIfozWAxmo.񁫨sѣT-Oo`]y`N]?>^`gbf+U" -Fz 3-.h޼)g_rZ>D;h I؅C{*sj?մoƲm8Ux96e px$r^ơxo1N@8J[mylʅ+ɲ?mJF{nxyGm)˵JVȚ[+)h%G.8%f{I -G4]>$۰WD#Mr3qzf=ڣqp5付l.v.P %4^THs4oK*Usڄfv{R awLuW'LSANz9Pr3!Up쒇7[mܒ&r4w{+j5lBWkzghԛy`P9T059qRs].ͼs_#Ma]O>4#_=oӒ4%6I!b{u\r&FQxQ)$~WsKxpoӅ|pJd 1O'' 9\M ϷFx -A g2%wgIӝr;Fb\}Ye7nzCcfaHx0sS@e?4z.aL sL|ƌ>:܍6K&ADRF;"PW=o^A$#N,v=X ht^L9^Cqjhoc1Rri0tzW$Pޱ]x&fS)Εx&79% ͷSAf?︝<<˛f^lsކBUE%FJc>0*PW^Pj#4)[̤ 7!&zɗDu<B+jBqa;7;<ឱ ŮmK-\8uz*[ ,CSԕ9gdЯ~39'òܚInCx-;xVa8V ,;i$fK^C Y7[oޢ cSCPhͧim 2bg+H LKk FF\BwV3 rC1 #=xJy4vZF=URx -J.Ti#4M$az ${N58iB~@e;&KZ뭆y+/##[2jc?VrN~ASIq]?Y,7vxQc%n߯X/ѫa7)!}b`0Q;=ٷFMKpQk y3(gM6Ao渋,q -^4~5-!Q+O9pj>*ˮb!AFF57Qv8g# *D:.6]iݫPߵrk(Y=B'^auL ƥ^=Z31Q2V]kz6"cMPem˼r=*4JS &guJgpdtpm݉9D{v؛ҴC?NV)x)`AIs]4a~NcrLwu^+Mxs 1 !lĹS Ⱥk͹oᗅdDG-PeߣLpuxu^E z k4.]xWWcyrP-=)}Xێ4;Pm[Rtn9CxŪ!)k|\" r/=bd܂\TGk\V,fgS򓍧͒Us3$ܗI =rKG TjLFR`9iɂC-^Fk[r,w :ge3X`cHO>3GClH_6{_Ke`2}& og>~G‚5jy0Ƃ!F>vUń[a5͘o q rZg|QQĴ:ȢF-ؕ܄re)u㼖E"""TչĩbF 5g?%ܗ^0zl(|POe}arXj}uhz^乲| -#rA/c?t|ti - L}jAmUF\ -9YM~ϱzg:iD ZO:(unO[g*nlxU5S8$Uتf>Ap^ C x(,߃ŮU>guԧ&w =f?nx]x0:V n*wr;Sr$Uq-n}*],w57sR -odUMKRVnFy- -l>c| ~ѯsAj[V }b+ӵrqT WE.Djᘈlsh+.w9;dr3=|2D֪_*8y7ގCKWp׶ޙsWTD h1iϚ]Hs] 5VEW[f,Y'Z !*Uv_ R -v 7^9 tsmd*I>|*>0C^M6>O6Wtx50{GKqC6oFp$:Xn?}C4U+娦$.FTk9n;ȴ恮h:a6&лmߨIAٱ >/Jҍ.">.!sh,!Ge.hmƶMdA}qaWM!3:6Mmz󽝳sEg*E"*a1t؅  X8EpY".kICR~ -MjsT. 2wn`Z5zD2M2^8Qij -#Wvӓ TTچK0" Ryw }0C?(V=;[=>Ial_~b,\boR҄&a. 6zd -Gzaj5٭9o7F/u(>??h, z.Qq@,bBK]7A)( #oWEJ/+g޽Bŵ^zaw]T}sI*jb7]^/ `22Gs, ,x`ڒ|±7'(]vZ[fq=7`_)H0'5Тe.\.Jx.Z+TTE4x4-K!`G|\s9+'D{M}0cw5UWufr,趮N*\צA3nhWD{sG5ᶈFv&TlZe[yLlϮܟ/V92fl׿o +֪]{AO,9ʢ"r:7^W7UOt9VCgna5kG?;_Q!xD^Bj!{վR{>9_s .zW:Ph~~;.o .'`Sފ .Lyt m @mZ]5.5c=.ѡbYnir۷,;Ls9w ۹:Ґn+d7+Jr+cw;pzYk|kM!fEgr#pDMӃ\t#0e>dwX50s]9w&V/ vwV[-}DZE]D=W%AHG8r>gxy\bNXlƔ͒RTДA髰(&gTpY@6W.S$xH*w]J4P zp7'Zy[$z|\!o.4=2,_jŋޢsk$cF_x 8N(|}0;!YJWW3Ƒ&݄?MI{:S<ݮ볠r#[p횻;ϑi'C^SKIۼ}B=jH1Z5#cHS&I'j|7Fݵ9+NzS&hA0&NvWt)ApXq}0|ݼ ͊]R.]J[3W_^/p}ڢ&-1} G|{k[ 4ޢCXC?=e4ͅsx [,mFSwS6̉Yʤݱ`4t[]Ǫ/#8G -qw?cU;~viԗ&x(Q0;/jfv)*q!wp~ՁJwc+7OzM-;2WA>{Bf0Mx.XUrT1knuf#o4ЗCv .e6Q^. /J,KPNUڊ ֜rhݙ:w=ū[khwφ_/(՛1ڭv^U6N}˧OРdxԭB`;9+'j6i;R/rr:rbn1C 2pB{'a1VB\q-& G[ܗOeN> 3V]irKh7YfG5%,j6 k؟\fM+fZ8P[;p=nB?,i'/Jjr~5e^ C(&jKsCVE3E#`x/|EtZBsOLk\1o+jBԑ`%XatLt"=\[gCsLkzB cP{Ft,KLi_mi"N7OBJYL!!J9qDָJ]vrkɪ=^=m機􊝮߅iQIo(D0p0ySvvٜH 3P οn?5U8Q.F ff,0[6p](i؅BUqznR}SVQjZ8/EE>׾wxKbN,7,TDR &N7.>Ȩ~'kzcY4E6*IS}>zdӹ< J`HPʦCoT4ޛmd v^揿:%-=2,[s zȺ8M),=Yv;@t'sra佪 jin1oF/%&F01z"Vob! YðyMGVSp6AS^j}F֒;%~ _XJ]:&*'/( ECԵu-斚A0sqˉӳpzdD8w崋ZeǓ}Uc$Yu+i hCh,lvy& {RRVHt#b_`wWSNߒs]U'\NK0Mm:]II"֧NuwomY潇~fV>ͤ'sҨ+i!1o4Yn(T5ak؏Kc2ǟ{c*#ҴF{:5(qvL 0Ji]l+?`Y<4IBfѠ^[/;aAl6sGuGÖc8Fjsvof;1tۄWޣqmF?cV-SieV,HL Ƚ.#Z|i YzjDd_o!O#bJӧ,;]>eQf7R.d y`g{XEN@v o~c`Ɛ3Uq|DfP^jbmg#u?U-&\MzoԣؗvfBds -E'-E YT1n+vE+ !B2Nl\v3LvqҭU}fapy -_YK -:֮Amg[y&ذl3C|K0 -% '!EaE7|pұ-VʞҦ?,w+CZ#00Vvplӻ -3j 6i.ɻbu ӷt-IVFzf;>;V7ds Sӱ2'[k|ohҚ"NpMbCdͩ(5t11CHܒ4|{@?/<ᲊ`{ZǮ5#o8oF)kݩ Df% 3AXhVd1:M =C/a0hOaD_hYEVP.;ВL2rSnB޸)nv"Xi=jg:8`mMnr$s򰴍~evl:7=45~0yMaS"F+o4 ה-e6BSY'Tkʴ4ƇDmjMg zm =jL>Jn̒GCW/z]tm7,/UjHB]nM+ixM]?~T2T!SR7;+mHI]rJT69۳>W}_jt:i>v<}C eV'.GdǧWbӺFKA2[BYs:qsBz&w5Qѽyo@PkI)$>\1\P8ɰ/K/ >z3|#' -P\_?P]d =L kDٷXormnatH!6h fbR}L -M yNa]y*ܲ뛊%L׍',.vҰ{kj{\4^sSɸ(ɵ6:S| m#orBGv)նQ@I5(Wvhb25 8lt^2@7;M[gA~oCk6Ĩ4Ǯ~y7ּ_wCwٹ^ -1j ozRn°>ɡFouJխ'zCxSZ7yOɽHo>ƌY}WWK qwҬkokH!뙑Way 0Q|buokO߂l˫VJ)VX;%Y {W^S$i4e ZZ,Iu]jMeK3Vĝ FAcXUNkzP -QIG: 5٭vlڹ;bu] 1F[y譹|^+KC7uܨ7Z4LaE1X# =gMؼ/\+ Ѣao*c_ٙ5o1#grDfYN]!2|f7Տխn܉%o:p";BZX$h[ o?^eڜ&V7M'XʰR?qyƭӓeW[AVhʫ|6~׻[E -t}VvEkdIn+YT;ciKR_kq>D9#5atVK5 -uDՃGPyT ;|zrd/ Y#l}ݢ1ʟ]44i,;msXk'dvta -q1lދ,6V`ÝNCg]%:Dn2{](b_fy^-B# s.ռ|:6:;q);0~[1ԬqZ wv3Yq}BR;$ni8;gr/+4إ6AE4sFSeų&v;Q6@sb\lA؅Bp,wwWJjK2Awnx܇9_$m:|+WG&F֒:MgMpbpm[Rx+ʋ ܷ0@+B'm4wd5;WE_pIv;p/eR5ּ u}KHvWQ.~ZHd]V(,6;W{tܥߛ,HɊRkYM;?Bߢ`]S7 8oD?oC^'rewNuÂrNEL{WEΥ>T knfcTb3TgWS Rk ۨO=1pyW] N=_GRyN_1MrDfytƭofSMص[jDu HzȊ#g]k$jdrt,[|G‰Q:6&Xhfsk%miQ'\g7Ҝ8/م aS8%tqZG3FlvA׼&cpBy`W> -PIzXr9q[Bbϥ2p%9]$}0^fC5lvhv2JQ - -Y7u,<ӗR05oԇp[o-jXJHY;=}A\L}ڣT-pOc $=>6š -?ap +tښ:usgv?x1(XQk;#GE]gSZq(ďVގ܎UF{?L`3{ +c9أp\V凤ڂzrMo78\V;-]5YjclnF9!:"xoe1}j9\# Zi<(p[NZ}Vd Hsb"' 36,ZwJ,҅C!TX8a>OT9?Kf@?0FokmOftDuMXt Sa5Dm$o g>#D'ۖT{uuKل fy~jUY,F{>x>4$&J%vw5GT1 YT;rbV%ۛ,P'o=WҲZaƩ~z"~gQ3}eWa(F4zXgUN.Z$;>Fِ+y"]JtetY?(4 -8wGӪbmW [:Ez:YO1܄K˯UA%Hgl7a<#Qr< rxN%bOWF uV:ȤZ G{(luSb{aBkX=(Np@drRఠxbD}]O*8ϯ~. 8LjZ/1R-앛9]GKZ\k -E 3,BGkd={5m{m-7?${ kE(U9P{g e[q_zWx^&..DnLA>Ț(їNT;Kϖ鞪Rt3}q$0p< we|8 4k"6v@}8Ш3 )nѵPk Vkj}Ks97jihFm)¬Ak*<D&mISanF{i_'@&AkFť <"yw3!q̼]P)B;Ss!QXND&\(ޕy55B((\Ցmwb7iLE~)[*̙4KQ"aQy.(S%~{#ӹDm]L -I{\҆E7m8} -Byy\9K?.HgkZn#ƾcK\_|FcMf7xAN-4JgP*#oQ@4{ "nhrYNoA'`ރ<9ɖt7׿P)E(|ִ:fȜ$Jc2~bBLi{CQaej0FZZPoӾX;M,-Z`WO=/LŠԠx,fɮf^ҘR{ބƗҊ֦lB%-nIpe9m.NeEU"xy5 -eecv!C51^}*=YUtTw{' ~5f`!$orp4,z39`* CO"$ :>:^: (f9YyNI og(ø!f[Uv{3@wde|_wkMO17'NgTОYJ-yZ__}AqOc`VQy\Nc|pOgtTQ4ڃŸ X 5w/&x)|zc> MaqU!c?:Jk%~!RWqn`ܛҗck+A&#lʼ)֑fޝ􎋉mϝ'Oٲeo*6-K+9ɟ_ɦ6a Jnw%ei:U -MYL^K.(QEnOXly Vn~U*g_Y"si{6ʵ\{BM0)zNʊfy -gaOouW?+=TUu/kʥgٕ2nlcAJHPYv#(a?<?< nX= [2;JXcgUMkF$C(35Dl֝pntJ$kda]> -~Ej텂+qsw -n}E&}LѮY֩`~Wa5KnI(bȧStg'{a< OD}ZGF;GBӛKv3yAlɒO_Pr #}b"VH!ޑua4kYs 49Ki1Ae-B>KO CV:Ê2kGZ]+mzQ7Toj9R KJz. pKRqRH7 isZi2fؽf -hy>Noyx'?VDolvy&]슃R_tGH&Pϝ+X,Ud}_hc߼cTyMfP0bwmtFM#)#l'żݿvZYǁ,4O{}"L4[-3FeHWeϖ:rw3 RWqi[9Z1pO=@yZ ;GȭukZSg g[Sᡊzu&v$\bVʯ -oAM\MP "Z.2FV$'-(x~.)yR(vSp:4՘mW^&9'˵\%;Ufէ!JK+~A2:TK1d:Q;3ۓzTǷ%}RP]A^-;U|=.uef!qRӣW3K~i0@8:Xi}MڙPL< F sB ^_VGޓ b:v.gBsiQ%z']kgL@1-7qn*7ju)ݡ4\$I:XI7S _L\qDUU=|onz2UOIXM XH*/4xQO,(b2֤y=֢ɼyPT1B"T>VW 9]EيT/C{VteW ^lػ'OjN^m=V\.*pb]%]?Rh]]S%d~ԡΉ4JGlWTnO(9d?.w @?h"}t-6ecz1>)OFr܇F$;FU`nN$6'ſҍѐ`IT™Dh4K#Z 4@v ~Dw-t[X7Ql_ -?_isgqg $ӂ@,>IP}E[?UTrZkŒߜ:Zcj~Mh#@q%HeTp>W,RӋ糫֚Ks2 -}AjMv j^mXxB5T?bkџ - WKLthjՈ`F) D:nNllu8a'!_&U"Kʵv }Vjx>|i4۱$S*bg2C)6ʔ݀x(%UVWct/sOmן`}WvJEGpQsP9?>e]ζj;ѱY( 8?[q$ :=c8z~K&`CGNL;7PȀzlygֲ ^JL2RuNLGeYҡn_7# -p |sxsn@ܧRXqj==]9@? 1KZ?z7|I(7kBƊྫ?8Yy -A"#K+BcqjfҞ11 4rE\RôTMڻgBFj2)sS(oFkaҪ#UxtSO3L;)1hV\Owן|;(.Ѻu^e+Q,Ν --a%Ye5 dF& HkH+qy͒KX&* -UjL*xwi}l.'} v֨R7Y?T€mZZl'Ab$ț_$[E3"= -LƮsX#JDM[UJ>ʎ7ub-̽ c-̌Sr? -qjۻlAYo 87ܭ,ڂ=߇e<ݩu:P!{Q\A{_ۦwmr:v_*y; -S{L!w/t{qnoqc_,Csq>(zXS<jSzu\rɭ)V,r3tlqn9b -r"x\y (Q|,!.gPyNg|vo]IJˑitZQ}pݢ|!M;mBao}!mlV2aEe.Pe`<4 -%t -t_ -\8? R(%Ohw7C^UD쮗Lnj5Fk =ol?og3Ax5ݘ洣u49a/;X=*ڭpcޮR|/Q6//5Y3o齟/rčXaV?yNAJʮnƛ{mA^G~u}q/3ˮa饵FB 3KtMg?rC/CT%'^U'}αW*lub{ѵ-B7)5. c;R徏Oys[6K˻1xA:ndPnڭ2?(hoXv~?;o ҙm">@i4K;j^ZiP">CC -@Cm8_G[\ת@z4"o>q>(ӽE7t n^~HdmLKO\ޭ0)g9'Wgb0_Q-)yo;=(D$9{-T~RR&[Yh;K?.}tROv&ћQ5!*ZCV}Gxcl_}?@v:ho/[bbu""WXб!E ew&\ = U'KEb|r9kq -/WmD<^PӬneiR>rӿM͌kKSߩ]F.FhfٕvFh4Y-& -7&Q12b$rf%-l0#m:-8h7۟_.㘳+>ue;gcvm:ĭWȃ1#9Iyy]ן5;oblS"d/*OP52c/-a9|GNv|uAỏɳXrٚnhH8,rJ%ܽAVo#L-w߲C&D(,كQ@?@m ѯq -U~׵=T%.0 I -^jiЎ rHOEBy'N# -ʹ3}7NWz?wLtb$)Ih*I[a^:b4[x[N%YQ:4awC; vJɠ -=P`ƛ6_0W.00<ǚ)QuWe +Kub=m7v/qbgy27kݎJ)qh~#/{Z.<"0 ({{ϕxڝ(:[IfO?9 U -˭X.XsprT-"=CA췝zӢF69@n.e"][hΖ Bs't讻<'/GKuOW\]ۑ++0cw?,>Xh`0O,gxlFEE/΂kT,7%fYؓw*ugn ?h0ͥLߝS GByUV=ޭjX1f&2PG à8崺]NpF9b7)a} iWoc^7븏nǺJUִ%GtUέ#fEvnFJRBu oԭNx 5yGzS<0URQu\G -:Oq PhLv]KiH>QPJ3\e@FKtCuBGGZ5#)2ڠ{Rz5CMs>9xV#anT}BrXyet9#<;5OЏ1>6*aOnOaY|}#t:l1yGUJuJ_& :vMcYE|5^_U܅YZ)OgM]ڭpMe=#mOȡ>]{RX{2SSlpz6C߷[kA쯲j"d@*b.yjzeBR(Y۝ eYU=~EAs-X\TqLhs L*'jwHZu"׊藝l`h~O!go~庳P7>lsѣXuq΍@EV-{"ECGs kϰptzlG$A50: nxӏ6@!nafh-o -2}P7QdeȲDE˸[*Ncv.H~.i|+jV^0SI;ϟG,/G‡&%<ٛ?`Vhk]Ќ#T[$\߅r1yñ^tSsxNty9t,˭{e3T3W56!Y2ǩ"Nyd0g0PW.q-|LZ^ -3Ta 3X!\7(~?>WwF?i,Jh'|nP{Zm1=[/ .}z4̶>&f}_v+pGB1fǔZNʚ˪剝I1coۅr>rVeej5g avc#zx1gr_^'ߙ͌1xFi_ǾQ˛G$x׸[ 3,\2:J,1AQa:+VmJNS*@[>C>k%5MGc n5VN[.FΣtȰ)P mXXGjCm~|hw${UEgiKW1{3&yb1Y^J_n_{"1:Ml3u\kW Yi(7bWsAKj^7'*(ߎ[elբ!bKawjn7k^'iv9o/p/^][kr9خXu2hۈ¸p*ßӦ%R)cߥ~W7y;]ͪ47ɻ5^l;{X+Xo.@L^hL~eUJ~ב|O'?k^ 6ζr8K?MUBCo޶ΕoYՙEޥr9 N_BhG6@kT8a Bnܜ/\)AnkxٺU*ಶhq㡨x= -KiᵿF6 =c g/ 6}d6] ^UݽS ԣ|#hdv!:S(X7c?llxv\5$tzϊښ8UZKNyv~M&.ӟbmXn7ag h`ްRsjb6?[ 6?8[.C2CUV#]NsR" `̳[L z<6;]:Ũvn׺{}N~t[W&'8^w|/x f.A\Yq٭?+m4-wx?>oWd5Ȏgºq 2mTWbSyGD";<{&&L%)H+~I`~YhlJ|h~WѤ TAުjFULsZ?m6M}$eNsu~0Fk;?M0i/ѱߎYSh|&9 iupA7GEot]4ْNup]N ;HymӞ⡁[gKWƥm\vv* mpy8Av5q6u oSw6+tK.&܇w\Ip9/t틸p6T'J\?1νy98ISyKw+oPR]D75U/pixqXJ#xB=DdLW9fh,7Q#CU.oeB?5}NYc_$tP1zѸϫ7 o%A?jFl|{fH|v5/bVʌn%B/axp6<9f<%߬ѕ֬,=;@mV#Hª].VҞ@9 ǽ?R^gt9r'fd}6_M<Iy V33=ocpd}|w8H/kq1 M&U\}J,>?%C LknR[3hzV>/LˏBK笞+JL^M#ҮѨh =N{^2R՛ʻ=!zk:!PV>۴A?dzAŮ ;.)P}n-SMjS1"7 ?3"|?f̗P6Y/yvS-丶 >KX-Yּ"7e=jZND8|Al35Ͷt{5ľ#?ٔJ r[A\Ѹ#0y@os>`gt>q|=s?cW]'+'?(g>ry_q]q<֖V}+ -CKt':~HfpN5 -gR]FU0V:%wgXXyR@7䇢?`=ʽq/*:u+#\s(飤TW~*(9.F&REwS2S[kUgS} 5[" r1Ɛb8 f+[3X[Z epw/kt'! .A3^pC{O!?,dM< K]AȎ=߲{) Krr@f"3t,5 T'̄[U.? .1kTժk#kQn+X<sCCv_߼GO˘mc;ncܧ6M!\]~=#vCΩq4HV] vM6\A%}mޛy 3Xn+R*1]oIo3W[0 $Շd>!hToStDPsgMIn=&kF -XٵۗS=24%IA8"o֝!O7Th_WSx6YFK`Rq?-x,3gʧ^`;uG4Cu7w%'Lk4]xm=y`77¦ #nV}6pe -`|~xT:<.)y/NAJ{]Y 8[&yeLE'Б.>E<*-lpuZIu(|vj ~N6B$2Fh="#H_@M aHh_P^)CRcc+/h$jẚ }SG̐ H>DL]-H2v 1Z0ge7-l_LMEL]>2CZYb8d0) UͯΠʓ&J_|`+"W )żjƝd?xzv58k .n케”ގiro·PnЍD7@4&s 40o xLꛅA-⽥sE~u+GaF~ yT6dZ&aY 6rFOdOJcθ[0m%mbn~7 M6D)_Ҧ̰~z TfêZN,wfI8 WCOpJ(yɈՓXs&b9ӎTiF{0ͱax֧zCڽ0U|,N~.pג0uc (Ƈ'ŝ-shx4?퐬-mtji[B<}]L=XW,ոQٰ|;_aaDŽc/1jї-H3wY}|ҠnG^u]ӴJg?a7^yuc'd6ySCQd+[7k(o{E/_ =a}ՅXJy /gʢX;Jq**^l$3]N+B,qzjcԢS')&m-m  ag%KT?Re"I/B5 "K0(>*^RL%NFK9{M|B8x4Ee˄7tx5{K@Ooyg޾-ֺ -JrIӴMY?̪Vibm.|TX(Y )OtPR~'=䗻"T@Kr%[5FصuY^s~Σ xgבpTj oÔFj o.DXVhFVΞa[LlI(IZzבsM4&͉scL -lW[Ut%{U (ކkVE9s}cd.3²?&Mn@w=YFf6P߸B1d6pݘU?^T/==x'mJOqH -2^ t Nݒ햾W|'.*v}}qɼPdV.e95w*ݑ:DŷWj~'O.ѣ Wvk֫.s"|n#͸y n5p[Zu]ijR,f#ogIa0 a|/:}dvm]bl(uW=TX6J]iķzƉJ;ڮ1'4'Xf-xFɓ wrN'Mwd^k1ei?y(ڧ*\tQj_-w4:EOr}sׇ:Sw+JTVv}S[o%QՂX?ε^NzIEų:E_| EȊm!>lkczt>Ii>#:ïӼUu#e<PMoJڳEFʍж3kuڎO|lkhA Y?,mɏZW(5cZi NMֶbGo=aS' foʼl__Š~q"3=_-5mlSlh1:N?6ƞX21-]s[. tݱ.PM0 -M -Za\\~!__>$f RUaYBGжr^}1<"NԔY[Ҥ -[|9zhVMpRK -\GXI7eG [ouU1 E?.myF,fߥ4^۠9DWD'84c u -UZë͒%q"9Z垒8J3y-k;xI #NwHȣSMi:}oO;Uلؘn*1Z(=pݜ&E}vƻ3xQ^vBܪLd Хc/$}쬐#v-i0޺/[XuvOc̏ fu{p51:YKaFT|z[Hr?`8m:9| Xo,݅W/ۇk4woFį^%T.pu %W}i7#usnB+~]mNY|*V#] _Z]=J$3#,Xnϣ欫Ѿ7Ĝf;ff|=#xmmϿeώ%ATfbM5wSll. %"R}uf,csЫz~D/_3 @tO]0|Nə3)ێ8< =t0hV\xq[vM#?s4[Ԗn(':ym8ٛ;PKlb'xҺR9Krx3Ddfm+3Ü #A+P< -cU<~*(J;?(4D^cIG'ꁢ:-MNJ?:/ykQp' USNAETj2k{"57h/ h!KnFxaHla׎tXҴ?[$Cyul|;e#?/8W J!,RSS9%j%\!fe/=Ld;() aɫSƄ?2_fR&֚c[؝+SxFtVfFyuԤ>k4 Yǫw\Ps[OX6֭L?ޮSW|*ah"agyPI0m˞wed@k >_ yyDKxz4$΃zY 7qY 4ַ{yfK*7?/SLُQ9/ MTz`U)zA#ãPe,{HNpNtIG.Nگ2I% -WR,{ozT@70FGsdcӯSwUCU⡲_\'к/x”?(2c3g"g•OH OLeS0'?|e=5'E,\woI.2h'oTݯoC0@V9.gzJ(l17&-uЦi"`,vM&& K3O=ˤC}YLBҕ+w_^uloafCr-D'xtK8ז/Y2pe"E2$PJNgBEKcʗÓ +;RVd ݄cC< -MWק eӈ~cYpƪ- øySH4#(9+i/X '>'QQ{^Euِj7yଛo:/ SKHTM 5/,ԔWT5D][jo/Sxb4>R14[OO$Tϣ,I~ZxW.#.4٪VM#8Wkz0ikbwXns=KĂkv7Lx]pk ,Y=z}l~FúQ䞐^)VjmS\V(o![9f(i~>n\cŠ8\؏dh XfΡzi`| ?;VM =Z% j"!2~⋩ނ -g^d>R9/rsWoߤ[Df*XTrlmTExgoQ(f)Gg -8|BάzKlӢ{g{- --Fۈ=hyh ^MykwѪIp[yV͆>(iW7 -,?nek׷=h&9ܨLz*ץQQͭ_# 잁mGb,8$YL<xU>/PEZCE)1c~ ΞCQJ\ԇ3Ϳ>QϒW><]kOVݡSڝEpk)2/$`_ z6zSpwOǗϟ_WO?t]Q$s8+ (_~W|߮Ϊ㊵6{q{MؽJT[{4bn ZSWnꨥ[KcGX2wV<y uQF֘鑅 m\4yײKgΗ2W~_]&TVIk{.d&N\{?]^g.ݕ՗ -l@ 7ያەd[2gMt+Na3 5DrUt8ҠtvCۍ2 iIv)*{nVSF9mhSqs^ -m\#|b& s"%NHQдf|Xۚ2cG]!Pl -\q;" te\չм@<8Y ֻڞĻ )uPB畲@2YI3f0rR9{Fb5 $yzk$ȑj[uAl>wP -NMؽX\0lJǘ ub']ez%k|geId\a$iL^eovV p{HcoTޒ$mQ%FYRТmGhr#+@lTEW-q96V"#cI_4&Ϟ"N9I0(RAې|AAg GLHclKWF7X( juCqJ5&Ls[|͓ nimaxȏxhT_Mt9"~S[wLf Z3 95p9Uj(G[6OH*-퐯@F˜_{c-mnP8"*iӟ Zw2):ܝ[}pD+V[a<>3]Nf(\0*>T}3YkM ';.;ƤS[αWPB6fs02oB026%0ͥ( '-߬V1ƎO[ -*=--wxvNeVZγi s ճ™y~T_e(0 SNHё,Dٟ7Fd]Z:5sײcU8Ş< @n`6l$7 ȝs%szFUi}fP+o'̉Fr闳'csnuP.2ȭ.Er;V32A`tk{!אG~JtPKsKJlhb\_7` Cb!zg[xb{ >5h7Bd(׿X*T{Q^:k+'j/[zEZ^Eh -\K cpa0ةa[ʙd6;J2BoW+ U>P?!ݼE{Z +oeXLدmJ w3ƅWDQoQY.-vsyɜEGѸWZWmr?]3<ic(XEƚ4oW.LvBZpp2xDizho|N:]gOA1Ft>ƃ70|~s9<ѤFңa Ic~fcHJٜX(hk . --謶xx@ޭSAȏ -ϻ:7-]̛5#1\#y» -ew+ 2KcT|;QHw>vzw%7,8eLoܳ#xy^hV%),X 7["ө[#ɃaQNclC/Jwۥt`MCHR1bK1X QWvs 25p|eq!tVňڄ],Th;A"=G G3jNZLdD1Ҹ/(pTcE6(x׭ԞwRv~bI1lFuwl!r5p#ϋ~2kp:KfaC --,rYik*0F_<quσ+ti䦲6$׍#<SDܩ-uk\˻ zY4 /`rHZ"Uq(Ĥ5MCܭכX`'-+ =sk@ZZ˅`z(ڰp-xf!#Q"X , $(7[Dz`+LL%HdzC='+2? 15z#Z7ҋrCsMkd/3V2F"W )6Cj͉w5cG_M؋@ZW(Z - 8=3-y/8 쮧yx?^]JXfEزփp+up#E QWҞha.LoW+#ɂ9VC(riM6bˆR7xϏI&*˕d|GeOqi{gmOsC}Ap8Fm*?x?'3 ^{A5H=FmVJyj O܃gskȵ 86Oi7$]YEz l aOoc67Owg/f[~gx~2L{Xͭ'VV`X ]$QL ~Ľ+R U -/r3Wq{5P#Z' !@`|f}44d\_, Ԫ4?+ ~qVqUGѥf3X]Z3&b@+Z-eHG%}>f,y" oѧ.\Z׉5kή`Ów'eT2فtGfpR@ıbXYK$ouQtrhOFw8PM?nj ry%,F6py{o -R2(|0M+ 9 Xވ90D84O+N2m_T*q<ۻ6 }>Ezа{#'ղvEx -_I+4 j;&UP(n{o/,.#Qld`RovSI%Ʀ7}(* ~ GΐweiFv"ݐvzQZұ%Eq^lk}V珀4"R}Ҏ0dfkVȫqV@F_f׺,LKX")w!@.֝uFmfZ#Mdlvf&ұQZx̀ -1ƺތl`b]J"ܑa=o'u߲}Oa8O& -{Hs|Y]9vxRʌQc̾ls+~*+vXRwHɟgҶt'l(N5G݇~DZ̒d -dN{$Ѵ|*a I^wiΝ3Ey_|~K /&q=>AԘ}=pNf4޴ΫjgUjAGJ.l0g 2uP?듗Ga(4'„確-k($A[K5?#3EwɡVW }ʹW| WiY ߪ.  - B/X L8DzSjF( Ê;Y0Y||.slN&BKdmM#Ysg녁%r29[_䚷LK>x6mP_|͐+Ci,qj^x6v`ABuƾ*[UP93[=|E[ ZMi ԢN0[r9t`u Dz+t -d:љUf]OX6Qaޘ0|uVkfȂа弙3W#EP1Nm[h6%\ܲk_q==uSARKٜ˚\ -5N7: KD -}/f9qd-H%/s-3Gor"8Vb=+e&ȉkr{Jn7ACY[U5`v@c±&cA_@ub]_Qy;M3\7JF[ i贚L\vqTΧKHy|pFA@X5iZNvpkR,/P렷IVfS/XvM r{\WyҬ4l[8uj%?cq/NVekKE g vЅ@?n{A͚oE8Fm̲}U9Vt8O˽m&w#ԖHT*A7fٙwEs>4G`^κ!GKݸ_ -_Єxd*/6˘mMpCeϻ;SP5I98Y1XrQ̦bwV/rc~!땪pk_ -v%Ś*ܢy=r~)%:}KAM6O5Ls^:K:Zp|7Zw}A-][^mP!_xfBfr\7oZ p XllDraGu+ B.Y!+}itsAzboo{ֹ7GA_mlVqؗlw\CYm}oVy|i/S֊~$4˛TRjcWGT#X| vN>yzfpl='yy'vu[+߿>0:|ցuϛ6 [߈& 7 ~]=) Sh?fqN⛿UOץ=Jiȕ_J+[?=AtQGVV:鶿,CMW\9ͬt_({qhy&*vmW>f?zYZ%}1d>i -,?Jϡ}2G`՝V6)g -I430Rʣsf! rUE)=h=ͣuO/\_Z۵-Es/& D@JTΏܒR=Tv.kvyt.{}"VɹTDj 9kg;T~64tx,Z9).խ³GLQ <6h۰dPǜ)kK&7>mH@>j'`0fUbؐ«hYѡ2Φϣ76)oyvmxj5w\Q!C#SxÐ/$D4w:ٍ֝ 4V# 0881mm+7`c䇏Y(DYeO>"9oiʝ/ pQFpGF:Q{Vaד}}"Pj}5:x*wCH?U)cj< D#JJsjz<8Nz\$ζdN_N]C9n_N~ɫLo}h/_I4/hv_WQJ^]V?i;8ʣf{iBֵ5[.X5,wχ@X7֊JC-MHizQq z~⾟1pP<#I5kn\w5.)h{WZ9|f燾?4BR +Cyweʄt٪(Xg3`c\dN9d4d CZ)Q#P*6]㒘 $S?_ CW|s谶q(_fw9WeΖU6*^G=Ǿ~Fl}<)ls΄_EF=Ҥc|Xn_վTr[{7ThǗ^GTҽ0w3 -Ҷ賱~q&'3{#E*qN-KsE~F`ZnʙfK4L5!?>RKl )AT >{ oBNm pSn y\ Ϻ%8V9QNcFzfj<1(Qϐ!ӤGOL)# (sI4i^q}`e^bYsEvz D^$N%b*7TOqU  hTl: ~n -,tRiz.EmvƊZ(9 - '>+" - e%Vb*ew $xѽ(`3;/(#2}R0s$8v,|v( ~RsL \ո$HCT|^ /v>RYy>M.ϙo;i)N#Nz[[ͫs$6~`IdA#3xi2bڴW4RyvЫ3[Z'AT8]t-PY2ai -3d:~EEC M=ƻ,eޖ9j2iH~b΁eQi œt?(qmĈrG9D_pru~mtFłOZ0 w"L8v[JH#:7osypZUjxGqj6 8mT?\!)]k԰sRwZc}wVrۅڹ7ޯ _ - -sΦ~y&w_߄oDF4 ?Ohb6W6;8?ڶCa2,1XoOwoه,6 --bHohOK_t 6_7W3il[~jl; kW|w'$ sE]g)ҭkhj4?{{>Kb=wz=U(ʭsVBr /nkޞ ٻÙ l`||1yhjl֣74Z[rHKqʬq -~r&3;o~KtM -yvǰB#!GEv+M7##瘙]vmdS.u`sn۱v|;$ 泤&ç14If| U- \6k U1Lʉ3 e- i T.TW{WRL'g$ͬy^AY\ ;u@NPpt|TEIVêOb?QL BVLhzuqSbd6v ;?{V]cLji^lHT2S(C>bi`g.{]aʺh? xI3#܆/f^-YӼ `5QRSB+f -NL!R@84zpѵn'-4}hyS*FSn ]a" - &f*`gW}ʒL&| SN0t99oqò.Lo7፝'Eog^p*8Kɶ --Xq\f~gN `Z(}#Ferϵ%6 \%^P;,} 1sPʬN)H)Sʈ?/[ߒGV[36XNfZ 6Eܬ8ދnlFL֎Z￟Cjp0_dr Yr2y23 -=!Ļ(.`e }n>9}tRX"WWzVX%Sf#_ON2O8D`y&Yn⤇:Hk1loF?am- 9ՍPFD0-F.37ѨHTV-|Z02m+kk6gs -s~'L_Sn/F}D9+ [d #. -9Ui\4^cO 澄6+ \4s:B6h 70D?HK8_(0s]7`z~OXApk?BeoM=O7;V+mٻ':>-gἹP~7ʡJ+>HZiD͟{~/~͏1/'mkF۳+u=_l%i~JU^IR} ~ܨ7q\\ {&= Hή5[A<]?4/`s?/i9m*a/s߄ r!Xs7^/\ `8&"򃼹1ռ~ +#T*{{Ym=։oE2+Ƈl3g48z%3wsƗj@! I=m^:JNo}{Nk5hO;#\n 2|fGy2k,1-a W({׮NשL?wc\im֫۞??yĂc5|?߄ 1yn_# -h"}A%{8Xa=$%zN虻8k[1,06ۮ #gv>Ǯq+Ue=gtF3=];Um6o䌼uL`ak{c{k_CJF[H#VZѓ)QV= -|ig *u{iݳ3]W6b+H'hpU橪r|a~@pPh ks kt֙[V"k|nel%MmCK-!e|e\)IĐdmwv8 W7gjyL:A{uA"EAYC뵳43qDԷb.U 1/? PW?Dk k3rg<^J &#=>oXyU4ͅ}luZB)'iVe(֋k&C|'5O" -ʜp{֞r"bp=Ry8tks?f3P] JyrKWⱭiĢvJhx18oEw1BMyi?u<b >MΚy4;wġUY\kYO񫴰 tJѸ9oɇ5\iCvNنtD"Zi^܌EV=]zgfj1D~o6X,U\ ӵՖj5_%}DPI.rj'"5S(9u7drM]c=,w;.,(ZᗙQ*$7%lνthO'vJ/ꍂ]s !O>c*9.9mqo&EWm.#4H8[,MY֘1/?U5#"rK‘l2yz]O'؅=Ma[uv$Xt*g7}ø6vKkr <í`N4Q :]X ]=ipVL\`G37i4Iٲ~bd+~$`g1Pt.=7Ju8] $T]$9lQ?ɶy^Nhx ٩v –Z.%cE j8`J TϚt~|*|)f\HV6?l.7d,։!B\grp#"1$\kmՓZLH!usQ4}w&cR揤I(\J#˒ [NУ~ E^ym,B$vV`#.-ϻȣrg99fyDrاRtL͎<7"(TԦU~1Xb%댲]ѵx}13$k/T);$썸8qvVkHd5W *%5CRv)2ͩ70_襼JK{\$ƹ9wl՗7Ra:"{Ʊ̗֕Bn=4Fې~3.y"0E,+qf 1䗂5 8 75|z[ EZyuȝFd.VGq`]tIeOuȴ:iچԴ=ZJ f)HW;=^[!?/G6,buGfB~\G{0<0QYc=7!K9ݍBb9 VB['jEx?z?$f%gXHvaH?>ٳu&6z!:SLbxy'm2zɸZ3l%NA ?( QZ򜧼WQ\@dյu0ݜE2|l=JYD)C팣02pcݎg>~ 1ODC74~CGohƐr.4c&ZL[-fBq55,5ϫ՗f 9^cF6X?U&>4?O$ޠAYO{V& 󊗧nݛ4ii[3?j2jO4ȸ<'p˴cNtoIA}"˯~h#X!|WOO;(<6>I޸t -7V]׺tN揪㸫y0< nN3q4 oC!( -;`oD=(yGv5eKAh,% O+z4Ű;Mǘk`WyNR9i}C}?0Gt;-}v܇v j♨̡gQO\ٳ v7CUϫS Qmf>@q־DaOZ( Gqh"I侞%T:?ևyk eڍ| )JooӘs>LgJ!FRMX {Đ?+$4F(j6‡oT)=q/rxw}GKdV&L.vq)I9>.j% %pM3ʵ>;- C rJzjʩsy,#L?PDS~HjXJ{ɽi^ꏫCP<Q_1[nAy>#"֩Y(_6' -nwy-i>FKŪӆjFirJ-4 7C~)wѷr*I(*bxOGݩX tϵeitk=2 4 _}O:Ez3՚}s~;ﯖIىb]QKNf-y>+$r ʗtl:S[-秉։Z?#SnI͹h QZ]uՂ#_!?ϿP.Z'وs -t * y$b/jmGTң bx7.GJ'˔ELa$ -B9s_>(s;,Ekjq$ |J8e7ʽ~t#h=`x^>0U d$U0,0P]u+m]Lі_$&϶ lqAdy;ZFu]̍χ)y~]3j dlxhY@elˏ{2eއ[*g$a֯ɶ6/WzY=UGvfZ\J8w'yy~5!b$ 3@}Kv}-IP9Fb2pnMtOLvUפ+R)tVZ>ku9t쥣e7~$_cGD -QkN Y/k ;Wg? a=[{l*vbg]Ly$hOJLs4Sgs7j7{jI N[~v`EAE_Bn#P6Zah^vubL+HrF)C앭ӆ'o [݃wdj+O >Ou%Q׼F/v-\ݱ6Y>+&f )LӼY;K %aZy=7RHHkFzO>KFJҍp%?C ˏoYxl/X}9nzҺgK^_#U&{ˇ{D!ɥ.x OEr1NQRBn?LP#uUY~=s[iv]eyKDɞQl+>65.wܮ'>̴æ=iC;zcѐnU0U业M2f)8|:ë D.=m=u-%(٦}O4Q:e )?$84ئFD -vGqN4bSJz?y5_ٳvB'^!fIQxPnNǨ@ -.zhfɵF>EN[79w0'k>.}w R.ۚYjqg颴i1Ϫ؝x_+"C[E0F_EC74?+$Ն_1jղБAGEWx(?wR5]X\J(T9?vV lީzE2:2[(!:f>k}~y$*}WuYڂͮr(jy~>չ.{[(aZ<{p@lh\뜻P~D;PtSU[Ԝ"TMe*f;.{WU"Ti1:f#s6½z:[m:mR[ -$܈$EP[hMroS6-LRY_ -6O%݉SZy1CI{[MD 2 -R%> hAI># {ʤ ȱWYܩٱ-y f}uvΕ|l~ {ŧzpנ[%g#Do;̆*nU]p73:?UE쎖FAk)~ l5\BZuVzUwyR|=<<@wg S31v/ - `ކ=1,wy0]Nn%uRՏ^+n;[ox+.1J!E;ǥ,I ݃2g).o]snq(+ɓJXlNCz "O'XzpZ)jRUn䕜S+_<݃͝P؁.p'dn&ibipn}',vqFY>Sy<ť~"mߚ,!i)ʼn[d۳4Q3qT‰K[?E ˏ-&Oj0}|9lqJrVR8M'jllΓWAh˚H{9JEy ID" Ss|s0^u`&7yyl:yu_F0Tw8XWh -E"ITtl,4Y}uPG[lWnNW-܏B}*tŕ7IJ'Xdr2)#D#i6jъHt8~-F=b/б3W!8ۼZ7"C[4ۯao]n n}sfrhҤP s/^SohÞ4S^"p{!=f-(mZ@Q?O#P.mp+Sh~cN%.eՔrڍMS@l`2z857[i$?2TH`Uidrٟ]za]rr# ˸.St)g' - -|]_ga=>EziM55i2:u%m!?A V T | -ɞ{f-3iv'垂IU7sMen̴eփY` bFd~$R"J]g@g -@=>^ٲ}k^\r]9J -^[2` +8/=Lg6N&*eJoptH֚k^y5 D* PJaw.bVfiܢBۦoݍaنr#Hpa6 '7kI\ʊy2ہYr Ӡl]PiXǭO-JG^7i/. ??28QJ dG,C;$IR#Ԡ?"nw hIrvޒ^nw?C~)3\ʺ\Q9l!EŦN!l UoSPYJB}s+ þ%&W~i/󞵮KΔ74 ~C3cohƐ, M݆ ~Z^zha}}^+4?jB0Ȭ~jz鷏LjC;Vv:h.Jܔt+yվ5;{~!>~zev&Gmtv-bjhu|"?sAQEwx5!XkqV/vu1^nCzqTI\L4 j'0:P3Pps7^Ϻ- kͩ;xti/zHjve[[yq_آ]՛۫*v^^A6GrcNHXu nZ0~iaQ⇨a]X2-VxAq[z>M:Tb ѣE9)`V(*ur OOCE7c}%b/>dߞJvp!Fn1xB=I3 -宾v^\:j[Ww1Y Of6qo&tt5h\ZP0@Ye,} PPb_0>P$\Q 1p^ c"s4Ĵorp~⚹ RuԵ Q3Xu6⇨Aѫ}6(~|$L}A17d^-gf7#}i՞9m$[Q{iUﺉ3? w:'5⸖:Fwy -Zo[:4v+gBIUV^5A_ 4z|6&Rvr3;Y $5a ؼ}vPPRׄg{ʝ. -ۍ=9}}<_N&dO`,f#tkD9ZVBm/,c@>oSK*lb*uJVJ?k#/1,oHBŃ=jƉHsCM["E;>|Verٳ\ia<:y]+?.g N(woZj諴VOUM~}Rv[S5> -bq\23.a -вy[v@yG *wOl>ߖY"sQw~e 6Y{բ۔ixSxyn>{qq0 Đ%(|K(zNU|,$|b]_+=M{/>&>Q8*xOyx Y EtF57o:ʬsNt8M2s::b|_ʭ'exݬBQcmhJt0TBq|9^Xw=fݵ^ ^ -E$WŌ[$Üvoʝ[6ZOߚNmCãxVG>X ۛieƒV8}x55|1LF<@9I%6ϼ~(܇vѭGu 08Up_.N+m.s_3r?[G4;c]F7v^0NFa{c '_Njg@_Dwj0K۲xJ - ͱFc3eRәFr7*r3A~|/ʅQh@/ZzOvzy@ť`.6(\NIX!Z;EU1/Vݚ4247mrfF=.~)tb ʮ<9w ۼJޅ/U_"gʇe`ZNH^ ŜqNQE{'JR͖d%$Պ|88v -Y}U!C 1JAq29ɟPĈk k[JZm8m -k JOzĎ[GW|6<Ɵ97]1jx݌nv^Bm[,N|1룃\+ ܹS,5:=f9MKv=g;wxxtrgDZe yEhkuʩ[kԃpt(/ (PXF5r*_8+cSݣ7{½ -9:M+I(A.KZaH]IEKAx޿l*2&t9K7{(}3ioY<19:]B`KIKw ƶCkML!F,Kk;O -+\jhCr@CԮ?ʝz -,.A -SȄ~]ݜPAUgm娫Bk罭7ͱӔsC~H!+Õ kPVj+`ϵIA9fܣ -ج˔+BgR/W)Zˑm^Gxoٔl%k#ܻnCܠ<D.@@.j@>e@A !#BكlbyD+].G![AOW>6Is'Eo[kL/5oNw0v|) Eaoh?>4A-@~u@8E|Z ܁Ź=8-Y$j-${?q@‰?y%ex7^JYnf5rm[~:畚,yCE{>+*Wvi/NI4n^A Ci7GI Gݮ]6H[znB"R1͡ д%g\n]Rx7e//ΐR>W~Wn#{E~UɾΖ[H=bKzN,e[ /?8^륇ph"]3e{"wXdt2f&s.V+&7Ir>͚  K/"q7O@g9FTU,ϔ7pF3?o׮ͻU'%E_(<ǷؽlR[N7bID^T6W$ȊdZdҿ-"}5 [_~fP=BU}gM_Elǎi\+y$G%#wihi ZmhГzP_5?/1M{r,&e$s4 MgG_]VA.T&h3kL8)J($$D;m^1PLl4.Oj' -J([krNv%1)}'$T>jۊ,rM&41۝Bc.;(Q!K!]>۰wPzxYPǝyXQ[ӄUɧ6ح栺5P;-7g}xm\M"7^sv\ V(-Xݔ>+$Wpt#V_CG,w RօcEA彆h4ҵV"[n˼!sZ\m]o&S#{^u~7bBx“K!<|QK+yck9cYS_[9euw<8!kxF7  nq 7N7F07k*Ce6I6FHooQ< ܹt{^q["ËݿJf(ZHJ:҈;K7tk?ؔ]qsE`d\6c7xzc(q(e_[PFV$z[G]i v3W0)0ךÛwesQչƎl9ꫜ?7ZbkC|)bT?(.v1 -#! &ժE{+|j9Ys9vcT6&-8;lxsBG&Pz}IYG0&NrD雌Ç@Aݦ`GIT&6˝[W>Ͼ]FLvn Mo-_piUq1姣NOXPYI"goMF;wp?ϖ|žPiP|qy:^>@Q1H8broآc.uzݪ.ӈYLo϶ -U2Vc ~;k-Ԫ)OC'_|0](lgoFAPu^7,Y8e%wZl{EUt޵m:wMN}2T%Lң}].Cdzs{UnJĪlFx8 e~;dt5I VPVlZ2M\u -&'.y>.s%a?yXWwEJh_%w.GZVe>K~46 !umXᑬ|m3C -߀LjI|]zcvקLjW70DEtLjt=kЬnkl%^2y㱵2mޢFZRTĄ(~A R# -06ʺ^"WKxȓrFn(y}O>K.r4s/UF'tA.tXOߧa ӻ /;ȞLV>v5_4~CF( eB5l j.Pe@^δ@SZJz}=ם>~y[>gv" z}cAK?KnuwW!nA dQ9ԁ@!nPgA!хert]9<>Y=K!j%tj+"߃4On|ŽUHh‘oy]fYW, Y+8xܪC0^v踕39f rf b,|$zH4WBjc=r]r Ax| ڎ<}緧K}>K`ù^Z\p'yP8n`x?|.0qc--HTP=رA Gp@;,};MU;f`!?A.0AL _@[@Q Z ->u)+mp\-'@t;SGѰ1%7'U󱠻d4ټWqJR\f5'[j{ 'FFr@ -^NuVMq86h.O0?ȟrI$b13+P|ӡ[ JU#jaK+e]CĢ!w$|݄ڊnCձ6[ͱF>i))_Jpe -eVvng~M'!G1hgS6oC,p៵)8w[~lMlJy+y硲6:)}&S{+079A (e| qƏ7?x2#LshAA}?yMTOr2 f[e5e~Pܬg5Εz;;dqWRK7d/h/1DN>?6YO>XyOvulod8UieK*ot6/<3w{_&Ŵ|i^"&Ckִkp~\\lt\])S o3.zi"b_LΪ ݳlU#S|ZSوg+^wַ e'?Az_y:}PPhW|ņ?+=0_O@#FHn7uʡ~/̴nlejk45C3 MiGSCֿٞC[ҾǃCcjP*ahx.ЧRxNZOсdxnzce-i8Rn=3^ے3gϞQD1:)`JFMkSC_P@|FP:|o27=/3k#7'~j|ڕ2뽼""pUpȒc#1xEp qpF(uGLZĨ@tG_ipdUG'c |A/jWϐΔ* KofƖ2^o-Vs DV?  괴Εe`M\e=zÈeX^{bVt\B/_C~KAuNAwtFQ[NJTMgۛQ 5xψr8-YYiFx9'Qfw:IrADĽxIvJ SrqP\PS*1 -/e:`I:>.WB\4e`"3 T}E1WF߹h1[™{cIaת]Nƺ7 - 2 -+ƿEX;3{:}O=/hwvUZjMn[g6?HVGo(b55*Rbk}VvNizjf3MZ8AӪjh(f\-ʸSgŒg[V'YluU%>j5 uO otHP%Pry:k^(Kh*7go'8Db@̊ST[:_޴!)bw4^`E>[Un{XQiXy6{Q`sm>gP "sDJhF/BS>FL2jQD,V>~}ap~3a6EiȉUQiK'=˪(f/<3`g/ -4*"p;2p4tآK¾lf7l N]SqCC65+ د3Qj/_! -}WV71qSLC۾(sAv@@ʗ@'{Mڶ_nse] G4=]ր#G5HނLaU{4m[CZ?PӮu-wSJ-Si^)zI JtZ-s0Ox?oqD Mߙ~gPS0k /9;͉"[:Zw?>Je^EOA%&Q{{x[0NNM:g~ΦE#Ky;9 ?LqsEMT'J}~=2F*Uat@]fjS):#%-vDmyiIS YٵG?2f0* }~Y PF *l%@~;ߛ{%}egE'MӵM3̶ZkBMnVycޚ:h?֣%Q$!_)ˆS7NytӌHkUԌcYLu T`Mk71 Z'4Å|sGZ7+ZNjKf;t>Z]\~%}z>y4~A?;NcF^'MP^gږkyk|X)usԽԖ3UuoʕR5sm5*6؟ᐝc-i=9َ endstream endobj 384 0 obj <>stream -ug}mYԢ>\8ec]_w#ԖJ7n57O&|3OF&A3Q?[r*_ 0KRDm.GZU*b4EA?C{`q~ ;mr=7CS6 |U[ -iP?*W;^Nvv1rta<O6.|\/P=o)yu_o.0 (U|3" 0XVj)ror r9DAk@9\AN^S[GGs?ůPՁkݧ܂2 P_3:p}b39i(t^ĠZՎ&d(4$ĥ -?gL1G=Pb3-6|1 -ws_ -p@powh)ОZ 4 GPkHV9F2FLSfg‹uh/-cƫX h64A l9|Vq0ǒM[*qjܧ$[H&B^Mpz -ޫ+ϥVěGԶ7EИ9pѧ9 -m,c7q3Д==>9b䧠ZpեvvlWhO+v~Dhb?W=U^ҭ%K^~ݽo­Z>J<~_<;<=p#ke:w>ɨ[3bPwxg]곀$B~ IFO:Z;?4S\)3$SgsޓWgO -~]YnF=1~TܼP(t, w) -ȕd}K -KBCҢYT_=۵A]ӝ䖵x(R`j݋NvOgsS_ᆣ mAkC@D+𢡊2k ;I3)>M.i勣G*eo^'&H8Bx4At|FnYaO?/T%^]Ց",k=?jT -vKŕ?mǓI%ӈKVtX8Ԯ:~ű=` uUxKf4>ïh2rVIop]^oP0hjC7LTy.B0UK ЅsC =Ylƒ6v&;oF絑D"dwϭ:)1Xa.vcM0af\2?'CbA:#k]..O,l0#¡)w:ں}8nWVN&K6VF/^$k ZlFhlC_J (:爙 =P WB>OfGn7T##(;SZk{\۴ՇiɶLWBb,x(M:~Q[ד^9ﵙz -< =P*%^rh|㆕=ȚZqWTo\]t)C-6slm}n9&~cYӶ޴ }(Wg>v)yXU{֡#qq-< -}d L@V99\IUgUo▒Ne g]M0t驼kV3Y<jۀ[g؇( MY_fi[]3o@^ /nJH,i>UJSf;ӃOPY*Wy(Z(ס4y,w  -m?XZfRNloky9i54*Lf͸ Mjey*jX?mc:ܴoQn89~Z[AS9z7{ѻ=rLq]@̠3l3(s[E Yђ5>m~utڕs4ЏU~`xmJ~sRU]/&id3&##b(m7)#BȽ7I r %(4x@{B{(\6=ɑczeMجG-w_s˖mz1+xW& -EuԪRlњ=Hmc"JnNN%|8`}׻(p^AiAqp_}zqx+|ǵ }*=hM. K<)!ڱ0T͸*SK]Fn[܇98 9S5n0wKapjBiic 8x׀얯PBt;wbX'zpnO^7`.)"#sFL -ik#Fjv@*R1#fS[o+G7S -(hQLRe*:qjwb\Ưyu -i9b- xT/Ng3~LۈH'DϽ8zߕ*Y%j=GXNihmP[c-^UxP;gQ"TZAn% O7Jϓ&훽݋[͟r6s2wfR d$,mmAލ^+qNc69rTvUa=|$.2oгJʋxnS)Q 3@]s{ixxA:ǻ, ]3ŧ2mő1A#9l#0gB#UZSۅ. mb2O8wA*{UNM1s; n2comf\{;-u5s6 WOe>7h2q%9sMGm&]AZ-*3r2\j -N/ {^r}bmNYJ=eu E"0ȬvTbf)Ky:\Q.Nj^asY- -޲ M-=(D(նX]u[# 6ϛTɶY3/Ѹqqdkq[Dw?ȪVEsRZkʺGG)D`Ʃ=`^Lַ{*[G|QKJ]hή޸hQo;m-u_e1*3vYw&72xJDkE{g*҉NQ}1;x(JY.9׉Jג`@V:rXfnLofGk[߯׵[o}$5}t;6Y.6pƤ@~yPe-Y h[T1fBP=FV_+@KeRJ -e33p˚Ɛ l|2zdX}rћkF;NT{&p@kVi4 Ihm)H3ꖪKVWlJ7A -g ~p([0ي׮čU -3X/!{9C(R}ܕr4CѾ*n6*Wtwt(~.ɝܭxZb9{<υ,o*i:m"pT3) >>@GQ -`Y``Pod@ "2es( hsudN8@*J Ut? 4xt,w0߻Ew27\`̦͏* -؞ HBE -}J_qZ%j+T{}DP-SZY zE*UNhuK>j䬹 -r -}6Д Vn -0[YCMLb@&'ؖr j{vwjcBTQPs - -S'0kqMU SoBc1Y+#C; l2@) -G 7LV j=V_V}$jS}Mڊ>} u+Di?>)l'>)˸Ͼ>O9l PA ȂաP?CqlN',ޕKjPhrtYm} x~.z8+헝++4NKGPy4o>xBT`Mг$;L~D_y:'uK+ -KtTNgw VnV -q^Lu_];4Wo)s|0]Bo@-o1"Hq%wӢkXe=fnN$[Ƽ9!]8 -YFm sVe~@{ y.}DCH(v?}׏=YcicAma魇._(? Ba*\.Χ>qt+ir%Ey#7o.qn҄\t=Qo`*u pKSFm+\=s8+ؽ&hH6m޿{T2],_v&~|H 䆞|X4i ?MkDIG9զpuB|Y?VN˥JSq*Vcq̴*ѩU d}(|i/~/w[%zd6 N0˅]R~ KW١WwwuK5[Kg[''k(,5ZP2)*M657@{9l?jΏ7^ҋNmNNlW<)zMڷEzdRNLISCgCNKrv?P)aL[o8u"?` - nJX[B/dfdƚjAv_̱L9Ysm5R!nZx(R47ܕI8x+1!O6,T>,iC  -@z6ȏ׸%+{2swUb>t5>*eRNg`yaPXjQWr#H rޒ4اZ\F5yrvIX =\>mGf\ -,Xpw.ݝb+Zl=퍕nƫE+Q+phQ~Yo /}HE'wX.=iG]&ޭ)r:p6/Cy1mVs//;ۄ\A[UnN:,`Ҹʦ߸Ƶu/ k1*=/VR[!Z$K7(Dި=E}dM֥Pufw͋ 4o?^fKk~u?ChBz9Ndn5&}p'yaAI5MSy1+bcNj'l[BkJS1mmG^HzAjtxVI+]\|vJ+ȴ NpY\U^MTAq[Gy9X/E>lF<u -gj~.|}2:_oQ{<-rxV, R~l7CCsek&ӫ~hX&PjS?'JInZqBz!t;Vt|؁B.d ܉2}㟛\cW3qR CH:VQ^@K%|䖆rtf'@R mɸ zۧ+Y催!G,_Ye!8TAVj/"hN"@-hdo -];(oچ.p.Psru|Qu\Z @r'_. JyeOKP>ΧxFݺ¯LP)jܥO]56"T{34'C G -56,@/ F>u?g(s_ANO[>jrt]_;%:oM< H |zP)B~Mšuă9@ -JV63@* $ɭɁW EgNj4.8}棊QehVI%ҹlg2 7BT-a|oƮ: .c4:]pP1H]vϙO=a /dqc+*s3\UC>j.rίݧK(m /BWSl'*U{I-G5=y6{R(sܻ;O9igucN5ǔ@8\/KQ'u#m+q'O(7 =:bdy@pg"NR|L4IAY&~OE-B׾/؝ M_`̾ ZK3v+ ?HivY WiBr2ASBVd:57mX=3!BS)qaβi: M~|fַÍ,gPNU-wӚ _9)}اʷ5 @:>Yd:RmUlpQl~=42\vƳe`oH|3s6I -61\}ӡWѧ|}pQCjQGK(-s^%Z#XӬaa4e:f<} -33Mk2 E+;S=á= iM:wcuBe`|mp%yO?cؾoVZa72 f矶,WXm y>}O[=J@ׯsdC3ʼxM58&vУ']T|8ƙ;,WTwؘzs)Rf޲ֶ#$7%!$eJ|*OLȓ9ſ -(v={u0CPcubSԔ\ȏ;wT1!\WS_߬8|/zjzdFv qƕ4㺃`tag43C>9껚ylŦuRA1;$wW|Evk]Fpbͮw)D2w+% 3ȯ3(vT&Sg0s\Vo8-/r,Y tq8󧭥IFV/SV0~hb.3@^ŝdu8s.w,og|eM -׵Dϣ3܃Π:snQVC,oUMpTCP/N(ZBRX^ysX9g$nnBQ~](S9_ -L -˼:кtN0hÜQ]Sk.^4#3DkҜ昖*+ٛגH{ĕ¡zM씂ߏʝZy~{f{}Nu4W<'6ylؼ}̢U7(,Kӧ8&NAlE#edv&usd5*cg?I[0W7z?/ JOz=/=nzsw~qCܕ;vչPOw!W)q"Y< -ؑ>BEwzSw|}W\dEfXi/!!c7 ǣH lMsJW ӭLiޛ.UE!#Ms:iEMA-%.1'4yYDqbί ׏}e)~B_\KVۅL'SC[*Wy[눒ZEwH3:빹{zvښ+cmb`r4#+|69չ3Yt3= Zut:JRK( ʜ>O)^ƪo/Xᄩoa5V#MJvlw"D]t^Xg^~o4`\ɉ7v'};SZ)[YBlFѨS'MjN꨼K͓#a䝟,gƞRp'uVLWeD.wDx \a2qM@=d"3mzQnɯn";\j^H9Aڹkhvsk& KK8:A}鶜]٩/g9a/=5Qvޕ!IuA8̾ݵsJU\aCr}ec>{<:>:j1"wy,IMNl5y77 =볹؆LKMfbggtPW%xCU)mj;\җLJ\AlhL3PiqTݨy[hnKʍ|m'٘Z" !+3椴w+ߛ&Y 廦 KZ^ŮzA)b Z{w6Y[Ev0nV4Jj !jk{^W,k$V宎;F:JFC6;fr}fT>>9Ss5 )ƝbkFSԪNrXh\=52ỳBGM~JjJG.@iVBoRll#:)45jf?u6 -Zy֒ 0S,]ӫv7x%Z'i'\/wa:$_a혞[[;Da<̄s'ݢMyQWHUzfo<-ZuƬ)y2%O̱XNB-\hdt*9h"܅HҔXNů:.hGr>Ne m'Cl@.U|R o<RK[r:Z?4%eD,'\Gu DZ+oޒX  mv|ܫ/'b҃*nf'n^ @1N@6e yU9,O<8@'Z sԨ$F[)5@v@eiirU}R9 n{ -JTkK -B ,s;|r5~/c:u_ezyrIt-(npsqFJ0LT@/x%[_f!WAN˔B?˜ߵ " wd0!s{!dts5yM -~dx^Á-2~+$LCU{tVAm%\S=%P8ٓvLCd;W2ۧ^4T1SNޱTa|Њj",,/8Y^z_iAny=44'<[V٨}QjӣsG>^殲ͣN68b5xsWjMbwIZ [^mYÏQ{-Ƌk={ϛP)L{'#5y] S{j>?8dW}dМ.U_0Y||Ju;zg9erͨ#}1E~ ktTp4HAFAֺ|KqٮIt(>@>d]tTȩm47Y?p t=g<+|޴{G򃩘R:*  Y?3<>LB9B|PKMwƙ-Аo|0n^e}Դ:YeLR8IOVB7༩1 sߪkܪvlBn&_rbꫫG3Щi$%u9S>;G9 6Ez.񌸦uJsS"G]s.!ʧfAfRkHK/,\ɼom7Ge}=;yl{?IFJƸԻ6P%[78+"ߥRiu+_/7!5333kRB?4PB#ỽ:\{U,ߦp[-RӶȸfq.U'4bӼ8d:XU')Mn'=ve,[b}s 2 -r}`,J|MV0NZՑrӥd(XV`zN91;*s9њae $'5p*G*QWx?( ?ԑtY7@gG&Ї lj‰Ǒ,16-[Y]LTP/u6}&-) htF#oӱduZoFi~bY#TɍVj=z]r.ӊZ q oރ˷s9]2ӹ'AR=#ߏ;8)EYMcIzC<'rYm2V0waۮO=l{":n7L˃l3AqGNss[rW9"U^]śe$yϕr&?Y^&n,ì9pS`Ga22Gpp'U>Ƿfj;ݞn=d`&>n,]$<5^:V=n>gԖT;r˪Q=a/iiNXBOxji3+ysx RmekЕv.FtCFՖW\7ln/`4TZ>7WZjmWO>BŬȰpYPT?qyJdsEֱqmleXc-3)$i ʚKQ?7E: -ֽ m_)"0[TۇG26yT[|״-MukWnU%| {8|:Q%o"y/+;raņ9n~HJ?ڽx>w[FQtLv{<"v4vDKm^944rFc2v=3 KK<^o}f=j_s*tx?y-JVnz7WU>\0~5{\R$-`ϵn$g-ӥTàaNqԿˤTxnn',UStm6՘&X>:7l&,Of(lr -7;F -UYk/ Ys.528"ױ4)q+o;b6\F}WZ\ݮIS}c錔YmN7Lu͒6ujTw6a[3Yo0[$ov3kjAҭ\uK7Um(rX{۷)v{|U8f9P+ fQEk[QٺSw'zF~|:m7{Vs,XEUXb}Ef2\`,t{ 5Q:,۹iج׆*>kI'/VޮlnWVCAɫ?Ky]!oXWbGc\iyDf4_^=TqdJoJ-Q=m4SN_*ʭu-wjx6Yœ -5šonk|oOYH}p߿I!83r i |bCZ-yǵ;,te0(g4S'wO/] -SS0L}̮LD;c 4;]!]%ddD -ifI=3RX)oU؜g58'UI$PR$ -5J5MNJ.̠&b#J=;q2^v޳`8M@s ܾ pP *@[)A68ZD8Fg @^ -CX*Bp@5 .@ B|'M ^_`_/Nb]Є)Q]@{g иRn&%? `x &oin >a g%LuAe]cD$2;)+PIƷ:z. `!0ԦzIg%]nЌsz䎠 C>W(8@6E BY&[=#;'={txnoh~-U7>PB;`VVX<`m`ړX 6Z}o=yFoU~Bo$[&xb^yq y~96u{]o#Z % UwdGf|=ClS%g=a~-tJvh5iް؟X$6Sʻ*Yi'˯K\   ѹ]C.>JE(BnbXuZG7*?!+/~ր}ΈQ Pޚ+](ߨvE=ZDTvAɽ"}~"k_(͗.e -={Њ5p- ^{ ~z>6ify[-)QQxظH^]Rz">Z^H'5 6k{4ǡ'>f7|> -c@7@*qu# `\,Bm.Rs3u=5k{]lă9~f ?*z n뮔DH -7Qʜ=@*䗀 U2J# -͒kpW?n.lj;5 ؉~絋#6á$_E᳓Jfd̯^:mm{Wj}XY}W8g (4A{~&tS7ʋ۳yvjcvQmKl,'שmd%B$zqlFzUlF> 6t;.{X"7 ;Є{2[l }{1byp:s \F.]/㻪7GwhQvZ>S^V]j;vOdNjx|'2zhWuz>osQm=,R1_y4N>q8Dlc$^48^[GAIRH힯 B_{4P8#R_mnyoӋ9>q\3vd:vjZpM嵬N9512+)1; to(=Յtص`^Jﭮ[27@@Iu&^T{Hh:=Nެ7an}./.KuU4'1*^)1La8Җc/6>7 {rPG$g%NCր' XRuNk}Xpޮyࠩ˼7m $U&nUd=+?풴}bvAr2=*-Du1nW -jS6:ePad|2.OBDU1ghuZ921>`3k1̍9;^]Ʀm J5/nJb5ĠNu ,F358|xR=6'=v1V^{ʬ}`K)e\O-ZtΔtuܽזּvYN[L:LW ?؟&ИWϣ2I&bngiLɂͦcejQN*"yqRƊ٥Hn;F -t#V6}WvԥBOY@mήvn6JZ=imb.uy=!jgbO/SOR"m&)r}L;"IQhD́kv}ǟz{&^y|'mV{.-ˬ7M>.o`m3iHmPWxjgu똉>%En-O&sMyg'ɠ5j4H럟EonZ6In_(z}v\'flkvMϝ7kӌ(SGur>筞~CRFMJIgIjn/'.xe6b0g+zjѭ4޺ePj3M5)uꑚj?=v{sԽzMZ˹W+T\i^X} -7nn"J76WcZeE AOFv6[8[5iߨ {nS5 rU̷rVSiQxu╪fT= Vν'F"wէ5ţ~D֦[PnϓFp1ju׬%6과P_|J5h/ojW{Q߭l|X֧Af)iЋz -۱]:oX"_1o/z}phݡJҥdRs'kRhO޾<;0_zrf* qyZyh"T1SJx^B`SB y:9ɹd2%dJnE'fE{KY z;8 -\\l؆!Q;K捺jk&T?>3XW0ﷂ|HGN6gƞm rۦH<bLܳE\4V&tΏ4 mH2~Hk2@2wWb#zY9q S @z킴1AZ5lf=4%!6"mÿ: M<"ށtvN08Ep~{7ׅBU W IPC@Bb 8CGw#@ @t=W WbTS u2J/bw^bz9obAcO@/d+5YZN}@zЎ}(pDMxC| d,ag}k·n{AO @0h T7أٜ:3q4{\1[.*Uܣ;t2@ʔro I,ݿWC.@:(+4Z90y'3xm]Q|O^~q3hr58ԹGw؍Sy):G^uɺD@=qДVjL69vf60)''*o2zOy~|rs!LJ }LڽN:R!`cvx1ߍ(t"FY=8>:^[^<)7=+Q/bL!۲U8F`c,2{a\X4\B8ŋHS>!~[l^e[5f$i(vkIdC ½RQzYB&L=RS)*y:NXM+W[}cvt@?-{?>d -5o0A~PU<" F "{E!~>EZ7癘aN )v}+[흶#6>\9Ҹϭz*:cm궷ra/~И -Eq;YwOv>oYgn?nm!EvWwVQ<\wykw˱hYlaFnqPP&!|j}{}UֵNzydq%uȊur_mp>5Unq*XU쮣.-ogu2GNZYyaVE}^mYZpv~7qGȔ\k)j~2Mw\wmج6kR&2&+ϐ<0KF0><q ۀ6/J_8QRp2jZŜ'e3L# %,$uq8[,;Vo2rU2}v4hV]gA@ D4lyx !ޣNj9[_Bg[Yʗ!twe_ INM)dͺ cJ -vl7L>V$ qNSs:J=+m -> Їw ~=QE q9$3FvsaXdg5E?',Tv_7UJ|wo"CbaE|ORR`gv~||n!냥8t`grY CbOzBeb7 $I7k'}6\Ej,?WNr}^=9~+c)bbb測Ϗt.gL?9U{ s%ӥnC;.-;5Z-]O ^ɝb}:./%Įƪ5zj(ʯْgj(UiE\g[ւcbvԫy44O!艹+AOkd4uUDjjAS(-{vcryglC;hg>"`v[x@Nl'#nTwwtUk,??֖C)WjW7_׮K'?X -zB=U?V~Pi[Ȗn7\AϔƼDdWjqBW|?5^YWr -h$VsraȏldϤxMx-*mʅU -gyNrxڟVhzO'-&[#IZV+7sy(q ?)}u#A?ӰwR8Hv(/Eob gB<-=}ѷ' -7ua`۴)SIb%*<f3`V Ѩ>"w&j_HK6{!6EuLj7[j; ˋ֤lw70Ƚp -^ҫ[nLe{t-frMčs{I4J{>OlYnzHQJ5[F_?c}X\ |`/ Uә\NJƣ@8F/^-Ʉ+ ե0:C2jgXPݔ@l6 1XW$^w֪+ե.TQ+otRMGbҽ^;;Xl <,iGɉY!Tŕͥ`v -}lshymbSjY7##`5#'D26n\*vd3ȕR-aU,+-r{m3}R#(/տt_DP]nYK}S~΍1}bkdj~)WUq^}̎r\qŕq.wax|J>+rW*@dr~\ܧvMR/6n(8uVjfyW/gZlJEɛ4 -AaB(M>gwJ6fk̠}wvSE bO -'OG4\AʋqyiVE0㾜)R>$,G^cz1ɩfR jyÐEBvwٺ -;)`Ys( Qt)Blv(tYzPAՓQ)ԧmsC:~|.AğwG3 z;o{ot>&ƅ1GuYT <cTT=;:C{q?Hy\)qڭ0!>,SdH6jB@] @8.^VYy{Aj-@+H7Ho|hyfiyhYNiy0qn m}j1vekFk\PRKԖvr -WPs'ܿ9TX~d2B€G(+&ټu^vI&J!A^g>&N#S=0ߏ%~>Kѷ-|7 ?Wed;/ʋVxjT?¹5:5w;u7q96'jﳟۉf\ٍ-g,MpMBzP*d[m1X_k,c#yIW/Q<o޵8{rXvpUrz{ަV9GOd5t {`MO_VZ|Ll^{e&pИW(4d*0P<~ܓ(US\^,+Gۭ´ەhp/G`jnFg4Ğ:5s,qMڐzthӺoI$_EӿYEy=MUaJwST6w5e['2NcjLb_ťt7F=`gk_hJ[gϴYxjl7|kZ/?nRV"6B?ʟ&n!_t. q%[5 }QV~v-2liԆޤ"ba_-s| A<1R~.?<[vh- J_[7ie9dzN']N[l$[\0]:b9=LCY]z0KV1f6XuՔNڙ)T>NӲrXgd%4~-(q)=t/=v2}x4]Ml$(r;\IoDx]e.-K빹:p>LӋpiZtK&>fڋ^JKUJo~ރBUP64TW|sz!_ǶҶḵW2?q3QNofENµ-{gLºSM+_oF ,"h]@e8S8 Wr8;GsƲF#cC41hg?#8!lȥO,-8NRSlsuoq~]6'w~6,Ọ1H-/|$v"82#^Y_z,./3tݓѶ{tQiiz)\U&/E7ºL٘[) pha2c{XƝ BnsRsӃ' " - ~ ӴٶX\hD=%4 -+%HbeSR筡1Ό ->9 R/C[zP @ڗ}ڱviҋᝫf5}ZOY91NBm y+ڧ$իv]~w 4.}:Z>/iy -]@{qrJ~@VS*aӥM=BS9xM:,[Tot_ ()~^@K"wJ{΁Y-CwhvӲ{R/o=h‹Ŗ [ε;=KR*U{yr;O3SǷj1Lc -RɬVD OZ>tzfkC:[#"33aBW -t!j?BmnݮUt;qJj'ڹl"46p,.F50N勜+/ԏWVB. tdf);VSO&u eW;I^h@h$4U:`QQۗ:6N''icu\Oʐ$n >E#,ziL)HكcZjða 3P܎wxQ.#݌@îW+`#zVsv>.^`8SBze -Es3޽>DD|X&+ЈNx}{`42ԭ&`3ڴ  ]k'@]v -zUzU܄ý<k*N0nR,6u.~M~NS{+Hﮟy(65nSym,u^Fglҧ}I뵱N3YkPg{W-0+VȖVHCI#~]Y#blwz/*%b)nn]N6ӺMvn4` )DXWcW;7'G2UQxyQr9dyr)UIX%H핋 /ʩ7X0n\+u RlNy8EݧM1*PSmuKiu?hȩеנ\|\%x\Jл(* ~j}Q ІBz#$9t*9ۂdk/yyL3}S#87G 8ݭ+ VXRoBx?I%(qBTƹzpux#ZzͶ)te0rF2h\>d/&?|R)r٫9[fb -W$ʍ^U%lvuQs>M>C1hd̺':p)|f ?ʘ2~SǴ|t!Œ)㯢-baB3OmWR>i.od\N'M~R*;)qA,OPƤq"Ͻf Q˘[W7<0Ì|z)JOY)ZJߏwkOޛ#uNs93r]4gD4WG)7.=Fd[@I@ed =hT{Z7տ<6ϰS<=+Shz>zk/u͙kw7b"2F%1,6HH)R^4wZinҜ -ug lOjp~51T^ӹ0F{qmPDZ ަ/˸^8`/`Ɯ_74ߊ3"-x串"ƤxiJ<>e%>;/go [-|WInzW]c G"箣ϛ1 A먶I``ero[|y{/??1:4 p @e|HKJ~!s `W dhl9tw^bٰ"5l{g}h8K\1~3Ϋޗ^b] \fVB<<H.G2|"'<nk:ڀS^Y=8-nU׹W,w ], 0o$Uie>%NӅcKӬ˅s|kɼݞ6U[ԻK_ު[ܲ\ێ605n2kdͩ,)wR(dnތZo] P;2&ڥ΅ou4keM8J*&h]qeeTw*pqɠlrsb6]ˍ\>뱵Frь 5=Q8 4$ƕvBیk':K -&ar^ˏkOpJXbkPaT=~aņf'RQn"<%iV| B8KF cc2ԏ'|!KD];o4{~Wf%d"q=>z'dpG;ufu']8J -/j"ˠ)4x1@}tiz%ў虁tC12׹[~­֗]*d{2vt~,60'=[֓GmT>yV65u -bff)aԷx8~FezBN}[:_,v=mLoܐ|tsӓ-Ѝ>l,R`qtAG_t?W?+::SՆ- ӷ>S^R/MveJqa^.\zȃv 4R`N07F@hpA8 tU1^g"`'qH0x.Wy)}eO'z?;?`ZmԨӵɧkw6kAa/~eYrqׅ ~OUdHˡnlAa6,U!\8rkjvx2FI=Ǵe r%eI+zmqv}ҙ[;VPm-PxOM`lݱ/5uw9ʱoITv%YjK*90reSO|11؍zt򢍑-'DN AO(綈G -%G6Fc 99P}瀌Ꮍ}lt,a30h9McJԧ9SQ69/\ievDznk5'Ks7Ȕ;k$[Wb2Nv+Uj*}yѤ}9 m r]wUefv kLm#cڈJięoRQXvT'H<'r [w.PmfS*mmp vZBj=h~VdKQ5qK>1.'՜LCH}| ͸o7ى3I&[eJ ;zA*7 ($Iz+:cՇIFw 6!P|B|V%SEP}<,2w1y[ 12}5iⱑ!Y?F|;_K՛aAςѷ6)@(S\jcJy2gdQoAI^/^D_Ee@>>!}&=vm.*޽V.׫%vKM(qhޓTFHѸ9Tx9XHs,LsO7HH$ܼ3 b2~VP=ܣFo^Tiݢܶ{bև׮bB#s~o5 6+V_=1 (]+QhD^8J] -A8Xo$ xݏg73 -|^ؒ(,<> -? -~L'kjq$Ka*d˭o:$U+eY55Ngy^qsWp8 5i1Y~ڎ/v꾦UvZ-d21gz9wyw3jd+6{K֕9"'x῿#j]Tf%?^ˇ?T(\؂t{wv6ԬXv+2ٙ:N`ons1{̉xu=mt4Hn;c+/!W">"|j*o$LzeߧfoLQB'$!-tLFyc~p6u9!wA]]|rFB/g - q@"oOX3\3 cQ5uI7+\˹Tw;WO'5oe3d~^V_θx"ȑXrƎ>P?5Co]G3ZEEg赛C|PD3Jʜ̝s,?|sjfv9PǛ%c$˙J\ZJhmw4!,tڅQ-U}) ՛F#S 9$Ss/ͧ"㊜l- c/>%OlHY_$K<3Z yv -`.S}d\S8+hti T:$uJqlG ґ ü{5\+Y$j/_Fȶ֊}e31=;ag՜Z˧hS~\u$b#8${E -yqimoجJA+,%6ޫiܹ 4$֏f5s*\w@Xm9ݛLsm3ɼhhLǐYuмR 9 ^ afp]cv{!!^pl cl=~1#Σ+XtqٺޖE[b@Q -UT`${{a~@ғ! טՓ)"x)!ÏRY_tj.炦bgcM~/ Vi7&~~{#?/Uf^d4?.w-T9l˦_%x|Lzbw|ZB+uPp? ͗uZU6]6؂̷*sZ>vBƺg,}&Pi$(+;`;˙x( a`l٘ܟ7od -&f/DEWk6)pYRM Ȩy)Q+fTSm&upK7?i~YJ'"!p╧hj#ڴ)ՆLQ君*42>Fq/+Wn⯼X 7֗`/cgȞҒb5c Yw7%eqAOw%lLG{ o! -鷫(1],J4 蝋y+1a1ձ03dqHTAu-'zL.@_ -mDP7v"ed3 -]7/]hZ|jh8ļǭXգ5M4VmY//~7tߙeVuMxTP6-\)ZGJ3OS +&ܭ+y4ǚݾ=F.1r*֌"Zta1MyԚY6B|kp`aF-~~qw@ca6a[xlt$G|w$x9{',ixw%괼-e>kѭ[-V5Oo2xh[gvf uakxe}5suP#Nt8o5IaMF-=){; {2BΞJ+zׯM8^7jk5UOExXՂUmf 8jG;xkj\LV_-rԁrj̬rJ(LT? C·-{̡t <*\Gbx@ ҇^ -˹_.RZ?dg5ST͓Q6RQ8GP7 X=q~ 멖t#Y < L*65ݥfa%:ʦF ;-V}k V8!7Ќf~;x&}Jgd~dAYddf:@/4l"`+w _ ;u)􊍵;T ̭_vp@iC:V:V}4cNg$"@5@@cP$H.@|j<2?m6JpSO*FYQv - x\Ў& @i(CteXt v~fmF;X)#tƑuDW~I>4 Siۺɻev&yKͭ&⨟qՓTB c=$ft$e >lW+I[k a)=Os% tAJoD%onOލR=yE"I/,̨hdYjݖuLҸu|J\3%#n\ő#}>7]]QTHcHv4˷&\u2E;_\^\+&Lu_|1'^IrR|xqsS%ϲ p(hۿȄ^)"_ţ q!n))`6js$H)ݵ\dƆWOa -M(?nW'v~0kaWs!GAzҎ]|jz^WSPrtAMCcF GQ ]Y4\g,)_ %ģLzGJϾUk]D4X_؎u9Sv~z -oݪ֎ZQos}Buv琞 Sxk?dq6zrNs'Ls[1}9 "t?W -^c9D}@^ -;nt1XṽfhF.7Mre;,R 7e8]fGt[}2ȂK4&4_i4/i~px yv)8͎ wh.vчؒ"z8$QQ 4 l/t);|>?W Y^uƪu+g/l+ݷn~d14'z:6b$?j6 -i**5 - įcE^^5}ibz7^ښ K8fz(m9~0p^~P5g=ΚB٤ev'֘MnYN5gh-?d_D=0"(; -pEeSʶ~+"%^$'S.ζgag´3}l(Ee3oo[s?jT.C'#Z9CR(B?c]S1 W, [G o?-A{z1G(MM7qM]Έ/_K˩dc}A׼+p1ɆhMȏ[0Fp -uCrCGKٵe04,P_6sczbeUc{o04 gmtČ٘Y@Z8߲11FK й|>S6:jˍ9#CRԡ3+Z˞}"[I?yu:0׏_W/WjS֓ne6×Lo{"fk+o/_~INXziKyGC6$;c,2|6ωiȾ`HPXˡH~&}py<ܼEsl^Z"=!7Rto+:WfTF/R fQ3(Cޓ͡xՖή$bG씲X[fcQ PXKR{"N=7AoUmR}.\%mɿ39+rk,MrIzp^C=P:M\hBKu禉bn[)`.LX;d3PcieX6Wyf$Ϣyg0!8,Q +#tg]v5Sf)bok-甁>ik[>ig{'6|2Z:Rh6u"w'wcNku;TA?W&׎| Jax.C{]0pWxj8K-+QWʱ/Og7bS=p@M0aE -U|=UiY op@I)3zPƭ]ؿڽB]N u oM)8w&7Nfmi5/fQWѹP7TnHS;6 DU'J8J-D$u KPz: H=pB{e??R - -<("?B3ȧsYEpu(=k0uwڱiؚ^1>Dzۢ±䮶D^$b}>bDRI1η<2r_@r-t~EWf:L -[n7GeZF8;>[D::~z }X6J 2-c[F^hE1|+^%?l疪0+@s"'(*APfQPv;5:c0R~e/4i^N,H,[,nY{՜e|՜gЎrm~~X2f>_ZyS8j f լn|3byy47{{\f*ͲY˲C@m䁆< 3&k_KAGu ZY$vko'x4>rŦg=?+ȾϾq}h"/6={f=˂#D2^x3}x>5~f}vĮ[}P8R)L5{ -D&:9>|rRd_eɣu&yܷyEs1|~ėcE?Kf^Q6ˀqZ[_朏H!PP3?S-M7! ו߮v3@`U`&rw _̻/_4y *˥r6ێv䶞dzZXjp4NkqG!-o$%6KS> %XvOyo/۫_m3OE]x35j n[!iZ;Sxkuqɾaagyph10I~&ĞlxWxlBM$^F띨C`dkxwɑ|`WrL6vVfNFAjuq?Mu)ʶba.dfil7IaRnXmZw?E%PwG:,lY k=ge -)]`ݑlS/CS\;zPwd'b:B1㚳SFZXƑ"c{#gXՑ7;Tݩ%ɌX v&WhI[}{0w M~ŎUVh^MV7;_a\GGfѶ/w\G86[R[5)R_~)grCS[o}0=|g^öCrCU, #SW7>~1>?~X-cuQi>;5xKd_>>A܋R/ņ<^V{Tfq6!C"x`I~mLᣬΆ@ y(o/U0p +2b6ri%}r3"nC~,*̆-vkM,@N1QS,'p-7l,GӮa W\  - USw=FW j5ǂ㺳Uj#ܮgD)kJ"REm9YM;y}e\͹;}pɹ,J_3LÛ5bpNIݡ5cJm8:~{0,ZOW*;JݵHcMi:B6H7AgXOW. V]9'޿={</kVمQ=ZxHo@K -^-!Ҹ$E|i!򴷚{A0Wr Ư:u7aLώLLUi=bG2lת/" ɐ*~PPݯtUZ;|VYϱʅ99,`U2ƴ."=m4e:L( q$9/1lWSX;okn[e ͧNrp -(jҌv*. -dޑlaЀ"Vz^uY̡/tJ4)j ͐l*$CDƒЦ%.5$;ֽK~y4[EKKib"Y>xW[Ĕ{q %]ccTb&j҈(\E#ҍzG]6~@|UH/:CTA"P`N}HK/֭6{k^=]HI큺.f+ިϿyG5 -i^bi̥6sAT rrV#@V~kI伂 z:h.atPRh??"  =fgշE4Miԇ n f1=afN[#1zQ5Lf=I2#=}X qAxY:,jE}Cte -!mc'ccׅǕ+ >_zH{AOV%wq)/Տ9buMD|﯌m(.TuS&iMd?A >Ӵtmj߷;'t(v(u!)! 'jmmN!ʻ3EIi0j:kކ> 3(@1eh+)# _ca害Ϋ:w:phƃo+[--xNpoEseFDs5?~7ӗRj3Vm$싋ܜ- -9|9*ne'+N!^\`[vsVTL%D_Iظ)oynz<Ѵ^׿޶q-%S-m)+%efnG2N{|b~^vxViٝصeC;6#R:GݐP;Afp I;L6Q䪃D*Qhg8=Wr,c ˾{ۦ+e evݧY衩S٘`AV9UnǠ v%fyN룓dV^ϲgeH]rvoH $H.hp -(kVk\ھ䬤&t[~]ciR佥#GhtXGx(̭)M煎,4|x܊EnY1Z;5/"(vӶp -ia2? pC-A*g44~A_} _o?`|Ey5e~nU.FUn= -P]#dr.A߷~% 8'ydb9^HKg&CTFdzN!nO|_ H^w2gg]yom >z~JemJ @}!@#"rhgZ+9 @GNTI 1`,ѽ^F(N4t(:A8S&ZUi|9nPpmn)_ @JwzA4́?,l)&TtmW9 `;bN/({[-e=usg9> -̺9O:x\ʙ.W^^j|M􌚇)'4xT]}.ہiH7kٷ^SL~ -N}}u#{G'5j~z t1W) 9G"cVދ۞ŧp8Mh zۻ7rxߥw״^p(.64.X\nP>̀.R&CIkE'??ѾGIvVY:Ƀ{n9|BF'=}kO{]ݸ`u~L&EiL#8-TNQxaCCuOj/*h+생샃Z^ ,Dy?6Md!z6Ϗ')Jޢʤ@͟L>:vMltIP#9odpg>9.֩݅nWzټOeGzy/=?p^e>k}VőYwnb\A4`JT_4p;w˴`}\_*mL4FiAgcũ-y\xuKxN'.@Š:i| fZ{3-R|ZZui,x wgCQEdߗ 7@Oz ~KI@%%Wls|HDڮ)Ҙ=<,[ ]1`"u#K[Ӌc/򂞷gU'&]G37SfwpwG?xyx?sXq`5R:V]rŬw}_/P&Wڹj )fguC~-LȾaPg;|g/y]c}l \6|RX%v]]N}nAs2WߙџWJ4SKK~&鉳_q1.:Xsٵj'?CQ=8BBfȡV?/B\şpL8L(D&VZ%Ul9̂%'$>>ziX`4}˹ʐ`godz%n/ -EȢxX^?QRcz@zÀ?~@=U"⏇ޔs1};coDG9)rz[dEs^+@0? g?=??C - I4[Xc&d#F_a!Nq'ԑJEږ9խk(\=x\  /x31N:Pށ5Y&&>כW4-Hu4,#(2ɹnPGB5ڙ~֞QPQTg9YOWQV -?dKݛnc/V[,w%tjv9ش|3Z>Y}<< l'2kci4UNBx^)ޕ)*ҒU4:RK۾$ ݎ4 {/?Lj2u}>D>>i;EMpF[UFz_2ƨ-RAS$9g f#B7rN콥N(QJ| s>!-`{(-KfSV91ڽFzխN6O-uq|"p#Pq( 6ީr$]8Np,z -dBtrCyQ\|>sYt~iqThTgP|ٙݛǍ7`>A`.Qaѣj^bLj*Uw %N3bJЪ` s~yS/r_br֓X=Gn?~I$w2eΫNdP˹1!;@ca`yP4JX)2X:OK/X? -?R?[V[|\~?"jnlnLGNVe7n݃p_vWi@.ޝ~ٙow,/N]W765{12#EWwHWqr[CZ'J#Qn>#[< F[ X)qi*Еi.h^/oA㣉Q ٽrp45FxPW Z iDvcdT<jkz(w-R^0Nh2;`Ž>Ul)V#ē3uv (_D?# N5(q.Y`}xFF{'@Z#y8[΄{/sy-.,JX'm.m0[739>oV(sP~' ~WgH2R̙4Hj3l7 Cx*;//NiJiǖ(g.t [GW-KvA.@amlHN!^;kĖ+%|,P*5s/:ڿU[j'bUJ=`ܺҿķb-b-Y 4⤷^VVVa0}0⛗XNMFp2$"Ox_pXOp -P膬˨.sJ?*ѝ;5$t5UxRÛJiy -k؉d6/ \>%L3BMjI6]#<'=lռXy7# dvck؝WfhWyzK;^m=00P-! OQ2&iY[=;=!X|pP+(-t4Ptp\ȾHKM xtzKWH:fq!qNl|q&1st%.}ɂ>Ba:l$W@mfJ|VgBo-P rhi"v5x|A:+ƒ]1;*vHᶽ"(|o-,-7o-G*%S>G*3A-Qk0}>>(f̘NMb'#))0"H.>م'N:~m6kаf7A LH+@;l4+fqO7jC}V}w)rPkYyQ#ӥ+Zpd`5wzDs@F]kܔibAc& U˅b]zjb}׺^ j1S(ʔb - -^b[bѫVϠt^f+&jUcc#䬊+X~j7?`9K?Dzge ewjbȻś F[6~@Q`+`nA(t9t[+&iϗFѕ"=>kLi:.h՛ȈexC`u} -0KsfU3*[CUd;) ~@꼏:A5p@G$sP9_jUo m{6 m낟n=K7|5ÑYua^b\S<K~`KŇ9 `Q -Z';"k9!`YWp<q -Rι^N ^(k tx4]:j:Keo7auͪdeBQ}֊>KBq=Ee.$weC} xI  K:dȟr?X)fJj* ZP PХ<'4xh -4=kM b-f϶snƋ8J@{- -.FID+;@w7@KN~9 #B1Fw'0h0r0-fzC6j.B:z^7O#695bΨO&4k}Q^*5Qr±?B#-xlov&I#Tfw*uRĸ2]d%S,Sx#Ĥ}Ga }2fժmutGD{{cJnR2nz64);^']\l09>L!]NәOKbسS?џ}4ϲw1/Ƀc?t=ɋ$iqWoMk@ -/]lSgRA*)sӲ#LNES/ f΀]{cI\dR xwQV=[!/V.G>0U5tm?!cyLn<]<-[8ށGẫ6x'7>3wr$pv}i乹x{0Nyxͽ]j]mj"'>/TE?z-7~$7/4[fQD|;Ľ8I>A򆾵=?^ܼAλ¸U ͷD5 w2<'o>=Gbg恪[./5-E3Կ\n`qٓsF<9{ۂjyZ߮V %g40@;P84`RKt5wL/S>z -Qb/ٷra~(Zn;p!<^M\fHP'qp'[^;\b/ } U8YAcfϪm8jejδ8,Ⱦr{*ԘfTVҾS?HKׂ?yA:u6c }O2eﻝ^[Y^qE꼜CzpnV+6ϮfP/7ÜI6n gkVL|ߦ闯 esO!h&" yY#v%[L༘U>o;Dj4=:/'/k=vP2oHlzx2}{=Ҟsv8}F,Rrȝ 0S'ңMX Z}4 k;6> U _mq|n͛0o~Fl.n,Μ䋯`,ѶNZ,AVy'#cn /+>ĺrN\/7trG5ҝWVMx^:rc7Cu6ˇp:s4ae]0Q5ў7:lj\HݸRނlЃhfrZIbCM_FA͘MS2tWm??paXCQ"O,on;]I<+_LcF|nirCtt{ˡ-!^DJy/|,Tqߋ1DnRDY7[mȵVa/׈ %uH混tgj?SOzeE[@w&b51go}^YUV%Xe#eu"+Γv(%#D b'^YYke$ 1Fl\z> ܇;ztc{.@xGͫ%樳6s~3P8. swz= k?ʢOv9;+ +L{ -/;P"Wza\2%4b5)<$j}Mh)az-׿zbQbJ6g-y/*]*+Rq,tɑ?+<ւؐ;.Vqۺկ+AUڂ p%;/ߖ3:x&gzi'9Bϗ58Br@3㐧ဲ'$/W _DN ĸƠ“Fed4FE(Z{+RO;Y5:Ӗ]QW"c}7b{zdtP %b%2"̙6z A;ݘ=UQIb -6՝n>+~W:6'-EvtgG>VF^LaV]b`=:Τ15FdӎFxS' -ƭ*iDv,:G!͝7IywMntn8D̷^yҞFz;s'P˧ڽm-[~eO|oyqg)Umuվt48IJ'f1KR:)^ -8|ViatL+wC\[s|to^ւ­RbBi7ugX˴!אZ݃iT»ߨW{B oKrQXI;@U;U(G;آnu-ik3FSxsNE#MCN'ZkTج7> U3RjHeTK\ݭʾ - -OSw I떆:=+;:1O&XH#۽|ehzsː&֪DZ -A̪̓2͞xQ   ZwtإCw)+CHo!vEɌu lkdxrm_Sd>ډT:woqG@qf!nTb3qrKPX gMJ(Ue*I|wQHkgQh^{6r?~EQU`涧h@= @DqءӖܔ%_K)]q dQ;t~c3;5 &Edw9`CSe @6 qb~y=YKV_IF>e?2(|,/v5r;^8nnV;7tBR)Y8V$#&e}׌q -`(Jq`]=_Vqc JXOW;)(3|ov\/*Chl{Z;V?FD(5p\{Oge}50,k w7@a -- - ; b#'mDk,B[nj:8:M 7^{ήWMݶ*Y.sTӊf$E F*@Љ=vbq -#lW) 91T_ [/@' GQ'N@@.ucM?v~GB}`"ZU찮aW.U@I-eC= w?w]i*>_R~t7o˴\woX/`O0z9`&9g#|26PR ,ZQfYsm ײ^E(=S{Bxl/e`%>MS[gܮмe[`ۢu~\pvLx&p;K.ÿz7^ϱF9?tϼx -E apֿﯞ[{^ˎ^t^krv+/L&?;'bljOCuq{do6%[bl'[Oa'o:3vrvFE(b-F5ĥ~: -IK귯}صW'vnf1ZvxL^e6,LyvP__>ֿTI\{MByOQ:3 -G -N)kSph&46KUaG?lv_qv:H5dlh!s7^'7|ҵ.F=R -,RAJ-8(6wf9iC:YD ׽-¸?NE)c-w'QS3EHx (lmqW)].ުډ2>ar=&~/'IiDfHgw?f{sAco&jc9/]WKJ`(eg۹^(HUzn8AZ[ZohV {~'/QE-EᅧɟEu#Ϗ +ԉ<yލ%U_fv -u;fG9 mQ"`c>`chMO J~  6|λy^|\Lgjߢǵչ۳uPfUA5wULVtNϩbi_>i5=-!M& q$/_ce%@ά / "V~嵌~F]W\==Up,;Hmڶ kNX3Oxӛ.&ƓNJj0u,ix6`H9+}Tζ_7CO v[܋|UUԙOG;fY];YTf[uĔ&_U: ]4±ڢ߾S30veh}ov/lmZDNCbPkka71R7&a_\ ]PZ90?EyUޔHKU>w4O2imKؼe1D>#wt.ݐsMy>f+oC^hyV3֣٪̧ƹl kp3"0]R+TrXS.uG:J!bIeKiR^|T &F _1^ͦ: u$՛w#K8U%0$77Ԟc`mTn^5+ -)Py8$9n[R}N}{8䣊y(6..{28Ie?FY4űvezl6nLwٚX[܂4[ɔ7@цȱ~kOU2\BvK^NrKT<"XYGQ;q-t( -Y/QyzW# -ӭĒ9tfרyZ|oX:iWI&:c}IjČr2d$?Hy%-pUG:\kGFծНဿc_yXWu_O]Y.gM~Q<[᪾.Y M -z?/!<{c)e=Pkǝy-QZPy\ ^BH=׫|Č^ܨ&8ݰ߉b1°i%dނ_֛e%UF _?Oصѱ5EŭIL +jLCLF]JIMlK]$W :Nfެ 5'>Zܻq=Y;8Yl)v-X2f?3$cmlsfmŴ+C:zwiLDou{& - R['D).,ycF)$+\eW}.==*M,9Xv7R6[X>\uzئImN-[2>7N4qMckCCePC)">Ϝ_[{R^ݒqyR}2OSڂNSe@̲p1ֲeJ6[IW!L5'Il&I@L\B\ZROR*HRIA|mA-@Ny5Eu[0}֙~h  m<˔vͯM%ԧGْi>iMaԔ6jBv_@ ڋ_D -] -֯JxAlնyⶦ I|F SӤ @4=\ձtJi1XuV-PYO{oJgr#j -㾷AZM #-1!Ƕ@o)<܎ק?/?BVeř5V(,r،sh WԱJaʾ:8z6xHEt4Gg.u'sdPb+?Bmx.%c*fЅf: t?T,N̔Z>aPSŨKS'2Ci -U@y׾T9 ~WrtQG Q7wq -j ),+`.$?L&y'Ӽ~м;jZ@&YM|o&׉6ܙk+0HhsDOjV*A:E|uR,,3PjOڳz^r'V)CrV$2M@΍ eqv, h?t;㉺BY) }~xgWFrv7)[%205 ιR]*9|u44 Pϊ "@2Ctt\0](|&F)XtꖎucCiT͜257b6F6`aA^o]eNQe;599SyV#An$`%.#nu( LsK3ٖK|{0,yLC K --' /"wL9kPcIGFq^mﯞε" XMF ع!ѓ) -p8G 6 -p-!a7fzT.3\vzxK}gܚNV쌐7GhFk0hzi_l p'i#K - yp*!j@X- W ӟ -/Q@` a74r@Pj!ZgRIkt)J7WJ^­_O+z诂fX;POϠ6S销(a U{3Ё4@*w{̯RBK CBK̻ua}E}8-n׺q'?PeJMK绣Q $/ ^rDf _?%.(Ϭg\gw;a7BDBQjOsTRW;'[vX ~@ӻf1g@;|Mz&/=G =:hcx]x7\軺J?G*IFz6I=8#1(O -v;& _vW=_IFZuqSg^<왷zs="mgAZZ)Ѹ}&M7=[nda-Ո\V"4GRouRvuV&:}I IR/^+O ?i7N~; =?ؤ.!FDQ[~;e{wa@sAgbՋ&VS|K1u^  F q~@X[V~x(&&|F{61]^~Sx?s3/f&1wkSKm&mcmg,\hDX:|\LD+yF/R^uZ]z/':ƪrkpm{xσKJ>O©zG'3Za4<^uXBz`nB~P쮔ȹn{[D*eEx~n/vywz#ZU$?Pאs>CZ}+ѢtlONLCr<.%"D%z1)kvBs}uP;Op: ->h(=^Z)*%Vs3Mb~ߍߋ_\)W~hr_W|#5PG"78hzԋCp}<kG`]8}⹵@Z`^g\~vg8I>V]TLZA3? -{Ï *ّ'of`ـ7觺9o;qZ$8WWqۋmؖlYgdpO`lVkV8i'a=9CK{h'RϢV#&|zЬ6Unݰ>wɼWz<}}X;PuJMitΪMsiVl 5ȳ׻~yhSN9ϛr\KA^-U`xA3JSW+g1m/Ŧr bh<3\8v/?.N T_cK׹]o=bPbRA3S40J> xEZVzUrgI*['zp(b!zp/Xf׎X:10b< >}yJ^m٬,]f3nlX>բ6}QDWٲKK6g(;fT5b>;h5vvsT~2/<섎e{::>/~K>ߨ][Jzfl;ڪ<q .):+=@K S*_mWlvSlHBvLg|m [˶ߪڙc05gq,vyi"9nTK*Җq‹U=Z'Ԯm. -d,0i7:. ʼnWEg?]¯ -|;?#vBކ02vmgF&WlW9%]?F'H;2Wٺ/4sxZemr0xyTpПs߳oNb }-s -c؍ӅZk 'cA<\Ҭ(wd $miϖ |ȥ%rC8[/^pN^lIQaŪ]v1 }Q1l$j1>1.Tr~DgOmv4'e;p<48Pu2]OH*jv#bFJe]leTS̍jvNeBX@1^fԮDA\N`JD;xDr""ٳ_9zv|lvmWٺu 9WV;U=\[;M2 |3EB{kF5`L\'NMՀ8=A;~m8Ccw4ȰK~XQՁԫ* ߯>WoЄj=l4&&W\.¹ZT|mJY=ǝ$T#+!I)FζL0޳a["޸*nkȧؤjlPX.PMtF_~kP(0T?r6/n{sƊ^RՂxG;Kȓ8lizvJM6<݆ٱj  -[f6_*&:U)o1Zv/.r(6;!(+eպ"tg1-M ܤ2 ';O0jF>+*e_.o%QF)U 9:Ҝ:aǥ\ޖNQ2r17"|5]Qf~-~c1/uoBdVCuCE=F6'?0 :ѓ*wg8.ʙ)?:Vbӫx;"߶)Iuxntxp8GG_/ PYn9VnaQ^s Cal#þ3da}$d{I@Bg|rf@bZszy#Ҿ[!c RVg(ԩUPs/WUG.e>Xu mn4mgZ*O7".,64hqȊ4]w r_@cyh3(!h @O>з`{΅G` - 4%߱菻Ȱ9ZT[k3K.c9vcևʔHBBGO(`*`F90*ASڙ-vؼ0>`Aطw.eˀ*hw1Gsx>G![UxZq\2c~$CVȉxՓV߉C -Ep`F>&?5x_ɝ y ɿAH K !]/Ox`傦 ݉̃JP]탙k0o\Z[I/( movzp#נVi@~ڦS"US X^m ]n1Ζq<_QBXC]^"5s?<ԙO_{v%7y%gu>,i0_or@o[=W)5\gaWN1jPGH_wM[jqw7&i@Mꭤs2Z!^b.᎛¾]IITdua`xM~}:Pur,Ջ]&g^NXNzǽM҆f~,fq4Hzx0=z~~0Wi`yreNV[  iWXvOcaE>i-һ^z\tՆ3fչ)fNRwms{4b0\>f.zz[z+>׬0ia@x7 y>s_ Nn nNjգ]<(^kAo- ܦtzX:֮]cǝii|Z}d~lw`̼I{9Za_jZuVo LcY05bwIg2ۢZX+~PƯ~)ʸfP2 ::j^G.b WxAx_؋ZZ5N_CCﬗY*l8nPH+3U\;eRi\vJ."1yf%Z?\iF2/ͣt#KDn4BJ{U -~{`+_s^ڥ< r?z?KhB.ϪCT f eⰎvCy[e,t$#;r"jڡX!-YSʰiv֜2tra?:-Uy>$кz&s=Tn<펴YAHdwNAVeEm_#R>LJ&8Ke4z~kٕEBó\'lq%pjUbLGl"UJ֚){kcwNSfƭ_饝Ch! U8jv6Uy-jn{I+X憻邙}+ھ&&I;ɿt-p.sۉZ9E6Z' 1Z(-0 -cՔJ7vQ yڏp1e2{f%)^X,hGLx8"p^}8uuPwos/[AKzy"ŬdLb a:nqY+;*W24 gUdJ[9hY<Ž,LU7n溂SH3rS:+ -@3:kO6g> -ԦDS~.W/  ^:yԮ*yڥK ]\+27 "t Zx,6b,Ҷ^;ST {Dg[Tŕ=A ;,CɶD_I1U{1@Td̃| HK 4$=roP -1ʪP.I Iݛ姯bY%i\eNL.$\mŁst}U8%:RlӜAo?XCc”1тvBE eW 3@O@|=C'}"6Kj*6jiqOp-Ә[<l֭ĶgX.OPde5X$ +=$%@ֿ ab2zS@N 39dBF <Ƞ}=p<M=l E~;1LW[,&R(q~ +ykPE)Cͽx@ov0xP qT]]@> -FUmjK=N-E@+@ʰ ͲK_Jyu3kkgHVz@TXU σ}ə-E.;CCh| 0N=;`jf]2p r,9 7 bX%x%Yt/ۅ:MyW4__ -cPѓ U,3_=W˽Sс [s>bux_?wGKj|O3QVnncaF.b40d ktȅʠPۥ>[t~[d?xVЇV@OƼ?ߚ]7' qѺD"7Zg^:/~_ endstream endobj 385 0 obj <>stream -C)fK.~k#>?qon90; !6ޤ2Xx^n˅q'wmJM&I=>MR&l㈆pŕшG柬3%_7̵h/&)Wzv5ݰ_m8Р~LU2㪷a1_ Mcz1+? 17Hz2zuC-sE̜VF=9p or=׬N0iJv7]Yv" -^6 d+lN]̮i2:ٝ S A2k9I]5>?K-r -}f>{M4eR˺v.{:(w#xK@ΡP;6J[]5wr mK&XsߣWM)sf5[R F(8c͍kc@}tMach7 sbGx(eVZm v1L1|to{{fDhNRpGٽ熟Ja>"^~7'h~KV՛ܮ6կnR8dȻ뭞]n}qAتjjZ^~C9~5h6Zƣ bu:ű0r{}zvV{]tyg{cN{96$z2[֡YAE57m Y\w^uО}wZFפeח64Z uܽIlT̺xԝOi:Jt\DQ&6,4=aA4|f)z;Lz{-?Yb J$:~u,vQzåpkuRXrrtK gl+0.,;Us[-]=P pwi -haeU'F5]TksCym2 -"7|WW^!e8^A]:Z^f=ZN۾5WP4rIfqt}74]5♧tNTۛ<C֦nI.4nuG2_To3Rq#OجwpƵ*Yn7UW^Lbܕ3:noZpP24^SbӐȕ6,;u-E BmZ#d#V꽨V5‡p* [V5Sqˌ'h3e6_,R.*6Zx:h}N:y:2/qE:gJ\ΜX,VjYMXG=$3uǏ/-P/,.<+B5C}p g~@nlg ;idJD}2)cyV-d?$+y beVk>ɱH]y]Ȭ%5'(қ}V(B VjM04x?6[ܗ:m{iݍu?,ա %weJWc*ZF NT3 6"w{+Mm&~j+2EsUsMώNAvtZV~1SWutOYkzu㠾xPZ}]U{5 oiͳ1⢂xؗ%>Qͱe0gb kLyأM[7R]ʰ(W}L@+iHEvIjq ^RT=ܙȻ^=o!@DWPH}2}sV&[{~&|nL!O~\i"d)S΄ɮ>UV[r>fwΤ0 qnӂ8*8uĺc$wӽhXN3hvUG 71X u{mͺ! jƀvL/*m'j.󺽨Γ;-O~R#$Z5M?o'rx#hVNzg~g*wLKEv˛dW@2Eҝ\\+}aM:8i#jGGTfdÑd&?qNDwɿpyq&zMVz=k$&nL/6 -{2/oUng9L"}517Le "v{x鳊w@[l侚IJ'@8sB!<n}u(9gAQbNŜeky -d$cM OV 1N(%b,NڑK\tZci(Ozg_ԴihҿhpP)QXۤ'aUEkmۼv\Rj+^薹9j7$zgZQn{zt'n3 *<孞EV\Yxdf$o_MD&6AO݈RZ];kuz.A˗9}۞d[JN=`"Ҿչب0_iCF7C?pRvA|QB1wn(ީAeZ0c M͋[5ԠJd\Q6̮dje0rj7d]6;kw!E4epCMѐ}XA^A)X"=VJy^u[t֫\_f)_kbCC/,H@FCOg T XuI6#$*<"JNq|O~Dc4IɍP}}!YLm^Õ\> 2k8z/GТU{ "nX oKNj 0Slϒry@'!gsNѐn6[ub[ËST᛻R)eHºGp]D^Z]P.`n0Eqb,:1>)$`QDIz)>=Xg X\{6)f!`>A:xʀrVmwjN&zYc}#{ O *`);.>:_=:vT=G pV.0cpE\INR\Cp98~N9xRv&d|JXr!~bX[逸t,5?yZ|nq|Փ;i&lH ?qũn[FΌ~:BDþdzVxL J'e 9&\AZl 4; #ί7 i @̏L a fbUzzO8ށ6Y vbn0'1 J" {bC@ݐYoo@߽No ؂]ReY 2.dپE&Yݨم-OG}E؇gIVcީb]2'`{܆P$>nYWY[Wgġy~8qjFԢJ -.Xv;3d-=|}6U荬>D.QvQrx6@EOΝa>d3l+k]ufKZ:fsB7O?-w[z*`Z}+eu>bO<;lw>EĔ~MC&I*6LJRGh5[`S|\;ފi}&!p>8fi٥bQܹ@̻ٸ> ӵsRt|2#;P|q*L4`_s@/'`xbO{>׺`]˿?34,DZ)me>JϵiFCb?Re3"Hl"xyT\ZR Y]۰N*qHVȀVQn5A1QeI\e1?٘_q#W]2ub;S@kwTtmڡ]eMeQC0Ox[VRnyv2M5bIm0ǾPxSy K[uQb5V8ݚ_1=PC}~@v -Z܈r]m\nt2waP|ogxT- _ݣ2_q8s7u3f7=q7Z ]qV.V%]YW>drȗ%W r6vtǥZ.q yyl'>\OKӧ 񱮦Z_e՛r;vրGR$m95٤aN$T,E3+y])vOQ4Z'x)|`w+Z]NR< -/S'O畕1K5n?)DZt!TݴwYqig{΢vMWO? -d"9`)(y]O;O!tvt:pm}TUo2xb~1>7VO"jW-uh5?v=T)YlWbqmcg;4\@8R%ùT~!XElŨW:p&AZ,/,LkO,Ab -UbtX1xzb% mDbuit~M/mqm _;IWkpeְc)t[1t,Tf5&u5H?_ '#''kyW+ńA/;QO6wihO֥R˵e@a\\fΓiF/X͚91+F~ف؟E~Ć77/ {l!sѿ(c7de0׎du5.zZyGm LneʪGBFPqzF0 mvӏyo{9ML2]Y;U{{^׎yI^LVm!ˎҫ3U~L[paRK[EÙo$'(7&'/!LY+.sr9ۦf-\(ΩeI{1}7*uOcJr{VS`HuEB1 )<2lbig)wuqnuq͜$t'$tNm3v(ɫgln[st_1R(`_~DC榊+VPIS`$T =q^EkbhNժ -WYTRsj2mӭ5W /N\}[d?>LJ EfIV vFSs΢<bA] I,JXYw@HJX0I( -@2l'A?;zB~z,lLi~%Y\ϳqXEVZyΫ)VSvK7*q%aOX#H(ͺs% 3_ ϭ; kh'>=̶]D2ܒpiE#ExT뀝KqVm3N1M/ME2zkjJ'[|k69giLrg5rgad*k%Eh ?B߳S\799c|R)LYo4V-zhZ&)ni~=r;yY/B'Q4 R2N€Vd., hըx.ހ_(Se@g-z?i'0M},Z'ͼq*Q^LBjh(<NOWaV=4 1E.S_J\E 0va ,I00Ma){?fUY3p;#| >mbKMuYgXAc wk+,xK =*`  Rg'PlSl3\`MlѳM$Ozt&nߠU+Nnc+#")}<t^D>D`OBpl pfb2\5\Ynn'Gg=/s*x}, Mvbsv,ɯKmW*Yc -snB-vI2}o):xPGMzm?E\o=96b ݶY +3DDS >@XON!s -'Yzra}Nę㝨EI=Y[O{ y\܁=l_@0OI&2~ݶ=Nq;2@F,QuҳGw z%o(7 :A7rCvzmZ!#o?ZW;jP*~CxPَT jt[~-*Bm TNFĦw i {. _awH*WW;{'W22 -bi]zF@vt8EiĻ_OlR9NfJGß%rÀ -Uv԰k+*owΞuUĎd |k];Vz 5/r/\ O¬kaPrڐ$m)\70taey+ϥW@'* s7gy-ΰq~gqeyz+$v?rn15K -$VC\3CYJxV{a|,#_}*ºW/5r7xR%XѹփG 8<``c {۽g?uh6#t;%&Fcxτ[<ѱIQ&HٟaW7\6x6w@Aؾg8uYȳh' Ү5Ĝ:GeBֻԛ6ni(?[kw^jsG8ӵ5F56GbIu8:!(xU.DXģhEi:bq9PD9]~Ņֻko\Fe]/*y,Wu!S;̽?5| 7@Aqvtϭsڪ-J d -gc']bfְeӭ.|JueRܲYNv*er B)K>I/vswh`v ؔ)|HS\/V-NNOwR10qY ϗՀX%Im@!ˆPciZ:8ixFXNu/^1J0.KFr]}^>f-zxyz)YM ڻ[x̡ylnt 'Ůdlt|) G%6 43G"^##APuVrmD+gԉD (t&:pu)Hf"]ik*7)LG1oHQki2,I61%Sgkpsg)W:6EҜm=FJY7"upDl`k::%+Wm,?)~~gE1UʴL[}XU) Mrcd6;Թbٕ^?KANY<b쀕) +7)朝+;L} jsJzE@)&/bxc4M6>箿(`nKjFˡZL}W! +oꁣͽ0+ vPo>>`Cfmfvj4w?1McV"M4 ojk}TpU3J7H1Bқ@0.K^#oJg&݋cM\uXei7#7#}~cqX'N7kYPǖa՜AO}_~I;4&^znTr癪{!)7y Ro{J f%ѩHH3jxDD1DvxuW])fYt0uۭK'3UkVfFx M:vH[=w_P1|C~FG#c :&`#QBS**A6"𛁶 -ŦƳtˡsL@f|CcǕ -rOvzɬmYkW篪^˴SQOUĖЕhRr$g.][OHXPtҙ$t 5Yq'jRdž=ii'|R|1i ÞLlZ4r0,dJmFﵭLU1w]SL&ټ&JD /B)JΕr'WAykwyrr{_}afO hIfg=:b:h@?6:SitSwt]y*l܎eZ,F&uuxg؊h2TI\N{Pjp -}qu.SdO ʶl3J -[2-3`tO%t麴ULG),FO9bH >﯍8gw}YkI#k>ZBM׾o ݇Xp(Q(y+S==s#~駆6S}(Ǵ5_&ԧRc)PeQp؋j1-WbQRhbRmJN<x-wUvt'Gc[z}W(zɣ nwQ$pϱ2zϱdSY)Gk -ӣCQfDnnEHH,v5 Z50a&,ƙpZdC;|gs3˄}Yϲ"laݖwP-(2຾/[X;Y; "+cX4M!'RnVY6ȴqHҪmd4Ȅ,ì=+5xfVEدS L9cgڏSi~Bgg`MC _[+{ Yl"e6ۃ)Wn{~rxmHq!R ZMfn1hR H=B=9 yժExVu +iQ_|_X6w44W2=B"2Ӡ`-{l` -X;u,o=sz3F73gĒ n9x1v-h:2ߐvehڞFд-ah;<wr13FNC`*.5wYm) -H]k-<%*2`CU <%9P]G.b>U#qҬ*_jbY,T榄Ǻt{kTiV6s55Ø[ʤ\l[oowOT!{`r"\}Uz n5G [b7yLYd˄}Z+L>1!An;{Y?3cx5/ŬJfR|RV*̀dwsJn_Ž f]o?A䏔.EooVubP| 8eR|̧:PPFhmelU>C`f=c/2sEw\__=9TpgQo? -?6ܯt^Tf)=K=,t^bV?d70EЯp3H,<>WA"p=xyIG}fzG˃w_XA;,aIzr[zT6\\ |]E,XުH{@ ?f~{ ~]yRfsNgZ$ ynA&Vm-ι*"hy pųM։[NJ]4^7w4@cbXMt0v̿jau5 צ6.< -M(o*e݀JY_u#cIgF3o+jNTwn­¸AU6RT{ؕ-ֳ}BYjmBzul%^]=/N֦Rp9Wbbp!'GLJގ"vylvxw8{>t uQ:"nvBI%ӊ5<εul+2ZKy7RWZWzMև,ZPe~l٘K߇p6awKQ^zG {:]KA歛5n<:ڰ!}Wn-.f*5 P]Hu5ιy!.MzK?g">keq#Ljgb_.ߞOnG~VȊv \Ѩ~x1EJ#FC,Iz+'rK/PrψrU*emYevQjQBήVg&&6ETTUti*)W(:.Pv%ot% F8áCayaBaZtB쏄I!ĩ1f<;r}?2I~zדEҹygDbV/#*7(1',r?ϖѱ֔'f"01< >x x#FC:*K0\o%B\FME\2')cRw`0tpaON`:(^F(6~FsNT:r}uElr- 'uݜ X;wZɡoD>X&mV*9k|ݧ=f]梻VDɄoVk?ٚ -̏ڱk. уhyeTGx"N*E_tv3bϨUǫ*Ǘ{dd3s (Q79/m)&-v˦pfPo+PÛUJmLN3hBN'1W5( -̪dVˬ:c"%M.y?P.]k^xj,) -|ya_+.JGM7SP'Tȩ7IK|?yS\m7UL2%ehRY+9J>8+x+Om~m~zwu1Wy+{+cЬ#ӫRPS -!_hCL@:;O% -C&72kp3ͺph$z2&.I61w*/o~ʚBŞ>L }gsG`t?q!j">w9Pe$P?(^u_Σ撝ZE O$oF4^GD8O#ݑC !#|ܮPȼnޘZm6H GS, -}9N,y'856le$:~ɧaGuclHqh;s6AZV=6EGa d&܄rn!C &M1ߎsɑv6PANvSX+GW -MOD0i!48hƑGM"foz(d݁! ɮC9|XL8` Sؽ9dN NO0S y7LTz)o(#]RMe0}~Sh6'O;к@ȑ<2l`pqjB14@, 3-I -R4ҁC@ͯ|^=q߹\m@\ { :;g1O6piS3=FA-%2#LE3Rz2`HdCH~@y~du##-@\q9ŧ Ƚ3M1 RY@. Ox(}vIVqPffԒs<\#|՞C;[jEY hw=q9hfh@CPʝ( -]PQ5'ַ.bРOqZ2)VܠKHwsԁR<_[7ZYKJGp5g)A+[c$]QCY' V$,NkI@[UЅtMa}pʀ~N=0~3Iz&_$U-&Pbd7`V S5N+M4'2%5V1%~f]rJY3)ea/K}9`:Ge_oB[;ƀ=6#k 8RRsp0 -1{Y.`9Dts7NDzY鲷Oܠ.a׹ ZúwH_rv_#ݲoW5" ۽^T]thz3RLC oÌ!*~iM?)N\J[)ggtm )tJob^/j֡bs3W#bOx@B|I[bH8@|-4 M ; n2'3CA^-Ѣ:@!O@43Kggҟ|%Q],r -W6&YNiN3z(f"ns@!{C@7Oj|{/sTQEpd䱷Bg LwG;JoΌ_yOrPZ0ڥ@Ko@ d ڤb_MFXOpXx]Ku ǛgOž ;{dE$ rؖ^&%.FJ?sMQGP9GEQLsB1焢T?*bּk0\K22G·xǕ-sty7md|o^Hry(>Ԯm[i1nM=&]Xfu}oE>ʲ>9ssm{WĖ= z$觢p2k^? -?9'8Wٮ^7|[zm&U4y,|=05Cy-b´3$hĸcX't~呗7Gdu@nh?n2}}=_yĺlk] -˸Lͷ?=i+.sM# _&tG`gK^>}{uw[vn]EϜzy[cZ,$~fM4'w5aއ^7+\!:Xe؀t}w(o^n:Vv{h[;at0jp`޴Qx̡݄r^b[XmX 5[CR=^{y"% kÓϬZNW}qJۗMx>by Uj1g4S=|ܣ+nKWHT(P -g"EmJѫE[Xf,T"˨M'Onerr6/n~dեIƤ`+76kܜqE5tȾߙ,wrV}Is33w,;\1_jV:߱x坽yiVC:~&X֙AaϘlW>ob$5xŊ;LZ -{V'j>t1K6K4G,qWa#lꍍ{ybDN-5e,!ۭR&͘ -הe˚cY]I{5R; SS}6W$_؎ZfQŒ.`ņv^=Z\iZl=UevkQNKgʔLy<͚`AMZXFrҢ¢fTʤml6&ո{eES?|}]A|uvpojEZRST~<<$IejM"ᒘyo嚵k|̼^pڂKI-[GrI^I M?Qe~_E]<4ҁҚdջ՞g(SRe-O|,gb\%T%¢6^2l,b< [mM+;TqU` "t "ZO}ɣ碔giUUexJ1<ʙԑܵ*OKʭ2spoEw M^zՖ})5e<乀?pNGN^Ey~Zez6a2w?FSsL5iHy?o%ngttd{ pdqY$ HJq$۴#TlE6|HV?spN5Jjia1S1KXmϫ3-оJFjΌ|ϧi,J+ẃɞd8zI[!al6 $a#BbÔV/ ko^b'+3~'m-͐ZDٷ2F.2L$ %#MJqZ}gs|QŎOkS1*p*vF%$k,rM9eiVrR(®{F*-ށaJw'pA\z yr#EXg1s_׉u:XϰK=cȃЫ%hlJh*OPGTYl+he ~ڈ٧n X2Dŵvʹ,!p.oJWjO8z˄ccsNÚbbI︔BU5%: [-BH@ U~]]1$T,mE]ۖ2Ex<>_)ۀJ7rYDĦ;{vNʋzw?ұ$$&NAn|߇[̂`w>o)z^uJ+穌,nsVf$] M'ם;IJ?v6g߿g!b0\+ rS<fçUYr\uN7ޑrJJI%w!0VCpzz?$ 5$HXe79i’@jy U6S itukg~@/>6l]U.uyr~._CC -RsQK(oKʧf(@١u.;'LcdaȺ0! =/Ouw[Wn owj7/hStz{rbi灮cd%σS6F(1(g/hk Fx{Y%=;1GG;~A\GÅf l0_]ձZT>rR0 {bܘhved~ -K(m߫ {n])mw*npj^@֗''3Kל3^OOMU>z(Q6ClFg;T^x̻-JǦU>><ƛZ㌊5yolGS\RM/{6ao=W{^ڶ\TmʯsݕTZhP)So/psR&YIWC;TMF lY6\6et1}P=4^Q~ݖJ׌ eRْN1ZݢM[Ex?vVm>緃K;廉W~;:K8+CNxrحj.5suhR.ѢQۓVBbOђ]mv.8|+`G)e! ?5LbRU~ӲS2,YֹAX@;Дx)G|)K+fI |2vn\2O^z_\|M&zh7[AJp8ʠaZ)n))9|X]9 yY+JR;̴>,QG4~PcWFg!y@g`X sZyCd,UP5h)S~I1e]O׮ݭ0[inCypjGr kLb3 V+I`T C|{&|)vlOXݍo{V1kL΢:VyYLb3_HQkr!(˒Lo7N#[dg+"Rsf ЂĖ(YAAŷsg& yL(/Ee'h+PyG2rO%HnHjK]zj15+ }H޿ZI>0{VwG|,3mVQ_nkqDŽRF塠 y; e踶 V,N1b;}}RN{֙\kZHZ H6!2/"m/l'{ϏzԌhc9sZ=OgX#7_rG#WқsY~q#)8چཀ2@9>Ȁܔi4sރ^.ek\#{L^oL -'c}#D4RX}\lWnFo#<ҦN=73||iC!h7s<z"o2jW/@/q5bf36Q3w}yr}0VJOz‰ʥ;` -k|K NqU~E#O+WeX0l}V7Յul,:I ֬wVS݉*-=MT"7LԺf'hNng1i #>{,7"<B,6iapeN,1^/!.&#W@9VqcdGXbW>|љ{+ک-/mO GG7Nw$|i@?XN"b:^Ћ||ǿƶYL|Pws89!ϹbWgLzN!<y] t=D 1j,l,* |gm%#R5[\QaGJDQT_̈́ p)}1 Ww+5 O@d"Y@N ׻ 7T!z+=@U@3u@- ' --ՏW:()p$ooMyv@ګ(Y=^oLAyg,[; $!NjF68)JL02*4ϟBAԡA*葀fo+4o~0dTL҉e&Cb*7Aـa;C> - uVp[V//3QE -Kp:K"lSRRc˿1n,&?u~qd8i`} sv5n~ӳ;ǾhڤH#jP4l埢/JǁD7/\_j; p -@e[|Hݪʫ"ɴTҎ$\")%"- ,8pl;?M(yHfKz%k6% |>U.2RO9ds5̟ѿ./ƨJ (F(P5&^(-n3nyP(7#^uz**ص7???6I'z;~ۯ72Km{]mЭ -tրN_@ -lh }e+G2X1;.ڣrE2?.r W-drk?1mV=򝭴7V4]E_z*Y&'%lb[eVwIJph[^EB'$4NdZx;_[-|j t6^"OvݗtJ/^馹N9Qy`Wp,,znA()§ۢI6Qp!$fw(q]\% 6{a cibn*jx;ϓ-hwoD"L(d)IZ!o|պ/}z_}'=Vm=9g[oʼn$';ف ndc}3jSe7=k9NPrR^\@CgYVjb3_c2VָZ}j 4ͼq3\k׭G PZyd,-U^WJZըQQTU9v)AR!PMΤ2I4MΞ9ޮ"X"(̚5s p8 ̡lDy F~8]R2| ^[<^eUĔ';OςN+vMDyPP&!P#s=G,F.z &&"Nꗸ7 IF%N(VJF -:MFoZ &:psv9opuy4Q|Deq#rmFXF;.&9K/c+z6;mvJ*Jh\OMJ(Z0jFTf-R%ld&]VLѣ+:"z3`'#;#:97hWgHi=VskWݿ=0Ѽ64<Tj5-.ԍgK9.:txOc&~oea 6_>\og%R3**&HRDqFv-tppca`GOr'rkYn -p0S:MPx~ƭYihAtl^ɥ h]e@bI?yn1&2\2Ә̇QqZC?Zgqr;sۀ׈&28`ډ`r7Ȑg!:5f74@ *ˡaO,ҙϚ)w r|0CېMu6cbժ nquz!衭yܙ)ŎFk0oo=A `m ,X%{rsܘT6j ~{ dR07RG]+NQ֙\tk!;MgryAP`q``)oS9\\cy$4*Ʋ\7WV8q-X]̊(S]Q^BKL%J9i 8B48Aʮ^h~%G\ UXT, |. J0#n}|?|o8q~/vJo_^HwR@@_8S陀YF9bye>7\3A[[j{*Y0L$X;XJI7Y.IZ" ~JJJx/H.)Qc+fzO?SW]f -l#~AHG9:H3t3⯑ڤ,rI_r oZ¿ޯ61jҺ}7R0K^x,wk 5]8Ҙ7W!u?K_pԐ7$ \لͮ ,m m{:@YoW@@1(کW{ 1';η?/NJmbHNI -Iq\×Z?B1ipkݸE7=+'Э tWZ  <xBIh>dC -zl `eʵ <&SVJ3c2JCqnzP/v.~/rӬNB!f}I`V̟Jܡ$\z|_V5xumAkۂæ#՘z0;7EV=SSt& Y5pa{Rc-A.KD?hxY=,IphïV;4)@g= ӹEisƛ3~111̙vKu2q1鎲ao8$՞Jhc>-zzέӈ:j0s/)a,Pmzqڄ7gwυvR{à^ϓw9Iq?o7+ 63VXhkl\sNW@̸f`5nz6Rh Pn6HP:y?hm[f5bou~qUJot3LE^9XT$f4vl~D)^Ɍ5&84q5WEuX/w+n~KVcW.^#ܹ!m}$j)Gfibf -E}W]SRCCdwgɟ^Nyyx]MIbvrVõAfPo3&#EVx +<5Vx$tbdv ]rpJ<$84?K}vubYec v+ײ7oejHzI$s+WָR[ӿ(2X&S~'`3WCcRIF9Ʈ\$v6uS 'L0ocVR`ˢP(UQbѹX -R,C# fVʑ&v -- -檻%R7mlGoijb0붵{΋ Vo;v>Z_6DOlD\Rc6qeqꈹYRѻퟮn$0Dzc2<)_jch2O5X(fH*6&g3#퓴'fB,p%2MQ|cSU4 exk]%4Z0z@Jy$_ k)Mrө}CeVL,M !Q%U(>_O3 Wx5N8Vlw lb8:W|T6=\eUETs<=z*c$1+"y2۱ C3u)hL6\6jD7b7&Ng&v,n V=׀IhӅQqzfQS{%ru%}D9 wp|bpvp|X?>ԵI!>/=&T='Ixf ,ynE֯눠7q uPuh:xGtȵob.H'kÿpiC[j7"B, i{ZYؔZ#B?76vlg;[!AL'+C,[:NܸŽu4`ku4m繠(v+"P1s&I (꿍U==Owc3>aˮz%tɑ˥A~ F6nxzVFVPPfrv=EJ)"I~{LHu/&GW;wkPyGSL"ݢAc{s\QdW0dGhua{^ٛ =ˌ ׽z"E !=!u/{0OvT^wR+qbLKc$ØRFn!(i=,܁ 1c K;EܟP4hCh|( -e(taB'X9*S`cND`X~@aZ[E]`V,iVX y N,P@ ErJ -.]Pʂ=:])`ۈνp9\ת!t8PRf }:I)C3! j0:H,#&BoX=$ - ApdAp!s/ߋ\ &<]MZ~ʺk\CjI3;eD"'ȿb R-ks֔88'=B0| > 4 -亅C2CM@׮ُ o9 ,&7^ZUƂH4c<$߃o3T]W#$R&fI -[W.uaw0HDMbabiEbݻ-AhL/@\4Ť@1ybL<Ő5+Ytcڞ¯~ػc輥&'Sgw+.ȓ6 WڙMV=a@а_ p -`4{5(^{7+g>a Vx_s,8v)(ox='C|.X?]ewZыL+Dܶ%>,(W@3cw %ADы %+XPbgƀ<2_ڂ`Fh %nJ(SHSq(9 E X|>FZF +?d'"%T^~d.Ic ߔx(f@JUJn>V%(IVP)d J@R/RB'JS# -Ko1!5cf*Mdp-y&jJgr& 8^D(=xtc CB1KyR!X#XB)]o8_L#4&/7ސ^ѭi_9ͷҹeW< -oYe9k(;y(ۜl|E4eqO+YPfB*ƷL[Y+O~5" E~uޗpӯJ]*m -gdz)(ZP>]Š|b@reae*W󯅳_kf×ˊAt%J~ihԖkdRbՀOx  *.AnYEPn{^Jy=t;hR~uK/W:Zxھz_ ,9ו۾}t._@A%J׃ZʘᤒQ#ٳ[ނWO[ /ׅ5mi -l:_C fzچz ]l) @n {&6g#6I$tRa~ytw1B>k^題`9>U -_GuZ-ң:G,CƩC>ӺC暈Y ƅH]-KNmXT9t%Lcg0؟+v=<]GikcۛyX=Ч;kw|Ӯ] O?rk!G?zk! x#zGān(лR*Mf5,VOlavz+R ιětUoc'>"LܓZ0zXWzRǧt<73Z٨U$o.^yim('d}°W -ya@˄<@J"z;_Rz1ZhfeZṸK-{?:MO \d; Y?9jm} ;^p$'m]k%vK>"m`Zu[?g$\b,wnc{z`(1]ɘ1"G?w)$joWt.{n8eIB%ZNm &pY[+vLtO=M9Kcv*9ZHnp6\p4E`"2\HPLܴF7Ɉ3Nfk)8Oo *k8'~AnrwBcPˬ^,\Y^lSȾ]]] .Znlfº#%bEݖw ԽҩYP&EM_$so%/b|q51-f˻aHA?Om!fmO:3|_~Opߗ\?U`\˟Tr@?g˅_HSn_` w {!/'ϠtCLp4p܄`J_0ϔ8¼v-48NOB@$mߖ͆orď{syXtV1[{ NBH{|c1w!Vʓ覞KFi`Η@-?=e6**Cʐ2V!VEEKeFun4/`#?#!CK<7I{\溹R9ƨ'[[9H➬s7tg ´CwHHf6SXjMJ~=GV=u]UV#u UV -S+.6-5d;ľ>c)~͆_}<_i؏+NuV?r) \#pɺit~`9)'!obsnMmjkp%NÝsm.ӦG:044p;X p />Ih҈|#!{[?%ۅ{nj<)X2W-l4pkă+2)Lw)hq0R1aH'ǎ v 4Jl -_M&TbTuW(C* cό=l6y_c:{GiueTb FFS"K"w^#m'D] :n*?%bf(l}TL3~0` |hT=+Gm6F>z;{D]5;h99Z1aI}P*)u[F;#RKZg}HmK^t)|%G/<^Ӆш18 w0!I8&a:{pܞL0]`9-/W~_t_Sn_`w +E?gO r/|-:1f$'?W3yS/@}>}.A/M F@XȻ -X|?a$px% {D\x+/*$ą6W~|܈Gy ^zz@OXNQќ$wCL#8"7(n@]pb"KGjzb;^u0_)ffng_U:g6]SG$M~n~O=Qybu<9%j^L7]Ԛ(eՖ[gvM$u}(g-J*/;`sc LP[fPZ9gs[/w W {?ٗ?}~oBǐueiZ<aSYUigӮ[ٓOL8gnw!Q~͆_HOP $)1r5 &`? ~Ke0Q׳$ GlR8 [ GV0w=a|ZA>{cQp?%! *?8}m9tު;+vOmEETYoIu/[43_#~1oB`ڃse`8#UȰ-{7Io^x4sW`fvT#=G%lzݩ\L*SR[}wq]# z;tO.Obxơ>'3:P& @^;t oq{jCAjRf{s+EbSHFCwnߍ`]x8 8:VqaMmd+V#An 86\&nc)F|#&Gx '@(Lq(LZ䙵K犍}ٶaFn J_7]O1fC њxMqCjR.ZgYF$yJN3.-5ΡptsI !f9ĔxD OIi3+1Ƚ2wL`NvLaw[kvZQID҂ƃ84w]rN8{z'r%JdC ?%o}Dq5s<Աebo]Umouw =\M|c[Fak]zm n=]'A>) rz\fJgUh]`g@{xڴxnfӉF_hJN#[v\} m `EA#BXW~@z._(\kB4mzL(ͼ%d -2:Zg6dёc A&$s'S= Ud{^%H/?xxXk3ɤ|&rѰ5/,~ -jӔ=NAop׋&pdn"sDLFO mG.OajݤR>H'7`Ŵ^z3`FKeH؁^M@!ԓRF -Se>ܽtZ{DدHKc<؉nnQ m-5lc]>Ѓ\4 \CȴHXJι;հ:AH ~#aW_[wz[`W NV&}:>xpAq*a= PD@yk\"1*!QorZ:/#A:/(G8;]v|tKbݤi}smryfKǚ,zl!ҵF -LloaVlݟcF W l'v5E+ѮDnM v?G'|+R4Wa[.],(z g{Sr&5h\%*3aqgwJB7ڊU7$ GqVhWEWӫ-򒛗ț:1aGX\ORGA;>4,nD?×# - -J*{^FV6U:Z/.Im*EkӋ)=zSvZmS=t:mQ2Ӷ@@we#Ֆoq(L*-ݶ3m}iProyn]-=>ן\~nɕIz{WxyyX^YmNȇ޺m! RtC>jx'R1;r!ž?@ ~y@k#4?Ǟq2NݽbEf$}Qyս4xiYVchyG[u䰹*Muk7WYe!b]U2_geIz4m@ v(?S=q"Ҙ6;EY651FJ"+ܷ~/wu"V\*1q3%xW:ɽ25֙|\fg?w I4uvbCK;=!W%rWo-A扥"iF\HqKKFCR)Axw|_ϫ<¹*-Msc:J_M<th|Xf-U4lp]襯r0h8V8gms''͞妓(mr̐ɔ_}iW%8[*:{tytAHHRX*8qKJtXf*;̺$h\aa~nҭsvA6_86Ne|^"A=XT#msZ}QpN~D**ڱWDHȯbCn9r),2Zi:@y~x~_gV/3Q%mR%>FOw:.zEb5~J3bݾNӛW?O6e1ixhhhda1F[bGp6 1&mNm8Qو4Wfr:]f3>fʌen>4q{*#욣̙C%Ä;: Lrt̳M{j U" bX{/΍\Ⱥ%w-ސ/қepM|VᬒDKDG 7#Wn4.i]r4Ls׹*%%?G@fϙ9?^b7wd:uVT?7Y/d[X@f].%!_@)' Ss6qNLmoSmiz|T- ]n;o˃n,=~F`Z8ၡIws)c`xKY1SDG~ªzɦOʭߔVz}}^g;A.Y]7~"nkԹn:%DlFcbD1@ag+S`c[S6HG7ǵzmGZDŪUdur T4R %)s_@Kt<nfLC}IZV/dnwYHkϷF&*;w#zD.d,9Uv$^`?l B9;hiVՋ۲vS^`䶛8W`K==j ݯd قBՔ= ?|%1 7g&=:Q=;&znZ?gc~`T/ zm,PJ Os›Kj$ꑉp"v%!?)?l o0Myt U/1@2);F~ZGZN:(ty>!dq]wZ(jz&;vN@+fM pp08K@~6~V&abRJz!@n7/,swbs#HUѝzlvWXvüuM̴ԁ5Qz6HVy`^m+#c5rIt]AQXX nwsDtʰyS4Wӥގ&jyjj\*fQqlszK(VXSV)?/r#m]*ϧWɦ&ML2ݖ(e +ā(Sˮߺ,aj޻eL_2i -ɳ^a `fp2~nѶ3M~'+ۙ^<-u]XJ u ed-t -HbKe쎮:G1TJBS1wO'E`_iޕq}czzRN%Y& soMdee}wr~ڬS`> `_s_8-pztYI+1_TE$r۶j,c\$q(mn`}3-մ/+Xy{y¤r'8~:bΖ$_J}3r)C4Q^#O -ͯ(ja[訿;@L>=/uu8u6*? m_5 zQM,5=:ϱ]s3 - ҍ08ħ2Uӓlb#rzvb̬tSw\gI^=r,GUy -ũ|>W7;@8J }fmM뇼5u%0~ �NEPp\+øz 4&FǾS ז ->~GKG}?[+>IdhDF;)W&'k8hh6>]u.}U$=oKnso:HS45S_8p|ԡc1N`d?wsھKqLe-֫FvNU^"ma={>-y\`0[n|ij>ysRϱMa?~vzRsDð=\jӽÄbdjaCDžvMvzi5T.b>v -Q+hϓjjgNE<Նf{s`o~360C_.gshT_K#j{(G7?!R]1fiyЎ|a6 &AYRtU꧒rκw?%/P 3~C~.)O, $TUܩCnQgE㠝GWK%ۣJ$ZZ^l=@x; A$K~nӫc"?hPxy!v9[g3hNp:t,TUS<*(w \%h9>%~NT__Vђ5 j29ڹgs ѩKJG7U/d /V,ՑKZa,,'"Bz'3 NA83[΀m~ 2eN8SE"~sT-W.-WIYӋ썑`8 ,?~ZUAeAb^k̩T_p4; r9lu zKK[VgmySlW06{ZV0S򆙇soA'&u/{W(Hm^w|`"#EwZyvTr|%Gw W{q_KC/@+˟?k4M}c>oShc:~t‘Ҿ[4bWDC&nfڌv&9,9}uRb]q~-bvSiuލ)f~.! ~>|H%fjp1~*yFf*tt4{6ճvox3\Q8mc(#ӛUDu5E|?oֻ3Luc]lcθRniVsECd+!cggdyy\=B~e !s?~^aJP8跊zᔊN:m2N|j@$(ڃ$yHFݪ@*KT=y6U8V7zUvQ;eOַ856J';jpV\1&NJUomE% x=^hudx2v"l;l>0̦iP )[r~FX8emNm+sٚfNsGqJkׂto|X2.uCGR]/?-%K@ݽ}TB hzXNҟ!gYޤt2Q9D -yHEc*gG ӌȉ2_+r+|DG~# *"^?%~le Yt/*|xӝGiW1v<ʕ-0A]g^k]r9ԗrxKY0nht#)/{NZ~7𵿌{T8@꾻EI1+/Q͞ڜ3)TTT^uUץXQX$ϖv>KrOI[ -T1 1sT(q7.&VCn9qZZԘ[E2ջ 8. `W"×A,tny%8_,w|3(+?O:SLJ4 E\{S>՛U)iE_fk?6epywJwW% 8dyRbev p=]|5“!C$gKQ.h'\j3Y;~5C1+ͧ/_(/ wMfd|8hX$8Hcvs12e(lv t/>rڰ{u8"(*8"**" CXׯ׻z Nɝ=2oձWG~p*)f-^}8'J2 Jiw<#BJEM7о2ŅLl*hk\!j'BJpIqw@87˲MtUj˥~Urm]w Wmؾh -*&0)pEoRv;5uHKu訟cqoVN?SK|[s@ 1jc{-LET_2ь]^'$_Y.Ƀ0ݕ@99oe!}7fW.3Nbn'VN}_7#>-Wݞtz@X6_>(2rYHyw':ۢ΀ǝ>^1yc3 }tSQeQ5{8]F^Z/Mۘ5+XZҶ_.gHz"o\loI?*5* YR3NzMjRTj.}a´A BˎI iR_Q{)4oҶnL*pH=ӁŲjO$.i'hV}m瓴{|kRg#bRmkLnr? P8=o(h -mRt*~MŇ*~'3O zb?)VmE9OU)|!U[:ƪxcnx.p&G|YHWv^ٝg-<C9pTL#<yV@S$*Bv.4DׂmkMv]1" όfJAC@ X5 ?4'*sVۃcdy>LtsZ%is]ݟBr/l?sndx&%~ɟ [G?X\RݝͲ[qU=ee=Bj'*6Ku_ObN@1_(V#Lxg;W/1EietJ `nM~kdY;)&A5j"jZXR#Il[-EA<BM xMl$AZ/$ʙRP^ Ys,%5fkKtGSPglJ`z/O9 v xnW1$/~G9h |B*.1)UUg2k@ͦTӮ<|ZYjt4gx-&2% -Y)ة~&9}* p[^E3B<(z*ӑU©hǹ]#%9X^Z1Ê]?Xcfj~~b 9rfOK*Der5|Q -'4u!/eX eM?6>66{[^v7+|Υu}5RvU_]Cݍh-|&?c@ \#t̼R:})2ZZ2}/gK4K{S%>Q|HդKӤ-%9y]HR|Q~ jN (rPbn@Eeoc^fW{M:uYHJEхTb >KA`"iNJx{?':(fPNaj>Ģ ""WC!ɸoxXں⯤Ł 1o/vNY r6e'{b7Щjka]iեHU(RVB>(uųֹڷTuAG!m歊CHUQM,ekԃpss5֣<퇺ph**hS\uX&wGJ2mt^Ũlyؼr-nx%熚Oݙ?AFRb'={&-F^^5:SSc<$ꭾ. PrP-Z;"NaaWsKnnIR>Ƙ'|&EHCgwiJR&ɨ<}GqY /ӆSks(Oxc$D͸~9(<(PiPΪի0+^7j@Gzv@лշ%BpUbFZZtI{ntQÙ? kq5ƶ L۫yD;Pfm#~ifl,ZfvZ:9)Dmʪ5ĭGL'S~{%:v5 X R# >0-7Lcf=k48_;im>fFE<4e {Jh^y4 -X| jc|82 FCL~^~i,~/@Ɵk~o -pL!zof](2 3qdW "h^6b>igECg/13W##]:Iuf ֵ"o|!# W|oB@&U<{SlN^M1M5d6*)Vj0Fv;ۿņ{_YiaLZF.(8O%ni -?K| -D{+E^,N{qc컷 yTOB/W6[H^Yhxೱޑ+!_U蘅 Uk2A#CrV̧;n=3(kJ|~P/p  IU9'cq6fKh2ݧ&!0WycΛɰ(U3t|ܮJKWd~9(,2(,PS᚟xd -&j/3q'q!#4+ h:'m#d{c{ŀfQ)̦׼"Z&g唒ﺝ"; )糈c8#մ`#YhJ}!oMp煚8bD{CshN y}6׀hi2#'/]w@$vGt_EBaq&`M{ߨk?ZIY*RUR8p|~(:l2ayEORg" -,/^u-S8J"P0醠 t +j㵂(mΥy3%q$CTOknh,OTX9OP6%@4P€hȇWShxJĢ!kU4gb{`R0EOsDz"50zLߐ~kNjvh>ɻZ>(K+q_s[.s~yѯt;{zvgfI/Wx+UHZ른5uZBgS%sQ$KU 3,x9 +'}yEy\o10Tg-2hmGoo(VWhJs%K -IȌm*7] dUZՙ(dc <+qlYxwy9aq4(ٽnys/f}FF±+wLPYcE -iqR\a./#8W<љ - #t @\^ۖ>V>EPYPi䣹;Cd -krp /UPc[]t1l\0;e:KJj:LN`ԹSF;6c &t2xɡ|ׅ? >;ltXgz,֕oZZ?gwa˕wxmޫ>TћHZ:B!͏*`rs-nsy.ǹ97?Tz4)oPDO=`6,5ϸfPo0}%⹣%_q_~$M`m0ʓ,JaG6+2@S%N/p0xXP}wA9vvz](VAX^Kz++bL6:5ތ5 -cy=gy2wy#E<rnu:yjE Sj% 8'h3jzk~]x組:l2sMl O*XRS͘zL.䶨nu;-886u"-{GrIƆdIZs(/5O^2ݍ5h|^m2/gZ1 uKUdgA(&'ۃS#-i ô&ɶ`Qh -Oz$dfѐNKQqZAG=F\0~b[EvMzg:V羽YG' -)6/I<' G[+VZdkd>+!R:*eR<{!H8ܖJ?ΈK3(wZ/j9][d?ugE4lԐ<_6#Hjm9譪cg[GYy쩲 -Ch^%x3I݌&>|*<fV"Hd=iSFTaGkY>*S0B+BLbJF-]p4aN^ -/7l˶y .ĩ,= }>8s/mۯj߾VU[J4MiޭV?O{U-UCg /4h+5G@[6@3)-]<;/|!q\Wfռޟ-FL>ZyDѫ2[qtjW_?F M#!(k@ P6h`tAb^[ ̉Rի$腦:H E[ *GIQ_޴O`?<'c1@{̿,@[z:x}~9y -./fq]+%g&EI<`CC;O^9}Via>.-u9ՓmN<6_zF@E(z?=-=h(}x,w<'&fj/$tí^! 64$BW5k17Jj3T6PZ>C`MO b.#'uELkSp(aPO@ 0}ysjp\Y>x>"kJ!]nـ`&s>qFByܹ0eG}wP}Yi ݙ+|L5*Br VFF`OGx!snZy755zs 5Իs ^p?0?դwPRެbu>$~vn)n _"Aga,7~3W1 -%j s\/bl+P:6tg[[j0K_b*Scvc51 ?{5x䗟Š5hǰ'w8rFoTmNJM} /At6{UthkM/ !SPۀWzh~`en~.//;0 *:(.@Q}6^@f6w"-܃!GPwT#MlիCHͲ.j'L^c+e2fјd!+{ %(6L4`쯾[6m5wX\ʾxzv0 -Ѧln'[bň9 řc6.eey PN./o,6V@b,~DT%8daY̞|i[)r -6{潪*̜ۜ04 xZ4ԏ!o_ȟk_h J0'}7 -O@)!]cӱOX>꽮z%)kf-U;"# ԣ7tggBf}qcGQU"_/r! n:|pd(~ꃽ5ήǠ,^^涵 yJ d.69=ޓd0 TTwo+mT LJ+(b g{YG`a޺﫶(;E8jvzp*By+]i>d6v7\nDƽpb՝ \g|!CQ)ztAi-nj1?,}=*x]yu%--0;V2]3w- z;4Gl b1x{#3}jtʾ:?`& -5 |V(%.Cݕ8k.Ƿcxr]&or%g3|6獋3xgfRIft O -?xXU7[̈́3N{, -2wl4Kic#%)Il0^UI٦[fϤ>MسKlS6:UPB3#e{wUw@U7JH<+gӀќJ3 ~C{oZRTq\Ieܶavq".9n<& Z .NnNEuOc,se3@9@APPUΣНnT(4&ċ,7erVty% -}?%#6N]V*='QThQh-ҍu΃xQBPe1 @KbKO&hZ[6ust5m驇J/R -v~OguL>_ 5aYV}/ pkfv*7۩m~*EZ!ugi-sz=&jov {䴺=]DyE:ݷo&+ƕ^wF<WV F;?2ӭcF@ &R?;ؼַyh|KO 7}z^ӢE+U}ViU҆?%bfSG}yrO\U͝})g>uu>c5u?4[x?{$>Ƕ',=d-uνSƹ0xHS;뤲~4gY4+ c5*L H`YbYvէQc?Vy Ele\@9 -`bާeoQ̃Yhiv>N5Sb<J"4XYlm15GK5g$ZgT([-t~Z8{60O:}̓򜋓r ݺ칷9x4o֝Рwf3nY%j1 -;kKͳ] w_,;(Ps@bϦG/$~Z3Qtn}ݔ{VR涟̞W&Tu&-eCzɶ|TIM?o|!.n7X9Ce@^}q_{h^Ώ}σ1_-ɚꞼV ``LvQ]o~OQmA%򝔦)TFt %VY]}'AAWg@ ]%_.~DLϋhy@{by=3{_=J>]5MǨ%.QYgB=UM$vfکm\TkSq9nг3ֆ冷~8X6K:mK>Yf݁2L0=o㪅ĸսj6.zMX:c_` 뙔\' sq-^.yqb 7mX( Mv<7*kxJl^ lp*u_yN,S-EְTANQ'z>^v~Rv5AZ"onՃFvTo v3-FwLV:+L!ƍVaH.yK^vp]>5ϗEab\ȅ3[86t#wڹZs꽏[n[XMY{z#]L\ cU 'f -S^:"*kY g]o^'&EV6[BM͇˟Y.3Rb?h7REY֨Ա_ҟF%lMXm[ -=GrI,ENR ?tZYIn`hRq+n}guZiSLF\)j{WRU}XV||ZDw - !+|G3 @%p]̕?CZfA`&i#6[ `@CLrؠIḮЫiDJԵ~ڔm_7n^?$2Dqv"ߩM[SdV.w_ױXqYP R;UN1Hk9b?gv. U:G, Ճ}yexujzv:nTtl_4#NnDֺj?t[Q ׽yңwؠ"( "z'Ygc֮Yx "$%{|r:{e2Q@UyKbfܲ$29໡pc| s-\E -a % ςbg&xs:Lr"l_G( -XA h$@x+R[#YvjC-dj6T;9A ^ubQPڱڦ_}5`k]:=@4(fOQ4W?G@_ /[Q<r<@Ԛ41Pk]Z;@Z8y~@_c\PQxS!uҏSe0<wv͠m%}I$ C6O*sjZv/Z)6,V&Pm^ZkEuCהJjT֠xQxǹ}Ӆ{鹅v- sg:& s3TJʂ: M@7(ݭ '9m> o@ds 02@SzE_>m:PkoV&P)ve"_?Rs8LP]r_^1.=@5lP.ΕV^rnq|Z' v= ڤ:7j<am PғS9:s](CBJ{?u[z?`@{н&{(eCw?s"#Ƚh~`Y3Oyh H|*4?~B(ιeC0~d S0e)=t7C3Xgy[¢f~ vk`G]v5By -@(@c2Ƿj{f<QHRRKg}ţOpt}[HP9_I܋ WJGJU-/lg^,!oQ'&i-3r9|-!6lcyM<ӹĩ\vsVv{,^ -!fR+O2;NP.ߦhW=GRk/{>stream -<"\2L+T1+mmπO"@?K:[[\BvDU]Q^{sMB8%51O*ONJ.Џb*w]l]aLT 樕iHj,O:V?3"y6>Q/#Bn-7o=1i6k~F{_4 ׿;- `>1 f6*-ɇ>h -JfS,VuOh\`5conbXĿy 5֠sg nPmN3PmP YNW,5']`Us,Fi8yI$>SJTI{Ae(3zLB y zoEP_U@ȍ Ҫ p FTku愰7֔Xޠ8XkW!*">[Uyj cvДP#qCbj - x Pk+֓y3v:P&E!oP2=IETדuQn++(t Pxc^"i]8@v#@ c&DWnao!9jBs prqx@Su -`}[qo̿iՠT4+%Zn+}@PwٺzR%U/(Qmw@m+;¨>s·\Opl.6QYӊBa-= )ld-87WKSpNٿ0ӆ.M)tҸЀ,$u@ _ 5 >o> u;έ(˰@hs$\|N_e= U>Yp<7P#ZzgxHЍzgױJ&@8v>ϒ'|S"Q2(WC-n]=EڗnjMw*:% -@@˃˔0 5 ݎ -rx:x^ɟͤcy>g-0>n^?VhC^|/w\S]:͙u{k5tbaG~M6$m`|@ӟ @&m ^Of[EcYzoX:nxi^ =drS$R9J\w_`HvZWq kՒNHdkNzڇ~v1r?L5E˫aW̚C@M{ hJn9Vm+߭_Xѫ-8'o/[0&W -3hqZcuˇIE!nrI~cˍzgy3ˢt=7H/V*]Z B  -HWg'i(jy|&I+Pdѹ[qU߇[{ewZnZS̘mlezF)Z5bNll֛R2j|ʀBv%:^$|Iد,%=>j{{L5 K鮫U=kn)j35H4fSrFd|:8teDdm ;:g7fJ\O9>|_]lB{ޱ˖As}mMƾ\>} - -SWVS&*긓'iSf!M!1Vf0u\&4?-I?`aT!OuQwd7R HE6`S~j_?Adה=O+-jF˓X,#̏ -N ؛jh\!O?p킜MWjj4l=|˹%_㫟~ @;} -20]Syrs̆N]}SOzd;x™ԅy:<2fv^uSr^ݭa+&qOOw -\ ~Ϟoр/m5㺱?z(/?$ʺV;|Ahz_qpսGwgAfiq.T6hw'b褲rXbJ$=Koxph!e/٣ j'7b0lwVdW]tY:yV9p盍]na @?eZoNۃPLGD{*gyP<hF6ߍ{y{%r߷wIݚ 'NP{ }-DGۍG=бX!3`v>;HzVٜAqI;}d -YuvaIOy.!ƭ_v) `jv*1f(E~}<Ѳn/twνqs^Ğx; B^!,OV_RxY6@zJHŷ_dgQ}-τ**yKIaQW'X W:(wI :D\}}  S`r#Uڨ.^7=g$[PeeRR+wTU&&q7W\^1U_Ѩlc-z؋N=nNI~*kAS2fpyzɨѻ]\jd+VTy|uk_tZ=9_+=9,z+YاKecϡqhvʛYc̲%u\RƢtǔBkTz}#S:5b'l|v%c]ukB :ZCb#z NQ*gtN޵AAg9k36q]FݿO՟7;궧gɂaw(*ų]١5btaYHR;G;fW+yqKW.fT]h윩 >1hlH0quII?M J6.99٥Q+VY,t^"\:r<׹Be˸}eg!F~W\m/hnO%jn -CL3?%#ih9:ry4hckW6U?T),^B/0sL^s^Ȗ\Qx=e>-6mO-*L'B71%ri,yzq_?G\$3T9ׯ}Ci!7vTccRpBd2%.G3F•+j)Si`z0"#o*?SƞHbj,@O>@%@k $0Ux@  osyK @fC{HaZx^;2.$vg~WXg e0nؒ j~}t_T+P~}kG(2ȋ(qX -s_E'+ }YcJ_vԡsbW= p~XDX7u@bJ^K.4C=0$G&!n -@z"7P.6wաLen]g[xmGU-z0X -`=ZیP}3K#X!?' kDAߗԪl$&wn7~5dZ1c>z":4+qdar [%M^KXfyS{CWbKwAff#\Ԛߕ[Qc.?mhB -՛M=(=]«,[W -ҡ |ԀNZ*`ANccpŽ_n] ̈́a'DqY'ɐmW,4|2 uO^X)̿M@+ M -$:_=$&a,::Khvئu-[ ݠ}UiF'K眃7'ޟ  k -GT2kPO%nT?xsk{voݶ;}YKlD8{XMDnå%ʲ1t2 _/Y_O $r~|c~ɀtN -L='ɖ,])-ݽa\iE\j:=sr -uT8QʢSvw~on=2&_|]NR,loqӀ3'+u:w f >-H(W˟ ۋnԏ?^\#&Q'}3)ߕ{޶چS-lRZ -ϙՐF qXzZ,J&C1'~`vw2@Aw.y 3_+廘|oBto3| v޻<~n-mQ.#[wV}٫!e10GVykM#I4DJdMQI+޾W m ӛyW`j&`6ʷ;kL>7a;mT7/e[oxl]ju)rrhQ/% -o k̡ŦQ7A#hd(AG'9MslReăJ]7)m߼3]gܳh#h>NVqH5zH c}>Ļ?}^I`҃;jnhˇ_ee$N> #Ņ-sY^1; !#j^3'Ӂp5 -Ǔ}p? սCؿcA#}O..ϧcr*)>fuf_S;ݟ(+OU#Wu:&"Ü=Sexߏ=fܮWXGD[_͹wAx"o{ߪ zݙǵ'xX珧~EPu(Y+0|NW;ÌOr_66kŭ9}2-kpZ>Aϛ}r -xvÞG*^zn,9do7B"`dڳ{&Z 2y\t`hōռYX nKuGpp6|]W gLi3D3>9UIlx"t+.vgT%Kgitsq>å`tx=t1sQ-YVQdH.Ҍ]\F+nZgMʒɾuhK&Et3NՒ:@vmF s^p2;7*ƦUe+Η:?)N9u",l:€aQ..~ԎtM'} ^9rܖ*ߧ?PҳrӕK!\4jZDJOZW3GvSdqGkJO ܇m9o-l)=GUoOY -6 34wn ph*/gۃ.Fr5'_6Dw욬1-R7SѰW=ҽur[d>DvB|7R)Y\3m~p.w#ԋ!.7>:GF|9@G+љ)=szhdmJς6,fq }Av/iduǡ DjEػok7KcxVh_Ěk-W|XBX9ibw~b~>rf??;o6f{'R߻^qm i1Ȍ77RR,b8k!.p.a\w%a:X?"*²@'g+B:G:)a\~U%6pp,$3@} -]1cg2uy<3dGr+׽+Vs0KvzĄ -򲿈L79 -\%Xw̢5U~-TV -rdLrM̖Ԩ(c n9a:2'4( -$ұ E:HC~}`H߹kR(C([@ @ˋ%@S " %@ -!B}zwR1@VAѵ8 aW `/%/|[NAyh -W0+tn&@֐Y -P,:U-,ix&.Wj)@5t ]%k| 8tj@^(j%-/qO$=]ğy픺?$VYPUuQ -&mg}W 8h5um9 -nhSbՀx~ѻoyq[OnXwq*TT-<4KV|SBGRk@xzWRg/JI=DF"c`Qa'::;c28!ݡ&!)"g?;:~붋RgP@vz,wçh~ȼW ނM1#)UZ܆}̞_8}巙CUo[-3;woAn҃w#lυs<l9~Wu>I zA Ov%Ջe>estcYAzVO?{9_g:Z޸wLd:\??؅*8}r%@65@jLB_jJ$YӋqGՏ{sJn.X;(oqcr=6]WnF/vq}Xp7YBH[iʩaZ~QON ߺq}y2bNѬ.)ɺdE!9ͬz>0Qkݤq8r*5PAM -ICk0e|yMSᭃYK| |}\&e;uc,Md:;j:HIKRϗ)}\jѓR#6#U0? %j |樓Vp[6W#m1>p1wﴱSNd;:avAB]ۤ9hve]o78?_S2l3jH;ͥv/,&os\]2R7NR1}!'Cֵy6Ɂ9{ -o9NP.7>dٙAO2`gf c;0Gfa 7B4BFaywiF)$n9N6Zk-\K5~ @UOkFK* 9EO[y\Mޤ8&ݻčUkY9+YYo>M@\\ΎyZ1g4]s -ka_ܹ*V.t> XQ}Ϡ&T3gP5:-V*ր Ypޛ\$;siq~ R}Hg",nvgmj \ lkv\HHNai0zKb~m(U򕮩4gyY@\"8P)MT.kq[η -زӨN2bKpb/1:+2l:ț~8QnOyZ4_89[R)/Fwkrcخ=m9-&-:Q-9[Z;j%"J[98_IvH h̻FVZql4[Ơ_ۙnO'C6BS"+ϝWLFZ\ہ>0H(| &LR꺩L竦;譞?~tR%pb/sn;w؎g̦叩>*VױWҫ= dM?]OgٽNmTη`[MmTɇgkrgqvד$-1 -Cv_yoR'Z}@C>z+~_fa;]PTL  *Ίℨu=u;{?i% VHV{w GJ5[cMZʉ^i 5oy국%^1X8ח6 osoPʦ%'~z'  ~;}14 J}k{o^^YUX"v?ẑzz6z՜`;BT6bQ&ҧjtQWmZq\gR8LC7FD룜+ݭ2 ׌zfmyiЬH;!p>Hn}nrUӦ ,mF皽W9UW*<*["G؀/S9YߌvF:+s^ JKר"{JQ 3xPM6%93,MylJa雟f+^*wKf -c 9̠0xg 1Cܽs" -X9O]TMM5NϕڽeJ*;M){b˝Yzje]Yd !Rz{QIHA'OO ڿagT0h%Ok'%wլ؜o\顃\'LXg]#BtWy帬$`aN+0IL[#Jd5Ws\ c(]Bfh}Z#I#ק|gGg ׂxO)N+N. #:'zV3/SdJikٹ.Հr r)]XDvLFx5!)H|/nxELˈdAz Meh3$*eq<@@ҳA?8;8IEAn8[&4N5y\sq2됥'CoV}?8qY8N Do?t芦a>  0e ac ~qH픾=Aq"X{H?9?5xІ_aU..61>hVV@COb'-:{'@"҂}t+Xd!;d|'= Ͼ߻~',WP<{@G1 3Sf*ܨ0~|w ' tge@e PnjA@x.2{AbvUؐ&nmw=z4x?ws|Qf۰jk;7s̊/ޫow̮v^8pxB$,>9a pz З@6: q -/Mm38F{*1R7k{/\iBB@/<[ -\g&.AA!F {fR0wőׇO!8]6q+Iϳ҆Ef,-{x09|g }3wa\boܿz0?>+S \>=9kڻc?oF%%c,@AU|N/cU}(>&Sھkw] .iR_Ts>7jt.#4⩖*}g, =W稹fZCuvD_<{5"¬Hmo~y(x?,q{l^b[}癡Ra%9ԥLdb7=k<5{nU//`G*u -5?drS-> -ᶶ\|oI ^ȯ8z2;b ntzݴJ0O7Imo=ZמE*s߀u]kS( 4̖w'm4Uuw {eL!L>`<]ѹH0+IJ`=6'N;Ǵgn#q$-Ē$o~h5J/G/_>ԃazxOi4`zvk{7W_yBȟ>dvƦv.-8j|: lBӺtgJ)lsCMۊYjJ)vl rT*_}C+ԛŠYna'c'mmvcR]n"X +깝Vf4A_2(ikU+ЊƩ{8aktFT3Кn> 'ʉ9CYV:L>}l-ZgtAJn&c_'p"Dkiw|Bt,u7n0}W4vD;N!"i'?פ=#ũOBCoke=|Vh+EZ튖)ԆJzO*՛hI"Tk+m|Dަ3^9|q׺.g?Dw+YQXs#:Cy^W47wNb3GCgKZӥȾԪ>lژc43I(jF!7J[S(I*Ŏ2ϥJwNr:;X4cZV3v^P@^ Kv)rFN+ޗW糌MLIN|)ulj?Z'ԾTSއzܦծ2Oq3{莆I{k]-Bz Q.w{6B66jc;_ tCRg!Gnw+IVH]1YUtƫnHfmيmƍktnJ$7Jm^7LeY6Ϥ\/e[F ֞|Ƚcd-fRz8¯~MEjb;lK@a@gV4oKl,DҰXV_T]p>6oJnݬP2R32]]R1#6.rlt<A,3Ͷ *Gq,"42"a,VקL"gaB,80)N N!'<\rEH,G7\ C>?q -cH?6=E// -I9RO}@@! s28W>1w7[d\@zȁ gÆ8R',NA6}nQWJKc3^]y6H` 0- tyzYU*PY4Z. @f= !0(@,@l "S[/`:_!1 -iFxzNOmO64 < XswS.} 0a,:~ -8IR -@KUx.BCH2[|N+GcgQ曏{WG4>5 Y%ߙx+/`)( 4 NQ9S=`؁LWX.v;F:Ӟ?lv]yT!cp2<7nntm^fip68k[Taˊ_f~K)ଂ|~!ZߑvOyX;(se*oKZɗrjkXcp)n^.Hrgge_NYi4BCK6ewNߧ~{ޢm'Mos:y047Y)MQF,9ԧ5,/np.=v%k]5nqy$#ɦkF/Eΰ}~*Zyu3"ت,XL&vp:G[jQK.SS-ƅa/3A1#7iUs?\V}6%?wSr_<4raKquyzugZ"[3)Ǖpiton`ֳhX^(-l{ yΧt_he/%-g@$p8 ݙ{qP_P3̬S|f6v.^S6C{ y-B:9j'ޯ!Դ7}IVlȼz)~=KCPݍl6lF|]pdi6n7:V6A<f':h۔{dz1WJ3? -%WM8NV:J E=>O=N.,vsϕeIwbg_=?`Zh 6>CƜt8 J,sU`V$;WVAK2{ r-ɶv5PŊr(_iB<`.zqYLz&+Ǟ;4:N>=+ciR:Xwш^7^omm.T*Jiz_;BK-af[_nb@*/@jw6s9enզ"τ0ia<#f?-_aᕋ(I>ʷhj[w=ePt5;"k4;M=lv$.FI)%I]!u"*yR͇ݐbU<'iw/8PnN}tU_6;]t+<9mZMsH髒s #)B%*xV8u$J85x8 _<^z}b:=ٸsꕬLGJc`lX|ñڤ ŔVu&JRnvFk &̱f ԱN˰C4ʧ۰ka@acb#BgIf~}(^Z~Wh_fYjbŇ~_>KsQj ";1ɵEn<%Tv. ڵ_C uu6/W Kd u@9Kg>=R L7 ffhPG}]qגJ9̓bcS?ph{sV2/CCGڸQ6_,͊aۅa%eq=8tBHɱE@w|ٝO8?#}_ lym:ӄfi[J7)E^YԒ/SkLJۨǪ9_ (qYcr3vLmq[aDDpg/*QTخ)\di`*6-r -CqjG(e[Zk=u9;8]vd )G7]ԪOj~yV>L~K+w;.L7.akE3E]R| ņ'NW|a̵"]hG­ɺ-Г aC9>"C'Wu&gsOFK+˔Zwr J}pbs+N~0! -^]SDxα@_,L?厼 #ݚU@GPK+Usr_1?%\fQEBpc|ET&7J=JΎ쫽>eR π,$~b$# B,#t,|_~?g=[tڝ40$/,-YY \nVvŢ_ - -[>";̱9M3 -&y7:G!zT>MfΧYJn|#\V%|A ENKS6LVD[#]RGU(: R{ -fsۗ%= r?%-2S8,,`Su =׽ī1WDG¿3rxYWN*ZXN0'*qNg٢="b/%2~WSr?Q(Q+TD etyqn侏hD`>"wF ce\HļKgv6t4NZ>2EqٗfD&E4jH]%0N"T/&W >If]Xm40>0ZONm! } na5'gq cؒcྨL#T!cL M2@N@DEN S 7ĀK -0RIAT )a08F{}ş6ўCTIB  z[1WH=}C.DҨC-MBA 6Y͜㏵E%*7\&" *@ z -0pXB|0;8F8/8ac,l -w$N9b Y83:A3;6Ч_检]Qm5Ukw>< Ci>< 3I2e -@^} DA46,."q:ֈX⽽oyhg^ç>+͢s ;x07pfn FUڨ aa«4dx"v`uh7{uxuNxA 3q&ztO&'sޗΆX;] -QTGSJǚS8|v_t+ 'wzlߐʛg?pͨ.g35cdt%$5Yk -s;籸;ihz&2U:`٠o^;/$s;m2Xzs<?nRO'*얍ǃwP|vo/{ϞuY=)~d}q/52Jԫ$|'0ܧ++1Ca.ܚÅ2|RXga,XLjZ,n{0~h|]汿Eo!@;O,SWlPmL-/Nz{*ͯ͝QۯY=uY9sIХ,eP]@10f`rr~'T@~I/QF" -=vӥW޿*/tNJn"wF6lBeϯ'/Gl-fsw<}-\g#t26(l~ZUf2*q4cmң[%GK%K6 X !c0!.c}8] O[Xs[ 4+LMwuwt&Sד!z'u۳C;5JrNMks?>ɛlשO/PaޒAPc>0eoݙ|vV~ iP2eK~`&ҬplOHƯŴEd:Jf$Ug'K.B8`^!C_]3V`5)C1)w"KGdap[%ܦ8G.; >Q{VIYMᱜs>72M2gd{AiG4D_#ޱptLv~H4X/9D[󅪵LKy^< &G4ڈ4ku.h;9`0'eX= Ctd}4). VM}Uة#:H;-sŸ>Bgi[UV[Xf71exM/sYcV* Q9N ->FiyޤdK$);*Kة՗HR>% {TJT$%Z˥螗"rmx৫ue5ZʥS3[ijTSdk5ؐT6gBAۛ5@5:5ޒY| {e />t&>׽ԋb$T1kNb}y{d| H4kb㠴S{6D_ɬ76پ=u9MNf:r42*MδRlIW"zʠVs?Xr|Y:|}Ćc~7"1B!_(}?{g$EngQ|؛EV\np8'V#IӷLn7Jmo1[x{]rUmSWfJ'PR}W}c4˅X" |? a -őMVro0[?TgI7Y?3imrNA$N"u(k8v]Qۚ6LCDiuhup^ѪGt6̂;MJ1anǜ^ G3k<'h Y'?.Jh|SOݿOVfµ_v,ƳxG뉝ֱVfSdF-~ZGKzҟ\0DՊd;̔*] dxv ͍"Ai:&-sn4MW؈Uom`:x J"VtS_wf;v- '(ݫڠۂjWCTx-;jUnޕr/8%0_KOZF̥$hhw=]8׶nf{<8TISi|(zV٫ qRKmIf1U]_}Fi:ylT^O~\}.xp! -ǜ~joډV-Â8cB]^-#,ZcVLjGTC%V Vk^> %o{2EAfY<4l]Q/`^,σP8w[}9w"s檷!cQ3Y$Zux)uV?Q_^Tc"{}5xZb|ꑮJ-IN\8>q~%3DD̎D/Ï;ttf&MP~r7̰$@LKjՃN`ܛcsLˋ.7˅|ųz"_zCҘA)䎪Oe8.7D^7QnAgIڲ^ opAY) {_Խgj^\ߵ$.y,}.Ue6ȢW{$=R9y]_m,5~ eZȼ7>E/6.!ۇgKᓂ D ǝYqTw wvI*0cc-0;{1TZX5<9z.Z1OGEԐmVuE_WLY'?ĥZ%eyψj/9G$p.+/bHn)zeq C+U&ل,ߟ usR%wMt3rWiЗ .d? -=hRHl(tz ԻhʥGu/=_9ruQ>4]xk| x%f7nўs!qeDg{uS=ڞJcۻm@޷.F]S6]s_R`f3{p5\]iܑn.[~l1 zv_} -F_n&E;ozozhgzb9[%ER(^~c֠0 w8>,|CKUekEbN ɒP@1b΂>|?k<ؽvR`1kR]$=wF|hvҍkV۫ʴN.ft -^WugH8M=-T s/\LV:8xiqE lqN ÓP} y;q7\\uYk?sC#?gܬ7c 6  /djv6YHN:|ُƐ7i^ >Gy9ԣ뉼syX\*>}J@H'j^ml\]x v;4sz4#4">cq`-j'7[Ki}'n Zsexnmhx ~k:k¸`5-qb:#2#Y9[b̍Z|4 'k׏cN} 9\'2ۗs4|fI%47\ZOf~ -DM6$5-^0͸ j_}9L0?Ih%n$͌ؗѢUt3k kRU8CZ`ޕ1.9":ڪjI.l\|SAiI9ָw GQk V˖4Zf|z~wj+0p04MTV߾* -*Se }|ɲfe0/NJ;D]0{Uz6hPbp.Ϙ.NGlűr#1U|Q08-uBSc$G -;w~8>J;'I.UXV[/+*a//? P;]m\^w͂0 4/4Q׍N3(*Nlp]Xpu0>9nIokA!,SJeI;vo -N1<د2U#&sqPA#.7›r4DOv1xH;$i옻73LoHS)aEeG.{!!l 4EM7)՗l/p1paE ~- ǝqPrbWb7u2΃ U< #W1˓zlZ+t-GȀsٿ -xgþ`mL :`sաMQ7KR_͸|b qlgZ KchPakz0Ř3'oPP8&m19c2X J^/Ahi2ҍF[F|tф ~τɿy4  M`Gd\_+*{,r$S϶̛nzvٽ˷ EY&j˝VxoQ!J'twP6C] GA Br95=oTB}u_}ϚT}(dMO׏^pɡ+̎KcH<=@mXtv[s}J:msmwZX9XՄZ,7[:nۜW-T[lQ6LeîB^)>ppq z4:DpKnvp6^;Gt4 0ըyYغfٺ%z'ñK(cR!*;*MT˸Leylg8}uN\Fb,8dEӹ{~kzM+oH?|y<0y$;ʢ%.$ ,*3Ya>i,ӨGGJ -/fW=S~`scXܕ9zCQF!H>Kޱ1+w#]I*$Zzr')02b#IsqGXErb%foFkP]c\n$޻۟/?ǿ$<($좘n, 'LC1yPثj2Kw oP݋qq'p~xeGoM vj\ۣZn͊e -3I - -N -[O.Yg]C]Wb/ߎsy{#DgБSe$lu~9v-VhW0p>D-VSvcQQN{u®`ם|>'45ldqNP-w:mO -_E!Eyjњr6u -T\ 1oi^6Ù]~3xsC~r4x7ua~un(B\XW|\7O^gi+-!Ka 7/j >7ykkA0tvb9x$bjy_9t'ͥY'SqQv\.d2/w5X]40]˅xyTos-(SY] j;0{pS~o>g6.M*}}yA܎A"ͭ瓿cy!~\}ãݐ.\5i l|z(+0Egd5'k84nȹs=.3גsݹibw0;&Ctn~cLBqzŇy8)~Kz6AEi\l٠LPi(#\Nցo]|o:ڽQù#A>ƍ)?L>.=C;k}OkA2s^zz֬9_F H'|s.ec,Neꕙ37qp&J87wнsQWԨ6|Kj sP= \_t+jW6*h8XuJxJ+$gR ,3÷>2$q:'Qv^eqgO썆Ӛi#-ӒOdz_QCok зTJMG[Y7Bo蔔ޔ.K+MZb} {M Tֻ7_tA'~x [[I34tDҧoiy"@[h7!i㰢wmbXu/gr祪+qg󰨄(9T}u9T[O΍mgrhju:t8zuoNspE0wal7>x#W@^'+rR&ȽC˼?c66 'P1ӿhl]ƾ Cc)a6t ao =l:DLYsYNe]EϦ-wDHlnX0Ι?ϊsӾzn\e6bQM)PF.wMR*=?BmH8-@ԬHB}\!Wno1£J2T^מ%}uz4'sǺ-{+FB ޻n(p>Uvq|^)U( CDgw& 6{fp0k ,,S&?i0jEM_(Le9zgkTj V3Ai֍'T%.@:O顗Z/42EZn𤫐GWrw3<32nx=eiw Q9;NsP? zG)qebe8N:"OgFo^= Qn?_q׆ۥ9m`c)K귰NI΄;sI -. ->0ІZ~r労/S.gn_ls*hid}fRCd>μsN;@+Pm/weϟ*DjuGU$!Ih'IyPĪ̜[sJ͢U5| Ԩ@(1V/~㡖esT,=`xuV!R9r %ܹ^M19OltfI>zlHըzS\]-cF »z:U%1Qr2:*xw*Kن}l^J]eBK=z|D[6dV/_^P<2YUpKLʔ@4<9U.raW` BXOl|="^ Ay]@W]N{\xGЦevp⸥fTa!C;ᑱmrgܦsv`>npqУ&hl|] _ͱmA8PGItFjOXxvu-?A9wl1Лh![@].f*OS[w[q-;4Vٗ7Mwc պ,/I'r" Wo)˩UsC8Yq\ipkJJ\e1\K1/bb[b+wJT7 - uruK}9'kp-C*Y pJ9VrT8TN]mh{ d}艿or0B0KH.\K PY_!y!{>֖oqP@;Vo3wEgY߉j-gaVf:V}6$0IBkZMXܒ1TB; e^2.$+ ;v~P0&N/Ucw<_ ];Io=IK%3&Val8,FPS g;wZyXfƽ/U - jlv_ڥ'w^a>Vp?I#; %,ZP5?0m[ϘYpF+zyyrXAC{]ymOօ[:ZY':v)ڃJ#>v)gm3ľk 6ej{wnh=f7- 뉚W ..y^ez9?$8cY`Dq -O'Ǽ3OOgl'g䍂ąa,IJ߮)?{=ǒXMq\;ycg,'_QUwԋc>݄wYo7qag8M&X?$v|L[ˢe^i-[f!;ѣ7.{;#ZpGn;Ӊ]ح!zR|=Umz$[*'` -_Ci!&|#R&ȿ$9kϣ!׻X\V#cYmxI">ozq|.lz7x#*_[K=ev*-*t5㚾׵-j4j1;EdzJqdf"O3z=Z(E';Fsq0vTݵ7vYX3Zx!9e>PQrٹfD7MOiY ,mJ0 -*~aY&r-˓E -̞Z -8gYC{҅qYKrEd6+y#`YWQiOWp|6O?`@G_}ZeNZoZ2Y^ >.Nwg# ^/4kiW r?2`[d ן6[I,(1Y$@msF}n%Cw_~?X[fAbHdN$誾\)br^K>gb̥Z^e5RoMd@HlzK +^"9c|tZyEOg#C<1. 6Xޤwa -}]*^wƜ>P\#_6#tu4)QEclG{/=0QAN-`&5)')50R*LHu;%͈Ǹ+;=l f^áE^ͼ+-uj֑ ix暄7l;!{$P:70+}-!'fuwC ٗX@=iQjpfGjVr fu"5#j}M\c#{)vNR<<ϣQC\T(*3QS%Zt:j - b BK 揶󝶋SPm?^T+zr֞(U5OH/KNSxw^p{;TXnĞF~ڴ志ց͂4 uiؘG0ԘvcZ2B'},m~y.O[s?.@݄a8Ykaa -nAآgi)r|mF-hOJNcISTCJ}ܺRb6ִ:Ӫ59z4ۇc -U@[>iJY+ž!9n 9ޣ7Fno7 I9bmˍfz}jydUO}Rڹu8{UFlXA DXɲgLw**%PRF-S"7߄A[y.Y髬@"V - ykԱVBrs6jNW{V\l+ e -j˘*jt`wyxW4,8wpf)mӼ8Yoߞp~;j3m=BIhQB Ng$^:+Fͅ,#aRV*o y4͘<X/eLIRԿ[jp.s:r?b(MAp_s3&=ĸtˡIø)-}Xu!gGݞ16;>_]o$mt*lDmN:J{}3b6O'wϢ{ʐ]cdui'xhrOx 1^EFsJ/$D*I}XTs -o<>xr;3Of+7y5W2t#\F,+Sy(/S(KC;=p&B_ nз{^M,٬Y,; BZS28Xhqq5e_^):58~RȜJEW;;.z'Ɲ >ڌaTk5VC̯r_C=ȬQ[%^>tּ.}Fsdf'"kG:F wvoi"-w\Ot kVWr^dGofY ~b58̫Cv\Fb3Cj,ħ[ɧSe'M%R }:q3)eCIW!UzerEl,O蓼gK]^J3;ry4h ǻ5F|Iu`zO>d~q q5 -Ӈcc?m}iC%& - xW@j>6&KP$ h?3G;'f>^Zvnn`t ;e!W/% 71FNkƎD˷yӑ)!Ϊdf?}uܜ Oc҈-_<5Q:7;# -޼dDMU6M!x>5o~97x uIfu7n8iήC!\9f,/٦X}\ Ŗ5h6yCx-݄>,-F5jNPn6Ybv{?Sr3=z45|t4vH>]ŅoШ+XNZC;f^zeK[Y,W5lk.<Ϧxf"?AZ94;I0 ʊF,\b?}.䯰U*] S>hu]+vB ƐzMRa:}YIJW(EPɬ^B[FIܯ}͚r7t@0Yc+s}a4=m-Q|r /jo#9ūNOWωe);`oeܥK!kk9T ׫*4(rC;l'5N\֣X jX "4CG뚷h0~=MBOK U[D䩥X,WCsix.ϐ܃//%fimVb^{qrƞ%XnԡmN}׀l@F],D|Ɗ`!B4lG<.N 껔Qc@2Y4dd%HKB79C)4-y'H'ZZy֜iڐL#ɀTқmjw+bĺCv]?Xnv4>fbv̧,2itt+ttGN4^KIŌq58UrGbInniՆSz0ZheGRb.I5(n(Kzw]1显ωl'y [-`\~@9(PPZ&cq#c3EPfA#3**;gKb[nq g߲MDIB=?W;frI>> -F$ a{z"@Gre}zizhDD? zoC:=6g~0FJ*p"uޮ(w-\̝ci,`o`h,%ECl€0Z~{)' 8! tثHehK̱M KuE.-Q? `#+~&}>x7X:(3 haM=ߤjzK:Iai'm9[w{57>qU iT~B%#RkshPt U197* d /dİa[.gi5".Yg^3_xVjCgaқ8-}%t7$re-ILS<{ ysH~Yƃ_'A30 nT()͹dQdi1Ru:SߛN*^q@c&0ԳKG]3H-Dd.$6R+ \\IOp]\ɰ? - f*\ -ǴliQ+Q!NGyKق -G:wdǯ0VnuVCռu7O6X}3 }{V `? C0~=oKvVcAVI fU&bϱJ}#]&G]9rݚv[»v)81sJ04,]ƥvo繭I+ٴ6C$5hҋIt疲ڭ$(sP PQ sD=MOϻ޵KMYH]sL&M+W^WKZ|OBQQLZzvqpn4 F"T:w2u d#2wd\z((yX̕bgJTQF`U_&5|eCn./vwO|>bлtVvv$~XvmQ2”;r2Ú8_ʜ}ugI 6Y5RiR|>Z ;ɡc ͬ~!ܜӃNHiU6DGL0F%P(1[KTAppt^)dGbf@|ԖZ(Rw%jt-N }Ŝ|yHX.%=xYWkr&}ʙjj)X1z8Ǧi2` `Lvxh}#u"AEeJA3܏:=rQ\Y7sY$egYW2C1АY0^[2E3H%" . -pxzO['jsbTȷ)ެ%S-aY-@TV:LQs&$T=[=d9,K $ 1=r|T2^X|_=cj4r&4_HRPU'@} }wQp[9=+Ph JJ1mRjy9~*xÁѸԐ]P/]P}e{8D2/cjsT['Tt J֓%Xnth~:x|`w0d&z'#B5OQL^X|9AFj~_? |(?FY;0y0vV^N6q\="{Hv~JoW ַOzF(\wkگ-6>U(zktj>@93'n ر+k @|m& }RҼtGOup-3 -p7vS5j/+[4]`1:Ktfͩ'~B=<z=d<ܿUA=B6)I^s\ صPu9b6V<e ;usx+أu¬="ܝyJ,%3r"Q:T fŬ'并 UO@KΩa <өwԑ+Κ烜κ%U-ʆu>ٙgzk7~^1dŭQKw܈"M5'>inZE_Q"9gyHާctV!.s{c[sN[ 5WX[gPAnȸޢ/$zff>ti5ʬf&ļCJ u &Hvd߈_F]n2qt: g/T+?vtմi'NgwSlF uا xgX0,/$32(2Ҵ?[=~Fd=Fzr?zar -ƎjU~{>Åߙz/Ev)ěȌYoMq O=Ih{.`|s}f{۴B^nw΍hȪ} S[YV-+-L3;ИF{ +5[cw;hmW -C_UQ/ǿ>U&]-.Tͷ%kǞzŖ:Xؼ5b5g?i9X{#P ^ݺɯgPOrz6cJؕ?֬[?Ȩ=z].͎ڏTiz%9/]/f& J[Gd&S簨qvEdtdul.[˕NUrfB'G B-:guuh?z@w&fn}'\9[gYkj|oU- @z2MuJMU+s>4*X=.TZ2fe|p猶>jdTݐH禗Av/j*RmqXƪv..x:~E`"l3VȋzTW*4hV~Z=\(=T7E&[nsr??8Ɛ|bt~(:Fs䵼Y[mf3Ku]߬Ԅ+7 Lk W[^W&R9Bjv]X3;PP@WPRxNWWӡ(I|@=u?n]k\٥uI͑cbY(+Ѳ8?N\L=ʵ*D'M\Ѿbw٬D12K^Pm@;adѣ^v9*3Mn,S_!Knt)!5#d{E9^_Y9KM:mrR3^x c00xM?MW胁L+c-ؓ5EGպoR7l+-6(kvQȴXؙ^]jێy:COjz,Jn3. =xsf OS^GكU|Q䫶ϕqSHgё*,ŌVccӦd#.77TZY:Pޜ`Ԝ}Jś4f!3 -)b@P akUĕe[Vre//df]d%T$~ 8'RrRVLa nҞWrܥN<7M' j7\Y,o3T]JdM{l/at(hBW^+mk\1) Gʇ ljJox, <3*eQ滤!{Vn/G j<XHA=v8~^;A_WQ0ϓ [Uy֧~EޏQ) w%"%mN -eQ.|ȏĞ/f7+6&<=p  Ά!r_H(V)Z2aTrλPk-%JAٝ65 rk{KW^J{|ZJHZ'H)gsgՊ Fa;8ر86޲:K=LE֞͵c/jWSj)s.CNڏ hgend$_c~-*Z #5 s'ˉ7tE*u+.}%Ksz,fxR떧Nǀ:ɘL֖ws~=)Mn֭䵶mG.{{R'*.|<@ҸڬrUl+皬YGg,(n{a\uY$RQ葽<-xI/.&@:$cynV%O֕uqsYW[:*vaT2?(!b8WmtBQpt+}6hvYԥm.p[{% }?6|diO1Qg"`I"}ti!km&Jc$M5׬Yes+wM?2r$Q4b a=h03琱[%b *Od _z-nkNtv3iYV"šv9mgi{Ss(J=m0?dZݳnGD.6:vgeBA-g -ԧ/W,WbZ67y,P܁i0sgXj}VLA52]o`!-W94$*6 -5n8)ZR K=z'U6.>V Nsu󤆑'_h._A$okMs*7)wa&sig`Qd|9咙 D+,.,py͡e*Ҁ91ɧGPԿ&<>e?bo7סnh<ү8IΥL8@ )x~)<8ZIP.j]qptJ+`_6\IWgSP%C9%- R=67j',&4`2O5v9#W: -5GOn4 x^C߫z YC#@'W L m+Z&[*A@-F:5|hՆsk{Ka*?%6ecFeVՓ>--? -p@x= 2LHhH4@* %H ;ǁZP@7(%oaFѶx$';qO}bb^zr6~]'[r«,P -Pv#w '\f Pq*Ovc2/K" ܾ{Ge)y|b/u=jӟiϧ)a+tb@Om}h|ky'MN,y)Vu7~,?:VHPq&O(`I`P =cu|W'\ࣃh~ŏP0?<:?'Aj$;w=U/OsnnsUXj{eJ χ">uԸt^Տ1~;7|t67dnsraކڗxي{RA}ULEu|x'GiX79|on*/dg>:l*i?w V!r+ݧYquH VgfNM.;B&wh7m,mS[p2lE.! tV!fL `1T[e\E({X8nLwCoM-7'.]t6WOwo[|1tndUk'S\5&w1vszg??s.6u?y휕^_Ț]t7(G!3fSqQ=wɪkO9o+`<&Wl^QAi%C;|XO:>^ŽB3EeYDUeo瞇ZaC.MHi}yFvwD4ZJFXvoN(n c<=zNU[.kGZRs. t'Ѿ_[b:'sXaٱgU2<3./nlS_2ȇg+F }ۼu34[H8ߜo|Ҿsܼu*ujK=tp(feDRW'"ZR^paiiJb.hh6ТxZߒE4>DqXf.䰃BݎRu[aV,4+65C/];5 iμ`ί*LT0.yL|~%R+ɠ/3 \M1O0]Ӌf?ny#~>}!'A՛p] -jgQ&yjZƉ2%H+sө`pXvmTΥےbE4xŎc -xd`"uC3.^6īɆsdatp[ -Vn2R-q]%߹ -Np/v8,Q~ozqulpUf> -m<=C[#(k3c~.˫jmZ.mem3e?|!j &z*NLw+䠹zTeBoU84.V%j-rfKlhE.aAٙBWoxՉkU#U逅 ^٫TJo2J1tdz)ԥ=M~c#qȊ>pE?ۚuc?iMymbeR4`39TacķsFj3TQyGVG]Nj3.z=fOA"%ˢmQKa0n8롌?ԼYYFO[}Ud[s5i#W' jFꆼhA`jOڪJOU~x5r^^fX[ Y7yיּ,#Xy΄BBn򾍏 C_5 ]81uT+ ^Jɘ,IiF̛*1>rɞF2h;3 ĵ}+լ;߯leђY]Y -ڞFjz -UolѐŶ4WnvȍbJ&hz і/J7s .ػϙ<˱* 1,h+ ѐgL?{.ilvGq5#Tk y/+{'ujzTuBc,Xx2eeV- g! 8ǣ4#f[2rZ#c6D}ԶB͉n2oě9MuR GAkR/`~aGsgɰXkVY4;h"yT6oa\^Xy3$~zVR+PkQ48INITL]Ջ37WsSyBTetb0E( \< <{'6wQҒ~ '«S\>z _΀|/$d -, -*l堷 4D|jʮs*t},} =F]smtmv?q_H+J5.Uk4+}6 ).u(iDk)fk1鲷6ºx $ͣxa5jx{~!NW3KGDBcWUt[׵>zo~_7ެk1_ޖt IrP -|[K5|uR!wWt0?A!4r F2:/udta[ “8w͠èN Pe:&KE/˜rTF:Pgq~t*QzT:WϘRr[l& g7);p{(7\>d;$+TB3 j;!3TPjȄjb@ue@P/>eiFTsXz\ 9W" /#>d 8(-S|=2034Ћ^** F&0E@&߸ l8`]1 !` 0qH?hpignTL//5wE䏍HBl^& -oyFj& XahY`6`'4iI;ր (`J]Ku\= }eq6E}yHGkISGO~*<'yKa]3]</s777ltH>e:A_AL17}M^1YV4W=*?&e$2G)[R?=@H5 @#Ze[ l@wz,@,?7iBrgi_G(Gow Ze@{m4igM~_ߛoG 0$86i:7x[Ral|;'%'I`|(?2G -~7&?a~O2ȁxXȾ[ws{ܺ\ukѕUs'z W>33}:'ѧ]?;k; -krA;m ~ЏkuSczrüj_HR`A%rgN/V6GiiVaop7N[l7T|tٿX1SucGm\A?[n;ArwsURoXTl{>ȎJ4gu-ƞs ~^~yYy+<[)ropa#9}{=W/ǢPr4Y)% ?@'o;jޗeN|hug39+>% y@ -ȸl*ӹvM2qsB̅\kd{졍Q}F { ב~^أ6u]é򣽱뭒t^H)z7l\d'+cDNHV&2GriMctŇA qwhsͭlm:KU_I?B_jGطncG}^t.ݚb텇v2:;8)@'֟|޼J=l!r>4.9umaľ} iOSkFH3Ӗ )xJ?H.nwÊ+=QQ[Ӭma!vYJG}@Iâ ZuNm%KSq,vdڂP^Zr*z_5 \@[Fzm4\KBG&ru37XW*8"q󝝡we7A~|D-{4sR̵9^p&Kpcvm_H## Xw43V)vyOZG9ᶫ+pُ]z×/D ws(t^a-!ェs#Y&\;J.3q+ƮNb#a? x_UV,D Kw{b]*S)gWJh_AnU/Nrz.}Za*5ݰwЦ[=q{{vAs3,]*ӦVKu>y54S&+9Ȯ2E'pFE,lZP<[ h@acy3ШԽ*ؚ}ǫ*+j1鱎>z0 zQ%m-]]EWDŕIiݧ\:(nUX(lz{N9^'X2h%%i@7(4b~BrMX:ޝe~Lb騏pC#ZYP](5*ߕwf$)k|/qy.^(2WNwŒkږ_uxPЮx03gHeQU7Xd?WY u w}p2X(N!#7fsE7S:1dMjՖu{oOD\ ,uT(7bT3Ug!FdlQh'lr5{X"vmvrmRĊWzI|fNfMc,7S:*q'*/^%A%h[+4_'5GO&'Δoy$[1&:M./r~G0 s2fo?pNڋ(U~c.5Q+ 2K+)Or/vV&ĩ~JP,ÏêL="f,Rẘ Y4\ʃ3YT3 -~1G߃Y4m.9*n,f+YH0K%(3cz)ˊ7bp\̕E$*Z9*8{\Tf5ϕՙ\n-є^S^(V LP1|o󲝁r!keT -ɖ F-:qv?wȮiC$IB>rś[eɥZd5gzG_F4$u,X:yi,*d~6HЋ -:& t,`q $ CIAt(ɘLC`Նi_5j}s}A2IkYڎ++S"Fdwtӹ!s#~”>\dYDqu/oL/xfJ.GAb5.X-VTl.l,.,Na_xxYLߕ\n{ABrQv`F| |ϬWuy~ &d^Ѿ+ǘ;d&kd>/El, XaWeaa%ݎOìy A{ !E;͋_u+O#k_ԂZ zpcC@&CȂfI]5qŋ%mӝ%yN B|F{}n48-ξeZ{)OVkxpq~ˆPkIAC\IYG -6k4x~L||uy7A^8 E| Mv54|E3cJCƽ 1+ZmpDt!Cn@m9(MR>J䑩&uy vO@`y)"@NI }I0?2ѻA= pN(h;b?ٺ^ojKi| u%=*Y@mk.AhS]B[7@KY Xt$x㷕2*jLUwh( D"OS~ɺ -ҽ؊q-s3 -Oz e^#^ `$L'3%Ob,؎64Q4`#p8 -PwMPʌ*0:˕$6>&t܄Y䶨ԮJD)9昏t`6׻wS˖j@:drD`B@ܮD7bQl@Z3 $˝duJr$e/trwo_Q5dE$\?l[NnN7ǡv(bR4n P(y|ڐ@! ;ɮtMvrXK'dMs1;$!!sSRvti?鶴e^[@h^2HCЄ4?M )\ hjv~>o⟴l[G 5~-|>nN#8%f:W0ƺ~O!|7(}Gss'oYqn+{'L[%>zGZ 8oFl&9-wsH9hʱl^W\z%|Y A%Oٔ'~ =ܳ L!¯BS[j5V'#D'/nG kVr=3#y⧥VrGiwq{БwSgY!moMFbatŬO~cc?7Z\}g&G.jz{wsogjvu:Iշbjy DͦJ]5hd_bxXУSQo\_؃ǽ1/ʅ%&j|Ju|Kʻ[b>q7ޖg\.;CfğjPsAzP?/jZ6ǟ3,zVNd Sl8HVI'(u:0=1P:d!;xCq*+mfj>:#ڽjFe{;6HKNFZs/?B:oOm^]08r:!@̢83;[cog&X/zqâw㭻u/^Puۊ~$ň_tw>skRk1ШQT B# -FxtPy{STK/n5.l Ok - QPK0zzZwCo-s /W \ J;A<3ydNXֽ燩Y +>ZT&Nn"v%rZsIfDcbg V⫹# S#O}4T3Yө[PE4V^-z۞v{QzC_odSŦ:T2 -b?oF~m)?:M<|ql{yqzlʻ2ոT N-%#iLN(SBN+UǡN1gIzIb+@L -]_/8Sd9g(W^QmzܤB_b}ʃyAk'_7P:s5w]c*^p_.40;G l7Oo^F2.)>L-} -3[?u6u[mcoEI/sO)eWIUczNmr(<[>Pr\#G|Qe׽k0(p]sK˩=ynG վJGn%1"İedY?aq{,:xW0+k^žBmy6(X'8#e #?U8ƜPHyL[vmfN,w~so̼P .R-hgvdZí3-jDxsܺ~S[ueʠ|s]x5v9;-չuۼ9b4{L5܉u>C]NWڢ]5|gjY -*?RsF!CM>O|o\= 4sE'p,J$>X4Ff) O:.?) ٧ܼ^85v5nTY -W,1,ylaJv8<rvbv 3P<]}NWȢE}~n"kTk,8d"ew -닙9=6jO?.ӓ_-T,8[|OY^f9|sguW2k:,u f# c3T+0n5n}GB$B1(y3Ib^:?2[ e[gAthhr~xw–[5K+.pI@N]U"5II;TE8ƤՓc첃mH8h@*اm/5ת}PJ@ƹi[䞠'8pvM r/r^-dYt(u /nٜQJ=Niɬ\o|F"v=g}V0: =ovhL>wpS'8'h |))h<4Z&M*!5ԃPTuīɇ(:y;R\ʭY| $92' -S^/Ir Q3'.` -zG`\ LG&i4{wa:`90=LLx&<8~ X [e|鬩b8E">_#xP8nw'φɠ9` -f XlY_~JC]pnMׯ+Hwp%yMQOpLzQzEB0  Z ۾ftgUdp?zrudv7Ҁ%X_$_Mc$׷5y,yu/[X8 -W.yb1*~ 0zA lw89̟bWEJ -qaRX>@"Q$Trяn&ΒRw}%x@$%FZ" ,S9͹&v2<LK6߁J>wmoWF8Spv. -%ozW@-kmjƤZw?^kjJkmÝ* ," -_MוW7| V&|-zր +o;,΁`ws~Z?%;<6N7pHomЧݻຸ:5WiAt!zͿد&1Z+e_kZWW^WY{C~\^~A_QG弑/HyO#H?z#ݻwC/ _zկ7'~m;x&>þ[t殰y#ǧ^f}{}5콍~z G#tjY&򲎌ũpir?{4J;}7_ZWE \0:=vkL^_ܭm,rCϯ,)\Yw8oQytg1tnWW{8hoV:o~!wX@SFװ0? Q7rhdqr?g1}^I,85e -#zUukuN({pxpjm֧׫W@M=ՓvrC8D'5:9'9 ۅ#u!j`"/}FTaVND{9  iss=q7[åԉЎܺ.Ƚ|\K) e 7OM#hsiBl_)6-e~::r;_<_ޑgskKw{GN#IFtBBK K]WƈL/iI&fpe-ijG_VKTWlMai'RL)5mR#iSQ+^!ΰёjT'C+ f:.2,a xnϧS7U62|v\N]CJoeǽzI܀r,$HI>g;bW?7a`HYK{?9hP>ɃU[_TУ ºi!%>F?Zʮ.o<ɍ |"9HŊ7ݡi L( )F3@0k˔vgi0Of^MR͘IoCU-;0`cU-RjbqØDc'MMELIDEPs,T"<9ykoOWOV73%G[T D >>[<Cns7o]<}0[*JYt_Թv)[jr3V+3y/N]>j~h{ #iΈoT ƣR_AMIm -wծZY1R%}q:Y7Z۟ejyhjŭdDJ?q'8dq)ܱ1 -EՌ&qX `o OO+;}dTf_nb£`XdWwgKҧÛT.95ZhЧEoJ ٕg@S!B#H- -Cbͳ[DŠґ@ݤa.%#K+<7h<ADGsWVBԗ4m rx05LK|H\I?XD>73;ݾ:adg%oYBf!D6TB;@je 2 G1Ot@ [!Zr3S :+4чw`fr\`Qf#`fAB #f VSd[?;4|V$Ͳ -,t]n cezy/[S'0HHmjY]S͠dQe^wlʱ̤l3 trZšW" Yq3<,^X&SrTa!D]wf;4G آOMM2>ElU/=`@W|,Ḓ.zjZSy5>E<"y"  B `3C~`?% d3t(G;7A'*7%| Xo`_JQ4(هg1 Zq (l?, ]dVSLT@\GM@3i@bәE~QnG{N,s8L%X֑y^zY6â -"3@l - \7HA+W, T䥖ϓ#µ74K#Gu&TXhw$P:G0 XQ/f=]@1u0tM]=?[1<$p -SiڳM:ֲ+<gxv_bUL C2LcL/Q003LӘt,N+x(إHv=;:`;`k)uX\iFՎѻ_W7|̷H꽀4Щte<׼ڔލ?izo7_w)]뛌͈mmU:dS]ޫrW!7 m}tk,DF 3=T*}~Caʡ_Wtȡe#ѯ59/A7#6zqxO]\^Kw9uW8qüz2E(NAYt+hޕW ᰅjk]WC?%WmG>sOm}o{hD^ceƏ.Yncl63_㪒\{Znf!\-YwSZ( Us~ѣS5ƻ5/Kz{9q]zhg]w#umaM :icq47o뀏t5N^m?Tr׬ٙvêƓVe'N -倕2M#Jqum<8U`/&ZcWzKs(v6lC]ϵ9Ucwl~+~–T&vwTV>C`ߤS+GEm -m;hro`TOq.'a7RBc[A4d>ԁgNqѭa9 Ÿ*`Pvw.HͿ~µ_ -jq ߳XXϧW{7ϋ];n /.g[U}$%H1F}"ސW{ute*)JXZF6@{mN8同wfN˲]V Μ%9-cV-Waj޼U)ќ_~k2o-5Sorq)p}߂;U9J>39dTk;^Õ|a[؂WZyBG4;njm'S7~}Sy["3ТUPvs.I`b77|pxsQ f؝ Prt*RTTHA-RCKkȼK;S͋e:Йi3gk/YcKÛ{0\xXG擒ΡoY츋WH).VX&syh7JeVYOϦ3(p=?LOeGypPUT99ʶ_7ڷKQ/wGV=̞uRF;(CM [8Zu2\;uAzfNfiz[0{y`c_WO鮢IB6Ļ~SDmx€b -Bkq>.i0qy;qa] -¬1ÔFvOs\E5%%7<|RE_V"/v,_"SʕO6BlZQ8˺]? 4Wp!L$>b#Y"m/qd(k72Cs"'S#ۤbN'ၿ [Wv -˸ThB~M?[km#+d1|b86WZ٥XY"xL٢3N4"Pӻ0q5>Cl|8qXfmE͖JOYD/0 /[d|qMt';ԈdC -U$r-9L:>L20L`QZQRP"oJAQx1OaoL6 lX0#m*@hp_E=&FX8W[կdH|E4{ijAJ:ER.:ѯ^y|˸8j?;Y9s -M-U-u*RaaG --F|855֋ =Ǿo per[V79ufSթfW4ѻ!es-r]]]Lܛ(Ȯ̑9 6b$@erC| )YlfhFI{{CEk6&Ron@H4\e{|= RC@ʢ7Gi MI] -دՠ9nCq)1M1LF?Rl8ik S1[Y(3i2>NY:W:-f_%|r%ep B9CS5ǣ.W -Q-q?OőuQNi-Z]srjlPV=X^,kӰӰ@.5@F\p H[{i0ddrz9@ҝ# c0d0وG6 r&cz6Mz5Gr$'!DE"Q5dlUHa#J%,> -lP\PQ yhL=mDh+ZX|܃-$@SFhؽ9uMվf`1O%z7aRnYVkAN}2zy,uPZ,\Jn{ CI380 0c K,yK79?ۨdky: %})ro@1i1PJ ۱ .u 3.[ XGWOմUnGpJW%>stream -h.^UsYũ#1e~~ -rOnXQW/#6R9vCmݣ`m&zF:."l_nl~<~u} XxfYFŻ8@1,_~]կ?˃;k63}Zm]-^D+되9f,tT! d69 (?'%TxPzJ{WN T7Վzĝ{F$wFö9.Wj=;kvvZWO{9Lvh@0k'Te{zeĺ%geL-ZبGJqr axmPyGq ֵƠ|G]BŧNyyT@E_&|d},A -_6S`E' -O[q[g倬KjTGoS{#DSPS<̿簛 ^G9x{=R@HnNj.\\QfXHe=*r%t>hBe -qyxG -c Պ#{Nf?&^\_ެf TO+W-neI󸚆FCj2+5$|~Snn/>нsiW[O -zMIP-Ƕ-aڳy.XђYRoLmhaw(D>ig&MeZC]Y7^9Ϙ M -v%jiYYeY}à%#z#p5bV!%-nr0ݼWgyggy{m^ ۗН&Wbbk^q.I j y*k/YTXl1poЕuMU݋poN=1RG˔+x#nφPɖcX7L՝jS1>ȩE+)z%T|4=27qFF=/:"*0f/պϧiaǍGyss'M# iRH\i `r\U'[nԙ%Z"ksA"Nz/E)IkoTry{V켰9frɔ:zC)ʜTOBc -q!ѩ6jtH)3/㙟,+'}3%50|YQNBJIkJsI Kgq<0PdJ-PJEWgD-%W RMʘ6%&=S#;&\);&;=n?ۇd1:"]^73rnt^nV2+?R`Nr<C" 9-ASf^%J8!yiTcR&k? =KADJϺ_`XobEk`æ5dENT _ӷ]]JMR-5*j<(/([Y`(u,^l -TMKro콀s<z:#D,^UHLT]a?1ʮ| -  -9 ViIJbL @L  fs5@ 2@xG"nlk~];T ! -c;y9|UrEYh4l`RpVgʱ[sX;@(6ȡd{vӀ뀬oQ(|'p?iTOIAy|[~OY\lD  >[-U4- (:rnPqP~T2j|;SP3e/^5E@6n'+cm蔾8`a ^NjDsBx:zUD˓H&ہ ֢$*D@h10ml@PЃvs,o~SH,g : K{O?N'hcsZ᱕<]BFY֫CΨWS5@O&`84 MH3m$dU!4+#3n+@ߠgOk<[/JN\;zInJ~=.yêC{pjy;;A,@˷X._|pBEUx(xFb wߢpBnrHbrMۤZ5ڎ@3}/oAm@kM hBZ onE@kMh*NǙ&oĮ -Q &%}Mȿ y:ސ_&[66S6ll3O`[íM*}nd1bAkbM*VYb=?(±+r=!2=9{*2*|ʝhڻ֢,R|/YST;udCvZWb'8c]ot1Q+2rnc|+KUCG 1GRÔ][[nqke @ JNhmXDBcx~r+eu&GHûGh&:O߬ZI,܎1S -q=4IH**"aeTuٵdf4M+~3*>7l0K{p׍/Sg_酝:3㋗Ӻ~7v5ip]]f&mj}٤ڦDmu>E:^H0+>Rcu -짻qt/IxYU_K:6n4] ݊,*C0Ђ\z@rU^l׍;yl8cp Y=~iDzJHuhnTςfRcO\)B[pgY֞ N~Ӣ}W[}i{; 2gn9 ^Yn*23ŅdJ,4?'xjJhB74;W5vJN;2|[mTľ:jyu4]RyF|݆M!gլ -C^&(@kP'E+Kq=^/@ݘ1NˀtW+Y{fSW_|Z-}Ldh8׾ͽNRR?¡Z\;+Q:u) h-\渺5ț . ʴӛLW}OW>zJ.]Vm5(? n=|{}+?.6)o$Za&ży6(TXʘo1yV%}eV5*s#U;Ma]!jUqwJ燊f?ro s5ixRJ4$DDk"7Dĩb7n `=">4s7r{o)MO4]]4C81OG4uѲ.νPQOgՓu"Z)YJNk CU)uvjZ`䒧vo}Z7{ yidv1o{_+KNEQC冫 L)aTrӑ$4-OgJ ӮB_X,SNz{s#pi3!j0Jy_f8cBb5gyTU<ɻ"nۯA=ͳ+Q+,VN&x'M#R# ҜVP<6s?@4G.1/ RnoS#:xLFy2t`aMe|RԠPѼ:ˡ$IT~v i*_Fk/^ Z".#B w>7r==jZT ,bz K8kC91DobR -JȘKfĈͧ4x62C]5JNxKlwy$Q|+|Oge-$3W'ԙ2EtG;ߠ{Kf)5, { -i]7RiTxIS|"lmv%9!=;o]$Út.Usj##HD,21~SܡQ|deHLna㬓toXi#;0pL&VyD,R oW|eQ*U*_ƥDYmķ+u/S:Mk Jq봯]s~Vҍ,TxbۧJQ(bAz0`Juy#I$A.M,\?Ge _WOM̡=Kf.q~oʨQe]RoWLDA='p=*Qtq5IXJc$ -škS<ہPb)Fqq35(2D9GBx?8m -Gx-rzm^9*vgD|nE8=-~8C7@Ԇ4^WvY,2WZ|g*7255cwѦN)HG'auN^E=J2P $ܐ9įXeDe6|?YySCQX K ԸϦV'~rY0h7N+i))][h͆XRQ.+ 5ìwo<^FGtϛXUJ HGVd3>k˙:hDqz)8yC\,^Wbcl;V_zkg$,وL ʶ(KZM`ShXơbՒ񺚀xXX >m,2:_ 1 *J$;DŃQƚ:y U fzHT)0-|apB!hT==I]B*dp9Ӏ|re@Ky -(dwX[Lub9^5g_MgG᳃v[^2 -MuuIlL4MĨ+lO?%_tnbfD$ep|&= #sK|DWNMwY=7׍`Z|$SwcEj́K!lԴ%Z_oolZB/@X5?Ib,th6@=+D*,~'d'wk~vOku1hl\\ĤZr\җZ''Cp} -yгbX }Zr4vΨ@z7 bwrh^l4A5 Qme[oė"+k%oZnbs -4f-Hɍ< k ԇ?uuu}wfJVoT|%x- bdnٿܶ3r- S!00-x\.0 ]qd $럌ؤ>Iܶm,07 |e׆3oP /_Iy pWuw'/vm)IսxQeY|"{-dF ק>߿On1 -޴0`xS~=R`䬀XynBmڝ:VwkL7\_m@l!$դoO)?80 )Pjʥh~a}Y$ڦB g\ Ģ]].?HEe}L* -.Zn]* ]UƗ5N{Ȯ[ES;W- 6$RbpY8AHhmX JYy - Q)p^V,~SgY<7)OVW{=7OOh_ZrmgYtl.a8(轛]?+auތ(]a=]g籬Ͽ^. Tn xWۊ^I?+l䤂fiE!g ܉h~CWWg1s6pU 5euWS:tM[OpC$9^nmů׊_[cʿ*̖دA|7m*__l,T<b)* -$Yу_7OA0qİUNb\v^iw7%FI<;Tʯ(^!lkjӻAT_-ϾLyOlv®8^6g퓕O2%[rg[XV~SB.O>8  ?J+ʥKդ"+RuP'>  fn-$m.հwb:Gчz0w"*MgXޗvFן) o~s0\wdH1%DAՉPf,sBA$0TD]3I;aFdTA|fx+ EθSDyc^Q>ðd'3KMjDZI9Ѱ@Vo |v 㟜#lzem Wd%z%_=w߇ 2z =4#!()"=k%:?ɀQ|mZ^JcMб&lՙUL4LIa0/\̵6 鞤)qޒ6[H)ƶŪmX%#{@s -ֹ!".a1桧.@FeO {ͭLp&ԛ$%w-Ys㻥@9b%Qw(߬c]V:v#pƩziU_ z,uhE[COHG8tD~ p7s#wUSYZdClK:ȧft͸y﵍RE,hr^2LCD<$yVC3nx rd< Pke+O>pw9E{a-%CfWR u| PcgjTX-/OZ2f2-ӫ<]o\㱇Ymg:|PH;ee8WX2v*aoE -ߔqz#ib @?k `ubxKaX!jrz&X6=6} ^ZxE?/Kc 3]PA5犯CpjW=g -XBH``}RcY z,b,oV'6c6 -h:`]9zG,\ȍT贈#CVO7wܦIV09ΞѴnX~\<:>&|g7& p{GʱT3X+7'W#< -p%\]L:OJ8R5wz4C^Xݗ7D}]o S8C]>^`|y2&8@:Z,Wele_LK|0b.[ s - -ⲱ5}|SCUCqI7JW@H[9:@ .q+$5 iWqٌO'ٜ~"a@ylp/1 +`\ 9E'7"Kbgf F5w\bMmx,ؼi *z)op,(6B=!Rukg"hvT\xm5T QRRbMՊR%T3IZx85&I54@o״ZRB uhbxx=Vg25w4,"^wtnBD; w_pXn dKӁ<غ@nKdf)̀tb@j@j6YO8$#т'k'' I ;NvMWf7nomBm+1ښR _-@=62P.P){8IAy 7$%H붭u/x_y Imt'KI.r@%_ O()n8>OdĶ$ڶlI<0s98wI`S M MbFoڀU|-ޙoo>#"S4zxo7m'_WsR#Xw@,m[! Dg_m?H;Nt_ Ŷ~V-X7ȷ랅G Mph{Մ&qI-7d+ j>6:21'ξeS-̷Coﶕ -MË70OA3> 94m -u]u឵MAke)W*]zaE eTN/=BU7~^MXm&KogU,Ϟtg hWPF?!S 'h{E  u+mX:Fy>!  &wśP'\Z\a6ɜm|kV;^[DtR6KBEfW̝ ^ė'l]̜tt7К$њl ;`ZY(YdtuȩlIc%#^jv业euɴǽ)}gtWs]RrHtJaDfD^OgSXq6r.x% -1А昫!bF<"!q߷q`kΔ-NI8+.^*ru*80x1"CSf=A=5aԎȯl^3X9z+$:ժ1@<^!ʰrн1C摄^#+y|Ө)]X+iM"l|ыgpzy(ǚނ)9ΦF$4 'KIp`UH1OE8ץP^:ʈy[+ZWЃ -^^'`RVF¹,[0RI)iQ߬tMUs#wYD ZՇK܁Ə 5x$OIJk46jwm,3}nwE*)D Z z\Ë~ʹRCm8Mc!i.WDX|͒HI(^SňsNAʡm2zP@.HXaq C}֦H]װ;~k -=!8&D:dTݸ"#HpˁB@ gǵOUhgԻݥ2J -PDf{ sN;8 Ko~ -2<2Z/AXG-ׁZyҤuZ t=w0?lyK:2êe6 FSkשlX5+L?d,e9 P;ӌ:hT/O6@E$>TUHXrl4l(y6¬"԰Io^a3@m 3ˋ$5gq%ODzPF -N,6(u )4?zZ] Yλ'=[1EXt"iv#s"dxc6K -QN/(~s0 5```GK ey5bi;X5%yC4qY6]=1}Ky*3p8!LVT7hbglwsa(7a E:58`v~p[{Z|2E~Kѱr,,X (~a|Q6,}C'x;[ؽql?Z7(l%Tzy2Y̕w{~..3O=#JM`7_6ivԖˈp!8 㛾0f[@l7@:(Psm Q|HYgI9MPagi$1))II 3Iw*omG gS H%Cء'ZǏ"ѭZۼFNojd6$6)Oa 3$\T@k@+Ф]jU @ P T":4hPzP!W_r2N|淞ٶ, m5aVͶ5xPP0&0\}=H@w^ '~GǺd&-~b$n9@s _O4J=ZOmⶑ+8egp4!B -dVf9B1 3z<'[=?3"s(1HVDWDQofMnbZw+DcWwh|O|[fMrt*aЄ֕{cV Iq --Ǎ6r.!soy n;ֿ=۾ @})^F3f9l7fιpG,=ub -5Ϫu9xYMz)ȟn߿6OaI2 /M`hu5~M944s8v| JAƁ;Q YVU_)B(̪|=ޫ+q&TTYi4mpUݿR,suФX2I@d\q"8YY*8t.{︼=A=WwK{nsA+NFPַdx52VTAX˛99Kv^i8FHb*MrߪC_jCG0U!Iűb }6+gtl\g~vsۏMt_m}Ϳu"f)JjՋV$?$40rڮίaTWĄ÷#(#n e!KUtW'XavZxꩧY/P=g;Q%l̇Jw`~%9h?ھtQMl |%- -v( O$uRNU}JC7Xc'y;OT|ފhy?ɧ_ʧfx5Uq~)2 -yVucKf <<đxK7Dљ1]MVP['G$3ZF hVoڤ'BB)c݂iܧ ڌrNKyӲ|.9iG%Y;/HHTS@<LaF>[e`9爓Ecͅu -WQm.u{]Y}ĪjTU']\DZpΤ=5pd6D}emKܝMݮl@|.| ,c06ʺ,&8ŜCQ:1I1=X:,>ǵɧj\ֻ6e (7!ovR pFf^ᕚ>k2>BS/m7Q7Nopf2^]#pz)"NLnQoҙ{/6j,@Nl&1@*G0b8U:EZ̨j$(X]=du>& /<+wtñ} - 3 +&Vm2qݻC<X1,!rt#/S!݌$ҁ(<sC|W+?]=PȵʃWUAX;6;HԬu}Ta\Fm18WQ?ʛi|z_f/F셯Xb~O-o3U`>Qr)l{xW=78`^) 0a<&(قNe<6e2jo$3(ljx]\.̵oܴQ=V4ՏTѹ(GYl *ȠD% wHi$[Kbld´fO݆K{ ë_ɩi8Ȱ@dXpTT)z59:jX m|?G 4JK2('{ l& \ae$ә %M%3K+nƄ>U ǗbLϣAGh'rtBYϨ:t]tL:C6@ @lK<t>pof4ϸ[MNZqw}~dmwz{,X• @`{Ayd‰Y,AF29!q,|rc_,Ut5NOf|FIލc+Vըu28H\擨f%5W MO^]`pmXa 0KG yGٵV;wfn2DW]g;?9J$[J'qeGNÆEdhyL *x&S-B~\(WIFx~C޸=oax.}80rԄ0ߛ׭[I3 -z`Y35NA^8]>zqCR_)}Y_R|+oIObj-WBZڕ]N.* n<W8AY]!x Ө: Ŭ]'KqQy1KpWN(>m_pX<5zuvIzlwe%6`_Z%u,C'} G*w-RX[S ynh~+ sa=!I H+Lv|4|:WP_( %I< /ȒM_/:Ƿ"̧z×8.7L~<.}hREH:t{$H6uLd-c r䀶b7GעaԾa5`H?ٶۆnCٶa0Ѷ;~ICöAv ;cD6EA~ ;HfFUC -&W0[:LMblJ_Qm}ٶa$~)kͲ_>V 6;;(Lu d}Ղƴr/4}qBE>lbo5bÇM% -kn4Ϗʶyóۣ"znE}XI^UxHNEwf劁z -moOw℆{C? -ǻo$}گ;Lʳ<JԮY<{q4#'xtwnI9ZQbbdn+x9Wq]+npK5SpҽIȜq -}G]ahi}8kHnڡaɖ&xef;N?p3wW`*)M Ѭb;qd9v1v]Xf5?Cy\;"iä1Y,6zQ|E.UcJx#ޓ45KXWkD&muJ6|zn6jq~+8pTQ}Sm`{Э /\07,4&]F^ SZz svWS>-+ - [2kűtb.}:d&XKN<Ȯ4* -g<:޵M*QC^4:LKDN=+:IBO4tkb!vֿK]/'n$V5||;l灼mk^|oXۗ'Q5,SuG]uۥkԸWF*ѴDc>xlW2ƫ}]3 kCv!9H z+=R*Űhc|bE中u_w|+)2u"MWz("$Q!}ݥQ3|!޿.&u ߇`h4bh%8sx)WgW2d]HehɜVu!+sQkPݠ: )g/|%Kɬx[˜dەHLk̥Ѓ0woD-tbBy.! +I:[.t#z'3YP0TI %`jȷr*|lI| AFcMNncncruwɺSuI?WP\~WM :a(]065ttO*^Y6oxF؞X%0۝3؃1g2ozWǩBg& unN8)'yyK/cqq+e~L.1T(8\ΊduHyXhɈB:*JlLܫJ(I~>m8VMl[U}Ep+S+]?I 0Z'B {E)"N@x''bb"神;v3gr3 ̽.{p8x3^4:I=j{{n̥Zq 0V>|] -=r0B!I1]̧Db.8[[x},İCgSяUTI/mMub|QL 䶷 JR1TL(TvMf!ɾ!lAm#RO+GJCcV8h Wؑp/_%=CF\qQ-#XtEha`=wxINɨ=c3sk[*jr#[]ndB"[]|/x8kƺP1V3aW1a˰e@0hn#|>t -xuTDFD'QCvդ%UNglxƢϢJaWO!$j rL{8 ePt#=3C&*xbs -UBp=He zs7 z K"KfG ̞MO#S↤(WJ^JsH DKAUs:~=0")!@|UH>H!\<RIs @z*͒,^dr񰟼tvNORcv}>vty0B&p'}v=}P3zr![Ƚ4.S@Y - '^vpbqIfToO$g1m/&tPNH%z@F<#ٰ'qab -{x_ fT Wu@=P*Z;I?f ֨KК 4>򮂻set'dDS|9:_%`<2@Ϙ0!4c{ -S-#lA+uߜ̍IB'Lf^'6䒴Ɍ; SlCAHod/ -n~2n㊁^<~H8#?J_t4x^SRovG/Oe,~ ٳJ_j|loj\5=sfiCxΕ` -%ee1M@6  Q |qʖ!V""aޝx/;#eSiAbSLH?,_=fiL mM/S[AJW]8?8r6~* X@;Y&,}Myȶ2S"~ɥ#,tB/K8#ޘ]\QZF+kD*Vh*T` -|{{_ ],R ĦFSa@/@L\AT)Yꐤ5{' R}qa6ulU^I]`] j =u,)mDL|bDA y???9$ K%j݆Τ#g -*s 2XF=mN;[鑎7CsU847^pmCO;dye?}KN_[~m* ʆ>"V-k4$y+ $s4>K_xZ-?E'E لDZ2~چfڶJʆ>_a\^l"d( Qr@ mTz1*t1kIJC6DZɡ0چEB['^^^B~?"E?ٶG> -W}Fr(e6ۃ:gpf]H)ST㥍? rdU@%2"n,A'S&PZZzn]ǹRfhow[vk[X❓׈/ʀ=#ܷNMFe ܜo>U`V-n?ZޫGT`{j{B"߿ zI.8)B5UN'b(J6>ڍi۱;3{r©]\,?Hb-6SNK9 -c -yHVY}5kNCA'zvk`+:5]Dw ՛T|E; -gPYn:FbjReEmi{" c{ؑGKlE֏=fmJuCT2vuoX]a:ZGrz4{Jռ P##YϢ_g*G~r:S0p נ#L u[{{w^·b*xj̸;qilll!qfS܄]CIX`)48KHۇ[j -ʲvY9Ig۝qffpRnɂ*t?w:Sk(q8Z#$lOWqHtpܘ9Dd6OPWN]bK W>C -n(uoeLt*ⶅWmV1?[g:l#؟Mŀ}D=O@(xF FeHT" -0WA?r׫t'L{`2Mp[Y֭4W:;!ٍ=~,1~0/.\Z^usSޮU-=7&~YٝsNVחcU:6 RX< 7Jto!GeD}#85/f%Wl5\I"epno !0eK5CsǠͦپ 4cbʴʬ=i.nv?]D<T^}T^"mz˪A]\:m.uנ).*YVTRf 5[%D< -e{@[5AVw &G`ZwY,ug<0P;cy0Y.iɤ$;ݟWBNnKTqe=9iz^ -tES%#.*jZ5֜ve"ߟKN~{CʨrkGGWއemI_QYiZ)?T}kGsβ-a&&qeRh3uInʝ5>K%=Ϡdᅦ^]J*2W1l@⳹RlnG9N/ 7>%-NmxW~vcG*ZھkKWq>*\F?zk" ܽ/?^GGWgQw5lŚɵUO_Ua-tKn˥O3wOWǭok>;:yÝ.k/򠰆^YkzT2!;>_쇉~[~w~~DOi࿿-?gŁږ_~q࿫-?GD'D{{_-?#E_'4?gŁh1\ x)  -9BQI>LF2nͶl(YVZQ;]=={n3jeќ^]?JS++C2i^v!Gy1A{ʍSr{dwtxPu6(_kiA> =>D<M\u M[Ak cR5F7C3oöwW:.QqYX/3k3_BÁwQ эx3Uc8]f<~NeU%><L cC#|;JpMp)Y)~ <#XcL]aг5$, ki]Y &eiw5d/!?@gկDcL $؅k{ɕiS4J)6$z/㜦^nwT -jt`%|-!Ԏ `mд%.{b6uqoBʜ V9oŰ-nX(Ƹ"w|(.~#=@*D7SCZڥ)WGaܛըd -G}rbjWV,Z1Y.9hNl_\!/__pRLx">lr$4mhƓf:e, [cƾZhRj _~v1 z8#'y`W.-O^hN,EOa_,F^eӷtXa Iɖ}~DPu8*G)<#?];+x2#[G˸Iugӣ1α4/\l5#qn -%iN.sR/Q^4? 9uk2i2ۮ.otQ7=$a֯,;=PXFWxsN$C`K8р}|psk;4aS*E_ع)қ&F3wJ|%FJy6|/yk0lqe5h'7^5L_>c.Jn!z: 4y~@`KO3W~+n+](P1O~UT#]RJ>vS&wH7JPX ?ڏ>kd+u]Zc3d&[0g>CtTt[=s(g}nѸ6c݇Nu]a4 XL.2#yY@>u6Ӷp ]6 f1X .AaKîW}֏?VSÝ81C`AЮq[""jaA^]fRs|]yUI s;vׅۺJ3DG 4fpnϣ,5*֢:X>TYll (/+@==,keAhTTf܃ =kgKN?=g|ڷzp|urھϝUƶ%=>Ѽ6u@3 ?J;hF&?mفi5 |HG7zF_i/ |Hɿ6!OiaD4#O s40tiYg3h׸ti|r/:gs]LXm1XZl_oZ'9g.⩶7RK`>߈&.  -Z^p~/zmщ>pҕr3z8m̌O+-[k 3G*$`m{3b/E_cQt5V/iiGڎuř6lw%Xzx9?y9=Oގ͗CzT:`xz$%?AlKjxMtԉuK[ټ?uf+`ߗe53,Ļ}4 uLrN~>}x%,Ӆ"ћJKD?J5hs MouIhT`9bt>⎃ fþVodFGtrb AU|:B. N -m?&V7e 7ndVj5Tkִٛ DZgDq5s%(-Gi-TrB}؁ d(8ZD|9Xّné7YQ{k]lkm+#L*jl]HSTC?t֙ u SgG)ᥙqw5IbY?w|Ë*ܺ&5*u5<"|_TǺ.uUNKH!8YzTM]"ط;yzHk NC[9holw]'? -ܴd lMȝ1CIz2twxPZ VŻ?{$ǽ<]x?V7ז14Y\-o`̦٠רOmmE^}4+bШHPo^w}"x3թ?@1rfUw~ٯ7ɽ!^OU_MP'8$ܚY,KkiGo "zsdUoؖKɪ?2QfTzep+h9T]/Rt?WMݩNCvM['39j&/ƻbecK7^Ͻ \LCzN!G1 X어G{er}~sdˠzKnf3f|ebT\|%h{s[hݫ;R;#Εr֚ln5721kf_b=g|nBN$[ތrPF?( Yhћ0;-kv ykceǎ u|_ݵ,QEY09څz(ُChDoX87qǖ`_ nm4 -[N{v_T6qy1F5#9aW+$P5$zס L}-$ [synμUۜ:]^͙R֚FKIQOF5=6`!/! -=&>&rCx _Zyy,ۉfU].b6Lfv,maP:•u n/!`& /'[: 8*~x3J@ppf{0r+ CS O7U|(;tQ3폸 o,Dk%A"W_[tNW2mazO}U]ga]զk; 6y(ɲ؛`aϖƑK-HL<-ӵn_BgdT Ϭ{=xu7<m/~K6B+<1+Zcx_~ae;뭞:Kun1cRf^덠=P"X_ rpLT`goO^Oެ{^>Xbp{1~OvuۣũG#&f$tq.RTN6+(4>ZBYԊuHw/NQz 劉||vCޮC|JbT~۔rc(fi`mM7RD+67^n*Q쿄 |6g\G'JULf{&rALi5cEuIt' y5MM07 JxUr|sX!Y6[tx ӫ }ʍWRup"شv#~wŗNSѧ8wE ԛ!eکD`bؖ?9b2vmtt33ow-VoŴ{٣c>^}#cܧK{tGb@ͷmimW3N=@*'%ZJ h}KAom1U'N1%pGky0$lW4oXCz۪"@E,Q j!O82ue]4Fn_NGe*sP@:s/ Xb{S>$^ -&+~fO;h~!Z_ȿ~,ք߭oOW+ /WwѸXE&]E>PԺA}m=WYl;;ʜ8'xsգҬZu\Yj:~~4<ScxBlԾ@dc!isw8۲pQm{[R[[9_`|r|p퟾0:XR(V`QΟڠ\`\)ڠȑPcu£*%  Pzy{%yδUg_(+(N;1tAMƢCt{N'-8벥Ze!s6LZ3ʭGXb?:H,ay..CPp9\}gf-j 6ǺXW|3P.ԃڙ&+"'v|0:eSTNS/WQfo~tayj'14yZJs@ϸ^9ԞLcg\rJ=cdsC>Oͩqy:};72ү`|y&CPB 嫅OFG+p_s}!ᒭ͸5_Q3fCLT~W,JݬJVEAv'2w0JJ'@ŎPĵ!,nq'b=]0U\ 1UlgZRq~%% πk˿ ? -E 6jVw*>nW*Hu$%b\V׷IӕN ҈eϘOUi+7~vd^đk.&5Pl-#l=(: @j:~ սi:`5,2]K_=U\G;v? Φ,J'K)9Bᜇ%K!,OɷF@e|mg i.+,28[h=ciq\l[4hQ#VZ! }gDE}~C@ŝO_ёNM\t05+7Չb٣5{uii#)0~'ɜ[1#:ųD~OaW-1Ni~y[jSb1ծ1Vc 2+nNozw5̏1ŷ%侉TVᦲӥ"(cwx}<#׭5&}:[epǝy`gsZiP޶tj|l&R;37w*JV#<{psټxK2M:5H;9`ZΘcdp9FdJdY*a7'ɁRJ\=5I )ޢ|y[K4'Z/d~`Z`֗`Zk~^tN ,&HwɹA64<ܩoIܐq0=RAR~@RٌޝCwٟԂ!c{F3\] w圕!kj.ՠ8b./"aln^0VV^iq6Y]j#Qri&U -Y{ - Uڳ'1ttۃg\NHD)^$6ׄpzO/0er(ݷ2N-2]}3 c#كeW HW"q uрX -1Sc}vtVz^Ú|B_rL'\e<=DCcwت/lBK%M`Pu`$&g븽Xejv*5&ȴwZj"/PR -xX ZY!A\n7s%(ԡT/*% r#B ST9m$|o/odV6[%&f^7)8iLMNe4ucKjV ~q&みكb|&xI X]4*uh*zJB_}1*\ퟓ`澒j,w%zdΥOW'G C=)WwI_ȿ&,+Ϳ._W+kք_ -5,hLBV9 3TV`K< -^q΍}NzrRuMrTvNc|޴i[.p]|0&`G 7VA15@qOAꃢ+8V~m~/jˏcqAǩaXt7`W/gM9;_נP2&g("4@PJ_/;I:>FQo!_5mXn3|WfA谟oƯ9o|5*'vI ϧ3f.;N%,K)_ (X(:>@јbA m@F0?c38aء|l_= ɚEÁyLƸV5=pui$^l ۆh*C[aϦrJ[o+ٝ _ȟn@᱊`o2s̠( -ι(TTRO2~>W_oؔJӫڳa I͖=6N:rvyHhjwS[,QϦ,զ-r> -N 9mVfZ]kӊsS&}x /#;|=<6Pf3YE m5S>9ה,KJK%Bİ7ݡBt@*u;kqsQ=#wFA(K0Vd#ZcQx8 1WM/(幵` =Pʹ(TfT,*3NY$wNh*\~!,*7J$qƣ$4htȊl_ 悿9HUoOퟜ(&=6΄`jN_>{`f~_,LL-TΪ< KRS[ZYǝN q SηsǂF ->Fm'1nc,l9jS'f_5gķSȍXq>E#GS KpL⋠t&[sK+{{kYu3uN QVl9s=d ơD%Gh(QF\:``Yp&J`HU@jLA5}>zân<[\vgkQ7ykuC{6}q+"ϼ&&Gt<TT^8{YDd]ֹۯ/Ӿ̫lXTً\krʮLwsv nKDFEMh w'q<=G4P!1( YP4$hﰿ|po%a/[bcdM= *KVjҵ2 ~`?"9t6E{Xpx.yts b} d kz -D(u6?^:}_\:'E VT|\; c k-LEMpF= OR֢㍚sN<*`Pe1٠ %kc_(|pdJYN/#&ov.=&V5Ћ/ux'Z QGvK]pM`\ɦ;~Gs)W" 3xPoϭ*6ٞӻq΢3e{'p9Ii_.39ش6~I ڣ~gn$bm$ -ͩt|?Qa7{wt.G|_nGz -n)# YO'xFnl_ԂN6ҭ!> f -KxB׾>{e[kpYwRu Tvw; y7DMй*,zht1jy|}op$tjo7,xb/sK)I}N\ڙ5jv :Aƣ6&ʹleWc%-Cl+w`1KfS_TzCyz%lwRrYͪ!lOoٲ5h^,"f+,ȍn.h8W (dA~evAaSVEPh.Mi1F|6l|S1؏?=R{.*t6fY 񟎬y@Gζ]nS@?B4-MXb݊ք˿U Cc2e/@C06qs -y=.|݋`OEee0~.E6:.h{%)=_w;k*/~ot) -<!\Lm6<" 0,,<@aEM0 ށ :\{ ՃJZFw۔3YY p /Zw89_m`*G -9`hr;*IЭr38Iۭl3,6#1Vg?yʝRB)@~wg&S-;2(6#/2(_pmE>&EڎzaA/}dDʺd'8Y]څwzAh߫ #UOSiL˺BſWw$`$ ` {2zR:E9\#{cth}2swhkFwyܚ|8j˭ap ,9񁣪tYY>\CPjďDf䜨؅@Y ~eQ%uGlnKYNtcCdv -vOY~G},Y:*{ށR@(5@9sXލU# -:΅+e;5ʓw9s]czSJ=wKAP*'3[|!*Kt JƠ4PZ[x -Qs.!B"Y QwtrGn)h;(fOZK -vL.e[ fr0?ȿ'ohNd(chn)6${ڹlb.CŚz0!&wC)9V'܋U.^C>mVkz~p[w]t_y^$; (U e; Ē1cu7ھu\V#d![o>牚>=Rj:Pq ;PK x -Jh{>'e@ΓQ) y)i3t~vJRz.JpiJ[KJ Q}*z3F唑`𧀚iw|rk;}@%c' z^Zp8z߈7"q x(DJ;4tU*s90|?7H<ܧd7-y_X.Y^fXڡ?(D}>ʭ5#'H99:)B|GzhÉvŽioE2oCV?S&IVk/km%k³o @)cQnL1б9PT[!C|ŞF%H 6VVrlɓԇq0~P R+A.l_|KM4٢mhqǐ &=}a)e {otQ&f9j2W<`˷X-ZcBhS{G_%\umPGΤb-._`d:u]kT]MeonhD+XOHlޒ `$xh Uk)sb=WƢ\,,fIUX.<^Zwk}s5Vzumǽ،nQ?Z+\c{r~{]V8lc}'sYm[|H6[kF7(ՕɹZ'fm~"k22´m8ZVh7SЎ!EoYPHp%r A N/>@| rWrkrljrd*F\_=p.hMO1dۺo Q[h~4~&هFg,OA5-@W[J PgɇA _jM=h IAnX^Ņzy޷uZJRe[mǶ\[~^:D(>)(G@ -( -Qo7+woXtYqйZץD)p Y^ Z:xj34;tsl (`Qrh8>"sʓ.HCipE ~2}>*P5JOαX^6;Ov\]B_ݔh EFO2?X=E*4];s-[] 0ņ1yEجZBn[?;iϽ>z [3{ -(?l}Ec}ɰF&9zg^3OG#:%VњaAvLRu"k!kt{8fI1uuV Xm?h璠gp}A m۠ - NR_aN=V|y4pid2SLs߫IJdh#?͗Z!6gqW Eg98P$&(^0{]PJBTcaʮ)r"1Z bӿeyy֋3\esdƑ;nkݝI~C_Xz42OF0._@[ ԃ^_ |4a>Pr!|?$~(= yGRoak* ԛsG~/jV1ʹoSPǫLˍ7"6r@/O7ȧF&l -ylAi@)e#/C|wL9%9<׻,0(:U[ؒvOk5Oj˒r)v󍧭'H,II9,1jSgaDof|u'ʝvJ^0hXwȘgb9;7:vZE<%n~"zg/D"Vꀤ8CG4~c -oR5Z2rcCa%;~v[RӷmL< $w2 -o.sH 2x-)x܊Jgkq4yF B>TzƟ]org'%Q(:έA#(j{zƂ,Y]*UߦzѲvl-x+PhpE~M -A.9s]VAhY|K`;Qkim%a9I*;Lڎa4-gi_okX,ěs$R#/IVN>˓aSBgׯ"~֝Zr1j'\[\#.OQUzt(/Oqmӽ-keמ-mf{]ݻG߸5=V^jCoI:INܰOR^!Va3r03 Xn͍؉Nia{.~N礄> -l,f\N8?˦)|۳q`g8xV(`|鸍|G7n县y!'a؛LWpz5^6NsB=IQ.\L!pq'\!N*7O_h13B ƾJGw* >ex*m?1ki$Zrc -l}!q!l;ƭ21*uPb8jnaӭQ0LFiBE) Eeɻ ܥx֎ /h\v̚2s5eQ[.!&Úa\eMB N͔kuzH:C @l^V#P|e{ȿ/MtM #ڱ_nzo=^ax)Ƴ.L v*@p9]-S5nL)¿*:?Ȼw ` 8*u wtkyEQM3&qϣGi/^8-䰺N03YN_rIkm#ߦwg|!/{^a&@m= W4jo2t!SNgj$#Huf-8ܚ0~;EQ@ZNmy<<y8~4[wPzu`vl50F֏v:CNڰX}KlOk9Iq4ԓ90XYM~<\I:{XUteOwảwy=.0$9Zo.d;̃ܒT:yCJEd4OeM~<"\n7:Q"ң̕jh]|-`S1B:jmE:Cu^N]&x7#Ԧ`P+&d-5%L;~4The&@8m\&>nOYyhʦuRqYZO=LxHˢ/OWbP YK h!;SVU Kc/IuQ]\$/뿂5ϸ2LWįHY_a=]&Xr߇eڟlőwg !GgOϞ>=v'UEN3kTFmsWRh\~SK-_~.)3{>9:&3{:KMWb"T AV!zf؝Y¯훤0Zֻj8B{7WkI+ clgey/ku*1P~vWe\׍?]O*I%N7 L7̦K, P.+uAnMfߪ$ 2OU'Pkíװ:jƵIT0,/rRfz%i:->rhU舃6@.@{:r֌99:uN'ƥJH$@l\E[+!_ey+nxr '?BAԚ 럡HT}h$ -%}&xȭQ$ :l*݅dd"wP)*&m"[߉>~#P_A.ٹMk|΀|{yA>SjL",rA9m<e R5Q6o1*oېJj)uJy7&-] $mih<;Jk&Š]ϙqhB!mWhwm< -Ŧ:g;};%Z+|!rr r Y Q>8`(/GPh4| #/:+@4:t1c'_H\ e4r۠Бg;[{^~̯?}䋥Ovz{@ P0(0&($C6 -#0wt+Ѩ\{V{Ve -q6w)hgMhhS=.wVֳ6|h9e i}e({YhX!@p߽@jX^s!ݞvb/Wя߳5S=lǬ,Nx_RW7S6}F4vnqfoq rӟ4(PP8$^aL&,OwrQ6޵|IWG9$o6V9Q&嵍d4#Ӆ5| l &Ņ|7q,,|݁o@1uYyJ$4tt$ .DB_~Eg'dv}g+6U2Gm?VQٟndXx9U]_uYWhO~c.HѶLVr6Mi\w5.iVA߉w#%l@3mG+,sYGiclmwlBM낮GF@Tb[V3yOUyG7uú+m; -_-?wsN`m&{D6r+A"{Ύ+8w;OIΪDuӮd:`)Y~4u?qGcqvF?d쿒qF]jɕnb%O_{/EяA/"dCY< ?ҋZQN*iP߷I. w_wܻ5-s?9JzWf S8>can]VvB+Ԉk+^Uͦ&/訳 >W"ZC.2 ??^K -p.'G R;ej5R9d8Ux/9νƨB_heBεD)wۊ}CϼAQI[]jIYw{@Szֽ;5wI2=_;r1li['1k)P;f)m,]^T_U.Ƕ}Q[9oӀ3ǵtrFk*_|PMZh%fwht\z~ (VlNf0wFӳ4.uλB;qUҭ8Zefmlhrh6هl g9̸t>;~檾/7;|+= kzqۥן}Dcv37灗9=cz+UtΊlBA'QY[d4_p̽73k z-s-TzbnQҕXl%ہ:{Ѭ󜣵t],vҍs<)|Rm#I*.8' m6M M~89qY[}7.]A{7x7Nvz}˳ypHZ]27#5RlOgݮξܺb[I>ߪ5Tfiz؟rH l}u;u|=vյ8tlΖt4'2Z˿zo+vŒ{7՗l,毜ӾMVC>qsgC'<_/r5ѭ尮U_N4kn?*;nh-чVIL`7ͤM\nv Z!6?>63ɡϬ!u=A]ԣiN;y -ۚTNgTY4xn,j 9:62φўlj:2{Ѩ](ݢW6aa [ =A\:};WOsCrja%J'Wdeni.w$ %rW yZ._@8GI`[GNTJme7nnc3:]6K*ZV);)}*ԐSR y0rrUw?~f\`m5ފC} :Tc\;!3N4N+l)[TeB2+lGɲ@c3D+3.UWЭ W~0wAM}jQXP%o{vq֯ -nPkrXMnPEu ޶nh!1yť9~T@i\upEE{}wuU#0!a۞*62CÁ `e8EH+%@ym^Rh+aѶXܧ0%Oů=;Ph%}uXCLt@Hxcx}# -~0d6:otlM$Q -{}0گG_7>|Qyp19@W~(= DO$5 6t.<yld jA̟~՘ߜ?d4J%e8wL|tzp ƀ!  s `-SK߄p׃} BNvNK˽"ƾKAx1O͹qj}՛!ɤb=9;X ج5| !b@2-S+X<У Y@օ*  ٱW)?geQ;M^zL0`Gro'Nr[^(G\hó5Ev߻E^wl2'%k/ Ȳ]a=BP@>FȒ Y+U::_JtLZl\ΣU<Ό.Lib]7oplrVG% -{^QƦej=@PB7H~~v [4n˹eu3Ov. 0˜A<>R>451HI[׌Χu=Wm5pK(sP誖.*K@ۛL[X<=0}Y[~_I6c]i9-w}x-k$X޽on;3F3(nO]F澨DZ롰\6D&Vec84PIGw : -eT@c7)ַÜSEcu![}2^Ԓ.5sGjm Ρ;;﷯y؞ - :`yW 4J 9~WV N服~9#vjZe[Fg?P[|JJji\o'C*MP9zP'kώ53kѕ+~+\]̧}BG}̗7$LUsژKv䦥I)XwG&?wILVL*JH^q\ea%v=Wjr%vûCNn-wVƒ@S͏*6TyC0d/w?||~G_z|>wwzW.F_ -֬Hk)~VMX[~b1hjUdF[8Wu p ڸ s8n|%fz7cz#׃.S %q!}aDZMi[nT2ƒj&q Us(W~G2`T !Ŀ2O5 FiBo-/PQl9 x='2ĖAt -ZΎFjoFDn0ȴ?}MsA&$^3 wP& r?czޱ|9u;=_߳o92^rp:T_ԔsJ#-}gTj烽cS[w*DF[gā3+~?V|@#v ţAF>]&`rK-oK'^Ku";}֘1="1LgTė\]nYp_.fiGۥ>Iy X>>oScVeݾm0N2[ -]?;zx h|nmjp@58&rNӯq VubQwjv]?x톥7 Zc^BOoZy3oLX\;p.`@~:3RwK3qCқ  ױ u7z ~׽wY.*9e6O՝T+=X׉v,{i,^L:$>1vZѷv%||<$<2g!츾=X~]Vll]ݺG8Vj@t7H\նl`:k4EPE=hdzb+Pݹ-{z0#*owi%E+5GUcSvT6ZݨfEq3яJ<;ܴ3q9\.̦2MZI͹ZU<[UIlGUۻV be#4,ɇ>xRy]Ւ--9jWpS.O{4\an'Tbs{eVXѨ[(݉lNDs%i9SjcLN&k6e-}8C 4+'T,;ڊ/9z$Y&jAoWfR#7$cR 檳=pa,XC+MWXTv^sbI׾wE?Ha;9A(ԿN|j9!nx(Iz^fVœN5?kR|[,GlhUv* */ t_Sׂ\~4XA+1:f2 -mWWiK1,֋ۉi'LkD rd$IB)b+<9t*̖Rb 1l,eNP؎|0-r'' -TaLBsNsE&ԖؙX "oܳQPQ2fy3[ЗqãTh8tG9g*4@DpȆ~C8 DR6S z - -bZOhEN_bBQn^]-:* QVY@.cPuheCht(RN*$p K޶?x|yJ|RlTu_: 5w;qe`ӗ -q S(@.lk:՘)TPux֥V4O hS_{7?| 61Gt~l25 c`݁D.O:7=!2uӰP2E/ss{*b|@uu&ꆹ$=ZxZy ,>ߧz` Prh~Vq:=e*dcp9Stۇh C}lSws`,ѻ?X7*|︳S_-XSaY+45U84j @%g*MNmNǍ9x@)uzRoM/E`ȝ'/]ż,ݕ|g(GPY,+nrPy*̒oɠAc{nl^5Rc c&e'mEݝ_ji!e;Me?vq~slόYBMHAt!GCS2Ք^YЏD{,aDJ幾|X}O`>\J7Q1άU\޴&?ϓ̰b,_d_817in"Z"gmx -p N%0|+~FxGOq槵7vn 傴Uz>p¬Li<IG*L1wf,Mz>HDw@SІgɥ1-&yxYbI2'{;-=*? {g{G&K%yɵljMAL%a&qBwp3` `*^U{k^yN5A`<.7b^dWY'a -5RHd2sn,D``E;̰Hh(p`,;4@089^[LŃמ|i`{٨,p34$%Șr7#7T:n8V!.9,'e{v~Kw&xBD? 4Y(аK3\*f~cG۾V.=>*1͢8 =|ǘOG7 OtӚ>>BcGf} -~;=ek?z;WPoSB4_Л4ux;}kPi}q 9 BV> ]nBK\Vݔ77KhЈ*ϫrt\"Z`dZiW_jd>>r0-X5>탮`@)ꅳA{5R<oӹTϤ'a{.K@Vsn.^W"MnX(+4"'1ӡɳT(S>Ϸ^,Sm:@b[DS0oI+І.AxRt1o:njy]R?Py4k1SmfhouT ^rul?=BNPM/Ka#,Ӣ3{H}Dwu }3̍v>OUbz댻uԣ1`'S-n8;;=8ZVutL9ãx6}[lb721*ЭS̶%a+h pݦiBe~ fa{Kl@2婷ϕmQ.Pu*VPx=Rq4Q.{`FnlƏOs:(nl.ɍj,Of 7\{i )` >6Kk,:ߖ6UţqÇǽ+S#Jby$*ì}5*="x0>\ʮR ߊzynv"4"[hdkܯz=nks:ke3 a?ŧ`:ǝiρIoQJ)w?zKBX`6;mUפL=ҩjz4jwjkZJJ)#ZwM/ZPb8ʳ[`'d@#֭FP}Od.sD$_Ńݳe~h梯8ה 8z)bt=Ub/Adb[uz=um](K}Js"giX0^;X^K/Sa%Hk 78.I VXn-o̖0j RnR%b~o[#w)'yb -/)ƥ%SS+Hk<_u7?H3;,%|1VsemFQ5Iz[.J-9+}N䦴QI(Eob L(}p2WPpfBD䮔4q+ij x>-8Ħ`x^ՅǖMJ2OºʧK J>ysn nDM9m>Z?/zt4TmƍϧU V!Y -U6떓MImX؎qx<m|sEȒɥ'Ck|ɓc;:־}i\/yuWm)\FzO\eN_wQ`Ȃ7wWY 5D|[# aSa d 6鲧6C@%7z8YWN|!Kb qlɃH!,* -!$mvmEʯHe!$MbIv@7y(2}£LTO>JM9.>P]հ6@.t5- -u;pI CB< -+7KZ|9 iPh*'7`_vLV/M: >@5@c4hFl$oyϮ23%/ qb:^}o_TLQ __˔С5`m'Bl&! -]|(@ǵ@a )}5 @vl'[$<6 LQŰ}j>۷r_ŇBߌ -:! p;1Nj[l|+}lUu1!<]{E+yJOgKxRB>Vs-xn7Z(+jQ\؋_Av PUƆ# -Y@xnu~,? ˹qqOm -ʻ53yd=] -7qswcD䖎:릐CE/LPcOWx:ZǍt8\Pv^[ԣ6l7zV:U򮳈^ߗcoA:vLt(wkjiO@3P$|/Y5=eDÛǐ6ǹTBRרKBEaUuq* - L~/=s -lKqqrR5jbރ쯀.:At -Ȍng7ޓNЌCNW}֘_Pc ns'[M{0Vvbs|յpgKv}tH7ڡV :gϯ ~> ߝTy9*{Hz+]^'Gn-(AڪyEyUt+ZStnEH=T^js6_gE0h7#jELOŷ虇P1n;R&MRjp`-cbZӧ[\eS8Vuz {~|ЙI0[>?y de]O3'KO[%eWW@L3eƸ("u4rz'WyK7=}B*SU&?su~>Bs9 gfS$-7k}#ʐʸ:3GzmDzpn9ׯ~SY-d΀gS{*GK?.q}O.ҦsZ+ JCyF0mpx9h#qp0 -{1qd2KBE!pp+ (Ȥg"ed/UbN\z۳?}Mci7}ZbE81'u54K# MO QǏo4FJI &|(nph\Odpͧp$C"Wr~S9]F.ő} YHH7fI|54T$ņb^ǯmDn -=@5)L pGT~ՔAJ/N)o.FՕ9[@Ypz_:lNqY-a%/)QڊQv PHUT"~i -woAx;@w+9sxJal'uXqGNC|"o4EиχU_wSzgR󣺙a}'{mz3]|j/ƜXs}dŨجbgnZ=OnuW O'ٞwkϺNzw+ۻ ȷ)Rh H{b -hC}MݾGLyc-\tm=@G3G+G)ͽZ6uno*Tv]{k)#eBmmUjdStWK#"@􋥱 ?N ;q{sIs8;ܯ`vke ~ ]3*/on[<[é,Ee)V&mrOW487"v;I#ص>;f(|Ao9^45'4!6ҋXrG=KɯΑ].֟e\YZRqpx\ -c -h) ^(R[d˟Ŏtk̵ۅ3a6 úwXնɱ[ۂ붎qýw]/ ;q1rt/⊬sIc05SWfZj^B^:~(!.ʭ:QO~>(E.q=Eo/d?l~S0qȶM)d*K֐N"FaO u/ U+h%?Բ64/yC3oFNFtu9C_azUGdL(rYnϕ{T .C9eNvvnYieh5x&\Ze%x.Y](ōM-gb-rl)3?#clT"mAaVQԙ$bخCWizsaӛڥf,ްH -DܹF? Zf ͔ѤFvm)56V2<B++cޥNrڣr('G:k3t%^ӵWkEZn/ k)gw -muC*ޟ-29/<7PM]pf.5f${. @' ~>>zWg*ue6{LGu- ҫ~42ὴR"|0I%wx3CX/C5&JY+B}U*wYH $#'d^;N^Ae)q( QP7Z\9m>`?+ L ^ q:~H^&YM. )>}a  vf{2q*׀+\3+@CG ЮM/ zgS -P4^Xd=ydz - %H5 z<~)FG'NGcr9k.}{86GjmkC -@ 0l 63tCm@%2z{mgnsQbE_&.^]>HFf!KMj;ZZ3z`B!p% tF=H,Ω:K,#3]fC ; -P<[R58?t kpzJO}.NKQVX7ׇ4]{7^ԍ`BCzNKW+qzI])n'9:O˥(F -g G dOWG+=^w]l-2`kG#"h1%ttM[hs<93;+'`0F8}f0_NB%(AnrwX\.mWZe Ǐ=2iR,{3im˅<,5ig5r|N}Vp`E# iU}//ԅ6al|}3N~Zk#hQyZb'{ɦT[]5du\ɕGFRHS5r5I6 F t(0ؤ\.wJxmH쵭_ǟ7ڗZ-ӳݬ4DX,-z˸< JU5=P4{dKJgf=G {N/9\rosO6`rcimm mCih^~1(y٩~O)C|9_#1S3g:ȱDMcr#wC7kwl 3=8Icn\"zTC6VwWNΊykrLu@n]|^W؛x?8ћxCReO΢Eei{YSMj>YKz7ʼnUt2qkWAXZ+;TJ8޷:׬e*׮H:Rl+& Ͳf#^>]}6D6OO'FN:K>v)/I[j.zцh/`)p{Z28R|Jz;Y:g4h"V>k32 _~vc53vh -'r7WPO~~\Ruƌu}Fnjt6G硖#$g#sUbvۓ{-,Ł0mEYBTū-).yCޭ0}&]DMIM eOMΕd0Cz9S->U蒞m>4x-%!)WP:ܐ { -KmwYu )3Q,27%΢pףPVӪk"7 -V{zPm wqfѴKc tdpxq"_¸̲SOio™Flrv,lؘ%#sWUϏ~ժ?xZn+3pe_a> ->-7BSFWe,9}ߙ”ݝ?u[OFisܧ3KqTQpu nŏU[N_+4T[/gL$L)~HEW1el|ץ -Ѿԡ@V7z,c0JWyҢa9aXͱ<d8eThDzJ5RF-q9zGa73SuU)M+Sʪm^ 8GD69+9qNf>~%~1:G)PpFn܀4+~Z ZI]ݨۊ4yV?]q!\((.eZw_ +(7zZ ]PAZ !@H JALb\^C(N.|A%^(fvZ/{=P\ -=.{2NY#90ŏΒKK ^rQ??-X; ^tQI Fo2\~:nw!s*8 -@t 1V>}nx"o^X Buuwnj Є%?84 v `g0 kr ^!XJOC\JÐ" 1s%1ؿ -|T ^qV?.nm___ q}\2om/y`fLՉĭ - 8R~3a_YU= y\/qo!}]>`ͮuW~6y, *wssntAHzduX+FOy{=OF~:c6HzԻ-a|{:u-<"C^!u.:P(7QC`cd{"*0*!%SpoFaWvf. [җt䞳(]Ua&|;[գgr7@)B%:#@[W=/KK'9o>stream -RՓL֚[dor?&u@w(Tz !PRoX:nDyVϥ{CVNz]G[괈Qav">J6sb{MBC~OM6kؿ|b4 ?v kZeϢȿx`ݼWL݌shx# }<vǴ,C g&3J7bJߍav^N]N6 ?o泦Kߵtj[{4]DL97De=tW2ÂC1f58? þ7)q.$}J|k9!͝1gΝr%jXb׋oilK+uG~&{OYWﭱ-n۪DK;Cc -X/0ROp66s;=stz末9vM^I 4G{7/yDB &iJiO{CA/-4]3[Ϣ!~2Ǘu}mUtl8=[ 9ڰZGcPbYuWiy7j0?rZt4D:_7Q\}UBO|!0#6QF.Y֥IZHʜ{ 2z>u;fZ>9 SZrۦtGzyhy\) \y x9P29Y*?""fEi9, OP.K(ư`.DEϣgv 2ń~?=iwJ'IywYeMm+/Ʋ9 -{tSOF2RD.9n'hAʈz-Q4#GB.zl͒Z+z!L^\87o2L7B~d'.w^^~ +(`zkGV^ϸKό2aixt aʦX~Ag\;U]Vr*r9lSUOoIg |nwJWɸMnBZ#ƫgئrĨ;GeIo&O$zK}w-<SՁQ nr9qFX)ΑY߮4g)N!q%HḪi׾Wqe;1f~r텃h2ZLFYUyKnbb|p@JMf9Y;f -ǞY)uNK- A7˝ծz= Qjľw1;}ni|y@XL}ũ54=%%YRG&|{_*=/S={LϺ3+L}T8 ԥ\ig!jLQGw(ۻf?eFhKd=/|3 -0}Qv|$yz?A'?1 'wDEQN;]NA3F-7K -I0mǴo c߮L|f!C#sAjzk66jrz莊 Dr6Olv|ޝ3$R.zqVoo7]oaǤȴ!Csڭ f,c.6tb-H{st⃌X=Mux J~齼ݰQnwօiWHJR㳭0Io1gF>U6r0? .)%$X/SǪl2o]w`$P؍{:ɕ^äC'mM GbnR)blCN *zmkbvѕD7EcVSW,p˟>col7,5V^kCmu/Su~n4EU{>jXbZ+\ͪɴ#U5^eiʧ\#Y>|-qSHGHV#~gNʹ5SiyՂ5&תƤYQ3~w &ڕѹq|KTٽ΅rz6;TUW;qIuQ2LqKoS)SkNc]Kuœ: 1@<޿ T_-:6`dQTҗ#wfI$R R{ f R !!v2(HٻHmr]Yw`DBst h''4+j 6[ \?Ҧr r'*H@/Jt$:j|FQR6<XLsv*"G'>^r3k+.cGoۏoo+A;(tCfuTVx~hdf$ x\`#bl%Aagp0}N^ŞDPhJ,>c~ވuQĽs'\ly`G TfeiK6zx8/oQo÷=(> j*Wu"rSy%眝Pz):5wFb}_bw\Rܻ1 ky ->-mor`sٌs0w,\ ֩|\K 4<rn<67Qo`eP]69B, c{GndKГzAEK+Zh>x7 }r:7Nj2Q9ay^}`}ϫ=o;#]5s9S?ezm(Sqk {( /vLw:Wl^Qa)AG*4~P=};h)g9K۩Ҷ~+@vKN0n^=O,7QK> -eճr#{۳.73 ܧ/|wM>[d˻tK7T?mvR;6#W 2蚓_'dDÒ?2",Pւ$N)A':;0ծiwLc>we,R6ϟe7BLj#ü3[J^T)qŨG=VMД{.w-_@ - o{B> }6䫳 Jqt 6ww2ڬ\l#MY4I -F2l=eY}rFMH}4O 0-USovtj4 67@WA1 Cg5t;;t0G:9]Uֶ؟A0O#s?W20C7]5ùgnHS xك[WE4}P19L<ȿZpF2*>^ER-nRT}oVԳ/ GZC|f5 bm?̫EoDf=7 -̩DE|yC۝'ϪBX{>W3K5t-,QeӲ3ޞrXڽ}J5Fd+ȉٚG E+3̫r88_Vݡ_ta$@2%>qoa5%ۃ]Rm#A#d/֡?ٝ;n@F -hzQ,-3k0)N,3 b=犣}=SylYY~&H^o]"Nsd!ӡx׷xkLSs.7:)yVf')η֦PC1w¹3H٫I :l}nH~6aGjo!õ"  ?e_bص`U^̧KExc?û=5L~C?XnrjcN/PTT_RG]T9K'n N TA4'drh2^v;#7要8gtӟF=w~hC"jT@3Sk7=JϭI=,o$_Jm|Yjj.aQdKyUq -D3Q.Po>yoy-R-7B#}41s_\`gLSzlN1oNLL'Qyrb{>"5rg'"(IQe!E_4Zɭߟ/nI'ws1gj^vvʹS3kvۿJLFxd>]ES=LW')yЭGRFU'р9TFo6ݑݳ%u狡Jʾ+4S*v[:Ȗ55[o;G2A9xu}_SޕZ줫 ӋC܇τpeuin%}ݠ~pVѫ1>tGDVo\,5*4Z41Tzdc&u;M٢C2U|B{1'E\ɳʼ4sfe -xN3 %Þ6ߙ34neK4> 1Ա5F\?sS?t\aszԮoh鼶\{5^g+Ԇǟ.R5wS3[nWl)UJVUuLjmv -4)-23riW]UL5mkȵƠm][vՠV?mYyD bԚ9܏F3ZUmdiFtZ^!W(YPl7uwqV OA*Gnv 'XmRsx T ʕU=ɣ|gX.Fqܛ J8ϵ*c W66YF3̧]dlgUX^-4;=W5MZFRfm䴐|, -qr9͵I~:3i:"'2ڊNGW MUքYB*(D@'!@<>G0ěw)wTb.ql,ǔՎxcKO|ZP"qeR)7^%v') 3t&$O䃗ITVBqFN,TSSzTHux2vgM{`?Gn4g -g {o^љN? I.7kC7g"QHEvaH%:ա*. )8Hd!tTTEl 4Ƞ{:%iHD R Rm Ri}|U~7|dl&~& hB |'I5 1C<Am(s (/AUHfz)H 5HU' _ J 9z2(l#><ϓ˃vn^_r#x/X(?U9$8@34@L  -mo \ނ98Ծ@ʜdAJ j{eYa( "7UEMUEx~} ̞^R(Luà<)F9 -Pg:def@=@ VP]hAH@G`HJ~9וF/9.?AxL.G~c* -F yU>o87]U].u#y%mNg.%<['s֭"W_,3t{7~qs*1QBU`HC `NbV~Oti6cZ M -w*o1n-?&R92a~L\[(sNU@LHKIy"pL~̧}[Kc,>q6+bPnOHM8a8LZwq.Hrg"Dۙm3?/Z`9΃8h+=z=~-&^G -KӪĤs > A~{wQwWۅ LJm]lڛGn/uòrϮABzz;J teVdk|Wv`fv]up N:6,[S#nw3po|*6S8,z5pF6w|5ӇX񕦗X9jY"*$+c֚ {5wi_#u.*X^{;ٻ}[u.cվ p>BC4Ys#̬RX^凗rب;+}".IAAsMv_oA?"1h+pk.w[F4 ^Bۙj7}>J;6\ Xƍ,wȧ.pD&"> iy#693fΊ.͜C͜_!E6nm4q@ZX94?gv%1wW;~' onb3|q ˶xi)A[[3#Jsҋ$*“.}Q|PhQ.'jyGs2OhCo>Y.DbU` #E4X$Ԍ: vh /Ee@ GSs @s2e_tCܠ@h٭-'fMv kBEk/L>:߮LhA;eG;Xcc{Ln2 3‑ -T7I+OB "A$YvQzr?]r/<>i3e/6GrT/,ghҍjVw~k:1.?2߼gy%:Oj-Ѷ)E 1mbڻ~D-;U\~?>+H8G~?*I4~ZYUdgt|+='7JTՉr[޴A+$[F:h4^{b?7gxJK./=S(Fp a~- Gcn8zo']< AcZ/6YtYcbCH|a69Uj>tcznr|D$'fhA"|$W]::}@3f~2:}׾w~sz^3bmxm wA4%fO+QA2PFXTB -Jx909ͱ`9޾A&9qO4\r.y//'G/͝QZvtn޳ѭHNޡ'TC~ġ0JjR3!xK\osKEmi+j)^*^Lme{/v\|pٽV>XBV*Y.*(~J `Sw3OXDZ &jW=bVtq.db)L :[ږ0RS53vqŽRypikoyK ͬRυ:Zi{@ޖ1ص,ֽymn-~?-=e'!lAOo;e?259l74mM/R 6N)~lU_?牅kt+եasV8BV+G&"r4zɲQ2ώS9L}nnUk[՛]El 7OsseZHYJA#ݾw|.;x~2;_Y 5NJ}Y?o`^Jw7&S,hH4M)IW^ܧym{sq5!%e%=N9%E!sqZl iy?wE[ V5Io׳>{J=v=V -#//_9M#D³ƋZC%KG^˵jޫ͸6kٙڬd`W9?\rlt_gO;;e[I!e7cUhQ'-昘nOv;a!QOsM!i{MM1IjO?xr7ǪIj˗*f)UV]a8]aYll;.DK|l'riJ :zS8 ?8v*6~kjҐEf@M~h^j^.tƚbEysve}&[W^7ǻR:z==ʢXL ۿNl_f;m4BK&-rGKl-Mv4QBta6+5t/M]_Svg8qXiR沣emʕ9ZK<_땛;rzik(2GK?X>`AݱӹaU= ?g-:fupȖufR(8M!L~ұ.Yp|5L.2T-O{uV~ bvx#&wƒI{i+(&bk{UcNT_A]*" xkzNy$_#ҋ-6<Nbqn<--i7j:GVHV϶oJ{QʻE|:O*Wsc_wꪯ~zujBFJͺ/~Lb݈qc1nIY'Qi0nm2YZeJD )ҕ˟2_t} ,딜c_s̰|hp@g ‡t困vXBVLRwn2>xP.l^RR1t 0_4:>\vU:»%17/fm ! x)+ 昱_$а'% d)Xc* 3g/ğ -ՈSOT;FE0Vvaw4An?q:΂.`_|_td KB.$L18F;>q88ԪSO݋u.mlJZ:csk8" 9l tIa/͎~QK ^'UJ o|P;3Й8c^V-V6z=0~B:ͪW"5y_b\@Uxw$ Ī[I$sbFzOơwuP5 1c˘bC.waܢ2ײQoٟA}#I'ɮLfdq"s9VA -$Ӡ>.n4u>)kΝA+ע_Ag;?sG\6k@ TһPDH21Jd}8Nr0Ir͎"AkL$(|n8?ّ/{>Wލ}!C;B{NRƕ x]9 s˷}ęR0~9{zޏ[48ݽ1E 7@KL"IiNcOħ #6?ާA^~i\Tn].M yzdEv'ANg!Ny=hZLݨ[%_ղlrmw1btAYoNTјΜ.R(;7p zнwΛv=a&Dž?MݡeGU T2sz z_q*Z4RJڮ+|EW@i"ZxNT}*#o_$0h4@Q~n&ZĝЋ\ݫ5g+-E-јJ`&lG,]Iڴ2X[Rn0; E-,R-ø+q*GJsuL0~Y$/Y|#LIauϛ,L4:O wbwOfjjQ$* '(&wFԑ$#@ r# b 嗨\__ -^ZvmAʟe$K~yEvi 4Kg#HwF}RKm=Y#8܃f jMYsc3yԈJ?n䔬q d'zy9A̰r#X~Qxt[}[P]/\$uɶD nV^M&kJ͖poMy1ts4_;s -dcsޞp6VT' /|Χ16+CIzl QCk xUYxmgU]ٝU# ~]mO97+7GȍN6&' -sCL?hJ}`*2蝰KII>wrOFG30$R['9JQzhK,Yk> RL"sxeX3?xS|0_'KyѦ.fڵs߿0;Wa#m-LFxVZCVMtpM;Oz{ ݝLH|A@2Wsj?'x 'aëOW7ȮAq/˭hQZ$jUo98ùfSn(9O+̜hOM#M{~@Ҏ{kl|,0J~#0GN:}96k/Mzx954X("|;Gl?:\/jk5{,SMԓ{{t&J =c'5Q%'| kc®^K}T)f.p3Io ]͈➳N|57{0aoDsdśUMH a:흊y -ɧ#I@|$%cqOkFԱV^Pqlçh Ve1|-D% nmc+F"#4vtBQdĔ[F :P\ly`N0včgfB(n( ƸܡZR: -1u8̒g@sʰYmBБH,$+6t -]on-4]djk8(4ݵspUTy2!QɸBQgxkjCPtoC^"L7VԟZLpMͽo/?u“5aK˓&A벼>FMg\1%SdwH-,?OvOd]=:a@}>m5ƃ|%)صhw]l@nݴޭ?[w>]֮*sǦ<ʦXJӽ=90ЄF$>4\eEQA# m~+BG#3jVk {5yLwBOlb${On9o(V3Wt1v]iXn[[9dbd)wjq"dBo8]qo+/-+qs?NP괥Fޞla оAwB3Չf*GukL1q]hƸxޯaĒ2MA[?Ğ5[HbvTH~V,d -Xvkw:g(_/.cEgFnrs@! < - , ,28;X=6We>ͧ-Inr."Ri2h_T -.SSf4Ϟ*|Ѭ$|4'LU,axF^tX#wi`m W I!!|$U3T21c_,rFkti ,Ry{X{+}F,(6˸>l8 -߽tБԱ{!/;(ί' - E.a0@jEwl|@{0N]&N'0N<d43 }ɸ@-8s`/ɠؗ8Ao94*_(y%x Wno8E}. 7>>u WsM^"ƕ5kk -1$[d$ےI|,lI&7'sYi++Bh5vH1nEoɃ#񛲑37(: Ղ\t8Ʈ`JxNόrߍGE(!88v2^I&ApSK$Wp_Iy4#-RB_L(Zy]?.^gqЌO}7s|В'M)Nl7ˣEd 16MuoD1oIjI$E[)tsJ ;TLAZ*R'y}h#d`&kٝoϧyBzh%4n|K{gѐ9T%=T>]Ѹ9^~H>;÷.䭗{Wv|݉&Qi,X; L2eKk񾾹l-*؞6]{*lt\4=P2ǻwmW,uW;z\E ѢcyϏOCwT\7"Aś0%u w%iLH_()̽y8_=|U÷8zۼIhC؏-׿5ͯK=Qsew,#ujT*tQRTEhlerK9$1O;n's[E%޷̨ˮ'^aacy,: ֒;YOrq75x *bK# ` jYL]bR61O6mٵygwk7_8^]=7o 3ߘޗo߃ oeWGi\=j…t2g}@oisdCT)%r8*(NN!52.e{qx, _] -rv,l4b^u}n's&S ԔlLf?_GymЃAXu+24Vѵ7] -N.gs>v>,]aOY4ll.C,1;,~_//?"ixO625w«[~,UlXtȽyY>0Ll(TALo>Lۛ.sJ`~Fsq΋aLJKe) YarCLϛFNQd7-R%GTB}'ĘcO7OeW(,s#m#3Sv'͟3eϗy1~r~֔5_xgsqoyrOвK>h1lKk飬.œͷ5=92fVIwʌ:ISEYG ?}rk9r0Ɂؔmz1':R}VV= -ڮf}g0f/HG`Cmbm;|r1+lb -nHd)Ymy5] U*{gҎGkg^k^4X.D-v˲0ў>㬼?0S`zkIX~~>'`(S}KhL vd/Ҩto88x5Bx=Z'J/ŭ@l)4IYhCT*2 -6Ò_՗۶;O;9[ƫ5W5wߓQIWO\kC#m5ކB -i -a7 krk_>:4*FԓUS;l8L,RZkJI%:PWmS}N  3W!j:ƔV#%,0WCb jxY}GA=Lu0^ᔅfd~` O9I-/^*i -IsULɤiGeG}}֭qnh+ $Dv:> ˞j*uϙ^W/lI* -˽Knn7 -]nt]謎w[(`Pb.MF5F+,bi_O jDRtFƅ}@fq"Nݺ:}_;U׷vn}Ktfu 0m߾{ 6y-X,Ųh8vaEl34a9!H熏+pC6|}W;KϾrZAuְg̓ЀYnbOe{P66V)Ҍ1}Uj4iP'N1nvAF#/xú=LLfKU㚝VMOu9 -kf+u+,?Ŵ8f`Je*^waߙ<6 5^,frJ`e4=j}V:=ՌDki(nU^3b .bڪV ,VlQIMqՏ -Z -&uM&o -Ow?4hn yҨ(kv%vtWk1`A 9^2(qVGB ҊWmu/Ԭ_ė{<3"`srkCz]nV\Sܾ>\vh3al E&񊁆N]LLhDN˘N=^CH_6kM*wb,a.͇ se>լ̵ؐ\@ ԗǀ`,<BoV!nX B˴[Vl a]+oUOP_ {WhӨXxLUb֨rٸo.f )-΀ XB=6\8$&q[dWD(Z;ʧԿ\ 3=-g>`rY7!6|dGI5r.ѺoYO{x\C9G), I:9mIv1FǸCwZ }܄.i7ku\#[&+BcQb>P3;g5& `ߍmX)ۍ7lf*RKMen'Broul&95oV`4bMʹo#kH=UԑuzXx?w^ KPݮM0f3/p^֓Z^箋0m啣j6Hʀ{$W|QdǍ(ęQd{~v`O}^gVھV/}s\qn5i0@+^AJotZj,hfd} kl6?ꍷ_DV}/iRA+;dL>LhYA4h4avJaYca-jWfQ( -U |>C>;M9f(<͎ Q8S惡yČ?aJ񚱙eW|/RNInO6 M92m눨INI0'BpFfWonwƲ}nχVzjF)fjuFu}Ɩ }:,4qӋTNdZ.h:]8]daz .?Qᭇ!wPi-]qa6Udu2'w%9+')ϵZzQ5ɰM͓lmE!%xrE8Ƶi54k*5'wDo6'r -d:}rB.EXF/LO&OL:B/g߬LP# 5!2MMOĬ1{x ]t<ѷ:_D?{RS70S :n$N0W^0>Ǯ{˲dfh9e8T%pkz$R :Ja6I&lk5ʍz9*w]WZAw'J4Ưhv7W\:^t>Q*ҝ12bjM7 ~ځՋ^ -O{mjQ̏JY=İKiե\ 4\5ȒRTv)&`戋9]-ǜɸEW)j[д 2gH~RQA=ZgmV&:|p3 Mn@S>^?ŰTʕu;FUQ>MdfnNg>sja%2XBؼ,#(Jz~>*z[zZA"m h.d -ݗ ϯ`Q2,J/N]6x_\BwJ,{Yb:O{<֚-)|Lt.9(M핛@%A\d:YDmۣk+ZFoƝvEQ㷽Vfj =RBc ҜU,y.w">q3_IX$c->EⰠ >5 څkꗣ}rչ: NwiY/~`S(`6)/66GI%tG-sjxxve>^ w0 #Z!dp,msܛޫ [ó-̛nJbGh z5fd-fwf:͈pj†jխ&Ю0>q:E^ow>-Z=Vv8..mEguSڽig4a֓jC}Xc,FvXc*IId?ov{ݹ%>lErSxnM wd[ҫ5^`LF~JDC\a;j} -3q^YU-RWjy1+W9(Vb\Y-tU/f8\r8f8EvҟoH NTwAkw;ugUo9W^O7reEmڕv*SEz]=0/IfZB"TKvq0"%Ԃ7§ MRgA~f -+j}sC8cv>3sjZŸcitٺ]fSY6_МLr7gIg.;RnM\d*Wj/9keȖ3FjI|QkzS5O sz*.*HϛV+\/vagJ.d.CS .M ^u 6CȽlAȣCCAH T\=oW<>ZuaԻ|kA=ؠ΅#T`ww5s vkdzIW]Wh8rBh>PzBEBh#ڑl#ʞY#nB% PPC7?쒞fhr|\ -Rl:^jXڏ7?vY?Hbj;9N1NaȋֈNlp61T?m*8Dq -*ӓkxyasď薽ord0z g(Y@'眞n}5m \:oiHq:88S#tX}Η&^s*g^\70NߢgιH(#@ğLմsߝ\_q -32{>~`=3_h6H D6 =_nTKB^y_U=HOnPlI%+K SV52t^-Tt,߾+뱋je4&kY[4{o 6HR7:TCܸfEY}ؾ(ݮL%ف?+Pp 5\[+VgLFLs7ZzG%Ym7jDЩ~QYCmBL2FJ2.c+XB@.egSh.{?etJ8{ŢQoTG^hu LA>NE琭u橌\O(b?- \*jpwD=&-:0jOO>vjGe)v\m']\}M_~WI4mʸW-):1]-KsߊurAR{8Cre7u< e$IvQ^k;ul Ix-8mؤ'u|AmІ|^~0ֈ%/`|YEIhSc߼xCx}5z0C Ԡ -}a7ƧE[~/.u SYʋvx˝.kkSm/w!|;9܆y`34xHTF3xMuu+'We/\Y._%|4X Qcjq[1"^=X\uY9+ϚUWp0+1w3';L -ӹK*% 7/泱lbMԩyhNv\гnK 8tL9t_&x]lbwdo3.Cx>А$NꚟƄHZց_`}sEw2fs)I7(sMU.X>>TTr( (` E=9WsKU54rjQi c?]c?9u>?`SVHWS*Ds; /`Ɲ ^9U2x_:f^U;(fC\X#BmKH{X NF,Q !MYR`( n톸=9q*0rW Z':-jq|pxyߡ:-[Gmt=pUKUSn%GeDdٵ,'il~@%w۝#:Md tpUH c}|@B^BQ! -LnۯdUup`tVZ{OWVιf4*U#,k#g}_e]Eq-| O&&N "\+6$|𽢾83pOwY=A :xLۈeF(SXzُҎ!)S8_Ա.;ZUBN B\w+l$3ݳ(Q#j-Ί޽e%^w‰^R^1 eĂ~LjSA+bڼ6O6U;>y oAZ)SzcPXr3ѯlzF٥NZ;vb/ B\|R;o%Ҧ†Sd#eV7@Wyh R*{Rn$0a3NsF -j/s{]Ѳn)kg Ӫ E#2'K½b:v<,tQfJsޠkơ9fLEeI\XL <^1a K_0ja>0? 4:H7_0ۋ:4Guc]_QloYeYln͚j/{nZ'\έق_hck>}|TT-9=g8em/trPҍ9#kqFWGk@s<~&%H+8l]3 WT6mNfGaN9U2GqT$Fk7CCCOܐ>$߾n즡#7'ff30]d݈D:=m6bS^bsNcYf+ @iGЮ -u{o - _.l/ l2lG5l"tx =oҷ67aACYk*-gK F>:˻ H0yMڵ-WiA?>|%+=FƓ`10tY5hSn>Zԃ6_L&7:T]ꊵpty>Ĵ)xNep[ݟ6bմK+|Ӑ7ִ"'0/FTCh&8mdfYgN9'jR֮$QehVU3hl'X"S2, ' ,`_8嘭89F x{;˝j2=kdDžFUcZnޔv]=,9ÊXB봄r -tb6w>rbQaIm->jd r gԼ;t}(TJ;ej -}T[ ,BS<琡|;ѓ6B)"B"yAʵ=I4$a$-I=O1& )Mo%/m9I\%yP_A(^= 0NQ?8M186%T8}3??%sB--oߏI]YTG W7^ .+xSV֦L^՗vj(%IS3U=O7cO:cxKt9:ûNܝKmM׷[KWU+]l4~茬zSJxTC.Yuao3rC!yFd1>sq$*WkvŴTnvUJ^\tݸ:_x;#sr|%}~fûe@#󣂵^wkfgVWtנmFJMSkK&mNC9q?nn9:)nS}cWYj,Is7]dq^iVDT RhT`;?טNM/ӏ33azfvvپzdn5s_tA>[syN<pl~g UyL9M%˙6lrI'<1s% N7Z2?DSHQgbOglOvڭکҧmy}̩^vvrzi^'r ].IyИ71H!Y:'e7cwH(xf23Si[ á+ɬ9#\慙GOŲ|y ^@w6ۑNxw9r2¢' ծ;h;B =*M륅\bvZ~(s; 1^q~: We֪fsG[򜶏b0үq}eFUV׮ݝRn} -{]]ǭ)^ްXzLI Ki-^q%QY -jpxh.`xK4SvF,4Wv^n|c AyeNO8ҘC``1Q|v `{?@TMr_Cr_fTMT>(P"hSkڝ<_gWpJ_+',dž4cс{cSUT|j@%Y 8:X znhЧhؒHZz ܉2ٷPFX MJal0'<,!'>[x=<,9mJ]Ec*1%7@>Ghvn̐Ni{i/.\]:*Vkpv)R 13-`91ۜSjva -5L(T$h ~@T<:&ٚzCbza Ubc_ƀ@ɑE:ܖ_Ii6ͽ5چO:܅OO/4!B8S ?l9sfYh@ E^F:Nr:Ƭ6ֈt$#s*B^qt]uZ-%n׃m@7An4&ljuVh 7/&n)5L}~w8vsptFۈ7 ich^Ixmu3AEc5P_,;MF#,4|1>l'|[.WSB羖 Z^2_˩<|FvBVbǍ3w#ugqaK^WO'Vkߙ*YTc4Ll[k@m6UJ56kENU&W+eǭzlFn2\]НJ'_SͥҝZP2u WTg?x=D-`=sT WdѠ[>Tҥ_Jn*!sV'縰@4tlH惨]^siEsj(JK=X@P5YsDwb4u5#Y45x%~ޤk-yq 7XXRޢ9NWY5ftȘ5ɘur6xU~1ȎU#lxN͛U*leI2\6Q=m伂VY|$9/(5b0=-Ӭ:`w>"YɞS,F 2K#&z!Q=6bv}h-+6GjMDhcUgҫőw6BjMWPUSSLz).Әޤ8_* 8y5,՛D` g‘07O. c6,(ʦ4vKq^`W޷{f&Xp -8RxDSg!08^NKq7[ OiMILʷMcn$2*@a>X^ON/IO ʯ  |;y"HrZ(T -z -Ey# @)JɃU!*X^e0&g!LڕMOݼj}*ݷ*Qΰ5'f~X[#Bh^_W[wW];}̨آ_$a$N$Ue)q)((2$l=J1>' W̤)DVBʿD:ťP;9%fMls`jh0IB_R85SO)PNDR -7۶).ԲjydP v'uRQ}W{o8m${)MB3jU' *e>j6}dսxv'-^@^,A\M˟ZjUOt{92 TRO^e?&8EirH//LS%*yXc*nݕiX;Я)5&sǂ} fS$AKC7x]WzԼz1Nb ]yidL4ż?R-weT!}PSs&Ƥ2iT:á(4>gpgJ1,a?HELsRAXнz&~{Zz$@~.T(Zzh@5Ğz?`gr1B+r?+zEu_JZ[UWFA|g;֨9(Z=[gkszd_+`Wy.a^9y*]?\|@ (fP?TQǃlަY=wP&.Skt$;qEb:j='Τ?U~W{3_鉸\AXMC AU†EY%u?&?=r=O5\s*k.έ$ ={#ʰy޼X{Jș[7+ݒ[_gW;Sޟ -\>H'vg6>G?&FN}6 U@Z} 5_Ya ʄ{dnV[k ޾qHJ3I#{:¯ Zo .J!AeRs ~p8Ƭd)W.Grx; -lm,.+mSkn2Ej>rPQ'eM#rP_Mf_'B]Vȑ٩8'˳alI}"VԶX36Ξ(@hބ&d~OG8MWlfdFc?b6ˏVNi2{cju͚52ZBǪFBʴ2uoEyR.\W i9RDt:i1yq(Å.]uCd2)Kݙ僿ftj4{УP",ñ,`CqC%@ǚtB.L{tdXv˷/knIM-W,Bdkf._ I'`cy}Iː+F!Z@$h@ ǂMeO54(a;E~{^~2@F. -.zb*NdT l^Xٵx,m]thRGq^R:Ɛe⮷8 bj vfDP|{%R[6ix» HAGFth6Z[Q_& "5.Ļ弓N|޸gˉȻ(T2 pv#0*9Dh(W$rOb\˙ڧV:䋭Zބ]yLUBcv5iķ5܈ۈ}#xD?ڕWo\[ y}܃tL@Vx)bSi+D6=қnv~ʄM7p`K<4U+Xc؛vkήex$6=3SqW|HΈ07r 9JsoBAP-6im.AM}/"Rc -fBaz07u.l[նTJ:\U2*^&=]0/~@dX>s[\|Cz0}ec9O?3О}~E5hbݬ}*ZnUUjpW.n )lj%WӋx9)YZ{@ c@Hu[T_ -o7eMUn΋8\E -s2!Ɨnlb{M.bZxh] @;|@Q>}yۏ|fr)!O!$\0ܜ>rRFO\1"\qr!sϥ#7{W|kXg1/= ע-ߐZ; =Yhеd|w\kIX:&l-fvKt9[$ZWN@ v{@I.VZ 2IhƇ/|夾%ɲKTȓ,}S}{'!ZWr?¸PL+` -As^!,)v)h6;Ż'6&Nqmw^C=&I-վj!OG}!/3ƥ4^!5屁f\n(<.ۀ c@ QYgH)> 7sϛjw@@M R(28PiZf1*W[}Lވ.g5$咫U&h5ș&R[\F#![K;{g2CрDj~.|u|}*uЊ-z`;c&S$\-$:IR߃a\_'upv|0_Rc_b5T< )H?k"nM -Pm/$: 8du*h,f`TVwd]Np,쇃m'ۖxZ˿mg+˟z}h ;9dNM[UF $ƫ'ٞ/gAD$?mjI‚d$O4I2O&~~@Bob~W !S&ھ/[fIsjT"orZ?ɳkf3.g7#S祉XoF0w'%~K,'Ǥ&G.=WT צ77Әٌl -t6_$ŒBT\΅t?N?nmuy%Ѷ„f]A#yӡ6QfSyWv:4Ms=&ι?Fjw܈/XQ"r/ -y̺z0ɸu >]IH|jkgGUvJ0 Z -o 9x9ߪ6[|UNLj‘pR_kѾwNHr3ԹN7WwܶX5 z ->\;?H )_JRM\kx:_%;]'u%Th'_9m.).Fc.QGKPZy2^炌r?Ah1{e}DitUu9Z7'/2E-.}VwQN _+GGYC;blqґf-7b.E`v{|ְrV~EeghF9]S=1`4r'5E ;{ezJh{\ -Ṿr#>Ze>De2cELWZκΖ.&Weoʄ"Ѻ<\';8ОTp['kIJz'uvA~kct'Syɾ@m2Rfѓ^ b@gTC-ȌW7R?un=nCb5 ]unOʽl5GY6 w)"XɻLF2τ3d@Xs:RsARUY=ۤQ8wLGC~i^Y? la:kC >,[A7n;lܘjcd\u]\@+pu*u6CH2C\ -tGFY˷%(V;n ٪BGiY`6| ->ۍp{Cx6bm4pG/ҬS>9^e$mj|8"׍Ywr](AN.S| F -tޫst#Nb[Ao4&uuwϵISTg^YIE^n*e>Tʘ:¬"*enפ 'M!>+0k&Ȏ Ni{^ J̼o C\@ Z?0sݪlJ s2r3?l皢ڵD0sPAAQ& sCU. 4B7s~#5 Y -Z3hCb WЦ5 bm2^۫hmS[@^x1sDTn_#*8Dq0u3]bi6j,-]\{3dHC0x t,\`#65x00~m4f]x01rDew.zƎbBwŌW?eDrypY -OK pī -8q=89\<nIeQpC؀۔vR{ӡeWeZ|8.,9]fbUYyijPId>u/˨W2-Fg?Z# 5\!QeZ@oݒ^te DSMr+7"Jy/6GLy09 pv*?zۯzmQp0DݦB J <$Lhi^'̯ܡ1ns3;>/8/=Pi89||j?ɌgBwmp@?&nD]}tOGz -vDR/S@":("dB U ^5~&Vm$d{߯mc\tkUBa }#R?F 3ʪ`dZ [t*nlQ_{O'3r=|,W#`1 ~[[Eo9\qC:޴Z% -rFK݂+thpla[jyW_?=WogxJ !t QyjgdW'Į} T*?\ѹ&iO;}yčçֽ -6;ˬY_,-RXƘ!}s.):;'N8Ge6wȧ2/xxPGwA gAZЌar#+7DPI]a}a{U Qi8\pi}g*HzՐosĈ fc !xv %P6 @pN?,+~^5-koutGT4;-Ȑs8/ػt_6H#{Non^wt䟪ݻoO8ƳmSQYcԬ&Poci̥s2“Rјzou:WYy,}h>]uVٛ kkH7)%uڡA,hk!=4_YL؅5BT= -rI5Z6O V/xm,dk/Tj>کּZ^/Z8*l)ə>;dVwhr(2,g7;57ھź%P5 -k`+U Y-f泿I0 -ݪl^ȼR?paH婕P`JMzݾN*\=U~\;}u]lpwk)鳮jϾ -4Bi2K_;7Oߧkr/߫O_}V(;W*ʕbh -N,V,GJzf\J#*wiܸ۬Ol zi6Oيßݕv^۾P-KSW6PԞʕlb|݋Q6T{wUdٗz*c~!텖"R'FgD3H)[:2'`309\^3W -֏+3_x/ORAiiOb%zL;(KV)Ts'oE#-)LjmY"o!{XlEds T;Fi@d0&Ifտw$>&{Wfڔxƿ:piRCRzKsEPUu%1-.\V=5&#|%QK-'pEXA,& !`߿E,߿[| 1T?Ph -eC><49zclA{֨/*Qӫ3!T -aN)( R#A5HԔO(>0\j^Y^>_Y n㪏S.ɥVf#aFWz !FW<;3<+æIÑJ%8ܕ\|OK-r Evе![1{_[n4z_9t?YUY9!9 -6c?pZ]f) -TLCivqkv밅LdUNeqsO5pȼ zneWz}**7ܚu9S'_0߬'f!CɭL^[26k,avUj6{ԮvbAՑe~qykmǡ!tid7/\ ~[łƍa+w iͤ:L< [XɌ}j -LH2^E\N$5 v t~p~|q -l Iͣ*ov\U -yC %%Pqr3ˢ6!i TɐUqmm7%Y$#:UN.⼁^r G76:6wlh/|쵎:k;*|}gvשȆպ?>§Ċ]pqwq~IOoZ6hH0[j_~k|T8M2깈GnL4*d=K3z8UœrFK=˴qnH\[l?.`g+e"yfޤwqWbWA_hfYT -8u͂Z%gyrQTxOl|/gHn&Fe#k'dt4&@ju9%Jed!s dO/uE35ꔷk L ZxLQme.ntWE w^Fh<`~iGqˈE[{k< -`uv|u!%hg2 y|3]~pB(QrP(ai:h%=mViANebjnpEW) UԪ?Ag:sFM6*19R3,̸ HU:tɊ~ xh-n<3uf\Υu4YJ5JI]O/#.N#)I$ P58@:Z/qM-2}@ -G@8|P`#$rYk1`N?Ҫ@X67t\ y'8<}esZ€ހI1| 1`=l%b)z0JaLq=v1QZ#Q<{ -{inwXfp%6btsU/`XVl0c\l! p 8'^T}p6-i圱oBh S$ {5eJBeFNQSCoΤ?n\ʷe sNj_{k  -RM`@y G z#DeUBs1\~i5>|2-]M[4GA鏊&9FG,ߚ j1H H*iuan Hq kԃ2zi 7@"'q] DX+x|CҨƘ UV6/wݾ" Ϫ/`P.h" A@q(~QlǾݶ*TYEomxϒnZWǒIɲ^Mwh$ZP3='%6ɃMDQBȴxE>ZJu7V[?t׷fFb[& .@!ϑbrgzlJ'?mNܬU9=n3RvN9p ]nxbP~:^ ^<3A?BCK-״W sɟ{|uwj{ė;.-:BfuT)s &Wܶ/{.nZ(Yw>,,3JfȔ…a*btI迚7s^,.NާDO{'cKݮEZYRݺ1ϡn6ӟzCXMAZϽL]ܕFֳ|=uW۽bDԨ> *ٱ_Trgyau3EJy7gG]&82fiA6wps޵2ݿnsΧ9<-0'NøYA\)9556ƠQoyN[ÿ潺?Y;%V^stTCӘW1-[OylzmO A[f/N+7-z85gíi!NHc- ڰO ޷=0T.)UjWnM)Gll^ _K5?޳sgk.ږ2U|]caiF v\{ }X"ǤrњRY ҫgfBS&j+F]#!An~D<x0o^yU=eEF_'igݐ^K Gú5uK|۵U?Z&Y”#3Q6G2Dv+JcֶL@'׳v$EsCGcF`Wx>׉A(ޮyp4ۥ{o.{AGڒoV l<)( *fjTZ5ROwLfv֎娵y1z16\Y1z?/);UM&)4bVNMx"袊:JPk%5;Ԧը 2_Z+ WWMQqِvJ((g5/NEXʥ&{"3-?eYoIUֽ`N֩|}w *dKj%s+"E:c)9mғ7v|+9p.FVܔ(S0~V|tYxraqKnV5ꝥ^#Z}jr +"cVL^^)TYv^(9[扮EVq/0OwYwkԚܫ?W),r/sG4  ysӢp98C$ݠ; %CTkR:] ]+DC>ŷ4)'Y 0qCٻMʭBϑC5Z-qMh^o:TK΋<.RJw8g&Ȃ=>0L6 DJuRxf'a^c"y^c㎘l綥kOӅ DmM,Q¡MΎ<4'4ϫЀ]khک۟6gZZFLgB/`nD\oF씟YxvH1s3cN0ٳ!+]-~Y{j"rΒ<7ձIh4 Ʈ ΪW7k_0d5Aִsϖ*xp\/Lhk$iT:ERD3W}dDQ2z*Ym =v nO[w ƞc_ cE`by1 ĉͭE1N}w5t/6q>7osY3SM<.UR*ʰ5خ1n&FrLYd բVִO󅌮th*ݎQ3ȓdiuVJh4mSK9&"=y>f_0/6k`%;b}B5ͣBfT0mEra6WݿF9FY*M]7l%KÁP$ґmҟ&=)\OHo|{)C э*v"*@u 5[)m\9Ca VY,egeNG7w)a(5 `+}TAbY_/3.z}.ܬY'.4_LV`o)l1ү4çyMo[-gbI8hD88.r]/_ -p.B8ӆU$Bgnp?9a>Oz|Q`p -"Git1X ךY[XA+(玷(n1Dh {7x@ - z+u -~OaU@{=@f䉀 -GTZujc8zh-ٱQ;~ٽI֪.4瞲|?\ -Gr*F -n -P0q$Lf֓/uS ȵ\&Lџl$%61P1wOeѽ>F~pf{#άv3uܫu=.;2k~JLOGVՎMĵ&rLI%]%3?SvNfD3=eO>C.;6 - -;iD+yI]`Ӕ:2oHKZH910W_=)QGGM {Gej'̅N3l796in:C?-|kI u5`+?/banScɑѹ)Fܩ { |)P`0.Ѡ<Ь1?S_W+jN޻;NЭ.-;V>":3)4sHjB,- ?C"XUИp4>V[EWC~ ?8k1{r>3i*gfh -b&r:_pVRGٔOʯ -+2qGnt`2xsf><(jwm4j?r!hZvO3nP3έ2}4C3Md:UF5֭@jNjgTK,mZ)]+nЌ*%4cZt|/!̿c1mvE/?zWHT UzUTW%+gӣʹkyٜ"Bn+K:ې]r#KxvQ=o<2 -U]Q-;~ YYHKoWJ)™% d ^^% H\ڼqS㞄>sySN"[Py'swN5^P?wQ~XYGsvvw(۽C;URH -Q¹<-ǰM+ň.b^ѹ@BFj *%mA.pjK! u^d+{47uQ4 :9N ?4rM2{[vdfkPֻb [.cGJ;[҇gƊ)6DzUͤVۈy3VkN<w8ËfItcS&L~ކU!BC„_sTGtva,ٶ̑]ĕRƽ NgH'{~6>^#C2Eڸe;+fisV&/' ZuX0 ?< #[e7r>IRKk)BeM2bB&]pq;eyEfT5j~,ȏPA˶hinU;>c79\$?_:>z FiIbt0r 1G,\z7ŷ-&x3*y2"C'ZJsCOfýLS3T04ߒe_YZG!gC9`?p1Y_$6F(į 4N|_ >ۘ]m; :5+8c!.ѓ6suz,?6Y_,ݐ"6ҙg vbHMRnb˷CGxJ zu% by@2~qdڨ$VZIDuG^ߺrF8:,n,5n(h>bl*H -^4}E1g Fb|P@U.ƴ Ռ1:/= Tԝ=`**.U1Y[\qw̭&6xUxJٮ⑁t8~1 b9`- NV̤fsu!!Ụ4̓|G1$AMQJ]hkj#YnٞV0% 8n),' 9]4|F +DxUd?Mniۂ1˷\\g{ $HVq2r}u̸IRWCh"/ImMJg]ޏq -?#׷o S-N5[(]nADv!9GZLOÓǛↅu_߀Ydi&mrwݴTwo=ɹB?(.>((p_Øcv##lOqcf1gu#eZ]NId$puBYfjNJ[5 m슍:LMϙ6å72gu΃z` 07]S;'Z9!v%ON3 ~GOVgWٙvCq:W学AssB33B麜U,[ ܘ+(卸04xv@yσq $#իmϧ>o돞pnݯHYD횛V,}5ouNv::GUt;:݃垆XNgC'Hjvѭdv#n"zn߂ זRڿU=j}af<}'V…'Nz)|(϶PEE[hT6Q]Sk?j{>QG7ɯvOR!9Գn14B Zlfj8@o|9ْ]r[|U7PF3vV9t ; 7wr`ӰqTߵ:qC+Q<,  -ZujRf{V7-.?(bRM u_N= #YMoL|7y|W^=\uyvUmT=Co:qW:+f6A5h -3n̲:{f,ޗZd7 U7݆E]N#*CdV<B[/Kbcr~)of>RuQY5ғ3'Q4GB3fmjdSS_) ^wO5 2fY;VHPnd֙d$W~y]繠m'sFbVs\gs|EUY!6h9wq9Jψ2]6r@dA]Dw/[8k@0g&VNuڵ_tmTWj Q`4WҺ7`gfxvBh!Xujڀ7?xML=457Hꥥpҙ0Ԫ7?&>#6_i\3jK-U5YT(V qgK{#(۲t[A$U=n^/ F"5 RL_rb- 9ZMӇqgF='U'5V8X]MR1(!xA<RvHHɊSsSO⾐>qy m VMlAi - -puBzՒpub4\L%}L=ij@w̍#wre{6zɩJ#f%7U$dΉ>"lM~a'B8\~}/GZ[sMYXq'lګ0mfTfugڅR'%yn,LR5QPj1.Tk~v+f,!BНg͛H Ք-)@@[oW!E3 $'. '. ' &][Mn6dV|nKqOkMݨ͢Ëf̘T#G$2^LrwوXtZTbJCna0ъk)MhqAPKQ WtB{k ޾!煐ϩ4ɥ|CRl@@(@5 ̕#Xڷjsؗcu{Ұg՛]2)[9?8eyLć_)Q»,zP(} }K5N+ V>&P}1\TxfnNunV%'Wpu;z$J -s7О̬^Wb ->/DP4aqLp>f['==fnK+@2A"X6zZd^kJA[kB2yiUp" 'aLx,;]aBliCZn6 -Q0LЛ'Jm2Ol) Xj&$/Œ7`q\k[i$fqޯS)}Dv}o4+YQ͔^J}âedz9Dpy9OWMf`nכ&&/ïv+# pQ ;EŽd3 riknIo YbJmlT=l7(Yy$ȧ`H d źiee,Az=`@oIg%WBo۵!-dmYO-m7 -g6U@, "H{W1@x@w:ǟ,4oOFtv+o$eI ^HNaW1^q,WrDW3CY/ɻT"LNuWUp>ǁr#.+yV6Xy@P(9:A$X@ 1P(E~r#O~6 -*Jf0Qp_ivAzzY YhSܵq{}YJr[qޤ]u\7+OnON;*Q`(;{sKMw|AaZܺC7c |}ۓ~z]髿uϕky9S,t=ܜti8["q#v q$bxa?zM͖@}DvBv7*WPxJBl9|7ko|O֕9O-=٤HvJn[:{O HMiA}+-ֳeSw-mϭK%P12 ^V7JѶײm=UPj:axMoɅZ̳=9WGGVy~ev -) Km+%͋f )Lls1Cы<שԔtj'߭IqQ0Ǖn8Iei:,$GlznbNc)gZN`E6g3+U͑:ٷ|76| |d0Utrh<>MVgGAW3<R(+a||}R2D_I8w7w(݅oNo<5/AJыUmzqUz% -VziNm>rGBhJK,HzWْ]/5v'*#q_+J 0_feyQ0Ջ6nqȸϛ7cU?u8o޾էm D\H꺒,,3q:?Do\E"VN"Ed= 41n/+vKAnyt}y_\g%|dZKC)7.*䧨Gr@͏!]Ids:'{ -sd ֯ 2WƕgSͳ4TJ<(cK}sY cUP9u-Ϸ^QotWj+mun=aˠIYhX6Am &? -V¯67/$gmhJ>{L 'q_zQ'p{j+KǮjPb{u-Gez,'c_W`'dd?0Wb3ZiWJ/4Ҡ;wM˜z55W*sN4 -e$ĬO$42 M|v$f/1(^j~H>c.*6wOhɤ֗6zC+Bi1ذTi 6 꽭Ԡ/RQ0$='$1fr& Hf|[8ؖ=ͱ^F~kUOۊ#CE7VN{NRd*:lw h)%_+niUҤwD5kr61Ww;pfbۢjJ;&emDwWiEbAez2ޣflҟ43&.}f75XLXl(MN9ޭ*x9Z1ӱ,Y H)51*;a;I/|/pFЫv@}1栾Wc|;8HK{:Efo i. n0W")̈yw m\ΡD#KWά9n ) -e" -AQARn,?€ @b]ju}Z {xR kfȆ v(jXp-NkKDiʠg(9[Ԡ=YK8fބw<[La&)$!Nt88A_$%ܒ҃ HXIc󫙵kzx+ZF\ݡ77J}ZZE -)O lk+/i=jؑesH{1)\ -@1`Ch$Xű}P<6, -Ϗ&n N ϻ: /3 79$Ak^[ȩlA0Ƴ: OpRb>G3;7Ojg#,hڂ SG[~3A h2'lhU^14;K-&f[:/@0PKo.Qr6ï UEMuܞh> 7;_cMI1^RdoZ̨O0&Z7\Z؂q:&XI[^G |LaVlM,~g+'ucfX!pl"QohuJ0#I&Ah.X[Y=K&^\ʾqz;n56Nq=f\!)b۾n;^x~aRӨIaӲ&4AN0;$8]?_ ' Mpi~R~ʿ?HO~_)w& goWܳ6 P,PIhxø1ym/Ww sĚ=ҁ8 V] { 0,bb8|51XUĵZTb5vv2h?hqwSd $Aۿ9W&'J@.L`<ѿ9~|>́^a@ތSk ϊO1;UJMe\f+IjnE$g߿'ULo_nF~[ eVmtSj}|CN5r6 =l]N76 Ӣf?MKʍ!}_7GO'9+~}feaΊG`40-jC -5f>UpQ '^loFlcO Ѷ@?IYs-Y;T:1ohuU[p% ,)Y_Y+rO@'$N>^߇rG ˟i6&#tS;Mnhy,h{\什>?*%s6\݀{x;ATŌ7>[cqo(' |$9PsHb7w1'Duȝ70}Ca~kݻTnlǂmfY/uة䈭k rq_ۅJ1?fSԺ2}g8o3OWVCijs~7[)(5~ [( 5af{4snqeON?Z:҈4j*hŤ;ոnY]P|6>ݲJ2[Y8#W}YkDž -_9?KqGiԘfh~Leܨ7}>swϞvRݮ*]󦓝qKqފXA5ߓ0c(^dSyKkIjm 2]<+<ڸT-70j>Huhk.5kjWޫ,ָRKep/A u]V?[K/p~Pqg\N@@P$Ozj{;ֽQr&V)2wj* OUT^lڏE||}Wu=7oqnIoW\58d7Lp?Hۓܶ^+ꌸKyI.FeI1-pny yqCrҍȡx:,z Ve[.}Sl*ξFm&0G3*EDm: [粰ا/fJ#7t;0LLwbʇy 2s.R\QQE uz0n춗sC$SV٪V;nV9 Lu:lX c9sP5~occ<Ά}}}ݗ\h99'֕[gR&۳Fqlu_ͅ9'.DC'H}^t%DVySq#ZrQ<UwlhfNsGQK>¶q6ؖ{s & -+F2h fS{NX>k3*ʭGZi=4_Ivm(lerWH{*Dy;jFF]51`Ke㭤 bw&oDzvô6Vno iaMUtKH/$˝.JtTIξJ05{}aǿuܦ}=S|fe/\A~uٹ`iF͂PF/r^2$Ud%DM\W4Ev˦WHoՔ2^[H]RtIDÁc%hV;6׍;pn"2sMVz>w+ -qcZc^"> Ҁ)L kGD_HEȣBS A{3`6kU+eg z^Ѐ9щ oEQ;>-sւ]7DAlV^! o3?73kԨ5#깳Ud /XgX!_ydM/3ψEۼ:X8+XRguaZ$0!ѷiw~9TT/o:yjmevrK^O[#=Ln,*<%2eUq`gIa`5eZBȨA..3P Gy[1̒ócKLALKDr-9v)޷\Vɕaq̺ed^$5O5"IњcC0k@28f3voFᚡ^4SY'G,&vdX{SeG>\p8uxQ)Es!ao~7Q}aq  -gE\yE~:vڐ+76soAeSQ#w`.HViC nh‹U|Db`a\f{Z h}Gh}){>=8"::}L&f"dk:>mHr6 -/5ȩz#~1L,4Na bD| t /Nl~4#Ys/W428*^ru$49DQeFN¦uac$Werr|GUue L~;+ҦIw4CbC{cª΍xInA䣒8!;^p<:#jExOzC.CO6qYzHԁ( 8)~uSm.`鎕s_ZIm@֭Mm=65\Yz*m'loƂo+d62e =w EJeӢ/bÚh2 Jzi'J;8kY32V w!@}@'AJO M!}/8|mSNL||WN -CdׄݲY%톃N_~n_Fy)CًLi֠J#WsE U!w+, FAL0<]@4Jn ~4ذrJNZ -H~_yNQ&COr_Mt0u153RcaiI^n&?=B/iR#r@a=V*$UAd7YY,-H0 t&y/fɝP3 (eXf/Iq%XS #Y.g!W @,%A`jB-gSV4%Fv`84(%T~LOc`rB?zq<=* -] W:hTղq 2ĺN0̈|4xc#/Oܠ~۴o.Ft MP&[ o:YJVK.kV/$X @̅3.*F ]J2KHWŏ4"=#j0~3i+Tǀn?-=/=7 C %ˀ5:<^&!6 O0iF{q'MƩYU$K?pF ͟6 _Uw]T^@8 Pxd`?qXHn@LI! -[j ;ao3< ( 7h4 -Eh*Ausq/_F8|l_Af>2'Y&[ +gȪf9-@+y8QzEZbդRK$luG4WyJUEN?jk'~Prj5Mupu5bziANz#@ݹ.PFC˱~2Ħ9hFJEԋpzyYʂs)^yg[OhvyTf7堭ɰ_M׺i=CW^ls댥K\|l?\nސT&U oƿ?Uf-_9MACK?ybiɳs`Oo7]ޛ#g_*}(-)Q[ќ_Kh4׶hwD̕^Oƺr=5ΧB|TBJA`_$3 ৩iUt!^Odn@AR}^.Bkuoߓ:3O~?9/#zU. yGTdO]Ewͮy?v; w}j{H$Zm5~WozIlC%ӟSaLt&() zv t}ہzk98 Zm$*)ϓ$ UqaC{;ltoUV_j8^W}817\q:ז3q/:&ukҁ:t8sL3ZL)jp_UxКP5R !%~ڢ>j:455pY)vTJC+oB-_n٤N.6b~5 endstream endobj 389 0 obj <>stream -v:ƈԸn_jGJn^SZ@ꃌƞ!йWm e2W7ATZ)HgS'.fkTc2?J6Cۉ׽V=OoPS%e՘nѓOxs&6Ca8 ̤کUXNM-:$^Bخb={J ?|h>*fٞ/\o8 Ώ+#*r5Y?dnܸxy#i|g:8N8-sP2*,|+o=Vۦeq|gsАH\d%>scܮbJ?:N{JK,ukCmUޞ^QrނG]ܬf{o?ʜ^FĎs㝵-F;ˢsP]u^j%3X71]-]_?tE9_ WX_9~0YoKV_ TOvkރy>jCo oYat;?9VMmA/eY[1m;\_Jخ e+xSZʷE*==XaAJUW.00+~rsf Sl'Xy(>͠BElo}lM\8431yVjzag3ZTsi5NJEʜh?fQȞbwe2X|wgr{~~/#}.{V| φwX$jݏUmͤ9 F /E[R}bLd=MZcVsIu 怲ц"/Hg]kNbS|*&^b|Mvk>!\ܟmoA|29=+H7AVm8.7}=oZO ia ,ea$os'ָN.%}fI`sۤqr`+/beaI*qrD;lZZ'_>?}0\:bn5Jc0Sꛀ -ի|:ׄ¾Ԗlh}USTCB}ph#D-wkD6%{c{˕ Ʋo# i * d%I-4j c^+Osrz^G9ҹB 5nei>-~\̥݊dI |B8%8ze+ -f6cbR Th'I0IMDP Vpqޗ^ o(n ըR~軿.ݺ1^鷹7 TX$oxqjsalu&uEN5+!qB9(L0×7w54Ro);MeN% uZ?lN^Xvd~_-O&|-aF{Nqɻc]4"^?ߐ<#(A᫗kYv85 RoM9XV)$o05Jp~gtCan2W&@Q%4Aqлu0c#Lc~gy=IuItPƿx4yBOTjpAk?DƏ ٤MnVd@ RTXٞeS? 8jܠ'e|pJ*u$YY|P-p[Kś^3\ ؕCFҹ%BN -lN0"Dsi#AB4J8\ѓWpr8pgڸò!8W[l{Y6N͇ : HPIwg@LcMUUn'R 5((u-b/:εr3v=m}*3t0 z εƛ-~3nv+i LBiv_TL 2-g$3+ (D@)n?eBf $P  -KH|t|7qq×ȱ:dWoswo7`2[0C4ˤ$͟blHOx 0tW)&D2Yh|V'j,A%@Z >xC -h9AЉ_(@3%0a5r~k -7M ]_tМ:O ִC$́Vh e&_33Vh=.plfbOfVN9l `z: 7儀Qԯ -FځgnM Xn;Gd4A36RpZ pN|^ 1 -pU}C -ch+r^ 2q9$^ ǥú&`B՝Y#/mfώv dj%)l \ъ7Ī`Kzߑ?:voyc  iB-gQVqRB΋F ٚdЈxW M--?2 ْpa?[u۽av2}`O R-&bSu+!{%5e|O -T{LLUrH.8c -a~x?7#|j*7h -,/B(QK"e$5Yqk-awGbln hmTOOq[&}G&|$g5zbqn9.K[9 |} -*ϲ^h_=5nP9>/}>b? Q]urT}cdխc^K\;h!-= 3( Po7^nW\'D~LםrT}(/HCew1[Cӹ/g_y('Y/ihr9rڤ|! `K^6p]_iޜx̚jX~9"_4w'dZbF=2LnW@ }[{1(kkM5#ctOj̉Vh7O9|#l-VM+w|znuh~AetiuQCߚ郳*R#K>L#mjv]t y\ج"]G^;)ܮ *.^*:>7k2wu9Ǡ\*6ZzeM xܨƻ^eP:T;= -⬛YXD3ш=d:/3uBK=/^[KYBKMA[ް;ěNfK Mzjz˥zz3-!z:\&]L&Lrˤ]HX3{v%ZayIAaߘ@OB7/[NgOu3R0϶Ee?dq_ QYGyYt9g|(ǣ3+z# -Sl0l)aȓhV=R![!JᒂyO԰TJjV6(!dZ4.AZ'أ-SYka.W&ESV%l'-#MZ~o~i3Q(9EtȀK7,sj8w2-8b Zb/c~LҬpAVȤ"]N9S\V| -W4 "1J9JFgj&OȀ+%,8Ǽzp)4YKwrb -lkn3ֶrVæ/| {(}Fc%*Vthitt2$BvgEvyreDZxn* `$!Ufts ڧmX6~lu4b&eSͽm P\~䵩W궛DEqZΛҶ$(2V֊ -ebjH}n%!;Ce<2F#21$ r^[[R-,]yled챹j|$ڡQ8.8p\?UR<1ǒxclj["IQ :b/JNT{]2c+bO ܁'6Y ~cw3ou N镆F?զ4zƿuy~JٕXz+K:}JI~ 9gmK+1+,ݶWN1dUts_4/f㫏S .'#|ʛ^iX k&*M:~;@=0|Aeݧz~p:¿TE  ;ϑ9i=N.UgKԿ7(?{F(?HM҈n -|KYYT`h #'|HH;!xbV{F: G - -IgcI뢡|2\@8;1+{Ik׈RgU -fnZ9b%NHDo@O+@~  xy_O@N|EL󺜽,IY  ~X(`܍;O[A)fvL<[b%D`E2@Y]/ U;]jqGQTo5r5&K PʪײF©jI -޷ -7-6l:wI& .93=U!ɏ]K$bP"Rx=PbhI=!JC@/<= ]%>_Qt-a7(hX{0 4R$Ә\@!ۜ&i-{QYKk@%yȒ``벯 -X 3`=DXPXߚpZ`M!>&},W==OVs(tٵJgInz<4&6!SUݢ۶ A UQقba/iY)J&S ىa+;wp4xڌGrg2aeƥbK7ɞ!5\I`SV -+li eDr99QȂ+|&ߍ!jŰ;`,& @5@f[@*vf[$<|L&ʭAjA|^$nnf=zcmTHQMz0ĻS#Š@E[ 0*;Z}@+@vn`I:r'@^vmv~imGÄ~m?e.[o^lwy!ٍi ._ȉ Q(CG.v^{DaոmkԷ+-' [%knye/NieD^?{dkξGKVlsG)\5[W>X}v9 -t ˼$Y抒ӉQ`Lie1,p_Ooom|GyS wpiC43R^g@gPS(ŪbHccz*#ý+KfI|mhJRN?B>>l7'OtC~Q\?dͱ^7B-vkɾד'qϟ]?MtmsSM\+tJsSzܞw4 7]z'83nOOBYxf/t0ľIWsv A]n^+MnZ[oHQ*Ok]^}u[RAÈVa-.qF1tO^ml -n:;TƏ<#ߛ'z6vrTC -Y} lՂoռZAPyv&ȗJ)_tnʯάضV;>W0 - X~|lVwַ8]#qS:=GZK/,+6r }"KJyWՓ7xHQOx9ԛC0t||5?)śt7m'ra7n^Xi8F+P#XM_MݑI6120^U7<[bMDŽOyߟġʅ>GQvZ]}sL#dӗ9.uRwfTRݪD~N `LZ ð@| '80b>U??5+S\n=/w9O޾ -ZA+g!dkyqyh425StI|3smNiahL"W7uK1uOD xq 2,~`(51|3 M`O~,,ZvՅ]-%7Q ^1zF -ڍLNQN\/DCCgC䕝-DlaQ//##h3)wj1<觌ѦK=zXҞ~[?r,皙sfIq>-l(uwOEDͻY![9鲚.+}wn$Ma &"`@hU䝜3IL{$fpy};qf3(NH߻Jc m:YȘOYiS@5-T,ozV~H0tƄ^DSnsIK%a5{~ޟx'}pp]F02lIfCG'QO ;Sr;7*1.W:k/ZTS9ucJKvM~LZi 9=s;'C׬_dd0ȯѧ~D+w?u BϚJ>7ۣ% N:N6epV9[ #>qmy?䵼{%e SZBbmnUfh9tgf\ipKM%h18)aL䓈7i<^[ٮGs [#jl-̴ -Ew3^nv.Yu#H)Q2RjY{(dFg1>n4:iK>yH8ʼn,x-xm\cf@GNDee44(kѻif=gn/{ϊ= 3ՒUa>C[>dd>c<";g -/ =>t9 -N]ZCOZs.HΰHRm Z35ZE>@G?@ a0?P.L* -_HPZNPӲL%ucRKaLd-;.榠F3kܨG1(8G -IYH2|82#>t)(V^'ڐ(#~// -Ur˔>f$rr"9\2FX.3zz|nd\H4Bpэ\2@+a.p@:/ n4VA2~=!p͘*o`8McX&Ig릶-kU*-Q u!Zik<#xWyח^M2+r&7-Yabp#kGWh -7:t+j/x 9xX0)#*3Xq Yppp\U5kr+,%1UDζz" /DU˺4O$l_^8 -1tm_FG>N2. {sON>vBN{w$r,r LH kٯDlG-oݮc$̫g}'+ )Ce~KW ]hr"^ZcU@v=<_ć^@Kv> d @ .f-a*ch_:zV=jQ|t&eWLڌ8p '#|i ˪M#P80~TC #slu -\ -Pj&(@G,+;*D*YRYӱ9Hk %A!2s+MrzE9l;;AHJ;`O` x!1:HÉae.c? %MZzfSft%:53NIDy`N$. > iB_/.<*(lGq##Be/g[&C&İ(!b{:0"BXxl5 ߛ]XyٹBO5 ~7gŰrQj1'j{n|}(+nx8M,w4SeJ~iv>X|]h!zW 1lS - m n`d [L|2/m^.g S?[ 7=Gm@h'T9n }A*'@)(n95@g-54{^m0^MsH;]\5!Z2Ql"܉mBb0f?]OϱHK_.&ccXN`Ն XNg?J@Zl0k-S!yl':k)˥¾(TnC -*̭ogQobxkzyBNz&'ka9 |]hplny:C@aUt# !7,Nr6WۏIß<ެۓ+HV&,Hmt=HE$32HZ$*Zsrɴe'J|'5NS)On A/Pr|џN ِAn @:ƻ' -Sw2ow[Bb`^]P[iveR HYGOOKOz6tlx7LnQs n ; 4tzxx#p+'] -;s|y-1JG<mtsߝeZm!M Np`\awiN Y[ZI6qMM%Xĵp MnH-Z+~K. 򑷔{Ѧ7oaӝ hɷԿ}jQlrSNb`. fGoR~9HAuh5Uht*|oXAГU97Ҧ=J܅'@Ue⎋:ޥ{(YE:${| aԇ]I{Wo$cMl߿T&ݟ;n'VZE/P\ō:mt2w:Tvx *b,Xz&ۑL.l9魲M}OEoy -o-?꫷WЙC}- =)͸QvLq( -o33nM&#:#>iO,ؚV~2De,Ѽo͎VRx[ A`6s!G!$+D^]aJg&ثL)JrX%_¤* - I>1xg7˛^3xֆd/ӈT\QjW/tHbӚ.Uj*t(q -GQ"I(:4IK -¤[ky ǹp4b8/줳Cs2_)%Fdr7rX*(]nP"l#Jol0Ƨ32ơ"G^kKOFl/DVU(;X uϻ% 0peҎZnʈtE7J+.GJtȐKDKΆ2WᆊOfr m'}vc4k#78osUo ӣ*$>H>WJM,anjX\F> ҝ읎i1Bak'(ƕ&VA@WuEFK2e]=F9Gpm)3V(cNX4RX5 d1J/XQj8 \Ib%*/|ثua("խ|!hQHe5Z u BNƇk|4%R)Ru·pF b؋$zTN-u2[Fqw {5ųQNJF2.#/aE!CKkuX[iRu'ѵi4q f%SJ:T:bz ʪ4vR#IG߿9:,y ֫iTbbC݋7g ,C6! id$nz97O Sh,@TZf+ Ӽ+po]> mI=k:i@=5PBcJZq No7Jj:ɜ2RKiKYה'LC ;ə9d"B&tĽC EsN@g3_{P0b^95ƴwLrߵH|+{ -jZ神>* ц֧/ʀ/N T@r}IGD?=4:&5N1 |S[^l,h>U2ě^;b85EY68fW!"ch_ [;AJ?~Ԙǰ1@W2pI&jo>9ى T [%_Y#tnltJ~ Y;Nĥ"$:{*5 +xxI*[Xv rOg1WQ # mFa.݌7Oa,< T|l(OﺵR)\$W7ӶBfkWJBqC?գJ؄{ Jt1[IOzLƏ -m zn`k whSpcXڲvm8@h~hsyTr~zZ.b[Md;CX73,+f[[ءrkBZp@upx:^l:<b8_V_1"G1lbcq0̊qsN)p]>"t>۹x4dy=${ -y1l!mUCNvyd6I:|e>,>^*(G9UaT4b,nqc%<..Qmy^bKr|Y3Rd%zڨb-ZIOѻf#"/'7}`Q@x -~{N("~K(ưJK1Lz --[/#87V/vNeӝMuq薃 "^]a jRٸ^P= ́EO?do׉M -$@N1='NcE -8 kjxm29R e@r,>[L"ԎNƶEQ'8(1gsې׶Kx7|Pavo>OwE$n.#q¤>´BQAv.yN!^[aN%ޔDӤqO|n#m?~4|gφw{{AOHdz7)d-] -$^կt+ c*} X/_GoOgÛճ_<˖}^@yg@%P)AkcLU;PU&u籉jõS3#N61i-ʜ][: 0ٔfh kjZ=٫\zH╶[K)?'L^dzdǵa \KlEZvuoz5ϝXsC6G6չ xa+>] -i -³ AW rZ Mv}v)-/65=U>^3k^w~h=/O -UUUO$k_;OHK -5+;>^wcnN$a߹kҝbdL 2×|UDDǓ,Jղ763 ũ܊1[OPm}!9u zA|ЛKT;:wZeg~+In~ę 7CAjjvzsn+rm \ә]^npEN:fLy;\[7N9.ZD2Cn[bS^Z`G - -]ΫvñA8Lq>fevg9 k7#R`=㪕f&P*+=w @|3f׏ON}S"i4*4_}u>:?Bς̚?^}&D)nC#i^4,Zں9xg1ɦG 2lR=wްo tH2̴{/r_bBp码om _k}m}= |9IJ*qMI\t(~>:GR0S&I. -\yT72OTq]u[L܉T-lKs:䣿P?bh>TScpD7f/; 34KuR0nk-2 t4u#Hv\vZR9`]4|fU9@:t#+C1xT|u}n Xe֟D_RӳRtfJ/>,QWh8ZQzrȝځQR )8_p1-ݪUguUjTcbj -uTEdOȐ})'ī_"O7 a1 Zåkr%ƭ}E(un͐g$| -NohDKuRnX_>~f{AU|qp1`l.JZH qޝXG2W-2TZ -ϲ1ѕ`T -[BƯ#?\UQRqiC+w(W/j%MOO2ƻ"s(FJpS toY3?rBAP2Y5~ Kc: -&N#2yrE,~'I#𬜃1è]Nƃ)'Ł1t]GHqbZ)IX]Ik`V 趰He{Pe#Rw.8>z.G^R"@WʹzMpAorfTG$`rCCUʹI!s!zV%ǪIIP,rRڛcEՓُˍˇ% Cnhx=J#(b89gz} ky >e<\ DSRCrn֝rP(5\Wa[$u adBdQSNvA}/rUXĞI=3͈ S [4.s}9i *;ZlJd/S|F9Z."űm -596LrcRkm ؚ$6f$\\)x]/Re@˟jsg]K֑Oأf ~Es\Ryza{w. zw d+Er<ް`@؁G,#E/'f\4\7C0D,jP\4ĺ}ӁMǎ/W5GvÆ{wQSOʋ&?6Wl?W؇72׭f a_1bBjD鶌'UO6{l/5r~4@ y&8j;xƐ SYJYVvA?of -S}JUX/rJl *zX`oagxdcfCFݹ -0d/WrH69gUO1y> :i5bc2iWp!.+*CIO>4QMLLdjk3k7%]>~O^i3!ω$C̙mc:y|jsҕ3N+t?BAٌΓpq|uGgc.ڎiFm6sRk\QVq':XP ZSŷ~ 6'Rz+cH!Uv2T)!0̴?W^jߵdom8Lk98$'&*EZ lfZzAe(ޤ3ch.3_y :Qy9H$pG/)}HmvUr>.Q ]Kz=)=Vu楔uS!c`FqEE% -ERv6RMxaW4Ϡi83&zIm4IȜU fc'zaJD胰ۘj`kچ{ -:l-jnGdx2XjY+aQ -@=WLns_(h75$VvqZּLXO9٨zh2"uYJ&xijWaW=R}Y&H=Cy`"&1pOIgI Z1 =Y۩x)I掳;v:D$9ԉ̾mOYsL2^OzSv>-(TV - sJ5EA W7w9SV&OӿPByqsFGpjZNDY(WO;l|1@7#ZfdT}pA!HpUK<]'"Qn]WTiP[|Գ0fܔUx+c*^0ڹƝv<yZ?C"G Le2Nw09ZLƘ5J;}Y)6|S_(eU7i{?]/M`ob*L#.4N@/sS'  7#Nurp2]5 -X,c 0Àm%gkJPJクi cLfGНd.rt+zns85#S=n_buC}MuS_(@1ƩI];W.(5f{OL -"jMܙ\H3Q_EȯRa I8E#I6}jf r蠓7f4 2i|zwlNQDIEB]殗~t_͟@(Tb0ǯU]ǠUhW`V!嫾_Ll.ΥQ)ԫ#w؝ynj[i\;MK$. uk5U9W6fm3!Og{kDWmP"5Žw|'UE+^kIHR%zRp(ŧ?" /PWGQf. uNg;+Ju?ʇe9Lad5;njõtѪ͇[p1^c.95J] ,^_1S.AGd%G+{ޟ{tϧ\v_LM c?'?&B /gV6]%zk8uބZϣe= (&.}<:*v%j<܋;(PY(+،s|~TF0jKOXPPr^ 4jޙ?AD2G|7>oG鰛-ӏ4~@eq?qugJ6&>n VrFnxYJq?IOz9:3Ǘn;6 |+&: -g=pʽz*Xou!|8]ɄVZv9A?P}=.7cj(þpi=Er>Oe 8L9T~\Nn1"]s2nW4?kuSψۙ噹H-fjvUM}ܠ<'5LDJs|mEձ01PǖoR7~޼T$#?H(ߒch 'gяcGY[>DiS食>SדYld x<\oK}b_-y:,ﲗDʐx#_e6ktÑX ->kX\93k8g 6zjڈw3Z -0] 9v^efqd+ɗ:wQL(|9)]iF4Bu|z ٝ8R3s71Q^z7%ʨ]8'DߢoDH1ic7(Lg \dwnrb^KƵ IڛPcKN(tþIeOgXUpN1v(rnCq"!8Ayzbb )'8\xS~ւ(ƯI,gZǪU@p+iRkc geMh&&BÀ6*0566_$.߸^:&{ Juѷ,0FܒxV\L -=0AOQcg\/ -}!@x.oP8u4M0.o> -ꂍː@џ|5G#WT>P\(>e@dF@xb#fuuvŒ!KCF߮=@d$?alF*Xd -sEsAG6gظC*S cM"M -iPuoXjQsU|ƒ$R}rE3Ec8veȐ\+t[ED?<0U?]ma4ax"[WAMJu&y/Q4*ܔeNW*r+Sߜ&2g-4h@s6D~1-Bn5\s(}B4RƋj0:0$}qܱe/`6f: V#nDGTHt+)eϞb(-]nt%жq:hkƝPqM h+ƉjJ/'"LJRN4oouPk!MA]V6n]wYȐSx.TQE PT0x"Μ)q ohD6jH$@ -ASweTt!_܁NJ9UC|3R{n-`ho>HڀAFx"\DuXX)d~7dJᰨ۽f!Zw1beh}t~C(eW/5iAX>ȲNkqaS%k 8S[?mͲ^]Ky~Ӥ3ǁ6Ƴ݀1.HA^3B; _(̽@n[NU0~Ul7QO,;{m@}ɭQ-:R."eB.c 9O6Å42`x,-'7#N4{ 8ӁjJ)]2GצtZ"C$!~DKÿўc-tSd3O̝%SA ~M,̽lD8L])t&CuUǾ"oӮ9-]$:Sv? á"%ȄT3h:8H[iTW (f L?sNg yM 'g.)6h $nffKM¾螮yS#k er8N>3LO*>=?u vd(ƘrUП߷A24* 6xVD-T؏G#mZEd,<K&1"&OQ 6 岰9C%PACk;SRcO }ЕJk_ee@?pcV;:縩21?+r>jNYr^ktv?kb\P=ё.7 VZlF7ŠK/eؾV), JY=bq`Ii.|N`s-tĒ*VRYVjOSbxv̤ o(#ZMf^Nyl:8@+JD ١UxQEh8u{"0PPXPP#4^ZId~;Ѥ KqU IRdiPsn.4@{"ނvҚJ*SdO _ 0qm!dՀHG<^vPsj.l'Kuѵ+@u鄇 .ʋ83 -i-]1ڶ=JS=:p -O%𰆈TvSP\4:jc ,!pWle7]#C7ۨTlqt!P;aB fB_T{Fv`Ü@Np%+fؿNj:8*?A}&<ǎ&S -h6P>KO)F2ҹZDĝ:ѷ11K}sM sEK -xʔ{Ӷ$p;az;7;Olѻʹ7Èh$JSv5 Wϵ}u"@>M+GH2mPq&gY?6dP!t lGiԾLzvbk",/z\/_XrJ}:P;]e=a5;G*kG!QےsY {ytSRU럷%h o8/_]XAߤ?9*"(9vJdq;ҍh{Exsnz'qkJѵRK:-XS#|F{^h 5[C͎iVcf665h~6F=클 F m%k)IcHpY`&e wR{c-hK6SCK.}TrVtƶ{7mF/{y?ꜿa(lc 0&k/wwm9Ю 9.l"<13Xb~Q{ii8fݵee:\F餶ۋmݝN!Ts`--c0=#ATXS[LӏaqZL?&>}r;?~b8C ۏz}>"CG-rBY>G%dvcVcN Mv7.K v}u q.ӏ 6~c߯!yl/[7$-@n7_?v?[\.a%N+D'z|X"zԜ_VPvui_tzwL`%`s:oG8؍`R_~:; WOdxOr+ =kJ#IW~8Z)5k.#cL,`CbFC@cJ!vxqmq@AfV|)ϧ"aqih뎻0lޱ m޸e| -a /C`tBT̀dI1?Ê.[=D~Hr  j*,ϭ$9lǀB˯'xŒp$ـ02k̂\E1v)?"F<7%9I -?!W.Ⱥv[(vrDSxwpq06{e5i -`?ohҢ{FJ<'6nčfTqb1mvAA24ɔf.:ks*ܗ;@{&B ]>@]yRx o TiHYo'φ1?d66=u{/B1FAzAd춧 -}em78BZ#| -*b.]Ӵ8oFr-au{e{"M·kL}Ƞn[EsjM % DGi?lKx!QB Aocd0M,|MtEޑ@ʍ@ʽ+ KLQ&O"gb`3cOX?i ? -#utNs-w=ZǔWeŶ>LLlsCal8bw#j;X,Pel/ʄoukoKx~=P6QAM\podCZ;-k]KHVӿ6 Ԝ7 c<@C)v;md$~hov M;A҈6O̶]F"tahKfۓizda -I<,?rV8jݛ.f\?EN#uؠn?[W'56ӞrN !x?`pNp/uPy;K>d29^캩jhNG0='ߚjX~zGs|4!h2gqoEX䷝·UfVËGq1L{Fn;:Ct9ũ\]1\HAEIȀz"pjjaM_ 0GTƐ_ֽA W218_%u}QKlz搿v0@cP^y,ɟKy ȗK X1S;cQ0r2!4mI& -p1z-ƟO'Zҏl!.$ȧJ{c>L>87_ڮo'J+! ^Zs!ѬHR}pj?;a3TVB+.5+a5CӖG_CAuΎsAYc(H1pF3yAw4%w{#qN]K"Eqm sqZ]+-UbDz0[tbP/ - -^FylГVpvq:(Gw{Ԛ >wlaWI.ݺSʖ-\h-JC7 lyYLIU7!]XX=g4-_̝P1X(oKx:}eq@Ա(u"*KN -WEJ$k (X:X\YF"|@DJީてaufdjဂu:)%6i=A@IUm'宩(}'QrHOuᡧR6p@عU;m,1ҵMw__9] -ipXZM請*|3u J{H%,eɍGiq@n*)Tޞ_&\UԵg=S zF D:~W -9* 83M~]hȎJ7) -%,.$z#>=.V@sNdX@a [-\"=ge'jTͧ\%v Z~|@3D($ؼGH?CXMcC |@D?G:i@#YʻVgY@!w"v<Т^:4@eΌvd\R, P=iְ^P/[&-t!Ծ] -v{M`mIݝ˩5,T=\I=,Q$}iݓ7e"'!PW*@gb7Q0$А%gdղ5R%J|8uy|x:uq -yxnO9#DZeW}z8M-F{yf416%S1"?4S.z{޼7} -6`˜0fuQ3h `RxzU]T9.OƤBpuy>b>oO, UQVRp|*l)3Dolj͸|Cjm[6Աd;9Yo `yi2@* wYՁԽCiPnS$ZӦc{Vp H^3?(^ /Pdu%Jhto`@߼@jv`3M@R-ҀNT* (,kcu,k@eɡ1"(?sN޹"(P N1Z%>?s ~P`>5\)uI0W-FvDM^@'|R#2AOCDvx 1.v5ׁT)ʍRZ}tw$š _/?UJuL_TtzD֡T>{ͤFįO 4 ~=4y1+3f ֛9PR7(ƐG!-?(P#+Ґ$rH>ͪf/p6/??qo2g &ݦ*! 16A?1[7DΒS8KR$sXxa=Cb %?,I|uLѓ _j f ~ɤ"2.КfV^0\q=@\KXh/\I0S3pSp坩HҪ”mcBJTc=7&D'9:_ِA*Q."k?Zn4`Q E62uҩ,dy|+3 e)=-$w,wnd :aE`}U J~sh#ɑuAT7sF6i;4/vMaL`IHM${ZCz2V擔EIȫ12v$1X_ wmK:$=r,}ۥŰu4:޽X -;Z?aOk(\?m dޙYx\:#މ10f}'laږ)B3p y aD"F;F[)|rV\9O3-WR5!ϯt~,:( ftD@5PDT\Uz+wҶP]//j8 2N8N|Op'[: : -ߒC'A|.N8EBp$DsX^^ǟ6XCbbwuRL8H=OA:'齏'6Kw‰SU8Ό'3qY蹡E`4[+t/9#}Jq cFϕ <4ǧ6cɕm ΰd,ъɮ6JŞI"RLM|(/Dd=O/Lv{U6u~?p6Cpa0CtP"{6(d8 Iln+$rݴ ,13!潿u̪Ŗl0jc^]hEy|;/t5"RӘ C-kxL` -漋IJ8'-Lpɰ3[Sކ1G1&p8XI/edD[G<_l|Ѝy-s.v -awQjYm*O{iacr BM0M}Q+͛v'uG'G=)AgW[D{8A=摹GF`H;U^( >䑹8di95T|"s^ɩMe LC:O)G -^Ƞe{ް-k^OC CE-KE.p9 \t0P^'h]ͫ{4vk%AĈJ!qE"5sĝ͇s_ߛȳ$vFܻgp0pDχH}_>ޑ .ǔ|wUCdsp<1o·ZwCȇY/X'ƹ|b"Y%L>{WLdfFX](!Xk-P?Xc_`?"ELȺMˆ7&ı#1 7Nw& wa6u6DHL.ϫW]^aĩdg|Ǯ|7Q՘+aW*ͺs7#-ٝKTgZdlT.Hʦ"nՅ2RoHm)#&h'"`k #N6 1@\&6p\<ػRី TkJlgJC~pXp8SȽO.V?v#<ԉ X K p:{S 1|3D%Ν@Ӳۂc"Ġ޸֌Ò3d_q.5n}KH[Ojzy ~kK KcSVjWb(˩b ?ȐwI9Z:222׃;TBLF ;:/u/٭#aw%2fU^*K?:afرUPV|1 EvXWΔag׶y3숤U6_ޙ`4e4n@ׂIh^ ˱UkUz.7j!de)%ZŻ ¤KhW&Up!Ԙ,+~~WǪN|/?sşT\N%\SGY/\\|p< 50q}uVD7xNݭ.>k5 ~Hf9HJ)_|w`"il:e? 'g\,e^Zoɦù17OM a(M˥l:^W0]Y^!M˥Q|&wNCzߙM{;hȺQkt\:nV5wgVf+K9Mx9K$[ptw47e=}:fӽ cwuG0\6ޜM˥c%:'%oϦIq|4?gt/l;ξ)BW mt"NߘMr;xR8$C+tѓqdbnⵒ@[7)N]?X控ZݓI7~>>2wɷBw/)e^(*s'jk,֗c o `-TwH~eso#S -*}~\ub2`dx -"C:?L3|ֽ|a_2ZiZY`z =^ڼ1 -thFd4pǰ/6Gɽ1͡սݑ"1<9VDd""`W&QRdbjot&2D""&R.&*2QYEɡ ׫P^=qTuOs)`{3{Q7DWc]}әQw^pKD\t_\D]Ǯq/r*`&^l~Ɇ*܉˯|;M<\Ym -wϳ![~Z>ѭR?^+aDV_[aSEX}M0ڦU-+S2oHY\<~^O X~;,M֐*"Ә.$Jlf[h]LsjD^."`ϡ{ Mdbd]#Z%1QPE4+ImFv+CRr\۸ כehkPŸʎxj*+If&Bi2\'ܣ "[=^2RߑzSaxm!H$|RCRj'Vrr}an\ح胻xnS0[/s2wjnOu4#EeEęiBzF^D2,JdK MI@SCbo4|8w9\,GߺΗXMݞ/ ?wN0v1v_7͗p+Wƈe#^\$yV T-wk@@i̹fTUiDѷK -y"DytS1=ʊvsOHJ樝o4S|*V0K[-6 *fCWaub堋u -syוk#$%$+d{Z-J el{nYD~~r5# -^]NӎyܦLR[X~~mn*'q{!Wyߣx d'BYMM6CILTʹCEEr$ vwJϬlRՁ__ؽ\0vIR=ʈ0_?k"~y: Pl[5]QZ,f^dV`4 PlDJ@KYbv䧳mDzE7,@j4Y(Ǯ(dm=]aY+ʇ½(߽:)ʇnhۋzZ3|o_y(qA h r]?~ozPꕺ~n(޺~^!l]?~O '.˯|pU?1,*dQ'hE*MH|U__ uk.:uP}.~,ܣuQ{ ot;GkbJ{C]Kl]i왺~tw3 r߫l/QO8zbb]?07-4ԸsWquQy;I|pcb' f =PO+ǞHuܪ~{P=ؐ dooa^D]nyA}0/oaL֫X4-PO{[5o֭ku.p ?QO8z]?''\ՏvR]?a.ߒ1q/<$>[;}yu!ou0 K%otw 'IJrncAug׾ޞW][Տ/Ѻ~BA -}^AIƽF|Ф%˵K`\_B]{oL\Lct@f=Tٮ1Ĥ`Pb ,[澥FhFR[uO5OcC ֻmTu٥j/vղ70[F$=0vDpyWuDAw4UAT(QMz y/gD34*GtO@WQO +?1u;Up}tfg*s+jiUZ]zZ7:b{0'UV{S=zݲZvlג%u:D@R~',> ތòkV -R1lO..~m/3J@揨7z.CB4e}c;xd$R]pt7'(jU9ďporNrɍ}YE5a/8L3"g -͖Tc`m~`I͒hV ڂ7,遃I{%:}z -jE61k6{AyNjҙ @7ɾ+xJ`pFr -:O3%iLud/ر Ogd/F+ Gm 5rh]LX p9mPa_^z^uN3\-7 MI&̩E橘`.2&Cdi9 DޒUl` eX_6\S&d+\^$&fi2;e FI~9ٞ-jv_V!ƪ }*ѵiƳ|' -7a]W G /֥&9]+vM76&gf&ӷKUvA6.8~͟ xPR<Ee"QƺY9h8).!>3/b.e.MP - 54yuH¸a >8\: -X K-#lDI0c$C\>'Ys ͬf>|`1fBVFR"cG],4]aƫk6i1*2 9jm,qHư%Hu4ajnrӄVVI"$,]VĢ~{_'_hQy{>ϐ)'ħuGd^'Ķ(.:Ktʙ 5fnA/XLTKg,b,TRuҁ5? i\5nvU2ݣ|hUeHcx'gd B?ᲩtƘy)k1жCob}ђRB7VK> ݼb[Խ9sZEu,`Dڟ8as랣y1sAe倊h&i%թ1U9ɦ?DL`a $(XR{ FvoRr`0j$5`C@&?E]bI&aEYbwDy !_AI߈E4(|(2M9UEXHPL/kQozbbIaKXP{CBGw 03v%S$,RZ Š PL_oɡ1hk6/vvIC"{CKu$7m(ČPǐC! -V!fH+`sQQ$L{T n%b+V@?DE<111gڪ8!6!QKbܚ`GOlwoBm `޴GB# `C0)DyƇU ZIJ"i&$mX8.Ql(ٍ\O0dJ!:gTHѺD4=wؙ\/mVmz|M =jFYDZhmIwe-*v\PA@ť]m[;|EdUYPls{sBT/"{o ir7Vim:u71(b t*nU^WYq':XlAu%lʣg8MߗQa`a(S]2-Mv2MX: 1*nK{S|F5'FN`|1F7X|7P1x+ӈ*n<8 =< ;)/5OD*9]Y?W/|Ezlٌ-@-M'{Tgr.ަK/YĺlW>k -=8>[u^Yĭq5? -!j,^@;?.%1] ƷH9Ul)7J[i5ךw -WK~f_ds\/Ǎ_uJbIs 䏳9'c3>.kʳfl-DC6NYN:_$T?||):u?f*V&]'DlVJ-&x[) - rV?nٍle -CX8Ewv%g}Y+hu7U.F+7^nM8G;FT qoV>ĢWj˥ t4*߈Ta90JE7Ur~eT+sj+wwRN p =zzqo9ߊf -.`#%'fK-X~ױ޲&>zT%ףdgh5f*mZNtcJU,[l_^=-i&[o?%k\4#2ILcIpΟ.7}hekYp+߃Ʋ2Mhf -,$Xc ǬT>,-4X, ʬx> nvn49X/na er}Hד,VG;[%p6ʰ^%ˤ~_Ǧ0LI:E8wh Zfw`D6n v̽s[Q1ܷd֎·d&Os\F,y^R+yZw۹TXSJM -6(!҆?}g8;\x/?FNο!{r'v$4 -tUyr)m'X<9&[2{/oK`Vn\AT+_V-OTKfŇ|i)<#&8fk[%4}biBufAef ;=%0+LeF8cS$ػiegϽRNݥa:^k( BǏG_pΓeLqJӴuf`ݷE8iic AQ+ Z $'%I,@?!O3[+'gŇBUR\,G_5Fc3--6xpi?βDߘB>\x3Msez(^YyO -JM5qїNC`qdDS1|﵎>c3wnW/oNsIEEzbv%[u];^՛]nN~bd)^zd0UG̘ɃP4.=^{'lMIlze<s!e$Q<93> noQDUl77r~A|5qL<4 -1XO>|5d%@Y/,P8JB䇘,">V@I5Ǝ l1z9]b, Mux"cvH~J9N-Ы!c4h&G;:W0UlrːZa*xVD!NN#yФ]h_d6'UD"_V{U0UX|9[s*ұWy筥Wd - -g7aN|:j.iq%U[^#^nzIkFf/q̳k}#ՆTwyG 1<x-m}Jʇok\<%/|K6N`ֶ53;pwO -{Gff>Pf zsFGUvk2,ͼep-|;1feԜ4Q䖟oWg쭕g/,zΌG5ϳxRƫ>}"#s\BXf#`a"j^-n۲.)2Z[s0 ?O^wZ&ϢyATEp=[s6.eFj|<"c>G##?{V1x~|_c5%,5xVg1Bc0'w)ClbP*-ca!lr1\>V#:}|DAxXWgpfn˚_$bů#j -kAyo^:wuY.SHu?u~$SmRJ[Ǡ/11ߖɺKcWZyar 2u nDQFOfWvuCOle0aHZX~S5,L,%;!ejt`y4C$yp^P< ZIܬj,e#@2adµ̀'fd|O淏F y1CQrnG)OR`e^u˞A -|5Gm" <1Ҭݣ۾H=Ηu<wSc -z f󱲊gs1W?vssŘ{D*sHɹ5 &'(6Vϛ 5 KFY>cZТӱuoZtcdTPRxFo2XMp?&ÊYzI5T^n Iewh>ys*l\_|7ƧѿY1>׺{nP"sHK)9ZmJ*:x^|”X4ˣpf Rfe$RcY]eULHiInR۱֡4wꐌ"1(SH1Y3=Ny)8c VzڤZw@Dx(@dd@dFDo -3?8c'?$xYm %qҚ1pt^{;ufW0]C~GtƉv]:![ : 0$C>x`Ee׎nF Ւpdej1\$yUMbZdwrZWn#f-NnQ&q8j>`2>Z-3ZGX+1嘆X+PX`i,L? ,ae˸/(~w Pl0V^CD-W2=a}w=p$RAUup@+8<Jżz,3]?p, 7(}_1vɈ z#/1੼xZ)~39_8mWo)(5UY˵C:ꊠ4ÏƥLGc -1Pw[d3;>z;G1{Wnv|p/drx Ƈ&=^".Ff:AfM3g?fM oFf 70;B}}b4GhFDߺb4Gh8~&c -㵒|9ZI_F+i외3fAg0[l왿:K7l:@ɦZ\ChWL -M,IY*J@պdy gME)xCj5Vf>SS){.d^ŇzjFG^ΟTe3o1֛ i9n6C1q/2RTab<5V,ê8m/װT^s]y2^91H53c4p4S#9GPdOG ~,8yq4S%f& ~,HT5ȬyKfyo?2s*S{oB7bsyj'{ɮ;pTY=[x^YfcLW:׷sLr_˰BxR]B3I~>SCryWxAoI3۱îOŠM-Fc>/2qaux^X~[7 z# 5 %%+2a5O\;'f5K$Z-")%WErSx}0nyjYkzPݠ-E -hr溬ky}4go -PfU_{zlJk!+U߻MI;:9 xV &nU2 -qC҇'w`=$=8d8=$/[(&Xau(&豐V=p9Ъ_"\~ 2N.Of}ā,| ԋ߁潎@ZŕFuk yRXTt A4x aA{f<swV ªA3_X5,cÐm{{(mu<TܪОp -6 -x IJ`\SgexCH=off4jG -,+N<|ukjprXGqj8ums~\9S"^Zw@dk3B=uf>|:|7Ľ<}Tg[Q5P+N=xg(rT+GhU ?{FzxF -EawKv#WGj8gWϮ$5Y&טnOebRqrx((/oXG&Rc)q@ȹO6h6(>۽z!Wŭ^ee X*npאx )[ƿtKHٍ9!𢀰g$pOe,8GS94#q#V4Ny '9gpü{p.s(938Gp eS\fmo;'wwѳ>Hn;g\95[d6o^~U6R4j9w,Sx5p$aQT&D[sl:wq:J8L }yn'>!^7_?\Ug,xX ksxSi)J6^O$HeKY -UҒ"t4}"x3/YSS;720pҖ%q7q,մ>>kߗwrZ3^^P)uT(Ёx:YN%4}u(hL,ӝR=|ew 鏺J90ךeZ6&6ûdeGA{T?-f*v.L`T[23-MHanB }skzl5[mLO5R3(nif$ uW[Ciw\j (:/7?Y{-A5#5B=sv\6Ny}O?Y`vNY0 L7gwE[mj67,B,}wSފ1ZJCp,T>z]c;mfq;7Xv|~ݰ/̺WZym\]_%0X/eē^Ukd! 5 s3g?~#A - :-}ej%;.~1Q7%D0)o|#3k%3Las%3wϴNuZGϮ5VCABТFmeK^vMܛmOՃه=[Ŋ.'˞I{oVU.珢^cbh})Olb} lvzM 99^<ؽ9<A㬥?V챜^9H̅s%`ɫc`E8Js]sޣ2uNn^dNoծN~L}[I% ..^Y6 b! Tv63Y{A#XÝ~ꛜht*G5TҔ4~ғ ɧmyRh - _bcjvcahb2/9KX ikz>XZgmLQrzR6YN:1g~M_b겵.v jD[;>m6(Ԑx-ϳnHSWp߾|=?JSՃƆT7/<;oW(X]u|YTccfUOqoX<,'3;p! I5p&]&+w4V77NӑD*+}L]Ld {v>jJ!K~YMU\v弾TnW+ŏ˿"],6띣㟫 5y5-VfVʺ7e/]6 #OᄓN|+¼I5;80eڔeg[kE.zu_9*U)z(/^+Q_{Ch'xm{B`D6$G`|b -1J]`K%(F+cQ#"15+,ͫHE@ԧHW#\ B+GMρcccQKs/T^v2[h2VZ{Vܿo4f`Ƴ%|x<حM_ip˱, ͮ.ލ7{R0=mwMѻhftk+?ë4apՂP//"75UyеIL0潎& 7=e%j96Vpk(*[2vDy_nj~z}*xC7VfݾG-l @`ti'+f-b{Z)5s*1?{'sevz A[,ԿLcnzf3q}q}7 ^{Ec .^~">@|Vt>ujܰfgeS+_WK) Or /XHΜ3ž,&+͘_,tw<s]2[l|l ΅߇0\ e˽֛L|a7TYPDI#P_8i71(=gҵ=%VߚLžy&T+)ߜ)8NƝlMZq'A:ݝ`ol -%]'ApwqnNY}5*a -); IlΙ.~O,z7{^q؍}Ow%q^D=0u caٌ}Wck{ړ 8qr5cadg=(W^|xIw*'bcOgx^7}4;xz?zE{y=v2 \^ciat*=5j\FD*+߆˳͝q{-0'_춭a<a^|Mׇә~t_?u}ulH@/lL/ϖs˸3qϞ U]8WO/_of}ءq >Ylfeet'oMk*v&Z:9ӴDjŷb@^_6n=8͜`>}R4mv2m,Fxg)]0{k~ҧmo+6Z,//X։?.W -WtlQF>JI,:0@ʇIuo}%|{uv)imcbDŽQS#s9cm ^׻9BNׇEk_Ogĉ]_ecj]$[Z}~=.Z3m.<kz.M<[vY>Y"sWv:02myny!bj[閺t;yTfhGveIsY)T=|4Wm|0o,bX]S=L9f.Uw)rνGRʑo+q+Gm0 -foC#44p68hgQ ? 䛀$/>_91]e,D0#,NXњ ZT,sq?W|F9a1y| d&R)Z S"jUEO׉4"sBZ]O.Շχ_S%xyQ*M\ Six UCK| =/`~݄(!L=‡ $(SoSDڞ:;n ِU Q7TTA A#"j"M=%( -HETє0I'H*D!!@E(d"ǒ(Cas(nL$DQUe 1:|h)]dC!*ʚ(Oy^,2;L{`vܡC/(j(¡p:(A &LnH 70;ĠH{$#tTo|;TqSZRzwZOm޷_S*U^ޟ—Jҫ/ψ^xz/Eqix~|hu}n[ϟ5EuE$ix.4ynu"Fߺ}p{R2XRY1&~?1IU&%+.\̏_֌ nb~ - (Fh?1:'Sŵ\t^nk/sjdvUE&=k-Bq[Oo}Wt(A }*"`HD?: -H - DtjDU -u*h -GYa"D*M,䧤jB%]S})0/0Tb]j=I+!}2bA}wKME{MFǡ  ӠGRčzp!񏗉Qh:<zqqAВtEޔI?l"hˬőa"Xn -+I]?cDISDO2LdO66`_SE|n9Ca Ѳ-K6N3S?m'K,,XM~X7B`Q*,#TA¹D ]B, TKXUtl7J@QDLKH(Pҩ(I$]Pɒ TרDeuPJ+D X -=DrDCIlRxAU]A*ځq"8D&4!PwCc$&2|l2\( %J0re@B\CAw5BPYШ,,Öb#p܅*S(- U Bܿxj8nđBETCx&Rl#٠DVuQ%1E)j`j -pl&+J)Ð$X$"D5 5eNK>rͿ"'DK8_`\;*Jp,\CDۘ'z.7J?0ípqߛ@xAwAt o(Hjl9@ -# %i5R@oUA:<"II$s6{l%DQj(T%p\,ps[.p;>աZ"EWG8-`k:HѫPBˠqFI#h=b)ps&|tG\"$Phc&My>JEMd4l1<Ȑ%" AkUT*!~"؋!i{r sd#C4q4D\>,A0^>e FԉGDDY^BMS/"Pac U)1Cacoy@A$=gI{iMd?Rbm4飰8\4c!)4.:hEx.( ,z,.DM/,C@)e+ϲ&/vUpl\n +ǦXйv` X}o+2~0?j 5~` c Ġ>?0 -5 0PQ" - T(+Fo`,^@*!00D8P9$󀁁ICrhIc 0_ D8N@, 4 #Ke0P* -%=(ʆ"ʥ`c{b#u -8" -77qݶ28qkݶnG0x,2\, IkGX>xV0K (`( - -y004:}X|00P@@/Y$00Dc&#AhMd?RbOqq[,M9p XF.sQnpn@/Q$00Dr@Gz vcP@:ntbtx7:~?AIǏY@.25ݐt0)QP ]RHtY(ր` u#)T(P x"=%07\UWDhR* -UbWsMtSݎC.5&9! -q9W0h]LtŊ,«&u47B ~n8@)0Ͽ:Y;f"W "ŭ`(؈@3W-C%^iAT'  BK -wR9. AE"TPe#5%pUAQ &T/L*3ᒂA`3!Y,&",뺆MfS$a&kҸd:ГD0@:2TnI QaQ:<`cH&R_jtSi.Ѥտq;@+ؠ#X/]R~F帥2*#Fvq;qZ -h|>bbM脏2> (`IFL QŨt2,%˲&A"` y^ L8rHb؈&iLa(DDyV HF3ȩȪvNϡ7!Ũ2d |>\30⏖:s M'wPNL" -CUͺ67:XL$ -Ah;`y.w}*-Iש+]hIP8?_s pȮfO4̈́N4p?*"GwyltYQF"D4@W#'{MU$#BsE1#(DODu#%@ -TĴz*袨J"R( DS3SCI0A/.{ 78~X$yK -)RAC~HD'xMֳ8DUA"tVx -P$ZW4GڭQ]U Y"\"jmـ2$ASГ{" N"ꭋ#B쀧t9L:'LD֟0Qp1]-i3A3Px -X_ L\pC+cL?C7AM#t4W78jn"λ+ΥH?8#Q/8ó43ɠ>r־xnj'0;X u"H`7QY*ÍTi|1@ _y^h}ZeO -R}{NTWfў_SZ le endstream endobj 390 0 obj <>stream -%AI12_CompressedDataxu&?dF Ypl* -ؤFZT7k \H~{dϺ4@2##r_ۏǻ?ɧo^{~tg~Mߤyvf۽o߽}{~S|ۏo?{}w׷?/.?1G7qgįÇfrݧn~w|vB2; Cs~l_{kv{Gw/?y^|pq.~~z/7_ywx}򏫟<{ׯg[~ysϿ /߾Z3r/n_7.{! Cq||ǏX[{sCoE޽~GK9McÝ1ۛ7^cceج{1[x=f̲<7v{{o3}x?|z}wooyU~q5kY ^#Bn{Ᾰşqyǟ8~oI: -+bfI2g~22w,;[q}~W/ʁy~Gw u@xkPׯb ~,ag߾:R]#1Of'c>ɋi=/.~:b&#yw ~𯃛Ǜx<}O7;cۛ{|}vgW/?}9G㛳_ۆ-η\ή_޾qͿ]ɿ_g77,?͏ܮ]y+w޻սw˽wy(򭟎g{_]WKkKzٻ\w8/\}X¢Wg/ށq~g`YnȤ}|uٯ>aޛ۷7ٛOǃWQoo7^~zyy}I۳u5~;۷zCysIJ@ ݾ?~ysv7[;W?!d,<#?Su<O$eі5oyG>IY- gb…Dg޼g/W™|G)Xzg -at0ǿ_X_oxw3L9@ kuo_pV>:\  &D0ׯ_~&Fٻw>?i>՛w`<~{Ŝ.o^Y %ov֏5hŷ?gSEc 8~nן? ?P. ,|;`WotoB|9>7|G<yps꫇_s0•rǞoxO?ͫݯ~~_;Ƞ?s_h2SHS)9CH*=vs94χb9ܟ=q܅tW96x>G_zsl 6bHa.0|Lvr4MqJOc2&W.q]::5J"yq9\5>+aq:ί@NW 837?p-FѫKiơ=qel7~>?XaqL<8x|gcq9M9ܜ4\mp'8\ނ,ȂT,1Xno-~r6r -VWlt*S.R{j兇(RV:;GGh8J\2 \AIX$\:, ^YS8h] -n-7H)& -)oKi'zc.ϵ%4N0ӴO&ljZ#ge|m -&u| -J;ѻ>fn{ t;Uh] q+m)}v`]F>Fs}:$|MOٌ߽}{`:b9|&B\N.a=>*_AiĪ ^\\QI /Kx-01_d7ڥ\fR\\A./p.ȫ2Yx,R@WD~C'*i`/Gy֥\~y d1`WXs<*N`W'0`*30Kv 0| -ky' m 7_ ]` L.[ k¹3~h>Ɯ%H CY=HeKPaZΐ3@:TcGH+ 9p( =D /3:lDmlpS C Jgx@7k(Ik^ݫPRuٽz0>GsPw4k#n.9t埬Ir|&ɨAqˬF:i6ߣvt.1ngfՏ;?g3Lr'i}>l041M贵>O])0nĀ;Թ`gue;W3D\4N˰UC'9׃_ Du8@}fkuhar/EQH"((`X+1^\\CDPQTeaEqREEAd4XabYAiȳ AijYQlexG܅.K;ʻ,(D˒RA\$`-.~?vEfx)p c.r1fIEYBcYNBREXrqQ*[Is^(-SR&;Z[. -#l8y"a"cĕ! "?f 2ʑ$2f$V&~A!(RPZ<?}6 Ⱦ Z !,1 v 0%fh+$:b2{VVky/y?mtD7Z,qAѦ'/h7]I ^GWY(?sxk)6\x űR{װKyz!xlFշ[58kx/W?߬~h{cb$SHR#^:㗆0MoP`>*y2>|ؓ|v3Xf gy;UkwS(E}XD>7ƏRM^nPz{:wxh ].2i$AzMHS~y'zJ x22 DAltF*;ž uJ:@O@Jl_ ',)DJb ZXĶ@Ux.4 7;HͥD%r<W>mʫulUTA|9/b6fys \cTt^/+i-YvU0snASjb6-J{5 &4*wT YeU|͋RwQN熎J7Sչ"x"gν2*U!>z-B<, |HDY'D<!}WtGӼ'uF&'9#o;ġIGIL|>Ϣ4w-J3-JZas]i,xb{?nk6q%Zjj k{I-h\ \Ie!$\2`"*Z*B*'NTP@?<9Pk]AeSCeXQi R*qQn¦ʪQAUʑTs\Y8ΰR|JV)qfO"[I_R=Ƈ‹6d?~Ǎ_ /Wn{SȬXO%,&APz/UV*x+LЪtMLjo_갭:<>Y=Ni}ׂj' ]=t ҿտhtkv˥NUE><+4:zcnΖq -ǢnxdK0g"Z*EVe?|gL.T&|̎M{M"74n6 bu7ݺ[rҽo=~;>kEt8>.Nh_#zu鐮15]\aeP[u*B6 dm#}:X-SO7,J!QF? S,Ȩ&3 TxiF&o,Jykç_Cvs);ʁVܸNꪥP -fq47D}'oOD[ -6|Yє|Ny TD!PْK؏vڮꣲvŒ%dA)ֿXrz}9r 21,M1<D̅ޞf{v⍹v/G. ovW{qNaiK0,S -^>_zoiq mK)%$0[l)?*]B2SƎmt{ѓBý8EA[F {l>_m9HԑbbENaA@kUN*C'ԡg~:F5mР>ϫP iV":I;=r -Sй/@>Ƭp(k!F/%8Sq - =')T4)T7dS(hEGH+c,J` P=LͿ9BD0XHi($[?iHxnvN@]`8QV4ߧx$4+zƄ-^0Y}IFIbq%FB'5zRXE(}?>辩qmstٴ9SG?ЄG檍YlBkP6mM:F/u:qXck4cgi7qF"NxU\x/K*:x{פa?}_6:u5<_umrqlbצ,<<>{eyƩ`Ԇ,_/,C6=un9z]O- 5;_XQۮR~+mPڬ4-VJ8ñʱʺʺJ2:l{ԀkJQʪ1ʠ'u.v8^{5>ϰE `sM0Ļî"ZϿ#7`.ftig14ˉ95HŌp[k&>MLڸ vr"F,p{*Nw6@#.mJX5&=ӎ9J4x ;x08&qs;hl<bFBM8|cf?cF>[ޚPB`T"cVҴO1%jpc%!V~Kc >Gw`l{'L jOp&y'!EZ D<C͡8iG"&| - ܠK Q'CsdGBW(1퀑4zS"@29g2LsT48`?crXQflmn?"`_ -Y#^2p-@`?a_0+Ctqt<KpDŽÈs6 tʔG);bcQAR~k<`2W>+R8 <l[;Xh!KtS'ƄšG`JӌM$(P(8#/5`?/p8o.xA{^,)<c#'mA~FzxU4rGft)%dDXpl&>gU+2"1.]`X1pl8dð C~ ݳ RֈT㾀Z'pb2T|D4|xVHgG,!:=u\tO`0K3@^g量W=-pm$#xb!Hiű97@dQgܞEOiDx - A@áޝ#W_7'I77ׯqO.οo{&U30߼8f~|xmv]+\c䧻_G8>ɜ/$ >S~EBp=Vn(v -qCc+G3;7 -p$|܁Н^ngV lۍDATz!x=0;Acہ )?x(H| k|CIh˰%CZk)):qphwce+`&:Hr UXi #Ir ceCŮsIl]3 2CH;0=XrS.O'I.#2z=,8 7-`~а+pX;C8NG (y,tA`HlJ3^]vrrljOg*i0}2 ;dxUΰl;g=tkªG Z"6O,"A\ -ew({:Yڀq < H=pΚc}z4A''Ba\ۉƹ+ , cPţf;* a`J"N&oaԢyO-P1|, JoR^Pis<F8W~_= ͘tU|Ȋ;8}]=GDH -F +Iy_de8헔Z6D`;P" 11 %tG[=&@5y- 8 |`ҞQ7`5c||G>g7@+GFΓP.hLcˊCg[/D`Mt`@,AA>dg8z2NLZ 쉝!0X)烝@ 6Xlr<4 ؀3Ǐ?xK!LGp 6;M`{*u, \ ZuI>/&kp|r} l=bE,> -Ko ]& njSrP 1XU%+^`@ P- p"dY/P Gr8ܐFwx`C -m|0r &( -50Pq1T]4 X* j΂m*|&@FƭA8N3˾О'u,<}LVTf)ae -: ǚfbXrvG`!5Ala `)(1Ig.?7`m)& "*E@8zOHB,f$./\N9N<*bN԰=aqJ̷u4?&?/h6yy882:Qx: dxa੸P,J~N$Tދ@CA@1K<**P. f}}[#Aj0,"}s@] RXԂWPЎ< *+v|8]981xLQ2"E -y/« O=?tj$^@֠d=bs 6Wpg]T!@!5G-m4̐S~ U m>&6\JF!Fs} $hܐ)O%C= XB#r@3%`5;:Ȟ,=,(iqê@\WFiԔ/V"rǃ.,RY -|MnmuBOZWV|)#ǃg $5V6*m^†с p"CE`cn4{A#nlI%T  \!ʟd Lo6$\7.ɼ@'P~PI\xeaBw MtOߎed@Bکb:xc[><^JAHKœ7'&s"z>ۣ8OEk}cdgEvR؏tY \KjnYa>͖d=jdO(Ȟ_vW™ p9ZP;큗GYlj7觢?> "2L28%@ e)- qbH{)1Cۼ`'{}4爁0#(k_‰R,C3wfۤn;A|K00*{h$tV^u C G*p q?n3i?hGbN#.םJl  -ZNM':=hBo8 gSStF 68ʢlx4y<:Rk QQ-5KiEo5fYPb|[dnIW^,3/O*+^HzoU -Ce%cP&[D`BST F8j {`D "cq81b0IL;Z]̐BmfHT[U;Q1 uHWpGaN(<9 gJ7گDU`J<]3`O ->[ݛL xeNiCU)(dtM`EɀaxiZ᳭!S{C6V0Pԓ͌d1uY"3^b,h'ULk_!?hki跙 ʠ;9xa/îdfF=s0*Ӛ7Jd(+\F -#Z{(IpmhsqT)CF kўk8K2ѽL{L3nP΋QmgKx[X$ ĸm c<#芊LɰLZr7M\ @@זdHVa\ ^ uܙ4SWc'c0衑U31PEjJC,+ڷ -GG.e(5 #5KX&#AuNq"p3iEܝ>Pi#]{qO)&@a"mM1;`g11f -vKC.G,8xJ-;Lt z:\"5-Q-nL܀7 -;3̨qv ބ]jaZ^wxbXwV9&QTgf㳡mXyT{AcՑj=T*Gӽu#WޠΊ(u=)bs_Xeqr+v|㫐PұEtաUk٭5Zѯ"=|P"]L)0.~RA0 -TM#kOE*$H׋^@QŽ* I?ɰ(&kܓB*ZP+G(- NbĔn˔~iFݫpJQlC6N)*f1wt]ڭ D*v~樟FH*2rtf Ne:Q1] -!U}AKʈ"] J*3ZT^eA=˞1n*8UHs1SP++h Qhmeʌ{`Q^Mm2B [E+ -WFto5ƫp}eW)2cC%6t$*/BWtnE9=D BK}[HfMtY;9tJyNSk5Q`t6z_^LY9"}R捕w_{ZHLn5ڝ'6ybH&=+ ,hl.ֵ7 # ,Uub5 Z/gv[߼ݮiWzuEpu6j>Vm|)T7Vj1~K<&m61]S&i}I<eU = A @;~0lYwDx,uX{%4$0$sZ$Yv]in`@,t ͵*0vo}++H("lMi6c+ 5!D ;pt`ށ/-}Tp<$XE)C|Ꝕ@ HĩO\ -a];H5Wu3l'Ht1?zavP(eDk쬚.Ѹ$aaLd gh?74܂1"YC[VuQ厯idwpٶsS .Ke5¡-`mԂGZԤfKZvJCI%I?hN᠗]+ͦC!΀H?n?|j"8Zf9 z# :J}qx~u5H-HrݒE)&JYi#dAY:N-b"<їC^*VQr7$oP&29XF%1o`9^<_No+&#Ub'j<d-Z&e^Y(2i}y~@P(S1IJ`, i4^:%iêo#_K^BFȞ -zteMEdfDilj0cs/ob]4K)(e)DKdz:Dw6N+@mW_c 2Q2ˉVtcdJ`Đi"Gk) JwR嚽7Jf猥~t\ ̉usl1KY:L?[ -% ;bQ:1l#Ys (9rCv_YxkV2t~wqEs)Y~ -gd3H0}'$.hҌop+r$03esȏ ;'Cn`I֥(_`|!es7fDY·?A]q?Τ)ռCBc2:܉[? ):α4,)%qULƜPz~Dm57 v*|Ąnyjf -pZ%BcoyAQ_ʚ}9|&k![}K 3x 4A"(Q+0` 1Ƹ\`<Ԫ>杠DƩ3#&+0<Ƣ%mc<.g; -"J(I2I^/>]ϴ|Nu," isg? $ƣtɈ& b&"GVf*]- ):|qF+HGIb3/6AҼlh'`J8d㚔BT)֩Θd6kIK5Jhu*=~Rg֜} [BcIz29 CeIc}-Y2잱GTdI{Sf)pF=a9!cizN-kr-yjTo4o#3ʍ:sPSL|{޹.pFw;cxh1{%K>jrF"oŦ͒83L[L$mZaX˝d`-Θc,[]y /ϡ3,/ZqvLRhۚheNnm蟃 `N)oh#eRe 2ZhLx3sbhҵM%X2wګ!x,s^ 0+205jf vlD^BōWCذ2$1i0]D }308O#\6t0逊ӐL H?@>\6ɪէA=51{ixiP0%ʡbk3(Yt GIʖi<|4~Oq.x4},Y "g瑹߉(?ĉvI~E>$U1OzcFkT62>5(n+6  - ~#R̤\ -"s']~JLH.0Fܞ 7eƉk)dB9cGO'ԙ֛bWP3cf+0ld[aS - ""c'1A9ML]b\5@G׫ق&H$.EɲCnk*4(FƄMio&J=@ -fJǖbکT*R'0 RM2HĊ b$ r|[:øŘ~ډf ;g.7}ϰ4dXR~AP(/T6[\W|5m\0_uڵ`g֢}dn?%͘b!})WgV=j̇ZGc e{ms.9F,-kaQd_gFC0jXöY6jnTi_yP,&8K@S5Fj"#K3 nf%#΢&)gl7/tFyhh|fk;вJQiŽԽXZCkጼbj=jP:XNVE~D,T7VZ~ZL$M3M.cwh͑9$m"U&!۩ J. 2 'YPMTu86ݫ@!ȳmZ7c*tjT9 9Qam,O Gek>5ptSMD)|$AhMq~VH穾QuvQ؛l˄{͡Pu-qt@mmnO -ݨb{ u4-d+ /TXNsX*XoR]ȱncTʋT·6E+3gӕ. \Jc.T8YYTtRafhhҤUzUشPQMa-rPp2[ -XU>VdUVU[TJҹ -tK*J*T/W0X-UxX!5UXnd;i *ܹSֹsWS V(\UVow庫*}+-[1\)9RTSJWKNz*v>GJ/ޕ6sZm(*}o5.Ϛ_~g]/|+@VuKj"P9aĠTjh&,V^υQ:5Ńt;E(8Q 9YEZ]Q I2?ut(jQ]5uTKlpmkր돢mm6lngeTaQn#e^U mٶm5wVe킔[=FHʴv{.5^e7&E'3hu(ro+CӺek:oI hO4>j=_)ccr)^_]}Q:ĩ"۲'״׳_JTm]5lU^`AP^ -N i[!"4/uwar+Ц<婸nO!U`6SH2hFA((z[E`(x74oc?B:UlEWǩ_R6lA7ZkusGOwkfU6HJ1N9mxVUU?lmeײ؆n* -T&M33jEܪYW6Z*~VUmנw=ZV -ڽ[yOVvv5ӫheׁխ@h R"J^5o~+Si\&ʕvr=PGڂG}nsϯ28>pU#5tr}Wy:,]='+jnlMIGas[eBퟓ%#-ߠICNmt塩"i=Bh3%Z%r઒v*k󈕊6;ɶ9\:ьGOm7IEFNIwoKseH[xymi* C)fU5߈YR !^ö\TECW8駩v`\0!3NkF1 -JFaV !:$P?8ܕew1JP5 N1Di"4E;]@v,=w.pح͚.:}cRpҋlg yr'Z$Ɣ!g:-$d2RlYUe%6l;\w~Ԕ/nK JhKޫlU;פ' KmFG:b1!CizJAQ]Tř{SZu(ڥ2ţjR)Pm_V`h6 # r{&cLJ2ϨnrL{fy+^El?6N_9Ʋ%݁]gx>m_Wlp16V<gc ھr̸ TKer!X h+H" Q%+ Or"P˛T_9jPg.Mdj,2 bwX&Yc;=j0}U%ihi6D)?z$oRlI4M -N\ m# -H'aԥ‰fs̕¬eKFKўE73Mm9 Nl>ۉZFE3vsw(;ŏLupL1wX J7m@1"Gc˒ -ٷPv~p7x{3\S\εmzl^cDfXYaE Bg+C$3I,T=4'Jj rH,ffBdC{wk,C"aÌ]< zݬ?Dӣ06vPr.biK$(2%Ot"{pk `r sv0t<-i|f^$Z`DLtXimoQ2l5V\F,#"(jPRHd8̛5.9J6yP+8`S1ՁǺ_3RA,bs ,HC+=ɂ6A!3GH 5E%Mbʹu49 -|&fQ[(I";)Hl#[V!!mgM$X 0/GFū,, Ձs4Ũ(QOY񙝙kY^,ĩ%9ް-C;ά!l%YEd͔PЯ Ge2l^88aЪ+e* Lqbf}#)Jr  d8d2bCqd:4o1YϱRN:XkeqR^11vC*$F)1ïCL"8Γ$2%dV.|)/52E'6g!V'Z+uOߺQ6L?MIa/ޞ&lO(PߤS,%' *m9$HX\E*!Olz0c5uPfYM3ZJR>qZ<ͳj,=ׁ-jmu&Kcyˎr9lI'PdQ.lq9PO\""*YI8 -&,;lI& CPv@ -`T6:ĩ`/$1]tvO`$1YH&?yG')%Lθٱ.Ne -( ؋#E ۠4YA1bb7FFvFfk-׋'u=0IZ~Φ(( 3)7)lfE*;R64Y2faeLa i,OpB:)=%ɡd6{&!eź@.vӔ7DϤYR'II 2)oe"a vL ]eH~n=\$Ph# &:8Gp"$/&P`UY*r -/u92藔#&I#cdgp#T$SXĐ}҃d&/3XRJ^,&)֑w`H"12>"9O(ʲAs_~R u獯Ξͣl/M2jeVTD [ -tR4cmC-"Q"TJr.PCH>X]4X{M>BMi jk2~i+R +g5uGoJviط& *VV )M{Cm7!t `E`wY:G]|^q;z;TNbcLȋ / -TV4oN'CYR(BbĪCIJWС#a7 -> ->hIжNٟfiI}VLːaʍ,m!YIE&F*)0R+Ad$E7R6Jz=LhWҌc CJE!lj`%,(A .PIR 'h/aGIAna+EXp jHMd b>4}qcO"j#eϱҘwd*Q\(Ė^#A֤ R]DIW09wU:44Te6ƆFN 3lP#kX1$םLj' -PkiSJLb( Zqȳy֬:xnQy4Ѕ)YK)VOU,Ej^@!D5Zw@}WOJlEp(?2'5u4ZNY*=ZL;ϵ&|dtLLG*[711GaצQS$c#;a(Bбn0`$g,@NXIx-4$&m dAci(t=\q01W<^$,.1l:n,g8X8nr1?n-y|n2<ҿF)Cݫ)εYKH4V@< ?o mzSRe1zγTCXs==xGyAc #in|ؓ)` b; Ђ$"'e¦Pdy16w%cV !Q -}c:}PvX7H2ݗݵ4WRr#,#uСI \X< |(X(@9!~b֣ =W~JvPCrq&(\{_o7ulDtkӥ:A抣iYQΉ:Eiv=y.fp((Yg0̱鱰Y33FHO&9Y:n.jP`\yĝ¼L\ycͩ;^g[h_a@UC= /8i.9)Jw3_jKaNf(%Ydq"}{O+syZ$y. lІ r+ᤉOHȝif/KV)# bHM5EQgF\7&IZZkBM Jad3'D0e7+JIeM"><*A95c%TQ$n,r{8Ֆ=B5eĭwbar eemz`M`eW ::}Hو*1@R@Mˀ)+ ik IS&`tbҨʒ @jJU8Ei<v՘I"= ~$ -R` Fm=D'mCXINI?D˺ϋrLd!q'ꒀv\y%dyTmzUK@ڻg$i=${kKr]=a1n➙'njXPe QjMAQߘk3!uԒE%\y1/c!YZ -z@ʭjfKlD-^BͧA)̷\۵v$#S - fa*VXUX k>!?@wmQ͎Ϲev%0W<$Cy,&][ cZWdݵO'ۭ+7+;$5Ѭ6OaZ9ȂSNַ\k M&qؗ' 耞6_i)A2vxxYah|޵q Xg3lD,7a|vE\E+R#ĀxgE?Gaؓ`\egftMlMm@@@7;D@ %P۽.žYa ba`Ճ#oas -7T?,/[0oyC[v nr@dwwzz;H 5b>ZPx.gQ 4Rƥ@7g-y~~AobC"b7)ӇtI⇔KoS71 boFz&PaMf成 MlZhY2{qd! }'!zWehf:C+MȘ.;zkލBk.8tmSʡI)d>R$yh -аuC6B Ch\.uf#t̅Iۖ^B_&{ wq,T^1[JPzWi޵j-U,/}X(6U, O[Xbޯ55s>ᣙ+)Aچ=-Y+lҏcPN'~9ZfhLʽRTEX_Md*: i:Ɗܦjk6V(YU,k%:Z0u-&|P~2fۊ<6¼ ~fNĕ/>E~~Ge]mmpqVއeQ8h$|F^GD'O|dQI%h7+-ד?%{I'^̮"*9y _˅rB7yPF"F|9H|b⬑%i&i&Em_sp0ćMy9X|:@lM؂#\Cg ]Kש!mٺG#F\w/묃壏YťM+*h4@3W-B$ʁP:JF+fM -f'PԼȔDj;Bq+`o6-vыDۃW5=f`) _K?ݚz|ϻO,nЗZCv!>7ڪ`0}*aPs3?e&(~7/g~#AΜ̞OӰWIA—3ꨀۙje]&+V A~G Pj̒L8]nW\.T(4`SR->uF[fQi/%:"~!uG8mGn Xm絛P' -*|E zzJ{!7\֡˯N&sHnF LQ('}BAJ*Wxn;p@"ܙ@7ӧK..~Nnu"4,]s WI?DHa,y 8.ְP"j䈶48}cO0яg86g!|ѺF*Gi^9m*@y d!00gDmfoۆ@) -\vO)yI_;dգf2̶t'ʾ%.I?Yaa ptl`~Z#9^0x4سSScNտQ)Тg0?9 {tWWɈm(=ݾH!_GŝhqC͠131nd[6=vk.%="|yQL`seXHޮ &=#~Qh F"5RY27E5" y nm5:xȴkMjOPp3lpnv\ KA'Δ33-#Fv$s-2jv"REtօVHt_0t`< r]]U-c5Oh@&mQveig՘}lk,p~(Qs L*9Ow(/ -".یWv*| 'uAjҤF5/ -ˮޙ$mvД XyLA# -2eHDh TpGsIJԼg`AO]B62 |L,==/b/FKP ݎGW%ؗ pP~uLWYo]*)N١t=Wg]e [)IrU]R7' qئMmQ&0̫۬Y8g`*С8#)7j)l:B.:ɿ-~dܥSv -RUD[N4 . = 2fܟV TT.125mN߰? ֹ;ajJk --~oao1rBlћoh&<SPG8D>%&x( Z;5ג.BP׌ס -Т[PfC"TSC@h83aY:Yoyru^6GH"əwspur%7RN9Ĝ3;~V=X8T"@Es~=Ѣc6"CJR>BD>v h}d2$`7RRiFvtl^.6ZșqG(UAG{`H^0wdP)$?zb9C3g"ޙA3`^PD~Fƭ΢l"c҂+ls@:p}Y!/C2TM{ -ɡl2&i?^-~GQi e/3go}w>:i -5I D!]>dnj,H|y9Ih(ca*Ԉ7eC6'"[ɌhEpz'4ivv >bFĢn E9i4'ވy e֥3h`|.c#NL"Ǻ}>]F۞.5W agAf&+Q(,v5W}8*DlY Fzqv9)-4LgUFЪ7ٺ=ڥŢ dUAX4HE!d4O 獜|QP|{>ļn:F@=f|Q$Քp(otĴ {^ج; - :>]TJ;m3c&&QUf0h{4i TQ* Zdd/SfG*Pn[iuQ:~I*:itp(w)IW`^M6r U71mv~E - Ӛrtn 7. Ծ ?Fvrq nvfpBmtSe)![ryoztq:ЛmOms69ٹN - '65' -;R6[fQ;;5KŚ>7;uzi :v.!~h+K|IBqci0`ޮw6v-k|ڶ?<5Z D'%f8)N#M>BRBLNZPׄO^z-43gJ:K;x8iV@Û'}C:&K"" -*QI4rґzN6nыm +iE"%,7tcƍ+?ub  Q7궍}OnBw4.5\1 ! lN}OߏYQ;}:`]ߪXJWuttk!k\~i'RCd߹)b!8MR_EwyD0noF<;Y3J&ğlJf ~29 ʗpo -x0pGT%|slJDqqKf<9ΊjGdtYK}9.:||nEGtK8t}FIplx뢓N8:AFtѹ& t&asMa,T9E/񅻙r=\]#d1fE7S;,܎E/,\]g'U1ioP*vr{]An7 -6B/+Pm{ָ W~Ѩ@pb$06)0{f5|nl%h -)iYsܞ y/{7Q̏ {x2 -~Vlf6T ·N' دSR38"Sj䭣n{ XT<O Q Вtf}7J`"0ٽ?LfE }.78K:nHYZ󉿴s Xj ӗnz'*_n:4M܊'M@R~ů3z"$=Q޹8tUĘ#GO4[Sg8ztE`fh^(J;zd[?] Jj`s/"ie:TSƚ8Zx̢\uf$ !-:l_:~~ źج7:.8fmu\]jP:Z 9u*bF4 d3#ROan_g(n7=Գgۖ+in- R\4ϥ`CKgȶ"*% 5 -뮅_519VcW_|#>~Bp[_OG,=D/CdLʾ@o,˷Dwh//O C*W~CӈZUsbcԦKgW~y'P)gaʯ+ڡ!yT:cz$p}tPfH|A(lJ[K(i?׀٬"0Z? Z Հ6Kҟ62o^0qQ/`Ȃ_|s_0^xf^QFJ[FA;hi6I0f3$lAgQmTYoIɴ W\!ŵMzH?m%eyW I} Vgg=i'/\qBC,Syo~俳A#8loF!X"Ti4b۞E@p6os]Ogsp 1& )㬘aMgꕡ<疲\@j™zMH.'Hm6$)$8{(5aPc3GmM=G6R]ǏA_3:cW'*xmX HTǦ9on_fp6H͂ZNCÓM<<;<&vQ٪#aUΧt_Jj= -H2L~\ӠC%KNN7,17 -PLY챶ۛUW]Tټ7#f Y3hQU o 3ҝ0(NAQ!>V$/kF;:"^thtH%ٺ]ZwqxyN6X=cIh 6O\lx>N9M5%üw_?[:RהBWH6OklDlJCZ\x"XXkxFN%ᓻc.9ڷ, #[CgW1I Q_:Å>̍k&Qk -ݤ/Ǎ6C):d ֖g q~fNJ'G|Y҂j> 4 W - 32Ĺ䐑n#L]o:dӵJ F䂚5gHa^:D4yAl'7s.o3[TP`Pa+L:Z&5è!TY0YJ&ϵ}^F3 [˽ "ؘLP`%;NOgbyl~E/ ;;3 - .71HBϹY툇>LL)`ɔ=e'f(&2Gbdы"`(yh_\Hۨn@RHry OZZ~0eʺy;FRNY"^d7F#,r4ol)Ϯ2# FTrTy%@N^d]7blEwղ}PY~^{\e7A U(Q p'q*y)H<׵ ycqoQL$3袀㪎!H)qxxiR;[\m -0(WM4J vJWtHЦmʢz7X%YYgms0L\V>f‚fҰ7шD7",n;=m CnUql\}ca5Ofz@'3h%|%^oC+PF1 D66LuKspg¶ph` y!AF&ɵ; B/oWrNlU_r&g& -2"\zY$+= -Kh;͹:-9p#(J31F -dû=zߘ%B&}z"HNy!e -ަ* IUϾISAxMuqv_B˘Q2UV;5&Zjsmrw/dJj!a(x^Ynwwl8F]7P`Wc%q^P$.j$8cz3XbYš[Ql *qT{.m~6^ -* Û :aBta TWip/O/$\>d |#r;=/]flHu|q5pER3{`F/޺^63*b;JsYo`/xsY -fm=S,+qy:h]/q1fKnQӻBAH0T;PƼtmCuֶT#jyw -83t4mcz>P-HdFk4Dzvwl/DIi u㈷&`Z(`3צ%i`H5F83kDHI-m>;V'w - ہءm5aJ?ઢZi -.1, Nh8_:S.'f9d剭ޜݤuln[^|Jzᰰg>D`w *3۽]É|\]suefSƍY=|ᄩ?08R -tTnRS摚Cp-Xf}$I@hE]b,{ 8<ði pGKܸ"?L ׬=p˹ag7Qx:34 Ti;e{m2PT !exz->Vt3S)nnS4`=tMJV.NhL@_Hϰv\H6)!Hu;ǴUUcTIxP*1ƍE=4q'Xڐ.I@4l_YR ?U09k:=C7s1t}ң$B%XVB ˳ZΠ! `S~ub($ʉIg8hĔ3ʖ5+R[E9VL-fjpnI³|mּ)/#XѾMNY;TW2#b,|mV]Lsc52ݤK=I;l/kr]ѝ]0i}hiwjCI/QN 5'@r\%B5b(>tj {ߏؕ|PzRS#ש)zz1kb"g17"ȍ$f;V2uSČxy{Z%2HDZE^3Y [Pu(G#جW Z2y8yۧ[qz~|CZN?Oyf -^vA' -hc<_@slo@*oNS02ܭH O035+h4v);7imԽU/xkXؿ,օpH\bn ȼLP_p&pح) O~I6`,;=pd6j =V6 X -m"wų.Cp@3׀5b?>cZvv?fƮ' - s 92ĵb"z[a7N kBmtumrފN1F'v'\PEMo!:lS.Og=í]P A\wer٪xV?>mJCL Yy=G#<1Xq6?%$ΚiC54 $;GV{>krjQ=zԔ~` -JxDZOx~3|RN-hm#}Ce$"Qgɤ|Qyq Oqnm _M?1_ )IjAY -WC# Xr1z#\ }A*VEltQT1Ѕ,<>Ⱥm+밄ā]Ϗx?WErRYJMwvET =K2^*_@h\Aj1SgySr&')#jo`qO'Gx}FyU"- MނhC$5 4gr-/k̳ih0 *gDf>*)T Fu󐨔KO7d)K$Xٖi:ə,+Yg/4I㜙H#O_B5@Ưo;'WGR7SԟB~ l$34 O z3&?=#|m*p9f߮^*.kУ=WIhxAv+1v^Fv)&H%G0WmFώݿj*a]dՅ^7(T1y籑e&w̚tקVK{0N.{gnʱp#f=/xotprɛ'7` =ȋIa(5$; z؏u^vVΤ<6H_o(GRڄ&\úsUƒ ,ǹyvHn{"zً7ѯC -"#M]CwZ?.+?qG$gI4 ݹ6(y0TP/Nّmg6 tG4Af<;!"+ΗvNh@⹂%7)s0%B/"!ف÷ K\<[`<'6x" ~˰ylɺObX -;ia7^gwԷ$8a",M>89Ouv|iaѿ'U撫V- (#oG7ٲ/uMrT7ٱ/&|9țUdýMVm{9HR&[ eW)@LF(<ةF3o -ឨM+[iy#r -Q1|=Jcbٜip3^Rn4k~['HOwb5q2h#j_W?rOHQ5,'1q:WleW9qhCoemlqT*b`}ɽ^A-np^el Dz?f5`B~ŲOڋ=Erf]Y*5ߛTSDa{Qpwwþw n -n76{orQhw=wMnU {\PqAq7lvQ{wnvArwEu^ww{]T]dQzwE u^xw{]](˻v/įM6"v, Ϫp#>kɶg)YnQ^ w{]%]Z4/Iv v/NlokFf+ "'rA^'׽bAv_j`\}uԯތofShS)eӂSGt}7eSiO< -U6>n3{?y2}МVH͟2{l)y2&CAR -N^vROɃ&A1'sBS"7{_nlS+hKu$*Au$7p,jIsjؘc>%h cK<aXu* waA:E xnS4\#"7.q]ڏr؇`倐? Q't\:~n}2oF“ 堮>Sk6omugߥ|Y-yX7d쇟HI[o># u*#,v;3\5`=`&IODaa6\JKE{zQdaU|^፜J/qLjjQߦI؞@>-p紽]:;lUĹe vs_gYr$/~Lno.QrSVLིu~fX" C2a(~`§ 3Lḫn)M:?9J(#MQ$E!VsË -y/hH!2! jw9EfX\SLiNOBRg 8̓ ["ZC(Z0 QgG}'Iʆ@%XFlT#A )ֺxJw*Me3e<_.dẙQt @G}(`W,">XqQ':D yʮ槵mi>"Cܜ3"\UCvL`#$B6V+ -eT0N:{VYM/VNBs_ap#MI)6CGm=괨|;L"D]G|SZY$]΅Db-?8Fz;T5Ͼ [ AI` -TwӔ[@A~}z#bCB~um>䷿M@wrf;,k3$˒j?y=ng!# ! 2o3˜_/~8C/ѿj TJ.ZK>gHz銊g%?ُ>*f0gWMX"A2IE]KtX}*W24 PodcfͶ, ګ(h.E֠X?BZ憉a}oI&-:%,r$K @öu§eKTyrvq܀⬫}eU73iذ]5HyыGEfNE\(9LKb,Ԛ,inD]I_&6 .A9t]1Z} lC -Mld4= TDN-3 -tϹbaQ$]qͩ]b'թOLC UPar&ٔ6v:_ㅂkw aVYy`l[=KL ^d6- G|7 o`U;&8\C*ټ|ۊB]Rb9˹F^xm_Fg¸HSP&)-CsJb5{w*4 -|Qѩ3Vdu-7Y';^4&UvKuNm'gfWB\ԚJբ  -ȊeWzT`6:3XiٴL*Ӽ$"9Uw´H-,@&IFTuMB)e1jB YBɢ@N, Z=ocO"_@ MbD\pݸyW1 MYƀIkVA1Vʼn}D ٿ90X昜_ w!Ѷ<KmݴNqouwz)aWڲRfVQ!@&{w5R7#CGACd7J C=0{R#ؼm@,:k39w#MOgZi6!Y-ΓIqE6:$dAVH6-?bkXQ&H:\Uz4g -P8cLB,E $w26,*BIߊ' iI!2FaQ5,m6H!z2͞y)=_ZP{h0Yz|Ci]@*k'MwMJ"z'2ATL*>}u%\LєtI @W1DD&qF*IfSf̀oRO&rF,98v#sw1ŭ>(R£·X3+37I mFSTwCͩ0Lmɿ4;]^x kW{%5K|)i ;؇#}+?'-[Ƶ8 JH7|*<>Qxv/yU}x秧n{EUӐ`-slNDi{E}3$?c%;Y -%ʡ!Co9x7%(A̫ݫI@dJPvy.x\h#7hxZ+mUD. 5H"NޙrQOmސ -.&WV7>:悐!:Vݖ>JP6Y+ -S^q c#Bn9waV-p {;5cH$51{듂Ŋ{`"SvXjs7hxy noakPvn.MUt41i߁:(Gd&h.-zA~HTeu#' -]s1ސΑj5MLd7!^v-+,פֿ9;Kpnנ:ӳ|r7Yv>-|Tdges,aKCoЋb]&I0;ĮZRVSȇ?z3Hdq0}(&VTHݣr /hh *DrD1dNC~ ,g6V9⎄o'!׿g1lx@`mA:TYB}]B}(qf2 W-"՚zo[xI } c@˝@^:twv Xw>E1qˆu3$cȡ AHwx;1U:כm&+N>A2A?2V IB(&LsA4΋5I;N?imcwDPqNhzy6";ȗv1'Ubf(,w<ɏ -|6=*8;;+Q]]+L,X7` ˝Dž23XEwE[U񶐼b3f梁nj(SK2%^} uD`~e>C^Vq if~[ s,u9V%:,3_QuXGC[pbC0fZ2TknO;AkէbrzVⳄ3 -cVչ§]9Sj-Mƙ7^ˤU[b[ qz_l%hqJnz4v@fV BP&!lE"lЀ7($Ou%8"yN'8S+fhQp|)\owp蒮nh]A^!bKe[G<Ԃn1X!sbE܅dbb(sFBH}Pcpb$m|ZC<Wj -d+6 Vlnc}>d -ǼJ̊Zfx&ytL1M61!½CB,w:y\_3z}H'\0}`͋mV50Ҹ{b]!7shئCHt$},{L}3!nнh=r$8+Hs3"]R\cI'yտǵ^+\cZpex7oV+oE+@=qov`ZU|Gwo8;3E4C!1 ogA4(\Nv.-UE`q|L|]^ i0OJDBFqۖ&v##K/<'̵o{=:q䮹Ť/*,*5_*_>4By5/LO o›/%~AÁy?dY4ٓb֪3l,$މs lA푔 )̱Έqf(Hڷ6\ PkcQogz79Ff ޼@?5<.Ż{-Ɓп6s/EucwƥnMn^o8}>7vRL ѵ!Q~wb9Vot1ٞQ W #㝱Ѧ-s(H3; [Hƪ) -<4H;?"%CLS9uw0IBIT |R di.G4"9q)Vn4\duRє/Lhq!=>Д4m.|$x/IPEp=\b ~Ztd=ڪRDJ<#* #0x%ܲ5a6")uNYUSOUtpyL[_XP7ky. PoZ3,>1>(w `sbtmQY>69ܢWp8>Ǭu Gﮞ)P=ofd36yY-pl`,PTpj"He-/]QR_q0 9;MWT0.ʍiW^ȓ.Hq.-(ݺs3^.v89s;M\|v?T$Kz?LC0| "7`%L z`ZzbhQSNZ`l bYtks(n({@nKFp(_"l8 {Vaάub{vY^Ug 2va&؇ܨ[Dz^"KVL0`8/~{QKUfuSm%if -xX(7c§/c^D)\GufPJ+MuFRiKىs+ ٗQ"3%c&GUtfX]Tz33}([{HU(u{wOk/u1kH6Fݵ>[񕒧(8%d,OTX\T%l^\) RTsHc->'T",`v2^K]@U4w>s0:`ub|ԝ|Bm92T[*`]^bW!S@Y`8m3Yg,#'g; DAPǙ\ L*;BX_ -N w'ư9&ORC>e/CcⷨpO-jRp4DqG)4B~ Ju -_ aРS3S9-_5IA@2MYxYDÖ{ 2 Ḁ]>pUM)%;j#@\$S64:Ƀv?Z߾Y.BKިmZ.f\N_= -u԰ }R#d ğ)"Y싀Wb!vRqoȕ{56d;æq?j~`"D|0u*fӆ.Dt;>w5;:vrt pkB|qw'2:mrRzүG&!il2Y׼}vsyF0j~1RL졞VLC!dfO|H&q#x)RyVIl32T u7#쮮E֙TN$`e[}3dqc8ʾ|-^\oŕHDK=k?Mҽ ޓ xaN(owv^_ͩ,^KɻTwpcT3HK^Th$U|1|w (9+Ac]ovY9tt{J=6N1zޢ)*{z]޿4] c[-|`lu3}s3u~W;9?b~o~*Ww^Om|s_蒿~U=۶og?|~n=ο_~mѯ6}ů?So/⇿?u]5{ {/? }?xy3?].$!B?D R'ݟi?_g?oo~ o!|z^2!C:|Ef5gdO$nЎ%_˿3moeTy^lwVx?NW_lgg>|RoU&JU~m{Lߞ0wHrL`&{R :J>|ծO1 ӜqH`lGPMsK*i=5o -S17W ǘ)gEW.Bg&7$gx]̮A4v׼>ݗSUMWvvRYG$3 C?<~KEs,j"G'ng)j.TL|OT'A RBj[}ޮ2Ԇ#H^;SAVp?콝36kʎg _T(*3hh)tj w|{'ƌ+ҏ9h4N"*Bru?*N=U2&3rml83_q=/r'FWhFU` >y` K$>.ul,;Y>NnSP YmCY}3{g+G$04S⚤qkTtJq4?KwۂHI$@;N~ATIÈ+]/y8զ4 Q!4KxFK= -1DkW< /afӫԞ.c0o.M1d)OW ҍ߉cÒ߉K} 8PG\tGɇ`! }a1 T>7+ 4ﮈm`CjM|)#=H o+5I;oDĤeE>Խ-$O]eU -U#1?8[ˉCuNU2}(hWccc'$(! ހ`l㋽T[bRDwӏt۟^K]n|^@8sAgeG>yUF)bzZ@%[Ɩ+Xrzxa*NpnKtí@dD讟@G-{ `zb,@|SQ6w>OgC!x8_:҇,ȵa@ү$zl-340( 5]xpb'l@,xh_Rg%0`'(0Qق$reNKs0MrC?0'QR6pW3J2͊o)溴mC&B W#iV -@<~}'HjS<'6 8Ď\a_7kb |0`]>rרg3! Ɇ <7ˁ!Kb3:록-n9Nћ$u:,Z쿲9eMSʇ39pbAr/r@1R9B %`٢ -Af(2UCl IoЃccZd= HBx5t>G!2=jŮ>̳ĕ~<ˁLJjU -o/[!6J:}@';\˭*2kIDD" 0xhti .z7lsD6U N" ΰ { -R%l7A5`zQ\vp@1ı*$t̍~U:@2l9ȁ1dӶ闳h%P]AЦwYdGN r$b%D풞ɱ ' vLB:,QTZe)V@. ԣzШ.盀NoDAy;ﳪ.Sg" E -DS/‹^ә-T6k:jgQ?};!\ΝJb_EQ49?%{@NY5̚ Q>^-U $6 8v~Cֳ b6} -B#jMIPυ%A?ՠL-Ŝa176x$蝠13/*4VU}0fz]Ba\%A5[ K:Meǃh]t8BnE 9|$YQ-MEԔOLcp UY -gydflW.H:Ig0RMX5ȏ;PaHIpCٵn+AHP>pl_^,"\i<1ds\Ֆح}g| %MR%pZ3* 8h="c(B үO3"2skpxrᤂ57DqA4p.mn8eALĮtWgl#àI2p!"mΰ^L'rLZ:HvwPY,yM:NS. C3,C 5z1/ߝ$4D=\% mz Vh3ПޠN, /@]j$o%mSU.YtVDG'fad$2[TDfqkqxr+vsfzٝn8d<6d# !{Fpk@! 6xj5Ig/=]Kl~!Egq *v4\>S%>S΍䵘mj$z;>;?(F8h9~čvKl X8vBU\ "0AUz`lnEϘ*}JnųT9ZŠ3w6g$4\EE7C`|A_w׫d[O%qAtvar6奮'`-;m"+lqD(.{ycSY;lVaEg -tL! -iR`A$1˹cs/gxɄt,i0$lS x(t3#Zz0a K^ԯGm*B,9T`g(b+#BV 0G nǢafuB?D@e7SjҠ`3 --xss؄M.PH 93ֽ}9h;MVŜxl[)eQZ|,+TC/BHqvJ\H C0C!|POox@ql .dE_ng/q7滩f^fP{ǮC0@梣zҦYɫw3fr opM]A'xyNb r:8EܧK8}F9 ;?zP8]\t!mx,ׂtm*.6Sbcng;ۛC؁Qwǂ -$h^*G !"U<SA-(P9CσFVo©=`ǬOFcj)D$aωh촔 Nuh gKGD/DȾdco=gV QnhJf?塢J~ی>xOd0]y[ R$UpS1ဇ޶ocבsJ0)do~`1HJTĽ}|Juv\ -9QS`ڔhwqioXYbjjQJLSABA 8ٙн0fDϋI/K@2b$H&^8Uj*&GNaۙnKgIj\,_~>wt;ڻʖdyOwؗo9b^e@Æ|!Ai&V~{\VbVsfC8 WgIYHEF5[pEB%lsA,ڇ6Ti; -g.nTD - {Vzl=.ψ}mb*b'ʞ#~]֔oT#j+{4t| #tEV*-zAY2XyDleY!kl-x* 4}sMsI(,0 -gP`gQgt 1(;xHq嚩̂Ք~Voڡ Jb^>M;jy8de^w{I-J{.l;X3tPS쟂_2y8L_qle Kq&;qpQUE6-5ͭ,sY_N+/Ua% l -RPZ -^} EO5!,#uRH%1g|f-(ݠE%M̠B; 2ʱYj.2.wyAV0y4Cz81aOCؠ^SwU}Or;eA';l0:hse`S߹(KEDvPy(l6L5Z87 NҠk `T^&QzmETp89a#ďk&dݐ -oӿ?o7Q^`!G&чijOgy$E?}+'0~bom 0u¶[aԓ-GVX Hh)ўɃ႞/%Ex!xMdsx Q݅9H!4䉰Z; ozBTieqAp@g/~!]iU; PˋY>MM->r]y0<_gg)kM5;deniz_y~ej)HT;H8 9f&Ŭ,鏚0 ~$G (piVϢhꋻG@v)bXZDZ -rmx&UFi^ $`(8 "`T|6/e_ٻJ~X-C}>=:_0V89ez Y5tAXFVJjGZzV0"n/!=kk'fQ;>rWcƓC[TL޷D<-D瀧A#cӤΪZ",Pkru'4w 3z)r^T2amnq.A]#r*]z.XJ[+0GwPϵ( Оv@ibn6Ͻq-EqHۭGtTǑK4m H+ܒ|#9A3z.Rk1W SR1 -%6A@M |\4<%!]SvD.D@G΅ѳ&jݦ3}NAo=w}vdd{3vr;{k3,Ryf-z{cU<5_3F &K6=S_ƛCȂGX=JR &((s~£ݿKBIf)Ao{WE[3d@VD>}wj!6~N#^8ăq3J: -C:іX&㡕"Ta YᠢgI3*Q02+YRoZ0iv6LB%Q=Qb?AIxl?%G aR N,߳;;i(Ѭ4 ]KمțOd.Y0 2Afqw zz<+6R텽Y, t>l)̡)Ȱ7c ?]i":|9"),|)EE6xh ][)`k΄_- ~9c_UaěK=_k∂lVx3=3MX3cԤ X@&S=է#ó _G+)y VB:sO! KEN-4Ad`PF ۏ0(M%z4vsP7K &}6P=+ܦ¾FG{AJ%,]j屗pH4U[]x1Kc_XuI&wEp wGBRdJd?%Ź_(!p rd".q8F[kB=O{!I%y]~-"pQ-KW*_ՄeBo/ K<^Hd7ۥ? ~tc8tN߬ڊz C -lFp4+& O2P(y:KboM9H"B.2C!{ *Ň ~&}ChqKFJ.gѳW/{QÌyN;Ugcǭ/!` wN*J#[:\Ld(#؅Wo"s,EbLYC)d-171FTGPrU:(ᒪu VP P$vʭ!∸cx+^N=&;EHF/K@}0qjx* -MЦ0+,)!ZӣqźdSSuC؂b Tf9UZXõ8EZM=UGS<[z0 -;Ujt2QwILE-ȽIpKH1A+ 8RbI,`;R9܂&E8R=ibH`hzOmBƠbd}dщ2!o2 vyk}4H9n٩`AVU* ٵW3x "Jv -DW3(كnE=NQ E ]t{SgƟF -N>th$QbA; Ziaq&S@wB\GEɹ"yȸb#LYZs ը*Hb%0a֊Z:½/J tlg[{Y.9ÌwA3.dᠽ~ǞPFGIOn_@T()4˃R},<3+!8Q Bx -0J#rLBv:BqPzMya6]rڙ:40Ka,aJu?yn[>A쐋(QDW><54ydۚ^¿Jl3C2EF;oqYHׂjHl`o 3+0VoҖ%H2"5ن_ɧr,>SPL@ |J! {\xY -֫YMT@6t(ݡPq3is7uL@-[F\iYTKmhwOQ$>qbQE5B>gFq`aUShEDCj;r4*+߼k>3BT^@ٵcm9BϾt.8 ؊rAOT_joNÆtG7?7q'+Ij2U˖GYWZԊ8;fE -'^;QXF9͸r܅I(eq-%σXBĠ|x*sHdt:;{F^n`j#̇s&.9ƭL(  &<¶j 88`pC3`D9ZTʡjf( V5ph^VQlQ5S:Œ.#Ad'"LBx-RLpB\D:؛&zD6DZ & *1ϥy ,m ~- \Q![oxXI'& E(E=ͲpUV[~i{hG/J`8|*|iyUi4*DbcFW1Pƶk>!ZЬyFXZtJ~{"iU1)>stream -W3SQd@6s S]ӞpxN[J^j|~eri\!V3|U#2"!N-K(Z]~16/by^FUx+K΃~疌G{}E;WFR ,o/N=āC4%=#]mk1V%hL6O:vTQhA=Z];BJm(h,unr?eQ@7oJ6 6Ͼx !% TBgvܿ{0R * $KA78 #-Ȼ 7/QxAIt Zr onOcXS{͍vjudf<nVg rO|$ &¸qAr \wG1͂1%·xURL?>o”^ٻ˲M9"rX4yeSDϠW`.n -λ^jj)7J §T"n&E;5KDOBw* ;Gx!'*{UB̒QN5wŎFIYpYCT%lm: u"${ &iG ΌǸrp>jt7@М e>zlDjB7.z]DShAՍ*\>6=*p2@G *]Y~헅O|k;ȀI?5)ac=]6lޫ(*p$V4`Vxd]dtY:K;?5UnKY^ȿs@a$ێKQ dv_ʓU!k!꒽|cH @(?RYaC( 6@(ȒPѼWC<말N8HP (E}r3gv'~Qz[B` kIyLuI'=,o3"0}G_ϳ= -Rھт&ܧj9eDZ|8)v%caf!q|Gr}z g: -Ixn@d"%p@p!oUG" 2^I*[8Ʒtϗ7M5 -uf(tP=I*a@-aB -T 'I_ P>np -A/!gY6:JtWiQfp5O׻ ~F/:^Ys3~>U _̒-G]$)'d[=8anatÁR= ɘ-x"da5Myb9ת5 bYD͢uqT -wgVYp=N[׋Z/-> Ҹ'XC4M:j)Zlhaj%W"Р{Qûs gA@q?t~k-5v{F:ꌮa~~ul7VDahӋUbQk@fu%ufiG%7c(&SY{%W+WiO Ao]=լ.FmTr\ {6RapES n94!fv6B""s?]C۴ @9BzV){K2lYauE{[~Ki y| -7T@D7zE^RxSH2a4Z WDPV,8xj! ;JBN%'=c"譗+|VTF;7Rtʅ3f,QL+@<' "F "G^uy;ї*ɏV\6[^ٕGq`f; -Ψ<|4үYl{Z8${?q"92kLhtErR/j6(>́cE '=Ct0v3a7zV\&(i:YrPTa<ԧˊY3Ti^҃fĶ_Dz̎贝f"scM҂%݄>DJmudCrØsNDQ:i(9xF]>X;WT؃+Q[H& + P+^mkCiyyзÙQj,('G ,Y( ^~Iv]s Ȕ1O1Hڛ0@Ǚ1O9ӐY .J}E+K$ؾ !>d-"JkRMM(¬`8iZU?nNR%}q}ۈ]ؾ6KLv-2c "Wrx4,HiNt_^R ^YFdG@TF@%X{ϔI5&MN#&yc|3fF3f`DuL=ybG1Qbgݍ -U!v=KAמЀ81>ECA\~7|0+j6 劮_1д"RġcEߐvކLԭ mu||NqZC~61XZxqs)/|qJVtƞ1,KZ'7`6{a;`_|5C<``y[^>.-¢u_[ v+$&hdෑIW'cTýah5>a1# -`.RU -Tmοπ,&a%GWc98ԑO!Tf49q.ֱTy}#\'72Sl;\Қ70ɥ=>dM] 2* +~l8c9Z8J4,x&#\ˋ6/WH/=S ax-i<=?}JX>2˜h\XO^m&g0>k\omzI<Ѽ"7KlEā̔woRTGP\Xr_=ql -x r;cE7D>:iu#vQ]h+TnI(=2Ѐgt@vnn4ToOKNuҲwu7 Y -R|HNO,>éc@ -ޏq? bF#ȯK-4_X읝WN|sqU5o^aVQV= Hf/W0oYUZjZS11(~8"jЄ-T')b7Y΄ؑ= ~ʧ|}ZiT`)jzQ = YʰC: wXBoXM:60JmA 0Lw^V$Iz]O`D|lܿ)WݴwS;f jQ,ZB8-+'X{%,+]Asf{fϭ> S QB;Es3U /jyRC+:*A&O%tބ`:0pe78d2 g8T9A t#&)o]؆;W& ;9P/u}jl̀-L{x≝98d&8ĕX'+GIz*p@"?ౠMHd;R XLv$jc ӴpkJ7 6]:nŒ;cǂo-tP&+i}@-&7[A,j:=Oͪg -"`Mx yC#$Qs -, ^J `t]1WѾLubv`=q;8ROC#99mG0y/t:y=p|G'n2Rɡms5;gw93Hu?݁09C/M~CoU"n(Wm?W؋ʂьر/~QG<MԸjV݋(*jKYi)B$. PӲ lW -'F߯ƒ%>2_?YOO?8ן/w>/S3Q_'LsĠod@! -D,h|&8@I?!ӏӎGSU@2E@Î8PډbP{=\S*HF5ˎꜱ3qVB̠3%,I>O0Ž<&a;3gƌ!/UJ̘;v32]њ@!ԛ9S H! A'I!T[N ](=* 4={/ڐ;A*vߊ(KMM04 09n帋h*YxxHa3^D(:G7Cz+pL;&ǤtM{9P<(3Vt*h8xU2 GbZYa v#coG6ڑj#Σi*bqYAR Z@,&jZy ?`bF?b@v,”q >E?{c̢kǬ>3a>BK(*:NSx] |9mLLRhٛ`^tgZT4Ea!\Fz_G[clq4GG^.9L>^,$$8i[1)• -Eh;ctqȬu2FXA yJٗD,@ཱ^Y"(Za rFuJ 'k8* -MicW|xbl։~A!=6Iv#1Hs];)3T2@b73V4T_<xJl 3`!vRf 1K6"+ObvH'obImǐz<- d2ɶ9ׇXPo4cP}/ʢ#΢0$zrcX$. //]%i4hH0/AS5.VAbHu< #\*X΀b`7_*=4#SݒЕz2<EZetN=h)pmǾyP%51 d\ Ą{IA{ ķ ߢ'F>Ip_w|Ba8:;?&6.{OC@dm45a ! |m{ǧ zkDQ[⩅,O|H+3uB4փ}(ȃ)QQ{^mG}Aޱ9`7P-/7M¾ -o{?9Nh# êL`>t 3SQDY?jp])-cLq Ms±|z]"Ht+%g͹b75j1/rUNwsWv"+#ẗٛ+xbZ VPSRkү%XɽOȲ=F)?4"AKR)Q5IG+噕J73,bEk6|8}| ldc)#GX O`Ž+r8ފD`ѫ2gz` 0I21 s\+b>{F(ᑙԴ/fD;y>oǁ -׬BvǔJ AHEOf(@ߎV. v"IWW){ ̏6@W({σ+,"4; -8)gZ} [?LǧzބC0kDmؕ2vmE{4xM"'!{ 1A{Â\ -شtG 4\IT:@J-!iv.8xaLHK5{Aĕ <C -?|)(Qkޣo]Z< M>5 /Hz>D:}D JU8#Xlo ]כ;pݜ~oe*W}+dտA*ݕJ wH/ ^_6!^jᱲ*޷?E8tͫyv*V|gd^])eZ#{0!Ysyp';\Xٍ(x yz.8iY᱉؀1eX>޹x -DXpPcS ~>M"oU9Na\y:l엍=xq=\$*& >w]-GCӀˇKkȋŏAac lt0pSK?=Mya+714ƒz?rlͽF^: -s 0<,T9* %}aHY,H>U:JǓoC_?"^-\z} -?D"L6 ,tCz7t~D民C>NdS5[7(0PV"ƚa4ulx"^Bt[lT~k:ng T#EHզ(H$%VOưpR6CRZ /:p8/SfU\[!Kpd F qpDxD{3cr.PP<| U]^ڒ<*O xFg]kSs|YA%ER@5y;Z{2Wr Le4F8K=GX 7*Lc%|گJl9wuC`b+R!#DP DR{7 7Gߓ ڹ~jT$ |, -7)+syVJ'-8>fw회P6B֌>?vYG-PU:=L-ܿIA4̂4 |BH}w$Aי`1+0:ُ_ +L1aպp!}i`ywQ.q/.˖ܽ75E P 'I_t™ aҩVg # V4jWxYLH5D%3䖀%ɢ=<Ŝ8>naȾI Ņ >&3`&%/,>|H/@4B_≨#Ïvg_̐UݑbV0:.W~(Ėk븸dQ/q PdEwY<({;"3U X?ΙJxX X1 x8 -6/ЛH<ì -vHJQꃕÚNf3D34`g|S"v>@ ^`@Adx-@vMf80iaa?+ -KO,H{*כY F6 VfI^w/ -&tNd}hz\Ų{~<_NBB4Z5̓C;sNgc\tH >\ ODx /N LVX >OPr>`o킏Y0l܍uT-Qn;]>M<_7h'GwJȞ0uA9 cެ[?_ B:lGմ[Y<^*5ĝU4"dZ3> A䪈gnA< z&jw$>ȃ/U2x cy~ >GPޛr\CMI׉'Uy L$K~^Y< *ڞ a::/ТRubHp,p"4KD Z+tS < ze^l bZ; zb#BZD -,0_zʇ ubg2Uns6UⲀ*aT$|z֣61M}+H,9Ѿ]hp&"U #_ |NҕL}`yQ-A^DnBHbsN-O%Q-HDz G0`iO]FG:3Kjv' șZtFXb6K]qw^p qÌU\ 0q@"ם`]*]Ä zNl$K/ -& CeC⩣v\k8p8{L(7YÎ}4hFbjOpJJ[ 74.De>F=> M=[؍~ C2?- ihGݹbPL6ϛ` u׸v`kó]y3F܀- -UR;L!;}wa~fp4b'-b?o3!{"^cفW, 6VI* qZDWT&m'>G vD[Ab;Lq#0?vKA/Ƴ`~S^!#@%p_'oS_B!y22^3fx4s"y! ,{A iu*Фy$ri`>W"&y 鎹-n[+fk2-St0eL}M׀[\HځRՀt-(/(W/A7Kc‷ΌF@x~AȆ] g,|>zi58v E#̡ P"=3:Q_gJscL5Lz𓏺,z~=z:Tu򶐠?a_}Y6H syL7U@=632d/ xZM\QU%è -UfH3?f?7&X>YI]xQ|לI } +69ץ^`z 3*ۤ("IvԨ:yNT:-.7!r['_NjQl">(nHЍ cB$]ɍ- e -y9kU8(7HT^qzPTf>r00!TںLLVGL:*gu 8]TB8 -e XR.Ac:rzyd%K @ -!/gee\xSk jqXHԮ B_&".K5z1e^(B[nP -$#;*&6 -n 7>{ b1@Aˑ*}Su%O`~Hq൱dYEPl/aȻ5, w8$|UǤ=Gȷ7'd| cDh! ˙9ì2WłUrͬFyK-' Pzu@k5I3JȂޣxT1)ݠoO@rx<}^Ƥ0 V-f -/Lǁ --Guk$|Ӫ@ -AED'oހ 22v^TI(X.n;9CmSn8o~ XP0sօڞ=@Ic4SUx,mT3/WN +# -ׅo/0Ux-$}I*l\|p#h׭F~ 2C_x 9637 V`1Չ^&u־v![Dy=?P,  Ɏ+fi/9MBw*(w7(-GvӮ((ЛZb Z&e6Nx9oasb WHSa%"eD'_r+ɕa"kA= ʇX'ݛ/Kw_I4PY9vUr3(W5~ < Gm&XTY!۷.ĆF?U- -@^(Qk*R㫔"-S<Ƿ2:]Ie[gl6Ǐ:JHÑ)%%m}=xp9`LCcEr~+]2tpꊉ íkMMD%4}qCh9BU*RtmK㡱O:YUfFor,x[_ғkcA6;a҆MU3J, IuJGP /nS١Jj589#3FCy]Go} - Rф8ЃBA;^zpݴ=EK0Q@rYQ] $fOd# <8:䮇%:V"5! Є _ƚQ8,mHPّ90kM&=c"Eޘ¬H/ĪaXYk -v \4h`(QM) .g+cM -[?$VBān9df&lf.I^gEgǙT%(Ӧ6O'tf퇒Y0K^%Cp=C'ea4?5R}5yi) B.A#FV7oiY -P\Sf;(X9$ @eT91HQR(`ۛ]8L7ꘀQF-nR$};Rrأ%:yA2:B PׄJ^Ph#j%{(Vy )F%z! 2+3Y4 SpdSLn k>1tGmaEWKb tjv +#o!uX}@╜, (SNAB3-JAHjq-k (j鶺q>  ;t&m۝#'?1CwHJ28d]p4C:tlf!,ϙCW~ NNHxneJ<"dz:c$ gi&toʑ+t\kxZ\w\X/30wH7֊Л*{p#PWJg<:-51alGa|ڇ/;Mͽ`I @] 'V3Gw$=( X=)W2)axڽB,QQm -JGm8HY0䁇 0aRzgģ͐Mѐ;$~")\Py#0d4@Zp5fZ]̈N`ϐ.gGքr+wMBN̾zxer>@F*YN5 -$dkK 砏e X)%?豳m[)^CMV|WR.)vB4 MK Lbea`|MG I} ¶(kOĎ\()h8( -ٛX -K8u)~h1<`u ٕz*ýYFW|glUɇMAϭshȒ&W%-S~`*7T4w저k!qz@$g5L8XruBA+˩DܒR(y&1a@=@ѤƆ 0M1rZtY1Z (^JF9YH(#ԝ'qbڑ"H*^Tԩp *'OO!@SA(AIIJt/){ؙi:u(z6=2ZBO"jOX$)ĝ <ձ*f زVW JBPEBO&k5Q::#+<U/Y YG^ (Zp; -@J Q@ l|U'Ss#(RizÏB,nj5 ϐ.@un?u@y§c?I]M,! -&ekߐͰvYpRmlSxN -;tV?ZLO8G;m VduG֟ʎ߆2@#t/j 5~{SH:ާLJ,j:ZhpA74~r\{GS 5/%Ov-iɮS,[L[Cg<_X@]ˮ X(sc Dl"ozDG >&J$Ukђ)B>Z f0f~{`&{ .#2}|t`5j=ox >v6 Jv (hc0?i *EJD`#u܅ج$SU)2(l { (",kݔz6"Ι3 1@]v{ -W2<8Pzd@,ç u}YSbyIqOEMQټҕsg%\=*JQ(̥73 gw"&S@6ҧWτD}(BO7Hݩ%=":@~L*IM9:@P -wH|q8 S>`*=ȫ໘O-1\DxfWR˴}7lDK ;Xۅy`'] -^oQ @k4O:Rڞɠ5,<O<"|ou3R4۟k!ֈ ֜&MݳēXI5G89Z$Dx|_{~aaJW2oy:pJ,5g5޻ND]"E9zh8(S]D((|L¾,+P -Ƈ~uad>ϙj)YSAoc$r}j'+ :ȃ(2f&P؄06Z0fε/fDR[N$NqJ`Q!@,VקL9_S4)h%Q^P:W4]%W}) AC@Y0Wy_YGD0 4fmo,F8_`q@)-WeUq:wJ*b2R!".R׏ -`~tJQr%D^΀M4{y~Z4 8,KtqD"z܊>SsKv\DU7Ƅ7L4;v{vsU<.,qۿ{XaP˳yO0CZh**te`Q0z=iY>c E8qp +_ԝ^jǬn>d)$ ~"K2Z?2Lu/Er wUv}#͹JdfpY՟b"6iK-a?}Aj8J0e'xZi-ގc2l/BZFad/YQ!Nݡ^daQփx+gY zV]W ն۠꼂/O -Ê]ڐU״W^~ЫL@#R5<3 -q>GAR:Ji --^O}Dz9ViŹX ZP0SX%YuK؁s- g]9նP@&ڢXCRH2aLH t?T\Z!p$|9TA4]A9kB" 0AhN"( R`3Z+Ӗǹ_{SHYahg,^IY?vWeJgMzX+4lHQ3PPM 8~T 4ni}g5Xa;Q)>.L-|` j@t$$j7;ëGCu2:¦FRhO3ʙ+p|%%8|!%ÙzjF 4W6 4, .e#YA29%o@rތY:̕q3l+C5D32-;&mlN°tMQ)Drdզ:qo-fJ91ي~nרZ0wve:2>4;VQW3@Q-9>Rbk N,QP/-Z;ZcO60ҸljbTHFoln-kTęAۋ^)ws2uӊ(VgTVi@ >h?5Պ|g]\Ϋ4bƗ۬(ue|y@b<^,9X7 )7p/?x2PF-6_EdĽLJm,B%1l(mRs;PeBQ |JIQriȢ}Ihkd -3Ⱦ g0_e1ڙC9cX>[Ό3`njyf;)Ym1`JR!njϸ3$asjjRE=:B0rRBW#VfX8[=Ryl)b`l"$=c0զg^.aULGz Чwie~ĺ'` cE{VPpZЮj"/F4p9hB#K^}8.E 9@>BkXa>4_!) ѵ0iIpPA6S&)DUtm<8!Z_` -`2aھC9XP ,26=dLnΏ96~Ҁ^D]riO3ascj)dfts 6!Zv%miO2oES?Q¹Sj$=西M+a%bi ރd#!uǧTS:Md0aGoaRp;y3\R9JoNVR9|ĝ x/h}vӾAS9'@`'o"9o$w~Y;UY3=_a}ΌC1f`O9j$À5cywx7ޫF 4MK*22#G5,= ;NuNLC{0M a;r.|CHm {bD?G x%i<{9cF:5մFYdfXh-Oi%V@V[ ~㗏*KZwRNR$4@ 枢o7Z,3EwAf3_9ڋ1;wKSbm9 y͉$sp -:޵Vn?א,-BzqGP0GwY>ȢksJ|Q,Ea - L~0D&\VCEIJC7وej]GW[R#_! rpc@ Nz;KY zk z&vb|BJn *Ca^yC({;lZcäzR)1N"n<խN|;ڠ Z2+q YyǨ`\A /ǻmA1}ht}QS0?Ct0(q>1zRf@ჼLtR*/3o/"uo犋&d(xJv< 'LtBF<Aa{wVq}#e@HԦ?H. y=_gtǛ&ph& k= 0oHGQU<P Ţ'Ck'Z4sG獈7DNwK$Df=I+pqXm -#L Fބ[B{Z?vt>gcWuvPЮQB - O &i N`2z+[w8YW^Ɍo"y(2AELrE'vk/R9?MhKDLɣPiV5Uiu?-  OB>}3pN)[j I o|.r`6#kR3r֚VN[ "@S YAÎ-:fpdcJ}vYQ׌+3MpUkGwЊڃrgo=4"`)@ [! ||z^ͷ\bա˗^+Rw{D1H]g$O͔@)f(x?3kdnz,Dֿ)+ c|7xGb?1uR2izxKͭ~p!tji0*0W?Pw, %"!*"':"^:wf`X0떔A91;?jDt¶(_y. Yid =F)ySgeף&.ݷb!/.$+;+tNY륑rD&]w~)Eb+ E6뱳7buOZB@osoց)k]kc<.ch3([!ؔϸۑv@%հUTb^Agl,DsǑ@%*zr^/Ebڂ6ghXݦ~I` ~$>H lCpISkm.6bՎ\) i~2 74B?ґ_]pFu?jegZ aqd/?r -CN X1fŘ.I" NYpjL$*RH -g\.(;:tp$~FD #R̺Za3~[}Zn "2ꅿ>qmxx%+Q<*Nӑesʾ6&yotr -ymw-^EpFۗ>#ջqx~k;Ώwͨu.D:+ oLxvGXglD zL3A4Y=BH0 c>""6p/3, &kqR8,}ܤL<垁h1|MF) |F;:V T:C VVx.UGw#ק5H^!X}@͈{( \GJy~> "{9w&A<?wh н|Lhd+ALo~RO*Jw po3|/࿛^-읐yt2Tmdzlq[6Ptw w~Mbr;DZuK&hpѣ -0Z]51> u-(潘 Y-wpaylG| ubFg;Ap1PwHG[9]n\fzݟFsHb!վmPgi6ql'8!cmhQ !^#^$^: O\^-a=MH|_cs8LoIxXw ֺkylWkO񯨎vb^.w5^WTܪj ~fmWJ$ܸB-RHɇX\ -)u3i"KY) VRz8)40w@ZA2+*'W EV5w^im-AcШAE" gv&_L3wJ@Pp->i{2U}k+a;֟(;hU0{W-z~#™m^2ߢnV  [i-ޕt,a 5ׅ$6>Ōzk-#mWDٞEi~`+U[A*H kbEv d-,Wz5&9Tz~Z~wrq`g!G-~Emy?YA'{[cì@~1kva(;˚єâJ:go3k10ew>tb\c_WT#MWGpk8=#˵ədk;w) -V>Zt&8o}5E g.)q M\yT@MBDJx"AHxP- hZ"CsCElwz⎿L&max D fWwaQU ^^wΙEvE58AhǕ`T{q2 ޕPoH! J 0тK&Jd wW#4Y STHMAwhvmM`+-hyjb=%*԰ NṪ՝=&T] ]Bdf߮jY#R+@*ݓM\@cuyG(OyWAPI*ZG|WPFGA%µ+<@.I){Y V-$]b<qD7jlu,Gֺŷ.>)37[:5]Y8+<$EPPeE7GR7]6ܥ rlEMCcOO^qޠ|ϫѯn+`(@'[SWUCTqǨvq"D6@oז"Z迢 B`k+J9Q5XqP/wH&ZN~y̠Ny=01K3]Z7F\|-x ᣸fz:g:_-Tș8 6@́ dvRkE3( r <@Q3ݹ {mjEO*ۢ LK){=ƳoAϡ=AEU@[k"=1nL 3*o/lZv+H}*TARq&EYu@{/z. @Ũ=4DYRk悕6QW^ -e;VoэXw9 hOK] VH'\Q+O袁w\)TMJJS;bocY2;)3l_d"Ct4~ k潗ae)YCU3GQȵ{{E#3H'V?Z=% PGcwv_I_豸p@./Xڝ=S^*A<"utt -7#0$n]zf} 3uq {JVPcSC=(mdE jDx&Ռb7p+'g-A,i*Ȩ3y\Eh)%fb9dʝ׃YfȎ_7[àbv(fex &\QR.-Pk0WI&Gͧ%slz l'Kwݼ6Je`%3hcou?aQoq9~G{aaуA򧢹\ 솠 Gv "/bnXQ֞[@ٗIq=y\QCkFrշҗwϿ t2%n!cjg c N(҂lȵ,FVXWF]P/#Hq܌\ l;pv@}5S)_ȐwAq܁=ݯ~g=˻uc* Y,J 10T(#R25 xd8,E:ndb?}j-3]a -hO ̇!E߫2 蕕 QY+[XS/dmi/?sS<$Q*Oاj{q9zNA70p J@ijxQg!7E%p}C3PR[ֲ~Po6Q"jZgm4KBQPU.m Xf`P1ATZ+5Tq)uw]yZ˼h,;'vm/-A 9vWҰvGlkh qIzp(=SLݑ åȠ?tT<=R5tn^Fum9b: JD -ovXG.k!lG -GmPo;ՊbWCF|n#m9VΓu. "SVy `_qozvFF0wG -u{[⾍M M[# -N@z?6YA%_M)눭6IpDi*%_bΛ7ą _9? 8G;yL;}³:x)CSW^_MBNozI?0"cB zo.`^[C fj˅2 |=DtHCTDe`1_=rdl(M^r~P̪0:TƞN'\B7u69+zCDH!yp};bɪc2Ԓ:˃"'yfD0kٮ]XН꼵֒igOBZ:$]k+'=KlkKjŦaK)_:RN e\bkLuk?[o:ǩ=kƊmtn2@ߩS(IVlMI¬|ns ,Ύ+4 '۶k#} E}P娎]^H"iIvw13umK @w.FEC'E`vzQ["b]1i9(zXYӱ0r1]!..逮z~&a0MWpT$#fg}to?|[Si"Zz& zϑc 8:*f: w 8ȅWcA0NL~*g0膇``ѳ,@Ϧ{(DS|$ _tr<7c ڎITҥ'td>\c81ѤZyqx؇yBkͭ^Uj݉HP_}>ؐߓr]#p}Y[%,N1Ifծ⊺ґ7N -k5 ET$.p,-( KeJndqP,F^gfAbMs6 6FP LH$5nC2>/ts=*aK]Ƅ(A@VĮ-4X3VxR$Rt) \6,Wo;,ǦHHB.̭^@`W>@iƸQ0Pr`5|\+H@!MܲR;~*CA$%2wld3l(H!?aznN4w#97GN*n<9ѾnKLɛY0uXjmtPQoBTf =cc|NYq֬:F8Mޚ -Ȅ 6@{:jP|Th۸yM*mg"̌HBNOE-`l9tƉ7EGD#: afgsp)C؝52qU&dql+ <0 嵋%z$mt=j>DW3 -GQQ(&s溸PAxƃ0 l: z}*UtAgDSg] A=Ӑ#,@TpX@b<$u >i7"~ -g|掬W:X+3A>s_0 M _jq!b'r˅ A\ "&hyPEa%[*'CkCo֠zDWGE{ s:&*3鷣ȣйEuSt`"-:_31‹$:xZL#n5nPl[Cpdk'KE8Kb,<$xH d5%[uR8Cop<èGÌ{Zamu6V1~3B2y&WuJv -؏Ȱ|9Szʷwnh!@ii >Q3Ʀ2{"e|Rn-nl_(PfQIPG?>S>9ߝɖY[@, h'~N60JSGwZSpU)aO^vtځ_ JyELH*(Y$X9j~ -"D 4 nzz ifM/p5B9"\^wF-}9SbErI7|DYbXqaRKU<[m"cme`˙"Hs;|V roXA6](Za:cpE-jXݶ*M,o-xDK/_ )' "`&7,"l0JlWߨjYй"3;h(}%tڐ=Hkrb(R SC(&W:g&Q?~b )hL4ouw7;]&G+XDl"2;ݟ(!mѠc5sGA;P:& [[f{E IDhsAz eq7l3egmeX` c#f\[ώE)<+(+b;B!@Jm UVLV4aU|ߵ<f\ɠ^Z7"ULm9ܕ粃Sǜ[5{B8D@mZ6瞩YP&nYcK2d!ɾdsdlWqeڋN xcTwmQ -h2&r<Ї -_ ly:R'o5t6A x6>U)^N<-Esu5PQFiװx= Z`puK@} x8q'ӺE~p^ }=M@)ur[aZ(<?{{p:q@] F$qi;?A[c9fF!O6> 5fIdM{ Yևd@9#BTt䃻n,$c6ZLJ[`E,2~cZ+;g#O|ӊ}X1)Eu*?p?|@X?x!H~hb\q ǹOXw]wS:K!$]]#M#j2Rr({~^o@{ak.u":Q{LpEvr[J i38ĕة8\u3sWh#C9Ek\JY2fVe˛"WȄ!cMRiO/nbtg۴?ʗNɅYIU~QMQ ߯;Y\y5rY۝8wd/"ݙHS,ʰ)uZu+(l9Z֐D3Pe긃?Z -&_}8aɨA: +d+G -klA8GjA D^:0ֹDōXbpkӠ麁RTP`noR&;ZgIZ7$4[x^%w;8Ҿ^6rh{=hgg*l:~#jg}73jǯ6̂:Sk{t!h -6aB.2}l Q0~@uSl:>I69z/ e@Mml -׉&I@Hz~(&˴bV+I!)q|fx -=~Ѣ< X|L)'P}3-.?Ptyz8:-aխ^pESk~Z -)dP $H!nB 3D*;B*1CРe vMUIyiCuf4 m̅J\vQ48fx^7¥@M(lz֍iHωaIg3TM;O=]be[q37E͍y\a5w̢qlNU}Ff\D[[ '8^~mߦ랚3xv\(!M\zu֥oՌB"ר2ymz}{S\Ng4 -+ -k}Ȃ,a>^uq_ mtn=d猓ḺmU~,i - -;RZX~ -–{QRbu +-J+nnX˓>ĊzP"Ƭ-QgM$4)9 -z̸Kt&SGzEmn-cCTH9[iLOtAYP2=z 4ƞWw_A’TcuwӲo= /d:@姻sBy)r|T-Bd3O @z`-cAU-[v>آu]D`K@OoNh?_V5%`p]}|o{_sɎQ;ע5SCc}]^fKl`[}*ܪӈxnÌRw/&[pW~T.sU̯'OK zdqy͢yxŽ|E{dӊ$Ms;PĸN)ʠ;emS UBհR؜表=m9a)4OKB/smf[4qCsr5=պy@i^??aVbĻrIq>8H牌(w׀26"5hqO/襳( $ -CKby๋#Qo('_G@Sq/&ڂ&?xύrR|cv#+asǔ -;*#oi{G!O/\^R1/鱀,A\Du7A@m`|1я -DE{ۖ^hl -^[5.'dhwe쌺oߔ^0*z+c烥cX4MoCa]|;nQ4Ĕ8oC5't3-$V?]9ܹ))Au;h5K~\:5 H& -촒B?F!Q*tx6m)A#AۦB,7c f9v#?q!TA5[4s6k\.PXl6+A8}O}ی} f/ԗWo"ʈș7UssV;S$^ h4E8t(F臵낹Sni.>1rL?<Ôfnor \!(ap$!3l$gi5MȝTI~;>&L8R!bgh 9QxȺ@5ᣟR' -#~;뙡5d[Qρ=( 0Mß!+MU]+z&Gj]zS3E ?4b?d35é_xQAoP PYZkVx}o8$ȟnjX02t&jKIwݲ xZ۷l#֜`@K6d{DQddBQJ9]:뾿qZn!eT7b[DH}EEK VwoYxm[j \v@gXS&tON[*v%T+jN&Jx%Hz5WHNϼǵ1eATIYzEڂX@@sIaoIrv8pHj0O[_PK]񤨒4xkvU_6A#_cxUi4?Y)= ضzU@݄/w&QH~؞F!]W]6Pzu3|[:ndM-oEbe>&@t=zT\YyOFr9-pZB#.D!׷5G+|7y[ 톰U5N-oaE|p>\Z9 -Y4tDk{8^Yy9Mp&C>9aW䉋?E(a.M/\2H7.~yD :,Ggk`NpeDIv;8 5. GKnAԷCq -{ iAx θB#͸ޏwR&t75E"SG-b2"*"~o̷`vr^Haſ}Y'Xƈ30N܍yn~f7ةY^oৱ*-u`X`UECqjEBieQzd>#9a=D@ Fʷh.zG QT 7ti[OfRP76`l~+Q^o"d>_fwޣI-H8=l¡>^ic\i+1}.vO;m$ZpBxK`5A[y~R.j-Fd2tfg`fD{σ멌'6n[0Z^RVWd6l<`ɞGk -qW ֑Bw? NWhgr_eHghD8P=_u/sg$&op?wCP)<7tfgQYŌNժE <02CS}3öò#(ooE2`N1X7rx❰ymaye|iV``hzcЭ r-)k}@JߜmruIߙqмtFA!ƚo ki2y&^sZ 52<a@DQaz GO7? *B` -@磔azҥ vr#ыaok]sWPp  o5~+kUK}`%G7$!o*ylصҬe uۅCUz-=llua."D,x,bI:ѱV5F4.{ kG}Y!qey_'ZK!'Bn:L`_ |^L00Zᓘ <Ok~#F==zTx6(w6[uڛz]CN%S+*-@}11d <_jZ?bfSAR;\a埣YP}X3wf\UC@sHk^A+kۧof 69-2hs͘E`$UsP3+:ӏ*?4bzϟ{7VY0l>!>"jQp+ܝHv[R*ILGʊ>xZQN踠Vh>z̄ \ R_'Fd Y?'3F;[I=\x5}?SyOAz|HC |՝6pQj{~sxeX{*uRBd`UvC qn}<~<:iT fg(-Up6pp&aR^"zC(:2C.VHZN"Mu:Dwk6鲽n nڨ];Q,ߐ|ε@fa' S#v?vs13&-=0UI_jbg_\3&Lp*og3 5v: %PW;/ˣ3Ø3֧XW]+uLbdsK`]TCmۜ_}~ޏMCHaBc6{ր@{V<I}qblkV_.E -/]~O7E]5<14RR Q0n7$֋;yYM(&㧳GNƃue kP=9Ch=:kW_GB $Xlas ћ9ݸ9BN≅޷ -h]ty;WPBE^7Z1b!cwD]8+-S;@fztDNCYMQらR &VDum$/TJlYyZ'v -^Ҽ)ٳR#5+wf꫇otLU>;l}B6# yp q8v-8QS iܬf(=.0K٤rn21f_3rug殳ſR 8nFYꏮg%XS0Ϝ`a=ʾ~`c2׉JxHIeK8X'-Չ yKl"W׎d +" O"=dݴ&E -~O 3ͺCPjڣ%1S;CR;:qmF)!Fv PV} -r3>A, -h@60A/TP>!<EC.#Vl0`Yuq]n[ .R'z>_'63qZN4Gꈞg{mHҟ&լ!cҳ  i*GZPWљI)x;-P[Y4֬fq -%{x5jsDK2A>ۚXj,8ܸHG[ܲkǸWwe gAvVy -:nj\iY_2Ë yI̬I=`)t1=ʱǙl@rrm˒m`K} E3EN鷘߻sHxӓ hnٶ yy7:n_FP)]aO57Jrm ]Hafd=LP0VM/Hc W5Cy u|hbL/t9Ik!HQ6LJmׇ=EwL>SImGA]ŵ ku4MdaM7W4\楰| :8yL7l-pטtl-9Rs  xLj~Q{D{gыqta3VaT=7{c-Kg[̶0G|W~{H -w_=91nL%bC8طJ7ցJ))o3]ѫqIAr Oc^eqЊAzrϺAD2 ۧ7zØt5jHN" \VS1#1Y\|Iq3*Sb0Hy;欩j/}F+9~ÏavT- +=H:f7~4WJWG(?tE~`Œ3mCH<B,XU -+>D]]C^m2Qȡ[vQŌvc@ꠠPv=p.f!F?y"կ's(q6g-2vuC Zl=uxd^޺DjE{ -N5F]r&U`NwҎ`]XRpCZKNfrgzg8h罼4" 3z|qb641d y׼vBڳ| F̷a›Ȥ#;ϗ\v6z}E'&uov`XJQ-#F<ͪ#Qh+ap#Z=iԐe8ɴ'<(eķA -8CQȅ'_L[}g(#0~_. rhOJ"q>u䜍`Eoqw:(q;MLʙS^CMǮwCRTt9`3AMkwA8ڈgͧQnm#MG(ؽr5Ү -֯Ht~tUc'8H+ڳPywQ&@ixWo#nB=1<{IQ'}Ef s$vBJ"W0oĊ:?˳ '>#oEf!qѴЏ:(2Ƽ"B)u,E%yH)ƕAX`Gf:G*Dh(\Is Է(`ã*ғ;YXӡQZ|Du -$o+9Y%ohFD'v}Cb3ף LqpE5(S7Nm10 EW̿MZiNbeȂٹV'PV$׾~w+Wu|ۧSwYy&,iW3'@JÀd?-K,يc&!wi}ʼ Lh/4 -IˁQ7)svi(=oX+Ҡitol$% V`fǸbph E#K8| %uXf@x0 ;(NE_%#ٹTPAg2B3?gIߘheqի(u"0 LҌߑ0I[yf:ɒ(5RCT<Aܝ8󹖂q{EL4f7$Đ?R ;1M<$헪z{#;vvfHeɌ?ښY e psq^'@L֝NgZ 0Yg,~P@O󛾿Lqja`ylOH[vvM%`}D=uBǦM,߯R>sFsah\ yT*g(-.xFﳤ4aWEAk5cnTUIj}@J{emp }ϮK1.lM[/!дv5N3~Ղ⹢O>3*0>>.R^{c(f HϙmZmӷK'4ډÒ .p JK*Q,DHëapW-+[w'%kogܺf+K,=2gMt _$Z?Y3XPY|T'b`&ى\ANsz"OTGڝ9>`ʯ B1+iTdÙ;A"sf`}E@~YcT0^1@i*DwP㠇Ҏ -|z,)7c|u)^l `:Pȩڇ?~uT.0W2ju@Hmjl_q4`HClH6xe4obruD@U|w=4 .j3C -=sL -'JCWH ϙ,1rbl -ۡ|o!>jJ{z.[>Y7}?0սΆ}u :gvl ꓴߎ`4o+.3=+ʀ|glX.M ΘQ>7u̓TQ"N.;V; i1(.WzLmˏ<C<1w+ -!Q5y;Cdޓ{N"C>c,,/.?BfQ7g`i, e tގ-ml,3SHww5ܧ)mt']1Ǟ׌]#յ6i•3 iϙ^T'_J*  Wd C%3(֟afQ5&C-(g%d J,W9Y' pkYnjXAлw" ~Do3rF:Q?T{i}&Rc{>y bNke;6a8˵Awj- -?25+DY n.#[၀)v;E{}?v(lg|I Q"w,6*?00j 9%Imh"PBHx/MGˌ̳jFCeތx62#w-;C1"kdB* -cH)጑׉wd{xXSjѭN̰: }ܫ ̠f̧ -RNO<:B5|\XKk]q(`XA9L|uEeImBCٲoӆ -;K!H2!$`Dddf9!apRid!Nƻ a\IaHqʋ},MIKri8QR>re[IҕB׌sܶ*bRtfF!R\˕8T5[eVxγnjmAI%B/BYg["ȟv?`3͡c Uif)MK8pGb]6~1/́ز  -dmi ;U V*Td$Z:srTyx]xpeT"Pk q%Ǥt}jƠVP/DքYT7}; $G/~ > p|4%Þ,Y4@N1M|.@Y>[tCqh5c =LO8%H ڶ NiCP$[ -f -q*0ֳ5:Dي5qAv` qCwIQvO+jJa Ѓ"a k N!f\k GbZ$Pmr˨4\iw|le"9WZ9VCha$ wICm}J@V |֮_O+ {k*ewEfQlfG^MOEh"O_:g»y 5b:{:>NN%0wnu\.W,wӎc}\E~uKU!N_Pڛ8Gؐk\x:fqܴ, ҊE56xfU tx&z#=xp1<(24n'8(DۃR:6N` ~]2U!G`.HXgnJ _⥺OxQ֦TZI[P^Ozkc$J"tNY1Mm)&͑H ޮHsҍ9{B_=l@Q#T+u15v\ͦ.=W~3d_<h7l|@TDw0XFTP{Q;ҁ:Dwu?!N 0kcJhN ykb>_PzhdN(ĚQ#wDv3hR 򀫴( SslRJ`~o\IJ#~hUyU7,ڀ)I !>q@ 3~̘qH/lo!753qrh{ o~A)Tac;3,ݭ@L:lu1k`@,vv3][vC_+x@}HUSK/ $R0D+3*H@˙Pz̍Ur"[1rn~d@PUwd'GO4X,cDjJk9 KxmH]S#TM]wH -T-)%:ݦiId? ڛ|K*>'VƖը - Z|4ٯ@%$`6NMYP1 ґY&Nӳ2YKs 7,Rd&Շ4 6x#}v^<^rCu{%~ڜ#sv{ h X+4:YH]6^pUW.,ps\ hT՜P];HĞa5'㰧 -n%s=~ e@4!d - r!mn +ӏ];&WjVn\$*:#xB)SuHVl&T֭y<]PRlۦwT!Y!-q{s^_(J%B@ 2Q^:p ǢoUCevhtf'H!-أq`h)$,ޞ2&]Ctew=d$Ɉ{H(ǀ9%ϔ uЬG#32kc+˖"^= @'M?8Ф4i1-#οSy :_6MXb.zG X}L_w~̇o.H]+1ž՝KK]X߯S-浵<.Vk!43,_QPg/'5(>SHaV xYkn,~RqcR<!fU(/0܄[H/#>ۚ3؈1U噰`h-LN`>UCt~)cqP]j7kzEona.%]=~R[:pe Ӧ`=qZy^pc$#*JRܷpNm\cWF^H$ҷ:Y<ڇQ?|qΌh(]QVS&/@g YڻК&ҶVC{@șC H/nPeCԶؿRaU?9M"eMXDMVܟq]m˗_.iQs -k͵ -ٽ7Ӵ Z<'vhK5qCn&ڠglMBFuMtBeke?<Ϳ؜'D/^HZz:Zpg'ԥS6WYi\wsVh+q{PTW@>~뱙~ -Lcsnr".d۟BcR!SLJmv E,uzLrbw -VQB hïb?ǂކe˃ZBm!u\犩[BA/HZ.RZˀj)m۴V0ym6Y:A3lgykּ+Y9UM2!ܻ٫2Hd[M:_fh FFy*%=Ōr֭mG&Oѧ#BK(mJRKj=CS/?"TQ@݅uB.Ho. -՚"NמGN 5|$-wȝΎ 90'\HZ{ -_iuXo(WR R~D ȕnƵ^r#(T"fwԣIG(vo(u˅JU٨GVT2X5X VMCW>ga`m'T&P*'% 4Aـp˻q"[Wyah|7=f/8àlV [ϛ(3?Z3beH8G 72*XD52xW -c5Xk^v{q@] -<}ԍ;4З .3g~f7٘EoYU€v#7 4TB{XmR;CZ.YXPpZi@մ|`^ieRD1VYLܳWѵc4uFvVT.#gntg=4Z_{\6C!)U(PrRrX?q}d+0~燄w-"I#z82,Յnrb36]|u^\E;Jlېڝ.^3}mz%=ϻ:t|va$I#@d" wz8sL!%H7IAۼ4c8GpN.9"me+&~-"IW7. vj%rã$GkKD8BZhBޡxcގ|@ÚXW%vnjU"kNul +8}9 "~N#&f̘z%\>IwЄ'u*<3e\|nj)>EWz()6{:/|D3WEa5i`u+`z&?DQh@#)X \4^*8-O"MI4.'Aw*2gJCYiqL^Y,x4Gc)ȗ%j .yafZni$Tlj]}ї~*_X00@9KȽFß# j? MSqhLfGv9ql|۟[f8?La73z(' ,VyLm@<9Ĝ(MQwTqntha=ˮ7Xao~, ަYq5^ {s6"_yx0WG:н"F|}Bx&QJHۢe4t@vv $ƈ [Ȅ)ȇkrQ(I5ύxi5c掸\y4c$$A rѭZ8SZ8![ -A)Of75A@Cj2oV&O|*LI,PIrL ī9>FBߌ-u,Bz(~W"@%lٵ0T"6+ *g |Gȯ™TՑFؚzÅy Q脌er>6k 5|Dk/ jAS3W;, w{:nU fvk-ހ9h} zH9{J -"h} }K*>Zh2k{g1=%%szu)G;SY;s=. B6Ʀ;$}-jWzD: - -H轾 eg/Be\qH\X{h/jFiZ 3S3휈+!͉n, $x9bgKʃ%qDApz2ɿT8rwہ#Z,\D@nޔY}pcR!o:X)&+}hV=J>?A;\Եc}0-Tw'ΒUʏ-ƫEH-%'1;++uC}'*N o\F0!B,͎ bC5ɷi>+S8}o`*MJv>* ~Wɹ .ݖWGA qPFEaYT]֕N!{OCzC\v/>^S,b;4`~Eg*bJdp Nmj`jyrILquP*`Jx! -!9my1f;&z#S QF=dZmC)ɰLxk7悛ʊ i3Ď)/~3BZdjy)үIr%nPצL5uDd*tBxN'U'i+9ZD#)TJz9<6VBW>y2 ,G1}͔ -G`)~0" q*S{2x IdxH\ _Fo]N^ևsVdBzwJ݇hap}+y"']'IxT_j*HwGXW,ԸQȊ_C,di+=MA3&]XL $3Nr48U32fpAc%k4Q2]KPߏ?0@)@joh%̝=hjR6ca -7rfDZk!Ӫb!2rO:b5zM}NƎᑖ刧QԸ= f9z\Q Е&f1^ -gڛOlۡ7r$[7n=E72m -HT+&9LhE!=̽J^hRaiՍ -$Uуn?E 1ryxmM*|>W [WA{C`/4^aIjEYDY!m|y,^efA B &}\XD MӮ?̉ l!jFd_O !N.m:ngC{[ Nl˄߿ܟ`xI&?)J|#dE@C?D~DL0~= {B 1[9HAP@mQ;@ d +4UXQg7l~ *|p_^'>Dlt*S͟1` 2W N#,(6d:J?h7 <1PQ qLwD㨒` #Mpw7h^ Pu:CA -AA~p̓k6d,]7p2S^^`Lt{?!|p`:‡%/}x+)g^YP]@cVd2ye5q~;ޝBK~A&@=b#4|)ٹ<uWEDV9*"lmDR_Tab0x q/>I2 ]o>f*;C P-B]#$E]@H=_7:`-|@rLpQ5├ZKz[P>0*,5KHO+i\\dy,Gę bL}:ʁ̘ڌ+P.u+J]J -?Һ!0*"l7#eAtq\_30$F"6擉dn@ V"RK ,k62jTC_7**:ƯһjK QXh˨ꋜai4fKҮ*NTam K3:_:QH"9H:KAaYI9caF`?Gqs?)ի]ZLT~_Jew E5 ϿFi)+PFD[F]6RR Vui0 MH[X%787FTXT99CC#.h1n`TE -f@?v(Oe֑/ܩ^x }D"z2#vGNW^ -9} ˈs@?pat LU R.Esa=ncLA\hEHySɋO8 -Ő6 7xQJݔqWXD`k~0j:|k,CiWyvhu)SR 2DWGb:mD #"vb;H~rWmVwnpY}KS4~Gїk+R8 7Ζs-o#r6Q꺈IA$J Wʏ> q~DfNub8Ч7TZZh&R'':d'--EuhtK߮{l2W}?0D5j#Q~=N!t֙j?J}by͟DxYd^`G/28([q{ߣ it~a:DMIzE:uF\S1RR\'N>>Gg&Vx_Amy9$%E h( vI6^ӾznoPܠ~x>Ȇxeٓn7zB$l ɄeAWG#ݧGB%ԳQyIl?\Ҩ㘺 FnP @8cU^v$_V^>Q֖sdi\EfM՛?í5""D_ xz0$8M)Oi-\XyFd>5p'"m"*@Cܣ.t fV 7ßˎ,QQR48r8t~ ؑkK[rQ åL3F{#>#O(]NK.AFAh(?9!it1V<_u/i\#_}In#qqXF`(N<:\E]hH!nU4eNM!}& E- ٥ :uhkjFzZGV!H<R\PV# (Yy#HI~v_~t): #zz% [Fg5^`.[Ebի>¤bљ>uY.`ao#,fT:p,]A E$ irb(ABE.s`οuoh?' Z _sdc_~/x#6 ?9WǑkkfU:$&3͸^(, \2FtD(sM0O}"F~8xY AtDl? bo2"kʈ/hHopN+QBDYcӷfV8"Ș^_-?+d+Dp=V0W֍PeҬp3xhg,) ;`< =yipJU+~Gl{13CI)jK]UR n s+u|EeGho|D䙥{IJJt\c*vN>k%t0ݚuuޟ=e9\'L=Hm 2w8/뇀x`W#a!V2CRJ80yT4gD~/q6A \1dЂsiz\:=hXXoψ|A|LVq`UJj}BDb* D41ݯ= B`huE5h}D*8+ g`[(~i_u^MBԏ~#[8TXŻ` -?կP NB#_屐 '_%*Olgf{Kf%:Rޞ*0s &y1}N EQOĪ_釀FH>f0alEUoЪՋ3k -Fufխps !Dj|w9"saqs7ڛc1M!3۽#F@G)W44i_gn3lN -xsꛛN-zQnW*uy3 8cбݏc+ch1#LrG|:;Üo舴(栐L0Q\}m`TTZ'J^[dڟ}!{x̙,]5ZaNå]}0@xWEoeMS}ψ}vzǹefR2\E3m{OӞx~pOVs'-Ԡ͕!N٘~?Ӎ3sPTz\*18x-Eң(56N4@V@'jiI{կDm;ޟ K"J=5OH|>"yfxj 7%c`v\ {&)̥>Y{NP@:NՎ{W<ɝ f*_Gi {c_{,<4(c׎~D@pkq dBqniLiș 2DګcpA0!ކGz^PN%6ߣʤ47# \II -XG=# n/6l%Nė;/T(P'Oڑ[;&GLxW|api;X%j$sKD3 -JKdS6@V8|+Ќ-UWW ֒d[]Zl -1VXDٰkH2Ր_#F(?zZTvISL>fG`mG&Hw%p`@]Ct?ǘEE]>-X)~8 q`i#mV1Z#U`:ٳ\8Dre u;*F]C., *q>6$+PLY]C2_]2$G \f"SZ쯨g[Rɇ"J>URhz%.|E}o3tکη/g*RPN]>CSA E+.@f:>=άo`m8Dњj@% -Oq4ȟj0O" k=*3+v@[9N؅;fntS!vpBqapvR ayh-rX?ɢ- - ~%8)^9ӱBe,ȃbRuJ -)= -e"އT_1^Q~8N?HbuKe_ˏ+Zx琪CW2LvCr;=\5[Iߙa u 0ep>v h>[0z_a -QHcrhu8)o(9\ -ҎUퟨ,HZG=귶?Ezq{xѣߢ1U텄Ӓ Ł -Dbșj=TLF?snZS>J9Tq'ڿ˹w_d2{O(ϕ&QGSwuKY z3=8crQCP^EqMPEF,KSX,U":F]PK%wKaMxNiZߣJACGYBA#i KԦ+I V(\..F葻Z46j p[|D{,izϡ#߈.?LztIR_/Zì/O6WkZԪ4ns$]B*w[e>+ *#^\R >Dðw{HO.UG0|DWHSΘiHQr~UJbQ5!Oܪ-p_qϓ9³2Ow䕒;h/ԙ~(zZ:Pc_39bU^pa3!ѽcJDܮi$FD1h7LK@;ZHH;UsiCaT`@ ~Ls^&̆hQT~;i ub/[ -UuG` xzLuS,Yи`^^t22i2r`80t~wMD,L8ReF{/8we=ڵ^$6=RGcP!V2;,X'Blj9,2xÌ Lϕt}6j8%P\dV`HmņH,ࡥ 0>J̏"0т&8q3C ڑX-MyMDKkq'{&]1B#m"pr ڃuj*Z/ WkE)Zl0K+V<^JSTHD\2g_'~"]SCg83S#@@ʜVC9,ɸBΎFEWn:][X<qy{ G1 \"K$~_#]d&&2.A0Ɲ]'E x~4Ɵ.'CĢ\0ɋz{[ɟ!4] -Wݝ9,D̃Pv1 zKrҤ8\AۋyM尀-.ԝH;}%< Me݀t90;ws&"$*wMKKto5( n@!OV)|N s:ׂ-Bsǿ,Ker@8e UI|m|/S,$OXH`$Mgtbkz9Tc -7,W*pX6N 3KrBtYXX`dQ`sAIDžt=5Y!8OEIn/y8kXj% %ypPDl_A~ S23jMXm*BԴ^`(l^2^r͆1x<^\IuHi_mG80p k'upUK< j2tu(uYh>;'BLe)' -ڼC9b>UP2~ a[Z1=U:ه.i*7 ٥?Q(d~4q08sLV SKO(Z0)Q0ARȈ!y"Ȯ/,[ e,chș֙:^u -#4վA\D@ 0 kM;/##EfJ* 9pbZ̯QDt3gsZy{%l&Md4F~ @8P$4{w=QLS0gvs\ , T?YI*ڗSگ,N{[]+69_⇟ `$8K-1Y -5FxGNcfg;<73}DՃg ^ - G<8t#)fi}w?%I)+aOIתl#{3>?S?:s!pu3`3a5i*݊ DuxD钀-JKz OFQyhYte-UOɧtx,B?JIgw^3Z/!ٜP}F=)Ҽ\_]~"n`jD]}Fdk;UqK ux{pNid@?`1KCۉI\J$kQִW#J /xWuȤy c[=ڗ  ЂDrD #ÒD)#r;!S /- }+Lɐ?W+tID`مAzO -hS\`N8JH5ozpYg󍫺OggdK FPUjanŽ=0Bd=yUdt=Wج-s3nrrBK]u3? Rz#oRk15c>135瓦2 ~rc)ۥ: -D ڞu.+?qr}úۜQ@HQ}pge/i(u2Z) -΅k:B캎%&=bbW̋P^؀%Ezu/I,y;B—b\KiŃa9sC ˭|d(m3Mk;亱-&J[;L!51ҟ.@0 ={$?G@lU*4w:=LΏGYЯLO\RhڜW5~diM= --6޴&^|6<| Yc(}^! eMy, &o+uXSjί"qA˓5cY$yk1re0_n<}I|Xվ8Hz33Hжx~{ 3c\Zd6$#gbwp sw9㏎d/#XG)8I\Qs^ngb3xh7DF5*#ʑgrxȂWsz:E)p[D?Ų '~#h/U6r{ӹ4v +6"ߩtc'*a}U*9fSaD9._"u#XZd_.`b#IBmxYVӐ5Pg'mNƓ@ޗ!͕J -w\#̛\diψ}k1Q@8Jvbm>U3 S -D]Hmh EM˥*:Npv?}a"<62}ǻ;kn! ?r0ŏܝAcd3YN#jZ7I|a: endstream endobj 392 0 obj <>stream - [^-::-ɸw~}OZo~Z-2"T?O4fM"yyS]ćl[|

ʴ[|`'N2ǒ0i<;M`$s&/"E!(ނ'[(zlxX gxz尖>5 B}}qk.*:uň#!.'K-g sY8t/ 8~`zOv,GkVTQs|~s$>2s>y3q&gVB -`8 j^O3*LP)V,6[fC{T;U5PT1䂻ָk5XY/x3 -#hr0>QṕmcYbU*(gwf -桽 9>sx.3 XTǸ"rb|T&6''Dڧd$]@2 u2յ -oj?̈́֞chL):QEGHz=5fcќ.\- 7:U{N~t~pZsjԍ@XDA&JϖD1IfFIb~_Ղa~|TƔ=\yV C3KwNĔ|zBynӯuD?,֛,rXzWߣ؅g?ψa(|#DtNpmAe![T$8>=zzUH<ϟPhqQH3/hb]NPGr[FUN[h_}p@D9aCh#2xFk9𹂼9R0Kp ǩZ1/zQ ™!Br[ MIů裋uFO7a(ģ'@Qu3bp=82@5 G|ߣ8Bd/x .דּ3o9 T ja[~A?m''%m? - >Sýq-np6O޶#z{>980>EafHzH,.M@ p(u8\Be-F~LL jMCBZ]tG1VD;;U3=/L*AN$`ļe2!.w\FQ)+,h\zP.,jr3fu摦ߞLqF BʾCMǂ`-xj)fNX;¬'s f5H΄73?Mwh=$~ě~ (G#H]2 eK~dqWǓM ~$@)V'qFè;.+I/a0ՒVxEkH ޝ0_OI,nL6oR4g!2(U]SY.4Nb^9=-? jlZU PzcC 5\d|pF^t= -}ޝ K` -UNqt]+3^+ ]$0 ,N# -DŨy4 -No08a{!""k M۶f(.ZEa4䊿dl/^k}D\}~s5H & -(fRQ;z҇dtr΁_/p0T Q}"B;»iHTu2)ܳOڕ_ݩ?WK@{5>gف@iZR`nOr8n{uvde}M+[ʏ 0In޾BiO?GT]˶K1-i$Fk4+IBkw9~?/_3FG^zp7ɓ0~ÙfS ~Z..3 oA, 5}HBCCψ rfXQ5G+.?D{a&ju~S,ȮĬ9V*'W N -e5X -yZߡJ( -GNQ< d䨷ag9 ADŚk -E)1D5L0B}R<ޱ K(e]=e0Zc̴b(ע:M40P%*.j;0֪2V =YR;A Mbw*י``ߠpgS_1tMbęmh-^ RCO)F(#e`1prŨO9 TY[;&Q,F 'XAT>:}9S (z-1/^9LRTt P>+Î_ jh)Wgwp@ƿKD@< t{X3Y\r-䣝/< $6Gk3NCB cs卺G80P~stkT95bVbɏ{)r\Xn3G~#Bb\~pgף BM0ÒGӊ u݅[žA7iF,QR2aC큲W%«l=`O QK(H@v(8~FDxr*1w_-gِݥmP;O1>] -<QK{ϛ,=b!< 7ݨ@m\ة -GW*{ėlEިmEk#Wb%[ae`OvBNU/c_53iD8Ѵ!l?غzXOa2HBU&zMYY^0ikja?\JM_e.pYJ[# -j4t.̂jP0&^OuEcO0Ju 'T_;ZUZZhx#r>y/5n% lފ ,=8M8 )DaSAyM{K|u} {6t#@^2xRmv>50ATxv[L/ :-;(P&ԆtX!=K7̳*ߨ0s|ʌ FI=I銆xOtiQIIBZg{4ؑJk?g#ope"m(kG J;&9ʜmpFbo_dW"dQOGukƳ9_Pz&t4v;_0傘cc@˳K=0vZV{Ya`oed[822h.x1CkVH{0wm~2Wdņj?@rY;oq;RG*;Ī1T b'hyf(89(ejYǗ;RF ;t憖>:[|ZAR.9wf,G%qHtQ)Higo-T~W]tZ| aˮ:u7dot%.vE{O -Bzi[E4hn LE),0C>uZR޼k:Zc OϸOt826*% QГ Pjs-*4Is݂#!N,&i-~q?t$B橦.ѡIF`j'Ab'T'}T_-t~h'taDź#%vOF1N[C*!"cP}G/#h}{ HavL]?u9LK)}:z}r諟no]'%L-I(#W4]]ʼ­i6@KCKz7wD/ӑUy,З:6z%ʪg8*A}-S9zkʠ[q}OϠ~uU_OtA9Gbdw)"岵#d2X>2}q*YӺ~y\_ 0}!Ei94!m? X>wK1Jv=y.̬ "аmBef="£cJhv s:&hbgK:2Af=tL :N&#;.L!gTEsVՕGj93p^ Zb#d}x'nipN&6tM63@ԞnXСۍ 0c;Yz=O/(;zl-9[L5P h'%ܘ$qFk}U:\%6xPȒ1~[CYwa[j(Ere3O峎̭X=7_ -m$<^r$3ʭa %%"i+؎{&eqLүc/,ȪYs*4:" "VdbxK d՜)bއwɞ(|BX-f?JV{ .^lfx :Wݜvu[# ]~5zis#nӘΜ:Z鮤>7r"avf3]KZq3_UlRDgA(z$Y_RRt_,/<&M(TFMQ괋mj$YtTW~=#(Pv`ݵ5' J휥*ʹBFgy900$0"x齣^т>n߸i{Bj¾S>DMo@֣Bg\(QD- )\iϭT"Ly2_'z"+S^PD$Kiº: "tv<^WɆÏA/Ju@_H㞝D~BuWK99;2ԒN!˕ãѾԮ -Ki;vU9(&JfbeH)WPC.U%cvc%J#jr6dll|Z2Y{KYCX\g(Pg7Ф(h{)ނ]SCEfP.5(TH;V|F؈XP ʚ5]Og*~EI#1$(3+Ziqtg -aISAZt s#(/P_SyPOsm(o+c`\ -)8vZIZQ/hoV%_ZVDli2b4JWњV_= -hJJ^YD\ϯ}! K#yXUET(\a6˙֝d: zYCy0+@8O0@h]3w5yY9_KD pa= ʣVl^UڨA>˜p -kୖɨ%w'Վq+I])*Ȇ4l1l HC9] -ڹ,cԴCS I4}|̍F6v+FXH,e@0o`pJeNS?-케/F u6BQm~ 2T@w}kS9"gIRɘC]H뭰쬩HCQ)63}vދ^bL -@vmFB[9{SO׫ogȓdN/ lO|g{"bFN&0[DQplg} Tp޶©Z6,BD)[B+]$oKeDaC|&QiH[u65z`񯚓!>8r491qkSDswv.$a5$44yPOۂwvtrI&ۙWQ.L 7L4z9e2fҢ>(|9圢wGG&C}$=SXd~$_~;I¾QۍUF{Y&w?&s42Ts=P/5݌o1ߝ'6W.ȪCU<5@bj Mz-^TA^<[*l8qKRA)hPzN+C>}Xg% ƪ9P; I cpi30r)sO'%ie0˛JiFO:TvwHWJj-Dچ<.aUui冲>Wo!|fkc-($l0 0/~#1jug3] ;h)(l | F P>_ -뚯dA[Wk xϙ;6x[?|DzKs)Ӡԭ0Bx?xvK:Ұ'9PB}P;o9, 8E^>E"X}䞼U%^sq7D GW9[56"]pJQɘZ X6pV-ss;.Ӿ/;tn -RΐƷ(YF0v3(*~ *5*BJ*^u4-yY F]NWzHLtWgNQ*ϳ"Pb˘'4n!/d׶|'eaP 2aM:`Rk@os.j`~t<&%%H?絢Ψ cc"FNթOv078\v Cpgld >.4N~22S[,zL]5*q[|7.t>m{dC0VLyB-tlDs xtw<)kC͈>0:L.ç5(Li|jq|KKzL\cv+#Ή&/@.DY$䗽bK.7 H  ih#rD\KwW|1m=GNs`A.j0f= _Z :dP5Cy"j Sтs-ck{Jn&=W?<ЄVe-9b<(Ek -e[c[#ԭOkN7߄ ;e:LЅade9 -J60Za?XQпu:0,#K - h\$qW$p7Sr,]:WM. ,4}T0T.8ꦾ/H]k#"󞠓Ux]grP ,A6f%^ժ+} 6%2)u C1{ Fg`/I 5ɹC>OZΘNqhYWt\3%IUN$p^NYaxv}i)8^bӴ Df:;5~Sx[ Uczqy=*(ߊ4^qd^ȯo?yCFI"뉾o`[MQlO@q'TnD 0+X9~LݮRt(tO* `3t1~IDg\3^e -LaP f Jo% %x=}?lTK ubO-BB -)vNjfhgzWd^uDzah Q yR;B0;#=u$8F@S3|}.DaXGXg |i҅u5ˁ4y)i{AKK0cS?s/$uoaUN*QM`57ُ) Zo:Z3Zv4.3%A v -CgWc>. 0(Iux?cT}@'!N7s(laa JE7ޮ: ݫ>7Fӯ;pq$ c:*C>&`^b+kpq9D~Cי_\\ /8`'G=Ufb: ksγy-~NpWq#zםAwEHEUA`y:oD;-k bw!'aZ&p UAj3. ;UJz57`̣O8e9 gx.J_n5>8p~G<(OeFHK!l#6璴]Ld=#ve4)+޲ yq!|y#ld$%!Wd]U/. -TÃ$Q@aEה -c0G䊣wg󗯫 `ONܵatfE\OJdzzw!7%>MM/6Ytv%r]Bzq,HF?QL`3o3AD1s ;hxwX;I qoϮ+1k^[ -0膏$~-~nnȸ|..[5jD6!=v.-M􀧇1 s"DK*}XLr9ƥdXP7аiW`HZa>m ̊l?f{DILLucyY(6HGٿV~݆y$Y9B}kc4ׅ~I -SM`6?H-Wހpzs>ip?"]Hx[`aAj <5fXOQ1q.܇g̋ j -mIV b'3ӄ?lׄB3;:JhJAg}a;+ڊi;$j%Z]LQf.*ArQsd ݂+4ܪWhyxpt^/.quB'ڇl6x:K/|WmU2̦ -PidQ*Ǚue'U+W0aOl#.gV4^aZ,X1aE|>iI^/TD\WٜbWhU4AHީPFg`өB=6'fT"Db8:sL.ݳ3{ހ1;KET6*yՈ]A \H2}qH9Ћ#ix23-`X--`W;9+4M[^ü;vjșd6DX-TD2 S4s<Ձ&5b50aÈ1mR4x*$j!l=b;+Os_$c}AI-cp -?\'s?CKD.l+`Nؾk(Ԃ+ -ji0gA.-W$=/&ELpj"49C‘?/$wԔL3L~ޑ[x_3k+=w9:Z#j!-%d3^bQDYox|a6h>gez=~v>I求BDsJB;ܩr癿/(#}ɗ: mȌmqL7`p1Bvgy -3x^DSݎ2$hu!&j8<p'@q<ÙS^X/ԖqG5Cw&ӹ\?5*y-j5"5)yd_hę̢WMlט (ԩFia x/Ȭ*x?EfZZ`=aQ&*}F¡hBVi* $$#.ţ ŽQ?#<R$U8NodRo:>K7{g>.{?ޒCƱ\_]9Eo %;|F\kcS-Mw f#ZfOVS%&5_(aDE"csoYkηu-;4ψV=!]x8(^JF+iU$*đ7꧕UyE{hQуE3C/TݻVG 5QD}n v~@ǺNwz8{ P>p2ݟ|]qBmM*7 Mu$$=Ua:i}!YI-zȈz"VJC]u%\(=}G0Y sQC-ԽGTgwmlS~ZWpEorOFɢi@ w7Bw&JO1*yfv-Ppƞ1A%=El u ysxԝ\ -V;m2oNd&$m[h<Lg&w0һߌj#17y(%GG$GKI݋Nbv>e }|p(a~UKxlY6H Zr&58KgFv#PF 0BS+K=Gs}F;»-TnыJM~/wQlAtnJEK -zX99py~fAFT5:k:x_m` -CWX`אH<Bh$OCHVcPZ5,ԣ4!a)"J-{= ԁnQ.t̤#wZ7NT?A{ glѯ˶y"CK $x 됷[G*@(+hID4JH$"%qWp#h%+:z]tÀZb( y"؀J+ޯS<{WmN+ӗ2t; }N{?2U#.CVM=/ܴeIc|g~Vq6d9 Y>UظrV_wپ nO -Fp u!J&eK{;wbS0Eo4v`f&+og19+^T%-ZN# -3fgݞSľͯ-ӰF\tޛD+żxOԜV ؜3ܼl<2q\m%3@' ۊxs^zPՠYp_KBq 5PV7yO"ǟcS5]װ8o}J -3ߣܳLMs=bi9M;"a6X]1~ - -jUJuI -Qv.&u-]ej+Ft_rL1sbE(j{{є,e:]+qD] Pn4%!7M -YUgD$cXRЁd3}]K%ӫ9_,+P̹zocO%NNɊO`PW08/rM*}9<+.jiE`Rvψl*w?e”*V%H%^J 8MD)׍ 1`N_>~Bt$Gov:^r~]{u΄mb?5|Nq3*>w-#0E#bYcʙ-tD$z*'XcT~d=ɞs&-tVa0Fda#@yֻ;EmrFyq Ӌ}0$^TgDpةʴ; Hs\^>b(@؞\ -a0;?5jIHa8 "; d0hlb ŨD=tVʷ 6` -Ju3$9WݾǙG6ɗ׃]"nVGgmvG+8dqh C gb{Z2 V r[2wwA@CFJ FXy=g)f\ ҌJZ9c/; -bT1ΖAm8dd[>g )AL̬W{TH -RRdo9³K`Σ.N=mb+ m -:oخr#1↘g\Jֳ|*’Em?Zm|AKmVwsGg@XKbc6f*WT,*J4VuX ^S#hE9oq $`} )]Ki{ 7Ձ_88ЭbwB(,* -xpM<>'&N5tF3;NQhR5+PQP)ģԜt1PF} Z| ƣַ(J%02:lי D[MM~0ؠfjr)^ 2=w}'36W6E_nfRyjM&kog;c' OEu!Su`R]x8j8l-@vA[շ`-֡V]ωk,Zﰗ$n;r_#9ĞҐ)@W5U6/P'Iʓܷ_[ҿ|/V_/????~?????̿Z&gf`-0 ݘjΈޏO;%ʚ9ЎZͱT^m osT(}s2wFY(Ei]nGh3)MF{_P~X<'|bGʟ!eS>"V`P@:77`g}4l &=(5 |O /`~ś}ZR>$2Lx4ìtp+eSp@y(gC1X˲'ToS1z˽C9a,_aP6zH7xmxGlPzL6",̖[}lN!J|L^it3Fۯٸ{տ8ob9YK`M- GEiaFk[`@Uן>WuӟX (S S퇮(+ a 35XA^PUeX#^z̵ B OJdL>}Bڑ Ik39hW!l@-}͍:SQMz#ĘMT֭T *yӉc+`'dbޣW|B*)HckvjTޯxfCOOr/?3T -_4ԂIM)w=ΕWZK&'o[_@}OG%^$ֳ0TY3*ݴJN#n:#]?U$o -cg)kE)쿢&I"}w WhARFL X;SWB#ڐ@c}KP }S[5Iaٗ s:]P֡ {_D`y8WWW, P,z)z& ^+8mJU …t ZEulx*l!t^V":ӑ;` -!b_iשjH=PY{ ͯ񣡅cx{i_ޑW:kq nw~'h}qQ$8 I5.h3mA#3izMθڎ녰%ip[<1} "}9ȃO٤L,G0hя2}F,`GY_)~"UPȴڻ&f!<(Pk|Kʐ˟ۆo@t83<y(zj%+ʝ P?}Mh<4kubc4ONˆ8~;5p8v)R/g]GN<7\WEc{6 & 6A,%d.^h4=LjT+Lj9t=v3ӹvYn_-αFip΁2][Xո vjm`+j~h}^#̥(4آAS6?*8+^y^!d;5=Enbv+Ug&v}AlO%T/fHUG֍q >ݗrmsS_P׭% -e{\+֢5֪v0p_Hw<9|36+,%=Bk˄EALz)RF:8BÈ#>!l[gI+Ala ec6W~0v-OѫQ 9*=dR]Vt%]z\Ů+waԩ$ `сꚉ?"Ʊ? -4rxT90ڶ#t@+b~zɖ&2DB ^Ke[jjha8YqMɋ58cJ|hRP$͢ԡ%f;0éڒR2Vתr~_Y,ڡz+ǎ #t[\"0Ȟe['vD%?0bihJ=cBh||RN̘"~veyTzwwQgxNRd<4l]6A+zM6`sRoZT{{u(;S>^y,!0:QJvDJ8Vh LZHZ\7~xJO&}ysyqJBwqMez> -Sŕu(RCƏ9F*lAxՄ'YB]ߝgtX@-SI8śh75pO6E ÁՏ^]lm_{òtfy+%뛈8&V,Ef*pylM;\REF|HM}2+HkouXy  ϲ2Y{nJ;-%mxߙY.c!8$=8I7ihwSF&\vyB.keHqߜw^6:L҄F14ݚ"BCN<+l!85JCtAZ io -ul/^Le-u23W2ȽR+R+Rv*kmBT.޷ e{HY!%0qZ_mS^0rwGB~ay -V_6q҄[*ˎʘ<-3.op -Z :o"F @-|Dc)|iwZZA̻kkһ[&&5z|m8g65 -|{ԱL.֊ul꡶ -c|ڤ -z|BiݙUۙmC'$+I [sFWQ1RLC.i"DuqֲAF ^3슫 N~/f&7٪`fcͩb{Ċ9PP'FMĔ/@ւ.34-Fy Z4Tϔ#zc4gDm k(O?*ٿxO-FaVۓQe(s0;Os;4_.λ֜ohṾ,ePV%AvSֈƮ nF$Ⱥj$/p_|+|ԔD躛h@[[lf&`xZ}[H(tX@=#*JUGT5\՚cxL#C kW 9(unS3u5;I J/o*lhe0VӁf~d{t(2)y  BQyܟA]wG -lZtvwHusJVP!dE9ЬּhʇW*"A5T)"B-k+gwI7[[S|;7ᇤXcmDRt|C* FLZKEG\7t+U]W:N"y~F|UNDscs8laeOذ\WХ7M#L<$R3XET841",>)0>zaZ#W83/% -vgA/|xϡv[ȷ>o_U}v̕Dw!7 o!ڰD3{g7Z|_q}s%7Q@c's]cK97ќHB-31|ⵕ)g E!{F\>2l &ůgl5[D}&="u9ή5 - AbcIh[X7%+e-\Y dmyGD|!yZBq+=Wb "%b*R/G nZP֍WسK& ~># -:U,iR P2ZRhp#~!:dk>˞bJQ+Z{_JPQڗ7ڎ~cH*dx`0,eM-XK٧ 3bCy,QCAv0FPCر#937l:̉`Dx n+{Jl(܊ˉ˥rAsYL˻I -~b~p1weɃ|#υjd}V(U*8C#SJ1)mW<]t~ˢؔF'51G$ɷ&턪ەI{؂$Ca019) r pw7tGp=]#3m9"E{yA`i5]?Dzfꅦ8[At(%4ԑI= BD#XVV}( -Jv%4V%ࡏkU7kO];|ahYV/b} NAu$Uчk ?XM$~- urHY7Xs]?jq2TCGVuB׹uj4tpi^ۈK}:9O6%W-5p æO_K%nq^ߝ^XD [=M})PRb&BzYAmˋ^3mÓbcO'A!]yךA[(tYFt1E:~\OTH:kj9bFDWa$ %3^N,gtf4˙noIsBplWz dTp*q)4Ў*/*Tu;y2|9Vղrݣ qI;LPY$y}=xSˆv[ѬeV#lP7%Lphjkx!Y(;U&d@O-Fi"SQp:(J`@z' R -#+^bC^Jr |}"&ȅm`SޢQ -6Ru X^vp4f'EgukkPlUm vl$i]혼:4i˧BȠEn'1 -ּB6Pg|YqS]:%*TyMI^lRŖ# kG?)bZyg }&]<҅l>|BxjUk:Wywi 24NgJϨ]~R2s3VYUzVn#8Z=q-8VsR8F_%sE:LWjSdaUieRxE`ly' :Z -R ZFls:#zpܲ5xl pVlP e֓۱iQIg ԀiM~)<N,yo?'q%N/}QBWYҊHp0q*l,?'-O{*8'H3{r}:x+G̅ lj}[1B9Fn \#CQL~Q~ V1aHtR_9և}@ϩd찐T(}ʬ`9P}Sc[?i)pP舘n -}#=UȠ#=0OB'14Û\{4i*w -Z]Sf2[HmҊe-?!Q&1T p濧Euj<]hNwCZ;(J(0q'LM_6Q/ڤv$<>cgjt'V~ cUxC9>wi^)7bWѯ .[iD*5ٳFyڽGKiJ6sBo<^P?V5J:Fbuvhxۑ`WsfWk |zJWseC -f+"j;iq# -w|]Ԩ.h?R}q+3pm}(4-,~05+h0^uánqk+c0PTOtvn۷x;$=dX -Ӛ(Bh>@٪m .f$aΝX -_ǓKc1k]/슢1 o-@kG?Lν}t<8I,@!%p~۷K_2F^ccOMa:C[Z-haɬuZխܴ!A |@w!_rE.];m@7@ymʔ{@tv<ϧ'Ca{ߝ#=n"z -XmS~,ڝ|1h)NR)KMaB C0T o?+YDUo -T?gF"Ȏ]@}4+:9Mm  -J 5"8b TGmH z,w0vm!*ˬ[,/rRLtrhmfү@EDD4,=Pm˞|{ڌR?9~'w큽CGM"89Pn)]ױvZmȌTuVJPxXdÏSzlX s1smPϤ*JksO~ZYSI#T@>@(&knlӠk:ӬqTaۮ7za3:YNW@!ZvwS8o - J! -bR|C{&L5H+4RTPt3FEbS2ӡZ}\WiVJo1R2u-kr|(5z}K$m|Q8^BCtMTev !S ȼI;flϘ ˭DQZ>E0qnd3s#E<-[B.wѳǸm -Zɂ%ynsB{ -9 n÷^ Vܘ!D7 ~+| ɥr7.=:ՎHKHP0{G$WGb4,e}鷜[jг< .(LȻ[{,xamUkhoM$D>Z<8B@2@nV/¶ pw-m 7iǬHXyF[Eel]!Q ݰ.;l‘pIߣNd@^xKsTX-@M/|=RhNCB} 2Sm9=`g65 9Rekf˧c7öEsNAN2\lU ~藗WTm -|}󱯚`%Ea\sXfa6|Oj\^g^cKq0qE>Idî -*pi9V"tyù๷ fE~e]$XՈ"2~zğhR)VH7VѰgf&8Z[@ 8Q7Ԟ $$3=ؿJQb 8"cY}fK'R([If3= nVsk|?,^GAs;m<R`Cb -?>Řie !rKLARf2f5cϢ ҫ^_iA^Ƕgf@|@XۡDL`*G x2h(+;&HZ̒}<]7\|4wWeA0rg]-J|r/* 6Wc jIMt=of#ս!ha,A훨SYݝ0djzީN{<3wWSu]*;D`Uտ{^/<ڂj[MIr6S+ttBĚBX׻uY)oLYUIw]*s)96 âܝSeZÎ=ֻ#uKB~"ʲQ&c!iEo/ OjIﭐ2n(F` $<\zļi.!Fw_soÍr0Q6#oFX-񋆮lfRп e h(&8f;;лp&k9ʑit2iQ⑷Qڂ}kk/#f)~26F9yd4lg:%V0 zө7`֬d"Hja jm!'DxH c6iU%tH5L}' sEaɺ-;D.c뉸1wn""z+t-W -eumC# 5KTikُ ;=w6osb:ڹn W|Dv\PZۤ8T!#TMRV!-TtޜwNp[9%gAPxkFqOQk"H4aZ[.&PDͶ\[nu[Y]_bkR.ogtOW:sV2 d . +̄W0"{ih}'ej<{2HSγAU VNw9?Wnd't)BY+$zt.y p]{*ˏk(Sd2s;^[vBB~;=F;iHn+ -P#$04絋Z}e#kSnMwKj[:띬6"n/,C 8ж"zw]!0gGi €}|yJejc EGjH@닉_z>v.7gNk7 x N?u@ϻݎ0H)d)P>@˃X4$b!!m$}dH#\ZCn`Oxٶa|g?7c-\IUc/dH{*r\ݭLD!|ULs)Q*L?)$ krlq KkoIGcMr_ȷ: r=Fcm;l$S/%js&qMv >ЋulǏ~ew~E??ЋNuښUg7 b35ER5 -[Vl?op<"5]<!f0ӫk^÷ -^ 评Mi!+2ZM3yվG*[xB__lK,+ꃨ]~7 A!xMC"^*8X 1g8zm5rv|WH %zl+b^1)G= 4`qdz oX̴&ub#,hm.ܘ>s6K > -wnX^sN -]C7VB#9 Ax+kG -Uv7u֏ ` %t_kX|F|;H?^?`IF#{Qq*Qv@vn־z{d:Q] ( 1-RIo7jM"@-Sb݈; Q&罔qђnZജI>Ώ1UW[Z7a\M pm "Hvg)VCG|~3"O??Z ̲6|Jʷ T9$-#zGTŊ\DT( n{n3ْ꺸>L?u$_l٢lsߙAp׼違M@ --V&O_ɷe{vOzeūڞ_hYnTWC}/߃1lۑ'3Pt݀s6xnϞl9գS*[}S8utR#%#6s銖MO]Et0cIUn e -='}.s -À7ՠ>TDg_|uٛ]DJ"30Xm;c+ -]|T&+ e"`Q=HxRM|E*%Qk?X3Sl@n֖<2alu߾T3M|Q~Kiʯ5x萭 /0XC 5gЂڵn/c+~Ķi@"t㴾*H K. 3--!'uU%1"V 龊΢:1=f[g%+Km -c_R0lj(gDvHm6"Drb]>Sf#Fy^IC2(S?LddU峞k +t#' -6aD FPx4#*8ϔ*~jFKtƉ+WK]p&BӸ5 |bKfǕG_cb[ď!Na-PAMW (\4Lj0l+}MCZnقrn؛ߋ߽di Re _Tљ4_kʌI>x luѱIaE^̨f> -(rMkυ2t@n8rDQ%tG=fjD<ѭw2IZt)$s tU[UuҟJomy%ŠWGa>o((Gbx.]S(&$=67z%*/ilіa\:;QPW q+蔣KpB1F@fD-f#O!CfX3Z6ȃ@B!J'P/㙳M;/ʵ׏\2%?~Ffq-v#}=6Nkg( rkQƏ>/7׍3tl//[ґD*@ms '/EA d9`˸@W5TZ} ʯ\Xer:t9t٣3.hϾs~][vuc)CFӃJQ^@@3ʣDqE^ݨw/ͅH'A ZCU 2֝LAy=zTeP%qJ^b  S\֬6ܝ5:rTwߑ_Ɋoo\wDS#KC>\nB]WOcXW+%R|hh#h^974lJD{pK/86_֭4T1~w0bu/Һ -^3^fI#ӄ!R|T[TƟMGsD9(j&=I@2:/QDw -R拦2s3 9uyA{۞y鿛٤vw %=lH`EQaH" -D? ܐ㆙8Kiˆt93jDPL80&z$RZ)q .{^hH3`I TA҇'* >TkQ,jc]iUgdW3Aށe:0S壙L~spdJtKX O_R/~OǁF1 -{ܼ3`w#?ؼ~D|7kDf LNܞ\@0#WZy`wY#q_;9 #tan˦5[4," Zۡd.k6C/{,5)M@Hy\viTBaVJ Eҩ&[TQ3E>R]mMm3$9Sgj`}tEecJ/(kxqeJWF-2+z4 ,\iWI.WA(gV6ŵY|־XlWX1Ҧ.­3жUM ݞ$ Rߒm3TKzcE/[o0p'Zhx{b%=Rlq7I8[#.:bd< B4<@C~, )} agu:{ku@#5}h{+Ӭ/4Ԇ%tZ+J>>lv<͐(bFϸJwen[伦&G ADVzmi(6+JQԬJΜ'T&Խ6?!V=WĜiջY)~HRGUIeR1̦_`gb[Y(FdD吵y@a\Noݐ֍ӾR3)fcw;7uq`RhoTVyeMpt|&!Cg29T_#p<lVֳ @pA 1?j#ݪ efXЋ/+Ll͹}lID\tؘd;r%FJ^9{4Wtn6*Q-zH7p\FnG+J f.L1hs-m ֺVu bH6k`hψT fRAb=JjoEdJ -g34mN&uоP%_Bo .1 -N)zm -鹴K/2j~Q+UԄ6F;bգ:~xӣFe -Պ*md:P#6ھ o '*%+G`#o3AVJNUV6͎J00lBqMsRPYz>:"~[:a%C])}eHI!hHE[Y4[j* KmX @Casy`5s?(]oLdIjl'< "ڲ0.`kj7?(eqNn%X`p_\Z/YѮ럾X_w?~w[ݯ; ׈Y_f7⪄Eeb鼰odd?EHP"u)q#"Xn6 Z<[Ab(\L֨scwpG^YFGiC -qA!"ꇎ2NAZܬ0N8*F7DlbeZA*6O5Èzu&b(ŽXTr%䎪hkʻ[l_Ior(@3mozi{-8@a<;|y^rPp}EJ"%uuɤBJi`w\{Vc;}˄; -\(%"@n\dan*;Q+j|Z|h{ٸ5@u0+}NCwTU<DUqn0Ѯʝ@,NO] kxUnp]itYnxy);`=w$gmշ@|Z ->0]"%iJ>.bRC i#. #ѳ"N+p4cu>#EnX%?CIFk Ӻ Suu4qLf]υR-Y$f:wUteF#N eUEEѠX 3kXAܜ1W\zJ4Ґ8UzI!>(ց,`ccnꩺ8k5> 9R5?@j~)7# c ǭ)yu_c[-aGЀ$)BUYv`UFo@=6ޙޡ>03-k5YW. A c(I5St̅`a; sA7 L~n "' D@ 輙@*IhcRu IL,d\vzmqcA39DOq L2bQPc%+(P[t)HZ$-TrNZ1 -M=$֍Z H<|G"|'eՓ@DIf٤8y|)U *u1 6¤j-@_C@&t2wAWmo+{ qS5Y͞حAQ*J6iGkL =DQ۠HtH ,zg"Nn%Bs!PB}#X -!e}{uHu/z Zá& ^֦viTݥɎ (8W6n/cG +}F$J׃Wmn5'7e)p,n`> -{iA3e3~unD"fDD8T9̝kpLpb"cW2^hNǀƷKӪS7|k~iVZ?`TjpP &Q(KXtiWԅa#md]Jl4FᖈK7H If&jM,y詀/īvʠ]R;JH -~U4}D vNmJGTY/dS'+4h~Dxf(YRj<2!ElQ(Rd7W<*e&!4d6(bFft ]sj>%j2Ew3J֤ⷜJĚYŸuBēiXԋ ]/AkA랆] |D^b.4'+ yļ(/Ԝ<rGVj֚U0w"8z|3EgFa^iy}v´4f[t"dJCpoEof QmuA8\ c{d;iYQG@˃/{ȿ<ݫ7"" o"*}@_2/M'e0u - m*\+uթi޴nB{BFg -AcHz<Hg2>FTڛa|Sըb3JÁˋU@#6".ks o[6-/`(>@K=}Ĕc~{߿ خf=)GDeadx"~@ )~4AP " -MvW>fU7lѷ^do"&-zp?owh:diW)f״?WkLE J؂p%A8gkFȹ KnPݔ[j[wdpż`;Ζ!.֕,p2F.Ϝsߙ֔x! t}1e_qOlU2ΚR"t TGwwCe&E+tt+N#qWC$oRTĮ{˓X6)I^y4n]#XU!U'B壵+Є`X*ht P`Ib菏*S㡃2} -QNId|HN0-}AIhZ&jH'AXLPLO]''2&Vg9Nɵ41۔vh(f'd<\ڈ籀D͕ 5wk<>DMTK[m)Jςs! -+pxc:ϾL½ي{Yam+~B,WzMy|ۇk`TQ ~tA:PtWcLN b 8eWiC ~t.qDt.Gqf؀3;NIvIP.K?NJsJIYQ XH)[e0P""KnhfKz'C\b(Ƹʹ|FT`.`y@P?XOpU'?5jxFm2ᅊl>z:7AUńGz".|`v)+Mjhγ\Pdt yX]M:Ujh&¯rQXö7ߜ:3r;*Prnŀ vscq6Ru~gc?67.rgW:4u_;;=4$PF6Fb*ZX {gED - -(D"x.o OcL$\[`xs|X %ѲaArR^O8HY5wh3E9EzK;n!e!Y\dTblz`@(sYy;<~\'r!UJI%:Ҫ ]006j+ (}00Vyh3 `u1; Z֯}kx2.V7@Y?j`| %v*7ɵr(T[iMhbovScFD]N[Tgpj$pPѢ\R$sX~w𼫖ZEˆ\}  w.t-$>TŤPJ\q( MֳyN:{ԏ\2C/;;k;;9]wvՔm$%v6]bs oP俤SږmCFS_M&Р>r{irdYìԗ670@:} ki?[3JPElfmh"cy +شC"}YtH%/6L5^䯼QmvtikeFK)@t'*l@iFH, >ƚ 0AyT*p?ĵR4Y Nm"\C=4)V,l%Gia+AfE)J -癟_)B͚P.xh sGڈmGI= -,4#I,3 YhcN"FYhR+=~)Zsޔ{뢵)B(ʋ)0&wףy[PD9s^/`k'Up~4pU=;o[#dF_ GwkG۞4+[TQ^m{,ģ$Uv5uvR4]wRW#X]V껮p_UN"!ד>C -6t7-g\jQxIGfpJqA,n`Ł-"-phL=¥$$cW5ODMļvzB籽_a[CL[YIA) #l]ܡcj5uF}[Ty`-@8F@wldBv,8?9PUvUp@D)cW!NfӞJ\ Z|G!жNhubx=j-F8YszfxETL-_WҠ5%%ɣ)H`Nđ&;hJ'b \+@{PF -go_O,J0+|ʵyؔ,aeo.Z j(B' <ű .j { nC(PBD -&b/";*g '|ۜf ZƪAk/`Mq>hݾ,LU^Oz8oxj:ϗWDa<SRTa@J흞 -:$0slЯ~gbֲ6uO 4-\E\\*ro? d/r6BQ@G6m(u 8[N]}u*|+\Q;&E@5q{?,&ͪ%PwreI@wʿM cLׁ{.dT%1l -ܡLǭ՝FAqe<6n+*"[yz¼sZmjյ#qP8xPcQl6";)~W5ӮOfٴ*eVSV3JNeF\{R.mCw -/(tcqp5}V |ڨ^c6kȬ&h{"VC&LsoJYxmD䣓:ZM Zȴ∞D+^1\-d'}㽧l_u|o(ڙ=%t!rh@`cvA G*kRYTQǁ SO#̆ xTiA$r>"k/ïq΋p0u 3 '%|[t =5I^IWz'aNV`e[@:il'αs$J/T,;QZKHӺ |o9Ը59`ά]!.{s+2LƸe A_SW6Uk`XTUg0z:P0ց"g;+}D!TUJ24#BTتE5˱qnH}nqg NIr^W iVTv( 䈰a=̽4lH+rIc-S?]|ہ7oԁnm`_.4yiyy.5Iz+StkߎDOWͲ&^@8{a_]r@ )gۀAue=, ^RE-H7nA;l>%# A d0Dl_ID%Ւnަ" $>6dξ9ܴAVuݡ&{}BcGjNWj&.nh3wdw2+ -b2L@N8 c#lƙn/fSm={JL C3$v)}sbع -+;MJ]'50\"n2N -+LdWU: MGR?ϲ*'Fc\o"/E>(EMܰkE[{+:ВgUZMڸtp=c7&,RUeFHn|DP4eyEJdOM;loDH:KAj$|MLGo"qK#a@Δ[䴆SK9gF[n Zv -m{ɽw% Dž@%eha;P3;B?@ {3`bo CM0!'?7:-uye -uNfA݂ץv oL IZB6YYes.gEvAmqûN mmK@)(unʾIX3|MڲajB[<8rW7X5öRugOO%+ˁ;#Of0eQzjcze;bx"?ǎ85|K %-{ re ^y-鄧$Q:/ggNW,c9i)DV? ~Qȴ`E;Xѳ/Z*nMc'n0> EۇܟnU]CN+:`8߳2lrEQD~ٹbJ MDZ844VYK vk?7laǜ)|V^ZG-ǩY >"zeɕgmՓֈfDk2iZ" ZiM0|NU#`hg)!0)wZqo ]tLʭ@|HYkɯ.p+3J >톖ёekr{>G -C& C*./4~>!U6`E- 6`7umթiԖVWAdYSlZGiX~`;Xĺ w&s''+ -Ԗ"JÅXs!:x<; #۹akP$r\a+׵+,~@:6 ? ssw_DbWSӷ.& wd_Jj󈦧/ڛ|m \|dPѸRkHcw_{̨jR̨PJP(& -=YݷL@;.U6&ؽVɫ3鈹WGR(8C~!I\5 )X5gqb)%u;[&`AJYYkfti1ȇ7nxdSƱdnkg:;PэPT&TLz2"?W6?lFVsUn< ',aȪ[(tXR8+$dži^t z;+Pغu"A P {=/zҵ{oCOŐ2ӥ.[th͍1_h\yǹR+'}xJ$8>'vg $?32Iz O7_o" 72bҹ{\{'0mos59# 50|} ~SSR3U`y,){DF -X|㷶r3! )S T9ANBCvh؈RupVܜDQ -ӚŋK2HsȜ7_9ݑ&UNr4֋LeFQ[QhHPOthq5`zP=pNuN͙#lL[κCW4#+}n`'k }$Ǥ>v:0;jbqqO@lPG(On-6V^-*ɾaBZDbTK8b`bAaמ7w7tˏl\H 8*,2=8{4L$\ Ysl,M>ײ^ʈ -rP2zAP4s[>6K#QZՇdyrci޺{1U>qan ׫Eȝ/;߁$ q{M 2p*9€4l֗+ _3Jb@f~pJL8F;Uĥ~-`fvieUYLu:Z"Zci% iIgԪ$CcͲZYr^]tGׯ"x $6r2_P#׭D![7WSZ6|_}VL@slyHfv8 Sz8¦,"v_?PXp0O(-:<4"YP)W2 KkN;BrٲV%sК ֲ쨐+č0__f32XB!P , zZ̓A*],UD<բXaa˚Za[Lz M.3Z/E>$!VR1-8I?%7?sZ~Rvbhz =ެ&k]VMx0G( JФ?t4?Qr-* -ZۈN="$:C5\s/Uќ^~*LiL%ڹAЮ KںPAcN5ȕ20ablxH!Wwy@Qhd k~YnXvl$(-IQV#"aԯGU/[_"2cw TܷDaaR2X*>TG~]`B\ռF$B;y۟iDԔ0j'CG=lc;˖֬~Kc qe}[]t4v,y_Yd!!'VYQ뺫SZ6bw QBRay*!T;*ix W̘ȷ'ݥ RJ;mRtϵ&\{HixڛD⒳1^ -H{e>J w@Ry+P#`>8v0eWf0 \C]/#b(v񧦧Lda[0j4 0p[uuf0*5!,) 1=RZK`h-B މ}yuˉgIs(y3;,`{3f+z\Ƴ@XLh1W=W-(+X$ˍ:qaQfMb!\,b5vG.5~kkd"]-VHBC縯RE.K@YieZ7noުJyݥB\š7x -0)]kfI^~<rBvq]-E!I G*>O<@"kAkj6I4;¡cmգ zZÃJ.~77†g(W{CspCqp>4^}/ ugZ;ڸj[ }ŪOȪl>]<4[?bX -{7׻ߠWQ) J:`t9TJhM<uY<>Bp?L:3=+k - (֤GAvw<@} ϙz'"~יmu+iW~P^Б~Η$iG A+_%aS'8<&M A$1xꭶf3$^ e^k(Q gyM:NOIwi(DGY;i5:{ZS] ]*AxlwbTr;\7?3I2|Yw:(Tw p^ JO|$yYwV7B"=%FzztŢHT 9KkڧS R\:׎s9,/ʀ ^՚5&Fkq+4mwj -A5͉ɭ.|NY_q k&iCNbOA?*o&qc8kUE^EVJe5+{Mqv7I͜^"aGd`;մQ0-YEF)HyfIԸzTߍMĴ)O"lh[4S4y(/wvU'}oPϽvDNc־hTD i}H=@%YXEnͼKDsb3!+c<ęd링Ba - tfhpԴtTE/~Ĥ*}!^NPpXAa3:}J1@J23:z/c;Ce&os(6= -qe'| J RueV!J"#RL0$VPjk -'_NȈrkeF~=9L+p -$嗃!gMZC8gߙ|?$fFح>0\bV\Th x"|7Vp3jqv=Q^aav09VM噥E~"ևf4Ha`"+}dh3X/2/, N-X#)?.]fZol|iGK7U}U_`*2 -, 5QRAF7Żce0Ӓ|ۚCFm_ii{|"Gma(nVbh>ږLW!Kvˈ}py|%ܹKv4yb1^}#'dO#(Xx:R}ng2b &Vrڱ}es1|@"TAvc AiTC_ *vr(2;.{MT =1j>3$`&=L)} -~0z܉\vFOO8q1z"`Q&{Ƚhqs"oMfUKCWH\XiTF,x6ԝ.׊r@]ڊۙp>:0%גk- -Jkdeם3JhGd%ɉfT1+p 9 -:_@_y*gI%ZU蚲 ף9DC+J0.0 2>]nt`͌3sLajHH̼|U~ql(sVD'XCIҗHGؾr~fLpgncܾ3Ǎ$ٿNނz ub3Sa佚""I`|+ov":Fm1Z%^!tt,`ΆېڧQJf ݑo`nXOI Pq*ˁh3!#]ɏ-fxjAETl/QVfib<\3OrBUpTL7cղDot-Osm<A -u: -/Lq\9hpW fpx(HVix"UўDۀu0Z`fMsD,RXbhZåcp kJ\k(rM'XE-(G'|nP85{yڧיJA`i$my?& ׃#CMx<!Pc|H1lYNpq?)]xYΕJeUМPFpad Ք"1QZ=+IWQIYO/6E^{! +ܢPzp/Z/,4"+U)ѯ>Vf66^%NUVq ?܋@tbzbM&"k@6dGhez.}}4zY4/|zfBV4 PG d9x6JvEGdbf=feCi++I{u[v^}'6!M!]< x\8Fp{gZ6F0^Qb?= GrR-U$A/ +>FaC6W(͒,iiz@a0lu:0l <$|Q7^1L>AdI@ - * )Z;k<tCjFfQm0y'm["*twZhq|?:TWdE߫:{Z*_}\Gцz,S O2 -Cq9tWlR+/eWŗ Ŗ!ՂHF=JR`\3=K~ $N,1PwGajT2i -U(h-$z 1vA=OS^0 Vkʘ% lH 2ރ-P#n 5F^+"Rxb3'IX?o!_>Ù9(uKkFc֠.3EbRgJϘV?XK= 42!sAtw&w ;)I4fI*U=ӥ+|)?Y%K gOZM֌_Tj1KxG7}QBTM[eD-q& UvNCy\f9rǮdPԅuMsz`/E տ[G'#w.qd*"X(7PwʫQb<^^G8,ђroC|œF{Ŝi;C3oWM(^}pvԿWoZ5jҝ]BU' ek˲D;_{Gq!+#kдDb^w(Q -tLM;GDho K\JUx3p~7_f8SPBVw;;5GAXW>OeޒDZKJRs"l <>#&n0&{Μx -$c?a*⪿>𸀌Al._˃JcPoh5EB8MCD;J׍*[H#8 ;Ih#R6)ѻz̸~]+\.aU*ҏ>n}r DGyN; >[Ngr5m!P>2[~Bvbt!T)]?d׾R3$NA jKgH3 6{^Hy~g3je,./ bDQ͏yXݟ~̗S eq% PmzQ3MЦ@ LQfG{RZ4|7E[Qɛ$~* ;'QMpܺ54XK0~Ie-!Hz۝' -} Rie3"=×sqZ? Moɼe慈Ah6OdUHVB1Xh>Iij -&;bdK 0SL{cב@؏gx - mKgqI -#RZ2;T˧mmmN@V.cګny! %D]skB!`iODhzAJѱrFΤǎx:݌Huu߯^j ? .S<@PpV" )RՃwd=#5e_GLyl  ɣGMEch>A*dվ4Sk/W먬`fה 1+1d42v}|I+UgXXl&q)QWKA=p)Qn0C3ڻ)3`I@|_#\V -WCRWB?>|9^.JKQhFFkެݬ;5M@5lpߍ: ԂFR,j8(] Sac$ z{P`F>]qI C3)*u O׫]BZ+SMN)|w)Vt!D)@G6eUVD: hB+J(….WC }pOG73Q5RH+qJD76LwwB -EzuXd?Mgq\-ڭZ+zR1 - -F;QFj]Us@]P[rjꀭ%@V?-f#=R<* t +e !·3?./`(>;hQOʌ3 C/ w?ќ)!|n(7ޡӾڹY=QP&43O0gr˫fBakhg5ᘬåFh5e(Łu)QVTFf~ _hh@=gbۜ;L@oH*(Gǡ 7 =Chŝ[r*2:3]K1_=ʧE~aBd"j>TQf>Z^x\rBX?|7/ݿ(kb0K18`p5Ù)鼱8BώZzQ)|"([8al DCزdA%Dw\=M֡Q=c܎ǰMF˚Y - fm"/\tvƻw'j R h]@͊`~@jW܄\P,~C: E}Cꗝ')YnP9m4t_4y{alCW6GAt 2ΏԋLu2lԺDϰ8IkGT8Ċb$dho7#XBnwKI#zƫ8z_>ҌM͡V32FU/[S6OsX'dn|j6^!*uc4^_>ǿCNޯTyb|]i #W`9[^Zv&`yIsŤ3eA*[}/KLSHk  -1ptv&5wR__#siURvjò˿2p!0.)N -Svxk?L Giɐ.pD -7'>Fl7pa;HBx"H-"ڭ;"b_?frп!"qQy7 -nZ<hL{ 6diQ{"V_owDm|$\3>HЀva)^6r"Bn,Nd8f΂`y1\I;#:XH#SW"˓˜F֌SL9!Cgu,ؤc$}Z}M*`"Rj?PBn5U;b*}[ǯ~?͆UY,A3`+浣$eP&F0ꈢD8i(C -9gDn;8ױCQd}rrMn<CL\oP('V_2~=SU5P18-@Ge{OP;DWzQCE}+4?qJT5y3"w#gf?UY|S~%u/{"rA(<=N"dFNdVw,AF,A({^_eYGEcn@ j@L_ -"(4.J#(י"@5S)oz}ݟvΔt7PQE$)F Cw4`EQc -cw*QHJr#Ssa(vHQO`ZKT~A#"ich+sTQ(AY-[sza,̫M)OgV -eAD4`qCz&6d5"K:Q_1[[^~0TVܲKaR&< ꊣ2se`J{cmJ;=q8Mᯎ~ pxAqg㬘ז5Eep_w a;,*zs}fo]! L͊f֩_e_`5Z]yFǽG\Q2wxEU}F}2w*QQ3QQiM#.1TkϸwVuJ>K'~"cY$ʁ^P {.tWcDQIX5ܗ](%;r%d`vu :Ef?/ X"jD$ 5!x>>@?`Fde_yH bh1A&A7zGw0⭱_kܹVҀ:N7{ Lw'{ʍߛ;{% w*pBO1(];o8ꮠfFYv\I0wW~v U"x{.zLtMR(}qf1]僩a(Tv@ul#S$bKF4*пAZ[@y.6BfF̀묪'UoU'JU8ίf896nYiAEP~1荂H~qȆ}%~ QjSDçT7S0Es$AZJ7q;ciՕ}<i,cǟ'Fax?au'u\G4\^>PvS73y_QR,9ߪ6CQB+*!6:r+WZUU?.VW( >;shBoЉ{G=3s|-%($14(׉;GZ^ ͞Ӡ\UIa pW:JX: FocN#wc~{Q<^F]͘%uӎuCCn -;돈&CA\M}Cb.絴KJQ’׭\D}M{^ʰ  Gb| ]xW ~ຟcW0]*ߏ&|&f7R*6O;]+ -2 QXzG_* aJ+DVrqchZs&BC. 'aDVmJ"4 O 7NZ@+yB]Z.x·v\mԘ eT`@W0@V g8<Ϯ/{cβe?()X.(81:&w EA[)e;&P^FBgCsrrп kt.ۿ~?d7ÿ???_??w?}u_9_ɗ_x哐V]^'Jk}FUEvp-T@MiFg%H>^QWI%ja'"˷`Fl֖rdo"lkoФ3 FSҐOHD he! -UafOT+-U3tq1`zrP)( -(Vʮqb; T A`/{%V_X&O( N&N*&B`wT:x;G-v)9A 3,-Z9+~fZ{52Iw]Uu֍B+?V;/]; xY]u&jvD r0!ojfj/V ~x øA6d@Q[N7pq=su248`r1k2Gb*@OW"LbmCid P\>;߯Pm~KoB v߹~G/ ~Z%nK&rӣso}+=2npJA㕺`d+f\Vw \b(J&7H.t -& `'b..ޘ>TG -/NC9euX:Q^1B`+q0b6V><ʥ>? ,xkƎT2g?k?9˕I2eL{*,?ΏAʷy.{0ɊCE3Ot'}Ӱ7aiN}tp ۷KءRSp%y-ƋZ.6ꑴO)% Nq4/ cn;"TMbDRLU-gƛ2*ԲtJ#bH%BE[B鞈Nz -؊0bdD}[ \qiMM2`+-Zrb1S+,PPLB[GG9m) n`bK,tV" aEJqI_ڙS~ |Q#VocwH "VEW -[̐ -r ̂}i g MyqugoDZۖ|=39Wjuv) }s#3n]2D%ϫNBDu|ňR C1v Tt2+uƓ#T3L(qsZU3'j[+[U|>RJ0)JQd^).5o ~n40(5" LE iDؗgNwH`3DTǝ}ɿ$_"p" kR K'UHD2L.suotiqZNț*6g|- uQ&@_w̓ 6 82des^[/m$m:{5/EEs,Bwo>rguE3ZOخ"H굴?FP?LJUB2rW8pp4e^ڋ 2i u{,k:qbblF{]w_9HiIކ,?#f@}EX^E -.Tl#cVв$y~Cu&KӼߣ$jK{}x4UCBm4y t#>"|Y୅YBbmrliaMIaX+17q` Z]ԟΉ0>e?,c`r7"Up09Q aW_{#|!4Llk%1Zt; _J̀T|]16CdceXFe[L3ؑ/T<6|;m/$,=W8&h{QyP;zUJ{v;+lәЖ?ΖނD3C^\CG^UH>|3 Il$ 3H_BhɊ$Woʺb!vq|/5 c8o}qع9}g" ƫ0}_kΕ܋cLJZ,LPڊ6-0V[SP!$x8!_r´ [tˁSLM INБ{n%ʷd{q30V >t$%|,fXKĚ}M)ĸ??Kb &ʾuȚpFO畽{њfj”L_(iBQ2MʹFd[nc#!=LE/u liޯT~nxb$)yY>#D(;凈-OװOoW-|$(2.l#-p,)*4x4mA -.>0mO)] t~kMJtt‘'?Dd.8~H `ώ؊R?>\ټzՋOhZYp(Џ{pW%H߷w/=?gU0"}-jqQX,X5Sl;*:Lj3}n;.6fq;;sT50~P5Yw^p3fcNQ3,5%"s2"R^ -qP$xpz@=V<T:41heCf%*,ܝ' -l^SNvԣ_jgDԩU8+J.唙ҙ:g\ӗLr$wC -=ț$F z;Xyg?*~R?[nU}*J{ԌՑHs<CMx%hgD l_ bGN=:qL@s 쭫!]ʻHuu?\+yj Ru,L*AH6fיQN{ap&&]l;ЉH榦# -GףÝ 2~!KgZk7ON4ҤL8\)LGP̀唷/$b<_Y_ - -f2j+"Sł;"(R "/ ՊDơ=A1j\P5_W$z'uK͞ aHqS%LQAzA"h/)6& -M燧SO+ tds} kNe'#jxI/) h.3L|=~] LHˊ,N3 (X쯞~66SV6~FnX} -w۩\K׸[Z͑GFոs!B4e>|ψwQQ$Qiu-l N3;ݛjvbn84[{z<ﰡEewGa31g:e+ Xz0\GԴ1lw1&!*6f:r@[uZ7}?IjYy -{1V*!ZcByx[NXͮŇg{-B]}^; ~#z"nOTP)hҚu_2[rd"ُ MKDH?Tg⏸vdT>L;A(TtL/S(V˽D/ F,K >;ꁔ/Ҫ~CQc>TH Q3irl!qLl8; ZDhjFphf]@}!^ Ul3쨦i5">N+D(ȯ}hU[-7{ EryJe#H+α} bp Zip|`ӡҔ@>i/qŠ/>IQ ͫ#d~3}R Ļ݁rǐW~F.,TUoq0=P*ᾄkGze~J_u}Jh) ebN)3 K88 -U~4te^wހnV[m8]q|tǣlo|F9b$?4nUC2/waw~v\&hzz Ԉo$ "ꖶ -F1sTGRIԣs9w&QlQh?v|NqƊ.u Gkam0Do>̙a1wP[D~o9qہoiQGݦ><45D:swrxܥv!E>:g6EUq@_}tb5K,SGDŅg~T?r Eun:sPgSץd4< -0Z -Oַjv~u(ә'5w"ZsQ_lfSF+&_وY{&7^M>#>[{w$ODi2:m+>E$<<ka Z#%XC%9~ " - ʾGQf^$+Eڏ|/UH?nC".֦R/u#/d! Az=XHn]Z -B_իBM+Mƴo(nz߂qەS#xRMPY A a5r=1~ A"8]Jar#G>>A1/gM֮ee/ }BfoC˨~d3J6淠3#󧠝C 0JaF]~ \3Yax|UѸG+ ?/D=}A8<HxL;Qd䠀!,(gcawk⼶ -jŔpŌK$@i1?}'/mגbRGȆzȻ=t[dVpī^G4(t>$ - Q^ ޒR_;݇/ɓx$a&g:,L ui5οIHi -a&PZ^ -;7DTOUy: 5 -Ȧ}ԂeKWBDKgD pEÅi̇TNe]jR4䙥4L@qeTrl; wP&wҭډ`J(k^?IZGo\!2(AMCOԾ5UfqCCJ,nOO#Q U_Ǥ(<~aʘD*']̊֎H=;^e=o81eIqxkI7@Ǖ!F -)r :d 7Ūǫp\p\mۻ`g:̬ -& F%߯¾@*#R[IA]_X0ilapR(K)R`h ѪC|rՉrc_Q!/O%O}KEuƪD}偘H=pt? -FɉH59LG -x3u{o\OѩA3nӍSWz+TomEΏJi:5t1!{:VhM%ΙQhP92UZuEjW+;<-{Ԫ-"rK~,E5f[*:9ӄRG22qC /FcDF[ GDtHb?;JGcxVg"5d] bETh!-qMNyŮ ]13OyaTȾSA5f9R^^RG\X-=\4`RBl{/tMa~d3Տ׆ۋ  *br@F$=td|QL2[-ŋdPľ8`ZGׄ+ܾŸVus3|;|g=ޘ[ܰ@T[)}Q2U2ֻlޣOIsz`G|^ |#EF62 x_9}O_K~ARu|\3O;H-c^)zz(ߨ>"ml#d2ԎeBCU@(w& h/+Z5f\S6;UG<~7.Xu%}Ǵ;dB* sy[.w]69Fk6't8r&sN -:>%.u?.--n! -RsώQ endstream endobj 393 0 obj <>stream -ltdP"6[Kޖnz(dzmH*֟4ӵr7&><먔ALyX2.[ JH+2,*;-) O .tMܨe$ծٿ6ܺZ2h#K du<=b9;!po+zNj[L;E Y40:̴5B]3ό~Ԣ/ v0APjEH!e=#ߤia^iX/KO5]bKjr@kzmtLylS|pf|d$vLl9HE]D}xbM)ēA*GO >A=7 y$qN`& ,%nėesH-r4fG{Пϙ >s=Ӿnoho︽ivH3Z2xF> WV F˼L;OluaCe84 EԱ{W𻧭m1D&(; 0ְ&Ȼ1F_^C; 3]P_&v+TƴbwzС]NNatW0+\&M^ -v)N5d4:y5U1JDLՈ瞂T"wPz`"edBEt^k%(!}d(-R_Ub+&W4zhp-;2xb sȗ0;{ZQhT[|7[J>juYmb pv{ev";vYp% e갛 & !2y "٢ӭ,e__T+mVc"Y}hNfSw6rLт>A -*ŁLg_ҷRiz%~kٽooae+(g:@B;F*^:mBk\y -rQJVݓS0_X݂a-(RPKq!6Lr6z0N_/uEp? `l?DJkyx> -l~?VBa3|H jJ6Xf[tNqE Wzߧ4+D*[|u@t+]D  lm_ꌾ4Ad?$O9jAF.@L;I"1MzOD.p')ң룶}gk>rPGp>9P=- 7 b~;AM|Č@Eժ"1F=$A)ɷG%XdD،:;b i.= -(|r>xk4 5Tjܒ9Ƶ$XR~f;m,ުvxm|PSPR}^ZU -2InɅ/*Xm]Q2#}7,6b߻=lt-RdvX!OWհ]rB 2"(ST - 4@2$9١ѻASj#RFOnPT -K -7B~D'Q ;%B#~EO gqdc܄$a:8 -X2Da@c5$!3G3]3 [1ϢZ7 ʌ+'8ou*.D22_ܬL3t䯌(lA"WA JuAfxLIS {pq#"Op՜=d?l@KxxD- irf1QQuq^֪Ɇir -8:J@ G՝JR S -H: \+h2]= w|xRg"կ<﷽_hPN_C=EF|GsAy$+aKmμDFH_yw{eJ fm1(OFυ'.F~"/O>\`zpxE׮ hTHⳤcF]?LҤN:qgoonWZ!/ o_o03U'y*|IOכcs2x^1C}GOÔ9lDC=~6KppU#:G#E܄Ӯs&]9Fy=jc$BϪ`r-(NgK=9xwunp}*i3dAH};dgpn$5M lQ)՟C)`GR𰈂".jz0WfchvD PpђCӆy?D, K5b,J g4WXIK`W(pSqOSŗHF|nP/ϧיb\"SQG/A9>ƕ)>a$@uki~NB(cKdJe÷z2UFDn̅޵ϣ2*^cѤӲ -b3"nAXX[jhopmm>i=+MW%w>L>DuGpãbh6kB -J0j{x~0o j칣SUw >ZO7[ȻzEF'w1 9ZjƇ,<>E;8ibFMfz1K&= :PfQ@suԍ6jxpozL'[,$u4K+"_:+T]L1!2_sqtN -#a)nt zUJϖKd2#h;s38(+۱WGM'z׏ᚡY};U!hڗ^u.tja_ʘ=!'){_GZc\,ʁSJc)nF+ -P*S 2hD=ziw).1GkLv@Kے}GGQ.np9r;WZUa$ܗсCBw~*uO421wPZc|cW;`*]|*@!SE+^ZH@߱_GEnHɟyZsݷYYX3r0C#\ ň`dNz" t";K:z:(Mb%mZ`cSHzߣO:wCv.KKDq?D.{%Jwbbr T>?}ӓ_f0*Ag7ɜ>& !$XM;h's\Q\Ҝ:s4 -~(eDXD%)׷qaS7rrP"pЗ9q⯖~Фt9|mVt`')B'{:T˧yzJyݷWvh /2|; ~H%7`?ǺVz+rF-sɚd%쥿".DbvԄRjX͟Zow$ɟhTQ/lv=^tdɂےѵ9]T(]pRlu)[2sS2H=(0^*gW?#zkr8> (O0$ʐ0r&QLZ_^(ӃQH$ -_(4_&Jʅ\c=h{+r/ UxL;:{2գ'z3GzNS5-3~{Cdjx'2i*hǧ법J9_)SzcS0:3@LG5DqjKe(Qf_DŢy7B0ȝvux, O;ƥ@l8x'$&u*_MbI2F>s"&RA`]!ZxVxTCWC:ؤl-m"r먆oHA9 GAD;Z=t 9n;@3]_~awdY_$7eNnj'8{3(km;"DgZ(87NQ(xI*OD+R-}ةDbWA@XQ\=i Q'RV="@`3F4!V%`*i4UY񨆐hvf#ӫnINר,^1h4iEF߾yv`s{I6͂zr]P 44N& s -6kTs4ڞ7̥OAΟ4tVM3xR&.3g^ 9hL!.P1}@ׇ n'oCDmEeQA]}I'jWćQSϙ]Eyd$'[z9 -ސ^G!7qbO/O2ܷ[lcX?Dc5̽6aSOL-Քx+-".(=b'4hO`<}fQr.lepuLc c*}Pufط57lSUޟl ^%(Ft? K' jK?[xL %75,A*WfY?CU[m)Yil3f;5AxUo#$L|+$@923aXf1iFp5mU&d֮Qz$[v|crסX[SjFd π?Lv·w"岭(rDkЈyKXYA?A43Y:Cr' ϯ3A -%җW5'F9D~ēLw9A͸JDV80撺Z@X[dbgQ;}O>cv*5O-Hg/!t)9a?;@f51C#bGRx@? ꚼQ@~=(HQ -x2paA֠L?#Wq)QQ}'kl5?EPM$+;BQ3kD4@wmtv.:&aTF2hψܫepW#d1B-(̤up$%R -,W7,"x!YBO;n1!N!#N!kϸb_iPYu[q/ #g)ӱn7 rL˶q9OH=+Z8@J[DTmG(hْ yM#~멶ZpDk~ :FR!ca;+Z{DyBu?PbSƖ -+4zWit:%,!~wċ;Ǜs^ҵm%pƎ9@ex1_Cck @ŰI#֨(9t[.]+"8 ~5aׄ>`SSȞ/wd:$( i LQO$Cb,}jV:?hYL8(~aPdWl},#lS|HI%N)OgN -MT4 tLOu̮Xp>#`iNًrvhг*fѹYuKc)-q bǤ$im-wf-f~K|%>s:v -,kR|DROE;m0Rq)FR>>mJ Z?T -^"r!x|p#8cu@sO b\I):3'(›BpÑ~Bx6n%E?I!,$l/u!\U0JڨJ/ly6:{)sw۵-9a`)χKiܦ-6ahԢbTIs5gUs18dFFP/N7NLU"ƒxsx4j}%ZNd .og,xΤBЭqHcl3g/*6Nq#ߍ9]VD eٞ!o!yN |yfL$G1ϳ=U'E[" UqYoqi} 5o%'dK" -yLʾ̊UsKE&wyk7ɼ#ԋf\59WNLhdz.#-2ClvboFp̂]hζ8qdЄ&9fR6,Q!e}~„,=#!E:L,Š.\o 9iq.BM `Ȓ".TaY/FV2hREy y0)V8PM>ldHT.EzuU@u1Pl!* ̛!Nïh[8&E_3'@f= 6m_Ҟx#1( =(/2@a -uMGN;n36dbnXD[}+Z?`mC~Jb[G+50FId "E"I8Ộ( -n꺂1)O -f -yQ "jI#a@7k%{'/ dL$_{tCaG>޽@_tBU< yg#y64Y8\T-|< &s9M:ꆤ(Pw[]+m_}F94ip^9M~@@OE\Q^2ؔ2 & T[P mlUN*4Yaw㴧??s»L6^f OD?Гd f&M_g+TlҿPXWsq Hªr WqDg=C";+P{lrx*A0T<帟znnF(Nzivh,ԎLW/0+RPGM_ԵHZkn]ٜ{[D&tId8i8fMHh:@|b $9;/Kbt"C'CcVMӤ?)͜ OĠ`.YViP-nӁK+Nr;4&i~S(ݣmʳMɈ l=rǧ?|626pԎ -WvqIm0k:N>γ: n{vY;N;sYPǍnr 'ݼ5J,p=iP b0TWQfUFs~?r{zRRӁ3v/ƙ.JW+]N)>jŲ1PZ{AʅLעa=QQ+ 'I~ S lJyl^x3K;驪R"!켞1tzrBdw\laCp1MϚ %r_4KX'#iu{P_6pܮu;jTÊv)WEHqc$nTCl -SوfY9m͑x{ղB7EF<fc[P6?BWE2"IDfs+$]FA-ZpUkŭ [<{l$(Et*~ТKc#>:[iI١xTIR d@4fD!Kj[CYE߿2-t2)rĆhD| .uϗ=d9NZ-.1lXxATu|l}X[X 5khp姫IT1c'i)չ2 .J$OWm5H2[ɋmB)vP{9ݟ'==^7翌??Y~~|7o/_o'}s/.n+7qoُz ъY& :ǏjD lПJÑ^'s6Ӑg5Ɇp"bO؜T0PNJUe1[zu/-Yef}JN0SJ F;ن#)trg-,?D->dL$TrI^izZ$C72f'o>p^f3H& fuԣwCă*ˈqfRfΜZ?*/Nӕtc?{;E&r<fק}u?uQD,%Y!234ߟ0ny<d9!QoCI+ QG? ImV3{WJz'&=zz0ՙ_ -D2`?4ԌO4Ll.efKtf8aW6UaiY #4|:2K02FLʠR 6.= -|$dQGΎ͡tf%W.LŶC5UTgfC1 vؗsUM3<}g_<&ARL, l}R$5>bcCM6G"mB/?'jF>!,5Ł.yC7 Y:ez:98u`0^ ¡H#> B\C׺c|eNti04҃;GѤ%NHsg20;B\FaC_u# ɻ>7K<1S7pNΫsȧPde \Ԗ}gKݍ2WMgMʥ*tGPf@(:BA M%y믵8{Xl]RFw]o3 ;rpQ:Utf 2#C ZC)v<ۿJ"q@T>¤ ibmf -@qa#j^I {o%?׭}b>+12BD: 9@Nյ%Y"Ze'FXXWhtAɅ 2|^a,:T*AH:N6yRfH>Q(SkA4=h0^rߔI6ͤ ->g<H9A%ٟH9mEl܂mE(ϻ6TѨוcXzjg9g.yow1=InlaH((jvyz&OjV{ζo7x$G?B!]_Ɔ ޖJzRUR<2M0O6Z?I6.r&uZGURњfȧ_`vf3go$Dy(̖eZmX ;r1`$$ %ešٞ3KHbce2"IEJycQ ty^IY遞d'uAgNM[D55e2B C*{D2A!?ى@Æ:\훠 S7"bdoc59aHh@BYpə*A|eD#ÜEU Q`*gsZh`SR帖-: 'CT KbN/K VkJo:ӐKdؘ -谆"|g%ehj.U\$$) S-ZKc+e`!<:ۣ*0dȒ~YpzU[?|PUo3mqN ^<ڨ/s tyc{cs)<ʠ+,QՖK%cVst祇 -E{?NdI2@ff>#ےfv~ph=ZPĒ}}NW2Oa}ۛ$QK!e=L}y}DᆈIؚyd 35n. xag'4|m֙Nt:yf&tpBI[y'Ad NyKqLOְCDd A/U{XaCacDЋC52@B}@{|>UJ)|Jg6j%);& *p!-5ci>c'z0DnKL57r#at~P&ÍNHIQ3~mLu{i|lt`!_Kzn|1j) L'vOtlq mwS8ztMBElpzK>=њoY&KtDZ-*V%LiYD2>g nzbk0TS@C}N(_dR6;Qm -Phaval^of@{Mf:{aoP/6WdfƊZ}a q#5'O (,kQ4f,)uc]{=[e*K8"/3\@!_c7B1t>ʥK!*h*-뉊Ӥ P|~u@|ZgŤ} hA?7kS$Vj{M:1+\jzi֯k<~qn9WɰcQ)r'EK)CBTd_eDDzHmHjLvtCQ@VKVs7vsAKޓalwz:5!Hpmꞛ -k ¶".vP'šw۰oO6tI6۟@VJ-n+?6oRHf#$/^]2$t)g/: *A\r8Ub^T)$gĶܼF:+g_e}hK $n3J6ʎ/ ߉ET6[@UV;#;ZJJ2H.ˢ#?uueĄ~(6%^WS٨/>x}%eQA$Kv&yXup69&B=2Ԩ`R2pl^[#Sd D"c>pztq79d@gL(C P"ŠPTLfjq, $fRAcS> +IEی6+k ?PytSd`H@@FizO {ǤDjw +Z2TJ%S\ktJ.tp1,z朮7_*ghR[JVYڻy4!O[akp$t8%Q&}ibBZ {ˈv~VxBL5$I_sTƤg*&Adhi‘i*dwD:9,3 In#gT}Y2}|ϵRp-{Sb|%\CX! v#ýp ܂%!h*.?q( u(ޅ -g6 Lf Jj믔0"Ŕcb:/e -ىΉFa<)rfҚ18Su468+b{)MQ6b<Bgͤ PພSi(k\?6lz:I`Ic5v]Th7 -k0[~}[3" ,@$!lf L^c{D!*L?K!B1{kSz@w^jjظqy[(~$'TBUM}] /.Ux̑H!ϖ޷z4<|Ot0ZzՏNZO*.GxCXRc&yF" dLLP;%c[dX?0Ջs{&q>7VS^oԋqRbWjzx*:$)Ma1׎#RiP_ӼUD#$5?!x`>N)b0\*+2z>_RC"U"s{aCr1p+ o MU@gѷϖGomQ:P - -)0з|,5')rôϷ<0dK ZD&6#VÛ6v5%` -BbŪ3<=XZ)̥)a{<'0,4jhW!LJE󚸌Uj|F =Kf_tէpHhO!ݤ9&a2l{YԼ]NQҊiQY8n:\p۹KT2"я=L3$=^+0˺(\ٵA W&>,qlrW/@3gQ32@2g+1:N*Lׁ@I40uLp.uWQoKs½3LHf}AYJ2X5%QIP7=co&H$n.!\BefKe. ƕcKT4gsD p480tR4~sp2â흡UIZ)l*$YF& ,4g9R|I+JE[p@dq?U N: IR:ra899Id 5Yp!+cFLdj 7L:Ive'Qz -e dP ]erEȲf+rR=wLdGfBSJ)w<&C/eY[uK1f|ij[s? -, =O,KL<`SEB0X8WaY%e1sHvb֕zQP$;wRo$yLJK@-%/ 0DWSn.*;KӉ\E)w: nlLO3Y:J lPmͲok5x7>K< d8K0/\5K J%*n TZN&i]r5+*R)!z5 -M]yf,V!|ItܛHuO` -+N -E J$5G(Mskˬ,ܸ0=_మKKϒ^}s: bDsºP%cW"Q/hlE1[:ڹztԟ'H cDBߪCFH┧6ljWs+f=uy$f!']9U4@Sa%];l7=rbF9ZE -}ݚIX$D$'3"1P%/gVc(Ŝh-:]2&:w؞ygc&b^/mяzP 9S5#(JHl eGq" le=HZ*wz dt&dǔ.L"wAOJsCv& }d,ϲGmoNto4m)7LUlE(4 t\ +DY!)hiLK%%nIq1V r0uKڧ'ZμpRg G8)N~FAMTpb{-?*9".!*b #sɛF3~+aa\Q'4v3S`k؉)Rca!g&)ޛj{^I'-@/} L7K,˝z?=L,` %3Dgy r.V-,0FQuڋ:(wbKUB"$JF^uwC5vl䞆 fp %)w$<*i10%n\v.ilU*q6P ůl9j"%;@zhqgMxu)*R>(Ϭ?ER܂:k?~!8, Lń *P8)yu>i|aI-p:HP.aG4.秢F>Nd%LjF/R>JQ짶v Vʱ(6!x7إikh'<Mo -M(6k^Gm,B_ga;!S ?Kt>)y'ؘ6-!z؄31+T\&!E;+p Y"=s\90,*s yyZUU>egF6xc/!d),{%%@a@ YP]v5 )*l:M.[ad0DP8 sa]cؑ9pˮpuw -3q iWE6 -|eg[(^e2>i_+)ɼsְH# x>."I"WR:dz}ER!΁H׎i`06<3OJ[o{d'DoViv[y奓bןiU ِ}lL_JNVpbJ`D|N&2w&q"JyxF!I}U5_rQE%DgLޔdNqT}ATn{@aRΦ펞& -6$?o aXj$:kZ -7G-%Ꙣ0 35YUOm!E?̸:sgt4J`1H@gR(? -hø:5TyU% MOD~a Lelb_0#'1:& l]iU1*"m 𔐁 F\ߣdi0N}7I&,U(H -tfJ6Dspzf`HJ(ȹz(Yfng6Lֆ FvN@ -Ox!Wᜭ_3/uX/wпrB@ - - - -wrK{ۄDʯL}qrۏ0I mAg(JټH3灘\դ8zfB^[6vRvz(%Vdp1c8}.bp|<l]ȯؙn-yyNi"+@dfZ-u 70Q@i\s@QEbbiRtc6TRz!%'Hԑl|M 91])s%jl]XjKKnurϦa7$O\/NgZ".Ҿf5:HT#\!Dˆb=\ |nZNqm -a3Іod`i@Qp횠W-\Yr65vu”HYH نpz1ȉە7: E](VL(A.OI4'YFgl[>҈=]-DюË-SzWT}qz;D؃@7~{uh x@8ߢBm ljl7~A( 7%w;E^!Br-DLzDT`'vKi(݄ib2V[mp lK@<\%^ :ݨte72pܦ2Y+pqH=f!|%7Q{kdLȀq%v\BC i$^fyG %!8>]-XHtEA2}-` KA ^>#E+#$WTE5UgX !ܥJRteꙊWCC -} cҊz+hb-=Ɯ],یWJ7)ra5L\dM*,A7\1i&Ӥ[LU^LwDB5GTM},jTъ+g -vkSo (Hi:5e OV!YZ|E).lPML$qʲI MFICjQ%[m]ʭ Rrk%]U-Fwyo)G:z3k:!pN\.y ɐLmEq2Y -k!w!lC:iK_]|eeTۗ0d4@X069d|/@v,Df92(.=1~^rpIڢ(.` z!xnԿ~_lLxţz/T=A4 %!;e 1F@A\}-^L&d= 4G㞉I5{$ǙxY͔ R^vчjB.ő8eډ(RG<:oW"-đ -dbGg,jp_/(_Ld1HdO6X(g,?'5_eۊ^f y"rb kEqPEϫq/*e`6emq_bAg]Ba?f+FKV @/eӰޖ+mN2+A?{,{e?$=ov@$|*`7T79crVLa<4쯵jIr5N'G*L2+l6DwfY&"ӔIρiW| -hSGʎ)uAq6mYI}ӻ2SV_.q\~:f\THHvW,;MO䮃x 详 IgLFPjCy(_$k=*wEP -XjUՔR5/A*gkUX7ȯg\+ =e&Vz IcbdUK՚R/jo ߷YD+x, 4FVHˢtXbG@OK8K1ՐnJN<I@& -b&4&,%Kr=@^])D 9sk-ڙ0ѳh9P)]Z6hy , mw&,{ffp_e,YHxg'ђ8!R 1d[ǾGZ=lQxzy mpQ6UǣwZZ4濤UqlŨ$aYq*`܋۷. w@/zpU6I% nl92ds{&[>3aLzx9!&k(pQ-ۆ'h [m0ʑ*˗͝E5f@5Oz$'8Ew$Z$)ִVƃ*`I^IKu!Ȁ -Ԙ9D["=|J.Ww"DD}8>0:-`sQD}q~j?T7,B zAіa@] -SHK!S+.Uc6ӇwÃ`] _DNw=&Pdd [bD9ō:ITZj~ۺp(xw -"" /sC"`.)L$!16)Q<T7: ZrlLS$N+xܧW2"ĉ +f[յ2_-'*AZ+hPdL2l6}1gPe`e KW[º '9P jLW 4Q'F=-|3 .Ť,4TAlFD蒀j"CIfprd3`$B[h:@n ``"ͅh^ kem*qq!EymBC*g3z.Aڠ*CH:70$WC(|MTkzi Ӭչ2 mbkLHA^w͡nZ -&6Tyms.!"[Rzj$Op#`U0!D&IJjp'&jăwXve@iWlP9S S( -F}\+߂޶.iHύ >`j aFUy!Q@kuYb.O.(-x4Ʀ($ $# KPGg~d? -dë ygbNCguxμ+H˵ c'hIL|gQϫt&%ӕ6,-c%֟|vXore1 VF,yTRޞ鯛"c@j eM\)+bjֺ"sV:/ Yvg-au#T,X(ӎ`FAtVd6ĈX{Ĕ [;9Pz@'hfs𩳺tU -=PPeaTJ;\{}+Ψj*. .su*]Ё4j!LaMLg-2`fsK? /L3#n}F|Dlj>s|XTp9’, ->FY$ ̑,brn:f_MJubљ5@(O4O?j{G3~?/#])kX5VN 0)~̌4lhMȮM ؕ#4 a'RE?zT>Iv y^!@]}OL2 * 5ӋrC;@ACCtAS$BJ _+.-,[R#Ru7(diBUa˳ JzQ#2!{LN[j剻M᳜>!8w5f$Q~*H(0+^6Ղeg1N?ζ0Q!/N2-jgUϲODfU4)$Q2|!0]v,R(R')x&;!|z"#/)Yvo4,)R9;+^:P5m~@][efW`LVMݦ4񈡡kUEԄ^&.q-: CSy%3Aʦy>ijbgz%P;[gS5a,U\d!AxfF2[@<_eS:!/ /0a`OZ?K%d׏eǸ ھ|$ ٟ3Ǫ4#U3,>]tN;d!3Vxռ. H,l/CCG[i$8SxR,+EJf^c۽;NBf!k4i sC!;1 >#*cT0ĭTЇCn:}굩Uqf0 `OFx“e<)SF5/;$qI\ap'G]2gۥZ-GrzAHE5e8'\xw|^0)8([KqJ^iQ޴KLzއ3*5Ŗ 8egrIRTjKQ[,f=+ɖbs`?R4b#sK )gG&- x@d Z@&hi'tȬF4y sC%ҀKS##1OFSF#(&HT2(< ɢm-G?8 -ȵ`;K3g9Q - - we K,js_Rjs\\N~JK@*hAc3@d KIz|2}*wӟOP~k1%rVVeAxy F.Wa2]fHlb-d)8I^Y -+`YoPT~5tz -NhY굈1FjP XxNByZ`p6b*^Qf-]( d7# U:s6Clb}vHHgvcf(JZZ w3KM01fV 10 -TiHn {Jr)!!y잓cHjva!R,6(Hjٰ!Y:Τ&liۜoR=a5-A5t2Z`\-,i8=JyCD%24QǓN I~iUlXFٝIQ[.W^&>ʄʿ}ÙrKݰys^ "XcSý20+)~<`3;evBs JQuYG^2ycCEmWfm&хil];'j&Gw2z&+X \iˆ`z3} 1{Qi2g\?xctٰBG*/ؿZl!xaؕr-K)JSj K CFG:t1PA*7#e[ j4MANg=0 a0l,6j0i{!c6iL.)]EvoVT -4AgCg,!kGnżlcgSuy.yO;j!qvAW:$J()[zx-#F MU4U/%1iZ" 9Z :b hvI?*+p06 -k Pp۝[^]cD*֙R☸4%Urӱ͸rxVHA@ԖlL۸!* -.!ADbʀzmUMtr4l;rmd&lq8л/ lS쮫vjC#|Rdc %@|7Q(tE.dT!YU*]B%!qJcY [ `ViBo X -}x]\NZr!I,k)Hݐ /uwQqNvi=ȞŒe.<FYҢÁ BpIe Cj>u4;=VBy]o$b%^נ,Wz$l;%pOxNm^9)T)-(N<fLό*; \B],-e'SBW!5KFo/B"^ؾ`t17 crTt@nh;m<])6<;_evGD;[^؊jh q6_oD'Lnq-*4||aׇ$8%.5v6zO#I^d'UlJM&%hž@plz\6U}n&r!^.h9 !+hAkd+ KRgJoA>: '&v*o:SsnZ㱵Q]"MUM8Wm$WxG̺<}mNEJ3:@7.PMiG"/3A sAesӤ.TP^#)SEtHM[$n^G-EtU+˪>Tjp{a>YQHI%_<ƙ2'|ثl yi\P EsX,U OEsFek Bp]>)/L7\FaۇM](M8fg}D3!E.i_]"'Z, ]&843K&am'0(J)Ti?G7"/U#I_2j'H4cFxVl#N]Xlzj#,`%O.q;KRĊC^&gP݇ن*B -ߜ!yJ`MS -Lhܫe7 4кLw)hQɡD Sm#KE~@ (\s俷긺&υpꯤV+Jԣf -s/}D.Q$n^.td[b.BZFtuU/G՟w}E@W 8qK_LO zb%۸rKx)h& U0(}?XO<.GsCE%OA"df)Xb^zp}OW׫d ̑T;Z+S,bJځnL34eֆ:jʉ<`c#ojٴcHZ!vwhNY7l_P"+`p[URX'>`8N|+i - N &haM.>y|JB>(7]VZ|˫(Ttwi%0Z "%%^f"mRRTsդskxMˡ> -la8 ,<+9=Ʒ;SҦb" u"_ztTBatO Q3Cm?w? TJ5)YסBAЃS;wy4^ ^ipƼ1|2 R?vj8DjX &U 0A3f ]24$ec@elU_zL{)evf5RR~Cbd_Lp/_q@kL:X kq!֩z')5SMU-=g"&yZN xEJC{$4;^xp4>!MaJ -a. pT"?= ,IB!cti9AK;.!p;Pن@%*ܔhZ^nc8&*mrfQFsvpҮ|h=8!E&;dZi?8P:U8d -H,:A d>t֧s9ud#lr+ȼxbK0I Drtkr%]lzED)rග{"sij?D^@™ݮ,轙W{cR|SRnp>BT;3IIz,Aj^(>;-f^6]{&I@=O`PZ' -ٚ\bc%eT}&M݇$8<%y>9>hDž\@WH8>KܨVLԹ%K"K]fR/GfX=AL!ϮBm" -$̞D5!6UǙקiIh iAp,y(b4,n jc>%ҍv~w?rQkϪ ??Y~~|7o/_o'}s/.n~x?'_]5o/?o?o7}6?6s5˷Ïދ9پwoy:BwO/_?/7οoKyo ko}|_ϑeG/o6߿_G_oo+~RI?g-Ȉ_I6=}eW_R 3g6)xZaiYcQu0!fTZ457̇1R - 쟾G0g9.f{B_5nP>䳏NK)!HG'fJٚ1FMxf'\5*;7.ۮoF,If>2ro/7n银|MIWo@f]xj{"}z En qE#~D8[ PwDWno&5Ӿ;rKx?kٶu}/ >V5w$>{0;vB{6D)`aK-|AJ񌁷-cc> IQ1Jj\tν9 Wƶl?Χqcai~C1vYw&/տh?=?[d9pa+tf3f$? gj* ->'ȩg$cvC'f[ћli4^|3EgX>Ȇ=w,S{S1yeq\^wh`}0TOvA~ S5H//qNWT3ELs;{ r^/mj^kөcĬ?C4lѿΛefʐo(:x{A'`"أ/ d!bFc3eQ1S['S4ysm%PzFx3j2NcOW>S)cB>$-e[֫wѷ|%`R?R~qӾ?wjOYbQٲFi5EWp]I f|l; PzN0WȭͯB5IĥM}d~w -6WqcgkDNziq3}wDp^]=Y1q kŝyL r#~:dzu~u<ހ  9O6s;'owy]1R(ޗbm}g}S%Lb~Ude-Ǡ☪ ҥMn ڧg0d :d@u?߸f).5Ӈg9edEDyֈ/g4S"%|ͭd4*]/Z0us>\YXr]yqG̟_w-zw[=ỉr>҈_"5[1?\=W*K`zu;f?$ߨa?%E 0~B5oeG_e3O(P -\t,/6ݤmvq Ǿ޼9q[_5w; N;O| pz-{OĨ -!'̏C91frX5]~ -?Ry{Em({}T^<:ufL'z_~8W`ۀXבmD[6ڇ0tȾf@}VR|yDžU`ھV}XjS7ҶN,HΛ0bǯ(1[iFxW ǽ7[C;=\IVzqYb7 ;<b~ f?qXcEgo^n61гB -E,vwr~u68+v68cU19i[ c9\0m|faG "w\2TJ"3o+Mj%ߎו/PW{]ɟ&)4c" - ܧ[Lo߈ Phz{yK[{LGA7埾g8D{qQ Ƽ]aU]ʙ?QQyاY:m}7辰h̾߉ygі// B?$0DԜz4ۋF2w `^kwhNwjFڢ|]>A w~ر]$OOuUK -wm<ټf)+U$֧SOE;iL3VnRR7^I[m\);nq%. Ae{uo=.ldӇB}^cEf`z/^a -1@1VOpI X2Qi1a% -89-uط6 ĥw%kOY6&y,-͇ԌLѫ5wO Q⋙&`[}'9~D3ĹgD5kBMϽC7il)v;nUhY"Sօvey c8"bữvGV#JmBE (rH%%<ư]g(E.Ё/٪4oܓ?{GA=y(pS߰;#wrP{vQsBQu޾;Fim{9b37?bos2R7:4! -,}m-+%6+pg,3@oӺiE =boqN>!ueDkk+}ȁ́:ҐZ>DvXm쇡U"rk"؞4Fsi2*]?c^@#HگF*ƹV@慉 ˱[cQmئuɡ6FO[0u@}d~2OE KcŞ1fvΈ&;9cbHuXMM_$1@htcbBcx'nf`[| #RD)$_IpyhLsJ͠\yj[B+L۸aWjq&#kcr՝r_)ip4V{M#Ǭn 3o*=y*Beb[gG2`VNel2p% -{ݑj ,+/+嘈rwr o:YG -]tTp>]D=٪1$gqYc'( s2ߝc3|ܸlH=:,68]]B>@NQ7㸠[޿DK,Hya"ܐ gocGm+*2\lG-9J_a)4q4FFgZ_ظH~?T=WyS -5\v PW?.\_IԜU#lS7ĕqiLZEveN4 -݉P ӱ럋G=iͽlկ ̼dznm"ڷJ܉ =|98+; /o -򺰰⽍=|AE$u\¨/7Una^^Ng#xE,-$a;q̀ ԖY-FK@FX_^G8 ԡyWf.t'_K|~/KhNjWzX")]Zyjen j -~1 쌶!/{xDJ爵Yx?)wiq9F|ۣnQb\b0e(,،M[Q-Cϱٚ}~0L)yo &.)@OI5~mR!8[@Z6Z5Pl -~Eb=߈uXh -ౖH̴G& HRvw4{75x63;:"#c.kK5zEE.җH=Pq]@5DJ^#X8g}Z :z{;T9GF12wɲ3}\^Oc=Ҝ&(IGa˳DrkC%on\+"c7\=Agͳď(JWB=#"a`ji[BߩݥE`׭b=o E"ޯޣ{֨g<5x&ʘ8I螜bNQvs -G{vw.=ddz~ISۃn-F*2ѸTczc_[?.e^bjgjAY6kJ;jGʐ7=t9v$~nwstWy]hW0Fm`Ջz_nH |>X3E'7 Z?.5vMrUcpe-B'hwThTB6_cֻp#h kRNWaj[xPPœ4^ݥy諼KtH{64_~gwν;wfznwm7@rEQ"E9#,˶dLL$P(}߱P:g?p9 1Ubdf,0r1i]j/:T=3|Oh,rG;- TMA%skԦ=YPY/0ki$1.-l$,@!Lg0Fy{M˗

#wESS*TZeJ\Lr - K6rl2, ²AFV9.f$ϤVs>m/%NE@ * -6Os"PFA$F}VbB:* I9a)*=d&  sК_X^F~\RYY(P+2[HTPC’IcùW~b\,.7qBE*ptm2f֯4ެJL2A) P22-sQ, -ɬB -MNF.#rdi(D1KLf#.)L5W6ӈSB:N0+Et$6qRL-25wt'9tDtJq шŊ2IJ\e\P(Aa2Q ^HeRF9HU?wi sKf0}`d;=LJD2_Y;| sմdr$YAU;2򇙐@:( vUq %3ʙsr?^t4cOIl$WVU(0ԡ0LEe{2>a;%yeI\RbV[8I{sBQ0TPNˇP4mj#QdIaS6&լYH38eOY0` -epepJ1 ŝ_N"! -O)$C9RRIh -F=Kup0ajއTNj&ťeI'aRҋEAR`<75UJ'wYfT:3B2Qle_̇FN;;`M4nI1 -% tY\FHd{ɟ6Y`RltV$f@Zd<AF - oF&&1?4T2x,LaµBҩ-V"_l 7- t2(TOEAxVl>+:+7$+TEq;3ܬp>G^Hxѧ# AdSYj bXCF%CHHzLF1ˤK^l$ 1rn*ba'~s ̼&UQY2PA!+PFVV +uH J)|hrJc%(\Rqܬy!i+sRTK´"XNuB`Ÿt1AhJ>\!1J -r*sMɵ\^J6u(#  PRI/6zS{Vh]YLS ;ѩD2ČB=!E 2_,ƨQR*ҋ9TYh>J1rXjo"YC{ B*E|On$jqK#3yug?둓&>wBRJ倐u9!j}NeQHdhDȣGҐrhUeʼ®T>dI)ALd.9`Xx;u lXtΓ3r%JgTP/Iz^+'$sUgdJ!T='LjRNfIIkK+dGo?#b Կt򗞪J -Ur} .^zN"OUҩRUuFJ yI~rC#OxWASNߓ8'*ToL%{$Ga'F r -~\Ş"sdGElq]NUTTڑ!I -됸 ǖ3ܧUR]9 -ʨfTϟ8eԙ9O08K\d\Tv<9'IVS)r/WFBKIO|/Nχ[NdJSA(\rzQӨ"⌜%SƎ(~Didc=AʶN :ШH$sUEaI^4 -@.ΦNa?q%]+\ t z.+kftZQ1] ^VYJzg5W1U2|R.PAkInPaJ3oeTY͏P&8xb.U/+gu)e#<#QTf;FRR›Q:j`ww"h sdd7d!MA)իЙ^X@#}T8* -`ɺ=TQBSygvL<䌆 +s_|0%p E -=+q)< TU ɢb:Z ơP\w+NY]h&DN\9$hӍT7ZJu3TB5!.?Cyg2ל $;A~SfA2*IVA>(,%=Ho՞b0ʣzхU,j('slWQY&BR+ʊu2\N/-G?b;L -P4 -RG -Tr -1 q{ -b1aVTX3Ksux(bCT.+ɳe $ԁmo *BNe+VO /e# ߋ?N#;q"q /Ҩ"TQQO 03߹/߮xA+(VɠP 3WINw@vMa{cUedP4c%d_"dAC.,%+Fd]m'@-;GGNIۥt8Ⱦ''ك}*C*9\iNj|miQ\c1=H!~QXj-TKA^밪醰O= Ho>BgNnھO߅@ⴽ⪷jsGޕ5>LJE8c›jC`(z>!,D l=b;Ol w` Ԅ皷Q6=$O'ɗ)4bRH޿ P85xԢGOWL=mqԽ (L.|dg;$w#*>#%,͞{爉N*2ZEOF- -tbI_觯7GnU?p@<ۢ\༅ʉ4t+s^+3-%n X\ |PyՂ;s !2~Q Dz?qN0PMZŏ@,NEcJ>UeyQ8ٱʋs8)?{oh3Q-QUߒ|;'D/$D(|$,#&zsFPV RK](,\:96~iNeh }zO E*2P*{ BLBbRKћ1PE`o벲_0إoڹ{fi W0L U?ܮ -=C K3Kۏr{srx`cop{t㩷/TՌ#-7mNoaiIj_<.e$w=NE羷a%Uu7cv 6nO)7'7fč1 1[X*<sZ1VHV6''&Չ9Un1^Uw1bWU,}:u829,Ώ 7jx%bO9nx&:g.ftg|\t -Ҵɗ$5qusc/69::{cSC9amҥ:I*>TEf'_=Ħ%v}&Sʩ[ Vqsmwxa[gQquҪf =g Wo.[w/;G,i2Owr`nNm>YTO_>[p2`L:m[SG^xhs00'-IU@+۶Ͻ|pԭ]yt{Bz69ka& HxuxsVٲHq:Ü[&+b#IkUX5+:mQ]fmԢ\{x廖?=젇':%UEʚF˟`/v'쪮}/ړl)wxK'zQ wII/$x' %喵}zUv^oL -XCE޴=)KڔͱW}eoL5@ϋOśCi_=lu01=N?>sC'M*: ^9[SI `>z RNe7Ћ I2>0`-Ky0lNK%$CD)O/zi?Ha-8UW#H”ǠLNtۢxӨzM2'a -9 -ŇmZjZ*d03 _~&咵`ǪJ8gۧ[֎s9 91.Kui>fe]`甽Kw^%{ʫZݿa=Y;7ajSA%/(a7ʲCAVֿU~$}fSġ欎`@8$M1ieۘ\yxġ_ջ᧪ؔtb{<2>b~itNݽe_{J[H"|6&`֞ ^JL J-I6'ܜMљt9M昦ӱs򦵧%u\m􉁽5jzHp`o_ݨW-M#_{;M[ w;g0eld Vtop^Ek%h4ŝ֤]ўu?5]n:um(K @,DǾO- u]6I©b%yBܩ$zA7 b4@td@%x59vȥ" gg͗ӁĸST؇9:>;:?۴4oZD$*6攵~Q3a$<팗ߍN2K17<'mZE =aWv#PCӡ; o_ԭ'j; JS);f\pOn[_MeQs?7튶GԸaH =]D` gЁkߞVǝ¦_O}Q߲}'ןHVu7Gٟ]ƴSPHZp)O92 f7&-Tn_N\>QYC8kNτ{6ᅬۨM{ʨCp̫'OnƤgӲĬ:i5Sޅy ʥ}M}y6{Iq[ñY]r!tt>q]+i [zcNY%:% .0Y{}w^w vαm$Uڰ=hl -a_ -Nlv}GشÎҗY[D^= -?W=]~YiaAr<2_ؘ߼||ɮ7'6i#؉UޔvנM& -+=ބCڈ;]̺g2IwǛտqi[X;/,o`a-lS+Hص lh!4dkxPƃ~#I'愵vE6=Q`, })72;;!4RJJv-* 0U&| QƭNqf9D%7sOt^$QN5 H[nEβzzA%enDQ+0|:ͯ̈́mv OkzμZu联5_?m.y)U7@p*9x ~k}ok e}ZYz|ek)<{NXp:Y Cڼ1=zBް5;dܶ .>{psIヘ ٹMz19U+p kҩfGf$9Vα蜠:_tsߟr)6^նs4<'8)ӵ4U{CѱzzVlBx22*pqkܗ̜n0:ad"2I!o[6~: 8#g' vakTGO?j+{6p:)e5bVqڳu_/SokX/?-?8茴w8Y-tEG"G%f鐜{n0);Iw w;>AݛE<;/NMkӽמY, -0{3-<\=vu(7<_+M:30 Hz. bN#Cx6u!jw`~@ҫ~fg`|dY(U:{2&7PߕN^PIpSu[֞rI ^.ĵ>>s"n[G1A۷-O#Jofg ;􂜏ĒJ/hq/p"t77)#vӁfC! 5]ٜKYΑw:5%@Rbnj۪|?<9b|W}2 ;G]J֏^<:Oy {edNZtZtc]¢hOU%gցN m=#grϱeшW?ΊG,s`~F;+ou]e)wڎ1h#ܣInf­Y5)\hsE홞k'~~_헎_ڽvavB3cC)$ʓrϷ z><}n O>n|Ń.un[k-]MOa,ṤS :U,4؇[ݞᝌLs'@WG{ٓ.i§SU2?{3>q+ -Y~*0'⮔WҖZ.*{2צC,I7r s]I(ؤYmzʖu gwIo@}7eC`/u0[f`?cң/zE -p0'u$bcKx )i b0`ʸ]Ttk@&m؅gD`H/ YGx`G4yX3𷸿g̛3`o~lsܴ>1P~3MMtuY/>ĭ&e/[{|7kue{Y3}u|% F ?~vzq1̦J{#X}^х?x8#IQYQuxZpr}qMx.6&UK/IdMxnot? }ČsǯS'fSv,4V"vK3>xg8a_x..=ӭQ$\ҋn$}#fkIa]exgR -` -~.23^5`)qavQfg4 |놈Mx6ׯ\~~tI6$fB9 6c<`07䧂zm20 Zܚ,fosO&|йg7|+}v^!,rk!)Q|CQ:`G>KDtP؜N 3AsI["6ީp-guC"0x-qJS~y/w Zu N -̀c6ٸ]ކLٞ3] Ѝ؀7 kWO;"`gRȏ ./=/I!;7"pi`L0Z4YktPJuKsix4|;k1#չ>Uu4 8+㎫w]a;1 hxhLM}q91`N:``ϲfXU 7هblnd(ͪ;_>xurhx eY}Xۥ[9xpjy +ؚiU":ݽ?<^?Ʀʱ9ķT4pÕǭev~v4*XWI.d0'Ni8̢MynOqOk6z|ZP͉cVis8Œ]+)3)#V]֜^J"<=מDR8A8z̡&@?j(i732![q13/Lϫ廋}@Ú6^꿼;oKԢG+^]0_J0{W'F9R;7F=qQp]ԩǝ&M;tń՘wkUxJϫq/w> iZZdLz]9L9ׄlI#s%N7:Uˇ+6y{J\|GiF XMW|\RY{pQ3AUڣ<k@ hĀٯ^VR͌\ځwȺʴC/JX͞/wЯc'6U}#:Y I3`&]Cܰ4R>9r߈865w_G`?aWvg̵q=) -(8E͊ZÊJab. )Q ]B8ի2c,~/>-n3Thm, rkr`dkNu-jv賸O鐒d-VTHI/*ߠǼ#_maV -zל WBǫ9-eWI Iu2R7 v4a?? _9.FFi7^^Y0d>ǯ \~^OzA2;7w)i9jنW.OV-:vp`=H.8lWtzo!ᒵݲ֘[ё@qiVt]ڜ1K_Of#;ơZqf$1~;k-|CqN^]DϱGts\ʞMɛ'%5+z*FG\Oa>z'`DkYJ:;ǚ]j, [3šWw:+Y5ph9ͱʥj~vSIa灯ğCz]pGXĵ۳̄SpK8%mox@B~R-k[I[/f; +ars[ ܞ]moLO}bjʍT݈UކJ*:b)~ :ckFkޑ/.;<-=-ڕ%m:NNύðO$I?>ܳ->݁gH8lO|=Cb(ri3x9F͒HZoc,aU]ᄡ jVD>Cf~V&3- S|!2?pIZ2{ Ziszܡn/_~ug8^RI]{gu=1|qZvVz>o昸9;w9b3"6${fUMÆovg5[U*bgiVUGԢc9骼#:w+u@> %9ף]Ax:ҊN /Xv:;7`؜.*,v&mN ,756~LWS֖rYɄ{;lO5lOw0^߯}ƭ&]s6ս`:S.hƫ~ČQKܘᝀX3+bfid\PEgZˤ_G {E/Z4 sB{#=@YlVjgYy##VSfRƁFc6-{{Z\>*8z{R|:ޏ^|SV\Ze SҳYE֔~!@x\zVÂFp4xp]3)imxFrng?X4:-Ot]ֿ~w^|]͇4`mNr;*2~Sg͈b3;v|pj/gbW>}¦,*NH.@*oL:7c+EʎOʖ9M~g8W"߉ۮ=q{c}8>?IxT[ns m:nNJm>{Bzt p].|C;Ec2mO~ tinRF\C^|^-i J0w?&m iCF%ܙWpAYf9'yUmD[RC_'mK ڰKٻ* OY?x߷aog|rNH)jSsam}7Q39pŝWHMYƘEݱL~>6NZq7bYkeR#Ëzs:u$2x*(+cYܩ#,-Iy5qoI|-شݰkp8u~>{FN˺K>Nǃ΂޸.f [vUm㜎Jo]}(vNq fOyl 3$|zc"8p#ze 6ļ);S~kD8p;0w Ǭד]NsbAHrA%T 8>`bZf~ UK &, Y4< . BEjwIu+[|klk* -0 x213TDuLšo3K_jwq`}ZZYBAs6~rg|D%:$ |=uq^Ov~pHг[ w{i)g`-nN?ٚ<'~Ĭ!Vqt[iO6A527]Ge&N84ЭzyI%U 6ٞCJ7}gq]yrWvCt~s xeݜQt>TǭG;$m -vq\;6'iD1ɸCɂt9lvK̋;vC -R -Q&аÓ踶+2%Iz:e&j!t5fkR׋;|1gӄS#5C԰5վocg]FM® s -^Qs+i](v ,1ٌ|; iCGx+Vdࡁ? t?,o7r^ֱc{-4ssEN.GJ{Ȝe{Nݽ>-mX<sNj ewv0wAc$=*`"Q -ԀZs07 .kĂ7ԧ楽AT\2tÞO,]>~a$(0Jm - Lye+$ fUq p=, !FwYҴo? a6XS &X#=g]X q'n1pg7RLHֱ?>ـ^[ѳz-:+i07~cwѲO^鰅u(!bNNuE4{j~%bӮtz>ڴ8a/-)eHۯz.F3kcۚܗtk e=cwNH֛:yG{澨[߲H.–>=h鰋}5$egjfqnvID WރyLP&J%`gM=n߆yߞ3"֫WvD+sii t?(4~ _L oYn,4 6/Gڨw&LgAvy,2t7w9vgen֔g"(|_Ɗٺ^1$veJ*_v5jfv]̤Kֆb(7_7oɀFyxh&]\P"N5 vl -\( <,F!cnHN\t]<0EfQ@vH iwjN68p3_zR. -AQR%n†w2e3ޞ3vbkFۓ9T@) -;=ˏ<G *$Ɣ4p1*>҉y' uI6BC}\n}7-Yɔk[7pQ }Ax "ƽ" ʝt`|A'v+>]e‹w[6 ;V7 .EǚK޲:%ف>JxW#Ϋ%hMN -O9Χ|~ 7F5<0-1?F=v%fz6`ƒJR]TޠFИO+12G^]X,jFFb>M.)7^OK듎7kᛈ&m IxV{.>\‚=*>i,q{:'n|9:Gn+-|qvfvTP{hl_xoZVZCJ;AaN@ܹ>9 /~Y37Alcu 퓴gZ%|/N - -܆#QN[Eq2o0!.Xs襱9U{ܡꊻuO#n֛r6Q:#sR*ǽ*6qiؓTP.M;Լ^np)5v%|W=w '@xxnG -- [U IMdv/2ӧHU/eVF[ˣ⺴ͨ 5M YuYأjEv^rA)kYy&ޜQ{)ol|ܳET4dGgZ58i%7x#4_ܙW"v  6F+oqޏ^?e}{qC'=#_|-yJzB߆oܞ㰮_֍|S0<[+99'"naHo8͛=w5 W "Vyv -ܓWo;`.yR\)O_%Y2]I?7]>7_O/Yb^ҙF1+:x^z  "@Ǩ"4𮄷o(]M #.JZ-=Ö ͂+oB: $3FƯe'n?iTyGY'Ma " gE[cci]0/Oڅg#SۓIskT|j1ͧ;0*{x+f6lX3= ZENġEuN"n w*u[0n";s/IN|,:1=k³š9Qڄ֤t{hQcO ty"n4;..XoSᅈ 9;/oհWd(oЩI ìIDvJ}Kj„հ_>Yg!lۓEs{Nw0U7p`\?Kxp&w^[b wԯ}_U.s.(kVo%[^Ozdb^.dQK}pc@+NB05Ix%wwV4z49#OyDzR:ļ$l}il-ςN-z׀3Ӌk' L_Wpn60 ͑qKz$=ΰM~o/b -V/nIYN*|e|l62:%NGms>{ݖ?|;;C8%MI%!6'8Iڥ3:%XqAtq=:-Gu茨:<;5=1޿1)?S־s4bºZg'8_߽~yq -#0)baܚؗt -gE5c]/|\49i>a6'xGGY'XQnH T̆ʄMٹ=)JIvۧgmwލKϥl)m pߜ}!7bVamdk_|8N>CO8'k$dCGښMy nL}I~ql:e5vi'5Q4Kކ;%{kd3z]*o%]LnRNᕈW,|7!9;ilN~qfC -~9ӁKLJz^{glD9~0/D!}_fQg֔Wpp`$`WC_-W2 -KjE!m,fQNC)i)O~F+2˯Ntk3yMpbp R'yG1;nIe-(4=ᅥم20)94Ȝ][C(婀ACAN95߲[cc/c y#g˷[^//kz7<=jַzҝX.ןzŭgҪWxeu|[5*8|۰_*7]רּOObv~g]/u39UP9&8˖o>HYUILbߚ~[vۊ"*VP@{-H=!${'$J脞}9!w0>3k~̬g -طcY39">廚e^J&8 -0;WQUYlb#/Ue˙2Ϲ2ę>S & *8<̠NIgW|Y91Y.4Vx^5b|>CL) hE6BSNX*Z<<zYԄdg[\r:KN?Q~iD8~45O.f(Ȑ/ 1REHH0cMΔi6X^>.P3̓(t6y1K>τ @=]r'8Y`.\MCr&9-6P@;waߗ(l~ -r6OEA6Xˤ74"9>d GF-TA\ % ˜yFF~%[ɺqR&LAa^fc6U<7:.+j!{oadeE&% )E9ؔP͓VT~*EH/|e |ឦ1Bqo`(Ͱ Q\ȑ7F>3qjzEpmhiֺk/5OͷL? bN}z, -) f9nSXۦ1(_h SMjK?EJ:S~E&LK4g4Y ~)*vя2 r챢c3r6?lz%-ҋr8S)k:S3L5맖Q-.sO, -w0^e=IN:'J  pHUNbLHVn|6 ۦ$%Yep})&R.w\MV: +uyFA6v7I;g{Ӌ'BCϝϺDNM7HQ*Uk\%. [d@lS5!OUdKuYw;Xj}ڜ|:sKtGe|,0 h,~~d9Nnn68*!$^q)oTk xx =繮3YʰQ]ָE6&gڀ8Sÿlv!Fȱs<r42\MLʻs;ߋz4Kj50`G|zV9E;q_0_1_wBv'hd'F[\qEFKHIq|`޼wra}97z;C?<0 A0.`U8j%~(6)5L0f86=W? {B@$2\zAG Q Rm8\nyۤž6oty=@I\ \ #lDY\d)B\CTArN)dy[*EMS21!?٘5,ϧZy.2}>O)8UM\vh{\N}kE>Lbc %h4zK0.O=QM΀/;Q#bn_VWRC~c>4)r^i}9K{4|}s0 ɹ܇X8lGm8JɳM١@,ĘP]PE6;#|f+nk-21t Tb1$.|$adlVkSV&?XJsXIu:.ÃziV;8u?~\m59G#/<҉ H) qlo{}%뀗\Kvz8H$GX(?O?Lꄼ]{R\]$=HiG@=%@({w]WRcBMӭ iT\ z$F׵е=/lkd? :LJJy47 z]hDH8.+.aLLyya:t+$!%>v+٘B]ϓ3)bWz15LSUe2u&IQOcomsnf8"Npʑ?4_챜vO4\fO׎еZmv)<&?x?m1;ע*{}w8VגםbՎ zbdqWq!Q[h<\eFDݺ/7wrNm -񦔞rk@J=5)Hp{Z^zk벞W9EiIw'17d.h(5^w, Gc/]Ix8,…-Tbv;SeavZ.-EU9DYV79 fΏ~{}(o(5au|;c|-a naQԷ']GΛDǹ'!dqYo]9wo  # cz(AN7#J{Xl)塸X8Jt c"TY/+s-Vf> t׵Wz]yiqN1|~hK *^a̷k]0*^c!I!.O!J3u:t/Ά)C1\5..i;tY8fegm`VZ6ۡ=h)aKbÔ JFq{2a>륄uo<ⱱ/'IOZeL+-ր<{7FN5c|:Nc o(ɯo ҵ(Ey#eyCC7 zkfk0o -!, bZs_s]hqwɸIN-ĸ8gL-zX|p0FוBFB\?ybO(g@n1I1+Bn7{K5bS^ b.T? -<.{ўALkxK6^"cQ{]7(ߥ&;vʗ EN-_IyV\?Xѫu/7reл 1d4M -9^ƿ PB}~Gh+c0^)xXq,g "Zs َMy/檲Y fPU%w[рRSu׀n9nKQv:I?M9=a'shZa <Ss"$)~7S - x8368lLԔYnܽxaӍ ERޑBFXڅȹoNdГYl1#f;9z]BKKOr퀸xKI6 >Nۘz,3bw1;_vzKR|EuSt_HJarwtoxr܃ -|+CP;<ɏ9f_sW-IM/w!dRsne:!(2PFd2Q}EYI׻`_?LyqjX>f&q1EMzD~s$r3U:v'>aw 0] i~ee%Eҝ^ZЕAtjwe{4FI-d4_L@]- 86ypo܈d:a"\q,!X>W= `1>lޑ֘Ty,'ꅇ\5ücMuѫNj^-{mv{*b}suHI.b:{[&9G̼V2穒Xx(gjFFE+Ŋameƣ9ߊ Ѷʐ]?{ofRJuØYAyaӭ6"bu;=eӉ=!F`CؐQZѹX%? WVx69 = STIY l -FykSrJw큸GJ}~z^(*6Kp!LEʃYAÌ `!ÍcUviFA.6}ىKģ 2] cD8L^N8S 8.֪# #Rmu3.r0'*F%n+#1.fk; -:- -K /^̋ u2ךmg.JKn++r$$^-v?&E##A~ g۵L!RÃ:nFbVbNv"a7{0 /Gc8u3֯9QӃa߱Ypvy -t~_Up$7]{k )n朷xݟfǚBnRPS9grʏelC:mh(-j(ќ0nk!p/= _hz;dOףZEɗ羕R>OJ6b{̣S1"x .>aoȾ:t M{9c5LM^\,]zeICy,R)bn]mHyߕy?bmrj<crvGXI{gw5|!?_*[B}k&$: ܂OGuiow:AD=k{ZD+-NGݸ?zqu8&D\@X/Ք|  vI{eo-pd*(#m25궔b,qQA~$?;]p' -t_>TBq0c v⏥>20JLr` j.RKALM4ɱ'J.lNXfVN`,  WY۔BS|2Et2OUb`Ž&9{oˍkvGɱcld8^k-r0KpnSzfS_cYhYEXEC]7~`c[`Fڤؤ9:zoR8F#e.幊=tt UIJI{hZa6}_k#)!my`iVRMr|QK4qLo~}(vm7ij*2&pcͪ,o)TLmE/t=7 - f^L}mNkUPu$YEELX=Q6a@AZz9lbjoĀr(5ժ#L.nȼ?v:)5OI6DØYf99M?J5P8m;Djykzq հd~}?[SJYZ&8Qi[ޞj a q qZS+JYkF0Eo]k&\Uo_dzC=wFGRb9fəܒzbjoSgN߯#ȇ~WpmrŁ1w⡗,j:Ңb8I.^'<8}-coG/kIuOb晪lxo;%k.(%%xhg!,+uucao 1iԾ~;7_8cD%bE.Ǻ|I׍!OӍ6='׋Q z)"01ɅW\X*p8O0uG? XE)I%j*ڵ^ Zpw {fE{Ik[#s $쉅źAVF79pmk-"]@g? =YNR-prmZXXl|Yj#Y ;#e`=wΔB Pp:*O o60!m*A(oIT-Sva0`ggu}Ī.șJAS] sklfuW7[.^2Q*ɲe#mC39&|~lϪZ -=`^@^H*yVZC "2F:GCB*J6Rm8}/;YS e>d`VHٞbM1ܒrŬlsK񡻝%l %e[i7{Qgcg2e s8_,v/ն-Vb4=l^(lgDf8K pm3k a)jg*V!ҏ>TW[5]klҎ5ӐZۑH\)^T5{Ԥ{~5Xk1M,ż^xwƝ}^ZvB_?1!]~z/@N -9)n >U:ePsg_)eˑ2s'A$&8'-. -/!JmVTAE~qź ~ވJ~! endstream endobj 394 0 obj <>stream -wXFILCux -{/xTNIZsqM~ZW"޽r}]Df=,K6EȣX^nə)c^ %(AE?I44۳Ǡ߼J5,|n@A;YalZڠC;^{bZJ9ΔpKNq/g!WCy$a]F^*&OJ RķWJ&2Yٻ2Rž'F)J/BZiZL \{Y΢ȇ/סaW\@B33Ŕ {g+]*a\[e~m+0+oIғ#oJק;5y7I<[ -0+]`1TB;kЁ*(uiCe$>rOrw3o=7@.X6k#4 /VYcmEA p85hpfBMJ.r_.AeU%-*q`%=3_pit9 K` nkTo0 sFG=@o}  -Oq> -, Lv8InDTR?6n 7عi`!W^ Jqwz߾:|z ~tuP{>\.،n(};Ʌ>ľ_DOs;iTBMTJꕜ`PW ս@>O^989uݷAN `!k!Vn;bx& .3+ZAߕϠ,/|o/P§ @<{o^ ~~3()LQI\~^l4@,:P -j[#5#2* -2Eyz}[׮/y8o -QߙZX g ?GSARys5/@o~=' [@׮z\/;@]O="Hګͪڊi!nkhDœHi@^rs}|y -z - 8ߝA/k|t 7G dTbN.]9ݾ -xK {P 3udm(O\a*Ksݮ "de^q| -r(-#'ӻ@x998bf] Qo e% 1 Z,d2t^l_Yh]O X[+請3(3w'ÿ?qx^|xq4#FmCW;ߴԀ!U +(  ׵렧7o;_EswnQJNxG;7t#HXȵ/>dOA,?g@?= OA/=:凄NLqm3IY[KYaDuSOQB?}z~r៷\xϛ{? r} * ZIpsws]Xs ;SԃM;*nb~sx%'7^ w[=pݧϟ\L -=;^5*r"HӔ47MWkETk}WbĦށF¦jK݉J|jl]t%^1O`0:\|_ƾᑼg1_ُWK.V*BMmՖ5ss}% *NSPrUl^5*8Oli dssNJk%y Q)06Pow~(e\?ajy]i4f}9TQrd aeDw'*6tCM荁3l:eT)XC)nl{ .۝"ortk 3PȀPG74'W4vXn{KYeUH٬h)lZ,x'΋8J;m5+ܛB U);k-Yة -jϔ67 ^UҭQ!} +c(&g >6pRRz6imk4PNL+'tmX7"?LjVU[Ye^k67v7,u~2<RuRIj*ez~Cmʫ='[VqOyoVן5Omڪ d寏4[ˣ芸f~d>7Ila>GwUtwb&+C Y%:/kET35;ԍ#vj:0U 'GCnKvE&9,sFZe=miGELuѺn:R );ØUU2_}Uk[d!|FHߞ.GH+}qֺ6nbi&+bf(5zy:fш)n` kNr!?Yl6k+J! '(/ !wOxl9@BI5H 1(4#6ŜhkU|]6MɎ,P&%bccn.jEyV@6*tbmioKؾ3ֻVsVѫ"NZWp!Z6bQrw=~}D ᑬ+UuC#Сߵlu K9`y-aH)GJĚ:ņQ' uGΖ$reZjmQ*<l ]srY!ioы>R!m0m*xafcf}u_vu4ilL?Qtys6ǜ(ye!xK+,L{Аv?׎ -҆ElEyPUmӔtca½gGrT^r]hS&/D &3m12t-J_eoYaSND{ȀfYiRhp[kЪlﳨ[zle%9-M/ĭxə_ZJƨIRuQhĤ5~<;|Mq"cbE3rr⁔p8MUɄ?Vj:O#)%I?Vm#M*ze-g-w3M&%֙n?ؗ3嵸xu=>h `B+`:''+1O3$Bmᬈ9׍?RqumLeM.KK"\Ю1rn鑜UdVK5@.p&?(+T}<~@ruCXT.W[%źNBC瑦@iY&~<_N9_Wk_t9[$A~ &9D̀sJ6ؾؖ84уmFzJ9Ӆ.rM к&?OU{Op@lݏd虶|T[Y}Q;˹2UI?cL5t Q^~]{Qb0SͩxJ]Ϯ 6Pi%ߵ5pV96q?(T(6NyΚ]om3T46rI<_i_eb -"滮n,fMwat q*ܮk{9)NI|ѯU4M8Qs- -B¾ܞ@ - N};"Njڎzq~40OyTBKR.O0d'yPZt4S]nT@}6~_p:z $'mRRmrΙQ}bh >eg\f'_.rI`.hb{>yT&O!c -|I),?bjЋ5:r_!;ffbN@?8DO;gQYɎRn]f*bơj%&-2JW*7 Fg~_b^6ӝ7f*__1Yuodu$-dnOݙIP%XC/:^-6kz6+.7?W*>ycjU {LDs-UuN6V5|mѺ8t,u ullZgK`.C#Бοf*jxt4~5i>NC?Ny+oc_*+?5s$O&90_Zη kDBddQROc1wu0**/5ӓt1`lc0ϭ12,g R򬒲 -}k 5H<d c3ػ\lr +2] ! c\U}(eVWW~ckIC)2h_ CN NbIQ:?pPVĔA\`=6n\m?4Xhf&haM 4(|qжXpx'}O)&0qHƇ%̂@;)R :.2IC]o֍_= -4eP+Pӥ2muO)ՋiެGW튶Nycp~8%no~:X9O|U6_tyw p a<:eVc{SP}g{2u|PF[,x ~sr2k8B,&?*O\bJT~ paU#*+'<jߟ?;] -10E4x yQlN5njcm*F皝Y])2NaS/mVv@,7OY55նYD]8װJƐGJyފ9[-:Hi61)ajj"gl C:(wʓX5X{C耭B!JEY wAan4o]加- -`dk#ݳix(-~r]&bUƓơb5ӎ -/pՏIxoD }2ÄM"7>$%E@ű0|2;`l*d0w{QlsM}2/g)9J:Ԣ$eZg(SqB|3e{i/M+Y1.`!dK5|eK -uO'q1{{GFMӶ&)#l¾6|O0lYb1MR"qiv*ѩt(>pxc$7J/,s"d)KmUkW\-i%6528$ l2Z)<=ͬŸAd&)9n4H.VZlj6ڤ$hAk_BNS3$B=vm3& m03h(RcC}9Ւ!V5>٨( -2 Џf=_G/W֝Fuϭ%ZMz.u8-l353Lzi5j {b4T)dܜ.ޫZ}PtlЏ g?Yf,S?/^gٖQbuk|ZU*\ӄ$4_zƉ.3 rw30.1kf6I)KX?BmMϟ|>8m?.3S@4wWt9A/dW2/E@˿ǩCܢgQA׾d_ߑq 9Yh|ZϺoH-FA>j3qٛGze#= k#έu>ٛU8g8AT<{0<2rmiE9 + $}umcصg[bkp#-jO@nOe)D"h=rwT^E%9@_etk -9f<24=ҋ}jB`륛Omeu}c(4:Z|lzƫWUF8ƪVK}O< -_Sm.xIuTG$(, -K }@yWᲬaTrs54v5l0~v4LQtdjߜ&mL2}Ȼм!a7CBM`8>q5 Ipqhؒ}*sW9qL~]&#}jl_gitWByT̪}=q\ӼĤTtwXoWŨ] {1R7 l+iiL(2zc噣`.rO#}*.jwZ|ecT\,*~ټ0Zr{ecf냽Vhg 4<~(:މ۫ -6䧭qD0$xk p -@ =\``N-¨=-ˣ1iO17{ $ܭ \0yӧ8j.,w#r&ϽZckX GFcnwV5˃n7U5zoWj0+ڙEFJ]3=5_j4<,ONO#|2x¦[Ys&$OIu V!8#sbm㾥u\ӹkrvHQ 7d8[p܀6 GqKIqgCScY1'*18bϺܑ6&_el̻u2k] '_9pKRb>-*\ӤE.}k1P~򅹱f(hu9ͽEK{ÞZ ,04?Xͽ˽U пеQ\RA` 1Z)x083C;]r\ZrOf|5`iK1J =c遾^zkh=Jn)l;3ww?KH)CxY6:H?z>kWαObJNtƑ -[lK6ܫ'Z'+bpzʆY^-u~Hn C7QtU0JN{cD|f]Ħ%\soYZ -_l!6&9oOJ{:DDuCyQ_ק,3|Zrž,e_USQb~_oͽw I!wV=x+^jI+_&_;Xv %1&~[5|ߌ*=u"'afXN 5{Oɨ\|_WG}i3kIp1;ᱥ8$~}b H8QOutĎse ?бIKY>9*;O֍YMyr).-"zYNlg^T.f!tBށpO1nSkZfC-Ԥ_7էBo᙮>866JN>аQ'F*uF>^oMT'q{feeh9<@z$!&5M>xǙㄤ'/ݓȅƜJA.ЛS]Mck( -6wU^i| ֡o'Ԝo,:Up+r{=Eʙ`>P,o/2c>/{N|sFU[~UZ2oB~$n7P+4v2s5;XǡZu>%J鑿9 zua-ڎ TlV?$l䅱j2lQ$,t}ᜠ9'H!7ICj[*>2'(ؓy;2H-;oy9btGɄ͵U'T*Wqac*_DSԫW?acgU;Gxn}pO35l1&iQq+#쿗$hQ@-XKXiбiT4-ק2Fy,WAUE&֙XE.fN-,# -aJȗE:qW-\_n-ߒ~r)duKՋqb2uҾmەB6nvfݴtdi`{ Ċ HwPq z5ʫj?6 x \GͲ@ILm$9~}d@bl§Ȱ)SL޹%Α֥|Q`bd[[y?Ӟkr -"ånS}̊91; 3wdp>^_7W N-ln6`<-wR\GNٜ'7VLc')UABT $C"]u6H/sI `)cT1T2S޲RF֕]UÐ!p$mPWUYzn{d_y}k vT ɢaӁ66?Tsr,?pM㒷eJC'ݮOք]V>3_4俼SIk+җ؇q_ ]_lDȲO1'4u$]TӺcjr)EBZxj:ѱ)_ ,ܡZ+}*mE6:fcMݑMS |6w†1WD/ݞ06I^5ҫdod -xՕ};Ѵ?o8H66MN"D[I3{Cy4|j|O>0΃E%\^԰&~}jn -t~IުҧZƠ9)'%o<6 ])~q#y!o˘J۫bWۤ&=ġ@%ڧ^;\OAjgNLC1u?ZDgXUR.dciařj0/Ԣ 'g=s2n{5]Аy9 -Y,xfjθkvlBfbT$>H/`E"z9N3=q+`JJ{ =Pv]3wA_<з\>_ubZ:Z5SH TQ@nCjl<Obׇ2/IߎK{W3fٰi&9K&%ͱkt0s -C8YNmMQ*%eS+rd@k `[.,n`c6/d>sڇ39:hOƨ^H\zܕyXO'aHPŶ61(e }#=}E|v4~li80PKS] pI?kV-3$l=mťXG .T{ uK42|6+*~$(s}9_z&#}/+>fI?aMEOZ7x%%1D\b@GvhO<m{^edU-GIϺPvj`~b69vLۘ{gA3nU2;nu&UtC::ƃ>2; <xN,t) zuqsV֎}G[Չ55KFipYDC\G~8;HPv5W ȸ^1{Տ -B~Y탆X/frաd ] gsp_L}'Fg?DϡfTzs'"\-z>\YY?pI!xdcmhG)$}N).es%fR-ixO'jR*}s vQX{l }'gm 7pgkJ~z}r1wѩ-/˟Jɷ'れ[{nBT))lN-^Or.z,#] TR`}IWT"恿w/؆wLmRz -`y.jU\se+lC=pYV9*d;ޭdU٤ԥ>x Ж -:Xbl) JsLUlIpiC@>t)%6)%ߩt\SAXg%.%"4"8&fq ~m:6ÁJꑱ+!3SWU}[ͥPukaBkg 25n&e 9DOTܑ3^"&ms<2f]O1 \*㋹m?Iߖ;DR/(=E+J;sRrıQ%[Zx.[)|`l|Ԇ^ro̰uߟ<C;(.:IJuwRyl ܉rI j/f='mr)aZ7;Y)_W{7!!Ö -/Z ,QX< "%| YDcO3'JdbSj -d[ߖUEx"f*ߋWOf= -٧ -ԉ_T -un6^27c6ȹK]UW{{ЉW=3" L!f\Tԃ>mdQu' ]KlGˀzX}S]ᢠ#s侱cXײYQ- - c{1{P団]ԵQh 2)VyV9u-PWtj*+%1'en"e/k^QP^&&E fzY̬rbVrt%,s1{e'|6kh~f#i__x﫹]%0讶Χ\s,Զz{蛦dyf9IT5뮮I87K:?W`qgtqrR>IHozi&BCά[͍C>CmCI+1~e:|k˼! s0N 1X ƢrrR %>?cj* 3=2LGMO@#zr_F\Bήڜ e(o-[醄:_5Zfgܘ+ CMzSI.v`3JPe.G&aOC4GiƱEQW'`!{U}JLB'2 zOp, 1&Bge 1w6 ->0`n?#,ȁ7!y~GM*Y2i#rYIgoy0z`n9l{,>(gJ^ucb/.rKBʵ b#]&)+,wt,ޢ޻6  <ڎie]LVt~mxk\p8rc 3☁gC=3V8yqhmet>{+VcX t}Rp`n5ui-Ն06hYˀ^\aԁjہV t} ۇ}OWڋ*y=[O/}'*t1IE|26*n zɀ>@LKHQ׼2ȫp(GsM;Z!$j78al0lTBǮvͲORss99}=8a{ԝ{g?> 1⧫bJފP]]CUyK 3#n$*ŇZd:aK\1U&ul[N;TSc <̯j4r_ӵd1z>bJgkcSm089&?YOceޖp p >]](Z쨌Y4⚡8% ,M|- -KSJpq!r^Xl^qCɮ PL-׺VqU@ޒv1: `pDuHeb 3G%D[>PٞOS=-C%٧!*B Twhc1Q1KmBG3#;*k2.f#Ȅ;MAČo~Y'kdm -yMK`i@lCcyd"CN- Q!]/&.@[Ssl˾W -O -^ҵg>g}ndR3)aOA?e1963&*D>4+LJw/2_V9ÈABҎFf[Ag(Wj2C{*jꞡgފ7҇CmҪ0RdZkcZۥ|DÄ. m{藊U1b\Ÿ]Unʂn@"hFGCؤ< pp{: -ǫ \ |K#C*z{A 8P18v5jS2y!g}N,me4?Ы,$%^?[]wDxUȌM@ oW'([SR.`99e[s $9Ϊ_`?O.lek#iz9`uOO`}sRd$!]K"?v1vypjגŵ뫭YFJ{Í^;s+w,̿FGR8Zbn*] _ltepSh##y9agqOhkkE@>} W6KvTLX*ꯅoS@E{JBwnA|Z>W4@a22.{&8^`^P,ذRa#(lm]@]]3G*ߑ4b;`|Ѻ'ڦ@ 1\_cL3^9}j2zr[=]]M(P"o+IT}- ܍T=_ml??{`ZC㥂[ 4n&Կj:}蜵1țAfcD~6=>PnP}cAd/K]'W$:2@E^ ,,1ש7ge>?#.p_ CE5m!jΣ~xoE?5i- ?^`7F9s5y2N4cn):k[ۖK8@9ZbPt"aS3s%/,fRm{Kti,yZuH$KdsN{N{PylmWmO ܍|!p5ۈy׊*~r!rއ/]nQ.<9S"sˀ9KrR uJqk:ngX@Djkn?i(l=ntt🔔ߕt@ #BkF^j>}:#, -||;pT[2be9 Ց2\e*xy ٸM#8?Zl;4l4ɾl -{Nc F*b'Msq *9@Bl[݅oT\ Y{RRC.F}U&WJXL+,c욪e>gę/Wl"wb7dt|V@WA;3\4=~j^vBζWFYSdyAp !:HۚD&m+X+RNŎǮqL¾Ct|Q55y+$Vqcs7#ÈS-tQS=5lu@s/4UU<7CW!"umYNiFŎ@ k -^.uWw!k@Z]S*sWųyQcC#z$d{HD (9:Xﳢw'&&{l)Gzd*%fP-/>|==)nt,5ޙe&y~Q}2dꨰ58↥]-rϰB΋I.hC/wP9&\47W~٧:.}OEqT܄^@r2 -5qQ!h.E%ȸR1q3bT8Vf+C$X@G+^Ϟ6$(:)%;&YABYF)A+ĤIJզ?ϱ3n.ԕ>D'߫*!aeB"|߅|-CemK^ۥU;tMz–5~ ?[t`6rM  %Y9\WI,B3DEjNPE/%!FwJHyJQ>t~GiY9 !gB_F_+v.~M_1bOk/Tu9ց9d֑/:67>圣:e2oPb\m/'G} 콚 ѳ/S RVsUeEDcC6^:NJ^lft|jLʮ>P . );N8VJT-Ŧk0 hZ5su8- y)%5EM: ӍidӬWv1p(6OQQg͍W:!f)咹Tڇ1)tk؄MNɗZF==y`"M]u@,_ *_)JoaOWk]2rpuz?:z<B%\҉zf)[/5sRZ&,'\dNx8!.js4(ݕazo<ȟ ->;*~%?49r~ϫ4j}Cq| -}ܒ`_M*Ͱ~UD<;Puj]}jir) v#uMSϿwSQb\myj_ŋu19wt_K?{x1 -5w}4?A%~=s --C'Ywl@o5,*9:2@*dV7jZs~~Y"MxMHs(h Rz_uR@K噆~^9g&Hjg0A?L[aA%oQ_Q~s}(y}H#S![Sqj]'xtSCDwFY^aUn c u7繹ń'ڰ%+zA=cM͞X0*֍ -_iM˼brꑿOPyRcͣip| -x&uEƹa;3Ωp-%*YջUBɱO0 l#r]SQL/#Ȱ_)їU@MO˦@ s8#D# fd= @h0*fjʞ/m:Z*lʦ6}oQN ϥ!h=͎8]5?ԷW3wQc\BHe\Ip%'a[-t@"$RU*m\Z*/{fAZ%],zs+Fi7W){jZ厬2=V6.DjAwe -^5Cs~lBTz*.pIrC葹h~pMaL}P13ru{m%lM7$b\3]Jz]h RO#{E4=ong}zᑿJhy{i^J$ /ԫƦmmala~/苳܂؄翬w+Ie˭4ԛA_ݐg?tUq - ^ˋO}3Ґ4Įc,~k.'+GȎ<Nkqy|qu(<&\ڒ_)oJ9OY/BX񜔖xq}kH?SBƎR B -n}Z;g[Vڦ݃pKZ -Ru1 -ۚs|s2JIҋTգ٠7ҧARGN6_"ױlVtnMu3<J[4:EʷMRׇQ#h^TUivu9RWGO\B]XG'EY& ؤGymA6 6}ΣT侑E *.nOA+Gp)Ͳr.-w?Ji:<3Uo)[S -R"VFa6)!kWE<E.۫¿n :NDC'@84V%cC6'v 5d~ң2K>2p>.:涂!^ LU/\^ͽ y6F3ba]5K-K]ab螡1Fo} P'kȸŚSM΂`rroKՉ.߂a9EŖJQQJn|f^{RVvpOC谟+} U.( 0v`cM=ȏ<-m!e>:׋SJK!+(E!+WFFY4rmuґ ۟4 %8w^R|m))sߥ<]; A?~|=7e?ik:M$7}WS{*:5Eۙ%fm=2/9hAXr?(bN(2yG΀bR)5zwUu9@uLayŎ !#or*և[X@ƭa?YLj 9!emQ{mzdo£J Fxk qHpI+S,IRJӾ4!wnǸ$#Hp)T aIl=G-QQ9jC=g⭳=={`󿘅u9 -ȧ9&IN,]`1_N4!߭ 2Lk#+C+ʀ]oYk}=eA[4:!CƛG_+_\7Vr> ol9Zl_[؞'TKA/%YAdH -vVB30>| Qgza=Ιo*eG'%BNK=8C1RY_JߌۑsZ>vʷe%|@ DeD.~h&Ŏ - -r?OO/BcB &7B/&&=欠(05's_1 ⯫yOMwu$:ɏ]Ш_Mc=g}]⮬as#lP쫳mdAgA/G:BNgF(|8953K UL#='awʞ"g$rE ;7LJ >Q.Y_)*l[Z8Ï=J4KIy14H?CzًݑmU @)[}iR>E׎N뗼5,ݖ)b9A!L3s4}G߿cY8@a|Z)g e[bL]/(y16ygא -7G>)P `hEĸgEJįgSAǻCvbd9o+/K|"u垮 -;^vz~Ϳ JJZ/J(p*h0㱁27G%;sl:JJ4w=20D ѓ[ 2!}?/Joȼ]OJ̧p|:6Z׏pkyi36CRؒg̼arb'*D/YvSe^f8gxlKMw۲nn Uy"܇`^8&!B;^m`#gv5<➮fs^ (^0ANvD+G*Z1ٺ~Ft ≘ ?AFT,m&G}]Sh[|i^T~^p(*̃)FU*DJ.BD "鼌S䯹2Z4P4i uPK&;Zh5X>ϼ<yfwn da2ϽyƲ׶ǑqzA fB?A/cAo{ߖlP̫rn{fv)I&Ibzb${EzG** HRUTb=wwyn??x]yp{Ua~4Mo&}|`N$MۿNK^Z:/nqu'B~X|4zYLv -4UذkVÒYin6n:h&>|L\|BSH9=^la@aNjA* u`Pw93d=oS:a@`#`n[ ;L֡PDf89;fG%]]51N;'ڲC<\꒝Cq׍yg޳rVuLzuX 3CʜT1sYjrƁ;`1,Y*eTb׬p+^:^h]wf :R5"w -6i5hᘴTwYESʸTnusU h;>sFfͪ+NAϛH>N אJYeӒ[>Vy;`KYX@_4-BT;*熸.QŇ@i v\_8G:f'2ٯNb_ k( -pn|@wgTRxɩϮlZ/4|V5񁶴kvl}?>̣O蠡NLԲQ}+fr~D-=^GԒnB5[3Ş?κ[4V،#`.$Z2S ZWj'yEnL޴ 8e¥+ ai0@Sׇq=ޚ(yϹ8 ~elL?ajRlu€L2Pq&}{: PG$rÉ&V0[֣?m8Й%풕T  uz峦ܺ ~ʬ h 7Hs;fzʭ_my'|<ڬZl,[ؼ-O3 -y9,FxlFALבsaӸ^2'u&hj2֒h!ZŴ4ǧɾC>RYu' 7¦.{8-tL/]!]xiٌ+^6ݤe;(hj RvVW -5_}gk--vNPUbb]j =p3,p֘iEU3VȘB21N,)n&qVzl=6v[ff"@-\bB ̊9 )=Ѕ -ȑo Q5#w?gosi0Q"ּ|w*<>c9xqK+6`lϘZ!h4z/`:pY&\9>.ac?ܚ7F-S}["άow[ѐsv -(%\qvec<(Q6ؕA6.V.2~~Ǩ!z\]Re&xmZyk^lBmɜFf㔖X -;ܲ?ڻ5*0vBޢR:!;KֆdρX>h4TIԗ`3m/ j߳ ;&+isX hk'w,T<3"3- pD)~[_p:A;);&=BE.W+ѫ[Zmj1q=, -O{'8G~ pOqdzB"OV1 Q\*oGkY6e~Z=| -#:1xoet1 -,hd 2gjZy%ϴYCoP{蕴1Bs-VO>Q۲+9>1e^h]|LNJ `f4Ml)1VMY NjA~按Qoj"^QtBl]]6i2L6f;ӁIhKf켏O4삛^g/c"VscKkW L=EwMH(੢dS| -"YR{S9ZZ5m@| =TLWbf\F$1v -i<訙v0mr[pNTH~0@#lʊK@4l N۷Eʽ ,0Hgk#{uVл;ᬏqs kW2an%~zKޙ`q\)#h%ltև[Snz%xfI>UGR7WD3cO{D5Ô d mjUk)Y!wf$^~~؊^x<=Q{Ь7&B9ΕIGزP;.L;iR5Q:JޑSJa3l/ԪmDsa#$)W0p֍v4꼮 7ڪ'\-[/ qL*5)n_^K> ^{$\ rfNwЗR֤ҊFة>ӯ廴rejM7sMvST:R)43HdKlR'OGIKjXZ }"ߚ;|f%or7rڀ3 j5%|;Ӵ9|%|R_/-dC;1S:~*+fFL(|@7һ%6uR&Y{vSS,87 z4jFkiE$Vӱ6&UdwuNл?iޙZ6':+iTwI* ^)Utq$1ξmJBQ%LP2X9i8 veݮc`,^۠*jMkNLno!M$ i{_/iI6er!X1dy4~>&c 0="x{J~XrTu>Hsh(ʏPT4Z3f6*VzyHdD%E?6Yʴ4lR_,śh"_`\I""p͂\/nFVtHZT =B55"1c&͂lca|ezn~T>t5H;WHB+ M0E((VQ,kEJzgˎ+IQdvU5:d`Q}HlG|YUGTj/ybҮ\rffțxkCNb1=C$E1795Ez f*%hO_TTu:-O㻏VDND%O\{Pr/ Z`NkR=Sz(u:(Y6[B%4 -DW* k륄x?>)$~H]"5VXe/y8q$Dz -Ů6sSZ9\:bm.EoY:_/˭uϓ|K 6(ʐƉgH!76 CD%OD$ x~*2u\ Kv ^K'Oo{`"\o iAFIY_SN>|Z<Ɍ2'ldȍ%!0!n|?>zgzM6ju^GNeȖTNMs13t5|GnTL"Trd{̉K~ -yy"o?p=Cg 2b;RmAsHVdғ覟ÜO.h?.;-Gై8JȟqM'>UNeVCo;RW'bqX^: }m=$jJ3#WCJϼ|9Մ &EL ̓o'3a͢@~OqGY+S ]2AgK/sԿ}HazxΉWQOjC=Ir'ˡ"s'UʻDf}ScLdB&dJZ"O|For2=l)i~Mc[/wA2sXe -݉H͏9g_'~zwi懄N'C= pԋDAH|z2˙mV;[G%ՠ]V12VT%k6~Tgc5O`۟ -8$.nWBq4l23+c^;D!CsQʤ=Tny-kx"JdR `M}wa(-l.**ko'(hCL`lJqQvh0jT\Y(KۭE> -G~ Ljpy$_ wb$=I}2m['-ݔMt0/Pv:l1zl~Vߋhoo:c֧ǣ1ɬFƣOE}caO T@hkV ^liW FؔqW[*JSZUx4Xc4z w2O\:q'Bmd= 9cB}d% y7dȞ^} n -*\j+Rm*M̈́y0 w9]Ѐ]c5}}F\j4[~H-mOUv%wwu{rիd>7d%: ^8Ei^Qyag%{ u.rBLy(Z :5і%tZVc?Nԕ΍!JӺW7ޢڛ;]VEFۋQUuE=#Pv/$&A\-Oͮ侂Sy9ij|X[aG, xQ=mr$ΩjP -)Eʌ^Kd,Sw ZowhO -Y֊`;.1"!3jB漆T7L䤉^U7Ydc)n0T}nYgF"Ht ܺvG+OٻyVʒ[H\I{#<@3w׃.?HAM) 1e XZ1IiFK T[Z1򡭷%w@0l`h6ΦKa>t@]1fZ0caT/nYҲ -jF]R.YȦ&U(nh"=&DwlYLs:CW.Z7}JۚSY4>fOҲND]ՋQ( PnOCk鎨ƜA1[^x_lWiHcn꼎Q&o/CLlO{7Wrŧ|7r*Dʗ~|ٌ)g'iSvΨP9*\ ->zG] ;3v!r%xZh~6a Y+.J`AW=}?HF}k촊1h]t*"}mnácTM1n}sᰳ0@g̀@eVطk*_sa&ÇR=dœJNSIp0HFu;Xgs{VzDkTH^P:h#fN{c^PS/̼T2 -n[1 Zf@-탿n+7^pQ* >zqŖU&o=a׍ * Ūae1Zrڴ$3 ׼9z+: 0 @-[?l S e{^R6hg:Qy4JClq=ȷj\P݌X0ge1INbq qA?nJC͝C91gh}B+GŏGwUS¸Ykܜ)-%smK ]C{;VD`3xK׷H8g M_zdb9~6wm w4L9Ĥ]h`{*rȪY׬MEkcΎ Jt(p=ujռ?,}bI9T QDOMXg wo^6|ӏJS14fy>Cl)Cmw3 twM;t⸙?{̰Ί"}7Lg=6`&sXR=y1S:֑wgF^p߁&MD ڷ9xsےd[;MУy^( e*[am,WE(5WG#x_Ćm?KVԆ Qd`Zг5f8IUzT¸P2ֻH\ dcQ_Gɸ/CDġ SuWmI40S`KK?WLm <1ֵOf\B6q^RGZi99m;eNK0ٟآC:wKvMX򤋞F~L隶Bo Cm;"yMrV-% J=UCyU`=&0r%T:VhdgE5_0S9G}R'QIkIq?g:Bg,h)VtGֽ^5ؑ{6䧠7tfT}:-O&ЎmF%e~$zp#2]iC[{Os?r^|сRwv\ͬ '!V %ۮQ:nk O{.|Ų}@\|oT{oRk]5 S6l،m/f8$H-;6Ow4&__ẗ́}PvXЪn'Zv\M;I&Z)ƌ}wS <(Xu -Z7doQǙ+'Y-+*sm@^BV1FqMS 8bR f L$ - -՟81u{qKv w&{S|ɒ8 Ղ|EUr8Gј 6j7UBX\¨1slgs=TUOȥ~|ڨńվ }xV3O0N8;scկ}˪'㹓3=Q]e:Ն}?˺`OEe c;:}ߍ+uovlT@V𸠢 dޚWuw@c_Qⶴc\ܙ~Di&/O_ss8MthV혛׵ qK§Q=/;0/ζPiZ cC$"_/k Zz.hԙ`oM~﫛o:{u Cf~vcJ - ig!ӰH {CeR>Ѕs]W53mXQUro+R'՛yfo jY=:c -cǮj" $h$t6؛J6}կ׵M#$ T9SЅH,w:U_[E-ˢJ|dP|"w[fGm"`[Õ.^}GkI'd[ؽ!:8}?TM5'ߧ/h> _փ\7C#RF3̰O.Lš :p㊎gՠMsk{aq͹^M=)o8U|@.\bִCߎwT?4^놽d=;*}߅3rrV|4$-ϾIU5ЧP7~ԫNڇ3JݽUJb_x~l_#OmIqIaED/>qцaQOX6Ĭ!SRqIyغ)}JY6錋w=КwwYQdKY,ߏk#9WFZyٙVW_S~;m>VcOIi 0R녾uŎpJe "{݀o2#sĨߌėߜfAo 3rdƢ>tQ vCVߪu\3냽[;k}e9),$['HcQU?@*[P|}=z)y)~0DMH%y4Uc)74[u1QÑ☿(/>DPkEU`gƍumaXOz.s4I2zژp`k=uSkzٛ aWv4|嚦2KLdFDwkjF@S=7MP7G9_-KGR)Maxآd ͢#t7k@1K١Ko>im8σ/=l߃+ "{OJ |3?k\*m3֞dmYI ܇_іۃnՀ*4؋ZPbX;tgU-,zST?%1"Lc^ zks1vĂ[F\qC_/yg.@Xi;׷DLwyEMزJ|g"d_jxz.jL4bӂʺV+:bWٽI9<\I*Q脕PF.6ּ_r??50QJ s(&lk)Gvbњ/o// ]C4k+Ҝg+{ebɺ^! QSwGnx@݇ee@Zx^16+[{7J?d|΃a_fHp 6)'MC˽`S0y3}1[Lt h/ƬMII'Y%2A~U#6H+V>?6Ӿ;F`g.;-弙raQScoLnɽeWeՑKiJn>ӮꉉW=9M뫚ʰͲ>jg!yMSγ#~z9cX8Spp-cm̶oX.I4.D \5/s -Z Y7_>J_[5Ml[q+۫}eKJPĮ*U92)%Aԥs%mٶT]$ϸ}#όJA_^b)=oo4Ȼ5(9 yCoCę)U1&㬙RzQMSO]NV#qa%-&-,{\ݶWT*}[CPQ>mɿa|\n>$:ʼèM"?*`Y@w_y⤕楞wL˻5DK9>A~|ҙS\hB<|CߐClM38vAY|r1Jd};UO75/]s:dԙ~i/~<7Ikˇ+&ȡ8;ZqFH4T,V=kڴ57 u%͎8tYQ`x{u#n¨Wޛ>֙ Im{~LPvRߚw{H\pˊضsg@;JvTFHOXU sH薃zW@^V7D/*n"z*mcY O[7c2Ը ?tU=e&0~`!Cq4u` e6J5'-7-낇zϧ>߉N|l*_55e{8@b~5'uҢ~Yח{X"ʙg3gsҒ$ߎܟȺH}tަ.hD(:ya0?òlUGSߧZxК!vZaIݶCOS.xaIC?9 -Ћ/@|߬CCǙ⧃x`ĥJk"6,9 콿=j6%2'7;⴬UPO2âd\c^i|v1֒y! ʿ<7ZJֿ V?]Xyi}nϽmE2%Glȅ>^ޅq=9OAeyecՉv9-⃊uMG̾؏L=ռ;)Qɀ ;JƮM<ǯyw~ˌ hM=uc[זUaYVC]s.t+ EpwOHPz3k;zPx3%, xIz&rN_1`3 S f;!oTup'.spTT׽5.hfDUU^kx1LM/ 1A} oٹOٞM]雿G&t"Krp)3,~n1A֝-c sښF%#$uAd2+5į+6u1#hs=_S݊6f߃),=ذUٰUf xqUG~:zǎ82xrgq$:e6|9x9:Awg*vV -V|zz6%G^tޮ*z; u U#$_xݝ܂g {^VZsnHS]կM,;m;xP'qc]eOO'^ռPnN#5uI ʤ.dnJ;$ʾTCV=)G;ǟXP)CюWD檢8tRqm\猴檮^bM@Z`ہz<'}޲ՅO&N?4#6fM "܂(u 4BCgڏTRPτʪy!"$3G"s -Ò ˆX/7ז|g]?G lB +zz n3꣖ՈM36G0a>i4aDhB*X`?~T¸tGŽ9%0;Җ -{8@LJ;j¾vdʆ[s,W*@M'_ /VZSvuMzDv@RxLTx{ρݶra ~)锬Ց[4':+nl?-/؇xTV)ӖG(_V^QٹN\恛{ptq1J';Х>\ц>->@/b]_6!SO˺:%G߼Ԇe/2ri|g ~驪 [5 =|֮^3 rԾB;Y]B0ڒG 꼇AUq$~߭*+_Y?NY@7/ϼ3ywmZr?S9zciHUG[xmӈJiHm^YU].W\P-Y9PE*09gs̢r  r ẃ{;j^ZՋ =}>{O8G1;.>F-i91ƪmkV>Ni{ =?ӶWcfnm7>)_3SleO_MJO8[GwkӿBN.Lӷkք,뻺^מZ:r] kY5{U|j?1=M*p#2|{}sy?^r~u_E!9rRnn{$kp=F7߃f ^.5a7ߴ~nQ1cofYxIk4~sQl{=3(Aůxؼ_ 25JLH`N)I q1͍~Ipy`kU\Ifcs1;U [@{44B}XnڞWmRR[͹ '2uײĝWƅfj=IST1hQ(h&?CZG\OC|r~zSEjݣ3o*-bs]ZEWSRQGW~=;O;X-oї@]? ;Nю]0a:oc1`D߻)EY#7lЗۚe9OHQ[xaD%|_%@ "UۅgOSƵ\^,>k> dmXiqMSK=U)զ~7 w+`a;JsʣͽEdMjߞy ,km5 7_CO)K6=mL ]Àůn'd+FWB:v[lk)Un:LL%Zڴڏʲ'>'dlJi-;|`kEW!0š>OY<)vPJ_`έZT}=O[@\.FB_'9%>9u= pyýϔTJJB%L]gj1*(eW2ꈚpMQ,`'7|cT [z -=%ۣ+sw.lB_ HYu[C71=D^v/y֌(1!;{͗rVۼLZp^"篃|.)S+k{Xc 1nsRo5=Z[\fהi}M):ʻ=1}`E,U!إW)jFtf__&DFibT0ל찾O0=B\esV_5% -4f&CO]|ޖQw -eBWhH,P}9-8.Kּ #c<Đհ6̘]rJxkp_wD7a5 I^>C/^f, kmQdz1"hvN1Y~[PA.޴s!eC}JV~,pZw y`f":bCʴZcKY렂xZ%UFlN@~.;/j4oYg.~r+RwWLsV$+#2>̭ݚ`{g@lnQ0#UƔstP^O^6|;۫순Qe!q>n_%@9&+oQbA~T/6'gf~zrvg[oaC!fqӷ #6$ξBP%.mFTWF*yȨI쭍AUk;rANSC\xEt -ZETÅRA.8욦U'=r'q7pA嘂ݐH~ -942<G=P tʈTjà6g90;tmX0v\ctPYƟ_6*nR"F ]~~\w )Aym!5h}͡39,ip@[3\3%(a|{%cYOڇ_5;XE_O Qq!8FZ9~)ЯwU M(VkX9a"lMQ -i#swr^x&eB>Xhy?oy ;q1SoC/~jW@9_c +oiY2 ->%5}TR Q^u7{m0*lB|mu9.=Ff/j:Fg{5䊘~dAܮښڵ=br6h\ _F 킵QRsZ3[cV!gs>@vӋo:G!Zyk5tpLX -J5*)Yy>ޣVxd,p7_Jp!=kJ\|K6¨p(%[7ȳ+oZOjz*Pw.P6{~-wr|mՏ[C 䂅7ʿLxlN^yۖf{R^ŗQDOVQʂ*NCʮݘ`:]DuL"ӗ5(k/l~-JZnĒ{ZRәfQԕͧZj}̣T\DP%D Bҿ軪,qw1$i'Y_3Tw!8K)A{&ѷFaFЗ=3b\U?eG#Z"T?x[U"}9і-E渦Ci) iI%]JRAQZE@F^mMI|wRg?i|0;E$;1'_z95WLP^$nLҊǎ)VsiuWwqO_Ǭ=O֕|E_茐0OO iX'A%9>ٱ9ɪ<\`bx̃·y!d܆a]_R1.M IνվEХmL2J=".w X'#I6.ؘbb0IR5S '|RTFLjmibw kuqى7@>,w 8m~%.50I~Ts<[}gvG_ O*>fBp12alL-~^;>ȳnzJyix_yp7Jz(0ܫA-[#~XS:֏ϰad*P2龙'E_?PfPn 26G(QjIHC;XF.5i[ʛ@mz--䴤4YXClʴ<*nEOnW9eC?Ί -L2c[+Qxs-oa.\7,mwѶѦ3q>[癮}ӌJ뫦㖧-Z/C3CJ.J$n?W>͝vh5a5jrޣD'\Ә3m{;1ˍ/!=/ٞ`~\*jnϰݵ_8F9A%!`S>uM4!b}2({IO[xz3E$ܐgqYi!e fE:92BIP˂X{KBW*$2\Jj\Jj듥^b=]ꧯ,goywrwYy*$Lt!' Q7^WGýUqg|j-!hwvGz%po׼;H~%4d[ql6гٮ1E\w0H3%_j>l; ֋>U9-h=;6HN'm׭?;QY>̓{3wSIQzpc `@yD[cP1[GXKޚ6ڇYCͩk_oE^t.mM -^"?n-㯛o+OP'CiV_!B@1yx'\-WFVkj1zG>E; OVlSR^-npKi*=)vuXC\^ٗ{8ZjryStv(C4qft]Ei{Jvo{GmU=-i 坁g|93̵LrO+™o pJ{z6QcF-aR*j"k CnͿU*cf!r{^Pa Zn巎axss"f"Q|+cCIJ\#W wֈKXDwץ d}Rbz;QgHQxu/5nHBz4$Чh ~2 騍E+eAc:.zÃ@>uN!cf~ة{KЃU`Ei)@kib zŇ-B@Oӑ~9(Yt湔[Eg#ʝ1soOqI% dUw益fE_|.nRV(5gk@^]i>o{?:9+i9/[υ'aLiuC^j|\֋^Y'11.L@vG==%A7 2.ħn'mL}?j%o/fhQ]j^drgIP oio ǝc+!51tq\2FXyP l]շ?yg in9:0f|:SR̍%1Źg%#[¥R GijhvD{"{'MN'χT҈u n髿9@ H I=)n*Yx>lͲO5wB+{A-+ma-eywMvvi⭧'l>9'z IabG !W|Ƨ@+ |k=~kyg)] ⲟn )-VA1qKGJ?>~.0Âv$ pM Ta"uS2)jH540*UkV6Yb#0xBw[|5ЁUuRGOE/E^^xZI7n)Я&"a5ίBXmtkң 7I`,+vPdzL <|75|DPE &a䣃j%8&ێkZVw7 ~ɗTZ'x1*ۚd=F.;MwA]o< =-9GD5~)xG].8S˂}4#fŰQzGo!><゛#1LQ%:YJ7=Avȉ O37ݶsϰ7bj>ddDF *7Dž_[}KJ=z5Ɨ%M|lDφ5fR~Qѯg6:e|,,0K)j٭9>ɫ fHy.)>^h=#*=p M,LmmLJHDx$6s] eƻc{5{ -TVޡ;'I9.-`]Ø#7/xU48%{iò[iB~LIMĤ8c4$8Cܳn{ %6;#Ք0FP˅yfy-aSOwRqQ=ᘆ]tל[+ $,,ŦQ=F1V8m}xz v&WT VYE!/g`]-C:Jb\Ow΋Rh|j(:;K?h76zrHzj{ݪwVISYĉ9E!'gXh^ڤk7bos7ӣKw@ [| ^$6gsv9`}Z}>T -/V|PiM|+hp.m%; 6Ц,JrHMc.9dzh tVBtIR0aM8p}_M,P ,855!sM %51R:z4F?RRB/oNA:٥ -im102v*>sns 8fA^Ihy,4T܍w]Ð3 Ե%zf!-3ZSo3frc~hw~KC$)x,fD얰RP --Vϥs|XI./zdԆx Yv56ELa\;?ٳ"{LBpyq-`vFRkY ֬M_wNyb{ɵ!TzbGMouAګUG8Ia=˃l2m/?~1BI&ꣀNdNDNX1^YN_i9b}V}p7Ѫ~>'g|Wή 8/աڟ2U wӋ;KOSsO cf!!]sL7 - p& &f4ne 8$ֆ=3"l_|$oj1$ybQ;/b|X}v70! @mPMw˺I^Wprra@A/-}k'hK ǧx픀Oem1u}1AS0a+~yc>I_~=Q˒ΠJS>5=yiy8 t0V%`w Srܳ2ZU+VFYFE,&8ו:FNWGjr f-1-~7dB[Nۇ ]C߃^jk]d -K ؁3yΞᑲz.^YUxϮIȹK^9dԚ^ 뙈IDZ)d{doam GJEĹNOKTs€"@nW9.!Cm1lb^rC3 V:*؇kN.uP *y>"`j kø ěCMXļU5EovagrP:k]]`BZz}*nS!ōj)7u/kJ.x%>%XMN" =laM΋x1r_}L.#9Cۜ lʢ ,ܯ.AM6=,97Z\o{F.6i#XQJ5}^61 ђ*&.§@=3J4uͷCͧs=+FnI Wx]+R~%%zf,>4i`quy:/jdD\tQ#/Hk" V-1lGtb|t!4gh%߮ݢE4ԊZtOkPI -F.ݛ:&'ݎ8iz}p=:> -7u7޵\WLțȸu>>/$%,\ `zF֞Q@@GeN@Hww%mw|JZN`lU"R?0gݘy<>'f/Ebe}V ԭ1 -òaU$V0W(yߎ p @x iظ԰A0# v=F"#E4\`[g7u,Nl[:n|R|s R.~˷lyYrWgÈU@ޚ/iV$;6>ϣͿ;9m)xoYē1v;aa">x_ dTAj3Np+oq7қv_`k"c}yP>; D4"wT蒢[ꀔ\ *?˪^|r~{3v𰑃:}v[Վws6ot4 Sbߺ#V&ĭ ~KXŁٶ׭Vw!1eKN/ݒsjʻߌ{Cs -u[ 2fS6ZXή;E~b$3΅Ts` URK^)*ff E[+\FHki%$9z˅_*t[qN}n~rV2VgGv]`NHߚ!s%#?>k{ېɮi j9}]K0^98U|ۋg%n}n}5I+ࢰ w ngE!*'W{5rHie)bnrIұ(Ehɵ! &faw'm=l}"UȊhA~ k-::$(`:fZCZLiPπv&E2kw?pn;gVU/ipv -s|kR> 6ǵQh^@E:!a ; ^l[QvHym{Z'w@ A.ha=hB^L,A'q7]R\g3v_yKllLo (eYvMC&"bXnL83;s\\Hj->.bC@C5quӦrj󁜜w&!`OreĈ 09)&"jdü6`fYX-@,HDC,^xZ}law A`v2vrX't@7 _3܀effqtɶOu{>X{au1?oPO˽:>:Uk|Ӭ4?HyQqMЊ#.>^ M!!7# bZLiٞDdZ}밚۱=K,tu,_E{ #.(kˌ訵>0FqwNAs<0 OlqM27`W}k>5(r24!7贰V19Y~56fa!vW㰩S $.ƽqD|ۣ`0n֮y`}_DÀ:vlW[֎;%>kwi|A"I+3sI1Pc&Bl$$)9=Mmx_&0+ӔJ{jvxEmVP3] -Yf{ }3ZS1M_\n̲1as{.Xґ8XhJy!% Gpە[~X?HD;V.6n`3CJ\VL˨1rq158gwAH]bF$hM~vs=F.c5ō\ĎYD n~Y4ҿo%jς<_́zzϔ=N)=;eV&x[A~j -iQ3¯n⟭=o:,c$L_z:ۚVO{[I3dm#+[RF5`fy9DŽTg栢Z1*\m6J(;1Iȟ)bJ j~p@L 8`\ήY@yȀZm>9PW -^FM=]A-̯HmHi Z"*,`%f ":cnepCIO15zr\orCzq[Ƭۉ>e/ۯlMT}m~XQ՗`,7.t-bpvrtNPoBt%IųMr'aD[icfE O~N uK%FݯE]˜}oI ȥ!-êX[ZЪAfFXPT9 =|\mVNyk|ؙ{A,E}{;}'/`i-Q&> >l``Cб[Yۓϋ#}\H>J;*ur\͂Ԋz1 -5 "zfe!Xk- -Jqh]H8mA^@䒨\V}*V86w}˵1l~ruN\]ّZ[&h)iVk484M*xx^)ƻ;f>qg7jsϯo9YlO"{d[Sup]q=4mk9#1Jvp -FUĆ'MFm}^YVЉ Y^}Yl{Gey;߲>YUܻf>.e7.A[_T*,#TlHHN h}kIgx\M?5A -5ũqW_2|ҶK9dND,O=z"bs8F7Rdx_UWK([-BftՖX2:گd5dJ` wS%֯LlLF'0yg *mM{w `i-fﻐK#o\bBUmW3 ȉɅX3\mW2䭐ykFn>Xc3%5=S3m79۞s9-cBýWng1kBk -* I؝ 5eiП~]cؘ}3wq =9sy%$'4f&twGH;|a-;ȉ1eyZ3#:#|;8j=el=KSD5F_+H; Tد<5agȦ94Լ`q۶8$+I'bL/OM`K6*1sso,`҄GlgAݜhKB.-aac -u -؞PR¯nQYvFⰉcV.%#D 50>#S1j=SkoNX\|R<Sנm7?jGo$$͑Yb qQڛ"f«VGIFb֮X}`c }sOoDl192r۪K>)=Vʒ |bgWāۧ6K fl" u@0N`rT{ -ĤSKVcN蠊V~'';s"1{c[~u`*1Vs=*D(wc}U 4g&-ٻK<*e쀑_By'е!~DJ㩩.U5A嗓n0qbu$:9z0J~ETJߚ`.TlhP߽pͩIxZHGo -:ۓ 15u}LR N)!X4ӛ " s͏Xy͈%_`"ZB h>5##^,qu Tbs:Do%S{֗Ihz@C i-@{ }ZZWM1FV RH~3 pI}A))V2k5|Yyluw~%G7\kR -vlPUlpMan5b{Rϣ(=nn~5*& HWvLƘ,q4l`5.hE - y`B*jOi垧I9}[F*h# | JTj`i@}$ 1:1z6Wɪe~ -[\M::@mvN" a9> 3,6{u{盯NDXj5"K!eÙqݯ`T"[¨ٚh9 t8ȅ~*fZP9\~|mї!L6bb8USe1V»'W3.+;SʕBwR"~rI*AO􊈺4vo&-y9iY;BʖM'5 1zfssq#i>S\Z#I=-$o.f\X\m3JJY@C wV;ؼ=C3H9v6j)u %+"чe1ŝit}ԏZq0(u@C,n*-duc}efm. l/_79,5O$6+zO8φΛ_ۿ 6E^%tyxp>~1´?;R/UQ#7-e- -Dq+wLMs|񟝓'3}o]#ywP3ɍѷq !ױ(f}\cvV5>][/=Cky5-*KFUDƩjzL(ɰ7rF5]1bls1j`AM_whkc[#_بmE] uE_|ǵ; LHY[{BՕ?BH>X`턑޼=za8ȀQ8}'u 1۫"ڧa4 )Vln:q`%V~2XeCS@Uָߖ4mTFtY U"L Z">' , (e^B ->|zg -vi}ͷ-wUȌηyS'ggj׽L.9[Όڙ !Qsgz{o@ݖgkz.Ed T{L -)W]3=UEXw7ڝz_- cs&emOc/|w-1r9icwc=58p"iNJc0\X.,7Ө]rSC3 -xƥͱ@{T,A) @iO_;G/&1;9rȑfG6;rّ#͎9lva#G9rȑfG6;rّ#͎9lva#G9rȑfG6;rّ#͎9lva#G9rȑfG6;rّ#͎9lva#G9rȑfG6;rّ#͎9lva#G9rȑfG6;rّ#͎9lva#G9rȑfG6CXձ"' - y؟T}rq1- a$޺tcp(S1ѱj6@71(*s`ӛIc{3Sߛq5(.?ǁ>Hh8=>vrO. xJ;v " Ao ]:Vy v6}'32_9hW^ȸ+\?m{?"-5_ٖN)bOnWT}=v͓e]vX۱˗Ҁ_R~&s9Uic}''N q,3O 8иQBh$e5zՌѲk6 Ґ_j#s춸E jY.!ϧgU1cQ =":.4dVjjZ✢W ], ƵҰ -u3a†BnO!hJ饞i썀46'*ǕBDZ9Ƙ5QͧnːJ|Qotݙ3czFcDMe1LF\ń|vv&x3Irr}gU Q Y =.U= gs5y[D[zBjъI5H_t1;]՟ ̠nj:15bԄA5wSvr>;Â혻c&: -4߻)Ϊ̪JtA̠YIbfG䬚;{~dIqb}߳>9 /Vv"6qef_j+Yw \{S=jnP - d}}zsW -kTƭ;3si#2PQ -9PwC8MPFEy DP -6wfswq/cN^^/~E'擴 >iKq -ˉzDդOzܣy~;Vn,<⒵YnH#}3#F36N oǦhG@>! znR'Oͅ,8k'n\X+'g2s -̥⁞xP'Et|hN)|ʬ}9595)Vc.lm=s/Gwq/ehBrV"6vTGɒ6qlƧbaONIq;Ԉ E݊\ju6nU6- +KXV迩ֶ#yw0"aY*H-׬[#+Ozv05~S6uG!k6w;Q#1zxޔsr.E0Ny"ŸGE&"'m"}\P*FgP@ I6yCrZ^;_|qtE{e͘ލY8>;%?aF縗R6ӿ}M+)^l9 -5WBzey:dœʴxv> 9(hN)\_ -k <,%;o1OؘP%#J' ,w7S^I+N{m9hRcȢf 7N\Vray[\ǝ+۳> daT܏훓 nL~9YuνyQտ(J?v&#7|_R*SyWqa4Ai%Oѹjs\x>P%E﮽䝧\FYoݢWl,eU4>rٔv9<!C+ޞ]LqŅml OFU5ʽf~OX3vug&k>=\c\) wxA )2anu)p2rw5S8۬N5 M=fױpakНӿjG7vbRհ9,8w"{^:trު0rԈ[c~LbypK -oPKTtQjP"WZMGތNO vC˹`xȤrw[VT|P-ڋu\2B QPA/  u(@c 5obn*Sa`}sa|zd ` -RZ.}zOLxDUikavqm-/0O<*$j$"nim̬! -vd*f.m8DWڊHU"t4,%TWH;ɰXE6nюU޼> ZcA粌zŵKђwK8gʩfp],hЁ&#l@ecpn'ro$+/:'s^I}#%MG@ jeƽF9 6y=nS֟umwqZqY-β˛SN~ijqp - ->:25mǬǡnRdVVZ%>V_yX\|: ŧ/28l*@]苖}/П:< iJ'0Ar NC]`.rz}/&nCs)kጝ}qKpCZULJBKxǑW_LJ;fe5dJ.I ˋ|v귑{%A椵K/|}lUݱ\|̴[~QHg鎣Y>nG@O'm!d2g++[ZOxy,"k=,Z[mٟ8 |v^ @-g&ԣ.-EܢJ xkEu"U-x@z޼b ;86cV^QFUBT))Ĕ"*$mK0U)<Qާq-b|^)d}~{}&a橸?=6i -YE2(g>Tk}9tر -.兘36l2OoKކ.jto @jq_r:Cĵ_ȅ\Ȩw_% Pi4AY;U FJs2w -4Eew^(,hT(lr2}h>5XXis* c -WEdO-jn3,FJХ#g.ae= Sޚ ɠF,7Uv]y9 -EרdP\x`029rXz!jŠ0Z#Z dF%XYSVιwP"w\}TI -Ky9yu?H͔OP6YꛪO@W& cdfEP15>:{ڏ!tI6dC b_Di[VZ)0F1>6߭CKZުu1 Ҷ%'ṢΣi4\|g-N[~PSV~DǑT+|rqbkrta.6$ K;0d9ޅiΙ \j}jM~/߰sW<5>1ʏ㣬㫯:_M VrQЁ~Ai] ˂w> 79+Jr΢`OPA:~K/C}AY^N,l{\XА HZ -~Q Vqs/g]"Ȅ>4c]L:%I蕄u7ct-MFc_EN"0KZ/% -IZ2.9 70h0F daw q*q}ܹ>9멨[ -#<M.f0S :? -=&B]~|ԗW Ǭ^`AΣ[ z)ڦU y̘t3g d\Z.^)m$;Sp]Vu_+a>iB8ڎGEښh?cծΨjn}Q9bS6焥Q֨<}Vq˗[3fcRpqkRTy2d;5_!st Y{-C܂ZGOOşww[5(ine/>8EI̩/oπliams>KXPKA!GߥԬh{ydguu -+HqA M ^:e #-o]WmwC!ɡ$]Yc6P>(]!g%[Z]m>qe vƒ~n9 ֎YWJl@5+^`/=j ,,;ޮ߂FC.dJ9`kNay#\m:ZRʹFy5_TG Ƭ_-lͅNF *<R @_ZXTrJjq.Q~I+k$b REҝCp5٧f@wf'I梨& 鈐3-7i͛Z3iٝ3 $,^b<(mMt6;e0whߣܒ6PNAe|yWO&kJ[a9:Wf<:ZQUbNp) |^ʚȠZ*;֭O^>kgq";-,Ljv5 /ԥ#V #ߣK^}eƜDel -AQvN@xT\ -V򨠰61׸/='kEF΄23'm5M\N&!vAd_p%CƝ&ftjю_uqjTU3QG-j4DLoV뵘[uuzEhPP-_,x=(>+`EBYkZ^.U@?s+OXzWڔv.!$<*Ɔ| ||M{n"r64SI+I3sFkd2l2' gr%o!&͎q*>q8LH{ow*6&} 4h:|f]|BrѾ'AY_XiCZJA -՝_תs -Dqھ?]c!}s760BEod ޙaUt ]g>q_JzSkϳ.e -HFL]Cf 6#}vs}jkBppixT@-Od'嵘75mLk[6&ePKk/m2O.45h[ǧ)QM{ȶȜUB+ef'VJ=uj} -׆\|tkxKZ6^}R#mUGC5 -2EfDjTٙ[*VHۏM>`@VH.1H[w a65\Op/mh" 2+(j!mlO' ]ZdϠS0gI5}t8 \JQ 7D&@! gݪN*}Qu"eR!3j 7!Q}>&i򽅮f`zi^ؕ E_+./͛`W/Fsp56 \hJ9Iesw5=;F:M!([۶[72Ȼ&=pT,ɦl= YU7R><#&!3)IEeX-+NOA]ؐ^:c5 _8x$ϘYzOEtro>&iUu<Ƙ 1daef}1 %k^dQǘ]Ҝ͗uf&gs0\{]oqwqӖoÜ9F,6~ATOΰ/@n s~-8Ԁ:ֈ-^5|Zeaw%\.|ר̹"!o+0x{Tud0s*U!nqDӾYew+M9lwAg`#`u);Yq qO CJVp - J Ѓ׬Յhx@ʃƾ]0R!/ fBԽQ ݐY>Kͬjm|B X>½֫V;/`8Dk$s R2]{a^o8dvrq{rvҴEaӱ3z¯^8R|zeO֜)PG+=9tu\\T[wEK:9Ieܴ֡KZ]un b^­f+O>Mpצg!aWH;ia{ZRqhk;[/9%a#]R -*7'{o u9aCU͢A.,BOZ|RӆïO'z^3`|Tp0hi2imurQ%ӼGvYg$2Nd-[ScK͋{6hX4`7;|9A dWљnЕ[dP!o}%UNۙ9MgڮxgiةYiEzVH,ڕ!`IN܍v l>!~MygKʮᬎ ѫ/k2tߠru[vuJ0Z"uNa؇g}FlYF<.*=9{{{.d=^֣hY1'qm4Neg.*C|*D.\VsI@Sc3TT YBFz2շ1㬌)G!ܦ]WhW_~h1z]|S9=9:k퀟kh}*混R4?ki9$6fѬt.C> R6IץgEWҔr0:r˷wݹE>+#i1N=>K{d?O kiAv%rUՎ95lKcꋿ#M¦j۞, q26sѩ_y~^CGt^U6;/<&5jŨGKȮ݆ya!o%Ҧ̌v3c7mB|=d=ҫ&h;>9˚*&ض3ON~\j&xB;ՊN^0:gR;sKP5ݬZ5;l̩lOV|AC_mzx -Ƅ -{Gzld֪b~& .<Ұ /0pX[NkG$*Fu߅JLtUVy!: - \xKvc]3+kN[%]݁<2$n·R -2 -uFsIeMeA=z¯FzRA n9$53QCŌpO"n=!cfK6eĜtaqh{\Hk&\E|=DDo""^2l0#iLU<*+ͅ Fx ]d 5o#=NM Yy< qeׇMDSu *H ^шGdDHg~3R A}OWfvī.wޒ3}{Rv=7Ҳ²o8'ꈅ@v:MJL<sjYq0OIrZ|XaY}؟Ue䌴>stream -iejNR E3ܩ^9y$Շ5_g}:7QZLy_1;'μRvmM}'}wuQN`~v½ {7*kUbןb[#wIơlmayw'c;Dܓh%PciKoA|ΝcY,bQ~I26wO/{iiEluʯ:uz==h> E&Τ0$h -eRc2T01q8v`>lST YPzl}D5*nL-Dd ɸ1\4FT, >}%!ϡa6U2߰.r(dK/!"U2Trk;VnL ِZWDN1[ԫa;qIabݷkr#/׽w*+j΅{]FŶͬ(Ec.! ( '"j}׆{`w$`E4{;1ͅJY MEU"j^g M=Yy -cPB=hWں9< '9.-IϪA&ovڜ}ecg?.:2J霂zlz}c7$eҶ]HK3+*ŝ&֤nkF\A?[js :'i P K6`~3;Ue|pT@IxD5]X9l:%K; C&&[75%k8\ -1/,f'<\@EL=,dRc֦nKɫS̴񫨰!5#^.^zND0/kcKϴ!'ME]֬MTNx|+Cpth`(xd<' =TOɤҖ]\C@!.I_޸(? -/FM{Qc$+^Iz}P-&<&%&J6chPoD#ݷ +XLXEd@Dd>sQ J :lNwݘ"~|^%%{),tتYnc}PZ5`qt-<3vEV0>R=T@7FkTLgͫhU:r7ۏni}6q@fFكčXbW>GZҧ,jTQ~^ƄMZ=˽[t,o r^vbg˝٦0q;$eSI|~oԧ튦]ݹ=+ݞi?;j %xQˆ~$v_&tN EY -)9{oIk%L$$!cR&>/h'÷x  }ޮ dTB*! GM}X:"(iEe+GV~1╵αϢ|K"f&j9v!Nm& ,,t##F3i3uȝzpa"d4@_uiǴ\ȠĀ$0/{Oa~Oa^g[2 :]ź30 -uPYs٤Zw{7hܾ`bFTCO|D]F5kMI6 E>x/5gVg41IEvRP9 lMNG^0IGx,GQ ƾ -\bXs[ʡWz-SQ Jj MEل_v(sI!+) =d~9S>YG-93ى!VtoQˢ ~T[`;UTj&k ]ki}5nM21~[ӪƝ)e ٍʘdt8 k6^ 2>?aAȇ Zfˠr4J."fveVZA= }CE.b9+LJ`/ԭI>bd=iY!𙞤W'ZZVfeDx%lXXߍ,xr?H "S -= *3~^i)BrE \ࣖWy!%[F|zE©ag}*KRUmyM3]]ȣ_"i!5%1- y4cUBoAuG[#X]ȍzv%z2)Zxv6%ooI)),(Do{Ӌ} jF3pUja`ĂºCPyBBF欴w_n ;tLZ 8`2le !,|s u`j5n)d ._Rdj͘W -H?ۍM՞KmUz?P(:KCOɈVQXԛv}dLG ^eYcgP\z}ˇPпQѥ7F~~n"<yʡlMI־\u _Vƛg⪜Өً_؞7.4 -У{12,d=1YI|NL'=fɇ|-\6Qna sLTϩ=TV>bS.%z&p/oJ/m9qkQI"Dܬ }\KޯpJj0`<P 0ǭfXowuk3\d=Ki~>`pm?Aqzs0KZvO;~ ٓ HZ0hޒUe<s!xw4[6]/,o7F,K]Vwr:HwR"s3>K7}J3PUTlAB]P-$xc Ѯ(dXɘB0VƄt6"(n|ըr1`/kOy4h6̃Pa9;5+*IL/U0/w 3ܳiEܩjO/opGyv:%0:Yj7UFvkD Qvߗߛ}*X.Lq.~Y*,KE[SKi#w%pOIП,hy&4^[֥YƮͦ,]PCȘ9ˉi  w*J=_ @悲v-4"2h='׀)W} -/ -)C=6"`S=Q! >~a{gȿ):{re!/^(k,f* "}Z>ҼLFȺɢq*w{a5x@9@FOsq;O-DݰZ |Կtm:ug7Qc>qUq5f5<<}+ !]a%/7&'W:fwr)PfF\_L%&9'_u6ʓO(7Hp vQ)QE{fF^X ýFbSffIaIfNTOLqOLOL+F6FY6{!uqK8=K =ds ~ޡ!N<s6La#JZEe Iqޥ,}Ɲ'\\ru,5:NEU{Q%S*Q=9-*F풪]`{-k9줴tȾ/Qs+k󁷐wTWC]~ùrI!*c~q=b]NLΓ&KlA񀊍5 [ѬtIy)iYɿByye{!%;畷ni=FF?JK}ϣ*)nHQl8v|D-ٸSPfLY Q@/̅-@>jt$@gIS'N Aրq´ :7A5գ^$? UXx\L_Պ[ڐ޲)[VEwٞ֬fHT| -]i+d2)6Uژ,XJfjwsq..~i%kbDr^,<Ø^ ski|s˯:&XgH@/'ͯϡz3 ЭV&&^2&8SSʍaɥ͟GnVyIg)C/X|8]],ufNV=?C?Iʪ6GgW0%g$c>K,sK:wL 2v%!(Oo΅5=>*D:TmCP1g[LL2&Yz>;lZPiY!r"VitVӊiZ V5dfTM[o~_|9Һ?5:vr/#nzJRz J]Y^FB_/ʎ|PΠ(*( g*5`n5ˬ*s*Z3 qz&(^fZC.I[eie.o%d \5yp{}ƮdHʫ#VY#2+,AgE;o8G7^M.gf$)uSrRӂ9Mټ=&8u$3ǽ *UR9'40Aʥf0a l9@`=GR6"g*\y%uI;huq,2Y0 UB'" -ufWc9 ,?q4}<}L1OcN^Δ8ZtrhfZR|j?uB*(0ŔKSz渦yk\r2𥝉Οv[''f:_S7^sW@N.=`}xؿۭWC#ӡ5}wlsXtn䢽[üSM^3?}~vE#ٝaq M֓NigCXxϑcXb{i{Rߙw\%VXWsm!lK]NM@?Jv_l [}û(է JVfWF˺϶9ΪqP5rF֤:r}}O9-9LzVTLz孤O [zM)ԫim%YMZ\Xy1‹?oeLАsqkO/:6}I79?f$ Cֆߝ]sNnϑrN6uG~vyGG_-__r.?njqKZz<Auu{` ?{"v)|%I{L*S 8pX 11y`pzV =Em 7k>ܨy?=Hk<z# 4VU+`k L.&r4xGsIFz9E)`!ϝD}=)>=C?EuҴ[ݙW$>R,<cK/Q^Du)ɬ[LD(%Gx쓈M_C퟈~~8Ə=<]~0w`>΁;oמ -c|9=ZxvrEve\VFzy5ʳ"j?4_ݤw;-߬><?82;֟V-t>k>v(2sڎĴf8̈́kOɌ1OdYVu՗3#ĘP;[HLw -?7e۵Gv=:]KK?_{(H:1.m<ѳs=IC3酇c{7>\zU|w63%|9F8Wʐ߰ק:!]"} -v)O ٵI[h4z;_~ڿ c3"8U$H6_p~|:[7ۿ>;Nx ҔMݶE? \aZn~qϽU@͇[C?_}}?|s8|dR)] gⓂ )W?o[W,و8 Ü[M/>i[0[<50W쟣[>\ c ][b6(~/Cw}%,qq7!`IwK6^+OZ8/ Cӱ{M/ sg\T yx%Ӧ?SO'̩Z6p"z'߭'2!OkO;ީpUywf)Mcܥ2c\X3i{[gsν}8m5/c q)o^)Ky;<$8qrBx1>;KǾY-% u\{JĦnG%s,5y*>\|>Pm]Z|]._QyYx?:,-{zևe垫5~??՝7;?÷Zg3=ӓ]PF:䤴4k Sv(rWx(zMvU/=l6;%.]?`mGw0^՗5n$z5 s;#ͩ"7G7v]?Z Z_ןN4~:odE_9CGg%ؿ~:k3}5s/wl]R_cs:c#a[NKqm{ Y?}]~;Y'=4[ lSgWW['Uzh}SͽE݀zZfy6]z4Z#CJB РFVwz;ȗ:qK [/A|bsnjlz.*e)a2&bGa O-QU}*'%̉ _wb5/W[o.w)g$>"NW<[n'whBiq!$>%=PH7 >Fg -Kvw;5 7▱9>e ֈ|ǁ{cU~7s{c-a]q~|S4!"7[owg_j(rOtBc3ʅ![Pϕ7sU*jH9ͥ^u -BJcǕ?yw DdM S SɎ*'E]7C?NT}Z}`ޑ#.¤2RB4"YAq̿|M|y'E_,x6iFź2lUֳ1zڂh >f qo1џ>ct &/[n?# -G].^N~+p#A=WZ?JI_[3=5ZgJ]>hzOqucԏk(zLr0p]hK櫒,d<3A5h쇨$66R4lы5/Wr/ץSCo;d؃ 6Wq >fkdO3UO'9qw73_9' M؄f/'mTeqn+e9)z:T#iOg\2>lw oEWۡaWU/*4jy7řO:>mÿ KCӓ\'k3wFJw{lv"Vf>4TA^rAB֏#n. 6JK_Āvq}Vu_Ԝjro}1]|oԃQY~ /9P~:์mi{߮)AֺhOw1iU)@x( >Vd_f'kA|])ՇQBMI~ 8w`?,CNd2vJ^heN -v׺сJN-J}ۃqD:^S~e5-.Qojx.mwa[}}9>qO5gYn/y^FoǗk?uuRv^% 繜|[L3FO~4LJ})adzwCc?_zu - kQwsƎW+M. I/7:$e8Ϗ2PNx"Q]+mSϡ R*6p L}=WXZI|Hz{$|#,D-t8EG9FQV2lMZ2Sr`SA|dTtxIf_s /~Β1zή]iga}v$*n|=k҃:Wb_Fgv6-JP]/^Oe?QwB.f 1Y'.|NF]n+??N8S1J4C OԜGrj˵&O˰tc(NrPPR2f!C =1|F6 8 -g=I q}4%)Ǧ9HSVW|MʣϦ!vF-"9AJJs y9^b͞<CMb!zC} =bO(ZF*eL/{*Z]ASK7삸 ~bkt[A&SNx?3u})`ǓDnowO22Ly(?$?87i(BS/6DH`l+t Gݟpd^m$xR[n 16.A຀߳lԁg%.v5}5+Dvs8Ué, x2#dZܻN _j.;oj8@)%X.v%H2x<3R]ulr?O1|1xۃ ;1]:]I9Fۈo\n/sZ4X濯$Ԯ$賥Z紈mQRߍ-߭v 9ȳ R{#8`ݟqUGʂ׫_\M8;t~׈ҁJ}8-\h-./Ǭa 6Smw萲9G}5l}dfP{aL,h_Aj5p x8BS_Zl|7Y%㤼^itA}z[ -%?ow7{Q?ہވOHq1qRN\Sɧ1^C+lM껍v,x_A\7?Pq`V):r)ut{W7&z'9mPu}R=VӐfIS-)1]t:_/:FHt`{SaZtEQrf1Ve)3l[O)ꡜԇەLTmתBk}fa@n{*1qrlfOifE9НQr&\SEJNH1jhZY}usiȩbzJ?_2-4z9,@'s5 -n eo¾ק?_j9Lٖгc"Ž\4IpT1u?MS^ۑ# d,Щ*."F;_{ JT&< ݓȨojAzKAMc }<])ETDUMNJyܧW1+=ȿr\z{f'06Ү¥ؔؔqLmd޵ (džJdVTJS `e[с{UMf%g*ґn?b{ Ccǿ's 94)E;ô~Jr1t'Yy@S1:LNqye6d^~?TKLc =pQ F]IL5B-C(т}hC MՋ;і1vy7UUJ] Zi8F:!6.Yn|Q䵧`PAsG(qO/5|ץF$8 C$R#>E""!xz6M/9lt2w^fVG}ԸG㬴 M} 0~uA蝿gD=%  2#bZ*ܡvlu> . ~IaȗDٚ'N>H#f-wѣ}yz)ۑw>9(s,tZ͖ry2 TV*PçjK<k ^|Lcm9ݩa- 5/wdc}eA$1Co m`wmST/v)y&SfV[;pd{YS3ɏ=Ί;΍uOsƝx;{%áa&hƶRQk~ݵ|B9xgdꥧ- s|QN> hن̏yNjkV޲V?$?^8^b6uo m9ovӅrBeuWGNG_{6V=f;U=^`+=*ʷnJV\Ma~_?J/{޼|xt%?Aߞ߿@LzUwe^2,jdyqx9>oǖA2kRb.pc.TgMB GנaYIOTBf#iGƂNn|'/Q!"{vZe EW)P ^D\⧆_!F@A\@WYI;R!|WQ ݤ3[C% ̋n n?䑄s_ -7e) -۠/.2R" ^i?ݠ,q{ҢBf|Ty]%u!>Q/H"Jo1S..x -J BB-:n_M:9Nߒ#748fJ7.ܺυy_.^ -z(R^N?ɀ9&)=}[JwNrGz>IY( -Ta|++n\] D]FE~LHqyXO/| UweS"և[ ~[>=p}۸u "F;AnZj(\ x=qmP^矣_ )lr_9dA7*,jKF }-ȦA -DY '`Eݬ̍}CX2H|e0aR6j߮p`Yq}+zOȻB/ȹT -vs z~6ãW]MPkPnυfX(ӡ!l ;qsQɽ/JI - -dOwP/( ۃ[Oo@n\ r 볍ںwc'Sm쨉ZN@;UHN(kЫ;7A_?zt_?@ ݿ.uu yԎf`G6^h%ͻ|ڂC4`)/{e|?y<mПzu^xyN"7-*ayl#jzI1Gzt-rz% dx ('GWu 3~t\ x0X< -_)5pQ+K".Jj:>RIx0( => wAoݻvt9/W`??C%?'si>HϧlWAN['DhQ?3}|A>޿ -zwA/n=z zp:?@πK bdF⿭ !&YخN0k#4U=C?Z OCH$Ow7\A^׻u2/?ݸCgRvk]Mޛ>X,xY(6Yy+#lC71t^-{?@x@sbb~G=5@Ww;4">9|',lGQ4ƇW`6JרK?<.<}tۗ?}%|j֝zՄԎ꘤Z8ZTjrj d]D&D_(zx}{ -8=yOO?HI!1)x}'!P]l`u`UXCt^`oOP{Ы@pG@nyz({PYXFX}-&K3 \iݯt(;Z +Q*̸z3o@ܾz;\@?<@JeU`Q3+O;JJQ͟a װVaq. 1r}{x㫠[ ztЇǮh1ޟ/U-"vM >t J`c^u1q/D0mpD@ >>h/oSKAO]1k7н p{!+m 'ӡb>gKphYQRDm[es!$,Rڗ$O]@, -ç?ݿ~X'A/>兄O~0UK_㥛eMBZMELw~R ".~z 5Wz `kwz>{r{Tb$nD~LeA,;-n(*ȦɚU=G@s =qx'Ϟ^gjV1"|GY!uz"x#n2ǭ KyA#/)"swXEuO\w;us ځgLTNT`U6f@ͫ#pύKp-- ɽAHϼOJHIq@|&kYbjw .¢`n h5qA@ E;5gq\&%>(Kȹ - ϼ z.PˏA=%_zab'=z[-(DX=?)!qdtWZFunSk)1>۪,S<[j kUU9bi -0Xе#g:x ˣUIoq|qWMG -֮90X]C5bn(-$tݢ޺"P@C5&B; RȝfxEePѮjS5Szgݖ1W;_v<ʊ*'$jƊ--:2ֵSW|lf.yV%!զe8 v}Mm}w` Ȁ3tybemV &+{%noT[=cM+fT`w =Xj.*Mئوm5#oYBl.nW-HfK&씱&fPX7Ug;j"N;\:g,E/HYU|yk`5=,ijDei#zM/;v3*jf['$ԲJw'XW-OU0\wjw&jZI-Y֥(854JJܙzyK`Dr̀}j5æBJt ݡ$ZS|J@ݛni[7Z5Z 5jqX6ʫ1*Ğ -gzdn* &v\8:щZtf`" VV#݉ pڊmudkO,FnsBt\cJN鮒QhW1Ku9xAeukܟ0Adǔ9ec-*kbbx^3[n׵kM*:tgip`3y6*{0QGȯEl,"&hir],?&ߕrKlʞ҅fhXlcom{^\kG]V.VybёAp<'?4TOWy:B8!fir]Bt~쎒,JydiGG/R4-cj6`8"lRrf88脍CxG,m}Jґnob@UsCa -:3֌ؕ |҆0ٝ/kNkqu7;<-{ϷM4 ---:ߚ$/<]}۝Uu[|)mw,>VgX䈨lZ*.r;6s2LSe>0{GaBU(=PDto_clAT}8FMUL7ElJ11}%/fkce-bΦ vi&R]zѿ}  -ّj:|O+oO0+)O|v|U%4TeWcb-Ph =y<_7pާݪ"v(i;RxZwGH}1Zy~4Ur'dUhc -t>v;e}d -wb YZۡdc~- &1n]'Nw\Pgs|p gvԤ=1HG+9n>=M(:Xn!ĝr`NZ}>q2WN{I 2+6ؠ1EmXK 䢓%*t'2+Qj|E]r4zpz4M?&2\\/srb[5>hVrP]E#9lVz2\L{>H2)zs2MM[D:hfC^\(:|KN渤}p__Q7(=Z:Z5?D5vҏ uj3QMi!@'d䣹_낺izSK/>^jumx^A_#F& 2B`#\zUXY`އ ڬmE^\Ԁ( w֕Ϧ Slʢ$ 9:t ߫UrGO( -!PKWs?M’^V}/$HS]Y1C:^X`OuX&%mM\'F -ƹ%{lԎ{Ɓ_9֗Yn#%mpq:P˕Y> V\y8SU}6M鰉 -ElKA ЎaS3յ:6 |~؏ EG<1asխ:4%߮A%eS"#*㔈JŅ{UuwT-cH4yAl莌:[ydi`u(i"%'wEjпa.8Z,9c[*vYJIUqFqE-!h9r%e>#ح|kV\u4%djyX8?92l6)9hy[8cRkg_yTuحqVsx:N]j~{r<4:Nz`_şѱN5"<paS'd9">7Te?L` -{Ci`n߾KڛnjXo'N"$Y\L#0.QZW3 -,RL{GS-xqy!7X0р1NN5&c,J&n{ -xGuN -w,^bed@c&τT - mU;JrENJAi9wGf*Nrj0Uzd {eJlYU!ʚ=\{sdi; -XEޑ~88%kwkͮ䛥~x4$Io-u~N*LMO((iAc[FLڒM2zXP܁\mB)9TkEXwa* (o#+jo"nVz4G)"b>pyzQy2Ǣu -{NrpI[zK } /Ӧlmޟ8P5tC(Hqvg.!+XmPs/goUe{ofZ`_VZ>&) bsJc<f&91[Q~r]9/gLw_h~6xrs>2w=ԘiA)^] 'le6a2OW"/=e9Ѣd7X4x;\K,8T6)Ny7A(=:t} gt7v<щCn~: v"p`t: fAX],SҶAkntp=kS$jmZ.~X -{Z6td{s1JJuj++35rXW_8__&!ntCX(Obe~;c9h>cR={`P)|Bm `rJE$cjr/0c",rtpE`L2\ :bnW]6jS1未uNci.`iW`]y:Qõ/+ş-= Sv&Ϧ&dx =&:iwhD w%A~}; x".A3,ܯ%ZАj.yɈO-@'ƘЁ]0/CcKUE#);V+"OcpG*&.!w3K rS hYTv?V3͌975[+m@O'iSyݏIIV 9a{ы FY׵`əs߶G(﹜yuZs1Z}w*a`S܈In7Y>W <-8-\N8ҙ*'en=u|sثBf/qJ~=*M;!@W.&'ŐWzA 宜r&9PǨm) KQv%'+1YhF:~/c)E'zj$9Y?c]̗*lM+5MCH!nGO$ATeXi+ ٙ`#1#YZtFȶOrQ{SL霨jF\vuu -83ש %uAWsrda*G7'5 ^hqY6<̩&e(') F9 -t+բ8S6x|RSKGŬ~;qO[gĵK@K,e DeU23B {zxlc8f{]a^cr۟(01PҒ4P]!4.L3WLUZ@_5pK1{_(y -))vms6fq:Xǁ\j}1SPfFT_V_ !z `PXx892 -_y3 2Nl_A,ޟrFYz6]NxKVIRSArrG~wSp!ȯÄ Q?s)ZلhDF\m)wV2\m:{g(?x)0-B+6DO/4%d~r pI葚<2&hJu,5Cѿl¾%?r9FV8=|Gyd>[jB%ms2_$}{G+6>*W%7@|~-p3No#2u%aSjZWg*J\;".'_O3E^Ӣ{^ה.G3t̾qxyc2Y=+3G/BD0R6s -v x^dwG<4 -w1IKR-hƼ_9c\͕O u`aj^ŖLC=7#L0!2rc8s)='^x -d9tScΪfEKٛ&.rvbBt^?wl<uӊ_Y%ty5Sd:y/%?v2d3)AjGZ.l{{A=]o}6`ܪfh>s.Bܑ ^3bncn(^[ C.ܕ▛ -?y٤ЙڌaÕ6_* s%Rgb[js]oOM2bi|uK}-d v-Js /ƨäx}5V!b]bFXA~B^A# rcY9˕Iq)F!յ ˵\o f%:u1TcZU95G -N9+IYP#-reӉGRrxC 5o]dl{2v$"pc"dg3"D;GIN .a 4YPZr 0lM͞4̉GkpuSAs94])62IU1]ŞBQJdKCȏ]TYELP? rEw=<:Ir艎M }I~f{m|>5pNn12*¡dv -.tE\Q#of{NR!, *-q8Uj4޿gZʭKܕ(pw " %Q@(2{c}wgYy6:2=b 8>@W\cO\IИvoU=z jؒ>:{+Y'LSK#XEhܽod`jjw>_yQ#5|vrWˀ98/V5'z -(]Smڮr'fW͇I)ʹR}ÿX46%!G&!l u9n\ -X4C*X򑙁^x<ևorZL |^x;k}wB3U۳ĭ)hZ+ '%8Hţx5ȑCTߎi a~5 ڻn zBb/.m}KũYF t0V{A;*0}[h.m;=:ϞQ|Ŧ/\ٱ*yؙvڕv˲}1JD^̡~^` -9R?SO-`o*yEqáCՑJo$?a(dn̒CzZMHˌӉ֒kf͞tN;CGխ z񖊋rR34:#Ct، kn__l=~ _!Ss `I=ЧeT b.Oڡm5M )2E~9ZY@6&MWj:@͔ ЖUJ8!-'d vrFzym6md#Z `2( 9MObc ,%A1oVlyД{7^.y`m͹/~pdE %RiBڡE>pK\ơM ~7ɲ hj,z{AAܴe\F<6]%u~[~z8ϣy= A*ԧBfۇj%EzprE5[#|l2,6|ns<ȳ*KHt<2J.,SeA} ((9:V.L@16.ek7ghe!뗳+:T5x1 Mee\C嬎@{jN5("qOI?cwU̺徼ܫG"௏BcFoY(햂crkxj[pFy/zH>-LYM&'{ko)f^-:@C̓GͳA5|u/VGi2cE꽛i4.#'yg(HyWxN-][@<ߖ $$("vgTJ|v&ao^߷$Å֩H6@Mm g"F09RZ{fk+UKՔJeI)1HM> :>iD*;@9 bb -NO(\;&HhGą2/8^cK Ҷ_蓃c$6&i9* ԋݵOc ] M ڱ)WɥA9OvOA5w>YytnDCxULr1+&L W4[:vwq)ĪtQ@ಶ,*9QJ@Uѿ 6&Ɏ3,.mm6z6YƯĤH}n)|HH-9L֥Ԕr&Xv<m=Eh̜zؑ A=3GLL$03qa7fh]!;RC}FǑ1zF}-tWjf\u=e1;(萺2~m({{@Qnȕ.*wp¿XVui~PqXm"0̡ۙS<ҁG?si3H]b蛍QRSsI,%MdGϧr|aHJI;*ۛ#* sv׾&E߭ b?-mX|Fa#ot (:m,;C\\U> vmKcޥfoO ƐS眢,KqfkyCc-aܲn1M&8/VqÖXd{԰M9$1zPc w(2H|e^iUf-p6Q˞E|h bNuD>^6.GQ&:VU+yw 745n ɠypE.Rj2l#F[]OΦ}[bXˑ~kO=ؠFPxzy Mk2ry,})%uT5!!?\{ʫε4D#ioAȵ⒜ *2;I\FTnO -su3%/FH6mc10ʼn һRjg[e<餽U6 -ץlJ_xg$ȑC -IEzjt/Bp [,}6K ޟ`^ %8[ -Eg')1@*ʻ;WzW t&9xb>ïd1~ [LX'iܙcbzrKVvMZ垅mn)*ܣF@^N, sCd;s4g -@GFhY@,z:R(,f=X̲QV036y0"}@h}{YSqNOΆ!F@*Ȑʱ[RDwck+cuk<{?!1[&e!L[WM*7<"{kov$dHqN[^s0Aӌ-U Witk[OաKz?ܿtcq d<syEgCB[02K9؋|0J${vI׮CpA=:AMߘfz2rC_ںEzWMth#u^\wtYFY]j):1r#$&I#νfrTYzAu;0LM򝣭&g84#GvʥGcmQ0$y+;^WlM#{敥Q}I˩Y۠̍\R[P3g 0rkXStkUb?F$ضݚA>3,ʖ -晅F%;*\_ LO@uaT]4>ᒔYt{誹oW0RN7Iś_P)WZjr>]Uàݓ˽Ч}rstQ bK$LB,2`pBB_Ĉ8ppL[A+=KarRg+)>ӭ$OAn٤Dt@"0pf*$ǫ!4B0􍱭ּP֓n=&z [˄x"n}>Mms~HGEۇ/7a+LÆt oL֥h۫2/`f[4%nx=NcNmIcĬYF/m!=nG QlK?IٳwjCV? %:lӂz!ͣHqUiW Jf~K[;잎Yt&*|\'gpO.okYH]=`UwIQ;'fhIw^o%a@ 2@3mSъ;:VmoeiZ?@O)C -|k n Pہͽ8F9֮ҁ~iŬ焕4cO??No*on(wU;jbޡQvRv?(|Usc]\P5&3_;atΖQ(nnȹld_ wy:z16ÕN#`M?N),Y.L+~YbpYPv8VFL0+62yod?]u@bu\x[Ksa{owɹO~q)ȃ2L,WkHy^%Ч6mgS>-I9y L!#e ͹WL#[[~R}{e= >)Qqyf%@K1y[*lіRpI۷1;K\[Du?~տz?LsVV+COmlI~_xIrΏQ4w&tY|;B76ۑczlقQPpa-*y=/䬗oNQ\ ˉE97JxS;3t^hædRB*jݎ -QNB>LKCJTF@G(6`_lݻ6r¥nL3s7;ZzE7I _^+_ǻ -5_SvtD`n;`7fi^%PrIuI;sr2$κlLCcf -ܥQڸ0kYC'URyxEyCi;43Q%E3 |$4=\9XvŮ]&ZXdws캒:')2]' UOD7߷Ɲ3u FIMKc4QH]`:ZY|ot Ze/@-4>RpaI':k>*.y*QXx}k.6 &YӨ- ҋ{&*'͉ɹ'@ؔ=$e bήO -ɱ9Kw (2G7Z鮞OK)z[IYZW12}} %7,3RW} ыK/“=X18L;$T?0J*n/4C_ @M,~̀lMI)){yle?/3pGVl"2Ђ8 -~J/z!3{,^UR<|q럶UqwއR+7RSƷgJ:7BR^]奾>/ Rxbs/JĜ! W -|R,AP\JJ>\oXx3 0A'R!S\WkՏ#gO:IqLONN\-9IWKF)Z쭍|w:o]Z#X -ڎV-3o2JώݱCu-3I$%A> Hͻ?e'}2rVRre{AygYAadĬ ̌ߜ̀w=%&x)hݻG:QƆsC@rdkb7G+X9!)l3bXV6g~A,Wz䎊F/Ƚbi̽]aDoʽk|lk6^,{({ޟ/c3:&d_/be,&HG 2jdydbTi֬ 5p]|{7 -NfQLe^46>H9KUwq=gagl] 3$M`Mb5e,= ۵0Q4$yr扶.J_E)]|t4#3fI񽠒 -j$^\#4SJn(7́K<=stQF?|ȟ>g:A ,}{=6 )$!WA[J|h˻'WlHNԭq T_E*5z2Xp-&nkX)N-8'+?w^{̤:,꞉Py:n2˫7ȹkuoܛ~Y{!A-,8d垚"矪%M~ i}7=nt6 vkCD4~S4 sVQ ;f>ë8)8!/R -v(BCe#𬔘4!r]/2s",(.(yWE{6)NeHJ<zܜupBɞ -qtV Nס(jfbrt–ֲF.%xX%1A861Tԏgu ͠k7w@N,5ּ4VCƜ03owy}iewMrQaai+6\#æ:Go 䊰KpKC(,*ߛ/+–ܘfsDYT 2ʫVOT *or_@~k2|U -Jbm7-v{Tܛk.g<񄟖^3*3 )93,]qg%Зf|qCN~y$1󬵵 -s$3m'*0)| w`aaZ.έ`V+ĕ)v5..cCx㞀CzЏd|ztK͟ĝΚAX:k^z+_<֖FDRJr.ec9Ǖ8QQKBzrI(d?)]x~Ɖ.ޔB^,ݵ @^'yaJƮ^ LT<]tks*N) B+cV#:O}aRvs| ಬRV5`]b@IS6g 2ȫv"YsmF ;(Z}9AL7d3Q.쬳G&QŖ;+EOF<\^oyEYyQ9\Ѷ-";aE߱uܰ+ -:1[rvxuЏo/U/0GI֎ ^8mjm -:J:Z7[y;db}jJ<6KDaKsU0˼̲!BR[C85L/(^Tu9Z'$L7/F|A-,؅KDޟwD/uV>~T¯rzib3,EiA&Ey{"8Y?w=CS!.GW"D܅|n)'.\dfd>gW|s+_[yl>PqҒsEJ=Q0ڄI\85͞1Zю9PvWΌ=zꝡK^؇kล^T60Q%?bտVB C)8Ie+}wD`m`% -$%h ġzf1Y\]G神$&]J;%eXGFQCDTvsN k8zm9NHlkQW] 3·qć>}{qnupe`hg1PԘ|=QӜ3D .qGTZ"x` k;sbu'壡rucBZ<?vAY,C9b{7==.]}d{UdX#Koͳ!/ĄWÁ<ԶG[l;\j|e$]2t_è\'gy$r*6hN͍>v|+(!󬱡BPЄx钒=2F1K}uQ.06Ak7MMbR2F`>jB)^biE쨹8SdZ'rO͋QR)fm5vH}k/9q^r^X+F^;RBOS²Kܢ?O~}7q|Q'*뙢z1wpUtd=SmYsk#B-䄱w0Oio|l\{}_3V(̊*o:IɼgGQ;t)F̵{ߩ)"2JΈ:4ҩ_%jݞSWƉ kS3 a?aP0nwTb8&a( +@]֖\@5wbGKgUl ) '\&6mW*6`mS3Q*QM)6uSmDk 6@BJ ҍq{>lH-TW+^F^h>G&K> -oz| -Lr@M*>XЏ9/6O@tg.xVD]Z2] CtV^9WO^m־9ZB-qf\=;ůvSmݕElE7qFCxPp+$)^u3Niz!w"nX}4z10/̓V-96UlwU2J\FGW׺^l }uIQfOdK^k*ԑwy}iPC-q`k͟-B.n)9^?YGV |Ү^1(ԐVX 4&,T15ؗ#| 3cV+ޘk%Br ~Oŭ51Jn0Jss^}?N9naB %V- U1 vR-{yn<i {P*VxzXTP@cWK06B} ~R6'OITbc 0 {Vl?Cowr+yyk[p\#GK͹ռ_+}eh(y`e?泙?6::=U~ 9io)~4+zywiF밑 )!G8B99I;=>>D|ZtYЫD:oOPN8 |ѻ/1+ږA߹JoG@ -owA~')FᏠ\A}.oOf!"䇍 TP-RDw>vE7OnTʱ]yhS }dA -noi.t1JÍqd"=Ii{z\rGAHx.9l'+ug=_l))c]B^-m{&ȄYXc+cWgY%KuioZ6\m]=7$oYJEPVr5"2 m{s3=5~5vX'dcWuOVE޶0\2L|+[ K_)@T잎 -g#GuLcB;!E:&)JRAhtͱkoLk -)7cs!}0\wm/C Kuo{ @qz5uMF/ti0,zM)ߙЏōa Q2PR!~- 6spiJ֪YdHb&RV49BJ6ᣧĐra]in)3MK1߂zO˴ssM}l}(>J36wCF\bS69ƫ<:1xn a喝^~9TJJ˾8N;ヮf3- JI;hbr_ݛArR/)٫bT L86+#/ő3ZLhŃe פ3| F|>F+@mEW tFSk[)bCրmpQ[+%9N8f*Gg?޻TSI^9-B[=c?1'oh7DV:3[ښvQIi)vVrm k2fEWU=$xaYG)U>بH)dw1M-P6<&.:։tp}7/riCD3 :qe[>ZB+恣nxs +2^mӋX[OKyuM.}y+C%"%,Ӥ'jxKƪR0+CFb$,M+Ս5Q{ <ơCpFzi%5s!emyk#=ՀZKںJT?WKr,vW>Ұj& -qm6|׀/Ӂ,=XIq)1l32 #ظ}+21VFCYHU[|NxZ 3ee%mp! KN+񶎇Fj%<Ǵ!6\ ңbF&xI6kr!?4>d:!&u" dҧ7f8 5y]JYғE^}.epfZkӭ,R,ԯc÷t_vyKYbWF hin%hmc)y0^mms)5Gھ+n|cD;"s{Qڋy>E/\Xhx.&)^ HXW 4ꀚٖ6ʤlnBn3 -z_ͭef/S~tQr]Ӵr4[9&t]3:U,rE_մ7,O kT͌xpufy]!׫5K-Y3NfM3A |oY,]^ztVpGEܔS6lx_h5h^-TGG6P[ HU4٦>sԭδQ&fTZ9^vaP#,O -'ڱFz8@GA9\I BO/ .O`&Ww5s|L@!|5 fOeBD]Z0q69ea3e7hV?g7o-d9'6Krxº>[զBL21gV81P!E.ÞZ+oEH˨ 6*w1*\x_J6BšBy ߣ\tv;uu=ͭ2,3T ?Goݗھ;;f̞viDZ TT,@)iI1[w|}<_ZuG,: #!׭ _04lQ幤 1:Z;^oY[<ٺp˲%E5%եYi_㮻U6hk( ?=HY17V iomsu_o0M-)};vRێd'eb^.U[&\Pn .}e`wZMmg3hۼ;Ժ=ڶ3>/p[6tԢ -T3'5JBsUMZZ'H!Z>¹-ҵ&r{wBZ:%tH-ݶ7[:.l5szc*I |규-M涣vD`V\0WlTh#iD]5|:9c<nCcͤ2dW9K#]شÑֆ={n8f7FLΖ*m1a wᎧ=V괆7.ipd쒦ܫ/VTLXGߞ=%|FƁKڊ}{g; *y@-iYkٔ:Oh+6qɚ7899bAҪ%B1ٖ,oԯinCw.}Pr4A:a;ηlZjJJ撊^caT8]|ĬT2!.}ZBN=C7-,̖e3+ @Z9ǮF8 b![6n{djLrFa^Ǧc}>)@J^٭&ZJ5Ǔm&ښjr13[,|Dp"t4*1&ԭ{ZF{ًzaP;+=!0`p0.od5[8eo2=hh-BպޒvO]ll&zOlzD{}W?ڽ:@l{۶;ͤȔE&guKzXg.׻,ub\tTbg0&f,ꨕ{N${Ţ tȄ`@f䓥1&iڟmLH5 cl U^=zM"6p 21)hJjw͊z*fJC r[qV62dK;Z(c*FYڔ1< h -Cik}[Gk)t;ӵ-$UK(UX{VlگPu b!B+, *ղ6mB.4u¦pȇDzPYU,fISHdU7 EVN6mgMrIgpl閖{۳El\ekm\z\Ձ43?Io\=73@^l.Z6h+6ߩznۂ;i>ߣ{Ws@O^P7k\35TR`к&K@Nb夼qJZjjHS6m(Y6M (Eg^ʸc_wMW=UawՁl~"諶f쒣he+bkYx5G=rh5k+~TZFfPhC\q,EʤkzُJees6l܊[iPKgZy糯f:4; &xWftb^2,Nz맆\ 8@jcV̤ c-lhf%=6Mq%lBKgQj`q\wwNmNf⴮O0B2JdEӰ9Z>fkI«20uK3ӭ#EVkžNYZOӨn ?]-,um!]QuV2(.䡈4VrEdR޻6]r0y-:mlޟw{csvg~ŧ|T}\Zis1,bBZ0nݏxxփ 8J}inC0u'mƲW"c~LKn %&s!:C\ -Gкo{f_|AxA_2t1'>3,>hlcw` t -ȴ\d?x^^ !Az#=T_%Hx[Ygc#a^CJ^5!R'f%)bSuCD.>䋥$˫2Mo+ -#sb.ZNEσ|~5VN}ndj{U36&k#j.)w:֊z5%~/w oa@=^x27w7q]mcх_ /( lm>I`GԬLaGv>>x\\{ -<^~y<*0?u!)sN^dy`A,Z6J&W7;sI]p|xZ&~inF$  qY-c;o&!+}xȊqja憊&Li"ɕfv$g Dci)T[j$QUP!J-j_V4WכPf,\|hlPtĬI!٩dJ; ^I~pőꥯLoFY.1w1:Z4zv1:*Nސ'e؇d5)mV^fo9[' ]tz˓9hg3_ .Í>-B N/ܻ< !(&Kf2=ԆjTϰ8ܭ'wLUg:I!15~&.sL\4kURlOF#?#8ٵT?qRJ%$^a-}%B[Y 4ȉ/d,iIZB] 2[9SEIWU676pL 4(heDwu zVSwl8fAg-pצS t -R-BvwG46P o1W y7~Ni= bP<~_I^&|PzN@&"dwEtI</]:AGn}_E2Nziڅϲ=B>nyꐡ!xZNIW BYd`4=H]!=Cl؅=t5.2jZI#iu6NAWe$IogB?/Y Җ -jH XIB͇fES+zibjBں~+C͑(qe4*>%Ty%g۵1DɋJ! ##mhb![SDtIT!~qoIYopSmYueWa1JQP2P٧ -aNG:cUq?&QAk 0!qLOru HyΩl+Jmr^Y]"kHTQ6z5>EU)}KGX[ǮQ=/#-%F֎53(˚P:tߵ)eFzɤ_1KL\U`YkR5dZ09oI4LʝI6l3 oZ3Kݣ' sXʺ!*I40D\2٠!Rt ▗q -"e ܇ce%- >v{K^܃sQ0@5tu3I[ڊe)⽡CDoRԘ4%%|_Ecdر?f;IUvr颩|=}TN_3FnGEvrɑZħn꫿L0\4ԗ :rg?7gdh'.d^M]2՘3| "jYUoN -Ӳ͘whl'tό=5-<a7j&xFO A2)%L鸨1ygͨ)sVIML- jYSiIo@f43>ONr%La)ˊ}s=x4j,򤱧ަl;v@j3X:3`ğ'^-ѣ]Y/5/.LɡVO izӡ^FN\TEZ1b,lM:u0i] ,ٙt|Ӂ? M(9.9-BhS]+y=?5<+}wh:ebl}n[ 2՛t^ߓշ3=WN3_gS͝O3O\O>Sؗu6Kge,m-XKҟ_S瘍{c ,RaMsG]Gsln&ͨ1SjbhhյhiD,7T1GNZ -IBX0N\UŮ `V*r_KRp%2|VR`g?5"u5.mW?8bOluY@ ;FM꿍a@ʼWˢ{ۚ{Fd^qdo$lɥ:@G6toIZ~أ%%"lYI8T؉fJ$|"5s'jt*aK۔Wtd^ν?[βhjH a BlҖ.qIT鷥 ?6PnqpS :ҮmdCZHEcDM}9" sU 9urd۩'&a\pPq€= 4`m55%,Ђ/YSV~(fyaCUN,gT6uN.kQ#Ċ/dخEj5XܝfogX:bˀ[i;FD†$~`vX04< Po¤jj:bU5jd⡵-bWueimI#3*ꛓTr:L^x&ؗrIez -eCHtԦY G#/y'`-cMƞu>sLs׀NO -r#,/.&zg,j{?#EnZlc˞[ -Ɖvbپ0 3֮U,le Sm¿֘=mEж6ŇzsB.)y̲AR1gݢղ 'OtUzO|<ұmMoAFZn8Y1'Zo+Fzզ6~&-|s4Tqj>ϝMu9O5U[rwEl[[oK9LMuU p 5"-ӑ]yg%^.>u/,ںDDZnqȰ?4-)U$Lub9[ʚUIIn{k%TG=1E^ `u2aCV5P&W2RaA"hc--Dlo뱶3wTکSxhpص4 w C'3zщEI9~ 9t_G.>5#Kץ%>Y%~=#W K*Ϝoc á}CuԾ! vϟyl@KژC+Zx<惞}&ִ!`Iن-{ _9 -ԁϋ -}#p[ (RB|vGU'VB钰8ĂL,ku*OZl*J"VD ]Uig;hUV4/|q6¤;'2^%=2؉?r@˲0EetO׮<ɨ?j`|!#cJ6s:l6 -YSC?l`'h-:xSrb ` +κ;QhQT&go(PJ޺UVn] -mkY5 ,ЈN9en߭϶5"|g+d.T\h@Ltz|%g&<1,8:VH=ɼ"LSIM誼*f]Y.偟Mul )=5}.J*.Yh&۳AoD%{yλ=ޞyg-{evo] x6Ht󻓓eP1-abБf&)_o@gn\#34]*KM -SKpmxuvІJ5 AU/Ğ:f] 4b.УMu}[{"{9Pz1q oܢvie0hY -oI\-,|%/} x"B<@EMfRa6--u'>ڔ**þ6nނ]T -Zޱ6iEݗpK(<d>:b*=`{Rw`T؞)yqSW( -sWSsg;)tNIwfDMƺ3k].IϿbx -gf"㌆na ɶ[]j?o6OG%gCȆ>Ae/r:#u8Nz|hE1T܎CQ~{C}MIh+~2 -z`kݝ*rAt؊`eߜ+{kC܄KjqI:DԶgG] _7$_31rIKbϚ 귫 -Y[+}#¥Č[?*t-6-50ԅ/ |%.^gCОܲ" Q8 mRA_G;ؔ]&`SI!Ze%.%k0{KmUM()# wxEB"/-uk tT|"N+#T/>D"}ƕ|N=謹))2 3GOOKyFgѸgD2l JcKE)xs8sBXRlmX,ybaDU9ՙg_CvR؆=p$~<KLe|E]"fo-MyO Yw,̬SgJLSєywπNY6OGC5@mm)}=іsg[ =.uSU4K1DC o"'Bdoce@͂BȻɎG -|Ҁ??(צ':zEIuDGts#㑃P[vq aT#o}5q*d !{S OA—%- AEpOKEKo6ew,+*TХ~[{w=¢nW&nh˃;pVb~yȸ]uUԎ|2~"*zذ3T".0[}Uך8?svKK3F:J=u&.kNͺbyk~}ksozKyů纳nHq2A|O )t@jBW$U;j¯)[:L |4s eVGޥxIECԘ{fzu+=t؂ȡž67 6F--?1߶ԅ^ ! ȸ#+tb1%ߣ9u0'fR!W*|gU7䨘]'kgzMprX7Ӽ'k2߯.v׹^lɷm1WP=-&^C[2΋kV54K}A\̥A|t%&g 5ڪ&b@-:Sc M{3xr K -c*}\ܲw !'tVtfkfnћJX؞ -UС+jEs|mrZHjEm9]rfAX@ƉiV$7RG[bMD\U'|6hWK0y~ɛNѳ?W>}[^}0Tԟjtyqqdž:*.(Mマ!Z.p0q] Dk tVP=/5HsoBu]{g-%%?BqҸS *yZwr3neA%dT!펚{m7=:y6[qKLxpSMqm*޷{&tt'nWޒ彙|6әbG1sKɑW'G(mu;=_nNVuPWEfַ - >p`b@`Aֶ2^TXКղ,[ZoȆá*jVT䫥ߒ 3:{It1\Gs(`IĞdgG]ށ;M{( sxҒ΁|0*~~IAbwg?b5gܝ-|g˓0<ӽozS×8"y~ڳԛS]Oĕ嵟Q7}/hGb"n[S(R["4gOy(v3[M⩥:x])R͚`o-RӯneMm؜[Iz$>6Ԥ~}qXs%Κ2?܆,{k3kSS KǾ|}Yx_c6hČ8s?БzBPԸa2hJ="~GUxjEh+æ{Ӟgy8&l bkO@xlB%X3ٝe ^Z聄~s63wȸ-U5*~A:U*~@)\,x:֒t{qIPz_[cFE]Cu̱x˘-2bW˄Tj}2B*TׄH6޻ZdG&];5KPTo @ޞY.|D~b!na2Wk4 Eaܲ3eEˀ3 N&Z+cV, ->;habdKJƗ0G6qTosK /Ql;כ,*K8GJŁZ?Jz` Ǜ:J@Mh[Γᆔ=w9Cscs[R,. _ri ᷧ:<@_gu=ɶk_A"EyA_/ymnJ?+_rdNFH`6~CY:04Mc3cKNࡅRyV/oS4?#=R>toK3`ف{ -WqlL ʼ!GHU:J£+Smϗ`IgdV&Eu~O0R߹ԛmw7T+*J,0rVMS#|J). = ׇ⟸&|z :ޔsΊ*^WUOAO/.2J*?2NGiEI;gs gcm+\W3{=bFd0*e[  _WA>++vvL[wMH{Et?@9M_y750XՖnQ[zhwPEgkٵeAd9'C#µAX*|q*~D(y͍c , -y0/^B|Rq/}n %}kޫ+,K}:]]+uV4JLy 9$$&S)  -'"{(q_2z0sD"f֜ml9T|xEϕuɺ427I˥5\jH%T\ `Ю7nN3jwMY訽e}0as~nݲ.;H@'l䮭)X(#tVM 6fjԺP$pzM6 ƻ9DnLhi=Ym#}R\{qS /8ޔ]rhaeKoINǖⓡ9 v}Vl~R?T,_x-k~ VPmy)[fmnn*~(95x1ya~}pcڻ$DZ[#ߙE&o9Z2L -͎ c3uwu- .C>FҊ!: -AޙUUѕY*$;󘒽l^P2rS^55ӟL@o;vR^HGF,b$Lw*r՟y夶_⇙Ua_fy]%h0;{o#&5ba"|$,w7E//#Wdہytg} [9bEBKۙ!r COW|=',lpFXY^aӱRIzS>ޥ#c5P]=g=S=)\D/dun}.q{ߺ|ssx=֩г;]# koW^!{M0%/j.j[bu w}5}9yvyqT帎ѕ]31A-u=xȷ$&s3fud3/:s=sIv۝f}XAxUݽ?sv] (xp(Ju'dYT!ĹF9 -QSK9/ʸ=z:sZ+W|fUItP. tJ'Qm- -fW}o -s{qYyLm"8He9zk\pT>^us;hkz˿OYܣJywWsu] - ]50KDwx+SiA[u<=PsɽZ!5UP7a>'\0&l9Lj ̑[Rb[\Mncw[V"𗅾oeg@-˘=asU0@X'\bDܝŕ?,'T$ָ߰Ṭmp%Fu $p3GQ-JmX,J|r6 -~ $Lo9_w,K,O˿'Ex\ݞg輘  OTS` ?Tdz\QY"{I9\t[s Vۭ?0%5V<ȿH -,ʣ*fW%xen)=l7ї7N-H*}i@ --}oz1m}}'r=ws_3 @x} -hSjX{Qe`Ss) b}{+a{ZFGTrM]}]wXw VO6,rNwTF, -1lczcw\r-DkܙFJ@ȇjNZl= x[ƆkCr+&rD4 cRRa@L/Y j=s`gb:> gve쎵)z_3tgW1 (Azj3z*^[8 T:'/DH#fm"䝦O?}F=YɃ(y>cށ]- p7&85evڮ5f6W;+#[3ԪQYJظ̰Ȩu`Lz;~P}RTO -ϋiIP;!neӼ=Mkeyamq CY_m -o;3|sWk\wmT$-0 \pq+[~g7;KbTP+T"G΃nNnC]\qwckV>­I%踾0WN|9I.tOܳV5E*pO /Ftڰ3x =#aIx_}1dVfc+ds&%5:( ԙq"騜1 /+JR^])j{R|QH]x߳sq#9 VMv-m"F` ]=y~]ŰOJD"wӑ6 -iQ3,#UpΫ#g9,E,zqwܲ1>}a!\Ɩ\{ Ye#|961[@rE Ջئ V%j@;x/nd{Yi-[]7Sf&a!MxBʛFW -jKPMm -^9>$`jk ]&LD%',lEi1y.s-4:-p @3o'uDny)@JXg~~Rvk )uOU?ǯau쎘IČ]9m,:u)riex}mjZ^@-"9m9TAanm8ӻ6fYM+k;brchz3cDD蒍 C@?g4=Md[],nysm뜢d"|80=M,ZLP΀e0'@3{>ڎT vK0PKAVe_ajlD 0e- P18Li]f- -Jc4=8MS3 Hr>`St׹kYl/]`+f`fWwOO~u7j̾M>xoύpA \Q=?:v)[|k/X4ݳ3~9agXRSv&:a?ao4) 7jP-KI= ~6K${V$nр)Yy8=I*.!AY?#k(AѦ2\WF*huiv ӫ!3Oڙ#;py%Zدz-;%6$p}udp\2K:15Ot} -į|~1: x=?;V2ݕnYVy^fe {ųH䔅v9gzy} v! V=&e5^sbA{}hǙ¯V65\lZ; AW1AL۩1=y!- },O9FôC\vM681vHZa  (rԭyd+YÃ:^vݝ# rWԊ%FOTN co΋n=:ˍ8<u1crR^QL#YSi>xzeew}KJ,$5&{KذZDvK^oax5 3K](˦o7^wC/7q3 Hs'$lP} ]ZZGpgt>m|@Do^&~>8y~;db,|>awzrWIkX%S:,&TEȂYzk\/'m &ؤMpvX ɋ1e1Ƴ5GYNdw^ĵ>stream -U˭Ktϊgƙʻ-i0tί?S?>븺=M)]ರuRlYH h^%!eGLsREIH5ha QCJjCLA''SG}g3"J.*a`^ -lp ڑNi畐 -U @;F0lz< [ԐؘIh HYy/N驕{*.OY~ C%wЫ _wzC[F|SH26P;cgU⒏u^ga¯X ,)a p\aLZۚpan  G_fFjk@F -,2Z<2fkB˂DKؘ2;}r*#VSTG+9+ڧm_i#2z>ОڈURng55QU2fWtj$F|j hQ#Ưh_ly -, ]BaUrrCD,H+'&4l>+E~*TRZVFڎn{zH7iz y@,lk{'㴚 W:JgTK' T4OIok /t ->۴$ ʸS4c-E"7jTI5p_?z`ޯD"aBd_+q&}9qnw_Л;+B~Gu:/xHAf;ef@\%|Q%&4ItΫ;袔x3{ܳYbyhPÍǓF.>i Fw5bk [1vѿ!sTBD{YqS//m0CQA|.gBSzJ uCg_zQ51<֭Iz51r3ͷ>iN%)R?Tap kG W]UΓ2fGH.ƅw5\goyR7)vF;.g꙰ c{D\cz|u'm}@7 DLBk%U8DZ7=jb$,|ZXCe6<3-+ѥI 2cjSFk+)1=3)TYE3Әlk~)Y~suHd.#kyĄe臔E2T<\l%<Ϯ12dw -b2d\֘aWoO,eunNJ?]}kBFLi ĵ^DK$iF]27?o= 35&aڳq1{tnR-ƕzT鵗m]KBIL-3һ.a &1 8_X.nurR\?v灾uv:h8GY 6ЊͮYh)-"fy>Z[iyƻ, -WMEv[X#?|5}~{K -3y5٨O,;3(M;=vWrlq^TEo ɰ%lfLIn^ ,@r=FxY9n4a-19^؅=o92f4 V؛`"*Loreus q%HkH U|$[Dpύ _PlOgE߈(XQ{+]t6YYkcz:$la"{v$k̵wK1=9mv>jlLݚ_[lٽ=E^/j2M/cRJRMvG o{BΛ6]Uޤх'֦ea}kuIli*f`YcoN\pc+BZ6%afK^9h{\[6si+PZ\}DzOE-,=PϠ@y{_ti .2 Bmk83 mam/wgX2:bJ."_"Oq|2B|Us`9\qW9"eg4辍ڝ -3q!i^d ]ɔгl]}G4.(CDհb5սWJRSPC܋SQ -\qDOi@vDN 3:^Tg; -`{rryU_eҟ{ߐ<  ccZOkݾDs%=+I )X<( -x_KY(ँKI=5ff##:nђ`NZFOPIhȬ+W(};+'WmL}KKs |v;8o.**F[Hi(;/d-K%ZmH%@{dWh S[q?}/PXh0_3V!3mSJ\o_}^ I{!?ٚ:O+~ph%gv+8+w~_W.il]!S>N+y\uwN4{hi襍Sc]SQr'n}&l\őZ|Wy@\3A]ҽ2RU@I;qAlؑc:!fo F_V/"8t"sfoa/ 5 Bj7h؄^xkDNH%<boc+r.e~^J~$@%,X@}mߍO _y܃MAWt~7s_\#Z\{Cg%.c^26>[ԷZS ׿qM7ni9= gduW4 -d~Dmۜ9'8-ތMx%W >u { %m/19+($Z]dxj_BOqytV7xuT/Y.E1F̮3yJg 3pi ѧh:5] +aʧq%9ǚe}9>5%n3GðtMi:N~EǍ\;i Z^x\.~}XȿjwraMÝ'V?۟VM?'c%WgV%"3Pyy䵘VLO :5ŃEO3AZH-ώ4T[:(KwOTaYoM"OзyUޙa*vp^iOskq) -j9+8&o=$t,}Eԛ.FM1xQ- 50^ᳳCRB Nwq>9IG%OFkhe7S/5z7i>ȑfG6;rّ#͎9lva#G9rȑfG6;rّ#͎9lva#G9rȑfG6;rّ#͎9lva#G9rȑfG6;rّ#͎9lva#G9rȑfG6;rّ#͎9lva#G9rȑfG6;rّ#͎9lva#G9rȑfG6;r7?^JE ؈:*>8^Xyބ`1>8;_dPl` -偯.x& |ly ltIf9y |pXα !lW\ɹ|5…k/˽v깋^{cFr]ͽxbNΕFFu{\ps)79Fp?~c'Nh^b>`or:~e2&?X#& lS vV;wv@WY/_>{Xϱr\ASϽc)\/{=lv:*gXDRDmEFWAhi+-Td10{f=btq%RVSsI׊%QҺj=- -K"KҸP#n$|BX{QH_[x稕\vtZa)6JrNZVO*ؙ_uNv˰I35"Ԕ愑NQkB -rk;[R&bĴj*dE lgX5 ` RZ]B#D4BrHNk+q-6rfI@z.+e@~p>Șڑ1qO9VjJf_N =ecsbJOj3V!7c`N\9bUL'fǍTKC`RV9pF~GNjp:0a-WުQ%'llƥ,,TD*e :%ֿYШ jHa-1iecjk%f,慅HK* U9,c’&g"ywoWIZ>ѫb#|~; b^qZrivm?4XI^1+af"2v+ +hMqy -e!ow ae|}܄oqAԳ?lj}) {leLk#%`,⸍N(Q*!f1*Zs'M\ V`јPы1=RJrlOɁMkca`Qo$҄]PJjS@ņFAXCxq[o!'=+ED^BNIxȈ=W;3i/+i.W lxR/$ƍBr &9% "Z IJ}&ĪIЌU_&Lb6!zH -,ZZ,7kCr/mQbF>-< ,0|[rzSLFoN]i##cu 樜Ց҉ )WfL  \L'LJ,椬C1Pa7v?̣,|J -y6bV:ty$R!yZr{3+>vҺ^y` vnO4l>dC>VI_HûD0S=#3{[=gv!ꔒ+Ϻߑngm6$TpiUO'jʯzz.Nu^*i+ Z wb6y2bjLdž$=F<; _ղ TQy~g\-&Ԭ.lۏE4>(~1οڙ"No'2BgE#ϚZ~Mj~:{_(J 5i i g{@}ڷ0Rb oVg?+˽}Q'cB/&>齅r`VgrBtљ"]ڝlޙ1[2f:$i`tb^/I{Y1Q:*.4k`N9Bsٞf]R:Mپu"NXτ!J3rfKLIKh=I q 9<ۏ]_;ׇIwH%ZPĒ;c8 „ зrcHmx XfN9~1A%"/ƕD۽ \"ox&PWrfkIJqpU9a,`u"f$8=8v§,Lj*h404 +5k,S #td$= -|QR!=j{iE r(82f2yqQLskN~wPX"!dҠYwMZrH/ ^nUgVpk٦چH^kgHi9FXey -hҨc$ܚ虤gB~+q~1QLw٩1+r:3s2JH<'ާ%$4؇ne٥Ů쟖gSIX'L3Jߍa̕w,msҏ]2,4EN'>pM\{K 'VsYJZkpBga rOVҕ؞], ZIYF!, >_)r{Z  E,.!SM}\ Yؕ~=)\[0--fS4ʰY7#`)ի%͔ܨYX_-~#dBOښ&'h;+j|"#vo^"}7'B. -% $,ᆁB7!0 "ƽIM,h s01T3 (^c{ҧȳYn--;jr"$Jsiz >"H91L5 YdyC ^ Wa+UF,ܴ9Vu3c& deF_CFSyL>Sx x".|n@**[cp\%=EtM36鏐gmߖ#=صkԤQz -%RѢS['\S%%|SH iY֗Cj9/Qs溲Xg74e}nxml<څk`WDz?JwyG!5JM hxQ31q0c/w\F-@ BG{皦g&)ø[;~ -'l`PCԴAk%gMm߬T^q@Oq0V0_J1B1.5%ͭ 4f2vgeu[ZF"w8R{&QW<|Jd 9jۂFnNz D2 93bQfZ ,w{yv9Q Z|QqR?fI֚Эe{M̅S.ВC9V0ыB6k ƐE9؈x*N&6:>gGgy]h.℁=jJ*xH^ץfnMtF56C^!+ fkZ*b~Y8dٝE2:l;C"d-,[AȩRR![xԿ8DBfí:(~\3Uy24z:JU=EMr첰E@uxӴ4s{ѕ!#5ezFE[’~zl?rBO1$~@(M@ɩ+ÕW7ɉau=!(wM27G(;C=+4.\A]0;$"}q -JϹhN&zX dd@|*j(boZ@ i%̀ZHj'{V"8#6зԔuFD_ \+|O jKDA[hD0^muPcYg1k^mM3\jlwy0MQY"kZ^.@?SVzi̭!!l5=9ؙ0򯕃 X9UJ䙾RQd$ l+ËzO)­i=T0ZTG5 3!u~x5?l}2 -%IKnpjydaڧ:0U2'iI½jbH+$Gs;) ] -ָ|m ҍ:9JjZL<4:Xvvsa]moJ4YWL`,ue_r5B`V0WQ:!ٯӄެCSuW@fE"_ ^3/@LVqN\G!.ίbnF Ra̬l-iE1+ނP -paSݓ Rڇ΁KIjF@Y1`Qmȅ1ą3C<7< \ C!kmk^L"ڧ NLjdC ^ab3\SS Դxތ:j&#7h[Wu2N <{<e!L g1ɥ>I00Y"bj6,h ]ZBs u-qIpXn6d0c}\_J`K6qGu3A8hXd*aېF+FGjkw|!GM6nD8I&^W ->e57js9``iaRBV$`n|pP. +!cz^%;b)9. ⰹ1d嵻BA|NZ \Z&N -[룳ϣsJs6 y*)?!^5hkżw*ل,dMKI-UeۅRLGQC&!o2\Z.Flȭ#ץ"ZP_R)q[M>>/|O+x5)fW-ZW1ږ}?z캐NLLg;LVư}܇S͏jukhS9Z^$:їyI66RˬԩOO])`h묟JF-k=JhZT<\:Yr֋q{gizYd43]qͯ!r낣1=X:i.] jdV7!/FJ -1vqOG?m(ɣٿ'jAWچö.SGMYښd@3Un4> _5^; |kc]RFLߵW:[~_5@0VJ[WugK%$.㎦{so>EruGSqۯ#g"ZFid `{Q[_QH_toG5u5d=W'KOZV2xK+85*3Уb'9ҧp ~hj/mgA+8%^U7MKC|F&"ꦰ!6's/?YS4?Gu"N>γal]չ q׊N4QMwj1XR26HA==IIe1V<ӉHې7YOvey> 3-z5̭g1WLϊ8GnȘr4 0gC/B5m%bsV - xTb_Q7W}ԁIF$/Tso~sPңï]V*tH*LҔmʊqɎQճ 'nC{;gw&+wא2"3M_#FZ<6N 9c{?%E@3P>Vܣ|wWT(;PY9ǿ -CnRB"vD,2Y#[NHDVKhn - qZXznQWq!mVsqa @ψX<=w4O<. ruu~,d`lJPЉa+ X@~>CELX!q -Q2H\ fnԴA57O!{CGx]Q5 b{9ׅ9,(E43diCv+#Ŋ.ծ)nsblE=삠FC~}F1ٛȍj1c;N!LVD-: jX$5bO<;ǣ1 Y -jdAݓ΀?`hyޣ8:wBE[/bBiZC:[5Uu˫&<()32K37QFz.h`zG+ >SC[d Aq;2`%9's5ُ1dAr ;%ll@M\egsC_x=yo\nvtCwi%̐] -*bAF-<4茞K𙟎OVgB_Հg> $쀩S'M33m15[4 -{(y!տvjeMB0eQ -~3'm"QxNR%^.7FjBV…#(\6w u Csh29n"VW(A򐥮5^QOvg%]1QFBꮃVg 1ݣ"Vh)އ\;0BzFY@Kx Somsm̈^3>%lf,L -zo{qjFNE, i^59c}$Q.o Y!xOd,`}Nj<4A"kyaTqsjA|kd=cwNߛsns};P+Aṇ|` f~ȌXsv|T\|];\.x\c>d-7bȱ8uȞ^@ysr#b ۣc"1໘4tc cnuJX7.Eτ=52JZK=GC°O\EL -)dJV$)q{GC͈X@R@ɛC (k`bNHF_qNۿҝ:Es};}CYD irA(t|p9k9ŭZf0 A C2SU1k>إ;fg{.ioG h.٣fuBk\* fh!dìXgmq!]KG 8GHIC^Ȉ[^=5cdBVD -V4Kffެ7+qCf2J'YޅW&UD-0vȿ&vXe|V-Կ9j#qXqK8!{5w&OlM]q.%64Y_EM2[-Q 6'Y{^T7 [,͝AȢgH@>#hⓁa$t0iނHe|VjAx RKö6UѦ Gf[ = ưC X0_!obˠZ0+ -dJ`iJ,O} #ݡYm!lJX1Ss/v"6'. wB 2)R2،JpNXQSr|], uωϊ{ aXF* #f/ǫBXW8njxkQBn,V^ڞ !ZjM8JJO5'!fw@MA.qco=V)␾9li!a%!9栖N<'|t:6(dF.vt3, aljd]y1z.3bƧ?!SzbZȮt?c7-`K~(ϹN-1JfF7>eRKښڨ3nm)#+k'1׃F1cc_5+cSݓ\CW|^$30h1;x<`ʪ١CJ ->RSջOzuOgv b_4 -ٚ}F^TIj暖)lG%lo+II~{P<6rDt)~);hG=/wfǠnw꺢"y߃|lWSu?bf@Q 2n~P7# 5R3'+ r - ^*mٺ7نw8M]&ZzC:8['hhz}?=r5nN! cΘ)n|dٿ40Ы$rqJvi)֯c0aҳ }==c㋈Ov&ŀD3\buS-mLX Њ*}xWߔ]Uۆe☭U41nnx1I~8N^y2XϼQT -ٙ|T܃i=o)-dfDyQ ^#emN#kvhÇBV|FDc6NKr:`q 7E2pdhudy(3Њjjg'YA]si^\ -% M9gn -Z]* 8D\vNy Q2--}[|?>_KM\kjE-rv]9A|W;.НaݭQEpa ZTɘ^%VmDd'GC, = ED kH$>~2Yc(\5b.\~-fn-n.tU]미 \K9.51rO7T~-% `iu3ȋjd=zCF_G-2qztoFϰ^Pzv;9%@!z*@>'eJz^P,Gl"v@.A3Ep|1-jkXbvE ==8|&x3@FObn̤\\ZwMZfA?4 hmcP[ F -lxd61Km3pmc4#|.[)1wV^C32Ç,@;IJ//8Q I|QS<]n[R<΋5M? YZ=f m ;@L2~ĪB62rT?CE1n nωJm纕<ٚf,1SVI[c嚦ecqi`ܥd]\6qiEnQI&s+loz(u{ -j9{6^? e@F-lLP(h;$4f~>ly Lj 숙2zNE,D k~5:GK{M ȿI$$Z7QD VNGEԼ ^D hwf .x2̭ދ9UQ,Zx &h G6#$ Y&l ys=abV&2xawi(Q# $݅v࠷jG 86& -yY < ǩ ksD5I|X*Gl,l¸42쳶Mu/󐕋Ze~#>ݯgnsK:pLz+:W62+YAr8Ⱦe@gj[VV. Z .lDL)q; k0wVwg|?L(y!3$`fm䠹- ' Y@l}>r?9f׃zj4=9Li%EcA -5FNrot=wx~ 3NOyaps_FBß~wHd5-=^y72iP.ls=all(ȩcGaTEC6Af>!hl+17*JzX.EzeQXY %=d"CfYySұ%l^Td]:6'Ǒ{z>ސsw˸-n-K98GP㸻nQđepQ[Msz iΈ>3Ye;0?NdKĆ:6' TL~0k%O3XqqbOե1,mR #6̯o G+ݸK}'wtwuޚJGɯ1m:{=FN j^k`s_q{|5[<\>DfwW?Ty]vtM婵ne$%%dU^Y䚢xTd>Z'X_%/ W\boԔ􀒑9L~Ё=vUx;NxR3 ćiRw9@SBtH-z% ~ mz?k -!t6r{bv{q6r=ȧbb& 'U,,׈Gi)Z:S!`}*˧U\WpD'-ao:!YqzjX)+ff?oUɪ!#7.M2Ԕ͵о1zg -jAOCmQl oxHQ f4b{\vp|kLR5|GSr -F) W>T]Z&pnoS-Pt%v@Vg]Uk:oo8)ysTR=.$,u/-'^Y!)EQ-҉K?BOu2ҡ V^.92F9IӜrgٸLSVtWvs Z!4!;)#ˈ|҃o*;]^81ܓx[|),~HϭψEL/ԿKݞ$&.nG1RziI6Y)4$9Ly&l⮮}0^|GooC kgoOSĻ}Ļ~r~w sYݐ̳nŷ'ޖ_xmw }|5 j_on#5νDT,\k2@y3쭒K#1^誺ڃd@r Jaeǽ kւO ~t Ч2vm[Ka㒟OO ~rw8)>%9? 􊰁f_]?fs+l:A('gcgscog>0zH(Hia#Џ^^|CN&)eqV6gf/K쇋o/^/.&^{=I9{ztcp rwe5JYz9cSxtTŨ8 ߖU]߁?xEtuUΜY~=&\yD㜂t~ xf_XM}OO%uNmoV\b{[ynLJ'Ʈ^LGji]em bon n8I;#Ԕ+ ;U'b1ϵoͼğ\xSqG~co4b߰@֧֪ %\W,v+!DD?中iQ_ѿ,_؄1&x9;̔īW/~\r`|;\JAr3pkpo}v-+1wjn.8E3ߎ:〈ZZqNu+ʍQqٷc/5%?g>Mg?õ.ҵZې%K}jVzq#o͏~^-9niT a9E+=kWO"? -i8(ANOظ5"(ă6JMIR>b<Ӣ%ސ_*k'n C^+*8MrW< -[J~r/ ɥKUW37څh}'ɼ" oTT^dojIݷ֋dh+>3 ĬqF{ԉtMW wO]uR;]Z~Gii)?d}: -R~dT^ζ/lsU MjjϾvIa!v'sz}5񂵥j{gyT_d}R睪pgߠ34צRҐo$c}G[SO-u0Rs^^OHa^ 3yt [oV`ICS~^n;ƽjpqeSSwuKgC=mயtc/.tr*=fh^ې̫;cG◯ ъ:Qyfykw<-.ՕʳEWOT_oMHcRVHDGEL\e}Q}|ij)9􊘰8mtPW3.ңO:>e~eMEk~:3BKrGRynRsUMƷâG@s}Jid.v`~3|7)|JۗU298p5/'ϿE[ ْuq8oeucφ ;\f% ܘMIQ򗽤2V'|xE%ݘz3",}ҁ>9VrpuT_:wxy>l@[nV\;E9+Qf#*Zd'۞N~`%Pv//ǜ^,?xXӐ_]9{ꭹU--eeGE? |.FK VzM%?NW>U1;˓k}}GYMY}@ 3 L;O/{:=8xG?kokb9GiAU^[T:<Ӫ-"4׎:Eon8E<„𰶡;fup5务rBTzfZQ||O_mZ򹱡볲cW}jRTsW+>FXeۙ'S%}GwE'7 mm}",;2.ɤ?mY9Z[K~v:=Y_|zT^v5.TB{}q}zEpyk;!wX{؊:WE?qU/'sq)˧n V%eK(ޡOٞ24MgK=,}>cm~]][u}ya^y׈c}E8"ɵ?mc %ʓN {gg3?`ӄ mugÜ0|6uV)/)*tr`Օ7e -pOSo>r%.uM?ro0\ -ʹg|'ߓϨ߼^J}8{NSNտtV]qǛs೷YK)wzȷ5RSms S_땢Wk_O,]o'\QdK3T[h9:ٽWߦZjϾ1LI+9f8![X O0R1~^`mafڡ.tOeY_M\\^~lm+=d}Zrh599{\A.#^pq;ғo ڞgǼy $duCo+ֺ7^^zdxR~T~OQw-9IƆo-M9?,wO' ؛5QR}g8T|GÜ۟m ]cbxg\>CF=inJOS}s/sޚe֦~kjw?wLOQr<*C|82̼e%h[ӵ7h-ezup}DVh2`{RrX)M>W'.VW;/?LMԘ^.X\}]qb5 s|B`'/'W+闙wNVgmgr/(_W?]\U\RX7)9 -m7>)I]?_oa~֊{7? xQzD]M'fڋNn54n2W \s`oxDZ6PtKN5+[,tRnν8we m\oYՍ^V8+s { m`c̒5달}ҵQq!nֿl-]ۮiRdK܌?gb_l(%6xX/f?%АRxpT~iխiq<4pӗ{{Hc5eo'7zYqr0|'I֏'cr;Mk̅!^z3} }6 ۚ`Ihۣ͊vB0/ANV҃JS'"_:F96-}[5M:ܘW:צy_i<ßU|;njŞ{Ivo{ҭn6;8'>Ubږifdޝj0kgdl rrſnK#hTl]e^NJ3} 172jN{9:㐡'3/U0|uhRr`R?۩?k_RӔ蠭Ve//gLp 3AcqeZh/K1fZIщ-wU\[tBߔUŕ䯴 %') [lgw늏s>,mp 'D0eV?T^}:b~::~Qzw'J9aڇ#U)ڧe `r~:w_9@{a&_}S򬯇% M6]Q_FjOm3 q"@{8^S$V>J#eWʨ apc9.ŭ|&I~E3^TxoTO%f3}F&;YWor!4t R/g֚ -vʸ>T!Zs}* ^m]fp3FG{gN3sv%!9]tW>ۜP[BJК|OMX^YBXzPWZ %_pJiϯ=W|җ0Qʚ-XxqU΋0Ӝh%K4?H87J<}؏)..v?Y){6X!GGp <^!tb}5Zr,x0J8%ywɘJf/5 1˃%_̠ͭqdЇ(W6TmsD#[E1Yxl,UI-8cwAbdz ,* -| S*LOx(6ѣnOT7G8y-T%'A,S= LJ?*ϿQv YruILvZa!}m2U..:!+fFA~uauq3m]dK)DI[cv$5 L]xmi}wkVV@|Q|q0=JZM qAuO5o-4w|MkTFM 7 dS{+?k9%(xDuȭ3\ -jLS35/R/cYHYYKYlmtawWyH琠la8حd~Lm[# -_L~!ɭf6C -ntmW|s`32z\M,H' Qnz^s Ѓ{wwsOX-3O; -d}^|g fRNisEݰ"bo7! B-un --{fRTKJro虸}5^>Xqro~L\mI=(k!bEH0˝Rm5QQIzGU~.e[~jk*9b6WaKH-YR 37"[ -2̶ٛ~.+-VXeجފ[:!3$ۛ؛V%Hm]}s[m2`ߟoMp }Ŀ]? ?5MqHtKG[d ܇]a(HIe[1I+ x$E0;OΞ< -8ݭG o61ϲi?4@7^MT`w[ BpHG1G3~cƜ$~9Or,=PG˳nÒР>Z](YG,u⾮3;9r!1ۊ\.rv#԰dz^:!(5POGڡIv-an) ['#4n|?؏E]m(JK kZxWW -;C)p 6WKH^8Dϐ LI>xa^6ş~? -9QD?1N?6Sh$6rku2|]H4B|ʛ1fnl-A^Œ'>^ѥson$|ԎIz ,sT%Nm 楬n޻{4իwo^\]8;: `ܧ@ou<&o͵- FHm'nLDyٌh 5 R3Sx$+PfkPޛ`Pg@|,c@wϟrM ůLEjf8*#KC#~>"q ~t)M> 8Q[~iu aHq¾߿> -vyӋAϟ]/Gs/yuJѧ-3(ؖqzF22:7tD7˗AO.^ܾ -gρ^ -_GnK~eF]b߭ VSS-VQˣ4U=q?$@M?K $HdSг@ooܽ ̉5\:uwP h${+}}&*UN޼@$c-Pe5 $<*&XG׸7_:  _ DZOOw!sJf -"^'9|wtӳG=흋AnE>puCo]ZJSA.qKGDꮚ*?pqG G*bzqp||z.nsrȠI.RXOINgu5N djVqyy|y -y #݋@'W..\Psw*'XX -]-1̽\I} P CT^tJ88{@vCg/np@/Y'|QhS -_fۋ%);7\x[\+J9Sл{@A/@nЙ~=xPt1/_,H2׆ ߖSrF+6s+u-,-;(!8%Y '@.>YPGG=;rhef;cyLlK(_#+k!uWUߎ~ J -z5E /~_]4*0'on_zp-7lrZ>NU$psݤhK%IJ.K -?k7w_@^=r5 y-PaףUcݰ?Q1?UmWMmML!KrO㣏=;%07[A tiAo2#sbXƣ鎊/ +e4*uYOФ#1v9S2<9|?.V^San'506x'SbdhQYVs!U]LK1 P EhpRsq1pgI gi))HD4_솼\Ą4 4Vi jBxXMI8]\s!*rexzt<WA.\%9AJ-ъ g;RvM.D[DžNr SA? -o*"u%Kp{A/ r4-,(81$bͩ`U{&Q͎A,j9ҁx{(3oo  -:9Po'(nefW׮q6'+r`FΏ"Q1eN|:G o{t3Ƿ@P)%Iܸ*]Ũ+(#훫ۛΊ4,evHp\p%bVyTFYHJo?3PO_&|<pZ4hiD)ti ےUӷ]664#^qDE7[r G7'Cg$I ;g^JK<pRPzG>bh伬+/3º9ЇZ>ަ-hVj2:'Wo~G%AI_EQ)7M r.£cF`CԴ:X-nehƽwJ-V:kÍ䭉jC%fSsV\` 7lU#-+E67^ۙSHhi;;_+_/lX*kԦ]U0*1cM./t39subP_C5& -B;ij\3qgR<ԀwvT u;jg9)sg8@O F. DQ:U٘lkʔ5kkƅ=MSm. "&!*]Cڕ )-}.=W'8T b׳> 5VFdKZ6Vݧ>Q{LJ -]m)pm95^S)eIR4W|mgRfls )fE -pWU֭RęQV%K/+0<< cyI#ͪlmkADF{qmvo?\n6hm5%ϩW\茬LoN4$[XU}u~;#&}.jPs]w#kckt+J 6quz$)%d1tC䪒3Bx- ­nU3HMuqUFn*əCU &zYBRe ļ)rfL7m*V'x(1RR}և_'ǬKk4ZXJ bW+dl=*kM-}c}[Du ȶ8#Bd%ݣ֭+kZᦂ],GHpQ r!Ѯ[z9sg3_3Xp`>XR~ -IޮwL-K]ϲuu{E޽e fKjԆ5&gF:_ѡ%;l{FT`:_h_O.>4ӑn-*c(8%O%uybNE͙Kr.k׭MuֽVoî$( 7*tguRhq8ZRґZ>}C'b3',&畗:dֆN9%ۧ j6`Y/So*9ז:s>yDa]VWuS{FxG@-@Bsʘ -X֏eQck Ħ @&-ᮑGpdE 5<)|r\8S"ํ,D)ޙgaW?ͻ>߭nq隺v&e.)*!4R3:K7Gqq\{Rs -lWG/h){@YZT{&?gl$`SOؔⷔuSv?叙]SC[#fl֑76IGs >3B+Bg]c[PsPsYVARrEgm+@1<.>V2}ݭŁ}ꦭI a!=JWW]dek 4l"x "c,ȶVTa"5Wy``jbPAZ] 2B/t!nw g[w/zj103Sӵ6A-pL"Heffʭ@&f(bSc-}UŅ;ZLSL?0Q{U!T :"jF -bƶ*C' cY^J:%f&XEfy<Րs\*LtkMYq" ۶UTx뺷t<]IH) -`Q -[*6|A64@ُSf^2ח>Rmø-6zm@8*a k|. 1kg -^bV͊}FrS6{+^%I.Ω4v\XOprV1` TS;:>uCYCR7U$e<֖vh52ʩFdV "f|t݀N '%А'y/ CkXoů=Zѡb94|8*ܩ&Ӯ) 7V\pC3D,ut)3u-1.vłmkW1@|ZxL?n0 5!mG)e}0n-:yu(6;0p^0FR&#f[_s|Ui{K @O]򍱂׶fbpZt9coѩgB7ȻzuT5riIdN>ծ'5- 61M(e{3 @4|ʎO;p -d߀\L5H|yR+oLD놊YPiV)R?L/&tu .o)y> ѓ<S7_[6,3_DŽ5|O1|ALMYD?~v RZ7br[6TؖZK.i,he^Mɀՠï#n -3^e{0JsFlm֍նQLԀ0!?jށSp"SOiP?޺4įEnL$Rvul -1m2rDFla:lrgmũrRF馔6KC bGA临~'^dW:*gf 00.@\4 ,޵0;ubgQ?+–Y1+#og{+7F ذI6I\wu[ }6(!17F%^l^`[fj{&s 6ME2P*,IW匸+*ڭEXG_,Lr NHN2ߟ?5?(瘤Àzl.,'uC`??M]SDr`XN7*$1*hȶ74oFE |JfWFJߑڋ:XtIom3ܤ .o."[KĿfx\ oK@,27q >qaR\Cٓ[&QU(}CJԶE?vJvG˂{T8,b'MZ;$T8]ĥ&y'{zD2X~G&z4 @`& ٻjrKpk i[rl(&l0&Q -IX*Q -m~ȫ-@&:jٞל -'[hl{ujnbMt= R,Z朠d9cwtOHkm۝Ud{cşSLUVyLrhx1[ -tUVSfQk΋rY>3lG?oI_RW  embJJlN5)*AmC=yNB. d83׍P'ݟ5pl< ?چ.!oJV`Ƃ檜؇—!Dc P -,Y]3֡쪢/6E\ߋL򶖒7U?3]YႸ粨g!Avt◖')6g(X8} ` L*ś#%/AbP6{>4-)!%kH1uO(Zm+yf- ^h/x^O[_ӦZ_/@7%UƂls`> WnG+=C]@nhQdF8TVXy{ډMc V(N)&:B= {u('oSРkxU -2ߝv=^4i tٿ= N9gs1 JZ䕼*7 !.׆g띹%9ϗ{S -ܖww)1D tӑƎATª. Cư/aCsm#4XYk -X/\D}YhߟJ#6?Xtoٮ'NTuᴸy_j`IݚnɐaN9xtkcgw=bGK,¦_:i/FxݡccWo^aGDŽXDuӵkcظ1B['jWe}=t}>vIR{86eB#,؂]#*գ8&Z #ݓ @}3mJi. 2+vM\Be֢' .51w/ "<:pEXճ; lkIRL 2|TS -m$c=YWrQ55g`cZRS^{]^*{\ 삾 _ޡƧdTnOll2sڇUܘwGU~EL: -R.`jJb[pYi9/7mJ>}∙'[2B(.qt[|\-wңOW>&vNQqϼzxul2Sߌ5a8FlHk!RSĸ&2旙 -;01о)>WOIFrOK>1T~.F{=:!@z0aC8..&l "z/}+0? ¾ձSSFLҡagJ,Zʷu m%s{/s]2x؎9TZ/6ݜt4 s^arwh"WB,fmT\7D: Va~Ȼ){u3#ݝlܝl.52жqT Ȩ-!=%8oc -fD n FPuyϺ sӞfGن 관'e+sڬjfRY%'~s 5IYoa7UWxKJ;%Ҡڜk]%1̛gǼ -z}9ל{{:놹.8*3`˗q] \@'y1hBGAț93%lh>5rhȥ­':ʃ;*ޘkKm -}z8;K$|pGV4aѧ[ #[j /C_rS̴STLSҵ.,vCRb. ]kjy}.Buノ]@~^C|8$pr޹FV j% 6s]ϼ9אޅHZd|uFgUe=6MW%\̰).m;|FbpSjJ_+wU\K;,.ӝcG^{ tJw 0Bs?|ST8]|_ Ah oq6l 9ֿۧc-Tدy*|0_-~eEGyu,̢fn9磜2Ƨa2\ 'h1'1KI#j~ch{7TdC ͸$^^h+yXYe"#[u϶/6$>{Sf=]ţMكb''ωOj_&iԀFȫc )ܗ.)! -dTczC1'rk9+Yk%/}:&yQӾCёr=3M_,Yj.u CF_/uGiyQ=Fw`ֻPIʼ⏿߼ ->oGA+D7dxyVGMZ-~9X!EꐩYg}ӀAE-3IW=7C/6 -_LOIinR0:Q?)%G]n~q%~̱XGU„O?gD_vw --Ś}K*O[*>y"hin-6OyCZZuTxu5E(- \oJm=3Q|^9Qzi`?0n9.o<ialFk &"))yVM[eo3wts1"܆PgtC E>3P-,oh>v TUu-'A~l!2@F" )!6/˘ǩgֻJC~•QlҖw(xUU -ޙSnm#~MW5%z$$ :UM!N0!bwM 1ei(U+Z^A`.%R4tVvUN7BL~L ;WAא,Ok_mpecrcu㐐s\l);4ȁYX= _l/|dƞ2.4>]GU'\|{Bgy4DRd-~'ެ+ysxBI1ǣMu埗Jz5#E4$ԸD$9ߥ6F1nڮ|sҰG)͈cclLGEMҊ \Gt)2@sM9_5] b';Щ(>0QT8'ώqZfcMůW: -Ɗ>ۣ΂w*3;lk}AkP$ߡ) -߅ͩwi円{[J&96kyu@<m }oezENr`;t\$1{&X`~"M6CmRrɾAıbZ2nnd? _-4߶PC|#y{kcu/{ҏӱIO 7 Tj7>-gtXylK ôgzhTWx՘ew'9?y*l -Y`^=[woWza߷qiN%1cM?bnL -]cmmySK^BT bI0u!O\n/6o߄oʾZ_= -F1 ]aCE,t6g]t5[?$zӕM)xWG~m?n˻;יu˩(yk5 -2zΎ=((e+3 m܊|iЗAtAޜBױϹqkje__6whBYGs^[[lܙn҈65ǴȦ4p{b"r/3hkߋA\ks>Kes܀7 ACWD/ kRRkZ0|]bizg{1T-{[Anac7ǰ1TUuh1 ;&&k򙛤>K}l]YA9xZriЋJ(O>6B5>2ӯ-#f;+^ʹV{EekC)~Ѓ A>#8pu`{4"KˆojL<0T`SbRlꎞwk9(o= u8Md鶢Kbm iE5+1/TԼRr8!Y! ~gfϵ6פ޶|oy6]r:`k%}gbg8ie4ەġFok()ܧi7M6r,]?U^+ v({$S3UF{wYᓳ)b%ȅ 7I17j6v]O`U3Xa}5ī`Wc;,>0I{&>oҗpu LSƣᒐP w-lv?7M1ُʚ=> ϡ§϶e=Pq/)XҞ95T0;Y*\?{@^h1jU.׈!O)^5}zbsk:I:݁xsZpË K[^ȿ dK ]I{fͼ9z4.{df25I2I45D{tiJT&۳y/X \sv|뜽:N&{Ka)MKkZ'JVZS,2 .1I d'mu$6 %QkwXxU`kF"Q14'( Yw)ך)_y+8tSHQp/^~:Jha>9C+1|@51P좱6M;ߴp_@i2`ϑko8 -fɶԊ=SU`|uz[}$&ϲ-)@lS*l &rϷ@j3EKŖ11E3ovcWplcs}M6% [z囫g>$"W ]i꯿fߜ1Ϲl9YjOܐB7-϶um<87Y]BS03 \tG7)ڷ wjM1 .HYԃxvl_COW+/JX * sZaխ=&&. -S3`‹]oSUg=W|vE -[Hlbh`Yڒ֠ -Wrl=WGA3༠ )GͲ ^?aXG+!-~ąt OI @Xk$hHUYDʆP˳,wR y/I3W˽UoJX֘< x/>8DDԑ(){ )Aw*]I:;y7==Fq:w _m$4[9#GdEп@}3iap@C FLlܦqkl )9?{lerW\Nv>O&h ʷuQAS D]GE~iLC'2Z4-58)d.c[ gW'RmLB_Ky?BO8u׆yEuȬ|khu-a5BT -+و1j͖(C 5DE(2 opG/9zm6a&Nq`@CU K<+gU%7S%7=q\)BJdcC\g3Hjt}"!"Vػ!Gk{ ڲ 7c{Jxx~5HnPkӠKݕ]Đp`u4jd{ֶ?p Fc d@I!wmKGCPj Ym9RK(LS}2\mͯp1=/o}h|f/Wbr-6|I` --Kᶎ^o m0 _oO]2wT\vR{Z2~De((:O-8:9詹dzȬmPChb* Hu> {&7YZam%q?cC_}b!V$|Ͻ{US jDvT ́]#L_5'Wkl -?e=-/k^o7\y!JΩ8=eKRlў~h"Ps5ꠂ0uSys$vaÕ;YX<1SC *n~@/}QX})u=[+ _V" b:̤|\as8 -m4FҘzcfuu坩{Ӏny˃>XeC'$7Ũr>"R~lz+1` T&0+;;M !w et*rDJO[Gx?oa`b65\c͉4l&,ǕGPGmv#7]]&$hc,q;fro[rѾe`@K ,WJ(Xd\(=GdSs:y}D|[P )>@wP)i׈l(.۳Fj2*~3bZbOKm ̱kto]N*} avhmn@/)5Ja㭥~ NrGK0Ihwᖧ꒎SraYGҭӟkCdlLCغonKoGel@=2l٘[ ꆧ];K\汝߹o04$WSW!OUK\D'}񃥣9ck._6td?99}sFJHmxe=졖F"ܮ_}1elJe l󱙎heBقl +&Cf!o!^aS&ʡ"ڗwFjvCԂӶa諈wla4I hmB<1bA7Vבc`$޻*DjuRxE\y2;z>%@oC_7\骹9J>S{YxW{xݨֶ͑c _{ -vL֨iD}`8vyv)rYtz5OKݚA'3펊t#0E(<2w_y$ <5ORxȉw^jx92cAZR˖qhY}=bG,w=j}Ep-w}Rv}uhmQ3NfŏS+]M؂K:ؐkX5psnHQ?_8"׺$y* 6nRx ;[>:mhTZ}3YK<ޱc(/KdSq _ײήIoqJ ]G] -},«W_/Q97q~3.C+Ќǀ8b*liHn()uzf\aar?Y)#'EZ_NK!vW޺ğ] 4dWvC(-@XzD/R#ʶ4 \⁃YaP -lqsZ%=OW-|z߄G߰@9մg暟z'훐UsGwt\\Do8,t}lb>L;f{9 -Iu4[UpҫcbEکط[tt|^TCoQ̶C 6~ڃI -*m{? kZVY=&#D TOH/Ӷ1 LU{Xꔹ&5RW!6Д==p [s`DoqȲ#h%2 ܘFmJx^)䍱x{Jh擴7%ok_y+陈q, ,PzG.?δ~'m[/O:V ~q5dpW-7{1MC+x7lZ'7( rUzOh?J;G^ś$̟M[3m1%7 ۜemii-1Dh IWgF6ITGy= S 2#>iu0#KL܁EȈ{qH^@s/w5_7ܲt^_nҪ +*FJѭ@aUw LCD^ca jc^ZzZ^nC-Q3:Tؚ_􃝊"#툒c48T1t= ;$GpuW^TQ3=N4wjX4:wvKM3\Q^,=eկѺg=H⫞[`f>*[$1%A)8&*gf\}Vd Sp;%AJtIH -{e=/j*Jɯ*㣃Ag'M;)}sR`s"8&Khq#:oxƱY3]ϿZ_]g'Ǘن[uoN$_|刺h]Z(gŌٸ=koi:O>7od(&j}-+-!%1"HcWU!Ǿ9$ǕN+>M*Z4#dy0&'v`~ݜl{&iI-Šlk"29]#$T4 +{O띞Y%/Z7vKGu-;邌pjjLAk --ihr>Wr]\u?4Suo}~XZzۀS \rX+-3}-$7o)Kֈ[4-mE!7K*vװN s)r4_7>?@ٜ`'\i>'8i -L"Gd[ ؄_C(Z2I(= v`#BM >X/CMt3⏫_1ٖ{ƐiZv͉f~P݁))NHi@A2~Q1/h/M 2X$O$g?soz/ggֳl,/O/Ѯb,2N]z]w>w]7JZsAµY Iw̿آcꛩ*.ѴuT蚁W|; RT3RwsuW!uڷ1 ORUύG3g?r&$kNRó|:D^-0gGȓ?ZNUboI⡋?;>%t"؎мԸ9+􎵽q -jYD@o 䔊Z("?eև'_rsio3/|6|wnVwo ,ز)6f"u a $e^ZFt|TIkڜD -+ϛJ1ͣv! C -~p*.x=}7FRCfAOOý9z]L=ɞ?Zh~/+b_ݞh}aQ #-*kWGk51>9$0GO]WfW=UT} go}WCi'W:X:jLA_4r6&V+l5;ev\wI@LB^lqfڷ@tJ` X6yV9]F:tcc]ZeN#Є y!U'EͮF,| &#s}C̠CT!Yd2wd:,rLO)u~nC S*匆~SUkLAgYߏ4rs,oz''V3c+yh#:ү4(yuwWF!uȎVH=2gC -y= L<Qu+dޣ]- xtRL,1\tǀ(?1[Z.V/8痣j>8YR$UP3^珅8l1A?t}XHxMixMXS,ap33vM#3f0"^Us #_i 5矝W]v5^s7Oe^+keȼxNpLzőEjkD*۷p_=rɗΚFw.|xk$!E=rϹ_徸9ϝ { bc}^19wg IJzy9ϫ#7ZHg `кm= mƔZ6l"؊kB'y\\eh(,<[KEA̹'& LjpN؜dU8A>hN 6ѩM[C`3b\š?N=+:;6!":,2ٷКe Yqx2#C3K3M/rTGF(8 N[)O0.1$10I FrvMֹv*"nLlj}+ylg3- 입{&frp0(˄m-,8 -B$v~{c8>lM|ocG2Pr#TچyARV߷=Rw7$a鎩{G{V3DPKEtLضIطutMƯ-lp>y{ZXK[4,DOKWZzӕZr7$:W yrr WF]ble]blFh+;Fil }a ﹆Lb3/Qnv{mڇqӤ3ҷ'_cS^~+MeQwrSxr;sI_IN`e7s -p[2<Cw;-lV`.?'7 -71E,nL}pXtLjbSpi_I߭N)M,>vTOv ڷsחn}d8 -WI\N‘DMBh## -p4&wxQB= i%㊒\twc=uG)OSVkR ~x$Xij햜a T+@q]#MϢ=uBC Ģ4[zuSȴ ktɛ/ucwb*ʍi[mHXY2yniy9 M(U:w(G4C+Fg`\l -vb2?rFu꿜lܜCnm3x{u+n{1JhS7)5;zpe:{~wߔ <lS/{&"cg&~iKg?L`==n8m OhB7I=H|hTTXמ0ǦV\RV'+/욋'blg_xv}RRu>R%J/_hx‹t{ <@' p|__ O!gv,YX WBLǧ 9iiS!'k{O]bޔ[>t "R3ZM#OW&NLg_78:+T\ت$@~V3ky%u7\ƠW%vm~5 @grl}Dْ&Q`KIx+}mSւ3Q۳[4[G '79 j=b\_69m -NMT[0쨽QXP?Jg;I'3DIַc?n%(gWYf7'=5Ѡ1+Ύ^}A+Y|ү(/v>>8#4e.\gHQ97gr-m[$wJ4_rk-ݟ%\-wȌ?;?9O65aQ=tKՔ -탛׼ }p-"?8{UBc z}2Оy{ -_xwc>yퟅǴ}2o 6DgVhŽ 8m5=u_ӊ~g|mz;ƇZJ㡉>PtʎY[VLU|ʆsE*4mS娼#6lZ.ptkR|z7mSR~rt_s(>@&oH Q=⣝-6]}/jQXņFlľ;M =HY،" 0gEdl%e2ԷHkZCXod׆3\I f Af]n{מV&&^9ԧe ^&xKDp@4COjͻf:=[SR*7U]s``M)ts\GP/xWA):UscyVC/s 5Ju3 -Fnsob>8 9&wU؜Ǖy8m-OS-/>X PޚI3)1x{zV%Nօ|%eW -lM7l}-\#m/VŸalzPM)-kӈ]Ϝoyw\{W2by],CO+ojwWFfOzVίǛs2\E"[? 03۞p̶1M@s Ju.!`Qw֞[QLA_'=Y"v|IB;{w9:=3 ^\sAT_EoXq=%]Tog J윒[wWOkmA 蜍yBɆJH ;abg3]!,hWҺGaY{fVgw[Oncs{"87j`W& 8dRٸXcK7lDӠk@u" a㸬3{j㖜ZLT(Ŕ2Kt4%[#7Wxt䆀P9O(ؐR6Lu#!焰APy1Pqkm᱆_p6XhKKmTTo/ё tDKQe u7lh{LbJ}t`gbKdā B---`w3~u?LR#sݒ'v OA,񶞉kL9`!j{6͎ɞMwH R#5~'%ZX̰+= ʰӡzVU /0A9(#U2 W` fzULP o;u|*Rq*0aZfx=~09hiLٖQ KBJ|glgo"h_.`q̜_fN΢Db'h]A__%%1Ǩ]"fۇP a9Ru^ў}9CJFsHm ɠu ٫bz?ftD,L,[(:zJˮ[bbblC_A$keAn쨻e뫽}DS 4GRCtLXC4> ^;X}yhkر[殡5u$f9(GcwN`yQ}kjL,3bb0os`!w_` -\cR[M=p1& Q &^$ߧԯymaSx[nu`p5!- ZacS"%xeZ4`}V'W.*kM+ts"rca|.+``*} -1s+Cv0?Iw -ZX2Fj¢L^u*A|ҦXpC@MT64p" [{Bfh]KoYDû+ި 1[\5tN>;''ڍY*;/E]}]R2{m -d$Y۞N“M|NH+,wĖuU_O΃T|KʮUv5Ÿ8$>uS -&.9bfx4m?VzY GI~`.fs`즧{iQ- -n{W&) -8N{Ql\C -,.9I">l,\d#::vw --K4Y䡜IpJ=kc^kl"=db -<і-uĖd牗PA8e]ڜlyզ5I(Sӫ'Np%lHaQ#}wu@c{5)ro = V8MU?bum[)@kDC޷ɀTδ]3Cj*(g *\w5~#7kSWg[^FGZ+I -/Mt\Ja.+tHkm3c37u ˮ xeDR0gYX\e7 옭klC 9FU{ģyy'> PXOM-ϊyyعw)ƌtDgF]quNDWE۫‰4Rk*r}X`ʻH\Pi@Ul<ޖI*) [&:`baz޾eX[_L6? V L쮃qS2P~-k6 #ظ{+cQkѾmMe6a]M()Z_ϟUݾw֫`)Av_,U4swM|f7[*TqӭgV!gjr[N ۷-2OvJ-far@%?u6f]۔i; 5VsRXgtTƻ^ 1C̳l 66-f7q=֋:0 5MwC5}]!]WW@Ɇڇa 7 -lE"zg j^P}{plcp̡(SJdC~,I{,OѬFOցv`M>:d0k*~s[- x;ΩMl9OZvdx̆[vW#}Qi04\NtdL/֨+#,fڶF^۲11G߰GtWޞ*I?JK׍P'qa&jCDŽ9ek!ޱ -:W;v6`2O Rr@gOlI1mi>2S]cͮqzF(1ukܶK0Hf)P;LQC]_;把A)vl:#6|mhԪFyCRHM)/qD7 Mu#sViL+- hƆ[,c3Ҵ~8+\i5Mh!^#VoVOcFZ}`whgBbv(H{ rFk V|vk]I`X%r$9A:Ы -ڷ(`Eʾ@L3Rdypn6UVD]h7pॱdǟ8^8찍C_3v;CnzchWtRMӴ`71y|'jT;\:remXXȆ9.+c(Q^v&S&Al2\EDi>#M=1~w2SVՈpajm\"%+>+G~,J+&hI5PJ;e u&|UU9_?jd}?NNG˽cj!|xX2,Y:(}Ad WZ,u7tT׷sO$Lrt}P_,5FLHc]e*a9B:,m%}P\o]6s_I{6MpjҷH{xWwn-J"@cP)!e{S1:fX}A$w00$bDhmv$RO=}=PLa{z[c:Ǚju3řab~3{Z6{Ft E^?%}vʋq?Qd觭jD"]Wj\"LVwOʫwL דP4j ߭*̫̂81&`&L:fDcD@7dgq&s>y1 -&܆>Y;7;>I*=of'fƤ/}R(zQ2V䘽_%Yw -s WwסIͭg9h/YS!pNVHW?DY;6lOwMڇv(eə.- -It$| 6wJhn;0|緪 -O\X+'.Myq@Zg;`9 obzt Y p~}l^I%C?VVO~_T2q5/xمu}{gϞ6(9D}wޫ׬nG'qlN;D@D1lYǡ}]g\'3~YUG]0&ࡶ:mzed0m&+3/匑 -/?";g}zLqf^mrl\n۞:F Ybw;Mhtt:6MuZ2zXF(()²9)=L[DPb?1^)U L2z Yx+:r}H}ϐ<<~k'8/R#7;yR}&a\Եfl^&꥽5"o_N*.U -b !2sHkbfᅢ"7\ܠN~17hN_1g<]2#ikN0S9 ^RgtiUj>~6t.h~T?"rs`w.N|qNw^9s R8"WE7[-m }6c5o^ZId]Ux78W1[SVD]JbW[DSC ]O~MJbފAKpj,R6ִı!V[oYJլ"Y0dwI8q; HsS|,>S!hv&U)f T̺BVv"|Ùu-$d6/.!_A\^P;vr!:}GwAQVyzhZSvL+kDŽ\vt6,kT݌-t{Sp{) oLw~ jo}g²ie\?]QW+ևf|#G;B'"KqE7wz@vt *WDg댢lT4j&;!^;S'^HPnFv!U?ĥ}|X^'"nbQ{9wFt7uukZ3)5?ǁ| 1|#2sO|8NO]NnaE?xs] -fiZ -tm&T-p~&Oxpztzp U}`j)0}{ƣy.KUVψV|a5T_Y[&\8iYO>#n|`8|D:9XGuwzK+_.>2?'!sC:kxB͠2셢,f'oǧgG!:~7Fdr͏!Q@iuUz|vj?U?J%8o ]otO8PoeߥNu}l+֎2הY',d'kE#Ø־i,IK-y#K&_&\TGSjVݏɠ_BS[~ e˿ az݄U3eq+Rf▊LԒ(`e`okR -]>MMxS8JOS> &Q)UgWBv>%)7ԇGWd3۬ʕc6),yW]'0r[5`Xbo~dllcȽjbcIms܊)R q*?Cy䂦7 )y|e \B,iJFzԒQIXSV-I9rF(=$g Zzr -+Dr<_{|%é?D݉͡_8Y%nL?!,J)K"Zh3)xXs5 p|Z j~]@|Z8[Oԍ_9=w،. Mimi5C,evKZ" WVڡΫmQ%kT" ͮ=$ )^MTuNrGC~}U3yj$) -()\F:QeBf G BuLHBF5=)/)jQP[\ ?6 -&iRZ†(rdRD,$ vu+r%Tv<ŅD~OewFtuu wZiX'|ڶ,*t HX2@hoo - np}\N㲚~,! "r5JE :iF6d֢<$b@+9Vp_}BGGG|`VaQ+ ֣\~d:n|-5\tH&: "v뿥`[uUyJZW8;zԐΉw2U!NSЌa֙TE##}*YvNF9_Y|ZU?#f\\(ݹŽָW E !a-oL@FJ}ee9k:deSUMе}6'8]c-\_i.IP=쨞jz~Q&IUrA)2 XFOQhTW"j:FچFA=3騨FRv5Q CnIVs4Րj`\(lyUzƔ0ۚ|y9Aϙf̍fdm&q#L+h+ԉ:PjA{!hy(yE,+vI#H}_xq-n5-uS-2􊐑.b*ű<&w'keE*f;QVՎtNvS60wuڪj4g7ev{$b/KsMdHY梲>?5Hj@#-gA=楸nb1cR܍1Kbzej_38q-;5fNJB%q>|'_I D"[+g(]}ٽ "?:])SY7e!5M؁yi04c.g[G3'SꎂnR0@Hm@60M&< '߂dˆZAR bJC-PÄKW_41- CP)YUQ3 ?5V^i랟)֍p{af ?(GXʬs\cR{Z?.hȳ9FɡV"/ZM%U{}y԰ Urcb(˚]#:Uɰqvݞ$go -lwz6lM+#,cM5h^L8xU T=D+1[K*쪪?Wp0|`j('vjPnQCUyꡲ=IiV&l$bEٰ`! JI=8L.6VZD'Sy"g?T9zC\Oޒr V_̭γE6%i,o[yl9j3m2d!xZȽ j򪄜"fg]F&9žhIJ$%,ճkr:qh~$|oddYE&2FKї3|Y.rG؟.Bj4hE^R$}5zu 1ZP 1hIX.M8ROѱa&PM5?6|Ԇ^В#ș^L/>hN@7DPu;3jiiҢ/iv!XSv1s5J4eۑ"/gEG&.~^Gxv.fyroFHkCaGZ2:ȀL`Cg\.@7^a0PZfSey9;kVͺ\ؕW>,4ꪳ^tCݏT3SaC΀(kIq{|U\s|қ9:-V4ig |Z|uke5lkkp<{^;p8#/q(]lS sVR_[Dϗ4ȷy&e~TEeor1 #+m35 (!EK;ց C[ۚ -1Yޚ#Mҋ6$jȠ jyr@BM?JHg2mC95uc.$. )xOUCe9iGBMYigZJ4cgꯩO|P{MYg*˿NA)OJBCCqtK9!:֑RuAGb|ȆwnfO C.S`Hnۦ@I^G[6$إ8+*]lIڞtux̆H9:aW) 6Ii6Zz$Mc-H{C3F.6=v%%PN90 T9"}=1_rJOD$^gA+J*r꺫z(&pKB?VH 1v)6.nP[b\؞kW3Ajb_&f$!q3`wYU}]dxpI򱖎:6.Jr.f`v%e\\X?PRz"rϿ`{cE!6|W'/ֶei-UVpMR4|r0bMR]ۖz6cG{=GGU6P ؓ <~g|:c FJ}~CsbFh3m(υNcy% D.枩2L]ӽB]+ޕanۂagIdJQBONH3= ~f`o|-I72ɠveiұPv% .xfDyz1L[˥"ȇ.l.@My8N mvs6&(2L <8Urav9=&Ƅm 5'8+koϷ>3r7E%k8g.CeIOzY N^hSB!bʙ$\p8T?xos,̑ ߌIR4gm`v9LO<Z3^Z܁~>bUf>YBXȯṢdaZE~6)>bciI[vJ ,CK-9/vZZ`e){w(LOιQ̦|W86j֮IpaM ڗC/S-_!$k5\q%d{mFzB `'8S!áGE!!xb !< b9ϡga6GP ٮ#SY|EEr \i>^hW~]n~W3.k[_.dccp)2 endstream endobj 397 0 obj <>stream -3c TCh!8l_jxln6ٗ2#]v -a;#.] Z.\macMiMK\01|rB˂NRsv@>vv}>^i`CԻ*) -4)DW#->\lH9 ZEϷ#C-QqԎ#D.u̷zDN_7CȘ4gsMoA]>j6e}:]nJ4}KW}\dg| -SC  $G*g%>O}F_]AJ8ug$|[DHIi91C ]tX{#鑎td7{PƼc4mb>r+`ƪ[㌰;VhlO)@O -6>0K{(73D!('UN83sdg_#*T, kN}7ݕ,pn $s+IdXĹf\ _YDKF\V"ԡ(8)YaW^RV/OL.qLӠ"@NE\d [K`o keGsmi3 O3^.Q3 9J9T G鲊[4Jl"p@\c܃Lt3#|Tpd YK#1s} c#C~f,ԌIRJw#-)PMN=Pcep_H!85!r|{LW z<s 6H!PCenU ޗ[\fN7]s(pW>!gf:Q= k_H =&+ʲ*>اA׶s /wC?XFڃLįݨC93k_FI |t!n8/⽇XQo(lС 8ۜ@[~kqPfo2Wq(Irrc >_* nM1Sm`%}m❽QL0I"7cUܣtQJOB{ϥ&ii&jKK->t= -4בڃ݉l%:jeVc8Z˿U5i^f:&GҢn\ĸ(„=82 Dy %;mяe_n~2=()2^}R_;0 -2;(g%/oS+K̍oύԢs3F~[@fc]ie=UIL62@/'*KrVs߭߭| CN˰BtU;Ӽo@zNlSkCuy3C'gtbkP m }㰨 2kl >ӏcUw>Wx,dҔV7mMYm|g/q9̣I|`Kc}ȯw `|[o9BJ wTRWd<37#  )*9`}0ӘhOqjhqBG=sM:N/3) D~5 CZ2_"oA~m{~`'YE|g2_ fZe Z^곞b~낇`zKYFX,}{L(!p+%!Cyo-:D bO/m(\+,A?ibn~>J{K6 LUC @;2 -Wz_LUvq3ss )ؗ6kt=NK6[U?Q)o #mChh`jO|;&clbc yZgR3ڐ^zNSML-ҌrWj4`r{sz~ZTCӕn}&p3>BLPq>-znлb|MNNRUfz~ ANia!f֡\uiC*J\W'ꉚwWK%;8 ݥB=MW@F4>+wCMM5/g$?U& kЕB|g.u>-@-t;"//jYᗟqؖ!F~fRޕaB#:P< ߯6)T=퓄eJ1+=ynƺwx!&kk{㼢5T{aJCĐ;Q-BZ>,N3C9W/wgS5g=\h;Abs棥;}}).ܞJJ y{81fޛn{-a=* }Y'̴g#8cs{3"߿9[ur ,=P~-/Ӟl *aNNJ,Mu!sδfn'륱"~'3p;mؠ臻ȕfmkxzhδ$fpsQ#^0u4wB/^%2^i`^ -K__ -Pr)+ 9#)yGJO5Cݧj^5]Usޝzȡ$Og͉ - 9/g5U:Ӑp6[R|tuᗟ!w7 l磧>h*,t>gWrwFҝ'&Hj7KxkAͨgF"D؉D[+mmc j!ƪ#<ºypj ^y&o'BIy6Uf>,,4̚!<ޒvULij+z)P!ӥ?* t2XG*u\7Q俧 ߯e;詰˩Rpdu433pf:~s]6XaT|ĺ)) ]삹4DؠC[p yaWmzc맫I|z5>/M9)EYxe$fKJOєgF6F=L -m{gfWp&x+X~b.=8y7Zz[HD7K 7#&Q^0^>|u`lE/tj]16Pʁh^rOۅ.7 >ؗ^k w:~L!f`El=uk#\mx܅K25""ny>ֶ;&qu߬}~R4)u7#%R_阀ꁸ0*CG?~:q@lϣ\O}E]1fҟ;4,R/s6Os桜fWᓁ: )Sk{qVb-Bl1gb w-iCMi,Bt 2xp21r"呄}g+qR f 6NH.6'?o|ґ:91z_DuH9馲҃q\Mq%bk˾eMomb)+02tT})w}Y81sYI9zn3XacabDF;t̹{RD6#Кa7Bʹ)Ez\)dG@c#>yk8`[HޗSO l9֡,Mܓ'{qZ8+{CD8#W]EkC`cC}Se6t^l}t!.4\CI$#0AURByuYO >l ?C !m-lsΫ#9PIM}c: "NK=)ef-twD^ed{M7f<~t 'Ŝ;bώz.wS.ڊ'Rz'&/ Qbe9F g.=5Ps&Ct9&@5϶Lj *B( -fJ+=34@ =Ɖ2O_j*^ RZ¡*C,wcO:k1WMж80E xq@`O@BuޔUAO[Z qLAZmEE08hsssacڬ7'emȷ]/uؤ7gS5iKs2S=;ݗ:Rғf)|[AQɗ6R&}>X:Iak~6Ia8@H1uƒ-|/#%0gtRy&f[BJhtxi$dySLx!ES`/gdh[VY_6*kjL&J sx_`wVkzOS:D[v2qzPljc:Բ!? -?Sb#eW}.IFu҃|1\W3NHWS @I|ٗgd$5PȾ3z%$~RkڄzҔ%?rDn4xl84 -tԉx((!@Ƈ7їFDCJ6M\] &dS~;<bw$B1ju: q;rP;6~9&{UV!3ͰO0# @*z'9Ձ3`-q9Y氮.}MgD쒥~BQH{t{4q;n&bwU }rc*Pn !a9_^+Nz:ݑxh=pY֧u+/+k}0_5(%CO>1ҡ=1}Hʸ\`Mi&|<5tn)J .''f -hd~M\SƯ :DKx/icCoO'yo ?jb6'>zS*vP ;#ػ5D@_!ycJZa* t(g:&'wB4>ߕPRXCmÕ"7>/B}gꠟ6{QG -Zwfc&0cКgZbʅv(`ueTEu.Lf\[Lжp\]]_4\y V䳥*'e.9~~ܡ"lIb\c kDlKvq᧦r򒀘: -[ƄO",}┿VE=R4LWSr:J y.yO#evo(մMq':Z8X{jmp1q -l4yjfϧC-3{`gaR;06XY$z&$mQЏ@|:3Ћ -sj{yV~+,6f C=>?2Or'g/t[aZj{/[FaMRem)*ro=lk0ULOWo0vd %ql$~Sr\]_AZ聹.>_~ܛ; ?SQhgHlJv]UKTC=CuݍaU_@z^'fSDK6u5Y -.H-@K3-TW8 aBυ{j&~"ĄǦsS Fbڙ -p*:a{D-*X6 **t{ړztx6'a.Ʀ⠃jCQ_M,6%JMqW_&J^fs=6a_^8әВt3߫@]v$g3#( |1aJ2(k4lԉ:@3\Ǝ8R>ihǮ*M9TSya"OԼ^|,^tyݚo~1\q&ϰUU@ ]kmr8ЗnR|MEV(HFH+ [#K@#—z=C=!blQ譭;Z~6d~[-Vsd"Y7p9 R⁉Zxel7I6)9y}rщwE (x,**|ܱhIv -t=ГR0F!ӱ -sner̺$&0A~Z{}M#]e@]- 9e jn=5xH1ܗJFmKg -ξ -<#E|?mX iuTL}mܞ|1umWOc-ЪgmI*ܾ]dC;+NVǾ m?FWg=WF>"}z@N YH˄kˊ\P&鞩*2r[ 5&\__jk3$1iO?CB>Q> *nԄ@AJ P~4 C3u4HG~0RǰQĴ9ߦ9=1I'6z t9WY5FKj_+,߫-Ws\d殈ܲ)']pJ_UTi ;_rm:r%pgT5x:OC_- -eM1C~g>9j5P]4ؐN _~Z`<²-*+K ܓ>9kbb&pKLL%%IiSbnH|_C@^zjךTOJͷƿ\zꒀ,D>Kxr4FIwHJBE]1*\URx~t:(4pg4m_ -/lւwUOvk-wwz &foK|>9.ܮ+wX3=!TGHd<6^uZ|ܔb"/xСo<;3̶ؓSUܽ Z:uqk01,a1ЭqZbm2S" F-f>dfIBM[N|irTĞ$@_ -\_-TZMv٧RciW^g/Qr=]G9j mco+<,u6 \-pYWsu-&PK?ky˟%&PM;0hWKu4 -8:蕫yUR҉qDKသ I -B╩'LJܴMa{x<{ hSg0v9ƖB;#0qu!FvĬ AI:P}4*+IڔT@,ƒŽ=B -:@gܶ{`{]!3-:ԔQsmOå_Pv=d8<](Z.|-)J +PIe:DM⏴)ۣEplfm i/#QXC˸Q,@{$@t<Ӓ&.=D,<31WUu  -hMo -;!nؗC% t x |4 jU,# /PWZ5k1vmiFQ'FR޷%@+02Q_YX` G[ -h*|#,3 Ԣ-_K4҅hRmlnYF~9fbO%GҜ92a$.x_WUsLⱁELѠOѱ]빘k6%&Cx/f$I˝cuU%ES p=5P VG0.0).jM83K-rW4TQ$,ԃЊ'f66 (y eþօ;H#+K~ hnujteeᾧFj|E48XY S-&*ؔ]1Ƀ,c묹of*YS->L̻42-nRMHITO x8Vɀ/M;52\kBG -\CGWslҩ#X%I;à+9ov]iL/Km5KVv:f;l98ygzrul -fu:u1ۯϓjsq1Z-e=빊:+<7!b^C"}wDY/'rzIy>{Ole(X]MAvFQ -v$lWRO rBn>')9-)+mx n,r?Sb:^'БZ6\8Ӊбa&#Rfb]Jy4|U][A)Jb -.uol;SAݠ -vs3!a* юIB$1@/aՄx>ʦ뫷F~Ån$G,q9ڂAv-1t`;TG24jV麧VEnsH{ޗhx9&}mޕ7$?V*j4+ &<>TCM+(F]SWUZVѹSGQa*R^ScS"OscW&NwozjT]*Rʤd2 [{AХ7R*a[xz=̪b݈ٙa#x zs}xhٰ /RjzJ[L'l̰lI3kz֟[t]9M_ [gKcGS֟ھ۞@\)VăaR梠'a -kָ>>68;rnto}4Isyd}ߗsɇ.[1PskK~W||>%]W }wI=Hw RLWo:ۈρo'~_?[yw -tKa-V QAwݼrqy{F -ed~>vx ,u g-0Q5HF5zG5oM u7CP.ugo/~NaK*~9$6c#k9~%{j}r;ݚO447gbRqpZ"ws; {5%U攉#| qp b菨ĸ؜!oM"6x">evl<0DŽ9LX" ho&,;]'Y-P-qZC3b?w3Kt~IXyQ-P+A.v;iDȱ:Hۿ}[EXN#J+b: -vc{tDKiN;nGM 4Bymk讓CJ8Đ)]̖ndاT"THjߞ”xg1vA쨖%ވkzsa폻Dr5x/Ƈ[~ ;5K1=jrG-@mhC-aiDǡg, oRTGm B*o{^.Okf'n`!'ೣ&ګ79)?&Vf1ŁYdQBN C ^$㬓Y{ ; -v+= 疄yB 4j,^_}K_UFs -(E~ - 9nւwgQ FB*IՕ.+o +ՁY96i@W6ǩ%iajkr}{ }:f6i|KQ׋(qv5[ ʙّ֑+o]O0GޑNǜZEIix:4gW~|AYbiDJY=P|fLܜ\.ɬM{=i"؂)MtQ@iN"DPEŬc֞e^R/.WS q -1n  O.i]?;@؞U-R^Ym?LIOg|I I=#ĕz^u8T[t癔їuI3 -Ykn ˡElƻkoNGBJ9p'fp.ŸQ6H(ߜ>(=ǮM>I@/7m+{Om5col"VMLxq?5<4JB= ] >.7& Koheʝyv5e1y\^H+!D`QTSCj9x* 5磼 1E/=@|8I3PШX\\\ ,pZJNG.߭ۺ?mk.8/ ̳Z*~ -q~Koo7띃gq:5zkte^0 )3mG*$1Q?]ߧ̞iNg {xWBGmïغ[:slcg?mOkcu} K3,"^؜b<81IѰ*]`Mg'C|~ά ޞ"@.9[V::Sp(a-nu$$mZThY=%6'2n)vYѭ*6⿼f\CDi9BuF)XEOhq-U?scyp+~%,`S|we6`^ -> ᫗g}LFqŏk;=B~\Ch V i _鿯UXCy&/,|Ʀƿl x6KAČBnY(ݐE´QJV|*&c~! *M;s䆈֛qyzǀ}fDTJ|EMٙ*ry,aSy:1N_ŠV:5!~7-_ -RT0c5%g9MgS pܰٻ7]GClI~NRÂx%fb -NzwHo-yԼ0~!^ͰZ -Y;5d7'45Q1)\)\͡Vi1 >M/59żC&IԼJjqީ6κo*i3wyU *N'9ZQVv-2jf[@CL䦬b^.恚"뺰?c!lw<+"ۘA ZG۹(5iKqr)R;vZ8Ie,"۸V\M˶*@rgpN9bѽ>-Z1>nos:aKz7fF-KF2N+|ob;ö/vfH97?Rxߍs d|%anKR'% 􎴙Wbz%t]$ʋ@g(y 81gX a6Ub5h5(BDNh$1r#+/GUWV˽s׶?bm஻5|#WX; µVuoyy$oҫ#&8l܍e9bQgmWZ>y{Z?]`ʄǖ:jVPwO폺oLF><"e7GEY|EDKlȍ~5)qc|sV9EqIdiD7 QӐ#jzSæݷ3{gH!BVL$|kX[~}$wlS1`@;f8j?p~q39Mǭ_r^?P[pn:/[_B:.22S'mRjW[7}$$6ږ2sps(Y+4zL#-^7&!(Qw!$tl9I4zhِ <ڞM!&nJyȌAD*)=S3;z"m"V{L(`8-q*u{q; -Skf.09RF!Оs=\4uqWfaDUdHꊩlw[z+C%!4̨FJjf37k} ^ef&o3z @^\\[y|P7'IO^- wiVghV^ N—JT;2=AŞ3iqszK`A\吰+0+MŴӽ3hT{?DUtX>(eIZVWP {)%A+s+|I si 356l$T,΀8Nmcvq-%klnʌߚԹ_Q^PF 7jB*f_X V$п ĩLj:ul-{߼D,Xђ*  -6).g, duϷ/ڿ;Tak .~-z$  -ϡ/F+ 9m" zNO蜬MHDVn4쎠J kAzT-D% \`vHjiYI6(ǵ_LsOy)Y 8yNoJе3N)s>&W[`6&N͘x]JxxO 5n&Ug G;āH%)>k}3⌙d733F*^\?Ғsf*nN/#])!osP0X)a?&) -PƁƙ]˚ )oX{v:eS[AyXGA\(= imQ <E򎯜:N2rK)Ft".!vP=٘_ܞ&bn%m@ɯ75AR \)=YȋG:ᱶgmft|E[)V UA.LeҜK,.XѰ{vℍimֆԦuq(9;/%57$V<;/hv -,m xA,E"~~}9W2t;F̷Ǔ WZw\$'Lt$\@l0qß  -iKI,i'l2Q&Notwf?îGH$KqAA/'UZ=[ <$c=X -qt}@I"2 y*fN -h[X"*&EUրպ2z}~g [0zϺ_~ iV422M*_}~L!cZ2Qrؿh\\TQYҊ ^8\l};}/?hV#8Rip k7?eY @Q -vWD)%D7:>;.=,ܯvt?7a-Ǫ)N樨s҅Lɤ y-a$֦BJ`O/a0f3x7w$t:gU*s>+ -q /e2Z:`u(|IP-O۸gҝM95'lLFz'0%GnLjC[~ WzwqomyA}dFKFZf9e)ct8A2S/l./ =11Aw*r}ߘ>ӞP1[}-J?[^Gv rH[XzEbVJ+J9Y%!= -sy{٤;ݛ LkqiW3yRYB73{6!e9 8_TC/ % rR\?`|s@߾Fzsdue@t$|LC@?zg7z^ZukFwSӡkRj~3I=+ދ{C .H844m^vDubFG&zOlLtۚE\IXŭCZ.&nRB!`bjztI\P0# -"lc y%ByB ^P# ~$i%>A?pK>]~CHjYY\5֯'Ua^|9:_`4eI!vuxu%ګI24o٘~닽Sq`cf;/9 5< 'LX$̷sŕ3gFRkd bD>-wcu;]$եmT؇5oY2ijJtG o_NX[zu%I+ÖwYVV*z1s-?TH6wm*RV6ֿW'To+gwQWnh#qK[ ڹ#&6oa>:d4pޭ |HG~w>n~2%P($,obBVEf_M $x|?SWȢ8$6 D'̓C1-%ΔBΑkW5|҃CQ&c~yȁOt<]phJG(ϛYU@j=!'a0;Q/c , Rлߩo ̕T欕c*lOiѐVO/n94Ji1v?ԯᵅؽylmm:3 ?;zg\eu%BBG.mZ DJ]/NQMYY4j"nus,abt\b!Du̎ە1]&Vi6׽iyak{:w+c쪠BF\b{F J|%uIua'z]L%d  Jy&.y:c2i"lJ{Oš$@mLϬamǭ DQ,N)17@8q"='7VƧ@ܘ0 A]m3 -JMHMmL’l$YҍADJxX ؏q35%_="ѮEt4%6`l0L2Ӧ?NE|rX/FYU|6e%mV/>:aE] -lu7I+uWakyLOOR\&%!n ^0r!c%[6v\]yq'دnk,Z^C XY*m\O) cmP9k )or1koMH{Mm9OMߑrz -weZT+yԾԘ6sآ<`,xu\M,[zu7' c\~ -_ߜU..mRJب}EUɊ;v!=@([zxhspRh*$M hYmI9F\d瑵-skNؤ[ gmLdmYa'fB@m,, 9TZ;ewLg;sF24è:m_fPπZd=Wק:!!cSFfGTC[9pNߵ3@{sAעJ^ORv)1~Vk&a -yryXC$MB!faSRj߽D@k|S+/>_{}Hr@.d&[u^[ߊδ=)Ρ/&Rf<|3;ܥ_ZXT}%= .FJh=rO۳C4н1e?;h+YDTX{mRP)v;Ъ[]>5:ih8$(I܍C] 0qϝR9wS7@7CgN]8޳7C»sO_j9ISKH!#j1;kv$_њc&v_*b59BI`R\`4$|t $zL-@Ŵ^ty~q5|{_Y I :D]NO!À8(Ы}آ {->OM*AZ~Cc6s}gy~sDTM[1FBKkMشEL1.la.$̮={5^DŜΓSYFCJ'TddAB I 4 Ŝves)=3ycqniWcoagߴ~E3Z15d4EUΤ^ϚeU> -;χgYy`0һr lČkQ.fw՞2V'~؜:1R&i.n4tg\0HIPSj -byQΣf"~, ()M=':.jS%2>5mSV.!c`b:bU\MHHI=q g2V.&ia.eDu΄HxK*K;ȫ֛m`0jPR6RCFnλXEHX5> Ưm- eY$03:.x7ï@]YmdwEtq@IFBWQ{ ^O ia-vgt;M.q#?8Y)6c "6#Kzט p,1 =rDy<>QG+q#bLKܑdus9]]aǏ1Z3Sھ\}/)l?.' 4AKHM7<>="V-'Wι9ČKY@Ms1 çTNm{,<,LE*qF{н+BejO{6=RX+sX9LJ}lYT$_. e!?9kr:t3k(7w+,E%>3 _[Fd,j,   VKNh! "i#z"eBz5s3j*-q(I7;禗 h"9睷'E O:=pI8HBcu@uMQ54& ޓXv|qY|Ky@nl`av,t\_$+HEBE[SsQVʶ'{~\}u`uc }!' $%h-)0,вΣs>,BRHE(e Q=Ӏ],ȭiH6t33mg&Rm|S~3`36&`@g̀I1&eLdd~i2r/a"aEAMU!=!r%W1bV$$W(% 8[#,eF/ln{yhUÆުd0`£|9)7o -&fgp,1q0I)zk5rguG Sɺ%[*-K GȠs<,O֟u|v n=7f諌j 艙ɷ)N،U2#؆j+, p;y蛨ADԳ`1 ӿƥRP#XOnĀWXz^{_P;U6?:`17xP#1  ҍ]X=Ok^ON滎;~zNf{NUP i>u_HmY~̷~kn5ZQW[oȬ՝ܙ;h7@>穕I=Y+u`{ -qzsq7^uvfI 5 a%7_Ki-37n1HK%̄ݳ5 ?i}T1)q߉u 3AΛҬED(u@Blo`tԀneA\̠(.Dv=Jo$6\C c@-Cn:yԘbh=T5fxt@_tˤ9c~ֺ35GMӽGK -n)r $-ޕvJe1(u>PNV6gx@ 92q3u'bTG5- 8oXGm+%>aED+p36&Ϻ3.8`vWCDⴋ2v(lr-HYiH@[yu|[ =5&u`8a"j0Wg\UX]tZ*0G)ٰ]@y-5(jb -&eԏc'N&.]:QbKzل,\9$WftB+ibYpR!WUݹ5w|_6"ρIUIcSy8m8tYRi$qݝ6 -a$},;ʳFliۂhT}B -jMLI,GiAU煮Heb֨HXi 9 0)#.wMԴALꄄ^KgIYH{Ā ~am -_׋Y/I3R@uޞ00HAlR2.( Z"JZHM1]^3jvj{`na Oy0n>-8o.x2f~@okH99K$t./q?*Z}BxwiI3 ѓk#@M|Q#W{YCH;}Y\x5Pb(? @:;7 x/%auGk߄\6!%|G@@7_B^wyX T'%2%"jpT*ZCeϒHs)5 -;?q]ϝɬ{A΀̑oO[lE‰ ֻob:iN,ĤQ.L) 'jBَP❣V Br& 7bΤHxQN/&w}|ӸAܣ4Xk fIPa'7g ڬQ&@UDZ+%|8t^)^['d$6'PA M_<}x t5-k:Pk|xhs-&zQ2)C 4C-`ޝ0Q9<p$ _ z*b_[F:eqfsw,mI28i_$aM„mƂok^4sл5HgmXqO0!v+0 엥l2a@h,]EU.kY-@%<|ayyX24{ R~p$X=\ےXq]۶=lHN$13-YdIL bV1W sx[֚kޟ>1FUC0#s]l$eN93*rɫkViqgsrb4.jQSUV/A z=4կI**+mS֊8a|iCѮj[Q&'f@] iDC0VA?Lh.뽨gr=vXgubA@+:و!1ӝ RZ{2)==@swLdHI/Zϼڙ_F-r֝uaw0{yξUp1GX)F(O0AfnM;01 v&:.8&.9f{PU 5ͥL;]DBM1#u}rF5ݯ)TW.D?qqy0 P UQrV -cBfʩ` !A)YYhl Y=Zap`B_#0q,eu!?V$ a#V! *tTH+lW+oWɊD])c J˘F>5B0+LB(5m6ȩрJ& 3Q5 ֳؒf5~H_^ԕUǫ\ъGMRQUպU"ZXVu\rR3G+M$"s߀ASW_W6|!xGFKmeoҦrqG 4Kq*%+> ҋ9+” F(@1Wή $")i'c5aF-ee R0a.#1|7IbO bM"iaP^51cwྦ+RJ}W8AU\GFJ9/d*ޙc7&XYGj@6Ն,eՇ+r##of"˫܆V+SR6@* Q {-i%wclS -|SJ-ܛԔ\g.5h}CEhޥ] -/Sܳ.+&8˹v)?ONti e< wKhUg>/7K9tK, ;Yl3stQ|j.9JO:Um3{~)Vr,Hˢ=W,yPQ=jzqH#`1w1;^P>WrPJB٦'o _D0L>bW&az%44a8piw=ZoBnP0h -+WN0&ap",p~ H.Qr1GZ0ꕉ?fcSEOD l׾ 6%l30}W,uC>թ\]V}$:|Ћ\4SW!#ġA: }dب)k4+H[VSS:\1l i9{rj1q}ᐳQK7^4WwꀶK=z.٭a 䔕~INASyW[Y4 A*d -\: ;s}r`K+T^T=L|NH _F,ڞGE~cң{?8h¦x^ E"6` +[\.hKX CBxrMx[*j .Ӛ3!7K*2ΥaI+#Ec7EF#d'B<W0yJ$1 -0Ocޱ5zًg-ͽ,1׆ oӴ? <\6a GZd26Gp.PAyzv@|xR{ޥ͎ a<`m-M<Ԕe"%ӳ;S|r#6uo:LOBڊe*SHVc UU墀}_<4t,x>mK7:hT<61>i0+jGaRrm*fmtг͉;K2n|9K>M}GWL_ѫӔIaJ1˥ꪛAvyA(Zp 8L+kQ9X^FU]4@M:^bt_1Oφײ~p`5s>԰Pcg@} ;j,ʣD:2^׬@gLW8\9DU<1hx>UTb"%hz-7Ww̠\t`j)E}5 , j,zȡL] xЊlGK8Zl?ެ7< ɇJ౭IŁYR 78! bS`69ŕ%W ^ AkhY;) {Rpd<5I7R΁3r}7ܫ>;dcJ )> oi= ~ 8C 3ON (Čif%=s}w4oZ$.II>)#/e#<3l#gc LJ DPMH~7_S[(i B֡=4RIw+88P[K⚰\ #}*TtPM? fqe,Ȥ"wL0<3<{]2&RGE/jH] |WV́HQ9 ɠ_}J&#%<bA%>-> -#fy 6mwj*ݚtPW -2HB5b՗ʦ0zp,谎b-Ofra@Nv(In6B}!TS -JRVH€3D>sEK%!=0h`@ a§ejAPKɻ/v~&̦τgƏjjK升KսVE(a -xV+ۡ* ꖾ2y j%-T, ^r>,>3:k}!JUje`cUVj냬ct-q%'; O1#|-[ÉFrk%}*Z)tRJr?{z@K))a/Y7.&_9!-6~K $B٥y\}ʭ & @-BMC\K+~>? {liO<5u+ '9DgڣgѬil-WlEtm"&{yO£ -&ʸ#Ak^ SSL+D-cda>-H!BŹԀqr`i^kЕ;4pHAh2_'%gۯqlSX>)R\OEαY55"OuL_M =2iYXţVH9c:2 %0fut8ңep/hhg|+L+ GmϺf/|?&&;lLHS~hi;Љ.! i9s%o=Z!Ϫ@zG@&"j=*J_. +>De*k$fh21`M, X岆?6Ǐք5! 7Ix!hE\ր4P2q "!ѫl.`"}оr`pITp~gDJEk,-xs w,U:4/ި9\B'$QQx. WT}OkƐw*1֩'a/#' LHUVplT\9\>GΆ|)A֗8^ 2?EZ k sLP=t{3׮YC=>J.+^e@oC `=7VX.i}kEjNP (*lL֣R;_kȄ4K]*b^7^sEӪU9Ӧd#m*JM:@su ){S%wFgK|,:TrlBzz:R^VTVyC.ޘMu^FP7 *|򆀱6d!d$f٥XC?Z/1n>íçTx0C꧎6AoTڽ 9QBF:ڣ@D'ꁁVzdb\aiˁj֭$Η> h -Q+˕^7K9Y$ CUKA}Y rؑTVsd}8&~8Xx*׀,\4Uq`-暞CCy푮*yPhR@CURLLw*LSr`C&٩!F?ik>lmk#&,ؚM4h=^ i_.f-ͅ q` yd,rOڹG*cAo"h⑾cX'duzlG(A= Gw` -Av>**y˸nȽpY>=,W+`\GX\ 0 -w(`lA X萞K;ZPߛ!̒@IqKYFoq9`< -i˫+TFvV6~$ -YOk, -6eC~2|E$Y z4R7ΌH691(ӲqԫnrG+uaIrVyW_75-u~PN f[4~=Zg -)]e]P|h/.%&ڣ ՗W'ãy5 oC(;è7A=.ëB@ گeWP8 1 hsғ=jlKKʄG"ΡpmM$:x !|}OShNA /AnL@;@}Zfb#r*n8X?Z4HA5ڐ~3f]$? +zݥ-Yj>B tܯFI]>}YX54N AKYdXCKLDޭ)!ֹtNCY׀.;QRԴ)v?`OPE)2)EԂ?Ee&ic Jہ59# 4P -P>pK^Y zS(hK8)Rk (Ii9\cPK쓔Q+8  r< -Ze7rXs"BJX,sv.)%ӫbXQruPRT7Cϊpw#O@<@˝RJαN=ֈ!)oNugRn{"cźJR}9E@O^΄A%%ۯ f9ff/dL|U 2SzBzPM^$/D}rR=ᴡ#W9ȿa(k LyL.`rXEa=13l#ttĻ~X.drKʐo ,h- xMI@DYW x$1:k⺃i!SugxE>D!@K>Za> $kh -5Ь\+ @L,R`/3z5у~잣$.=4P齭«9*|JXM(!<2bWFN (ia5=v-^Ј^)枧eئsd99Z1qR8GvIq1nPn9)enKؕ}qw\*y_NwI9X$ڦJE> O(iy|c9~T3?YX rXG˧`8fɉ>5}HEٟ(~ޗ3MK)4\}?-_Np͖>.=dcKyc@]Ɓ{*zd#C:JWOCz!0mq@tlGFH -)P85< -խ $uxy,́QTKҡмኀ|CF럀 }7! ذ>gb xXjEqG&Z/y`* 87,^=Y|lZ#`\&ȇe+|aH) -YA9V0G/`MUK,KLŸKEwޓ7&q^UUW[V?GF= -ل0g(w VIZ.%e÷&DŽ5,sWbJBʁ^1=X*o -.7BG˕݇`C.ίb\2r8p!C  !9<}g½16\IfJ,^Z,3z'Ǜ# x mSQrw8).$grbo= {䘄?pO#_$?֍cnϹD<6һy.̷\{cC?*; -\@s)p ^4wEz iHЙc,V@փ2zѡNXkȣD) S\_Aze"W)^ԯ`16hAJLA>ž\Z:g !#O=  "b{hG&QȐa ]xIɾybu bgh 9=GMsp -bSꕲ' ]f,XԓOϣ8O'ed$}΁_尿SV,ytdOW3D0{ i' k9,Gz9BWQ.vI%>Fs^۝MR@N!X'a sW'8*SQk${֎cſ@_F"FlcG٩[}c#좩 9jm+eo+eX Q#w1mH)bch+?ZRr>G܅cqcww>۟ xVp !jpxw<((٥6,99MI1^ho0v_M{#%w7𱊺7{Xh<>~ #.?X`{5O=I~h ȧ3Ӯ)l,^> 舐U)Ev>Tqr1 xmw iNꖬS܋#'I;lWT}nᘦ@#ҍkXre ygm A%%1zCZrs;|މz;ObxBYYZ׶a[ƦUymt#׻1˭km+-[[=˭[aR -hD<6uޝ%}SX'C֛weku:zn}m$:;CǞ9ƳPݟR"V:/ZѷMM:m#H 5}w2`y(/>y..eb< -jK -r_οjj9kn,di)}}u噥;ƨ'揹n?ݏT iN)w\üY*VtES8?u W;lo)?>3O g?>c&`^F o5<=#%.|dWNڃȻ1W]C‰ᒇ)JFHǧ9f)}EV:ϨjN Rg̸z'?n_/ίo/Nb&jv>NOOsYkݸG}{ڬ'Jn^>M;)k=gKƆ9}< ۋzh|w>!Dyc̴NU}M )' 'V:/M Oj/:K=7ol_7^3~043~ލ|B85n{٣Fēwr0gZxQW62IȁIr.1ԳAk=7,mW4 )*~^jK;AG|{];{}Fwm#Q@\żv㭠6w1OzJXZro?%= 3[Y< JV!=E&^א uഽaMeΩ j{џR~0㟯~sLbm/wEʓ圝Ep{x85a䦶!yA7 o?""1N?څybV`V?-ِu^pOqFo'%e#4um 򻿏P+«3g3HyN{Q\s=E]ӨW15ӴL@rdGcK C9}C鵎[J6vֽؐ< ]S7/ d]4%XkϾ]fdfon^v]'T(V^yڜE5\}9F|ł0qJ䗃_LE=70Դ#6㔢"儴,N 3OuOjk; ۝7yq|-҆=_mE;@ߞnuî4_n*EYqҌ}4͆Cx }oҁokiݟJߘznC3}@Em_omZ)n:"'S #FJ;□𼱩&̬8t+:o60oȑӋe'&9I,I>HɣqAZI4z0MKYzBS Ѓȯd$/]|k^oѭ֜kltGWnce -m=AFĕ6={V=/oɪ2ςl⠕}s5EU)]cb83Ųo{M+q󯫪._@VrN󛲺z/5^SUt~XӀ>':-8knA^]]LSw1NaE^7SWKxԅmǟ)닮mTPȹ߆ ?O̲b!=zoѳ{(wf^|^+Ks/s6ĭ[kY/\^؃ح5\奯IqmS-XjKߵUY f4in - -7/za7L.hN)+Nh*pS`ͥ7g -]=삩ހ8t_kp9/1aL }JӼF~ai]οP{_ȇ?JO|3Fy'seYד/Ǩ^,#=TxJG<^,][+n̛D? 2N=Y7o3(*.*O^ _K{)i1n6[J޸al>3/`76/:S1襸6}=Fؕ ~XqjF%4r|= 7םa!lk܁}k*ln)хzځy8%I9K~jE(5Q&~".7*olt=ڛ$${&)6- O. Q#Dcm6xk=Tu#(9Q[%6{QKnZ`-%\`М}Ғ}~uUν'DsrduMYUu V7u_+ ĸQn'19EMuܲ7^RqķΒG 3‚w?,cGśg7r.Z.Pniɸ暧/>6\XE,wv/E?Լ/IY}_]3<$t±ꤟ 6{`\ب1'_~f肽;k>mO99xu&yYjoF_=:~WoWZ3YI) ?٫T]cхʌSs">j司,|`lHEՐxRߔqTzbuoU -{ X;:ezTo}U/kmzQJ_~Jo]-*P-l+:KA]]x'kR/!us#??c˭<7'"~UgԾ9#<=ÎnFEeɛPi N.|,(JiƳKS5iJ&y=ȗ{cEwU&%y7wGXk=+m[۽&hKiIo;/i+혗aL4OJjuc9<iM߆ԉ{:>9LPsʹoHLOs %R^UzcyaՏWA?^vcuCckqaj73zp~wp<:R'u;#vJ!r$83)N7)g7W_<;iA) g#ge35%4O<2456*{ 2P,'UE4ogO?z'uψIVu5擂jnN;o9?OZK^mD+ϽZdaظ 1B./[ oOf A]:gi%oV{kߧּ8kaz1/%!jzgk#w@w<u>A{s~ /ME%&1r9$-u?74,f8ƈ,{C$YUѹVZZ]' ^EU?#s.={QF(+ y$+qF٥&ܻR]3酪цy2|P|NlFP%|!~d=K20| %RӐvW_w|M:mh=o!%V^|?JPqTFoڍ[no 1?.^ľ̻:ϸ 0>ݟbfxYv^(颾cmǂ7+^eyکG;S8敒vJ3|Vʳn!/'}(N= NqRuv|McIޓ/(!+=ՏEWeP>@^Րs -z_OKN[K5DED>y9 -Q[ݾ{_= xX<3wco ԵEjRΎ2Cߜqq:?%gUA\kࢾ>yᛯO]ν@vLPӖegEEʆ?M~5g^2|H`ǾS},|4";?C)iyWJDls}V';„_i_8b 򭮫[Ƅb7q/60/ KO "~ꧽ L]>Rԏ[hdqw;+tpp(2FM& v"eprq!=ǁ{Ԕ|4H㰞;^sɩwfʳ("Q>Z 5S^-/FIfWJ/YG<fCgvfi٫=;;}-93qiZGnOV k\U6[-?el+}jC?j$F}KN>#iJOFMҚp1X'S6]r;#]S⯃g_Ob~h?G'ٗԉZ/y昘RssgSG9ZjV9:)D+ykyE֩Qڻ/9qg&Q?a}6yuBGj_6zpCD00eۙE8c1ɱt@~ޚ7uW7P l{/MITzgk5Bvn?-]Ž*kZl,Uz[>댬:㔮u |2g*̔ mefu#iNs/RׇX!fF>Ym1,u__J>7)n{XvgNSۜbFQE7z nLcJ&l@f?}D<ĝbj 6g+%s L5*joڎnȽ*o=2M]A$',"oQ7 iB$>~O# s5Ȥ{M{9.lyԃxd^ъK#Դ7WNjtSisZHt6]\ͼoM>nLҋ+I#<)իqJY!>+d&#]W ,rwm} ԭqht3HK {2>^2/G)^(':9v4%esjk1K'=vɈQV..& >Zbuѹ›VBu ʸ{ȵ΂;Ɩsƶi ~LSvz$;82Sq0kC#)P[Y Y/2'<9M۔va= 4us@!F~?ʈq VBX}MoBr}lkYxssc2PV);+ܙCu ??,khL86FJwWq(Vz9k>r{=Mݞ%lUW۾%mhޚa6G=g(  a>ģlk^Tו\4&e{"m4ta+n13=Jlg>xc&c"۝*/USmҲS[ݥOwqy>)l3"iNzWqדo'1ROB0=:]ۛ:L|,6Jy8ۥ+hh/uc&ntRG/^[>fXE/R M=ѣ|gsTYʣ -C!O#u.SM'@GqȘ B4m2&RVpyg5LJpՊv'C\C_e|rmҁ8Q'K;b`hA?X<3 nsMWPF$pK/;Nyfl=ZnDXo啔ےd~Pnlu2:xIz6IBaHJw5ȯsu+M;9 I><WmÜLs(UQNag>1r^n|z+r4e)9peE/<f٧4Bˀmkf뚊͉S~|/6-F˵|s8y&HЂk?iŴwe%+ݨGwl zݺ.LLW6~y!6'@Q7 ;`7vz/C 8'OJ cBx32NtJZ{=YF].}`i+3jjZ=flߛaC#{U*t.rYdlxt:j+YIsko}zJ} (+]SO~n~ȿzqYߚ{ϣVfB暾qRWvֽRwww ; $X>G\ fs>9g"h[gfD{ U{T8\YЖhG>esgS>|_چ#}Mu,r%|wm 9+ǰqΈ>Ȩ昔 LYARv ߉qo'9C4vde~Ap`ǘQǪS~c&G&F{xcyƝzH-GILЊa˝fL'+^wZת -PNBC~pzrdƻ7B>< -~rX;8R;-2\ry*SU u# -.#KP~^XፆE!ݾ(㔤?u 7d@eĂc]4DM`U\ In*=OMqp*㰐/gpQ_OcB?6[ qHp)L1 -pLT?MqI+NvCӫA9Nqxr)) 9us P2= gSf;J;*{2'lާwNJ~:8 | -tv}y|*;F?>ޮl*>yc : ׷BBmȏS-h V.rOz柿nճA\|mSh -P!2jc_Q.,d#EǺ"  -$~I=ZJ9Av쟈or>ˏ9N.a[C$Zfo`a؍|Hir~0Pk?Г@w/^=~ %9XsyE5?kד CȀAz>TQ>)J=MJ>Y@%?<z;OB1anğӍ7꺲'[J6z񵝀K/t,3(#Çρ>ܸ.U峿N@K}&X"jvJlcŌ H-(Xrz/b_}yM#`>] ЃKsbD^,jX@ћF-9Ə -1~Ըc h7P=ЫWA/o^]:'A'ϩЫIXh{*bfyyE(R]ws4$(S+/|\=tt?AOνͭ2^C7+ZޣQ#Ƨ,cB7kȰߐa߾_z~2(=P'_ݻp{N?/?qY]:+ؚh)2JMՃ3}ԋP`N^ܸ -p6NǸ:A~WၿjjrL*xe-gqiKOGO(5YQ'޽:8] &ei`-/T|Lg,l~S_|L*A-8L9YpWǮ_+@Oo|q~ cۡJRM͂/gF%򮚬*?01ǡq*#~w~=}|X߁9zX \$qF~BM^~!T6:iw0UH5xNΙO\ - -/ϯ_\ :ًo*~XL_^%, *. -ԧ,󌠼Vqv*+㛷@8{}''|@.\M;n_3OIf)-,- _Z$ f3\CPD,:]P+Ћ۷9>:oW}@W]=|x7'+ϷBwĤt:x Њ6@G<袢 .䅆K-(k ۠s'On07@.;Pc?\adeRC۝0\L]M+_JkK0o% -~|?(%7AW\i o~tsߺ w>({oȫZܗ>VigW .RJ,I=1훠|rO /.x` <޾  :^Uz?"5MEh5q%~KYS$9G/E| [{?O}p[gwnk1bFE!$LGE!G;H& -Gy!xopx*PÓe O,D.OWTۉ Mq%x>ZhQHY?+q0PEzEB>`˗).T}~wURKeQfE%uS&,WcgGZ0xsgF. cB eW+zz7@w/ſ 4 - IScO~xN=pSu6d[TMWf(-S {e&,MV%mjtMDJԙ/OgM;pwNff_wWr ?bb+aŜseyЇ;J.֬Ȫ(Vj2+7eFĝG@ óTH!3|薘 s۔75mfm4^Dh1Gg⇋m-p#q{l f %bTBmUn5T5U,YfR0+R6dIƆOVf!vz0_,v}u}heTvdm>YWetukjC&nzr7?kDXzzcuN -ůb3 vR2Ԁ:RлzƭѝJ5y2@dYgc)}9J\[1g} *jDu=^ llEZE8pQ9AI]ꄼaSղ#%&^=Lg|j nikmά#}u_Q] e3=xdy-$[ՔRR`4Zu;]cM!:ri<4WBhQ]†@5[ -|=ZkYT=ٺ^AX;0^tukV-),5IFcUXWnTE Г&}"}e'}qϰ5 >[†rLb!XS=Ѕ:HZ*!(#ij)񣕽…FI QpqkH PpwK\J2u=\/naUG;F9~n13 kxd~ڨp$u6uuI^I8.FR[]1 T[$5 ʀ\2*l V0+ - t"tt]gVXcR2[Rb,`m 2Bt%ϥ:@ҋ=Z|[G,ْbigp&96AUtJ@NR->lR?1jclˣUxgq*J>mG!VY{8"8tCM+ۑ -"V+ߥUA;i-oG^ߒ0KWưc1ekoʍNAU3Z( VӬrbM Gs|MZd\sjYQ׷ZWݫZ 1bn,Fln?c`Uz Z\q -jCykE*ܡDCKӥH~,·\Z@=NM,fW\l_ĤLە33\Kl~}؞|G_D -ܙuW'5 s- YhBX) ^VFޑ@2V֙Ƽ֚7n= L8Xߒr'}Ͳ/lX]#3Ӥ" -nb6> ovk~Ǯnrhv !4m*}-:K>jJF1ILCEw(Y6 :~WM-w)I{@[d{Mc؟2*bޖ%nK YL)n٪vtCC!p񦑒汊/ڭ -&tvR4y9Lk]N*g!3uoBmN$Ոu-2cG\Iro=t,xQl{-SW7mxdm'뎂w+ٻZSKׇћ"pȞ -^ٖ0v$BZsg -a_Ô ӥ#6ʿB|L`Rp̑mGNX!fQ;JB`Vd >Id9*ť*Yv*vsLk-QQmGɨ5Vf꺷EKٙf,5R,DoɘNzz5g_]|h`S}m~?ilƄmБ6J;&G2Yfgn-XֹV -|SE.XCD6{kmR.>IxCEP4cC177z ?=?WYs7` ƒiRrwk8*IfM#(lȡ{ytG'q)N 6ӣ!NW7kq+kE ̣i f_OӑJ"|ھM5H ܠخ"o3OpEDoP Ɖi{36  {žCtF&{]-.{pS~e &[$8@'YUx4to^KrjC#|h$kK)wO+rۤ^7dLjH1M/Cg{Qk H12q. ХvN7yeWW]!]t\?"zkw`nuZBM?*:nq4(嗅\ CfUu;vy{`!IhCK|)e}l Lv^ϣr]J.Ƥ:B>~i"?K5Cl -ZHlg[v.7d2!?K[mqQ٫#z+n7F!n~4-xTt(AjSn.A3+JBGp8f5LȡnٛٞgϷ>Yj{>m%Ulni1WV{cdC 3,Lphx4OڝaF"۟_w=@ܣy9MXUdwh]fcBi'> sQsދ躨]whIFlK. -Lء:ULȮVvB3"JH%"Bs ^kb>AL]~oÅg'_|ko @ -v8W_#&sc.\0ay[rdeE@&wv7K<:Lo(}>S}PygZz(Hl(` +[!!~bYEԒ) T2Jr$$l'y42.SM* *xTV>trə8g[At :@tˉEv9&͡ĥlQ]caT,nKN7!~lg -x&i)^}:0?{=xצɆ &hڜ"Y]gdJRᐓKmS &hZ'ԭ܆6"'E'5#k|0ɡ[%}*X=5-A&Ĩ9 f1JjpE\msyhYE!cgmvs 1Df?8ԥeLjy?&gMN5jbB -G~k⏲9MçnMM y UYL+Ch!k*-t[P#,@$,%:qGIʙ*ᚩᢰ犠c$9Րƶ.FuH#4"s|pBG+>u~oa[%.emG%)yvh{cxk -p>yOL+\k+~a-~؞BƏF3Ve^ŧZ-n -L"F$ /rKW%?]%|hbuG@#T٦RfևNtVR.)ɗeޱMM#>v΋77>rJX^F&S[b}v&˃~(IRL_mL~Q4Wڲ)E)x{󑆅>Ph6 1kk9Jz_qU>ퟃnW ^ltfzқ췯U*i D{X5vbKh]v (yADbSk!pyJJ`e}P/S䔫cċJ-57|CF,amqB[}dԞ^P֗ 8Sw4ZhAձY=ЯoK-TPIa6?8퐘OzR!E%nztd80La22+lewƋ;'oM1-"rꮂVQNOb\A|95}rrsk4Mn03|g#w90,!gkIu0H8ofpS!SRD]/;X؝mko.YJrj19s MÀZdFP:̀o?<s#b_5?4U&ޚN㭅 D^4m?amIAbo84!_,*wcif`)fos.&< -&poun -ufcXro-wA[F)9Q1u6 /yw%\SB)V".Mפ=_) 9CBY7a>j.34 }atLWAP7NN|}rFKX-z5X:LrnЙ@ӌ "/ԏk`Λ;WCo LC%'( b -;gxuU ]Xi|u$53e?gx_C! %mj_'8f=KͺXdyz[rk|KC}pE៫\ g!jr+f?iy,HWdJN_&)r8xrCL~8O|X'~==WYݐk[mpesP'~γٻz:j[Os(e~$SԒ /}ޏ - crvܽM#şVD֍wE)>>^0גl0%Vo]9*iF; -u4Ϥ/^l[acgbǰQڢg ;#ؽiR16tKƳ^\Dm C}Xi*?-s%Q ޞ ;lO3#; 5?\z"覹,r+# \[M|k/x:Ӗ`3MR,ٛ"jSIk^yRc}c+lC L쎄Ybhp)`M+~MgޟW3c/ǡ1i6)WL_ƚ_F&s8 K1f36̴\PiVf9hFvg~=%K/ k` -kb6cڟgw,C>duA3;m}hWK -9u 6{Jltf=hK}hG$yᨛ -Q2w$,8:zk96`V%*9M< phWk^yn`Y@KȦdn\cًZ~ͥOaX\gśBFtWP>*X!nzpemq ߣ'\HE.iW2[8 dWO3Ѧ tתS:,K ䷆:!"XL[^DiQ>.m70"f*Qd -aseC΋\y e9^P|2Pu5L}}`拃x[ZFlacyi&pEݟa]_ȑ s]9ϭ2p䎂g`{mڶ)\+oM7| Z@G"..ױn T$Y$wʬy{p&)^}gQk~*rUGnF|7v1욠Y1qb~~Bm^G'J),Sx{s,"q߮1Nα+yIFGC.J+=Eo7{!オL} z£x 6w+e7CS}9xWBE/t_lKw0M%Ms9ke}OҞ6=.{md2/Ff*kԴ -0|]}5/g??+ƙHXPlvNؤ^7_G쪩0,{דKБaLy䭳ԐДrO<ېr !溠RA⭵~HS9iXL{JAXs~7CEش4.ۢ%di? IY[)5zCuYNHa: _ZƉI}ĸfbnR͵!XS*9]->go-|A72hk Hy}!|i -Ty1 OkX<{h1R:wX۹oV{ LCIL'KB"R]._!'IإQѷ:϶C/]E]fܳQ' -6mJ &rkCVh!]Z2,!{Ӕ -"SZtMC(])my -m ::1I0bA^ !lbW[:f2ו:Re[k\{%%'`a hEJ̛AqdWf :זXƊ"%]l~9olJyaSˆ/Wf`N䅂Z'$rJ6ioo ux뚪:`/Z>֣rdl gK W{R^hk3_=m NP)[.<*2|m85h'8rv8DӲ)*y*$<We`+émvj+.3])p8$$>H]5&.ř+G~)x0_5W%_i5?lt&0__*:20p?{t&ձv. . sc}f%N@U}.tl4y g}V2ZvSI2ԄKz&hNhzﳄ~LbGab|.^P9A) jZRC^*)NAblGg?0F.۷ޭ41eއ!xUP?*j72~w{GK}>KN{`l%ge> ^.P@.lp-v={Mt- -:k3z+JR>Iw{1ۻ]ApKy[}Ƞ{3MwVz͓ԢQNYUS h] `|]˰r|샸{o_gU3b| >\`q~ZI\BS%h[I^?-wU$+6DN>J伭IREF)tۜm+} 4piXC]lRrL#n𗕶+И)VN -]ugL[ԍ(o]c<v8*NTossd̾bf}_alL6b[ɡk;We*ElS Z,~cdE;X;\=0 -j-cGA 3td?@06.4>97 '#f ynfڳNob7V16ѥ%9ԤO?W6t, =KvXPαV !f+IVê.xbS}m欇NǭApо"JC(IQV1,W1C􅶊˝)B5E']UHyDV7M(0.=KtMK+pLU,js cncMXհ|wkck CKɛLz#<覈66 u+rfإK=+WYs/ _lx[)ŀ`ڏk"gB%\MtshX\x=&kT7~O[SKŘm}^xEB.ڟToJ1s', -;:ڮga,rJȝAcqGS-ܔ*!wO/;2TVL(%ia4HK'JS_QSSA@ؕ-8d9.uUC.K}I:ڑvdD<|c4n/UJ5Vrݸ;j.z[Mj2ޟ׹+k\r)8(7A]ZaCBl 8.1AJMӶƔmKraƣ1zҽT̕ bչ1\|;:ơR:tWǦ9 R `R?4hc #JN4{ Gm?~x\JA|L]~]MIP6 :4ZБZ#~MJzv9(!IVzG'x^޺,* ܍v*/Z<;U>vW -Q3MSxCcaJyW|_ |vVLUxD.Xȕ6l(b3鶼*|qX)t]@ٴY.b`5ئĦUBޮї`1CҊ*A -o?P@7X97è˭%_/>ZNͭ-%mp<(kayasX(BEwQ6@- z<@,{yv1@l2\*⪼+ ]A)f[DH7YA/Eo ]@/J)u%˩1zzw_mܦf!vT,M21!2uSB͗W'EEYl{,%>5 !zСS=7 Y:3G"GI{3¦iaIBxcZ5LIFݔ˶2 -knZQWGHYfkMo6QX2_\MUj}x5.XSƄ[Fq~ծ"ߥ -N%/nw߫ A^(*j}h/~VS9g4k#ܴKpvmY>hU;|mKDѩV !< _n|3rs|tui(*f=bwUu̍^CäKE]j*".7mõ':UШn@[= -?ER ]+X!j.|Rk[c]]?1b] "vsvVŖ7-%aK='a⶧(y2"į-ҥT;4KE(e}[N5Qݡt<&UhTǗ`v, SN5b"7 @.%>c<rcp -!ZΏ~zr/-ec47o -%x9Ȍ}qT9z6Ӯv2LS]iK -[BAN  Tgy}#$Ws{ek⪅ v)[fden%F~\-&3#C. !SB&bqԏ-gՎ+3տqJmD;֣F NV;ۀOq>`?YBO)IBnܛ }(T_w]꺑ZvԿ=FKM+oNv -}OԾr\ZTUhMs-DM(wb - D7虣Q2 6▎ٴK*`NMBzЙy/71oc>l9fC\W,{eOqe~Qa__T?jz$ޗA~m/pB\tgu̟ӒFژVR l6dP i*pݝ`RCn*l)|pZسeGxh)25H q]fNo+e/C]'i7 \V؇KPɧוo!IGeWiEMdoG=Ewni4atAwV١O߲3wVg;7߇Ʈ*e㭠zLn=侃]x%({5ni2"7kX7jH _/Sb획X1|X@]#CdÄ/w6XVڤ/qAÄ%5mȈNxE=Q=:tO۟{1hy)D{[OoSLiQ>^V I..+#iі6w'4n~7D=',e9:"(A홱 -pK4X]4cLk}>L6,m:Flu󋲆o#l*%궭MxC3)С~&qIOm6D:ɵ> -|Y4"(idtAJ{Q V Tr_A,]X[*%:}J*<"9a\7Ͼ.Z}77/RDADyc '1ha_@qcs[xq -kxfDjK= 3=oK0O޲10_&ZŶK$W F Շ5's -x7)eUeo],FZ݅~p巭Ĭ?Ig4O0~\ ^ Լ䁣Nw6Ş]U2 ͉^A]KuMm{cl ~|ZEFJɘ鹅Y|VR9,O/Ps̶[B qͧLa~%33ωZdꁇdIx}/mFm5&qGXCb ٤}ґ=*}1,}oo|2'C_;Vfu#5Dw =tK_LK^X .BgCփu<[+z00~Ce=<:3dBEMgo45,:eXi '83ygY5nZiiNcRnW#ޝٱԼWkS} UxBHˇo7d0 J({J{?sѼ>e߅-\ՀcۊX7ݮ>vLT\%7hG- mT$m.k_=9^ ڲf0A6glV-ugt=1aFQQճE"^UAx\'x1^ۛ6xՊ C6k{u#>:e -}VQ cmy9DZ[֦Om'qBјfm̿Wy1.5)(1?){}\MοH+|{Z ͸炞.+93JZya:ĭ_Q!6 u hXbT8 -hj(`_U=XB|*m%Uŝ 0;Jvp*)|[VbE 6g[!E+frzʌa%OIs>{ϣ0)dw=qT^jRRrIdY2Bgvq(1vn_VZyrPp)\X9O}WԄnPXP4wvW>?3R鋛M%5.` ZRɊ)N&=հnͫ1N.aB_V!De׽l=&.G)4 {-=/9UW>aَٝ}z[Cr4'[Y>ݖ}!?c endstream endobj 325 0 obj [/ICCBased 372 0 R] endobj 6 0 obj <> endobj 138 0 obj <> endobj 242 0 obj [/View/Design] endobj 243 0 obj <>>> endobj 111 0 obj [/View/Design] endobj 112 0 obj <>>> endobj 270 0 obj [269 0 R] endobj 398 0 obj <> endobj xref 0 399 0000000004 65535 f -0000000016 00000 n -0000000178 00000 n -0000062829 00000 n -0000000005 00000 f -0000000007 00000 f -0001693298 00000 n -0000000009 00000 f -0000062880 00000 n -0000000010 00000 f -0000000011 00000 f -0000000012 00000 f -0000000013 00000 f -0000000014 00000 f -0000000015 00000 f -0000000016 00000 f -0000000017 00000 f -0000000018 00000 f -0000000019 00000 f -0000000020 00000 f -0000000021 00000 f -0000000022 00000 f -0000000023 00000 f -0000000024 00000 f -0000000025 00000 f -0000000026 00000 f -0000000027 00000 f -0000000028 00000 f -0000000029 00000 f -0000000030 00000 f -0000000031 00000 f -0000000032 00000 f -0000000033 00000 f -0000000034 00000 f -0000000035 00000 f -0000000036 00000 f -0000000037 00000 f -0000000038 00000 f -0000000039 00000 f -0000000040 00000 f -0000000041 00000 f -0000000042 00000 f -0000000043 00000 f -0000000044 00000 f -0000000045 00000 f -0000000046 00000 f -0000000047 00000 f -0000000048 00000 f -0000000049 00000 f -0000000050 00000 f -0000000051 00000 f -0000000052 00000 f -0000000053 00000 f -0000000054 00000 f -0000000055 00000 f -0000000056 00000 f -0000000057 00000 f -0000000058 00000 f -0000000059 00000 f -0000000060 00000 f -0000000061 00000 f -0000000062 00000 f -0000000063 00000 f -0000000064 00000 f -0000000065 00000 f -0000000066 00000 f -0000000067 00000 f -0000000068 00000 f -0000000069 00000 f -0000000070 00000 f -0000000071 00000 f -0000000072 00000 f -0000000073 00000 f -0000000074 00000 f -0000000075 00000 f -0000000076 00000 f -0000000077 00000 f -0000000078 00000 f -0000000079 00000 f -0000000080 00000 f -0000000081 00000 f -0000000082 00000 f -0000000083 00000 f -0000000084 00000 f -0000000085 00000 f -0000000086 00000 f -0000000087 00000 f -0000000088 00000 f -0000000089 00000 f -0000000090 00000 f -0000000091 00000 f -0000000092 00000 f -0000000093 00000 f -0000000094 00000 f -0000000095 00000 f -0000000096 00000 f -0000000097 00000 f -0000000098 00000 f -0000000099 00000 f -0000000100 00000 f -0000000101 00000 f -0000000102 00000 f -0000000103 00000 f -0000000104 00000 f -0000000105 00000 f -0000000106 00000 f -0000000107 00000 f -0000000108 00000 f -0000000109 00000 f -0000000110 00000 f -0000000113 00000 f -0001693562 00000 n -0001693594 00000 n -0000000114 00000 f -0000000115 00000 f -0000000116 00000 f -0000000117 00000 f -0000000118 00000 f -0000000119 00000 f -0000000120 00000 f -0000000121 00000 f -0000000122 00000 f -0000000123 00000 f -0000000124 00000 f -0000000125 00000 f -0000000126 00000 f -0000000127 00000 f -0000000128 00000 f -0000000129 00000 f -0000000130 00000 f -0000000131 00000 f -0000000132 00000 f -0000000133 00000 f -0000000134 00000 f -0000000135 00000 f -0000000136 00000 f -0000000193 00000 f -0000000000 00000 f -0001693370 00000 n -0000000000 00000 f -0000000000 00000 f -0000000000 00000 f -0000000000 00000 f -0000000000 00000 f -0000000000 00000 f -0000000000 00000 f -0000000000 00000 f -0000000000 00000 f -0000000000 00000 f -0000000000 00000 f -0000000000 00000 f -0000000000 00000 f -0000000000 00000 f -0000000000 00000 f -0000000000 00000 f -0000000000 00000 f -0000000000 00000 f -0000000000 00000 f -0000000000 00000 f -0000000000 00000 f -0000000000 00000 f -0000000000 00000 f -0000000000 00000 f -0000000000 00000 f -0000000000 00000 f -0000000000 00000 f -0000000000 00000 f -0000000000 00000 f -0000000000 00000 f -0000000000 00000 f -0000000000 00000 f -0000000000 00000 f -0000000000 00000 f -0000000000 00000 f -0000000000 00000 f -0000000000 00000 f -0000000000 00000 f -0000000000 00000 f -0000000000 00000 f -0000000000 00000 f -0000000000 00000 f -0000000000 00000 f -0000000000 00000 f -0000000000 00000 f -0000000000 00000 f -0000000000 00000 f -0000000000 00000 f -0000000000 00000 f -0000000000 00000 f -0000000000 00000 f -0000000000 00000 f -0000000000 00000 f -0000000000 00000 f -0000000000 00000 f -0000000000 00000 f -0000000000 00000 f -0000000000 00000 f -0000000000 00000 f -0000000000 00000 f -0000000000 00000 f -0000000000 00000 f -0000000000 00000 f -0000000000 00000 f -0000000000 00000 f -0000000000 00000 f -0000000000 00000 f -0000000000 00000 f -0000000000 00000 f -0000000000 00000 f -0000000000 00000 f -0000000000 00000 f -0000000000 00000 f -0000000000 00000 f -0000000000 00000 f -0000000000 00000 f -0000000000 00000 f -0000000000 00000 f -0000000000 00000 f -0000000000 00000 f -0000000000 00000 f -0000000000 00000 f -0000000000 00000 f -0000000000 00000 f -0000000000 00000 f -0000000000 00000 f -0000000000 00000 f -0000000000 00000 f -0000000000 00000 f -0000000000 00000 f -0000000000 00000 f -0000000000 00000 f -0000000000 00000 f -0000000000 00000 f -0000000000 00000 f -0000000000 00000 f -0000000000 00000 f -0000000000 00000 f -0000000000 00000 f -0000000000 00000 f -0000000000 00000 f -0000000000 00000 f -0000000000 00000 f -0001693444 00000 n -0001693476 00000 n -0000000000 00000 f -0000000000 00000 f -0000000000 00000 f -0000000000 00000 f -0000000000 00000 f -0000000000 00000 f -0000000000 00000 f -0000000000 00000 f -0000000000 00000 f -0000000000 00000 f -0000000000 00000 f -0000000000 00000 f -0000000000 00000 f -0000000000 00000 f -0000000000 00000 f -0000000000 00000 f -0000000000 00000 f -0000000000 00000 f -0000000000 00000 f -0000000000 00000 f -0000000000 00000 f -0000000000 00000 f -0000000000 00000 f -0000000000 00000 f -0000479946 00000 n -0000479754 00000 n -0001693680 00000 n -0000063959 00000 n -0000064821 00000 n -0000484225 00000 n -0000087131 00000 n -0000483747 00000 n -0000087017 00000 n -0000483861 00000 n -0000483985 00000 n -0000484109 00000 n -0000066876 00000 n -0000067722 00000 n -0000068564 00000 n -0000068899 00000 n -0000069401 00000 n -0000069915 00000 n -0000070362 00000 n -0000070868 00000 n -0000071204 00000 n -0000071540 00000 n -0000072048 00000 n -0000072554 00000 n -0000072890 00000 n -0000073405 00000 n -0000073741 00000 n -0000074242 00000 n -0000074755 00000 n -0000075201 00000 n -0000075705 00000 n -0000076040 00000 n -0000076375 00000 n -0000076881 00000 n -0000077385 00000 n -0000077720 00000 n -0000078167 00000 n -0000078502 00000 n -0000079001 00000 n -0000079533 00000 n -0000079995 00000 n -0000080499 00000 n -0000080835 00000 n -0000081171 00000 n -0000081679 00000 n -0000082211 00000 n -0000082547 00000 n -0000083055 00000 n -0000083391 00000 n -0000083908 00000 n -0000084422 00000 n -0000084934 00000 n -0000085269 00000 n -0000085604 00000 n -0000086112 00000 n -0000086618 00000 n -0000064885 00000 n -0001693261 00000 n -0000066312 00000 n -0000066362 00000 n -0000479690 00000 n -0000479626 00000 n -0000479562 00000 n -0000479498 00000 n -0000479434 00000 n -0000479370 00000 n -0000479306 00000 n -0000479242 00000 n -0000479178 00000 n -0000479114 00000 n -0000479050 00000 n -0000478986 00000 n -0000478922 00000 n -0000478858 00000 n -0000478794 00000 n -0000478730 00000 n -0000478666 00000 n -0000478602 00000 n -0000478538 00000 n -0000478474 00000 n -0000478410 00000 n -0000478346 00000 n -0000478282 00000 n -0000478218 00000 n -0000478154 00000 n -0000478090 00000 n -0000478026 00000 n -0000477962 00000 n -0000477898 00000 n -0000477834 00000 n -0000477770 00000 n -0000477706 00000 n -0000477642 00000 n -0000477578 00000 n -0000477514 00000 n -0000477450 00000 n -0000477386 00000 n -0000477322 00000 n -0000477258 00000 n -0000477194 00000 n -0000477130 00000 n -0000477066 00000 n -0000477002 00000 n -0000086953 00000 n -0000087168 00000 n -0000479828 00000 n -0000479860 00000 n -0000480362 00000 n -0000480720 00000 n -0000484301 00000 n -0000484881 00000 n -0000486189 00000 n -0000521227 00000 n -0000586816 00000 n -0000652405 00000 n -0000717994 00000 n -0000783583 00000 n -0000849172 00000 n -0000914761 00000 n -0000980350 00000 n -0001045939 00000 n -0001111528 00000 n -0001168549 00000 n -0001234138 00000 n -0001299727 00000 n -0001365316 00000 n -0001430905 00000 n -0001496494 00000 n -0001562083 00000 n -0001627672 00000 n -0001693707 00000 n -trailer <<0D2641520B8A479089A970C9DC928918>]>> startxref 1693896 %%EOF \ No newline at end of file diff --git a/docs/develop/psyplot_framework.ai.license b/docs/develop/psyplot_framework.ai.license deleted file mode 100644 index b21fae9..0000000 --- a/docs/develop/psyplot_framework.ai.license +++ /dev/null @@ -1,3 +0,0 @@ -SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH - -SPDX-License-Identifier: CC-BY-4.0 diff --git a/docs/develop/psyplot_framework.gif.license b/docs/develop/psyplot_framework.gif.license deleted file mode 100644 index d46171c..0000000 --- a/docs/develop/psyplot_framework.gif.license +++ /dev/null @@ -1,4 +0,0 @@ -SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH - -SPDX-License-Identifier: CC-BY-4.0 -SPDX-License-Identifier: CC-BY-4.0 diff --git a/docs/develop/psyplot_framework.png b/docs/develop/psyplot_framework.png deleted file mode 100644 index e96f8a1..0000000 Binary files a/docs/develop/psyplot_framework.png and /dev/null differ diff --git a/docs/develop/psyplot_framework.png.license b/docs/develop/psyplot_framework.png.license deleted file mode 100644 index b21fae9..0000000 --- a/docs/develop/psyplot_framework.png.license +++ /dev/null @@ -1,3 +0,0 @@ -SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH - -SPDX-License-Identifier: CC-BY-4.0 diff --git a/docs/make.bat b/docs/make.bat deleted file mode 100644 index 2e7d9b0..0000000 --- a/docs/make.bat +++ /dev/null @@ -1,39 +0,0 @@ -REM SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH -REM -REM SPDX-License-Identifier: CC0-1.0 - -@ECHO OFF - -pushd %~dp0 - -REM Command file for Sphinx documentation - -if "%SPHINXBUILD%" == "" ( - set SPHINXBUILD=sphinx-build -) -set SOURCEDIR=. -set BUILDDIR=_build - -if "%1" == "" goto help - -%SPHINXBUILD% >NUL 2>NUL -if errorlevel 9009 ( - echo. - echo.The 'sphinx-build' command was not found. Make sure you have Sphinx - echo.installed, then set the SPHINXBUILD environment variable to point - echo.to the full path of the 'sphinx-build' executable. Alternatively you - echo.may add the Sphinx directory to PATH. - echo. - echo.If you don't have Sphinx installed, grab it from - echo.http://sphinx-doc.org/ - exit /b 1 -) - -%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% -goto end - -:help -%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% - -:end -popd diff --git a/docs/requirements.txt b/docs/requirements.txt deleted file mode 100644 index b6c239f..0000000 --- a/docs/requirements.txt +++ /dev/null @@ -1,7 +0,0 @@ -# SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH -# -# SPDX-License-Identifier: CC0-1.0 - -git+https://codebase.helmholtz.cloud/hcdc/hereon-netcdf/sphinxext.git -git+https://codebase.helmholtz.cloud/psyplot/psy-simple.git@fix-ci -git+https://codebase.helmholtz.cloud/psyplot/psy-maps.git@fix-ci diff --git a/genindex.html b/genindex.html new file mode 100644 index 0000000..b26137a --- /dev/null +++ b/genindex.html @@ -0,0 +1,1802 @@ + + + + + + Index — psyplot documentation + + + + + + + + + + + + + + + + + + + + + + + + +

+ + +
+ +
+
+
+
    +
  • + +
  • +
  • +
+
+
+
+
+ + +

Index

+ +
+ A + | B + | C + | D + | E + | F + | G + | H + | I + | J + | K + | L + | M + | N + | O + | P + | R + | S + | T + | U + | V + | W + +
+

A

+ + + +
+ +

B

+ + + +
+ +

C

+ + + +
+ +

D

+ + + +
+ +

E

+ + + +
+ +

F

+ + + +
+ +

G

+ + + +
+ +

H

+ + + +
+ +

I

+ + + +
+ +

J

+ + + +
+ +

K

+ + +
+ +

L

+ + + +
+ +

M

+ + + +
+ +

N

+ + + +
+ +

O

+ + + +
+ +

P

+ + + +
+ +

R

+ + + +
+ +

S

+ + + +
+ +

T

+ + + +
+ +

U

+ + + +
+ +

V

+ + + +
+ +

W

+ + + +
+ + + +
+
+ + + +
+
+
+
+ + + + \ No newline at end of file diff --git a/getting_started.html b/getting_started.html new file mode 100644 index 0000000..f855778 --- /dev/null +++ b/getting_started.html @@ -0,0 +1,1248 @@ + + + + + + + Getting started — psyplot documentation + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +
+

Getting started

+
+

Initialization and interactive usage

+

This section shall introduce you how to read data from a netCDF file and +visualize it via psyplot. For this, you need to have netCDF4 and the +psy-maps psyplot plugin to be installed (see install).

+

Furthermore we use the demo.nc netCDF file for our +demonstrations.

+
+

Note

+

We recommend to either run this example using our +GUI. However, you can also either use +IPython from the terminal via

+
conda install ipython  # or pip install ipython
+ipython  # starts the ipython console
+
+
+

and copy-paste the commands in this +example, or you use a jupyter notebook via

+
conda install jupyter  # or pip install jupyter
+jupyter notebook  # starts the notebook server
+
+
+

Then create a new notebook in the desired location and copy-paste the +examples below. If you want, we also recommend to include the following +commands in the notebook

+
import psyplot.project as psy
+# show the figures inline in the notebook and not in a separate window
+%matplotlib inline
+# don't close the figures after showing them, because than the update
+# would not work
+%config InlineBackend.close_figures = False
+# show the figures after they are drawn or updated. This is useful
+# for the visualization in the jupyter notebook
+psy.rcParams['auto_show'] = True
+
+
+
+

After you installed psyplot, you can import the package via

+
In [1]: import psyplot
+
+
+

Psyplot has several modules and subpackages. The main module for the use of +psyplot is the project module.

+
In [2]: import psyplot.project as psy
+
+
+

Plots can be created using the attributes of the plot instance of +the ProjectPlotter.

+

Each new plugin defines several plot methods. In case of the psy-maps +package, those are

+
In [3]: psy.plot.show_plot_methods()
+barplot
+    Make a bar plot of one-dimensional data
+combined
+    Plot a 2D scalar field with an overlying vector field
+density
+    Make a density plot of point data
+fldmean
+    Calculate and plot the mean over x- and y-dimensions
+lineplot
+    Make a line plot of one-dimensional data
+mapcombined
+    Plot a 2D scalar field with an overlying vector field on a map
+mapplot
+    Plot a 2D scalar field on a map
+mapvector
+    Plot a 2D vector field on a map
+plot2d
+    Make a simple plot of a 2D scalar field
+vector
+    Make a simple plot of a 2D vector field
+violinplot
+    Make a violin plot of your data
+
+
+

So to create a simple 2D plot of the temperature field 't2m', you can +type

+
In [4]: p = psy.plot.mapplot('demo.nc', name='t2m')
+
+
+_images/docs_getting_started.png +
+

Note

+

If you’re not using the GUI, you have to +call the show() method to display the plot, i.e. just run

+
p.show()
+
+
+
+

Now you created your first project

+
In [5]: p
+Out[5]: psyplot.project.Project([    arr0: 2-dim DataArray of t2m, with (lat, lon)=(96, 192), lev=1e+05, time=1979-01-31T18:00:00])
+
+
+

which contains the xarray.DataArray that stores the data and the +corresponding plotter that visualizes it

+
In [6]: p[0]
+Out[6]: 
+<xarray.DataArray 't2m' (lat: 96, lon: 192)> Size: 74kB
+array([[251.41689, 251.454  , 251.48915, ..., 251.29774, 251.33876, 251.37978],
+       [254.16493, 254.33095, 254.50087, ..., 253.54774, 253.76845, 253.96376],
+       [255.86024, 256.3114 , 256.72742, ..., 254.40712, 254.90517, 255.42665],
+       ...,
+       [263.70984, 263.6454 , 263.58875, ..., 263.96375, 263.86804, 263.78406],
+       [262.4989 , 262.48718, 262.47742, ..., 262.5536 , 262.5321 , 262.51453],
+       [260.8485 , 260.8661 , 260.88367, ..., 260.79578, 260.81335, 260.83093]],
+      dtype=float32)
+Coordinates:
+  * lon      (lon) float64 2kB 0.0 1.875 3.75 5.625 ... 352.5 354.4 356.2 358.1
+  * lat      (lat) float64 768B 88.57 86.72 84.86 83.0 ... -84.86 -86.72 -88.57
+    lev      float64 8B 1e+05
+    time     datetime64[ns] 8B 1979-01-31T18:00:00
+Attributes:
+    long_name:  Temperature
+    units:      K
+    code:       130
+    table:      128
+    grid_type:  gaussian
+
+In [7]: type(p[0].psy.plotter)
+Out[7]: psy_maps.plotters.FieldPlotter
+
+
+

The visualization and data handling within the psyplot framework is designed to +be as easy, flexible and interactive as possible. The appearance of a plot is +controlled by the formatoptions of the plotter. In our case, they are the +following:

+
In [8]: p.keys()
++-------------------+-------------------+-------------------+-------------------+
+| background        | bounds            | cbar              | cbarspacing       |
++-------------------+-------------------+-------------------+-------------------+
+| clabel            | clabelprops       | clabelsize        | clabelweight      |
++-------------------+-------------------+-------------------+-------------------+
+| clat              | clip              | clon              | cmap              |
++-------------------+-------------------+-------------------+-------------------+
+| cticklabels       | ctickprops        | cticks            | cticksize         |
++-------------------+-------------------+-------------------+-------------------+
+| ctickweight       | datagrid          | extend            | figtitle          |
++-------------------+-------------------+-------------------+-------------------+
+| figtitleprops     | figtitlesize      | figtitleweight    | google_map_detail |
++-------------------+-------------------+-------------------+-------------------+
+| grid_color        | grid_labels       | grid_labelsize    | grid_settings     |
++-------------------+-------------------+-------------------+-------------------+
+| interp_bounds     | levels            | lonlatbox         | lsm               |
++-------------------+-------------------+-------------------+-------------------+
+| map_extent        | mask              | mask_datagrid     | maskbetween       |
++-------------------+-------------------+-------------------+-------------------+
+| maskgeq           | maskgreater       | maskleq           | maskless          |
++-------------------+-------------------+-------------------+-------------------+
+| miss_color        | plot              | post              | post_timing       |
++-------------------+-------------------+-------------------+-------------------+
+| projection        | stock_img         | text              | tight             |
++-------------------+-------------------+-------------------+-------------------+
+| title             | titleprops        | titlesize         | titleweight       |
++-------------------+-------------------+-------------------+-------------------+
+| transform         | transpose         | xgrid             | ygrid             |
++-------------------+-------------------+-------------------+-------------------+
+
+
+

they can be investigated through the Project.keys(), +summaries() and docs(), or the corresponding +low level methods of the Plotter class, +show_keys(), +show_summaries() and +show_docs().

+

Updating a formatoption is straight forward. Each formatoption accepts a certain +type of data. Let’s say, we want to have a different projection. Then we can +look at the types this formatoption accepts using the Project.docs()

+
In [9]: p.docs('projection')
+projection
+==========
+Specify the projection for the plot
+
+This formatoption defines the projection of the plot
+
+Possible types
+--------------
+cartopy.crs.CRS
+    A cartopy projection instance (e.g. :class:`cartopy.crs.PlateCarree`)
+str
+    A string specifies the projection instance to use. The centered
+    longitude and latitude are determined by the :attr:`clon` and
+    :attr:`clat` formatoptions.
+    Possible strings are (each standing for the specified projection)
+
+    =========== =======================================
+    cf          try to decode the CF-conventions
+    cyl         :class:`cartopy.crs.PlateCarree`
+    robin       :class:`cartopy.crs.Robinson`
+    moll        :class:`cartopy.crs.Mollweide`
+    geo         :class:`cartopy.crs.Geostationary`
+    northpole   :class:`cartopy.crs.NorthPolarStereo`
+    southpole   :class:`cartopy.crs.SouthPolarStereo`
+    ortho       :class:`cartopy.crs.Orthographic`
+    stereo      :class:`cartopy.crs.Stereographic`
+    near        :class:`cartopy.crs.NearsidePerspective`
+    rotated     :class:`cartopy.crs.RotatedPole`
+    =========== =======================================
+
+    The special case ``'cf'`` tries to decode the CF-conventions in the
+    data. If this is not possible, we assume a standard lat-lon projection
+    (``'cyl'``)
+
+See Also
+--------
+`Grid-mappings of cf-conventions <http://cfconventions.org/Data/cf-conventions/cf-conventions-1.8/cf-conventions.html#appendix-grid-mappings>`__
+
+Warnings
+--------
+An update of the projection clears the axes!
+
+
+

Let’s use an orthogonal projection. The update goes via the +Project.update() method which goes all the way down to the +psyplot.plotter.Plotter.update() and the +psy_maps.plotters.Projection.update() method of the formatoption.

+
In [10]: p.update(projection='ortho')
+
+
+_images/docs_getting_started_1.png +
+

Note

+

Actually, in this case an update of the projection requires that the entire +axes is cleared and the plot is drawn again. If you want to know more about +it, check the requires_clearing +attribute of the formatoption.

+
+

Our framework also let’s us update the dimensions of the data we show. For +example, if we want to display the field for february, we can type

+
# currently we are displaying january
+In [11]: p[0].time.values
+Out[11]: np.datetime64('1979-01-31T18:00:00.000000000')
+
+In [12]: p.update(time='1979-02', method='nearest')
+
+# now its february
+In [13]: p[0].time.values
+Out[13]: np.datetime64('1979-01-31T18:00:00.000000000')
+
+
+

which is in our case equivalent for choosing the second index in our time +coordinate via

+
In [14]: p.update(time=1)
+
+
+

So far for the first quick introduction. If you are interested you are welcomed +to visit our example galleries or continue with this +guide.

+

In the end, don’t forget to close the project in order to delete the data from +the memory and close the figures

+
In [15]: p.close(True, True, True)
+
+
+
+
+

Choosing the dimension

+

As you saw already above, the scalar variable 't2m' has multiple time +steps and we can control what is shown via the update() +method. By default, the mapplot() +plot method chooses the first time step and the first vertical level +(if those dimensions exist).

+

However, you can also specify the exact data slice for your visualization based +upon the dimensions in you dataset. When doing that, you basically do not have +to care about the exact dimension names in the netCDF files, because those are +decoded following the CF Conventions. Hence +each of the above dimensions are assigned to one of the general dimensions +'t' (time), 'z' (vertical dimension), 'y' (horizontal North-South +dimension) and 'x' (horizontal East-West dimension). In our demo file, +the dimensions are therefore decoded as 'time''t', +'lev''z', 'lon''x', +'lat''y'.

+

Hence it is equivalent if you type

+
In [16]: psy.plot.mapplot('demo.nc', name='t2m', t=1)
+Out[16]: psyplot.project.Project([    arr0: 2-dim DataArray of t2m, with (lat, lon)=(96, 192), lev=1e+05, time=1979-02-28T18:00:00])
+
+
+

or

+
In [17]: psy.plot.mapplot('demo.nc', name='t2m', time=1)
+Out[17]: psyplot.project.Project([    arr0: 2-dim DataArray of t2m, with (lat, lon)=(96, 192), lev=1e+05, time=1979-02-28T18:00:00])
+
+
+

Finally you can also be very specific using the dims keyword via

+
In [18]: psy.plot.mapplot('demo.nc', name='t2m', dims={'time': 1})
+Out[18]: psyplot.project.Project([    arr0: 2-dim DataArray of t2m, with (lat, lon)=(96, 192), lev=1e+05, time=1979-02-28T18:00:00])
+
+
+

You can also use the method keyword from the plotting function to use the +advantages of the xarray.DataArray.sel() method. E.g. to plot the data +corresponding to March 1979 you can use

+
In [19]: psy.plot.mapplot('demo.nc', name='t2m', t='1979-03',
+   ....:                  method='nearest', z=100000)
+   ....: 
+Out[19]: psyplot.project.Project([    arr0: 2-dim DataArray of t2m, with (lat, lon)=(96, 192), lev=1e+05, time=1979-02-28T18:00:00])
+
+
+
+

Note

+

If your netCDF file does (for whatever reason) not follow the CF Conventions, +we interprete the last dimension as the x-dimension, the second +last dimension (if existent) as the y-dimension, the third last dimension as +the z-dimension. The time dimension however has to have the name +'time'. If that still does not fit your netCDF files, you can specify +the correct names in the rcParams, namely

+
In [20]: psy.rcParams.find_all('decoder.(x|y|z|t)')
+Out[20]: 
+RcParams({'decoder.t': {'time'},
+          'decoder.x': set(),
+          'decoder.y': set(),
+          'decoder.z': set()})
+
+
+
+
+
+

Configuring the appearance of the plot

+

psyplot is build upon the great and extensive features of the matplotlib +package. Hence, our framework can in principle be seen as a high-level +interface to the matplotlib functionalities. However you can always access +the basic matplotlib objects like figures and axes if you need.

+

In the psyplot framework, the communication to matplotlib is done via +formatoptions that control the appearence of a plot. Each plot method +(i.e. each attribute of psyplot.project.plot) has several a set of +them and they set up the corresponding plotter.

+

Formatoptions are all designed for an interactive usage and can usually be +controlled with very simple commands. They range from simple formatoptions +like choosing the title to +choosing the latitude-longitude box of the data.

+

The formatoptions depend on the specific plotting method and can be seen via +the methods

+ + + + + + + + + + + + +

keys(*args, **kwargs)

Classmethod to return a nice looking table with the given formatoptions

summaries(*args, **kwargs)

Method to print the summaries of the formatoptions

docs(*args, **kwargs)

Method to print the full documentations of the formatoptions

+

For example to look at the formatoptions of the +mapplot method in an interactive +session, type

+
In [21]: psy.plot.mapplot.keys(grouped=True)  # to see the fmt keys
+******************
+Axes formatoptions
+******************
++------------+------------+------------+
+| background | tight      | transpose  |
++------------+------------+------------+
+
+**************************
+Color coding formatoptions
+**************************
++-------------+-------------+-------------+-------------+
+| bounds      | cbar        | cbarspacing | cmap        |
++-------------+-------------+-------------+-------------+
+| ctickprops  | cticksize   | ctickweight | extend      |
++-------------+-------------+-------------+-------------+
+| levels      | miss_color  |             |             |
++-------------+-------------+-------------+-------------+
+
+*******************
+Label formatoptions
+*******************
++----------------+----------------+----------------+----------------+
+| clabel         | clabelprops    | clabelsize     | clabelweight   |
++----------------+----------------+----------------+----------------+
+| figtitle       | figtitleprops  | figtitlesize   | figtitleweight |
++----------------+----------------+----------------+----------------+
+| text           | title          | titleprops     | titlesize      |
++----------------+----------------+----------------+----------------+
+| titleweight    |                |                |                |
++----------------+----------------+----------------+----------------+
+
+***************************
+Miscallaneous formatoptions
+***************************
++-------------------+-------------------+-------------------+-------------------+
+| clat              | clip              | clon              | datagrid          |
++-------------------+-------------------+-------------------+-------------------+
+| google_map_detail | grid_color        | grid_labels       | grid_labelsize    |
++-------------------+-------------------+-------------------+-------------------+
+| grid_settings     | interp_bounds     | lonlatbox         | lsm               |
++-------------------+-------------------+-------------------+-------------------+
+| map_extent        | mask_datagrid     | projection        | stock_img         |
++-------------------+-------------------+-------------------+-------------------+
+| transform         | xgrid             | ygrid             |                   |
++-------------------+-------------------+-------------------+-------------------+
+
+***********************
+Axis tick formatoptions
+***********************
++-------------+-------------+
+| cticklabels | cticks      |
++-------------+-------------+
+
+*********************
+Masking formatoptions
+*********************
++-------------+-------------+-------------+-------------+
+| mask        | maskbetween | maskgeq     | maskgreater |
++-------------+-------------+-------------+-------------+
+| maskleq     | maskless    |             |             |
++-------------+-------------+-------------+-------------+
+
+******************
+Plot formatoptions
+******************
++------+
+| plot |
++------+
+
+*****************************
+Post processing formatoptions
+*****************************
++-------------+-------------+
+| post        | post_timing |
++-------------+-------------+
+
+In [22]: psy.plot.mapplot.summaries(['title', 'cbar'])  # to see the fmt summaries
+title
+    Show the title
+cbar
+    Specify the position of the colorbars
+
+In [23]: psy.plot.mapplot.docs('title')  # to see the full fmt docs
+title
+=====
+Show the title
+
+Set the title of the plot.
+You can insert any meta key from the :attr:`xarray.DataArray.attrs` via a
+string like ``'%(key)s'``. Furthermore there are some special cases:
+
+- Strings like ``'%Y'``, ``'%b'``, etc. will be replaced using the
+  :meth:`datetime.datetime.strftime` method as long as the data has a time
+  coordinate and this can be converted to a :class:`~datetime.datetime`
+  object.
+- ``'%(x)s'``, ``'%(y)s'``, ``'%(z)s'``, ``'%(t)s'`` will be replaced
+  by the value of the x-, y-, z- or time coordinate (as long as this
+  coordinate is one-dimensional in the data)
+- any attribute of one of the above coordinates is inserted via
+  ``axis + key`` (e.g. the name of the x-coordinate can be inserted via
+  ``'%(xname)s'``).
+- Labels defined in the :class:`psyplot.rcParams` ``'texts.labels'`` key
+  are also replaced when enclosed by '{}'. The standard labels are
+
+  - tinfo: ``%H:%M``
+  - dtinfo: ``%B %d, %Y. %H:%M``
+  - dinfo: ``%B %d, %Y``
+  - desc: ``%(long_name)s [%(units)s]``
+  - sdesc: ``%(name)s [%(units)s]``
+
+Possible types
+--------------
+str
+    The title for the :func:`~matplotlib.pyplot.title` function.
+
+Notes
+-----
+This is the title of this specific subplot! For the title of the whole
+figure, see the :attr:`figtitle` formatoption.
+
+See Also
+--------
+figtitle, titlesize, titleweight, titleprops
+
+
+

But of course you can also use the +online documentation of the +method your interested in.

+

To include a formatoption from the beginning, you can simply pass in the key +and the desired value as keyword argument, e.g.

+
In [24]: psy.plot.mapplot('demo.nc', name='t2m', title='my title',
+   ....:                  cbar='r')
+   ....: 
+Out[24]: psyplot.project.Project([    arr0: 2-dim DataArray of t2m, with (lat, lon)=(96, 192), lev=1e+05, time=1979-01-31T18:00:00])
+
+
+

This works generally well as long as there are no dimensions in the desired +data with the same name as one of the passed in formatoptions. If you want to +be really sure, use the fmt keyword via

+
In [25]: psy.plot.mapplot('demo.nc', name='t2m', fmt={'title': 'my title',
+   ....:                                              'cbar': 'r'})
+   ....: 
+Out[25]: psyplot.project.Project([    arr0: 2-dim DataArray of t2m, with (lat, lon)=(96, 192), lev=1e+05, time=1979-01-31T18:00:00])
+
+
+

The same methodology works for the interactive usage, i.e. you can use

+
In [26]: p.update(title='my title', cbar='r')
+
+# or
+In [27]: p.update(fmt={'title': 'my title', 'cbar': 'r'})
+
+
+
+
+

Controlling the update

+
+

Automatic update

+

By default, a call of the update() method +forces an automatic update and redrawing of all the plots. There are +however several ways to modify this behavior:

+
    +
  1. Changing the behavior of one single project

    +
      +
    1. in the initialization of a project using the auto_update keyword

      +
      In [28]: p = psy.plot.mapplot('demo.nc', name='t2m', auto_update=False)
      +
      +
      +
    2. +
    3. setting the no_auto_update attribute

      +
      In [29]: p.no_auto_update = True
      +
      +
      +
    4. +
    +
  2. +
  3. Changing the default configuration in the 'lists.auto_update' +key in the rcParams

    +
    +
    In [30]: psy.rcParams['lists.auto_update'] = False
    +
    +
    +
    +
  4. +
  5. Using the no_auto_update attribute as a +context manager

    +
    In [31]: with p.no_auto_update:
    +   ....:    p.update(title='test')
    +   ....: 
    +
    +
    +
  6. +
+

If you disabled the automatical update via one of the above methods, you have +to start the registered updates manually via

+
In [32]: p.update(auto_update=True)
+
+# or
+In [33]: p.start_update()
+
+
+
+
+

Direct control on formatoption update

+

By default, when updating a formatoption, it is checked for each plot whether +the formatoption would change during the update or not. If not, the +formatoption is not updated. However, sometimes you may want to do that and +for this, you can use the force keyword in the +update() method.

+
+
+
+

Creating and managing multiple plots

+
+

Creating multiple plots

+

One major advantage of the psyplot framework is the systematic management of +multiple plots at the same time. To create multiple plots, simply pass in a +list of dimension values and/or names. For example

+
In [34]: psy.plot.mapplot('demo.nc', name='t2m', time=[0, 1])
+Out[34]: 
+psyplot.project.Project([
+    arr0: 2-dim DataArray of t2m, with (lat, lon)=(96, 192), lev=1e+05, time=1979-01-31T18:00:00,
+    arr1: 2-dim DataArray of t2m, with (lat, lon)=(96, 192), lev=1e+05, time=1979-02-28T18:00:00])
+
+
+

created two plots: one for the first and one for the second time step.

+

Furthermore

+
In [35]: psy.plot.mapplot('demo.nc', name=['t2m', 'u'], time=[0, 1])
+Out[35]: 
+psyplot.project.Project([
+    arr0: 2-dim DataArray of t2m, with (lat, lon)=(96, 192), lev=1e+05, time=1979-01-31T18:00:00,
+    arr1: 2-dim DataArray of t2m, with (lat, lon)=(96, 192), lev=1e+05, time=1979-02-28T18:00:00,
+    arr2: 2-dim DataArray of u, with (lat, lon)=(96, 192), lev=1e+05, time=1979-01-31T18:00:00,
+    arr3: 2-dim DataArray of u, with (lat, lon)=(96, 192), lev=1e+05, time=1979-02-28T18:00:00])
+
+
+

created four plots. By default, each plot is made in an own figure but you can +also use the ax keyword to setup how the plots will be arranged. The sort +keyword allows you to sort the plots.

+

As an example we plot the variables 't2m' and 'u' for the first and +second time step into one figure and sort by time. This will produce

+
In [36]: psy.plot.mapplot(
+   ....:     'demo.nc', name=['t2m', 'u'], time=[0, 1], ax=(2, 2), sort=['time'],
+   ....:     title='%(long_name)s, %b')
+   ....: 
+Out[36]: 
+psyplot.project.Project([
+    arr0: 2-dim DataArray of t2m, with (lat, lon)=(96, 192), lev=1e+05, time=1979-01-31T18:00:00,
+    arr1: 2-dim DataArray of u, with (lat, lon)=(96, 192), lev=1e+05, time=1979-01-31T18:00:00,
+    arr2: 2-dim DataArray of t2m, with (lat, lon)=(96, 192), lev=1e+05, time=1979-02-28T18:00:00,
+    arr3: 2-dim DataArray of u, with (lat, lon)=(96, 192), lev=1e+05, time=1979-02-28T18:00:00])
+
+
+_images/docs_multiple_plots.png +
+

Warning

+

As the xarray package, the slicing is based upon positional indexing with +lists (see the xarray documentation on positional indexing). +Hence you might think of choosing your data slice via +psy.plot.mapplot(..., x=[1, 2, 3, 4, 5], ...). However this would result +in 5 different plots! Instead you have to write +psy.plot.mapplot(..., x=[[1, 2, 3, 4, 5]], ...). The same is true +for plotting methods like the +mapvector method. Since this +method needs two variables (one for the latitudinal and one for the +longitudinal direction), typing

+
In [37]:  psy.plot.mapvector('demo.nc', name=['u', 'v'])
+ValueError: Can only plot 3-dimensional data!
+
+
+

results in a ValueError. Instead you have to write

+
In [38]: psy.plot.mapvector('demo.nc', name=[['u', 'v']])
+Out[38]: psyplot.project.Project([    arr0: 3-dim DataArray of u, v, with (variable, lat, lon)=(2, 96, 192), lev=1e+05, time=1979-01-31T18:00:00])
+
+
+

Please have a look into the documentations of the +mapvector and +mapcombined for getting examples +on how to use this methods.

+
+
+
+

Slicing and filtering the project

+

Managing a whole lot of plots is basically the same as managing a single plot. +However, you can always get the single array and handle it separately.

+

You can either get it through the usual list slicing (the Project class +actually is a simple list subclass) or you can use meta attributes, +dimensions and the specific arr_name +attribute. For the latter one, just call the project with your filtering +attributes

+

This behavior is especially useful if you want to address only some arrays +with your update. For example, let’s consider we want to choose a 'winter' +colormap for the zonal wind variable and a colormap ranging from blue to red +for the temperature. Then we could do this via

+
In [39]: p(name='t2m').update(cmap='RdBu_r')
+
+In [40]: p(name='u').update(cmap='winter')
+
+
+
+

Note

+

When doing so, we recommend to temporarily disable the automatic update +because then the figure will only be drawn once and the update will be +done in parallel.

+

Hence, it is better to use the context manager +no_auto_update (see Automatic update)

+
In [41]: with p.no_auto_update:
+   ....:     p(name='t2m').update(cmap='RdBu_r')
+   ....:     p(name='u').update(cmap='winter')
+   ....:     p.start_update()
+   ....: 
+
+
+
+

Finally you can access the plots created by a specific plotting method +through the corresponding attribute in the Project +class. In this case this is of course useless because all plots in maps +were created by the same plotting method, but it may be helpful when having +different plotters in one project (see The psyplot framework). Anyway, the plots +created by the mapplot method could be +accessed via

+
In [42]: p.mapplot
+Out[42]: 
+psyplot.project.Project([
+])
+
+
+
+
+
+

Saving and loading your project

+

Within the psyplot framework, you can also save and restore your plots easily +and flexibel.

+

To save your project, use the save_project() +method:

+
In [43]: p.save_project('my_project.pkl')
+
+
+

This saves the plot-settings into the file 'my_project.pkl', a simple pickle +file that you could open by yourself using

+
In [44]: import pickle
+
+In [45]: with open('my_project.pkl', 'rb') as f:
+   ....:     d = pickle.load(f)
+   ....: 
+
+In [46]: import os
+   ....: os.remove('my_project.pkl')
+   ....: 
+
+
+

In order to not avoid large project files, we do not store the data but only the +filenames of the datasets. Hence, if you want to load the project again, make +sure that the datasets are accessible through the path as they are listed in the +dsnames attribute.

+

Otherwise you have several options to avoid wrong paths:

+
    +
  1. Use the alternative_paths parameter and provide for each filename a +specific path when you save the project

    +
    In [47]: p.dsnames
    +Out[47]: {'demo.nc'}
    +
    +In [48]: p.save_project(
    +   ....:     'test.pkl', alternative_paths={'demo.nc': 'other_path.nc'})
    +   ....: 
    +
    +
    +
  2. +
  3. pack the whole data to the place where you want to store the project file

    +
    In [49]: p.save_project('target-folder/test.pkl', pack=True)
    +
    +
    +
  4. +
  5. specify where the datasets can be found when you load the project:

    +
    In [50]: p = psy.Project.load_project(
    +   ....:     'test.pkl', alternative_paths={'demo.nc': 'other_path.nc'})
    +   ....: 
    +
    +
    +
  6. +
  7. Save the data in the pickle file, too

    +
    +
    In [51]: p.save_project('test.pkl', ds_description={'arr'})
    +
    +
    +
    +
  8. +
+

To restore your project, simply use the +load_project() method via

+
In [52]: maps = psy.Project.load_project('test.pkl')
+
+
+
+

Note

+

Saving a project stores the figure informations like axes positions, +background colors, etc. However only the axes informations from from the +axes within the project are stored. Other axes in the matplotlib figures are +not considered and will not be restored. You can, however, use the +alternative_axes keyword in the Project.load_project() method if +you want to restore your settings and/or customize your plot with the +post formatoption (see +Adding your own script: The post formatoption)

+
+
+
+

Using presets

+

You can save and load presets to reuse the formatoption settings. For instance, +let’s say temperature should always use a 'Reds' cmap, the colorbar label +should show the long name and the title should be 'time'. This is of course +possible via

+
In [53]: sp = psy.plot.mapplot(
+   ....:     'demo.nc', name='t2m', cmap="Reds", clabel="%(long_name)s",
+   ....:     title='%(time)s')
+   ....: 
+
+
+_images/docs_presets_1.png +

But instead of writing this all the time, you can also save it as a preset

+
In [54]: sp.save_preset("t2m-preset")
+
+
+

and reload this preset either via the preset keyword

+
In [55]: sp = psy.plot.mapplot('demo.nc', name='t2m', preset='t2m-preset')
+
+
+_images/docs_presets_2.png +

or the load_preset() method

+
In [56]: sp.load_preset('t2m-preset')
+
+
+

You can list the available presets from the command line

+
In [57]: !psyplot --list-presets
+t2m-preset: /root/.config/psyplot/presets/t2m-preset.yml
+
+
+
+
+

Adding your own script: The post formatoption

+

Very likely, you will face the problem that not all your needs are satisfied +by the formatoptions in one plotter. You then have two choices:

+
    +
  1. define your own plotter with new formatoptions (see How to implement your own plotters and plugins)

    +
    +
    Pros
      +
    • more structured approach

    • +
    • you can enhance the plotter with other formatoptions afterwards and +reuse it

    • +
    +
    +
    Cons
      +
    • more complicated

    • +
    • you always have to ship the module where you define your plotter when +you want to save and load your project

    • +
    • can get messy if you define a lot of different plotters

    • +
    +
    +
    +
  2. +
  3. use the post formatoption

    +
    +
    Pros
    +
    +
    Cons
      +
    • may get complicated for large scripts

    • +
    • has to be enabled manually by the user

    • +
    +
    +
    +
  4. +
+

For most of the cases, the post formatoptions +is probably what you are looking for (the first option is described in our +developers guide).

+

This formatoption is designed for applying your own postprocessing script to +your plot. It accepts a string that is executed using the built-in exec() +function and is executed at the very end of the plotting. In this python +script, the formatoption itself (and therefore the +plotter and +axes can be accessed inside the +script through the self variable. An example how to handle this +formatoption can be found in +our example gallery.

+
+
+ + +
+
+ + + +
+
+
+
+ + + + \ No newline at end of file diff --git a/icon/CreateICNS.sh b/icon/CreateICNS.sh deleted file mode 100644 index cac708a..0000000 --- a/icon/CreateICNS.sh +++ /dev/null @@ -1,22 +0,0 @@ -# Create the iconset file for the psyplot icon. - -# SPDX-FileCopyrightText: 2016-2024 University of Lausanne -# SPDX-FileCopyrightText: 2020-2021 Helmholtz-Zentrum Geesthacht - -# SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH -# -# SPDX-License-Identifier: CC-BY-4.0 - -mkdir main.iconset -sips -z 16 16 icon1024.png --out main.iconset/icon_16x16.png -sips -z 32 32 icon1024.png --out main.iconset/icon_16x16@2x.png -sips -z 32 32 icon1024.png --out main.iconset/icon_32x32.png -sips -z 64 64 icon1024.png --out main.iconset/icon_32x32@2x.png -sips -z 128 128 icon1024.png --out main.iconset/icon_128x128.png -sips -z 256 256 icon1024.png --out main.iconset/icon_128x128@2x.png -sips -z 256 256 icon1024.png --out main.iconset/icon_256x256.png -sips -z 512 512 icon1024.png --out main.iconset/icon_256x256@2x.png -sips -z 512 512 icon1024.png --out main.iconset/icon_512x512.png -cp icon1024.png main.iconset/icon_512x512@2x.png -iconutil -c icns main.iconset -rm -R main.iconset diff --git a/icon/CreateICO.sh b/icon/CreateICO.sh deleted file mode 100644 index 06f45c2..0000000 --- a/icon/CreateICO.sh +++ /dev/null @@ -1,10 +0,0 @@ -# Create the psyplot.ico file for the psyplot icon - -# SPDX-FileCopyrightText: 2016-2024 University of Lausanne -# SPDX-FileCopyrightText: 2020-2021 Helmholtz-Zentrum Geesthacht - -# SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH -# -# SPDX-License-Identifier: CC-BY-4.0 - -convert icon1024.png -define icon:auto-resize=64,48,32,16 psyplot.ico diff --git a/icon/icon.py b/icon/icon.py deleted file mode 100644 index 99fdc8c..0000000 --- a/icon/icon.py +++ /dev/null @@ -1,45 +0,0 @@ -"""Create the psyplot icon. - -This script creates the psyplot icon with a dpi of 128 and a width and height -of 8 inches. The file is saved it to ``'icon1024.pkl'``""" - -# SPDX-FileCopyrightText: 2016-2024 University of Lausanne -# SPDX-FileCopyrightText: 2020-2021 Helmholtz-Zentrum Geesthacht - -# SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH -# -# SPDX-License-Identifier: LGPL-3.0-only - -import cartopy.crs as ccrs -import cartopy.feature as cf -import matplotlib.pyplot as plt -from matplotlib.text import FontProperties - -# The path to the font -fontpath = "/Library/Fonts/FreeSansBoldOblique.ttf" - -fig = plt.figure(figsize=(8, 8), dpi=128) - -ax = fig.add_axes( - [0.0, 0.0, 1.0, 1.0], projection=ccrs.Orthographic(central_latitude=5) -) - -land = ax.add_feature(cf.LAND, facecolor="0.975") -ocean = ax.add_feature(cf.OCEAN, facecolor=plt.get_cmap("Blues")(0.5)) - -text = ax.text( - 0.47, - 0.5, - "Psy", - transform=fig.transFigure, - name="FreeSans", - fontproperties=FontProperties(fname=fontpath), - size=256, - ha="center", - va="center", - weight=400, -) - -ax.outline_patch.set_edgecolor("none") - -plt.savefig("icon1024.png", transparent=True) diff --git a/icon/icon1024.png.license b/icon/icon1024.png.license deleted file mode 100644 index b21fae9..0000000 --- a/icon/icon1024.png.license +++ /dev/null @@ -1,3 +0,0 @@ -SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH - -SPDX-License-Identifier: CC-BY-4.0 diff --git a/index.html b/index.html new file mode 100644 index 0000000..bef0754 --- /dev/null +++ b/index.html @@ -0,0 +1,635 @@ + + + + + + + Interactive data visualization with python — psyplot documentation + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +
+

Interactive data visualization with python

+

CI +Code coverage +Latest Release +PyPI version +Code style: black +Imports: isort +PEP8 +|REUSE status|

+psyplot logo +

Welcome! Looking for a fast and flexible visualization software? Here we +present psyplot, an open source python project that mainly combines the +plotting utilities of matplotlib and the data management of the xarray +package and integrates them into a software that can be used via command-line +and via a GUI!

+

The main purpose is to have a framework that allows a fast, attractive, +flexible, easily applicable, easily reproducible and especially an interactive +visualization of your data.

+

The ultimate goal is to help scientists in their daily work by providing a +flexible visualization tool that can be enhanced by their own visualization +scripts. psyplot can be used via command line and with the +graphical user interface (GUI) from the +psyplot-gui and +psy-view module.

+

If you want more motivation: Have a look into the About psyplot section.

+
+

Documentation

+ +
+
+

Get in touch

+

Any quesions? Do not hessitate to get in touch with the psyplot developers.

+ +

See also the code of conduct, and our +contribution guide for more information and a guide +about good bug reports.

+
+
+

How to cite this software

+
+
+
+Please do cite this software!
+
+ +
+

Sommer P.S. psyplot: Interactive data visualization with Python DOI: 10.5281/zenodo.593798 URL: https://codebase.helmholtz.cloud/psyplot/psyplot
+
+
+

+
+ +
+

@misc{YourReferenceHere,
+author = {Sommer, Philipp S.},
+doi = {10.5281/zenodo.593798},
+title = {psyplot: Interactive data visualization with Python},
+url = {https://codebase.helmholtz.cloud/psyplot/psyplot}
+}
+
+
+

+
+ +
+

TY  - GEN
+AU  - Sommer, Philipp S.
+DO  - 10.5281/zenodo.593798
+KW  - psyplot
+KW  - python
+KW  - visualization
+KW  - xarray
+KW  - matplotlib
+KW  - netcdf4
+KW  - interactive
+KW  - climate models
+KW  - unstructured
+TI  - psyplot: Interactive data visualization with Python
+UR  - https://codebase.helmholtz.cloud/psyplot/psyplot
+ER
+
+
+

+
+ +
+

%0 Generic
+%A Sommer, Philipp S.
+%K psyplot
+%K python
+%K visualization
+%K xarray
+%K matplotlib
+%K netcdf4
+%K interactive
+%K climate models
+%K unstructured
+%R 10.5281/zenodo.593798
+%T psyplot: Interactive data visualization with Python
+%U https://codebase.helmholtz.cloud/psyplot/psyplot
+
+
+

+
+ +
+

# SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH
+#
+# SPDX-License-Identifier: CC0-1.0
+
+# YAML 1.2
+---
+cff-version: "1.2.0"
+message: "If you use this software, please cite both the article from preferred-citation and the software itself."
+title: "psyplot: Interactive data visualization with Python"
+authors:
+  - family-names: Sommer
+    given-names: "Philipp S."
+    affiliation: "Helmholtz-Zentrum Hereon"
+    orcid: "https://orcid.org/0000-0001-6171-7716"
+    website: "https://www.philipp-s-sommer.de"
+    post-code: 21502
+    city: Geesthacht
+    country: DE
+    email: philipp.sommer@hereon.de
+doi: "10.5281/zenodo.593798"
+contact:
+  - email: psyplot@hereon.de
+    name: "Psyplot developers at hereon"
+license: "LGPL-3.0-only"
+repository-code: https://codebase.helmholtz.cloud/psyplot/psyplot
+type: software
+keywords:
+  - psyplot
+  - python
+  - visualization
+  - xarray
+  - matplotlib
+  - netcdf4
+  - interactive
+  - climate models
+  - unstructured
+preferred-citation:
+  title: "The psyplot interactive visualization framework"
+  authors:
+    - family-names: Sommer
+      given-names: "Philipp S."
+      affiliation: "Helmholtz-Zentrum Hereon"
+      orcid: "https://orcid.org/0000-0001-6171-7716"
+  year: 2017
+  type: article
+  doi: "10.21105/joss.00363"
+  date-published: 2017-08-22
+  journal: Journal of Open Source Software
+  volume: 2
+  number: 16
+  pages: 363
+  publisher:
+    name: The Open Journal
+  license: CC-BY-4.0
+...
+
+
+

+
+
+
+
+

Furthermore, each release of psyplot and it’s subprojects is +associated with a DOI on zenodo. If you want to cite a specific +version or plugin, please refer to the releases page of psyplot or the +releases page of the corresponding subproject.

+
+
+

Acknowledgment

+

This package is being developed by Philipp S. Sommer at the +Helmholtz Coastal Data Center (HCDC) of the Helmholtz-Zentrum Hereon.

+

I want to thank the matplotlib, xarray and cartopy developers +for their great packages and of course the python developers for their +fascinating work on this beautiful language.

+

A special thanks to Stefan Hagemann and Tobias Stacke from the +Max-Planck-Institute of Meteorology in Hamburg, Germany for the motivation on +this project and to the people of the Not yet visible agency for their +advice in designing the logo and webpage.

+

Finally the author thanks the Swiss National Science Foundation (SNF) for their +support. Funding for the author came from the ACACIA grant (CR10I2_146314) +and the HORNET grant (200021_169598).

+
+
+
+

Indices and tables

+ +
+ + +
+
+ + + +
+
+
+
+ + + + \ No newline at end of file diff --git a/installing.html b/installing.html new file mode 100644 index 0000000..36231b2 --- /dev/null +++ b/installing.html @@ -0,0 +1,586 @@ + + + + + + + Installation — psyplot documentation + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +
+

Installation

+
+

How to install

+

There basically three different methodologies for the installation. You should +choose the one, which is the most appropriate solution concerning your skills +and your usage:

+
+
The recommended installation

We recommend to use anaconda for installing python and psyplot (see +Installation using conda). If you however already have python installed on +your system, you can also use pip (see Installation using pip).

+
+
The developer installation

Install it from source (see Installation from source)

+
+
+
+

Installation using conda

+

We highly recommend to use conda for installing psyplot. After having +downloaded the miniconda installer, you can install psyplot +and the optional plugins (see Optional dependencies) via:

+
$ conda install -c conda-forge psy-view psy-reg
+
+
+

If you only want to install the core, i.e. the raw framework, run:

+
$ conda install -c conda-forge psyplot
+
+
+

If you want to be able to read GeoTiff Raster files, you will need to have +gdal installed:

+
$ conda install gdal
+
+
+

Please also visit the xarray installation notes +for more informations on how to best configure the xarray +package for your needs.

+
+
+

Installation using pip

+

If you do not want to use conda for managing your python packages, you can also +use the python package manager pip and install via:

+
$ pip install psyplot
+
+
+

However to be on the safe side, make sure you have the Dependencies +installed.

+
+
+

Installation from source

+

To install it from source, make sure you have the Dependencies +installed, clone the github repository via:

+
git clone https://codebase.helmholtz.cloud/psyplot/psyplot.git
+
+
+

and install it via:

+
python setup.py install
+
+
+
+
+
+

Dependencies

+
+

Required dependencies

+

Psyplot supports officially python 3.6 and 3.7. Previous versions are also +available for python 2.7. Furthermore the package is built upon multiple other +packages, mainly

+
    +
  • xarray>=0.8: Is used for the data management in the psyplot package

  • +
  • matplotlib>=1.4.3<3.1: The python visualiation package

  • +
  • PyYAML: Needed for the configuration of psyplot

  • +
  • docrep: A package for efficient documentation processing for large APIs

  • +
  • funcargparse: A package to create command line parsers from function +docstrings

  • +
+
+
+

Optional dependencies

+

We furthermore recommend to use

+
    +
  • psyplot-gui: A graphical user interface to psyplot

  • +
  • psy-view: An ncview-like interface based on psyplot +and psy-maps

  • +
  • psy-simple: A psyplot plugin to make simple plots

  • +
  • psy-maps: A psyplot plugin for visualizing data on a +map

  • +
  • psy-reg: A psyplot plugin for visualizing fits to +your data

  • +
+
+
+
+

Running the tests

+

We us pytest to run our tests. So you can either run clone out the github +repository and run:

+
$ python setup.py test
+
+
+

or install pytest by yourself and run:

+
$ py.test
+
+
+

To also test the plugin functionality, install the psyplot_test module in +tests/test_plugin via:

+
$ cd tests/test_plugin && python setup.py install
+
+
+

and run the tests via one of the above mentioned commands.

+
+
+

Building the docs

+

To build the docs, check out the github repository and install the +requirements in 'docs/environment.yml'. The easiest way to do this is via +anaconda by typing:

+
$ conda env create -f docs/environment.yml
+$ source activate psyplot_docs
+
+
+

Then build the docs via:

+
$ cd docs
+$ make html
+
+
+
+

Note

+

The building of the docs always preprocesses the examples. You might want to +disable that by setting process_examples = False. Otherwise please note +that the examples are written as python3 notebooks, hence you may have to +install a python3 kernel through ipykernel. Just create a new environment +'py37' and install it via:

+
conda create -n py37 python=3.7
+source activate py37
+conda install notebook ipykernel
+ipython kernel install --user
+
+
+

You then have to install the necessary modules for each of the examples in +the new 'py37' environment.

+
+
+
+

Uninstallation

+

The uninstallation depends on the system you used to install psyplot. Either +you did it via conda (see +Uninstallation via conda), via pip or from the +source files (see Uninstallation via pip).

+

Anyway, if you may want to remove the psyplot configuration files. If you did +not specify anything else (see psyplot.config.rcsetup.psyplot_fname()), +the configuration files for psyplot are located in the user home directory. +Under linux and OSX, this is $HOME/.config/psyplot. On other platforms it +is in the .psyplot directory in the user home.

+
+

Uninstallation via conda

+

If you installed psyplot via conda, simply run:

+
conda remove psyplot
+
+
+

or, if you installed it into an own conda environment, remove the environment +via:

+
conda env remove -n <environment-name>
+
+
+
+
+

Uninstallation via pip

+

Uninstalling via pip simply goes via:

+
pip uninstall psyplot
+
+
+

Note, however, that you should use conda if you also +installed it via conda.

+
+
+
+ + +
+
+ + + +
+
+
+
+ + + + \ No newline at end of file diff --git a/objects.inv b/objects.inv new file mode 100644 index 0000000..44a523b Binary files /dev/null and b/objects.inv differ diff --git a/paper.bib b/paper.bib deleted file mode 100644 index 0c7a5d1..0000000 --- a/paper.bib +++ /dev/null @@ -1,71 +0,0 @@ -@Comment{ -SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH - -SPDX-License-Identifier: CC-BY-4.0 -} - -@article{Hunter2007, - author={J. D. Hunter}, - journal={Computing in Science Engineering}, - title={Matplotlib: A 2D Graphics Environment}, - year={2007}, - volume={9}, - number={3}, - pages={90--95}, - keywords={computer graphics;mathematics computing;object-oriented programming;software packages;2D graphics package;Matplotlib;Python;application development;interactive scripting;operating system;publication-quality image generation;user interface;Computer languages;Equations;Graphical user interfaces;Graphics;Image generation;Interpolation;Operating systems;Packaging;Programming profession;User interfaces;Python;application development;scientific programming;scripting languages}, - doi={10.1109/MCSE.2007.55}, - ISSN={1521-9615}, - month={May} -} - -@online{psyplot, - author = {Philipp S Sommer}, - title = {The psyplot interactive visualization framework}, - year = 2017, - url = {https://github.com/Chilipp/psyplot}, - urldate = {2017-07-28}, -} - -@online{psy-simple, - author = {Philipp S Sommer}, - title = {psy-simple: The psyplot plugin for simple visualizations}, - year = 2017, - url = {https://github.com/Chilipp/psy-simple}, - urldate = {2017-07-28}, -} - -@online{psy-maps, - author = {Philipp S Sommer}, - title = {psy-maps: The psyplot plugin for visualizations on a map}, - year = 2017, - url = {https://github.com/Chilipp/psy-maps}, - urldate = {2017-07-28}, -} - -@online{psy-reg, - author = {Philipp S Sommer}, - title = {psy-reg: Psyplot plugin for visualizing and calculating regression plot}, - year = 2017, - url = {https://github.com/Chilipp/psy-reg}, - urldate = {2017-07-28}, -} - -@online{psyplot-gui, - author = {Philipp S Sommer}, - title = {Graphical User Interface for the psyplot package}, - year = 2017, - url = {https://github.com/Chilipp/psyplot-gui}, - urldate = {2017-07-28}, -} - -@article{hoyer2017xarray, - title = {xarray: {N-D} labeled arrays and datasets in {Python}}, - author = {Hoyer, S. and J. Hamman}, - journal = {Journal of Open Research Software}, - volume = {5}, - number = {1}, - year = {2017}, - publisher = {Ubiquity Press}, - doi = {10.5334/jors.148}, - url = {http://doi.org/10.5334/jors.148} -} diff --git a/paper.md b/paper.md deleted file mode 100644 index 88375c2..0000000 --- a/paper.md +++ /dev/null @@ -1,50 +0,0 @@ - - ---- -title: 'The psyplot interactive visualization framework' -tags: - - visualization - - netcdf - - raster - - cartopy - - earth science - - climate - - matplotlib - - python -authors: - - name: Philipp S Sommer - orcid: 0000-0001-6171-7716 - affiliation: 1 -affiliations: - - name: Institute of Earth Surface Dynamics, University of Lausanne, Géopolis, 1015 Lausanne, Switzerland - index: 1 -date: 28 July 2017 -bibliography: paper.bib ---- - -# Summary - -psyplot [@psyplot] is an cross-platform open source python project that mainly -combines the plotting utilities of matplotlib [@Hunter2007] and the data -management of the xarray [@hoyer2017xarray] package and integrates them into a -software that can be used via command-line and via a GUI. - -The main purpose is to have a framework that allows a fast, attractive, -flexible, easily applicable, easily reproducible and especially an interactive -visualization of data. - -The ultimate goal is to help scientists in their daily work by providing a -flexible visualization tool that can be enhanced by their own visualization -scripts. - -The framework is extended by multiple plugins: psy-simple [@psy-simple] for -simple visualization tasks, psy-maps [@psy-maps] for georeferenced data -visualization and psy-reg [@psy-reg] for the visualization of fits. It is -furthermore extended by the optional graphical user interface psyplot-gui -[@psyplot-gui]. - -# References diff --git a/plugins.html b/plugins.html new file mode 100644 index 0000000..52c446f --- /dev/null +++ b/plugins.html @@ -0,0 +1,559 @@ + + + + + + + Psyplot plugins — psyplot documentation + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +
+

Psyplot plugins

+

psyplot only provides the abstract framework on how to make the interactive +visualization and data analysis. The real work is implemented in +plugins to this framework. Each +plugin is a separate package that has to be installed independent of psyplot and +each plugin registers new plot methods for psyplot.project.plot.

+
+

Existing plugins

+
+
psy_simple.plugin

A psyplot plugin for simple visualization tasks. This plugin provides a +bases for all the other plugins +- plot methods

+
+
+
psyplot.project.plot.density

Make a density plot of point data

+
+
psyplot.project.plot.plot2d

Make a simple plot of a 2D scalar field

+
+
psyplot.project.plot.combined

Plot a 2D scalar field with an overlying vector field

+
+
psyplot.project.plot.violinplot

Make a violin plot of your data

+
+
psyplot.project.plot.lineplot

Make a line plot of one-dimensional data

+
+
psyplot.project.plot.vector

Make a simple plot of a 2D vector field

+
+
psyplot.project.plot.barplot

Make a bar plot of one-dimensional data

+
+
+
+
+
psy_maps.plugin

A psyplot plugin for visualizing data on a map

+ +
+
psy_reg.plugin

A psyplot plugin for visualizing and calculating regression fits

+ +
+
+

If you have new plugins that you think should be included in this list, please +do not hesitate to open an issue on the github project page of psyplot or +implement it by yourself in this file and make a pull request.

+
+

Note

+

Because psyplot plugins are imported right at the startup time of psyplot +but nevertheless use the psyplot.config.rcsetup.RcParams class, +you always have to import psyplot first if you want to load a psyplot +plugin. In other words, if you want to import one of the above mentiond +modules manually, you always have to type

+
import psyplot
+import PLUGIN_NAME.plugin
+
+
+

instead of

+
import PLUGIN_NAME.plugin
+import psyplot
+
+
+

where PLUGIN_NAME is any of psy_simple, psy_maps, etc.

+
+
+
+

How to exclude plugins

+

The psyplot package loads all plugins right when the psyplot is imported. In +other words, the statement

+
import psyplot
+
+
+

already includes that all the psyplot plugin packages are loaded.

+

You can however exclude plugins from the automatic loading via the +PSYPLOT_PLUGINS environment variable and exclude specific plot methods of a +plugin via the PSYPLOT_PLOTMETHODS variable.

+
+

The PSYPLOT_PLUGINS environment variable

+

This environment variable is a :: separated string with plugin names. If a +plugin name is preceded by a no:, this plugin is excluded. Otherwise, only +this plugin is included.

+

To show this behaviour, we can use psyplot --list-plugins which shows the +plugins that are used. By default, all plugins are included

+
In [1]: !psyplot --list-plugins
+- EntryPoint(name='plugin', value='psy_simple.plugin', group='psyplot')
+- EntryPoint(name='plugin', value='psy_maps.plugin', group='psyplot')
+
+
+

Excluding psy-maps works via

+
In [2]: !PSYPLOT_PLUGINS=no:psy_maps.plugin psyplot --list-plugins
+- EntryPoint(name='plugin', value='psy_simple.plugin', group='psyplot')
+
+
+

Including only psy-maps works via

+
In [3]: !PSYPLOT_PLUGINS='yes:psy_maps.plugin' psyplot --list-plugins
+- EntryPoint(name='plugin', value='psy_maps.plugin', group='psyplot')
+
+
+
+
+

The PSYPLOT_PLOTMETHODS environment variable

+

The same principle is used when the plot methods are loaded from the plugins. +If you want to manually exclude a plot method from loading, you include it via +no:<plugin-module>:<plotmethod>. For example, to exclude the +:attr:mapplot <psy_maps:psyplot.project.plot.mapplot> plot method from the +psy-maps plugin, you can use

+
In [4]: !PSYPLOT_PLOTMETHODS=no:psy_maps.plugin:mapplot psyplot --list-plot-methods
+barplot: Make a bar plot of one-dimensional data
+combined: Plot a 2D scalar field with an overlying vector field
+density: Make a density plot of point data
+fldmean: Calculate and plot the mean over x- and y-dimensions
+lineplot: Make a line plot of one-dimensional data
+mapcombined: Plot a 2D scalar field with an overlying vector field on a map
+mapvector: Plot a 2D vector field on a map
+plot2d: Make a simple plot of a 2D scalar field
+vector: Make a simple plot of a 2D vector field
+violinplot: Make a violin plot of your data
+
+
+

and the same if you only want to include the +:attr:mapplot <psy_maps:psyplot.project.plot.mapplot> and the +:attr:lineplot <psy_simple:psyplot.project.plot.lineplot> methods

+
In [5]: !PSYPLOT_PLOTMETHODS='yes:psy_maps.plugin:mapplot::yes:psy_simple.plugin:lineplot' psyplot --list-plot-methods
+lineplot: Make a line plot of one-dimensional data
+mapplot: Plot a 2D scalar field on a map
+
+
+
+
+
+ + +
+
+ + + +
+
+
+
+ + + + \ No newline at end of file diff --git a/projects.html b/projects.html new file mode 100644 index 0000000..2fbc128 --- /dev/null +++ b/projects.html @@ -0,0 +1,441 @@ + + + + + + + Subprojects — psyplot documentation + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +
+

Subprojects

+

psyplot is only the over-arching framework. It’s capabilities are +splitted into several subprojects. Each of them is accessible via +https://psyplot.github.io/<project-name>

+
    +
  • the psyplot_gui package: The GUI to psyplot +Source +Latest release

  • +
  • the psy_view package: An ncview-like interface for +psyplot +Source +Latest release

  • +
  • the psy-simple package: A plugin for simple +visualization +Source +Latest release

  • +
  • the psy-maps package: A psyplot plugin for +visualizing data on a map +Source +Latest release

  • +
  • the psy-reg package: A psyplot plugin for visualizing +and calculating regression fits +Source +Latest release

  • +
+

See Psyplot plugins for more informations on the plugins.

+
+ + +
+
+ + + +
+
+
+
+ + + + \ No newline at end of file diff --git a/psyplot/__init__.py b/psyplot/__init__.py deleted file mode 100755 index 8c5d556..0000000 --- a/psyplot/__init__.py +++ /dev/null @@ -1,175 +0,0 @@ -"""psyplot visualization framework.""" - -# SPDX-FileCopyrightText: 2016-2024 University of Lausanne -# SPDX-FileCopyrightText: 2020-2021 Helmholtz-Zentrum Geesthacht -# SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH -# -# SPDX-License-Identifier: LGPL-3.0-only - -import datetime as dt -import logging as _logging -import sys - -import psyplot.config as config -from psyplot.config.rcsetup import rcParams -from psyplot.data import ( # noqa: F401 - ArrayList, - InteractiveArray, - InteractiveList, - open_dataset, - open_mfdataset, -) -from psyplot.warning import critical, disable_warnings, warn # noqa: F401 - -from ._version import get_versions - -__version__ = get_versions()["version"] -del get_versions - - -__author__ = "Philipp S. Sommer" -__copyright__ = """ -2016-2024 University of Lausanne -2020-2021 Helmholtz-Zentrum Geesthacht -2021-2024 Helmholtz-Zentrum hereon GmbH -""" -__credits__ = ["Philipp S. Sommer"] -__license__ = "LGPL-3.0-only" - -__maintainer__ = "Philipp S. Sommer" -__email__ = "psyplot@hereon.de" - -__status__ = "Production" - - -logger = _logging.getLogger(__name__) -logger.debug( - "%s: Initializing psyplot, version %s", - dt.datetime.now().isoformat(), - __version__, -) -logger.debug("Logging configuration file: %s", config.logcfg_path) -logger.debug("Configuration file: %s", config.config_path) - - -rcParams.HEADER += "\n\npsyplot version: " + __version__ -rcParams.load_plugins() -rcParams.load_from_file() - - -_project_imported = False - -#: Boolean that is True, if psyplot runs inside the graphical user interface -#: by the ``psyplot_gui`` module -with_gui = False - - -def get_versions(requirements=True, key=None): - """ - Get the version information for psyplot, the plugins and its requirements - - Parameters - ---------- - requirements: bool - If True, the requirements of the plugins and psyplot are investigated - key: func - A function that determines whether a plugin shall be considererd or - not. The function must take a single argument, that is the name of the - plugin as string, and must return True (import the plugin) or False - (skip the plugin). If None, all plugins are imported - - Returns - ------- - dict - A mapping from ``'psyplot'``/the plugin names to a dictionary with the - ``'version'`` key and the corresponding version is returned. If - `requirements` is True, it also contains a mapping from - ``'requirements'`` a dictionary with the versions - - Examples - -------- - Using the built-in JSON module, we get something like - - .. code-block:: python - - import json - - print(json.dumps(psyplot.get_versions(), indent=4)) - { - "psy_simple.plugin": {"version": "1.0.0.dev0"}, - "psyplot": { - "version": "1.0.0.dev0", - "requirements": { - "matplotlib": "1.5.3", - "numpy": "1.11.3", - "pandas": "0.19.2", - "xarray": "0.9.1", - }, - }, - "psy_maps.plugin": { - "version": "1.0.0.dev0", - "requirements": {"cartopy": "0.15.0"}, - }, - } - """ - from psyplot.utils import plugin_entrypoints - - eps = plugin_entrypoints("psyplot", "plugin") - - ret = {"psyplot": _get_versions(requirements)} - for ep in eps: - if str(ep) in rcParams._plugins: - logger.debug("Loading entrypoint %s", ep) - - try: - ep.module - except AttributeError: # python<3.10 - ep.module = ep.pattern.match(ep.value).group("module") - - if key is not None and not key(ep.module): - continue - try: - mod = ep.load() - except (ImportError, ModuleNotFoundError): - logger.debug("Could not import %s" % (ep,), exc_info=True) - logger.warning("Could not import %s" % (ep,), exc_info=True) - else: - try: - ret[str(ep.module)] = mod.get_versions(requirements) - except AttributeError: - ret[str(ep.module)] = { - "version": getattr( - mod, - "plugin_version", - getattr(mod, "__version__", ""), - ) - } - if key is None: - try: - import psyplot_gui - except ImportError: - pass - else: - ret["psyplot_gui"] = psyplot_gui.get_versions(requirements) - return ret - - -def _get_versions(requirements=True): - if requirements: - import matplotlib as mpl - import numpy as np - import pandas as pd - import xarray as xr - - return { - "version": __version__, - "requirements": { - "matplotlib": mpl.__version__, - "xarray": xr.__version__, - "pandas": pd.__version__, - "numpy": np.__version__, - "python": " ".join(sys.version.splitlines()), - }, - } - else: - return {"version": __version__} diff --git a/psyplot/__main__.py b/psyplot/__main__.py deleted file mode 100644 index 425ff8a..0000000 --- a/psyplot/__main__.py +++ /dev/null @@ -1,581 +0,0 @@ -# -*- coding: utf-8 -*- -"""Main commandline entrypoint for psyplot.""" - -# SPDX-FileCopyrightText: 2016-2024 University of Lausanne -# SPDX-FileCopyrightText: 2020-2021 Helmholtz-Zentrum Geesthacht - -# SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH -# -# SPDX-License-Identifier: LGPL-3.0-only - -import argparse -import glob -import logging -import os.path as osp -import pickle -import sys -from collections import defaultdict -from itertools import chain - -import six -import xarray as xr -import yaml -from funcargparse import FuncArgParser - -import psyplot -from psyplot.docstring import docstrings -from psyplot.utils import get_default_value -from psyplot.warning import warn - -rcParams = psyplot.rcParams - - -logger = logging.getLogger(__name__) - - -def main(args=None): - """Main function for usage of psyplot from the command line - - This function creates a parser that parses command lines to the - :func:`make_plot` functions or (if the ``psyplot_gui`` module is - present, to the :func:`psyplot_gui.start_app` function) - - Returns - ------- - psyplot.parser.FuncArgParser - The parser that has been used from the command line""" - try: - from psyplot_gui import get_parser as _get_parser - except (ImportError, ModuleNotFoundError): - logger.debug("Failed to import gui", exc_info=True) - parser = get_parser(create=False) - parser.update_arg("output", required=True) - parser.create_arguments() - parser.parse2func(args) - else: - parser = _get_parser(create=False) - parser.create_arguments() - parser.parse_known2func(args) - - -@docstrings.get_sections(base="make_plot") -@docstrings.dedent -def make_plot( - fnames=[], - name=[], - dims=None, - plot_method=None, - output=None, - project=None, - engine=None, - formatoptions=None, - tight=False, - rc_file=None, - encoding=None, - enable_post=False, - seaborn_style=None, - output_project=None, - concat_dim=get_default_value(xr.open_mfdataset, "concat_dim"), - chname={}, - preset=None, -): - """ - Eventually start the QApplication or only make a plot - - Parameters - ---------- - fnames: list of str - Either the filenames to show, or, if the `project` parameter is set, - the a list of `,`-separated filenames to make a mapping from the - original filename to a new one - name: list of str - The variable names to plot if the `output` parameter is set - dims: dict - A mapping from coordinate names to integers if the `project` is not - given - plot_method: str - The name of the plot_method to use - output: str or list of str - If set, the data is loaded and the figures are saved to the specified - filename and now graphical user interface is shown - project: str - If set, the project located at the given file name is loaded - engine: str - The engine to use for opening the dataset (see - :func:`psyplot.data.open_dataset`) - formatoptions: dict - A dictionary of formatoption that is applied to the data visualized by - the chosen `plot_method` - tight: bool - If True/set, it is tried to figure out the tight bbox of the figure and - adjust the paper size of the `output` to it - rc_file: str - The path to a yaml configuration file that can be used to update the - :attr:`~psyplot.config.rcsetup.rcParams` - encoding: str - The encoding to use for loading the project. If None, it is - automatically determined by pickle. Note: Set this to ``'latin1'`` - if using a project created with python2 on python3. - enable_post: bool - Enable the :attr:`~psyplot.plotter.Plotter.post` processing - formatoption. If True/set, post processing scripts are enabled in the - given `project`. Only set this if you are sure that you can trust the - given project file because it may be a security vulnerability. - seaborn_style: str - The name of the style of the seaborn package that can be used for - the :func:`seaborn.set_style` function - output_project: str - The name of a project file to save the project to - concat_dim: str - The concatenation dimension if multiple files in `fnames` are - provided - chname: dict - A mapping from variable names in the project to variable names in the - datasets that should be used instead - preset: str - The filename or identifier of a preset. If the given `preset` is - the path to an existing yaml file, it will be loaded. Otherwise we - look up the `preset` in the psyplot configuration directory (see - :func:`~psyplot.config.rcsetup.get_configdir`). - """ - if project is not None and (name != [] or dims is not None): - warn( - "The `name` and `dims` parameter are ignored if the `project`" - " parameter is set!" - ) - if rc_file is not None: - rcParams.load_from_file(rc_file) - - if dims is not None and not isinstance(dims, dict): - dims = dict(chain(*map(six.iteritems, dims))) - - if len(output) == 1: - output = output[0] - if not fnames and not project: - raise ValueError( - "Either a filename or a project file must be provided if " - "the output parameter is set!" - ) - elif project is None and plot_method is None: - raise ValueError( - "A plotting method must be provided if the output parameter " - "is set and not the project!" - ) - if seaborn_style is not None: - import seaborn as sns - - sns.set_style(seaborn_style) - import psyplot.project as psy - - if project is not None: - fnames = [s.split(",") for s in fnames] - chname = dict(chname) - single_files = (fn_list[0] for fn_list in fnames if len(fn_list) == 1) - alternative_paths = defaultdict(lambda: next(single_files, None)) - alternative_paths.update( - (fn_list for fn_list in fnames if len(fn_list) == 2) - ) - p = psy.Project.load_project( - project, - alternative_paths=alternative_paths, - engine=engine, - encoding=encoding, - enable_post=enable_post, - chname=chname, - ) - if preset: - p.load_preset(preset) - if formatoptions is not None: - p.update(fmt=formatoptions) - p.export(output, tight=tight) - else: - pm = getattr(psy.plot, plot_method, None) - if pm is None: - raise ValueError("Unknown plot method %s!" % plot_method) - kwargs = {"name": name} if name else {} - p = pm( - fnames, - dims=dims or {}, - engine=engine, - preset=preset, - fmt=formatoptions or {}, - mf_mode=True, - concat_dim=concat_dim, - **kwargs, - ) - p.export(output, tight=tight) - if output_project is not None: - p.save_project(output_project) - return - - -def get_parser(create=True): - """Return a parser to make that can be used to make plots or open files - from the command line - - Returns - ------- - psyplot.parser.FuncArgParser - The :class:`argparse.ArgumentParser` instance""" - #: The parse that is used to parse arguments from the command line - epilog = docstrings.get_sections( - docstrings.dedent( - """ - Examples - -------- - - Here are some examples on how to use psyplot from the command line. - - Plot the variable ``'t2m'`` in a netCDF file ``'myfile.nc'`` and save - the plot to ``'plot.pdf'``:: - - $ psyplot myfile.nc -n t2m -pm mapplot -o test.pdf - - Create two plots for ``'t2m'`` with the first and second timestep on - the second vertical level:: - - $ psyplot myfile.nc -n t2m -pm mapplot -o test.pdf -d t,0,1 z,1 - - If you have save a project using the - :meth:`psyplot.project.Project.save_project` method into a file named - ``'project.pkl'``, you can replot this via:: - - $ psyplot -p project.pkl -o test.pdf - - If you use a different dataset than the one you used in the project - (e.g. ``'other_ds.nc'``), you can replace it via:: - - $ psyplot other_dataset.nc -p project.pkl -o test.pdf - - or explicitly via:: - - $ psyplot old_ds.nc,other_ds.nc -p project.pkl -o test.pdf - - You can also load formatoptions from a configuration file, e.g.:: - - $ echo 'title: my title' > fmt.yaml - $ psyplot myfile.nc -n t2m -pm mapplot -fmt fmt.yaml -o test.pdf - """ - ), - "parser", - ["Examples"], - ) - - epilog = ".. rubric:: Examples\n" + "\n".join(epilog.splitlines()[2:]) - - parser = FuncArgParser( - description=""" - Load a dataset, make the plot and save the result to a file""", - epilog=epilog, - formatter_class=argparse.RawDescriptionHelpFormatter, - ) - - info_grp = parser.add_argument_group( - "Info options", "Options that print informations and quit afterwards" - ) - - parser.update_arg( - "version", - short="V", - long="version", - action="version", - version=psyplot.__version__, - if_existent=False, - group=info_grp, - ) - - parser.update_arg( - "all_versions", - short="aV", - long="all-versions", - action=AllVersionsAction, - if_existent=False, - group=info_grp, - ) - - parser.update_arg( - "list_plugins", - short="lp", - long="list-plugins", - action=ListPluginsAction, - if_existent=False, - group=info_grp, - ) - parser.update_arg( - "list_plot_methods", - short="lpm", - long="list-plot-methods", - action=ListPlotMethodsAction, - if_existent=False, - group=info_grp, - ) - parser.update_arg( - "list_datasets", - short="lds", - long="list-datasets", - action=ListDsNamesAction, - if_existent=False, - group=info_grp, - help="""List the used dataset names in the given `project`.""", - ) - - parser.update_arg( - "list_presets", - short="lps", - long="list-presets", - action=ListPresetsAction, - if_existent=False, - group=info_grp, - ) - - parser.setup_args(make_plot) - - output_grp = parser.add_argument_group( - "Output options", - "Options that only have an effect if the `-o` option is set.", - ) - - parser.update_arg("fnames", positional=True, nargs="*") - - parser.update_arg( - "name", short="n", nargs="*", metavar="variable_name", const=None - ) - - parser.update_arg( - "dims", - short="d", - nargs="+", - type=_load_dims, - metavar="dim,val1[,val2[,...]]", - ) - - pm_choices = { - pm - for pm, d in filter( - lambda t: t[1].get("plot_func", True), - six.iteritems(rcParams["project.plotters"]), - ) - } - if psyplot._project_imported: - import psyplot.project as psy - - pm_choices.update(set(psy.plot._plot_methods)) - parser.update_arg( - "plot_method", - short="pm", - choices=pm_choices, - metavar="{%s}" % ", ".join(map(repr, pm_choices)), - ) - - parser.update_arg("output", short="o", group=output_grp) - parser.update_arg("output_project", short="op", group=output_grp) - - parser.update_arg("project", short="p") - - parser.update_arg( - "formatoptions", - short="fmt", - type=_load_dict, - help=""" - The path to a yaml (``'.yml'`` or ``'.yaml'``) or pickle file - defining a dictionary of formatoption that is applied to the data - visualized by the chosen `plot_method`""", - metavar="FILENAME", - ) - - parser.update_arg( - "chname", - type=lambda s: s.split(","), - nargs="*", - help=""" - A mapping from variable names in the project to variable names in the - datasets that should be used instead. Variable names should be - separated by a comma.""", - metavar="project-variable,variable-to-use", - ) - - parser.update_arg("tight", short="t", group=output_grp) - - parser.update_arg("rc_file", short="rc") - parser.pop_key("rc_file", "metavar") - - parser.update_arg("encoding", short="e") - - parser.pop_key("enable_post", "short") - - parser.update_arg("seaborn_style", short="sns") - - parser.update_arg("concat_dim", short="cd") - - if create: - parser.create_arguments() - - return parser - - -def _load_dict(fname): - with open(fname) as f: - if fname.endswith(".yml") or fname.endswith(".yaml"): - return yaml.load(f, Loader=yaml.SafeLoader) - return pickle.load(f) - - -def _load_dims(s): - s = s.split(",") - if len(s) > 1: - return {s[0]: list(map(int, s[1:]))} - return {} - - -class AllVersionsAction(argparse.Action): - def __init__( - self, - option_strings, - dest=argparse.SUPPRESS, - nargs=None, - default=argparse.SUPPRESS, - **kwargs, - ): - if nargs is not None: - raise ValueError("nargs not allowed") - kwargs["help"] = ( - "Print the versions of all plugins and requirements " "and exit" - ) - kwargs["default"] = default - super(AllVersionsAction, self).__init__( - option_strings, nargs=0, dest=dest, **kwargs - ) - - def __call__(self, parser, namespace, values, option_string=None): - print(yaml.dump(psyplot.get_versions(), default_flow_style=False)) - sys.exit(0) - - -class ListPresetsAction(argparse.Action): - def __init__( - self, - option_strings, - dest=argparse.SUPPRESS, - nargs=None, - default=argparse.SUPPRESS, - **kwargs, - ): - if nargs is not None: - raise ValueError("nargs not allowed") - kwargs["help"] = "Print available presets and exit" - kwargs["default"] = default - super().__init__(option_strings, nargs=0, dest=dest, **kwargs) - - def __call__(self, parser, namespace, values, option_string=None): - from psyplot.config.rcsetup import get_configdir - - presets_dir = osp.join(get_configdir(), "presets") - if not osp.exists(presets_dir): - sys.exit(0) - else: - presets = { - osp.splitext(osp.basename(fname))[0]: fname - for fname in glob.glob(osp.join(presets_dir, "*.yml")) - } - print("\n".join(map(": ".join, presets.items()))) - sys.exit(0) - - -class ListPluginsAction(argparse.Action): - def __init__( - self, - option_strings, - dest=argparse.SUPPRESS, - nargs=None, - default=argparse.SUPPRESS, - **kwargs, - ): - if nargs is not None: - raise ValueError("nargs not allowed") - kwargs["help"] = "Print the names of the plugins and exit" - kwargs["default"] = default - super(ListPluginsAction, self).__init__( - option_strings, nargs=0, dest=dest, **kwargs - ) - - def __call__(self, parser, namespace, values, option_string=None): - print(yaml.dump(psyplot.rcParams._plugins, default_flow_style=False)) - sys.exit(0) - - -class ListPlotMethodsAction(argparse.Action): - def __init__( - self, - option_strings, - dest=argparse.SUPPRESS, - nargs=None, - default=argparse.SUPPRESS, - **kwargs, - ): - if nargs is not None: - raise ValueError("nargs not allowed") - kwargs["help"] = "List the available plot methods and what they do" - kwargs["default"] = default - super(ListPlotMethodsAction, self).__init__( - option_strings, nargs=0, dest=dest, **kwargs - ) - - def __call__(self, parser, namespace, values, option_string=None): - pm_choices = {} - for pm, d in filter( - lambda t: t[1].get("plot_func", True), - six.iteritems(rcParams["project.plotters"]), - ): - pm_choices[pm] = d.get("summary") or ( - "Open and plot data via :class:`%s.%s` plotters" - % (d["module"], d["plotter_name"]) - ) - if psyplot._project_imported: - import psyplot.project as psy - - pm_choices.update(psy.plot._plot_methods) - print(yaml.dump(pm_choices, default_flow_style=False)) - sys.exit(0) - - -class ListDsNamesAction(argparse.Action): - """An action to list the used file names in a project""" - - def __init__( - self, - option_strings, - dest=argparse.SUPPRESS, - nargs=None, - default=argparse.SUPPRESS, - **kwargs, - ): - if nargs is not None: - raise ValueError("nargs not allowed") - kwargs["default"] = default - super(ListDsNamesAction, self).__init__( - option_strings, nargs=0, dest=dest, **kwargs - ) - - def __call__(self, parser, namespace, values, option_string=None): - if namespace.project is None: - print( - "A project is required before this argument! Call syntax:\n" - "%s -p .pkl %s" % (parser.prog, option_string) - ) - sys.exit(1) - import pickle - - import psyplot.data as psyd - - with open(namespace.project, "rb") as f: - d = pickle.load(f)["arrays"] - names = list( - filter(None, (t[0] for t in psyd.ArrayList._get_dsnames(d))) - ) - if names: - print(yaml.dump(names, default_flow_style=False)) - sys.exit(0) - - -if __name__ == "__main__": - main() diff --git a/psyplot/_version.py b/psyplot/_version.py deleted file mode 100644 index cc7e4a0..0000000 --- a/psyplot/_version.py +++ /dev/null @@ -1,698 +0,0 @@ -# SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH -# -# SPDX-License-Identifier: LGPL-3.0-only - -# This file helps to compute a version number in source trees obtained from -# git-archive tarball (such as those provided by githubs download-from-tag -# feature). Distribution tarballs (built by setup.py sdist) and build -# directories (produced by setup.py build) will contain a much shorter file -# that just contains the computed version number. - -# This file is released into the public domain. Generated by -# versioneer-0.21 (https://github.com/python-versioneer/python-versioneer) - -"""Git implementation of _version.py.""" - -import errno -import os -import re -import subprocess -import sys -from typing import Callable, Dict - - -def get_keywords(): - """Get the keywords needed to look up the version information.""" - # these strings will be replaced by git during git-archive. - # setup.py/versioneer.py will grep for the variable names, so they must - # each be defined on a line of their own. _version.py will just call - # get_keywords(). - git_refnames = "$Format:%d$" - git_full = "$Format:%H$" - git_date = "$Format:%ci$" - keywords = {"refnames": git_refnames, "full": git_full, "date": git_date} - return keywords - - -class VersioneerConfig: - """Container for Versioneer configuration parameters.""" - - -def get_config(): - """Create, populate and return the VersioneerConfig() object.""" - # these strings are filled in when 'setup.py versioneer' creates - # _version.py - cfg = VersioneerConfig() - cfg.VCS = "git" - cfg.style = "pep440" - cfg.tag_prefix = "v" - cfg.parentdir_prefix = "psyplot-" - cfg.versionfile_source = "psyplot/_version.py" - cfg.verbose = False - return cfg - - -class NotThisMethod(Exception): - """Exception raised if a method is not valid for the current scenario.""" - - -LONG_VERSION_PY: Dict[str, str] = {} -HANDLERS: Dict[str, Dict[str, Callable]] = {} - - -def register_vcs_handler(vcs, method): # decorator - """Create decorator to mark a method as the handler of a VCS.""" - - def decorate(f): - """Store f in HANDLERS[vcs][method].""" - if vcs not in HANDLERS: - HANDLERS[vcs] = {} - HANDLERS[vcs][method] = f - return f - - return decorate - - -def run_command( - commands, args, cwd=None, verbose=False, hide_stderr=False, env=None -): - """Call the given command(s).""" - assert isinstance(commands, list) - process = None - for command in commands: - try: - dispcmd = str([command] + args) - # remember shell=False, so use git.cmd on windows, not just git - process = subprocess.Popen( - [command] + args, - cwd=cwd, - env=env, - stdout=subprocess.PIPE, - stderr=(subprocess.PIPE if hide_stderr else None), - ) - break - except OSError: - e = sys.exc_info()[1] - if e.errno == errno.ENOENT: - continue - if verbose: - print("unable to run %s" % dispcmd) - print(e) - return None, None - else: - if verbose: - print("unable to find command, tried %s" % (commands,)) - return None, None - stdout = process.communicate()[0].strip().decode() - if process.returncode != 0: - if verbose: - print("unable to run %s (error)" % dispcmd) - print("stdout was %s" % stdout) - return None, process.returncode - return stdout, process.returncode - - -def versions_from_parentdir(parentdir_prefix, root, verbose): - """Try to determine the version from the parent directory name. - - Source tarballs conventionally unpack into a directory that includes both - the project name and a version string. We will also support searching up - two directory levels for an appropriately named parent directory - """ - rootdirs = [] - - for _ in range(3): - dirname = os.path.basename(root) - if dirname.startswith(parentdir_prefix): - return { - "version": dirname[len(parentdir_prefix) :], - "full-revisionid": None, - "dirty": False, - "error": None, - "date": None, - } - rootdirs.append(root) - root = os.path.dirname(root) # up a level - - if verbose: - print( - "Tried directories %s but none started with prefix %s" - % (str(rootdirs), parentdir_prefix) - ) - raise NotThisMethod("rootdir doesn't start with parentdir_prefix") - - -@register_vcs_handler("git", "get_keywords") -def git_get_keywords(versionfile_abs): - """Extract version information from the given file.""" - # the code embedded in _version.py can just fetch the value of these - # keywords. When used from setup.py, we don't want to import _version.py, - # so we do it with a regexp instead. This function is not used from - # _version.py. - keywords = {} - try: - with open(versionfile_abs, "r") as fobj: - for line in fobj: - if line.strip().startswith("git_refnames ="): - mo = re.search(r'=\s*"(.*)"', line) - if mo: - keywords["refnames"] = mo.group(1) - if line.strip().startswith("git_full ="): - mo = re.search(r'=\s*"(.*)"', line) - if mo: - keywords["full"] = mo.group(1) - if line.strip().startswith("git_date ="): - mo = re.search(r'=\s*"(.*)"', line) - if mo: - keywords["date"] = mo.group(1) - except OSError: - pass - return keywords - - -@register_vcs_handler("git", "keywords") -def git_versions_from_keywords(keywords, tag_prefix, verbose): - """Get version information from git keywords.""" - if "refnames" not in keywords: - raise NotThisMethod("Short version file found") - date = keywords.get("date") - if date is not None: - # Use only the last line. Previous lines may contain GPG signature - # information. - date = date.splitlines()[-1] - - # git-2.2.0 added "%cI", which expands to an ISO-8601 -compliant - # datestamp. However we prefer "%ci" (which expands to an "ISO-8601 - # -like" string, which we must then edit to make compliant), because - # it's been around since git-1.5.3, and it's too difficult to - # discover which version we're using, or to work around using an - # older one. - date = date.strip().replace(" ", "T", 1).replace(" ", "", 1) - refnames = keywords["refnames"].strip() - if refnames.startswith("$Format"): - if verbose: - print("keywords are unexpanded, not using") - raise NotThisMethod("unexpanded keywords, not a git-archive tarball") - refs = {r.strip() for r in refnames.strip("()").split(",")} - # starting in git-1.8.3, tags are listed as "tag: foo-1.0" instead of - # just "foo-1.0". If we see a "tag: " prefix, prefer those. - TAG = "tag: " - tags = {r[len(TAG) :] for r in refs if r.startswith(TAG)} - if not tags: - # Either we're using git < 1.8.3, or there really are no tags. We use - # a heuristic: assume all version tags have a digit. The old git %d - # expansion behaves like git log --decorate=short and strips out the - # refs/heads/ and refs/tags/ prefixes that would let us distinguish - # between branches and tags. By ignoring refnames without digits, we - # filter out many common branch names like "release" and - # "stabilization", as well as "HEAD" and "master". - tags = {r for r in refs if re.search(r"\d", r)} - if verbose: - print("discarding '%s', no digits" % ",".join(refs - tags)) - if verbose: - print("likely tags: %s" % ",".join(sorted(tags))) - for ref in sorted(tags): - # sorting will prefer e.g. "2.0" over "2.0rc1" - if ref.startswith(tag_prefix): - r = ref[len(tag_prefix) :] - # Filter out refs that exactly match prefix or that don't start - # with a number once the prefix is stripped (mostly a concern - # when prefix is '') - if not re.match(r"\d", r): - continue - if verbose: - print("picking %s" % r) - return { - "version": r, - "full-revisionid": keywords["full"].strip(), - "dirty": False, - "error": None, - "date": date, - } - # no suitable tags, so version is "0+unknown", but full hex is still there - if verbose: - print("no suitable tags, using unknown + full revision id") - return { - "version": "0+unknown", - "full-revisionid": keywords["full"].strip(), - "dirty": False, - "error": "no suitable tags", - "date": None, - } - - -@register_vcs_handler("git", "pieces_from_vcs") -def git_pieces_from_vcs(tag_prefix, root, verbose, runner=run_command): - """Get version from 'git describe' in the root of the source tree. - - This only gets called if the git-archive 'subst' keywords were *not* - expanded, and _version.py hasn't already been rewritten with a short - version string, meaning we're inside a checked out source tree. - """ - GITS = ["git"] - TAG_PREFIX_REGEX = "*" - if sys.platform == "win32": - GITS = ["git.cmd", "git.exe"] - TAG_PREFIX_REGEX = r"\*" - - _, rc = runner( - GITS, ["rev-parse", "--git-dir"], cwd=root, hide_stderr=True - ) - if rc != 0: - if verbose: - print("Directory %s not under git control" % root) - raise NotThisMethod("'git rev-parse --git-dir' returned error") - - # if there is a tag matching tag_prefix, this yields TAG-NUM-gHEX[-dirty] - # if there isn't one, this yields HEX[-dirty] (no NUM) - describe_out, rc = runner( - GITS, - [ - "describe", - "--tags", - "--dirty", - "--always", - "--long", - "--match", - "%s%s" % (tag_prefix, TAG_PREFIX_REGEX), - ], - cwd=root, - ) - # --long was added in git-1.5.5 - if describe_out is None: - raise NotThisMethod("'git describe' failed") - describe_out = describe_out.strip() - full_out, rc = runner(GITS, ["rev-parse", "HEAD"], cwd=root) - if full_out is None: - raise NotThisMethod("'git rev-parse' failed") - full_out = full_out.strip() - - pieces = {} - pieces["long"] = full_out - pieces["short"] = full_out[:7] # maybe improved later - pieces["error"] = None - - branch_name, rc = runner( - GITS, ["rev-parse", "--abbrev-ref", "HEAD"], cwd=root - ) - # --abbrev-ref was added in git-1.6.3 - if rc != 0 or branch_name is None: - raise NotThisMethod("'git rev-parse --abbrev-ref' returned error") - branch_name = branch_name.strip() - - if branch_name == "HEAD": - # If we aren't exactly on a branch, pick a branch which represents - # the current commit. If all else fails, we are on a branchless - # commit. - branches, rc = runner(GITS, ["branch", "--contains"], cwd=root) - # --contains was added in git-1.5.4 - if rc != 0 or branches is None: - raise NotThisMethod("'git branch --contains' returned error") - branches = branches.split("\n") - - # Remove the first line if we're running detached - if "(" in branches[0]: - branches.pop(0) - - # Strip off the leading "* " from the list of branches. - branches = [branch[2:] for branch in branches] - if "master" in branches: - branch_name = "master" - elif not branches: - branch_name = None - else: - # Pick the first branch that is returned. Good or bad. - branch_name = branches[0] - - pieces["branch"] = branch_name - - # parse describe_out. It will be like TAG-NUM-gHEX[-dirty] or HEX[-dirty] - # TAG might have hyphens. - git_describe = describe_out - - # look for -dirty suffix - dirty = git_describe.endswith("-dirty") - pieces["dirty"] = dirty - if dirty: - git_describe = git_describe[: git_describe.rindex("-dirty")] - - # now we have TAG-NUM-gHEX or HEX - - if "-" in git_describe: - # TAG-NUM-gHEX - mo = re.search(r"^(.+)-(\d+)-g([0-9a-f]+)$", git_describe) - if not mo: - # unparsable. Maybe git-describe is misbehaving? - pieces["error"] = ( - "unable to parse git-describe output: '%s'" % describe_out - ) - return pieces - - # tag - full_tag = mo.group(1) - if not full_tag.startswith(tag_prefix): - if verbose: - fmt = "tag '%s' doesn't start with prefix '%s'" - print(fmt % (full_tag, tag_prefix)) - pieces["error"] = "tag '%s' doesn't start with prefix '%s'" % ( - full_tag, - tag_prefix, - ) - return pieces - pieces["closest-tag"] = full_tag[len(tag_prefix) :] - - # distance: number of commits since tag - pieces["distance"] = int(mo.group(2)) - - # commit: short hex revision ID - pieces["short"] = mo.group(3) - - else: - # HEX: no tags - pieces["closest-tag"] = None - count_out, rc = runner(GITS, ["rev-list", "HEAD", "--count"], cwd=root) - pieces["distance"] = int(count_out) # total number of commits - - # commit date: see ISO-8601 comment in git_versions_from_keywords() - date = runner(GITS, ["show", "-s", "--format=%ci", "HEAD"], cwd=root)[ - 0 - ].strip() - # Use only the last line. Previous lines may contain GPG signature - # information. - date = date.splitlines()[-1] - pieces["date"] = date.strip().replace(" ", "T", 1).replace(" ", "", 1) - - return pieces - - -def plus_or_dot(pieces): - """Return a + if we don't already have one, else return a .""" - if "+" in pieces.get("closest-tag", ""): - return "." - return "+" - - -def render_pep440(pieces): - """Build up version string, with post-release "local version identifier". - - Our goal: TAG[+DISTANCE.gHEX[.dirty]] . Note that if you - get a tagged build and then dirty it, you'll get TAG+0.gHEX.dirty - - Exceptions: - 1: no tags. git_describe was just HEX. 0+untagged.DISTANCE.gHEX[.dirty] - """ - if pieces["closest-tag"]: - rendered = pieces["closest-tag"] - if pieces["distance"] or pieces["dirty"]: - rendered += plus_or_dot(pieces) - rendered += "%d.g%s" % (pieces["distance"], pieces["short"]) - if pieces["dirty"]: - rendered += ".dirty" - else: - # exception #1 - rendered = "0+untagged.%d.g%s" % (pieces["distance"], pieces["short"]) - if pieces["dirty"]: - rendered += ".dirty" - return rendered - - -def render_pep440_branch(pieces): - """TAG[[.dev0]+DISTANCE.gHEX[.dirty]] . - - The ".dev0" means not master branch. Note that .dev0 sorts backwards - (a feature branch will appear "older" than the master branch). - - Exceptions: - 1: no tags. 0[.dev0]+untagged.DISTANCE.gHEX[.dirty] - """ - if pieces["closest-tag"]: - rendered = pieces["closest-tag"] - if pieces["distance"] or pieces["dirty"]: - if pieces["branch"] != "master": - rendered += ".dev0" - rendered += plus_or_dot(pieces) - rendered += "%d.g%s" % (pieces["distance"], pieces["short"]) - if pieces["dirty"]: - rendered += ".dirty" - else: - # exception #1 - rendered = "0" - if pieces["branch"] != "master": - rendered += ".dev0" - rendered += "+untagged.%d.g%s" % (pieces["distance"], pieces["short"]) - if pieces["dirty"]: - rendered += ".dirty" - return rendered - - -def pep440_split_post(ver): - """Split pep440 version string at the post-release segment. - - Returns the release segments before the post-release and the - post-release version number (or -1 if no post-release segment is present). - """ - vc = str.split(ver, ".post") - return vc[0], int(vc[1] or 0) if len(vc) == 2 else None - - -def render_pep440_pre(pieces): - """TAG[.postN.devDISTANCE] -- No -dirty. - - Exceptions: - 1: no tags. 0.post0.devDISTANCE - """ - if pieces["closest-tag"]: - if pieces["distance"]: - # update the post release segment - tag_version, post_version = pep440_split_post( - pieces["closest-tag"] - ) - rendered = tag_version - if post_version is not None: - rendered += ".post%d.dev%d" % ( - post_version + 1, - pieces["distance"], - ) - else: - rendered += ".post0.dev%d" % (pieces["distance"]) - else: - # no commits, use the tag as the version - rendered = pieces["closest-tag"] - else: - # exception #1 - rendered = "0.post0.dev%d" % pieces["distance"] - return rendered - - -def render_pep440_post(pieces): - """TAG[.postDISTANCE[.dev0]+gHEX] . - - The ".dev0" means dirty. Note that .dev0 sorts backwards - (a dirty tree will appear "older" than the corresponding clean one), - but you shouldn't be releasing software with -dirty anyways. - - Exceptions: - 1: no tags. 0.postDISTANCE[.dev0] - """ - if pieces["closest-tag"]: - rendered = pieces["closest-tag"] - if pieces["distance"] or pieces["dirty"]: - rendered += ".post%d" % pieces["distance"] - if pieces["dirty"]: - rendered += ".dev0" - rendered += plus_or_dot(pieces) - rendered += "g%s" % pieces["short"] - else: - # exception #1 - rendered = "0.post%d" % pieces["distance"] - if pieces["dirty"]: - rendered += ".dev0" - rendered += "+g%s" % pieces["short"] - return rendered - - -def render_pep440_post_branch(pieces): - """TAG[.postDISTANCE[.dev0]+gHEX[.dirty]] . - - The ".dev0" means not master branch. - - Exceptions: - 1: no tags. 0.postDISTANCE[.dev0]+gHEX[.dirty] - """ - if pieces["closest-tag"]: - rendered = pieces["closest-tag"] - if pieces["distance"] or pieces["dirty"]: - rendered += ".post%d" % pieces["distance"] - if pieces["branch"] != "master": - rendered += ".dev0" - rendered += plus_or_dot(pieces) - rendered += "g%s" % pieces["short"] - if pieces["dirty"]: - rendered += ".dirty" - else: - # exception #1 - rendered = "0.post%d" % pieces["distance"] - if pieces["branch"] != "master": - rendered += ".dev0" - rendered += "+g%s" % pieces["short"] - if pieces["dirty"]: - rendered += ".dirty" - return rendered - - -def render_pep440_old(pieces): - """TAG[.postDISTANCE[.dev0]] . - - The ".dev0" means dirty. - - Exceptions: - 1: no tags. 0.postDISTANCE[.dev0] - """ - if pieces["closest-tag"]: - rendered = pieces["closest-tag"] - if pieces["distance"] or pieces["dirty"]: - rendered += ".post%d" % pieces["distance"] - if pieces["dirty"]: - rendered += ".dev0" - else: - # exception #1 - rendered = "0.post%d" % pieces["distance"] - if pieces["dirty"]: - rendered += ".dev0" - return rendered - - -def render_git_describe(pieces): - """TAG[-DISTANCE-gHEX][-dirty]. - - Like 'git describe --tags --dirty --always'. - - Exceptions: - 1: no tags. HEX[-dirty] (note: no 'g' prefix) - """ - if pieces["closest-tag"]: - rendered = pieces["closest-tag"] - if pieces["distance"]: - rendered += "-%d-g%s" % (pieces["distance"], pieces["short"]) - else: - # exception #1 - rendered = pieces["short"] - if pieces["dirty"]: - rendered += "-dirty" - return rendered - - -def render_git_describe_long(pieces): - """TAG-DISTANCE-gHEX[-dirty]. - - Like 'git describe --tags --dirty --always -long'. - The distance/hash is unconditional. - - Exceptions: - 1: no tags. HEX[-dirty] (note: no 'g' prefix) - """ - if pieces["closest-tag"]: - rendered = pieces["closest-tag"] - rendered += "-%d-g%s" % (pieces["distance"], pieces["short"]) - else: - # exception #1 - rendered = pieces["short"] - if pieces["dirty"]: - rendered += "-dirty" - return rendered - - -def render(pieces, style): - """Render the given version pieces into the requested style.""" - if pieces["error"]: - return { - "version": "unknown", - "full-revisionid": pieces.get("long"), - "dirty": None, - "error": pieces["error"], - "date": None, - } - - if not style or style == "default": - style = "pep440" # the default - - if style == "pep440": - rendered = render_pep440(pieces) - elif style == "pep440-branch": - rendered = render_pep440_branch(pieces) - elif style == "pep440-pre": - rendered = render_pep440_pre(pieces) - elif style == "pep440-post": - rendered = render_pep440_post(pieces) - elif style == "pep440-post-branch": - rendered = render_pep440_post_branch(pieces) - elif style == "pep440-old": - rendered = render_pep440_old(pieces) - elif style == "git-describe": - rendered = render_git_describe(pieces) - elif style == "git-describe-long": - rendered = render_git_describe_long(pieces) - else: - raise ValueError("unknown style '%s'" % style) - - return { - "version": rendered, - "full-revisionid": pieces["long"], - "dirty": pieces["dirty"], - "error": None, - "date": pieces.get("date"), - } - - -def get_versions(): - """Get version information or return default if unable to do so.""" - # I am in _version.py, which lives at ROOT/VERSIONFILE_SOURCE. If we have - # __file__, we can work backwards from there to the root. Some - # py2exe/bbfreeze/non-CPython implementations don't do __file__, in which - # case we can only use expanded keywords. - - cfg = get_config() - verbose = cfg.verbose - - try: - return git_versions_from_keywords( - get_keywords(), cfg.tag_prefix, verbose - ) - except NotThisMethod: - pass - - try: - root = os.path.realpath(__file__) - # versionfile_source is the relative path from the top of the source - # tree (where the .git directory might live) to this file. Invert - # this to find the root from __file__. - for _ in cfg.versionfile_source.split("/"): - root = os.path.dirname(root) - except NameError: - return { - "version": "0+unknown", - "full-revisionid": None, - "dirty": None, - "error": "unable to find root of source tree", - "date": None, - } - - try: - pieces = git_pieces_from_vcs(cfg.tag_prefix, root, verbose) - return render(pieces, cfg.style) - except NotThisMethod: - pass - - try: - if cfg.parentdir_prefix: - return versions_from_parentdir(cfg.parentdir_prefix, root, verbose) - except NotThisMethod: - pass - - return { - "version": "0+unknown", - "full-revisionid": None, - "dirty": None, - "error": "unable to compute version", - "date": None, - } diff --git a/psyplot/config/__init__.py b/psyplot/config/__init__.py deleted file mode 100755 index 1b2a52d..0000000 --- a/psyplot/config/__init__.py +++ /dev/null @@ -1,24 +0,0 @@ -"""Configuration module of the psyplot package - -This module contains the module for managing rc parameters and the logging. -Default parameters are defined in the :data:`rcsetup.defaultParams` -dictionary, however you can set up your own configuration in a yaml file (see -:func:`psyplot.load_rc_from_file`)""" - -# SPDX-FileCopyrightText: 2016-2024 University of Lausanne -# SPDX-FileCopyrightText: 2020-2021 Helmholtz-Zentrum Geesthacht - -# SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH -# -# SPDX-License-Identifier: LGPL-3.0-only - -from .logsetup import setup_logging -from .rcsetup import psyplot_fname - -#: :class:`str`. Path to the yaml logging configuration file -logcfg_path = setup_logging() - - -#: class:`str` or ``None``. Path to the yaml configuration file (if found). -#: See :func:`~psyplot.config.rcsetup.psyplot_fname` for further information -config_path = psyplot_fname() diff --git a/psyplot/config/logging.yml b/psyplot/config/logging.yml deleted file mode 100755 index 050ffe0..0000000 --- a/psyplot/config/logging.yml +++ /dev/null @@ -1,86 +0,0 @@ -# SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH -# -# SPDX-License-Identifier: CC0-1.0 - ---- -# logging settings for the nc2map module - -version: 1 - -disable_existing_loggers: False - -formatters: - - simple: - - format: "[%(name)s] - %(levelname)s - %(message)s" - - level_message: - - format: "%(levelname)s: %(message)s" - - full: - format: "%(asctime)s - [%(name)s.%(funcName)s] - %(levelname)s - %(message)s" - - -handlers: - - console: - - class: logging.StreamHandler - - level: INFO - - formatter: simple - - stream: ext://sys.stdout - - warning_console: - - class: logging.StreamHandler - - level: INFO - - formatter: level_message - - stream: ext://sys.stdout - - - debug_file_handler: - - class: logging.handlers.RotatingFileHandler - - mode: w - - level: DEBUG - - formatter: full - - filename: ~/.debug_psyplot.log - - maxBytes: 10485760 # 10MB - - backupCount: 5 - - encoding: utf8 - - delay: True - -loggers: - - psyplot: - - handlers: [console, debug_file_handler] - - propagate: False - - level: INFO - - psyplot.warning: - - handlers: [warning_console, debug_file_handler] - - propagate: False - - level: WARNING -... diff --git a/psyplot/config/logging_debug.yml b/psyplot/config/logging_debug.yml deleted file mode 100755 index 4dc6ec3..0000000 --- a/psyplot/config/logging_debug.yml +++ /dev/null @@ -1,86 +0,0 @@ -# SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH -# -# SPDX-License-Identifier: CC0-1.0 - ---- -# debug logging settings (sets the level of the nc2map logger to DEBUG) - -version: 1 - -disable_existing_loggers: False - -formatters: - - simple: - - format: "[%(name)s] - %(levelname)s - %(message)s" - - level_message: - - format: "%(levelname)s: %(message)s" - - full: - format: "%(asctime)s - [%(name)s.%(funcName)s] - %(levelname)s - %(message)s" - - -handlers: - - console: - - class: logging.StreamHandler - - level: INFO - - formatter: simple - - stream: ext://sys.stdout - - warning_console: - - class: logging.StreamHandler - - level: INFO - - formatter: level_message - - stream: ext://sys.stdout - - - debug_file_handler: - - class: logging.handlers.RotatingFileHandler - - mode: w - - level: DEBUG - - formatter: full - - filename: ~/.debug_psyplot.log - - maxBytes: 10485760 # 10MB - - backupCount: 5 - - encoding: utf8 - - delay: True - -loggers: - - psyplot: - - handlers: [console, debug_file_handler] - - propagate: False - - level: DEBUG - - psyplot.warning: - - handlers: [warning_console, debug_file_handler] - - propagate: False - - level: WARNING -... diff --git a/psyplot/config/logsetup.py b/psyplot/config/logsetup.py deleted file mode 100755 index 9d12a26..0000000 --- a/psyplot/config/logsetup.py +++ /dev/null @@ -1,95 +0,0 @@ -"""Logging configuration module of the psyplot package - -This module defines the essential functions for setting up the -:class:`logging.Logger` instances that are used by the psyplot package.""" - -# SPDX-FileCopyrightText: 2016-2024 University of Lausanne -# SPDX-FileCopyrightText: 2020-2021 Helmholtz-Zentrum Geesthacht - -# SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH -# -# SPDX-License-Identifier: LGPL-3.0-only - -import logging -import logging.config -import os -import sys - -import six -import yaml - -from psyplot.docstring import dedent - - -def _get_home(): - """Find user's home directory if possible. - Otherwise, returns None. - - :see: http://mail.python.org/pipermail/python-list/2005-February/325395.html - - This function is copied from matplotlib version 1.4.3, Jan 2016 - """ - try: - if six.PY2 and sys.platform == "win32": - path = os.path.expanduser(b"~").decode(sys.getfilesystemencoding()) - else: - path = os.path.expanduser("~") - except ImportError: - # This happens on Google App Engine (pwd module is not present). - pass - else: - if os.path.isdir(path): - return path - for evar in ("HOME", "USERPROFILE", "TMP"): - path = os.environ.get(evar) - if path is not None and os.path.isdir(path): - return path - return None - - -@dedent -def setup_logging( - default_path=None, default_level=logging.INFO, env_key="LOG_PSYPLOT" -): - """ - Setup logging configuration - - Parameters - ---------- - default_path: str - Default path of the yaml logging configuration file. If None, it - defaults to the 'logging.yaml' file in the config directory - default_level: int - Default: :data:`logging.INFO`. Default level if default_path does not - exist - env_key: str - environment variable specifying a different logging file than - `default_path` (Default: 'LOG_CFG') - - Returns - ------- - path: str - Path to the logging configuration file - - Notes - ----- - Function taken from - http://victorlin.me/posts/2012/08/26/good-logging-practice-in-python""" - path = default_path or os.path.join( - os.path.dirname(__file__), "logging.yml" - ) - value = os.getenv(env_key, None) - home = _get_home() - if value: - path = value - if os.path.exists(path): - with open(path, "rt") as f: - config = yaml.load(f.read(), Loader=yaml.SafeLoader) - for handler in config.get("handlers", {}).values(): - if "~" in handler.get("filename", ""): - handler["filename"] = handler["filename"].replace("~", home) - logging.config.dictConfig(config) - else: - path = None - logging.basicConfig(level=default_level) - return path diff --git a/psyplot/config/rcsetup.py b/psyplot/config/rcsetup.py deleted file mode 100755 index d05fe08..0000000 --- a/psyplot/config/rcsetup.py +++ /dev/null @@ -1,1358 +0,0 @@ -"""Default management of the psyplot package - -This module defines the necessary classes, data and functions for the default -configuration of the module. -The structure is motivated and to larger parts taken from the matplotlib_ -package. - -.. _matplotlib: http://matplotlib.org/api/""" - -# SPDX-FileCopyrightText: 2016-2024 University of Lausanne -# SPDX-FileCopyrightText: 2020-2021 Helmholtz-Zentrum Geesthacht - -# SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH -# -# SPDX-License-Identifier: LGPL-3.0-only - -import contextlib -import inspect -import logging -import os -import re -import sys -from collections import UserDict, defaultdict -from itertools import chain - -import six -import yaml - -from psyplot.config.logsetup import _get_home -from psyplot.docstring import dedent, docstrings, safe_modulo -from psyplot.utils import isstring -from psyplot.warning import warn - - -@docstrings.get_sections(base="safe_list") -@dedent -def safe_list(iterable): - """Function to create a list - - Parameters - ---------- - iterable: iterable or anything else - Parameter that shall be converted to a list. - - - If string or any non-iterable, it will be put into a list - - if iterable, it will be converted to a list - - Returns - ------- - list - `l` put (or converted) into a list""" - if isstring(iterable): - return [iterable] - try: - return list(iterable) - except TypeError: - return [iterable] - - -class SubDict(UserDict, dict): # type: ignore - """Class that keeps week reference to the base dictionary - - This class is used by the :meth:`RcParams.find_and_replace` method - to provide an easy handable instance that keeps reference to the - base rcParams dictionary.""" - - @property - def data(self): - """Dictionary representing this :class:`SubDict` instance - - See Also - -------- - iteritems - """ - return dict(list(self.iteritems())) - - @property - def replace(self): - """:class:`bool`. If True, matching strings in the :attr:`base_str` - attribute are replaced with an empty string.""" - return self._replace - - @replace.setter - def replace(self, value): - def replace_base(key): - for pattern in self.patterns: - try: - return pattern.match(key).group("key") - except AttributeError: # if match is None - pass - raise KeyError( - "Could not find any matching key for %s in the base " - "dictionary!" % key - ) - - value = bool(value) - if hasattr(self, "_replace") and value == self._replace: - return - if not hasattr(self, "_replace"): - self._replace = value - return - # if the value has changed, we change the key in the SubDict instance - # to match the ones in the base dictionary (if they exist) - for key, val in iter(dict.items(self)): - try: - if value: - new_key = replace_base(key) - else: - new_key = self._get_val_and_base(key)[0] - except KeyError: - continue - else: - dict.__setitem__(self, new_key, dict.pop(self, key)) - self._replace = value - - #: :class:`dict`. Reference dictionary - base = {} - - #: list of strings. The strings that are used to set and get a specific key - #: from the :attr:`base` dictionary - base_str = [] - - #: list of compiled patterns from the :attr:`base_str` attribute, that - #: are used to look for the matching keys in :attr:`base` - patterns = [] - - #: :class:`bool`. If True, changes are traced back to the :attr:`base` dict - trace = False - - @docstrings.get_sections(base="SubDict.add_base_str") - @dedent - def add_base_str( - self, base_str, pattern=".+", pattern_base=None, append=True - ): - r""" - Add further base string to this instance - - Parameters - ---------- - base_str: str or list of str - Strings that are used as to look for keys to get and set keys in - the :attr:`base` dictionary. If a string does not contain - ``'%(key)s'``, it will be appended at the end. ``'%(key)s'`` will - be replaced by the specific key for getting and setting an item. - pattern: str - Default: ``'.+'``. This is the pattern that is inserted for - ``%(key)s`` in a base string to look for matches (using the - :mod:`re` module) in the `base` dictionary. The default `pattern` - matches everything without white spaces. - pattern_base: str or list or str - If None, the whatever is given in the `base_str` is used. - Those strings will be used for generating the final search - patterns. You can specify this parameter by yourself to avoid the - misinterpretation of patterns. For example for a `base_str` like - ``'my.str'`` it is recommended to additionally provide the - `pattern_base` keyword with ``'my\.str'``. - Like for `base_str`, the ``%(key)s`` is appended if not already in - the string. - append: bool - If True, the given `base_str` are appended (i.e. it is first - looked for them in the :attr:`base` dictionary), otherwise they are - put at the beginning""" - base_str = safe_list(base_str) - pattern_base = safe_list(pattern_base or []) - for i, s in enumerate(base_str): - if "%(key)s" not in s: - base_str[i] += "%(key)s" - if pattern_base: - for i, s in enumerate(pattern_base): - if "%(key)s" not in s: - pattern_base[i] += "%(key)s" - else: - pattern_base = base_str - self.base_str = base_str + self.base_str - self.patterns = ( - list( - map( - lambda s: re.compile( - s.replace("%(key)s", "(?P%s)" % pattern) - ), - pattern_base, - ) - ) - + self.patterns - ) - - docstrings.delete_params("SubDict.add_base_str.parameters", "append") - - @docstrings.get_sections(base="SubDict") - @docstrings.dedent - def __init__( - self, - base, - base_str, - pattern=".+", - pattern_base=None, - trace=False, - replace=True, - ): - """ - Parameters - ---------- - base: dict - base dictionary - %(SubDict.add_base_str.parameters.no_append)s - trace: bool - Default: False. If True, changes in the SubDict are traced back to - the `base` dictionary. You can change this behaviour also - afterwards by changing the :attr:`trace` attribute - replace: bool - Default: True. If True, everything but the '%%(key)s' part in a - base string is replaced (see examples below) - - - Notes - ----- - - If a key of matches multiple strings in `base_str`, the first - matching one is used. - - the SubDict class is (of course) not that efficient as the - :attr:`base` dictionary, since we loop multiple times through it's - keys - - Examples - -------- - Initialization example:: - - >>> from psyplot import rcParams - >>> d = rcParams.find_and_replace(['plotter.baseplotter.', - ... 'plotter.vector.']) - >>> print d['title'] - - >>> print d['arrowsize'] - 1.0 - - To convert it to a usual dictionary, simply use the :attr:`data` - attribute:: - - >>> d.data - {'title': None, 'arrowsize': 1.0, ...} - - Note that changing one keyword of your :class:`SubDict` will not change - the :attr:`base` dictionary, unless you set the :attr:`trace` attribute - to ``True``:: - - >>> d['title'] = 'my title' - >>> print(d['title']) - my title - - >>> print(rcParams['plotter.baseplotter.title']) - - >>> d.trace = True - >>> d['title'] = 'my second title' - >>> print(d['title']) - my second title - >>> print(rcParams['plotter.baseplotter.title']) - my second title - - Furthermore, changing the :attr:`replace` attribute will change how you - can access the keys:: - - >>> d.replace = False - - # now setting d['title'] = 'anything' would raise an error (since - # d.trace is set to True and 'title' is not a key in the rcParams - # dictionary. Instead we need - >>> d['plotter.baseplotter.title'] = 'anything' - - See Also - -------- - RcParams.find_and_replace""" - self.base = base - self.base_str = [] - self.patterns = [] - self.replace = bool(replace) - self.trace = bool(trace) - self.add_base_str( - base_str, pattern=pattern, pattern_base=pattern_base, append=False - ) - - def __getitem__(self, key): - if key in iter(dict.keys(self)): - return dict.__getitem__(self, key) - if not self.replace: - return self.base[key] - return self._get_val_and_base(key)[1] - - def __setitem__(self, key, val): - # set it in the SubDict instance if trace is False - if not self.trace: - dict.__setitem__(self, key, val) - return - base = self.base - # set it with the given key, if trace is True - if not self.replace: - base[key] = val - dict.pop(self, key, None) - return - # first look if the key already exists in the base dictionary - for s, patt in self._iter_base_and_pattern(key): - m = patt.match(s) - if m and s in base: - base[m.group()] = val - return - # if the key does not exist, we set it - self.base[key] = val - - def _get_val_and_base(self, key): - found = False - e = None - for s, patt in self._iter_base_and_pattern(key): - found = True - try: - m = patt.match(s) - if m: - return m.group(), self.base[m.group()] - else: - raise KeyError( - "{0} does not match the specified pattern!".format(s) - ) - except KeyError: - pass - if not found: - if e is not None: - raise - raise KeyError("{0} does not match the specified pattern!".format(key)) - - def _iter_base_and_pattern(self, key): - return zip( - map(lambda s: safe_modulo(s, {"key": key}), self.base_str), - self.patterns, - ) - - def iterkeys(self): - """Unsorted iterator over keys""" - patterns = self.patterns - replace = self.replace - seen = set() - for key in six.iterkeys(self.base): - for pattern in patterns: - m = pattern.match(key) - if m: - ret = m.group("key") if replace else m.group() - if ret not in seen: - seen.add(ret) - yield ret - break - for key in iter(dict.keys(self)): - if key not in seen: - yield key - - def iteritems(self): - """Unsorted iterator over items""" - return ((key, self[key]) for key in self.iterkeys()) - - def itervalues(self): - """Unsorted iterator over values""" - return (val for key, val in self.iteritems()) - - def update(self, *args, **kwargs): - """Update the dictionary""" - for k, v in six.iteritems(dict(*args, **kwargs)): - self[k] = v - - -docstrings.delete_params("SubDict.parameters", "base") - - -class RcParams(dict): - """A dictionary object including validation - - validating functions are defined and associated with rc parameters in - :data:`defaultParams` - - This class is essentially the same as in maplotlibs - :class:`~matplotlib.RcParams` but has the additional - :meth:`find_and_replace` method.""" - - @property - def validate(self): - """Dictionary with validation methods as values""" - depr = self._all_deprecated - return dict( - (key, val[1]) - for key, val in six.iteritems(self.defaultParams) - if key not in depr - ) - - @property - def descriptions(self): - """The description of each keyword in the rcParams dictionary""" - return { - key: val[2] - for key, val in six.iteritems(self.defaultParams) - if len(val) >= 3 - } - - HEADER = """Configuration parameters of the psyplot module - -You can copy this file (or parts of it) to another path and save it as -psyplotrc.yml. The directory should then be stored in the PSYPLOTCONFIGDIR -environment variable.""" - - msg_depr = "%s is deprecated and replaced with %s; please use the latter." - msg_depr_ignore = "%s is deprecated and ignored. Use %s" - - #: possible connections that shall be called if the rcParams value change - _connections = defaultdict(list) - - #: the names of the entry points that are loaded during the - #: :meth:`load_plugins` method - _plugins = [] - - @property - def _all_deprecated(self): - return set(chain(self._deprecated_ignore_map, self._deprecated_map)) - - @property - def defaultParams(self): - return getattr(self, "_defaultParams", defaultParams) - - @defaultParams.setter - def defaultParams(self, value): - self._defaultParams = value - - @defaultParams.deleter - def defaultParams(self): - del self._defaultParams - - # validate values on the way in - def __init__(self, *args, **kwargs): - """ - Parameters - ---------- - defaultParams: dict - The defaultParams to use (see the :attr:`defaultParams` attribute). - By default, the :attr:`psyplot.config.rcsetup.defaultParams` - dictionary is used - - Other Parameters - ---------------- - *args, **kwargs - Any key-value pair for the initialization of the dictionary - """ - defaultParams = kwargs.pop("defaultParams", None) - if defaultParams is not None: - self.defaultParams = defaultParams - self._deprecated_map = {} - self._deprecated_ignore_map = {} - for k, v in six.iteritems(dict(*args, **kwargs)): - try: - self[k] = v - except (ValueError, RuntimeError): - # force the issue - warn( - _rcparam_warn_str.format( - key=repr(k), value=repr(v), func="__init__" - ) - ) - dict.__setitem__(self, k, v) - - def __setitem__(self, key, val): - key, val = self._get_depreceated(key, val) - if key is None: - return - try: - cval = self.validate[key](val) - except ValueError as ve: - raise ValueError("Key %s: %s" % (key, str(ve))) - dict.__setitem__(self, key, cval) - for func in self._connections.get(key, []): - func(cval) - - def _get_depreceated(self, key, *args): - if key in self._deprecated_map: - alt_key, alt_val = self._deprecated_map[key] - warn(self.msg_depr % (key, alt_key)) - key = alt_key - return key, alt_val(args[0]) if args else None - elif key in self._deprecated_ignore_map: - alt = self._deprecated_ignore_map[key] - warn(self.msg_depr_ignore % (key, alt)) - return None, None - elif key not in self.defaultParams: - raise KeyError( - "%s is not a valid rc parameter. See rcParams.keys() for a " - "list of valid parameters." % (key,) - ) - return key, args[0] if args else None - - def __getitem__(self, key): - key = self._get_depreceated(key)[0] - if key is not None: - return dict.__getitem__(self, key) - - def connect(self, key, func): - """Connect a function to the given formatoption - - Parameters - ---------- - key: str - The rcParams key - func: function - The function that shall be called when the rcParams key changes. - It must accept a single value that is the new value of the - key.""" - key = self._get_depreceated(key)[0] - if key is not None: - self._connections[key].append(func) - - def disconnect(self, key=None, func=None): - """Disconnect the connections to the an rcParams key - - Parameters - ---------- - key: str - The rcParams key. If None, all keys are used - func: function - The function that is connected. If None, all functions are - connected - """ - if key is None: - for key, connections in self._connections.items(): - for conn in connections[:]: - if func is None or conn is func: - connections.remove(conn) - else: - connections = self._connections[key] - for conn in connections[:]: - if func is None or conn is func: - connections.remove(conn) - - def remove(self, key, func): - key = self._get_depreceated(key)[0] - if key is not None: - self._connections[key].remove(func) - - # the default dict `update` does not use __setitem__ - # so rcParams.update(...) (such as in seaborn) side-steps - # all of the validation over-ride update to force - # through __setitem__ - def update(self, *args, **kwargs): - for k, v in six.iteritems(dict(*args, **kwargs)): - try: - self[k] = v - except (ValueError, RuntimeError): - # force the issue - warn( - _rcparam_warn_str.format( - key=repr(k), value=repr(v), func="update" - ) - ) - dict.__setitem__(self, k, v) - - def update_from_defaultParams(self, defaultParams=None, plotters=True): - """Update from the a dictionary like the :attr:`defaultParams` - - Parameters - ---------- - defaultParams: dict - The :attr:`defaultParams` like dictionary. If None, the - :attr:`defaultParams` attribute will be updated - plotters: bool - If True, ``'project.plotters'`` will be updated too""" - if defaultParams is None: - defaultParams = self.defaultParams - self.update( - { - key: val[0] - for key, val in defaultParams.items() - if plotters or key != "project.plotters" - } - ) - - def __repr__(self): - import pprint - - class_name = self.__class__.__name__ - indent = len(class_name) + 1 - repr_split = pprint.pformat( - dict(self), indent=1, width=80 - indent - ).split("\n") - repr_indented = ("\n" + " " * indent).join(repr_split) - return "{0}({1})".format(class_name, repr_indented) - - def __str__(self): - return "\n".join( - "{0}: {1}".format(k, v) for k, v in sorted(self.items()) - ) - - def keys(self): - """ - Return sorted list of keys. - """ - k = list(dict.keys(self)) - k.sort() - return k - - def values(self): - """ - Return values in order of sorted keys. - """ - return [self[k] for k in self.keys()] - - def find_all(self, pattern): - """ - Return the subset of this RcParams dictionary whose keys match, - using :func:`re.search`, the given ``pattern``. - - Parameters - ---------- - pattern: str - pattern as suitable for re.compile - - Returns - ------- - RcParams - RcParams instance with entries that match the given `pattern` - - Notes - ----- - Changes to the returned dictionary are (different from - :meth:`find_and_replace` are *not* propagated to the parent RcParams - dictionary. - - See Also - -------- - find_and_replace""" - pattern_re = re.compile(pattern) - ret = RcParams() - ret.defaultParams = self.defaultParams - ret.update( - (key, value) - for key, value in self.items() - if pattern_re.search(key) - ) - return ret - - @docstrings.dedent - def find_and_replace(self, *args, **kwargs): - """ - Like :meth:`find_all` but the given strings are replaced - - This method returns a dictionary-like object that keeps weak reference - to this rcParams instance. The resulting `SubDict` instance takes the - keys from this rcParams instance but leaves away what is found in - `base_str`. - - ``*args`` and ``**kwargs`` are determined by the :class:`SubDict` - class, where the `base` dictionary is this one. - - Parameters - ---------- - %(SubDict.parameters.no_base)s - - Returns - ------- - SubDict - SubDict with this rcParams instance as reference. - - Examples - -------- - The syntax is the same as for the initialization of the - :class:`SubDict` class:: - - >>> from psyplot import rcParams - >>> d = rcParams.find_and_replace(['plotter.baseplotter.', - ... 'plotter.vector.']) - >>> print(d['title']) - None - - >>> print(d['arrowsize']) - 1.0 - - See Also - -------- - find_all - SubDict""" - return SubDict(self, *args, **kwargs) - - def load_from_file(self, fname=None): - """Update rcParams from user-defined settings - - This function updates the instance with what is found in `fname` - - Parameters - ---------- - fname: str - Path to the yaml configuration file. Possible keys of the - dictionary are defined by :data:`config.rcsetup.defaultParams`. - If None, the :func:`config.rcsetup.psyplot_fname` function is used. - - See Also - -------- - dump_to_file, psyplot_fname""" - fname = fname or psyplot_fname() - if fname and os.path.exists(fname): - with open(fname) as f: - d = yaml.load(f, Loader=yaml.SafeLoader) - self.update(d) - if ( - d.get("project.plotters.user") - and "project.plotters" in self - ): - self["project.plotters"].update(d["project.plotters.user"]) - - def dump( - self, - fname=None, - overwrite=True, - include_keys=None, - exclude_keys=["project.plotters"], - include_descriptions=True, - **kwargs, - ): - """Dump this instance to a yaml file - - Parameters - ---------- - fname: str or None - file name to write to. If None, the string that would be written - to a file is returned - overwrite: bool - If True and `fname` already exists, it will be overwritten - include_keys: None or list of str - Keys in the dictionary to be included. If None, all keys are - included - exclude_keys: list of str - Keys from the :class:`RcParams` instance to be excluded - - Other Parameters - ---------------- - ``**kwargs`` - Any other parameter for the :func:`yaml.dump` function - - Returns - ------- - str or None - if fname is ``None``, the string is returned. Otherwise, ``None`` - is returned - - Raises - ------ - IOError - If `fname` already exists and `overwrite` is False - - See Also - -------- - load_from_file""" - if fname is not None and not overwrite and os.path.exists(fname): - raise IOError( - "%s already exists! Set overwrite=True to overwrite it!" - % (fname) - ) - if six.PY2: - kwargs.setdefault("encoding", "utf-8") - d = { - key: val - for key, val in six.iteritems(self) - if (include_keys is None or key in include_keys) - and key not in exclude_keys - } - kwargs["default_flow_style"] = False - if include_descriptions: - s = yaml.dump(d, **kwargs) - desc = self.descriptions - i = 2 - header = ( - self.HEADER.splitlines() - + ["", "Created with python", ""] - + sys.version.splitlines() - + ["", ""] - ) - lines = ["# " + line for line in header] + s.splitlines() - for line in lines[2:]: - key = line.split(":")[0] - if key in desc: - lines.insert(i, "# " + "\n# ".join(desc[key].splitlines())) - i += 1 - i += 1 - s = "\n".join(lines) - if fname is None: - return s - else: - with open(fname, "w") as f: - f.write(s) - else: - if fname is None: - return yaml.dump(d, **kwargs) - with open(fname, "w") as f: - yaml.dump(d, f, **kwargs) - return None - - def _load_plugin_entrypoints(self): - """Load the modules for the psyplot plugins - - Yields - ------ - importlib.metadata.EntryPoint - The entry point for the psyplot plugin module""" - from psyplot.utils import plugin_entrypoints - - def load_plugin(ep): - try: - ep.module - except AttributeError: # python<3.10 - try: - ep.module = ep.pattern.match(ep.value).group("module") - except AttributeError: # python<3.8 - ep.module = ep.module_name - - if plugins_env == ["no"]: - return False - elif ep.module in exclude_plugins: - return False - elif include_plugins and ep.module not in include_plugins: - return False - return True - - self._plugins = self._plugins or [] - - plugins_env = os.getenv("PSYPLOT_PLUGINS", "").split("::") - include_plugins = [s[4:] for s in plugins_env if s.startswith("yes:")] - exclude_plugins = [s[3:] for s in plugins_env if s.startswith("no:")] - - logger = logging.getLogger(__name__) - - eps = plugin_entrypoints("psyplot", "plugin") - for ep in eps: - if not load_plugin(ep): - logger.debug("Skipping entrypoint %s", ep) - continue - self._plugins.append(str(ep)) - logger.debug("Loading entrypoint %s", ep) - yield ep - - def load_plugins(self, raise_error=False): - """ - Load the plotters and defaultParams from the plugins - - This method loads the `plotters` attribute and `defaultParams` - attribute from the plugins that use the entry point specified by - `group`. Entry points must be objects (or modules) that have a - `defaultParams` and a `plotters` attribute. - - Parameters - ---------- - raise_error: bool - If True, an error is raised when multiple plugins define the same - plotter or rcParams key. Otherwise only a warning is raised""" - - pm_env = os.getenv("PSYPLOT_PLOTMETHODS", "").split("::") - include_pms = [s[4:] for s in pm_env if s.startswith("yes:")] - exclude_pms = [s[3:] for s in pm_env if s.startswith("no:")] - - logger = logging.getLogger(__name__) - - plotters = self["project.plotters"] - def_plots = {"default": list(plotters)} - defaultParams = self.defaultParams - def_keys = {"default": defaultParams} - - def register_pm(ep, name): - full_name = "%s:%s" % (ep.module, name) - ret = True - if pm_env == ["no"]: - ret = False - elif name in exclude_pms or full_name in exclude_pms: - ret = False - elif include_pms and ( - name not in include_pms and full_name not in include_pms - ): - ret = False - if not ret: - logger.debug("Skipping plot method %s", full_name) - return ret - - for ep in self._load_plugin_entrypoints(): - try: - plugin_mod = ep.load() - except (ModuleNotFoundError, ImportError): - logger.debug("Failed to import %s!" % (ep,), exc_info=True) - logger.warning("Failed to import %s!" % (ep,)) - continue - rc = plugin_mod.rcParams - - # load the plotters - plugin_plotters = { - key: val - for key, val in rc.get("project.plotters", {}).items() - if register_pm(ep, key) - } - already_defined = set(plotters).intersection(plugin_plotters) - if already_defined: - msg = ( - "Error while loading psyplot plugin %s! The " - "following plotters have already been " - "defined" - ) % ep - msg += "and will be overwritten:" if not raise_error else ":" - msg += "\n" + "\n".join( - chain.from_iterable( - ( - ( - "%s by %s" % (key, plugin) - for plugin, keys in def_plots.items() - if key in keys - ) - for key in already_defined - ) - ) - ) - if raise_error: - raise ImportError(msg) - else: - warn(msg) - for d in plugin_plotters.values(): - d["plugin"] = ep.module - plotters.update(plugin_plotters) - def_plots[ep] = list(plugin_plotters) - - # load the defaultParams keys - plugin_defaultParams = rc.defaultParams - already_defined = set(defaultParams).intersection( - plugin_defaultParams - ) - {"project.plotters"} - if already_defined: - msg = ( - "Error while loading psyplot plugin %s! The " - "following default keys have already been " - "defined:" - ) % ep - msg += "\n" + "\n".join( - chain.from_iterable( - ( - ( - "%s by %s" % (key, plugin) - for plugin, keys in def_keys.items() - if key in keys - ) - for key in already_defined - ) - ) - ) - if raise_error: - raise ImportError(msg) - else: - warn(msg) - update_keys = set(plugin_defaultParams) - {"project.plotters"} - def_keys[ep] = update_keys - self.defaultParams.update( - {key: plugin_defaultParams[key] for key in update_keys} - ) - - # load the rcParams (without validation) - super(RcParams, self).update({key: rc[key] for key in update_keys}) - - # add the deprecated keys - self._deprecated_ignore_map.update(rc._deprecated_ignore_map) - self._deprecated_map.update(rc._deprecated_map) - - def copy(self): - """Make sure, the right class is retained""" - return RcParams(self) - - @contextlib.contextmanager - def catch(self): - """Context manager to reset the rcParams afterwards - - Usage:: - - rcParams['some_key'] = 0 - with rcParams.catch(): - rcParams['some_key'] = 1 - assert rcParams['some_key'] == 1 - assert rcParams['some_key'] == 0 - """ - save = dict(self) - yield - super().update(save) # reset settings - - -def psyplot_fname(env_key="PSYPLOTRC", fname="psyplotrc.yml", if_exists=True): - """ - Get the location of the config file. - - The file location is determined in the following order - - - `$PWD/psyplotrc.yml` - - - environment variable `PSYPLOTRC` (pointing to the file location or a - directory containing the file `psyplotrc.yml`) - - - `$PSYPLOTCONFIGDIR/psyplot` - - - On Linux and osx, - - - `$HOME/.config/psyplot/psyplotrc.yml` - - - On other platforms, - - - `$HOME/.psyplot/psyplotrc.yml` if `$HOME` is defined. - - - Lastly, it looks in `$PSYPLOTDATA/psyplotrc.yml` for a - system-defined copy. - - Parameters - ---------- - env_key: str - The environment variable that can be used for the configuration - directory - fname: str - The name of the configuration file - if_exists: bool - If True, the path is only returned if the file exists - - Returns - ------- - None or str - None, if no file could be found and `if_exists` is True, else the path - to the psyplot configuration file - - Notes - ----- - This function is motivated by the :func:`matplotlib.matplotlib_fname` - function""" - cwd = os.getcwd() - full_fname = os.path.join(cwd, fname) - if os.path.exists(full_fname): - return full_fname - - if env_key in os.environ: - path = os.environ[env_key] - if os.path.exists(path): - if os.path.isdir(path): - full_fname = os.path.join(path, fname) - if os.path.exists(full_fname): - return full_fname - else: - return path - - configdir = get_configdir() - if configdir is not None: - full_fname = os.path.join(configdir, fname) - if os.path.exists(full_fname) or not if_exists: - return full_fname - - return None - - -def get_configdir(name="psyplot", env_key="PSYPLOTCONFIGDIR"): - """ - Return the string representing the configuration directory. - - The directory is chosen as follows: - - 1. If the `env_key` environment variable is supplied, choose that. - - 2a. On Linux and osx, choose ``'$HOME/.config/' + name``. - - 2b. On other platforms, choose ``'$HOME/.' + name``. - - 3. If the chosen directory exists, use that as the - configuration directory. - 4. A directory: return None. - - Parameters - ---------- - name: str - The name of the program - env_key: str - The environment variable that can be used for the configuration - directory - - Notes - ----- - This function is motivated by the :func:`matplotlib.matplotlib_fname` - function""" - configdir = os.environ.get(env_key) - if configdir is not None: - return os.path.abspath(configdir) - - p = None - h = _get_home() - if ( - sys.platform.startswith("linux") or sys.platform == "darwin" - ) and h is not None: - p = os.path.join(h, ".config/" + name) - elif h is not None: - p = os.path.join(h, "." + name) - - if not os.path.exists(p): - os.makedirs(p, exist_ok=True) - return p - - -def validate_path_exists(s): - """If s is a path, return s, else False""" - if s is None: - return None - if os.path.exists(s): - return s - else: - raise ValueError('"%s" should be a path but it does not exist' % s) - - -def validate_files_exist(files): - """Validate if all pathnames in a given list exists""" - return [validate_str(fn) and validate_path_exists(fn) for fn in files] - - -def validate_dict(d): - """Validate a dictionary - - Parameters - ---------- - d: dict or str - If str, it must be a path to a yaml file - - Returns - ------- - dict - - Raises - ------ - ValueError""" - try: - return dict(d) - except TypeError: - d = validate_path_exists(d) - try: - with open(d) as f: - return dict(yaml.load(f, Loader=yaml.SafeLoader)) - except Exception: - raise ValueError("Could not convert {} to dictionary!".format(d)) - - -def validate_bool_maybe_none(b): - "Convert b to a boolean or raise" - if isinstance(b, six.string_types): - b = b.lower() - if b is None or b == "none": - return None - return validate_bool(b) - - -def validate_bool(b): - """Convert b to a boolean or raise""" - if isinstance(b, six.string_types): - b = b.lower() - if b in ("t", "y", "yes", "on", "true", "1", 1, True): - return True - elif b in ("f", "n", "no", "off", "false", "0", 0, False): - return False - else: - raise ValueError('Could not convert "%s" to boolean' % b) - - -def validate_str(s): - """Validate a string - - Parameters - ---------- - s: str - - Returns - ------- - str - - Raises - ------ - ValueError""" - if not isinstance(s, six.string_types): - raise ValueError("Did not found string!") - return six.text_type(s) - - -def validate_stringlist(s): - """Validate a list of strings - - Parameters - ---------- - val: iterable of strings - - Returns - ------- - list - list of str - - Raises - ------ - ValueError""" - if isinstance(s, six.string_types): - return [six.text_type(v.strip()) for v in s.split(",") if v.strip()] - else: - try: - return list(map(validate_str, s)) - except TypeError as e: - raise ValueError(e.message) - - -def validate_stringset(*args, **kwargs): - """Validate a set of strings - - Parameters - ---------- - val: iterable of strings - - Returns - ------- - set - set of str - - Raises - ------ - ValueError""" - return set(validate_stringlist(*args, **kwargs)) - - -#: :class:`dict` with default values and validation functions -defaultParams = { - # user defined plotter keys - "plotter.user": [ - {}, - validate_dict, - inspect.cleandoc( - """ - formatoption keys and values that are defined by the user to be used by - the specified plotters. For example to modify the title of all - :class:`psyplot.plotter.maps.FieldPlotter` instances, set - ``{'plotter.fieldplotter.title': 'my title'}``""" - ), - ], - "gridweights.use_cdo": [ - None, - validate_bool_maybe_none, - "Boolean flag to control whether CDOs (Climate Data Operators) should " - "be used to calculate grid weights. If None, they are tried to be " - "used.", - ], - # decoder - "decoder.x": [ - set(), - validate_stringset, - "names that shall be interpreted as the longitudinal x dim", - ], - "decoder.y": [ - set(), - validate_stringset, - "names that shall be interpreted as the latitudinal y dim", - ], - "decoder.z": [ - set(), - validate_stringset, - "names that shall be interpreted as the vertical z dim", - ], - "decoder.t": [ - {"time"}, - validate_stringset, - "names that shall be interpreted as the time dimension", - ], - "decoder.interp_kind": [ - "linear", - validate_str, - "interpolation method to calculate 2D-bounds (see the `kind` parameter" - "in the :meth:`psyplot.data.CFDecoder.get_plotbounds` method)", - ], - # specify automatic drawing and showing of figures - "auto_draw": [ - True, - validate_bool, - ( - "Automatically draw the figures if the draw keyword in the " - "update and start_update methods is None" - ), - ], - "auto_show": [ - False, - validate_bool, - ( - "Automatically show the figures after the update and" - "start_update methods" - ), - ], - # data - "datapath": [None, validate_path_exists, "path for supplementary data"], - # list settings - "lists.auto_update": [ - True, - validate_bool, - "default value (boolean) for the auto_update " - "parameter in the initialization of Plotter, " - "Project, etc. instances", - ], - # project settings - # auto_import: If True the plotters in project,plotters are automatically - # imported - "project.auto_import": [ - False, - validate_bool, - "boolean controlling whether all plotters " - "specified in the project.plotters item will be " - "automatically imported when importing the " - "psyplot.project module", - ], - "project.import_seaborn": [ - None, - validate_bool_maybe_none, - "boolean controlling whether the seaborn module shall be imported " - "when importing the project module. If None, it is only tried to " - "import the module.", - ], - "project.plotters": [ - {}, - validate_dict, - "mapping from identifier to plotter definitions for the Project class." - " See the :func:`psyplot.project.register_plotter` function for " - "possible keywords and values. See " - ":attr:`psyplot.project.registered_plotters` for examples.", - ], - "project.plotters.user": [ - {}, - validate_dict, - "Plot methods that are defined by the user and overwrite those in the" - "``'project.plotters'`` key. Use this if you want to define your own " - "plotters without writing a plugin", - ], - # presets - "presets.trusted": [ - [], - validate_files_exist, - "A list of filenames with trusted presets", - ], -} - - -_rcparam_warn_str = ( - "Trying to set {key} to {value} via the {func} " - "method of RcParams which does not validate cleanly. " -) - - -_seq_err_msg = ( - "You must supply exactly {n:d} values, you provided " "{num:d} values: {s}" -) - - -_str_err_msg = ( - "You must supply exactly {n:d} comma-separated values, " - "you provided " - "{num:d} comma-separated values: {s}" -) - -#: :class:`~psyplot.config.rcsetup.RcParams` instance that stores default -#: formatoptions and configuration settings. -rcParams = RcParams() -rcParams.update_from_defaultParams() - -defaultParams_orig = defaultParams.copy() diff --git a/psyplot/data.py b/psyplot/data.py deleted file mode 100755 index 61f5571..0000000 --- a/psyplot/data.py +++ /dev/null @@ -1,5734 +0,0 @@ -"""Data management core routines of psyplot.""" - -# SPDX-FileCopyrightText: 2016-2024 University of Lausanne -# SPDX-FileCopyrightText: 2020-2021 Helmholtz-Zentrum Geesthacht - -# SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH -# -# SPDX-License-Identifier: LGPL-3.0-only - -from __future__ import division - -import datetime as dt -import inspect -import logging -import os -import os.path as osp -import re -from collections import defaultdict -from functools import partial -from glob import glob -from importlib import import_module -from itertools import chain, count, cycle, islice, product, repeat, starmap -from queue import Queue -from threading import Thread -from warnings import warn - -import numpy as np -import six -import xarray as xr -import xarray.backends.api as xarray_api -from pandas import to_datetime -from xarray.core.formatting import first_n_items, format_item -from xarray.core.utils import NDArrayMixin - -import psyplot.utils as utils -from psyplot.config.rcsetup import rcParams, safe_list -from psyplot.docstring import dedent, docstrings -from psyplot.utils import isstring -from psyplot.warning import PsyPlotRuntimeWarning - -try: - import dask # noqa: F401 - - with_dask = True -except ImportError: - with_dask = False - -try: - import xarray.backends.plugins as xr_plugins -except ImportError: - xr_plugins = None # type: ignore - - -# No data variable. This is used for filtering if an attribute could not have -# been accessed -_NODATA = object - - -VARIABLELABEL = "variable" - - -logger = logging.getLogger(__name__) - - -_ds_counter = count(1) - -xr_version = tuple(map(int, xr.__version__.split(".")[:2])) - - -def _no_auto_update_getter(self): - """:class:`bool`. Boolean controlling whether the :meth:`start_update` - method is automatically called by the :meth:`update` method - - - Examples - -------- - You can disable the automatic update via - - >>> with data.no_auto_update: - ... data.update(time=1) - ... data.start_update() - - To permanently disable the automatic update, simply set - - >>> data.no_auto_update = True - >>> data.update(time=1) - >>> data.no_auto_update = False # reenable automatical update""" - if getattr(self, "_no_auto_update", None) is not None: - return self._no_auto_update - else: - self._no_auto_update = utils._TempBool() - return self._no_auto_update - - -def _infer_interval_breaks(coord): - """ - >>> _infer_interval_breaks(np.arange(5)) - array([-0.5, 0.5, 1.5, 2.5, 3.5, 4.5]) - - Taken from xarray.plotting.plot module - """ - coord = np.asarray(coord) - deltas = 0.5 * (coord[1:] - coord[:-1]) - first = coord[0] - deltas[0] - last = coord[-1] + deltas[-1] - return np.r_[[first], coord[:-1] + deltas, [last]] - - -def _get_variable_names(arr): - """Return the variable names of an array""" - if VARIABLELABEL in arr.dims: - return arr.coords[VARIABLELABEL].tolist() - else: - return arr.name - - -def _get_dims(arr): - """Return all dimensions but the :attr:`VARIABLELABEL`""" - return tuple(filter(lambda d: d != VARIABLELABEL, arr.dims)) - - -def _open_store(store_mod, store_cls, fname): - try: - return getattr(import_module(store_mod), store_cls).open(fname) - except AttributeError: - return getattr(import_module(store_mod), store_cls)(fname) - - -def _fix_times(dims): - # xarray 0.16 fails with pandas 1.1.0 for datetime, see - # https://github.com/pydata/xarray/issues/4283 - for key, val in dims.items(): - if np.issubdtype(np.asarray(val).dtype, np.datetime64): - dims[key] = to_datetime([val])[0] - - -@docstrings.get_sections(base="setup_coords") -@dedent -def setup_coords(arr_names=None, sort=[], dims={}, **kwargs): - """ - Sets up the arr_names dictionary for the plot - - Parameters - ---------- - arr_names: string, list of strings or dictionary - Set the unique array names of the resulting arrays and (optionally) - dimensions. - - - if string: same as list of strings (see below). Strings may - include {0} which will be replaced by a counter. - - list of strings: those will be used for the array names. The final - number of dictionaries in the return depend in this case on the - `dims` and ``**furtherdims`` - - dictionary: - Then nothing happens and an :class:`dict` version of - `arr_names` is returned. - sort: list of strings - This parameter defines how the dictionaries are ordered. It has no - effect if `arr_names` is a dictionary (use a - :class:`dict` for that). It can be a list of - dimension strings matching to the dimensions in `dims` for the - variable. - dims: dict - Keys must be variable names of dimensions (e.g. time, level, lat or - lon) or 'name' for the variable name you want to choose. - Values must be values of that dimension or iterables of the values - (e.g. lists). Note that strings will be put into a list. - For example dims = {'name': 't2m', 'time': 0} will result in one plot - for the first time step, whereas dims = {'name': 't2m', 'time': [0, 1]} - will result in two plots, one for the first (time == 0) and one for the - second (time == 1) time step. - ``**kwargs`` - The same as `dims` (those will update what is specified in `dims`) - - Returns - ------- - dict - A mapping from the keys in `arr_names` and to dictionaries. Each - dictionary corresponds defines the coordinates of one data array to - load""" - try: - return dict(arr_names) - except (ValueError, TypeError): - # ValueError for cydict, TypeError for dic - pass - if arr_names is None: - arr_names = repeat("arr{0}") - elif isstring(arr_names): - arr_names = repeat(arr_names) - dims = dict(dims) - for key, val in six.iteritems(kwargs): - dims.setdefault(key, val) - sorted_dims = dict() - if sort: - for key in sort: - sorted_dims[key] = dims.pop(key) - for key, val in six.iteritems(dims): - sorted_dims[key] = val - else: - # make sure, it is first sorted for the variable names - if "name" in dims: - sorted_dims["name"] = None - for key, val in sorted(dims.items()): - sorted_dims[key] = val - for key, val in six.iteritems(kwargs): - sorted_dims.setdefault(key, val) - for key, val in six.iteritems(sorted_dims): - sorted_dims[key] = iter(safe_list(val)) - return dict( - [ - (arr_name.format(i), dict(zip(sorted_dims.keys(), dim_tuple))) - for i, (arr_name, dim_tuple) in enumerate( - zip(arr_names, product(*map(list, sorted_dims.values()))) - ) - ] - ) - - -def to_slice(arr): - """Test whether `arr` is an integer array that can be replaced by a slice - - Parameters - ---------- - arr: numpy.array - Numpy integer array - - Returns - ------- - slice or None - If `arr` could be converted to an array, this is returned, otherwise - `None` is returned - - See Also - -------- - get_index_from_coord""" - if isinstance(arr, slice): - return arr - if len(arr) == 1: - return slice(arr[0], arr[0] + 1) - step = np.unique(arr[1:] - arr[:-1]) - if len(step) == 1: - return slice(arr[0], arr[-1] + step[0], step[0]) - - -def get_index_from_coord(coord, base_index): - """Function to return the coordinate as integer, integer array or slice - - If `coord` is zero-dimensional, the corresponding integer in `base_index` - will be supplied. Otherwise it is first tried to return a slice, if that - does not work an integer array with the corresponding indices is returned. - - Parameters - ---------- - coord: xarray.Coordinate or xarray.Variable - Coordinate to convert - base_index: pandas.Index - The base index from which the `coord` was extracted - - Returns - ------- - int, array of ints or slice - The indexer that can be used to access the `coord` in the - `base_index` - """ - try: - values = coord.values - except AttributeError: - values = coord - if values.ndim == 0: - return base_index.get_loc(values[()]) - if len(values) == len(base_index) and (values == base_index).all(): - return slice(None) - values = np.array(list(map(lambda i: base_index.get_loc(i), values))) - return to_slice(values) or values - - -#: mapping that translates datetime format strings to regex patterns -t_patterns = { - "%Y": "[0-9]{4}", - "%m": "[0-9]{1,2}", - "%d": "[0-9]{1,2}", - "%H": "[0-9]{1,2}", - "%M": "[0-9]{1,2}", - "%S": "[0-9]{1,2}", -} - - -@docstrings.get_sections(base="get_tdata") -@dedent -def get_tdata(t_format, files): - """ - Get the time information from file names - - Parameters - ---------- - t_format: str - The string that can be used to get the time information in the files. - Any numeric datetime format string (e.g. %Y, %m, %H) can be used, but - not non-numeric strings like %b, etc. See [1]_ for the datetime format - strings - files: list of str - The that contain the time informations - - Returns - ------- - pandas.Index - The time coordinate - list of str - The file names as they are sorten in the returned index - - References - ---------- - .. [1] https://docs.python.org/2/library/datetime.html""" - - def median(arr): - return arr.min() + (arr.max() - arr.min()) / 2 - - import re - - from pandas import Index - - t_pattern = t_format - for fmt, patt in t_patterns.items(): - t_pattern = t_pattern.replace(fmt, patt) - t_pattern = re.compile(t_pattern) - time = list(range(len(files))) - for i, f in enumerate(files): - time[i] = median( - np.array( - list( - map( - lambda s: np.datetime64( - dt.datetime.strptime(s, t_format) - ), - t_pattern.findall(f), - ) - ) - ) - ) - ind = np.argsort(time) # sort according to time - files = np.array(files)[ind] - time = np.array(time)[ind] - return to_datetime(Index(time, name="time")), files - - -docstrings.get_sections( - xr.Dataset.to_netcdf.__doc__, "xarray.Dataset.to_netcdf" -) - - -@docstrings.dedent -def to_netcdf(ds, *args, **kwargs): - """ - Store the given dataset as a netCDF file - - This functions works essentially the same as the usual - :meth:`xarray.Dataset.to_netcdf` method but can also encode absolute time - units - - Parameters - ---------- - ds: xarray.Dataset - The dataset to store - %(xarray.Dataset.to_netcdf.parameters)s - """ - to_update = {} - for v, obj in six.iteritems(ds.variables): - units = obj.attrs.get("units", obj.encoding.get("units", None)) - if units == "day as %Y%m%d.%f" and np.issubdtype( - obj.dtype, np.datetime64 - ): - to_update[v] = xr.Variable( - obj.dims, - AbsoluteTimeEncoder(obj), - attrs=obj.attrs.copy(), - encoding=obj.encoding, - ) - to_update[v].attrs["units"] = units - if to_update: - ds = ds.copy() - ds.update(to_update) - return xarray_api.to_netcdf(ds, *args, **kwargs) - - -def _get_fname_netCDF4(store): - """Try to get the file name from the NetCDF4DataStore store""" - return getattr(store, "_filename", None) - - -def _get_fname_scipy(store): - """Try to get the file name from the ScipyDataStore store""" - try: - return store.ds.filename - except AttributeError: - return None - - -def _get_fname_nio(store): - """Try to get the file name from the NioDataStore store""" - try: - f = store.ds.file - except AttributeError: - return None - try: - return f.path - except AttributeError: - return None - - -class Signal(object): - """Signal to connect functions to a specific event - - This class behaves almost similar to PyQt's - :class:`PyQt4.QtCore.pyqtBoundSignal` - """ - - instance = None - owner = None - - def __init__(self, name=None, cls_signal=False): - self.name = name - self.cls_signal = cls_signal - self._connections = [] - - def connect(self, func): - if func not in self._connections: - self._connections.append(func) - - def emit(self, *args, **kwargs): - if not getattr(self.owner, "block_signals", False) and not getattr( - self.instance, "block_signals", False - ): - logger.debug("Emitting signal %s", self.name) - for func in self._connections[:]: - logger.debug("Calling %s", func) - func(*args, **kwargs) - - def disconnect(self, func=None): - """Disconnect a function call to the signal. If None, all connections - are disconnected""" - if func is None: - self._connections = [] - else: - self._connections.remove(func) - - def __get__(self, instance, owner): - self.owner = owner - if instance is None or self.cls_signal: - return self - ret = getattr(instance, self.name, None) - if ret is None: - setattr(instance, self.name, Signal(self.name)) - ret = getattr(instance, self.name, None) - ret.instance = instance - return ret - - -#: functions to use to extract the file name from a data store -get_fname_funcs = [_get_fname_netCDF4, _get_fname_scipy, _get_fname_nio] - - -@docstrings.get_sections(base="get_filename_ds") -@docstrings.dedent -def get_filename_ds(ds, dump=True, paths=None, **kwargs): - """ - Return the filename of the corresponding to a dataset - - This method returns the path to the `ds` or saves the dataset - if there exists no filename - - Parameters - ---------- - ds: xarray.Dataset - The dataset you want the path information for - dump: bool - If True and the dataset has not been dumped so far, it is dumped to a - temporary file or the one generated by `paths` is used - paths: iterable or True - An iterator over filenames to use if a dataset has no filename. - If paths is ``True``, an iterator over temporary files will be - created without raising a warning - - Other Parameters - ---------------- - ``**kwargs`` - Any other keyword for the :func:`to_netcdf` function - %(xarray.Dataset.to_netcdf.parameters)s - - Returns - ------- - str or None - None, if the dataset has not yet been dumped to the harddisk and - `dump` is False, otherwise the complete the path to the input - file - str - The module of the :class:`xarray.backends.common.AbstractDataStore` - instance that is used to hold the data - str - The class name of the - :class:`xarray.backends.common.AbstractDataStore` instance that is - used to open the data - """ - from tempfile import NamedTemporaryFile - - # if already specified, return that filename - if ds.psy._filename is not None: - return tuple([ds.psy._filename] + list(ds.psy.data_store)) - - def dump_nc(): - # make sure that the data store is not closed by providing a - # write argument - if xr_version < (0, 11): - kwargs.setdefault("writer", xarray_api.ArrayWriter()) - store = to_netcdf(ds, fname, **kwargs) - else: - # `writer` parameter was removed by - # https://github.com/pydata/xarray/pull/2261 - kwargs.setdefault("multifile", True) - store = to_netcdf(ds, fname, **kwargs)[1] - store_mod = store.__module__ - store_cls = store.__class__.__name__ - ds._file_obj = store - return store_mod, store_cls - - def tmp_it(): - while True: - yield NamedTemporaryFile(suffix=".nc").name - - fname = None - if paths is True or (dump and paths is None): - paths = tmp_it() - elif paths is not None: - if isstring(paths): - paths = iter([paths]) - else: - paths = iter(paths) - store_mod, store_cls = ds.psy.data_store - if "source" in ds.encoding: - fname = ds.encoding["source"] - store_mod = None - store_cls = None - - # check if paths is provided and if yes, save the file - if fname is None and paths is not None: - fname = next(paths, None) - if dump and fname is not None: - store_mod, store_cls = dump_nc() - - ds.psy.filename = fname - ds.psy.data_store = (store_mod, store_cls) - - return fname, store_mod, store_cls - - -class CFDecoder(object): - """ - Class that interpretes the coordinates and attributes accordings to - cf-conventions""" - - _registry = [] - - @property - def logger(self): - """:class:`logging.Logger` of this instance""" - try: - return self._logger - except AttributeError: - name = "%s.%s" % (self.__module__, self.__class__.__name__) - self._logger = logging.getLogger(name) - self.logger.debug("Initializing...") - return self._logger - - @logger.setter - def logger(self, value): - self._logger = value - - def __init__(self, ds=None, x=None, y=None, z=None, t=None): - self.ds = ds - self.x = rcParams["decoder.x"].copy() if x is None else set(x) - self.y = rcParams["decoder.y"].copy() if y is None else set(y) - self.z = rcParams["decoder.z"].copy() if z is None else set(z) - self.t = rcParams["decoder.t"].copy() if t is None else set(t) - - @staticmethod - def register_decoder(decoder_class, pos=0): - """Register a new decoder - - This function registeres a decoder class to use - - Parameters - ---------- - decoder_class: type - The class inherited from the :class:`CFDecoder` - pos: int - The position where to register the decoder (by default: the first - position""" - CFDecoder._registry.insert(pos, decoder_class) - - @classmethod - @docstrings.get_sections( - base="CFDecoder.can_decode", sections=["Parameters", "Returns"] - ) - def can_decode(cls, ds, var): - """ - Class method to determine whether the object can be decoded by this - decoder class. - - Parameters - ---------- - ds: xarray.Dataset - The dataset that contains the given `var` - var: xarray.Variable or xarray.DataArray - The array to decode - - Returns - ------- - bool - True if the decoder can decode the given array `var`. Otherwise - False - - Notes - ----- - The default implementation returns True for any argument. Subclass this - method to be specific on what type of data your decoder can decode - """ - return True - - @classmethod - @docstrings.dedent - def get_decoder(cls, ds, var, *args, **kwargs): - """ - Class method to get the right decoder class that can decode the - given dataset and variable - - Parameters - ---------- - %(CFDecoder.can_decode.parameters)s - - Returns - ------- - CFDecoder - The decoder for the given dataset that can decode the variable - `var`""" - for decoder_cls in cls._registry: - if decoder_cls.can_decode(ds, var): - return decoder_cls(ds, *args, **kwargs) - return CFDecoder(ds, *args, **kwargs) - - @staticmethod - @docstrings.get_sections( - base="CFDecoder.decode_coords", sections=["Parameters", "Returns"] - ) - def decode_coords(ds, gridfile=None): - """ - Sets the coordinates and bounds in a dataset - - This static method sets those coordinates and bounds that are marked - marked in the netCDF attributes as coordinates in :attr:`ds` (without - deleting them from the variable attributes because this information is - necessary for visualizing the data correctly) - - Parameters - ---------- - ds: xarray.Dataset - The dataset to decode - gridfile: str - The path to a separate grid file or a xarray.Dataset instance which - may store the coordinates used in `ds` - - Returns - ------- - xarray.Dataset - `ds` with additional coordinates""" - - def add_attrs(obj): - if "coordinates" in obj.attrs: - extra_coords.update(obj.attrs["coordinates"].split()) - obj.encoding["coordinates"] = obj.attrs.pop("coordinates") - if "grid_mapping" in obj.attrs: - extra_coords.add(obj.attrs["grid_mapping"]) - if "bounds" in obj.attrs: - extra_coords.add(obj.attrs["bounds"]) - - if gridfile is not None and not isinstance(gridfile, xr.Dataset): - gridfile = open_dataset(gridfile) - extra_coords = set(ds.coords) - for k, v in six.iteritems(ds.variables): - add_attrs(v) - add_attrs(ds) - if gridfile is not None: - ds.update( - { - k: v - for k, v in six.iteritems(gridfile.variables) - if k in extra_coords - } - ) - if xr_version < (0, 11): - ds.set_coords( - extra_coords.intersection(ds.variables), inplace=True - ) - else: - ds._coord_names.update(extra_coords.intersection(ds.variables)) - return ds - - @docstrings.get_sections( - base="CFDecoder.is_unstructured", sections=["Parameters", "Returns"] - ) - @docstrings.get_sections( - base="CFDecoder.get_cell_node_coord", - sections=["Parameters", "Returns"], - ) - @dedent - def get_cell_node_coord(self, var, coords=None, axis="x", nans=None): - """ - Checks whether the bounds in the variable attribute are triangular - - Parameters - ---------- - var: xarray.Variable or xarray.DataArray - The variable to check - coords: dict - Coordinates to use. If None, the coordinates of the dataset in the - :attr:`ds` attribute are used. - axis: {'x', 'y'} - The spatial axis to check - nans: {None, 'skip', 'only'} - Determines whether values with nan shall be left (None), skipped - (``'skip'``) or shall be the only one returned (``'only'``) - - Returns - ------- - xarray.DataArray or None - the bounds corrdinate (if existent)""" - if coords is None: - coords = self.ds.coords - axis = axis.lower() - get_coord = self.get_x if axis == "x" else self.get_y - coord = get_coord(var, coords=coords) - if coord is not None: - bounds = self._get_coord_cell_node_coord( - coord, coords, nans, var=var - ) - if bounds is None: - bounds = self.get_plotbounds(coord) - if bounds.ndim == 1: - dim0 = coord.dims[-1] - bounds = xr.DataArray( - np.dstack([bounds[:-1], bounds[1:]])[0], - dims=(dim0, "_bnds"), - attrs=coord.attrs.copy(), - name=coord.name + "_bnds", - ) - elif bounds.ndim == 2: - warn("2D bounds are not yet sufficiently tested!") - bounds = xr.DataArray( - np.dstack( - [ - bounds[1:, 1:].ravel(), - bounds[1:, :-1].ravel(), - bounds[:-1, :-1].ravel(), - bounds[:-1, 1:].ravel(), - ] - )[0], - dims=("".join(var.dims[-2:]), "_bnds"), - attrs=coord.attrs.copy(), - name=coord.name + "_bnds", - ) - else: - raise NotImplementedError( - "More than 2D-bounds are not supported" - ) - if bounds is not None and bounds.shape[-1] == 2: - # normal CF-Conventions for rectangular grids - arr = bounds.values - if axis == "y": - stacked = np.c_[ - arr[..., :1], arr[..., :1], arr[..., 1:], arr[..., 1:] - ] - if bounds.ndim == 2: - stacked = np.repeat( - stacked.reshape((-1, 4)), - len(self.get_x(var, coords)), - axis=0, - ) - else: - stacked = stacked.reshape((-1, 4)) - else: - stacked = np.c_[arr, arr[..., ::-1]] - if bounds.ndim == 2: - stacked = np.tile( - stacked, (len(self.get_y(var, coords)), 1) - ) - else: - stacked = stacked.reshape((-1, 4)) - bounds = xr.DataArray( - stacked, - dims=("cell", bounds.dims[1]), - name=bounds.name, - attrs=bounds.attrs, - ) - - return bounds - return None - - docstrings.delete_params( - "CFDecoder.get_cell_node_coord.parameters", "var", "axis" - ) - - @docstrings.dedent - def _get_coord_cell_node_coord( - self, coord, coords=None, nans=None, var=None - ): - """ - Get the boundaries of an unstructed coordinate - - Parameters - ---------- - coord: xr.Variable - The coordinate whose bounds should be returned - %(CFDecoder.get_cell_node_coord.parameters.no_var|axis)s - - Returns - ------- - %(CFDecoder.get_cell_node_coord.returns)s - """ - bounds = coord.attrs.get("bounds") - if bounds is not None: - bounds = self.ds.coords.get(bounds) - if bounds is not None: - if coords is not None: - bounds = bounds.sel( - **{ - key: coords[key] - for key in set(coords).intersection(bounds.dims) - } - ) - if nans is not None and var is None: - raise ValueError("Need the variable to deal with NaN!") - elif nans is None: - pass - elif nans == "skip": - dims = [dim for dim in set(var.dims) - set(bounds.dims)] - mask = var.notnull().all(list(dims)) if dims else var.notnull() - try: - bounds = bounds[mask.values] - except IndexError: # 3D bounds - bounds = bounds.where(mask) - elif nans == "only": - dims = [dim for dim in set(var.dims) - set(bounds.dims)] - mask = var.isnull().all(list(dims)) if dims else var.isnull() - bounds = bounds[mask.values] - else: - raise ValueError( - "`nans` must be either None, 'skip', or 'only'! " - "Not {0}!".format(str(nans)) - ) - return bounds - - @docstrings.get_sections( - base="CFDecoder._check_unstructured_bounds", - sections=["Parameters", "Returns"], - ) - @docstrings.dedent - def _check_unstructured_bounds( - self, var, coords=None, axis="x", nans=None - ): - """ - Checks whether the bounds in the variable attribute are triangular - - Parameters - ---------- - %(CFDecoder.get_cell_node_coord.parameters)s - - Returns - ------- - bool or None - True, if unstructered, None if it could not be determined - xarray.Coordinate or None - the bounds corrdinate (if existent)""" - # !!! WILL BE REMOVED IN THE NEAR FUTURE! !!! - bounds = self.get_cell_node_coord(var, coords, axis=axis, nans=nans) - if bounds is not None: - return bounds.shape[-1] == 3, bounds - else: - return None, None - - @docstrings.dedent - def is_unstructured(self, var): - """ - Test if a variable is on an unstructered grid - - Parameters - ---------- - %(CFDecoder.is_unstructured.parameters)s - - Returns - ------- - %(CFDecoder.is_unstructured.returns)s - - Notes - ----- - Currently this is the same as :meth:`is_unstructured` method, but may - change in the future to support hexagonal grids""" - if str(var.attrs.get("grid_type")) == "unstructured": - return True - xcoord = self.get_x(var) - if xcoord is not None: - bounds = self._get_coord_cell_node_coord(xcoord) - if ( - bounds is not None - and bounds.ndim == 2 - and bounds.shape[-1] > 2 - ): - return True - - @docstrings.dedent - def is_circumpolar(self, var): - """ - Test if a variable is on a circumpolar grid - - Parameters - ---------- - %(CFDecoder.is_unstructured.parameters)s - - Returns - ------- - %(CFDecoder.is_unstructured.returns)s""" - xcoord = self.get_x(var) - return xcoord is not None and xcoord.ndim == 2 - - def get_variable_by_axis(self, var, axis, coords=None): - """Return the coordinate matching the specified axis - - This method uses to ``'axis'`` attribute in coordinates to return the - corresponding coordinate of the given variable - - Possible types - -------------- - var: xarray.Variable - The variable to get the dimension for - axis: {'x', 'y', 'z', 't'} - The axis string that identifies the dimension - coords: dict - Coordinates to use. If None, the coordinates of the dataset in the - :attr:`ds` attribute are used. - - Returns - ------- - xarray.Coordinate or None - The coordinate for `var` that matches the given `axis` or None if - no coordinate with the right `axis` could be found. - - Notes - ----- - This is a rather low-level function that only interpretes the - CFConvention. It is used by the :meth:`get_x`, - :meth:`get_y`, :meth:`get_z` and :meth:`get_t` methods - - Warning - ------- - If None of the coordinates have an ``'axis'`` attribute, we use the - ``'coordinate'`` attribute of `var` (if existent). - Since however the CF Conventions do not determine the order on how - the coordinates shall be saved, we try to use a pattern matching - for latitude (``'lat'``) and longitude (``lon'``). If this patterns - do not match, we interpret the coordinates such that x: -1, y: -2, - z: -3. This is all not very safe for awkward dimension names, - but works for most cases. If you want to be a hundred percent sure, - use the :attr:`x`, :attr:`y`, :attr:`z` and :attr:`t` attribute. - - See Also - -------- - get_x, get_y, get_z, get_t""" - - def get_coord(cname, raise_error=True): - try: - return coords[cname] - except KeyError: - if cname not in self.ds.coords: - if raise_error: - raise - return None - ret = self.ds.coords[cname] - try: - idims = var.psy.idims - except AttributeError: # got xarray.Variable - idims = {} - return ret.isel( - **{d: sl for d, sl in idims.items() if d in ret.dims} - ) - - axis = axis.lower() - if axis not in list("xyzt"): - raise ValueError( - "Axis must be one of X, Y, Z, T, not {0}".format(axis) - ) - # we first check for the dimensions and then for the coordinates - # attribute - coords = coords or self.ds.coords - coord_names = var.attrs.get( - "coordinates", var.encoding.get("coordinates", "") - ).split() - if not coord_names: - return - ret = [] - matched = [] - for coord in map( - lambda dim: coords[dim], - filter(lambda dim: dim in coords, chain(coord_names, var.dims)), - ): - # check for the axis attribute or whether the coordinate is in the - # list of possible coordinate names - if coord.name not in (c.name for c in ret): - if coord.name in getattr(self, axis): - matched.append(coord) - elif coord.attrs.get("axis", "").lower() == axis: - ret.append(coord) - if matched: - if len(set([c.name for c in matched])) > 1: - warn( - "Found multiple matches for %s coordinate in the " - "coordinates: %s. I use %s" - % ( - axis, - ", ".join([c.name for c in matched]), - matched[0].name, - ), - PsyPlotRuntimeWarning, - ) - return matched[0] - elif ret: - return None if len(ret) > 1 else ret[0] - # If the coordinates attribute is specified but the coordinate - # variables themselves have no 'axis' attribute, we interpret the - # coordinates such that x: -1, y: -2, z: -3 - # Since however the CF Conventions do not determine the order on how - # the coordinates shall be saved, we try to use a pattern matching - # for latitude and longitude. This is not very nice, hence it is - # better to specify the :attr:`x` and :attr:`y` attribute - tnames = self.t.intersection(coord_names) - if axis == "x": - for cname in filter( - lambda cname: re.search("lon", cname), coord_names - ): - return get_coord(cname) - return get_coord(coord_names[-1], raise_error=False) - elif axis == "y" and len(coord_names) >= 2: - for cname in filter( - lambda cname: re.search("lat", cname), coord_names - ): - return get_coord(cname) - return get_coord(coord_names[-2], raise_error=False) - elif ( - axis == "z" - and len(coord_names) >= 3 - and coord_names[-3] not in tnames - ): - return get_coord(coord_names[-3], raise_error=False) - elif axis == "t" and tnames: - tname = next(iter(tnames)) - if len(tnames) > 1: - warn( - "Found multiple matches for time coordinate in the " - "coordinates: %s. I use %s" % (", ".join(tnames), tname), - PsyPlotRuntimeWarning, - ) - return get_coord(tname, raise_error=False) - - @docstrings.get_sections( - base="CFDecoder.get_x", sections=["Parameters", "Returns"] - ) - @dedent - def get_x(self, var, coords=None): - """ - Get the x-coordinate of a variable - - This method searches for the x-coordinate in the :attr:`ds`. It first - checks whether there is one dimension that holds an ``'axis'`` - attribute with 'X', otherwise it looks whether there is an intersection - between the :attr:`x` attribute and the variables dimensions, otherwise - it returns the coordinate corresponding to the last dimension of `var` - - Possible types - -------------- - var: xarray.Variable - The variable to get the x-coordinate for - coords: dict - Coordinates to use. If None, the coordinates of the dataset in the - :attr:`ds` attribute are used. - - Returns - ------- - xarray.Coordinate or None - The y-coordinate or None if it could be found""" - coords = coords or self.ds.coords - coord = self.get_variable_by_axis(var, "x", coords) - if coord is not None: - return coord - return coords.get(self.get_xname(var)) - - def get_xname(self, var, coords=None): - """Get the name of the x-dimension - - This method gives the name of the x-dimension (which is not necessarily - the name of the coordinate if the variable has a coordinate attribute) - - Parameters - ---------- - var: xarray.Variables - The variable to get the dimension for - coords: dict - The coordinates to use for checking the axis attribute. If None, - they are not used - - Returns - ------- - str - The coordinate name - - See Also - -------- - get_x""" - if coords is not None: - coord = self.get_variable_by_axis(var, "x", coords) - if coord is not None and coord.name in var.dims: - return coord.name - dimlist = list(self.x.intersection(var.dims)) - if dimlist: - if len(dimlist) > 1: - warn( - "Found multiple matches for x coordinate in the variable:" - "%s. I use %s" % (", ".join(dimlist), dimlist[0]), - PsyPlotRuntimeWarning, - ) - return dimlist[0] - # otherwise we return the coordinate in the last position - if var.dims: - return var.dims[-1] - - @docstrings.get_sections( - base="CFDecoder.get_y", sections=["Parameters", "Returns"] - ) - @dedent - def get_y(self, var, coords=None): - """ - Get the y-coordinate of a variable - - This method searches for the y-coordinate in the :attr:`ds`. It first - checks whether there is one dimension that holds an ``'axis'`` - attribute with 'Y', otherwise it looks whether there is an intersection - between the :attr:`y` attribute and the variables dimensions, otherwise - it returns the coordinate corresponding to the second last dimension of - `var` (or the last if the dimension of var is one-dimensional) - - Possible types - -------------- - var: xarray.Variable - The variable to get the y-coordinate for - coords: dict - Coordinates to use. If None, the coordinates of the dataset in the - :attr:`ds` attribute are used. - - Returns - ------- - xarray.Coordinate or None - The y-coordinate or None if it could be found""" - coords = coords or self.ds.coords - coord = self.get_variable_by_axis(var, "y", coords) - if coord is not None: - return coord - return coords.get(self.get_yname(var)) - - def get_yname(self, var, coords=None): - """Get the name of the y-dimension - - This method gives the name of the y-dimension (which is not necessarily - the name of the coordinate if the variable has a coordinate attribute) - - Parameters - ---------- - var: xarray.Variables - The variable to get the dimension for - coords: dict - The coordinates to use for checking the axis attribute. If None, - they are not used - - Returns - ------- - str - The coordinate name - - See Also - -------- - get_y""" - if coords is not None: - coord = self.get_variable_by_axis(var, "y", coords) - if coord is not None and coord.name in var.dims: - return coord.name - dimlist = list(self.y.intersection(var.dims)) - if dimlist: - if len(dimlist) > 1: - warn( - "Found multiple matches for y coordinate in the variable:" - "%s. I use %s" % (", ".join(dimlist), dimlist[0]), - PsyPlotRuntimeWarning, - ) - return dimlist[0] - # otherwise we return the coordinate in the last or second last - # position - if var.dims: - if self.is_unstructured(var): - return var.dims[-1] - return var.dims[-2 if var.ndim > 1 else -1] - - @docstrings.get_sections( - base="CFDecoder.get_z", sections=["Parameters", "Returns"] - ) - @dedent - def get_z(self, var, coords=None): - """ - Get the vertical (z-) coordinate of a variable - - This method searches for the z-coordinate in the :attr:`ds`. It first - checks whether there is one dimension that holds an ``'axis'`` - attribute with 'Z', otherwise it looks whether there is an intersection - between the :attr:`z` attribute and the variables dimensions, otherwise - it returns the coordinate corresponding to the third last dimension of - `var` (or the second last or last if var is two or one-dimensional) - - Possible types - -------------- - var: xarray.Variable - The variable to get the z-coordinate for - coords: dict - Coordinates to use. If None, the coordinates of the dataset in the - :attr:`ds` attribute are used. - - Returns - ------- - xarray.Coordinate or None - The z-coordinate or None if no z coordinate could be found""" - coords = coords or self.ds.coords - coord = self.get_variable_by_axis(var, "z", coords) - if coord is not None: - return coord - zname = self.get_zname(var) - if zname is not None: - return coords.get(zname) - return None - - def get_zname(self, var, coords=None): - """Get the name of the z-dimension - - This method gives the name of the z-dimension (which is not necessarily - the name of the coordinate if the variable has a coordinate attribute) - - Parameters - ---------- - var: xarray.Variables - The variable to get the dimension for - coords: dict - The coordinates to use for checking the axis attribute. If None, - they are not used - - Returns - ------- - str or None - The coordinate name or None if no vertical coordinate could be - found - - See Also - -------- - get_z""" - if coords is not None: - coord = self.get_variable_by_axis(var, "z", coords) - if coord is not None and coord.name in var.dims: - return coord.name - dimlist = list(self.z.intersection(var.dims)) - if dimlist: - if len(dimlist) > 1: - warn( - "Found multiple matches for z coordinate in the variable:" - "%s. I use %s" % (", ".join(dimlist), dimlist[0]), - PsyPlotRuntimeWarning, - ) - return dimlist[0] - # otherwise we return the coordinate in the third last position - if var.dims: - is_unstructured = self.is_unstructured(var) - icheck = -2 if is_unstructured else -3 - min_dim = ( - abs(icheck) if "variable" not in var.dims else abs(icheck - 1) - ) - if var.ndim >= min_dim and var.dims[icheck] != self.get_tname( - var, coords - ): - return var.dims[icheck] - return None - - @docstrings.get_sections( - base="CFDecoder.get_t", sections=["Parameters", "Returns"] - ) - @dedent - def get_t(self, var, coords=None): - """ - Get the time coordinate of a variable - - This method searches for the time coordinate in the :attr:`ds`. It - first checks whether there is one dimension that holds an ``'axis'`` - attribute with 'T', otherwise it looks whether there is an intersection - between the :attr:`t` attribute and the variables dimensions, otherwise - it returns the coordinate corresponding to the first dimension of `var` - - Possible types - -------------- - var: xarray.Variable - The variable to get the time coordinate for - coords: dict - Coordinates to use. If None, the coordinates of the dataset in the - :attr:`ds` attribute are used. - - Returns - ------- - xarray.Coordinate or None - The time coordinate or None if no time coordinate could be found""" - coords = coords or self.ds.coords - coord = self.get_variable_by_axis(var, "t", coords) - if coord is not None: - return coord - dimlist = list(self.t.intersection(var.dims).intersection(coords)) - if dimlist: - if len(dimlist) > 1: - warn( - "Found multiple matches for time coordinate in the " - "variable: %s. I use %s" - % (", ".join(dimlist), dimlist[0]), - PsyPlotRuntimeWarning, - ) - return coords[dimlist[0]] - tname = self.get_tname(var) - if tname is not None: - return coords.get(tname) - return None - - def get_tname(self, var, coords=None): - """Get the name of the t-dimension - - This method gives the name of the time dimension - - Parameters - ---------- - var: xarray.Variables - The variable to get the dimension for - coords: dict - The coordinates to use for checking the axis attribute. If None, - they are not used - - Returns - ------- - str or None - The coordinate name or None if no time coordinate could be found - - See Also - -------- - get_t""" - if coords is not None: - coord = self.get_variable_by_axis(var, "t", coords) - if coord is not None and coord.name in var.dims: - return coord.name - dimlist = list(self.t.intersection(var.dims)) - if dimlist: - if len(dimlist) > 1: - warn( - "Found multiple matches for t coordinate in the variable:" - "%s. I use %s" % (", ".join(dimlist), dimlist[0]), - PsyPlotRuntimeWarning, - ) - return dimlist[0] - # otherwise we return None - return None - - def get_idims(self, arr, coords=None): - """Get the coordinates in the :attr:`ds` dataset as int or slice - - This method returns a mapping from the coordinate names of the given - `arr` to an integer, slice or an array of integer that represent the - coordinates in the :attr:`ds` dataset and can be used to extract the - given `arr` via the :meth:`xarray.Dataset.isel` method. - - Parameters - ---------- - arr: xarray.DataArray - The data array for which to get the dimensions as integers, slices - or list of integers from the dataset in the :attr:`base` attribute - coords: iterable - The coordinates to use. If not given all coordinates in the - ``arr.coords`` attribute are used - - Returns - ------- - dict - Mapping from coordinate name to integer, list of integer or slice - - See Also - -------- - xarray.Dataset.isel, InteractiveArray.idims""" - if coords is None: - coords = arr.coords - else: - coords = { - label: coord - for label, coord in six.iteritems(arr.coords) - if label in coords - } - ret = self.get_coord_idims(coords) - # handle the coordinates that are not in the dataset - missing = set(arr.dims).difference(ret) - if missing: - warn( - "Could not get slices for the following dimensions: %r" - % (missing,), - PsyPlotRuntimeWarning, - ) - return ret - - def get_coord_idims(self, coords): - """Get the slicers for the given coordinates from the base dataset - - This method converts `coords` to slicers (list of - integers or ``slice`` objects) - - Parameters - ---------- - coords: dict - A subset of the ``ds.coords`` attribute of the base dataset - :attr:`ds` - - Returns - ------- - dict - Mapping from coordinate name to integer, list of integer or slice - """ - ret = dict( - (label, get_index_from_coord(coord, self.ds.indexes[label])) - for label, coord in six.iteritems(coords) - if label in self.ds.indexes - ) - return ret - - @docstrings.get_sections( - base="CFDecoder.get_plotbounds", sections=["Parameters", "Returns"] - ) - @dedent - def get_plotbounds(self, coord, kind=None, ignore_shape=False): - """ - Get the bounds of a coordinate - - This method first checks the ``'bounds'`` attribute of the given - `coord` and if it fails, it calculates them. - - Parameters - ---------- - coord: xarray.Coordinate - The coordinate to get the bounds for - kind: str - The interpolation method (see :func:`scipy.interpolate.interp1d`) - that is used in case of a 2-dimensional coordinate - ignore_shape: bool - If True and the `coord` has a ``'bounds'`` attribute, this - attribute is returned without further check. Otherwise it is tried - to bring the ``'bounds'`` into a format suitable for (e.g.) the - :func:`matplotlib.pyplot.pcolormesh` function. - - Returns - ------- - bounds: np.ndarray - The bounds with the same number of dimensions as `coord` but one - additional array (i.e. if `coord` has shape (4, ), `bounds` will - have shape (5, ) and if `coord` has shape (4, 5), `bounds` will - have shape (5, 6)""" - if "bounds" in coord.attrs: - bounds = self.ds.coords[coord.attrs["bounds"]] - if ignore_shape: - return bounds.values.ravel() - if not bounds.shape[:-1] == coord.shape: - bounds = self.ds.isel(**self.get_idims(coord)) - try: - return self._get_plotbounds_from_cf(coord, bounds) - except ValueError as e: - warn( - (e.message if six.PY2 else str(e)) - + " Bounds are calculated automatically!" - ) - return self._infer_interval_breaks(coord, kind=kind) - - @staticmethod - @docstrings.dedent - def _get_plotbounds_from_cf(coord, bounds): - """ - Get plot bounds from the bounds stored as defined by CFConventions - - Parameters - ---------- - coord: xarray.Coordinate - The coordinate to get the bounds for - bounds: xarray.DataArray - The bounds as inferred from the attributes of the given `coord` - - Returns - ------- - %(CFDecoder.get_plotbounds.returns)s - - Notes - ----- - this currently only works for rectilinear grids""" - - if bounds.shape[:-1] != coord.shape or bounds.shape[-1] != 2: - raise ValueError( - "Cannot interprete bounds with shape {0} for {1} " - "coordinate with shape {2}.".format( - bounds.shape, coord.name, coord.shape - ) - ) - ret = np.zeros(tuple(map(lambda i: i + 1, coord.shape))) - ret[tuple(map(slice, coord.shape))] = bounds[..., 0] - last_slices = tuple(slice(-1, None) for _ in coord.shape) - ret[last_slices] = bounds[tuple(chain(last_slices, [1]))] - return ret - - docstrings.keep_params( - "CFDecoder._check_unstructured_bounds.parameters", "nans" - ) - - @docstrings.get_sections( - base="CFDecoder.get_triangles", sections=["Parameters", "Returns"] - ) - @docstrings.dedent - def get_triangles( - self, - var, - coords=None, - convert_radian=True, - copy=False, - src_crs=None, - target_crs=None, - nans=None, - stacklevel=1, - ): - """ - Get the triangles for the variable - - Parameters - ---------- - var: xarray.Variable or xarray.DataArray - The variable to use - coords: dict - Alternative coordinates to use. If None, the coordinates of the - :attr:`ds` dataset are used - convert_radian: bool - If True and the coordinate has units in 'radian', those are - converted to degrees - copy: bool - If True, vertice arrays are copied - src_crs: cartopy.crs.Crs - The source projection of the data. If not None, a transformation - to the given `target_crs` will be done - target_crs: cartopy.crs.Crs - The target projection for which the triangles shall be transformed. - Must only be provided if the `src_crs` is not None. - %(CFDecoder._check_unstructured_bounds.parameters.nans)s - - Returns - ------- - matplotlib.tri.Triangulation - The spatial triangles of the variable - - Raises - ------ - ValueError - If `src_crs` is not None and `target_crs` is None""" - warn( - "The 'get_triangles' method is depreceated and will be removed " - "soon! Use the 'get_cell_node_coord' method!", - DeprecationWarning, - stacklevel=stacklevel, - ) - from matplotlib.tri import Triangulation - - def get_vertices(axis): - bounds = self._check_unstructured_bounds( - var, coords=coords, axis=axis, nans=nans - )[1] - if coords is not None: - bounds = coords.get(bounds.name, bounds) - vertices = bounds.values.ravel() - if convert_radian: - coord = getattr(self, "get_" + axis)(var) - if coord.attrs.get("units") == "radian": - vertices = vertices * 180.0 / np.pi - return vertices if not copy else vertices.copy() - - if coords is None: - coords = self.ds.coords - - xvert = get_vertices("x") - yvert = get_vertices("y") - if src_crs is not None and src_crs != target_crs: - if target_crs is None: - raise ValueError( - "Found %s for the source crs but got None for the " - "target_crs!" % (src_crs,) - ) - arr = target_crs.transform_points(src_crs, xvert, yvert) - xvert = arr[:, 0] - yvert = arr[:, 1] - triangles = np.reshape(range(len(xvert)), (len(xvert) // 3, 3)) - return Triangulation(xvert, yvert, triangles) - - docstrings.delete_params( - "CFDecoder.get_plotbounds.parameters", "ignore_shape" - ) - - @staticmethod - def _infer_interval_breaks(coord, kind=None): - """ - Interpolate the bounds from the data in coord - - Parameters - ---------- - %(CFDecoder.get_plotbounds.parameters.no_ignore_shape)s - - Returns - ------- - %(CFDecoder.get_plotbounds.returns)s - - Notes - ----- - this currently only works for rectilinear grids""" - if coord.ndim == 1: - return _infer_interval_breaks(coord) - elif coord.ndim == 2: - from scipy.interpolate import interp2d - - kind = kind or rcParams["decoder.interp_kind"] - y, x = map(np.arange, coord.shape) - new_x, new_y = map(_infer_interval_breaks, [x, y]) - coord = np.asarray(coord) - return interp2d(x, y, coord, kind=kind, copy=False)(new_x, new_y) - - @classmethod - @docstrings.get_sections(base="CFDecoder._decode_ds") - @docstrings.dedent - def _decode_ds( - cls, ds, gridfile=None, decode_coords=True, decode_times=True - ): - """ - Static method to decode coordinates and time informations - - This method interpretes absolute time informations (stored with units - ``'day as %Y%m%d.%f'``) and coordinates - - Parameters - ---------- - %(CFDecoder.decode_coords.parameters)s - decode_times : bool, optional - If True, decode times encoded in the standard NetCDF datetime - format into datetime objects. Otherwise, leave them encoded as - numbers. - decode_coords : bool, optional - If True, decode the 'coordinates' attribute to identify coordinates - in the resulting dataset.""" - if decode_coords: - ds = cls.decode_coords(ds, gridfile=gridfile) - if decode_times: - for k, v in six.iteritems(ds.variables): - # check for absolute time units and make sure the data is not - # already decoded via dtype check - if v.attrs.get("units", "") == "day as %Y%m%d.%f" and ( - np.issubdtype(v.dtype, np.float64) - ): - decoded = xr.Variable( - v.dims, - AbsoluteTimeDecoder(v), - attrs=v.attrs, - encoding=v.encoding, - ) - ds.update({k: decoded}) - return ds - - @classmethod - @docstrings.dedent - def decode_ds(cls, ds, *args, **kwargs): - """ - Static method to decode coordinates and time informations - - This method interpretes absolute time informations (stored with units - ``'day as %Y%m%d.%f'``) and coordinates - - Parameters - ---------- - %(CFDecoder._decode_ds.parameters)s - - Returns - ------- - xarray.Dataset - The decoded dataset""" - for decoder_cls in cls._registry + [CFDecoder]: - ds = decoder_cls._decode_ds(ds, *args, **kwargs) - return ds - - def correct_dims(self, var, dims={}, remove=True): - """Expands the dimensions to match the dims in the variable - - Parameters - ---------- - var: xarray.Variable - The variable to get the data for - dims: dict - a mapping from dimension to the slices - remove: bool - If True, dimensions in `dims` that are not in the dimensions of - `var` are removed""" - method_mapping = { - "x": self.get_xname, - "z": self.get_zname, - "t": self.get_tname, - } - dims = dict(dims) - if self.is_unstructured(var): # we assume a one-dimensional grid - method_mapping["y"] = self.get_xname - else: - method_mapping["y"] = self.get_yname - for key in six.iterkeys(dims.copy()): - if key in method_mapping and key not in var.dims: - dim_name = method_mapping[key](var, self.ds.coords) - if dim_name in dims: - dims.pop(key) - else: - new_name = method_mapping[key](var) - if new_name is not None: - dims[new_name] = dims.pop(key) - # now remove the unnecessary dimensions - if remove: - for key in set(dims).difference(var.dims): - dims.pop(key) - self.logger.debug( - "Could not find a dimensions matching %s in variable %s!", - key, - var, - ) - return dims - - def standardize_dims(self, var, dims={}): - """Replace the coordinate names through x, y, z and t - - Parameters - ---------- - var: xarray.Variable - The variable to use the dimensions of - dims: dict - The dictionary to use for replacing the original dimensions - - Returns - ------- - dict - The dictionary with replaced dimensions""" - dims = dict(dims) - name_map = { - self.get_xname(var, self.ds.coords): "x", - self.get_yname(var, self.ds.coords): "y", - self.get_zname(var, self.ds.coords): "z", - self.get_tname(var, self.ds.coords): "t", - } - dims = dict(dims) - for dim in set(dims).intersection(name_map): - dims[name_map[dim]] = dims.pop(dim) - return dims - - -class UGridDecoder(CFDecoder): - """ - Decoder for UGrid data sets - - Warnings - -------- - Currently only triangles are supported.""" - - def is_unstructured(self, *args, **kwargs): - """Reimpletemented to return always True. Any ``*args`` and ``**kwargs`` - are ignored""" - return True - - def get_mesh(self, var, coords=None): - """Get the mesh variable for the given `var` - - Parameters - ---------- - var: xarray.Variable - The data source whith the ``'mesh'`` attribute - coords: dict - The coordinates to use. If None, the coordinates of the dataset of - this decoder is used - - Returns - ------- - xarray.Coordinate - The mesh coordinate""" - mesh = var.attrs.get("mesh") - if mesh is None: - return None - if coords is None: - coords = self.ds.coords - return coords.get(mesh, self.ds.coords.get(mesh)) - - @classmethod - @docstrings.dedent - def can_decode(cls, ds, var): - """ - Check whether the given variable can be decoded. - - Returns True if a mesh coordinate could be found via the - :meth:`get_mesh` method - - Parameters - ---------- - %(CFDecoder.can_decode.parameters)s - - Returns - ------- - %(CFDecoder.can_decode.returns)s""" - return cls(ds).get_mesh(var) is not None - - @docstrings.dedent - def get_triangles( - self, - var, - coords=None, - convert_radian=True, - copy=False, - src_crs=None, - target_crs=None, - nans=None, - stacklevel=1, - ): - """ - Get the of the given coordinate. - - Parameters - ---------- - %(CFDecoder.get_triangles.parameters)s - - Returns - ------- - %(CFDecoder.get_triangles.returns)s - - Notes - ----- - If the ``'location'`` attribute is set to ``'node'``, a delaunay - triangulation is performed using the - :class:`matplotlib.tri.Triangulation` class. - - .. todo:: - Implement the visualization for UGrid data shown on the edge of the - triangles""" - warn( - "The 'get_triangles' method is depreceated and will be removed " - "soon! Use the 'get_cell_node_coord' method!", - DeprecationWarning, - stacklevel=stacklevel, - ) - from matplotlib.tri import Triangulation - - if coords is None: - coords = self.ds.coords - - def get_coord(coord): - return coords.get(coord, self.ds.coords.get(coord)) - - mesh = self.get_mesh(var, coords) - nodes = self.get_nodes(mesh, coords) - if any(n is None for n in nodes): - raise ValueError("Could not find the nodes variables!") - xvert, yvert = nodes - xvert = xvert.values - yvert = yvert.values - loc = var.attrs.get("location", "face") - if loc == "face": - triangles = get_coord( - mesh.attrs.get("face_node_connectivity", "") - ).values - if triangles is None: - raise ValueError( - "Could not find the connectivity information!" - ) - elif loc == "node": - triangles = None - else: - raise ValueError( - "Could not interprete location attribute (%s) of mesh " - "variable %s!" % (loc, mesh.name) - ) - - if convert_radian: - for coord in nodes: - if coord.attrs.get("units") == "radian": - coord = coord * 180.0 / np.pi - if src_crs is not None and src_crs != target_crs: - if target_crs is None: - raise ValueError( - "Found %s for the source crs but got None for the " - "target_crs!" % (src_crs,) - ) - xvert = xvert[triangles].ravel() - yvert = yvert[triangles].ravel() - arr = target_crs.transform_points(src_crs, xvert, yvert) - xvert = arr[:, 0] - yvert = arr[:, 1] - if loc == "face": - triangles = np.reshape(range(len(xvert)), (len(xvert) // 3, 3)) - - return Triangulation(xvert, yvert, triangles) - - @docstrings.dedent - def get_cell_node_coord(self, var, coords=None, axis="x", nans=None): - """ - Checks whether the bounds in the variable attribute are triangular - - Parameters - ---------- - %(CFDecoder.get_cell_node_coord.parameters)s - - Returns - ------- - %(CFDecoder.get_cell_node_coord.returns)s""" - if coords is None: - coords = self.ds.coords - - idims = self.get_coord_idims(coords) - - def get_coord(coord): - coord = coords.get(coord, self.ds.coords.get(coord)) - return coord.isel( - **{d: sl for d, sl in idims.items() if d in coord.dims} - ) - - mesh = self.get_mesh(var, coords) - if mesh is None: - return - nodes = self.get_nodes(mesh, coords) - if not len(nodes): - raise ValueError( - "Could not find the nodes variables for the %s " - "coordinate!" % axis - ) - vert = nodes[0 if axis == "x" else 1] - if vert is None: - raise ValueError( - "Could not find the nodes variables for the %s " - "coordinate!" % axis - ) - loc = var.attrs.get("location", "face") - if loc == "node": - # we assume a triangular grid and use matplotlibs triangulation - from matplotlib.tri import Triangulation - - xvert, yvert = nodes - triangles = Triangulation(xvert, yvert) - if axis == "x": - bounds = triangles.x[triangles.triangles] - else: - bounds = triangles.y[triangles.triangles] - elif loc in ["edge", "face"]: - connectivity = get_coord( - mesh.attrs.get("%s_node_connectivity" % loc, "") - ) - if connectivity is None: - raise ValueError( - "Could not find the connectivity information!" - ) - connectivity = connectivity.values - bounds = vert.values[ - np.where( - np.isnan(connectivity), connectivity[:, :1], connectivity - ).astype(int) - ] - else: - raise ValueError( - "Could not interprete location attribute (%s) of mesh " - "variable %s!" % (loc, mesh.name) - ) - dim0 = "__face" if loc == "node" else var.dims[-1] - return xr.DataArray( - bounds, - coords={ - key: val for key, val in coords.items() if (dim0,) == val.dims - }, - dims=( - dim0, - "__bnds", - ), - name=vert.name + "_bnds", - attrs=vert.attrs.copy(), - ) - - @staticmethod - @docstrings.dedent - def decode_coords(ds, gridfile=None): - """ - Reimplemented to set the mesh variables as coordinates - - Parameters - ---------- - %(CFDecoder.decode_coords.parameters)s - - Returns - ------- - %(CFDecoder.decode_coords.returns)s""" - extra_coords = set(ds.coords) - for var in six.itervalues(ds.variables): - if "mesh" in var.attrs: - mesh = var.attrs["mesh"] - if mesh not in extra_coords: - extra_coords.add(mesh) - try: - mesh_var = ds.variables[mesh] - except KeyError: - warn("Could not find mesh variable %s" % mesh) - continue - if "node_coordinates" in mesh_var.attrs: - extra_coords.update( - mesh_var.attrs["node_coordinates"].split() - ) - if "face_node_connectivity" in mesh_var.attrs: - extra_coords.add( - mesh_var.attrs["face_node_connectivity"] - ) - if gridfile is not None and not isinstance(gridfile, xr.Dataset): - gridfile = open_dataset(gridfile) - ds.update( - { - k: v - for k, v in six.iteritems(gridfile.variables) - if k in extra_coords - } - ) - if xr_version < (0, 11): - ds.set_coords( - extra_coords.intersection(ds.variables), inplace=True - ) - else: - ds._coord_names.update(extra_coords.intersection(ds.variables)) - return ds - - def get_nodes(self, coord, coords): - """Get the variables containing the definition of the nodes - - Parameters - ---------- - coord: xarray.Coordinate - The mesh variable - coords: dict - The coordinates to use to get node coordinates""" - - def get_coord(coord): - return coords.get(coord, self.ds.coords.get(coord)) - - return list( - map(get_coord, coord.attrs.get("node_coordinates", "").split()[:2]) - ) - - @docstrings.dedent - def get_x(self, var, coords=None): - """ - Get the centers of the triangles in the x-dimension - - Parameters - ---------- - %(CFDecoder.get_y.parameters)s - - Returns - ------- - %(CFDecoder.get_y.returns)s""" - if coords is None: - coords = self.ds.coords - # first we try the super class - ret = super(UGridDecoder, self).get_x(var, coords) - # but if that doesn't work because we get the variable name in the - # dimension of `var`, we use the means of the triangles - if ( - ret is None - or ret.name in var.dims - or (hasattr(var, "mesh") and ret.name == var.mesh) - ): - bounds = self.get_cell_node_coord(var, axis="x", coords=coords) - if bounds is not None: - centers = bounds.mean(axis=-1) - x = self.get_nodes(self.get_mesh(var, coords), coords)[0] - try: - cls = xr.IndexVariable - except AttributeError: # xarray < 0.9 - cls = xr.Coordinate - return cls(x.name, centers, attrs=x.attrs.copy()) - else: - return ret - - @docstrings.dedent - def get_y(self, var, coords=None): - """ - Get the centers of the triangles in the y-dimension - - Parameters - ---------- - %(CFDecoder.get_y.parameters)s - - Returns - ------- - %(CFDecoder.get_y.returns)s""" - if coords is None: - coords = self.ds.coords - # first we try the super class - ret = super(UGridDecoder, self).get_y(var, coords) - # but if that doesn't work because we get the variable name in the - # dimension of `var`, we use the means of the triangles - if ( - ret is None - or ret.name in var.dims - or (hasattr(var, "mesh") and ret.name == var.mesh) - ): - bounds = self.get_cell_node_coord(var, axis="y", coords=coords) - if bounds is not None: - centers = bounds.mean(axis=-1) - y = self.get_nodes(self.get_mesh(var, coords), coords)[1] - try: - cls = xr.IndexVariable - except AttributeError: # xarray < 0.9 - cls = xr.Coordinate - return cls(y.name, centers, attrs=y.attrs.copy()) - else: - return ret - - -# register the UGridDecoder -CFDecoder.register_decoder(UGridDecoder) - -docstrings.keep_params("CFDecoder.decode_coords.parameters", "gridfile") -docstrings.get_sections( - inspect.cleandoc(xr.open_dataset.__doc__.split("\n", 1)[1]), - "xarray.open_dataset", -) -docstrings.delete_params("xarray.open_dataset.parameters", "engine") - - -@docstrings.get_sections(base="open_dataset") -@docstrings.dedent -def open_dataset( - filename_or_obj, - decode_cf=True, - decode_times=True, - decode_coords=True, - engine=None, - gridfile=None, - **kwargs, -): - """ - Open an instance of :class:`xarray.Dataset`. - - This method has the same functionality as the :func:`xarray.open_dataset` - method except that is supports an additional 'gdal' engine to open - gdal Rasters (e.g. GeoTiffs) and that is supports absolute time units like - ``'day as %Y%m%d.%f'`` (if `decode_cf` and `decode_times` are True). - - Parameters - ---------- - %(xarray.open_dataset.parameters.no_engine)s - engine: {'netcdf4', 'scipy', 'pydap', 'h5netcdf', 'gdal'}, optional - Engine to use when reading netCDF files. If not provided, the default - engine is chosen based on available dependencies, with a preference for - 'netcdf4'. - %(CFDecoder.decode_coords.parameters.gridfile)s - - Returns - ------- - xarray.Dataset - The dataset that contains the variables from `filename_or_obj`""" - # use the absolute path name (is saver when saving the project) - if isstring(filename_or_obj) and osp.exists(filename_or_obj): - filename_or_obj = osp.abspath(filename_or_obj) - if engine == "gdal": - from psyplot.gdal_store import GdalStore - - filename_or_obj = GdalStore(filename_or_obj) - engine = None - ds = xr.open_dataset( - filename_or_obj, - decode_cf=decode_cf, - decode_coords=False, - engine=engine, - decode_times=decode_times, - **kwargs, - ) - if isstring(filename_or_obj): - ds.psy.filename = filename_or_obj - if decode_cf: - ds = CFDecoder.decode_ds( - ds, - decode_coords=decode_coords, - decode_times=decode_times, - gridfile=gridfile, - ) - return ds - - -docstrings.get_sections( - inspect.cleandoc(xr.open_mfdataset.__doc__.split("\n", 1)[1]), - "xarray.open_mfdataset", -) -docstrings.delete_params("xarray.open_mfdataset.parameters", "engine") -docstrings.keep_params("get_tdata.parameters", "t_format") - -docstrings.params["xarray.open_mfdataset.parameters.no_engine"] = ( - docstrings.params["xarray.open_mfdataset.parameters.no_engine"] - .replace("**kwargs", "``**kwargs``") - .replace('"path/to/my/files/*.nc"', '``"path/to/my/files/*.nc"``') -) - - -docstrings.keep_params("open_dataset.parameters", "engine") - - -@docstrings.dedent -def open_mfdataset( - paths, - decode_cf=True, - decode_times=True, - decode_coords=True, - engine=None, - gridfile=None, - t_format=None, - **kwargs, -): - """ - Open multiple files as a single dataset. - - This function is essentially the same as the :func:`xarray.open_mfdataset` - function but (as the :func:`open_dataset`) supports additional decoding - and the ``'gdal'`` engine. - You can further specify the `t_format` parameter to get the time - information from the files and use the results to concatenate the files - - Parameters - ---------- - %(xarray.open_mfdataset.parameters.no_engine)s - %(open_dataset.parameters.engine)s - %(get_tdata.parameters.t_format)s - %(CFDecoder.decode_coords.parameters.gridfile)s - - Returns - ------- - xarray.Dataset - The dataset that contains the variables from `filename_or_obj`""" - if t_format is not None or engine == "gdal": - if isinstance(paths, six.string_types): - paths = sorted(glob(paths)) - if not paths: - raise IOError("no files to open") - if t_format is not None: - time, paths = get_tdata(t_format, paths) - kwargs["concat_dim"] = "time" - if xr_version > (0, 11): - kwargs["combine"] = "nested" - if all(map(isstring, paths)): - filenames = list(paths) - else: - filenames = None - if engine == "gdal": - from psyplot.gdal_store import GdalStore - - paths = list(map(GdalStore, paths)) - engine = None - if xr_version < (0, 18): - kwargs["lock"] = False - - ds = xr.open_mfdataset( - paths, - decode_cf=decode_cf, - decode_times=decode_times, - engine=engine, - decode_coords=False, - **kwargs, - ) - ds.psy.filename = filenames - if decode_cf: - ds = CFDecoder.decode_ds( - ds, - gridfile=gridfile, - decode_coords=decode_coords, - decode_times=decode_times, - ) - ds.psy._concat_dim = kwargs.get("concat_dim") - ds.psy._combine = kwargs.get("combine") - if t_format is not None: - ds["time"] = time - return ds - - -class InteractiveBase(object): - """Class for the communication of a data object with a suitable plotter - - This class serves as an interface for data objects (in particular as a - base for :class:`InteractiveArray` and :class:`InteractiveList`) to - communicate with the corresponding :class:`~psyplot.plotter.Plotter` in the - :attr:`plotter` attribute""" - - #: The :class:`psyplot.project.DataArrayPlotter` - _plot = None - - @property - def plotter(self): - """:class:`psyplot.plotter.Plotter` instance that makes the interactive - plotting of the data""" - return self._plotter - - @plotter.setter - def plotter(self, value): - self._plotter = value - - @plotter.deleter - def plotter(self): - self._plotter = None - - no_auto_update = property( - _no_auto_update_getter, doc=_no_auto_update_getter.__doc__ - ) - - @property - def plot(self): - """An object to visualize this data object - - To make a 2D-plot with the :mod:`psy-simple ` - plugin, you can just type - - .. code-block:: python - - plotter = da.psy.plot.plot2d() - - It will create a new :class:`psyplot.plotter.Plotter` instance with the - extracted and visualized data. - - See Also - -------- - psyplot.project.DataArrayPlotter: for the different plot methods""" - if self._plot is None: - import psyplot.project as psy - - self._plot = psy.DataArrayPlotter(self) - return self._plot - - @no_auto_update.setter - def no_auto_update(self, value): - if self.plotter is not None: - self.plotter.no_auto_update = value - self.no_auto_update.value = bool(value) - - @property - def logger(self): - """:class:`logging.Logger` of this instance""" - try: - return self._logger - except AttributeError: - name = "%s.%s.%s" % ( - self.__module__, - self.__class__.__name__, - self.arr_name, - ) - self._logger = logging.getLogger(name) - self.logger.debug("Initializing...") - return self._logger - - @logger.setter - def logger(self, value): - self._logger = value - - @property - def ax(self): - """The matplotlib axes the plotter of this data object plots on""" - return None if self.plotter is None else self.plotter.ax - - @ax.setter - def ax(self, value): - if self.plotter is None: - raise ValueError( - "Cannot set the axes because the plotter attribute is None!" - ) - self.plotter.ax = value - - block_signals = utils._temp_bool_prop( - "block_signals", "Block the emitting of signals of this instance" - ) - - # ------------------------------------------------------------------------- - # -------------------------------- SIGNALS -------------------------------- - # ------------------------------------------------------------------------- - - #: :class:`Signal` to be emitted when the object has been updated - onupdate = Signal("_onupdate") - _onupdate = None - - _plotter = None - - @property - @docstrings.get_docstring(base="InteractiveBase._njobs") - @dedent - def _njobs(self): - """ - The number of jobs taken from the queue during an update process - - Returns - ------- - list of int - The length of the list determines the number of neccessary queues, - the numbers in the list determines the number of tasks per queue - this instance fullfills during the update process""" - return self.plotter._njobs if self.plotter is not None else [] - - @property - def arr_name(self): - """:class:`str`. The internal name of the :class:`InteractiveBase`""" - return self._arr_name - - @arr_name.setter - def arr_name(self, value): - self._arr_name = value - try: - del self._logger - except AttributeError: - pass - self.onupdate.emit() - - _arr_name = None - - _no_auto_update = None - - @docstrings.get_sections(base="InteractiveBase") - @dedent - def __init__(self, plotter=None, arr_name="arr0", auto_update=None): - """ - Parameters - ---------- - plotter: Plotter - Default: None. Interactive plotter that makes the plot via - formatoption keywords. - arr_name: str - Default: ``'data'``. unique string of the array - auto_update: bool - Default: None. A boolean indicating whether this list shall - automatically update the contained arrays when calling the - :meth:`update` method or not. See also the :attr:`no_auto_update` - attribute. If None, the value from the ``'lists.auto_update'`` - key in the :attr:`psyplot.rcParams` dictionary is used.""" - self.plotter = plotter - self.arr_name = arr_name - if auto_update is None: - auto_update = rcParams["lists.auto_update"] - self.no_auto_update = not bool(auto_update) - self.replot = False - - def _finish_all(self, queues): - for n, queue in zip(safe_list(self._njobs), safe_list(queues)): - if queue is not None: - for i in range(n): - queue.task_done() - - @docstrings.get_sections(base="InteractiveBase._register_update") - @dedent - def _register_update( - self, replot=False, fmt={}, force=False, todefault=False - ): - """ - Register new formatoptions for updating - - Parameters - ---------- - replot: bool - Boolean that determines whether the data specific formatoptions - shall be updated in any case or not. Note, if `dims` is not empty - or any coordinate keyword is in ``**kwargs``, this will be set to - True automatically - fmt: dict - Keys may be any valid formatoption of the formatoptions in the - :attr:`plotter` - force: str, list of str or bool - If formatoption key (i.e. string) or list of formatoption keys, - thery are definitely updated whether they changed or not. - If True, all the given formatoptions in this call of the are - :meth:`update` method are updated - todefault: bool - If True, all changed formatoptions (except the registered ones) - are updated to their default value as stored in the - :attr:`~psyplot.plotter.Plotter.rc` attribute - - See Also - -------- - start_update""" - self.replot = self.replot or replot - if self.plotter is not None: - self.plotter._register_update( - replot=self.replot, fmt=fmt, force=force, todefault=todefault - ) - - @docstrings.get_sections( - base="InteractiveBase.start_update", sections=["Parameters", "Returns"] - ) - @dedent - def start_update(self, draw=None, queues=None): - """ - Conduct the formerly registered updates - - This method conducts the updates that have been registered via the - :meth:`update` method. You can call this method if the - :attr:`no_auto_update` attribute of this instance and the `auto_update` - parameter in the :meth:`update` method has been set to False - - Parameters - ---------- - draw: bool or None - Boolean to control whether the figure of this array shall be drawn - at the end. If None, it defaults to the `'auto_draw'`` parameter - in the :attr:`psyplot.rcParams` dictionary - queues: list of :class:`Queue.Queue` instances - The queues that are passed to the - :meth:`psyplot.plotter.Plotter.start_update` method to ensure a - thread-safe update. It can be None if only one single plotter is - updated at the same time. The number of jobs that are taken from - the queue is determined by the :meth:`_njobs` attribute. Note that - there this parameter is automatically configured when updating - from a :class:`~psyplot.project.Project`. - - Returns - ------- - bool - A boolean indicating whether a redrawing is necessary or not - - See Also - -------- - :attr:`no_auto_update`, update - """ - if self.plotter is not None: - return self.plotter.start_update(draw=draw, queues=queues) - - docstrings.keep_params("InteractiveBase.start_update.parameters", "draw") - - @docstrings.get_sections( - base="InteractiveBase.update", sections=["Parameters", "Notes"] - ) - @docstrings.dedent - def update( - self, - fmt={}, - replot=False, - draw=None, - auto_update=False, - force=False, - todefault=False, - **kwargs, - ): - """ - Update the coordinates and the plot - - This method updates all arrays in this list with the given coordinate - values and formatoptions. - - Parameters - ---------- - %(InteractiveBase._register_update.parameters)s - auto_update: bool - Boolean determining whether or not the :meth:`start_update` method - is called at the end. This parameter has no effect if the - :attr:`no_auto_update` attribute is set to ``True``. - %(InteractiveBase.start_update.parameters.draw)s - ``**kwargs`` - Any other formatoption that shall be updated (additionally to those - in `fmt`) - - Notes - ----- - If the :attr:`no_auto_update` attribute is True and the given - `auto_update` parameter are is False, the update of the plots are - registered and conducted at the next call of the :meth:`start_update` - method or the next call of this method (if the `auto_update` parameter - is then True). - """ - fmt = dict(fmt) - fmt.update(kwargs) - - self._register_update( - replot=replot, fmt=fmt, force=force, todefault=todefault - ) - - if not self.no_auto_update or auto_update: - self.start_update(draw=draw) - - def to_interactive_list(self): - """Return a :class:`InteractiveList` that contains this object""" - raise NotImplementedError( - "Not implemented for the %s class" % (self.__class__.__name__,) - ) - - -@xr.register_dataarray_accessor("psy") -class InteractiveArray(InteractiveBase): - """Interactive psyplot accessor for the data array - - This class keeps reference to the base :class:`xarray.Dataset` where the - :class:`array.DataArray` originates from and enables to switch between the - coordinates in the array. Furthermore it has a :attr:`plotter` attribute to - enable interactive plotting via an :class:`psyplot.plotter.Plotter` - instance.""" - - @property - def base(self): - """Base dataset this instance gets its data from""" - if self._base is None: - if "variable" in self.arr.dims: - - def to_dataset(i): - ret = self.isel(variable=i).to_dataset( - name=self.arr.coords["variable"].values[i] - ) - try: - return ret.drop_vars("variable") - except ValueError: # 'variable' Variable not defined - pass - return ret - - ds = to_dataset(0) - if len(self.arr.coords["variable"]) > 1: - for i in range(1, len(self.arr.coords["variable"])): - ds.update(ds.merge(to_dataset(i))) - self._base = ds - else: - self._base = self.arr.to_dataset( - name=self.arr.name or self.arr_name - ) - self.onbasechange.emit() - return self._base - - @base.setter - def base(self, value): - self._base = value - self.onbasechange.emit() - - @property - def decoder(self): - """The decoder of this array""" - try: - return self._decoder - except AttributeError: - self._decoder = CFDecoder.get_decoder(self.base, self.arr) - return self._decoder - - @decoder.setter - def decoder(self, value): - self._decoder = value - - @property - def idims(self): - """Coordinates in the :attr:`base` dataset as int or slice - - This attribute holds a mapping from the coordinate names of this - array to an integer, slice or an array of integer that represent the - coordinates in the :attr:`base` dataset""" - if self._idims is None: - self._idims = self.decoder.get_idims(self.arr) - return self._idims - - @idims.setter - def idims(self, value): - self._idims = value - - @property - @docstrings - def _njobs(self): - """%(InteractiveBase._njobs)s""" - ret = super(self.__class__, self)._njobs or [0] - ret[0] += 1 - return ret - - logger = InteractiveBase.logger - _idims = None - _base = None - - # -------------- SIGNALS -------------------------------------------------- - #: :class:`Signal` to be emiited when the base of the object changes - onbasechange = Signal("_onbasechange") - _onbasechange = None - - @docstrings.dedent - def __init__(self, xarray_obj, *args, **kwargs): - """ - The ``*args`` and ``**kwargs`` are essentially the same as for the - :class:`xarray.DataArray` method, additional ``**kwargs`` are - described below. - - Other Parameters - ---------------- - base: xarray.Dataset - Default: None. Dataset that serves as the origin of the data - contained in this DataArray instance. This will be used if you want - to update the coordinates via the :meth:`update` method. If None, - this instance will serve as a base as soon as it is needed. - decoder: psyplot.CFDecoder - The decoder that decodes the `base` dataset and is used to get - bounds. If not given, a new :class:`CFDecoder` is created - idims: dict - Default: None. dictionary with integer values and/or slices in the - `base` dictionary. If not given, they are determined automatically - %(InteractiveBase.parameters)s - """ - self.arr = xarray_obj - super(InteractiveArray, self).__init__(*args, **kwargs) - self._registered_updates = {} - self._new_dims = {} - self.method = None - - def init_accessor( - self, base=None, idims=None, decoder=None, *args, **kwargs - ): - """ - Initialize the accessor instance - - This method initializes the accessor - - Parameters - ---------- - base: xr.Dataset - The base dataset for the data - idims: dict - A mapping from dimension name to indices. If not provided, it is - calculated when the :attr:`idims` attribute is accessed - decoder: CFDecoder - The decoder of this object - %(InteractiveBase.parameters)s - """ - if base is not None: - self.base = base - self.idims = idims - if decoder is not None: - self.decoder = decoder - super(InteractiveArray, self).__init__(*args, **kwargs) - - @property - def iter_base_variables(self): - """An iterator over the base variables in the :attr:`base` dataset""" - if VARIABLELABEL in self.arr.coords: - return ( - self._get_base_var(name) - for name in safe_list( - self.arr.coords[VARIABLELABEL].values.tolist() - ) - ) - name = self.arr.name - if name is None: - return iter([self.arr._variable]) - return iter([self.base.variables[name]]) - - def _get_base_var(self, name): - try: - return self.base.variables[name] - except KeyError: - return self.arr.sel(**{VARIABLELABEL: name}).rename(name) - - @property - def base_variables(self): - """A mapping from the variable name to the variablein the :attr:`base` - dataset.""" - if VARIABLELABEL in self.arr.coords: - return dict( - [ - (name, self._get_base_var(name)) - for name in safe_list( - self.arr.coords[VARIABLELABEL].values.tolist() - ) - ] - ) - name = self.arr.name - if name is None: - return {name: self.arr._variable} - else: - return {self.arr.name: self.base.variables[self.arr.name]} - - docstrings.keep_params("setup_coords.parameters", "dims") - - @docstrings.get_sections(base="InteractiveArray._register_update") - @docstrings.dedent - def _register_update( - self, - method="isel", - replot=False, - dims={}, - fmt={}, - force=False, - todefault=False, - ): - """ - Register new dimensions and formatoptions for updating - - Parameters - ---------- - method: {'isel', None, 'nearest', ...} - Selection method of the xarray.Dataset to be used for setting the - variables from the informations in `dims`. - If `method` is 'isel', the :meth:`xarray.Dataset.isel` method is - used. Otherwise it sets the `method` parameter for the - :meth:`xarray.Dataset.sel` method. - %(setup_coords.parameters.dims)s - %(InteractiveBase._register_update.parameters)s - - See Also - -------- - start_update""" - if self._new_dims and self.method != method: - raise ValueError( - "New dimensions were already specified for with the %s method!" - " I can not choose a new method %s" % (self.method, method) - ) - else: - self.method = method - if "name" in dims: - self._new_dims["name"] = dims.pop("name") - if "name" in self._new_dims: - name = self._new_dims["name"] - if not isstring(name): - name = name[0] # concatenated array - arr = self.base[name] - else: - arr = next(six.itervalues(self.base_variables)) - self._new_dims.update(self.decoder.correct_dims(arr, dims)) - InteractiveBase._register_update( - self, - fmt=fmt, - replot=replot or bool(self._new_dims), - force=force, - todefault=todefault, - ) - - def _update_concatenated(self, dims, method): - """Updates a concatenated array to new dimensions""" - - def is_unequal(v1, v2): - try: - return bool(v1 != v2) - except ValueError: # arrays - try: - (v1 == v2).all() - except AttributeError: - return False - - def filter_attrs(item): - """Checks whether the attribute is from the base variable""" - return item[0] not in self.base.attrs or is_unequal( - item[1], self.base.attrs[item[0]] - ) - - saved_attrs = list(filter(filter_attrs, six.iteritems(self.arr.attrs))) - saved_name = self.arr.name - self.arr.name = "None" - if "name" in dims: - name = dims.pop("name") - else: - name = list(self.arr.coords["variable"].values) - base_dims = self.base[name].dims - if method == "isel": - self.idims.update(dims) - dims = self.idims - for dim in set(base_dims) - set(dims): - dims[dim] = slice(None) - for dim in set(dims) - set(self.base[name].dims): - del dims[dim] - res = self.base[name].isel(**dims).to_array() - else: - self._idims = None - for key, val in six.iteritems(self.arr.coords): - if key in base_dims and key != "variable": - dims.setdefault(key, val) - kws = dims.copy() - # the sel method does not work with slice objects - if not any(isinstance(idx, slice) for idx in dims.values()): - kws["method"] = method - try: - res = self.base[name].sel(**kws) - except KeyError: - _fix_times(kws) - res = self.base[name].sel(**kws) - res = res.to_array() - if "coordinates" in self.base[name[0]].encoding: - res.encoding["coordinates"] = self.base[name[0]].encoding[ - "coordinates" - ] - self.arr._variable = res._variable - self.arr._coords = res._coords - try: - self.arr._indexes = ( - res._indexes.copy() if res._indexes is not None else None - ) - except AttributeError: # res.indexes not existent for xr<0.12 - pass - self.arr.name = saved_name - for key, val in saved_attrs: - self.arr.attrs[key] = val - - def _update_array(self, dims, method): - """Updates the array to the new dims from then :attr:`base` dataset""" - - def is_unequal(v1, v2): - try: - return bool(v1 != v2) - except ValueError: # arrays - try: - (v1 == v2).all() - except AttributeError: - return False - - def filter_attrs(item): - """Checks whether the attribute is from the base variable""" - return item[0] not in base_var.attrs or is_unequal( - item[1], base_var.attrs[item[0]] - ) - - base_var = self.base.variables[self.arr.name] - if "name" in dims: - name = dims.pop("name") - self.arr.name = name - else: - name = self.arr.name - # save attributes that have been changed by the user - saved_attrs = list(filter(filter_attrs, six.iteritems(self.arr.attrs))) - if method == "isel": - self.idims.update(dims) - dims = self.idims - for dim in set(self.base[name].dims) - set(dims): - dims[dim] = slice(None) - for dim in set(dims) - set(self.base[name].dims): - del dims[dim] - res = self.base[name].isel(**dims) - else: - self._idims = None - old_dims = self.arr.dims[:] - for key, val in six.iteritems(self.arr.coords): - if key in base_var.dims: - dims.setdefault(key, val) - kws = dims.copy() - # the sel method does not work with slice objects - if not any(isinstance(idx, slice) for idx in dims.values()): - kws["method"] = method - try: - res = self.base[name].sel(**kws) - except KeyError: - _fix_times(kws) - res = self.base[name].sel(**kws) - # squeeze the 0-dimensional dimensions - res = res.isel( - **{ - dim: 0 - for i, dim in enumerate(res.dims) - if (res.shape[i] == 1 and dim not in old_dims) - } - ) - self.arr._variable = res._variable - self.arr._coords = res._coords - try: - self.arr._indexes = ( - res._indexes.copy() if res._indexes is not None else None - ) - except AttributeError: # res.indexes not existent for xr<0.12 - pass - # update to old attributes - for key, val in saved_attrs: - self.arr.attrs[key] = val - - def shiftlon(self, central_longitude): - """ - Shift longitudes and the data so that they match map projection region. - - Only valid for cylindrical/pseudo-cylindrical global projections and - data on regular lat/lon grids. longitudes need to be 1D. - - Parameters - ---------- - central_longitude - center of map projection region - - References - ---------- - This function is copied and taken from the - :class:`mpl_toolkits.basemap.Basemap` class. The only difference is - that we do not mask values outside the map projection region - """ - if xr_version < (0, 10): - raise NotImplementedError( - "xarray>=0.10 is required for the shiftlon method!" - ) - arr = self.arr - ret = arr.copy(True, arr.values.copy()) - if arr.ndim > 2: - xname = self.get_dim("x") - yname = self.get_dim("y") - shapes = dict( - [ - (dim, range(i)) - for dim, i in zip(arr.dims, arr.shape) - if dim not in [xname, yname] - ] - ) - dims = list(shapes) - for indexes in product(*shapes.values()): - d = dict(zip(dims, indexes)) - shifted = ret[d].psy.shiftlon(central_longitude) - ret[d] = shifted.values - - x = shifted.psy.get_coord("x") - ret[x.name] = shifted[x.name].variable - - return ret - - lon = self.get_coord("x").variable - xname = self.get_dim("x") - ix = arr.dims.index(xname) - lon = lon.copy(True, lon.values.copy()) - lonsin = lon.values - datain = arr.values.copy() - - clon = np.asarray(central_longitude) - - if lonsin.ndim not in [1]: - raise ValueError("1D longitudes required") - elif clon.ndim: - raise ValueError( - "Central longitude must be a scalar, not " - "%i-dimensional!" % clon.ndim - ) - - lonsin = np.where(lonsin > clon + 180, lonsin - 360, lonsin) - lonsin = np.where(lonsin < clon - 180, lonsin + 360, lonsin) - londiff = np.abs(lonsin[0:-1] - lonsin[1:]) - londiff_sort = np.sort(londiff) - thresh = 360.0 - londiff_sort[-2] - itemindex = len(lonsin) - np.where(londiff >= thresh)[0] - if itemindex.size: - # check to see if cyclic (wraparound) point included - # if so, remove it. - if np.abs(lonsin[0] - lonsin[-1]) < 1.0e-4: - hascyclic = True - lonsin_save = lonsin.copy() - lonsin = lonsin[1:] - if datain is not None: - datain_save = datain.copy() - datain = datain[1:] - else: - hascyclic = False - lonsin = np.roll(lonsin, itemindex - 1) - if datain is not None: - datain = np.roll(datain, itemindex - 1, [ix]) - # add cyclic point back at beginning. - if hascyclic: - lonsin_save[1:] = lonsin - lonsin_save[0] = lonsin[-1] - 360.0 - lonsin = lonsin_save - if datain is not None: - datain_save[1:] = datain - datain_save[0] = datain[-1] - datain = datain_save - ret = ret.copy(True, datain) - lon.values[:] = lonsin - ret[lon.name] = lon - return ret - - @docstrings.dedent - def start_update(self, draw=None, queues=None): - """ - Conduct the formerly registered updates - - This method conducts the updates that have been registered via the - :meth:`update` method. You can call this method if the - :attr:`no_auto_update` attribute of this instance is True and the - `auto_update` parameter in the :meth:`update` method has been set to - False - - Parameters - ---------- - %(InteractiveBase.start_update.parameters)s - - Returns - ------- - %(InteractiveBase.start_update.returns)s - - See Also - -------- - :attr:`no_auto_update`, update - """ - - def filter_attrs(item): - return ( - item[0] not in self.base.attrs - or item[1] != self.base.attrs[item[0]] - ) - - if queues is not None: - # make sure that no plot is updated during gathering the data - queues[0].get() - try: - dims = self._new_dims - method = self.method - if dims: - if VARIABLELABEL in self.arr.coords: - self._update_concatenated(dims, method) - else: - self._update_array(dims, method) - if queues is not None: - queues[0].task_done() - self._new_dims = {} - self.onupdate.emit() - except Exception: - self._finish_all(queues) - raise - return InteractiveBase.start_update(self, draw=draw, queues=queues) - - @docstrings.get_sections( - base="InteractiveArray.update", sections=["Parameters", "Notes"] - ) - @docstrings.dedent - def update( - self, - method="isel", - dims={}, - fmt={}, - replot=False, - auto_update=False, - draw=None, - force=False, - todefault=False, - **kwargs, - ): - """ - Update the coordinates and the plot - - This method updates all arrays in this list with the given coordinate - values and formatoptions. - - Parameters - ---------- - %(InteractiveArray._register_update.parameters)s - auto_update: bool - Boolean determining whether or not the :meth:`start_update` method - is called after the end. - %(InteractiveBase.start_update.parameters)s - ``**kwargs`` - Any other formatoption or dimension that shall be updated - (additionally to those in `fmt` and `dims`) - - Notes - ----- - When updating to a new array while trying to set the dimensions at the - same time, you have to specify the new dimensions via the `dims` - parameter, e.g.:: - - da.psy.update(name='new_name', dims={'new_dim': 3}) - - if ``'new_dim'`` is not yet a dimension of this array - - %(InteractiveBase.update.notes)s""" - dims = dict(dims) - fmt = dict(fmt) - vars_and_coords = set( - chain(self.arr.dims, self.arr.coords, ["name", "x", "y", "z", "t"]) - ) - furtherdims, furtherfmt = utils.sort_kwargs(kwargs, vars_and_coords) - dims.update(furtherdims) - fmt.update(furtherfmt) - - self._register_update( - method=method, - replot=replot, - dims=dims, - fmt=fmt, - force=force, - todefault=todefault, - ) - - if not self.no_auto_update or auto_update: - self.start_update(draw=draw) - - def _short_info(self, intend=0, maybe=False): - str_intend = " " * intend - if "variable" in self.arr.coords: - name = ", ".join(self.arr.coords["variable"].values) - else: - name = self.arr.name - if self.arr.ndim > 0: - dims = ", with (%s)=%s" % ( - ", ".join(self.arr.dims), - self.arr.shape, - ) - else: - dims = "" - return str_intend + "%s: %i-dim %s of %s%s, %s" % ( - self.arr_name, - self.arr.ndim, - self.arr.__class__.__name__, - name, - dims, - ", ".join( - "%s=%s" % (coord, format_item(val.values)) - for coord, val in six.iteritems(self.arr.coords) - if val.ndim == 0 - ), - ) - - def __getitem__(self, key): - ret = self.arr.__getitem__(key) - ret.psy._base = self.base - return ret - - def isel(self, *args, **kwargs): - """Select a subset of the array based on position. - - Same method as :meth:`xarray.DataArray.isel` but keeps information on - the base dataset. - """ - ret = self.arr.isel(*args, **kwargs) - ret.psy._base = self._base - return ret - - def sel(self, *args, **kwargs): - """Select a subset of the array based on indexes. - - Same method as :meth:`xarray.DataArray.sel` but keeps information on - the base dataset. - """ - ret = self.arr.sel(*args, **kwargs) - ret.psy._base = self._base - return ret - - def copy(self, deep=False): - """Copy the array - - This method returns a copy of the underlying array in the :attr:`arr` - attribute. It is more stable because it creates a new `psy` accessor""" - arr = self.arr.copy(deep) - try: - arr.psy = InteractiveArray(arr) - except AttributeError: # attribute is read-only for xarray >=0.13 - pass - return arr - - def to_interactive_list(self): - return InteractiveList([self], arr_name=self.arr_name) - - @docstrings.get_sections(base="InteractiveArray.get_coord") - @docstrings.dedent - def get_coord(self, what, base=False): - """ - The x-coordinate of this data array - - Parameters - ---------- - what: {'t', 'x', 'y', 'z'} - The letter of the axis - base: bool - If True, use the base variable in the :attr:`base` dataset.""" - what = what.lower() - return getattr(self.decoder, "get_" + what)( - next(six.itervalues(self.base_variables)) if base else self.arr, - self.arr.coords, - ) - - @docstrings.dedent - def get_dim(self, what, base=False): - """ - The name of the x-dimension of this data array - - Parameters - ---------- - %(InteractiveArray.get_coord.parameters)s""" - what = what.lower() - return getattr(self.decoder, "get_%sname" % what)( - next(six.itervalues(self.base_variables)) if base else self.arr - ) - - # ------------------ Calculations ----------------------------------------- - - def _gridweights(self): - """Calculate the gridweights with a simple rectangular approximation""" - arr = self.arr - xcoord = self.get_coord("x") - ycoord = self.get_coord("y") - # convert the units - xcoord_orig = xcoord - ycoord_orig = ycoord - units = xcoord.attrs.get("units", "") - in_metres = False - in_degree = False - if "deg" in units or ( - "rad" not in units - and "lon" in xcoord.name - and "lat" in ycoord.name - ): - xcoord = xcoord * np.pi / 180 - ycoord = ycoord * np.pi / 180 - in_degree = True - elif "rad" in units: - pass - else: - in_metres = True - - # calculate the gridcell boundaries - xbounds = self.decoder.get_plotbounds(xcoord, arr.coords) - ybounds = self.decoder.get_plotbounds(ycoord, arr.coords) - if xbounds.ndim == 1: - xbounds, ybounds = np.meshgrid(xbounds, ybounds) - - # calculate the weights based on the units - if xcoord.ndim == 2 or self.decoder.is_unstructured(self.arr): - warn( - "[%s] - Curvilinear grids are not supported! " - "Using constant grid cell area weights!" % self.logger.name, - PsyPlotRuntimeWarning, - ) - weights = np.ones_like(xcoord.values) - elif in_metres: - weights = np.abs(xbounds[:-1, :-1] - xbounds[1:, 1:]) * ( - np.abs(ybounds[:-1, :-1] - ybounds[1:, 1:]) - ) - else: - weights = np.abs(xbounds[:-1, :-1] - xbounds[1:, 1:]) * ( - np.sin(ybounds[:-1, :-1]) - np.sin(ybounds[1:, 1:]) - ) - - # normalize the weights by dividing through the sum - if in_degree: - xmask = (xcoord_orig.values < -400) | (xcoord_orig.values > 400) - ymask = (ycoord_orig.values < -200) | (ycoord_orig.values > 200) - if xmask.any() or ymask.any(): - if xmask.ndim == 1 and weights.ndim != 1: - xmask, ymask = np.meshgrid(xmask, ymask) - weights[xmask | ymask] = np.nan - if np.any(~np.isnan(weights)): - weights /= np.nansum(weights) - return weights - - def _gridweights_cdo(self): - """Estimate the gridweights using CDOs""" - from tempfile import NamedTemporaryFile - - from cdo import Cdo - - sdims = {self.get_dim("y"), self.get_dim("x")} - cdo = Cdo() - fname = NamedTemporaryFile(prefix="psy", suffix=".nc").name - arr = self.arr - base = arr.psy.base - dims = arr.dims - ds = arr.isel(**{d: 0 for d in set(dims) - sdims}).to_dataset() - for coord in six.itervalues(ds.coords): - bounds = coord.attrs.get("bounds", coord.encoding.get("bounds")) - if ( - bounds - and bounds in set(base.coords) - set(ds.coords) - and sdims.intersection(base.coords[bounds].dims) - ): - ds[bounds] = base.sel( - **{d: arr.coords[d].values for d in sdims} - ).coords[bounds] - ds = ds.drop_vars( - [c.name for c in six.itervalues(ds.coords) if not c.ndim] - ) - to_netcdf(ds, fname) - ret = cdo.gridweights(input=fname, returnArray="cell_weights") - try: - os.remove(fname) - except Exception: - pass - return ret - - def _weights_to_da(self, weights, keepdims=False, keepshape=False): - """Convert the 2D weights into a DataArray and potentially enlarge it""" - arr = self.arr - xcoord = self.get_coord("x") - ycoord = self.get_coord("y") - sdims = (self.get_dim("y"), self.get_dim("x")) - if sdims[0] == sdims[1]: # unstructured grids - sdims = sdims[:1] - if (ycoord.name, xcoord.name) != sdims: - attrs = dict(coordinates=ycoord.name + " " + xcoord.name) - else: - attrs = {} - - # reshape and expand if necessary - if not keepdims and not keepshape: - coords = {ycoord.name: ycoord, xcoord.name: xcoord} - dims = sdims - elif keepshape: - if with_dask: - from dask.array import broadcast_to, notnull - else: - from numpy import broadcast_to, isnan - - def notnull(a): - return ~isnan(a) - - dims = arr.dims - coords = arr.coords - weights = broadcast_to(weights / weights.sum(), arr.shape) - # set nans to zero weigths. This step takes quite a lot of time for - # large arrays since it involves a copy of the entire `arr` - weights = weights * notnull(arr) - # normalize the weights - if with_dask: - summed_weights = weights.sum( - axis=tuple(map(dims.index, sdims)), keepdims=True - ) - else: - summed_weights = weights.sum( - axis=tuple(map(dims.index, sdims)) - ) - weights = weights / summed_weights - else: - dims = arr.dims - coords = arr.isel( - **{d: 0 if d not in sdims else slice(None) for d in dims} - ).coords - weights = weights.reshape( - tuple( - 1 if dim not in sdims else s - for s, dim in zip(arr.shape, arr.dims) - ) - ) - return xr.DataArray( - weights, dims=dims, coords=coords, name="cell_weights", attrs=attrs - ) - - def gridweights(self, keepdims=False, keepshape=False, use_cdo=None): - """Calculate the cell weights for each grid cell - - Parameters - ---------- - keepdims: bool - If True, keep the number of dimensions - keepshape: bool - If True, keep the exact shape as the source array and the missing - values in the array are masked - use_cdo: bool or None - If True, use Climate Data Operators (CDOs) to calculate the - weights. Note that this is used automatically for unstructured - grids. If None, it depends on the ``'gridweights.use_cdo'`` - item in the :attr:`psyplot.rcParams`. - - Returns - ------- - xarray.DataArray - The 2D-DataArray with the grid weights""" - if use_cdo is None: - use_cdo = rcParams["gridweights.use_cdo"] - if not use_cdo and self.decoder.is_unstructured(self.arr): - use_cdo = True - if use_cdo is None or use_cdo: - try: - weights = self._gridweights_cdo() - except Exception: - if use_cdo: - raise - else: - weights = self._gridweights() - else: - weights = self._gridweights() - - return self._weights_to_da( - weights, keepdims=keepdims, keepshape=keepshape - ) - - def _fldaverage_args(self): - """Masked array, xname, yname and axis for calculating the average""" - arr = self.arr - sdims = (self.get_dim("y"), self.get_dim("x")) - if sdims[0] == sdims[1]: - sdims = sdims[:1] - axis = tuple(map(arr.dims.index, sdims)) - return arr, sdims, axis - - def _insert_fldmean_bounds(self, da, keepdims=False): - xcoord = self.get_coord("x") - ycoord = self.get_coord("y") - sdims = (self.get_dim("y"), self.get_dim("x")) - xbounds = np.array([[xcoord.min(), xcoord.max()]]) - ybounds = np.array([[ycoord.min(), ycoord.max()]]) - xdims = (sdims[-1], "bnds") if keepdims else ("bnds",) - ydims = (sdims[0], "bnds") if keepdims else ("bnds",) - xattrs = xcoord.attrs.copy() - xattrs.pop("bounds", None) - yattrs = ycoord.attrs.copy() - yattrs.pop("bounds", None) - da.psy.base.coords[xcoord.name + "_bnds"] = xr.Variable( - xdims, xbounds if keepdims else xbounds[0], attrs=xattrs - ) - da.psy.base.coords[ycoord.name + "_bnds"] = xr.Variable( - ydims, ybounds if keepdims else ybounds[0], attrs=yattrs - ) - - def fldmean(self, keepdims=False): - """Calculate the weighted mean over the x- and y-dimension - - This method calculates the weighted mean of the spatial dimensions. - Weights are calculated using the :meth:`gridweights` method, missing - values are ignored. x- and y-dimensions are identified using the - :attr:`decoder`s :meth:`~CFDecoder.get_xname` and - :meth:`~CFDecoder.get_yname` methods. - - Parameters - ---------- - keepdims: bool - If True, the dimensionality of this array is maintained - - Returns - ------- - xr.DataArray - The computed fldmeans. The dimensions are the same as in this - array, only the spatial dimensions are omitted if `keepdims` is - False. - - See Also - -------- - fldstd: For calculating the weighted standard deviation - fldpctl: For calculating weighted percentiles - """ - gridweights = self.gridweights() - arr, sdims, axis = self._fldaverage_args() - - xcoord = self.decoder.get_x( - next(six.itervalues(self.base_variables)), arr.coords - ) - ycoord = self.decoder.get_y( - next(six.itervalues(self.base_variables)), arr.coords - ) - means = ((arr * gridweights)).sum(axis=axis) * ( - gridweights.size / arr.notnull().sum(axis=axis) - ) - if keepdims: - means = means.expand_dims(sdims, axis=axis) - - if keepdims: - means[xcoord.name] = xcoord.mean().expand_dims(xcoord.dims[0]) - means[ycoord.name] = ycoord.mean().expand_dims(ycoord.dims[0]) - else: - means[xcoord.name] = xcoord.mean() - means[ycoord.name] = ycoord.mean() - means.coords[xcoord.name].attrs["bounds"] = xcoord.name + "_bnds" - means.coords[ycoord.name].attrs["bounds"] = ycoord.name + "_bnds" - self._insert_fldmean_bounds(means, keepdims) - means.name = arr.name - return means - - def fldstd(self, keepdims=False): - """Calculate the weighted standard deviation over x- and y-dimension - - This method calculates the weighted standard deviation of the spatial - dimensions. Weights are calculated using the :meth:`gridweights` - method, missing values are ignored. x- and y-dimensions are identified - using the :attr:`decoder`s :meth:`~CFDecoder.get_xname` and - :meth:`~CFDecoder.get_yname` methods. - - Parameters - ---------- - keepdims: bool - If True, the dimensionality of this array is maintained - - Returns - ------- - xr.DataArray - The computed standard deviations. The dimensions are the same as - in this array, only the spatial dimensions are omitted if - `keepdims` is False. - - See Also - -------- - fldmean: For calculating the weighted mean - fldpctl: For calculating weighted percentiles - """ - arr, sdims, axis = self._fldaverage_args() - means = self.fldmean(keepdims=True) - weights = self.gridweights(keepshape=True) - variance = ((arr - means.values) ** 2 * weights).sum(axis=axis) - if keepdims: - variance = variance.expand_dims(sdims, axis=axis) - for key, coord in six.iteritems(means.coords): - if key not in variance.coords: - dims = set(sdims).intersection(coord.dims) - variance[key] = ( - coord - if keepdims - else coord.isel(**dict(zip(dims, repeat(0)))) - ) - for key, coord in six.iteritems(means.psy.base.coords): - if key not in variance.psy.base.coords: - dims = set(sdims).intersection(coord.dims) - variance.psy.base[key] = ( - coord - if keepdims - else coord.isel(**dict(zip(dims, repeat(0)))) - ) - std = variance**0.5 - std.name = arr.name - return std - - def fldpctl(self, q, keepdims=False): - """Calculate the percentiles along the x- and y-dimensions - - This method calculates the specified percentiles along the given - dimension. Percentiles are weighted by the :meth:`gridweights` method - and missing values are ignored. x- and y-dimensions are estimated - through the :attr:`decoder`s :meth:`~CFDecoder.get_xname` and - :meth:`~CFDecoder.get_yname` methods - - Parameters - ---------- - q: float or list of floats between 0 and 100 - The quantiles to estimate - keepdims: bool - If True, the number of dimensions of the array are maintained - - Returns - ------- - xr.DataArray - The data array with the dimensions. If `q` is a list or `keepdims` - is True, the first dimension will be the percentile ``'pctl'``. - The other dimensions are the same as in this array, only the - spatial dimensions are omitted if `keepdims` is False. - - See Also - -------- - fldstd: For calculating the weighted standard deviation - fldmean: For calculating the weighted mean - - Warnings - -------- - This method does load the entire array into memory! So take care if you - handle big data.""" - gridweights = self.gridweights(keepshape=True) - arr = self.arr - - q = np.asarray(q) / 100.0 - if not (np.all(q >= 0) and np.all(q <= 100)): - raise ValueError("q should be in [0, 100]") - reduce_shape = False if keepdims else (not bool(q.ndim)) - if not q.ndim: - q = q[np.newaxis] - data = arr.values.copy() - sdims, axis = self._fldaverage_args()[1:] - weights = gridweights.values - # flatten along the spatial axis - for ax in axis: - data = np.rollaxis(data, ax, 0) - weights = np.rollaxis(weights, ax, 0) - data = data.reshape( - (np.product(data.shape[: len(axis)]),) + data.shape[len(axis) :] - ) - weights = weights.reshape( - (np.product(weights.shape[: len(axis)]),) - + weights.shape[len(axis) :] - ) - - # sort the data - sorter = np.argsort(data, axis=0) - all_indices = map(tuple, product(*map(range, data.shape[1:]))) - for indices in all_indices: - indices = (slice(None),) + indices - data.__setitem__( - indices, data.__getitem__(indices)[sorter.__getitem__(indices)] - ) - weights.__setitem__( - indices, - weights.__getitem__(indices)[sorter.__getitem__(indices)], - ) - - # compute the percentiles - try: - weights = np.nancumsum(weights, axis=0) - 0.5 * weights - except AttributeError: - notnull = ~np.isnan(weights) - weights[notnull] = np.cumsum(weights[notnull]) - all_indices = map(tuple, product(*map(range, data.shape[1:]))) - pctl = np.zeros((len(q),) + data.shape[1:]) - - for indices in all_indices: - indices = (slice(None),) + indices - mask = ~np.isnan(data.__getitem__(indices)) - pctl.__setitem__( - indices, - np.interp( - q, - weights.__getitem__(indices)[mask], - data.__getitem__(indices)[mask], - ), - ) - - # setup the data array and it's coordinates - xcoord = self.decoder.get_x( - next(six.itervalues(self.base_variables)), arr.coords - ) - ycoord = self.decoder.get_y( - next(six.itervalues(self.base_variables)), arr.coords - ) - coords = dict(arr.coords) - if keepdims: - pctl = pctl.reshape( - (len(q),) - + tuple(1 if i in axis else s for i, s in enumerate(arr.shape)) - ) - coords[xcoord.name] = xcoord.mean().expand_dims(xcoord.dims[0]) - coords[ycoord.name] = ycoord.mean().expand_dims(ycoord.dims[0]) - dims = arr.dims - else: - coords[xcoord.name] = xcoord.mean() - coords[ycoord.name] = ycoord.mean() - dims = tuple(d for d in arr.dims if d not in sdims) - if reduce_shape: - pctl = pctl[0] - coords["pctl"] = xr.Variable( - (), q[0] * 100.0, attrs={"long_name": "Percentile"} - ) - else: - coords["pctl"] = xr.Variable( - ("pctl",), q * 100.0, attrs={"long_name": "Percentile"} - ) - dims = ("pctl",) + dims - coords[xcoord.name].attrs["bounds"] = xcoord.name + "_bnds" - coords[ycoord.name].attrs["bounds"] = ycoord.name + "_bnds" - coords = { - name: c for name, c in coords.items() if set(c.dims) <= set(dims) - } - ret = xr.DataArray( - pctl, - name=arr.name, - dims=dims, - coords=coords, - attrs=arr.attrs.copy(), - ) - self._insert_fldmean_bounds(ret, keepdims) - return ret - - -class ArrayList(list): - """Base class for creating a list of interactive arrays from a dataset - - This list contains and manages :class:`InteractiveArray` instances""" - - docstrings.keep_params("InteractiveBase.parameters", "auto_update") - - @property - def dims(self): - """Dimensions of the arrays in this list""" - return set(chain(*(arr.dims for arr in self))) - - @property - def dims_intersect(self): - """Dimensions of the arrays in this list that are used in all arrays""" - return set.intersection( - *map( - set, (getattr(arr, "dims_intersect", arr.dims) for arr in self) - ) - ) - - @property - def arr_names(self): - """Names of the arrays (!not of the variables!) in this list - - This attribute can be set with an iterable of unique names to change - the array names of the data objects in this list.""" - return list(arr.psy.arr_name for arr in self) - - @arr_names.setter - def arr_names(self, value): - value = list(islice(value, 0, len(self))) - if not len(set(value)) == len(self): - raise ValueError( - "Got %i unique array names for %i data objects!" - % (len(set(value)), len(self)) - ) - for arr, n in zip(self, value): - arr.psy.arr_name = n - - @property - def names(self): - """Set of the variable in this list""" - ret = set() - for arr in self: - if isinstance(arr, InteractiveList): - ret.update(arr.names) - else: - ret.add(arr.name) - return ret - - @property - def all_names(self): - """The variable names for each of the arrays in this list""" - return [ - _get_variable_names(arr) - if not isinstance(arr, ArrayList) - else arr.all_names - for arr in self - ] - - @property - def all_dims(self): - """The dimensions for each of the arrays in this list""" - return [ - _get_dims(arr) if not isinstance(arr, ArrayList) else arr.all_dims - for arr in self - ] - - @property - def is_unstructured(self): - """A boolean for each array whether it is unstructured or not""" - return [ - arr.psy.decoder.is_unstructured(arr) - if not isinstance(arr, ArrayList) - else arr.is_unstructured - for arr in self - ] - - @property - def coords(self): - """Names of the coordinates of the arrays in this list""" - return set(chain(*(arr.coords for arr in self))) - - @property - def coords_intersect(self): - """Coordinates of the arrays in this list that are used in all arrays""" - return set.intersection( - *map( - set, - (getattr(arr, "coords_intersect", arr.coords) for arr in self), - ) - ) - - @property - def with_plotter(self): - """The arrays in this instance that are visualized with a plotter""" - return self.__class__( - (arr for arr in self if arr.psy.plotter is not None), - auto_update=bool(self.auto_update), - ) - - no_auto_update = property( - _no_auto_update_getter, doc=_no_auto_update_getter.__doc__ - ) - - @no_auto_update.setter - def no_auto_update(self, value): - for arr in self: - arr.psy.no_auto_update = value - self.no_auto_update.value = bool(value) - - @property - def logger(self): - """:class:`logging.Logger` of this instance""" - try: - return self._logger - except AttributeError: - name = "%s.%s" % (self.__module__, self.__class__.__name__) - self._logger = logging.getLogger(name) - self.logger.debug("Initializing...") - return self._logger - - @logger.setter - def logger(self, value): - self._logger = value - - @property - def arrays(self): - """A list of all the :class:`xarray.DataArray` instances in this list""" - return list( - chain.from_iterable( - ( - [arr] - if not isinstance(arr, InteractiveList) - else arr.arrays - for arr in self - ) - ) - ) - - @docstrings.get_sections( - base="ArrayList.rename", sections=["Parameters", "Raises"] - ) - @dedent - def rename(self, arr, new_name=True): - """ - Rename an array to find a name that isn't already in the list - - Parameters - ---------- - arr: InteractiveBase - A :class:`InteractiveArray` or :class:`InteractiveList` instance - whose name shall be checked - new_name: bool or str - If False, and the ``arr_name`` attribute of the new array is - already in the list, a ValueError is raised. - If True and the ``arr_name`` attribute of the new array is not - already in the list, the name is not changed. Otherwise, if the - array name is already in use, `new_name` is set to 'arr{0}'. - If not True, this will be used for renaming (if the array name of - `arr` is in use or not). ``'{0}'`` is replaced by a counter - - Returns - ------- - InteractiveBase - `arr` with changed ``arr_name`` attribute - bool or None - True, if the array has been renamed, False if not and None if the - array is already in the list - - Raises - ------ - ValueError - If it was impossible to find a name that isn't already in the list - ValueError - If `new_name` is False and the array is already in the list""" - name_in_me = arr.psy.arr_name in self.arr_names - if not name_in_me: - return arr, False - elif name_in_me and not self._contains_array(arr): - if new_name is False: - raise ValueError( - "Array name %s is already in use! Set the `new_name` " - "parameter to None for renaming!" % arr.psy.arr_name - ) - elif new_name is True: - new_name = new_name if isstring(new_name) else "arr{0}" - arr.psy.arr_name = self.next_available_name(new_name) - return arr, True - return arr, None - - docstrings.keep_params("ArrayList.rename.parameters", "new_name") - docstrings.keep_params("InteractiveBase.parameters", "auto_update") - - @docstrings.get_sections(base="ArrayList") - @docstrings.dedent - def __init__(self, iterable=[], attrs={}, auto_update=None, new_name=True): - """ - Parameters - ---------- - iterable: iterable - The iterable (e.g. another list) defining this list - attrs: dict-like or iterable, optional - Global attributes of this list - %(InteractiveBase.parameters.auto_update)s - %(ArrayList.rename.parameters.new_name)s""" - super(ArrayList, self).__init__() - self.attrs = dict(attrs) - if auto_update is None: - auto_update = rcParams["lists.auto_update"] - self.auto_update = not bool(auto_update) - # append the data in order to set the correct names - self.extend( - filter( - lambda arr: isinstance( - getattr(arr, "psy", None), InteractiveBase - ), - iterable, - ), - new_name=new_name, - ) - - def copy(self, deep=False): - """Returns a copy of the list - - Parameters - ---------- - deep: bool - If False (default), only the list is copied and not the contained - arrays, otherwise the contained arrays are deep copied""" - if not deep: - return self.__class__( - self[:], - attrs=self.attrs.copy(), - auto_update=not bool(self.no_auto_update), - ) - else: - return self.__class__( - [arr.psy.copy(deep) for arr in self], - attrs=self.attrs.copy(), - auto_update=not bool(self.auto_update), - ) - - docstrings.keep_params("InteractiveArray.update.parameters", "method") - - @classmethod - @docstrings.get_sections( - base="ArrayList.from_dataset", - sections=["Parameters", "Other Parameters", "Returns"], - ) - @docstrings.dedent - def from_dataset( - cls, - base, - method="isel", - default_slice=None, - decoder=None, - auto_update=None, - prefer_list=False, - squeeze=True, - attrs=None, - load=False, - **kwargs, - ): - """ - Construct an ArrayList instance from an existing base dataset - - Parameters - ---------- - base: xarray.Dataset - Dataset instance that is used as reference - %(InteractiveArray.update.parameters.method)s - %(InteractiveBase.parameters.auto_update)s - prefer_list: bool - If True and multiple variable names pher array are found, the - :class:`InteractiveList` class is used. Otherwise the arrays are - put together into one :class:`InteractiveArray`. - default_slice: indexer - Index (e.g. 0 if `method` is 'isel') that shall be used for - dimensions not covered by `dims` and `furtherdims`. If None, the - whole slice will be used. Note that the `default_slice` is always - based on the `isel` method. - decoder: CFDecoder or dict - Arguments for the decoder. This can be one of - - - an instance of :class:`CFDecoder` - - a subclass of :class:`CFDecoder` - - a dictionary with keyword-arguments to the automatically - determined decoder class - - None to automatically set the decoder - squeeze: bool, optional - Default True. If True, and the created arrays have a an axes with - length 1, it is removed from the dimension list (e.g. an array - with shape (3, 4, 1, 5) will be squeezed to shape (3, 4, 5)) - attrs: dict, optional - Meta attributes that shall be assigned to the selected data arrays - (additional to those stored in the `base` dataset) - load: bool or dict - If True, load the data from the dataset using the - :meth:`xarray.DataArray.load` method. If :class:`dict`, those will - be given to the above mentioned ``load`` method - - Other Parameters - ---------------- - %(setup_coords.parameters)s - - Returns - ------- - ArrayList - The list with the specified :class:`InteractiveArray` instances - that hold a reference to the given `base`""" - try: - load = dict(load) - except (TypeError, ValueError): - - def maybe_load(arr): - return arr.load() if load else arr - - else: - - def maybe_load(arr): - return arr.load(**load) - - def iter_dims(dims): - """Split the given dictionary into multiples and iterate over it""" - if not dims: - while 1: - yield {} - else: - dims = dict(dims) - keys = dims.keys() - for vals in zip(*map(cycle, map(safe_list, dims.values()))): - yield dict(zip(keys, vals)) - - def recursive_selection(key, dims, names): - names = safe_list(names) - if len(names) > 1 and prefer_list: - keys = ("arr%i" % i for i in range(len(names))) - return InteractiveList( - starmap(sel_method, zip(keys, iter_dims(dims), names)), - auto_update=auto_update, - arr_name=key, - ) - elif len(names) > 1: - return sel_method(key, dims, tuple(names)) - else: - return sel_method(key, dims, names[0]) - - def ds2arr(arr): - base_var = next( - var - for key, var in arr.variables.items() - if key not in arr.coords - ) - attrs = base_var.attrs - arr = arr.to_array() - if "coordinates" in base_var.encoding: - arr.encoding["coordinates"] = base_var.encoding["coordinates"] - arr.attrs.update(attrs) - return arr - - decoder_input = decoder - - def get_decoder(arr): - if decoder_input is None: - return CFDecoder.get_decoder(base, arr) - elif isinstance(decoder_input, CFDecoder): - return decoder_input - elif isinstance(decoder_input, dict): - return CFDecoder.get_decoder(base, arr, **decoder_input) - else: - return decoder_input(base) - - def add_missing_dimensions(arr): - # add the missing dimensions to the dataset. This is not anymore - # done by default from xarray >= 0.9 but we need it to ensure the - # interactive treatment of DataArrays - missing = set(arr.dims).difference(base.coords) - {"variable"} - for dim in missing: - try: - size = base.sizes[dim] - except AttributeError: - # old xarray version - size = base.dims[dim] - base[dim] = arr.coords[dim] = np.arange(size) - - if squeeze: - - def squeeze_array(arr): - return arr.isel( - **{ - dim: 0 - for i, dim in enumerate(arr.dims) - if arr.shape[i] == 1 - } - ) - - else: - - def squeeze_array(arr): - return arr - - if method == "isel": - - def sel_method(key, dims, name=None): - if name is None: - return recursive_selection(key, dims, dims.pop("name")) - elif isinstance( - name, six.string_types - ) or not utils.is_iterable(name): - arr = base[name] - decoder = get_decoder(arr) - dims = decoder.correct_dims(arr, dims) - else: - arr = base[list(name)] - decoder = get_decoder(base[name[0]]) - dims = decoder.correct_dims(base[name[0]], dims) - def_slice = ( - slice(None) if default_slice is None else default_slice - ) - dims.update( - { - dim: def_slice - for dim in set(arr.dims).difference(dims) - if dim != "variable" - } - ) - add_missing_dimensions(arr) - ret = arr.isel(**dims) - if not isinstance(ret, xr.DataArray): - ret = ds2arr(ret) - ret = squeeze_array(ret) - # delete the variable dimension for the idims - dims.pop("variable", None) - ret.psy.init_accessor( - arr_name=key, base=base, idims=dims, decoder=decoder - ) - return maybe_load(ret) - - else: - - def sel_method(key, dims, name=None): - if name is None: - return recursive_selection(key, dims, dims.pop("name")) - elif isinstance( - name, six.string_types - ) or not utils.is_iterable(name): - arr = base[name] - decoder = get_decoder(arr) - dims = decoder.correct_dims(arr, dims) - else: - arr = base[list(name)] - decoder = get_decoder(base[name[0]]) - dims = decoder.correct_dims(base[name[0]], dims) - if default_slice is not None: - if isinstance(default_slice, slice): - dims.update( - { - dim: default_slice - for dim in set(arr.dims).difference(dims) - if dim != "variable" - } - ) - else: - dims.update( - { - dim: arr.coords[dim][default_slice] - for dim in set(arr.dims).difference(dims) - if dim != "variable" - } - ) - kws = dims.copy() - kws["method"] = method - # the sel method does not work with slice objects - for dim, val in dims.items(): - if isinstance(val, slice): - if val == slice(None): - kws.pop(dim) # the full slice is the default - else: - kws.pop("method", None) - add_missing_dimensions(arr) - try: - ret = arr.sel(**kws) - except KeyError: - _fix_times(kws) - ret = arr.sel(**kws) - if not isinstance(ret, xr.DataArray): - ret = ds2arr(ret) - ret = squeeze_array(ret) - ret.psy.init_accessor(arr_name=key, base=base, decoder=decoder) - return maybe_load(ret) - - if "name" not in kwargs: - default_names = list( - key for key in base.variables if key not in base.coords - ) - try: - default_names.sort() - except TypeError: - pass - kwargs["name"] = default_names - names = setup_coords(**kwargs) - # check coordinates - possible_keys = ["t", "x", "y", "z", "name"] + list(base.dims) - for key in set(chain(*six.itervalues(names))): - utils.check_key(key, possible_keys, name="dimension") - instance = cls( - starmap(sel_method, six.iteritems(names)), - attrs=base.attrs, - auto_update=auto_update, - ) - # convert to interactive lists if an instance is not - if prefer_list and any( - not isinstance(arr, InteractiveList) for arr in instance - ): - # if any instance is an interactive list, than convert the others - if any(isinstance(arr, InteractiveList) for arr in instance): - for i, arr in enumerate(instance): - if not isinstance(arr, InteractiveList): - instance[i] = InteractiveList([arr]) - else: # put everything into one single interactive list - instance = cls( - [ - InteractiveList( - instance, attrs=base.attrs, auto_update=auto_update - ) - ] - ) - instance[0].psy.arr_name = instance[0][0].psy.arr_name - if attrs is not None: - for arr in instance: - arr.attrs.update(attrs) - return instance - - @classmethod - def _get_dsnames( - cls, - data, - ignore_keys=["attrs", "plotter", "ds"], - concat_dim=False, - combine=False, - ): - """Recursive method to get all the file names out of a dictionary - `data` created with the :meth`array_info` method""" - - def filter_ignores(item): - return item[0] not in ignore_keys and isinstance(item[1], dict) - - if "fname" in data: - return { - tuple( - [data["fname"], data["store"]] - + ([data.get("concat_dim")] if concat_dim else []) - + ([data.get("combine")] if combine else []) - ) - } - return set( - chain( - *map( - partial( - cls._get_dsnames, - concat_dim=concat_dim, - combine=combine, - ignore_keys=ignore_keys, - ), - dict(filter(filter_ignores, six.iteritems(data))).values(), - ) - ) - ) - - @classmethod - def _get_ds_descriptions( - cls, data, ds_description={"ds", "fname", "arr"}, **kwargs - ): - def new_dict(): - return defaultdict(list) - - ret = defaultdict(new_dict) - ds_description = set(ds_description) - for d in cls._get_ds_descriptions_unsorted(data, **kwargs): - try: - num = d.get("num") or d["ds"].psy.num - except KeyError: - raise ValueError( - "Could not find either the dataset number nor the dataset " - "in the data! However one must be provided." - ) - d_ret = ret[num] - for key, val in six.iteritems(d): - if key == "arr": - d_ret["arr"].append(d["arr"]) - else: - d_ret[key] = val - return ret - - @classmethod - def _get_ds_descriptions_unsorted( - cls, data, ignore_keys=["attrs", "plotter"], nums=None - ): - """Recursive method to get all the file names or datasets out of a - dictionary `data` created with the :meth`array_info` method""" - ds_description = {"ds", "fname", "num", "arr", "store"} - if "ds" in data: - # make sure that the data set has a number assigned to it - data["ds"].psy.num - keys_in_data = ds_description.intersection(data) - if keys_in_data: - return {key: data[key] for key in keys_in_data} - for key in ignore_keys: - data.pop(key, None) - func = partial( - cls._get_ds_descriptions_unsorted, - ignore_keys=ignore_keys, - nums=nums, - ) - return chain( - *map( - lambda d: [d] if isinstance(d, dict) else d, - map(func, six.itervalues(data)), - ) - ) - - @classmethod - @docstrings.get_sections(base="ArrayList.from_dict") - @docstrings.dedent - def from_dict( - cls, - d, - alternative_paths={}, - datasets=None, - pwd=None, - ignore_keys=["attrs", "plotter", "ds"], - only=None, - chname={}, - **kwargs, - ): - """ - Create a list from the dictionary returned by :meth:`array_info` - - This classmethod creates an :class:`~psyplot.data.ArrayList` instance - from a dictionary containing filename, dimension infos and array names - - Parameters - ---------- - d: dict - The dictionary holding the data - alternative_paths: dict or list or str - A mapping from original filenames as used in `d` to filenames that - shall be used instead. If `alternative_paths` is not None, - datasets must be None. Paths must be accessible from the current - working directory. - If `alternative_paths` is a list (or any other iterable) is - provided, the file names will be replaced as they appear in `d` - (note that this is very unsafe if `d` is not and dict) - datasets: dict or list or None - A mapping from original filenames in `d` to the instances of - :class:`xarray.Dataset` to use. If it is an iterable, the same - holds as for the `alternative_paths` parameter - pwd: str - Path to the working directory from where the data can be imported. - If None, use the current working directory. - ignore_keys: list of str - Keys specified in this list are ignored and not seen as array - information (note that ``attrs`` are used anyway) - only: string, list or callable - Can be one of the following three things: - - - a string that represents a pattern to match the array names - that shall be included - - a list of array names to include - - a callable with two arguments, a string and a dict such as - - .. code-block:: python - - def filter_func(arr_name: str, info: dict): -> bool - ''' - Filter the array names - - This function should return True if the array shall be - included, else False - - Parameters - ---------- - arr_name: str - The array name (i.e. the ``arr_name`` attribute) - info: dict - The dictionary with the array informations. Common - keys are ``'name'`` that points to the variable name - and ``'dims'`` that points to the dimensions and - ``'fname'`` that points to the file name - ''' - return True or False - - The function should return ``True`` if the array shall be - included, else ``False``. This function will also be given to - subsequents instances of :class:`InteractiveList` objects that - are contained in the returned value - chname: dict - A mapping from variable names in the project to variable names - that should be used instead - - Other Parameters - ---------------- - ``**kwargs`` - Any other parameter from the `psyplot.data.open_dataset` function - %(open_dataset.parameters)s - - Returns - ------- - psyplot.data.ArrayList - The list with the interactive objects - - See Also - -------- - from_dataset, array_info""" - pwd = pwd or os.getcwd() - if only is None: - - def only_filter(arr_name, info): - return True - - elif callable(only): - only_filter = only - elif isstring(only): - - def only_filter(arr_name, info): - return patt.search(arr_name) is not None - - patt = re.compile(only) - only = None - else: - - def only_filter(arr_name, info): - return arr_name in save_only - - save_only = only - only = None - - def get_fname_use(fname): - squeeze = isstring(fname) - fname = safe_list(fname) - ret = tuple( - f - if utils.is_remote_url(f) or osp.isabs(f) - else osp.join(pwd, f) - for f in fname - ) - return ret[0] if squeeze else ret - - def get_name(name): - if not isstring(name): - return list(map(get_name, name)) - else: - return chname.get(name, name) - - if not isinstance(alternative_paths, dict): - it = iter(alternative_paths) - alternative_paths = defaultdict(partial(next, it, None)) - # first open all datasets if not already done - if datasets is None: - replace_concat_dim = "concat_dim" not in kwargs - replace_combine = "combine" not in kwargs - - names_and_stores = cls._get_dsnames( - d, concat_dim=True, combine=True - ) - datasets = {} - for ( - fname, - (store_mod, store_cls), - concat_dim, - combine, - ) in names_and_stores: - fname_use = fname - got = True - if replace_concat_dim and concat_dim is not None: - kwargs["concat_dim"] = concat_dim - elif replace_concat_dim and concat_dim is None: - kwargs.pop("concat_dim", None) - if replace_combine and combine is not None: - kwargs["combine"] = combine - elif replace_combine and combine is None: - kwargs.pop("combine", None) - try: - fname_use = alternative_paths[fname] - except KeyError: - got = False - if not got or not fname_use: - if fname is not None: - fname_use = get_fname_use(fname) - if fname_use is not None: - datasets[fname] = _open_ds_from_store( - fname_use, store_mod, store_cls, **kwargs - ) - if alternative_paths is not None: - for fname in set(alternative_paths).difference(datasets): - datasets[fname] = _open_ds_from_store(fname, **kwargs) - elif not isinstance(datasets, dict): - it_datasets = iter(datasets) - datasets = defaultdict(partial(next, it_datasets, None)) - arrays = [0] * len(d) - i = 0 - for arr_name, info in six.iteritems(d): - if arr_name in ignore_keys or not only_filter(arr_name, info): - arrays.pop(i) - continue - if not {"fname", "ds", "arr"}.intersection(info): - # the described object is an InteractiveList - arr = InteractiveList.from_dict( - info, - alternative_paths=alternative_paths, - datasets=datasets, - chname=chname, - ) - if not arr: - warn("Skipping empty list %s!" % arr_name) - arrays.pop(i) - continue - else: - if "arr" in info: - arr = info.pop("arr") - elif "ds" in info: - arr = cls.from_dataset( - info["ds"], - dims=info["dims"], - name=get_name(info["name"]), - )[0] - else: - fname = info["fname"] - if fname is None: - warn( - "Could not open array %s because no filename was " - "specified!" % arr_name - ) - arrays.pop(i) - continue - try: # in case, datasets is a defaultdict - datasets[fname] - except KeyError: - pass - if fname not in datasets: - warn( - "Could not open array %s because %s was not in " - "the list of datasets!" % (arr_name, fname) - ) - arrays.pop(i) - continue - arr = cls.from_dataset( - datasets[fname], - dims=info["dims"], - name=get_name(info["name"]), - )[0] - for key, val in six.iteritems(info.get("attrs", {})): - arr.attrs.setdefault(key, val) - arr.psy.arr_name = arr_name - arrays[i] = arr - i += 1 - return cls(arrays, attrs=d.get("attrs", {})) - - docstrings.delete_params("get_filename_ds.parameters", "ds", "dump") - - @docstrings.get_sections(base="ArrayList.array_info") - @docstrings.dedent - def array_info( - self, - dump=None, - paths=None, - attrs=True, - standardize_dims=True, - pwd=None, - use_rel_paths=True, - alternative_paths={}, - ds_description={"fname", "store"}, - full_ds=True, - copy=False, - **kwargs, - ): - """ - Get dimension informations on you arrays - - This method returns a dictionary containing informations on the - array in this instance - - Parameters - ---------- - dump: bool - If True and the dataset has not been dumped so far, it is dumped to - a temporary file or the one generated by `paths` is used. If it is - False or both, `dump` and `paths` are None, no data will be stored. - If it is None and `paths` is not None, `dump` is set to True. - %(get_filename_ds.parameters.no_ds|dump)s - attrs: bool, optional - If True (default), the :attr:`ArrayList.attrs` and - :attr:`xarray.DataArray.attrs` attributes are included in the - returning dictionary - standardize_dims: bool, optional - If True (default), the real dimension names in the dataset are - replaced by x, y, z and t to be more general. - pwd: str - Path to the working directory from where the data can be imported. - If None, use the current working directory. - use_rel_paths: bool, optional - If True (default), paths relative to the current working directory - are used. Otherwise absolute paths to `pwd` are used - ds_description: 'all' or set of {'fname', 'ds', 'num', 'arr', 'store'} - Keys to describe the datasets of the arrays. If all, all keys - are used. The key descriptions are - - fname - the file name is inserted in the ``'fname'`` key - store - the data store class and module is inserted in the ``'store'`` - key - ds - the dataset is inserted in the ``'ds'`` key - num - The unique number assigned to the dataset is inserted in the - ``'num'`` key - arr - The array itself is inserted in the ``'arr'`` key - full_ds: bool - If True and ``'ds'`` is in `ds_description`, the entire dataset is - included. Otherwise, only the DataArray converted to a dataset is - included - copy: bool - If True, the arrays and datasets are deep copied - - - Other Parameters - ---------------- - %(get_filename_ds.other_parameters)s - - Returns - ------- - dict - An ordered mapping from array names to dimensions and filename - corresponding to the array - - See Also - -------- - from_dict""" - saved_ds = kwargs.pop("_saved_ds", {}) - - def get_alternative(f): - return next( - filter( - lambda t: osp.samefile(f, t[0]), - six.iteritems(alternative_paths), - ), - [False, f], - ) - - if copy: - - def copy_obj(obj): - # try to get the number of the dataset and create only one copy - # copy for each dataset - try: - num = obj.psy.num - except AttributeError: - pass - else: - try: - return saved_ds[num] - except KeyError: - saved_ds[num] = obj.psy.copy(True) - return saved_ds[num] - return obj.psy.copy(True) - - else: - - def copy_obj(obj): - return obj - - ret = dict() - if ds_description == "all": - ds_description = {"fname", "ds", "num", "arr", "store"} - if paths is not None: - if dump is None: - dump = True - paths = iter(paths) - elif dump is None: - dump = False - if pwd is None: - pwd = os.getcwd() - for arr in self: - if isinstance(arr, InteractiveList): - ret[arr.arr_name] = arr.array_info( - dump, - paths, - pwd=pwd, - attrs=attrs, - standardize_dims=standardize_dims, - use_rel_paths=use_rel_paths, - ds_description=ds_description, - alternative_paths=alternative_paths, - copy=copy, - _saved_ds=saved_ds, - **kwargs, - ) - else: - if standardize_dims: - idims = arr.psy.decoder.standardize_dims( - next(arr.psy.iter_base_variables), arr.psy.idims - ) - else: - idims = arr.psy.idims - ret[arr.psy.arr_name] = d = {"dims": idims} - if "variable" in arr.coords: - d["name"] = [list(arr.coords["variable"].values)] - else: - d["name"] = arr.name - if "fname" in ds_description or "store" in ds_description: - fname, store_mod, store_cls = get_filename_ds( - arr.psy.base, dump=dump, paths=paths, **kwargs - ) - if "store" in ds_description: - d["store"] = (store_mod, store_cls) - if "fname" in ds_description: - d["fname"] = [] - for i, f in enumerate(safe_list(fname)): - if f is None or utils.is_remote_url(f): - d["fname"].append(f) - else: - found, f = get_alternative(f) - if use_rel_paths: - f = osp.relpath(f, pwd) - else: - f = osp.abspath(f) - d["fname"].append(f) - if fname is None or isinstance( - fname, six.string_types - ): - d["fname"] = d["fname"][0] - else: - d["fname"] = tuple(safe_list(fname)) - if arr.psy.base.psy._concat_dim is not None: - d["concat_dim"] = arr.psy.base.psy._concat_dim - if arr.psy.base.psy._combine is not None: - d["combine"] = arr.psy.base.psy._combine - if "ds" in ds_description: - if full_ds: - d["ds"] = copy_obj(arr.psy.base) - else: - d["ds"] = copy_obj(arr.to_dataset()) - if "num" in ds_description: - d["num"] = arr.psy.base.psy.num - if "arr" in ds_description: - d["arr"] = copy_obj(arr) - if attrs: - d["attrs"] = arr.attrs - ret["attrs"] = self.attrs - return ret - - def _get_tnames(self): - """Get the name of the time coordinate of the objects in this list""" - tnames = set() - for arr in self: - if isinstance(arr, InteractiveList): - tnames.update(arr.get_tnames()) - else: - tnames.add( - arr.psy.decoder.get_tname( - next(arr.psy.iter_base_variables), arr.coords - ) - ) - return tnames - {None} - - @docstrings.dedent - def _register_update( - self, - method="isel", - replot=False, - dims={}, - fmt={}, - force=False, - todefault=False, - ): - """ - Register new dimensions and formatoptions for updating. The keywords - are the same as for each single array - - Parameters - ---------- - %(InteractiveArray._register_update.parameters)s""" - - for arr in self: - arr.psy._register_update( - method=method, - replot=replot, - dims=dims, - fmt=fmt, - force=force, - todefault=todefault, - ) - - @docstrings.get_sections(base="ArrayList.start_update") - @dedent - def start_update(self, draw=None): - """ - Conduct the registered plot updates - - This method starts the updates from what has been registered by the - :meth:`update` method. You can call this method if you did not set the - `auto_update` parameter when calling the :meth:`update` method to True - and when the :attr:`no_auto_update` attribute is True. - - Parameters - ---------- - draw: bool or None - If True, all the figures of the arrays contained in this list will - be drawn at the end. If None, it defaults to the `'auto_draw'`` - parameter in the :attr:`psyplot.rcParams` dictionary - - See Also - -------- - :attr:`no_auto_update`, update""" - - def worker(arr): - results[arr.psy.arr_name] = arr.psy.start_update( - draw=False, queues=queues - ) - - if len(self) == 0: - return - - results = {} - threads = [ - Thread( - target=worker, args=(arr,), name="update_%s" % arr.psy.arr_name - ) - for arr in self - ] - jobs = [arr.psy._njobs for arr in self] - queues = [Queue() for _ in range(max(map(len, jobs)))] - # populate the queues - for i, arr in enumerate(self): - for j, n in enumerate(jobs[i]): - for k in range(n): - queues[j].put(arr.psy.arr_name) - for thread in threads: - thread.setDaemon(True) - for thread in threads: - thread.start() - for thread in threads: - thread.join() - if draw is None: - draw = rcParams["auto_draw"] - if draw: - self( - arr_name=[ - name for name, adraw in six.iteritems(results) if adraw - ] - ).draw() - if rcParams["auto_show"]: - self.show() - - docstrings.keep_params("InteractiveArray.update.parameters", "auto_update") - - @docstrings.get_sections(base="ArrayList.update") - @docstrings.dedent - def update( - self, - method="isel", - dims={}, - fmt={}, - replot=False, - auto_update=False, - draw=None, - force=False, - todefault=False, - enable_post=None, - **kwargs, - ): - """ - Update the coordinates and the plot - - This method updates all arrays in this list with the given coordinate - values and formatoptions. - - Parameters - ---------- - %(InteractiveArray._register_update.parameters)s - %(InteractiveArray.update.parameters.auto_update)s - %(ArrayList.start_update.parameters)s - enable_post: bool - If not None, enable (``True``) or disable (``False``) the - :attr:`~psyplot.plotter.Plotter.post` formatoption in the plotters - ``**kwargs`` - Any other formatoption or dimension that shall be updated - (additionally to those in `fmt` and `dims`) - - Notes - ----- - %(InteractiveArray.update.notes)s - - See Also - -------- - no_auto_update, start_update""" - dims = dict(dims) - fmt = dict(fmt) - vars_and_coords = set( - chain(self.dims, self.coords, ["name", "x", "y", "z", "t"]) - ) - furtherdims, furtherfmt = utils.sort_kwargs(kwargs, vars_and_coords) - dims.update(furtherdims) - fmt.update(furtherfmt) - - self._register_update( - method=method, - replot=replot, - dims=dims, - fmt=fmt, - force=force, - todefault=todefault, - ) - if enable_post is not None: - for arr in self.with_plotter: - arr.psy.plotter.enable_post = enable_post - if not self.no_auto_update or auto_update: - self.start_update(draw) - - def draw(self): - """Draws all the figures in this instance""" - for fig in set( - chain( - *map(lambda arr: arr.psy.plotter.figs2draw, self.with_plotter) - ) - ): - self.logger.debug("Drawing figure %s", fig.number) - fig.canvas.draw() - for arr in self: - if arr.psy.plotter is not None: - arr.psy.plotter._figs2draw.clear() - self.logger.debug("Done drawing.") - - def __call__(self, types=None, method="isel", fmts=[], **attrs): - """Get the arrays specified by their attributes - - Parameters - ---------- - types: type or tuple of types - Any class that shall be used for an instance check via - :func:`isinstance`. If not None, the :attr:`plotter` attribute - of the array is checked against this `types` - method: {'isel', 'sel'} - Selection method for the dimensions in the arrays to be used. - If `method` is 'isel', dimension values in `attrs` must correspond - to integer values as they are found in the - :attr:`InteractiveArray.idims` attribute. - Otherwise the :meth:`xarray.DataArray.coords` attribute is used. - fmts: list - List of formatoption strings. Only arrays with plotters who have - this formatoption are returned - ``**attrs`` - Parameters may be any attribute of the arrays in this instance, - including the matplotlib axes (``ax``), matplotlib figure - (``fig``) and the array name (``arr_name``). - Values may be iterables (e.g. lists) of the attributes to consider - or callable functions that accept the attribute as a value. If the - value is a string, it will be put into a list.""" - - def safe_item_list(key, val): - return key, val if callable(val) else safe_list(val) - - def filter_list(arr): - other_attrs = attrs.copy() - arr_names = other_attrs.pop("arr_name", None) - return ( - arr_names is None - or ( - arr_names(arr.psy.arr_name) - if callable(arr_names) - else arr.psy.arr_name in arr_names - ) - ) and len(arr) == len( - arr(types=types, method=method, **other_attrs) - ) - - if not attrs: - - def filter_by_attrs(arr): - return True - - elif method == "sel": - - def filter_by_attrs(arr): - if isinstance(arr, InteractiveList): - return filter_list(arr) - tname = arr.psy.decoder.get_tname( - next(six.itervalues(arr.psy.base_variables)) - ) - - def check_values(arr, key, vals): - if key == "arr_name": - attr = arr.psy.arr_name - elif key == "ax": - attr = arr.psy.ax - elif key == "fig": - attr = getattr(arr.psy.ax, "figure", None) - else: - try: - attr = getattr(arr, key) - except AttributeError: - return False - if np.ndim(attr): # do not filter for multiple items - return False - if hasattr(arr.psy, "decoder") and (arr.name == tname): - try: - vals = np.asarray(vals, dtype=np.datetime64) - except ValueError: - pass - else: - return attr.values.astype(vals.dtype) in vals - if callable(vals): - return vals(attr) - return getattr(attr, "values", attr) in vals - - return all( - check_values(arr, key, val) - for key, val in six.iteritems( - arr.psy.decoder.correct_dims( - next(six.itervalues(arr.psy.base_variables)), - attrs, - remove=False, - ) - ) - ) - - else: - - def check_values(arr, key, vals): - if key == "arr_name": - attr = arr.psy.arr_name - elif key == "ax": - attr = arr.psy.ax - elif key == "fig": - attr = getattr(arr.psy.ax, "figure", None) - elif key in arr.coords: - attr = arr.psy.idims[key] - else: - try: - attr = getattr(arr, key) - except AttributeError: - return False - if np.ndim(attr): # do not filter for multiple items - return False - if callable(vals): - return vals(attr) - return attr in vals - - def filter_by_attrs(arr): - if isinstance(arr, InteractiveList): - return filter_list(arr) - return all( - check_values(arr, key, val) - for key, val in six.iteritems( - arr.psy.decoder.correct_dims( - next(six.itervalues(arr.psy.base_variables)), - attrs, - remove=False, - ) - ) - ) - - attrs = dict(starmap(safe_item_list, six.iteritems(attrs))) - ret = self.__class__( - # iterable - ( - arr - for arr in self - if (types is None or isinstance(arr.psy.plotter, types)) - and filter_by_attrs(arr) - ), - # give itself as base and the auto_update parameter - auto_update=bool(self.auto_update), - ) - # now filter for the formatoptions - if fmts: - fmts = set(safe_list(fmts)) - ret = self.__class__( - filter( - lambda arr: ( - arr.psy.plotter and fmts <= set(arr.psy.plotter) - ), - ret, - ) - ) - return ret - - def __contains__(self, val): - try: - name = val if isstring(val) else val.psy.arr_name - except AttributeError: - return False - else: - return name in self.arr_names and ( - isstring(val) or self._contains_array(val) - ) - - def _contains_array(self, val): - """Checks whether exactly this array is in the list""" - arr = self(arr_name=val.psy.arr_name)[0] - is_not_list = any( - map(lambda a: not isinstance(a, InteractiveList), [arr, val]) - ) - is_list = any( - map(lambda a: isinstance(a, InteractiveList), [arr, val]) - ) - # if one is an InteractiveList and the other not, they differ - if is_list and is_not_list: - return False - # if both are interactive lists, check the lists - if is_list: - return all(a in arr for a in val) and all(a in val for a in arr) - # else we check the shapes and values - return arr is val - - def _short_info(self, intend=0, maybe=False): - if maybe: - intend = 0 - str_intend = " " * intend - if len(self) == 1: - return str_intend + "%s%s.%s([%s])" % ( - "" if not hasattr(self, "arr_name") else self.arr_name + ": ", - self.__class__.__module__, - self.__class__.__name__, - self[0].psy._short_info(intend + 4, maybe=True), - ) - return str_intend + "%s%s.%s([\n%s])" % ( - "" if not hasattr(self, "arr_name") else self.arr_name + ": ", - self.__class__.__module__, - self.__class__.__name__, - ",\n".join( - "%s" % (arr.psy._short_info(intend + 4)) for arr in self - ), - ) - - def __str__(self): - return self._short_info() - - def __repr__(self): - return self.__str__() - - def __getitem__(self, key): - """Overwrites lists __getitem__ by returning an ArrayList if `key` is a - slice""" - if isinstance(key, slice): # return a new ArrayList - return self.__class__(super(ArrayList, self).__getitem__(key)) - else: # return the item - return super(ArrayList, self).__getitem__(key) - - if six.PY2: # for compatibility to python 2.7 - - def __getslice__(self, *args): - return self[slice(*args)] - - def next_available_name(self, fmt_str="arr{0}", counter=None): - """Create a new array out of the given format string - - Parameters - ---------- - format_str: str - The base string to use. ``'{0}'`` will be replaced by a counter - counter: iterable - An iterable where the numbers should be drawn from. If None, - ``range(100)`` is used - - Returns - ------- - str - A possible name that is not in the current project""" - names = self.arr_names - counter = counter or iter(range(1000)) - try: - new_name = next( - filter(lambda n: n not in names, map(fmt_str.format, counter)) - ) - except StopIteration: - raise ValueError("{0} already in the list".format(fmt_str)) - return new_name - - @docstrings.dedent - def append(self, value, new_name=False): - """ - Append a new array to the list - - Parameters - ---------- - value: InteractiveBase - The data object to append to this list - %(ArrayList.rename.parameters.new_name)s - - Raises - ------ - %(ArrayList.rename.raises)s - - See Also - -------- - list.append, extend, rename""" - arr, renamed = self.rename(value, new_name) - if renamed is not None: - super(ArrayList, self).append(value) - - @docstrings.dedent - def extend(self, iterable, new_name=False): - """ - Add further arrays from an iterable to this list - - Parameters - ---------- - iterable - Any iterable that contains :class:`InteractiveBase` instances - %(ArrayList.rename.parameters.new_name)s - - Raises - ------ - %(ArrayList.rename.raises)s - - See Also - -------- - list.extend, append, rename""" - # extend those arrays that aren't alredy in the list - super(ArrayList, self).extend( - t[0] - for t in filter( - lambda t: t[1] is not None, - (self.rename(arr, new_name) for arr in iterable), - ) - ) - - def remove(self, arr): - """Removes an array from the list - - Parameters - ---------- - arr: str or :class:`InteractiveBase` - The array name or the data object in this list to remove - - Raises - ------ - ValueError - If no array with the specified array name is in the list""" - name = arr if isinstance(arr, six.string_types) else arr.psy.arr_name - if arr not in self: - raise ValueError("Array {0} not in the list".format(name)) - for i, arr in enumerate(self): - if arr.psy.arr_name == name: - del self[i] - return - raise ValueError("No array found with name {0}".format(name)) - - -@xr.register_dataset_accessor("psy") -class DatasetAccessor(object): - """A dataset accessor to interface with the psyplot package""" - - _filename = None - _data_store = None - _num = None - _plot = None - - #: The concatenation dimension for datasets opened with open_mfdataset - _concat_dim = None - - #: The combine method to open multiple datasets with open_mfdataset - _combine = None - - @property - def num(self): - """A unique number for the dataset""" - if self._num is None: - self._num = next(_ds_counter) - return self._num - - @num.setter - def num(self, value): - self._num = value - - def __init__(self, ds): - self.ds = ds - - @property - def plot(self): - """An object to generate new plots from this dataset - - To make a 2D-plot with the :mod:`psy-simple ` - plugin, you can just type - - .. code-block:: python - - project = ds.psy.plot.plot2d(name='variable-name') - - It will create a new subproject with the extracted and visualized data. - - See Also - -------- - psyplot.project.DatasetPlotter: for the different plot methods - """ - if self._plot is None: - import psyplot.project as psy - - self._plot = psy.DatasetPlotter(self.ds) - return self._plot - - @property - def filename(self): - """The name of the file that stores this dataset""" - fname = self._filename - if fname is None: - fname = get_filename_ds(self.ds, dump=False)[0] - return fname - - @filename.setter - def filename(self, value): - self._filename = value - - @property - def data_store(self): - """The :class:`xarray.backends.common.AbstractStore` used to save the - dataset""" - store_info = self._data_store - if store_info is None or any(s is None for s in store_info): - store = getattr(self.ds, "_file_obj", None) - store_mod = store.__module__ if store is not None else None - store_cls = store.__class__.__name__ if store is not None else None - return store_mod, store_cls - return store_info - - @data_store.setter - def data_store(self, value): - self._data_store = value - - @docstrings.dedent - def create_list(self, *args, **kwargs): - """ - Create a :class:`psyplot.data.ArrayList` with arrays from this dataset - - Parameters - ---------- - %(ArrayList.from_dataset.parameters)s - - Other Parameters - ---------------- - %(ArrayList.from_dataset.other_parameters)s - - Returns - ------- - %(ArrayList.from_dataset.returns)s - - See Also - -------- - psyplot.data.ArrayList.from_dataset""" - return ArrayList.from_dataset(self.ds, *args, **kwargs) - - def to_array(self, *args, **kwargs): - """Same as :meth:`xarray.Dataset.to_array` but sets the base""" - # the docstring is set below - ret = self.ds.to_array(*args, **kwargs) - ret.psy.base = self.ds - return ret - - to_array.__doc__ = xr.Dataset.to_array.__doc__ - - def __getitem__(self, key): - ret = self.ds[key] - if isinstance(ret, xr.DataArray): - ret.psy.base = self.ds - return ret - - def __getattr__(self, attr): - if attr != "ds" and attr in self.ds: - ret = getattr(self.ds, attr) - ret.psy.base = self.ds - return ret - else: - raise AttributeError( - "%s has not Attribute %s" % (self.__class__.__name__, attr) - ) - - def copy(self, deep=False): - """Copy the array - - This method returns a copy of the underlying array in the :attr:`arr` - attribute. It is more stable because it creates a new `psy` accessor""" - ds = self.ds.copy(deep) - ds.psy = DatasetAccessor(ds) - return ds - - -class InteractiveList(ArrayList, InteractiveBase): - """List of :class:`InteractiveArray` instances that can be plotted itself - - This class combines the :class:`ArrayList` and the interactive plotting - through :class:`psyplot.plotter.Plotter` classes. It is mainly used by the - :mod:`psyplot.plotter.simple` module""" - - no_auto_update = property( - _no_auto_update_getter, doc=_no_auto_update_getter.__doc__ - ) - - @no_auto_update.setter - def no_auto_update(self, value): - ArrayList.no_auto_update.fset(self, value) - InteractiveBase.no_auto_update.fset(self, value) - - @property - @docstrings - def _njobs(self): - """%(InteractiveBase._njobs)s""" - ret = super(self.__class__, self)._njobs or [0] - ret[0] += 1 - return ret - - @property - def psy(self): - """Return the list itself""" - return self - - logger = InteractiveBase.logger - - docstrings.delete_params("InteractiveBase.parameters", "auto_update") - - @docstrings.dedent - def __init__(self, *args, **kwargs): - """ - Parameters - ---------- - %(ArrayList.parameters)s - %(InteractiveBase.parameters.no_auto_update)s""" - ibase_kwargs, array_kwargs = utils.sort_kwargs( - kwargs, ["plotter", "arr_name"] - ) - self._registered_updates = {} - InteractiveBase.__init__(self, **ibase_kwargs) - with self.block_signals: - ArrayList.__init__(self, *args, **kwargs) - - @docstrings.dedent - def _register_update( - self, - method="isel", - replot=False, - dims={}, - fmt={}, - force=False, - todefault=False, - ): - """ - Register new dimensions and formatoptions for updating - - Parameters - ---------- - %(InteractiveArray._register_update.parameters)s""" - ArrayList._register_update(self, method=method, dims=dims) - InteractiveBase._register_update( - self, - fmt=fmt, - todefault=todefault, - replot=bool(dims) or replot, - force=force, - ) - - @docstrings.dedent - def start_update(self, draw=None, queues=None): - """ - Conduct the formerly registered updates - - This method conducts the updates that have been registered via the - :meth:`update` method. You can call this method if the - :attr:`auto_update` attribute of this instance is True and the - `auto_update` parameter in the :meth:`update` method has been set to - False - - Parameters - ---------- - %(InteractiveBase.start_update.parameters)s - - Returns - ------- - %(InteractiveBase.start_update.returns)s - - See Also - -------- - :attr:`no_auto_update`, update - """ - if queues is not None: - queues[0].get() - try: - for arr in self: - arr.psy.start_update(draw=False) - self.onupdate.emit() - except Exception: - self._finish_all(queues) - raise - if queues is not None: - queues[0].task_done() - return InteractiveBase.start_update(self, draw=draw, queues=queues) - - def to_dataframe(self): - def to_df(arr): - df = arr.to_pandas() - if hasattr(df, "to_frame"): - df = df.to_frame() - if not keep_names: - return df.rename(columns={df.keys()[0]: arr.psy.arr_name}) - return df - - if len(self) == 1: - return self[0].to_series().to_frame() - else: - keep_names = len(set(arr.name for arr in self)) == self - df = to_df(self[0]) - for arr in self[1:]: - df = df.merge( - to_df(arr), left_index=True, right_index=True, how="outer" - ) - return df - - docstrings.delete_params("ArrayList.from_dataset.parameters", "plotter") - docstrings.delete_kwargs( - "ArrayList.from_dataset.other_parameters", "args", "kwargs" - ) - - @classmethod - @docstrings.dedent - def from_dataset(cls, *args, **kwargs): - """ - Create an InteractiveList instance from the given base dataset - - Parameters - ---------- - %(ArrayList.from_dataset.parameters.no_plotter)s - plotter: psyplot.plotter.Plotter - The plotter instance that is used to visualize the data in this - list - make_plot: bool - If True, the plot is made - - Other Parameters - ---------------- - %(ArrayList.from_dataset.other_parameters.no_args_kwargs)s - ``**kwargs`` - Further keyword arguments may point to any of the dimensions of the - data (see `dims`) - - Returns - ------- - %(ArrayList.from_dataset.returns)s""" - plotter = kwargs.pop("plotter", None) - make_plot = kwargs.pop("make_plot", True) - instance = super(InteractiveList, cls).from_dataset(*args, **kwargs) - if plotter is not None: - plotter.initialize_plot(instance, make_plot=make_plot) - return instance - - def extend(self, *args, **kwargs): - # reimplemented to emit onupdate - super(InteractiveList, self).extend(*args, **kwargs) - self.onupdate.emit() - - def append(self, *args, **kwargs): - # reimplemented to emit onupdate - super(InteractiveList, self).append(*args, **kwargs) - self.onupdate.emit() - - def to_interactive_list(self): - return self - - -class _MissingModule(object): - """Class that can be used if an optional module is not avaible. - - This class raises an error if any attribute is accessed or it is called""" - - def __init__(self, error): - """ - Parameters - ---------- - error: ImportError - The error that has been raised when tried to import the module""" - self.error = error - - def __getattr__(self, attr): - raise self.error - - def __call__(self, *args, **kwargs): - raise self.error - - -def _open_ds_from_store(fname, store_mod=None, store_cls=None, **kwargs): - """Open a dataset and return it""" - if isinstance(fname, xr.Dataset): - return fname - if not isstring(fname): - try: # test iterable - fname[0] - except TypeError: - pass - else: - if store_mod is not None and store_cls is not None: - if isstring(store_mod): - store_mod = repeat(store_mod) - if isstring(store_cls): - store_cls = repeat(store_cls) - fname = [ - _open_store(sm, sc, f) - for sm, sc, f in zip(store_mod, store_cls, fname) - ] - kwargs["engine"] = None - kwargs["lock"] = False - return open_mfdataset(fname, **kwargs) - else: - # try guessing with open_dataset - return open_mfdataset(fname, **kwargs) - if store_mod is not None and store_cls is not None: - fname = _open_store(store_mod, store_cls, fname) - return open_dataset(fname, **kwargs) - - -def decode_absolute_time(times): - def decode(t): - day = np.floor(t).astype(int) - sub = t - day - rest = dt.timedelta(days=sub) - # round microseconds - if rest.microseconds: - rest += dt.timedelta(microseconds=1e6 - rest.microseconds) - return np.datetime64(dt.datetime.strptime("%i" % day, "%Y%m%d") + rest) - - return np.vectorize(decode, [np.datetime64])(times) - - -def encode_absolute_time(times): - def encode(t): - t = to_datetime(t) - return ( - float(t.strftime("%Y%m%d")) - + (t - dt.datetime(t.year, t.month, t.day)).total_seconds() - / 86400.0 - ) - - return np.vectorize(encode, [float])(times) - - -class AbsoluteTimeDecoder(NDArrayMixin): - def __init__(self, array): - self.array = array - example_value = first_n_items(array, 1) or 0 - try: - result = decode_absolute_time(example_value) - except Exception: - logger.error("Could not interprete absolute time values!") - raise - else: - self._dtype = getattr(result, "dtype", np.dtype("object")) - - @property - def dtype(self): - return self._dtype - - def __getitem__(self, key): - return decode_absolute_time(self.array[key]) - - -class AbsoluteTimeEncoder(NDArrayMixin): - def __init__(self, array): - self.array = array - example_value = first_n_items(array, 1) or 0 - try: - result = encode_absolute_time(example_value) - except Exception: - logger.error("Could not interprete absolute time values!") - raise - else: - self._dtype = getattr(result, "dtype", np.dtype("object")) - - @property - def dtype(self): - return self._dtype - - def __getitem__(self, key): - return encode_absolute_time(self.array[key]) diff --git a/psyplot/docstring.py b/psyplot/docstring.py deleted file mode 100755 index d1e6673..0000000 --- a/psyplot/docstring.py +++ /dev/null @@ -1,102 +0,0 @@ -"""Docstring module of the psyplot package - -We use the docrep_ package for managing our docstrings - -.. _docrep: http://docrep.readthedocs.io/en/latest/ -""" - -# SPDX-FileCopyrightText: 2016-2024 University of Lausanne -# SPDX-FileCopyrightText: 2020-2021 Helmholtz-Zentrum Geesthacht - -# SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH -# -# SPDX-License-Identifier: LGPL-3.0-only - -import inspect -import types - -import six -from docrep import DocstringProcessor, safe_modulo # noqa: F401 - - -def dedent(func): - """ - Dedent the docstring of a function and substitute with :attr:`params` - - Parameters - ---------- - func: function - function with the documentation to dedent""" - if isinstance(func, types.MethodType) and not six.PY3: - func = func.im_func - func.__doc__ = func.__doc__ and inspect.cleandoc(func.__doc__) - return func - - -def indent(text, num=4): - """Indet the given string""" - str_indent = " " * num - return str_indent + ("\n" + str_indent).join(text.splitlines()) - - -def append_original_doc(parent, num=0): - """Return an iterator that append the docstring of the given `parent` - function to the applied function""" - - def func(func): - func.__doc__ = func.__doc__ and func.__doc__ + indent( - parent.__doc__, num - ) - return func - - return func - - -_docstrings = DocstringProcessor() - -_docstrings.get_sections(base="DocstringProcessor.get_sections")( - dedent(DocstringProcessor.get_sections) -) - - -class PsyplotDocstringProcessor(DocstringProcessor): - """ - A :class:`docrep.DocstringProcessor` subclass with possible types section - """ - - param_like_sections = DocstringProcessor.param_like_sections + [ - "Possible types" - ] - - @_docstrings.dedent - def get_sections( - self, - s=None, - base=None, - sections=["Parameters", "Other Parameters", "Possible types"], - ): - """ - Extract the specified sections out of the given string - - The same as the :meth:`docrep.DocstringProcessor.get_sections` method - but uses the ``'Possible types'`` section by default, too - - Parameters - ---------- - %(DocstringProcessor.get_sections.parameters)s - - Returns - ------- - str - The replaced string - """ - return super(PsyplotDocstringProcessor, self).get_sections( - s, base, sections - ) - - -del _docstrings - -#: :class:`docrep.PsyplotDocstringProcessor` instance that simplifies the reuse -#: of docstrings from between different python objects. -docstrings = PsyplotDocstringProcessor() diff --git a/psyplot/gdal_store.py b/psyplot/gdal_store.py deleted file mode 100755 index 0c700ab..0000000 --- a/psyplot/gdal_store.py +++ /dev/null @@ -1,167 +0,0 @@ -# -*- coding: utf-8 -*- -"""Gdal Store for reading GeoTIFF files into an :class:`xarray.Dataset` - -This module contains the definition of the :class:`GdalStore` class that can -be used to read in a GeoTIFF file into an :class:`xarray.Dataset`. -It requires that you have the python gdal module installed. - -Examples --------- -to open a GeoTIFF file named ``'my_tiff.tiff'`` you can do:: - - >>> from psyplot.gdal_store import GdalStore - >>> from xarray import open_dataset - >>> ds = open_dataset(GdalStore("my_tiff")) - -Or you use the `engine` of the :func:`psyplot.open_dataset` function: - - >>> ds = open_dataset("my_tiff.tiff", engine="gdal") -""" - -# SPDX-FileCopyrightText: 2016-2024 University of Lausanne -# SPDX-FileCopyrightText: 2020-2021 Helmholtz-Zentrum Geesthacht - -# SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH -# -# SPDX-License-Identifier: LGPL-3.0-only - - -import six -from numpy import arange, dtype, nan -from xarray import Variable -from xarray.backends.common import AbstractDataStore - -import psyplot.data as psyd -from psyplot.warning import warn - -try: - from xarray.core.utils import FrozenOrderedDict -except ImportError: - FrozenOrderedDict = dict -try: - import gdal - from osgeo import gdal_array -except ImportError as e: - gdal = psyd._MissingModule(e) -try: - from dask.array import Array - - with_dask = True -except ImportError: - with_dask = False - - -class GdalStore(AbstractDataStore): - """Datastore to read raster files suitable for the gdal package - - We recommend to use the :func:`psyplot.open_dataset` function to open - a geotiff file:: - - >>> ds = psyplot.open_dataset("my_geotiff.tiff", engine="gdal") - - Notes - ----- - The :class:`GdalStore` object is not as elaborate as, for example, the - `gdal_translate` command. Many attributes, e.g. variable names or netCDF - dimensions will not be interpreted. We only support two - dimensional arrays and each band is saved into one variable named like - ``'Band1', 'Band2', ...``. If you want a more elaborate translation of your - GDAL Raster, convert the file to a netCDF file using ``gdal_translate`` or - the ``gdal.GetDriverByName('netCDF').CreateCopy`` method. However this - class does not create an extra file on your hard disk as it is done by - GDAL.""" - - def __init__(self, filename_or_obj): - """ - Parameters - ---------- - filename_or_obj: str - The path to the GeoTIFF file or a gdal dataset""" - if isinstance(psyd.safe_list(filename_or_obj)[0], six.string_types): - self.ds = gdal.Open(filename_or_obj) - self._filename = filename_or_obj - else: - self.ds = filename_or_obj - fnames = self.ds.GetFileList() - self._filename = fnames[0] if len(fnames) == 1 else fnames - - def get_variables(self): - def load(band): - band = ds.GetRasterBand(band) - a = band.ReadAsArray() - no_data = band.GetNoDataValue() - if no_data is not None: - try: - a[a == no_data] = a.dtype.type(nan) - except ValueError: - pass - return a - - ds = self.ds - dims = ["lat", "lon"] - chunks = ((ds.RasterYSize,), (ds.RasterXSize,)) - shape = (ds.RasterYSize, ds.RasterXSize) - variables = dict() - for iband in range(1, ds.RasterCount + 1): - band = ds.GetRasterBand(iband) - dt = dtype(gdal_array.codes[band.DataType]) - if with_dask: - dsk = {("x", 0, 0): (load, iband)} - arr = Array(dsk, "x", chunks, shape=shape, dtype=dt) - else: - arr = load(iband) - attrs = band.GetMetadata_Dict() - try: - dt.type(nan) - attrs["_FillValue"] = nan - except ValueError: - no_data = band.GetNoDataValue() - attrs.update({"_FillValue": no_data} if no_data else {}) - variables["Band%i" % iband] = Variable(dims, arr, attrs) - variables["lat"], variables["lon"] = self._load_GeoTransform() - return FrozenOrderedDict(variables) - - def _load_GeoTransform(self): - """Calculate latitude and longitude variable calculated from the - gdal.Open.GetGeoTransform method""" - - def load_lon(): - return arange(ds.RasterXSize) * b[1] + b[0] - - def load_lat(): - return arange(ds.RasterYSize) * b[5] + b[3] - - ds = self.ds - b = self.ds.GetGeoTransform() # bbox, interval - if with_dask: - lat = Array( - {("lat", 0): (load_lat,)}, - "lat", - (self.ds.RasterYSize,), - shape=(self.ds.RasterYSize,), - dtype=float, - ) - lon = Array( - {("lon", 0): (load_lon,)}, - "lon", - (self.ds.RasterXSize,), - shape=(self.ds.RasterXSize,), - dtype=float, - ) - else: - lat = load_lat() - lon = load_lon() - return Variable(("lat",), lat), Variable(("lon",), lon) - - def get_attrs(self): - from osr import SpatialReference - - attrs = self.ds.GetMetadata() - try: - sp = SpatialReference(wkt=self.ds.GetProjection()) - proj4 = sp.ExportToProj4() - except Exception: - warn("Could not identify projection") - else: - attrs["proj4"] = proj4 - return FrozenOrderedDict(attrs) diff --git a/psyplot/plotter.py b/psyplot/plotter.py deleted file mode 100755 index ddd50b6..0000000 --- a/psyplot/plotter.py +++ /dev/null @@ -1,2746 +0,0 @@ -"""Core package for interactive visualization in the psyplot package - -This package defines the :class:`Plotter` and :class:`Formatoption` classes, -the core of the visualization in the :mod:`psyplot` package. Each -:class:`Plotter` combines a set of formatoption keys where each formatoption -key is represented by a :class:`Formatoption` subclass.""" - -# SPDX-FileCopyrightText: 2016-2024 University of Lausanne -# SPDX-FileCopyrightText: 2020-2021 Helmholtz-Zentrum Geesthacht - -# SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH -# -# SPDX-License-Identifier: LGPL-3.0-only - -import logging -import weakref -from abc import ABCMeta, abstractmethod -from collections import defaultdict -from datetime import datetime, timedelta -from itertools import chain, groupby, repeat, starmap, tee -from textwrap import TextWrapper -from threading import RLock - -import six -from numpy import datetime64, inf, ndarray, timedelta64 -from xarray.core.formatting import format_timedelta, format_timestamp - -from psyplot import rcParams -from psyplot.config.rcsetup import SubDict -from psyplot.data import CFDecoder, InteractiveList, _no_auto_update_getter -from psyplot.docstring import dedent, docstrings -from psyplot.utils import ( - Defaultdict, - _temp_bool_prop, - _TempBool, - check_key, - unique_everseen, -) -from psyplot.warning import PsyPlotRuntimeWarning, warn - -#: the default function to use when printing formatoption infos (the default is -#: use print or in the gui, use the help explorer) -default_print_func = six.print_ - - -#: :class:`dict`. Mapping from group to group names -groups = { - "data": "Data manipulation formatoptions", - "axes": "Axes formatoptions", - "labels": "Label formatoptions", - "plotting": "Plot formatoptions", - "post_processing": "Post processing formatoptions", - "colors": "Color coding formatoptions", - "misc": "Miscallaneous formatoptions", - "ticks": "Axis tick formatoptions", - "vector": "Vector plot formatoptions", - "masking": "Masking formatoptions", - "regression": "Fitting formatoptions", -} - - -def _identity(*args): - """identity function to make no validation - - Returns - ------- - object - just return the last argument in ``*args``""" - return args[-1] - - -def format_time(x): - """Formats date values - - This function formats :class:`datetime.datetime` and - :class:`datetime.timedelta` objects (and the corresponding numpy objects) - using the :func:`xarray.core.formatting.format_timestamp` and the - :func:`xarray.core.formatting.format_timedelta` functions. - - Parameters - ---------- - x: object - The value to format. If not a time object, the value is returned - - Returns - ------- - str or `x` - Either the formatted time object or the initial `x`""" - if isinstance(x, (datetime64, datetime)): - return format_timestamp(x) - elif isinstance(x, (timedelta64, timedelta)): - return format_timedelta(x) - elif isinstance(x, ndarray): - return list(x) if x.ndim else x[()] - return x - - -def is_data_dependent(fmto, data): - """Check whether a formatoption is data dependent - - Parameters - ---------- - fmto: Formatoption - The :class:`Formatoption` instance to check - data: xarray.DataArray - The data array to use if the :attr:`~Formatoption.data_dependent` - attribute is a callable - - Returns - ------- - bool - True, if the formatoption depends on the data""" - if callable(fmto.data_dependent): - return fmto.data_dependent(data) - return fmto.data_dependent - - -def _child_property(childname): - def get_x(self): - return getattr(self.plotter, self._child_mapping[childname]) - - return property( - get_x, doc=childname + " Formatoption instance in the plotter" - ) - - -class FormatoptionMeta(ABCMeta): - """Meta class for formatoptions - - This class serves as a meta class for formatoptions and allows a more - efficient docstring generation by using the - :attr:`psyplot.docstring.docstrings` when creating a new formatoption - class""" - - def __new__(cls, clsname, bases, dct): - """Assign an automatic documentation to the formatoption""" - doc = dct.get("__doc__") - if doc is not None: - dct["__doc__"] = docstrings.dedent(doc) - new_cls = super(FormatoptionMeta, cls).__new__( - cls, clsname, bases, dct - ) - for childname in chain( - new_cls.children, - new_cls.dependencies, - new_cls.connections, - new_cls.parents, - ): - setattr(new_cls, childname, _child_property(childname)) - if new_cls.plot_fmt: - new_cls.data_dependent = True - return new_cls - - -# priority values - -#: Priority value of formatoptions that are updated before the data is loaded. -START = 30 -#: Priority value of formatoptions that are updated before the plot it made. -BEFOREPLOTTING = 20 -#: Priority value of formatoptions that are updated at the end. -END = 10 - - -@six.add_metaclass(FormatoptionMeta) -class Formatoption(object): - """Abstract formatoption - - This class serves as an abstract version of an formatoption descriptor - that can be used by :class:`~psyplot.plotter.Plotter` instances.""" - - priority = END - """:class:`int`. Priority value of the the formatoption determining when - the formatoption is updated. - - - 10: at the end (for labels, etc.) - - 20: before the plotting (e.g. for colormaps, etc.) - - 30: before loading the data (e.g. for lonlatbox)""" - - #: :class:`str`. Formatoption key of this class in the - #: :class:`~psyplot.plotter.Plotter` class - key = None - - _plotter = None - - @property - def plotter(self): - """:class:`~psyplot.plotter.Plotter`. Plotter instance this - formatoption belongs to""" - if self._plotter is None: - return - return self._plotter() - - @plotter.setter - def plotter(self, value): - if value is not None: - self._plotter = weakref.ref(value) - else: - self._plotter = value - - #: `list of str`. List of formatoptions that have to be updated before this - #: one is updated. Those formatoptions are only updated if they exist in - #: the update parameters. - children = [] - - #: `list of str`. List of formatoptions that force an update of this - #: formatoption if they are updated. - dependencies = [] - - #: `list of str`. Connections to other formatoptions that are (different - #: from :attr:`dependencies` and :attr:`children`) not important for the - #: update process - connections = [] - - #: `list of str`. List of formatoptions that, if included in the update, - #: prevent the update of this formatoption. - parents = [] - - #: :class:`bool`. Has to be True if the formatoption has a ``make_plot`` - #: method to make the plot. - plot_fmt = False - - #: :class:`bool`. True if an update of this formatoption requires a - #: clearing of the axes and reinitialization of the plot - requires_clearing = False - - #: :class:`str`. Key of the group name in :data:`groups` of this - #: formatoption keyword - group = "misc" - - #: :class:`bool` or a callable. This attribute indicates whether this - #: :class:`Formatoption` depends on the data and should be updated if the - #: data changes. If it is a callable, it must accept one argument: the - #: new data. (Note: This is automatically set to True for plot - #: formatoptions) - data_dependent = False - - #: :class:`bool`. True if this formatoption needs an update after the plot - #: has changed - update_after_plot = False - - #: :class:`set` of the :class:`Formatoption` instance that are shared - #: with this instance. - shared = set() - - #: int or None. Index that is used in case the plotting data is a - #: :class:`psyplot.InteractiveList` - index_in_list = 0 - - #: :class:`str`. A bit more verbose name than the formatoption key to be - #: included in the gui. If None, the key is used in the gui - name = None - - #: Boolean that is True if an update of the formatoption requires a replot - requires_replot = False - - @property - def init_kwargs(self): - """:class:`dict` key word arguments that are passed to the - initialization of a new instance when accessed from the descriptor""" - return self._child_mapping - - @property - def project(self): - """Project of the plotter of this instance""" - return self.plotter.project - - @property - def ax(self): - """The axes this Formatoption plots on""" - return self.plotter.ax - - @property - def lock(self): - """A :class:`threading.Rlock` instance to lock while updating - - This lock is used when multiple :class:`plotter` instances are - updated at the same time while sharing formatoptions.""" - try: - return self._lock - except AttributeError: - self._lock = RLock() - return self._lock - - @property - def logger(self): - """Logger of the plotter""" - return self.plotter.logger.getChild(self.key) - - @property - def groupname(self): - """Long name of the group this formatoption belongs too.""" - try: - return groups[self.group] - except KeyError: - warn( - "Unknown formatoption group " + str(self.group), - PsyPlotRuntimeWarning, - ) - return self.group - - @property - def raw_data(self): - """The original data of the plotter of this formatoption""" - if self.index_in_list is not None and isinstance( - self.plotter.data, InteractiveList - ): - return self.plotter.data[self.index_in_list] - else: - return self.plotter.data - - @property - def decoder(self): - """The :class:`~psyplot.data.CFDecoder` instance that decodes the - :attr:`raw_data`""" - # If the decoder is modified by one of the formatoptions, use this one - if self.plotter.plot_data_decoder is not None: - if self.index_in_list is not None and isinstance( - self.plotter.plot_data, InteractiveList - ): - ret = self.plotter.plot_data_decoder[self.index_in_list] - if ret is not None: - return ret - else: - return self.plotter.plot_data_decoder - data = self.raw_data - check = isinstance(data, InteractiveList) - while check: - data = data[0] - check = isinstance(data, InteractiveList) - return data.psy.decoder - - @decoder.setter - def decoder(self, value): - self.set_decoder(value, self.index_in_list) - - @property - def any_decoder(self): - """Return the first possible decoder""" - ret = self.decoder - while not isinstance(ret, CFDecoder): - ret = ret[0] - return ret - - @property - def data(self): - """The data that is plotted""" - if self.index_in_list is not None and isinstance( - self.plotter.plot_data, InteractiveList - ): - return self.plotter.plot_data[self.index_in_list] - else: - return self.plotter.plot_data - - @data.setter - def data(self, value): - self.set_data(value, self.index_in_list) - - @property - def iter_data(self): - """Returns an iterator over the plot data arrays""" - data = self.data - if isinstance(data, InteractiveList): - return iter(data) - return iter([data]) - - @property - def iter_raw_data(self): - """Returns an iterator over the original data arrays""" - data = self.raw_data - if isinstance(data, InteractiveList): - return iter(data) - return iter([data]) - - @property - def validate(self): - """Validation method of the formatoption""" - try: - return self._validate - except AttributeError: - try: - self._validate = self.plotter.get_vfunc(self.key) - except KeyError: - warn( - "Could not find a validation function for %s " - "formatoption keyword! No validation will be made!" - % (self.key), - PsyPlotRuntimeWarning, - logger=self.logger, - ) - self._validate = _identity - return self._validate - - @validate.setter - def validate(self, value): - self._validate = value - - @property - def default(self): - """Default value of this formatoption""" - return self.plotter.rc[self.key] - - @property - def default_key(self): - """The key of this formatoption in the :attr:`psyplot.rcParams`""" - return self.plotter.rc._get_val_and_base(self.key)[0] - - @property - def shared_by(self): - """None if the formatoption is not controlled by another formatoption - of another plotter, otherwise the corresponding :class:`Formatoption` - instance""" - return self.plotter._shared.get(self.key) - - @property - def value(self): - """Value of the formatoption in the corresponding :attr:`plotter` or - the shared value""" - shared_by = self.shared_by - if shared_by: - return shared_by.value2share - return self.plotter[self.key] - - @property - @dedent - def changed(self): - """ - :class:`bool` indicating whether the value changed compared to the - default or not.""" - return self.diff(self.default) - - @property - @dedent - def value2share(self): - """ - The value that is passed to shared formatoptions (by default, the - :attr:`value` attribute)""" - return self.value - - @property - @dedent - def value2pickle(self): - """ - The value that can be used when pickling the information of the project - """ - return self.value - - @docstrings.get_sections(base="Formatoption") - @dedent - def __init__( - self, - key, - plotter=None, - index_in_list=None, - additional_children=[], - additional_dependencies=[], - **kwargs, - ): - """ - Parameters - ---------- - key: str - formatoption key in the `plotter` - plotter: psyplot.plotter.Plotter - Plotter instance that holds this formatoption. If None, it is - assumed that this instance serves as a descriptor. - index_in_list: int or None - The index that shall be used if the data is a - :class:`psyplot.InteractiveList` - additional_children: list or str - Additional children to use (see the :attr:`children` attribute) - additional_dependencies: list or str - Additional dependencies to use (see the :attr:`dependencies` - attribute) - ``**kwargs`` - Further keywords may be used to specify different names for - children, dependencies and connection formatoptions that match the - setup of the plotter. Hence, keywords may be anything of the - :attr:`children`, :attr:`dependencies` and :attr:`connections` - attributes, with values being the name of the new formatoption in - this plotter.""" - self.key = key - self.plotter = plotter - self.index_in_list = index_in_list - self.shared = set() - self.additional_children = additional_children - self.additional_dependencies = additional_dependencies - self.children = self.children + additional_children - self.dependencies = self.dependencies + additional_dependencies - self._child_mapping = dict( - zip( - *tee( - chain( - self.children, - self.dependencies, - self.connections, - self.parents, - ), - 2, - ) - ) - ) - # check kwargs - for key in (key for key in kwargs if key not in self._child_mapping): - raise TypeError( - "%s.__init__() got an unexpected keyword argument %r" - % (self.__class__.__name__, key) - ) - # set up child mapping - self._child_mapping.update(kwargs) - # reset the dependency lists to match the current plotter setup - for attr in ["children", "dependencies", "connections", "parents"]: - setattr( - self, - attr, - [self._child_mapping[key] for key in getattr(self, attr)], - ) - - def __set__(self, instance, value): - if isinstance(value, Formatoption): - setattr(instance, "_" + self.key, value) - else: - fmto = getattr(instance, self.key) - fmto.set_value(value) - - def __get__(self, instance, owner): - if instance is None: - return self - try: - return getattr(instance, "_" + self.key) - except AttributeError: - fmto = self.__class__( - self.key, - instance, - self.index_in_list, - additional_children=self.additional_children, - additional_dependencies=self.additional_dependencies, - **self.init_kwargs, - ) - setattr(instance, "_" + self.key, fmto) - return fmto - - def __delete__(self, instance, owner): - fmto = getattr(instance, "_" + self.key) - with instance.no_validation: - instance[self.key] = fmto.default - - @docstrings.get_sections(base="Formatoption.set_value") - @dedent - def set_value(self, value, validate=True, todefault=False): - """ - Set (and validate) the value in the plotter. This method is called by - the plotter when it attempts to change the value of the formatoption. - - Parameters - ---------- - value - Value to set - validate: bool - if True, validate the `value` before it is set - todefault: bool - True if the value is updated to the default value""" - # do nothing if the key is shared - if self.key in self.plotter._shared: - return - with self.plotter.no_validation: - self.plotter[self.key] = ( - value if not validate else self.validate(value) - ) - - def set_data(self, data, i=None): - """ - Replace the data to plot - - This method may be used to replace the data that is visualized by the - plotter. It changes it's behaviour depending on whether an - :class:`psyplot.data.InteractiveList` is visualized or a single - :class:`pysplot.data.InteractiveArray` - - Parameters - ---------- - data: psyplot.data.InteractiveBase - The data to insert - i: int - The position in the InteractiveList where to insert the data (if - the plotter visualizes a list anyway) - - Notes - ----- - This method uses the :attr:`Formatoption.data` attribute - """ - if self.index_in_list is not None: - i = self.index_in_list - if i is not None and isinstance( - self.plotter.plot_data, InteractiveList - ): - self.plotter.plot_data[i] = data - else: - self.plotter.plot_data = data - - def set_decoder(self, decoder, i=None): - """ - Replace the data to plot - - This method may be used to replace the data that is visualized by the - plotter. It changes it's behaviour depending on whether an - :class:`psyplot.data.InteractiveList` is visualized or a single - :class:`pysplot.data.InteractiveArray` - - Parameters - ---------- - decoder: psyplot.data.CFDecoder - The decoder to insert - i: int - The position in the InteractiveList where to insert the data (if - the plotter visualizes a list anyway) - """ - # we do not modify the raw data but instead set it on the plotter - # TODO: This is not safe for encapsulated InteractiveList instances! - if i is not None and isinstance( - self.plotter.plot_data, InteractiveList - ): - n = len(self.plotter.plot_data) - decoders = self.plotter.plot_data_decoder or [None] * n - decoders[i] = decoder - self.plotter.plot_data_decoder = decoders - else: - if isinstance( - self.plotter.plot_data, InteractiveList - ) and isinstance(decoder, CFDecoder): - decoder = [decoder] * len(self.plotter.plot_data) - self.plotter.plot_data_decoder = decoder - - def get_decoder(self, i=None): - # we do not modify the raw data but instead set it on the plotter - # TODO: This is not safe for encapsulated InteractiveList instances! - if i is not None and isinstance( - self.plotter.plot_data, InteractiveList - ): - n = len(self.plotter.plot_data) - decoders = self.plotter.plot_data_decoder or [None] * n - return decoders[i] or self.plotter.plot_data[i].psy.decoder - else: - return self.decoder - - def check_and_set(self, value, todefault=False, validate=True): - """Checks the value and sets the value if it changed - - This method checks the value and sets it only if the :meth:`diff` - method result of the given `value` is True - - Parameters - ---------- - value - A possible value to set - todefault: bool - True if the value is updated to the default value - - Returns - ------- - bool - A boolean to indicate whether it has been set or not""" - if validate: - value = self.validate(value) - if self.diff(value): - self.set_value(value, validate=False, todefault=todefault) - return True - return False - - def diff(self, value): - """Checks whether the given value differs from what is currently set - - Parameters - ---------- - value - A possible value to set (make sure that it has been validate via - the :attr:`validate` attribute before) - - Returns - ------- - bool - True if the value differs from what is currently set""" - return value != self.value - - def initialize_plot(self, value, *args, **kwargs): - """Method that is called when the plot is made the first time - - Parameters - ---------- - value - The value to use for the initialization""" - self.update(value, *args, **kwargs) - - @abstractmethod - def update(self, value): - """Method that is call to update the formatoption on the axes - - Parameters - ---------- - value - Value to update""" - pass - - def get_fmt_widget(self, parent, project): - """Get a widget to update the formatoption in the GUI - - This method should return a QWidget that is loaded by the psyplot-gui - when the formatoption is selected in the - :attr:`psyplot_gui.main.Mainwindow.fmt_widget`. It should call the - :meth:`~psyplot_gui.fmt_widget.FormatoptionWidget.insert_text` method - when the update text for the formatoption should be changed. - - Parameters - ---------- - parent: psyplot_gui.fmt_widget.FormatoptionWidget - The parent widget that contains the returned QWidget - project: psyplot.project.Project - The current subproject (see :func:`psyplot.project.gcp`) - - Returns - ------- - PyQt5.QtWidgets.QWidget - The widget to control the formatoption""" - return None - - def share(self, fmto, initializing=False, **kwargs): - """Share the settings of this formatoption with other data objects - - Parameters - ---------- - fmto: Formatoption - The :class:`Formatoption` instance to share the attributes with - ``**kwargs`` - Any other keyword argument that shall be passed to the update - method of `fmto`""" - # lock all the childrens and the formatoption itself - self.lock.acquire() - fmto._lock_children() - fmto.lock.acquire() - # update the other plotter - if initializing: - fmto.initialize_plot(self.value2share, **kwargs) - else: - fmto.update(self.value2share, **kwargs) - self.shared.add(fmto) - # release the locks - fmto.lock.release() - fmto._release_children() - self.lock.release() - - def _lock_children(self): - """acquire the locks of the children""" - plotter = self.plotter - for key in self.children + self.dependencies: - try: - getattr(plotter, key).lock.acquire() - except AttributeError: - pass - - def _release_children(self): - """release the locks of the children""" - plotter = self.plotter - for key in self.children + self.dependencies: - try: - getattr(plotter, key).lock.release() - except AttributeError: - pass - - def finish_update(self): - """Finish the update, initialization and sharing process - - This function is called at the end of the :meth:`Plotter.start_update`, - :meth:`Plotter.initialize_plot` or the :meth:`Plotter.share` methods. - """ - pass - - @dedent - def remove(self): - """ - Method to remove the effects of this formatoption - - This method is called when the axes is cleared due to a - formatoption with :attr:`requires_clearing` set to True. You don't - necessarily have to implement this formatoption if your plot results - are removed by the usual :meth:`matplotlib.axes.Axes.clear` method.""" - pass - - @docstrings.get_extended_summary(base="Formatoption.convert_coordinate") - @docstrings.get_sections( - base="Formatoption.convert_coordinate", - sections=["Parameters", "Returns"], - ) - def convert_coordinate(self, coord, *variables): - """Convert a coordinate to units necessary for the plot. - - This method takes a single coordinate variable (e.g. the `bounds` of a - coordinate, or the coordinate itself) and transforms the units that the - plotter requires. - - One might also provide additional `variables` that are supposed to be - on the same unit, in case the given `coord` does not specify a `units` - attribute. `coord` might be a CF-conform `bounds` variable, and one of - the variables might be the corresponding `coordinate`. - - Parameters - ---------- - coord: xr.Variable - The variable to transform - ``*variables`` - The variables that are on the same unit as `coord` - - Returns - ------- - xr.Variable - The transformed `coord` - - Notes - ----- - By default, this method uses the :meth:`~Plotter.convert_coordinate` - method of the :attr:`plotter`. - """ - return self.plotter.convert_coordinate(coord, *variables) - - -class DictFormatoption(Formatoption): - """ - Base formatoption class defining an alternative set_value that works for - dictionaries.""" - - @docstrings.dedent - def set_value(self, value, validate=True, todefault=False): - """ - Set (and validate) the value in the plotter - - Parameters - ---------- - %(Formatoption.set_value.parameters)s - - Notes - ----- - - If the current value in the plotter is None, then it will be set with - the given `value`, otherwise the current value in the plotter is - updated - - If the value is an empty dictionary, the value in the plotter is - cleared""" - value = value if not validate else self.validate(value) - # if the key in the plotter is not already set (i.e. it is initialized - # with None, we set it) - if self.plotter[self.key] is None: - with self.plotter.no_validation: - self.plotter[self.key] = value.copy() - # in case of an empty dict, clear the value - elif not value: - self.plotter[self.key].clear() - # otherwhise we update the dictionary - else: - if todefault: - self.plotter[self.key].clear() - self.plotter[self.key].update(value) - - -class PostTiming(Formatoption): - """ - Determine when to run the :attr:`post` formatoption - - This formatoption determines, whether the :attr:`post` formatoption - should be run never, after replot or after every update. - - Possible types - -------------- - 'never' - Never run post processing scripts - 'always' - Always run post processing scripts - 'replot' - Only run post processing scripts when the data changes or a replot - is necessary - - See Also - -------- - post: The post processing formatoption""" - - default = "never" - - priority = -inf - - group = "post_processing" - - name = "Timing of the post processing" - - @staticmethod - def validate(value): - value = six.text_type(value) - possible_values = ["never", "always", "replot"] - if value not in possible_values: - raise ValueError( - "String must be one of %s, not %r" % (possible_values, value) - ) - return value - - def update(self, value): - pass - - def get_fmt_widget(self, parent, project): - from psyplot_gui.compat.qtcompat import QComboBox - - combo = QComboBox(parent) - combo.addItems(["never", "always", "replot"]) - combo.setCurrentText( - next((plotter[self.key] for plotter in project.plotters), "never") - ) - combo.currentTextChanged.connect(parent.set_obj) - return combo - - -class PostProcDependencies(object): - """The dependencies of this formatoption""" - - def __get__(self, instance, owner): - if ( - instance is None - or instance.plotter is None - or not instance.plotter._initialized - ): - return [] - elif instance.post_timing.value == "always": - return list(set(instance.plotter) - {instance.key}) - else: - return [] - - def __set__(self, instance, value): - pass - - -class PostProcessing(Formatoption): - """ - Apply your own postprocessing script - - This formatoption let's you apply your own post processing script. Just - enter the script as a string and it will be executed. The formatoption - will be made available via the ``self`` variable - - Possible types - -------------- - None - Don't do anything - str - The post processing script as string - - Note - ---- - This formatoption uses the built-in :func:`exec` function to compile the - script. Since this poses a security risk when loading psyplot projects, - it is by default disabled through the :attr:`Plotter.enable_post` - attribute. If you are sure that you can trust the script in this - formatoption, set this attribute of the corresponding :class:`Plotter` to - ``True`` - - Examples - -------- - Assume, you want to manually add the mean of the data to the title of the - matplotlib axes. You can simply do this via - - .. code-block:: python - - from psyplot.plotter import Plotter - from xarray import DataArray - - plotter = Plotter(DataArray([1, 2, 3])) - # enable the post formatoption - plotter.enable_post = True - plotter.update(post="self.ax.set_title(str(self.data.mean()))") - plotter.ax.get_title() - "2.0" - - By default, the ``post`` formatoption is only ran, when it is explicitly - updated. However, you can use the :attr:`post_timing` formatoption, to - run it automatically. E.g. for running it after every update of the - plotter, you can set - - .. code-block:: python - - plotter.update(post_timing="always") - - See Also - -------- - post_timing: Determine the timing of this formatoption""" - - children = ["post_timing"] - - default = None - - priority = -inf - - group = "post_processing" - - name = "Custom post processing script" - - @staticmethod - def validate(value): - if value is None: - return value - elif not isinstance(value, six.string_types): - raise ValueError("Expected a string, not %s" % (type(value),)) - else: - return six.text_type(value) - - @property - def data_dependent(self): - """True if the corresponding :class:`post_timing ` - formatoption is set to ``'replot'`` to run the post processing script - after every change of the data""" - return self.post_timing.value == "replot" - - dependencies = PostProcDependencies() - - def update(self, value): - if value is None: - return - if not self.plotter.enable_post: - warn( - "Post processing is disabled. Set the ``enable_post`` " - "attribute to True to run the script" - ) - else: - exec(value, {"self": self}) - - -class Plotter(dict): - """Interactive plotting object for one or more data arrays - - This class is the base for the interactive plotting with the psyplot - module. It capabilities are determined by it's descriptor classes that are - derived from the :class:`Formatoption` class""" - - #: List of base strings in the :attr:`psyplot.rcParams` dictionary - _rcparams_string = [] - - post_timing = PostTiming("post_timing") - post = PostProcessing("post") - - no_validation = _temp_bool_prop( - "no_validation", - """ - Temporarily disable the validation - - Examples - -------- - Although it is not recommended to set a value with disabled validation, - you can disable it via:: - - >>> with plotter.no_validation: - ... plotter["ticksize"] = "x" - ... - - To permanently disable the validation, simply set - - >>> plotter.no_validation = True - >>> plotter["ticksize"] = "x" - >>> plotter.no_validation = False # reenable validation""", - ) - - #: Temporarily include links in the key descriptions from - #: :meth:`show_keys`, :meth:`show_docs` and :meth:`show_summaries`. - #: Note that this is a class attribute, so each change to the value of this - #: attribute will affect all instances and subclasses - include_links = _TempBool() - - @property - def ax(self): - """Axes instance of the plot""" - if self._ax is None: - import matplotlib.pyplot as plt - - plt.figure() - self._ax = plt.axes(projection=self._get_sample_projection()) - return self._ax - - @ax.setter - def ax(self, value): - self._ax = value - - #: The :class:`psyplot.project.Project` instance this plotter belongs to - _project = None - - @property - def project(self): - """:class:`psyplot.project.Project` instance this plotter belongs to""" - if self._project is None: - return - return self._project() - - @project.setter - def project(self, value): - if value is not None: - self._project = weakref.ref(value) - else: - self._project = value - - @property - @dedent - def rc(self): - """ - Default values for this plotter - - This :class:`~psyplot.config.rcsetup.SubDict` stores the default values - for this plotter. A modification of the dictionary does not affect - other plotter instances unless you set the - :attr:`~psyplot.config.rcsetup.SubDict.trace` attribute to True""" - try: - return self._rc - except AttributeError: - self._set_rc() - return self._rc - - @property - def base_variables(self): - """A mapping from the base_variable names to the variables""" - if isinstance(self.data, InteractiveList): - return dict( - chain( - *map( - lambda arr: six.iteritems(arr.psy.base_variables), - self.data, - ) - ) - ) - else: - return self.data.psy.base_variables - - @property - def iter_base_variables(self): - """A mapping from the base_variable names to the variables""" - if isinstance(self.data, InteractiveList): - return chain(*(arr.psy.iter_base_variables for arr in self.data)) - else: - return self.data.psy.iter_base_variables - - no_auto_update = property( - _no_auto_update_getter, doc=_no_auto_update_getter.__doc__ - ) - - @no_auto_update.setter - def no_auto_update(self, value): - self.no_auto_update.value = bool(value) - - @property - def changed(self): - """:class:`dict` containing the key value pairs that are not the - default""" - return { - key: value - for key, value in six.iteritems(self) - if getattr(self, key).changed - } - - @property - def figs2draw(self): - """All figures that have been manipulated through sharing and the own - figure. - - Notes - ----- - Using this property set will reset the figures too draw""" - return self._figs2draw.union([self.ax.get_figure()]) - - @property - @docstrings - def _njobs(self): - """%(InteractiveBase._njobs)s""" - if self.disabled: - return [0] - return [1, 1] - - @property - def _fmtos(self): - """Iterator over the formatoptions""" - return (getattr(self, key) for key in self) - - @property - def _fmto_groups(self): - """Mapping from group to a set of formatoptions""" - ret = defaultdict(set) - for key in self: - ret[getattr(self, key).group].add(getattr(self, key)) - return dict(ret) - - @property - def fmt_groups(self): - """A mapping from the formatoption group to the formatoptions""" - ret = defaultdict(set) - for key in self: - ret[getattr(self, key).group].add(key) - return dict(ret) - - @property - def groups(self): - """A mapping from the group short name to the group description""" - return {group: groups[group] for group in self.fmt_groups} - - @property - def data(self): - """The :class:`psyplot.InteractiveBase` instance of this plotter""" - return self._data - - @data.setter - def data(self, value): - self._data = value - - @property - def plot_data(self): - """The data that is used for plotting""" - return getattr(self, "_plot_data", self.data) - - @plot_data.setter - def plot_data(self, value): - self._set_data(value) - - #: The decoder to use for the formatoptions. If None, the decoder of the - #: raw data is used - plot_data_decoder = None - - #: :class:`bool` that has to be ``True`` if the post processing script in - #: the :attr:`post` formatoption should be enabled - enable_post = False - - def _set_data(self, value): - if isinstance(value, InteractiveList): - self._plot_data = value.copy() - else: - self._plot_data = value - - @property - def logger(self): - """:class:`logging.Logger` of this plotter""" - try: - return self.data.psy.logger.getChild(self.__class__.__name__) - except AttributeError: - name = "%s.%s" % (self.__module__, self.__class__.__name__) - return logging.getLogger(name) - - docstrings.keep_params("InteractiveBase.parameters", "auto_update") - - @docstrings.get_sections(base="Plotter") - @docstrings.dedent - def __init__( - self, - data=None, - ax=None, - auto_update=None, - project=None, - draw=False, - make_plot=True, - clear=False, - enable_post=False, - **kwargs, - ): - """ - Parameters - ---------- - data: InteractiveArray or ArrayList, optional - Data object that shall be visualized. If given and `plot` is True, - the :meth:`initialize_plot` method is called at the end. Otherwise - you can call this method later by yourself - ax: matplotlib.axes.Axes - Matplotlib Axes to plot on. If None, a new one will be created as - soon as the :meth:`initialize_plot` method is called - %(InteractiveBase.parameters.auto_update)s - %(InteractiveBase.start_update.parameters.draw)s - make_plot: bool - If True, and `data` is not None, the plot is initialized. Otherwise - only the framework between plotter and data is set up - clear: bool - If True, the axes is cleared first - enable_post: bool - If True, the :attr:`post` formatoption is enabled and post - processing scripts are allowed - ``**kwargs`` - Any formatoption key from the :attr:`formatoptions` attribute that - shall be used""" - self.project = project - self.ax = ax - self.data = data - self.enable_post = enable_post - if auto_update is None: - auto_update = rcParams["lists.auto_update"] - self.no_auto_update = not bool(auto_update) - self._registered_updates = {} - self._todefault = False - self._old_fmt = [] - self._figs2draw = set() - #: formatoptions that have to be updated by other plotters that share - #: the given formatoption with this Plotter. :attr:`_to_update` is a - #: mapping from the formatoptions in this plotter to the corresponding - #: other plotter - self._to_update = {} - self.disabled = False - #: Dictionary holding the Formatoption instances of other plotters - #: if their value shall be used instead of the one in this instance - self._shared = {} - #: list of str. Formatoption keys that were changed during the last - #: update - self._last_update = [] - #: The set of formatoptions that shall be updated even if they did not - #: change - self._force = set() - self.replot = True - self.cleared = clear - self._updating = False - # will be set to True when the plot is first initialized - self._initialized = False - - # first we initialize all keys with None. This is necessary in order - # to make the validation functioning - with self.no_validation: - for key in self._get_formatoptions(): - self[key] = None - for key in self: # then we set the default values - fmto = getattr(self, key) - self._try2set(fmto, fmto.default, validate=False) - self._set_rc() - for key, value in six.iteritems(kwargs): # then the user values - self[key] = value - self.initialize_plot( - data, ax=ax, draw=draw, clear=clear, make_plot=make_plot - ) - - def _try2set(self, fmto, *args, **kwargs): - """Sets the value in `fmto` and gives additional informations when fail - - Parameters - ---------- - fmto: Formatoption - ``*args`` and ``**kwargs`` - Anything that is passed to `fmto`s :meth:`~Formatoption.set_value` - method""" - fmto.set_value(*args, **kwargs) - - def __getitem__(self, key): - try: - return dict.__getitem__(self, key) - except KeyError: - self.check_key(key) - - def __setitem__(self, key, value): - if not self.no_validation: - self.check_key(key) - self._try2set(getattr(self, key), value) - return - # prevent from setting during an update process - getattr(self, key).lock.acquire() - dict.__setitem__(self, key, value) - getattr(self, key).lock.release() - - def __delitem__(self, key): - self[key] = getattr(self, key).default - - docstrings.delete_params("check_key.parameters", "possible_keys", "name") - - @docstrings.dedent - def check_key(self, key, raise_error=True, *args, **kwargs): - """ - Checks whether the key is a valid formatoption - - Parameters - ---------- - %(check_key.parameters.no_possible_keys|name)s - - Returns - ------- - %(check_key.returns)s - - Raises - ------ - %(check_key.raises)s""" - return check_key( - key, - possible_keys=list(self), - raise_error=raise_error, - name="formatoption keyword", - *args, - **kwargs, - ) - - @classmethod - @docstrings.get_sections( - base="Plotter.check_data", sections=["Parameters", "Returns"] - ) - @dedent - def check_data(cls, name, dims, is_unstructured): - """ - A validation method for the data shape - - The default method does nothing and should be subclassed to validate - the results. If the plotter accepts a :class:`InteractiveList`, it - should accept a list for name and dims - - Parameters - ---------- - name: str or list of str - The variable name(s) of the data - dims: list of str or list of lists of str - The dimension name(s) of the data - is_unstructured: bool or list of bool - True if the corresponding array is unstructured - - Returns - ------- - list of bool or None - True, if everything is okay, False in case of a serious error, - None if it is intermediate. Each object in this list corresponds to - one in the given `name` - list of str - The message giving more information on the reason. Each object in - this list corresponds to one in the given `name`""" - if isinstance(name, six.string_types): - name = [name] - dims = [dims] - is_unstructured = [is_unstructured] - N = len(name) - if len(dims) != N or len(is_unstructured) != N: - return [False] * N, [ - "Number of provided names (%i) and dimensions " - "(%i) or unstructured information (%i) are not the same" - % (N, len(dims), len(is_unstructured)) - ] * N - return [True] * N, [""] * N - - docstrings.keep_params("Plotter.parameters", "ax", "make_plot", "clear") - - @docstrings.dedent - def initialize_plot( - self, - data=None, - ax=None, - make_plot=True, - clear=False, - draw=False, - remove=False, - priority=None, - ): - """ - Initialize the plot for a data array - - Parameters - ---------- - data: InteractiveArray or ArrayList, optional - Data object that shall be visualized. - - - If not None and `plot` is True, the given data is visualized. - - If None and the :attr:`data` attribute is not None, the data in - the :attr:`data` attribute is visualized - - If both are None, nothing is done. - %(Plotter.parameters.ax|make_plot|clear)s - %(InteractiveBase.start_update.parameters.draw)s - remove: bool - If True, old effects by the formatoptions in this plotter are - undone first - priority: int - If given, initialize only the formatoption with the given priority. - This value must be out of :data:`START`, :data:`BEFOREPLOTTING` or - :data:`END` - """ - if data is None and self.data is not None: - data = self.data - else: - self.data = data - self.ax = ax - if data is None: # nothing to do if no data is given - return - self.no_auto_update = not ( - not self.no_auto_update or not data.psy.no_auto_update - ) - data.psy.plotter = self - if not make_plot: # stop here if we shall not plot - return - self.logger.debug("Initializing plot...") - if remove: - self.logger.debug(" Removing old formatoptions...") - for fmto in self._fmtos: - try: - fmto.remove() - except Exception: - self.logger.debug( - "Could not remove %s while initializing", - fmto.key, - exc_info=True, - ) - if clear: - self.logger.debug(" Clearing axes...") - self.ax.clear() - self.cleared = True - # get the formatoptions. We sort them here by key to make sure that the - # order always stays the same (easier for debugging) - fmto_groups = self._grouped_fmtos( - self._sorted_by_priority( - sorted(self._fmtos, key=lambda fmto: fmto.key) - ) - ) - self.plot_data = self.data - self._updating = True - for fmto_priority, grouper in fmto_groups: - if priority is None or fmto_priority == priority: - self._plot_by_priority( - fmto_priority, grouper, initializing=True - ) - self._release_all(True) # finish the update - self.cleared = False - self.replot = False - self._initialized = True - self._updating = False - - if draw is None: - draw = rcParams["auto_draw"] - if draw: - self.draw() - if rcParams["auto_show"]: - self.show() - - docstrings.keep_params( - "InteractiveBase._register_update.parameters", "force", "todefault" - ) - - @docstrings.get_sections(base="Plotter._register_update") - @docstrings.dedent - def _register_update( - self, fmt={}, replot=False, force=False, todefault=False - ): - """ - Register formatoptions for the update - - Parameters - ---------- - fmt: dict - Keys can be any valid formatoptions with the corresponding values - (see the :attr:`formatoptions` attribute) - replot: bool - Boolean that determines whether the data specific formatoptions - shall be updated in any case or not. - %(InteractiveBase._register_update.parameters.force|todefault)s""" - if self.disabled: - return - self.replot = self.replot or replot - self._todefault = self._todefault or todefault - if force is True: - force = list(fmt) - self._force.update( - [ret[0] for ret in map(self.check_key, force or [])] - ) - # check the keys - list(map(self.check_key, fmt)) - self._registered_updates.update(fmt) - - def make_plot(self): - """Method for making the plot - - This method is called at the end of the :attr:`BEFOREPLOTTING` stage if - and only if the :attr:`plot_fmt` attribute is set to ``True``""" - pass - - @docstrings.dedent - def start_update(self, draw=None, queues=None, update_shared=True): - """ - Conduct the registered plot updates - - This method starts the updates from what has been registered by the - :meth:`update` method. You can call this method if you did not set the - `auto_update` parameter to True when calling the :meth:`update` method - and when the :attr:`no_auto_update` attribute is True. - - Parameters - ---------- - %(InteractiveBase.start_update.parameters)s - - Returns - ------- - %(InteractiveBase.start_update.returns)s - - See Also - -------- - :attr:`no_auto_update`, update""" - - def update_the_others(): - for fmto in fmtos: - for other_fmto in fmto.shared: - if not other_fmto.plotter._updating: - other_fmto.plotter._register_update( - force=[other_fmto.key] - ) - for fmto in fmtos: - for other_fmto in fmto.shared: - if not other_fmto.plotter._updating: - other_draw = other_fmto.plotter.start_update( - draw=False, update_shared=False - ) - if other_draw: - self._figs2draw.add( - other_fmto.plotter.ax.get_figure() - ) - - if self.disabled: - return False - - if queues is not None: - queues[0].get() - self.logger.debug( - "Starting update of %r", self._registered_updates.keys() - ) - # update the formatoptions - self._save_state() - try: - # get the formatoptions. We sort them here by key to make sure that - # the order always stays the same (easier for debugging) - fmtos = sorted(self._set_and_filter(), key=lambda fmto: fmto.key) - except Exception: - # restore last (working) state - last_state = self._old_fmt.pop(-1) - with self.no_validation: - for key in self: - self[key] = last_state.get(key, getattr(self, key).default) - if queues is not None: - queues[0].task_done() - self._release_all(queue=None if queues is None else queues[1]) - # raise the error - raise - for fmto in fmtos: - for fmto2 in fmto.shared: - fmto2.plotter._to_update[fmto2] = self - if queues is not None: - self._updating = True - queues[0].task_done() - # wait for the other tasks to finish - queues[0].join() - queues[1].get() - fmtos.extend( - [ - fmto - for fmto in self._insert_additionals(list(self._to_update)) - if fmto not in fmtos - ] - ) - self._to_update.clear() - - fmto_groups = self._grouped_fmtos(self._sorted_by_priority(fmtos[:])) - # if any formatoption requires a clearing of the axes is updated, - # we reinitialize the plot - try: - if self.cleared: - self.reinit(draw=draw) - update_the_others() - arr_draw = True - else: - # otherwise we update it - arr_draw = False - for priority, grouper in fmto_groups: - arr_draw = True - self._plot_by_priority(priority, grouper) - update_the_others() - except Exception: - raise - finally: - # make sure that all locks are released - self._release_all( - finish=True, queue=None if queues is None else queues[1] - ) - if draw is None: - draw = rcParams["auto_draw"] - if draw and arr_draw: - self.draw() - if rcParams["auto_show"]: - self.show() - self.replot = False - return arr_draw - - def _release_all(self, finish=False, queue=None): - # make sure that all locks are released - try: - for fmto in self._fmtos: - if finish: - fmto.finish_update() - try: - fmto.lock.release() - except RuntimeError: - pass - except Exception: - raise - finally: - if queue is not None: - queue.task_done() - queue.join() - self._updating = False - - def _plot_by_priority(self, priority, fmtos, initializing=False): - def update(fmto): - other_fmto = self._shared.get(fmto.key) - if other_fmto: - self.logger.debug( - "%s is shared with %s", - fmto.key, - other_fmto.plotter.logger.name, - ) - other_fmto.share(fmto, initializing=initializing) - # but if not, share them - else: - if initializing: - self.logger.debug("Initializing %s", fmto.key) - fmto.initialize_plot(fmto.value) - else: - self.logger.debug("Updating %s", fmto.key) - fmto.update(fmto.value) - try: - fmto.lock.release() - except RuntimeError: - pass - - self._initializing = initializing - - self.logger.debug( - "%s formatoptions with priority %i", - "Initializing" if initializing else "Updating", - priority, - ) - - if priority >= START or priority == END: - for fmto in fmtos: - update(fmto) - elif priority == BEFOREPLOTTING: - for fmto in fmtos: - update(fmto) - self._make_plot() - - self._initializing = False - - @docstrings.dedent - def reinit(self, draw=None, clear=False): - """ - Reinitializes the plot with the same data and on the same axes. - - Parameters - ---------- - %(InteractiveBase.start_update.parameters.draw)s - clear: bool - Whether to clear the axes or not - - Warnings - -------- - The axes may be cleared when calling this method (even if `clear` is - set to False)!""" - # call the initialize_plot method. Note that clear can be set to - # False if any fmto has requires_clearing attribute set to True, - # because this then has been cleared before - self.initialize_plot( - self.data, - self._ax, - draw=draw, - clear=clear or any(fmto.requires_clearing for fmto in self._fmtos), - remove=True, - ) - - def draw(self): - """Draw the figures and those that are shared and have been changed""" - for fig in self.figs2draw: - fig.canvas.draw() - self._figs2draw.clear() - - def _grouped_fmtos(self, fmtos): - def key_func(fmto): - if fmto.priority >= START: - return START - elif fmto.priority >= BEFOREPLOTTING: - return BEFOREPLOTTING - else: - return END - - return groupby(fmtos, key_func) - - def _set_and_filter(self): - """Filters the registered updates and sort out what is not needed - - This method filters out the formatoptions that have not changed, sets - the new value and returns an iterable that is sorted by the priority - (highest priority comes first) and dependencies - - Returns - ------- - list - list of :class:`Formatoption` objects that have to be updated""" - fmtos = [] - seen = set() - for key in self._force: - self._registered_updates.setdefault(key, getattr(self, key).value) - for key, value in chain( - six.iteritems(self._registered_updates), - six.iteritems({key: getattr(self, key).default for key in self}) - if self._todefault - else (), - ): - if key in seen: - continue - seen.add(key) - fmto = getattr(self, key) - # if the key is shared, a warning will be printed as long as - # this plotter is not also updating (for example due to a whole - # project update) - if key in self._shared and key not in self._force: - if not self._shared[key].plotter._updating: - warn( - ( - "%s formatoption is shared with another plotter." - " Use the unshare method to enable the updating" - ) - % (fmto.key), - logger=self.logger, - ) - changed = False - else: - try: - changed = fmto.check_and_set( - value, - todefault=self._todefault, - validate=not self.no_validation, - ) - except Exception as e: - self._registered_updates.pop(key, None) - self.logger.debug("Failed to set %s", key) - raise e - changed = changed or key in self._force - if changed: - fmtos.append(fmto) - fmtos = self._insert_additionals(fmtos, seen) - for fmto in fmtos: - fmto.lock.acquire() - self._todefault = False - self._registered_updates.clear() - self._force.clear() - return fmtos - - def _insert_additionals(self, fmtos, seen=None): - """ - Insert additional formatoptions into `fmtos`. - - This method inserts those formatoptions into `fmtos` that are required - because one of the following criteria is fullfilled: - - 1. The :attr:`replot` attribute is True - 2. Any formatoption with START priority is in `fmtos` - 3. A dependency of one formatoption is in `fmtos` - - Parameters - ---------- - fmtos: list - The list of formatoptions that shall be updated - seen: set - The formatoption keys that shall not be included. If None, all - formatoptions in `fmtos` are used - - Returns - ------- - fmtos - The initial `fmtos` plus further formatoptions - - Notes - ----- - `fmtos` and `seen` are modified in place (except that any formatoption - in the initial `fmtos` has :attr:`~Formatoption.requires_clearing` - attribute set to True)""" - - def get_dependencies(fmto): - if fmto is None: - return [] - return fmto.dependencies + list( - chain( - *map( - lambda key: get_dependencies(getattr(self, key, None)), - fmto.dependencies, - ) - ) - ) - - seen = seen or {fmto.key for fmto in fmtos} - keys = {fmto.key for fmto in fmtos} - self.replot = self.replot or any( - fmto.requires_replot for fmto in fmtos - ) - if self.replot or any(fmto.priority >= START for fmto in fmtos): - self.replot = True - self.plot_data = self.data - new_fmtos = dict( - (f.key, f) - for f in self._fmtos - if ((f not in fmtos and is_data_dependent(f, self.data))) - ) - seen.update(new_fmtos) - keys.update(new_fmtos) - fmtos += list(new_fmtos.values()) - - # insert the formatoptions that have to be updated if the plot is - # changed - if any(fmto.priority >= BEFOREPLOTTING for fmto in fmtos): - new_fmtos = dict( - (f.key, f) - for f in self._fmtos - if ((f not in fmtos and f.update_after_plot)) - ) - fmtos += list(new_fmtos.values()) - for fmto in set(self._fmtos).difference(fmtos): - all_dependencies = get_dependencies(fmto) - if keys.intersection(all_dependencies): - fmtos.append(fmto) - if any(fmto.requires_clearing for fmto in fmtos): - self.cleared = True - return list(self._fmtos) - return fmtos - - def _sorted_by_priority(self, fmtos, changed=None): - """Sort the formatoption objects by their priority and dependency - - Parameters - ---------- - fmtos: list - list of :class:`Formatoption` instances - changed: list - the list of formatoption keys that have changed - - Yields - ------ - Formatoption - The next formatoption as it comes by the sorting - - Warnings - -------- - The list `fmtos` is cleared by this method!""" - - def pop_fmto(key): - idx = fmtos_keys.index(key) - del fmtos_keys[idx] - return fmtos.pop(idx) - - def get_children(fmto, parents_keys): - all_fmtos = fmtos_keys + parents_keys - for key in fmto.children + fmto.dependencies: - if key not in fmtos_keys: - continue - child_fmto = pop_fmto(key) - for childs_child in get_children( - child_fmto, parents_keys + [child_fmto.key] - ): - yield childs_child - # filter out if parent is in update list - if ( - any(key in all_fmtos for key in child_fmto.parents) - or fmto.key in child_fmto.parents - ): - continue - yield child_fmto - - fmtos.sort(key=lambda fmto: fmto.priority, reverse=True) - fmtos_keys = [fmto.key for fmto in fmtos] - self._last_update = changed or fmtos_keys[:] - self.logger.debug("Update the formatoptions %s", fmtos_keys) - while fmtos: - del fmtos_keys[0] - fmto = fmtos.pop(0) - # first update children - for child_fmto in get_children(fmto, [fmto.key]): - yield child_fmto - # filter out if parent is in update list - if any(key in fmtos_keys for key in fmto.parents): - continue - yield fmto - - @classmethod - def _get_formatoptions(cls, include_bases=True): - """ - Iterator over formatoptions - - This class method returns an iterator that contains all the - formatoption keys that are in this class and that are defined - in the base classes - - Notes - ----- - There is absolutely no need to call this method besides the plotter - initialization, since all formatoptions are in the plotter itself. - Just type:: - - >>> list(plotter) - - to get the formatoptions. - - See Also - -------- - _format_keys""" - - def base_fmtos(base): - return filter( - lambda key: isinstance(getattr(cls, key), Formatoption), - getattr(base, "_get_formatoptions", empty)(False), - ) - - def empty(*args, **kwargs): - return list() - - fmtos = ( - attr - for attr, obj in six.iteritems(cls.__dict__) - if isinstance(obj, Formatoption) - ) - if not include_bases: - return fmtos - return unique_everseen(chain(fmtos, *map(base_fmtos, cls.__mro__))) - - docstrings.keep_types( - "check_key.parameters", "kwargs", r"``\*args,\*\*kwargs``" - ) - - @classmethod - @docstrings.get_sections(base="Plotter._enhance_keys") - @docstrings.dedent - def _enhance_keys(cls, keys=None, *args, **kwargs): - """ - Enhance the given keys by groups - - Parameters - ---------- - keys: list of str or None - If None, the all formatoptions of the given class are used. Group - names from the :attr:`psyplot.plotter.groups` mapping are replaced - by the formatoptions - - Other Parameters - ---------------- - %(check_key.parameters.kwargs)s - - Returns - ------- - list of str - The enhanced list of the formatoptions""" - all_keys = list(cls._get_formatoptions()) - if isinstance(keys, six.string_types): - keys = [keys] - else: - keys = list(keys or sorted(all_keys)) - fmto_groups = defaultdict(list) - for key in all_keys: - fmto_groups[getattr(cls, key).group].append(key) - new_i = 0 - for i, key in enumerate(keys[:]): - if key in fmto_groups: - del keys[new_i] - for key2 in fmto_groups[key]: - if key2 not in keys: - keys.insert(new_i, key2) - new_i += 1 - else: - valid, similar, message = check_key( - key, - all_keys, - False, - "formatoption keyword", - *args, - **kwargs, - ) - if not valid: - keys.remove(key) - new_i -= 1 - warn(message) - new_i += 1 - return keys - - @classmethod - @docstrings.get_sections( - base="Plotter.show_keys", - sections=["Parameters", "Returns", "Other Parameters"], - ) - @docstrings.dedent - def show_keys( - cls, - keys=None, - indent=0, - grouped=False, - func=None, - include_links=False, - *args, - **kwargs, - ): - """ - Classmethod to return a nice looking table with the given formatoptions - - Parameters - ---------- - %(Plotter._enhance_keys.parameters)s - indent: int - The indentation of the table - grouped: bool, optional - If True, the formatoptions are grouped corresponding to the - :attr:`Formatoption.groupname` attribute - - Other Parameters - ---------------- - func: function or None - The function the is used for returning (by default it is printed - via the :func:`print` function or (when using the gui) in the - help explorer). The given function must take a string as argument - include_links: bool or None, optional - Default False. If True, links (in restructured formats) are - included in the description. If None, the behaviour is determined - by the :attr:`psyplot.plotter.Plotter.include_links` attribute. - %(Plotter._enhance_keys.other_parameters)s - - Returns - ------- - results of `func` - None if `func` is the print function, otherwise anything else - - See Also - -------- - show_summaries, show_docs""" - - def titled_group(groupname): - bars = str_indent + "*" * len(groupname) + "\n" - return bars + str_indent + groupname + "\n" + bars - - keys = cls._enhance_keys(keys, *args, **kwargs) - str_indent = " " * indent - func = func or default_print_func - # call this function recursively when grouped is True - if grouped: - grouped_keys = Defaultdict(list) - for fmto in map(lambda key: getattr(cls, key), keys): - grouped_keys[fmto.groupname].append(fmto.key) - text = "" - for group, keys in six.iteritems(grouped_keys): - text += ( - titled_group(group) - + cls.show_keys( - keys, - indent=indent, - grouped=False, - func=six.text_type, - include_links=include_links, - ) - + "\n\n" - ) - return func(text.rstrip()) - - if not keys: - return - n = len(keys) - ncols = min([4, n]) # number of columns - # The number of cells in the table is one of the following cases: - # 1. The number of columns and equal to the number of keys - # 2. The number of keys - # 3. The number of keys plus the empty cells in the last column - ncells = n + ((ncols - (n % ncols)) if n != ncols else 0) - if include_links or (include_links is None and cls.include_links): - long_keys = list( - map( - lambda key: ":attr:`~%s.%s.%s`" - % (cls.__module__, cls.__name__, key), - keys, - ) - ) - else: - long_keys = keys - maxn = max(map(len, long_keys)) # maximal lenght of the keys - # extend with empty cells - long_keys.extend([" " * maxn] * (ncells - n)) - bars = (str_indent + "+-" + ("-" * (maxn) + "-+-") * ncols)[:-1] - lines = ( - "| %s |\n%s" - % ( - " | ".join( - key.ljust(maxn) for key in long_keys[i : i + ncols] - ), - bars, - ) - for i in range(0, n, ncols) - ) - text = bars + "\n" + str_indent + ("\n" + str_indent).join(lines) - if six.PY2: - text = text.encode("utf-8") - - return func(text) - - @classmethod - @docstrings.dedent - def _show_doc( - cls, - fmt_func, - keys=None, - indent=0, - grouped=False, - func=None, - include_links=False, - *args, - **kwargs, - ): - """ - Classmethod to print the formatoptions and their documentation - - This function is the basis for the :meth:`show_summaries` and - :meth:`show_docs` methods - - Parameters - ---------- - fmt_func: function - A function that takes the key, the key as it is printed, and the - documentation of a formatoption as argument and returns what shall - be printed - %(Plotter.show_keys.parameters)s - - Other Parameters - ---------------- - %(Plotter.show_keys.other_parameters)s - - Returns - ------- - %(Plotter.show_keys.returns)s - - See Also - -------- - show_summaries, show_docs""" - - def titled_group(groupname): - bars = str_indent + "*" * len(groupname) + "\n" - return bars + str_indent + groupname + "\n" + bars - - func = func or default_print_func - - keys = cls._enhance_keys(keys, *args, **kwargs) - str_indent = " " * indent - if grouped: - grouped_keys = Defaultdict(list) - for fmto in map(lambda key: getattr(cls, key), keys): - grouped_keys[fmto.groupname].append(fmto.key) - text = "\n\n".join( - titled_group(group) - + cls._show_doc( - fmt_func, - keys, - indent=indent, - grouped=False, - func=str, - include_links=include_links, - ) - for group, keys in six.iteritems(grouped_keys) - ) - return func(text.rstrip()) - - if include_links or (include_links is None and cls.include_links): - long_keys = list( - map( - lambda key: ":attr:`~%s.%s.%s`" - % (cls.__module__, cls.__name__, key), - keys, - ) - ) - else: - long_keys = keys - - text = "\n".join( - str_indent - + long_key - + "\n" - + fmt_func(key, long_key, getattr(cls, key).__doc__) - for long_key, key in zip(long_keys, keys) - ) - return func(text) - - @classmethod - @docstrings.dedent - def show_summaries(cls, keys=None, indent=0, *args, **kwargs): - """ - Classmethod to print the summaries of the formatoptions - - Parameters - ---------- - %(Plotter.show_keys.parameters)s - - Other Parameters - ---------------- - %(Plotter.show_keys.other_parameters)s - - Returns - ------- - %(Plotter.show_keys.returns)s - - See Also - -------- - show_keys, show_docs""" - - def find_summary(key, key_txt, doc): - return "\n".join(wrapper.wrap(doc[: doc.find("\n\n")])) - - str_indent = " " * indent - wrapper = TextWrapper( - width=80, - initial_indent=str_indent + " " * 4, - subsequent_indent=str_indent + " " * 4, - ) - return cls._show_doc( - find_summary, keys=keys, indent=indent, *args, **kwargs - ) - - @classmethod - @docstrings.dedent - def show_docs(cls, keys=None, indent=0, *args, **kwargs): - """ - Classmethod to print the full documentations of the formatoptions - - Parameters - ---------- - %(Plotter.show_keys.parameters)s - - Other Parameters - ---------------- - %(Plotter.show_keys.other_parameters)s - - Returns - ------- - %(Plotter.show_keys.returns)s - - See Also - -------- - show_keys, show_docs""" - - def full_doc(key, key_txt, doc): - return ("=" * len(key_txt)) + "\n" + doc + "\n" - - return cls._show_doc( - full_doc, keys=keys, indent=indent, *args, **kwargs - ) - - @classmethod - def _get_rc_strings(cls): - """ - Recursive method to get the base strings in the rcParams dictionary. - - This method takes the :attr:`_rcparams_string` attribute from the given - `class` and combines it with the :attr:`_rcparams_string` attributes - from the base classes. - The returned frozenset can be used as base strings for the - :meth:`psyplot.config.rcsetup.RcParams.find_and_replace` method. - - Returns - ------- - list - The first entry is the :attr:`_rcparams_string` of this class, - the following the :attr:`_rcparams_string` attributes of the - base classes according to the method resolution order of this - class""" - return list( - unique_everseen( - chain( - *map( - lambda base: getattr(base, "_rcparams_string", []), - cls.__mro__, - ) - ) - ) - ) - - def _set_rc(self): - """Method to set the rcparams and defaultParams for this plotter""" - base_str = self._get_rc_strings() - # to make sure that the '.' is not interpreted as a regex pattern, - # we specify the pattern_base by ourselves - pattern_base = map(lambda s: s.replace(".", r"\."), base_str) - # pattern for valid keys being all formatoptions in this plotter - pattern = "(%s)(?=$)" % "|".join(self._get_formatoptions()) - self._rc = rcParams.find_and_replace( - base_str, pattern=pattern, pattern_base=pattern_base - ) - user_rc = SubDict( - rcParams["plotter.user"], - base_str, - pattern=pattern, - pattern_base=pattern_base, - ) - self._rc.update(user_rc.data) - - self._defaultParams = SubDict( - rcParams.defaultParams, - base_str, - pattern=pattern, - pattern_base=pattern_base, - ) - - docstrings.keep_params("InteractiveBase.update.parameters", "auto_update") - - @docstrings.dedent - def update( - self, - fmt={}, - replot=False, - auto_update=False, - draw=None, - force=False, - todefault=False, - **kwargs, - ): - """ - Update the formatoptions and the plot - - If the :attr:`data` attribute of this plotter is None, the plotter is - updated like a usual dictionary (see :meth:`dict.update`). Otherwise - the update is registered and the plot is updated if `auto_update` is - True or if the :meth:`start_update` method is called (see below). - - Parameters - ---------- - %(Plotter._register_update.parameters)s - %(InteractiveBase.start_update.parameters)s - %(InteractiveBase.update.parameters.auto_update)s - ``**kwargs`` - Any other formatoption that shall be updated (additionally to those - in `fmt`) - - Notes - ----- - %(InteractiveBase.update.notes)s""" - if self.disabled: - return - fmt = dict(fmt) - if kwargs: - fmt.update(kwargs) - # if the data is None, update like a usual dictionary (but with - # validation) - if not self._initialized: - for key, val in six.iteritems(fmt): - self[key] = val - return - - self._register_update( - fmt=fmt, replot=replot, force=force, todefault=todefault - ) - if not self.no_auto_update or auto_update: - self.start_update(draw=draw) - - def _set_sharing_keys(self, keys): - """ - Set the keys to share or unshare - - Parameters - ---------- - keys: string or iterable of strings - The iterable may contain formatoptions that shall be shared (or - unshared), or group names of formatoptions to share all - formatoptions of that group (see the :attr:`fmt_groups` property). - If None, all formatoptions of this plotter are inserted. - - Returns - ------- - set - The set of formatoptions to share (or unshare)""" - if isinstance(keys, str): - keys = {keys} - keys = set(self) if keys is None else set(keys) - fmto_groups = self._fmto_groups - keys.update( - chain( - *( - map(lambda fmto: fmto.key, fmto_groups[key]) - for key in keys.intersection(fmto_groups) - ) - ) - ) - keys.difference_update(fmto_groups) - return keys - - @docstrings.get_sections(base="Plotter.share") - @docstrings.dedent - def share(self, plotters, keys=None, draw=None, auto_update=False): - """ - Share the formatoptions of this plotter with others - - This method shares the formatoptions of this :class:`Plotter` instance - with others to make sure that, if the formatoption of this changes, - those of the others change as well - - Parameters - ---------- - plotters: list of :class:`Plotter` instances or a :class:`Plotter` - The plotters to share the formatoptions with - keys: string or iterable of strings - The formatoptions to share, or group names of formatoptions to - share all formatoptions of that group (see the - :attr:`fmt_groups` property). If None, all formatoptions of this - plotter are unshared. - %(InteractiveBase.start_update.parameters.draw)s - %(InteractiveBase.update.parameters.auto_update)s - - See Also - -------- - unshare, unshare_me""" - auto_update = auto_update or not self.no_auto_update - if isinstance(plotters, Plotter): - plotters = [plotters] - keys = self._set_sharing_keys(keys) - for plotter in plotters: - for key in keys: - fmto = self._shared.get(key, getattr(self, key)) - if not getattr(plotter, key) == fmto: - plotter._shared[key] = getattr(self, key) - fmto.shared.add(getattr(plotter, key)) - # now exit if we are not initialized - if self._initialized: - self.update(force=keys, auto_update=auto_update, draw=draw) - for plotter in plotters: - if not plotter._initialized: - continue - old_registered = plotter._registered_updates.copy() - plotter._registered_updates.clear() - try: - plotter.update(force=keys, auto_update=auto_update, draw=draw) - except Exception: - raise - finally: - plotter._registered_updates.clear() - plotter._registered_updates.update(old_registered) - if draw is None: - draw = rcParams["auto_draw"] - if draw: - self.draw() - if rcParams["auto_show"]: - self.show() - - @docstrings.dedent - def unshare(self, plotters, keys=None, auto_update=False, draw=None): - """ - Close the sharing connection of this plotter with others - - This method undoes the sharing connections made by the :meth:`share` - method and releases the given `plotters` again, such that the - formatoptions in this plotter may be updated again to values different - from this one. - - Parameters - ---------- - plotters: list of :class:`Plotter` instances or a :class:`Plotter` - The plotters to release - keys: string or iterable of strings - The formatoptions to unshare, or group names of formatoptions to - unshare all formatoptions of that group (see the - :attr:`fmt_groups` property). If None, all formatoptions of this - plotter are unshared. - %(InteractiveBase.start_update.parameters.draw)s - %(InteractiveBase.update.parameters.auto_update)s - - See Also - -------- - share, unshare_me""" - auto_update = auto_update or not self.no_auto_update - if isinstance(plotters, Plotter): - plotters = [plotters] - keys = self._set_sharing_keys(keys) - for plotter in plotters: - plotter.unshare_me( - keys, auto_update=auto_update, draw=draw, update_other=False - ) - self.update(force=keys, auto_update=auto_update, draw=draw) - - @docstrings.get_sections(base="Plotter.unshare_me") - @docstrings.dedent - def unshare_me( - self, keys=None, auto_update=False, draw=None, update_other=True - ): - """ - Close the sharing connection of this plotter with others - - This method undoes the sharing connections made by the :meth:`share` - method and release this plotter again. - - Parameters - ---------- - keys: string or iterable of strings - The formatoptions to unshare, or group names of formatoptions to - unshare all formatoptions of that group (see the - :attr:`fmt_groups` property). If None, all formatoptions of this - plotter are unshared. - %(InteractiveBase.start_update.parameters.draw)s - %(InteractiveBase.update.parameters.auto_update)s - - See Also - -------- - share, unshare""" - auto_update = auto_update or not self.no_auto_update - keys = self._set_sharing_keys(keys) - to_update = [] - for key in keys: - fmto = getattr(self, key) - try: - other_fmto = self._shared.pop(key) - except KeyError: - pass - else: - other_fmto.shared.remove(fmto) - if update_other: - other_fmto.plotter._register_update(force=[other_fmto.key]) - to_update.append(other_fmto.plotter) - self.update(force=keys, draw=draw, auto_update=auto_update) - if update_other and auto_update: - for plotter in to_update: - plotter.start_update(draw=draw) - - def get_vfunc(self, key): - """Return the validation function for a specified formatoption - - Parameters - ---------- - key: str - Formatoption key in the :attr:`rc` dictionary - - Returns - ------- - function - Validation function for this formatoption""" - return self._defaultParams[key][1] - - def _save_state(self): - """Saves the current formatoptions""" - self._old_fmt.append(self.changed) - - def show(self): - """Shows all open figures""" - import matplotlib.pyplot as plt - - plt.show(block=False) - - @dedent - def has_changed(self, key, include_last=True): - """ - Determine whether a formatoption changed in the last update - - Parameters - ---------- - key: str - A formatoption key contained in this plotter - include_last: bool - if True and the formatoption has been included in the last update, - the return value will not be None. Otherwise the return value will - only be not None if it changed during the last update - - Returns - ------- - None or list - - None, if the value has not been changed during the last update or - `key` is not a valid formatoption key - - a list of length two with the old value in the first place and - the given `value` at the second""" - if self._initializing or key not in self: - return - fmto = getattr(self, key) - if self._old_fmt and key in self._old_fmt[-1]: - old_val = self._old_fmt[-1][key] - else: - old_val = fmto.default - if fmto.diff(old_val) or ( - include_last and fmto.key in self._last_update - ): - return [old_val, fmto.value] - - def get_enhanced_attrs(self, arr, axes=["x", "y", "t", "z"]): - if isinstance(arr, InteractiveList): - all_attrs = list( - starmap(self.get_enhanced_attrs, zip(arr, repeat(axes))) - ) - attrs = { - key: val - for key, val in six.iteritems(all_attrs[0]) - if all( - key in attrs and attrs[key] == val - for attrs in all_attrs[1:] - ) - } - attrs.update(arr.attrs) - else: - attrs = arr.attrs.copy() - base_variables = self.base_variables - if len(base_variables) > 1: # multiple variables - for name, base_var in six.iteritems(base_variables): - attrs.update( - { - six.text_type(name) + key: value - for key, value in six.iteritems(base_var.attrs) - } - ) - else: - base_var = next(six.itervalues(base_variables)) - attrs["name"] = arr.name - for dim, coord in six.iteritems(getattr(arr, "coords", {})): - if coord.size == 1: - attrs[dim] = format_time(coord.values) - if isinstance(self.data, InteractiveList): - decoder = self.data[0].psy.decoder - else: - decoder = self.data.psy.decoder - for dim in axes: - for obj in [base_var, arr]: - coord = getattr(decoder, "get_" + dim)( - obj, coords=getattr(arr, "coords", None) - ) - if coord is None: - continue - if coord.size == 1: - attrs[dim] = format_time(coord.values) - attrs[dim + "name"] = coord.name - for key, val in six.iteritems(coord.attrs): - attrs[dim + key] = val - self._enhanced_attrs = attrs - return attrs - - def _make_plot(self): - plot_fmtos = [fmto for fmto in self._fmtos if fmto.plot_fmt] - plot_fmtos.sort(key=lambda fmto: fmto.priority, reverse=True) - for fmto in plot_fmtos: - self.logger.debug("Making plot with %s formatoption", fmto.key) - fmto.make_plot() - - @classmethod - def _get_sample_projection(cls): - """Returns None. May be subclassed to return a projection that - can be used when creating a subplot""" - pass - - @docstrings.dedent - def convert_coordinate(self, coord, *variables): - """Convert a coordinate to units necessary for the plot. - - %(Formatoption.convert_coordinate.summary_ext)s - - Parameters - ---------- - %(Formatoption.convert_coordinate.parameters)s - - Returns - ------- - %(Formatoption.convert_coordinate.returns)s - - Notes - ----- - This method is supposed to be implemented by subclasses. The default - implementation by the :class:`Plotter` class does nothing. - """ - return coord diff --git a/psyplot/project.py b/psyplot/project.py deleted file mode 100755 index 6411ce5..0000000 --- a/psyplot/project.py +++ /dev/null @@ -1,3090 +0,0 @@ -"""Project module of the psyplot Package. - -This module contains the :class:`Project` class that serves as the main -part of the psyplot API. One instance of the :class:`Project` class serves as -coordinator of multiple plots and can be distributed into subprojects that -keep reference to the main project without holding all array instances - -Furthermore this module contains an easy pyplot-like API to the current -subproject.""" - -# SPDX-FileCopyrightText: 2016-2024 University of Lausanne -# SPDX-FileCopyrightText: 2020-2021 Helmholtz-Zentrum Geesthacht - -# SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH -# -# SPDX-License-Identifier: LGPL-3.0-only - -import logging -import os -import os.path as osp -import pickle -import sys -from collections import defaultdict -from copy import deepcopy as _deepcopy -from functools import partial, wraps -from importlib import import_module -from itertools import chain, count, cycle, islice, repeat - -import matplotlib as mpl -import matplotlib.figure as mfig -import numpy as np -import pandas as pd -import six -import xarray -import yaml -from matplotlib.axes import SubplotBase - -import psyplot -import psyplot.utils as utils -from psyplot import get_versions, rcParams -from psyplot.config.rcsetup import get_configdir, psyplot_fname -from psyplot.data import ( - ArrayList, - CFDecoder, - InteractiveList, - Signal, - _MissingModule, - open_dataset, - open_mfdataset, - safe_list, -) -from psyplot.docstring import dedent, docstrings, safe_modulo -from psyplot.plotter import Plotter, unique_everseen -from psyplot.utils import get_default_value as _get_default_value -from psyplot.warning import critical, warn - -try: - from cdo import CDO_PY_VERSION as cdo_version - from cdo import Cdo as _CdoBase - - with_cdo = True - cdo_version = tuple(map(int, cdo_version.split(".")[:2])) -except ImportError as e: - Cdo = _MissingModule(e) - with_cdo = False - cdo_version = None - -try: # try import show_colormaps for convenience - from psy_simple.colors import get_cmap, show_colormaps # noqa: F401 -except ImportError: - pass - -if rcParams["project.import_seaborn"] is not False: - try: - import seaborn as _sns - except ImportError as e: - if rcParams["project.import_seaborn"]: - raise - _sns = _MissingModule(e) - -_open_projects = [] # list of open projects -_current_project = None # current main project -_current_subproject = None # current subproject - -# the informations on the psyplot and plugin versions -_versions = get_versions(requirements=False) - - -_concat_dim_default = _get_default_value(xarray.open_mfdataset, "concat_dim") - - -def _update_versions(): - """Update :attr:`_versions` with the registered plotter methods""" - for pm_name in plot._plot_methods: - pm = getattr(plot, pm_name) - plugin = pm._plugin - if ( - plugin is not None - and plugin not in _versions - and pm.module in sys.modules - ): - _versions.update(get_versions(key=lambda s: s == plugin)) - - -@docstrings.get_sections(base="multiple_subplots") -@docstrings.dedent -def multiple_subplots( - rows=1, - cols=1, - maxplots=None, - n=1, - delete=True, - for_maps=False, - *args, - **kwargs, -): - """ - Function to create subplots. - - This function creates so many subplots on so many figures until the - specified number `n` is reached. - - Parameters - ---------- - rows: int - The number of subplots per rows - cols: int - The number of subplots per column - maxplots: int - The number of subplots per figure (if None, it will be row*cols) - n: int - number of subplots to create - delete: bool - If True, the additional subplots per figure are deleted - for_maps: bool - If True this is a simple shortcut for setting - ``subplot_kw=dict(projection=cartopy.crs.PlateCarree())`` and is - useful if you want to use the :attr:`~ProjectPlotter.mapplot`, - :attr:`~ProjectPlotter.mapvector` or - :attr:`~ProjectPlotter.mapcombined` plotting methods - ``*args`` and ``**kwargs`` - anything that is passed to the :func:`matplotlib.pyplot.subplots` - function - - Returns - ------- - list - list of maplotlib.axes.SubplotBase instances""" - import matplotlib.pyplot as plt - - axes = np.array([]) - maxplots = maxplots or rows * cols - kwargs.setdefault("figsize", [min(8.0 * cols, 16), min(6.5 * rows, 12)]) - if for_maps: - import cartopy.crs as ccrs - - subplot_kw = kwargs.setdefault("subplot_kw", {}) - subplot_kw["projection"] = ccrs.PlateCarree() - for i in range(0, n, maxplots): - fig, ax = plt.subplots(rows, cols, *args, **kwargs) - try: - axes = np.append(axes, ax.ravel()[:maxplots]) - if delete: - for iax in range(maxplots, rows * cols): - fig.delaxes(ax.ravel()[iax]) - except AttributeError: # got a single subplot - axes = np.append(axes, [ax]) - if i + maxplots > n and delete: - for ax2 in axes[n:]: - fig.delaxes(ax2) - axes = axes[:n] - return axes - - -def _is_slice(val): - return isinstance(val, slice) - - -def _only_main(func): - """Call the given `func` only from the main project""" - - @wraps(func) - def wrapper(self, *args, **kwargs): - if not self.is_main: - return getattr(self.main, func.__name__)(*args, **kwargs) - return func(self, *args, **kwargs) - - return wrapper - - -def _first_main(func): - """Call the given `func` with the same arguments but after the function - of the main project""" - - @wraps(func) - def wrapper(self, *args, **kwargs): - if not self.is_main: - getattr(self.main, func.__name__)(*args, **kwargs) - return func(self, *args, **kwargs) - - return wrapper - - -class Project(ArrayList): - """A manager of multiple interactive data projects""" - - _main = None - - _registered_plotters = {} #: registered plotter identifiers - - #: signal to be emiitted when the current main and/or subproject changes - oncpchange = Signal(name="oncpchange", cls_signal=True) - - # block the signals of this class - block_signals = utils._TempBool() - - @property - def main(self): - """:class:`Project`. The main project of this subproject""" - return self._main if self._main is not None else self - - @main.setter - def main(self, value): - self._main = value - - @property - @dedent - def plot(self): - """ - Plotting instance of this :class:`Project`. See the - :class:`ProjectPlotter` class for method documentations""" - return self._plot - - @property - def _fmtos(self): - """An iterator over formatoption objects - - Contains only the formatoption whose keys are in all plotters in this - list""" - plotters = self.plotters - if len(plotters) == 0: - return {} - p0 = plotters[0] - if len(plotters) == 1: - return p0._fmtos - return ( - getattr(p0, key) - for key in set(p0).intersection(*map(set, plotters[1:])) - ) - - @property - def is_csp(self): - """Boolean that is True if the project is the current subproject""" - return self is _current_subproject - - @property - def is_cmp(self): - """Boolean that is True if the project is the current main project""" - return self is _current_project - - @property - def figs(self): - """A mapping from figures to data objects with the plotter in this - figure""" - ret = utils.Defaultdict(lambda: self[1:0]) - for arr in self: - if arr.psy.plotter is not None: - ret[arr.psy.plotter.ax.get_figure()].append(arr) - return dict(ret) - - @property - def axes(self): - """A mapping from axes to data objects with the plotter in this axes""" - ret = utils.Defaultdict(lambda: self[1:0]) - for arr in self: - if arr.psy.plotter is not None: - ret[arr.psy.plotter.ax].append(arr) - return dict(ret) - - @property - def is_main(self): - """:class:`bool`. True if this :class:`Project` is a main project""" - return self._main is None - - @property - def logger(self): - """:class:`logging.Logger` of this instance""" - if not self.is_main: - return self.main.logger - try: - return self._logger - except AttributeError: - name = "%s.%s.%s" % ( - self.__module__, - self.__class__.__name__, - self.num, - ) - self._logger = logging.getLogger(name) - self.logger.debug("Initializing...") - return self._logger - - @logger.setter - def logger(self, value): - self._logger = value - - def with_plotter(self): - ret = super(Project, self).with_plotter - ret.main = self.main - return ret - - with_plotter = property(with_plotter, doc=ArrayList.with_plotter.__doc__) - - @property - def arr_names(self): - """Names of the arrays (!not of the variables!) in this list - - This attribute can be set with an iterable of unique names to change - the array names of the data objects in this list.""" - return list(arr.psy.arr_name for arr in self) - - @arr_names.setter - def arr_names(self, value): - value = list(islice(value, len(self))) - if not len(set(value)) == len(self): - raise ValueError( - "Got %i unique array names for %i data objects!" - % (len(set(value)), len(self)) - ) - elif not self.is_main and set(value) & ( - set(self.main.arr_names) - set(self.arr_names) - ): - raise ValueError( - "Cannot rename arrays because there are duplicates with the " - "main project: %s" - % ( - set(value) - & (set(self.main.arr_names) - set(self.arr_names)), - ) - ) - for arr, n in zip(self, value): - arr.psy.arr_name = n - if self.main is gcp(True): - for arr in self: - arr.psy.onupdate.emit() - - @property - def plotters(self): - """A list of all the plotters in this instance""" - return [arr.psy.plotter for arr in self.with_plotter] - - @property - def datasets(self): - """A mapping from dataset numbers to datasets in this list""" - return { - key: val["ds"] - for key, val in six.iteritems( - self._get_ds_descriptions( - self.array_info(ds_description=["ds"]) - ) - ) - } - - @property - def dsnames_map(self): - """A dictionary from the dataset numbers in this list to their - filenames""" - return { - key: val["fname"] - for key, val in six.iteritems( - self._get_ds_descriptions( - self.array_info(ds_description=["num", "fname"]), - ds_description={"fname"}, - ) - ) - } - - @property - def dsnames(self): - """The set of dataset names in this instance""" - return {t[0] for t in self._get_dsnames(self.array_info()) if t[0]} - - @docstrings.get_sections(base="Project") - @docstrings.dedent - def __init__(self, *args, **kwargs): - """ - Parameters - ---------- - %(ArrayList.parameters)s - main: Project - The main project this subproject belongs to (or None if this - project is the main project) - num: int - The number of the project - """ - self.main = kwargs.pop("main", None) - self._plot = ProjectPlotter(self) - self.num = kwargs.pop("num", 1) - self._ds_counter = count() - with self.block_signals: - super(Project, self).__init__(*args, **kwargs) - - @classmethod - @docstrings.get_sections(base="Project._register_plotter") - @dedent - def _register_plotter( - cls, identifier, module, plotter_name, plotter_cls=None - ): - """ - Register a plotter in the :class:`Project` class to easy access it - - Parameters - ---------- - identifier: str - Name of the attribute that is used to filter for the instances - belonging to this plotter - module: str - The module from where to import the `plotter_name` - plotter_name: str - The name of the plotter class in `module` - plotter_cls: type - The imported class of `plotter_name`. If None, it will be imported - when it is needed - """ - if plotter_cls is not None: # plotter has already been imported - - def get_x(self): - return self(plotter_cls) - - else: - - def get_x(self): - return self(getattr(import_module(module), plotter_name)) - - setattr( - cls, - identifier, - property( - get_x, - doc=( - "List of data arrays that are plotted by :class:`%s.%s`" - " plotters" - ) - % (module, plotter_name), - ), - ) - cls._registered_plotters[identifier] = (module, plotter_name) - - def disable(self): - """Disables the plotters in this list""" - for arr in self: - if arr.psy.plotter: - arr.psy.plotter.disabled = True - - def enable(self): - for arr in self: - if arr.psy.plotter: - arr.psy.plotter.disabled = False - - def __call__(self, *args, **kwargs): - ret = super(Project, self).__call__(*args, **kwargs) - ret.main = self.main - return ret - - def __enter__(self): - return self - - def __exit__(self, exc_type, exc_val, exc_tb): - self.close(True, True, True) - - @staticmethod - @docstrings.get_sections( - base="Project._load_preset", sections=["Parameters", "Notes"] - ) - def _load_preset(preset: str): - """Load a preset from disk - - Parameters - ---------- - preset: str or dict - The filename or identifier of a preset. If the given `preset` is - the path to an existing yaml file, it will be loaded. Otherwise we - look up the `preset` in the psyplot configuration directory (see - :func:`~psyplot.config.rcsetup.get_configdir`). - If a dictionary is provided, we assume that this is the preset - - Returns - ------- - dict - The loaded preset - - Notes - ----- - An identifier is the filename without extension. If you want to list - the available presets, run ``psyplot -lp`` from the command-line""" - if isinstance(preset, dict): - config = preset - else: - path = Project._resolve_preset_path(preset) - if path in rcParams["presets.trusted"]: - loader = yaml.Loader - else: - loader = yaml.SafeLoader - with open(path) as f: - try: - config = yaml.load(f, loader) - except yaml.constructor.ConstructorError as e: - e.note = (e.note or "") + ( - " You might want to add it to the trusted presets " - 'via\n\npsy.rcParams["presets.trusted"].append("{}")\n\n' - "and run this method again. To permanently store " - "this preset, edit the file at\n\n{} " - ).format(path, psyplot_fname()) - raise - - return config - - @staticmethod - def _resolve_preset_path(preset, if_exists=True): - if osp.exists(preset): - return preset - else: - confdir = get_configdir() - presets_dir = osp.join(confdir, "presets") - if osp.exists(osp.join(presets_dir, preset)): - return osp.join(presets_dir, preset) - elif osp.exists(osp.join(presets_dir, preset + ".yml")): - return osp.join(presets_dir, preset + ".yml") - else: - if if_exists: - raise ValueError( - f"Could not find a preset with name {preset}" - ) - else: - if not preset.endswith(".yml"): - return osp.join(presets_dir, preset + ".yml") - return preset - - @docstrings.dedent - def load_preset(self, preset: str, **kwargs): - """Load a preset from disk and apply it to the open project. - - This method loads a preset and updates the corresponding plots - - Parameters - ---------- - %(Project._load_preset.parameters)s - ``**kwargs`` - Any other parameter that shall be passed to the - :meth:`~psyplot.data.ArrayList.update` method - - Notes - ----- - %(Project._load_preset.notes)s - """ - config = self._load_preset(preset) - plotmethods = self.plot._plot_methods - pm_config, defaults = utils.sort_kwargs(config, plotmethods) - with self.no_auto_update: - for pm in plotmethods: - method = getattr(self.plot, pm) - if method.is_imported: - sp = getattr(self, pm) - if sp: - valid = list(method.plotter_cls._get_formatoptions()) - fmts = { - key: val - for key, val in defaults.items() - if key in valid - } - fmts.update(pm_config.get(pm, {})) - sp.update(fmt=fmts, **kwargs) - self.start_update() - - @staticmethod - def extract_fmts_from_preset(preset: str, plotmethod: str): - """Extract the formatoptions for a plotmethod from a given preset - - This method takes the preset and extracts the formatoptions valid for - the given plotmethod - - Parameters - ---------- - %(Project._load_preset.parameters)s - plotmethod: str - The plotmethod to use""" - preset = Project._load_preset(preset) - try: - plotmethod._method - except AttributeError: - method = getattr(plot, plotmethod) - else: - method = plotmethod - plotmethod = method._method - - plotmethods = plot._plot_methods - pm_config, defaults = utils.sort_kwargs(preset, plotmethods) - valid = list(method.plotter_cls._get_formatoptions()) - fmts = {key: val for key, val in defaults.items() if key in valid} - fmts.update(pm_config.get(plotmethod, {})) - return fmts - - def save_preset(self, fname=None, include_defaults=False, update=False): - """Save the formatoptions of this project as a preset - - This method takes the formatoptions in the plotters of this project and - saves it as a preset file""" - - def include(fmto, plotters): - key = fmto.key - for plotter in plotters: - if fmto.diff(plotter[key]): - return False - return True if include_defaults else fmto.changed - - if update: - with open(fname) as f: - preset = yaml.load(f, yaml.Loader) - else: - preset = {} - plotters = self.plotters - - for fmto in self._fmtos: - if include(fmto, plotters): - preset[fmto.key] = fmto.value - - for pm in self.plot._plot_methods: - method = getattr(self.plot, pm) - if method.is_imported: - sp = getattr(self, pm) - plotters = sp.plotters - for fmto in sp._fmtos: - if fmto.key not in preset and include(fmto, plotters): - preset.setdefault(pm, {}) - preset[pm][fmto.key] = fmto.value - if fname is not None: - fname = self._resolve_preset_path(fname, False) - os.makedirs(osp.dirname(fname), exist_ok=True) - with open(fname, "w") as f: - yaml.dump(preset, f) - else: - return preset - - @_first_main - def extend(self, *args, **kwargs): - len0 = len(self) - ret = super(Project, self).extend(*args, **kwargs) - if self._main is None: - for arr in self: - if arr.psy.plotter is not None: - arr.psy.plotter._project = self - if len(self) > len0 and (self.is_csp or self.is_cmp): - self.oncpchange.emit(self) - return ret - - extend.__doc__ = ArrayList.extend.__doc__ - - @_first_main - def append(self, *args, **kwargs): - len0 = len(self) - ret = super(Project, self).append(*args, **kwargs) - if self._main is None: - for arr in self: - if arr.psy.plotter is not None: - arr.psy.plotter._project = self - if len(self) > len0 and (self.is_csp or self.is_cmp): - self.oncpchange.emit(self) - return ret - - append.__doc__ = ArrayList.append.__doc__ - - __call__.__doc__ = ArrayList.__call__.__doc__ - - @docstrings.get_sections(base="Project.close") - @dedent - def close(self, figs=True, data=False, ds=False, remove_only=False): - """ - Close this project instance - - Parameters - ---------- - figs: bool - Close the figures - data: bool - delete the arrays from the (main) project - ds: bool - If True, close the dataset as well - remove_only: bool - If True and `figs` is True, the figures are not closed but the - plotters are removed""" - import matplotlib.pyplot as plt - - close_ds = ds - for arr in self[:]: - if figs and arr.psy.plotter is not None: - if remove_only: - for fmto in arr.psy.plotter._fmtos: - try: - fmto.remove() - except Exception: - pass - else: - plt.close(arr.psy.plotter.ax.get_figure().number) - arr.psy.plotter = None - if data: - self.remove(arr) - if not self.is_main: - try: - self.main.remove(arr) - except ValueError: # arr not in list - pass - if close_ds: - if isinstance(arr, InteractiveList): - for ds in [ - val["ds"] - for val in six.itervalues( - arr._get_ds_descriptions( - arr.array_info( - ds_description=["ds"], - standardize_dims=False, - ) - ) - ) - ]: - ds.close() - else: - arr.psy.base.close() - if self.is_main and self is gcp(True) and data: - scp(None) - elif self.is_main and self.is_cmp: - self.oncpchange.emit(self) - elif self.main.is_cmp: - self.oncpchange.emit(self.main) - - docstrings.keep_params("multiple_subplots.parameters", "delete") - docstrings.delete_params("ArrayList.from_dataset.parameters", "base") - docstrings.delete_kwargs( - "ArrayList.from_dataset.other_parameters", kwargs="kwargs" - ) - docstrings.keep_params("xarray.open_mfdataset.parameters", "concat_dim") - docstrings.keep_params("Project._load_preset.parameters", "preset") - - @_only_main - @docstrings.get_sections( - base="Project._add_data", - sections=["Parameters", "Other Parameters", "Returns"], - ) - @docstrings.dedent - def _add_data( - self, - plotter_cls, - filename_or_obj, - fmt={}, - make_plot=True, - draw=False, - mf_mode=False, - ax=None, - engine=None, - delete=True, - share=False, - clear=False, - enable_post=None, - concat_dim=_concat_dim_default, - load=False, - *args, - **kwargs, - ): - """ - Extract data from a dataset and visualize it with the given plotter - - Parameters - ---------- - plotter_cls: type - The subclass of :class:`psyplot.plotter.Plotter` to use for - visualization - filename_or_obj: filename, :class:`xarray.Dataset` or data store - The object (or file name) to open. If not a dataset, the - :func:`psyplot.data.open_dataset` will be used to open a dataset - fmt: dict - Formatoptions that shall be when initializing the plot (you can - however also specify them as extra keyword arguments) - make_plot: bool - If True, the data is plotted at the end. Otherwise you have to - call the :meth:`psyplot.plotter.Plotter.initialize_plot` method or - the :meth:`psyplot.plotter.Plotter.reinit` method by yourself - %(InteractiveBase.start_update.parameters.draw)s - mf_mode: bool - If True, the :func:`psyplot.open_mfdataset` method is used. - Otherwise we use the :func:`psyplot.open_dataset` method which can - open only one single dataset - ax: None, tuple (x, y[, z]) or (list of) matplotlib.axes.Axes - Specifies the subplots on which to plot the new data objects. - - - If None, a new figure will be created for each created plotter - - If tuple (x, y[, z]), `x` specifies the number of rows, `y` the - number of columns and the optional third parameter `z` the - maximal number of subplots per figure. - - If :class:`matplotlib.axes.Axes` (or list of those, e.g. created - by the :func:`matplotlib.pyplot.subplots` function), the data - will be plotted on these subplots - %(open_dataset.parameters.engine)s - %(multiple_subplots.parameters.delete)s - share: bool, fmt key or list of fmt keys - Determines whether the first created plotter shares it's - formatoptions with the others. If True, all formatoptions are - shared. Strings or list of strings specify the keys to share. - clear: bool - If True, axes are cleared before making the plot. This is only - necessary if the `ax` keyword consists of subplots with projection - that differs from the one that is needed - enable_post: bool - If True, the :attr:`~psyplot.plotter.Plotter.post` formatoption is - enabled and post processing scripts are allowed. If ``None``, this - parameter is set to True if there is a value given for the `post` - formatoption in `fmt` or `kwargs` - %(xarray.open_mfdataset.parameters.concat_dim)s - This parameter only does have an effect if `mf_mode` is True. - load: bool - If True, load the complete dataset into memory before plotting. - This might be useful if the data of other variables in the dataset - has to be accessed multiple times, e.g. for unstructured grids. - %(ArrayList.from_dataset.parameters.no_base)s - - Other Parameters - ---------------- - %(ArrayList.from_dataset.other_parameters.no_args_kwargs)s - ``**kwargs`` - Any other dimension or formatoption that shall be passed to `dims` - or `fmt` respectively. - - Returns - ------- - Project - The subproject that contains the new (visualized) data array""" - if not isinstance(filename_or_obj, xarray.Dataset): - if mf_mode: - filename_or_obj = open_mfdataset( - filename_or_obj, engine=engine, concat_dim=concat_dim - ) - else: - filename_or_obj = open_dataset(filename_or_obj, engine=engine) - if load: - old = filename_or_obj - filename_or_obj = filename_or_obj.load() - old.close() - - fmt = dict(fmt) - possible_fmts = list(plotter_cls._get_formatoptions()) - additional_fmt, kwargs = utils.sort_kwargs(kwargs, possible_fmts) - fmt.update(additional_fmt) - if enable_post is None: - enable_post = bool(fmt.get("post")) - # create the subproject - sub_project = self.from_dataset(filename_or_obj, **kwargs) - sub_project.main = self - sub_project.no_auto_update = not ( - not sub_project.no_auto_update or not self.no_auto_update - ) - # create the subplots - proj = plotter_cls._get_sample_projection() - if isinstance(ax, tuple): - axes = iter( - multiple_subplots( - *ax, n=len(sub_project), subplot_kw={"projection": proj} - ) - ) - elif ax is None or isinstance( - ax, (mpl.axes.SubplotBase, mpl.axes.Axes) - ): - axes = repeat(ax) - else: - axes = iter(ax) - clear = clear or (isinstance(ax, tuple) and proj is not None) - - for arr in sub_project: - plotter_cls( - arr, - make_plot=(not bool(share) and make_plot), - draw=False, - ax=next(axes), - clear=clear, - project=self, - enable_post=enable_post, - **fmt, - ) - if share: - if share is True: - share = possible_fmts - elif isinstance(share, six.string_types): - share = [share] - else: - share = list(share) - sub_project[0].psy.plotter.share( - [arr.psy.plotter for arr in sub_project[1:]], - keys=share, - draw=False, - ) - if make_plot: - for arr in sub_project: - arr.psy.plotter.reinit(draw=False, clear=clear) - if draw is None: - draw = rcParams["auto_draw"] - if draw: - sub_project.draw() - if rcParams["auto_show"]: - self.show() - self.extend(sub_project, new_name=True) - if self is gcp(True): - scp(sub_project) - return sub_project - - def __getitem__(self, key): - """Overwrites lists __getitem__ by returning subproject if `key` is a - slice""" - if isinstance(key, slice): # return a new project - ret = self.__class__(super(Project, self).__getitem__(key)) - ret.main = self.main - else: # return the item - ret = super(Project, self).__getitem__(key) - return ret - - if six.PY2: # for compatibility to python 2.7 - - def __getslice__(self, *args): - return self[slice(*args)] - - def __add__(self, other): - # overwritte to return a subproject - ret = self.__class__(super(Project, self).__add__(other)) - ret.main = self.main - return ret - - @staticmethod - def show(): - """Shows all open figures""" - import matplotlib.pyplot as plt - - plt.show(block=False) - - docstrings.keep_params("join_dicts.parameters", "delimiter") - docstrings.keep_params("join_dicts.parameters", "keep_all") - - @docstrings.get_sections(base="Project.joined_attrs") - @docstrings.with_indent(8) - def joined_attrs( - self, delimiter=", ", enhanced=True, plot_data=False, keep_all=True - ): - """Join the attributes of the arrays in this project - - Parameters - ---------- - %(join_dicts.parameters.delimiter)s - enhanced: bool - If True, the :meth:`psyplot.plotter.Plotter.get_enhanced_attrs` - method is used, otherwise the :attr:`xarray.DataArray.attrs` - attribute is used. - plot_data: bool - It True, use the :attr:`psyplot.plotter.Plotter.plot_data` - attribute of the plotters rather than the raw data in this project - %(join_dicts.parameters.keep_all)s - - Returns - ------- - dict - A mapping from the attribute to the joined attributes which are - either strings or (if there is only one attribute value), the - data type of the corresponding value""" - if enhanced: - all_attrs = [ - plotter.get_enhanced_attrs( - getattr(plotter, "plot_data" if plot_data else "data") - ) - for plotter in self.plotters - ] - else: - if plot_data: - all_attrs = [ - plotter.plot_data.attrs for plotter in self.plotters - ] - else: - all_attrs = [arr.attrs for arr in self] - return utils.join_dicts( - all_attrs, delimiter=delimiter, keep_all=keep_all - ) - - @docstrings.get_sections(base="Project.format_string") - @docstrings.with_indent(8) - def format_string( - self, s, use_time=False, format_args=None, *args, **kwargs - ): - """Format a string with the attributes in this project - - Parameters - ---------- - s: str - The string that is subject to be formatted - use_time: bool - If True, formatting strings for the - :meth:`datetime.datetime.strftime` are expected to be found in - `output` (e.g. ``'%%m'``, ``'%%Y'``, etc.). If so, other formatting - strings must be escaped by double ``'%%'`` (e.g. ``'%%%i'`` - instead of (``'%%i'``)) - format_args: tuple - A tuple of arguments that shall be inserted in `s` via - ``s %% format_args``. (There will be no error, when this fails!) - %(Project.joined_attrs.parameters)s - - Returns - ------- - str - The formatted string `s` - """ - attrs = self.joined_attrs(*args, **kwargs) - if use_time: - tnames = self._get_tnames() - tname = next(iter(tnames)) if len(tnames) == 1 else None - - time = attrs[tname] - try: # assume a valid datetime.datetime instance - s = pd.to_datetime(time).strftime(s) - except ValueError: - pass - if format_args is not None: - try: - s = safe_modulo(s, format_args, print_warning=False) - except TypeError: - pass - return safe_modulo(s, attrs) - - docstrings.keep_params("Project.format_string.parameters", "use_time") - - @docstrings.with_indent(8) - def export( - self, - output, - tight=False, - concat=True, - close_pdf=None, - use_time=False, - **kwargs, - ): - """Exports the figures of the project to one or more image files - - Parameters - ---------- - output: str, iterable or matplotlib.backends.backend_pdf.PdfPages - if string or list of strings, those define the names of the output - files. Otherwise you may provide an instance of - :class:`matplotlib.backends.backend_pdf.PdfPages` to save the - figures in it. - If string (or iterable of strings), attribute names in the - xarray.DataArray.attrs attribute as well as index dimensions - are replaced by the respective value (see examples below). - Furthermore a single format string without key (e.g. %%i, %%s, %%d, - etc.) is replaced by a counter. - tight: bool - If True, it is tried to figure out the tight bbox of the figure - (same as bbox_inches='tight') - concat: bool - if True and the output format is `pdf`, all figures are - concatenated into one single pdf - close_pdf: bool or None - If True and the figures are concatenated into one single pdf, - the resulting pdf instance is closed. If False it remains open. - If None and `output` is a string, it is the same as - ``close_pdf=True``, if None and `output` is neither a string nor an - iterable, it is the same as ``close_pdf=False`` - %(Project.format_string.parameters.use_time)s - ``**kwargs`` - Any valid keyword for the :func:`matplotlib.pyplot.savefig` - function - - Returns - ------- - matplotlib.backends.backend_pdf.PdfPages or None - a PdfPages instance if output is a string and close_pdf is False, - otherwise None - - Examples - -------- - Simply save all figures into one single pdf:: - - >>> p = psy.gcp() - >>> p.export('my_plots.pdf') - - Save all figures into separate pngs with increasing numbers (e.g. - ``'my_plots_1.png'``):: - - >>> p.export('my_plots_%%i.png') - - Save all figures into separate pngs with the name of the variables - shown in each figure (e.g. ``'my_plots_t2m.png'``):: - - >>> p.export('my_plots_%%(name)s.png') - - Save all figures into separate pngs with the name of the variables - shown in each figure and with increasing numbers (e.g. - ``'my_plots_1_t2m.png'``):: - - >>> p.export('my_plots_%%i_%%(name)s.png') - - Specify the names for each figure directly via a list:: - - >>> p.export(['my_plots1.pdf', 'my_plots2.pdf']) - """ - from matplotlib.backends.backend_pdf import PdfPages - - if tight: - kwargs["bbox_inches"] = "tight" - - not_enough_files_warnings = ( - "Not enough output files specified! %i figures are open " - "but only %i filenames have been given! This will cause " - "that some figures may be overwritten after being " - "exported! Use a pdf instead if you want to save all " - "figures or include a '%%i' string in the filename to " - "avoid duplicates." - ) - - if isinstance(output, six.string_types): # a single string - out_fmt = kwargs.pop("format", os.path.splitext(output))[1][1:] - if out_fmt.lower() == "pdf" and concat: - output = self.format_string(output, use_time, delimiter="-") - pdf = PdfPages(output) - - for fig in self.figs: - pdf.savefig(fig, **kwargs) - if close_pdf is None or close_pdf: - pdf.close() - return - else: - return pdf - else: - output = [output] * len(self.figs) - - if utils.is_iterable(output): # a list of strings - output = [ - sp.format_string(out, use_time, i, delimiter="-") - for i, (out, sp) in enumerate( - zip(output, self.figs.values()), 1 - ) - ] - if len(set(output)) != len(output): - warn(not_enough_files_warnings % (len(output), len(self.figs))) - output = iter(output) - - for fig, out in zip(self.figs, output): - fig.savefig(out, **kwargs) - else: # an instances of matplotlib.backends.backend_pdf.PdfPages - for fig in self.figs: - output.savefig(fig, **kwargs) - if close_pdf: - output.close() - - docstrings.keep_params("Plotter.share.parameters", "keys") - docstrings.delete_params("Plotter.share.parameters", "keys", "plotters") - - @docstrings.dedent - def share(self, base=None, keys=None, by=None, **kwargs): - """ - Share the formatoptions of one plotter with all the others - - This method shares specified formatoptions from `base` with all the - plotters in this instance. - - Parameters - ---------- - base: None, Plotter, xarray.DataArray, InteractiveList, or list of them - The source of the plotter that shares its formatoptions with the - others. It can be None (then the first instance in this project - is used), a :class:`~psyplot.plotter.Plotter` or any data object - with a *psy* attribute. If `by` is not None, then it is expected - that `base` is a list of data objects for each figure/axes - %(Plotter.share.parameters.keys)s - by: {'fig', 'figure', 'ax', 'axes'} - Share the formatoptions only with the others on the same - ``'figure'`` or the same ``'axes'``. In this case, base must either - be ``None`` or a list of the types specified for `base` - %(Plotter.share.parameters.no_keys|plotters)s - - See Also - -------- - psyplot.plotter.share""" - if by is not None: - if base is not None: - if hasattr(base, "psy") or isinstance(base, Plotter): - base = [base] - if by.lower() in ["ax", "axes"]: - bases = { - ax: p[0] for ax, p in six.iteritems(Project(base).axes) - } - elif by.lower() in ["fig", "figure"]: - bases = { - fig: p[0] - for fig, p in six.iteritems(Project(base).figs) - } - else: - raise ValueError( - "*by* must be out of {'fig', 'figure', 'ax', 'axes'}. " - "Not %s" % (by,) - ) - else: - bases = {} - projects = self.axes if by == "axes" else self.figs - for obj, p in projects.items(): - p.share(bases.get(obj), keys, **kwargs) - else: - plotters = self.plotters - if not plotters: - return - if base is None: - if len(plotters) == 1: - return - base = plotters[0] - plotters = plotters[1:] - elif not isinstance(base, Plotter): - base = getattr(getattr(base, "psy", base), "plotter", base) - base.share(plotters, keys=keys, **kwargs) - - @docstrings.dedent - def unshare(self, **kwargs): - """ - Unshare the formatoptions of all the plotters in this instance - - This method uses the :meth:`psyplot.plotter.Plotter.unshare_me` - method to release the specified formatoptions in `keys`. - - Parameters - ---------- - %(Plotter.unshare_me.parameters)s - - See Also - -------- - psyplot.plotter.Plotter.unshare, psyplot.plotter.Plotter.unshare_me""" - for plotter in self.plotters: - plotter.unshare_me(**kwargs) - - docstrings.delete_params("ArrayList.array_info.parameters", "pwd", "copy") - - @docstrings.get_sections(base="Project.save_project") - @docstrings.dedent - def save_project(self, fname=None, pwd=None, pack=False, **kwargs): - """ - Save this project to a file - - Parameters - ---------- - fname: str or None - If None, the dictionary will be returned. Otherwise the necessary - information to load this project via the :meth:`load` method is - saved to `fname` using the :mod:`pickle` module - pwd: str or None, optional - Path to the working directory from where the data can be imported. - If None and `fname` is the path to a file, `pwd` is set to the - directory of this file. Otherwise the current working directory is - used. - pack: bool - If True, all datasets are packed into the folder of `fname` - and will be used if the data is loaded - %(ArrayList.array_info.parameters.no_pwd|copy)s - - Notes - ----- - You can also store the entire data in the pickled file by setting - ``ds_description={'ds'}``""" - # store the figure informatoptions and array informations - if fname is not None and pwd is None and not pack: - pwd = os.path.dirname(fname) - if pack and fname is not None: - target_dir = os.path.dirname(fname) - if not os.path.exists(target_dir): - os.makedirs(target_dir, exist_ok=True) - - def tmp_it(): - from tempfile import NamedTemporaryFile - - while True: - yield NamedTemporaryFile(dir=target_dir, suffix=".nc").name - - kwargs.setdefault("paths", tmp_it()) - if fname is not None: - kwargs["copy"] = True - - _update_versions() - ret = { - "figs": dict(map(_ProjectLoader.inspect_figure, self.figs)), - "arrays": self.array_info(pwd=pwd, **kwargs), - "versions": _deepcopy(_versions), - } - if pack and fname is not None: - # we get the filenames out of the results and copy the datasets - # there. After that we check the filenames again and force them - # to the desired directory - from shutil import copyfile - - fnames = (f[0] for f in self._get_dsnames(ret["arrays"])) - alternative_paths = kwargs.pop("alternative_paths", {}) - counters = defaultdict(int) - if kwargs.get("use_rel_paths", True): - get_path = partial(os.path.relpath, start=target_dir) - else: - get_path = os.path.abspath - for ds_fname in unique_everseen(chain(alternative_paths, fnames)): - if ds_fname is None or utils.is_remote_url(ds_fname): - continue - dst_file = alternative_paths.get( - ds_fname, - os.path.join(target_dir, os.path.basename(ds_fname)), - ) - orig_dst_file = dst_file - if counters[dst_file] and ( - not os.path.exists(dst_file) - or not os.path.samefile(ds_fname, dst_file) - ): - dst_file, ext = os.path.splitext(dst_file) - dst_file += "-" + str(counters[orig_dst_file]) + ext - if not os.path.exists(dst_file) or not os.path.samefile( - ds_fname, dst_file - ): - copyfile(ds_fname, dst_file) - counters[orig_dst_file] += 1 - alternative_paths.setdefault(ds_fname, get_path(dst_file)) - ret["arrays"] = self.array_info( - pwd=pwd, alternative_paths=alternative_paths, **kwargs - ) - # store the plotter settings - for arr, d in zip(self, six.itervalues(ret["arrays"])): - if arr.psy.plotter is None: - continue - plotter = arr.psy.plotter - d["plotter"] = { - "ax": _ProjectLoader.inspect_axes(plotter.ax), - "fmt": { - key: getattr(plotter, key).value2pickle for key in plotter - }, - "cls": ( - plotter.__class__.__module__, - plotter.__class__.__name__, - ), - "shared": {}, - } - d["plotter"]["ax"]["shared"] = set( - other.psy.arr_name - for other in self - if other.psy.ax == plotter.ax - ) - if plotter.ax._sharex: - d["plotter"]["ax"]["sharex"] = next( - ( - other.psy.arr_name - for other in self - if other.psy.ax == plotter.ax._sharex - ), - None, - ) - if plotter.ax._sharey: - d["plotter"]["ax"]["sharey"] = next( - ( - other.psy.arr_name - for other in self - if other.psy.ax == plotter.ax._sharey - ), - None, - ) - shared = d["plotter"]["shared"] - for fmto in plotter._fmtos: - if fmto.shared: - shared[fmto.key] = [ - other_fmto.plotter.data.psy.arr_name - for other_fmto in fmto.shared - ] - if fname is not None: - with open(fname, "wb") as f: - pickle.dump(ret, f) - return None - - return ret - - @docstrings.dedent - def keys(self, *args, **kwargs): - """ - Show the available formatoptions in this project - - Parameters - ---------- - %(Plotter.show_keys.parameters)s - - Other Parameters - ---------------- - %(Plotter.show_keys.other_parameters)s - - Returns - ------- - %(Plotter.show_keys.returns)s""" - - class TmpClass(Plotter): - pass - - for fmto in self._fmtos: - setattr(TmpClass, fmto.key, type(fmto)(fmto.key)) - return TmpClass.show_keys(*args, **kwargs) - - @docstrings.dedent - def summaries(self, *args, **kwargs): - """ - Show the available formatoptions and their summaries in this project - - Parameters - ---------- - %(Plotter.show_keys.parameters)s - - Other Parameters - ---------------- - %(Plotter.show_keys.other_parameters)s - - Returns - ------- - %(Plotter.show_keys.returns)s""" - - class TmpClass(Plotter): - pass - - for fmto in self._fmtos: - setattr(TmpClass, fmto.key, type(fmto)(fmto.key)) - return TmpClass.show_summaries(*args, **kwargs) - - @docstrings.dedent - def docs(self, *args, **kwargs): - """ - Show the available formatoptions in this project and their full docu - - Parameters - ---------- - %(Plotter.show_keys.parameters)s - - Other Parameters - ---------------- - %(Plotter.show_keys.other_parameters)s - - Returns - ------- - %(Plotter.show_keys.returns)s""" - - class TmpClass(Plotter): - pass - - for fmto in self._fmtos: - setattr(TmpClass, fmto.key, type(fmto)(fmto.key)) - return TmpClass.show_docs(*args, **kwargs) - - @classmethod - @docstrings.with_indent(8) - def from_dataset(cls, *args, **kwargs): - """Construct an ArrayList instance from an existing base dataset - - Parameters - ---------- - %(ArrayList.from_dataset.parameters)s - main: Project - The main project that this project corresponds to - - Other Parameters - ---------------- - %(ArrayList.from_dataset.other_parameters)s - - Returns - ------- - Project - The newly created project instance - """ - main = kwargs.pop("main", None) - ret = super(Project, cls).from_dataset(*args, **kwargs) - if main is not None: - ret.main = main - main.extend(ret, new_name=False) - return ret - - docstrings.delete_params("ArrayList.from_dict.parameters", "d", "pwd") - docstrings.keep_params("Project._add_data.parameters", "make_plot") - docstrings.keep_params("Project._add_data.parameters", "clear") - - @classmethod - @docstrings.get_sections(base="Project.load_project") - @docstrings.dedent - def load_project( - cls, - fname, - auto_update=None, - make_plot=True, - draw=False, - alternative_axes=None, - main=False, - encoding=None, - enable_post=False, - new_fig=True, - clear=None, - **kwargs, - ): - """ - Load a project from a file or dict - - This classmethod allows to load a project that has been stored using - the :meth:`save_project` method and reads all the data and creates the - figures. - - Since the data is stored in external files when saving a project, - make sure that the data is accessible under the relative paths - as stored in the file `fname` or from the current working directory - if `fname` is a dictionary. Alternatively use the `alternative_paths` - parameter or the `pwd` parameter - - Parameters - ---------- - fname: str or dict - The string might be the path to a file created with the - :meth:`save_project` method, or it might be a dictionary from this - method - %(InteractiveBase.parameters.auto_update)s - %(Project._add_data.parameters.make_plot)s - %(InteractiveBase.start_update.parameters.draw)s - alternative_axes: dict, None or list - alternative axes instances to use - - - If it is None, the axes and figures from the saving point will be - reproduced. - - a dictionary should map from array names in the created - project to matplotlib axes instances - - a list should contain axes instances that will be used for - iteration - main: bool, optional - If True, a new main project is created and returned. - Otherwise (by default default) the data is added to the current - main project. - encoding: str - The encoding to use for loading the project. If None, it is - automatically determined by pickle. Note: Set this to ``'latin1'`` - if using a project created with python2 on python3. - enable_post: bool - If True, the :attr:`~psyplot.plotter.Plotter.post` formatoption is - enabled and post processing scripts are allowed. Do only set this - parameter to ``True`` if you know you can trust the information in - `fname` - new_fig: bool - If True (default) and `alternative_axes` is None, new figures are - created if the figure already exists - %(Project._add_data.parameters.clear)s - pwd: str or None, optional - Path to the working directory from where the data can be imported. - If None and `fname` is the path to a file, `pwd` is set to the - directory of this file. Otherwise the current working directory is - used. - %(ArrayList.from_dict.parameters.no_d|pwd)s - - Other Parameters - ---------------- - %(ArrayList.from_dict.parameters)s - - Returns - ------- - Project - The project in state of the saving point""" - from pkg_resources import iter_entry_points - - def get_ax_base(name, alternatives): - ax_base = next(iter(obj(arr_name=name).axes), None) - if ax_base is None: - ax_base = next(iter(obj(arr_name=alternatives).axes), None) - if ax_base is not None: - alternatives.difference_update(obj(ax=ax_base).arr_names) - return ax_base - - pwd = kwargs.pop("pwd", None) - if isinstance(fname, six.string_types): - with open(fname, "rb") as f: - pickle_kws = {} if not encoding else {"encoding": encoding} - d = pickle.load(f, **pickle_kws) - pwd = pwd or os.path.dirname(fname) - else: - d = dict(fname) - pwd = pwd or os.getcwd() - # check for patches of plugins - for ep in iter_entry_points("psyplot", name="patches"): - patches = ep.load() - for arr_d in d.get("arrays").values(): - plotter_cls = arr_d.get("plotter", {}).get("cls") - if plotter_cls is not None and plotter_cls in patches: - # apply the patch - patches[plotter_cls]( - arr_d["plotter"], d.get("versions", {}) - ) - fig_map = {} - if alternative_axes is None: - for fig_dict in six.itervalues(d.get("figs", {})): - orig_num = fig_dict.get("num") or 1 - fig_map[orig_num] = _ProjectLoader.load_figure( - fig_dict, new_fig=new_fig - ).number - elif not isinstance(alternative_axes, dict): - alternative_axes = cycle(iter(alternative_axes)) - obj = cls.from_dict(d["arrays"], pwd=pwd, **kwargs) - if main: - # we create a new project with the project factory to make sure - # that everything is handled correctly - obj = project(None, obj) - axes = {} - arr_names = obj.arr_names - sharex = defaultdict(set) - sharey = defaultdict(set) - for arr, (arr_name, arr_dict) in zip( - obj, - filter(lambda t: t[0] in arr_names, six.iteritems(d["arrays"])), - ): - if not arr_dict.get("plotter"): - continue - plot_dict = arr_dict["plotter"] - plotter_cls = getattr( - import_module(plot_dict["cls"][0]), plot_dict["cls"][1] - ) - ax = None - if alternative_axes is not None: - if isinstance(alternative_axes, dict): - ax = alternative_axes.get(arr.arr_name) - else: - ax = next(alternative_axes, None) - if ax is None and "ax" in plot_dict: - already_opened = ( - plot_dict["ax"].get("shared", set()).intersection(axes) - ) - if already_opened: - ax = axes[next(iter(already_opened))] - else: - plot_dict["ax"].pop("shared", None) - plot_dict["ax"]["fig"] = fig_map[ - plot_dict["ax"].get("fig") or 1 - ] - if plot_dict["ax"].get("sharex"): - sharex[plot_dict["ax"].pop("sharex")].add( - arr.psy.arr_name - ) - if plot_dict["ax"].get("sharey"): - sharey[plot_dict["ax"].pop("sharey")].add( - arr.psy.arr_name - ) - axes[arr.psy.arr_name] = ax = _ProjectLoader.load_axes( - plot_dict["ax"] - ) - plotter_cls( - arr, - make_plot=False, - draw=False, - clear=False, - ax=ax, - project=obj.main, - enable_post=enable_post, - **plot_dict["fmt"], - ) - # handle shared x and y-axes - for key, names in sharex.items(): - ax_base = get_ax_base(key, names) - if ax_base is not None: - ax_base.get_shared_x_axes().join( - ax_base, *obj(arr_name=names).axes - ) - for ax in obj(arr_name=names).axes: - ax._sharex = ax_base - for key, names in sharey.items(): - ax_base = get_ax_base(key, names) - if ax_base is not None: - ax_base.get_shared_y_axes().join( - ax_base, *obj(arr_name=names).axes - ) - for ax in obj(arr_name=names).axes: - ax._sharey = ax_base - for arr in obj.with_plotter: - shared = d["arrays"][arr.psy.arr_name]["plotter"].get("shared", {}) - for key, arr_names in six.iteritems(shared): - arr.psy.plotter.share( - obj(arr_name=arr_names).plotters, keys=[key] - ) - if make_plot: - for plotter in obj.plotters: - plotter.reinit( - draw=False, - clear=clear - or ( - clear is None - and plotter_cls._get_sample_projection() is not None - ), - ) - if draw is None: - draw = rcParams["auto_draw"] - if draw: - obj.draw() - if rcParams["auto_show"]: - obj.show() - if auto_update is None: - auto_update = rcParams["lists.auto_update"] - if not main: - obj._main = gcp(True) - obj.main.extend(obj, new_name=True) - obj.no_auto_update = not auto_update - scp(obj) - return obj - - @classmethod - @docstrings.get_sections(base="Project.scp") - @dedent - def scp(cls, project): - """ - Set the current project - - Parameters - ---------- - project: Project or None - The project to set. If it is None, the current subproject is set - to empty. If it is a sub project (see:attr:`Project.is_main`), - the current subproject is set to this project. Otherwise it - replaces the current main project - - See Also - -------- - scp: The global version for setting the current project - gcp: Returns the current project - project: Creates a new project""" - if project is None: - _scp(None) - cls.oncpchange.emit(gcp()) - elif not project.is_main: - if project.main is not _current_project: - _scp(project.main, True) - cls.oncpchange.emit(project.main) - _scp(project) - cls.oncpchange.emit(project) - else: - _scp(project, True) - cls.oncpchange.emit(project) - sp = project[:] - _scp(sp) - cls.oncpchange.emit(sp) - - docstrings.delete_params("Project.parameters", "num") - - @classmethod - @docstrings.dedent - def new(cls, num=None, *args, **kwargs): - """ - Create a new main project - - Parameters - ---------- - num: int - The number of the project - %(Project.parameters.no_num)s - - Returns - ------- - Project - The with the given `num` (if it does not already exist, it is - created) - - See Also - -------- - scp: Sets the current project - gcp: Returns the current project - """ - project = cls(*args, num=num, **kwargs) - scp(project) - return project - - def __str__(self): - return (("%i Main " % self.num) if self.is_main else "") + super( - Project, self - ).__str__() - - -class _ProjectLoader(object): - """Class to inspect a project and reproduce it""" - - @staticmethod - def inspect_figure(fig): - """Get the parameters (heigth, width, etc.) to create a figure - - This method returns the number of the figure and a dictionary - containing the necessary information for the - :func:`matplotlib.pyplot.figure` function""" - return fig.number, { - "num": fig.number, - "figsize": (fig.get_figwidth(), fig.get_figheight()), - "dpi": fig.get_dpi() / getattr(fig.canvas, "_dpi_ratio", 1), - "facecolor": fig.get_facecolor(), - "edgecolor": fig.get_edgecolor(), - "frameon": fig.get_frameon(), - "tight_layout": fig.get_tight_layout(), - "subplotpars": vars(fig.subplotpars), - } - - @staticmethod - def load_figure(d, new_fig=True): - """Create a figure from what is returned by :meth:`inspect_figure`""" - import matplotlib.pyplot as plt - - subplotpars = d.pop("subplotpars", None) - if subplotpars is not None: - subplotpars.pop("validate", None) - subplotpars.pop("_validate", None) - subplotpars = mfig.SubplotParams(**subplotpars) - if new_fig: - nums = plt.get_fignums() - if d.get("num") in nums: - d["num"] = next( - i - for i in range(max(plt.get_fignums()) + 1, 0, -1) - if i not in nums - ) - return plt.figure(subplotpars=subplotpars, **d) - - @staticmethod - def inspect_axes(ax): - """Inspect an axes or subplot to get the initialization parameters""" - ret = {"fig": ax.get_figure().number} - if mpl.__version__ < "2.0": - ret["axisbg"] = ax.get_axis_bgcolor() - else: # axisbg is depreceated - ret["facecolor"] = ax.get_facecolor() - proj = getattr(ax, "projection", None) - if proj is not None and not isinstance(proj, six.string_types): - proj = (proj.__class__.__module__, proj.__class__.__name__) - ret["projection"] = proj - ret["visible"] = ax.get_visible() - ret["spines"] = {} - ret["zorder"] = ax.get_zorder() - ret["yaxis_inverted"] = ax.yaxis_inverted() - ret["xaxis_inverted"] = ax.xaxis_inverted() - for key, val in ax.spines.items(): - ret["spines"][key] = {} - for prop in [ - "linestyle", - "edgecolor", - "linewidth", - "facecolor", - "visible", - ]: - ret["spines"][key][prop] = getattr(val, "get_" + prop)() - if isinstance(ax, SubplotBase): - sp = ax.get_subplotspec().get_topmost_subplotspec() - ret["grid_spec"] = sp.get_geometry()[:2] - ret["subplotspec"] = [sp.num1, sp.num2] - ret["is_subplot"] = True - else: - ret["args"] = [ax.get_position(True).bounds] - ret["is_subplot"] = False - return ret - - @staticmethod - def load_axes(d): - """Create an axes or subplot from what is returned by - :meth:`inspect_axes`""" - import matplotlib.pyplot as plt - - fig = plt.figure(d.pop("fig", None)) - proj = d.pop("projection", None) - spines = d.pop("spines", None) - invert_yaxis = d.pop("yaxis_inverted", None) - invert_xaxis = d.pop("xaxis_inverted", None) - if mpl.__version__ >= "2.0" and "axisbg" in d: # axisbg is depreceated - d["facecolor"] = d.pop("axisbg") - elif mpl.__version__ < "2.0" and "facecolor" in d: - d["axisbg"] = d.pop("facecolor") - if proj is not None and not isinstance(proj, six.string_types): - proj = getattr(import_module(proj[0]), proj[1])() - if d.pop("is_subplot", None): - grid_spec = mpl.gridspec.GridSpec(*d.pop("grid_spec", (1, 1))) - subplotspec = mpl.gridspec.SubplotSpec( - grid_spec, *d.pop("subplotspec", (1, None)) - ) - return fig.add_subplot(subplotspec, projection=proj, **d) - ret = fig.add_axes(*d.pop("args", []), projection=proj, **d) - if spines is not None: - for key, val in spines.items(): - ret.spines[key].update(val) - if invert_xaxis: - if ret.get_xlim()[0] < ret.get_xlim()[1]: - ret.invert_xaxis() - if invert_yaxis: - if ret.get_ylim()[0] < ret.get_ylim()[1]: - ret.invert_yaxis() - return ret - - -class ProjectPlotter(object): - """Plotting methods of the :class:`psyplot.project.Project` class""" - - #: the base class for new plot methods. Is set below with the - #: :class:`PlotterInterface` class - _plot_method_base_cls = None - - @property - def project(self): - return self._project if self._project is not None else gcp(True) - - def __init__(self, project=None): - self._project = project - - docstrings.keep_params( - "ArrayList.from_dataset.parameters", "default_slice" - ) - - @property - def _plot_methods(self): - """A dictionary with mappings from plot method to their summary""" - ret = {} - for attr in filter(lambda s: not s.startswith("_"), dir(self)): - obj = getattr(self, attr) - if isinstance(obj, PlotterInterface): - ret[attr] = obj._summary - return ret - - def show_plot_methods(self): - """Print the plotmethods of this instance""" - print_func = PlotterInterface._print_func - if print_func is None: - print_func = six.print_ - s = "\n".join( - "%s\n %s" % t for t in six.iteritems(self._plot_methods) - ) - return print_func(s) - - @docstrings.get_sections( - base="ProjectPlotter._add_data", - sections=["Parameters", "Other Parameters", "Returns"], - ) - @docstrings.dedent - def _add_data(self, *args, **kwargs): - """ - Add new plots to the project - - Parameters - ---------- - %(Project._add_data.parameters)s - - Other Parameters - ---------------- - %(Project._add_data.other_parameters)s - - Returns - ------- - %(Project._add_data.returns)s - """ - # this method is just a shortcut to the :meth:`Project._add_data` - # method but is reimplemented by subclasses as the - # :class:`DatasetPlotter` or the :class:`DataArrayPlotter` - return self.project._add_data(*args, **kwargs) - - @classmethod - @docstrings.get_sections(base="ProjectPlotter._register_plotter") - @docstrings.dedent - def _register_plotter( - cls, - identifier, - module, - plotter_name, - plotter_cls=None, - summary="", - prefer_list=False, - default_slice=None, - default_dims={}, - show_examples=True, - example_call="filename, name=['my_variable'], ...", - plugin=None, - ): - """ - Register a plotter for making plots - - This class method registeres a plot function for the :class:`Project` - class under the name of the given `identifier` - - Parameters - ---------- - %(Project._register_plotter.parameters)s - - Other Parameters - ---------------- - prefer_list: bool - Determines the `prefer_list` parameter in the `from_dataset` - method. If True, the plotter is expected to work with instances of - :class:`psyplot.InteractiveList` instead of - :class:`psyplot.InteractiveArray`. - %(ArrayList.from_dataset.parameters.default_slice)s - default_dims: dict - Default dimensions that shall be used for plotting (e.g. - {'x': slice(None), 'y': slice(None)} for longitude-latitude plots) - show_examples: bool, optional - If True, examples how to access the plotter documentation are - included in class documentation - example_call: str, optional - The arguments and keyword arguments that shall be included in the - example of the generated plot method. This call will then appear as - ``>>> psy.plot.%%(identifier)s(%%(example_call)s)`` in the - documentation - plugin: str - The name of the plugin - """ - full_name = "%s.%s" % (module, plotter_name) - if plotter_cls is not None: # plotter has already been imported - docstrings.params[ - "%s.formatoptions" % (full_name) - ] = plotter_cls.show_keys( - indent=4, - func=str, - # include links in sphinx doc - include_links=None, - ) - doc_str = ( - "Possible formatoptions are\n\n" "%%(%s.formatoptions)s" - ) % full_name - else: - doc_str = "" - - summary = summary or ( - "Open and plot data via :class:`%s.%s` plotters" - % (module, plotter_name) - ) - - if plotter_cls is not None: - _versions.update(get_versions(key=lambda s: s == plugin)) - - class PlotMethod(cls._plot_method_base_cls): - __doc__ = cls._gen_doc( - summary, - full_name, - identifier, - example_call, - doc_str, - show_examples, - ) - - _default_slice = default_slice - _default_dims = default_dims - _plotter_cls = plotter_cls - _prefer_list = prefer_list - _plugin = plugin - - _summary = summary - - setattr(cls, identifier, PlotMethod(identifier, module, plotter_name)) - - @classmethod - def _gen_doc( - cls, - summary, - full_name, - identifier, - example_call, - doc_str, - show_examples, - ): - """Generate the documentation docstring for a PlotMethod""" - ret = docstrings.dedent( - """ - %s - - This plotting method adds data arrays and plots them via - :class:`%s` plotters - - To plot data from a netCDF file type:: - - >>> psy.plot.%s(%s) - - %s""" - % (summary, full_name, identifier, example_call, doc_str) - ) - - if show_examples: - ret += "\n\n" + cls._gen_examples(identifier) - return ret - - @classmethod - def _gen_examples(cls, identifier): - """Generate examples how to axes the formatoption docs""" - return docstrings.dedent( - """ - Examples - -------- - To explore the formatoptions and their documentations, use the - ``keys``, ``summaries`` and ``docs`` methods. For example:: - - >>> import psyplot.project as psy - - # show the keys corresponding to a group or multiple - # formatopions - >>> psy.plot.%(id)s.keys('labels') - - # show the summaries of a group of formatoptions or of a - # formatoption - >>> psy.plot.%(id)s.summaries('title') - - # show the full documentation - >>> psy.plot.%(id)s.docs('plot') - - # or access the documentation via the attribute - >>> psy.plot.%(id)s.plot""" - % {"id": identifier} - ) - - -class PlotterInterface(object): - """Base class for visualizing a data array from an predefined plotter - - See the :meth:`__call__` method for details on plotting.""" - - @property - def _logger(self): - name = "%s.%s.%s" % ( - self.__module__, - self.__class__.__name__, - self._method, - ) - return logging.getLogger(name) - - @property - def is_imported(self): - """True if the module for this plot method has been imported already""" - return self.module in sys.modules - - @property - def plotter_cls(self): - """The plotter class""" - ret = self._plotter_cls - if ret is None: - self._logger.debug("importing %s", self.module) - mod = import_module(self.module) - plotter = self.plotter_name - if plotter not in vars(mod): - raise ImportError( - "Module %r does not have a %r plotter!" % (mod, plotter) - ) - ret = self._plotter_cls = getattr(mod, plotter) - _versions.update(get_versions(key=lambda s: s == self._plugin)) - return ret - - _prefer_list = False - _default_slice = None - _default_dims = {} - - _print_func = None - - @property - def print_func(self): - """The function that is used to return a formatoption - - By default the :func:`print` function is used (i.e. it is printed to - the terminal)""" - return self._print_func or six.print_ - - @print_func.setter - def print_func(self, value): - self._print_func = value - - def __init__(self, methodname, module, plotter_name, project_plotter=None): - self._method = methodname - self._project_plotter = project_plotter - self.module = module - self.plotter_name = plotter_name - - docstrings.delete_params( - "ProjectPlotter._add_data.parameters", "plotter_cls" - ) - - @docstrings.dedent - def __call__(self, *args, **kwargs): - """ - Parameters - ---------- - %(ProjectPlotter._add_data.parameters.no_plotter_cls)s - %(Project._load_preset.parameters.preset)s - - Other Parameters - ---------------- - %(ProjectPlotter._add_data.other_parameters)s - - - Returns - ------- - %(ProjectPlotter._add_data.returns)s - """ - preset = kwargs.pop("preset", None) - if preset: - preset = self._project_plotter.project._load_preset(preset) - if len(args) >= 2: - fmt = args[1] - else: - fmt = kwargs.setdefault("fmt", {}) - for key, val in preset.get(self._method, {}).items(): - fmt.setdefault(key, val) - valid = list(self.plotter_cls._get_formatoptions()) - for key, val in preset.items(): - if key in valid: - fmt.setdefault(key, val) - - return self._project_plotter._add_data( - self.plotter_cls, - *args, - **dict( - chain( - [ - ("prefer_list", self._prefer_list), - ("default_slice", self._default_slice), - ], - six.iteritems(self._default_dims), - six.iteritems(kwargs), - ) - ), - ) - - def __getattr__(self, attr): - if attr in self.plotter_cls._get_formatoptions(): - return partial( - self.print_func, getattr(self.plotter_cls, attr).__doc__ - ) - else: - raise AttributeError( - "%s instance does not have a %s attribute" - % (self.__class__.__name__, attr) - ) - - def __get__(self, instance, owner): - if instance is None: - return self - else: - try: - return getattr(instance, "_" + self._method) - except AttributeError: - setattr( - instance, - "_" + self._method, - self.__class__( - self._method, self.module, self.plotter_name, instance - ), - ) - return getattr(instance, "_" + self._method) - - def __set__(self, instance, value): - """Actually not required. We just implement it to ensure the python - "help" function works well""" - setattr(instance, "_" + self._method, value) - - def __dir__(self): - try: - return sorted( - chain( - dir(self.__class__), - self.__dict__, - self.plotter_cls._get_formatoptions(), - ) - ) - except Exception: - return sorted(chain(dir(self.__class__), self.__dict__)) - - @docstrings.dedent - def keys(self, *args, **kwargs): - """ - Classmethod to return a nice looking table with the given formatoptions - - Parameters - ---------- - %(Plotter.show_keys.parameters)s - - Other Parameters - ---------------- - %(Plotter.show_keys.other_parameters)s - - Returns - ------- - %(Plotter.show_keys.returns)s - - See Also - -------- - summaries, docs""" - return self.plotter_cls.show_keys(*args, **kwargs) - - @docstrings.dedent - def summaries(self, *args, **kwargs): - """ - Method to print the summaries of the formatoptions - - Parameters - ---------- - %(Plotter.show_keys.parameters)s - - Other Parameters - ---------------- - %(Plotter.show_keys.other_parameters)s - - Returns - ------- - %(Plotter.show_keys.returns)s - - See Also - -------- - keys, docs""" - return self.plotter_cls.show_summaries(*args, **kwargs) - - @docstrings.dedent - def docs(self, *args, **kwargs): - """ - Method to print the full documentations of the formatoptions - - Parameters - ---------- - %(Plotter.show_keys.parameters)s - - Other Parameters - ---------------- - %(Plotter.show_keys.other_parameters)s - - Returns - ------- - %(Plotter.show_keys.returns)s - - See Also - -------- - keys, docs""" - return self.plotter_cls.show_docs(*args, **kwargs) - - @docstrings.dedent - def check_data(self, ds, name, dims, decoder=None, *args, **kwargs): - """ - A validation method for the data shape - - Parameters - ---------- - name: list of lists of strings - The variable names (see the - :meth:`~psyplot.plotter.Plotter.check_data` method of the - :attr:`plotter_cls` attribute for details) - dims: list of dictionaries - The dimensions of the arrays. It will be enhanced by the default - dimensions of this plot method - is_unstructured: bool or list of bool - True if the corresponding array is unstructured. - decoder: :class:`psyplot.data.CFDecoder`, dict or a list of them - The decoders to use per array. Dictionaries are parsed as keyword - arguments to the :meth:`psyplot.data.CFDecoder.get_decoder` - method - - Returns - ------- - %(Plotter.check_data.returns)s - """ - if isinstance(name, six.string_types): - name = [name] - dims = [dims] - decoders = [decoder] - else: - dims = list(dims) - decoders = list(decoder if decoder is not None else [None]) - variables = [ds[safe_list(n)[0]] for n in name] - if decoders is None: - decoders = [CFDecoder.get_decoder(ds, var) for var in variables] - else: - for i, (decoder, var) in enumerate(zip(decoders, variables)): - if decoder is None: - decoder = {} - if isinstance(decoder, dict): - decoders[i] = CFDecoder.get_decoder(ds, var, **decoder) - default_slice = ( - slice(None) if self._default_slice is None else self._default_slice - ) - for i, (dim_dict, var, decoder) in enumerate( - zip(dims, variables, decoders) - ): - corrected = decoder.correct_dims( - var, - dict( - chain(six.iteritems(self._default_dims), dim_dict.items()) - ), - ) - # now use the default slice (we don't do this before because the - # `correct_dims` method doesn't use 'x', 'y', 'z' and 't' (as used - # for the _default_dims) if the real dimension name is already in - # the dictionary) - for dim in var.dims: - corrected.setdefault(dim, default_slice) - dims[i] = [ - dim - for dim, val in map( - lambda t: (t[0], safe_list(t[1])), six.iteritems(corrected) - ) - if val and (len(val) > 1 or _is_slice(val[0])) - ] - return self.plotter_cls.check_data( - name, - dims, - [ - decoder.is_unstructured(var) - for decoder, var in zip(decoders, variables) - ], - ) - - -# set the base class for the :class:`ProjectPlotter` plot methods -ProjectPlotter._plot_method_base_cls = PlotterInterface - - -class DatasetPlotterInterface(PlotterInterface): - """Interface for the :class:`DatasetPlotter` to a plotter""" - - # there are not changes here compared to :class:`PlotterInterface`, except - # for a different docstring for the __call__ method - - docstrings.delete_params( - "ProjectPlotter._add_data.parameters", "plotter_cls", "filename_or_obj" - ) - - @docstrings.dedent - def __call__(self, *args, **kwargs): - """ - Parameters - ---------- - %(ProjectPlotter._add_data.parameters.no_plotter_cls|filename_or_obj)s - - Other Parameters - ---------------- - %(ProjectPlotter._add_data.other_parameters)s - - - Returns - ------- - %(ProjectPlotter._add_data.returns)s - """ - return super(DatasetPlotterInterface, self).__call__(*args, **kwargs) - - -class DatasetPlotter(ProjectPlotter): - """Interface between the :class:`xarray.Dataset` and the psyplot project - - This class can be used to make new plots from a given dataset and add them - to the current :func:`psyplot.project` - """ - - _plot_method_base_cls = DatasetPlotterInterface - - def __init__(self, ds, *args, **kwargs): - super(DatasetPlotter, self).__init__(*args, **kwargs) - self._ds = ds - - docstrings.delete_params( - "ProjectPlotter._add_data.parameters", "filename_or_obj" - ) - - @docstrings.get_sections( - base="ProjectPlotter._add_data", - sections=["Parameters", "Other Parameters", "Returns"], - ) - @docstrings.dedent - def _add_data(self, plotter_cls, *args, **kwargs): - """ - Add new plots to the project - - Parameters - ---------- - %(ProjectPlotter._add_data.parameters.no_filename_or_obj)s - - Other Parameters - ---------------- - %(ProjectPlotter._add_data.other_parameters)s - - Returns - ------- - %(ProjectPlotter._add_data.returns)s - """ - # this method is just a shortcut to the :meth:`Project._add_data` - # method but is reimplemented by subclasses as the - # :class:`DatasetPlotter` or the :class:`DataArrayPlotter` - return super(DatasetPlotter, self)._add_data( - plotter_cls, self._ds, *args, **kwargs - ) - - @classmethod - def _gen_doc( - cls, - summary, - full_name, - identifier, - example_call, - doc_str, - show_examples, - ): - """Generate the documentation docstring for a PlotMethod""" - # leave out the first argument - example_call = ", ".join(map(str.strip, example_call.split(",")[1:])) - ret = docstrings.dedent( - """ - %s - - This plotting method adds data arrays and plots them via - :class:`%s` plotters - - To plot a variable in this dataset, type:: - - >>> ds.psy.plot.%s(%s) - - %s""" - % (summary, full_name, identifier, example_call, doc_str) - ) - - if show_examples: - ret += "\n\n" + cls._gen_examples(identifier) - return ret - - @classmethod - def _gen_examples(cls, identifier): - """Generate examples how to axes the formatoption docs""" - return docstrings.dedent( - """ - Examples - -------- - To explore the formatoptions and their documentations, use the - ``keys``, ``summaries`` and ``docs`` methods. For example:: - - # show the keys corresponding to a group or multiple - # formatopions - >>> ds.psy.plot.%(id)s.keys('labels') - - # show the summaries of a group of formatoptions or of a - # formatoption - >>> ds.psy.plot.%(id)s.summaries('title') - - # show the full documentation - >>> ds.psy.plot.%(id)s.docs('plot') - - # or access the documentation via the attribute - >>> ds.psy.plot.%(id)s.plot""" - % {"id": identifier} - ) - - -class DataArrayPlotterInterface(PlotterInterface): - """Interface for the :class:`DataArrayPlotter` to a plotter""" - - # we reimplement the call method because we do not use the - # prefer_list, etc. keywords. And we reimplment the check_data method - # because we use the data array directly - - docstrings.delete_params("Plotter.parameters", "data") - - @docstrings.dedent - def __call__(self, *args, **kwargs): - """ - Parameters - ---------- - %(Plotter.parameters.no_data)s - - - Returns - ------- - psyplot.plotter.Plotter - The plotter that visualizes the data - """ - checks, messages = self.check_data() - if not all(checks): - raise ValueError( - "Cannot visualize the data using %s! Reasons:\n %s" - % (self.plotter_name, "\n ".join(filter(None, messages))) - ) - return self._project_plotter._add_data( - self.plotter_cls, *args, **kwargs - ) - - def check_data(self, *args, **kwargs): - """Check whether the plotter of this plot method can visualize the data""" - plotter_cls = self.plotter_cls - da_list = self._project_plotter._da.psy.to_interactive_list() - return plotter_cls.check_data( - da_list.all_names, da_list.all_dims, da_list.is_unstructured - ) - - -class DataArrayPlotter(ProjectPlotter): - """Interface between the :class:`xarray.Dataset` and the psyplot project - - This class can be used to make new plots from a given dataset and add them - to the current :func:`psyplot.project` - """ - - _plot_method_base_cls = DataArrayPlotterInterface - - def __init__(self, da, *args, **kwargs): - super(DataArrayPlotter, self).__init__(*args, **kwargs) - self._da = getattr(da, "arr", da) - - @docstrings.dedent - def _add_data(self, plotter_cls, *args, **kwargs): - """ - Visualize this data array - - Parameters - ---------- - %(Plotter.parameters.no_data)s - - Returns - ------- - psyplot.plotter.Plotter - The plotter that visualizes the data - """ - # this method is just a shortcut to the :meth:`Project._add_data` - # method but is reimplemented by subclasses as the - # :class:`DatasetPlotter` or the :class:`DataArrayPlotter` - return plotter_cls(self._da, *args, **kwargs) - - @classmethod - def _gen_doc( - cls, - summary, - full_name, - identifier, - example_call, - doc_str, - show_examples, - ): - """Generate the documentation docstring for a PlotMethod""" - # leave out the first argument - example_call = ", ".join(map(str.strip, example_call.split(",")[1:])) - ret = docstrings.dedent( - """ - %s - - This plotting method visualizes the data via a - :class:`%s` plotters - - To plot a variable in this dataset, type:: - - >>> da.psy.plot.%s() - - %s""" - % (summary, full_name, identifier, doc_str) - ) - - if show_examples: - ret += "\n\n" + cls._gen_examples(identifier) - return ret - - @classmethod - def _gen_examples(cls, identifier): - """Generate examples how to axes the formatoption docs""" - return docstrings.dedent( - """ - Examples - -------- - To explore the formatoptions and their documentations, use the - ``keys``, ``summaries`` and ``docs`` methods. For example:: - - # show the keys corresponding to a group or multiple - # formatopions - >>> da.psy.plot.%(id)s.keys('labels') - - # show the summaries of a group of formatoptions or of a - # formatoption - >>> da.psy.plot.%(id)s.summaries('title') - - # show the full documentation - >>> da.psy.plot.%(id)s.docs('plot') - - # or access the documentation via the attribute - >>> da.psy.plot.%(id)s.plot""" - % {"id": identifier} - ) - - -if with_cdo: - CDF_MOD_NCREADER = "xarray" - - docstrings.keep_params( - "Project._add_data.parameters", - "dims", - "fmt", - "ax", - "make_plot", - "method", - ) - - class Cdo(_CdoBase): - __doc__ = docstrings.dedent( - """ - Subclass of the original cdo.Cdo class in the cdo.py module - - Requirements are a working cdo binary and the installed cdo.py - python module. - - For a documentation of an operator, use the python help function, - for a list of operators, use the builtin dir function. - Further documentation on the operators can be found here: - https://code.zmaw.de/projects/cdo/wiki/Cdo%7Brbpy%7D - and on the usage of the cdo.py module here: - https://code.zmaw.de/projects/cdo - - For a demonstration script on how cdos are implemented, see the - examples of the psyplot package - - Compared to the original cdo.Cdo class, the following things - changed, the default cdf handler is the - :func:`psyplot.data.open_dataset` function and the following - keywords are implemented for each cdo operator. If any of those is - specified, the return will be a subproject (i.e. an instance of - :class:`psyplot.project.Project`) - - Other Parameters - ---------------- - plot_method: str or psyplot.project.PlotterInterface - An registered plotting function to plot the data (e.g. - `psyplot.project.plot.mapplot` to plot on a map). If ``None``, - no plot will be created. In any case, the returned value is a - subproject. If string, it must correspond to the attribute of - the :class:`psyplot.project.ProjectPlotter` class - name: str or list of str - The variable names to plot/extract - %(Project._add_data.parameters.dims|fmt|ax|make_plot|method)s - - Examples - -------- - Calculate the timmean of a 3-dimensional array and plot it on a map - using the psy-maps package - - .. code-block:: python - - cdo = psy.Cdo() - sp = cdo.timmean(input='ifile.nc', name='temperature', - plot_method='mapplot') - - which is essentially the same as - - .. code-block:: python - - sp = cdo.timmean(input='ifile.nc', name='temperature', - plot_method=psy.plot.mapplot) - # and - sp = psy.plot.mapplot( - cdo.timmean(input='ifile.nc', returnCdf=True), - name='temperature', plot_method=psy.plot.mapplot) - """ - ) - - def __init__(self, *args, **kwargs): - if cdo_version < (1, 5): - kwargs.setdefault("cdfMod", CDF_MOD_NCREADER) - super(Cdo, self).__init__(*args, **kwargs) - if cdo_version < (1, 5): - self.loadCdf() - - def loadCdf(self, *args, **kwargs): - """Load data handler as specified by self.cdfMod""" - if cdo_version < (1, 5): - - def open_nc(*args, **kwargs): - kwargs.pop("mode", None) - return open_dataset(*args, **kwargs) - - if self.cdfMod == CDF_MOD_NCREADER: - self.cdf = open_nc - else: - super(Cdo, self).loadCdf(*args, **kwargs) - else: - super(Cdo, self).readCdf(*args, **kwargs) - - def __getattr__(self, method_name): - def my_get(get): - """Wrapper for get method of Cdo class to include several plotters""" - - @wraps(get) - def wrapper(self, *args, **kwargs): - added_kwargs = {"plot_method", "name", "dims", "fmt"} - if added_kwargs.intersection(kwargs): - plot_method = kwargs.pop("plot_method", None) - ax = kwargs.pop("ax", None) - make_plot = kwargs.pop("make_plot", True) - fmt = kwargs.pop("fmt", {}) - dims = kwargs.pop("dims", {}) - name = kwargs.pop("name", None) - method = kwargs.pop("method", "isel") - if cdo_version < (1, 5): - kwargs["returnCdf"] = True - else: - kwargs["returnXDataset"] = True - ds = get(*args, **kwargs) - if isinstance(plot_method, six.string_types): - plot_method = getattr(plot, plot_method) - if plot_method is None: - ret = Project.from_dataset( - ds, name=name, dims=dims, method=method - ) - ret.main = gcp(True) - return ret - else: - return plot_method( - ds, - name=name, - fmt=fmt, - dims=dims, - ax=ax, - make_plot=make_plot, - method=method, - ) - else: - return get(*args, **kwargs) - - return wrapper - - get = my_get(super(Cdo, self).__getattr__(method_name)) - setattr(self.__class__, method_name, get) - return get.__get__(self) - - -@dedent -def gcp(main=False): - """ - Get the current project - - Parameters - ---------- - main: bool - If True, the current main project is returned, otherwise the current - subproject is returned. - See Also - -------- - scp: Sets the current project - project: Creates a new project""" - if main: - return project() if _current_project is None else _current_project - else: - return ( - gcp(True) if _current_subproject is None else _current_subproject - ) - - -@dedent -def scp(project): - """ - Set the current project - - Parameters - ---------- - %(Project.scp.parameters)s - - See Also - -------- - gcp: Returns the current project - project: Creates a new project""" - return PROJECT_CLS.scp(project) - - -def _scp(p, main=False): - """scp version that allows a bit more control over whether the project is a - main project or not""" - global _current_subproject - global _current_project - if p is None: - mp = ( - project() if main or _current_project is None else _current_project - ) - _current_subproject = Project(main=mp) - elif not main: - _current_subproject = p - else: - _current_project = p - - -@docstrings.dedent -def project(num=None, *args, **kwargs): - """ - Create a new main project - - Parameters - ---------- - num: int - The number of the project - %(Project.parameters.no_num)s - - Returns - ------- - Project - The with the given `num` (if it does not already exist, it is created) - - See Also - -------- - scp: Sets the current project - gcp: Returns the current project - """ - numbers = [project.num for project in _open_projects] - if num in numbers: - return _open_projects[numbers.index(num)] - if num is None: - num = max(numbers) + 1 if numbers else 1 - project = PROJECT_CLS.new(num, *args, **kwargs) - _open_projects.append(project) - return project - - -@docstrings.dedent -def close(num=None, figs=True, data=True, ds=True, remove_only=False): - """ - Close the project - - This method closes the current project (figures, data and datasets) or the - project specified by `num` - - Parameters - ---------- - num: int, None or 'all' - if :class:`int`, it specifies the number of the project, if None, the - current subproject is closed, if ``'all'``, all open projects are - closed - %(Project.close.parameters)s - - See Also - -------- - Project.close""" - kws = dict(figs=figs, data=data, ds=ds, remove_only=remove_only) - cp_num = gcp(True).num - got_cp = False - if num is None: - project = gcp() - scp(None) - project.close(**kws) - elif num == "all": - for project in _open_projects[:]: - project.close(**kws) - got_cp = got_cp or project.main.num == cp_num - del _open_projects[0] - else: - if isinstance(num, Project): - project = num - else: - project = [ - project for project in _open_projects if project.num == num - ][0] - project.close(**kws) - try: - _open_projects.remove(project) - except ValueError: - pass - got_cp = got_cp or project.main.num == cp_num - if got_cp: - if _open_projects: - # set last opened project to the current - scp(_open_projects[-1]) - else: - _scp(None, True) # set the current project to None - - -docstrings.delete_params("Project._register_plotter.parameters", "plotter_cls") - - -@docstrings.dedent -def register_plotter( - identifier, - module, - plotter_name, - plotter_cls=None, - sorter=True, - plot_func=True, - import_plotter=None, - **kwargs, -): - """ - Register a :class:`psyplot.plotter.Plotter` for the projects - - This function registers plotters for the :class:`Project` class to allow - a dynamical handling of different plotter classes. - - Parameters - ---------- - %(Project._register_plotter.parameters.no_plotter_cls)s - sorter: bool, optional - If True, the :class:`Project` class gets a new property with the name - of the specified `identifier` which allows you to access the instances - that are plotted by the specified `plotter_name` - plot_func: bool, optional - If True, the :class:`ProjectPlotter` (the class that holds the - plotting method for the :class:`Project` class and can be accessed via - the :attr:`Project.plot` attribute) gets an additional method to plot - via the specified `plotter_name` (see `Other Parameters` below.) - import_plotter: bool, optional - If True, the plotter is automatically imported, otherwise it is only - imported when it is needed. If `import_plotter` is None, then it is - determined by the :attr:`psyplot.rcParams` ``'project.auto_import'`` - item. - - Other Parameters - ---------------- - %(ProjectPlotter._register_plotter.other_parameters)s - """ - if plotter_cls is None: - if ( - import_plotter is None and rcParams["project.auto_import"] - ) or import_plotter: - try: - plotter_cls = getattr(import_module(module), plotter_name) - except Exception as e: - critical( - ("Could not import %s!\n" % module) + e.message - if six.PY2 - else str(e) - ) - return - if sorter: - if hasattr(Project, identifier): - raise ValueError( - "Project class already has a %s attribute" % identifier - ) - Project._register_plotter( - identifier, module, plotter_name, plotter_cls - ) - if plot_func: - if hasattr(ProjectPlotter, identifier): - raise ValueError( - "Project class already has a %s attribute" % identifier - ) - ProjectPlotter._register_plotter( - identifier, module, plotter_name, plotter_cls, **kwargs - ) - DatasetPlotter._register_plotter( - identifier, module, plotter_name, plotter_cls, **kwargs - ) - DataArrayPlotter._register_plotter( - identifier, module, plotter_name, plotter_cls, **kwargs - ) - if identifier not in registered_plotters: - kwargs.update( - dict( - module=module, - plotter_name=plotter_name, - sorter=sorter, - plot_func=plot_func, - import_plotter=import_plotter, - ) - ) - registered_plotters[identifier] = kwargs - return - - -def unregister_plotter(identifier, sorter=True, plot_func=True): - """ - Unregister a :class:`psyplot.plotter.Plotter` for the projects - - Parameters - ---------- - identifier: str - Name of the attribute that is used to filter for the instances - belonging to this plotter or to create plots with this plotter - sorter: bool - If True, the identifier will be unregistered from the :class:`Project` - class - plot_func: bool - If True, the identifier will be unregistered from the - :class:`ProjectPlotter` class - """ - d = registered_plotters.get(identifier, {}) - if sorter and hasattr(Project, identifier): - delattr(Project, identifier) - d["sorter"] = False - if plot_func and hasattr(ProjectPlotter, identifier): - for cls in [ProjectPlotter, DatasetPlotter, DataArrayPlotter]: - delattr(cls, identifier) - try: - delattr(plot, "_" + identifier) - except AttributeError: - pass - d["plot_func"] = False - if sorter and plot_func: - registered_plotters.pop(identifier, None) - - -registered_plotters = {} - -for _identifier, _plotter_settings in rcParams["project.plotters"].items(): - register_plotter(_identifier, **_plotter_settings) - - -def get_project_nums(): - """Returns the project numbers of the open projects""" - return [p.num for p in _open_projects] - - -#: :class:`ProjectPlotter` of the current project. See the class documentation -#: for available plotting methods -plot = ProjectPlotter() - -#: The project class that is used for creating new projects -PROJECT_CLS = Project - -psyplot._project_imported = True diff --git a/psyplot/sphinxext/__init__.py b/psyplot/sphinxext/__init__.py deleted file mode 100755 index 02dc29c..0000000 --- a/psyplot/sphinxext/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -"""Sphinx extension package of the psyplot module""" - -# SPDX-FileCopyrightText: 2016-2024 University of Lausanne -# SPDX-FileCopyrightText: 2020-2021 Helmholtz-Zentrum Geesthacht - -# SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH -# -# SPDX-License-Identifier: LGPL-3.0-only diff --git a/psyplot/sphinxext/extended_napoleon.py b/psyplot/sphinxext/extended_napoleon.py deleted file mode 100755 index 7f68c49..0000000 --- a/psyplot/sphinxext/extended_napoleon.py +++ /dev/null @@ -1,171 +0,0 @@ -"""Sphinx extension module to provide additional sections for numpy docstrings - -This extension extends the :mod:`sphinx.ext.napoleon` package with an -additional *Possible types* section in order to document possible types for -descriptors. - -Notes ------ -If you use this module as a sphinx extension, you should not list the -:mod:`sphinx.ext.napoleon` module in the extensions variable of your conf.py. -This module has been tested for sphinx 1.3.1.""" - -# SPDX-FileCopyrightText: 2016-2024 University of Lausanne -# SPDX-FileCopyrightText: 2020-2021 Helmholtz-Zentrum Geesthacht - -# SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH -# -# SPDX-License-Identifier: LGPL-3.0-only - -from abc import ABCMeta, abstractmethod - -from sphinx.ext.napoleon import GoogleDocstring, NumpyDocstring -from sphinx.ext.napoleon import setup as napoleon_setup - - -class DocstringExtension(object): - """Class that introduces a "Possible Types" section - - This class serves as a base class for - :class:`sphinx.ext.napoleon.NumpyDocstring` and - :class:`sphinx.ext.napoleon.GoogleDocstring` to introduce - another section names *Possible types* - - Examples - -------- - The usage is the same as for the NumpyDocstring class, but it supports - the `Possible types` section:: - - >>> from sphinx.ext.napoleon import Config - - >>> from psyplot.sphinxext.extended_napoleon import ( - ... ExtendedNumpyDocstring, - ... ) - >>> config = Config(napoleon_use_param=True, napoleon_use_rtype=True) - >>> docstring = ''' - ... Possible types - ... -------------- - ... type1 - ... Description of `type1` - ... type2 - ... Description of `type2`''' - >>> print(ExtendedNumpyDocstring(docstring, config)) - .. rubric:: Possible types - - * *type1* -- - Description of `type1` - * *type2* -- - Description of `type2`""" - - __metaclass__ = ABCMeta - - def _parse_possible_types_section(self, section): - fields = self._consume_fields(prefer_type=True) - lines = [".. rubric:: %s" % section, ""] - multi = len(fields) > 1 - for _name, _type, _desc in fields: - field = self._format_field(_name, _type, _desc) - if multi: - lines.extend(self._format_block("* ", field)) - else: - lines.extend(field) - return lines + [""] - - @abstractmethod - def _parse(self): - pass - - -class ExtendedNumpyDocstring(NumpyDocstring, DocstringExtension): - """:class:`sphinx.ext.napoleon.NumpyDocstring` with more sections""" - - def _parse(self, *args, **kwargs): - self._sections["possible types"] = self._parse_possible_types_section - return super(ExtendedNumpyDocstring, self)._parse(*args, **kwargs) - - -class ExtendedGoogleDocstring(GoogleDocstring, DocstringExtension): - """:class:`sphinx.ext.napoleon.GoogleDocstring` with more sections""" - - def _parse(self, *args, **kwargs): - self._sections["possible types"] = self._parse_possible_types_section - return super(ExtendedGoogleDocstring, self)._parse(*args, **kwargs) - - -def process_docstring(app, what, name, obj, options, lines): - """Process the docstring for a given python object. - - Called when autodoc has read and processed a docstring. `lines` is a list - of docstring lines that `_process_docstring` modifies in place to change - what Sphinx outputs. - - The following settings in conf.py control what styles of docstrings will - be parsed: - - * ``napoleon_google_docstring`` -- parse Google style docstrings - * ``napoleon_numpy_docstring`` -- parse NumPy style docstrings - - Parameters - ---------- - app : sphinx.application.Sphinx - Application object representing the Sphinx process. - what : str - A string specifying the type of the object to which the docstring - belongs. Valid values: "module", "class", "exception", "function", - "method", "attribute". - name : str - The fully qualified name of the object. - obj : module, class, exception, function, method, or attribute - The object to which the docstring belongs. - options : sphinx.ext.autodoc.Options - The options given to the directive: an object with attributes - inherited_members, undoc_members, show_inheritance and noindex that - are True if the flag option of same name was given to the auto - directive. - lines : list of str - The lines of the docstring, see above. - - .. note:: `lines` is modified *in place* - - Notes - ----- - This function is (to most parts) taken from the :mod:`sphinx.ext.napoleon` - module, sphinx version 1.3.1, and adapted to the classes defined here""" - result_lines = lines - if app.config.napoleon_numpy_docstring: - docstring = ExtendedNumpyDocstring( - result_lines, app.config, app, what, name, obj, options - ) - result_lines = docstring.lines() - if app.config.napoleon_google_docstring: - docstring = ExtendedGoogleDocstring( - result_lines, app.config, app, what, name, obj, options - ) - result_lines = docstring.lines() - - lines[:] = result_lines[:] - - -def setup(app): - """Sphinx extension setup function - - When the extension is loaded, Sphinx imports this module and executes - the ``setup()`` function, which in turn notifies Sphinx of everything - the extension offers. - - Parameters - ---------- - app : sphinx.application.Sphinx - Application object representing the Sphinx process - - Notes - ----- - This function uses the setup function of the :mod:`sphinx.ext.napoleon` - module""" - from sphinx.application import Sphinx - - if not isinstance(app, Sphinx): - return # probably called by tests - - app.connect("autodoc-process-docstring", process_docstring) - return napoleon_setup(app) diff --git a/psyplot/utils.py b/psyplot/utils.py deleted file mode 100644 index 3c2cfe1..0000000 --- a/psyplot/utils.py +++ /dev/null @@ -1,399 +0,0 @@ -"""Miscallaneous utility functions for the psyplot package.""" - -# SPDX-FileCopyrightText: 2016-2024 University of Lausanne -# SPDX-FileCopyrightText: 2020-2021 Helmholtz-Zentrum Geesthacht - -# SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH -# -# SPDX-License-Identifier: LGPL-3.0-only - -import inspect -import re -import sys -from difflib import get_close_matches -from itertools import chain, filterfalse - -import six - -from psyplot.docstring import dedent, docstrings - - -def get_default_value(func, arg): - argspec = inspect.getfullargspec(func) - return next( - default - for a, default in zip(reversed(argspec[0]), reversed(argspec.defaults)) - if a == arg - ) - - -def isstring(s): - return isinstance(s, str) - - -def plugin_entrypoints(group="psyplot", name="name"): - """This utility function gets the entry points of the psyplot plugins""" - if sys.version_info[:2] > (3, 7): - from importlib.metadata import entry_points - - try: - eps = entry_points(group=group, name=name) - except TypeError: # python<3.10 - eps = [ - ep for ep in entry_points().get(group, []) if ep.name == name - ] - else: - from pkg_resources import iter_entry_points - - eps = iter_entry_points(group=group, name=name) - return eps - - -class Defaultdict(dict): - """An ordered :class:`collections.defaultdict` - - Taken from http://stackoverflow.com/a/6190500/562769""" - - def __init__(self, default_factory=None, *a, **kw): - if default_factory is not None and not callable(default_factory): - raise TypeError("first argument must be callable") - dict.__init__(self, *a, **kw) - self.default_factory = default_factory - - def __getitem__(self, key): - try: - return dict.__getitem__(self, key) - except KeyError: - return self.__missing__(key) - - def __missing__(self, key): - if self.default_factory is None: - raise KeyError(key) - self[key] = value = self.default_factory() - return value - - def __reduce__(self): - if self.default_factory is None: - args = tuple() - else: - args = (self.default_factory,) - return type(self), args, None, None, self.items() - - def copy(self): - """Return a shallow copy of the dictionary""" - return self.__copy__() - - def __copy__(self): - return type(self)(self.default_factory, self) - - def __deepcopy__(self, memo): - import copy - - return type(self)(self.default_factory, copy.deepcopy(self.items())) - - def __repr__(self): - return "Defaultdict(%s, %s)" % ( - self.default_factory, - dict.__repr__(self), - ) - - -class _TempBool(object): - """Wrapper around a boolean defining an __enter__ and __exit__ method - - Notes - ----- - If you want to use this class as an instance property, rather use the - :func:`_temp_bool_prop` because this class as a descriptor is ment to be a - class descriptor""" - - #: default boolean value for the :attr:`value` attribute - default = False - - #: boolean value indicating whether there shall be a validation or not - value = False - - def __init__(self, default=False): - """ - Parameters - ---------- - default: bool - value of the object""" - self.default = default - self.value = default - self._entered = [] - - def __enter__(self): - self.value = not self.default - self._entered.append(1) - - def __exit__(self, type, value, tb): - self._entered.pop(-1) - if not self._entered: - self.value = self.default - - if six.PY2: - - def __nonzero__(self): - return self.value - - else: - - def __bool__(self): - return self.value - - def __repr__(self): - return repr(bool(self)) - - def __str__(self): - return str(bool(self)) - - def __call__(self, value=None): - """ - Parameters - ---------- - value: bool or None - If None, the current value will be negated. Otherwise the current - value of this instance is set to the given `value`""" - if value is None: - self.value = not self.value - else: - self.value = value - - def __get__(self, instance, owner): - return self - - def __set__(self, instance, value): - self.value = value - - -def _temp_bool_prop(propname, doc="", default=False): - """Creates a property that uses the :class:`_TempBool` class - - Parameters - ---------- - propname: str - The attribute name to use. The _TempBool instance will be stored in the - ``'_' + propname`` attribute of the corresponding instance - doc: str - The documentation of the property - default: bool - The default value of the _TempBool class""" - - def getx(self): - if getattr(self, "_" + propname, None) is not None: - return getattr(self, "_" + propname) - else: - setattr(self, "_" + propname, _TempBool(default)) - return getattr(self, "_" + propname) - - def setx(self, value): - getattr(self, propname).value = bool(value) - - def delx(self): - getattr(self, propname).value = default - - return property(getx, setx, delx, doc) - - -def unique_everseen(iterable, key=None): - """List unique elements, preserving order. Remember all elements ever seen. - - Function taken from https://docs.python.org/2/library/itertools.html""" - # unique_everseen('AAAABBBCCDAABBB') --> A B C D - # unique_everseen('ABBCcAD', str.lower) --> A B C D - seen = set() - seen_add = seen.add - if key is None: - for element in filterfalse(seen.__contains__, iterable): - seen_add(element) - yield element - else: - for element in iterable: - k = key(element) - if k not in seen: - seen_add(k) - yield element - - -def is_remote_url(path): - patt = re.compile(r"^https?\://") - if not isinstance(path, six.string_types): - return all(map(patt.search, (s or "" for s in path))) - return bool(re.search(r"^https?\://", path)) - - -@docstrings.get_sections( - base="check_key", sections=["Parameters", "Returns", "Raises"] -) -@dedent -def check_key( - key, - possible_keys, - raise_error=True, - name="formatoption keyword", - msg=("See show_fmtkeys function for possible formatopion " "keywords"), - *args, - **kwargs, -): - """ - Checks whether the key is in a list of possible keys - - This function checks whether the given `key` is in `possible_keys` and if - not looks for similar sounding keys - - Parameters - ---------- - key: str - Key to check - possible_keys: list of strings - a list of possible keys to use - raise_error: bool - If not True, a list of similar keys is returned - name: str - The name of the key that shall be used in the error message - msg: str - The additional message that shall be used if no close match to - key is found - *args, **kwargs - They are passed to the :func:`difflib.get_close_matches` function - (i.e. `n` to increase the number of returned similar keys and - `cutoff` to change the sensibility) - - Returns - ------- - str - The `key` if it is a valid string, else an empty string - list - A list of similar formatoption strings (if found) - str - An error message which includes - - Raises - ------ - KeyError - If the key is not a valid formatoption and `raise_error` is True""" - if key not in possible_keys: - similarkeys = get_close_matches(key, possible_keys, *args, **kwargs) - if similarkeys: - msg = ("Unknown %s %s! Possible similiar " "frasings are %s.") % ( - name, - key, - ", ".join(similarkeys), - ) - else: - msg = ("Unknown %s %s! ") % (name, key) + msg - if not raise_error: - return "", similarkeys, msg - raise KeyError(msg) - else: - return key, [key], "" - - -def sort_kwargs(kwargs, *param_lists): - """Function to sort keyword arguments and sort them into dictionaries - - This function returns dictionaries that contain the keyword arguments - from `kwargs` corresponding given iterables in ``*params`` - - Parameters - ---------- - kwargs: dict - Original dictionary - ``*param_lists`` - iterables of strings, each standing for a possible key in kwargs - - Returns - ------- - list - len(params) + 1 dictionaries. Each dictionary contains the items of - `kwargs` corresponding to the specified list in ``*param_lists``. The - last dictionary contains the remaining items""" - return chain( - ( - {key: kwargs.pop(key) for key in params.intersection(kwargs)} - for params in map(set, param_lists) - ), - [kwargs], - ) - - -def hashable(val): - """Test if `val` is hashable and if not, get it's string representation - - Parameters - ---------- - val: object - Any (possibly not hashable) python object - - Returns - ------- - val or string - The given `val` if it is hashable or it's string representation""" - if val is None: - return val - try: - hash(val) - except TypeError: - return repr(val) - else: - return val - - -@docstrings.get_sections(base="join_dicts") -def join_dicts(dicts, delimiter=None, keep_all=False): - """Join multiple dictionaries into one - - Parameters - ---------- - dicts: list of dict - A list of dictionaries - delimiter: str - The string that shall be used as the delimiter in case that there - are multiple values for one attribute in the arrays. If None, they - will be returned as sets - keep_all: bool - If True, all formatoptions are kept. Otherwise only the intersection - - Returns - ------- - dict - The combined dictionary""" - if not dicts: - return {} - if keep_all: - all_keys = set(chain(*(d.keys() for d in dicts))) - else: - all_keys = set(dicts[0]) - for d in dicts[1:]: - all_keys.intersection_update(d) - ret = {} - for key in all_keys: - vals = {hashable(d.get(key, None)) for d in dicts} - {None} - if len(vals) == 1: - ret[key] = next(iter(vals)) - elif delimiter is None: - ret[key] = vals - else: - ret[key] = delimiter.join(map(str, vals)) - return ret - - -def is_iterable(iterable): - """Test if an object is iterable - - Parameters - ---------- - iterable: object - The object to test - - Returns - ------- - bool - True, if the object is an iterable object""" - try: - iter(iterable) - except TypeError: - return False - else: - return True diff --git a/psyplot/warning.py b/psyplot/warning.py deleted file mode 100755 index 7a3e2d4..0000000 --- a/psyplot/warning.py +++ /dev/null @@ -1,128 +0,0 @@ -# coding: utf-8 -"""Warning module of the psyplot python module. - -This module controls the warning behaviour of the module via the python -builtin warnings module and introduces three new warning classes: - -..autosummay:: - - PsPylotRuntimeWarning - PsyPlotWarning - PsyPlotCritical""" - -# SPDX-FileCopyrightText: 2016-2024 University of Lausanne -# SPDX-FileCopyrightText: 2020-2021 Helmholtz-Zentrum Geesthacht - -# SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH -# -# SPDX-License-Identifier: LGPL-3.0-only - -import logging -import warnings - -# disable a warning about "comparison to 'None' in backend_pdf which occurs -# in the matplotlib.backends.backend_pdf.PdfPages class -warnings.filterwarnings( - "ignore", - "comparison", - FutureWarning, - "matplotlib.backends.backend_pdf", - 2264, -) -# disable a warning about "np.array_split" that occurs for certain numpy -# versions -warnings.filterwarnings( - "ignore", - "in the future np.array_split will retain", - FutureWarning, - "numpy.lib.shape_base", - 431, -) -# disable a warning about "elementwise comparison of a string" in the -# matplotlib.collection.Collection.get_edgecolor method that occurs for certain -# matplotlib and numpy versions -warnings.filterwarnings( - "ignore", - "elementwise comparison failed", - FutureWarning, - "matplotlib.collections", - 590, -) - - -logger = logging.getLogger(__name__) - - -class PsyPlotRuntimeWarning(RuntimeWarning): - """Runtime warning that appears only ones""" - - pass - - -class PsyPlotWarning(UserWarning): - """Normal UserWarning for psyplot module""" - - pass - - -class PsyPlotCritical(UserWarning): - """Critical UserWarning for psyplot module""" - - pass - - -warnings.simplefilter("always", PsyPlotWarning, append=True) -warnings.simplefilter("always", PsyPlotCritical, append=True) - - -def disable_warnings(critical=False): - """Function that disables all warnings and all critical warnings (if - critical evaluates to True) related to the psyplot Module. - Please note that you can also configure the warnings via the - psyplot.warning logger (logging.getLogger(psyplot.warning)).""" - warnings.filterwarnings("ignore", r"\w", PsyPlotWarning, "psyplot", 0) - if critical: - warnings.filterwarnings("ignore", r"\w", PsyPlotCritical, "psyplot", 0) - - -def warn(message, category=PsyPlotWarning, logger=None): - """wrapper around the warnings.warn function for non-critical warnings. - logger may be a logging.Logger instance""" - if logger is not None: - message = "[Warning by %s]\n%s" % (logger.name, message) - warnings.warn(message, category, stacklevel=3) - - -def critical(message, category=PsyPlotCritical, logger=None): - """wrapper around the warnings.warn function for critical warnings. - logger may be a logging.Logger instance""" - if logger is not None: - message = "[Critical warning by %s]\n%s" % (logger.name, message) - warnings.warn(message, category, stacklevel=2) - - -old_showwarning = warnings.showwarning - - -def customwarn(message, category, filename, lineno, *args, **kwargs): - """Use the psyplot.warning logger for categories being out of - PsyPlotWarning and PsyPlotCritical and the default warnings.showwarning - function for all the others.""" - if category is PsyPlotWarning: - logger.warning( - warnings.formatwarning( - "\n%s" % message, category, filename, lineno - ) - ) - elif category is PsyPlotCritical: - logger.critical( - warnings.formatwarning( - "\n%s" % message, category, filename, lineno - ), - exc_info=True, - ) - else: - old_showwarning(message, category, filename, lineno, *args, **kwargs) - - -warnings.showwarning = customwarn diff --git a/py-modindex.html b/py-modindex.html new file mode 100644 index 0000000..555259c --- /dev/null +++ b/py-modindex.html @@ -0,0 +1,487 @@ + + + + + + Python Module Index — psyplot documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+
    +
  • + +
  • +
  • +
+
+
+
+
+ + +

Python Module Index

+ +
+ p +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
 
+ p
+ psyplot +
    + psyplot.config +
    + psyplot.config.logsetup +
    + psyplot.config.rcsetup +
    + psyplot.data +
    + psyplot.docstring +
    + psyplot.gdal_store +
    + psyplot.plotter +
    + psyplot.project +
    + psyplot.sphinxext +
    + psyplot.sphinxext.extended_napoleon +
    + psyplot.utils +
    + psyplot.warning +
+ + +
+
+ + + +
+
+
+
+ + + + \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml deleted file mode 100644 index a09b350..0000000 --- a/pyproject.toml +++ /dev/null @@ -1,149 +0,0 @@ -# SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH -# -# SPDX-License-Identifier: CC0-1.0 - -[build-system] -build-backend = 'setuptools.build_meta' -requires = ['setuptools >= 61.0', 'versioneer[toml]'] - -[project] -name = "psyplot" -dynamic = ["version"] -description = "Python package for interactive data visualization" - -readme = "README.rst" -keywords = [ - "visualization", - - "netcdf", - - "raster", - - "cartopy", - - "earth-sciences", - ] - -authors = [ - { name = 'Philipp S. Sommer', email = 'philipp.sommer@hereon.de' }, -] -maintainers = [ - { name = 'Philipp S. Sommer', email = 'philipp.sommer@hereon.de' }, -] -license = { text = 'LGPL-3.0-only' } - -classifiers = [ - "Development Status :: 5 - Production/Stable", - "Intended Audience :: Developers", - "Topic :: Scientific/Engineering :: Visualization", - "Topic :: Scientific/Engineering :: GIS", - "Topic :: Scientific/Engineering", - "License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.6", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - "Operating System :: OS Independent", -] - -requires-python = '>= 3.9' -dependencies = [ - # add your dependencies here - "matplotlib", - "docrep>=0.3", - "funcargparse", - "xarray>=0.17", - "PyYAML>=4.2b", -] - -[project.urls] -Homepage = 'https://codebase.helmholtz.cloud/psyplot/psyplot' -Documentation = "https://psyplot.github.io" -Source = "https://codebase.helmholtz.cloud/psyplot/psyplot" -Tracker = "https://codebase.helmholtz.cloud/psyplot/psyplot/issues/" - - -[project.optional-dependencies] -testsite = [ - "tox", - "isort==5.12.0", - "black==23.1.0", - "blackdoc==0.3.8", - "flake8==6.0.0", - "pre-commit", - "mypy", - "pytest-cov", - "reuse", - "cffconvert", - "netCDF4", - "dask", - "scipy", - "pytest", -] -docs = [ - "autodocsumm", - "sphinx-rtd-theme", - "hereon-netcdf-sphinxext", - "sphinx-design", - "ipython", - "pickleshare", # required for IPythons savefig - "seaborn", - "dask", - "netCDF4", - "sphinx-argparse", - "cdo", -] -dev = [ - "psyplot[testsite]", - "psyplot[docs]", - "PyYAML", - "types-PyYAML", -] - -[project.scripts] -psyplot = "psyplot.__main__:main" - -[tool.mypy] -ignore_missing_imports = true - -[tool.setuptools] -zip-safe = false -license-files = ["LICENSES/*"] - -[tool.setuptools.package-data] -psyplot = [] - -[tool.setuptools.packages.find] -namespaces = false -exclude = [ - 'docs', - 'tests*', - 'examples' -] - -[tool.pytest.ini_options] -addopts = '-v' - -[tool.versioneer] -VCS = 'git' -style = 'pep440' -versionfile_source = 'psyplot/_version.py' -versionfile_build = 'psyplot/_version.py' -tag_prefix = 'v' -parentdir_prefix = 'psyplot-' - -[tool.isort] -profile = "black" -line_length = 79 -src_paths = ["psyplot"] -float_to_top = true -known_first_party = "psyplot" - -[tool.black] -line-length = 79 -target-version = ['py39'] - -[tool.coverage.run] -omit = ["psyplot/_version.py"] diff --git a/search.html b/search.html new file mode 100644 index 0000000..46bd92d --- /dev/null +++ b/search.html @@ -0,0 +1,427 @@ + + + + + + Search — psyplot documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+
    +
  • + +
  • +
  • +
+
+
+
+
+ + + + +
+ +
+ +
+
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/searchindex.js b/searchindex.js new file mode 100644 index 0000000..3c4f3cb --- /dev/null +++ b/searchindex.js @@ -0,0 +1 @@ +Search.setIndex({"alltitles": {"API Reference": [[2, null]], "About psyplot": [[0, null]], "About the author": [[0, "about-the-author"]], "Acknowledgment": [[24, "acknowledgment"]], "Added": [[16, "added"], [16, "id2"], [16, "id4"], [16, "id7"], [16, "id11"], [16, "id13"], [16, "id15"], [16, "id16"]], "Adding your own script: The post formatoption": [[23, "adding-your-own-script-the-post-formatoption"]], "Annotating licenses": [[19, "annotating-licenses"]], "Automatic update": [[23, "automatic-update"]], "Building the docs": [[25, "building-the-docs"]], "Changed": [[16, "changed"], [16, "id3"], [16, "id5"], [16, "id8"], [16, "id12"], [16, "id14"], [16, "id17"]], "Changelog": [[16, null]], "Choosing the dimension": [[23, "choosing-the-dimension"]], "Code of Conduct": [[19, "code-of-conduct"]], "Command line usage": [[17, null]], "Configuration": [[18, null]], "Configuring the appearance of the plot": [[23, "configuring-the-appearance-of-the-plot"]], "Contributing in the development": [[19, "contributing-in-the-development"]], "Contributing to the code": [[19, "contributing-to-the-code"]], "Contribution and development hints": [[19, null]], "Controlling the update": [[23, "controlling-the-update"]], "Creating a package with the psyplot-plugin-template": [[22, "creating-a-package-with-the-psyplot-plugin-template"]], "Creating and managing multiple plots": [[23, "creating-and-managing-multiple-plots"]], "Creating multiple plots": [[23, "creating-multiple-plots"]], "Creating new plugins": [[22, "creating-new-plugins"]], "Creating plots with the dataarray accessor": [[1, "creating-plots-with-the-dataarray-accessor"]], "Creating plotters": [[22, "creating-plotters"]], "Default formatoptions": [[18, "default-formatoptions"]], "Dependencies": [[25, "dependencies"]], "Developers guide": [[21, null]], "Direct control on formatoption update": [[23, "direct-control-on-formatoption-update"]], "Documentation": [[24, "documentation"]], "Examples": [[3, null], [6, null], [6, null], [7, null], [7, null], [7, null], [9, null], [10, null], [10, null], [10, null], [10, null], [11, null], [11, null], [11, null], [11, null], [11, null], [11, null], [11, null], [11, null], [11, null], [11, null], [11, null], [11, null], [11, null], [11, null], [11, null], [11, null], [11, null], [11, null], [11, null], [11, null], [11, null], [11, null], [11, null], [11, null], [11, null], [11, null], [11, null], [11, null], [11, null], [11, null], [11, null], [11, null], [11, null], [11, null], [13, null]], "Existing plugins": [[26, "existing-plugins"]], "First solution: Use a dict": [[22, "first-solution-use-a-dict"]], "Fixed": [[16, "fixed"], [16, "id6"], [16, "id9"], [16, "id10"]], "Fixing the docs": [[19, "fixing-the-docs"]], "Formatoptions": [[20, "formatoptions"]], "Get in touch": [[24, "get-in-touch"]], "Getting started": [[23, null]], "Helpers": [[19, "helpers"]], "How to cite this software": [[24, "how-to-cite-this-software"]], "How to exclude plugins": [[26, "how-to-exclude-plugins"]], "How to implement your own plotters and plugins": [[22, null]], "How to install": [[25, "how-to-install"]], "Indices and tables": [[24, "indices-and-tables"]], "Info options": [[17, "info-options"]], "Initialization and interactive usage": [[23, "initialization-and-interactive-usage"]], "Installation": [[25, null]], "Installation from source": [[25, "installation-from-source"]], "Installation using conda": [[25, "installation-using-conda"]], "Installation using pip": [[25, "installation-using-pip"]], "Interactive data objects": [[20, "interactive-data-objects"]], "Interactive data visualization with python": [[24, null]], "Interface for the plotter": [[22, "interface-for-the-plotter"]], "Interface to the data": [[22, "interface-to-the-data"]], "Interfacing to other formatoptions": [[22, "interfacing-to-other-formatoptions"]], "License": [[0, "license"]], "Named Arguments": [[17, "named-arguments"]], "Optional dependencies": [[25, "optional-dependencies"]], "Output options": [[17, "output-options"]], "Positional Arguments": [[17, "positional-arguments"]], "Psyplot plugins": [[26, null]], "Removed": [[16, "removed"]], "Required dependencies": [[25, "required-dependencies"]], "Running the tests": [[25, "running-the-tests"]], "Saving and loading your project": [[23, "saving-and-loading-your-project"]], "Second solution: Interact with other formatoptions": [[22, "second-solution-interact-with-other-formatoptions"]], "Shortcuts with make": [[19, "shortcuts-with-make"]], "Slicing and filtering the project": [[23, "slicing-and-filtering-the-project"]], "Submodules": [[3, "submodules"], [4, "submodules"], [12, "submodules"]], "Subpackages": [[3, "subpackages"]], "Subprojects": [[27, null]], "Table of Contents": [[19, "table-of-contents"]], "The DatasetAccessor dataset accessor": [[1, "the-datasetaccessor-dataset-accessor"]], "The InteractiveArray dataarray accessor": [[1, "the-interactivearray-dataarray-accessor"]], "The InteractiveBase and the Plotter classes": [[20, "the-interactivebase-and-the-plotter-classes"]], "The PSYPLOT_PLOTMETHODS environment variable": [[26, "the-psyplot-plotmethods-environment-variable"]], "The PSYPLOT_PLUGINS environment variable": [[26, "the-psyplot-plugins-environment-variable"]], "The project() function": [[20, "the-project-function"]], "The psyplot framework": [[19, "the-psyplot-framework"], [20, null]], "The rcParams": [[18, "the-rcparams"]], "ToDos": [[28, null]], "Todo": [[7, "id1"]], "Uninstallation": [[25, "uninstallation"]], "Uninstallation via conda": [[25, "uninstallation-via-conda"]], "Uninstallation via pip": [[25, "uninstallation-via-pip"]], "Updating plots and arrays with the dataarray accessor": [[1, "updating-plots-and-arrays-with-the-dataarray-accessor"]], "Updating the skeleton for this package": [[19, "updating-the-skeleton-for-this-package"]], "Using presets": [[23, "using-presets"]], "Visualization objects": [[20, "visualization-objects"]], "What it is": [[0, "what-it-is"]], "What it is not": [[0, "what-it-is-not"]], "What it is, and what it is not": [[0, "what-it-is-and-what-it-is-not"]], "What should I know before I get started?": [[19, "what-should-i-know-before-i-get-started"]], "Why psyplot?": [[0, "why-psyplot"]], "psyplot package": [[3, null]], "psyplot.config package": [[4, null]], "psyplot.sphinxext package": [[12, null]], "rcParams handling in plugins": [[22, "rcparams-handling-in-plugins"]], "v1.0.0": [[16, "v1-0-0"]], "v1.1.0": [[16, "v1-1-0"]], "v1.2.0": [[16, "v1-2-0"]], "v1.2.1": [[16, "v1-2-1"]], "v1.3.0": [[16, "v1-3-0"]], "v1.3.1": [[16, "v1-3-1"]], "v1.3.2": [[16, "v1-3-2"]], "v1.4.0": [[16, "v1-4-0"]], "v1.4.1": [[16, "v1-4-1"]], "v1.4.2": [[16, "v1-4-2"]], "v1.4.3": [[16, "v1-4-3"]], "v1.5.0": [[16, "v1-5-0"]], "v1.5.1": [[16, "v1-5-1"]], "xarray Accessors": [[1, null]]}, "docnames": ["about", "accessors", "api", "api/psyplot", "api/psyplot.config", "api/psyplot.config.logsetup", "api/psyplot.config.rcsetup", "api/psyplot.data", "api/psyplot.docstring", "api/psyplot.gdal_store", "api/psyplot.plotter", "api/psyplot.project", "api/psyplot.sphinxext", "api/psyplot.sphinxext.extended_napoleon", "api/psyplot.utils", "api/psyplot.warning", "changelog", "command_line", "configuration", "contributing", "develop/framework", "develop/index", "develop/plugins_guide", "getting_started", "index", "installing", "plugins", "projects", "todos"], "envversion": {"sphinx": 61, "sphinx.domains.c": 3, "sphinx.domains.changeset": 1, "sphinx.domains.citation": 1, "sphinx.domains.cpp": 9, "sphinx.domains.index": 1, "sphinx.domains.javascript": 3, "sphinx.domains.math": 2, "sphinx.domains.python": 4, "sphinx.domains.rst": 2, "sphinx.domains.std": 2, "sphinx.ext.intersphinx": 1, "sphinx.ext.todo": 2, "sphinx.ext.viewcode": 1}, "filenames": ["about.rst", "accessors.rst", "api.rst", "api/psyplot.rst", "api/psyplot.config.rst", "api/psyplot.config.logsetup.rst", "api/psyplot.config.rcsetup.rst", "api/psyplot.data.rst", "api/psyplot.docstring.rst", "api/psyplot.gdal_store.rst", "api/psyplot.plotter.rst", "api/psyplot.project.rst", "api/psyplot.sphinxext.rst", "api/psyplot.sphinxext.extended_napoleon.rst", "api/psyplot.utils.rst", "api/psyplot.warning.rst", "changelog.rst", "command_line.rst", "configuration.rst", "contributing.rst", "develop/framework.rst", "develop/index.rst", "develop/plugins_guide.rst", "getting_started.rst", "index.rst", "installing.rst", "plugins.rst", "projects.rst", "todos.rst"], "indexentries": {"absolutetimedecoder (class in psyplot.data)": [[7, "psyplot.data.AbsoluteTimeDecoder", false]], "absolutetimeencoder (class in psyplot.data)": [[7, "psyplot.data.AbsoluteTimeEncoder", false]], "add_base_str() (psyplot.config.rcsetup.subdict method)": [[6, "psyplot.config.rcsetup.SubDict.add_base_str", false]], "all_dims (psyplot.data.arraylist property)": [[7, "psyplot.data.ArrayList.all_dims", false]], "all_names (psyplot.data.arraylist property)": [[7, "psyplot.data.ArrayList.all_names", false]], "any_decoder (psyplot.plotter.formatoption property)": [[10, "psyplot.plotter.Formatoption.any_decoder", false]], "append() (psyplot.data.arraylist method)": [[7, "psyplot.data.ArrayList.append", false]], "append() (psyplot.data.interactivelist method)": [[7, "psyplot.data.InteractiveList.append", false]], "append() (psyplot.project.project method)": [[11, "psyplot.project.Project.append", false]], "append_original_doc() (in module psyplot.docstring)": [[8, "psyplot.docstring.append_original_doc", false]], "arr_name (psyplot.data.interactivebase property)": [[7, "psyplot.data.InteractiveBase.arr_name", false]], "arr_names (psyplot.data.arraylist property)": [[7, "psyplot.data.ArrayList.arr_names", false]], "arr_names (psyplot.project.project property)": [[11, "psyplot.project.Project.arr_names", false]], "array_info() (psyplot.data.arraylist method)": [[7, "psyplot.data.ArrayList.array_info", false]], "arraylist (class in psyplot.data)": [[7, "psyplot.data.ArrayList", false]], "arrays (psyplot.data.arraylist property)": [[7, "psyplot.data.ArrayList.arrays", false]], "ax (psyplot.data.interactivebase property)": [[7, "psyplot.data.InteractiveBase.ax", false]], "ax (psyplot.plotter.formatoption property)": [[10, "psyplot.plotter.Formatoption.ax", false]], "ax (psyplot.plotter.plotter property)": [[10, "psyplot.plotter.Plotter.ax", false]], "axes (psyplot.project.project property)": [[11, "psyplot.project.Project.axes", false]], "barplot (psyplot.project.dataarrayplotter attribute)": [[11, "psyplot.project.DataArrayPlotter.barplot", false]], "barplot (psyplot.project.datasetplotter attribute)": [[11, "psyplot.project.DatasetPlotter.barplot", false]], "barplot (psyplot.project.project property)": [[11, "psyplot.project.Project.barplot", false]], "barplot (psyplot.project.projectplotter attribute)": [[11, "psyplot.project.ProjectPlotter.barplot", false]], "base (psyplot.config.rcsetup.subdict attribute)": [[6, "psyplot.config.rcsetup.SubDict.base", false]], "base (psyplot.data.interactivearray property)": [[7, "psyplot.data.InteractiveArray.base", false]], "base_str (psyplot.config.rcsetup.subdict attribute)": [[6, "psyplot.config.rcsetup.SubDict.base_str", false]], "base_variables (psyplot.data.interactivearray property)": [[7, "psyplot.data.InteractiveArray.base_variables", false]], "base_variables (psyplot.plotter.plotter property)": [[10, "psyplot.plotter.Plotter.base_variables", false]], "beforeplotting (in module psyplot.plotter)": [[10, "psyplot.plotter.BEFOREPLOTTING", false]], "block_signals (psyplot.data.interactivebase property)": [[7, "psyplot.data.InteractiveBase.block_signals", false]], "block_signals (psyplot.project.project attribute)": [[11, "psyplot.project.Project.block_signals", false]], "can_decode() (psyplot.data.cfdecoder class method)": [[7, "psyplot.data.CFDecoder.can_decode", false]], "can_decode() (psyplot.data.ugriddecoder class method)": [[7, "psyplot.data.UGridDecoder.can_decode", false]], "catch() (psyplot.config.rcsetup.rcparams method)": [[6, "psyplot.config.rcsetup.RcParams.catch", false]], "cfdecoder (class in psyplot.data)": [[7, "psyplot.data.CFDecoder", false]], "changed (psyplot.plotter.formatoption property)": [[10, "psyplot.plotter.Formatoption.changed", false]], "changed (psyplot.plotter.plotter property)": [[10, "psyplot.plotter.Plotter.changed", false]], "check_and_set() (psyplot.plotter.formatoption method)": [[10, "psyplot.plotter.Formatoption.check_and_set", false]], "check_data() (psyplot.plotter.plotter class method)": [[10, "psyplot.plotter.Plotter.check_data", false]], "check_data() (psyplot.project.dataarrayplotterinterface method)": [[11, "psyplot.project.DataArrayPlotterInterface.check_data", false]], "check_data() (psyplot.project.plotterinterface method)": [[11, "psyplot.project.PlotterInterface.check_data", false]], "check_key() (in module psyplot.utils)": [[14, "psyplot.utils.check_key", false]], "check_key() (psyplot.plotter.plotter method)": [[10, "psyplot.plotter.Plotter.check_key", false]], "children (psyplot.plotter.formatoption attribute)": [[10, "psyplot.plotter.Formatoption.children", false]], "children (psyplot.plotter.postprocessing attribute)": [[10, "psyplot.plotter.PostProcessing.children", false]], "clear_cache() (psyplot.data.cfdecoder method)": [[7, "psyplot.data.CFDecoder.clear_cache", false]], "close() (in module psyplot.project)": [[11, "psyplot.project.close", false]], "close() (psyplot.project.project method)": [[11, "psyplot.project.Project.close", false]], "combined (psyplot.project.dataarrayplotter attribute)": [[11, "psyplot.project.DataArrayPlotter.combined", false]], "combined (psyplot.project.datasetplotter attribute)": [[11, "psyplot.project.DatasetPlotter.combined", false]], "combined (psyplot.project.project property)": [[11, "psyplot.project.Project.combined", false]], "combined (psyplot.project.projectplotter attribute)": [[11, "psyplot.project.ProjectPlotter.combined", false]], "config_path (in module psyplot.config)": [[4, "psyplot.config.config_path", false]], "connect() (psyplot.config.rcsetup.rcparams method)": [[6, "psyplot.config.rcsetup.RcParams.connect", false]], "connect() (psyplot.data.signal method)": [[7, "psyplot.data.Signal.connect", false]], "connections (psyplot.plotter.formatoption attribute)": [[10, "psyplot.plotter.Formatoption.connections", false]], "convert_coordinate() (psyplot.plotter.formatoption method)": [[10, "psyplot.plotter.Formatoption.convert_coordinate", false]], "convert_coordinate() (psyplot.plotter.plotter method)": [[10, "psyplot.plotter.Plotter.convert_coordinate", false]], "coords (psyplot.data.arraylist property)": [[7, "psyplot.data.ArrayList.coords", false]], "coords_intersect (psyplot.data.arraylist property)": [[7, "psyplot.data.ArrayList.coords_intersect", false]], "copy() (psyplot.config.rcsetup.rcparams method)": [[6, "psyplot.config.rcsetup.RcParams.copy", false]], "copy() (psyplot.data.arraylist method)": [[7, "psyplot.data.ArrayList.copy", false]], "copy() (psyplot.data.datasetaccessor method)": [[7, "psyplot.data.DatasetAccessor.copy", false]], "copy() (psyplot.data.interactivearray method)": [[7, "psyplot.data.InteractiveArray.copy", false]], "copy() (psyplot.utils.defaultdict method)": [[14, "psyplot.utils.Defaultdict.copy", false]], "correct_dims() (psyplot.data.cfdecoder method)": [[7, "psyplot.data.CFDecoder.correct_dims", false]], "create_list() (psyplot.data.datasetaccessor method)": [[7, "psyplot.data.DatasetAccessor.create_list", false]], "critical() (in module psyplot.warning)": [[15, "psyplot.warning.critical", false]], "customwarn() (in module psyplot.warning)": [[15, "psyplot.warning.customwarn", false]], "data (psyplot.config.rcsetup.subdict property)": [[6, "psyplot.config.rcsetup.SubDict.data", false]], "data (psyplot.plotter.formatoption property)": [[10, "psyplot.plotter.Formatoption.data", false]], "data (psyplot.plotter.plotter property)": [[10, "psyplot.plotter.Plotter.data", false]], "data_dependent (psyplot.plotter.formatoption attribute)": [[10, "psyplot.plotter.Formatoption.data_dependent", false]], "data_dependent (psyplot.plotter.postprocessing property)": [[10, "psyplot.plotter.PostProcessing.data_dependent", false]], "data_store (psyplot.data.datasetaccessor property)": [[7, "psyplot.data.DatasetAccessor.data_store", false]], "dataarrayplotter (class in psyplot.project)": [[11, "psyplot.project.DataArrayPlotter", false]], "dataarrayplotterinterface (class in psyplot.project)": [[11, "psyplot.project.DataArrayPlotterInterface", false]], "datasetaccessor (class in psyplot.data)": [[7, "psyplot.data.DatasetAccessor", false]], "datasetplotter (class in psyplot.project)": [[11, "psyplot.project.DatasetPlotter", false]], "datasetplotterinterface (class in psyplot.project)": [[11, "psyplot.project.DatasetPlotterInterface", false]], "datasets (psyplot.project.project property)": [[11, "psyplot.project.Project.datasets", false]], "decode_absolute_time() (in module psyplot.data)": [[7, "psyplot.data.decode_absolute_time", false]], "decode_coords() (psyplot.data.cfdecoder static method)": [[7, "psyplot.data.CFDecoder.decode_coords", false]], "decode_coords() (psyplot.data.ugriddecoder static method)": [[7, "psyplot.data.UGridDecoder.decode_coords", false]], "decode_ds() (psyplot.data.cfdecoder class method)": [[7, "psyplot.data.CFDecoder.decode_ds", false]], "decoder (psyplot.data.interactivearray property)": [[7, "psyplot.data.InteractiveArray.decoder", false]], "decoder (psyplot.plotter.formatoption property)": [[10, "psyplot.plotter.Formatoption.decoder", false]], "dedent() (in module psyplot.docstring)": [[8, "psyplot.docstring.dedent", false]], "default (psyplot.plotter.formatoption property)": [[10, "psyplot.plotter.Formatoption.default", false]], "default (psyplot.plotter.postprocessing attribute)": [[10, "psyplot.plotter.PostProcessing.default", false]], "default (psyplot.plotter.posttiming attribute)": [[10, "psyplot.plotter.PostTiming.default", false]], "default_key (psyplot.plotter.formatoption property)": [[10, "psyplot.plotter.Formatoption.default_key", false]], "default_print_func() (in module psyplot.plotter)": [[10, "psyplot.plotter.default_print_func", false]], "defaultdict (class in psyplot.utils)": [[14, "psyplot.utils.Defaultdict", false]], "defaultparams (in module psyplot.config.rcsetup)": [[6, "psyplot.config.rcsetup.defaultParams", false]], "defaultparams (psyplot.config.rcsetup.rcparams property)": [[6, "psyplot.config.rcsetup.RcParams.defaultParams", false]], "density (psyplot.project.dataarrayplotter attribute)": [[11, "psyplot.project.DataArrayPlotter.density", false]], "density (psyplot.project.datasetplotter attribute)": [[11, "psyplot.project.DatasetPlotter.density", false]], "density (psyplot.project.project property)": [[11, "psyplot.project.Project.density", false]], "density (psyplot.project.projectplotter attribute)": [[11, "psyplot.project.ProjectPlotter.density", false]], "dependencies (psyplot.plotter.formatoption attribute)": [[10, "psyplot.plotter.Formatoption.dependencies", false]], "dependencies (psyplot.plotter.postprocessing attribute)": [[10, "psyplot.plotter.PostProcessing.dependencies", false]], "descriptions (psyplot.config.rcsetup.rcparams property)": [[6, "psyplot.config.rcsetup.RcParams.descriptions", false]], "dictformatoption (class in psyplot.plotter)": [[10, "psyplot.plotter.DictFormatoption", false]], "diff() (psyplot.plotter.formatoption method)": [[10, "psyplot.plotter.Formatoption.diff", false]], "dims (psyplot.data.arraylist property)": [[7, "psyplot.data.ArrayList.dims", false]], "dims_intersect (psyplot.data.arraylist property)": [[7, "psyplot.data.ArrayList.dims_intersect", false]], "disable() (psyplot.project.project method)": [[11, "psyplot.project.Project.disable", false]], "disable_warnings() (in module psyplot.warning)": [[15, "psyplot.warning.disable_warnings", false]], "disconnect() (psyplot.config.rcsetup.rcparams method)": [[6, "psyplot.config.rcsetup.RcParams.disconnect", false]], "disconnect() (psyplot.data.signal method)": [[7, "psyplot.data.Signal.disconnect", false]], "docs() (psyplot.project.plotterinterface method)": [[11, "psyplot.project.PlotterInterface.docs", false]], "docs() (psyplot.project.project method)": [[11, "psyplot.project.Project.docs", false]], "docstringextension (class in psyplot.sphinxext.extended_napoleon)": [[13, "psyplot.sphinxext.extended_napoleon.DocstringExtension", false]], "docstrings (in module psyplot.docstring)": [[8, "psyplot.docstring.docstrings", false]], "draw() (psyplot.data.arraylist method)": [[7, "psyplot.data.ArrayList.draw", false]], "draw() (psyplot.plotter.plotter method)": [[10, "psyplot.plotter.Plotter.draw", false]], "dsnames (psyplot.project.project property)": [[11, "psyplot.project.Project.dsnames", false]], "dsnames_map (psyplot.project.project property)": [[11, "psyplot.project.Project.dsnames_map", false]], "dtype (psyplot.data.absolutetimedecoder property)": [[7, "psyplot.data.AbsoluteTimeDecoder.dtype", false]], "dtype (psyplot.data.absolutetimeencoder property)": [[7, "psyplot.data.AbsoluteTimeEncoder.dtype", false]], "dump() (psyplot.config.rcsetup.rcparams method)": [[6, "psyplot.config.rcsetup.RcParams.dump", false]], "emit() (psyplot.data.signal method)": [[7, "psyplot.data.Signal.emit", false]], "enable() (psyplot.project.project method)": [[11, "psyplot.project.Project.enable", false]], "enable_post (psyplot.plotter.plotter attribute)": [[10, "psyplot.plotter.Plotter.enable_post", false]], "encode_absolute_time() (in module psyplot.data)": [[7, "psyplot.data.encode_absolute_time", false]], "end (in module psyplot.plotter)": [[10, "psyplot.plotter.END", false]], "export() (psyplot.project.project method)": [[11, "psyplot.project.Project.export", false]], "extend() (psyplot.data.arraylist method)": [[7, "psyplot.data.ArrayList.extend", false]], "extend() (psyplot.data.interactivelist method)": [[7, "psyplot.data.InteractiveList.extend", false]], "extend() (psyplot.project.project method)": [[11, "psyplot.project.Project.extend", false]], "extendedgoogledocstring (class in psyplot.sphinxext.extended_napoleon)": [[13, "psyplot.sphinxext.extended_napoleon.ExtendedGoogleDocstring", false]], "extendednumpydocstring (class in psyplot.sphinxext.extended_napoleon)": [[13, "psyplot.sphinxext.extended_napoleon.ExtendedNumpyDocstring", false]], "extract_fmts_from_preset() (psyplot.project.project static method)": [[11, "psyplot.project.Project.extract_fmts_from_preset", false]], "figs (psyplot.project.project property)": [[11, "psyplot.project.Project.figs", false]], "figs2draw (psyplot.plotter.plotter property)": [[10, "psyplot.plotter.Plotter.figs2draw", false]], "filename (psyplot.data.datasetaccessor property)": [[7, "psyplot.data.DatasetAccessor.filename", false]], "find_all() (psyplot.config.rcsetup.rcparams method)": [[6, "psyplot.config.rcsetup.RcParams.find_all", false]], "find_and_replace() (psyplot.config.rcsetup.rcparams method)": [[6, "psyplot.config.rcsetup.RcParams.find_and_replace", false]], "finish_update() (psyplot.plotter.formatoption method)": [[10, "psyplot.plotter.Formatoption.finish_update", false]], "fldmean (psyplot.project.dataarrayplotter attribute)": [[11, "psyplot.project.DataArrayPlotter.fldmean", false]], "fldmean (psyplot.project.datasetplotter attribute)": [[11, "psyplot.project.DatasetPlotter.fldmean", false]], "fldmean (psyplot.project.project property)": [[11, "psyplot.project.Project.fldmean", false]], "fldmean (psyplot.project.projectplotter attribute)": [[11, "psyplot.project.ProjectPlotter.fldmean", false]], "fldmean() (psyplot.data.interactivearray method)": [[7, "psyplot.data.InteractiveArray.fldmean", false]], "fldpctl() (psyplot.data.interactivearray method)": [[7, "psyplot.data.InteractiveArray.fldpctl", false]], "fldstd() (psyplot.data.interactivearray method)": [[7, "psyplot.data.InteractiveArray.fldstd", false]], "fmt_groups (psyplot.plotter.plotter property)": [[10, "psyplot.plotter.Plotter.fmt_groups", false]], "format_string() (psyplot.project.project method)": [[11, "psyplot.project.Project.format_string", false]], "format_time() (in module psyplot.plotter)": [[10, "psyplot.plotter.format_time", false]], "formatoption (class in psyplot.plotter)": [[10, "psyplot.plotter.Formatoption", false]], "formatoptionmeta (class in psyplot.plotter)": [[10, "psyplot.plotter.FormatoptionMeta", false]], "from_dataset() (psyplot.data.arraylist class method)": [[7, "psyplot.data.ArrayList.from_dataset", false]], "from_dataset() (psyplot.data.interactivelist class method)": [[7, "psyplot.data.InteractiveList.from_dataset", false]], "from_dataset() (psyplot.project.project class method)": [[11, "psyplot.project.Project.from_dataset", false]], "from_dict() (psyplot.data.arraylist class method)": [[7, "psyplot.data.ArrayList.from_dict", false]], "gcp() (in module psyplot.project)": [[11, "psyplot.project.gcp", false]], "gdalstore (class in psyplot.gdal_store)": [[9, "psyplot.gdal_store.GdalStore", false]], "get_attrs() (psyplot.gdal_store.gdalstore method)": [[9, "psyplot.gdal_store.GdalStore.get_attrs", false]], "get_cell_node_coord() (psyplot.data.cfdecoder method)": [[7, "psyplot.data.CFDecoder.get_cell_node_coord", false]], "get_cell_node_coord() (psyplot.data.ugriddecoder method)": [[7, "psyplot.data.UGridDecoder.get_cell_node_coord", false]], "get_configdir() (in module psyplot.config.rcsetup)": [[6, "psyplot.config.rcsetup.get_configdir", false]], "get_coord() (psyplot.data.interactivearray method)": [[7, "psyplot.data.InteractiveArray.get_coord", false]], "get_coord_idims() (psyplot.data.cfdecoder method)": [[7, "psyplot.data.CFDecoder.get_coord_idims", false]], "get_coord_info() (psyplot.data.cfdecoder method)": [[7, "psyplot.data.CFDecoder.get_coord_info", false]], "get_decoder() (psyplot.data.cfdecoder class method)": [[7, "psyplot.data.CFDecoder.get_decoder", false]], "get_decoder() (psyplot.plotter.formatoption method)": [[10, "psyplot.plotter.Formatoption.get_decoder", false]], "get_default_value() (in module psyplot.utils)": [[14, "psyplot.utils.get_default_value", false]], "get_dim() (psyplot.data.interactivearray method)": [[7, "psyplot.data.InteractiveArray.get_dim", false]], "get_enhanced_attrs() (psyplot.plotter.plotter method)": [[10, "psyplot.plotter.Plotter.get_enhanced_attrs", false]], "get_filename_ds() (in module psyplot.data)": [[7, "psyplot.data.get_filename_ds", false]], "get_fmt_widget() (psyplot.plotter.formatoption method)": [[10, "psyplot.plotter.Formatoption.get_fmt_widget", false]], "get_fmt_widget() (psyplot.plotter.posttiming method)": [[10, "psyplot.plotter.PostTiming.get_fmt_widget", false]], "get_fname_funcs (in module psyplot.data)": [[7, "psyplot.data.get_fname_funcs", false]], "get_grid_type_info() (psyplot.data.cfdecoder method)": [[7, "psyplot.data.CFDecoder.get_grid_type_info", false]], "get_idims() (psyplot.data.cfdecoder method)": [[7, "psyplot.data.CFDecoder.get_idims", false]], "get_index_from_coord() (in module psyplot.data)": [[7, "psyplot.data.get_index_from_coord", false]], "get_mesh() (psyplot.data.ugriddecoder method)": [[7, "psyplot.data.UGridDecoder.get_mesh", false]], "get_metadata_for_section() (psyplot.data.cfdecoder method)": [[7, "psyplot.data.CFDecoder.get_metadata_for_section", false]], "get_metadata_for_variable() (psyplot.data.cfdecoder method)": [[7, "psyplot.data.CFDecoder.get_metadata_for_variable", false]], "get_metadata_sections() (psyplot.data.cfdecoder method)": [[7, "psyplot.data.CFDecoder.get_metadata_sections", false]], "get_nodes() (psyplot.data.ugriddecoder method)": [[7, "psyplot.data.UGridDecoder.get_nodes", false]], "get_plotbounds() (psyplot.data.cfdecoder method)": [[7, "psyplot.data.CFDecoder.get_plotbounds", false]], "get_project_nums() (in module psyplot.project)": [[11, "psyplot.project.get_project_nums", false]], "get_projection_info() (psyplot.data.cfdecoder method)": [[7, "psyplot.data.CFDecoder.get_projection_info", false]], "get_sections() (psyplot.docstring.psyplotdocstringprocessor method)": [[8, "psyplot.docstring.PsyplotDocstringProcessor.get_sections", false]], "get_t() (psyplot.data.cfdecoder method)": [[7, "psyplot.data.CFDecoder.get_t", false]], "get_t_metadata() (psyplot.data.cfdecoder method)": [[7, "psyplot.data.CFDecoder.get_t_metadata", false]], "get_tdata() (in module psyplot.data)": [[7, "psyplot.data.get_tdata", false]], "get_tname() (psyplot.data.cfdecoder method)": [[7, "psyplot.data.CFDecoder.get_tname", false]], "get_triangles() (psyplot.data.cfdecoder method)": [[7, "psyplot.data.CFDecoder.get_triangles", false]], "get_triangles() (psyplot.data.ugriddecoder method)": [[7, "psyplot.data.UGridDecoder.get_triangles", false]], "get_variable_by_axis() (psyplot.data.cfdecoder method)": [[7, "psyplot.data.CFDecoder.get_variable_by_axis", false]], "get_variables() (psyplot.gdal_store.gdalstore method)": [[9, "psyplot.gdal_store.GdalStore.get_variables", false]], "get_versions() (in module psyplot)": [[3, "psyplot.get_versions", false]], "get_vfunc() (psyplot.plotter.plotter method)": [[10, "psyplot.plotter.Plotter.get_vfunc", false]], "get_x() (psyplot.data.cfdecoder method)": [[7, "psyplot.data.CFDecoder.get_x", false]], "get_x() (psyplot.data.ugriddecoder method)": [[7, "psyplot.data.UGridDecoder.get_x", false]], "get_x_metadata() (psyplot.data.cfdecoder method)": [[7, "psyplot.data.CFDecoder.get_x_metadata", false]], "get_xname() (psyplot.data.cfdecoder method)": [[7, "psyplot.data.CFDecoder.get_xname", false]], "get_y() (psyplot.data.cfdecoder method)": [[7, "psyplot.data.CFDecoder.get_y", false]], "get_y() (psyplot.data.ugriddecoder method)": [[7, "psyplot.data.UGridDecoder.get_y", false]], "get_y_metadata() (psyplot.data.cfdecoder method)": [[7, "psyplot.data.CFDecoder.get_y_metadata", false]], "get_yname() (psyplot.data.cfdecoder method)": [[7, "psyplot.data.CFDecoder.get_yname", false]], "get_z() (psyplot.data.cfdecoder method)": [[7, "psyplot.data.CFDecoder.get_z", false]], "get_z_metadata() (psyplot.data.cfdecoder method)": [[7, "psyplot.data.CFDecoder.get_z_metadata", false]], "get_zname() (psyplot.data.cfdecoder method)": [[7, "psyplot.data.CFDecoder.get_zname", false]], "gridweights() (psyplot.data.interactivearray method)": [[7, "psyplot.data.InteractiveArray.gridweights", false]], "group (psyplot.plotter.formatoption attribute)": [[10, "psyplot.plotter.Formatoption.group", false]], "group (psyplot.plotter.postprocessing attribute)": [[10, "psyplot.plotter.PostProcessing.group", false]], "group (psyplot.plotter.posttiming attribute)": [[10, "psyplot.plotter.PostTiming.group", false]], "groupname (psyplot.plotter.formatoption property)": [[10, "psyplot.plotter.Formatoption.groupname", false]], "groups (in module psyplot.plotter)": [[10, "psyplot.plotter.groups", false]], "groups (psyplot.plotter.plotter property)": [[10, "psyplot.plotter.Plotter.groups", false]], "has_changed() (psyplot.plotter.plotter method)": [[10, "psyplot.plotter.Plotter.has_changed", false]], "hashable() (in module psyplot.utils)": [[14, "psyplot.utils.hashable", false]], "header (psyplot.config.rcsetup.rcparams attribute)": [[6, "psyplot.config.rcsetup.RcParams.HEADER", false]], "idims (psyplot.data.interactivearray property)": [[7, "psyplot.data.InteractiveArray.idims", false]], "include_links (psyplot.plotter.plotter attribute)": [[10, "psyplot.plotter.Plotter.include_links", false]], "indent() (in module psyplot.docstring)": [[8, "psyplot.docstring.indent", false]], "index_in_list (psyplot.plotter.formatoption attribute)": [[10, "psyplot.plotter.Formatoption.index_in_list", false]], "init_accessor() (psyplot.data.interactivearray method)": [[7, "psyplot.data.InteractiveArray.init_accessor", false]], "init_kwargs (psyplot.plotter.formatoption property)": [[10, "psyplot.plotter.Formatoption.init_kwargs", false]], "initialize_plot() (psyplot.plotter.formatoption method)": [[10, "psyplot.plotter.Formatoption.initialize_plot", false]], "initialize_plot() (psyplot.plotter.plotter method)": [[10, "psyplot.plotter.Plotter.initialize_plot", false]], "instance (psyplot.data.signal attribute)": [[7, "psyplot.data.Signal.instance", false]], "interactivearray (class in psyplot.data)": [[7, "psyplot.data.InteractiveArray", false]], "interactivebase (class in psyplot.data)": [[7, "psyplot.data.InteractiveBase", false]], "interactivelist (class in psyplot.data)": [[7, "psyplot.data.InteractiveList", false]], "is_circumpolar() (psyplot.data.cfdecoder method)": [[7, "psyplot.data.CFDecoder.is_circumpolar", false]], "is_cmp (psyplot.project.project property)": [[11, "psyplot.project.Project.is_cmp", false]], "is_csp (psyplot.project.project property)": [[11, "psyplot.project.Project.is_csp", false]], "is_data_dependent() (in module psyplot.plotter)": [[10, "psyplot.plotter.is_data_dependent", false]], "is_imported (psyplot.project.plotterinterface property)": [[11, "psyplot.project.PlotterInterface.is_imported", false]], "is_iterable() (in module psyplot.utils)": [[14, "psyplot.utils.is_iterable", false]], "is_main (psyplot.project.project property)": [[11, "psyplot.project.Project.is_main", false]], "is_remote_url() (in module psyplot.utils)": [[14, "psyplot.utils.is_remote_url", false]], "is_unstructured (psyplot.data.arraylist property)": [[7, "psyplot.data.ArrayList.is_unstructured", false]], "is_unstructured() (psyplot.data.cfdecoder method)": [[7, "psyplot.data.CFDecoder.is_unstructured", false]], "is_unstructured() (psyplot.data.ugriddecoder method)": [[7, "psyplot.data.UGridDecoder.is_unstructured", false]], "isel() (psyplot.data.interactivearray method)": [[7, "psyplot.data.InteractiveArray.isel", false]], "isstring() (in module psyplot.utils)": [[14, "psyplot.utils.isstring", false]], "iter_base_variables (psyplot.data.interactivearray property)": [[7, "psyplot.data.InteractiveArray.iter_base_variables", false]], "iter_base_variables (psyplot.plotter.plotter property)": [[10, "psyplot.plotter.Plotter.iter_base_variables", false]], "iter_data (psyplot.plotter.formatoption property)": [[10, "psyplot.plotter.Formatoption.iter_data", false]], "iter_raw_data (psyplot.plotter.formatoption property)": [[10, "psyplot.plotter.Formatoption.iter_raw_data", false]], "iteritems() (psyplot.config.rcsetup.subdict method)": [[6, "psyplot.config.rcsetup.SubDict.iteritems", false]], "iterkeys() (psyplot.config.rcsetup.subdict method)": [[6, "psyplot.config.rcsetup.SubDict.iterkeys", false]], "itervalues() (psyplot.config.rcsetup.subdict method)": [[6, "psyplot.config.rcsetup.SubDict.itervalues", false]], "join_dicts() (in module psyplot.utils)": [[14, "psyplot.utils.join_dicts", false]], "joined_attrs() (psyplot.project.project method)": [[11, "psyplot.project.Project.joined_attrs", false]], "key (psyplot.plotter.formatoption attribute)": [[10, "psyplot.plotter.Formatoption.key", false]], "keys() (psyplot.config.rcsetup.rcparams method)": [[6, "psyplot.config.rcsetup.RcParams.keys", false]], "keys() (psyplot.project.plotterinterface method)": [[11, "psyplot.project.PlotterInterface.keys", false]], "keys() (psyplot.project.project method)": [[11, "psyplot.project.Project.keys", false]], "lineplot (psyplot.project.dataarrayplotter attribute)": [[11, "psyplot.project.DataArrayPlotter.lineplot", false]], "lineplot (psyplot.project.datasetplotter attribute)": [[11, "psyplot.project.DatasetPlotter.lineplot", false]], "lineplot (psyplot.project.project property)": [[11, "psyplot.project.Project.lineplot", false]], "lineplot (psyplot.project.projectplotter attribute)": [[11, "psyplot.project.ProjectPlotter.lineplot", false]], "load_from_file() (psyplot.config.rcsetup.rcparams method)": [[6, "psyplot.config.rcsetup.RcParams.load_from_file", false]], "load_plugins() (psyplot.config.rcsetup.rcparams method)": [[6, "psyplot.config.rcsetup.RcParams.load_plugins", false]], "load_preset() (psyplot.project.project method)": [[11, "psyplot.project.Project.load_preset", false]], "load_project() (psyplot.project.project class method)": [[11, "psyplot.project.Project.load_project", false]], "lock (psyplot.plotter.formatoption property)": [[10, "psyplot.plotter.Formatoption.lock", false]], "logcfg_path (in module psyplot.config)": [[4, "psyplot.config.logcfg_path", false]], "logger (psyplot.data.arraylist property)": [[7, "psyplot.data.ArrayList.logger", false]], "logger (psyplot.data.cfdecoder property)": [[7, "psyplot.data.CFDecoder.logger", false]], "logger (psyplot.data.interactivearray property)": [[7, "psyplot.data.InteractiveArray.logger", false]], "logger (psyplot.data.interactivebase property)": [[7, "psyplot.data.InteractiveBase.logger", false]], "logger (psyplot.data.interactivelist property)": [[7, "psyplot.data.InteractiveList.logger", false]], "logger (psyplot.plotter.formatoption property)": [[10, "psyplot.plotter.Formatoption.logger", false]], "logger (psyplot.plotter.plotter property)": [[10, "psyplot.plotter.Plotter.logger", false]], "logger (psyplot.project.project property)": [[11, "psyplot.project.Project.logger", false]], "main (psyplot.project.project property)": [[11, "psyplot.project.Project.main", false]], "make_plot() (psyplot.plotter.plotter method)": [[10, "psyplot.plotter.Plotter.make_plot", false]], "mapcombined (psyplot.project.dataarrayplotter attribute)": [[11, "psyplot.project.DataArrayPlotter.mapcombined", false]], "mapcombined (psyplot.project.datasetplotter attribute)": [[11, "psyplot.project.DatasetPlotter.mapcombined", false]], "mapcombined (psyplot.project.project property)": [[11, "psyplot.project.Project.mapcombined", false]], "mapcombined (psyplot.project.projectplotter attribute)": [[11, "psyplot.project.ProjectPlotter.mapcombined", false]], "mapplot (psyplot.project.dataarrayplotter attribute)": [[11, "psyplot.project.DataArrayPlotter.mapplot", false]], "mapplot (psyplot.project.datasetplotter attribute)": [[11, "psyplot.project.DatasetPlotter.mapplot", false]], "mapplot (psyplot.project.project property)": [[11, "psyplot.project.Project.mapplot", false]], "mapplot (psyplot.project.projectplotter attribute)": [[11, "psyplot.project.ProjectPlotter.mapplot", false]], "maps (psyplot.project.project property)": [[11, "psyplot.project.Project.maps", false]], "mapvector (psyplot.project.dataarrayplotter attribute)": [[11, "psyplot.project.DataArrayPlotter.mapvector", false]], "mapvector (psyplot.project.datasetplotter attribute)": [[11, "psyplot.project.DatasetPlotter.mapvector", false]], "mapvector (psyplot.project.project property)": [[11, "psyplot.project.Project.mapvector", false]], "mapvector (psyplot.project.projectplotter attribute)": [[11, "psyplot.project.ProjectPlotter.mapvector", false]], "module": [[3, "module-psyplot", false], [4, "module-psyplot.config", false], [5, "module-psyplot.config.logsetup", false], [6, "module-psyplot.config.rcsetup", false], [7, "module-psyplot.data", false], [8, "module-psyplot.docstring", false], [9, "module-psyplot.gdal_store", false], [10, "module-psyplot.plotter", false], [11, "module-psyplot.project", false], [12, "module-psyplot.sphinxext", false], [13, "module-psyplot.sphinxext.extended_napoleon", false], [14, "module-psyplot.utils", false], [15, "module-psyplot.warning", false]], "msg_depr (psyplot.config.rcsetup.rcparams attribute)": [[6, "psyplot.config.rcsetup.RcParams.msg_depr", false]], "msg_depr_ignore (psyplot.config.rcsetup.rcparams attribute)": [[6, "psyplot.config.rcsetup.RcParams.msg_depr_ignore", false]], "multiple_subplots() (in module psyplot.project)": [[11, "psyplot.project.multiple_subplots", false]], "name (psyplot.plotter.formatoption attribute)": [[10, "psyplot.plotter.Formatoption.name", false]], "name (psyplot.plotter.postprocessing attribute)": [[10, "psyplot.plotter.PostProcessing.name", false]], "name (psyplot.plotter.posttiming attribute)": [[10, "psyplot.plotter.PostTiming.name", false]], "names (psyplot.data.arraylist property)": [[7, "psyplot.data.ArrayList.names", false]], "new() (psyplot.project.project class method)": [[11, "psyplot.project.Project.new", false]], "next_available_name() (psyplot.data.arraylist method)": [[7, "psyplot.data.ArrayList.next_available_name", false]], "no_auto_update (psyplot.data.arraylist property)": [[7, "psyplot.data.ArrayList.no_auto_update", false]], "no_auto_update (psyplot.data.interactivebase property)": [[7, "psyplot.data.InteractiveBase.no_auto_update", false]], "no_auto_update (psyplot.data.interactivelist property)": [[7, "psyplot.data.InteractiveList.no_auto_update", false]], "no_auto_update (psyplot.plotter.plotter property)": [[10, "psyplot.plotter.Plotter.no_auto_update", false]], "no_validation (psyplot.plotter.plotter property)": [[10, "psyplot.plotter.Plotter.no_validation", false]], "num (psyplot.data.datasetaccessor property)": [[7, "psyplot.data.DatasetAccessor.num", false]], "onbasechange (psyplot.data.interactivearray attribute)": [[7, "psyplot.data.InteractiveArray.onbasechange", false]], "oncpchange (psyplot.project.project attribute)": [[11, "psyplot.project.Project.oncpchange", false]], "onupdate (psyplot.data.interactivebase attribute)": [[7, "psyplot.data.InteractiveBase.onupdate", false]], "open_dataset() (in module psyplot.data)": [[7, "psyplot.data.open_dataset", false]], "open_mfdataset() (in module psyplot.data)": [[7, "psyplot.data.open_mfdataset", false]], "owner (psyplot.data.signal attribute)": [[7, "psyplot.data.Signal.owner", false]], "param_like_sections (psyplot.docstring.psyplotdocstringprocessor attribute)": [[8, "psyplot.docstring.PsyplotDocstringProcessor.param_like_sections", false]], "parents (psyplot.plotter.formatoption attribute)": [[10, "psyplot.plotter.Formatoption.parents", false]], "patterns (psyplot.config.rcsetup.subdict attribute)": [[6, "psyplot.config.rcsetup.SubDict.patterns", false]], "plot (in module psyplot.project)": [[11, "psyplot.project.plot", false]], "plot (psyplot.data.datasetaccessor property)": [[7, "psyplot.data.DatasetAccessor.plot", false]], "plot (psyplot.data.interactivebase property)": [[7, "psyplot.data.InteractiveBase.plot", false]], "plot (psyplot.project.project property)": [[11, "psyplot.project.Project.plot", false]], "plot2d (psyplot.project.dataarrayplotter attribute)": [[11, "psyplot.project.DataArrayPlotter.plot2d", false]], "plot2d (psyplot.project.datasetplotter attribute)": [[11, "psyplot.project.DatasetPlotter.plot2d", false]], "plot2d (psyplot.project.project property)": [[11, "psyplot.project.Project.plot2d", false]], "plot2d (psyplot.project.projectplotter attribute)": [[11, "psyplot.project.ProjectPlotter.plot2d", false]], "plot_data (psyplot.plotter.plotter property)": [[10, "psyplot.plotter.Plotter.plot_data", false]], "plot_data_decoder (psyplot.plotter.plotter attribute)": [[10, "psyplot.plotter.Plotter.plot_data_decoder", false]], "plot_fmt (psyplot.plotter.formatoption attribute)": [[10, "psyplot.plotter.Formatoption.plot_fmt", false]], "plotter (class in psyplot.plotter)": [[10, "psyplot.plotter.Plotter", false]], "plotter (psyplot.data.interactivebase property)": [[7, "psyplot.data.InteractiveBase.plotter", false]], "plotter (psyplot.plotter.formatoption property)": [[10, "psyplot.plotter.Formatoption.plotter", false]], "plotter_cls (psyplot.project.plotterinterface property)": [[11, "psyplot.project.PlotterInterface.plotter_cls", false]], "plotterinterface (class in psyplot.project)": [[11, "psyplot.project.PlotterInterface", false]], "plotters (psyplot.project.project property)": [[11, "psyplot.project.Project.plotters", false]], "plugin_entrypoints() (in module psyplot.utils)": [[14, "psyplot.utils.plugin_entrypoints", false]], "post (psyplot.plotter.plotter attribute)": [[10, "psyplot.plotter.Plotter.post", false]], "post_timing (psyplot.plotter.plotter attribute)": [[10, "psyplot.plotter.Plotter.post_timing", false]], "post_timing (psyplot.plotter.postprocessing property)": [[10, "psyplot.plotter.PostProcessing.post_timing", false]], "postprocdependencies (class in psyplot.plotter)": [[10, "psyplot.plotter.PostProcDependencies", false]], "postprocessing (class in psyplot.plotter)": [[10, "psyplot.plotter.PostProcessing", false]], "posttiming (class in psyplot.plotter)": [[10, "psyplot.plotter.PostTiming", false]], "print_func (psyplot.project.plotterinterface property)": [[11, "psyplot.project.PlotterInterface.print_func", false]], "priority (psyplot.plotter.formatoption attribute)": [[10, "psyplot.plotter.Formatoption.priority", false]], "priority (psyplot.plotter.postprocessing attribute)": [[10, "psyplot.plotter.PostProcessing.priority", false]], "priority (psyplot.plotter.posttiming attribute)": [[10, "psyplot.plotter.PostTiming.priority", false]], "process_docstring() (in module psyplot.sphinxext.extended_napoleon)": [[13, "psyplot.sphinxext.extended_napoleon.process_docstring", false]], "project (class in psyplot.project)": [[11, "psyplot.project.Project", false]], "project (psyplot.plotter.formatoption property)": [[10, "psyplot.plotter.Formatoption.project", false]], "project (psyplot.plotter.plotter property)": [[10, "psyplot.plotter.Plotter.project", false]], "project (psyplot.project.projectplotter property)": [[11, "psyplot.project.ProjectPlotter.project", false]], "project() (in module psyplot.project)": [[11, "psyplot.project.project", false]], "project_cls (in module psyplot.project)": [[11, "psyplot.project.PROJECT_CLS", false]], "projectplotter (class in psyplot.project)": [[11, "psyplot.project.ProjectPlotter", false]], "psy (psyplot.data.interactivelist property)": [[7, "psyplot.data.InteractiveList.psy", false]], "psyplot": [[3, "module-psyplot", false]], "psyplot.config": [[4, "module-psyplot.config", false]], "psyplot.config.logsetup": [[5, "module-psyplot.config.logsetup", false]], "psyplot.config.rcsetup": [[6, "module-psyplot.config.rcsetup", false]], "psyplot.data": [[7, "module-psyplot.data", false]], "psyplot.docstring": [[8, "module-psyplot.docstring", false]], "psyplot.gdal_store": [[9, "module-psyplot.gdal_store", false]], "psyplot.plotter": [[10, "module-psyplot.plotter", false]], "psyplot.project": [[11, "module-psyplot.project", false]], "psyplot.sphinxext": [[12, "module-psyplot.sphinxext", false]], "psyplot.sphinxext.extended_napoleon": [[13, "module-psyplot.sphinxext.extended_napoleon", false]], "psyplot.utils": [[14, "module-psyplot.utils", false]], "psyplot.warning": [[15, "module-psyplot.warning", false]], "psyplot_fname() (in module psyplot.config.rcsetup)": [[6, "psyplot.config.rcsetup.psyplot_fname", false]], "psyplotcritical": [[15, "psyplot.warning.PsyPlotCritical", false]], "psyplotdocstringprocessor (class in psyplot.docstring)": [[8, "psyplot.docstring.PsyplotDocstringProcessor", false]], "psyplotruntimewarning": [[15, "psyplot.warning.PsyPlotRuntimeWarning", false]], "psyplotwarning": [[15, "psyplot.warning.PsyPlotWarning", false]], "raw_data (psyplot.plotter.formatoption property)": [[10, "psyplot.plotter.Formatoption.raw_data", false]], "rc (psyplot.plotter.plotter property)": [[10, "psyplot.plotter.Plotter.rc", false]], "rcparams (class in psyplot.config.rcsetup)": [[6, "psyplot.config.rcsetup.RcParams", false]], "rcparams (in module psyplot.config.rcsetup)": [[6, "psyplot.config.rcsetup.rcParams", false]], "register_decoder() (psyplot.data.cfdecoder static method)": [[7, "psyplot.data.CFDecoder.register_decoder", false]], "register_plotter() (in module psyplot.project)": [[11, "psyplot.project.register_plotter", false]], "reinit() (psyplot.plotter.plotter method)": [[10, "psyplot.plotter.Plotter.reinit", false]], "remove() (psyplot.config.rcsetup.rcparams method)": [[6, "psyplot.config.rcsetup.RcParams.remove", false]], "remove() (psyplot.data.arraylist method)": [[7, "psyplot.data.ArrayList.remove", false]], "remove() (psyplot.plotter.formatoption method)": [[10, "psyplot.plotter.Formatoption.remove", false]], "rename() (psyplot.data.arraylist method)": [[7, "psyplot.data.ArrayList.rename", false]], "replace (psyplot.config.rcsetup.subdict property)": [[6, "psyplot.config.rcsetup.SubDict.replace", false]], "requires_clearing (psyplot.plotter.formatoption attribute)": [[10, "psyplot.plotter.Formatoption.requires_clearing", false]], "requires_replot (psyplot.plotter.formatoption attribute)": [[10, "psyplot.plotter.Formatoption.requires_replot", false]], "safe_list() (in module psyplot.config.rcsetup)": [[6, "psyplot.config.rcsetup.safe_list", false]], "save_preset() (psyplot.project.project method)": [[11, "psyplot.project.Project.save_preset", false]], "save_project() (psyplot.project.project method)": [[11, "psyplot.project.Project.save_project", false]], "scp() (in module psyplot.project)": [[11, "psyplot.project.scp", false]], "scp() (psyplot.project.project class method)": [[11, "psyplot.project.Project.scp", false]], "sel() (psyplot.data.interactivearray method)": [[7, "psyplot.data.InteractiveArray.sel", false]], "set_data() (psyplot.plotter.formatoption method)": [[10, "psyplot.plotter.Formatoption.set_data", false]], "set_decoder() (psyplot.plotter.formatoption method)": [[10, "psyplot.plotter.Formatoption.set_decoder", false]], "set_value() (psyplot.plotter.dictformatoption method)": [[10, "psyplot.plotter.DictFormatoption.set_value", false]], "set_value() (psyplot.plotter.formatoption method)": [[10, "psyplot.plotter.Formatoption.set_value", false]], "setup() (in module psyplot.sphinxext.extended_napoleon)": [[13, "psyplot.sphinxext.extended_napoleon.setup", false]], "setup_coords() (in module psyplot.data)": [[7, "psyplot.data.setup_coords", false]], "setup_logging() (in module psyplot.config.logsetup)": [[5, "psyplot.config.logsetup.setup_logging", false]], "share() (psyplot.plotter.formatoption method)": [[10, "psyplot.plotter.Formatoption.share", false]], "share() (psyplot.plotter.plotter method)": [[10, "psyplot.plotter.Plotter.share", false]], "share() (psyplot.project.project method)": [[11, "psyplot.project.Project.share", false]], "shared (psyplot.plotter.formatoption attribute)": [[10, "psyplot.plotter.Formatoption.shared", false]], "shared_by (psyplot.plotter.formatoption property)": [[10, "psyplot.plotter.Formatoption.shared_by", false]], "shiftlon() (psyplot.data.interactivearray method)": [[7, "psyplot.data.InteractiveArray.shiftlon", false]], "show() (psyplot.plotter.plotter method)": [[10, "psyplot.plotter.Plotter.show", false]], "show() (psyplot.project.project static method)": [[11, "psyplot.project.Project.show", false]], "show_docs() (psyplot.plotter.plotter class method)": [[10, "psyplot.plotter.Plotter.show_docs", false]], "show_keys() (psyplot.plotter.plotter class method)": [[10, "psyplot.plotter.Plotter.show_keys", false]], "show_plot_methods() (psyplot.project.projectplotter method)": [[11, "psyplot.project.ProjectPlotter.show_plot_methods", false]], "show_summaries() (psyplot.plotter.plotter class method)": [[10, "psyplot.plotter.Plotter.show_summaries", false]], "signal (class in psyplot.data)": [[7, "psyplot.data.Signal", false]], "simple (psyplot.project.project property)": [[11, "psyplot.project.Project.simple", false]], "sort_kwargs() (in module psyplot.utils)": [[14, "psyplot.utils.sort_kwargs", false]], "standardize_dims() (psyplot.data.cfdecoder method)": [[7, "psyplot.data.CFDecoder.standardize_dims", false]], "start (in module psyplot.plotter)": [[10, "psyplot.plotter.START", false]], "start_update() (psyplot.data.arraylist method)": [[7, "psyplot.data.ArrayList.start_update", false]], "start_update() (psyplot.data.interactivearray method)": [[7, "psyplot.data.InteractiveArray.start_update", false]], "start_update() (psyplot.data.interactivebase method)": [[7, "psyplot.data.InteractiveBase.start_update", false]], "start_update() (psyplot.data.interactivelist method)": [[7, "psyplot.data.InteractiveList.start_update", false]], "start_update() (psyplot.plotter.plotter method)": [[10, "psyplot.plotter.Plotter.start_update", false]], "subdict (class in psyplot.config.rcsetup)": [[6, "psyplot.config.rcsetup.SubDict", false]], "summaries() (psyplot.project.plotterinterface method)": [[11, "psyplot.project.PlotterInterface.summaries", false]], "summaries() (psyplot.project.project method)": [[11, "psyplot.project.Project.summaries", false]], "supports_spatial_slicing (psyplot.data.cfdecoder attribute)": [[7, "psyplot.data.CFDecoder.supports_spatial_slicing", false]], "supports_spatial_slicing (psyplot.data.ugriddecoder attribute)": [[7, "psyplot.data.UGridDecoder.supports_spatial_slicing", false]], "t_patterns (in module psyplot.data)": [[7, "psyplot.data.t_patterns", false]], "to_array() (psyplot.data.datasetaccessor method)": [[7, "psyplot.data.DatasetAccessor.to_array", false]], "to_dataframe() (psyplot.data.interactivelist method)": [[7, "psyplot.data.InteractiveList.to_dataframe", false]], "to_interactive_list() (psyplot.data.interactivearray method)": [[7, "psyplot.data.InteractiveArray.to_interactive_list", false]], "to_interactive_list() (psyplot.data.interactivebase method)": [[7, "psyplot.data.InteractiveBase.to_interactive_list", false]], "to_interactive_list() (psyplot.data.interactivelist method)": [[7, "psyplot.data.InteractiveList.to_interactive_list", false]], "to_netcdf() (in module psyplot.data)": [[7, "psyplot.data.to_netcdf", false]], "to_slice() (in module psyplot.data)": [[7, "psyplot.data.to_slice", false]], "trace (psyplot.config.rcsetup.subdict attribute)": [[6, "psyplot.config.rcsetup.SubDict.trace", false]], "ugriddecoder (class in psyplot.data)": [[7, "psyplot.data.UGridDecoder", false]], "unique_everseen() (in module psyplot.utils)": [[14, "psyplot.utils.unique_everseen", false]], "unregister_plotter() (in module psyplot.project)": [[11, "psyplot.project.unregister_plotter", false]], "unshare() (psyplot.plotter.plotter method)": [[10, "psyplot.plotter.Plotter.unshare", false]], "unshare() (psyplot.project.project method)": [[11, "psyplot.project.Project.unshare", false]], "unshare_me() (psyplot.plotter.plotter method)": [[10, "psyplot.plotter.Plotter.unshare_me", false]], "update() (psyplot.config.rcsetup.rcparams method)": [[6, "psyplot.config.rcsetup.RcParams.update", false]], "update() (psyplot.config.rcsetup.subdict method)": [[6, "psyplot.config.rcsetup.SubDict.update", false]], "update() (psyplot.data.arraylist method)": [[7, "psyplot.data.ArrayList.update", false]], "update() (psyplot.data.interactivearray method)": [[7, "psyplot.data.InteractiveArray.update", false]], "update() (psyplot.data.interactivebase method)": [[7, "psyplot.data.InteractiveBase.update", false]], "update() (psyplot.plotter.formatoption method)": [[10, "psyplot.plotter.Formatoption.update", false]], "update() (psyplot.plotter.plotter method)": [[10, "psyplot.plotter.Plotter.update", false]], "update() (psyplot.plotter.postprocessing method)": [[10, "psyplot.plotter.PostProcessing.update", false]], "update() (psyplot.plotter.posttiming method)": [[10, "psyplot.plotter.PostTiming.update", false]], "update_after_plot (psyplot.plotter.formatoption attribute)": [[10, "psyplot.plotter.Formatoption.update_after_plot", false]], "update_from_defaultparams() (psyplot.config.rcsetup.rcparams method)": [[6, "psyplot.config.rcsetup.RcParams.update_from_defaultParams", false]], "validate (psyplot.config.rcsetup.rcparams property)": [[6, "psyplot.config.rcsetup.RcParams.validate", false]], "validate (psyplot.plotter.formatoption property)": [[10, "psyplot.plotter.Formatoption.validate", false]], "validate() (psyplot.plotter.postprocessing static method)": [[10, "psyplot.plotter.PostProcessing.validate", false]], "validate() (psyplot.plotter.posttiming static method)": [[10, "psyplot.plotter.PostTiming.validate", false]], "validate_bool() (in module psyplot.config.rcsetup)": [[6, "psyplot.config.rcsetup.validate_bool", false]], "validate_bool_maybe_none() (in module psyplot.config.rcsetup)": [[6, "psyplot.config.rcsetup.validate_bool_maybe_none", false]], "validate_dict() (in module psyplot.config.rcsetup)": [[6, "psyplot.config.rcsetup.validate_dict", false]], "validate_files_exist() (in module psyplot.config.rcsetup)": [[6, "psyplot.config.rcsetup.validate_files_exist", false]], "validate_path_exists() (in module psyplot.config.rcsetup)": [[6, "psyplot.config.rcsetup.validate_path_exists", false]], "validate_str() (in module psyplot.config.rcsetup)": [[6, "psyplot.config.rcsetup.validate_str", false]], "validate_stringlist() (in module psyplot.config.rcsetup)": [[6, "psyplot.config.rcsetup.validate_stringlist", false]], "validate_stringset() (in module psyplot.config.rcsetup)": [[6, "psyplot.config.rcsetup.validate_stringset", false]], "value (psyplot.plotter.formatoption property)": [[10, "psyplot.plotter.Formatoption.value", false]], "value2pickle (psyplot.plotter.formatoption property)": [[10, "psyplot.plotter.Formatoption.value2pickle", false]], "value2share (psyplot.plotter.formatoption property)": [[10, "psyplot.plotter.Formatoption.value2share", false]], "values() (psyplot.config.rcsetup.rcparams method)": [[6, "psyplot.config.rcsetup.RcParams.values", false]], "vector (psyplot.project.dataarrayplotter attribute)": [[11, "psyplot.project.DataArrayPlotter.vector", false]], "vector (psyplot.project.datasetplotter attribute)": [[11, "psyplot.project.DatasetPlotter.vector", false]], "vector (psyplot.project.project property)": [[11, "psyplot.project.Project.vector", false]], "vector (psyplot.project.projectplotter attribute)": [[11, "psyplot.project.ProjectPlotter.vector", false]], "violinplot (psyplot.project.dataarrayplotter attribute)": [[11, "psyplot.project.DataArrayPlotter.violinplot", false]], "violinplot (psyplot.project.datasetplotter attribute)": [[11, "psyplot.project.DatasetPlotter.violinplot", false]], "violinplot (psyplot.project.project property)": [[11, "psyplot.project.Project.violinplot", false]], "violinplot (psyplot.project.projectplotter attribute)": [[11, "psyplot.project.ProjectPlotter.violinplot", false]], "warn() (in module psyplot.warning)": [[15, "psyplot.warning.warn", false]], "with_gui (in module psyplot)": [[3, "psyplot.with_gui", false]], "with_plotter (psyplot.data.arraylist property)": [[7, "psyplot.data.ArrayList.with_plotter", false]], "with_plotter (psyplot.project.project property)": [[11, "psyplot.project.Project.with_plotter", false]]}, "objects": {"": [[3, 0, 0, "-", "psyplot"]], "psyplot": [[4, 0, 0, "-", "config"], [7, 0, 0, "-", "data"], [8, 0, 0, "-", "docstring"], [9, 0, 0, "-", "gdal_store"], [3, 2, 1, "", "get_versions"], [10, 0, 0, "-", "plotter"], [11, 0, 0, "-", "project"], [12, 0, 0, "-", "sphinxext"], [14, 0, 0, "-", "utils"], [15, 0, 0, "-", "warning"], [3, 1, 1, "", "with_gui"]], "psyplot.config": [[4, 1, 1, "", "config_path"], [4, 1, 1, "", "logcfg_path"], [5, 0, 0, "-", "logsetup"], [6, 0, 0, "-", "rcsetup"]], "psyplot.config.logsetup": [[5, 2, 1, "", "setup_logging"]], "psyplot.config.rcsetup": [[6, 3, 1, "", "RcParams"], [6, 3, 1, "", "SubDict"], [6, 1, 1, "", "defaultParams"], [6, 2, 1, "", "get_configdir"], [6, 2, 1, "", "psyplot_fname"], [6, 1, 1, "", "rcParams"], [6, 2, 1, "", "safe_list"], [6, 2, 1, "", "validate_bool"], [6, 2, 1, "", "validate_bool_maybe_none"], [6, 2, 1, "", "validate_dict"], [6, 2, 1, "", "validate_files_exist"], [6, 2, 1, "", "validate_path_exists"], [6, 2, 1, "", "validate_str"], [6, 2, 1, "", "validate_stringlist"], [6, 2, 1, "", "validate_stringset"]], "psyplot.config.rcsetup.RcParams": [[6, 4, 1, "", "HEADER"], [6, 5, 1, "", "catch"], [6, 5, 1, "", "connect"], [6, 5, 1, "", "copy"], [6, 6, 1, "", "defaultParams"], [6, 6, 1, "", "descriptions"], [6, 5, 1, "", "disconnect"], [6, 5, 1, "", "dump"], [6, 5, 1, "", "find_all"], [6, 5, 1, "", "find_and_replace"], [6, 5, 1, "", "keys"], [6, 5, 1, "", "load_from_file"], [6, 5, 1, "", "load_plugins"], [6, 4, 1, "", "msg_depr"], [6, 4, 1, "", "msg_depr_ignore"], [6, 5, 1, "", "remove"], [6, 5, 1, "", "update"], [6, 5, 1, "", "update_from_defaultParams"], [6, 6, 1, "", "validate"], [6, 5, 1, "", "values"]], "psyplot.config.rcsetup.SubDict": [[6, 5, 1, "", "add_base_str"], [6, 4, 1, "", "base"], [6, 4, 1, "", "base_str"], [6, 6, 1, "", "data"], [6, 5, 1, "", "iteritems"], [6, 5, 1, "", "iterkeys"], [6, 5, 1, "", "itervalues"], [6, 4, 1, "", "patterns"], [6, 6, 1, "", "replace"], [6, 4, 1, "", "trace"], [6, 5, 1, "", "update"]], "psyplot.data": [[7, 3, 1, "", "AbsoluteTimeDecoder"], [7, 3, 1, "", "AbsoluteTimeEncoder"], [7, 3, 1, "", "ArrayList"], [7, 3, 1, "", "CFDecoder"], [7, 3, 1, "", "DatasetAccessor"], [7, 3, 1, "", "InteractiveArray"], [7, 3, 1, "", "InteractiveBase"], [7, 3, 1, "", "InteractiveList"], [7, 3, 1, "", "Signal"], [7, 3, 1, "", "UGridDecoder"], [7, 2, 1, "", "decode_absolute_time"], [7, 2, 1, "", "encode_absolute_time"], [7, 2, 1, "", "get_filename_ds"], [7, 1, 1, "", "get_fname_funcs"], [7, 2, 1, "", "get_index_from_coord"], [7, 2, 1, "", "get_tdata"], [7, 2, 1, "", "open_dataset"], [7, 2, 1, "", "open_mfdataset"], [7, 2, 1, "", "setup_coords"], [7, 1, 1, "", "t_patterns"], [7, 2, 1, "", "to_netcdf"], [7, 2, 1, "", "to_slice"]], "psyplot.data.AbsoluteTimeDecoder": [[7, 6, 1, "", "dtype"]], "psyplot.data.AbsoluteTimeEncoder": [[7, 6, 1, "", "dtype"]], "psyplot.data.ArrayList": [[7, 6, 1, "", "all_dims"], [7, 6, 1, "", "all_names"], [7, 5, 1, "", "append"], [7, 6, 1, "", "arr_names"], [7, 5, 1, "", "array_info"], [7, 6, 1, "", "arrays"], [7, 6, 1, "", "coords"], [7, 6, 1, "", "coords_intersect"], [7, 5, 1, "", "copy"], [7, 6, 1, "", "dims"], [7, 6, 1, "", "dims_intersect"], [7, 5, 1, "", "draw"], [7, 5, 1, "", "extend"], [7, 5, 1, "", "from_dataset"], [7, 5, 1, "", "from_dict"], [7, 6, 1, "", "is_unstructured"], [7, 6, 1, "", "logger"], [7, 6, 1, "", "names"], [7, 5, 1, "", "next_available_name"], [7, 6, 1, "", "no_auto_update"], [7, 5, 1, "", "remove"], [7, 5, 1, "", "rename"], [7, 5, 1, "", "start_update"], [7, 5, 1, "", "update"], [7, 6, 1, "", "with_plotter"]], "psyplot.data.CFDecoder": [[7, 5, 1, "", "can_decode"], [7, 5, 1, "", "clear_cache"], [7, 5, 1, "", "correct_dims"], [7, 5, 1, "", "decode_coords"], [7, 5, 1, "", "decode_ds"], [7, 5, 1, "", "get_cell_node_coord"], [7, 5, 1, "", "get_coord_idims"], [7, 5, 1, "", "get_coord_info"], [7, 5, 1, "", "get_decoder"], [7, 5, 1, "", "get_grid_type_info"], [7, 5, 1, "", "get_idims"], [7, 5, 1, "", "get_metadata_for_section"], [7, 5, 1, "", "get_metadata_for_variable"], [7, 5, 1, "", "get_metadata_sections"], [7, 5, 1, "", "get_plotbounds"], [7, 5, 1, "", "get_projection_info"], [7, 5, 1, "", "get_t"], [7, 5, 1, "", "get_t_metadata"], [7, 5, 1, "", "get_tname"], [7, 5, 1, "", "get_triangles"], [7, 5, 1, "", "get_variable_by_axis"], [7, 5, 1, "", "get_x"], [7, 5, 1, "", "get_x_metadata"], [7, 5, 1, "", "get_xname"], [7, 5, 1, "", "get_y"], [7, 5, 1, "", "get_y_metadata"], [7, 5, 1, "", "get_yname"], [7, 5, 1, "", "get_z"], [7, 5, 1, "", "get_z_metadata"], [7, 5, 1, "", "get_zname"], [7, 5, 1, "", "is_circumpolar"], [7, 5, 1, "", "is_unstructured"], [7, 6, 1, "", "logger"], [7, 5, 1, "", "register_decoder"], [7, 5, 1, "", "standardize_dims"], [7, 4, 1, "", "supports_spatial_slicing"]], "psyplot.data.DatasetAccessor": [[7, 5, 1, "", "copy"], [7, 5, 1, "", "create_list"], [7, 6, 1, "", "data_store"], [7, 6, 1, "", "filename"], [7, 6, 1, "", "num"], [7, 6, 1, "", "plot"], [7, 5, 1, "", "to_array"]], "psyplot.data.InteractiveArray": [[7, 6, 1, "", "base"], [7, 6, 1, "", "base_variables"], [7, 5, 1, "", "copy"], [7, 6, 1, "", "decoder"], [7, 5, 1, "", "fldmean"], [7, 5, 1, "", "fldpctl"], [7, 5, 1, "", "fldstd"], [7, 5, 1, "", "get_coord"], [7, 5, 1, "", "get_dim"], [7, 5, 1, "", "gridweights"], [7, 6, 1, "", "idims"], [7, 5, 1, "", "init_accessor"], [7, 5, 1, "", "isel"], [7, 6, 1, "", "iter_base_variables"], [7, 6, 1, "", "logger"], [7, 4, 1, "", "onbasechange"], [7, 5, 1, "", "sel"], [7, 5, 1, "", "shiftlon"], [7, 5, 1, "", "start_update"], [7, 5, 1, "", "to_interactive_list"], [7, 5, 1, "", "update"]], "psyplot.data.InteractiveBase": [[7, 6, 1, "", "arr_name"], [7, 6, 1, "", "ax"], [7, 6, 1, "", "block_signals"], [7, 6, 1, "", "logger"], [7, 6, 1, "", "no_auto_update"], [7, 4, 1, "", "onupdate"], [7, 6, 1, "", "plot"], [7, 6, 1, "", "plotter"], [7, 5, 1, "", "start_update"], [7, 5, 1, "", "to_interactive_list"], [7, 5, 1, "", "update"]], "psyplot.data.InteractiveList": [[7, 5, 1, "", "append"], [7, 5, 1, "", "extend"], [7, 5, 1, "", "from_dataset"], [7, 6, 1, "", "logger"], [7, 6, 1, "", "no_auto_update"], [7, 6, 1, "", "psy"], [7, 5, 1, "", "start_update"], [7, 5, 1, "", "to_dataframe"], [7, 5, 1, "", "to_interactive_list"]], "psyplot.data.Signal": [[7, 5, 1, "", "connect"], [7, 5, 1, "", "disconnect"], [7, 5, 1, "", "emit"], [7, 4, 1, "", "instance"], [7, 4, 1, "", "owner"]], "psyplot.data.UGridDecoder": [[7, 5, 1, "", "can_decode"], [7, 5, 1, "", "decode_coords"], [7, 5, 1, "", "get_cell_node_coord"], [7, 5, 1, "", "get_mesh"], [7, 5, 1, "", "get_nodes"], [7, 5, 1, "", "get_triangles"], [7, 5, 1, "", "get_x"], [7, 5, 1, "", "get_y"], [7, 5, 1, "", "is_unstructured"], [7, 4, 1, "", "supports_spatial_slicing"]], "psyplot.docstring": [[8, 3, 1, "", "PsyplotDocstringProcessor"], [8, 2, 1, "", "append_original_doc"], [8, 2, 1, "", "dedent"], [8, 1, 1, "", "docstrings"], [8, 2, 1, "", "indent"]], "psyplot.docstring.PsyplotDocstringProcessor": [[8, 5, 1, "", "get_sections"], [8, 4, 1, "", "param_like_sections"]], "psyplot.gdal_store": [[9, 3, 1, "", "GdalStore"]], "psyplot.gdal_store.GdalStore": [[9, 5, 1, "", "get_attrs"], [9, 5, 1, "", "get_variables"]], "psyplot.plotter": [[10, 1, 1, "", "BEFOREPLOTTING"], [10, 3, 1, "", "DictFormatoption"], [10, 1, 1, "", "END"], [10, 3, 1, "", "Formatoption"], [10, 3, 1, "", "FormatoptionMeta"], [10, 3, 1, "", "Plotter"], [10, 3, 1, "", "PostProcDependencies"], [10, 3, 1, "", "PostProcessing"], [10, 3, 1, "", "PostTiming"], [10, 1, 1, "", "START"], [10, 2, 1, "", "default_print_func"], [10, 2, 1, "", "format_time"], [10, 1, 1, "", "groups"], [10, 2, 1, "", "is_data_dependent"]], "psyplot.plotter.DictFormatoption": [[10, 5, 1, "", "set_value"]], "psyplot.plotter.Formatoption": [[10, 6, 1, "", "any_decoder"], [10, 6, 1, "", "ax"], [10, 6, 1, "", "changed"], [10, 5, 1, "", "check_and_set"], [10, 4, 1, "", "children"], [10, 4, 1, "", "connections"], [10, 5, 1, "", "convert_coordinate"], [10, 6, 1, "", "data"], [10, 4, 1, "", "data_dependent"], [10, 6, 1, "", "decoder"], [10, 6, 1, "", "default"], [10, 6, 1, "", "default_key"], [10, 4, 1, "", "dependencies"], [10, 5, 1, "", "diff"], [10, 5, 1, "", "finish_update"], [10, 5, 1, "", "get_decoder"], [10, 5, 1, "", "get_fmt_widget"], [10, 4, 1, "", "group"], [10, 6, 1, "", "groupname"], [10, 4, 1, "", "index_in_list"], [10, 6, 1, "", "init_kwargs"], [10, 5, 1, "", "initialize_plot"], [10, 6, 1, "", "iter_data"], [10, 6, 1, "", "iter_raw_data"], [10, 4, 1, "", "key"], [10, 6, 1, "", "lock"], [10, 6, 1, "", "logger"], [10, 4, 1, "", "name"], [10, 4, 1, "", "parents"], [10, 4, 1, "", "plot_fmt"], [10, 6, 1, "", "plotter"], [10, 4, 1, "", "priority"], [10, 6, 1, "", "project"], [10, 6, 1, "", "raw_data"], [10, 5, 1, "", "remove"], [10, 4, 1, "", "requires_clearing"], [10, 4, 1, "", "requires_replot"], [10, 5, 1, "", "set_data"], [10, 5, 1, "", "set_decoder"], [10, 5, 1, "", "set_value"], [10, 5, 1, "", "share"], [10, 4, 1, "", "shared"], [10, 6, 1, "", "shared_by"], [10, 5, 1, "", "update"], [10, 4, 1, "", "update_after_plot"], [10, 6, 1, "", "validate"], [10, 6, 1, "", "value"], [10, 6, 1, "", "value2pickle"], [10, 6, 1, "", "value2share"]], "psyplot.plotter.Plotter": [[10, 6, 1, "", "ax"], [10, 6, 1, "", "base_variables"], [10, 6, 1, "", "changed"], [10, 5, 1, "", "check_data"], [10, 5, 1, "", "check_key"], [10, 5, 1, "", "convert_coordinate"], [10, 6, 1, "", "data"], [10, 5, 1, "", "draw"], [10, 4, 1, "", "enable_post"], [10, 6, 1, "", "figs2draw"], [10, 6, 1, "", "fmt_groups"], [10, 5, 1, "", "get_enhanced_attrs"], [10, 5, 1, "", "get_vfunc"], [10, 6, 1, "", "groups"], [10, 5, 1, "", "has_changed"], [10, 4, 1, "", "include_links"], [10, 5, 1, "", "initialize_plot"], [10, 6, 1, "", "iter_base_variables"], [10, 6, 1, "", "logger"], [10, 5, 1, "", "make_plot"], [10, 6, 1, "", "no_auto_update"], [10, 6, 1, "", "no_validation"], [10, 6, 1, "", "plot_data"], [10, 4, 1, "", "plot_data_decoder"], [10, 4, 1, "", "post"], [10, 4, 1, "", "post_timing"], [10, 6, 1, "", "project"], [10, 6, 1, "", "rc"], [10, 5, 1, "", "reinit"], [10, 5, 1, "", "share"], [10, 5, 1, "", "show"], [10, 5, 1, "", "show_docs"], [10, 5, 1, "", "show_keys"], [10, 5, 1, "", "show_summaries"], [10, 5, 1, "", "start_update"], [10, 5, 1, "", "unshare"], [10, 5, 1, "", "unshare_me"], [10, 5, 1, "", "update"]], "psyplot.plotter.PostProcessing": [[10, 4, 1, "", "children"], [10, 6, 1, "", "data_dependent"], [10, 4, 1, "", "default"], [10, 4, 1, "", "dependencies"], [10, 4, 1, "", "group"], [10, 4, 1, "", "name"], [10, 6, 1, "", "post_timing"], [10, 4, 1, "", "priority"], [10, 5, 1, "", "update"], [10, 5, 1, "", "validate"]], "psyplot.plotter.PostTiming": [[10, 4, 1, "", "default"], [10, 5, 1, "", "get_fmt_widget"], [10, 4, 1, "", "group"], [10, 4, 1, "", "name"], [10, 4, 1, "", "priority"], [10, 5, 1, "", "update"], [10, 5, 1, "", "validate"]], "psyplot.project": [[11, 3, 1, "", "DataArrayPlotter"], [11, 3, 1, "", "DataArrayPlotterInterface"], [11, 3, 1, "", "DatasetPlotter"], [11, 3, 1, "", "DatasetPlotterInterface"], [11, 4, 1, "", "PROJECT_CLS"], [11, 3, 1, "", "PlotterInterface"], [11, 3, 1, "", "Project"], [11, 3, 1, "", "ProjectPlotter"], [11, 2, 1, "", "close"], [11, 2, 1, "", "gcp"], [11, 2, 1, "", "get_project_nums"], [11, 2, 1, "", "multiple_subplots"], [11, 1, 1, "", "plot"], [11, 2, 1, "", "project"], [11, 2, 1, "", "register_plotter"], [11, 2, 1, "", "scp"], [11, 2, 1, "", "unregister_plotter"]], "psyplot.project.DataArrayPlotter": [[11, 4, 1, "", "barplot"], [11, 4, 1, "", "combined"], [11, 4, 1, "", "density"], [11, 4, 1, "", "fldmean"], [11, 4, 1, "", "lineplot"], [11, 4, 1, "", "mapcombined"], [11, 4, 1, "", "mapplot"], [11, 4, 1, "", "mapvector"], [11, 4, 1, "", "plot2d"], [11, 4, 1, "", "vector"], [11, 4, 1, "", "violinplot"]], "psyplot.project.DataArrayPlotterInterface": [[11, 5, 1, "", "check_data"]], "psyplot.project.DatasetPlotter": [[11, 4, 1, "", "barplot"], [11, 4, 1, "", "combined"], [11, 4, 1, "", "density"], [11, 4, 1, "", "fldmean"], [11, 4, 1, "", "lineplot"], [11, 4, 1, "", "mapcombined"], [11, 4, 1, "", "mapplot"], [11, 4, 1, "", "mapvector"], [11, 4, 1, "", "plot2d"], [11, 4, 1, "", "vector"], [11, 4, 1, "", "violinplot"]], "psyplot.project.PlotterInterface": [[11, 5, 1, "", "check_data"], [11, 5, 1, "", "docs"], [11, 6, 1, "", "is_imported"], [11, 5, 1, "", "keys"], [11, 6, 1, "", "plotter_cls"], [11, 6, 1, "", "print_func"], [11, 5, 1, "", "summaries"]], "psyplot.project.Project": [[11, 5, 1, "", "append"], [11, 6, 1, "", "arr_names"], [11, 6, 1, "", "axes"], [11, 6, 1, "", "barplot"], [11, 4, 1, "", "block_signals"], [11, 5, 1, "", "close"], [11, 6, 1, "", "combined"], [11, 6, 1, "", "datasets"], [11, 6, 1, "", "density"], [11, 5, 1, "", "disable"], [11, 5, 1, "", "docs"], [11, 6, 1, "", "dsnames"], [11, 6, 1, "", "dsnames_map"], [11, 5, 1, "", "enable"], [11, 5, 1, "", "export"], [11, 5, 1, "", "extend"], [11, 5, 1, "", "extract_fmts_from_preset"], [11, 6, 1, "", "figs"], [11, 6, 1, "", "fldmean"], [11, 5, 1, "", "format_string"], [11, 5, 1, "", "from_dataset"], [11, 6, 1, "", "is_cmp"], [11, 6, 1, "", "is_csp"], [11, 6, 1, "", "is_main"], [11, 5, 1, "", "joined_attrs"], [11, 5, 1, "", "keys"], [11, 6, 1, "", "lineplot"], [11, 5, 1, "", "load_preset"], [11, 5, 1, "", "load_project"], [11, 6, 1, "", "logger"], [11, 6, 1, "", "main"], [11, 6, 1, "", "mapcombined"], [11, 6, 1, "", "mapplot"], [11, 6, 1, "", "maps"], [11, 6, 1, "", "mapvector"], [11, 5, 1, "", "new"], [11, 4, 1, "", "oncpchange"], [11, 6, 1, "", "plot"], [11, 6, 1, "", "plot2d"], [11, 6, 1, "", "plotters"], [11, 5, 1, "", "save_preset"], [11, 5, 1, "", "save_project"], [11, 5, 1, "", "scp"], [11, 5, 1, "", "share"], [11, 5, 1, "", "show"], [11, 6, 1, "", "simple"], [11, 5, 1, "", "summaries"], [11, 5, 1, "", "unshare"], [11, 6, 1, "", "vector"], [11, 6, 1, "", "violinplot"], [11, 6, 1, "", "with_plotter"]], "psyplot.project.ProjectPlotter": [[11, 4, 1, "", "barplot"], [11, 4, 1, "", "combined"], [11, 4, 1, "", "density"], [11, 4, 1, "", "fldmean"], [11, 4, 1, "", "lineplot"], [11, 4, 1, "", "mapcombined"], [11, 4, 1, "", "mapplot"], [11, 4, 1, "", "mapvector"], [11, 4, 1, "", "plot2d"], [11, 6, 1, "", "project"], [11, 5, 1, "", "show_plot_methods"], [11, 4, 1, "", "vector"], [11, 4, 1, "", "violinplot"]], "psyplot.sphinxext": [[13, 0, 0, "-", "extended_napoleon"]], "psyplot.sphinxext.extended_napoleon": [[13, 3, 1, "", "DocstringExtension"], [13, 3, 1, "", "ExtendedGoogleDocstring"], [13, 3, 1, "", "ExtendedNumpyDocstring"], [13, 2, 1, "", "process_docstring"], [13, 2, 1, "", "setup"]], "psyplot.utils": [[14, 3, 1, "", "Defaultdict"], [14, 2, 1, "", "check_key"], [14, 2, 1, "", "get_default_value"], [14, 2, 1, "", "hashable"], [14, 2, 1, "", "is_iterable"], [14, 2, 1, "", "is_remote_url"], [14, 2, 1, "", "isstring"], [14, 2, 1, "", "join_dicts"], [14, 2, 1, "", "plugin_entrypoints"], [14, 2, 1, "", "sort_kwargs"], [14, 2, 1, "", "unique_everseen"]], "psyplot.utils.Defaultdict": [[14, 5, 1, "", "copy"]], "psyplot.warning": [[15, 7, 1, "", "PsyPlotCritical"], [15, 7, 1, "", "PsyPlotRuntimeWarning"], [15, 7, 1, "", "PsyPlotWarning"], [15, 2, 1, "", "critical"], [15, 2, 1, "", "customwarn"], [15, 2, 1, "", "disable_warnings"], [15, 2, 1, "", "warn"]]}, "objnames": {"0": ["py", "module", "Python module"], "1": ["py", "data", "Python data"], "2": ["py", "function", "Python function"], "3": ["py", "class", "Python class"], "4": ["py", "attribute", "Python attribute"], "5": ["py", "method", "Python method"], "6": ["py", "property", "Python property"], "7": ["py", "exception", "Python exception"]}, "objtypes": {"0": "py:module", "1": "py:data", "2": "py:function", "3": "py:class", "4": "py:attribute", "5": "py:method", "6": "py:property", "7": "py:exception"}, "terms": {"": [0, 6, 7, 8, 10, 11, 14, 17, 18, 20, 22, 23, 24, 27], "0": [0, 1, 3, 6, 7, 8, 10, 11, 17, 18, 20, 22, 23, 24, 25], "00": [1, 20, 23], "0000": 24, "000000000": [1, 23], "0001": 24, "00363": 24, "01": [1, 20, 23], "02": 23, "03": [1, 18, 23], "04": 20, "05": [1, 20, 23], "08": [5, 24], "0x7f39a07a62b0": 20, "1": [1, 3, 6, 7, 10, 11, 13, 14, 17, 18, 20, 22, 23, 24, 25, 26], "10": [1, 10, 16, 18, 20, 22, 23, 24], "100": [7, 20], "100000": 23, "11": [1, 3, 20, 22, 23], "110m": 20, "12": [1, 16, 18, 20, 22, 23], "128": [1, 23], "13": [1, 20, 22, 23], "130": [1, 23], "14": [1, 16, 20, 22, 23], "15": [1, 3, 20, 22, 23], "16": [1, 16, 20, 22, 23, 24], "16493": 23, "17": [1, 16, 20, 22, 23], "18": [1, 16, 20, 22, 23], "18432": 1, "19": [1, 3, 16, 18, 20, 22, 23], "192": [1, 20, 23], "1979": [1, 20, 23], "1d": 7, "1e": [1, 20, 23], "2": [1, 3, 7, 10, 14, 18, 20, 22, 23, 24, 25, 26], "20": [1, 5, 10, 16, 20, 22, 23], "200021_169598": 24, "2012": 5, "2016": 0, "2017": 24, "2020": 0, "2021": [0, 24], "2024": [18, 24], "21": [1, 20, 22, 23], "21105": 24, "21502": 24, "22": [1, 16, 20, 22, 23, 24], "23": [20, 22, 23], "24": [16, 20, 22, 23], "25": [20, 22, 23], "251": 23, "253": 23, "254": 23, "255": 23, "256": 23, "26": [5, 20, 23], "260": 23, "262": 23, "263": 23, "27": [20, 23], "28": [20, 23], "28t18": 23, "29": [20, 23], "29774": 23, "2a": 6, "2b": 6, "2d": [7, 11, 18, 23, 26], "2e": 20, "2kb": [1, 23], "3": [0, 1, 3, 7, 10, 11, 13, 18, 20, 22, 23, 24, 25, 26], "30": [10, 16, 20, 23], "31": [20, 23], "3114": 23, "31t18": [1, 20, 23], "32": [16, 20, 23], "33": [16, 20, 23], "33095": 23, "33876": 23, "34": 23, "35": [1, 23], "352": [1, 23], "354": [1, 23], "356": [1, 23], "358": [1, 23], "36": 23, "363": 24, "37": 23, "37978": 23, "38": 23, "39": [1, 16, 23], "3d": 16, "4": [1, 3, 7, 8, 11, 18, 20, 22, 23, 24, 25, 26], "40": [1, 23], "40712": 23, "41": 23, "41689": 23, "42": 23, "42665": 23, "43": 23, "44": 23, "45": [16, 23], "454": 23, "46": 23, "47": [16, 23], "47742": 23, "48": [1, 23], "48718": 23, "48915": 23, "49": [16, 23], "4989": 23, "5": [1, 3, 7, 11, 18, 20, 22, 23, 26], "50": 23, "50087": 23, "51": 23, "51453": 23, "52": [1, 18, 23], "5281": 24, "53": [16, 23], "5321": 23, "54": 23, "54774": 23, "55": 23, "5536": 23, "56": 23, "562769": 14, "57": [1, 20, 23], "58875": 23, "593798": 24, "5e": 20, "6": [1, 7, 16, 18, 20, 22, 23, 25], "60": 16, "61": 16, "6171": 24, "6190500": 14, "62": 16, "625": [1, 23], "63": 16, "64": [7, 16], "6454": 23, "65": 16, "66": 16, "660c703": 16, "67": 16, "68": 16, "69": 16, "7": [1, 16, 18, 20, 22, 23, 25], "70984": 23, "71": [1, 16], "72": [1, 23], "72742": 23, "74": [1, 16], "74kb": [1, 23], "75": [1, 23], "76": 16, "76845": 23, "768b": [1, 23], "7716": 24, "78406": 23, "79578": 23, "8": [1, 16, 18, 20, 22, 23, 25], "81335": 23, "83": [1, 23], "83093": 23, "84": [1, 23], "8485": 23, "86": [1, 23], "86024": 23, "8661": 23, "86804": 23, "875": [1, 23], "88": [1, 20, 23], "88367": 23, "8b": [1, 23], "9": [1, 3, 7, 16, 18, 20, 22, 23], "90517": 23, "96": [1, 20, 23], "96375": 23, "96376": 23, "A": [0, 3, 6, 7, 8, 10, 11, 13, 14, 16, 17, 18, 22, 23, 24, 25, 26, 27], "And": 22, "As": [1, 18, 19, 22, 23], "At": 18, "BY": 24, "But": [0, 18, 22, 23], "By": [6, 7, 10, 11, 19, 23, 26], "FOR": 0, "For": [6, 7, 11, 18, 20, 22, 23, 26], "If": [0, 1, 3, 5, 6, 7, 9, 10, 11, 13, 14, 16, 17, 18, 19, 20, 22, 23, 24, 25, 26], "In": [1, 6, 7, 11, 16, 18, 20, 21, 22, 23, 26], "It": [0, 1, 6, 7, 9, 10, 11, 16, 17, 19, 20, 22, 23, 27], "No": 0, "Not": [0, 24], "Of": 0, "On": [6, 18, 19, 25], "One": [10, 11, 23], "Or": [9, 22], "That": 22, "The": [0, 3, 6, 7, 8, 9, 10, 11, 13, 14, 16, 17, 21, 22, 24, 25, 27], "Then": [7, 11, 22, 23, 25], "There": [0, 11, 19, 20, 22, 23, 25], "These": 16, "To": [6, 7, 10, 11, 18, 22, 23, 25, 26], "With": 7, "_": [7, 23], "__": [19, 23], "__call__": 11, "__enter__": 11, "__exit__": 11, "__init__": 22, "__main__": 17, "_build": 19, "_concat_dim": 16, "_fillvalu": 7, "_get_fname_netcdf4": 7, "_get_fname_nio": 7, "_get_fname_scipi": 7, "_load_preset": 11, "_njob": [7, 10], "_plugin": 19, "_process_docstr": 13, "_rcparams_str": 22, "_static": 22, "_summary_": 7, "_temp_bool_prop": 11, "_templat": 22, "_version": 22, "aai": 19, "abcmeta": 10, "abl": 25, "about": [22, 23, 24], "abov": [7, 11, 13, 18, 19, 22, 23, 25, 26], "absolut": [7, 11], "absolutetimedecod": [2, 3, 7], "absolutetimeencod": [2, 3, 7], "abstract": [10, 16, 20, 26], "abstractdatastor": [7, 9], "abstractstor": 7, "acacia": 24, "accept": [6, 7, 10, 16, 22, 23], "access": [0, 1, 6, 7, 10, 11, 20, 22, 23, 27], "accessor": [7, 16, 20, 24], "accord": 7, "accossor": 16, "account": [7, 19], "activ": [7, 19, 25], "actual": 23, "ad": 11, "adapt": [0, 13], "add": [6, 7, 10, 11, 16, 20], "add_base_str": [2, 3, 4, 6], "add_licens": 19, "add_offset": 7, "add_subplot": 20, "addit": [6, 7, 10, 11, 13, 14, 16, 22], "addition": [6, 7, 10, 16, 18], "additional_children": [10, 16], "additional_depend": [10, 16], "address": [16, 23], "adjust": [0, 17], "advanc": [1, 22], "advantag": [0, 20, 22, 23], "advic": 24, "affect": 10, "affili": 24, "after": [7, 10, 18, 20, 23, 25], "afterward": [6, 17, 19, 23], "again": [0, 10, 20, 23], "against": 7, "agenc": 24, "agre": 0, "all": [0, 3, 6, 7, 10, 11, 14, 15, 16, 17, 18, 19, 20, 22, 23, 26], "all_dim": [2, 3, 7], "all_nam": [2, 3, 7], "allow": [0, 7, 10, 11, 16, 19, 20, 22, 23, 24], "almost": 7, "along": [0, 7], "alpha": 11, "alreadi": [0, 6, 7, 11, 22, 23, 25, 26], "also": [0, 1, 3, 6, 7, 10, 11, 15, 16, 17, 18, 19, 20, 22, 23, 24, 25], "altern": [7, 10, 11], "alternate_path": 16, "alternative_ax": [11, 23], "alternative_path": [7, 11, 16, 23], "although": [0, 10, 18, 20, 22], "alwai": [0, 7, 10, 11, 16, 18, 22, 23, 25, 26], "an": [0, 1, 6, 7, 8, 9, 10, 11, 13, 14, 16, 17, 19, 20, 22, 23, 24, 25, 26, 27], "anaconda": 25, "analogi": 20, "analysi": [0, 19, 26], "andstart_upd": 18, "ani": [0, 1, 6, 7, 8, 10, 11, 13, 14, 16, 18, 19, 22, 23, 24, 26], "anoth": [6, 7, 10, 11, 13, 16, 18, 22], "another_plott": 22, "any_decod": [2, 3, 10], "anymor": 16, "anyth": [6, 10, 11, 25], "anywai": [0, 7, 10, 11, 20, 23, 25], "apa": 24, "api": [0, 7, 11, 22, 24, 25], "app": 13, "appear": [0, 7, 11, 15], "append": [2, 3, 6, 7, 8, 11, 20], "append_original_doc": [2, 3, 8], "appendix": 23, "appli": [8, 10, 11, 16, 17, 23], "applic": [0, 13, 24], "approach": [22, 23], "appropri": [7, 25], "ar": [0, 1, 3, 4, 5, 6, 7, 10, 11, 13, 14, 16, 17, 18, 19, 20, 22, 23, 25, 26, 27], "arch": 27, "arg": [6, 7, 8, 10, 11, 14, 15], "argument": [3, 7, 10, 11, 14, 16, 22, 23], "around": [0, 11, 15], "arr": [7, 10, 11, 20, 23], "arr0": [1, 7, 20, 23], "arr1": [20, 23], "arr2": [20, 23], "arr3": [20, 23], "arr_nam": [1, 2, 3, 7, 11, 23], "arrai": [2, 3, 7, 9, 10, 11, 14, 16, 20, 22, 23], "arrang": 23, "array_info": [2, 3, 7, 16], "arraylist": [2, 3, 7, 10, 11, 16, 22], "arrows": [6, 11, 20], "arrowstyl": 11, "articl": 24, "artist": [0, 20], "ask": [19, 24], "assert": [6, 16], "assign": [7, 10, 11, 22, 23], "associ": [6, 24], "assum": [7, 10, 11, 22, 23], "attach": 16, "attempt": [7, 10], "attr": [7, 11, 16, 18, 20, 23, 26], "attract": 24, "attribut": [1, 6, 7, 8, 9, 10, 11, 13, 14, 16, 18, 20, 22, 23], "au": 24, "author": [19, 24], "auto": [7, 13], "auto_draw": [7, 10, 11, 18], "auto_import": [11, 18], "auto_show": [18, 23], "auto_upd": [7, 10, 11, 18, 23], "autodoc": 13, "autom": [19, 22], "automat": [0, 7, 10, 11, 16, 17, 18, 19, 20, 22, 26], "autosummai": 15, "av": 17, "avail": [7, 10, 11, 17, 19, 23, 25], "avoid": [1, 6, 7, 16, 22, 23], "awai": [6, 22], "awkward": 7, "ax": [2, 3, 7, 10, 11, 16, 20, 22, 23], "axi": [7, 10, 16, 23], "axiscolor": 11, "b": [1, 6, 7, 20, 23], "back": [6, 7, 16], "backend": [0, 7, 11], "backend_kwarg": 7, "backend_pdf": 11, "background": [11, 20, 23], "band": 9, "band1": 9, "band2": 9, "bar": [8, 11, 23, 26], "barplot": [2, 3, 11, 17, 23, 26], "barplott": 11, "base": [1, 2, 3, 4, 6, 7, 8, 9, 10, 11, 13, 14, 15, 16, 18, 20, 22, 23, 25, 26], "base_index": 7, "base_str": [2, 3, 4, 6], "base_vari": [2, 3, 7, 10], "basemap": 7, "baseplott": [6, 16, 18], "basic": [20, 22, 23, 25], "bat": [19, 22], "bbox": [11, 17], "bbox_inch": 11, "beat": 0, "beauti": 24, "becaus": [0, 7, 11, 17, 22, 23, 26], "becom": 7, "been": [1, 7, 10, 11, 13, 16, 19, 20, 22], "befor": [10, 11, 22], "beforeplot": [2, 3, 10, 22], "begin": [6, 23], "behav": [7, 8], "behavior": [7, 23], "behaviour": [6, 10, 11, 15, 26], "being": [0, 7, 10, 15, 24], "believ": 19, "belong": [10, 11, 13, 18], "below": [0, 6, 7, 10, 11, 19, 23], "berlin": 1, "best": [0, 19, 25], "better": 23, "between": [7, 8, 10, 11, 20], "bibtex": 24, "big": [7, 22], "bin": [11, 19], "bit": [0, 7, 10, 22], "black": 19, "blackdoc": 19, "block": 7, "block_sign": [2, 3, 7, 11], "blue": 23, "bool": [3, 6, 7, 10, 11, 14], "boolean": [3, 6, 7, 10, 11, 18, 22], "both": [7, 10, 11, 24], "bound": [7, 10, 11, 16, 18, 20, 23], "boundari": 7, "boundsmethod": 20, "bout": 7, "box": [16, 23], "break": 7, "bring": [7, 22], "broadcast": 7, "broadcast_equ": 7, "browser": 19, "bug": [16, 19, 24], "build": [0, 4, 16, 19, 23], "built": [0, 3, 10, 19, 23, 25], "builtin": [15, 20], "by_coord": 7, "byte": 7, "c": [16, 25], "cach": 7, "calcul": [7, 11, 16, 18, 23, 26, 27], "calendar": 7, "call": [1, 6, 7, 10, 11, 13, 16, 20, 22, 23], "callabl": [7, 10, 11], "came": 24, "can": [0, 1, 4, 6, 7, 8, 9, 10, 11, 15, 16, 17, 18, 19, 20, 22, 23, 24, 25, 26], "can_decod": [2, 3, 7], "cannot": 16, "capabl": [10, 27], "captur": 7, "care": [7, 19, 22, 23], "cartopi": [3, 7, 11, 20, 23, 24], "case": [6, 7, 10, 11, 14, 16, 23], "catch": [2, 3, 4, 6, 16], "categor": 11, "categori": [15, 22], "cbar": [11, 20, 23], "cbarspac": [11, 20, 23], "cc": 24, "cc0": 24, "ccr": 20, "cd": [16, 17, 19, 25], "cdo": [7, 16, 18], "cell": [7, 16], "center": [0, 7, 23, 24], "central": 16, "central_longitud": 7, "certain": [16, 23], "cf": [0, 7, 10, 20, 23], "cfconvent": [7, 23], "cfdecod": [2, 3, 7, 10, 11, 16, 18, 22], "cff": [16, 19, 22, 24], "cffconvert": 19, "cftime": 7, "challeng": 22, "chang": [0, 1, 2, 3, 6, 7, 10, 11, 13, 14, 17, 18, 19, 20, 22, 23], "changelog": [22, 24], "charact": [7, 8], "chat": 24, "check": [7, 10, 11, 14, 19, 23, 25], "check_and_set": [2, 3, 10], "check_data": [2, 3, 10, 11, 16], "check_kei": [2, 3, 10, 14], "children": [2, 3, 10, 16, 22], "chilipp": 16, "chname": [7, 11, 16, 17], "choic": [17, 23], "choos": [0, 6, 7, 11, 20, 25], "chosen": [6, 7, 17], "chunk": 7, "chunked_array_typ": 7, "chunkmanagerenetrypoint": 7, "chunkmanagerentrypoint": 7, "ci": [16, 19], "cicleci": 16, "circumpolar": 7, "citat": [16, 19, 22, 24], "citi": 24, "clabel": [11, 20, 23], "clabelprop": [11, 20, 23], "clabels": [11, 20, 23], "clabelweight": [11, 20, 23], "class": [1, 4, 6, 7, 8, 9, 10, 11, 13, 14, 15, 16, 18, 22, 23, 26], "classic": 7, "classmethod": [7, 10, 11], "clat": [11, 20, 23], "clear": [7, 10, 11, 23], "clear_cach": [2, 3, 7], "cli": 16, "click": 0, "client": 7, "climat": [7, 18, 24], "clip": [11, 20, 23], "clon": [11, 20, 23], "clone": [19, 25], "close": [2, 3, 10, 11, 14, 16, 23], "close_figur": 23, "close_pdf": 11, "cloud": [16, 19, 22, 24, 25], "cls_signal": 7, "clsname": 10, "cmap": [1, 11, 16, 17, 20, 23], "coast": 20, "coastal": [0, 24], "code": [0, 1, 10, 16, 20, 22, 23, 24], "codebas": [16, 19, 22, 24, 25], "coerc": 7, "col": 11, "collabor": 19, "collect": [7, 14], "color": [10, 11, 16, 20, 23], "colorbar": 23, "colormap": [10, 20, 22, 23], "column": 11, "com": [14, 16], "combin": [0, 2, 3, 7, 10, 11, 14, 17, 22, 23, 24, 26], "combine_by_coord": 7, "combine_nest": 7, "combinedplott": [11, 20], "combinedsimpleplott": 11, "come": [0, 1, 7], "comfort": 0, "comma": 17, "command": [0, 1, 9, 11, 16, 23, 24, 25], "comment": [19, 22], "commit": 19, "common": [7, 11], "commun": [7, 23], "compar": [7, 10], "compat": [7, 16], "compil": [0, 6, 10], "complet": [1, 7, 22], "complevel": 7, "complex": 22, "complic": 23, "comprehens": 0, "compress": 7, "compression_opt": 7, "compris": 20, "comput": [0, 7], "con": 23, "concat": [11, 17], "concat_charact": 7, "concat_dim": [7, 16], "concaten": [7, 11, 17], "conceptu": 20, "concern": [19, 25], "conda": [16, 23], "conduct": [7, 10, 24], "conf": [13, 22], "config": [2, 3, 5, 6, 13, 16, 18, 19, 22, 23, 25, 26], "config_path": [2, 3, 4], "configur": [4, 5, 6, 7, 10, 11, 15, 16, 17, 22, 24, 25], "conflict": 7, "conform": 10, "conftest": 22, "confus": 22, "connect": [2, 3, 4, 6, 7, 10, 22], "consecut": 16, "consid": [1, 16, 20, 23], "considererd": 3, "consist": 11, "consol": 23, "constantli": 0, "construct": [7, 11], "contact": [19, 24], "contain": [3, 4, 6, 7, 9, 10, 11, 14, 16, 20, 22, 23], "content": 20, "context": [6, 23], "continu": [19, 22, 23], "contrast": 20, "contribut": [22, 24], "control": [7, 10, 11, 13, 15, 18, 20, 22], "convent": [0, 7, 8, 23], "convert": [6, 7, 9, 10, 11, 16, 23], "convert_coordin": [2, 3, 10, 16], "convert_radian": 7, "cookiecutt": 22, "coord": [2, 3, 7, 10, 11, 16], "coordin": [1, 7, 10, 11, 16, 17, 20, 23], "coords_intersect": [2, 3, 7], "copi": [0, 2, 3, 4, 6, 7, 14, 18, 22, 23], "copyright": [0, 19], "core": [7, 10, 20, 25], "corrdin": 7, "correct": [18, 19, 23], "correct_dim": [2, 3, 7], "correctli": [7, 19], "correspond": [3, 7, 10, 11, 14, 16, 19, 20, 22, 23, 24], "could": [6, 7, 18, 20, 22, 23], "counter": [7, 11], "countri": 24, "cours": [0, 6, 22, 23, 24], "cover": [7, 11], "cpu": 0, "cr": [7, 11, 20, 23], "cr10i2_146314": 24, "creat": [0, 6, 7, 9, 10, 11, 16, 17, 18, 19, 20, 21, 24, 25], "create_list": [1, 2, 3, 7], "createcopi": 9, "creation": 1, "critic": [2, 3, 15], "cruft": 22, "ctick": [11, 20, 23], "cticklabel": [11, 20, 23], "ctickprop": [11, 20, 23], "cticksiz": [11, 20, 23], "ctickweight": [11, 20, 23], "current": [1, 7, 10, 11, 16, 20, 22, 23], "custom": [0, 10, 16, 22, 23], "customtext": 22, "customtexts": 22, "customwarn": [2, 3, 15], "cutoff": [10, 14], "cyl": 23, "cylindr": 7, "d": [1, 6, 7, 9, 11, 16, 17, 20, 22, 23], "da": [1, 7, 11], "dai": 7, "daili": 24, "dask": 7, "data": [0, 1, 2, 3, 4, 6, 7, 8, 10, 11, 16, 17, 18, 19, 23, 25, 26, 27], "data_depend": [2, 3, 10], "data_stor": [2, 3, 7], "dataarrai": [7, 10, 11, 16, 20, 22, 23], "dataarrayplott": [2, 3, 7, 11], "dataarrayplotterinterfac": [2, 3, 11], "datagrid": [11, 20, 23], "datapath": 18, "dataset": [2, 3, 7, 9, 11, 16, 17, 20, 23], "datasetaccessor": [2, 3, 7], "datasetplott": [2, 3, 7, 11], "datasetplotterinterfac": [2, 3, 11], "dataslic": 20, "datastor": [7, 9], "date": [7, 10, 24], "datetim": [7, 10, 11, 23], "datetime64": [1, 7, 23], "dct": 10, "de": 24, "deal": 20, "debug": 16, "decod": [0, 2, 3, 7, 10, 11, 16, 17, 18, 22, 23], "decode_absolute_tim": [2, 3, 7], "decode_cf": 7, "decode_coord": [2, 3, 7], "decode_d": [2, 3, 7, 16], "decode_tim": 7, "decode_timedelta": 7, "decoder_class": 7, "dedent": [2, 3, 8], "deep": 7, "deeper": 21, "def": [7, 11, 22], "default": [2, 3, 4, 5, 6, 7, 8, 10, 11, 15, 16, 17, 22, 23, 26], "default_context": 22, "default_dim": 11, "default_factori": 14, "default_kei": [2, 3, 10, 18, 22], "default_level": 5, "default_path": 5, "default_print_func": [2, 3, 10], "default_slic": [7, 11], "defaultdict": [2, 3, 14], "defaultparam": [2, 3, 4, 6, 22], "defin": [1, 4, 5, 6, 7, 8, 10, 11, 13, 17, 18, 20, 22, 23], "definit": [7, 9, 10, 18, 22], "degre": 7, "delaunai": 7, "delet": [7, 11, 23], "delimit": [11, 14], "demo": [1, 20, 22, 23], "demonstr": [22, 23], "densiti": [2, 3, 11, 17, 19, 23, 26], "densityplott": 11, "densityreg": [19, 26], "depend": [2, 3, 7, 10, 11, 22, 23], "deploi": 19, "deprec": [6, 7, 16], "depress": 22, "deriv": 10, "desc": [20, 23], "describ": [7, 11, 22, 23], "descript": [2, 3, 4, 6, 7, 10, 11, 13, 22], "descriptor": [10, 11, 13, 22], "design": [1, 20, 23, 24], "desir": [7, 23], "detail": [0, 7, 11], "determin": [3, 6, 7, 10, 11, 17, 19, 23], "dev": 19, "dev0": 3, "develop": [0, 22, 23, 24, 25], "deviat": [7, 16], "dict": [3, 6, 7, 10, 11, 14], "dictformatopt": [2, 3, 10, 22], "dictionari": [3, 4, 6, 7, 10, 11, 14, 17, 18, 20, 22], "did": [7, 10, 25], "diff": [2, 3, 10], "differ": [0, 1, 5, 6, 7, 8, 10, 11, 16, 17, 20, 22, 23, 25], "difficult": 0, "difflib": [10, 14], "dim": [1, 2, 3, 7, 10, 11, 17, 18, 20, 23], "dimens": [7, 9, 10, 11, 16, 17, 18, 26], "dimension": [1, 7, 9, 11, 23, 26], "dimnam": 7, "dims_intersect": [2, 3, 7], "dinfo": 23, "direct": 13, "directli": [1, 7, 11, 16], "directori": [5, 6, 7, 11, 16, 17, 18, 19, 22, 25], "disabl": [2, 3, 7, 10, 11, 15, 23, 25], "disable_warn": [2, 3, 15], "disconnect": [2, 3, 4, 6, 7], "discuss": 0, "disk": [7, 9, 11], "displai": [16, 22, 23], "distribut": [0, 11], "divid": 7, "do": [0, 7, 9, 10, 11, 16, 17, 18, 19, 20, 22, 23, 24, 25, 26], "doc": [2, 3, 7, 11, 14, 16, 22, 23], "docrep": [8, 25], "docstr": [2, 3, 8, 10, 13, 25], "docstringextens": [2, 3, 12, 13], "docstringprocessor": 8, "docu": 11, "document": [0, 7, 8, 10, 11, 13, 16, 17, 19, 20, 22, 23, 25], "doe": [1, 5, 6, 7, 9, 10, 11, 16, 22, 23], "doi": 24, "don": [0, 10, 19, 20, 23], "done": [7, 9, 10, 19, 22, 23], "doubl": 11, "down": [0, 19, 23], "download": [16, 25], "downsid": 0, "draw": [2, 3, 7, 10, 11, 18, 26], "drawn": [7, 10, 11, 23], "drop": [7, 16], "drop_vari": 7, "ds0": 1, "ds_descript": [7, 11, 23], "dsname": [2, 3, 11, 23], "dsnames_map": [2, 3, 11], "dtinfo": 23, "dtype": [1, 2, 3, 7, 23], "due": [0, 10], "dump": [2, 3, 4, 6, 7, 11, 18], "dump_to_fil": 6, "dure": [10, 23], "dynam": [0, 11], "e": [0, 1, 6, 7, 9, 10, 11, 14, 16, 17, 19, 20, 22, 23, 25], "each": [0, 6, 7, 8, 9, 10, 11, 14, 20, 22, 23, 24, 25, 26, 27], "earth": 20, "easi": [0, 6, 11, 22, 23], "easier": 16, "easiest": 25, "easili": [7, 20, 22, 23, 24], "easilii": 22, "east": 23, "echo": [17, 22], "edg": 7, "edit": 0, "effect": [7, 10, 11, 17], "effici": [0, 6, 10, 25], "effort": [0, 22], "either": [0, 6, 7, 8, 10, 11, 17, 22, 23, 25], "elabor": 9, "element": 14, "els": [6, 7, 10, 11, 14, 25], "email": 24, "emiit": [7, 11], "emit": [2, 3, 7], "empti": [6, 7, 10, 11, 14], "enabl": [2, 3, 7, 10, 11, 17, 23], "enable_post": [2, 3, 7, 10, 11], "enclos": 23, "encod": [7, 11, 17, 19], "encode_absolute_tim": [2, 3, 7], "encount": 19, "end": [0, 2, 3, 6, 7, 10, 11, 16, 20, 22, 23], "endnot": 24, "engin": [7, 9, 16, 17], "enhanc": [10, 11, 16, 23, 24], "ensur": [7, 10, 19, 20], "enter": [7, 10], "entir": [7, 11, 16, 22, 23], "entri": [6, 14], "entrypoint": [16, 26], "env": 25, "env_kei": [5, 6], "environ": [5, 6, 18, 19, 22, 25], "equal": 7, "equival": [1, 7, 16, 20, 23], "er": 24, "err_calc": 11, "error": [6, 7, 10, 11, 14, 19], "erroralpha": 11, "escap": 11, "especi": [18, 19, 23, 24], "essenti": [5, 6, 7], "estim": 7, "etc": [0, 7, 10, 11, 18, 19, 20, 22, 23, 26], "europ": 20, "evalu": [0, 15], "even": [0, 10], "event": 7, "ever": [0, 14], "everi": [10, 18, 19, 20, 22], "everyon": 19, "everyth": [0, 6, 10, 11, 13, 19], "exact": [7, 23], "exactli": [20, 22], "exampl": [1, 16, 17, 18, 20, 22, 23, 24, 25, 26], "example_cal": 11, "example_plott": 22, "examplefmt": 22, "exampleplott": 22, "except": [7, 10, 13, 15], "exclud": [6, 7], "exclude_kei": [6, 18], "exec": [10, 23], "execut": [10, 13, 19, 23], "exist": [0, 5, 6, 7, 10, 11, 17, 18, 23], "exit": 17, "expand": 7, "expect": [11, 16, 19, 22], "experiment": 7, "explain": 22, "explicit": 7, "explicitli": [7, 10, 17, 22], "explor": [0, 10, 11], "export": [2, 3, 11, 16], "expos": 7, "ext": 13, "extend": [2, 3, 7, 11, 13, 18, 20, 23], "extended_napoleon": 13, "extendedgoogledocstr": [2, 3, 12, 13], "extendednumpydocstr": [2, 3, 12, 13], "extens": [11, 12, 13, 23], "extern": 11, "extra": 9, "extract": [7, 8, 11, 20], "extract_fmts_from_preset": [2, 3, 11], "f": [6, 7, 18, 23, 25], "face": 23, "fail": [7, 11, 19], "fail_on_error": 7, "fall": [7, 16], "fallback": [7, 16], "fals": [3, 6, 7, 10, 11, 14, 15, 17, 18, 20, 23, 25], "famili": 24, "familiar": 19, "far": [7, 11, 20, 23], "fascin": 24, "fast": [0, 22, 23, 24], "fastest": 0, "featur": [7, 19, 23], "februari": 23, "feedback": 19, "few": 0, "field": [11, 20, 23, 26], "fieldplott": [11, 18, 20, 23], "fig": [2, 3, 11], "figs2draw": [2, 3, 10], "figtitl": [11, 20, 23], "figtitleprop": [11, 20, 23], "figtitles": [11, 20, 23], "figtitleweight": [11, 20, 23], "figur": [1, 7, 10, 11, 17, 18, 20, 23], "file": [0, 4, 5, 6, 7, 9, 11, 16, 17, 18, 19, 20, 22, 23, 25, 26], "filecopyrighttext": 24, "filenam": [2, 3, 7, 11, 15, 17, 18, 23], "filename_or_obj": [7, 9], "filename_or_yaml": 17, "filepath": 7, "fill": 20, "filter": [7, 11, 16], "filter_func": [7, 11], "final": [6, 7, 11, 22, 23, 24], "find": [7, 11, 16, 19, 20, 22], "find_al": [2, 3, 4, 6, 23], "find_and_replac": [2, 3, 4, 6], "finish": 10, "finish_upd": [2, 3, 10], "first": [0, 6, 7, 10, 11, 16, 17, 18, 23, 26], "fit": [0, 10, 23, 25, 26, 27], "flag": [13, 18], "flake8": 19, "flask": 0, "fldmean": [2, 3, 7, 11, 16, 17, 23, 26], "fldmeanplott": 11, "fldpctl": [2, 3, 7, 16], "fldstd": [2, 3, 7, 16], "flexibel": 23, "flexibl": [0, 23, 24], "float": 7, "float32": [1, 23], "float64": [1, 23], "fmt": [7, 10, 16, 17, 23], "fmt1": 22, "fmt_group": [2, 3, 10, 11], "fmt_kei": 22, "fmt_str": 7, "fmt_widget": 10, "fmto": 10, "fname": [6, 7, 11, 17], "focu": 0, "folder": [11, 19, 23], "follow": [0, 1, 6, 7, 8, 11, 13, 19, 20, 22, 23], "font": 22, "fontsiz": 22, "footer": 22, "for_map": [11, 20], "forc": [7, 10, 22, 23], "forg": [16, 25], "forget": 23, "fork": 19, "form": 7, "format": [7, 10, 11, 16, 17, 19], "format_arg": 11, "format_str": [2, 3, 7, 11, 16], "format_tim": [2, 3, 10], "format_timedelta": 10, "format_timestamp": 10, "formatopion": [11, 14], "formatopt": [0, 1, 2, 3, 6, 7, 10, 11, 14, 16, 17], "formatoptionmeta": [2, 3, 10], "formatoptionwidget": 10, "formatt": [19, 22], "formerli": 7, "formula": 7, "forward": [19, 20, 23], "found": [4, 6, 7, 10, 11, 14, 16, 19, 23], "foundat": [0, 24], "four": 23, "framework": [0, 1, 3, 10, 21, 22, 23, 24, 25, 26, 27], "free": 0, "friendli": 0, "from": [0, 1, 3, 5, 6, 7, 8, 9, 10, 11, 13, 14, 16, 17, 18, 19, 20, 22, 23, 24, 26], "from_arrai": 7, "from_array_kwarg": 7, "from_dataset": [2, 3, 7, 11, 16], "from_dict": [2, 3, 7], "full": [0, 7, 10, 11, 20, 23], "full_d": [7, 11], "full_sp": 16, "fulli": [7, 13], "func": [3, 6, 7, 8, 10, 11, 14, 18, 23], "funcargpars": 25, "function": [0, 3, 5, 6, 7, 8, 9, 10, 11, 13, 14, 15, 17, 18, 19, 22, 23, 25], "fund": 24, "further": [4, 6, 7, 10, 11], "furtherdim": [7, 11], "furthermor": [6, 7, 8, 11, 16, 20, 23, 24, 25], "futur": [0, 7], "g": [0, 1, 7, 9, 10, 11, 16, 17, 19, 20, 22, 23], "galleri": [23, 24], "gather": 16, "gaussian": [1, 23], "gb": 7, "gcc": 18, "gcf": 20, "gcp": [2, 3, 10, 11, 16, 20], "gdal": [7, 9, 25], "gdal_stor": 9, "gdal_transl": 9, "gdalstor": [2, 3, 9], "geesthacht": [0, 24], "gen": 24, "gener": [0, 6, 7, 10, 11, 16, 19, 23, 24], "geo": 23, "geostationari": 23, "geotiff": [7, 9, 25], "germani": [0, 24], "get": [0, 3, 6, 7, 10, 11, 14, 16, 18, 22], "get_attr": [2, 3, 9], "get_cell_node_coord": [2, 3, 7, 16], "get_close_match": [10, 14], "get_configdir": [2, 3, 4, 6, 11, 17], "get_coord": [2, 3, 7], "get_coord_idim": [2, 3, 7], "get_coord_info": [2, 3, 7], "get_decod": [2, 3, 7, 10, 11], "get_default_valu": [2, 3, 14], "get_dim": [2, 3, 7], "get_enhanced_attr": [2, 3, 10, 11], "get_filename_d": [2, 3, 7], "get_fmt_widget": [2, 3, 10, 16], "get_fname_func": [2, 3, 7], "get_grid_type_info": [2, 3, 7], "get_i": [2, 3, 7], "get_idim": [2, 3, 7], "get_index_from_coord": [2, 3, 7], "get_mesh": [2, 3, 7], "get_metadata_for_sect": [2, 3, 7], "get_metadata_for_vari": [2, 3, 7], "get_metadata_sect": [2, 3, 7], "get_nod": [2, 3, 7], "get_plotbound": [2, 3, 7, 18], "get_project_num": [2, 3, 11], "get_projection_info": [2, 3, 7], "get_sect": [2, 3, 8], "get_t": [2, 3, 7], "get_t_metadata": [2, 3, 7], "get_tdata": [2, 3, 7], "get_titl": 10, "get_tnam": [2, 3, 7], "get_triangl": [2, 3, 7], "get_vari": [2, 3, 9], "get_variable_by_axi": [2, 3, 7, 16], "get_vers": [2, 3], "get_vfunc": [2, 3, 10], "get_x": [2, 3, 7], "get_x_metadata": [2, 3, 7], "get_xnam": [2, 3, 7, 16], "get_y_metadata": [2, 3, 7], "get_ynam": [2, 3, 7], "get_z": [2, 3, 7], "get_z_metadata": [2, 3, 7], "get_znam": [2, 3, 7], "getattr": 22, "getdriverbynam": 9, "getlogg": 15, "git": [19, 22, 25], "github": [16, 19, 25, 26, 27], "gitlab": 19, "gitter": 16, "give": [7, 10, 11, 16, 22], "given": [6, 7, 8, 10, 11, 13, 14, 16, 17, 24], "glob": [7, 22], "global": [7, 11], "gmbh": 24, "gnu": 0, "go": [0, 19], "goal": 24, "goe": [23, 25], "good": [0, 5, 24], "googl": [13, 19], "google_map_detail": [11, 20, 23], "googledocstr": 13, "govern": 19, "gpu": 0, "grant": 24, "graph": 7, "graphic": [0, 3, 16, 17, 19, 24, 25], "great": [23, 24], "gregorian": 7, "grid": [0, 7, 11, 16, 17, 18, 23], "grid_color": [11, 20, 23], "grid_label": [11, 20, 23], "grid_labels": [11, 20, 23], "grid_map": [7, 16], "grid_set": [11, 20, 23], "grid_typ": [1, 23], "gridfil": 7, "gridweight": [2, 3, 7, 16, 18], "group": [2, 3, 6, 7, 10, 11, 14, 22, 23, 26], "groupnam": [2, 3, 10, 11], "guess": 16, "gui": [0, 10, 11, 16, 17, 18, 19, 23, 24, 25, 27], "guid": [16, 22, 23, 24], "guidanc": 0, "gunzip": 7, "gz": 7, "gzip": 7, "h": [7, 17, 23], "h5netcdf": 7, "h5py": 7, "ha": [0, 1, 6, 7, 10, 11, 13, 16, 18, 19, 20, 22, 23, 26], "hagemann": 24, "hamburg": 24, "handabl": 6, "handl": [0, 7, 11, 16, 18, 19, 23], "happen": [7, 11], "hard": [9, 19, 22], "harddisk": 7, "has_chang": [2, 3, 10], "hashabl": [2, 3, 14], "have": [0, 1, 6, 7, 9, 10, 11, 16, 17, 18, 19, 20, 22, 23, 24, 25, 26], "hcdc": [0, 19, 24], "hdf": 7, "hdf5": 7, "head": 19, "header": [2, 3, 4, 6], "helmholtz": [0, 16, 19, 22, 24, 25], "help": [0, 10, 11, 19, 20, 22, 23, 24], "henc": [1, 10, 16, 20, 22, 23, 25], "here": [13, 17, 22, 24], "herebi": 1, "hereon": [0, 19, 24], "hesit": [19, 26], "hessit": 24, "hexagon": 7, "hi": [0, 22], "high": [0, 19, 23], "highli": [19, 20, 22, 25], "hint": 24, "hold": [1, 7, 10, 11], "home": [6, 16, 18, 19, 25], "hook": 19, "hope": 0, "hopefulli": 0, "horizont": 23, "hornet": 24, "host": [16, 19], "hour": 7, "how": [0, 1, 6, 7, 11, 17, 19, 20, 21, 23], "howev": [4, 7, 9, 10, 16, 18, 19, 20, 22, 23, 25, 26], "html": [7, 14, 19, 22, 23, 25], "http": [0, 5, 7, 14, 16, 19, 22, 23, 24, 25, 27], "hundr": 7, "i": [1, 3, 6, 7, 9, 10, 11, 13, 14, 16, 17, 18, 20, 21, 22, 23, 24, 25, 26, 27], "i_": 11, "icon": 0, "ident": 7, "identifi": [7, 11, 17, 18, 22, 24], "idim": [2, 3, 7], "if_exist": [6, 18], "ignor": [6, 7, 11, 16], "ignore_kei": [7, 11], "ignore_shap": 7, "illustr": 22, "imag": [0, 11], "impact": 7, "implement": [0, 7, 10, 16, 19, 20, 21, 23, 26], "impli": 0, "import": [1, 3, 6, 7, 9, 10, 11, 13, 18, 19, 20, 22, 23, 26], "import_plott": 11, "import_seaborn": 18, "imposs": [7, 11], "improv": [0, 16, 19], "includ": [6, 7, 10, 11, 14, 22, 23, 26], "include_default": 11, "include_descript": 6, "include_kei": [6, 18], "include_last": 10, "include_link": [2, 3, 10, 11], "include_traceback": 7, "inconsist": 7, "increas": [10, 11, 14], "inde": 22, "indent": [2, 3, 8, 10, 11], "independ": [0, 26], "indet": 8, "index": [7, 10, 11, 16, 19, 22, 23, 24], "index_in_list": [2, 3, 10, 22], "indic": [7, 10, 11], "individu": 7, "inevit": 0, "inf": 10, "influenc": 22, "info": [5, 7, 10, 11, 16], "inform": [1, 3, 4, 7, 10, 11, 16, 17, 20, 22, 23, 24, 25, 27], "inherit": [7, 18, 20], "inherited_memb": 13, "ini": 22, "init_accessor": [2, 3, 7], "init_kwarg": [2, 3, 10], "initi": [6, 7, 8, 10, 16, 18, 20], "initialize_plot": [2, 3, 10, 11, 16, 22], "inlin": [7, 23], "inline_arrai": 7, "inlinebackend": 23, "inplac": 16, "input": [7, 16, 22], "insert": [6, 7, 10, 11, 16, 22, 23], "insert_text": 10, "insid": [3, 20, 22, 23], "instal": [7, 9, 16, 17, 19, 22, 23, 24, 26], "instanc": [0, 1, 2, 3, 5, 6, 7, 8, 10, 11, 15, 16, 18, 20, 22, 23], "instead": [6, 7, 11, 16, 17, 20, 22, 23, 26], "institut": [19, 24], "instruct": 19, "int": [5, 7, 10, 11, 22], "int16": 7, "integ": [7, 17], "integr": [0, 19, 22, 24], "interact": [0, 1, 7, 10, 11, 19, 26], "interactivearrai": [2, 3, 7, 10, 11, 20], "interactivebas": [2, 3, 7, 10, 11, 22], "interactivelist": [1, 2, 3, 7, 10, 11, 20, 22], "interest": 23, "interfac": [0, 3, 7, 10, 11, 16, 17, 19, 20, 23, 24, 25, 27], "intermedi": [10, 11], "intern": [1, 7, 22], "interp1d": 7, "interp_bound": [11, 20, 23], "interp_kind": 18, "interpol": [7, 18], "interpret": [0, 7, 9, 16, 18, 22, 23], "intersect": [7, 11, 14], "intial": 16, "intrins": 10, "introduc": [13, 15, 23], "introduct": [21, 23], "intuit": 0, "investig": [3, 23], "invok": 19, "io": [7, 16, 27], "ioerror": 6, "ipykernel": 25, "ipython": [23, 25], "is_circumpolar": [2, 3, 7], "is_cmp": [2, 3, 11], "is_csp": [2, 3, 11], "is_data_depend": [2, 3, 10], "is_import": [2, 3, 11], "is_iter": [2, 3, 14], "is_main": [2, 3, 11, 20], "is_remote_url": [2, 3, 14], "is_unstructur": [2, 3, 7, 10, 11], "isel": [1, 2, 3, 7, 11], "isinst": 22, "isn": [7, 11], "isort": 19, "isstr": [2, 3, 14], "issu": [7, 16, 19, 22, 24, 26], "item": [6, 7, 11, 14, 18, 20, 22], "iter": [6, 7, 8, 10, 11, 14], "iter_base_vari": [2, 3, 7, 10], "iter_data": [2, 3, 10, 22], "iter_raw_data": [2, 3, 10, 22], "iteritem": [2, 3, 4, 6], "iterkei": [2, 3, 4, 6], "itertool": 14, "itervalu": [2, 3, 4, 6], "its": [0, 3, 7, 11, 23], "itself": [1, 7, 10, 11, 20, 23, 24], "januari": [1, 23], "job": [7, 10], "join": [11, 14], "join_dict": [2, 3, 14], "joined_attr": [2, 3, 11], "joss": 24, "journal": 24, "json": [3, 17], "jul": 18, "jupyt": 23, "just": [1, 7, 10, 18, 19, 20, 22, 23, 25], "k": [1, 6, 20, 23, 24], "keen": 0, "keep": [6, 7, 11, 20], "keep_al": [11, 14], "keepdim": 7, "keepshap": 7, "kei": [2, 3, 4, 6, 7, 10, 11, 14, 16, 18, 20, 22, 23], "kept": [11, 14], "kernel": 25, "keyerror": [7, 10, 14], "keyword": [6, 7, 10, 11, 14, 16, 18, 22, 23, 24], "kind": [7, 18], "know": [0, 11, 23], "known": 1, "kw": [14, 24], "kwarg": [6, 7, 8, 10, 11, 14, 15], "l": [1, 6], "label": [10, 11, 20, 22, 23], "labelprop": 11, "labels": 11, "labelweight": 11, "lack": 6, "lambda": 22, "languag": [0, 24], "larg": [20, 22, 23, 25], "larger": 6, "last": [7, 10, 14, 20, 23], "lastli": 6, "lat": [1, 7, 11, 20, 23], "later": [7, 10], "latest": 16, "latin1": [11, 17], "latitud": [7, 11, 23], "latitudin": [18, 23], "latter": [6, 20, 23], "lausann": 0, "ld": 17, "leav": [6, 7], "left": 7, "legend": [11, 22], "legendlabel": [11, 20], "len": 14, "length": [7, 10, 11], "less": 0, "lesser": 0, "let": [0, 10, 19, 20, 22, 23], "letter": 7, "lev": [1, 20, 23], "level": [5, 7, 11, 17, 20, 22, 23], "lgpl": [0, 16, 24], "librari": [7, 14], "licens": [16, 22, 24], "license_logo": 22, "like": [0, 3, 6, 7, 8, 9, 10, 11, 16, 18, 20, 22, 23, 25, 27], "limit": 0, "line": [0, 1, 11, 13, 16, 20, 23, 24, 25, 26], "linear": 18, "lineno": 15, "lineplot": [1, 2, 3, 11, 16, 17, 18, 19, 20, 23, 26], "lineplott": [11, 20], "linewidth": [11, 20], "link": [7, 10, 11, 16], "linkcheck": 16, "linreg": [16, 19, 26], "linregplott": 22, "linter": 22, "linux": [6, 16, 18, 19, 25], "list": [0, 1, 6, 7, 8, 10, 11, 13, 14, 17, 18, 19, 20, 22, 23, 24, 26], "live": 16, "ll": 7, "load": [6, 7, 10, 11, 13, 16, 17, 18, 26], "load_from_fil": [2, 3, 4, 6], "load_plugin": [2, 3, 4, 6], "load_preset": [2, 3, 11, 23], "load_project": [2, 3, 11, 23], "load_rc_from_fil": 4, "local": 19, "locat": [6, 7, 17, 18, 23, 25], "lock": [2, 3, 7, 10], "log": [4, 5, 7, 10, 11, 15, 16], "log_cfg": 5, "log_psyplot": 5, "logcfg_path": [2, 3, 4], "logger": [2, 3, 5, 7, 10, 11, 15], "login": 19, "logo": 24, "logsetup": 5, "lon": [1, 7, 11, 20, 23], "long": [10, 23], "long_nam": [1, 20, 23], "longitud": [1, 7, 11, 23], "longitudin": [18, 23], "lonlatbox": [0, 10, 11, 20, 23], "look": [6, 7, 8, 10, 11, 14, 17, 18, 19, 20, 22, 23, 24], "loop": 6, "lot": [18, 22, 23], "low": [7, 23], "lower": 16, "lowest": 22, "lp": [11, 17], "lpm": 17, "lsm": [11, 20, 23], "lzf": 7, "m": [7, 11, 17, 19, 23], "made": [0, 7, 10, 23], "mai": [7, 10, 11, 15, 17, 20, 22, 23, 25], "mail": 24, "main": [0, 1, 2, 3, 10, 11, 16, 18, 19, 20, 23, 24], "mainli": [7, 16, 18, 22, 24, 25], "maintain": [7, 19], "maintan": 19, "mainwindow": 10, "major": [7, 23], "make": [0, 1, 6, 7, 10, 11, 16, 17, 18, 22, 23, 25, 26], "make_plot": [2, 3, 7, 10, 11, 22], "makefil": [19, 22], "manag": [4, 6, 7, 8, 11, 20, 24, 25], "mani": [9, 11, 22], "manifest": 22, "manipul": [0, 10, 22], "manual": [10, 16, 19, 23, 26], "map": [2, 3, 7, 10, 11, 16, 17, 18, 19, 20, 22, 23, 25, 26, 27], "map_ext": [11, 20, 23], "mapcombin": [2, 3, 11, 17, 19, 23, 26], "maplotlib": [6, 11], "mapplot": [0, 1, 2, 3, 11, 16, 17, 18, 19, 20, 23, 26], "mapplott": 11, "mapvector": [2, 3, 11, 17, 19, 20, 23, 26], "march": [1, 23], "mark": 7, "marker": 11, "markers": 11, "mask": [7, 10, 11, 20, 23], "mask_and_scal": 7, "mask_datagrid": [11, 20, 23], "maskbetween": [11, 20, 23], "maskgeq": [11, 20, 23], "maskgreat": [11, 20, 23], "maskleq": [11, 20, 23], "maskless": [11, 20, 23], "match": [6, 7, 10, 11, 14], "matplotlib": [0, 1, 3, 6, 7, 10, 11, 16, 18, 20, 22, 23, 24, 25], "matplotlib_fnam": 6, "matrix": 16, "mattermost": [16, 24], "max": 24, "maxplot": 11, "md": 22, "me": [0, 5], "mean": [7, 10, 11, 16, 22, 23, 26], "medium": 20, "memori": [7, 23], "ment": 11, "mention": [7, 11, 25], "mentiond": 26, "merchant": 0, "merg": [7, 16, 19], "merge_request": 16, "mesh": [7, 20], "messag": [10, 11, 14, 15, 19, 24], "messi": 23, "meta": [7, 10, 11, 16, 23], "metadata": [7, 16], "meteorologi": 24, "meth": [7, 18, 23], "method": [0, 1, 6, 7, 8, 9, 10, 11, 13, 14, 16, 17, 18, 19, 20, 22, 23, 26], "methodnam": 11, "methodologi": [1, 22, 23, 25], "mfmode": 16, "microsecond": 7, "might": [0, 10, 11, 19, 22, 23, 25], "migrat": 16, "millisecond": 7, "miniconda": [16, 25], "minor": 16, "minut": 7, "misc": [10, 24], "miscallan": [10, 14, 23], "miscellan": 10, "misinterpret": 6, "miss": [7, 19], "miss_color": [11, 20, 23], "missing_valu": 7, "mode": [7, 19], "model": [0, 24], "modif": 10, "modifi": [0, 13, 18, 22, 23], "modul": [3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 15, 16, 17, 18, 19, 20, 23, 24, 25, 26], "modular": 0, "moll": 23, "mollweid": 23, "more": [0, 1, 7, 9, 10, 11, 13, 16, 20, 22, 23, 24, 25, 27], "most": [0, 7, 8, 13, 22, 23, 25], "motiv": [6, 18, 24], "move": 16, "mp": 20, "mpl": 16, "mpl_toolkit": 7, "msg": [10, 14], "msg_depr": [2, 3, 4, 6], "msg_depr_ignor": [2, 3, 4, 6], "much": 19, "multifil": 16, "multipl": [1, 6, 7, 10, 11, 14, 17, 20, 22, 25], "multiple_subplot": [2, 3, 11, 20], "must": [3, 6, 7, 8, 10, 11, 22], "my": [0, 6, 7, 17, 18, 20, 22, 23], "my_fmt": 22, "my_fmtsiz": 22, "my_geotiff": 9, "my_plot": 11, "my_plots1": 11, "my_plots2": 11, "my_plots_": 11, "my_plots_1": 11, "my_plots_1_t2m": 11, "my_plots_t2m": 11, "my_plott": 22, "my_plugin": 22, "my_project": 23, "my_python_packag": 22, "my_tiff": 9, "my_vari": [7, 11], "myfil": 17, "myformatopt": 22, "mypi": 19, "myplott": 22, "n": [1, 6, 7, 8, 10, 11, 14, 17, 20, 23, 25], "na": 7, "name": [1, 2, 3, 6, 7, 9, 10, 11, 13, 14, 16, 18, 19, 20, 22, 23, 24, 25, 26, 27], "nan": 7, "napoleon": [8, 13], "napoleon_google_docstr": 13, "napoleon_numpy_docstr": 13, "napoleon_use_param": 13, "napoleon_use_rtyp": 13, "nation": 24, "nc": [0, 1, 7, 16, 17, 20, 22, 23], "ncl": 0, "ncview": [0, 25, 27], "ndarrai": 7, "ndarraymixin": 7, "nearest": [1, 7, 11, 16, 23], "nearsideperspect": 23, "necessari": [6, 7, 10, 11, 19, 21, 22, 25], "necessarili": [0, 7, 10], "need": [0, 6, 7, 10, 11, 19, 20, 23, 25], "neither": [0, 11, 20], "nenviron": 6, "nest": 7, "netcdf": [0, 7, 9, 11, 17, 22, 23], "netcdf3": 7, "netcdf3_64bit": 7, "netcdf3_class": 7, "netcdf4": [7, 23, 24], "netcdf4_class": 7, "never": [0, 10, 20], "nevertheless": [0, 20, 22, 26], "new": [0, 1, 2, 3, 6, 7, 10, 11, 15, 16, 17, 19, 20, 21, 23, 25, 26], "new_dim": 7, "new_fig": 11, "new_nam": [7, 11], "newli": [11, 16, 20], "newlin": 8, "next": [7, 10, 20, 22], "next_available_nam": [2, 3, 7], "nice": [10, 11], "no_auto_upd": [2, 3, 7, 10, 11, 23], "no_conflict": 7, "no_valid": [2, 3, 10], "node": [7, 16], "noindex": 13, "non": [6, 7, 15, 19], "none": [3, 4, 5, 6, 7, 8, 10, 11, 13, 14, 15, 17, 18, 20, 22], "nor": 11, "norm": 11, "normal": 15, "north": 23, "northpol": 23, "northpolarstereo": 23, "note": [1, 5, 6, 7, 8, 9, 10, 11, 13, 15, 17, 19, 23, 25], "notebook": [23, 25], "noth": [0, 7, 10, 11, 16], "notifi": 13, "now": [6, 16, 17, 20, 22, 23], "np": [7, 23], "npsyplotrc": 6, "null": [7, 18], "num": [2, 3, 7, 8, 11], "number": [7, 10, 11, 14, 17, 20, 24], "numer": [0, 7], "numpi": [0, 3, 7, 8, 10, 13], "numpydocstr": 13, "nyc": 1, "nyou": 6, "o": [0, 17, 18, 23], "obj": 13, "object": [1, 6, 7, 8, 9, 10, 11, 13, 14, 18, 22, 23], "obviou": 18, "occupi": 20, "occur": 7, "offer": 13, "offici": [16, 25], "offset": 7, "often": [0, 22], "okai": [10, 11], "old": [10, 16, 20], "old_d": 17, "omit": 7, "onbasechang": [2, 3, 7], "onc": [7, 19, 23], "oncpchang": [2, 3, 11], "one": [0, 6, 7, 9, 10, 11, 14, 16, 17, 19, 20, 22, 23, 25, 26], "ones": [7, 10, 15], "onli": [0, 1, 6, 7, 8, 9, 10, 11, 14, 15, 16, 17, 18, 20, 22, 23, 24, 25, 26, 27], "onlin": 23, "onupd": [2, 3, 7], "op": 17, "open": [0, 7, 9, 10, 11, 16, 17, 18, 19, 20, 23, 24, 26], "open_dataset": [1, 2, 3, 7, 9, 16, 17, 20, 22], "open_mfdataset": [2, 3, 7, 16], "open_zarr": 7, "opendap": 7, "oper": [7, 18], "opinion": 0, "option": [0, 7, 10, 11, 13, 16, 19, 20, 23], "orcid": [19, 24], "order": [6, 7, 11, 13, 14, 19, 23], "org": [0, 7, 14, 23, 24], "origin": [7, 10, 11, 14, 17, 22], "original_valu": 7, "ortho": [20, 23], "orthogon": [20, 23], "orthograph": 23, "osx": [6, 16, 25], "other": [0, 6, 7, 8, 10, 11, 15, 16, 19, 23, 25, 26], "other_d": 17, "other_dataset": 17, "other_formatopt": 22, "other_path": 23, "otherwis": [6, 7, 10, 11, 14, 17, 22, 23, 25, 26], "our": [0, 8, 18, 19, 22, 23, 24, 25], "out": [7, 8, 10, 11, 15, 16, 17, 18, 19, 20, 22, 23, 24, 25], "output": [11, 13, 16], "outsid": [0, 7], "over": [0, 6, 7, 10, 11, 16, 19, 23, 26, 27], "overli": [11, 23, 26], "overrid": 7, "overview": [19, 21], "overwrit": [1, 6, 18], "overwritten": [6, 7], "own": [0, 4, 10, 18, 19, 20, 21, 24, 25], "owner": [2, 3, 7], "p": [11, 17, 20, 22, 23, 24], "pack": [11, 20, 23], "packag": [0, 1, 2, 5, 6, 7, 8, 9, 10, 11, 13, 14, 16, 17, 18, 20, 23, 24, 25, 26, 27], "page": [16, 19, 24, 26], "pair": [6, 10, 20], "panda": [3, 7], "panopli": 0, "paper": 17, "parallel": [7, 19, 23], "param": [8, 14], "param_like_sect": [2, 3, 8], "param_list": 14, "paramet": [3, 4, 5, 6, 7, 8, 9, 10, 11, 13, 14, 16, 17, 18, 23], "parameterin": 18, "paraview": 0, "parent": [2, 3, 6, 8, 10, 22], "pari": 1, "pars": [7, 11, 13, 17], "parser": [17, 25], "part": [0, 6, 7, 11, 13, 18, 20, 22], "particip": 19, "particular": [0, 7, 20], "pass": [7, 10, 11, 14, 16, 22, 23], "past": [0, 23], "patch": 16, "path": [4, 5, 6, 7, 9, 11, 14, 17, 18, 23], "pathlib": 7, "pathnam": 6, "pattern": [2, 3, 4, 6, 7, 11], "pattern_bas": 6, "pcolormesh": 7, "pctl": 7, "pdf": [11, 17], "pdfpage": 11, "peopl": 24, "per": 11, "percent": 7, "percentil": [7, 16], "perform": 7, "perman": [7, 10, 18], "person": 22, "pher": [7, 11], "philipp": [0, 24], "pick": 7, "pickl": [10, 11, 17, 23], "piec": 0, "pip": [19, 23], "pipelin": 16, "pkl": [17, 23], "place": [10, 13, 16, 22, 23], "planck": 24, "platecarre": [11, 23], "platform": [6, 25], "pleas": [0, 6, 7, 15, 19, 23, 24, 25, 26], "plot": [0, 2, 3, 7, 10, 11, 16, 17, 18, 19, 20, 22, 24, 25, 26], "plot2d": [2, 3, 7, 11, 17, 19, 23, 26], "plot_data": [2, 3, 10, 11], "plot_data_decod": [2, 3, 10], "plot_fmt": [2, 3, 10, 22], "plot_func": 11, "plot_method": [16, 17], "plotmethod": [1, 11, 16, 26], "plotter": [1, 2, 3, 6, 7, 10, 11, 16, 18, 21, 23], "plotter_cl": [2, 3, 11, 18], "plotter_nam": 11, "plotterinterfac": [2, 3, 11], "plt": 20, "plu": 22, "plugin": [0, 3, 6, 7, 11, 14, 16, 17, 18, 20, 21, 23, 24, 25, 27], "plugin_entrypoint": [2, 3, 14], "plugin_nam": 26, "pm": 17, "png": [11, 16, 22], "po": 7, "point": [0, 6, 7, 11, 14, 23, 26], "pose": 10, "posit": [7, 8, 10, 22, 23], "possibl": [0, 6, 7, 8, 10, 11, 13, 14, 16, 17, 18, 22, 23], "possible_kei": 14, "possibli": 14, "post": [2, 3, 5, 7, 10, 11, 17, 20, 24], "post_process": 10, "post_tim": [2, 3, 10, 11, 20, 23], "postprocdepend": [2, 3, 10], "postprocess": [2, 3, 10, 23], "posttim": [2, 3, 10], "potenti": 7, "power": 0, "practic": 5, "pre": 19, "preced": 26, "precis": 11, "predefin": 11, "prefer": [7, 18, 24], "prefer_list": [7, 11], "preprocess": 25, "present": [6, 22, 24], "preserv": 14, "preset": [11, 16, 17, 18], "pretti": 0, "prevent": 10, "previou": [1, 22, 25], "principl": [0, 22, 23, 26], "print": [1, 3, 6, 10, 11, 13, 17, 18, 19, 22], "print_func": [2, 3, 11], "prioriti": [2, 3, 10, 16, 22], "pro": 23, "probabl": [22, 23], "problem": [7, 23], "process": [10, 11, 13, 17, 22, 23, 25], "process_docstr": [2, 3, 12, 13], "process_exampl": 25, "produc": 23, "program": [0, 6, 17], "project": [1, 2, 3, 6, 7, 10, 11, 16, 17, 18, 19, 22, 24, 26, 27], "project_cl": [2, 3, 11], "project_plott": 11, "project_slug": 22, "projectplott": [2, 3, 11, 23], "proleptic_gregorian": 7, "promis": 0, "propag": 6, "properti": [6, 7, 10, 11], "propos": 0, "provid": [0, 6, 7, 8, 10, 11, 13, 16, 17, 19, 20, 21, 22, 23, 24, 26], "pseudo": 7, "pspylotruntimewarn": 15, "psy": [0, 1, 2, 3, 7, 11, 16, 18, 19, 20, 22, 23, 24, 25, 26, 27], "psy_map": [3, 11, 18, 20, 23, 26], "psy_reg": [22, 26], "psy_simpl": [3, 11, 18, 22, 26], "psy_view": 27, "psyplot": [1, 2, 5, 6, 7, 8, 9, 10, 11, 13, 14, 15, 16, 17, 18, 21, 23, 24, 25, 27], "psyplot_doc": 25, "psyplot_fnam": [2, 3, 4, 6, 18, 25], "psyplot_gui": [3, 10, 27], "psyplot_test": 25, "psyplotconfigdir": [6, 18], "psyplotcrit": [2, 3, 15], "psyplotdata": 6, "psyplotdocstringprocessor": [2, 3, 8], "psyplotrc": [6, 16, 18], "psyplotruntimewarn": [2, 3, 15], "psyplotwarn": [2, 3, 15], "public": 0, "publish": [0, 24], "pull": [0, 26], "purpos": [0, 18, 24], "push": 19, "put": [0, 6, 7, 11, 16], "pwd": [6, 7, 11], "py": [13, 19, 22, 25], "py37": 25, "pydap": 7, "pyplot": [7, 11, 20, 22, 23], "pyproject": [19, 22], "pyqt": 7, "pyqt4": 7, "pyqt5": 10, "pyqtboundsign": 7, "pysplot": 10, "pytest": 25, "python": [0, 5, 7, 8, 9, 13, 14, 15, 16, 17, 18, 19, 22, 23, 25], "python2": [11, 17], "python3": [11, 17, 25], "pyyaml": 25, "q": 7, "qtcore": 7, "qtwidget": 10, "qualifi": 13, "qualiti": 19, "quantil": 7, "quesion": 24, "question": [0, 19], "queue": [7, 10], "quick": [22, 23], "quit": [17, 19, 22], "qwidget": 10, "r": [0, 19, 20, 23, 24], "radian": 7, "rais": [6, 7, 8, 10, 11, 14], "raise_error": [6, 10, 14], "ran": 10, "rang": [0, 7, 20, 23], "raster": [0, 7, 9, 25], "rather": [0, 7, 11], "raw": [10, 11, 25], "raw_data": [2, 3, 10, 22], "rb": 23, "rc": [2, 3, 4, 6, 7, 10, 17, 20], "rc_file": 17, "rcparam": [2, 3, 4, 6, 7, 10, 11, 16, 17, 23, 26], "rcsetup": [4, 6, 18, 22, 25, 26], "rdbu_r": 23, "re": [6, 20, 22, 23], "reach": 11, "read": [7, 9, 11, 13, 23, 25], "readm": 22, "readthedoc": [16, 19], "real": [7, 11, 20, 22, 26], "realli": [0, 22, 23], "reason": [10, 11, 23], "receiv": 0, "recommend": [6, 9, 10, 19, 20, 22, 23, 25], "recurs": 22, "red": [1, 17, 23], "redirect": 16, "redistribut": 0, "redraw": [7, 10, 23], "reenabl": [7, 10], "refer": [6, 7, 11, 20, 24], "reg": [0, 16, 19, 25, 27], "regardless": 7, "regex": 7, "region": 7, "regist": [1, 7, 10, 11, 19, 22, 23, 26], "register": 7, "register_dataset_accessor": 1, "register_decod": [2, 3, 7], "register_plott": [2, 3, 11, 18, 22], "registered_plott": 18, "regress": [10, 26, 27], "regular": 7, "reimplement": 7, "reimplet": 7, "reinit": [2, 3, 10, 11], "reiniti": 10, "rel": [7, 11], "relat": 15, "relationship": 20, "releas": [0, 10, 11, 16, 24], "relev": 7, "reli": [7, 16], "reload": 23, "remain": [11, 14], "rememb": 14, "remov": [2, 3, 4, 6, 7, 10, 11, 23, 25], "remove_onli": 11, "renam": [2, 3, 7, 11], "repeat": 0, "replac": [2, 3, 4, 6, 7, 8, 10, 11, 16, 17, 22, 23], "replot": [7, 10, 16, 17], "report": [19, 24], "repositori": [0, 16, 19, 24, 25], "repres": [6, 7, 10, 11, 13, 20, 22], "represent": 14, "reproduc": [7, 11, 19, 24], "request": [0, 16, 19, 26], "requir": [3, 8, 9, 10, 16, 17, 19, 22, 23], "requires_clear": [2, 3, 10, 23], "requires_replot": [2, 3, 10, 16], "reset": [6, 10], "resourc": 7, "respect": 11, "respons": 20, "restor": 23, "restructur": [10, 11, 19], "result": [0, 1, 6, 7, 10, 11, 16, 17, 20, 22, 23], "retain": 6, "return": [3, 5, 6, 7, 8, 10, 11, 14, 16, 20, 22], "returnda": 16, "returnlin": 16, "returnmap": 16, "reus": [0, 8, 19, 23, 24], "review": 19, "rewritten": 22, "ri": 24, "rich": 0, "right": [6, 7, 20, 26], "risk": 10, "rlock": 10, "robin": 23, "robinson": 23, "root": [0, 19, 23], "rotat": 23, "rotatedpol": 23, "round": 20, "routin": 7, "row": 11, "rst": 19, "rubric": 13, "run": [0, 3, 10, 11, 17, 19, 23], "runtim": 15, "runtimewarn": 15, "safe": [7, 10, 25], "safe_list": [2, 3, 4, 6], "sai": 23, "said": 22, "same": [0, 1, 6, 7, 8, 10, 11, 13, 16, 18, 20, 22, 23, 26], "satisfi": 23, "save": [6, 7, 9, 11, 16, 17, 18, 22], "save_preset": [2, 3, 11, 23], "save_project": [2, 3, 11, 16, 17, 23], "savefig": 11, "saw": 23, "scalar": [11, 20, 23, 26], "scale": 7, "scale_factor": 7, "schedul": 7, "scienc": 24, "scientif": 0, "scientist": [0, 24], "scipi": [0, 7, 16], "scp": [2, 3, 11, 16, 20], "script": [0, 10, 11, 17, 19, 22, 24], "sdesc": 23, "seaborn": [0, 16, 17, 18], "search": [6, 7, 24], "second": [6, 7, 10, 11, 17, 20, 23], "secondplott": 22, "section": [1, 7, 8, 13, 21, 22, 23, 24], "secur": [10, 17], "see": [0, 1, 4, 6, 7, 10, 11, 13, 14, 16, 17, 18, 19, 20, 22, 23, 24, 25, 27], "seen": [7, 11, 14, 23], "sel": [1, 2, 3, 7, 11, 23], "select": [1, 7, 10, 11, 16, 19], "self": [1, 10, 22, 23], "send": 0, "sens": 22, "sensibl": [10, 14], "separ": [0, 7, 11, 16, 17, 23, 26], "sequenc": 7, "seriou": [10, 11], "serv": [7, 10, 11, 13, 20], "server": [0, 23], "session": [22, 23], "set": [0, 4, 5, 6, 7, 10, 11, 13, 14, 16, 17, 18, 19, 20, 22, 23, 25], "set_data": [2, 3, 10], "set_decod": [2, 3, 10], "set_fonts": 22, "set_styl": 17, "set_text": 22, "set_titl": 10, "set_valu": [2, 3, 10], "setup": [2, 3, 5, 10, 12, 13, 19, 20, 22, 23, 25], "setup_coord": [2, 3, 7], "setup_log": [2, 3, 4, 5], "sever": [16, 19, 20, 22, 23, 27], "shall": [3, 6, 7, 8, 10, 11, 14, 18, 22, 23], "shallow": 14, "shape": [7, 10, 11], "shapefil": 0, "share": [2, 3, 10, 11], "shareabl": 22, "shared_bi": [2, 3, 10], "shell": 0, "shift": 7, "shiftlon": [2, 3, 7], "ship": 23, "short": [0, 10], "shortcut": 11, "should": [0, 6, 7, 10, 11, 13, 17, 18, 22, 23, 25, 26], "show": [1, 2, 3, 10, 11, 17, 18, 23, 26], "show_doc": [2, 3, 10, 20, 23], "show_exampl": 11, "show_fmtkei": 14, "show_inherit": 13, "show_kei": [2, 3, 10, 20, 23], "show_plot_method": [2, 3, 11, 23], "show_summari": [2, 3, 10, 20, 23], "shown": [7, 11, 17, 23], "showwarn": 15, "side": 25, "signal": [2, 3, 7, 11], "silent": 7, "similar": [7, 10, 14], "simpl": [0, 2, 3, 7, 11, 16, 17, 19, 20, 22, 23, 25, 26, 27], "simple2dplott": 11, "simpleplotterbas": 11, "simplevectorplott": 11, "simpli": [6, 7, 10, 11, 17, 18, 19, 20, 22, 23, 25], "simplifi": 8, "sinc": [1, 6, 7, 10, 11, 16, 20, 22, 23], "singl": [3, 6, 7, 10, 11, 16, 23], "size": [1, 7, 17, 22, 23], "skill": 25, "skip": [3, 7, 19, 22], "slice": [1, 7, 11], "slicer": 7, "slightli": 17, "slow": 19, "small": 0, "sn": 17, "snf": [16, 24], "so": [0, 1, 7, 10, 11, 20, 22, 23, 25], "social": 19, "softwar": [0, 19], "solut": 25, "some": [0, 17, 19, 22, 23], "some_kei": [6, 16], "someth": [0, 3, 16, 19, 22], "sometim": 23, "sommer": [0, 24], "soon": [7, 10], "sophist": 22, "sort": [6, 7, 11, 14, 23], "sort_kwarg": [2, 3, 14], "sorten": 7, "sorter": 11, "sound": 14, "sourc": [0, 1, 3, 5, 6, 7, 8, 9, 10, 11, 13, 14, 15, 19, 24, 27], "south": 23, "southpol": 23, "southpolarstereo": 23, "sp": [1, 16, 20, 23], "sp1": 16, "sp2": 16, "space": 6, "spatial": [7, 16], "spdx": 24, "special": [0, 23, 24], "specif": [6, 7, 10, 16, 19, 20, 23, 24, 26], "specifi": [5, 6, 7, 8, 10, 11, 13, 14, 16, 17, 18, 22, 23, 25], "speed": [0, 20], "sphinx": [12, 13, 19], "sphinxconfig": 13, "sphinxext": [2, 3, 13], "split": [8, 27], "squeez": [7, 11], "src_cr": 7, "stabl": [7, 22], "stack": [7, 24], "stacklevel": 7, "stackoverflow": 14, "stage": [10, 22], "stand": [14, 23], "standard": [0, 1, 7, 16, 19, 22, 23], "standard_nam": 1, "standardize_dim": [2, 3, 7, 11], "start": [0, 2, 3, 7, 10, 22, 24], "start_upd": [2, 3, 7, 10, 11, 18, 23], "startup": 26, "state": 11, "statement": [19, 26], "static": [7, 10, 11, 19], "statist": 0, "statu": 24, "steadi": 22, "stefan": 24, "step": [7, 11, 19, 22, 23], "stereo": 23, "stereograph": 23, "still": [0, 19, 22, 23], "stock_img": [11, 20, 23], "store": [6, 7, 9, 10, 11, 16, 18, 20, 23], "str": [4, 5, 6, 7, 8, 9, 10, 11, 13, 14, 17, 22, 23], "straight": [20, 22, 23], "straightforward": 0, "stream": 20, "strftime": [11, 23], "string": [3, 6, 7, 8, 10, 11, 13, 14, 16, 22, 23, 26], "strongli": 22, "structur": [0, 1, 6, 20, 23], "style": [7, 13, 16, 17], "sub": [11, 16], "subclass": [7, 8, 10, 11, 16, 20, 22, 23], "subdict": [2, 3, 4, 6, 10], "subject": 11, "submodul": 2, "subpackag": [2, 23], "subplot": [11, 20, 23], "subplot_kw": 11, "subplotbas": 11, "subproject": [0, 1, 7, 10, 11, 16, 20, 24], "subscrib": 24, "subsequ": [7, 11], "subset": [1, 6, 7], "substitut": 8, "suffici": 0, "suggest": 19, "suitabl": [6, 7, 9], "summari": [2, 3, 10, 11, 23], "supp": 19, "supplementari": 18, "suppli": [6, 7], "support": [0, 7, 9, 13, 16, 19, 24, 25], "supports_spatial_sl": [2, 3, 7], "suppos": 10, "sure": [0, 6, 7, 10, 11, 17, 23, 25], "swiss": 24, "switch": 7, "sym_lim": 11, "syntax": [6, 16], "system": [6, 7, 25], "systemat": 23, "t": [0, 7, 10, 11, 16, 17, 18, 19, 20, 23, 24], "t2m": [1, 7, 11, 17, 20, 22, 23], "t_format": 7, "t_pattern": [2, 3, 7], "tabl": [1, 10, 11, 23], "tackl": 0, "take": [3, 6, 7, 10, 11, 16, 19], "taken": [5, 6, 7, 10, 13, 14], "talk": 22, "target": [7, 20, 23], "target_cr": 7, "task": [7, 26], "team": 24, "techniqu": [0, 19], "tell": [1, 22], "temperatur": [1, 20, 23], "templat": [16, 19], "tempor": 7, "temporari": [7, 11, 16], "temporarili": [10, 23], "term": 0, "termin": [11, 23], "test": [7, 13, 14, 16, 17, 19, 22, 23], "test_import": 22, "test_plugin": 25, "text": [8, 10, 11, 19, 20, 22, 23], "than": [0, 5, 7, 10, 11, 16, 17, 23], "thank": [19, 24], "the_chosen_str": 22, "thei": [0, 6, 7, 10, 11, 14, 16, 17, 18, 20, 22, 23], "them": [0, 6, 7, 11, 14, 19, 20, 22, 23, 24, 27], "therebi": [20, 22], "therefor": [0, 18, 23], "theri": [7, 10], "thi": [0, 1, 4, 5, 6, 7, 9, 10, 11, 13, 14, 15, 16, 17, 18, 20, 21, 22, 23, 25, 26], "thing": [1, 7, 11], "think": [0, 23, 26], "third": [7, 22, 23], "those": [6, 7, 10, 11, 18, 20, 22, 23], "thread": [7, 10], "three": [7, 11, 15, 20, 22, 25], "through": [0, 6, 7, 10, 20, 22, 23, 25], "ti": 24, "tick": [10, 23], "ticksiz": [10, 11], "tickweight": 11, "tiff": 9, "tight": [11, 17, 20, 23], "time": [0, 1, 6, 7, 10, 11, 18, 19, 20, 22, 23, 26], "timedelta": [7, 10], "timeslic": 1, "timestep": 17, "tinfo": 23, "titl": [1, 6, 10, 11, 17, 18, 20, 22, 23, 24], "titleprop": [11, 20, 23], "titles": [11, 20, 23], "titleweight": [11, 20, 23], "to_arrai": [2, 3, 7], "to_dataarrai": 7, "to_datafram": [2, 3, 7], "to_interactive_list": [2, 3, 7], "to_netcdf": [2, 3, 7], "to_slic": [2, 3, 7], "tobia": 24, "todefault": [7, 10], "todo": 24, "togeth": [7, 11, 22], "toml": [19, 22], "ton": 0, "too": [6, 8, 10, 18, 22, 23], "tool": [0, 19, 24], "top": 0, "tornado": 0, "total": 0, "tox": 22, "trace": [2, 3, 4, 6, 10], "traceback": 7, "tracker": 24, "transform": [7, 10, 11, 20, 23], "translat": [7, 9], "transpos": [11, 20, 23], "tri": [7, 11, 17, 18, 23], "triangl": 7, "triangul": 7, "triangular": 7, "troubl": 19, "true": [1, 3, 6, 7, 10, 11, 13, 14, 15, 16, 17, 18, 20, 22, 23], "trust": [10, 11, 17, 18], "try": [0, 7, 23], "tupl": 11, "turn": 13, "two": [0, 1, 7, 9, 10, 11, 17, 20, 22, 23], "txt": [19, 22], "ty": 24, "type": [1, 3, 4, 5, 6, 7, 8, 10, 11, 13, 14, 16, 19, 20, 22, 23, 24, 25, 26], "type1": 13, "type2": 13, "u": [19, 20, 22, 23, 24, 25], "u_var": 11, "ugrid": [0, 7], "ugriddecod": [2, 3, 7], "ultim": 24, "under": [0, 11, 16, 18, 25], "underli": [7, 18], "understand": 19, "undo": 10, "undoc_memb": 13, "undon": 10, "uniform": 20, "union": 7, "uniqu": [7, 11, 14, 20, 22], "unique_everseen": [2, 3, 14], "unit": [1, 7, 10, 23], "univers": 0, "unless": [6, 7, 10], "unregist": 11, "unregister_plott": [2, 3, 11], "unsaf": [7, 11], "unshar": [2, 3, 10, 11], "unshare_m": [2, 3, 10, 11], "unsort": 6, "unstruct": 7, "unstructur": [0, 7, 10, 11, 16, 24], "until": 11, "up": [0, 4, 5, 7, 10, 11, 17, 20, 22, 23], "updat": [2, 3, 4, 6, 7, 10, 11, 16, 17, 18, 20, 22], "update_after_plot": [2, 3, 10], "update_from_defaultparam": [2, 3, 4, 6, 22], "update_oth": 10, "update_shar": 10, "uphold": 19, "upon": [7, 23, 25], "ur": 24, "url": [7, 24], "us": [0, 1, 3, 5, 6, 7, 8, 9, 10, 11, 13, 14, 15, 16, 17, 18, 19, 20, 24, 26], "usag": [0, 1, 6, 13, 16, 20, 24, 25], "use_cdo": [7, 18], "use_cftim": 7, "use_rel_path": [7, 11], "use_tim": 11, "useless": 23, "user": [0, 3, 6, 16, 17, 18, 19, 22, 23, 24, 25], "userdict": 6, "userwarn": 15, "usual": [0, 6, 7, 10, 20, 22, 23], "util": [0, 14, 24], "v": [6, 17, 20, 23], "v_var": 11, "val": [6, 14, 22], "val1": 17, "val2": 17, "valid": [2, 3, 4, 6, 7, 10, 11, 13, 14, 19, 22], "validate_bool": [2, 3, 4, 6], "validate_bool_maybe_non": [2, 3, 4, 6], "validate_dict": [2, 3, 4, 6], "validate_files_exist": [2, 3, 4, 6], "validate_path_exist": [2, 3, 4, 6], "validate_str": [2, 3, 4, 6], "validate_stringlist": [2, 3, 4, 6], "validate_stringset": [2, 3, 4, 6], "valu": [1, 2, 3, 4, 6, 7, 10, 11, 13, 14, 18, 20, 22, 23, 26], "value2pickl": [2, 3, 10], "value2shar": [2, 3, 10], "valueerror": [6, 7, 11, 23], "var": 7, "variabl": [1, 5, 6, 7, 9, 10, 11, 13, 16, 17, 18, 20, 22, 23], "variable_nam": 17, "variablein": 7, "variou": 16, "varnam": 22, "vbound": 11, "vcbar": 11, "vcbarspac": 11, "vclabel": 11, "vclabelprop": 11, "vclabels": 11, "vclabelweight": 11, "vcmap": 11, "vctick": 11, "vcticklabel": 11, "vctickprop": 11, "vcticksiz": 11, "vctickweight": 11, "vector": [2, 3, 6, 10, 11, 17, 19, 20, 23, 26], "vectorplott": 11, "veloc": 20, "venv": 19, "verbos": 10, "veri": [0, 7, 11, 20, 22, 23], "verifi": 19, "version": [0, 3, 7, 10, 11, 13, 16, 17, 24, 25], "vertic": [7, 17, 18, 23], "via": [0, 1, 7, 10, 11, 15, 16, 17, 18, 19, 20, 22, 23, 24, 26, 27], "victorlin": 5, "view": [0, 19, 24, 25], "violin": [11, 23, 26], "violinplot": [2, 3, 11, 17, 23, 26], "violinplott": 11, "viridi": 20, "virtual": 19, "visibl": 24, "visit": [23, 25], "visual": [0, 1, 3, 7, 10, 11, 16, 17, 19, 22, 23, 25, 26, 27], "visuali": 25, "volum": 24, "vplot": 11, "vulner": 17, "w": [7, 18], "wa": [7, 11, 13, 20], "wai": [22, 23, 25], "want": [0, 7, 9, 10, 11, 18, 19, 20, 22, 23, 24, 25, 26], "warn": [2, 3, 6, 7, 11, 15, 16, 23], "warranti": 0, "we": [0, 1, 3, 6, 7, 8, 9, 11, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26], "weak": 6, "web": 0, "webpag": 24, "websit": 24, "week": 6, "weight": [7, 16, 18], "welcom": [22, 23, 24], "well": [0, 1, 7, 10, 11, 16, 23], "were": [7, 22, 23], "west": 23, "what": [6, 7, 10, 11, 13, 17, 20, 23], "whatev": [6, 7, 23], "when": [0, 6, 7, 10, 11, 13, 16, 18, 22, 23, 26], "where": [0, 1, 6, 7, 10, 11, 19, 20, 22, 23, 26], "wherea": [7, 11, 20], "whether": [0, 3, 7, 10, 11, 14, 18, 22, 23], "which": [0, 7, 10, 11, 13, 14, 16, 18, 19, 20, 22, 23, 25, 26], "whichev": 7, "while": [0, 7, 10], "white": 6, "white_blue_r": 20, "whith": 7, "whole": [7, 11, 20, 23], "whose": [6, 7], "widget": [10, 16, 18], "width": 11, "wind": [20, 23], "window": [18, 19, 23], "winter": 23, "wish": 19, "with_gui": [2, 3], "with_plott": [2, 3, 7, 11], "within": [0, 22, 23], "without": [0, 6, 7, 11, 16, 18], "word": [10, 26], "work": [0, 1, 7, 10, 11, 16, 20, 22, 23, 24, 26], "world": 0, "worth": 0, "would": [0, 1, 6, 20, 22, 23], "wrapper": [11, 15], "write": [6, 7, 18, 23], "written": [0, 6, 19, 25], "wrong": 23, "www": [0, 24], "x": [7, 10, 11, 16, 17, 18, 20, 22, 23, 26], "xarrai": [0, 3, 7, 9, 10, 11, 16, 20, 22, 23, 24, 25], "xarray_obj": 7, "xaxis_invert": 16, "xgrid": [11, 20, 23], "xlabel": [11, 22], "xlim": 11, "xname": 23, "xr": [7, 10, 20], "xrang": 11, "xrotat": 11, "xtick": [1, 11], "xticklabel": [1, 11, 20], "xtickprop": 11, "xx": 22, "y": [7, 10, 11, 16, 18, 20, 22, 23, 26], "yaml": [4, 5, 6, 11, 16, 17, 18, 19, 22, 24], "yaxis_invert": 16, "ye": 26, "year": 24, "yet": [7, 19, 24], "ygrid": [11, 20, 23], "ylabel": [11, 20], "ylim": 11, "yml": [4, 6, 16, 17, 18, 23, 25], "you": [0, 1, 4, 6, 7, 8, 9, 10, 11, 13, 15, 16, 17, 18, 19, 20, 22, 23, 24, 25, 26], "your": [0, 1, 4, 6, 7, 9, 10, 11, 13, 18, 19, 20, 21, 24, 25, 26], "yourreferenceher": 24, "yourself": [0, 6, 10, 11, 23, 25, 26], "yrang": 11, "yrotat": 11, "ytick": 11, "yticklabel": 11, "ytickprop": 11, "z": [7, 10, 11, 16, 17, 18, 20, 23], "zarr": 7, "zenodo": 24, "zentrum": [0, 19, 24], "zero": 7, "zlib": 7, "zname": 20, "zonal": [20, 23], "zunit": 20, "\u00aa": 23, "\u00b2": 23, "\u00b3": 23, "\u00b5": 23, "\u00b9": 23, "\u00ba": 23, "\u00bc": 23, "\u00bd": 23, "\u00be": 23, "\u03c9": 23, "\u215b": 23, "\u215c": 23, "\u215d": 23, "\u215e": 23}, "titles": ["About psyplot", "xarray Accessors", "API Reference", "psyplot package", "psyplot.config package", "<no title>", "<no title>", "<no title>", "<no title>", "<no title>", "<no title>", "<no title>", "psyplot.sphinxext package", "<no title>", "<no title>", "<no title>", "Changelog", "Command line usage", "Configuration", "Contribution and development hints", "The psyplot framework", "Developers guide", "How to implement your own plotters and plugins", "Getting started", "Interactive data visualization with python", "Installation", "Psyplot plugins", "Subprojects", "ToDos"], "titleterms": {"0": 16, "1": 16, "2": 16, "3": 16, "4": 16, "5": 16, "The": [1, 18, 19, 20, 23, 26], "about": 0, "accessor": 1, "acknowledg": 24, "ad": [16, 23], "annot": 19, "api": 2, "appear": 23, "argument": 17, "arrai": 1, "author": 0, "automat": 23, "befor": 19, "build": 25, "chang": 16, "changelog": 16, "choos": 23, "cite": 24, "class": 20, "code": 19, "command": 17, "conda": 25, "conduct": 19, "config": 4, "configur": [18, 23], "content": 19, "contribut": 19, "control": 23, "creat": [1, 22, 23], "data": [20, 22, 24], "dataarrai": 1, "dataset": 1, "datasetaccessor": 1, "default": 18, "depend": 25, "develop": [19, 21], "dict": 22, "dimens": 23, "direct": 23, "doc": [19, 25], "document": 24, "environ": 26, "exampl": [3, 6, 7, 9, 10, 11, 13], "exclud": 26, "exist": 26, "filter": 23, "first": 22, "fix": [16, 19], "formatopt": [18, 20, 22, 23], "framework": [19, 20], "from": 25, "function": 20, "get": [19, 23, 24], "guid": 21, "handl": 22, "helper": 19, "hint": 19, "how": [22, 24, 25, 26], "i": [0, 19], "implement": 22, "indic": 24, "info": 17, "initi": 23, "instal": 25, "interact": [20, 22, 23, 24], "interactivearrai": 1, "interactivebas": 20, "interfac": 22, "know": 19, "licens": [0, 19], "line": 17, "load": 23, "make": 19, "manag": 23, "multipl": 23, "name": 17, "new": 22, "object": 20, "option": [17, 25], "other": 22, "output": 17, "own": [22, 23], "packag": [3, 4, 12, 19, 22], "pip": 25, "plot": [1, 23], "plotter": [20, 22], "plugin": [22, 26], "posit": 17, "post": 23, "preset": 23, "project": [20, 23], "psyplot": [0, 3, 4, 12, 19, 20, 22, 26], "psyplot_plotmethod": 26, "psyplot_plugin": 26, "python": 24, "rcparam": [18, 22], "refer": 2, "remov": 16, "requir": 25, "run": 25, "save": 23, "script": 23, "second": 22, "shortcut": 19, "should": 19, "skeleton": 19, "slice": 23, "softwar": 24, "solut": 22, "sourc": 25, "sphinxext": 12, "start": [19, 23], "submodul": [3, 4, 12], "subpackag": 3, "subproject": 27, "tabl": [19, 24], "templat": 22, "test": 25, "thi": [19, 24], "todo": [7, 28], "touch": 24, "uninstal": 25, "updat": [1, 19, 23], "us": [22, 23, 25], "usag": [17, 23], "v1": 16, "variabl": 26, "via": 25, "visual": [20, 24], "what": [0, 19], "why": 0, "xarrai": 1, "your": [22, 23]}}) \ No newline at end of file diff --git a/setup.py b/setup.py deleted file mode 100644 index c6adc8a..0000000 --- a/setup.py +++ /dev/null @@ -1,12 +0,0 @@ -# SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH -# -# SPDX-License-Identifier: CC0-1.0 - -"""Setup script for the psyplot package.""" -import versioneer -from setuptools import setup - -setup( - version=versioneer.get_version(), - cmdclass=versioneer.get_cmdclass(), -) diff --git a/tests/_base_testing.py b/tests/_base_testing.py deleted file mode 100644 index 8502096..0000000 --- a/tests/_base_testing.py +++ /dev/null @@ -1,39 +0,0 @@ -"""Base testing module.""" - -# SPDX-FileCopyrightText: 2016-2024 University of Lausanne -# SPDX-FileCopyrightText: 2020-2021 Helmholtz-Zentrum Geesthacht - -# SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH -# -# SPDX-License-Identifier: LGPL-3.0-only - -import os -import os.path as osp -import subprocess as spr -import sys - -test_dir = osp.dirname(__file__) - - -os.environ["PSYPLOT_PLUGINS"] = "yes:psyplot_test.plugin" - - -def get_file(fname): - """Get the full path to the given file name in the test directory""" - return osp.join(test_dir, fname) - - -# check if the seaborn version is smaller than 0.8 (without actually importing -# it), due to https://github.com/mwaskom/seaborn/issues/966 -# If so, disable the import of it when import psyplot.project -try: - sns_version = spr.check_output( - [sys.executable, "-c", "import seaborn; print(seaborn.__version__)"] - ) -except spr.CalledProcessError: # seaborn is not installed - pass -else: - if sns_version.decode("utf-8") < "0.8": - import psyplot - - psyplot.rcParams["project.import_seaborn"] = False diff --git a/tests/circumpolar_test.nc b/tests/circumpolar_test.nc deleted file mode 100644 index 158aee5..0000000 Binary files a/tests/circumpolar_test.nc and /dev/null differ diff --git a/tests/circumpolar_test.nc.license b/tests/circumpolar_test.nc.license deleted file mode 100644 index 919c9c1..0000000 --- a/tests/circumpolar_test.nc.license +++ /dev/null @@ -1,3 +0,0 @@ -SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH - -SPDX-License-Identifier: CC0-1.0 diff --git a/tests/conftest.py b/tests/conftest.py deleted file mode 100644 index b3bc92d..0000000 --- a/tests/conftest.py +++ /dev/null @@ -1,27 +0,0 @@ -"""pytest configuration file.""" - -# SPDX-FileCopyrightText: 2016-2024 University of Lausanne -# SPDX-FileCopyrightText: 2020-2021 Helmholtz-Zentrum Geesthacht - -# SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH -# -# SPDX-License-Identifier: LGPL-3.0-only - - -def pytest_addoption(parser): - group = parser.getgroup("psyplot", "psyplot specific options") - group.addoption( - "--no-removal", - help="Do not remove created test files", - action="store_true", - ) - - -def pytest_configure(config): - if config.getoption("no_removal"): - import test_project - - test_project.remove_temp_files = False - import test_main - - test_main.remove_temp_files = False diff --git a/tests/icon_test.nc b/tests/icon_test.nc deleted file mode 100644 index 59f3911..0000000 Binary files a/tests/icon_test.nc and /dev/null differ diff --git a/tests/icon_test.nc.license b/tests/icon_test.nc.license deleted file mode 100644 index 919c9c1..0000000 --- a/tests/icon_test.nc.license +++ /dev/null @@ -1,3 +0,0 @@ -SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH - -SPDX-License-Identifier: CC0-1.0 diff --git a/tests/logging.yml b/tests/logging.yml deleted file mode 100755 index c24e9cd..0000000 --- a/tests/logging.yml +++ /dev/null @@ -1,86 +0,0 @@ -# SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH -# -# SPDX-License-Identifier: CC0-1.0 - ---- -# logging settings for the nc2map module - -version: 1 - -disable_existing_loggers: False - -formatters: - - simple: - - format: "[%(name)s] - %(levelname)s - %(message)s" - - level_message: - - format: "%(levelname)s: %(message)s" - - full: - format: "%(asctime)s - [%(name)s.%(funcName)s] - %(levelname)s - %(message)s" - - -handlers: - - console: - - class: logging.StreamHandler - - level: DEBUG - - formatter: simple - - stream: ext://sys.stdout - - warning_console: - - class: logging.StreamHandler - - level: INFO - - formatter: level_message - - stream: ext://sys.stdout - - - debug_file_handler: - - class: logging.handlers.RotatingFileHandler - - mode: w - - level: DEBUG - - formatter: full - - filename: ~/.debug_psyplot.log - - maxBytes: 10485760 # 10MB - - backupCount: 5 - - encoding: utf8 - - delay: True - -loggers: - - psyplot: - - handlers: [console] - - propagate: False - - level: DEBUG - - psyplot.warning: - - handlers: [warning_console, debug_file_handler] - - propagate: False - - level: WARNING -... diff --git a/tests/main.py b/tests/main.py deleted file mode 100755 index 0d294fc..0000000 --- a/tests/main.py +++ /dev/null @@ -1,29 +0,0 @@ -#!/usr/bin/env python -"""Script to run all tests from the psyplot package - -This script may be used from the command line run all tests from the psyplot -package. See:: - - python main.py -h - -for details. You can also use the py.test module (which however leads to a -failure of some tests in python 3.4) -""" - -# SPDX-FileCopyrightText: 2016-2024 University of Lausanne -# SPDX-FileCopyrightText: 2020-2021 Helmholtz-Zentrum Geesthacht - -# SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH -# -# SPDX-License-Identifier: LGPL-3.0-only - -import unittest - -import _base_testing as bt - -test_suite = unittest.defaultTestLoader.discover(bt.test_dir) - - -if __name__ == "__main__": - test_runner = unittest.TextTestRunner(verbosity=2, failfast=True) - test_runner.run(test_suite) diff --git a/tests/rotated-pole-test.nc b/tests/rotated-pole-test.nc deleted file mode 100644 index 0b0ff18..0000000 Binary files a/tests/rotated-pole-test.nc and /dev/null differ diff --git a/tests/rotated-pole-test.nc.license b/tests/rotated-pole-test.nc.license deleted file mode 100644 index 919c9c1..0000000 --- a/tests/rotated-pole-test.nc.license +++ /dev/null @@ -1,3 +0,0 @@ -SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH - -SPDX-License-Identifier: CC0-1.0 diff --git a/tests/simple_triangular_grid_si0.nc b/tests/simple_triangular_grid_si0.nc deleted file mode 100644 index 6614f1a..0000000 Binary files a/tests/simple_triangular_grid_si0.nc and /dev/null differ diff --git a/tests/simple_triangular_grid_si0.nc.license b/tests/simple_triangular_grid_si0.nc.license deleted file mode 100644 index 919c9c1..0000000 --- a/tests/simple_triangular_grid_si0.nc.license +++ /dev/null @@ -1,3 +0,0 @@ -SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH - -SPDX-License-Identifier: CC0-1.0 diff --git a/tests/test-t2m-1979-01-31T18-00-00.tif b/tests/test-t2m-1979-01-31T18-00-00.tif deleted file mode 100644 index 2d970ee..0000000 Binary files a/tests/test-t2m-1979-01-31T18-00-00.tif and /dev/null differ diff --git a/tests/test-t2m-1979-01-31T18-00-00.tif.license b/tests/test-t2m-1979-01-31T18-00-00.tif.license deleted file mode 100644 index 919c9c1..0000000 --- a/tests/test-t2m-1979-01-31T18-00-00.tif.license +++ /dev/null @@ -1,3 +0,0 @@ -SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH - -SPDX-License-Identifier: CC0-1.0 diff --git a/tests/test-t2m-1979-02-28T18-00-00.tif b/tests/test-t2m-1979-02-28T18-00-00.tif deleted file mode 100644 index 0c85507..0000000 Binary files a/tests/test-t2m-1979-02-28T18-00-00.tif and /dev/null differ diff --git a/tests/test-t2m-1979-02-28T18-00-00.tif.license b/tests/test-t2m-1979-02-28T18-00-00.tif.license deleted file mode 100644 index 919c9c1..0000000 --- a/tests/test-t2m-1979-02-28T18-00-00.tif.license +++ /dev/null @@ -1,3 +0,0 @@ -SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH - -SPDX-License-Identifier: CC0-1.0 diff --git a/tests/test-t2m-u-v.nc b/tests/test-t2m-u-v.nc deleted file mode 100644 index 7594faf..0000000 Binary files a/tests/test-t2m-u-v.nc and /dev/null differ diff --git a/tests/test-t2m-u-v.nc.license b/tests/test-t2m-u-v.nc.license deleted file mode 100644 index 919c9c1..0000000 --- a/tests/test-t2m-u-v.nc.license +++ /dev/null @@ -1,3 +0,0 @@ -SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH - -SPDX-License-Identifier: CC0-1.0 diff --git a/tests/test_data.py b/tests/test_data.py deleted file mode 100755 index 1a0b429..0000000 --- a/tests/test_data.py +++ /dev/null @@ -1,1953 +0,0 @@ -"""Test module of the :mod:`psyplot.data` module""" - -# SPDX-FileCopyrightText: 2016-2024 University of Lausanne -# SPDX-FileCopyrightText: 2020-2021 Helmholtz-Zentrum Geesthacht - -# SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH -# -# SPDX-License-Identifier: LGPL-3.0-only - -import os -import os.path as osp -import tempfile -import unittest - -import _base_testing as bt -import numpy as np -import pandas as pd -import six -import xarray as xr - -import psyplot.data as psyd - -try: - import PyNio - - with_nio = True -except ImportError as e: - PyNio = psyd._MissingModule(e) - with_nio = False - -try: - import netCDF4 as nc - - with_netcdf4 = True -except ImportError as e: - nc = psyd._MissingModule(e) - with_netcdf4 = False - -try: - import scipy - - with_scipy = True -except ImportError as e: - scipy = psyd._MissingModule(e) - with_scipy = False - -try: - from cdo import Cdo - - Cdo() -except Exception: - with_cdo = False -else: - with_cdo = True - - -xr_version = tuple(map(float, xr.__version__.split(".")[:3])) - - -class AlmostArrayEqualMixin(object): - def assertAlmostArrayEqual( - self, actual, desired, rtol=1e-07, atol=0, msg=None, **kwargs - ): - """Asserts that the two given arrays are almost the same - - This method uses the :func:`numpy.testing.assert_allclose` function - to compare the two given arrays. - - Parameters - ---------- - actual : array_like - Array obtained. - desired : array_like - Array desired. - rtol : float, optional - Relative tolerance. - atol : float, optional - Absolute tolerance. - equal_nan : bool, optional. - If True, NaNs will compare equal. - err_msg : str, optional - The error message to be printed in case of failure. - verbose : bool, optional - If True, the conflicting values are appended to the error message. - """ - try: - np.testing.assert_allclose( - actual, - desired, - rtol=rtol, - atol=atol, - err_msg=msg or "", - **kwargs, - ) - except AssertionError as e: - self.fail(e if six.PY3 else e.message) - - -class DecoderTest(unittest.TestCase, AlmostArrayEqualMixin): - """Test the :class:`psyplot.data.CFDecoder` class""" - - def test_decode_grid_mapping(self): - ds = xr.Dataset() - ds["var"] = (("x", "y"), np.zeros((5, 4)), {"grid_mapping": "crs"}) - ds["crs"] = ((), 1) - - self.assertNotIn("crs", ds.coords) - ds = psyd.CFDecoder.decode_coords(ds) - self.assertIn("crs", ds.coords) - - def test_1D_cf_bounds(self): - """Test whether the CF Conventions for 1D bounaries are correct""" - final_bounds = np.arange(-180, 181, 30) - lon = xr.Variable( - ("lon",), np.arange(-165, 166, 30), {"bounds": "lon_bounds"} - ) - cf_bounds = xr.Variable(("lon", "bnds"), np.zeros((len(lon), 2))) - for i in range(len(lon)): - cf_bounds[i, :] = final_bounds[i : i + 2] - ds = xr.Dataset(coords={"lon": lon, "lon_bounds": cf_bounds}) - decoder = psyd.CFDecoder(ds) - self.assertEqual(list(final_bounds), list(decoder.get_plotbounds(lon))) - - def test_1D_bounds_calculation(self): - """Test whether the 1D cell boundaries are calculated correctly""" - final_bounds = np.arange(-180, 181, 30) - lon = xr.Variable(("lon",), np.arange(-165, 166, 30)) - ds = xr.Dataset(coords={"lon": lon}) - decoder = psyd.CFDecoder(ds) - self.assertEqual(list(final_bounds), list(decoder.get_plotbounds(lon))) - - def _test_dimname( - self, func_name, name, uname=None, name2d=False, circ_name=None - ): - def check_ds(name): - self.assertEqual(getattr(d, func_name)(ds.t2m), name) - self.assertEqual( - getattr(d, func_name)(ds.t2m, coords=ds.t2m.coords), name - ) - if name2d: - self.assertEqual(getattr(d, func_name)(ds.t2m_2d), name) - else: - self.assertIsNone(getattr(d, func_name)(ds.t2m_2d)) - if six.PY3: - # Test whether the warning is raised if the decoder finds - # multiple dimensions - with self.assertWarnsRegex(RuntimeWarning, "multiple matches"): - coords = "time lat lon lev x y latitude longitude".split() - ds.t2m.attrs.pop("coordinates", None) - for dim in "xytz": - getattr(d, dim).update(coords) - for coord in set(coords).intersection(ds.coords): - ds.coords[coord].attrs.pop("axis", None) - getattr(d, func_name)(ds.t2m) - - uname = uname or name - circ_name = circ_name or name - ds = psyd.open_dataset(os.path.join(bt.test_dir, "test-t2m-u-v.nc")) - d = psyd.CFDecoder(ds) - check_ds(name) - ds.close() - ds = psyd.open_dataset(os.path.join(bt.test_dir, "icon_test.nc")) - d = psyd.CFDecoder(ds) - check_ds(uname) - ds.close() - ds = psyd.open_dataset( - os.path.join(bt.test_dir, "circumpolar_test.nc") - ) - d = psyd.CFDecoder(ds) - check_ds(circ_name) - ds.close() - - def test_xname_no_dims(self): - """Test the get_xname method for a variable without dimensions""" - da = xr.DataArray(1) - self.assertIsNone(da.psy.get_dim("x")) - - def test_yname_no_dims(self): - """Test the get_yname method for a variable without dimensions""" - da = xr.DataArray(1) - self.assertIsNone(da.psy.get_dim("y")) - - def test_zname_no_dims(self): - """Test the get_zname method for a variable without dimensions""" - da = xr.DataArray(1) - self.assertIsNone(da.psy.get_dim("z")) - - def test_tname_no_dims(self): - """Test the get_tname method for a variable without dimensions""" - da = xr.DataArray(1) - self.assertIsNone(da.psy.get_dim("t")) - - def test_xcoord_no_dims(self): - """Test the get_x method for a variable without dimensions""" - da = xr.DataArray(1) - self.assertIsNone(da.psy.get_coord("x")) - - def test_ycoord_no_dims(self): - """Test the get_y method for a variable without dimensions""" - da = xr.DataArray(1) - self.assertIsNone(da.psy.get_coord("y")) - - def test_zcoord_no_dims(self): - """Test the get_z method for a variable without dimensions""" - da = xr.DataArray(1) - self.assertIsNone(da.psy.get_coord("z")) - - def test_tcoord_no_dims(self): - """Test the get_t method for a variable without dimensions""" - da = xr.DataArray(1) - self.assertIsNone(da.psy.get_coord("t")) - - def _test_coord( - self, func_name, name, uname=None, name2d=False, circ_name=None - ): - def check_ds(name): - self.assertEqual(getattr(d, func_name)(ds.t2m).name, name) - if name2d: - self.assertEqual(getattr(d, func_name)(ds.t2m_2d).name, name) - else: - self.assertIsNone(getattr(d, func_name)(ds.t2m_2d)) - if six.PY3: - # Test whether the warning is raised if the decoder finds - # multiple dimensions - with self.assertWarnsRegex(RuntimeWarning, "multiple matches"): - coords = "time lat lon lev x y latitude longitude".split() - ds.t2m.attrs.pop("coordinates", None) - for dim in "xytz": - getattr(d, dim).update(coords) - for coord in set(coords).intersection(ds.coords): - ds.coords[coord].attrs.pop("axis", None) - getattr(d, func_name)(ds.t2m) - - uname = uname or name - circ_name = circ_name or name - ds = psyd.open_dataset(os.path.join(bt.test_dir, "test-t2m-u-v.nc")) - d = psyd.CFDecoder(ds) - check_ds(name) - ds.close() - ds = psyd.open_dataset(os.path.join(bt.test_dir, "icon_test.nc")) - d = psyd.CFDecoder(ds) - check_ds(uname) - ds.close() - ds = psyd.open_dataset( - os.path.join(bt.test_dir, "circumpolar_test.nc") - ) - d = psyd.CFDecoder(ds) - check_ds(circ_name) - ds.close() - - def test_tname(self): - """Test CFDecoder.get_tname method""" - self._test_dimname("get_tname", "time") - - def test_zname(self): - """Test CFDecoder.get_zname method""" - self._test_dimname("get_zname", "lev") - - def test_xname(self): - """Test CFDecoder.get_xname method""" - self._test_dimname("get_xname", "lon", "ncells", True, circ_name="x") - - def test_yname(self): - """Test CFDecoder.get_yname method""" - self._test_dimname("get_yname", "lat", "ncells", True, circ_name="y") - - def test_t(self): - """Test CFDecoder.get_t method""" - self._test_coord("get_t", "time") - - def test_z(self): - """Test CFDecoder.get_z method""" - self._test_coord("get_z", "lev") - - def test_x(self): - """Test CFDecorder.get_x method""" - self._test_coord("get_x", "lon", "clon", True, circ_name="longitude") - - def test_y(self): - """Test CFDecoder.get_y method""" - self._test_coord("get_y", "lat", "clat", True, circ_name="latitude") - - def test_standardization(self): - """Test the :meth:`psyplot.data.CFDecoder.standardize_dims` method""" - ds = psyd.open_dataset(os.path.join(bt.test_dir, "test-t2m-u-v.nc")) - decoder = psyd.CFDecoder(ds) - dims = {"time": 1, "lat": 2, "lon": 3, "lev": 4} - replaced = decoder.standardize_dims(ds.t2m, dims) - for dim, rep in [ - ("time", "t"), - ("lat", "y"), - ("lon", "x"), - ("lev", "z"), - ]: - self.assertIn(rep, replaced) - self.assertEqual( - replaced[rep], - dims[dim], - msg="Wrong value for %s (%s-) dimension" % (dim, rep), - ) - - def test_idims(self): - """Test the extraction of the slicers of the dimensions""" - ds = psyd.open_dataset(bt.get_file("test-t2m-u-v.nc")) - arr = ds.t2m[1:, 1] - arr.psy.init_accessor(base=ds) - dims = arr.psy.idims - for dim in ["time", "lev", "lat", "lon"]: - self.assertEqual( - psyd.safe_list(ds[dim][dims[dim]]), - psyd.safe_list(arr.coords[dim]), - msg="Slice %s for dimension %s is wrong!" % (dims[dim], dim), - ) - # test with unknown dimensions - if xr_version[:2] >= (0, 9): - try: - ds = ds.drop_vars("time") - except AttributeError: # xarray <=0.13 - ds = ds.drop("time") - arr = ds.t2m[1:, 1] - arr.psy.init_accessor(base=ds) - if not six.PY2: - with self.assertWarnsRegex(RuntimeWarning, "time"): - dims = arr.psy.idims - arrays = psyd.ArrayList.from_dataset( - ds, name="t2m", time=slice(1, None), lev=85000.0, method="sel" - ) - arr = arrays[0] - dims = arr.psy.idims - for dim in ["time", "lev", "lat", "lon"]: - if dim == "time": - self.assertEqual(dims[dim], slice(1, 5, 1)) - else: - self.assertEqual( - psyd.safe_list(ds[dim][dims[dim]]), - psyd.safe_list(arr.coords[dim]), - msg="Slice %s for dimension %s is wrong!" - % (dims[dim], dim), - ) - - def test_unstructured_bounds(self): - """Test the extraction of unstructured bounds""" - ds = psyd.open_dataset(os.path.join(bt.test_dir, "icon_test.nc")) - decoder = psyd.CFDecoder(ds) - var = ds.t2m[0, 0] - var.attrs.pop("grid_type", None) - self.assertTrue(decoder.is_unstructured(var)) - # x bounds - xbounds = decoder.get_cell_node_coord(var, axis="x") - self.assertIsNotNone(xbounds) - self.assertEqual(xbounds.shape, ds.clon_bnds.shape) - # y bounds - ybounds = decoder.get_cell_node_coord(var, axis="y") - self.assertIsNotNone(ybounds) - self.assertEqual(ybounds.shape, ds.clon_bnds.shape) - - # Test for correct falsification - ds = psyd.open_dataset(os.path.join(bt.test_dir, "test-t2m-u-v.nc")) - decoder = psyd.CFDecoder(ds) - var = ds.t2m[0, 0] - self.assertFalse(decoder.is_unstructured(var)) - xbounds = decoder.get_cell_node_coord(var, axis="x") - self.assertEqual(xbounds.shape, (np.prod(var.shape), 4)) - - def test_is_unstructured_2D_bounds(self): - """Test that 3D bounds are not interpreted as unstructured""" - with psyd.open_dataset( - os.path.join(bt.test_dir, "rotated-pole-test.nc") - ) as ds: - decoder = psyd.CFDecoder(ds) - self.assertFalse(decoder.is_unstructured(ds.psy["HSURF"])) - - def test_is_circumpolar(self): - """Test whether the is_circumpolar method works""" - ds = psyd.open_dataset( - os.path.join(bt.test_dir, "circumpolar_test.nc") - ) - decoder = psyd.CFDecoder(ds) - self.assertTrue(decoder.is_circumpolar(ds.t2m)) - - # test for correct falsification - ds = psyd.open_dataset(os.path.join(bt.test_dir, "icon_test.nc")) - decoder = psyd.CFDecoder(ds) - self.assertFalse(decoder.is_circumpolar(ds.t2m)) - - def test_get_variable_by_axis(self): - """Test the :meth:`CFDecoder.get_variable_by_axis` method""" - ds = psyd.open_dataset( - os.path.join(bt.test_dir, "circumpolar_test.nc") - ) - decoder = psyd.CFDecoder(ds) - arr = ds.t2m - arr.attrs.pop("coordinates", None) - arr.encoding.pop("coordinates", None) - for c in ds.coords.values(): - c.attrs.pop("axis", None) - for dim in ["x", "y", "z", "t"]: - self.assertIsNone( - decoder.get_variable_by_axis(arr, dim), - msg="Accidently found coordinate %s" % dim, - ) - - # test coordinates attribute - arr.attrs["coordinates"] = "latitude longitude" - self.assertEqual( - decoder.get_variable_by_axis(arr, "x").name, "longitude" - ) - self.assertEqual( - decoder.get_variable_by_axis(arr, "y").name, "latitude" - ) - self.assertIsNone(decoder.get_variable_by_axis(arr, "z")) - - # test coordinates attribute but without specifying axis or matching - # latitude or longitude - axes = {"lev": "z", "time": "t", "x": "x", "y": "y"} - arr.attrs["coordinates"] = "time lev y x" - for name, axis in axes.items(): - self.assertEqual( - decoder.get_variable_by_axis(arr, axis).name, name - ) - - # test with specified axis attribute - arr.attrs["coordinates"] = "time lev longitude latitude" - axes = {"lev": "Z", "time": "T", "latitude": "X", "longitude": "Y"} - for name, axis in axes.items(): - ds.coords[name].attrs["axis"] = axis - for name, axis in axes.items(): - self.assertEqual( - decoder.get_variable_by_axis(arr, axis.lower()).name, name - ) - - # close the dataset - ds.close() - - def test_get_variable_by_axis_02(self): - """Test the :meth:`CFDecoder.get_variable_by_axis` method with missing - coordinates, see https://github.com/psyplot/psyplot/pull/19""" - fname = os.path.join(bt.test_dir, "icon_test.nc") - with psyd.open_dataset(fname) as ds: - ds["ncells"] = ("ncells", np.arange(ds.dims["ncells"])) - decoder = psyd.CFDecoder(ds) - arr = ds.psy["t2m"].psy.isel(ncells=slice(3, 10)) - del arr["clon"] - xcoord = decoder.get_variable_by_axis(arr, "x", arr.coords) - self.assertEqual(xcoord.name, "clon") - self.assertEqual(list(xcoord.ncells), list(arr.ncells)) - - def test_plot_bounds_1d(self): - """Test to get 2d-interval breaks""" - x = xr.Variable(("x",), np.arange(1, 5)) - d = psyd.CFDecoder() - bounds = d.get_plotbounds(x) - self.assertAlmostArrayEqual(bounds, np.arange(0.5, 4.51, 1.0)) - - def test_plot_bounds_2d(self): - x = np.arange(1, 5) - y = np.arange(5, 10) - x2d, y2d = np.meshgrid(x, y) - x_bnds = np.arange(0.5, 4.51, 1.0) - y_bnds = np.arange(4.5, 9.51, 1.0) - # the borders are not modified - x_bnds[0] = 1.0 - x_bnds[-1] = 4.0 - y_bnds[0] = 5.0 - y_bnds[-1] = 9.0 - x2d_bnds, y2d_bnds = np.meshgrid(x_bnds, y_bnds) - d = psyd.CFDecoder() - # test x bounds - bounds = d.get_plotbounds(xr.Variable(("y", "x"), x2d)) - self.assertAlmostArrayEqual(bounds, x2d_bnds) - - # test y bounds - bounds = d.get_plotbounds(xr.Variable(("y", "x"), y2d)) - self.assertAlmostArrayEqual(bounds, y2d_bnds) - - -class UGridDecoderTest(unittest.TestCase, AlmostArrayEqualMixin): - """Test the :class:`psyplot.data.UGridDecoder` class""" - - def test_get_decoder(self): - """Test to get the right decoder""" - ds = psyd.open_dataset(bt.get_file("simple_triangular_grid_si0.nc")) - d = psyd.CFDecoder.get_decoder(ds, ds.Mesh2_fcvar) - self.assertIsInstance(d, psyd.UGridDecoder) - return ds, d - - def test_x(self): - """Test the get_x method""" - ds, d = self.test_get_decoder() - x = d.get_x(ds.Mesh2_fcvar) - self.assertIn("standard_name", x.attrs) - self.assertEqual(x.attrs["standard_name"], "longitude") - self.assertAlmostArrayEqual(x.values, [0.3, 0.56666667]) - - def test_y(self): - """Test the get_y method""" - ds, d = self.test_get_decoder() - y = d.get_y(ds.Mesh2_fcvar) - self.assertIn("standard_name", y.attrs) - self.assertEqual(y.attrs["standard_name"], "latitude") - self.assertAlmostArrayEqual(y.values, [0.4, 0.76666668]) - - -class TestInteractiveArray(unittest.TestCase, AlmostArrayEqualMixin): - """Test the :class:`psyplot.data.InteractiveArray` class""" - - def tearDown(self): - psyd.rcParams.update_from_defaultParams(plotters=False) - - def test_auto_update(self): - """Test the :attr:`psyplot.plotter.Plotter.no_auto_update` attribute""" - ds = psyd.open_dataset(bt.get_file("test-t2m-u-v.nc")) - arr = ds.psy.t2m.psy[0, 0, 0] - arr.psy.init_accessor(auto_update=False) - - arr.psy.update(time=1) - self.assertEqual(arr.time, ds.time[0]) - arr.psy.start_update() - self.assertEqual(arr.time, ds.time[1]) - - arr.psy.no_auto_update = False - arr.psy.update(time=2) - self.assertEqual(arr.time, ds.time[2]) - - def test_update_01_isel(self): - """test the update of a single array through the isel method""" - ds = psyd.open_dataset(bt.get_file("test-t2m-u-v.nc")) - arr = ds.psy.t2m.psy[0, 0, 0] - arr.attrs["test"] = 4 - self.assertNotIn("test", ds.t2m.attrs) - self.assertIs(arr.psy.base, ds) - self.assertEqual( - dict(arr.psy.idims), - {"time": 0, "lev": 0, "lat": 0, "lon": slice(None)}, - ) - # update to next time step - arr.psy.update(time=1) - self.assertEqual(arr.time, ds.time[1]) - self.assertEqual( - arr.values.tolist(), ds.t2m[1, 0, 0, :].values.tolist() - ) - self.assertEqual( - dict(arr.psy.idims), - {"time": 1, "lev": 0, "lat": 0, "lon": slice(None)}, - ) - self.assertNotIn("test", ds.t2m.attrs) - self.assertIn("test", arr.attrs) - self.assertEqual(arr.test, 4) - - @unittest.skipIf(xr_version[:2] < (0, 10), "Not implemented for xr<0.10") - def test_shiftlon(self): - ds = psyd.open_dataset(bt.get_file("test-t2m-u-v.nc")) - da = ds.t2m - nlon = da.lon.size - - # shift to the mean (this should not change anything) - shifted = da.psy.shiftlon(ds.lon.values.mean()) - self.assertAlmostArrayEqual(shifted.lon, da.lon) - self.assertAlmostArrayEqual(shifted, da) - - # shift to left - shifted = da.psy.shiftlon(da.lon.min()) - self.assertEqual(shifted.lon[nlon // 2 - 1], da.lon[0]) - self.assertEqual(shifted.lon[-1], da.lon[nlon // 2]) - self.assertAlmostArrayEqual(shifted[..., nlon // 2 - 1], da[..., 0]) - self.assertAlmostArrayEqual(shifted[..., -1], da[..., nlon // 2]) - - # shift 25% to left - shifted = da.psy.shiftlon(da.lon[nlon // 4]) - self.assertEqual(shifted.lon[0], da.lon[-nlon // 4 + 1] - 360) - self.assertAlmostArrayEqual( - shifted[..., nlon // 2 - 1], da[..., nlon // 4] - ) - self.assertAlmostArrayEqual(shifted[..., 0], da[..., -nlon // 4 + 1]) - - def test_update_02_sel(self): - """test the update of a single array through the sel method""" - ds = psyd.open_dataset(bt.get_file("test-t2m-u-v.nc")) - arr = ds.psy.t2m.psy[0, 0, 0] - arr.attrs["test"] = 4 - self.assertNotIn("test", ds.t2m.attrs) - self.assertIs(arr.psy.base, ds) - self.assertEqual( - dict(arr.psy.idims), - {"time": 0, "lev": 0, "lat": 0, "lon": slice(None)}, - ) - # update to next time step - arr.psy.update(time="1979-02-28T18:00", method="nearest") - self.assertEqual(arr.time, ds.time[1]) - self.assertEqual( - arr.values.tolist(), ds.t2m[1, 0, 0, :].values.tolist() - ) - self.assertEqual( - dict(arr.psy.idims), - {"time": 1, "lev": 0, "lat": 0, "lon": slice(None)}, - ) - self.assertNotIn("test", ds.t2m.attrs) - self.assertIn("test", arr.attrs) - self.assertEqual(arr.test, 4) - - def test_update_03_isel_concat(self): - """test the update of a concatenated array through the isel method""" - ds = psyd.open_dataset(bt.get_file("test-t2m-u-v.nc"))[["t2m", "u"]] - arr = ds.psy.to_array().psy.isel(time=0, lev=0, lat=0) - arr.attrs["test"] = 4 - self.assertNotIn("test", ds.t2m.attrs) - arr.name = "something" - self.assertIs(arr.psy.base, ds) - self.assertEqual( - dict(arr.psy.idims), - {"time": 0, "lev": 0, "lat": 0, "lon": slice(None)}, - ) - self.assertEqual(arr.coords["variable"].values.tolist(), ["t2m", "u"]) - # update to next time step - arr.psy.update(time=1) - self.assertEqual(arr.time, ds.time[1]) - self.assertEqual(arr.coords["variable"].values.tolist(), ["t2m", "u"]) - self.assertEqual( - arr.values.tolist(), - ds[["t2m", "u"]].to_array()[:, 1, 0, 0, :].values.tolist(), - ) - self.assertEqual( - dict(arr.psy.idims), - {"time": 1, "lev": 0, "lat": 0, "lon": slice(None)}, - ) - self.assertNotIn("test", ds.t2m.attrs) - self.assertIn("test", arr.attrs) - self.assertEqual(arr.test, 4) - self.assertEqual(arr.name, "something") - - def test_update_04_sel_concat(self): - """test the update of a concatenated array through the isel method""" - ds = psyd.open_dataset(bt.get_file("test-t2m-u-v.nc"))[["t2m", "u"]] - arr = ds.psy.to_array().psy.isel(time=0, lev=0, lat=0) - arr.attrs["test"] = 4 - self.assertNotIn("test", ds.t2m.attrs) - self.assertIs(arr.psy.base, ds) - self.assertEqual( - dict(arr.psy.idims), - {"time": 0, "lev": 0, "lat": 0, "lon": slice(None)}, - ) - self.assertEqual(arr.coords["variable"].values.tolist(), ["t2m", "u"]) - # update to next time step - arr.psy.update(time="1979-02-28T18:00", method="nearest") - self.assertEqual(arr.time, ds.time[1]) - self.assertEqual(arr.coords["variable"].values.tolist(), ["t2m", "u"]) - self.assertEqual( - arr.values.tolist(), - ds[["t2m", "u"]].to_array()[:, 1, 0, 0, :].values.tolist(), - ) - self.assertEqual( - dict(arr.psy.idims), - {"time": 1, "lev": 0, "lat": 0, "lon": slice(None)}, - ) - self.assertNotIn("test", ds.t2m.attrs) - self.assertIn("test", arr.attrs) - self.assertEqual(arr.test, 4) - - def test_update_05_1variable(self): - """Test to change the variable""" - ds = psyd.open_dataset(bt.get_file("test-t2m-u-v.nc")) - arr = ds.psy.t2m.psy[0, 0, 0] - arr.attrs["test"] = 4 - self.assertNotIn("test", ds.t2m.attrs) - self.assertIs(arr.psy.base, ds) - self.assertEqual( - dict(arr.psy.idims), - {"time": 0, "lev": 0, "lat": 0, "lon": slice(None)}, - ) - # update to next time step - arr.psy.update(name="u", time=1) - self.assertEqual(arr.time, ds.time[1]) - self.assertEqual(arr.name, "u") - self.assertEqual(arr.values.tolist(), ds.u[1, 0, 0, :].values.tolist()) - self.assertEqual( - dict(arr.psy.idims), - {"time": 1, "lev": 0, "lat": 0, "lon": slice(None)}, - ) - self.assertNotIn("test", ds.t2m.attrs) - self.assertIn("test", arr.attrs) - self.assertEqual(arr.test, 4) - - def test_update_06_2variables(self): - """test the change of the variable of a concatenated array""" - ds = psyd.open_dataset(bt.get_file("test-t2m-u-v.nc")) - arr = ds[["t2m", "u"]].to_array().isel(time=0, lev=0, lat=0) - arr.attrs["test"] = 4 - self.assertNotIn("test", ds.t2m.attrs) - arr.name = "something" - arr.psy.base = ds - self.assertEqual( - dict(arr.psy.idims), - {"time": 0, "lev": 0, "lat": 0, "lon": slice(None)}, - ) - self.assertEqual(arr.coords["variable"].values.tolist(), ["t2m", "u"]) - # update to next time step - arr.psy.update(time=1, name=["u", "v"]) - self.assertEqual(arr.time, ds.time[1]) - self.assertEqual(arr.coords["variable"].values.tolist(), ["u", "v"]) - self.assertEqual( - arr.values.tolist(), - ds[["u", "v"]].to_array()[:, 1, 0, 0, :].values.tolist(), - ) - self.assertEqual( - dict(arr.psy.idims), - {"time": 1, "lev": 0, "lat": 0, "lon": slice(None)}, - ) - self.assertNotIn("test", ds.t2m.attrs) - self.assertIn("test", arr.attrs) - self.assertEqual(arr.test, 4) - self.assertEqual(arr.name, "something") - - def test_update_07_variable_with_new_dims(self): - ds = xr.Dataset() - ds["test1"] = (tuple("ab"), np.zeros((5, 4))) - ds["test2"] = (tuple("abc"), np.zeros((5, 4, 3))) - ds["a"] = ("a", np.arange(5)) - ds["b"] = ("b", np.arange(4)) - ds["c"] = ("c", np.arange(3)) - - da = ds.psy["test1"].psy.isel(a=slice(1, 3)) - self.assertEqual(da.name, "test1") - self.assertEqual(da.shape, (2, 4)) - self.assertEqual(da.psy.idims, {"a": slice(1, 3, 1), "b": slice(None)}) - - # update to test2 - da.psy.update(name="test2") - self.assertEqual(da.name, "test2") - self.assertEqual(da.shape, (2, 4, 3)) - self.assertEqual( - da.psy.idims, - {"a": slice(1, 3, 1), "b": slice(None), "c": slice(None)}, - ) - - # update back to test1 - da.psy.update(name="test1") - self.assertEqual(da.name, "test1") - self.assertEqual(da.shape, (2, 4)) - self.assertEqual(da.psy.idims, {"a": slice(1, 3, 1), "b": slice(None)}) - - # update to test2 but this time with specifying a dimension for c - # does not yet work with c=1 - da.psy.update(name="test2", dims=dict(c=1)) - self.assertEqual(da.name, "test2") - self.assertEqual(da.shape, (2, 4)) - self.assertEqual( - da.psy.idims, {"a": slice(1, 3, 1), "b": slice(None), "c": 1} - ) - self.assertEqual(da["c"], 1) - - def test_update_08_2variables_with_new_dims(self): - ds = xr.Dataset() - ds["test1"] = (tuple("ab"), np.zeros((5, 4))) - ds["test11"] = (tuple("ab"), np.zeros((5, 4))) - ds["test2"] = (tuple("abc"), np.zeros((5, 4, 3))) - ds["test22"] = (tuple("abc"), np.zeros((5, 4, 3))) - ds["a"] = ("a", np.arange(5)) - ds["b"] = ("b", np.arange(4)) - ds["c"] = ("c", np.arange(3)) - - da = ds.psy.create_list( - name=[["test1", "test11"]], prefer_list=False, a=slice(1, 3, 1) - )[0] - self.assertEqual(da.shape, (2, 2, 4)) - self.assertEqual(list(da["variable"]), ["test1", "test11"]) - self.assertEqual(da.psy.idims, {"a": slice(1, 3, 1), "b": slice(None)}) - - # update to test2 - da.psy.update(name=["test2", "test22"]) - self.assertEqual(da.shape, (2, 2, 4, 3)) - self.assertEqual(list(da["variable"]), ["test2", "test22"]) - self.assertEqual( - da.psy.idims, - {"a": slice(1, 3, 1), "b": slice(None), "c": slice(None)}, - ) - - # update back to test1 - da.psy.update(name=["test1", "test11"]) - self.assertEqual(da.shape, (2, 2, 4)) - self.assertEqual(list(da["variable"]), ["test1", "test11"]) - self.assertEqual(da.psy.idims, {"a": slice(1, 3, 1), "b": slice(None)}) - - # update to test2 but this time with specifying a dimension for c - # does not yet work with c=1 - da.psy.update(name=["test2", "test22"], dims=dict(c=1)) - self.assertEqual(list(da["variable"]), ["test2", "test22"]) - self.assertEqual(da.shape, (2, 2, 4)) - self.assertEqual( - da.psy.idims, {"a": slice(1, 3, 1), "b": slice(None), "c": 1} - ) - self.assertEqual(da["c"], 1) - - @unittest.skipIf(not with_cdo, "CDOs are not installed") - def test_gridweights_01_lola(self): - fname = bt.get_file("test-t2m-u-v.nc") - ds = psyd.open_dataset(fname) - weights = ds.psy.t2m.psy.gridweights() - ds.close() - ref = Cdo().gridweights(input=fname, returnArray="cell_weights") - self.assertAlmostArrayEqual(weights, ref, atol=1e-7) - - @unittest.skipIf(not with_cdo, "CDOs are not installed") - def test_gridweights_02_icon(self): - fname = bt.get_file("icon_test.nc") - ds = psyd.open_dataset(fname) - weights = ds.psy.t2m.psy.gridweights() - ds.close() - ref = Cdo().gridweights(input=fname, returnArray="cell_weights") - self.assertAlmostArrayEqual(weights, ref) - - @unittest.skipIf(not with_cdo, "CDOs are not installed") - @unittest.skipIf(xr_version[:2] < (0, 9), "xarray version too low") - def test_fldmean_01_lola(self): - from psyplot.project import Cdo - - fname = bt.get_file("test-t2m-u-v.nc") - ds = psyd.open_dataset(fname) - psyd.rcParams["gridweights.use_cdo"] = True - means = ds.psy.t2m.psy.fldmean().values - ref = Cdo().fldmean(input=fname, name="t2m")[0] - self.assertAlmostArrayEqual(means, ref) - # try it with the self defined gridweights - psyd.rcParams["gridweights.use_cdo"] = False - means = ds.psy.t2m.psy.fldmean().values - self.assertAlmostArrayEqual(means, ref, rtol=1e-5) - ds.close() - - @unittest.skipIf(not with_cdo, "CDOs are not installed") - @unittest.skipIf(xr_version[:2] < (0, 9), "xarray version too low") - def test_fldmean_02_icon(self): - from psyplot.project import Cdo - - fname = bt.get_file("icon_test.nc") - ds = psyd.open_dataset(fname) - psyd.rcParams["gridweights.use_cdo"] = True - means = ds.psy.t2m.psy.fldmean().values - ref = Cdo().fldmean(input=fname, name="t2m")[0] - self.assertAlmostArrayEqual(means, ref) - ds.close() - - @unittest.skipIf(not with_cdo, "CDOs are not installed") - @unittest.skipIf(xr_version[:2] < (0, 9), "xarray version too low") - def test_fldstd_01_lola(self): - from psyplot.project import Cdo - - fname = bt.get_file("test-t2m-u-v.nc") - ds = psyd.open_dataset(fname) - psyd.rcParams["gridweights.use_cdo"] = True - std = ds.psy.t2m.psy.fldstd(keepdims=True).values - ref = Cdo().fldstd(input=fname, returnArray="t2m") - self.assertAlmostArrayEqual(std, ref) - # try it with the self defined gridweights - psyd.rcParams["gridweights.use_cdo"] = False - std = ds.psy.t2m.psy.fldstd(keepdims=True).values - self.assertAlmostArrayEqual(std, ref, rtol=1e-3) - ds.close() - - @unittest.skipIf(not with_cdo, "CDOs are not installed") - @unittest.skipIf(xr_version[:2] < (0, 9), "xarray version too low") - def test_fldstd_02_icon(self): - from psyplot.project import Cdo - - fname = bt.get_file("icon_test.nc") - ds = psyd.open_dataset(fname) - psyd.rcParams["gridweights.use_cdo"] = True - std = ds.psy.t2m.psy.fldstd().values - ds.close() - ref = Cdo().fldstd(input=fname, name="t2m")[0] - self.assertAlmostArrayEqual(std, ref) - - @unittest.skipIf(not with_cdo, "CDOs are not installed") - @unittest.skipIf(xr_version[:2] < (0, 9), "xarray version too low") - def test_fldpctl_01_lola(self): - fname = bt.get_file("test-t2m-u-v.nc") - ds = psyd.open_dataset(fname) - pctl = ds.psy.t2m.psy.fldpctl(5).values - self.assertEqual(pctl.shape, ds.t2m.shape[:-2]) - - pctl = ds.psy.t2m.psy.fldpctl([5, 95]).values - self.assertEqual(pctl.shape, (2,) + ds.t2m.shape[:-2]) - self.assertTrue( - (pctl[1] >= pctl[0]).all(), - msg=( - "95th percentile should always be greater or " - "equal than the 5th percentile! %s %s" - ) - % (pctl[0], pctl[1]), - ) - ds.close() - - @unittest.skipIf(not with_cdo, "CDOs are not installed") - @unittest.skipIf(xr_version[:2] < (0, 9), "xarray version too low") - def test_fldpctl_02_icon(self): - fname = bt.get_file("icon_test.nc") - ds = psyd.open_dataset(fname) - pctl = ds.psy.t2m.psy.fldpctl(5).values - self.assertEqual(pctl.shape, ds.t2m.shape[:-1]) - - pctl = ds.psy.t2m.psy.fldpctl([5, 95]).values - self.assertEqual(pctl.shape, (2,) + ds.t2m.shape[:-1]) - self.assertTrue( - (pctl[1] >= pctl[0]).all(), - msg=( - "95th percentile should always be greater or " - "equal than the 5th percentile! %s %s" - ) - % (pctl[0], pctl[1]), - ) - ds.close() - - -class TestArrayList(unittest.TestCase): - """Test the :class:`psyplot.data.ArrayList` class""" - - _created_files = set() - - def setUp(self): - self._created_files = set() - - def tearDown(self): - for f in self._created_files: - try: - os.remove(f) - except Exception: - pass - self._created_files.clear() - - list_class = psyd.ArrayList - - def test_setup_coords(self): - """Set the :func:`psyplot.data.setup_coords` function""" - coords = {"first": [1, 2]} - self.assertEqual( - psyd.setup_coords(second=3, **coords), - { - "arr0": {"first": 1, "second": 3}, - "arr1": {"first": 2, "second": 3}, - }, - ) - self.assertEqual( - psyd.setup_coords(dims=coords, second=3), - { - "arr0": {"first": 1, "second": 3}, - "arr1": {"first": 2, "second": 3}, - }, - ) - coords["third"] = [1, 2, 3] - # test sorting - ret = psyd.setup_coords( - arr_names="test{}", second=3, sort=["third", "first"], **coords - ) - self.assertEqual( - ret, - { - "test0": {"third": 1, "first": 1, "second": 3}, - "test1": {"third": 1, "first": 2, "second": 3}, - "test2": {"third": 2, "first": 1, "second": 3}, - "test3": {"third": 2, "first": 2, "second": 3}, - "test4": {"third": 3, "first": 1, "second": 3}, - "test5": {"third": 3, "first": 2, "second": 3}, - }, - ) - - @property - def _filter_test_ds(self): - return xr.Dataset( - { - "v0": xr.Variable( - ("ydim", "xdim"), - np.zeros((4, 4)), - attrs={"test": 1, "test2": 1}, - ), - "v1": xr.Variable( - ("xdim",), np.zeros(4), attrs={"test": 2, "test2": 2} - ), - "v2": xr.Variable( - ("xdim",), np.zeros(4), attrs={"test": 3, "test2": 3} - ), - }, - { - "ydim": xr.Variable(("ydim",), np.arange(1, 5)), - "xdim": xr.Variable(("xdim",), np.arange(4)), - }, - ) - - def test_filter_1_name(self): - """Test the filtering of the ArrayList""" - ds = self._filter_test_ds - arrays = self.list_class.from_dataset(ds, ydim=0) - arrays.extend( - self.list_class.from_dataset(ds, ydim=1, name="v0"), new_name=True - ) - # filter by name - self.assertEqual([arr.name for arr in arrays(name="v1")], ["v1"]) - self.assertEqual( - [arr.name for arr in arrays(name=["v1", "v2"])], ["v1", "v2"] - ) - self.assertEqual( - [ - arr.psy.arr_name - for arr in arrays(arr_name=lambda name: name == "arr1") - ], - ["arr1"], - ) - - def test_filter_2_arr_name(self): - """Test the filtering of the ArrayList""" - ds = self._filter_test_ds - arrays = self.list_class.from_dataset(ds, ydim=0) - arrays.extend( - self.list_class.from_dataset(ds, ydim=1, name="v0"), new_name=True - ) - # fillter by array name - self.assertEqual( - [arr.psy.arr_name for arr in arrays(arr_name="arr1")], ["arr1"] - ) - self.assertEqual( - [arr.psy.arr_name for arr in arrays(arr_name=["arr1", "arr2"])], - ["arr1", "arr2"], - ) - self.assertEqual( - [ - arr.psy.arr_name - for arr in arrays(name=lambda name: name == "v1") - ], - ["arr1"], - ) - - def test_filter_3_attribute(self): - """Test the filtering of the ArrayList""" - ds = self._filter_test_ds - arrays = self.list_class.from_dataset(ds, ydim=0) - arrays.extend( - self.list_class.from_dataset(ds, ydim=1, name="v0"), new_name=True - ) - # filter by attribute - self.assertEqual([arr.name for arr in arrays(test=2)], ["v1"]) - self.assertEqual( - [arr.name for arr in arrays(test=[2, 3])], ["v1", "v2"] - ) - self.assertEqual( - [arr.name for arr in arrays(test=[1, 2], test2=2)], ["v1"] - ) - self.assertEqual( - [arr.psy.arr_name for arr in arrays(test=lambda val: val == 2)], - ["arr1"], - ) - - def test_filter_4_coord(self): - """Test the filtering of the ArrayList""" - ds = self._filter_test_ds - arrays = self.list_class.from_dataset(ds, ydim=0) - arrays.extend( - self.list_class.from_dataset(ds, ydim=1, name="v0"), new_name=True - ) - # filter by coordinate - self.assertEqual([arr.psy.arr_name for arr in arrays(y=0)], ["arr0"]) - self.assertEqual([arr.psy.arr_name for arr in arrays(y=1)], ["arr3"]) - self.assertEqual( - [arr.psy.arr_name for arr in arrays(y=1, method="sel")], ["arr0"] - ) - self.assertEqual( - [arr.psy.arr_name for arr in arrays(y=lambda val: val == 0)], - ["arr0"], - ) - - def test_filter_5_mixed(self): - """Test the filtering of the ArrayList""" - ds = self._filter_test_ds - arrays = self.list_class.from_dataset(ds, ydim=0) - arrays.extend( - self.list_class.from_dataset(ds, ydim=1, name="v0"), new_name=True - ) - # mix criteria - self.assertEqual( - [ - arr.psy.arr_name - for arr in arrays(arr_name=["arr0", "arr1"], test=1) - ], - ["arr0"], - ) - - def test_filter_6_ax(self): - """Test the filtering of the ArrayList""" - import matplotlib.pyplot as plt - - from psyplot.plotter import Plotter - - ds = self._filter_test_ds - arrays = self.list_class.from_dataset(ds, ydim=[0, 1], name="v0") - axes = plt.subplots(1, 2)[1] - for i, arr in enumerate(arrays): - Plotter(arr, ax=axes[i]) - # mix criteria - self.assertEqual( - [arr.psy.arr_name for arr in arrays(ax=axes[0])], - [arrays[0].psy.arr_name], - ) - self.assertEqual( - [arr.psy.arr_name for arr in arrays(ax=axes[1])], - [arrays[1].psy.arr_name], - ) - - def test_filter_7_fig(self): - """Test the filtering of the ArrayList""" - import matplotlib.pyplot as plt - - from psyplot.plotter import Plotter - - ds = self._filter_test_ds - arrays = self.list_class.from_dataset(ds, ydim=[0, 1], name="v0") - figs = [0, 0] - axes = [0, 0] - figs[0], axes[0] = plt.subplots() - figs[1], axes[1] = plt.subplots() - for i, arr in enumerate(arrays): - Plotter(arr, ax=axes[i]) - # mix criteria - self.assertEqual( - [arr.psy.arr_name for arr in arrays(fig=figs[0])], - [arrays[0].psy.arr_name], - ) - self.assertEqual( - [arr.psy.arr_name for arr in arrays(fig=figs[1])], - [arrays[1].psy.arr_name], - ) - - def test_filter_8_fmts(self): - from test_plotter import SimpleFmt, TestPlotter - - ds = self._filter_test_ds - arrays = self.list_class.from_dataset(ds, ydim=[0, 1], name="v0") - - class TestPlotter2(TestPlotter): - fmt_test = SimpleFmt("fmt_test") - - TestPlotter(arrays[0]) - TestPlotter2(arrays[1]) - - self.assertEqual(arrays(fmts=["fmt1"]).arr_names, arrays.arr_names) - self.assertEqual( - arrays(fmts=["fmt_test"]).arr_names, [arrays[1].psy.arr_name] - ) - - def test_list_filter_1_name(self): - """Test the filtering of InteractiveList by the variable name""" - ds = self._filter_test_ds - arrays = self.list_class.from_dataset( - ds, name="v1", ydim=[0, 1], prefer_list=True - ) - arrays.extend( - self.list_class.from_dataset( - ds, name="v2", xdim=[0, 1], prefer_list=True - ), - new_name=True, - ) - self.assertEqual( - [arr.psy.arr_name for arr in arrays(name="v1")], ["arr0"] - ) - self.assertEqual( - [arr.psy.arr_name for arr in arrays(name="v2")], ["arr1"] - ) - self.assertEqual( - [arr.psy.arr_name for arr in arrays(name=lambda n: n == "v1")], - ["arr0"], - ) - - def test_list_filter_2_arr_name(self): - """Test the filtering of InteractiveList by the array name""" - ds = self._filter_test_ds - arrays = self.list_class.from_dataset( - ds, name="v1", ydim=[0, 1], prefer_list=True - ) - arrays.extend( - self.list_class.from_dataset( - ds, name="v2", xdim=[0, 1], prefer_list=True - ), - new_name=True, - ) - self.assertEqual( - [arr.psy.arr_name for arr in arrays(arr_name="arr0")], ["arr0"] - ) - self.assertEqual( - [arr.psy.arr_name for arr in arrays(arr_name="arr1")], ["arr1"] - ) - self.assertEqual( - [ - arr.psy.arr_name - for arr in arrays(arr_name=lambda an: an == "arr0") - ], - ["arr0"], - ) - - def test_list_filter_3_attribute(self): - """Test the filtering of InteractiveList by attribute""" - ds = self._filter_test_ds - arrays = self.list_class.from_dataset( - ds, name="v1", ydim=[0, 1], prefer_list=True - ) - arrays.extend( - self.list_class.from_dataset( - ds, name="v2", xdim=[0, 1], prefer_list=True - ), - new_name=True, - ) - self.assertEqual( - [arr.psy.arr_name for arr in arrays(test=2)], ["arr0"] - ) - self.assertEqual( - [arr.psy.arr_name for arr in arrays(test=3)], ["arr1"] - ) - self.assertEqual( - [arr.psy.arr_name for arr in arrays(test=lambda i: i == 2)], - ["arr0"], - ) - - def test_list_filter_4_coord(self): - """Test the filtering of InteractiveList by the coordinate""" - ds = self._filter_test_ds - arrays = self.list_class.from_dataset( - ds, name=["v1", "v2"], xdim=0, prefer_list=True - ) - arrays.extend( - self.list_class.from_dataset( - ds, name=["v1", "v2"], xdim=1, prefer_list=True - ), - new_name=True, - ) - self.assertEqual( - [arr.psy.arr_name for arr in arrays(xdim=0)], ["arr0"] - ) - self.assertEqual( - [arr.psy.arr_name for arr in arrays(xdim=1)], ["arr1"] - ) - self.assertEqual( - [arr.psy.arr_name for arr in arrays(xdim=1, method="sel")], - ["arr1"], - ) - self.assertEqual( - [arr.psy.arr_name for arr in arrays(xdim=lambda i: i == 0)], - ["arr0"], - ) - self.assertEqual( - [ - arr.psy.arr_name - for arr in arrays(xdim=lambda i: i == 1, method="sel") - ], - ["arr1"], - ) - - def test_list_filter_5_coord_list(self): - """Test the filtering of InteractiveList by the coordinate with a list""" - ds = self._filter_test_ds - arrays = self.list_class.from_dataset( - ds, name="v0", ydim=[0, 1], prefer_list=True - ) - arrays.extend( - self.list_class.from_dataset( - ds, name="v0", ydim=[2, 3], prefer_list=True - ), - new_name=True, - ) - self.assertEqual( - [arr.psy.arr_name for arr in arrays(ydim=[0, 1])], ["arr0"] - ) - self.assertEqual( - [arr.psy.arr_name for arr in arrays(ydim=[2, 3])], ["arr1"] - ) - self.assertEqual( - [arr.psy.arr_name for arr in arrays(ydim=[1, 2], method="sel")], - ["arr0"], - ) - self.assertEqual( - [arr.psy.arr_name for arr in arrays(ydim=[3, 4], method="sel")], - ["arr1"], - ) - - def test_list_filter_6_mixed(self): - """Test the filtering of InteractiveList by attribute""" - ds = self._filter_test_ds - arrays = self.list_class.from_dataset( - ds, name="v0", ydim=[0, 1], prefer_list=True - ) - arrays.extend( - self.list_class.from_dataset( - ds, name="v0", ydim=[2, 3], prefer_list=True - ), - new_name=True, - ) - self.assertEqual( - [arr.psy.arr_name for arr in arrays(name="v0", ydim=[2, 3])], - ["arr1"], - ) - - @property - def _from_dataset_test_variables(self): - """The variables and coords needed for the from_dataset tests""" - variables = { - # 3d-variable - "v0": xr.Variable(("time", "ydim", "xdim"), np.zeros((4, 4, 4))), - # 2d-variable with time and x - "v1": xr.Variable( - ( - "time", - "xdim", - ), - np.zeros((4, 4)), - ), - # 2d-variable with y and x - "v2": xr.Variable( - ( - "ydim", - "xdim", - ), - np.zeros((4, 4)), - ), - # 1d-variable - "v3": xr.Variable(("xdim",), np.zeros(4)), - } - coords = { - "ydim": xr.Variable(("ydim",), np.arange(1, 5)), - "xdim": xr.Variable(("xdim",), np.arange(4)), - "time": xr.Variable( - ("time",), - pd.date_range("1999-01-01", "1999-05-01", freq="M").values, - ), - } - return variables, coords - - def test_from_dataset_01_basic(self): - """test creation without any additional information""" - variables, coords = self._from_dataset_test_variables - ds = xr.Dataset(variables, coords) - arrays = self.list_class.from_dataset(ds) - self.assertEqual(len(arrays), 4) - self.assertEqual(set(arrays.names), set(variables)) - for arr in arrays: - self.assertEqual( - arr.dims, - variables[arr.name].dims, - msg="Wrong dimensions for variable " + arr.name, - ) - self.assertEqual( - arr.shape, - variables[arr.name].shape, - msg="Wrong shape for variable " + arr.name, - ) - - def test_from_dataset_02_name(self): - """Test the from_dataset creation method with selected names""" - variables, coords = self._from_dataset_test_variables - ds = xr.Dataset(variables, coords) - arrays = self.list_class.from_dataset(ds, name="v2") - self.assertEqual(len(arrays), 1) - self.assertEqual(set(arrays.names), {"v2"}) - for arr in arrays: - self.assertEqual( - arr.dims, - variables[arr.name].dims, - msg="Wrong dimensions for variable " + arr.name, - ) - self.assertEqual( - arr.shape, - variables[arr.name].shape, - msg="Wrong shape for variable " + arr.name, - ) - - def test_from_dataset_03_simple_selection(self): - """Test the from_dataset creation method with x- and t-selection""" - variables, coords = self._from_dataset_test_variables - ds = xr.Dataset(variables, coords) - arrays = self.list_class.from_dataset(ds, x=0, t=0) - self.assertEqual(len(arrays), 4) - self.assertEqual(set(arrays.names), set(variables)) - for arr in arrays: - self.assertEqual( - arr.xdim.ndim, 0, msg="Wrong x dimension for " + arr.name - ) - if "time" in arr.dims: - self.assertEqual( - arr.time, - coords["time"], - msg="Wrong time dimension for " + arr.name, - ) - - def test_from_dataset_04_exact_selection(self): - """Test the from_dataset creation method with selected names""" - variables, coords = self._from_dataset_test_variables - ds = xr.Dataset(variables, coords) - arrays = self.list_class.from_dataset( - ds, ydim=2, method=None, name=["v0", "v2"] - ) - self.assertEqual(len(arrays), 2) - self.assertEqual(set(arrays.names), {"v0", "v2"}) - for arr in arrays: - self.assertEqual( - arr.ydim, 2, msg="Wrong ydim slice for " + arr.name - ) - - def test_from_dataset_05_exact_array_selection(self): - """Test the from_dataset creation method with selected names""" - variables, coords = self._from_dataset_test_variables - ds = xr.Dataset(variables, coords) - arrays = self.list_class.from_dataset( - ds, ydim=[[2, 3]], method=None, name=["v0", "v2"] - ) - self.assertEqual(len(arrays), 2) - self.assertEqual(set(arrays.names), {"v0", "v2"}) - for arr in arrays: - self.assertEqual( - arr.ydim.values.tolist(), - [2, 3], - msg="Wrong ydim slice for " + arr.name, - ) - - def test_from_dataset_06_nearest_selection(self): - """Test the from_dataset creation method with selected names""" - variables, coords = self._from_dataset_test_variables - ds = xr.Dataset(variables, coords) - arrays = self.list_class.from_dataset( - ds, ydim=1.7, method="nearest", name=["v0", "v2"] - ) - self.assertEqual(len(arrays), 2) - self.assertEqual(set(arrays.names), {"v0", "v2"}) - for arr in arrays: - self.assertEqual( - arr.ydim, 2, msg="Wrong ydim slice for " + arr.name - ) - - def test_from_dataset_07_time_selection(self): - """Test the from_dataset creation method with selected names""" - variables, coords = self._from_dataset_test_variables - ds = xr.Dataset(variables, coords) - arrays = self.list_class.from_dataset( - ds, t="1999-02-28", method=None, name=["v0", "v1"] - ) - self.assertEqual(len(arrays), 2) - self.assertEqual(set(arrays.names), {"v0", "v1"}) - for arr in arrays: - self.assertEqual( - arr.time, - coords["time"][1], - msg="Wrong time slice for " + arr.name, - ) - - def test_from_dataset_08_time_array_selection(self): - """Test the from_dataset creation method with selected names""" - variables, coords = self._from_dataset_test_variables - ds = xr.Dataset(variables, coords) - # test with array of time - arrays = self.list_class.from_dataset( - ds, t=[coords["time"][1:3]], method=None, name=["v0", "v1"] - ) - self.assertEqual(len(arrays), 2) - self.assertEqual(set(arrays.names), {"v0", "v1"}) - for arr in arrays: - self.assertEqual( - arr.time.values.tolist(), - coords["time"][1:3].values.tolist(), - msg="Wrong time slice for " + arr.name, - ) - - def test_from_dataset_09_nearest_time_selection(self): - """Test the from_dataset creation method with selected names""" - variables, coords = self._from_dataset_test_variables - ds = xr.Dataset(variables, coords) - arrays = self.list_class.from_dataset( - ds, t="1999-02-20", method="nearest", name=["v0", "v1"] - ) - self.assertEqual(len(arrays), 2) - self.assertEqual(set(arrays.names), {"v0", "v1"}) - for arr in arrays: - self.assertEqual( - arr.time, - coords["time"][1], - msg="Wrong time slice for " + arr.name, - ) - - def test_from_dataset_10_2_vars(self): - """Test the creation of arrays out of two variables""" - variables, coords = self._from_dataset_test_variables - variables["v4"] = variables["v3"].copy() - ds = xr.Dataset(variables, coords) - arrays = self.list_class.from_dataset( - ds, name=[["v3", "v4"], "v2"], xdim=[[2]], squeeze=False - ) - self.assertEqual(len(arrays), 2) - self.assertIn("variable", arrays[0].dims) - self.assertEqual( - arrays[0].coords["variable"].values.tolist(), ["v3", "v4"] - ) - self.assertEqual(arrays[0].ndim, 2) - - self.assertEqual(arrays[1].name, "v2") - self.assertEqual(arrays[1].ndim, variables["v2"].ndim) - - def test_from_dataset_11_list(self): - """Test the creation of a list of InteractiveLists""" - variables, coords = self._from_dataset_test_variables - ds = xr.Dataset(variables, coords) - # Create two lists, each containing two arrays of variables v1 and v2. - # In the first list, the xdim dimensions are 0 and 1. - # In the second, the xdim dimensions are both 2 - arrays = self.list_class.from_dataset( - ds, name=[["v1", "v2"]], xdim=[[0, 1], 2], prefer_list=True - ) - - self.assertEqual(len(arrays), 2) - self.assertIsInstance(arrays[0], psyd.InteractiveList) - self.assertIsInstance(arrays[1], psyd.InteractiveList) - self.assertEqual(len(arrays[0]), 2) - self.assertEqual(len(arrays[1]), 2) - self.assertEqual(arrays[0][0].xdim, 0) - self.assertEqual(arrays[0][1].xdim, 1) - self.assertEqual(arrays[1][0].xdim, 2) - self.assertEqual(arrays[1][1].xdim, 2) - - def test_from_dataset_12_list_and_2_vars(self): - """Test the creation of a list of Interactive lists with one array out - of 2 variables""" - variables, coords = self._from_dataset_test_variables - variables["v4"] = variables["v3"].copy() - ds = xr.Dataset(variables, coords) - arrays = ds.psy.create_list( - ds, name=[["v1", ["v3", "v4"]], ["v1", "v2"]], prefer_list=True - ) - - self.assertEqual(len(arrays), 2) - self.assertIsInstance(arrays[0], psyd.InteractiveList) - self.assertIsInstance(arrays[1], psyd.InteractiveList) - self.assertEqual(len(arrays[0]), 2) - self.assertEqual(len(arrays[1]), 2) - - def test_from_dataset_13_decoder_class(self): - ds = xr.Dataset(*self._from_dataset_test_variables) - - class MyDecoder(psyd.CFDecoder): - pass - - arrays = self.list_class.from_dataset(ds, name="v2", decoder=MyDecoder) - self.assertIsInstance(arrays[0].psy.decoder, MyDecoder) - - def test_from_dataset_14_decoder_instance(self): - ds = xr.Dataset(*self._from_dataset_test_variables) - - class MyDecoder(psyd.CFDecoder): - pass - - decoder = MyDecoder(ds) - - arrays = self.list_class.from_dataset(ds, name="v2", decoder=decoder) - self.assertIs(arrays[0].psy.decoder, decoder) - - def test_from_dataset_15_decoder_kws(self): - ds = xr.Dataset(*self._from_dataset_test_variables) - - arrays = self.list_class.from_dataset( - ds, name="v2", decoder=dict(x={"myx"}) - ) - self.assertEqual(arrays[0].psy.decoder.x, {"myx"}) - - def test_from_dataset_16_default_slice(self): - """Test selection with default_slice=0""" - variables, coords = self._from_dataset_test_variables - ds = xr.Dataset(variables, coords) - arrays = self.list_class.from_dataset( - ds, ydim=2, default_slice=0, method=None, name=["v0", "v2"] - ) - self.assertEqual(len(arrays), 2) - self.assertEqual(set(arrays.names), {"v0", "v2"}) - for arr in arrays: - self.assertEqual( - arr.ydim, 2, msg="Wrong ydim slice for " + arr.name - ) - - def test_array_info(self): - variables, coords = self._from_dataset_test_variables - variables["v4"] = variables["v3"].copy() - ds = xr.Dataset(variables, coords) - fname = osp.relpath(bt.get_file("test-t2m-u-v.nc"), ".") - ds2 = xr.open_dataset(fname) - arrays = ds.psy.create_list( - name=[["v1", ["v3", "v4"]], ["v1", "v2"]], prefer_list=True - ) - arrays.extend( - ds2.psy.create_list(name=["t2m"], x=0, t=1), new_name=True - ) - if xr_version < (0, 17): - nc_store = ("xarray.backends.netCDF4_", "NetCDF4DataStore") - else: - nc_store = (None, None) - self.assertEqual( - arrays.array_info(engine="netCDF4"), - dict( - [ - # first list contating an array with two variables - ( - "arr0", - dict( - [ - ( - "arr0", - { - "dims": { - "t": slice(None), - "x": slice(None), - }, - "attrs": dict(), - "store": (None, None), - "name": "v1", - "fname": None, - }, - ), - ( - "arr1", - { - "dims": {"y": slice(None)}, - "attrs": dict(), - "store": (None, None), - "name": [["v3", "v4"]], - "fname": None, - }, - ), - ("attrs", dict()), - ] - ), - ), - # second list with two arrays containing each one variable - ( - "arr1", - dict( - [ - ( - "arr0", - { - "dims": { - "t": slice(None), - "x": slice(None), - }, - "attrs": dict(), - "store": (None, None), - "name": "v1", - "fname": None, - }, - ), - ( - "arr1", - { - "dims": { - "y": slice(None), - "x": slice(None), - }, - "attrs": dict(), - "store": (None, None), - "name": "v2", - "fname": None, - }, - ), - ("attrs", dict()), - ] - ), - ), - # last array from real dataset - ( - "arr2", - { - "dims": { - "z": slice(None), - "y": slice(None), - "t": 1, - "x": 0, - }, - "attrs": ds2.t2m.attrs, - "store": nc_store, - "name": "t2m", - "fname": fname, - }, - ), - ("attrs", dict()), - ] - ), - ) - return arrays - - def test_from_dict_01(self): - """Test the creation from a dictionary""" - arrays = self.test_array_info() - d = arrays.array_info(engine="netCDF4") - self.assertEqual( - self.list_class.from_dict(d).array_info(), arrays[-1:].array_info() - ) - d = arrays.array_info(ds_description={"ds"}) - self.assertEqual( - self.list_class.from_dict(d).array_info(), arrays.array_info() - ) - - def test_from_dict_02_only(self): - """Test the only keyword""" - arrays = self.test_array_info() - d = arrays.array_info(ds_description={"ds"}) - # test to use only the first 2 - self.assertEqual( - self.list_class.from_dict( - d, only=arrays.arr_names[1:] - ).array_info(), - arrays[1:].array_info(), - ) - # test to a pattern - self.assertEqual( - self.list_class.from_dict( - d, only="|".join(arrays.arr_names[1:]) - ).array_info(), - arrays[1:].array_info(), - ) - # test to a function - self.assertEqual( - self.list_class.from_dict( - d, - only=lambda n, info: ( - n in arrays.arr_names[1:] and "name" not in "info" - ), - ).array_info(), - arrays[1:].array_info(), - ) - - def test_from_dict_03_mfdataset(self): - """Test opening a multifile dataset""" - ds = xr.Dataset(*self._from_dataset_test_variables) - ds1 = ds.isel(time=slice(0, 2)) - ds2 = ds.isel(time=slice(2, None)) - fname1 = tempfile.NamedTemporaryFile( - suffix=".nc", prefix="tmp_psyplot_" - ).name - ds1.to_netcdf(fname1) - self._created_files.add(fname1) - fname2 = tempfile.NamedTemporaryFile( - suffix=".nc", prefix="tmp_psyplot_" - ).name - ds2.to_netcdf(fname2) - self._created_files.add(fname2) - - # now open the mfdataset - ds = psyd.open_mfdataset([fname1, fname2]) - arrays = self.list_class.from_dataset(ds, name=["v0"], time=[0, 3]) - if xr_version >= (0, 18): - ds.psy.filename = [fname1, fname2] - self.assertEqual( - self.list_class.from_dict(arrays.array_info()).array_info(), - arrays.array_info(), - ) - ds.close() - - def test_from_dict_04_concat_dim(self): - """Test opening a multifile dataset that requires a ``concat_dim``""" - ds = xr.Dataset(*self._from_dataset_test_variables) - ds1 = ds.isel(time=0) - ds2 = ds.isel(time=1) - fname1 = tempfile.NamedTemporaryFile( - suffix=".nc", prefix="tmp_psyplot_" - ).name - ds1.to_netcdf(fname1) - self._created_files.add(fname1) - fname2 = tempfile.NamedTemporaryFile( - suffix=".nc", prefix="tmp_psyplot_" - ).name - ds2.to_netcdf(fname2) - self._created_files.add(fname2) - - # now open the mfdataset - ds = psyd.open_mfdataset( - [fname1, fname2], concat_dim="time", combine="nested" - ) - arrays = self.list_class.from_dataset(ds, name=["v0"], time=[0, 1]) - self.assertEqual( - self.list_class.from_dict(arrays.array_info()).array_info(), - arrays.array_info(), - ) - - def test_logger(self): - """Test whether one can access the logger""" - import logging - - arrays = self.test_array_info() - self.assertIsInstance(arrays.logger, logging.Logger) - - -class TestInteractiveList(TestArrayList): - """Test case for the :class:`psyplot.data.InteractiveList` class""" - - list_class = psyd.InteractiveList - - def test_to_dataframe(self): - variables, coords = self._from_dataset_test_variables - variables["v1"][:] = np.arange(variables["v1"].size).reshape( - variables["v1"].shape - ) - ds = xr.Dataset(variables, coords) - arrays = psyd.InteractiveList.from_dataset(ds, name="v1", t=[0, 1]) - arrays.extend( - psyd.InteractiveList.from_dataset( - ds, name="v1", t=2, x=slice(1, 3) - ), - new_name=True, - ) - self.assertEqual(len(arrays), 3) - self.assertTrue(all(arr.ndim == 1 for arr in arrays), msg=arrays) - df = arrays.to_dataframe() - self.assertEqual(df.shape, (ds.xdim.size, 3)) - self.assertEqual(df.index.values.tolist(), ds.xdim.values.tolist()) - self.assertEqual( - df[arrays[0].psy.arr_name].values.tolist(), - ds.v1[0].values.tolist(), - ) - self.assertEqual( - df[arrays[1].psy.arr_name].values.tolist(), - ds.v1[1].values.tolist(), - ) - self.assertEqual(df[arrays[2].psy.arr_name].notnull().sum(), 2) - self.assertEqual( - df[arrays[2].psy.arr_name] - .values[df[arrays[2].psy.arr_name].notnull().values] - .tolist(), - ds.v1[2, 1:3].values.tolist(), - ) - - -class AbsoluteTimeTest(unittest.TestCase, AlmostArrayEqualMixin): - """TestCase for loading and storing absolute times""" - - _created_files = set() - - def setUp(self): - self._created_files = set() - - def tearDown(self): - for f in self._created_files: - try: - os.remove(f) - except Exception: - pass - self._created_files.clear() - - @property - def _test_ds(self): - import pandas as pd - import xarray as xr - - time = xr.Variable( - "time", - pd.to_datetime( - [ - "1979-01-01T12:00:00", - "1979-01-01T18:00:00", - "1979-01-01T18:30:00", - ] - ), - encoding={"units": "day as %Y%m%d.%f"}, - ) - var = xr.Variable(("time", "x"), np.zeros((len(time), 5))) - return xr.Dataset({"test": var}, {"time": time}) - - def test_to_netcdf(self): - """Test whether the data is stored correctly""" - import netCDF4 as nc - - ds = self._test_ds - fname = tempfile.NamedTemporaryFile( - suffix=".nc", prefix="tmp_psyplot_" - ).name - self._created_files.add(fname) - psyd.to_netcdf(ds, fname) - with nc.Dataset(fname) as nco: - self.assertAlmostArrayEqual( - nco.variables["time"][:], - [19790101.5, 19790101.75, 19790101.75 + 30.0 / (24.0 * 60.0)], - rtol=0, - atol=1e-5, - ) - self.assertEqual(nco.variables["time"].units, "day as %Y%m%d.%f") - return fname - - def test_open_dataset(self): - fname = self.test_to_netcdf() - ref_ds = self._test_ds - ds = psyd.open_dataset(fname) - self.assertEqual( - pd.to_datetime(ds.time.values).tolist(), - pd.to_datetime(ref_ds.time.values).tolist(), - ) - - -class FilenamesTest(unittest.TestCase): - """Test whether the filenames can be extracted correctly""" - - @property - def fname(self): - return osp.join(osp.dirname(__file__), "test-t2m-u-v.nc") - - def _test_engine(self, engine): - from importlib import import_module - - fname = self.fname - ds = psyd.open_dataset(fname, engine=engine).load() - self.assertEqual(ds.psy.filename, fname) - store_mod, store = ds.psy.data_store - # try to load the dataset - mod = import_module(store_mod) - try: - ds2 = psyd.open_dataset(getattr(mod, store).open(fname)) - except AttributeError: - ds2 = psyd.open_dataset(getattr(mod, store)(fname)) - ds.close() - ds2.close() - ds.psy.filename = None - dumped_fname, dumped_store_mod, dumped_store = psyd.get_filename_ds( - ds, dump=True, engine=engine, paths=True - ) - self.assertTrue(dumped_fname) - self.assertTrue(osp.exists(dumped_fname), msg="Missing %s" % fname) - self.assertEqual(dumped_store_mod, store_mod) - self.assertEqual(dumped_store, store) - ds.close() - ds.psy.filename = None - os.remove(dumped_fname) - - dumped_fname, dumped_store_mod, dumped_store = psyd.get_filename_ds( - ds, dump=True, engine=engine, paths=dumped_fname - ) - self.assertTrue(dumped_fname) - self.assertTrue(osp.exists(dumped_fname), msg="Missing %s" % fname) - self.assertEqual(dumped_store_mod, store_mod) - self.assertEqual(dumped_store, store) - ds.close() - os.remove(dumped_fname) - - @unittest.skipIf(xr_version >= (0, 17), "Not supported for xarray>=0.18") - @unittest.skipIf(not with_nio, "Nio module not installed") - def test_nio(self): - self._test_engine("pynio") - - @unittest.skipIf(xr_version >= (0, 17), "Not supported for xarray>=0.18") - @unittest.skipIf(not with_netcdf4, "netCDF4 module not installed") - def test_netcdf4(self): - self._test_engine("netcdf4") - - @unittest.skipIf(xr_version >= (0, 17), "Not supported for xarray>=0.18") - @unittest.skipIf(not with_scipy, "scipy module not installed") - def test_scipy(self): - self._test_engine("scipy") - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/test_gdalstore.py b/tests/test_gdalstore.py deleted file mode 100644 index bb1fe74..0000000 --- a/tests/test_gdalstore.py +++ /dev/null @@ -1,55 +0,0 @@ -"""Module to test the :mod:`psyplot.gdal_store` module.""" - -# SPDX-FileCopyrightText: 2016-2024 University of Lausanne -# SPDX-FileCopyrightText: 2020-2021 Helmholtz-Zentrum Geesthacht - -# SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH -# -# SPDX-License-Identifier: LGPL-3.0-only - -import unittest - -import _base_testing as bt -import pandas as pd - -import psyplot.data as psyd - -try: - import gdal -except ImportError: - gdal = False - - -class TestGdalStore(unittest.TestCase): - """Class to test the :class:`psyplot.gdal_store.GdalStore` class""" - - @unittest.skipIf(not gdal, "GDAL module not installed") - def test_open_geotiff(self): - """Test to open a GeoTiff file""" - ds_ref = psyd.open_dataset(bt.get_file("test-t2m-u-v.nc")) - ds_tiff = psyd.open_dataset( - bt.get_file("test-t2m-1979-01-31T18-00-00.tif"), engine="gdal" - ) - self.assertListEqual( - ds_tiff.Band1.values.tolist(), - ds_ref.isel(time=0, lev=0).t2m.values.tolist(), - ) - - @unittest.skipIf(not gdal, "GDAL module not installed") - def test_open_mf_geotiff(self): - """Test to open multiple GeoTiff files and extract the time from the - file name""" - ds_ref = psyd.open_dataset(bt.get_file("test-t2m-u-v.nc")) - ds_tiff = psyd.open_mfdataset( - bt.get_file("test-t2m-*.tif"), - engine="gdal", - t_format="test-t2m-%Y-%m-%dT%H-%M-%S", - ) - self.assertListEqual( - ds_ref.isel(time=[0, 1], lev=0).t2m.values.tolist(), - ds_tiff.Band1.values.tolist(), - ) - self.assertListEqual( - pd.to_datetime(ds_tiff.time.values).tolist(), - pd.to_datetime(ds_ref.time[:2].values).tolist(), - ) diff --git a/tests/test_main.py b/tests/test_main.py deleted file mode 100644 index 35be0d3..0000000 --- a/tests/test_main.py +++ /dev/null @@ -1,243 +0,0 @@ -"""Test the :mod:`psyplot.__main__` module.""" - -# SPDX-FileCopyrightText: 2016-2024 University of Lausanne -# SPDX-FileCopyrightText: 2020-2021 Helmholtz-Zentrum Geesthacht - -# SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH -# -# SPDX-License-Identifier: LGPL-3.0-only - -import inspect -import os -import os.path as osp -import shutil -import subprocess as spr -import sys -import tempfile -import unittest -from itertools import product - -import _base_testing as bt -import matplotlib.pyplot as plt -import six -import test_plotter as tp -import yaml - -import psyplot -import psyplot.__main__ as main -import psyplot.project as psy - -remove_temp_files = True - - -class TestCommandLine(unittest.TestCase): - """Test the command line utitliy of psyplot""" - - _created_files = set() - - def setUp(self): - psy.close("all") - plt.close("all") - self._created_files = set() - - def tearDown(self): - for identifier in list(psy.registered_plotters): - psy.unregister_plotter(identifier) - psy.close("all") - plt.close("all") - tp.results.clear() - if remove_temp_files: - for f in self._created_files: - if osp.exists(f) and osp.isdir(f): - shutil.rmtree(f) - elif osp.exists(f): - os.remove(f) - self._created_files.clear() - - def _create_and_save_test_project(self): - psy.register_plotter( - "test_plotter", module="test_plotter", plotter_name="TestPlotter" - ) - sp = psy.plot.test_plotter( - bt.get_file("test-t2m-u-v.nc"), name=["t2m", "u"], time=[0, 1] - ) - self.assertEqual(len(sp), 4, sp) - fname = tempfile.NamedTemporaryFile( - suffix=".pkl", prefix="test_psyplot_" - ).name - self._created_files.add(fname) - sp.save_project(fname, use_rel_paths=False) - return sp, fname - - def test_get_parser(self): - parser = main.get_parser() - args = inspect.getfullargspec(main.make_plot)[0] - for arg in args: - self.assertIn( - arg, parser.unfinished_arguments, msg="Missing " + arg - ) - - def test_main_01_from_project(self): - """Test the :func:`psyplot.__main__.main` function""" - if not six.PY2: - with self.assertRaisesRegex(ValueError, "filename"): - main.main(["-o", "test.pdf"]) - sp, fname1 = self._create_and_save_test_project() - fname2 = tempfile.NamedTemporaryFile( - suffix=".pdf", prefix="test_psyplot_" - ).name - self._created_files.add(fname2) - sp.save_project(fname1, use_rel_paths=False) - psy.close("all") - if six.PY2: - main.main(["-p", fname1, "-o", fname2]) - else: - with self.assertWarnsRegex(UserWarning, "ignored"): - main.main(["-p", fname1, "-o", fname2, "-n", "t2m"]) - self.assertTrue(osp.exists(fname2), msg="Missing " + fname2) - self.assertEqual(len(psy.gcp(True)), 4) - - def test_main_02_alternative_ds(self): - sp, fname1 = self._create_and_save_test_project() - fname2 = tempfile.NamedTemporaryFile( - suffix=".pdf", prefix="test_psyplot_" - ).name - self._created_files.add(fname2) - sp.save_project(fname1, use_rel_paths=False) - psy.close("all") - main.main( - [bt.get_file("circumpolar_test.nc"), "-p", fname1, "-o", fname2] - ) - self.assertTrue(osp.exists(fname2), msg="Missing " + fname2) - mp = psy.gcp(True) - self.assertEqual(len(mp), 4) - self.assertEqual( - set( - t[0] - for t in mp._get_dsnames( - mp.array_info(dump=False, use_rel_paths=False) - ) - ), - {bt.get_file("circumpolar_test.nc")}, - ) - - def test_main_03_dims(self): - import yaml - - psy.register_plotter( - "test_plotter", module="test_plotter", plotter_name="TestPlotter" - ) - fname2 = tempfile.NamedTemporaryFile( - suffix=".pdf", prefix="test_psyplot_" - ).name - self._created_files.add(fname2) - # create a formatoptions file - fmt_file = tempfile.NamedTemporaryFile( - suffix=".yml", prefix="test_psyplot_" - ).name - self._created_files.add(fmt_file) - with open(fmt_file, "w") as f: - yaml.dump({"fmt1": "fmt1", "fmt2": "fmt2"}, f) - if not six.PY2: - with self.assertRaisesRegex(ValueError, "plotting method"): - main.main( - [ - bt.get_file("test-t2m-u-v.nc"), - "-o", - fname2, - "-d", - "time,1,2", - "y,3,4", - "-n", - "u", - "v", - ] - ) - main.main( - [ - bt.get_file("test-t2m-u-v.nc"), - "-o", - fname2, - "-d", - "time,1,2", - "y,3,4", - "-n", - "u", - "v", - "-pm", - "test_plotter", - "-fmt", - fmt_file, - ] - ) - mp = psy.gcp(True) - self.assertEqual(len(mp), 2 * 2 * 2, msg=mp) - all_dims = set(product((1, 2), (3, 4), ("u", "v"))) - for arr in mp: - idims = arr.psy.idims - all_dims -= {(idims["time"], idims["lat"], arr.name)} - self.assertFalse(all_dims) - for i, plotter in enumerate(mp.plotters): - self.assertEqual( - plotter["fmt1"], - "fmt1", - msg="Wrong value for fmt1 of plotter %i!" % i, - ) - self.assertEqual( - plotter["fmt2"], - "fmt2", - msg="Wrong value for fmt2 of plotter %i!" % i, - ) - - def test_all_versions(self): - """Test to display all versions""" - ref = psyplot.get_versions() - proc = spr.Popen( - [sys.executable, "-m", "psyplot", "-aV"], - stdout=spr.PIPE, - stderr=spr.PIPE, - ) - proc.wait() - self.assertFalse(proc.poll(), msg=proc.stderr.read()) - d = yaml.load(proc.stdout.read(), yaml.Loader) - d.pop("psyplot_gui", None) - ref.pop("psyplot_gui", None) - # make sure the version does not end with .dirty - d["psyplot"]["version"] = d["psyplot"]["version"].replace(".dirty", "") - ref["psyplot"]["version"] = ref["psyplot"]["version"].replace( - ".dirty", "" - ) - self.assertEqual(d, ref) - - def test_list_plugins(self): - """Test to display all versions""" - ref = psyplot.rcParams._plugins - proc = spr.Popen( - [sys.executable, "-m", "psyplot", "-lp"], - stdout=spr.PIPE, - stderr=spr.PIPE, - ) - proc.wait() - self.assertFalse(proc.poll(), msg=proc.stderr.read()) - d = yaml.load(proc.stdout.read(), yaml.Loader) - self.assertEqual(d, ref) - - def test_list_plot_methods(self): - """Test to display all versions""" - proc = spr.Popen( - [sys.executable, "-m", "psyplot", "-lpm"], - stdout=spr.PIPE, - stderr=spr.PIPE, - ) - proc.wait() - self.assertFalse(proc.poll(), msg=proc.stderr.read()) - import psyplot.project as psy - - for pm, d in psyplot.rcParams["project.plotters"].items(): - try: - psy.register_plotter(pm, **d) - except Exception: - pass - ref = psy.plot._plot_methods - d = yaml.load(proc.stdout.read(), yaml.Loader) - self.assertEqual(d, ref) diff --git a/tests/test_plotter.py b/tests/test_plotter.py deleted file mode 100644 index 1066bf5..0000000 --- a/tests/test_plotter.py +++ /dev/null @@ -1,1022 +0,0 @@ -"""Test module of the :mod:`psyplot.plotter` module.""" - -# SPDX-FileCopyrightText: 2016-2024 University of Lausanne -# SPDX-FileCopyrightText: 2020-2021 Helmholtz-Zentrum Geesthacht - -# SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH -# -# SPDX-License-Identifier: LGPL-3.0-only - -import os.path as osp -import unittest -from itertools import repeat - -import _base_testing as bt -import pandas as pd -import six -import xarray as xr - -import psyplot.config as psyc -import psyplot.data as psyd -import psyplot.plotter as psyp -from psyplot import rcParams - -try: - from textwrap import indent -except ImportError: - - def indent(text, prefix, predicate=None): # python2 - return "\n".join( - prefix + s if predicate is None or predicate(s) else s - for s in text.splitlines() - ) - - -docstrings = psyp.docstrings - - -psyc.setup_logging(osp.join(osp.dirname(__file__), "logging.yml")) - - -results = dict() - - -class TestFormatoption(psyp.Formatoption): - removed = False - - @property - def default(self): - try: - return super(TestFormatoption, self).default - except KeyError: - return "" - - _validate = str - - def update(self, value): - key = "%s.%s" % (self.plotter.data.psy.arr_name, self.key) - if not value: - results.pop(key, None) - else: - results[key] = value - - def remove(self): - self.removed = True - - -@docstrings.get_docstring(base="_testing.SimpleFmt") -@docstrings.get_sections(base="_testing.SimpleFmt") -class SimpleFmt(TestFormatoption): - """ - Just a simple formatoption to check the sharing possibility - - Possible types - -------------- - str - The string to use in the text""" - - group = "labels" - - children = ["fmt2"] - - dependencies = ["fmt3"] - - -class SimpleFmt2(SimpleFmt): - """%(_testing.SimpleFmt)s""" - - children = ["fmt3"] - - dependencies = [] - - -class SimpleFmt3(SimpleFmt): - """ - Third test to check the sharing by groups - - Possible types - -------------- - %(_testing.SimpleFmt.possible_types)s""" - - group = "something" - - children = dependencies = [] - - -ref_docstring = """Third test to check the sharing by groups - -Possible types --------------- -str - The string to use in the text""" - - -class TestPlotter(psyp.Plotter): - """A simple Plotter for testing the plotter-formatoption framework""" - - fmt1 = SimpleFmt("fmt1") - fmt2 = SimpleFmt2("fmt2") - fmt3 = SimpleFmt3("fmt3") - - -class TestPostFormatoption(unittest.TestCase): - """TestCase for the :class:`psyplot.plotter.PostProcessing` formatoption""" - - def test_timing(self): - plotter = TestPlotter(xr.DataArray([]), enable_post=True) - # test attribute for the formatoption - plotter.post.test = [] - plotter.update(post="self.test.append(1)") - # check if the post fmt has been updated - self.assertEqual(plotter.post.test, [1]) - plotter.update(fmt1="something") - # check if the post fmt has been updated - self.assertEqual(plotter.post.test, [1]) - - # -- test replot timing - plotter.update(post_timing="replot") - plotter.update(fmt1="something else") - # check if the post fmt has been updated - self.assertEqual(plotter.post.test, [1]) - plotter.update(fmt2="test", replot=True) - # check if the post fmt has been updated - self.assertEqual(plotter.post.test, [1, 1]) - - # -- test always timing - plotter.update(post_timing="always") - # check if the post fmt has been updated - self.assertEqual(plotter.post.test, [1, 1, 1]) - plotter.update(fmt1="okay") - # check if the post fmt has been updated - self.assertEqual(plotter.post.test, [1, 1, 1, 1]) - - def test_enable(self): - """Test if the warning is raised""" - plotter = TestPlotter( - xr.DataArray([]), post='self.ax.set_title("test")' - ) - self.assertEqual(plotter.ax.get_title(), "") - plotter.enable_post = True - plotter.update(post=plotter.post.value, force=True) - self.assertEqual(plotter.ax.get_title(), "test") - - -class PlotterTest(unittest.TestCase): - """TestCase for testing the Plotter-Formatoption framework""" - - def setUp(self): - results.clear() - rcParams.defaultParams = rcParams.defaultParams.copy() - - def tearDown(self): - results.clear() - rcParams.clear() - rcParams.defaultParams = psyc.rcsetup.defaultParams - rcParams.update_from_defaultParams() - - def test_docstring(self): - """Testing the docstring processing of formatoptions""" - self.assertEqual(SimpleFmt.__doc__, SimpleFmt2.__doc__) - self.assertEqual(SimpleFmt3.__doc__, ref_docstring) - - def test_shared(self): - """Testing the sharing of formatoptions""" - plotter1 = TestPlotter(xr.DataArray([])) - plotter2 = TestPlotter(xr.DataArray([])) - plotter1.data.psy.arr_name = "test1" - plotter2.data.psy.arr_name = "test2" - - results.clear() - # test sharing of two formatoptions - plotter1.share(plotter2, ["fmt1", "fmt3"]) - plotter1.update(fmt1="okay", fmt3="okay2") - # check source - self.assertIn("test1.fmt1", results) - self.assertEqual(results["test1.fmt1"], "okay") - self.assertIn("test1.fmt3", results) - self.assertEqual(results["test1.fmt3"], "okay2") - # checked shared - self.assertIn("test2.fmt1", results) - self.assertEqual(results["test2.fmt1"], "okay") - self.assertIn("test2.fmt3", results) - self.assertEqual(results["test2.fmt3"], "okay2") - - # unshare the formatoptions - plotter1.unshare(plotter2) - # check source - self.assertIn("test1.fmt1", results) - self.assertEqual(results["test1.fmt1"], "okay") - self.assertIn("test1.fmt3", results) - self.assertEqual(results["test1.fmt3"], "okay2") - # check (formerly) shared - self.assertNotIn( - "test2.fmt1", - results, - msg="Value of fmt1: %s, in results: %s" - % (plotter2.fmt1.value, results.get("test2.fmt1")), - ) - self.assertNotIn( - "test2.fmt3", - results, - msg="Value of fmt3: %s, in results: %s" - % (plotter2.fmt3.value, results.get("test2.fmt3")), - ) - - # test sharing of a group of formatoptions - plotter1.share(plotter2, "labels") - plotter1.update(fmt1="okay", fmt2="okay2") - # check source - self.assertIn("test1.fmt1", results) - self.assertEqual(results["test1.fmt1"], "okay") - self.assertIn("test1.fmt2", results) - self.assertEqual(results["test1.fmt2"], "okay2") - # check shared - self.assertIn("test2.fmt1", results) - self.assertEqual(results["test2.fmt1"], "okay") - self.assertIn("test2.fmt2", results) - self.assertEqual(results["test2.fmt2"], "okay2") - self.assertNotIn("test2.fmt3", results) - - # unshare the plotter - plotter2.unshare_me("fmt1") - self.assertNotIn("test2.fmt1", results) - self.assertIn("test2.fmt2", results) - plotter2.unshare_me("labels") - self.assertNotIn("test2.fmt2", results) - - def test_auto_update(self): - """Test the :attr:`psyplot.plotter.Plotter.no_auto_update` attribute""" - data = xr.DataArray([]) - plotter = TestPlotter(data, auto_update=False) - self.assertFalse(plotter.no_auto_update) - data.psy.init_accessor(auto_update=False) - plotter = TestPlotter(data, auto_update=False) - self.assertTrue(plotter.no_auto_update) - - plotter.update(fmt1=1) - self.assertEqual(plotter["fmt1"], "") - self.assertEqual(plotter._registered_updates["fmt1"], 1) - - plotter.start_update() - self.assertEqual(plotter["fmt1"], "1") - self.assertFalse(plotter._registered_updates) - - data.psy.no_auto_update = False - self.assertFalse(plotter.data.psy.no_auto_update) - self.assertFalse(plotter.no_auto_update) - - def test_rc(self): - """Test the default values and validation""" - - def validate(s): - return s + "okay" - - rcParams.defaultParams = rcParams.defaultParams.copy() - rcParams.defaultParams["plotter.test1.fmt1"] = ("test1", validate) - rcParams.defaultParams["plotter.test1.fmt2"] = ("test2", validate) - rcParams.defaultParams["plotter.test1.fmt3"] = ("test3", validate) - rcParams.defaultParams["plotter.test2.fmt3"] = ("test3.2", validate) - rcParams.update( - **{key: val[0] for key, val in rcParams.defaultParams.items()} - ) - - class ThisTestPlotter(TestPlotter): - _rcparams_string = ["plotter.test1."] - - class ThisTestPlotter2(ThisTestPlotter): - _rcparams_string = ["plotter.test2."] - - plotter1 = ThisTestPlotter(xr.DataArray([])) - plotter2 = ThisTestPlotter2(xr.DataArray([])) - - # plotter1 - self.assertEqual(plotter1.fmt1.value, "test1okay") - self.assertEqual(plotter1.fmt2.value, "test2okay") - self.assertEqual(plotter1.fmt3.value, "test3okay") - # plotter2 - self.assertEqual(plotter2.fmt1.value, "test1okay") - self.assertEqual(plotter2.fmt2.value, "test2okay") - self.assertEqual(plotter2.fmt3.value, "test3.2okay") - - def test_fmt_connections(self): - """Test the order of the updates""" - arr = xr.DataArray([]) - arr.psy.arr_name = "arr0" - plotter = TestPlotter(arr, fmt1="test", fmt2="test2", fmt3="test3") - - # check the initialization order - self.assertEqual( - list(results.keys()), ["arr0.fmt3", "arr0.fmt2", "arr0.fmt1"] - ) - - # check the connection properties - self.assertIs(plotter.fmt1.fmt2, plotter.fmt2) - self.assertIs(plotter.fmt1.fmt3, plotter.fmt3) - self.assertIs(plotter.fmt2.fmt3, plotter.fmt3) - - # check the update - results.clear() - plotter.update(fmt2="something", fmt3="else") - self.assertEqual( - list(results.keys()), ["arr0.fmt3", "arr0.fmt2", "arr0.fmt1"] - ) - self.assertEqual(plotter.fmt1.value, "test") - self.assertEqual(plotter.fmt2.value, "something") - self.assertEqual(plotter.fmt3.value, "else") - - self.assertEqual( - list( - plotter._sorted_by_priority( - [plotter.fmt1, plotter.fmt2, plotter.fmt3] - ) - ), - [plotter.fmt3, plotter.fmt2, plotter.fmt1], - ) - if six.PY3: - with self.assertRaisesRegex( - TypeError, "got an unexpected keyword argument 'wrong'" - ): - SimpleFmt("fmt1", wrong="something") - - def test_data_props_array(self): - """Test the data properties of Formatoptions with a DataArray""" - data = xr.DataArray([]) - plot_data = data.copy(True) - plotter = TestPlotter(data) - plotter.plot_data = plot_data - - self.assertIs(plotter.fmt1.raw_data, data) - self.assertIs(plotter.fmt1.data, plot_data) - - def test_data_props_list(self): - """Test the data properties of Formatoptions with an InteractiveList""" - data = psyd.InteractiveList([xr.DataArray([]), xr.DataArray([])]) - plot_data = data.copy(True) - plot_data.extend([xr.DataArray([]), xr.DataArray([])], new_name=True) - plotter = TestPlotter(data) - plotter.plot_data = plot_data - plot_data = plotter.plot_data # the data might have been copied - - self.assertIs(plotter.fmt1.raw_data, data) - self.assertIs(plotter.fmt1.data, plot_data) - - # test with index in list - plotter.fmt1.index_in_list = 1 - self.assertIs(plotter.fmt1.raw_data, data[1]) - self.assertIs(plotter.fmt1.data, plot_data[1]) - - # test with index in list of plot_data outside raw_data - plotter.fmt1.index_in_list = 3 - self.assertIs(plotter.fmt1.data, plot_data[3]) - - def test_decoder(self): - """Test the decoder property of Formatoptions with a DataArray""" - data = xr.DataArray([]) - data.psy.init_accessor(decoder=psyd.CFDecoder(data.psy.base)) - plot_data = data.copy(True) - plotter = TestPlotter(data) - plotter.plot_data = plot_data - - self.assertIsInstance(plotter.fmt1.decoder, psyd.CFDecoder) - self.assertIs(plotter.fmt1.decoder, data.psy.decoder) - - # test with index in list of plot_data outside raw_data - plotter.plot_data_decoder = decoder = psyd.CFDecoder(data.psy.base) - self.assertIsInstance(plotter.fmt1.decoder, psyd.CFDecoder) - self.assertIs(plotter.fmt1.decoder, decoder) - - def test_decoder_list(self): - """Test the decoder property with an InteractiveList""" - data = psyd.InteractiveList([xr.DataArray([]), xr.DataArray([])]) - plot_data = data.copy(True) - plot_data.extend([xr.DataArray([]), xr.DataArray([])], new_name=True) - for arr in data: - arr.psy.init_accessor(decoder=psyd.CFDecoder(arr.psy.base)) - plotter = TestPlotter(data) - plotter.plot_data = plot_data - plot_data = plotter.plot_data # the data might have been copied - - self.assertIsInstance(plotter.fmt1.decoder, psyd.CFDecoder) - self.assertIs(plotter.fmt1.decoder, data[0].psy.decoder) - - # test with index in list - plotter.fmt1.index_in_list = 1 - self.assertIsInstance(plotter.fmt1.decoder, psyd.CFDecoder) - self.assertIs(plotter.fmt1.decoder, data[1].psy.decoder) - - # test without index in list - decoder = psyd.CFDecoder(data[0].psy.base) - plotter.fmt2.decoder = decoder - for i, d2 in enumerate(plotter.plot_data_decoder): - self.assertIs( - d2, decoder, msg="Decoder %i has been set wrong!" % i - ) - self.assertEqual(plotter.fmt2.decoder, plotter.plot_data_decoder) - - # test with index in list of plot_data outside raw_data - plotter.fmt1.index_in_list = 3 - decoder2 = psyd.CFDecoder(data[0].psy.base) - plotter.fmt1.decoder = decoder2 - for i, d2 in enumerate(plotter.plot_data_decoder): - self.assertIs( - d2, - decoder if i != 3 else decoder2, - msg="Decoder %i has been set wrong!" % i, - ) - self.assertIsInstance(plotter.fmt1.decoder, psyd.CFDecoder) - self.assertIs(plotter.fmt1.decoder, plotter.plot_data_decoder[3]) - - def test_any_decoder(self): - """Test the decoder property with an InteractiveList""" - data = psyd.InteractiveList([xr.DataArray([]), xr.DataArray([])]) - plot_data = data.copy(True) - plot_data.extend([xr.DataArray([]), xr.DataArray([])], new_name=True) - for arr in data: - arr.psy.init_accessor(decoder=psyd.CFDecoder(arr.psy.base)) - plotter = TestPlotter(data) - plotter.plot_data = plot_data - plot_data = plotter.plot_data # the data might have been copied - - # test without index in list - decoder = psyd.CFDecoder(data[0].psy.base) - plotter.fmt2.decoder = decoder - for i, d2 in enumerate(plotter.plot_data_decoder): - self.assertIs( - d2, decoder, msg="Decoder %i has been set wrong!" % i - ) - self.assertEqual(plotter.fmt2.decoder, plotter.plot_data_decoder) - self.assertIs(plotter.fmt2.any_decoder, decoder) - - def test_get_enhanced_attrs_01_arr(self): - """Test the :meth:`psyplot.plotter.Plotter.get_enhanced_attrs` method""" - ds = psyd.open_dataset(bt.get_file("test-t2m-u-v.nc")) - plotter = TestPlotter(ds.t2m) - attrs = ds.t2m.attrs.copy() - for key, val in ds.lon.attrs.items(): - attrs["x" + key] = val - for key, val in ds.lat.attrs.items(): - attrs["y" + key] = val - for key, val in ds.lev.attrs.items(): - attrs["z" + key] = val - for key, val in ds.time.attrs.items(): - attrs["t" + key] = val - attrs["xname"] = "lon" - attrs["yname"] = "lat" - attrs["zname"] = "lev" - attrs["tname"] = "time" - attrs["name"] = "t2m" - self.assertEqual( - dict(plotter.get_enhanced_attrs(plotter.plot_data)), dict(attrs) - ) - - def test_get_enhanced_attrs_02_list(self): - """Test the :meth:`psyplot.plotter.Plotter.get_enhanced_attrs` method""" - ds = psyd.open_dataset(bt.get_file("test-t2m-u-v.nc")) - plotter = TestPlotter( - psyd.InteractiveList( - ds.psy.create_list(name=["t2m", "u"], x=0, t=0) - ) - ) - attrs = {} - for key, val in ds.t2m.attrs.items(): - attrs["t2m" + key] = val - for key, val in ds.u.attrs.items(): - attrs["u" + key] = val - for key, val in ds.lon.attrs.items(): - attrs["x" + key] = val - for key, val in ds.lat.attrs.items(): - attrs["y" + key] = val - attrs["x" + key] = val # overwrite the longitude information - # the plot_data has priority over the base variable, therefore we - # the plotter should replace the y information with the z information - for key, val in ds.lev.attrs.items(): - attrs["z" + key] = val - attrs["y" + key] = val # overwrite the latitude information - for key, val in ds.time.attrs.items(): - attrs["t" + key] = val - for key in set(ds.t2m.attrs) & set(ds.u.attrs): - if ds.t2m.attrs[key] == ds.u.attrs[key]: - attrs[key] = ds.t2m.attrs[key] - attrs["zname"] = attrs["yname"] = "lev" - attrs["xname"] = "lat" - attrs["tname"] = "time" - attrs["lon"] = attrs["x"] = ds.lon.values[0] - attrs["time"] = attrs["t"] = pd.to_datetime( - ds.time.values[0] - ).isoformat() - self.maxDiff = None - self.assertEqual( - dict(plotter.get_enhanced_attrs(plotter.plot_data)), dict(attrs) - ) - - def test_show_keys(self): - """Test the :meth:`psyplot.plotter.Plotter.show_keys` method""" - plotter = TestPlotter(xr.DataArray([])) - s = plotter.show_keys(["fmt1", "fmt2", "fmt3"], func=str) - self.assertEqual( - s, - "+------+------+------+\n" - "| fmt1 | fmt2 | fmt3 |\n" - "+------+------+------+", - ) - s = plotter.show_keys(["fmt1", "fmt2", "fmt3"], func=str, grouped=True) - title = psyp.groups["labels"] - self.assertEqual( - s, - "*" * len(title) + "\n" + title + "\n" + "*" * len(title) + "\n" - "+------+------+\n" - "| fmt1 | fmt2 |\n" - "+------+------+\n" - "\n" - "*********\n" - "something\n" - "*********\n" - "+------+\n" - "| fmt3 |\n" - "+------+", - ) - s = plotter.show_keys(["fmt1", "something"], func=str) - self.assertEqual( - s, "+------+------+\n" "| fmt1 | fmt3 |\n" "+------+------+" - ) - if six.PY3: - with self.assertWarnsRegex( - UserWarning, "(?i)unknown formatoption keyword" - ): - s = plotter.show_keys(["fmt1", "wrong", "something"], func=str) - self.assertEqual( - s, - "+------+------+\n" "| fmt1 | fmt3 |\n" "+------+------+", - ) - - def test_show_docs(self): - """Test the :meth:`psyplot.plotter.Plotter.show_docs` method""" - plotter = TestPlotter(xr.DataArray([])) - s = plotter.show_docs(func=str) - self.maxDiff = None - self.assertEqual( - s, - "\n".join( - [ - "fmt1", - "====", - SimpleFmt.__doc__, - "", - "fmt2", - "====", - SimpleFmt2.__doc__, - "", - "fmt3", - "====", - SimpleFmt3.__doc__, - "", - "post", - "====", - psyp.PostProcessing.__doc__, - "", - "post_timing", - "===========", - psyp.PostTiming.__doc__, - "", - ] - ), - ) - s = plotter.show_docs(["fmt1", "fmt2", "fmt3"], func=str, grouped=True) - title = psyp.groups["labels"] - self.assertEqual( - s, - "\n".join( - [ - "*" * len(title), - title, - "*" * len(title), - "fmt1", - "====", - SimpleFmt.__doc__, - "", - "fmt2", - "====", - SimpleFmt2.__doc__, - "", - "", - "*********", - "something", - "*********", - "fmt3", - "====", - SimpleFmt3.__doc__, - ] - ), - ) - - def test_show_summaries(self): - """Test the :meth:`psyplot.plotter.Plotter.show_summaries` method""" - plotter = TestPlotter(xr.DataArray([])) - s = plotter.show_summaries(func=str) - self.assertEqual( - s, - "\n".join( - [ - "fmt1", - indent(SimpleFmt.__doc__.splitlines()[0], " "), - "fmt2", - indent(SimpleFmt2.__doc__.splitlines()[0], " "), - "fmt3", - indent(SimpleFmt3.__doc__.splitlines()[0], " "), - "post", - indent( - psyp.PostProcessing.__doc__.splitlines()[0], " " - ), - "post_timing", - indent(psyp.PostTiming.__doc__.splitlines()[0], " "), - ] - ), - ) - s = plotter.show_summaries( - ["fmt1", "fmt2", "fmt3"], func=str, grouped=True - ) - title = psyp.groups["labels"] - self.assertEqual( - s, - "\n".join( - [ - "*" * len(title), - title, - "*" * len(title), - "fmt1", - indent(SimpleFmt.__doc__.splitlines()[0], " "), - "fmt2", - indent(SimpleFmt2.__doc__.splitlines()[0], " "), - "", - "*********", - "something", - "*********", - "fmt3", - indent(SimpleFmt3.__doc__.splitlines()[0], " "), - ] - ), - ) - - def test_has_changed(self): - """Test the :meth:`psyplot.plotter.Plotter.show_summaries` method""" - plotter = TestPlotter(xr.DataArray([]), fmt1="something") - self.assertEqual(plotter["fmt1"], "something") - for i in range(1, 4): - key = "fmt%i" % i - fmto = getattr(plotter, key) - self.assertEqual( - plotter.has_changed(key), - [fmto.default, plotter[key]], - msg="Wrong value for " + key, - ) - plotter.update() - self.assertIsNone(plotter.has_changed("fmt1")) - plotter.update(fmt1="test", fmt3=plotter.fmt3.default, force=True) - self.assertEqual(plotter.has_changed("fmt1"), ["something", "test"]) - self.assertIsNone(plotter.has_changed("fmt2")) - self.assertIsNone(plotter.has_changed("fmt3", include_last=False)) - self.assertEqual( - plotter.has_changed("fmt3"), - [plotter.fmt3.default, plotter.fmt3.default], - ) - - def test_insert_additionals(self): - """Test whether the right formatoptions are inserted""" - depend = 0 - - class StartFormatoption(TestFormatoption): - priority = psyp.START - - class BeforePlottingFmt(TestFormatoption): - priority = psyp.BEFOREPLOTTING - - class PlotFmt(TestFormatoption): - priority = psyp.BEFOREPLOTTING - plot_fmt = True - - def make_plot(self): - results["plot_made"] = True - - class DataDependentFmt(TestFormatoption): - priority = psyp.START - - def data_dependent(self, *args, **kwargs): - return bool(depend) - - class DataDependentFmt2(TestFormatoption): - data_dependent = True - - class ThisTestPlotter(TestPlotter): - fmt_start = StartFormatoption("fmt_start") - fmt_plot = PlotFmt("fmt_plot") - fmt_plot1 = BeforePlottingFmt("fmt_plot1") - fmt_plot2 = BeforePlottingFmt("fmt_plot2") - fmt_data1 = DataDependentFmt("fmt_data1") - fmt_data2 = DataDependentFmt2("fmt_data2") - - def key_name(key): - return "%s.%s" % (aname, key) - - plotter = ThisTestPlotter(xr.DataArray([])) - for key in set(plotter) - {"post", "post_timing"}: - plotter[key] = 999 - aname = plotter.data.psy.arr_name - results.clear() - - # test whether everything is updated - plotter.update(fmt_start=1) - self.assertTrue(results.pop("plot_made")) - self.assertEqual( - list(results), - [ - key_name("fmt_start"), - key_name("fmt_plot"), - key_name("fmt_data2"), - ], - ) - - results.clear() - - # test whether the plot is updated - plotter.update(fmt_plot1=1) - self.assertEqual(list(results), [key_name("fmt_plot1"), "plot_made"]) - - results.clear() - - # test whether the data dependent formatoptions are updated - plotter.update(replot=True) - self.assertTrue(results.pop("plot_made")) - self.assertEqual( - list(results), [key_name("fmt_plot"), key_name("fmt_data2")] - ) - - results.clear() - depend = 1 - plotter.update(replot=True) - self.assertTrue(results.pop("plot_made")) - self.assertEqual( - sorted(list(results)), - sorted( - [ - key_name("fmt_plot"), - key_name("fmt_data1"), - key_name("fmt_data2"), - ] - ), - ) - - def test_reinit(self): - """Test the reinitialization of a plotter""" - - class ClearingFormatoption(SimpleFmt): - def remove(self): - results["removed"] = True - - requires_clearing = True - - class AnotherFormatoption(SimpleFmt): - def remove(self): - results["removed2"] = True - - class ThisTestPlotter(TestPlotter): - fmt_clear = ClearingFormatoption("fmt_clear") - fmt_remove = AnotherFormatoption("fmt_remove") - - import matplotlib.pyplot as plt - - ax = plt.axes(label="new axis") - ax.plot([6, 7]) - - plotter = ThisTestPlotter() - keys = list(set(plotter) - {"post", "post_timing"}) - plotter = ThisTestPlotter( - xr.DataArray([]), ax=ax, **dict(zip(keys, repeat(1))) - ) - - self.assertNotIn("removed", results) - self.assertNotIn("removed2", results) - arr_name = plotter.data.psy.arr_name - for key in keys: - self.assertIn("%s.%s" % (arr_name, key), results) - self.assertTrue(ax.lines) # axes should not be cleared - - results.clear() - - plotter.reinit() - self.assertIn("removed", results) - self.assertIn("removed2", results) - for key in keys: - self.assertIn("%s.%s" % (arr_name, key), results) - self.assertFalse(ax.lines) # axes should be cleared - - results.clear() - - ax.plot([6, 7]) - keys.remove("fmt_clear") - keys.remove("fmt_remove") - plotter = TestPlotter( - xr.DataArray([]), ax=ax, **dict(zip(keys, repeat(1))) - ) - for key in keys: - self.assertIn("%s.%s" % (arr_name, key), results) - self.assertTrue(ax.lines) # axes should not be cleared - - results.clear() - - plotter.reinit() - for key in keys: - self.assertIn("%s.%s" % (arr_name, key), results) - self.assertTrue(ax.lines) # axes should not be cleared - - def test_check_data(self): - """Tests the :meth:`psyplot.plotter.Plotter.check_data` method""" - self.assertEqual( - TestPlotter.check_data("test", ("dim1",), True), ([True], [""]) - ) - checks, messages = TestPlotter.check_data( - ["test1", "test2"], [("dim1",)], [False, False] - ) - self.assertEqual(checks, [False, False]) - self.assertIn("not the same", messages[0]) - self.assertIn("not the same", messages[1]) - - -class FormatoptionTest(unittest.TestCase): - """A class to test the :class:`psyplot.plotter.Formatoption` class""" - - def setUp(self): - results.clear() - rcParams.defaultParams = rcParams.defaultParams.copy() - - def tearDown(self): - results.clear() - rcParams.clear() - rcParams.defaultParams = psyc.rcsetup.defaultParams - rcParams.update_from_defaultParams() - - def test_data(self): - """Test the :attr:`psyplot.plotter.Formatoption.data` attribute""" - - class OtherTestPlotter(TestPlotter): - fmt4 = SimpleFmt3("fmt4", index_in_list=2) - - raw_data = psyd.InteractiveList([xr.DataArray([]) for _ in range(4)]) - plotter = OtherTestPlotter(raw_data) - plotter.plot_data = plot_data = plotter.data.copy(True) - copied = plotter.plot_data[2].copy() - # -- test the getters - self.assertIs(plotter.fmt1.data, plotter.plot_data) - self.assertIsNot(plotter.fmt1.data, plotter.data) - self.assertIs(plotter.fmt1.raw_data, plotter.data) - self.assertIsNot(plotter.fmt1.raw_data, plotter.plot_data) - # -- test the setters - plotter.fmt4.data = copied - self.assertIs(plotter.plot_data[2], copied) - self.assertIsNot(plotter.data[2], copied) - self.assertIs(plotter.plot_data[1], plot_data[1]) - - plotter.fmt3.data = raw_data - for i, arr in enumerate(plotter.plot_data): - self.assertIs( - arr, raw_data[i], msg="Wrong array at position %i" % i - ) - # undo the setting - plotter.fmt3.data = plot_data - - # -- test iteration over data - # plot data - it_data = plotter.fmt3.iter_data - for i, arr in enumerate(plotter.fmt3.data): - self.assertIs( - next(it_data), arr, msg="Wrong array at position %i" % i - ) - # raw data - it_data = plotter.fmt3.iter_raw_data - for i, arr in enumerate(plotter.fmt3.raw_data): - self.assertIs( - next(it_data), - arr, - msg="Wrong raw data array at position %i" % i, - ) - self.assertIs(next(plotter.fmt4.iter_data), plot_data[2]) - self.assertIs(next(plotter.fmt4.iter_raw_data), raw_data[2]) - - def test_rc(self): - """Test whether the rcParams are interpreted correctly""" - checks = [] - - def validate(val): - checks.append(val) - return val - - def validate_false(val): - if val == 4: - raise ValueError("Expected ValueError") - return val - - try: - - class RcTestPlotter(TestPlotter): - _rcparams_string = ["plotter.test.data."] - - # delete the validation - del TestFormatoption._validate - rcParams.defaultParams["plotter.test.data.fmt1"] = (1, validate) - rcParams.defaultParams["plotter.test.data.fmt3"] = ( - 3, - validate_false, - ) - rcParams.update_from_defaultParams() - plotter = RcTestPlotter(xr.DataArray([])) - - self.assertEqual( - checks, [1], msg="Validation function has not been called!" - ) - # test general functionality - self.assertEqual( - plotter.fmt1.default_key, "plotter.test.data.fmt1" - ) - self.assertEqual( - plotter.fmt3.default_key, "plotter.test.data.fmt3" - ) - if six.PY3: - with self.assertRaisesRegex(KeyError, "fmt2"): - plotter.fmt2.default_key - self.assertEqual(plotter["fmt1"], 1) - self.assertEqual(plotter.fmt1.default, 1) - self.assertFalse(plotter.fmt2.value) - self.assertIs(plotter.fmt1.validate, validate) - # test after update - plotter.update(fmt1=8) - self.assertEqual( - checks, [1, 8], msg="Validation function has not been called!" - ) - self.assertEqual(plotter.fmt1.value, 8) - self.assertEqual(plotter["fmt1"], 8) - self.assertEqual(plotter.fmt1.default, 1) - # test false validation - if six.PY3: - with self.assertWarnsRegex( - RuntimeWarning, "Could not find a validation " "function" - ): - plotter.fmt2.validate - with self.assertRaisesRegex(ValueError, "Expected ValueError"): - plotter.update(fmt3=4) - plotter.update(fmt3=3) - plotter.fmt2.validate = validate - plotter.update(fmt2=9) - self.assertEqual(checks, [1, 8, 9]) - self.assertEqual(plotter.fmt2.value, 9) - except Exception: - raise - finally: - TestFormatoption._validate = str - - def test_groupname(self): - if not six.PY2: - with self.assertWarnsRegex( - RuntimeWarning, "Unknown formatoption group" - ): - self.assertEqual(TestPlotter.fmt3.groupname, "something") - self.assertEqual(TestPlotter.fmt1.groupname, psyp.groups["labels"]) - - -class TestDictFormatoption(unittest.TestCase): - """Test the :class:`psyplot.plotter.DictFormatoption` class""" - - def test_update(self): - class TestDictFormatoption(psyp.DictFormatoption): - @property - def default(self): - try: - return super(TestDictFormatoption, self).default - except KeyError: - return {} - - _validate = psyp._identity - - def update(self, value): - pass - - class ThisTestPlotter(TestPlotter): - fmt4 = TestDictFormatoption("fmt4") - - plotter = ThisTestPlotter(xr.DataArray([])) - - self.assertEqual(plotter.fmt4.value, {}) - # perform 2 updates which each should be considered - plotter.update(fmt4=dict(a=1)) - plotter.update(fmt4=dict(b=2)) - self.assertEqual(plotter.fmt4.value, dict(a=1, b=2)) - - # update to default - plotter.update(fmt4=dict(a=3), todefault=True) - self.assertEqual(plotter.fmt4.value, dict(a=3)) - - # clear the value - plotter.update(fmt4=None) - self.assertEqual(plotter.fmt4.value, {}) - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/test_plugin/psyplot_test/__init__.py b/tests/test_plugin/psyplot_test/__init__.py deleted file mode 100644 index d4af897..0000000 --- a/tests/test_plugin/psyplot_test/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -"""Dummy psyplot plugin test.""" - -# SPDX-FileCopyrightText: 2016-2024 University of Lausanne -# SPDX-FileCopyrightText: 2020-2021 Helmholtz-Zentrum Geesthacht - -# SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH -# -# SPDX-License-Identifier: LGPL-3.0-only diff --git a/tests/test_plugin/psyplot_test/plotter.py b/tests/test_plugin/psyplot_test/plotter.py deleted file mode 100644 index bc93a7a..0000000 --- a/tests/test_plugin/psyplot_test/plotter.py +++ /dev/null @@ -1,26 +0,0 @@ -# Test module that defines a plotter -# -# The plotter in this module has been registered by the rcParams in the plugin -# package - -# SPDX-FileCopyrightText: 2016-2024 University of Lausanne -# SPDX-FileCopyrightText: 2020-2021 Helmholtz-Zentrum Geesthacht - -# SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH -# -# SPDX-License-Identifier: LGPL-3.0-only - -from psyplot.plotter import Formatoption, Plotter - - -class TestFmt(Formatoption): - """Some documentation""" - - default = None - - def update(self, value): - pass - - -class TestPlotter(Plotter): - fmt1 = TestFmt("fmt1") diff --git a/tests/test_plugin/psyplot_test/plugin.py b/tests/test_plugin/psyplot_test/plugin.py deleted file mode 100644 index c88e40e..0000000 --- a/tests/test_plugin/psyplot_test/plugin.py +++ /dev/null @@ -1,44 +0,0 @@ -"""Dummy plugin file.""" - -# SPDX-FileCopyrightText: 2016-2024 University of Lausanne -# SPDX-FileCopyrightText: 2020-2021 Helmholtz-Zentrum Geesthacht - -# SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH -# -# SPDX-License-Identifier: LGPL-3.0-only - -from psyplot.config.rcsetup import RcParams, validate_dict - -plugin_version = "1.0.0" - - -rcParams = RcParams( - defaultParams={ - "test": [1, lambda i: int(i)], - "project.plotters": [ - { - "test_plotter": { - "module": "psyplot_test.plotter", - "plotter_name": "TestPlotter", - "import_plotter": True, - } - }, - validate_dict, - ], - } -) -rcParams.update_from_defaultParams() - - -patch_check = [] - -checking_patch = False - - -def test_patch(plotter_d, versions): - if not checking_patch: - raise ValueError("Accidently applied the patch!") - patch_check.append({"plotter": plotter_d, "versions": versions}) - - -patches = {("psyplot_test.plotter", "TestPlotter"): test_patch} diff --git a/tests/test_plugin/setup.py b/tests/test_plugin/setup.py deleted file mode 100644 index 62e821c..0000000 --- a/tests/test_plugin/setup.py +++ /dev/null @@ -1,22 +0,0 @@ -from setuptools import find_packages, setup - -# SPDX-FileCopyrightText: 2016-2024 University of Lausanne -# SPDX-FileCopyrightText: 2020-2021 Helmholtz-Zentrum Geesthacht - -# SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH -# -# SPDX-License-Identifier: LGPL-3.0-only - -setup( - name="psyplot_test", - version="1.0.0", - license="GPLv2", - packages=find_packages(exclude=["docs", "tests*", "examples"]), - entry_points={ - "psyplot": [ - "plugin=psyplot_test.plugin", - "patches=psyplot_test.plugin:patches", - ] - }, - zip_safe=False, -) diff --git a/tests/test_project.py b/tests/test_project.py deleted file mode 100644 index ec6ed21..0000000 --- a/tests/test_project.py +++ /dev/null @@ -1,2197 +0,0 @@ -"""Test module of the :mod:`psyplot.project` module.""" - -# SPDX-FileCopyrightText: 2016-2024 University of Lausanne -# SPDX-FileCopyrightText: 2020-2021 Helmholtz-Zentrum Geesthacht - -# SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH -# -# SPDX-License-Identifier: LGPL-3.0-only - -import os -import os.path as osp -import shutil -import unittest -from itertools import chain - -import _base_testing as bt -import matplotlib.pyplot as plt -import pytest -import six -import test_data as td -import test_plotter as tp -import xarray as xr -import yaml - -import psyplot.data as psyd -import psyplot.plotter as psyp -import psyplot.project as psy -from psyplot.config.rcsetup import get_configdir - -try: - from cdo import Cdo - - Cdo() -except Exception: - with_cdo = False -else: - with_cdo = True - -remove_temp_files = True - - -def get_col_num(ax): - try: - return ax.get_subplotspec().colspan.start - except AttributeError: - return ax.colNum - - -def get_row_num(ax): - try: - return ax.get_subplotspec().rowspan.start - except AttributeError: - return ax.rowNum - - -@pytest.fixture -def project(): - try: - psy.register_plotter( - "test_plotter", - import_plotter=True, - module="test_plotter", - plotter_name="TestPlotter", - ) - except ValueError: - pass - yield psy.Project() - for identifier in list(psy.registered_plotters): - psy.unregister_plotter(identifier) - - -@pytest.mark.parametrize( - "preset,path", - [ - ("test", osp.join(get_configdir(), "presets", "test.yml")), - ("test.yml", osp.join(get_configdir(), "presets", "test.yml")), - ("test.yml", "test.yml"), - ], -) -def test_load_preset(project, preset, path): - if osp.dirname(path): - os.makedirs(osp.dirname(path), exist_ok=True) - with open(path, "w") as f: - yaml.dump({"fmt1": "test", "fmt2": "this should be ignored"}, f) - try: - sp = project.plot.test_plotter(xr.Dataset({"x": (("a"), [1])})) - sp.load_preset(preset) - plotter = sp.plotters[0] - assert plotter.fmt1.value == "test" - finally: - os.remove(path) - - -def test_safe_load_preset(project, tmpdir): - import matplotlib.pyplot as plt - - preset = osp.join(tmpdir, "test.yml") - with open(preset, "w") as f: - yaml.dump({"cmap": plt.get_cmap("Reds")}, f) - - with pytest.raises(yaml.constructor.ConstructorError): - project.load_preset(preset) - - with psy.rcParams.catch(): - psy.rcParams["presets.trusted"].append(preset) - project.load_preset(preset) - - -def test_extract_preset(project): - preset = { - "fmt1": "test1", - "test_plotter": {"fmt2": "test2"}, - "not_existent": 1, - } - fmts = project.extract_fmts_from_preset(preset, "test_plotter") - assert fmts == {"fmt1": "test1", "fmt2": "test2"} - - -def test_save_preset(project): - sp = project.plot.test_plotter( - xr.Dataset({"x": (("a"), [1])}), name=["x", "x"] - ) - assert sp.save_preset() == {} - assert ( - sp.save_preset(include_defaults=True)["fmt1"] == sp.plotters[0]["fmt1"] - ) - - sp[1].psy.update(fmt1="changed") - assert sp.save_preset() == {} - - sp[0].psy.update(fmt1="changed") - assert sp.save_preset() == {"fmt1": "changed"} - - -class TestProject(td.TestArrayList): - """Testclass for the :class:`psyplot.project.Project` class""" - - _created_files = set() - - def setUp(self): - for identifier in list(psy.registered_plotters): - psy.unregister_plotter(identifier) - psy.close("all") - plt.close("all") - self._created_files = set() - - def tearDown(self): - for identifier in list(psy.registered_plotters): - psy.unregister_plotter(identifier) - psy.close("all") - plt.close("all") - tp.results.clear() - if remove_temp_files: - for f in self._created_files: - if osp.exists(f) and osp.isdir(f): - shutil.rmtree(f) - elif osp.exists(f): - try: - os.remove(f) - except Exception: - pass - self._created_files.clear() - - def test_with(self): - """Test __enter__ and __exit__ methods""" - psy.register_plotter( - "test_plotter", - import_plotter=True, - module="test_plotter", - plotter_name="TestPlotter", - ) - self.assertFalse(psy.gcp(True)) - self.assertFalse(psy.gcp()) - with psy.plot.test_plotter(bt.get_file("test-t2m-u-v.nc")): - self.assertTrue(psy.gcp(True)) - self.assertTrue(psy.gcp()) - - self.assertFalse(psy.gcp(True)) - self.assertFalse(psy.gcp()) - - def test_save_and_load_01_simple(self): - """Test the saving and loading of a Project""" - psy.register_plotter( - "test_plotter", - import_plotter=True, - module="test_plotter", - plotter_name="TestPlotter", - ) - ds = psy.open_dataset(bt.get_file("test-t2m-u-v.nc")) - plt.close("all") - sp = psy.plot.test_plotter( - ds, name=["t2m", "u"], x=0, y=4, ax=(2, 2, 1), fmt1="test" - ) - self.assertEqual(len(sp), 2) - self.assertEqual(sp[0].psy.ax.get_figure().number, 1) - self.assertEqual(get_row_num(sp[0].psy.ax), 0) - self.assertEqual(get_col_num(sp[0].psy.ax), 0) - - gs = sp[0].psy.ax.get_gridspec() - - self.assertEqual(gs.ncols, 2) - self.assertEqual(gs.nrows, 2) - self.assertEqual(sp[1].psy.ax.get_figure().number, 2) - self.assertEqual(get_row_num(sp[1].psy.ax), 0) - self.assertEqual(get_col_num(sp[1].psy.ax), 0) - - gs = sp[1].psy.ax.get_gridspec() - - self.assertEqual(gs.ncols, 2) - self.assertEqual(gs.nrows, 2) - arr_names = sp.arr_names - self.assertEqual(tp.results[arr_names[0] + ".fmt1"], "test") - self.assertEqual(tp.results[arr_names[1] + ".fmt1"], "test") - fname = "test.pkl" - self._created_files.add(fname) - sp.save_project(fname) - psy.close() - tp.results.clear() - sp = psy.Project.load_project(fname) - self.assertEqual(len(sp), 2) - self.assertEqual(sp[0].psy.ax.get_figure().number, 1) - self.assertEqual(get_row_num(sp[0].psy.ax), 0) - self.assertEqual(get_col_num(sp[0].psy.ax), 0) - - gs = sp[0].psy.ax.get_gridspec() - - self.assertEqual(gs.ncols, 2) - self.assertEqual(gs.nrows, 2) - self.assertEqual(sp[1].psy.ax.get_figure().number, 2) - self.assertEqual(get_row_num(sp[1].psy.ax), 0) - self.assertEqual(get_col_num(sp[1].psy.ax), 0) - - gs = sp[1].psy.ax.get_gridspec() - - self.assertEqual(gs.ncols, 2) - self.assertEqual(gs.nrows, 2) - - def test_save_and_load_02_alternative_axes(self): - """Test the saving and loading of a Project providing alternative axes""" - psy.register_plotter( - "test_plotter", - import_plotter=True, - module="test_plotter", - plotter_name="TestPlotter", - ) - ds = psy.open_dataset(bt.get_file("test-t2m-u-v.nc")) - plt.close("all") - sp = psy.plot.test_plotter( - ds, name=["t2m", "u"], x=0, y=4, ax=(2, 2, 1), fmt1="test" - ) - self.assertEqual(len(sp), 2) - self.assertEqual(sp[0].psy.ax.get_figure().number, 1) - self.assertEqual(get_row_num(sp[0].psy.ax), 0) - self.assertEqual(get_col_num(sp[0].psy.ax), 0) - - gs = sp[0].psy.ax.get_gridspec() - - self.assertEqual(gs.ncols, 2) - self.assertEqual(gs.nrows, 2) - self.assertEqual(sp[1].psy.ax.get_figure().number, 2) - self.assertEqual(get_row_num(sp[1].psy.ax), 0) - self.assertEqual(get_col_num(sp[1].psy.ax), 0) - - gs = sp[1].psy.ax.get_gridspec() - - self.assertEqual(gs.ncols, 2) - self.assertEqual(gs.nrows, 2) - arr_names = sp.arr_names - self.assertEqual(tp.results[arr_names[0] + ".fmt1"], "test") - self.assertEqual(tp.results[arr_names[1] + ".fmt1"], "test") - fname = "test.pkl" - self._created_files.add(fname) - sp.save_project(fname) - psy.close() - tp.results.clear() - fig, axes = plt.subplots(1, 2) - sp = psy.Project.load_project(fname, alternative_axes=axes.ravel()) - self.assertEqual(len(sp), 2) - self.assertEqual(sp[0].psy.ax.get_figure().number, 1) - self.assertEqual(get_row_num(sp[0].psy.ax), 0) - self.assertEqual(get_col_num(sp[0].psy.ax), 0) - - gs = sp[0].psy.ax.get_gridspec() - - self.assertEqual(gs.ncols, 2) - self.assertEqual(gs.nrows, 1) - self.assertEqual(sp[1].psy.ax.get_figure().number, 1) - self.assertEqual(get_row_num(sp[1].psy.ax), 0) - self.assertEqual(get_col_num(sp[1].psy.ax), 1) - - gs = sp[1].psy.ax.get_gridspec() - - self.assertEqual(gs.ncols, 2) - self.assertEqual(gs.nrows, 1) - - def test_save_and_load_03_alternative_ds(self): - """Test the saving and loading of a Project providing alternative axes""" - psy.register_plotter( - "test_plotter", - import_plotter=True, - module="test_plotter", - plotter_name="TestPlotter", - ) - ds = psy.open_dataset(bt.get_file("test-t2m-u-v.nc")) - plt.close("all") - sp = psy.plot.test_plotter( - ds, name=["t2m", "u"], x=0, y=4, ax=(2, 2, 1), fmt1="test" - ) - self.assertEqual(len(sp), 2) - self.assertEqual(sp[0].psy.ax.get_figure().number, 1) - self.assertEqual(get_row_num(sp[0].psy.ax), 0) - self.assertEqual(get_col_num(sp[0].psy.ax), 0) - - gs = sp[0].psy.ax.get_gridspec() - - self.assertEqual(gs.ncols, 2) - self.assertEqual(gs.nrows, 2) - self.assertEqual(sp[1].psy.ax.get_figure().number, 2) - self.assertEqual(get_row_num(sp[1].psy.ax), 0) - self.assertEqual(get_col_num(sp[1].psy.ax), 0) - - gs = sp[1].psy.ax.get_gridspec() - - self.assertEqual(gs.ncols, 2) - self.assertEqual(gs.nrows, 2) - arr_names = sp.arr_names - self.assertEqual(tp.results[arr_names[0] + ".fmt1"], "test") - self.assertEqual(tp.results[arr_names[1] + ".fmt1"], "test") - fname = "test.pkl" - self._created_files.add(fname) - sp.save_project(fname) - psy.close() - tp.results.clear() - fig, axes = plt.subplots(1, 2) - ds = psy.open_dataset(bt.get_file("circumpolar_test.nc")) - sp = psy.Project.load_project(fname, datasets=[ds], new_fig=False) - self.assertEqual(len(sp), 2) - self.assertEqual(sp[0].psy.ax.get_figure().number, 1) - self.assertEqual(get_row_num(sp[0].psy.ax), 0) - self.assertEqual(get_col_num(sp[0].psy.ax), 0) - - gs = sp[0].psy.ax.get_gridspec() - - self.assertEqual(gs.ncols, 2) - self.assertEqual(gs.nrows, 2) - self.assertEqual(sp[1].psy.ax.get_figure().number, 2) - self.assertEqual(get_row_num(sp[1].psy.ax), 0) - self.assertEqual(get_col_num(sp[1].psy.ax), 0) - - gs = sp[1].psy.ax.get_gridspec() - - self.assertEqual(gs.ncols, 2) - self.assertEqual(gs.nrows, 2) - self.assertIs(sp[0].psy.base, ds) - self.assertIs(sp[1].psy.base, ds) - - def test_save_and_load_04_alternative_fname(self): - """Test the saving and loading of a Project providing alternative axes""" - psy.register_plotter( - "test_plotter", - import_plotter=True, - module="test_plotter", - plotter_name="TestPlotter", - ) - ds = psy.open_dataset(bt.get_file("test-t2m-u-v.nc")) - plt.close("all") - sp = psy.plot.test_plotter( - ds, name=["t2m", "u"], x=0, y=4, ax=(2, 2, 1), fmt1="test" - ) - self.assertEqual(len(sp), 2) - self.assertEqual(sp[0].psy.ax.get_figure().number, 1) - self.assertEqual(get_row_num(sp[0].psy.ax), 0) - self.assertEqual(get_col_num(sp[0].psy.ax), 0) - - gs = sp[0].psy.ax.get_gridspec() - - self.assertEqual(gs.ncols, 2) - self.assertEqual(gs.nrows, 2) - self.assertEqual(sp[1].psy.ax.get_figure().number, 2) - self.assertEqual(get_row_num(sp[1].psy.ax), 0) - self.assertEqual(get_col_num(sp[1].psy.ax), 0) - - gs = sp[1].psy.ax.get_gridspec() - - self.assertEqual(gs.ncols, 2) - self.assertEqual(gs.nrows, 2) - arr_names = sp.arr_names - self.assertEqual(tp.results[arr_names[0] + ".fmt1"], "test") - self.assertEqual(tp.results[arr_names[1] + ".fmt1"], "test") - fname = "test.pkl" - self._created_files.add(fname) - sp.save_project(fname) - psy.close() - tp.results.clear() - fig, axes = plt.subplots(1, 2) - sp = psy.Project.load_project( - fname, - alternative_paths=[bt.get_file("circumpolar_test.nc")], - new_fig=False, - ) - self.assertEqual(len(sp), 2) - self.assertEqual(sp[0].psy.ax.get_figure().number, 1) - self.assertEqual(get_row_num(sp[0].psy.ax), 0) - self.assertEqual(get_col_num(sp[0].psy.ax), 0) - - gs = sp[0].psy.ax.get_gridspec() - - self.assertEqual(gs.ncols, 2) - self.assertEqual(gs.nrows, 2) - self.assertEqual(sp[1].psy.ax.get_figure().number, 2) - self.assertEqual(get_row_num(sp[1].psy.ax), 0) - self.assertEqual(get_col_num(sp[1].psy.ax), 0) - - gs = sp[1].psy.ax.get_gridspec() - - self.assertEqual(gs.ncols, 2) - self.assertEqual(gs.nrows, 2) - self.assertEqual( - psyd.get_filename_ds(sp[0].psy.base)[0], - bt.get_file("circumpolar_test.nc"), - ) - self.assertEqual( - psyd.get_filename_ds(sp[1].psy.base)[0], - bt.get_file("circumpolar_test.nc"), - ) - - def test_save_and_load_05_pack(self): - import tempfile - - psy.register_plotter( - "test_plotter", - import_plotter=True, - module="test_plotter", - plotter_name="TestPlotter", - ) - tempdir1 = tempfile.mkdtemp(prefix="psyplot_test_") - tempdir2 = tempfile.mkdtemp(prefix="psyplot_test_") - tempdir3 = tempfile.mkdtemp(prefix="psyplot_test_") - outdir = tempfile.mkdtemp(prefix="psyplot_test_") - self._created_files.update([tempdir1, tempdir2, tempdir3, outdir]) - # first test file - shutil.copyfile( - bt.get_file("test-t2m-u-v.nc"), - osp.join(tempdir1, "test-t2m-u-v.nc"), - ) - psy.plot.test_plotter( - osp.join(tempdir1, "test-t2m-u-v.nc"), name="t2m", t=[1, 2] - ) - # second test file - shutil.copyfile( - bt.get_file("test-t2m-u-v.nc"), - osp.join(tempdir2, "test-t2m-u-v.nc"), - ) - psy.plot.test_plotter( - osp.join(tempdir2, "test-t2m-u-v.nc"), name="t2m", t=[3, 4] - ) - # third test file - shutil.copyfile( - bt.get_file("test-t2m-u-v.nc"), - osp.join(tempdir3, "test-t2m-u-v.nc"), - ) - psy.plot.test_plotter( - osp.join(tempdir3, "test-t2m-u-v.nc"), name="t2m", t=[3, 4] - ) - # fourth test file with different name - psy.plot.test_plotter( - bt.get_file("circumpolar_test.nc"), name="t2m", t=[0, 1] - ) - mp = psy.gcp(True) - - mp.save_project(osp.join(outdir, "test.pkl"), pack=True) - files = { - "test-t2m-u-v.nc", - "test-t2m-u-v-1.nc", - "test-t2m-u-v-2.nc", - "test.pkl", - "circumpolar_test.nc", - } - self.assertEqual(set(os.listdir(outdir)), files) - - psy.close(mp) - - # move the directory to check whether it is still working - outdir2 = tempfile.mkdtemp(prefix="psyplot_test_") - self._created_files.add(outdir2) - for f in files: - shutil.move(osp.join(outdir, f), osp.join(outdir2, f)) - mp = psy.Project.load_project( - osp.join(outdir2, "test.pkl"), - main=True, - ) - - self.assertEqual(len(mp), 8, msg=mp) - - paths = { - osp.join(outdir2, "test-t2m-u-v.nc"), - osp.join(outdir2, "test-t2m-u-v-1.nc"), - osp.join(outdir2, "test-t2m-u-v-2.nc"), - } - found = set() - - for i in range(6): - found.add(psyd.get_filename_ds(mp[i].psy.base)[0]) - self.assertFalse( - paths - found, - msg="expected %s\n%s\nfound %s" % (paths, "-" * 80, found), - ) - self.assertEqual( - psyd.get_filename_ds(mp[6].psy.base)[0], - osp.join(outdir2, "circumpolar_test.nc"), - ) - self.assertEqual( - psyd.get_filename_ds(mp[7].psy.base)[0], - osp.join(outdir2, "circumpolar_test.nc"), - ) - - def test_save_and_load_06_post_fmt(self): - """Test whether the :attr:`psyplot.plotter.Plotter.post` fmt works""" - psy.register_plotter( - "test_plotter", - import_plotter=True, - module="test_plotter", - plotter_name="TestPlotter", - ) - ds = psy.open_dataset(bt.get_file("test-t2m-u-v.nc")) - plt.close("all") - sp = psy.plot.test_plotter( - ds, - name=["t2m", "u"], - x=0, - y=4, - ax=(2, 2, 1), - fmt1="test", - post='self.ax.set_title("test")', - ) - self.assertEqual(sp.plotters[0].ax.get_title(), "test") - fname = "test.pkl" - self._created_files.add(fname) - sp.save_project(fname) - psy.close("all") - # test without enabled post - sp = psy.Project.load_project(fname) - self.assertEqual(sp.plotters[0].ax.get_title(), "") - psy.close("all") - # test with enabled post - sp = psy.Project.load_project(fname, enable_post=True) - self.assertEqual(sp.plotters[0].ax.get_title(), "test") - - def test_save_and_load_07_sharedx(self): - """Test whether shared x- and y-axis are restored correctly""" - psy.register_plotter( - "test_plotter", - import_plotter=True, - module="test_plotter", - plotter_name="TestPlotter", - ) - ds = psy.open_dataset(bt.get_file("test-t2m-u-v.nc")) - plt.close("all") - fig, axes = plt.subplots(1, 3, sharex=True) - sp = psy.plot.test_plotter( - ds, name=["t2m", "u", "v"], x=0, y=4, ax=axes - ) - axes[0].set_xlim(5, 10) - self.assertEqual(list(axes[1].get_xlim()), [5, 10]) - # save the project - fname = "test.pkl" - self._created_files.add(fname) - sp.save_project(fname) - psy.close("all") - - # load the project - sp = psy.Project.load_project(fname) - self.assertEqual(len(sp.axes), 3, msg=sp.axes) - sp[0].psy.ax.set_xlim(10, 15) - self.assertEqual(list(sp[1].psy.ax.get_xlim()), [10, 15]) - - # now we test, if it still works, if we remove the source axes - names2use = sp.arr_names[1:] - psy.close("all") - sp = psy.Project.load_project(fname, only=names2use) - self.assertEqual(len(sp.axes), 2, msg=sp.axes) - sp[0].psy.ax.set_xlim(10, 15) - self.assertEqual(list(sp[1].psy.ax.get_xlim()), [10, 15]) - - def test_save_and_load_08_sharedy(self): - """Test whether shared x- and y-axis are restored correctly""" - psy.register_plotter( - "test_plotter", - import_plotter=True, - module="test_plotter", - plotter_name="TestPlotter", - ) - ds = psy.open_dataset(bt.get_file("test-t2m-u-v.nc")) - plt.close("all") - fig, axes = plt.subplots(1, 3, sharey=True) - sp = psy.plot.test_plotter( - ds, name=["t2m", "u", "v"], x=0, y=4, ax=axes - ) - axes[0].set_ylim(5, 10) - self.assertEqual(list(axes[1].get_ylim()), [5, 10]) - # save the project - fname = "test.pkl" - self._created_files.add(fname) - sp.save_project(fname) - psy.close("all") - - # load the project - sp = psy.Project.load_project(fname) - self.assertEqual(len(sp.axes), 3, msg=sp.axes) - sp[0].psy.ax.set_ylim(10, 15) - self.assertEqual(list(sp[1].psy.ax.get_ylim()), [10, 15]) - - # now we test, if it still works, if we remove the source axes - names2use = sp.arr_names[1:] - psy.close("all") - sp = psy.Project.load_project(fname, only=names2use) - self.assertEqual(len(sp.axes), 2, msg=sp.axes) - sp[0].psy.ax.set_ylim(10, 15) - self.assertEqual(list(sp[1].psy.ax.get_ylim()), [10, 15]) - - def test_versions_and_patch(self): - import warnings - - try: - import psyplot_test.plugin as test_plugin - except ImportError: - self.skipTest("Could not import the psyplot_test package") - return - rc = psyd.rcParams - with warnings.catch_warnings(): - warnings.simplefilter("ignore") - rc.load_plugins() - psy._versions.clear() - psy.register_plotter( - "test_plotter", **rc["project.plotters"]["test_plotter"] - ) - psy.register_plotter( - "test_plotter2", - import_plotter=True, - module="test_plotter", - plotter_name="TestPlotter", - ) - - psy.plot.test_plotter( - bt.get_file("test-t2m-u-v.nc"), name="t2m", t=[1, 2] - ) - psy.plot.test_plotter2( - bt.get_file("test-t2m-u-v.nc"), name="t2m", t=[1, 2] - ) - mp = psy.gcp(True) - self.assertEqual(len(mp), 4, msg=mp) - d = mp.save_project() - self.assertIn("versions", d) - self.assertEqual(len(d["versions"]), 2, msg=d["versions"]) - self.assertIn("psyplot", d["versions"]) - self.assertIn("psyplot_test.plugin", d["versions"]) - self.assertEqual( - d["versions"]["psyplot_test.plugin"]["version"], "1.0.0" - ) - - # test the patch - self.assertEqual(test_plugin.patch_check, []) - test_plugin.checking_patch = True - try: - mp.close(True, True, True) - mp = psy.Project.load_project(d) - self.assertEqual(len(test_plugin.patch_check), 2) - self.assertIs( - test_plugin.patch_check[0]["plotter"], - d["arrays"]["arr0"]["plotter"], - ) - self.assertIs( - test_plugin.patch_check[1]["plotter"], - d["arrays"]["arr1"]["plotter"], - ) - self.assertIs( - test_plugin.patch_check[0]["versions"], d["versions"] - ) - self.assertIs( - test_plugin.patch_check[1]["versions"], d["versions"] - ) - finally: - test_plugin.checking_patch = False - - def test_keys(self): - """Test the :meth:`psyplot.project.Project.keys` method""" - import test_plotter as tp - - import psyplot.plotter as psyp - - psy.register_plotter( - "test_plotter", - import_plotter=True, - module="test_plotter", - plotter_name="TestPlotter", - ) - - class TestPlotter2(tp.TestPlotter): - fmt2 = None - - psy.register_plotter( - "test_plotter2", - module="something", - plotter_name="anyway", - plotter_cls=TestPlotter2, - ) - variables, coords = self._from_dataset_test_variables - ds = xr.Dataset(variables, coords) - sp1 = psy.plot.test_plotter(ds, name="v0") - # add a second project without a fmt2 formatoption - sp2 = psy.plot.test_plotter2(ds, name="v1") - mp = sp1 + sp2 - self.assertEqual( - sp1.keys(["fmt1", "fmt2", "fmt3"], func=str), - "+------+------+------+\n" - "| fmt1 | fmt2 | fmt3 |\n" - "+------+------+------+", - ) - self.assertEqual( - mp.keys(["fmt1", "fmt2", "fmt3"], func=str), - "+------+------+\n" "| fmt1 | fmt3 |\n" "+------+------+", - ) - title = psyp.groups["labels"] - self.assertEqual( - sp1.keys(["fmt1", "fmt2", "fmt3"], func=str, grouped=True), - "*" * len(title) + "\n" + title + "\n" + "*" * len(title) + "\n" - "+------+------+\n" - "| fmt1 | fmt2 |\n" - "+------+------+\n" - "\n" - "*********\n" - "something\n" - "*********\n" - "+------+\n" - "| fmt3 |\n" - "+------+", - ) - self.assertEqual( - mp.keys(["fmt1", "fmt2", "fmt3"], func=str, grouped=True), - "*" * len(title) + "\n" + title + "\n" + "*" * len(title) + "\n" - "+------+\n" - "| fmt1 |\n" - "+------+\n" - "\n" - "*********\n" - "something\n" - "*********\n" - "+------+\n" - "| fmt3 |\n" - "+------+", - ) - self.assertEqual( - sp1.keys(["fmt1", "something"], func=str), - "+------+------+\n" "| fmt1 | fmt3 |\n" "+------+------+", - ) - if six.PY3: - with self.assertWarnsRegex( - UserWarning, "(?i)unknown formatoption keyword" - ): - self.assertEqual( - sp1.keys(["fmt1", "wrong", "something"], func=str), - "+------+------+\n" "| fmt1 | fmt3 |\n" "+------+------+", - ) - - def test_docs(self): - """Test the :meth:`psyplot.project.Project.docs` method""" - import test_plotter as tp - - import psyplot.plotter as psyp - - psy.register_plotter( - "test_plotter", - import_plotter=True, - module="test_plotter", - plotter_name="TestPlotter", - ) - - class TestPlotter2(tp.TestPlotter): - fmt2 = None - - psy.register_plotter( - "test_plotter2", - module="something", - plotter_name="anyway", - plotter_cls=TestPlotter2, - ) - variables, coords = self._from_dataset_test_variables - ds = xr.Dataset(variables, coords) - sp1 = psy.plot.test_plotter(ds, name="v0") - # add a second project without a fmt2 formatoption - sp2 = psy.plot.test_plotter2(ds, name="v1") - mp = sp1 + sp2 - self.assertEqual( - sp1.docs(func=str), - "\n".join( - [ - "fmt1", - "====", - tp.SimpleFmt.__doc__, - "", - "fmt2", - "====", - tp.SimpleFmt2.__doc__, - "", - "fmt3", - "====", - tp.SimpleFmt3.__doc__, - "", - "post", - "====", - psyp.PostProcessing.__doc__, - "", - "post_timing", - "===========", - psyp.PostTiming.__doc__, - "", - ] - ), - ) - # test summed project - self.assertEqual( - mp.docs(func=str), - "\n".join( - [ - "fmt1", - "====", - tp.SimpleFmt.__doc__, - "", - "fmt3", - "====", - tp.SimpleFmt3.__doc__, - "", - "post", - "====", - psyp.PostProcessing.__doc__, - "", - "post_timing", - "===========", - psyp.PostTiming.__doc__, - "", - ] - ), - ) - title = psyp.groups["labels"] - self.assertEqual( - sp1.docs(["fmt1", "fmt2", "fmt3"], func=str, grouped=True), - "\n".join( - [ - "*" * len(title), - title, - "*" * len(title), - "fmt1", - "====", - tp.SimpleFmt.__doc__, - "", - "fmt2", - "====", - tp.SimpleFmt2.__doc__, - "", - "", - "*********", - "something", - "*********", - "fmt3", - "====", - tp.SimpleFmt3.__doc__, - ] - ), - ) - # test summed project - self.assertEqual( - mp.docs(["fmt1", "fmt3"], func=str, grouped=True), - "\n".join( - [ - "*" * len(title), - title, - "*" * len(title), - "fmt1", - "====", - tp.SimpleFmt.__doc__, - "", - "", - "*********", - "something", - "*********", - "fmt3", - "====", - tp.SimpleFmt3.__doc__, - ] - ), - ) - - def test_summaries(self): - """Test the :meth:`psyplot.project.Project.summaries` method""" - import test_plotter as tp - - import psyplot.plotter as psyp - - psy.register_plotter( - "test_plotter", - import_plotter=True, - module="test_plotter", - plotter_name="TestPlotter", - ) - - class TestPlotter2(tp.TestPlotter): - fmt2 = None - - psy.register_plotter( - "test_plotter2", - module="something", - plotter_name="anyway", - plotter_cls=TestPlotter2, - ) - variables, coords = self._from_dataset_test_variables - ds = xr.Dataset(variables, coords) - sp1 = psy.plot.test_plotter(ds, name="v0") - # add a second project without a fmt2 formatoption - sp2 = psy.plot.test_plotter2(ds, name="v1") - mp = sp1 + sp2 - self.assertEqual( - sp1.summaries(func=str), - "\n".join( - [ - "fmt1", - tp.indent(tp.SimpleFmt.__doc__.splitlines()[0], " "), - "fmt2", - tp.indent(tp.SimpleFmt2.__doc__.splitlines()[0], " "), - "fmt3", - tp.indent(tp.SimpleFmt3.__doc__.splitlines()[0], " "), - "post", - tp.indent( - psyp.PostProcessing.__doc__.splitlines()[0], " " - ), - "post_timing", - tp.indent(psyp.PostTiming.__doc__.splitlines()[0], " "), - ] - ), - ) - # test summed project - self.assertEqual( - mp.summaries(func=str), - "\n".join( - [ - "fmt1", - tp.indent(tp.SimpleFmt.__doc__.splitlines()[0], " "), - "fmt3", - tp.indent(tp.SimpleFmt3.__doc__.splitlines()[0], " "), - "post", - tp.indent( - psyp.PostProcessing.__doc__.splitlines()[0], " " - ), - "post_timing", - tp.indent(psyp.PostTiming.__doc__.splitlines()[0], " "), - ] - ), - ) - title = psyp.groups["labels"] - self.assertEqual( - sp1.summaries(["fmt1", "fmt2", "fmt3"], func=str, grouped=True), - "\n".join( - [ - "*" * len(title), - title, - "*" * len(title), - "fmt1", - tp.indent(tp.SimpleFmt.__doc__.splitlines()[0], " "), - "fmt2", - tp.indent(tp.SimpleFmt2.__doc__.splitlines()[0], " "), - "", - "*********", - "something", - "*********", - "fmt3", - tp.indent(tp.SimpleFmt3.__doc__.splitlines()[0], " "), - ] - ), - ) - # test summed project - self.assertEqual( - mp.summaries(["fmt1", "fmt3"], func=str, grouped=True), - "\n".join( - [ - "*" * len(title), - title, - "*" * len(title), - "fmt1", - tp.indent(tp.SimpleFmt.__doc__.splitlines()[0], " "), - "", - "*********", - "something", - "*********", - "fmt3", - tp.indent(tp.SimpleFmt3.__doc__.splitlines()[0], " "), - ] - ), - ) - - def test_figs(self): - """Test the :attr:`psyplot.project.Project.figs` attribute""" - psy.register_plotter( - "test_plotter", - import_plotter=True, - module="test_plotter", - plotter_name="TestPlotter", - ) - ds = psy.open_dataset(bt.get_file("test-t2m-u-v.nc")) - sp = psy.plot.test_plotter(ds, name="t2m", time=[1, 2]) - self.assertEqual(sp[0].psy.ax.figure.number, 1) - self.assertEqual(sp[1].psy.ax.figure.number, 2) - figs = sp.figs - self.assertIn(sp[0].psy.ax.figure, figs) - self.assertIs(figs[sp[0].psy.ax.figure][0], sp[0]) - self.assertIn(sp[1].psy.ax.figure, figs) - self.assertIs(figs[sp[1].psy.ax.figure][0], sp[1]) - - def test_axes(self): - """Test the :attr:`psyplot.project.Project.axes` attribute""" - psy.register_plotter( - "test_plotter", - import_plotter=True, - module="test_plotter", - plotter_name="TestPlotter", - ) - ds = psy.open_dataset(bt.get_file("test-t2m-u-v.nc")) - sp = psy.plot.test_plotter(ds, name="t2m", time=[1, 2]) - self.assertIsNot(sp[0].psy.ax, sp[1].psy.ax) - axes = sp.axes - self.assertIn(sp[0].psy.ax, axes) - self.assertIs(axes[sp[0].psy.ax][0], sp[0]) - self.assertIn(sp[1].psy.ax, axes) - self.assertIs(axes[sp[1].psy.ax][0], sp[1]) - - def test_close(self): - """Test the :meth:`psyplot.project.Project.close` method""" - psy.register_plotter( - "test_plotter", module="test_plotter", plotter_name="TestPlotter" - ) - ds = psy.open_dataset(bt.get_file("test-t2m-u-v.nc")) - sp0 = psy.plot.test_plotter(ds, name="t2m", time=[1]) - sp1 = psy.plot.test_plotter(ds, name="t2m", time=[1, 2]) - sp2 = psy.plot.test_plotter(ds, name="t2m", time=[3, 4]) - mp = psy.gcp(True) - names0 = sp0.arr_names - names1 = sp1.arr_names - names2 = sp2.arr_names - # some checks in the beginning - self.assertIs(sp0.main, mp) - self.assertIs(sp1.main, mp) - self.assertIs(sp2.main, mp) - self.assertEqual(mp.arr_names, names0 + names1 + names2) - self.assertEqual(mp.with_plotter.arr_names, names0 + names1 + names2) - plotters = sp0.plotters - # close sp0 but only remove the formatoptions - sp0.close(data=True, remove_only=True) - self.assertTrue(plotters[0].fmt1.removed) - self.assertEqual(mp.arr_names, names1 + names2) - self.assertEqual(mp.with_plotter.arr_names, names1 + names2) - # close sp1 - sp1.close() - self.assertEqual(mp.arr_names, names1 + names2) - self.assertEqual(mp.with_plotter.arr_names, names2) - # remove the data - sp1.close(True, True) - self.assertEqual(mp.arr_names, names2) - self.assertEqual(mp.with_plotter.arr_names, names2) - self.assertEqual(sp1, []) - self.assertEqual(len(mp), 2) - # close the dataset in sp2 - sp2.close(True, True, True) - self.assertEqual(sp2, []) - self.assertEqual(mp, []) - - def test_close_global(self): - """Test the :func:`psyplot.project.close` function""" - psy.register_plotter( - "test_plotter", module="test_plotter", plotter_name="TestPlotter" - ) - with psy.open_dataset(bt.get_file("test-t2m-u-v.nc")) as ds: - time = ds.time.values - lev = ds.lev.values - mp0 = psy.plot.test_plotter( - bt.get_file("test-t2m-u-v.nc"), name="t2m", lev=[0, 1] - ).main - mp1 = psy.project() - psy.plot.test_plotter( - bt.get_file("test-t2m-u-v.nc"), name="t2m", time=[1, 2] - ) - mp2 = psy.project() - sp1 = psy.plot.test_plotter( - bt.get_file("test-t2m-u-v.nc"), name="t2m", time=[3, 4] - ) - sp2 = psy.plot.test_plotter( - bt.get_file("test-t2m-u-v.nc"), name="t2m", lev=[2, 3] - ) - # some checks in the beginning - self.assertEqual(len(mp0), 2) - self.assertEqual(len(mp1), 2) - self.assertEqual(len(mp2), 4) - self.assertEqual(mp0[0].lev.values, lev[0]) - self.assertEqual(mp0[1].lev.values, lev[1]) - self.assertEqual(mp1[0].time.values, time[1]) - self.assertEqual(mp1[1].time.values, time[2]) - self.assertEqual(mp2[0].time.values, time[3]) - self.assertEqual(mp2[1].time.values, time[4]) - self.assertEqual(mp2[2].lev.values, lev[2]) - self.assertEqual(mp2[3].lev.values, lev[3]) - self.assertIs(psy.gcp(True), mp2) - self.assertIs(psy.gcp(), sp2) - # close the current subproject - ds = mp2[2].psy.base - psy.close() - - self.assertIs(psy.gcp(True), mp2) - self.assertEqual(psy.gcp(), []) - self.assertEqual(sp2, []) - self.assertEqual(len(sp1), 2) - self.assertEqual(mp2.arr_names, sp1.arr_names) - # close the current mainproject - ds = mp2[0].psy.base - ds.v.values # check that the data can be loaded - psy.close(mp2.num) - self.assertIs(psy.gcp(True), mp1) - self.assertEqual(mp2, []) - self.assertIs(psy.gcp().main, mp1) - self.assertEqual(psy.gcp().arr_names, mp1.arr_names) - - # close all projects - ds0 = mp0[0].psy.base - ds0.v.values # check that the data can be loaded - ds1 = mp1[0].psy.base - ds1.v.values # check that the data can be loaded - psy.close("all") - self.assertEqual(mp0, []) - self.assertEqual(mp1, []) - self.assertEqual(psy.gcp(), []) - self.assertIsNot(psy.gcp(True), mp0) - self.assertIsNot(psy.gcp(True), mp1) - - def test_oncpchange_signal(self): - """Test whether the correct signal is fired""" - psy.register_plotter( - "test_plotter", module="test_plotter", plotter_name="TestPlotter" - ) - check_mains = [] - projects = [] - - def check(p): - check_mains.append(p.is_main) - projects.append(p) - - psy.Project.oncpchange.connect(check) - ds = psy.open_dataset(bt.get_file("test-t2m-u-v.nc")).load() - sp = psy.plot.test_plotter(ds, name="t2m", lev=[0, 1]) - # the signal should have been fired 2 times, one times from the - # subproject, one times from the project - self.assertEqual(len(check_mains), 2) - self.assertIn(False, check_mains) - self.assertIn(True, check_mains) - self.assertEqual(len(projects[0]), 2, msg=str(projects[0])) - self.assertEqual(len(projects[1]), 2, msg=str(projects[1])) - - # try scp - check_mains = [] - projects = [] - p = sp[1:] - psy.scp(p) - self.assertEqual( - check_mains, [False], msg="projects: %s" % (projects,) - ) - self.assertIs(projects[0], p) - - # test appending - check_mains = [] - projects = [] - p.append(sp[0]) - self.assertEqual( - check_mains, [False], msg="projects: %s" % (projects,) - ) - self.assertIs(projects[0], p) - p.pop(1) - - # close a part of the project - check_mains = [] - projects = [] - sp[:1].close(True, True) - self.assertEqual(check_mains, [True]) - self.assertEqual(len(projects[0]), 1, msg=str(projects[0])) - - # close the remaining part of the project - check_mains = [] - projects = [] - psy.close() - self.assertEqual( - len(check_mains), 2, msg="%s, %s" % (check_mains, projects) - ) - self.assertIn(False, check_mains) - self.assertIn(True, check_mains) - self.assertEqual(len(projects[0]), 0, msg=str(projects[0])) - self.assertEqual(len(projects[1]), 0, msg=str(projects[1])) - - psy.Project.oncpchange.disconnect(check) - - def test_share_01_on_creation(self): - """Test the sharing within a project when creating it""" - psy.register_plotter( - "test_plotter", module="test_plotter", plotter_name="TestPlotter" - ) - sp = psy.plot.test_plotter( - bt.get_file("test-t2m-u-v.nc"), - name="t2m", - time=[0, 1, 2], - share="something", - ) - self.assertEqual(len(sp), 3, msg=sp) - self.assertEqual( - sp.plotters[0].fmt3.shared, - {sp.plotters[1].fmt3, sp.plotters[2].fmt3}, - ) - sp[0].psy.update(fmt3="test3") - self.assertEqual(sp.plotters[1].fmt3.value, "test3") - self.assertEqual(sp.plotters[2].fmt3.value, "test3") - - def test_share_02_method(self): - """Test the :meth:`psyplot.project.Project.share` method""" - psy.register_plotter( - "test_plotter", module="test_plotter", plotter_name="TestPlotter" - ) - sp = psy.plot.test_plotter( - bt.get_file("test-t2m-u-v.nc"), name="t2m", time=[0, 1, 2] - ) - # share within the project - sp.share(keys="something") - self.assertEqual(len(sp), 3, msg=sp) - self.assertEqual( - sp.plotters[0].fmt3.shared, - {sp.plotters[1].fmt3, sp.plotters[2].fmt3}, - ) - sp[0].psy.update(fmt3="test3") - self.assertEqual(sp.plotters[1].fmt3.value, "test3") - self.assertEqual(sp.plotters[2].fmt3.value, "test3") - - sp.unshare() - self.assertFalse(sp.plotters[0].fmt3.shared) - - # share from outside the project - sp[::2].share(sp[1], keys="something") - self.assertEqual( - sp.plotters[1].fmt3.shared, - {sp.plotters[0].fmt3, sp.plotters[2].fmt3}, - ) - sp[1].psy.update(fmt3="test3") - self.assertEqual(sp.plotters[0].fmt3.value, "test3") - self.assertEqual(sp.plotters[2].fmt3.value, "test3") - - sp.unshare() - self.assertFalse(sp.plotters[1].fmt3.shared) - - def test_share_03_method_by(self): - """Test the :meth:`psyplot.project.Project.share` method by axes/figure""" - import matplotlib.pyplot as plt - - psy.register_plotter( - "test_plotter", module="test_plotter", plotter_name="TestPlotter" - ) - fig1, ax1 = plt.subplots() - fig2, axes = plt.subplots(1, 2) - ax2, ax3 = axes - sp = psy.plot.test_plotter( - bt.get_file("test-t2m-u-v.nc"), - name="t2m", - time=range(4), - ax=[ax1, ax2, ax1, ax3], - ) - - self.assertEqual(len(sp), 4, msg=sp) - - # share by axes - sp.share(by="axes", keys="something") - self.assertEqual(sp.plotters[0].fmt3.shared, {sp.plotters[2].fmt3}) - self.assertFalse(sp.plotters[1].fmt3.shared) - self.assertFalse(sp.plotters[3].fmt3.shared) - sp[0].psy.update(fmt3="test3") - self.assertEqual(sp.plotters[2].fmt3.value, "test3") - - sp.unshare() - self.assertFalse(sp.plotters[0].fmt3.shared) - - # share by figure - sp.share(by="fig", keys="something") - self.assertEqual(sp.plotters[0].fmt3.shared, {sp.plotters[2].fmt3}) - self.assertEqual(sp.plotters[1].fmt3.shared, {sp.plotters[3].fmt3}) - sp[0].psy.update(fmt3="test3") - sp[1].psy.update(fmt3="test4") - self.assertEqual(sp.plotters[2].fmt3.value, "test3") - self.assertEqual(sp.plotters[3].fmt3.value, "test4") - - sp.unshare() - self.assertFalse(sp.plotters[0].fmt3.shared) - self.assertFalse(sp.plotters[1].fmt3.shared) - - # share with provided bases by figure - sp[2:].share(sp[:2], keys="something", by="fig") - - self.assertEqual(sp.plotters[0].fmt3.shared, {sp.plotters[2].fmt3}) - self.assertEqual(sp.plotters[1].fmt3.shared, {sp.plotters[3].fmt3}) - sp[0].psy.update(fmt3="test3") - sp[1].psy.update(fmt3="test4") - self.assertEqual(sp.plotters[2].fmt3.value, "test3") - self.assertEqual(sp.plotters[3].fmt3.value, "test4") - - sp.unshare() - self.assertFalse(sp.plotters[0].fmt3.shared) - self.assertFalse(sp.plotters[1].fmt3.shared) - - # share with provided bases by axes - sp[2:].share(sp[:2], keys="something", by="axes") - self.assertEqual(sp.plotters[0].fmt3.shared, {sp.plotters[2].fmt3}) - self.assertFalse(sp.plotters[1].fmt3.shared) - self.assertFalse(sp.plotters[3].fmt3.shared) - sp[0].psy.update(fmt3="test3") - self.assertEqual(sp.plotters[2].fmt3.value, "test3") - - sp.unshare() - self.assertFalse(sp.plotters[0].fmt3.shared) - - def _register_export_plotter(self): - class SimplePlotFormatoption(tp.TestFormatoption): - plot_fmt = True - priority = psyp.BEFOREPLOTTING - - def update(self, value): - pass - - def make_plot(self): - self.data.plot(ax=self.ax) - - class TestPlotter(psyp.Plotter): - fmt1 = SimplePlotFormatoption("fmt1") - - psy.register_plotter( - "test_plotter", - module="something", - plotter_name="irrelevant", - plotter_cls=TestPlotter, - ) - - def test_export_01_replacement(self): - """Test exporting a project""" - from tempfile import NamedTemporaryFile - - import matplotlib.pyplot as plt - import numpy as np - import pandas as pd - from matplotlib.testing.compare import compare_images - - self._register_export_plotter() - - with psy.open_dataset(bt.get_file("test-t2m-u-v.nc")) as ds: - time = ds.time - time.values # make sure the data is loaded - - ds = xr.Dataset( - { - "v0": xr.Variable(("x", "y"), np.arange(3 * 5).reshape(3, 5)), - "v1": xr.Variable( - ("time", "y"), np.arange(5 * 5).reshape(5, 5) - ), - }, - { - "x": xr.Variable(("x",), [4, 5, 6]), - "y": xr.Variable(("y",), [6, 7, 8, 9, 10]), - "time": time, - }, - ) - # create reference plots - reffiles = [] - fig, ax = plt.subplots() - ds.v0[1].plot(ax=ax) - reffiles.append( - NamedTemporaryFile(prefix="psyplot_", suffix=".png").name - ) - self._created_files.update(reffiles) - fig.savefig(reffiles[-1]) - - # figure with two plots - fig, axes = plt.subplots(1, 2) - ds.v0.plot(ax=axes[0]) - ds.v0[1:].plot(ax=axes[1]) - reffiles.append( - NamedTemporaryFile(prefix="psyplot_", suffix=".png").name - ) - self._created_files.update(reffiles) - fig.savefig(reffiles[-1]) - - plt.close("all") - - # create project - psy.plot.test_plotter( - ds, name="v0", x=1, attrs={"test": 7}, ax=plt.subplots()[1] - ) - psy.plot.test_plotter( - ds, - name="v0", - x=[slice(None), slice(1, None)], - attrs={"test": 3}, - ax=plt.subplots(1, 2)[1], - ) - mp = psy.gcp(True) - self.assertEqual(len(mp), 3, msg=mp) - - base_name = NamedTemporaryFile(prefix="psyplot_").name - mp.export(base_name + "%i_%(test)s.png") - # compare reference files and exported files - self.assertTrue( - osp.exists(base_name + "1_7.png"), - msg="Missing " + base_name + "1_7.png", - ) - self._created_files.add(base_name + "1_7.png") - self.assertTrue( - osp.exists(base_name + "2_3.png"), - msg="Missing " + base_name + "2_3.png", - ) - self._created_files.add(base_name + "2_3.png") - results = compare_images(reffiles[0], base_name + "1_7.png", 1) - self.assertIsNone(results, msg=results) - results = compare_images(reffiles[1], base_name + "2_3.png", 1) - self.assertIsNone(results, msg=results) - - # check time formatting - psy.close(mp) - reffiles = [] - fig, ax = plt.subplots() - ds.v1[1].plot(ax=ax) - reffiles.append( - NamedTemporaryFile(prefix="psyplot_", suffix=".png").name - ) - self._created_files.update(reffiles) - fig.savefig(reffiles[-1]) - - fig, axes = plt.subplots(1, 2) - ds.v1[2, :2].plot(ax=axes[0]) - ds.v1[2, 2:].plot(ax=axes[1]) - reffiles.append( - NamedTemporaryFile(prefix="psyplot_", suffix=".png").name - ) - self._created_files.update(reffiles) - fig.savefig(reffiles[-1]) - - plt.close("all") - - # create project - psy.plot.test_plotter( - ds, name="v1", time=1, attrs={"test": 3}, ax=plt.subplots()[1] - ) - psy.plot.test_plotter( - ds, - name="v1", - time=2, - attrs={"test": 5}, - y=[slice(0, 2), slice(2, None)], - ax=plt.subplots(1, 2)[1], - ) - mp = psy.gcp(True) - self.assertEqual(len(mp), 3, msg=mp) - mp.export(base_name + "%%i_%m_%%(test)s.png", use_time=True) - - # compare reference files and exported files - t1 = pd.to_datetime(time.values[1]).strftime("%m") - t2 = pd.to_datetime(time.values[2]).strftime("%m") - self.assertTrue( - osp.exists(base_name + ("1_%s_3.png" % t1)), - msg="Missing " + base_name + ("1_%s_3.png" % t1), - ) - self._created_files.add(base_name + ("1_%s_3.png" % t1)) - self.assertTrue( - osp.exists(base_name + ("2_%s_5.png" % t2)), - msg="Missing " + base_name + ("2_%s_5.png" % t2), - ) - self._created_files.add(base_name + ("2_%s_5.png" % t2)) - results = compare_images( - reffiles[0], base_name + ("1_%s_3.png" % t1), 1 - ) - self.assertIsNone(results, msg=results) - results = compare_images( - reffiles[1], base_name + ("2_%s_5.png" % t2), 1 - ) - self.assertIsNone(results, msg=results) - - # check pdf replacement - psy.close(mp) - sp = psy.plot.test_plotter( - ds, name="v1", time=1, attrs={"test": 3}, ax=plt.subplots()[1] - ) - sp.export(base_name + "%m_%%(test)s.pdf", use_time=True) - self.assertTrue( - osp.exists(base_name + ("%s_3.pdf" % t1)), - msg="Missing " + base_name + ("%s_3.pdf" % t1), - ) - self._created_files.add(base_name + ("%s_3.pdf" % t1)) - - def test_export_02_list(self): - """Test whether the exporting to a list works well""" - import tempfile - - self._register_export_plotter() - sp = psy.plot.test_plotter( - bt.get_file("test-t2m-u-v.nc"), name="t2m", time=[1, 2, 3], z=0 - ) - self.assertEqual(len(sp), 3, msg=sp) - - fnames = list( - tempfile.NamedTemporaryFile(suffix=".png", prefix="psyplot_").name - for _ in range(3) - ) - self._created_files.update(fnames) - - sp.export(fnames) - - for fname in fnames: - self.assertTrue(osp.exists(fname), msg="Missing " + fname) - - def test_export_03_append(self): - """Append to a pdf file""" - import tempfile - - self._register_export_plotter() - fig1, ax1 = plt.subplots(1, 2) - fig2, ax2 = plt.subplots() - axes = list(ax1) + [ax2] - sp = psy.plot.test_plotter( - bt.get_file("test-t2m-u-v.nc"), - name="t2m", - time=[1, 2, 3], - z=0, - y=0, - ax=axes, - ) - self.assertEqual(len(sp), 3, msg=sp) - - fname = tempfile.NamedTemporaryFile( - suffix=".pdf", prefix="psyplot_" - ).name - self._created_files.add(fname) - - pdf = sp.export(fname, close_pdf=False) - - self.assertEqual(pdf.get_pagecount(), 2) - - sp.export(pdf) - - self.assertEqual(pdf.get_pagecount(), 4) - - pdf.close() - - def test_update(self): - """Test the update of an :class:`psyplot.data.ArrayList`""" - variables, coords = self._from_dataset_test_variables - ds = xr.Dataset(variables, coords) - psy.register_plotter( - "test_plotter", - module="something", - plotter_name="unimportant", - plotter_cls=tp.TestPlotter, - ) - # add 2 arrays - psy.plot.test_plotter(ds, name=["v0", "v1"], t=0) - # add a list - psy.plot.test_plotter(ds, name=["v0", "v1"], t=0, prefer_list=True) - - mp = psy.gcp(True) - - self.assertEqual(len(mp), 3, msg=mp) - self.assertEqual(len(mp.plotters), 3, msg=mp) - - # update the list - mp.update(t=1, fmt2="updated") - - for i, plotter in enumerate(mp.plotters): - self.assertEqual( - plotter["fmt2"], - "updated", - msg="Plotter of array %i not updated! %s" % (i, mp[i]), - ) - - self.assertEqual(mp[0].time, ds.time[1]) - self.assertEqual(mp[1].time, ds.time[1]) - for data in mp[2]: - self.assertEqual(data.time, ds.time[1]) - - -class TestPlotterInterface(unittest.TestCase): - list_class = psy.Project - - def setUp(self): - for identifier in list(psy.registered_plotters): - psy.unregister_plotter(identifier) - psy.close("all") - plt.close("all") - - def tearDown(self): - for identifier in list(psy.registered_plotters): - psy.unregister_plotter(identifier) - psy.close("all") - plt.close("all") - tp.results.clear() - - def test_plotter_registration(self): - """Test the registration of a plotter""" - psy.register_plotter( - "test_plotter", - import_plotter=True, - module="test_plotter", - plotter_name="TestPlotter", - ) - self.assertTrue(hasattr(psy.plot, "test_plotter")) - self.assertIs(psy.plot.test_plotter.plotter_cls, tp.TestPlotter) - psy.plot.test_plotter.print_func = str - self.assertEqual(psy.plot.test_plotter.fmt1(), tp.SimpleFmt.__doc__) - psy.plot.test_plotter.print_func = None - # test the warning - if not six.PY2: - with self.assertWarnsRegex(UserWarning, "not_existent_module"): - psy.register_plotter( - "something", - "not_existent_module", - "not_important", - import_plotter=True, - ) - psy.unregister_plotter("test_plotter") - self.assertFalse(hasattr(psy.Project, "test_plotter")) - self.assertFalse(hasattr(psy.plot, "test_plotter")) - - def test_plot_creation_01_array(self): - """Test the plot creation with a plotter that takes one array""" - psy.register_plotter( - "test_plotter", - import_plotter=True, - module="test_plotter", - plotter_name="TestPlotter", - ) - ds = psy.open_dataset(bt.get_file("test-t2m-u-v.nc")) - sp = psy.plot.test_plotter(ds, name="t2m") - self.assertEqual(len(sp), 1) - self.assertEqual(sp[0].name, "t2m") - self.assertEqual(sp[0].shape, ds.t2m.shape) - self.assertEqual(sp[0].values.tolist(), ds.t2m.values.tolist()) - psy.close() - psy.unregister_plotter("test_plotter") - - def test_plot_creation_02_array_default_dims(self): - # add a default value for the y dimension - psy.register_plotter( - "test_plotter", - import_plotter=True, - module="test_plotter", - plotter_name="TestPlotter", - default_dims={"y": 0}, - ) - ds = psy.open_dataset(bt.get_file("test-t2m-u-v.nc")) - sp = psy.plot.test_plotter(ds, name="t2m") - self.assertEqual(len(sp), 1) - self.assertEqual(sp[0].name, "t2m") - self.assertEqual(sp[0].shape, ds.t2m.isel(lat=0).shape) - self.assertEqual( - sp[0].values.tolist(), ds.t2m.isel(lat=0).values.tolist() - ) - psy.close() - psy.unregister_plotter("test_plotter") - - def test_plot_creation_03_2arrays(self): - # try multiple names and dimension - psy.register_plotter( - "test_plotter", - import_plotter=True, - module="test_plotter", - plotter_name="TestPlotter", - default_dims={"y": 0}, - ) - ds = psy.open_dataset(bt.get_file("test-t2m-u-v.nc")) - sp = psy.plot.test_plotter(ds, name=["t2m", "u"], x=slice(3, 5)) - self.assertEqual(len(sp), 2) - self.assertEqual(sp[0].name, "t2m") - self.assertEqual(sp[1].name, "u") - self.assertEqual( - sp[0].shape, ds.t2m.isel(lat=0, lon=slice(3, 5)).shape - ) - self.assertEqual(sp[1].shape, ds.u.isel(lat=0, lon=slice(3, 5)).shape) - self.assertEqual( - sp[0].values.tolist(), - ds.t2m.isel(lat=0, lon=slice(3, 5)).values.tolist(), - ) - self.assertEqual( - sp[1].values.tolist(), - ds.u.isel(lat=0, lon=slice(3, 5)).values.tolist(), - ) - psy.close() - psy.unregister_plotter("test_plotter") - - def test_plot_creation_04_2variables(self): - # test with array out of 2 variables - psy.register_plotter( - "test_plotter", - import_plotter=True, - module="test_plotter", - plotter_name="TestPlotter", - default_dims={"y": 0}, - ) - ds = psy.open_dataset(bt.get_file("test-t2m-u-v.nc")) - sp = psy.plot.test_plotter( - ds, name=[["u", "v"]], x=slice(3, 5), load=True - ) - self.assertEqual(len(sp), 1) - self.assertIn("variable", sp[0].dims) - self.assertEqual(sp[0].coords["variable"].values.tolist(), ["u", "v"]) - self.assertEqual( - list(sp[0].shape), - [2] + list(ds.t2m.isel(lat=0, lon=slice(3, 5)).shape), - ) - self.assertEqual( - sp[0].values.tolist(), - ds[["u", "v"]] - .to_array() - .isel(lat=0, lon=slice(3, 5)) - .values.tolist(), - ) - psy.close() - psy.unregister_plotter("test_plotter") - - def test_plot_creation_05_array_and_2variables(self): - # test a combination of them - # psyplot.project.Project([ - # arr0: 2-dim DataArray of t2m, with - # (time, lev)=(5, 4), lon=1.875, lat=88.5721685, - # arr1: 2-dim DataArray of t2m, with - # (time, lev)=(5, 4), lon=3.75, lat=88.5721685, - # arr2: 3-dim DataArray of u, v, with - # (variable, time, lev)=(2, 5, 4), lat=88.5721685, lon=1.875, - # arr3: 3-dim DataArray of u, v, with - # (variable, time, lev)=(2, 5, 4), lat=88.5721685, lon=3.75]) - psy.register_plotter( - "test_plotter", - import_plotter=True, - module="test_plotter", - plotter_name="TestPlotter", - default_dims={"y": 0}, - ) - ds = psy.open_dataset(bt.get_file("test-t2m-u-v.nc")) - sp = psy.plot.test_plotter(ds, name=["t2m", ["u", "v"]], x=[1, 2]) - self.assertEqual(len(sp), 4, msg=str(sp)) - self.assertEqual(sp[0].shape, ds.t2m.isel(lat=0, lon=1).shape) - self.assertEqual(sp[1].shape, ds.t2m.isel(lat=0, lon=2).shape) - self.assertEqual( - list(sp[2].shape), [2] + list(ds.u.isel(lat=0, lon=1).shape) - ) - self.assertEqual( - list(sp[2].shape), [2] + list(ds.u.isel(lat=0, lon=2).shape) - ) - self.assertEqual( - sp[0].values.tolist(), ds.t2m.isel(lat=0, lon=1).values.tolist() - ) - self.assertEqual( - sp[1].values.tolist(), ds.t2m.isel(lat=0, lon=2).values.tolist() - ) - self.assertEqual( - sp[2].values.tolist(), - ds[["u", "v"]].isel(lat=0, lon=1).to_array().values.tolist(), - ) - self.assertEqual( - sp[3].values.tolist(), - ds[["u", "v"]].isel(lat=0, lon=2).to_array().values.tolist(), - ) - psy.close() - psy.unregister_plotter("test_plotter") - - def test_plot_creation_06_list(self): - """Test the plot creation with a plotter that takes a list of arrays""" - psy.register_plotter( - "test_plotter", - import_plotter=True, - module="test_plotter", - plotter_name="TestPlotter", - prefer_list=True, - ) - ds = psy.open_dataset(bt.get_file("test-t2m-u-v.nc")) - # test the creation of one list - # psyplot.project.Project([arr4: psyplot.data.InteractiveList([ - # arr0: 4-dim DataArray of t2m, with - # (time, lev, lat, lon)=(5, 4, 96, 192), , - # arr1: 4-dim DataArray of u, with - # (time, lev, lat, lon)=(5, 4, 96, 192), ])]) - sp = psy.plot.test_plotter(ds, name=["t2m", "u"]) - self.assertEqual(len(sp), 1) - self.assertEqual(len(sp[0]), 2) - self.assertEqual(sp[0][0].name, "t2m") - self.assertEqual(sp[0][1].name, "u") - self.assertEqual(sp[0][0].shape, ds.t2m.shape) - self.assertEqual(sp[0][1].shape, ds.u.shape) - self.assertEqual(sp[0][0].values.tolist(), ds.t2m.values.tolist()) - self.assertEqual(sp[0][1].values.tolist(), ds.u.values.tolist()) - psy.close() - psy.unregister_plotter("test_plotter") - - def test_plot_creation_07_list_and_dims(self): - # use dimensions which should result in one list with 4 arrays, - # t2m, t2m, u, u - # psyplot.project.Project([arr3: psyplot.data.InteractiveList([ - # arr0: 3-dim DataArray of t2m, with - # (time, lev, lat)=(5, 4, 96), lon=1.875, - # arr1: 3-dim DataArray of t2m, with - # (time, lev, lat)=(5, 4, 96), lon=3.75, - # arr2: 3-dim DataArray of u, with - # (time, lev, lat)=(5, 4, 96), lon=1.875, - # arr3: 3-dim DataArray of u, with - # (time, lev, lat)=(5, 4, 96), lon=3.75])]) - psy.register_plotter( - "test_plotter", - import_plotter=True, - module="test_plotter", - plotter_name="TestPlotter", - prefer_list=True, - ) - ds = psy.open_dataset(bt.get_file("test-t2m-u-v.nc")) - sp = psy.plot.test_plotter(ds, name=["t2m", "u"], x=[1, 2]) - self.assertEqual(len(sp), 1) - self.assertEqual(len(sp[0]), 4) - self.assertEqual(sp[0][0].name, "t2m") - self.assertEqual(sp[0][1].name, "t2m") - self.assertEqual(sp[0][2].name, "u") - self.assertEqual(sp[0][3].name, "u") - self.assertEqual(sp[0][0].shape, ds.t2m.isel(lon=1).shape) - self.assertEqual(sp[0][1].shape, ds.t2m.isel(lon=2).shape) - self.assertEqual(sp[0][2].shape, ds.u.isel(lon=1).shape) - self.assertEqual(sp[0][3].shape, ds.u.isel(lon=2).shape) - self.assertEqual( - sp[0][0].values.tolist(), ds.t2m.isel(lon=1).values.tolist() - ) - self.assertEqual( - sp[0][1].values.tolist(), ds.t2m.isel(lon=2).values.tolist() - ) - self.assertEqual( - sp[0][2].values.tolist(), ds.u.isel(lon=1).values.tolist() - ) - self.assertEqual( - sp[0][3].values.tolist(), ds.u.isel(lon=2).values.tolist() - ) - psy.close() - psy.unregister_plotter("test_plotter") - - def test_plot_creation_08_list_and_2variables(self): - # test with arrays out of 2 variables. Should result in a list of - # two arrays, both should have the two variables 't2m' and 'u' - # psyplot.project.Project([arr2: psyplot.data.InteractiveList([ - # arr0: 4-dim DataArray of t2m, u, with - # (variable, time, lev, lat)=(2, 5, 4, 96), lon=1.875, - # arr1: 4-dim DataArray of t2m, u, with - # (variable, time, lev, lat)=(2, 5, 4, 96), lon=3.75])]) - psy.register_plotter( - "test_plotter", - import_plotter=True, - module="test_plotter", - plotter_name="TestPlotter", - prefer_list=True, - ) - ds = psy.open_dataset(bt.get_file("test-t2m-u-v.nc")) - sp = psy.plot.test_plotter(ds, name=[[["t2m", "u"]]], x=[1, 2]) - self.assertEqual(len(sp), 1) - self.assertEqual(len(sp[0]), 2) - self.assertIn("variable", sp[0][0].dims) - self.assertIn("variable", sp[0][1].dims) - self.assertEqual( - list(sp[0][0].shape), [2] + list(ds.t2m.isel(lon=1).shape) - ) - self.assertEqual( - list(sp[0][1].shape), [2] + list(ds.u.isel(lon=1).shape) - ) - self.assertEqual( - sp[0][0].values.tolist(), - ds[["t2m", "u"]].to_array().isel(lon=1).values.tolist(), - ) - self.assertEqual( - sp[0][1].values.tolist(), - ds[["t2m", "u"]].to_array().isel(lon=2).values.tolist(), - ) - psy.close() - psy.unregister_plotter("test_plotter") - - def test_plot_creation_09_list_of_list_of_arrays(self): - # test list of list of arrays - # psyplot.project.Project([ - # arr0: psyplot.data.InteractiveList([ - # arr0: 3-dim DataArray of t2m, with - # (time, lev, lat)=(5, 4, 96), lon=1.875, - # arr1: 3-dim DataArray of u, with # - # (time, lev, lat)=(5, 4, 96), lon=1.875]), - # arr1: psyplot.data.InteractiveList([ - # arr0: 3-dim DataArray of t2m, with - # (time, lev, lat)=(5, 4, 96), lon=3.75, - # arr1: 3-dim DataArray of u, with - # (time, lev, lat)=(5, 4, 96), lon=3.75])]) - psy.register_plotter( - "test_plotter", - import_plotter=True, - module="test_plotter", - plotter_name="TestPlotter", - prefer_list=True, - ) - ds = psy.open_dataset(bt.get_file("test-t2m-u-v.nc")) - sp = psy.plot.test_plotter( - bt.get_file("test-t2m-u-v.nc"), name=[["t2m", "u"]], x=[1, 2] - ) - self.assertEqual(len(sp), 2) - self.assertEqual(len(sp[0]), 2) - self.assertEqual(len(sp[1]), 2) - self.assertEqual(sp[0][0].name, "t2m") - self.assertEqual(sp[0][1].name, "u") - self.assertEqual(sp[1][0].name, "t2m") - self.assertEqual(sp[1][1].name, "u") - self.assertEqual(sp[0][0].shape, ds.t2m.isel(lon=1).shape) - self.assertEqual(sp[0][1].shape, ds.u.isel(lon=1).shape) - self.assertEqual(sp[1][0].shape, ds.t2m.isel(lon=2).shape) - self.assertEqual(sp[1][1].shape, ds.u.isel(lon=2).shape) - self.assertEqual( - sp[0][0].values.tolist(), ds.t2m.isel(lon=1).values.tolist() - ) - self.assertEqual( - sp[0][1].values.tolist(), ds.u.isel(lon=1).values.tolist() - ) - self.assertEqual( - sp[1][0].values.tolist(), ds.t2m.isel(lon=2).values.tolist() - ) - self.assertEqual( - sp[1][1].values.tolist(), ds.u.isel(lon=2).values.tolist() - ) - psy.close() - ds.close() - psy.unregister_plotter("test_plotter") - - def test_plot_creation_10_list_array_and_2variables(self): - # test list of list with array and an array out of 2 variables - # psyplot.project.Project([ - # arr0: psyplot.data.InteractiveList([ - # arr0: 3-dim DataArray of t2m, with - # (time, lev, lat)=(5, 4, 96), lon=1.875, - # arr1: 4-dim DataArray of u, v, with - # (variable, time, lev, lat)=(2, 5, 4, 96), lon=1.875]), - # arr1: psyplot.data.InteractiveList([ - # arr0: 3-dim DataArray of t2m, with - # (time, lev, lat)=(5, 4, 96), lon=1.875, - # arr1: 4-dim DataArray of u, v, with - # (variable, time, lev, lat)=(2, 5, 4, 96), lon=1.875])]) - psy.register_plotter( - "test_plotter", - import_plotter=True, - module="test_plotter", - plotter_name="TestPlotter", - prefer_list=True, - ) - ds = psy.open_dataset(bt.get_file("test-t2m-u-v.nc")) - sp = psy.plot.test_plotter(ds, name=[["t2m", ["u", "v"]]], x=[1, 2]) - self.assertEqual(len(sp), 2) - self.assertEqual(len(sp[0]), 2) - self.assertEqual(len(sp[1]), 2) - self.assertEqual(sp[0][0].name, "t2m") - self.assertIn("variable", sp[0][1].dims) - self.assertEqual( - sp[0][1].coords["variable"].values.tolist(), ["u", "v"] - ) - self.assertEqual(sp[1][0].name, "t2m") - self.assertIn("variable", sp[1][1].dims) - self.assertEqual( - sp[1][1].coords["variable"].values.tolist(), ["u", "v"] - ) - self.assertEqual(sp[0][0].shape, ds.t2m.isel(lon=1).shape) - self.assertEqual( - list(sp[0][1].shape), [2] + list(ds.u.isel(lon=1).shape) - ) - self.assertEqual(sp[1][0].shape, ds.t2m.isel(lon=2).shape) - self.assertEqual( - list(sp[1][1].shape), [2] + list(ds.u.isel(lon=2).shape) - ) - self.assertEqual( - sp[0][0].values.tolist(), ds.t2m.isel(lon=1).values.tolist() - ) - self.assertEqual( - sp[0][1].values.tolist(), - ds[["u", "v"]].isel(lon=1).to_array().values.tolist(), - ) - self.assertEqual( - sp[1][0].values.tolist(), ds.t2m.isel(lon=2).values.tolist() - ) - self.assertEqual( - sp[1][1].values.tolist(), - ds[["u", "v"]].isel(lon=2).to_array().values.tolist(), - ) - psy.close() - psy.unregister_plotter("test_plotter") - - def test_plot_creation_11_post_fmt(self): - """Test the :attr:`psyplot.plotter.Plotter.post` formatoption""" - psy.register_plotter( - "test_plotter", - import_plotter=True, - module="test_plotter", - plotter_name="TestPlotter", - ) - ds = psy.open_dataset(bt.get_file("test-t2m-u-v.nc")) - # test whether it is plotted automatically - sp = psy.plot.test_plotter( - ds, name="t2m", post='self.ax.set_title("test")' - ) - self.assertEqual(sp.plotters[0].ax.get_title(), "test") - # test whether the disabling works - sp = psy.plot.test_plotter( - ds, name="t2m", enable_post=False, post='self.ax.set_title("test")' - ) - self.assertEqual(sp.plotters[0].ax.get_title(), "") - - def test_check_data(self): - """Test the :meth:`psyplot.project._PlotterInterface.check_data` method""" - - class TestPlotter(psyp.Plotter): - @classmethod - def check_data(cls, name, dims, is_unstructured): - checks, messages = super(TestPlotter, cls).check_data( - name, dims, is_unstructured - ) - self.assertEqual(name, ["t2m"]) - - for n, d in zip(name, dims): - if test_x: # changed x-coordinate - removed = {"lev", "time"} - else: - removed = {"lev", "lon"} - self.assertEqual(len(d), len(set(ds.t2m.dims) - removed)) - self.assertEqual(set(d), set(ds.t2m.dims) - removed) - - test_x = False - - ds = psy.open_dataset(bt.get_file("test-t2m-u-v.nc")) - - psy.register_plotter( - "test_plotter", - module="nothing", - plotter_name="dont_care", - plotter_cls=TestPlotter, - default_dims={"x": 1}, - default_slice=slice(1, 3), - ) - - psy.plot.test_plotter.check_data(ds, "t2m", {"lev": 3}) - - test_x = True - psy.plot.test_plotter.check_data( - ds, "t2m", {"lev": 3}, {"x": {"time"}} - ) - - psy.unregister_plotter("test_plotter") - - -class TestDatasetPlotter(unittest.TestCase): - """Test the Dataset accessor and :class:`psyplot.project.DatasetPlotter`""" - - def setUp(self): - for identifier in list(psy.registered_plotters): - psy.unregister_plotter(identifier) - psy.close("all") - plt.close("all") - - def tearDown(self): - for identifier in list(psy.registered_plotters): - psy.unregister_plotter(identifier) - psy.close("all") - plt.close("all") - tp.results.clear() - - def test_plotting(self): - psy.register_plotter( - "test_plotter", - import_plotter=True, - module="test_plotter", - plotter_name="TestPlotter", - ) - - ds = psy.open_dataset(bt.get_file("test-t2m-u-v.nc")) - sp = ds.psy.plot.test_plotter(name="t2m") - self.assertEqual(len(sp), 1) - self.assertEqual(sp[0].name, "t2m") - self.assertEqual(sp[0].shape, ds.t2m.shape) - self.assertEqual(sp[0].values.tolist(), ds.t2m.values.tolist()) - self.assertIs(sp, psy.gcp()) - - -class TestDataArrayPlotter(unittest.TestCase): - """Test DataArray accessor and :class:`psyplot.project.DataArrayPlotter`""" - - def setUp(self): - for identifier in list(psy.registered_plotters): - psy.unregister_plotter(identifier) - plt.close("all") - - def tearDown(self): - for identifier in list(psy.registered_plotters): - psy.unregister_plotter(identifier) - plt.close("all") - tp.results.clear() - - def test_plotting(self): - psy.register_plotter( - "test_plotter", - import_plotter=True, - module="test_plotter", - plotter_name="TestPlotter", - ) - - ds = psy.open_dataset(bt.get_file("test-t2m-u-v.nc")) - plotter = ds.t2m.psy.plot.test_plotter(fmt1="fmt1 set") - self.assertTrue(plotter.fmt1.value, "fmt1 set") - - -@unittest.skipIf(not with_cdo, "Cdo not installed") -class TestCdo(unittest.TestCase): - def setUp(self): - psy.close("all") - plt.close("all") - - def tearDown(self): - for identifier in list(psy.registered_plotters): - psy.unregister_plotter(identifier) - psy.close("all") - plt.close("all") - tp.results.clear() - - def test_cdo(self): - cdo = psy.Cdo() - sp = cdo.timmean( - input=bt.get_file("test-t2m-u-v.nc"), - name="t2m", - dims=dict(z=[1, 2]), - ) - with psy.open_dataset(bt.get_file("test-t2m-u-v.nc")) as ds: - lev = ds.lev.values - self.assertEqual(len(sp), 2, msg=str(sp)) - self.assertEqual(sp[0].name, "t2m") - self.assertEqual(sp[1].name, "t2m") - self.assertEqual(sp[0].lev.values, lev[1]) - self.assertEqual(sp[1].lev.values, lev[2]) - self.assertIsNone(sp[0].psy.plotter) - self.assertIsNone(sp[1].psy.plotter) - - def test_cdo_plotter(self): - cdo = psy.Cdo() - psy.register_plotter( - "test_plotter", module="test_plotter", plotter_name="TestPlotter" - ) - sp = cdo.timmean( - input=bt.get_file("test-t2m-u-v.nc"), - name="t2m", - dims=dict(z=[1, 2]), - plot_method="test_plotter", - ) - with psy.open_dataset(bt.get_file("test-t2m-u-v.nc")) as ds: - lev = ds.lev.values - self.assertEqual(len(sp), 2, msg=str(sp)) - self.assertEqual(sp[0].name, "t2m") - self.assertEqual(sp[1].name, "t2m") - self.assertEqual(sp[0].lev.values, lev[1]) - self.assertEqual(sp[1].lev.values, lev[2]) - self.assertIsInstance(sp[0].psy.plotter, tp.TestPlotter) - self.assertIsInstance(sp[1].psy.plotter, tp.TestPlotter) - - -class TestMultipleSubplots(unittest.TestCase): - def test_one_subplot(self): - plt.close("all") - axes = psy.multiple_subplots() - self.assertEqual(len(axes), 1) - self.assertEqual(plt.get_fignums(), [1]) - self.assertEqual(len(plt.gcf().axes), 1) - self.assertIs(axes[0], plt.gcf().axes[0]) - plt.close("all") - - def test_multiple_subplots(self): - plt.close("all") - axes = psy.multiple_subplots(2, 2, 3, 5) - self.assertEqual(len(axes), 5) - self.assertEqual(plt.get_fignums(), [1, 2]) - self.assertEqual(len(plt.figure(1).axes), 3) - self.assertEqual(len(plt.figure(2).axes), 2) - it_ax = iter(axes) - for ax2 in chain(plt.figure(1).axes, plt.figure(2).axes): - self.assertIs(next(it_ax), ax2) - plt.close("all") - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/test_rcsetup.py b/tests/test_rcsetup.py deleted file mode 100644 index 018e570..0000000 --- a/tests/test_rcsetup.py +++ /dev/null @@ -1,238 +0,0 @@ -"""Test module of the :mod:`psyplot.config.rcsetup` module.""" - -# SPDX-FileCopyrightText: 2016-2024 University of Lausanne -# SPDX-FileCopyrightText: 2020-2021 Helmholtz-Zentrum Geesthacht - -# SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH -# -# SPDX-License-Identifier: LGPL-3.0-only - -import unittest - -import six - -import psyplot -from psyplot.config.rcsetup import RcParams, SubDict, rcParams - - -class SubDictTest(unittest.TestCase): - def test_basic(self): - """Test the basic functionality""" - d = { - "test.1": "test1", - "test.2": "test2", - "test1.1": "test11", - "test1.2": "test12", - } - sub = SubDict(d, "test.", pattern_base=r"test\.") - self.assertIn("1", sub) - self.assertIn("2", sub) - self.assertEqual(sub["1"], "test1") - self.assertEqual(sub["2"], "test2") - self.assertNotIn( - "test11", sub.values(), msg="Item test1.1 catched in %s" % (sub,) - ) - self.assertNotIn( - "test12", sub.values(), msg="Item test1.2 catched in %s" % (sub,) - ) - - def test_replace(self): - """Test the replace property""" - d = { - "test.1": "test1", - "test.2": "test2", - "test1.1": "test11", - "test1.2": "test12", - } - sub = SubDict(d, "test.", pattern_base=r"test\.") - sub["test"] = 5 # test something that is not traced back to d - self.assertNotIn("test.1", sub) - self.assertIn("1", sub) - sub.replace = False - sub.trace = True - sub["test.2"] = 4 - self.assertIn("test.1", sub) - self.assertNotIn("1", sub) - self.assertEqual(sub["test.2"], 4) - self.assertEqual(d["test.2"], 4) - sub.replace = True - self.assertNotIn("test.1", sub) - self.assertIn("1", sub) - - def test_trace(self): - """Test the backtracing to the origin dictionary""" - d = { - "test.1": "test1", - "test.2": "test2", - "test1.1": "test11", - "test1.2": "test12", - } - sub = SubDict(d, "test.", pattern_base=r"test\.", trace=True) - self.assertIn("1", sub) - sub["1"] = "change in d" - sub["test.3"] = "test3" # new item - self.assertEqual(d["test.1"], "change in d") - self.assertEqual(sub["1"], "change in d") - self.assertIn("3", sub) - self.assertIn("test.3", d) - - sub.trace = False - sub["1"] = "do not change in d" - sub["4"] = "test4" - self.assertEqual(d["test.1"], "change in d") - self.assertEqual(sub["1"], "do not change in d") - self.assertIn("4", sub) - self.assertNotIn("4", d) - - -class RcParamsTest(unittest.TestCase): - """Test the functionality of RcParams""" - - @unittest.skipIf(six.PY2, "Missing necessary unittest methods") - def test_dump(self): - """Test the dumping of the rcParams""" - rc = RcParams( - defaultParams={ - "some.test": [1, lambda i: int(i), "The documentation"], - "some.other_test": [ - 2, - lambda i: int(i), - "Another documentation", - ], - } - ) - rc.update_from_defaultParams() - - rc.HEADER = "the header" - s = rc.dump(default_flow_style=False) - self.assertIn("the header", s) - self.assertRegex(s, r"# The documentation\n\s*some.test") - self.assertRegex(s, r"# Another documentation\n\s*some.other_test") - - def test_catch(self): - rc = RcParams( - defaultParams={ - "some.test": [1, lambda i: int(i), "The documentation"], - "some.other_test": [ - 2, - lambda i: int(i), - "Another documentation", - ], - } - ) - rc.update_from_defaultParams() - with rc.catch(): - rc["some.test"] = 2 - self.assertEqual(rc["some.test"], 2) - self.assertEqual(rc["some.test"], 1) - - @unittest.skipIf(six.PY2, "Method not available on Python2") - def test_error(self): - """Test whether the correct Error is raised""" - - def validate(i): - try: - return int(i) - except Exception: - raise ValueError("Expected failure") - - rc = RcParams( - defaultParams={ - "some.test": [1, validate, "The documentation"], - "some.other_test": [2, validate, "Another documentation"], - } - ) - rc.update_from_defaultParams() - with self.assertRaisesRegex(ValueError, "Expected failure"): - rc["some.test"] = "test" - with self.assertRaises(KeyError): - rc["wrong_key"] = 1 - rc._deprecated_map["something"] = ["some.test", lambda x: x] - with self.assertWarnsRegex( - UserWarning, rc.msg_depr % ("something", "some.test") - ): - rc["something"] = 3 - # check whether the value has been changed correctly - self.assertEqual(rc["some.test"], 3) - rc._deprecated_ignore_map["ignored"] = "some.test" - with self.assertWarnsRegex( - UserWarning, rc.msg_depr_ignore % ("ignored", "some.test") - ): - rc["ignored"] = None - # check whether the value has not been changed - self.assertEqual(rc["some.test"], 3) - - def test_findall(self): - rc = RcParams( - defaultParams={ - "some.test": [1, lambda i: int(i), "The documentation"], - "some.other_test": [ - 2, - lambda i: int(i), - "Another documentation", - ], - } - ) - rc.update_from_defaultParams() - self.assertEqual(rc.find_all("other"), {"some.other_test": 2}) - - @unittest.skipIf(six.PY2, "Missing necessary unittest methods") - def test_plugin(self): - """Test whether the plugin interface works""" - - try: - from psyplot_test.plugin import rcParams as test_rc - except ImportError: - self.skipTest("Could not import the psyplot_test package") - return - rc = psyplot.rcParams.copy() - rc.load_plugins() - self.assertIn("test", rc) - self.assertEqual(rc["test"], 1) - with self.assertRaisesRegex( - ImportError, "plotters have already been defined" - ): - rc.load_plugins(True) - plotters = test_rc.pop("project.plotters") - try: - with self.assertRaisesRegex( - ImportError, "default keys have already been defined" - ): - rc.load_plugins(True) - except Exception: - raise - finally: - test_rc["project.plotters"] = plotters - - def test_connect(self): - """Test the connection and disconnection to rcParams""" - x = set() - y = set() - - def update_x(val): - x.update(val) - - def update_y(val): - y.update(val) - - rcParams.connect("decoder.x", update_x) - rcParams.connect("decoder.y", update_y) - - rcParams["decoder.x"] = {"test"} - self.assertEqual(x, {"test"}) - self.assertEqual(y, set()) - - rcParams["decoder.y"] = {"test2"} - self.assertEqual(y, {"test2"}) - - rcParams.disconnect("decoder.x", update_x) - rcParams["decoder.x"] = {"test3"} - self.assertEqual(x, {"test"}) - - rcParams.disconnect() - rcParams["decoder.y"] = {"test4"} - self.assertEqual(y, {"test2"}) - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/test_utils.py b/tests/test_utils.py deleted file mode 100644 index 936005a..0000000 --- a/tests/test_utils.py +++ /dev/null @@ -1,53 +0,0 @@ -"""Module for testing the :mod:`psyplot.utils` module.""" - -# SPDX-FileCopyrightText: 2016-2024 University of Lausanne -# SPDX-FileCopyrightText: 2020-2021 Helmholtz-Zentrum Geesthacht - -# SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH -# -# SPDX-License-Identifier: LGPL-3.0-only - -import unittest - -import psyplot.utils as utils - - -class TestTempBool(unittest.TestCase): - """Test the :class:`psyplot.utils._TempBool` class""" - - def test_descriptor(self): - """Test the descriptor functionality""" - - class Test(object): - test = utils._temp_bool_prop("test") - - t = Test() - - self.assertFalse(t.test) - with t.test: - self.assertTrue(t.test) - - t.test = True - self.assertTrue(t.test) - with t.test: - self.assertTrue(t.test) - - del t.test - self.assertFalse(t.test) - - -class TestUniqueEverSeen(unittest.TestCase): - """Test the :func:`psyplot.utils.unique_everseen` function""" - - def test_simple(self): - self.assertEqual( - list(utils.unique_everseen([1, 1, 2, 3, 4, 3])), [1, 2, 3, 4] - ) - - def test_key(self): - self.assertEqual( - list( - utils.unique_everseen([1, 1, 2, 3, 4, 3], key=lambda i: i % 3) - ), - [1, 2, 3], - ) diff --git a/todos.html b/todos.html new file mode 100644 index 0000000..cf1a846 --- /dev/null +++ b/todos.html @@ -0,0 +1,416 @@ + + + + + + + ToDos — psyplot documentation + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +
+

ToDos

+
+ + +
+
+ + + +
+
+
+
+ + + + \ No newline at end of file diff --git a/tox.ini b/tox.ini deleted file mode 100644 index 3be5bb3..0000000 --- a/tox.ini +++ /dev/null @@ -1,23 +0,0 @@ -; SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH -; -; SPDX-License-Identifier: CC0-1.0 - -[tox] - -[testenv] -extras = - testsite - -commands = - # mypy psyplot # disabled for now - isort --check psyplot - black --line-length 79 --check psyplot - flake8 psyplot - pytest -v --cov=psyplot -x - reuse lint - cffconvert --validate - -[pytest] -DJANGO_SETTINGS_MODULE = testproject.settings -python_files = tests.py test_*.py *_tests.py -norecursedirs = .* build dist *.egg venv docs